diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index dae954a0970b..92a51eb84a4a 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -16,7 +16,7 @@ The Magento 2 development team will review all issues and contributions submitte 4. PRs which include bug fixes must be accompanied with a step-by-step description of how to reproduce the bug. 3. PRs which include new logic or new features must be submitted along with: * Unit/integration test coverage -* Proposed [documentation](http://devdocs.magento.com) updates. Documentation contributions can be submitted via the [devdocs GitHub](https://github.com/magento/devdocs). +* Proposed [documentation](https://devdocs.magento.com) updates. Documentation contributions can be submitted via the [devdocs GitHub](https://github.com/magento/devdocs). 4. For larger features or changes, please [open an issue](https://github.com/magento/magento2/issues) to discuss the proposed changes prior to development. This may prevent duplicate or unnecessary effort and allow other contributors to provide input. 5. All automated tests must pass (all builds on [Travis CI](https://travis-ci.org/magento/magento2) must be green). @@ -27,7 +27,7 @@ If you are a new GitHub user, we recommend that you create your own [free github 1. Search current [listed issues](https://github.com/magento/magento2/issues) (open or closed) for similar proposals of intended contribution before starting work on a new contribution. 2. Review the [Contributor License Agreement](https://magento.com/legaldocuments/mca) if this is your first time contributing. 3. Create and test your work. -4. Fork the Magento 2 repository according to the [Fork A Repository instructions](http://devdocs.magento.com/guides/v2.2/contributor-guide/contributing.html#fork) and when you are ready to send us a pull request – follow the [Create A Pull Request instructions](http://devdocs.magento.com/guides/v2.2/contributor-guide/contributing.html#pull_request). +4. Fork the Magento 2 repository according to the [Fork A Repository instructions](https://devdocs.magento.com/guides/v2.2/contributor-guide/contributing.html#fork) and when you are ready to send us a pull request – follow the [Create A Pull Request instructions](https://devdocs.magento.com/guides/v2.2/contributor-guide/contributing.html#pull_request). 5. Once your contribution is received the Magento 2 development team will review the contribution and collaborate with you as needed. ## Code of Conduct diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 12ad4e452b1c..2b1720ccaaba 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -5,11 +5,12 @@ - Information on your environment, - Steps to reproduce, - Expected and actual results, + Fields marked with (*) are required. Please don't remove the template. Please also have a look at our guidelines article before adding a new issue https://github.com/magento/magento2/wiki/Issue-reporting-guidelines --> -### Preconditions +### Preconditions (*) 1. [Screenshots, logs or description] -### Actual result +### Actual result (*) 1. [Screenshots, logs or description] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000000..33a6ef02ace1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,34 @@ +--- +name: Bug report +about: Technical issue with the Magento 2 core components + +--- + + + +### Preconditions (*) + +1. +2. + +### Steps to reproduce (*) + +1. +2. + +### Expected result (*) + +1. [Screenshots, logs or description] +2. + +### Actual result (*) + +1. [Screenshots, logs or description] +2. diff --git a/.github/ISSUE_TEMPLATE/developer-experience-issue.md b/.github/ISSUE_TEMPLATE/developer-experience-issue.md new file mode 100644 index 000000000000..423d4818fb31 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/developer-experience-issue.md @@ -0,0 +1,19 @@ +--- +name: Developer experience issue +about: Issues related to customization, extensibility, modularity + +--- + + + +### Summary (*) + + +### Examples (*) + + +### Proposed solution + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000000..f64185773cab --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,21 @@ +--- +name: Feature request +about: Please consider reporting directly to https://github.com/magento/community-features + +--- + + + +### Description (*) + + +### Expected behavior (*) + + +### Benefits + + +### Additional information + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 5b0b9d74e453..f191bd9aaba6 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -3,12 +3,13 @@ To help us process this pull request we recommend that you add the following information: - Summary of the pull request, - Issue(s) related to the changes made, - - Manual testing scenarios, + - Manual testing scenarios + Fields marked with (*) are required. Please don't remove the template. --> -### Description +### Description (*) + + + + System + Notifications + magento-backend-system + + + Notifications + Notifications + magento-adminnotification-system-adminnotification + + diff --git a/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationNavigateMenuTest.xml b/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationNavigateMenuTest.xml new file mode 100644 index 000000000000..75dceb402862 --- /dev/null +++ b/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationNavigateMenuTest.xml @@ -0,0 +1,36 @@ + + + + + + + + + + <description value="Admin should be able to navigate to System > Notifications"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14125"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToSystemNotificationPage"> + <argument name="menuUiId" value="{{AdminMenuSystem.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSystemOtherSettingsNotifications.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSystemOtherSettingsNotifications.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/AdminNotification/etc/db_schema.xml b/app/code/Magento/AdminNotification/etc/db_schema.xml index 35e6045b607d..29d928ced208 100644 --- a/app/code/Magento/AdminNotification/etc/db_schema.xml +++ b/app/code/Magento/AdminNotification/etc/db_schema.xml @@ -21,16 +21,16 @@ default="0" comment="Flag if notification read"/> <column xsi:type="smallint" name="is_remove" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Flag if notification might be removed"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="notification_id"/> </constraint> - <index name="ADMINNOTIFICATION_INBOX_SEVERITY" indexType="btree"> + <index referenceId="ADMINNOTIFICATION_INBOX_SEVERITY" indexType="btree"> <column name="severity"/> </index> - <index name="ADMINNOTIFICATION_INBOX_IS_READ" indexType="btree"> + <index referenceId="ADMINNOTIFICATION_INBOX_IS_READ" indexType="btree"> <column name="is_read"/> </index> - <index name="ADMINNOTIFICATION_INBOX_IS_REMOVE" indexType="btree"> + <index referenceId="ADMINNOTIFICATION_INBOX_IS_REMOVE" indexType="btree"> <column name="is_remove"/> </index> </table> @@ -40,7 +40,7 @@ default="0" comment="Problem type"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Create date"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="identity"/> </constraint> </table> diff --git a/app/code/Magento/AdminNotification/view/adminhtml/layout/adminhtml_notification_block.xml b/app/code/Magento/AdminNotification/view/adminhtml/layout/adminhtml_notification_block.xml index c68313211c2e..06fd380cb2a4 100644 --- a/app/code/Magento/AdminNotification/view/adminhtml/layout/adminhtml_notification_block.xml +++ b/app/code/Magento/AdminNotification/view/adminhtml/layout/adminhtml_notification_block.xml @@ -11,7 +11,7 @@ <block class="Magento\Backend\Block\Widget\Grid" name="adminhtml.notification.container.grid" as="grid"> <arguments> <argument name="id" xsi:type="string">notificationGrid</argument> - <argument name="dataSource" xsi:type="object">Magento\AdminNotification\Model\ResourceModel\Grid\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\AdminNotification\Model\ResourceModel\Grid\Collection</argument> <argument name="default_dir" xsi:type="string">DESC</argument> <argument name="default_sort" xsi:type="string">date_added</argument> <argument name="save_parameters_in_session" xsi:type="string">1</argument> diff --git a/app/code/Magento/AdminNotification/view/adminhtml/layout/default.xml b/app/code/Magento/AdminNotification/view/adminhtml/layout/default.xml index aa8ba23d0ee5..eed6b53f3431 100644 --- a/app/code/Magento/AdminNotification/view/adminhtml/layout/default.xml +++ b/app/code/Magento/AdminNotification/view/adminhtml/layout/default.xml @@ -8,7 +8,7 @@ <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceContainer name="notifications"> - <uiComponent name="notification_area"/> + <uiComponent name="notification_area" aclResource="Magento_AdminNotification::show_list"/> <block class="Magento\AdminNotification\Block\System\Messages\UnreadMessagePopup" name="unread_system_messages" as="unread_system_messages" diff --git a/app/code/Magento/AdvancedPricingImportExport/Controller/Adminhtml/Export/GetFilter.php b/app/code/Magento/AdvancedPricingImportExport/Controller/Adminhtml/Export/GetFilter.php index 818bcda1da65..d78266ab7531 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Controller/Adminhtml/Export/GetFilter.php +++ b/app/code/Magento/AdvancedPricingImportExport/Controller/Adminhtml/Export/GetFilter.php @@ -5,12 +5,14 @@ */ namespace Magento\AdvancedPricingImportExport\Controller\Adminhtml\Export; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\ImportExport\Controller\Adminhtml\Export as ExportController; use Magento\Framework\Controller\ResultFactory; use Magento\AdvancedPricingImportExport\Model\Export\AdvancedPricing as ExportAdvancedPricing; use Magento\Catalog\Model\Product as CatalogProduct; -class GetFilter extends ExportController +class GetFilter extends ExportController implements HttpGetActionInterface, HttpPostActionInterface { /** * Get grid-filter of entity attributes action. diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php index 2e17e734b1e6..591e648547d6 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php +++ b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php @@ -185,6 +185,7 @@ class AdvancedPricing extends \Magento\ImportExport\Model\Import\Entity\Abstract * @param AdvancedPricing\Validator\Website $websiteValidator * @param AdvancedPricing\Validator\TierPrice $tierPriceValidator * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @throws \Exception */ public function __construct( \Magento\Framework\Json\Helper\Data $jsonHelper, @@ -255,6 +256,7 @@ public function getEntityTypeCode() * @param array $rowData * @param int $rowNum * @return bool + * @throws \Zend_Validate_Exception */ public function validateRow(array $rowData, $rowNum) { @@ -308,6 +310,7 @@ protected function _importData() * Save advanced pricing * * @return $this + * @throws \Exception */ public function saveAdvancedPricing() { @@ -319,6 +322,7 @@ public function saveAdvancedPricing() * Deletes Advanced price data from raw data. * * @return $this + * @throws \Exception */ public function deleteAdvancedPricing() { @@ -347,6 +351,7 @@ public function deleteAdvancedPricing() * Replace advanced pricing * * @return $this + * @throws \Exception */ public function replaceAdvancedPricing() { @@ -360,6 +365,7 @@ public function replaceAdvancedPricing() * @return $this * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * @throws \Exception */ protected function saveAndReplaceAdvancedPrices() { @@ -368,8 +374,8 @@ protected function saveAndReplaceAdvancedPrices() $this->_cachedSkuToDelete = null; } $listSku = []; + $tierPrices = []; while ($bunch = $this->_dataSourceModel->getNextBunch()) { - $tierPrices = []; foreach ($bunch as $rowNum => $rowData) { if (!$this->validateRow($rowData, $rowNum)) { $this->addRowError(ValidatorInterface::ERROR_SKU_IS_EMPTY, $rowNum); @@ -397,23 +403,28 @@ protected function saveAndReplaceAdvancedPrices() ]; } } - if (\Magento\ImportExport\Model\Import::BEHAVIOR_REPLACE == $behavior) { - if ($listSku) { - $this->processCountNewPrices($tierPrices); - if ($this->deleteProductTierPrices(array_unique($listSku), self::TABLE_TIER_PRICE)) { - $this->saveProductPrices($tierPrices, self::TABLE_TIER_PRICE); - $this->setUpdatedAt($listSku); - } - } - } elseif (\Magento\ImportExport\Model\Import::BEHAVIOR_APPEND == $behavior) { + + if (\Magento\ImportExport\Model\Import::BEHAVIOR_APPEND == $behavior) { $this->processCountExistingPrices($tierPrices, self::TABLE_TIER_PRICE) ->processCountNewPrices($tierPrices); + $this->saveProductPrices($tierPrices, self::TABLE_TIER_PRICE); if ($listSku) { $this->setUpdatedAt($listSku); } } } + + if (\Magento\ImportExport\Model\Import::BEHAVIOR_REPLACE == $behavior) { + if ($listSku) { + $this->processCountNewPrices($tierPrices); + if ($this->deleteProductTierPrices(array_unique($listSku), self::TABLE_TIER_PRICE)) { + $this->saveProductPrices($tierPrices, self::TABLE_TIER_PRICE); + $this->setUpdatedAt($listSku); + } + } + } + return $this; } @@ -423,6 +434,7 @@ protected function saveAndReplaceAdvancedPrices() * @param array $priceData * @param string $table * @return $this + * @throws \Exception */ protected function saveProductPrices(array $priceData, $table) { @@ -454,6 +466,7 @@ protected function saveProductPrices(array $priceData, $table) * @param array $listSku * @param string $table * @return boolean + * @throws \Exception */ protected function deleteProductTierPrices(array $listSku, $table) { @@ -531,6 +544,7 @@ protected function getCustomerGroupId($customerGroup) * Retrieve product skus * * @return array + * @throws \Exception */ protected function retrieveOldSkus() { @@ -551,6 +565,7 @@ protected function retrieveOldSkus() * @param array $prices * @param string $table * @return $this + * @throws \Exception */ protected function processCountExistingPrices($prices, $table) { @@ -562,11 +577,14 @@ protected function processCountExistingPrices($prices, $table) $tableName = $this->_resourceFactory->create()->getTable($table); $productEntityLinkField = $this->getProductEntityLinkField(); - $existingPrices = $this->_connection->fetchAssoc( + $existingPrices = $this->_connection->fetchAll( $this->_connection->select()->from( $tableName, - ['value_id', $productEntityLinkField, 'all_groups', 'customer_group_id'] - )->where($productEntityLinkField . ' IN (?)', $existProductIds) + [$productEntityLinkField, 'all_groups', 'customer_group_id', 'qty'] + )->where( + $productEntityLinkField . ' IN (?)', + $existProductIds + ) ); foreach ($existingPrices as $existingPrice) { foreach ($prices as $sku => $skuPrices) { @@ -591,8 +609,10 @@ protected function incrementCounterUpdated($prices, $existingPrice) foreach ($prices as $price) { if ($existingPrice['all_groups'] == $price['all_groups'] && $existingPrice['customer_group_id'] == $price['customer_group_id'] + && (int) $existingPrice['qty'] === (int) $price['qty'] ) { $this->countItemsUpdated++; + continue; } } } diff --git a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/WebsiteTest.php b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/WebsiteTest.php index b46e286e7500..d78c4f5e61af 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/WebsiteTest.php +++ b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/WebsiteTest.php @@ -103,13 +103,13 @@ public function testGetAllWebsitesValue() $this->webSiteModel->expects($this->once())->method('getBaseCurrency')->willReturn($currency); $expectedResult = AdvancedPricing::VALUE_ALL_WEBSITES . ' [' . $currencyCode . ']'; - $this->websiteString = $this->getMockBuilder( + $websiteString = $this->getMockBuilder( \Magento\AdvancedPricingImportExport\Model\Import\AdvancedPricing\Validator\Website::class ) ->setMethods(['_clearMessages', '_addMessages']) ->setConstructorArgs([$this->storeResolver, $this->webSiteModel]) ->getMock(); - $result = $this->websiteString->getAllWebsitesValue(); + $result = $websiteString->getAllWebsitesValue(); $this->assertEquals($expectedResult, $result); } 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 340e81746f02..2aa59c1cfb75 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/Model/Recommendations/DataProvider.php b/app/code/Magento/AdvancedSearch/Model/Recommendations/DataProvider.php index 546983bb5e5a..c0c224766eb3 100644 --- a/app/code/Magento/AdvancedSearch/Model/Recommendations/DataProvider.php +++ b/app/code/Magento/AdvancedSearch/Model/Recommendations/DataProvider.php @@ -10,6 +10,9 @@ use Magento\Search\Model\QueryInterface; use Magento\AdvancedSearch\Model\SuggestedQueriesInterface; +/** + * Class DataProvider + */ class DataProvider implements SuggestedQueriesInterface { /** @@ -51,6 +54,8 @@ class DataProvider implements SuggestedQueriesInterface private $recommendationsFactory; /** + * DataProvider constructor. + * * @param ScopeConfigInterface $scopeConfig * @param \Magento\Catalog\Model\Layer\Resolver $layerResolver * @param \Magento\AdvancedSearch\Model\ResourceModel\RecommendationsFactory $recommendationsFactory @@ -69,18 +74,20 @@ public function __construct( } /** + * Is Results Count Enabled + * * @return bool */ public function isResultsCountEnabled() { - return (bool)$this->scopeConfig->getValue( + return $this->scopeConfig->isSetFlag( self::CONFIG_RESULTS_COUNT_ENABLED, ScopeInterface::SCOPE_STORE ); } /** - * {@inheritdoc} + * @inheritdoc */ public function getItems(QueryInterface $query) { @@ -102,6 +109,8 @@ public function getItems(QueryInterface $query) } /** + * Return Search Recommendations + * * @param QueryInterface $query * @return array */ @@ -126,17 +135,21 @@ private function getSearchRecommendations(\Magento\Search\Model\QueryInterface $ } /** + * Is Search Recommendations Enabled + * * @return bool */ private function isSearchRecommendationsEnabled() { - return (bool)$this->scopeConfig->getValue( + return $this->scopeConfig->isSetFlag( self::CONFIG_IS_ENABLED, ScopeInterface::SCOPE_STORE ); } /** + * Return Search Recommendations Count + * * @return int */ private function getSearchRecommendationsCount() diff --git a/app/code/Magento/AdvancedSearch/etc/adminhtml/system.xml b/app/code/Magento/AdvancedSearch/etc/adminhtml/system.xml index fa7774f5cec1..2c4f7fca1834 100644 --- a/app/code/Magento/AdvancedSearch/etc/adminhtml/system.xml +++ b/app/code/Magento/AdvancedSearch/etc/adminhtml/system.xml @@ -48,18 +48,18 @@ </depends> </field> <!--<group id="suggestions">--> - <field id="search_suggestion_enabled" translate="label comment" type="select" sortOrder="70" showInDefault="1" showInWebsite="1" showInStore="1"> + <field id="search_suggestion_enabled" translate="label comment" type="select" sortOrder="90" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Enable Search Suggestions</label> <comment>When you enable this option your site may slow down.</comment> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> - <field id="search_suggestion_count" translate="label" type="text" sortOrder="71" showInDefault="1" showInWebsite="1" showInStore="1"> + <field id="search_suggestion_count" translate="label" type="text" sortOrder="91" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Search Suggestions Count</label> <depends> <field id="search_suggestion_enabled">1</field> </depends> </field> - <field id="search_suggestion_count_results_enabled" translate="label" type="select" sortOrder="72" showInDefault="1" showInWebsite="1" showInStore="1"> + <field id="search_suggestion_count_results_enabled" translate="label" type="select" sortOrder="92" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Show Results Count for Each Suggestion</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <comment>When you enable this option your site may slow down.</comment> diff --git a/app/code/Magento/AdvancedSearch/etc/db_schema.xml b/app/code/Magento/AdvancedSearch/etc/db_schema.xml index 9fae40411098..2dd8c68e2d5f 100644 --- a/app/code/Magento/AdvancedSearch/etc/db_schema.xml +++ b/app/code/Magento/AdvancedSearch/etc/db_schema.xml @@ -14,13 +14,13 @@ default="0" comment="Query Id"/> <column xsi:type="int" name="relation_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Relation Id"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> </constraint> - <constraint xsi:type="foreign" name="CATALOGSEARCH_RECOMMENDATIONS_QUERY_ID_SEARCH_QUERY_QUERY_ID" + <constraint xsi:type="foreign" referenceId="CATALOGSEARCH_RECOMMENDATIONS_QUERY_ID_SEARCH_QUERY_QUERY_ID" table="catalogsearch_recommendations" column="query_id" referenceTable="search_query" referenceColumn="query_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CATALOGSEARCH_RECOMMENDATIONS_RELATION_ID_SEARCH_QUERY_QUERY_ID" + <constraint xsi:type="foreign" referenceId="CATALOGSEARCH_RECOMMENDATIONS_RELATION_ID_SEARCH_QUERY_QUERY_ID" table="catalogsearch_recommendations" column="relation_id" referenceTable="search_query" referenceColumn="query_id" onDelete="CASCADE"/> </table> diff --git a/app/code/Magento/AdvancedSearch/view/adminhtml/layout/catalog_search_block.xml b/app/code/Magento/AdvancedSearch/view/adminhtml/layout/catalog_search_block.xml index b6ef596281e5..f3544863348e 100644 --- a/app/code/Magento/AdvancedSearch/view/adminhtml/layout/catalog_search_block.xml +++ b/app/code/Magento/AdvancedSearch/view/adminhtml/layout/catalog_search_block.xml @@ -11,7 +11,7 @@ <block class="Magento\AdvancedSearch\Block\Adminhtml\Search\Grid" name="search.edit.grid" as="grid"> <arguments> <argument name="id" xsi:type="string">catalog_search_grid</argument> - <argument name="dataSource" xsi:type="object">Magento\AdvancedSearch\Model\ResourceModel\Search\Grid\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\AdvancedSearch\Model\ResourceModel\Search\Grid\Collection</argument> <argument name="default_sort" xsi:type="string">name</argument> <argument name="default_dir" xsi:type="string">ASC</argument> <argument name="save_parameters_in_session" xsi:type="string">1</argument> diff --git a/app/code/Magento/Analytics/Controller/Adminhtml/BIEssentials/SignUp.php b/app/code/Magento/Analytics/Controller/Adminhtml/BIEssentials/SignUp.php index ff9126a83d59..87666cb880e5 100644 --- a/app/code/Magento/Analytics/Controller/Adminhtml/BIEssentials/SignUp.php +++ b/app/code/Magento/Analytics/Controller/Adminhtml/BIEssentials/SignUp.php @@ -5,6 +5,7 @@ */ namespace Magento\Analytics\Controller\Adminhtml\BIEssentials; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Backend\App\Action; use Magento\Backend\App\Action\Context; use Magento\Framework\App\Config\ScopeConfigInterface; @@ -12,7 +13,7 @@ /** * Provides link to BI Essentials signup */ -class SignUp extends Action +class SignUp extends Action implements HttpGetActionInterface { /** * Path to config value with URL to BI Essentials sign-up page. diff --git a/app/code/Magento/Analytics/Controller/Adminhtml/Reports/Show.php b/app/code/Magento/Analytics/Controller/Adminhtml/Reports/Show.php index cec09377770b..9068654fa944 100644 --- a/app/code/Magento/Analytics/Controller/Adminhtml/Reports/Show.php +++ b/app/code/Magento/Analytics/Controller/Adminhtml/Reports/Show.php @@ -5,6 +5,7 @@ */ namespace Magento\Analytics\Controller\Adminhtml\Reports; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Analytics\Model\Exception\State\SubscriptionUpdateException; use Magento\Analytics\Model\ReportUrlProvider; use Magento\Backend\App\Action; @@ -16,7 +17,7 @@ /** * Provide redirect to resource with reports. */ -class Show extends Action +class Show extends Action implements HttpGetActionInterface { /** * @var ReportUrlProvider diff --git a/app/code/Magento/Analytics/Model/Cryptographer.php b/app/code/Magento/Analytics/Model/Cryptographer.php index 665d564814b1..efddcb501aab 100644 --- a/app/code/Magento/Analytics/Model/Cryptographer.php +++ b/app/code/Magento/Analytics/Model/Cryptographer.php @@ -129,7 +129,12 @@ private function getInitializationVector() */ private function validateCipherMethod($cipherMethod) { - $methods = openssl_get_cipher_methods(); + $methods = array_map( + 'strtolower', + openssl_get_cipher_methods() + ); + $cipherMethod = strtolower($cipherMethod); + return (false !== array_search($cipherMethod, $methods)); } } diff --git a/app/code/Magento/Analytics/README.md b/app/code/Magento/Analytics/README.md index 7ec64abcd9b8..f600f1d406e4 100644 --- a/app/code/Magento/Analytics/README.md +++ b/app/code/Magento/Analytics/README.md @@ -1,6 +1,6 @@ # Magento_Analytics Module -The Magento_Analytics module integrates your Magento instance with the [Magento Business Intelligence (MBI)](https://magento.com/products/business-intelligence) to use [Advanced Reporting](http://devdocs.magento.com/guides/v2.2/advanced-reporting/modules.html) functionality. +The Magento_Analytics module integrates your Magento instance with the [Magento Business Intelligence (MBI)](https://magento.com/products/business-intelligence) to use [Advanced Reporting](https://devdocs.magento.com/guides/v2.2/advanced-reporting/modules.html) functionality. The module implements the following functionality: @@ -16,8 +16,8 @@ The module implements the following functionality: ## Structure -Beyond the [usual module file structure](http://devdocs.magento.com/guides/v2.2/architecture/archi_perspectives/components/modules/mod_intro.html) the module contains a directory `ReportXml`. -[Report XML](http://devdocs.magento.com/guides/v2.2/advanced-reporting/report-xml.html) is a markup language used to build reports for Advanced Reporting. +Beyond the [usual module file structure](https://devdocs.magento.com/guides/v2.2/architecture/archi_perspectives/components/modules/mod_intro.html) the module contains a directory `ReportXml`. +[Report XML](https://devdocs.magento.com/guides/v2.2/advanced-reporting/report-xml.html) is a markup language used to build reports for Advanced Reporting. The language declares SQL queries using XML declaration. ## Subscription Process diff --git a/app/code/Magento/Analytics/ReportXml/DB/ColumnsResolver.php b/app/code/Magento/Analytics/ReportXml/DB/ColumnsResolver.php index 14b80c6814ba..3af168886a44 100644 --- a/app/code/Magento/Analytics/ReportXml/DB/ColumnsResolver.php +++ b/app/code/Magento/Analytics/ReportXml/DB/ColumnsResolver.php @@ -76,7 +76,7 @@ public function getColumns(SelectBuilder $selectBuilder, $entityConfig) $columnName = $this->nameResolver->getName($attributeData); if (isset($attributeData['function'])) { $prefix = ''; - if (isset($attributeData['distinct']) && $attributeData['distinct'] == true) { + if (!empty($attributeData['distinct'])) { $prefix = ' DISTINCT '; } $expression = new ColumnValueExpression( diff --git a/app/code/Magento/Analytics/ReportXml/DB/SelectBuilder.php b/app/code/Magento/Analytics/ReportXml/DB/SelectBuilder.php index 81ee9b15781d..b4b7adebf745 100644 --- a/app/code/Magento/Analytics/ReportXml/DB/SelectBuilder.php +++ b/app/code/Magento/Analytics/ReportXml/DB/SelectBuilder.php @@ -11,6 +11,7 @@ /** * Responsible for Select object creation, works as a builder. Returns Select as result; + * * Used in SQL assemblers. */ class SelectBuilder @@ -85,11 +86,13 @@ public function getJoins() * Set joins conditions * * @param array $joins - * @return void + * @return $this */ public function setJoins($joins) { $this->joins = $joins; + + return $this; } /** @@ -106,11 +109,13 @@ public function getConnectionName() * Set connection name * * @param string $connectionName - * @return void + * @return $this */ public function setConnectionName($connectionName) { $this->connectionName = $connectionName; + + return $this; } /** @@ -127,11 +132,13 @@ public function getColumns() * Set columns * * @param array $columns - * @return void + * @return $this */ public function setColumns($columns) { $this->columns = $columns; + + return $this; } /** @@ -148,11 +155,13 @@ public function getFilters() * Set filters * * @param array $filters - * @return void + * @return $this */ public function setFilters($filters) { $this->filters = $filters; + + return $this; } /** @@ -169,11 +178,13 @@ public function getFrom() * Set from condition * * @param array $from - * @return void + * @return $this */ public function setFrom($from) { $this->from = $from; + + return $this; } /** @@ -236,11 +247,13 @@ public function getGroup() * Set group * * @param array $group - * @return void + * @return $this */ public function setGroup($group) { $this->group = $group; + + return $this; } /** @@ -257,11 +270,13 @@ public function getParams() * Set parameters * * @param array $params - * @return void + * @return $this */ public function setParams($params) { $this->params = $params; + + return $this; } /** @@ -278,10 +293,12 @@ public function getHaving() * Set having condition * * @param array $having - * @return void + * @return $this */ public function setHaving($having) { $this->having = $having; + + return $this; } } diff --git a/app/code/Magento/Analytics/ReportXml/ReportProvider.php b/app/code/Magento/Analytics/ReportXml/ReportProvider.php index 60e722930c24..8966d018dc6b 100644 --- a/app/code/Magento/Analytics/ReportXml/ReportProvider.php +++ b/app/code/Magento/Analytics/ReportXml/ReportProvider.php @@ -55,7 +55,7 @@ public function __construct( private function getIteratorName(Query $query) { $config = $query->getConfig(); - return isset($config['iterator']) ? $config['iterator'] : null; + return $config['iterator'] ?? null; } /** diff --git a/app/code/Magento/Analytics/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Analytics/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 000000000000..01b86101def2 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminMenuReports"> + <data key="pageTitle">Reports</data> + <data key="title">Reports</data> + <data key="dataUiId">magento-reports-report</data> + </entity> + <entity name="AdminMenuReportsBusinessIntelligenceAdvancedReporting"> + <data key="pageTitle">AdvancedReporting</data> + <data key="title">AdvancedReporting</data> + <data key="dataUiId">magento-analytics-advanced-reporting</data> + </entity> +</entities> diff --git a/app/code/Magento/Analytics/Test/Mftf/Data/UserData.xml b/app/code/Magento/Analytics/Test/Mftf/Data/UserData.xml index f6e5b955816e..83f27def4b4e 100644 --- a/app/code/Magento/Analytics/Test/Mftf/Data/UserData.xml +++ b/app/code/Magento/Analytics/Test/Mftf/Data/UserData.xml @@ -7,7 +7,7 @@ --> <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> <entity name="adminNoReport" type="user"> <data key="username" unique="suffix">noreport</data> <data key="firstname">No</data> @@ -18,7 +18,7 @@ <data key="interface_local">en_US</data> <data key="is_active">true</data> <data key="current_password">123123q</data> -</entity> + </entity> <entity name="restrictedWebUser" type="user"> <data key="username" unique="suffix">restrictedWebUser</data> <data key="firstname">restricted</data> diff --git a/app/code/Magento/Analytics/Test/Mftf/Page/AdminConfigPage.xml b/app/code/Magento/Analytics/Test/Mftf/Page/AdminConfigPage.xml new file mode 100644 index 000000000000..c4ced12e67e0 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Mftf/Page/AdminConfigPage.xml @@ -0,0 +1,12 @@ +<?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="AdminConfigGeneralAnalyticsPage" url="admin/system_config/edit/section/analytics/" area="admin" module="Magento_Config"> + <section name="AdminConfigAdvancedReportingSection"/> + </page> +</pages> 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 000000000000..7b6851e5bdf3 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Mftf/Section/AdminAdvancedReportingSection.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminAdvancedReportingSection"> + <element name="goToAdvancedReporting" type="text" selector="//div[@class='dashboard-advanced-reports-actions']/a[@title='Go to Advanced Reporting']" timeout="30"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Analytics/Test/Mftf/Section/AdminConfigAdvancedReportingSection.xml b/app/code/Magento/Analytics/Test/Mftf/Section/AdminConfigAdvancedReportingSection.xml new file mode 100644 index 000000000000..2e5f2b762a7b --- /dev/null +++ b/app/code/Magento/Analytics/Test/Mftf/Section/AdminConfigAdvancedReportingSection.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminConfigAdvancedReportingSection"> + <element name="advancedReportingMenuItem" type="text" selector="//*[@class='admin__page-nav-link item-nav']//span[text()='Advanced Reporting']"/> + <element name="advancedReportingService" type="select" selector="#analytics_general_enabled"/> + <element name="advancedReportingServiceLabel" type="text" selector="#row_analytics_general_enabled>td.label>label>span"/> + <element name="advancedReportingServiceStatus" type="text" selector="#row_analytics_general_enabled>td.value>p>span"/> + <element name="advancedReportingIndustry" type="select" selector="#analytics_general_vertical"/> + <element name="advancedReportingIndustryLabel" type="text" selector=".config-vertical-label>label>span"/> + <element name="advancedReportingHour" type="select" selector="#row_analytics_general_collection_time>td:nth-child(2)>select:nth-child(2)"/> + <element name="advancedReportingMinute" type="select" selector="#row_analytics_general_collection_time>td:nth-child(2)>select:nth-child(3)"/> + <element name="advancedReportingSeconds" type="select" selector="#row_analytics_general_collection_time>td:nth-child(2)>select:nth-child(4)"/> + <element name="advancedReportingBlankIndustryError" type="text" selector=".message-error>div"/> + </section> +</sections> 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 000000000000..e660a2eb8d42 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingButtonTest.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminAdvancedReportingButtonTest"> + <annotations> + <stories value="AdvancedReporting"/> + <title value="AdvancedReportingButtonTest"/> + <description value="Test log in to AdvancedReporting and tests AdvancedReportingButtonTest"/> + <testCaseId value="MC-14800"/> + <skip> + <issueId value="MC-14800" /> + </skip> + <severity value="CRITICAL"/> + <group value="analytics"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Navigate through Advanced Reporting button on dashboard to Sign Up page--> + <amOnPage url="{{AdminDashboardPage.url}}" stepKey="amOnDashboardPage"/> + <waitForPageLoad stepKey="waitForDashboardPageLoad"/> + <click selector="{{AdminAdvancedReportingSection.goToAdvancedReporting}}" stepKey="clickGoToAdvancedReporting"/> + <switchToNextTab stepKey="switchToNewTab"/> + <seeInCurrentUrl url="advancedreporting.rjmetrics.com/report" stepKey="seeAssertAdvancedReportingPageUrl"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingNavigateMenuTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingNavigateMenuTest.xml new file mode 100644 index 000000000000..67d671528569 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingNavigateMenuTest.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminAdvancedReportingNavigateMenuTest"> + <annotations> + <features value="Analytics"/> + <stories value="Menu Navigation"/> + <title value="Admin advanced reporting navigate menu test"/> + <description value="Admin should be able to navigate through advanced reporting admin menu to BI reports page"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14152"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateAdvancedReportingPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsBusinessIntelligenceAdvancedReporting.dataUiId}}"/> + </actionGroup> + <switchToNextTab stepKey="switchToNewTab"/> + <seeInCurrentUrl url="advancedreporting.rjmetrics.com/report" stepKey="seeAssertAdvancedReportingPageUrl"/> + </test> +</tests> diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationBlankIndustryTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationBlankIndustryTest.xml index ff89ca9b663e..914cb59b64e4 100644 --- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationBlankIndustryTest.xml +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationBlankIndustryTest.xml @@ -18,15 +18,14 @@ <group value="analytics"/> </annotations> <after> - <amOnPage stepKey="amOnLogoutPage" url="admin/admin/auth/logout/"/> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> </after> <actionGroup ref="LoginActionGroup" stepKey="loginAsAdmin"/> - <amOnPage stepKey="amOnAdminConfig" url="{{AdminConfigPage.url}}"/> - <waitForPageLoad stepKey="waitForAdminConfig"/> - <click stepKey="clickAdvancedReportingConfigMenu" selector="{{AdminConfigSection.advancedReportingMenuItem}}"/> - <see stepKey="seeAdvancedReportingIndustryLabel" selector="{{AdminConfigSection.advancedReportingIndustryLabel}}" userInput="Industry"/> - <selectOption stepKey="selectAdvancedReportingIndustry" selector="{{AdminConfigSection.advancedReportingIndustry}}" userInput="--Please Select--"/> - <click stepKey="clickSaveConfigButton" selector="{{AdminConfigSection.saveButton}}"/> - <see stepKey="seeBlankIndustryErrorMessage" selector="{{AdminConfigSection.advancedReportingBlankIndustryError}}" userInput="Please select an industry."/> + <amOnPage url="{{AdminConfigGeneralAnalyticsPage.url}}" stepKey="amOnAdminConfig"/> + <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingService}}" userInput="Enable" stepKey="selectAdvancedReportingServiceEnabled"/> + <see selector="{{AdminConfigAdvancedReportingSection.advancedReportingIndustryLabel}}" userInput="Industry" stepKey="seeAdvancedReportingIndustryLabel"/> + <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingIndustry}}" userInput="--Please Select--" stepKey="selectAdvancedReportingIndustry"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveConfigButton"/> + <see selector="{{AdminConfigAdvancedReportingSection.advancedReportingBlankIndustryError}}" userInput="Please select an industry." stepKey="seeBlankIndustryErrorMessage"/> </test> </tests> diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationEnableDisableAnalyticsTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationEnableDisableAnalyticsTest.xml index d9617209dcdf..1c1a3b27b06a 100644 --- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationEnableDisableAnalyticsTest.xml +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationEnableDisableAnalyticsTest.xml @@ -16,32 +16,26 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-66465"/> <group value="analytics"/> - <skip> - <issueId value="MAGETWO-90659"/> - </skip> </annotations> <after> - <amOnPage stepKey="amOnLogoutPage" url="admin/admin/auth/logout/"/> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> </after> <actionGroup ref="LoginActionGroup" stepKey="loginAsAdmin"/> - <!--Goto admin stores configure page --> - <amOnPage stepKey="amOnAdminConfig" url="{{AdminConfigPage.url}}"/> - <!--Enable Advanced Reporting--> - <click stepKey="clickAdvancedReportingConfigMenu" selector="{{AdminConfigSection.advancedReportingMenuItem}}"/> - <see stepKey="seeAdvancedReportingServiceLabelEnabled" selector="{{AdminConfigSection.advancedReportingServiceLabel}}" userInput="Advanced Reporting Service"/> - <selectOption stepKey="selectAdvancedReportingServiceEnabled" selector="{{AdminConfigSection.advancedReportingService}}" userInput="Enable"/> - <see stepKey="seeAdvancedReportingIndustryLabel" selector="{{AdminConfigSection.advancedReportingIndustryLabel}}" userInput="Industry"/> - <selectOption stepKey="selectAdvancedReportingIndustry" selector="{{AdminConfigSection.advancedReportingIndustry}}" userInput="Apps and Games"/> - <click stepKey="clickSaveConfigButtonEnabled" selector="{{AdminConfigSection.saveButton}}"/> - <see stepKey="seeSaveConfigurationMessageEnabled" selector="{{AdminConfigSection.advancedReportingSuccessMessage}}" userInput="You saved the configuration."/> - <see stepKey="seeAdvancedReportingServiceEnabled" selector="{{AdminConfigSection.advancedReportingService}}" userInput="Enable"/> - <see stepKey="seeAdvancedReportingServiceStatusEnabled" selector="{{AdminConfigSection.advancedReportingServiceStatus}}" userInput="Subscription status: Pending"/> + <amOnPage url="{{AdminConfigGeneralAnalyticsPage.url}}" stepKey="amOnAdminConfig"/> + <see selector="{{AdminConfigAdvancedReportingSection.advancedReportingServiceLabel}}" userInput="Advanced Reporting Service" stepKey="seeAdvancedReportingServiceLabelEnabled"/> + <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingService}}" userInput="Enable" stepKey="selectAdvancedReportingServiceEnabled"/> + <see selector="{{AdminConfigAdvancedReportingSection.advancedReportingIndustryLabel}}" userInput="Industry" stepKey="seeAdvancedReportingIndustryLabel"/> + <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingIndustry}}" userInput="Apps and Games" stepKey="selectAdvancedReportingIndustry"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveConfigButton1"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the configuration." stepKey="seeSuccess"/> + <see selector="{{AdminConfigAdvancedReportingSection.advancedReportingService}}" userInput="Enable" stepKey="seeAdvancedReportingServiceEnabled"/> + <see selector="{{AdminConfigAdvancedReportingSection.advancedReportingServiceStatus}}" userInput="Subscription status: Pending" stepKey="seeAdvancedReportingServiceStatusEnabled"/> <!--Disable Advanced Reporting--> - <see stepKey="seeAdvancedReportingServiceLabelDisabled" selector="{{AdminConfigSection.advancedReportingServiceLabel}}" userInput="Advanced Reporting Service"/> - <selectOption stepKey="selectAdvancedReportingServiceDisabled" selector="{{AdminConfigSection.advancedReportingService}}" userInput="Disable"/> - <click stepKey="clickSaveConfigButtonDisabled" selector="{{AdminConfigSection.saveButton}}"/> - <see stepKey="seeSaveConfigurationMessageDisabled" selector="{{AdminConfigSection.advancedReportingSuccessMessage}}" userInput="You saved the configuration."/> - <see stepKey="seeAdvancedReportingServiceDisabled" selector="{{AdminConfigSection.advancedReportingService}}" userInput="Disable"/> - <see stepKey="seeAdvancedReportingServiceStatusDisabled" selector="{{AdminConfigSection.advancedReportingServiceStatus}}" userInput="Subscription status: Disabled"/> + <see selector="{{AdminConfigAdvancedReportingSection.advancedReportingServiceLabel}}" userInput="Advanced Reporting Service" stepKey="seeAdvancedReportingServiceLabelDisabled"/> + <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingService}}" userInput="Disable" stepKey="selectAdvancedReportingServiceDisabled"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveConfigButton2"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the configuration." stepKey="seeSuccess2"/> + <see selector="{{AdminConfigAdvancedReportingSection.advancedReportingService}}" userInput="Disable" stepKey="seeAdvancedReportingServiceDisabled"/> + <see selector="{{AdminConfigAdvancedReportingSection.advancedReportingServiceStatus}}" userInput="Subscription status: Disabled" stepKey="seeAdvancedReportingServiceStatusDisabled"/> </test> </tests> diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationIndustryTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationIndustryTest.xml index 2d5594a43b1a..bb682c446801 100644 --- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationIndustryTest.xml +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationIndustryTest.xml @@ -17,18 +17,14 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-63898"/> <group value="analytics"/> - <skip> - <issueId value="MAGETWO-90659"/> - </skip> </annotations> <actionGroup ref="LoginActionGroup" stepKey="loginAsAdmin"/> - - <amOnPage stepKey="amOnAdminConfig" url="{{AdminConfigPage.url}}"/> - <click stepKey="clickAdvancedReportingConfigMenu" selector="{{AdminConfigSection.advancedReportingMenuItem}}"/> - <see stepKey="seeAdvancedReportingIndustryLabel" selector="{{AdminConfigSection.advancedReportingIndustryLabel}}" userInput="Industry"/> - <selectOption stepKey="selectAdvancedReportingIndustry" selector="{{AdminConfigSection.advancedReportingIndustry}}" userInput="Apps and Games"/> - <click stepKey="clickSaveConfigButton" selector="{{AdminConfigSection.saveButton}}"/> - <see stepKey="seeIndustrySuccessMessage" selector="{{AdminConfigSection.advancedReportingSuccessMessage}}" userInput="You saved the configuration."/> + <amOnPage url="{{AdminConfigGeneralAnalyticsPage.url}}" stepKey="amOnAdminConfig"/> + <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingService}}" userInput="Enable" stepKey="selectAdvancedReportingServiceEnabled"/> + <see selector="{{AdminConfigAdvancedReportingSection.advancedReportingIndustryLabel}}" userInput="Industry" stepKey="seeAdvancedReportingIndustryLabel"/> + <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingIndustry}}" userInput="Apps and Games" stepKey="selectAdvancedReportingIndustry"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveConfigButton"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the configuration." stepKey="seeSuccess"/> </test> </tests> diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationPermissionTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationPermissionTest.xml index d8aed1250d82..58e809ec45c4 100644 --- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationPermissionTest.xml +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationPermissionTest.xml @@ -17,49 +17,49 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-82648"/> <group value="analytics"/> - <skip> - <issueId value="MAGETWO-90659"/> - </skip> </annotations> <before> - <createData stepKey="noReportUserRole" entity="adminNoReportRole"/> - <createData stepKey="noReportUser" entity="adminNoReport"/> + <createData entity="adminNoReportRole" stepKey="noReportUserRole"/> + <createData entity="adminNoReport" stepKey="noReportUser"/> + <actionGroup ref="LoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> - <amOnPage stepKey="amOnLogoutPage" url="admin/admin/auth/logout/"/> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> </after> - <actionGroup ref="LoginActionGroup" stepKey="loginAsAdmin"/> + <amOnPage url="{{AdminUsersPage.url}}" stepKey="amOnAdminUsersPage"/> + <fillField selector="{{AdminUserGridSection.usernameFilterTextField}}" userInput="$$noReportUser.username$$" stepKey="fillUsernameSearch"/> + <click selector="{{AdminUserGridSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad time="10" stepKey="wait1"/> + <see selector="{{AdminUserGridSection.usernameInFirstRow}}" userInput="$$noReportUser.username$$" stepKey="seeFoundUsername"/> + <click selector="{{AdminUserGridSection.searchResultFirstRow}}" stepKey="clickFoundUsername"/> + <waitForPageLoad time="30" stepKey="wait2"/> + <seeInField selector="{{AdminEditUserSection.usernameTextField}}" userInput="$$noReportUser.username$$" stepKey="seeUsernameInField"/> + <fillField selector="{{AdminEditUserSection.currentPasswordField}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}" stepKey="fillCurrentPassword"/> + <click selector="{{AdminEditUserSection.userRoleTab}}" stepKey="clickUserRoleTab"/> - <amOnPage stepKey="amOnAdminUsersPage" url="{{AdminUsersPage.url}}"/> - <fillField stepKey="fillUsernameSearch" selector="{{AdminUserGridSection.usernameFilterTextField}}" userInput="$$noReportUser.username$$"/> - <click stepKey="clickSearchButton" selector="{{AdminUserGridSection.searchButton}}"/> - <waitForPageLoad stepKey="wait1" time="10"/> - <see stepKey="seeFoundUsername" selector="{{AdminUserGridSection.usernameInFirstRow}}" userInput="$$noReportUser.username$$"/> - <click stepKey="clickFoundUsername" selector="{{AdminUserGridSection.searchResultFirstRow}}"/> - <waitForPageLoad stepKey="wait2" time="30"/> - <seeInField stepKey="seeUsernameInField" selector="{{AdminEditUserSection.usernameTextField}}" userInput="$$noReportUser.username$$"/> - <fillField stepKey="fillCurrentPassword" selector="{{AdminEditUserSection.currentPasswordField}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}"/> - <click stepKey="clickUserRoleTab" selector="{{AdminEditUserSection.userRoleTab}}"/> + <fillField selector="{{AdminEditUserSection.roleNameFilterTextField}}" userInput="$$noReportUserRole.rolename$$" stepKey="fillRoleNameSearch"/> + <click selector="{{AdminEditUserSection.searchButton}}" stepKey="clickSearchButtonUserRole"/> + <waitForPageLoad time="10" stepKey="wait3"/> + <see selector="{{AdminEditUserSection.roleNameInFirstRow}}" userInput="$$noReportUserRole.rolename$$" stepKey="seeFoundRoleName"/> + <click selector="{{AdminEditUserSection.searchResultFirstRow}}" stepKey="clickFoundRoleName"/> + <click selector="{{AdminEditUserSection.saveButton}}" stepKey="clickSaveButton"/> + <waitForPageLoad time="10" stepKey="wait4"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the user." stepKey="seeSuccess"/> - <fillField stepKey="fillRoleNameSearch" selector="{{AdminEditUserSection.roleNameFilterTextField}}" userInput="$$noReportUserRole.rolename$$"/> - <click stepKey="clickSearchButtonUserRole" selector="{{AdminEditUserSection.searchButton}}"/> - <waitForPageLoad stepKey="wait3" time="10"/> - <see stepKey="seeFoundRoleName" selector="{{AdminEditUserSection.roleNameInFirstRow}}" userInput="$$noReportUserRole.rolename$$"/> - <click stepKey="clickFoundRoleName" selector="{{AdminEditUserSection.searchResultFirstRow}}"/> - <click stepKey="clickSaveButton" selector="{{AdminEditUserSection.saveButton}}"/> - <waitForPageLoad stepKey="wait4" time="10"/> - <see stepKey="saveUserSuccessMessage" selector="{{AdminUserGridSection.successMessage}}" userInput="You saved the user."/> + <amOnPage url="{{AdminConfigPage.url}}" stepKey="amOnAdminConfig"/> + <conditionalClick selector="{{AdminConfigSection.generalTab}}" dependentSelector="{{AdminConfigSection.generalTabOpened}}" visible="false" stepKey="openGeneralTabIfClosed"/> + <scrollTo selector="{{AdminConfigAdvancedReportingSection.advancedReportingMenuItem}}" stepKey="scrollToMenuItem"/> + <!--<see stepKey="seeAdvancedReportingConfigMenuItem" selector="{{AdminConfigAdvancedReportingSection.advancedReportingMenuItem}}" userInput="Advanced Reporting"/>--> + <seeElementInDOM selector="{{AdminConfigAdvancedReportingSection.advancedReportingMenuItem}}" stepKey="seeAdvancedReportingConfigMenuItem"/> + <actionGroup ref="logout" stepKey="logoutOfAdmin2"/> - <amOnPage stepKey="amOnAdminConfig" url="{{AdminConfigPage.url}}"/> - <see stepKey="seeAdvancedReportingConfigMenuItem" selector="{{AdminConfigSection.advancedReportingMenuItem}}" userInput="Advanced Reporting"/> - <amOnPage stepKey="amOnLogoutPage2" url="admin/admin/auth/logout/"/> - - <amOnPage stepKey="amOnAdminLoginPage" url="{{AdminLoginPage.url}}"/> - <fillField stepKey="fillUsernameNoReport" selector="{{AdminLoginFormSection.username}}" userInput="$$noReportUser.username$$"/> - <fillField stepKey="fillPasswordNoReport" selector="{{AdminLoginFormSection.password}}" userInput="$$noReportUser.password$$"/> - <click stepKey="clickOnSignIn2" selector="{{AdminLoginFormSection.signIn}}"/> - <waitForPageLoad stepKey="wait5" time="10"/> - <amOnPage stepKey="amOnAdminConfig2" url="{{AdminConfigPage.url}}"/> - <dontSee stepKey="dontSeeAdvancedReportingConfigMenuItem" selector="{{AdminConfigSection.advancedReportingMenuItem}}" userInput="Advanced Reporting"/> + <amOnPage url="{{AdminLoginPage.url}}" stepKey="amOnAdminLoginPage"/> + <fillField selector="{{AdminLoginFormSection.username}}" userInput="$$noReportUser.username$$" stepKey="fillUsernameNoReport"/> + <fillField selector="{{AdminLoginFormSection.password}}" userInput="$$noReportUser.password$$" stepKey="fillPasswordNoReport"/> + <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickOnSignIn2"/> + <waitForPageLoad time="10" stepKey="wait5"/> + <amOnPage url="{{AdminConfigPage.url}}" stepKey="amOnAdminConfig2"/> + <conditionalClick selector="{{AdminConfigSection.generalTab}}" dependentSelector="{{AdminConfigSection.generalTabOpened}}" visible="false" stepKey="openGeneralTabIfClosed2"/> + <dontSeeElementInDOM selector="{{AdminConfigAdvancedReportingSection.advancedReportingMenuItem}}" stepKey="dontSeeAdvancedReportingConfigMenuItem"/> </test> </tests> diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml index 3f17df108b50..58e62500b820 100644 --- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml @@ -19,17 +19,16 @@ <group value="analytics"/> </annotations> <after> - <amOnPage stepKey="amOnLogoutPage" url="admin/admin/auth/logout/"/> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> </after> <actionGroup ref="LoginActionGroup" stepKey="loginAsAdmin"/> - <amOnPage stepKey="amOnAdminConfig" url="{{AdminConfigPage.url}}"/> - <waitForPageLoad stepKey="waitForAdminConfig"/> - <click stepKey="clickAdvancedReportingConfigMenu" selector="{{AdminConfigSection.advancedReportingMenuItem}}"/> - <selectOption stepKey="selectAdvancedReportingIndustry" selector="{{AdminConfigSection.advancedReportingIndustry}}" userInput="Apps and Games"/> - <selectOption stepKey="selectAdvancedReportingHour" selector="{{AdminConfigSection.advancedReportingHour}}" userInput="11"/> - <selectOption stepKey="selectAdvancedReportingMinute" selector="{{AdminConfigSection.advancedReportingMinute}}" userInput="11"/> - <selectOption stepKey="selectAdvancedReportingSeconds" selector="{{AdminConfigSection.advancedReportingSeconds}}" userInput="00"/> - <click stepKey="clickSaveConfigButton" selector="{{AdminConfigSection.saveButton}}"/> - <see stepKey="seeBlankIndustryErrorMessage" selector="{{AdminConfigSection.advancedReportingSuccessMessage}}" userInput="You saved the configuration."/> + <amOnPage url="{{AdminConfigGeneralAnalyticsPage.url}}" stepKey="amOnAdminConfig"/> + <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingService}}" userInput="Enable" stepKey="selectAdvancedReportingServiceEnabled"/> + <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingIndustry}}" userInput="Apps and Games" stepKey="selectAdvancedReportingIndustry"/> + <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingHour}}" userInput="11" stepKey="selectAdvancedReportingHour"/> + <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingMinute}}" userInput="11" stepKey="selectAdvancedReportingMinute"/> + <selectOption selector="{{AdminConfigAdvancedReportingSection.advancedReportingSeconds}}" userInput="00" stepKey="selectAdvancedReportingSeconds"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveConfigButton"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the configuration." stepKey="seeSuccess"/> </test> </tests> diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/AdditionalCommentTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/AdditionalCommentTest.php index 407e323aeaae..9428f8954c60 100644 --- a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/AdditionalCommentTest.php +++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/AdditionalCommentTest.php @@ -39,6 +39,15 @@ protected function setUp() ->setMethods(['getComment', 'getLabel']) ->disableOriginalConstructor() ->getMock(); + + $objectManager = new ObjectManager($this); + $escaper = $objectManager->getObject(\Magento\Framework\Escaper::class); + $reflection = new \ReflectionClass($this->abstractElementMock); + $reflection_property = $reflection->getProperty('_escaper'); + $reflection_property->setAccessible(true); + $reflection_property->setValue($this->abstractElementMock, $escaper); + + $this->abstractElementMock->setEscaper($escaper); $this->contextMock = $this->getMockBuilder(Context::class) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php index d567d6588235..08ee3c356937 100644 --- a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php +++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php @@ -46,6 +46,14 @@ protected function setUp() ->setMethods(['getComment']) ->disableOriginalConstructor() ->getMock(); + + $objectManager = new ObjectManager($this); + $escaper = $objectManager->getObject(\Magento\Framework\Escaper::class); + $reflection = new \ReflectionClass($this->abstractElementMock); + $reflection_property = $reflection->getProperty('_escaper'); + $reflection_property->setAccessible(true); + $reflection_property->setValue($this->abstractElementMock, $escaper); + $this->contextMock = $this->getMockBuilder(Context::class) ->setMethods(['getLocaleDate']) ->disableOriginalConstructor() diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php index 78ff581f3de9..b43225be9570 100644 --- a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php +++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php @@ -51,6 +51,14 @@ protected function setUp() ->setMethods(['getComment']) ->disableOriginalConstructor() ->getMock(); + + $objectManager = new ObjectManager($this); + $escaper = $objectManager->getObject(\Magento\Framework\Escaper::class); + $reflection = new \ReflectionClass($this->abstractElementMock); + $reflection_property = $reflection->getProperty('_escaper'); + $reflection_property->setAccessible(true); + $reflection_property->setValue($this->abstractElementMock, $escaper); + $this->formMock = $this->getMockBuilder(Form::class) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php index 6a0cecc78106..0b5e86a52333 100644 --- a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php +++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php @@ -39,6 +39,14 @@ protected function setUp() ->setMethods(['getComment', 'getLabel', 'getHint']) ->disableOriginalConstructor() ->getMock(); + + $objectManager = new ObjectManager($this); + $escaper = $objectManager->getObject(\Magento\Framework\Escaper::class); + $reflection = new \ReflectionClass($this->abstractElementMock); + $reflection_property = $reflection->getProperty('_escaper'); + $reflection_property->setAccessible(true); + $reflection_property->setValue($this->abstractElementMock, $escaper); + $this->contextMock = $this->getMockBuilder(Context::class) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerTest.php index 47747027ed70..cf00556cfe59 100644 --- a/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerTest.php +++ b/app/code/Magento/Analytics/Test/Unit/Model/ExportDataHandlerTest.php @@ -191,7 +191,7 @@ public function testPrepareExportData($isArchiveSourceDirectory) ->with( $archiveSource, $archiveAbsolutePath, - $isArchiveSourceDirectory ? true : false + $isArchiveSourceDirectory ); $fileContent = 'Some text'; @@ -222,7 +222,7 @@ public function prepareExportDataDataProvider() { return [ 'Data source for archive is directory' => [true], - 'Data source for archive doesn\'t directory' => [false], + 'Data source for archive isn\'t directory' => [false], ]; } diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/SelectBuilderTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/SelectBuilderTest.php index 8be2f0ee968e..a4362d583dfb 100644 --- a/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/SelectBuilderTest.php +++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/DB/SelectBuilderTest.php @@ -64,12 +64,12 @@ public function testCreate() ['link-type' => 'right', 'table' => 'attribute', 'condition' => 'neq'], ]; $groups = ['id', 'name']; - $this->selectBuilder->setConnectionName($connectionName); - $this->selectBuilder->setFrom($from); - $this->selectBuilder->setColumns($columns); - $this->selectBuilder->setFilters([$filter]); - $this->selectBuilder->setJoins($joins); - $this->selectBuilder->setGroup($groups); + $this->selectBuilder->setConnectionName($connectionName) + ->setFrom($from) + ->setColumns($columns) + ->setFilters([$filter]) + ->setJoins($joins) + ->setGroup($groups); $this->resourceConnectionMock->expects($this->once()) ->method('getConnection') ->with($connectionName) diff --git a/app/code/Magento/Analytics/etc/adminhtml/system.xml b/app/code/Magento/Analytics/etc/adminhtml/system.xml index 4e21648d00ce..c7da840b7e66 100644 --- a/app/code/Magento/Analytics/etc/adminhtml/system.xml +++ b/app/code/Magento/Analytics/etc/adminhtml/system.xml @@ -36,6 +36,9 @@ <source_model>Magento\Analytics\Model\Config\Source\Vertical</source_model> <backend_model>Magento\Analytics\Model\Config\Backend\Vertical</backend_model> <frontend_model>Magento\Analytics\Block\Adminhtml\System\Config\Vertical</frontend_model> + <depends> + <field id="analytics/general/enabled">1</field> + </depends> </field> <field id="additional_comment" translate="label comment" type="label" sortOrder="40" showInDefault="1" showInWebsite="0" showInStore="0"> <label><![CDATA[<strong>Get more insights from Magento Business Intelligence</strong>]]></label> diff --git a/app/code/Magento/AsynchronousOperations/Api/Data/BulkSummaryInterface.php b/app/code/Magento/AsynchronousOperations/Api/Data/BulkSummaryInterface.php index f5dd5bb13eab..a433ec0953a8 100644 --- a/app/code/Magento/AsynchronousOperations/Api/Data/BulkSummaryInterface.php +++ b/app/code/Magento/AsynchronousOperations/Api/Data/BulkSummaryInterface.php @@ -13,6 +13,8 @@ */ interface BulkSummaryInterface extends \Magento\Framework\Bulk\BulkSummaryInterface { + const USER_TYPE = 'user_type'; + /** * Retrieve existing extension attributes object. * @@ -31,4 +33,19 @@ public function getExtensionAttributes(); public function setExtensionAttributes( \Magento\AsynchronousOperations\Api\Data\BulkSummaryExtensionInterface $extensionAttributes ); + + /** + * Get user type + * + * @return int + */ + public function getUserType(); + + /** + * Set user type + * + * @param int $userType + * @return $this + */ + public function setUserType($userType); } diff --git a/app/code/Magento/AsynchronousOperations/Controller/Adminhtml/Bulk/Details.php b/app/code/Magento/AsynchronousOperations/Controller/Adminhtml/Bulk/Details.php index 9e9dbd3dd67c..a450187dd094 100644 --- a/app/code/Magento/AsynchronousOperations/Controller/Adminhtml/Bulk/Details.php +++ b/app/code/Magento/AsynchronousOperations/Controller/Adminhtml/Bulk/Details.php @@ -6,9 +6,9 @@ namespace Magento\AsynchronousOperations\Controller\Adminhtml\Bulk; /** - * Class View Opertion Details Controller + * Class View Operation Details Controller */ -class Details extends \Magento\Backend\App\Action +class Details extends \Magento\Backend\App\Action implements \Magento\Framework\App\Action\HttpGetActionInterface { /** * @var \Magento\Framework\View\Result\PageFactory diff --git a/app/code/Magento/AsynchronousOperations/Model/BulkManagement.php b/app/code/Magento/AsynchronousOperations/Model/BulkManagement.php index 4f086ce8ac2c..faf01921e573 100644 --- a/app/code/Magento/AsynchronousOperations/Model/BulkManagement.php +++ b/app/code/Magento/AsynchronousOperations/Model/BulkManagement.php @@ -5,6 +5,7 @@ */ namespace Magento\AsynchronousOperations\Model; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\ResourceConnection; use Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface; use Magento\AsynchronousOperations\Api\Data\BulkSummaryInterfaceFactory; @@ -13,6 +14,7 @@ use Magento\Framework\EntityManager\EntityManager; use Magento\Framework\EntityManager\MetadataPool; use Magento\AsynchronousOperations\Model\ResourceModel\Operation\CollectionFactory; +use Magento\Authorization\Model\UserContextInterface; /** * Class BulkManagement @@ -51,6 +53,11 @@ class BulkManagement implements \Magento\Framework\Bulk\BulkManagementInterface */ private $resourceConnection; + /** + * @var \Magento\Authorization\Model\UserContextInterface + */ + private $userContext; + /** * @var \Psr\Log\LoggerInterface */ @@ -65,6 +72,7 @@ class BulkManagement implements \Magento\Framework\Bulk\BulkManagementInterface * @param MetadataPool $metadataPool * @param ResourceConnection $resourceConnection * @param \Psr\Log\LoggerInterface $logger + * @param UserContextInterface $userContext */ public function __construct( EntityManager $entityManager, @@ -73,7 +81,8 @@ public function __construct( BulkPublisherInterface $publisher, MetadataPool $metadataPool, ResourceConnection $resourceConnection, - \Psr\Log\LoggerInterface $logger + \Psr\Log\LoggerInterface $logger, + UserContextInterface $userContext = null ) { $this->entityManager = $entityManager; $this->bulkSummaryFactory= $bulkSummaryFactory; @@ -82,6 +91,7 @@ public function __construct( $this->resourceConnection = $resourceConnection; $this->publisher = $publisher; $this->logger = $logger; + $this->userContext = $userContext ?: ObjectManager::getInstance()->get(UserContextInterface::class); } /** @@ -93,6 +103,10 @@ public function scheduleBulk($bulkUuid, array $operations, $description, $userId $connection = $this->resourceConnection->getConnectionByName($metadata->getEntityConnectionName()); // save bulk summary and related operations $connection->beginTransaction(); + $userType = $this->userContext->getUserType(); + if ($userType === null) { + $userType = UserContextInterface::USER_TYPE_ADMIN; + } try { /** @var \Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface $bulkSummary */ $bulkSummary = $this->bulkSummaryFactory->create(); @@ -100,6 +114,7 @@ public function scheduleBulk($bulkUuid, array $operations, $description, $userId $bulkSummary->setBulkId($bulkUuid); $bulkSummary->setDescription($description); $bulkSummary->setUserId($userId); + $bulkSummary->setUserType($userType); $bulkSummary->setOperationCount((int)$bulkSummary->getOperationCount() + count($operations)); $this->entityManager->save($bulkSummary); diff --git a/app/code/Magento/AsynchronousOperations/Model/BulkSummary.php b/app/code/Magento/AsynchronousOperations/Model/BulkSummary.php index e99233d07695..1d834ad10b2e 100644 --- a/app/code/Magento/AsynchronousOperations/Model/BulkSummary.php +++ b/app/code/Magento/AsynchronousOperations/Model/BulkSummary.php @@ -78,6 +78,22 @@ public function setUserId($userId) return $this->setData(self::USER_ID, $userId); } + /** + * @inheritDoc + */ + public function getUserType() + { + return $this->getData(self::USER_TYPE); + } + + /** + * @inheritDoc + */ + public function setUserType($userType) + { + return $this->setData(self::USER_TYPE, $userType); + } + /** * @inheritDoc */ diff --git a/app/code/Magento/AsynchronousOperations/Model/MassConsumer.php b/app/code/Magento/AsynchronousOperations/Model/MassConsumer.php index 28bc8141a8e9..af1ef4400e44 100644 --- a/app/code/Magento/AsynchronousOperations/Model/MassConsumer.php +++ b/app/code/Magento/AsynchronousOperations/Model/MassConsumer.php @@ -9,11 +9,12 @@ namespace Magento\AsynchronousOperations\Model; use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Registry; use Psr\Log\LoggerInterface; use Magento\Framework\MessageQueue\MessageLockException; use Magento\Framework\MessageQueue\ConnectionLostException; use Magento\Framework\Exception\NotFoundException; -use Magento\Framework\MessageQueue\CallbackInvoker; +use Magento\Framework\MessageQueue\CallbackInvokerInterface; use Magento\Framework\MessageQueue\ConsumerConfigurationInterface; use Magento\Framework\MessageQueue\EnvelopeInterface; use Magento\Framework\MessageQueue\QueueInterface; @@ -29,7 +30,7 @@ class MassConsumer implements ConsumerInterface { /** - * @var \Magento\Framework\MessageQueue\CallbackInvoker + * @var CallbackInvokerInterface */ private $invoker; @@ -58,23 +59,30 @@ class MassConsumer implements ConsumerInterface */ private $operationProcessor; + /** + * @var Registry + */ + private $registry; + /** * Initialize dependencies. * - * @param CallbackInvoker $invoker + * @param CallbackInvokerInterface $invoker * @param ResourceConnection $resource * @param MessageController $messageController * @param ConsumerConfigurationInterface $configuration * @param OperationProcessorFactory $operationProcessorFactory * @param LoggerInterface $logger + * @param Registry $registry */ public function __construct( - CallbackInvoker $invoker, + CallbackInvokerInterface $invoker, ResourceConnection $resource, MessageController $messageController, ConsumerConfigurationInterface $configuration, OperationProcessorFactory $operationProcessorFactory, - LoggerInterface $logger + LoggerInterface $logger, + Registry $registry = null ) { $this->invoker = $invoker; $this->resource = $resource; @@ -84,13 +92,17 @@ public function __construct( 'configuration' => $configuration ]); $this->logger = $logger; + $this->registry = $registry ?? \Magento\Framework\App\ObjectManager::getInstance() + ->get(Registry::class); } /** - * {@inheritdoc} + * @inheritdoc */ public function process($maxNumberOfMessages = null) { + $this->registry->register('isSecureArea', true, true); + $queue = $this->configuration->getQueue(); if (!isset($maxNumberOfMessages)) { @@ -98,6 +110,8 @@ public function process($maxNumberOfMessages = null) } else { $this->invoker->invoke($queue, $maxNumberOfMessages, $this->getTransactionCallback($queue)); } + + $this->registry->unregister('isSecureArea'); } /** diff --git a/app/code/Magento/AsynchronousOperations/Model/MassSchedule.php b/app/code/Magento/AsynchronousOperations/Model/MassSchedule.php index 2d516e82f401..89d468159c6e 100644 --- a/app/code/Magento/AsynchronousOperations/Model/MassSchedule.php +++ b/app/code/Magento/AsynchronousOperations/Model/MassSchedule.php @@ -8,6 +8,7 @@ namespace Magento\AsynchronousOperations\Model; +use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject\IdentityGeneratorInterface; use Magento\Framework\Exception\LocalizedException; use Magento\AsynchronousOperations\Api\Data\ItemStatusInterfaceFactory; @@ -18,9 +19,13 @@ use Magento\Framework\Exception\BulkException; use Psr\Log\LoggerInterface; use Magento\AsynchronousOperations\Model\ResourceModel\Operation\OperationRepository; +use Magento\Authorization\Model\UserContextInterface; +use Magento\Framework\Encryption\Encryptor; /** * Class MassSchedule used for adding multiple entities as Operations to Bulk Management with the status tracking + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Suppressed without refactoring to not introduce BiC */ class MassSchedule { @@ -54,6 +59,16 @@ class MassSchedule */ private $operationRepository; + /** + * @var \Magento\Authorization\Model\UserContextInterface + */ + private $userContext; + + /** + * @var Encryptor + */ + private $encryptor; + /** * Initialize dependencies. * @@ -63,6 +78,8 @@ class MassSchedule * @param BulkManagementInterface $bulkManagement * @param LoggerInterface $logger * @param OperationRepository $operationRepository + * @param UserContextInterface $userContext + * @param Encryptor|null $encryptor */ public function __construct( IdentityGeneratorInterface $identityService, @@ -70,7 +87,9 @@ public function __construct( AsyncResponseInterfaceFactory $asyncResponseFactory, BulkManagementInterface $bulkManagement, LoggerInterface $logger, - OperationRepository $operationRepository + OperationRepository $operationRepository, + UserContextInterface $userContext = null, + Encryptor $encryptor = null ) { $this->identityService = $identityService; $this->itemStatusInterfaceFactory = $itemStatusInterfaceFactory; @@ -78,15 +97,17 @@ public function __construct( $this->bulkManagement = $bulkManagement; $this->logger = $logger; $this->operationRepository = $operationRepository; + $this->userContext = $userContext ?: ObjectManager::getInstance()->get(UserContextInterface::class); + $this->encryptor = $encryptor ?: ObjectManager::getInstance()->get(Encryptor::class); } /** * Schedule new bulk operation based on the list of entities * - * @param $topicName - * @param $entitiesArray - * @param null $groupId - * @param null $userId + * @param string $topicName + * @param array $entitiesArray + * @param string $groupId + * @param string $userId * @return AsyncResponseInterface * @throws BulkException * @throws LocalizedException @@ -95,6 +116,10 @@ public function publishMass($topicName, array $entitiesArray, $groupId = null, $ { $bulkDescription = __('Topic %1', $topicName); + if ($userId == null) { + $userId = $this->userContext->getUserId(); + } + if ($groupId == null) { $groupId = $this->identityService->generateId(); @@ -114,9 +139,13 @@ public function publishMass($topicName, array $entitiesArray, $groupId = null, $ $requestItem = $this->itemStatusInterfaceFactory->create(); try { - $operations[] = $this->operationRepository->createByTopic($topicName, $entityParams, $groupId); + $operation = $this->operationRepository->createByTopic($topicName, $entityParams, $groupId); + $operations[] = $operation; $requestItem->setId($key); $requestItem->setStatus(ItemStatusInterface::STATUS_ACCEPTED); + $requestItem->setDataHash( + $this->encryptor->hash($operation->getSerializedData(), Encryptor::HASH_VERSION_SHA256) + ); $requestItems[] = $requestItem; } catch (\Exception $exception) { $this->logger->error($exception); diff --git a/app/code/Magento/AsynchronousOperations/Test/Unit/Model/BulkManagementTest.php b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/BulkManagementTest.php index 3a45c34df17f..e5951fb129e7 100644 --- a/app/code/Magento/AsynchronousOperations/Test/Unit/Model/BulkManagementTest.php +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/BulkManagementTest.php @@ -108,6 +108,7 @@ public function testScheduleBulk() $bulkUuid = 'bulk-001'; $description = 'Bulk summary description...'; $userId = 1; + $userType = \Magento\Authorization\Model\UserContextInterface::USER_TYPE_ADMIN; $connectionName = 'default'; $topicNames = ['topic.name.0', 'topic.name.1']; $operation = $this->getMockBuilder(\Magento\AsynchronousOperations\Api\Data\OperationInterface::class) @@ -131,6 +132,7 @@ public function testScheduleBulk() $bulkSummary->expects($this->once())->method('setBulkId')->with($bulkUuid)->willReturnSelf(); $bulkSummary->expects($this->once())->method('setDescription')->with($description)->willReturnSelf(); $bulkSummary->expects($this->once())->method('setUserId')->with($userId)->willReturnSelf(); + $bulkSummary->expects($this->once())->method('setUserType')->with($userType)->willReturnSelf(); $bulkSummary->expects($this->once())->method('getOperationCount')->willReturn(1); $bulkSummary->expects($this->once())->method('setOperationCount')->with(3)->willReturnSelf(); $this->entityManager->expects($this->once())->method('save')->with($bulkSummary)->willReturn($bulkSummary); diff --git a/app/code/Magento/AsynchronousOperations/composer.json b/app/code/Magento/AsynchronousOperations/composer.json index 7d5a097eeea3..18927b5f4ecc 100644 --- a/app/code/Magento/AsynchronousOperations/composer.json +++ b/app/code/Magento/AsynchronousOperations/composer.json @@ -10,7 +10,6 @@ "magento/module-authorization": "*", "magento/module-backend": "*", "magento/module-ui": "*", - "magento/module-user": "*", "php": "~7.1.3||~7.2.0" }, "suggest": { diff --git a/app/code/Magento/AsynchronousOperations/etc/db_schema.xml b/app/code/Magento/AsynchronousOperations/etc/db_schema.xml index 1b99ce9a2805..5cd55408838f 100644 --- a/app/code/Magento/AsynchronousOperations/etc/db_schema.xml +++ b/app/code/Magento/AsynchronousOperations/etc/db_schema.xml @@ -14,20 +14,22 @@ <column xsi:type="varbinary" name="uuid" nullable="true" length="39" comment="Bulk UUID (can be exposed to reference bulk entity)"/> <column xsi:type="int" name="user_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="ID of the user that performed an action"/> + comment="ID of the WebAPI user that performed an action"/> + <column xsi:type="int" name="user_type" nullable="true" comment="Which type of user"/> <column xsi:type="varchar" name="description" nullable="true" length="255" comment="Bulk Description"/> <column xsi:type="int" name="operation_count" padding="10" unsigned="true" nullable="false" identity="false" comment="Total number of operations scheduled within this bulk"/> <column xsi:type="timestamp" name="start_time" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Bulk start time"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> </constraint> - <constraint xsi:type="foreign" name="MAGENTO_BULK_USER_ID_ADMIN_USER_USER_ID" table="magento_bulk" - column="user_id" referenceTable="admin_user" referenceColumn="user_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="MAGENTO_BULK_UUID"> + <constraint xsi:type="unique" referenceId="MAGENTO_BULK_UUID"> <column name="uuid"/> </constraint> + <index referenceId="MAGENTO_BULK_USER_ID_ADMIN_USER_USER_ID" indexType="btree"> + <column name="user_id"/> + </index> </table> <table name="magento_operation" resource="default" engine="innodb" comment="Operation entity"> <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" @@ -45,12 +47,12 @@ comment="Code of the error that appeared during operation execution (used to aggregate related failed operations)"/> <column xsi:type="varchar" name="result_message" nullable="true" length="255" comment="Operation result message"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> </constraint> - <constraint xsi:type="foreign" name="MAGENTO_OPERATION_BULK_UUID_MAGENTO_BULK_UUID" table="magento_operation" + <constraint xsi:type="foreign" referenceId="MAGENTO_OPERATION_BULK_UUID_MAGENTO_BULK_UUID" table="magento_operation" column="bulk_uuid" referenceTable="magento_bulk" referenceColumn="uuid" onDelete="CASCADE"/> - <index name="MAGENTO_OPERATION_BULK_UUID_ERROR_CODE" indexType="btree"> + <index referenceId="MAGENTO_OPERATION_BULK_UUID_ERROR_CODE" indexType="btree"> <column name="bulk_uuid"/> <column name="error_code"/> </index> @@ -60,13 +62,13 @@ <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Internal ID"/> <column xsi:type="varbinary" name="bulk_uuid" nullable="true" length="39" comment="Related Bulk UUID"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> </constraint> - <constraint xsi:type="foreign" name="MAGENTO_ACKNOWLEDGED_BULK_BULK_UUID_MAGENTO_BULK_UUID" + <constraint xsi:type="foreign" referenceId="MAGENTO_ACKNOWLEDGED_BULK_BULK_UUID_MAGENTO_BULK_UUID" table="magento_acknowledged_bulk" column="bulk_uuid" referenceTable="magento_bulk" referenceColumn="uuid" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="MAGENTO_ACKNOWLEDGED_BULK_BULK_UUID"> + <constraint xsi:type="unique" referenceId="MAGENTO_ACKNOWLEDGED_BULK_BULK_UUID"> <column name="bulk_uuid"/> </constraint> </table> diff --git a/app/code/Magento/AsynchronousOperations/etc/db_schema_whitelist.json b/app/code/Magento/AsynchronousOperations/etc/db_schema_whitelist.json index d62f16ffca8c..9b6c0709e191 100644 --- a/app/code/Magento/AsynchronousOperations/etc/db_schema_whitelist.json +++ b/app/code/Magento/AsynchronousOperations/etc/db_schema_whitelist.json @@ -4,14 +4,19 @@ "id": true, "uuid": true, "user_id": true, + "user_type": true, "description": true, "operation_count": true, "start_time": true }, + "index": { + "MAGENTO_BULK_USER_ID_ADMIN_USER_USER_ID": true, + "MAGENTO_BULK_USER_ID": true + }, "constraint": { "PRIMARY": true, - "MAGENTO_BULK_USER_ID_ADMIN_USER_USER_ID": true, - "MAGENTO_BULK_UUID": true + "MAGENTO_BULK_UUID": true, + "MAGENTO_BULK_USER_ID_ADMIN_USER_USER_ID": true } }, "magento_operation": { diff --git a/app/code/Magento/Authorization/Model/Role.php b/app/code/Magento/Authorization/Model/Role.php index 2546df86d09d..dcc46ee77ee1 100644 --- a/app/code/Magento/Authorization/Model/Role.php +++ b/app/code/Magento/Authorization/Model/Role.php @@ -51,19 +51,29 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritDoc + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + $properties = parent::__sleep(); return array_diff($properties, ['_resource', '_resourceCollection']); } /** - * {@inheritdoc} + * @inheritDoc + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + parent::__wakeup(); $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); $this->_resource = $objectManager->get(\Magento\Authorization\Model\ResourceModel\Role::class); diff --git a/app/code/Magento/Authorization/etc/db_schema.xml b/app/code/Magento/Authorization/etc/db_schema.xml index 45c02128bfc9..a38828eb6efc 100644 --- a/app/code/Magento/Authorization/etc/db_schema.xml +++ b/app/code/Magento/Authorization/etc/db_schema.xml @@ -21,14 +21,14 @@ comment="User ID"/> <column xsi:type="varchar" name="user_type" nullable="true" length="16" comment="User Type"/> <column xsi:type="varchar" name="role_name" nullable="true" length="50" comment="Role Name"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="role_id"/> </constraint> - <index name="AUTHORIZATION_ROLE_PARENT_ID_SORT_ORDER" indexType="btree"> + <index referenceId="AUTHORIZATION_ROLE_PARENT_ID_SORT_ORDER" indexType="btree"> <column name="parent_id"/> <column name="sort_order"/> </index> - <index name="AUTHORIZATION_ROLE_TREE_LEVEL" indexType="btree"> + <index referenceId="AUTHORIZATION_ROLE_TREE_LEVEL" indexType="btree"> <column name="tree_level"/> </index> </table> @@ -40,17 +40,17 @@ <column xsi:type="varchar" name="resource_id" nullable="true" length="255" comment="Resource ID"/> <column xsi:type="varchar" name="privileges" nullable="true" length="20" comment="Privileges"/> <column xsi:type="varchar" name="permission" nullable="true" length="10" comment="Permission"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rule_id"/> </constraint> - <constraint xsi:type="foreign" name="AUTHORIZATION_RULE_ROLE_ID_AUTHORIZATION_ROLE_ROLE_ID" + <constraint xsi:type="foreign" referenceId="AUTHORIZATION_RULE_ROLE_ID_AUTHORIZATION_ROLE_ROLE_ID" table="authorization_rule" column="role_id" referenceTable="authorization_role" referenceColumn="role_id" onDelete="CASCADE"/> - <index name="AUTHORIZATION_RULE_RESOURCE_ID_ROLE_ID" indexType="btree"> + <index referenceId="AUTHORIZATION_RULE_RESOURCE_ID_ROLE_ID" indexType="btree"> <column name="resource_id"/> <column name="role_id"/> </index> - <index name="AUTHORIZATION_RULE_ROLE_ID_RESOURCE_ID" indexType="btree"> + <index referenceId="AUTHORIZATION_RULE_ROLE_ID_RESOURCE_ID" indexType="btree"> <column name="role_id"/> <column name="resource_id"/> </index> diff --git a/app/code/Magento/Authorizenet/Block/Adminhtml/Order/View/Info/FraudDetails.php b/app/code/Magento/Authorizenet/Block/Adminhtml/Order/View/Info/FraudDetails.php index a7a670d64d7c..c693ebe95d52 100644 --- a/app/code/Magento/Authorizenet/Block/Adminhtml/Order/View/Info/FraudDetails.php +++ b/app/code/Magento/Authorizenet/Block/Adminhtml/Order/View/Info/FraudDetails.php @@ -3,13 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Block\Adminhtml\Order\View\Info; use Magento\Authorizenet\Model\Directpost; /** + * Fraud information block for Authorize.net payment method + * * @api * @since 100.0.2 + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class FraudDetails extends \Magento\Backend\Block\Template { @@ -33,6 +38,8 @@ public function __construct( } /** + * Return payment method model + * * @return \Magento\Sales\Model\Order\Payment */ public function getPayment() @@ -42,6 +49,8 @@ public function getPayment() } /** + * Produce and return the block's HTML output + * * @return string */ protected function _toHtml() diff --git a/app/code/Magento/Authorizenet/Block/Adminhtml/Order/View/Info/PaymentDetails.php b/app/code/Magento/Authorizenet/Block/Adminhtml/Order/View/Info/PaymentDetails.php new file mode 100644 index 000000000000..23034270640d --- /dev/null +++ b/app/code/Magento/Authorizenet/Block/Adminhtml/Order/View/Info/PaymentDetails.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Authorizenet\Block\Adminhtml\Order\View\Info; + +use Magento\Framework\Phrase; +use Magento\Payment\Block\ConfigurableInfo; + +/** + * Payment information block for Authorize.net payment method + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method + */ +class PaymentDetails extends ConfigurableInfo +{ + /** + * Returns localized label for payment info block + * + * @param string $field + * @return string | Phrase + */ + protected function getLabel($field) + { + return __($field); + } +} diff --git a/app/code/Magento/Authorizenet/Block/Transparent/Iframe.php b/app/code/Magento/Authorizenet/Block/Transparent/Iframe.php index 296d22d6f61b..65161413cb18 100644 --- a/app/code/Magento/Authorizenet/Block/Transparent/Iframe.php +++ b/app/code/Magento/Authorizenet/Block/Transparent/Iframe.php @@ -3,13 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Block\Transparent; use Magento\Payment\Block\Transparent\Iframe as TransparentIframe; /** + * Transparent Iframe block for Authorize.net payments * @api * @since 100.0.2 + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class Iframe extends TransparentIframe { diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/AddConfigured.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/AddConfigured.php index 46d395b978eb..f71314613fc1 100644 --- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/AddConfigured.php +++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/AddConfigured.php @@ -4,8 +4,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; -class AddConfigured extends \Magento\Sales\Controller\Adminhtml\Order\Create\AddConfigured +use Magento\Framework\App\Action\HttpPutActionInterface; +use Magento\Sales\Controller\Adminhtml\Order\Create\AddConfigured as BaseAddConfigured; + +/** + * Class AddConfigured + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method + */ +class AddConfigured extends BaseAddConfigured implements HttpPutActionInterface { } diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Cancel.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Cancel.php index 3432e14d77b9..3ebea4704db7 100644 --- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Cancel.php +++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Cancel.php @@ -4,8 +4,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; -class Cancel extends \Magento\Sales\Controller\Adminhtml\Order\Create\Cancel +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Sales\Controller\Adminhtml\Order\Create\Cancel as BaseCancel; + +/** + * Class Cancel + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method + */ +class Cancel extends BaseCancel implements HttpPostActionInterface { } diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ConfigureProductToAdd.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ConfigureProductToAdd.php index 9fa3c7dd19b8..19eb4571a852 100644 --- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ConfigureProductToAdd.php +++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ConfigureProductToAdd.php @@ -4,8 +4,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; -class ConfigureProductToAdd extends \Magento\Sales\Controller\Adminhtml\Order\Create\ConfigureProductToAdd +use Magento\Framework\App\Action\HttpPutActionInterface; +use Magento\Sales\Controller\Adminhtml\Order\Create\ConfigureProductToAdd as BaseConfigureProductToAdd; + +/** + * Class ConfigureProductToAdd + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method + */ +class ConfigureProductToAdd extends BaseConfigureProductToAdd implements HttpPutActionInterface { } diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ConfigureQuoteItems.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ConfigureQuoteItems.php index c1ea98aea238..d314149059c7 100644 --- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ConfigureQuoteItems.php +++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ConfigureQuoteItems.php @@ -4,8 +4,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; -class ConfigureQuoteItems extends \Magento\Sales\Controller\Adminhtml\Order\Create\ConfigureQuoteItems +use Magento\Framework\App\Action\HttpPutActionInterface; +use Magento\Sales\Controller\Adminhtml\Order\Create\ConfigureQuoteItems as BaseConfigureQuoteItems; + +/** + * Class ConfigureQuoteItems + * @deprecated 2.3 Authorize.net is removing all support for this payment method + */ +class ConfigureQuoteItems extends BaseConfigureQuoteItems implements HttpPutActionInterface { } diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Index.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Index.php index b206f89ab8bf..33ac620499e7 100644 --- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Index.php +++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Index.php @@ -4,8 +4,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; +/** + * Class Index + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method + */ class Index extends \Magento\Sales\Controller\Adminhtml\Order\Create\Index { } diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/LoadBlock.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/LoadBlock.php index 43e456e76693..577840c0a9ba 100644 --- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/LoadBlock.php +++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/LoadBlock.php @@ -4,8 +4,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; +/** + * Class LoadBlock + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method + */ class LoadBlock extends \Magento\Sales\Controller\Adminhtml\Order\Create\LoadBlock { } diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Place.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Place.php index b393015ce123..fc4cce07bd08 100644 --- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Place.php +++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Place.php @@ -3,21 +3,26 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; -use Magento\Framework\Escaper; -use Magento\Catalog\Helper\Product; +use Magento\Authorizenet\Helper\Backend\Data as DataHelper; use Magento\Backend\App\Action\Context; -use Magento\Framework\View\Result\PageFactory; use Magento\Backend\Model\View\Result\ForwardFactory; -use Magento\Authorizenet\Helper\Backend\Data as DataHelper; +use Magento\Catalog\Helper\Product; +use Magento\Framework\Escaper; +use Magento\Framework\View\Result\PageFactory; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Sales\Controller\Adminhtml\Order\Create; /** * Class Place * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ -class Place extends \Magento\Sales\Controller\Adminhtml\Order\Create +class Place extends Create implements HttpPostActionInterface { /** * @var DataHelper diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ProcessData.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ProcessData.php index 35720249be35..3d0d572bd626 100644 --- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ProcessData.php +++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ProcessData.php @@ -4,8 +4,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; -class ProcessData extends \Magento\Sales\Controller\Adminhtml\Order\Create\ProcessData +use Magento\Sales\Controller\Adminhtml\Order\Create\ProcessData as BaseProcessData; +use Magento\Framework\App\Action\HttpPostActionInterface; + +/** + * Class ProcessData + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method + */ +class ProcessData extends BaseProcessData implements HttpPostActionInterface { } diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Redirect.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Redirect.php index bd9de956dc64..333751f93653 100644 --- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Redirect.php +++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Redirect.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; use Magento\Backend\App\Action; @@ -10,11 +12,16 @@ use Magento\Framework\View\Result\LayoutFactory; use Magento\Framework\View\Result\PageFactory; use Magento\Payment\Block\Transparent\Iframe; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Sales\Controller\Adminhtml\Order\Create; /** + * Class Redirect * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ -class Redirect extends \Magento\Sales\Controller\Adminhtml\Order\Create +class Redirect extends Create implements HttpGetActionInterface, HttpPostActionInterface { /** * Core registry diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Reorder.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Reorder.php index 80b9f54524f0..06a6403915ff 100644 --- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Reorder.php +++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Reorder.php @@ -4,8 +4,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; -class Reorder extends \Magento\Sales\Controller\Adminhtml\Order\Create\Reorder +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Sales\Controller\Adminhtml\Order\Create\Reorder as BaseReorder; + +/** + * Class Reorder + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method + */ +class Reorder extends BaseReorder implements HttpPostActionInterface { } diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ReturnQuote.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ReturnQuote.php index 82a5ee08f7ce..c42e7ecbeef0 100644 --- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ReturnQuote.php +++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ReturnQuote.php @@ -4,9 +4,19 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; -class ReturnQuote extends \Magento\Sales\Controller\Adminhtml\Order\Create +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Sales\Controller\Adminhtml\Order\Create; + +/** + * Class ReturnQuote + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method + */ +class ReturnQuote extends Create implements HttpPostActionInterface, HttpGetActionInterface { /** * Return quote diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Save.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Save.php index 7519f3415c40..cc93ce5daede 100644 --- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Save.php +++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Save.php @@ -4,8 +4,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; +/** + * Class Save + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method + */ class Save extends \Magento\Sales\Controller\Adminhtml\Order\Create\Save { } diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ShowUpdateResult.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ShowUpdateResult.php index b55da878b2e3..af80bde10831 100644 --- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ShowUpdateResult.php +++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/ShowUpdateResult.php @@ -4,8 +4,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; +/** + * Class ShowUpdateResult + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method + */ class ShowUpdateResult extends \Magento\Sales\Controller\Adminhtml\Order\Create\ShowUpdateResult { } diff --git a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Start.php b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Start.php index f0ac5f80c11a..689b30d63be6 100644 --- a/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Start.php +++ b/app/code/Magento/Authorizenet/Controller/Adminhtml/Authorizenet/Directpost/Payment/Start.php @@ -4,8 +4,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Adminhtml\Authorizenet\Directpost\Payment; -abstract class Start extends \Magento\Sales\Controller\Adminhtml\Order\Create +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Sales\Controller\Adminhtml\Order\Create; + +/** + * Class Start + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method + */ +abstract class Start extends Create implements HttpPostActionInterface { } diff --git a/app/code/Magento/Authorizenet/Controller/Directpost/Payment.php b/app/code/Magento/Authorizenet/Controller/Directpost/Payment.php index fc9e7807cd99..cfaa5f1cfcd0 100644 --- a/app/code/Magento/Authorizenet/Controller/Directpost/Payment.php +++ b/app/code/Magento/Authorizenet/Controller/Directpost/Payment.php @@ -3,16 +3,22 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Directpost; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Payment\Block\Transparent\Iframe; +use Magento\Framework\App\Action\Action; /** * DirectPost Payment Controller * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ -abstract class Payment extends \Magento\Framework\App\Action\Action +abstract class Payment extends Action implements HttpGetActionInterface, HttpPostActionInterface { /** * Core registry @@ -44,6 +50,8 @@ public function __construct( } /** + * Get checkout model + * * @return \Magento\Checkout\Model\Session */ protected function _getCheckout() @@ -63,6 +71,7 @@ protected function _getDirectPostSession() /** * Response action. + * * Action for Authorize.net SIM Relay Request. * * @param string $area diff --git a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/BackendResponse.php b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/BackendResponse.php index 70565ea8ac65..e0610a92feb6 100644 --- a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/BackendResponse.php +++ b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/BackendResponse.php @@ -4,6 +4,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Directpost\Payment; use Magento\Authorizenet\Helper\DataFactory; @@ -16,9 +18,18 @@ use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Registry; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; use Psr\Log\LoggerInterface; -class BackendResponse extends \Magento\Authorizenet\Controller\Directpost\Payment implements CsrfAwareActionInterface +/** + * Class BackendResponse + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method + */ +class BackendResponse extends \Magento\Authorizenet\Controller\Directpost\Payment implements + CsrfAwareActionInterface, + HttpGetActionInterface, + HttpPostActionInterface { /** * @var LoggerInterface @@ -70,6 +81,7 @@ public function validateForCsrf(RequestInterface $request): ?bool /** * Response action. + * * Action for Authorize.net SIM Relay Request. * * @return \Magento\Framework\Controller\ResultInterface diff --git a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Place.php b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Place.php index 92957481b929..7d672a75f5b1 100644 --- a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Place.php +++ b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Place.php @@ -3,9 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Authorizenet\Controller\Directpost\Payment; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Authorizenet\Controller\Directpost\Payment; use Magento\Authorizenet\Helper\DataFactory; use Magento\Checkout\Model\Type\Onepage; @@ -24,8 +26,9 @@ * Class Place * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ -class Place extends Payment +class Place extends Payment implements HttpPostActionInterface { /** * @var \Magento\Quote\Api\CartManagementInterface diff --git a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Redirect.php b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Redirect.php index 028b90bf7da5..8c9510243f61 100644 --- a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Redirect.php +++ b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Redirect.php @@ -4,15 +4,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Directpost\Payment; -use Magento\Framework\App\ObjectManager; +use Magento\Authorizenet\Controller\Directpost\Payment; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Payment\Block\Transparent\Iframe; /** * Class Redirect + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ -class Redirect extends \Magento\Authorizenet\Controller\Directpost\Payment +class Redirect extends Payment implements HttpGetActionInterface, HttpPostActionInterface { /** * Retrieve params and put javascript into iframe diff --git a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Response.php b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Response.php index d562df9fb24a..17fc3cb72e45 100644 --- a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Response.php +++ b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/Response.php @@ -4,13 +4,22 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Directpost\Payment; use Magento\Framework\App\CsrfAwareActionInterface; use Magento\Framework\App\Request\InvalidRequestException; use Magento\Framework\App\RequestInterface; +use Magento\Authorizenet\Controller\Directpost\Payment; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; -class Response extends \Magento\Authorizenet\Controller\Directpost\Payment implements CsrfAwareActionInterface +/** + * Class Response + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method + */ +class Response extends Payment implements CsrfAwareActionInterface, HttpGetActionInterface, HttpPostActionInterface { /** * @inheritDoc @@ -31,6 +40,7 @@ public function validateForCsrf(RequestInterface $request): ?bool /** * Response action. + * * Action for Authorize.net SIM Relay Request. * * @return \Magento\Framework\Controller\ResultInterface diff --git a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/ReturnQuote.php b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/ReturnQuote.php index 3030a75055b7..c974632f584b 100644 --- a/app/code/Magento/Authorizenet/Controller/Directpost/Payment/ReturnQuote.php +++ b/app/code/Magento/Authorizenet/Controller/Directpost/Payment/ReturnQuote.php @@ -4,9 +4,19 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Controller\Directpost\Payment; -class ReturnQuote extends \Magento\Authorizenet\Controller\Directpost\Payment +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Authorizenet\Controller\Directpost\Payment; + +/** + * Class ReturnQuote + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method + */ +class ReturnQuote extends Payment implements HttpPostActionInterface, HttpGetActionInterface { /** * Return customer quote by ajax diff --git a/app/code/Magento/Authorizenet/Helper/Backend/Data.php b/app/code/Magento/Authorizenet/Helper/Backend/Data.php index 24bdb2387326..d291125ccae0 100644 --- a/app/code/Magento/Authorizenet/Helper/Backend/Data.php +++ b/app/code/Magento/Authorizenet/Helper/Backend/Data.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Helper\Backend; use Magento\Authorizenet\Helper\Data as FrontendDataHelper; @@ -16,6 +18,7 @@ * * @api * @since 100.0.2 + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class Data extends FrontendDataHelper { diff --git a/app/code/Magento/Authorizenet/Helper/Data.php b/app/code/Magento/Authorizenet/Helper/Data.php index 8bcc1f3f3f03..e240cd692a13 100644 --- a/app/code/Magento/Authorizenet/Helper/Data.php +++ b/app/code/Magento/Authorizenet/Helper/Data.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Helper; use Magento\Framework\App\Helper\AbstractHelper; @@ -17,6 +19,7 @@ * * @api * @since 100.0.2 + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class Data extends AbstractHelper { @@ -153,6 +156,7 @@ public function getSuccessOrderUrl($params) /** * Update all child and parent order's edit increment numbers. + * * Needed for Admin area. * * @param \Magento\Sales\Model\Order $order @@ -255,6 +259,7 @@ protected function getOperation($requestType) /** * Format price with currency sign + * * @param \Magento\Payment\Model\InfoInterface $payment * @param float $amount * @return string diff --git a/app/code/Magento/Authorizenet/Helper/DataFactory.php b/app/code/Magento/Authorizenet/Helper/DataFactory.php index f3ccf16e7d39..71f16ab4af64 100644 --- a/app/code/Magento/Authorizenet/Helper/DataFactory.php +++ b/app/code/Magento/Authorizenet/Helper/DataFactory.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Helper; use Magento\Framework\Exception\LocalizedException; @@ -10,6 +12,7 @@ /** * Class DataFactory + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class DataFactory { diff --git a/app/code/Magento/Authorizenet/Model/Authorizenet.php b/app/code/Magento/Authorizenet/Model/Authorizenet.php index ae9ac833a439..9370b649a23c 100644 --- a/app/code/Magento/Authorizenet/Model/Authorizenet.php +++ b/app/code/Magento/Authorizenet/Model/Authorizenet.php @@ -3,15 +3,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Model; use Magento\Authorizenet\Model\TransactionService; use Magento\Framework\HTTP\ZendClientFactory; /** + * Model for Authorize.net payment method + * * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ abstract class Authorizenet extends \Magento\Payment\Model\Method\Cc { diff --git a/app/code/Magento/Authorizenet/Model/Debug.php b/app/code/Magento/Authorizenet/Model/Debug.php index 255c2e3aba44..93d508cc744e 100644 --- a/app/code/Magento/Authorizenet/Model/Debug.php +++ b/app/code/Magento/Authorizenet/Model/Debug.php @@ -3,9 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Model; /** + * Authorize.net debug payment method model + * * @method string getRequestBody() * @method \Magento\Authorizenet\Model\Debug setRequestBody(string $value) * @method string getResponseBody() @@ -18,10 +22,13 @@ * @method \Magento\Authorizenet\Model\Debug setRequestDump(string $value) * @method string getResultDump() * @method \Magento\Authorizenet\Model\Debug setResultDump(string $value) + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class Debug extends \Magento\Framework\Model\AbstractModel { /** + * Construct debug class + * * @return void */ protected function _construct() diff --git a/app/code/Magento/Authorizenet/Model/Directpost.php b/app/code/Magento/Authorizenet/Model/Directpost.php index d5c11ab54cd9..946ec8ba01a0 100644 --- a/app/code/Magento/Authorizenet/Model/Directpost.php +++ b/app/code/Magento/Authorizenet/Model/Directpost.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Model; use Magento\Framework\App\ObjectManager; @@ -14,6 +16,7 @@ * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class Directpost extends \Magento\Authorizenet\Model\Authorizenet implements TransparentInterface, ConfigInterface { @@ -27,7 +30,7 @@ class Directpost extends \Magento\Authorizenet\Model\Authorizenet implements Tra /** * @var string */ - protected $_infoBlockType = \Magento\Payment\Block\Info::class; + protected $_infoBlockType = \Magento\Authorizenet\Block\Adminhtml\Order\View\Info\PaymentDetails::class; /** * Payment Method feature @@ -371,8 +374,7 @@ public function void(\Magento\Payment\Model\InfoInterface $payment) } /** - * Refund the amount - * Need to decode last 4 digits for request. + * Refund the amount need to decode last 4 digits for request. * * @param \Magento\Framework\DataObject|\Magento\Payment\Model\InfoInterface $payment * @param float $amount @@ -544,15 +546,16 @@ public function setResponseData(array $postData) public function validateResponse() { $response = $this->getResponse(); - //md5 check - if (!$this->getConfigData('trans_md5') - || !$this->getConfigData('login') - || !$response->isValidHash($this->getConfigData('trans_md5'), $this->getConfigData('login')) + $hashConfigKey = !empty($response->getData('x_SHA2_Hash')) ? 'signature_key' : 'trans_md5'; + + //hash check + if (!$response->isValidHash($this->getConfigData($hashConfigKey), $this->getConfigData('login')) ) { throw new \Magento\Framework\Exception\LocalizedException( __('The transaction was declined because the response hash validation failed.') ); } + return true; } @@ -626,6 +629,14 @@ protected function fillPaymentByResponse(\Magento\Framework\DataObject $payment) $payment->setIsTransactionPending(true) ->setIsFraudDetected(true); } + + $additionalInformationKeys = explode(',', $this->getValue('paymentInfoKeys')); + foreach ($additionalInformationKeys as $paymentInfoKey) { + $paymentInfoValue = $response->getDataByKey($paymentInfoKey); + if ($paymentInfoValue !== null) { + $payment->setAdditionalInformation($paymentInfoKey, $paymentInfoValue); + } + } } /** @@ -644,7 +655,7 @@ public function checkResponseCode() case self::RESPONSE_CODE_ERROR: $errorMessage = $this->dataHelper->wrapGatewayError($this->getResponse()->getXResponseReasonText()); $order = $this->getOrderFromResponse(); - $this->paymentFailures->handle((int)$order->getQuoteId(), $errorMessage); + $this->paymentFailures->handle((int)$order->getQuoteId(), (string)$errorMessage); throw new \Magento\Framework\Exception\LocalizedException($errorMessage); default: throw new \Magento\Framework\Exception\LocalizedException( @@ -682,6 +693,7 @@ protected function matchAmount($amount) /** * Operate with order using information from Authorize.net. + * * Authorize order or authorize and capture it. * * @param \Magento\Sales\Model\Order $order @@ -699,6 +711,7 @@ protected function processOrder(\Magento\Sales\Model\Order $order) //decline the order (in case of wrong response code) but don't return money to customer. $message = $e->getMessage(); $this->declineOrder($order, $message, false); + throw $e; } @@ -769,7 +782,7 @@ protected function processPaymentFraudStatus(\Magento\Sales\Model\Order\Payment } /** - * Add status comment + * Add status comment to history * * @param \Magento\Sales\Model\Order\Payment $payment * @return $this @@ -824,6 +837,7 @@ protected function declineOrder(\Magento\Sales\Model\Order $order, $message = '' ->void($response); } $order->registerCancellation($message)->save(); + $this->_eventManager->dispatch('order_cancel_after', ['order' => $order ]); } catch (\Exception $e) { //quiet decline $this->getPsrLogger()->critical($e); @@ -858,7 +872,7 @@ public function getConfigInterface() * Getter for specified value according to set payment method code * * @param mixed $key - * @param null $storeId + * @param mixed $storeId * @return mixed */ public function getValue($key, $storeId = null) @@ -918,10 +932,12 @@ public function fetchTransactionInfo(\Magento\Payment\Model\InfoInterface $payme $payment->setIsTransactionDenied(true); } $this->addStatusCommentOnUpdate($payment, $response, $transactionId); - return []; + return $response->getData(); } /** + * Add status comment on update + * * @param \Magento\Sales\Model\Order\Payment $payment * @param \Magento\Framework\DataObject $response * @param string $transactionId @@ -996,8 +1012,9 @@ protected function getTransactionResponse($transactionId) } /** - * @return \Psr\Log\LoggerInterface + * Get psr logger. * + * @return \Psr\Log\LoggerInterface * @deprecated 100.1.0 */ private function getPsrLogger() @@ -1038,7 +1055,9 @@ private function getOrderIncrementId(): string } /** - * Checks if filter action is Report Only. Transactions that trigger this filter are processed as normal, + * Checks if filter action is Report Only. + * + * Transactions that trigger this filter are processed as normal, * but are also reported in the Merchant Interface as triggering this filter. * * @param string $fdsFilterAction diff --git a/app/code/Magento/Authorizenet/Model/Directpost/Request.php b/app/code/Magento/Authorizenet/Model/Directpost/Request.php index fc78d836b608..10be4cd5febf 100644 --- a/app/code/Magento/Authorizenet/Model/Directpost/Request.php +++ b/app/code/Magento/Authorizenet/Model/Directpost/Request.php @@ -3,13 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Authorizenet\Model\Directpost; use Magento\Authorizenet\Model\Request as AuthorizenetRequest; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Intl\DateTimeFactory; /** * Authorize.net request model for DirectPost model + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class Request extends AuthorizenetRequest { @@ -18,9 +22,35 @@ class Request extends AuthorizenetRequest */ protected $_transKey = null; + /** + * Hexadecimal signature key. + * + * @var string + */ + private $signatureKey = ''; + + /** + * @var DateTimeFactory + */ + private $dateTimeFactory; + + /** + * @param array $data + * @param DateTimeFactory $dateTimeFactory + */ + public function __construct( + array $data = [], + DateTimeFactory $dateTimeFactory = null + ) { + $this->dateTimeFactory = $dateTimeFactory ?? ObjectManager::getInstance() + ->get(DateTimeFactory::class); + parent::__construct($data); + } + /** * Return merchant transaction key. - * Needed to generate sign. + * + * Needed to generate MD5 sign. * * @return string */ @@ -31,7 +61,8 @@ protected function _getTransactionKey() /** * Set merchant transaction key. - * Needed to generate sign. + * + * Needed to generate MD5 sign. * * @param string $transKey * @return $this @@ -43,7 +74,7 @@ protected function _setTransactionKey($transKey) } /** - * Generates the fingerprint for request. + * Generates the MD5 fingerprint for request. * * @param string $merchantApiLoginId * @param string $merchantTransactionKey @@ -63,7 +94,7 @@ public function generateRequestSign( ) { return hash_hmac( "md5", - $merchantApiLoginId . "^" . $fpSequence . "^" . $fpTimestamp . "^" . $amount . "^" . $currencyCode, + $merchantApiLoginId . '^' . $fpSequence . '^' . $fpTimestamp . '^' . $amount . '^' . $currencyCode, $merchantTransactionKey ); } @@ -78,7 +109,7 @@ public function setConstantData(\Magento\Authorizenet\Model\Directpost $paymentM { $this->setXVersion('3.1')->setXDelimData('FALSE')->setXRelayResponse('TRUE'); - $this->setXTestRequest($paymentMethod->getConfigData('test') ? 'TRUE' : 'FALSE'); + $this->setSignatureKey($paymentMethod->getConfigData('signature_key')); $this->setXLogin($paymentMethod->getConfigData('login')) ->setXMethod(\Magento\Authorizenet\Model\Authorizenet::REQUEST_METHOD_CC) @@ -162,23 +193,88 @@ public function setDataFromOrder( /** * Set sign hash into the request object. - * All needed fields should be placed in the object fist. + * + * All needed fields should be placed in the object first. * * @return $this */ public function signRequestData() { - $fpTimestamp = time(); - $hash = $this->generateRequestSign( - $this->getXLogin(), - $this->_getTransactionKey(), - $this->getXAmount(), - $this->getXCurrencyCode(), - $this->getXFpSequence(), - $fpTimestamp - ); + $fpDate = $this->dateTimeFactory->create('now', new \DateTimeZone('UTC')); + $fpTimestamp = $fpDate->getTimestamp(); + + if (!empty($this->getSignatureKey())) { + $hash = $this->generateSha2RequestSign( + (string)$this->getXLogin(), + (string)$this->getSignatureKey(), + (string)$this->getXAmount(), + (string)$this->getXCurrencyCode(), + (string)$this->getXFpSequence(), + $fpTimestamp + ); + } else { + $hash = $this->generateRequestSign( + $this->getXLogin(), + $this->_getTransactionKey(), + $this->getXAmount(), + $this->getXCurrencyCode(), + $this->getXFpSequence(), + $fpTimestamp + ); + } + $this->setXFpTimestamp($fpTimestamp); $this->setXFpHash($hash); + return $this; } + + /** + * Generates the SHA2 fingerprint for request. + * + * @param string $merchantApiLoginId + * @param string $merchantSignatureKey + * @param string $amount + * @param string $currencyCode + * @param string $fpSequence An invoice number or random number. + * @param int $fpTimestamp + * @return string The fingerprint. + */ + private function generateSha2RequestSign( + string $merchantApiLoginId, + string $merchantSignatureKey, + string $amount, + string $currencyCode, + string $fpSequence, + int $fpTimestamp + ): string { + $message = $merchantApiLoginId . '^' . $fpSequence . '^' . $fpTimestamp . '^' . $amount . '^' . $currencyCode; + + return strtoupper(hash_hmac('sha512', $message, pack('H*', $merchantSignatureKey))); + } + + /** + * Return merchant hexadecimal signature key. + * + * Needed to generate SHA2 sign. + * + * @return string + */ + private function getSignatureKey(): string + { + return $this->signatureKey; + } + + /** + * Set merchant hexadecimal signature key. + * + * Needed to generate SHA2 sign. + * + * @param string $signatureKey + * @return void + */ + private function setSignatureKey(string $signatureKey) + { + $this->signatureKey = $signatureKey; + } } diff --git a/app/code/Magento/Authorizenet/Model/Directpost/Request/Factory.php b/app/code/Magento/Authorizenet/Model/Directpost/Request/Factory.php index 2cdd02d7f848..6036935f57be 100644 --- a/app/code/Magento/Authorizenet/Model/Directpost/Request/Factory.php +++ b/app/code/Magento/Authorizenet/Model/Directpost/Request/Factory.php @@ -3,12 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Model\Directpost\Request; use Magento\Authorizenet\Model\Request\Factory as AuthorizenetRequestFactory; /** * Factory class for @see \Magento\Authorizenet\Model\Directpost\Request + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class Factory extends AuthorizenetRequestFactory { diff --git a/app/code/Magento/Authorizenet/Model/Directpost/Response.php b/app/code/Magento/Authorizenet/Model/Directpost/Response.php index dc62c1e990dc..b5604a78cb9c 100644 --- a/app/code/Magento/Authorizenet/Model/Directpost/Response.php +++ b/app/code/Magento/Authorizenet/Model/Directpost/Response.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Model\Directpost; use Magento\Authorizenet\Model\Response as AuthorizenetResponse; @@ -10,6 +12,7 @@ /** * Authorize.net response model for DirectPost model + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class Response extends AuthorizenetResponse { @@ -24,25 +27,31 @@ class Response extends AuthorizenetResponse */ public function generateHash($merchantMd5, $merchantApiLogin, $amount, $transactionId) { - if (!$amount) { - $amount = '0.00'; - } - return strtoupper(md5($merchantMd5 . $merchantApiLogin . $transactionId . $amount)); } /** * Return if is valid order id. * - * @param string $merchantMd5 + * @param string $storedHash * @param string $merchantApiLogin * @return bool */ - public function isValidHash($merchantMd5, $merchantApiLogin) + public function isValidHash($storedHash, $merchantApiLogin) { - $hash = $this->generateHash($merchantMd5, $merchantApiLogin, $this->getXAmount(), $this->getXTransId()); + if (empty($this->getData('x_amount'))) { + $this->setData('x_amount', '0.00'); + } - return Security::compareStrings($hash, $this->getData('x_MD5_Hash')); + if (!empty($this->getData('x_SHA2_Hash'))) { + $hash = $this->generateSha2Hash($storedHash); + return Security::compareStrings($hash, $this->getData('x_SHA2_Hash')); + } elseif (!empty($this->getData('x_MD5_Hash'))) { + $hash = $this->generateHash($storedHash, $merchantApiLogin, $this->getXAmount(), $this->getXTransId()); + return Security::compareStrings($hash, $this->getData('x_MD5_Hash')); + } + + return false; } /** @@ -54,4 +63,54 @@ public function isApproved() { return $this->getXResponseCode() == \Magento\Authorizenet\Model\Directpost::RESPONSE_CODE_APPROVED; } + + /** + * Generates an SHA2 hash to compare against AuthNet's. + * + * @param string $signatureKey + * @return string + * @see https://support.authorize.net/s/article/MD5-Hash-End-of-Life-Signature-Key-Replacement + */ + private function generateSha2Hash(string $signatureKey): string + { + $hashFields = [ + 'x_trans_id', + 'x_test_request', + 'x_response_code', + 'x_auth_code', + 'x_cvv2_resp_code', + 'x_cavv_response', + 'x_avs_code', + 'x_method', + 'x_account_number', + 'x_amount', + 'x_company', + 'x_first_name', + 'x_last_name', + 'x_address', + 'x_city', + 'x_state', + 'x_zip', + 'x_country', + 'x_phone', + 'x_fax', + 'x_email', + 'x_ship_to_company', + 'x_ship_to_first_name', + 'x_ship_to_last_name', + 'x_ship_to_address', + 'x_ship_to_city', + 'x_ship_to_state', + 'x_ship_to_zip', + 'x_ship_to_country', + 'x_invoice_num', + ]; + + $message = '^'; + foreach ($hashFields as $field) { + $message .= ($this->getData($field) ?? '') . '^'; + } + + return strtoupper(hash_hmac('sha512', $message, pack('H*', $signatureKey))); + } } diff --git a/app/code/Magento/Authorizenet/Model/Directpost/Response/Factory.php b/app/code/Magento/Authorizenet/Model/Directpost/Response/Factory.php index c2a24ef386ab..4fda5ac62b49 100644 --- a/app/code/Magento/Authorizenet/Model/Directpost/Response/Factory.php +++ b/app/code/Magento/Authorizenet/Model/Directpost/Response/Factory.php @@ -3,12 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Model\Directpost\Response; use Magento\Authorizenet\Model\Response\Factory as AuthorizenetResponseFactory; /** * Factory class for @see \Magento\Authorizenet\Model\Directpost\Response + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class Factory extends AuthorizenetResponseFactory { diff --git a/app/code/Magento/Authorizenet/Model/Directpost/Session.php b/app/code/Magento/Authorizenet/Model/Directpost/Session.php index 7ddedac16139..26c5ff0cb7e3 100644 --- a/app/code/Magento/Authorizenet/Model/Directpost/Session.php +++ b/app/code/Magento/Authorizenet/Model/Directpost/Session.php @@ -3,12 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Model\Directpost; use Magento\Framework\Session\SessionManager; /** * Authorize.net DirectPost session model + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class Session extends SessionManager { diff --git a/app/code/Magento/Authorizenet/Model/Request.php b/app/code/Magento/Authorizenet/Model/Request.php index dc52f84baece..552439fc8bb9 100644 --- a/app/code/Magento/Authorizenet/Model/Request.php +++ b/app/code/Magento/Authorizenet/Model/Request.php @@ -3,12 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Model; use Magento\Framework\DataObject; /** * Request object + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class Request extends DataObject { diff --git a/app/code/Magento/Authorizenet/Model/Request/Factory.php b/app/code/Magento/Authorizenet/Model/Request/Factory.php index e60bbd0c88e8..a7a636280e28 100644 --- a/app/code/Magento/Authorizenet/Model/Request/Factory.php +++ b/app/code/Magento/Authorizenet/Model/Request/Factory.php @@ -3,10 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Model\Request; /** * Factory class for @see \Magento\Authorizenet\Model\Request + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class Factory { diff --git a/app/code/Magento/Authorizenet/Model/ResourceModel/Debug.php b/app/code/Magento/Authorizenet/Model/ResourceModel/Debug.php index ee6ec6783bb0..2c21d0e2e28e 100644 --- a/app/code/Magento/Authorizenet/Model/ResourceModel/Debug.php +++ b/app/code/Magento/Authorizenet/Model/ResourceModel/Debug.php @@ -3,10 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Model\ResourceModel; /** * Resource Authorize.net debug model + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class Debug extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb { diff --git a/app/code/Magento/Authorizenet/Model/ResourceModel/Debug/Collection.php b/app/code/Magento/Authorizenet/Model/ResourceModel/Debug/Collection.php index 095ac5a91dd7..b84ee1e72a2d 100644 --- a/app/code/Magento/Authorizenet/Model/ResourceModel/Debug/Collection.php +++ b/app/code/Magento/Authorizenet/Model/ResourceModel/Debug/Collection.php @@ -3,10 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Model\ResourceModel\Debug; /** * Resource Authorize.net debug collection model + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection { diff --git a/app/code/Magento/Authorizenet/Model/Response.php b/app/code/Magento/Authorizenet/Model/Response.php index 52b43c251dca..c552663a1537 100644 --- a/app/code/Magento/Authorizenet/Model/Response.php +++ b/app/code/Magento/Authorizenet/Model/Response.php @@ -3,12 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Model; use Magento\Framework\DataObject; /** * Response object + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class Response extends DataObject { diff --git a/app/code/Magento/Authorizenet/Model/Response/Factory.php b/app/code/Magento/Authorizenet/Model/Response/Factory.php index 74bf8953471d..457809556600 100644 --- a/app/code/Magento/Authorizenet/Model/Response/Factory.php +++ b/app/code/Magento/Authorizenet/Model/Response/Factory.php @@ -3,10 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Model\Response; /** * Factory class for @see \Magento\Authorizenet\Model\Response + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class Factory { diff --git a/app/code/Magento/Authorizenet/Model/Source/Cctype.php b/app/code/Magento/Authorizenet/Model/Source/Cctype.php index 0a5fe8ab9b34..ffb358472245 100644 --- a/app/code/Magento/Authorizenet/Model/Source/Cctype.php +++ b/app/code/Magento/Authorizenet/Model/Source/Cctype.php @@ -3,16 +3,21 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Model\Source; use Magento\Payment\Model\Source\Cctype as PaymentCctype; /** * Authorize.net Payment CC Types Source Model + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class Cctype extends PaymentCctype { /** + * Return all supported credit card types + * * @return string[] */ public function getAllowedTypes() diff --git a/app/code/Magento/Authorizenet/Model/Source/PaymentAction.php b/app/code/Magento/Authorizenet/Model/Source/PaymentAction.php index 9943e1001da5..c6e57557f65c 100644 --- a/app/code/Magento/Authorizenet/Model/Source/PaymentAction.php +++ b/app/code/Magento/Authorizenet/Model/Source/PaymentAction.php @@ -3,18 +3,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Model\Source; use Magento\Framework\Option\ArrayInterface; /** - * * Authorize.net Payment Action Dropdown source + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class PaymentAction implements ArrayInterface { /** - * {@inheritdoc} + * @inheritdoc */ public function toOptionArray() { diff --git a/app/code/Magento/Authorizenet/Model/TransactionService.php b/app/code/Magento/Authorizenet/Model/TransactionService.php index 693a5b890fab..af0b02e94cf4 100644 --- a/app/code/Magento/Authorizenet/Model/TransactionService.php +++ b/app/code/Magento/Authorizenet/Model/TransactionService.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Authorizenet\Model; @@ -15,7 +16,7 @@ /** * Class TransactionService - * @package Magento\Authorizenet\Model + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method */ class TransactionService { @@ -74,6 +75,7 @@ public function __construct( /** * Get transaction information + * * @param \Magento\Authorizenet\Model\Authorizenet $context * @param string $transactionId * @return \Magento\Framework\Simplexml\Element @@ -142,6 +144,7 @@ protected function loadTransactionDetails(Authorizenet $context, $transactionId) /** * Create request body to get transaction details + * * @param string $login * @param string $transactionKey * @param string $transactionId diff --git a/app/code/Magento/Authorizenet/Observer/AddFieldsToResponseObserver.php b/app/code/Magento/Authorizenet/Observer/AddFieldsToResponseObserver.php index 03846dddfdee..bdd10437927c 100644 --- a/app/code/Magento/Authorizenet/Observer/AddFieldsToResponseObserver.php +++ b/app/code/Magento/Authorizenet/Observer/AddFieldsToResponseObserver.php @@ -3,11 +3,19 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Observer; use Magento\Framework\Event\ObserverInterface; use Magento\Sales\Model\Order; +/** + * Class AddFieldsToResponseObserver + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method + */ class AddFieldsToResponseObserver implements ObserverInterface { /** diff --git a/app/code/Magento/Authorizenet/Observer/SaveOrderAfterSubmitObserver.php b/app/code/Magento/Authorizenet/Observer/SaveOrderAfterSubmitObserver.php index 8426d004c203..45f0adfa96f4 100644 --- a/app/code/Magento/Authorizenet/Observer/SaveOrderAfterSubmitObserver.php +++ b/app/code/Magento/Authorizenet/Observer/SaveOrderAfterSubmitObserver.php @@ -3,11 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Observer; use Magento\Framework\Event\ObserverInterface; use Magento\Sales\Model\Order; +/** + * Class SaveOrderAfterSubmitObserver + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method + */ class SaveOrderAfterSubmitObserver implements ObserverInterface { /** diff --git a/app/code/Magento/Authorizenet/Observer/UpdateAllEditIncrementsObserver.php b/app/code/Magento/Authorizenet/Observer/UpdateAllEditIncrementsObserver.php index 3e62fe2278d3..d6cc51eb63c0 100644 --- a/app/code/Magento/Authorizenet/Observer/UpdateAllEditIncrementsObserver.php +++ b/app/code/Magento/Authorizenet/Observer/UpdateAllEditIncrementsObserver.php @@ -3,11 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Authorizenet\Observer; use Magento\Framework\Event\ObserverInterface; use Magento\Sales\Model\Order; +/** + * Class UpdateAllEditIncrementsObserver + * @deprecated 2.3.1 Authorize.net is removing all support for this payment method + */ class UpdateAllEditIncrementsObserver implements ObserverInterface { /** diff --git a/app/code/Magento/Authorizenet/Test/Unit/Model/Directpost/RequestTest.php b/app/code/Magento/Authorizenet/Test/Unit/Model/Directpost/RequestTest.php new file mode 100644 index 000000000000..94d8f3a0d27a --- /dev/null +++ b/app/code/Magento/Authorizenet/Test/Unit/Model/Directpost/RequestTest.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Authorizenet\Test\Unit\Model\Directpost; + +use Magento\Authorizenet\Model\Directpost\Request; +use Magento\Framework\Intl\DateTimeFactory; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + +class RequestTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var DateTimeFactory|MockObject + */ + private $dateTimeFactory; + + /** + * @var Request + */ + private $requestModel; + + protected function setUp() + { + $this->dateTimeFactory = $this->getMockBuilder(DateTimeFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $dateTime = new \DateTime('2016-07-05 00:00:00', new \DateTimeZone('UTC')); + $this->dateTimeFactory->method('create') + ->willReturn($dateTime); + + $this->requestModel = new Request([], $this->dateTimeFactory); + } + + /** + * @param string $signatureKey + * @param string $expectedHash + * @dataProvider signRequestDataProvider + */ + public function testSignRequestData(string $signatureKey, string $expectedHash) + { + /** @var \Magento\Authorizenet\Model\Directpost $paymentMethod */ + $paymentMethod = $this->createMock(\Magento\Authorizenet\Model\Directpost::class); + $paymentMethod->method('getConfigData') + ->willReturnMap( + [ + ['test', null, true], + ['login', null, 'login'], + ['trans_key', null, 'trans_key'], + ['signature_key', null, $signatureKey], + ] + ); + + $this->requestModel->setConstantData($paymentMethod); + $this->requestModel->signRequestData(); + $signHash = $this->requestModel->getXFpHash(); + + $this->assertEquals($expectedHash, $signHash); + } + + /** + * @return array + */ + public function signRequestDataProvider() + { + return [ + [ + 'signatureKey' => '3EAFCE5697C1B4B9748385C1FCD29D86F3B9B41C7EED85A3A01DFF65' . + '70C8C29373C2A153355C3313CDF4AF723C0036DBF244A0821713A910024EE85547CEF37F', + 'expectedHash' => '719ED94DF5CF3510CB5531E8115462C8F12CBCC8E917BD809E8D40B4FF06' . + '1E14953554403DD9813CCCE0F31B184EB4DEF558E9C0747505A0C25420372DB00BE1' + ], + [ + 'signatureKey' => '', + 'expectedHash' => '3656211f2c41d1e4c083606f326c0460' + ], + ]; + } +} diff --git a/app/code/Magento/Authorizenet/Test/Unit/Model/Directpost/ResponseTest.php b/app/code/Magento/Authorizenet/Test/Unit/Model/Directpost/ResponseTest.php index 15c7eecb09a6..ff4aa8b5ee36 100644 --- a/app/code/Magento/Authorizenet/Test/Unit/Model/Directpost/ResponseTest.php +++ b/app/code/Magento/Authorizenet/Test/Unit/Model/Directpost/ResponseTest.php @@ -13,53 +13,16 @@ class ResponseTest extends \PHPUnit\Framework\TestCase /** * @var \Magento\Authorizenet\Model\Directpost\Response */ - protected $responseModel; + private $responseModel; protected function setUp() { $objectManager = new ObjectManager($this); - $this->responseModel = $objectManager->getObject(\Magento\Authorizenet\Model\Directpost\Response::class); - } - - /** - * @param string $merchantMd5 - * @param string $merchantApiLogin - * @param float|null $amount - * @param float|string $amountTestFunc - * @param string $transactionId - * @dataProvider generateHashDataProvider - */ - public function testGenerateHash($merchantMd5, $merchantApiLogin, $amount, $amountTestFunc, $transactionId) - { - $this->assertEquals( - $this->generateHash($merchantMd5, $merchantApiLogin, $amountTestFunc, $transactionId), - $this->responseModel->generateHash($merchantMd5, $merchantApiLogin, $amount, $transactionId) + $this->responseModel = $objectManager->getObject( + \Magento\Authorizenet\Model\Directpost\Response::class ); } - /** - * @return array - */ - public function generateHashDataProvider() - { - return [ - [ - 'merchantMd5' => 'FCD7F001E9274FDEFB14BFF91C799306', - 'merchantApiLogin' => 'Magento', - 'amount' => null, - 'amountTestFunc' => '0.00', - 'transactionId' => '1' - ], - [ - 'merchantMd5' => '8AEF4E508261A287C3E2F544720FCA3A', - 'merchantApiLogin' => 'Magento2', - 'amount' => 100.50, - 'amountTestFunc' => 100.50, - 'transactionId' => '2' - ] - ]; - } - /** * @param $merchantMd5 * @param $merchantApiLogin @@ -73,7 +36,8 @@ protected function generateHash($merchantMd5, $merchantApiLogin, $amount, $trans } /** - * @param string $merchantMd5 + * @param string $storedHash + * @param string $hashKey * @param string $merchantApiLogin * @param float|null $amount * @param string $transactionId @@ -81,12 +45,21 @@ protected function generateHash($merchantMd5, $merchantApiLogin, $amount, $trans * @param bool $expectedValue * @dataProvider isValidHashDataProvider */ - public function testIsValidHash($merchantMd5, $merchantApiLogin, $amount, $transactionId, $hash, $expectedValue) - { + public function testIsValidHash( + string $storedHash, + string $hashKey, + string $merchantApiLogin, + $amount, + string $transactionId, + string $hash, + bool $expectedValue + ) { $this->responseModel->setXAmount($amount); $this->responseModel->setXTransId($transactionId); - $this->responseModel->setData('x_MD5_Hash', $hash); - $this->assertEquals($expectedValue, $this->responseModel->isValidHash($merchantMd5, $merchantApiLogin)); + $this->responseModel->setData($hashKey, $hash); + $result = $this->responseModel->isValidHash($storedHash, $merchantApiLogin); + + $this->assertEquals($expectedValue, $result); } /** @@ -94,9 +67,14 @@ public function testIsValidHash($merchantMd5, $merchantApiLogin, $amount, $trans */ public function isValidHashDataProvider() { + $signatureKey = '3EAFCE5697C1B4B9748385C1FCD29D86F3B9B41C7EED85A3A01DFF6570C8C' . + '29373C2A153355C3313CDF4AF723C0036DBF244A0821713A910024EE85547CEF37F'; + $expectedSha2Hash = '368D48E0CD1274BF41C059138DA69985594021A4AD5B4C5526AE88C8F' . + '7C5769B13C5E1E4358900F3E51076FB69D14B0A797904C22E8A11A52AA49CDE5FBB703C'; return [ [ 'merchantMd5' => 'FCD7F001E9274FDEFB14BFF91C799306', + 'hashKey' => 'x_MD5_Hash', 'merchantApiLogin' => 'Magento', 'amount' => null, 'transactionId' => '1', @@ -105,11 +83,21 @@ public function isValidHashDataProvider() ], [ 'merchantMd5' => '8AEF4E508261A287C3E2F544720FCA3A', + 'hashKey' => 'x_MD5_Hash', 'merchantApiLogin' => 'Magento2', 'amount' => 100.50, 'transactionId' => '2', 'hash' => '1F24A4EC9A169B2B2A072A5F168E16DC', 'expectedValue' => false + ], + [ + 'signatureKey' => $signatureKey, + 'hashKey' => 'x_SHA2_Hash', + 'merchantApiLogin' => 'Magento2', + 'amount' => 100.50, + 'transactionId' => '2', + 'hash' => $expectedSha2Hash, + 'expectedValue' => true ] ]; } diff --git a/app/code/Magento/Authorizenet/etc/adminhtml/system.xml b/app/code/Magento/Authorizenet/etc/adminhtml/system.xml index 1319fa102d0d..fc86c0d2dc68 100644 --- a/app/code/Magento/Authorizenet/etc/adminhtml/system.xml +++ b/app/code/Magento/Authorizenet/etc/adminhtml/system.xml @@ -9,7 +9,7 @@ <system> <section id="payment"> <group id="authorizenet_directpost" translate="label" type="text" sortOrder="34" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Authorize.net Direct Post</label> + <label>Authorize.Net Direct Post (Deprecated)</label> <field id="active" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Enabled</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> @@ -29,6 +29,10 @@ <label>Transaction Key</label> <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> </field> + <field id="signature_key" translate="label" type="obscure" sortOrder="55" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Signature Key</label> + <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> + </field> <field id="trans_md5" translate="label" type="obscure" sortOrder="60" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Merchant MD5</label> <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> diff --git a/app/code/Magento/Authorizenet/etc/config.xml b/app/code/Magento/Authorizenet/etc/config.xml index eacf77cda1e7..60356460f553 100644 --- a/app/code/Magento/Authorizenet/etc/config.xml +++ b/app/code/Magento/Authorizenet/etc/config.xml @@ -19,9 +19,10 @@ <order_status>processing</order_status> <payment_action>authorize</payment_action> <test>1</test> - <title>Credit Card Direct Post (Authorize.net) + Credit Card Direct Post (Authorize.Net) + 0 USD 1 @@ -32,6 +33,8 @@ https://secure.authorize.net/gateway/transact.dll https://apitest.authorize.net/xml/v1/request.api https://api2.authorize.net/xml/v1/request.api + x_card_type,x_account_number,x_avs_code,x_auth_code,x_response_reason_text,x_cvv2_resp_code + authorizenet diff --git a/app/code/Magento/Authorizenet/etc/di.xml b/app/code/Magento/Authorizenet/etc/di.xml index 4beb2456be11..69d24019f2fb 100644 --- a/app/code/Magento/Authorizenet/etc/di.xml +++ b/app/code/Magento/Authorizenet/etc/di.xml @@ -35,4 +35,9 @@ + + + Magento\Authorizenet\Model\Directpost + + diff --git a/app/code/Magento/Authorizenet/etc/payment.xml b/app/code/Magento/Authorizenet/etc/payment.xml new file mode 100644 index 000000000000..1d2cac374d8d --- /dev/null +++ b/app/code/Magento/Authorizenet/etc/payment.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/app/code/Magento/Authorizenet/i18n/en_US.csv b/app/code/Magento/Authorizenet/i18n/en_US.csv index bb59afffff2c..d724bd960d31 100644 --- a/app/code/Magento/Authorizenet/i18n/en_US.csv +++ b/app/code/Magento/Authorizenet/i18n/en_US.csv @@ -45,7 +45,7 @@ void,void "Fraud Filters","Fraud Filters" "Place Order","Place Order" "Sorry, but something went wrong. Please contact the seller.","Sorry, but something went wrong. Please contact the seller." -"Authorize.net Direct Post","Authorize.net Direct Post" +"Authorize.Net Direct Post (Deprecated)","Authorize.Net Direct Post (Deprecated)" Enabled,Enabled "Payment Action","Payment Action" Title,Title @@ -67,3 +67,9 @@ Debug,Debug "Minimum Order Total","Minimum Order Total" "Maximum Order Total","Maximum Order Total" "Sort Order","Sort Order" +"x_card_type","Credit Card Type" +"x_account_number", "Credit Card Number" +"x_avs_code","AVS Response Code" +"x_auth_code","Processor Authentication Code" +"x_response_reason_text","Processor Response Text" +"x_cvv2_resp_code","CVV2 Response Code" diff --git a/app/code/Magento/Authorizenet/view/adminhtml/templates/order/view/info/fraud_details.phtml b/app/code/Magento/Authorizenet/view/adminhtml/templates/order/view/info/fraud_details.phtml index 60fec263352f..ac91fa30bfbe 100644 --- a/app/code/Magento/Authorizenet/view/adminhtml/templates/order/view/info/fraud_details.phtml +++ b/app/code/Magento/Authorizenet/view/adminhtml/templates/order/view/info/fraud_details.phtml @@ -44,8 +44,8 @@ $fraudDetails = $payment->getAdditionalInformation('fraud_details'); - escapeHtml(__('Fraud Filters')) ?>: -
+ escapeHtml(__('Fraud Filters')) ?>: +
escapeHtml($filter['name']) ?>: escapeHtml($filter['action']) ?> diff --git a/app/code/Magento/Authorizenet/view/frontend/requirejs-config.js b/app/code/Magento/Authorizenet/view/frontend/requirejs-config.js index 8edc38dce6f6..8c4c90bf111d 100644 --- a/app/code/Magento/Authorizenet/view/frontend/requirejs-config.js +++ b/app/code/Magento/Authorizenet/view/frontend/requirejs-config.js @@ -6,7 +6,8 @@ var config = { map: { '*': { - transparent: 'Magento_Payment/transparent' + transparent: 'Magento_Payment/js/transparent', + 'Magento_Payment/transparent': 'Magento_Payment/js/transparent' } } }; diff --git a/app/code/Magento/AuthorizenetAcceptjs/Block/Form.php b/app/code/Magento/AuthorizenetAcceptjs/Block/Form.php new file mode 100644 index 000000000000..9f10b2df40e9 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Block/Form.php @@ -0,0 +1,62 @@ +config = $config; + $this->sessionQuote = $sessionQuote; + } + + /** + * Check if cvv validation is available + * + * @return boolean + */ + public function isCvvEnabled(): bool + { + return $this->config->isCvvEnabled($this->sessionQuote->getStoreId()); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Block/Info.php b/app/code/Magento/AuthorizenetAcceptjs/Block/Info.php new file mode 100644 index 000000000000..ea476eaa5571 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Block/Info.php @@ -0,0 +1,31 @@ +config = $config; + $this->json = $json; + } + + /** + * Retrieves the config that should be used by the block + * + * @return string + */ + public function getPaymentConfig(): string + { + $payment = $this->config->getConfig()['payment']; + $config = $payment[$this->getMethodCode()]; + $config['code'] = $this->getMethodCode(); + + return $this->json->serialize($config); + } + + /** + * Returns the method code for this payment method + * + * @return string + */ + public function getMethodCode(): string + { + return Config::METHOD; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/AcceptPaymentStrategyCommand.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/AcceptPaymentStrategyCommand.php new file mode 100644 index 000000000000..a72435644d23 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/AcceptPaymentStrategyCommand.php @@ -0,0 +1,74 @@ +commandPool = $commandPool; + $this->subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function execute(array $commandSubject): void + { + if ($this->shouldAcceptInGateway($commandSubject)) { + $this->commandPool->get(self::ACCEPT_FDS) + ->execute($commandSubject); + } + } + + /** + * Determines if the transaction needs to be accepted in the gateway + * + * @param array $commandSubject + * @return bool + * @throws CommandException + */ + private function shouldAcceptInGateway(array $commandSubject): bool + { + $details = $this->commandPool->get('get_transaction_details') + ->execute($commandSubject) + ->get(); + + return in_array($details['transaction']['transactionStatus'], self::NEEDS_APPROVAL_STATUSES); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/CaptureStrategyCommand.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/CaptureStrategyCommand.php new file mode 100644 index 000000000000..a4d895d4daae --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/CaptureStrategyCommand.php @@ -0,0 +1,140 @@ +commandPool = $commandPool; + $this->transactionRepository = $repository; + $this->filterBuilder = $filterBuilder; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function execute(array $commandSubject): void + { + /** @var PaymentDataObjectInterface $paymentDO */ + $paymentDO = $this->subjectReader->readPayment($commandSubject); + + $command = $this->getCommand($paymentDO); + $this->commandPool->get($command) + ->execute($commandSubject); + } + + /** + * Get execution command name. + * + * @param PaymentDataObjectInterface $paymentDO + * @return string + */ + private function getCommand(PaymentDataObjectInterface $paymentDO): string + { + $payment = $paymentDO->getPayment(); + ContextHelper::assertOrderPayment($payment); + + // If auth transaction does not exist then execute authorize&capture command + $captureExists = $this->captureTransactionExists($payment); + if (!$payment->getAuthorizationTransaction() && !$captureExists) { + return self::SALE; + } + + return self::CAPTURE; + } + + /** + * Check if capture transaction already exists + * + * @param OrderPaymentInterface $payment + * @return bool + */ + private function captureTransactionExists(OrderPaymentInterface $payment): bool + { + $this->searchCriteriaBuilder->addFilters( + [ + $this->filterBuilder + ->setField('payment_id') + ->setValue($payment->getId()) + ->create(), + ] + ); + + $this->searchCriteriaBuilder->addFilters( + [ + $this->filterBuilder + ->setField('txn_type') + ->setValue(TransactionInterface::TYPE_CAPTURE) + ->create(), + ] + ); + + $searchCriteria = $this->searchCriteriaBuilder->create(); + $count = $this->transactionRepository->getList($searchCriteria) + ->getTotalCount(); + + return $count > 0; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/FetchTransactionInfoCommand.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/FetchTransactionInfoCommand.php new file mode 100644 index 000000000000..bb9e7c26a45b --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/FetchTransactionInfoCommand.php @@ -0,0 +1,87 @@ +commandPool = $commandPool; + $this->subjectReader = $subjectReader; + $this->config = $config; + $this->handler = $handler; + } + + /** + * @inheritdoc + */ + public function execute(array $commandSubject): array + { + $paymentDO = $this->subjectReader->readPayment($commandSubject); + $order = $paymentDO->getOrder(); + + $command = $this->commandPool->get('get_transaction_details'); + $result = $command->execute($commandSubject); + $response = $result->get(); + + if ($this->handler) { + $this->handler->handle($commandSubject, $response); + } + + $additionalInformationKeys = $this->config->getTransactionInfoSyncKeys($order->getStoreId()); + $rawDetails = []; + foreach ($additionalInformationKeys as $key) { + if (isset($response['transaction'][$key])) { + $rawDetails[$key] = $response['transaction'][$key]; + } + } + + return $rawDetails; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/GatewayQueryCommand.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/GatewayQueryCommand.php new file mode 100644 index 000000000000..f8975ef38eed --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/GatewayQueryCommand.php @@ -0,0 +1,100 @@ +requestBuilder = $requestBuilder; + $this->transferFactory = $transferFactory; + $this->client = $client; + $this->validator = $validator; + $this->logger = $logger; + } + + /** + * @inheritdoc + * + * @throws Exception + */ + public function execute(array $commandSubject): ResultInterface + { + $transferO = $this->transferFactory->create( + $this->requestBuilder->build($commandSubject) + ); + + try { + $response = $this->client->placeRequest($transferO); + } catch (Exception $e) { + $this->logger->critical($e); + + throw new CommandException(__('There was an error while trying to process the request.')); + } + + $result = $this->validator->validate( + array_merge($commandSubject, ['response' => $response]) + ); + if (!$result->isValid()) { + throw new CommandException(__('There was an error while trying to process the request.')); + } + + return new ArrayResult($response); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/RefundTransactionStrategyCommand.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/RefundTransactionStrategyCommand.php new file mode 100644 index 000000000000..53a1f13fa878 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Command/RefundTransactionStrategyCommand.php @@ -0,0 +1,77 @@ +commandPool = $commandPool; + $this->subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function execute(array $commandSubject): void + { + $command = $this->getCommand($commandSubject); + + $this->commandPool->get($command) + ->execute($commandSubject); + } + + /** + * Determines the command that should be used based on the status of the transaction + * + * @param array $commandSubject + * @return string + * @throws CommandException + */ + private function getCommand(array $commandSubject): string + { + $details = $this->commandPool->get('get_transaction_details') + ->execute($commandSubject) + ->get(); + + if ($details['transaction']['transactionStatus'] === 'capturedPendingSettlement') { + return self::VOID; + } elseif ($details['transaction']['transactionStatus'] !== 'settledSuccessfully') { + throw new CommandException(__('This transaction cannot be refunded with its current status.')); + } + + return self::REFUND; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Config.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Config.php new file mode 100644 index 000000000000..2a28945d9835 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Config.php @@ -0,0 +1,199 @@ +getValue(Config::KEY_LOGIN_ID, $storeId); + } + + /** + * Gets the current environment + * + * @param int|null $storeId + * @return string + */ + public function getEnvironment($storeId = null): string + { + return $this->getValue(Config::KEY_ENVIRONMENT, $storeId); + } + + /** + * Gets the transaction key + * + * @param int|null $storeId + * @return string + */ + public function getTransactionKey($storeId = null): ?string + { + return $this->getValue(Config::KEY_TRANSACTION_KEY, $storeId); + } + + /** + * Gets the API endpoint URL + * + * @param int|null $storeId + * @return string + */ + public function getApiUrl($storeId = null): string + { + $environment = $this->getValue(Config::KEY_ENVIRONMENT, $storeId); + + return $environment === Environment::ENVIRONMENT_SANDBOX + ? self::ENDPOINT_URL_SANDBOX + : self::ENDPOINT_URL_PRODUCTION; + } + + /** + * Gets the configured signature key + * + * @param int|null $storeId + * @return string + */ + public function getTransactionSignatureKey($storeId = null): ?string + { + return $this->getValue(Config::KEY_SIGNATURE_KEY, $storeId); + } + + /** + * Gets the configured legacy transaction hash + * + * @param int|null $storeId + * @return string + */ + public function getLegacyTransactionHash($storeId = null): ?string + { + return $this->getValue(Config::KEY_LEGACY_TRANSACTION_HASH, $storeId); + } + + /** + * Gets the configured payment action + * + * @param int|null $storeId + * @return string + */ + public function getPaymentAction($storeId = null): ?string + { + return $this->getValue(Config::KEY_PAYMENT_ACTION, $storeId); + } + + /** + * Gets the configured client key + * + * @param int|null $storeId + * @return string + */ + public function getClientKey($storeId = null): ?string + { + return $this->getValue(Config::KEY_CLIENT_KEY, $storeId); + } + + /** + * Should authorize.net email the customer their receipt. + * + * @param int|null $storeId + * @return bool + */ + public function shouldEmailCustomer($storeId = null): bool + { + return (bool)$this->getValue(Config::KEY_SHOULD_EMAIL_CUSTOMER, $storeId); + } + + /** + * Should the cvv field be shown + * + * @param int|null $storeId + * @return bool + */ + public function isCvvEnabled($storeId = null): bool + { + return (bool)$this->getValue(Config::KEY_CVV_ENABLED, $storeId); + } + + /** + * Retrieves the solution id for the given store based on environment + * + * @param int|null $storeId + * @return string + */ + public function getSolutionId($storeId = null): ?string + { + $environment = $this->getValue(Config::KEY_ENVIRONMENT, $storeId); + + return $environment === Environment::ENVIRONMENT_SANDBOX + ? self::SOLUTION_ID_SANDBOX + : self::SOLUTION_ID_PRODUCTION; + } + + /** + * Returns the keys to be pulled from the transaction and displayed + * + * @param int|null $storeId + * @return string[] + */ + public function getAdditionalInfoKeys($storeId = null): array + { + return explode(',', $this->getValue(Config::KEY_ADDITIONAL_INFO_KEYS, $storeId) ?? ''); + } + + /** + * Returns the keys to be pulled from the transaction and displayed when syncing the transaction + * + * @param int|null $storeId + * @return string[] + */ + public function getTransactionInfoSyncKeys($storeId = null): array + { + return explode(',', $this->getValue(Config::KEY_TRANSACTION_SYNC_KEYS, $storeId) ?? ''); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/Client.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/Client.php new file mode 100644 index 000000000000..1b2efbb85721 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/Client.php @@ -0,0 +1,126 @@ +httpClientFactory = $httpClientFactory; + $this->config = $config; + $this->paymentLogger = $paymentLogger; + $this->logger = $logger; + $this->json = $json; + } + + /** + * Places request to gateway. Returns result as ENV array + * + * @param TransferInterface $transferObject + * @return array + * @throws \Magento\Payment\Gateway\Http\ClientException + */ + public function placeRequest(TransferInterface $transferObject) + { + $request = $transferObject->getBody(); + $log = [ + 'request' => $request, + ]; + $client = $this->httpClientFactory->create(); + $url = $this->config->getApiUrl(); + + $type = $request['payload_type']; + unset($request['payload_type']); + $request = [$type => $request]; + + try { + $client->setUri($url); + $client->setConfig(['maxredirects' => 0, 'timeout' => 30]); + $client->setRawData($this->json->serialize($request), 'application/json'); + $client->setMethod(ZendClient::POST); + + $responseBody = $client->request() + ->getBody(); + + // Strip BOM because Authorize.net sends it in the response + if ($responseBody && substr($responseBody, 0, 3) === pack('CCC', 0xef, 0xbb, 0xbf)) { + $responseBody = substr($responseBody, 3); + } + + $log['response'] = $responseBody; + + try { + $data = $this->json->unserialize($responseBody); + } catch (InvalidArgumentException $e) { + throw new \Exception('Invalid JSON was returned by the gateway'); + } + + return $data; + } catch (\Exception $e) { + $this->logger->critical($e); + + throw new ClientException( + __('Something went wrong in the payment gateway.') + ); + } finally { + $this->paymentLogger->debug($log); + } + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/Payload/Filter/RemoveFieldsFilter.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/Payload/Filter/RemoveFieldsFilter.php new file mode 100644 index 000000000000..a23397c09189 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/Payload/Filter/RemoveFieldsFilter.php @@ -0,0 +1,42 @@ +fields = $fields; + } + + /** + * @inheritdoc + */ + public function filter(array $data): array + { + foreach ($this->fields as $field) { + unset($data[$field]); + } + + return $data; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/Payload/FilterInterface.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/Payload/FilterInterface.php new file mode 100644 index 000000000000..35e563eacb0c --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Http/Payload/FilterInterface.php @@ -0,0 +1,23 @@ +transferBuilder = $transferBuilder; + $this->payloadFilters = $payloadFilters; + } + + /** + * Builds gateway transfer object + * + * @param array $request + * @return TransferInterface + */ + public function create(array $request) + { + foreach ($this->payloadFilters as $filter) { + $request = $filter->filter($request); + } + + return $this->transferBuilder + ->setBody($request) + ->build(); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AcceptFdsDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AcceptFdsDataBuilder.php new file mode 100644 index 000000000000..6883d63397be --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AcceptFdsDataBuilder.php @@ -0,0 +1,65 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $payment = $paymentDO->getPayment(); + $data = []; + + if ($payment instanceof Payment) { + $authorizationTransaction = $payment->getAuthorizationTransaction(); + + if (empty($authorizationTransaction)) { + $transactionId = $payment->getLastTransId(); + } else { + $transactionId = $authorizationTransaction->getParentTxnId(); + + if (empty($transactionId)) { + $transactionId = $authorizationTransaction->getTxnId(); + } + } + + $data = [ + 'heldTransactionRequest' => [ + 'action' => 'approve', + 'refTransId' => $transactionId, + ] + ]; + } + + return $data; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AddressDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AddressDataBuilder.php new file mode 100644 index 000000000000..e9c42e864440 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AddressDataBuilder.php @@ -0,0 +1,77 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $order = $paymentDO->getOrder(); + $billingAddress = $order->getBillingAddress(); + $shippingAddress = $order->getShippingAddress(); + $result = [ + 'transactionRequest' => [] + ]; + + if ($billingAddress) { + $result['transactionRequest']['billTo'] = [ + 'firstName' => $billingAddress->getFirstname(), + 'lastName' => $billingAddress->getLastname(), + 'company' => $billingAddress->getCompany() ?? '', + 'address' => $billingAddress->getStreetLine1(), + 'city' => $billingAddress->getCity(), + 'state' => $billingAddress->getRegionCode(), + 'zip' => $billingAddress->getPostcode(), + 'country' => $billingAddress->getCountryId() + ]; + } + + if ($shippingAddress) { + $result['transactionRequest']['shipTo'] = [ + 'firstName' => $shippingAddress->getFirstname(), + 'lastName' => $shippingAddress->getLastname(), + 'company' => $shippingAddress->getCompany() ?? '', + 'address' => $shippingAddress->getStreetLine1(), + 'city' => $shippingAddress->getCity(), + 'state' => $shippingAddress->getRegionCode(), + 'zip' => $shippingAddress->getPostcode(), + 'country' => $shippingAddress->getCountryId() + ]; + } + + if ($order->getRemoteIp()) { + $result['transactionRequest']['customerIP'] = $order->getRemoteIp(); + } + + return $result; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AmountDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AmountDataBuilder.php new file mode 100644 index 000000000000..601c329fe4f7 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AmountDataBuilder.php @@ -0,0 +1,46 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + return [ + 'transactionRequest' => [ + 'amount' => $this->formatPrice($this->subjectReader->readAmount($buildSubject)), + ] + ]; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AuthenticationDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AuthenticationDataBuilder.php new file mode 100644 index 000000000000..2387ab0ab89f --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AuthenticationDataBuilder.php @@ -0,0 +1,59 @@ +subjectReader = $subjectReader; + $this->config = $config; + } + + /** + * Adds authentication information to the request + * + * @param array $buildSubject + * @return array + */ + public function build(array $buildSubject): array + { + $storeId = $this->subjectReader->readStoreId($buildSubject); + + return [ + 'merchantAuthentication' => [ + 'name' => $this->config->getLoginId($storeId), + 'transactionKey' => $this->config->getTransactionKey($storeId) + ] + ]; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AuthorizeDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AuthorizeDataBuilder.php new file mode 100644 index 000000000000..226175f74d55 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/AuthorizeDataBuilder.php @@ -0,0 +1,69 @@ +subjectReader = $subjectReader; + $this->passthroughData = $passthroughData; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $payment = $paymentDO->getPayment(); + $data = []; + + if ($payment instanceof Payment) { + $data = [ + 'transactionRequest' => [ + 'transactionType' => self::REQUEST_AUTH_ONLY, + ] + ]; + + $this->passthroughData->setData( + 'transactionType', + $data['transactionRequest']['transactionType'] + ); + } + + return $data; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/CaptureDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/CaptureDataBuilder.php new file mode 100644 index 000000000000..0b17d10fb0d6 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/CaptureDataBuilder.php @@ -0,0 +1,73 @@ +subjectReader = $subjectReader; + $this->passthroughData = $passthroughData; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $payment = $paymentDO->getPayment(); + $data = []; + + if ($payment instanceof Payment) { + $authTransaction = $payment->getAuthorizationTransaction(); + $refId = $authTransaction->getAdditionalInformation('real_transaction_id'); + + $data = [ + 'transactionRequest' => [ + 'transactionType' => self::REQUEST_TYPE_PRIOR_AUTH_CAPTURE, + 'refTransId' => $refId + ] + ]; + + $this->passthroughData->setData( + 'transactionType', + $data['transactionRequest']['transactionType'] + ); + } + + return $data; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/CustomSettingsBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/CustomSettingsBuilder.php new file mode 100644 index 000000000000..e5b4472c098c --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/CustomSettingsBuilder.php @@ -0,0 +1,62 @@ +subjectReader = $subjectReader; + $this->config = $config; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + $result = []; + + if ($this->config->shouldEmailCustomer($this->subjectReader->readStoreId($buildSubject))) { + $result['transactionRequest'] = [ + 'transactionSettings' => [ + 'setting' => [ + [ + 'settingName' => 'emailCustomer', + 'settingValue' => 'true' + ] + ] + ] + ]; + } + + return $result; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/CustomerDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/CustomerDataBuilder.php new file mode 100644 index 000000000000..7cd0426e93dd --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/CustomerDataBuilder.php @@ -0,0 +1,52 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $order = $paymentDO->getOrder(); + $billingAddress = $order->getBillingAddress(); + $result = [ + 'transactionRequest' => [ + 'customer' => [ + 'id' => $order->getCustomerId(), + 'email' => $billingAddress->getEmail() + ] + ] + ]; + + return $result; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/OrderDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/OrderDataBuilder.php new file mode 100644 index 000000000000..b0e33c9ca961 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/OrderDataBuilder.php @@ -0,0 +1,48 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $order = $paymentDO->getOrder(); + + return [ + 'transactionRequest' => [ + 'order' => [ + 'invoiceNumber' => $order->getOrderIncrementId() + ] + ] + ]; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/PassthroughDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/PassthroughDataBuilder.php new file mode 100644 index 000000000000..0301d08ad42c --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/PassthroughDataBuilder.php @@ -0,0 +1,58 @@ +passthroughData = $passthroughData; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + $fields = []; + + foreach ($this->passthroughData->getData() as $key => $value) { + $fields[] = [ + 'name' => $key, + 'value' => $value + ]; + } + + if (!empty($fields)) { + return [ + 'transactionRequest' => [ + 'userFields' => [ + 'userField' => $fields + ] + ] + ]; + } + + return []; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/PaymentDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/PaymentDataBuilder.php new file mode 100644 index 000000000000..1ad73f623661 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/PaymentDataBuilder.php @@ -0,0 +1,56 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $payment = $paymentDO->getPayment(); + $data = []; + + if ($payment instanceof Payment) { + $dataDescriptor = $payment->getAdditionalInformation('opaqueDataDescriptor'); + $dataValue = $payment->getAdditionalInformation('opaqueDataValue'); + + $data['transactionRequest']['payment'] = [ + 'opaqueData' => [ + 'dataDescriptor' => $dataDescriptor, + 'dataValue' => $dataValue + ] + ]; + } + + return $data; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/PoDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/PoDataBuilder.php new file mode 100644 index 000000000000..ad8f8c2b05d9 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/PoDataBuilder.php @@ -0,0 +1,52 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $payment = $paymentDO->getPayment(); + $data = []; + + if ($payment instanceof Payment) { + $data = [ + 'transactionRequest' => [ + 'poNumber' => $payment->getPoNumber() + ] + ]; + } + + return $data; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RefundPaymentDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RefundPaymentDataBuilder.php new file mode 100644 index 000000000000..96f3e67720fe --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RefundPaymentDataBuilder.php @@ -0,0 +1,58 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + * @throws \Exception + */ + public function build(array $buildSubject): array + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $payment = $paymentDO->getPayment(); + $data = []; + + if ($payment instanceof Payment) { + $data = [ + 'transactionRequest' => [ + 'payment' => [ + 'creditCard' => [ + 'cardNumber' => $payment->getAdditionalInformation('ccLast4'), + 'expirationDate' => 'XXXX' + ] + ] + ] + ]; + } + + return $data; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RefundReferenceTransactionDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RefundReferenceTransactionDataBuilder.php new file mode 100644 index 000000000000..b8cb5f858d05 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RefundReferenceTransactionDataBuilder.php @@ -0,0 +1,53 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $payment = $paymentDO->getPayment(); + $data = []; + + if ($payment instanceof Payment) { + $transactionId = $payment->getAuthorizationTransaction()->getParentTxnId(); + $data = [ + 'transactionRequest' => [ + 'refTransId' => $transactionId + ] + ]; + } + + return $data; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RefundTransactionTypeDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RefundTransactionTypeDataBuilder.php new file mode 100644 index 000000000000..752be05f6b57 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RefundTransactionTypeDataBuilder.php @@ -0,0 +1,31 @@ + [ + 'transactionType' => self::REQUEST_TYPE_REFUND + ] + ]; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RequestTypeBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RequestTypeBuilder.php new file mode 100644 index 000000000000..16c3f9556de2 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/RequestTypeBuilder.php @@ -0,0 +1,45 @@ +type = $type; + } + + /** + * Adds the type of the request to the build subject + * + * @param array $buildSubject + * @return array + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function build(array $buildSubject): array + { + return [ + 'payload_type' => $this->type + ]; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/SaleDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/SaleDataBuilder.php new file mode 100644 index 000000000000..6ec27b105615 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/SaleDataBuilder.php @@ -0,0 +1,69 @@ +subjectReader = $subjectReader; + $this->passthroughData = $passthroughData; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $payment = $paymentDO->getPayment(); + $data = []; + + if ($payment instanceof Payment) { + $data = [ + 'transactionRequest' => [ + 'transactionType' => self::REQUEST_AUTH_AND_CAPTURE, + ] + ]; + + $this->passthroughData->setData( + 'transactionType', + $data['transactionRequest']['transactionType'] + ); + } + + return $data; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/ShippingDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/ShippingDataBuilder.php new file mode 100644 index 000000000000..390714579f0b --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/ShippingDataBuilder.php @@ -0,0 +1,56 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $payment = $paymentDO->getPayment(); + $order = $paymentDO->getOrder(); + $data = []; + + if ($payment instanceof Payment && $order instanceof Order) { + $data = [ + 'transactionRequest' => [ + 'shipping' => [ + 'amount' => $order->getBaseShippingAmount() + ] + ] + ]; + } + + return $data; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/SolutionDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/SolutionDataBuilder.php new file mode 100644 index 000000000000..0c89a0116def --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/SolutionDataBuilder.php @@ -0,0 +1,53 @@ +config = $config; + $this->subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + return [ + 'transactionRequest' => [ + 'solution' => [ + 'id' => $this->config->getSolutionId($this->subjectReader->readStoreId($buildSubject)), + ] + ] + ]; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/StoreConfigBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/StoreConfigBuilder.php new file mode 100644 index 000000000000..f44b1e5de9a2 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/StoreConfigBuilder.php @@ -0,0 +1,43 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $order = $paymentDO->getOrder(); + + return [ + 'store_id' => $order->getStoreId() + ]; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/TransactionDetailsDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/TransactionDetailsDataBuilder.php new file mode 100644 index 000000000000..e3a17e963684 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/TransactionDetailsDataBuilder.php @@ -0,0 +1,69 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + $data = []; + + if (!empty($buildSubject['transactionId'])) { + $data = [ + 'transId' => $buildSubject['transactionId'] + ]; + } else { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $payment = $paymentDO->getPayment(); + + if ($payment instanceof Payment) { + $authorizationTransaction = $payment->getAuthorizationTransaction(); + + if (empty($authorizationTransaction)) { + $transactionId = $payment->getLastTransId(); + } else { + $transactionId = $authorizationTransaction->getParentTxnId(); + + if (empty($transactionId)) { + $transactionId = $authorizationTransaction->getTxnId(); + } + } + + $data = [ + 'transId' => $transactionId + ]; + } + } + + return $data; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/VoidDataBuilder.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/VoidDataBuilder.php new file mode 100644 index 000000000000..ef0cb96774e6 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Request/VoidDataBuilder.php @@ -0,0 +1,60 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject): array + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $payment = $paymentDO->getPayment(); + $transactionData = []; + + if ($payment instanceof Payment) { + $authorizationTransaction = $payment->getAuthorizationTransaction(); + $refId = $authorizationTransaction->getAdditionalInformation('real_transaction_id'); + if (empty($refId)) { + $refId = $authorizationTransaction->getParentTxnId(); + } + + $transactionData['transactionRequest'] = [ + 'transactionType' => self::REQUEST_TYPE_VOID, + 'refTransId' => $refId + ]; + } + + return $transactionData; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/CloseParentTransactionHandler.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/CloseParentTransactionHandler.php new file mode 100644 index 000000000000..30b1ce88b083 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/CloseParentTransactionHandler.php @@ -0,0 +1,45 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function handle(array $handlingSubject, array $response): void + { + $paymentDO = $this->subjectReader->readPayment($handlingSubject); + $payment = $paymentDO->getPayment(); + + if ($payment instanceof Payment) { + $payment->setShouldCloseParentTransaction(true); + } + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/CloseTransactionHandler.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/CloseTransactionHandler.php new file mode 100644 index 000000000000..f0dff200e802 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/CloseTransactionHandler.php @@ -0,0 +1,46 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function handle(array $handlingSubject, array $response): void + { + $paymentDO = $this->subjectReader->readPayment($handlingSubject); + $payment = $paymentDO->getPayment(); + + if ($payment instanceof Payment) { + $payment->setIsTransactionClosed(true); + $payment->setShouldCloseParentTransaction(true); + } + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/PaymentResponseHandler.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/PaymentResponseHandler.php new file mode 100644 index 000000000000..16e8fbabb214 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/PaymentResponseHandler.php @@ -0,0 +1,55 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function handle(array $handlingSubject, array $response): void + { + $paymentDO = $this->subjectReader->readPayment($handlingSubject); + $payment = $paymentDO->getPayment(); + $transactionResponse = $response['transactionResponse']; + + if ($payment instanceof Payment) { + $payment->setCcLast4($payment->getAdditionalInformation('ccLast4')); + $payment->setCcAvsStatus($transactionResponse['avsResultCode']); + $payment->setIsTransactionClosed(false); + + if ($transactionResponse['responseCode'] == self::RESPONSE_CODE_HELD) { + $payment->setIsTransactionPending(true) + ->setIsFraudDetected(true); + } + } + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/PaymentReviewStatusHandler.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/PaymentReviewStatusHandler.php new file mode 100644 index 000000000000..9f7c62873669 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/PaymentReviewStatusHandler.php @@ -0,0 +1,63 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function handle(array $handlingSubject, array $response): void + { + $paymentDO = $this->subjectReader->readPayment($handlingSubject); + $payment = $paymentDO->getPayment(); + + if ($payment instanceof Payment) { + $paymentDO = $this->subjectReader->readPayment($handlingSubject); + $payment = $paymentDO->getPayment(); + + $status = $response['transaction']['transactionStatus']; + // This data is only used when updating the order payment via Get Payment Update + if (!in_array($status, self::REVIEW_PENDING_STATUSES)) { + $denied = in_array($status, self::REVIEW_DECLINED_STATUSES); + $payment->setData('is_transaction_denied', $denied); + $payment->setData('is_transaction_approved', !$denied); + } + } + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/TransactionDetailsResponseHandler.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/TransactionDetailsResponseHandler.php new file mode 100644 index 000000000000..0dab64145213 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/TransactionDetailsResponseHandler.php @@ -0,0 +1,65 @@ +subjectReader = $subjectReader; + $this->config = $config; + } + + /** + * @inheritdoc + */ + public function handle(array $handlingSubject, array $response): void + { + $storeId = $this->subjectReader->readStoreId($handlingSubject); + $paymentDO = $this->subjectReader->readPayment($handlingSubject); + $payment = $paymentDO->getPayment(); + $transactionResponse = $response['transactionResponse']; + + if ($payment instanceof Payment) { + // Add the keys that should show in the transaction details interface + $additionalInformationKeys = $this->config->getAdditionalInfoKeys($storeId); + $rawDetails = []; + foreach ($additionalInformationKeys as $paymentInfoKey) { + if (isset($transactionResponse[$paymentInfoKey])) { + $rawDetails[$paymentInfoKey] = $transactionResponse[$paymentInfoKey]; + $payment->setAdditionalInformation($paymentInfoKey, $transactionResponse[$paymentInfoKey]); + } + } + $payment->setTransactionAdditionalInfo(Payment\Transaction::RAW_DETAILS, $rawDetails); + } + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/TransactionIdHandler.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/TransactionIdHandler.php new file mode 100644 index 000000000000..bf5257f95dad --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/TransactionIdHandler.php @@ -0,0 +1,54 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function handle(array $handlingSubject, array $response): void + { + $paymentDO = $this->subjectReader->readPayment($handlingSubject); + $payment = $paymentDO->getPayment(); + $transactionResponse = $response['transactionResponse']; + + if ($payment instanceof Payment) { + if (!$payment->getParentTransactionId() + || $transactionResponse['transId'] != $payment->getParentTransactionId() + ) { + $payment->setTransactionId($transactionResponse['transId']); + } + $payment->setTransactionAdditionalInfo( + 'real_transaction_id', + $transactionResponse['transId'] + ); + } + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/VoidResponseHandler.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/VoidResponseHandler.php new file mode 100644 index 000000000000..06b16b37278b --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Response/VoidResponseHandler.php @@ -0,0 +1,49 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function handle(array $handlingSubject, array $response): void + { + $paymentDO = $this->subjectReader->readPayment($handlingSubject); + $payment = $paymentDO->getPayment(); + $transactionId = $response['transactionResponse']['transId']; + + if ($payment instanceof Payment) { + $payment->setIsTransactionClosed(true); + $payment->setShouldCloseParentTransaction(true); + $payment->setTransactionAdditionalInfo('real_transaction_id', $transactionId); + } + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/SubjectReader.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/SubjectReader.php new file mode 100644 index 000000000000..855d48e27968 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/SubjectReader.php @@ -0,0 +1,96 @@ +readPayment($subject) + ->getOrder() + ->getStoreId(); + } catch (\InvalidArgumentException $e) { + // No store id is current set + } + } + + return $storeId ? (int)$storeId : null; + } + + /** + * Reads amount from subject + * + * @param array $subject + * @return string + */ + public function readAmount(array $subject): string + { + return (string)Helper\SubjectReader::readAmount($subject); + } + + /** + * Reads response from subject + * + * @param array $subject + * @return array + */ + public function readResponse(array $subject): ?array + { + return Helper\SubjectReader::readResponse($subject); + } + + /** + * Reads login id from subject + * + * @param array $subject + * @return string|null + */ + public function readLoginId(array $subject): ?string + { + return $subject['merchantAuthentication']['name'] ?? null; + } + + /** + * Reads transaction key from subject + * + * @param array $subject + * @return string|null + */ + public function readTransactionKey(array $subject): ?string + { + return $subject['merchantAuthentication']['transactionKey'] ?? null; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Validator/GeneralResponseValidator.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Validator/GeneralResponseValidator.php new file mode 100644 index 000000000000..7ad4647b421a --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Validator/GeneralResponseValidator.php @@ -0,0 +1,79 @@ +resultFactory = $resultFactory; + $this->subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function validate(array $validationSubject): ResultInterface + { + $response = $this->subjectReader->readResponse($validationSubject); + $isValid = (isset($response['messages']['resultCode']) + && $response['messages']['resultCode'] === self::RESULT_CODE_SUCCESS); + $errorCodes = []; + $errorMessages = []; + + if (!$isValid) { + if (isset($response['messages']['message']['code'])) { + $errorCodes[] = $response['messages']['message']['code']; + $errorMessages[] = $response['messages']['message']['text']; + } elseif (isset($response['messages']['message'])) { + foreach ($response['messages']['message'] as $message) { + $errorCodes[] = $message['code']; + $errorMessages[] = $message['text']; + } + } elseif (isset($response['errors']['error'])) { + foreach ($response['errors']['error'] as $message) { + $errorCodes[] = $message['errorCode']; + $errorMessages[] = $message['errorText']; + } + } + } + + return $this->createResult($isValid, $errorMessages, $errorCodes); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Validator/TransactionHashValidator.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Validator/TransactionHashValidator.php new file mode 100644 index 000000000000..0d1c2ad033d8 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Validator/TransactionHashValidator.php @@ -0,0 +1,197 @@ +subjectReader = $subjectReader; + $this->config = $config; + } + + /** + * Validates the transaction hash matches the configured hash + * + * @param array $validationSubject + * @return ResultInterface + */ + public function validate(array $validationSubject): ResultInterface + { + $response = $this->subjectReader->readResponse($validationSubject); + $storeId = $this->subjectReader->readStoreId($validationSubject); + + if (!empty($response['transactionResponse']['transHashSha2'])) { + return $this->validateHash( + $validationSubject, + $this->config->getTransactionSignatureKey($storeId), + 'transHashSha2', + 'generateSha512Hash' + ); + } elseif (!empty($response['transactionResponse']['transHash'])) { + return $this->validateHash( + $validationSubject, + $this->config->getLegacyTransactionHash($storeId), + 'transHash', + 'generateMd5Hash' + ); + } + + return $this->createResult( + false, + [ + __('The authenticity of the gateway response could not be verified.') + ], + [self::ERROR_TRANSACTION_HASH] + ); + } + + /** + * Validates the response again the legacy MD5 spec + * + * @param array $validationSubject + * @param string $storedHash + * @param string $hashField + * @param string $generateFunction + * @return ResultInterface + */ + private function validateHash( + array $validationSubject, + string $storedHash, + string $hashField, + string $generateFunction + ): ResultInterface { + $storeId = $this->subjectReader->readStoreId($validationSubject); + $response = $this->subjectReader->readResponse($validationSubject); + $transactionResponse = $response['transactionResponse']; + + /* + * Authorize.net is inconsistent with how they hash and heuristically trying to detect whether or not they used + * the amount to calculate the hash is risky because their responses are incorrect in some cases. + * Refund uses the amount when referencing a transaction but will use 0 when refunding without a reference. + * Non-refund reference transactions such as (void/capture) don't use the amount. Authorize/auth&capture + * transactions will use amount but if there is an AVS error the response will indicate the transaction was a + * reference transaction so this can't be heuristically detected by looking at combinations of refTransID + * and transId (yes they also mixed the letter casing for "id"). Their documentation doesn't talk about this + * and to make this even better, none of their official SDKs support the new hash field to compare + * implementations. Therefore the only way to safely validate this hash without failing for even more + * unexpected corner cases we simply need to validate with and without the amount. + */ + try { + $amount = $this->subjectReader->readAmount($validationSubject); + } catch (\InvalidArgumentException $e) { + $amount = 0; + } + + $hash = $this->{$generateFunction}( + $storedHash, + $this->config->getLoginId($storeId), + sprintf('%.2F', $amount), + $transactionResponse['transId'] ?? '' + ); + $valid = Security::compareStrings($hash, $transactionResponse[$hashField]); + + if (!$valid && $amount > 0) { + $hash = $this->{$generateFunction}( + $storedHash, + $this->config->getLoginId($storeId), + '0.00', + $transactionResponse['transId'] ?? '' + ); + $valid = Security::compareStrings($hash, $transactionResponse[$hashField]); + } + + if ($valid) { + return $this->createResult(true); + } + + return $this->createResult( + false, + [ + __('The authenticity of the gateway response could not be verified.') + ], + [self::ERROR_TRANSACTION_HASH] + ); + } + + /** + * Generates a Md5 hash to compare against AuthNet's. + * + * @param string $merchantMd5 + * @param string $merchantApiLogin + * @param string $amount + * @param string $transactionId + * @return string + * @SuppressWarnings(PHPMD.UnusedPrivateMethod) + */ + private function generateMd5Hash( + $merchantMd5, + $merchantApiLogin, + $amount, + $transactionId + ) { + return strtoupper(md5($merchantMd5 . $merchantApiLogin . $transactionId . $amount)); + } + + /** + * Generates a SHA-512 hash to compare against AuthNet's. + * + * @param string $merchantKey + * @param string $merchantApiLogin + * @param string $amount + * @param string $transactionId + * @return string + * @SuppressWarnings(PHPMD.UnusedPrivateMethod) + */ + private function generateSha512Hash( + $merchantKey, + $merchantApiLogin, + $amount, + $transactionId + ) { + $message = '^' . $merchantApiLogin . '^' . $transactionId . '^' . $amount . '^'; + + return strtoupper(hash_hmac('sha512', $message, pack('H*', $merchantKey))); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Gateway/Validator/TransactionResponseValidator.php b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Validator/TransactionResponseValidator.php new file mode 100644 index 000000000000..326f4fb29ac8 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Gateway/Validator/TransactionResponseValidator.php @@ -0,0 +1,101 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function validate(array $validationSubject): ResultInterface + { + $response = $this->subjectReader->readResponse($validationSubject); + $transactionResponse = $response['transactionResponse']; + + if ($this->isResponseCodeAnError($transactionResponse)) { + $errorCodes = []; + $errorMessages = []; + + if (isset($transactionResponse['messages']['message']['code'])) { + $errorCodes[] = $transactionResponse['messages']['message']['code']; + $errorMessages[] = $transactionResponse['messages']['message']['text']; + } elseif (isset($transactionResponse['messages']['message'])) { + foreach ($transactionResponse['messages']['message'] as $message) { + $errorCodes[] = $message['code']; + $errorMessages[] = $message['description']; + } + } elseif (isset($transactionResponse['errors'])) { + foreach ($transactionResponse['errors'] as $message) { + $errorCodes[] = $message['errorCode']; + $errorMessages[] = $message['errorText']; + } + } + + return $this->createResult(false, $errorMessages, $errorCodes); + } + + return $this->createResult(true); + } + + /** + * Determines if the response code is actually an error + * + * @param array $transactionResponse + * @return bool + */ + private function isResponseCodeAnError(array $transactionResponse): bool + { + $code = $transactionResponse['messages']['message']['code'] + ?? $transactionResponse['messages']['message'][0]['code'] + ?? $transactionResponse['errors'][0]['errorCode'] + ?? null; + + return !in_array($transactionResponse['responseCode'], [ + self::RESPONSE_CODE_APPROVED, self::RESPONSE_CODE_HELD + ]) + || $code + && !in_array( + $code, + [ + self::RESPONSE_REASON_CODE_APPROVED, + self::RESPONSE_REASON_CODE_PENDING_REVIEW, + self::RESPONSE_REASON_CODE_PENDING_REVIEW_AUTHORIZED + ] + ); + } +} diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Framework/LICENSE.txt b/app/code/Magento/AuthorizenetAcceptjs/LICENSE.txt similarity index 100% rename from dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Framework/LICENSE.txt rename to app/code/Magento/AuthorizenetAcceptjs/LICENSE.txt diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Framework/LICENSE_AFL.txt b/app/code/Magento/AuthorizenetAcceptjs/LICENSE_AFL.txt similarity index 100% rename from dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Framework/LICENSE_AFL.txt rename to app/code/Magento/AuthorizenetAcceptjs/LICENSE_AFL.txt diff --git a/app/code/Magento/AuthorizenetAcceptjs/Model/Adminhtml/Source/Cctype.php b/app/code/Magento/AuthorizenetAcceptjs/Model/Adminhtml/Source/Cctype.php new file mode 100644 index 000000000000..046907ebb88c --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Model/Adminhtml/Source/Cctype.php @@ -0,0 +1,25 @@ + self::ENVIRONMENT_SANDBOX, + 'label' => 'Sandbox', + ], + [ + 'value' => self::ENVIRONMENT_PRODUCTION, + 'label' => 'Production' + ] + ]; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Model/Adminhtml/Source/PaymentAction.php b/app/code/Magento/AuthorizenetAcceptjs/Model/Adminhtml/Source/PaymentAction.php new file mode 100644 index 000000000000..907a1b2a51b8 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Model/Adminhtml/Source/PaymentAction.php @@ -0,0 +1,32 @@ + 'authorize', + 'label' => __('Authorize Only'), + ], + [ + 'value' => 'authorize_capture', + 'label' => __('Authorize and Capture') + ] + ]; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Model/PassthroughDataObject.php b/app/code/Magento/AuthorizenetAcceptjs/Model/PassthroughDataObject.php new file mode 100644 index 000000000000..b49ef7e62250 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Model/PassthroughDataObject.php @@ -0,0 +1,19 @@ +config = $config; + $this->cart = $cart; + } + + /** + * Retrieve assoc array of checkout configuration + * + * @return array + */ + public function getConfig() + { + $storeId = $this->cart->getStoreId(); + + return [ + 'payment' => [ + Config::METHOD => [ + 'clientKey' => $this->config->getClientKey($storeId), + 'apiLoginID' => $this->config->getLoginId($storeId), + 'environment' => $this->config->getEnvironment($storeId), + 'useCvv' => $this->config->isCvvEnabled($storeId), + ] + ] + ]; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Observer/DataAssignObserver.php b/app/code/Magento/AuthorizenetAcceptjs/Observer/DataAssignObserver.php new file mode 100644 index 000000000000..c7490ad0c80c --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Observer/DataAssignObserver.php @@ -0,0 +1,52 @@ +readDataArgument($observer); + + $additionalData = $data->getData(PaymentInterface::KEY_ADDITIONAL_DATA); + if (!is_array($additionalData)) { + return; + } + + $paymentInfo = $this->readPaymentModelArgument($observer); + + foreach ($this->additionalInformationList as $additionalInformationKey) { + if (isset($additionalData[$additionalInformationKey])) { + $paymentInfo->setAdditionalInformation( + $additionalInformationKey, + $additionalData[$additionalInformationKey] + ); + } + } + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/README.md b/app/code/Magento/AuthorizenetAcceptjs/README.md new file mode 100644 index 000000000000..b066f8a2d750 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/README.md @@ -0,0 +1 @@ +The Magento_AuthorizenetAcceptjs module implements the integration with the Authorize.Net payment gateway and makes the latter available as a payment method in Magento. diff --git a/app/code/Magento/AuthorizenetAcceptjs/Setup/Patch/Data/CopyCurrentConfig.php b/app/code/Magento/AuthorizenetAcceptjs/Setup/Patch/Data/CopyCurrentConfig.php new file mode 100644 index 000000000000..0675bd94b620 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Setup/Patch/Data/CopyCurrentConfig.php @@ -0,0 +1,230 @@ +scopeConfig = $scopeConfig; + $this->resourceConfig = $resourceConfig; + $this->encryptor = $encryptor; + $this->moduleDataSetup = $moduleDataSetup; + $this->storeManager = $storeManager; + } + + /** + * @inheritdoc + */ + public function apply(): void + { + $this->moduleDataSetup->startSetup(); + $this->migrateDefaultValues(); + $this->migrateWebsiteValues(); + $this->moduleDataSetup->endSetup(); + } + + /** + * Migrate configuration values from DirectPost to Accept.js on default scope + * + * @return void + */ + private function migrateDefaultValues(): void + { + foreach ($this->configFieldsToMigrate as $field) { + $configValue = $this->getOldConfigValue($field); + + if (!empty($configValue)) { + $this->saveNewConfigValue($field, $configValue); + } + } + + foreach ($this->encryptedConfigFieldsToMigrate as $field) { + $configValue = $this->getOldConfigValue($field); + + if (!empty($configValue)) { + $this->saveNewConfigValue( + $field, + $configValue, + ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + 0, + true + ); + } + } + } + + /** + * Migrate configuration values from DirectPost to Accept.js on all website scopes + * + * @return void + */ + private function migrateWebsiteValues(): void + { + foreach ($this->storeManager->getWebsites() as $website) { + $websiteID = (int) $website->getId(); + + foreach ($this->configFieldsToMigrate as $field) { + $configValue = $this->getOldConfigValue($field, ScopeInterface::SCOPE_WEBSITES, $websiteID); + + if (!empty($configValue)) { + $this->saveNewConfigValue($field, $configValue, ScopeInterface::SCOPE_WEBSITES, $websiteID); + } + } + + foreach ($this->encryptedConfigFieldsToMigrate as $field) { + $configValue = $this->getOldConfigValue($field, ScopeInterface::SCOPE_WEBSITES, $websiteID); + + if (!empty($configValue)) { + $this->saveNewConfigValue($field, $configValue, ScopeInterface::SCOPE_WEBSITES, $websiteID, true); + } + } + } + } + + /** + * Get old configuration value from the DirectPost module's configuration on the store scope + * + * @param string $field + * @param string $scope + * @param int $scopeID + * @return mixed + */ + private function getOldConfigValue( + string $field, + string $scope = ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + int $scopeID = null + ) { + return $this->scopeConfig->getValue( + sprintf(self::PAYMENT_PATH_FORMAT, self::DIRECTPOST_PATH, $field), + $scope, + $scopeID + ); + } + + /** + * Save configuration value for AcceptJS + * + * @param string $field + * @param mixed $value + * @param string $scope + * @param int $scopeID + * @param bool $isEncrypted + * @return void + */ + private function saveNewConfigValue( + string $field, + $value, + string $scope = ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + int $scopeID = 0, + bool $isEncrypted = false + ): void { + $value = $isEncrypted ? $this->encryptor->encrypt($value) : $value; + + $this->resourceConfig->saveConfig( + sprintf(self::PAYMENT_PATH_FORMAT, self::ACCEPTJS_PATH, $field), + $value, + $scope, + $scopeID + ); + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return []; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/ActionGroup/ConfigureAuthorizenetAcceptjsActionGroup.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/ActionGroup/ConfigureAuthorizenetAcceptjsActionGroup.xml new file mode 100644 index 000000000000..e9a194435e3e --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/ActionGroup/ConfigureAuthorizenetAcceptjsActionGroup.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/ActionGroup/FillPaymentInformationActionGroup.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/ActionGroup/FillPaymentInformationActionGroup.xml new file mode 100644 index 000000000000..d06bf996a1f2 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/ActionGroup/FillPaymentInformationActionGroup.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/ActionGroup/ViewAndValidateOrderActionGroup.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/ActionGroup/ViewAndValidateOrderActionGroup.xml new file mode 100644 index 000000000000..ba0e49d1876f --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/ActionGroup/ViewAndValidateOrderActionGroup.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Data/AuthorizenetAcceptjsOrderValidationData.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Data/AuthorizenetAcceptjsOrderValidationData.xml new file mode 100644 index 000000000000..59d4be98d450 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Data/AuthorizenetAcceptjsOrderValidationData.xml @@ -0,0 +1,18 @@ + + + + + + $24.68 + $128.00 + Processing + Capture + No + + diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTemplates/LICENSE.txt b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/LICENSE.txt similarity index 100% rename from dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTemplates/LICENSE.txt rename to app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/LICENSE.txt diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTemplates/LICENSE_AFL.txt b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/LICENSE_AFL.txt similarity index 100% rename from dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTemplates/LICENSE_AFL.txt rename to app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/LICENSE_AFL.txt diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/README.md b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/README.md new file mode 100644 index 000000000000..aba235e2cfad --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# AuthorizenetAcceptjs Functional Tests + +The Functional Test Module for **Magento AuthorizenetAcceptjs** module. diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/AdminMenuSection.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/AdminMenuSection.xml new file mode 100644 index 000000000000..defb91339ea8 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/AdminMenuSection.xml @@ -0,0 +1,25 @@ + + + + +
+ + + + + + + + + + + + +
+
diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/AuthorizenetAcceptjsConfigurationSection.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/AuthorizenetAcceptjsConfigurationSection.xml new file mode 100644 index 000000000000..31be865ea267 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/AuthorizenetAcceptjsConfigurationSection.xml @@ -0,0 +1,24 @@ + + + + +
+ + + + + + + + + + + +
+
diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/AuthorizenetCheckoutSection.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/AuthorizenetCheckoutSection.xml new file mode 100644 index 000000000000..5d97842de374 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/AuthorizenetCheckoutSection.xml @@ -0,0 +1,19 @@ + + + + +
+ + + + + + +
+
diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/ConfigurationMainActionsSection.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/ConfigurationMainActionsSection.xml new file mode 100644 index 000000000000..344330c4bc05 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/ConfigurationMainActionsSection.xml @@ -0,0 +1,14 @@ + + + + +
+ +
+
diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/GuestAuthorizenetCheckoutSection.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/GuestAuthorizenetCheckoutSection.xml new file mode 100644 index 000000000000..b5f2ecf64116 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/GuestAuthorizenetCheckoutSection.xml @@ -0,0 +1,22 @@ + + + + +
+ + + + + + + + + +
+
diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/OrdersGridSection.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/OrdersGridSection.xml new file mode 100644 index 000000000000..7ae3dd0ffee8 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/OrdersGridSection.xml @@ -0,0 +1,14 @@ + + + + +
+ +
+
diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/StoresConfigurationListSection.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/StoresConfigurationListSection.xml new file mode 100644 index 000000000000..f9f1bef38d17 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/StoresConfigurationListSection.xml @@ -0,0 +1,15 @@ + + + + +
+ + +
+
diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/StoresSubmenuSection.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/StoresSubmenuSection.xml new file mode 100644 index 000000000000..e54f9808fd49 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/StoresSubmenuSection.xml @@ -0,0 +1,14 @@ + + + + +
+ +
+
diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/ViewOrderSection.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/ViewOrderSection.xml new file mode 100644 index 000000000000..608067d7d31a --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Section/ViewOrderSection.xml @@ -0,0 +1,25 @@ + + + + +
+ + + + + + + + + + + + +
+
diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/FullCaptureAuthorizenetAcceptjsTest.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/FullCaptureAuthorizenetAcceptjsTest.xml new file mode 100644 index 000000000000..42a78291436e --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/FullCaptureAuthorizenetAcceptjsTest.xml @@ -0,0 +1,74 @@ + + + + + + + + + <description value="Capture an order placed using Authorize.net Accept.js"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-12255"/> + <skip> + <issueId value="DEVOPS-4604"/> + </skip> + <group value="AuthorizenetAcceptjs"/> + <group value="ThirdPartyPayments"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <createData stepKey="createCustomer" entity="Simple_US_Customer"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!--Configure Auth.net--> + <actionGroup ref="ConfigureAuthorizenetAcceptjs" stepKey="configureAuthorizenetAcceptjs"> + <argument name="paymentAction" value="Authorize Only"/> + </actionGroup> + + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="DisableAuthorizenetAcceptjs" stepKey="DisableAuthorizenetAcceptjs"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Storefront Login--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginStorefront"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!--Add product to cart--> + <amOnPage url="$$createProduct.name$$.html" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForProductPage"/> + <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> + <waitForPageLoad stepKey="waitForCartToFill"/> + + <!--Checkout steps--> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="GoToCheckoutFromMinicartActionGroup"/> + <waitForPageLoad stepKey="waitForCheckoutLoad"/> + <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShipping"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="submitShippingSelection"/> + <waitForPageLoad stepKey="waitForShippingToFinish"/> + <actionGroup ref="FillPaymentInformation" stepKey="fillPaymentInfo"/> + + <!--View and validate order--> + <actionGroup ref="ViewAndValidateOrderActionGroup" stepKey="viewAndValidateOrder"> + <argument name="amount" value="{{AuthorizenetAcceptjsOrderValidationData.twoSimpleProductsOrderAmount}}"/> + <argument name="status" value="{{AuthorizenetAcceptjsOrderValidationData.processingStatusProcessing}}"/> + <argument name="captureStatus" value="{{AuthorizenetAcceptjsOrderValidationData.captureStatusCapture}}"/> + <argument name="closedStatus" value="{{AuthorizenetAcceptjsOrderValidationData.closedStatusNo}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/GuestCheckoutVirtualProductAuthorizenetAcceptjsTest.xml b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/GuestCheckoutVirtualProductAuthorizenetAcceptjsTest.xml new file mode 100644 index 000000000000..95c243690521 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Mftf/Test/GuestCheckoutVirtualProductAuthorizenetAcceptjsTest.xml @@ -0,0 +1,82 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="GuestCheckoutVirtualProductAuthorizenetAcceptjsTest"> + <annotations> + <stories value="Authorize.net Accept.js"/> + <title value="Guest Checkout of Virtual Product using Authorize.net Accept.js"/> + <description value="Checkout a virtual product with a guest using Authorize.net Accept.js"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-12712"/> + <skip> + <issueId value="DEVOPS-4604"/> + </skip> + <group value="AuthorizenetAcceptjs"/> + <group value="ThirdPartyPayments"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Create virtual product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProduct"> + <argument name="product" value="defaultVirtualProduct"/> + </actionGroup> + <actionGroup ref="fillMainProductFormNoWeight" stepKey="fillProductForm"> + <argument name="product" value="defaultVirtualProduct"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProductForm"/> + + <!--Configure Auth.net--> + <actionGroup ref="ConfigureAuthorizenetAcceptjs" stepKey="configureAuthorizenetAcceptjs"> + <argument name="paymentAction" value="Authorize and Capture"/> + </actionGroup> + + </before> + <after> + <actionGroup ref="DisableAuthorizenetAcceptjs" stepKey="DisableAuthorizenetAcceptjs"/> + <!-- Delete virtual product --> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> + <argument name="product" value="defaultVirtualProduct"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Add product to cart twice--> + <amOnPage url="{{defaultVirtualProduct.sku}}.html" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForProductPage"/> + <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> + <waitForPageLoad stepKey="waitForCartToFill"/> + <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCartAgain"/> + <waitForPageLoad stepKey="waitForCartToFillAgain"/> + + <!--Checkout steps--> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="GoToCheckoutFromMinicartActionGroup"/> + <waitForPageLoad stepKey="waitForCheckoutLoad"/> + + <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{Simple_US_Customer.email}}" stepKey="enterEmail"/> + <click stepKey="clickOnAuthorizenetToggle" selector="{{AuthorizenetCheckoutSection.selectAuthorizenet}}"/> + <waitForPageLoad stepKey="waitForBillingInfoLoad"/> + <actionGroup ref="GuestCheckoutAuthorizenetFillBillingAddress" stepKey="fillAddressForm"> + <argument name="customer" value="Simple_US_Customer"/> + <argument name="customerAddress" value="CustomerAddressSimple"/> + </actionGroup> + <actionGroup ref="FillPaymentInformation" stepKey="fillPaymentInfo"/> + + <!--View and validate order--> + <actionGroup ref="ViewAndValidateOrderActionGroupNoSubmit" stepKey="viewAndValidateOrder"> + <argument name="amount" value="{{AuthorizenetAcceptjsOrderValidationData.virtualProductOrderAmount}}"/> + <argument name="status" value="{{AuthorizenetAcceptjsOrderValidationData.processingStatusProcessing}}"/> + <argument name="captureStatus" value="{{AuthorizenetAcceptjsOrderValidationData.captureStatusCapture}}"/> + <argument name="closedStatus" value="{{AuthorizenetAcceptjsOrderValidationData.closedStatusNo}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Block/FormTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Block/FormTest.php new file mode 100644 index 000000000000..020b651aaaf1 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Block/FormTest.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Block; + +use Magento\AuthorizenetAcceptjs\Block\Form; +use PHPUnit\Framework\MockObject\Builder\InvocationMocker; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Magento\AuthorizenetAcceptjs\Gateway\Config; +use Magento\Backend\Model\Session\Quote; +use Magento\Framework\View\Element\Template\Context; +use Magento\Payment\Model\Config as PaymentConfig; + +class FormTest extends TestCase +{ + /** + * @var Form + */ + private $block; + + /** + * @var Config|MockObject|InvocationMocker + */ + private $configMock; + + protected function setUp() + { + $contextMock = $this->createMock(Context::class); + $this->configMock = $this->createMock(Config::class); + $quoteMock = $this->getMockBuilder(Quote::class) + ->disableOriginalConstructor() + ->setMethods(['getStoreId']) + ->getMock(); + $quoteMock->method('getStoreId') + ->willReturn('123'); + $paymentConfig = $this->createMock(PaymentConfig::class); + + $this->block = new Form( + $contextMock, + $paymentConfig, + $this->configMock, + $quoteMock + ); + } + + public function testIsCvvEnabled() + { + $this->configMock->method('isCvvEnabled') + ->with('123') + ->willReturn(true); + $this->assertTrue($this->block->isCvvEnabled()); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Block/InfoTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Block/InfoTest.php new file mode 100644 index 000000000000..70dfb140e157 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Block/InfoTest.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Block; + +use Magento\AuthorizenetAcceptjs\Block\Info; +use Magento\AuthorizenetAcceptjs\Gateway\Config; +use Magento\Framework\Phrase; +use Magento\Framework\Phrase\RendererInterface; +use Magento\Framework\View\Element\Template\Context; +use Magento\Payment\Gateway\ConfigInterface; +use Magento\Payment\Model\InfoInterface; +use PHPUnit\Framework\MockObject\Builder\InvocationMocker; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class InfoTest extends TestCase +{ + public function testLabelsAreTranslated() + { + /** @var Context|MockObject|InvocationMocker $contextMock */ + $contextMock = $this->createMock(Context::class); + /** @var Config|MockObject|InvocationMocker $configMock */ + $configMock = $this->createMock(ConfigInterface::class); + $block = new Info($contextMock, $configMock); + /** @var InfoInterface|MockObject|InvocationMocker $payment */ + $payment = $this->createMock(InfoInterface::class); + /** @var RendererInterface|MockObject|InvocationMocker $translationRenderer */ + $translationRenderer = $this->createMock(RendererInterface::class); + + // only foo should be used + $configMock->method('getValue') + ->willReturnMap([ + ['paymentInfoKeys', null, 'foo'], + ['privateInfoKeys', null, ''] + ]); + + // Give more info to ensure only foo is translated + $payment->method('getAdditionalInformation') + ->willReturnCallback(function ($name = null) { + $info = [ + 'foo' => 'bar', + 'baz' => 'bash' + ]; + + if (empty($name)) { + return $info; + } + + return $info[$name]; + }); + + // Foo should be translated to Super Cool String + $translationRenderer->method('render') + ->with(['foo'], []) + ->willReturn('Super Cool String'); + + $previousRenderer = Phrase::getRenderer(); + Phrase::setRenderer($translationRenderer); + + try { + $block->setData('info', $payment); + + $info = $block->getSpecificInformation(); + } finally { + // No matter what, restore the renderer + Phrase::setRenderer($previousRenderer); + } + + // Assert the label was correctly translated + $this->assertSame($info['Super Cool String'], 'bar'); + $this->assertArrayNotHasKey('foo', $info); + $this->assertArrayNotHasKey('baz', $info); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Block/PaymentTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Block/PaymentTest.php new file mode 100644 index 000000000000..11ae27f9d2ea --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Block/PaymentTest.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Block; + +use Magento\AuthorizenetAcceptjs\Block\Payment; +use Magento\AuthorizenetAcceptjs\Model\Ui\ConfigProvider; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\View\Element\Template\Context; +use PHPUnit\Framework\MockObject\Builder\InvocationMocker; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class PaymentTest extends TestCase +{ + /** + * @var ConfigProvider|MockObject|InvocationMocker + */ + private $configMock; + + /** + * @var Payment + */ + private $block; + + protected function setUp() + { + $contextMock = $this->createMock(Context::class); + $this->configMock = $this->createMock(ConfigProvider::class); + $this->block = new Payment($contextMock, $this->configMock, new Json()); + } + + public function testConfigIsCreated() + { + $this->configMock->method('getConfig') + ->willReturn([ + 'payment' => [ + 'authorizenet_acceptjs' => [ + 'foo' => 'bar' + ] + ] + ]); + + $result = $this->block->getPaymentConfig(); + $expected = '{"foo":"bar","code":"authorizenet_acceptjs"}'; + $this->assertEquals($expected, $result); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Command/AcceptPaymentStrategyCommandTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Command/AcceptPaymentStrategyCommandTest.php new file mode 100644 index 000000000000..316fef544336 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Command/AcceptPaymentStrategyCommandTest.php @@ -0,0 +1,131 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Command; + +use Magento\AuthorizenetAcceptjs\Gateway\Command\AcceptPaymentStrategyCommand; +use Magento\AuthorizenetAcceptjs\Gateway\Command\RefundTransactionStrategyCommand; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Command\CommandPoolInterface; +use Magento\Payment\Gateway\Command\ResultInterface; +use Magento\Payment\Gateway\CommandInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class AcceptPaymentStrategyCommandTest extends TestCase +{ + /** + * @var CommandInterface|MockObject + */ + private $commandMock; + + /** + * @var CommandInterface|MockObject + */ + private $transactionDetailsCommandMock; + + /** + * @var CommandPoolInterface|MockObject + */ + private $commandPoolMock; + + /** + * @var RefundTransactionStrategyCommand + */ + private $command; + + /** + * @var ResultInterface|MockObject + */ + private $transactionResultMock; + + protected function setUp() + { + $this->transactionDetailsCommandMock = $this->createMock(CommandInterface::class); + $this->commandMock = $this->createMock(CommandInterface::class); + $this->transactionResultMock = $this->createMock(ResultInterface::class); + $this->commandPoolMock = $this->createMock(CommandPoolInterface::class); + $this->command = new AcceptPaymentStrategyCommand( + $this->commandPoolMock, + new SubjectReader() + ); + } + + /** + * @param string $status + * @dataProvider inReviewStatusesProvider + */ + public function testCommandWillAcceptInTheGatewayWhenInFDSReview(string $status) + { + // Assert command is executed + $this->commandMock->expects($this->once()) + ->method('execute'); + + $this->commandPoolMock->method('get') + ->willReturnMap([ + ['get_transaction_details', $this->transactionDetailsCommandMock], + ['accept_fds', $this->commandMock] + ]); + + $this->transactionResultMock->method('get') + ->willReturn([ + 'transaction' => [ + 'transactionStatus' => $status + ] + ]); + + $buildSubject = [ + 'foo' => '123' + ]; + + $this->transactionDetailsCommandMock->expects($this->once()) + ->method('execute') + ->with($buildSubject) + ->willReturn($this->transactionResultMock); + + $this->command->execute($buildSubject); + } + + public function testCommandWillDoNothingWhenTransactionHasAlreadyBeenAuthorized() + { + // Assert command is never executed + $this->commandMock->expects($this->never()) + ->method('execute'); + + $this->commandPoolMock->method('get') + ->willReturnMap([ + ['get_transaction_details', $this->transactionDetailsCommandMock], + ]); + + $this->transactionResultMock->method('get') + ->willReturn([ + 'transaction' => [ + 'transactionStatus' => 'anythingelseisfine' + ] + ]); + + $buildSubject = [ + 'foo' => '123' + ]; + + $this->transactionDetailsCommandMock->expects($this->once()) + ->method('execute') + ->with($buildSubject) + ->willReturn($this->transactionResultMock); + + $this->command->execute($buildSubject); + } + + public function inReviewStatusesProvider() + { + return [ + ['FDSPendingReview'], + ['FDSAuthorizedPendingReview'] + ]; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Command/CaptureStrategyCommandTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Command/CaptureStrategyCommandTest.php new file mode 100644 index 000000000000..4cbded976479 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Command/CaptureStrategyCommandTest.php @@ -0,0 +1,181 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Command; + +use Magento\AuthorizenetAcceptjs\Gateway\Command\CaptureStrategyCommand; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\Search\SearchCriteria; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Payment\Gateway\Command\CommandPoolInterface; +use Magento\Payment\Gateway\Command\GatewayCommand; +use Magento\Payment\Gateway\Data\PaymentDataObject; +use Magento\Sales\Api\Data\TransactionSearchResultInterface; +use Magento\Sales\Api\TransactionRepositoryInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class CaptureStrategyCommandTest extends TestCase +{ + /** + * @var CaptureStrategyCommand + */ + private $strategyCommand; + + /** + * @var CommandPoolInterface|MockObject + */ + private $commandPoolMock; + + /** + * @var TransactionRepositoryInterface|MockObject + */ + private $transactionRepositoryMock; + + /** + * @var FilterBuilder|MockObject + */ + private $filterBuilderMock; + + /** + * @var SearchCriteriaBuilder|MockObject + */ + private $searchCriteriaBuilderMock; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var PaymentDataObject|MockObject + */ + private $paymentDOMock; + + /** + * @var GatewayCommand|MockObject + */ + private $commandMock; + + /** + * @var TransactionSearchResultInterface|MockObject + */ + private $transactionsResult; + + protected function setUp() + { + // Simple mocks + $this->paymentDOMock = $this->createMock(PaymentDataObject::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + $this->commandMock = $this->createMock(GatewayCommand::class); + $this->commandPoolMock = $this->createMock(CommandPoolInterface::class); + $this->searchCriteriaBuilderMock = $this->createMock(SearchCriteriaBuilder::class); + $this->transactionRepositoryMock = $this->createMock(TransactionRepositoryInterface::class); + + // The search criteria builder should return the criteria with the specified filters + $this->filterBuilderMock = $this->createMock(FilterBuilder::class); + // We aren't coupling the implementation to the test. The test only cares how the result is processed + $this->filterBuilderMock->method('setField') + ->willReturnSelf(); + $this->filterBuilderMock->method('setValue') + ->willReturnSelf(); + $searchCriteria = new SearchCriteria(); + $this->searchCriteriaBuilderMock->method('addFilters') + ->willReturnSelf(); + $this->searchCriteriaBuilderMock->method('create') + ->willReturn($searchCriteria); + // The transaction result can be customized per test to simulate different scenarios + $this->transactionsResult = $this->createMock(TransactionSearchResultInterface::class); + $this->transactionRepositoryMock->method('getList') + ->with($searchCriteria) + ->willReturn($this->transactionsResult); + + $this->strategyCommand = new CaptureStrategyCommand( + $this->commandPoolMock, + $this->transactionRepositoryMock, + $this->filterBuilderMock, + $this->searchCriteriaBuilderMock, + new SubjectReader() + ); + } + + public function testExecuteWillAuthorizeWhenNotAuthorizedAndNotCaptured() + { + $subject = ['payment' => $this->paymentDOMock]; + + // Hasn't been authorized + $this->paymentMock->method('getAuthorizationTransaction') + ->willReturn(false); + // Hasn't been captured + $this->transactionsResult->method('getTotalCount') + ->willReturn(0); + // Assert authorize command was used + $this->commandPoolMock->expects($this->once()) + ->method('get') + ->with('sale') + ->willReturn($this->commandMock); + // Assert execute was called and with correct data + $this->commandMock->expects($this->once()) + ->method('execute') + ->with($subject); + + $this->strategyCommand->execute($subject); + // Assertions are performed via mock expects above + } + + public function testExecuteWillAuthorizeAndCaptureWhenAlreadyCaptured() + { + $subject = ['payment' => $this->paymentDOMock]; + + // Already authorized + $this->paymentMock->method('getAuthorizationTransaction') + ->willReturn(true); + // And already captured + $this->transactionsResult->method('getTotalCount') + ->willReturn(1); + // Assert authorize command was used + $this->commandPoolMock->expects($this->once()) + ->method('get') + ->with('settle') + ->willReturn($this->commandMock); + // Assert execute was called and with correct data + $this->commandMock->expects($this->once()) + ->method('execute') + ->with($subject); + + $this->strategyCommand->execute($subject); + // Assertions are performed via mock expects above + } + + public function testExecuteWillCaptureWhenAlreadyAuthorizedButNotCaptured() + { + $subject = ['payment' => $this->paymentDOMock]; + + // Was already authorized + $this->paymentMock->method('getAuthorizationTransaction') + ->willReturn(true); + // But, hasn't been captured + $this->transactionsResult->method('getTotalCount') + ->willReturn(0); + // Assert authorize command was used + $this->commandPoolMock->expects($this->once()) + ->method('get') + ->with('settle') + ->willReturn($this->commandMock); + // Assert execute was called and with correct data + $this->commandMock->expects($this->once()) + ->method('execute') + ->with($subject); + + $this->strategyCommand->execute($subject); + // Assertions are performed via mock expects above + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Command/FetchTransactionInfoCommandTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Command/FetchTransactionInfoCommandTest.php new file mode 100644 index 000000000000..757500c7e50e --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Command/FetchTransactionInfoCommandTest.php @@ -0,0 +1,132 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Command; + +use Magento\AuthorizenetAcceptjs\Gateway\Command\FetchTransactionInfoCommand; +use Magento\AuthorizenetAcceptjs\Gateway\Config; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Command\CommandPoolInterface; +use Magento\Payment\Gateway\Command\ResultInterface; +use Magento\Payment\Gateway\CommandInterface; +use Magento\Payment\Gateway\Data\PaymentDataObject; +use Magento\Payment\Gateway\Response\HandlerInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class FetchTransactionInfoCommandTest extends TestCase +{ + /** + * @var CommandInterface|MockObject + */ + private $transactionDetailsCommandMock; + + /** + * @var CommandPoolInterface|MockObject + */ + private $commandPoolMock; + + /** + * @var FetchTransactionInfoCommand + */ + private $command; + + /** + * @var ResultInterface|MockObject + */ + private $transactionResultMock; + + /** + * @var PaymentDataObject|MockObject + */ + private $paymentDOMock; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var Config + */ + private $configMock; + + /** + * @var HandlerInterface + */ + private $handlerMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObject::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + $this->configMock = $this->createMock(Config::class); + $this->configMock->method('getTransactionInfoSyncKeys') + ->willReturn(['foo', 'bar']); + $orderMock = $this->createMock(Order::class); + $this->paymentDOMock->method('getOrder') + ->willReturn($orderMock); + $this->transactionDetailsCommandMock = $this->createMock(CommandInterface::class); + $this->transactionResultMock = $this->createMock(ResultInterface::class); + $this->commandPoolMock = $this->createMock(CommandPoolInterface::class); + $this->handlerMock = $this->createMock(HandlerInterface::class); + $this->command = new FetchTransactionInfoCommand( + $this->commandPoolMock, + new SubjectReader(), + $this->configMock, + $this->handlerMock + ); + } + + public function testCommandWillMarkTransactionAsApprovedWhenNotVoid() + { + $response = [ + 'transaction' => [ + 'transactionStatus' => 'authorizedPendingCapture', + 'foo' => 'abc', + 'bar' => 'cba', + 'dontreturnme' => 'justdont' + ] + ]; + + $this->commandPoolMock->method('get') + ->willReturnMap([ + ['get_transaction_details', $this->transactionDetailsCommandMock], + ]); + + $this->transactionResultMock->method('get') + ->willReturn($response); + + $buildSubject = [ + 'payment' => $this->paymentDOMock + ]; + + $this->transactionDetailsCommandMock->expects($this->once()) + ->method('execute') + ->with($buildSubject) + ->willReturn($this->transactionResultMock); + + $this->handlerMock->expects($this->once()) + ->method('handle') + ->with($buildSubject, $response) + ->willReturn($this->transactionResultMock); + + $result = $this->command->execute($buildSubject); + + $expected = [ + 'foo' => 'abc', + 'bar' => 'cba' + ]; + + $this->assertSame($expected, $result); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Command/GatewayQueryCommandTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Command/GatewayQueryCommandTest.php new file mode 100644 index 000000000000..e37db3493638 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Command/GatewayQueryCommandTest.php @@ -0,0 +1,196 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Command; + +use Magento\AuthorizenetAcceptjs\Gateway\Command\GatewayQueryCommand; +use Magento\Payment\Gateway\Command\Result\ArrayResult; +use Magento\Payment\Gateway\Http\ClientInterface; +use Magento\Payment\Gateway\Http\TransferFactoryInterface; +use Magento\Payment\Gateway\Http\TransferInterface; +use Magento\Payment\Gateway\Request\BuilderInterface; +use Magento\Payment\Gateway\Validator\Result; +use Magento\Payment\Gateway\Validator\ValidatorInterface; +use PHPUnit\Framework\MockObject\Builder\InvocationMocker; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +class GatewayQueryCommandTest extends TestCase +{ + /** + * @var GatewayQueryCommand + */ + private $command; + + /** + * @var BuilderInterface|MockObject|InvocationMocker + */ + private $requestBuilderMock; + + /** + * @var TransferFactoryInterface|MockObject|InvocationMocker + */ + private $transferFactoryMock; + + /** + * @var ClientInterface|MockObject|InvocationMocker + */ + private $clientMock; + + /** + * @var LoggerInterface|MockObject|InvocationMocker + */ + private $loggerMock; + + /** + * @var ValidatorInterface|MockObject|InvocationMocker + */ + private $validatorMock; + + /** + * @var TransferInterface|MockObject|InvocationMocker + */ + private $transferMock; + + protected function setUp() + { + $this->requestBuilderMock = $this->createMock(BuilderInterface::class); + $this->transferFactoryMock = $this->createMock(TransferFactoryInterface::class); + $this->transferMock = $this->createMock(TransferInterface::class); + $this->clientMock = $this->createMock(ClientInterface::class); + $this->loggerMock = $this->createMock(LoggerInterface::class); + $this->validatorMock = $this->createMock(ValidatorInterface::class); + + $this->command = new GatewayQueryCommand( + $this->requestBuilderMock, + $this->transferFactoryMock, + $this->clientMock, + $this->loggerMock, + $this->validatorMock + ); + } + + public function testNormalExecution() + { + $buildSubject = [ + 'foo' => '123' + ]; + + $request = [ + 'bar' => '321' + ]; + + $response = [ + 'transaction' => [ + 'transactionType' => 'foo', + 'transactionStatus' => 'bar', + 'responseCode' => 'baz' + ] + ]; + + $validationSubject = $buildSubject; + $validationSubject['response'] = $response; + + $this->requestBuilderMock->method('build') + ->with($buildSubject) + ->willReturn($request); + + $this->transferFactoryMock->method('create') + ->with($request) + ->willReturn($this->transferMock); + + $this->clientMock->method('placeRequest') + ->with($this->transferMock) + ->willReturn($response); + + $this->validatorMock->method('validate') + ->with($validationSubject) + ->willReturn(new Result(true)); + + $result = $this->command->execute($buildSubject); + + $this->assertInstanceOf(ArrayResult::class, $result); + $this->assertEquals($response, $result->get()); + } + + /** + * @expectedExceptionMessage There was an error while trying to process the request. + * @expectedException \Magento\Payment\Gateway\Command\CommandException + */ + public function testExceptionIsThrownAndLoggedWhenRequestFails() + { + $buildSubject = [ + 'foo' => '123' + ]; + + $request = [ + 'bar' => '321' + ]; + + $this->requestBuilderMock->method('build') + ->with($buildSubject) + ->willReturn($request); + + $this->transferFactoryMock->method('create') + ->with($request) + ->willReturn($this->transferMock); + + $e = new \Exception('foobar'); + + $this->clientMock->method('placeRequest') + ->with($this->transferMock) + ->willThrowException($e); + + // assert the exception is logged + $this->loggerMock->expects($this->once()) + ->method('critical') + ->with($e); + + $this->command->execute($buildSubject); + } + /** + * @expectedExceptionMessage There was an error while trying to process the request. + * @expectedException \Magento\Payment\Gateway\Command\CommandException + */ + public function testExceptionIsThrownWhenResponseIsInvalid() + { + $buildSubject = [ + 'foo' => '123' + ]; + + $request = [ + 'bar' => '321' + ]; + + $response = [ + 'baz' => '456' + ]; + + $validationSubject = $buildSubject; + $validationSubject['response'] = $response; + + $this->requestBuilderMock->method('build') + ->with($buildSubject) + ->willReturn($request); + + $this->transferFactoryMock->method('create') + ->with($request) + ->willReturn($this->transferMock); + + $this->clientMock->method('placeRequest') + ->with($this->transferMock) + ->willReturn($response); + + $this->validatorMock->method('validate') + ->with($validationSubject) + ->willReturn(new Result(false)); + + $this->command->execute($buildSubject); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Command/RefundTransactionStrategyCommandTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Command/RefundTransactionStrategyCommandTest.php new file mode 100644 index 000000000000..df6d89d7bc58 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Command/RefundTransactionStrategyCommandTest.php @@ -0,0 +1,153 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Command; + +use Magento\AuthorizenetAcceptjs\Gateway\Command\RefundTransactionStrategyCommand; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Command\CommandPoolInterface; +use Magento\Payment\Gateway\Command\ResultInterface; +use Magento\Payment\Gateway\CommandInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class RefundTransactionStrategyCommandTest extends TestCase +{ + /** + * @var CommandInterface|MockObject + */ + private $commandMock; + + /** + * @var CommandInterface|MockObject + */ + private $transactionDetailsCommandMock; + + /** + * @var CommandPoolInterface|MockObject + */ + private $commandPoolMock; + + /** + * @var RefundTransactionStrategyCommand + */ + private $command; + + /** + * @var ResultInterface|MockObject + */ + private $transactionResultMock; + + protected function setUp() + { + $this->transactionDetailsCommandMock = $this->createMock(CommandInterface::class); + $this->commandMock = $this->createMock(CommandInterface::class); + $this->transactionResultMock = $this->createMock(ResultInterface::class); + $this->commandPoolMock = $this->createMock(CommandPoolInterface::class); + $this->command = new RefundTransactionStrategyCommand( + $this->commandPoolMock, + new SubjectReader() + ); + } + + public function testCommandWillVoidWhenTransactionIsPendingSettlement() + { + // Assert command is executed + $this->commandMock->expects($this->once()) + ->method('execute'); + + $this->commandPoolMock->method('get') + ->willReturnMap([ + ['get_transaction_details', $this->transactionDetailsCommandMock], + ['void', $this->commandMock] + ]); + + $this->transactionResultMock->method('get') + ->willReturn([ + 'transaction' => [ + 'transactionStatus' => 'capturedPendingSettlement' + ] + ]); + + $buildSubject = [ + 'foo' => '123' + ]; + + $this->transactionDetailsCommandMock->expects($this->once()) + ->method('execute') + ->with($buildSubject) + ->willReturn($this->transactionResultMock); + + $this->command->execute($buildSubject); + } + + public function testCommandWillRefundWhenTransactionIsSettled() + { + // Assert command is executed + $this->commandMock->expects($this->once()) + ->method('execute'); + + $this->commandPoolMock->method('get') + ->willReturnMap([ + ['get_transaction_details', $this->transactionDetailsCommandMock], + ['refund_settled', $this->commandMock] + ]); + + $this->transactionResultMock->method('get') + ->willReturn([ + 'transaction' => [ + 'transactionStatus' => 'settledSuccessfully' + ] + ]); + + $buildSubject = [ + 'foo' => '123' + ]; + + $this->transactionDetailsCommandMock->expects($this->once()) + ->method('execute') + ->with($buildSubject) + ->willReturn($this->transactionResultMock); + + $this->command->execute($buildSubject); + } + + /** + * @expectedException \Magento\Payment\Gateway\Command\CommandException + * @expectedExceptionMessage This transaction cannot be refunded with its current status. + */ + public function testCommandWillThrowExceptionWhenTransactionIsInInvalidState() + { + // Assert command is never executed + $this->commandMock->expects($this->never()) + ->method('execute'); + + $this->commandPoolMock->method('get') + ->willReturnMap([ + ['get_transaction_details', $this->transactionDetailsCommandMock], + ]); + + $this->transactionResultMock->method('get') + ->willReturn([ + 'transaction' => [ + 'transactionStatus' => 'somethingIsWrong' + ] + ]); + + $buildSubject = [ + 'foo' => '123' + ]; + + $this->transactionDetailsCommandMock->expects($this->once()) + ->method('execute') + ->with($buildSubject) + ->willReturn($this->transactionResultMock); + + $this->command->execute($buildSubject); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/ConfigTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/ConfigTest.php new file mode 100644 index 000000000000..da2b953d843b --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/ConfigTest.php @@ -0,0 +1,126 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway; + +use Magento\AuthorizenetAcceptjs\Gateway\Config; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Store\Model\ScopeInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class ConfigTest extends TestCase +{ + /** + * @var Config + */ + private $model; + + /** + * @var ScopeConfigInterface|MockObject + */ + private $scopeConfigMock; + + protected function setUp() + { + $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); + + $objectManager = new ObjectManager($this); + $this->model = $objectManager->getObject( + Config::class, + [ + 'scopeConfig' => $this->scopeConfigMock, + 'methodCode' => Config::METHOD, + ] + ); + } + + /** + * @param $getterName + * @param $configField + * @param $configValue + * @param $expectedValue + * @dataProvider configMapProvider + */ + public function testConfigGetters($getterName, $configField, $configValue, $expectedValue) + { + $this->scopeConfigMock->method('getValue') + ->with($this->getPath($configField), ScopeInterface::SCOPE_STORE, 123) + ->willReturn($configValue); + $this->assertEquals($expectedValue, $this->model->{$getterName}(123)); + } + + /** + * @dataProvider environmentUrlProvider + * @param $environment + * @param $expectedUrl + */ + public function testGetApiUrl($environment, $expectedUrl) + { + $this->scopeConfigMock->method('getValue') + ->with($this->getPath('environment'), ScopeInterface::SCOPE_STORE, 123) + ->willReturn($environment); + $this->assertEquals($expectedUrl, $this->model->getApiUrl(123)); + } + + /** + * @dataProvider environmentSolutionProvider + * @param $environment + * @param $expectedSolution + */ + public function testGetSolutionIdSandbox($environment, $expectedSolution) + { + $this->scopeConfigMock->method('getValue') + ->with($this->getPath('environment'), ScopeInterface::SCOPE_STORE, 123) + ->willReturn($environment); + $this->assertEquals($expectedSolution, $this->model->getSolutionId(123)); + } + + public function configMapProvider() + { + return [ + ['getLoginId', 'login', 'username', 'username'], + ['getEnvironment', 'environment', 'production', 'production'], + ['getClientKey', 'public_client_key', 'abc', 'abc'], + ['getTransactionKey', 'trans_key', 'password', 'password'], + ['getLegacyTransactionHash', 'trans_md5', 'abc123', 'abc123'], + ['getTransactionSignatureKey', 'trans_signature_key', 'abc123', 'abc123'], + ['getPaymentAction', 'payment_action', 'authorize', 'authorize'], + ['shouldEmailCustomer', 'email_customer', true, true], + ['isCvvEnabled', 'cvv_enabled', true, true], + ['getAdditionalInfoKeys', 'paymentInfoKeys', 'a,b,c', ['a', 'b', 'c']], + ['getTransactionInfoSyncKeys', 'transactionSyncKeys', 'a,b,c', ['a', 'b', 'c']], + ]; + } + public function environmentUrlProvider() + { + return [ + ['sandbox', 'https://apitest.authorize.net/xml/v1/request.api'], + ['production', 'https://api.authorize.net/xml/v1/request.api'], + ]; + } + + public function environmentSolutionProvider() + { + return [ + ['sandbox', 'AAA102993'], + ['production', 'AAA175350'], + ]; + } + + /** + * Return config path + * + * @param string $field + * @return string + */ + private function getPath($field) + { + return sprintf(Config::DEFAULT_PATH_PATTERN, Config::METHOD, $field); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Http/ClientTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Http/ClientTest.php new file mode 100644 index 000000000000..4086195ff4c9 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Http/ClientTest.php @@ -0,0 +1,218 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Http; + +use Magento\AuthorizenetAcceptjs\Gateway\Http\Client; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\HTTP\ZendClientFactory; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Payment\Gateway\Http\TransferInterface; +use Magento\Payment\Model\Method\Logger; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; +use Zend_Http_Client; +use Zend_Http_Response; + +class ClientTest extends TestCase +{ + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var Logger + */ + private $paymentLogger; + + /** + * @var ZendClientFactory + */ + private $httpClientFactory; + + /** + * @var Zend_Http_Client + */ + private $httpClient; + + /** + * @var Zend_Http_Response + */ + private $httpResponse; + + /** + * @var LoggerInterface + */ + private $logger; + + protected function setUp() + { + $this->objectManager = new ObjectManager($this); + $this->paymentLogger = $this->createMock(Logger::class); + $this->httpClientFactory = $this->createMock(ZendClientFactory::class); + $this->httpClient = $this->createMock(Zend_Http_Client::class); + $this->httpResponse = $this->createMock(Zend_Http_Response::class); + $this->httpClientFactory->method('create')->will($this->returnValue($this->httpClient)); + $this->httpClient->method('request') + ->willReturn($this->httpResponse); + /** @var MockObject $logger */ + $this->logger = $this->createMock(LoggerInterface::class); + } + + public function testCanSendRequest() + { + // Assert the raw data was set on the client + $this->httpClient->expects($this->once()) + ->method('setRawData') + ->with( + '{"doSomeThing":{"foobar":"baz"}}', + 'application/json' + ); + + $request = [ + 'payload_type' => 'doSomeThing', + 'foobar' => 'baz' + ]; + // Authorize.net returns a BOM and refuses to fix it + $response = pack('CCC', 0xef, 0xbb, 0xbf) . '{"foo":{"bar":"baz"}}'; + + $this->httpResponse->method('getBody') + ->willReturn($response); + + // Assert the logger was given the data + $this->paymentLogger->expects($this->once()) + ->method('debug') + ->with(['request' => $request, 'response' => '{"foo":{"bar":"baz"}}']); + + /** + * @var $apiClient Client + */ + $apiClient = $this->objectManager->getObject(Client::class, [ + 'httpClientFactory' => $this->httpClientFactory, + 'paymentLogger' => $this->paymentLogger, + 'json' => new Json() + ]); + + $result = $apiClient->placeRequest($this->getTransferObjectMock($request)); + + $this->assertSame('baz', $result['foo']['bar']); + } + + /** + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Something went wrong in the payment gateway. + */ + public function testExceptionIsThrownWhenEmptyResponseIsReceived() + { + // Assert the client has the raw data set + $this->httpClient->expects($this->once()) + ->method('setRawData') + ->with( + '{"doSomeThing":{"foobar":"baz"}}', + 'application/json' + ); + + $this->httpResponse->method('getBody') + ->willReturn(''); + + // Assert the exception is given to the logger + $this->logger->expects($this->once()) + ->method('critical') + ->with($this->callback(function ($e) { + return $e instanceof \Exception + && $e->getMessage() === 'Invalid JSON was returned by the gateway'; + })); + + $request = [ + 'payload_type' => 'doSomeThing', + 'foobar' => 'baz' + ]; + + // Assert the logger was given the data + $this->paymentLogger->expects($this->once()) + ->method('debug') + ->with(['request' => $request, 'response' => '']); + + /** + * @var $apiClient Client + */ + $apiClient = $this->objectManager->getObject(Client::class, [ + 'httpClientFactory' => $this->httpClientFactory, + 'paymentLogger' => $this->paymentLogger, + 'logger' => $this->logger, + 'json' => new Json() + ]); + + $apiClient->placeRequest($this->getTransferObjectMock($request)); + } + + /** + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Something went wrong in the payment gateway. + */ + public function testExceptionIsThrownWhenInvalidResponseIsReceived() + { + // Assert the client was given the raw data + $this->httpClient->expects($this->once()) + ->method('setRawData') + ->with( + '{"doSomeThing":{"foobar":"baz"}}', + 'application/json' + ); + + $this->httpResponse->method('getBody') + ->willReturn('bad'); + + $request = [ + 'payload_type' => 'doSomeThing', + 'foobar' => 'baz' + ]; + + // Assert the logger was given the data + $this->paymentLogger->expects($this->once()) + ->method('debug') + ->with(['request' => $request, 'response' => 'bad']); + + // Assert the exception was given to the logger + $this->logger->expects($this->once()) + ->method('critical') + ->with($this->callback(function ($e) { + return $e instanceof \Exception + && $e->getMessage() === 'Invalid JSON was returned by the gateway'; + })); + + /** + * @var $apiClient Client + */ + $apiClient = $this->objectManager->getObject(Client::class, [ + 'httpClientFactory' => $this->httpClientFactory, + 'paymentLogger' => $this->paymentLogger, + 'logger' => $this->logger, + 'json' => new Json() + ]); + + $apiClient->placeRequest($this->getTransferObjectMock($request)); + } + + /** + * Creates mock object for TransferInterface. + * + * @return TransferInterface|MockObject + */ + private function getTransferObjectMock(array $data) + { + $transferObjectMock = $this->createMock(TransferInterface::class); + $transferObjectMock->method('getBody') + ->willReturn($data); + + return $transferObjectMock; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Http/Payload/Filter/RemoveFieldsFilterTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Http/Payload/Filter/RemoveFieldsFilterTest.php new file mode 100644 index 000000000000..bcc6279f5b1f --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Http/Payload/Filter/RemoveFieldsFilterTest.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Http\Payload\Filter; + +use Magento\AuthorizenetAcceptjs\Gateway\Http\Payload\Filter\RemoveFieldsFilter; +use PHPUnit\Framework\TestCase; + +class RemoveFieldsFilterTest extends TestCase +{ + public function testFilterRemovesFields() + { + $filter = new RemoveFieldsFilter(['foo', 'bar']); + + $actual = $filter->filter([ + 'some' => 123, + 'data' => 321, + 'foo' => 'to', + 'filter' => ['blah'], + 'bar' => 'fields from' + ]); + + $expected = [ + 'some' => 123, + 'data' => 321, + 'filter' => ['blah'], + ]; + + $this->assertEquals($expected, $actual); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Http/TransferFactoryTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Http/TransferFactoryTest.php new file mode 100644 index 000000000000..954fd9782bd3 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Http/TransferFactoryTest.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Http; + +use Magento\AuthorizenetAcceptjs\Gateway\Http\Payload\Filter\RemoveFieldsFilter; +use Magento\AuthorizenetAcceptjs\Gateway\Http\TransferFactory; +use Magento\Payment\Gateway\Http\TransferBuilder; +use Magento\Payment\Gateway\Http\TransferInterface; +use Magento\AuthorizenetAcceptjs\Gateway\Http\Payload\FilterInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class TransferFactoryTest extends TestCase +{ + /** + * @var TransferFactory + */ + private $transferFactory; + + /** + * @var TransferFactory + */ + private $transferMock; + + /** + * @var TransferBuilder|MockObject + */ + private $transferBuilder; + + /** + * @var FilterInterface|MockObject + */ + private $filterMock; + + protected function setUp() + { + $this->transferBuilder = $this->createMock(TransferBuilder::class); + $this->transferMock = $this->createMock(TransferInterface::class); + $this->filterMock = $this->createMock(RemoveFieldsFilter::class); + + $this->transferFactory = new TransferFactory( + $this->transferBuilder, + [$this->filterMock] + ); + } + + public function testCreate() + { + $request = ['data1', 'data2']; + + // Assert the filter was created + $this->filterMock->expects($this->once()) + ->method('filter') + ->with($request) + ->willReturn($request); + + // Assert the body of the transfer was set + $this->transferBuilder->expects($this->once()) + ->method('setBody') + ->with($request) + ->willReturnSelf(); + + $this->transferBuilder->method('build') + ->willReturn($this->transferMock); + + $this->assertEquals($this->transferMock, $this->transferFactory->create($request)); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AcceptFdsDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AcceptFdsDataBuilderTest.php new file mode 100644 index 000000000000..00bb7ee84f98 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AcceptFdsDataBuilderTest.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\AcceptFdsDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Sales\Model\Order\Payment\Transaction; +use PHPUnit\Framework\TestCase; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; + +class AcceptFdsDataBuilderTest extends TestCase +{ + /** + * @var AcceptFdsDataBuilder + */ + private $builder; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var Payment|MockObject + */ + private $paymentDOMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->orderMock = $this->createMock(Order::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + + $this->builder = new AcceptFdsDataBuilder(new SubjectReader()); + } + + public function testBuild() + { + $transactionMock = $this->createMock(Transaction::class); + + $this->paymentMock->method('getAuthorizationTransaction') + ->willReturn($transactionMock); + + $transactionMock->method('getTxnId') + ->willReturn('foo'); + + $expected = [ + 'heldTransactionRequest' => [ + 'action' => 'approve', + 'refTransId' => 'foo' + ] + ]; + + $buildSubject = [ + 'payment' => $this->paymentDOMock, + ]; + + $this->assertEquals($expected, $this->builder->build($buildSubject)); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AddressDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AddressDataBuilderTest.php new file mode 100644 index 000000000000..6ddb30a64af9 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AddressDataBuilderTest.php @@ -0,0 +1,129 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\AddressDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\AddressAdapterInterface; +use Magento\Payment\Gateway\Data\OrderAdapterInterface; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class AddressDataBuilderTest extends TestCase +{ + /** + * @var AddressDataBuilder + */ + private $builder; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var PaymentDataObjectInterface|MockObject + */ + private $paymentDOMock; + + /** + * @var OrderAdapterInterface|MockObject + */ + private $orderMock; + + private $mockAddressData = [ + 'firstName' => [ + 'method' => 'getFirstname', + 'sampleData' => 'John' + ], + 'lastName' => [ + 'method' => 'getLastname', + 'sampleData' => 'Doe' + ], + 'company' => [ + 'method' => 'getCompany', + 'sampleData' => 'Magento' + ], + 'address' => [ + 'method' => 'getStreetLine1', + 'sampleData' => '11501 Domain Dr' + ], + 'city' => [ + 'method' => 'getCity', + 'sampleData' => 'Austin' + ], + 'state' => [ + 'method' => 'getRegionCode', + 'sampleData' => 'TX' + ], + 'zip' => [ + 'method' => 'getPostcode', + 'sampleData' => '78758' + ], + 'country' => [ + 'method' => 'getCountryId', + 'sampleData' => 'US' + ], + ]; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + $this->orderMock = $this->createMock(OrderAdapterInterface::class); + $this->paymentDOMock->method('getOrder') + ->willReturn($this->orderMock); + + $this->builder = new AddressDataBuilder(new SubjectReader()); + } + + public function testBuildWithBothAddresses() + { + $billingAddress = $this->createAddressMock('billing'); + $shippingAddress = $this->createAddressMock('shipping'); + $this->orderMock->method('getBillingAddress') + ->willReturn($billingAddress); + $this->orderMock->method('getShippingAddress') + ->willReturn($shippingAddress); + $this->orderMock->method('getRemoteIp') + ->willReturn('abc'); + + $buildSubject = [ + 'payment' => $this->paymentDOMock + ]; + + $result = $this->builder->build($buildSubject); + + $this->validateAddressData($result['transactionRequest']['billTo'], 'billing'); + $this->validateAddressData($result['transactionRequest']['shipTo'], 'shipping'); + $this->assertEquals('abc', $result['transactionRequest']['customerIP']); + } + + private function validateAddressData($responseData, $addressPrefix) + { + foreach ($this->mockAddressData as $fieldValue => $field) { + $this->assertEquals($addressPrefix . $field['sampleData'], $responseData[$fieldValue]); + } + } + + private function createAddressMock($prefix) + { + $addressAdapterMock = $this->createMock(AddressAdapterInterface::class); + + foreach ($this->mockAddressData as $field) { + $addressAdapterMock->method($field['method']) + ->willReturn($prefix . $field['sampleData']); + } + + return $addressAdapterMock; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AmountDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AmountDataBuilderTest.php new file mode 100644 index 000000000000..9da0139302a3 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AmountDataBuilderTest.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\AmountDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use PHPUnit\Framework\TestCase; + +class AmountDataBuilderTest extends TestCase +{ + /** + * @var AmountDataBuilder + */ + private $builder; + + protected function setUp() + { + $this->builder = new AmountDataBuilder( + new SubjectReader() + ); + } + + public function testBuild() + { + $expected = [ + 'transactionRequest' => [ + 'amount' => '123.45', + ] + ]; + + $buildSubject = [ + 'amount' => 123.45 + ]; + + $this->assertEquals($expected, $this->builder->build($buildSubject)); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AuthenticationDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AuthenticationDataBuilderTest.php new file mode 100644 index 000000000000..e9588e51b0fc --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AuthenticationDataBuilderTest.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Config; +use Magento\AuthorizenetAcceptjs\Gateway\Request\AuthenticationDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class AuthenticationDataBuilderTest extends TestCase +{ + /** + * @var AuthenticationDataBuilder + */ + private $builder; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var Payment|MockObject + */ + private $paymentDOMock; + + /** + * @var SubjectReader|MockObject + */ + private $subjectReaderMock; + + /** + * @var Config|MockObject + */ + private $configMock; + + protected function setUp() + { + $this->configMock = $this->createMock(Config::class); + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + /** @var MockObject|SubjectReader subjectReaderMock */ + $this->subjectReaderMock = $this->createMock(SubjectReader::class); + + $this->builder = new AuthenticationDataBuilder($this->subjectReaderMock, $this->configMock); + } + + public function testBuild() + { + $this->configMock->method('getLoginId') + ->willReturn('myloginid'); + $this->configMock->method('getTransactionKey') + ->willReturn('mytransactionkey'); + + $expected = [ + 'merchantAuthentication' => [ + 'name' => 'myloginid', + 'transactionKey' => 'mytransactionkey' + ] + ]; + + $buildSubject = []; + + $this->subjectReaderMock->method('readStoreId') + ->with($buildSubject) + ->willReturn(123); + + $this->assertEquals($expected, $this->builder->build($buildSubject)); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AuthorizationDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AuthorizationDataBuilderTest.php new file mode 100644 index 000000000000..438d681a2b5b --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/AuthorizationDataBuilderTest.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\AuthorizeDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\AuthorizenetAcceptjs\Model\PassthroughDataObject; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class AuthorizationDataBuilderTest extends TestCase +{ + /** + * @var AuthorizeDataBuilder + */ + private $builder; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var Payment|MockObject + */ + private $paymentDOMock; + + /** + * @var PassthroughDataObject + */ + private $passthroughData; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + $this->passthroughData = new PassthroughDataObject(); + + $this->builder = new AuthorizeDataBuilder( + new SubjectReader(), + $this->passthroughData + ); + } + + public function testBuildWillAddTransactionType() + { + $expected = [ + 'transactionRequest' => [ + 'transactionType' => 'authOnlyTransaction' + ] + ]; + + $buildSubject = [ + 'store_id' => 123, + 'payment' => $this->paymentDOMock, + ]; + + $this->assertEquals($expected, $this->builder->build($buildSubject)); + $this->assertEquals('authOnlyTransaction', $this->passthroughData->getData('transactionType')); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/CaptureDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/CaptureDataBuilderTest.php new file mode 100644 index 000000000000..537a685f1ff7 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/CaptureDataBuilderTest.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\CaptureDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\AuthorizenetAcceptjs\Model\PassthroughDataObject; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class CaptureDataBuilderTest extends TestCase +{ + /** + * @var CaptureDataBuilder + */ + private $builder; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var Payment|MockObject + */ + private $paymentDOMock; + + /** + * @var PassthroughDataObject + */ + private $passthroughData; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + $this->passthroughData = new PassthroughDataObject(); + + $this->builder = new CaptureDataBuilder( + new SubjectReader(), + $this->passthroughData + ); + } + + public function testBuildWillCaptureWhenAuthorizeTransactionExists() + { + $transactionMock = $this->createMock(Payment\Transaction::class); + $transactionMock->method('getAdditionalInformation') + ->with('real_transaction_id') + ->willReturn('prevtrans'); + $this->paymentMock->method('getAuthorizationTransaction') + ->willReturn($transactionMock); + + $expected = [ + 'transactionRequest' => [ + 'transactionType' => 'priorAuthCaptureTransaction', + 'refTransId' => 'prevtrans' + ] + ]; + + $buildSubject = [ + 'payment' => $this->paymentDOMock, + ]; + + $this->assertEquals($expected, $this->builder->build($buildSubject)); + $this->assertEquals('priorAuthCaptureTransaction', $this->passthroughData->getData('transactionType')); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/CustomSettingsBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/CustomSettingsBuilderTest.php new file mode 100644 index 000000000000..be7dd7eca176 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/CustomSettingsBuilderTest.php @@ -0,0 +1,74 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Config; +use Magento\AuthorizenetAcceptjs\Gateway\Request\CustomSettingsBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class CustomSettingsBuilderTest extends TestCase +{ + /** + * @var CustomSettingsBuilder + */ + private $builder; + + /** + * @var SubjectReader|MockObject + */ + private $subjectReaderMock; + + /** + * @var Config|MockObject + */ + private $configMock; + + protected function setUp() + { + $this->configMock = $this->createMock(Config::class); + /** @var MockObject|SubjectReader subjectReaderMock */ + $this->subjectReaderMock = $this->createMock(SubjectReader::class); + $this->subjectReaderMock->method('readStoreId') + ->willReturn('123'); + + $this->builder = new CustomSettingsBuilder($this->subjectReaderMock, $this->configMock); + } + + public function testBuildWithEmailCustomerDisabled() + { + $this->configMock->method('shouldEmailCustomer') + ->with('123') + ->willReturn(false); + + $this->assertEquals([], $this->builder->build([])); + } + + public function testBuildWithEmailCustomerEnabled() + { + $this->configMock->method('shouldEmailCustomer') + ->with('123') + ->willReturn(true); + + $expected = [ + 'transactionRequest' => [ + 'transactionSettings' => [ + 'setting' => [ + [ + 'settingName' => 'emailCustomer', + 'settingValue' => 'true' + ] + ] + ] + ] + ]; + + $this->assertEquals($expected, $this->builder->build([])); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/CustomerDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/CustomerDataBuilderTest.php new file mode 100644 index 000000000000..7c9116cad54b --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/CustomerDataBuilderTest.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\CustomerDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\AddressAdapterInterface; +use Magento\Payment\Gateway\Data\OrderAdapterInterface; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class CustomerDataBuilderTest extends TestCase +{ + /** + * @var CustomerDataBuilder + */ + private $builder; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var Payment|MockObject + */ + private $paymentDOMock; + + /** + * @var OrderAdapterInterface|MockObject + */ + private $orderMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + $this->orderMock = $this->createMock(OrderAdapterInterface::class); + $this->paymentDOMock->method('getOrder') + ->willReturn($this->orderMock); + + $this->builder = new CustomerDataBuilder(new SubjectReader()); + } + + public function testBuild() + { + $addressAdapterMock = $this->createMock(AddressAdapterInterface::class); + $addressAdapterMock->method('getEmail') + ->willReturn('foo@bar.com'); + $this->orderMock->method('getBillingAddress') + ->willReturn($addressAdapterMock); + $this->orderMock->method('getCustomerId') + ->willReturn('123'); + + $expected = [ + 'transactionRequest' => [ + 'customer' => [ + 'id' => '123', + 'email' => 'foo@bar.com' + ] + ] + ]; + + $buildSubject = [ + 'payment' => $this->paymentDOMock, + ]; + + $this->assertEquals($expected, $this->builder->build($buildSubject)); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/OrderDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/OrderDataBuilderTest.php new file mode 100644 index 000000000000..d66421d48ca8 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/OrderDataBuilderTest.php @@ -0,0 +1,74 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\OrderDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Payment\Model\InfoInterface; +use Magento\Payment\Gateway\Data\OrderAdapterInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class OrderDataBuilderTest extends TestCase +{ + /** + * @var OrderDataBuilder + */ + private $builder; + + /** + * @var InfoInterface|MockObject + */ + private $paymentMock; + + /** + * @var PaymentDataObjectInterface|MockObject + */ + private $paymentDOMock; + + /** + * @var OrderAdapterInterface|MockObject + */ + private $orderMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + $this->orderMock = $this->createMock(OrderAdapterInterface::class); + $this->paymentDOMock->method('getOrder') + ->willReturn($this->orderMock); + + $this->builder = new OrderDataBuilder(new SubjectReader()); + } + + public function testBuild() + { + $this->orderMock->method('getOrderIncrementId') + ->willReturn('10000015'); + + $expected = [ + 'transactionRequest' => [ + 'order' => [ + 'invoiceNumber' => '10000015' + ] + ] + ]; + + $buildSubject = [ + 'payment' => $this->paymentDOMock, + 'order' => $this->orderMock, + ]; + + $this->assertEquals($expected, $this->builder->build($buildSubject)); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/PassthroughDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/PassthroughDataBuilderTest.php new file mode 100644 index 000000000000..f4c5f56efe89 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/PassthroughDataBuilderTest.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\PassthroughDataBuilder; +use Magento\AuthorizenetAcceptjs\Model\PassthroughDataObject; +use PHPUnit\Framework\TestCase; + +class PassthroughDataBuilderTest extends TestCase +{ + public function testBuild() + { + $passthroughData = new PassthroughDataObject([ + 'foo' => 'bar', + 'baz' => 'bash' + ]); + $builder = new PassthroughDataBuilder($passthroughData); + + $expected = [ + 'transactionRequest' => [ + 'userFields' => [ + 'userField' => [ + [ + 'name' => 'foo', + 'value' => 'bar' + ], + [ + 'name' => 'baz', + 'value' => 'bash' + ], + ] + ] + ] + ]; + + $this->assertEquals($expected, $builder->build([])); + } + + public function testBuildWithNoData() + { + $passthroughData = new PassthroughDataObject(); + $builder = new PassthroughDataBuilder($passthroughData); + $expected = []; + + $this->assertEquals($expected, $builder->build([])); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/PaymentDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/PaymentDataBuilderTest.php new file mode 100644 index 000000000000..cf3842b8947b --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/PaymentDataBuilderTest.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\PaymentDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class PaymentDataBuilderTest extends TestCase +{ + /** + * @var PaymentDataBuilder + */ + private $builder; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var Payment|MockObject + */ + private $paymentDOMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->orderMock = $this->createMock(Order::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + + $this->builder = new PaymentDataBuilder(new SubjectReader()); + } + + public function testBuild() + { + $this->paymentMock->method('getAdditionalInformation') + ->willReturnMap([ + ['opaqueDataDescriptor', 'foo'], + ['opaqueDataValue', 'bar'] + ]); + + $expected = [ + 'transactionRequest' => [ + 'payment' => [ + 'opaqueData' => [ + 'dataDescriptor' => 'foo', + 'dataValue' => 'bar' + ] + ] + ] + ]; + + $buildSubject = [ + 'payment' => $this->paymentDOMock, + 'amount' => 123.45 + ]; + + $this->assertEquals($expected, $this->builder->build($buildSubject)); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/PoDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/PoDataBuilderTest.php new file mode 100644 index 000000000000..97b51c1e1807 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/PoDataBuilderTest.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\PoDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Payment\Model\InfoInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class PoDataBuilderTest extends TestCase +{ + /** + * @var PoDataBuilder + */ + private $builder; + + /** + * @var InfoInterface|MockObject + */ + private $paymentMock; + + /** + * @var PaymentDataObjectInterface|MockObject + */ + private $paymentDOMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + + $this->builder = new PoDataBuilder(new SubjectReader()); + } + + public function testBuild() + { + $this->paymentMock->method('getPoNumber') + ->willReturn('abc'); + + $expected = [ + 'transactionRequest' => [ + 'poNumber' => 'abc' + ] + ]; + + $buildSubject = [ + 'payment' => $this->paymentDOMock, + ]; + $this->assertEquals($expected, $this->builder->build($buildSubject)); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/RefundPaymentDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/RefundPaymentDataBuilderTest.php new file mode 100644 index 000000000000..c1879b3df83a --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/RefundPaymentDataBuilderTest.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\RefundPaymentDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class RefundPaymentDataBuilderTest extends TestCase +{ + /** + * @var RefundPaymentDataBuilder + */ + private $builder; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var Payment|MockObject + */ + private $paymentDOMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + + $this->builder = new RefundPaymentDataBuilder( + new SubjectReader() + ); + } + + public function testBuild() + { + $this->paymentMock->method('getAdditionalInformation') + ->with('ccLast4') + ->willReturn('1111'); + + $expected = [ + 'transactionRequest' => [ + 'payment' => [ + 'creditCard' => [ + 'cardNumber' => '1111', + 'expirationDate' => 'XXXX' + ] + ] + ] + ]; + + $buildSubject = [ + 'payment' => $this->paymentDOMock, + 'amount' => 123.45 + ]; + + $this->assertEquals($expected, $this->builder->build($buildSubject)); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/RefundReferenceTransactionDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/RefundReferenceTransactionDataBuilderTest.php new file mode 100644 index 000000000000..cf1803005ace --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/RefundReferenceTransactionDataBuilderTest.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\RefundReferenceTransactionDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Sales\Model\Order\Payment\Transaction; +use PHPUnit\Framework\TestCase; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; + +class RefundReferenceTransactionDataBuilderTest extends TestCase +{ + /** + * @var RefundReferenceTransactionDataBuilder + */ + private $builder; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var Payment|MockObject + */ + private $paymentDOMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->orderMock = $this->createMock(Order::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + + $this->builder = new RefundReferenceTransactionDataBuilder(new SubjectReader()); + } + + public function testBuild() + { + $transactionMock = $this->createMock(Transaction::class); + + $this->paymentMock->method('getAuthorizationTransaction') + ->willReturn($transactionMock); + + $transactionMock->method('getParentTxnId') + ->willReturn('foo'); + + $expected = [ + 'transactionRequest' => [ + 'refTransId' => 'foo' + ] + ]; + + $buildSubject = [ + 'payment' => $this->paymentDOMock, + ]; + + $this->assertEquals($expected, $this->builder->build($buildSubject)); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/RefundTransactionTypeDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/RefundTransactionTypeDataBuilderTest.php new file mode 100644 index 000000000000..4e0f5f75fb94 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/RefundTransactionTypeDataBuilderTest.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\RefundTransactionTypeDataBuilder; +use PHPUnit\Framework\TestCase; + +class RefundTransactionTypeDataBuilderTest extends TestCase +{ + private const REQUEST_TYPE_REFUND = 'refundTransaction'; + + public function testBuild() + { + $builder = new RefundTransactionTypeDataBuilder(); + + $expected = [ + 'transactionRequest' => [ + 'transactionType' => self::REQUEST_TYPE_REFUND + ] + ]; + + $this->assertEquals($expected, $builder->build([])); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/RequestTypeBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/RequestTypeBuilderTest.php new file mode 100644 index 000000000000..cb03dfc3dac5 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/RequestTypeBuilderTest.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\AuthenticationDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\Request\RequestTypeBuilder; +use PHPUnit\Framework\TestCase; + +class RequestTypeBuilderTest extends TestCase +{ + /** + * @var AuthenticationDataBuilder + */ + private $builder; + + protected function setUp() + { + $this->builder = new RequestTypeBuilder('foo'); + } + + public function testBuild() + { + $expected = [ + 'payload_type' => 'foo' + ]; + + $buildSubject = []; + $this->assertEquals($expected, $this->builder->build($buildSubject)); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/SaleDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/SaleDataBuilderTest.php new file mode 100644 index 000000000000..407b9bc85a2c --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/SaleDataBuilderTest.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\SaleDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\AuthorizenetAcceptjs\Model\PassthroughDataObject; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class SaleDataBuilderTest extends TestCase +{ + /** + * @var SaleDataBuilder + */ + private $builder; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var Payment|MockObject + */ + private $paymentDOMock; + + /** + * @var PassthroughDataObject + */ + private $passthroughData; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + $this->passthroughData = new PassthroughDataObject(); + + $this->builder = new SaleDataBuilder( + new SubjectReader(), + $this->passthroughData + ); + } + + public function testBuildWillAddTransactionType() + { + $expected = [ + 'transactionRequest' => [ + 'transactionType' => 'authCaptureTransaction' + ] + ]; + + $buildSubject = [ + 'store_id' => 123, + 'payment' => $this->paymentDOMock, + ]; + + $this->assertEquals($expected, $this->builder->build($buildSubject)); + $this->assertEquals('authCaptureTransaction', $this->passthroughData->getData('transactionType')); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/ShippingDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/ShippingDataBuilderTest.php new file mode 100644 index 000000000000..d6525e610a28 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/ShippingDataBuilderTest.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\ShippingDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class ShippingDataBuilderTest extends TestCase +{ + /** + * @var v + */ + private $builder; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var Payment|MockObject + */ + private $paymentDOMock; + + /** + * @var Order + */ + private $orderMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->orderMock = $this->createMock(Order::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + $this->paymentDOMock->method('getOrder') + ->willReturn($this->orderMock); + + $this->builder = new ShippingDataBuilder( + new SubjectReader() + ); + } + + public function testBuild() + { + $this->orderMock->method('getBaseShippingAmount') + ->willReturn('43.12'); + + $expected = [ + 'transactionRequest' => [ + 'shipping' => [ + 'amount' => '43.12' + ] + ] + ]; + + $buildSubject = [ + 'payment' => $this->paymentDOMock, + 'order' => $this->orderMock, + ]; + + $this->assertEquals($expected, $this->builder->build($buildSubject)); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/SolutionDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/SolutionDataBuilderTest.php new file mode 100644 index 000000000000..1b06546c2ea8 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/SolutionDataBuilderTest.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Config; +use Magento\AuthorizenetAcceptjs\Gateway\Request\SolutionDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class SolutionDataBuilderTest extends TestCase +{ + /** + * @var SolutionDataBuilder + */ + private $builder; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var Payment|MockObject + */ + private $paymentDOMock; + + /** + * @var SubjectReader|MockObject + */ + private $subjectReaderMock; + + /** + * @var Config|MockObject + */ + private $configMock; + + protected function setUp() + { + $this->configMock = $this->createMock(Config::class); + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + /** @var MockObject|SubjectReader subjectReaderMock */ + $this->subjectReaderMock = $this->createMock(SubjectReader::class); + + $this->builder = new SolutionDataBuilder($this->subjectReaderMock, $this->configMock); + } + + public function testBuild() + { + $this->subjectReaderMock->method('readStoreId') + ->willReturn('123'); + $this->configMock->method('getSolutionId') + ->with('123') + ->willReturn('solutionid'); + + $expected = [ + 'transactionRequest' => [ + 'solution' => [ + 'id' => 'solutionid', + ] + ] + ]; + + $buildSubject = []; + $this->assertEquals($expected, $this->builder->build($buildSubject)); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/StoreConfigBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/StoreConfigBuilderTest.php new file mode 100644 index 000000000000..2ed0cb13ed62 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/StoreConfigBuilderTest.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\StoreConfigBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Payment\Model\InfoInterface; +use Magento\Payment\Gateway\Data\OrderAdapterInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class StoreConfigBuilderTest extends TestCase +{ + /** + * @var StoreConfigBuilder + */ + private $builder; + + /** + * @var InfoInterface|MockObject + */ + private $paymentMock; + + /** + * @var PaymentDataObjectInterface|MockObject + */ + private $paymentDOMock; + + /** + * @var OrderAdapterInterface|MockObject + */ + private $orderMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(InfoInterface::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + $this->orderMock = $this->createMock(OrderAdapterInterface::class); + $this->paymentDOMock->method('getOrder') + ->willReturn($this->orderMock); + + $this->builder = new StoreConfigBuilder(new SubjectReader()); + } + + public function testBuild() + { + $this->orderMock->method('getStoreID') + ->willReturn(123); + + $expected = [ + 'store_id' => 123 + ]; + + $buildSubject = [ + 'payment' => $this->paymentDOMock, + ]; + $this->assertEquals($expected, $this->builder->build($buildSubject)); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/TransactionDetailsDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/TransactionDetailsDataBuilderTest.php new file mode 100644 index 000000000000..03c036c02714 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/TransactionDetailsDataBuilderTest.php @@ -0,0 +1,89 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\TransactionDetailsDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Sales\Model\Order\Payment\Transaction; +use PHPUnit\Framework\TestCase; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; + +class TransactionDetailsDataBuilderTest extends TestCase +{ + /** + * @var TransactionDetailsDataBuilder + */ + private $builder; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var Payment|MockObject + */ + private $paymentDOMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->orderMock = $this->createMock(Order::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + + $this->builder = new TransactionDetailsDataBuilder(new SubjectReader()); + } + + public function testBuild() + { + $transactionMock = $this->createMock(Transaction::class); + + $this->paymentMock->method('getAuthorizationTransaction') + ->willReturn($transactionMock); + + $transactionMock->method('getParentTxnId') + ->willReturn('foo'); + + $expected = [ + 'transId' => 'foo' + ]; + + $buildSubject = [ + 'payment' => $this->paymentDOMock, + ]; + + $this->assertEquals($expected, $this->builder->build($buildSubject)); + } + + public function testBuildWithIncludedTransactionId() + { + $transactionMock = $this->createMock(Transaction::class); + + $this->paymentMock->expects($this->never()) + ->method('getAuthorizationTransaction'); + + $transactionMock->expects($this->never()) + ->method('getParentTxnId'); + + $expected = [ + 'transId' => 'foo' + ]; + + $buildSubject = [ + 'payment' => $this->paymentDOMock, + 'transactionId' => 'foo' + ]; + + $this->assertEquals($expected, $this->builder->build($buildSubject)); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/VoidDataBuilderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/VoidDataBuilderTest.php new file mode 100644 index 000000000000..84460a1c744b --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Request/VoidDataBuilderTest.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Request; + +use Magento\AuthorizenetAcceptjs\Gateway\Request\VoidDataBuilder; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Sales\Model\Order\Payment; +use Magento\Sales\Model\Order\Payment\Transaction; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class VoidDataBuilderTest extends TestCase +{ + private const REQUEST_TYPE_VOID = 'voidTransaction'; + + /** + * @var VoidDataBuilder + */ + private $builder; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var Payment|MockObject + */ + private $paymentDOMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + + $this->builder = new VoidDataBuilder(new SubjectReader()); + } + + public function testBuild() + { + $transactionMock = $this->createMock(Transaction::class); + $this->paymentMock->method('getAuthorizationTransaction') + ->willReturn($transactionMock); + $transactionMock->method('getParentTxnId') + ->willReturn('myref'); + + $buildSubject = [ + 'payment' => $this->paymentDOMock + ]; + + $expected = [ + 'transactionRequest' => [ + 'transactionType' => self::REQUEST_TYPE_VOID, + 'refTransId' => 'myref', + ] + ]; + $this->assertEquals($expected, $this->builder->build($buildSubject)); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/CloseParentTransactionHandlerTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/CloseParentTransactionHandlerTest.php new file mode 100644 index 000000000000..e9929c631eb1 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/CloseParentTransactionHandlerTest.php @@ -0,0 +1,62 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Response; + +use Magento\AuthorizenetAcceptjs\Gateway\Response\CloseParentTransactionHandler; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Payment\Model\InfoInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class CloseParentTransactionHandlerTest extends TestCase +{ + /** + * @var CloseParentTransactionHandler + */ + private $handler; + + /** + * @var InfoInterface|MockObject + */ + private $paymentMock; + + /** + * @var PaymentDataObjectInterface|MockObject + */ + private $paymentDOMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + + $this->handler = new CloseParentTransactionHandler(new SubjectReader()); + } + + public function testHandleClosesTransactionByDefault() + { + $subject = [ + 'payment' => $this->paymentDOMock + ]; + $response = [ + 'transactionResponse' => [] + ]; + + // Assert the parent transaction i closed + $this->paymentMock->expects($this->once()) + ->method('setShouldCloseParentTransaction') + ->with(true); + + $this->handler->handle($subject, $response); + // Assertions are via mock expects above + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/CloseTransactionHandlerTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/CloseTransactionHandlerTest.php new file mode 100644 index 000000000000..a7093f0dac88 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/CloseTransactionHandlerTest.php @@ -0,0 +1,62 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Response; + +use Magento\AuthorizenetAcceptjs\Gateway\Response\CloseTransactionHandler; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Payment\Model\InfoInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class CloseTransactionHandlerTest extends TestCase +{ + /** + * @var CloseTransactionHandler + */ + private $handler; + + /** + * @var InfoInterface|MockObject + */ + private $paymentMock; + + /** + * @var PaymentDataObjectInterface|MockObject + */ + private $paymentDOMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + + $this->handler = new CloseTransactionHandler(new SubjectReader()); + } + + public function testHandleClosesTransactionByDefault() + { + $subject = [ + 'payment' => $this->paymentDOMock + ]; + $response = [ + 'transactionResponse' => [] + ]; + + // Assert the transaction is closed + $this->paymentMock->expects($this->once()) + ->method('setIsTransactionClosed') + ->with(true); + + $this->handler->handle($subject, $response); + // Assertions are via mock expects above + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/PaymentResponseHandlerTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/PaymentResponseHandlerTest.php new file mode 100644 index 000000000000..d051c7d2910a --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/PaymentResponseHandlerTest.php @@ -0,0 +1,112 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Response; + +use Magento\AuthorizenetAcceptjs\Gateway\Response\PaymentResponseHandler; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class PaymentResponseHandlerTest extends TestCase +{ + private const RESPONSE_CODE_APPROVED = 1; + private const RESPONSE_CODE_HELD = 4; + + /** + * @var PaymentResponseHandler + */ + private $builder; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var Payment|MockObject + */ + private $paymentDOMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + + $this->builder = new PaymentResponseHandler(new SubjectReader()); + } + + public function testHandleDefaultResponse() + { + $this->paymentMock->method('getAdditionalInformation') + ->with('ccLast4') + ->willReturn('1234'); + // Assert the avs code is saved + $this->paymentMock->expects($this->once()) + ->method('setCcAvsStatus') + ->with('avshurray'); + $this->paymentMock->expects($this->once()) + ->method('setCcLast4') + ->with('1234'); + $this->paymentMock->expects($this->once()) + ->method('setIsTransactionClosed') + ->with(false); + + $response = [ + 'transactionResponse' => [ + 'avsResultCode' => 'avshurray', + 'responseCode' => self::RESPONSE_CODE_APPROVED, + ] + ]; + $subject = [ + 'payment' => $this->paymentDOMock + ]; + + $this->builder->handle($subject, $response); + // Assertions are part of mocking above + } + + public function testHandleHeldResponse() + { + // Assert the avs code is saved + $this->paymentMock->expects($this->once()) + ->method('setCcAvsStatus') + ->with('avshurray'); + $this->paymentMock->expects($this->once()) + ->method('setIsTransactionClosed') + ->with(false); + // opaque data wasn't provided + $this->paymentMock->expects($this->never()) + ->method('setAdditionalInformation'); + // Assert the payment is flagged for review + $this->paymentMock->expects($this->once()) + ->method('setIsTransactionPending') + ->with(true) + ->willReturnSelf(); + $this->paymentMock->expects($this->once()) + ->method('setIsFraudDetected') + ->with(true); + + $response = [ + 'transactionResponse' => [ + 'avsResultCode' => 'avshurray', + 'responseCode' => self::RESPONSE_CODE_HELD, + ] + ]; + $subject = [ + 'payment' => $this->paymentDOMock + ]; + + $this->builder->handle($subject, $response); + // Assertions are part of mocking above + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/PaymentReviewStatusHandlerTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/PaymentReviewStatusHandlerTest.php new file mode 100644 index 000000000000..a52a1b317fbb --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/PaymentReviewStatusHandlerTest.php @@ -0,0 +1,130 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Response; + +use Magento\AuthorizenetAcceptjs\Gateway\Response\PaymentReviewStatusHandler; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Payment\Model\InfoInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class PaymentReviewStatusHandlerTest extends TestCase +{ + /** + * @var PaymentReviewStatusHandler + */ + private $handler; + + /** + * @var InfoInterface|MockObject + */ + private $paymentMock; + + /** + * @var PaymentDataObjectInterface|MockObject + */ + private $paymentDOMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + + $this->handler = new PaymentReviewStatusHandler(new SubjectReader()); + } + + public function testApprovesPayment() + { + $subject = [ + 'payment' => $this->paymentDOMock + ]; + $response = [ + 'transaction' => [ + 'transactionStatus' => 'approvedOrSomething', + ] + ]; + + // Assert payment is handled correctly + $this->paymentMock->expects($this->exactly(2)) + ->method('setData') + ->withConsecutive( + ['is_transaction_denied', false], + ['is_transaction_approved', true] + ); + + $this->handler->handle($subject, $response); + // Assertions are via mock expects above + } + + /** + * @param string $status + * @dataProvider declinedTransactionStatusesProvider + */ + public function testDeniesPayment(string $status) + { + $subject = [ + 'payment' => $this->paymentDOMock + ]; + $response = [ + 'transaction' => [ + 'transactionStatus' => $status, + ] + ]; + + // Assert payment is handled correctly + $this->paymentMock->expects($this->exactly(2)) + ->method('setData') + ->withConsecutive( + ['is_transaction_denied', true], + ['is_transaction_approved', false] + ); + $this->handler->handle($subject, $response); + } + + /** + * @param string $status + * @dataProvider pendingTransactionStatusesProvider + */ + public function testDoesNothingWhenPending(string $status) + { + $subject = [ + 'payment' => $this->paymentDOMock + ]; + $response = [ + 'transaction' => [ + 'transactionStatus' => $status, + ] + ]; + + // Assert payment is handled correctly + $this->paymentMock->expects($this->never()) + ->method('setData'); + + $this->handler->handle($subject, $response); + } + + public function pendingTransactionStatusesProvider() + { + return [ + ['FDSPendingReview'], + ['FDSAuthorizedPendingReview'] + ]; + } + + public function declinedTransactionStatusesProvider() + { + return [ + ['void'], + ['declined'] + ]; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/TransactionDetailsResponseHandlerTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/TransactionDetailsResponseHandlerTest.php new file mode 100644 index 000000000000..016e3a1e9538 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/TransactionDetailsResponseHandlerTest.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Response; + +use Magento\AuthorizenetAcceptjs\Gateway\Config; +use Magento\AuthorizenetAcceptjs\Gateway\Response\TransactionDetailsResponseHandler; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Payment\Model\InfoInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class TransactionDetailsResponseHandlerTest extends TestCase +{ + /** + * @var TransactionDetailsResponseHandler + */ + private $handler; + + /** + * @var InfoInterface|MockObject + */ + private $paymentMock; + + /** + * @var PaymentDataObjectInterface|MockObject + */ + private $paymentDOMock; + + /** + * @var Config|MockObject + */ + private $configMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->configMock = $this->createMock(Config::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + + $this->handler = new TransactionDetailsResponseHandler(new SubjectReader(), $this->configMock); + } + + public function testHandle() + { + $subject = [ + 'payment' => $this->paymentDOMock, + 'store_id' => 123, + ]; + $response = [ + 'transactionResponse' => [ + 'dontsaveme' => 'dontdoti', + 'abc' => 'foobar', + ] + ]; + + // Assert the information comes from the right store config + $this->configMock->method('getAdditionalInfoKeys') + ->with(123) + ->willReturn(['abc']); + + // Assert the payment has the most recent information always set on it + $this->paymentMock->expects($this->once()) + ->method('setAdditionalInformation') + ->with('abc', 'foobar'); + // Assert the transaction has the raw details from the transaction + $this->paymentMock->expects($this->once()) + ->method('setTransactionAdditionalInfo') + ->with('raw_details_info', ['abc' => 'foobar']); + + $this->handler->handle($subject, $response); + // Assertions are via mock expects above + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/TransactionIdHandlerTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/TransactionIdHandlerTest.php new file mode 100644 index 000000000000..710f99591849 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/TransactionIdHandlerTest.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Response; + +use Magento\AuthorizenetAcceptjs\Gateway\Response\TransactionIdHandler; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class TransactionIdHandlerTest extends TestCase +{ + /** + * @var TransactionIdHandler + */ + private $builder; + + /** + * @var Payment|MockObject + */ + private $paymentMock; + + /** + * @var Payment|MockObject + */ + private $paymentDOMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + + $this->builder = new TransactionIdHandler(new SubjectReader()); + } + + public function testHandleDefaultResponse() + { + $this->paymentMock->method('getParentTransactionId') + ->willReturn(null); + // Assert the id is set + $this->paymentMock->expects($this->once()) + ->method('setTransactionId') + ->with('thetransid'); + // Assert the id is set in the additional info for later + $this->paymentMock->expects($this->once()) + ->method('setTransactionAdditionalInfo') + ->with('real_transaction_id', 'thetransid'); + + $response = [ + 'transactionResponse' => [ + 'transId' => 'thetransid', + ] + ]; + $subject = [ + 'payment' => $this->paymentDOMock + ]; + + $this->builder->handle($subject, $response); + // Assertions are part of mocking above + } + + public function testHandleDifferenceInTransactionId() + { + $this->paymentMock->method('getParentTransactionId') + ->willReturn('somethingElse'); + // Assert the id is set + $this->paymentMock->expects($this->once()) + ->method('setTransactionId') + ->with('thetransid'); + + $response = [ + 'transactionResponse' => [ + 'transId' => 'thetransid', + ] + ]; + $subject = [ + 'payment' => $this->paymentDOMock + ]; + + $this->builder->handle($subject, $response); + // Assertions are part of mocking above + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/VoidResponseHandlerTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/VoidResponseHandlerTest.php new file mode 100644 index 000000000000..f99da2b2ec90 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Response/VoidResponseHandlerTest.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Response; + +use Magento\AuthorizenetAcceptjs\Gateway\Response\VoidResponseHandler; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Payment\Model\InfoInterface; +use Magento\Sales\Model\Order\Payment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class VoidResponseHandlerTest extends TestCase +{ + /** + * @var VoidResponseHandler + */ + private $handler; + + /** + * @var InfoInterface|MockObject + */ + private $paymentMock; + + /** + * @var PaymentDataObjectInterface|MockObject + */ + private $paymentDOMock; + + protected function setUp() + { + $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->paymentDOMock->method('getPayment') + ->willReturn($this->paymentMock); + + $this->handler = new VoidResponseHandler(new SubjectReader()); + } + + public function testHandle() + { + $subject = [ + 'payment' => $this->paymentDOMock + ]; + $response = [ + 'transactionResponse' => [ + 'transId' => 'abc123', + ] + ]; + + // Assert the transaction is closed + $this->paymentMock->expects($this->once()) + ->method('setIsTransactionClosed') + ->with(true); + // Assert the parent transaction is closed + $this->paymentMock->expects($this->once()) + ->method('setShouldCloseParentTransaction') + ->with(true); + // Assert the authorize.net transaction id is saved + $this->paymentMock->expects($this->once()) + ->method('setTransactionAdditionalInfo') + ->with('real_transaction_id', 'abc123'); + + $this->handler->handle($subject, $response); + // Assertions are via mock expects above + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/SubjectReaderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/SubjectReaderTest.php new file mode 100644 index 000000000000..42219024badb --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/SubjectReaderTest.php @@ -0,0 +1,119 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway; + +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\Payment\Gateway\Data\OrderAdapterInterface; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use PHPUnit\Framework\TestCase; + +class SubjectReaderTest extends TestCase +{ + /** + * @var SubjectReader + */ + private $subjectReader; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->subjectReader = new SubjectReader(); + } + + public function testReadPayment(): void + { + $paymentDO = $this->createMock(PaymentDataObjectInterface::class); + + $this->assertSame($paymentDO, $this->subjectReader->readPayment(['payment' => $paymentDO])); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Payment data object should be provided + */ + public function testReadPaymentThrowsExceptionWhenNotAPaymentObject(): void + { + $this->subjectReader->readPayment(['payment' => 'nope']); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Payment data object should be provided + */ + public function testReadPaymentThrowsExceptionWhenNotSet(): void + { + $this->subjectReader->readPayment([]); + } + + public function testReadResponse(): void + { + $expected = ['foo' => 'bar']; + + $this->assertSame($expected, $this->subjectReader->readResponse(['response' => $expected])); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Response does not exist + */ + public function testReadResponseThrowsExceptionWhenNotAvailable(): void + { + $this->subjectReader->readResponse([]); + } + + public function testReadStoreId(): void + { + $this->assertEquals(123, $this->subjectReader->readStoreId(['store_id' => '123'])); + } + + public function testReadStoreIdFromOrder(): void + { + $paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); + $orderMock = $this->createMock(OrderAdapterInterface::class); + $paymentDOMock->method('getOrder') + ->willReturn($orderMock); + $orderMock->method('getStoreID') + ->willReturn('123'); + + $result = $this->subjectReader->readStoreId([ + 'payment' => $paymentDOMock + ]); + + $this->assertEquals(123, $result); + } + + public function testReadLoginId(): void + { + $this->assertEquals('abc', $this->subjectReader->readLoginId([ + 'merchantAuthentication' => ['name' => 'abc'] + ])); + } + + public function testReadTransactionKey(): void + { + $this->assertEquals('abc', $this->subjectReader->readTransactionKey([ + 'merchantAuthentication' => ['transactionKey' => 'abc'] + ])); + } + + public function testReadAmount(): void + { + $this->assertSame('123.12', $this->subjectReader->readAmount(['amount' => 123.12])); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Amount should be provided + */ + public function testReadAmountThrowsExceptionWhenNotAvailable(): void + { + $this->subjectReader->readAmount([]); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Validator/GeneralResponseValidatorTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Validator/GeneralResponseValidatorTest.php new file mode 100644 index 000000000000..347cd071acc3 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Validator/GeneralResponseValidatorTest.php @@ -0,0 +1,161 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Validator; + +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\AuthorizenetAcceptjs\Gateway\Validator\GeneralResponseValidator; +use Magento\Payment\Gateway\Validator\ResultInterface; +use Magento\Payment\Gateway\Validator\ResultInterfaceFactory; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class GeneralResponseValidatorTest extends TestCase +{ + /** + * @var ResultInterfaceFactory|MockObject + */ + private $resultFactoryMock; + + /** + * @var GeneralResponseValidator + */ + private $validator; + + protected function setUp() + { + $this->resultFactoryMock = $this->createMock(ResultInterfaceFactory::class); + $this->validator = new GeneralResponseValidator($this->resultFactoryMock, new SubjectReader()); + } + + public function testValidateParsesSuccess() + { + $args = []; + + $this->resultFactoryMock->method('create') + ->with($this->callback(function ($a) use (&$args) { + // Spy on method call + $args = $a; + + return true; + })) + ->willReturn($this->createMock(ResultInterface::class)); + + $this->validator->validate([ + 'response' => [ + 'messages' => [ + 'resultCode' => 'Ok', + 'message' => [ + [ + 'code' => 'foo', + 'text' => 'bar' + ] + ] + ] + ] + ]); + + $this->assertTrue($args['isValid']); + $this->assertEmpty($args['errorCodes']); + $this->assertEmpty($args['failsDescription']); + } + + public function testValidateParsesErrors() + { + $args = []; + + $this->resultFactoryMock->method('create') + ->with($this->callback(function ($a) use (&$args) { + // Spy on method call + $args = $a; + + return true; + })) + ->willReturn($this->createMock(ResultInterface::class)); + + $this->validator->validate([ + 'response' => [ + 'errors' => [ + 'resultCode' => 'Error', + 'error' => [ + [ + 'errorCode' => 'foo', + 'errorText' => 'bar' + ] + ] + ] + ] + ]); + + $this->assertFalse($args['isValid']); + $this->assertSame(['foo'], $args['errorCodes']); + $this->assertSame(['bar'], $args['failsDescription']); + } + + public function testValidateParsesMessages() + { + $args = []; + + $this->resultFactoryMock->method('create') + ->with($this->callback(function ($a) use (&$args) { + // Spy on method call + $args = $a; + + return true; + })) + ->willReturn($this->createMock(ResultInterface::class)); + + $this->validator->validate([ + 'response' => [ + 'messages' => [ + 'resultCode' => 'Error', + 'message' => [ + [ + 'code' => 'foo', + 'text' => 'bar' + ] + ] + ] + ] + ]); + + $this->assertFalse($args['isValid']); + $this->assertSame(['foo'], $args['errorCodes']); + $this->assertSame(['bar'], $args['failsDescription']); + } + + public function testValidateParsesErrorsWhenOnlyOneIsReturned() + { + $args = []; + + $this->resultFactoryMock->method('create') + ->with($this->callback(function ($a) use (&$args) { + // Spy on method call + $args = $a; + + return true; + })) + ->willReturn($this->createMock(ResultInterface::class)); + + $this->validator->validate([ + 'response' => [ + 'messages' => [ + 'resultCode' => 'Error', + 'message' => [ + 'code' => 'foo', + 'text' => 'bar' + ] + ] + ] + ]); + + $this->assertFalse($args['isValid']); + $this->assertSame(['foo'], $args['errorCodes']); + $this->assertSame(['bar'], $args['failsDescription']); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Validator/TransactionHashValidatorTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Validator/TransactionHashValidatorTest.php new file mode 100644 index 000000000000..fb3f9d0520d4 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Validator/TransactionHashValidatorTest.php @@ -0,0 +1,280 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Validator; + +use Magento\AuthorizenetAcceptjs\Gateway\Config; +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\AuthorizenetAcceptjs\Gateway\Validator\TransactionHashValidator; +use Magento\Payment\Gateway\Validator\ResultInterface; +use Magento\Payment\Gateway\Validator\ResultInterfaceFactory; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class TransactionHashValidatorTest extends TestCase +{ + /** + * @var ResultInterfaceFactory|MockObject + */ + private $resultFactoryMock; + + /** + * @var TransactionHashValidator + */ + private $validator; + + /** + * @var Config|MockObject + */ + private $configMock; + + /** + * @var ResultInterface + */ + private $resultMock; + + protected function setUp() + { + $this->resultFactoryMock = $this->createMock(ResultInterfaceFactory::class); + $this->configMock = $this->createMock(Config::class); + $this->resultMock = $this->createMock(ResultInterface::class); + + $this->validator = new TransactionHashValidator( + $this->resultFactoryMock, + new SubjectReader(), + $this->configMock + ); + } + + /** + * @param $response + * @param $isValid + * @param $errorCodes + * @param $errorDescriptions + * @dataProvider sha512ResponseProvider + */ + public function testValidateSha512HashScenarios( + $response, + $isValid, + $errorCodes, + $errorDescriptions + ) { + $args = []; + + $this->resultFactoryMock->method('create') + ->with($this->callback(function ($a) use (&$args) { + // Spy on method call + $args = $a; + + return true; + })) + ->willReturn($this->resultMock); + + $this->configMock->method('getTransactionSignatureKey') + ->willReturn('abc'); + $this->configMock->method('getLoginId') + ->willReturn('username'); + + $this->validator->validate($response); + + $this->assertSame($isValid, $args['isValid']); + $this->assertEquals($errorCodes, $args['errorCodes']); + $this->assertEquals($errorDescriptions, $args['failsDescription']); + } + + /** + * @param $response + * @param $isValid + * @param $errorCodes + * @param $errorDescriptions + * @dataProvider md5ResponseProvider + */ + public function testValidateMd5HashScenarios( + $response, + $isValid, + $errorCodes, + $errorDescriptions + ) { + $args = []; + + $this->resultFactoryMock->method('create') + ->with($this->callback(function ($a) use (&$args) { + // Spy on method call + $args = $a; + + return true; + })) + ->willReturn($this->resultMock); + + $this->configMock->method('getLegacyTransactionHash') + ->willReturn('abc'); + $this->configMock->method('getLoginId') + ->willReturn('username'); + + $this->validator->validate($response); + + $this->assertSame($isValid, $args['isValid']); + $this->assertEquals($errorCodes, $args['errorCodes']); + $this->assertEquals($errorDescriptions, $args['failsDescription']); + } + + public function md5ResponseProvider() + { + return [ + [ + [ + 'response' => [ + 'transactionResponse' => [ + 'transId' => '123', + 'transHash' => 'C8675D9F7BE7BE4A04C18EA1B6F7B6FD' + ] + ] + ], + true, + [], + [] + ], + [ + [ + 'response' => [ + 'transactionResponse' => [ + 'transId' => '123', + 'transHash' => 'C8675D9F7BE7BE4A04C18EA1B6F7B6FD' + ] + ] + ], + true, + [], + [] + ], + [ + [ + 'amount' => '123.00', + 'response' => [ + 'transactionResponse' => [ + 'transHash' => 'bad' + ] + ] + ], + false, + ['ETHV'], + ['The authenticity of the gateway response could not be verified.'] + ], + [ + [ + 'amount' => '123.00', + 'response' => [ + 'transactionResponse' => [ + 'refTransID' => '123', + 'transId' => '123', + 'transHash' => 'C8675D9F7BE7BE4A04C18EA1B6F7B6FD' + ] + ] + ], + true, + [], + [] + ], + ]; + } + + public function sha512ResponseProvider() + { + return [ + [ + [ + 'response' => [ + 'transactionResponse' => [ + 'transId' => '123', + 'refTransID' => '123', + 'transHashSha2' => 'CC0FF465A081D98FFC6E502C40B2DCC7655ACF591F859135B6E66558D' + . '41E3A2C654D5A2ACF4749104F3133711175C232C32676F79F70211C2984B21A33D30DEE' + ] + ] + ], + true, + [], + [] + ], + [ + [ + 'response' => [ + 'transactionResponse' => [ + 'transId' => '0', + 'refTransID' => '123', + 'transHashSha2' => '563D42F4A5189F74334088EF6A02E84F320CD8C005FB0DC436EF96084D' + . 'FAC0C76DE081DFC58A3BF825465C63B7F38E4D463025EAC44597A68C024CBBCE7A3159' + ] + ] + ], + true, + [], + [] + ], + [ + [ + 'amount' => '123.00', + 'response' => [ + 'transactionResponse' => [ + 'transId' => '0', + 'transHashSha2' => 'DEE5309078D9F7A68BA4F706FB3E58618D3991A6A5E4C39DCF9C49E693' + . '673C38BD6BB15C235263C549A6B5F0B6D7019EC729E0C275C9FEA37FB91F8B612D0A5D' + ] + ] + ], + true, + [], + [] + ], + [ + [ + 'amount' => '123.00', + 'response' => [ + 'transactionResponse' => [ + 'transId' => '123', + 'transHashSha2' => '1DBD16DED0DA02F52A22A9AD71A49F70BD2ECD42437552889912DD5CE' + . 'CBA0E09A5E8E6221DA74D98A46E5F77F7774B6D9C39CADF3E9A33D85870A6958DA7C8B2' + ] + ] + ], + true, + [], + [] + ], + [ + [ + 'amount' => '123.00', + 'response' => [ + 'transactionResponse' => [ + 'transId' => '123', + 'refTransID' => '0', + 'transHashSha2' => '1DBD16DED0DA02F52A22A9AD71A49F70BD2ECD42437552889912DD5CE' + . 'CBA0E09A5E8E6221DA74D98A46E5F77F7774B6D9C39CADF3E9A33D85870A6958DA7C8B2' + ] + ] + ], + true, + [], + [] + ], + [ + [ + 'amount' => '123.00', + 'response' => [ + 'transactionResponse' => [ + 'transHashSha2' => 'bad' + ] + ] + ], + false, + ['ETHV'], + ['The authenticity of the gateway response could not be verified.'] + ], + ]; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Validator/TransactionResponseValidatorTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Validator/TransactionResponseValidatorTest.php new file mode 100644 index 000000000000..c59cf00899af --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Gateway/Validator/TransactionResponseValidatorTest.php @@ -0,0 +1,231 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Gateway\Validator; + +use Magento\AuthorizenetAcceptjs\Gateway\SubjectReader; +use Magento\AuthorizenetAcceptjs\Gateway\Validator\TransactionResponseValidator; +use Magento\Payment\Gateway\Validator\ResultInterface; +use Magento\Payment\Gateway\Validator\ResultInterfaceFactory; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Tests for the transaction response validator + */ +class TransactionResponseValidatorTest extends TestCase +{ + private const RESPONSE_CODE_APPROVED = 1; + private const RESPONSE_CODE_HELD = 4; + private const RESPONSE_CODE_DENIED = 2; + private const RESPONSE_REASON_CODE_APPROVED = 1; + private const RESPONSE_REASON_CODE_PENDING_REVIEW_AUTHORIZED = 252; + private const RESPONSE_REASON_CODE_PENDING_REVIEW = 253; + private const ERROR_CODE_AVS_MISMATCH = 27; + + /** + * @var ResultInterfaceFactory|MockObject + */ + private $resultFactoryMock; + + /** + * @var TransactionResponseValidator + */ + private $validator; + + /** + * @var ResultInterface + */ + private $resultMock; + + protected function setUp() + { + $this->resultFactoryMock = $this->createMock(ResultInterfaceFactory::class); + $this->resultMock = $this->createMock(ResultInterface::class); + + $this->validator = new TransactionResponseValidator( + $this->resultFactoryMock, + new SubjectReader() + ); + } + + /** + * @param $transactionResponse + * @param $isValid + * @param $errorCodes + * @param $errorMessages + * @dataProvider scenarioProvider + */ + public function testValidateScenarios($transactionResponse, $isValid, $errorCodes, $errorMessages) + { + $args = []; + + $this->resultFactoryMock->method('create') + ->with($this->callback(function ($a) use (&$args) { + // Spy on method call + $args = $a; + + return true; + })) + ->willReturn($this->resultMock); + + $this->validator->validate([ + 'response' => [ + 'transactionResponse' => $transactionResponse + ] + ]); + + $this->assertEquals($isValid, $args['isValid']); + $this->assertEquals($errorCodes, $args['errorCodes']); + $this->assertEquals($errorMessages, $args['failsDescription']); + } + + /** + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function scenarioProvider() + { + return [ + // Test for acceptable reason codes + [ + [ + 'responseCode' => self::RESPONSE_CODE_APPROVED, + 'messages' => [ + 'message' => [ + 'code' => self::RESPONSE_REASON_CODE_APPROVED, + ] + ] + ], + true, + [], + [] + ], + [ + [ + 'responseCode' => self::RESPONSE_CODE_APPROVED, + 'messages' => [ + 'message' => [ + 'code' => self::RESPONSE_REASON_CODE_PENDING_REVIEW, + ] + ] + ], + true, + [], + [] + ], + [ + [ + 'responseCode' => self::RESPONSE_CODE_APPROVED, + 'messages' => [ + 'message' => [ + 'code' => self::RESPONSE_REASON_CODE_PENDING_REVIEW_AUTHORIZED, + ] + ] + ], + true, + [], + [] + ], + [ + [ + 'responseCode' => self::RESPONSE_CODE_HELD, + 'messages' => [ + 'message' => [ + 'code' => self::RESPONSE_REASON_CODE_APPROVED, + ] + ] + ], + true, + [], + [] + ], + [ + [ + 'responseCode' => self::RESPONSE_CODE_HELD, + 'messages' => [ + 'message' => [ + 'code' => self::RESPONSE_REASON_CODE_PENDING_REVIEW, + ] + ] + ], + true, + [], + [] + ], + [ + [ + 'responseCode' => self::RESPONSE_CODE_HELD, + 'messages' => [ + 'message' => [ + 'code' => self::RESPONSE_REASON_CODE_PENDING_REVIEW_AUTHORIZED, + ] + ] + ], + true, + [], + [] + ], + + // Test for reason codes that aren't acceptable + [ + [ + 'responseCode' => self::RESPONSE_CODE_APPROVED, + 'messages' => [ + 'message' => [ + [ + 'description' => 'bar', + 'code' => 'foo', + ] + ] + ] + ], + false, + ['foo'], + ['bar'] + ], + [ + [ + 'responseCode' => self::RESPONSE_CODE_APPROVED, + 'messages' => [ + 'message' => [ + // Alternate, non-array sytax + 'text' => 'bar', + 'code' => 'foo', + ] + ] + ], + false, + ['foo'], + ['bar'] + ], + [ + [ + 'responseCode' => self::RESPONSE_CODE_DENIED, + 'errors' => [ + [ + 'errorCode' => self::ERROR_CODE_AVS_MISMATCH, + 'errorText' => 'bar' + ] + ] + ], + false, + [self::ERROR_CODE_AVS_MISMATCH], + ['bar'] + ], + // This validator only cares about successful edge cases so test for default behavior + [ + [ + 'responseCode' => 'foo', + ], + false, + [], + [] + ], + ]; + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Model/Ui/ConfigProviderTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Model/Ui/ConfigProviderTest.php new file mode 100644 index 000000000000..dea4557fd584 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Model/Ui/ConfigProviderTest.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Model\Ui; + +use Magento\AuthorizenetAcceptjs\Gateway\Config; +use Magento\AuthorizenetAcceptjs\Model\Ui\ConfigProvider; +use Magento\Quote\Api\Data\CartInterface; +use PHPUnit\Framework\MockObject\Builder\InvocationMocker; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class ConfigProviderTest extends TestCase +{ + /** + * @var CartInterface|MockObject|InvocationMocker + */ + private $cart; + + /** + * @var Config|MockObject|InvocationMocker + */ + private $config; + + /** + * @var ConfigProvider + */ + private $provider; + + protected function setUp() + { + $this->cart = $this->createMock(CartInterface::class); + $this->config = $this->createMock(Config::class); + $this->provider = new ConfigProvider($this->config, $this->cart); + } + + public function testProviderRetrievesValues() + { + $this->cart->method('getStoreId') + ->willReturn('123'); + + $this->config->method('getClientKey') + ->with('123') + ->willReturn('foo'); + + $this->config->method('getLoginId') + ->with('123') + ->willReturn('bar'); + + $this->config->method('getEnvironment') + ->with('123') + ->willReturn('baz'); + + $this->config->method('isCvvEnabled') + ->with('123') + ->willReturn(false); + + $expected = [ + 'payment' => [ + Config::METHOD => [ + 'clientKey' => 'foo', + 'apiLoginID' => 'bar', + 'environment' => 'baz', + 'useCvv' => false, + ] + ] + ]; + + $this->assertEquals($expected, $this->provider->getConfig()); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Observer/DataAssignObserverTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Observer/DataAssignObserverTest.php new file mode 100644 index 000000000000..ebb95263f54d --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Observer/DataAssignObserverTest.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Observer; + +use Magento\Framework\DataObject; +use Magento\Framework\Event; +use Magento\Framework\Event\Observer; +use Magento\Payment\Model\InfoInterface; +use Magento\Payment\Observer\AbstractDataAssignObserver; +use Magento\AuthorizenetAcceptjs\Observer\DataAssignObserver; +use Magento\Quote\Api\Data\PaymentInterface; +use PHPUnit\Framework\TestCase; + +class DataAssignObserverTest extends TestCase +{ + public function testExecuteSetsProperData() + { + $additionalInfo = [ + 'opaqueDataDescriptor' => 'foo', + 'opaqueDataValue' => 'bar', + 'ccLast4' => '1234' + ]; + + $observerContainer = $this->createMock(Observer::class); + $event = $this->createMock(Event::class); + $paymentInfoModel = $this->createMock(InfoInterface::class); + $dataObject = new DataObject([ + PaymentInterface::KEY_ADDITIONAL_DATA => $additionalInfo + ]); + $observerContainer->method('getEvent') + ->willReturn($event); + $event->method('getDataByKey') + ->willReturnMap( + [ + [AbstractDataAssignObserver::MODEL_CODE, $paymentInfoModel], + [AbstractDataAssignObserver::DATA_CODE, $dataObject] + ] + ); + $paymentInfoModel->expects($this->at(0)) + ->method('setAdditionalInformation') + ->with('opaqueDataDescriptor', 'foo'); + $paymentInfoModel->expects($this->at(1)) + ->method('setAdditionalInformation') + ->with('opaqueDataValue', 'bar'); + $paymentInfoModel->expects($this->at(2)) + ->method('setAdditionalInformation') + ->with('ccLast4', '1234'); + + $observer = new DataAssignObserver(); + $observer->execute($observerContainer); + } + + public function testDoestSetDataWhenEmpty() + { + $observerContainer = $this->createMock(Observer::class); + $event = $this->createMock(Event::class); + $paymentInfoModel = $this->createMock(InfoInterface::class); + $observerContainer->method('getEvent') + ->willReturn($event); + $event->method('getDataByKey') + ->willReturnMap( + [ + [AbstractDataAssignObserver::MODEL_CODE, $paymentInfoModel], + [AbstractDataAssignObserver::DATA_CODE, new DataObject()] + ] + ); + $paymentInfoModel->expects($this->never()) + ->method('setAdditionalInformation'); + + $observer = new DataAssignObserver(); + $observer->execute($observerContainer); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Setup/Patch/Data/CopyCurrentConfigTest.php b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Setup/Patch/Data/CopyCurrentConfigTest.php new file mode 100644 index 000000000000..5ac8a6ca9b3f --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/Test/Unit/Setup/Patch/Data/CopyCurrentConfigTest.php @@ -0,0 +1,149 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Test\Unit\Setup\Patch\Data; + +use Magento\AuthorizenetAcceptjs\Setup\Patch\Data\CopyCurrentConfig; +use Magento\Config\Model\ResourceModel\Config as ResourceConfig; +use Magento\Framework\App\Config; +use Magento\Framework\Encryption\Encryptor; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Setup\Module\DataSetup; +use Magento\Setup\Model\ModuleContext; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Model\Website; +use PHPUnit\Framework\TestCase; + +class CopyCurrentConfigTest extends TestCase +{ + /** + * @var \Magento\Framework\App\Config + */ + private $scopeConfig; + + /** + * @var \Magento\Config\Model\ResourceModel\Config + */ + private $resourceConfig; + + /** + * @var \Magento\Framework\Encryption\Encryptor + */ + private $encryptor; + + /** + * @var \Magento\Setup\Module\DataSetup + */ + private $setup; + + /** + * @var \Magento\Setup\Model\ModuleContext + */ + private $context; + + /** + * @var \Magento\Store\Model\StoreManager + */ + private $storeManager; + + /** + * @var \Magento\Store\Model\Website + */ + private $website; + + protected function setUp(): void + { + $this->scopeConfig = $this->createMock(Config::class); + $this->resourceConfig = $this->createMock(ResourceConfig::class); + $this->encryptor = $this->createMock(Encryptor::class); + $this->setup = $this->createMock(DataSetup::class); + + $this->setup->expects($this->once()) + ->method('startSetup') + ->willReturn(null); + + $this->setup->expects($this->once()) + ->method('endSetup') + ->willReturn(null); + + $this->context = $this->createMock(ModuleContext::class); + $this->storeManager = $this->createMock(StoreManagerInterface::class); + $this->website = $this->createMock(Website::class); + } + + public function testMigrateData(): void + { + $this->scopeConfig->expects($this->exactly(26)) + ->method('getValue') + ->willReturn('TestValue'); + + $this->resourceConfig->expects($this->exactly(26)) + ->method('saveConfig') + ->willReturn(null); + + $this->encryptor->expects($this->exactly(6)) + ->method('encrypt') + ->willReturn('TestValue'); + + $this->website->expects($this->once()) + ->method('getId') + ->willReturn(1); + + $this->storeManager->expects($this->once()) + ->method('getWebsites') + ->willReturn([$this->website]); + + $objectManager = new ObjectManager($this); + + $installer = $objectManager->getObject( + CopyCurrentConfig::class, + [ + 'moduleDataSetup' => $this->setup, + 'scopeConfig' => $this->scopeConfig, + 'resourceConfig' => $this->resourceConfig, + 'encryptor' => $this->encryptor, + 'storeManager' => $this->storeManager + ] + ); + + $installer->apply($this->context); + } + + public function testMigrateDataNullFields(): void + { + $this->scopeConfig->expects($this->exactly(13)) + ->method('getValue') + ->will($this->onConsecutiveCalls(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); + + $this->resourceConfig->expects($this->exactly(10)) + ->method('saveConfig') + ->willReturn(null); + + $this->encryptor->expects($this->never()) + ->method('encrypt'); + + $this->storeManager->expects($this->once()) + ->method('getWebsites') + ->willReturn([]); + + $objectManager = new ObjectManager($this); + + $installer = $objectManager->getObject( + CopyCurrentConfig::class, + [ + 'moduleDataSetup' => $this->setup, + 'scopeConfig' => $this->scopeConfig, + 'resourceConfig' => $this->resourceConfig, + 'encryptor' => $this->encryptor, + 'storeManager' => $this->storeManager + ] + ); + + $installer->apply($this->context); + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/composer.json b/app/code/Magento/AuthorizenetAcceptjs/composer.json new file mode 100644 index 000000000000..be2cd6d4e70f --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/composer.json @@ -0,0 +1,31 @@ +{ + "name": "magento/module-authorizenet-acceptjs", + "description": "N/A", + "config": { + "sort-packages": true + }, + "require": { + "php": "~7.1.3||~7.2.0", + "magento/framework": "*", + "magento/module-payment": "*", + "magento/module-sales": "*", + "magento/module-config": "*", + "magento/module-backend": "*", + "magento/module-checkout": "*", + "magento/module-store": "*", + "magento/module-quote": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\AuthorizenetAcceptjs\\": "" + } + } +} diff --git a/app/code/Magento/AuthorizenetAcceptjs/etc/adminhtml/di.xml b/app/code/Magento/AuthorizenetAcceptjs/etc/adminhtml/di.xml new file mode 100644 index 000000000000..320f8f79ee28 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/etc/adminhtml/di.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\AuthorizenetAcceptjs\Block\Payment"> + <arguments> + <argument name="config" xsi:type="object">Magento\AuthorizenetAcceptjs\Model\Ui\ConfigProvider</argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/AuthorizenetAcceptjs/etc/adminhtml/system.xml b/app/code/Magento/AuthorizenetAcceptjs/etc/adminhtml/system.xml new file mode 100644 index 000000000000..279a904d916a --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/etc/adminhtml/system.xml @@ -0,0 +1,118 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="payment"> + <group id="authorizenet_acceptjs" translate="label" type="text" sortOrder="34" showInDefault="1" showInWebsite="1" showInStore="1"> + <label>Authorize.Net</label> + <field id="active" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> + <label>Enabled</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <requires> + <group id="authorizenet_acceptjs_required"/> + </requires> + </field> + <group id="required" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="5"> + <label>Basic Authorize.Net Settings</label> + <attribute type="expanded">1</attribute> + <frontend_model>Magento\Config\Block\System\Config\Form\Fieldset</frontend_model> + <field id="title" translate="label" type="text" sortOrder="10" showInDefault="10" showInWebsite="1" showInStore="1" canRestore="1"> + <label>Title</label> + <config_path>payment/authorizenet_acceptjs/title</config_path> + </field> + <field id="environment" translate="label" type="select" sortOrder="15" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Environment</label> + <source_model>Magento\AuthorizenetAcceptjs\Model\Adminhtml\Source\Environment</source_model> + <config_path>payment/authorizenet_acceptjs/environment</config_path> + </field> + <field id="payment_action" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> + <label>Payment Action</label> + <source_model>Magento\AuthorizenetAcceptjs\Model\Adminhtml\Source\PaymentAction</source_model> + <config_path>payment/authorizenet_acceptjs/payment_action</config_path> + </field> + <field id="login" translate="label" type="obscure" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>API Login ID</label> + <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> + <config_path>payment/authorizenet_acceptjs/login</config_path> + </field> + <field id="trans_key" translate="label" type="obscure" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Transaction Key</label> + <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> + <config_path>payment/authorizenet_acceptjs/trans_key</config_path> + </field> + <field id="public_client_key" translate="label" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Public Client Key</label> + <config_path>payment/authorizenet_acceptjs/public_client_key</config_path> + </field> + <field id="trans_signature_key" translate="label" type="obscure" sortOrder="60" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Signature Key</label> + <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> + <config_path>payment/authorizenet_acceptjs/trans_signature_key</config_path> + </field> + <field id="trans_md5" translate="label" type="obscure" sortOrder="70" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Merchant MD5 (deprecated)</label> + <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> + <config_path>payment/authorizenet_acceptjs/trans_md5</config_path> + </field> + </group> + <group id="advanced" translate="label" showInDefault="1" showInWebsite="1" showInStore="0" sortOrder="20"> + <label>Advanced Authorize.Net Settings</label> + <attribute type="expanded">0</attribute> + <field id="currency" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> + <label>Accepted Currency</label> + <source_model>Magento\Config\Model\Config\Source\Locale\Currency</source_model> + <config_path>payment/authorizenet_acceptjs/currency</config_path> + </field> + <field id="debug" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> + <label>Debug</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <config_path>payment/authorizenet_acceptjs/debug</config_path> + </field> + <field id="email_customer" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> + <label>Email Customer</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <config_path>payment/authorizenet_acceptjs/email_customer</config_path> + </field> + <field id="cvv_enabled" translate="label" type="select" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> + <label>Enable Credit Card Verification Field</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <config_path>payment/authorizenet_acceptjs/cvv_enabled</config_path> + </field> + <field id="cctypes" translate="label" type="multiselect" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> + <label>Credit Card Types</label> + <source_model>Magento\AuthorizenetAcceptjs\Model\Adminhtml\Source\Cctype</source_model> + <config_path>payment/authorizenet_acceptjs/cctypes</config_path> + </field> + <field id="allowspecific" translate="label" type="allowspecific" sortOrder="60" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> + <label>Payment from Applicable Countries</label> + <source_model>Magento\Payment\Model\Config\Source\Allspecificcountries</source_model> + <config_path>payment/authorizenet_acceptjs/allowspecific</config_path> + </field> + <field id="specificcountry" translate="label" type="multiselect" sortOrder="70" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Payment from Specific Countries</label> + <source_model>Magento\Directory\Model\Config\Source\Country</source_model> + <config_path>payment/authorizenet_acceptjs/specificcountry</config_path> + </field> + <field id="min_order_total" translate="label" type="text" sortOrder="80" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Minimum Order Total</label> + <config_path>payment/authorizenet_acceptjs/min_order_total</config_path> + </field> + <field id="max_order_total" translate="label" type="text" sortOrder="90" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Maximum Order Total</label> + <config_path>payment/authorizenet_acceptjs/max_order_total</config_path> + </field> + <field id="sort_order" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Sort Order</label> + <frontend_class>validate-number</frontend_class> + <config_path>payment/authorizenet_acceptjs/sort_order</config_path> + </field> + </group> + </group> + </section> + </system> +</config> diff --git a/app/code/Magento/AuthorizenetAcceptjs/etc/authorizenet_acceptjs_error_mapping.xml b/app/code/Magento/AuthorizenetAcceptjs/etc/authorizenet_acceptjs_error_mapping.xml new file mode 100644 index 000000000000..507a9b14f917 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/etc/authorizenet_acceptjs_error_mapping.xml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<mapping xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Payment:etc/error_mapping.xsd"> + <message_list> + <message code="E00003" translate="true">Invalid request to gateway.</message> + <message code="E00007" translate="true">Invalid gateway credentials.</message> + <message code="E00027" translate="true">Transaction has been declined. Please try again later.</message> + <message code="ETHV" translate="true">The authenticity of the gateway response could not be verified.</message> + </message_list> +</mapping> diff --git a/app/code/Magento/AuthorizenetAcceptjs/etc/config.xml b/app/code/Magento/AuthorizenetAcceptjs/etc/config.xml new file mode 100644 index 000000000000..7324421d3c14 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/etc/config.xml @@ -0,0 +1,56 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd"> + <default> + <dev> + <js> + <minify_exclude> + <authorizenet_acceptjs>\.authorize\.net/v1/Accept</authorizenet_acceptjs> + </minify_exclude> + </js> + </dev> + <payment> + <authorizenet_acceptjs> + <active>0</active> + <cctypes>AE,VI,MC,DI,JCB,DN</cctypes> + <debug>0</debug> + <can_use_checkout>1</can_use_checkout> + <can_use_internal>1</can_use_internal> + <can_capture_partial>0</can_capture_partial> + <can_authorize>1</can_authorize> + <can_refund>1</can_refund> + <can_capture>1</can_capture> + <can_void>1</can_void> + <can_accept_payment>1</can_accept_payment> + <can_deny_payment>1</can_deny_payment> + <can_cancel>1</can_cancel> + <can_review_payment>1</can_review_payment> + <can_edit>1</can_edit> + <can_fetch_transaction_info>1</can_fetch_transaction_info> + <can_fetch_transaction_information>1</can_fetch_transaction_information> + <model>AuthorizenetAcceptjsFacade</model> + <email_customer>0</email_customer> + <login backend_model="Magento\Config\Model\Config\Backend\Encrypted" /> + <order_status>processing</order_status> + <payment_action>authorize</payment_action> + <title>Credit Card (Authorize.Net) + 1 + + + + + 0 + USD + production + authCode,avsResultCode,cvvResultCode,cavvResultCode + accountType,ccLast4,authCode,avsResultCode,cvvResultCode,cavvResultCode + transactionStatus,responseCode,responseReasonCode,authCode,AVSResponse,cardCodeResponse,CAVVResponse + + + + diff --git a/app/code/Magento/AuthorizenetAcceptjs/etc/di.xml b/app/code/Magento/AuthorizenetAcceptjs/etc/di.xml new file mode 100644 index 000000000000..cf10557d3869 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/etc/di.xml @@ -0,0 +1,428 @@ + + + + + + Magento\AuthorizenetAcceptjs\Gateway\Config::METHOD + + + + + authorizenet_acceptjs + Magento\AuthorizenetAcceptjs\Block\Form + AuthorizenetAcceptjsInfoBlock + AuthorizenetAcceptjsValueHandlerPool + AuthorizenetAcceptjsValidatorPool + AuthorizenetAcceptjsCommandPool + + + + + + AuthorizenetAcceptjsAuthorizeCommand + AuthorizenetAcceptjsCaptureCommand + AuthorizenetAcceptjsSaleCommand + AuthorizenetAcceptjsSettleCommand + AuthorizenetAcceptjsVoidCommand + AuthorizenetAcceptjsRefundCommand + AuthorizenetAcceptjsRefundSettledCommand + AuthorizenetAcceptjsCancelCommand + AuthorizenetAcceptjsAcceptPaymentCommand + AuthorizenetAcceptjsAcceptFdsCommand + AuthorizenetAcceptjsCancelCommand + AuthorizenetAcceptjsTransactionDetailsCommand + AuthorizenetAcceptjsFetchTransactionInfoCommand + + + + + + + + + Magento\AuthorizenetAcceptjs\Gateway\Validator\GeneralResponseValidator + + + + + + + true + + + Magento\AuthorizenetAcceptjs\Gateway\Validator\GeneralResponseValidator + Magento\AuthorizenetAcceptjs\Gateway\Validator\TransactionResponseValidator + Magento\AuthorizenetAcceptjs\Gateway\Validator\TransactionHashValidator + + + + + + + Magento\AuthorizenetAcceptjs\Gateway\Validator\GeneralResponseValidator + + + + + + Magento\AuthorizenetAcceptjs\Gateway\Config + + + + + + AuthorizenetAcceptjsCountryValidator + + + + + + + + + AuthorizenetAcceptjsCommandPool + + + + + AuthorizenetAcceptjsTransactionDetailsRequest + AuthorizenetAcceptjsDefaultTransferFactory + Magento\AuthorizenetAcceptjs\Gateway\Http\Client + AuthorizenetAcceptjsTransactionDetailsValidator + + + + + AuthorizenetAcceptjsAuthorizeRequest + AuthorizenetAcceptjsDefaultTransferFactory + Magento\AuthorizenetAcceptjs\Gateway\Http\Client + AuthorizenetAcceptjsAuthorizationHandler + AuthorizenetAcceptjsTransactionValidator + AuthorizenetAcceptjsVirtualErrorMessageMapper + + + + + AuthorizenetAcceptjsAcceptsFdsRequest + AuthorizenetAcceptjsDefaultTransferFactory + Magento\AuthorizenetAcceptjs\Gateway\Http\Client + AuthorizenetAcceptjsAcceptsFdsRequestValidator + + + + + AuthorizenetAcceptjsCommandPool + + + + + AuthorizenetAcceptjsSaleRequest + AuthorizenetAcceptjsSaleHandler + + + + + AuthorizenetAcceptjsCommandPool + + + + + AuthorizenetAcceptjsRefundRequest + AuthorizenetAcceptjsRefundSettledHandler + + + + + AuthorizenetAcceptjsCommandPool + + + + + AuthorizenetAcceptjsCaptureRequest + AuthorizenetAcceptjsCaptureTransactionHandler + + + + + AuthorizenetAcceptjsVoidRequest + AuthorizenetAcceptjsDefaultTransferFactory + Magento\AuthorizenetAcceptjs\Gateway\Http\Client + AuthorizenetAcceptjsVoidHandler + AuthorizenetAcceptjsTransactionValidator + AuthorizenetAcceptjsVirtualErrorMessageMapper + + + + + AuthorizenetAcceptjsCancelHandler + + + + + + + + + Magento\AuthorizenetAcceptjs\Gateway\Response\PaymentReviewStatusHandler + + + + + + + Magento\AuthorizenetAcceptjs\Gateway\Response\TransactionIdHandler + Magento\AuthorizenetAcceptjs\Gateway\Response\PaymentResponseHandler + Magento\AuthorizenetAcceptjs\Gateway\Response\TransactionDetailsResponseHandler + + + + + + + Magento\AuthorizenetAcceptjs\Gateway\Response\CloseParentTransactionHandler + + + + + + + Magento\AuthorizenetAcceptjs\Gateway\Response\CloseParentTransactionHandler + + + + + + + Magento\AuthorizenetAcceptjs\Gateway\Response\TransactionIdHandler + Magento\AuthorizenetAcceptjs\Gateway\Response\CloseParentTransactionHandler + Magento\AuthorizenetAcceptjs\Gateway\Response\CloseTransactionHandler + + + + + + + Magento\AuthorizenetAcceptjs\Gateway\Response\VoidResponseHandler + + + + + + + Magento\AuthorizenetAcceptjs\Gateway\Response\CloseTransactionHandler + Magento\AuthorizenetAcceptjs\Gateway\Response\CloseParentTransactionHandler + + + + + + + + + + + AuthorizenetAcceptjsTransactionDetailsRequestTypeBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\StoreConfigBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\AuthenticationDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\TransactionDetailsDataBuilder + + + + + + + AuthorizenetAcceptjsAcceptsFdsRequestTypeBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\StoreConfigBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\AuthenticationDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\AcceptFdsDataBuilder + + + + + + + AuthorizenetAcceptjsTransactionRequestTypeBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\StoreConfigBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\AuthenticationDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\AuthorizeDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\AmountDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\PaymentDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\ShippingDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\SolutionDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\OrderDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\PoDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\CustomerDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\AddressDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\CustomSettingsBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\PassthroughDataBuilder + + + + + + + Magento\AuthorizenetAcceptjs\Gateway\Request\SaleDataBuilder + + + + + + + AuthorizenetAcceptjsTransactionRequestTypeBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\StoreConfigBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\AuthenticationDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\RefundTransactionTypeDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\AmountDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\RefundPaymentDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\ShippingDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\RefundReferenceTransactionDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\OrderDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\PoDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\CustomerDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\AddressDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\CustomSettingsBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\PassthroughDataBuilder + + + + + + + AuthorizenetAcceptjsTransactionRequestTypeBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\StoreConfigBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\AuthenticationDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\CaptureDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\PassthroughDataBuilder + + + + + + + AuthorizenetAcceptjsTransactionRequestTypeBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\StoreConfigBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\AuthenticationDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\VoidDataBuilder + Magento\AuthorizenetAcceptjs\Gateway\Request\PassthroughDataBuilder + + + + + + + + + createTransactionRequest + + + + + getTransactionDetailsRequest + + + + + updateHeldTransactionRequest + + + + + + + + authorizenet_acceptjs_error_mapping.xml + + + + + AuthorizenetAcceptjsErrorMappingConfigReader + authorizenet_acceptjs_error_mapper + + + + + AuthorizenetAcceptjsErrorMappingData + + + + + + + AuthorizenetAcceptjsPaymentReviewStatusHandler + + + + + + AuthorizenetAcceptjsCommandManager + + + + + + + 1 + 1 + 1 + 1 + 1 + 1 + + + 1 + 1 + + + + + + Magento\AuthorizenetAcceptjs\Gateway\Config + + + + + + AuthorizenetAcceptjsDefaultValueHandler + + + + + + AuthorizenetAcceptjsCommandPool + + + + + Magento\AuthorizenetAcceptjs\Gateway\Config + + + + + AuthorizenetAcceptjsLogger + + + + + + store_id + + + + + + + AuthorizenetAcceptjsRemoveStoreConfigFilter + + + + + + Magento\AuthorizenetAcceptjs\Gateway\Config + + + diff --git a/app/code/Magento/AuthorizenetAcceptjs/etc/events.xml b/app/code/Magento/AuthorizenetAcceptjs/etc/events.xml new file mode 100644 index 000000000000..93dc448d1d89 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/etc/events.xml @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/app/code/Magento/AuthorizenetAcceptjs/etc/frontend/di.xml b/app/code/Magento/AuthorizenetAcceptjs/etc/frontend/di.xml new file mode 100644 index 000000000000..8b0e570abbd2 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/etc/frontend/di.xml @@ -0,0 +1,30 @@ + + + + + + + 1 + + + + + + + Magento\AuthorizenetAcceptjs\Model\Ui\ConfigProvider + + + + + + + Magento\AuthorizenetAcceptjs\Gateway\Config::METHOD + + + + diff --git a/app/code/Magento/AuthorizenetAcceptjs/etc/module.xml b/app/code/Magento/AuthorizenetAcceptjs/etc/module.xml new file mode 100644 index 000000000000..6bc8fe3c4dae --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/etc/module.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + diff --git a/app/code/Magento/AuthorizenetAcceptjs/etc/payment.xml b/app/code/Magento/AuthorizenetAcceptjs/etc/payment.xml new file mode 100644 index 000000000000..b9f8d40b0300 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/etc/payment.xml @@ -0,0 +1,15 @@ + + + + + + 0 + + + diff --git a/app/code/Magento/AuthorizenetAcceptjs/i18n/en_US.csv b/app/code/Magento/AuthorizenetAcceptjs/i18n/en_US.csv new file mode 100644 index 000000000000..da518301652f --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/i18n/en_US.csv @@ -0,0 +1,21 @@ +Authorize.net,Authorize.net +"Gateway URL","Gateway URL" +"Invalid payload type.","Invalid payload type." +"Something went wrong in the payment gateway.","Something went wrong in the payment gateway." +"Merchant MD5 (deprecated","Merchant MD5 (deprecated" +"Signature Key","Signature Key" +"Basic Authorize.Net Settings","Basic Authorize.Net Settings" +"Advanced Authorie.Net Settings","Advanced Authorie.Net Settings" +"Public Client Key","Public Client Key" +"Environment","Environment" +"Production","Production" +"Sandbox","Sandbox" +"accountType","Account Type" +"authCode", "Processor Response Text" +"avsResultCode", "AVS Response Code" +"cvvResultCode","CVV Response Code" +"cavvResultCode","CAVV Response Code" +"Enable Credit Card Verification Field","Enable Credit Card Verification Field" +"ccLast4","Last 4 Digits of Card" +"There was an error while trying to process the refund.","There was an error while trying to process the refund." +"This transaction cannot be refunded with its current status.","This transaction cannot be refunded with its current status." diff --git a/app/code/Magento/AuthorizenetAcceptjs/registration.php b/app/code/Magento/AuthorizenetAcceptjs/registration.php new file mode 100644 index 000000000000..5338c9a4ddc8 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/registration.php @@ -0,0 +1,11 @@ + + + + + + + Magento\AuthorizenetAcceptjs\Gateway\Config::METHOD + Magento_AuthorizenetAcceptjs::form/cc.phtml + + + + + + + diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/layout/sales_order_create_load_block_billing_method.xml b/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/layout/sales_order_create_load_block_billing_method.xml new file mode 100644 index 000000000000..13f6d38e2b81 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/layout/sales_order_create_load_block_billing_method.xml @@ -0,0 +1,17 @@ + + + + + + + Magento\AuthorizenetAcceptjs\Gateway\Config::METHOD + Magento_AuthorizenetAcceptjs::form/cc.phtml + + + + diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/templates/form/cc.phtml b/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/templates/form/cc.phtml new file mode 100644 index 000000000000..045bd5cfd81b --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/templates/form/cc.phtml @@ -0,0 +1,93 @@ +escapeHtml($block->getMethodCode()); +$ccType = $block->getInfoData('cc_type'); +$ccExpMonth = $block->getInfoData('cc_exp_month'); +$ccExpYear = $block->getInfoData('cc_exp_year'); +?> + diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/templates/payment/script.phtml b/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/templates/payment/script.phtml new file mode 100644 index 000000000000..6960bddf696a --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/templates/payment/script.phtml @@ -0,0 +1,17 @@ + + \ No newline at end of file diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/web/js/authorizenet.js b/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/web/js/authorizenet.js new file mode 100644 index 000000000000..0eb865d7666b --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/web/js/authorizenet.js @@ -0,0 +1,196 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'uiComponent', + 'Magento_Ui/js/modal/alert', + 'Magento_AuthorizenetAcceptjs/js/view/payment/acceptjs-client' +], function ($, Class, alert, AcceptjsClient) { + 'use strict'; + + return Class.extend({ + defaults: { + acceptjsClient: null, + $selector: null, + selector: 'edit_form', + container: 'payment_form_authorizenet_acceptjs', + active: false, + imports: { + onActiveChange: 'active' + } + }, + + /** + * @{inheritdoc} + */ + initConfig: function (config) { + this._super(); + + this.acceptjsClient = AcceptjsClient({ + environment: config.environment + }); + + return this; + }, + + /** + * @{inheritdoc} + */ + initObservable: function () { + this.$selector = $('#' + this.selector); + this._super() + .observe('active'); + + // re-init payment method events + this.$selector.off('changePaymentMethod.' + this.code) + .on('changePaymentMethod.' + this.code, this.changePaymentMethod.bind(this)); + + return this; + }, + + /** + * Enable/disable current payment method + * + * @param {Object} event + * @param {String} method + * @returns {Object} + */ + changePaymentMethod: function (event, method) { + this.active(method === this.code); + + return this; + }, + + /** + * Triggered when payment changed + * + * @param {Boolean} isActive + */ + onActiveChange: function (isActive) { + if (!isActive) { + + return; + } + + this.disableEventListeners(); + + window.order.addExcludedPaymentMethod(this.code); + + this.enableEventListeners(); + }, + + /** + * Sets the payment details on the form + * + * @param {Object} tokens + */ + setPaymentDetails: function (tokens) { + var $ccNumber = $(this.getSelector('cc_number')), + ccLast4 = $ccNumber.val().replace(/[^\d]/g, '').substr(-4); + + $(this.getSelector('opaque_data_descriptor')).val(tokens.opaqueDataDescriptor); + $(this.getSelector('opaque_data_value')).val(tokens.opaqueDataValue); + $(this.getSelector('cc_last_4')).val(ccLast4); + $ccNumber.val(''); + $(this.getSelector('cc_exp_month')).val(''); + $(this.getSelector('cc_exp_year')).val(''); + + if (this.useCvv) { + $(this.getSelector('cc_cid')).val(''); + } + }, + + /** + * Trigger order submit + */ + submitOrder: function () { + var authData = {}, + cardData = {}, + secureData = {}; + + this.$selector.validate().form(); + this.$selector.trigger('afterValidate.beforeSubmit'); + + authData.clientKey = this.clientKey; + authData.apiLoginID = this.apiLoginID; + + cardData.cardNumber = $(this.getSelector('cc_number')).val(); + cardData.month = $(this.getSelector('cc_exp_month')).val(); + cardData.year = $(this.getSelector('cc_exp_year')).val(); + + if (this.useCvv) { + cardData.cardCode = $(this.getSelector('cc_cid')).val(); + } + + secureData.authData = authData; + secureData.cardData = cardData; + + this.disableEventListeners(); + + this.acceptjsClient.createTokens(secureData) + .always(function () { + $('body').trigger('processStop'); + this.enableEventListeners(); + }.bind(this)) + .done(function (tokens) { + this.setPaymentDetails(tokens); + this.placeOrder(); + }.bind(this)) + .fail(function (messages) { + this.tokens = null; + + if (messages.length > 0) { + this._showError(messages[0]); + } + }.bind(this)); + + return false; + }, + + /** + * Place order + */ + placeOrder: function () { + this.$selector.trigger('realOrder'); + }, + + /** + * Get jQuery selector + * + * @param {String} field + * @returns {String} + */ + getSelector: function (field) { + return '#' + this.code + '_' + field; + }, + + /** + * Show alert message + * + * @param {String} message + */ + _showError: function (message) { + alert({ + content: message + }); + }, + + /** + * Enable form event listeners + */ + enableEventListeners: function () { + this.$selector.on('submitOrder.authorizenetacceptjs', this.submitOrder.bind(this)); + }, + + /** + * Disable form event listeners + */ + disableEventListeners: function () { + this.$selector.off('submitOrder'); + this.$selector.off('submit'); + } + + }); +}); diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/web/js/payment-form.js b/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/web/js/payment-form.js new file mode 100644 index 000000000000..68c2f22f6ed4 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/view/adminhtml/web/js/payment-form.js @@ -0,0 +1,18 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'Magento_AuthorizenetAcceptjs/js/authorizenet', + 'jquery' +], function (AuthorizenetAcceptjs, $) { + 'use strict'; + + return function (data, element) { + var $form = $(element), + config = data.config; + + config.active = $form.length > 0 && !$form.is(':hidden'); + new AuthorizenetAcceptjs(config); + }; +}); diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/base/requirejs-config.js b/app/code/Magento/AuthorizenetAcceptjs/view/base/requirejs-config.js new file mode 100644 index 000000000000..83ddd1094ea1 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/view/base/requirejs-config.js @@ -0,0 +1,19 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +var config = { + shim: { + acceptjs: { + exports: 'Accept' + }, + acceptjssandbox: { + exports: 'Accept' + } + }, + paths: { + acceptjssandbox: 'https://jstest.authorize.net/v1/Accept', + acceptjs: 'https://js.authorize.net/v1/Accept' + } +}; diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/acceptjs-client.js b/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/acceptjs-client.js new file mode 100644 index 000000000000..935465f5298e --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/acceptjs-client.js @@ -0,0 +1,73 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'uiClass', + 'Magento_AuthorizenetAcceptjs/js/view/payment/acceptjs-factory', + 'Magento_AuthorizenetAcceptjs/js/view/payment/validator-handler' +], function ($, Class, acceptjsFactory, validatorHandler) { + 'use strict'; + + return Class.extend({ + defaults: { + environment: 'production' + }, + + /** + * @{inheritdoc} + */ + initialize: function () { + validatorHandler.initialize(); + + this._super(); + + return this; + }, + + /** + * Creates the token pair with the provided data + * + * @param {Object} data + * @return {jQuery.Deferred} + */ + createTokens: function (data) { + var deferred = $.Deferred(); + + if (this.acceptjsClient) { + this._createTokens(deferred, data); + } else { + acceptjsFactory(this.environment) + .done(function (client) { + this.acceptjsClient = client; + this._createTokens(deferred, data); + }.bind(this)); + } + + return deferred.promise(); + }, + + /** + * Creates a token from the payment information in the form + * + * @param {jQuery.Deferred} deferred + * @param {Object} data + */ + _createTokens: function (deferred, data) { + this.acceptjsClient.dispatchData(data, function (response) { + validatorHandler.validate(response, function (valid, messages) { + if (valid) { + deferred.resolve({ + opaqueDataDescriptor: response.opaqueData.dataDescriptor, + opaqueDataValue: response.opaqueData.dataValue + }); + } else { + deferred.reject(messages); + } + }); + }); + } + }); +}); diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/acceptjs-factory.js b/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/acceptjs-factory.js new file mode 100644 index 000000000000..e98a204e36ce --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/acceptjs-factory.js @@ -0,0 +1,38 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery' +], function ($) { + 'use strict'; + + return function (environment) { + var deferred = $.Deferred(), + dependency = 'acceptjs'; + + if (environment === 'sandbox') { + dependency = 'acceptjssandbox'; + } + + require([dependency], function (accept) { + var $body = $('body'); + + /* + * Acceptjs doesn't safely load dependent files which leads to a race condition when trying to use + * the sdk right away. + * @see https://community.developer.authorize.net/t5/Integration-and-Testing/ + * Dynamically-loading-Accept-js-E-WC-03-Accept-js-is-not-loaded/td-p/63283 + */ + $body.on('handshake.acceptjs', function () { + deferred.resolve(accept); + $body.off('handshake.acceptjs'); + }); + }, + deferred.reject + ); + + return deferred.promise(); + }; +}); diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/response-validator.js b/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/response-validator.js new file mode 100644 index 000000000000..3c44ca2f9e49 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/response-validator.js @@ -0,0 +1,38 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'mage/translate' +], function ($, $t) { + 'use strict'; + + return { + /** + * Validate Authorizenet-Acceptjs response + * + * @param {Object} context + * @returns {jQuery.Deferred} + */ + validate: function (context) { + var state = $.Deferred(), + messages = []; + + if (context.messages.resultCode === 'Ok') { + state.resolve(); + } else { + if (context.messages.message.length > 0) { + $.each(context.messages.message, function (index, element) { + messages.push($t(element.text)); + }); + } + state.reject(messages); + } + + return state.promise(); + } + }; +}); + diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/validator-handler.js b/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/validator-handler.js new file mode 100644 index 000000000000..109f159c9a77 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/view/base/web/js/view/payment/validator-handler.js @@ -0,0 +1,59 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'Magento_AuthorizenetAcceptjs/js/view/payment/response-validator' +], function ($, responseValidator) { + 'use strict'; + + return { + validators: [], + + /** + * Init list of validators + */ + initialize: function () { + this.add(responseValidator); + }, + + /** + * Add new validator + * @param {Object} validator + */ + add: function (validator) { + this.validators.push(validator); + }, + + /** + * Run pull of validators + * @param {Object} context + * @param {Function} callback + */ + validate: function (context, callback) { + var self = this, + deferred; + + // no available validators + if (!self.validators.length) { + callback(true); + + return; + } + + // get list of deferred validators + deferred = $.map(self.validators, function (current) { + return current.validate(context); + }); + + $.when.apply($, deferred) + .done(function () { + callback(true); + }).fail(function (error) { + callback(false, error); + }); + } + }; +}); diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/frontend/layout/checkout_index_index.xml b/app/code/Magento/AuthorizenetAcceptjs/view/frontend/layout/checkout_index_index.xml new file mode 100644 index 000000000000..f31b06c9be9b --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/view/frontend/layout/checkout_index_index.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + uiComponent + + + + + + + + Magento_AuthorizenetAcceptjs/js/view/payment/authorizenet + + + true + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/frontend/web/js/view/payment/authorizenet.js b/app/code/Magento/AuthorizenetAcceptjs/view/frontend/web/js/view/payment/authorizenet.js new file mode 100644 index 000000000000..a05fe739a444 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/view/frontend/web/js/view/payment/authorizenet.js @@ -0,0 +1,20 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'uiComponent', + 'Magento_Checkout/js/model/payment/renderer-list' +], +function (Component, rendererList) { + 'use strict'; + + rendererList.push({ + type: 'authorizenet_acceptjs', + component: 'Magento_AuthorizenetAcceptjs/js/view/payment/method-renderer/authorizenet-accept' + }); + + /** Add view logic here if needed */ + return Component.extend({}); +}); diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/frontend/web/js/view/payment/method-renderer/authorizenet-accept.js b/app/code/Magento/AuthorizenetAcceptjs/view/frontend/web/js/view/payment/method-renderer/authorizenet-accept.js new file mode 100644 index 000000000000..983318c4cdaa --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/view/frontend/web/js/view/payment/method-renderer/authorizenet-accept.js @@ -0,0 +1,146 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'Magento_Payment/js/view/payment/cc-form', + 'Magento_AuthorizenetAcceptjs/js/view/payment/acceptjs-client', + 'Magento_Checkout/js/model/full-screen-loader', + 'Magento_Ui/js/model/messageList', + 'Magento_Payment/js/model/credit-card-validation/validator' +], function ($, Component, AcceptjsClient, fullScreenLoader, globalMessageList) { + 'use strict'; + + return Component.extend({ + defaults: { + active: false, + template: 'Magento_AuthorizenetAcceptjs/payment/authorizenet-acceptjs', + tokens: null, + ccForm: 'Magento_Payment/payment/cc-form', + acceptjsClient: null + }, + + /** + * Set list of observable attributes + * + * @returns {exports.initObservable} + */ + initObservable: function () { + this._super() + .observe(['active']); + + return this; + }, + + /** + * @returns {String} + */ + getCode: function () { + return 'authorizenet_acceptjs'; + }, + + /** + * Initialize form elements for validation + */ + initFormElement: function (element) { + this.formElement = element; + this.acceptjsClient = AcceptjsClient({ + environment: window.checkoutConfig.payment[this.getCode()].environment + }); + $(this.formElement).validation(); + }, + + /** + * @returns {Object} + */ + getData: function () { + return { + method: this.getCode(), + 'additional_data': { + opaqueDataDescriptor: this.tokens ? this.tokens.opaqueDataDescriptor : null, + opaqueDataValue: this.tokens ? this.tokens.opaqueDataValue : null, + ccLast4: this.creditCardNumber().substr(-4) + } + }; + }, + + /** + * Check if payment is active + * + * @returns {Boolean} + */ + isActive: function () { + var active = this.getCode() === this.isChecked(); + + this.active(active); + + return active; + }, + + /** + * Prepare data to place order + */ + beforePlaceOrder: function () { + var authData = {}, + cardData = {}, + secureData = {}; + + if (!$(this.formElement).valid()) { + return; + } + + authData.clientKey = window.checkoutConfig.payment[this.getCode()].clientKey; + authData.apiLoginID = window.checkoutConfig.payment[this.getCode()].apiLoginID; + + cardData.cardNumber = this.creditCardNumber(); + cardData.month = this.creditCardExpMonth(); + cardData.year = this.creditCardExpYear(); + + if (this.hasVerification()) { + cardData.cardCode = this.creditCardVerificationNumber(); + } + + secureData.authData = authData; + secureData.cardData = cardData; + + fullScreenLoader.startLoader(); + + this.acceptjsClient.createTokens(secureData) + .always(function () { + fullScreenLoader.stopLoader(); + }) + .done(function (tokens) { + this.tokens = tokens; + this.placeOrder(); + }.bind(this)) + .fail(function (messages) { + this.tokens = null; + this._showErrors(messages); + }.bind(this)); + }, + + /** + * Should the cvv field be used + * + * @return {Boolean} + */ + hasVerification: function () { + return window.checkoutConfig.payment[this.getCode()].useCvv; + }, + + /** + * Show error messages + * + * @param {String[]} errorMessages + */ + _showErrors: function (errorMessages) { + $.each(errorMessages, function (index, message) { + globalMessageList.addErrorMessage({ + message: message + }); + }); + } + }); +}); diff --git a/app/code/Magento/AuthorizenetAcceptjs/view/frontend/web/template/payment/authorizenet-acceptjs.html b/app/code/Magento/AuthorizenetAcceptjs/view/frontend/web/template/payment/authorizenet-acceptjs.html new file mode 100644 index 000000000000..6db52a2b1025 --- /dev/null +++ b/app/code/Magento/AuthorizenetAcceptjs/view/frontend/web/template/payment/authorizenet-acceptjs.html @@ -0,0 +1,45 @@ + +
+
+ + +
+
+ +
+ +
+
+ + +
+ +
+
+
+ +
+
+
+
diff --git a/app/code/Magento/Backend/App/DefaultPath.php b/app/code/Magento/Backend/App/DefaultPath.php index df8b71838974..b790a2edc3fa 100644 --- a/app/code/Magento/Backend/App/DefaultPath.php +++ b/app/code/Magento/Backend/App/DefaultPath.php @@ -42,6 +42,6 @@ public function __construct(\Magento\Backend\App\ConfigInterface $config) */ public function getPart($code) { - return isset($this->_parts[$code]) ? $this->_parts[$code] : null; + return $this->_parts[$code] ?? null; } } diff --git a/app/code/Magento/Backend/App/Request/BackendValidator.php b/app/code/Magento/Backend/App/Request/BackendValidator.php index 878f9cb4dc4c..7ec6c83f29b5 100644 --- a/app/code/Magento/Backend/App/Request/BackendValidator.php +++ b/app/code/Magento/Backend/App/Request/BackendValidator.php @@ -77,6 +77,8 @@ public function __construct( } /** + * Validate request + * * @param RequestInterface $request * @param ActionInterface $action * @@ -115,6 +117,8 @@ private function validateRequest( } /** + * Create exception + * * @param RequestInterface $request * @param ActionInterface $action * @@ -142,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, [ @@ -166,7 +171,7 @@ public function validate( ActionInterface $action ): void { if ($action instanceof AbstractAction) { - //Abstract Action has build-in validation. + //Abstract Action has built-in validation. if (!$action->_processUrlKeys()) { throw new InvalidRequestException($action->getResponse()); } diff --git a/app/code/Magento/Backend/Block/Dashboard/Graph.php b/app/code/Magento/Backend/Block/Dashboard/Graph.php index 8e238ccab44c..b76421e4e6f6 100644 --- a/app/code/Magento/Backend/Block/Dashboard/Graph.php +++ b/app/code/Magento/Backend/Block/Dashboard/Graph.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Backend\Block\Dashboard; /** @@ -15,7 +17,7 @@ class Graph extends \Magento\Backend\Block\Dashboard\AbstractDashboard /** * Api URL */ - const API_URL = 'http://chart.apis.google.com/chart'; + const API_URL = 'https://image-charts.com/chart'; /** * All series @@ -76,6 +78,7 @@ class Graph extends \Magento\Backend\Block\Dashboard\AbstractDashboard /** * Google chart api data encoding * + * @deprecated since the Google Image Charts API not accessible from March 14, 2019 * @var string */ protected $_encoding = 'e'; @@ -111,8 +114,8 @@ public function __construct( \Magento\Backend\Helper\Dashboard\Data $dashboardData, array $data = [] ) { - $this->_dashboardData = $dashboardData; parent::__construct($context, $collectionFactory, $data); + $this->_dashboardData = $dashboardData; } /** @@ -128,7 +131,7 @@ protected function _getTabTemplate() /** * Set data rows * - * @param array $rows + * @param string $rows * @return void */ public function setDataRows($rows) @@ -152,15 +155,14 @@ public function addSeries($seriesId, array $options) * Get series * * @param string $seriesId - * @return array|false + * @return array|bool */ public function getSeries($seriesId) { if (isset($this->_allSeries[$seriesId])) { return $this->_allSeries[$seriesId]; - } else { - return false; } + return false; } /** @@ -187,11 +189,12 @@ public function getChartUrl($directUrl = true) { $params = [ 'cht' => 'lc', - 'chf' => 'bg,s,ffffff', - 'chco' => 'ef672f', 'chls' => '7', - 'chxs' => '0,676056,15,0,l,676056|1,676056,15,0,l,676056', - 'chm' => 'h,f2ebde,0,0:1:.1,1,-1', + 'chf' => 'bg,s,f4f4f4|c,lg,90,ffffff,0.1,ededed,0', + 'chm' => 'B,f4d4b2,0,0,0', + 'chco' => 'db4814', + 'chxs' => '0,0,11|1,0,11', + 'chma' => '15,15,15,15' ]; $this->_allSeries = $this->getRowsData($this->_dataRows); @@ -279,20 +282,11 @@ public function getChartUrl($directUrl = true) $this->_axisLabels['x'] = $dates; $this->_allSeries = $datas; - //Google encoding values - if ($this->_encoding == "s") { - // simple encoding - $params['chd'] = "s:"; - $dataDelimiter = ""; - $dataSetdelimiter = ","; - $dataMissing = "_"; - } else { - // extended encoding - $params['chd'] = "e:"; - $dataDelimiter = ""; - $dataSetdelimiter = ","; - $dataMissing = "__"; - } + // Image-Charts Awesome data format values + $params['chd'] = "a:"; + $dataDelimiter = ","; + $dataSetdelimiter = "|"; + $dataMissing = "_"; // process each string in the array, and find the max length $localmaxvalue = [0]; @@ -306,7 +300,6 @@ public function getChartUrl($directUrl = true) $minvalue = min($localminvalue); // default values - $yrange = 0; $yLabels = []; $miny = 0; $maxy = 0; @@ -314,14 +307,13 @@ public function getChartUrl($directUrl = true) if ($minvalue >= 0 && $maxvalue >= 0) { if ($maxvalue > 10) { - $p = pow(10, $this->_getPow($maxvalue)); + $p = pow(10, $this->_getPow((int) $maxvalue)); $maxy = ceil($maxvalue / $p) * $p; $yLabels = range($miny, $maxy, $p); } else { $maxy = ceil($maxvalue + 1); $yLabels = range($miny, $maxy, 1); } - $yrange = $maxy; $yorigin = 0; } @@ -329,44 +321,14 @@ public function getChartUrl($directUrl = true) foreach ($this->getAllSeries() as $index => $serie) { $thisdataarray = $serie; - if ($this->_encoding == "s") { - // SIMPLE ENCODING - for ($j = 0; $j < sizeof($thisdataarray); $j++) { - $currentvalue = $thisdataarray[$j]; - if (is_numeric($currentvalue)) { - $ylocation = round( - (strlen($this->_simpleEncoding) - 1) * ($yorigin + $currentvalue) / $yrange - ); - $chartdata[] = substr($this->_simpleEncoding, $ylocation, 1) . $dataDelimiter; - } else { - $chartdata[] = $dataMissing . $dataDelimiter; - } - } - } else { - // EXTENDED ENCODING - for ($j = 0; $j < sizeof($thisdataarray); $j++) { - $currentvalue = $thisdataarray[$j]; - if (is_numeric($currentvalue)) { - if ($yrange) { - $ylocation = 4095 * ($yorigin + $currentvalue) / $yrange; - } else { - $ylocation = 0; - } - $firstchar = floor($ylocation / 64); - $secondchar = $ylocation % 64; - $mappedchar = substr( - $this->_extendedEncoding, - $firstchar, - 1 - ) . substr( - $this->_extendedEncoding, - $secondchar, - 1 - ); - $chartdata[] = $mappedchar . $dataDelimiter; - } else { - $chartdata[] = $dataMissing . $dataDelimiter; - } + $count = count($thisdataarray); + for ($j = 0; $j < $count; $j++) { + $currentvalue = $thisdataarray[$j]; + if (is_numeric($currentvalue)) { + $ylocation = $yorigin + $currentvalue; + $chartdata[] = $ylocation . $dataDelimiter; + } else { + $chartdata[] = $dataMissing . $dataDelimiter; } } $chartdata[] = $dataSetdelimiter; @@ -381,45 +343,13 @@ public function getChartUrl($directUrl = true) $valueBuffer = []; - if (sizeof($this->_axisLabels) > 0) { + if (count($this->_axisLabels) > 0) { $params['chxt'] = implode(',', array_keys($this->_axisLabels)); $indexid = 0; foreach ($this->_axisLabels as $idx => $labels) { if ($idx == 'x') { - /** - * Format date - */ - foreach ($this->_axisLabels[$idx] as $_index => $_label) { - if ($_label != '') { - $period = new \DateTime($_label, new \DateTimeZone($timezoneLocal)); - switch ($this->getDataHelper()->getParam('period')) { - case '24h': - $this->_axisLabels[$idx][$_index] = $this->_localeDate->formatDateTime( - $period->setTime($period->format('H'), 0, 0), - \IntlDateFormatter::NONE, - \IntlDateFormatter::SHORT - ); - break; - case '7d': - case '1m': - $this->_axisLabels[$idx][$_index] = $this->_localeDate->formatDateTime( - $period, - \IntlDateFormatter::SHORT, - \IntlDateFormatter::NONE - ); - break; - case '1y': - case '2y': - $this->_axisLabels[$idx][$_index] = date('m/Y', strtotime($_label)); - break; - } - } else { - $this->_axisLabels[$idx][$_index] = ''; - } - } - + $this->formatAxisLabelDate((string) $idx, (string) $timezoneLocal); $tmpstring = implode('|', $this->_axisLabels[$idx]); - $valueBuffer[] = $indexid . ":|" . $tmpstring; } elseif ($idx == 'y') { $valueBuffer[] = $indexid . ":|" . implode('|', $yLabels); @@ -438,12 +368,51 @@ public function getChartUrl($directUrl = true) foreach ($params as $name => $value) { $p[] = $name . '=' . urlencode($value); } - return self::API_URL . '?' . implode('&', $p); - } else { - $gaData = urlencode(base64_encode(json_encode($params))); - $gaHash = $this->_dashboardData->getChartDataHash($gaData); - $params = ['ga' => $gaData, 'h' => $gaHash]; - return $this->getUrl('adminhtml/*/tunnel', ['_query' => $params]); + return (string) self::API_URL . '?' . implode('&', $p); + } + $gaData = urlencode(base64_encode(json_encode($params))); + $gaHash = $this->_dashboardData->getChartDataHash($gaData); + $params = ['ga' => $gaData, 'h' => $gaHash]; + return $this->getUrl('adminhtml/*/tunnel', ['_query' => $params]); + } + + /** + * Format dates for axis labels + * + * @param string $idx + * @param string $timezoneLocal + * + * @return void + */ + private function formatAxisLabelDate($idx, $timezoneLocal) + { + foreach ($this->_axisLabels[$idx] as $_index => $_label) { + if ($_label != '') { + $period = new \DateTime($_label, new \DateTimeZone($timezoneLocal)); + switch ($this->getDataHelper()->getParam('period')) { + case '24h': + $this->_axisLabels[$idx][$_index] = $this->_localeDate->formatDateTime( + $period->setTime((int) $period->format('H'), 0, 0), + \IntlDateFormatter::NONE, + \IntlDateFormatter::SHORT + ); + break; + case '7d': + case '1m': + $this->_axisLabels[$idx][$_index] = $this->_localeDate->formatDateTime( + $period, + \IntlDateFormatter::SHORT, + \IntlDateFormatter::NONE + ); + break; + case '1y': + case '2y': + $this->_axisLabels[$idx][$_index] = date('m/Y', strtotime($_label)); + break; + } + } else { + $this->_axisLabels[$idx][$_index] = ''; + } } } @@ -540,6 +509,8 @@ protected function getHeight() } /** + * Sets data helper + * * @param \Magento\Backend\Helper\Dashboard\AbstractDashboard $dataHelper * @return void */ diff --git a/app/code/Magento/Backend/Block/DataProviders/ImageUploadConfig.php b/app/code/Magento/Backend/Block/DataProviders/ImageUploadConfig.php new file mode 100644 index 000000000000..9c17b0e5538f --- /dev/null +++ b/app/code/Magento/Backend/Block/DataProviders/ImageUploadConfig.php @@ -0,0 +1,40 @@ +imageUploadConfig = $imageUploadConfig; + } + + /** + * Get image resize configuration + * + * @return int + */ + public function getIsResizeEnabled(): int + { + return (int)$this->imageUploadConfig->isResizeEnabled(); + } +} diff --git a/app/code/Magento/Backend/Block/Media/Uploader.php b/app/code/Magento/Backend/Block/Media/Uploader.php index 5bad74d8a8be..84fa487281ac 100644 --- a/app/code/Magento/Backend/Block/Media/Uploader.php +++ b/app/code/Magento/Backend/Block/Media/Uploader.php @@ -3,10 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Backend\Block\Media; use Magento\Framework\App\ObjectManager; use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\Image\Adapter\UploadConfigInterface; +use Magento\Backend\Model\Image\UploadResizeConfigInterface; /** * Adminhtml media library uploader @@ -35,24 +39,46 @@ class Uploader extends \Magento\Backend\Block\Widget */ private $jsonEncoder; + /** + * @var UploadResizeConfigInterface + */ + private $imageUploadConfig; + + /** + * @var UploadConfigInterface + * @deprecated + * @see \Magento\Backend\Model\Image\UploadResizeConfigInterface + */ + private $imageConfig; + /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Framework\File\Size $fileSize * @param array $data * @param Json $jsonEncoder + * @param UploadConfigInterface $imageConfig + * @param UploadResizeConfigInterface $imageUploadConfig */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Framework\File\Size $fileSize, array $data = [], - Json $jsonEncoder = null + Json $jsonEncoder = null, + UploadConfigInterface $imageConfig = null, + UploadResizeConfigInterface $imageUploadConfig = null ) { $this->_fileSizeService = $fileSize; $this->jsonEncoder = $jsonEncoder ?: ObjectManager::getInstance()->get(Json::class); + $this->imageConfig = $imageConfig + ?: ObjectManager::getInstance()->get(UploadConfigInterface::class); + $this->imageUploadConfig = $imageUploadConfig + ?: ObjectManager::getInstance()->get(UploadResizeConfigInterface::class); parent::__construct($context, $data); } /** + * Initialize block. + * * @return void */ protected function _construct() @@ -90,6 +116,26 @@ public function getFileSizeService() return $this->_fileSizeService; } + /** + * Get Image Upload Maximum Width Config. + * + * @return int + */ + public function getImageUploadMaxWidth() + { + return $this->imageUploadConfig->getMaxWidth(); + } + + /** + * Get Image Upload Maximum Height Config. + * + * @return int + */ + public function getImageUploadMaxHeight() + { + return $this->imageUploadConfig->getMaxHeight(); + } + /** * Prepares layout and set element renderer * diff --git a/app/code/Magento/Backend/Block/Menu.php b/app/code/Magento/Backend/Block/Menu.php index 7d86497288a6..1e2561e2efe0 100644 --- a/app/code/Magento/Backend/Block/Menu.php +++ b/app/code/Magento/Backend/Block/Menu.php @@ -9,12 +9,12 @@ /** * Backend menu block * - * @api - * @method \Magento\Backend\Block\Menu setAdditionalCacheKeyInfo(array $cacheKeyInfo) + * @method $this setAdditionalCacheKeyInfo(array $cacheKeyInfo) * @method array getAdditionalCacheKeyInfo() - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api * @since 100.0.2 + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Menu extends \Magento\Backend\Block\Template { @@ -75,7 +75,12 @@ class Menu extends \Magento\Backend\Block\Template private $anchorRenderer; /** - * @param Template\Context $context + * @var \Magento\Framework\App\Route\ConfigInterface + */ + private $routeConfig; + + /** + * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Backend\Model\UrlInterface $url * @param \Magento\Backend\Model\Menu\Filter\IteratorFactory $iteratorFactory * @param \Magento\Backend\Model\Auth\Session $authSession @@ -84,6 +89,9 @@ class Menu extends \Magento\Backend\Block\Template * @param array $data * @param MenuItemChecker|null $menuItemChecker * @param AnchorRenderer|null $anchorRenderer + * @param \Magento\Framework\App\Route\ConfigInterface|null $routeConfig + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -94,7 +102,8 @@ public function __construct( \Magento\Framework\Locale\ResolverInterface $localeResolver, array $data = [], MenuItemChecker $menuItemChecker = null, - AnchorRenderer $anchorRenderer = null + AnchorRenderer $anchorRenderer = null, + \Magento\Framework\App\Route\ConfigInterface $routeConfig = null ) { $this->_url = $url; $this->_iteratorFactory = $iteratorFactory; @@ -103,6 +112,9 @@ public function __construct( $this->_localeResolver = $localeResolver; $this->menuItemChecker = $menuItemChecker; $this->anchorRenderer = $anchorRenderer; + $this->routeConfig = $routeConfig ?: + \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Framework\App\Route\ConfigInterface::class); parent::__construct($context, $data); } @@ -130,6 +142,7 @@ protected function _getAnchorLabel($menuItem) /** * Render menu item mouse events + * * @param \Magento\Backend\Model\Menu\Item $menuItem * @return string */ @@ -203,8 +216,9 @@ protected function _afterToHtml($html) */ protected function _callbackSecretKey($match) { + $routeId = $this->routeConfig->getRouteByFrontName($match[1]); return \Magento\Backend\Model\UrlInterface::SECRET_KEY_PARAM_NAME . '/' . $this->_url->getSecretKey( - $match[1], + $routeId ?: $match[1], $match[2], $match[3] ); @@ -342,7 +356,7 @@ protected function _columnBrake($items, $limit) * @param \Magento\Backend\Model\Menu\Item $menuItem * @param int $level * @param int $limit - * @param $id int + * @param int|null $id * @return string HTML code */ protected function _addSubMenu($menuItem, $level, $limit, $id = null) diff --git a/app/code/Magento/Backend/Block/Page/Footer.php b/app/code/Magento/Backend/Block/Page/Footer.php index 3d1570e5ddfe..e0c173a4cbfe 100644 --- a/app/code/Magento/Backend/Block/Page/Footer.php +++ b/app/code/Magento/Backend/Block/Page/Footer.php @@ -40,7 +40,7 @@ public function __construct( } /** - * @return void + * @inheritdoc */ protected function _construct() { @@ -57,4 +57,12 @@ public function getMagentoVersion() { return $this->productMetadata->getVersion(); } + + /** + * @inheritdoc + */ + protected function getCacheLifetime() + { + return 3600 * 24 * 10; + } } 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 5a09e1f17f61..6004d08b4b73 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/System/Store/Delete/Form.php b/app/code/Magento/Backend/Block/System/Store/Delete/Form.php index e479e8f560da..47a156c16ce3 100644 --- a/app/code/Magento/Backend/Block/System/Store/Delete/Form.php +++ b/app/code/Magento/Backend/Block/System/Store/Delete/Form.php @@ -5,6 +5,9 @@ */ namespace Magento\Backend\Block\System\Store\Delete; +use Magento\Backup\Helper\Data as BackupHelper; +use Magento\Framework\App\ObjectManager; + /** * Adminhtml cms block edit form * @@ -12,6 +15,25 @@ */ class Form extends \Magento\Backend\Block\Widget\Form\Generic { + /** + * @var BackupHelper + */ + private $backup; + + /** + * @inheritDoc + */ + public function __construct( + \Magento\Backend\Block\Template\Context $context, + \Magento\Framework\Registry $registry, + \Magento\Framework\Data\FormFactory $formFactory, + array $data = [], + ?BackupHelper $backup = null + ) { + parent::__construct($context, $registry, $formFactory, $data); + $this->backup = $backup ?? ObjectManager::getInstance()->get(BackupHelper::class); + } + /** * Init form * @@ -25,7 +47,7 @@ protected function _construct() } /** - * {@inheritdoc} + * @inheritDoc */ protected function _prepareForm() { @@ -45,6 +67,12 @@ protected function _prepareForm() $fieldset->addField('item_id', 'hidden', ['name' => 'item_id', 'value' => $dataObject->getId()]); + $backupOptions = ['0' => __('No')]; + $backupSelected = '0'; + if ($this->backup->isEnabled()) { + $backupOptions['1'] = __('Yes'); + $backupSelected = '1'; + } $fieldset->addField( 'create_backup', 'select', @@ -52,8 +80,8 @@ protected function _prepareForm() 'label' => __('Create DB Backup'), 'title' => __('Create DB Backup'), 'name' => 'create_backup', - 'options' => ['1' => __('Yes'), '0' => __('No')], - 'value' => '1' + 'options' => $backupOptions, + 'value' => $backupSelected ] ); diff --git a/app/code/Magento/Backend/Block/Template/Context.php b/app/code/Magento/Backend/Block/Template/Context.php index 6efc8d86802c..27c777c6d400 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/Button/ButtonList.php b/app/code/Magento/Backend/Block/Widget/Button/ButtonList.php index 94af9a1d7578..5a792ddb3913 100644 --- a/app/code/Magento/Backend/Block/Widget/Button/ButtonList.php +++ b/app/code/Magento/Backend/Block/Widget/Button/ButtonList.php @@ -7,6 +7,8 @@ namespace Magento\Backend\Block\Widget\Button; /** + * Button list widget + * * @api * @since 100.0.2 */ @@ -127,12 +129,6 @@ public function getItems() */ public function sortButtons(Item $itemA, Item $itemB) { - $sortOrderA = intval($itemA->getSortOrder()); - $sortOrderB = intval($itemB->getSortOrder()); - - if ($sortOrderA == $sortOrderB) { - return 0; - } - return ($sortOrderA < $sortOrderB) ? -1 : 1; + return (int)$itemA->getSortOrder() <=> (int)$itemB->getSortOrder(); } } diff --git a/app/code/Magento/Backend/Block/Widget/Form.php b/app/code/Magento/Backend/Block/Widget/Form.php index 30221618edbe..38d5d90a22d1 100644 --- a/app/code/Magento/Backend/Block/Widget/Form.php +++ b/app/code/Magento/Backend/Block/Widget/Form.php @@ -3,8 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Backend\Block\Widget; +use Magento\Framework\App\ObjectManager; + /** * Backend form widget * @@ -27,13 +30,23 @@ class Form extends \Magento\Backend\Block\Widget */ protected $_template = 'Magento_Backend::widget/form.phtml'; + /** @var Form\Element\ElementCreator */ + private $creator; + /** + * Constructs form + * * @param \Magento\Backend\Block\Template\Context $context * @param array $data + * @param Form\Element\ElementCreator|null $creator */ - public function __construct(\Magento\Backend\Block\Template\Context $context, array $data = []) - { + public function __construct( + \Magento\Backend\Block\Template\Context $context, + array $data = [], + Form\Element\ElementCreator $creator = null + ) { parent::__construct($context, $data); + $this->creator = $creator ?: ObjectManager::getInstance()->get(Form\Element\ElementCreator::class); } /** @@ -46,7 +59,6 @@ protected function _construct() parent::_construct(); $this->setDestElementId('edit_form'); - $this->setShowGlobalIcon(false); } /** @@ -148,6 +160,7 @@ protected function _beforeToHtml() /** * Initialize form fields values + * * Method will be called after prepareForm and can be used for field values initialization * * @return $this @@ -173,32 +186,11 @@ protected function _setFieldset($attributes, $fieldset, $exclude = []) if (!$this->_isAttributeVisible($attribute)) { continue; } - if (($inputType = $attribute->getFrontend()->getInputType()) && !in_array( - $attribute->getAttributeCode(), - $exclude - ) && ('media_image' != $inputType || $attribute->getAttributeCode() == 'image') + if (($inputType = $attribute->getFrontend()->getInputType()) + && !in_array($attribute->getAttributeCode(), $exclude) + && ('media_image' !== $inputType || $attribute->getAttributeCode() == 'image') ) { - $fieldType = $inputType; - $rendererClass = $attribute->getFrontend()->getInputRendererClass(); - if (!empty($rendererClass)) { - $fieldType = $inputType . '_' . $attribute->getAttributeCode(); - $fieldset->addType($fieldType, $rendererClass); - } - - $element = $fieldset->addField( - $attribute->getAttributeCode(), - $fieldType, - [ - 'name' => $attribute->getAttributeCode(), - 'label' => $attribute->getFrontend()->getLocalizedLabel(), - 'class' => $attribute->getFrontend()->getClass(), - 'required' => $attribute->getIsRequired(), - 'note' => $attribute->getNote() - ] - )->setEntityAttribute( - $attribute - ); - + $element = $this->creator->create($fieldset, $attribute); $element->setAfterElementHtml($this->_getAdditionalElementHtml($element)); $this->_applyTypeSpecificConfig($inputType, $element, $attribute); diff --git a/app/code/Magento/Backend/Block/Widget/Form/Container.php b/app/code/Magento/Backend/Block/Widget/Form/Container.php index 8b7babc1bb9b..febaae386168 100644 --- a/app/code/Magento/Backend/Block/Widget/Form/Container.php +++ b/app/code/Magento/Backend/Block/Widget/Form/Container.php @@ -56,6 +56,8 @@ class Container extends \Magento\Backend\Block\Widget\Container protected $_template = 'Magento_Backend::widget/form/container.phtml'; /** + * Initialize form. + * * @return void */ protected function _construct() @@ -83,7 +85,7 @@ protected function _construct() -1 ); - $objId = $this->getRequest()->getParam($this->_objectId); + $objId = (int)$this->getRequest()->getParam($this->_objectId); if (!empty($objId)) { $this->addButton( @@ -93,7 +95,7 @@ protected function _construct() 'class' => 'delete', 'onclick' => 'deleteConfirm(\'' . __( 'Are you sure you want to do this?' - ) . '\', \'' . $this->getDeleteUrl() . '\')' + ) . '\', \'' . $this->getDeleteUrl() . '\', {data: {}})' ] ); } @@ -151,11 +153,13 @@ public function getBackUrl() } /** + * Get URL for delete button. + * * @return string */ public function getDeleteUrl() { - return $this->getUrl('*/*/delete', [$this->_objectId => $this->getRequest()->getParam($this->_objectId)]); + return $this->getUrl('*/*/delete', [$this->_objectId => (int)$this->getRequest()->getParam($this->_objectId)]); } /** @@ -183,6 +187,8 @@ public function getFormActionUrl() } /** + * Get form HTML. + * * @return string */ public function getFormHtml() @@ -192,6 +198,8 @@ public function getFormHtml() } /** + * Get form init scripts. + * * @return string */ public function getFormInitScripts() @@ -203,6 +211,8 @@ public function getFormInitScripts() } /** + * Get form scripts. + * * @return string */ public function getFormScripts() @@ -214,6 +224,8 @@ public function getFormScripts() } /** + * Get header width. + * * @return string */ public function getHeaderWidth() @@ -222,6 +234,8 @@ public function getHeaderWidth() } /** + * Get header css class. + * * @return string */ public function getHeaderCssClass() @@ -230,6 +244,8 @@ public function getHeaderCssClass() } /** + * Get header HTML. + * * @return string */ public function getHeaderHtml() diff --git a/app/code/Magento/Backend/Block/Widget/Form/Element/Dependence.php b/app/code/Magento/Backend/Block/Widget/Form/Element/Dependence.php index eff49c3b75ab..d599d5fbad5e 100644 --- a/app/code/Magento/Backend/Block/Widget/Form/Element/Dependence.php +++ b/app/code/Magento/Backend/Block/Widget/Form/Element/Dependence.php @@ -4,14 +4,13 @@ * See COPYING.txt for license details. */ +namespace Magento\Backend\Block\Widget\Form\Element; + /** * Form element dependencies mapper * Assumes that one element may depend on other element values. * Will toggle as "enabled" only if all elements it depends from toggle as true. - */ -namespace Magento\Backend\Block\Widget\Form\Element; - -/** + * * @api * @since 100.0.2 */ @@ -117,6 +116,7 @@ public function addConfigOptions(array $options) /** * HTML output getter + * * @return string */ protected function _toHtml() @@ -139,7 +139,8 @@ protected function _toHtml() } /** - * Field dependences JSON map generator + * Field dependencies JSON map generator + * * @return string */ protected function _getDependsJson() diff --git a/app/code/Magento/Backend/Block/Widget/Form/Element/ElementCreator.php b/app/code/Magento/Backend/Block/Widget/Form/Element/ElementCreator.php new file mode 100644 index 000000000000..b9cdd259796d --- /dev/null +++ b/app/code/Magento/Backend/Block/Widget/Form/Element/ElementCreator.php @@ -0,0 +1,136 @@ +modifiers = $modifiers; + } + + /** + * Creates element + * + * @param Fieldset $fieldset + * @param Attribute $attribute + * + * @return AbstractElement + */ + public function create(Fieldset $fieldset, Attribute $attribute): AbstractElement + { + $config = $this->getElementConfig($attribute); + + if (!empty($config['rendererClass'])) { + $fieldType = $config['inputType'] . '_' . $attribute->getAttributeCode(); + $fieldset->addType($fieldType, $config['rendererClass']); + } + + return $fieldset + ->addField($config['attribute_code'], $config['inputType'], $config) + ->setEntityAttribute($attribute); + } + + /** + * Returns element config + * + * @param Attribute $attribute + * @return array + */ + private function getElementConfig(Attribute $attribute): array + { + $defaultConfig = $this->createDefaultConfig($attribute); + $config = $this->modifyConfig($defaultConfig); + + $config['label'] = __($config['label']); + + return $config; + } + + /** + * Returns default config + * + * @param Attribute $attribute + * @return array + */ + private function createDefaultConfig(Attribute $attribute): array + { + return [ + 'inputType' => $attribute->getFrontend()->getInputType(), + 'rendererClass' => $attribute->getFrontend()->getInputRendererClass(), + 'attribute_code' => $attribute->getAttributeCode(), + 'name' => $attribute->getAttributeCode(), + 'label' => $attribute->getFrontend()->getLabel(), + 'class' => $attribute->getFrontend()->getClass(), + 'required' => $attribute->getIsRequired(), + 'note' => $attribute->getNote(), + ]; + } + + /** + * Modify config + * + * @param array $config + * @return array + */ + private function modifyConfig(array $config): array + { + if ($this->isModified($config['attribute_code'])) { + return $this->applyModifier($config); + } + return $config; + } + + /** + * Returns bool if attribute need to modify + * + * @param string $attribute_code + * @return bool + */ + private function isModified($attribute_code): bool + { + return isset($this->modifiers[$attribute_code]); + } + + /** + * Apply modifier to config + * + * @param array $config + * @return array + */ + private function applyModifier(array $config): array + { + $modifiedConfig = $this->modifiers[$config['attribute_code']]; + foreach (array_keys($config) as $key) { + if (isset($modifiedConfig[$key])) { + $config[$key] = $modifiedConfig[$key]; + } + } + return $config; + } +} diff --git a/app/code/Magento/Backend/Block/Widget/Grid.php b/app/code/Magento/Backend/Block/Widget/Grid.php index 72ab5a265d80..66298d23389f 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid.php +++ b/app/code/Magento/Backend/Block/Widget/Grid.php @@ -12,7 +12,7 @@ * @api * @deprecated 100.2.0 in favour of UI component implementation * @method string getRowClickCallback() getRowClickCallback() - * @method \Magento\Backend\Block\Widget\Grid setRowClickCallback() setRowClickCallback(string $value) + * @method \Magento\Backend\Block\Widget\Grid setRowClickCallback(string $value) * @SuppressWarnings(PHPMD.TooManyFields) * @since 100.0.2 */ @@ -150,7 +150,10 @@ public function __construct( } /** + * Internal constructor, that is called from real constructor + * * @return void + * * @SuppressWarnings(PHPMD.NPathComplexity) */ protected function _construct() @@ -709,6 +712,7 @@ public function getGridUrl() /** * Grid url getter + * * Version of getGridUrl() but with parameters * * @param array $params url parameters diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Theme.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Theme.php index d49ad2941146..a0907726ccc4 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Theme.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Theme.php @@ -9,6 +9,9 @@ */ namespace Magento\Backend\Block\Widget\Grid\Column\Filter; +/** + * Theme grid filter + */ class Theme extends \Magento\Backend\Block\Widget\Grid\Column\Filter\AbstractFilter { /** @@ -54,7 +57,8 @@ public function getHtml() } /** - * Retrieve options setted in column. + * Retrieve options set in column. + * * Or load if options was not set. * * @return array 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 b8a2e283b29a..623a75015eb2 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/AbstractRenderer.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/AbstractRenderer.php @@ -28,6 +28,8 @@ abstract class AbstractRenderer extends \Magento\Backend\Block\AbstractBlock imp protected $_column; /** + * Set column for renderer. + * * @param Column $column * @return $this */ @@ -38,6 +40,8 @@ public function setColumn($column) } /** + * Returns row associated with the renderer. + * * @return Column */ public function getColumn() @@ -48,7 +52,7 @@ public function getColumn() /** * Renders grid column * - * @param Object $row + * @param DataObject $row * @return string */ public function render(DataObject $row) @@ -66,7 +70,7 @@ public function render(DataObject $row) /** * Render column for export * - * @param Object $row + * @param DataObject $row * @return string */ public function renderExport(DataObject $row) @@ -75,7 +79,9 @@ public function renderExport(DataObject $row) } /** - * @param Object $row + * Returns value of the row. + * + * @param DataObject $row * @return mixed */ protected function _getValue(DataObject $row) @@ -92,7 +98,9 @@ protected function _getValue(DataObject $row) } /** - * @param Object $row + * Get pre-rendered input element. + * + * @param DataObject $row * @return string */ public function _getInputValueElement(DataObject $row) @@ -108,7 +116,9 @@ public function _getInputValueElement(DataObject $row) } /** - * @param Object $row + * Get input value by row. + * + * @param DataObject $row * @return mixed */ protected function _getInputValue(DataObject $row) @@ -117,6 +127,8 @@ protected function _getInputValue(DataObject $row) } /** + * Renders header of the column, + * * @return string */ public function renderHeader() @@ -148,6 +160,8 @@ public function renderHeader() } /** + * Render HTML properties. + * * @return string */ public function renderProperty() @@ -172,6 +186,8 @@ public function renderProperty() } /** + * Returns HTML for CSS. + * * @return string */ public function renderCss() diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Currency.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Currency.php index b3f467ce37c8..03566bce3fc3 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Currency.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Currency.php @@ -82,7 +82,7 @@ public function render(\Magento\Framework\DataObject $row) { if ($data = (string)$this->_getValue($row)) { $currency_code = $this->_getCurrencyCode($row); - $data = floatval($data) * $this->_getRate($row); + $data = (float)$data * $this->_getRate($row); $sign = (bool)(int)$this->getColumn()->getShowNumberSign() && $data > 0 ? '+' : ''; $data = sprintf("%f", $data); $data = $this->_localeCurrency->getCurrency($currency_code)->toCurrency($data); @@ -118,10 +118,10 @@ protected function _getCurrencyCode($row) protected function _getRate($row) { if ($rate = $this->getColumn()->getRate()) { - return floatval($rate); + return (float)$rate; } if ($rate = $row->getData($this->getColumn()->getRateField())) { - return floatval($rate); + return (float)$rate; } return $this->_defaultBaseCurrency->getRate($this->_getCurrencyCode($row)); } diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Price.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Price.php index e4300c63485f..9da23af83f03 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Price.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Price.php @@ -60,7 +60,7 @@ public function render(\Magento\Framework\DataObject $row) return $data; } - $data = floatval($data) * $this->_getRate($row); + $data = (float)$data * $this->_getRate($row); $data = sprintf("%f", $data); $data = $this->_localeCurrency->getCurrency($currencyCode)->toCurrency($data); return $data; @@ -94,10 +94,10 @@ protected function _getCurrencyCode($row) protected function _getRate($row) { if ($rate = $this->getColumn()->getRate()) { - return floatval($rate); + return (float)$rate; } if ($rate = $row->getData($this->getColumn()->getRateField())) { - return floatval($rate); + return (float)$rate; } return 1; } diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php b/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php index 185b1116b8f6..891b2a3ada72 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php @@ -7,6 +7,7 @@ namespace Magento\Backend\Block\Widget\Grid\Massaction; use Magento\Backend\Block\Widget\Grid\Massaction\VisibilityCheckerInterface as VisibilityChecker; +use Magento\Framework\Data\Collection\AbstractDb; use Magento\Framework\DataObject; /** @@ -52,7 +53,7 @@ public function __construct( } /** - * @return void + * @inheritdoc */ protected function _construct() { @@ -217,6 +218,7 @@ public function getGridJsObjectName() * Retrieve JSON string of selected checkboxes * * @return string + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getSelectedJson() { @@ -231,6 +233,7 @@ public function getSelectedJson() * Retrieve array of selected checkboxes * * @return string[] + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getSelected() { @@ -252,6 +255,8 @@ public function getApplyButtonHtml() } /** + * Get mass action javascript code. + * * @return string */ public function getJavaScript() @@ -268,6 +273,8 @@ public function getJavaScript() } /** + * Get grid ids in JSON format. + * * @return string */ public function getGridIdsJson() @@ -275,23 +282,28 @@ public function getGridIdsJson() if (!$this->getUseSelectAll()) { return ''; } - /** @var \Magento\Framework\Data\Collection $allIdsCollection */ - $allIdsCollection = clone $this->getParentBlock()->getCollection(); - if ($this->getMassactionIdField()) { - $massActionIdField = $this->getMassactionIdField(); + /** @var \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection $collection */ + $collection = clone $this->getParentBlock()->getCollection(); + + if ($collection instanceof AbstractDb) { + $idsSelect = clone $collection->getSelect(); + $idsSelect->reset(\Magento\Framework\DB\Select::ORDER); + $idsSelect->reset(\Magento\Framework\DB\Select::LIMIT_COUNT); + $idsSelect->reset(\Magento\Framework\DB\Select::LIMIT_OFFSET); + $idsSelect->reset(\Magento\Framework\DB\Select::COLUMNS); + $idsSelect->columns($this->getMassactionIdField(), 'main_table'); + $idList = $collection->getConnection()->fetchCol($idsSelect); } else { - $massActionIdField = $this->getParentBlock()->getMassactionIdField(); + $idList = $collection->setPageSize(0)->getColumnValues($this->getMassactionIdField()); } - $gridIds = $allIdsCollection->setPageSize(0)->getColumnValues($massActionIdField); - if (!empty($gridIds)) { - return join(",", $gridIds); - } - return ''; + return implode(',', $idList); } /** + * Get Html id. + * * @return string */ public function getHtmlId() diff --git a/app/code/Magento/Backend/Block/Widget/Tabs.php b/app/code/Magento/Backend/Block/Widget/Tabs.php index 333904e398cf..c7c1f93e8ca7 100644 --- a/app/code/Magento/Backend/Block/Widget/Tabs.php +++ b/app/code/Magento/Backend/Block/Widget/Tabs.php @@ -8,6 +8,8 @@ use Magento\Backend\Block\Widget\Tab\TabInterface; /** + * Tabs widget + * * @api * @SuppressWarnings(PHPMD.NumberOfChildren) * @since 100.0.2 @@ -178,6 +180,8 @@ protected function _addTabByName($tab, $tabId) } /** + * Get active tab id + * * @return string */ public function getActiveTabId() @@ -187,6 +191,7 @@ public function getActiveTabId() /** * Set Active Tab + * * Tab has to be not hidden and can show * * @param string $tabId @@ -231,7 +236,7 @@ protected function _setActiveTab($tabId) } /** - * {@inheritdoc} + * @inheritdoc */ protected function _beforeToHtml() { @@ -282,6 +287,8 @@ private function reorderTabs() } /** + * Apply tabs order + * * @param array $orderByPosition * @param array $orderByIdentity * @@ -294,7 +301,7 @@ private function applyTabsCorrectOrder(array $orderByPosition, array $orderByIde /** * Rearrange the positions by using the after tag for each tab. * - * @var integer $position + * @var int $position * @var TabInterface $tab */ foreach ($orderByPosition as $position => $tab) { @@ -338,6 +345,8 @@ private function finalTabsSortOrder(array $orderByPosition) } /** + * Get js object name + * * @return string */ public function getJsObjectName() @@ -346,6 +355,8 @@ public function getJsObjectName() } /** + * Get tabs ids + * * @return string[] */ public function getTabsIds() @@ -358,6 +369,8 @@ public function getTabsIds() } /** + * Get tab id + * * @param \Magento\Framework\DataObject|TabInterface $tab * @param bool $withPrefix * @return string @@ -371,6 +384,8 @@ public function getTabId($tab, $withPrefix = true) } /** + * CVan show tab + * * @param \Magento\Framework\DataObject|TabInterface $tab * @return bool */ @@ -383,6 +398,8 @@ public function canShowTab($tab) } /** + * Get tab is hidden + * * @param \Magento\Framework\DataObject|TabInterface $tab * @return bool * @SuppressWarnings(PHPMD.BooleanGetMethodName) @@ -396,6 +413,8 @@ public function getTabIsHidden($tab) } /** + * Get tab url + * * @param \Magento\Framework\DataObject|TabInterface $tab * @return string */ @@ -414,6 +433,8 @@ public function getTabUrl($tab) } /** + * Get tab title + * * @param \Magento\Framework\DataObject|TabInterface $tab * @return string */ @@ -426,6 +447,8 @@ public function getTabTitle($tab) } /** + * Get tab class + * * @param \Magento\Framework\DataObject|TabInterface $tab * @return string */ @@ -441,6 +464,8 @@ public function getTabClass($tab) } /** + * Get tab label + * * @param \Magento\Framework\DataObject|TabInterface $tab * @return string */ @@ -453,6 +478,8 @@ public function getTabLabel($tab) } /** + * Get tab content + * * @param \Magento\Framework\DataObject|TabInterface $tab * @return string */ @@ -468,7 +495,8 @@ public function getTabContent($tab) } /** - * Mark tabs as dependant of each other + * Mark tabs as dependent of each other + * * Arbitrary number of tabs can be specified, but at least two * * @param string $tabOneId diff --git a/app/code/Magento/Backend/Console/Command/AbstractCacheCommand.php b/app/code/Magento/Backend/Console/Command/AbstractCacheCommand.php index 70b01046f6af..11da740c4660 100644 --- a/app/code/Magento/Backend/Console/Command/AbstractCacheCommand.php +++ b/app/code/Magento/Backend/Console/Command/AbstractCacheCommand.php @@ -11,13 +11,15 @@ use Symfony\Component\Console\Input\InputOption; /** + * Abstract cache command + * * @api * @since 100.0.2 */ abstract class AbstractCacheCommand extends Command { /** - * Input option bootsrap + * Input option bootstrap */ const INPUT_KEY_BOOTSTRAP = 'bootstrap'; @@ -40,7 +42,7 @@ public function __construct(Manager $cacheManager) } /** - * {@inheritdoc} + * @inheritdoc */ protected function configure() { diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Auth/DeniedJson.php b/app/code/Magento/Backend/Controller/Adminhtml/Auth/DeniedJson.php index ad4546097768..23731e29f0df 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/Auth/DeniedJson.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/Auth/DeniedJson.php @@ -6,7 +6,9 @@ */ namespace Magento\Backend\Controller\Adminhtml\Auth; -class DeniedJson extends \Magento\Backend\Controller\Adminhtml\Auth +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class DeniedJson extends \Magento\Backend\Controller\Adminhtml\Auth implements HttpGetActionInterface { /** * @var \Magento\Framework\Controller\Result\JsonFactory diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Auth/Login.php b/app/code/Magento/Backend/Controller/Adminhtml/Auth/Login.php index e1ea57f63035..1de77c810f31 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/Auth/Login.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/Auth/Login.php @@ -6,11 +6,14 @@ */ namespace Magento\Backend\Controller\Adminhtml\Auth; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGet; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPost; + /** * @api * @since 100.0.2 */ -class Login extends \Magento\Backend\Controller\Adminhtml\Auth +class Login extends \Magento\Backend\Controller\Adminhtml\Auth implements HttpGet, HttpPost { /** * @var \Magento\Framework\View\Result\PageFactory diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Auth/Logout.php b/app/code/Magento/Backend/Controller/Adminhtml/Auth/Logout.php index e55c449a0e5b..d7ad080395e2 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/Auth/Logout.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/Auth/Logout.php @@ -6,7 +6,10 @@ */ namespace Magento\Backend\Controller\Adminhtml\Auth; -class Logout extends \Magento\Backend\Controller\Adminhtml\Auth +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGet; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPost; + +class Logout extends \Magento\Backend\Controller\Adminhtml\Auth implements HttpGet, HttpPost { /** * Administrator logout action diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanImages.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanImages.php index 888ce11313b8..79bc19256d27 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanImages.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanImages.php @@ -6,10 +6,11 @@ */ namespace Magento\Backend\Controller\Adminhtml\Cache; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Controller\ResultFactory; -class CleanImages extends \Magento\Backend\Controller\Adminhtml\Cache +class CleanImages extends \Magento\Backend\Controller\Adminhtml\Cache implements HttpGetActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanMedia.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanMedia.php index 5df0a7779c4c..36aca1afcc48 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanMedia.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanMedia.php @@ -6,10 +6,11 @@ */ namespace Magento\Backend\Controller\Adminhtml\Cache; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Controller\ResultFactory; -class CleanMedia extends \Magento\Backend\Controller\Adminhtml\Cache +class CleanMedia extends \Magento\Backend\Controller\Adminhtml\Cache implements HttpGetActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanStaticFiles.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanStaticFiles.php index 489eb5799a5e..a3a26c5cf624 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanStaticFiles.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/CleanStaticFiles.php @@ -6,9 +6,10 @@ */ namespace Magento\Backend\Controller\Adminhtml\Cache; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Framework\Controller\ResultFactory; -class CleanStaticFiles extends \Magento\Backend\Controller\Adminhtml\Cache +class CleanStaticFiles extends \Magento\Backend\Controller\Adminhtml\Cache implements HttpGetActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushAll.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushAll.php index a2f18b4baf53..daf424d14c55 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushAll.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushAll.php @@ -6,7 +6,9 @@ */ namespace Magento\Backend\Controller\Adminhtml\Cache; -class FlushAll extends \Magento\Backend\Controller\Adminhtml\Cache +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class FlushAll extends \Magento\Backend\Controller\Adminhtml\Cache implements HttpGetActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushSystem.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushSystem.php index 90ed3432fa87..f3474bf43872 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushSystem.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/FlushSystem.php @@ -6,7 +6,9 @@ */ namespace Magento\Backend\Controller\Adminhtml\Cache; -class FlushSystem extends \Magento\Backend\Controller\Adminhtml\Cache +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class FlushSystem extends \Magento\Backend\Controller\Adminhtml\Cache implements HttpGetActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Cache/Index.php b/app/code/Magento/Backend/Controller/Adminhtml/Cache/Index.php index 05bd309ca620..f1e908bb842e 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/Cache/Index.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/Cache/Index.php @@ -6,7 +6,9 @@ */ namespace Magento\Backend\Controller\Adminhtml\Cache; -class Index extends \Magento\Backend\Controller\Adminhtml\Cache +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\Backend\Controller\Adminhtml\Cache implements HttpGetActionInterface { /** * Display cache management grid diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/Index.php b/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/Index.php index d8c52f6c50bb..decca6837fa0 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/Index.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/Index.php @@ -6,7 +6,11 @@ */ namespace Magento\Backend\Controller\Adminhtml\Dashboard; -class Index extends \Magento\Backend\Controller\Adminhtml\Dashboard +use Magento\Backend\Controller\Adminhtml\Dashboard as DashboardAction; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; + +class Index extends DashboardAction implements HttpGetActionInterface, HttpPostActionInterface { /** * @var \Magento\Framework\View\Result\PageFactory diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/ProductsViewed.php b/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/ProductsViewed.php index 3907f4a4f71a..a42a44814cb0 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/ProductsViewed.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/ProductsViewed.php @@ -6,12 +6,17 @@ */ namespace Magento\Backend\Controller\Adminhtml\Dashboard; -class ProductsViewed extends AjaxBlock +use Magento\Framework\App\Action\HttpPostActionInterface; + +/** + * Get most viewed products controller. + */ +class ProductsViewed extends AjaxBlock implements HttpPostActionInterface { /** * Gets most viewed products list * - * @return \Magento\Backend\Model\View\Result\Page + * @return \Magento\Framework\Controller\Result\Raw */ public function execute() { diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/RefreshStatistics.php b/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/RefreshStatistics.php index c10d1a77997b..c709859adb19 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/RefreshStatistics.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/Dashboard/RefreshStatistics.php @@ -6,7 +6,13 @@ namespace Magento\Backend\Controller\Adminhtml\Dashboard; -class RefreshStatistics extends \Magento\Reports\Controller\Adminhtml\Report\Statistics +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Reports\Controller\Adminhtml\Report\Statistics; + +/** + * Refresh Dashboard statistics action. + */ +class RefreshStatistics extends Statistics implements HttpPostActionInterface { /** * @param \Magento\Backend\App\Action\Context $context @@ -25,6 +31,8 @@ public function __construct( } /** + * Refresh statistics. + * * @return \Magento\Backend\Model\View\Result\Redirect */ public function execute() diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Index/GlobalSearch.php b/app/code/Magento/Backend/Controller/Adminhtml/Index/GlobalSearch.php index 9ca4021d0835..37f3064aeaa3 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/Index/GlobalSearch.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/Index/GlobalSearch.php @@ -6,11 +6,15 @@ */ namespace Magento\Backend\Controller\Adminhtml\Index; +use Magento\Backend\Controller\Adminhtml\Index as IndexAction; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; + /** * @api * @since 100.0.2 */ -class GlobalSearch extends \Magento\Backend\Controller\Adminhtml\Index +class GlobalSearch extends IndexAction implements HttpGetActionInterface, HttpPostActionInterface { /** * @var \Magento\Framework\Controller\Result\JsonFactory diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Index/Index.php b/app/code/Magento/Backend/Controller/Adminhtml/Index/Index.php index a5c71fb2dbc5..afb1b4573271 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/Index/Index.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/Index/Index.php @@ -6,7 +6,10 @@ */ namespace Magento\Backend\Controller\Adminhtml\Index; -class Index extends \Magento\Backend\Controller\Adminhtml\Index +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGet; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPost; + +class Index extends \Magento\Backend\Controller\Adminhtml\Index implements HttpGet, HttpPost { /** * Admin area entry point diff --git a/app/code/Magento/Backend/Controller/Adminhtml/Noroute/Index.php b/app/code/Magento/Backend/Controller/Adminhtml/Noroute/Index.php index ce59d2fd48e5..e84987d8e1d7 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/Noroute/Index.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/Noroute/Index.php @@ -6,6 +6,9 @@ */ namespace Magento\Backend\Controller\Adminhtml\Noroute; +/** + * @SuppressWarnings(PHPMD.AllPurposeAction) + */ class Index extends \Magento\Backend\App\Action { /** diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Account/Index.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Account/Index.php index 54771bfdc1a7..648f1be86f56 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/System/Account/Index.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Account/Index.php @@ -6,7 +6,9 @@ */ namespace Magento\Backend\Controller\Adminhtml\System\Account; -class Index extends \Magento\Backend\Controller\Adminhtml\System\Account +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\Backend\Controller\Adminhtml\System\Account implements HttpGetActionInterface { /** * @var \Magento\Framework\View\Result\PageFactory diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Index.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Index.php index 30b26f229419..c6a05b5a71d0 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Index.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Index.php @@ -6,7 +6,9 @@ */ namespace Magento\Backend\Controller\Adminhtml\System\Design; -class Index extends \Magento\Backend\Controller\Adminhtml\System\Design +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\Backend\Controller\Adminhtml\System\Design implements HttpGetActionInterface { /** * @return \Magento\Backend\Model\View\Result\Page diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Save.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Save.php index 0228b48f7f11..25cfb61d658c 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Save.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Design/Save.php @@ -6,7 +6,12 @@ */ namespace Magento\Backend\Controller\Adminhtml\System\Design; -class Save extends \Magento\Backend\Controller\Adminhtml\System\Design +use Magento\Framework\App\Action\HttpPostActionInterface; + +/** + * Save design action. + */ +class Save extends \Magento\Backend\Controller\Adminhtml\System\Design implements HttpPostActionInterface { /** * Filtering posted data. Converting localized data if needed @@ -26,6 +31,8 @@ protected function _filterPostData($data) } /** + * Save design action. + * * @return \Magento\Backend\Model\View\Result\Redirect */ public function execute() @@ -54,10 +61,10 @@ public function execute() } catch (\Exception $e) { $this->messageManager->addErrorMessage($e->getMessage()); $this->_objectManager->get(\Magento\Backend\Model\Session::class)->setDesignData($data); - return $resultRedirect->setPath('adminhtml/*/', ['id' => $design->getId()]); + return $resultRedirect->setPath('*/*/edit', ['id' => $design->getId()]); } } - return $resultRedirect->setPath('adminhtml/*/'); + return $resultRedirect->setPath('*/*/'); } } diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store.php index 0beeb5168b6d..a9be14b77b29 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store.php @@ -14,6 +14,7 @@ * Store controller * * @author Magento Core Team + * @SuppressWarnings(PHPMD.AllPurposeAction) */ abstract class Store extends Action { @@ -86,6 +87,8 @@ protected function createPage() * Backup database * * @return bool + * + * @deprecated Backup module is to be removed. */ protected function _backupDatabase() { diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteGroupPost.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteGroupPost.php index 23364aac1f0a..49c327060dae 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteGroupPost.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store/DeleteGroupPost.php @@ -1,16 +1,20 @@ coreConfig->getValue(Store::XML_PATH_UNSECURE_BASE_URL, 'default'), - PHP_URL_SCHEME - ) === 'https' - || $this->backendConfig->isSetFlag(Store::XML_PATH_SECURE_IN_ADMINHTML) - && parse_url( - (string)$this->coreConfig->getValue(Store::XML_PATH_SECURE_BASE_URL, 'default'), - PHP_URL_SCHEME - ) === 'https'; + $baseUrl = (string)$this->coreConfig->getValue(Store::XML_PATH_UNSECURE_BASE_URL, 'default'); + if (parse_url($baseUrl, PHP_URL_SCHEME) === 'https') { + return true; + } + + if ($this->backendConfig->isSetFlag(Store::XML_PATH_SECURE_IN_ADMINHTML)) { + if ($this->backendConfig->isSetFlag('admin/url/use_custom')) { + $adminBaseUrl = (string)$this->coreConfig->getValue('admin/url/custom', 'default'); + } else { + $adminBaseUrl = (string)$this->coreConfig->getValue(Store::XML_PATH_SECURE_BASE_URL, 'default'); + } + return parse_url($adminBaseUrl, PHP_URL_SCHEME) === 'https'; + } + + return false; } /** - * {@inheritdoc} - * - * @return string + * @inheritdoc */ public function getDefaultPath() { diff --git a/app/code/Magento/Backend/Model/Auth/Session.php b/app/code/Magento/Backend/Model/Auth/Session.php index 593b4219d45f..61db71c1803e 100644 --- a/app/code/Magento/Backend/Model/Auth/Session.php +++ b/app/code/Magento/Backend/Model/Auth/Session.php @@ -5,21 +5,25 @@ */ namespace Magento\Backend\Model\Auth; +use Magento\Framework\Acl; +use Magento\Framework\AclFactory; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Stdlib\Cookie\CookieMetadataFactory; use Magento\Framework\Stdlib\CookieManagerInterface; +use Magento\Backend\Spi\SessionUserHydratorInterface; +use Magento\Backend\Spi\SessionAclHydratorInterface; +use Magento\User\Model\User; +use Magento\User\Model\UserFactory; /** * Backend Auth session model * * @api - * @method \Magento\User\Model\User|null getUser() - * @method \Magento\Backend\Model\Auth\Session setUser(\Magento\User\Model\User $value) - * @method \Magento\Framework\Acl|null getAcl() - * @method \Magento\Backend\Model\Auth\Session setAcl(\Magento\Framework\Acl $value) * @method int getUpdatedAt() * @method \Magento\Backend\Model\Auth\Session setUpdatedAt(int $value) * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @todo implement solution that keeps is_first_visit flag in session during redirects * @api * @since 100.0.2 @@ -55,6 +59,36 @@ class Session extends \Magento\Framework\Session\SessionManager implements \Mage */ protected $_config; + /** + * @var SessionUserHydratorInterface + */ + private $userHydrator; + + /** + * @var SessionAclHydratorInterface + */ + private $aclHydrator; + + /** + * @var UserFactory + */ + private $userFactory; + + /** + * @var AclFactory + */ + private $aclFactory; + + /** + * @var User|null + */ + private $user; + + /** + * @var Acl|null + */ + private $acl; + /** * @param \Magento\Framework\App\Request\Http $request * @param \Magento\Framework\Session\SidResolverInterface $sidResolver @@ -69,6 +103,10 @@ class Session extends \Magento\Framework\Session\SessionManager implements \Mage * @param \Magento\Backend\Model\UrlInterface $backendUrl * @param \Magento\Backend\App\ConfigInterface $config * @throws \Magento\Framework\Exception\SessionException + * @param SessionUserHydratorInterface|null $userHydrator + * @param SessionAclHydratorInterface|null $aclHydrator + * @param UserFactory|null $userFactory + * @param AclFactory|null $aclFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -83,11 +121,19 @@ public function __construct( \Magento\Framework\App\State $appState, \Magento\Framework\Acl\Builder $aclBuilder, \Magento\Backend\Model\UrlInterface $backendUrl, - \Magento\Backend\App\ConfigInterface $config + \Magento\Backend\App\ConfigInterface $config, + ?SessionUserHydratorInterface $userHydrator = null, + ?SessionAclHydratorInterface $aclHydrator = null, + ?UserFactory $userFactory = null, + ?AclFactory $aclFactory = null ) { $this->_config = $config; $this->_aclBuilder = $aclBuilder; $this->_backendUrl = $backendUrl; + $this->userHydrator = $userHydrator ?? ObjectManager::getInstance()->get(SessionUserHydratorInterface::class); + $this->aclHydrator = $aclHydrator ?? ObjectManager::getInstance()->get(SessionAclHydratorInterface::class); + $this->userFactory = $userFactory ?? ObjectManager::getInstance()->get(UserFactory::class); + $this->aclFactory = $aclFactory ?? ObjectManager::getInstance()->get(AclFactory::class); parent::__construct( $request, $sidResolver, @@ -230,6 +276,16 @@ public function processLogin() return $this; } + /** + * @inheritDoc + */ + public function destroy(array $options = null) + { + $this->user = null; + $this->acl = null; + parent::destroy($options); + } + /** * Process of configuring of current auth storage when logout was performed * @@ -253,4 +309,136 @@ public function isValidForPath($path) { return true; } + + /** + * Logged-in user. + * + * @return User|null + */ + public function getUser() + { + if (!$this->user) { + $userData = $this->getUserData(); + if ($userData) { + /** @var User $user */ + $user = $this->userFactory->create(); + $this->userHydrator->hydrate($user, $userData); + $this->user = $user; + } + } + + return $this->user; + } + + /** + * Set logged-in user instance. + * + * @param User|null $user + * @return Session + */ + public function setUser($user) + { + $this->setUserData(null); + if ($user) { + $this->setUserData($this->userHydrator->extract($user)); + } + $this->user = $user; + + return $this; + } + + /** + * Is user logged in? + * + * @return bool + */ + public function hasUser() + { + return $this->user || $this->hasUserData(); + } + + /** + * Remove logged-in user. + * + * @return Session + */ + public function unsUser() + { + $this->user = null; + return $this->unsUserData(); + } + + /** + * Logged-in user's ACL data. + * + * @return Acl|null + */ + public function getAcl() + { + if (!$this->acl) { + $aclData = $this->getUserAclData(); + if ($aclData) { + /** @var Acl $acl */ + $acl = $this->aclFactory->create(); + $this->aclHydrator->hydrate($acl, $aclData); + $this->acl = $acl; + } + } + + return $this->acl; + } + + /** + * Set logged-in user's ACL data instance. + * + * @param Acl|null $acl + * @return Session + */ + public function setAcl($acl) + { + $this->setUserAclData(null); + if ($acl) { + $this->setUserAclData($this->aclHydrator->extract($acl)); + } + $this->acl = $acl; + + return $this; + } + + /** + * Whether ACL data is present. + * + * @return bool + */ + public function hasAcl() + { + return $this->acl || $this->hasUserAclData(); + } + + /** + * Remove ACL data. + * + * @return Session + */ + public function unsAcl() + { + $this->acl = null; + return $this->unsUserAclData(); + } + + /** + * @inheritDoc + */ + public function writeClose() + { + //Updating data in session in case these objects has been changed. + if ($this->user) { + $this->setUser($this->user); + } + if ($this->acl) { + $this->setAcl($this->acl); + } + + parent::writeClose(); + } } diff --git a/app/code/Magento/Backend/Model/Auth/SessionAclHydrator.php b/app/code/Magento/Backend/Model/Auth/SessionAclHydrator.php new file mode 100644 index 000000000000..34e01be69667 --- /dev/null +++ b/app/code/Magento/Backend/Model/Auth/SessionAclHydrator.php @@ -0,0 +1,36 @@ + $acl->_rules, 'resources' => $acl->_resources, 'roles' => $acl->_roleRegistry]; + } + + /** + * @inheritDoc + */ + public function hydrate(Acl $target, array $data): void + { + $target->_rules = $data['rules']; + $target->_resources = $data['resources']; + $target->_roleRegistry = $data['roles']; + } +} diff --git a/app/code/Magento/Backend/Model/Auth/SessionUserHydrator.php b/app/code/Magento/Backend/Model/Auth/SessionUserHydrator.php new file mode 100644 index 000000000000..6dee8b7b302c --- /dev/null +++ b/app/code/Magento/Backend/Model/Auth/SessionUserHydrator.php @@ -0,0 +1,54 @@ +roleFactory = $roleFactory; + } + + /** + * @inheritDoc + */ + public function extract(User $user): array + { + return ['data' => $user->getData(), 'role_data' => $user->getRole()->getData()]; + } + + /** + * @inheritDoc + */ + public function hydrate(User $target, array $data): void + { + $target->setData($data['data']); + /** @var Role $role */ + $role = $this->roleFactory->create(); + $role->setData($data['role_data']); + $target->setData('extracted_role', $role); + $target->getRole(); + } +} diff --git a/app/code/Magento/Backend/Model/Config/SessionLifetime/BackendModel.php b/app/code/Magento/Backend/Model/Config/SessionLifetime/BackendModel.php index c106afb90a09..f6d08883d7a6 100644 --- a/app/code/Magento/Backend/Model/Config/SessionLifetime/BackendModel.php +++ b/app/code/Magento/Backend/Model/Config/SessionLifetime/BackendModel.php @@ -16,14 +16,17 @@ */ class BackendModel extends Value { - /** Maximum dmin session lifetime; 1 year*/ + /** Maximum admin session lifetime; 1 year*/ const MAX_LIFETIME = 31536000; /** Minimum admin session lifetime */ const MIN_LIFETIME = 60; /** + * Processing object before save data + * * @since 100.1.0 + * @throws LocalizedException */ public function beforeSave() { diff --git a/app/code/Magento/Backend/Model/Image/UploadResizeConfig.php b/app/code/Magento/Backend/Model/Image/UploadResizeConfig.php new file mode 100644 index 000000000000..8155aa5e2fe2 --- /dev/null +++ b/app/code/Magento/Backend/Model/Image/UploadResizeConfig.php @@ -0,0 +1,72 @@ +config = $config; + } + + /** + * Get maximal width value for resized image + * + * @return int + */ + public function getMaxWidth(): int + { + return (int)$this->config->getValue(self::XML_PATH_MAX_WIDTH_IMAGE); + } + + /** + * Get maximal height value for resized image + * + * @return int + */ + public function getMaxHeight(): int + { + return (int)$this->config->getValue(self::XML_PATH_MAX_HEIGHT_IMAGE); + } + + /** + * Get config value for frontend resize + * + * @return bool + */ + public function isResizeEnabled(): bool + { + return (bool)$this->config->getValue(self::XML_PATH_ENABLE_RESIZE); + } +} diff --git a/app/code/Magento/Backend/Model/Image/UploadResizeConfigInterface.php b/app/code/Magento/Backend/Model/Image/UploadResizeConfigInterface.php new file mode 100644 index 000000000000..50582dfafbcd --- /dev/null +++ b/app/code/Magento/Backend/Model/Image/UploadResizeConfigInterface.php @@ -0,0 +1,37 @@ +getChildren()->add($item, null, $index); } else { - $index = intval($index); + $index = (int) $index; if (!isset($this[$index])) { $this->offsetSet($index, $item); $this->_logger->info( diff --git a/app/code/Magento/Backend/Model/Menu/Builder.php b/app/code/Magento/Backend/Model/Menu/Builder.php index ae572deab53d..1c6e900bc5cf 100644 --- a/app/code/Magento/Backend/Model/Menu/Builder.php +++ b/app/code/Magento/Backend/Model/Menu/Builder.php @@ -102,6 +102,6 @@ public function getResult(\Magento\Backend\Model\Menu $menu) */ protected function _getParam($params, $paramName, $defaultValue = null) { - return isset($params[$paramName]) ? $params[$paramName] : $defaultValue; + return $params[$paramName] ?? $defaultValue; } } diff --git a/app/code/Magento/Backend/Model/Search/Customer.php b/app/code/Magento/Backend/Model/Search/Customer.php index 35a7359ce998..e76a1b77ab2d 100644 --- a/app/code/Magento/Backend/Model/Search/Customer.php +++ b/app/code/Magento/Backend/Model/Search/Customer.php @@ -89,7 +89,7 @@ public function load() $this->searchCriteriaBuilder->setCurrentPage($this->getStart()); $this->searchCriteriaBuilder->setPageSize($this->getLimit()); - $searchFields = ['firstname', 'lastname', 'company']; + $searchFields = ['firstname', 'lastname', 'billing_company']; $filters = []; foreach ($searchFields as $field) { $filters[] = $this->filterBuilder diff --git a/app/code/Magento/Backend/Model/Session/Quote.php b/app/code/Magento/Backend/Model/Session/Quote.php index 11edaa26f443..ed0312874565 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/Spi/SessionAclHydratorInterface.php b/app/code/Magento/Backend/Spi/SessionAclHydratorInterface.php new file mode 100644 index 000000000000..7227cc92fcc8 --- /dev/null +++ b/app/code/Magento/Backend/Spi/SessionAclHydratorInterface.php @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminNavigateMenuActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminNavigateMenuActionGroup.xml new file mode 100644 index 000000000000..8e0f5a067610 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminNavigateMenuActionGroup.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertAdminDashboardPageIsVisibleActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertAdminDashboardPageIsVisibleActionGroup.xml new file mode 100644 index 000000000000..1c86a736ac2f --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertAdminDashboardPageIsVisibleActionGroup.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertAdminSuccessLoginActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertAdminSuccessLoginActionGroup.xml new file mode 100644 index 000000000000..844f58c789a1 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertAdminSuccessLoginActionGroup.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertMessageOnAdminLoginActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertMessageOnAdminLoginActionGroup.xml new file mode 100644 index 000000000000..607fba3736c4 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertMessageOnAdminLoginActionGroup.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginAdminWithCredentialsActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginAdminWithCredentialsActionGroup.xml new file mode 100644 index 000000000000..6aaa612b249b --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginAdminWithCredentialsActionGroup.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginAsAdminActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginAsAdminActionGroup.xml index a7ef237a232b..b2fbadcbe38e 100644 --- a/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginAsAdminActionGroup.xml +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/LoginAsAdminActionGroup.xml @@ -10,13 +10,12 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - + - - - + + - \ No newline at end of file + diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/SetAdminAccountActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/SetAdminAccountActionGroup.xml new file mode 100644 index 000000000000..9e5c0bb3f39b --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/SetAdminAccountActionGroup.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/SetWebsiteCountryOptionsToDefaultActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/SetWebsiteCountryOptionsToDefaultActionGroup.xml new file mode 100644 index 000000000000..4519648eb1d1 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/SetWebsiteCountryOptionsToDefaultActionGroup.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + diff --git a/app/code/Magento/Backend/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Backend/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 000000000000..4fe600d194e6 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,46 @@ + + + + + + Content + Content + magento-backend-content + + + Store Design Schedule + Schedule + magento-backend-system-design-schedule + + + Dashboard + Dashboard + magento-backend-dashboard + + + Stores + Stores + magento-backend-stores + + + Stores + All Stores + magento-backend-system-store + + + Configuration + Configuration + magento-config-system-config + + + Cache Management + Cache Management + magento-backend-system-cache + + diff --git a/app/code/Magento/Backend/Test/Mftf/Page/AdminConfigurationStoresPage.xml b/app/code/Magento/Backend/Test/Mftf/Page/AdminConfigurationStoresPage.xml index d1bf3c2cb2ed..8afc2c5bbb32 100644 --- a/app/code/Magento/Backend/Test/Mftf/Page/AdminConfigurationStoresPage.xml +++ b/app/code/Magento/Backend/Test/Mftf/Page/AdminConfigurationStoresPage.xml @@ -14,4 +14,7 @@
+ +
+ diff --git a/app/code/Magento/Backend/Test/Mftf/Page/AdminDashboardPage.xml b/app/code/Magento/Backend/Test/Mftf/Page/AdminDashboardPage.xml new file mode 100644 index 000000000000..ed30395406f7 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Page/AdminDashboardPage.xml @@ -0,0 +1,14 @@ + + + + + +
+ + diff --git a/app/code/Magento/Backend/Test/Mftf/Page/AdminForgotPasswordPage.xml b/app/code/Magento/Backend/Test/Mftf/Page/AdminForgotPasswordPage.xml new file mode 100644 index 000000000000..84af56d102d8 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Page/AdminForgotPasswordPage.xml @@ -0,0 +1,14 @@ + + + + + +
+ + diff --git a/app/code/Magento/Backend/Test/Mftf/Page/AdminLoginPage.xml b/app/code/Magento/Backend/Test/Mftf/Page/AdminLoginPage.xml index b68b9914186f..78226d79273d 100644 --- a/app/code/Magento/Backend/Test/Mftf/Page/AdminLoginPage.xml +++ b/app/code/Magento/Backend/Test/Mftf/Page/AdminLoginPage.xml @@ -9,6 +9,7 @@ +
diff --git a/app/code/Magento/Backend/Test/Mftf/Page/AdminSystemAccountPage.xml b/app/code/Magento/Backend/Test/Mftf/Page/AdminSystemAccountPage.xml new file mode 100644 index 000000000000..2f04c2c11d28 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Page/AdminSystemAccountPage.xml @@ -0,0 +1,14 @@ + + + + + +
+ + diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminDashboardSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminDashboardSection.xml new file mode 100644 index 000000000000..664c335a4cfc --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminDashboardSection.xml @@ -0,0 +1,19 @@ + + + + +
+ + + + + + +
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminForgotPasswordFormSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminForgotPasswordFormSection.xml new file mode 100644 index 000000000000..efaca2212335 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminForgotPasswordFormSection.xml @@ -0,0 +1,15 @@ + + + + +
+ + +
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml index 441ce886f117..5b517c7be8a7 100644 --- a/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml @@ -10,5 +10,6 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd">
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminLoginFormSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminLoginFormSection.xml index 3b10fac7bb9d..bd65dea89abc 100644 --- a/app/code/Magento/Backend/Test/Mftf/Section/AdminLoginFormSection.xml +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminLoginFormSection.xml @@ -12,5 +12,6 @@ +
diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminLoginMessagesSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminLoginMessagesSection.xml new file mode 100644 index 000000000000..f6ada50ada35 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminLoginMessagesSection.xml @@ -0,0 +1,14 @@ + + + + +
+ +
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminMainActionsSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminMainActionsSection.xml index cb164d43a49f..4867b5ba5ae0 100644 --- a/app/code/Magento/Backend/Test/Mftf/Section/AdminMainActionsSection.xml +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminMainActionsSection.xml @@ -9,7 +9,9 @@
- - + + + +
diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminMenuSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminMenuSection.xml new file mode 100644 index 000000000000..8498ad8c52e4 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminMenuSection.xml @@ -0,0 +1,28 @@ + + + + +
+ + + + + + + + + + + + + + + +
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminMessagesSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminMessagesSection.xml index b1350d5dcc1d..88e740d689cd 100644 --- a/app/code/Magento/Backend/Test/Mftf/Section/AdminMessagesSection.xml +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminMessagesSection.xml @@ -12,5 +12,6 @@ +
diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminSlideOutDialogSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminSlideOutDialogSection.xml new file mode 100644 index 000000000000..a01e025ba3dc --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminSlideOutDialogSection.xml @@ -0,0 +1,16 @@ + + + +
+ + + + +
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminSystemAccountSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminSystemAccountSection.xml new file mode 100644 index 000000000000..b9570ce94594 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminSystemAccountSection.xml @@ -0,0 +1,15 @@ + + + + +
+ + +
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Section/CountryOptionsSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/CountryOptionsSection.xml new file mode 100644 index 000000000000..2e2e5aec35ec --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Section/CountryOptionsSection.xml @@ -0,0 +1,18 @@ + + + + +
+ + + + + +
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Section/LocaleOptionsSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/LocaleOptionsSection.xml new file mode 100644 index 000000000000..a460aaebf105 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Section/LocaleOptionsSection.xml @@ -0,0 +1,16 @@ + + + + +
+ + + +
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminAttributeTextSwatchesCanBeFiledTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminAttributeTextSwatchesCanBeFiledTest.xml new file mode 100644 index 000000000000..2c061e54f550 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminAttributeTextSwatchesCanBeFiledTest.xml @@ -0,0 +1,116 @@ + + + + + + + + + + <description value="Check that attribute text swatches can be filed"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-96710"/> + <useCaseId value="MAGETWO-96409"/> + <group value="backend"/> + <group value="ui"/> + </annotations> + <before> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + </before> + <after> + <!-- Delete all 10 store views --> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView1"> + <argument name="customStore" value="customStore"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView2"> + <argument name="customStore" value="NewStoreViewData"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView3"> + <argument name="customStore" value="storeViewData"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView4"> + <argument name="customStore" value="storeViewData1"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView5"> + <argument name="customStore" value="storeViewData2"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView6"> + <argument name="customStore" value="storeViewData3"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView7"> + <argument name="customStore" value="storeViewData4"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView8"> + <argument name="customStore" value="storeViewData5"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView9"> + <argument name="customStore" value="storeViewData6"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView10"> + <argument name="customStore" value="storeViewData7"/> + </actionGroup> + + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Create 10 store views --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView1"> + <argument name="customStore" value="customStore"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView2"> + <argument name="customStore" value="NewStoreViewData"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView3"> + <argument name="customStore" value="storeViewData"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView4"> + <argument name="customStore" value="storeViewData1"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView5"> + <argument name="customStore" value="storeViewData2"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView6"> + <argument name="customStore" value="storeViewData3"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView7"> + <argument name="customStore" value="storeViewData4"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView8"> + <argument name="customStore" value="storeViewData5"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView9"> + <argument name="customStore" value="storeViewData6"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView10"> + <argument name="customStore" value="storeViewData7"/> + </actionGroup> + + <!--Navigate to Product attribute page--> + <amOnPage url="{{ProductAttributePage.url}}" stepKey="navigateToNewProductAttributePage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <fillField userInput="test_label" selector="{{AttributePropertiesSection.DefaultLabel}}" stepKey="fillDefaultLabel"/> + <selectOption selector="{{AttributePropertiesSection.InputType}}" userInput="Text Swatch" stepKey="selectInputType"/> + <click selector="{{AttributePropertiesSection.addSwatch}}" stepKey="clickAddSwatch"/> + <waitForAjaxLoad stepKey="waitForAjaxLoad"/> + + <!-- Fill Swatch and Description fields for Admin --> + <fillField selector="{{AttributeManageSwatchSection.swatchField('Admin')}}" userInput="test" stepKey="fillSwatchForAdmin"/> + <fillField selector="{{AttributeManageSwatchSection.descriptionField('Admin')}}" userInput="test" stepKey="fillDescriptionForAdmin"/> + + <!-- Grab value Swatch and Description fields for Admin --> + <grabValueFrom selector="{{AttributeManageSwatchSection.swatchField('Admin')}}" stepKey="grabSwatchForAdmin"/> + <grabValueFrom selector="{{AttributeManageSwatchSection.descriptionField('Admin')}}" stepKey="grabDescriptionForAdmin"/> + + <!-- Check that Swatch and Description fields for Admin are not empty--> + <assertNotEmpty actual="$grabSwatchForAdmin" stepKey="checkSwatchFieldForAdmin"/> + <assertNotEmpty actual="$grabDescriptionForAdmin" stepKey="checkDescriptionFieldForAdmin"/> + </test> +</tests> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminContentScheduleNavigateMenuTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminContentScheduleNavigateMenuTest.xml new file mode 100644 index 000000000000..bead59653eee --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminContentScheduleNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminContentScheduleNavigateMenuTest"> + <annotations> + <features value="Backend"/> + <stories value="Menu Navigation"/> + <title value="Admin content schedule navigate menu test"/> + <description value="Admin should be able to navigate to Content > Schedule"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14117"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToContentSchedulePage"> + <argument name="menuUiId" value="{{AdminMenuContent.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuContentDesignSchedule.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuContentDesignSchedule.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardNavigateMenuTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardNavigateMenuTest.xml new file mode 100644 index 000000000000..33561d7c3b03 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardNavigateMenuTest.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminDashboardNavigateMenuTest"> + <annotations> + <features value="Backend"/> + <stories value="Menu Navigation"/> + <title value="Admin dashboard navigate menu test"/> + <description value="Admin should be able to navigate to Dashboard"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14116"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <click selector="{{AdminMenuSection.menuItem(AdminMenuDashboard.dataUiId)}}" stepKey="clickOnMenuItem"/> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuDashboard.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardWithChartsChart.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardWithChartsChart.xml new file mode 100644 index 000000000000..55cb5a71505a --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardWithChartsChart.xml @@ -0,0 +1,122 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminDashboardWithChartsTest"> + <annotations> + <features value="Backend"/> + <title value="Google chart on Magento dashboard"/> + <description value="Google chart on Magento dashboard page is not broken"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-98934"/> + <useCaseId value="MAGETWO-98584"/> + <group value="backend"/> + </annotations> + <before> + <magentoCLI command="config:set admin/dashboard/enable_charts 1" stepKey="setEnableCharts" /> + <createData entity="SimpleProduct2" stepKey="createProduct"> + <field key="price">150</field> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer"> + <field key="firstname">John1</field> + <field key="lastname">Doe1</field> + </createData> + </before> + <after> + <!-- Reset admin order filter --> + <comment userInput="Reset admin order filter" stepKey="resetAdminOrderFilter"/> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderFilters"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingOrderGrid"/> + <magentoCLI command="config:set admin/dashboard/enable_charts 0" stepKey="setDisableChartsAsDefault" /> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Login as admin --> + <comment userInput="Login as admin" stepKey="adminLogin"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!-- Grab quantity value --> + <comment userInput="Grab quantity value from dashboard" stepKey="grabQuantityFromDashboard"/> + <grabTextFrom selector="{{AdminDashboardSection.dashboardTotals('Quantity')}}" stepKey="grabStartQuantity"/> + <!-- Login as customer --> + <comment userInput="Login as customer" stepKey="loginAsCustomer"/> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> + <argument name="Customer" value="$$createCustomer$$" /> + </actionGroup> + <!-- Add Product to Shopping Cart--> + <comment userInput="Add product to the shopping cart" stepKey="addProductToCart"/> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToSimpleProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + <!--Go to Checkout--> + <comment userInput="Go to checkout" stepKey="goToCheckout"/> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingCheckoutPageWithShippingMethod"/> + <click selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask1"/> + <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> + <!-- Checkout select Check/Money Order payment --> + <comment userInput="Select Check/Money payment" stepKey="checkoutSelectCheckMoneyPayment"/> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> + <!-- Place Order --> + <comment userInput="Place order" stepKey="placeOrder"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> + <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="seeSuccessTitle"/> + <see selector="{{CheckoutSuccessMainSection.orderNumberText}}" userInput="Your order number is: " stepKey="seeOrderNumber"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> + <!-- Search for Order in the order grid --> + <comment userInput="Search for Order in the order grid" stepKey="searchOrderInGrid"/> + <actionGroup ref="filterOrderGridById" stepKey="filterOrderGridById"> + <argument name="orderId" value="$grabOrderNumber"/> + </actionGroup> + <waitForLoadingMaskToDisappear stepKey="waitForSearchingOrder"/> + <!-- Create invoice --> + <comment userInput="Create invoice" stepKey="createInvoice"/> + <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> + <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceButton"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Invoice" stepKey="seeNewInvoiceInPageTitle" after="clickInvoiceButton"/> + <see selector="{{AdminInvoiceTotalSection.total('Subtotal')}}" userInput="$150.00" stepKey="seeCorrectGrandTotal"/> + <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeSuccessInvoiceMessage"/> + <!--Create Shipment for the order--> + <comment userInput="Create Shipment for the order" stepKey="createShipmentForOrder"/> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage2"/> + <waitForPageLoad time="30" stepKey="waitForOrderListPageLoading"/> + <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="openOrderPageForShip"/> + <waitForPageLoad stepKey="waitForOrderDetailsPage"/> + <click selector="{{AdminOrderDetailsMainActionsSection.ship}}" stepKey="clickShipAction"/> + <waitForPageLoad stepKey="waitForShipmentPagePage"/> + <seeInCurrentUrl url="{{AdminShipmentNewPage.url}}" stepKey="seeOrderShipmentUrl"/> + <!--Submit Shipment--> + <comment userInput="Submit Shipment" stepKey="submitShipment"/> + <click selector="{{AdminShipmentMainActionsSection.submitShipment}}" stepKey="clickSubmitShipment"/> + <waitForPageLoad stepKey="waitForShipmentSubmit"/> + <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The shipment has been created." stepKey="seeShipmentCreateSuccess"/> + <!-- Go to dashboard page --> + <comment userInput="Go to dashboard page" stepKey="goToDashboardPage"/> + <amOnPage url="{{AdminDashboardPage.url}}" stepKey="amOnDashboardPage"/> + <waitForPageLoad stepKey="waitForDashboardPageLoad4"/> + <!-- Grab quantity value --> + <comment userInput="Grab quantity value from dashboard at the end" stepKey="grabQuantityFromDashboardAtTheEnd"/> + <grabTextFrom selector="{{AdminDashboardSection.dashboardTotals('Quantity')}}" stepKey="grabEndQuantity"/> + <!-- Assert that page is not broken --> + <comment userInput="Assert that dashboard page is not broken" stepKey="assertDashboardPageIsNotBroken"/> + <seeElement selector="{{AdminDashboardSection.dashboardDiagramOrderContentTab}}" stepKey="seeOrderContentTab"/> + <seeElement selector="{{AdminDashboardSection.dashboardDiagramContent}}" stepKey="seeDiagramContent"/> + <click selector="{{AdminDashboardSection.dashboardDiagramAmounts}}" stepKey="clickDashboardAmount"/> + <waitForLoadingMaskToDisappear stepKey="waitForDashboardAmountLoading"/> + <seeElement selector="{{AdminDashboardSection.dashboardDiagramAmountsContentTab}}" stepKey="seeDiagramAmountContent"/> + <seeElement selector="{{AdminDashboardSection.dashboardDiagramTotals}}" stepKey="seeAmountTotals"/> + <dontSeeJsError stepKey="dontSeeJsError"/> + <assertGreaterThan expected="$grabStartQuantity" actual="$grabEndQuantity" stepKey="checkQuantityWasChanged"/> + </test> +</tests> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminMenuNavigationWithSecretKeysTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminMenuNavigationWithSecretKeysTest.xml new file mode 100644 index 000000000000..c9a3b8089cc1 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminMenuNavigationWithSecretKeysTest.xml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMenuNavigationWithSecretKeysTest"> + <annotations> + <features value="Backend"/> + <stories value="Menu Navigation"/> + <title value="Admin should be able to navigate between menu options with secret url keys enabled"/> + <description value="Admin should be able to navigate between menu options with secret url keys enabled"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-95349"/> + <group value="menu"/> + </annotations> + <before> + <magentoCLI command="config:set admin/security/use_form_key 1" stepKey="enableUrlSecretKeys"/> + <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches1"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <magentoCLI command="config:set admin/security/use_form_key 0" stepKey="disableUrlSecretKeys"/> + <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches2"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <click selector="{{AdminMenuSection.stores}}" stepKey="clickStoresMenuOption1"/> + <waitForLoadingMaskToDisappear stepKey="waitForStoresMenu1" /> + <click selector="{{AdminMenuSection.configuration}}" stepKey="clickStoresConfigurationMenuOption1"/> + <waitForPageLoad stepKey="waitForConfigurationPageLoad1"/> + <seeCurrentUrlMatches regex="~\/admin\/system_config\/~" stepKey="seeCurrentUrlMatchesConfigPath1"/> + + <click selector="{{AdminMenuSection.catalog}}" stepKey="clickCatalogMenuOption"/> + <waitForLoadingMaskToDisappear stepKey="waitForCatalogMenu1" /> + <click selector="{{AdminMenuSection.catalogProducts}}" stepKey="clickCatalogProductsMenuOption"/> + <waitForPageLoad stepKey="waitForProductsPageLoad"/> + <seeCurrentUrlMatches regex="~\/catalog\/product\/~" stepKey="seeCurrentUrlMatchesProductsPath"/> + + <click selector="{{AdminMenuSection.stores}}" stepKey="clickStoresMenuOption2"/> + <waitForLoadingMaskToDisappear stepKey="waitForStoresMenu2" /> + <click selector="{{AdminMenuSection.configuration}}" stepKey="clickStoresConfigurationMenuOption2"/> + <waitForPageLoad stepKey="waitForConfigurationPageLoad2"/> + <seeCurrentUrlMatches regex="~\/admin\/system_config\/~" stepKey="seeCurrentUrlMatchesConfigPath2"/> + </test> +</tests> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresAllStoresNavigateMenuTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresAllStoresNavigateMenuTest.xml new file mode 100644 index 000000000000..7758b387e393 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresAllStoresNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminStoresAllStoresNavigateMenuTest"> + <annotations> + <features value="Backend"/> + <stories value="Menu Navigation"/> + <title value="Admin stores all stores navigate menu test"/> + <description value="Admin should be able to navigate to Stores > All Stores"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14118"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToStoresAllStoresPage"> + <argument name="menuUiId" value="{{AdminMenuStores.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuStoresSettingsAllStores.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuStoresSettingsAllStores.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresConfigurationNavigateMenuTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresConfigurationNavigateMenuTest.xml new file mode 100644 index 000000000000..a54269b186ba --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresConfigurationNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminStoresConfigurationNavigateMenuTest"> + <annotations> + <features value="Backend"/> + <stories value="Menu Navigation"/> + <title value="Admin stores configuration navigate menu test"/> + <description value="Admin should be able to navigate to Stores > Configuration"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14119"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToStoresConfigurationPage"> + <argument name="menuUiId" value="{{AdminMenuStores.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuStoresSettingsConfiguration.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuStoresSettingsConfiguration.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminSystemCacheManagementNavigateMenuTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminSystemCacheManagementNavigateMenuTest.xml new file mode 100644 index 000000000000..516631c1bd16 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminSystemCacheManagementNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminSystemCacheManagementNavigateMenuTest"> + <annotations> + <features value="Backend"/> + <stories value="Menu Navigation"/> + <title value="Admin system cache management navigate menu test"/> + <description value="Admin should be able to navigate to System > Cache Management"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14120"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToSystemCacheManagementPage"> + <argument name="menuUiId" value="{{AdminMenuSystem.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSystemToolsCacheManagement.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSystemToolsCacheManagement.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminUserLoginWithStoreCodeInUrlTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminUserLoginWithStoreCodeInUrlTest.xml new file mode 100644 index 000000000000..5485dcaea33e --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminUserLoginWithStoreCodeInUrlTest.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUserLoginWithStoreCodeInUrlTest"> + <annotations> + <features value="Backend"/> + <title value="Admin panel should be accessible with Add Store Code to URL setting enabled"/> + <description value="Admin panel should be accessible with Add Store Code to URL setting enabled"/> + <testCaseId value="MC-14279" /> + <group value="backend"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <magentoCLI command="config:set {{StorefrontEnableAddStoreCodeToUrls.path}} {{StorefrontEnableAddStoreCodeToUrls.value}}" stepKey="addStoreCodeToUrlEnable"/> + </before> + <after> + <magentoCLI command="config:set {{StorefrontDisableAddStoreCodeToUrls.path}} {{StorefrontDisableAddStoreCodeToUrls.value}}" stepKey="addStoreCodeToUrlDisable"/> + </after> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="AssertAdminDashboardPageIsVisibleActionGroup" stepKey="seeDashboardPage"/> + </test> +</tests> diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php index e8143b5f6b43..e62b73f39241 100644 --- a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php +++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/MassactionTest.php @@ -269,62 +269,6 @@ public function testGetGridIdsJsonWithoutUseSelectAll() $this->assertEmpty($this->_block->getGridIdsJson()); } - /** - * @param array $items - * @param string $result - * - * @dataProvider dataProviderGetGridIdsJsonWithUseSelectAll - */ - public function testGetGridIdsJsonWithUseSelectAll(array $items, $result) - { - $this->_block->setUseSelectAll(true); - - if ($this->_block->getMassactionIdField()) { - $massActionIdField = $this->_block->getMassactionIdField(); - } else { - $massActionIdField = $this->_block->getParentBlock()->getMassactionIdField(); - } - - $collectionMock = $this->getMockBuilder(\Magento\Framework\Data\Collection::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->_gridMock->expects($this->once()) - ->method('getCollection') - ->willReturn($collectionMock); - $collectionMock->expects($this->once()) - ->method('setPageSize') - ->with(0) - ->willReturnSelf(); - $collectionMock->expects($this->once()) - ->method('getColumnValues') - ->with($massActionIdField) - ->willReturn($items); - - $this->assertEquals($result, $this->_block->getGridIdsJson()); - } - - /** - * @return array - */ - public function dataProviderGetGridIdsJsonWithUseSelectAll() - { - return [ - [ - [], - '', - ], - [ - [1], - '1', - ], - [ - [1, 2, 3], - '1,2,3', - ], - ]; - } - /** * @param string $itemId * @param array|\Magento\Framework\DataObject $item diff --git a/app/code/Magento/Backend/Test/Unit/Model/AdminPathConfigTest.php b/app/code/Magento/Backend/Test/Unit/Model/AdminPathConfigTest.php index 4911dc1e9968..b373459b7864 100644 --- a/app/code/Magento/Backend/Test/Unit/Model/AdminPathConfigTest.php +++ b/app/code/Magento/Backend/Test/Unit/Model/AdminPathConfigTest.php @@ -76,17 +76,35 @@ public function testGetCurrentSecureUrl() * @param $unsecureBaseUrl * @param $useSecureInAdmin * @param $secureBaseUrl + * @param $useCustomUrl + * @param $customUrl * @param $expected * @dataProvider shouldBeSecureDataProvider */ - public function testShouldBeSecure($unsecureBaseUrl, $useSecureInAdmin, $secureBaseUrl, $expected) - { - $coreConfigValueMap = [ + public function testShouldBeSecure( + $unsecureBaseUrl, + $useSecureInAdmin, + $secureBaseUrl, + $useCustomUrl, + $customUrl, + $expected + ) { + $coreConfigValueMap = $this->returnValueMap([ [\Magento\Store\Model\Store::XML_PATH_UNSECURE_BASE_URL, 'default', null, $unsecureBaseUrl], [\Magento\Store\Model\Store::XML_PATH_SECURE_BASE_URL, 'default', null, $secureBaseUrl], - ]; - $this->coreConfig->expects($this->any())->method('getValue')->will($this->returnValueMap($coreConfigValueMap)); - $this->backendConfig->expects($this->any())->method('isSetFlag')->willReturn($useSecureInAdmin); + ['admin/url/custom', 'default', null, $customUrl], + ]); + $backendConfigFlagsMap = $this->returnValueMap([ + [\Magento\Store\Model\Store::XML_PATH_SECURE_IN_ADMINHTML, $useSecureInAdmin], + ['admin/url/use_custom', $useCustomUrl], + ]); + $this->coreConfig->expects($this->atLeast(1))->method('getValue') + ->will($coreConfigValueMap); + $this->coreConfig->expects($this->atMost(2))->method('getValue') + ->will($coreConfigValueMap); + + $this->backendConfig->expects($this->atMost(2))->method('isSetFlag') + ->will($backendConfigFlagsMap); $this->assertEquals($expected, $this->adminPathConfig->shouldBeSecure('')); } @@ -96,13 +114,13 @@ public function testShouldBeSecure($unsecureBaseUrl, $useSecureInAdmin, $secureB public function shouldBeSecureDataProvider() { return [ - ['http://localhost/', false, 'default', false], - ['http://localhost/', true, 'default', false], - ['https://localhost/', false, 'default', true], - ['https://localhost/', true, 'default', true], - ['http://localhost/', false, 'https://localhost/', false], - ['http://localhost/', true, 'https://localhost/', true], - ['https://localhost/', true, 'https://localhost/', true], + ['http://localhost/', false, 'default', false, '', false], + ['http://localhost/', true, 'default', false, '', false], + ['https://localhost/', false, 'default', false, '', true], + ['https://localhost/', true, 'default', false, '', true], + ['http://localhost/', false, 'https://localhost/', false, '', false], + ['http://localhost/', true, 'https://localhost/', false, '', true], + ['https://localhost/', true, 'https://localhost/', false, '', true], ]; } diff --git a/app/code/Magento/Backend/Test/Unit/Model/Auth/SessionTest.php b/app/code/Magento/Backend/Test/Unit/Model/Auth/SessionTest.php deleted file mode 100644 index f1a4bc355b08..000000000000 --- a/app/code/Magento/Backend/Test/Unit/Model/Auth/SessionTest.php +++ /dev/null @@ -1,273 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Backend\Test\Unit\Model\Auth; - -use Magento\Backend\Model\Auth\Session; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; - -/** - * Class SessionTest tests Magento\Backend\Model\Auth\Session - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class SessionTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Backend\App\Config | \PHPUnit_Framework_MockObject_MockObject - */ - protected $config; - - /** - * @var \Magento\Framework\Session\Config | \PHPUnit_Framework_MockObject_MockObject - */ - protected $sessionConfig; - - /** - * @var \Magento\Framework\Stdlib\CookieManagerInterface | \PHPUnit_Framework_MockObject_MockObject - */ - protected $cookieManager; - - /** - * @var \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory | \PHPUnit_Framework_MockObject_MockObject - */ - protected $cookieMetadataFactory; - - /** - * @var \Magento\Framework\Session\Storage | \PHPUnit_Framework_MockObject_MockObject - */ - protected $storage; - - /** - * @var \Magento\Framework\Acl\Builder | \PHPUnit_Framework_MockObject_MockObject - */ - protected $aclBuilder; - - /** - * @var Session - */ - protected $session; - - protected function setUp() - { - $this->cookieMetadataFactory = $this->createPartialMock( - \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory::class, - ['createPublicCookieMetadata'] - ); - - $this->config = $this->createPartialMock(\Magento\Backend\App\Config::class, ['getValue']); - $this->cookieManager = $this->createPartialMock( - \Magento\Framework\Stdlib\Cookie\PhpCookieManager::class, - ['getCookie', 'setPublicCookie'] - ); - $this->storage = $this->createPartialMock( - \Magento\Framework\Session\Storage::class, - ['getUser', 'getAcl', 'setAcl'] - ); - $this->sessionConfig = $this->createPartialMock( - \Magento\Framework\Session\Config::class, - ['getCookiePath', 'getCookieDomain', 'getCookieSecure', 'getCookieHttpOnly'] - ); - $this->aclBuilder = $this->getMockBuilder(\Magento\Framework\Acl\Builder::class) - ->disableOriginalConstructor() - ->getMock(); - $objectManager = new ObjectManager($this); - $this->session = $objectManager->getObject( - \Magento\Backend\Model\Auth\Session::class, - [ - 'config' => $this->config, - 'sessionConfig' => $this->sessionConfig, - 'cookieManager' => $this->cookieManager, - 'cookieMetadataFactory' => $this->cookieMetadataFactory, - 'storage' => $this->storage, - 'aclBuilder' => $this->aclBuilder - ] - ); - } - - protected function tearDown() - { - $this->config = null; - $this->sessionConfig = null; - $this->session = null; - } - - /** - * @dataProvider refreshAclDataProvider - * @param $isUserPassedViaParams - */ - public function testRefreshAcl($isUserPassedViaParams) - { - $aclMock = $this->getMockBuilder(\Magento\Framework\Acl::class)->disableOriginalConstructor()->getMock(); - $this->aclBuilder->expects($this->any())->method('getAcl')->willReturn($aclMock); - $userMock = $this->getMockBuilder(\Magento\User\Model\User::class) - ->setMethods(['getReloadAclFlag', 'setReloadAclFlag', 'unsetData', 'save']) - ->disableOriginalConstructor() - ->getMock(); - $userMock->expects($this->any())->method('getReloadAclFlag')->willReturn(true); - $userMock->expects($this->once())->method('setReloadAclFlag')->with('0')->willReturnSelf(); - $userMock->expects($this->once())->method('save'); - $this->storage->expects($this->once())->method('setAcl')->with($aclMock); - $this->storage->expects($this->any())->method('getAcl')->willReturn($aclMock); - if ($isUserPassedViaParams) { - $this->session->refreshAcl($userMock); - } else { - $this->storage->expects($this->once())->method('getUser')->willReturn($userMock); - $this->session->refreshAcl(); - } - $this->assertSame($aclMock, $this->session->getAcl()); - } - - /** - * @return array - */ - public function refreshAclDataProvider() - { - return [ - 'User set via params' => [true], - 'User set to session object' => [false] - ]; - } - - public function testIsLoggedInPositive() - { - $user = $this->createPartialMock(\Magento\User\Model\User::class, ['getId', '__wakeup']); - $user->expects($this->once()) - ->method('getId') - ->will($this->returnValue(1)); - - $this->storage->expects($this->any()) - ->method('getUser') - ->will($this->returnValue($user)); - - $this->assertTrue($this->session->isLoggedIn()); - } - - public function testProlong() - { - $name = session_name(); - $cookie = 'cookie'; - $lifetime = 900; - $path = '/'; - $domain = 'magento2'; - $secure = true; - $httpOnly = true; - - $this->config->expects($this->once()) - ->method('getValue') - ->with(\Magento\Backend\Model\Auth\Session::XML_PATH_SESSION_LIFETIME) - ->willReturn($lifetime); - $cookieMetadata = $this->createMock(\Magento\Framework\Stdlib\Cookie\PublicCookieMetadata::class); - $cookieMetadata->expects($this->once()) - ->method('setDuration') - ->with($lifetime) - ->will($this->returnSelf()); - $cookieMetadata->expects($this->once()) - ->method('setPath') - ->with($path) - ->will($this->returnSelf()); - $cookieMetadata->expects($this->once()) - ->method('setDomain') - ->with($domain) - ->will($this->returnSelf()); - $cookieMetadata->expects($this->once()) - ->method('setSecure') - ->with($secure) - ->will($this->returnSelf()); - $cookieMetadata->expects($this->once()) - ->method('setHttpOnly') - ->with($httpOnly) - ->will($this->returnSelf()); - - $this->cookieMetadataFactory->expects($this->once()) - ->method('createPublicCookieMetadata') - ->will($this->returnValue($cookieMetadata)); - - $this->cookieManager->expects($this->once()) - ->method('getCookie') - ->with($name) - ->will($this->returnValue($cookie)); - $this->cookieManager->expects($this->once()) - ->method('setPublicCookie') - ->with($name, $cookie, $cookieMetadata); - - $this->sessionConfig->expects($this->once()) - ->method('getCookiePath') - ->will($this->returnValue($path)); - $this->sessionConfig->expects($this->once()) - ->method('getCookieDomain') - ->will($this->returnValue($domain)); - $this->sessionConfig->expects($this->once()) - ->method('getCookieSecure') - ->will($this->returnValue($secure)); - $this->sessionConfig->expects($this->once()) - ->method('getCookieHttpOnly') - ->will($this->returnValue($httpOnly)); - - $this->session->prolong(); - - $this->assertLessThanOrEqual(time(), $this->session->getUpdatedAt()); - } - - /** - * @dataProvider isAllowedDataProvider - * @param bool $isUserDefined - * @param bool $isAclDefined - * @param bool $isAllowed - * @param true $expectedResult - */ - public function testIsAllowed($isUserDefined, $isAclDefined, $isAllowed, $expectedResult) - { - $userAclRole = 'userAclRole'; - if ($isAclDefined) { - $aclMock = $this->getMockBuilder(\Magento\Framework\Acl::class)->disableOriginalConstructor()->getMock(); - $this->storage->expects($this->any())->method('getAcl')->willReturn($aclMock); - } - if ($isUserDefined) { - $userMock = $this->getMockBuilder(\Magento\User\Model\User::class)->disableOriginalConstructor()->getMock(); - $this->storage->expects($this->once())->method('getUser')->willReturn($userMock); - } - if ($isAclDefined && $isUserDefined) { - $userMock->expects($this->any())->method('getAclRole')->willReturn($userAclRole); - $aclMock->expects($this->once())->method('isAllowed')->with($userAclRole)->willReturn($isAllowed); - } - - $this->assertEquals($expectedResult, $this->session->isAllowed('resource')); - } - - /** - * @return array - */ - public function isAllowedDataProvider() - { - return [ - "Negative: User not defined" => [false, true, true, false], - "Negative: Acl not defined" => [true, false, true, false], - "Negative: Permission denied" => [true, true, false, false], - "Positive: Permission granted" => [true, true, false, false], - ]; - } - - /** - * @dataProvider firstPageAfterLoginDataProvider - * @param bool $isFirstPageAfterLogin - */ - public function testFirstPageAfterLogin($isFirstPageAfterLogin) - { - $this->session->setIsFirstPageAfterLogin($isFirstPageAfterLogin); - $this->assertEquals($isFirstPageAfterLogin, $this->session->isFirstPageAfterLogin()); - } - - /** - * @return array - */ - public function firstPageAfterLoginDataProvider() - { - return [ - 'First page after login' => [true], - 'Not first page after login' => [false], - ]; - } -} diff --git a/app/code/Magento/Backend/Test/Unit/Model/Authorization/RoleLocatorTest.php b/app/code/Magento/Backend/Test/Unit/Model/Authorization/RoleLocatorTest.php deleted file mode 100644 index 5b3910e9445f..000000000000 --- a/app/code/Magento/Backend/Test/Unit/Model/Authorization/RoleLocatorTest.php +++ /dev/null @@ -1,36 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Backend\Test\Unit\Model\Authorization; - -class RoleLocatorTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Backend\Model\Authorization\RoleLocator - */ - protected $_model; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $_sessionMock = []; - - protected function setUp() - { - $this->_sessionMock = $this->createPartialMock( - \Magento\Backend\Model\Auth\Session::class, - ['getUser', 'getAclRole', 'hasUser'] - ); - $this->_model = new \Magento\Backend\Model\Authorization\RoleLocator($this->_sessionMock); - } - - public function testGetAclRoleIdReturnsCurrentUserAclRoleId() - { - $this->_sessionMock->expects($this->once())->method('hasUser')->will($this->returnValue(true)); - $this->_sessionMock->expects($this->once())->method('getUser')->will($this->returnSelf()); - $this->_sessionMock->expects($this->once())->method('getAclRole')->will($this->returnValue('some_role')); - $this->assertEquals('some_role', $this->_model->getAclRoleId()); - } -} diff --git a/app/code/Magento/Backend/Test/Unit/Model/Locale/ManagerTest.php b/app/code/Magento/Backend/Test/Unit/Model/Locale/ManagerTest.php deleted file mode 100644 index 77eb7cdb34d1..000000000000 --- a/app/code/Magento/Backend/Test/Unit/Model/Locale/ManagerTest.php +++ /dev/null @@ -1,127 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Backend\Test\Unit\Model\Locale; - -use Magento\Framework\Locale\Resolver; - -class ManagerTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Backend\Model\Locale\Manager - */ - protected $_model; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\TranslateInterface - */ - protected $_translator; - - /** - * @var \Magento\Backend\Model\Session - */ - protected $_session; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Backend\Model\Auth\Session - */ - protected $_authSession; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Backend\App\ConfigInterface - */ - protected $_backendConfig; - - protected function setUp() - { - $this->_session = $this->createMock(\Magento\Backend\Model\Session::class); - - $this->_authSession = $this->createPartialMock(\Magento\Backend\Model\Auth\Session::class, ['getUser']); - - $this->_backendConfig = $this->getMockForAbstractClass( - \Magento\Backend\App\ConfigInterface::class, - [], - '', - false - ); - - $userMock = new \Magento\Framework\DataObject(); - - $this->_authSession->expects($this->any())->method('getUser')->will($this->returnValue($userMock)); - - $this->_translator = $this->getMockBuilder(\Magento\Framework\TranslateInterface::class) - ->setMethods(['init', 'setLocale']) - ->getMockForAbstractClass(); - - $this->_translator->expects($this->any())->method('setLocale')->will($this->returnValue($this->_translator)); - - $this->_translator->expects($this->any())->method('init')->will($this->returnValue(false)); - - $this->_model = new \Magento\Backend\Model\Locale\Manager( - $this->_session, - $this->_authSession, - $this->_translator, - $this->_backendConfig - ); - } - - /** - * @return array - */ - public function switchBackendInterfaceLocaleDataProvider() - { - return ['case1' => ['locale' => 'de_DE'], 'case2' => ['locale' => 'en_US']]; - } - - /** - * @param string $locale - * @dataProvider switchBackendInterfaceLocaleDataProvider - * @covers \Magento\Backend\Model\Locale\Manager::switchBackendInterfaceLocale - */ - public function testSwitchBackendInterfaceLocale($locale) - { - $this->_model->switchBackendInterfaceLocale($locale); - - $userInterfaceLocale = $this->_authSession->getUser()->getInterfaceLocale(); - $this->assertEquals($userInterfaceLocale, $locale); - - $sessionLocale = $this->_session->getSessionLocale(); - $this->assertEquals($sessionLocale, null); - } - - /** - * @covers \Magento\Backend\Model\Locale\Manager::getUserInterfaceLocale - */ - public function testGetUserInterfaceLocaleDefault() - { - $locale = $this->_model->getUserInterfaceLocale(); - - $this->assertEquals($locale, Resolver::DEFAULT_LOCALE); - } - - /** - * @covers \Magento\Backend\Model\Locale\Manager::getUserInterfaceLocale - */ - public function testGetUserInterfaceLocale() - { - $this->_model->switchBackendInterfaceLocale('de_DE'); - $locale = $this->_model->getUserInterfaceLocale(); - - $this->assertEquals($locale, 'de_DE'); - } - - /** - * @covers \Magento\Backend\Model\Locale\Manager::getUserInterfaceLocale - */ - public function testGetUserInterfaceGeneralLocale() - { - $this->_backendConfig->expects($this->any()) - ->method('getValue') - ->with('general/locale/code') - ->willReturn('test_locale'); - $locale = $this->_model->getUserInterfaceLocale(); - $this->assertEquals($locale, 'test_locale'); - } -} diff --git a/app/code/Magento/Backend/Test/Unit/Model/Menu/ConfigTest.php b/app/code/Magento/Backend/Test/Unit/Model/Menu/ConfigTest.php index 260a38a481b3..2b5f644e3597 100644 --- a/app/code/Magento/Backend/Test/Unit/Model/Menu/ConfigTest.php +++ b/app/code/Magento/Backend/Test/Unit/Model/Menu/ConfigTest.php @@ -168,6 +168,6 @@ public function testGetMenuGenericExceptionIsNotLogged() } catch (\Exception $e) { return; } - $this->fail("Generic \Exception was not throwed"); + $this->fail("Generic \Exception was not thrown"); } } 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 869d4ba3f45b..d159225089af 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/Test/Unit/Service/V1/ModuleServiceTest.php b/app/code/Magento/Backend/Test/Unit/Service/V1/ModuleServiceTest.php new file mode 100644 index 000000000000..c7ff1d95617b --- /dev/null +++ b/app/code/Magento/Backend/Test/Unit/Service/V1/ModuleServiceTest.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Backend\Test\Unit\Service\V1; + +use Magento\Backend\Service\V1\ModuleService; +use Magento\Framework\Module\ModuleListInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + +/** + * Module List Service Test + * + * Covers \Magento\Sales\Model\ValidatorResultMerger + */ +class ModuleServiceTest extends \PHPUnit\Framework\TestCase +{ + /** + * Testable Object + * + * @var ModuleService + */ + private $moduleService; + + /** + * @var ModuleListInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $moduleListMock; + + /** + * Object Manager + * + * @var ObjectManager + */ + private $objectManager; + + /** + * Set Up + * + * @return void + */ + protected function setUp() + { + $this->moduleListMock = $this->createMock(ModuleListInterface::class); + $this->objectManager = new ObjectManager($this); + $this->moduleService = $this->objectManager->getObject( + ModuleService::class, + [ + 'moduleList' => $this->moduleListMock, + ] + ); + } + + /** + * Test getModules method + * + * @return void + */ + public function testGetModules() + { + $moduleNames = ['Magento_Backend', 'Magento_Catalog', 'Magento_Customer']; + $this->moduleListMock->expects($this->once())->method('getNames')->willReturn($moduleNames); + + $expected = $moduleNames; + $actual = $this->moduleService->getModules(); + $this->assertEquals($expected, $actual); + } +} diff --git a/app/code/Magento/Backend/composer.json b/app/code/Magento/Backend/composer.json index f9408768136b..e54bd136b349 100644 --- a/app/code/Magento/Backend/composer.json +++ b/app/code/Magento/Backend/composer.json @@ -22,6 +22,7 @@ "magento/module-store": "*", "magento/module-translation": "*", "magento/module-ui": "*", + "magento/module-authorization": "*", "magento/module-user": "*" }, "suggest": { diff --git a/app/code/Magento/Backend/etc/adminhtml/di.xml b/app/code/Magento/Backend/etc/adminhtml/di.xml index d8e9674d2b4c..4abea272c549 100644 --- a/app/code/Magento/Backend/etc/adminhtml/di.xml +++ b/app/code/Magento/Backend/etc/adminhtml/di.xml @@ -14,8 +14,6 @@ <preference for="Magento\Framework\App\DefaultPathInterface" type="Magento\Backend\App\DefaultPath" /> <preference for="Magento\Backend\App\ConfigInterface" type="Magento\Backend\App\Config" /> <preference for="Magento\Framework\App\Response\Http\FileFactory" type="Magento\Backend\App\Response\Http\FileFactory" /> - <preference for="Magento\Framework\App\Request\ValidatorInterface" - type="Magento\Backend\App\Request\BackendValidator" /> <type name="Magento\Framework\Stdlib\DateTime\Timezone"> <arguments> <argument name="scopeType" xsi:type="const">Magento\Framework\App\Config\ScopeConfigInterface::SCOPE_TYPE_DEFAULT</argument> @@ -169,4 +167,6 @@ <argument name="defaultClass" xsi:type="string">Magento\Backend\Block\Template</argument> </arguments> </type> + <preference for="CsrfRequestValidator" type="Magento\Backend\App\Request\BackendValidator" /> + <preference for="Magento\Backend\Model\Image\UploadResizeConfigInterface" type="Magento\Backend\Model\Image\UploadResizeConfig" /> </config> diff --git a/app/code/Magento/Backend/etc/adminhtml/system.xml b/app/code/Magento/Backend/etc/adminhtml/system.xml index 138aa1e4c287..65744e56d94a 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"> @@ -129,10 +129,10 @@ <field id="*/*/template_hints_storefront">1</field> <field id="*/*/template_hints_storefront_show_with_parameter">1</field> </depends> - <comment>Add the following paramater to the URL to show template hints ?templatehints=[parameter_value]</comment> + <comment>Add the following parameter to the URL to show template hints ?templatehints=[parameter_value]</comment> </field> <field id="template_hints_admin" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="0" showInStore="0"> - <label>Enabled Template Path Hints for Admin</label> + <label>Enable Template Path Hints for Admin</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> <field id="template_hints_blocks" translate="label" type="select" sortOrder="21" showInDefault="1" showInWebsite="1" showInStore="1"> @@ -169,27 +169,31 @@ </group> <group id="js" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="1"> <label>JavaScript Settings</label> - <field id="merge_files" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> + <field id="merge_files" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Merge JavaScript Files</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> - <field id="enable_js_bundling" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> + <field id="enable_js_bundling" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Enable JavaScript Bundling</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> - <field id="minify_files" translate="label comment" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> + <field id="minify_files" translate="label comment" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Minify JavaScript Files</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <comment>Minification is not applied in developer mode.</comment> </field> + <field id="move_inline_to_bottom" translate="label" type="select" sortOrder="25" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <label>Move JS code to the bottom of the page</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + </field> </group> <group id="css" translate="label" type="text" sortOrder="110" showInDefault="1" showInWebsite="1" showInStore="1"> <label>CSS Settings</label> - <field id="merge_css_files" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> + <field id="merge_css_files" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Merge CSS Files</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> - <field id="minify_files" translate="label comment" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> + <field id="minify_files" translate="label comment" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Minify CSS Files</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <comment>Minification is not applied in developer mode.</comment> @@ -323,11 +327,11 @@ <label>Port (25)</label> <comment>For Windows server only.</comment> </field> - <field id="set_return_path" translate="label" type="select" sortOrder="70" showInDefault="1" showInWebsite="0" showInStore="0"> + <field id="set_return_path" translate="label" type="select" sortOrder="70" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Set Return-Path</label> <source_model>Magento\Config\Model\Config\Source\Yesnocustom</source_model> </field> - <field id="return_path_email" translate="label" type="text" sortOrder="80" showInDefault="1" showInWebsite="0" showInStore="0"> + <field id="return_path_email" translate="label" type="text" sortOrder="80" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Return-Path Email</label> <validate>validate-email</validate> <backend_model>Magento\Config\Model\Config\Backend\Email\Address</backend_model> @@ -336,6 +340,30 @@ </depends> </field> </group> + <group id="upload_configuration" translate="label" type="text" sortOrder="1000" showInDefault="1" showInWebsite="1" showInStore="1"> + <label>Images Upload Configuration</label> + <field id="enable_resize" translate="label" type="select" sortOrder="200" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> + <label>Enable Frontend Resize</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <comment>Resize performed via javascript before file upload.</comment> + </field> + <field id="max_width" translate="label comment" type="text" sortOrder="300" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> + <label>Maximum Width</label> + <validate>validate-greater-than-zero validate-number required-entry</validate> + <comment>Maximum allowed width for uploaded image.</comment> + <depends> + <field id="enable_resize">1</field> + </depends> + </field> + <field id="max_height" translate="label comment" type="text" sortOrder="400" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> + <label>Maximum Height</label> + <validate>validate-greater-than-zero validate-number required-entry</validate> + <comment>Maximum allowed height for uploaded image.</comment> + <depends> + <field id="enable_resize">1</field> + </depends> + </field> + </group> </section> <section id="admin" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="0" showInStore="0"> <label>Admin</label> diff --git a/app/code/Magento/Backend/etc/config.xml b/app/code/Magento/Backend/etc/config.xml index b7aaf8bf20db..8283fa18dd37 100644 --- a/app/code/Magento/Backend/etc/config.xml +++ b/app/code/Magento/Backend/etc/config.xml @@ -28,6 +28,11 @@ <dashboard> <enable_charts>1</enable_charts> </dashboard> + <upload_configuration> + <enable_resize>1</enable_resize> + <max_width>1920</max_width> + <max_height>1200</max_height> + </upload_configuration> </system> <general> <validator_data> diff --git a/app/code/Magento/Backend/etc/di.xml b/app/code/Magento/Backend/etc/di.xml index c526703da997..41db85b9323a 100644 --- a/app/code/Magento/Backend/etc/di.xml +++ b/app/code/Magento/Backend/etc/di.xml @@ -198,4 +198,8 @@ <argument name="anchorRenderer" xsi:type="object">Magento\Backend\Block\AnchorRenderer</argument> </arguments> </type> + <preference for="Magento\Backend\Spi\SessionUserHydratorInterface" + type="Magento\Backend\Model\Auth\SessionUserHydrator" /> + <preference for="Magento\Backend\Spi\SessionAclHydratorInterface" + type="Magento\Backend\Model\Auth\SessionAclHydrator" /> </config> diff --git a/app/code/Magento/Backend/etc/module.xml b/app/code/Magento/Backend/etc/module.xml index 6d1691a0e560..03976396f6fd 100644 --- a/app/code/Magento/Backend/etc/module.xml +++ b/app/code/Magento/Backend/etc/module.xml @@ -6,9 +6,10 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> - <module name="Magento_Backend" > + <module name="Magento_Backend"> <sequence> <module name="Magento_Directory"/> + <module name="Magento_Theme"/> </sequence> </module> </config> diff --git a/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_cache_block.xml b/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_cache_block.xml index 50d210f71025..6d2ecd8d36a9 100644 --- a/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_cache_block.xml +++ b/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_cache_block.xml @@ -11,7 +11,7 @@ <block class="Magento\Backend\Block\Widget\Grid" name="adminhtml.cache.grid" as="grid"> <arguments> <argument name="id" xsi:type="string">cache_grid</argument> - <argument name="dataSource" xsi:type="object">Magento\Backend\Model\Cache\ResourceModel\Grid\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Backend\Model\Cache\ResourceModel\Grid\Collection</argument> <argument name="pager_visibility" xsi:type="string">0</argument> </arguments> <block class="Magento\Backend\Block\Widget\Grid\Massaction" name="adminhtml.cache.massaction" as="grid.massaction"> diff --git a/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_design_grid_block.xml b/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_design_grid_block.xml index b96614f4bd8d..41bfe6e78a2a 100644 --- a/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_design_grid_block.xml +++ b/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_design_grid_block.xml @@ -11,7 +11,7 @@ <block class="Magento\Backend\Block\Widget\Grid" name="adminhtml.system.design.grid" as="grid"> <arguments> <argument name="id" xsi:type="string">designGrid</argument> - <argument name="dataSource" xsi:type="object">Magento\Theme\Model\ResourceModel\Design\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Theme\Model\ResourceModel\Design\Collection</argument> <argument name="use_ajax" xsi:type="string">1</argument> <argument name="save_parameters_in_session" xsi:type="string">1</argument> <argument name="grid_url" xsi:type="url" path="*/*/grid"> diff --git a/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_store_grid_block.xml b/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_store_grid_block.xml index 126de5eb4084..d0c0d8fcbf69 100644 --- a/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_store_grid_block.xml +++ b/app/code/Magento/Backend/view/adminhtml/layout/adminhtml_system_store_grid_block.xml @@ -12,7 +12,7 @@ <arguments> <argument name="id" xsi:type="string">storeGrid</argument> <argument name="save_parameters_in_session" xsi:type="string">1</argument> - <argument name="dataSource" xsi:type="object">Magento\Store\Model\ResourceModel\Website\Grid\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Store\Model\ResourceModel\Website\Grid\Collection</argument> </arguments> <block class="Magento\Backend\Block\Widget\Grid\ColumnSet" name="adminhtml.system.store.grid.columnSet" as="grid.columnSet"> <arguments> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/admin/login.phtml b/app/code/Magento/Backend/view/adminhtml/templates/admin/login.phtml index 805e9783f3f1..52d5dd6d114e 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/admin/login.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/admin/login.phtml @@ -43,7 +43,7 @@ data-validate="{required:true}" value="" placeholder="<?= /* @escapeNotVerified */ __('password') ?>" - autocomplete="new-password" + autocomplete="off" /> </div> </div> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/media/uploader.phtml b/app/code/Magento/Backend/view/adminhtml/templates/media/uploader.phtml index 1e14dd837634..4d9ba6a8c4ba 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/media/uploader.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/media/uploader.phtml @@ -13,8 +13,9 @@ data-mage-init='{ "Magento_Backend/js/media-uploader" : { "maxFileSize": <?= /* @escapeNotVerified */ $block->getFileSizeService()->getMaxFileSize() ?>, - "maxWidth":<?= /* @escapeNotVerified */ \Magento\Framework\File\Uploader::MAX_IMAGE_WIDTH ?> , - "maxHeight": <?= /* @escapeNotVerified */ \Magento\Framework\File\Uploader::MAX_IMAGE_HEIGHT ?> + "maxWidth": <?= /* @escapeNotVerified */ $block->getImageUploadMaxWidth() ?>, + "maxHeight": <?= /* @escapeNotVerified */ $block->getImageUploadMaxHeight() ?>, + "isResizeEnabled": <?= /* @noEscape */ $block->getImageUploadConfigData()->getIsResizeEnabled() ?> } }' > diff --git a/app/code/Magento/Backend/view/adminhtml/templates/pageactions.phtml b/app/code/Magento/Backend/view/adminhtml/templates/pageactions.phtml index 69d545f12d07..0a1dcb0b626e 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/pageactions.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/pageactions.phtml @@ -8,7 +8,7 @@ ?> <?php if ($block->getChildHtml()):?> - <div data-mage-init='{"floatingHeader": {}}' class="page-actions" <?= /* @escapeNotVerified */ $block->getUiId('content-header') ?>> + <div data-mage-init='{"floatingHeader": {}}' class="page-actions floating-header" <?= /* @escapeNotVerified */ $block->getUiId('content-header') ?>> <?= $block->getChildHtml() ?> </div> <?php endif; ?> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/system/cache/additional.phtml b/app/code/Magento/Backend/view/adminhtml/templates/system/cache/additional.phtml index 8e30afdf51f7..b4bc42b95d0a 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/system/cache/additional.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/system/cache/additional.phtml @@ -11,10 +11,10 @@ $permissions = $block->getData('permissions'); ?> <?php if ($permissions && $permissions->hasAccessToAdditionalActions()): ?> <div class="additional-cache-management"> + <h2> + <span><?= $block->escapeHtml(__('Additional Cache Management')); ?></span> + </h2> <?php if ($permissions->hasAccessToFlushCatalogImages()): ?> - <h2> - <span><?= $block->escapeHtml(__('Additional Cache Management')); ?></span> - </h2> <p> <button onclick="setLocation('<?= $block->escapeJs($block->getCleanImagesUrl()); ?>')" type="button"> <?= $block->escapeHtml(__('Flush Catalog Images Cache')); ?> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/system/search.phtml b/app/code/Magento/Backend/view/adminhtml/templates/system/search.phtml index b50183ced29b..af369800287c 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/system/search.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/system/search.phtml @@ -42,7 +42,7 @@ <% if (data.items.length) { %> <% _.each(data.items, function(value){ %> <li class="item" - <%- data.optionData(value) %> + <%= data.optionData(value) %> > <a href="<%- value.url %>" class="title"><%- value.name %></a> <span class="type"><%- value.type %></span> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid.phtml index c665c1095a54..fad8f5968009 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid.phtml @@ -107,7 +107,7 @@ $numColumns = !is_null($block->getColumns()) ? sizeof($block->getColumns()) : 0; <?= /* @escapeNotVerified */ __('of %1', '<span>' . $block->getCollection()->getLastPageNumber() . '</span>') ?> </label> <?php if ($_curPage < $_lastPage): ?> - <button title="<?= /* @escapeNotVerified */ __('Next page') ?>" + <button type="button" title="<?= /* @escapeNotVerified */ __('Next page') ?>" class="action-next" onclick="<?= /* @escapeNotVerified */ $block->getJsObjectName() ?>.setPage('<?= /* @escapeNotVerified */ ($_curPage + 1) ?>');return false;"> <span><?= /* @escapeNotVerified */ __('Next page') ?></span> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/tabshoriz.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/tabshoriz.phtml index 062528e74220..c76f10da0f92 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/tabshoriz.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/tabshoriz.phtml @@ -18,7 +18,7 @@ <?php $_tabType = (!preg_match('/\s?ajax\s?/', $_tabClass) && $block->getTabUrl($_tab) != '#') ? 'link' : '' ?> <?php $_tabHref = $block->getTabUrl($_tab) == '#' ? '#' . $block->getTabId($_tab) . '_content' : $block->getTabUrl($_tab) ?> <li> - <a href="<?= /* @escapeNotVerified */ $_tabHref ?>" id="<?= /* @escapeNotVerified */ $block->getTabId($_tab) ?>" title="<?= /* @escapeNotVerified */ $block->getTabTitle($_tab) ?>" class="<?php $_tabClass ?>" data-tab-type="<?php $_tabType ?>"> + <a href="<?= $block->escapeHtmlAttr($_tabHref) ?>" id="<?= $block->escapeHtmlAttr($block->getTabId($_tab)) ?>" title="<?= $block->escapeHtmlAttr($block->getTabTitle($_tab)) ?>" class="<?= $block->escapeHtmlAttr($_tabClass) ?>" data-tab-type="<?= $block->escapeHtmlAttr($_tabType) ?>"> <span> <span class="changed" title="<?= /* @escapeNotVerified */ __('The information in this tab has been changed.') ?>"></span> <span class="error" title="<?= /* @escapeNotVerified */ __('This tab contains invalid data. Please resolve this before saving.') ?>"></span> diff --git a/app/code/Magento/Backend/view/adminhtml/ui_component/design_config_listing.xml b/app/code/Magento/Backend/view/adminhtml/ui_component/design_config_listing.xml index 93309c9a22ef..b0abec3aa9be 100644 --- a/app/code/Magento/Backend/view/adminhtml/ui_component/design_config_listing.xml +++ b/app/code/Magento/Backend/view/adminhtml/ui_component/design_config_listing.xml @@ -6,6 +6,7 @@ */ --> <listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <listingToolbar name="listing_top" /> <columns name="design_config_columns"> <column name="theme_theme_id" component="Magento_Ui/js/grid/columns/select" sortOrder="40"> <settings> diff --git a/app/code/Magento/Backend/view/adminhtml/web/js/media-uploader.js b/app/code/Magento/Backend/view/adminhtml/web/js/media-uploader.js index f9f43cebc592..119e7a35747c 100644 --- a/app/code/Magento/Backend/view/adminhtml/web/js/media-uploader.js +++ b/app/code/Magento/Backend/view/adminhtml/web/js/media-uploader.js @@ -33,9 +33,20 @@ define([ * @private */ _create: function () { - var - self = this, - progressTmpl = mageTemplate('[data-template="uploader"]'); + var self = this, + progressTmpl = mageTemplate('[data-template="uploader"]'), + isResizeEnabled = this.options.isResizeEnabled, + resizeConfiguration = { + action: 'resize', + maxWidth: this.options.maxWidth, + maxHeight: this.options.maxHeight + }; + + if (!isResizeEnabled) { + resizeConfiguration = { + action: 'resize' + }; + } this.element.find('input[type=file]').fileupload({ dataType: 'json', @@ -52,8 +63,7 @@ define([ * @param {Object} data */ add: function (e, data) { - var - fileSize, + var fileSize, tmpl; $.each(data.files, function (index, file) { @@ -124,11 +134,9 @@ define([ process: [{ action: 'load', fileTypes: /^image\/(gif|jpeg|png)$/ - }, { - action: 'resize', - maxWidth: this.options.maxWidth, - maxHeight: this.options.maxHeight - }, { + }, + resizeConfiguration, + { action: 'save' }] }); diff --git a/app/code/Magento/Backend/view/adminhtml/web/js/validate-store.js b/app/code/Magento/Backend/view/adminhtml/web/js/validate-store.js index 0a692a9b868c..c2a0d4dab1fb 100644 --- a/app/code/Magento/Backend/view/adminhtml/web/js/validate-store.js +++ b/app/code/Magento/Backend/view/adminhtml/web/js/validate-store.js @@ -67,6 +67,7 @@ define([ * 'Confirm' action handler. */ confirm: function () { + $('body').trigger('processStart'); dataPost().postData(requestData); } } diff --git a/app/code/Magento/Backend/view/adminhtml/web/template/dynamic-rows/grid.html b/app/code/Magento/Backend/view/adminhtml/web/template/dynamic-rows/grid.html index fe30ca7e83f1..0033f4c071e4 100644 --- a/app/code/Magento/Backend/view/adminhtml/web/template/dynamic-rows/grid.html +++ b/app/code/Magento/Backend/view/adminhtml/web/template/dynamic-rows/grid.html @@ -69,7 +69,7 @@ <!-- ko foreach: { data: $record().elems(), as: 'elem'} --> <td if="elem.template" - visible="elem.visible" + visible="elem.visible() && elem.formElement !== 'hidden'" disable="elem.disabled" css="$parent.setClasses(elem)" template="elem.template" diff --git a/app/code/Magento/Backup/Controller/Adminhtml/Index.php b/app/code/Magento/Backup/Controller/Adminhtml/Index.php index dcafbc7370d2..b62963947d7b 100644 --- a/app/code/Magento/Backup/Controller/Adminhtml/Index.php +++ b/app/code/Magento/Backup/Controller/Adminhtml/Index.php @@ -5,21 +5,26 @@ */ namespace Magento\Backup\Controller\Adminhtml; +use Magento\Backend\App\Action; +use Magento\Backup\Helper\Data as Helper; +use Magento\Framework\App\ObjectManager; + /** * Backup admin controller * * @author Magento Core Team <core@magentocommerce.com> * @api * @since 100.0.2 + * @SuppressWarnings(PHPMD.AllPurposeAction) */ -abstract class Index extends \Magento\Backend\App\Action +abstract class Index extends Action { /** * Authorization level of a basic admin session * * @see _isAllowed() */ - const ADMIN_RESOURCE = 'Magento_Backend::backup'; + const ADMIN_RESOURCE = 'Magento_Backup::backup'; /** * Core registry @@ -48,6 +53,11 @@ abstract class Index extends \Magento\Backend\App\Action */ protected $maintenanceMode; + /** + * @var Helper + */ + private $helper; + /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Framework\Registry $coreRegistry @@ -55,6 +65,7 @@ abstract class Index extends \Magento\Backend\App\Action * @param \Magento\Framework\App\Response\Http\FileFactory $fileFactory * @param \Magento\Backup\Model\BackupFactory $backupModelFactory * @param \Magento\Framework\App\MaintenanceMode $maintenanceMode + * @param Helper|null $helper */ public function __construct( \Magento\Backend\App\Action\Context $context, @@ -62,13 +73,27 @@ public function __construct( \Magento\Framework\Backup\Factory $backupFactory, \Magento\Framework\App\Response\Http\FileFactory $fileFactory, \Magento\Backup\Model\BackupFactory $backupModelFactory, - \Magento\Framework\App\MaintenanceMode $maintenanceMode + \Magento\Framework\App\MaintenanceMode $maintenanceMode, + ?Helper $helper = null ) { $this->_coreRegistry = $coreRegistry; $this->_backupFactory = $backupFactory; $this->_fileFactory = $fileFactory; $this->_backupModelFactory = $backupModelFactory; $this->maintenanceMode = $maintenanceMode; + $this->helper = $helper ?? ObjectManager::getInstance()->get(Helper::class); parent::__construct($context); } + + /** + * @inheritDoc + */ + public function dispatch(\Magento\Framework\App\RequestInterface $request) + { + if (!$this->helper->isEnabled()) { + return $this->_redirect('*/*/disabled'); + } + + return parent::dispatch($request); + } } diff --git a/app/code/Magento/Backup/Controller/Adminhtml/Index/Create.php b/app/code/Magento/Backup/Controller/Adminhtml/Index/Create.php index 53f45aff50cb..ee5a56e81483 100644 --- a/app/code/Magento/Backup/Controller/Adminhtml/Index/Create.php +++ b/app/code/Magento/Backup/Controller/Adminhtml/Index/Create.php @@ -1,15 +1,18 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Backup\Controller\Adminhtml\Index; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; -class Create extends \Magento\Backup\Controller\Adminhtml\Index +/** + * Create backup controller + */ +class Create extends \Magento\Backup\Controller\Adminhtml\Index implements HttpPostActionInterface { /** * Create backup action. @@ -55,7 +58,9 @@ public function execute() $this->_coreRegistry->register('backup_manager', $backupManager); if ($this->getRequest()->getParam('maintenance_mode')) { - if (!$this->maintenanceMode->set(true)) { + $this->maintenanceMode->set(true); + + if (!$this->maintenanceMode->isOn()) { $response->setError( __( 'You need more permissions to activate maintenance mode right now.' diff --git a/app/code/Magento/Backup/Controller/Adminhtml/Index/Disabled.php b/app/code/Magento/Backup/Controller/Adminhtml/Index/Disabled.php new file mode 100644 index 000000000000..f6fe430ae083 --- /dev/null +++ b/app/code/Magento/Backup/Controller/Adminhtml/Index/Disabled.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Backup\Controller\Adminhtml\Index; + +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\View\Result\PageFactory; + +/** + * Inform that backup is disabled. + */ +class Disabled extends Action implements HttpGetActionInterface +{ + /** + * @see _isAllowed() + */ + const ADMIN_RESOURCE = 'Magento_Backend::backup'; + + /** + * @var PageFactory + */ + private $pageFactory; + + /** + * @param Context $context + * @param PageFactory $pageFactory + */ + public function __construct(Context $context, PageFactory $pageFactory) + { + parent::__construct($context); + $this->pageFactory = $pageFactory; + } + + /** + * @inheritDoc + */ + public function execute() + { + return $this->pageFactory->create(); + } +} diff --git a/app/code/Magento/Backup/Controller/Adminhtml/Index/Index.php b/app/code/Magento/Backup/Controller/Adminhtml/Index/Index.php index 3bbda65cb4cf..271e3713034d 100644 --- a/app/code/Magento/Backup/Controller/Adminhtml/Index/Index.php +++ b/app/code/Magento/Backup/Controller/Adminhtml/Index/Index.php @@ -6,7 +6,9 @@ */ namespace Magento\Backup\Controller\Adminhtml\Index; -class Index extends \Magento\Backup\Controller\Adminhtml\Index +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\Backup\Controller\Adminhtml\Index implements HttpGetActionInterface { /** * Backup list action diff --git a/app/code/Magento/Backup/Controller/Adminhtml/Index/Rollback.php b/app/code/Magento/Backup/Controller/Adminhtml/Index/Rollback.php index 0451f6ed09bd..7f450e7e313c 100644 --- a/app/code/Magento/Backup/Controller/Adminhtml/Index/Rollback.php +++ b/app/code/Magento/Backup/Controller/Adminhtml/Index/Rollback.php @@ -6,13 +6,16 @@ */ namespace Magento\Backup\Controller\Adminhtml\Index; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; /** + * Backup rollback controller. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Rollback extends \Magento\Backup\Controller\Adminhtml\Index +class Rollback extends \Magento\Backup\Controller\Adminhtml\Index implements HttpPostActionInterface { /** * Rollback Action @@ -82,7 +85,9 @@ public function execute() } if ($this->getRequest()->getParam('maintenance_mode')) { - if (!$this->maintenanceMode->set(true)) { + $this->maintenanceMode->set(true); + + if (!$this->maintenanceMode->isOn()) { $response->setError( __( 'You need more permissions to activate maintenance mode right now.' @@ -122,6 +127,7 @@ public function execute() $adminSession->destroy(); $response->setRedirectUrl($this->getUrl('*')); + // phpcs:disable Magento2.Exceptions.ThrowCatch } catch (\Magento\Framework\Backup\Exception\CantLoadSnapshot $e) { $errorMsg = __('We can\'t find the backup file.'); } catch (\Magento\Framework\Backup\Exception\FtpConnectionFailed $e) { diff --git a/app/code/Magento/Backup/Cron/SystemBackup.php b/app/code/Magento/Backup/Cron/SystemBackup.php index 750262ab1c14..9502377a39d0 100644 --- a/app/code/Magento/Backup/Cron/SystemBackup.php +++ b/app/code/Magento/Backup/Cron/SystemBackup.php @@ -8,6 +8,9 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Store\Model\ScopeInterface; +/** + * Performs scheduled backup. + */ class SystemBackup { const XML_PATH_BACKUP_ENABLED = 'system/backup/enabled'; @@ -101,6 +104,10 @@ public function __construct( */ public function execute() { + if (!$this->_backupData->isEnabled()) { + return $this; + } + if (!$this->_scopeConfig->isSetFlag(self::XML_PATH_BACKUP_ENABLED, ScopeInterface::SCOPE_STORE)) { return $this; } diff --git a/app/code/Magento/Backup/Helper/Data.php b/app/code/Magento/Backup/Helper/Data.php index 3d60bf9d9c9c..c6df6a736685 100644 --- a/app/code/Magento/Backup/Helper/Data.php +++ b/app/code/Magento/Backup/Helper/Data.php @@ -3,6 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Backup\Helper; use Magento\Framework\App\Filesystem\DirectoryList; @@ -110,7 +113,7 @@ public function getBackupsDir() public function getExtensionByType($type) { $extensions = $this->getExtensions(); - return isset($extensions[$type]) ? $extensions[$type] : ''; + return $extensions[$type] ?? ''; } /** @@ -285,4 +288,14 @@ public function extractDataFromFilename($filename) return $result; } + + /** + * Is backup functionality enabled. + * + * @return bool + */ + public function isEnabled(): bool + { + return $this->scopeConfig->isSetFlag('system/backup/functionality_enabled'); + } } diff --git a/app/code/Magento/Backup/Model/Backup.php b/app/code/Magento/Backup/Model/Backup.php index 3768f2bf8c8c..c3507ecf5b45 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/Config/Backend/Cron.php b/app/code/Magento/Backup/Model/Config/Backend/Cron.php index 4855ef112950..2f0e4069f049 100644 --- a/app/code/Magento/Backup/Model/Config/Backend/Cron.php +++ b/app/code/Magento/Backup/Model/Config/Backend/Cron.php @@ -76,8 +76,8 @@ public function afterSave() if ($enabled) { $cronExprArray = [ - intval($time[1]), # Minute - intval($time[0]), # Hour + (int) $time[1], # Minute + (int) $time[0], # Hour $frequency == $frequencyMonthly ? '1' : '*', # Day of the Month '*', # Month of the Year $frequency == $frequencyWeekly ? '1' : '*', # Day of the Week diff --git a/app/code/Magento/Backup/Model/Db.php b/app/code/Magento/Backup/Model/Db.php index 8fbd5da1c984..084b35448a82 100644 --- a/app/code/Magento/Backup/Model/Db.php +++ b/app/code/Magento/Backup/Model/Db.php @@ -5,11 +5,18 @@ */ 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; + /** * Database backup model * * @api * @since 100.0.2 + * @deprecated Backup module is to be removed. */ class Db implements \Magento\Framework\Backup\Db\BackupDbInterface { @@ -34,15 +41,40 @@ class Db implements \Magento\Framework\Backup\Db\BackupDbInterface protected $_resource = null; /** - * @param \Magento\Backup\Model\ResourceModel\Db $resourceDb + * @var Helper + */ + private $helper; + + /** + * @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, - \Magento\Framework\App\ResourceConnection $resource + ResourceModel\Db $resourceDb, + \Magento\Framework\App\ResourceConnection $resource, + ?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); } /** @@ -63,6 +95,8 @@ public function getResource() } /** + * Tables list. + * * @return array */ public function getTables() @@ -71,6 +105,8 @@ public function getTables() } /** + * Command to recreate given table. + * * @param string $tableName * @param bool $addDropIfExists * @return string @@ -81,6 +117,8 @@ public function getTableCreateScript($tableName, $addDropIfExists = false) } /** + * Generate table's data dump. + * * @param string $tableName * @return string */ @@ -90,6 +128,8 @@ public function getTableDataDump($tableName) } /** + * Header for dumps. + * * @return string */ public function getHeader() @@ -98,6 +138,8 @@ public function getHeader() } /** + * Footer for dumps. + * * @return string */ public function getFooter() @@ -106,6 +148,8 @@ public function getFooter() } /** + * Get backup SQL. + * * @return string */ public function renderSql() @@ -124,18 +168,19 @@ public function renderSql() } /** - * Create backup and stream write to adapter - * - * @param \Magento\Framework\Backup\Db\BackupInterface $backup - * @return $this + * @inheritDoc */ public function createBackup(\Magento\Framework\Backup\Db\BackupInterface $backup) { + if (!$this->helper->isEnabled()) { + throw new RuntimeException(__('Backup functionality is disabled')); + } + $backup->open(true); $this->getResource()->beginTransaction(); - $tables = $this->getResource()->getTables(); + $tables = $this->getListTables->execute(); $backup->write($this->getResource()->getHeader()); @@ -172,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()); @@ -179,8 +226,6 @@ public function createBackup(\Magento\Framework\Backup\Db\BackupInterface $backu $this->getResource()->commitTransaction(); $backup->close(); - - return $this; } /** 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 000000000000..73c4221feba3 --- /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 000000000000..51b49dcb9e48 --- /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 000000000000..c76ea2842180 --- /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 4f34f24c3a80..ebc4ac1fb056 100644 --- a/app/code/Magento/Backup/Test/Mftf/ActionGroup/DeleteBackupActionGroup.xml +++ b/app/code/Magento/Backup/Test/Mftf/ActionGroup/DeleteBackupActionGroup.xml @@ -17,9 +17,10 @@ <click selector="{{AdminGridTableSection.backupRowCheckbox(backup.name)}}" stepKey="selectBackupRow"/> <selectOption selector="{{AdminGridActionSection.actionSelect}}" userInput="Delete" stepKey="selectDeleteAction"/> <click selector="{{AdminGridActionSection.submitButton}}" stepKey="clickSubmit"/> + <waitForPageLoad stepKey="waitForConfirmWindowToAppear"/> <see selector="{{AdminConfirmationModalSection.message}}" userInput="Are you sure you want to delete the selected backup(s)?" stepKey="seeConfirmationModal"/> + <waitForPageLoad stepKey="waitForSubmitAction"/> <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="clickOkConfirmDelete"/> <dontSee selector="{{AdminGridTableSection.backupNameColumn}}" userInput="{{backup.name}}" stepKey="dontSeeBackupInGrid"/> </actionGroup> - </actionGroups> diff --git a/app/code/Magento/Backup/Test/Mftf/Data/BackupData.xml b/app/code/Magento/Backup/Test/Mftf/Data/BackupData.xml index ae97351cafca..ad218cdd5750 100644 --- a/app/code/Magento/Backup/Test/Mftf/Data/BackupData.xml +++ b/app/code/Magento/Backup/Test/Mftf/Data/BackupData.xml @@ -20,4 +20,8 @@ <data key="name" unique="suffix">databaseBackup</data> <data key="type">Database</data> </entity> -</entities> + <entity name="WebSetupWizardBackup" type="backup"> + <data key="name">WebSetupWizard</data> + <data key="type">Database</data> + </entity> +</entities> \ No newline at end of file diff --git a/app/code/Magento/Backup/Test/Mftf/Test/AdminCreateAndDeleteBackupsTest.xml b/app/code/Magento/Backup/Test/Mftf/Test/AdminCreateAndDeleteBackupsTest.xml index b83434f7ad1d..26f8817c0a1b 100644 --- a/app/code/Magento/Backup/Test/Mftf/Test/AdminCreateAndDeleteBackupsTest.xml +++ b/app/code/Magento/Backup/Test/Mftf/Test/AdminCreateAndDeleteBackupsTest.xml @@ -17,7 +17,9 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-94176"/> <group value="backup"/> - <group value="skip"/> + <skip> + <issueId value="MC-5807"/> + </skip> </annotations> <!--Login to admin area--> diff --git a/app/code/Magento/Backup/Test/Unit/Cron/SystemBackupTest.php b/app/code/Magento/Backup/Test/Unit/Cron/SystemBackupTest.php index b7dfb30c0a1b..56a7ef42a0bc 100644 --- a/app/code/Magento/Backup/Test/Unit/Cron/SystemBackupTest.php +++ b/app/code/Magento/Backup/Test/Unit/Cron/SystemBackupTest.php @@ -3,134 +3,44 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Backup\Test\Unit\Cron; +use Magento\Backup\Cron\SystemBackup; +use PHPUnit\Framework\TestCase; +use Magento\Backup\Helper\Data as Helper; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -class SystemBackupTest extends \PHPUnit\Framework\TestCase +class SystemBackupTest extends TestCase { /** - * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager - */ - private $objectManager; - - /** - * @var \Magento\Backup\Cron\SystemBackup - */ - private $systemBackup; - - /** - * @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $scopeConfigMock; - - /** - * @var \Magento\Backup\Helper\Data|\PHPUnit_Framework_MockObject_MockObject - */ - private $backupDataMock; - - /** - * @var \Magento\Framework\Registry|\PHPUnit_Framework_MockObject_MockObject - */ - private $registryMock; - - /** - * @var \Psr\Log\LoggerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var Helper|\PHPUnit_Framework_MockObject_MockObject */ - private $loggerMock; + private $helperMock; /** - * Filesystem facade - * - * @var \Magento\Framework\Filesystem|\PHPUnit_Framework_MockObject_MockObject + * @var SystemBackup */ - private $filesystemMock; + private $cron; /** - * @var \Magento\Framework\Backup\Factory|\PHPUnit_Framework_MockObject_MockObject + * @inheritDoc */ - private $backupFactoryMock; - - /** - * @var \Magento\Framework\App\MaintenanceMode|\PHPUnit_Framework_MockObject_MockObject - */ - private $maintenanceModeMock; - - /** - * @var \Magento\Framework\Backup\Db|\PHPUnit_Framework_MockObject_MockObject - */ - private $backupDbMock; - - /** - * @var \Magento\Framework\ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $objectManagerMock; - protected function setUp() { - $this->objectManagerMock = $this->getMockBuilder(\Magento\Framework\ObjectManagerInterface::class) - ->getMock(); - $this->scopeConfigMock = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class) - ->getMock(); - $this->backupDataMock = $this->getMockBuilder(\Magento\Backup\Helper\Data::class) - ->disableOriginalConstructor() - ->getMock(); - $this->registryMock = $this->getMockBuilder(\Magento\Framework\Registry::class) - ->disableOriginalConstructor() - ->getMock(); - $this->loggerMock = $this->getMockBuilder(\Psr\Log\LoggerInterface::class) - ->getMock(); - $this->filesystemMock = $this->getMockBuilder(\Magento\Framework\Filesystem::class) - ->disableOriginalConstructor() - ->getMock(); - $this->backupFactoryMock = $this->getMockBuilder(\Magento\Framework\Backup\Factory::class) - ->disableOriginalConstructor() - ->getMock(); - $this->maintenanceModeMock = $this->getMockBuilder(\Magento\Framework\App\MaintenanceMode::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->backupDbMock = $this->getMockBuilder(\Magento\Framework\Backup\Db::class) - ->disableOriginalConstructor() - ->getMock(); - $this->backupDbMock->expects($this->any())->method('setBackupExtension')->willReturnSelf(); - $this->backupDbMock->expects($this->any())->method('setTime')->willReturnSelf(); - $this->backupDbMock->expects($this->any())->method('setBackupsDir')->willReturnSelf(); - - $this->objectManager = new ObjectManager($this); - $this->systemBackup = $this->objectManager->getObject( - \Magento\Backup\Cron\SystemBackup::class, - [ - 'backupData' => $this->backupDataMock, - 'coreRegistry' => $this->registryMock, - 'logger' => $this->loggerMock, - 'scopeConfig' => $this->scopeConfigMock, - 'filesystem' => $this->filesystemMock, - 'backupFactory' => $this->backupFactoryMock, - 'maintenanceMode' => $this->maintenanceModeMock, - ] - ); + $objectManager = new ObjectManager($this); + $this->helperMock = $this->getMockBuilder(Helper::class)->disableOriginalConstructor()->getMock(); + $this->cron = $objectManager->getObject(SystemBackup::class, ['backupData' => $this->helperMock]); } /** - * @expectedException \Exception + * Test that cron doesn't do anything if backups are disabled. */ - public function testExecuteThrowsException() + public function testDisabled() { - $type = 'db'; - $this->scopeConfigMock->expects($this->any())->method('isSetFlag')->willReturn(true); - - $this->scopeConfigMock->expects($this->once())->method('getValue') - ->with('system/backup/type', 'store') - ->willReturn($type); - - $this->backupFactoryMock->expects($this->once())->method('create')->willReturn($this->backupDbMock); - - $this->backupDbMock->expects($this->once())->method('create')->willThrowException(new \Exception); - - $this->backupDataMock->expects($this->never())->method('getCreateSuccessMessageByType')->with($type); - $this->loggerMock->expects($this->never())->method('info'); - - $this->systemBackup->execute(); + $this->helperMock->expects($this->any())->method('isEnabled')->willReturn(false); + $this->cron->execute(); } } diff --git a/app/code/Magento/Backup/Test/Unit/Model/DbTest.php b/app/code/Magento/Backup/Test/Unit/Model/DbTest.php deleted file mode 100644 index 0cab5f0ad1e9..000000000000 --- a/app/code/Magento/Backup/Test/Unit/Model/DbTest.php +++ /dev/null @@ -1,243 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Backup\Test\Unit\Model; - -use Magento\Backup\Model\Db; -use Magento\Backup\Model\ResourceModel\Db as DbResource; -use Magento\Framework\App\ResourceConnection; -use Magento\Framework\Backup\Db\BackupInterface; -use Magento\Framework\DataObject; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; - -class DbTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var ObjectManager - */ - private $objectManager; - - /** - * @var Db - */ - private $dbModel; - - /** - * @var DbResource|\PHPUnit_Framework_MockObject_MockObject - */ - private $dbResourceMock; - - /** - * @var ResourceConnection|\PHPUnit_Framework_MockObject_MockObject - */ - private $connectionResourceMock; - - protected function setUp() - { - $this->dbResourceMock = $this->getMockBuilder(DbResource::class) - ->disableOriginalConstructor() - ->getMock(); - $this->connectionResourceMock = $this->getMockBuilder(ResourceConnection::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->objectManager = new ObjectManager($this); - $this->dbModel = $this->objectManager->getObject( - Db::class, - [ - 'resourceDb' => $this->dbResourceMock, - 'resource' => $this->connectionResourceMock - ] - ); - } - - public function testGetResource() - { - self::assertEquals($this->dbResourceMock, $this->dbModel->getResource()); - } - - public function testGetTables() - { - $tables = []; - $this->dbResourceMock->expects($this->once()) - ->method('getTables') - ->willReturn($tables); - - self::assertEquals($tables, $this->dbModel->getTables()); - } - - public function testGetTableCreateScript() - { - $tableName = 'some_table'; - $script = 'script'; - $this->dbResourceMock->expects($this->once()) - ->method('getTableCreateScript') - ->with($tableName, false) - ->willReturn($script); - - self::assertEquals($script, $this->dbModel->getTableCreateScript($tableName, false)); - } - - public function testGetTableDataDump() - { - $tableName = 'some_table'; - $dump = 'dump'; - $this->dbResourceMock->expects($this->once()) - ->method('getTableDataDump') - ->with($tableName) - ->willReturn($dump); - - self::assertEquals($dump, $this->dbModel->getTableDataDump($tableName)); - } - - public function testGetHeader() - { - $header = 'header'; - $this->dbResourceMock->expects($this->once()) - ->method('getHeader') - ->willReturn($header); - - self::assertEquals($header, $this->dbModel->getHeader()); - } - - public function testGetFooter() - { - $footer = 'footer'; - $this->dbResourceMock->expects($this->once()) - ->method('getFooter') - ->willReturn($footer); - - self::assertEquals($footer, $this->dbModel->getFooter()); - } - - public function testRenderSql() - { - $header = 'header'; - $script = 'script'; - $tableName = 'some_table'; - $tables = [$tableName, $tableName]; - $dump = 'dump'; - $footer = 'footer'; - - $this->dbResourceMock->expects($this->once()) - ->method('getTables') - ->willReturn($tables); - $this->dbResourceMock->expects($this->once()) - ->method('getHeader') - ->willReturn($header); - $this->dbResourceMock->expects($this->exactly(2)) - ->method('getTableCreateScript') - ->with($tableName, true) - ->willReturn($script); - $this->dbResourceMock->expects($this->exactly(2)) - ->method('getTableDataDump') - ->with($tableName) - ->willReturn($dump); - $this->dbResourceMock->expects($this->once()) - ->method('getFooter') - ->willReturn($footer); - - self::assertEquals( - $header . $script . $dump . $script . $dump . $footer, - $this->dbModel->renderSql() - ); - } - - public function testCreateBackup() - { - /** @var BackupInterface|\PHPUnit_Framework_MockObject_MockObject $backupMock */ - $backupMock = $this->getMockBuilder(BackupInterface::class)->getMock(); - /** @var DataObject $tableStatus */ - $tableStatus = new DataObject(); - - $tableName = 'some_table'; - $tables = [$tableName]; - $header = 'header'; - $footer = 'footer'; - $dropSql = 'drop_sql'; - $createSql = 'create_sql'; - $beforeSql = 'before_sql'; - $afterSql = 'after_sql'; - $dataSql = 'data_sql'; - $foreignKeysSql = 'foreign_keys'; - $triggersSql = 'triggers_sql'; - $rowsCount = 2; - $dataLength = 1; - - $this->dbResourceMock->expects($this->once()) - ->method('beginTransaction'); - $this->dbResourceMock->expects($this->once()) - ->method('commitTransaction'); - $this->dbResourceMock->expects($this->once()) - ->method('getTables') - ->willReturn($tables); - $this->dbResourceMock->expects($this->once()) - ->method('getTableDropSql') - ->willReturn($dropSql); - $this->dbResourceMock->expects($this->once()) - ->method('getTableCreateSql') - ->with($tableName, false) - ->willReturn($createSql); - $this->dbResourceMock->expects($this->once()) - ->method('getTableDataBeforeSql') - ->with($tableName) - ->willReturn($beforeSql); - $this->dbResourceMock->expects($this->once()) - ->method('getTableDataAfterSql') - ->with($tableName) - ->willReturn($afterSql); - $this->dbResourceMock->expects($this->once()) - ->method('getTableDataSql') - ->with($tableName, $rowsCount, 0) - ->willReturn($dataSql); - $this->dbResourceMock->expects($this->once()) - ->method('getTableStatus') - ->with($tableName) - ->willReturn($tableStatus); - $this->dbResourceMock->expects($this->once()) - ->method('getTables') - ->willReturn($createSql); - $this->dbResourceMock->expects($this->once()) - ->method('getHeader') - ->willReturn($header); - $this->dbResourceMock->expects($this->once()) - ->method('getTableHeader') - ->willReturn($header); - $this->dbResourceMock->expects($this->once()) - ->method('getFooter') - ->willReturn($footer); - $this->dbResourceMock->expects($this->once()) - ->method('getTableForeignKeysSql') - ->willReturn($foreignKeysSql); - $this->dbResourceMock->expects($this->once()) - ->method('getTableTriggersSql') - ->willReturn($triggersSql); - $backupMock->expects($this->once()) - ->method('open'); - $backupMock->expects($this->once()) - ->method('close'); - - $tableStatus->setRows($rowsCount); - $tableStatus->setDataLength($dataLength); - - $backupMock->expects($this->any()) - ->method('write') - ->withConsecutive( - [$this->equalTo($header)], - [$this->equalTo($header . $dropSql . "\n")], - [$this->equalTo($createSql . "\n")], - [$this->equalTo($beforeSql)], - [$this->equalTo($dataSql)], - [$this->equalTo($afterSql)], - [$this->equalTo($foreignKeysSql)], - [$this->equalTo($triggersSql)], - [$this->equalTo($footer)] - ); - - $this->dbModel->createBackup($backupMock); - } -} diff --git a/app/code/Magento/Backup/etc/adminhtml/system.xml b/app/code/Magento/Backup/etc/adminhtml/system.xml index 4028452d0443..aa6635b4dde4 100644 --- a/app/code/Magento/Backup/etc/adminhtml/system.xml +++ b/app/code/Magento/Backup/etc/adminhtml/system.xml @@ -9,15 +9,24 @@ <system> <section id="system"> <group id="backup" translate="label" type="text" sortOrder="500" showInDefault="1" showInWebsite="0" showInStore="0"> - <label>Scheduled Backup Settings</label> + <label>Backup Settings</label> + <field id="functionality_enabled" translate="label" type="select" sortOrder="5" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Enable Backup</label> + <comment>Disabled by default for security reasons</comment> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + </field> <field id="enabled" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0"> <label>Enable Scheduled Backup</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <depends> + <field id="functionality_enabled">1</field> + </depends> </field> <field id="type" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="0" showInStore="0"> - <label>Backup Type</label> + <label>Scheduled Backup Type</label> <depends> <field id="enabled">1</field> + <field id="functionality_enabled">1</field> </depends> <source_model>Magento\Backup\Model\Config\Source\Type</source_model> </field> @@ -25,12 +34,14 @@ <label>Start Time</label> <depends> <field id="enabled">1</field> + <field id="functionality_enabled">1</field> </depends> </field> <field id="frequency" translate="label" type="select" sortOrder="40" showInDefault="1" showInWebsite="0" showInStore="0"> <label>Frequency</label> <depends> <field id="enabled">1</field> + <field id="functionality_enabled">1</field> </depends> <source_model>Magento\Cron\Model\Config\Source\Frequency</source_model> <backend_model>Magento\Backup\Model\Config\Backend\Cron</backend_model> @@ -40,6 +51,7 @@ <comment>Please put your store into maintenance mode during backup.</comment> <depends> <field id="enabled">1</field> + <field id="functionality_enabled">1</field> </depends> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> diff --git a/app/code/Magento/Backup/etc/config.xml b/app/code/Magento/Backup/etc/config.xml new file mode 100644 index 000000000000..fb0808983b9c --- /dev/null +++ b/app/code/Magento/Backup/etc/config.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:module:Magento_Store:etc/config.xsd"> + <default> + <system> + <backup> + <functionality_enabled>0</functionality_enabled> + </backup> + </system> + </default> +</config> diff --git a/app/code/Magento/Backup/view/adminhtml/layout/backup_index_block.xml b/app/code/Magento/Backup/view/adminhtml/layout/backup_index_block.xml index e17618b97e21..e3e984d933f2 100644 --- a/app/code/Magento/Backup/view/adminhtml/layout/backup_index_block.xml +++ b/app/code/Magento/Backup/view/adminhtml/layout/backup_index_block.xml @@ -11,7 +11,7 @@ <block class="Magento\Backend\Block\Widget\Grid" name="adminhtml.system.backup.grid" as="grid"> <arguments> <argument name="id" xsi:type="string">backupsGrid</argument> - <argument name="dataSource" xsi:type="object">Magento\Backup\Model\Fs\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Backup\Model\Fs\Collection</argument> <argument name="default_sort" xsi:type="string">time</argument> <argument name="default_dir" xsi:type="string">desc</argument> </arguments> diff --git a/app/code/Magento/Backup/view/adminhtml/layout/backup_index_disabled.xml b/app/code/Magento/Backup/view/adminhtml/layout/backup_index_disabled.xml new file mode 100644 index 000000000000..3470f528e5ce --- /dev/null +++ b/app/code/Magento/Backup/view/adminhtml/layout/backup_index_disabled.xml @@ -0,0 +1,17 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <head> + <title>Backup functionality is disabled + + + + + + + diff --git a/app/code/Magento/Backup/view/adminhtml/templates/backup/disabled.phtml b/app/code/Magento/Backup/view/adminhtml/templates/backup/disabled.phtml new file mode 100644 index 000000000000..a5308dce5cc5 --- /dev/null +++ b/app/code/Magento/Backup/view/adminhtml/templates/backup/disabled.phtml @@ -0,0 +1,7 @@ + +

Backup functionality is currently disabled. Please use other means for backups

diff --git a/app/code/Magento/Braintree/Controller/Paypal/PlaceOrder.php b/app/code/Magento/Braintree/Controller/Paypal/PlaceOrder.php index 1acd16708ff4..ea8a44a1122b 100644 --- a/app/code/Magento/Braintree/Controller/Paypal/PlaceOrder.php +++ b/app/code/Magento/Braintree/Controller/Paypal/PlaceOrder.php @@ -9,6 +9,7 @@ use Magento\Braintree\Model\Paypal\Helper; use Magento\Checkout\Model\Session; use Magento\Framework\App\Action\Context; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Exception\LocalizedException; @@ -17,7 +18,7 @@ /** * Class PlaceOrder */ -class PlaceOrder extends AbstractAction +class PlaceOrder extends AbstractAction implements HttpPostActionInterface { /** * @var Helper\OrderPlace @@ -54,6 +55,7 @@ public function __construct( /** * @inheritdoc + * * @throws LocalizedException */ public function execute() @@ -71,7 +73,10 @@ public function execute() return $resultRedirect->setPath('checkout/onepage/success', ['_secure' => true]); } catch (\Exception $e) { $this->logger->critical($e); - $this->messageManager->addExceptionMessage($e, $e->getMessage()); + $this->messageManager->addExceptionMessage( + $e, + __('The order #%1 cannot be processed.', $quote->getReservedOrderId()) + ); } return $resultRedirect->setPath('checkout/cart', ['_secure' => true]); diff --git a/app/code/Magento/Braintree/Controller/Paypal/Review.php b/app/code/Magento/Braintree/Controller/Paypal/Review.php index ca252aabe54a..2923db6fa88c 100644 --- a/app/code/Magento/Braintree/Controller/Paypal/Review.php +++ b/app/code/Magento/Braintree/Controller/Paypal/Review.php @@ -12,17 +12,25 @@ use Magento\Braintree\Gateway\Config\PayPal\Config; use Magento\Braintree\Model\Paypal\Helper\QuoteUpdater; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Payment\Model\Method\Logger; /** * Class Review */ -class Review extends AbstractAction +class Review extends AbstractAction implements HttpPostActionInterface, HttpGetActionInterface { /** * @var QuoteUpdater */ private $quoteUpdater; + /** + * @var Logger + */ + private $logger; + /** * @var string */ @@ -35,15 +43,18 @@ class Review extends AbstractAction * @param Config $config * @param Session $checkoutSession * @param QuoteUpdater $quoteUpdater + * @param Logger $logger */ public function __construct( Context $context, Config $config, Session $checkoutSession, - QuoteUpdater $quoteUpdater + QuoteUpdater $quoteUpdater, + Logger $logger ) { parent::__construct($context, $config, $checkoutSession); $this->quoteUpdater = $quoteUpdater; + $this->logger = $logger; } /** @@ -55,12 +66,13 @@ public function execute() $this->getRequest()->getPostValue('result', '{}'), true ); + $this->logger->debug($requestData); $quote = $this->checkoutSession->getQuote(); try { $this->validateQuote($quote); - if ($this->validateRequestData($requestData)) { + if ($requestData && $this->validateRequestData($requestData)) { $this->quoteUpdater->execute( $requestData['nonce'], $requestData['details'], @@ -91,6 +103,8 @@ public function execute() } /** + * Validate request data + * * @param array $requestData * @return boolean */ diff --git a/app/code/Magento/Braintree/Gateway/Request/PayPal/VaultDataBuilder.php b/app/code/Magento/Braintree/Gateway/Request/PayPal/VaultDataBuilder.php index a035c84b4caf..4d63ee4125b7 100644 --- a/app/code/Magento/Braintree/Gateway/Request/PayPal/VaultDataBuilder.php +++ b/app/code/Magento/Braintree/Gateway/Request/PayPal/VaultDataBuilder.php @@ -49,6 +49,8 @@ public function build(array $buildSubject) $payment = $paymentDO->getPayment(); $data = $payment->getAdditionalInformation(); + // the payment token could be stored only if a customer checks the Vault flow on storefront + // see https://developers.braintreepayments.com/guides/paypal/vault/javascript/v2#invoking-the-vault-flow if (!empty($data[VaultConfigProvider::IS_ACTIVE_CODE])) { $result[self::$optionsKey] = [ self::$storeInVaultOnSuccess => true diff --git a/app/code/Magento/Braintree/Gateway/Request/VaultCaptureDataBuilder.php b/app/code/Magento/Braintree/Gateway/Request/VaultCaptureDataBuilder.php index 4280663178ef..950634ba2d9e 100644 --- a/app/code/Magento/Braintree/Gateway/Request/VaultCaptureDataBuilder.php +++ b/app/code/Magento/Braintree/Gateway/Request/VaultCaptureDataBuilder.php @@ -6,6 +6,8 @@ namespace Magento\Braintree\Gateway\Request; use Magento\Braintree\Gateway\SubjectReader; +use Magento\Framework\Exception\LocalizedException; +use Magento\Payment\Gateway\Command\CommandException; use Magento\Payment\Gateway\Request\BuilderInterface; use Magento\Payment\Helper\Formatter; @@ -41,6 +43,9 @@ public function build(array $buildSubject) $payment = $paymentDO->getPayment(); $extensionAttributes = $payment->getExtensionAttributes(); $paymentToken = $extensionAttributes->getVaultPaymentToken(); + if ($paymentToken === null) { + throw new CommandException(__('The Payment Token is not available to perform the request.')); + } return [ 'amount' => $this->formatPrice($this->subjectReader->readAmount($buildSubject)), 'paymentMethodToken' => $paymentToken->getGatewayToken() diff --git a/app/code/Magento/Braintree/Model/Multishipping/PlaceOrder.php b/app/code/Magento/Braintree/Model/Multishipping/PlaceOrder.php new file mode 100644 index 000000000000..a6c1b088400a --- /dev/null +++ b/app/code/Magento/Braintree/Model/Multishipping/PlaceOrder.php @@ -0,0 +1,177 @@ +orderManagement = $orderManagement; + $this->paymentExtensionFactory = $paymentExtensionFactory; + $this->getPaymentNonceCommand = $getPaymentNonceCommand; + } + + /** + * @inheritdoc + */ + public function place(array $orderList): array + { + if (empty($orderList)) { + return []; + } + + $errorList = []; + $firstOrder = $this->orderManagement->place(array_shift($orderList)); + // get payment token from first placed order + $paymentToken = $this->getPaymentToken($firstOrder); + + foreach ($orderList as $order) { + try { + /** @var OrderInterface $order */ + $orderPayment = $order->getPayment(); + $this->setVaultPayment($orderPayment, $paymentToken); + $this->orderManagement->place($order); + } catch (\Exception $e) { + $incrementId = $order->getIncrementId(); + $errorList[$incrementId] = $e; + } + } + + return $errorList; + } + + /** + * Sets vault payment method. + * + * @param OrderPaymentInterface $orderPayment + * @param PaymentTokenInterface $paymentToken + * @return void + */ + private function setVaultPayment(OrderPaymentInterface $orderPayment, PaymentTokenInterface $paymentToken): void + { + $vaultMethod = $this->getVaultPaymentMethod( + $orderPayment->getMethod() + ); + $orderPayment->setMethod($vaultMethod); + + $publicHash = $paymentToken->getPublicHash(); + $customerId = $paymentToken->getCustomerId(); + $result = $this->getPaymentNonceCommand->execute( + ['public_hash' => $publicHash, 'customer_id' => $customerId] + ) + ->get(); + + $orderPayment->setAdditionalInformation( + DataAssignObserver::PAYMENT_METHOD_NONCE, + $result['paymentMethodNonce'] + ); + $orderPayment->setAdditionalInformation( + PaymentTokenInterface::PUBLIC_HASH, + $publicHash + ); + $orderPayment->setAdditionalInformation( + PaymentTokenInterface::CUSTOMER_ID, + $customerId + ); + } + + /** + * Returns vault payment method. + * + * For placing sequence of orders, we need to replace the original method on the vault method. + * + * @param string $method + * @return string + */ + private function getVaultPaymentMethod(string $method): string + { + $vaultPaymentMap = [ + ConfigProvider::CODE => ConfigProvider::CC_VAULT_CODE, + PaypalConfigProvider::PAYPAL_CODE => PaypalConfigProvider::PAYPAL_VAULT_CODE + ]; + + return $vaultPaymentMap[$method] ?? $method; + } + + /** + * Returns payment token. + * + * @param OrderInterface $order + * @return PaymentTokenInterface + * @throws \BadMethodCallException + */ + private function getPaymentToken(OrderInterface $order): PaymentTokenInterface + { + $orderPayment = $order->getPayment(); + $extensionAttributes = $this->getExtensionAttributes($orderPayment); + $paymentToken = $extensionAttributes->getVaultPaymentToken(); + + if ($paymentToken === null) { + throw new \BadMethodCallException('Vault Payment Token should be defined for placed order payment.'); + } + + return $paymentToken; + } + + /** + * Gets payment extension attributes. + * + * @param OrderPaymentInterface $payment + * @return OrderPaymentExtensionInterface + */ + private function getExtensionAttributes(OrderPaymentInterface $payment): OrderPaymentExtensionInterface + { + $extensionAttributes = $payment->getExtensionAttributes(); + if (null === $extensionAttributes) { + $extensionAttributes = $this->paymentExtensionFactory->create(); + $payment->setExtensionAttributes($extensionAttributes); + } + + return $extensionAttributes; + } +} diff --git a/app/code/Magento/Braintree/Model/Paypal/Helper/OrderPlace.php b/app/code/Magento/Braintree/Model/Paypal/Helper/OrderPlace.php index 6c4332ef22a4..314404c79939 100644 --- a/app/code/Magento/Braintree/Model/Paypal/Helper/OrderPlace.php +++ b/app/code/Magento/Braintree/Model/Paypal/Helper/OrderPlace.php @@ -6,14 +6,15 @@ namespace Magento\Braintree\Model\Paypal\Helper; -use Magento\Quote\Model\Quote; +use Magento\Braintree\Model\Paypal\OrderCancellationService; +use Magento\Checkout\Api\AgreementsValidatorInterface; use Magento\Checkout\Helper\Data; +use Magento\Checkout\Model\Type\Onepage; use Magento\Customer\Model\Group; use Magento\Customer\Model\Session; -use Magento\Checkout\Model\Type\Onepage; -use Magento\Quote\Api\CartManagementInterface; use Magento\Framework\Exception\LocalizedException; -use Magento\Checkout\Api\AgreementsValidatorInterface; +use Magento\Quote\Api\CartManagementInterface; +use Magento\Quote\Model\Quote; /** * Class OrderPlace @@ -42,23 +43,29 @@ class OrderPlace extends AbstractHelper private $checkoutHelper; /** - * Constructor - * + * @var OrderCancellationService + */ + private $orderCancellationService; + + /** * @param CartManagementInterface $cartManagement * @param AgreementsValidatorInterface $agreementsValidator * @param Session $customerSession * @param Data $checkoutHelper + * @param OrderCancellationService $orderCancellationService */ public function __construct( CartManagementInterface $cartManagement, AgreementsValidatorInterface $agreementsValidator, Session $customerSession, - Data $checkoutHelper + Data $checkoutHelper, + OrderCancellationService $orderCancellationService ) { $this->cartManagement = $cartManagement; $this->agreementsValidator = $agreementsValidator; $this->customerSession = $customerSession; $this->checkoutHelper = $checkoutHelper; + $this->orderCancellationService = $orderCancellationService; } /** @@ -67,7 +74,7 @@ public function __construct( * @param Quote $quote * @param array $agreement * @return void - * @throws LocalizedException + * @throws \Exception */ public function execute(Quote $quote, array $agreement) { @@ -84,7 +91,12 @@ public function execute(Quote $quote, array $agreement) $this->disabledQuoteAddressValidation($quote); $quote->collectTotals(); - $this->cartManagement->placeOrder($quote->getId()); + try { + $this->cartManagement->placeOrder($quote->getId()); + } catch (\Exception $e) { + $this->orderCancellationService->execute($quote->getReservedOrderId()); + throw $e; + } } /** diff --git a/app/code/Magento/Braintree/Model/Paypal/Helper/QuoteUpdater.php b/app/code/Magento/Braintree/Model/Paypal/Helper/QuoteUpdater.php index fe5895541543..ae2b1b142364 100644 --- a/app/code/Magento/Braintree/Model/Paypal/Helper/QuoteUpdater.php +++ b/app/code/Magento/Braintree/Model/Paypal/Helper/QuoteUpdater.php @@ -123,8 +123,8 @@ private function updateShippingAddress(Quote $quote, array $details) { $shippingAddress = $quote->getShippingAddress(); - $shippingAddress->setLastname($details['lastName']); - $shippingAddress->setFirstname($details['firstName']); + $shippingAddress->setLastname($this->getShippingRecipientLastName($details)); + $shippingAddress->setFirstname($this->getShippingRecipientFirstName($details)); $shippingAddress->setEmail($details['email']); $shippingAddress->setCollectShippingRates(true); @@ -148,7 +148,7 @@ private function updateBillingAddress(Quote $quote, array $details) { $billingAddress = $quote->getBillingAddress(); - if ($this->config->isRequiredBillingAddress()) { + if ($this->config->isRequiredBillingAddress() && !empty($details['billingAddress'])) { $this->updateAddressData($billingAddress, $details['billingAddress']); } else { $this->updateAddressData($billingAddress, $details['shippingAddress']); @@ -188,4 +188,30 @@ private function updateAddressData(Address $address, array $addressData) $address->setSameAsBilling(false); $address->setCustomerAddressId(null); } + + /** + * Returns shipping recipient first name. + * + * @param array $details + * @return string + */ + private function getShippingRecipientFirstName(array $details) + { + return isset($details['shippingAddress']['recipientName']) + ? explode(' ', $details['shippingAddress']['recipientName'], 2)[0] + : $details['firstName']; + } + + /** + * Returns shipping recipient last name. + * + * @param array $details + * @return string + */ + private function getShippingRecipientLastName(array $details) + { + return isset($details['shippingAddress']['recipientName']) + ? explode(' ', $details['shippingAddress']['recipientName'], 2)[1] + : $details['lastName']; + } } diff --git a/app/code/Magento/Braintree/Model/Paypal/OrderCancellationService.php b/app/code/Magento/Braintree/Model/Paypal/OrderCancellationService.php new file mode 100644 index 000000000000..29757e35ea6f --- /dev/null +++ b/app/code/Magento/Braintree/Model/Paypal/OrderCancellationService.php @@ -0,0 +1,77 @@ +searchCriteriaBuilder = $searchCriteriaBuilder; + $this->orderRepository = $orderRepository; + } + + /** + * Cancels an order and authorization transaction. + * + * @param string $incrementId + * @return bool + */ + public function execute(string $incrementId): bool + { + $order = $this->getOrder($incrementId); + if ($order === null) { + return false; + } + + // `\Magento\Sales\Model\Service\OrderService::cancel` cannot be used for cancellation as the service uses + // the order repository with outdated payment method instance (ex. contains Vault instead of Braintree) + $order->cancel(); + $this->orderRepository->save($order); + return true; + } + + /** + * Gets order by increment ID. + * + * @param string $incrementId + * @return OrderInterface|null + */ + private function getOrder(string $incrementId) + { + $searchCriteria = $this->searchCriteriaBuilder->addFilter(OrderInterface::INCREMENT_ID, $incrementId) + ->create(); + + $items = $this->orderRepository->getList($searchCriteria) + ->getItems(); + + return array_pop($items); + } +} diff --git a/app/code/Magento/Braintree/Model/Ui/PayPal/ConfigProvider.php b/app/code/Magento/Braintree/Model/Ui/PayPal/ConfigProvider.php index e06b913db8ef..e6c5ee22c62b 100644 --- a/app/code/Magento/Braintree/Model/Ui/PayPal/ConfigProvider.php +++ b/app/code/Magento/Braintree/Model/Ui/PayPal/ConfigProvider.php @@ -47,6 +47,8 @@ public function __construct(Config $config, ResolverInterface $resolver) */ public function getConfig() { + $requireBillingAddressAll = \Magento\Paypal\Model\Config::REQUIRE_BILLING_ADDRESS_ALL; + return [ 'payment' => [ self::PAYPAL_CODE => [ @@ -60,6 +62,8 @@ public function getConfig() 'vaultCode' => self::PAYPAL_VAULT_CODE, 'skipOrderReview' => $this->config->isSkipOrderReview(), 'paymentIcon' => $this->config->getPayPalIcon(), + 'isRequiredBillingAddress' => + (int)$this->config->isRequiredBillingAddress() === $requireBillingAddressAll ] ] ]; diff --git a/app/code/Magento/Braintree/Plugin/OrderCancellation.php b/app/code/Magento/Braintree/Plugin/OrderCancellation.php new file mode 100644 index 000000000000..90c72839d977 --- /dev/null +++ b/app/code/Magento/Braintree/Plugin/OrderCancellation.php @@ -0,0 +1,81 @@ +orderCancellationService = $orderCancellationService; + $this->quoteRepository = $quoteRepository; + } + + /** + * Cancels an order if an exception occurs during the order creation. + * + * @param CartManagementInterface $subject + * @param \Closure $proceed + * @param int $cartId + * @param PaymentInterface $payment + * @return int + * @throws \Exception + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function aroundPlaceOrder( + CartManagementInterface $subject, + \Closure $proceed, + $cartId, + PaymentInterface $payment = null + ) { + try { + return $proceed($cartId, $payment); + } catch (\Exception $e) { + $quote = $this->quoteRepository->get((int) $cartId); + $payment = $quote->getPayment(); + $paymentCodes = [ + ConfigProvider::CODE, + ConfigProvider::CC_VAULT_CODE, + PayPalConfigProvider::PAYPAL_CODE, + PayPalConfigProvider::PAYPAL_VAULT_CODE + ]; + if (in_array($payment->getMethod(), $paymentCodes)) { + $incrementId = $quote->getReservedOrderId(); + $this->orderCancellationService->execute($incrementId); + } + + throw $e; + } + } +} diff --git a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/AdminOrderBraintreeFillActionGroup.xml b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/AdminOrderBraintreeFillActionGroup.xml new file mode 100644 index 000000000000..ce1d0a9aecc9 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/AdminOrderBraintreeFillActionGroup.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/AdminRoleActionGroup.xml b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/AdminRoleActionGroup.xml index e86d5403e11e..09ac0b77f861 100644 --- a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/AdminRoleActionGroup.xml +++ b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/AdminRoleActionGroup.xml @@ -5,9 +5,9 @@ * See COPYING.txt for license details. */ --> - + @@ -16,7 +16,7 @@ - + @@ -34,7 +34,6 @@ - @@ -48,4 +47,4 @@ - \ No newline at end of file + diff --git a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/AdminUserActionGroup.xml b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/AdminUserActionGroup.xml index 23c322083773..3f8bdaa4cd6b 100644 --- a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/AdminUserActionGroup.xml +++ b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/AdminUserActionGroup.xml @@ -5,9 +5,9 @@ * See COPYING.txt for license details. */ --> - + @@ -39,9 +39,8 @@ - - + diff --git a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/ConfigureBraintreeActionGroup.xml b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/ConfigureBraintreeActionGroup.xml index 27e2039fe526..cbb065704fbc 100644 --- a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/ConfigureBraintreeActionGroup.xml +++ b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/ConfigureBraintreeActionGroup.xml @@ -6,13 +6,13 @@ */ --> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - + @@ -46,4 +46,8 @@ + + + + \ No newline at end of file diff --git a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/CreateNewOrderActionGroup.xml b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/CreateNewOrderActionGroup.xml deleted file mode 100644 index ee7158c2b63f..000000000000 --- a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/CreateNewOrderActionGroup.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/DeleteCustomerActionGroup.xml b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/DeleteCustomerActionGroup.xml deleted file mode 100644 index 65eddc0d9e51..000000000000 --- a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/DeleteCustomerActionGroup.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/StorefrontFillCartDataActionGroup.xml b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/StorefrontFillCartDataActionGroup.xml new file mode 100644 index 000000000000..bf06bc7df520 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/StorefrontFillCartDataActionGroup.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/code/Magento/Braintree/Test/Mftf/Data/BraintreeData.xml b/app/code/Magento/Braintree/Test/Mftf/Data/BraintreeData.xml index 8e55362a6cba..f00e3fa286b0 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Data/BraintreeData.xml +++ b/app/code/Magento/Braintree/Test/Mftf/Data/BraintreeData.xml @@ -35,6 +35,34 @@ somePrivateKey + + BraintreeTitle + PaymentAction + Environment + MerchantId + PublicKey + PrivateKey + Status + + + Credit Card (Braintree) + + + authorize + + + sandbox + + + d4pdjhxgjfrsmzbf + + + m7q4wmh43xrgyrst + + + 67de364080b1b4e2492d7a3de413a572 + + DefaultTitle @@ -63,6 +91,28 @@ + + BraintreeValuteActive + EnableSolution + + + 1 + + + 1 + + + + DefaultBraintreeValuteActive + DefaultEnableSolution + + + 0 + + + 0 + + Website new_website @@ -79,7 +129,7 @@ John Smith admin123 - mail@mail.com + mail@mail.com diff --git a/app/code/Magento/Braintree/Test/Mftf/Data/NewCustomerData.xml b/app/code/Magento/Braintree/Test/Mftf/Data/NewCustomerData.xml deleted file mode 100644 index 772c1c39a04c..000000000000 --- a/app/code/Magento/Braintree/Test/Mftf/Data/NewCustomerData.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - Abgar - Abgaryan - m@m.com - Abgar - Abgaryan - Street - Yerevan - 9999 - 9999 - - - diff --git a/app/code/Magento/Braintree/Test/Mftf/Data/NewProductData.xml b/app/code/Magento/Braintree/Test/Mftf/Data/NewProductData.xml deleted file mode 100644 index 72661ae94076..000000000000 --- a/app/code/Magento/Braintree/Test/Mftf/Data/NewProductData.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - ProductTest - 100 - 100 - - - diff --git a/app/code/Magento/Braintree/Test/Mftf/Metadata/braintree_config-meta.xml b/app/code/Magento/Braintree/Test/Mftf/Metadata/braintree_config-meta.xml index 83018852bfeb..5e3b870d65c6 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Metadata/braintree_config-meta.xml +++ b/app/code/Magento/Braintree/Test/Mftf/Metadata/braintree_config-meta.xml @@ -42,4 +42,22 @@ + + + + + + + + integer + + + string + + + + + + + diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/AdminDeleteRoleSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/AdminDeleteRoleSection.xml deleted file mode 100644 index 220c9a444b02..000000000000 --- a/app/code/Magento/Braintree/Test/Mftf/Section/AdminDeleteRoleSection.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - -
- - - - -
-
\ No newline at end of file diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/AdminDeleteUserSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/AdminDeleteUserSection.xml deleted file mode 100644 index bf2e2b44eb60..000000000000 --- a/app/code/Magento/Braintree/Test/Mftf/Section/AdminDeleteUserSection.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - -
- - - - -
-
\ No newline at end of file diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/AdminEditRoleInfoSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/AdminEditRoleInfoSection.xml index e37ce8f4738b..a34cdf15e7ad 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Section/AdminEditRoleInfoSection.xml +++ b/app/code/Magento/Braintree/Test/Mftf/Section/AdminEditRoleInfoSection.xml @@ -5,7 +5,9 @@ * See COPYING.txt for license details. */ --> - + +
@@ -18,4 +20,4 @@
-
\ No newline at end of file +
diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/AdminEditUserRoleSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/AdminEditUserRoleSection.xml index e999413c96d7..216292b81162 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Section/AdminEditUserRoleSection.xml +++ b/app/code/Magento/Braintree/Test/Mftf/Section/AdminEditUserRoleSection.xml @@ -5,7 +5,9 @@ * See COPYING.txt for license details. */ --> - + +
@@ -14,4 +16,4 @@
-
\ No newline at end of file +
diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/AdminEditUserSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/AdminEditUserSection.xml index 2e5fcfb7b5c8..cee262864d8c 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Section/AdminEditUserSection.xml +++ b/app/code/Magento/Braintree/Test/Mftf/Section/AdminEditUserSection.xml @@ -5,7 +5,9 @@ * See COPYING.txt for license details. */ --> - + +
@@ -25,4 +27,4 @@
-
\ No newline at end of file +
diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/AdminMenuSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/AdminMenuSection.xml index 660c7393b406..24e5efdc610f 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Section/AdminMenuSection.xml +++ b/app/code/Magento/Braintree/Test/Mftf/Section/AdminMenuSection.xml @@ -7,13 +7,13 @@ --> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd">
- + diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/AdminRoleGridSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/AdminRoleGridSection.xml index 63cbadc71d3d..1cf54bf94e77 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Section/AdminRoleGridSection.xml +++ b/app/code/Magento/Braintree/Test/Mftf/Section/AdminRoleGridSection.xml @@ -5,7 +5,9 @@ * See COPYING.txt for license details. */ --> - + +
@@ -14,4 +16,4 @@
-
\ No newline at end of file +
diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/AdminUserGridSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/AdminUserGridSection.xml deleted file mode 100644 index 9564bc61f799..000000000000 --- a/app/code/Magento/Braintree/Test/Mftf/Section/AdminUserGridSection.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - -
- - - - - - -
-
\ No newline at end of file diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/BraintreeConfiguraionSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/BraintreeConfiguraionSection.xml index 016af2e10274..f8802e9a34ae 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Section/BraintreeConfiguraionSection.xml +++ b/app/code/Magento/Braintree/Test/Mftf/Section/BraintreeConfiguraionSection.xml @@ -5,7 +5,9 @@ * See COPYING.txt for license details. */ --> - + +
@@ -29,6 +31,5 @@ -
-
\ No newline at end of file +
diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/BraintreeConfigurationPaymentSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/BraintreeConfigurationPaymentSection.xml new file mode 100644 index 000000000000..d9f2b14a40e2 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Section/BraintreeConfigurationPaymentSection.xml @@ -0,0 +1,24 @@ + + + + +
+ + + + + + + + + + + +
+
diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/CatalogSubmenuSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/CatalogSubmenuSection.xml deleted file mode 100644 index 32f02a69f817..000000000000 --- a/app/code/Magento/Braintree/Test/Mftf/Section/CatalogSubmenuSection.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - -
- -
-
diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/ConfigurationListSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/ConfigurationListSection.xml deleted file mode 100644 index 100407438eaa..000000000000 --- a/app/code/Magento/Braintree/Test/Mftf/Section/ConfigurationListSection.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - -
- - -
-
diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/ConfigurationPaymentSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/ConfigurationPaymentSection.xml index 885a45be721f..2192dd935c33 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Section/ConfigurationPaymentSection.xml +++ b/app/code/Magento/Braintree/Test/Mftf/Section/ConfigurationPaymentSection.xml @@ -7,7 +7,7 @@ --> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd">
diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/CustomersPageSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/CustomersPageSection.xml deleted file mode 100644 index e4a75b1b6a84..000000000000 --- a/app/code/Magento/Braintree/Test/Mftf/Section/CustomersPageSection.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - -
- - - - - - -
-
diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/CustomersSubmenuSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/CustomersSubmenuSection.xml deleted file mode 100644 index 937afb83da96..000000000000 --- a/app/code/Magento/Braintree/Test/Mftf/Section/CustomersSubmenuSection.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - -
- -
-
diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/StoresSubmenuSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/StoresSubmenuSection.xml index f094baa9f344..806762f82646 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Section/StoresSubmenuSection.xml +++ b/app/code/Magento/Braintree/Test/Mftf/Section/StoresSubmenuSection.xml @@ -7,7 +7,7 @@ --> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd">
diff --git a/app/code/Magento/Braintree/Test/Mftf/Test/BraintreeCreditCardOnCheckoutTest.xml b/app/code/Magento/Braintree/Test/Mftf/Test/BraintreeCreditCardOnCheckoutTest.xml new file mode 100644 index 000000000000..a781841e0a77 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Test/BraintreeCreditCardOnCheckoutTest.xml @@ -0,0 +1,106 @@ + + + + + + + + + + <description value="Use saved for Braintree credit card on checkout with selecting billing address"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-93767"/> + <group value="braintree"/> + </annotations> + + <before> + <createData entity="_defaultCategory" stepKey="category"/> + <createData entity="SimpleProduct" stepKey="product"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="customer"/> + <createData entity="BraintreeConfig" stepKey="BraintreeConfigurationData"/> + <createData entity="CustomBraintreeConfigurationData" stepKey="CustomBraintreeConfigurationData"/> + </before> + + <after> + <deleteData createDataKey="product" stepKey="deleteProduct1"/> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <createData entity="DefaultBraintreeConfig" stepKey="DefaultBraintreeConfig"/> + <createData entity="RollBackCustomBraintreeConfigurationData" stepKey="RollBackCustomBraintreeConfigurationData"/> + <actionGroup ref="StorefrontSignOutActionGroup" stepKey="StorefrontSignOutActionGroup"/> + </after> + <!--Go to storefront--> + <amOnPage url="" stepKey="DoToStorefront"/> + <!--Create account--> + <actionGroup ref="SignUpNewUserFromStorefrontActionGroup" stepKey="SignUpNewUserFromStorefrontActionGroup"> + <argument name="Customer" value="Simple_US_Customer"/> + </actionGroup> + + <!--Add product to cart--> + <amOnPage url="$$product.sku$$.html" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <!--Proceed to checkout--> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="GoToCheckoutFromMinicartActionGroup"/> + + <actionGroup ref="LoggedInCheckoutFillNewBillingAddressActionGroup" stepKey="LoggedInCheckoutFillNewBillingAddressActionGroup"> + <argument name="Address" value="US_Address_CA"/> + </actionGroup> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> + <!--Fill cart data--> + <click selector="{{BraintreeConfigurationPaymentSection.creditCart}}" stepKey="SelectBraintreePaymentMethod"/> + <waitForPageLoad stepKey="waitForPageLoad3"/> + <scrollTo selector="{{BraintreeConfigurationPaymentSection.creditCart}}" stepKey="ScrollToCreditCardSection"/> + <actionGroup ref="StorefrontFillCartDataActionGroup" stepKey="StorefrontFillCartDataActionGroup"/> + <waitForPageLoad stepKey="waitForPageLoad4"/> + <!--Place order--> + <click selector="{{BraintreeConfigurationPaymentSection.paymentMethodContainer}}{{CheckoutPaymentSection.placeOrder}}" + stepKey="PlaceOrder"/> + <waitForPageLoad stepKey="waitForPageLoad5"/> + + <!--Add product to cart again--> + <amOnPage url="$$product.sku$$.html" stepKey="goToProductPage1"/> + <waitForPageLoad stepKey="waitForPageLoad6"/> + <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart1"/> + <waitForPageLoad stepKey="waitForPageLoad7"/> + <!--Proceed to checkout--> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="GoToCheckoutFromMinicartActionGroup1"/> + <click selector="{{CheckoutPaymentSection.addressAction('New Address')}}" stepKey="clickOnNewAddress"/> + <waitForPageLoad stepKey="waitForPageLoad8"/> + <actionGroup ref="LoggedInCheckoutFillNewBillingAddressActionGroup" stepKey="LoggedInCheckoutFillNewBillingAddressActionGroup1"> + <argument name="Address" value="US_Address_NY"/> + </actionGroup> + <click selector="{{CheckoutPaymentSection.addressAction('Ship here')}}" stepKey="SaveAddress"/> + <waitForPageLoad stepKey="waitForPageLoad9"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext1"/> + <waitForPageLoad stepKey="waitForPageLoad10"/> + <click selector="{{BraintreeConfigurationPaymentSection.paymentMethod}}" stepKey="SelectBraintreePaymentMethod1"/> + <waitForPageLoad stepKey="waitForPageLoad11"/> + <!--Place order--> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="PlaceOrder1"/> + <waitForPageLoad stepKey="waitForPageLoad13"/> + + <click selector="{{CheckoutOrderSummarySection.orderNumber}}" stepKey="ClickOnOrderNumber"/> + <waitForPageLoad stepKey="waitForPageLoad14"/> + <!--Check billing and shipping addresses also additional Address info--> + <click selector="{{CheckoutPaymentSection.addressBook}}" stepKey="goToAddressBook"/> + <grabTextFrom selector="{{CheckoutOrderSummarySection.shippingAddress}}" stepKey="shippingAddr"/> + <grabTextFrom selector="{{CheckoutOrderSummarySection.billingAddress}}" stepKey="billingAddr"/> + <grabTextFrom selector="{{CheckoutOrderSummarySection.additionalAddress}}" stepKey="additionalAddress"/> + <see userInput="Shipping Address" stepKey="seeShippingAddress"/> + <see userInput="Billing Address" stepKey="seeBillingAddress"/> + <assertEquals stepKey="assertValuesAreEqual" actual="$billingAddr" expected="$shippingAddr"/> + <assertNotEquals stepKey="assertValuesAreNotEqual" actual="$billingAddr" expected="$additionalAddress"/> + </test> +</tests> diff --git a/app/code/Magento/Braintree/Test/Mftf/Test/CreateAnAdminOrderUsingBraintreePaymentTest1.xml b/app/code/Magento/Braintree/Test/Mftf/Test/CreateAnAdminOrderUsingBraintreePaymentTest1.xml index df2e98816f0d..244052371e70 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Test/CreateAnAdminOrderUsingBraintreePaymentTest1.xml +++ b/app/code/Magento/Braintree/Test/Mftf/Test/CreateAnAdminOrderUsingBraintreePaymentTest1.xml @@ -17,9 +17,6 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-93677"/> <group value="braintree"/> - <skip> - <issueId value="MQE-1187" /> - </skip> </annotations> @@ -28,11 +25,13 @@ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> <!--CreateNewProduct--> - <actionGroup ref="CreateNewProductActionGroup" stepKey="CreateNewProduct"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> <!--Create New Customer--> - <actionGroup ref="CreateCustomerActionGroup" stepKey="CreateCustomer"/> - + <createData stepKey="createCustomer" entity="Simple_US_Customer"/> </before> @@ -41,47 +40,70 @@ <!--Create New Role--> <actionGroup ref="GoToUserRoles" stepKey="GoToUserRoles"/> - <actionGroup ref="AdminCreateRole" stepKey="AdminCreateNewRole"/> + <waitForPageLoad stepKey="waitForAllRoles" time="15"/> + <actionGroup ref="AdminCreateNewRole" stepKey="AdminCreateNewRole"/> - <!--Create New User With Specific Role--> + <!--Create new admin user--> <actionGroup ref="GoToAllUsers" stepKey="GoToAllUsers"/> + <waitForPageLoad stepKey="waitForUsers" time="15"/> <actionGroup ref="AdminCreateUserAction" stepKey="AdminCreateNewUser"/> <!--SignOut--> - <actionGroup ref="SignOut" stepKey="signOutFromAdmin"/> + <actionGroup ref="logout" stepKey="signOutFromAdmin"/> - <!--SignIn New User--> + <!--Log in as new user--> <actionGroup ref="LoginNewUser" stepKey="signInNewUser"/> <waitForPageLoad stepKey="waitForLogin" time="3"/> <!--Create New Order--> - <actionGroup ref="CreateNewOrderActionGroup" stepKey="createNewOrder"/> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="navigateToNewOrder"> + <argument name="customer" value="Simple_US_Customer"/> + </actionGroup> + + <!--Add Product to Order--> + <actionGroup ref="addSimpleProductToOrder" stepKey="addProduct"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + + <!--Fill Order Customer Information--> + <actionGroup ref="fillOrderCustomerInformation" stepKey="fillCustomerAddress"> + <argument name="customer" value="Simple_US_Customer"/> + <argument name="address" value="US_Address_TX"/> + </actionGroup> + + <!--Select Shipping--> + <actionGroup ref="orderSelectFlatRateShipping" stepKey="selectFlatRateShipping"/> + <waitForPageLoad stepKey="waitForShippingToFinish"/> + <!--Pay with Braintree --> + <actionGroup ref="useBraintreeForMasterCard" stepKey="selectCardWithBraintree"/> + + <!--Submit Order--> + <click stepKey="submitOrder" selector="{{NewOrderSection.submitOrder}}"/> + <waitForPageLoad stepKey="waitForSaveConfig"/> + <waitForElementVisible selector="{{NewOrderSection.successMessage}}" stepKey="waitForSuccessMessage"/> <after> + <!-- Disable BrainTree --> + <actionGroup ref="DisableBrainTree" stepKey="disableBrainTree"/> + <!--SignOut--> <actionGroup ref="SignOut" stepKey="signOutFromNewUser"/> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> <!--Delete Product--> - <actionGroup ref="DeleteProductActionGroup" stepKey="DeleteAllProducts"> - <argument name="productName" value="NewProductData.ProductName"/> - </actionGroup> + <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> <!--Delete Customer--> - <actionGroup ref="DeleteCustomerActionGroup" stepKey="DeleteCustomer"> - <argument name="lastName" value="NewCustomerData.LastName"/> - </actionGroup> + <deleteData stepKey="deleteCustomer" createDataKey="createCustomer"/> <!--Delete User --> <actionGroup ref="GoToAllUsers" stepKey="GoBackToAllUsers"/> - <actionGroup ref="AdminDeleteUserActionGroup" stepKey="AdminDeleteUserActionGroup"/> + <actionGroup ref="AdminDeleteNewUserActionGroup" stepKey="AdminDeleteUserActionGroup"/> <!--Delete Role--> <actionGroup ref="GoToUserRoles" stepKey="GoBackToUserRoles"/> <actionGroup ref="AdminDeleteRoleActionGroup" stepKey="AdminDeleteRoleActionGroup"/> - </after> - </test> </tests> diff --git a/app/code/Magento/Braintree/Test/Mftf/Test/CretateAdminOrderWithOnlinePaymentIncludingTaxAndDiscount.xml b/app/code/Magento/Braintree/Test/Mftf/Test/CretateAdminOrderWithOnlinePaymentIncludingTaxAndDiscount.xml new file mode 100644 index 000000000000..cf51f29db79f --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Test/CretateAdminOrderWithOnlinePaymentIncludingTaxAndDiscount.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="CreateAdminOrderPayedWithOnlinePaymentIncludingTaxAndDiscount"> + <annotations> + <features value="Braintree"/> + <stories value="Get access to a New Credit Memo Page from Invoice for Order payed with online payment via Admin"/> + <title value="Admin should be able to open a New Credit Memo Page from Invoice Page for Order with tax and discount and payed using online payment method"/> + <description value="Admin should be able to open a New Credit Memo Page from Invoice Page for Order with tax and discount and payed using online payment method"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-94472"/> + <group value="braintree"/> + </annotations> + + <before> + <!--Create Default Category--> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + + <!--Create Simple product--> + <createData entity="_defaultProduct" stepKey="simpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!--Create Tax Rule is based on default tax rates (Stores>Tax Rule) US-CA-*-Rate 1 = 8.2500 US-NY-*-Rate 1 = 8.3750 --> + <createData entity="SimpleTaxRule" stepKey="createTaxRule"/> + + <!--Configure Braintree Payment method--> + <createData entity="BraintreeConfig" stepKey="BraintreeConfigurationData"/> + <createData entity="CustomBraintreeConfigurationData" stepKey="enableBraintree"/> + + <!--Create Retailer Customer with US_CA address--> + <createData entity="Simple_US_Customer_CA" stepKey="simpleCustomer"> + <field key="group_id">3</field> + </createData> + + <!--Login as Admin User--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <after> + <!--Delete Cart Price Rule--> + <actionGroup ref="AdminDeleteCartPriceRuleForRetailerActionGroup" stepKey="deleteSalesRule"/> + + <!--Set to default configuration Tax Shipping Class--> + <actionGroup ref="setDefaultShippingTaxClass" stepKey="setdefaultClass"/> + + <!--Delete Simple Sub Category--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!--Delete Simple Product--> + <deleteData createDataKey="simpleProduct" stepKey="deleteSimpleProduct"/> + + <!-- Delete Tax Rule --> + <deleteData createDataKey="createTaxRule" stepKey="deleteTaxRule"/> + + <!-- Rollback Braintree to Default --> + <createData entity="RollBackCustomBraintreeConfigurationData" stepKey="rollbackBraintreeConfig"/> + + <!--Delete Customer--> + <deleteData createDataKey="simpleCustomer" stepKey="deleteSimpleCustomer"/> + + <!--Log Out--> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Create a cart price rule with 10% discount for whole cart --> + <click selector="{{AdminMenuSection.marketing}}" stepKey="clickOnMarketing" /> + <waitForPageLoad stepKey="waitForMarketing"/> + <click selector="{{CartPriceRulesSubmenuSection.cartPriceRules}}" stepKey="clickOnCartPriceRules"/> + <waitForPageLoad stepKey="waitForCartPriceRules"/> + <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> + <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{SimpleSalesRule.name}}" stepKey="fillRuleName"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsites"/> + <actionGroup ref="selectRetailerCustomerGroup" stepKey="selectRetailerCustomerGroup"/> + <click selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="clickToExpandActions"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.apply}}" userInput="Percent of product price discount" stepKey="selectActionType"/> + <fillField selector="{{AdminCartPriceRulesFormSection.discountAmount}}" userInput="10" stepKey="fillDiscountAmount"/> + <click selector="{{AdminCartPriceRulesFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForCartRuleLoad"/> + <see selector="{{AdminCartPriceRulesSection.messages}}" userInput="You saved the rule." stepKey="seeSuccessMessage"/> + + <!--Set Taxable Goods for Shipping Tax Class--> + <actionGroup ref="changeShippingTaxClass" stepKey="changeShippingTaxClass"/> + + <!--Adding Special price to product--> + <amOnPage url="{{AdminProductEditPage.url($$simpleProduct.id$$)}}" stepKey="openAdminProductEditPage"/> + <actionGroup ref="AddSpecialPriceToProductActionGroup" stepKey="addSpecialPrice"/> + <actionGroup ref="saveProductForm" stepKey="saveProductForm"/> + + <!--Create New Order--> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="navigateToNewOrderWithExistingCustomer"> + <argument name="customer" value="$$simpleCustomer$$"/> + </actionGroup> + + <!--Add a product to order--> + <actionGroup ref="addSimpleProductToOrder" stepKey="addProductToOrder"> + <argument name="product" value="$$simpleProduct$$"/> + </actionGroup> + + <!--Select FlatRate shipping method--> + <actionGroup ref="orderSelectFlatRateShipping" stepKey="orderSelectFlatRateShippingMethod"/> + + <!--Select Braintree online Payment method --> + <actionGroup ref="AdminOrderBraintreeFillActionGroup" stepKey="selectCreditCardPayment"/> + + <!--Submit Order--> + <click stepKey="submitOrder" selector="{{NewOrderSection.submitOrder}}"/> + <waitForPageLoad stepKey="waitForSubmitOrder"/> + <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="You created the order." stepKey="seeOrderSuccessMessage" after="waitForSubmitOrder"/> + + <!-- Create New invoice--> + <actionGroup ref="adminFastCreateInvoice" stepKey="createInvoice"/> + + <!--Get access to Credit Memo page from Invoice page--> + <click selector="{{AdminInvoiceMainActionsSection.openNewCreditMemoFromInvoice}}" stepKey="clickCreateNewCreditMemo"/> + <waitForPageLoad stepKey="waitForLoadNewCreditMemoPage"/> + <see selector="{{AdminCreditMemoOrderInformationSection.orderStatus}}" userInput="Processing" stepKey="seeNewCreditMemo"/> + </test> +</tests> diff --git a/app/code/Magento/Braintree/Test/Unit/Controller/Paypal/PlaceOrderTest.php b/app/code/Magento/Braintree/Test/Unit/Controller/Paypal/PlaceOrderTest.php index 4bea03153b93..9c25846e56da 100644 --- a/app/code/Magento/Braintree/Test/Unit/Controller/Paypal/PlaceOrderTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Controller/Paypal/PlaceOrderTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Braintree\Test\Unit\Controller\Paypal; @@ -16,6 +17,8 @@ use Magento\Framework\Controller\ResultInterface; use Magento\Framework\Message\ManagerInterface; use Magento\Quote\Model\Quote; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; /** * Class PlaceOrderTest @@ -27,34 +30,34 @@ class PlaceOrderTest extends \PHPUnit\Framework\TestCase { /** - * @var OrderPlace|\PHPUnit_Framework_MockObject_MockObject + * @var OrderPlace|MockObject */ - private $orderPlaceMock; + private $orderPlace; /** - * @var Config|\PHPUnit_Framework_MockObject_MockObject + * @var Config|MockObject */ - private $configMock; + private $config; /** - * @var Session|\PHPUnit_Framework_MockObject_MockObject + * @var Session|MockObject */ - private $checkoutSessionMock; + private $checkoutSession; /** - * @var RequestInterface|\PHPUnit_Framework_MockObject_MockObject + * @var RequestInterface|MockObject */ - private $requestMock; + private $request; /** - * @var ResultFactory|\PHPUnit_Framework_MockObject_MockObject + * @var ResultFactory|MockObject */ - private $resultFactoryMock; + private $resultFactory; /** - * @var ManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var ManagerInterface|MockObject */ - protected $messageManagerMock; + private $messageManager; /** * @var PlaceOrder @@ -62,139 +65,143 @@ class PlaceOrderTest extends \PHPUnit\Framework\TestCase private $placeOrder; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var MockObject */ - private $loggerMock; + private $logger; + /** + * @inheritdoc + */ protected function setUp() { - /** @var Context|\PHPUnit_Framework_MockObject_MockObject $contextMock */ - $contextMock = $this->getMockBuilder(Context::class) + /** @var Context|MockObject $context */ + $context = $this->getMockBuilder(Context::class) ->disableOriginalConstructor() ->getMock(); - $this->requestMock = $this->getMockBuilder(RequestInterface::class) + $this->request = $this->getMockBuilder(RequestInterface::class) ->setMethods(['getPostValue']) ->getMockForAbstractClass(); - $this->resultFactoryMock = $this->getMockBuilder(ResultFactory::class) + $this->resultFactory = $this->getMockBuilder(ResultFactory::class) ->disableOriginalConstructor() ->getMock(); - $this->checkoutSessionMock = $this->getMockBuilder(Session::class) + $this->checkoutSession = $this->getMockBuilder(Session::class) ->disableOriginalConstructor() ->getMock(); - $this->configMock = $this->getMockBuilder(Config::class) + $this->config = $this->getMockBuilder(Config::class) ->disableOriginalConstructor() ->getMock(); - $this->orderPlaceMock = $this->getMockBuilder(OrderPlace::class) + $this->orderPlace = $this->getMockBuilder(OrderPlace::class) ->disableOriginalConstructor() ->getMock(); - $this->messageManagerMock = $this->getMockBuilder(ManagerInterface::class) + $this->messageManager = $this->getMockBuilder(ManagerInterface::class) ->getMockForAbstractClass(); - $contextMock->expects(self::once()) - ->method('getRequest') - ->willReturn($this->requestMock); - $contextMock->expects(self::once()) - ->method('getResultFactory') - ->willReturn($this->resultFactoryMock); - $contextMock->expects(self::once()) - ->method('getMessageManager') - ->willReturn($this->messageManagerMock); - - $this->loggerMock = $this->getMockBuilder(\Psr\Log\LoggerInterface::class) + $context->method('getRequest') + ->willReturn($this->request); + $context->method('getResultFactory') + ->willReturn($this->resultFactory); + $context->method('getMessageManager') + ->willReturn($this->messageManager); + + $this->logger = $this->getMockBuilder(LoggerInterface::class) ->disableOriginalConstructor() ->getMock(); $this->placeOrder = new PlaceOrder( - $contextMock, - $this->configMock, - $this->checkoutSessionMock, - $this->orderPlaceMock, - $this->loggerMock + $context, + $this->config, + $this->checkoutSession, + $this->orderPlace, + $this->logger ); } + /** + * Checks if an order is placed successfully. + * + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NotFoundException + */ public function testExecute() { $agreement = ['test-data']; $quoteMock = $this->getQuoteMock(); - $quoteMock->expects(self::once()) - ->method('getItemsCount') + $quoteMock->method('getItemsCount') ->willReturn(1); $resultMock = $this->getResultMock(); - $resultMock->expects(self::once()) - ->method('setPath') + $resultMock->method('setPath') ->with('checkout/onepage/success') ->willReturnSelf(); - $this->resultFactoryMock->expects(self::once()) - ->method('create') + $this->resultFactory->method('create') ->with(ResultFactory::TYPE_REDIRECT) ->willReturn($resultMock); - $this->requestMock->expects(self::once()) - ->method('getPostValue') + $this->request->method('getPostValue') ->with('agreement', []) ->willReturn($agreement); - $this->checkoutSessionMock->expects(self::once()) - ->method('getQuote') + $this->checkoutSession->method('getQuote') ->willReturn($quoteMock); - $this->orderPlaceMock->expects(self::once()) - ->method('execute') + $this->orderPlace->method('execute') ->with($quoteMock, [0]); - $this->messageManagerMock->expects(self::never()) + $this->messageManager->expects(self::never()) ->method('addExceptionMessage'); self::assertEquals($this->placeOrder->execute(), $resultMock); } + /** + * Checks a negative scenario during place order action. + * + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NotFoundException + */ public function testExecuteException() { $agreement = ['test-data']; $quote = $this->getQuoteMock(); - $quote->expects(self::once()) - ->method('getItemsCount') + $quote->method('getItemsCount') ->willReturn(0); + $quote->method('getReservedOrderId') + ->willReturn('000000111'); $resultMock = $this->getResultMock(); - $resultMock->expects(self::once()) - ->method('setPath') + $resultMock->method('setPath') ->with('checkout/cart') ->willReturnSelf(); - $this->resultFactoryMock->expects(self::once()) - ->method('create') + $this->resultFactory->method('create') ->with(ResultFactory::TYPE_REDIRECT) ->willReturn($resultMock); - $this->requestMock->expects(self::once()) - ->method('getPostValue') + $this->request->method('getPostValue') ->with('agreement', []) ->willReturn($agreement); - $this->checkoutSessionMock->expects(self::once()) - ->method('getQuote') + $this->checkoutSession->method('getQuote') ->willReturn($quote); - $this->orderPlaceMock->expects(self::never()) + $this->orderPlace->expects(self::never()) ->method('execute'); - $this->messageManagerMock->expects(self::once()) - ->method('addExceptionMessage') + $this->messageManager->method('addExceptionMessage') ->with( self::isInstanceOf('\InvalidArgumentException'), - 'Checkout failed to initialize. Verify and try again.' + 'The order #000000111 cannot be processed.' ); self::assertEquals($this->placeOrder->execute(), $resultMock); } /** - * @return ResultInterface|\PHPUnit_Framework_MockObject_MockObject + * Gets mock object for a result. + * + * @return ResultInterface|MockObject */ private function getResultMock() { @@ -204,7 +211,9 @@ private function getResultMock() } /** - * @return Quote|\PHPUnit_Framework_MockObject_MockObject + * Gets mock object for a quote. + * + * @return Quote|MockObject */ private function getQuoteMock() { diff --git a/app/code/Magento/Braintree/Test/Unit/Controller/Paypal/ReviewTest.php b/app/code/Magento/Braintree/Test/Unit/Controller/Paypal/ReviewTest.php index 609b7f21dbf8..d68838bafbf0 100644 --- a/app/code/Magento/Braintree/Test/Unit/Controller/Paypal/ReviewTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Controller/Paypal/ReviewTest.php @@ -6,6 +6,7 @@ namespace Magento\Braintree\Test\Unit\Controller\Paypal; +use Magento\Payment\Model\Method\Logger; use Magento\Quote\Model\Quote; use Magento\Framework\View\Layout; use Magento\Checkout\Model\Session; @@ -65,6 +66,11 @@ class ReviewTest extends \PHPUnit\Framework\TestCase */ private $review; + /** + * @var Logger|\PHPUnit_Framework_MockObject_MockObject + */ + private $loggerMock; + protected function setUp() { /** @var Context|\PHPUnit_Framework_MockObject_MockObject $contextMock */ @@ -88,6 +94,9 @@ protected function setUp() ->getMock(); $this->messageManagerMock = $this->getMockBuilder(ManagerInterface::class) ->getMockForAbstractClass(); + $this->loggerMock = $this->getMockBuilder(Logger::class) + ->disableOriginalConstructor() + ->getMock(); $contextMock->expects(self::once()) ->method('getRequest') @@ -103,7 +112,8 @@ protected function setUp() $contextMock, $this->configMock, $this->checkoutSessionMock, - $this->quoteUpdaterMock + $this->quoteUpdaterMock, + $this->loggerMock ); } diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/VaultCaptureDataBuilderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/VaultCaptureDataBuilderTest.php index 25ccd8b32d10..d4e1f2745e3f 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/VaultCaptureDataBuilderTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/VaultCaptureDataBuilderTest.php @@ -3,10 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Braintree\Test\Unit\Gateway\Request; -use Magento\Braintree\Gateway\SubjectReader; use Magento\Braintree\Gateway\Request\VaultCaptureDataBuilder; +use Magento\Braintree\Gateway\SubjectReader; use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; use Magento\Sales\Api\Data\OrderPaymentExtension; use Magento\Sales\Model\Order\Payment; @@ -26,47 +28,46 @@ class VaultCaptureDataBuilderTest extends \PHPUnit\Framework\TestCase /** * @var PaymentDataObjectInterface|MockObject */ - private $paymentDOMock; + private $paymentDO; /** * @var Payment|MockObject */ - private $paymentMock; + private $payment; /** - * @var SubjectReader|\PHPUnit_Framework_MockObject_MockObject + * @var SubjectReader|MockObject */ - private $subjectReaderMock; + private $subjectReader; /** * @inheritdoc */ - protected function setUp() + protected function setUp(): void { - $this->paymentDOMock = $this->createMock(PaymentDataObjectInterface::class); - $this->paymentMock = $this->getMockBuilder(Payment::class) + $this->paymentDO = $this->createMock(PaymentDataObjectInterface::class); + $this->payment = $this->getMockBuilder(Payment::class) ->disableOriginalConstructor() ->getMock(); - $this->paymentDOMock->expects(static::once()) - ->method('getPayment') - ->willReturn($this->paymentMock); + $this->paymentDO->method('getPayment') + ->willReturn($this->payment); - $this->subjectReaderMock = $this->getMockBuilder(SubjectReader::class) + $this->subjectReader = $this->getMockBuilder(SubjectReader::class) ->disableOriginalConstructor() ->getMock(); - $this->builder = new VaultCaptureDataBuilder($this->subjectReaderMock); + $this->builder = new VaultCaptureDataBuilder($this->subjectReader); } /** - * \Magento\Braintree\Gateway\Request\VaultCaptureDataBuilder::build + * Checks the result after builder execution. */ - public function testBuild() + public function testBuild(): void { $amount = 30.00; $token = '5tfm4c'; $buildSubject = [ - 'payment' => $this->paymentDOMock, + 'payment' => $this->paymentDO, 'amount' => $amount, ]; @@ -75,36 +76,68 @@ public function testBuild() 'paymentMethodToken' => $token, ]; - $this->subjectReaderMock->expects(self::once()) - ->method('readPayment') + $this->subjectReader->method('readPayment') ->with($buildSubject) - ->willReturn($this->paymentDOMock); - $this->subjectReaderMock->expects(self::once()) - ->method('readAmount') + ->willReturn($this->paymentDO); + $this->subjectReader->method('readAmount') ->with($buildSubject) ->willReturn($amount); - $paymentExtensionMock = $this->getMockBuilder(OrderPaymentExtension::class) + /** @var OrderPaymentExtension|MockObject $paymentExtension */ + $paymentExtension = $this->getMockBuilder(OrderPaymentExtension::class) ->setMethods(['getVaultPaymentToken']) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $paymentTokenMock = $this->getMockBuilder(PaymentToken::class) + /** @var PaymentToken|MockObject $paymentToken */ + $paymentToken = $this->getMockBuilder(PaymentToken::class) ->disableOriginalConstructor() ->getMock(); - $paymentExtensionMock->expects(static::once()) - ->method('getVaultPaymentToken') - ->willReturn($paymentTokenMock); - $this->paymentMock->expects(static::once()) - ->method('getExtensionAttributes') - ->willReturn($paymentExtensionMock); + $paymentExtension->method('getVaultPaymentToken') + ->willReturn($paymentToken); + $this->payment->method('getExtensionAttributes') + ->willReturn($paymentExtension); - $paymentTokenMock->expects(static::once()) - ->method('getGatewayToken') + $paymentToken->method('getGatewayToken') ->willReturn($token); $result = $this->builder->build($buildSubject); self::assertEquals($expected, $result); } + + /** + * Checks a builder execution if Payment Token doesn't exist. + * + * @expectedException \Magento\Payment\Gateway\Command\CommandException + * @expectedExceptionMessage The Payment Token is not available to perform the request. + */ + public function testBuildWithoutPaymentToken(): void + { + $amount = 30.00; + $buildSubject = [ + 'payment' => $this->paymentDO, + 'amount' => $amount, + ]; + + $this->subjectReader->method('readPayment') + ->with($buildSubject) + ->willReturn($this->paymentDO); + $this->subjectReader->method('readAmount') + ->with($buildSubject) + ->willReturn($amount); + + /** @var OrderPaymentExtension|MockObject $paymentExtension */ + $paymentExtension = $this->getMockBuilder(OrderPaymentExtension::class) + ->setMethods(['getVaultPaymentToken']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->payment->method('getExtensionAttributes') + ->willReturn($paymentExtension); + $paymentExtension->method('getVaultPaymentToken') + ->willReturn(null); + + $this->builder->build($buildSubject); + } } diff --git a/app/code/Magento/Braintree/Test/Unit/Model/Paypal/Helper/OrderPlaceTest.php b/app/code/Magento/Braintree/Test/Unit/Model/Paypal/Helper/OrderPlaceTest.php index 1aecba91b9af..c8524017274a 100644 --- a/app/code/Magento/Braintree/Test/Unit/Model/Paypal/Helper/OrderPlaceTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Model/Paypal/Helper/OrderPlaceTest.php @@ -3,9 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Braintree\Test\Unit\Model\Paypal\Helper; use Magento\Braintree\Model\Paypal\Helper\OrderPlace; +use Magento\Braintree\Model\Paypal\OrderCancellationService; use Magento\Checkout\Api\AgreementsValidatorInterface; use Magento\Checkout\Helper\Data; use Magento\Checkout\Model\Type\Onepage; @@ -14,6 +17,7 @@ use Magento\Quote\Api\CartManagementInterface; use Magento\Quote\Model\Quote; use Magento\Quote\Model\Quote\Address; +use PHPUnit\Framework\MockObject\MockObject; /** * Class OrderPlaceTest @@ -27,62 +31,80 @@ class OrderPlaceTest extends \PHPUnit\Framework\TestCase const TEST_EMAIL = 'test@test.loc'; /** - * @var CartManagementInterface|\PHPUnit_Framework_MockObject_MockObject + * @var CartManagementInterface|MockObject */ - private $cartManagementMock; + private $cartManagement; /** - * @var AgreementsValidatorInterface|\PHPUnit_Framework_MockObject_MockObject + * @var AgreementsValidatorInterface|MockObject */ - private $agreementsValidatorMock; + private $agreementsValidator; /** - * @var Session|\PHPUnit_Framework_MockObject_MockObject + * @var Session|MockObject */ - private $customerSessionMock; + private $customerSession; /** - * @var Data|\PHPUnit_Framework_MockObject_MockObject + * @var Data|MockObject */ - private $checkoutHelperMock; + private $checkoutHelper; /** - * @var Address|\PHPUnit_Framework_MockObject_MockObject + * @var Address|MockObject */ - private $billingAddressMock; + private $billingAddress; /** * @var OrderPlace */ private $orderPlace; + /** + * @var OrderCancellationService|MockObject + */ + private $orderCancellation; + + /** + * @inheritdoc + */ protected function setUp() { - $this->cartManagementMock = $this->getMockBuilder(CartManagementInterface::class) + $this->cartManagement = $this->getMockBuilder(CartManagementInterface::class) ->getMockForAbstractClass(); - $this->agreementsValidatorMock = $this->getMockBuilder(AgreementsValidatorInterface::class) + $this->agreementsValidator = $this->getMockBuilder(AgreementsValidatorInterface::class) ->getMockForAbstractClass(); - $this->customerSessionMock = $this->getMockBuilder(Session::class) + $this->customerSession = $this->getMockBuilder(Session::class) + ->disableOriginalConstructor() + ->getMock(); + $this->checkoutHelper = $this->getMockBuilder(Data::class) ->disableOriginalConstructor() ->getMock(); - $this->checkoutHelperMock = $this->getMockBuilder(Data::class) + + $this->orderCancellation = $this->getMockBuilder(OrderCancellationService::class) ->disableOriginalConstructor() ->getMock(); $this->orderPlace = new OrderPlace( - $this->cartManagementMock, - $this->agreementsValidatorMock, - $this->customerSessionMock, - $this->checkoutHelperMock + $this->cartManagement, + $this->agreementsValidator, + $this->customerSession, + $this->checkoutHelper, + $this->orderCancellation ); } + /** + * Checks a scenario for a guest customer. + * + * @throws \Exception + */ public function testExecuteGuest() { $agreement = ['test', 'test']; $quoteMock = $this->getQuoteMock(); - $this->agreementsValidatorMock->expects(self::once()) + $this->agreementsValidator->expects(self::once()) ->method('isValid') ->willReturn(true); @@ -97,7 +119,7 @@ public function testExecuteGuest() ->method('getId') ->willReturn(10); - $this->cartManagementMock->expects(self::once()) + $this->cartManagement->expects(self::once()) ->method('placeOrder') ->with(10); @@ -105,9 +127,11 @@ public function testExecuteGuest() } /** - * @param \PHPUnit_Framework_MockObject_MockObject $quoteMock + * Disables address validation. + * + * @param MockObject $quoteMock */ - private function disabledQuoteAddressValidationStep(\PHPUnit_Framework_MockObject_MockObject $quoteMock) + private function disabledQuoteAddressValidationStep(MockObject $quoteMock) { $billingAddressMock = $this->getBillingAddressMock($quoteMock); $shippingAddressMock = $this->getMockBuilder(Address::class) @@ -115,26 +139,21 @@ private function disabledQuoteAddressValidationStep(\PHPUnit_Framework_MockObjec ->disableOriginalConstructor() ->getMock(); - $quoteMock->expects(self::once()) - ->method('getShippingAddress') + $quoteMock->method('getShippingAddress') ->willReturn($shippingAddressMock); - $billingAddressMock->expects(self::once()) - ->method('setShouldIgnoreValidation') + $billingAddressMock->method('setShouldIgnoreValidation') ->with(true) ->willReturnSelf(); - $quoteMock->expects(self::once()) - ->method('getIsVirtual') + $quoteMock->method('getIsVirtual') ->willReturn(false); - $shippingAddressMock->expects(self::once()) - ->method('setShouldIgnoreValidation') + $shippingAddressMock->method('setShouldIgnoreValidation') ->with(true) ->willReturnSelf(); - $billingAddressMock->expects(self::any()) - ->method('getEmail') + $billingAddressMock->method('getEmail') ->willReturn(self::TEST_EMAIL); $billingAddressMock->expects(self::never()) @@ -142,25 +161,24 @@ private function disabledQuoteAddressValidationStep(\PHPUnit_Framework_MockObjec } /** - * @param \PHPUnit_Framework_MockObject_MockObject $quoteMock + * Prepares checkout step. + * + * @param MockObject $quoteMock */ - private function getCheckoutMethodStep(\PHPUnit_Framework_MockObject_MockObject $quoteMock) + private function getCheckoutMethodStep(MockObject $quoteMock) { - $this->customerSessionMock->expects(self::once()) - ->method('isLoggedIn') + $this->customerSession->method('isLoggedIn') ->willReturn(false); $quoteMock->expects(self::at(1)) ->method('getCheckoutMethod') ->willReturn(null); - $this->checkoutHelperMock->expects(self::once()) - ->method('isAllowedGuestCheckout') + $this->checkoutHelper->method('isAllowedGuestCheckout') ->with($quoteMock) ->willReturn(true); - $quoteMock->expects(self::once()) - ->method('setCheckoutMethod') + $quoteMock->method('setCheckoutMethod') ->with(Onepage::METHOD_GUEST); $quoteMock->expects(self::at(2)) @@ -169,9 +187,11 @@ private function getCheckoutMethodStep(\PHPUnit_Framework_MockObject_MockObject } /** - * @param \PHPUnit_Framework_MockObject_MockObject $quoteMock + * Prepares quote. + * + * @param MockObject $quoteMock */ - private function prepareGuestQuoteStep(\PHPUnit_Framework_MockObject_MockObject $quoteMock) + private function prepareGuestQuoteStep(MockObject $quoteMock) { $billingAddressMock = $this->getBillingAddressMock($quoteMock); @@ -184,44 +204,44 @@ private function prepareGuestQuoteStep(\PHPUnit_Framework_MockObject_MockObject ->method('getEmail') ->willReturn(self::TEST_EMAIL); - $quoteMock->expects(self::once()) - ->method('setCustomerEmail') + $quoteMock->method('setCustomerEmail') ->with(self::TEST_EMAIL) ->willReturnSelf(); - $quoteMock->expects(self::once()) - ->method('setCustomerIsGuest') + $quoteMock->method('setCustomerIsGuest') ->with(true) ->willReturnSelf(); - $quoteMock->expects(self::once()) - ->method('setCustomerGroupId') + $quoteMock->method('setCustomerGroupId') ->with(Group::NOT_LOGGED_IN_ID) ->willReturnSelf(); } /** - * @param \PHPUnit_Framework_MockObject_MockObject $quoteMock - * @return Address|\PHPUnit_Framework_MockObject_MockObject + * Gets a mock object for a billing address entity. + * + * @param MockObject $quoteMock + * @return Address|MockObject */ - private function getBillingAddressMock(\PHPUnit_Framework_MockObject_MockObject $quoteMock) + private function getBillingAddressMock(MockObject $quoteMock) { - if (!isset($this->billingAddressMock)) { - $this->billingAddressMock = $this->getMockBuilder(Address::class) + if (!isset($this->billingAddress)) { + $this->billingAddress = $this->getMockBuilder(Address::class) ->setMethods(['setShouldIgnoreValidation', 'getEmail', 'setSameAsBilling']) ->disableOriginalConstructor() ->getMock(); } - $quoteMock->expects(self::any()) - ->method('getBillingAddress') - ->willReturn($this->billingAddressMock); + $quoteMock->method('getBillingAddress') + ->willReturn($this->billingAddress); - return $this->billingAddressMock; + return $this->billingAddress; } /** - * @return Quote|\PHPUnit_Framework_MockObject_MockObject + * Gets a mock object for a quote. + * + * @return Quote|MockObject */ private function getQuoteMock() { diff --git a/app/code/Magento/Braintree/Test/Unit/Model/Paypal/Helper/QuoteUpdaterTest.php b/app/code/Magento/Braintree/Test/Unit/Model/Paypal/Helper/QuoteUpdaterTest.php index 62452228b618..c2678d1c7843 100644 --- a/app/code/Magento/Braintree/Test/Unit/Model/Paypal/Helper/QuoteUpdaterTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Model/Paypal/Helper/QuoteUpdaterTest.php @@ -3,23 +3,24 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Braintree\Test\Unit\Model\Paypal\Helper; -use Magento\Quote\Model\Quote; -use Magento\Quote\Model\Quote\Address; -use Magento\Quote\Model\Quote\Payment; -use Magento\Quote\Api\CartRepositoryInterface; -use Magento\Braintree\Model\Ui\PayPal\ConfigProvider; -use Magento\Braintree\Observer\DataAssignObserver; use Magento\Braintree\Gateway\Config\PayPal\Config; use Magento\Braintree\Model\Paypal\Helper\QuoteUpdater; +use Magento\Braintree\Model\Ui\PayPal\ConfigProvider; +use Magento\Braintree\Observer\DataAssignObserver; +use Magento\Quote\Api\CartRepositoryInterface; use Magento\Quote\Api\Data\CartExtensionInterface; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Address; +use Magento\Quote\Model\Quote\Payment; +use PHPUnit_Framework_MockObject_MockObject as MockObject; /** * Class QuoteUpdaterTest * - * @see \Magento\Braintree\Model\Paypal\Helper\QuoteUpdater - * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class QuoteUpdaterTest extends \PHPUnit\Framework\TestCase @@ -27,24 +28,24 @@ class QuoteUpdaterTest extends \PHPUnit\Framework\TestCase const TEST_NONCE = '3ede7045-2aea-463e-9754-cd658ffeeb48'; /** - * @var Config|\PHPUnit_Framework_MockObject_MockObject + * @var Config|MockObject */ - private $configMock; + private $config; /** - * @var CartRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + * @var CartRepositoryInterface|MockObject */ - private $quoteRepositoryMock; + private $quoteRepository; /** - * @var Address|\PHPUnit_Framework_MockObject_MockObject + * @var Address|MockObject */ - private $billingAddressMock; + private $billingAddress; /** - * @var Address|\PHPUnit_Framework_MockObject_MockObject + * @var Address|MockObject */ - private $shippingAddressMock; + private $shippingAddress; /** * @var QuoteUpdater @@ -52,17 +53,17 @@ class QuoteUpdaterTest extends \PHPUnit\Framework\TestCase private $quoteUpdater; /** - * @return void + * @inheritdoc */ protected function setUp() { - $this->configMock = $this->getMockBuilder(Config::class) + $this->config = $this->getMockBuilder(Config::class) ->disableOriginalConstructor() ->getMock(); - $this->quoteRepositoryMock = $this->getMockBuilder(CartRepositoryInterface::class) + $this->quoteRepository = $this->getMockBuilder(CartRepositoryInterface::class) ->getMockForAbstractClass(); - $this->billingAddressMock = $this->getMockBuilder(Address::class) + $this->billingAddress = $this->getMockBuilder(Address::class) ->setMethods( [ 'setLastname', @@ -77,9 +78,10 @@ protected function setUp() 'setShouldIgnoreValidation', 'getEmail' ] - )->disableOriginalConstructor() + ) + ->disableOriginalConstructor() ->getMock(); - $this->shippingAddressMock = $this->getMockBuilder(Address::class) + $this->shippingAddress = $this->getMockBuilder(Address::class) ->setMethods( [ 'setLastname', @@ -93,61 +95,61 @@ protected function setUp() 'setPostcode', 'setShouldIgnoreValidation' ] - )->disableOriginalConstructor() + ) + ->disableOriginalConstructor() ->getMock(); $this->quoteUpdater = new QuoteUpdater( - $this->configMock, - $this->quoteRepositoryMock + $this->config, + $this->quoteRepository ); } /** - * @return void + * Checks if quote details can be update by the response from Braintree. + * * @throws \Magento\Framework\Exception\LocalizedException */ - public function testExecute() + public function testExecute(): void { $details = $this->getDetails(); - $quoteMock = $this->getQuoteMock(); - $paymentMock = $this->getPaymentMock(); + $quote = $this->getQuoteMock(); + $payment = $this->getPaymentMock(); - $quoteMock->expects(self::once()) - ->method('getPayment') - ->willReturn($paymentMock); + $quote->method('getPayment') + ->willReturn($payment); - $paymentMock->expects(self::once()) - ->method('setMethod') + $payment->method('setMethod') ->with(ConfigProvider::PAYPAL_CODE); - $paymentMock->expects(self::once()) - ->method('setAdditionalInformation') + $payment->method('setAdditionalInformation') ->with(DataAssignObserver::PAYMENT_METHOD_NONCE, self::TEST_NONCE); - $this->updateQuoteStep($quoteMock, $details); + $this->updateQuoteStep($quote, $details); - $this->quoteUpdater->execute(self::TEST_NONCE, $details, $quoteMock); + $this->quoteUpdater->execute(self::TEST_NONCE, $details, $quote); } /** + * Disables quote's addresses validation. + * * @return void */ - private function disabledQuoteAddressValidationStep() + private function disabledQuoteAddressValidationStep(): void { - $this->billingAddressMock->expects(self::once()) - ->method('setShouldIgnoreValidation') + $this->billingAddress->method('setShouldIgnoreValidation') ->with(true); - $this->shippingAddressMock->expects(self::once()) - ->method('setShouldIgnoreValidation') + $this->shippingAddress->method('setShouldIgnoreValidation') ->with(true); - $this->billingAddressMock->expects(self::once()) - ->method('getEmail') + $this->billingAddress->method('getEmail') ->willReturn('bt_buyer_us@paypal.com'); } /** + * Gets quote's details. + * * @return array */ - private function getDetails() + private function getDetails(): array { return [ 'email' => 'bt_buyer_us@paypal.com', @@ -163,7 +165,7 @@ private function getDetails() 'region' => 'IL', 'postalCode' => '60618', 'countryCodeAlpha2' => 'US', - 'recipientName' => 'John Doe', + 'recipientName' => 'Jane Smith', ], 'billingAddress' => [ 'streetAddress' => '123 Billing Street', @@ -177,54 +179,51 @@ private function getDetails() } /** + * Updates shipping address details. + * * @param array $details */ - private function updateShippingAddressStep(array $details) + private function updateShippingAddressStep(array $details): void { - $this->shippingAddressMock->expects(self::once()) - ->method('setLastname') - ->with($details['lastName']); - $this->shippingAddressMock->expects(self::once()) - ->method('setFirstname') - ->with($details['firstName']); - $this->shippingAddressMock->expects(self::once()) - ->method('setEmail') + $this->shippingAddress->method('setLastname') + ->with('Smith'); + $this->shippingAddress->method('setFirstname') + ->with('Jane'); + $this->shippingAddress->method('setEmail') ->with($details['email']); - $this->shippingAddressMock->expects(self::once()) - ->method('setCollectShippingRates') + $this->shippingAddress->method('setCollectShippingRates') ->with(true); - $this->updateAddressDataStep($this->shippingAddressMock, $details['shippingAddress']); + $this->updateAddressDataStep($this->shippingAddress, $details['shippingAddress']); } /** - * @param \PHPUnit_Framework_MockObject_MockObject $addressMock + * Updates address details. + * + * @param MockObject $address * @param array $addressData */ - private function updateAddressDataStep(\PHPUnit_Framework_MockObject_MockObject $addressMock, array $addressData) + private function updateAddressDataStep(MockObject $address, array $addressData): void { - $addressMock->expects(self::once()) - ->method('setStreet') + $address->method('setStreet') ->with([$addressData['streetAddress'], $addressData['extendedAddress']]); - $addressMock->expects(self::once()) - ->method('setCity') + $address->method('setCity') ->with($addressData['locality']); - $addressMock->expects(self::once()) - ->method('setRegionCode') + $address->method('setRegionCode') ->with($addressData['region']); - $addressMock->expects(self::once()) - ->method('setCountryId') + $address->method('setCountryId') ->with($addressData['countryCodeAlpha2']); - $addressMock->expects(self::once()) - ->method('setPostcode') + $address->method('setPostcode') ->with($addressData['postalCode']); } /** - * @param \PHPUnit_Framework_MockObject_MockObject $quoteMock + * Updates quote's address details. + * + * @param MockObject $quoteMock * @param array $details */ - private function updateQuoteAddressStep(\PHPUnit_Framework_MockObject_MockObject $quoteMock, array $details) + private function updateQuoteAddressStep(MockObject $quoteMock, array $details): void { $quoteMock->expects(self::exactly(2)) ->method('getIsVirtual') @@ -235,64 +234,61 @@ private function updateQuoteAddressStep(\PHPUnit_Framework_MockObject_MockObject } /** + * Updates billing address details. + * * @param array $details */ - private function updateBillingAddressStep(array $details) + private function updateBillingAddressStep(array $details): void { - $this->configMock->expects(self::once()) - ->method('isRequiredBillingAddress') + $this->config->method('isRequiredBillingAddress') ->willReturn(true); - $this->updateAddressDataStep($this->billingAddressMock, $details['billingAddress']); + $this->updateAddressDataStep($this->billingAddress, $details['billingAddress']); - $this->billingAddressMock->expects(self::once()) - ->method('setLastname') + $this->billingAddress->method('setLastname') ->with($details['lastName']); - $this->billingAddressMock->expects(self::once()) - ->method('setFirstname') + $this->billingAddress->method('setFirstname') ->with($details['firstName']); - $this->billingAddressMock->expects(self::once()) - ->method('setEmail') + $this->billingAddress->method('setEmail') ->with($details['email']); } /** - * @param \PHPUnit_Framework_MockObject_MockObject $quoteMock + * Updates quote details. + * + * @param MockObject $quote * @param array $details */ - private function updateQuoteStep(\PHPUnit_Framework_MockObject_MockObject $quoteMock, array $details) + private function updateQuoteStep(MockObject $quote, array $details): void { - $quoteMock->expects(self::once()) - ->method('setMayEditShippingAddress') + $quote->method('setMayEditShippingAddress') ->with(false); - $quoteMock->expects(self::once()) - ->method('setMayEditShippingMethod') + $quote->method('setMayEditShippingMethod') ->with(true); - $quoteMock->expects(self::exactly(2)) - ->method('getShippingAddress') - ->willReturn($this->shippingAddressMock); - $quoteMock->expects(self::exactly(2)) + $quote->method('getShippingAddress') + ->willReturn($this->shippingAddress); + $quote->expects(self::exactly(2)) ->method('getBillingAddress') - ->willReturn($this->billingAddressMock); + ->willReturn($this->billingAddress); - $this->updateQuoteAddressStep($quoteMock, $details); + $this->updateQuoteAddressStep($quote, $details); $this->disabledQuoteAddressValidationStep(); - $quoteMock->expects(self::once()) - ->method('collectTotals'); + $quote->method('collectTotals'); - $this->quoteRepositoryMock->expects(self::once()) - ->method('save') - ->with($quoteMock); + $this->quoteRepository->method('save') + ->with($quote); } /** - * @return Quote|\PHPUnit_Framework_MockObject_MockObject + * Creates a mock for Quote object. + * + * @return Quote|MockObject */ - private function getQuoteMock() + private function getQuoteMock(): MockObject { - $quoteMock = $this->getMockBuilder(Quote::class) + $quote = $this->getMockBuilder(Quote::class) ->setMethods( [ 'getIsVirtual', @@ -304,25 +300,27 @@ private function getQuoteMock() 'getBillingAddress', 'getExtensionAttributes' ] - )->disableOriginalConstructor() + ) + ->disableOriginalConstructor() ->getMock(); - $cartExtensionMock = $this->getMockBuilder(CartExtensionInterface::class) + $cartExtension = $this->getMockBuilder(CartExtensionInterface::class) ->setMethods(['setShippingAssignments']) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $quoteMock->expects(self::any()) - ->method('getExtensionAttributes') - ->willReturn($cartExtensionMock); + $quote->method('getExtensionAttributes') + ->willReturn($cartExtension); - return $quoteMock; + return $quote; } /** - * @return Payment|\PHPUnit_Framework_MockObject_MockObject + * Creates a mock for Payment object. + * + * @return Payment|MockObject */ - private function getPaymentMock() + private function getPaymentMock(): MockObject { return $this->getMockBuilder(Payment::class) ->disableOriginalConstructor() 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 372415d3530c..55e76cae9103 100644 --- a/app/code/Magento/Braintree/Test/Unit/Model/Report/BraintreeTransactionStub.php +++ b/app/code/Magento/Braintree/Test/Unit/Model/Report/BraintreeTransactionStub.php @@ -40,7 +40,7 @@ public function __get($name) } /** - * Checks for the existance of a property stored in the private $_attributes property + * Checks for the existence of a property stored in the private $_attributes property * * @ignore * @param string $name diff --git a/app/code/Magento/Braintree/Test/Unit/Model/Ui/PayPal/ConfigProviderTest.php b/app/code/Magento/Braintree/Test/Unit/Model/Ui/PayPal/ConfigProviderTest.php index 22f7f46bd98f..42469fe0faf4 100644 --- a/app/code/Magento/Braintree/Test/Unit/Model/Ui/PayPal/ConfigProviderTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Model/Ui/PayPal/ConfigProviderTest.php @@ -77,6 +77,9 @@ public function testGetConfig($expected) 'width' => 30, 'height' => 26, 'url' => 'https://icon.test.url' ]); + $this->config->method('isRequiredBillingAddress') + ->willReturn(1); + self::assertEquals($expected, $this->configProvider->getConfig()); } @@ -101,7 +104,8 @@ public function getConfigDataProvider() 'skipOrderReview' => false, 'paymentIcon' => [ 'width' => 30, 'height' => 26, 'url' => 'https://icon.test.url' - ] + ], + 'isRequiredBillingAddress' => true ] ] ] diff --git a/app/code/Magento/Braintree/composer.json b/app/code/Magento/Braintree/composer.json index ba51a5436fed..2f956076f384 100644 --- a/app/code/Magento/Braintree/composer.json +++ b/app/code/Magento/Braintree/composer.json @@ -6,7 +6,7 @@ }, "require": { "php": "~7.1.3||~7.2.0", - "braintree/braintree_php": "3.34.0", + "braintree/braintree_php": "3.35.0", "magento/framework": "*", "magento/magento-composer-installer": "*", "magento/module-catalog": "*", @@ -21,7 +21,8 @@ "magento/module-quote": "*", "magento/module-sales": "*", "magento/module-ui": "*", - "magento/module-vault": "*" + "magento/module-vault": "*", + "magento/module-multishipping": "*" }, "suggest": { "magento/module-checkout-agreements": "*", diff --git a/app/code/Magento/Braintree/etc/config.xml b/app/code/Magento/Braintree/etc/config.xml index a830c2936875..fe4cfab9c0e3 100644 --- a/app/code/Magento/Braintree/etc/config.xml +++ b/app/code/Magento/Braintree/etc/config.xml @@ -42,6 +42,7 @@ <paymentInfoKeys>cc_type,cc_number,avsPostalCodeResponseCode,avsStreetAddressResponseCode,cvvResponseCode,processorAuthorizationCode,processorResponseCode,processorResponseText,liabilityShifted,liabilityShiftPossible,riskDataId,riskDataDecision</paymentInfoKeys> <avs_ems_adapter>Magento\Braintree\Model\AvsEmsCodeMapper</avs_ems_adapter> <cvv_ems_adapter>Magento\Braintree\Model\CvvEmsCodeMapper</cvv_ems_adapter> + <group>braintree_group</group> </braintree> <braintree_paypal> <model>BraintreePayPalFacade</model> @@ -67,6 +68,7 @@ <privateInfoKeys>processorResponseCode,processorResponseText,paymentId</privateInfoKeys> <paymentInfoKeys>processorResponseCode,processorResponseText,paymentId,payerEmail</paymentInfoKeys> <supported_locales>en_US,en_GB,en_AU,da_DK,fr_FR,fr_CA,de_DE,zh_HK,it_IT,nl_NL,no_NO,pl_PL,es_ES,sv_SE,tr_TR,pt_BR,ja_JP,id_ID,ko_KR,pt_PT,ru_RU,th_TH,zh_CN,zh_TW</supported_locales> + <group>braintree_group</group> </braintree_paypal> <braintree_cc_vault> <model>BraintreeCreditCardVaultFacade</model> @@ -76,6 +78,7 @@ <tokenFormat>Magento\Braintree\Model\InstantPurchase\CreditCard\TokenFormatter</tokenFormat> <additionalInformation>Magento\Braintree\Model\InstantPurchase\PaymentAdditionalInformationProvider</additionalInformation> </instant_purchase> + <group>braintree_group</group> </braintree_cc_vault> <braintree_paypal_vault> <model>BraintreePayPalVaultFacade</model> @@ -85,6 +88,7 @@ <tokenFormat>Magento\Braintree\Model\InstantPurchase\PayPal\TokenFormatter</tokenFormat> <additionalInformation>Magento\Braintree\Model\InstantPurchase\PaymentAdditionalInformationProvider</additionalInformation> </instant_purchase> + <group>braintree_group</group> </braintree_paypal_vault> </payment> </default> diff --git a/app/code/Magento/Braintree/etc/di.xml b/app/code/Magento/Braintree/etc/di.xml index 67c90e6991e2..b81513caf17a 100644 --- a/app/code/Magento/Braintree/etc/di.xml +++ b/app/code/Magento/Braintree/etc/di.xml @@ -624,4 +624,8 @@ </argument> </arguments> </type> + + <type name="Magento\Quote\Api\CartManagementInterface"> + <plugin name="order_cancellation" type="Magento\Braintree\Plugin\OrderCancellation"/> + </type> </config> diff --git a/app/code/Magento/Braintree/etc/frontend/di.xml b/app/code/Magento/Braintree/etc/frontend/di.xml index ea417c407dff..d8d3a93b71dc 100644 --- a/app/code/Magento/Braintree/etc/frontend/di.xml +++ b/app/code/Magento/Braintree/etc/frontend/di.xml @@ -61,4 +61,12 @@ <argument name="resolver" xsi:type="object">Magento\Braintree\Model\LocaleResolver</argument> </arguments> </type> + <type name="Magento\Multishipping\Model\Checkout\Type\Multishipping\PlaceOrderPool"> + <arguments> + <argument name="services" xsi:type="array"> + <item name="braintree" xsi:type="string">Magento\Braintree\Model\Multishipping\PlaceOrder</item> + <item name="braintree_paypal" xsi:type="string">Magento\Braintree\Model\Multishipping\PlaceOrder</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Braintree/etc/payment.xml b/app/code/Magento/Braintree/etc/payment.xml new file mode 100644 index 000000000000..4cae049aaf5a --- /dev/null +++ b/app/code/Magento/Braintree/etc/payment.xml @@ -0,0 +1,23 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<payment xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Payment:etc/payment.xsd"> + <groups> + <group id="braintree_group"> + <label>Braintree</label> + </group> + </groups> + <methods> + <method name="braintree"> + <allow_multiple_address>1</allow_multiple_address> + </method> + <method name="braintree_paypal"> + <allow_multiple_address>1</allow_multiple_address> + </method> + </methods> +</payment> diff --git a/app/code/Magento/Braintree/i18n/en_US.csv b/app/code/Magento/Braintree/i18n/en_US.csv index 6bf677151ed0..e9145b35b56e 100644 --- a/app/code/Magento/Braintree/i18n/en_US.csv +++ b/app/code/Magento/Braintree/i18n/en_US.csv @@ -192,3 +192,5 @@ Currency,Currency "Too many concurrent attempts to refund this transaction. Try again later.","Too many concurrent attempts to refund this transaction. Try again later." "Too many concurrent attempts to void this transaction. Try again later.","Too many concurrent attempts to void this transaction. Try again later." "Braintree Settlement","Braintree Settlement" +"The Payment Token is not available to perform the request.","The Payment Token is not available to perform the request." +"The order #%1 cannot be processed.","The order #%1 cannot be processed." diff --git a/app/code/Magento/Braintree/view/adminhtml/templates/form/cc.phtml b/app/code/Magento/Braintree/view/adminhtml/templates/form/cc.phtml index 535a5a852fe7..4c15fffa8189 100644 --- a/app/code/Magento/Braintree/view/adminhtml/templates/form/cc.phtml +++ b/app/code/Magento/Braintree/view/adminhtml/templates/form/cc.phtml @@ -83,7 +83,7 @@ $ccType = $block->getInfoData('cc_type'); id="<?= /* @noEscape */ $code ?>_vault" name="payment[is_active_payment_token_enabler]" class="admin__control-checkbox"/> - <label class="label" for="<?= /* @noEscape */ $code ?>_vault"> + <label class="label admin__field-label" for="<?= /* @noEscape */ $code ?>_vault"> <span><?= $block->escapeHtml(__('Save for later use.')) ?></span> </label> </div> diff --git a/app/code/Magento/Braintree/view/frontend/layout/checkout_index_index.xml b/app/code/Magento/Braintree/view/frontend/layout/checkout_index_index.xml index ab294f8e797b..c4152e1c3ebf 100644 --- a/app/code/Magento/Braintree/view/frontend/layout/checkout_index_index.xml +++ b/app/code/Magento/Braintree/view/frontend/layout/checkout_index_index.xml @@ -35,6 +35,9 @@ <item name="braintree_paypal" xsi:type="array"> <item name="isBillingAddressRequired" xsi:type="boolean">false</item> </item> + <item name="braintree_cc_vault" xsi:type="array"> + <item name="isBillingAddressRequired" xsi:type="boolean">true</item> + </item> </item> </item> </item> diff --git a/app/code/Magento/Braintree/view/frontend/layout/multishipping_checkout_billing.xml b/app/code/Magento/Braintree/view/frontend/layout/multishipping_checkout_billing.xml new file mode 100644 index 000000000000..06390d403e63 --- /dev/null +++ b/app/code/Magento/Braintree/view/frontend/layout/multishipping_checkout_billing.xml @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceBlock name="checkout_billing"> + <arguments> + <argument name="form_templates" xsi:type="array"> + <item name="braintree" xsi:type="string">Magento_Braintree::multishipping/form.phtml</item> + <item name="braintree_paypal" xsi:type="string">Magento_Braintree::multishipping/form_paypal.phtml</item> + </argument> + </arguments> + </referenceBlock> + </body> +</page> diff --git a/app/code/Magento/Braintree/view/frontend/templates/multishipping/form.phtml b/app/code/Magento/Braintree/view/frontend/templates/multishipping/form.phtml new file mode 100644 index 000000000000..bf8aa8dd09c2 --- /dev/null +++ b/app/code/Magento/Braintree/view/frontend/templates/multishipping/form.phtml @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +?> +<script> + require([ + 'uiLayout', + 'jquery' + ], function (layout, $) { + $(function () { + var paymentMethodData = { + method: 'braintree' + }; + layout([ + { + component: 'Magento_Braintree/js/view/payment/method-renderer/multishipping/hosted-fields', + name: 'payment_method_braintree', + method: paymentMethodData.method, + item: paymentMethodData + } + ]); + + $('body').trigger('contentUpdated'); + }) + }) +</script> +<!-- ko template: getTemplate() --><!-- /ko --> diff --git a/app/code/Magento/Braintree/view/frontend/templates/multishipping/form_paypal.phtml b/app/code/Magento/Braintree/view/frontend/templates/multishipping/form_paypal.phtml new file mode 100644 index 000000000000..ea3eb2214c2d --- /dev/null +++ b/app/code/Magento/Braintree/view/frontend/templates/multishipping/form_paypal.phtml @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +?> +<script> + require([ + 'uiLayout', + 'jquery' + ], function (layout, $) { + $(function () { + var paymentMethodData = { + method: 'braintree_paypal' + }; + layout([ + { + component: 'Magento_Braintree/js/view/payment/method-renderer/multishipping/paypal', + name: 'payment_method_braintree_paypal', + method: paymentMethodData.method, + item: paymentMethodData + } + ]); + + $('body').trigger('contentUpdated'); + }) + }) +</script> +<!-- ko template: getTemplate() --><!-- /ko --> diff --git a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/hosted-fields.js b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/hosted-fields.js index 05c09abdb7b2..9e496e43b27c 100644 --- a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/hosted-fields.js +++ b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/hosted-fields.js @@ -10,8 +10,9 @@ define([ 'Magento_Braintree/js/view/payment/method-renderer/cc-form', 'Magento_Braintree/js/validator', 'Magento_Vault/js/view/payment/vault-enabler', - 'mage/translate' -], function ($, Component, validator, VaultEnabler, $t) { + 'mage/translate', + 'Magento_Checkout/js/model/payment/additional-validators' +], function ($, Component, validator, VaultEnabler, $t, additionalValidators) { 'use strict'; return Component.extend({ @@ -154,7 +155,7 @@ define([ * Trigger order placing */ placeOrderClick: function () { - if (this.validateCardType()) { + if (this.validateCardType() && additionalValidators.validate()) { this.isPlaceOrderActionAllowed(false); $(this.getSelector('submit')).trigger('click'); } diff --git a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/multishipping/hosted-fields.js b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/multishipping/hosted-fields.js new file mode 100644 index 000000000000..1ceebc8e6628 --- /dev/null +++ b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/multishipping/hosted-fields.js @@ -0,0 +1,102 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +/*browser:true*/ +/*global define*/ + +define([ + 'jquery', + 'Magento_Braintree/js/view/payment/method-renderer/hosted-fields', + 'Magento_Braintree/js/validator', + 'Magento_Ui/js/model/messageList', + 'mage/translate', + 'Magento_Checkout/js/model/full-screen-loader', + 'Magento_Checkout/js/action/set-payment-information', + 'Magento_Checkout/js/model/payment/additional-validators' +], function ( + $, + Component, + validator, + messageList, + $t, + fullScreenLoader, + setPaymentInformationAction, + additionalValidators +) { + 'use strict'; + + return Component.extend({ + defaults: { + template: 'Magento_Braintree/payment/multishipping/form' + }, + + /** + * Get list of available CC types + * + * @returns {Object} + */ + getCcAvailableTypes: function () { + var availableTypes = validator.getAvailableCardTypes(), + billingCountryId; + + billingCountryId = $('#multishipping_billing_country_id').val(); + + if (billingCountryId && validator.getCountrySpecificCardTypes(billingCountryId)) { + return validator.collectTypes( + availableTypes, validator.getCountrySpecificCardTypes(billingCountryId) + ); + } + + return availableTypes; + }, + + /** + * @override + */ + placeOrder: function () { + var self = this; + + this.validatorManager.validate(self, function () { + return self.setPaymentInformation(); + }); + }, + + /** + * @override + */ + setPaymentInformation: function () { + if (additionalValidators.validate()) { + + fullScreenLoader.startLoader(); + + $.when( + setPaymentInformationAction( + this.messageContainer, + this.getData() + ) + ).done(this.done.bind(this)) + .fail(this.fail.bind(this)); + } + }, + + /** + * {Function} + */ + fail: function () { + fullScreenLoader.stopLoader(); + + return this; + }, + + /** + * {Function} + */ + done: function () { + fullScreenLoader.stopLoader(); + $('#multishipping-billing-form').submit(); + + return this; + } + }); +}); diff --git a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/multishipping/paypal.js b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/multishipping/paypal.js new file mode 100644 index 000000000000..6702e58d1214 --- /dev/null +++ b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/multishipping/paypal.js @@ -0,0 +1,143 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +/*browser:true*/ +/*global define*/ +define([ + 'jquery', + 'underscore', + 'Magento_Braintree/js/view/payment/method-renderer/paypal', + 'Magento_Checkout/js/action/set-payment-information', + 'Magento_Checkout/js/model/payment/additional-validators', + 'Magento_Checkout/js/model/full-screen-loader', + 'mage/translate' +], function ( + $, + _, + Component, + setPaymentInformationAction, + additionalValidators, + fullScreenLoader, + $t +) { + 'use strict'; + + return Component.extend({ + defaults: { + template: 'Magento_Braintree/payment/multishipping/paypal', + submitButtonSelector: '#payment-continue span' + }, + + /** + * @override + */ + onActiveChange: function (isActive) { + this.updateSubmitButtonTitle(isActive); + + this._super(isActive); + }, + + /** + * @override + */ + beforePlaceOrder: function (data) { + this._super(data); + + this.updateSubmitButtonTitle(true); + }, + + /** + * @override + */ + getShippingAddress: function () { + return {}; + }, + + /** + * @override + */ + getData: function () { + var data = this._super(); + + data['additional_data']['is_active_payment_token_enabler'] = true; + + return data; + }, + + /** + * @override + */ + isActiveVault: function () { + return true; + }, + + /** + * Skipping order review step on checkout with multiple addresses is not allowed. + * + * @returns {Boolean} + */ + isSkipOrderReview: function () { + return false; + }, + + /** + * Checks if payment method nonce is already received. + * + * @returns {Boolean} + */ + isPaymentMethodNonceReceived: function () { + return this.paymentMethodNonce !== null; + }, + + /** + * Updates submit button title on multi-addresses checkout billing form. + * + * @param {Boolean} isActive + */ + updateSubmitButtonTitle: function (isActive) { + var title = this.isPaymentMethodNonceReceived() || !isActive ? + $t('Go to Review Your Order') : $t('Continue to PayPal'); + + $(this.submitButtonSelector).html(title); + }, + + /** + * @override + */ + placeOrder: function () { + if (!this.isPaymentMethodNonceReceived()) { + this.payWithPayPal(); + } else { + fullScreenLoader.startLoader(); + + $.when( + setPaymentInformationAction( + this.messageContainer, + this.getData() + ) + ).done(this.done.bind(this)) + .fail(this.fail.bind(this)); + } + }, + + /** + * {Function} + */ + fail: function () { + fullScreenLoader.stopLoader(); + + return this; + }, + + /** + * {Function} + */ + done: function () { + fullScreenLoader.stopLoader(); + $('#multishipping-billing-form').submit(); + + return this; + } + }); +}); diff --git a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/paypal.js b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/paypal.js index 7c75cc3e594e..eaebd8492b0a 100644 --- a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/paypal.js +++ b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/paypal.js @@ -7,7 +7,6 @@ define([ 'jquery', 'underscore', - 'mage/utils/wrapper', 'Magento_Checkout/js/view/payment/default', 'Magento_Braintree/js/view/payment/adapter', 'Magento_Checkout/js/model/quote', @@ -19,7 +18,6 @@ define([ ], function ( $, _, - wrapper, Component, Braintree, quote, @@ -105,6 +103,12 @@ define([ } }); + quote.shippingAddress.subscribe(function () { + if (self.isActive()) { + self.reInitPayPal(); + } + }); + // for each component initialization need update property this.isReviewRequired(false); this.initClientConfig(); @@ -206,7 +210,9 @@ define([ beforePlaceOrder: function (data) { this.setPaymentMethodNonce(data.nonce); - if (quote.billingAddress() === null && typeof data.details.billingAddress !== 'undefined') { + if ((this.isRequiredBillingAddress() || quote.billingAddress() === null) && + typeof data.details.billingAddress !== 'undefined' + ) { this.setBillingAddress(data.details, data.details.billingAddress); } @@ -220,9 +226,8 @@ define([ /** * Re-init PayPal Auth Flow - * @param {Function} callback - Optional callback */ - reInitPayPal: function (callback) { + reInitPayPal: function () { if (Braintree.checkout) { Braintree.checkout.teardown(function () { Braintree.checkout = null; @@ -233,17 +238,6 @@ define([ this.clientConfig.paypal.amount = this.grandTotalAmount; this.clientConfig.paypal.shippingAddressOverride = this.getShippingAddress(); - if (callback) { - this.clientConfig.onReady = wrapper.wrap( - this.clientConfig.onReady, - function (original, checkout) { - this.clientConfig.onReady = original; - original(checkout); - callback(); - }.bind(this) - ); - } - Braintree.setConfig(this.clientConfig); Braintree.setup(); }, @@ -264,6 +258,14 @@ define([ return window.checkoutConfig.payment[this.getCode()].isAllowShippingAddressOverride; }, + /** + * Is billing address required from PayPal side + * @returns {Boolean} + */ + isRequiredBillingAddress: function () { + return window.checkoutConfig.payment[this.getCode()].isRequiredBillingAddress; + }, + /** * Get configuration for PayPal * @returns {Object} @@ -418,19 +420,17 @@ define([ * Triggers when customer click "Continue to PayPal" button */ payWithPayPal: function () { - this.reInitPayPal(function () { - if (!additionalValidators.validate()) { - return; - } + if (!additionalValidators.validate()) { + return; + } - try { - Braintree.checkout.paypal.initAuthFlow(); - } catch (e) { - this.messageContainer.addErrorMessage({ - message: $t('Payment ' + this.getTitle() + ' can\'t be initialized.') - }); - } - }.bind(this)); + try { + Braintree.checkout.paypal.initAuthFlow(); + } catch (e) { + this.messageContainer.addErrorMessage({ + message: $t('Payment ' + this.getTitle() + ' can\'t be initialized.') + }); + } }, /** diff --git a/app/code/Magento/Braintree/view/frontend/web/template/payment/multishipping/form.html b/app/code/Magento/Braintree/view/frontend/web/template/payment/multishipping/form.html new file mode 100644 index 000000000000..964e15df166d --- /dev/null +++ b/app/code/Magento/Braintree/view/frontend/web/template/payment/multishipping/form.html @@ -0,0 +1,106 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<div data-bind="attr: {class: 'payment-method payment-method-' + getCode()}, css: {'_active': isActive()}"> + <div> + <form id="co-transparent-form-braintree" class="form" data-bind="" method="post" action="#" novalidate="novalidate"> + <fieldset data-bind="attr: {class: 'fieldset payment items ccard ' + getCode(), id: 'payment_form_' + getCode()}"> + <legend class="legend"> + <span><!-- ko i18n: 'Credit Card Information'--><!-- /ko --></span> + </legend> + <br> + <div class="field type"> + <div class="control"> + <ul class="credit-card-types"> + <!-- ko foreach: {data: getCcAvailableTypes(), as: 'item'} --> + <li class="item" data-bind="css: { + _active: $parent.selectedCardType() == item, + _inactive: $parent.selectedCardType() != null && $parent.selectedCardType() != item + } "> + <!--ko if: $parent.getIcons(item) --> + <img data-bind="attr: { + 'src': $parent.getIcons(item).url, + 'width': $parent.getIcons(item).width, + 'height': $parent.getIcons(item).height + }"> + <!--/ko--> + </li> + <!--/ko--> + </ul> + <input type="hidden" + name="payment[cc_type]" + class="input-text" + value="" + data-bind="attr: {id: getCode() + '_cc_type', 'data-container': getCode() + '-cc-type'}, + value: creditCardType + "> + </div> + </div> + <div class="field number required"> + <label data-bind="attr: {for: getCode() + '_cc_number'}" class="label"> + <span><!-- ko i18n: 'Credit Card Number'--><!-- /ko --></span> + </label> + <div class="control"> + <div data-bind="attr: {id: getCode() + '_cc_number'}" class="hosted-control"></div> + <div class="hosted-error"><!-- ko i18n: 'Please, enter valid Credit Card Number'--><!-- /ko --></div> + </div> + </div> + <div class="field number required"> + <label data-bind="attr: {for: getCode() + '_expiration'}" class="label"> + <span><!-- ko i18n: 'Expiration Date'--><!-- /ko --></span> + </label> + <div class="control"> + <div class="hosted-date-wrap"> + <div data-bind="attr: {id: getCode() + '_expirationMonth'}" + class="hosted-control hosted-date"></div> + + <div data-bind="attr: {id: getCode() + '_expirationYear'}" + class="hosted-control hosted-date"></div> + + <div class="hosted-error"><!-- ko i18n: 'Please, enter valid Expiration Date'--><!-- /ko --></div> + </div> + </div> + </div> + <!-- ko if: (hasVerification())--> + <div class="field cvv required" data-bind="attr: {id: getCode() + '_cc_type_cvv_div'}"> + <label data-bind="attr: {for: getCode() + '_cc_cid'}" class="label"> + <span><!-- ko i18n: 'Card Verification Number'--><!-- /ko --></span> + </label> + <div class="control _with-tooltip"> + <div data-bind="attr: {id: getCode() + '_cc_cid'}" class="hosted-control hosted-cid"></div> + <div class="hosted-error"><!-- ko i18n: 'Please, enter valid Card Verification Number'--><!-- /ko --></div> + + <div class="field-tooltip toggle"> + <span class="field-tooltip-action action-cvv" + tabindex="0" + data-toggle="dropdown" + data-bind="attr: {title: $t('What is this?')}, mageInit: {'dropdown':{'activeClass': '_active'}}"> + <span><!-- ko i18n: 'What is this?'--><!-- /ko --></span> + </span> + <div class="field-tooltip-content" + data-target="dropdown" + data-bind="html: getCvvImageHtml()"></div> + </div> + </div> + </div> + <!-- /ko --> + </fieldset> + <input type="submit" id="braintree_submit" style="display:none" /> + </form> + + <div class="actions-toolbar no-display"> + <div class="primary"> + <button data-role="review-save" + type="submit" + data-bind="{click: placeOrderClick}" + class="action primary checkout"> + <span data-bind="i18n: 'Place Order'"></span> + </button> + </div> + </div> + </div> +</div> \ No newline at end of file diff --git a/app/code/Magento/Braintree/view/frontend/web/template/payment/multishipping/paypal.html b/app/code/Magento/Braintree/view/frontend/web/template/payment/multishipping/paypal.html new file mode 100644 index 000000000000..722989e41f98 --- /dev/null +++ b/app/code/Magento/Braintree/view/frontend/web/template/payment/multishipping/paypal.html @@ -0,0 +1,40 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="payment-method" data-bind="css: {'_active': isActive()}"> + <div class="payment-method-title field choice"> + <label class="label" data-bind="attr: {'for': getCode()}"> + <!-- PayPal Logo --> + <img data-bind="attr: {src: getPaymentAcceptanceMarkSrc(), alt: $t('Acceptance Mark'), title: $t('Acceptance Mark')}" + class="payment-icon"/> + <!-- PayPal Logo --> + <span text="getTitle()"></span> + </label> + </div> + + <div class="payment-method-content"> + <each args="getRegion('messages')" render=""></each> + <fieldset class="braintree-paypal-fieldset" data-bind='attr: {id: "payment_form_" + getCode()}'> + <div id="paypal-container"></div> + </fieldset> + <div class="actions-toolbar braintree-paypal-actions" data-bind="visible: isReviewRequired()"> + <div class="payment-method-item braintree-paypal-account"> + <span class="payment-method-type">PayPal</span> + <span class="payment-method-description" text="customerEmail()"></span> + </div> + <div class="actions-toolbar no-display"> + <div class="primary"> + <button data-button="paypal-place" data-role="review-save" + type="submit" + data-bind="{click: placeOrder}" + class="action primary checkout"> + <span data-bind="i18n: 'Place Order'"></span> + </button> + </div> + </div> + </div> + </div> +</div> diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Checkbox.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Checkbox.php index b220e2c98d77..46db8a990734 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Checkbox.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Checkbox.php @@ -20,9 +20,7 @@ class Checkbox extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Op protected $_template = 'Magento_Bundle::product/composite/fieldset/options/type/checkbox.phtml'; /** - * @param string $elementId - * @param string $containerId - * @return string + * @inheritdoc */ public function setValidationContainer($elementId, $containerId) { @@ -34,4 +32,15 @@ public function setValidationContainer($elementId, $containerId) '\'; </script>'; } + + /** + * @inheritdoc + */ + public function getSelectionPrice($selection) + { + $price = parent::getSelectionPrice($selection); + $qty = $selection->getSelectionQty(); + + return $price * $qty; + } } diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Multi.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Multi.php index a4b8c6bde73a..629f08dc7510 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Multi.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Multi.php @@ -20,9 +20,7 @@ class Multi extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Optio protected $_template = 'Magento_Bundle::product/composite/fieldset/options/type/multi.phtml'; /** - * @param string $elementId - * @param string $containerId - * @return string + * @inheritdoc */ public function setValidationContainer($elementId, $containerId) { @@ -34,4 +32,15 @@ public function setValidationContainer($elementId, $containerId) '\'; </script>'; } + + /** + * @inheritdoc + */ + public function getSelectionPrice($selection) + { + $price = parent::getSelectionPrice($selection); + $qty = $selection->getSelectionQty(); + + return $price * $qty; + } } diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes/Extend.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes/Extend.php index 0e21e566d5e7..a768e2450bfe 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes/Extend.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes/Extend.php @@ -4,13 +4,13 @@ * See COPYING.txt for license details. */ +namespace Magento\Bundle\Block\Adminhtml\Catalog\Product\Edit\Tab\Attributes; + /** - * Bundle Extended Attribures Block + * Bundle Extended Attribures Block. * - * @author Magento Core Team <core@magentocommerce.com> + * @author Magento Core Team <core@magentocommerce.com> */ -namespace Magento\Bundle\Block\Adminhtml\Catalog\Product\Edit\Tab\Attributes; - class Extend extends \Magento\Catalog\Block\Adminhtml\Form\Renderer\Fieldset\Element { /** @@ -75,7 +75,7 @@ public function getElementHtml() } /** - * Execute method getElementHtml from parrent class + * Execute method getElementHtml from parent class * * @return string */ @@ -85,6 +85,8 @@ public function getParentElementHtml() } /** + * Get options. + * * @return array */ public function getOptions() @@ -106,6 +108,8 @@ public function getOptions() } /** + * Is disabled field. + * * @return bool */ public function isDisabledField() @@ -118,6 +122,8 @@ public function isDisabledField() } /** + * Get product. + * * @return mixed */ public function getProduct() @@ -129,6 +135,8 @@ public function getProduct() } /** + * Get extended element. + * * @param string $switchAttributeCode * @return \Magento\Framework\Data\Form\Element\Select * @throws \Magento\Framework\Exception\LocalizedException diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/Items/Renderer.php b/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/Items/Renderer.php index 23fc2026ab11..82a0086ad67e 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/Items/Renderer.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/Items/Renderer.php @@ -100,6 +100,8 @@ public function getChildren($item) } /** + * Check if item can be shipped separately + * * @param mixed $item * @return bool * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -136,6 +138,8 @@ public function isShipmentSeparately($item = null) } /** + * Check if child items calculated + * * @param mixed $item * @return bool * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -174,6 +178,8 @@ public function isChildCalculated($item = null) } /** + * Retrieve selection attributes values + * * @param mixed $item * @return mixed|null */ @@ -191,6 +197,8 @@ public function getSelectionAttributes($item) } /** + * Retrieve order item options array + * * @return array */ public function getOrderOptions() @@ -212,6 +220,8 @@ public function getOrderOptions() } /** + * Retrieve order item + * * @return mixed */ public function getOrderItem() @@ -223,6 +233,8 @@ public function getOrderItem() } /** + * Get html info for item + * * @param mixed $item * @return string */ @@ -245,6 +257,8 @@ public function getValueHtml($item) } /** + * Check if we can show price info for this item + * * @param object $item * @return bool */ diff --git a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle.php b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle.php index 542f170da8c3..fa488b073f51 100644 --- a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle.php +++ b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle.php @@ -7,6 +7,7 @@ use Magento\Bundle\Model\Option; use Magento\Catalog\Model\Product; +use Magento\Framework\DataObject; /** * Catalog bundle product info block @@ -56,6 +57,11 @@ class Bundle extends \Magento\Catalog\Block\Product\View\AbstractView */ private $catalogRuleProcessor; + /** + * @var array + */ + private $optionsPosition = []; + /** * @param \Magento\Catalog\Block\Product\Context $context * @param \Magento\Framework\Stdlib\ArrayUtils $arrayUtils @@ -86,6 +92,8 @@ public function __construct( } /** + * Return catalog rule processor or creates processor if it does not exist + * * @deprecated 100.2.0 * @return \Magento\CatalogRule\Model\ResourceModel\Product\CollectionProcessor */ @@ -101,6 +109,7 @@ private function getCatalogRuleProcessor() /** * Returns the bundle product options + * * Will return cached options data if the product options are already initialized * In a case when $stripSelection parameter is true will reload stored bundle selections collection from DB * @@ -135,6 +144,8 @@ public function getOptions($stripSelection = false) } /** + * Return true if product has options + * * @return bool */ public function hasOptions() @@ -150,7 +161,6 @@ public function hasOptions() * Returns JSON encoded config to be used in JS scripts * * @return string - * */ public function getJsonConfig() { @@ -161,7 +171,7 @@ public function getJsonConfig() $defaultValues = []; $preConfiguredFlag = $currentProduct->hasPreconfiguredValues(); - /** @var \Magento\Framework\DataObject|null $preConfiguredValues */ + /** @var DataObject|null $preConfiguredValues */ $preConfiguredValues = $preConfiguredFlag ? $currentProduct->getPreconfiguredValues() : null; $position = 0; @@ -172,19 +182,25 @@ public function getJsonConfig() } $optionId = $optionItem->getId(); $options[$optionId] = $this->getOptionItemData($optionItem, $currentProduct, $position); + $this->optionsPosition[$position] = $optionId; // Add attribute default value (if set) if ($preConfiguredFlag) { $configValue = $preConfiguredValues->getData('bundle_option/' . $optionId); if ($configValue) { $defaultValues[$optionId] = $configValue; + $configQty = $preConfiguredValues->getData('bundle_option_qty/' . $optionId); + if ($configQty) { + $options[$optionId]['selections'][$configValue]['qty'] = $configQty; + } } + $options = $this->processOptions($optionId, $options, $preConfiguredValues); } $position++; } $config = $this->getConfigData($currentProduct, $options); - $configObj = new \Magento\Framework\DataObject( + $configObj = new DataObject( [ 'config' => $config, ] @@ -370,6 +386,7 @@ private function getConfigData(Product $product, array $options) $config = [ 'options' => $options, 'selected' => $this->selectedOptions, + 'positions' => $this->optionsPosition, 'bundleId' => $product->getId(), 'priceFormat' => $this->localeFormat->getPriceFormat(), 'prices' => [ @@ -388,4 +405,30 @@ private function getConfigData(Product $product, array $options) ]; return $config; } + + /** + * Set preconfigured quantities and selections to options. + * + * @param string $optionId + * @param array $options + * @param DataObject $preConfiguredValues + * @return array + */ + private function processOptions(string $optionId, array $options, DataObject $preConfiguredValues) + { + $preConfiguredQtys = $preConfiguredValues->getData("bundle_option_qty/${optionId}") ?? []; + $selections = $options[$optionId]['selections']; + array_walk($selections, function (&$selection, $selectionId) use ($preConfiguredQtys) { + if (is_array($preConfiguredQtys) && isset($preConfiguredQtys[$selectionId])) { + $selection['qty'] = $preConfiguredQtys[$selectionId]; + } else { + if ((int)$preConfiguredQtys > 0) { + $selection['qty'] = $preConfiguredQtys; + } + } + }); + $options[$optionId]['selections'] = $selections; + + return $options; + } } diff --git a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option.php b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option.php index 5d326e7c01d1..7c5a64ca0232 100644 --- a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option.php +++ b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle/Option.php @@ -6,6 +6,8 @@ namespace Magento\Bundle\Block\Catalog\Product\View\Type\Bundle; +use Magento\Catalog\Model\Product; + /** * Bundle option renderer * @api @@ -167,7 +169,9 @@ protected function _getSelectedOptions() */ protected function assignSelection(\Magento\Bundle\Model\Option $option, $selectionId) { - if ($selectionId && $option->getSelectionById($selectionId)) { + if (is_array($selectionId)) { + $this->_selectedOptions = $selectionId; + } elseif ($selectionId && $option->getSelectionById($selectionId)) { $this->_selectedOptions = $selectionId; } elseif (!$option->getRequired()) { $this->_selectedOptions = 'None'; @@ -177,7 +181,7 @@ protected function assignSelection(\Magento\Bundle\Model\Option $option, $select /** * Define if selection is selected * - * @param \Magento\Catalog\Model\Product $selection + * @param Product $selection * @return bool */ public function isSelected($selection) @@ -217,7 +221,7 @@ protected function _getSelectedQty() /** * Get product model * - * @return \Magento\Catalog\Model\Product + * @return Product */ public function getProduct() { @@ -228,7 +232,9 @@ public function getProduct() } /** - * @param \Magento\Catalog\Model\Product $selection + * Get bundle option price title. + * + * @param Product $selection * @param bool $includeContainer * @return string */ @@ -250,7 +256,7 @@ public function getSelectionQtyTitlePrice($selection, $includeContainer = true) /** * Get price for selection product * - * @param \Magento\Catalog\Model\Product $selection + * @param Product $selection * @return int|float */ public function getSelectionPrice($selection) @@ -273,7 +279,7 @@ public function getSelectionPrice($selection) /** * Get title price for selection product * - * @param \Magento\Catalog\Model\Product $selection + * @param Product $selection * @param bool $includeContainer * @return string */ @@ -295,7 +301,7 @@ public function getSelectionTitlePrice($selection, $includeContainer = true) */ public function setValidationContainer($elementId, $containerId) { - return; + return ''; } /** @@ -314,7 +320,7 @@ public function setOption(\Magento\Bundle\Model\Option $option) /** * Format price string * - * @param \Magento\Catalog\Model\Product $selection + * @param Product $selection * @param bool $includeContainer * @return string */ diff --git a/app/code/Magento/Bundle/Block/Checkout/Cart/Item/Renderer.php b/app/code/Magento/Bundle/Block/Checkout/Cart/Item/Renderer.php index c75ebc700603..863f27322569 100644 --- a/app/code/Magento/Bundle/Block/Checkout/Cart/Item/Renderer.php +++ b/app/code/Magento/Bundle/Block/Checkout/Cart/Item/Renderer.php @@ -69,6 +69,7 @@ public function __construct( /** * Overloaded method for getting list of bundle options + * * Caches result in quote item, because it can be used in cart 'recent view' and on same page in cart checkout * * @return array @@ -88,7 +89,7 @@ public function getMessages() $messages = []; $quoteItem = $this->getItem(); - // Add basic messages occuring during this page load + // Add basic messages occurring during this page load $baseMessages = $quoteItem->getMessage(false); if ($baseMessages) { foreach ($baseMessages as $message) { diff --git a/app/code/Magento/Bundle/Block/DataProviders/OptionPriceRenderer.php b/app/code/Magento/Bundle/Block/DataProviders/OptionPriceRenderer.php new file mode 100644 index 000000000000..058b3a981b52 --- /dev/null +++ b/app/code/Magento/Bundle/Block/DataProviders/OptionPriceRenderer.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Bundle\Block\DataProviders; + +use Magento\Catalog\Model\Product; +use Magento\Catalog\Pricing\Price\TierPrice; +use Magento\Framework\Pricing\Render; +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\Framework\View\LayoutInterface; + +/** + * Provides additional data for bundle options + */ +class OptionPriceRenderer implements ArgumentInterface +{ + /** + * Parent layout of the block + * + * @var LayoutInterface + */ + private $layout; + + /** + * @param LayoutInterface $layout + */ + public function __construct(LayoutInterface $layout) + { + $this->layout = $layout; + } + + /** + * Format tier price string + * + * @param Product $selection + * @param array $arguments + * @return string + */ + public function renderTierPrice(Product $selection, array $arguments = []): string + { + if (!array_key_exists('zone', $arguments)) { + $arguments['zone'] = Render::ZONE_ITEM_OPTION; + } + + $priceHtml = ''; + + /** @var Render $priceRender */ + $priceRender = $this->layout->getBlock('product.price.render.default'); + if ($priceRender !== false) { + $priceHtml = $priceRender->render( + TierPrice::PRICE_CODE, + $selection, + $arguments + ); + } + + return $priceHtml; + } +} diff --git a/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/Crosssell.php b/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/Crosssell.php index ba7b9b5fcb59..08fac66c4a4f 100644 --- a/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/Crosssell.php +++ b/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/Crosssell.php @@ -6,6 +6,16 @@ */ namespace Magento\Bundle\Controller\Adminhtml\Bundle\Product\Edit; -class Crosssell extends \Magento\Catalog\Controller\Adminhtml\Product\Crosssell +use Magento\Catalog\Controller\Adminhtml\Product\Crosssell as CatalogCrossel; +use Magento\Framework\App\Action\HttpPostActionInterface; + +/** + * Class Crosssell + * + * @package Magento\Bundle\Controller\Adminhtml\Bundle\Product\Edit + * @deprecated Not used since cross-sell products grid moved to UI components. + * @see Magento_Catalog::view/adminhtml/ui_component/crosssell_product_listing.xml + */ +class Crosssell extends CatalogCrossel implements HttpPostActionInterface { } diff --git a/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/CrosssellGrid.php b/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/CrosssellGrid.php index 87550d768ca8..b301d6ee2fea 100644 --- a/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/CrosssellGrid.php +++ b/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/CrosssellGrid.php @@ -6,6 +6,16 @@ */ namespace Magento\Bundle\Controller\Adminhtml\Bundle\Product\Edit; -class CrosssellGrid extends \Magento\Catalog\Controller\Adminhtml\Product\CrosssellGrid +use Magento\Catalog\Controller\Adminhtml\Product\CrosssellGrid as CatalogCrosssellGrid; +use Magento\Framework\App\Action\HttpPostActionInterface; + +/** + * Class CrosssellGrid + * + * @package Magento\Bundle\Controller\Adminhtml\Bundle\Product\Edit + * @deprecated Not used since cross-sell products grid moved to UI components. + * @see Magento_Catalog::view/adminhtml/ui_component/crosssell_product_listing.xml + */ +class CrosssellGrid extends CatalogCrosssellGrid implements HttpPostActionInterface { } diff --git a/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/Related.php b/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/Related.php index 58f464fba3af..7534cfddcaad 100644 --- a/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/Related.php +++ b/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/Related.php @@ -6,6 +6,16 @@ */ namespace Magento\Bundle\Controller\Adminhtml\Bundle\Product\Edit; -class Related extends \Magento\Catalog\Controller\Adminhtml\Product\Related +use Magento\Catalog\Controller\Adminhtml\Product\Related as CatalogRelated; +use Magento\Framework\App\Action\HttpPostActionInterface; + +/** + * Class Related + * + * @package Magento\Bundle\Controller\Adminhtml\Bundle\Product\Edit + * @deprecated Not used since related products grid moved to UI components. + * @see Magento_Catalog::view/adminhtml/ui_component/related_product_listing.xml + */ +class Related extends CatalogRelated implements HttpPostActionInterface { } diff --git a/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/RelatedGrid.php b/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/RelatedGrid.php index a27efe8bc2b6..e0fc9226663f 100644 --- a/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/RelatedGrid.php +++ b/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/RelatedGrid.php @@ -6,6 +6,16 @@ */ namespace Magento\Bundle\Controller\Adminhtml\Bundle\Product\Edit; -class RelatedGrid extends \Magento\Catalog\Controller\Adminhtml\Product\RelatedGrid +use Magento\Catalog\Controller\Adminhtml\Product\RelatedGrid as CatalogRelatedGrid; +use Magento\Framework\App\Action\HttpPostActionInterface; + +/** + * Class RelatedGrid + * + * @package Magento\Bundle\Controller\Adminhtml\Bundle\Product\Edit + * @deprecated Not used since related products grid moved to UI components. + * @see Magento_Catalog::view/adminhtml/ui_component/related_product_listing.xml + */ +class RelatedGrid extends CatalogRelatedGrid implements HttpPostActionInterface { } diff --git a/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/Upsell.php b/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/Upsell.php index d0a6343569ee..239b13970e69 100644 --- a/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/Upsell.php +++ b/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/Upsell.php @@ -6,6 +6,16 @@ */ namespace Magento\Bundle\Controller\Adminhtml\Bundle\Product\Edit; -class Upsell extends \Magento\Catalog\Controller\Adminhtml\Product\Upsell +use Magento\Catalog\Controller\Adminhtml\Product\Upsell as CatalogUpsell; +use Magento\Framework\App\Action\HttpPostActionInterface; + +/** + * Class Upsell + * + * @package Magento\Bundle\Controller\Adminhtml\Bundle\Product\Edit + * @deprecated Not used since upsell products grid moved to UI components. + * @see Magento_Catalog::view/adminhtml/ui_component/upsell_product_listing.xml + */ +class Upsell extends CatalogUpsell implements HttpPostActionInterface { } diff --git a/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/UpsellGrid.php b/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/UpsellGrid.php index 74c87be5b710..ed3312d3b073 100644 --- a/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/UpsellGrid.php +++ b/app/code/Magento/Bundle/Controller/Adminhtml/Bundle/Product/Edit/UpsellGrid.php @@ -6,6 +6,13 @@ */ namespace Magento\Bundle\Controller\Adminhtml\Bundle\Product\Edit; +/** + * Class UpsellGrid + * + * @package Magento\Bundle\Controller\Adminhtml\Bundle\Product\Edit + * @deprecated Not used since upsell products grid moved to UI components. + * @see Magento_Catalog::view/adminhtml/ui_component/upsell_product_listing.xml + */ class UpsellGrid extends \Magento\Catalog\Controller\Adminhtml\Product\UpsellGrid { } diff --git a/app/code/Magento/Bundle/Model/OptionRepository.php b/app/code/Magento/Bundle/Model/OptionRepository.php index 59e658b08df2..0b96ea8d5b78 100644 --- a/app/code/Magento/Bundle/Model/OptionRepository.php +++ b/app/code/Magento/Bundle/Model/OptionRepository.php @@ -90,7 +90,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function get($sku, $optionId) { @@ -106,23 +106,24 @@ public function get($sku, $optionId) $productLinks = $this->linkManagement->getChildren($product->getSku(), $optionId); - /** @var \Magento\Bundle\Api\Data\OptionInterface $option */ + /** @var \Magento\Bundle\Api\Data\OptionInterface $optionDataObject */ $optionDataObject = $this->optionFactory->create(); $this->dataObjectHelper->populateWithArray( $optionDataObject, $option->getData(), \Magento\Bundle\Api\Data\OptionInterface::class ); - $optionDataObject->setOptionId($option->getId()) - ->setTitle($option->getTitle() === null ? $option->getDefaultTitle() : $option->getTitle()) - ->setSku($product->getSku()) - ->setProductLinks($productLinks); + + $optionDataObject->setOptionId($option->getId()); + $optionDataObject->setTitle($option->getTitle() === null ? $option->getDefaultTitle() : $option->getTitle()); + $optionDataObject->setSku($product->getSku()); + $optionDataObject->setProductLinks($productLinks); return $optionDataObject; } /** - * {@inheritdoc} + * @inheritdoc */ public function getList($sku) { @@ -131,6 +132,8 @@ public function getList($sku) } /** + * Return list of product options + * * @param ProductInterface $product * @return \Magento\Bundle\Api\Data\OptionInterface[] */ @@ -140,7 +143,7 @@ public function getListByProduct(ProductInterface $product) } /** - * {@inheritdoc} + * @inheritdoc */ public function delete(\Magento\Bundle\Api\Data\OptionInterface $option) { @@ -156,20 +159,19 @@ public function delete(\Magento\Bundle\Api\Data\OptionInterface $option) } /** - * {@inheritdoc} + * @inheritdoc */ public function deleteById($sku, $optionId) { - $product = $this->getProduct($sku); - $optionCollection = $this->type->getOptionsCollection($product); - $optionCollection->setIdFilter($optionId); - $hasBeenDeleted = $this->delete($optionCollection->getFirstItem()); + /** @var \Magento\Bundle\Api\Data\OptionInterface $option */ + $option = $this->get($sku, $optionId); + $hasBeenDeleted = $this->delete($option); return $hasBeenDeleted; } /** - * {@inheritdoc} + * @inheritdoc */ public function save( \Magento\Catalog\Api\Data\ProductInterface $product, @@ -189,6 +191,9 @@ public function save( * @param \Magento\Catalog\Api\Data\ProductInterface $product * @param \Magento\Bundle\Api\Data\OptionInterface $option * @return $this + * @throws InputException + * @throws NoSuchEntityException + * @throws \Magento\Framework\Exception\CouldNotSaveException */ protected function updateOptionSelection( \Magento\Catalog\Api\Data\ProductInterface $product, @@ -228,9 +233,12 @@ protected function updateOptionSelection( } /** + * Retrieve product by SKU + * * @param string $sku * @return \Magento\Catalog\Api\Data\ProductInterface - * @throws \Magento\Framework\Exception\InputException + * @throws InputException + * @throws NoSuchEntityException */ private function getProduct($sku) { diff --git a/app/code/Magento/Bundle/Model/Plugin/Frontend/Product.php b/app/code/Magento/Bundle/Model/Plugin/Frontend/Product.php new file mode 100644 index 000000000000..499f0cd2ca9c --- /dev/null +++ b/app/code/Magento/Bundle/Model/Plugin/Frontend/Product.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Bundle\Model\Plugin\Frontend; + +use Magento\Bundle\Model\Product\Type; +use Magento\Catalog\Model\Product as CatalogProduct; + +/** + * Add child identities to product identities on storefront. + */ +class Product +{ + /** + * @var Type + */ + private $type; + + /** + * @param Type $type + */ + public function __construct(Type $type) + { + $this->type = $type; + } + + /** + * Add child identities to product identities + * + * @param CatalogProduct $product + * @param array $identities + * @return array + */ + public function afterGetIdentities(CatalogProduct $product, array $identities): array + { + foreach ($this->type->getChildrenIds($product->getEntityId()) as $childIds) { + foreach ($childIds as $childId) { + $identities[] = CatalogProduct::CACHE_TAG . '_' . $childId; + } + } + + return array_unique($identities); + } +} diff --git a/app/code/Magento/Bundle/Model/Product/Price.php b/app/code/Magento/Bundle/Model/Product/Price.php index 00b6b2d7a3f5..0e75ad2dc828 100644 --- a/app/code/Magento/Bundle/Model/Product/Price.php +++ b/app/code/Magento/Bundle/Model/Product/Price.php @@ -11,8 +11,11 @@ use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory; /** + * Bundle product type price model + * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @since 100.0.2 */ class Price extends \Magento\Catalog\Model\Product\Type\Price @@ -180,9 +183,9 @@ protected function getBundleSelectionIds(\Magento\Catalog\Model\Product $product /** * Get product final price * - * @param float $qty - * @param \Magento\Catalog\Model\Product $product - * @return float + * @param float $qty + * @param \Magento\Catalog\Model\Product $product + * @return float */ public function getFinalPrice($qty, $product) { @@ -207,9 +210,9 @@ public function getFinalPrice($qty, $product) * Returns final price of a child product * * @param \Magento\Catalog\Model\Product $product - * @param float $productQty + * @param float $productQty * @param \Magento\Catalog\Model\Product $childProduct - * @param float $childProductQty + * @param float $childProductQty * @return float */ public function getChildFinalPrice($product, $productQty, $childProduct, $childProductQty) @@ -220,10 +223,10 @@ public function getChildFinalPrice($product, $productQty, $childProduct, $childP /** * Retrieve Price considering tier price * - * @param \Magento\Catalog\Model\Product $product - * @param string|null $which - * @param bool|null $includeTax - * @param bool $takeTierPrice + * @param \Magento\Catalog\Model\Product $product + * @param string|null $which + * @param bool|null $includeTax + * @param bool $takeTierPrice * @return float|array * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) @@ -255,67 +258,68 @@ public function getTotalPrices($product, $which = null, $includeTax = null, $tak foreach ($options as $option) { /* @var $option \Magento\Bundle\Model\Option */ $selections = $option->getSelections(); - if ($selections) { - $selectionMinimalPrices = []; - $selectionMaximalPrices = []; - - foreach ($option->getSelections() as $selection) { - /* @var $selection \Magento\Bundle\Model\Selection */ - if (!$selection->isSalable()) { - /** - * @todo CatalogInventory Show out of stock Products - */ - continue; - } - - $qty = $selection->getSelectionQty(); - - $item = $product->getPriceType() == self::PRICE_TYPE_FIXED ? $product : $selection; - - $selectionMinimalPrices[] = $this->_catalogData->getTaxPrice( - $item, - $this->getSelectionFinalTotalPrice( - $product, - $selection, - 1, - $qty, - true, - $takeTierPrice - ), - $includeTax - ); - $selectionMaximalPrices[] = $this->_catalogData->getTaxPrice( - $item, - $this->getSelectionFinalTotalPrice( - $product, - $selection, - 1, - null, - true, - $takeTierPrice - ), - $includeTax + if (empty($selections)) { + continue; + } + $selectionMinimalPrices = []; + $selectionMaximalPrices = []; + + foreach ($option->getSelections() as $selection) { + /* @var $selection \Magento\Bundle\Model\Selection */ + if (!$selection->isSalable()) { + /** + * @todo CatalogInventory Show out of stock Products + */ + continue; + } + + $qty = $selection->getSelectionQty(); + + $item = $product->getPriceType() == self::PRICE_TYPE_FIXED ? $product : $selection; + + $selectionMinimalPrices[] = $this->_catalogData->getTaxPrice( + $item, + $this->getSelectionFinalTotalPrice( + $product, + $selection, + 1, + $qty, + true, + $takeTierPrice + ), + $includeTax + ); + $selectionMaximalPrices[] = $this->_catalogData->getTaxPrice( + $item, + $this->getSelectionFinalTotalPrice( + $product, + $selection, + 1, + null, + true, + $takeTierPrice + ), + $includeTax + ); + } + + if (count($selectionMinimalPrices)) { + $selMinPrice = min($selectionMinimalPrices); + if ($option->getRequired()) { + $minimalPrice += $selMinPrice; + $minPriceFounded = true; + } elseif (true !== $minPriceFounded) { + $selMinPrice += $minimalPrice; + $minPriceFounded = false === $minPriceFounded ? $selMinPrice : min( + $minPriceFounded, + $selMinPrice ); } - if (count($selectionMinimalPrices)) { - $selMinPrice = min($selectionMinimalPrices); - if ($option->getRequired()) { - $minimalPrice += $selMinPrice; - $minPriceFounded = true; - } elseif (true !== $minPriceFounded) { - $selMinPrice += $minimalPrice; - $minPriceFounded = false === $minPriceFounded ? $selMinPrice : min( - $minPriceFounded, - $selMinPrice - ); - } - - if ($option->isMultiSelection()) { - $maximalPrice += array_sum($selectionMaximalPrices); - } else { - $maximalPrice += max($selectionMaximalPrices); - } + if ($option->isMultiSelection()) { + $maximalPrice += array_sum($selectionMaximalPrices); + } else { + $maximalPrice += max($selectionMaximalPrices); } } } @@ -338,23 +342,25 @@ public function getTotalPrices($product, $which = null, $includeTax = null, $tak $prices[] = $valuePrice; } - if (count($prices)) { - if ($customOption->getIsRequire()) { - $minimalPrice += $this->_catalogData->getTaxPrice($product, min($prices), $includeTax); - } - - $multiTypes = [ - \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX, - \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_MULTIPLE, - ]; - - if (in_array($customOption->getType(), $multiTypes)) { - $maximalValue = array_sum($prices); - } else { - $maximalValue = max($prices); - } - $maximalPrice += $this->_catalogData->getTaxPrice($product, $maximalValue, $includeTax); + if (empty($prices)) { + continue; + } + + if ($customOption->getIsRequire()) { + $minimalPrice += $this->_catalogData->getTaxPrice($product, min($prices), $includeTax); + } + + $multiTypes = [ + \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX, + \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_MULTIPLE, + ]; + + if (in_array($customOption->getType(), $multiTypes)) { + $maximalValue = array_sum($prices); + } else { + $maximalValue = max($prices); } + $maximalPrice += $this->_catalogData->getTaxPrice($product, $maximalValue, $includeTax); } else { $valuePrice = $customOption->getPrice(true); @@ -402,8 +408,8 @@ public function getOptions($product) * * @param \Magento\Catalog\Model\Product $bundleProduct * @param \Magento\Catalog\Model\Product $selectionProduct - * @param float|null $selectionQty - * @param null|bool $multiplyQty Whether to multiply selection's price by its quantity + * @param float|null $selectionQty + * @param null|bool $multiplyQty Whether to multiply selection's price by its quantity * @return float * * @see \Magento\Bundle\Model\Product\Price::getSelectionFinalTotalPrice() @@ -418,7 +424,7 @@ public function getSelectionPrice($bundleProduct, $selectionProduct, $selectionQ * * @param \Magento\Catalog\Model\Product $bundleProduct * @param \Magento\Catalog\Model\Product $selectionProduct - * @param float $qty + * @param float $qty * @return float */ public function getSelectionPreFinalPrice($bundleProduct, $selectionProduct, $qty = null) @@ -427,15 +433,14 @@ public function getSelectionPreFinalPrice($bundleProduct, $selectionProduct, $qt } /** - * Calculate final price of selection - * with take into account tier price + * Calculate final price of selection with take into account tier price * - * @param \Magento\Catalog\Model\Product $bundleProduct - * @param \Magento\Catalog\Model\Product $selectionProduct - * @param float $bundleQty - * @param float $selectionQty - * @param bool $multiplyQty - * @param bool $takeTierPrice + * @param \Magento\Catalog\Model\Product $bundleProduct + * @param \Magento\Catalog\Model\Product $selectionProduct + * @param float $bundleQty + * @param float $selectionQty + * @param bool $multiplyQty + * @param bool $takeTierPrice * @return float */ public function getSelectionFinalTotalPrice( @@ -454,7 +459,11 @@ public function getSelectionFinalTotalPrice( } if ($bundleProduct->getPriceType() == self::PRICE_TYPE_DYNAMIC) { - $price = $selectionProduct->getFinalPrice($takeTierPrice ? $selectionQty : 1); + $totalQty = $bundleQty * $selectionQty; + if (!$takeTierPrice || $totalQty === 0) { + $totalQty = 1; + } + $price = $selectionProduct->getFinalPrice($totalQty); } else { if ($selectionProduct->getSelectionPriceType()) { // percent @@ -485,10 +494,10 @@ public function getSelectionFinalTotalPrice( /** * Apply tier price for bundle * - * @param \Magento\Catalog\Model\Product $product - * @param float $qty - * @param float $finalPrice - * @return float + * @param \Magento\Catalog\Model\Product $product + * @param float $qty + * @param float $finalPrice + * @return float */ protected function _applyTierPrice($product, $qty, $finalPrice) { @@ -509,9 +518,9 @@ protected function _applyTierPrice($product, $qty, $finalPrice) /** * Get product tier price by qty * - * @param float $qty - * @param \Magento\Catalog\Model\Product $product - * @return float|array + * @param float $qty + * @param \Magento\Catalog\Model\Product $product + * @return float|array * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -605,11 +614,11 @@ public function getTierPrice($qty, $product) /** * Calculate and apply special price * - * @param float $finalPrice - * @param float $specialPrice + * @param float $finalPrice + * @param float $specialPrice * @param string $specialPriceFrom * @param string $specialPriceTo - * @param mixed $store + * @param mixed $store * @return float */ public function calculateSpecialPrice( @@ -634,7 +643,7 @@ public function calculateSpecialPrice( * * @param /Magento/Catalog/Model/Product $bundleProduct * @param float|string $price - * @param int $bundleQty + * @param int $bundleQty * @return float */ public function getLowestPrice($bundleProduct, $price, $bundleQty = 1) diff --git a/app/code/Magento/Bundle/Model/Product/SaveHandler.php b/app/code/Magento/Bundle/Model/Product/SaveHandler.php index fc215aa6b8e2..99e8188146bb 100644 --- a/app/code/Magento/Bundle/Model/Product/SaveHandler.php +++ b/app/code/Magento/Bundle/Model/Product/SaveHandler.php @@ -58,6 +58,8 @@ public function __construct( } /** + * Perform action on Bundle product relation/extension attribute + * * @param object $entity * @param array $arguments * @@ -83,7 +85,7 @@ public function execute($entity, $arguments = []) : []; if (!$entity->getCopyFromView()) { - $this->processRemovedOptions($entity->getSku(), $existingOptionsIds, $optionIds); + $this->processRemovedOptions($entity, $existingOptionsIds, $optionIds); $newOptionsIds = array_diff($optionIds, $existingOptionsIds); $this->saveOptions($entity, $bundleProductOptions, $newOptionsIds); } else { @@ -96,6 +98,8 @@ public function execute($entity, $arguments = []) } /** + * Remove option product links + * * @param string $entitySku * @param \Magento\Bundle\Api\Data\OptionInterface $option * @return void @@ -154,16 +158,19 @@ private function getOptionIds(array $options): array /** * Removes old options that no longer exists. * - * @param string $entitySku + * @param ProductInterface $entity * @param array $existingOptionsIds * @param array $optionIds * @return void */ - private function processRemovedOptions(string $entitySku, array $existingOptionsIds, array $optionIds): void + private function processRemovedOptions(ProductInterface $entity, array $existingOptionsIds, array $optionIds): void { + $metadata = $this->metadataPool->getMetadata(ProductInterface::class); + $parentId = $entity->getData($metadata->getLinkField()); foreach (array_diff($existingOptionsIds, $optionIds) as $optionId) { - $option = $this->optionRepository->get($entitySku, $optionId); - $this->removeOptionLinks($entitySku, $option); + $option = $this->optionRepository->get($entity->getSku(), $optionId); + $option->setParentId($parentId); + $this->removeOptionLinks($entity->getSku(), $option); $this->optionRepository->delete($option); } } diff --git a/app/code/Magento/Bundle/Model/Product/Type.php b/app/code/Magento/Bundle/Model/Product/Type.php index 17ecba545efa..2dc519dbf154 100644 --- a/app/code/Magento/Bundle/Model/Product/Type.php +++ b/app/code/Magento/Bundle/Model/Product/Type.php @@ -6,13 +6,14 @@ namespace Magento\Bundle\Model\Product; -use Magento\Framework\App\ObjectManager; +use Magento\Bundle\Model\ResourceModel\Selection\Collection as Selections; +use Magento\Bundle\Model\ResourceModel\Selection\Collection\FilterApplier as SelectionCollectionFilterApplier; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Framework\Serialize\Serializer\Json; -use Magento\Framework\EntityManager\MetadataPool; -use Magento\Bundle\Model\ResourceModel\Selection\Collection\FilterApplier as SelectionCollectionFilterApplier; -use Magento\Bundle\Model\ResourceModel\Selection\Collection as Selections; +use Magento\Framework\Stdlib\ArrayUtils; /** * Bundle Type Model @@ -160,6 +161,11 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType */ private $selectionCollectionFilterApplier; + /** + * @var ArrayUtils + */ + private $arrayUtility; + /** * @param \Magento\Catalog\Model\Product\Option $catalogProductOption * @param \Magento\Eav\Model\Config $eavConfig @@ -185,6 +191,7 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType * @param \Magento\Framework\Serialize\Serializer\Json $serializer * @param MetadataPool|null $metadataPool * @param SelectionCollectionFilterApplier|null $selectionCollectionFilterApplier + * @param ArrayUtils|null $arrayUtility * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -212,7 +219,8 @@ public function __construct( \Magento\CatalogInventory\Api\StockStateInterface $stockState, Json $serializer = null, MetadataPool $metadataPool = null, - SelectionCollectionFilterApplier $selectionCollectionFilterApplier = null + SelectionCollectionFilterApplier $selectionCollectionFilterApplier = null, + ArrayUtils $arrayUtility = null ) { $this->_catalogProduct = $catalogProduct; $this->_catalogData = $catalogData; @@ -232,6 +240,7 @@ public function __construct( $this->selectionCollectionFilterApplier = $selectionCollectionFilterApplier ?: ObjectManager::getInstance()->get(SelectionCollectionFilterApplier::class); + $this->arrayUtility= $arrayUtility ?: ObjectManager::getInstance()->get(ArrayUtils::class); parent::__construct( $catalogProductOption, @@ -308,8 +317,11 @@ public function getSku($product) $selectionIds = $this->serializer->unserialize($customOption->getValue()); if (!empty($selectionIds)) { $selections = $this->getSelectionsByIds($selectionIds, $product); - foreach ($selections->getItems() as $selection) { - $skuParts[] = $selection->getSku(); + foreach ($selectionIds as $selectionId) { + $entity = $selections->getItemByColumnValue('selection_id', $selectionId); + if (isset($entity) && $entity->getEntityId()) { + $skuParts[] = $entity->getSku(); + } } } } @@ -537,7 +549,7 @@ public function updateQtyOption($options, \Magento\Framework\DataObject $option, foreach ($options as $quoteItemOption) { if ($quoteItemOption->getCode() == 'selection_qty_' . $selection->getSelectionId()) { if ($optionUpdateFlag) { - $quoteItemOption->setValue(intval($quoteItemOption->getValue())); + $quoteItemOption->setValue((int) $quoteItemOption->getValue()); } else { $quoteItemOption->setValue($value); } @@ -559,7 +571,7 @@ public function updateQtyOption($options, \Magento\Framework\DataObject $option, */ public function prepareQuoteItemQty($qty, $product) { - return intval($qty); + return (int) $qty; } /** @@ -625,6 +637,7 @@ public function isSalable($product) /** * Prepare product and its configuration to be added to some products list. + * * Perform standard preparation process and then prepare of bundle selections options. * * @param \Magento\Framework\DataObject $buyRequest @@ -669,7 +682,7 @@ protected function _prepareProduct(\Magento\Framework\DataObject $buyRequest, $p $options ); - $selectionIds = $this->multiToFlatArray($options); + $selectionIds = array_values($this->arrayUtility->flatten($options)); // If product has not been configured yet then $selections array should be empty if (!empty($selectionIds)) { $selections = $this->getSelectionsByIds($selectionIds, $product); @@ -733,9 +746,9 @@ protected function _prepareProduct(\Magento\Framework\DataObject $buyRequest, $p * for selection (not for all bundle) */ $price = $product->getPriceModel() - ->getSelectionFinalTotalPrice($product, $selection, 0, $qty); + ->getSelectionFinalTotalPrice($product, $selection, 0, 1); $attributes = [ - 'price' => $this->priceCurrency->convert($price), + 'price' => $price, 'qty' => $qty, 'option_label' => $selection->getOption() ->getTitle(), @@ -790,6 +803,8 @@ protected function _prepareProduct(\Magento\Framework\DataObject $buyRequest, $p } /** + * Cast array values to int + * * @param array $array * @return int[]|int[][] */ @@ -808,24 +823,6 @@ private function recursiveIntval(array $array) return $array; } - /** - * @param array $array - * @return int[] - */ - private function multiToFlatArray(array $array) - { - $flatArray = []; - foreach ($array as $key => $value) { - if (is_array($value)) { - $flatArray = array_merge($flatArray, $this->multiToFlatArray($value)); - } else { - $flatArray[$key] = $value; - } - } - - return $flatArray; - } - /** * Retrieve message for specify option(s) * @@ -920,8 +917,7 @@ public function getOptionsByIds($optionIds, $product) } /** - * Prepare additional options/information for order item which will be - * created from this product + * Prepare additional options/information for order item which will be created from this product * * @param \Magento\Catalog\Model\Product $product * @return array @@ -987,6 +983,7 @@ public function getOrderOptions($product) /** * Sort selections method for usort function + * * Sort selections by option position, selection position and selection id * * @param \Magento\Catalog\Model\Product $firstItem @@ -1009,10 +1006,8 @@ public function shakeSelections($firstItem, $secondItem) $secondItem->getPosition(), $secondItem->getSelectionId(), ]; - if ($aPosition == $bPosition) { - return 0; - } - return $aPosition < $bPosition ? -1 : 1; + + return $aPosition <=> $bPosition; } /** @@ -1050,6 +1045,7 @@ public function getForceChildItemQtyChanges($product) /** * Retrieve additional searchable data from type instance + * * Using based on product id and store_id data * * @param \Magento\Catalog\Model\Product $product @@ -1118,6 +1114,7 @@ public function checkProductBuyState($product) /** * Retrieve products divided into groups required to purchase + * * At least one product in each group has to be purchased * * @param \Magento\Catalog\Model\Product $product @@ -1214,6 +1211,8 @@ public function getIdentities(\Magento\Catalog\Model\Product $product) } /** + * Returns selection qty + * * @param \Magento\Framework\DataObject $selection * @param int[] $qtys * @param int $selectionOptionId @@ -1232,6 +1231,8 @@ protected function getQty($selection, $qtys, $selectionOptionId) } /** + * Returns qty + * * @param \Magento\Catalog\Model\Product $product * @param \Magento\Framework\DataObject $selection * @return float|int @@ -1249,6 +1250,8 @@ protected function getBeforeQty($product, $selection) } /** + * Validate required options + * * @param \Magento\Catalog\Model\Product $product * @param bool $isStrictProcessMode * @param \Magento\Bundle\Model\ResourceModel\Option\Collection $optionsCollection @@ -1270,6 +1273,8 @@ protected function checkIsAllRequiredOptions($product, $isStrictProcessMode, $op } /** + * Check if selection is salable + * * @param \Magento\Bundle\Model\ResourceModel\Selection\Collection $selections * @param bool $skipSaleableCheck * @param \Magento\Bundle\Model\ResourceModel\Option\Collection $optionsCollection @@ -1300,6 +1305,8 @@ protected function checkSelectionsIsSale($selections, $skipSaleableCheck, $optio } /** + * Validate result + * * @param array $_result * @return void * @throws \Magento\Framework\Exception\LocalizedException @@ -1318,6 +1325,8 @@ protected function checkIsResult($_result) } /** + * Merge selections with options + * * @param \Magento\Catalog\Model\Product\Option[] $options * @param \Magento\Framework\DataObject[] $selections * @return \Magento\Framework\DataObject[] diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php b/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php index ca6d7f103012..5b88288ff72c 100644 --- a/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php +++ b/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php @@ -151,13 +151,7 @@ protected function _construct() */ public function _afterLoad() { - parent::_afterLoad(); - if ($this->getStoreId() && $this->_items) { - foreach ($this->_items as $item) { - $item->setStoreId($this->getStoreId()); - } - } - return $this; + return parent::_afterLoad(); } /** @@ -356,7 +350,10 @@ public function addPriceFilter($product, $searchMin, $useRegularPrice = false) } /** + * Get Catalog Rule Processor. + * * @return \Magento\CatalogRule\Model\ResourceModel\Product\CollectionProcessor + * * @deprecated 100.2.0 */ private function getCatalogRuleProcessor() diff --git a/app/code/Magento/Bundle/Plugin/UpdatePriceInQuoteItemOptions.php b/app/code/Magento/Bundle/Plugin/UpdatePriceInQuoteItemOptions.php new file mode 100644 index 000000000000..d5aafb8ad2b6 --- /dev/null +++ b/app/code/Magento/Bundle/Plugin/UpdatePriceInQuoteItemOptions.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Bundle\Plugin; + +use Magento\Quote\Model\Quote\Item as OrigQuoteItem; +use Magento\Quote\Model\Quote\Item\AbstractItem; +use Magento\Framework\Serialize\SerializerInterface; + +/** + * Update prices stored in quote item options after calculating quote item's totals + */ +class UpdatePriceInQuoteItemOptions +{ + /** + * @var SerializerInterface + */ + private $serializer; + + /** + * @param SerializerInterface $serializer + */ + public function __construct(SerializerInterface $serializer) + { + $this->serializer = $serializer; + } + + /** + * Update price on quote item options level + * + * @param OrigQuoteItem $subject + * @param AbstractItem $result + * @return AbstractItem + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterCalcRowTotal(OrigQuoteItem $subject, AbstractItem $result) + { + $bundleAttributes = $result->getProduct()->getCustomOption('bundle_selection_attributes'); + if ($bundleAttributes !== null) { + $actualPrice = $result->getPrice(); + $parsedValue = $this->serializer->unserialize($bundleAttributes->getValue()); + if (is_array($parsedValue) && array_key_exists('price', $parsedValue)) { + $parsedValue['price'] = $actualPrice; + } + $bundleAttributes->setValue($this->serializer->serialize($parsedValue)); + } + + return $result; + } +} diff --git a/app/code/Magento/Bundle/Pricing/Adjustment/Calculator.php b/app/code/Magento/Bundle/Pricing/Adjustment/Calculator.php index adb0777151b9..04a6ee0bd459 100644 --- a/app/code/Magento/Bundle/Pricing/Adjustment/Calculator.php +++ b/app/code/Magento/Bundle/Pricing/Adjustment/Calculator.php @@ -198,6 +198,8 @@ protected function getSelectionAmounts(Product $bundleProduct, $searchMin, $useR } /** + * Get selection price list provider. + * * @return SelectionPriceListProviderInterface * @deprecated 100.2.0 */ @@ -281,7 +283,7 @@ public function calculateBundleAmount($basePriceValue, $bundleProduct, $selectio * @param float $basePriceValue * @param Product $bundleProduct * @param \Magento\Bundle\Pricing\Price\BundleSelectionPrice[] $selectionPriceList - * @param null|bool|string|arrayy $exclude + * @param null|bool|string|array $exclude * @return \Magento\Framework\Pricing\Amount\AmountInterface */ protected function calculateFixedBundleAmount($basePriceValue, $bundleProduct, $selectionPriceList, $exclude) diff --git a/app/code/Magento/Bundle/Pricing/Price/BundleSelectionFactory.php b/app/code/Magento/Bundle/Pricing/Price/BundleSelectionFactory.php index 927b8fbff8d8..a28d721cc9a4 100644 --- a/app/code/Magento/Bundle/Pricing/Price/BundleSelectionFactory.php +++ b/app/code/Magento/Bundle/Pricing/Price/BundleSelectionFactory.php @@ -54,7 +54,7 @@ public function create( ) { $arguments['bundleProduct'] = $bundleProduct; $arguments['saleableItem'] = $selection; - $arguments['quantity'] = $quantity ? floatval($quantity) : 1.; + $arguments['quantity'] = $quantity ? (float)$quantity : 1.; return $this->objectManager->create(self::SELECTION_CLASS_DEFAULT, $arguments); } diff --git a/app/code/Magento/Bundle/Setup/Patch/Data/UpdateBundleRelatedEntityTypes.php b/app/code/Magento/Bundle/Setup/Patch/Data/UpdateBundleRelatedEntityTypes.php new file mode 100644 index 000000000000..701def7fc13d --- /dev/null +++ b/app/code/Magento/Bundle/Setup/Patch/Data/UpdateBundleRelatedEntityTypes.php @@ -0,0 +1,204 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Bundle\Setup\Patch\Data; + +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Eav\Setup\EavSetup; +use Magento\Eav\Setup\EavSetupFactory; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; +use Magento\Framework\Setup\Patch\PatchVersionInterface; + +/** + * Class UpdateBundleRelatedEntityTypes + * + * @package Magento\Bundle\Setup\Patch + */ +class UpdateBundleRelatedEntityTypes implements DataPatchInterface, PatchVersionInterface +{ + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * @var EavSetupFactory + */ + private $eavSetupFactory; + + /** + * UpdateBundleRelatedEntityTypes constructor. + * @param ModuleDataSetupInterface $moduleDataSetup + * @param EavSetupFactory $eavSetupFactory + */ + public function __construct( + ModuleDataSetupInterface $moduleDataSetup, + \Magento\Eav\Setup\EavSetupFactory $eavSetupFactory + ) { + $this->moduleDataSetup = $moduleDataSetup; + $this->eavSetupFactory = $eavSetupFactory; + } + + /** + * @inheritdoc + */ + public function apply() + { + /** @var \Magento\Eav\Setup\EavSetup $eavSetup */ + $eavSetup = $this->eavSetupFactory->create(['setup' => $this->moduleDataSetup]); + + $attributeSetId = $eavSetup->getDefaultAttributeSetId(ProductAttributeInterface::ENTITY_TYPE_CODE); + $eavSetup->addAttributeGroup( + ProductAttributeInterface::ENTITY_TYPE_CODE, + $attributeSetId, + 'Bundle Items', + 16 + ); + $this->upgradePriceType($eavSetup); + $this->upgradeSkuType($eavSetup); + $this->upgradeWeightType($eavSetup); + $this->upgradeShipmentType($eavSetup); + } + + /** + * Upgrade Dynamic Price attribute + * + * @param EavSetup $eavSetup + * @return void + */ + private function upgradePriceType(EavSetup $eavSetup) + { + $eavSetup->updateAttribute( + ProductAttributeInterface::ENTITY_TYPE_CODE, + 'price_type', + 'frontend_input', + 'boolean', + 31 + ); + $eavSetup->updateAttribute( + ProductAttributeInterface::ENTITY_TYPE_CODE, + 'price_type', + 'frontend_label', + 'Dynamic Price' + ); + $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'price_type', 'default_value', 0); + } + + /** + * Upgrade Dynamic Sku attribute + * + * @param EavSetup $eavSetup + * @return void + */ + private function upgradeSkuType(EavSetup $eavSetup) + { + $eavSetup->updateAttribute( + ProductAttributeInterface::ENTITY_TYPE_CODE, + 'sku_type', + 'frontend_input', + 'boolean', + 21 + ); + $eavSetup->updateAttribute( + ProductAttributeInterface::ENTITY_TYPE_CODE, + 'sku_type', + 'frontend_label', + 'Dynamic SKU' + ); + $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'sku_type', 'default_value', 0); + $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'sku_type', 'is_visible', 1); + } + + /** + * Upgrade Dynamic Weight attribute + * + * @param EavSetup $eavSetup + * @return void + */ + private function upgradeWeightType(EavSetup $eavSetup) + { + $eavSetup->updateAttribute( + ProductAttributeInterface::ENTITY_TYPE_CODE, + 'weight_type', + 'frontend_input', + 'boolean', + 71 + ); + $eavSetup->updateAttribute( + ProductAttributeInterface::ENTITY_TYPE_CODE, + 'weight_type', + 'frontend_label', + 'Dynamic Weight' + ); + $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'weight_type', 'default_value', 0); + $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'weight_type', 'is_visible', 1); + } + + /** + * Upgrade Ship Bundle Items attribute + * + * @param EavSetup $eavSetup + * @return void + */ + private function upgradeShipmentType(EavSetup $eavSetup) + { + $attributeSetId = $eavSetup->getDefaultAttributeSetId(ProductAttributeInterface::ENTITY_TYPE_CODE); + $eavSetup->addAttributeToGroup( + ProductAttributeInterface::ENTITY_TYPE_CODE, + $attributeSetId, + 'Bundle Items', + 'shipment_type', + 1 + ); + $eavSetup->updateAttribute( + ProductAttributeInterface::ENTITY_TYPE_CODE, + 'shipment_type', + 'frontend_input', + 'select' + ); + $eavSetup->updateAttribute( + ProductAttributeInterface::ENTITY_TYPE_CODE, + 'shipment_type', + 'frontend_label', + 'Ship Bundle Items' + ); + $eavSetup->updateAttribute( + ProductAttributeInterface::ENTITY_TYPE_CODE, + 'shipment_type', + 'source_model', + \Magento\Bundle\Model\Product\Attribute\Source\Shipment\Type::class + ); + $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'shipment_type', 'default_value', 0); + $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'shipment_type', 'is_visible', 1); + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return [ + ApplyAttributesUpdate::class, + ]; + } + + /** + * @inheritdoc + */ + public static function getVersion() + { + return '2.0.2'; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Bundle/Setup/Patch/Data/UpdateBundleRelatedEntityTytpes.php b/app/code/Magento/Bundle/Setup/Patch/Data/UpdateBundleRelatedEntityTytpes.php deleted file mode 100644 index 44647ea76a1c..000000000000 --- a/app/code/Magento/Bundle/Setup/Patch/Data/UpdateBundleRelatedEntityTytpes.php +++ /dev/null @@ -1,204 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Bundle\Setup\Patch\Data; - -use Magento\Eav\Setup\EavSetupFactory; -use Magento\Framework\App\ResourceConnection; -use Magento\Framework\Setup\ModuleDataSetupInterface; -use Magento\Framework\Setup\Patch\DataPatchInterface; -use Magento\Framework\Setup\Patch\PatchVersionInterface; -use Magento\Catalog\Api\Data\ProductAttributeInterface; -use Magento\Eav\Setup\EavSetup; - -/** - * Class UpdateBundleRelatedEntityTytpes - * @package Magento\Bundle\Setup\Patch - */ -class UpdateBundleRelatedEntityTytpes implements DataPatchInterface, PatchVersionInterface -{ - /** - * @var ModuleDataSetupInterface - */ - private $moduleDataSetup; - - /** - * @var EavSetupFactory - */ - private $eavSetupFactory; - - /** - * UpdateBundleRelatedEntityTytpes constructor. - * @param ModuleDataSetupInterface $moduleDataSetup - * @param EavSetupFactory $eavSetupFactory - */ - public function __construct( - ModuleDataSetupInterface $moduleDataSetup, - \Magento\Eav\Setup\EavSetupFactory $eavSetupFactory - ) { - $this->moduleDataSetup = $moduleDataSetup; - $this->eavSetupFactory = $eavSetupFactory; - } - - /** - * {@inheritdoc} - */ - public function apply() - { - /** @var \Magento\Eav\Setup\EavSetup $eavSetup */ - $eavSetup = $this->eavSetupFactory->create(['setup' => $this->moduleDataSetup]); - - $attributeSetId = $eavSetup->getDefaultAttributeSetId(ProductAttributeInterface::ENTITY_TYPE_CODE); - $eavSetup->addAttributeGroup( - ProductAttributeInterface::ENTITY_TYPE_CODE, - $attributeSetId, - 'Bundle Items', - 16 - ); - $this->upgradePriceType($eavSetup); - $this->upgradeSkuType($eavSetup); - $this->upgradeWeightType($eavSetup); - $this->upgradeShipmentType($eavSetup); - } - - /** - * Upgrade Dynamic Price attribute - * - * @param EavSetup $eavSetup - * @return void - */ - private function upgradePriceType(EavSetup $eavSetup) - { - $eavSetup->updateAttribute( - ProductAttributeInterface::ENTITY_TYPE_CODE, - 'price_type', - 'frontend_input', - 'boolean', - 31 - ); - $eavSetup->updateAttribute( - ProductAttributeInterface::ENTITY_TYPE_CODE, - 'price_type', - 'frontend_label', - 'Dynamic Price' - ); - $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'price_type', 'default_value', 0); - } - - /** - * Upgrade Dynamic Sku attribute - * - * @param EavSetup $eavSetup - * @return void - */ - private function upgradeSkuType(EavSetup $eavSetup) - { - $eavSetup->updateAttribute( - ProductAttributeInterface::ENTITY_TYPE_CODE, - 'sku_type', - 'frontend_input', - 'boolean', - 21 - ); - $eavSetup->updateAttribute( - ProductAttributeInterface::ENTITY_TYPE_CODE, - 'sku_type', - 'frontend_label', - 'Dynamic SKU' - ); - $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'sku_type', 'default_value', 0); - $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'sku_type', 'is_visible', 1); - } - - /** - * Upgrade Dynamic Weight attribute - * - * @param EavSetup $eavSetup - * @return void - */ - private function upgradeWeightType(EavSetup $eavSetup) - { - $eavSetup->updateAttribute( - ProductAttributeInterface::ENTITY_TYPE_CODE, - 'weight_type', - 'frontend_input', - 'boolean', - 71 - ); - $eavSetup->updateAttribute( - ProductAttributeInterface::ENTITY_TYPE_CODE, - 'weight_type', - 'frontend_label', - 'Dynamic Weight' - ); - $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'weight_type', 'default_value', 0); - $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'weight_type', 'is_visible', 1); - } - - /** - * Upgrade Ship Bundle Items attribute - * - * @param EavSetup $eavSetup - * @return void - */ - private function upgradeShipmentType(EavSetup $eavSetup) - { - $attributeSetId = $eavSetup->getDefaultAttributeSetId(ProductAttributeInterface::ENTITY_TYPE_CODE); - $eavSetup->addAttributeToGroup( - ProductAttributeInterface::ENTITY_TYPE_CODE, - $attributeSetId, - 'Bundle Items', - 'shipment_type', - 1 - ); - $eavSetup->updateAttribute( - ProductAttributeInterface::ENTITY_TYPE_CODE, - 'shipment_type', - 'frontend_input', - 'select' - ); - $eavSetup->updateAttribute( - ProductAttributeInterface::ENTITY_TYPE_CODE, - 'shipment_type', - 'frontend_label', - 'Ship Bundle Items' - ); - $eavSetup->updateAttribute( - ProductAttributeInterface::ENTITY_TYPE_CODE, - 'shipment_type', - 'source_model', - \Magento\Bundle\Model\Product\Attribute\Source\Shipment\Type::class - ); - $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'shipment_type', 'default_value', 0); - $eavSetup->updateAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'shipment_type', 'is_visible', 1); - } - - /** - * {@inheritdoc} - */ - public static function getDependencies() - { - return [ - ApplyAttributesUpdate::class, - ]; - } - - /** - * {@inheritdoc} - */ - public static function getVersion() - { - return '2.0.2'; - } - - /** - * {@inheritdoc} - */ - public function getAliases() - { - return []; - } -} diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminClearFiltersActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminClearFiltersActionGroup.xml old mode 100644 new mode 100755 diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminCreateApiBundleProductActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminCreateApiBundleProductActionGroup.xml new file mode 100644 index 000000000000..4cd16320d3d7 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminCreateApiBundleProductActionGroup.xml @@ -0,0 +1,167 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See 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="AdminCreateApiDynamicBundleProductActionGroup"> + <arguments> + <argument name="productName" defaultValue="Api Dynamic Bundle Product" type="string"/> + </arguments> + <!--Create 4 simple products--> + <createData entity="SimpleProduct2" stepKey="simpleProduct1"> + <field key="price">4.99</field> + </createData> + <createData entity="SimpleProduct2" stepKey="simpleProduct2"> + <field key="price">2.89</field> + </createData> + <createData entity="SimpleProduct2" stepKey="simpleProduct3"> + <field key="price">7.33</field> + </createData> + <createData entity="SimpleProduct2" stepKey="simpleProduct4"> + <field key="price">18.25</field> + </createData> + <!-- Create the bundle product based --> + <createData entity="ApiBundleProduct" stepKey="createBundleProduct"> + <field key="name">{{productName}}</field> + </createData> + <createData entity="MultipleSelectOption" stepKey="createBundleOption1_1"> + <requiredEntity createDataKey="createBundleProduct"/> + <field key="required">false</field> + </createData> + <createData entity="CheckboxOption" stepKey="createBundleOption1_2"> + <requiredEntity createDataKey="createBundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkOptionToProduct"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createBundleOption1_1"/> + <requiredEntity createDataKey="simpleProduct1"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkOptionToProduct2"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createBundleOption1_1"/> + <requiredEntity createDataKey="simpleProduct2"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkOptionToProduct3"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createBundleOption1_2"/> + <requiredEntity createDataKey="simpleProduct3"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkOptionToProduct4"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createBundleOption1_2"/> + <requiredEntity createDataKey="simpleProduct4"/> + </createData> + </actionGroup> + <actionGroup name="AdminCreateApiFixedBundleProductActionGroup"> + <arguments> + <argument name="productName" defaultValue="Api Fixed Bundle Product" type="string"/> + </arguments> + <!--Create 4 simple products--> + <createData entity="SimpleProduct2" stepKey="simpleProduct1"> + <field key="price">4.99</field> + </createData> + <createData entity="SimpleProduct2" stepKey="simpleProduct2"> + <field key="price">2.89</field> + </createData> + <createData entity="SimpleProduct2" stepKey="simpleProduct3"> + <field key="price">7.33</field> + </createData> + <createData entity="SimpleProduct2" stepKey="simpleProduct4"> + <field key="price">18.25</field> + </createData> + <!-- Create the bundle product based --> + <createData entity="ApiFixedBundleProduct" stepKey="createBundleProduct"> + <field key="name">{{productName}}</field> + </createData> + <createData entity="MultipleSelectOption" stepKey="createBundleOption1_1"> + <requiredEntity createDataKey="createBundleProduct"/> + <field key="required">false</field> + </createData> + <createData entity="CheckboxOption" stepKey="createBundleOption1_2"> + <requiredEntity createDataKey="createBundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkOptionToProduct"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createBundleOption1_1"/> + <requiredEntity createDataKey="simpleProduct1"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkOptionToProduct2"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createBundleOption1_1"/> + <requiredEntity createDataKey="simpleProduct2"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkOptionToProduct3"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createBundleOption1_2"/> + <requiredEntity createDataKey="simpleProduct3"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkOptionToProduct4"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createBundleOption1_2"/> + <requiredEntity createDataKey="simpleProduct4"/> + </createData> + </actionGroup> + <actionGroup name="AdminCreateApiDynamicBundleProductAllOptionTypesActionGroup"> + <arguments> + <argument name="productName" defaultValue="Api Dynamic Bundle Product" type="string"/> + </arguments> + <!-- Create simple products --> + <createData entity="SimpleProduct2" stepKey="simpleProduct1"> + <field key="price">10</field> + </createData> + <createData entity="SimpleProduct2" stepKey="simpleProduct2"> + <field key="price">20</field> + </createData> + <!-- Create Bundle product --> + <createData entity="ApiBundleProduct" stepKey="createBundleProduct"> + <field key="name">{{productName}}</field> + </createData> + <createData entity="DropDownBundleOption" stepKey="createDropDownBundleOption"> + <requiredEntity createDataKey="createBundleProduct"/> + <field key="title">Drop-down Option</field> + </createData> + <createData entity="RadioButtonsOption" stepKey="createBundleRadioButtonsOption"> + <requiredEntity createDataKey="createBundleProduct"/> + <field key="title">Radio Buttons Option</field> + </createData> + <createData entity="CheckboxOption" stepKey="createBundleCheckboxOption"> + <requiredEntity createDataKey="createBundleProduct"/> + <field key="title">Checkbox Option</field> + </createData> + <createData entity="ApiBundleLink" stepKey="linkCheckboxOptionToProduct1"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createBundleCheckboxOption"/> + <requiredEntity createDataKey="simpleProduct1"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkCheckboxOptionToProduct2"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createBundleCheckboxOption"/> + <requiredEntity createDataKey="simpleProduct2"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkDropDownOptionToProduct1"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createDropDownBundleOption"/> + <requiredEntity createDataKey="simpleProduct1"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkDropDownOptionToProduct2"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createDropDownBundleOption"/> + <requiredEntity createDataKey="simpleProduct2"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkRadioButtonsOptionToProduct1"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createBundleRadioButtonsOption"/> + <requiredEntity createDataKey="simpleProduct1"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkRadioButtonsOptionToProduct2"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createBundleRadioButtonsOption"/> + <requiredEntity createDataKey="simpleProduct2"/> + </createData> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/CreateBundleProductActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/CreateBundleProductActionGroup.xml index 6e889ea2432e..d86d720ed7f5 100644 --- a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/CreateBundleProductActionGroup.xml +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/CreateBundleProductActionGroup.xml @@ -15,11 +15,12 @@ <fillField selector="{{AdminProductFormBundleSection.productSku}}" userInput="{{BundleProduct.sku}}" stepKey="fillProductSku"/> <!--Trigger SEO drop down--> - <conditionalClick selector="{{AdminProductFormBundleSection.seoDropdown}}" dependentSelector="{{AdminProductFormBundleSection.seoDependent}}" visible="false" stepKey="OpenDropDownIfClosed"/> - <waitForPageLoad stepKey="WaitForDropDownSEO"/> + <scrollTo selector="{{AdminProductFormBundleSection.seoDropdown}}" stepKey="scrollToSeoDropDown"/> + <conditionalClick selector="{{AdminProductFormBundleSection.seoDropdown}}" dependentSelector="{{AdminProductFormBundleSection.urlKey}}" visible="false" stepKey="openDropDownIfClosed"/> + <waitForPageLoad stepKey="waitForDropDownSEO"/> <!--Fill URL input--> - <fillField userInput="{{BundleProduct.urlKey}}" selector="{{AdminProductFormBundleSection.urlKey}}" stepKey="FillsinSEOlinkExtension"/> + <fillField userInput="{{BundleProduct.urlKey}}" selector="{{AdminProductFormBundleSection.urlKey}}" stepKey="fillsInSeoLinkExtension"/> </actionGroup> <actionGroup name="addBundleOptionWithTwoProducts"> @@ -55,4 +56,74 @@ <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity(x, '0')}}" userInput="50" stepKey="fillQuantity1"/> <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity(x, '1')}}" userInput="50" stepKey="fillQuantity2"/> </actionGroup> + + <actionGroup name="addBundleOptionWithOneProduct" extends="addBundleOptionWithTwoProducts"> + <remove keyForRemoval="openProductFilters2"/> + <remove keyForRemoval="fillProductSkuFilter2"/> + <remove keyForRemoval="clickApplyFilters2"/> + <remove keyForRemoval="waitForFilteredGridLoad2"/> + <remove keyForRemoval="selectProduct2"/> + <remove keyForRemoval="selectProduct2"/> + <remove keyForRemoval="fillQuantity1"/> + <remove keyForRemoval="fillQuantity2"/> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity(x, '0')}}" userInput="1" stepKey="fillQuantity" after="clickAddButton1"/> + </actionGroup> + + <actionGroup name="addBundleOptionWithTreeProducts" extends="addBundleOptionWithTwoProducts"> + <arguments> + <argument name="prodTreeSku" type="string"/> + </arguments> + <remove keyForRemoval="fillQuantity1"/> + <remove keyForRemoval="fillQuantity2"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters3" after="selectProduct2"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters3" after="clickClearFilters3"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{prodTreeSku}}" stepKey="fillProductSkuFilter3" after="openProductFilters3"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters3" after="fillProductSkuFilter3"/> + <waitForElementNotVisible selector="{{AdminProductGridSection.loadingMask}}" stepKey="waitForFilteredGridLoad3" time="30" after="clickApplyFilters3"/> + <checkOption selector="{{AdminAddProductsToOptionPanel.firstCheckbox}}" stepKey="selectProduct3" after="waitForFilteredGridLoad3"/> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity(x, '0')}}" userInput="1" stepKey="fillQuantity1" after="clickAddButton1"/> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity(x, '1')}}" userInput="1" stepKey="fillQuantity2" after="fillQuantity1"/> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity(x, '2')}}" userInput="1" stepKey="fillQuantity3" after="fillQuantity2"/> + </actionGroup> + + <actionGroup name="addBundleOptionWithSixProducts" extends="addBundleOptionWithTwoProducts"> + <arguments> + <argument name="prodTreeSku" type="string"/> + <argument name="prodFourSku" type="string"/> + <argument name="prodFiveSku" type="string"/> + <argument name="prodSixSku" type="string"/> + </arguments> + <remove keyForRemoval="fillQuantity1"/> + <remove keyForRemoval="fillQuantity2"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters3" after="selectProduct2"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters3" after="clickClearFilters3"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{prodTreeSku}}" stepKey="fillProductSkuFilter3" after="openProductFilters3"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters3" after="fillProductSkuFilter3"/> + <waitForElementNotVisible selector="{{AdminProductGridSection.loadingMask}}" stepKey="waitForFilteredGridLoad3" time="30" after="clickApplyFilters3"/> + <checkOption selector="{{AdminAddProductsToOptionPanel.firstCheckbox}}" stepKey="selectProduct3" after="waitForFilteredGridLoad3"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters4" after="selectProduct3"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters4" after="clickClearFilters4"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{prodFourSku}}" stepKey="fillProductSkuFilter4" after="openProductFilters4"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters4" after="fillProductSkuFilter4"/> + <waitForElementNotVisible selector="{{AdminProductGridSection.loadingMask}}" stepKey="waitForFilteredGridLoad4" time="30" after="clickApplyFilters4"/> + <checkOption selector="{{AdminAddProductsToOptionPanel.firstCheckbox}}" stepKey="selectProduct4" after="clickApplyFilters4"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters5" after="selectProduct4"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters5" after="clickClearFilters5"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{prodFiveSku}}" stepKey="fillProductSkuFilter5" after="openProductFilters5"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters5" after="fillProductSkuFilter5"/> + <waitForElementNotVisible selector="{{AdminProductGridSection.loadingMask}}" stepKey="waitForFilteredGridLoad5" time="30" after="clickApplyFilters5"/> + <checkOption selector="{{AdminAddProductsToOptionPanel.firstCheckbox}}" stepKey="selectProduct5" after="waitForFilteredGridLoad5"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters6" after="selectProduct5"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters6" after="clickClearFilters6"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{prodSixSku}}" stepKey="fillProductSkuFilter6" after="openProductFilters6"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters6" after="fillProductSkuFilter6"/> + <waitForElementNotVisible selector="{{AdminProductGridSection.loadingMask}}" stepKey="waitForFilteredGridLoad6" time="30" after="clickApplyFilters6"/> + <checkOption selector="{{AdminAddProductsToOptionPanel.firstCheckbox}}" stepKey="selectProduct6" after="waitForFilteredGridLoad6"/> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity(x, '0')}}" userInput="2" stepKey="fillQuantity1" after="clickAddButton1"/> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity(x, '1')}}" userInput="2" stepKey="fillQuantity2" after="fillQuantity1"/> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity(x, '2')}}" userInput="2" stepKey="fillQuantity3" after="fillQuantity2"/> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity(x, '3')}}" userInput="2" stepKey="fillQuantity4" after="fillQuantity3"/> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity(x, '4')}}" userInput="2" stepKey="fillQuantity5" after="fillQuantity4"/> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity(x, '5')}}" userInput="2" stepKey="fillQuantity6" after="fillQuantity5"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/EnableDisableProductActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/EnableDisableProductActionGroup.xml index e3ac6483bc7b..20bde5f87bd7 100644 --- a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/EnableDisableProductActionGroup.xml +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/EnableDisableProductActionGroup.xml @@ -14,7 +14,8 @@ <fillField selector="{{AdminProductFormBundleSection.productSku}}" userInput="{{BundleProduct.sku}}" stepKey="fillProductSku"/> <!--Trigger SEO drop down--> - <conditionalClick selector="{{AdminProductFormBundleSection.seoDropdown}}" dependentSelector="{{AdminProductFormBundleSection.seoDependent}}" visible="false" stepKey="OpenDropDownIfClosed"/> + <scrollTo selector="{{AdminProductFormBundleSection.seoDropdown}}" stepKey="moveToSEOSection"/> + <conditionalClick selector="{{AdminProductFormBundleSection.seoDropdown}}" dependentSelector="{{AdminProductFormBundleSection.urlKey}}" visible="false" stepKey="openDropDownIfClosed"/> <waitForPageLoad stepKey="WaitForDropDownSEO"/> <!--Fill URL input--> diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StoreFrontAddProductToCartFromBundleActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StoreFrontAddProductToCartFromBundleActionGroup.xml new file mode 100644 index 000000000000..441303e8f1b8 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StoreFrontAddProductToCartFromBundleActionGroup.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="StoreFrontAddProductToCartFromBundleWithCurrencyActionGroup"> + <arguments> + <argument name="product"/> + <argument name="currency" type="string" defaultValue="US Dollar"/> + </arguments> + <click selector="{{StorefrontBundledSection.currencyTrigger}}" stepKey="openCurrencyTrigger"/> + <click selector="{{StorefrontBundledSection.currency(currency)}}" stepKey="chooseCurrency"/> + <click selector="{{StorefrontBundledSection.addToCart}}" stepKey="clickCustomize"/> + <waitForPageLoad stepKey="waitForBundleOpen"/> + <checkOption selector="{{StorefrontBundledSection.productInBundle(product.name)}}" stepKey="chooseProduct"/> + <click selector="{{StorefrontBundledSection.addToCartConfigured}}" stepKey="addToCartProduct"/> + <scrollToTopOfPage stepKey="scrollToTop"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml index f28ffbdc40ac..1767db0a0097 100644 --- a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml @@ -24,4 +24,38 @@ <waitForPageLoad time="30" stepKey="waitForPageLoad3"/> <waitForText userInput="{{quantity}}" selector="{{StorefrontMinicartSection.productCount}}" time="30" stepKey="assertProductCount"/> </actionGroup> + + <!-- Add Bundle Product to Cart from the category page --> + <actionGroup name="StorefrontAddBundleProductFromCategoryToCartActionGroup"> + <arguments> + <argument name="productName" type="string"/> + </arguments> + <moveMouseOver selector="{{StorefrontCategoryProductSection.ProductTitleByName(productName)}}" stepKey="moveMouseOverProduct"/> + <click selector="{{StorefrontCategoryProductSection.ProductTitleByName(productName)}}" stepKey="openProductPage"/> + <waitForPageLoad time="30" stepKey="waitForBundleProductPageLoad"/> + <click selector="{{StorefrontBundledSection.addToCart}}" stepKey="clickCustomizeAndAddToCart"/> + <click selector="{{StorefrontBundledSection.addToCartConfigured}}" stepKey="clickAddBundleProductToCart"/> + <waitForElementVisible selector="{{StorefrontMinicartSection.productCount}}" stepKey="waitProductCount"/> + <see userInput="You added {{productName}} to your shopping cart." selector="{{StorefrontMessagesSection.success}}" stepKey="seeSuccessMessage"/> + </actionGroup> + + <!-- Add Bundle Product to Cart from product Page--> + <actionGroup name="StorefrontAddBundleProductFromProductToCartActionGroup"> + <arguments> + <argument name="productName" type="string"/> + </arguments> + <click selector="{{StorefrontBundledSection.addToCart}}" stepKey="clickCustomizeAndAddToCart"/> + <click selector="{{StorefrontBundledSection.addToCartConfigured}}" stepKey="clickAddBundleProductToCart"/> + <waitForElementVisible selector="{{StorefrontMinicartSection.productCount}}" stepKey="waitProductCount"/> + <see userInput="You added {{productName}} to your shopping cart." selector="{{StorefrontMessagesSection.success}}" stepKey="seeSuccessMessage"/> + </actionGroup> + + <!-- Add Bundled Product to Cart with selected multiselect option--> + <actionGroup name="StorefrontAddBundleProductFromProductToCartWithMultiOption" extends="StorefrontAddBundleProductFromProductToCartActionGroup"> + <arguments> + <argument name="optionName" type="string"/> + <argument name="value" type="string"/> + </arguments> + <selectOption selector="{{StorefrontBundledSection.multiselectOptionFourProducts(optionName)}}" userInput="{{value}}" stepKey="selectValue" before="clickAddBundleProductToCart"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/VerifyProductTypeOrderActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/VerifyProductTypeOrderActionGroup.xml new file mode 100644 index 000000000000..a00f1e367864 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/VerifyProductTypeOrderActionGroup.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="VerifyProductTypeOrder"> + <seeElement stepKey="seeBundleInOrder" selector="{{AdminProductDropdownOrderSection.bundleProduct}}"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Bundle/Test/Mftf/Data/BundleProductsSummaryData.xml b/app/code/Magento/Bundle/Test/Mftf/Data/BundleProductsSummaryData.xml new file mode 100644 index 000000000000..5cd286c0c6aa --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Data/BundleProductsSummaryData.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="BundleProductsSummary" type="Quote"> + <data key="subtotal">1,968.00</data> + <data key="shipping">5.00</data> + <data key="total">1,973.00</data> + <data key="shippingMethod">Flat Rate - Fixed</data> + </entity> +</entities> diff --git a/app/code/Magento/Bundle/Test/Mftf/Data/CustomAttributeData.xml b/app/code/Magento/Bundle/Test/Mftf/Data/CustomAttributeData.xml index e6866ae74a7e..256bfd774695 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Data/CustomAttributeData.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Data/CustomAttributeData.xml @@ -23,4 +23,12 @@ <data key="attribute_code">price_view</data> <data key="value">0</data> </entity> + <entity name="CustomAttributeFixWeight" type="custom_attribute"> + <data key="attribute_code">weight_type</data> + <data key="value">1</data> + </entity> + <entity name="CustomAttributeFixSku" type="custom_attribute"> + <data key="attribute_code">sku_type</data> + <data key="value">1</data> + </entity> </entities> diff --git a/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml index af93200f946d..0a0c77755fc7 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml @@ -31,6 +31,22 @@ <data key="fixedPriceFormatted">$10.00</data> <data key="defaultAttribute">Default</data> </entity> + <entity name="FixedBundleProduct" type="product2"> + <data key="name" unique="suffix">FixedBundleProduct</data> + <data key="sku" unique="suffix">fixed-bundle-product</data> + <data key="type_id">bundle</data> + <data key="attribute_set_id">4</data> + <data key="price">1.23</data> + <data key="visibility">4</data> + <data key="status">1</data> + <data key="urlKey" unique="suffix">fixed-bundle-product</data> + <requiredEntity type="custom_attribute">CustomAttributeCategoryIds</requiredEntity> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute">CustomAttributePriceView</requiredEntity> + <requiredEntity type="custom_attribute">CustomAttributeFixPrice</requiredEntity> + <requiredEntity type="custom_attribute">CustomAttributeFixWeight</requiredEntity> + <requiredEntity type="custom_attribute">CustomAttributeFixSku</requiredEntity> + </entity> <entity name="ApiBundleProduct" type="product2"> <data key="name" unique="suffix">Api Bundle Product</data> <data key="sku" unique="suffix">api-bundle-product</data> @@ -60,4 +76,19 @@ <requiredEntity type="custom_attribute">CustomAttributeDynamicPrice</requiredEntity> <requiredEntity type="custom_attribute">CustomAttributePriceViewRange</requiredEntity> </entity> + <entity name="ApiFixedBundleProduct" type="product2"> + <data key="name" unique="suffix">Api Fixed Bundle Product</data> + <data key="sku" unique="suffix">api-fixed-bundle-product</data> + <data key="type_id">bundle</data> + <data key="attribute_set_id">4</data> + <data key="price">1.23</data> + <data key="visibility">4</data> + <data key="status">1</data> + <data key="urlKey" unique="suffix">api-fixed-bundle-product</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute">ApiProductDescription</requiredEntity> + <requiredEntity type="custom_attribute">ApiProductShortDescription</requiredEntity> + <requiredEntity type="custom_attribute">CustomAttributeFixPrice</requiredEntity> + <requiredEntity type="custom_attribute">CustomAttributePriceView</requiredEntity> + </entity> </entities> 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 000000000000..787f7ade8ffa --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductDropdownOrderSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminProductDropdownOrderSection"> + <element name="bundleProduct" type="text" selector="//li[not(preceding-sibling::li[span[@title='Downloadable Product']]) and not(following-sibling::li[span[@title='Simple Product']]) and not(following-sibling::li[span[@title='Configurable Product']]) and not(following-sibling::li[span[@title='Grouped Product']]) and not(following-sibling::li[span[@title='Virtual Product']])]/span[@title='Bundle Product']"/> + </section> +</sections> diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml index 814d03c52f4b..516f40ac2e7b 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml @@ -22,6 +22,8 @@ <element name="bundleOptionXProductYQuantity" type="input" selector="[name='bundle_options[bundle_options][{{x}}][bundle_selections][{{y}}][selection_qty]']" parameterized="true"/> <element name="addProductsToOption" type="button" selector="[data-index='modal_set']" timeout="30"/> <element name="nthAddProductsToOption" type="button" selector="//tr[{{var}}]//button[@data-index='modal_set']" timeout="30" parameterized="true"/> + <element name="bundlePriceType" type="select" selector="bundle_options[bundle_options][0][bundle_selections][0][selection_price_type]"/> + <element name="bundlePriceValue" type="input" selector="bundle_options[bundle_options][0][bundle_selections][0][selection_price_value]"/> <!--Select"url Key"InputForm--> <element name="urlKey" type="input" selector="//input[@name='product[url_key]']" timeout="30"/> <!--AddSelectedProducts--> diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml index 8d9f29814f76..30a7e8b777f3 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml @@ -9,6 +9,8 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontBundledSection"> + <element name="productCheckbox" type="select" selector="//*[@id='customizeTitle']/following-sibling::div[{{arg1}}]//div[{{arg2}}][@class='field choice']/input" parameterized="true"/> + <element name="bundleProductsPrice" type="text" selector="//*[@class='bundle-info']//*[contains(@id,'product-price')]/span"/> <element name="nthBundledOption" type="input" selector=".option:nth-of-type({{numOption}}) .choice:nth-of-type({{numOptionSelect}}) input" parameterized="true"/> <element name="addToCart" type="button" selector="#bundle-slide" timeout="30"/> <element name="addToCartConfigured" type="button" selector="#product-addtocart-button" timeout="30"/> @@ -24,10 +26,16 @@ <element name="bundleProductName" type="text" selector="//*[@id='maincontent']//span[@itemprop='name']"/> <element name="pageNotFound" type="text" selector="//h1[@class='page-title']//span[contains(., 'Whoops, our bad...')]"/> <element name="dropDownOptionOneProducts" type="select" selector="//label//span[contains(text(), '{{productName}}')]/../..//div[@class='control']//select" parameterized="true"/> + <element name="dropDownOptionTierPrices" type="text" selector="//label//span[contains(text(), '{{optionName}}')]/../..//div[@class='control']//div[@class='option-tier-prices']" parameterized="true"/> + <element name="productInBundle" type="select" selector="//label//span[contains(text(), '{{productName}}')]" parameterized="true"/> <element name="dropDownOptionOneQuantity" type="input" selector="//span[contains(text(), '{{productName}}')]/../..//input" parameterized="true"/> <element name="radioButtonOptionTwoProducts" type="checkbox" selector="//label//span[contains(text(), '{{productName}}')]/../..//div[@class='control']//div[@class='field choice'][{{productNumber}}]/input" parameterized="true"/> <element name="radioButtonOptionTwoQuantity" type="input" selector="//label//span[contains(text(), '{{productName}}')]/../..//div[@class='control']//div[@class='field qty qty-holder']//input" parameterized="true"/> + <element name="radioButtonOptionLabel" type="text" selector="//label//span[contains(text(), '{{optionName}}')]/../..//div[@class='control']//div[@class='field choice']//label[contains(.,'{{productName}}')]" parameterized="true"/> <element name="checkboxOptionThreeProducts" type="checkbox" selector="//label//span[contains(text(), '{{productName}}')]/../..//div[@class='control']//div[@class='field choice'][{{productNumber}}]/input" parameterized="true"/> + <element name="checkboxOptionLabel" type="text" selector="//label//span[contains(text(), '{{optionName}}')]/../..//div[@class='control']//div[@class='field choice']//label[contains(.,'{{productName}}')]" parameterized="true"/> <element name="multiselectOptionFourProducts" type="multiselect" selector="//label//span[contains(text(), '{{productName}}')]/../..//select[@multiple='multiple']" parameterized="true"/> + <element name="currencyTrigger" type="select" selector="#switcher-currency-trigger" timeout="30"/> + <element name="currency" type="select" selector="//a[text()='{{arg}}']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontProductInfoMainSection.xml index 735571375866..fae1ec331b66 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontProductInfoMainSection.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -13,5 +13,6 @@ <element name="priceTo" type="text" selector=".product-info-price .price-to"/> <element name="minPrice" type="text" selector="span[data-price-type='minPrice']"/> <element name="maxPrice" type="text" selector="span[data-price-type='minPrice']"/> + <element name="productBundleOptionsCheckbox" type="checkbox" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//label[contains(.,'{{childName}}')]/../input" parameterized="true" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDefaultVideoBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDefaultVideoBundleProductTest.xml index 3c0034469769..c49202f31aef 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDefaultVideoBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDefaultVideoBundleProductTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminAddDefaultVideoBundleProductTest" extends="AdminAddDefaultVideoSimpleProductTest"> <annotations> <features value="Bundle"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminBasicBundleProductAttributesTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminBasicBundleProductAttributesTest.xml index 3a70b189b4dc..c6a07f7ed95c 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminBasicBundleProductAttributesTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminBasicBundleProductAttributesTest.xml @@ -15,9 +15,6 @@ <description value="Admin should be able to set/edit all the basic product attributes when creating/editing a bundle product"/> <severity value="CRITICAL"/> <testCaseId value="MC-222"/> - <skip> - <issueId value="MQE-1214"/> - </skip> <group value="Bundle"/> </annotations> <before> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicProductTest.xml new file mode 100644 index 000000000000..bc9a3dba9a5f --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicProductTest.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminDeleteBundleDynamicProductTest"> + <annotations> + <features value="Bundle"/> + <stories value="Delete products"/> + <title value="Delete Bundle Dynamic Product"/> + <description value="Admin should be able to delete a bundle dynamic product"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-11016"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="ApiBundleProductPriceViewRange" stepKey="createDynamicBundleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteBundleProductFilteredBySkuAndName"> + <argument name="product" value="$$createDynamicBundleProduct$$"/> + </actionGroup> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="A total of 1 record(s) have been deleted." stepKey="deleteMessage"/> + <!-- Verify product on Product Page --> + <amOnPage url="{{StorefrontProductPage.url($$createDynamicBundleProduct.name$$)}}" stepKey="amOnBundleProductPage"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> + <!-- Search for the product by sku --> + <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createDynamicBundleProduct.sku$$" stepKey="fillSearchBarByProductSku"/> + <waitForPageLoad stepKey="waitForSearchButton"/> + <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForSearchResults"/> + <!-- Should not see any search results --> + <dontSee userInput="$$createDynamicBundleProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> + <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> + <!-- Go to the category page that we created in the before block --> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> + <!-- Should not see the product --> + <dontSee userInput="$$createDynamicBundleProduct.name$$" selector="{{StorefrontCategoryMainSection.productsList}}" stepKey="dontSeeProductInCategory"/> + <see selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" userInput="We can't find products matching the selection." stepKey="seeEmptyProductMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleFixedProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleFixedProductTest.xml new file mode 100644 index 000000000000..2527dae7eadf --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleFixedProductTest.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminDeleteBundleFixedProductTest"> + <annotations> + <features value="Bundle"/> + <stories value="Delete products"/> + <title value="Delete Bundle Fixed Product"/> + <description value="Admin should be able to delete a bundle fixed product"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-11017"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="FixedBundleProduct" stepKey="createFixedBundleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteBundleProductFilteredBySkuAndName"> + <argument name="product" value="$$createFixedBundleProduct$$"/> + </actionGroup> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="A total of 1 record(s) have been deleted." stepKey="deleteMessage"/> + <!-- Verify product on Product Page --> + <amOnPage url="{{StorefrontProductPage.url($$createFixedBundleProduct.name$$)}}" stepKey="amOnBundleProductPage"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> + <!-- Search for the product by sku --> + <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createFixedBundleProduct.sku$$" stepKey="fillSearchBarByProductSku"/> + <waitForPageLoad stepKey="waitForSearchButton"/> + <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForSearchResults"/> + <!-- Should not see any search results --> + <dontSee userInput="$$createFixedBundleProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> + <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> + <!-- Go to the category page that we created in the before block --> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> + <!-- Should not see the product --> + <dontSee userInput="$$createFixedBundleProduct.name$$" selector="{{StorefrontCategoryMainSection.productsList}}" stepKey="dontSeeProductInCategory"/> + <see selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" userInput="We can't find products matching the selection." stepKey="seeEmptyProductMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProducts.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProducts.xml index c0edbf14e894..2f891fcc8f16 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProducts.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProducts.xml @@ -99,8 +99,8 @@ <fillField selector="{{AdminProductFormBundleSection.productSku}}" userInput="{{BundleProduct.sku2}}" stepKey="fillProductSku2"/> <!--Trigger SEO drop down--> - <conditionalClick selector="{{AdminProductFormBundleSection.seoDropdown}}" dependentSelector="{{AdminProductFormBundleSection.seoDependent}}" visible="false" stepKey="OpenDropDownIfClosed2"/> - <waitForPageLoad stepKey="WaitForDropDownSEO"/> + <scrollTo selector="{{AdminProductFormBundleSection.seoDropdown}}" stepKey="moveToSEOSection"/> + <conditionalClick selector="{{AdminProductFormBundleSection.seoDropdown}}" dependentSelector="{{AdminProductFormBundleSection.urlKey}}" visible="false" stepKey="openDropDownIfClosed"/> <!--Fill URL input--> <fillField userInput="{{BundleProduct.urlKey2}}" selector="{{AdminProductFormBundleSection.urlKey}}" stepKey="FillsinSEOlinkExtension2"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultVideoBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultVideoBundleProductTest.xml index e3cb68b6664e..d050c5443d1f 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultVideoBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultVideoBundleProductTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminRemoveDefaultVideoBundleProductTest" extends="AdminRemoveDefaultVideoSimpleProductTest"> <annotations> <features value="Bundle"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleProductTest.xml index 0b220efaad49..52bce6760088 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleProductTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdvanceCatalogSearchBundleByNameTest" extends="AdvanceCatalogSearchSimpleProductByNameTest"> <annotations> <features value="Bundle"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductFixedPricingTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductFixedPricingTest.xml index 574c0dccdb07..c922b981aecd 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductFixedPricingTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductFixedPricingTest.xml @@ -31,6 +31,10 @@ <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> + <!-- Delete the bundled product we created in the test body --> + <actionGroup ref="deleteProductBySku" stepKey="deleteBundleProduct"> + <argument name="sku" value="{{BundleProduct.sku}}"/> + </actionGroup> <actionGroup ref="logout" stepKey="logout"/> </after> <!--Go to bundle product creation page--> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/CurrencyChangingBundleProductInCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/CurrencyChangingBundleProductInCartTest.xml new file mode 100644 index 000000000000..ded8bb3c8333 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/CurrencyChangingBundleProductInCartTest.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="CurrencyChangingBundleProductInCartTest"> + <annotations> + <features value="Bundle"/> + <stories value="Check that after changing currency price of cart is correct when the bundle product added to the cart"/> + <title value="User should be able change the currency and get right price in cart when the bundle product added to the cart"/> + <description value="User should be able change the currency and add one more product in cart and get right price in previous currency"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-94467"/> + <group value="Bundle"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> + </before> + <after> + <!-- Delete the bundled product --> + <actionGroup stepKey="deleteBundle" ref="deleteProductUsingProductGrid"> + <argument name="product" value="BundleProduct"/> + </actionGroup> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="ClearFiltersAfter"/> + <waitForPageLoad stepKey="waitForClearFilter"/> + <!--Clear Configs--> + <amOnPage url="{{AdminLoginPage.url}}" stepKey="navigateToAdmin"/> + <waitForPageLoad stepKey="waitForAdminLoginPageLoad"/> + <amOnPage url="{{ConfigCurrencySetupPage.url}}" stepKey="navigateToConfigCurrencySetupPage"/> + <waitForPageLoad stepKey="waitForConfigCurrencySetupPageForUnselectEuroCurrency"/> + <unselectOption selector="{{CurrencySetupSection.allowCurrencies}}" userInput="Euro" stepKey="unselectEuro"/> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <click selector="{{CurrencySetupSection.currencyOptions}}" stepKey="closeOptions"/> + <waitForPageLoad stepKey="waitForCloseOptions"/> + <click stepKey="saveUnselectedConfigs" selector="{{AdminConfigSection.saveButton}}"/> + <amOnPage url="{{AdminLogoutPage.url}}" stepKey="logout"/> + <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> + </after> + <!--Go to bundle product creation page--> + <amOnPage url="{{AdminProductCreatePage.url(BundleProduct.set, BundleProduct.type)}}" stepKey="goToBundleProductCreationPage"/> + <waitForPageLoad stepKey="waitForBundleProductCreatePageToLoad"/> + <actionGroup ref="fillMainBundleProductForm" stepKey="fillMainFieldsForBundle"/> + <!-- Add Option, a "Radio Buttons" type option --> + <actionGroup ref="addBundleOptionWithTwoProducts" stepKey="addBundleOptionWithTwoProducts2"> + <argument name="x" value="0"/> + <argument name="n" value="1"/> + <argument name="prodOneSku" value="$$simpleProduct1.sku$$"/> + <argument name="prodTwoSku" value="$$simpleProduct2.sku$$"/> + <argument name="optionTitle" value="Option"/> + <argument name="inputType" value="radio"/> + </actionGroup> + <checkOption selector="{{AdminProductFormBundleSection.userDefinedQuantity('0', '0')}}" stepKey="userDefinedQuantitiyOptionProduct0"/> + <checkOption selector="{{AdminProductFormBundleSection.userDefinedQuantity('0', '1')}}" stepKey="userDefinedQuantitiyOptionProduct1"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + <amOnPage url="{{ConfigCurrencySetupPage.url}}" stepKey="navigateToConfigCurrencySetupPage"/> + <waitForPageLoad stepKey="waitForConfigCurrencySetupPage"/> + <conditionalClick selector="{{CurrencySetupSection.currencyOptions}}" dependentSelector="{{CurrencySetupSection.allowCurrencies}}" visible="false" stepKey="openOptions"/> + <waitForPageLoad stepKey="waitForOptions"/> + <selectOption selector="{{CurrencySetupSection.allowCurrencies}}" parameterArray="['Euro', 'US Dollar']" stepKey="selectCurrencies"/> + <click stepKey="saveConfigs" selector="{{AdminConfigSection.saveButton}}"/> + <!-- Go to storefront BundleProduct --> + <amOnPage url="{{BundleProduct.sku}}.html" stepKey="goToStorefront"/> + <waitForPageLoad stepKey="waitForStorefront"/> + <actionGroup ref="StoreFrontAddProductToCartFromBundleWithCurrencyActionGroup" stepKey="addProduct1ToCartAndChangeCurrencyToEuro"> + <argument name="product" value="$$simpleProduct1$$"/> + <argument name="currency" value="EUR - Euro"/> + </actionGroup> + <actionGroup ref="StoreFrontAddProductToCartFromBundleWithCurrencyActionGroup" stepKey="addProduct2ToCartAndChangeCurrencyToUSD"> + <argument name="product" value="$$simpleProduct2$$"/> + <argument name="currency" value="USD - US Dollar"/> + </actionGroup> + <click stepKey="openMiniCart" selector="{{StorefrontMinicartSection.showCart}}"/> + <waitForPageLoad stepKey="waitForMiniCart"/> + <see stepKey="seeCartSubtotal" userInput="$12,300.00"/> + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/MassEnableDisableBundleProductsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/MassEnableDisableBundleProductsTest.xml index 0fb8a7b88250..ff192538637e 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/MassEnableDisableBundleProductsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/MassEnableDisableBundleProductsTest.xml @@ -17,9 +17,6 @@ <severity value="CRITICAL"/> <testCaseId value="MC-217"/> <group value="Bundle"/> - <skip> - <issueId value="MAGETWO-92898"/> - </skip> </annotations> <before> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> @@ -63,15 +60,7 @@ <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '0')}}" userInput="{{BundleProduct.defaultQuantity}}" stepKey="fillProductDefaultQty1"/> <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '1')}}" userInput="{{BundleProduct.defaultQuantity}}" stepKey="fillProductDefaultQty2"/> - <fillField selector="{{AdminProductFormBundleSection.productName}}" userInput="{{BundleProduct.name}}" stepKey="fillProductName"/> - <fillField selector="{{AdminProductFormBundleSection.productSku}}" userInput="{{BundleProduct.sku}}" stepKey="fillProductSku"/> - - <!--Trigger SEO drop down--> - <conditionalClick selector="{{AdminProductFormBundleSection.seoDropdown}}" dependentSelector="{{AdminProductFormBundleSection.seoDependent}}" visible="false" stepKey="OpenDropDownIfClosed"/> - <waitForPageLoad stepKey="WaitForDropDownSEO"/> - - <!--Fill URL input--> - <fillField userInput="{{BundleProduct.urlKey}}" selector="{{AdminProductFormBundleSection.urlKey}}" stepKey="FillsinSEOlinkExtension"/> + <actionGroup ref="AncillaryPrepBundleProduct" stepKey="createBundledProductForTwoSimpleProducts"/> <!--Save the product--> <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> @@ -107,7 +96,8 @@ <fillField selector="{{AdminProductFormBundleSection.productSku}}" userInput="{{BundleProduct.sku2}}" stepKey="fillProductSku2"/> <!--Trigger SEO drop down--> - <conditionalClick selector="{{AdminProductFormBundleSection.seoDropdown}}" dependentSelector="{{AdminProductFormBundleSection.seoDependent}}" visible="false" stepKey="OpenDropDownIfClosed2"/> + <scrollTo selector="{{AdminProductFormBundleSection.seoDropdown}}" stepKey="moveToSEOSection"/> + <conditionalClick selector="{{AdminProductFormBundleSection.seoDropdown}}" dependentSelector="{{AdminProductFormBundleSection.urlKey}}" visible="false" stepKey="openDropDownIfClosed"/> <waitForPageLoad stepKey="WaitForDropDownSEO2"/> <!--Fill URL input--> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleOptionsToCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleOptionsToCartTest.xml new file mode 100644 index 000000000000..a1630128638d --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleOptionsToCartTest.xml @@ -0,0 +1,140 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontAddBundleOptionsToCartTest"> + <annotations> + <features value="Bundle"/> + <stories value="MAGETWO-95813: Only two bundle options are added to the cart"/> + <title value="Checking adding of bundle options to the cart"/> + <description value="Verifying adding of bundle options to the cart"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-95933"/> + <group value="Bundle"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct3"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct4"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct5"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct6"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct7"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct8"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct9"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct10"/> + </before> + <after> + <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> + <deleteData createDataKey="simpleProduct3" stepKey="deleteSimpleProduct3"/> + <deleteData createDataKey="simpleProduct4" stepKey="deleteSimpleProduct4"/> + <deleteData createDataKey="simpleProduct5" stepKey="deleteSimpleProduct5"/> + <deleteData createDataKey="simpleProduct6" stepKey="deleteSimpleProduct6"/> + <deleteData createDataKey="simpleProduct7" stepKey="deleteSimpleProduct7"/> + <deleteData createDataKey="simpleProduct8" stepKey="deleteSimpleProduct8"/> + <deleteData createDataKey="simpleProduct9" stepKey="deleteSimpleProduct9"/> + <deleteData createDataKey="simpleProduct10" stepKey="deleteSimpleProduct10"/> + <!--delete created bundle product--> + <actionGroup stepKey="deleteProduct1" ref="deleteProductBySku"> + <argument name="sku" value="{{BundleProduct.sku}}"/> + </actionGroup> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" + dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Start creating a bundle product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductList"/> + <waitForPageLoad stepKey="waitForProductList"/> + <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProduct"> + <argument name="product" value="BundleProduct"/> + </actionGroup> + <actionGroup ref="fillProductNameAndSkuInProductForm" stepKey="fillNameAndSku"> + <argument name="product" value="BundleProduct"/> + </actionGroup> + + <!-- Add Option One, a "Checkbox" type option, with tree products --> + <actionGroup ref="addBundleOptionWithTreeProducts" stepKey="addBundleOptionWithTreeProducts"> + <argument name="x" value="0"/> + <argument name="n" value="1"/> + <argument name="prodOneSku" value="$$simpleProduct1.sku$$"/> + <argument name="prodTwoSku" value="$$simpleProduct2.sku$$"/> + <argument name="prodTreeSku" value="$$simpleProduct3.sku$$"/> + <argument name="optionTitle" value="Option One"/> + <argument name="inputType" value="checkbox"/> + </actionGroup> + + <!-- Add Option Two, a "Radio Buttons" type option, with one product --> + <actionGroup ref="addBundleOptionWithOneProduct" stepKey="addBundleOptionWithOneProduct"> + <argument name="x" value="1"/> + <argument name="n" value="2"/> + <argument name="prodOneSku" value="$$simpleProduct4.sku$$"/> + <argument name="prodTwoSku" value=""/> + <argument name="optionTitle" value="Option Two"/> + <argument name="inputType" value="radio"/> + </actionGroup> + + <!-- Add Option Tree, a "Checkbox" type option, with six products --> + <actionGroup ref="addBundleOptionWithSixProducts" stepKey="addBundleOptionWithSixProducts"> + <argument name="x" value="2"/> + <argument name="n" value="3"/> + <argument name="prodOneSku" value="$$simpleProduct5.sku$$"/> + <argument name="prodTwoSku" value="$$simpleProduct6.sku$$"/> + <argument name="prodTreeSku" value="$$simpleProduct7.sku$$"/> + <argument name="prodFourSku" value="$$simpleProduct8.sku$$"/> + <argument name="prodFiveSku" value="$$simpleProduct9.sku$$"/> + <argument name="prodSixSku" value="$$simpleProduct10.sku$$"/> + <argument name="optionTitle" value="Option Tree"/> + <argument name="inputType" value="checkbox"/> + </actionGroup> + + <!-- Save product--> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + + <!--Go to Storefront and open Bundle Product page--> + <amOnPage url="{{BundleProduct.sku}}.html" stepKey="goToStorefront"/> + <waitForPageLoad stepKey="waitForStorefront"/> + + <!--Click "Customize and Add to Cart" button--> + <click selector="{{StorefrontBundledSection.addToCart}}" stepKey="clickCustomize"/> + + <!--Assert Bundle Product Price--> + <grabTextFrom selector="{{StorefrontBundledSection.bundleProductsPrice}}" stepKey="grabProductsPrice"/> + <assertEquals expected='$123.00' expectedType="string" actual="$grabProductsPrice" message="ExpectedPrice" stepKey="assertBundleProductPrice"/> + + <!--Chose all products from 1st & 3rd options --> + <click stepKey="selectProduct1" selector="{{StorefrontBundledSection.productCheckbox('1','1')}}"/> + <click stepKey="selectProduct2" selector="{{StorefrontBundledSection.productCheckbox('1','2')}}"/> + <click stepKey="selectProduct3" selector="{{StorefrontBundledSection.productCheckbox('1','3')}}"/> + <click stepKey="selectProduct5" selector="{{StorefrontBundledSection.productCheckbox('3','1')}}"/> + <click stepKey="selectProduct6" selector="{{StorefrontBundledSection.productCheckbox('3','2')}}"/> + <click stepKey="selectProduct7" selector="{{StorefrontBundledSection.productCheckbox('3','3')}}"/> + <click stepKey="selectProduct8" selector="{{StorefrontBundledSection.productCheckbox('3','4')}}"/> + <click stepKey="selectProduct9" selector="{{StorefrontBundledSection.productCheckbox('3','5')}}"/> + <click stepKey="selectProduct10" selector="{{StorefrontBundledSection.productCheckbox('3','6')}}"/> + + <!--Click "Add to Cart" button--> + <click selector="{{StorefrontBundleProductActionSection.addToCartButton}}" stepKey="clickAddBundleProductToCart"/> + <waitForPageLoad time="30" stepKey="waitForAddBundleProductPageLoad"/> + + <!--Click "mini cart" icon--> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openCart"/> + <waitForPageLoad stepKey="waitForDetailsOpen"/> + + <!--Check all products and Cart Subtotal --> + <actionGroup ref="StorefrontCheckCartActionGroup" stepKey="cartAssert" after="waitForDetailsOpen"> + <argument name="subtotal" value="1,968.00"/> + <argument name="shipping" value="5.00"/> + <argument name="shippingMethod" value="Flat Rate - Fixed"/> + <argument name="total" value="1,973.00"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleProductWithZeroPriceToShoppingCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleProductWithZeroPriceToShoppingCartTest.xml new file mode 100644 index 000000000000..33181d6e920e --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleProductWithZeroPriceToShoppingCartTest.xml @@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontAddBundleProductWithZeroPriceToShoppingCartTest"> + <annotations> + <features value="Bundle"/> + <stories value="Add Bundle product with zero price to shopping cart"/> + <title value="Add Bundle product with zero price to shopping cart"/> + <description value="Add Bundle product with zero price to shopping cart"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-95167"/> + <group value="bundle"/> + </annotations> + <before> + <!--Enable freeShipping--> + <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> + <!--Create category--> + <createData entity="SimpleSubCategory" stepKey="createSubCategory"/> + <!--Create simple with zero price product--> + <createData entity="ApiProductWithDescription" stepKey="apiSimple"> + <field key="price">0</field> + </createData> + <!--Create Bundle product--> + <createData entity="ApiBundleProductPriceViewRange" stepKey="apiBundleProduct"> + <requiredEntity createDataKey="createSubCategory"/> + </createData> + <!--Create Attribute--> + <createData entity="DropDownBundleOption" stepKey="bundleOption"> + <requiredEntity createDataKey="apiBundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink"> + <requiredEntity createDataKey="apiBundleProduct"/> + <requiredEntity createDataKey="bundleOption"/> + <requiredEntity createDataKey="apiSimple"/> + </createData> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + </before> + <after> + <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShipping"/> + <deleteData createDataKey="apiSimple" stepKey="deleteSimple"/> + <deleteData createDataKey="apiBundleProduct" stepKey="deleteBundleProduct"/> + <deleteData createDataKey="createSubCategory" stepKey="deleteCategory"/> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearFilters"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Open category page--> + <amOnPage url="{{StorefrontCategoryPage.url($$createSubCategory.custom_attributes[url_key]$$)}}" stepKey="amOnCategoryPage"/> + <!--Add bundle product to cart--> + <actionGroup ref="StorefrontAddBundleProductFromCategoryToCartActionGroup" stepKey="addBundleProductToCart"> + <argument name="productName" value="$$apiBundleProduct.name$$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + + <!--Place order--> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShipping"> + <argument name="shippingMethod" value="Free Shipping"/> + </actionGroup> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="checkoutPlaceOrder"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> + + <!--Check subtotal in created order--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="filterOrderGridById" stepKey="filterOrderById"> + <argument name="orderId" value="$grabOrderNumber"/> + </actionGroup> + <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> + <waitForPageLoad stepKey="waitForAdminOrderPageLoad"/> + <scrollTo selector="{{AdminOrderTotalSection.subTotal}}" stepKey="scrollToOrderTotalSection"/> + <see selector="{{AdminOrderTotalSection.subTotal}}" userInput="$0.00" stepKey="checkSubtotal"/> + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCheckBundleProductOptionTierPrices.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCheckBundleProductOptionTierPrices.xml new file mode 100644 index 000000000000..4c39cbc4ab0a --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCheckBundleProductOptionTierPrices.xml @@ -0,0 +1,104 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCheckBundleProductOptionTierPrices"> + <annotations> + <features value="Bundle"/> + <stories value="View bundle products"/> + <title value="Check tier prices for bundle options"/> + <testCaseId value="MAGETWO-98968"/> + <useCaseId value="MAGETWO-98603"/> + <group value="catalog"/> + <group value="bundle"/> + </annotations> + <before> + <!-- Create Dynamic Bundle product --> + <actionGroup ref="AdminCreateApiDynamicBundleProductAllOptionTypesActionGroup" stepKey="createBundleProduct"/> + + <!-- Add tier prices to simple products --> + <!-- Simple product 1 --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <amOnPage url="{{AdminProductEditPage.url($$simpleProduct1CreateBundleProduct.id$$)}}" stepKey="openAdminEditPageProduct1"/> + <actionGroup ref="ProductSetAdvancedPricing" stepKey="addTierPriceProduct1"> + <argument name="group" value="ALL GROUPS"/> + <argument name="quantity" value="5"/> + <argument name="price" value="Discount"/> + <argument name="amount" value="50"/> + </actionGroup> + <!-- Simple product 2 --> + <amOnPage url="{{AdminProductEditPage.url($$simpleProduct2CreateBundleProduct.id$$)}}" stepKey="openAdminEditPageProduct2"/> + <actionGroup ref="ProductSetAdvancedPricing" stepKey="addTierPriceProduct2"> + <argument name="group" value="ALL GROUPS"/> + <argument name="quantity" value="7"/> + <argument name="price" value="Discount"/> + <argument name="amount" value="25"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logoutAsAdmin"/> + + <!-- Run reindex --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + </before> + <after> + <deleteData createDataKey="createBundleProductCreateBundleProduct" stepKey="deleteDynamicBundleProduct"/> + <deleteData createDataKey="simpleProduct1CreateBundleProduct" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="simpleProduct2CreateBundleProduct" stepKey="deleteSimpleProduct2"/> + </after> + + <!-- Go to storefront product page --> + <amOnPage url="{{StorefrontProductPage.url($$createBundleProductCreateBundleProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToBundleProductPage"/> + <click selector="{{StorefrontBundledSection.addToCart}}" stepKey="clickCustomize"/> + + <!--"Drop-down" type option--> + <!-- Check Tier Prices for product 1 --> + <selectOption selector="{{StorefrontBundledSection.dropDownOptionOneProducts('Drop-down Option')}}" userInput="$$simpleProduct1CreateBundleProduct.sku$$ +$$$simpleProduct1CreateBundleProduct.price$$.00" stepKey="selectDropDownOptionProduct1"/> + <seeOptionIsSelected selector="{{StorefrontBundledSection.dropDownOptionOneProducts('Drop-down Option')}}" userInput="$$simpleProduct1CreateBundleProduct.sku$$ +$$$simpleProduct1CreateBundleProduct.price$$.00" stepKey="checkDropDownOptionProduct1"/> + <grabTextFrom selector="{{StorefrontBundledSection.dropDownOptionTierPrices('Drop-down Option')}}" stepKey="DropDownTierPriceTextProduct1"/> + <assertContains stepKey="assertDropDownTierPriceTextProduct1"> + <expectedResult type="string">Buy 5 for $5.00 each and save 50%</expectedResult> + <actualResult type="variable">DropDownTierPriceTextProduct1</actualResult> + </assertContains> + <!-- Check Tier Prices for product 2 --> + <selectOption selector="{{StorefrontBundledSection.dropDownOptionOneProducts('Drop-down Option')}}" userInput="$$simpleProduct2CreateBundleProduct.sku$$ +$$$simpleProduct2CreateBundleProduct.price$$.00" stepKey="selectDropDownOptionProduct2"/> + <seeOptionIsSelected selector="{{StorefrontBundledSection.dropDownOptionOneProducts('Drop-down Option')}}" userInput="$$simpleProduct2CreateBundleProduct.sku$$ +$$$simpleProduct2CreateBundleProduct.price$$.00" stepKey="checkDropDownOptionProduct2"/> + <grabTextFrom selector="{{StorefrontBundledSection.dropDownOptionTierPrices('Drop-down Option')}}" stepKey="dropDownTierPriceTextProduct2"/> + <assertContains stepKey="assertDropDownTierPriceTextProduct2"> + <expectedResult type="string">Buy 7 for $15.00 each and save 25%</expectedResult> + <actualResult type="variable">dropDownTierPriceTextProduct2</actualResult> + </assertContains> + + <!--"Radio Buttons" type option--> + <!-- Check Tier Prices for product 1 --> + <grabTextFrom selector="{{StorefrontBundledSection.radioButtonOptionLabel('Radio Buttons Option', '$$simpleProduct1CreateBundleProduct.sku$$')}}" stepKey="radioButtonsOptionTierPriceTextProduct1"/> + <assertContains stepKey="assertRadioButtonsOptionTierPriceTextProduct1"> + <expectedResult type="string">Buy 5 for $5.00 each and save 50%</expectedResult> + <actualResult type="variable">radioButtonsOptionTierPriceTextProduct1</actualResult> + </assertContains> + <!-- Check Tier Prices for product 2 --> + <grabTextFrom selector="{{StorefrontBundledSection.radioButtonOptionLabel('Radio Buttons Option', '$$simpleProduct2CreateBundleProduct.sku$$')}}" stepKey="radioButtonsOptionTierPriceTextProduct2"/> + <assertContains stepKey="assertRadioButtonsOptionTierPriceTextProduct2"> + <expectedResult type="string">Buy 7 for $15.00 each and save 25%</expectedResult> + <actualResult type="variable">radioButtonsOptionTierPriceTextProduct2</actualResult> + </assertContains> + + <!--"Checkbox" type option--> + <!-- Check Tier Prices for product 1 --> + <grabTextFrom selector="{{StorefrontBundledSection.checkboxOptionLabel('Checkbox Option', '$$simpleProduct1CreateBundleProduct.sku$$')}}" stepKey="checkBoxOptionTierPriceTextProduct1"/> + <assertContains stepKey="assertCheckBoxOptionTierPriceTextProduct1"> + <expectedResult type="string">Buy 5 for $5.00 each and save 50%</expectedResult> + <actualResult type="variable">checkBoxOptionTierPriceTextProduct1</actualResult> + </assertContains> + <!-- Check Tier Prices for product 2 --> + <grabTextFrom selector="{{StorefrontBundledSection.checkboxOptionLabel('Checkbox Option', '$$simpleProduct2CreateBundleProduct.sku$$')}}" stepKey="checkBoxOptionTierPriceTextProduct2"/> + <assertContains stepKey="assertCheckBoxOptionTierPriceTextProduct2"> + <expectedResult type="string">Buy 7 for $15.00 each and save 25%</expectedResult> + <actualResult type="variable">checkBoxOptionTierPriceTextProduct2</actualResult> + </assertContains> + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontEditBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontEditBundleProductTest.xml index f94cd83f4e7d..58806126aee3 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontEditBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontEditBundleProductTest.xml @@ -94,9 +94,8 @@ <click stepKey="clickEdit" selector="{{CheckoutCartProductSection.nthEditButton('1')}}"/> <waitForPageLoad stepKey="waitForStorefront2"/> - <!-- Choose both of the options on the storefront --> - <click stepKey="selectFirstBundleOption2" selector="{{StorefrontBundledSection.nthBundledOption('1','1')}}"/> - <click stepKey="selectSecondBundleOption2" selector="{{StorefrontBundledSection.nthBundledOption('1','2')}}"/> + <!-- Check second one option to choose both of the options on the storefront --> + <click selector="{{StorefrontBundledSection.nthBundledOption('1','2')}}" stepKey="selectSecondBundleOption2"/> <waitForPageLoad stepKey="waitForPriceUpdate3"/> <see stepKey="seeDoublePrice" selector="{{StorefrontBundledSection.configuredPrice}}" userInput="2,460.00"/> diff --git a/app/code/Magento/Bundle/Test/Unit/Block/Catalog/Product/View/Type/BundleTest.php b/app/code/Magento/Bundle/Test/Unit/Block/Catalog/Product/View/Type/BundleTest.php index c252e5f99612..07d2e1b995cd 100644 --- a/app/code/Magento/Bundle/Test/Unit/Block/Catalog/Product/View/Type/BundleTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Block/Catalog/Product/View/Type/BundleTest.php @@ -280,6 +280,7 @@ public function testGetJsonConfigFixedPriceBundle() $this->assertEquals(110, $jsonConfig['prices']['oldPrice']['amount']); $this->assertEquals(100, $jsonConfig['prices']['basePrice']['amount']); $this->assertEquals(100, $jsonConfig['prices']['finalPrice']['amount']); + $this->assertEquals([1], $jsonConfig['positions']); } /** diff --git a/app/code/Magento/Bundle/Test/Unit/Block/DataProviders/OptionPriceRendererTest.php b/app/code/Magento/Bundle/Test/Unit/Block/DataProviders/OptionPriceRendererTest.php new file mode 100644 index 000000000000..1af73bafc625 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Unit/Block/DataProviders/OptionPriceRendererTest.php @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Bundle\Test\Unit\Block\DataProviders; + +use Magento\Bundle\Block\DataProviders\OptionPriceRenderer; +use Magento\Catalog\Model\Product; +use Magento\Framework\Pricing\Render; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\View\Element\BlockInterface; +use Magento\Framework\View\LayoutInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Class to test additional data for bundle options + */ +class OptionPriceRendererTest extends TestCase +{ + /** + * @var LayoutInterface|MockObject + */ + private $layoutMock; + + /** + * @var OptionPriceRenderer + */ + private $renderer; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = new ObjectManager($this); + + $this->layoutMock = $this->createMock( + LayoutInterface::class + ); + + $this->renderer = $objectManager->getObject( + OptionPriceRenderer::class, + ['layout' => $this->layoutMock] + ); + } + + /** + * Test to render Tier price html + * + * @return void + */ + public function testRenderTierPrice(): void + { + $expectedHtml = 'tier price html'; + $expectedArguments = ['zone' => Render::ZONE_ITEM_OPTION]; + + $productMock = $this->createMock(Product::class); + + $priceRenderer = $this->createPartialMock(BlockInterface::class, ['toHtml', 'render']); + $priceRenderer->expects($this->once()) + ->method('render') + ->with('tier_price', $productMock, $expectedArguments) + ->willReturn($expectedHtml); + + $this->layoutMock->method('getBlock') + ->with('product.price.render.default') + ->willReturn($priceRenderer); + + $this->assertEquals( + $expectedHtml, + $this->renderer->renderTierPrice($productMock), + 'Render Tier price is wrong' + ); + } + + /** + * Test to render Tier price html when render block is not exists + * + * @return void + */ + public function testRenderTierPriceNotExist(): void + { + $productMock = $this->createMock(Product::class); + + $this->layoutMock->method('getBlock') + ->with('product.price.render.default') + ->willReturn(false); + + $this->assertEquals( + '', + $this->renderer->renderTierPrice($productMock), + 'Render Tier price is wrong' + ); + } +} diff --git a/app/code/Magento/Bundle/Test/Unit/Model/OptionRepositoryTest.php b/app/code/Magento/Bundle/Test/Unit/Model/OptionRepositoryTest.php index b4a466b413af..2450f63c3893 100644 --- a/app/code/Magento/Bundle/Test/Unit/Model/OptionRepositoryTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Model/OptionRepositoryTest.php @@ -8,6 +8,7 @@ namespace Magento\Bundle\Test\Unit\Model; use Magento\Bundle\Model\OptionRepository; +use Magento\Framework\Exception\NoSuchEntityException; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -84,7 +85,7 @@ protected function setUp() ->getMock(); $this->optionResourceMock = $this->createPartialMock( \Magento\Bundle\Model\ResourceModel\Option::class, - ['delete', '__wakeup', 'save', 'removeOptionSelections'] + ['get', 'delete', '__wakeup', 'save', 'removeOptionSelections'] ); $this->storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); $this->linkManagementMock = $this->createMock(\Magento\Bundle\Api\ProductLinkManagementInterface::class); @@ -227,32 +228,92 @@ public function testDeleteThrowsExceptionIfCannotDelete() $this->model->delete($optionMock); } + /** + * Test successful delete action for given $optionId + */ public function testDeleteById() { $productSku = 'sku'; $optionId = 100; - $productMock = $this->createMock(\Magento\Catalog\Api\Data\ProductInterface::class); + + $optionMock = $this->createMock(\Magento\Bundle\Model\Option::class); + $optionMock->expects($this->exactly(2)) + ->method('getId') + ->willReturn($optionId); + + $optionMock->expects($this->once()) + ->method('getData') + ->willReturn([ + 'title' => 'Option title', + 'option_id' => $optionId + ]); + + $this->optionFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($optionMock); + + $productMock = $this->createPartialMock( + \Magento\Catalog\Model\Product::class, + ['getTypeId', 'getTypeInstance', 'getStoreId', 'getPriceType', '__wakeup', 'getSku'] + ); $productMock->expects($this->once()) ->method('getTypeId') ->willReturn(\Magento\Catalog\Model\Product\Type::TYPE_BUNDLE); - $this->productRepositoryMock->expects($this->once()) + $productMock->expects($this->exactly(2))->method('getSku')->willReturn($productSku); + + $this->productRepositoryMock + ->expects($this->once()) ->method('get') ->with($productSku) ->willReturn($productMock); + $optCollectionMock = $this->createMock(\Magento\Bundle\Model\ResourceModel\Option\Collection::class); + $optCollectionMock->expects($this->once())->method('getItemById')->with($optionId)->willReturn($optionMock); + $this->typeMock->expects($this->once()) + ->method('getOptionsCollection') + ->with($productMock) + ->willReturn($optCollectionMock); + + $this->assertTrue($this->model->deleteById($productSku, $optionId)); + } + + /** + * Tests if NoSuchEntityException thrown when provided $optionId not found + */ + public function testDeleteByIdException() + { + $productSku = 'sku'; + $optionId = null; + $optionMock = $this->createMock(\Magento\Bundle\Model\Option::class); + $optionMock->expects($this->exactly(1)) + ->method('getId') + ->willReturn($optionId); + + $productMock = $this->createPartialMock( + \Magento\Catalog\Model\Product::class, + ['getTypeId', 'getTypeInstance', 'getStoreId', 'getPriceType', '__wakeup', 'getSku'] + ); + $productMock->expects($this->once()) + ->method('getTypeId') + ->willReturn(\Magento\Catalog\Model\Product\Type::TYPE_BUNDLE); + + $this->productRepositoryMock + ->expects($this->once()) + ->method('get') + ->with($productSku) + ->willReturn($productMock); $optCollectionMock = $this->createMock(\Magento\Bundle\Model\ResourceModel\Option\Collection::class); + $optCollectionMock->expects($this->once())->method('getItemById')->with($optionId)->willReturn($optionMock); $this->typeMock->expects($this->once()) ->method('getOptionsCollection') ->with($productMock) ->willReturn($optCollectionMock); - $optCollectionMock->expects($this->once())->method('setIdFilter')->with($optionId)->willReturnSelf(); - $optCollectionMock->expects($this->once())->method('getFirstItem')->willReturn($optionMock); + $this->expectException(NoSuchEntityException::class); - $this->optionResourceMock->expects($this->once())->method('delete')->with($optionMock)->willReturnSelf(); - $this->assertTrue($this->model->deleteById($productSku, $optionId)); + $this->model->deleteById($productSku, $optionId); } /** diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Plugin/Frontend/ProductTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Plugin/Frontend/ProductTest.php new file mode 100644 index 000000000000..ee08618eab5d --- /dev/null +++ b/app/code/Magento/Bundle/Test/Unit/Model/Plugin/Frontend/ProductTest.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Bundle\Test\Unit\Model\Plugin\Frontend; + +use Magento\Bundle\Model\Plugin\Frontend\Product as ProductPlugin; +use Magento\Bundle\Model\Product\Type; +use Magento\Catalog\Model\Product; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + +class ProductTest extends \PHPUnit\Framework\TestCase +{ + /** @var \Magento\Bundle\Model\Plugin\Product */ + private $plugin; + + /** @var MockObject|Type */ + private $type; + + /** @var MockObject|\Magento\Catalog\Model\Product */ + private $product; + + protected function setUp() + { + $this->product = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->setMethods(['getEntityId']) + ->getMock(); + + $this->type = $this->getMockBuilder(Type::class) + ->disableOriginalConstructor() + ->setMethods(['getChildrenIds']) + ->getMock(); + + $this->plugin = new ProductPlugin($this->type); + } + + public function testAfterGetIdentities() + { + $baseIdentities = [ + 'SomeCacheId', + 'AnotherCacheId', + ]; + $id = 12345; + $childIds = [ + 1 => [1, 2, 5, 100500], + 12 => [7, 22, 45, 24612] + ]; + $expectedIdentities = [ + 'SomeCacheId', + 'AnotherCacheId', + Product::CACHE_TAG . '_' . 1, + Product::CACHE_TAG . '_' . 2, + Product::CACHE_TAG . '_' . 5, + Product::CACHE_TAG . '_' . 100500, + Product::CACHE_TAG . '_' . 7, + Product::CACHE_TAG . '_' . 22, + Product::CACHE_TAG . '_' . 45, + Product::CACHE_TAG . '_' . 24612, + ]; + $this->product->expects($this->once()) + ->method('getEntityId') + ->will($this->returnValue($id)); + $this->type->expects($this->once()) + ->method('getChildrenIds') + ->with($id) + ->will($this->returnValue($childIds)); + $identities = $this->plugin->afterGetIdentities($this->product, $baseIdentities); + $this->assertEquals($expectedIdentities, $identities); + } +} diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php index 4bbf5641c55d..9d7629c6f0a4 100644 --- a/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php @@ -15,6 +15,7 @@ use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\Stdlib\ArrayUtils; /** * Class TypeTest @@ -87,6 +88,11 @@ class TypeTest extends \PHPUnit\Framework\TestCase */ private $serializer; + /** + * @var ArrayUtils|\PHPUnit_Framework_MockObject_MockObject + */ + private $arrayUtility; + /** * @return void */ @@ -159,6 +165,11 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); + $this->arrayUtility = $this->getMockBuilder(ArrayUtils::class) + ->setMethods(['flatten']) + ->disableOriginalConstructor() + ->getMock(); + $objectHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->model = $objectHelper->getObject( \Magento\Bundle\Model\Product\Type::class, @@ -175,6 +186,7 @@ protected function setUp() 'priceCurrency' => $this->priceCurrency, 'serializer' => $this->serializer, 'metadataPool' => $this->metadataPool, + 'arrayUtility' => $this->arrayUtility ] ); } @@ -421,6 +433,8 @@ function ($key) use ($optionCollection, $selectionCollection) { return $resultValue; } ); + $bundleOptions = [3 => 5]; + $product->expects($this->any()) ->method('getId') ->willReturn(333); @@ -438,9 +452,7 @@ function ($key) use ($optionCollection, $selectionCollection) { ->with($selectionCollection, true, true); $productType->expects($this->once()) ->method('setStoreFilter'); - $buyRequest->expects($this->once()) - ->method('getBundleOption') - ->willReturn([3 => 5]); + $buyRequest->expects($this->once())->method('getBundleOption')->willReturn($bundleOptions); $selectionCollection->expects($this->any()) ->method('getItems') ->willReturn([$selection]); @@ -491,6 +503,9 @@ function ($key) use ($optionCollection, $selectionCollection) { $option->expects($this->once()) ->method('getTitle') ->willReturn('Title for option'); + + $this->arrayUtility->expects($this->once())->method('flatten')->willReturn($bundleOptions); + $buyRequest->expects($this->once()) ->method('getBundleOptionQty') ->willReturn([3 => 5]); @@ -513,10 +528,6 @@ function ($key) use ($optionCollection, $selectionCollection) { ->method('getSelectionId') ->willReturn(314); - $this->priceCurrency->expects($this->once()) - ->method('convert') - ->willReturn(3.14); - $result = $this->model->prepareForCartAdvanced($buyRequest, $product); $this->assertEquals([$product, $productType], $result); } @@ -657,6 +668,8 @@ function ($key) use ($optionCollection, $selectionCollection) { return $resultValue; } ); + $bundleOptions = [3 => 5]; + $product->expects($this->any()) ->method('getId') ->willReturn(333); @@ -676,7 +689,10 @@ function ($key) use ($optionCollection, $selectionCollection) { ->method('setStoreFilter'); $buyRequest->expects($this->once()) ->method('getBundleOption') - ->willReturn([3 => 5]); + ->willReturn($bundleOptions); + + $this->arrayUtility->expects($this->once())->method('flatten')->willReturn($bundleOptions); + $selectionCollection->expects($this->any()) ->method('getItems') ->willReturn([$selection]); @@ -737,10 +753,6 @@ function ($key) use ($optionCollection, $selectionCollection) { ->method('prepareForCart') ->willReturn([]); - $this->priceCurrency->expects($this->once()) - ->method('convert') - ->willReturn(3.14); - $result = $this->model->prepareForCartAdvanced($buyRequest, $product); $this->assertEquals('We can\'t add this item to your shopping cart right now.', $result); } @@ -898,9 +910,10 @@ function ($key) use ($optionCollection, $selectionCollection) { ->with($selectionCollection, true, true); $productType->expects($this->once()) ->method('setStoreFilter'); - $buyRequest->expects($this->once()) - ->method('getBundleOption') - ->willReturn([3 => 5]); + + $bundleOptions = [3 => 5]; + $buyRequest->expects($this->once())->method('getBundleOption')->willReturn($bundleOptions); + $selectionCollection->expects($this->any()) ->method('getItems') ->willReturn([$selection]); @@ -951,6 +964,9 @@ function ($key) use ($optionCollection, $selectionCollection) { $option->expects($this->once()) ->method('getTitle') ->willReturn('Title for option'); + + $this->arrayUtility->expects($this->once())->method('flatten')->willReturn($bundleOptions); + $buyRequest->expects($this->once()) ->method('getBundleOptionQty') ->willReturn([3 => 5]); @@ -961,10 +977,6 @@ function ($key) use ($optionCollection, $selectionCollection) { ->method('prepareForCart') ->willReturn('string'); - $this->priceCurrency->expects($this->once()) - ->method('convert') - ->willReturn(3.14); - $result = $this->model->prepareForCartAdvanced($buyRequest, $product); $this->assertEquals('string', $result); } @@ -1065,13 +1077,15 @@ function ($key) use ($optionCollection) { ->willReturn(333); $productType->expects($this->once()) ->method('setStoreFilter'); - $buyRequest->expects($this->once()) - ->method('getBundleOption') - ->willReturn([]); + + $bundleOptions = []; + $buyRequest->expects($this->once())->method('getBundleOption')->willReturn($bundleOptions); $buyRequest->expects($this->once()) ->method('getBundleOptionQty') ->willReturn([3 => 5]); + $this->arrayUtility->expects($this->once())->method('flatten')->willReturn($bundleOptions); + $result = $this->model->prepareForCartAdvanced($buyRequest, $product, 'single'); $this->assertEquals([$product], $result); } @@ -1177,9 +1191,12 @@ function ($key) use ($optionCollection, $selectionCollection) { ->with($selectionCollection, true, true); $productType->expects($this->once()) ->method('setStoreFilter'); - $buyRequest->expects($this->once()) - ->method('getBundleOption') - ->willReturn([3 => 5]); + + $bundleOptions = [3 => 5]; + $buyRequest->expects($this->once())->method('getBundleOption')->willReturn($bundleOptions); + + $this->arrayUtility->expects($this->once())->method('flatten')->willReturn($bundleOptions); + $selectionCollection->expects($this->at(0)) ->method('getItems') ->willReturn([$selection]); @@ -1301,9 +1318,12 @@ function ($key) use ($optionCollection, $selectionCollection) { ->willReturn($option); $productType->expects($this->once()) ->method('setStoreFilter'); - $buyRequest->expects($this->once()) - ->method('getBundleOption') - ->willReturn([3 => 5]); + + $bundleOptions = [3 => 5]; + $buyRequest->expects($this->once())->method('getBundleOption')->willReturn($bundleOptions); + + $this->arrayUtility->expects($this->once())->method('flatten')->willReturn($bundleOptions); + $selectionCollection->expects($this->any()) ->method('getItems') ->willReturn([$selection]); @@ -1595,7 +1615,7 @@ public function testGetSkuWithoutType() ->disableOriginalConstructor() ->getMock(); $selectionItemMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) - ->setMethods(['getSku', '__wakeup']) + ->setMethods(['getSku', 'getEntityId', '__wakeup']) ->disableOriginalConstructor() ->getMock(); @@ -1623,9 +1643,12 @@ public function testGetSkuWithoutType() ->will($this->returnValue($serializeIds)); $selectionMock = $this->getSelectionsByIdsMock($selectionIds, $productMock, 5, 6); $selectionMock->expects(($this->any())) - ->method('getItems') - ->will($this->returnValue([$selectionItemMock])); - $selectionItemMock->expects($this->any()) + ->method('getItemByColumnValue') + ->will($this->returnValue($selectionItemMock)); + $selectionItemMock->expects($this->at(0)) + ->method('getEntityId') + ->will($this->returnValue(1)); + $selectionItemMock->expects($this->once()) ->method('getSku') ->will($this->returnValue($itemSku)); diff --git a/app/code/Magento/Bundle/Test/Unit/Pricing/Price/SpecialPriceTest.php b/app/code/Magento/Bundle/Test/Unit/Pricing/Price/SpecialPriceTest.php index f38dfc5538cf..3e60e057fe62 100644 --- a/app/code/Magento/Bundle/Test/Unit/Pricing/Price/SpecialPriceTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Pricing/Price/SpecialPriceTest.php @@ -6,6 +6,7 @@ namespace Magento\Bundle\Test\Unit\Pricing\Price; use \Magento\Bundle\Pricing\Price\SpecialPrice; +use Magento\Store\Api\Data\WebsiteInterface; class SpecialPriceTest extends \PHPUnit\Framework\TestCase { @@ -77,12 +78,6 @@ public function testGetValue($regularPrice, $specialPrice, $isScopeDateInInterva ->method('getSpecialPrice') ->will($this->returnValue($specialPrice)); - $store = $this->getMockBuilder(\Magento\Store\Model\Store::class) - ->disableOriginalConstructor() - ->getMock(); - $this->saleable->expects($this->once()) - ->method('getStore') - ->will($this->returnValue($store)); $this->saleable->expects($this->once()) ->method('getSpecialFromDate') ->will($this->returnValue($specialFromDate)); @@ -92,7 +87,7 @@ public function testGetValue($regularPrice, $specialPrice, $isScopeDateInInterva $this->localeDate->expects($this->once()) ->method('isScopeDateInInterval') - ->with($store, $specialFromDate, $specialToDate) + ->with(WebsiteInterface::ADMIN_CODE, $specialFromDate, $specialToDate) ->will($this->returnValue($isScopeDateInInterval)); $this->priceCurrencyMock->expects($this->never()) diff --git a/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php b/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php index 98fd96c52ccd..ad6fc12712c1 100644 --- a/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php +++ b/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php @@ -14,6 +14,7 @@ use Magento\Framework\UrlInterface; use Magento\Ui\Component\Container; use Magento\Ui\Component\Form; +use Magento\Ui\Component\Form\Fieldset; use Magento\Ui\Component\Modal; /** @@ -69,13 +70,26 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc + * * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function modifyMeta(array $meta) { $meta = $this->removeFixedTierPrice($meta); - $path = $this->arrayManager->findPath(static::CODE_BUNDLE_DATA, $meta, null, 'children'); + + $groupCode = static::CODE_BUNDLE_DATA; + $path = $this->arrayManager->findPath($groupCode, $meta, null, 'children'); + if (empty($path)) { + $meta[$groupCode]['children'] = []; + $meta[$groupCode]['arguments']['data']['config'] = [ + 'componentType' => Fieldset::NAME, + 'label' => __('Bundle Items'), + 'collapsible' => true + ]; + + $path = $this->arrayManager->findPath($groupCode, $meta, null, 'children'); + } $meta = $this->arrayManager->merge( $path, @@ -220,7 +234,7 @@ private function removeFixedTierPrice(array $meta) } /** - * {@inheritdoc} + * @inheritdoc */ public function modifyData(array $data) { diff --git a/app/code/Magento/Bundle/etc/db_schema.xml b/app/code/Magento/Bundle/etc/db_schema.xml index 8092f34c533f..33738cd252d6 100644 --- a/app/code/Magento/Bundle/etc/db_schema.xml +++ b/app/code/Magento/Bundle/etc/db_schema.xml @@ -18,13 +18,13 @@ <column xsi:type="int" name="position" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Position"/> <column xsi:type="varchar" name="type" nullable="true" length="255" comment="Type"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="option_id"/> </constraint> - <constraint xsi:type="foreign" name="CAT_PRD_BNDL_OPT_PARENT_ID_CAT_PRD_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_BNDL_OPT_PARENT_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_product_bundle_option" column="parent_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <index name="CATALOG_PRODUCT_BUNDLE_OPTION_PARENT_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_BUNDLE_OPTION_PARENT_ID" indexType="btree"> <column name="parent_id"/> </index> </table> @@ -39,13 +39,13 @@ <column xsi:type="varchar" name="title" nullable="true" length="255" comment="Title"/> <column xsi:type="int" name="parent_product_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Parent Product Id"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="foreign" name="CAT_PRD_BNDL_OPT_VAL_OPT_ID_CAT_PRD_BNDL_OPT_OPT_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_BNDL_OPT_VAL_OPT_ID_CAT_PRD_BNDL_OPT_OPT_ID" table="catalog_product_bundle_option_value" column="option_id" referenceTable="catalog_product_bundle_option" referenceColumn="option_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CAT_PRD_BNDL_OPT_VAL_OPT_ID_PARENT_PRD_ID_STORE_ID"> + <constraint xsi:type="unique" referenceId="CAT_PRD_BNDL_OPT_VAL_OPT_ID_PARENT_PRD_ID_STORE_ID"> <column name="option_id"/> <column name="parent_product_id"/> <column name="store_id"/> @@ -73,19 +73,19 @@ comment="Selection Qty"/> <column xsi:type="smallint" name="selection_can_change_qty" padding="6" unsigned="false" nullable="false" identity="false" default="0" comment="Selection Can Change Qty"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="selection_id"/> </constraint> - <constraint xsi:type="foreign" name="CAT_PRD_BNDL_SELECTION_OPT_ID_CAT_PRD_BNDL_OPT_OPT_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_BNDL_SELECTION_OPT_ID_CAT_PRD_BNDL_OPT_OPT_ID" table="catalog_product_bundle_selection" column="option_id" referenceTable="catalog_product_bundle_option" referenceColumn="option_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CAT_PRD_BNDL_SELECTION_PRD_ID_CAT_PRD_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_BNDL_SELECTION_PRD_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_product_bundle_selection" column="product_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <index name="CATALOG_PRODUCT_BUNDLE_SELECTION_OPTION_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_BUNDLE_SELECTION_OPTION_ID" indexType="btree"> <column name="option_id"/> </index> - <index name="CATALOG_PRODUCT_BUNDLE_SELECTION_PRODUCT_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_BUNDLE_SELECTION_PRODUCT_ID" indexType="btree"> <column name="product_id"/> </index> </table> @@ -101,68 +101,68 @@ nullable="false" default="0" comment="Selection Price Value"/> <column xsi:type="int" name="parent_product_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Parent Product Id"/> - <constraint xsi:type="primary" name="PK_CATALOG_PRODUCT_BUNDLE_SELECTION_PRICE"> + <constraint xsi:type="primary" referenceId="PK_CATALOG_PRODUCT_BUNDLE_SELECTION_PRICE"> <column name="selection_id"/> <column name="parent_product_id"/> <column name="website_id"/> </constraint> - <constraint xsi:type="foreign" name="CAT_PRD_BNDL_SELECTION_PRICE_WS_ID_STORE_WS_WS_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_BNDL_SELECTION_PRICE_WS_ID_STORE_WS_WS_ID" table="catalog_product_bundle_selection_price" column="website_id" referenceTable="store_website" referenceColumn="website_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="FK_DCF37523AA05D770A70AA4ED7C2616E4" + <constraint xsi:type="foreign" referenceId="FK_DCF37523AA05D770A70AA4ED7C2616E4" table="catalog_product_bundle_selection_price" column="selection_id" referenceTable="catalog_product_bundle_selection" referenceColumn="selection_id" onDelete="CASCADE"/> - <index name="CATALOG_PRODUCT_BUNDLE_SELECTION_PRICE_WEBSITE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_BUNDLE_SELECTION_PRICE_WEBSITE_ID" indexType="btree"> <column name="website_id"/> </index> </table> <table name="catalog_product_bundle_price_index" resource="default" engine="innodb" comment="Catalog Product Bundle Price Index"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Website Id"/> <column xsi:type="int" name="customer_group_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Customer Group Id"/> + comment="Customer Group ID"/> <column xsi:type="decimal" name="min_price" scale="4" precision="12" unsigned="false" nullable="false" comment="Min Price"/> <column xsi:type="decimal" name="max_price" scale="4" precision="12" unsigned="false" nullable="false" comment="Max Price"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> <column name="website_id"/> <column name="customer_group_id"/> </constraint> - <constraint xsi:type="foreign" name="CAT_PRD_BNDL_PRICE_IDX_CSTR_GROUP_ID_CSTR_GROUP_CSTR_GROUP_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_BNDL_PRICE_IDX_CSTR_GROUP_ID_CSTR_GROUP_CSTR_GROUP_ID" table="catalog_product_bundle_price_index" column="customer_group_id" referenceTable="customer_group" referenceColumn="customer_group_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CAT_PRD_BNDL_PRICE_IDX_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_BNDL_PRICE_IDX_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_product_bundle_price_index" column="entity_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CAT_PRD_BNDL_PRICE_IDX_WS_ID_STORE_WS_WS_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_BNDL_PRICE_IDX_WS_ID_STORE_WS_WS_ID" table="catalog_product_bundle_price_index" column="website_id" referenceTable="store_website" referenceColumn="website_id" onDelete="CASCADE"/> - <index name="CATALOG_PRODUCT_BUNDLE_PRICE_INDEX_WEBSITE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_BUNDLE_PRICE_INDEX_WEBSITE_ID" indexType="btree"> <column name="website_id"/> </index> - <index name="CATALOG_PRODUCT_BUNDLE_PRICE_INDEX_CUSTOMER_GROUP_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_BUNDLE_PRICE_INDEX_CUSTOMER_GROUP_ID" indexType="btree"> <column name="customer_group_id"/> </index> </table> <table name="catalog_product_bundle_stock_index" resource="default" engine="innodb" comment="Catalog Product Bundle Stock Index"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <column xsi:type="smallint" name="stock_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Stock Id"/> + comment="Stock ID"/> <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Option Id"/> <column xsi:type="smallint" name="stock_status" padding="6" unsigned="false" nullable="true" identity="false" default="0" comment="Stock Status"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> <column name="website_id"/> <column name="stock_id"/> @@ -172,13 +172,13 @@ <table name="catalog_product_index_price_bundle_idx" resource="default" engine="innodb" comment="Catalog Product Index Price Bundle Idx"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="int" name="customer_group_id" padding="11" unsigned="false" nullable="false" identity="false"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <column xsi:type="smallint" name="tax_class_id" padding="5" unsigned="true" nullable="true" identity="false" - default="0" comment="Tax Class Id"/> + default="0" comment="Tax Class ID"/> <column xsi:type="smallint" name="price_type" padding="5" unsigned="true" nullable="false" identity="false" comment="Price Type"/> <column xsi:type="decimal" name="special_price" scale="4" precision="12" unsigned="false" nullable="true" @@ -197,7 +197,7 @@ comment="Tier Price"/> <column xsi:type="decimal" name="base_tier" scale="4" precision="12" unsigned="false" nullable="true" comment="Base Tier"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> <column name="customer_group_id"/> <column name="website_id"/> @@ -206,13 +206,13 @@ <table name="catalog_product_index_price_bundle_tmp" resource="default" engine="memory" comment="Catalog Product Index Price Bundle Tmp"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="int" name="customer_group_id" padding="11" unsigned="false" nullable="false" identity="false"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <column xsi:type="smallint" name="tax_class_id" padding="5" unsigned="true" nullable="true" identity="false" - default="0" comment="Tax Class Id"/> + default="0" comment="Tax Class ID"/> <column xsi:type="smallint" name="price_type" padding="5" unsigned="true" nullable="false" identity="false" comment="Price Type"/> <column xsi:type="decimal" name="special_price" scale="4" precision="12" unsigned="false" nullable="true" @@ -231,7 +231,7 @@ comment="Tier Price"/> <column xsi:type="decimal" name="base_tier" scale="4" precision="12" unsigned="false" nullable="true" comment="Base Tier"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> <column name="customer_group_id"/> <column name="website_id"/> @@ -240,11 +240,11 @@ <table name="catalog_product_index_price_bundle_sel_idx" resource="default" engine="innodb" comment="Catalog Product Index Price Bundle Sel Idx"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="int" name="customer_group_id" padding="11" unsigned="false" nullable="false" identity="false"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Option Id"/> <column xsi:type="int" name="selection_id" padding="10" unsigned="true" nullable="false" identity="false" @@ -257,7 +257,7 @@ comment="Price"/> <column xsi:type="decimal" name="tier_price" scale="4" precision="12" unsigned="false" nullable="true" comment="Tier Price"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> <column name="customer_group_id"/> <column name="website_id"/> @@ -268,11 +268,11 @@ <table name="catalog_product_index_price_bundle_sel_tmp" resource="default" engine="memory" comment="Catalog Product Index Price Bundle Sel Tmp"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="int" name="customer_group_id" padding="11" unsigned="false" nullable="false" identity="false"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Option Id"/> <column xsi:type="int" name="selection_id" padding="10" unsigned="true" nullable="false" identity="false" @@ -285,7 +285,7 @@ comment="Price"/> <column xsi:type="decimal" name="tier_price" scale="4" precision="12" unsigned="false" nullable="true" comment="Tier Price"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> <column name="customer_group_id"/> <column name="website_id"/> @@ -296,11 +296,11 @@ <table name="catalog_product_index_price_bundle_opt_idx" resource="default" engine="innodb" comment="Catalog Product Index Price Bundle Opt Idx"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="int" name="customer_group_id" padding="11" unsigned="false" nullable="false" identity="false"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Option Id"/> <column xsi:type="decimal" name="min_price" scale="4" precision="12" unsigned="false" nullable="true" @@ -313,7 +313,7 @@ comment="Tier Price"/> <column xsi:type="decimal" name="alt_tier_price" scale="4" precision="12" unsigned="false" nullable="true" comment="Alt Tier Price"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> <column name="customer_group_id"/> <column name="website_id"/> @@ -323,11 +323,11 @@ <table name="catalog_product_index_price_bundle_opt_tmp" resource="default" engine="memory" comment="Catalog Product Index Price Bundle Opt Tmp"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="int" name="customer_group_id" padding="11" unsigned="false" nullable="false" identity="false"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Website Id"/> + comment="Website ID"/> <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Option Id"/> <column xsi:type="decimal" name="min_price" scale="4" precision="12" unsigned="false" nullable="true" @@ -340,7 +340,7 @@ comment="Tier Price"/> <column xsi:type="decimal" name="alt_tier_price" scale="4" precision="12" unsigned="false" nullable="true" comment="Alt Tier Price"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> <column name="customer_group_id"/> <column name="website_id"/> diff --git a/app/code/Magento/Bundle/etc/di.xml b/app/code/Magento/Bundle/etc/di.xml index 733b089dccd4..72155d922a25 100644 --- a/app/code/Magento/Bundle/etc/di.xml +++ b/app/code/Magento/Bundle/etc/di.xml @@ -123,6 +123,9 @@ </argument> </arguments> </type> + <type name="Magento\Quote\Model\Quote\Item"> + <plugin name="update_price_for_bundle_in_quote_item_option" type="Magento\Bundle\Plugin\UpdatePriceInQuoteItemOptions"/> + </type> <type name="Magento\Quote\Model\Quote\Item\ToOrderItem"> <plugin name="append_bundle_data_to_order" type="Magento\Bundle\Model\Plugin\QuoteItem"/> </type> @@ -140,6 +143,13 @@ </argument> </arguments> </type> + <type name="Magento\Sales\Model\Order\ProductOption"> + <arguments> + <argument name="processorPool" xsi:type="array"> + <item name="bundle" xsi:type="object">Magento\Bundle\Model\ProductOptionProcessor</item> + </argument> + </arguments> + </type> <type name="Magento\Bundle\Ui\DataProvider\Product\Listing\Collector\BundlePrice"> <arguments> <argument name="excludeAdjustments" xsi:type="array"> diff --git a/app/code/Magento/Bundle/etc/frontend/di.xml b/app/code/Magento/Bundle/etc/frontend/di.xml index 54f6d3b4b0f4..fc820ff87a12 100644 --- a/app/code/Magento/Bundle/etc/frontend/di.xml +++ b/app/code/Magento/Bundle/etc/frontend/di.xml @@ -13,4 +13,7 @@ </argument> </arguments> </type> + <type name="Magento\Catalog\Model\Product"> + <plugin name="bundle" type="Magento\Bundle\Model\Plugin\Frontend\Product" sortOrder="100" /> + </type> </config> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/catalog/product/edit/tab/attributes/extend.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/catalog/product/edit/tab/attributes/extend.phtml index 224cd71538b7..a770ae864a74 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/catalog/product/edit/tab/attributes/extend.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/catalog/product/edit/tab/attributes/extend.phtml @@ -20,9 +20,9 @@ $isElementReadonly = $block->getElement() ->getReadonly(); ?> -<?php if (!($attributeCode === 'price' && $block->getCanReadPrice() === false)) { ?> +<?php if (!($attributeCode === 'price' && $block->getCanReadPrice() === false)): ?> <div class="<?= /* @escapeNotVerified */ $attributeCode ?> "><?= /* @escapeNotVerified */ $elementHtml ?></div> -<?php } ?> +<?php endif; ?> <?= $block->getExtendedElement($switchAttributeCode)->toHtml() ?> @@ -43,13 +43,13 @@ $isElementReadonly = $block->getElement() } else { if ($attribute) { <?php if ($attributeCode === 'price' && !$block->getCanEditPrice() && $block->getCanReadPrice() - && $block->getProduct()->isObjectNew()) { ?> + && $block->getProduct()->isObjectNew()): ?> <?php $defaultProductPrice = $block->getDefaultProductPrice() ?: "''"; ?> $attribute.value = <?= /* @escapeNotVerified */ $defaultProductPrice ?>; - <?php } else { ?> + <?php else: ?> $attribute.disabled = false; $attribute.addClassName('required-entry'); - <?php } ?> + <?php endif; ?> } if ($('dynamic-price-warning')) { $('dynamic-price-warning').hide(); @@ -58,9 +58,9 @@ $isElementReadonly = $block->getElement() } <?php if (!($attributeCode === 'price' && !$block->getCanEditPrice() - && !$block->getProduct()->isObjectNew())) { ?> + && !$block->getProduct()->isObjectNew())): ?> $('<?= /* @escapeNotVerified */ $switchAttributeCode ?>').observe('change', <?= /* @escapeNotVerified */ $switchAttributeCode ?>_change); - <?php } ?> + <?php endif; ?> Event.observe(window, 'load', function(){ <?= /* @escapeNotVerified */ $switchAttributeCode ?>_change(); }); diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml index ff26d67bd837..12da960a9c6c 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml @@ -28,8 +28,17 @@ <?php endif; ?> <?php foreach ($items as $_item): ?> + <?php + $shipTogether = ($_item->getOrderItem()->getProductType() == \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE) ? + !$_item->getOrderItem()->isShipSeparately() : !$_item->getOrderItem()->getParentItem()->isShipSeparately() + ?> <?php $block->setPriceDataObject($_item) ?> <?php if ($_item->getOrderItem()->getParentItem()): ?> + <?php + if ($shipTogether) { + continue; + } + ?> <?php $attributes = $block->getSelectionAttributes($_item) ?> <?php if ($_prevOptionId != $attributes['option_id']): ?> <tr> @@ -60,14 +69,14 @@ </td> <?php endif; ?> <td class="col-price"> - <?php if ($block->canShowPriceInfo($_item)): ?> + <?php if ($block->canShowPriceInfo($_item) || $shipTogether): ?> <?= $block->getColumnHtml($_item, 'price') ?> <?php else: ?>   <?php endif; ?> </td> <td class="col-qty"> - <?php if ($block->canShowPriceInfo($_item)): ?> + <?php if ($block->canShowPriceInfo($_item) || $shipTogether): ?> <table class="qty-table"> <tr> <th><?= /* @escapeNotVerified */ __('Ordered') ?></th> @@ -116,7 +125,7 @@ <?php endif; ?> </td> <td class="col-qty-invoice"> - <?php if ($block->canShowPriceInfo($_item)): ?> + <?php if ($block->canShowPriceInfo($_item) || $shipTogether): ?> <?php if ($block->canEditQty()) : ?> <input type="text" class="input-text admin__control-text qty-input" diff --git a/app/code/Magento/Bundle/view/base/web/js/price-bundle.js b/app/code/Magento/Bundle/view/base/web/js/price-bundle.js index e56cc6f32d80..49ee253ad1e8 100644 --- a/app/code/Magento/Bundle/view/base/web/js/price-bundle.js +++ b/app/code/Magento/Bundle/view/base/web/js/price-bundle.js @@ -27,7 +27,8 @@ define([ '<% } %>', controlContainer: 'dd', // should be eliminated priceFormat: {}, - isFixedPrice: false + isFixedPrice: false, + optionTierPricesBlocksSelector: '#option-tier-prices-{1} [data-role="selection-tier-prices"]' }; $.widget('mage.priceBundle', { @@ -91,6 +92,8 @@ define([ if (changes) { priceBox.trigger('updatePrice', changes); } + + this._displayTierPriceBlock(bundleOption); this.updateProductSummary(); }, @@ -207,6 +210,35 @@ define([ return this; }, + /** + * Show or hide option tier prices block + * + * @param {Object} optionElement + * @private + */ + _displayTierPriceBlock: function (optionElement) { + var optionType = optionElement.prop('type'), + optionId, + optionValue, + optionTierPricesElements; + + if (optionType === 'select-one') { + optionId = utils.findOptionId(optionElement[0]); + optionValue = optionElement.val() || null; + optionTierPricesElements = $(this.options.optionTierPricesBlocksSelector.replace('{1}', optionId)); + + _.each(optionTierPricesElements, function (tierPriceElement) { + var selectionId = $(tierPriceElement).data('selection-id') + ''; + + if (selectionId === optionValue) { + $(tierPriceElement).show(); + } else { + $(tierPriceElement).hide(); + } + }); + } + }, + /** * Handler to update productSummary box */ @@ -374,8 +406,17 @@ define([ function applyTierPrice(oneItemPrice, qty, optionConfig) { var tiers = optionConfig.tierPrice, magicKey = _.keys(oneItemPrice)[0], + tiersFirstKey = _.keys(optionConfig)[0], lowest = false; + if (!tiers) {//tiers is undefined when options has only one option + tiers = optionConfig[tiersFirstKey].tierPrice; + } + + tiers.sort(function (a, b) {//sorting based on "price_qty" + return a['price_qty'] - b['price_qty']; + }); + _.each(tiers, function (tier, index) { if (tier['price_qty'] > qty) { return; diff --git a/app/code/Magento/Bundle/view/frontend/layout/catalog_product_view_type_bundle.xml b/app/code/Magento/Bundle/view/frontend/layout/catalog_product_view_type_bundle.xml index 5b8c050e5af5..d12f2e8f6a95 100644 --- a/app/code/Magento/Bundle/view/frontend/layout/catalog_product_view_type_bundle.xml +++ b/app/code/Magento/Bundle/view/frontend/layout/catalog_product_view_type_bundle.xml @@ -29,10 +29,22 @@ <container name="product.info.bundle.options.top" as="product_info_bundle_options_top"> <block class="Magento\Catalog\Block\Product\View" name="bundle.back.button" as="backButton" before="-" template="Magento_Bundle::catalog/product/view/backbutton.phtml"/> </container> - <block class="Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Select" name="product.info.bundle.options.select" as="select"/> + <block class="Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Select" name="product.info.bundle.options.select" as="select"> + <arguments> + <argument name="tier_price_renderer" xsi:type="object">\Magento\Bundle\Block\DataProviders\OptionPriceRenderer</argument> + </arguments> + </block> <block class="Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Multi" name="product.info.bundle.options.multi" as="multi"/> - <block class="Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Radio" name="product.info.bundle.options.radio" as="radio"/> - <block class="Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Checkbox" name="product.info.bundle.options.checkbox" as="checkbox"/> + <block class="Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Radio" name="product.info.bundle.options.radio" as="radio"> + <arguments> + <argument name="tier_price_renderer" xsi:type="object">\Magento\Bundle\Block\DataProviders\OptionPriceRenderer</argument> + </arguments> + </block> + <block class="Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Checkbox" name="product.info.bundle.options.checkbox" as="checkbox"> + <arguments> + <argument name="tier_price_renderer" xsi:type="object">\Magento\Bundle\Block\DataProviders\OptionPriceRenderer</argument> + </arguments> + </block> </block> </referenceBlock> <referenceBlock name="product.info.form.options"> diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml index bda649eb603e..830d03c826f3 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml @@ -19,6 +19,7 @@ <div class="nested options-list"> <?php if ($block->showSingle()): ?> <?= /* @escapeNotVerified */ $block->getSelectionQtyTitlePrice($_selections[0]) ?> + <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selections[0]) ?> <input type="hidden" class="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?> product bundle option" name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" @@ -38,6 +39,8 @@ <label class="label" for="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>"> <span><?= /* @escapeNotVerified */ $block->getSelectionQtyTitlePrice($_selection) ?></span> + <br/> + <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selection) ?> </label> </div> <?php endforeach; ?> diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/radio.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/radio.phtml index 7ea89e860981..1f33d97227ea 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/radio.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/radio.phtml @@ -21,6 +21,7 @@ <div class="nested options-list"> <?php if ($block->showSingle()): ?> <?= /* @escapeNotVerified */ $block->getSelectionTitlePrice($_selections[0]) ?> + <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selections[0]) ?> <input type="hidden" class="bundle-option-<?= (int)$_option->getId() ?> product bundle option" name="bundle_option[<?= (int)$_option->getId() ?>]" @@ -57,6 +58,8 @@ <label class="label" for="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?>-<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>"> <span><?= /* @escapeNotVerified */ $block->getSelectionTitlePrice($_selection) ?></span> + <br/> + <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selection) ?> </label> </div> <?php endforeach; ?> diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/select.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/select.phtml index 977daa2b2a44..4ea00f62b204 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/select.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/select.phtml @@ -20,6 +20,7 @@ <div class="control"> <?php if ($block->showSingle()): ?> <?= /* @escapeNotVerified */ $block->getSelectionTitlePrice($_selections[0]) ?> + <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selections[0]) ?> <input type="hidden" class="bundle-option-<?= /* @escapeNotVerified */ $_option->getId() ?> product bundle option" name="bundle_option[<?= /* @escapeNotVerified */ $_option->getId() ?>]" @@ -39,6 +40,15 @@ </option> <?php endforeach; ?> </select> + <div id="option-tier-prices-<?= /* @escapeNotVerified */ $_option->getId() ?>" class="option-tier-prices"> + <?php foreach ($_selections as $_selection): ?> + <div data-role="selection-tier-prices" + data-selection-id="<?= /* @escapeNotVerified */ $_selection->getSelectionId() ?>" + class="selection-tier-prices"> + <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selection) ?> + </div> + <?php endforeach; ?> + </div> <?php endif; ?> <div class="nested"> <div class="field qty qty-holder"> diff --git a/app/code/Magento/Bundle/view/frontend/templates/sales/order/items/renderer.phtml b/app/code/Magento/Bundle/view/frontend/templates/sales/order/items/renderer.phtml index 063d66edb9e7..74e1c5f87495 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/sales/order/items/renderer.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/sales/order/items/renderer.phtml @@ -7,95 +7,111 @@ // @codingStandardsIgnoreFile /** @var $block \Magento\Bundle\Block\Sales\Order\Items\Renderer */ +$parentItem = $block->getItem(); +$items = array_merge([$parentItem], $parentItem->getChildrenItems()); +$index = 0; +$prevOptionId = ''; ?> -<?php $parentItem = $block->getItem() ?> -<?php $items = array_merge([$parentItem], $parentItem->getChildrenItems()); ?> -<?php $_index = 0 ?> -<?php $_prevOptionId = '' ?> +<?php foreach ($items as $item): ?> -<?php foreach ($items as $_item): ?> - - <?php if ($block->getItemOptions() || $parentItem->getDescription() || $this->helper('Magento\GiftMessage\Helper\Message')->isMessagesAllowed('order_item', $parentItem) && $parentItem->getGiftMessageId()): ?> - <?php $_showlastRow = true ?> + <?php if ($block->getItemOptions() + || $parentItem->getDescription() + || $this->helper('Magento\GiftMessage\Helper\Message')->isMessagesAllowed('order_item', $parentItem) + && $parentItem->getGiftMessageId()): ?> + <?php $showLastRow = true; ?> <?php else: ?> - <?php $_showlastRow = false ?> + <?php $showLastRow = false; ?> <?php endif; ?> - <?php if ($_item->getParentItem()): ?> - <?php $attributes = $block->getSelectionAttributes($_item) ?> - <?php if ($_prevOptionId != $attributes['option_id']): ?> + <?php if ($item->getParentItem()): ?> + <?php $attributes = $block->getSelectionAttributes($item) ?> + <?php if ($prevOptionId != $attributes['option_id']): ?> <tr class="options-label"> - <td class="col label" colspan="5"><?= /* @escapeNotVerified */ $attributes['option_label'] ?></td> + <td class="col label" colspan="5"><?= $block->escapeHtml($attributes['option_label']); ?></td> </tr> - <?php $_prevOptionId = $attributes['option_id'] ?> + <?php $prevOptionId = $attributes['option_id'] ?> <?php endif; ?> <?php endif; ?> -<tr id="order-item-row-<?= /* @escapeNotVerified */ $_item->getId() ?>" class="<?php if ($_item->getParentItem()): ?>item-options-container<?php else: ?>item-parent<?php endif; ?>"<?php if ($_item->getParentItem()): ?> data-th="<?= /* @escapeNotVerified */ $attributes['option_label'] ?>"<?php endif; ?>> - <?php if (!$_item->getParentItem()): ?> - <td class="col name" data-th="<?= $block->escapeHtml(__('Product Name')) ?>"> - <strong class="product name product-item-name"><?= $block->escapeHtml($_item->getName()) ?></strong> +<tr id="order-item-row-<?= /* @noEscape */ $item->getId() ?>" + class="<?php if ($item->getParentItem()): ?> + item-options-container + <?php else: ?> + item-parent + <?php endif; ?>" + <?php if ($item->getParentItem()): ?> + data-th="<?= $block->escapeHtml($attributes['option_label']); ?>" + <?php endif; ?>> + <?php if (!$item->getParentItem()): ?> + <td class="col name" data-th="<?= $block->escapeHtml(__('Product Name')); ?>"> + <strong class="product name product-item-name"><?= $block->escapeHtml($item->getName()); ?></strong> </td> <?php else: ?> - <td class="col value" data-th="<?= $block->escapeHtml(__('Product Name')) ?>"><?= $block->getValueHtml($_item) ?></td> + <td class="col value" data-th="<?= $block->escapeHtml(__('Product Name')); ?>"> + <?= $block->getValueHtml($item); ?> + </td> <?php endif; ?> - <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"><?= /* @escapeNotVerified */ $block->prepareSku($_item->getSku()) ?></td> - <td class="col price" data-th="<?= $block->escapeHtml(__('Price')) ?>"> - <?php if (!$_item->getParentItem()): ?> - <?= $block->getItemPriceHtml() ?> + <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')); ?>"> + <?= /* @noEscape */ $block->prepareSku($item->getSku()); ?> + </td> + <td class="col price" data-th="<?= $block->escapeHtml(__('Price')); ?>"> + <?php if (!$item->getParentItem()): ?> + <?= /* @noEscape */ $block->getItemPriceHtml(); ?> <?php else: ?>   <?php endif; ?> </td> - <td class="col qty" data-th="<?= $block->escapeHtml(__('Quantity')) ?>"> + <td class="col qty" data-th="<?= $block->escapeHtml(__('Quantity')); ?>"> <?php if ( - ($_item->getParentItem() && $block->isChildCalculated()) || - (!$_item->getParentItem() && !$block->isChildCalculated()) || ($_item->getQtyShipped() > 0 && $_item->getParentItem() && $block->isShipmentSeparately())):?> + ($item->getParentItem() && $block->isChildCalculated()) || + (!$item->getParentItem() && !$block->isChildCalculated()) || + ($item->getQtyShipped() > 0 && $item->getParentItem() && $block->isShipmentSeparately())): ?> <ul class="items-qty"> <?php endif; ?> - <?php if (($_item->getParentItem() && $block->isChildCalculated()) || - (!$_item->getParentItem() && !$block->isChildCalculated())): ?> - <?php if ($_item->getQtyOrdered() > 0): ?> + <?php if (($item->getParentItem() && $block->isChildCalculated()) || + (!$item->getParentItem() && !$block->isChildCalculated())): ?> + <?php if ($item->getQtyOrdered() > 0): ?> <li class="item"> - <span class="title"><?= /* @escapeNotVerified */ __('Ordered') ?></span> - <span class="content"><?= /* @escapeNotVerified */ $_item->getQtyOrdered()*1 ?></span> + <span class="title"><?= $block->escapeHtml(__('Ordered')); ?></span> + <span class="content"><?= /* @noEscape */ $item->getQtyOrdered() * 1; ?></span> </li> <?php endif; ?> - <?php if ($_item->getQtyShipped() > 0 && !$block->isShipmentSeparately()): ?> + <?php if ($item->getQtyShipped() > 0 && !$block->isShipmentSeparately()): ?> <li class="item"> - <span class="title"><?= /* @escapeNotVerified */ __('Shipped') ?></span> - <span class="content"><?= /* @escapeNotVerified */ $_item->getQtyShipped()*1 ?></span> + <span class="title"><?= $block->escapeHtml(__('Shipped')); ?></span> + <span class="content"><?= /* @noEscape */ $item->getQtyShipped() * 1; ?></span> </li> <?php endif; ?> - <?php if ($_item->getQtyCanceled() > 0): ?> + <?php if ($item->getQtyCanceled() > 0): ?> <li class="item"> - <span class="title"><?= /* @escapeNotVerified */ __('Canceled') ?></span> - <span class="content"><?= /* @escapeNotVerified */ $_item->getQtyCanceled()*1 ?></span> + <span class="title"><?= $block->escapeHtml(__('Canceled')); ?></span> + <span class="content"><?= /* @noEscape */ $item->getQtyCanceled() * 1; ?></span> </li> <?php endif; ?> - <?php if ($_item->getQtyRefunded() > 0): ?> + <?php if ($item->getQtyRefunded() > 0): ?> <li class="item"> - <span class="title"><?= /* @escapeNotVerified */ __('Refunded') ?></span> - <span class="content"><?= /* @escapeNotVerified */ $_item->getQtyRefunded()*1 ?></span> + <span class="title"><?= $block->escapeHtml(__('Refunded')); ?></span> + <span class="content"><?= /* @noEscape */ $item->getQtyRefunded() * 1; ?></span> </li> <?php endif; ?> - <?php elseif ($_item->getQtyShipped() > 0 && $_item->getParentItem() && $block->isShipmentSeparately()): ?> + <?php elseif ($item->getQtyShipped() > 0 && $item->getParentItem() && $block->isShipmentSeparately()): ?> <li class="item"> - <span class="title"><?= /* @escapeNotVerified */ __('Shipped') ?></span> - <span class="content"><?= /* @escapeNotVerified */ $_item->getQtyShipped()*1 ?></span> + <span class="title"><?= $block->escapeHtml(__('Shipped')); ?></span> + <span class="content"><?= /* @noEscape */ $item->getQtyShipped() * 1; ?></span> </li> <?php else: ?> -   + <span class="content"><?= /* @noEscape */ $parentItem->getQtyOrdered() * 1; ?></span> <?php endif; ?> <?php if ( - ($_item->getParentItem() && $block->isChildCalculated()) || - (!$_item->getParentItem() && !$block->isChildCalculated()) || ($_item->getQtyShipped() > 0 && $_item->getParentItem() && $block->isShipmentSeparately())):?> + ($item->getParentItem() && $block->isChildCalculated()) || + (!$item->getParentItem() && !$block->isChildCalculated()) || + ($item->getQtyShipped() > 0 && $item->getParentItem() && $block->isShipmentSeparately())):?> </ul> <?php endif; ?> </td> <td class="col subtotal" data-th="<?= $block->escapeHtml(__('Subtotal')) ?>"> - <?php if (!$_item->getParentItem()): ?> - <?= $block->getItemRowTotalHtml() ?> + <?php if (!$item->getParentItem()): ?> + <?= /* @noEscape */ $block->getItemRowTotalHtml(); ?> <?php else: ?>   <?php endif; ?> @@ -103,33 +119,38 @@ </tr> <?php endforeach; ?> -<?php if ($_showlastRow && (($_options = $block->getItemOptions()) || $block->escapeHtml($_item->getDescription()))): ?> +<?php if ($showLastRow && (($options = $block->getItemOptions()) || $block->escapeHtml($item->getDescription()))): ?> <tr> <td class="col options" colspan="5"> - <?php if ($_options = $block->getItemOptions()): ?> + <?php if ($options = $block->getItemOptions()): ?> <dl class="item-options"> - <?php foreach ($_options as $_option) : ?> - <dt><?= $block->escapeHtml($_option['label']) ?></dt> + <?php foreach ($options as $option) : ?> + <dt><?= $block->escapeHtml($option['label']) ?></dt> <?php if (!$block->getPrintStatus()): ?> - <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> - <dd<?php if (isset($_formatedOptionValue['full_view'])): ?> class="tooltip wrapper"<?php endif; ?>> - <?= /* @escapeNotVerified */ $_formatedOptionValue['value'] ?> - <?php if (isset($_formatedOptionValue['full_view'])): ?> + <?php $formattedOptionValue = $block->getFormatedOptionValue($option) ?> + <dd<?php if (isset($formattedOptionValue['full_view'])): ?> + class="tooltip wrapper" + <?php endif; ?>> + <?= /* @noEscape */ $formattedOptionValue['value'] ?> + <?php if (isset($formattedOptionValue['full_view'])): ?> <div class="tooltip content"> <dl class="item options"> - <dt><?= $block->escapeHtml($_option['label']) ?></dt> - <dd><?= /* @escapeNotVerified */ $_formatedOptionValue['full_view'] ?></dd> + <dt><?= $block->escapeHtml($option['label']); ?></dt> + <dd><?= /* @noEscape */ $formattedOptionValue['full_view']; ?></dd> </dl> </div> <?php endif; ?> </dd> <?php else: ?> - <dd><?= $block->escapeHtml((isset($_option['print_value']) ? $_option['print_value'] : $_option['value'])) ?></dd> + <dd><?= $block->escapeHtml((isset($option['print_value']) ? + $option['print_value'] : + $option['value'])); ?> + </dd> <?php endif; ?> <?php endforeach; ?> </dl> <?php endif; ?> - <?= $block->escapeHtml($_item->getDescription()) ?> + <?= $block->escapeHtml($item->getDescription()); ?> </td> </tr> <?php endif; ?> diff --git a/app/code/Magento/Bundle/view/frontend/web/js/product-summary.js b/app/code/Magento/Bundle/view/frontend/web/js/product-summary.js index d8d4cb1e99b7..1e7fe6b6673d 100644 --- a/app/code/Magento/Bundle/view/frontend/web/js/product-summary.js +++ b/app/code/Magento/Bundle/view/frontend/web/js/product-summary.js @@ -56,8 +56,9 @@ define([ // Clear Summary box this.element.html(''); - - $.each(this.cache.currentElement.selected, $.proxy(this._renderOption, this)); + this.cache.currentElement.positions.forEach(function (optionId) { + this._renderOption(optionId, this.cache.currentElement.selected[optionId]); + }, this); this.element .parents(this.options.bundleSummaryContainer) .toggleClass('empty', !this.cache.currentElementCount); // Zero elements equal '.empty' container diff --git a/app/code/Magento/BundleGraphQl/Model/BundleProductTypeResolver.php b/app/code/Magento/BundleGraphQl/Model/BundleProductTypeResolver.php index b904d3f62a74..211d625fbc75 100644 --- a/app/code/Magento/BundleGraphQl/Model/BundleProductTypeResolver.php +++ b/app/code/Magento/BundleGraphQl/Model/BundleProductTypeResolver.php @@ -8,19 +8,22 @@ namespace Magento\BundleGraphQl\Model; use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; +use Magento\Bundle\Model\Product\Type as Type; /** - * {@inheritdoc} + * @inheritdoc */ class BundleProductTypeResolver implements TypeResolverInterface { + const BUNDLE_PRODUCT = 'BundleProduct'; + /** - * {@inheritdoc} + * @inheritdoc */ public function resolveType(array $data) : string { - if (isset($data['type_id']) && $data['type_id'] == 'bundle') { - return 'BundleProduct'; + if (isset($data['type_id']) && $data['type_id'] == Type::TYPE_CODE) { + return self::BUNDLE_PRODUCT; } return ''; } diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItemLinks.php b/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItemLinks.php index f90945d19f94..184f7177a995 100644 --- a/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItemLinks.php +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItemLinks.php @@ -7,15 +7,15 @@ namespace Magento\BundleGraphQl\Model\Resolver; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\BundleGraphQl\Model\Resolver\Links\Collection; use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Query\Resolver\Value; use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; /** - * {@inheritdoc} + * @inheritdoc */ class BundleItemLinks implements ResolverInterface { @@ -42,16 +42,14 @@ public function __construct( } /** - * {@inheritDoc} + * @inheritdoc */ - public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) : Value + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) { if (!isset($value['option_id']) || !isset($value['parent_id'])) { - $result = function () { - return null; - }; - return $this->valueFactory->create($result); + throw new LocalizedException(__('"option_id" and "parent_id" values should be specified')); } + $this->linkCollection->addIdFilters((int)$value['option_id'], (int)$value['parent_id']); $result = function () use ($value) { return $this->linkCollection->getLinksForOptionId((int)$value['option_id']); diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItems.php b/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItems.php index 9474f825fe5e..b67bd69ecf92 100644 --- a/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItems.php +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItems.php @@ -13,12 +13,11 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Query\Resolver\Value; use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; /** - * {@inheritdoc} + * @inheritdoc */ class BundleItems implements ResolverInterface { @@ -35,21 +34,21 @@ class BundleItems implements ResolverInterface /** * @var MetadataPool */ - private $metdataPool; + private $metadataPool; /** * @param Collection $bundleOptionCollection * @param ValueFactory $valueFactory - * @param MetadataPool $metdataPool + * @param MetadataPool $metadataPool */ public function __construct( Collection $bundleOptionCollection, ValueFactory $valueFactory, - MetadataPool $metdataPool + MetadataPool $metadataPool ) { $this->bundleOptionCollection = $bundleOptionCollection; $this->valueFactory = $valueFactory; - $this->metdataPool = $metdataPool; + $this->metadataPool = $metadataPool; } /** @@ -57,9 +56,9 @@ public function __construct( * * {@inheritDoc} */ - public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) : Value + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) { - $linkField = $this->metdataPool->getMetadata(ProductInterface::class)->getLinkField(); + $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); if ($value['type_id'] !== Type::TYPE_CODE || !isset($value[$linkField]) || !isset($value[ProductInterface::SKU]) diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Collection.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Collection.php index 149155c86275..7608d6e9e4d9 100644 --- a/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Collection.php +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Collection.php @@ -61,6 +61,7 @@ public function __construct( * Add parent id/sku pair to use for option filter at fetch time. * * @param int $parentId + * @param int $parentEntityId * @param string $sku */ public function addParentFilterData(int $parentId, int $parentEntityId, string $sku) : void diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Label.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Label.php index a4757108ee5a..de72b18982c1 100644 --- a/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Label.php +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Label.php @@ -7,10 +7,10 @@ namespace Magento\BundleGraphQl\Model\Resolver\Options; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Deferred\Product; +use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Deferred\Product as ProductDataProvider; use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Query\Resolver\Value; use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; @@ -19,29 +19,28 @@ */ class Label implements ResolverInterface { - /** * @var ValueFactory */ private $valueFactory; /** - * @var Product + * @var ProductDataProvider */ private $product; /** * @param ValueFactory $valueFactory - * @param Product $product + * @param ProductDataProvider $product */ - public function __construct(ValueFactory $valueFactory, Product $product) + public function __construct(ValueFactory $valueFactory, ProductDataProvider $product) { $this->valueFactory = $valueFactory; $this->product = $product; } /** - * @inheritDoc + * @inheritdoc */ public function resolve( Field $field, @@ -49,12 +48,9 @@ public function resolve( ResolveInfo $info, array $value = null, array $args = null - ): Value { + ) { if (!isset($value['sku'])) { - $result = function () { - return null; - }; - return $this->valueFactory->create($result); + throw new LocalizedException(__('"sku" value should be specified')); } $this->product->addProductSku($value['sku']); diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/DynamicPrice.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/DynamicPrice.php index e8dc3decc2ad..978e1c455fc0 100644 --- a/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/DynamicPrice.php +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/DynamicPrice.php @@ -5,36 +5,20 @@ */ declare(strict_types=1); - namespace Magento\BundleGraphQl\Model\Resolver\Product\Fields; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Bundle\Model\Product\Type as Bundle; use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Query\Resolver\Value; -use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; /** - * {@inheritdoc} + * @inheritdoc */ class DynamicPrice implements ResolverInterface { /** - * @var ValueFactory - */ - private $valueFactory; - - /** - * @param ValueFactory $valueFactory - */ - public function __construct(ValueFactory $valueFactory) - { - $this->valueFactory = $valueFactory; - } - - /** - * {@inheritdoc} + * @inheritdoc */ public function resolve( Field $field, @@ -42,16 +26,12 @@ public function resolve( ResolveInfo $info, array $value = null, array $args = null - ): Value { + ) { $result = null; if ($value['type_id'] === Bundle::TYPE_CODE) { $result = isset($value['price_type']) ? !$value['price_type'] : null; } - return $this->valueFactory->create( - function () use ($result) { - return $result; - } - ); + return $result; } } diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/DynamicSku.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/DynamicSku.php index 37e1557d36df..73f84c278a63 100644 --- a/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/DynamicSku.php +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/DynamicSku.php @@ -5,36 +5,20 @@ */ declare(strict_types=1); - namespace Magento\BundleGraphQl\Model\Resolver\Product\Fields; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Bundle\Model\Product\Type as Bundle; use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Query\Resolver\Value; -use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; /** - * {@inheritdoc} + * @inheritdoc */ class DynamicSku implements ResolverInterface { /** - * @var ValueFactory - */ - private $valueFactory; - - /** - * @param ValueFactory $valueFactory - */ - public function __construct(ValueFactory $valueFactory) - { - $this->valueFactory = $valueFactory; - } - - /** - * {@inheritdoc} + * @inheritdoc */ public function resolve( Field $field, @@ -42,18 +26,12 @@ public function resolve( ResolveInfo $info, array $value = null, array $args = null - ): Value { - $result = function () { - return null; - }; + ) { + $result = null; if ($value['type_id'] === Bundle::TYPE_CODE) { $result = isset($value['sku_type']) ? !$value['sku_type'] : null; } - return $this->valueFactory->create( - function () use ($result) { - return $result; - } - ); + return $result; } } diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/DynamicWeight.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/DynamicWeight.php index 5f79bba449e5..a4bb8ef64fc9 100644 --- a/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/DynamicWeight.php +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/DynamicWeight.php @@ -5,36 +5,20 @@ */ declare(strict_types=1); - namespace Magento\BundleGraphQl\Model\Resolver\Product\Fields; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Bundle\Model\Product\Type as Bundle; use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Query\Resolver\Value; -use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; /** - * {@inheritdoc} + * @inheritdoc */ class DynamicWeight implements ResolverInterface { /** - * @var ValueFactory - */ - private $valueFactory; - - /** - * @param ValueFactory $valueFactory - */ - public function __construct(ValueFactory $valueFactory) - { - $this->valueFactory = $valueFactory; - } - - /** - * {@inheritdoc} + * @inheritdoc */ public function resolve( Field $field, @@ -42,18 +26,12 @@ public function resolve( ResolveInfo $info, array $value = null, array $args = null - ): Value { - $result = function () { - return null; - }; + ) { + $result = null; if ($value['type_id'] === Bundle::TYPE_CODE) { $result = isset($value['weight_type']) ? !$value['weight_type'] : null; } - return $this->valueFactory->create( - function () use ($result) { - return $result; - } - ); + return $result; } } diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/PriceView.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/PriceView.php index ef8e93748c73..b7351b09d437 100644 --- a/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/PriceView.php +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/PriceView.php @@ -5,19 +5,16 @@ */ declare(strict_types=1); - namespace Magento\BundleGraphQl\Model\Resolver\Product\Fields; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Bundle\Model\Product\Type as Bundle; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\EnumLookup; -use Magento\Framework\GraphQl\Query\Resolver\Value; -use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; /** - * {@inheritdoc} + * @inheritdoc */ class PriceView implements ResolverInterface { @@ -26,23 +23,16 @@ class PriceView implements ResolverInterface */ private $enumLookup; - /** - * @var ValueFactory - */ - private $valueFactory; - /** * @param EnumLookup $enumLookup - * @param ValueFactory $valueFactory */ - public function __construct(EnumLookup $enumLookup, ValueFactory $valueFactory) + public function __construct(EnumLookup $enumLookup) { $this->enumLookup = $enumLookup; - $this->valueFactory = $valueFactory; } /** - * {@inheritdoc} + * @inheritdoc */ public function resolve( Field $field, @@ -50,19 +40,13 @@ public function resolve( ResolveInfo $info, array $value = null, array $args = null - ): Value { - $result = function () { - return null; - }; + ) { + $result = null; if ($value['type_id'] === Bundle::TYPE_CODE) { $result = isset($value['price_view']) ? $this->enumLookup->getEnumValueFromField('PriceViewEnum', $value['price_view']) : null; } - return $this->valueFactory->create( - function () use ($result) { - return $result; - } - ); + return $result; } } diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/ShipBundleItems.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/ShipBundleItems.php index e2bd12a84e2b..6babf6520e10 100644 --- a/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/ShipBundleItems.php +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Product/Fields/ShipBundleItems.php @@ -5,19 +5,16 @@ */ declare(strict_types=1); - namespace Magento\BundleGraphQl\Model\Resolver\Product\Fields; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Bundle\Model\Product\Type as Bundle; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\EnumLookup; -use Magento\Framework\GraphQl\Query\Resolver\Value; -use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; /** - * {@inheritdoc} + * @inheritdoc */ class ShipBundleItems implements ResolverInterface { @@ -26,23 +23,16 @@ class ShipBundleItems implements ResolverInterface */ private $enumLookup; - /** - * @var ValueFactory - */ - private $valueFactory; - /** * @param EnumLookup $enumLookup - * @param ValueFactory $valueFactory */ - public function __construct(EnumLookup $enumLookup, ValueFactory $valueFactory) + public function __construct(EnumLookup $enumLookup) { $this->enumLookup = $enumLookup; - $this->valueFactory = $valueFactory; } /** - * {@inheritdoc} + * @inheritdoc */ public function resolve( Field $field, @@ -50,19 +40,10 @@ public function resolve( ResolveInfo $info, array $value = null, array $args = null - ): Value { - $result = function () { - return null; - }; - if ($value['type_id'] === Bundle::TYPE_CODE) { - $result = isset($value['shipment_type']) - ? $this->enumLookup->getEnumValueFromField('ShipBundleItemsEnum', $value['shipment_type']) : null; - } + ) { + $result = isset($value['shipment_type']) && $value['type_id'] === Bundle::TYPE_CODE + ? $this->enumLookup->getEnumValueFromField('ShipBundleItemsEnum', $value['shipment_type']) : null; - return $this->valueFactory->create( - function () use ($result) { - return $result; - } - ); + return $result; } } diff --git a/app/code/Magento/BundleImportExport/Model/Export/RowCustomizer.php b/app/code/Magento/BundleImportExport/Model/Export/RowCustomizer.php index e0c94097e4d3..2cefc60a4297 100644 --- a/app/code/Magento/BundleImportExport/Model/Export/RowCustomizer.php +++ b/app/code/Magento/BundleImportExport/Model/Export/RowCustomizer.php @@ -322,7 +322,7 @@ function ($title, $storeName) { */ protected function getTypeValue($type) { - return isset($this->typeMapping[$type]) ? $this->typeMapping[$type] : self::VALUE_DYNAMIC; + return $this->typeMapping[$type] ?? self::VALUE_DYNAMIC; } /** @@ -333,7 +333,7 @@ protected function getTypeValue($type) */ protected function getPriceViewValue($type) { - return isset($this->priceViewMapping[$type]) ? $this->priceViewMapping[$type] : self::VALUE_PRICE_RANGE; + return $this->priceViewMapping[$type] ?? self::VALUE_PRICE_RANGE; } /** @@ -344,7 +344,7 @@ protected function getPriceViewValue($type) */ protected function getPriceTypeValue($type) { - return isset($this->priceTypeMapping[$type]) ? $this->priceTypeMapping[$type] : null; + return $this->priceTypeMapping[$type] ?? null; } /** @@ -355,7 +355,7 @@ protected function getPriceTypeValue($type) */ private function getShipmentTypeValue($type) { - return isset($this->shipmentTypeMapping[$type]) ? $this->shipmentTypeMapping[$type] : null; + return $this->shipmentTypeMapping[$type] ?? null; } /** diff --git a/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php b/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php index 3ed7e144ddd5..81a47d72602b 100644 --- a/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php +++ b/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php @@ -20,6 +20,7 @@ /** * Class Bundle + * * @package Magento\BundleImportExport\Model\Import\Product\Type * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ @@ -349,6 +350,8 @@ protected function populateSelectionTemplate($selection, $optionId, $parentId, $ } /** + * Deprecated method for retrieving mapping between skus and products. + * * @deprecated Misspelled method * @see retrieveProductsByCachedSkus */ @@ -600,6 +603,7 @@ protected function insertOptions() /** * Populate array for insert option values + * * @param array $optionIds * @return array */ @@ -779,7 +783,7 @@ protected function clear() */ private function getStoreIdByCode(string $storeCode): int { - if (!isset($this->storeIdToCode[$storeCode])) { + if (!isset($this->storeCodeToId[$storeCode])) { /** @var $store \Magento\Store\Model\Store */ foreach ($this->storeManager->getStores() as $store) { $this->storeCodeToId[$store->getCode()] = $store->getId(); diff --git a/app/code/Magento/BundleImportExport/Test/Unit/Model/Import/Product/Type/BundleTest.php b/app/code/Magento/BundleImportExport/Test/Unit/Model/Import/Product/Type/BundleTest.php index b0794f456464..a8650a4e6e9e 100644 --- a/app/code/Magento/BundleImportExport/Test/Unit/Model/Import/Product/Type/BundleTest.php +++ b/app/code/Magento/BundleImportExport/Test/Unit/Model/Import/Product/Type/BundleTest.php @@ -242,7 +242,7 @@ public function testSaveData($skus, $bunch, $allowImport) 'price_type' => 'fixed', 'shipment_type' => '1', 'default_qty' => '1', - 'is_defaul' => '1', + 'is_default' => '1', 'position' => '1', 'option_id' => '1'] ] @@ -264,7 +264,7 @@ public function testSaveData($skus, $bunch, $allowImport) 'price_type' => 'percent', 'shipment_type' => 0, 'default_qty' => '2', - 'is_defaul' => '1', + 'is_default' => '1', 'position' => '6', 'option_id' => '6'] ] @@ -324,7 +324,7 @@ public function saveDataProvider() . 'price_type=fixed,' . 'shipment_type=separately,' . 'default_qty=1,' - . 'is_defaul=1,' + . 'is_default=1,' . 'position=1,' . 'option_id=1 | name=Bundle2,' . 'type=dropdown,' @@ -333,7 +333,7 @@ public function saveDataProvider() . 'price=10,' . 'price_type=fixed,' . 'default_qty=1,' - . 'is_defaul=1,' + . 'is_default=1,' . 'position=2,' . 'option_id=2' ], diff --git a/app/code/Magento/CacheInvalidate/Model/PurgeCache.php b/app/code/Magento/CacheInvalidate/Model/PurgeCache.php index 8acf170d43cf..b2aa0d000e9c 100644 --- a/app/code/Magento/CacheInvalidate/Model/PurgeCache.php +++ b/app/code/Magento/CacheInvalidate/Model/PurgeCache.php @@ -7,6 +7,9 @@ use Magento\Framework\Cache\InvalidateLogger; +/** + * Class PurgeCache + */ class PurgeCache { const HEADER_X_MAGENTO_TAGS_PATTERN = 'X-Magento-Tags-Pattern'; @@ -26,6 +29,18 @@ class PurgeCache */ private $logger; + /** + * Batch size of the purge request. + * + * Based on default Varnish 4 http_req_hdr_len size minus a 512 bytes margin for method, + * header name, line feeds etc. + * + * @see https://varnish-cache.org/docs/4.1/reference/varnishd.html + * + * @var int + */ + private $requestSize = 7680; + /** * Constructor * @@ -44,18 +59,66 @@ public function __construct( } /** - * Send curl purge request - * to invalidate cache by tags pattern + * Send curl purge request to invalidate cache by tags pattern * * @param string $tagsPattern * @return bool Return true if successful; otherwise return false */ public function sendPurgeRequest($tagsPattern) { + $successful = true; $socketAdapter = $this->socketAdapterFactory->create(); $servers = $this->cacheServer->getUris(); - $headers = [self::HEADER_X_MAGENTO_TAGS_PATTERN => $tagsPattern]; $socketAdapter->setOptions(['timeout' => 10]); + + $formattedTagsChunks = $this->splitTags($tagsPattern); + foreach ($formattedTagsChunks as $formattedTagsChunk) { + if (!$this->sendPurgeRequestToServers($socketAdapter, $servers, $formattedTagsChunk)) { + $successful = false; + } + } + + return $successful; + } + + /** + * Split tags by batches + * + * @param string $tagsPattern + * @return \Generator + */ + private function splitTags($tagsPattern) + { + $tagsBatchSize = 0; + $formattedTagsChunk = []; + $formattedTags = explode('|', $tagsPattern); + foreach ($formattedTags as $formattedTag) { + if ($tagsBatchSize + strlen($formattedTag) > $this->requestSize - count($formattedTagsChunk) - 1) { + yield implode('|', $formattedTagsChunk); + $formattedTagsChunk = []; + $tagsBatchSize = 0; + } + + $tagsBatchSize += strlen($formattedTag); + $formattedTagsChunk[] = $formattedTag; + } + if (!empty($formattedTagsChunk)) { + yield implode('|', $formattedTagsChunk); + } + } + + /** + * Send curl purge request to servers to invalidate cache by tags pattern + * + * @param \Zend\Http\Client\Adapter\Socket $socketAdapter + * @param \Zend\Uri\Uri[] $servers + * @param string $formattedTagsChunk + * @return bool Return true if successful; otherwise return false + */ + private function sendPurgeRequestToServers($socketAdapter, $servers, $formattedTagsChunk) + { + $headers = [self::HEADER_X_MAGENTO_TAGS_PATTERN => $formattedTagsChunk]; + $unresponsiveServerError = []; foreach ($servers as $server) { $headers['Host'] = $server->getHost(); try { @@ -69,12 +132,31 @@ public function sendPurgeRequest($tagsPattern) $socketAdapter->read(); $socketAdapter->close(); } catch (\Exception $e) { - $this->logger->critical($e->getMessage(), compact('server', 'tagsPattern')); + $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', 'tagsPattern')); + $this->logger->execute(compact('servers', 'formattedTagsChunk')); return true; } } diff --git a/app/code/Magento/Captcha/Controller/Refresh/Index.php b/app/code/Magento/Captcha/Controller/Refresh/Index.php index e89a80646ed8..e401e03e9551 100644 --- a/app/code/Magento/Captcha/Controller/Refresh/Index.php +++ b/app/code/Magento/Captcha/Controller/Refresh/Index.php @@ -8,9 +8,10 @@ */ namespace Magento\Captcha\Controller\Refresh; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\App\Action\Context; -class Index extends \Magento\Framework\App\Action\Action +class Index extends \Magento\Framework\App\Action\Action implements HttpPostActionInterface { /** * @var \Magento\Captcha\Helper\Data diff --git a/app/code/Magento/Captcha/CustomerData/Captcha.php b/app/code/Magento/Captcha/CustomerData/Captcha.php new file mode 100644 index 000000000000..a744daacdc67 --- /dev/null +++ b/app/code/Magento/Captcha/CustomerData/Captcha.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Captcha\CustomerData; + +use Magento\Customer\CustomerData\SectionSourceInterface; + +/** + * Captcha section + */ +class Captcha extends \Magento\Framework\DataObject implements SectionSourceInterface +{ + /** + * @var array + */ + private $formIds; + + /** + * @var \Magento\Captcha\Helper\Data + */ + private $helper; + + /** + * @param \Magento\Captcha\Helper\Data $helper + * @param array $formIds + * @param array $data + * @codeCoverageIgnore + */ + public function __construct( + \Magento\Captcha\Helper\Data $helper, + array $formIds, + array $data = [] + ) { + parent::__construct($data); + $this->helper = $helper; + $this->formIds = $formIds; + } + + /** + * @inheritdoc + */ + public function getSectionData() :array + { + $data = []; + + foreach ($this->formIds as $formId) { + $captchaModel = $this->helper->getCaptcha($formId); + $data[$formId] = [ + 'isRequired' => $captchaModel->isRequired(), + 'timestamp' => time() + ]; + } + + return $data; + } +} diff --git a/app/code/Magento/Captcha/Model/Checkout/ConfigProvider.php b/app/code/Magento/Captcha/Model/Checkout/ConfigProvider.php index ef5f5a8edce7..34ee62044ff5 100644 --- a/app/code/Magento/Captcha/Model/Checkout/ConfigProvider.php +++ b/app/code/Magento/Captcha/Model/Checkout/ConfigProvider.php @@ -5,6 +5,9 @@ */ namespace Magento\Captcha\Model\Checkout; +/** + * Configuration provider for Captcha rendering. + */ class ConfigProvider implements \Magento\Checkout\Model\ConfigProviderInterface { /** @@ -38,7 +41,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getConfig() { @@ -49,7 +52,8 @@ public function getConfig() 'imageHeight' => $this->getImageHeight($formId), 'imageSrc' => $this->getImageSrc($formId), 'refreshUrl' => $this->getRefreshUrl(), - 'isRequired' => $this->isRequired($formId) + 'isRequired' => $this->isRequired($formId), + 'timestamp' => time() ]; } return $config; diff --git a/app/code/Magento/Captcha/Model/Customer/Plugin/AjaxLogin.php b/app/code/Magento/Captcha/Model/Customer/Plugin/AjaxLogin.php index 91f3a785df36..84ac71046c34 100644 --- a/app/code/Magento/Captcha/Model/Customer/Plugin/AjaxLogin.php +++ b/app/code/Magento/Captcha/Model/Customer/Plugin/AjaxLogin.php @@ -10,6 +10,9 @@ use Magento\Framework\Session\SessionManagerInterface; use Magento\Framework\Controller\Result\JsonFactory; +/** + * Around plugin for login action. + */ class AjaxLogin { /** @@ -61,6 +64,8 @@ public function __construct( } /** + * Check captcha data on login action. + * * @param \Magento\Customer\Controller\Ajax\Login $subject * @param \Closure $proceed * @return $this @@ -94,18 +99,20 @@ public function aroundExecute( if ($formId === $loginFormId) { $captchaModel = $this->helper->getCaptcha($formId); if ($captchaModel->isRequired($username)) { - $captchaModel->logAttempt($username); if (!$captchaModel->isCorrect($captchaString)) { $this->sessionManager->setUsername($username); + $captchaModel->logAttempt($username); return $this->returnJsonError(__('Incorrect CAPTCHA')); } } + $captchaModel->logAttempt($username); } } return $proceed(); } /** + * Format JSON response. * * @param \Magento\Framework\Phrase $phrase * @return \Magento\Framework\Controller\Result\Json diff --git a/app/code/Magento/Captcha/Model/DefaultModel.php b/app/code/Magento/Captcha/Model/DefaultModel.php index cf6690df5c85..483f9c3fb4d2 100644 --- a/app/code/Magento/Captcha/Model/DefaultModel.php +++ b/app/code/Magento/Captcha/Model/DefaultModel.php @@ -78,6 +78,11 @@ class DefaultModel extends \Zend\Captcha\Image implements \Magento\Captcha\Model */ protected $session; + /** + * @var string + */ + private $words; + /** * @param \Magento\Framework\Session\SessionManagerInterface $session * @param \Magento\Captcha\Helper\Data $captchaData @@ -311,18 +316,18 @@ public function getImgUrl() */ public function isCorrect($word) { - $storedWord = $this->getWord(); + $storedWords = $this->getWords(); $this->clearWord(); - if (!$word || !$storedWord) { + if (!$word || !$storedWords) { return false; } if (!$this->isCaseSensitive()) { - $storedWord = strtolower($storedWord); + $storedWords = strtolower($storedWords); $word = strtolower($word); } - return $word === $storedWord; + return in_array($word, explode(',', $storedWords)); } /** @@ -481,7 +486,7 @@ private function getTargetForms() /** * Get captcha word * - * @return string + * @return string|null */ public function getWord() { @@ -489,6 +494,17 @@ public function getWord() return time() < $sessionData['expires'] ? $sessionData['data'] : null; } + /** + * Get captcha words + * + * @return string|null + */ + private function getWords() + { + $sessionData = $this->session->getData($this->getFormIdKey(self::SESSION_WORD)); + return time() < $sessionData['expires'] ? $sessionData['words'] : null; + } + /** * Set captcha word * @@ -498,9 +514,10 @@ public function getWord() */ protected function setWord($word) { + $this->words = $this->words ? $this->words . ',' . $word : $word; $this->session->setData( $this->getFormIdKey(self::SESSION_WORD), - ['data' => $word, 'expires' => time() + $this->getTimeout()] + ['data' => $word, 'words' => $this->words, 'expires' => time() + $this->getTimeout()] ); $this->word = $word; return $this; diff --git a/app/code/Magento/Captcha/Observer/CaptchaStringResolver.php b/app/code/Magento/Captcha/Observer/CaptchaStringResolver.php index 9b97225e60de..39579616fa92 100644 --- a/app/code/Magento/Captcha/Observer/CaptchaStringResolver.php +++ b/app/code/Magento/Captcha/Observer/CaptchaStringResolver.php @@ -18,6 +18,6 @@ public function resolve(\Magento\Framework\App\RequestInterface $request, $formI { $captchaParams = $request->getPost(\Magento\Captcha\Helper\Data::INPUT_NAME_FIELD_VALUE); - return isset($captchaParams[$formId]) ? $captchaParams[$formId] : ''; + return $captchaParams[$formId] ?? ''; } } diff --git a/app/code/Magento/Captcha/Observer/CheckUserLoginObserver.php b/app/code/Magento/Captcha/Observer/CheckUserLoginObserver.php index bdc8dfa21897..dd4974c5d842 100644 --- a/app/code/Magento/Captcha/Observer/CheckUserLoginObserver.php +++ b/app/code/Magento/Captcha/Observer/CheckUserLoginObserver.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Captcha\Observer; use Magento\Customer\Model\AuthenticationInterface; @@ -11,7 +12,10 @@ use Magento\Customer\Api\CustomerRepositoryInterface; /** + * Check captcha on user login page observer. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class CheckUserLoginObserver implements ObserverInterface { @@ -140,7 +144,7 @@ public function execute(\Magento\Framework\Event\Observer $observer) $customer = $this->getCustomerRepository()->get($login); $this->getAuthentication()->processAuthenticationFailure($customer->getId()); } catch (NoSuchEntityException $e) { - //do nothing as customer existance is validated later in authenticate method + //do nothing as customer existence is validated later in authenticate method } $this->messageManager->addError(__('Incorrect CAPTCHA')); $this->_actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true); diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AdminLoginWithCaptchaActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AdminLoginWithCaptchaActionGroup.xml new file mode 100644 index 000000000000..07329e265987 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AdminLoginWithCaptchaActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminLoginWithCaptchaActionGroup" extends="LoginAsAdmin"> + <arguments> + <argument name="captcha" type="string" /> + </arguments> + <fillField stepKey="fillCaptchaField" after="fillPassword" userInput="{{captcha}}" selector="{{AdminLoginFormSection.captchaField}}" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaNotVisibleOnCustomerLoginFormActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaNotVisibleOnCustomerLoginFormActionGroup.xml new file mode 100644 index 000000000000..a371f177e355 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaNotVisibleOnCustomerLoginFormActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertCaptchaNotVisibleOnCustomerLoginFormActionGroup"> + <waitForPageLoad stepKey="waitForPageLoaded" /> + <dontSee selector="{{StorefrontCustomerSignInFormSection.captchaField}}" stepKey="dontSeeCaptchaField"/> + <dontSee selector="{{StorefrontCustomerSignInFormSection.captchaImg}}" stepKey="dontSeeCaptchaImage"/> + <dontSee selector="{{StorefrontCustomerSignInFormSection.captchaReload}}" stepKey="dontSeeCaptchaReloadButton"/> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitForPageReloaded" /> + <dontSee selector="{{StorefrontCustomerSignInFormSection.captchaField}}" stepKey="dontSeeCaptchaFieldAfterPageReload"/> + <dontSee selector="{{StorefrontCustomerSignInFormSection.captchaImg}}" stepKey="dontSeeCaptchaImageAfterPageReload"/> + <dontSee selector="{{StorefrontCustomerSignInFormSection.captchaReload}}" stepKey="dontSeeCaptchaReloadButtonAfterPageReload"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnAdminLoginFormActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnAdminLoginFormActionGroup.xml new file mode 100644 index 000000000000..aa02588000d2 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnAdminLoginFormActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertCaptchaVisibleOnAdminLoginFormActionGroup"> + <waitForElementVisible selector="{{AdminLoginFormSection.captchaField}}" stepKey="seeCaptchaField"/> + <waitForElementVisible selector="{{AdminLoginFormSection.captchaImg}}" stepKey="seeCaptchaImage"/> + <waitForElementVisible selector="{{AdminLoginFormSection.captchaReload}}" stepKey="seeCaptchaReloadButton"/> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitForPageReloaded" /> + <waitForElementVisible selector="{{AdminLoginFormSection.captchaField}}" stepKey="seeCaptchaFieldAfterPageReload"/> + <waitForElementVisible selector="{{AdminLoginFormSection.captchaImg}}" stepKey="seeCaptchaImageAfterPageReload"/> + <waitForElementVisible selector="{{AdminLoginFormSection.captchaReload}}" stepKey="seeCaptchaReloadButtonAfterPageReload"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnContactUsFormActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnContactUsFormActionGroup.xml new file mode 100644 index 000000000000..d800c65cabb6 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnContactUsFormActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertCaptchaVisibleOnContactUsFormActionGroup"> + <waitForElementVisible selector="{{StorefrontContactUsFormSection.captchaField}}" stepKey="waitToSeeCaptchaField"/> + <waitForElementVisible selector="{{StorefrontContactUsFormSection.captchaImg}}" stepKey="waitToSeeCaptchaImage"/> + <waitForElementVisible selector="{{StorefrontContactUsFormSection.captchaReload}}" stepKey="waitToSeeCaptchaReloadButton"/> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitForPageReloaded" /> + <waitForElementVisible selector="{{StorefrontContactUsFormSection.captchaField}}" stepKey="waitToSeeCaptchaFieldAfterPageReload"/> + <waitForElementVisible selector="{{StorefrontContactUsFormSection.captchaImg}}" stepKey="waitToSeeCaptchaImageAfterPageReload"/> + <waitForElementVisible selector="{{StorefrontContactUsFormSection.captchaReload}}" stepKey="waitToSeeCaptchaReloadButtonAfterPageReload"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerAccountCreatePageActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerAccountCreatePageActionGroup.xml new file mode 100644 index 000000000000..6c09d1d49381 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerAccountCreatePageActionGroup.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertCaptchaVisibleOnCustomerAccountCreatePageActionGroup"> + <waitForElementVisible selector="{{StorefrontCustomerCreateFormSection.captchaField}}" stepKey="waitForCaptchaField"/> + <waitForElementVisible selector="{{StorefrontCustomerCreateFormSection.captchaImg}}" stepKey="waitForCaptchaImage"/> + <waitForElementVisible selector="{{StorefrontCustomerCreateFormSection.captchaReload}}" stepKey="waitForCaptchaReloadButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerAccountInfoActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerAccountInfoActionGroup.xml new file mode 100644 index 000000000000..c68ffbfb5be4 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerAccountInfoActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertCaptchaVisibleOnCustomerAccountInfoActionGroup"> + <checkOption selector="{{StorefrontCustomerAccountInformationSection.changeEmail}}" stepKey="clickChangeEmailCheckbox" /> + <waitForElementVisible selector="{{StorefrontCustomerAccountInformationSection.captchaField}}" stepKey="seeCaptchaField"/> + <waitForElementVisible selector="{{StorefrontCustomerAccountInformationSection.captchaImg}}" stepKey="seeCaptchaImage"/> + <waitForElementVisible selector="{{StorefrontCustomerAccountInformationSection.captchaReload}}" stepKey="seeCaptchaReloadButton"/> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitForPageReloaded" /> + <checkOption selector="{{StorefrontCustomerAccountInformationSection.changeEmail}}" stepKey="clickChangeEmailCheckboxAfterPageReload" /> + <waitForElementVisible selector="{{StorefrontCustomerAccountInformationSection.captchaField}}" stepKey="seeCaptchaFieldAfterPageReload"/> + <waitForElementVisible selector="{{StorefrontCustomerAccountInformationSection.captchaImg}}" stepKey="seeCaptchaImageAfterPageReload"/> + <waitForElementVisible selector="{{StorefrontCustomerAccountInformationSection.captchaReload}}" stepKey="seeCaptchaReloadButtonAfterPageReload"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerLoginFormActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerLoginFormActionGroup.xml new file mode 100644 index 000000000000..5616b099c026 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerLoginFormActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertCaptchaVisibleOnCustomerLoginFormActionGroup"> + <waitForElementVisible selector="{{StorefrontCustomerSignInFormSection.captchaField}}" stepKey="waitToSeeCaptchaField"/> + <waitForElementVisible selector="{{StorefrontCustomerSignInFormSection.captchaImg}}" stepKey="waitToSeeCaptchaImage"/> + <waitForElementVisible selector="{{StorefrontCustomerSignInFormSection.captchaReload}}" stepKey="waitToSeeCaptchaReloadButton"/> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitForPageReloaded" /> + <waitForElementVisible selector="{{StorefrontCustomerSignInFormSection.captchaField}}" stepKey="waitToSeeCaptchaFieldAfterPageReload"/> + <waitForElementVisible selector="{{StorefrontCustomerSignInFormSection.captchaImg}}" stepKey="waitToSeeCaptchaImageAfterPageReload"/> + <waitForElementVisible selector="{{StorefrontCustomerSignInFormSection.captchaReload}}" stepKey="waitToSeeCaptchaReloadButtonAfterPageReload"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/CaptchaFormsDisplayingActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/CaptchaFormsDisplayingActionGroup.xml index 71a876bbbcdb..f3b6eb1d9af8 100644 --- a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/CaptchaFormsDisplayingActionGroup.xml +++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/CaptchaFormsDisplayingActionGroup.xml @@ -7,13 +7,13 @@ --> <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="CaptchaFormsDisplayingActionGroup"> <click selector="{{CaptchaFormsDisplayingSection.store}}" stepKey="ClickToGoStores"/> <waitForPageLoad stepKey="waitForStoresLoaded"/> <click selector="{{CaptchaFormsDisplayingSection.config}}" stepKey="ClickToGoConfiguration"/> <waitForPageLoad stepKey="waitForConfigurationsLoaded"/> - <scrollTo selector="{{CaptchaFormsDisplayingSection.customer}}" stepKey="ScrollToCustomers"/> + <scrollTo selector="{{CaptchaFormsDisplayingSection.customer}}" x="0" y="-80" stepKey="ScrollToCustomers"/> <click selector="{{CaptchaFormsDisplayingSection.customer}}" stepKey="ClickToCustomers"/> <waitForPageLoad stepKey="waitForCustomerConfigurationsLoaded"/> <click selector="{{CaptchaFormsDisplayingSection.customerConfig}}" stepKey="ClickToGoCustomerConfiguration"/> diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontCustomerChangeEmailWithCaptchaActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontCustomerChangeEmailWithCaptchaActionGroup.xml new file mode 100644 index 000000000000..8aff3d5482f2 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontCustomerChangeEmailWithCaptchaActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontCustomerChangeEmailWithCaptchaActionGroup" extends="StorefrontCustomerChangeEmailActionGroup"> + <arguments> + <argument name="captcha" type="string" /> + </arguments> + + <fillField selector="{{StorefrontCustomerAccountInformationSection.captchaField}}" userInput="{{captcha}}" stepKey="fillCaptchaField" after="fillCurrentPassword" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontFillContactUsFormWithCaptchaActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontFillContactUsFormWithCaptchaActionGroup.xml new file mode 100644 index 000000000000..3546fa2e57a3 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontFillContactUsFormWithCaptchaActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontFillContactUsFormWithCaptchaActionGroup" extends="StorefrontFillContactUsFormActionGroup"> + <arguments> + <argument name="captcha" type="string" /> + </arguments> + <fillField stepKey="fillCaptchaField" after="fillComment" userInput="{{captcha}}" selector="{{StorefrontContactUsFormSection.captchaField}}" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontFillCustomerAccountCreationFormWithCaptchaActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontFillCustomerAccountCreationFormWithCaptchaActionGroup.xml new file mode 100644 index 000000000000..d67ebc1a0076 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontFillCustomerAccountCreationFormWithCaptchaActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontFillCustomerAccountCreationFormWithCaptchaActionGroup" extends="StorefrontFillCustomerAccountCreationFormActionGroup"> + <arguments> + <argument name="captcha" type="string" /> + </arguments> + <fillField stepKey="fillCaptchaField" after="fillConfirmPassword" userInput="{{captcha}}" selector="{{StorefrontCustomerCreateFormSection.captchaField}}" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontFillCustomerLoginFormWithCaptchaActionGroup.xml b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontFillCustomerLoginFormWithCaptchaActionGroup.xml new file mode 100644 index 000000000000..5ad727a8fe99 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/ActionGroup/StorefrontFillCustomerLoginFormWithCaptchaActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontFillCustomerLoginFormWithCaptchaActionGroup" extends="StorefrontFillCustomerLoginFormActionGroup"> + <arguments> + <argument name="captcha" type="string" /> + </arguments> + <fillField stepKey="fillCaptchaField" after="fillPassword" userInput="{{captcha}}" selector="{{StorefrontCustomerSignInFormSection.captchaField}}" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaConfigData.xml b/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaConfigData.xml new file mode 100644 index 000000000000..90f48c320f2a --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaConfigData.xml @@ -0,0 +1,142 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="StorefrontCustomerCaptchaEnableConfigData"> + <!-- Magento default value --> + <data key="path">customer/captcha/enable</data> + <data key="scope_id">0</data> + <data key="label">Yes</data> + <data key="value">1</data> + </entity> + <entity name="StorefrontCustomerCaptchaDisableConfigData"> + <data key="path">customer/captcha/enable</data> + <data key="scope_id">0</data> + <data key="label">No</data> + <data key="value">0</data> + </entity> + <entity name="StorefrontCaptchaOnCustomerCreateFormConfigData"> + <data key="path">customer/captcha/forms</data> + <data key="scope_id">0</data> + <data key="label">Create user</data> + <data key="value">user_create</data> + </entity> + <entity name="StorefrontCaptchaOnContactUsFormConfigData"> + <data key="path">customer/captcha/forms</data> + <data key="scope_id">0</data> + <data key="label">Contact Us</data> + <data key="value">contact_us</data> + </entity> + <entity name="StorefrontCaptchaOnCustomerLoginConfigData"> + <!-- Magento default value --> + <data key="path">customer/captcha/forms</data> + <data key="scope_id">0</data> + <data key="label">Login</data> + <data key="value">user_login</data> + </entity> + <entity name="StorefrontCaptchaOnCustomerChangePasswordConfigData"> + <data key="path">customer/captcha/forms</data> + <data key="scope_id">0</data> + <data key="label">Change password</data> + <data key="value">user_edit</data> + </entity> + <entity name="StorefrontCaptchaOnCustomerForgotPasswordConfigData"> + <!-- Magento default value --> + <data key="path">customer/captcha/forms</data> + <data key="scope_id">0</data> + <data key="label">Forgot password</data> + <data key="value">user_forgotpassword</data> + </entity> + <entity name="StorefrontCustomerCaptchaModeAlwaysConfigData"> + <data key="path">customer/captcha/mode</data> + <data key="scope_id">0</data> + <data key="label">Always</data> + <data key="value">always</data> + </entity> + <entity name="StorefrontCustomerCaptchaModeAfterFailConfigData"> + <!-- Magento default value --> + <data key="path">customer/captcha/mode</data> + <data key="scope_id">0</data> + <data key="label">After number of attempts to login</data> + <data key="value">after_fail</data> + </entity> + <entity name="StorefrontCustomerCaptchaLength3ConfigData"> + <data key="path">customer/captcha/length</data> + <data key="scope">admin</data> + <data key="scope_id">1</data> + <data key="label">3</data> + <data key="value">3</data> + </entity> + <entity name="StorefrontCustomerCaptchaSymbols1ConfigData"> + <data key="path">customer/captcha/symbols</data> + <data key="scope">admin</data> + <data key="scope_id">1</data> + <data key="label">1</data> + <data key="value">1</data> + </entity> + <entity name="StorefrontCustomerCaptchaDefaultLengthConfigData"> + <!-- Magento default value --> + <data key="path">customer/captcha/length</data> + <data key="scope">admin</data> + <data key="scope_id">1</data> + <data key="label">4-5</data> + <data key="value">4-5</data> + </entity> + <entity name="StorefrontCustomerCaptchaDefaultSymbolsConfigData"> + <!-- Magento default value --> + <data key="path">customer/captcha/symbols</data> + <data key="scope">admin</data> + <data key="scope_id">1</data> + <data key="label">ABCDEFGHJKMnpqrstuvwxyz23456789</data> + <data key="value">ABCDEFGHJKMnpqrstuvwxyz23456789</data> + </entity> + <entity name="AdminCaptchaEnableConfigData"> + <!-- Magento default value --> + <data key="path">admin/captcha/enable</data> + <data key="scope_id">0</data> + <data key="label">Yes</data> + <data key="value">1</data> + </entity> + <entity name="AdminCaptchaDisableConfigData"> + <data key="path">admin/captcha/enable</data> + <data key="scope_id">0</data> + <data key="label">No</data> + <data key="value">0</data> + </entity> + <entity name="AdminCaptchaLength3ConfigData"> + <data key="path">admin/captcha/length</data> + <data key="scope">admin</data> + <data key="scope_id">1</data> + <data key="label">3</data> + <data key="value">3</data> + </entity> + <entity name="AdminCaptchaSymbols1ConfigData"> + <data key="path">admin/captcha/symbols</data> + <data key="scope">admin</data> + <data key="scope_id">1</data> + <data key="label">1</data> + <data key="value">1</data> + </entity> + <entity name="AdminCaptchaDefaultLengthConfigData"> + <!-- Magento default value --> + <data key="path">admin/captcha/length</data> + <data key="scope">admin</data> + <data key="scope_id">1</data> + <data key="label">4-5</data> + <data key="value">4-5</data> + </entity> + <entity name="AdminCaptchaDefaultSymbolsConfigData"> + <!-- Magento default value --> + <data key="path">admin/captcha/symbols</data> + <data key="scope">admin</data> + <data key="scope_id">1</data> + <data key="label">ABCDEFGHJKMnpqrstuvwxyz23456789</data> + <data key="value">ABCDEFGHJKMnpqrstuvwxyz23456789</data> + </entity> +</entities> diff --git a/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaData.xml b/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaData.xml new file mode 100644 index 000000000000..d8fb206b8111 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaData.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="WrongCaptcha"> + <data key="value" unique="suffix">WrongCAPTCHA</data> + </entity> + + <!-- This CAPTCHA will only work if "StorefrontCustomerCaptchaLength3ConfigData" and "StorefrontCustomerCaptchaSymbols1ConfigData" config is set. --> + <entity name="PreconfiguredCaptcha"> + <data key="value">111</data> + </entity> +</entities> diff --git a/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaFormsDisplayingData.xml b/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaFormsDisplayingData.xml index 9db8110c0f64..57a09219fe4d 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaFormsDisplayingData.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaFormsDisplayingData.xml @@ -7,7 +7,7 @@ --> <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> <entity name="CaptchaData"> <data key="createUser">Create user</data> <data key="login">Login</data> diff --git a/app/code/Magento/Captcha/Test/Mftf/Section/AdminLoginFormSection.xml b/app/code/Magento/Captcha/Test/Mftf/Section/AdminLoginFormSection.xml new file mode 100644 index 000000000000..2bcc6fc542d8 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/Section/AdminLoginFormSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminLoginFormSection"> + <element name="captchaField" type="input" selector="#login-form input[name='captcha[backend_login]']" /> + <element name="captchaImg" type="block" selector="#login-form img#backend_login"/> + <element name="captchaReload" type="block" selector="#login-form img#captcha-reload"/> + </section> +</sections> diff --git a/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontContactUsCaptchaSection.xml b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontContactUsCaptchaSection.xml new file mode 100644 index 000000000000..f587812576ff --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontContactUsCaptchaSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontContactUsCaptchaSection"> + <element name="captchaField" type="input" selector="#captcha_contact_us"/> + <element name="captchaImg" type="block" selector=".captcha-img"/> + <element name="captchaReload" type="block" selector=".captcha-reload"/> + </section> +</sections> diff --git a/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontContactUsFormSection.xml b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontContactUsFormSection.xml new file mode 100644 index 000000000000..60cf961ba7e8 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontContactUsFormSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontContactUsFormSection"> + <element name="captchaField" type="input" selector="#contact-form input[name='captcha[contact_us]']" /> + <element name="captchaImg" type="block" selector="#contact-form img.captcha-img"/> + <element name="captchaReload" type="block" selector="#contact-form button.captcha-reload"/> + </section> +</sections> diff --git a/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerAccountInformationSection.xml b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerAccountInformationSection.xml new file mode 100644 index 000000000000..a273c8d4abd2 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerAccountInformationSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCustomerAccountInformationSection"> + <element name="captchaField" type="input" selector="#captcha_user_edit"/> + <element name="captchaImg" type="block" selector=".captcha-img"/> + <element name="captchaReload" type="block" selector=".captcha-reload"/> + </section> +</sections> diff --git a/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerCreateFormSection.xml b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerCreateFormSection.xml new file mode 100644 index 000000000000..f48e6124cb21 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerCreateFormSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCustomerCreateFormSection"> + <element name="captchaField" type="input" selector="#captcha_user_create"/> + <element name="captchaImg" type="block" selector=".captcha-img"/> + <element name="captchaReload" type="block" selector=".captcha-reload"/> + </section> +</sections> diff --git a/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml new file mode 100644 index 000000000000..54aa36d1ca26 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerSignInFormSection.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="StorefrontCustomerSignInFormSection"> + <element name="captchaField" type="input" selector="#captcha_user_login"/> + <element name="captchaImg" type="block" selector=".captcha-img"/> + <element name="captchaReload" type="block" selector=".captcha-reload"/> + </section> +</sections> diff --git a/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerSignInPopupFormSection.xml b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerSignInPopupFormSection.xml new file mode 100644 index 000000000000..7a0557c4a274 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerSignInPopupFormSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCustomerSignInPopupFormSection"> + <element name="captchaField" type="input" selector="#captcha_user_login"/> + <element name="captchaImg" type="block" selector=".captcha-img"/> + <element name="captchaReload" type="block" selector=".captcha-reload"/> + </section> +</sections> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/AdminLoginWithCaptchaTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/AdminLoginWithCaptchaTest.xml new file mode 100644 index 000000000000..e5ee55910df6 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/Test/AdminLoginWithCaptchaTest.xml @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminLoginWithCaptchaTest"> + <annotations> + <features value="Captcha"/> + <stories value="Admin login + Captcha"/> + <title value="Captcha on Admin login form"/> + <description value="Test creation for admin login with captcha."/> + <testCaseId value="MC-14012" /> + <severity value="MAJOR"/> + <group value="captcha"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <magentoCLI command="config:set {{AdminCaptchaLength3ConfigData.path}} {{AdminCaptchaLength3ConfigData.value}}" stepKey="setCaptchaLength" /> + <magentoCLI command="config:set {{AdminCaptchaSymbols1ConfigData.path}} {{AdminCaptchaSymbols1ConfigData.value}}" stepKey="setCaptchaSymbols" /> + <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + </before> + <after> + <magentoCLI command="config:set {{AdminCaptchaDefaultLengthConfigData.path}} {{AdminCaptchaDefaultLengthConfigData.value}}" stepKey="setDefaultCaptchaLength" /> + <magentoCLI command="config:set {{AdminCaptchaDefaultSymbolsConfigData.path}} {{AdminCaptchaDefaultSymbolsConfigData.value}}" stepKey="setDefaultCaptchaSymbols" /> + <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + </after> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdminWithWrongCredentialsFirstAttempt"> + <argument name="adminUser" value="AdminUserWrongCredentials" /> + </actionGroup> + <actionGroup ref="AssertMessageOnAdminLoginActionGroup" stepKey="seeFirstLoginErrorMessage" /> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdminWithWrongCredentialsSecondAttempt"> + <argument name="adminUser" value="AdminUserWrongCredentials" /> + </actionGroup> + <actionGroup ref="AssertMessageOnAdminLoginActionGroup" stepKey="seeSecondLoginErrorMessage" /> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdminWithWrongCredentialsThirdAttempt"> + <argument name="adminUser" value="AdminUserWrongCredentials" /> + </actionGroup> + <actionGroup ref="AssertMessageOnAdminLoginActionGroup" stepKey="seeThirdLoginErrorMessage" /> + + <!-- Check captcha visibility on admin login page --> + <actionGroup ref="AssertCaptchaVisibleOnAdminLoginFormActionGroup" stepKey="assertCaptchaVisible" /> + + <!-- Submit form with incorrect captcha --> + <actionGroup ref="AdminLoginWithCaptchaActionGroup" stepKey="loginAsAdminWithIncorrectCaptcha"> + <argument name="adminUser" value="DefaultAdminUser" /> + <argument name="captcha" value="{{WrongCaptcha.value}}" /> + </actionGroup> + <actionGroup ref="AssertMessageOnAdminLoginActionGroup" stepKey="seeIncorrectCaptchaErrorMessage"> + <argument name="message" value="Incorrect CAPTCHA." /> + </actionGroup> + <actionGroup ref="AssertCaptchaVisibleOnAdminLoginFormActionGroup" stepKey="assertCaptchaVisibleAfterIncorrectCaptcha" /> + + <actionGroup ref="AdminLoginWithCaptchaActionGroup" stepKey="loginAsAdminWithCorrectCaptcha"> + <argument name="adminUser" value="DefaultAdminUser" /> + <argument name="captcha" value="{{PreconfiguredCaptcha.value}}" /> + </actionGroup> + <actionGroup ref="AssertAdminSuccessLoginActionGroup" stepKey="verifyAdminLoggedIn" /> + </test> +</tests> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/AdminResetUserPasswordFailedTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/AdminResetUserPasswordFailedTest.xml new file mode 100644 index 000000000000..8f9c5828e2f5 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/Test/AdminResetUserPasswordFailedTest.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminResetUserPasswordFailedTest"> + <before> + <magentoCLI command="config:set {{AdminCaptchaDisableConfigData.path}} {{AdminCaptchaDisableConfigData.value}} " stepKey="disableAdminCaptcha"/> + </before> + <after> + <magentoCLI command="config:set {{AdminCaptchaEnableConfigData.path}} {{AdminCaptchaEnableConfigData.value}} " stepKey="enableAdminCaptcha"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest.xml index 8f764899706a..035e58de06cc 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="CaptchaFormsDisplayingTest"> <annotations> <features value="Captcha"/> @@ -64,6 +64,52 @@ <!--Roll back configuration--> <scrollToTopOfPage stepKey="ScrollToTop"/> <click selector="{{CaptchaFormsDisplayingSection.captcha}}" stepKey="ClickToCloseCaptcha"/> - + </test> + <test name="CaptchaWithDisabledGuestCheckout"> + <annotations> + <features value="Captcha"/> + <stories value="MC-5602 - CAPTCHA doesn't appear in login popup after refreshing page."/> + <title value="Captcha is displaying on login form with disabled guest checkout"/> + <description value="Captcha is displaying on login form with disabled guest checkout"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-96691"/> + <group value="captcha"/> + </annotations> + <before> + <magentoCLI command="config:set checkout/options/guest_checkout 0" stepKey="disableGuestCheckout"/> + <magentoCLI command="config:set customer/captcha/failed_attempts_login 1" stepKey="decreaseLoginAttempt"/> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <magentoCLI command="config:set checkout/options/guest_checkout 1" stepKey="enableGuestCheckout"/> + <magentoCLI command="config:set customer/captcha/failed_attempts_login 3" stepKey="increaseLoginAttempt"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct1"/> + </after> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.sku$$)}}" stepKey="openProductPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addToCart" /> + <waitForText userInput="You added $$createSimpleProduct.name$$ to your shopping cart." stepKey="waitForText"/> + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickCart"/> + <click selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="goToCheckout"/> + <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.email}}" stepKey="waitEmailFieldVisible"/> + <fillField selector="{{StorefrontCustomerSignInPopupFormSection.email}}" userInput="{{Simple_US_Customer.email}}" stepKey="fillCustomerEmail"/> + <fillField selector="{{StorefrontCustomerSignInPopupFormSection.password}}" userInput="incorrectPassword" stepKey="fillIncorrectCustomerPassword"/> + <click selector="{{StorefrontCustomerSignInPopupFormSection.signIn}}" stepKey="clickSignIn"/> + <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.errorMessage}}" stepKey="seeErrorMessage"/> + <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.captchaField}}" stepKey="seeCaptchaField"/> + <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.captchaImg}}" stepKey="seeCaptchaImage"/> + <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.captchaReload}}" stepKey="seeCaptchaReloadButton"/> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickCart2"/> + <click selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="goToCheckout2"/> + <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.email}}" stepKey="waitEmailFieldVisible2"/> + <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.captchaField}}" stepKey="seeCaptchaField2"/> + <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.captchaImg}}" stepKey="seeCaptchaImage2"/> + <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.captchaReload}}" stepKey="seeCaptchaReloadButton2"/> </test> </tests> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaEditCustomerEmailTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaEditCustomerEmailTest.xml new file mode 100644 index 000000000000..54237087227d --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaEditCustomerEmailTest.xml @@ -0,0 +1,102 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCaptchaEditCustomerEmailTest"> + <annotations> + <features value="Captcha"/> + <stories value="Customer Account Info Edit + Captcha"/> + <title value="Test for checking captcha on the customer account edit page."/> + <description value="Test for checking captcha on the customer account edit page and customer is locked."/> + <testCaseId value="MC-14013" /> + <severity value="MAJOR"/> + <group value="captcha"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Setup CAPTCHA for testing --> + <magentoCLI command="config:set {{StorefrontCaptchaOnCustomerChangePasswordConfigData.path}} {{StorefrontCaptchaOnCustomerChangePasswordConfigData.value}}" stepKey="enableUserEditCaptcha"/> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaLength3ConfigData.path}} {{StorefrontCustomerCaptchaLength3ConfigData.value}}" stepKey="setCaptchaLength" /> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaSymbols1ConfigData.path}} {{StorefrontCustomerCaptchaSymbols1ConfigData.value}}" stepKey="setCaptchaSymbols" /> + <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + + <createData entity="Simple_US_Customer" stepKey="customer"/> + <!-- Sign in as customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$$customer$$"/> + </actionGroup> + </before> + <after> + <!-- Revert Captcha forms configurations --> + <magentoCLI command="config:set {{StorefrontCaptchaOnCustomerLoginConfigData.path}} {{StorefrontCaptchaOnCustomerLoginConfigData.value}},{{StorefrontCaptchaOnCustomerForgotPasswordConfigData.value}}" stepKey="enableCaptchaOnDefaultForms" /> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultLengthConfigData.path}} {{StorefrontCustomerCaptchaDefaultLengthConfigData.value}}" stepKey="setDefaultCaptchaLength" /> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.path}} {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.value}}" stepKey="setDefaultCaptchaSymbols" /> + <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + </after> + + <!-- Open Customer edit page --> + <actionGroup ref="StorefrontOpenCustomerAccountInfoEditPageActionGroup" stepKey="goToCustomerEditPage" /> + + <!-- Update email with incorrect password 3 times. --> + <actionGroup ref="StorefrontCustomerChangeEmailActionGroup" stepKey="changeEmailFirstAttempt"> + <argument name="email" value="$$customer.email$$" /> + <argument name="password" value="{{Colorado_US_Customer.password}}" /> + </actionGroup> + + <actionGroup ref="AssertMessageCustomerChangeAccountInfoActionGroup" stepKey="assertAccountMessageFirstAttempt"> + <argument name="message" value="The password doesn't match this account. Verify the password and try again." /> + <argument name="messageType" value="error" /> + </actionGroup> + + <actionGroup ref="StorefrontCustomerChangeEmailActionGroup" stepKey="changeEmailSecondAttempt"> + <argument name="email" value="$$customer.email$$" /> + <argument name="password" value="{{Colorado_US_Customer.password}}" /> + </actionGroup> + + <actionGroup ref="AssertMessageCustomerChangeAccountInfoActionGroup" stepKey="assertAccountMessageSecondAttempt"> + <argument name="message" value="The password doesn't match this account. Verify the password and try again." /> + <argument name="messageType" value="error" /> + </actionGroup> + + <actionGroup ref="StorefrontCustomerChangeEmailActionGroup" stepKey="changeEmailThirdAttempt"> + <argument name="email" value="$$customer.email$$" /> + <argument name="password" value="{{Colorado_US_Customer.password}}" /> + </actionGroup> + + <actionGroup ref="AssertMessageCustomerChangeAccountInfoActionGroup" stepKey="assertAccountMessageThirdAttempt"> + <argument name="message" value="The password doesn't match this account. Verify the password and try again." /> + <argument name="messageType" value="error" /> + </actionGroup> + + <!-- Check captcha visibility after incorrect password submit form --> + <actionGroup ref="AssertCaptchaVisibleOnCustomerAccountInfoActionGroup" stepKey="assertCaptchaVisible" /> + + <!-- Try to submit form with incorrect captcha --> + <actionGroup ref="StorefrontCustomerChangeEmailWithCaptchaActionGroup" stepKey="changeEmailWithIncorrectCaptcha"> + <argument name="email" value="$$customer.email$$" /> + <argument name="password" value="{{Colorado_US_Customer.password}}" /> + <argument name="captcha" value="{{WrongCaptcha.value}}" /> + </actionGroup> + + <actionGroup ref="AssertMessageCustomerChangeAccountInfoActionGroup" stepKey="assertAccountMessageAfterIncorrectCaptcha"> + <argument name="message" value="Incorrect CAPTCHA" /> + <argument name="messageType" value="error" /> + </actionGroup> + + <!-- Update customer email correct password and CAPTCHA --> + <actionGroup ref="StorefrontCustomerChangeEmailWithCaptchaActionGroup" stepKey="changeEmailCorrectAttempt"> + <argument name="email" value="$$customer.email$$" /> + <argument name="password" value="$$customer.password$$" /> + <argument name="captcha" value="{{PreconfiguredCaptcha.value}}" /> + </actionGroup> + <actionGroup ref="AssertMessageCustomerChangeAccountInfoActionGroup" stepKey="assertAccountMessageCorrectAttempt" /> + </test> +</tests> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnContactUsTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnContactUsTest.xml new file mode 100644 index 000000000000..0c6a3f31c1df --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnContactUsTest.xml @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCaptchaOnContactUsTest"> + <annotations> + <features value="Captcha"/> + <stories value="Submit Contact us form + Captcha"/> + <title value="Captcha on contact us form test"/> + <description value="Test creation for send comment using the contact us form with captcha."/> + <testCaseId value="MC-14103" /> + <severity value="MAJOR"/> + <group value="captcha"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaLength3ConfigData.path}} {{StorefrontCustomerCaptchaLength3ConfigData.value}}" stepKey="setCaptchaLength" /> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaSymbols1ConfigData.path}} {{StorefrontCustomerCaptchaSymbols1ConfigData.value}}" stepKey="setCaptchaSymbols" /> + <magentoCLI command="config:set {{StorefrontCaptchaOnContactUsFormConfigData.path}} {{StorefrontCaptchaOnContactUsFormConfigData.value}}" stepKey="enableUserEditCaptcha"/> + <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + </before> + <after> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultLengthConfigData.path}} {{StorefrontCustomerCaptchaDefaultLengthConfigData.value}}" stepKey="setDefaultCaptchaLength" /> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.path}} {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.value}}" stepKey="setDefaultCaptchaSymbols" /> + <magentoCLI command="config:set {{StorefrontCaptchaOnCustomerLoginConfigData.path}} {{StorefrontCaptchaOnCustomerLoginConfigData.value}},{{StorefrontCaptchaOnCustomerForgotPasswordConfigData.value}}" stepKey="enableCaptchaOnDefaultForms" /> + <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + </after> + + <!-- Open storefront contact us form --> + <actionGroup ref="StorefrontOpenContactUsPageActionGroup" stepKey="goToContactUsPage" /> + + <!-- Check Captcha items --> + <actionGroup ref="AssertCaptchaVisibleOnContactUsFormActionGroup" stepKey="seeCaptchaOnContactUsForm" /> + + <!-- Submit Contact Us form --> + <actionGroup ref="StorefrontFillContactUsFormWithCaptchaActionGroup" stepKey="fillContactUsFormWithWrongCaptcha"> + <argument name="customer" value="Simple_US_Customer" /> + <argument name="contactUsData" value="DefaultContactUsData" /> + <argument name="captcha" value="{{WrongCaptcha.value}}" /> + </actionGroup> + <actionGroup ref="StorefrontSubmitContactUsFormActionGroup" stepKey="submitContactUsFormWithWrongCaptcha" /> + + <!-- Check Captcha items after form reload --> + <actionGroup ref="AssertMessageContactUsFormActionGroup" stepKey="verifyErrorMessage"> + <argument name="message" value="Incorrect CAPTCHA" /> + <argument name="messageType" value="error" /> + </actionGroup> + <actionGroup ref="AssertCaptchaVisibleOnContactUsFormActionGroup" stepKey="seeCaptchaOnContactUsFormAfterWrongCaptcha" /> + + <actionGroup ref="StorefrontFillContactUsFormWithCaptchaActionGroup" stepKey="fillContactUsFormWithCorrectCaptcha"> + <argument name="customer" value="Simple_US_Customer" /> + <argument name="contactUsData" value="DefaultContactUsData" /> + <argument name="captcha" value="{{PreconfiguredCaptcha.value}}" /> + </actionGroup> + <actionGroup ref="StorefrontSubmitContactUsFormActionGroup" stepKey="submitContactUsFormWithCorrectCaptcha" /> + <actionGroup ref="AssertMessageContactUsFormActionGroup" stepKey="verifySuccessMessage" /> + </test> +</tests> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnCustomerLoginTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnCustomerLoginTest.xml new file mode 100644 index 000000000000..5a1be68d3f25 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnCustomerLoginTest.xml @@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCaptchaOnCustomerLoginTest"> + <annotations> + <features value="Captcha"/> + <stories value="Login with Customer Account + Captcha"/> + <title value="Captcha customer login page test"/> + <description value="Check CAPTCHA on Storefront Login Page."/> + <severity value="MAJOR"/> + <testCaseId value="MC-14010" /> + <group value="captcha"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaLength3ConfigData.path}} {{StorefrontCustomerCaptchaLength3ConfigData.value}}" stepKey="setCaptchaLength" /> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaSymbols1ConfigData.path}} {{StorefrontCustomerCaptchaSymbols1ConfigData.value}}" stepKey="setCaptchaSymbols" /> + <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + <createData entity="Simple_US_Customer" stepKey="customer"/> + </before> + <after> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultLengthConfigData.path}} {{StorefrontCustomerCaptchaDefaultLengthConfigData.value}}" stepKey="setDefaultCaptchaLength" /> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.path}} {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.value}}" stepKey="setDefaultCaptchaSymbols" /> + <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + </after> + + <!-- Open storefront login form --> + <actionGroup ref="StorefrontOpenCustomerLoginPageActionGroup" stepKey="goToSignInPage" /> + + <!-- Login with wrong credentials 3 times --> + <actionGroup ref="StorefrontFillCustomerLoginFormActionGroup" stepKey="fillLoginFormFirstAttempt"> + <argument name="customer" value="Colorado_US_Customer" /> + </actionGroup> + <actionGroup ref="StorefrontClickSignOnCustomerLoginFormActionGroup" stepKey="clickSignInAccountButtonFirstAttempt" /> + <actionGroup ref="AssertMessageCustomerLoginActionGroup" stepKey="seeErrorMessageAfterFirstAttempt" /> + <actionGroup ref="AssertCaptchaNotVisibleOnCustomerLoginFormActionGroup" stepKey="dontSeeCaptchaAfterFirstAttempt" /> + + <actionGroup ref="StorefrontFillCustomerLoginFormActionGroup" stepKey="fillLoginFormSecondAttempt"> + <argument name="customer" value="Colorado_US_Customer" /> + </actionGroup> + <actionGroup ref="StorefrontClickSignOnCustomerLoginFormActionGroup" stepKey="clickSignInAccountButtonSecondAttempt" /> + <actionGroup ref="AssertMessageCustomerLoginActionGroup" stepKey="seeErrorMessageAfterSecondAttempt" /> + <actionGroup ref="AssertCaptchaNotVisibleOnCustomerLoginFormActionGroup" stepKey="dontSeeCaptchaAfterSecondAttempt" /> + + <actionGroup ref="StorefrontFillCustomerLoginFormActionGroup" stepKey="fillLoginFormThirdAttempt"> + <argument name="customer" value="Colorado_US_Customer" /> + </actionGroup> + <actionGroup ref="StorefrontClickSignOnCustomerLoginFormActionGroup" stepKey="clickSignInAccountButtonThirdAttempt" /> + <actionGroup ref="AssertMessageCustomerLoginActionGroup" stepKey="seeErrorMessageAfterThirdAttempt" /> + <actionGroup ref="AssertCaptchaVisibleOnCustomerLoginFormActionGroup" stepKey="seeCaptchaAfterThirdAttempt" /> + + <!-- Submit form with incorrect captcha --> + <actionGroup ref="StorefrontFillCustomerLoginFormWithCaptchaActionGroup" stepKey="fillLoginFormCorrectAccountIncorrectCaptcha"> + <argument name="customer" value="$$customer$$" /> + <argument name="captcha" value="{{WrongCaptcha.value}}" /> + </actionGroup> + <actionGroup ref="StorefrontClickSignOnCustomerLoginFormActionGroup" stepKey="clickSignInAccountButtonCorrectAccountIncorrectCaptcha" /> + <actionGroup ref="AssertMessageCustomerLoginActionGroup" stepKey="seeErrorMessageAfterIncorrectCaptcha"> + <argument name="message" value="Incorrect CAPTCHA" /> + </actionGroup> + + <actionGroup ref="StorefrontFillCustomerLoginFormWithCaptchaActionGroup" stepKey="fillLoginFormCorrectAccountCorrectCaptcha"> + <argument name="customer" value="$$customer$$" /> + <argument name="captcha" value="{{PreconfiguredCaptcha.value}}" /> + </actionGroup> + <actionGroup ref="StorefrontClickSignOnCustomerLoginFormActionGroup" stepKey="clickSignInAccountButtonCorrectAccountCorrectCaptcha" /> + <actionGroup ref="AssertCustomerWelcomeMessageActionGroup" stepKey="assertCustomerLoggedIn"> + <argument name="customerFullName" value="$$customer.firstname$$ $$customer.lastname$$" /> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaRegisterNewCustomerTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaRegisterNewCustomerTest.xml new file mode 100644 index 000000000000..2c331f958e46 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaRegisterNewCustomerTest.xml @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCaptchaRegisterNewCustomerTest"> + <annotations> + <features value="Captcha"/> + <stories value="Create New Customer Account + Captcha"/> + <title value="Test creation for customer register with captcha on storefront."/> + <description value="Test creation for customer register with captcha on storefront."/> + <severity value="MAJOR"/> + <testCaseId value="MC-14805" /> + <group value="captcha"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Enable captcha for customer. --> + <magentoCLI command="config:set {{StorefrontCaptchaOnCustomerCreateFormConfigData.path}} {{StorefrontCaptchaOnCustomerCreateFormConfigData.value}}" stepKey="enableUserRegistrationCaptcha" /> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaModeAlwaysConfigData.path}} {{StorefrontCustomerCaptchaModeAlwaysConfigData.value}}" stepKey="alwaysEnableCaptcha" /> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaLength3ConfigData.path}} {{StorefrontCustomerCaptchaLength3ConfigData.value}}" stepKey="setCaptchaLength" /> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaSymbols1ConfigData.path}} {{StorefrontCustomerCaptchaSymbols1ConfigData.value}}" stepKey="setCaptchaSymbols" /> + <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + </before> + <after> + <!-- Set default configuration. --> + <magentoCLI command="config:set {{StorefrontCaptchaOnCustomerLoginConfigData.path}} {{StorefrontCaptchaOnCustomerLoginConfigData.value}},{{StorefrontCaptchaOnCustomerForgotPasswordConfigData.value}}" stepKey="enableCaptchaOnDefaultForms" /> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaModeAfterFailConfigData.path}} {{StorefrontCustomerCaptchaModeAfterFailConfigData.value}}" stepKey="defaultCaptchaMode" /> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultLengthConfigData.path}} {{StorefrontCustomerCaptchaDefaultLengthConfigData.value}}" stepKey="setDefaultCaptchaLength" /> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.path}} {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.value}}" stepKey="setDefaultCaptchaSymbols" /> + <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + </after> + + <!-- Open Customer registration page --> + <actionGroup ref="StorefrontOpenCustomerAccountCreatePageActionGroup" stepKey="goToCustomerAccountCreatePage" /> + + <!-- Check captcha visibility registration page load --> + <actionGroup ref="AssertCaptchaVisibleOnCustomerAccountCreatePageActionGroup" stepKey="verifyCaptchaVisible" /> + + <!-- Submit form with incorrect captcha --> + <actionGroup ref="StorefrontFillCustomerAccountCreationFormWithCaptchaActionGroup" stepKey="fillNewCustomerAccountFormWithIncorrectCaptcha"> + <argument name="customer" value="Simple_US_Customer" /> + <argument name="captcha" value="{{WrongCaptcha.value}}" /> + </actionGroup> + + <actionGroup ref="StorefrontClickCreateAnAccountCustomerAccountCreationFormActionGroup" stepKey="clickCreateAnAccountButton" /> + + <actionGroup ref="AssertMessageCustomerCreateAccountActionGroup" stepKey="assertMessage"> + <argument name="message" value="Incorrect CAPTCHA" /> + <argument name="messageType" value="error" /> + </actionGroup> + + <actionGroup ref="AssertCaptchaVisibleOnCustomerAccountCreatePageActionGroup" stepKey="verifyCaptchaVisibleAfterFail" /> + + <!-- Submit form with correct captcha --> + <actionGroup ref="StorefrontFillCustomerAccountCreationFormWithCaptchaActionGroup" stepKey="fillNewCustomerAccountFormWithCorrectCaptcha"> + <argument name="customer" value="Simple_US_Customer" /> + <argument name="captcha" value="{{PreconfiguredCaptcha.value}}" /> + </actionGroup> + <actionGroup ref="StorefrontClickCreateAnAccountCustomerAccountCreationFormActionGroup" stepKey="clickCreateAnAccountButtonAfterCorrectCaptcha" /> + <actionGroup ref="AssertMessageCustomerCreateAccountActionGroup" stepKey="assertSuccessMessage" /> + </test> +</tests> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontResetCustomerPasswordFailedTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontResetCustomerPasswordFailedTest.xml new file mode 100644 index 000000000000..36d7989b9acc --- /dev/null +++ b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontResetCustomerPasswordFailedTest.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontResetCustomerPasswordFailedTest"> + <before> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaDisableConfigData.path}} {{StorefrontCustomerCaptchaDisableConfigData.value}}" stepKey="disableCaptcha"/> + </before> + <after> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaEnableConfigData.path}} {{StorefrontCustomerCaptchaEnableConfigData.value}}" stepKey="enableCaptcha"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Captcha/Test/Unit/Model/Checkout/ConfigProviderTest.php b/app/code/Magento/Captcha/Test/Unit/Model/Checkout/ConfigProviderTest.php index 655fcd6118e2..8764dbd4cec1 100644 --- a/app/code/Magento/Captcha/Test/Unit/Model/Checkout/ConfigProviderTest.php +++ b/app/code/Magento/Captcha/Test/Unit/Model/Checkout/ConfigProviderTest.php @@ -77,6 +77,7 @@ public function testGetConfig($isRequired, $captchaGenerations, $expectedConfig) ->will($this->returnValue('https://magento.com/captcha')); $config = $this->model->getConfig(); + unset($config['captcha'][$this->formId]['timestamp']); $this->assertEquals($config, $expectedConfig); } diff --git a/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php b/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php index 1edbcc029e4c..eef75d2c01ec 100644 --- a/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php +++ b/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php @@ -183,7 +183,13 @@ public function testIsCorrect() { self::$_defaultConfig['case_sensitive'] = '1'; $this->assertFalse($this->_object->isCorrect('abcdef5')); - $sessionData = ['user_create_word' => ['data' => 'AbCdEf5', 'expires' => time() + self::EXPIRE_FRAME]]; + $sessionData = [ + 'user_create_word' => [ + 'data' => 'AbCdEf5', + 'words' => 'AbCdEf5', + 'expires' => time() + self::EXPIRE_FRAME + ] + ]; $this->_object->getSession()->setData($sessionData); self::$_defaultConfig['case_sensitive'] = '0'; $this->assertTrue($this->_object->isCorrect('abcdef5')); @@ -224,7 +230,7 @@ public function testGetWord() { $this->assertEquals($this->_object->getWord(), 'AbCdEf5'); $this->_object->getSession()->setData( - ['user_create_word' => ['data' => 'AbCdEf5', 'expires' => time() - 360]] + ['user_create_word' => ['data' => 'AbCdEf5', 'words' => 'AbCdEf5','expires' => time() - 360]] ); $this->assertNull($this->_object->getWord()); } @@ -247,7 +253,13 @@ protected function _getSessionStub() ->getMock(); $session->expects($this->any())->method('isLoggedIn')->will($this->returnValue(false)); - $session->setData(['user_create_word' => ['data' => 'AbCdEf5', 'expires' => time() + self::EXPIRE_FRAME]]); + $session->setData([ + 'user_create_word' => [ + 'data' => 'AbCdEf5', + 'words' => 'AbCdEf5', + 'expires' => time() + self::EXPIRE_FRAME + ] + ]); return $session; } diff --git a/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserLoginBackendObserverTest.php b/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserLoginBackendObserverTest.php new file mode 100644 index 000000000000..415f022a7364 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserLoginBackendObserverTest.php @@ -0,0 +1,137 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Captcha\Test\Unit\Observer; + +use Magento\Captcha\Helper\Data; +use Magento\Captcha\Model\DefaultModel; +use Magento\Captcha\Observer\CaptchaStringResolver; +use Magento\Captcha\Observer\CheckUserLoginBackendObserver; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\Event; +use Magento\Framework\Event\Observer; +use Magento\Framework\Message\ManagerInterface; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + +/** + * Class CheckUserLoginBackendObserverTest + */ +class CheckUserLoginBackendObserverTest extends TestCase +{ + /** + * @var CheckUserLoginBackendObserver + */ + private $observer; + + /** + * @var ManagerInterface|MockObject + */ + private $messageManagerMock; + + /** + * @var CaptchaStringResolver|MockObject + */ + private $captchaStringResolverMock; + + /** + * @var RequestInterface|MockObject + */ + private $requestMock; + + /** + * @var Data|MockObject + */ + private $helperMock; + + /** + * Set Up + * + * @return void + */ + protected function setUp() + { + $this->helperMock = $this->createMock(Data::class); + $this->messageManagerMock = $this->createMock(ManagerInterface::class); + $this->captchaStringResolverMock = $this->createMock(CaptchaStringResolver::class); + $this->requestMock = $this->createMock(RequestInterface::class); + + $this->observer = new CheckUserLoginBackendObserver( + $this->helperMock, + $this->captchaStringResolverMock, + $this->requestMock + ); + } + + /** + * Test check user login in backend with correct captcha + * + * @dataProvider requiredCaptchaDataProvider + * @param bool $isRequired + * @return void + */ + public function testCheckOnBackendLoginWithCorrectCaptcha(bool $isRequired): void + { + $formId = 'backend_login'; + $login = 'admin'; + $captchaValue = 'captcha-value'; + + /** @var Observer|MockObject $observerMock */ + $observerMock = $this->createPartialMock(Observer::class, ['getEvent']); + $eventMock = $this->createPartialMock(Event::class, ['getUsername']); + $captcha = $this->createMock(DefaultModel::class); + + $eventMock->method('getUsername')->willReturn('admin'); + $observerMock->method('getEvent')->willReturn($eventMock); + $captcha->method('isRequired')->with($login)->willReturn($isRequired); + $captcha->method('isCorrect')->with($captchaValue)->willReturn(true); + $this->helperMock->method('getCaptcha')->with($formId)->willReturn($captcha); + $this->captchaStringResolverMock->method('resolve')->with($this->requestMock, $formId) + ->willReturn($captchaValue); + + $this->observer->execute($observerMock); + } + + /** + * @return array + */ + public function requiredCaptchaDataProvider(): array + { + return [ + [true], + [false] + ]; + } + + /** + * Test check user login in backend with wrong captcha + * + * @return void + * @expectedException \Magento\Framework\Exception\Plugin\AuthenticationException + */ + public function testCheckOnBackendLoginWithWrongCaptcha(): void + { + $formId = 'backend_login'; + $login = 'admin'; + $captchaValue = 'captcha-value'; + + /** @var Observer|MockObject $observerMock */ + $observerMock = $this->createPartialMock(Observer::class, ['getEvent']); + $eventMock = $this->createPartialMock(Event::class, ['getUsername']); + $captcha = $this->createMock(DefaultModel::class); + + $eventMock->method('getUsername')->willReturn($login); + $observerMock->method('getEvent')->willReturn($eventMock); + $captcha->method('isRequired')->with($login)->willReturn(true); + $captcha->method('isCorrect')->with($captchaValue)->willReturn(false); + $this->helperMock->method('getCaptcha')->with($formId)->willReturn($captcha); + $this->captchaStringResolverMock->method('resolve')->with($this->requestMock, $formId) + ->willReturn($captchaValue); + + $this->observer->execute($observerMock); + } +} diff --git a/app/code/Magento/Captcha/etc/db_schema.xml b/app/code/Magento/Captcha/etc/db_schema.xml index fa9a14abb896..158e2f43b9f5 100644 --- a/app/code/Magento/Captcha/etc/db_schema.xml +++ b/app/code/Magento/Captcha/etc/db_schema.xml @@ -9,11 +9,11 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="captcha_log" resource="default" engine="innodb" comment="Count Login Attempts"> <column xsi:type="varchar" name="type" nullable="false" length="32" comment="Type"/> - <column xsi:type="varchar" name="value" nullable="false" length="32" comment="Value"/> + <column xsi:type="varchar" name="value" nullable="false" length="255" comment="Value"/> <column xsi:type="int" name="count" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Count"/> <column xsi:type="timestamp" name="updated_at" on_update="false" nullable="true" comment="Update Time"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="type"/> <column name="value"/> </constraint> diff --git a/app/code/Magento/Captcha/etc/di.xml b/app/code/Magento/Captcha/etc/di.xml index 3a929f5e6cc0..83c4e8aa1e2c 100644 --- a/app/code/Magento/Captcha/etc/di.xml +++ b/app/code/Magento/Captcha/etc/di.xml @@ -27,7 +27,7 @@ </arguments> </type> <type name="Magento\Customer\Controller\Ajax\Login"> - <plugin name="configurable_product" type="Magento\Captcha\Model\Customer\Plugin\AjaxLogin" sortOrder="50" /> + <plugin name="captcha_validation" type="Magento\Captcha\Model\Customer\Plugin\AjaxLogin" sortOrder="50" /> </type> <type name="Magento\Captcha\Model\Customer\Plugin\AjaxLogin"> <arguments> diff --git a/app/code/Magento/Captcha/etc/frontend/di.xml b/app/code/Magento/Captcha/etc/frontend/di.xml index 0c4ab0cda073..490f1eab8519 100644 --- a/app/code/Magento/Captcha/etc/frontend/di.xml +++ b/app/code/Magento/Captcha/etc/frontend/di.xml @@ -20,4 +20,18 @@ </argument> </arguments> </type> + <type name="Magento\Captcha\CustomerData\Captcha"> + <arguments> + <argument name="formIds" xsi:type="array"> + <item name="user_login" xsi:type="string">user_login</item> + </argument> + </arguments> + </type> + <type name="Magento\Customer\CustomerData\SectionPoolInterface"> + <arguments> + <argument name="sectionSourceMap" xsi:type="array"> + <item name="captcha" xsi:type="string">Magento\Captcha\CustomerData\Captcha</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Captcha/etc/frontend/sections.xml b/app/code/Magento/Captcha/etc/frontend/sections.xml new file mode 100644 index 000000000000..7f2070e10c8a --- /dev/null +++ b/app/code/Magento/Captcha/etc/frontend/sections.xml @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Customer:etc/sections.xsd"> + <action name="customer/ajax/login"> + <section name="captcha"/> + </action> +</config> diff --git a/app/code/Magento/Captcha/view/frontend/requirejs-config.js b/app/code/Magento/Captcha/view/frontend/requirejs-config.js index 3b322711f8b1..42c80632d3e9 100644 --- a/app/code/Magento/Captcha/view/frontend/requirejs-config.js +++ b/app/code/Magento/Captcha/view/frontend/requirejs-config.js @@ -6,7 +6,8 @@ var config = { map: { '*': { - captcha: 'Magento_Captcha/captcha' + captcha: 'Magento_Captcha/js/captcha', + 'Magento_Captcha/captcha': 'Magento_Captcha/js/captcha' } } }; diff --git a/app/code/Magento/Captcha/view/frontend/web/captcha.js b/app/code/Magento/Captcha/view/frontend/web/js/captcha.js similarity index 100% rename from app/code/Magento/Captcha/view/frontend/web/captcha.js rename to app/code/Magento/Captcha/view/frontend/web/js/captcha.js diff --git a/app/code/Magento/Captcha/view/frontend/web/js/model/captcha.js b/app/code/Magento/Captcha/view/frontend/web/js/model/captcha.js index 3a235df73a91..e79cfb35ee08 100644 --- a/app/code/Magento/Captcha/view/frontend/web/js/model/captcha.js +++ b/app/code/Magento/Captcha/view/frontend/web/js/model/captcha.js @@ -17,11 +17,12 @@ define([ imageSource: ko.observable(captchaData.imageSrc), visibility: ko.observable(false), captchaValue: ko.observable(null), - isRequired: captchaData.isRequired, + isRequired: ko.observable(captchaData.isRequired), isCaseSensitive: captchaData.isCaseSensitive, imageHeight: captchaData.imageHeight, refreshUrl: captchaData.refreshUrl, isLoading: ko.observable(false), + timestamp: null, /** * @return {String} @@ -41,7 +42,7 @@ define([ * @return {Boolean} */ getIsVisible: function () { - return this.visibility; + return this.visibility(); }, /** @@ -55,14 +56,14 @@ define([ * @return {Boolean} */ getIsRequired: function () { - return this.isRequired; + return this.isRequired(); }, /** * @param {Boolean} flag */ setIsRequired: function (flag) { - this.isRequired = flag; + this.isRequired(flag); }, /** diff --git a/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/defaultCaptcha.js b/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/defaultCaptcha.js index f80b2ab163ff..d79c42a71156 100644 --- a/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/defaultCaptcha.js +++ b/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/defaultCaptcha.js @@ -7,8 +7,10 @@ define([ 'jquery', 'uiComponent', 'Magento_Captcha/js/model/captcha', - 'Magento_Captcha/js/model/captchaList' -], function ($, Component, Captcha, captchaList) { + 'Magento_Captcha/js/model/captchaList', + 'Magento_Customer/js/customer-data', + 'underscore' +], function ($, Component, Captcha, captchaList, customerData, _) { 'use strict'; var captchaConfig; @@ -34,12 +36,49 @@ define([ if (window[this.configSource] && window[this.configSource].captcha) { captchaConfig = window[this.configSource].captcha; $.each(captchaConfig, function (formId, captchaData) { + var captcha; + captchaData.formId = formId; - captchaList.add(Captcha(captchaData)); - }); + captcha = Captcha(captchaData); + this.checkCustomerData(formId, customerData.get('captcha')(), captcha); + this.subscribeCustomerData(formId, captcha); + captchaList.add(captcha); + }.bind(this)); } }, + /** + * Check customer data for captcha configuration. + * + * @param {String} formId + * @param {Object} captchaData + * @param {Object} captcha + */ + checkCustomerData: function (formId, captchaData, captcha) { + if (!_.isEmpty(captchaData) && + !_.isEmpty(captchaData)[formId] && + captchaData[formId].timestamp > captcha.timestamp + ) { + if (!captcha.isRequired() && captchaData[formId].isRequired) { + captcha.refresh(); + } + captcha.isRequired(captchaData[formId].isRequired); + captcha.timestamp = captchaData[formId].timestamp; + } + }, + + /** + * Subscribe for customer data updates. + * + * @param {String} formId + * @param {Object} captcha + */ + subscribeCustomerData: function (formId, captcha) { + customerData.get('captcha').subscribe(function (captchaData) { + this.checkCustomerData(formId, captchaData, captcha); + }.bind(this)); + }, + /** * @return {Boolean} */ @@ -89,6 +128,15 @@ define([ return this.currentCaptcha !== null ? this.currentCaptcha.getIsRequired() : false; }, + /** + * Set isRequired on current captcha model. + * + * @param {Boolean} flag + */ + setIsRequired: function (flag) { + this.currentCaptcha.setIsRequired(flag); + }, + /** * @return {Boolean} */ diff --git a/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/loginCaptcha.js b/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/loginCaptcha.js index 7709febea60a..49528f6ce850 100644 --- a/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/loginCaptcha.js +++ b/app/code/Magento/Captcha/view/frontend/web/js/view/checkout/loginCaptcha.js @@ -6,9 +6,10 @@ define([ 'Magento_Captcha/js/view/checkout/defaultCaptcha', 'Magento_Captcha/js/model/captchaList', - 'Magento_Customer/js/action/login' + 'Magento_Customer/js/action/login', + 'underscore' ], -function (defaultCaptcha, captchaList, loginAction) { +function (defaultCaptcha, captchaList, loginAction, _) { 'use strict'; return defaultCaptcha.extend({ @@ -26,9 +27,10 @@ function (defaultCaptcha, captchaList, loginAction) { loginAction.registerLoginCallback(function (loginData) { if (loginData['captcha_form_id'] && - loginData['captcha_form_id'] == self.formId //eslint-disable-line eqeqeq + loginData['captcha_form_id'] === self.formId && + self.isRequired() ) { - self.refresh(); + _.defer(self.refresh.bind(self)); } }); } diff --git a/app/code/Magento/Captcha/view/frontend/web/template/checkout/captcha.html b/app/code/Magento/Captcha/view/frontend/web/template/checkout/captcha.html index 575b3ca6f732..3f48ec330c0a 100644 --- a/app/code/Magento/Captcha/view/frontend/web/template/checkout/captcha.html +++ b/app/code/Magento/Captcha/view/frontend/web/template/checkout/captcha.html @@ -4,12 +4,12 @@ * See COPYING.txt for license details. */ --> +<input name="captcha_form_id" type="hidden" data-bind="value: formId, attr: {'data-scope': dataScope}" /> <!-- ko if: (isRequired() && getIsVisible())--> <div class="field captcha required" data-bind="blockLoader: getIsLoading()"> <label data-bind="attr: {for: 'captcha_' + formId}" class="label"><span data-bind="i18n: 'Please type the letters and numbers below'"></span></label> <div class="control captcha"> <input name="captcha_string" type="text" class="input-text required-entry" data-bind="value: captchaValue(), attr: {id: 'captcha_' + formId, 'data-scope': dataScope}" autocomplete="off"/> - <input name="captcha_form_id" type="hidden" data-bind="value: formId, attr: {'data-scope': dataScope}" /> <div class="nested"> <div class="field captcha no-label"> <div class="control captcha-image"> diff --git a/app/code/Magento/Catalog/Api/CategoryLinkRepositoryInterface.php b/app/code/Magento/Catalog/Api/CategoryLinkRepositoryInterface.php index 30ac06107ba1..a65355c69092 100644 --- a/app/code/Magento/Catalog/Api/CategoryLinkRepositoryInterface.php +++ b/app/code/Magento/Catalog/Api/CategoryLinkRepositoryInterface.php @@ -32,18 +32,20 @@ public function save(\Magento\Catalog\Api\Data\CategoryProductLinkInterface $pro * * @throws \Magento\Framework\Exception\CouldNotSaveException * @throws \Magento\Framework\Exception\StateException + * @throws \Magento\Framework\Exception\InputException */ public function delete(\Magento\Catalog\Api\Data\CategoryProductLinkInterface $productLink); /** * Remove the product assignment from the category by category id and sku * - * @param string $sku + * @param int $categoryId * @param string $sku * @return bool will returned True if products successfully deleted * * @throws \Magento\Framework\Exception\CouldNotSaveException * @throws \Magento\Framework\Exception\StateException + * @throws \Magento\Framework\Exception\InputException */ public function deleteByIds($categoryId, $sku); } diff --git a/app/code/Magento/Catalog/Api/Data/CategoryInterface.php b/app/code/Magento/Catalog/Api/Data/CategoryInterface.php index b9a23e9d08ec..1940a0ac80c0 100644 --- a/app/code/Magento/Catalog/Api/Data/CategoryInterface.php +++ b/app/code/Magento/Catalog/Api/Data/CategoryInterface.php @@ -1,7 +1,5 @@ <?php /** - * Category data interface - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -9,6 +7,8 @@ namespace Magento\Catalog\Api\Data; /** + * Category data interface. + * * @api * @since 100.0.2 */ @@ -46,11 +46,15 @@ interface CategoryInterface extends \Magento\Framework\Api\CustomAttributesDataI /**#@-*/ /** + * Retrieve category id. + * * @return int|null */ public function getId(); /** + * Set category id. + * * @param int $id * @return $this */ @@ -74,7 +78,7 @@ public function setParentId($parentId); /** * Get category name * - * @return string + * @return string|null */ public function getName(); @@ -132,60 +136,82 @@ public function getLevel(); public function setLevel($level); /** + * Retrieve children ids comma separated. + * * @return string|null */ public function getChildren(); /** + * Retrieve category creation date and time. + * * @return string|null */ public function getCreatedAt(); /** + * Set category creation date and time. + * * @param string $createdAt * @return $this */ public function setCreatedAt($createdAt); /** + * Retrieve category last update date and time. + * * @return string|null */ public function getUpdatedAt(); /** + * Set category last update date and time. + * * @param string $updatedAt * @return $this */ public function setUpdatedAt($updatedAt); /** + * Retrieve category full path. + * * @return string|null */ public function getPath(); /** + * Set category full path. + * * @param string $path * @return $this */ public function setPath($path); /** + * Retrieve available sort by for category. + * * @return string[]|null */ public function getAvailableSortBy(); /** + * Set available sort by for category. + * * @param string[]|string $availableSortBy * @return $this */ public function setAvailableSortBy($availableSortBy); /** + * Get category is included in menu. + * * @return bool|null */ public function getIncludeInMenu(); /** + * Set category is included in menu. + * * @param bool $includeInMenu * @return $this */ diff --git a/app/code/Magento/Catalog/Api/Data/ProductRender/ImageInterface.php b/app/code/Magento/Catalog/Api/Data/ProductRender/ImageInterface.php index 4cdb2631edea..45b070d2706d 100644 --- a/app/code/Magento/Catalog/Api/Data/ProductRender/ImageInterface.php +++ b/app/code/Magento/Catalog/Api/Data/ProductRender/ImageInterface.php @@ -92,6 +92,7 @@ public function setWidth($width); /** * Retrieve image label + * * Image label is short description of this image * * @return string @@ -111,7 +112,7 @@ public function setLabel($label); /** * Retrieve resize width * - * This width is image dimension, which represents the width, that can be used for perfomance improvements + * This width is image dimension, which represents the width, that can be used for performance improvements * * @return float * @since 101.1.0 @@ -128,6 +129,8 @@ public function getResizedWidth(); public function setResizedWidth($width); /** + * Set resized height + * * @param string $height * @return void * @since 101.1.0 diff --git a/app/code/Magento/Catalog/Api/Data/ProductRender/PriceInfoInterface.php b/app/code/Magento/Catalog/Api/Data/ProductRender/PriceInfoInterface.php index 2ef4d068317d..9768b3c08c8a 100644 --- a/app/code/Magento/Catalog/Api/Data/ProductRender/PriceInfoInterface.php +++ b/app/code/Magento/Catalog/Api/Data/ProductRender/PriceInfoInterface.php @@ -8,6 +8,7 @@ /** * Price interface. + * * @api * @since 101.1.0 */ @@ -23,6 +24,7 @@ public function getFinalPrice(); /** * Set the final price: usually it calculated as minimal price of the product + * * Can be different depends on type of product * * @param float $finalPrice @@ -33,6 +35,7 @@ public function setFinalPrice($finalPrice); /** * Retrieve max price of a product + * * E.g. for product with custom options is price with the most expensive custom option * * @return float @@ -51,6 +54,7 @@ public function setMaxPrice($maxPrice); /** * Set max regular price + * * Max regular price is the same, as maximum price, except of excluding calculating special price and catalog rules * in it * @@ -105,6 +109,8 @@ public function setSpecialPrice($specialPrice); public function getSpecialPrice(); /** + * Retrieve minimal price + * * @return float * @since 101.1.0 */ @@ -129,6 +135,7 @@ public function getRegularPrice(); /** * Regular price - is price of product without discounts and special price with taxes and fixed product tax + * * Usually this price is corresponding to price in admin panel of product * * @param float $regularPrice @@ -148,7 +155,7 @@ public function getFormattedPrices(); /** * Set dto with formatted prices * - * @param string[] $formattedPriceInfo + * @param FormattedPriceInfoInterface $formattedPriceInfo * @return void * @since 101.1.0 */ diff --git a/app/code/Magento/Catalog/Api/Data/ProductRenderInterface.php b/app/code/Magento/Catalog/Api/Data/ProductRenderInterface.php index 910168d8854e..166a1aba76b6 100644 --- a/app/code/Magento/Catalog/Api/Data/ProductRenderInterface.php +++ b/app/code/Magento/Catalog/Api/Data/ProductRenderInterface.php @@ -30,7 +30,7 @@ public function getAddToCartButton(); /** * Set information needed for render "Add To Cart" button on front * - * @param \Magento\Catalog\Api\Data\ProductRender\ButtonInterface $addToCartData + * @param ButtonInterface $cartAddToCartButton * @return void * @since 101.1.0 */ @@ -47,7 +47,7 @@ public function getAddToCompareButton(); /** * Set information needed for render "Add To Compare" button on front * - * @param ButtonInterface $compareUrlData + * @param ButtonInterface $compareButton * @return string * @since 101.1.0 */ @@ -55,6 +55,7 @@ public function setAddToCompareButton(ButtonInterface $compareButton); /** * Provide information needed for render prices and adjustments for different product types on front + * * Prices are represented in raw format and in current currency * * @return \Magento\Catalog\Api\Data\ProductRender\PriceInfoInterface @@ -73,6 +74,7 @@ public function setPriceInfo(PriceInfoInterface $priceInfo); /** * Provide enough information, that needed to render image on front + * * Images can be separated by image codes * * @return \Magento\Catalog\Api\Data\ProductRender\ImageInterface[] @@ -167,6 +169,7 @@ public function getIsSalable(); /** * Set information about product saleability (Stock, other conditions) + * * Is used to provide information to frontend JS renders * You can add plugin, in order to hide product on product page or product list on front * @@ -178,6 +181,7 @@ public function setIsSalable($isSalable); /** * Provide information about current store id or requested store id + * * Product should be assigned to provided store id * This setting affect store scope attributes * @@ -197,6 +201,7 @@ public function setStoreId($storeId); /** * Provide current or desired currency code to product + * * This setting affect formatted prices* * * @return string diff --git a/app/code/Magento/Catalog/Api/ProductRenderListInterface.php b/app/code/Magento/Catalog/Api/ProductRenderListInterface.php index f79efa4c814d..954acd35a07d 100644 --- a/app/code/Magento/Catalog/Api/ProductRenderListInterface.php +++ b/app/code/Magento/Catalog/Api/ProductRenderListInterface.php @@ -4,18 +4,22 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Catalog\Api; /** - * Interface which provides product renders information for products + * Interface which provides product renders information for products. + * * @api * @since 101.1.0 */ interface ProductRenderListInterface { /** - * Collect and retrieve the list of product render info - * This info contains raw prices and formated prices, product name, stock status, store_id, etc + * Collect and retrieve the list of product render info. + * + * This info contains raw prices and formatted prices, product name, stock status, store_id, etc. + * * @see \Magento\Catalog\Api\Data\ProductRenderInfoDtoInterface * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/AbstractCategory.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/AbstractCategory.php index 331679874629..ffb648cdf438 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Category/AbstractCategory.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/AbstractCategory.php @@ -67,6 +67,8 @@ public function getCategory() } /** + * Get category id + * * @return int|string|null */ public function getCategoryId() @@ -78,6 +80,8 @@ public function getCategoryId() } /** + * Get category name + * * @return string */ public function getCategoryName() @@ -86,6 +90,8 @@ public function getCategoryName() } /** + * Get category path + * * @return mixed */ public function getCategoryPath() @@ -97,6 +103,8 @@ public function getCategoryPath() } /** + * Check store root category + * * @return bool */ public function hasStoreRootCategory() @@ -109,6 +117,8 @@ public function hasStoreRootCategory() } /** + * Get store from request + * * @return Store */ public function getStore() @@ -118,6 +128,8 @@ public function getStore() } /** + * Get root category for tree + * * @param mixed|null $parentNodeCategory * @param int $recursionLevel * @return Node|array|null @@ -149,10 +161,11 @@ public function getRoot($parentNodeCategory = null, $recursionLevel = 3) $root = $tree->getNodeById($rootId); - if ($root && $rootId != \Magento\Catalog\Model\Category::TREE_ROOT_ID) { + if ($root) { $root->setIsVisible(true); - } elseif ($root && $root->getId() == \Magento\Catalog\Model\Category::TREE_ROOT_ID) { - $root->setName(__('Root')); + if ($root->getId() == \Magento\Catalog\Model\Category::TREE_ROOT_ID) { + $root->setName(__('Root')); + } } $this->_coreRegistry->register('root', $root); @@ -162,6 +175,8 @@ public function getRoot($parentNodeCategory = null, $recursionLevel = 3) } /** + * Get Default Store Id + * * @return int */ protected function _getDefaultStoreId() @@ -170,6 +185,8 @@ protected function _getDefaultStoreId() } /** + * Get category collection + * * @return \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection */ public function getCategoryCollection() @@ -227,6 +244,8 @@ public function getRootByIds($ids) } /** + * Get category node for tree + * * @param mixed $parentNodeCategory * @param int $recursionLevel * @return Node @@ -249,6 +268,8 @@ public function getNode($parentNodeCategory, $recursionLevel = 2) } /** + * Get category save url + * * @param array $args * @return string */ @@ -260,6 +281,8 @@ public function getSaveUrl(array $args = []) } /** + * Get category edit url + * * @return string */ public function getEditUrl() diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Edit/DeleteButton.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Edit/DeleteButton.php index 20411a4c4d76..2eef1188e391 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Edit/DeleteButton.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Edit/DeleteButton.php @@ -27,7 +27,8 @@ public function getButtonData() return [ 'id' => 'delete', 'label' => __('Delete'), - 'on_click' => "categoryDelete('" . $this->getDeleteUrl() . "')", + 'on_click' => "deleteConfirm('" .__('Are you sure you want to delete this category?') ."', '" + . $this->getDeleteUrl() . "', {data: {}})", 'class' => 'delete', 'sort_order' => 10 ]; diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Pricestep.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Pricestep.php index b77a5e2e9524..3266922d116e 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Pricestep.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Pricestep.php @@ -11,6 +11,9 @@ */ namespace Magento\Catalog\Block\Adminhtml\Category\Helper; +/** + * Pricestep Helper + */ class Pricestep extends \Magento\Framework\Data\Form\Element\Text { /** @@ -40,7 +43,7 @@ public function getElementHtml() $disabled = true; } - parent::addClass('validate-number validate-number-range number-range-0.01-1000000000'); + parent::addClass('validate-number validate-number-range number-range-0.01-9999999999999999'); $html = parent::getElementHtml(); $htmlId = 'use_config_' . $this->getHtmlId(); $html .= '<br/><input id="' . $htmlId . '" name="use_config[]" value="' . $this->getId() . '"'; diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php index a67f55235b6d..83ec50159248 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php @@ -71,7 +71,7 @@ public function __construct( } /** - * @return void + * @inheritdoc */ protected function _construct() { @@ -80,7 +80,7 @@ protected function _construct() } /** - * @return $this + * @inheritdoc */ protected function _prepareLayout() { @@ -182,6 +182,8 @@ public function getSuggestedCategoriesJson($namePart) } /** + * Get add root button html + * * @return string */ public function getAddRootButtonHtml() @@ -190,6 +192,8 @@ public function getAddRootButtonHtml() } /** + * Get add sub button html + * * @return string */ public function getAddSubButtonHtml() @@ -198,6 +202,8 @@ public function getAddSubButtonHtml() } /** + * Get expand button html + * * @return string */ public function getExpandButtonHtml() @@ -206,6 +212,8 @@ public function getExpandButtonHtml() } /** + * Get collapse button html + * * @return string */ public function getCollapseButtonHtml() @@ -214,6 +222,8 @@ public function getCollapseButtonHtml() } /** + * Get store switcher + * * @return string */ public function getStoreSwitcherHtml() @@ -222,6 +232,8 @@ public function getStoreSwitcherHtml() } /** + * Get loader tree url + * * @param bool|null $expanded * @return string */ @@ -235,6 +247,8 @@ public function getLoadTreeUrl($expanded = null) } /** + * Get nodes url + * * @return string */ public function getNodesUrl() @@ -243,6 +257,8 @@ public function getNodesUrl() } /** + * Get switcher tree url + * * @return string */ public function getSwitchTreeUrl() @@ -254,6 +270,8 @@ public function getSwitchTreeUrl() } /** + * Get is was expanded + * * @return bool * @SuppressWarnings(PHPMD.BooleanGetMethodName) */ @@ -263,6 +281,8 @@ public function getIsWasExpanded() } /** + * Get move url + * * @return string */ public function getMoveUrl() @@ -271,6 +291,8 @@ public function getMoveUrl() } /** + * Get tree + * * @param mixed|null $parenNodeCategory * @return array */ @@ -282,6 +304,8 @@ public function getTree($parenNodeCategory = null) } /** + * Get tree json + * * @param mixed|null $parenNodeCategory * @return string */ @@ -367,7 +391,7 @@ protected function _getNodeJson($node, $level = 0) } } - if ($isParent || $node->getLevel() < 2) { + if ($isParent || $node->getLevel() < 1) { $item['expanded'] = true; } @@ -390,6 +414,8 @@ public function buildNodeName($node) } /** + * Is category movable + * * @param Node|array $node * @return bool */ @@ -403,6 +429,8 @@ protected function _isCategoryMoveable($node) } /** + * Is parent selected category + * * @param Node|array $node * @return bool */ diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Fieldset/Element.php b/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Fieldset/Element.php index ad6df27b8933..8f1d1dcf7eed 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Fieldset/Element.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Fieldset/Element.php @@ -4,13 +4,13 @@ * See COPYING.txt for license details. */ +namespace Magento\Catalog\Block\Adminhtml\Form\Renderer\Fieldset; + /** * Catalog fieldset element renderer * * @author Magento Core Team <core@magentocommerce.com> */ -namespace Magento\Catalog\Block\Adminhtml\Form\Renderer\Fieldset; - class Element extends \Magento\Backend\Block\Widget\Form\Renderer\Fieldset\Element { /** @@ -29,7 +29,7 @@ public function getDataObject() } /** - * Retireve associated with element attribute object + * Retrieve associated with element attribute object * * @return \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Advanced.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Advanced.php index dd09e40ac5b3..1b6756968662 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Advanced.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Advanced.php @@ -4,11 +4,6 @@ * See COPYING.txt for license details. */ -/** - * Product attribute add/edit form main tab - * - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Catalog\Block\Adminhtml\Product\Attribute\Edit\Tab; use Magento\Backend\Block\Widget\Form\Generic; @@ -18,6 +13,8 @@ use Magento\Framework\App\ObjectManager; /** + * Product attribute add/edit form main tab + * * @api * @since 100.0.2 */ @@ -73,6 +70,7 @@ public function __construct( * Adding product form elements for editing attribute * * @return $this + * @throws \Magento\Framework\Exception\LocalizedException * @SuppressWarnings(PHPMD) */ protected function _prepareForm() @@ -255,7 +253,7 @@ protected function _prepareForm() } /** - * Initialize form fileds values + * Initialize form fields values * * @return $this */ diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main.php index 1b188de40710..3b9036c1fbbc 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main.php @@ -6,13 +6,13 @@ namespace Magento\Catalog\Block\Adminhtml\Product\Attribute\Set; /** - * Adminhtml Catalog Attribute Set Main Block - * * @author Magento Core Team <core@magentocommerce.com> */ use Magento\Catalog\Model\Entity\Product\Attribute\Group\AttributeMapperInterface; /** + * Adminhtml Catalog Attribute Set Main Block. + * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 @@ -140,7 +140,7 @@ protected function _prepareLayout() ) . '\', \'' . $this->getUrl( 'catalog/*/delete', ['id' => $setId] - ) . '\')', + ) . '\',{data: {}})', 'class' => 'delete' ] ); diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Formgroup.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Formgroup.php index ee92fd7c19b8..26ffc6e0df3d 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Formgroup.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Formgroup.php @@ -11,6 +11,9 @@ use Magento\Backend\Block\Widget\Form; +/** + * Form group for attribute set + */ class Formgroup extends \Magento\Backend\Block\Widget\Form\Generic { /** @@ -37,6 +40,8 @@ public function __construct( } /** + * Prepare form elements + * * @return void */ protected function _prepareForm() @@ -77,13 +82,15 @@ protected function _prepareForm() } /** + * Returns set id + * * @return int */ protected function _getSetId() { - return intval( + return (int)( $this->getRequest()->getParam('id') - ) > 0 ? intval( + ) > 0 ? (int)( $this->getRequest()->getParam('id') ) : $this->_typeFactory->create()->load( $this->_coreRegistry->registry('entityType') diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Attributes.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Attributes.php index 4310f61c5716..2df0ff0b6cd7 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Attributes.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Attributes.php @@ -9,13 +9,18 @@ * * @author Magento Core Team <core@magentocommerce.com> */ +declare(strict_types=1); + namespace Magento\Catalog\Block\Adminhtml\Product\Edit\Action\Attribute\Tab; use Magento\Framework\Data\Form\Element\AbstractElement; /** + * Attributes tab block + * * @api * @SuppressWarnings(PHPMD.DepthOfInheritance) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 */ class Attributes extends \Magento\Catalog\Block\Adminhtml\Form implements @@ -31,6 +36,9 @@ class Attributes extends \Magento\Catalog\Block\Adminhtml\Form implements */ protected $_attributeAction; + /** @var array */ + private $excludeFields; + /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Framework\Registry $registry @@ -38,6 +46,7 @@ class Attributes extends \Magento\Catalog\Block\Adminhtml\Form implements * @param \Magento\Catalog\Model\ProductFactory $productFactory * @param \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeAction * @param array $data + * @param array|null $excludeFields */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -45,37 +54,25 @@ public function __construct( \Magento\Framework\Data\FormFactory $formFactory, \Magento\Catalog\Model\ProductFactory $productFactory, \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeAction, - array $data = [] + array $data = [], + array $excludeFields = null ) { $this->_attributeAction = $attributeAction; $this->_productFactory = $productFactory; - parent::__construct($context, $registry, $formFactory, $data); - } + $this->excludeFields = $excludeFields ?: []; - /** - * @return void - */ - protected function _construct() - { - parent::_construct(); - $this->setShowGlobalIcon(true); + parent::__construct($context, $registry, $formFactory, $data); } /** + * Prepares form + * * @return void + * @throws \Magento\Framework\Exception\LocalizedException */ - protected function _prepareForm() + protected function _prepareForm(): void { - $this->setFormExcludedFieldList( - [ - 'category_ids', - 'gallery', - 'image', - 'media_gallery', - 'quantity_and_stock_status', - 'tier_price', - ] - ); + $this->setFormExcludedFieldList($this->getExcludedFields()); $this->_eventManager->dispatch( 'adminhtml_catalog_product_form_prepare_excluded_field_list', ['object' => $this] @@ -149,12 +146,14 @@ protected function _getAdditionalElementHtml($element) weightHandle.hideWeightSwitcher(); });</script> HTML; - // @codingStandardsIgnoreEnd + // @codingStandardsIgnoreEnd } return $html; } /** + * Returns tab label + * * @return \Magento\Framework\Phrase */ public function getTabLabel() @@ -163,6 +162,8 @@ public function getTabLabel() } /** + * Return Tab title + * * @return \Magento\Framework\Phrase */ public function getTabTitle() @@ -171,6 +172,8 @@ public function getTabTitle() } /** + * Can show tab in tabs + * * @return bool */ public function canShowTab() @@ -179,10 +182,22 @@ public function canShowTab() } /** + * Tab not hidden + * * @return bool */ public function isHidden() { return false; } + + /** + * Returns excluded fields + * + * @return array + */ + private function getExcludedFields(): array + { + return $this->excludeFields; + } } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Inventory.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Inventory.php index 750bf6f8a021..964872b6e51b 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Inventory.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Inventory.php @@ -70,11 +70,11 @@ public function getFieldSuffix() * Retrieve current store id * * @return int + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getStoreId() { - $storeId = $this->getRequest()->getParam('store'); - return intval($storeId); + return (int)$this->getRequest()->getParam('store'); } /** @@ -99,6 +99,8 @@ public function getTabLabel() } /** + * Return Tab title. + * * @return \Magento\Framework\Phrase */ public function getTabTitle() @@ -107,7 +109,7 @@ public function getTabTitle() } /** - * @return bool + * @inheritdoc */ public function canShowTab() { @@ -115,7 +117,7 @@ public function canShowTab() } /** - * @return bool + * @inheritdoc */ public function isHidden() { @@ -123,6 +125,8 @@ public function isHidden() } /** + * Get availability status. + * * @param string $fieldName * @return bool * @SuppressWarnings(PHPMD.UnusedFormalParameter) diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Crosssell.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Crosssell.php index e4f700e5790a..e5ce59c550af 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Crosssell.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Crosssell.php @@ -10,9 +10,13 @@ use Magento\Catalog\Model\Product; /** + * Crossel product edit tab + * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 + * @deprecated Not used since cross-sell products grid moved to UI components. + * @see \Magento\Catalog\Ui\DataProvider\Product\Related\CrossSellDataProvider */ class Crosssell extends Extended { diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Related.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Related.php index 0c59a7402dac..23b927598e8e 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Related.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Related.php @@ -9,8 +9,12 @@ use Magento\Backend\Block\Widget\Grid\Extended; /** + * Related product edit tab + * * @api * @since 100.0.2 + * @deprecated Not used since related products grid moved to UI components. + * @see \Magento\Catalog\Ui\DataProvider\Product\Related\RelatedDataProvider */ class Related extends Extended { @@ -318,7 +322,7 @@ protected function _prepareColumns() } /** - * Rerieve grid URL + * Retrieve grid URL * * @return string */ diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Upsell.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Upsell.php index 323b1785bc96..41ad72ca39e5 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Upsell.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Upsell.php @@ -6,8 +6,12 @@ namespace Magento\Catalog\Block\Adminhtml\Product\Edit\Tab; /** + * Upsell product edit tab + * * @api * @since 100.0.2 + * @deprecated Not used since upsell products grid moved to UI components. + * @see \Magento\Catalog\Ui\DataProvider\Product\Related\CrossSellDataProvider */ class Upsell extends \Magento\Backend\Block\Widget\Grid\Extended { diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tabs.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tabs.php index 04c3a208b97f..37ad3f4bea20 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tabs.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tabs.php @@ -8,7 +8,7 @@ use Magento\Backend\Block\Template\Context; use Magento\Backend\Block\Widget\Accordion; -use Magento\Backend\Block\Widget\Tabs as WigetTabs; +use Magento\Backend\Block\Widget\Tabs as WidgetTabs; use Magento\Backend\Model\Auth\Session; use Magento\Catalog\Helper\Catalog; use Magento\Catalog\Helper\Data; @@ -22,7 +22,7 @@ * Admin product edit tabs * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Tabs extends WigetTabs +class Tabs extends WidgetTabs { const BASIC_TAB_GROUP_CODE = 'basic'; @@ -109,7 +109,7 @@ public function __construct( } /** - * @return void + * @inheritdoc */ protected function _construct() { @@ -119,6 +119,8 @@ protected function _construct() } /** + * Get group collection. + * * @param int $attributeSetId * @return \Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\Collection */ @@ -131,10 +133,11 @@ public function getGroupCollection($attributeSetId) } /** - * @return $this + * @inheritdoc * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ protected function _prepareLayout() { @@ -315,6 +318,8 @@ public function getAttributeTabBlock() } /** + * Set attribute tab block. + * * @param string $attributeTabBlock * @return $this */ @@ -337,6 +342,8 @@ protected function _translateHtml($html) } /** + * Get accordion. + * * @param string $parentTab * @return string */ diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Frontend/Product/Watermark.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Frontend/Product/Watermark.php index 1f74969c3d16..7f80aece60ee 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Frontend/Product/Watermark.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Frontend/Product/Watermark.php @@ -4,15 +4,15 @@ * See COPYING.txt for license details. */ +namespace Magento\Catalog\Block\Adminhtml\Product\Frontend\Product; + +use Magento\Framework\Data\Form\Element\AbstractElement; + /** * Fieldset config form element renderer * * @author Magento Core Team <core@magentocommerce.com> */ -namespace Magento\Catalog\Block\Adminhtml\Product\Frontend\Product; - -use Magento\Framework\Data\Form\Element\AbstractElement; - class Watermark extends \Magento\Backend\Block\AbstractBlock implements \Magento\Framework\Data\Form\Element\Renderer\RendererInterface { @@ -60,6 +60,8 @@ public function __construct( } /** + * Render form element as HTML + * * @param AbstractElement $element * @return string */ @@ -124,13 +126,14 @@ public function render(AbstractElement $element) } /** + * Get header html for render + * * @param AbstractElement $element * @return string * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ protected function _getHeaderHtml($element) { - $id = $element->getHtmlId(); $default = !$this->getRequest()->getParam('website') && !$this->getRequest()->getParam('store'); $html = '<h4 class="icon-head head-edit-form">' . $element->getLegend() . '</h4>'; @@ -148,6 +151,8 @@ protected function _getHeaderHtml($element) } /** + * Get footer html for render + * * @param AbstractElement $element * @return string * @SuppressWarnings(PHPMD.UnusedFormalParameter) diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery.php index 6cb6f0e526e9..36740f5853d7 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery.php @@ -11,11 +11,16 @@ */ namespace Magento\Catalog\Block\Adminhtml\Product\Helper\Form; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\Request\DataPersistorInterface; use Magento\Framework\Registry; use Magento\Catalog\Model\Product; use Magento\Eav\Model\Entity\Attribute; use Magento\Catalog\Api\Data\ProductInterface; +/** + * Adminhtml gallery block + */ class Gallery extends \Magento\Framework\View\Element\AbstractBlock { /** @@ -66,27 +71,37 @@ class Gallery extends \Magento\Framework\View\Element\AbstractBlock */ protected $registry; + /** + * @var DataPersistorInterface + */ + private $dataPersistor; + /** * @param \Magento\Framework\View\Element\Context $context * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param Registry $registry * @param \Magento\Framework\Data\Form $form * @param array $data + * @param DataPersistorInterface|null $dataPersistor */ public function __construct( \Magento\Framework\View\Element\Context $context, \Magento\Store\Model\StoreManagerInterface $storeManager, Registry $registry, \Magento\Framework\Data\Form $form, - $data = [] + $data = [], + DataPersistorInterface $dataPersistor = null ) { $this->storeManager = $storeManager; $this->registry = $registry; $this->form = $form; + $this->dataPersistor = $dataPersistor ?: ObjectManager::getInstance()->get(DataPersistorInterface::class); parent::__construct($context, $data); } /** + * Returns element html. + * * @return string */ public function getElementHtml() @@ -102,7 +117,24 @@ public function getElementHtml() */ public function getImages() { - return $this->getDataObject()->getData('media_gallery') ?: null; + $images = $this->getDataObject()->getData('media_gallery') ?: null; + if ($images === null) { + $images = ((array)$this->dataPersistor->get('catalog_product'))['product']['media_gallery'] ?? null; + } + + return $images; + } + + /** + * Get value for given type. + * + * @param string $type + * @return string|null + */ + public function getImageValue(string $type) + { + $product = (array)$this->dataPersistor->get('catalog_product'); + return $product['product'][$type] ?? null; } /** @@ -122,6 +154,8 @@ public function getContentHtml() } /** + * Returns html id + * * @return string */ protected function getHtmlId() @@ -130,6 +164,8 @@ protected function getHtmlId() } /** + * Returns name + * * @return string */ public function getName() @@ -138,6 +174,8 @@ public function getName() } /** + * Returns suffix for field name + * * @return string */ public function getFieldNameSuffix() @@ -146,6 +184,8 @@ public function getFieldNameSuffix() } /** + * Returns data scope html id + * * @return string */ public function getDataScopeHtmlId() @@ -230,7 +270,6 @@ public function getDataObject() /** * Retrieve attribute field name * - * * @param Attribute $attribute * @return string */ @@ -244,6 +283,8 @@ public function getAttributeFieldName($attribute) } /** + * Returns html content of the block + * * @return string */ public function toHtml() diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php index 8d51f78ff5b6..063503682f4d 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php @@ -13,11 +13,16 @@ */ namespace Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery; +use Magento\Framework\App\ObjectManager; use Magento\Backend\Block\Media\Uploader; use Magento\Framework\View\Element\AbstractBlock; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Exception\FileSystemException; +use Magento\Backend\Block\DataProviders\ImageUploadConfig as ImageUploadConfigDataProvider; +/** + * Block for gallery content. + */ class Content extends \Magento\Backend\Block\Widget { /** @@ -40,29 +45,44 @@ class Content extends \Magento\Backend\Block\Widget */ private $imageHelper; + /** + * @var ImageUploadConfigDataProvider + */ + private $imageUploadConfigDataProvider; + /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Framework\Json\EncoderInterface $jsonEncoder * @param \Magento\Catalog\Model\Product\Media\Config $mediaConfig * @param array $data + * @param ImageUploadConfigDataProvider $imageUploadConfigDataProvider */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Framework\Json\EncoderInterface $jsonEncoder, \Magento\Catalog\Model\Product\Media\Config $mediaConfig, - array $data = [] + array $data = [], + ImageUploadConfigDataProvider $imageUploadConfigDataProvider = null ) { $this->_jsonEncoder = $jsonEncoder; $this->_mediaConfig = $mediaConfig; parent::__construct($context, $data); + $this->imageUploadConfigDataProvider = $imageUploadConfigDataProvider + ?: ObjectManager::getInstance()->get(ImageUploadConfigDataProvider::class); } /** + * Prepare layout. + * * @return AbstractBlock */ protected function _prepareLayout() { - $this->addChild('uploader', \Magento\Backend\Block\Media\Uploader::class); + $this->addChild( + 'uploader', + \Magento\Backend\Block\Media\Uploader::class, + ['image_upload_config_data' => $this->imageUploadConfigDataProvider] + ); $this->getUploader()->getConfig()->setUrl( $this->_urlBuilder->addSessionParam()->getUrl('catalog/product_gallery/upload') @@ -103,6 +123,8 @@ public function getUploaderHtml() } /** + * Returns js object name + * * @return string */ public function getJsObjectName() @@ -111,6 +133,8 @@ public function getJsObjectName() } /** + * Returns buttons for add image action. + * * @return string */ public function getAddImagesButton() @@ -124,6 +148,8 @@ public function getAddImagesButton() } /** + * Returns image json + * * @return string */ public function getImagesJson() @@ -169,6 +195,8 @@ private function sortImagesByPosition($images) } /** + * Returns image values json + * * @return string */ public function getImagesValuesJson() @@ -193,9 +221,11 @@ public function getImageTypes() $imageTypes = []; foreach ($this->getMediaAttributes() as $attribute) { /* @var $attribute \Magento\Eav\Model\Entity\Attribute */ + $value = $this->getElement()->getDataObject()->getData($attribute->getAttributeCode()) + ?: $this->getElement()->getImageValue($attribute->getAttributeCode()); $imageTypes[$attribute->getAttributeCode()] = [ 'code' => $attribute->getAttributeCode(), - 'value' => $this->getElement()->getDataObject()->getData($attribute->getAttributeCode()), + 'value' => $value, 'label' => $attribute->getFrontend()->getLabel(), 'scope' => __($this->getElement()->getScopeLabel($attribute)), 'name' => $this->getElement()->getAttributeFieldName($attribute), @@ -241,6 +271,8 @@ public function getImageTypesJson() } /** + * Returns image helper object. + * * @return \Magento\Catalog\Helper\Image * @deprecated 101.0.3 */ diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Rss/NotifyStock.php b/app/code/Magento/Catalog/Block/Adminhtml/Rss/NotifyStock.php index ac1fd8c692ed..c296a5aa0dbb 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Rss/NotifyStock.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Rss/NotifyStock.php @@ -9,6 +9,7 @@ /** * Class NotifyStock + * * @package Magento\Catalog\Block\Adminhtml\Rss */ class NotifyStock extends \Magento\Backend\Block\AbstractBlock implements DataProviderInterface @@ -41,7 +42,7 @@ public function __construct( } /** - * @return void + * @inheritdoc */ protected function _construct() { @@ -50,12 +51,12 @@ protected function _construct() } /** - * {@inheritdoc} + * @inheritdoc */ public function getRssData() { - $newUrl = $this->rssUrlBuilder->getUrl(['_secure' => true, '_nosecret' => true, 'type' => 'notifystock']); - $title = __('Low Stock Products'); + $newUrl = $this->rssUrlBuilder->getUrl(['_secure' => true, '_nosecret' => true, 'type' => 'notifystock']); + $title = __('Low Stock Products')->render(); $data = ['title' => $title, 'description' => $title, 'link' => $newUrl, 'charset' => 'UTF-8']; foreach ($this->rssModel->getProductsCollection() as $item) { @@ -65,7 +66,7 @@ public function getRssData() ['id' => $item->getId(), '_secure' => true, '_nosecret' => true] ); $qty = 1 * $item->getQty(); - $description = __('%1 has reached a quantity of %2.', $item->getName(), $qty); + $description = __('%1 has reached a quantity of %2.', $item->getName(), $qty)->render(); $data['entries'][] = ['title' => $item->getName(), 'link' => $url, 'description' => $description]; } @@ -73,7 +74,7 @@ public function getRssData() } /** - * {@inheritdoc} + * @inheritdoc */ public function getCacheLifetime() { @@ -81,7 +82,7 @@ public function getCacheLifetime() } /** - * {@inheritdoc} + * @inheritdoc */ public function isAllowed() { @@ -89,7 +90,7 @@ public function isAllowed() } /** - * {@inheritdoc} + * @inheritdoc */ public function getFeeds() { @@ -97,7 +98,7 @@ public function getFeeds() } /** - * {@inheritdoc} + * @inheritdoc */ public function isAuthRequired() { diff --git a/app/code/Magento/Catalog/Block/Product/AbstractProduct.php b/app/code/Magento/Catalog/Block/Product/AbstractProduct.php index 4102c82a0a31..c8da0f70f73b 100644 --- a/app/code/Magento/Catalog/Block/Product/AbstractProduct.php +++ b/app/code/Magento/Catalog/Block/Product/AbstractProduct.php @@ -125,6 +125,7 @@ public function __construct(\Magento\Catalog\Block\Product\Context $context, arr /** * Retrieve url for add product to cart + * * Will return product view page URL if product has required options * * @param \Magento\Catalog\Model\Product $product @@ -473,7 +474,9 @@ public function getProductDetailsHtml(\Magento\Catalog\Model\Product $product) } /** - * @param null $type + * Get the renderer that will be used to render the details block + * + * @param string|null $type * @return bool|\Magento\Framework\View\Element\AbstractBlock */ public function getDetailsRenderer($type = null) @@ -489,6 +492,8 @@ public function getDetailsRenderer($type = null) } /** + * Return the list of details + * * @return \Magento\Framework\View\Element\RendererList */ protected function getDetailsRendererList() diff --git a/app/code/Magento/Catalog/Block/Product/Compare/ListCompare.php b/app/code/Magento/Catalog/Block/Product/Compare/ListCompare.php index 6c54aa4e171e..76f5dbd1bea8 100644 --- a/app/code/Magento/Catalog/Block/Product/Compare/ListCompare.php +++ b/app/code/Magento/Catalog/Block/Product/Compare/ListCompare.php @@ -122,12 +122,7 @@ public function __construct( */ public function getAddToWishlistParams($product) { - $continueUrl = $this->urlEncoder->encode($this->getUrl('customer/account')); - $urlParamName = Action::PARAM_NAME_URL_ENCODED; - - $continueUrlParams = [$urlParamName => $continueUrl]; - - return $this->_wishlistHelper->getAddParams($product, $continueUrlParams); + return $this->_wishlistHelper->getAddParams($product); } /** diff --git a/app/code/Magento/Catalog/Block/Product/Image.php b/app/code/Magento/Catalog/Block/Product/Image.php index 20a556ab4145..7a7f9c0affc7 100644 --- a/app/code/Magento/Catalog/Block/Product/Image.php +++ b/app/code/Magento/Catalog/Block/Product/Image.php @@ -6,6 +6,8 @@ namespace Magento\Catalog\Block\Product; /** + * Product image block + * * @api * @method string getImageUrl() * @method string getWidth() @@ -13,6 +15,7 @@ * @method string getLabel() * @method float getRatio() * @method string getCustomAttributes() + * @method string getClass() * @since 100.0.2 */ class Image extends \Magento\Framework\View\Element\Template diff --git a/app/code/Magento/Catalog/Block/Product/ImageFactory.php b/app/code/Magento/Catalog/Block/Product/ImageFactory.php index f9a576367dde..aa303af656a5 100644 --- a/app/code/Magento/Catalog/Block/Product/ImageFactory.php +++ b/app/code/Magento/Catalog/Block/Product/ImageFactory.php @@ -77,16 +77,29 @@ private function getStringCustomAttributes(array $attributes): string { $result = []; foreach ($attributes as $name => $value) { - $result[] = $name . '="' . $value . '"'; + if ($name != 'class') { + $result[] = $name . '="' . $value . '"'; + } } return !empty($result) ? implode(' ', $result) : ''; } + /** + * Retrieve image class for HTML element + * + * @param array $attributes + * @return string + */ + private function getClass(array $attributes): string + { + return $attributes['class'] ?? 'product-image-photo'; + } + /** * Calculate image ratio * - * @param $width - * @param $height + * @param int $width + * @param int $height * @return float */ private function getRatio(int $width, int $height): float @@ -98,8 +111,9 @@ private function getRatio(int $width, int $height): float } /** - * @param Product $product + * Get image label * + * @param Product $product * @param string $imageType * @return string */ @@ -114,6 +128,7 @@ private function getLabel(Product $product, string $imageType): string /** * Create image block from product + * * @param Product $product * @param string $imageId * @param array|null $attributes @@ -154,6 +169,7 @@ public function create(Product $product, string $imageId, array $attributes = nu 'label' => $this->getLabel($product, $imageMiscParams['image_type']), 'ratio' => $this->getRatio($imageMiscParams['image_width'], $imageMiscParams['image_height']), 'custom_attributes' => $this->getStringCustomAttributes($attributes), + 'class' => $this->getClass($attributes), 'product_id' => $product->getId() ], ]; diff --git a/app/code/Magento/Catalog/Block/Product/ListProduct.php b/app/code/Magento/Catalog/Block/Product/ListProduct.php index c1b255c762db..c1d79894162a 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/ProductList/Crosssell.php b/app/code/Magento/Catalog/Block/Product/ProductList/Crosssell.php index 0c547f81c85d..596cd7cc5bdc 100644 --- a/app/code/Magento/Catalog/Block/Product/ProductList/Crosssell.php +++ b/app/code/Magento/Catalog/Block/Product/ProductList/Crosssell.php @@ -9,6 +9,9 @@ */ namespace Magento\Catalog\Block\Product\ProductList; +/** + * Crosssell block for product + */ class Crosssell extends \Magento\Catalog\Block\Product\AbstractProduct { /** @@ -25,7 +28,7 @@ class Crosssell extends \Magento\Catalog\Block\Product\AbstractProduct */ protected function _prepareData() { - $product = $this->_coreRegistry->registry('product'); + $product = $this->getProduct(); /* @var $product \Magento\Catalog\Model\Product */ $this->_itemCollection = $product->getCrossSellProductCollection()->addAttributeToSelect( @@ -43,6 +46,7 @@ protected function _prepareData() /** * Before rendering html process + * * Prepare items collection * * @return \Magento\Catalog\Block\Product\ProductList\Crosssell diff --git a/app/code/Magento/Catalog/Block/Product/ProductList/Related.php b/app/code/Magento/Catalog/Block/Product/ProductList/Related.php index 219922f9e46d..6de70bb97136 100644 --- a/app/code/Magento/Catalog/Block/Product/ProductList/Related.php +++ b/app/code/Magento/Catalog/Block/Product/ProductList/Related.php @@ -77,11 +77,13 @@ public function __construct( } /** + * Prepare data + * * @return $this */ protected function _prepareData() { - $product = $this->_coreRegistry->registry('product'); + $product = $this->getProduct(); /* @var $product \Magento\Catalog\Model\Product */ $this->_itemCollection = $product->getRelatedProductCollection()->addAttributeToSelect( @@ -103,6 +105,8 @@ protected function _prepareData() } /** + * Before to html handler + * * @return $this */ protected function _beforeToHtml() @@ -112,6 +116,8 @@ protected function _beforeToHtml() } /** + * Get collection items + * * @return Collection */ public function getItems() diff --git a/app/code/Magento/Catalog/Block/Product/ProductList/Toolbar.php b/app/code/Magento/Catalog/Block/Product/ProductList/Toolbar.php index 0b8d2d6c89e7..c530ba4785ad 100644 --- a/app/code/Magento/Catalog/Block/Product/ProductList/Toolbar.php +++ b/app/code/Magento/Catalog/Block/Product/ProductList/Toolbar.php @@ -7,6 +7,8 @@ use Magento\Catalog\Helper\Product\ProductList; use Magento\Catalog\Model\Product\ProductList\Toolbar as ToolbarModel; +use Magento\Catalog\Model\Product\ProductList\ToolbarMemorizer; +use Magento\Framework\App\ObjectManager; /** * Product list toolbar @@ -77,6 +79,7 @@ class Toolbar extends \Magento\Framework\View\Element\Template /** * @var bool $_paramsMemorizeAllowed + * @deprecated */ protected $_paramsMemorizeAllowed = true; @@ -96,6 +99,7 @@ class Toolbar extends \Magento\Framework\View\Element\Template * Catalog session * * @var \Magento\Catalog\Model\Session + * @deprecated */ protected $_catalogSession; @@ -104,6 +108,11 @@ class Toolbar extends \Magento\Framework\View\Element\Template */ protected $_toolbarModel; + /** + * @var ToolbarMemorizer + */ + private $toolbarMemorizer; + /** * @var ProductList */ @@ -119,6 +128,16 @@ class Toolbar extends \Magento\Framework\View\Element\Template */ protected $_postDataHelper; + /** + * @var \Magento\Framework\App\Http\Context + */ + private $httpContext; + + /** + * @var \Magento\Framework\Data\Form\FormKey + */ + private $formKey; + /** * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Catalog\Model\Session $catalogSession @@ -128,6 +147,11 @@ class Toolbar extends \Magento\Framework\View\Element\Template * @param ProductList $productListHelper * @param \Magento\Framework\Data\Helper\PostHelper $postDataHelper * @param array $data + * @param ToolbarMemorizer|null $toolbarMemorizer + * @param \Magento\Framework\App\Http\Context|null $httpContext + * @param \Magento\Framework\Data\Form\FormKey|null $formKey + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, @@ -137,7 +161,10 @@ public function __construct( \Magento\Framework\Url\EncoderInterface $urlEncoder, ProductList $productListHelper, \Magento\Framework\Data\Helper\PostHelper $postDataHelper, - array $data = [] + array $data = [], + ToolbarMemorizer $toolbarMemorizer = null, + \Magento\Framework\App\Http\Context $httpContext = null, + \Magento\Framework\Data\Form\FormKey $formKey = null ) { $this->_catalogSession = $catalogSession; $this->_catalogConfig = $catalogConfig; @@ -145,6 +172,15 @@ public function __construct( $this->urlEncoder = $urlEncoder; $this->_productListHelper = $productListHelper; $this->_postDataHelper = $postDataHelper; + $this->toolbarMemorizer = $toolbarMemorizer ?: ObjectManager::getInstance()->get( + ToolbarMemorizer::class + ); + $this->httpContext = $httpContext ?: ObjectManager::getInstance()->get( + \Magento\Framework\App\Http\Context::class + ); + $this->formKey = $formKey ?: ObjectManager::getInstance()->get( + \Magento\Framework\Data\Form\FormKey::class + ); parent::__construct($context, $data); } @@ -152,6 +188,7 @@ public function __construct( * Disable list state params memorizing * * @return $this + * @deprecated */ public function disableParamsMemorizing() { @@ -165,6 +202,7 @@ public function disableParamsMemorizing() * @param string $param parameter name * @param mixed $value parameter value * @return $this + * @deprecated */ protected function _memorizeParam($param, $value) { @@ -244,13 +282,13 @@ public function getCurrentOrder() $defaultOrder = $keys[0]; } - $order = $this->_toolbarModel->getOrder(); + $order = $this->toolbarMemorizer->getOrder(); if (!$order || !isset($orders[$order])) { $order = $defaultOrder; } - if ($order != $defaultOrder) { - $this->_memorizeParam('sort_order', $order); + if ($this->toolbarMemorizer->isMemorizingAllowed()) { + $this->httpContext->setValue(ToolbarModel::ORDER_PARAM_NAME, $order, $defaultOrder); } $this->setData('_current_grid_order', $order); @@ -270,13 +308,13 @@ public function getCurrentDirection() } $directions = ['asc', 'desc']; - $dir = strtolower($this->_toolbarModel->getDirection()); + $dir = strtolower($this->toolbarMemorizer->getDirection()); if (!$dir || !in_array($dir, $directions)) { $dir = $this->_direction; } - if ($dir != $this->_direction) { - $this->_memorizeParam('sort_direction', $dir); + if ($this->toolbarMemorizer->isMemorizingAllowed()) { + $this->httpContext->setValue(ToolbarModel::DIRECTION_PARAM_NAME, $dir, $this->_direction); } $this->setData('_current_grid_direction', $dir); @@ -392,6 +430,8 @@ public function getPagerUrl($params = []) } /** + * Get pager encoded url. + * * @param array $params * @return string */ @@ -412,11 +452,15 @@ public function getCurrentMode() return $mode; } $defaultMode = $this->_productListHelper->getDefaultViewMode($this->getModes()); - $mode = $this->_toolbarModel->getMode(); + $mode = $this->toolbarMemorizer->getMode(); if (!$mode || !isset($this->_availableMode[$mode])) { $mode = $defaultMode; } + if ($this->toolbarMemorizer->isMemorizingAllowed()) { + $this->httpContext->setValue(ToolbarModel::MODE_PARAM_NAME, $mode, $defaultMode); + } + $this->setData('_current_grid_mode', $mode); return $mode; } @@ -568,13 +612,13 @@ public function getLimit() $defaultLimit = $keys[0]; } - $limit = $this->_toolbarModel->getLimit(); + $limit = $this->toolbarMemorizer->getLimit(); if (!$limit || !isset($limits[$limit])) { $limit = $defaultLimit; } - if ($limit != $defaultLimit) { - $this->_memorizeParam('limit_page', $limit); + if ($this->toolbarMemorizer->isMemorizingAllowed()) { + $this->httpContext->setValue(ToolbarModel::LIMIT_PARAM_NAME, $limit, $defaultLimit); } $this->setData('_current_limit', $limit); @@ -582,6 +626,8 @@ public function getLimit() } /** + * Check if limit is current used in toolbar. + * * @param int $limit * @return bool */ @@ -591,6 +637,8 @@ public function isLimitCurrent($limit) } /** + * Pager number of items from which products started on current page. + * * @return int */ public function getFirstNum() @@ -600,6 +648,8 @@ public function getFirstNum() } /** + * Pager number of items products finished on current page. + * * @return int */ public function getLastNum() @@ -609,6 +659,8 @@ public function getLastNum() } /** + * Total number of products in current category. + * * @return int */ public function getTotalNum() @@ -617,6 +669,8 @@ public function getTotalNum() } /** + * Check if current page is the first. + * * @return bool */ public function isFirstPage() @@ -625,6 +679,8 @@ public function isFirstPage() } /** + * Return last page number. + * * @return int */ public function getLastPageNum() @@ -692,6 +748,8 @@ public function getWidgetOptionsJson(array $customOptions = []) 'orderDefault' => $this->getOrderField(), 'limitDefault' => $this->_productListHelper->getDefaultLimitPerPageValue($defaultMode), 'url' => $this->getPagerUrl(), + 'formKey' => $this->formKey->getFormKey(), + 'post' => $this->toolbarMemorizer->isMemorizingAllowed() ? true : false ]; $options = array_replace_recursive($options, $customOptions); return json_encode(['productListToolbarForm' => $options]); diff --git a/app/code/Magento/Catalog/Block/Product/ProductList/Upsell.php b/app/code/Magento/Catalog/Block/Product/ProductList/Upsell.php index 10aebf270579..24822447ae91 100644 --- a/app/code/Magento/Catalog/Block/Product/ProductList/Upsell.php +++ b/app/code/Magento/Catalog/Block/Product/ProductList/Upsell.php @@ -7,7 +7,6 @@ namespace Magento\Catalog\Block\Product\ProductList; use Magento\Catalog\Model\ResourceModel\Product\Collection; -use Magento\Framework\View\Element\AbstractBlock; /** * Catalog product upsell items block @@ -92,11 +91,13 @@ public function __construct( } /** + * Prepare data + * * @return $this */ protected function _prepareData() { - $product = $this->_coreRegistry->registry('product'); + $product = $this->getProduct(); /* @var $product \Magento\Catalog\Model\Product */ $this->_itemCollection = $product->getUpSellProductCollection()->setPositionOrder()->addStoreFilter(); if ($this->moduleManager->isEnabled('Magento_Checkout')) { @@ -122,6 +123,8 @@ protected function _prepareData() } /** + * Before to html handler + * * @return $this */ protected function _beforeToHtml() @@ -131,6 +134,8 @@ protected function _beforeToHtml() } /** + * Get items collection + * * @return Collection */ public function getItemCollection() @@ -146,6 +151,8 @@ public function getItemCollection() } /** + * Get collection items + * * @return \Magento\Framework\DataObject[] */ public function getItems() @@ -157,6 +164,8 @@ public function getItems() } /** + * Get row count + * * @return float */ public function getRowCount() @@ -165,18 +174,22 @@ public function getRowCount() } /** + * Set column count + * * @param string $columns * @return $this */ public function setColumnCount($columns) { - if (intval($columns) > 0) { - $this->_columnCount = intval($columns); + if ((int) $columns > 0) { + $this->_columnCount = (int) $columns; } return $this; } /** + * Get column count + * * @return int */ public function getColumnCount() @@ -185,6 +198,8 @@ public function getColumnCount() } /** + * Reset items iterator + * * @return void */ public function resetItemsIterator() @@ -194,6 +209,8 @@ public function resetItemsIterator() } /** + * Get iterable item + * * @return mixed */ public function getIterableItem() @@ -205,6 +222,7 @@ public function getIterableItem() /** * Set how many items we need to show in upsell block + * * Notice: this parameter will be also applied * * @param string $type @@ -213,13 +231,15 @@ public function getIterableItem() */ public function setItemLimit($type, $limit) { - if (intval($limit) > 0) { - $this->_itemLimits[$type] = intval($limit); + if ((int) $limit > 0) { + $this->_itemLimits[$type] = (int) $limit; } return $this; } /** + * Get item limit + * * @param string $type * @return array|int */ diff --git a/app/code/Magento/Catalog/Block/Product/View/Attributes.php b/app/code/Magento/Catalog/Block/Product/View/Attributes.php index cb59d86a7451..5b9777cbfd1e 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Attributes.php +++ b/app/code/Magento/Catalog/Block/Product/View/Attributes.php @@ -16,6 +16,8 @@ use Magento\Framework\Pricing\PriceCurrencyInterface; /** + * Attributes attributes block + * * @api * @since 100.0.2 */ @@ -56,6 +58,8 @@ public function __construct( } /** + * Returns a Product + * * @return Product */ public function getProduct() @@ -88,9 +92,9 @@ public function getAdditionalData(array $excludeAttr = []) $value = $this->priceCurrency->convertAndFormat($value); } - if (is_string($value) && strlen($value)) { + if (is_string($value) && strlen(trim($value))) { $data[$attribute->getAttributeCode()] = [ - 'label' => __($attribute->getStoreLabel()), + 'label' => $attribute->getStoreLabel(), 'value' => $value, 'code' => $attribute->getAttributeCode(), ]; diff --git a/app/code/Magento/Catalog/Block/Product/View/Details.php b/app/code/Magento/Catalog/Block/Product/View/Details.php new file mode 100644 index 000000000000..e76c5bf20133 --- /dev/null +++ b/app/code/Magento/Catalog/Block/Product/View/Details.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Block\Product\View; + +/** + * Product details block. + * + * Holds a group of blocks to show as tabs. + * + * @api + */ +class Details extends \Magento\Framework\View\Element\Template +{ + /** + * Get sorted child block names. + * + * @param string $groupName + * @param string $callback + * @throws \Magento\Framework\Exception\LocalizedException + * + * @return array + */ + public function getGroupSortedChildNames(string $groupName, string $callback): array + { + $groupChildNames = $this->getGroupChildNames($groupName, $callback); + $layout = $this->getLayout(); + + $childNamesSortOrder = []; + + foreach ($groupChildNames as $childName) { + $alias = $layout->getElementAlias($childName); + $sortOrder = (int)$this->getChildData($alias, 'sort_order') ?? 0; + + $childNamesSortOrder[$sortOrder] = $childName; + } + + ksort($childNamesSortOrder, SORT_NUMERIC); + + return $childNamesSortOrder; + } +} diff --git a/app/code/Magento/Catalog/Block/Product/View/Gallery.php b/app/code/Magento/Catalog/Block/Product/View/Gallery.php index ab01fc6d134e..8b98fbdc8f7e 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Gallery.php +++ b/app/code/Magento/Catalog/Block/Product/View/Gallery.php @@ -23,6 +23,8 @@ use Magento\Framework\Stdlib\ArrayUtils; /** + * Product gallery block + * * @api * @since 100.0.2 */ @@ -139,7 +141,7 @@ public function getGalleryImagesJson() 'thumb' => $image->getData('small_image_url'), 'img' => $image->getData('medium_image_url'), 'full' => $image->getData('large_image_url'), - 'caption' => $image->getData('label'), + 'caption' => ($image->getLabel() ?: $this->getProduct()->getName()), 'position' => $image->getData('position'), 'isMain' => $this->isMainImage($image), 'type' => str_replace('external-', '', $image->getMediaType()), @@ -196,6 +198,8 @@ public function isMainImage($image) } /** + * Returns image attribute + * * @param string $imageId * @param string $attributeName * @param string $default @@ -203,9 +207,9 @@ public function isMainImage($image) */ public function getImageAttribute($imageId, $attributeName, $default = null) { - $attributes = - $this->getConfigView()->getMediaAttributes('Magento_Catalog', Image::MEDIA_TYPE_CONFIG_NODE, $imageId); - return isset($attributes[$attributeName]) ? $attributes[$attributeName] : $default; + $attributes = $this->getConfigView() + ->getMediaAttributes('Magento_Catalog', Image::MEDIA_TYPE_CONFIG_NODE, $imageId); + return $attributes[$attributeName] ?? $default; } /** @@ -222,6 +226,8 @@ private function getConfigView() } /** + * Returns image gallery config object + * * @return Collection */ private function getGalleryImagesConfig() diff --git a/app/code/Magento/Catalog/Block/Product/View/GalleryOptions.php b/app/code/Magento/Catalog/Block/Product/View/GalleryOptions.php new file mode 100644 index 000000000000..0384c9cd9acc --- /dev/null +++ b/app/code/Magento/Catalog/Block/Product/View/GalleryOptions.php @@ -0,0 +1,156 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Catalog\Block\Product\View; + +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Catalog\Block\Product\Context; +use Magento\Framework\Stdlib\ArrayUtils; + +/** + * Gallery options block. + */ +class GalleryOptions extends AbstractView implements ArgumentInterface +{ + /** + * @var Json + */ + private $jsonSerializer; + + /** + * @var Gallery + */ + private $gallery; + + /** + * @param Context $context + * @param ArrayUtils $arrayUtils + * @param Json $jsonSerializer + * @param Gallery $gallery + * @param array $data + */ + public function __construct( + Context $context, + ArrayUtils $arrayUtils, + Json $jsonSerializer, + Gallery $gallery, + array $data = [] + ) { + $this->gallery = $gallery; + $this->jsonSerializer = $jsonSerializer; + parent::__construct($context, $arrayUtils, $data); + } + + /** + * Retrieve gallery options in JSON format + * + * @return string + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.ElseExpression) + */ + public function getOptionsJson() + { + $optionItems = null; + + //Special case for gallery/nav which can be the string "thumbs/false/dots" + if (is_bool($this->getVar("gallery/nav"))) { + $optionItems['nav'] = $this->getVar("gallery/nav") ? 'true' : 'false'; + } else { + $optionItems['nav'] = $this->escapeHtml($this->getVar("gallery/nav")); + } + + $optionItems['loop'] = $this->getVar("gallery/loop"); + $optionItems['keyboard'] = $this->getVar("gallery/keyboard"); + $optionItems['arrows'] = $this->getVar("gallery/arrows"); + $optionItems['allowfullscreen'] = $this->getVar("gallery/allowfullscreen"); + $optionItems['showCaption'] = $this->getVar("gallery/caption"); + $optionItems['width'] = (int)$this->escapeHtml( + $this->gallery->getImageAttribute('product_page_image_medium', 'width') + ); + $optionItems['thumbwidth'] = (int)$this->escapeHtml( + $this->gallery->getImageAttribute('product_page_image_small', 'width') + ); + + if ($this->gallery->getImageAttribute('product_page_image_small', 'height') || + $this->gallery->getImageAttribute('product_page_image_small', 'width')) { + $optionItems['thumbheight'] = (int)$this->escapeHtml( + $this->gallery->getImageAttribute('product_page_image_small', 'height') ?: + $this->gallery->getImageAttribute('product_page_image_small', 'width') + ); + } + + if ($this->gallery->getImageAttribute('product_page_image_medium', 'height') || + $this->gallery->getImageAttribute('product_page_image_medium', 'width')) { + $optionItems['height'] = (int)$this->escapeHtml( + $this->gallery->getImageAttribute('product_page_image_medium', 'height') ?: + $this->gallery->getImageAttribute('product_page_image_medium', 'width') + ); + } + + if ($this->getVar("gallery/transition/duration")) { + $optionItems['transitionduration'] = + (int)$this->escapeHtml($this->getVar("gallery/transition/duration")); + } + + $optionItems['transition'] = $this->escapeHtml($this->getVar("gallery/transition/effect")); + $optionItems['navarrows'] = $this->getVar("gallery/navarrows"); + $optionItems['navtype'] = $this->escapeHtml($this->getVar("gallery/navtype")); + $optionItems['navdir'] = $this->escapeHtml($this->getVar("gallery/navdir")); + + if ($this->getVar("gallery/thumbmargin")) { + $optionItems['thumbmargin'] = (int)$this->escapeHtml($this->getVar("gallery/thumbmargin")); + } + + return $this->jsonSerializer->serialize($optionItems); + } + + /** + * Retrieve gallery fullscreen options in JSON format + * + * @return string + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.ElseExpression) + */ + public function getFSOptionsJson() + { + $fsOptionItems = null; + + //Special case for gallery/nav which can be the string "thumbs/false/dots" + if (is_bool($this->getVar("gallery/fullscreen/nav"))) { + $fsOptionItems['nav'] = $this->getVar("gallery/fullscreen/nav") ? 'true' : 'false'; + } else { + $fsOptionItems['nav'] = $this->escapeHtml($this->getVar("gallery/fullscreen/nav")); + } + + $fsOptionItems['loop'] = $this->getVar("gallery/fullscreen/loop"); + $fsOptionItems['navdir'] = $this->escapeHtml($this->getVar("gallery/fullscreen/navdir")); + $fsOptionItems['navarrows'] = $this->getVar("gallery/fullscreen/navarrows"); + $fsOptionItems['navtype'] = $this->escapeHtml($this->getVar("gallery/fullscreen/navtype")); + $fsOptionItems['arrows'] = $this->getVar("gallery/fullscreen/arrows"); + $fsOptionItems['showCaption'] = $this->getVar("gallery/fullscreen/caption"); + + if ($this->getVar("gallery/fullscreen/transition/duration")) { + $fsOptionItems['transitionduration'] = (int)$this->escapeHtml( + $this->getVar("gallery/fullscreen/transition/duration") + ); + } + + $fsOptionItems['transition'] = $this->escapeHtml($this->getVar("gallery/fullscreen/transition/effect")); + + if ($this->getVar("gallery/fullscreen/keyboard")) { + $fsOptionItems['keyboard'] = $this->getVar("gallery/fullscreen/keyboard"); + } + + if ($this->getVar("gallery/fullscreen/thumbmargin")) { + $fsOptionItems['thumbmargin'] = + (int)$this->escapeHtml($this->getVar("gallery/fullscreen/thumbmargin")); + } + + return $this->jsonSerializer->serialize($fsOptionItems); + } +} diff --git a/app/code/Magento/Catalog/Block/Product/View/Options.php b/app/code/Magento/Catalog/Block/Product/View/Options.php index 0720c018f6a9..c457b20cd090 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 181211a0fc4a..059580b9b5ea 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php +++ b/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php @@ -9,11 +9,14 @@ * * @author Magento Core Team <core@magentocommerce.com> */ + namespace Magento\Catalog\Block\Product\View\Options; use Magento\Catalog\Pricing\Price\CustomOptionPriceInterface; /** + * Product aoptions section abstract block. + * * @api * @since 100.0.2 */ @@ -46,7 +49,7 @@ abstract class AbstractOptions extends \Magento\Framework\View\Element\Template /** * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Framework\Pricing\Helper\Data $pricingHelper - * @param \Magento\Catalog\Helper\Data $catalogData, + * @param \Magento\Catalog\Helper\Data $catalogData * @param array $data */ public function __construct( @@ -123,6 +126,8 @@ public function getFormattedPrice() } /** + * Retrieve formatted price. + * * @return string * * @deprecated @@ -134,7 +139,7 @@ public function getFormatedPrice() } /** - * Return formated price + * Return formatted price * * @param array $value * @param bool $flag diff --git a/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select.php b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select.php index 7df9b972e150..d9d663b32f4d 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select.php +++ b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select.php @@ -3,8 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Catalog\Block\Product\View\Options\Type; +use Magento\Catalog\Model\Product\Option; +use Magento\Catalog\Block\Product\View\Options\Type\Select\CheckableFactory; +use Magento\Catalog\Block\Product\View\Options\Type\Select\MultipleFactory; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Element\Template\Context; +use Magento\Framework\Pricing\Helper\Data; +use Magento\Catalog\Helper\Data as CatalogHelper; + /** * Product options text type block * @@ -13,169 +22,60 @@ */ class Select extends \Magento\Catalog\Block\Product\View\Options\AbstractOptions { + /** + * @var CheckableFactory + */ + private $checkableFactory; + /** + * @var MultipleFactory + */ + private $multipleFactory; + + /** + * Select constructor. + * @param Context $context + * @param Data $pricingHelper + * @param CatalogHelper $catalogData + * @param array $data + * @param CheckableFactory|null $checkableFactory + * @param MultipleFactory|null $multipleFactory + */ + public function __construct( + Context $context, + Data $pricingHelper, + CatalogHelper $catalogData, + array $data = [], + CheckableFactory $checkableFactory = null, + MultipleFactory $multipleFactory = null + ) { + parent::__construct($context, $pricingHelper, $catalogData, $data); + $this->checkableFactory = $checkableFactory ?: ObjectManager::getInstance()->get(CheckableFactory::class); + $this->multipleFactory = $multipleFactory ?: ObjectManager::getInstance()->get(MultipleFactory::class); + } + /** * Return html for control element * * @return string - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function getValuesHtml() { - $_option = $this->getOption(); - $configValue = $this->getProduct()->getPreconfiguredValues()->getData('options/' . $_option->getId()); - $store = $this->getProduct()->getStore(); - - $this->setSkipJsReloadPrice(1); - // Remove inline prototype onclick and onchange events - - if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN || - $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_MULTIPLE + $option = $this->getOption(); + $optionType = $option->getType(); + if ($optionType === Option::OPTION_TYPE_DROP_DOWN || + $optionType === Option::OPTION_TYPE_MULTIPLE ) { - $require = $_option->getIsRequire() ? ' required' : ''; - $extraParams = ''; - $select = $this->getLayout()->createBlock( - \Magento\Framework\View\Element\Html\Select::class - )->setData( - [ - 'id' => 'select_' . $_option->getId(), - 'class' => $require . ' product-custom-option admin__control-select' - ] - ); - if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN) { - $select->setName('options[' . $_option->getId() . ']')->addOption('', __('-- Please Select --')); - } else { - $select->setName('options[' . $_option->getId() . '][]'); - $select->setClass('multiselect admin__control-multiselect' . $require . ' product-custom-option'); - } - foreach ($_option->getValues() as $_value) { - $priceStr = $this->_formatPrice( - [ - 'is_percent' => $_value->getPriceType() == 'percent', - 'pricing_value' => $_value->getPrice($_value->getPriceType() == 'percent'), - ], - false - ); - $select->addOption( - $_value->getOptionTypeId(), - $_value->getTitle() . ' ' . strip_tags($priceStr) . '', - ['price' => $this->pricingHelper->currencyByStore($_value->getPrice(true), $store, false)] - ); - } - if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_MULTIPLE) { - $extraParams = ' multiple="multiple"'; - } - if (!$this->getSkipJsReloadPrice()) { - $extraParams .= ' onchange="opConfig.reloadPrice()"'; - } - $extraParams .= ' data-selector="' . $select->getName() . '"'; - $select->setExtraParams($extraParams); - - if ($configValue) { - $select->setValue($configValue); - } - - return $select->getHtml(); + $optionBlock = $this->multipleFactory->create(); } - - if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_RADIO || - $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX + if ($optionType === Option::OPTION_TYPE_RADIO || + $optionType === Option::OPTION_TYPE_CHECKBOX ) { - $selectHtml = '<div class="options-list nested" id="options-' . $_option->getId() . '-list">'; - $require = $_option->getIsRequire() ? ' required' : ''; - $arraySign = ''; - switch ($_option->getType()) { - case \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_RADIO: - $type = 'radio'; - $class = 'radio admin__control-radio'; - if (!$_option->getIsRequire()) { - $selectHtml .= '<div class="field choice admin__field admin__field-option">' . - '<input type="radio" id="options_' . - $_option->getId() . - '" class="' . - $class . - ' product-custom-option" name="options[' . - $_option->getId() . - ']"' . - ' data-selector="options[' . $_option->getId() . ']"' . - ($this->getSkipJsReloadPrice() ? '' : ' onclick="opConfig.reloadPrice()"') . - ' value="" checked="checked" /><label class="label admin__field-label" for="options_' . - $_option->getId() . - '"><span>' . - __('None') . '</span></label></div>'; - } - break; - case \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX: - $type = 'checkbox'; - $class = 'checkbox admin__control-checkbox'; - $arraySign = '[]'; - break; - } - $count = 1; - foreach ($_option->getValues() as $_value) { - $count++; - - $priceStr = $this->_formatPrice( - [ - 'is_percent' => $_value->getPriceType() == 'percent', - 'pricing_value' => $_value->getPrice($_value->getPriceType() == 'percent'), - ] - ); - - $htmlValue = $_value->getOptionTypeId(); - if ($arraySign) { - $checked = is_array($configValue) && in_array($htmlValue, $configValue) ? 'checked' : ''; - } else { - $checked = $configValue == $htmlValue ? 'checked' : ''; - } - - $dataSelector = 'options[' . $_option->getId() . ']'; - if ($arraySign) { - $dataSelector .= '[' . $htmlValue . ']'; - } - - $selectHtml .= '<div class="field choice admin__field admin__field-option' . - $require . - '">' . - '<input type="' . - $type . - '" class="' . - $class . - ' ' . - $require . - ' product-custom-option"' . - ($this->getSkipJsReloadPrice() ? '' : ' onclick="opConfig.reloadPrice()"') . - ' name="options[' . - $_option->getId() . - ']' . - $arraySign . - '" id="options_' . - $_option->getId() . - '_' . - $count . - '" value="' . - $htmlValue . - '" ' . - $checked . - ' data-selector="' . $dataSelector . '"' . - ' price="' . - $this->pricingHelper->currencyByStore($_value->getPrice(true), $store, false) . - '" />' . - '<label class="label admin__field-label" for="options_' . - $_option->getId() . - '_' . - $count . - '"><span>' . - $_value->getTitle() . - '</span> ' . - $priceStr . - '</label>'; - $selectHtml .= '</div>'; - } - $selectHtml .= '</div>'; - - return $selectHtml; + $optionBlock = $this->checkableFactory->create(); } + return $optionBlock + ->setOption($option) + ->setProduct($this->getProduct()) + ->setSkipJsReloadPrice(1) + ->_toHtml(); } } diff --git a/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Checkable.php b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Checkable.php new file mode 100644 index 000000000000..3d856f85dbd9 --- /dev/null +++ b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Checkable.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Block\Product\View\Options\Type\Select; + +use Magento\Catalog\Api\Data\ProductCustomOptionValuesInterface; +use Magento\Catalog\Block\Product\View\Options\AbstractOptions; +use Magento\Catalog\Model\Product\Option; + +/** + * Represent needed logic for checkbox and radio button option types + */ +class Checkable extends AbstractOptions +{ + /** + * @var string + */ + protected $_template = 'Magento_Catalog::product/composite/fieldset/options/view/checkable.phtml'; + + /** + * Returns formated price + * + * @param ProductCustomOptionValuesInterface $value + * @return string + */ + public function formatPrice(ProductCustomOptionValuesInterface $value): string + { + /** @noinspection PhpMethodParametersCountMismatchInspection */ + return parent::_formatPrice( + [ + 'is_percent' => $value->getPriceType() === 'percent', + 'pricing_value' => $value->getPrice($value->getPriceType() === 'percent') + ] + ); + } + + /** + * Returns current currency for store + * + * @param ProductCustomOptionValuesInterface $value + * @return float|string + */ + public function getCurrencyByStore(ProductCustomOptionValuesInterface $value) + { + /** @noinspection PhpMethodParametersCountMismatchInspection */ + return $this->pricingHelper->currencyByStore( + $value->getPrice(true), + $this->getProduct()->getStore(), + false + ); + } + + /** + * Returns preconfigured value for given option + * + * @param Option $option + * @return string|array|null + */ + public function getPreconfiguredValue(Option $option) + { + return $this->getProduct()->getPreconfiguredValues()->getData('options/' . $option->getId()); + } +} diff --git a/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Multiple.php b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Multiple.php new file mode 100644 index 000000000000..09a931dfa069 --- /dev/null +++ b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Multiple.php @@ -0,0 +1,112 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Block\Product\View\Options\Type\Select; + +use Magento\Catalog\Block\Product\View\Options\AbstractOptions; +use Magento\Catalog\Model\Product\Option; +use Magento\Framework\View\Element\Html\Select; + +/** + * Represent needed logic for dropdown and multi-select + */ +class Multiple extends AbstractOptions +{ + /** + * @inheritdoc + * + * @return string + * @throws \Magento\Framework\Exception\LocalizedException + */ + protected function _toHtml() + { + $option = $this->getOption(); + $optionType = $option->getType(); + $configValue = $this->getProduct()->getPreconfiguredValues()->getData('options/' . $option->getId()); + $require = $option->getIsRequire() ? ' required' : ''; + $extraParams = ''; + /** @var Select $select */ + $select = $this->getLayout()->createBlock( + Select::class + )->setData( + [ + 'id' => 'select_' . $option->getId(), + 'class' => $require . ' product-custom-option admin__control-select' + ] + ); + $select = $this->insertSelectOption($select, $option); + $select = $this->processSelectOption($select, $option); + if ($optionType === Option::OPTION_TYPE_MULTIPLE) { + $extraParams = ' multiple="multiple"'; + } + if (!$this->getSkipJsReloadPrice()) { + $extraParams .= ' onchange="opConfig.reloadPrice()"'; + } + $extraParams .= ' data-selector="' . $select->getName() . '"'; + $select->setExtraParams($extraParams); + if ($configValue) { + $select->setValue($configValue); + } + return $select->getHtml(); + } + + /** + * Returns select with inserted option give as a parameter + * + * @param Select $select + * @param Option $option + * @return Select + */ + private function insertSelectOption(Select $select, Option $option): Select + { + $require = $option->getIsRequire() ? ' required' : ''; + if ($option->getType() === Option::OPTION_TYPE_DROP_DOWN) { + $select->setName('options[' . $option->getId() . ']')->addOption('', __('-- Please Select --')); + } else { + $select->setName('options[' . $option->getId() . '][]'); + $select->setClass('multiselect admin__control-multiselect' . $require . ' product-custom-option'); + } + + return $select; + } + + /** + * Returns select with formated option prices + * + * @param Select $select + * @param Option $option + * @return Select + */ + private function processSelectOption(Select $select, Option $option): Select + { + $store = $this->getProduct()->getStore(); + foreach ($option->getValues() as $_value) { + $isPercentPriceType = $_value->getPriceType() === 'percent'; + $priceStr = $this->_formatPrice( + [ + 'is_percent' => $isPercentPriceType, + 'pricing_value' => $_value->getPrice($isPercentPriceType) + ], + false + ); + $select->addOption( + $_value->getOptionTypeId(), + $_value->getTitle() . ' ' . strip_tags($priceStr) . '', + [ + 'price' => $this->pricingHelper->currencyByStore( + $_value->getPrice(true), + $store, + false + ) + ] + ); + } + + return $select; + } +} diff --git a/app/code/Magento/Catalog/Block/Product/Widget/NewWidget.php b/app/code/Magento/Catalog/Block/Product/Widget/NewWidget.php index 704271b58f48..b4c24231a741 100644 --- a/app/code/Magento/Catalog/Block/Product/Widget/NewWidget.php +++ b/app/code/Magento/Catalog/Block/Product/Widget/NewWidget.php @@ -139,7 +139,7 @@ public function getCacheKeyInfo() [ $this->getDisplayType(), $this->getProductsPerPage(), - intval($this->getRequest()->getParam($this->getData('page_var_name'), 1)), + (int) $this->getRequest()->getParam($this->getData('page_var_name'), 1), $this->serializer->serialize($this->getRequest()->getParams()) ] ); diff --git a/app/code/Magento/Catalog/Block/Ui/ProductViewCounter.php b/app/code/Magento/Catalog/Block/Ui/ProductViewCounter.php index da35b566d7e7..dd2e23e67f3d 100644 --- a/app/code/Magento/Catalog/Block/Ui/ProductViewCounter.php +++ b/app/code/Magento/Catalog/Block/Ui/ProductViewCounter.php @@ -20,8 +20,8 @@ /** * Reports Viewed Products Counter * - * The main responsilibity of this class is provide necessary data to track viewed products - * by customer on frontend and data to synchornize this tracks with backend + * The main responsibility of this class is provide necessary data to track viewed products + * by customer on frontend and data to synchronize this tracks with backend * * @api * @since 101.1.0 @@ -109,6 +109,8 @@ public function __construct( * * @return string {JSON encoded data} * @since 101.1.0 + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function getCurrentProductData() { diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Add.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Add.php index 6456b6d578bb..733e270174e4 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Add.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Add.php @@ -6,12 +6,14 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Category; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + /** * Class Add Category * * @package Magento\Catalog\Controller\Adminhtml\Category */ -class Add extends \Magento\Catalog\Controller\Adminhtml\Category +class Add extends \Magento\Catalog\Controller\Adminhtml\Category implements HttpGetActionInterface { /** * Forward factory for result diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/CategoriesJson.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/CategoriesJson.php index b8865f2de8d1..752257f5b900 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/CategoriesJson.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/CategoriesJson.php @@ -6,7 +6,9 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Category; -class CategoriesJson extends \Magento\Catalog\Controller\Adminhtml\Category +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; + +class CategoriesJson extends \Magento\Catalog\Controller\Adminhtml\Category implements HttpPostActionInterface { /** * @var \Magento\Framework\Controller\Result\JsonFactory diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Delete.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Delete.php index 0a54475b15f9..39122d139c90 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Delete.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Delete.php @@ -6,7 +6,9 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Category; -class Delete extends \Magento\Catalog\Controller\Adminhtml\Category +use Magento\Framework\App\Action\HttpPostActionInterface; + +class Delete extends \Magento\Catalog\Controller\Adminhtml\Category implements HttpPostActionInterface { /** * @var \Magento\Catalog\Api\CategoryRepositoryInterface diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Edit.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Edit.php index 6ff478e49a30..0450ff1607a0 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Edit.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Edit.php @@ -6,7 +6,9 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Category; -class Edit extends \Magento\Catalog\Controller\Adminhtml\Category +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Edit extends \Magento\Catalog\Controller\Adminhtml\Category implements HttpGetActionInterface { /** * @var \Magento\Framework\Controller\Result\JsonFactory diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Image/Upload.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Image/Upload.php index 4cc0f2d89d17..d1efa0014d42 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Image/Upload.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Image/Upload.php @@ -5,12 +5,13 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Category\Image; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\Controller\ResultFactory; /** * Class Upload */ -class Upload extends \Magento\Backend\App\Action +class Upload extends \Magento\Backend\App\Action implements HttpPostActionInterface { /** * Image uploader @@ -54,14 +55,6 @@ public function execute() try { $result = $this->imageUploader->saveFileToTmpDir($imageId); - - $result['cookie'] = [ - 'name' => $this->_getSession()->getName(), - 'value' => $this->_getSession()->getSessionId(), - 'lifetime' => $this->_getSession()->getCookieLifetime(), - 'path' => $this->_getSession()->getCookiePath(), - 'domain' => $this->_getSession()->getCookieDomain(), - ]; } catch (\Exception $e) { $result = ['error' => $e->getMessage(), 'errorcode' => $e->getCode()]; } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Index.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Index.php index 902d71775a3d..a5be6223bee7 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Index.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Index.php @@ -6,7 +6,12 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Category; -class Index extends \Magento\Catalog\Controller\Adminhtml\Category +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +/** + * Controller for category listing + */ +class Index extends \Magento\Catalog\Controller\Adminhtml\Category implements HttpGetActionInterface { /** * @var \Magento\Backend\Model\View\Result\ForwardFactory diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Move.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Move.php index df2c80eda141..082101ff0782 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Move.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Move.php @@ -6,7 +6,12 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Category; -class Move extends \Magento\Catalog\Controller\Adminhtml\Category +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; + +/** + * Move category admin controller + */ +class Move extends \Magento\Catalog\Controller\Adminhtml\Category implements HttpPostActionInterface { /** * @var \Magento\Framework\Controller\Result\JsonFactory @@ -26,7 +31,7 @@ class Move extends \Magento\Catalog\Controller\Adminhtml\Category /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory - * @param \Magento\Framework\View\LayoutFactory $layoutFactory, + * @param \Magento\Framework\View\LayoutFactory $layoutFactory * @param \Psr\Log\LoggerInterface $logger */ public function __construct( @@ -44,7 +49,7 @@ public function __construct( /** * Move category action * - * @return \Magento\Framework\Controller\Result\Raw + * @return \Magento\Framework\Controller\Result\Json */ public function execute() { diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/RefreshPath.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/RefreshPath.php index 9384397b67f9..e3d40bee214d 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/RefreshPath.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/RefreshPath.php @@ -1,12 +1,16 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Catalog\Controller\Adminhtml\Category; -class RefreshPath extends \Magento\Catalog\Controller\Adminhtml\Category +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +/** + * Class RefreshPath + */ +class RefreshPath extends \Magento\Catalog\Controller\Adminhtml\Category implements HttpGetActionInterface { /** * @var \Magento\Framework\Controller\Result\JsonFactory @@ -42,6 +46,7 @@ public function execute() 'id' => $categoryId, 'path' => $category->getPath(), 'parentId' => $category->getParentId(), + 'level' => $category->getLevel() ]); } } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php index aa9ae88754b6..77518fd9bf5c 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php @@ -6,6 +6,7 @@ namespace Magento\Catalog\Controller\Adminhtml\Category; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Catalog\Api\Data\CategoryAttributeInterface; use Magento\Store\Model\StoreManagerInterface; @@ -14,7 +15,7 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Save extends \Magento\Catalog\Controller\Adminhtml\Category +class Save extends \Magento\Catalog\Controller\Adminhtml\Category implements HttpPostActionInterface { /** * @var \Magento\Framework\Controller\Result\RawFactory @@ -146,6 +147,7 @@ public function execute() $parentCategory = $this->getParentCategory($parentId, $storeId); $category->setPath($parentCategory->getPath()); $category->setParentId($parentCategory->getId()); + $category->setLevel(null); } /** diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Validate.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Validate.php index 4eda49068ac3..66a5fb1008a7 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Validate.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Validate.php @@ -5,10 +5,14 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Category; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; +use Magento\Catalog\Controller\Adminhtml\Category as CategoryAction; + /** * Catalog category validate */ -class Validate extends \Magento\Catalog\Controller\Adminhtml\Category +class Validate extends CategoryAction implements HttpGetActionInterface, HttpPostActionInterface { /** * @var \Magento\Framework\Controller\Result\JsonFactory diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Edit.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Edit.php index 7eb391dedf81..3cba09b1e8e9 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Edit.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Edit.php @@ -6,10 +6,18 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Ui\Component\MassAction\Filter; use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; +use Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute as AttributeAction; -class Edit extends \Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute +/** + * Form for mass updatings products' attributes. + * Can be accessed by GET since it's a form, + * can be accessed by POST since it's used as a processor of a mass-action button. + */ +class Edit extends AttributeAction implements HttpGetActionInterface, HttpPostActionInterface { /** * @var \Magento\Framework\View\Result\PageFactory diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php index 0fbf9054ef1b..342bbc388f87 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php @@ -6,84 +6,80 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute; +use Magento\AsynchronousOperations\Api\Data\OperationInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Backend\App\Action; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; /** * Class Save * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Save extends \Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute +class Save extends \Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute implements HttpPostActionInterface { /** - * @var \Magento\Catalog\Model\Indexer\Product\Flat\Processor + * @var \Magento\Framework\Bulk\BulkManagementInterface */ - protected $_productFlatIndexerProcessor; + private $bulkManagement; /** - * @var \Magento\Catalog\Model\Indexer\Product\Price\Processor + * @var \Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory */ - protected $_productPriceIndexerProcessor; + private $operationFactory; /** - * Catalog product - * - * @var \Magento\Catalog\Helper\Product + * @var \Magento\Framework\DataObject\IdentityGeneratorInterface */ - protected $_catalogProduct; + private $identityService; /** - * @var \Magento\CatalogInventory\Api\Data\StockItemInterfaceFactory + * @var \Magento\Framework\Serialize\SerializerInterface */ - protected $stockItemFactory; + private $serializer; /** - * Stock Indexer - * - * @var \Magento\CatalogInventory\Model\Indexer\Stock\Processor + * @var \Magento\Authorization\Model\UserContextInterface */ - protected $_stockIndexerProcessor; + private $userContext; /** - * @var \Magento\Framework\Api\DataObjectHelper + * @var int */ - protected $dataObjectHelper; + private $bulkSize; /** * @param Action\Context $context * @param \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeHelper - * @param \Magento\Catalog\Model\Indexer\Product\Flat\Processor $productFlatIndexerProcessor - * @param \Magento\Catalog\Model\Indexer\Product\Price\Processor $productPriceIndexerProcessor - * @param \Magento\CatalogInventory\Model\Indexer\Stock\Processor $stockIndexerProcessor - * @param \Magento\Catalog\Helper\Product $catalogProduct - * @param \Magento\CatalogInventory\Api\Data\StockItemInterfaceFactory $stockItemFactory - * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper + * @param \Magento\Framework\Bulk\BulkManagementInterface $bulkManagement + * @param \Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory $operartionFactory + * @param \Magento\Framework\DataObject\IdentityGeneratorInterface $identityService + * @param \Magento\Framework\Serialize\SerializerInterface $serializer + * @param \Magento\Authorization\Model\UserContextInterface $userContext + * @param int $bulkSize */ public function __construct( Action\Context $context, \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeHelper, - \Magento\Catalog\Model\Indexer\Product\Flat\Processor $productFlatIndexerProcessor, - \Magento\Catalog\Model\Indexer\Product\Price\Processor $productPriceIndexerProcessor, - \Magento\CatalogInventory\Model\Indexer\Stock\Processor $stockIndexerProcessor, - \Magento\Catalog\Helper\Product $catalogProduct, - \Magento\CatalogInventory\Api\Data\StockItemInterfaceFactory $stockItemFactory, - \Magento\Framework\Api\DataObjectHelper $dataObjectHelper + \Magento\Framework\Bulk\BulkManagementInterface $bulkManagement, + \Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory $operartionFactory, + \Magento\Framework\DataObject\IdentityGeneratorInterface $identityService, + \Magento\Framework\Serialize\SerializerInterface $serializer, + \Magento\Authorization\Model\UserContextInterface $userContext, + int $bulkSize = 100 ) { - $this->_productFlatIndexerProcessor = $productFlatIndexerProcessor; - $this->_productPriceIndexerProcessor = $productPriceIndexerProcessor; - $this->_stockIndexerProcessor = $stockIndexerProcessor; - $this->_catalogProduct = $catalogProduct; - $this->stockItemFactory = $stockItemFactory; parent::__construct($context, $attributeHelper); - $this->dataObjectHelper = $dataObjectHelper; + $this->bulkManagement = $bulkManagement; + $this->operationFactory = $operartionFactory; + $this->identityService = $identityService; + $this->serializer = $serializer; + $this->userContext = $userContext; + $this->bulkSize = $bulkSize; } /** * Update product attributes * - * @return \Magento\Backend\Model\View\Result\Redirect - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @return \Magento\Framework\Controller\Result\Redirect */ public function execute() { @@ -92,128 +88,184 @@ public function execute() } /* Collect Data */ - $inventoryData = $this->getRequest()->getParam('inventory', []); $attributesData = $this->getRequest()->getParam('attributes', []); $websiteRemoveData = $this->getRequest()->getParam('remove_website_ids', []); $websiteAddData = $this->getRequest()->getParam('add_website_ids', []); - /* Prepare inventory data item options (use config settings) */ - $options = $this->_objectManager->get(\Magento\CatalogInventory\Api\StockConfigurationInterface::class) - ->getConfigItemOptions(); - foreach ($options as $option) { - if (isset($inventoryData[$option]) && !isset($inventoryData['use_config_' . $option])) { - $inventoryData['use_config_' . $option] = 0; - } - } + $storeId = $this->attributeHelper->getSelectedStoreId(); + $websiteId = $this->attributeHelper->getStoreWebsiteId($storeId); + $productIds = $this->attributeHelper->getProductIds(); + + $attributesData = $this->sanitizeProductAttributes($attributesData); try { - $storeId = $this->attributeHelper->getSelectedStoreId(); - if ($attributesData) { - $dateFormat = $this->_objectManager->get(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class) - ->getDateFormat(\IntlDateFormatter::SHORT); - - foreach ($attributesData as $attributeCode => $value) { - $attribute = $this->_objectManager->get(\Magento\Eav\Model\Config::class) - ->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attributeCode); - if (!$attribute->getAttributeId()) { - unset($attributesData[$attributeCode]); - continue; - } - if ($attribute->getBackendType() == 'datetime') { - if (!empty($value)) { - $filterInput = new \Zend_Filter_LocalizedToNormalized(['date_format' => $dateFormat]); - $filterInternal = new \Zend_Filter_NormalizedToLocalized( - ['date_format' => \Magento\Framework\Stdlib\DateTime::DATE_INTERNAL_FORMAT] - ); - $value = $filterInternal->filter($filterInput->filter($value)); - } else { - $value = null; - } - $attributesData[$attributeCode] = $value; - } elseif ($attribute->getFrontendInput() == 'multiselect') { - // Check if 'Change' checkbox has been checked by admin for this attribute - $isChanged = (bool)$this->getRequest()->getPost('toggle_' . $attributeCode); - if (!$isChanged) { - unset($attributesData[$attributeCode]); - continue; - } - if (is_array($value)) { - $value = implode(',', $value); - } - $attributesData[$attributeCode] = $value; - } - } + $this->publish($attributesData, $websiteRemoveData, $websiteAddData, $storeId, $websiteId, $productIds); + $this->messageManager->addSuccessMessage(__('Message is added to queue')); + } catch (\Magento\Framework\Exception\LocalizedException $e) { + $this->messageManager->addErrorMessage($e->getMessage()); + } catch (\Exception $e) { + $this->messageManager->addExceptionMessage( + $e, + __('Something went wrong while updating the product(s) attributes.') + ); + } - $this->_objectManager->get(\Magento\Catalog\Model\Product\Action::class) - ->updateAttributes($this->attributeHelper->getProductIds(), $attributesData, $storeId); - } + return $this->resultRedirectFactory->create()->setPath('catalog/product/', ['store' => $storeId]); + } - if ($inventoryData) { - // TODO why use ObjectManager? - /** @var \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry */ - $stockRegistry = $this->_objectManager - ->create(\Magento\CatalogInventory\Api\StockRegistryInterface::class); - /** @var \Magento\CatalogInventory\Api\StockItemRepositoryInterface $stockItemRepository */ - $stockItemRepository = $this->_objectManager - ->create(\Magento\CatalogInventory\Api\StockItemRepositoryInterface::class); - foreach ($this->attributeHelper->getProductIds() as $productId) { - $stockItemDo = $stockRegistry->getStockItem( - $productId, - $this->attributeHelper->getStoreWebsiteId($storeId) - ); - if (!$stockItemDo->getProductId()) { - $inventoryData['product_id'] = $productId; - } - - $stockItemId = $stockItemDo->getId(); - $this->dataObjectHelper->populateWithArray( - $stockItemDo, - $inventoryData, - \Magento\CatalogInventory\Api\Data\StockItemInterface::class + /** + * Sanitize product attributes + * + * @param array $attributesData + * + * @return array + */ + private function sanitizeProductAttributes($attributesData) + { + $dateFormat = $this->_objectManager->get(TimezoneInterface::class)->getDateFormat(\IntlDateFormatter::SHORT); + $config = $this->_objectManager->get(\Magento\Eav\Model\Config::class); + + foreach ($attributesData as $attributeCode => $value) { + $attribute = $config->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attributeCode); + if (!$attribute->getAttributeId()) { + unset($attributesData[$attributeCode]); + continue; + } + if ($attribute->getBackendType() === 'datetime') { + if (!empty($value)) { + $filterInput = new \Zend_Filter_LocalizedToNormalized(['date_format' => $dateFormat]); + $filterInternal = new \Zend_Filter_NormalizedToLocalized( + ['date_format' => \Magento\Framework\Stdlib\DateTime::DATE_INTERNAL_FORMAT] ); - $stockItemDo->setItemId($stockItemId); - $stockItemRepository->save($stockItemDo); + $value = $filterInternal->filter($filterInput->filter($value)); + } else { + $value = null; } - $this->_stockIndexerProcessor->reindexList($this->attributeHelper->getProductIds()); - } - - if ($websiteAddData || $websiteRemoveData) { - /* @var $actionModel \Magento\Catalog\Model\Product\Action */ - $actionModel = $this->_objectManager->get(\Magento\Catalog\Model\Product\Action::class); - $productIds = $this->attributeHelper->getProductIds(); - - if ($websiteRemoveData) { - $actionModel->updateWebsites($productIds, $websiteRemoveData, 'remove'); + $attributesData[$attributeCode] = $value; + } elseif ($attribute->getFrontendInput() === 'multiselect') { + // Check if 'Change' checkbox has been checked by admin for this attribute + $isChanged = (bool)$this->getRequest()->getPost('toggle_' . $attributeCode); + if (!$isChanged) { + unset($attributesData[$attributeCode]); + continue; } - if ($websiteAddData) { - $actionModel->updateWebsites($productIds, $websiteAddData, 'add'); + if (is_array($value)) { + $value = implode(',', $value); } - - $this->_eventManager->dispatch('catalog_product_to_website_change', ['products' => $productIds]); + $attributesData[$attributeCode] = $value; } + } + return $attributesData; + } - $this->messageManager->addSuccessMessage( - __('A total of %1 record(s) were updated.', count($this->attributeHelper->getProductIds())) - ); - - $this->_productFlatIndexerProcessor->reindexList($this->attributeHelper->getProductIds()); + /** + * Schedule new bulk + * + * @param array $attributesData + * @param array $websiteRemoveData + * @param array $websiteAddData + * @param int $storeId + * @param int $websiteId + * @param array $productIds + * @throws \Magento\Framework\Exception\LocalizedException + * + * @return void + */ + private function publish( + $attributesData, + $websiteRemoveData, + $websiteAddData, + $storeId, + $websiteId, + $productIds + ):void { + $productIdsChunks = array_chunk($productIds, $this->bulkSize); + $bulkUuid = $this->identityService->generateId(); + $bulkDescription = __('Update attributes for ' . count($productIds) . ' selected products'); + $operations = []; + foreach ($productIdsChunks as $productIdsChunk) { + if ($websiteRemoveData || $websiteAddData) { + $dataToUpdate = [ + 'website_assign' => $websiteAddData, + 'website_detach' => $websiteRemoveData + ]; + $operations[] = $this->makeOperation( + 'Update website assign', + 'product_action_attribute.website.update', + $dataToUpdate, + $storeId, + $websiteId, + $productIdsChunk, + $bulkUuid + ); + } - if ($this->_catalogProduct->isDataForPriceIndexerWasChanged($attributesData) - || !empty($websiteRemoveData) - || !empty($websiteAddData) - ) { - $this->_productPriceIndexerProcessor->reindexList($this->attributeHelper->getProductIds()); + if ($attributesData) { + $operations[] = $this->makeOperation( + 'Update product attributes', + 'product_action_attribute.update', + $attributesData, + $storeId, + $websiteId, + $productIdsChunk, + $bulkUuid + ); } - } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addErrorMessage($e->getMessage()); - } catch (\Exception $e) { - $this->messageManager->addExceptionMessage( - $e, - __('Something went wrong while updating the product(s) attributes.') + } + + if (!empty($operations)) { + $result = $this->bulkManagement->scheduleBulk( + $bulkUuid, + $operations, + $bulkDescription, + $this->userContext->getUserId() ); + if (!$result) { + throw new \Magento\Framework\Exception\LocalizedException( + __('Something went wrong while processing the request.') + ); + } } + } + + /** + * Make asynchronous operation + * + * @param string $meta + * @param string $queue + * @param array $dataToUpdate + * @param int $storeId + * @param int $websiteId + * @param array $productIds + * @param int $bulkUuid + * + * @return OperationInterface + */ + private function makeOperation( + $meta, + $queue, + $dataToUpdate, + $storeId, + $websiteId, + $productIds, + $bulkUuid + ): OperationInterface { + $dataToEncode = [ + 'meta_information' => $meta, + 'product_ids' => $productIds, + 'store_id' => $storeId, + 'website_id' => $websiteId, + 'attributes' => $dataToUpdate + ]; + $data = [ + 'data' => [ + 'bulk_uuid' => $bulkUuid, + 'topic_name' => $queue, + 'serialized_data' => $this->serializer->serialize($dataToEncode), + 'status' => \Magento\Framework\Bulk\OperationInterface::STATUS_TYPE_OPEN, + ] + ]; - return $this->resultRedirectFactory->create() - ->setPath('catalog/product/', ['store' => $this->attributeHelper->getSelectedStoreId()]); + return $this->operationFactory->create($data); } } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Validate.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Validate.php index a873f08d082d..30a6629dd1c2 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Validate.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Validate.php @@ -6,7 +6,11 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute; -class Validate extends \Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; +use Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute as AttributeAction; + +class Validate extends AttributeAction implements HttpGetActionInterface, HttpPostActionInterface { /** * @var \Magento\Framework\Controller\Result\JsonFactory diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/AddAttributeToTemplate.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/AddAttributeToTemplate.php index bbef1de28e5b..09eacbbf0731 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/AddAttributeToTemplate.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/AddAttributeToTemplate.php @@ -6,8 +6,10 @@ namespace Magento\Catalog\Controller\Adminhtml\Product; +use Magento\Backend\App\Action\Context; use Magento\Catalog\Api\AttributeSetRepositoryInterface; use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Controller\Adminhtml\Product; use Magento\Eav\Api\AttributeGroupRepositoryInterface; use Magento\Eav\Api\AttributeManagementInterface; use Magento\Eav\Api\AttributeRepositoryInterface; @@ -16,8 +18,14 @@ use Magento\Eav\Api\Data\AttributeInterface; use Magento\Eav\Api\Data\AttributeSetInterface; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\Result\JsonFactory; +use Magento\Framework\DataObject; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\App\ObjectManager; use Psr\Log\LoggerInterface; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Api\ExtensionAttributesFactory; /** @@ -25,10 +33,10 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class AddAttributeToTemplate extends \Magento\Catalog\Controller\Adminhtml\Product +class AddAttributeToTemplate extends Product implements HttpPostActionInterface { /** - * @var \Magento\Framework\Controller\Result\JsonFactory + * @var JsonFactory */ protected $resultJsonFactory; @@ -75,33 +83,34 @@ class AddAttributeToTemplate extends \Magento\Catalog\Controller\Adminhtml\Produ /** * Constructor * - * @param \Magento\Backend\App\Action\Context $context + * @param Context $context * @param Builder $productBuilder - * @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory - * @param \Magento\Eav\Api\Data\AttributeGroupInterfaceFactory|null $attributeGroupFactory + * @param JsonFactory $resultJsonFactory + * @param AttributeGroupInterfaceFactory|null $attributeGroupFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * @SuppressWarnings(PHPMD.LongVariable) */ public function __construct( - \Magento\Backend\App\Action\Context $context, - \Magento\Catalog\Controller\Adminhtml\Product\Builder $productBuilder, - \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory, - \Magento\Eav\Api\Data\AttributeGroupInterfaceFactory $attributeGroupFactory = null + Context $context, + Builder $productBuilder, + JsonFactory $resultJsonFactory, + AttributeGroupInterfaceFactory $attributeGroupFactory = null ) { parent::__construct($context, $productBuilder); $this->resultJsonFactory = $resultJsonFactory; - $this->attributeGroupFactory = $attributeGroupFactory ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Eav\Api\Data\AttributeGroupInterfaceFactory::class); + $this->attributeGroupFactory = $attributeGroupFactory ?: ObjectManager::getInstance() + ->get(AttributeGroupInterfaceFactory::class); } /** * Add attribute to attribute set * - * @return \Magento\Framework\Controller\Result\Json + * @return Json */ public function execute() { $request = $this->getRequest(); - $response = new \Magento\Framework\DataObject(); + $response = new DataObject(); $response->setError(false); try { @@ -124,12 +133,12 @@ public function execute() ->getItems(); if (!$attributeGroupItems) { - throw new \Magento\Framework\Exception\NoSuchEntityException; + throw new NoSuchEntityException; } /** @var AttributeGroupInterface $attributeGroup */ $attributeGroup = reset($attributeGroupItems); - } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + } catch (NoSuchEntityException $e) { /** @var AttributeGroupInterface $attributeGroup */ $attributeGroup = $this->attributeGroupFactory->create(); } @@ -176,101 +185,114 @@ public function execute() * Adding basic filters * * @return SearchCriteriaBuilder - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ private function getBasicAttributeSearchCriteriaBuilder() { - $attributeIds = (array)$this->getRequest()->getParam('attributeIds', []); + $attributeIds = (array) $this->getRequest()->getParam('attributeIds', []); if (empty($attributeIds['selected'])) { throw new LocalizedException(__('Attributes were missing and must be specified.')); } return $this->getSearchCriteriaBuilder() - ->addFilter('attribute_set_id', new \Zend_Db_Expr('null'), 'is') ->addFilter('attribute_id', [$attributeIds['selected']], 'in'); } /** + * Get AttributeRepositoryInterface + * * @return AttributeRepositoryInterface */ private function getAttributeRepository() { if (null === $this->attributeRepository) { - $this->attributeRepository = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Eav\Api\AttributeRepositoryInterface::class); + $this->attributeRepository = ObjectManager::getInstance() + ->get(AttributeRepositoryInterface::class); } return $this->attributeRepository; } /** + * Get AttributeSetRepositoryInterface + * * @return AttributeSetRepositoryInterface */ private function getAttributeSetRepository() { if (null === $this->attributeSetRepository) { - $this->attributeSetRepository = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Catalog\Api\AttributeSetRepositoryInterface::class); + $this->attributeSetRepository = ObjectManager::getInstance() + ->get(AttributeSetRepositoryInterface::class); } return $this->attributeSetRepository; } /** + * Get AttributeGroupInterface + * * @return AttributeGroupRepositoryInterface */ private function getAttributeGroupRepository() { if (null === $this->attributeGroupRepository) { - $this->attributeGroupRepository = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Eav\Api\AttributeGroupRepositoryInterface::class); + $this->attributeGroupRepository = ObjectManager::getInstance() + ->get(AttributeGroupRepositoryInterface::class); } return $this->attributeGroupRepository; } /** + * Get SearchCriteriaBuilder + * * @return SearchCriteriaBuilder */ private function getSearchCriteriaBuilder() { if (null === $this->searchCriteriaBuilder) { - $this->searchCriteriaBuilder = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Framework\Api\SearchCriteriaBuilder::class); + $this->searchCriteriaBuilder = ObjectManager::getInstance() + ->get(SearchCriteriaBuilder::class); } return $this->searchCriteriaBuilder; } /** + * Get AttributeManagementInterface + * * @return AttributeManagementInterface */ private function getAttributeManagement() { if (null === $this->attributeManagement) { - $this->attributeManagement = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Eav\Api\AttributeManagementInterface::class); + $this->attributeManagement = ObjectManager::getInstance() + ->get(AttributeManagementInterface::class); } return $this->attributeManagement; } /** + * Get LoggerInterface + * * @return LoggerInterface */ private function getLogger() { if (null === $this->logger) { - $this->logger = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Psr\Log\LoggerInterface::class); + $this->logger = ObjectManager::getInstance() + ->get(LoggerInterface::class); } return $this->logger; } /** + * Get ExtensionAttributesFactory. + * * @return ExtensionAttributesFactory */ private function getExtensionAttributesFactory() { if (null === $this->extensionAttributesFactory) { - $this->extensionAttributesFactory = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Framework\Api\ExtensionAttributesFactory::class); + $this->extensionAttributesFactory = ObjectManager::getInstance() + ->get(ExtensionAttributesFactory::class); } return $this->extensionAttributesFactory; } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Delete.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Delete.php index bef6aee0e2af..faa9e4ddf49b 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Delete.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Delete.php @@ -6,7 +6,9 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product\Attribute; -class Delete extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute +use Magento\Framework\App\Action\HttpPostActionInterface; + +class Delete extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute implements HttpPostActionInterface { /** * @return \Magento\Backend\Model\View\Result\Redirect diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Edit.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Edit.php index a99cbdbade18..a41cd71aea46 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Edit.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Edit.php @@ -6,7 +6,9 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product\Attribute; -class Edit extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Edit extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute implements HttpGetActionInterface { /** * @return \Magento\Framework\Controller\ResultInterface diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Index.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Index.php index 9bdc54b289c2..34267121f9b8 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Index.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Index.php @@ -6,7 +6,9 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product\Attribute; -class Index extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute implements HttpGetActionInterface { /** * @return \Magento\Backend\Model\View\Result\Page diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/NewAction.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/NewAction.php index e954d9730591..fdfde7e80609 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/NewAction.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/NewAction.php @@ -6,7 +6,9 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product\Attribute; -class NewAction extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class NewAction extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute implements HttpGetActionInterface { /** * @var \Magento\Backend\Model\View\Result\ForwardFactory diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php index 817de6828e48..853cc6527030 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php @@ -7,12 +7,14 @@ namespace Magento\Catalog\Controller\Adminhtml\Product\Attribute; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Backend\App\Action\Context; use Magento\Backend\Model\View\Result\Redirect; use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Catalog\Controller\Adminhtml\Product\Attribute; use Magento\Catalog\Helper\Product; use Magento\Catalog\Model\Product\Attribute\Frontend\Inputtype\Presentation; +use Magento\Framework\Serialize\Serializer\FormData; use Magento\Catalog\Model\Product\AttributeSet\BuildFactory; use Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory; use Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype\Validator; @@ -31,9 +33,11 @@ use Magento\Framework\View\Result\PageFactory; /** + * Product attribute save controller. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Save extends Attribute +class Save extends Attribute implements HttpPostActionInterface { /** * @var BuildFactory @@ -75,6 +79,11 @@ class Save extends Attribute */ private $presentation; + /** + * @var FormData|null + */ + private $formDataSerializer; + /** * @param Context $context * @param FrontendInterface $attributeLabelCache @@ -88,6 +97,7 @@ class Save extends Attribute * @param Product $productHelper * @param LayoutFactory $layoutFactory * @param Presentation|null $presentation + * @param FormData|null $formDataSerializer * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -102,7 +112,8 @@ public function __construct( FilterManager $filterManager, Product $productHelper, LayoutFactory $layoutFactory, - Presentation $presentation = null + Presentation $presentation = null, + FormData $formDataSerializer = null ) { parent::__construct($context, $attributeLabelCache, $coreRegistry, $resultPageFactory); $this->buildFactory = $buildFactory; @@ -113,19 +124,38 @@ public function __construct( $this->groupCollectionFactory = $groupCollectionFactory; $this->layoutFactory = $layoutFactory; $this->presentation = $presentation ?: ObjectManager::getInstance()->get(Presentation::class); + $this->formDataSerializer = $formDataSerializer + ?: ObjectManager::getInstance()->get(FormData::class); } /** + * @inheritdoc + * * @return Redirect * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @throws \Zend_Validate_Exception */ public function execute() { + try { + $optionData = $this->formDataSerializer + ->unserialize($this->getRequest()->getParam('serialized_options', '[]')); + } catch (\InvalidArgumentException $e) { + $message = __("The attribute couldn't be saved due to an error. Verify your information and try again. " + . "If the error persists, please try again later."); + $this->messageManager->addErrorMessage($message); + return $this->returnResult('catalog/*/edit', ['_current' => true], ['error' => true]); + } + $data = $this->getRequest()->getPostValue(); + $data = array_replace_recursive( + $data, + $optionData + ); + if ($data) { - $this->preprocessOptionsData($data); $setId = $this->getRequest()->getParam('set'); $attributeSet = null; @@ -134,7 +164,7 @@ public function execute() $name = trim($name); try { - /** @var $attributeSet Set */ + /** @var Set $attributeSet */ $attributeSet = $this->buildFactory->create() ->setEntityTypeId($this->_entityTypeId) ->setSkeletonId($setId) @@ -156,7 +186,7 @@ public function execute() $attributeId = $this->getRequest()->getParam('attribute_id'); - /** @var $model ProductAttributeInterface */ + /** @var ProductAttributeInterface $model */ $model = $this->attributeFactory->create(); if ($attributeId) { $model->load($attributeId); @@ -165,30 +195,11 @@ public function execute() ? $model->getAttributeCode() : $this->getRequest()->getParam('attribute_code'); $attributeCode = $attributeCode ?: $this->generateCode($this->getRequest()->getParam('frontend_label')[0]); - if (strlen($attributeCode) > 0) { - $validatorAttrCode = new \Zend_Validate_Regex( - ['pattern' => '/^[a-z\x{600}-\x{6FF}][a-z\x{600}-\x{6FF}_0-9]{0,30}$/u'] - ); - if (!$validatorAttrCode->isValid($attributeCode)) { - $this->messageManager->addErrorMessage( - __( - 'Attribute code "%1" is invalid. Please use only letters (a-z), ' . - 'numbers (0-9) or underscore(_) in this field, first character should be a letter.', - $attributeCode - ) - ); - return $this->returnResult( - 'catalog/*/edit', - ['attribute_id' => $attributeId, '_current' => true], - ['error' => true] - ); - } - } $data['attribute_code'] = $attributeCode; //validate frontend_input if (isset($data['frontend_input'])) { - /** @var $inputType Validator */ + /** @var Validator $inputType */ $inputType = $this->validatorFactory->create(); if (!$inputType->isValid($data['frontend_input'])) { foreach ($inputType->getMessages() as $message) { @@ -229,14 +240,14 @@ public function execute() $data['backend_model'] = $this->productHelper->getAttributeBackendModelByInputType( $data['frontend_input'] ); + + if ($model->getIsUserDefined() === null) { + $data['backend_type'] = $model->getBackendTypeByInput($data['frontend_input']); + } } $data += ['is_filterable' => 0, 'is_filterable_in_search' => 0]; - if ($model->getIsUserDefined() === null || $model->getIsUserDefined() != 0) { - $data['backend_type'] = $model->getBackendTypeByInput($data['frontend_input']); - } - $defaultValueField = $model->getDefaultValueByInput($data['frontend_input']); if ($defaultValueField) { $data['default_value'] = $this->getRequest()->getParam($defaultValueField); @@ -316,28 +327,8 @@ public function execute() } /** - * Extract options data from serialized options field and append to data array. - * - * This logic is required to overcome max_input_vars php limit - * that may vary and/or be inaccessible to change on different instances. + * Provides an initialized Result object. * - * @param array $data - * @return void - */ - private function preprocessOptionsData(&$data) - { - if (isset($data['serialized_options'])) { - $serializedOptions = json_decode($data['serialized_options'], JSON_OBJECT_AS_ARRAY); - foreach ($serializedOptions as $serializedOption) { - $option = []; - parse_str($serializedOption, $option); - $data = array_replace_recursive($data, $option); - } - } - unset($data['serialized_options']); - } - - /** * @param string $path * @param array $params * @param array $response 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 db452113ada0..c74a382724a0 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php @@ -7,9 +7,20 @@ namespace Magento\Catalog\Controller\Adminhtml\Product\Attribute; +use Magento\Catalog\Controller\Adminhtml\Product\Attribute as AttributeAction; +use Magento\Eav\Model\Validator\Attribute\Code as AttributeCodeValidator; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject; +use Magento\Framework\Serialize\Serializer\FormData; -class Validate extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute +/** + * Product attribute validate controller. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class Validate extends AttributeAction implements HttpGetActionInterface, HttpPostActionInterface { const DEFAULT_MESSAGE_KEY = 'message'; @@ -28,6 +39,16 @@ class Validate extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute */ private $multipleAttributeList; + /** + * @var FormData|null + */ + private $formDataSerializer; + + /** + * @var AttributeCodeValidator + */ + private $attributeCodeValidator; + /** * Constructor * @@ -38,6 +59,8 @@ class Validate extends \Magento\Catalog\Controller\Adminhtml\Product\Attribute * @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory * @param \Magento\Framework\View\LayoutFactory $layoutFactory * @param array $multipleAttributeList + * @param FormData|null $formDataSerializer + * @param AttributeCodeValidator|null $attributeCodeValidator */ public function __construct( \Magento\Backend\App\Action\Context $context, @@ -46,15 +69,24 @@ public function __construct( \Magento\Framework\View\Result\PageFactory $resultPageFactory, \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory, \Magento\Framework\View\LayoutFactory $layoutFactory, - array $multipleAttributeList = [] + array $multipleAttributeList = [], + FormData $formDataSerializer = null, + AttributeCodeValidator $attributeCodeValidator = null ) { parent::__construct($context, $attributeLabelCache, $coreRegistry, $resultPageFactory); $this->resultJsonFactory = $resultJsonFactory; $this->layoutFactory = $layoutFactory; $this->multipleAttributeList = $multipleAttributeList; + $this->formDataSerializer = $formDataSerializer ?: ObjectManager::getInstance() + ->get(FormData::class); + $this->attributeCodeValidator = $attributeCodeValidator ?: ObjectManager::getInstance()->get( + AttributeCodeValidator::class + ); } /** + * @inheritdoc + * * @return \Magento\Framework\Controller\ResultInterface * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -63,6 +95,15 @@ public function execute() { $response = new DataObject(); $response->setError(false); + try { + $optionsData = $this->formDataSerializer + ->unserialize($this->getRequest()->getParam('serialized_options', '[]')); + } catch (\InvalidArgumentException $e) { + $message = __("The attribute couldn't be validated due to an error. Verify your information and try again. " + . "If the error persists, please try again later."); + $this->setMessageToResponse($response, [$message]); + $response->setError(true); + } $attributeCode = $this->getRequest()->getParam('attribute_code'); $frontendLabel = $this->getRequest()->getParam('frontend_label'); @@ -75,7 +116,7 @@ public function execute() $attributeCode ); - if ($attribute->getId() && !$attributeId) { + 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); @@ -85,6 +126,12 @@ public function execute() $response->setError(true); $response->setProductAttribute($attribute->toArray()); } + + if (!$this->attributeCodeValidator->isValid($attributeCode)) { + $this->setMessageToResponse($response, $this->attributeCodeValidator->getMessages()); + $response->setError(true); + } + if ($this->getRequest()->has('new_attribute_set_name')) { $setName = $this->getRequest()->getParam('new_attribute_set_name'); /** @var $attributeSet \Magento\Eav\Model\Entity\Attribute\Set */ @@ -102,10 +149,10 @@ public function execute() } $multipleOption = $this->getRequest()->getParam("frontend_input"); - $multipleOption = null == $multipleOption ? 'select' : $multipleOption; + $multipleOption = (null === $multipleOption) ? 'select' : $multipleOption; - if (isset($this->multipleAttributeList[$multipleOption]) && !(null == ($multipleOption))) { - $options = $this->getRequest()->getParam($this->multipleAttributeList[$multipleOption]); + if (isset($this->multipleAttributeList[$multipleOption])) { + $options = $optionsData[$this->multipleAttributeList[$multipleOption]] ?? null; $this->checkUniqueOption( $response, $options @@ -123,7 +170,8 @@ public function execute() } /** - * Throws Exception if not unique values into options + * Throws Exception if not unique values into options. + * * @param array $optionsValues * @param array $deletedOptions * @return bool @@ -132,7 +180,7 @@ private function isUniqueAdminValues(array $optionsValues, array $deletedOptions { $adminValues = []; foreach ($optionsValues as $optionKey => $values) { - if (!(isset($deletedOptions[$optionKey]) and $deletedOptions[$optionKey] === '1')) { + if (!(isset($deletedOptions[$optionKey]) && $deletedOptions[$optionKey] === '1')) { $adminValues[] = reset($values); } } @@ -157,6 +205,8 @@ private function setMessageToResponse($response, $messages) } /** + * Performs checking the uniqueness of the attribute options. + * * @param DataObject $response * @param array|null $options * @return $this diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Builder.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Builder.php index 125406061aed..78ad9f423871 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/Crosssell.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Crosssell.php index fa94a2fb7be2..e51d3ffe94ae 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Crosssell.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Crosssell.php @@ -6,7 +6,17 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product; -class Crosssell extends \Magento\Catalog\Controller\Adminhtml\Product +use Magento\Catalog\Controller\Adminhtml\Product; +use Magento\Framework\App\Action\HttpPostActionInterface; + +/** + * Class Crosssell + * + * @package Magento\Catalog\Controller\Adminhtml\Product + * @deprecated Not used since cross-sell products grid moved to UI components. + * @see Magento_Catalog::view/adminhtml/ui_component/crosssell_product_listing.xml + */ +class Crosssell extends Product implements HttpPostActionInterface { /** * @var \Magento\Framework\View\Result\LayoutFactory diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/CrosssellGrid.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/CrosssellGrid.php index daaf2c21ee2b..5039d0c052b5 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/CrosssellGrid.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/CrosssellGrid.php @@ -6,7 +6,17 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product; -class CrosssellGrid extends \Magento\Catalog\Controller\Adminhtml\Product +use Magento\Catalog\Controller\Adminhtml\Product; +use Magento\Framework\App\Action\HttpPostActionInterface; + +/** + * Class CrosssellGrid + * + * @package Magento\Catalog\Controller\Adminhtml\Product + * @deprecated Not used since cross-sell products grid moved to UI components. + * @see Magento_Catalog::view/adminhtml/ui_component/crosssell_product_listing.xml + */ +class CrosssellGrid extends Product implements HttpPostActionInterface { /** * @var \Magento\Framework\View\Result\LayoutFactory diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Edit.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Edit.php index 1b9316a95ad5..c31ceabcda65 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Edit.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Edit.php @@ -6,7 +6,9 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product; -class Edit extends \Magento\Catalog\Controller\Adminhtml\Product +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Edit extends \Magento\Catalog\Controller\Adminhtml\Product implements HttpGetActionInterface { /** * Array of actions which can be processed without secret key validation diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Gallery/Upload.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Gallery/Upload.php index b5660ea87934..ff7311e93175 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Gallery/Upload.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Gallery/Upload.php @@ -6,9 +6,10 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product\Gallery; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\App\Filesystem\DirectoryList; -class Upload extends \Magento\Backend\App\Action +class Upload extends \Magento\Backend\App\Action implements HttpPostActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/GridOnly.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/GridOnly.php index 40e62895caff..51aaa8c178ed 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/GridOnly.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/GridOnly.php @@ -1,12 +1,16 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Catalog\Controller\Adminhtml\Product; -class GridOnly extends \Magento\Catalog\Controller\Adminhtml\Product +use Magento\Framework\App\Action\HttpGetActionInterface; + +/** + * Get specified tab grid controller. + */ +class GridOnly extends \Magento\Catalog\Controller\Adminhtml\Product implements HttpGetActionInterface { /** * @var \Magento\Framework\Controller\Result\RawFactory @@ -47,7 +51,7 @@ public function execute() $this->productBuilder->build($this->getRequest()); $block = $this->getRequest()->getParam('gridOnlyBlock'); - $blockClassSuffix = str_replace(' ', '_', ucwords(str_replace('_', ' ', $block))); + $blockClassSuffix = ucwords($block, '_'); /** @var \Magento\Framework\Controller\Result\Raw $resultRaw */ $resultRaw = $this->resultRawFactory->create(); diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Index.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Index.php index ea66ecf6b162..7755a512eb9b 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Index.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Index.php @@ -6,7 +6,9 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product; -class Index extends \Magento\Catalog\Controller\Adminhtml\Product +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\Catalog\Controller\Adminhtml\Product implements HttpGetActionInterface { /** * @var \Magento\Framework\View\Result\PageFactory diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php index d82f4a04fb25..f11d16755ef0 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php @@ -19,6 +19,8 @@ use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper\AttributeFilter; /** + * Product helper + * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 @@ -365,6 +367,8 @@ private function overwriteValue($optionId, $option, $overwriteOptions) } /** + * Get link resolver instance + * * @return LinkResolver * @deprecated 101.0.0 */ @@ -377,6 +381,8 @@ private function getLinkResolver() } /** + * Get DateTimeFilter instance + * * @return \Magento\Framework\Stdlib\DateTime\Filter\DateTime * @deprecated 101.0.0 */ @@ -391,6 +397,7 @@ private function getDateTimeFilter() /** * Remove ids of non selected websites from $websiteIds array and return filtered data + * * $websiteIds parameter expects array with website ids as keys and 1 (selected) or 0 (non selected) as values * Only one id (default website ID) will be set to $websiteIds array when the single store mode is turned on * @@ -463,6 +470,7 @@ private function fillProductOptions(Product $product, array $productOptions) private function convertSpecialFromDateStringToObject($productData) { if (isset($productData['special_from_date']) && $productData['special_from_date'] != '') { + $productData['special_from_date'] = $this->getDateTimeFilter()->filter($productData['special_from_date']); $productData['special_from_date'] = new \DateTime($productData['special_from_date']); } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassDelete.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassDelete.php index f32c6edd5739..8fceba3c45e2 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassDelete.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassDelete.php @@ -6,13 +6,14 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\Controller\ResultFactory; use Magento\Backend\App\Action\Context; use Magento\Ui\Component\MassAction\Filter; use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; use Magento\Catalog\Api\ProductRepositoryInterface; -class MassDelete extends \Magento\Catalog\Controller\Adminhtml\Product +class MassDelete extends \Magento\Catalog\Controller\Adminhtml\Product implements HttpPostActionInterface { /** * Massactions filter diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassStatus.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassStatus.php index e3623aabfa1a..9d7273fb3f23 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassStatus.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/MassStatus.php @@ -6,6 +6,7 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Backend\App\Action; use Magento\Catalog\Controller\Adminhtml\Product; use Magento\Framework\Controller\ResultFactory; @@ -13,9 +14,10 @@ use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; /** + * Updates status for a batch of products. * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class MassStatus extends \Magento\Catalog\Controller\Adminhtml\Product +class MassStatus extends \Magento\Catalog\Controller\Adminhtml\Product implements HttpPostActionInterface { /** * @var \Magento\Catalog\Model\Indexer\Product\Price\Processor @@ -86,7 +88,7 @@ public function execute() $filterRequest = $this->getRequest()->getParam('filters', null); $status = (int) $this->getRequest()->getParam('status'); - if (null !== $storeId && null !== $filterRequest) { + if (null === $storeId && null !== $filterRequest) { $storeId = (isset($filterRequest['store_id'])) ? (int) $filterRequest['store_id'] : 0; } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/NewAction.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/NewAction.php index 0b027105cd7d..0b1ef98c386c 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/NewAction.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/NewAction.php @@ -6,11 +6,12 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Backend\App\Action; use Magento\Catalog\Controller\Adminhtml\Product; use Magento\Framework\App\ObjectManager; -class NewAction extends \Magento\Catalog\Controller\Adminhtml\Product +class NewAction extends \Magento\Catalog\Controller\Adminhtml\Product implements HttpGetActionInterface { /** * @var Initialization\StockDataFilter diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Related.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Related.php index 4bc15d14a29b..f54f8d469c3e 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Related.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Related.php @@ -7,7 +7,17 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product; -class Related extends \Magento\Catalog\Controller\Adminhtml\Product +use Magento\Catalog\Controller\Adminhtml\Product; +use Magento\Framework\App\Action\HttpPostActionInterface; + +/** + * Class Related + * + * @package Magento\Catalog\Controller\Adminhtml\Product + * @deprecated Not used since related products grid moved to UI components. + * @see Magento_Catalog::view/adminhtml/ui_component/related_product_listing.xml + */ +class Related extends Product implements HttpPostActionInterface { /** * @var \Magento\Framework\View\Result\LayoutFactory @@ -29,6 +39,8 @@ public function __construct( } /** + * Execute + * * @return \Magento\Framework\View\Result\Layout */ public function execute() diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/RelatedGrid.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/RelatedGrid.php index b2fc7ebe1eb3..b1092bba0d36 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/RelatedGrid.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/RelatedGrid.php @@ -7,6 +7,15 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product; -class RelatedGrid extends Related +use Magento\Framework\App\Action\HttpPostActionInterface; + +/** + * Class RelatedGrid + * + * @package Magento\Catalog\Controller\Adminhtml\Product + * @deprecated Not used since related products grid moved to UI components. + * @see Magento_Catalog::view/adminhtml/ui_component/related_product_listing.xml + */ +class RelatedGrid extends Related implements HttpPostActionInterface { } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Reload.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Reload.php index ff87e7f57413..a0963e60d888 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Reload.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Reload.php @@ -5,12 +5,13 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\Controller\ResultFactory; /** * Backend reload of product create/edit form */ -class Reload extends \Magento\Catalog\Controller\Adminhtml\Product +class Reload extends \Magento\Catalog\Controller\Adminhtml\Product implements HttpPostActionInterface { /** * {@inheritdoc} diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php index ff3ce60d9278..825d0ee032d6 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php @@ -7,7 +7,9 @@ namespace Magento\Catalog\Controller\Adminhtml\Product; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Backend\App\Action; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Controller\Adminhtml\Product; use Magento\Store\Model\StoreManagerInterface; use Magento\Framework\App\Request\DataPersistorInterface; @@ -16,7 +18,7 @@ * Class Save * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Save extends \Magento\Catalog\Controller\Adminhtml\Product +class Save extends \Magento\Catalog\Controller\Adminhtml\Product implements HttpPostActionInterface { /** * @var Initialization\Helper @@ -53,6 +55,16 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Product */ private $storeManager; + /** + * @var \Magento\Framework\Escaper|null + */ + private $escaper; + + /** + * @var null|\Psr\Log\LoggerInterface + */ + private $logger; + /** * Save constructor. * @@ -62,6 +74,8 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Product * @param \Magento\Catalog\Model\Product\Copier $productCopier * @param \Magento\Catalog\Model\Product\TypeTransitionManager $productTypeManager * @param \Magento\Catalog\Api\ProductRepositoryInterface $productRepository + * @param \Magento\Framework\Escaper|null $escaper + * @param \Psr\Log\LoggerInterface|null $logger */ public function __construct( \Magento\Backend\App\Action\Context $context, @@ -69,13 +83,17 @@ public function __construct( Initialization\Helper $initializationHelper, \Magento\Catalog\Model\Product\Copier $productCopier, \Magento\Catalog\Model\Product\TypeTransitionManager $productTypeManager, - \Magento\Catalog\Api\ProductRepositoryInterface $productRepository + \Magento\Catalog\Api\ProductRepositoryInterface $productRepository, + \Magento\Framework\Escaper $escaper = null, + \Psr\Log\LoggerInterface $logger = null ) { $this->initializationHelper = $initializationHelper; $this->productCopier = $productCopier; $this->productTypeManager = $productTypeManager; $this->productRepository = $productRepository; parent::__construct($context, $productBuilder); + $this->escaper = $escaper ?? $this->_objectManager->get(\Magento\Framework\Escaper::class); + $this->logger = $logger ?? $this->_objectManager->get(\Psr\Log\LoggerInterface::class); } /** @@ -102,7 +120,6 @@ public function execute() $this->productBuilder->build($this->getRequest()) ); $this->productTypeManager->processProduct($product); - if (isset($data['product'][$product->getIdFieldName()])) { throw new \Magento\Framework\Exception\LocalizedException( __('The product was unable to be saved. Please try again.') @@ -110,6 +127,7 @@ public function execute() } $originalSku = $product->getSku(); + $canSaveCustomOptions = $product->getCanSaveCustomOptions(); $product->save(); $this->handleImageRemoveError($data, $product->getId()); $this->getCategoryLinkManagement()->assignProductToCategories( @@ -119,21 +137,17 @@ public function execute() $productId = $product->getEntityId(); $productAttributeSetId = $product->getAttributeSetId(); $productTypeId = $product->getTypeId(); - - $this->copyToStores($data, $productId); - + $extendedData = $data; + $extendedData['can_save_custom_options'] = $canSaveCustomOptions; + $this->copyToStores($extendedData, $productId); $this->messageManager->addSuccessMessage(__('You saved the product.')); $this->getDataPersistor()->clear('catalog_product'); if ($product->getSku() != $originalSku) { $this->messageManager->addNoticeMessage( __( 'SKU for product %1 has been changed to %2.', - $this->_objectManager->get( - \Magento\Framework\Escaper::class - )->escapeHtml($product->getName()), - $this->_objectManager->get( - \Magento\Framework\Escaper::class - )->escapeHtml($product->getSku()) + $this->escaper->escapeHtml($product->getName()), + $this->escaper->escapeHtml($product->getSku()) ) ); } @@ -143,17 +157,21 @@ public function execute() ); if ($redirectBack === 'duplicate') { + $product->unsetData('quantity_and_stock_status'); $newProduct = $this->productCopier->copy($product); + $this->checkUniqueAttributes($product); $this->messageManager->addSuccessMessage(__('You duplicated the product.')); } } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); + $this->logger->critical($e); $this->messageManager->addExceptionMessage($e); + $data = isset($product) ? $this->persistMediaData($product, $data) : $data; $this->getDataPersistor()->set('catalog_product', $data); $redirectBack = $productId ? true : 'new'; } catch (\Exception $e) { - $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); + $this->logger->critical($e); $this->messageManager->addErrorMessage($e->getMessage()); + $data = isset($product) ? $this->persistMediaData($product, $data) : $data; $this->getDataPersistor()->set('catalog_product', $data); $redirectBack = $productId ? true : 'new'; } @@ -186,6 +204,7 @@ public function execute() /** * Notify customer when image was not deleted in specific case. + * * TODO: temporary workaround must be eliminated in MAGETWO-45306 * * @param array $postData @@ -239,6 +258,7 @@ protected function copyToStores($data, $productId) ->setStoreId($copyFrom) ->load($productId) ->setStoreId($copyTo) + ->setCanSaveCustomOptions($data['can_save_custom_options']) ->setCopyFromView(true) ->save(); } @@ -250,6 +270,8 @@ protected function copyToStores($data, $productId) } /** + * Get categoryLinkManagement in a backward compatible way. + * * @return \Magento\Catalog\Api\CategoryLinkManagementInterface */ private function getCategoryLinkManagement() @@ -262,6 +284,8 @@ private function getCategoryLinkManagement() } /** + * Get storeManager in a backward compatible way. + * * @return StoreManagerInterface * @deprecated 101.0.0 */ @@ -288,4 +312,57 @@ protected function getDataPersistor() return $this->dataPersistor; } + + /** + * Persist media gallery on error, in order to show already saved images on next run. + * + * @param ProductInterface $product + * @param array $data + * @return array + */ + private function persistMediaData(ProductInterface $product, array $data) + { + $mediaGallery = $product->getData('media_gallery'); + if (!empty($mediaGallery['images'])) { + foreach ($mediaGallery['images'] as $key => $image) { + if (!isset($image['new_file'])) { + //Remove duplicates. + unset($mediaGallery['images'][$key]); + } + } + $data['product']['media_gallery'] = $mediaGallery; + $fields = [ + 'image', + 'small_image', + 'thumbnail', + 'swatch_image', + ]; + foreach ($fields as $field) { + $data['product'][$field] = $product->getData($field); + } + } + + return $data; + } + + /** + * Check unique attributes and add error to message manager + * + * @param \Magento\Catalog\Model\Product $product + */ + private function checkUniqueAttributes(\Magento\Catalog\Model\Product $product) + { + $uniqueLabels = []; + foreach ($product->getAttributes() as $attribute) { + if ($attribute->getIsUnique() && $attribute->getIsUserDefined() + && $product->getData($attribute->getAttributeCode()) !== null + ) { + $uniqueLabels[] = $attribute->getDefaultFrontendLabel(); + } + } + if ($uniqueLabels) { + $uniqueLabels = implode('", "', $uniqueLabels); + $this->messageManager->addErrorMessage(__('The value of attribute(s) "%1" must be unique', $uniqueLabels)); + } + } } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Search.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Search.php index c7c71b2f5602..316983298a1b 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Search.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Search.php @@ -9,11 +9,12 @@ namespace Magento\Catalog\Controller\Adminhtml\Product; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\App\Action\HttpGetActionInterface; /** * Controller to search product for ui-select component */ -class Search extends \Magento\Backend\App\Action +class Search extends \Magento\Backend\App\Action implements HttpGetActionInterface { /** * Authorization level of a basic admin session @@ -48,6 +49,8 @@ public function __construct( } /** + * Execute product search. + * * @return \Magento\Framework\Controller\ResultInterface */ public function execute() : \Magento\Framework\Controller\ResultInterface diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Add.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Add.php index 480c30322a07..bfe474abba1b 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Add.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Add.php @@ -6,7 +6,9 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product\Set; -class Add extends \Magento\Catalog\Controller\Adminhtml\Product\Set +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Add extends \Magento\Catalog\Controller\Adminhtml\Product\Set implements HttpGetActionInterface { /** * @var \Magento\Framework\View\Result\PageFactory diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Delete.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Delete.php index f2695311732f..771cc83f79e8 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Delete.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Delete.php @@ -6,7 +6,9 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product\Set; -class Delete extends \Magento\Catalog\Controller\Adminhtml\Product\Set +use Magento\Framework\App\Action\HttpPostActionInterface; + +class Delete extends \Magento\Catalog\Controller\Adminhtml\Product\Set implements HttpPostActionInterface { /** * @var \Magento\Eav\Api\AttributeSetRepositoryInterface diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Edit.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Edit.php index ec540180b034..6f6870cb0849 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Edit.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Edit.php @@ -6,7 +6,9 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product\Set; -class Edit extends \Magento\Catalog\Controller\Adminhtml\Product\Set +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Edit extends \Magento\Catalog\Controller\Adminhtml\Product\Set implements HttpGetActionInterface { /** * @var \Magento\Framework\View\Result\PageFactory diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Index.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Index.php index 29f7dff4f0d4..aadf724f6006 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Index.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Index.php @@ -6,7 +6,9 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product\Set; -class Index extends \Magento\Catalog\Controller\Adminhtml\Product\Set +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\Catalog\Controller\Adminhtml\Product\Set implements HttpGetActionInterface { /** * @var \Magento\Framework\View\Result\PageFactory diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Save.php index c5dd9ce6d8e7..83620de25b01 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Set/Save.php @@ -6,12 +6,13 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product\Set; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\App\ObjectManager; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Save extends \Magento\Catalog\Controller\Adminhtml\Product\Set +class Save extends \Magento\Catalog\Controller\Adminhtml\Product\Set implements HttpPostActionInterface { /** * @var \Magento\Framework\View\LayoutFactory diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Upsell.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Upsell.php index 614bddd0ebc8..1cec8e867879 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Upsell.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Upsell.php @@ -6,7 +6,17 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product; -class Upsell extends \Magento\Catalog\Controller\Adminhtml\Product +use Magento\Catalog\Controller\Adminhtml\Product; +use Magento\Framework\App\Action\HttpPostActionInterface; + +/** + * Class Upsell + * + * @package Magento\Catalog\Controller\Adminhtml\Product + * @deprecated Not used since upsell products grid moved to UI components. + * @see Magento_Catalog::view/adminhtml/ui_component/upsell_product_listing.xml + */ +class Upsell extends Product implements HttpPostActionInterface { /** * @var \Magento\Framework\View\Result\LayoutFactory diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/UpsellGrid.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/UpsellGrid.php index 50beb588b15d..581531e7c93f 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/UpsellGrid.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/UpsellGrid.php @@ -6,7 +6,17 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product; -class UpsellGrid extends \Magento\Catalog\Controller\Adminhtml\Product +use Magento\Catalog\Controller\Adminhtml\Product; +use Magento\Framework\App\Action\HttpPostActionInterface; + +/** + * Class UpsellGrid + * + * @package Magento\Catalog\Controller\Adminhtml\Product + * @deprecated Not used since upsell products grid moved to UI components. + * @see Magento_Catalog::view/adminhtml/ui_component/upsell_product_listing.xml + */ +class UpsellGrid extends Product implements HttpPostActionInterface { /** * @var \Magento\Framework\View\Result\LayoutFactory diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Validate.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Validate.php index e131bfe38c54..77c9cfcd40f0 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Validate.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Validate.php @@ -6,6 +6,8 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Backend\App\Action; use Magento\Catalog\Controller\Adminhtml\Product; use Magento\Framework\App\ObjectManager; @@ -16,7 +18,7 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Validate extends \Magento\Catalog\Controller\Adminhtml\Product +class Validate extends Product implements HttpPostActionInterface, HttpGetActionInterface { /** * @var \Magento\Framework\Stdlib\DateTime\Filter\Date diff --git a/app/code/Magento/Catalog/Controller/Category/View.php b/app/code/Magento/Catalog/Controller/Category/View.php index 226e57250507..da3d99a8d274 100644 --- a/app/code/Magento/Catalog/Controller/Category/View.php +++ b/app/code/Magento/Catalog/Controller/Category/View.php @@ -7,53 +7,73 @@ namespace Magento\Catalog\Controller\Category; use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\Design; use Magento\Catalog\Model\Layer\Resolver; +use Magento\Catalog\Model\Product\ProductList\ToolbarMemorizer; +use Magento\Catalog\Model\Session; +use Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator; +use Magento\Framework\App\Action\Action; +use Magento\Framework\App\Action\Context; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\App\ActionInterface; +use Magento\Framework\Controller\Result\ForwardFactory; +use Magento\Framework\Controller\ResultInterface; +use Magento\Framework\DataObject; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; +use Magento\Framework\View\Result\Page; use Magento\Framework\View\Result\PageFactory; +use Magento\Store\Model\StoreManagerInterface; +use Psr\Log\LoggerInterface; /** + * View a category on storefront. Needs to be accessible by POST because of the store switching. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class View extends \Magento\Framework\App\Action\Action +class View extends Action implements HttpGetActionInterface, HttpPostActionInterface { /** * Core registry * - * @var \Magento\Framework\Registry + * @var Registry */ protected $_coreRegistry = null; /** * Catalog session * - * @var \Magento\Catalog\Model\Session + * @var Session */ protected $_catalogSession; /** * Catalog design * - * @var \Magento\Catalog\Model\Design + * @var Design */ protected $_catalogDesign; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $_storeManager; /** - * @var \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator + * @var CategoryUrlPathGenerator */ protected $categoryUrlPathGenerator; /** - * @var \Magento\Framework\View\Result\PageFactory + * @var PageFactory */ protected $resultPageFactory; /** - * @var \Magento\Framework\Controller\Result\ForwardFactory + * @var ForwardFactory */ protected $resultForwardFactory; @@ -69,32 +89,39 @@ class View extends \Magento\Framework\App\Action\Action */ protected $categoryRepository; + /** + * @var ToolbarMemorizer + */ + private $toolbarMemorizer; + /** * Constructor * - * @param \Magento\Framework\App\Action\Context $context - * @param \Magento\Catalog\Model\Design $catalogDesign - * @param \Magento\Catalog\Model\Session $catalogSession - * @param \Magento\Framework\Registry $coreRegistry - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator $categoryUrlPathGenerator - * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory - * @param \Magento\Framework\Controller\Result\ForwardFactory $resultForwardFactory + * @param Context $context + * @param Design $catalogDesign + * @param Session $catalogSession + * @param Registry $coreRegistry + * @param StoreManagerInterface $storeManager + * @param CategoryUrlPathGenerator $categoryUrlPathGenerator + * @param PageFactory $resultPageFactory + * @param ForwardFactory $resultForwardFactory * @param Resolver $layerResolver * @param CategoryRepositoryInterface $categoryRepository + * @param ToolbarMemorizer|null $toolbarMemorizer * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Framework\App\Action\Context $context, - \Magento\Catalog\Model\Design $catalogDesign, - \Magento\Catalog\Model\Session $catalogSession, - \Magento\Framework\Registry $coreRegistry, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator $categoryUrlPathGenerator, + Context $context, + Design $catalogDesign, + Session $catalogSession, + Registry $coreRegistry, + StoreManagerInterface $storeManager, + CategoryUrlPathGenerator $categoryUrlPathGenerator, PageFactory $resultPageFactory, - \Magento\Framework\Controller\Result\ForwardFactory $resultForwardFactory, + ForwardFactory $resultForwardFactory, Resolver $layerResolver, - CategoryRepositoryInterface $categoryRepository + CategoryRepositoryInterface $categoryRepository, + ToolbarMemorizer $toolbarMemorizer = null ) { parent::__construct($context); $this->_storeManager = $storeManager; @@ -106,12 +133,13 @@ public function __construct( $this->resultForwardFactory = $resultForwardFactory; $this->layerResolver = $layerResolver; $this->categoryRepository = $categoryRepository; + $this->toolbarMemorizer = $toolbarMemorizer ?: $context->getObjectManager()->get(ToolbarMemorizer::class); } /** * Initialize requested category object * - * @return \Magento\Catalog\Model\Category|bool + * @return Category|bool */ protected function _initCategory() { @@ -130,13 +158,14 @@ protected function _initCategory() } $this->_catalogSession->setLastVisitedCategoryId($category->getId()); $this->_coreRegistry->register('current_category', $category); + $this->toolbarMemorizer->memorizeParams(); try { $this->_eventManager->dispatch( 'catalog_controller_category_init_after', ['category' => $category, 'controller_action' => $this] ); - } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); + } catch (LocalizedException $e) { + $this->_objectManager->get(LoggerInterface::class)->critical($e); return false; } @@ -146,13 +175,12 @@ protected function _initCategory() /** * Category view action * - * @return \Magento\Framework\Controller\ResultInterface - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) + * @return ResultInterface + * @throws NoSuchEntityException */ public function execute() { - if ($this->_request->getParam(\Magento\Framework\App\ActionInterface::PARAM_NAME_URL_ENCODED)) { + if ($this->_request->getParam(ActionInterface::PARAM_NAME_URL_ENCODED)) { return $this->resultRedirectFactory->create()->setUrl($this->_redirect->getRedirectUrl()); } $category = $this->_initCategory(); @@ -173,29 +201,18 @@ public function execute() $page->getConfig()->setPageLayout($settings->getPageLayout()); } - $hasChildren = $category->hasChildren(); - if ($category->getIsAnchor()) { - $type = $hasChildren ? 'layered' : 'layered_without_children'; - } else { - $type = $hasChildren ? 'default' : 'default_without_children'; - } + $pageType = $this->getPageType($category); - if (!$hasChildren) { + if (!$category->hasChildren()) { // Two levels removed from parent. Need to add default page type. - $parentType = strtok($type, '_'); - $page->addPageLayoutHandles(['type' => $parentType], null, false); + $parentPageType = strtok($pageType, '_'); + $page->addPageLayoutHandles(['type' => $parentPageType], null, false); } - $page->addPageLayoutHandles(['type' => $type], null, false); + $page->addPageLayoutHandles(['type' => $pageType], null, false); $page->addPageLayoutHandles(['id' => $category->getId()]); // apply custom layout update once layout is loaded - $layoutUpdates = $settings->getLayoutUpdates(); - if ($layoutUpdates && is_array($layoutUpdates)) { - foreach ($layoutUpdates as $layoutUpdate) { - $page->addUpdate($layoutUpdate); - $page->addPageLayoutHandles(['layout_update' => md5($layoutUpdate)], null, false); - } - } + $this->applyLayoutUpdates($page, $settings); $page->getConfig()->addBodyClass('page-products') ->addBodyClass('categorypath-' . $this->categoryUrlPathGenerator->getUrlPath($category)) @@ -206,4 +223,40 @@ public function execute() return $this->resultForwardFactory->create()->forward('noroute'); } } + + /** + * Get page type based on category + * + * @param Category $category + * @return string + */ + private function getPageType(Category $category) : string + { + $hasChildren = $category->hasChildren(); + if ($category->getIsAnchor()) { + return $hasChildren ? 'layered' : 'layered_without_children'; + } + + return $hasChildren ? 'default' : 'default_without_children'; + } + + /** + * Apply custom layout updates + * + * @param Page $page + * @param DataObject $settings + * @return void + */ + private function applyLayoutUpdates( + Page $page, + DataObject $settings + ) { + $layoutUpdates = $settings->getLayoutUpdates(); + if ($layoutUpdates && is_array($layoutUpdates)) { + foreach ($layoutUpdates as $layoutUpdate) { + $page->addUpdate($layoutUpdate); + $page->addPageLayoutHandles(['layout_update' => sha1($layoutUpdate)], null, false); + } + } + } } diff --git a/app/code/Magento/Catalog/Controller/Index/Index.php b/app/code/Magento/Catalog/Controller/Index/Index.php index eae3325df9fc..bd00c9720499 100644 --- a/app/code/Magento/Catalog/Controller/Index/Index.php +++ b/app/code/Magento/Catalog/Controller/Index/Index.php @@ -5,12 +5,17 @@ */ namespace Magento\Catalog\Controller\Index; -class Index extends \Magento\Framework\App\Action\Action +use Magento\Framework\App\Action\HttpGetActionInterface; + +/** + * Catalog index page controller. + */ +class Index extends \Magento\Framework\App\Action\Action implements HttpGetActionInterface { /** * Index action * - * @return $this + * @return \Magento\Framework\Controller\Result\Redirect */ public function execute() { diff --git a/app/code/Magento/Catalog/Controller/Product/Compare.php b/app/code/Magento/Catalog/Controller/Product/Compare.php index 1ee146e5aaa7..084a82f87d64 100644 --- a/app/code/Magento/Catalog/Controller/Product/Compare.php +++ b/app/code/Magento/Catalog/Controller/Product/Compare.php @@ -6,6 +6,7 @@ namespace Magento\Catalog\Controller\Product; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Framework\Data\Form\FormKey\Validator; use Magento\Framework\View\Result\PageFactory; @@ -15,7 +16,7 @@ * @SuppressWarnings(PHPMD.LongVariable) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -abstract class Compare extends \Magento\Framework\App\Action\Action +abstract class Compare extends \Magento\Framework\App\Action\Action implements HttpGetActionInterface { /** * Customer id @@ -139,4 +140,15 @@ public function setCustomerId($customerId) $this->_customerId = $customerId; return $this; } + + /** + * @inheritdoc + */ + public function execute() + { + $resultRedirect = $this->resultRedirectFactory->create(); + $resultRedirect->setPath('catalog/product_compare'); + + return $resultRedirect; + } } diff --git a/app/code/Magento/Catalog/Controller/Product/Compare/Add.php b/app/code/Magento/Catalog/Controller/Product/Compare/Add.php index eb9cc8312554..d99901c915a1 100644 --- a/app/code/Magento/Catalog/Controller/Product/Compare/Add.php +++ b/app/code/Magento/Catalog/Controller/Product/Compare/Add.php @@ -6,9 +6,10 @@ */ namespace Magento\Catalog\Controller\Product\Compare; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\Exception\NoSuchEntityException; -class Add extends \Magento\Catalog\Controller\Product\Compare +class Add extends \Magento\Catalog\Controller\Product\Compare implements HttpPostActionInterface { /** * Add item to compare list diff --git a/app/code/Magento/Catalog/Controller/Product/Compare/Clear.php b/app/code/Magento/Catalog/Controller/Product/Compare/Clear.php index 568fbf1d0567..2703e9869bd4 100644 --- a/app/code/Magento/Catalog/Controller/Product/Compare/Clear.php +++ b/app/code/Magento/Catalog/Controller/Product/Compare/Clear.php @@ -6,9 +6,10 @@ */ namespace Magento\Catalog\Controller\Product\Compare; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\Controller\ResultFactory; -class Clear extends \Magento\Catalog\Controller\Product\Compare +class Clear extends \Magento\Catalog\Controller\Product\Compare implements HttpPostActionInterface { /** * Remove all items from comparison list diff --git a/app/code/Magento/Catalog/Controller/Product/Compare/Index.php b/app/code/Magento/Catalog/Controller/Product/Compare/Index.php index 3eba058318a7..c0aa32a56ed1 100644 --- a/app/code/Magento/Catalog/Controller/Product/Compare/Index.php +++ b/app/code/Magento/Catalog/Controller/Product/Compare/Index.php @@ -6,6 +6,7 @@ */ namespace Magento\Catalog\Controller\Product\Compare; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Framework\Data\Form\FormKey\Validator; use Magento\Framework\View\Result\PageFactory; @@ -13,7 +14,7 @@ /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Index extends \Magento\Catalog\Controller\Product\Compare +class Index extends \Magento\Catalog\Controller\Product\Compare implements HttpGetActionInterface { /** * @var \Magento\Framework\Url\DecoderInterface diff --git a/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php b/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php index 2acbe5ce4d58..eac0ddf94af2 100644 --- a/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php +++ b/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php @@ -6,9 +6,10 @@ */ namespace Magento\Catalog\Controller\Product\Compare; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\Exception\NoSuchEntityException; -class Remove extends \Magento\Catalog\Controller\Product\Compare +class Remove extends \Magento\Catalog\Controller\Product\Compare implements HttpPostActionInterface { /** * Remove item from compare list diff --git a/app/code/Magento/Catalog/Controller/Product/View.php b/app/code/Magento/Catalog/Controller/Product/View.php index ed437361fddd..024123e15150 100644 --- a/app/code/Magento/Catalog/Controller/Product/View.php +++ b/app/code/Magento/Catalog/Controller/Product/View.php @@ -6,10 +6,16 @@ */ namespace Magento\Catalog\Controller\Product; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Framework\App\Action\Context; use Magento\Framework\View\Result\PageFactory; +use Magento\Catalog\Controller\Product as ProductAction; -class View extends \Magento\Catalog\Controller\Product +/** + * View a product on storefront. Needs to be accessible by POST because of the store switching. + */ +class View extends ProductAction implements HttpGetActionInterface, HttpPostActionInterface { /** * @var \Magento\Catalog\Helper\Product\View diff --git a/app/code/Magento/Catalog/Cron/DeleteOutdatedPriceValues.php b/app/code/Magento/Catalog/Cron/DeleteOutdatedPriceValues.php index 7cc3eb9e3d2d..25f6d0c32368 100644 --- a/app/code/Magento/Catalog/Cron/DeleteOutdatedPriceValues.php +++ b/app/code/Magento/Catalog/Cron/DeleteOutdatedPriceValues.php @@ -12,6 +12,8 @@ use Magento\Store\Model\Store; /** + * Cron job for removing outdated prices. + * * Cron operation is responsible for deleting all product prices on WEBSITE level * in case 'Catalog Price Scope' configuration parameter is set to GLOBAL. */ @@ -76,7 +78,7 @@ public function execute() /** * Checks if price scope config option explicitly equal to global value. * - * Such strict comparision is required to prevent price deleting when + * Such strict comparison is required to prevent price deleting when * price scope config option is null for some reason. * * @return bool diff --git a/app/code/Magento/Catalog/Cron/FrontendActionsFlush.php b/app/code/Magento/Catalog/Cron/FrontendActionsFlush.php index 6e7699abb477..99e9898eab3c 100644 --- a/app/code/Magento/Catalog/Cron/FrontendActionsFlush.php +++ b/app/code/Magento/Catalog/Cron/FrontendActionsFlush.php @@ -57,8 +57,7 @@ private function getLifeTimeByNamespace($namespace) ]; } - return isset($configuration['lifetime']) ? - (int) $configuration['lifetime'] : FrontendStorageConfigurationInterface::DEFAULT_LIFETIME; + return (int)$configuration['lifetime'] ?? FrontendStorageConfigurationInterface::DEFAULT_LIFETIME; } /** diff --git a/app/code/Magento/Catalog/Helper/Data.php b/app/code/Magento/Catalog/Helper/Data.php index ae20cda46079..3e9676363283 100644 --- a/app/code/Magento/Catalog/Helper/Data.php +++ b/app/code/Magento/Catalog/Helper/Data.php @@ -7,6 +7,7 @@ use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Store\Model\ScopeInterface; use Magento\Customer\Model\Session as CustomerSession; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Pricing\PriceCurrencyInterface; @@ -273,7 +274,8 @@ public function setStoreId($store) /** * Return current category path or get it from current category - * and creating array of categories|product paths for breadcrumbs + * + * Creating array of categories|product paths for breadcrumbs * * @return array */ @@ -382,6 +384,7 @@ public function getLastViewedUrl() /** * Split SKU of an item by dashes and spaces + * * Words will not be broken, unless this length is greater than $length * * @param string $sku @@ -410,14 +413,15 @@ public function getAttributeHiddenFields() /** * Retrieve Catalog Price Scope * - * @return int + * @return int|null */ - public function getPriceScope() + public function getPriceScope(): ?int { - return $this->scopeConfig->getValue( + $priceScope = $this->scopeConfig->getValue( self::XML_PATH_PRICE_SCOPE, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE ); + return isset($priceScope) ? (int)$priceScope : null; } /** @@ -439,7 +443,7 @@ public function isUsingStaticUrlsAllowed() { return $this->scopeConfig->isSetFlag( self::CONFIG_USE_STATIC_URLS, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE ); } @@ -454,7 +458,7 @@ public function isUrlDirectivesParsingAllowed() { return $this->scopeConfig->isSetFlag( self::CONFIG_PARSE_URL_DIRECTIVES, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $this->_storeId ); } @@ -472,6 +476,7 @@ public function getPageTemplateProcessor() /** * Whether to display items count for each filter option + * * @param int $storeId Store view ID * @return bool */ @@ -479,12 +484,14 @@ public function shouldDisplayProductCountOnLayer($storeId = null) { return $this->scopeConfig->isSetFlag( self::XML_PATH_DISPLAY_PRODUCT_COUNT, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $storeId ); } /** + * Convert tax address array to address data object with country id and postcode + * * @param array $taxAddress * @return \Magento\Customer\Api\Data\AddressInterface|null */ diff --git a/app/code/Magento/Catalog/Helper/Image.php b/app/code/Magento/Catalog/Helper/Image.php index 4f128d639b2b..9b8d0ad75a8c 100644 --- a/app/code/Magento/Catalog/Helper/Image.php +++ b/app/code/Magento/Catalog/Helper/Image.php @@ -6,6 +6,7 @@ namespace Magento\Catalog\Helper; use Magento\Framework\App\Helper\AbstractHelper; +use Magento\Framework\View\Element\Block\ArgumentInterface; /** * Catalog image helper @@ -14,7 +15,7 @@ * @SuppressWarnings(PHPMD.TooManyFields) * @since 100.0.2 */ -class Image extends AbstractHelper +class Image extends AbstractHelper implements ArgumentInterface { /** * Media config node @@ -298,6 +299,7 @@ public function resize($width, $height = null) * * @param int $quality * @return $this + * @deprecated */ public function setQuality($quality) { @@ -406,7 +408,8 @@ public function rotate($angle) /** * Add watermark to image - * size param in format 100x200 + * + * Size param in format 100x200 * * @param string $fileName * @param string $position @@ -533,6 +536,8 @@ public function getUrl() } /** + * Save changes + * * @return $this */ public function save() @@ -553,6 +558,8 @@ public function getResizedImageInfo() } /** + * Getter for placeholder url + * * @param null|string $placeholder * @return string */ @@ -655,7 +662,8 @@ protected function getWatermarkPosition() /** * Set watermark size - * param size in format 100x200 + * + * Param size in format 100x200 * * @param string $size * @return $this @@ -757,7 +765,7 @@ protected function getImageFile() protected function parseSize($string) { $size = explode('x', strtolower($string)); - if (sizeof($size) == 2) { + if (count($size) == 2) { return ['width' => $size[0] > 0 ? $size[0] : null, 'height' => $size[1] > 0 ? $size[1] : null]; } return false; @@ -859,7 +867,7 @@ public function getFrame() */ protected function getAttribute($name) { - return isset($this->attributes[$name]) ? $this->attributes[$name] : null; + return $this->attributes[$name] ?? null; } /** diff --git a/app/code/Magento/Catalog/Helper/Output.php b/app/code/Magento/Catalog/Helper/Output.php index ad0f9508cb67..33e261dc353b 100644 --- a/app/code/Magento/Catalog/Helper/Output.php +++ b/app/code/Magento/Catalog/Helper/Output.php @@ -116,7 +116,7 @@ public function addHandler($method, $handler) public function getHandlers($method) { $method = strtolower($method); - return isset($this->_handlers[$method]) ? $this->_handlers[$method] : []; + return $this->_handlers[$method] ?? []; } /** diff --git a/app/code/Magento/Catalog/Helper/Product/Compare.php b/app/code/Magento/Catalog/Helper/Product/Compare.php index 90d98874e00c..d6d35c5c76dd 100644 --- a/app/code/Magento/Catalog/Helper/Product/Compare.php +++ b/app/code/Magento/Catalog/Helper/Product/Compare.php @@ -166,7 +166,15 @@ public function getListUrl() */ public function getPostDataParams($product) { - return $this->postHelper->getPostData($this->getAddUrl(), ['product' => $product->getId()]); + $params = ['product' => $product->getId()]; + $requestingPageUrl = $this->_getRequest()->getParam('requesting_page_url'); + + if (!empty($requestingPageUrl)) { + $encodedUrl = $this->urlEncoder->encode($requestingPageUrl); + $params[\Magento\Framework\App\ActionInterface::PARAM_NAME_URL_ENCODED] = $encodedUrl; + } + + return $this->postHelper->getPostData($this->getAddUrl(), $params); } /** diff --git a/app/code/Magento/Catalog/Helper/Product/Configuration.php b/app/code/Magento/Catalog/Helper/Product/Configuration.php index 9b47e2990099..5b8f6fad6e18 100644 --- a/app/code/Magento/Catalog/Helper/Product/Configuration.php +++ b/app/code/Magento/Catalog/Helper/Product/Configuration.php @@ -55,6 +55,7 @@ class Configuration extends AbstractHelper implements ConfigurationInterface * @param \Magento\Framework\Filter\FilterManager $filter * @param \Magento\Framework\Stdlib\StringUtils $string * @param Json $serializer + * @param Escaper $escaper */ public function __construct( \Magento\Framework\App\Helper\Context $context, diff --git a/app/code/Magento/Catalog/Helper/Product/ProductList.php b/app/code/Magento/Catalog/Helper/Product/ProductList.php index fbea73a6324d..3aa6aeed3779 100644 --- a/app/code/Magento/Catalog/Helper/Product/ProductList.php +++ b/app/code/Magento/Catalog/Helper/Product/ProductList.php @@ -42,6 +42,7 @@ class ProductList /** * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + * @param \Magento\Framework\Registry $coreRegistry */ public function __construct( \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, diff --git a/app/code/Magento/Catalog/Helper/Product/View.php b/app/code/Magento/Catalog/Helper/Product/View.php index 5753910c125d..74f40a18971d 100644 --- a/app/code/Magento/Catalog/Helper/Product/View.php +++ b/app/code/Magento/Catalog/Helper/Product/View.php @@ -10,7 +10,9 @@ /** * Catalog category helper + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class View extends \Magento\Framework\App\Helper\AbstractHelper { @@ -105,19 +107,16 @@ public function __construct( * * @param \Magento\Framework\View\Result\Page $resultPage * @param \Magento\Catalog\Model\Product $product - * @return \Magento\Framework\View\Result\Page + * @return $this */ private function preparePageMetadata(ResultPage $resultPage, $product) { $pageLayout = $resultPage->getLayout(); $pageConfig = $resultPage->getConfig(); - $title = $product->getMetaTitle(); - if ($title) { - $pageConfig->getTitle()->set($title); - } else { - $pageConfig->getTitle()->set($product->getName()); - } + $metaTitle = $product->getMetaTitle(); + $pageConfig->setMetaTitle($metaTitle); + $pageConfig->getTitle()->set($metaTitle ?: $product->getName()); $keyword = $product->getMetaKeyword(); $currentCategory = $this->_coreRegistry->registry('current_category'); diff --git a/app/code/Magento/Catalog/Model/AbstractModel.php b/app/code/Magento/Catalog/Model/AbstractModel.php index 007635b12433..78a49cd1e8b1 100644 --- a/app/code/Magento/Catalog/Model/AbstractModel.php +++ b/app/code/Magento/Catalog/Model/AbstractModel.php @@ -179,7 +179,7 @@ public function isLockedAttribute($attributeCode) * * @param string|array $key * @param mixed $value - * @return \Magento\Framework\DataObject + * @return $this */ public function setData($key, $value = null) { @@ -282,9 +282,9 @@ public function getWebsiteStoreIds() * * Default value existing is flag for using store value in data * - * @param string $attributeCode - * @param mixed $value - * @return $this + * @param string $attributeCode + * @param mixed $value + * @return $this * * @deprecated 101.0.0 */ @@ -332,11 +332,10 @@ public function getAttributeDefaultValue($attributeCode) } /** - * Set attribute code flag if attribute has value in current store and does not use - * value of default store as value + * Set attribute code flag if attribute has value in current store and does not use value of default store as value * - * @param string $attributeCode - * @return $this + * @param string $attributeCode + * @return $this * * @deprecated 101.0.0 */ diff --git a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/EavAttributeCondition.php b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/EavAttributeCondition.php index d3c84e69c954..e296c8d3b897 100644 --- a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/EavAttributeCondition.php +++ b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/EavAttributeCondition.php @@ -58,22 +58,38 @@ public function build(Filter $filter): string $conditionValue = $this->mapConditionValue($conditionType, $filter->getValue()); // NOTE: store scope was ignored intentionally to perform search across all stores - $attributeSelect = $this->resourceConnection->getConnection() - ->select() - ->from( - [$tableAlias => $attribute->getBackendTable()], - $tableAlias . '.' . $attribute->getEntityIdField() - )->where( - $this->resourceConnection->getConnection()->prepareSqlCondition( - $tableAlias . '.' . $attribute->getIdFieldName(), - ['eq' => $attribute->getAttributeId()] - ) - )->where( - $this->resourceConnection->getConnection()->prepareSqlCondition( - $tableAlias . '.value', - [$conditionType => $conditionValue] - ) - ); + if ($conditionType == 'is_null') { + $entityResourceModel = $attribute->getEntity(); + $attributeSelect = $this->resourceConnection->getConnection() + ->select() + ->from( + [Collection::MAIN_TABLE_ALIAS => $entityResourceModel->getEntityTable()], + Collection::MAIN_TABLE_ALIAS . '.' . $entityResourceModel->getEntityIdField() + )->joinLeft( + [$tableAlias => $attribute->getBackendTable()], + $tableAlias . '.' . $attribute->getEntityIdField() . '=' . Collection::MAIN_TABLE_ALIAS . + '.' . $entityResourceModel->getEntityIdField() . ' AND ' . $tableAlias . '.' . + $attribute->getIdFieldName() . '=' . $attribute->getAttributeId(), + '' + )->where($tableAlias . '.value is null'); + } else { + $attributeSelect = $this->resourceConnection->getConnection() + ->select() + ->from( + [$tableAlias => $attribute->getBackendTable()], + $tableAlias . '.' . $attribute->getEntityIdField() + )->where( + $this->resourceConnection->getConnection()->prepareSqlCondition( + $tableAlias . '.' . $attribute->getIdFieldName(), + ['eq' => $attribute->getAttributeId()] + ) + )->where( + $this->resourceConnection->getConnection()->prepareSqlCondition( + $tableAlias . '.value', + [$conditionType => $conditionValue] + ) + ); + } return $this->resourceConnection ->getConnection() @@ -86,6 +102,8 @@ public function build(Filter $filter): string } /** + * Get attribute entity by its code + * * @param string $field * @return Attribute * @throws \Magento\Framework\Exception\LocalizedException diff --git a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/NativeAttributeCondition.php b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/NativeAttributeCondition.php index d072acf4c719..71b9a9c47037 100644 --- a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/NativeAttributeCondition.php +++ b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ConditionBuilder/NativeAttributeCondition.php @@ -77,7 +77,7 @@ private function mapConditionType(string $conditionType, string $field): string ]; } - return isset($conditionsMap[$conditionType]) ? $conditionsMap[$conditionType] : $conditionType; + return $conditionsMap[$conditionType] ?? $conditionType; } /** diff --git a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ProductCategoryCondition.php b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ProductCategoryCondition.php index f70bab73d083..66a9132ae44b 100644 --- a/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ProductCategoryCondition.php +++ b/app/code/Magento/Catalog/Model/Api/SearchCriteria/CollectionProcessor/ConditionProcessor/ProductCategoryCondition.php @@ -38,6 +38,7 @@ class ProductCategoryCondition implements CustomConditionInterface /** * @param \Magento\Framework\App\ResourceConnection $resourceConnection + * @param \Magento\Catalog\Model\CategoryRepository $categoryRepository */ public function __construct( \Magento\Framework\App\ResourceConnection $resourceConnection, @@ -104,7 +105,7 @@ private function getCategoryIds(Filter $filter): array } } - return array_unique(array_merge($categoryIds, ...$childCategoryIds)); + return array_map('intval', array_unique(array_merge($categoryIds, ...$childCategoryIds))); } /** diff --git a/app/code/Magento/Catalog/Model/Attribute/Backend/Consumer.php b/app/code/Magento/Catalog/Model/Attribute/Backend/Consumer.php new file mode 100644 index 000000000000..dc24a3090481 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Attribute/Backend/Consumer.php @@ -0,0 +1,163 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Attribute\Backend; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\EntityManager\EntityManager; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Exception\TemporaryStateExceptionInterface; +use Magento\Framework\Bulk\OperationInterface; + +/** + * Consumer for export message. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class Consumer +{ + /** + * @var \Psr\Log\LoggerInterface + */ + private $logger; + + /** + * @var \Magento\Catalog\Model\Indexer\Product\Flat\Processor + */ + private $productFlatIndexerProcessor; + + /** + * @var \Magento\Catalog\Model\Indexer\Product\Price\Processor + */ + private $productPriceIndexerProcessor; + + /** + * @var \Magento\Catalog\Helper\Product + */ + private $catalogProduct; + + /** + * @var \Magento\Catalog\Model\Product\Action + */ + private $productAction; + + /** + * @var \Magento\Framework\Serialize\SerializerInterface + */ + private $serializer; + + /** + * @var \Magento\Framework\Bulk\OperationManagementInterface + */ + private $operationManagement; + /** + * @var EntityManager + */ + private $entityManager; + + /** + * @param \Magento\Catalog\Helper\Product $catalogProduct + * @param \Magento\Catalog\Model\Indexer\Product\Flat\Processor $productFlatIndexerProcessor + * @param \Magento\Catalog\Model\Indexer\Product\Price\Processor $productPriceIndexerProcessor + * @param \Magento\Framework\Bulk\OperationManagementInterface $operationManagement + * @param \Magento\Catalog\Model\Product\Action $action + * @param \Psr\Log\LoggerInterface $logger + * @param \Magento\Framework\Serialize\SerializerInterface $serializer + * @param EntityManager $entityManager + */ + public function __construct( + \Magento\Catalog\Helper\Product $catalogProduct, + \Magento\Catalog\Model\Indexer\Product\Flat\Processor $productFlatIndexerProcessor, + \Magento\Catalog\Model\Indexer\Product\Price\Processor $productPriceIndexerProcessor, + \Magento\Framework\Bulk\OperationManagementInterface $operationManagement, + \Magento\Catalog\Model\Product\Action $action, + \Psr\Log\LoggerInterface $logger, + \Magento\Framework\Serialize\SerializerInterface $serializer, + EntityManager $entityManager + ) { + $this->catalogProduct = $catalogProduct; + $this->productFlatIndexerProcessor = $productFlatIndexerProcessor; + $this->productPriceIndexerProcessor = $productPriceIndexerProcessor; + $this->productAction = $action; + $this->logger = $logger; + $this->serializer = $serializer; + $this->operationManagement = $operationManagement; + $this->entityManager = $entityManager; + } + + /** + * Process + * + * @param \Magento\AsynchronousOperations\Api\Data\OperationInterface $operation + * @throws \Exception + * + * @return void + */ + public function process(\Magento\AsynchronousOperations\Api\Data\OperationInterface $operation) + { + try { + $serializedData = $operation->getSerializedData(); + $data = $this->serializer->unserialize($serializedData); + $this->execute($data); + } catch (\Zend_Db_Adapter_Exception $e) { + $this->logger->critical($e->getMessage()); + if ($e instanceof \Magento\Framework\DB\Adapter\LockWaitException + || $e instanceof \Magento\Framework\DB\Adapter\DeadlockException + || $e instanceof \Magento\Framework\DB\Adapter\ConnectionException + ) { + $status = OperationInterface::STATUS_TYPE_RETRIABLY_FAILED; + $errorCode = $e->getCode(); + $message = $e->getMessage(); + } else { + $status = OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; + $errorCode = $e->getCode(); + $message = __( + 'Sorry, something went wrong during product attributes update. Please see log for details.' + ); + } + } catch (NoSuchEntityException $e) { + $this->logger->critical($e->getMessage()); + $status = ($e instanceof TemporaryStateExceptionInterface) + ? OperationInterface::STATUS_TYPE_RETRIABLY_FAILED + : OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; + $errorCode = $e->getCode(); + $message = $e->getMessage(); + } catch (LocalizedException $e) { + $this->logger->critical($e->getMessage()); + $status = OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; + $errorCode = $e->getCode(); + $message = $e->getMessage(); + } catch (\Exception $e) { + $this->logger->critical($e->getMessage()); + $status = OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; + $errorCode = $e->getCode(); + $message = __('Sorry, something went wrong during product attributes update. Please see log for details.'); + } + + $operation->setStatus($status ?? OperationInterface::STATUS_TYPE_COMPLETE) + ->setErrorCode($errorCode ?? null) + ->setResultMessage($message ?? null); + + $this->entityManager->save($operation); + } + + /** + * Execute + * + * @param array $data + * + * @return void + */ + private function execute($data): void + { + $this->productAction->updateAttributes($data['product_ids'], $data['attributes'], $data['store_id']); + if ($this->catalogProduct->isDataForPriceIndexerWasChanged($data['attributes'])) { + $this->productPriceIndexerProcessor->reindexList($data['product_ids']); + } + + $this->productFlatIndexerProcessor->reindexList($data['product_ids']); + } +} diff --git a/app/code/Magento/Catalog/Model/Attribute/Backend/ConsumerWebsiteAssign.php b/app/code/Magento/Catalog/Model/Attribute/Backend/ConsumerWebsiteAssign.php new file mode 100644 index 000000000000..32ba39d9afd9 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Attribute/Backend/ConsumerWebsiteAssign.php @@ -0,0 +1,168 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Attribute\Backend; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Exception\TemporaryStateExceptionInterface; +use Magento\Framework\Bulk\OperationInterface; +use Magento\Framework\EntityManager\EntityManager; + +/** + * Consumer for export message. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ConsumerWebsiteAssign +{ + /** + * @var \Psr\Log\LoggerInterface + */ + private $logger; + + /** + * @var \Magento\Catalog\Model\Indexer\Product\Flat\Processor + */ + private $productFlatIndexerProcessor; + + /** + * @var \Magento\Catalog\Model\Product\Action + */ + private $productAction; + + /** + * @var \Magento\Framework\Serialize\SerializerInterface + */ + private $serializer; + + /** + * @var \Magento\Catalog\Model\Indexer\Product\Price\Processor + */ + private $productPriceIndexerProcessor; + + /** + * @var EntityManager + */ + private $entityManager; + + /** + * @param \Magento\Catalog\Model\Indexer\Product\Flat\Processor $productFlatIndexerProcessor + * @param \Magento\Catalog\Model\Indexer\Product\Price\Processor $productPriceIndexerProcessor + * @param \Magento\Catalog\Model\Product\Action $action + * @param \Psr\Log\LoggerInterface $logger + * @param \Magento\Framework\Serialize\SerializerInterface $serializer + * @param EntityManager $entityManager + */ + public function __construct( + \Magento\Catalog\Model\Indexer\Product\Flat\Processor $productFlatIndexerProcessor, + \Magento\Catalog\Model\Indexer\Product\Price\Processor $productPriceIndexerProcessor, + \Magento\Catalog\Model\Product\Action $action, + \Psr\Log\LoggerInterface $logger, + \Magento\Framework\Serialize\SerializerInterface $serializer, + EntityManager $entityManager + ) { + $this->productFlatIndexerProcessor = $productFlatIndexerProcessor; + $this->productAction = $action; + $this->logger = $logger; + $this->serializer = $serializer; + $this->productPriceIndexerProcessor = $productPriceIndexerProcessor; + $this->entityManager = $entityManager; + } + + /** + * Process + * + * @param \Magento\AsynchronousOperations\Api\Data\OperationInterface $operation + * @throws \Exception + * + * @return void + */ + public function process(\Magento\AsynchronousOperations\Api\Data\OperationInterface $operation) + { + try { + $serializedData = $operation->getSerializedData(); + $data = $this->serializer->unserialize($serializedData); + $this->execute($data); + } catch (\Zend_Db_Adapter_Exception $e) { + $this->logger->critical($e->getMessage()); + if ($e instanceof \Magento\Framework\DB\Adapter\LockWaitException + || $e instanceof \Magento\Framework\DB\Adapter\DeadlockException + || $e instanceof \Magento\Framework\DB\Adapter\ConnectionException + ) { + $status = OperationInterface::STATUS_TYPE_RETRIABLY_FAILED; + $errorCode = $e->getCode(); + $message = __($e->getMessage()); + } else { + $status = OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; + $errorCode = $e->getCode(); + $message = __( + 'Sorry, something went wrong during product attributes update. Please see log for details.' + ); + } + } catch (NoSuchEntityException $e) { + $this->logger->critical($e->getMessage()); + $status = ($e instanceof TemporaryStateExceptionInterface) + ? OperationInterface::STATUS_TYPE_RETRIABLY_FAILED + : OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; + $errorCode = $e->getCode(); + $message = $e->getMessage(); + } catch (LocalizedException $e) { + $this->logger->critical($e->getMessage()); + $status = OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; + $errorCode = $e->getCode(); + $message = $e->getMessage(); + } catch (\Exception $e) { + $this->logger->critical($e->getMessage()); + $status = OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; + $errorCode = $e->getCode(); + $message = __('Sorry, something went wrong during product attributes update. Please see log for details.'); + } + + $operation->setStatus($status ?? OperationInterface::STATUS_TYPE_COMPLETE) + ->setErrorCode($errorCode ?? null) + ->setResultMessage($message ?? null); + + $this->entityManager->save($operation); + } + + /** + * Update website in products + * + * @param array $productIds + * @param array $websiteRemoveData + * @param array $websiteAddData + * + * @return void + */ + private function updateWebsiteInProducts($productIds, $websiteRemoveData, $websiteAddData): void + { + if ($websiteRemoveData) { + $this->productAction->updateWebsites($productIds, $websiteRemoveData, 'remove'); + } + if ($websiteAddData) { + $this->productAction->updateWebsites($productIds, $websiteAddData, 'add'); + } + } + + /** + * Execute + * + * @param array $data + * + * @return void + */ + private function execute($data): void + { + $this->updateWebsiteInProducts( + $data['product_ids'], + $data['attributes']['website_detach'], + $data['attributes']['website_assign'] + ); + $this->productPriceIndexerProcessor->reindexList($data['product_ids']); + $this->productFlatIndexerProcessor->reindexList($data['product_ids']); + } +} diff --git a/app/code/Magento/Catalog/Model/Category.php b/app/code/Magento/Catalog/Model/Category.php index cf79ff01d315..d911bec0aaac 100644 --- a/app/code/Magento/Catalog/Model/Category.php +++ b/app/code/Magento/Catalog/Model/Category.php @@ -300,7 +300,7 @@ protected function _construct() } /** - * {@inheritdoc} + * @inheritdoc */ protected function getCustomAttributesCodes() { @@ -312,6 +312,8 @@ protected function getCustomAttributesCodes() } /** + * Returns model resource + * * @throws \Magento\Framework\Exception\LocalizedException * @return \Magento\Catalog\Model\ResourceModel\Category * @deprecated because resource models should be used directly @@ -562,7 +564,7 @@ public function getStoreIds() * * If store id is underfined for category return current active store id * - * @return integer + * @return int */ public function getStoreId() { @@ -648,6 +650,8 @@ public function formatUrlKey($str) } /** + * Returns image url + * * @param string $attributeCode * @return bool|string * @throws \Magento\Framework\Exception\LocalizedException @@ -708,7 +712,7 @@ public function getParentId() return $parentId; } $parentIds = $this->getParentIds(); - return intval(array_pop($parentIds)); + return (int) array_pop($parentIds); } /** @@ -796,6 +800,7 @@ public function getChildren($recursive = false, $isActive = true, $sortByPositio /** * Retrieve Stores where isset category Path + * * Return comma separated string * * @return string @@ -826,6 +831,7 @@ public function checkId($id) /** * Get array categories ids which are part of category path + * * Result array contain id of current category because it is part of the path * * @return array @@ -1029,7 +1035,8 @@ public function getAvailableSortBy() /** * Retrieve Available Product Listing Sort By - * code as key, value - name + * + * Code as key, value - name * * @return array */ @@ -1112,10 +1119,15 @@ public function reindex() } } $productIndexer = $this->indexerRegistry->get(Indexer\Category\Product::INDEXER_ID); - if (!$productIndexer->isScheduled() - && (!empty($this->getAffectedProductIds()) || $this->dataHasChangedFor('is_anchor')) - ) { - $productIndexer->reindexList($this->getPathIds()); + + if (!empty($this->getAffectedProductIds()) + || $this->dataHasChangedFor('is_anchor') + || $this->dataHasChangedFor('is_active')) { + if (!$productIndexer->isScheduled()) { + $productIndexer->reindexList($this->getPathIds()); + } else { + $productIndexer->invalidate(); + } } } @@ -1140,16 +1152,27 @@ public function getIdentities() $identities = [ self::CACHE_TAG . '_' . $this->getId(), ]; - if (!$this->getId() || $this->hasDataChanges() - || $this->isDeleted() || $this->dataHasChangedFor(self::KEY_INCLUDE_IN_MENU) - ) { + + if ($this->hasDataChanges()) { + $identities[] = Product::CACHE_PRODUCT_CATEGORY_TAG . '_' . $this->getId(); + } + + if ($this->dataHasChangedFor('is_anchor') || $this->dataHasChangedFor('is_active')) { + foreach ($this->getPathIds() as $id) { + $identities[] = Product::CACHE_PRODUCT_CATEGORY_TAG . '_' . $id; + } + } + + if (!$this->getId() || $this->isDeleted() || $this->dataHasChangedFor(self::KEY_INCLUDE_IN_MENU)) { $identities[] = self::CACHE_TAG; $identities[] = Product::CACHE_PRODUCT_CATEGORY_TAG . '_' . $this->getId(); } - return $identities; + return array_unique($identities); } /** + * Returns path + * * @codeCoverageIgnoreStart * @return string|null */ @@ -1159,6 +1182,8 @@ public function getPath() } /** + * Returns position + * * @return int|null */ public function getPosition() @@ -1167,6 +1192,8 @@ public function getPosition() } /** + * Returns children count + * * @return int */ public function getChildrenCount() @@ -1175,6 +1202,8 @@ public function getChildrenCount() } /** + * Returns created at + * * @return string|null */ public function getCreatedAt() @@ -1183,6 +1212,8 @@ public function getCreatedAt() } /** + * Returns updated at + * * @return string|null */ public function getUpdatedAt() @@ -1191,6 +1222,8 @@ public function getUpdatedAt() } /** + * Returns is active + * * @return bool * @SuppressWarnings(PHPMD.BooleanGetMethodName) */ @@ -1200,6 +1233,8 @@ public function getIsActive() } /** + * Returns category id + * * @return int|null */ public function getCategoryId() @@ -1208,6 +1243,8 @@ public function getCategoryId() } /** + * Returns display mode + * * @return string|null */ public function getDisplayMode() @@ -1216,6 +1253,8 @@ public function getDisplayMode() } /** + * Returns is include in menu + * * @return bool|null */ public function getIncludeInMenu() @@ -1224,6 +1263,8 @@ public function getIncludeInMenu() } /** + * Returns url key + * * @return string|null */ public function getUrlKey() @@ -1232,6 +1273,8 @@ public function getUrlKey() } /** + * Returns children data + * * @return \Magento\Catalog\Api\Data\CategoryTreeInterface[]|null */ public function getChildrenData() @@ -1347,6 +1390,8 @@ public function setLevel($level) } /** + * Set updated at + * * @param string $updatedAt * @return $this */ @@ -1356,6 +1401,8 @@ public function setUpdatedAt($updatedAt) } /** + * Set created at + * * @param string $createdAt * @return $this */ @@ -1365,6 +1412,8 @@ public function setCreatedAt($createdAt) } /** + * Set path + * * @param string $path * @return $this */ @@ -1374,6 +1423,8 @@ public function setPath($path) } /** + * Set available sort by + * * @param string[]|string $availableSortBy * @return $this */ @@ -1383,6 +1434,8 @@ public function setAvailableSortBy($availableSortBy) } /** + * Set include in menu + * * @param bool $includeInMenu * @return $this */ @@ -1403,6 +1456,8 @@ public function setProductCount($productCount) } /** + * Set children data + * * @param \Magento\Catalog\Api\Data\CategoryTreeInterface[] $childrenData * @return $this */ @@ -1412,7 +1467,7 @@ public function setChildrenData(array $childrenData = null) } /** - * {@inheritdoc} + * @inheritdoc * * @return \Magento\Catalog\Api\Data\CategoryExtensionInterface|null */ @@ -1422,7 +1477,7 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * @inheritdoc * * @param \Magento\Catalog\Api\Data\CategoryExtensionInterface $extensionAttributes * @return $this diff --git a/app/code/Magento/Catalog/Model/Category/Attribute/Source/Layout.php b/app/code/Magento/Catalog/Model/Category/Attribute/Source/Layout.php index 1890ea0f7d99..20ea899a3d0d 100644 --- a/app/code/Magento/Catalog/Model/Category/Attribute/Source/Layout.php +++ b/app/code/Magento/Catalog/Model/Category/Attribute/Source/Layout.php @@ -17,6 +17,12 @@ class Layout extends \Magento\Eav\Model\Entity\Attribute\Source\AbstractSource */ protected $pageLayoutBuilder; + /** + * @inheritdoc + * @deprecated since the cache is now handled by \Magento\Theme\Model\PageLayout\Config\Builder::$configFiles + */ + protected $_options = null; + /** * @param \Magento\Framework\View\Model\PageLayout\Config\BuilderInterface $pageLayoutBuilder */ @@ -26,14 +32,14 @@ public function __construct(\Magento\Framework\View\Model\PageLayout\Config\Buil } /** - * {@inheritdoc} + * @inheritdoc */ public function getAllOptions() { - if (!$this->_options) { - $this->_options = $this->pageLayoutBuilder->getPageLayoutsConfig()->toOptionArray(); - array_unshift($this->_options, ['value' => '', 'label' => __('No layout updates')]); - } - return $this->_options; + $options = $this->pageLayoutBuilder->getPageLayoutsConfig()->toOptionArray(); + array_unshift($options, ['value' => '', 'label' => __('No layout updates')]); + $this->_options = $options; + + return $options; } } diff --git a/app/code/Magento/Catalog/Model/Category/Link/SaveHandler.php b/app/code/Magento/Catalog/Model/Category/Link/SaveHandler.php index f22c6903a230..4ea06d4e34d7 100644 --- a/app/code/Magento/Catalog/Model/Category/Link/SaveHandler.php +++ b/app/code/Magento/Catalog/Model/Category/Link/SaveHandler.php @@ -6,7 +6,6 @@ namespace Magento\Catalog\Model\Category\Link; use Magento\Catalog\Api\Data\CategoryLinkInterface; -use Magento\Catalog\Model\Indexer\Product\Category; use Magento\Framework\EntityManager\Operation\ExtensionInterface; /** @@ -40,6 +39,8 @@ public function __construct( } /** + * Execute + * * @param object $entity * @param array $arguments * @return object @@ -78,6 +79,8 @@ public function execute($entity, $arguments = []) } /** + * Get category links positions + * * @param object $entity * @return array */ @@ -106,27 +109,19 @@ private function getCategoryLinksPositions($entity) */ private function mergeCategoryLinks($newCategoryPositions, $oldCategoryPositions) { - $result = []; if (empty($newCategoryPositions)) { - return $result; + return []; } + $categoryPositions = array_combine(array_column($oldCategoryPositions, 'category_id'), $oldCategoryPositions); foreach ($newCategoryPositions as $newCategoryPosition) { - $key = array_search( - $newCategoryPosition['category_id'], - array_column($oldCategoryPositions, 'category_id') - ); - - if ($key === false) { - $result[] = $newCategoryPosition; - } elseif (isset($oldCategoryPositions[$key]) - && $oldCategoryPositions[$key]['position'] != $newCategoryPosition['position'] - ) { - $result[] = $newCategoryPositions[$key]; - unset($oldCategoryPositions[$key]); + $categoryId = $newCategoryPosition['category_id']; + if (!isset($categoryPositions[$categoryId])) { + $categoryPositions[$categoryId] = ['category_id' => $categoryId]; } + $categoryPositions[$categoryId]['position'] = $newCategoryPosition['position']; } - $result = array_merge($result, $oldCategoryPositions); + $result = array_values($categoryPositions); return $result; } diff --git a/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php b/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php index 1e07c0cdd924..44bf153f8369 100644 --- a/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php +++ b/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php @@ -43,6 +43,8 @@ public function getPositions(int $categoryId): array $categoryId )->order( 'ccp.position ' . \Magento\Framework\DB\Select::SQL_ASC + )->order( + 'ccp.product_id ' . \Magento\Framework\DB\Select::SQL_DESC ); return array_flip($connection->fetchCol($select)); diff --git a/app/code/Magento/Catalog/Model/Category/Tree.php b/app/code/Magento/Catalog/Model/Category/Tree.php index 6080f74d5fa0..0a9cb25d7b0e 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 790ea6b921fb..cab8e013d9ba 100644 --- a/app/code/Magento/Catalog/Model/CategoryList.php +++ b/app/code/Magento/Catalog/Model/CategoryList.php @@ -15,6 +15,9 @@ use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; +/** + * Class for getting category list. + */ class CategoryList implements CategoryListInterface { /** @@ -64,7 +67,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getList(SearchCriteriaInterface $searchCriteria) { diff --git a/app/code/Magento/Catalog/Model/Config.php b/app/code/Magento/Catalog/Model/Config.php index d2ffd6f44004..5dce940308a4 100644 --- a/app/code/Magento/Catalog/Model/Config.php +++ b/app/code/Magento/Catalog/Model/Config.php @@ -381,7 +381,7 @@ public function getProductTypeName($id) $this->loadProductTypes(); - return isset($this->_productTypesById[$id]) ? $this->_productTypesById[$id] : false; + return $this->_productTypesById[$id] ?? false; } /** diff --git a/app/code/Magento/Catalog/Model/Config/CatalogClone/Media/Image.php b/app/code/Magento/Catalog/Model/Config/CatalogClone/Media/Image.php index e2b0a9157402..10675a7b7c7e 100644 --- a/app/code/Magento/Catalog/Model/Config/CatalogClone/Media/Image.php +++ b/app/code/Magento/Catalog/Model/Config/CatalogClone/Media/Image.php @@ -5,6 +5,9 @@ */ namespace Magento\Catalog\Model\Config\CatalogClone\Media; +use Magento\Framework\Escaper; +use Magento\Framework\App\ObjectManager; + /** * Clone model for media images related config fields * @@ -26,6 +29,11 @@ class Image extends \Magento\Framework\App\Config\Value */ protected $_attributeCollectionFactory; + /** + * @var Escaper + */ + private $escaper; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -36,6 +44,9 @@ class Image extends \Magento\Framework\App\Config\Value * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data + * @param Escaper|null $escaper + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\Model\Context $context, @@ -46,8 +57,10 @@ public function __construct( \Magento\Eav\Model\Config $eavConfig, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + Escaper $escaper = null ) { + $this->escaper = $escaper ?? ObjectManager::getInstance()->get(Escaper::class); $this->_attributeCollectionFactory = $attributeCollectionFactory; $this->_eavConfig = $eavConfig; parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data); @@ -71,10 +84,9 @@ public function getPrefixes() $prefixes = []; foreach ($collection as $attribute) { - /* @var $attribute \Magento\Eav\Model\Entity\Attribute */ $prefixes[] = [ 'field' => $attribute->getAttributeCode() . '_', - 'label' => $attribute->getFrontend()->getLabel(), + 'label' => $this->escaper->escapeHtml($attribute->getFrontend()->getLabel()), ]; } diff --git a/app/code/Magento/Catalog/Model/Design.php b/app/code/Magento/Catalog/Model/Design.php index bd7cdabb4085..853bbeac8eb3 100644 --- a/app/code/Magento/Catalog/Model/Design.php +++ b/app/code/Magento/Catalog/Model/Design.php @@ -5,6 +5,8 @@ */ namespace Magento\Catalog\Model; +use \Magento\Framework\TranslateInterface; + /** * Catalog Custom Category design Model * @@ -31,14 +33,20 @@ class Design extends \Magento\Framework\Model\AbstractModel */ protected $_localeDate; + /** + * @var TranslateInterface + */ + private $translator; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate * @param \Magento\Framework\View\DesignInterface $design - * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource - * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection + * @param \Magento\Framework\Model\ResourceModel\AbstractResource|null $resource + * @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection * @param array $data + * @param TranslateInterface|null $translator */ public function __construct( \Magento\Framework\Model\Context $context, @@ -47,10 +55,13 @@ public function __construct( \Magento\Framework\View\DesignInterface $design, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + TranslateInterface $translator = null ) { $this->_localeDate = $localeDate; $this->_design = $design; + $this->translator = $translator ?: + \Magento\Framework\App\ObjectManager::getInstance()->get(TranslateInterface::class); parent::__construct($context, $registry, $resource, $resourceCollection, $data); } @@ -63,6 +74,7 @@ public function __construct( public function applyCustomDesign($design) { $this->_design->setDesignTheme($design); + $this->translator->loadData(null, true); return $this; } diff --git a/app/code/Magento/Catalog/Model/FilterProductCustomAttribute.php b/app/code/Magento/Catalog/Model/FilterProductCustomAttribute.php index b83bb97301b9..497ed2fd4995 100644 --- a/app/code/Magento/Catalog/Model/FilterProductCustomAttribute.php +++ b/app/code/Magento/Catalog/Model/FilterProductCustomAttribute.php @@ -27,11 +27,12 @@ public function __construct(array $blackList = []) /** * Delete custom attribute - * @param array $attributes + * + * @param array $attributes set objects attributes @example ['attribute_code'=>'attribute_object'] * @return array */ public function execute(array $attributes): array { - return array_diff($attributes, $this->blackList); + return array_diff_key($attributes, array_flip($this->blackList)); } } diff --git a/app/code/Magento/Catalog/Model/ImageExtractor.php b/app/code/Magento/Catalog/Model/ImageExtractor.php index 1c2060867067..1cb1f305a220 100644 --- a/app/code/Magento/Catalog/Model/ImageExtractor.php +++ b/app/code/Magento/Catalog/Model/ImageExtractor.php @@ -9,6 +9,9 @@ use Magento\Catalog\Model\Product\Attribute\Backend\Media\ImageEntryConverter; use Magento\Framework\View\Xsd\Media\TypeDataExtractorInterface; +/** + * Image extractor from xml configuration + */ class ImageExtractor implements TypeDataExtractorInterface { /** @@ -17,6 +20,7 @@ class ImageExtractor implements TypeDataExtractorInterface * @param \DOMElement $mediaNode * @param string $mediaParentTag * @return array + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function process(\DOMElement $mediaNode, $mediaParentTag) { @@ -36,7 +40,13 @@ public function process(\DOMElement $mediaNode, $mediaParentTag) if ($attributeTagName === 'background') { $nodeValue = $this->processImageBackground($attribute->nodeValue); } elseif ($attributeTagName === 'width' || $attributeTagName === 'height') { - $nodeValue = intval($attribute->nodeValue); + $nodeValue = (int) $attribute->nodeValue; + } elseif ($attributeTagName === 'constrain' + || $attributeTagName === 'aspect_ratio' + || $attributeTagName === 'frame' + || $attributeTagName === 'transparency' + ) { + $nodeValue = in_array($attribute->nodeValue, [true, 1, 'true', '1'], true) ?? false; } else { $nodeValue = $attribute->nodeValue; } diff --git a/app/code/Magento/Catalog/Model/ImageUploader.php b/app/code/Magento/Catalog/Model/ImageUploader.php index ce92a2c1d958..b5ca0895d6d1 100644 --- a/app/code/Magento/Catalog/Model/ImageUploader.php +++ b/app/code/Magento/Catalog/Model/ImageUploader.php @@ -67,14 +67,9 @@ class ImageUploader /** * List of allowed image mime types * - * @var array + * @var string[] */ - private $allowedMimeTypes = [ - 'image/jpg', - 'image/jpeg', - 'image/gif', - 'image/png', - ]; + private $allowedMimeTypes; /** * ImageUploader constructor @@ -87,6 +82,7 @@ class ImageUploader * @param string $baseTmpPath * @param string $basePath * @param string[] $allowedExtensions + * @param string[] $allowedMimeTypes */ public function __construct( \Magento\MediaStorage\Helper\File\Storage\Database $coreFileStorageDatabase, @@ -96,7 +92,8 @@ public function __construct( \Psr\Log\LoggerInterface $logger, $baseTmpPath, $basePath, - $allowedExtensions + $allowedExtensions, + $allowedMimeTypes = [] ) { $this->coreFileStorageDatabase = $coreFileStorageDatabase; $this->mediaDirectory = $filesystem->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::MEDIA); @@ -106,6 +103,7 @@ public function __construct( $this->baseTmpPath = $baseTmpPath; $this->basePath = $basePath; $this->allowedExtensions = $allowedExtensions; + $this->allowedMimeTypes = $allowedMimeTypes; } /** @@ -165,7 +163,7 @@ public function getBasePath() } /** - * Retrieve base path + * Retrieve allowed extensions * * @return string[] */ 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 4b16a4810c0a..1506ccf6963b 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Flat/AbstractAction.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Flat/AbstractAction.php @@ -7,8 +7,10 @@ namespace Magento\Catalog\Model\Indexer\Category\Flat; use Magento\Framework\App\ResourceConnection; -use Magento\Framework\EntityManager\MetadataPool; +/** + * Abstract action class for category flat indexers. + */ class AbstractAction { /** @@ -111,7 +113,7 @@ public function getColumns() public function getMainStoreTable($storeId = \Magento\Store\Model\Store::DEFAULT_STORE_ID) { if (is_string($storeId)) { - $storeId = intval($storeId); + $storeId = (int) $storeId; } $suffix = sprintf('store_%d', $storeId); @@ -131,7 +133,7 @@ protected function getFlatTableStructure($tableName) $table = $this->connection->newTable( $tableName )->setComment( - sprintf("Catalog Category Flat", $tableName) + 'Catalog Category Flat' ); //Adding columns @@ -379,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']; @@ -497,6 +499,8 @@ protected function getTableName($name) } /** + * Get category metadata instance. + * * @return \Magento\Framework\EntityManager\EntityMetadata */ private function getCategoryMetadata() @@ -510,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/Flat/Action/Full.php b/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Action/Full.php index 64a8f930d83e..a62e3d8f83b8 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Action/Full.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Action/Full.php @@ -5,6 +5,9 @@ */ namespace Magento\Catalog\Model\Indexer\Category\Flat\Action; +/** + * Class for full reindex flat categories + */ class Full extends \Magento\Catalog\Model\Indexer\Category\Flat\AbstractAction { /** @@ -92,6 +95,7 @@ protected function populateFlatTables(array $stores) /** * Create table and add attributes as fields for specified store. + * * This routine assumes that DDL operations are allowed * * @param int $store @@ -109,6 +113,7 @@ protected function createTable($store) /** * Create category flat tables and add attributes as fields. + * * Tables are created only if DDL operations are allowed * * @param \Magento\Store\Model\Store[] $stores if empty, create tables for all stores of the application @@ -167,6 +172,44 @@ protected function switchTables(array $stores = []) return $this; } + /** + * Retrieve all actual Catalog Product Flat Table names + * + * @return string[] + */ + private function getActualStoreTablesForCategoryFlat(): array + { + $actualStoreTables = []; + foreach ($this->storeManager->getStores() as $store) { + $actualStoreTables[] = sprintf( + '%s_store_%s', + $this->connection->getTableName('catalog_category_flat'), + $store->getId() + ); + } + + return $actualStoreTables; + } + + /** + * Delete all category flat tables for not existing stores + * + * @return void + */ + private function deleteAbandonedStoreCategoryFlatTables(): void + { + $existentTables = $this->connection->getTables( + $this->connection->getTableName('catalog_category_flat_store_%') + ); + $actualStoreTables = $this->getActualStoreTablesForCategoryFlat(); + + $tablesToDelete = array_diff($existentTables, $actualStoreTables); + + foreach ($tablesToDelete as $table) { + $this->connection->dropTable($table); + } + } + /** * Transactional rebuild flat data from eav * @@ -182,7 +225,7 @@ public function reindexAll() $stores = $this->storeManager->getStores(); $this->populateFlatTables($stores); $this->switchTables($stores); - + $this->deleteAbandonedStoreCategoryFlatTables(); $this->allowTableChanges = true; return $this; diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php index 6a499883ac72..178f4172ce6f 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php @@ -123,6 +123,11 @@ abstract class AbstractAction */ private $queryGenerator; + /** + * @var int + */ + private $currentStoreId = 0; + /** * @param ResourceConnection $resource * @param \Magento\Store\Model\StoreManagerInterface $storeManager @@ -164,6 +169,7 @@ protected function reindex() { foreach ($this->storeManager->getStores() as $store) { if ($this->getPathFromCategoryId($store->getRootCategoryId())) { + $this->currentStoreId = $store->getId(); $this->reindexRootCategory($store); $this->reindexAnchorCategories($store); $this->reindexNonAnchorCategories($store); @@ -586,6 +592,8 @@ protected function createAnchorSelect(Store $store) } /** + * Get temporary table name + * * Get temporary table name for concurrent indexing in persistent connection * Temp table name is NOT shared between action instances and each action has it's own temp tree index * @@ -597,7 +605,7 @@ protected function getTemporaryTreeIndexTableName() if (empty($this->tempTreeIndexTableName)) { $this->tempTreeIndexTableName = $this->connection->getTableName('temp_catalog_category_tree_index') . '_' - . substr(md5(time() . random_int(0, 999999999)), 0, 8); + . substr(sha1(time() . random_int(0, 999999999)), 0, 8); } return $this->tempTreeIndexTableName; @@ -641,7 +649,6 @@ protected function makeTempCategoryTreeIndex() ['child_id'], ['type' => \Magento\Framework\DB\Adapter\AdapterInterface::INDEX_TYPE_INDEX] ); - // Drop the temporary table in case it already exists on this (persistent?) connection. $this->connection->dropTemporaryTable($temporaryName); $this->connection->createTemporaryTable($temporaryTable); @@ -659,11 +666,31 @@ protected function makeTempCategoryTreeIndex() */ protected function fillTempCategoryTreeIndex($temporaryName) { + $isActiveAttributeId = $this->config->getAttribute( + \Magento\Catalog\Model\Category::ENTITY, + 'is_active' + )->getId(); + $categoryMetadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\CategoryInterface::class); + $categoryLinkField = $categoryMetadata->getLinkField(); $selects = $this->prepareSelectsByRange( $this->connection->select() ->from( ['c' => $this->getTable('catalog_category_entity')], ['entity_id', 'path'] + )->joinInner( + ['ccacd' => $this->getTable('catalog_category_entity_int')], + 'ccacd.' . $categoryLinkField . ' = c.' . $categoryLinkField . ' AND ccacd.store_id = 0' . + ' AND ccacd.attribute_id = ' . $isActiveAttributeId, + [] + )->joinLeft( + ['ccacs' => $this->getTable('catalog_category_entity_int')], + 'ccacs.' . $categoryLinkField . ' = c.' . $categoryLinkField + . ' AND ccacs.attribute_id = ccacd.attribute_id AND ccacs.store_id = ' . + $this->currentStoreId, + [] + )->where( + $this->connection->getIfNullSql('ccacs.value', 'ccacd.value') . ' = ?', + 1 ), 'entity_id' ); 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 f8121b55dbf9..eb59acb56c35 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Full.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Full.php @@ -3,33 +3,46 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Catalog\Model\Indexer\Category\Product\Action; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Config; +use Magento\Catalog\Model\Indexer\Category\Product\AbstractAction; use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Query\Generator as QueryGenerator; use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Select; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Indexer\BatchProviderInterface; +use Magento\Framework\Indexer\BatchSizeManagementInterface; use Magento\Indexer\Model\ProcessManager; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; /** * Class Full reindex action * - * @package Magento\Catalog\Model\Indexer\Category\Product\Action * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Full extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractAction +class Full extends AbstractAction { /** - * @var \Magento\Framework\Indexer\BatchSizeManagementInterface + * @var BatchSizeManagementInterface */ private $batchSizeManagement; /** - * @var \Magento\Framework\Indexer\BatchProviderInterface + * @var BatchProviderInterface */ private $batchProvider; /** - * @var \Magento\Framework\EntityManager\MetadataPool + * @var MetadataPool */ protected $metadataPool; @@ -52,25 +65,25 @@ class Full extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio /** * @param ResourceConnection $resource - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Catalog\Model\Config $config + * @param StoreManagerInterface $storeManager + * @param Config $config * @param QueryGenerator|null $queryGenerator - * @param \Magento\Framework\Indexer\BatchSizeManagementInterface|null $batchSizeManagement - * @param \Magento\Framework\Indexer\BatchProviderInterface|null $batchProvider - * @param \Magento\Framework\EntityManager\MetadataPool|null $metadataPool + * @param BatchSizeManagementInterface|null $batchSizeManagement + * @param BatchProviderInterface|null $batchProvider + * @param MetadataPool|null $metadataPool * @param int|null $batchRowsCount * @param ActiveTableSwitcher|null $activeTableSwitcher * @param ProcessManager $processManager * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Framework\App\ResourceConnection $resource, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Catalog\Model\Config $config, + ResourceConnection $resource, + StoreManagerInterface $storeManager, + Config $config, QueryGenerator $queryGenerator = null, - \Magento\Framework\Indexer\BatchSizeManagementInterface $batchSizeManagement = null, - \Magento\Framework\Indexer\BatchProviderInterface $batchProvider = null, - \Magento\Framework\EntityManager\MetadataPool $metadataPool = null, + BatchSizeManagementInterface $batchSizeManagement = null, + BatchProviderInterface $batchProvider = null, + MetadataPool $metadataPool = null, $batchRowsCount = null, ActiveTableSwitcher $activeTableSwitcher = null, ProcessManager $processManager = null @@ -81,15 +94,15 @@ public function __construct( $config, $queryGenerator ); - $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); + $objectManager = ObjectManager::getInstance(); $this->batchSizeManagement = $batchSizeManagement ?: $objectManager->get( - \Magento\Framework\Indexer\BatchSizeManagementInterface::class + BatchSizeManagementInterface::class ); $this->batchProvider = $batchProvider ?: $objectManager->get( - \Magento\Framework\Indexer\BatchProviderInterface::class + BatchProviderInterface::class ); $this->metadataPool = $metadataPool ?: $objectManager->get( - \Magento\Framework\EntityManager\MetadataPool::class + MetadataPool::class ); $this->batchRowsCount = $batchRowsCount; $this->activeTableSwitcher = $activeTableSwitcher ?: $objectManager->get(ActiveTableSwitcher::class); @@ -97,33 +110,39 @@ public function __construct( } /** + * Create the store tables + * * @return void */ - private function createTables() + private function createTables(): void { foreach ($this->storeManager->getStores() as $store) { - $this->tableMaintainer->createTablesForStore($store->getId()); + $this->tableMaintainer->createTablesForStore((int)$store->getId()); } } /** + * Truncates the replica tables + * * @return void */ - private function clearReplicaTables() + private function clearReplicaTables(): void { foreach ($this->storeManager->getStores() as $store) { - $this->connection->truncateTable($this->tableMaintainer->getMainReplicaTable($store->getId())); + $this->connection->truncateTable($this->tableMaintainer->getMainReplicaTable((int)$store->getId())); } } /** + * Switches the active table + * * @return void */ - private function switchTables() + private function switchTables(): void { $tablesToSwitch = []; foreach ($this->storeManager->getStores() as $store) { - $tablesToSwitch[] = $this->tableMaintainer->getMainTable($store->getId()); + $tablesToSwitch[] = $this->tableMaintainer->getMainTable((int)$store->getId()); } $this->activeTableSwitcher->switchTable($this->connection, $tablesToSwitch); } @@ -133,12 +152,13 @@ private function switchTables() * * @return $this */ - public function execute() + public function execute(): self { $this->createTables(); $this->clearReplicaTables(); $this->reindex(); $this->switchTables(); + return $this; } @@ -147,7 +167,7 @@ public function execute() * * @return void */ - protected function reindex() + protected function reindex(): void { $userFunctions = []; @@ -165,9 +185,9 @@ protected function reindex() /** * Execute indexation by store * - * @param \Magento\Store\Model\Store $store + * @param Store $store */ - private function reindexStore($store) + private function reindexStore($store): void { $this->reindexRootCategory($store); $this->reindexAnchorCategories($store); @@ -177,31 +197,31 @@ private function reindexStore($store) /** * Publish data from tmp to replica table * - * @param \Magento\Store\Model\Store $store + * @param Store $store * @return void */ - private function publishData($store) + private function publishData($store): void { - $select = $this->connection->select()->from($this->tableMaintainer->getMainTmpTable($store->getId())); + $select = $this->connection->select()->from($this->tableMaintainer->getMainTmpTable((int)$store->getId())); $columns = array_keys( - $this->connection->describeTable($this->tableMaintainer->getMainReplicaTable($store->getId())) + $this->connection->describeTable($this->tableMaintainer->getMainReplicaTable((int)$store->getId())) ); - $tableName = $this->tableMaintainer->getMainReplicaTable($store->getId()); + $tableName = $this->tableMaintainer->getMainReplicaTable((int)$store->getId()); $this->connection->query( $this->connection->insertFromSelect( $select, $tableName, $columns, - \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE + AdapterInterface::INSERT_ON_DUPLICATE ) ); } /** - * {@inheritdoc} + * @inheritdoc */ - protected function reindexRootCategory(\Magento\Store\Model\Store $store) + protected function reindexRootCategory(Store $store): void { if ($this->isIndexRootCategoryNeeded()) { $this->reindexCategoriesBySelect($this->getAllProducts($store), 'cp.entity_id IN (?)', $store); @@ -211,10 +231,10 @@ protected function reindexRootCategory(\Magento\Store\Model\Store $store) /** * Reindex products of anchor categories * - * @param \Magento\Store\Model\Store $store + * @param Store $store * @return void */ - protected function reindexAnchorCategories(\Magento\Store\Model\Store $store) + protected function reindexAnchorCategories(Store $store): void { $this->reindexCategoriesBySelect($this->getAnchorCategoriesSelect($store), 'ccp.product_id IN (?)', $store); } @@ -222,10 +242,10 @@ protected function reindexAnchorCategories(\Magento\Store\Model\Store $store) /** * Reindex products of non anchor categories * - * @param \Magento\Store\Model\Store $store + * @param Store $store * @return void */ - protected function reindexNonAnchorCategories(\Magento\Store\Model\Store $store) + protected function reindexNonAnchorCategories(Store $store): void { $this->reindexCategoriesBySelect($this->getNonAnchorCategoriesSelect($store), 'ccp.product_id IN (?)', $store); } @@ -233,40 +253,42 @@ protected function reindexNonAnchorCategories(\Magento\Store\Model\Store $store) /** * Reindex categories using given SQL select and condition. * - * @param \Magento\Framework\DB\Select $basicSelect + * @param Select $basicSelect * @param string $whereCondition - * @param \Magento\Store\Model\Store $store + * @param Store $store * @return void */ - private function reindexCategoriesBySelect(\Magento\Framework\DB\Select $basicSelect, $whereCondition, $store) + private function reindexCategoriesBySelect(Select $basicSelect, $whereCondition, $store): void { - $this->tableMaintainer->createMainTmpTable($store->getId()); + $this->tableMaintainer->createMainTmpTable((int)$store->getId()); - $entityMetadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); + $entityMetadata = $this->metadataPool->getMetadata(ProductInterface::class); $columns = array_keys( - $this->connection->describeTable($this->tableMaintainer->getMainTmpTable($store->getId())) + $this->connection->describeTable($this->tableMaintainer->getMainTmpTable((int)$store->getId())) ); $this->batchSizeManagement->ensureBatchSize($this->connection, $this->batchRowsCount); - $batches = $this->batchProvider->getBatches( - $this->connection, - $entityMetadata->getEntityTable(), + + $select = $this->connection->select(); + $select->distinct(true); + $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); + + $batchQueries = $this->prepareSelectsByRange( + $select, $entityMetadata->getIdentifierField(), - $this->batchRowsCount + (int)$this->batchRowsCount ); - foreach ($batches as $batch) { - $this->connection->delete($this->tableMaintainer->getMainTmpTable($store->getId())); + + foreach ($batchQueries as $query) { + $this->connection->delete($this->tableMaintainer->getMainTmpTable((int)$store->getId())); + $entityIds = $this->connection->fetchCol($query); $resultSelect = clone $basicSelect; - $select = $this->connection->select(); - $select->distinct(true); - $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); - $entityIds = $this->batchProvider->getBatchIds($this->connection, $select, $batch); $resultSelect->where($whereCondition, $entityIds); $this->connection->query( $this->connection->insertFromSelect( $resultSelect, - $this->tableMaintainer->getMainTmpTable($store->getId()), + $this->tableMaintainer->getMainTmpTable((int)$store->getId()), $columns, - \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE + AdapterInterface::INSERT_ON_DUPLICATE ) ); $this->publishData($store); diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php index a218266c2503..cb708695255d 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php @@ -17,6 +17,8 @@ use Magento\Store\Model\StoreManagerInterface; /** + * Category rows indexer. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Rows extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractAction @@ -213,6 +215,7 @@ protected function isRangingNeeded() /** * Returns a list of category ids which are assigned to product ids in the index * + * @param array $productIds * @return \Magento\Framework\Indexer\CacheContext */ private function getCategoryIdsFromIndex(array $productIds) diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Eav/Action/Full.php b/app/code/Magento/Catalog/Model/Indexer/Product/Eav/Action/Full.php index 802176092d14..ed8f692885d9 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Eav/Action/Full.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Eav/Action/Full.php @@ -7,26 +7,41 @@ namespace Magento\Catalog\Model\Indexer\Product\Eav\Action; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Indexer\Product\Eav\AbstractAction; use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\DecimalFactory; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Query\BatchIteratorInterface; +use Magento\Framework\DB\Query\Generator as QueryGenerator; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Indexer\BatchProviderInterface; +use Magento\Store\Model\ScopeInterface; /** * Class Full reindex action + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Full extends \Magento\Catalog\Model\Indexer\Product\Eav\AbstractAction +class Full extends AbstractAction { /** - * @var \Magento\Framework\EntityManager\MetadataPool + * @var MetadataPool */ private $metadataPool; /** - * @var \Magento\Framework\Indexer\BatchProviderInterface + * @var BatchProviderInterface */ private $batchProvider; /** - * @var \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator + * @var BatchSizeCalculator */ private $batchSizeCalculator; @@ -36,44 +51,54 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Eav\AbstractAction private $activeTableSwitcher; /** - * @var \Magento\Framework\App\Config\ScopeConfigInterface + * @var ScopeConfigInterface */ private $scopeConfig; /** - * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\DecimalFactory $eavDecimalFactory - * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory $eavSourceFactory - * @param \Magento\Framework\EntityManager\MetadataPool|null $metadataPool - * @param \Magento\Framework\Indexer\BatchProviderInterface|null $batchProvider - * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator $batchSizeCalculator + * @var QueryGenerator|null + */ + private $batchQueryGenerator; + + /** + * @param DecimalFactory $eavDecimalFactory + * @param SourceFactory $eavSourceFactory + * @param MetadataPool|null $metadataPool + * @param BatchProviderInterface|null $batchProvider + * @param BatchSizeCalculator $batchSizeCalculator * @param ActiveTableSwitcher|null $activeTableSwitcher - * @param \Magento\Framework\App\Config\ScopeConfigInterface|null $scopeConfig + * @param ScopeConfigInterface|null $scopeConfig + * @param QueryGenerator|null $batchQueryGenerator */ public function __construct( - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\DecimalFactory $eavDecimalFactory, - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory $eavSourceFactory, - \Magento\Framework\EntityManager\MetadataPool $metadataPool = null, - \Magento\Framework\Indexer\BatchProviderInterface $batchProvider = null, - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator $batchSizeCalculator = null, + DecimalFactory $eavDecimalFactory, + SourceFactory $eavSourceFactory, + MetadataPool $metadataPool = null, + BatchProviderInterface $batchProvider = null, + BatchSizeCalculator $batchSizeCalculator = null, ActiveTableSwitcher $activeTableSwitcher = null, - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig = null + ScopeConfigInterface $scopeConfig = null, + QueryGenerator $batchQueryGenerator = null ) { - $this->scopeConfig = $scopeConfig ?: \Magento\Framework\App\ObjectManager::getInstance()->get( - \Magento\Framework\App\Config\ScopeConfigInterface::class + $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get( + ScopeConfigInterface::class ); parent::__construct($eavDecimalFactory, $eavSourceFactory, $scopeConfig); - $this->metadataPool = $metadataPool ?: \Magento\Framework\App\ObjectManager::getInstance()->get( - \Magento\Framework\EntityManager\MetadataPool::class + $this->metadataPool = $metadataPool ?: ObjectManager::getInstance()->get( + MetadataPool::class ); - $this->batchProvider = $batchProvider ?: \Magento\Framework\App\ObjectManager::getInstance()->get( - \Magento\Framework\Indexer\BatchProviderInterface::class + $this->batchProvider = $batchProvider ?: ObjectManager::getInstance()->get( + BatchProviderInterface::class ); - $this->batchSizeCalculator = $batchSizeCalculator ?: \Magento\Framework\App\ObjectManager::getInstance()->get( - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator::class + $this->batchSizeCalculator = $batchSizeCalculator ?: ObjectManager::getInstance()->get( + BatchSizeCalculator::class ); - $this->activeTableSwitcher = $activeTableSwitcher ?: \Magento\Framework\App\ObjectManager::getInstance()->get( + $this->activeTableSwitcher = $activeTableSwitcher ?: ObjectManager::getInstance()->get( ActiveTableSwitcher::class ); + $this->batchQueryGenerator = $batchQueryGenerator ?: ObjectManager::getInstance()->get( + QueryGenerator::class + ); } /** @@ -81,10 +106,10 @@ public function __construct( * * @param array|int|null $ids * @return void - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function execute($ids = null) + public function execute($ids = null): void { if (!$this->isEavIndexerEnabled()) { return; @@ -94,20 +119,21 @@ public function execute($ids = null) $connection = $indexer->getConnection(); $mainTable = $this->activeTableSwitcher->getAdditionalTableName($indexer->getMainTable()); $connection->truncateTable($mainTable); - $entityMetadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); - $batches = $this->batchProvider->getBatches( - $connection, - $entityMetadata->getEntityTable(), + $entityMetadata = $this->metadataPool->getMetadata(ProductInterface::class); + + $select = $connection->select(); + $select->distinct(true); + $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); + + $batchQueries = $this->batchQueryGenerator->generate( $entityMetadata->getIdentifierField(), - $this->batchSizeCalculator->estimateBatchSize($connection, $indexerName) + $select, + $this->batchSizeCalculator->estimateBatchSize($connection, $indexerName), + BatchIteratorInterface::NON_UNIQUE_FIELD_ITERATOR ); - foreach ($batches as $batch) { - /** @var \Magento\Framework\DB\Select $select */ - $select = $connection->select(); - $select->distinct(true); - $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); - $entityIds = $this->batchProvider->getBatchIds($connection, $select, $batch); + foreach ($batchQueries as $query) { + $entityIds = $connection->fetchCol($query); if (!empty($entityIds)) { $indexer->reindexEntities($this->processRelations($indexer, $entityIds, true)); $this->syncData($indexer, $mainTable); @@ -116,14 +142,14 @@ public function execute($ids = null) $this->activeTableSwitcher->switchTable($indexer->getConnection(), [$indexer->getMainTable()]); } } catch (\Exception $e) { - throw new \Magento\Framework\Exception\LocalizedException(__($e->getMessage()), $e); + throw new LocalizedException(__($e->getMessage()), $e); } } /** * @inheritdoc */ - protected function syncData($indexer, $destinationTable, $ids = null) + protected function syncData($indexer, $destinationTable, $ids = null): void { $connection = $indexer->getConnection(); $connection->beginTransaction(); @@ -136,7 +162,7 @@ protected function syncData($indexer, $destinationTable, $ids = null) $select, $destinationTable, $targetColumns, - \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE + AdapterInterface::INSERT_ON_DUPLICATE ); $connection->query($query); $connection->commit(); @@ -155,7 +181,7 @@ private function isEavIndexerEnabled(): bool { $eavIndexerStatus = $this->scopeConfig->getValue( self::ENABLE_EAV_INDEXER, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE ); return (bool)$eavIndexerStatus; diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Eraser.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Eraser.php index 8182e6f07fab..ad734b96d59d 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Eraser.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Eraser.php @@ -8,7 +8,12 @@ namespace Magento\Catalog\Model\Indexer\Product\Flat\Action; use Magento\Framework\App\ResourceConnection; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Store\Model\Store; +/** + * Flat item eraser. Used to clear items from the catalog flat table. + */ class Eraser { /** @@ -50,12 +55,7 @@ public function __construct( */ public function removeDeletedProducts(array &$ids, $storeId) { - $select = $this->connection->select()->from( - $this->productIndexerHelper->getTable('catalog_product_entity') - )->where( - 'entity_id IN(?)', - $ids - ); + $select = $this->getSelectForProducts($ids); $result = $this->connection->query($select); $existentProducts = []; @@ -69,6 +69,62 @@ public function removeDeletedProducts(array &$ids, $storeId) $this->deleteProductsFromStore($productsToDelete, $storeId); } + /** + * Remove products with "Disabled" status from the flat table(s). + * + * @param array $ids + * @param int $storeId + * @return void + */ + public function removeDisabledProducts(array &$ids, $storeId) + { + /* @var $statusAttribute \Magento\Eav\Model\Entity\Attribute */ + $statusAttribute = $this->productIndexerHelper->getAttribute('status'); + + $select = $this->getSelectForProducts($ids); + $select->joinLeft( + ['status_global_attr' => $statusAttribute->getBackendTable()], + ' status_global_attr.attribute_id = ' . (int)$statusAttribute->getAttributeId() + . ' AND status_global_attr.store_id = ' . Store::DEFAULT_STORE_ID, + [] + ); + $select->joinLeft( + ['status_attr' => $statusAttribute->getBackendTable()], + ' status_attr.attribute_id = ' . (int)$statusAttribute->getAttributeId() + . ' AND status_attr.store_id = ' . $storeId, + [] + ); + $select->where('IFNULL(status_attr.value, status_global_attr.value) = ?', Status::STATUS_DISABLED); + + $result = $this->connection->query($select); + + $disabledProducts = []; + foreach ($result->fetchAll() as $product) { + $disabledProducts[] = $product['entity_id']; + } + + if (!empty($disabledProducts)) { + $ids = array_diff($ids, $disabledProducts); + $this->deleteProductsFromStore($disabledProducts, $storeId); + } + } + + /** + * Get Select object for existed products. + * + * @param array $ids + * @return \Magento\Framework\DB\Select + */ + private function getSelectForProducts(array $ids) + { + $productTable = $this->productIndexerHelper->getTable('catalog_product_entity'); + $select = $this->connection->select() + ->from(['product_table' => $productTable]) + ->columns('entity_id') + ->where('product_table.entity_id IN(?)', $ids); + return $select; + } + /** * Delete products from flat table(s) * diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Indexer.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Indexer.php index 3a1611299288..c14bc0dd7e50 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Indexer.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Indexer.php @@ -9,6 +9,7 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Framework\App\ResourceConnection; use Magento\Framework\EntityManager\MetadataPool; +use Magento\Store\Model\Store; /** * Class Indexer @@ -53,42 +54,39 @@ public function __construct( * @param int $storeId * @param int $productId * @param string $valueFieldSuffix - * @return \Magento\Catalog\Model\Indexer\Product\Flat + * @return $this * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @SuppressWarnings(PHPMD.NPathComplexity) */ public function write($storeId, $productId, $valueFieldSuffix = '') { $flatTable = $this->_productIndexerHelper->getFlatTableName($storeId); + $entityTableName = $this->_productIndexerHelper->getTable('catalog_product_entity'); $attributes = $this->_productIndexerHelper->getAttributes(); $eavAttributes = $this->_productIndexerHelper->getTablesStructure($attributes); $updateData = []; $describe = $this->_connection->describeTable($flatTable); + $metadata = $this->getMetadataPool()->getMetadata(ProductInterface::class); + $linkField = $metadata->getLinkField(); foreach ($eavAttributes as $tableName => $tableColumns) { $columnsChunks = array_chunk($tableColumns, self::ATTRIBUTES_CHUNK_SIZE, true); foreach ($columnsChunks as $columns) { $select = $this->_connection->select(); - $selectValue = $this->_connection->select(); - $keyColumns = [ - 'entity_id' => 'e.entity_id', - 'attribute_id' => 't.attribute_id', - 'value' => $this->_connection->getIfNullSql('`t2`.`value`', '`t`.`value`'), - ]; - - if ($tableName != $this->_productIndexerHelper->getTable('catalog_product_entity')) { + + if ($tableName != $entityTableName) { $valueColumns = []; $ids = []; $select->from( - ['e' => $this->_productIndexerHelper->getTable('catalog_product_entity')], - $keyColumns - ); - - $selectValue->from( - ['e' => $this->_productIndexerHelper->getTable('catalog_product_entity')], - $keyColumns + ['e' => $entityTableName], + [ + 'entity_id' => 'e.entity_id', + 'attribute_id' => 't.attribute_id', + 'value' => 't.value' + ] ); /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ @@ -97,40 +95,35 @@ public function write($storeId, $productId, $valueFieldSuffix = '') $ids[$attribute->getId()] = $columnName; } } - $linkField = $this->getMetadataPool()->getMetadata(ProductInterface::class)->getLinkField(); - $select->joinLeft( + $select->joinInner( ['t' => $tableName], sprintf('e.%s = t.%s ', $linkField, $linkField) . $this->_connection->quoteInto( ' AND t.attribute_id IN (?)', array_keys($ids) - ) . ' AND t.store_id = 0', - [] - )->joinLeft( - ['t2' => $tableName], - sprintf('t.%s = t2.%s ', $linkField, $linkField) . - ' AND t.attribute_id = t2.attribute_id ' . - $this->_connection->quoteInto( - ' AND t2.store_id = ?', - $storeId - ), + ) . ' AND ' . $this->_connection->quoteInto('t.store_id IN(?)', [ + Store::DEFAULT_STORE_ID, + $storeId + ]), [] )->where( 'e.entity_id = ' . $productId - )->where( - 't.attribute_id IS NOT NULL' - ); + )->order('t.store_id ASC'); $cursor = $this->_connection->query($select); while ($row = $cursor->fetch(\Zend_Db::FETCH_ASSOC)) { $updateData[$ids[$row['attribute_id']]] = $row['value']; $valueColumnName = $ids[$row['attribute_id']] . $valueFieldSuffix; if (isset($describe[$valueColumnName])) { - $valueColumns[$row['value']] = $valueColumnName; + $valueColumns[$row['attribute_id']] = [ + 'value' => $row['value'], + 'column_name' => $valueColumnName + ]; } } //Update not simple attributes (eg. dropdown) if (!empty($valueColumns)) { - $valueIds = array_keys($valueColumns); + $valueIds = array_column($valueColumns, 'value'); + $optionIdToAttributeName = array_column($valueColumns, 'column_name', 'value'); $select = $this->_connection->select()->from( ['t' => $this->_productIndexerHelper->getTable('eav_attribute_option_value')], @@ -139,14 +132,14 @@ public function write($storeId, $productId, $valueFieldSuffix = '') $this->_connection->quoteInto('t.option_id IN (?)', $valueIds) )->where( $this->_connection->quoteInto('t.store_id IN(?)', [ - \Magento\Store\Model\Store::DEFAULT_STORE_ID, + Store::DEFAULT_STORE_ID, $storeId ]) ) ->order('t.store_id ASC'); $cursor = $this->_connection->query($select); while ($row = $cursor->fetch(\Zend_Db::FETCH_ASSOC)) { - $valueColumnName = $valueColumns[$row['option_id']]; + $valueColumnName = $optionIdToAttributeName[$row['option_id']]; if (isset($describe[$valueColumnName])) { $updateData[$valueColumnName] = $row['value']; } @@ -156,8 +149,9 @@ public function write($storeId, $productId, $valueFieldSuffix = '') $columnNames = array_keys($columns); $columnNames[] = 'attribute_set_id'; $columnNames[] = 'type_id'; + $columnNames[] = $linkField; $select->from( - ['e' => $this->_productIndexerHelper->getTable('catalog_product_entity')], + ['e' => $entityTableName], $columnNames )->where( 'e.entity_id = ' . $productId @@ -165,6 +159,7 @@ public function write($storeId, $productId, $valueFieldSuffix = '') $cursor = $this->_connection->query($select); $row = $cursor->fetch(\Zend_Db::FETCH_ASSOC); if (!empty($row)) { + $linkFieldId = $linkField; foreach ($row as $columnName => $value) { $updateData[$columnName] = $value; } @@ -175,7 +170,9 @@ public function write($storeId, $productId, $valueFieldSuffix = '') if (!empty($updateData)) { $updateData += ['entity_id' => $productId]; - $updateData += ['row_id' => $productId]; + if ($linkField !== $metadata->getIdentifierField()) { + $updateData += [$linkField => $linkFieldId]; + } $updateFields = []; foreach ($updateData as $key => $value) { $updateFields[$key] = $key; @@ -187,6 +184,8 @@ public function write($storeId, $productId, $valueFieldSuffix = '') } /** + * Get MetadataPool instance + * * @return \Magento\Framework\EntityManager\MetadataPool */ private function getMetadataPool() diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Row.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Row.php index b5dbdb68606f..64a7c4be4e03 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Row.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Action/Row.php @@ -5,16 +5,20 @@ */ namespace Magento\Catalog\Model\Indexer\Product\Flat\Action; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Indexer\Product\Flat\FlatTableBuilder; use Magento\Catalog\Model\Indexer\Product\Flat\TableBuilder; +use Magento\Framework\EntityManager\MetadataPool; /** - * Class Row reindex action + * Class Row reindex action. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Row extends \Magento\Catalog\Model\Indexer\Product\Flat\AbstractAction { /** - * @var \Magento\Catalog\Model\Indexer\Product\Flat\Action\Indexer + * @var Indexer */ protected $flatItemWriter; @@ -23,6 +27,11 @@ class Row extends \Magento\Catalog\Model\Indexer\Product\Flat\AbstractAction */ protected $flatItemEraser; + /** + * @var MetadataPool + */ + private $metadataPool; + /** * @param \Magento\Framework\App\ResourceConnection $resource * @param \Magento\Store\Model\StoreManagerInterface $storeManager @@ -32,6 +41,7 @@ class Row extends \Magento\Catalog\Model\Indexer\Product\Flat\AbstractAction * @param FlatTableBuilder $flatTableBuilder * @param Indexer $flatItemWriter * @param Eraser $flatItemEraser + * @param MetadataPool|null $metadataPool */ public function __construct( \Magento\Framework\App\ResourceConnection $resource, @@ -41,7 +51,8 @@ public function __construct( TableBuilder $tableBuilder, FlatTableBuilder $flatTableBuilder, Indexer $flatItemWriter, - Eraser $flatItemEraser + Eraser $flatItemEraser, + MetadataPool $metadataPool = null ) { parent::__construct( $resource, @@ -53,6 +64,8 @@ public function __construct( ); $this->flatItemWriter = $flatItemWriter; $this->flatItemEraser = $flatItemEraser; + $this->metadataPool = $metadataPool ?: + \Magento\Framework\App\ObjectManager::getInstance()->get(MetadataPool::class); } /** @@ -70,24 +83,50 @@ public function execute($id = null) ); } $ids = [$id]; - foreach ($this->_storeManager->getStores() as $store) { + $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); + + $stores = $this->_storeManager->getStores(); + foreach ($stores as $store) { $tableExists = $this->_isFlatTableExists($store->getId()); if ($tableExists) { $this->flatItemEraser->removeDeletedProducts($ids, $store->getId()); + $this->flatItemEraser->removeDisabledProducts($ids, $store->getId()); } - if (isset($ids[0])) { + + /* @var $status \Magento\Eav\Model\Entity\Attribute */ + $status = $this->_productIndexerHelper->getAttribute(ProductInterface::STATUS); + $statusTable = $status->getBackend()->getTable(); + $catalogProductEntityTable = $this->_productIndexerHelper->getTable('catalog_product_entity'); + $statusConditions = [ + 's.store_id IN(0,' . (int)$store->getId() . ')', + 's.attribute_id = ' . (int)$status->getId(), + 'e.entity_id = ' . (int)$id, + ]; + $select = $this->_connection->select(); + $select->from(['e' => $catalogProductEntityTable], ['s.value']) + ->where(implode(' AND ', $statusConditions)) + ->joinLeft(['s' => $statusTable], "e.{$linkField} = s.{$linkField}", []) + ->order('s.store_id DESC') + ->limit(1); + $result = $this->_connection->query($select); + $status = $result->fetchColumn(0); + + if ($status == \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) { if (!$tableExists) { $this->_flatTableBuilder->build( $store->getId(), - [$ids[0]], + $ids, $this->_valueFieldSuffix, $this->_tableDropSuffix, false ); } - $this->flatItemWriter->write($store->getId(), $ids[0], $this->_valueFieldSuffix); + $this->flatItemWriter->write($store->getId(), $id, $this->_valueFieldSuffix); + } else { + $this->flatItemEraser->deleteProductsFromStore($id, $store->getId()); } } + return $this; } } diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php index fbe0d4b550fa..2252b3e3d550 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php @@ -10,7 +10,8 @@ use Magento\Framework\EntityManager\MetadataPool; /** - * Class FlatTableBuilder + * Class for building flat index + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class FlatTableBuilder @@ -346,12 +347,21 @@ protected function _updateTemporaryTableByStoreValues( } //Update not simple attributes (eg. dropdown) - if (isset($flatColumns[$attributeCode . $valueFieldSuffix])) { - $select = $this->_connection->select()->joinInner( - ['t' => $this->_productIndexerHelper->getTable('eav_attribute_option_value')], - 't.option_id = et.' . $attributeCode . ' AND t.store_id=' . $storeId, - [$attributeCode . $valueFieldSuffix => 't.value'] - ); + $columnName = $attributeCode . $valueFieldSuffix; + if (isset($flatColumns[$columnName])) { + $columnValue = $this->_connection->getIfNullSql('ts.value', 't0.value'); + $select = $this->_connection->select(); + $select->joinLeft( + ['t0' => $this->_productIndexerHelper->getTable('eav_attribute_option_value')], + 't0.option_id = et.' . $attributeCode . ' AND t0.store_id = 0', + [] + )->joinLeft( + ['ts' => $this->_productIndexerHelper->getTable('eav_attribute_option_value')], + 'ts.option_id = et.' . $attributeCode . ' AND ts.store_id = ' . $storeId, + [] + )->columns( + [$columnName => $columnValue] + )->where($columnValue . ' IS NOT NULL'); if (!empty($changedIds)) { $select->where($this->_connection->quoteInto('et.entity_id IN (?)', $changedIds)); } @@ -374,6 +384,8 @@ protected function _getTemporaryTableName($tableName) } /** + * Get metadata pool + * * @return \Magento\Framework\EntityManager\MetadataPool */ private function getMetadataPool() diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/TableBuilder.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/TableBuilder.php index a32379b8c0a6..e6c098ab0254 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/TableBuilder.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/TableBuilder.php @@ -7,6 +7,9 @@ use Magento\Catalog\Model\Indexer\Product\Flat\Table\BuilderInterfaceFactory; +/** + * Class TableBuilder + */ class TableBuilder { /** @@ -112,7 +115,7 @@ public function build($storeId, $changedIds, $valueFieldSuffix) /** * Create empty temporary table with given columns list * - * @param string $tableName Table name + * @param string $tableName Table name * @param array $columns array('columnName' => \Magento\Catalog\Model\ResourceModel\Eav\Attribute, ...) * @param string $valueFieldSuffix * @@ -137,13 +140,23 @@ protected function _createTemporaryTable($tableName, array $columns, $valueField ); $flatColumns = $this->_productIndexerHelper->getFlatColumns(); - $temporaryTableBuilder->addColumn('entity_id', \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER); + $temporaryTableBuilder->addColumn( + 'entity_id', + \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, + null, + ['unsigned'=>true] + ); $temporaryTableBuilder->addColumn('type_id', \Magento\Framework\DB\Ddl\Table::TYPE_TEXT); $temporaryTableBuilder->addColumn('attribute_set_id', \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER); - $valueTemporaryTableBuilder->addColumn('entity_id', \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER); + $valueTemporaryTableBuilder->addColumn( + 'entity_id', + \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, + null, + ['unsigned'=>true] + ); /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ foreach ($columns as $columnName => $attribute) { @@ -198,9 +211,10 @@ protected function _getTemporaryTableName($tableName) * Fill temporary entity table * * @param string $tableName - * @param array $columns - * @param array $changedIds + * @param array $columns + * @param array $changedIds * @return void + * @throws \Exception */ protected function _fillTemporaryEntityTable($tableName, array $columns, array $changedIds = []) { @@ -244,11 +258,12 @@ protected function _addPrimaryKeyToTable($tableName, $columnName = 'entity_id') * Fill temporary table by data from products EAV attributes by type * * @param string $tableName - * @param array $tableColumns - * @param array $changedIds + * @param array $tableColumns + * @param array $changedIds * @param string $valueFieldSuffix * @param int $storeId * @return void + * @throws \Exception */ protected function _fillTemporaryTable( $tableName, @@ -289,12 +304,16 @@ protected function _fillTemporaryTable( /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ foreach ($columnsList as $columnName => $attribute) { - $countTableName = 't' . $iterationNum++; + $countTableName = 't' . ($iterationNum++); $joinCondition = sprintf( - 'e.%3$s = %1$s.%3$s AND %1$s.attribute_id = %2$d AND %1$s.store_id = 0', + 'e.%3$s = %1$s.%3$s' . + ' AND %1$s.attribute_id = %2$d' . + ' AND (%1$s.store_id = %4$d' . + ' OR %1$s.store_id = 0)', $countTableName, $attribute->getId(), - $metadata->getLinkField() + $metadata->getLinkField(), + $storeId ); $select->joinLeft( @@ -308,9 +327,10 @@ protected function _fillTemporaryTable( $columnValueName = $attributeCode . $valueFieldSuffix; if (isset($flatColumns[$columnValueName])) { $valueJoinCondition = sprintf( - 'e.%1$s = %2$s.option_id AND %2$s.store_id = 0', + 'e.%1$s = %2$s.option_id AND (%2$s.store_id = %3$d OR %2$s.store_id = 0)', $attributeCode, - $countTableName + $countTableName, + $storeId ); $selectValue->joinLeft( [ @@ -345,6 +365,8 @@ protected function _fillTemporaryTable( } /** + * Get Metadata Pool + * * @return \Magento\Framework\EntityManager\MetadataPool * @deprecated 101.1.0 */ 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 1a7575157065..858eba3ab217 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php @@ -3,41 +3,64 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Catalog\Model\Indexer\Product\Price\Action; +use Magento\Catalog\Model\Indexer\Product\Price\AbstractAction; +use Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory; +use Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BatchSizeCalculator; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Factory; +use Magento\Directory\Model\CurrencyFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\PriceInterface; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Query\BatchIterator; +use Magento\Framework\DB\Query\Generator as QueryGenerator; +use Magento\Framework\DB\Select; use Magento\Framework\EntityManager\EntityMetadataInterface; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Indexer\BatchProviderInterface; use Magento\Framework\Indexer\DimensionalIndexerInterface; use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider; +use Magento\Framework\Stdlib\DateTime; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Indexer\Model\ProcessManager; use Magento\Store\Model\Indexer\WebsiteDimensionProvider; +use Magento\Store\Model\StoreManagerInterface; /** * Class Full reindex action * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Full extends \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction +class Full extends AbstractAction { /** - * @var \Magento\Framework\EntityManager\MetadataPool + * @var MetadataPool */ private $metadataPool; /** - * @var \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BatchSizeCalculator + * @var BatchSizeCalculator */ private $batchSizeCalculator; /** - * @var \Magento\Framework\Indexer\BatchProviderInterface + * @var BatchProviderInterface */ private $batchProvider; /** - * @var \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher + * @var ActiveTableSwitcher */ private $activeTableSwitcher; @@ -47,54 +70,61 @@ class Full extends \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction private $productMetaDataCached; /** - * @var \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory + * @var DimensionCollectionFactory */ private $dimensionCollectionFactory; /** - * @var \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer + * @var TableMaintainer */ private $dimensionTableMaintainer; /** - * @var \Magento\Indexer\Model\ProcessManager + * @var ProcessManager */ private $processManager; /** - * @param \Magento\Framework\App\Config\ScopeConfigInterface $config - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Directory\Model\CurrencyFactory $currencyFactory - * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate - * @param \Magento\Framework\Stdlib\DateTime $dateTime - * @param \Magento\Catalog\Model\Product\Type $catalogProductType - * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Factory $indexerPriceFactory - * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice $defaultIndexerResource - * @param \Magento\Framework\EntityManager\MetadataPool|null $metadataPool - * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BatchSizeCalculator|null $batchSizeCalculator - * @param \Magento\Framework\Indexer\BatchProviderInterface|null $batchProvider - * @param \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher|null $activeTableSwitcher - * @param \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory|null $dimensionCollectionFactory - * @param \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer|null $dimensionTableMaintainer - * @param \Magento\Indexer\Model\ProcessManager $processManager + * @var QueryGenerator|null + */ + private $batchQueryGenerator; + + /** + * @param ScopeConfigInterface $config + * @param StoreManagerInterface $storeManager + * @param CurrencyFactory $currencyFactory + * @param TimezoneInterface $localeDate + * @param DateTime $dateTime + * @param Type $catalogProductType + * @param Factory $indexerPriceFactory + * @param DefaultPrice $defaultIndexerResource + * @param MetadataPool|null $metadataPool + * @param BatchSizeCalculator|null $batchSizeCalculator + * @param BatchProviderInterface|null $batchProvider + * @param ActiveTableSwitcher|null $activeTableSwitcher + * @param DimensionCollectionFactory|null $dimensionCollectionFactory + * @param TableMaintainer|null $dimensionTableMaintainer + * @param ProcessManager $processManager + * @param QueryGenerator|null $batchQueryGenerator * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Framework\App\Config\ScopeConfigInterface $config, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Directory\Model\CurrencyFactory $currencyFactory, - \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, - \Magento\Framework\Stdlib\DateTime $dateTime, - \Magento\Catalog\Model\Product\Type $catalogProductType, - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Factory $indexerPriceFactory, - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice $defaultIndexerResource, - \Magento\Framework\EntityManager\MetadataPool $metadataPool = null, - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BatchSizeCalculator $batchSizeCalculator = null, - \Magento\Framework\Indexer\BatchProviderInterface $batchProvider = null, - \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher $activeTableSwitcher = null, - \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory $dimensionCollectionFactory = null, - \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer $dimensionTableMaintainer = null, - \Magento\Indexer\Model\ProcessManager $processManager = null + ScopeConfigInterface $config, + StoreManagerInterface $storeManager, + CurrencyFactory $currencyFactory, + TimezoneInterface $localeDate, + DateTime $dateTime, + Type $catalogProductType, + Factory $indexerPriceFactory, + DefaultPrice $defaultIndexerResource, + MetadataPool $metadataPool = null, + BatchSizeCalculator $batchSizeCalculator = null, + BatchProviderInterface $batchProvider = null, + ActiveTableSwitcher $activeTableSwitcher = null, + DimensionCollectionFactory $dimensionCollectionFactory = null, + TableMaintainer $dimensionTableMaintainer = null, + ProcessManager $processManager = null, + QueryGenerator $batchQueryGenerator = null ) { parent::__construct( $config, @@ -107,26 +137,27 @@ public function __construct( $defaultIndexerResource ); $this->metadataPool = $metadataPool ?: ObjectManager::getInstance()->get( - \Magento\Framework\EntityManager\MetadataPool::class + MetadataPool::class ); $this->batchSizeCalculator = $batchSizeCalculator ?: ObjectManager::getInstance()->get( - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BatchSizeCalculator::class + BatchSizeCalculator::class ); $this->batchProvider = $batchProvider ?: ObjectManager::getInstance()->get( - \Magento\Framework\Indexer\BatchProviderInterface::class + BatchProviderInterface::class ); $this->activeTableSwitcher = $activeTableSwitcher ?: ObjectManager::getInstance()->get( - \Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher::class + ActiveTableSwitcher::class ); $this->dimensionCollectionFactory = $dimensionCollectionFactory ?: ObjectManager::getInstance()->get( - \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory::class + DimensionCollectionFactory::class ); $this->dimensionTableMaintainer = $dimensionTableMaintainer ?: ObjectManager::getInstance()->get( - \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer::class + TableMaintainer::class ); $this->processManager = $processManager ?: ObjectManager::getInstance()->get( - \Magento\Indexer\Model\ProcessManager::class + ProcessManager::class ); + $this->batchQueryGenerator = $batchQueryGenerator ?? ObjectManager::getInstance()->get(QueryGenerator::class); } /** @@ -137,13 +168,13 @@ public function __construct( * @throws \Exception * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function execute($ids = null) + public function execute($ids = null): void { try { //Prepare indexer tables before full reindex $this->prepareTables(); - /** @var \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice $indexer */ + /** @var DefaultPrice $indexer */ foreach ($this->getTypeIndexers(true) as $typeId => $priceIndexer) { if ($priceIndexer instanceof DimensionalIndexerInterface) { //New price reindex mechanism @@ -170,7 +201,7 @@ public function execute($ids = null) * @return void * @throws \Exception */ - private function prepareTables() + private function prepareTables(): void { $this->_defaultIndexerResource->getTableStrategy()->setUseIdxTable(false); @@ -185,7 +216,7 @@ private function prepareTables() * @return void * @throws \Exception */ - private function truncateReplicaTables() + private function truncateReplicaTables(): void { foreach ($this->dimensionCollectionFactory->create() as $dimension) { $dimensionTable = $this->dimensionTableMaintainer->getMainReplicaTable($dimension); @@ -202,12 +233,12 @@ private function truncateReplicaTables() * @return void * @throws \Exception */ - private function reindexProductTypeWithDimensions(DimensionalIndexerInterface $priceIndexer, string $typeId) + private function reindexProductTypeWithDimensions(DimensionalIndexerInterface $priceIndexer, string $typeId): void { $userFunctions = []; foreach ($this->dimensionCollectionFactory->create() as $dimensions) { $userFunctions[] = function () use ($priceIndexer, $dimensions, $typeId) { - return $this->reindexByBatches($priceIndexer, $dimensions, $typeId); + $this->reindexByBatches($priceIndexer, $dimensions, $typeId); }; } $this->processManager->execute($userFunctions); @@ -223,10 +254,13 @@ private function reindexProductTypeWithDimensions(DimensionalIndexerInterface $p * @return void * @throws \Exception */ - private function reindexByBatches(DimensionalIndexerInterface $priceIndexer, array $dimensions, string $typeId) - { + private function reindexByBatches( + DimensionalIndexerInterface $priceIndexer, + array $dimensions, + string $typeId + ): void { foreach ($this->getBatchesForIndexer($typeId) as $batch) { - $this->reindexByBatchWithDimensions($priceIndexer, $batch, $dimensions, $typeId); + $this->reindexByBatchWithDimensions($priceIndexer, $batch, $dimensions); } } @@ -235,16 +269,20 @@ private function reindexByBatches(DimensionalIndexerInterface $priceIndexer, arr * * @param string $typeId * - * @return \Generator + * @return BatchIterator * @throws \Exception */ - private function getBatchesForIndexer(string $typeId) + private function getBatchesForIndexer(string $typeId): BatchIterator { $connection = $this->_defaultIndexerResource->getConnection(); - return $this->batchProvider->getBatches( - $connection, - $this->getProductMetaData()->getEntityTable(), + $entityMetadata = $this->getProductMetaData(); + $select = $connection->select(); + $select->distinct(true); + $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); + + return $this->batchQueryGenerator->generate( $this->getProductMetaData()->getIdentifierField(), + $select, $this->batchSizeCalculator->estimateBatchSize( $connection, $typeId @@ -256,20 +294,18 @@ private function getBatchesForIndexer(string $typeId) * Reindex by batch for new 'Dimensional' price indexer * * @param DimensionalIndexerInterface $priceIndexer - * @param array $batch + * @param Select $batchQuery * @param array $dimensions - * @param string $typeId * * @return void * @throws \Exception */ private function reindexByBatchWithDimensions( DimensionalIndexerInterface $priceIndexer, - array $batch, - array $dimensions, - string $typeId - ) { - $entityIds = $this->getEntityIdsFromBatch($typeId, $batch); + Select $batchQuery, + array $dimensions + ): void { + $entityIds = $this->getEntityIdsFromBatch($batchQuery); if (!empty($entityIds)) { $this->dimensionTableMaintainer->createMainTmpTable($dimensions); @@ -295,10 +331,10 @@ private function reindexByBatchWithDimensions( * @return void * @throws \Exception */ - private function reindexProductType(PriceInterface $priceIndexer, string $typeId) + private function reindexProductType(PriceInterface $priceIndexer, string $typeId): void { foreach ($this->getBatchesForIndexer($typeId) as $batch) { - $this->reindexBatch($priceIndexer, $batch, $typeId); + $this->reindexBatch($priceIndexer, $batch); } } @@ -306,15 +342,13 @@ private function reindexProductType(PriceInterface $priceIndexer, string $typeId * Reindex by batch for old price indexer * * @param PriceInterface $priceIndexer - * @param array $batch - * @param string $typeId - * + * @param Select $batch * @return void * @throws \Exception */ - private function reindexBatch(PriceInterface $priceIndexer, array $batch, string $typeId) + private function reindexBatch(PriceInterface $priceIndexer, Select $batch): void { - $entityIds = $this->getEntityIdsFromBatch($typeId, $batch); + $entityIds = $this->getEntityIdsFromBatch($batch); if (!empty($entityIds)) { // Temporary table will created if not exists @@ -339,27 +373,15 @@ private function reindexBatch(PriceInterface $priceIndexer, array $batch, string /** * Get Entity Ids from batch * - * @param string $typeId - * @param array $batch - * + * @param Select $batch * @return array * @throws \Exception */ - private function getEntityIdsFromBatch(string $typeId, array $batch) + private function getEntityIdsFromBatch(Select $batch): array { $connection = $this->_defaultIndexerResource->getConnection(); - // Get entity ids from batch - $select = $connection - ->select() - ->distinct(true) - ->from( - ['e' => $this->getProductMetaData()->getEntityTable()], - $this->getProductMetaData()->getIdentifierField() - ) - ->where('type_id = ?', $typeId); - - return $this->batchProvider->getBatchIds($connection, $select, $batch); + return $connection->fetchCol($batch); } /** @@ -368,7 +390,7 @@ private function getEntityIdsFromBatch(string $typeId, array $batch) * @return EntityMetadataInterface * @throws \Exception */ - private function getProductMetaData() + private function getProductMetaData(): EntityMetadataInterface { if ($this->productMetaDataCached === null) { $this->productMetaDataCached = $this->metadataPool->getMetadata(ProductInterface::class); @@ -383,7 +405,7 @@ private function getProductMetaData() * @return string * @throws \Exception */ - private function getReplicaTable() + private function getReplicaTable(): string { return $this->activeTableSwitcher->getAdditionalTableName( $this->_defaultIndexerResource->getMainTable() @@ -394,8 +416,9 @@ private function getReplicaTable() * Replacement of tables from replica to main * * @return void + * @throws \Zend_Db_Statement_Exception */ - private function switchTables() + private function switchTables(): void { // Switch dimension tables $mainTablesByDimension = []; @@ -417,13 +440,14 @@ private function switchTables() /** * Move data from old price indexer mechanism to new indexer mechanism by dimensions. + * * Used only for backward compatibility * * @param array $dimensions - * * @return void + * @throws \Zend_Db_Statement_Exception */ - private function moveDataFromReplicaTableToReplicaTables(array $dimensions) + private function moveDataFromReplicaTableToReplicaTables(array $dimensions): void { if (!$dimensions) { return; @@ -455,17 +479,17 @@ private function moveDataFromReplicaTableToReplicaTables(array $dimensions) $select, $replicaTablesByDimension, [], - \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE + AdapterInterface::INSERT_ON_DUPLICATE ) ); } /** - * @deprecated + * Retrieves the index table that should be used * - * @inheritdoc + * @deprecated */ - protected function getIndexTargetTable() + protected function getIndexTargetTable(): string { return $this->activeTableSwitcher->getAdditionalTableName($this->_defaultIndexerResource->getMainTable()); } diff --git a/app/code/Magento/Catalog/Model/Layer/Filter/AbstractFilter.php b/app/code/Magento/Catalog/Model/Layer/Filter/AbstractFilter.php index d21a8666ec0a..f2e2e67f944e 100644 --- a/app/code/Magento/Catalog/Model/Layer/Filter/AbstractFilter.php +++ b/app/code/Magento/Catalog/Model/Layer/Filter/AbstractFilter.php @@ -139,7 +139,7 @@ public function apply(\Magento\Framework\App\RequestInterface $request) } /** - * Get fiter items count + * Get filter items count * * @return int */ diff --git a/app/code/Magento/Catalog/Model/Layer/Filter/Decimal.php b/app/code/Magento/Catalog/Model/Layer/Filter/Decimal.php index dac2632ff6db..d76711cb21db 100644 --- a/app/code/Magento/Catalog/Model/Layer/Filter/Decimal.php +++ b/app/code/Magento/Catalog/Model/Layer/Filter/Decimal.php @@ -32,8 +32,8 @@ class Decimal extends \Magento\Catalog\Model\Layer\Filter\AbstractFilter * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Catalog\Model\Layer $layer * @param \Magento\Catalog\Model\Layer\Filter\Item\DataBuilder $itemDataBuilder - * @param \Magento\Catalog\Model\ResourceModel\Layer\Filter\DecimalFactory $filterDecimalFactory * @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency + * @param \Magento\Catalog\Model\Layer\Filter\DataProvider\DecimalFactory $dataProviderFactory * @param array $data */ public function __construct( diff --git a/app/code/Magento/Catalog/Model/Layer/Filter/Item/DataBuilder.php b/app/code/Magento/Catalog/Model/Layer/Filter/Item/DataBuilder.php index 4d2878b0b1e8..07c9c2eaa249 100644 --- a/app/code/Magento/Catalog/Model/Layer/Filter/Item/DataBuilder.php +++ b/app/code/Magento/Catalog/Model/Layer/Filter/Item/DataBuilder.php @@ -4,11 +4,11 @@ * See COPYING.txt for license details. */ +namespace Magento\Catalog\Model\Layer\Filter\Item; + /** * Item Data Builder */ -namespace Magento\Catalog\Model\Layer\Filter\Item; - class DataBuilder { /** @@ -29,7 +29,7 @@ class DataBuilder * Add Item Data * * @param string $label - * @param string $label + * @param string $value * @param int $count * @return void */ diff --git a/app/code/Magento/Catalog/Model/Plugin/ProductRepository/TransactionWrapper.php b/app/code/Magento/Catalog/Model/Plugin/ProductRepository/TransactionWrapper.php index c88215d92357..f51b2e4f90a6 100644 --- a/app/code/Magento/Catalog/Model/Plugin/ProductRepository/TransactionWrapper.php +++ b/app/code/Magento/Catalog/Model/Plugin/ProductRepository/TransactionWrapper.php @@ -7,6 +7,9 @@ */ namespace Magento\Catalog\Model\Plugin\ProductRepository; +/** + * Transaction wrapper for product repository CRUD. + */ class TransactionWrapper { /** @@ -24,8 +27,10 @@ public function __construct( } /** + * Transaction wrapper for save action. + * * @param \Magento\Catalog\Api\ProductRepositoryInterface $subject - * @param callable $proceed + * @param \Closure $proceed * @param \Magento\Catalog\Api\Data\ProductInterface $product * @param bool $saveOptions * @return \Magento\Catalog\Api\Data\ProductInterface @@ -51,8 +56,10 @@ public function aroundSave( } /** + * Transaction wrapper for delete action. + * * @param \Magento\Catalog\Api\ProductRepositoryInterface $subject - * @param callable $proceed + * @param \Closure $proceed * @param \Magento\Catalog\Api\Data\ProductInterface $product * @return bool * @throws \Exception @@ -76,8 +83,10 @@ public function aroundDelete( } /** + * Transaction wrapper for delete by id action. + * * @param \Magento\Catalog\Api\ProductRepositoryInterface $subject - * @param callable $proceed + * @param \Closure $proceed * @param string $productSku * @return bool * @throws \Exception diff --git a/app/code/Magento/Catalog/Model/Product.php b/app/code/Magento/Catalog/Model/Product.php index b29eee9a47f8..1e774e45df41 100644 --- a/app/code/Magento/Catalog/Model/Product.php +++ b/app/code/Magento/Catalog/Model/Product.php @@ -498,21 +498,26 @@ protected function _getResource() } /** - * Get a list of custom attribute codes that belongs to product attribute set. If attribute set not specified for - * product will return all product attribute codes + * Get a list of custom attribute codes that belongs to product attribute set. + * + * If attribute set not specified for product will return all product attribute codes * * @return string[] */ protected function getCustomAttributesCodes() { if ($this->customAttributesCodes === null) { - $this->customAttributesCodes = array_keys($this->eavConfig->getEntityAttributes( - self::ENTITY, - $this - )); - - $this->customAttributesCodes = $this->filterCustomAttribute->execute($this->customAttributesCodes); - $this->customAttributesCodes = array_diff($this->customAttributesCodes, ProductInterface::ATTRIBUTES); + $this->customAttributesCodes = array_diff( + array_keys( + $this->filterCustomAttribute->execute( + $this->eavConfig->getEntityAttributes( + self::ENTITY, + $this + ) + ) + ), + ProductInterface::ATTRIBUTES + ); } return $this->customAttributesCodes; @@ -526,9 +531,9 @@ protected function getCustomAttributesCodes() public function getStoreId() { if ($this->hasData(self::STORE_ID)) { - return $this->getData(self::STORE_ID); + return (int)$this->getData(self::STORE_ID); } - return $this->_storeManager->getStore()->getId(); + return (int)$this->_storeManager->getStore()->getId(); } /** @@ -584,8 +589,9 @@ public function getPrice() } /** - * @codeCoverageIgnoreStart * Get visibility status + * + * @codeCoverageIgnoreStart * @see \Magento\Catalog\Model\Product\Visibility * * @return int @@ -662,6 +668,7 @@ public function getStatus() /** * Retrieve type instance of the product. + * * Type instance implements product type depended logic and is a singleton shared by all products of the same type. * * @return \Magento\Catalog\Model\Product\Type\AbstractType @@ -715,7 +722,7 @@ public function getIdBySku($sku) public function getCategoryId() { $category = $this->_registry->registry('current_category'); - if ($category) { + if ($category && in_array($category->getId(), $this->getCategoryIds())) { return $category->getId(); } return false; @@ -810,6 +817,9 @@ public function getStoreIds() if (!$this->hasStoreIds()) { $storeIds = []; if ($websiteIds = $this->getWebsiteIds()) { + if ($this->_storeManager->isSingleStoreMode()) { + $websiteIds = array_keys($websiteIds); + } foreach ($websiteIds as $websiteId) { $websiteStores = $this->_storeManager->getWebsite($websiteId)->getStoreIds(); $storeIds = array_merge($storeIds, $websiteStores); @@ -822,9 +832,10 @@ public function getStoreIds() /** * Retrieve product attributes - * if $groupId is null - retrieve all product attributes * - * @param int $groupId Retrieve attributes of the specified group + * If $groupId is null - retrieve all product attributes + * + * @param int $groupId Retrieve attributes of the specified group * @param bool $skipSuper Not used * @return \Magento\Eav\Model\Entity\Attribute\AbstractAttribute[] * @SuppressWarnings(PHPMD.UnusedFormalParameter) @@ -916,10 +927,11 @@ public function beforeSave() /** * Check/set if options can be affected when saving product + * * If value specified, it will be set. * - * @param bool $value - * @return bool + * @param bool $value + * @return bool */ public function canAffectOptions($value = null) { @@ -976,7 +988,7 @@ public function setQty($qty) */ public function getQty() { - return $this->getData('qty'); + return (float)$this->getData('qty'); } /** @@ -1036,9 +1048,11 @@ public function reindex() /** * Clear cache related with product and protect delete from not admin + * * Register indexing event before delete product * * @return \Magento\Catalog\Model\Product + * @throws \Magento\Framework\Exception\LocalizedException */ public function beforeDelete() { @@ -1545,12 +1559,13 @@ public function hasGalleryAttribute() /** * Add image to media gallery * - * @param string $file file path of image in file system - * @param string|array $mediaAttribute code of attribute with type 'media_image', - * leave blank if image should be only in gallery - * @param boolean $move if true, it will move source file - * @param boolean $exclude mark image as disabled in product page view + * @param string $file file path of image in file system + * @param string|array $mediaAttribute code of attribute with type 'media_image', + * leave blank if image should be only in gallery + * @param bool $move if true, it will move source file + * @param bool $exclude mark image as disabled in product page view * @return \Magento\Catalog\Model\Product + * @throws \Magento\Framework\Exception\LocalizedException */ public function addImageToMediaGallery($file, $mediaAttribute = null, $move = false, $exclude = true) { @@ -1711,7 +1726,6 @@ public function getIsSalable() /** * Check is a virtual product - * Data helper wrapper * * @return bool */ @@ -1804,8 +1818,8 @@ public function formatUrlKey($str) * Save current attribute with code $code and assign new value * * @param string $code Attribute code - * @param mixed $value New attribute value - * @param int $store Store ID + * @param mixed $value New attribute value + * @param int $store Store ID * @return void */ public function addAttributeUpdate($code, $value, $store) @@ -1875,6 +1889,7 @@ public function getRequestPath() /** * Custom function for other modules + * * @return string */ public function getGiftMessageAvailable() @@ -1993,6 +2008,8 @@ public function getOptions() } /** + * Set product options + * * @param \Magento\Catalog\Api\Data\ProductCustomOptionInterface[] $options * @return $this */ @@ -2016,10 +2033,10 @@ public function getIsVirtual() /** * Add custom option information to product * - * @param string $code Option code - * @param mixed $value Value of the option - * @param int|Product $product Product ID - * @return $this + * @param string $code Option code + * @param mixed $value Value of the option + * @param int|Product|null $product Product ID + * @return $this */ public function addCustomOption($code, $value, $product = null) { @@ -2213,6 +2230,7 @@ public function getPreconfiguredValues() /** * Prepare product custom options. + * * To be sure that all product custom options does not has ID and has product instance * * @return \Magento\Catalog\Model\Product @@ -2547,9 +2565,9 @@ public function setTypeId($typeId) } /** - * {@inheritdoc} + * @inheritdoc * - * @return \Magento\Catalog\Api\Data\ProductExtensionInterface + * @return \Magento\Framework\Api\ExtensionAttributesInterface */ public function getExtensionAttributes() { @@ -2557,7 +2575,7 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * @inheritdoc * * @param \Magento\Catalog\Api\Data\ProductExtensionInterface $extensionAttributes * @return $this @@ -2570,8 +2588,11 @@ public function setExtensionAttributes(\Magento\Catalog\Api\Data\ProductExtensio //@codeCoverageIgnoreEnd /** + * Convert Image to ProductAttributeMediaGalleryEntryInterface + * * @param array $mediaGallery * @return \Magento\Catalog\Api\Data\ProductAttributeMediaGalleryEntryInterface[] + * @throws \Magento\Framework\Exception\LocalizedException */ protected function convertToMediaGalleryInterface(array $mediaGallery) { @@ -2587,7 +2608,10 @@ protected function convertToMediaGalleryInterface(array $mediaGallery) } /** + * Get media gallery entries + * * @return \Magento\Catalog\Api\Data\ProductAttributeMediaGalleryEntryInterface[]|null + * @throws \Magento\Framework\Exception\LocalizedException */ public function getMediaGalleryEntries() { @@ -2601,8 +2625,11 @@ public function getMediaGalleryEntries() } /** + * Set media gallery entries + * * @param ProductAttributeMediaGalleryEntryInterface[] $mediaGalleryEntries * @return $this + * @throws \Magento\Framework\Exception\LocalizedException */ public function setMediaGalleryEntries(array $mediaGalleryEntries = null) { @@ -2643,6 +2670,8 @@ public function setId($value) } /** + * Get link repository + * * @return ProductLinkRepositoryInterface */ private function getLinkRepository() @@ -2655,6 +2684,8 @@ private function getLinkRepository() } /** + * Get media gallery processor + * * @return Product\Gallery\Processor */ private function getMediaGalleryProcessor() diff --git a/app/code/Magento/Catalog/Model/Product/Action.php b/app/code/Magento/Catalog/Model/Product/Action.php index f78048424b42..3863cf245724 100644 --- a/app/code/Magento/Catalog/Model/Product/Action.php +++ b/app/code/Magento/Catalog/Model/Product/Action.php @@ -168,5 +168,7 @@ public function updateWebsites($productIds, $websiteIds, $type) if (!$categoryIndexer->isScheduled()) { $categoryIndexer->reindexList(array_unique($productIds)); } + + $this->_eventManager->dispatch('catalog_product_to_website_change', ['products' => $productIds]); } } diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Boolean.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Boolean.php index c2108b0273bd..be1e523960f2 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Boolean.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Boolean.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Model\Product\Attribute\Backend; use Magento\Catalog\Model\Product\Attribute\Source\Boolean as BooleanSource; @@ -25,7 +27,9 @@ public function beforeSave($object) $attributeCode = $this->getAttribute()->getName(); if ($object->getData('use_config_' . $attributeCode)) { $object->setData($attributeCode, BooleanSource::VALUE_USE_CONFIG); + return $this; } - return $this; + + return parent::beforeSave($object); } } diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/GroupPrice/AbstractGroupPrice.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/GroupPrice/AbstractGroupPrice.php index 3779cab431cb..e26717e47274 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/GroupPrice/AbstractGroupPrice.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/GroupPrice/AbstractGroupPrice.php @@ -247,6 +247,8 @@ public function validate($object) } /** + * Validate price. + * * @param array $priceRow * @return void * @throws \Magento\Framework\Exception\LocalizedException @@ -312,6 +314,8 @@ public function afterLoad($object) } /** + * Get website id. + * * @param int $storeId * @return int|null */ @@ -327,6 +331,8 @@ private function getWebsiteId($storeId) } /** + * Set price data. + * * @param \Magento\Catalog\Model\Product $object * @param array $priceData */ @@ -373,122 +379,16 @@ protected function modifyPriceData($object, $data) * * @param \Magento\Catalog\Model\Product $object * @return $this - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function afterSave($object) { - $websiteId = $this->_storeManager->getStore($object->getStoreId())->getWebsiteId(); - $isGlobal = $this->getAttribute()->isScopeGlobal() || $websiteId == 0; - - $priceRows = $object->getData($this->getAttribute()->getName()); - if (null === $priceRows) { - return $this; - } - - $priceRows = array_filter((array)$priceRows); - - $old = []; - $new = []; - - // prepare original data for compare - $origPrices = $object->getOrigData($this->getAttribute()->getName()); - if (!is_array($origPrices)) { - $origPrices = []; - } - foreach ($origPrices as $data) { - if ($data['website_id'] > 0 || $data['website_id'] == '0' && $isGlobal) { - $key = implode( - '-', - array_merge( - [$data['website_id'], $data['cust_group']], - $this->_getAdditionalUniqueFields($data) - ) - ); - $old[$key] = $data; - } - } - - // prepare data for save - foreach ($priceRows as $data) { - $hasEmptyData = false; - foreach ($this->_getAdditionalUniqueFields($data) as $field) { - if (empty($field)) { - $hasEmptyData = true; - break; - } - } - - if ($hasEmptyData || !isset($data['cust_group']) || !empty($data['delete'])) { - continue; - } - if ($this->getAttribute()->isScopeGlobal() && $data['website_id'] > 0) { - continue; - } - if (!$isGlobal && (int)$data['website_id'] == 0) { - continue; - } - - $key = implode( - '-', - array_merge([$data['website_id'], $data['cust_group']], $this->_getAdditionalUniqueFields($data)) - ); - - $useForAllGroups = $data['cust_group'] == $this->_groupManagement->getAllCustomersGroup()->getId(); - $customerGroupId = !$useForAllGroups ? $data['cust_group'] : 0; - $new[$key] = array_merge( - $this->getAdditionalFields($data), - [ - 'website_id' => $data['website_id'], - 'all_groups' => $useForAllGroups ? 1 : 0, - 'customer_group_id' => $customerGroupId, - 'value' => isset($data['price']) ? $data['price'] : null, - ], - $this->_getAdditionalUniqueFields($data) - ); - } - - $delete = array_diff_key($old, $new); - $insert = array_diff_key($new, $old); - $update = array_intersect_key($new, $old); - - $isChanged = false; - $productId = $object->getData($this->getMetadataPool()->getMetadata(ProductInterface::class)->getLinkField()); - - if (!empty($delete)) { - foreach ($delete as $data) { - $this->_getResource()->deletePriceData($productId, null, $data['price_id']); - $isChanged = true; - } - } - - if (!empty($insert)) { - foreach ($insert as $data) { - $price = new \Magento\Framework\DataObject($data); - $price->setData( - $this->getMetadataPool()->getMetadata(ProductInterface::class)->getLinkField(), - $productId - ); - $this->_getResource()->savePriceData($price); - - $isChanged = true; - } - } - - if (!empty($update)) { - $isChanged |= $this->updateValues($update, $old); - } - - if ($isChanged) { - $valueChangedKey = $this->getAttribute()->getName() . '_changed'; - $object->setData($valueChangedKey, 1); - } - return $this; } /** + * Update values. + * * @param array $valuesToUpdate * @param array $oldValues * @return boolean @@ -544,6 +444,8 @@ public function getResource() } /** + * Get metadata pool. + * * @return \Magento\Framework\EntityManager\MetadataPool */ private function getMetadataPool() diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Sku.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Sku.php index a652d0ef9021..98738e055ca8 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Sku.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Sku.php @@ -4,15 +4,13 @@ * See COPYING.txt for license details. */ -/** - * Catalog product SKU backend attribute model - * - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Catalog\Model\Product\Attribute\Backend; use Magento\Catalog\Model\Product; +/** + * Catalog product SKU backend attribute model. + */ class Sku extends \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend { /** @@ -97,6 +95,7 @@ protected function _generateUniqueSku($object) public function beforeSave($object) { $this->_generateUniqueSku($object); + $this->trimValue($object); return parent::beforeSave($object); } @@ -127,4 +126,19 @@ protected function _getLastSimilarAttributeValueIncrement($attribute, $object) $data = $connection->fetchOne($select, $bind); return abs((int)str_replace($value, '', $data)); } + + /** + * Remove extra spaces from attribute value before save. + * + * @param Product $object + * @return void + */ + private function trimValue($object) + { + $attrCode = $this->getAttribute()->getAttributeCode(); + $value = $object->getData($attrCode); + if ($value) { + $object->setData($attrCode, trim($value)); + } + } } diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/AbstractHandler.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/AbstractHandler.php new file mode 100644 index 000000000000..fc0f090937db --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/AbstractHandler.php @@ -0,0 +1,101 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Product\Attribute\Backend\TierPrice; + +use Magento\Framework\EntityManager\Operation\ExtensionInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Customer\Api\GroupManagementInterface; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice; + +/** + * Tier price data abstract handler. + */ +abstract class AbstractHandler implements ExtensionInterface +{ + /** + * @var \Magento\Customer\Api\GroupManagementInterface + */ + protected $groupManagement; + + /** + * @param \Magento\Customer\Api\GroupManagementInterface $groupManagement + */ + public function __construct( + GroupManagementInterface $groupManagement + ) { + $this->groupManagement = $groupManagement; + } + + /** + * Get additional tier price fields. + * + * @param array $objectArray + * @return array + */ + protected function getAdditionalFields(array $objectArray): array + { + $percentageValue = $this->getPercentage($objectArray); + + return [ + 'value' => $percentageValue ? null : $objectArray['price'], + 'percentage_value' => $percentageValue ?: null, + ]; + } + + /** + * Check whether price has percentage value. + * + * @param array $priceRow + * @return float|null + */ + protected function getPercentage(array $priceRow): ?float + { + return isset($priceRow['percentage_value']) && is_numeric($priceRow['percentage_value']) + ? (float)$priceRow['percentage_value'] + : null; + } + + /** + * Prepare tier price data by provided price row data. + * + * @param array $data + * @return array + * @throws \Magento\Framework\Exception\LocalizedException + */ + protected function prepareTierPrice(array $data): array + { + $useForAllGroups = (int)$data['cust_group'] === $this->groupManagement->getAllCustomersGroup()->getId(); + $customerGroupId = $useForAllGroups ? 0 : $data['cust_group']; + $tierPrice = array_merge( + $this->getAdditionalFields($data), + [ + 'website_id' => $data['website_id'], + 'all_groups' => (int)$useForAllGroups, + 'customer_group_id' => $customerGroupId, + 'value' => $data['price'] ?? null, + 'qty' => $this->parseQty($data['price_qty']), + ] + ); + + return $tierPrice; + } + + /** + * Parse quantity value into float. + * + * @param mixed $value + * @return float|int + */ + protected function parseQty($value) + { + return $value * 1; + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/SaveHandler.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/SaveHandler.php new file mode 100644 index 000000000000..9cb2ac014589 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/SaveHandler.php @@ -0,0 +1,112 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Product\Attribute\Backend\TierPrice; + +use Magento\Framework\EntityManager\Operation\ExtensionInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Customer\Api\GroupManagementInterface; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice; + +/** + * Process tier price data for handled new product + */ +class SaveHandler extends AbstractHandler +{ + /** + * @var \Magento\Store\Model\StoreManagerInterface + */ + private $storeManager; + + /** + * @var \Magento\Catalog\Api\ProductAttributeRepositoryInterface + */ + private $attributeRepository; + + /** + * @var \Magento\Framework\EntityManager\MetadataPool + */ + private $metadataPoll; + + /** + * @var \Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice + */ + private $tierPriceResource; + + /** + * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param \Magento\Catalog\Api\ProductAttributeRepositoryInterface $attributeRepository + * @param \Magento\Customer\Api\GroupManagementInterface $groupManagement + * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool + * @param \Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice $tierPriceResource + */ + public function __construct( + StoreManagerInterface $storeManager, + ProductAttributeRepositoryInterface $attributeRepository, + GroupManagementInterface $groupManagement, + MetadataPool $metadataPool, + Tierprice $tierPriceResource + ) { + parent::__construct($groupManagement); + + $this->storeManager = $storeManager; + $this->attributeRepository = $attributeRepository; + $this->metadataPoll = $metadataPool; + $this->tierPriceResource = $tierPriceResource; + } + + /** + * Set tier price data for product entity + * + * @param \Magento\Catalog\Api\Data\ProductInterface|object $entity + * @param array $arguments + * @return \Magento\Catalog\Api\Data\ProductInterface|object + * @throws \Magento\Framework\Exception\InputException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function execute($entity, $arguments = []) + { + $attribute = $this->attributeRepository->get('tier_price'); + $priceRows = $entity->getData($attribute->getName()); + if (null !== $priceRows) { + if (!is_array($priceRows)) { + throw new \Magento\Framework\Exception\InputException( + __('Tier prices data should be array, but actually other type is received') + ); + } + $websiteId = $this->storeManager->getStore($entity->getStoreId())->getWebsiteId(); + $isGlobal = $attribute->isScopeGlobal() || $websiteId === 0; + $identifierField = $this->metadataPoll->getMetadata(ProductInterface::class)->getLinkField(); + $priceRows = array_filter($priceRows); + $productId = (int) $entity->getData($identifierField); + + // prepare and save data + foreach ($priceRows as $data) { + $isPriceWebsiteGlobal = (int)$data['website_id'] === 0; + if ($isGlobal === $isPriceWebsiteGlobal + || !empty($data['price_qty']) + || isset($data['cust_group']) + ) { + $tierPrice = $this->prepareTierPrice($data); + $price = new \Magento\Framework\DataObject($tierPrice); + $price->setData( + $identifierField, + $productId + ); + $this->tierPriceResource->savePriceData($price); + $valueChangedKey = $attribute->getName() . '_changed'; + $entity->setData($valueChangedKey, 1); + } + } + } + + return $entity; + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/UpdateHandler.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/UpdateHandler.php new file mode 100644 index 000000000000..663b7facf425 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/UpdateHandler.php @@ -0,0 +1,260 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Product\Attribute\Backend\TierPrice; + +use Magento\Framework\EntityManager\Operation\ExtensionInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Customer\Api\GroupManagementInterface; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice; + +/** + * Process tier price data for handled existing product. + */ +class UpdateHandler extends AbstractHandler +{ + /** + * @var \Magento\Store\Model\StoreManagerInterface + */ + private $storeManager; + + /** + * @var \Magento\Catalog\Api\ProductAttributeRepositoryInterface + */ + private $attributeRepository; + + /** + * @var \Magento\Framework\EntityManager\MetadataPool + */ + private $metadataPoll; + + /** + * @var \Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice + */ + private $tierPriceResource; + + /** + * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param \Magento\Catalog\Api\ProductAttributeRepositoryInterface $attributeRepository + * @param \Magento\Customer\Api\GroupManagementInterface $groupManagement + * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool + * @param \Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice $tierPriceResource + */ + public function __construct( + StoreManagerInterface $storeManager, + ProductAttributeRepositoryInterface $attributeRepository, + GroupManagementInterface $groupManagement, + MetadataPool $metadataPool, + Tierprice $tierPriceResource + ) { + parent::__construct($groupManagement); + + $this->storeManager = $storeManager; + $this->attributeRepository = $attributeRepository; + $this->metadataPoll = $metadataPool; + $this->tierPriceResource = $tierPriceResource; + } + + /** + * Perform action on relation/extension attribute. + * + * @param \Magento\Catalog\Api\Data\ProductInterface|object $entity + * @param array $arguments + * @return \Magento\Catalog\Api\Data\ProductInterface|object + * @throws \Magento\Framework\Exception\InputException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function execute($entity, $arguments = []) + { + $attribute = $this->attributeRepository->get('tier_price'); + $priceRows = $entity->getData($attribute->getName()); + if (null !== $priceRows) { + if (!is_array($priceRows)) { + throw new \Magento\Framework\Exception\InputException( + __('Tier prices data should be array, but actually other type is received') + ); + } + $websiteId = (int)$this->storeManager->getStore($entity->getStoreId())->getWebsiteId(); + $isGlobal = $attribute->isScopeGlobal() || $websiteId === 0; + $identifierField = $this->metadataPoll->getMetadata(ProductInterface::class)->getLinkField(); + $productId = (int)$entity->getData($identifierField); + + // prepare original data to compare + $origPrices = []; + $originalId = $entity->getOrigData($identifierField); + if (empty($originalId) || $entity->getData($identifierField) == $originalId) { + $origPrices = $entity->getOrigData($attribute->getName()); + } + + $old = $this->prepareOldTierPriceToCompare($origPrices); + // prepare data for save + $new = $this->prepareNewDataForSave($priceRows, $isGlobal); + + $delete = array_diff_key($old, $new); + $insert = array_diff_key($new, $old); + $update = array_intersect_key($new, $old); + + $isAttributeChanged = $this->deleteValues($productId, $delete); + $isAttributeChanged |= $this->insertValues($productId, $insert); + $isAttributeChanged |= $this->updateValues($update, $old); + + if ($isAttributeChanged) { + $valueChangedKey = $attribute->getName() . '_changed'; + $entity->setData($valueChangedKey, 1); + } + } + + return $entity; + } + + /** + * Update existing tier prices for processed product + * + * @param array $valuesToUpdate + * @param array $oldValues + * @return bool + */ + private function updateValues(array $valuesToUpdate, array $oldValues): bool + { + $isChanged = false; + foreach ($valuesToUpdate as $key => $value) { + if ((!empty($value['value']) && (float)$oldValues[$key]['price'] !== (float)$value['value']) + || $this->getPercentage($oldValues[$key]) !== $this->getPercentage($value) + ) { + $price = new \Magento\Framework\DataObject( + [ + 'value_id' => $oldValues[$key]['price_id'], + 'value' => $value['value'], + 'percentage_value' => $this->getPercentage($value) + ] + ); + $this->tierPriceResource->savePriceData($price); + $isChanged = true; + } + } + + return $isChanged; + } + + /** + * Insert new tier prices for processed product + * + * @param int $productId + * @param array $valuesToInsert + * @return bool + */ + private function insertValues(int $productId, array $valuesToInsert): bool + { + $isChanged = false; + $identifierField = $this->metadataPoll->getMetadata(ProductInterface::class)->getLinkField(); + foreach ($valuesToInsert as $data) { + $price = new \Magento\Framework\DataObject($data); + $price->setData( + $identifierField, + $productId + ); + $this->tierPriceResource->savePriceData($price); + $isChanged = true; + } + + return $isChanged; + } + + /** + * Delete tier price values for processed product + * + * @param int $productId + * @param array $valuesToDelete + * @return bool + */ + private function deleteValues(int $productId, array $valuesToDelete): bool + { + $isChanged = false; + foreach ($valuesToDelete as $data) { + $this->tierPriceResource->deletePriceData($productId, null, $data['price_id']); + $isChanged = true; + } + + return $isChanged; + } + + /** + * Get generated price key based on price data + * + * @param array $priceData + * @return string + */ + private function getPriceKey(array $priceData): string + { + $qty = $this->parseQty($priceData['price_qty']); + $key = implode( + '-', + array_merge([$priceData['website_id'], $priceData['cust_group']], [$qty]) + ); + + return $key; + } + + /** + * Check by id is website global + * + * @param int $websiteId + * @return bool + */ + private function isWebsiteGlobal(int $websiteId): bool + { + return $websiteId === 0; + } + + /** + * Prepare old data to compare. + * + * @param array|null $origPrices + * @return array + */ + private function prepareOldTierPriceToCompare(?array $origPrices): array + { + $old = []; + if (is_array($origPrices)) { + foreach ($origPrices as $data) { + $key = $this->getPriceKey($data); + $old[$key] = $data; + } + } + + return $old; + } + + /** + * Prepare new data for save. + * + * @param array $priceRows + * @param bool $isGlobal + * @return array + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function prepareNewDataForSave(array $priceRows, bool $isGlobal = true): array + { + $new = []; + $priceRows = array_filter($priceRows); + foreach ($priceRows as $data) { + if (empty($data['delete']) + && (!empty($data['price_qty']) + || isset($data['cust_group']) + || $isGlobal === $this->isWebsiteGlobal((int)$data['website_id'])) + ) { + $key = $this->getPriceKey($data); + $new[$key] = $this->prepareTierPrice($data); + } + } + + return $new; + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Tierprice.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Tierprice.php index 92b9a2e4239b..db967052cb7a 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Tierprice.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Tierprice.php @@ -13,6 +13,9 @@ use Magento\Catalog\Model\Attribute\ScopeOverriddenValue; +/** + * Backend model for Tierprice attribute + */ class Tierprice extends \Magento\Catalog\Model\Product\Attribute\Backend\GroupPrice\AbstractGroupPrice { /** @@ -159,6 +162,7 @@ protected function validatePrice(array $priceRow) */ protected function modifyPriceData($object, $data) { + /** @var \Magento\Catalog\Model\Product $object */ $data = parent::modifyPriceData($object, $data); $price = $object->getPrice(); foreach ($data as $key => $tierPrice) { @@ -172,6 +176,10 @@ protected function modifyPriceData($object, $data) } /** + * Update Price values in DB + * + * Updates price values in DB from array comparing to old values. Returns bool if updated + * * @param array $valuesToUpdate * @param array $oldValues * @return boolean diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/DataProvider.php b/app/code/Magento/Catalog/Model/Product/Attribute/DataProvider.php index 2bb10d3b31a2..893000544a72 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/DataProvider.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/DataProvider.php @@ -113,27 +113,28 @@ private function customizeAttributeCode($meta) */ private function customizeFrontendLabels($meta) { + $labelConfigs = []; + foreach ($this->storeRepository->getList() as $store) { $storeId = $store->getId(); if (!$storeId) { continue; } - - $meta['manage-titles']['children'] = [ - 'frontend_label[' . $storeId . ']' => $this->arrayManager->set( - 'arguments/data/config', - [], - [ - 'formElement' => Input::NAME, - 'componentType' => Field::NAME, - 'label' => $store->getName(), - 'dataType' => Text::NAME, - 'dataScope' => 'frontend_label[' . $storeId . ']' - ] - ), - ]; + $labelConfigs['frontend_label[' . $storeId . ']'] = $this->arrayManager->set( + 'arguments/data/config', + [], + [ + 'formElement' => Input::NAME, + 'componentType' => Field::NAME, + 'label' => $store->getName(), + 'dataType' => Text::NAME, + 'dataScope' => 'frontend_label[' . $storeId . ']' + ] + ); } + $meta['manage-titles']['children'] = $labelConfigs; + return $meta; } diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/OptionManagement.php b/app/code/Magento/Catalog/Model/Product/Attribute/OptionManagement.php index f2039a5002dc..8b638feafaaf 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/OptionManagement.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/OptionManagement.php @@ -8,6 +8,9 @@ use Magento\Framework\Exception\InputException; +/** + * Option management model for product attribute. + */ class OptionManagement implements \Magento\Catalog\Api\ProductAttributeOptionManagementInterface { /** @@ -25,7 +28,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getItems($attributeCode) { @@ -36,7 +39,7 @@ public function getItems($attributeCode) } /** - * {@inheritdoc} + * @inheritdoc */ public function add($attributeCode, $option) { @@ -47,7 +50,7 @@ public function add($attributeCode, $option) /** @var \Magento\Eav\Api\Data\AttributeOptionInterface $attributeOption */ $attributeOption = $attributeOption->getLabel(); }); - if (in_array($option->getLabel(), $currentOptions)) { + if (in_array($option->getLabel(), $currentOptions, true)) { return false; } } @@ -59,7 +62,7 @@ public function add($attributeCode, $option) } /** - * {@inheritdoc} + * @inheritdoc */ public function delete($attributeCode, $optionId) { diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Repository.php b/app/code/Magento/Catalog/Model/Product/Attribute/Repository.php index f6d3ca36c1e1..99edfe5bc720 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Repository.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Repository.php @@ -11,6 +11,8 @@ use Magento\Framework\Exception\NoSuchEntityException; /** + * Product attribute repository + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Repository implements \Magento\Catalog\Api\ProductAttributeRepositoryInterface @@ -78,7 +80,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function get($attributeCode) { @@ -89,7 +91,7 @@ public function get($attributeCode) } /** - * {@inheritdoc} + * @inheritdoc */ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria) { @@ -100,12 +102,17 @@ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCr } /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ public function save(\Magento\Catalog\Api\Data\ProductAttributeInterface $attribute) { + $attribute->setEntityTypeId( + $this->eavConfig + ->getEntityType(\Magento\Catalog\Api\Data\ProductAttributeInterface::ENTITY_TYPE_CODE) + ->getId() + ); if ($attribute->getAttributeId()) { $existingModel = $this->get($attribute->getAttributeCode()); @@ -144,11 +151,6 @@ public function save(\Magento\Catalog\Api\Data\ProductAttributeInterface $attrib $attribute->setBackendModel( $this->productHelper->getAttributeBackendModelByInputType($attribute->getFrontendInput()) ); - $attribute->setEntityTypeId( - $this->eavConfig - ->getEntityType(\Magento\Catalog\Api\Data\ProductAttributeInterface::ENTITY_TYPE_CODE) - ->getId() - ); $attribute->setIsUserDefined(1); } if (!empty($attribute->getData(AttributeInterface::OPTIONS))) { @@ -180,7 +182,7 @@ public function save(\Magento\Catalog\Api\Data\ProductAttributeInterface $attrib } /** - * {@inheritdoc} + * @inheritdoc */ public function delete(\Magento\Catalog\Api\Data\ProductAttributeInterface $attribute) { @@ -189,7 +191,7 @@ public function delete(\Magento\Catalog\Api\Data\ProductAttributeInterface $attr } /** - * {@inheritdoc} + * @inheritdoc */ public function deleteById($attributeCode) { @@ -200,7 +202,7 @@ public function deleteById($attributeCode) } /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function getCustomAttributesMetadata($dataObjectClassName = null) diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Source/Layout.php b/app/code/Magento/Catalog/Model/Product/Attribute/Source/Layout.php index 63b1444d1db0..dbc7535dccfa 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Source/Layout.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Source/Layout.php @@ -17,6 +17,12 @@ class Layout extends \Magento\Eav\Model\Entity\Attribute\Source\AbstractSource */ protected $pageLayoutBuilder; + /** + * @inheritdoc + * @deprecated since the cache is now handled by \Magento\Theme\Model\PageLayout\Config\Builder::$configFiles + */ + protected $_options = null; + /** * @param \Magento\Framework\View\Model\PageLayout\Config\BuilderInterface $pageLayoutBuilder */ @@ -26,14 +32,14 @@ public function __construct(\Magento\Framework\View\Model\PageLayout\Config\Buil } /** - * @return array + * @inheritdoc */ public function getAllOptions() { - if (!$this->_options) { - $this->_options = $this->pageLayoutBuilder->getPageLayoutsConfig()->toOptionArray(); - array_unshift($this->_options, ['value' => '', 'label' => __('No layout updates')]); - } - return $this->_options; + $options = $this->pageLayoutBuilder->getPageLayoutsConfig()->toOptionArray(); + array_unshift($options, ['value' => '', 'label' => __('No layout updates')]); + $this->_options = $options; + + return $options; } } diff --git a/app/code/Magento/Catalog/Model/Product/Compare/Item.php b/app/code/Magento/Catalog/Model/Product/Compare/Item.php index 3b7e47a46a0a..fd07380bebd7 100644 --- a/app/code/Magento/Catalog/Model/Product/Compare/Item.php +++ b/app/code/Magento/Catalog/Model/Product/Compare/Item.php @@ -158,8 +158,8 @@ public function addProductData($product) { if ($product instanceof Product) { $this->setProductId($product->getId()); - } elseif (intval($product)) { - $this->setProductId(intval($product)); + } elseif ((int) $product) { + $this->setProductId((int) $product); } return $this; diff --git a/app/code/Magento/Catalog/Model/Product/Copier.php b/app/code/Magento/Catalog/Model/Product/Copier.php index e94104ae473a..44ebdf0f1f28 100644 --- a/app/code/Magento/Catalog/Model/Product/Copier.php +++ b/app/code/Magento/Catalog/Model/Product/Copier.php @@ -1,14 +1,20 @@ <?php /** - * Catalog product copier. Creates product duplicate - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Catalog\Model\Product; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product; +/** + * Catalog product copier. + * + * Creates product duplicate. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class Copier { /** @@ -49,7 +55,7 @@ public function __construct( * @param \Magento\Catalog\Model\Product $product * @return \Magento\Catalog\Model\Product */ - public function copy(\Magento\Catalog\Model\Product $product) + public function copy(Product $product) { $product->getWebsiteIds(); $product->getCategoryIds(); @@ -70,21 +76,9 @@ public function copy(\Magento\Catalog\Model\Product $product) $duplicate->setUpdatedAt(null); $duplicate->setId(null); $duplicate->setStoreId(\Magento\Store\Model\Store::DEFAULT_STORE_ID); - $this->copyConstructor->build($product, $duplicate); - $isDuplicateSaved = false; - do { - $urlKey = $duplicate->getUrlKey(); - $urlKey = preg_match('/(.*)-(\d+)$/', $urlKey, $matches) - ? $matches[1] . '-' . ($matches[2] + 1) - : $urlKey . '-1'; - $duplicate->setUrlKey($urlKey); - try { - $duplicate->save(); - $isDuplicateSaved = true; - } catch (\Magento\Framework\Exception\AlreadyExistsException $e) { - } - } while (!$isDuplicateSaved); + $this->setDefaultUrl($product, $duplicate); + $this->setStoresUrl($product, $duplicate); $this->getOptionRepository()->duplicate($product, $duplicate); $product->getResource()->duplicate( $product->getData($metadata->getLinkField()), @@ -94,6 +88,83 @@ public function copy(\Magento\Catalog\Model\Product $product) } /** + * Set default URL. + * + * @param Product $product + * @param Product $duplicate + * @return void + */ + private function setDefaultUrl(Product $product, Product $duplicate) : void + { + $duplicate->setStoreId(\Magento\Store\Model\Store::DEFAULT_STORE_ID); + $resource = $product->getResource(); + $attribute = $resource->getAttribute('url_key'); + $productId = $product->getId(); + $urlKey = $resource->getAttributeRawValue($productId, 'url_key', \Magento\Store\Model\Store::DEFAULT_STORE_ID); + do { + $urlKey = $this->modifyUrl($urlKey); + $duplicate->setUrlKey($urlKey); + } while (!$attribute->getEntity()->checkAttributeUniqueValue($attribute, $duplicate)); + $duplicate->setData('url_path', null); + $duplicate->save(); + } + + /** + * Set URL for each store. + * + * @param Product $product + * @param Product $duplicate + * @return void + */ + private function setStoresUrl(Product $product, Product $duplicate) : void + { + $storeIds = $duplicate->getStoreIds(); + $productId = $product->getId(); + $productResource = $product->getResource(); + $defaultUrlKey = $productResource->getAttributeRawValue( + $productId, + 'url_key', + \Magento\Store\Model\Store::DEFAULT_STORE_ID + ); + $duplicate->setData('save_rewrites_history', false); + foreach ($storeIds as $storeId) { + $isDuplicateSaved = false; + $duplicate->setStoreId($storeId); + $urlKey = $productResource->getAttributeRawValue($productId, 'url_key', $storeId); + if ($urlKey === $defaultUrlKey) { + continue; + } + do { + $urlKey = $this->modifyUrl($urlKey); + $duplicate->setUrlKey($urlKey); + $duplicate->setData('url_path', null); + try { + $duplicate->save(); + $isDuplicateSaved = true; + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock + } catch (\Magento\Framework\Exception\AlreadyExistsException $e) { + } + } while (!$isDuplicateSaved); + } + $duplicate->setStoreId(\Magento\Store\Model\Store::DEFAULT_STORE_ID); + } + + /** + * Modify URL key. + * + * @param string $urlKey + * @return string + */ + private function modifyUrl(string $urlKey) : string + { + return preg_match('/(.*)-(\d+)$/', $urlKey, $matches) + ? $matches[1] . '-' . ($matches[2] + 1) + : $urlKey . '-1'; + } + + /** + * Returns product option repository. + * * @return Option\Repository * @deprecated 101.0.0 */ @@ -107,6 +178,8 @@ private function getOptionRepository() } /** + * Returns metadata pool. + * * @return \Magento\Framework\EntityManager\MetadataPool * @deprecated 101.0.0 */ diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php index bd28b65bb798..e06e85e90a2d 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php @@ -102,6 +102,8 @@ public function __construct( } /** + * Execute create handler + * * @param object $product * @param array $arguments * @return object @@ -167,23 +169,19 @@ public function execute($product, $arguments = []) if (empty($attrData) && empty($clearImages) && empty($newImages) && empty($existImages)) { continue; } - if (in_array($attrData, $clearImages)) { - $product->setData($mediaAttrCode, 'no_selection'); - } - - if (in_array($attrData, array_keys($newImages))) { - $product->setData($mediaAttrCode, $newImages[$attrData]['new_file']); - $product->setData($mediaAttrCode . '_label', $newImages[$attrData]['label']); - } - - if (in_array($attrData, array_keys($existImages)) && isset($existImages[$attrData]['label'])) { - $product->setData($mediaAttrCode . '_label', $existImages[$attrData]['label']); - } - if (!empty($product->getData($mediaAttrCode))) { - $product->addAttributeUpdate( + $this->processMediaAttribute( + $product, + $mediaAttrCode, + $clearImages, + $newImages + ); + if (in_array($mediaAttrCode, ['image', 'small_image', 'thumbnail'])) { + $this->processMediaAttributeLabel( + $product, $mediaAttrCode, - $product->getData($mediaAttrCode), - $product->getStoreId() + $clearImages, + $newImages, + $existImages ); } } @@ -208,6 +206,8 @@ public function execute($product, $arguments = []) } /** + * Returns media gallery attribute instance + * * @return \Magento\Catalog\Api\Data\ProductAttributeInterface * @since 101.0.0 */ @@ -223,17 +223,22 @@ public function getAttribute() } /** + * Process delete images + * * @param \Magento\Catalog\Model\Product $product * @param array $images * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @since 101.0.0 + * phpcs:disable Magento2.CodeAnalysis.EmptyBlock */ protected function processDeletedImages($product, array &$images) { } /** + * Process images + * * @param \Magento\Catalog\Model\Product $product * @param array $images * @return void @@ -296,6 +301,8 @@ protected function processNewImage($product, array &$image) } /** + * Duplicate attribute + * * @param \Magento\Catalog\Model\Product $product * @return $this * @since 101.0.0 @@ -312,7 +319,7 @@ protected function duplicate($product) $this->resourceModel->duplicate( $this->getAttribute()->getAttributeId(), - isset($mediaGalleryData['duplicate']) ? $mediaGalleryData['duplicate'] : [], + $mediaGalleryData['duplicate'] ?? [], $product->getOriginalLinkId(), $product->getData($this->metadata->getLinkField()) ); @@ -364,6 +371,8 @@ private function getSafeFilename($file) } /** + * Returns file name according to tmp name + * * @param string $file * @return string * @since 101.0.0 @@ -392,6 +401,7 @@ protected function getUniqueFileName($file, $forTmp = false) $destinationFile = $forTmp ? $this->mediaDirectory->getAbsolutePath($this->mediaConfig->getTmpMediaPath($file)) : $this->mediaDirectory->getAbsolutePath($this->mediaConfig->getMediaPath($file)); + // phpcs:disable Magento2.Functions.DiscouragedFunction $destFile = dirname($file) . '/' . FileUploader::getNewFileName($destinationFile); } @@ -412,6 +422,7 @@ protected function copyImage($file) $destinationFile = $this->getUniqueFileName($file); if (!$this->mediaDirectory->isFile($this->mediaConfig->getMediaPath($file))) { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception(); } @@ -429,6 +440,7 @@ protected function copyImage($file) } return str_replace('\\', '/', $destinationFile); + // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (\Exception $e) { $file = $this->mediaConfig->getMediaPath($file); throw new \Magento\Framework\Exception\LocalizedException( @@ -449,4 +461,81 @@ private function getMediaAttributeCodes() } return $this->mediaAttributeCodes; } + + /** + * Process media attribute + * + * @param \Magento\Catalog\Model\Product $product + * @param string $mediaAttrCode + * @param array $clearImages + * @param array $newImages + */ + private function processMediaAttribute( + \Magento\Catalog\Model\Product $product, + $mediaAttrCode, + array $clearImages, + array $newImages + ) { + $attrData = $product->getData($mediaAttrCode); + if (in_array($attrData, $clearImages)) { + $product->setData($mediaAttrCode, 'no_selection'); + } + + if (in_array($attrData, array_keys($newImages))) { + $product->setData($mediaAttrCode, $newImages[$attrData]['new_file']); + } + if (!empty($product->getData($mediaAttrCode))) { + $product->addAttributeUpdate( + $mediaAttrCode, + $product->getData($mediaAttrCode), + $product->getStoreId() + ); + } + } + + /** + * Process media attribute label + * + * @param \Magento\Catalog\Model\Product $product + * @param string $mediaAttrCode + * @param array $clearImages + * @param array $newImages + * @param array $existImages + */ + private function processMediaAttributeLabel( + \Magento\Catalog\Model\Product $product, + $mediaAttrCode, + array $clearImages, + array $newImages, + array $existImages + ) { + $resetLabel = false; + $attrData = $product->getData($mediaAttrCode); + if (in_array($attrData, $clearImages)) { + $product->setData($mediaAttrCode . '_label', null); + $resetLabel = true; + } + + if (in_array($attrData, array_keys($newImages))) { + $product->setData($mediaAttrCode . '_label', $newImages[$attrData]['label']); + } + + if (in_array($attrData, array_keys($existImages)) && isset($existImages[$attrData]['label'])) { + $product->setData($mediaAttrCode . '_label', $existImages[$attrData]['label']); + } + + if ($attrData === 'no_selection' && !empty($product->getData($mediaAttrCode . '_label'))) { + $product->setData($mediaAttrCode . '_label', null); + $resetLabel = true; + } + if (!empty($product->getData($mediaAttrCode . '_label')) + || $resetLabel === true + ) { + $product->addAttributeUpdate( + $mediaAttrCode . '_label', + $product->getData($mediaAttrCode . '_label'), + $product->getStoreId() + ); + } + } } diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php b/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php index 4d274a071d08..c993e51c8bc0 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -8,13 +7,16 @@ namespace Magento\Catalog\Model\Product\Gallery; use Magento\Catalog\Api\Data\ProductAttributeMediaGalleryEntryInterface; -use Magento\Catalog\Api\Data\ProductInterface as Product; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\StateException; use Magento\Framework\Api\ImageContentValidatorInterface; /** + * Class GalleryManagement + * + * Provides implementation of api interface ProductAttributeMediaGalleryManagementInterface + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class GalleryManagement implements \Magento\Catalog\Api\ProductAttributeMediaGalleryManagementInterface @@ -44,7 +46,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function create($sku, ProductAttributeMediaGalleryEntryInterface $entry) { @@ -54,7 +56,7 @@ public function create($sku, ProductAttributeMediaGalleryEntryInterface $entry) if (!$this->contentValidator->isValid($entryContent)) { throw new InputException(__('The image content is invalid. Verify the content and try again.')); } - $product = $this->productRepository->get($sku); + $product = $this->productRepository->get($sku, true); $existingMediaGalleryEntries = $product->getMediaGalleryEntries(); $existingEntryIds = []; @@ -69,8 +71,10 @@ public function create($sku, ProductAttributeMediaGalleryEntryInterface $entry) $product->setMediaGalleryEntries($existingMediaGalleryEntries); try { $product = $this->productRepository->save($product); + // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (InputException $inputException) { throw $inputException; + // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (\Exception $e) { throw new StateException(__("The product can't be saved.")); } @@ -84,11 +88,11 @@ public function create($sku, ProductAttributeMediaGalleryEntryInterface $entry) } /** - * {@inheritdoc} + * @inheritdoc */ public function update($sku, ProductAttributeMediaGalleryEntryInterface $entry) { - $product = $this->productRepository->get($sku); + $product = $this->productRepository->get($sku, true); $existingMediaGalleryEntries = $product->getMediaGalleryEntries(); if ($existingMediaGalleryEntries == null) { throw new NoSuchEntityException( @@ -103,7 +107,10 @@ public function update($sku, ProductAttributeMediaGalleryEntryInterface $entry) if ($existingEntry->getId() == $entry->getId()) { $found = true; - if ($entry->getFile()) { + + $file = $entry->getContent(); + + if ($file && $file->getBase64EncodedData() || $entry->getFile()) { $entry->setId(null); } $existingMediaGalleryEntries[$key] = $entry; @@ -125,11 +132,11 @@ public function update($sku, ProductAttributeMediaGalleryEntryInterface $entry) } /** - * {@inheritdoc} + * @inheritdoc */ public function remove($sku, $entryId) { - $product = $this->productRepository->get($sku); + $product = $this->productRepository->get($sku, true); $existingMediaGalleryEntries = $product->getMediaGalleryEntries(); if ($existingMediaGalleryEntries == null) { throw new NoSuchEntityException( @@ -155,7 +162,7 @@ public function remove($sku, $entryId) } /** - * {@inheritdoc} + * @inheritdoc */ public function get($sku, $entryId) { @@ -176,7 +183,7 @@ public function get($sku, $entryId) } /** - * {@inheritdoc} + * @inheritdoc */ public function getList($sku) { diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/Processor.php b/app/code/Magento/Catalog/Model/Product/Gallery/Processor.php index c6c7fbda7e9e..091232474536 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/Processor.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/Processor.php @@ -6,9 +6,10 @@ namespace Magento\Catalog\Model\Product\Gallery; +use Magento\Framework\Api\Data\ImageContentInterface; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\Filesystem\DriverInterface; +use Magento\Framework\App\ObjectManager; /** * Catalog product Media Gallery attribute processor. @@ -56,28 +57,39 @@ class Processor */ protected $resourceModel; + /** + * @var \Magento\Framework\File\Mime + */ + private $mime; + /** * @param \Magento\Catalog\Api\ProductAttributeRepositoryInterface $attributeRepository * @param \Magento\MediaStorage\Helper\File\Storage\Database $fileStorageDb * @param \Magento\Catalog\Model\Product\Media\Config $mediaConfig * @param \Magento\Framework\Filesystem $filesystem * @param \Magento\Catalog\Model\ResourceModel\Product\Gallery $resourceModel + * @param \Magento\Framework\File\Mime|null $mime + * @throws \Magento\Framework\Exception\FileSystemException */ public function __construct( \Magento\Catalog\Api\ProductAttributeRepositoryInterface $attributeRepository, \Magento\MediaStorage\Helper\File\Storage\Database $fileStorageDb, \Magento\Catalog\Model\Product\Media\Config $mediaConfig, \Magento\Framework\Filesystem $filesystem, - \Magento\Catalog\Model\ResourceModel\Product\Gallery $resourceModel + \Magento\Catalog\Model\ResourceModel\Product\Gallery $resourceModel, + \Magento\Framework\File\Mime $mime = null ) { $this->attributeRepository = $attributeRepository; $this->fileStorageDb = $fileStorageDb; $this->mediaConfig = $mediaConfig; $this->mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); $this->resourceModel = $resourceModel; + $this->mime = $mime ?: ObjectManager::getInstance()->get(\Magento\Framework\File\Mime::class); } /** + * Return media_gallery attribute + * * @return \Magento\Catalog\Api\Data\ProductAttributeInterface * @since 101.0.0 */ @@ -183,6 +195,13 @@ public function addImage( $attrCode = $this->getAttribute()->getAttributeCode(); $mediaGalleryData = $product->getData($attrCode); $position = 0; + + $absoluteFilePath = $this->mediaDirectory->getAbsolutePath($file); + $imageMimeType = $this->mime->getMimeType($absoluteFilePath); + $imageContent = $this->mediaDirectory->readFile($absoluteFilePath); + $imageBase64 = base64_encode($imageContent); + $imageName = $pathinfo['filename']; + if (!is_array($mediaGalleryData)) { $mediaGalleryData = ['images' => []]; } @@ -197,9 +216,17 @@ public function addImage( $mediaGalleryData['images'][] = [ 'file' => $fileName, 'position' => $position, - 'media_type' => 'image', 'label' => '', 'disabled' => (int)$exclude, + 'media_type' => 'image', + 'types' => $mediaAttribute, + 'content' => [ + 'data' => [ + ImageContentInterface::NAME => $imageName, + ImageContentInterface::BASE64_ENCODED_DATA => $imageBase64, + ImageContentInterface::TYPE => $imageMimeType, + ] + ] ]; $product->setData($attrCode, $mediaGalleryData); @@ -358,7 +385,8 @@ public function setMediaAttribute(\Magento\Catalog\Model\Product $product, $medi } /** - * get media attribute codes + * Get media attribute codes + * * @return array * @since 101.0.0 */ @@ -368,6 +396,8 @@ public function getMediaAttributeCodes() } /** + * Trim .tmp ending from filename + * * @param string $file * @return string * @since 101.0.0 @@ -489,7 +519,6 @@ public function getAffectedFields($object) /** * Attribute value is not to be saved in a conventional way, separate table is used to store the complex value * - * {@inheritdoc} * @since 101.0.0 */ public function isScalar() diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/ReadHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/ReadHandler.php index c785d08e64b7..a3726207b302 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/ReadHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/ReadHandler.php @@ -47,6 +47,8 @@ public function __construct( } /** + * Execute read handler for catalog product gallery + * * @param Product $entity * @param array $arguments * @return object @@ -55,9 +57,6 @@ public function __construct( */ public function execute($entity, $arguments = []) { - $value = []; - $value['images'] = []; - $mediaEntries = $this->resourceModel->loadProductGalleryByAttributeId( $entity, $this->getAttribute()->getAttributeId() @@ -72,6 +71,8 @@ public function execute($entity, $arguments = []) } /** + * Add media data to product + * * @param Product $product * @param array $mediaEntries * @return void @@ -79,40 +80,18 @@ public function execute($entity, $arguments = []) */ public function addMediaDataToProduct(Product $product, array $mediaEntries) { - $attrCode = $this->getAttribute()->getAttributeCode(); - $value = []; - $value['images'] = []; - $value['values'] = []; - - foreach ($mediaEntries as $mediaEntry) { - $mediaEntry = $this->substituteNullsWithDefaultValues($mediaEntry); - $value['images'][$mediaEntry['value_id']] = $mediaEntry; - } - $product->setData($attrCode, $value); - } - - /** - * @param array $rawData - * @return array - */ - private function substituteNullsWithDefaultValues(array $rawData) - { - $processedData = []; - foreach ($rawData as $key => $rawValue) { - if (null !== $rawValue) { - $processedValue = $rawValue; - } elseif (isset($rawData[$key . '_default'])) { - $processedValue = $rawData[$key . '_default']; - } else { - $processedValue = null; - } - $processedData[$key] = $processedValue; - } - - return $processedData; + $product->setData( + $this->getAttribute()->getAttributeCode(), + [ + 'images' => array_column($mediaEntries, null, 'value_id'), + 'values' => [] + ] + ); } /** + * Get attribute + * * @return \Magento\Catalog\Api\Data\ProductAttributeInterface * @since 101.0.0 */ @@ -126,6 +105,8 @@ public function getAttribute() } /** + * Find default value + * * @param string $key * @param string[] &$image * @return string diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php index 8ad8dcb4812e..189135776b68 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php @@ -16,7 +16,8 @@ class UpdateHandler extends \Magento\Catalog\Model\Product\Gallery\CreateHandler { /** - * {@inheritdoc} + * @inheritdoc + * * @since 101.0.0 */ protected function processDeletedImages($product, array &$images) @@ -31,7 +32,7 @@ protected function processDeletedImages($product, array &$images) foreach ($images as &$image) { if (!empty($image['removed'])) { - if (!empty($image['value_id']) && !isset($picturesInOtherStores[$image['file']])) { + if (!empty($image['value_id'])) { if (preg_match('/\.\.(\\\|\/)/', $image['file'])) { continue; } @@ -52,7 +53,8 @@ protected function processDeletedImages($product, array &$images) } /** - * {@inheritdoc} + * @inheritdoc + * * @since 101.0.0 */ protected function processNewImage($product, array &$image) @@ -79,6 +81,8 @@ protected function processNewImage($product, array &$image) } /** + * Retrieve store ids from product. + * * @param \Magento\Catalog\Model\Product $product * @return array * @since 101.0.0 @@ -97,6 +101,8 @@ protected function extractStoreIds($product) } /** + * Remove deleted images. + * * @param array $files * @return null * @since 101.0.0 diff --git a/app/code/Magento/Catalog/Model/Product/Image.php b/app/code/Magento/Catalog/Model/Product/Image.php index 09ba68ddbe2b..a0be36c5a327 100644 --- a/app/code/Magento/Catalog/Model/Product/Image.php +++ b/app/code/Magento/Catalog/Model/Product/Image.php @@ -15,6 +15,8 @@ use Magento\Catalog\Model\Product\Image\ParamsBuilder; /** + * Image operations + * * @method string getFile() * @method string getLabel() * @method string getPosition() @@ -24,6 +26,11 @@ */ class Image extends \Magento\Framework\Model\AbstractModel { + /** + * Config path for the jpeg image quality value + */ + const XML_PATH_JPEG_QUALITY = 'system/upload_configuration/jpeg_quality'; + /** * @var int */ @@ -38,8 +45,9 @@ class Image extends \Magento\Framework\Model\AbstractModel * Default quality value (for JPEG images only). * * @var int + * @deprecated use config setting with path self::XML_PATH_JPEG_QUALITY */ - protected $_quality = 80; + protected $_quality = null; /** * @var bool @@ -203,13 +211,13 @@ class Image extends \Magento\Framework\Model\AbstractModel * @param \Magento\Framework\Image\Factory $imageFactory * @param \Magento\Framework\View\Asset\Repository $assetRepo * @param \Magento\Framework\View\FileSystem $viewFileSystem + * @param ImageFactory $viewAssetImageFactory + * @param PlaceholderFactory $viewAssetPlaceholderFactory * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data - * @param ImageFactory|null $viewAssetImageFactory - * @param PlaceholderFactory|null $viewAssetPlaceholderFactory - * @param SerializerInterface|null $serializer + * @param SerializerInterface $serializer * @param ParamsBuilder $paramsBuilder * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.UnusedLocalVariable) @@ -249,6 +257,8 @@ public function __construct( } /** + * Set image width property + * * @param int $width * @return $this */ @@ -259,6 +269,8 @@ public function setWidth($width) } /** + * Get image width property + * * @return int */ public function getWidth() @@ -267,6 +279,8 @@ public function getWidth() } /** + * Set image height property + * * @param int $height * @return $this */ @@ -277,6 +291,8 @@ public function setHeight($height) } /** + * Get image height property + * * @return int */ public function getHeight() @@ -289,6 +305,7 @@ public function getHeight() * * @param int $quality * @return $this + * @deprecated use config setting with path self::XML_PATH_JPEG_QUALITY */ public function setQuality($quality) { @@ -303,10 +320,14 @@ public function setQuality($quality) */ public function getQuality() { - return $this->_quality; + return $this->_quality === null + ? $this->_scopeConfig->getValue(self::XML_PATH_JPEG_QUALITY) + : $this->_quality; } /** + * Set _keepAspectRatio property + * * @param bool $keep * @return $this */ @@ -317,6 +338,8 @@ public function setKeepAspectRatio($keep) } /** + * Set _keepFrame property + * * @param bool $keep * @return $this */ @@ -327,6 +350,8 @@ public function setKeepFrame($keep) } /** + * Set _keepTransparency + * * @param bool $keep * @return $this */ @@ -337,6 +362,8 @@ public function setKeepTransparency($keep) } /** + * Set _constrainOnly + * * @param bool $flag * @return $this */ @@ -347,6 +374,8 @@ public function setConstrainOnly($flag) } /** + * Set background color + * * @param int[] $rgbArray * @return $this */ @@ -357,6 +386,8 @@ public function setBackgroundColor(array $rgbArray) } /** + * Set size + * * @param string $size * @return $this */ @@ -411,6 +442,8 @@ public function setBaseFile($file) } /** + * Get base filename + * * @return string */ public function getBaseFile() @@ -419,6 +452,8 @@ public function getBaseFile() } /** + * Get new file + * * @deprecated 101.1.0 * @return bool|string */ @@ -438,6 +473,8 @@ public function isBaseFilePlaceholder() } /** + * Set image processor + * * @param MagentoImage $processor * @return $this */ @@ -448,6 +485,8 @@ public function setImageProcessor($processor) } /** + * Get image processor + * * @return MagentoImage */ public function getImageProcessor() @@ -461,11 +500,13 @@ public function getImageProcessor() $this->_processor->keepTransparency($this->_keepTransparency); $this->_processor->constrainOnly($this->_constrainOnly); $this->_processor->backgroundColor($this->_backgroundColor); - $this->_processor->quality($this->_quality); + $this->_processor->quality($this->getQuality()); return $this->_processor; } /** + * Resize image + * * @see \Magento\Framework\Image\Adapter\AbstractAdapter * @return $this */ @@ -479,12 +520,14 @@ public function resize() } /** + * Rotate image + * * @param int $angle * @return $this */ public function rotate($angle) { - $angle = intval($angle); + $angle = (int) $angle; $this->getImageProcessor()->rotate($angle); return $this; } @@ -505,7 +548,8 @@ public function setAngle($angle) /** * Add watermark to image - * size param in format 100x200 + * + * Size param in format 100x200 * * @param string $file * @param string $position @@ -564,6 +608,8 @@ public function setWatermark( } /** + * Save file + * * @return $this */ public function saveFile() @@ -578,6 +624,8 @@ public function saveFile() } /** + * Get url + * * @return string */ public function getUrl() @@ -586,6 +634,8 @@ public function getUrl() } /** + * Set destination subdir + * * @param string $dir * @return $this */ @@ -596,6 +646,8 @@ public function setDestinationSubdir($dir) } /** + * Get destination subdir + * * @return string */ public function getDestinationSubdir() @@ -604,6 +656,8 @@ public function getDestinationSubdir() } /** + * Check is image cached + * * @return bool */ public function isCached() @@ -636,7 +690,8 @@ public function getWatermarkFile() /** * Get relative watermark file path - * or false if file not found + * + * Return false if file not found * * @return string | bool */ @@ -771,7 +826,10 @@ public function getWatermarkHeight() } /** + * Clear cache + * * @return void + * @throws \Magento\Framework\Exception\FileSystemException */ public function clearCache() { @@ -784,6 +842,7 @@ public function clearCache() /** * First check this file on FS + * * If it doesn't exist - try to download it from DB * * @param string $filename @@ -802,6 +861,7 @@ protected function _fileExists($filename) /** * Return resized product image information + * * @return array * @throws NotLoadInfoImageException */ @@ -843,7 +903,7 @@ private function getMiscParams() 'transparency' => $this->_keepTransparency, 'background' => $this->_backgroundColor, 'angle' => $this->_angle, - 'quality' => $this->_quality + 'quality' => $this->getQuality() ] ); } diff --git a/app/code/Magento/Catalog/Model/Product/Image/ParamsBuilder.php b/app/code/Magento/Catalog/Model/Product/Image/ParamsBuilder.php index dd8d352feceb..4a55714a27ec 100644 --- a/app/code/Magento/Catalog/Model/Product/Image/ParamsBuilder.php +++ b/app/code/Magento/Catalog/Model/Product/Image/ParamsBuilder.php @@ -10,17 +10,13 @@ use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\View\ConfigInterface; use Magento\Store\Model\ScopeInterface; +use Magento\Catalog\Model\Product\Image; /** * Builds parameters array used to build Image Asset */ class ParamsBuilder { - /** - * @var int - */ - private $defaultQuality = 80; - /** * @var array */ @@ -69,6 +65,8 @@ public function __construct( } /** + * Build image params + * * @param array $imageArguments * @return array * @SuppressWarnings(PHPMD.NPathComplexity) @@ -89,6 +87,8 @@ public function build(array $imageArguments): array } /** + * Overwrite default values + * * @param array $imageArguments * @return array */ @@ -100,11 +100,12 @@ private function overwriteDefaultValues(array $imageArguments): array $transparency = $imageArguments['transparency'] ?? $this->defaultKeepTransparency; $background = $imageArguments['background'] ?? $this->defaultBackground; $angle = $imageArguments['angle'] ?? $this->defaultAngle; + $quality = (int) $this->scopeConfig->getValue(Image::XML_PATH_JPEG_QUALITY); return [ 'background' => (array) $background, 'angle' => $angle, - 'quality' => $this->defaultQuality, + 'quality' => $quality, 'keep_aspect_ratio' => (bool) $aspectRatio, 'keep_frame' => (bool) $frame, 'keep_transparency' => (bool) $transparency, @@ -113,6 +114,8 @@ private function overwriteDefaultValues(array $imageArguments): array } /** + * Get watermark + * * @param string $type * @return array */ @@ -153,13 +156,12 @@ private function getWatermark(string $type): array /** * Get frame from product_image_white_borders + * * @return bool */ private function hasDefaultFrame(): bool { - return (bool) $this->viewConfig->getViewConfig()->getVarValue( - 'Magento_Catalog', - 'product_image_white_borders' - ); + return (bool) $this->viewConfig->getViewConfig(['area' => \Magento\Framework\App\Area::AREA_FRONTEND]) + ->getVarValue('Magento_Catalog', 'product_image_white_borders'); } } diff --git a/app/code/Magento/Catalog/Model/Product/Media/Config.php b/app/code/Magento/Catalog/Model/Product/Media/Config.php index 72936d317399..33af93db13b4 100644 --- a/app/code/Magento/Catalog/Model/Product/Media/Config.php +++ b/app/code/Magento/Catalog/Model/Product/Media/Config.php @@ -10,11 +10,9 @@ use Magento\Store\Model\StoreManagerInterface; /** - * Catalog product media config + * Catalog product media config. * * @api - * - * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 */ class Config implements ConfigInterface @@ -31,6 +29,11 @@ class Config implements ConfigInterface */ private $attributeHelper; + /** + * @var string[] + */ + private $mediaAttributeCodes; + /** * @param StoreManagerInterface $storeManager */ @@ -40,8 +43,7 @@ public function __construct(StoreManagerInterface $storeManager) } /** - * Filesystem directory path of product images - * relatively to media folder + * Get filesystem directory path for product images relative to the media directory. * * @return string */ @@ -51,8 +53,7 @@ public function getBaseMediaPathAddition() } /** - * Web-based directory path of product images - * relatively to media folder + * Get web-based directory path for product images relative to the media directory. * * @return string */ @@ -62,7 +63,7 @@ public function getBaseMediaUrlAddition() } /** - * @return string + * @inheritdoc */ public function getBaseMediaPath() { @@ -70,7 +71,7 @@ public function getBaseMediaPath() } /** - * @return string + * @inheritdoc */ public function getBaseMediaUrl() { @@ -79,8 +80,7 @@ public function getBaseMediaUrl() } /** - * Filesystem directory path of temporary product images - * relatively to media folder + * Filesystem directory path of temporary product images relative to the media directory. * * @return string */ @@ -90,6 +90,8 @@ public function getBaseTmpMediaPath() } /** + * Get temporary base media URL. + * * @return string */ public function getBaseTmpMediaUrl() @@ -100,8 +102,7 @@ public function getBaseTmpMediaUrl() } /** - * @param string $file - * @return string + * @inheritdoc */ public function getMediaUrl($file) { @@ -109,8 +110,7 @@ public function getMediaUrl($file) } /** - * @param string $file - * @return string + * @inheritdoc */ public function getMediaPath($file) { @@ -118,6 +118,8 @@ public function getMediaPath($file) } /** + * Get temporary media URL. + * * @param string $file * @return string */ @@ -127,8 +129,7 @@ public function getTmpMediaUrl($file) } /** - * Part of URL of temporary product images - * relatively to media folder + * Part of URL of temporary product images relative to the media directory. * * @param string $file * @return string @@ -139,7 +140,7 @@ public function getTmpMediaShortUrl($file) } /** - * Part of URL of product images relatively to media folder + * Part of URL of product images relatively to media folder. * * @param string $file * @return string @@ -150,6 +151,8 @@ public function getMediaShortUrl($file) } /** + * Get path to the temporary media. + * * @param string $file * @return string */ @@ -159,6 +162,8 @@ public function getTmpMediaPath($file) } /** + * Process file path. + * * @param string $file * @return string */ @@ -168,15 +173,23 @@ protected function _prepareFile($file) } /** + * Get codes of media attribute. + * * @return array * @since 100.0.4 */ public function getMediaAttributeCodes() { - return $this->getAttributeHelper()->getAttributeCodesByFrontendType('media_image'); + if (!isset($this->mediaAttributeCodes)) { + // the in-memory object-level caching allows to prevent unnecessary calls to the DB + $this->mediaAttributeCodes = $this->getAttributeHelper()->getAttributeCodesByFrontendType('media_image'); + } + return $this->mediaAttributeCodes; } /** + * Get attribute helper. + * * @return Attribute */ private function getAttributeHelper() diff --git a/app/code/Magento/Catalog/Model/Product/Option.php b/app/code/Magento/Catalog/Model/Product/Option.php index acfc454883e1..b4a4ec08d390 100644 --- a/app/code/Magento/Catalog/Model/Product/Option.php +++ b/app/code/Magento/Catalog/Model/Product/Option.php @@ -323,7 +323,7 @@ public function getGroupByType($type = null) self::OPTION_TYPE_TIME => self::OPTION_GROUP_DATE, ]; - return isset($optionGroupsToTypes[$type]) ? $optionGroupsToTypes[$type] : ''; + return $optionGroupsToTypes[$type] ?? ''; } /** diff --git a/app/code/Magento/Catalog/Model/Product/Option/Repository.php b/app/code/Magento/Catalog/Model/Product/Option/Repository.php index 9dc9695daffd..bb4e247de32d 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Repository.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Repository.php @@ -14,6 +14,8 @@ use Magento\Framework\App\ObjectManager; /** + * Product custom options repository + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Repository implements \Magento\Catalog\Api\ProductCustomOptionRepositoryInterface @@ -83,7 +85,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getList($sku) { @@ -92,7 +94,7 @@ public function getList($sku) } /** - * {@inheritdoc} + * @inheritdoc */ public function getProductOptions(ProductInterface $product, $requiredOnly = false) { @@ -104,7 +106,7 @@ public function getProductOptions(ProductInterface $product, $requiredOnly = fal } /** - * {@inheritdoc} + * @inheritdoc */ public function get($sku, $optionId) { @@ -117,7 +119,7 @@ public function get($sku, $optionId) } /** - * {@inheritdoc} + * @inheritdoc */ public function delete(\Magento\Catalog\Api\Data\ProductCustomOptionInterface $entity) { @@ -126,7 +128,7 @@ public function delete(\Magento\Catalog\Api\Data\ProductCustomOptionInterface $e } /** - * {@inheritdoc} + * @inheritdoc */ public function duplicate( \Magento\Catalog\Api\Data\ProductInterface $product, @@ -142,7 +144,7 @@ public function duplicate( } /** - * {@inheritdoc} + * @inheritdoc */ public function save(\Magento\Catalog\Api\Data\ProductCustomOptionInterface $option) { @@ -184,7 +186,7 @@ public function save(\Magento\Catalog\Api\Data\ProductCustomOptionInterface $opt } /** - * {@inheritdoc} + * @inheritdoc */ public function deleteByIdentifier($sku, $optionId) { @@ -209,8 +211,8 @@ public function deleteByIdentifier($sku, $optionId) /** * Mark original values for removal if they are absent among new values * - * @param $newValues array - * @param $originalValues \Magento\Catalog\Model\Product\Option\Value[] + * @param array $newValues + * @param \Magento\Catalog\Model\Product\Option\Value[] $originalValues * @return array */ protected function markRemovedValues($newValues, $originalValues) @@ -234,6 +236,8 @@ protected function markRemovedValues($newValues, $originalValues) } /** + * Get hydrator pool + * * @return \Magento\Framework\EntityManager\HydratorPool * @deprecated 101.0.0 */ diff --git a/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php b/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php index c4a2d60414a7..9cb6cda4d0a0 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Model\Product\Option; use Magento\Catalog\Api\ProductCustomOptionRepositoryInterface as OptionRepository; @@ -28,6 +30,8 @@ public function __construct( } /** + * Perform action on relation/extension attribute + * * @param object $entity * @param array $arguments * @return \Magento\Catalog\Api\Data\ProductInterface|object @@ -35,6 +39,10 @@ public function __construct( */ public function execute($entity, $arguments = []) { + if ($entity->getOptionsSaved()) { + return $entity; + } + $options = $entity->getOptions(); $optionIds = []; @@ -52,11 +60,26 @@ public function execute($entity, $arguments = []) } } if ($options) { - foreach ($options as $option) { - $this->optionRepository->save($option); - } + $this->processOptionsSaving($options, (bool)$entity->dataHasChangedFor('sku'), (string)$entity->getSku()); } return $entity; } + + /** + * Save custom options + * + * @param array $options + * @param bool $hasChangedSku + * @param string $newSku + */ + private function processOptionsSaving(array $options, bool $hasChangedSku, string $newSku) + { + foreach ($options as $option) { + if ($hasChangedSku && $option->hasData('product_sku')) { + $option->setProductSku($newSku); + } + $this->optionRepository->save($option); + } + } } diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php b/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php index 7517459da650..2b4739ebeb73 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php @@ -12,6 +12,7 @@ * Catalog product option date type * * @author Magento Core Team <core@magentocommerce.com> + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class Date extends \Magento\Catalog\Model\Product\Option\Type\DefaultType { @@ -102,11 +103,11 @@ public function validateUserValue($values) $this->setUserValue( [ 'date' => isset($value['date']) ? $value['date'] : '', - 'year' => isset($value['year']) ? intval($value['year']) : 0, - 'month' => isset($value['month']) ? intval($value['month']) : 0, - 'day' => isset($value['day']) ? intval($value['day']) : 0, - 'hour' => isset($value['hour']) ? intval($value['hour']) : 0, - 'minute' => isset($value['minute']) ? intval($value['minute']) : 0, + 'year' => isset($value['year']) ? (int) $value['year'] : 0, + 'month' => isset($value['month']) ? (int) $value['month'] : 0, + 'day' => isset($value['day']) ? (int) $value['day'] : 0, + 'hour' => isset($value['hour']) ? (int) $value['hour'] : 0, + 'minute' => isset($value['minute']) ? (int) $value['minute'] : 0, 'day_part' => isset($value['day_part']) ? $value['day_part'] : '', 'date_internal' => isset($value['date_internal']) ? $value['date_internal'] : '', ] @@ -147,7 +148,6 @@ public function validateUserValue($values) public function prepareForCart() { if ($this->getIsValid() && $this->getUserValue() !== null) { - $option = $this->getOption(); $value = $this->getUserValue(); if (isset($value['date_internal']) && $value['date_internal'] != '') { diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php b/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php index 2390a049fbeb..c388be8b6f39 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php @@ -279,7 +279,7 @@ public function getFormattedOptionValue($optionValue) */ public function getCustomizedView($optionInfo) { - return isset($optionInfo['value']) ? $optionInfo['value'] : $optionInfo; + return $optionInfo['value'] ?? $optionInfo; } /** diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ExistingValidate.php b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ExistingValidate.php new file mode 100644 index 000000000000..c9afdf023b30 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ExistingValidate.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Model\Product\Option\Type\File; + +/** + * Validator for existing (already saved) files. + */ +class ExistingValidate extends \Zend_Validate +{ + /** + * @inheritDoc + * + * @param string $value File's full path. + * @param string|null $originalName Original file's name (when uploaded). + */ + public function isValid($value, string $originalName = null) + { + $this->_messages = []; + $this->_errors = []; + + if (!is_string($value)) { + $this->_messages[] = __('Full file path is expected.')->render(); + return false; + } + + $result = true; + $fileInfo = null; + if ($originalName) { + $fileInfo = ['name' => $originalName]; + } + foreach ($this->_validators as $element) { + $validator = $element['instance']; + if ($validator->isValid($value, $fileInfo)) { + continue; + } + $result = false; + $messages = $validator->getMessages(); + $this->_messages = array_merge($this->_messages, $messages); + $this->_errors = array_merge($this->_errors, array_keys($messages)); + if ($element['breakChainOnFailure']) { + break; + } + } + return $result; + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidateFactory.php b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidateFactory.php index 32c901afe8e7..a7add0ad87b8 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidateFactory.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidateFactory.php @@ -6,13 +6,18 @@ namespace Magento\Catalog\Model\Product\Option\Type\File; +/** + * Class ValidateFactory. Creates Validator with type "ExistingValidate" + */ class ValidateFactory { /** + * Main factory method + * * @return \Zend_Validate */ public function create() { - return new \Zend_Validate(); + return new ExistingValidate(); } } diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFile.php b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFile.php index d6a5cb1cbc29..fef4999a1174 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFile.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFile.php @@ -10,8 +10,12 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Catalog\Model\Product\Exception as ProductException; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Math\Random; +use Magento\Framework\App\ObjectManager; /** + * Validator class. Represents logic for validation file given from product option + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ValidatorFile extends Validator @@ -63,11 +67,19 @@ class ValidatorFile extends Validator protected $isImageValidator; /** + * @var Random + */ + private $random; + + /** + * Constructor method + * * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\Framework\Filesystem $filesystem * @param \Magento\Framework\File\Size $fileSize * @param \Magento\Framework\HTTP\Adapter\FileTransferFactory $httpFactory * @param \Magento\Framework\Validator\File\IsImage $isImageValidator + * @param Random|null $random * @throws \Magento\Framework\Exception\FileSystemException */ public function __construct( @@ -75,16 +87,21 @@ public function __construct( \Magento\Framework\Filesystem $filesystem, \Magento\Framework\File\Size $fileSize, \Magento\Framework\HTTP\Adapter\FileTransferFactory $httpFactory, - \Magento\Framework\Validator\File\IsImage $isImageValidator + \Magento\Framework\Validator\File\IsImage $isImageValidator, + Random $random = null ) { $this->mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); $this->filesystem = $filesystem; $this->httpFactory = $httpFactory; $this->isImageValidator = $isImageValidator; + $this->random = $random + ?? ObjectManager::getInstance()->get(Random::class); parent::__construct($scopeConfig, $filesystem, $fileSize); } /** + * Setter method for the product + * * @param Product $product * @return $this */ @@ -95,6 +112,8 @@ public function setProduct(Product $product) } /** + * Validation method + * * @param \Magento\Framework\DataObject $processingParams * @param \Magento\Catalog\Model\Product\Option $option * @return array @@ -154,8 +173,6 @@ public function validate($processingParams, $option) $userValue = []; if ($upload->isUploaded($file) && $upload->isValid($file)) { - $extension = pathinfo(strtolower($fileInfo['name']), PATHINFO_EXTENSION); - $fileName = \Magento\MediaStorage\Model\File\Uploader::getCorrectFileName($fileInfo['name']); $dispersion = \Magento\MediaStorage\Model\File\Uploader::getDispersionPath($fileName); @@ -163,7 +180,8 @@ public function validate($processingParams, $option) $tmpDirectory = $this->filesystem->getDirectoryRead(DirectoryList::SYS_TMP); $fileHash = md5($tmpDirectory->readFile($tmpDirectory->getRelativePath($fileInfo['tmp_name']))); - $filePath .= '/' . $fileHash . '.' . $extension; + $fileRandomName = $this->random->getRandomString(32); + $filePath .= '/' .$fileRandomName; $fileFullPath = $this->mediaDirectory->getAbsolutePath($this->quotePath . $filePath); $upload->addFilter(new \Zend_Filter_File_Rename(['target' => $fileFullPath, 'overwrite' => true])); @@ -243,6 +261,8 @@ protected function initFilesystem() } /** + * Validate contents length method + * * @return bool * @todo need correctly name */ diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorInfo.php b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorInfo.php index 37e4c7b310a8..100ad37273cf 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorInfo.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorInfo.php @@ -6,6 +6,9 @@ namespace Magento\Catalog\Model\Product\Option\Type\File; +/** + * Validator for existing files. + */ class ValidatorInfo extends Validator { /** @@ -34,6 +37,8 @@ class ValidatorInfo extends Validator protected $fileRelativePath; /** + * Construct method + * * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\Framework\Filesystem $filesystem * @param \Magento\Framework\File\Size $fileSize @@ -53,6 +58,8 @@ public function __construct( } /** + * Setter method for property "useQuotePath" + * * @param mixed $useQuotePath * @return $this */ @@ -63,6 +70,8 @@ public function setUseQuotePath($useQuotePath) } /** + * Validate method for the option value depends on an option + * * @param array $optionValue * @param \Magento\Catalog\Model\Product\Option $option * @return bool @@ -90,7 +99,7 @@ public function validate($optionValue, $option) } $result = false; - if ($validatorChain->isValid($this->fileFullPath)) { + if ($validatorChain->isValid($this->fileFullPath, $optionValue['title'])) { $result = $this->rootDirectory->isReadable($this->fileRelativePath) && isset($optionValue['secret_key']) && $this->buildSecretKey($this->fileRelativePath) == $optionValue['secret_key']; @@ -109,6 +118,8 @@ public function validate($optionValue, $option) } /** + * Method for creation secret key for the given file + * * @param string $fileRelativePath * @return string */ @@ -118,6 +129,8 @@ protected function buildSecretKey($fileRelativePath) } /** + * Calculates path for the file + * * @param array $optionValue * @return void */ diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php b/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php index 4a257a478106..d88dd5836289 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php @@ -71,7 +71,7 @@ public function validateUserValue($values) } if (!$this->_isSingleSelection()) { $valuesCollection = $option->getOptionValuesByOptionId($value, $this->getProduct()->getStoreId())->load(); - $valueCount = is_array($value) ? count($value) : 1; + $valueCount = is_array($value) ? count($value) : 0; if ($valuesCollection->count() != $valueCount) { $this->setIsValid(false); throw new LocalizedException( diff --git a/app/code/Magento/Catalog/Model/Product/Option/Validator/DefaultValidator.php b/app/code/Magento/Catalog/Model/Product/Option/Validator/DefaultValidator.php index ee508e30cc93..08455430ccac 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Validator/DefaultValidator.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Validator/DefaultValidator.php @@ -9,6 +9,9 @@ use Magento\Catalog\Model\Product\Option; use Zend_Validate_Exception; +/** + * Product option default validator + */ class DefaultValidator extends \Magento\Framework\Validator\AbstractValidator { /** @@ -25,13 +28,20 @@ class DefaultValidator extends \Magento\Framework\Validator\AbstractValidator */ protected $priceTypes; + /** + * @var \Magento\Framework\Locale\FormatInterface + */ + private $localeFormat; + /** * @param \Magento\Catalog\Model\ProductOptions\ConfigInterface $productOptionConfig * @param \Magento\Catalog\Model\Config\Source\Product\Options\Price $priceConfig + * @param \Magento\Framework\Locale\FormatInterface|null $localeFormat */ public function __construct( \Magento\Catalog\Model\ProductOptions\ConfigInterface $productOptionConfig, - \Magento\Catalog\Model\Config\Source\Product\Options\Price $priceConfig + \Magento\Catalog\Model\Config\Source\Product\Options\Price $priceConfig, + \Magento\Framework\Locale\FormatInterface $localeFormat = null ) { foreach ($productOptionConfig->getAll() as $option) { foreach ($option['types'] as $type) { @@ -42,6 +52,9 @@ public function __construct( foreach ($priceConfig->toOptionArray() as $item) { $this->priceTypes[] = $item['value']; } + + $this->localeFormat = $localeFormat ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Framework\Locale\FormatInterface::class); } /** @@ -134,11 +147,11 @@ protected function validateOptionType(Option $option) */ protected function validateOptionValue(Option $option) { - return $this->isInRange($option->getPriceType(), $this->priceTypes); + return $this->isInRange($option->getPriceType(), $this->priceTypes) && $this->isNumber($option->getPrice()); } /** - * Check whether value is empty + * Check whether the value is empty * * @param mixed $value * @return bool @@ -149,7 +162,7 @@ protected function isEmpty($value) } /** - * Check whether value is in range + * Check whether the value is in range * * @param string $value * @param array $range @@ -161,13 +174,24 @@ protected function isInRange($value, array $range) } /** - * Check whether value is not negative + * Check whether the value is negative * * @param string $value * @return bool */ protected function isNegative($value) { - return intval($value) < 0; + return $this->localeFormat->getNumber($value) < 0; + } + + /** + * Check whether the value is a number + * + * @param string $value + * @return bool + */ + public function isNumber($value) + { + return is_numeric($this->localeFormat->getNumber($value)); } } diff --git a/app/code/Magento/Catalog/Model/Product/Option/Validator/Pool.php b/app/code/Magento/Catalog/Model/Product/Option/Validator/Pool.php index 1e0065424955..2256f031098f 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Validator/Pool.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Validator/Pool.php @@ -29,6 +29,6 @@ public function __construct(array $validators) */ public function get($type) { - return isset($this->validators[$type]) ? $this->validators[$type] : $this->validators['default']; + return $this->validators[$type] ?? $this->validators['default']; } } diff --git a/app/code/Magento/Catalog/Model/Product/Option/Validator/Select.php b/app/code/Magento/Catalog/Model/Product/Option/Validator/Select.php index 44756890b6ed..209531f59981 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Validator/Select.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Validator/Select.php @@ -8,6 +8,9 @@ use Magento\Catalog\Model\Product\Option; +/** + * Select validator class + */ class Select extends DefaultValidator { /** @@ -83,7 +86,7 @@ protected function isValidOptionPrice($priceType, $price, $storeId) if (!$priceType && !$price) { return true; } - if (!$this->isInRange($priceType, $this->priceTypes)) { + if (!$this->isInRange($priceType, $this->priceTypes) || !$this->isNumber($price)) { return false; } diff --git a/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php b/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php index 1bddd2d07cd8..3ee064670a46 100644 --- a/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php +++ b/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php @@ -97,7 +97,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function get(array $skus) { @@ -107,7 +107,7 @@ public function get(array $skus) } /** - * {@inheritdoc} + * @inheritdoc */ public function update(array $prices) { @@ -128,7 +128,7 @@ public function update(array $prices) } /** - * {@inheritdoc} + * @inheritdoc */ public function replace(array $prices) { @@ -144,7 +144,7 @@ public function replace(array $prices) } /** - * {@inheritdoc} + * @inheritdoc */ public function delete(array $prices) { @@ -171,16 +171,17 @@ private function getExistingPrices(array $skus, $groupBySku = false) $ids = $this->retrieveAffectedIds($skus); $rawPrices = $this->tierPricePersistence->get($ids); $prices = []; - - $linkField = $this->tierPricePersistence->getEntityLinkField(); - $skuByIdLookup = $this->buildSkuByIdLookup($skus); - foreach ($rawPrices as $rawPrice) { - $sku = $skuByIdLookup[$rawPrice[$linkField]]; - $price = $this->tierPriceFactory->create($rawPrice, $sku); - if ($groupBySku) { - $prices[$sku][] = $price; - } else { - $prices[] = $price; + if ($rawPrices) { + $linkField = $this->tierPricePersistence->getEntityLinkField(); + $skuByIdLookup = $this->buildSkuByIdLookup($skus); + foreach ($rawPrices as $rawPrice) { + $sku = $skuByIdLookup[$rawPrice[$linkField]]; + $price = $this->tierPriceFactory->create($rawPrice, $sku); + if ($groupBySku) { + $prices[$sku][] = $price; + } else { + $prices[] = $price; + } } } diff --git a/app/code/Magento/Catalog/Model/Product/PriceModifier.php b/app/code/Magento/Catalog/Model/Product/PriceModifier.php index 48d53b461452..c4d5bdfedcd5 100644 --- a/app/code/Magento/Catalog/Model/Product/PriceModifier.php +++ b/app/code/Magento/Catalog/Model/Product/PriceModifier.php @@ -9,6 +9,9 @@ use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\NoSuchEntityException; +/** + * Product form price modifier + */ class PriceModifier { /** @@ -26,6 +29,8 @@ public function __construct( } /** + * Remove tier price + * * @param \Magento\Catalog\Model\Product $product * @param int|string $customerGroupId * @param int $qty @@ -46,11 +51,11 @@ public function removeTierPrice(\Magento\Catalog\Model\Product $product, $custom foreach ($prices as $key => $tierPrice) { if ($customerGroupId == 'all' && $tierPrice['price_qty'] == $qty - && $tierPrice['all_groups'] == 1 && intval($tierPrice['website_id']) === intval($websiteId) + && $tierPrice['all_groups'] == 1 && (int) $tierPrice['website_id'] === (int) $websiteId ) { unset($prices[$key]); } elseif ($tierPrice['price_qty'] == $qty && $tierPrice['cust_group'] == $customerGroupId - && intval($tierPrice['website_id']) === intval($websiteId) + && (int) $tierPrice['website_id'] === (int) $websiteId ) { unset($prices[$key]); } diff --git a/app/code/Magento/Catalog/Model/Product/ProductFrontendAction/Synchronizer.php b/app/code/Magento/Catalog/Model/Product/ProductFrontendAction/Synchronizer.php index 3ec8e968aa24..24775a791e59 100644 --- a/app/code/Magento/Catalog/Model/Product/ProductFrontendAction/Synchronizer.php +++ b/app/code/Magento/Catalog/Model/Product/ProductFrontendAction/Synchronizer.php @@ -16,6 +16,8 @@ use Magento\Framework\EntityManager\EntityManager; /** + * A Product Widget Synchronizer. + * * Service which allows to sync product widget information, such as product id with db. In order to reuse this info * on different devices */ @@ -85,9 +87,10 @@ public function __construct( } /** - * Find lifetime in configuration. Configuration is hold in Stores Configuration - * Also this configuration is generated by: - * @see \Magento\Catalog\Model\Widget\RecentlyViewedStorageConfiguration + * Finds lifetime in configuration. + * + * Configuration is hold in Stores Configuration. Also this configuration is generated by + * {@see Magento\Catalog\Model\Widget\RecentlyViewedStorageConfiguration} * * @param string $namespace * @return int @@ -108,6 +111,8 @@ private function getLifeTimeByNamespace($namespace) } /** + * Filters actions. + * * In order to avoid suspicious actions, we need to filter them in DESC order, and slice only items that * can be persisted in database. * @@ -138,7 +143,9 @@ private function getProductIdsByActions(array $actions) $productIds = []; foreach ($actions as $action) { - $productIds[] = $action['product_id']; + if (isset($action['product_id'])) { + $productIds[] = $action['product_id']; + } } return $productIds; @@ -159,33 +166,37 @@ public function syncActions(array $productsData, $typeId) $customerId = $this->session->getCustomerId(); $visitorId = $this->visitor->getId(); $collection = $this->getActionsByType($typeId); - $collection->addFieldToFilter('product_id', $this->getProductIdsByActions($productsData)); - - /** - * Note that collection is also filtered by visitor id and customer id - * This collection shouldn't be flushed when visitor has products and then login - * It can remove only products for visitor, or only products for customer - * - * ['product_id' => 'added_at'] - * @var ProductFrontendActionInterface $item - */ - foreach ($collection as $item) { - $this->entityManager->delete($item); - } - - foreach ($productsData as $productId => $productData) { - /** @var ProductFrontendActionInterface $action */ - $action = $this->productFrontendActionFactory->create([ - 'data' => [ - 'visitor_id' => $customerId ? null : $visitorId, - 'customer_id' => $this->session->getCustomerId(), - 'added_at' => $productData['added_at'], - 'product_id' => $productId, - 'type_id' => $typeId - ] - ]); - - $this->entityManager->save($action); + $productIds = $this->getProductIdsByActions($productsData); + + if ($productIds) { + $collection->addFieldToFilter('product_id', $productIds); + + /** + * Note that collection is also filtered by visitor id and customer id + * This collection shouldn't be flushed when visitor has products and then login + * It can remove only products for visitor, or only products for customer + * + * ['product_id' => 'added_at'] + * @var ProductFrontendActionInterface $item + */ + foreach ($collection as $item) { + $this->entityManager->delete($item); + } + + foreach ($productsData as $productId => $productData) { + /** @var ProductFrontendActionInterface $action */ + $action = $this->productFrontendActionFactory->create([ + 'data' => [ + 'visitor_id' => $customerId ? null : $visitorId, + 'customer_id' => $this->session->getCustomerId(), + 'added_at' => $productData['added_at'], + 'product_id' => $productId, + 'type_id' => $typeId + ] + ]); + + $this->entityManager->save($action); + } } } diff --git a/app/code/Magento/Catalog/Model/Product/ProductList/ToolbarMemorizer.php b/app/code/Magento/Catalog/Model/Product/ProductList/ToolbarMemorizer.php new file mode 100644 index 000000000000..9c1a781d594f --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/ProductList/ToolbarMemorizer.php @@ -0,0 +1,179 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Model\Product\ProductList; + +use Magento\Catalog\Model\Session as CatalogSession; +use Magento\Framework\App\Config\ScopeConfigInterface; + +/** + * Class ToolbarMemorizer + * + * Responds for saving toolbar settings to catalog session + */ +class ToolbarMemorizer +{ + /** + * XML PATH to enable/disable saving toolbar parameters to session + */ + const XML_PATH_CATALOG_REMEMBER_PAGINATION = 'catalog/frontend/remember_pagination'; + + /** + * @var CatalogSession + */ + private $catalogSession; + + /** + * @var Toolbar + */ + private $toolbarModel; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var string|bool + */ + private $order; + + /** + * @var string|bool + */ + private $direction; + + /** + * @var string|bool + */ + private $mode; + + /** + * @var string|bool + */ + private $limit; + + /** + * @var bool + */ + private $isMemorizingAllowed; + + /** + * @param Toolbar $toolbarModel + * @param CatalogSession $catalogSession + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct( + Toolbar $toolbarModel, + CatalogSession $catalogSession, + ScopeConfigInterface $scopeConfig + ) { + $this->toolbarModel = $toolbarModel; + $this->catalogSession = $catalogSession; + $this->scopeConfig = $scopeConfig; + } + + /** + * Get sort order + * + * @return string|bool + */ + public function getOrder() + { + if ($this->order === null) { + $this->order = $this->toolbarModel->getOrder() ?? + ($this->isMemorizingAllowed() ? $this->catalogSession->getData(Toolbar::ORDER_PARAM_NAME) : null); + } + return $this->order; + } + + /** + * Get sort direction + * + * @return string|bool + */ + public function getDirection() + { + if ($this->direction === null) { + $this->direction = $this->toolbarModel->getDirection() ?? + ($this->isMemorizingAllowed() ? $this->catalogSession->getData(Toolbar::DIRECTION_PARAM_NAME) : null); + } + return $this->direction; + } + + /** + * Get sort mode + * + * @return string|bool + */ + public function getMode() + { + if ($this->mode === null) { + $this->mode = $this->toolbarModel->getMode() ?? + ($this->isMemorizingAllowed() ? $this->catalogSession->getData(Toolbar::MODE_PARAM_NAME) : null); + } + return $this->mode; + } + + /** + * Get products per page limit + * + * @return string|bool + */ + public function getLimit() + { + if ($this->limit === null) { + $this->limit = $this->toolbarModel->getLimit() ?? + ($this->isMemorizingAllowed() ? $this->catalogSession->getData(Toolbar::LIMIT_PARAM_NAME) : null); + } + return $this->limit; + } + + /** + * Method to save all catalog parameters in catalog session + * + * @return void + */ + public function memorizeParams() + { + if (!$this->catalogSession->getParamsMemorizeDisabled() && $this->isMemorizingAllowed()) { + $this->memorizeParam(Toolbar::ORDER_PARAM_NAME, $this->getOrder()) + ->memorizeParam(Toolbar::DIRECTION_PARAM_NAME, $this->getDirection()) + ->memorizeParam(Toolbar::MODE_PARAM_NAME, $this->getMode()) + ->memorizeParam(Toolbar::LIMIT_PARAM_NAME, $this->getLimit()); + } + } + + /** + * Check configuration for enabled/disabled toolbar memorizing + * + * @return bool + */ + public function isMemorizingAllowed() + { + if ($this->isMemorizingAllowed === null) { + $this->isMemorizingAllowed = $this->scopeConfig->isSetFlag(self::XML_PATH_CATALOG_REMEMBER_PAGINATION); + } + return $this->isMemorizingAllowed; + } + + /** + * Memorize parameter value for session + * + * @param string $param parameter name + * @param mixed $value parameter value + * @return $this + */ + private function memorizeParam($param, $value) + { + if ($value && $this->catalogSession->getData($param) != $value) { + $this->catalogSession->setData($param, $value); + } + return $this; + } +} diff --git a/app/code/Magento/Catalog/Model/Product/TierPriceManagement.php b/app/code/Magento/Catalog/Model/Product/TierPriceManagement.php index 822959bfc851..f2da1e770279 100644 --- a/app/code/Magento/Catalog/Model/Product/TierPriceManagement.php +++ b/app/code/Magento/Catalog/Model/Product/TierPriceManagement.php @@ -15,6 +15,8 @@ use Magento\Framework\Exception\TemporaryStateExceptionInterface; /** + * Product tier price management + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class TierPriceManagement implements \Magento\Catalog\Api\ProductTierPriceManagementInterface @@ -82,7 +84,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -148,7 +150,7 @@ public function add($sku, $customerGroupId, $price, $qty) } /** - * {@inheritdoc} + * @inheritdoc */ public function remove($sku, $customerGroupId, $qty) { @@ -163,7 +165,7 @@ public function remove($sku, $customerGroupId, $qty) } /** - * {@inheritdoc} + * @inheritdoc */ public function getList($sku, $customerGroupId) { @@ -181,7 +183,7 @@ public function getList($sku, $customerGroupId) $prices = []; foreach ($product->getData('tier_price') as $price) { - if ((is_numeric($customerGroupId) && intval($price['cust_group']) === intval($customerGroupId)) + if ((is_numeric($customerGroupId) && (int) $price['cust_group'] === (int) $customerGroupId) || ($customerGroupId === 'all' && $price['all_groups']) ) { /** @var \Magento\Catalog\Api\Data\ProductTierPriceInterface $tierPrice */ diff --git a/app/code/Magento/Catalog/Model/Product/Type.php b/app/code/Magento/Catalog/Model/Product/Type.php index dc3971397acb..4c973be20dee 100644 --- a/app/code/Magento/Catalog/Model/Product/Type.php +++ b/app/code/Magento/Catalog/Model/Product/Type.php @@ -232,7 +232,7 @@ public function getOptions() public function getOptionText($optionId) { $options = $this->getOptionArray(); - return isset($options[$optionId]) ? $options[$optionId] : null; + return $options[$optionId] ?? null; } /** @@ -285,7 +285,7 @@ public function getTypesByPriority() $types = $this->getTypes(); foreach ($types as $typeId => $typeInfo) { - $priority = isset($typeInfo['index_priority']) ? abs(intval($typeInfo['index_priority'])) : 0; + $priority = isset($typeInfo['index_priority']) ? abs((int) $typeInfo['index_priority']) : 0; if (!empty($typeInfo['composite'])) { $compositePriority[$typeId] = $priority; } else { @@ -307,7 +307,7 @@ public function getTypesByPriority() } /** - * {@inheritdoc} + * @inheritdoc */ public function toOptionArray() { diff --git a/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php b/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php index 1b5cf37f6cbb..e6804d9246fa 100644 --- a/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php +++ b/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php @@ -935,7 +935,7 @@ public function getForceChildItemQtyChanges($product) */ public function prepareQuoteItemQty($qty, $product) { - return floatval($qty); + return (float)$qty; } /** diff --git a/app/code/Magento/Catalog/Model/Product/Type/FrontSpecialPrice.php b/app/code/Magento/Catalog/Model/Product/Type/FrontSpecialPrice.php new file mode 100644 index 000000000000..dabfdb74f011 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Type/FrontSpecialPrice.php @@ -0,0 +1,133 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Product\Type; + +use Magento\Store\Model\Store; +use Magento\Catalog\Model\ResourceModel\Product\Price\SpecialPrice; +use Magento\Catalog\Api\Data\SpecialPriceInterface; +use Magento\Store\Api\Data\WebsiteInterface; + +/** + * Product special price model. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * + * @deprecated + * @see \Magento\Catalog\Model\Product\Type\Price + */ +class FrontSpecialPrice extends Price +{ + /** + * @var SpecialPrice + */ + private $specialPrice; + + /** + * @param \Magento\CatalogRule\Model\ResourceModel\RuleFactory $ruleFactory + * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate + * @param \Magento\Customer\Model\Session $customerSession + * @param \Magento\Framework\Event\ManagerInterface $eventManager + * @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency + * @param \Magento\Customer\Api\GroupManagementInterface $groupManagement + * @param \Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory $tierPriceFactory + * @param \Magento\Framework\App\Config\ScopeConfigInterface $config + * @param SpecialPrice $specialPrice + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + \Magento\CatalogRule\Model\ResourceModel\RuleFactory $ruleFactory, + \Magento\Store\Model\StoreManagerInterface $storeManager, + \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, + \Magento\Customer\Model\Session $customerSession, + \Magento\Framework\Event\ManagerInterface $eventManager, + \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency, + \Magento\Customer\Api\GroupManagementInterface $groupManagement, + \Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory $tierPriceFactory, + \Magento\Framework\App\Config\ScopeConfigInterface $config, + SpecialPrice $specialPrice + ) { + $this->specialPrice = $specialPrice; + parent::__construct( + $ruleFactory, + $storeManager, + $localeDate, + $customerSession, + $eventManager, + $priceCurrency, + $groupManagement, + $tierPriceFactory, + $config + ); + } + + /** + * @inheritdoc + * + * @deprecated + */ + protected function _applySpecialPrice($product, $finalPrice) + { + if (!$product->getSpecialPrice()) { + return $finalPrice; + } + + $specialPrices = $this->getSpecialPrices($product); + $specialPrice = !(empty($specialPrices)) ? min($specialPrices) : $product->getSpecialPrice(); + + $specialPrice = $this->calculateSpecialPrice( + $finalPrice, + $specialPrice, + $product->getSpecialFromDate(), + $product->getSpecialToDate(), + WebsiteInterface::ADMIN_CODE + ); + $product->setData('special_price', $specialPrice); + + return $specialPrice; + } + + /** + * Get special prices. + * + * @param mixed $product + * @return array + */ + private function getSpecialPrices($product): array + { + $allSpecialPrices = $this->specialPrice->get([$product->getSku()]); + $specialPrices = []; + foreach ($allSpecialPrices as $price) { + if ($this->isSuitableSpecialPrice($product, $price)) { + $specialPrices[] = $price['value']; + } + } + + return $specialPrices; + } + + /** + * Price is suitable from default and current store + start and end date are equal. + * + * @param mixed $product + * @param array $price + * @return bool + */ + private function isSuitableSpecialPrice($product, array $price): bool + { + $priceStoreId = $price[Store::STORE_ID]; + if (($priceStoreId == Store::DEFAULT_STORE_ID || $product->getStoreId() == $priceStoreId) + && $price[SpecialPriceInterface::PRICE_FROM] == $product->getSpecialFromDate() + && $price[SpecialPriceInterface::PRICE_TO] == $product->getSpecialToDate()) { + return true; + } + + return false; + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Type/Price.php b/app/code/Magento/Catalog/Model/Product/Type/Price.php index f6caa299d66d..b30624b79dd5 100644 --- a/app/code/Magento/Catalog/Model/Product/Type/Price.php +++ b/app/code/Magento/Catalog/Model/Product/Type/Price.php @@ -11,12 +11,14 @@ use Magento\Store\Model\Store; use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory; use Magento\Framework\App\ObjectManager; +use Magento\Store\Api\Data\WebsiteInterface; /** * Product type price model * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @since 100.0.2 */ class Price @@ -184,6 +186,8 @@ public function getFinalPrice($qty, $product) } /** + * Retrieve final price for child product + * * @param Product $product * @param float $productQty * @param Product $childProduct @@ -428,6 +432,8 @@ public function setTierPrices($product, array $tierPrices = null) } /** + * Retrieve customer group id from product + * * @param Product $product * @return int */ @@ -453,7 +459,7 @@ protected function _applySpecialPrice($product, $finalPrice) $product->getSpecialPrice(), $product->getSpecialFromDate(), $product->getSpecialToDate(), - $product->getStore() + WebsiteInterface::ADMIN_CODE ); } @@ -601,7 +607,7 @@ public function calculatePrice( $specialPrice, $specialPriceFrom, $specialPriceTo, - $sId + WebsiteInterface::ADMIN_CODE ); if ($rulePrice === false) { diff --git a/app/code/Magento/Catalog/Model/Product/Url.php b/app/code/Magento/Catalog/Model/Product/Url.php index c291dc33feda..f3ac9f55d1ae 100644 --- a/app/code/Magento/Catalog/Model/Product/Url.php +++ b/app/code/Magento/Catalog/Model/Product/Url.php @@ -7,6 +7,7 @@ use Magento\UrlRewrite\Model\UrlFinderInterface; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; +use Magento\Framework\App\Config\ScopeConfigInterface; /** * Product Url model @@ -45,6 +46,11 @@ class Url extends \Magento\Framework\DataObject */ protected $urlFinder; + /** + * @var \Magento\Framework\App\Config\ScopeConfigInterface + */ + private $scopeConfig; + /** * @param \Magento\Framework\UrlFactory $urlFactory * @param \Magento\Store\Model\StoreManagerInterface $storeManager @@ -52,6 +58,7 @@ class Url extends \Magento\Framework\DataObject * @param \Magento\Framework\Session\SidResolverInterface $sidResolver * @param UrlFinderInterface $urlFinder * @param array $data + * @param ScopeConfigInterface|null $scopeConfig */ public function __construct( \Magento\Framework\UrlFactory $urlFactory, @@ -59,7 +66,8 @@ public function __construct( \Magento\Framework\Filter\FilterManager $filter, \Magento\Framework\Session\SidResolverInterface $sidResolver, UrlFinderInterface $urlFinder, - array $data = [] + array $data = [], + ScopeConfigInterface $scopeConfig = null ) { parent::__construct($data); $this->urlFactory = $urlFactory; @@ -67,16 +75,8 @@ public function __construct( $this->filter = $filter; $this->sidResolver = $sidResolver; $this->urlFinder = $urlFinder; - } - - /** - * Retrieve URL Instance - * - * @return \Magento\Framework\UrlInterface - */ - private function getUrlInstance() - { - return $this->urlFactory->create(); + $this->scopeConfig = $scopeConfig ?: + \Magento\Framework\App\ObjectManager::getInstance()->get(ScopeConfigInterface::class); } /** @@ -157,10 +157,19 @@ public function getUrl(\Magento\Catalog\Model\Product $product, $params = []) UrlRewrite::ENTITY_TYPE => \Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator::ENTITY_TYPE, UrlRewrite::STORE_ID => $storeId, ]; + $useCategories = $this->scopeConfig->getValue( + \Magento\Catalog\Helper\Product::XML_PATH_PRODUCT_URL_USE_CATEGORY, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + if ($categoryId) { $filterData[UrlRewrite::METADATA]['category_id'] = $categoryId; + } elseif (!$useCategories) { + $filterData[UrlRewrite::METADATA]['category_id'] = ''; } + $rewrite = $this->urlFinder->findOneByData($filterData); + if ($rewrite) { $requestPath = $rewrite->getRequestPath(); $product->setRequestPath($requestPath); @@ -194,6 +203,7 @@ public function getUrl(\Magento\Catalog\Model\Product $product, $params = []) $routeParams['_query'] = []; } - return $this->getUrlInstance()->setScope($storeId)->getUrl($routePath, $routeParams); + $url = $this->urlFactory->create()->setScope($storeId); + return $url->getUrl($routePath, $routeParams); } } diff --git a/app/code/Magento/Catalog/Model/Product/Website/ReadHandler.php b/app/code/Magento/Catalog/Model/Product/Website/ReadHandler.php index e81cdedd6d37..8acb4a6593a4 100644 --- a/app/code/Magento/Catalog/Model/Product/Website/ReadHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Website/ReadHandler.php @@ -9,6 +9,9 @@ use Magento\Catalog\Model\ResourceModel\Product\Website\Link as ProductWebsiteLink; use Magento\Framework\EntityManager\Operation\ExtensionInterface; +/** + * Add websites ids to product extension attributes. + */ class ReadHandler implements ExtensionInterface { /** @@ -18,7 +21,7 @@ class ReadHandler implements ExtensionInterface /** * ReadHandler constructor. - * @param ProductWebsiteLink $resourceModel + * @param ProductWebsiteLink $productWebsiteLink */ public function __construct( ProductWebsiteLink $productWebsiteLink @@ -27,6 +30,8 @@ public function __construct( } /** + * Add website ids to product extension attributes, if no set. + * * @param ProductInterface $product * @param array $arguments * @SuppressWarnings(PHPMD.UnusedFormalParameter) diff --git a/app/code/Magento/Catalog/Model/ProductCategoryList.php b/app/code/Magento/Catalog/Model/ProductCategoryList.php index 5bbae772d5c2..c3a88a505c51 100644 --- a/app/code/Magento/Catalog/Model/ProductCategoryList.php +++ b/app/code/Magento/Catalog/Model/ProductCategoryList.php @@ -80,7 +80,10 @@ public function getCategoryIds($productId) Select::SQL_UNION_ALL ); - $this->categoryIdList[$productId] = $this->productResource->getConnection()->fetchCol($unionSelect); + $this->categoryIdList[$productId] = array_map( + 'intval', + $this->productResource->getConnection()->fetchCol($unionSelect) + ); } return $this->categoryIdList[$productId]; diff --git a/app/code/Magento/Catalog/Model/ProductIdLocator.php b/app/code/Magento/Catalog/Model/ProductIdLocator.php index 2d9af6829ad6..2d382164f264 100644 --- a/app/code/Magento/Catalog/Model/ProductIdLocator.php +++ b/app/code/Magento/Catalog/Model/ProductIdLocator.php @@ -37,23 +37,43 @@ class ProductIdLocator implements \Magento\Catalog\Model\ProductIdLocatorInterfa */ private $idsBySku = []; + /** + * Batch size to iterate collection + * + * @var int + */ + private $batchSize; + /** * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool * @param \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $collectionFactory - * @param string $limitIdsBySkuValues + * @param string $idsLimit + * @param int $batchSize defines how many items can be processed by one iteration */ public function __construct( \Magento\Framework\EntityManager\MetadataPool $metadataPool, \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $collectionFactory, - $idsLimit + $idsLimit, + int $batchSize = 5000 ) { $this->metadataPool = $metadataPool; $this->collectionFactory = $collectionFactory; $this->idsLimit = (int)$idsLimit; + $this->batchSize = $batchSize; } /** - * {@inheritdoc} + * @inheritdoc + * + * Load product items by provided products SKUs. + * Products collection will be iterated by pages with the $this->batchSize as a page size (for a cases when to many + * products SKUs were provided in parameters. + * Loaded products will be chached in the $this->idsBySku variable, but in the end of the method these storage will + * be truncated to $idsLimit quantity. + * As a result array with the products data will be returned with the following scheme: + * $data['product_sku']['link_field_value' => 'product_type'] + * + * @throws \Exception */ public function retrieveProductIdsBySkus(array $skus) { @@ -72,8 +92,16 @@ public function retrieveProductIdsBySkus(array $skus) $linkField = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class) ->getLinkField(); - foreach ($collection as $item) { - $this->idsBySku[strtolower(trim($item->getSku()))][$item->getData($linkField)] = $item->getTypeId(); + $collection->setPageSize($this->batchSize); + $pages = $collection->getLastPageNumber(); + for ($currentPage = 1; $currentPage <= $pages; $currentPage++) { + $collection->setCurPage($currentPage); + foreach ($collection->getItems() as $item) { + $sku = strtolower(trim($item->getSku())); + $itemIdentifier = $item->getData($linkField); + $this->idsBySku[$sku][$itemIdentifier] = $item->getTypeId(); + } + $collection->clear(); } } diff --git a/app/code/Magento/Catalog/Model/ProductLink/CollectionProvider.php b/app/code/Magento/Catalog/Model/ProductLink/CollectionProvider.php index bc212adae2c3..b96aff148e75 100644 --- a/app/code/Magento/Catalog/Model/ProductLink/CollectionProvider.php +++ b/app/code/Magento/Catalog/Model/ProductLink/CollectionProvider.php @@ -9,6 +9,9 @@ use Magento\Catalog\Model\ProductLink\Converter\ConverterPool; use Magento\Framework\Exception\NoSuchEntityException; +/** + * Provides a collection of linked product items (crosssells, related, upsells, ...) + */ class CollectionProvider { /** @@ -47,22 +50,20 @@ public function getCollection(\Magento\Catalog\Model\Product $product, $type) $products = $this->providers[$type]->getLinkedProducts($product); $converter = $this->converterPool->getConverter($type); - $output = []; $sorterItems = []; foreach ($products as $item) { - $output[$item->getId()] = $converter->convert($item); + $itemId = $item->getId(); + $sorterItems[$itemId] = $converter->convert($item); + $sorterItems[$itemId]['position'] = $sorterItems[$itemId]['position'] ?? 0; } - foreach ($output as $item) { - $itemPosition = $item['position']; - if (!isset($sorterItems[$itemPosition])) { - $sorterItems[$itemPosition] = $item; - } else { - $newPosition = $itemPosition + 1; - $sorterItems[$newPosition] = $item; - } - } - ksort($sorterItems); + usort($sorterItems, function ($itemA, $itemB) { + $posA = (int)$itemA['position']; + $posB = (int)$itemB['position']; + + return $posA <=> $posB; + }); + return $sorterItems; } } diff --git a/app/code/Magento/Catalog/Model/ProductLink/Repository.php b/app/code/Magento/Catalog/Model/ProductLink/Repository.php index 5bac99dbebbb..98977de7effa 100644 --- a/app/code/Magento/Catalog/Model/ProductLink/Repository.php +++ b/app/code/Magento/Catalog/Model/ProductLink/Repository.php @@ -10,6 +10,7 @@ use Magento\Catalog\Api\Data\ProductLinkExtensionFactory; use Magento\Catalog\Model\Product\Initialization\Helper\ProductLinks as LinksInitializer; use Magento\Catalog\Model\Product\LinkTypeProvider; +use Magento\Framework\Api\SimpleDataObjectConverter; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\EntityManager\MetadataPool; @@ -170,7 +171,7 @@ public function getList(\Magento\Catalog\Api\Data\ProductInterface $product) foreach ($item['custom_attributes'] as $option) { $name = $option['attribute_code']; $value = $option['value']; - $setterName = 'set'.ucfirst($name); + $setterName = 'set' . SimpleDataObjectConverter::snakeCaseToUpperCamelCase($name); // Check if setter exists if (method_exists($productLinkExtension, $setterName)) { call_user_func([$productLinkExtension, $setterName], $value); diff --git a/app/code/Magento/Catalog/Model/ProductRender.php b/app/code/Magento/Catalog/Model/ProductRender.php index 702c04b910d4..5efb0343cd99 100644 --- a/app/code/Magento/Catalog/Model/ProductRender.php +++ b/app/code/Magento/Catalog/Model/ProductRender.php @@ -206,7 +206,7 @@ public function getExtensionAttributes() * Set an extension attributes object. * * @param \Magento\Catalog\Api\Data\ProductRenderExtensionInterface $extensionAttributes - * @return $this + * @return void */ public function setExtensionAttributes( \Magento\Catalog\Api\Data\ProductRenderExtensionInterface $extensionAttributes diff --git a/app/code/Magento/Catalog/Model/ProductRender/Image.php b/app/code/Magento/Catalog/Model/ProductRender/Image.php index 774199a0dbf0..5e024938d37e 100644 --- a/app/code/Magento/Catalog/Model/ProductRender/Image.php +++ b/app/code/Magento/Catalog/Model/ProductRender/Image.php @@ -9,14 +9,16 @@ use Magento\Catalog\Api\Data\ProductRender\ImageInterface; /** - * @inheritdoc + * Product image renderer model. */ class Image extends \Magento\Framework\Model\AbstractExtensibleModel implements ImageInterface { /** + * Set url to image. + * * @param string $url - * @return @return void + * @return void */ public function setUrl($url) { @@ -34,6 +36,8 @@ public function getUrl() } /** + * Retrieve image code. + * * @return string */ public function getCode() @@ -42,6 +46,8 @@ public function getCode() } /** + * Set image code. + * * @param string $code * @return void */ @@ -51,6 +57,8 @@ public function setCode($code) } /** + * Set image height. + * * @param string $height * @return void */ @@ -60,6 +68,8 @@ public function setHeight($height) } /** + * Retrieve image height. + * * @return float */ public function getHeight() @@ -68,6 +78,8 @@ public function getHeight() } /** + * Retrieve image width. + * * @return float */ public function getWidth() @@ -76,6 +88,8 @@ public function getWidth() } /** + * Set image width. + * * @param string $width * @return void */ @@ -85,6 +99,8 @@ public function setWidth($width) } /** + * Retrieve image label. + * * @return string */ public function getLabel() @@ -93,6 +109,8 @@ public function getLabel() } /** + * Set image label. + * * @param string $label * @return void */ @@ -102,6 +120,8 @@ public function setLabel($label) } /** + * Retrieve image width after image resize. + * * @return float */ public function getResizedWidth() @@ -110,6 +130,8 @@ public function getResizedWidth() } /** + * Set image width after image resize. + * * @param string $width * @return void */ @@ -119,6 +141,8 @@ public function setResizedWidth($width) } /** + * Set image height after image resize. + * * @param string $height * @return void */ @@ -128,6 +152,8 @@ public function setResizedHeight($height) } /** + * Retrieve image height after image resize. + * * @return float */ public function getResizedHeight() @@ -149,7 +175,7 @@ public function getExtensionAttributes() * Set an extension attributes object. * * @param \Magento\Catalog\Api\Data\ProductRender\ImageExtensionInterface $extensionAttributes - * @return $this + * @return void */ public function setExtensionAttributes( \Magento\Catalog\Api\Data\ProductRender\ImageExtensionInterface $extensionAttributes diff --git a/app/code/Magento/Catalog/Model/ProductRenderList.php b/app/code/Magento/Catalog/Model/ProductRenderList.php index a3d906cf10c1..d1f60c098630 100644 --- a/app/code/Magento/Catalog/Model/ProductRenderList.php +++ b/app/code/Magento/Catalog/Model/ProductRenderList.php @@ -17,8 +17,8 @@ /** * Provide product render information (this information should be enough for rendering product on front) - * for one or few products * + * Render information provided for one or few products */ class ProductRenderList implements ProductRenderListInterface { @@ -64,7 +64,6 @@ class ProductRenderList implements ProductRenderListInterface * @param ProductRenderSearchResultsFactory $searchResultFactory * @param ProductRenderFactory $productRenderDtoFactory * @param Config $config - * @param Product\Visibility $productVisibility * @param CollectionModifier $collectionModifier * @param array $productAttributes */ diff --git a/app/code/Magento/Catalog/Model/ProductRepository.php b/app/code/Magento/Catalog/Model/ProductRepository.php index 0dbfda210347..c87b6e976320 100644 --- a/app/code/Magento/Catalog/Model/ProductRepository.php +++ b/app/code/Magento/Catalog/Model/ProductRepository.php @@ -1,18 +1,18 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Catalog\Model; +use Magento\Catalog\Api\CategoryLinkManagementInterface; use Magento\Catalog\Api\Data\ProductExtension; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Product\Gallery\MimeTypeExtensionMap; +use Magento\Catalog\Model\ProductRepository\MediaGalleryProcessor; use Magento\Catalog\Model\ResourceModel\Product\Collection; use Magento\Eav\Model\Entity\Attribute\Exception as AttributeException; -use Magento\Framework\Api\Data\ImageContentInterface; use Magento\Framework\Api\Data\ImageContentInterfaceFactory; use Magento\Framework\Api\ImageContentValidatorInterface; use Magento\Framework\Api\ImageProcessorInterface; @@ -20,15 +20,17 @@ use Magento\Framework\DB\Adapter\ConnectionException; use Magento\Framework\DB\Adapter\DeadlockException; use Magento\Framework\DB\Adapter\LockWaitException; +use Magento\Framework\EntityManager\Operation\Read\ReadExtensions; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; -use Magento\Framework\Exception\TemporaryState\CouldNotSaveException as TemporaryCouldNotSaveException; use Magento\Framework\Exception\StateException; +use Magento\Framework\Exception\TemporaryState\CouldNotSaveException as TemporaryCouldNotSaveException; use Magento\Framework\Exception\ValidatorException; /** + * Product Repository. * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) */ @@ -120,11 +122,15 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa protected $fileSystem; /** + * @deprecated + * * @var ImageContentInterfaceFactory */ protected $contentFactory; /** + * @deprecated + * * @var ImageProcessorInterface */ protected $imageProcessor; @@ -135,10 +141,17 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa protected $extensionAttributesJoinProcessor; /** + * @deprecated + * * @var \Magento\Catalog\Model\Product\Gallery\Processor */ protected $mediaGalleryProcessor; + /** + * @var MediaGalleryProcessor + */ + private $mediaProcessor; + /** * @var CollectionProcessorInterface */ @@ -154,6 +167,16 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa */ private $serializer; + /** + * @var ReadExtensions + */ + private $readExtensions; + + /** + * @var CategoryLinkManagementInterface + */ + private $linkManagement; + /** * ProductRepository constructor. * @param ProductFactory $productFactory @@ -179,6 +202,8 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa * @param CollectionProcessorInterface $collectionProcessor [optional] * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer * @param int $cacheLimit [optional] + * @param ReadExtensions $readExtensions + * @param CategoryLinkManagementInterface $linkManagement * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -205,7 +230,9 @@ public function __construct( \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $extensionAttributesJoinProcessor, CollectionProcessorInterface $collectionProcessor = null, \Magento\Framework\Serialize\Serializer\Json $serializer = null, - $cacheLimit = 1000 + $cacheLimit = 1000, + ReadExtensions $readExtensions = null, + CategoryLinkManagementInterface $linkManagement = null ) { $this->productFactory = $productFactory; $this->collectionFactory = $collectionFactory; @@ -228,10 +255,14 @@ public function __construct( $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Framework\Serialize\Serializer\Json::class); $this->cacheLimit = (int)$cacheLimit; + $this->readExtensions = $readExtensions ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(ReadExtensions::class); + $this->linkManagement = $linkManagement ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(CategoryLinkManagementInterface::class); } /** - * {@inheritdoc} + * @inheritdoc */ public function get($sku, $editMode = false, $storeId = null, $forceReload = false) { @@ -261,7 +292,7 @@ public function get($sku, $editMode = false, $storeId = null, $forceReload = fal } /** - * {@inheritdoc} + * @inheritdoc */ public function getById($productId, $editMode = false, $storeId = null, $forceReload = false) { @@ -351,6 +382,8 @@ protected function initializeProductData(array $productData, $createNew) } /** + * Assign product to websites. + * * @param \Magento\Catalog\Model\Product $product * @return void */ @@ -366,6 +399,11 @@ private function assignProductToWebsites(\Magento\Catalog\Model\Product $product } /** + * Process new gallery media entry. + * + * @deprecated + * @see MediaGalleryProcessor::processNewMediaGalleryEntry() + * * @param ProductInterface $product * @param array $newEntry * @return $this @@ -377,40 +415,8 @@ protected function processNewMediaGalleryEntry( ProductInterface $product, array $newEntry ) { - /** @var ImageContentInterface $contentDataObject */ - $contentDataObject = $newEntry['content']; - - /** @var \Magento\Catalog\Model\Product\Media\Config $mediaConfig */ - $mediaConfig = $product->getMediaConfig(); - $mediaTmpPath = $mediaConfig->getBaseTmpMediaPath(); - - $relativeFilePath = $this->imageProcessor->processImageContent($mediaTmpPath, $contentDataObject); - $tmpFilePath = $mediaConfig->getTmpMediaShortUrl($relativeFilePath); + $this->getMediaGalleryProcessor()->processNewMediaGalleryEntry($product, $newEntry); - if (!$product->hasGalleryAttribute()) { - throw new StateException( - __("The product that was requested doesn't exist. Verify the product and try again.") - ); - } - - $imageFileUri = $this->getMediaGalleryProcessor()->addImage( - $product, - $tmpFilePath, - isset($newEntry['types']) ? $newEntry['types'] : [], - true, - isset($newEntry['disabled']) ? $newEntry['disabled'] : true - ); - // Update additional fields that are still empty after addImage call - $this->getMediaGalleryProcessor()->updateImage( - $product, - $imageFileUri, - [ - 'label' => $newEntry['label'], - 'position' => $newEntry['position'], - 'disabled' => $newEntry['disabled'], - 'media_type' => $newEntry['media_type'], - ] - ); return $this; } @@ -485,68 +491,13 @@ private function processLinks(ProductInterface $product, $newLinks) * @return $this * @throws InputException * @throws StateException + * @throws LocalizedException * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ protected function processMediaGallery(ProductInterface $product, $mediaGalleryEntries) { - $existingMediaGallery = $product->getMediaGallery('images'); - $newEntries = []; - $entriesById = []; - if (!empty($existingMediaGallery)) { - foreach ($mediaGalleryEntries as $entry) { - if (isset($entry['value_id'])) { - $entriesById[$entry['value_id']] = $entry; - } else { - $newEntries[] = $entry; - } - } - foreach ($existingMediaGallery as $key => &$existingEntry) { - if (isset($entriesById[$existingEntry['value_id']])) { - $updatedEntry = $entriesById[$existingEntry['value_id']]; - if ($updatedEntry['file'] === null) { - unset($updatedEntry['file']); - } - $existingMediaGallery[$key] = array_merge($existingEntry, $updatedEntry); - } else { - //set the removed flag - $existingEntry['removed'] = true; - } - } - $product->setData('media_gallery', ["images" => $existingMediaGallery]); - } else { - $newEntries = $mediaGalleryEntries; - } - - $images = (array)$product->getMediaGallery('images'); - $images = $this->determineImageRoles($product, $images); + $this->getMediaGalleryProcessor()->processMediaGallery($product, $mediaGalleryEntries); - $this->getMediaGalleryProcessor()->clearMediaAttribute($product, array_keys($product->getMediaAttributes())); - - foreach ($images as $image) { - if (!isset($image['removed']) && !empty($image['types'])) { - $this->getMediaGalleryProcessor()->setMediaAttribute($product, $image['types'], $image['file']); - } - } - - foreach ($newEntries as $newEntry) { - if (!isset($newEntry['content'])) { - throw new InputException(__('The image content is invalid. Verify the content and try again.')); - } - /** @var ImageContentInterface $contentDataObject */ - $contentDataObject = $this->contentFactory->create() - ->setName($newEntry['content']['data'][ImageContentInterface::NAME]) - ->setBase64EncodedData($newEntry['content']['data'][ImageContentInterface::BASE64_ENCODED_DATA]) - ->setType($newEntry['content']['data'][ImageContentInterface::TYPE]); - $newEntry['content'] = $contentDataObject; - $this->processNewMediaGalleryEntry($product, $newEntry); - - $finalGallery = $product->getData('media_gallery'); - $newEntryId = key(array_diff_key($product->getData('media_gallery')['images'], $entriesById)); - $newEntry = array_replace_recursive($newEntry, $finalGallery['images'][$newEntryId]); - $entriesById[$newEntryId] = $newEntry; - $finalGallery['images'][$newEntryId] = $newEntry; - $product->setData('media_gallery', $finalGallery); - } return $this; } @@ -557,6 +508,7 @@ protected function processMediaGallery(ProductInterface $product, $mediaGalleryE */ public function save(ProductInterface $product, $saveOptions = false) { + $assignToCategories = false; $tierPrices = $product->getData('tier_price'); try { @@ -574,6 +526,7 @@ public function save(ProductInterface $product, $saveOptions = false) $extensionAttributes = $product->getExtensionAttributes(); if (empty($extensionAttributes->__toArray())) { $product->setExtensionAttributes($existingProduct->getExtensionAttributes()); + $assignToCategories = true; } } catch (NoSuchEntityException $e) { $existingProduct = null; @@ -611,6 +564,12 @@ public function save(ProductInterface $product, $saveOptions = false) } $this->saveProduct($product); + if ($assignToCategories === true && $product->getCategoryIds()) { + $this->linkManagement->assignProductToCategories( + $product->getSku(), + $product->getCategoryIds() + ); + } $this->removeProductFromLocalCache($product->getSku()); unset($this->instancesById[$product->getId()]); @@ -618,7 +577,7 @@ public function save(ProductInterface $product, $saveOptions = false) } /** - * {@inheritdoc} + * @inheritdoc */ public function delete(ProductInterface $product) { @@ -629,10 +588,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); @@ -642,7 +602,7 @@ public function delete(ProductInterface $product) } /** - * {@inheritdoc} + * @inheritdoc */ public function deleteById($sku) { @@ -651,7 +611,7 @@ public function deleteById($sku) } /** - * {@inheritdoc} + * @inheritdoc */ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria) { @@ -668,6 +628,7 @@ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCr $collection->load(); $collection->addCategoryIds(); + $this->addExtensionAttributes($collection); $searchResult = $this->searchResultsFactory->create(); $searchResult->setSearchCriteria($searchCriteria); $searchResult->setItems($collection->getItems()); @@ -678,7 +639,7 @@ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCr $this->getCacheKey( [ false, - $product->hasData(\Magento\Catalog\Model\Product::STORE_ID) ? $product->getStoreId() : null + $product->getStoreId() ] ), $product @@ -688,6 +649,20 @@ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCr return $searchResult; } + /** + * Add extension attributes to loaded items. + * + * @param Collection $collection + * @return Collection + */ + private function addExtensionAttributes(Collection $collection) : Collection + { + foreach ($collection->getItems() as $item) { + $this->readExtensions->execute($item); + } + return $collection; + } + /** * Helper function that adds a FilterGroup to the collection. * @@ -733,41 +708,18 @@ public function cleanCache() } /** - * Ascertain image roles, if they are not set against the gallery entries + * Retrieve media gallery processor. * - * @param ProductInterface $product - * @param array $images - * @return array - */ - private function determineImageRoles(ProductInterface $product, array $images) : array - { - $imagesWithRoles = []; - foreach ($images as $image) { - if (!isset($image['types'])) { - $image['types'] = []; - if (isset($image['file'])) { - foreach (array_keys($product->getMediaAttributes()) as $attribute) { - if ($image['file'] == $product->getData($attribute)) { - $image['types'][] = $attribute; - } - } - } - } - $imagesWithRoles[] = $image; - } - return $imagesWithRoles; - } - - /** - * @return Product\Gallery\Processor + * @return MediaGalleryProcessor */ private function getMediaGalleryProcessor() { - if (null === $this->mediaGalleryProcessor) { - $this->mediaGalleryProcessor = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Catalog\Model\Product\Gallery\Processor::class); + if (null === $this->mediaProcessor) { + $this->mediaProcessor = \Magento\Framework\App\ObjectManager::getInstance() + ->get(MediaGalleryProcessor::class); } - return $this->mediaGalleryProcessor; + + return $this->mediaProcessor; } /** @@ -879,6 +831,7 @@ private function saveProduct($product): void throw new CouldNotSaveException(__($e->getMessage())); } catch (LocalizedException $e) { throw $e; + // phpcs:disable Magento2.Exceptions.ThrowCatch } catch (\Exception $e) { throw new CouldNotSaveException( __('The product was unable to be saved. Please try again.'), diff --git a/app/code/Magento/Catalog/Model/ProductRepository/MediaGalleryProcessor.php b/app/code/Magento/Catalog/Model/ProductRepository/MediaGalleryProcessor.php new file mode 100644 index 000000000000..70311954f63e --- /dev/null +++ b/app/code/Magento/Catalog/Model/ProductRepository/MediaGalleryProcessor.php @@ -0,0 +1,239 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\ProductRepository; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product\Gallery\Processor; +use Magento\Catalog\Model\Product\Media\Config; +use Magento\Framework\Api\Data\ImageContentInterface; +use Magento\Framework\Api\Data\ImageContentInterfaceFactory; +use Magento\Framework\Api\ImageProcessorInterface; +use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\StateException; + +/** + * Process Media gallery data for ProductRepository before save product. + */ +class MediaGalleryProcessor +{ + /** + * Catalog gallery processor. + * + * @var Processor + */ + private $processor; + + /** + * Image content data object factory. + * + * @var ImageContentInterfaceFactory + */ + private $contentFactory; + + /** + * Image processor. + * + * @var ImageProcessorInterface + */ + private $imageProcessor; + + /** + * @param Processor $processor + * @param ImageContentInterfaceFactory $contentFactory + * @param ImageProcessorInterface $imageProcessor + */ + public function __construct( + Processor $processor, + ImageContentInterfaceFactory $contentFactory, + ImageProcessorInterface $imageProcessor + ) { + $this->processor = $processor; + $this->contentFactory = $contentFactory; + $this->imageProcessor = $imageProcessor; + } + + /** + * Process Media gallery data before save product. + * + * Compare Media Gallery Entries Data with existing Media Gallery + * * If Media entry has not value_id set it as new + * * If Existing entry 'value_id' absent in Media Gallery set 'removed' flag + * * Merge Existing and new media gallery + * + * @param ProductInterface $product contains only existing media gallery items + * @param array $mediaGalleryEntries array which contains all media gallery items + * @return void + * @throws InputException + * @throws StateException + * @throws LocalizedException + */ + public function processMediaGallery(ProductInterface $product, array $mediaGalleryEntries) :void + { + $existingMediaGallery = $product->getMediaGallery('images'); + $newEntries = []; + $entriesById = []; + if (!empty($existingMediaGallery)) { + foreach ($mediaGalleryEntries as $entry) { + if (isset($entry['value_id'])) { + $entriesById[$entry['value_id']] = $entry; + } else { + $newEntries[] = $entry; + } + } + foreach ($existingMediaGallery as $key => &$existingEntry) { + if (isset($entriesById[$existingEntry['value_id']])) { + $updatedEntry = $entriesById[$existingEntry['value_id']]; + if ($updatedEntry['file'] === null) { + unset($updatedEntry['file']); + } + $existingMediaGallery[$key] = array_merge($existingEntry, $updatedEntry); + } else { + //set the removed flag + $existingEntry['removed'] = true; + } + } + $product->setData('media_gallery', ["images" => $existingMediaGallery]); + } else { + $newEntries = $mediaGalleryEntries; + } + + $images = (array)$product->getMediaGallery('images'); + $images = $this->determineImageRoles($product, $images); + + $this->processor->clearMediaAttribute($product, array_keys($product->getMediaAttributes())); + + $this->processMediaAttributes($product, $images); + $this->processEntries($product, $newEntries, $entriesById); + } + + /** + * Process new gallery media entry. + * + * @param ProductInterface $product + * @param array $newEntry + * @return void + * @throws InputException + * @throws StateException + * @throws LocalizedException + */ + public function processNewMediaGalleryEntry( + ProductInterface $product, + array $newEntry + ) :void { + /** @var ImageContentInterface $contentDataObject */ + $contentDataObject = $newEntry['content']; + + /** @var Config $mediaConfig */ + $mediaConfig = $product->getMediaConfig(); + $mediaTmpPath = $mediaConfig->getBaseTmpMediaPath(); + + $relativeFilePath = $this->imageProcessor->processImageContent($mediaTmpPath, $contentDataObject); + $tmpFilePath = $mediaConfig->getTmpMediaShortUrl($relativeFilePath); + + if (!$product->hasGalleryAttribute()) { + throw new StateException( + __("The product that was requested doesn't exist. Verify the product and try again.") + ); + } + + $imageFileUri = $this->processor->addImage( + $product, + $tmpFilePath, + isset($newEntry['types']) ? $newEntry['types'] : [], + true, + isset($newEntry['disabled']) ? $newEntry['disabled'] : true + ); + // Update additional fields that are still empty after addImage call + $this->processor->updateImage( + $product, + $imageFileUri, + [ + 'label' => $newEntry['label'], + 'position' => $newEntry['position'], + 'disabled' => $newEntry['disabled'], + 'media_type' => $newEntry['media_type'], + ] + ); + } + + /** + * Ascertain image roles, if they are not set against the gallery entries. + * + * @param ProductInterface $product + * @param array $images + * @return array + */ + private function determineImageRoles(ProductInterface $product, array $images) : array + { + $imagesWithRoles = []; + foreach ($images as $image) { + if (!isset($image['types'])) { + $image['types'] = []; + if (isset($image['file'])) { + foreach (array_keys($product->getMediaAttributes()) as $attribute) { + if ($image['file'] == $product->getData($attribute)) { + $image['types'][] = $attribute; + } + } + } + } + $imagesWithRoles[] = $image; + } + + return $imagesWithRoles; + } + + /** + * Convert entries into product media gallery data and set to product. + * + * @param ProductInterface $product + * @param array $newEntries + * @param array $entriesById + * @throws InputException + * @throws LocalizedException + * @throws StateException + */ + private function processEntries(ProductInterface $product, array $newEntries, array $entriesById): void + { + foreach ($newEntries as $newEntry) { + if (!isset($newEntry['content'])) { + throw new InputException(__('The image content is invalid. Verify the content and try again.')); + } + /** @var ImageContentInterface $contentDataObject */ + $contentDataObject = $this->contentFactory->create() + ->setName($newEntry['content']['data'][ImageContentInterface::NAME]) + ->setBase64EncodedData($newEntry['content']['data'][ImageContentInterface::BASE64_ENCODED_DATA]) + ->setType($newEntry['content']['data'][ImageContentInterface::TYPE]); + $newEntry['content'] = $contentDataObject; + $this->processNewMediaGalleryEntry($product, $newEntry); + + $finalGallery = $product->getData('media_gallery'); + $newEntryId = key(array_diff_key($product->getData('media_gallery')['images'], $entriesById)); + $newEntry = array_replace_recursive($newEntry, $finalGallery['images'][$newEntryId]); + $entriesById[$newEntryId] = $newEntry; + $finalGallery['images'][$newEntryId] = $newEntry; + $product->setData('media_gallery', $finalGallery); + } + } + + /** + * Set media attribute values. + * + * @param ProductInterface $product + * @param array $images + */ + private function processMediaAttributes(ProductInterface $product, array $images): void + { + foreach ($images as $image) { + if (!isset($image['removed']) && !empty($image['types'])) { + $this->processor->setMediaAttribute($product, $image['types'], $image['file']); + } + } + } +} diff --git a/app/code/Magento/Catalog/Model/ResourceModel/AbstractCollection.php b/app/code/Magento/Catalog/Model/ResourceModel/AbstractCollection.php index d4f5fdd5137c..2896849b76cc 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/AbstractCollection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/AbstractCollection.php @@ -7,6 +7,7 @@ /** * Flat abstract collection + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ abstract class AbstractCollection extends \Magento\Framework\Model\ResourceModel\Db\VersionControl\Collection @@ -34,7 +35,7 @@ public function setSelectCountSql(\Magento\Framework\DB\Select $countSelect) } /** - * get select count sql + * Get select count sql * * @return \Magento\Framework\DB\Select */ @@ -69,6 +70,7 @@ protected function _attributeToField($attribute) /** * Add attribute to select result set. + * * Backward compatibility with EAV collection * * @param string $attribute @@ -82,6 +84,7 @@ public function addAttributeToSelect($attribute) /** * Specify collection select filter by attribute value + * * Backward compatibility with EAV collection * * @param string|\Magento\Eav\Model\Entity\Attribute $attribute @@ -96,6 +99,7 @@ public function addAttributeToFilter($attribute, $condition = null) /** * Specify collection select order by attribute value + * * Backward compatibility with EAV collection * * @param string $attribute @@ -110,6 +114,7 @@ public function addAttributeToSort($attribute, $dir = 'asc') /** * Set collection page start and records to show + * * Backward compatibility with EAV collection * * @param int $pageNum @@ -124,11 +129,12 @@ public function setPage($pageNum, $pageSize) /** * Create all ids retrieving select with limitation + * * Backward compatibility with EAV collection * * @param int $limit * @param int $offset - * @return \Magento\Eav\Model\Entity\Collection\AbstractCollection + * @return \Magento\Framework\DB\Select */ protected function _getAllIdsSelect($limit = null, $offset = null) { @@ -144,6 +150,7 @@ protected function _getAllIdsSelect($limit = null, $offset = null) /** * Retrieve all ids for collection + * * Backward compatibility with EAV collection * * @param int $limit diff --git a/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php b/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php index b9e629912a5b..3d7f863b7c0d 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php @@ -7,6 +7,10 @@ namespace Magento\Catalog\Model\ResourceModel; use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend; +use Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend; +use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource; +use Magento\Eav\Model\Entity\Attribute\UniqueValidationInterface; /** * Catalog entity abstract model @@ -37,16 +41,18 @@ abstract class AbstractResource extends \Magento\Eav\Model\Entity\AbstractEntity * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Catalog\Model\Factory $modelFactory * @param array $data + * @param UniqueValidationInterface|null $uniqueValidator */ public function __construct( \Magento\Eav\Model\Entity\Context $context, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Catalog\Model\Factory $modelFactory, - $data = [] + $data = [], + UniqueValidationInterface $uniqueValidator = null ) { $this->_storeManager = $storeManager; $this->_modelFactory = $modelFactory; - parent::__construct($context, $data); + parent::__construct($context, $data, $uniqueValidator); } /** @@ -86,16 +92,14 @@ protected function _isApplicableAttribute($object, $attribute) /** * Check whether attribute instance (attribute, backend, frontend or source) has method and applicable * - * @param AbstractAttribute|\Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend - * |\Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend - * |\Magento\Eav\Model\Entity\Attribute\Source\AbstractSource $instance + * @param AbstractAttribute|AbstractBackend|AbstractFrontend|AbstractSource $instance * @param string $method * @param array $args array of arguments * @return boolean */ protected function _isCallableAttributeInstance($instance, $method, $args) { - if ($instance instanceof \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend + if ($instance instanceof AbstractBackend && ($method == 'beforeSave' || $method == 'afterSave') ) { $attributeCode = $instance->getAttribute()->getAttributeCode(); @@ -112,6 +116,7 @@ protected function _isCallableAttributeInstance($instance, $method, $args) /** * Retrieve select object for loading entity attributes values + * * Join attribute store value * * @param \Magento\Framework\DataObject $object @@ -244,6 +249,7 @@ protected function _saveAttributeValue($object, $attribute, $value) /** * Check if attribute present for non default Store View. + * * Prevent "delete" query locking in a case when nothing to delete * * @param AbstractAttribute $attribute @@ -485,7 +491,7 @@ protected function _canUpdateAttribute(AbstractAttribute $attribute, $value, arr * Retrieve attribute's raw value from DB. * * @param int $entityId - * @param int|string|array $attribute atrribute's ids or codes + * @param int|string|array $attribute attribute's ids or codes * @param int|\Magento\Store\Model\Store $store * @return bool|string|array * @SuppressWarnings(PHPMD.CyclomaticComplexity) diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category.php b/app/code/Magento/Catalog/Model/ResourceModel/Category.php index 9de0e8a84904..536fda7e093d 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category.php @@ -16,6 +16,8 @@ use Magento\Framework\EntityManager\EntityManager; /** + * Resource model for category entity + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Category extends AbstractResource @@ -198,7 +200,7 @@ protected function _getTree() * delete child categories * * @param \Magento\Framework\DataObject $object - * @return $this + * @return void */ protected function _beforeDelete(\Magento\Framework\DataObject $object) { @@ -249,7 +251,8 @@ public function deleteChildren(\Magento\Framework\DataObject $object) /** * Process category data before saving - * prepare path and increment children count for parent categories + * + * Prepare path and increment children count for parent categories * * @param \Magento\Framework\DataObject $object * @return $this @@ -298,7 +301,8 @@ protected function _beforeSave(\Magento\Framework\DataObject $object) /** * Process category data after save category object - * save related products ids and update path value + * + * Save related products ids and update path value * * @param \Magento\Framework\DataObject $object * @return $this @@ -482,8 +486,20 @@ public function getProductsPosition($category) $this->getCategoryProductTable(), ['product_id', 'position'] )->where( - 'category_id = :category_id' + "{$this->getTable('catalog_category_product')}.category_id = ?", + $category->getId() ); + $websiteId = $category->getStore()->getWebsiteId(); + if ($websiteId) { + $select->join( + ['product_website' => $this->getTable('catalog_product_website')], + "product_website.product_id = {$this->getTable('catalog_category_product')}.product_id", + [] + )->where( + 'product_website.website_id = ?', + $websiteId + ); + } $bind = ['category_id' => (int)$category->getId()]; return $this->getConnection()->fetchPairs($select, $bind); @@ -664,7 +680,7 @@ public function getProductCount($category) $bind = ['category_id' => (int)$category->getId()]; $counts = $this->getConnection()->fetchOne($select, $bind); - return intval($counts); + return (int) $counts; } /** @@ -862,6 +878,7 @@ public function isInRootCategoryList($category) /** * Check category is forbidden to delete. + * * If category is root and assigned to store group return false * * @param integer $categoryId @@ -918,7 +935,7 @@ public function changeParent( $childrenCount = $this->getChildrenCount($category->getId()) + 1; $table = $this->getEntityTable(); $connection = $this->getConnection(); - $levelFiled = $connection->quoteIdentifier('level'); + $levelField = $connection->quoteIdentifier('level'); $pathField = $connection->quoteIdentifier('path'); /** @@ -958,7 +975,7 @@ public function changeParent( $newPath . '/' ) . ')' ), - 'level' => new \Zend_Db_Expr($levelFiled . ' + ' . $levelDisposition) + 'level' => new \Zend_Db_Expr($levelField . ' + ' . $levelDisposition) ], [$pathField . ' LIKE ?' => $category->getPath() . '/%'] ); @@ -982,6 +999,7 @@ public function changeParent( /** * Process positions of old parent category children and new parent category children. + * * Get position for moved category * * @param \Magento\Catalog\Model\Category $category @@ -1062,7 +1080,7 @@ public function load($object, $entityId, $attributes = []) } /** - * {@inheritdoc} + * @inheritdoc */ public function delete($object) { @@ -1088,6 +1106,8 @@ public function save(\Magento\Framework\Model\AbstractModel $object) } /** + * Returns EntityManager object + * * @return EntityManager */ private function getEntityManager() @@ -1100,6 +1120,8 @@ private function getEntityManager() } /** + * Returns AggregateCount object + * * @return Category\AggregateCount */ private function getAggregateCount() diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php index 46bb74513b59..657daca13055 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php @@ -82,7 +82,6 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig - * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -323,9 +322,7 @@ public function loadProductCount($items, $countRegular = true, $countAnchor = tr 'main_table.category_id=e.entity_id', [] )->where( - 'e.entity_id = :entity_id' - )->orWhere( - 'e.path LIKE :c_path' + '(e.entity_id = :entity_id OR e.path LIKE :c_path)' ); if ($websiteId) { $select->join( diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/Flat.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/Flat.php index 01e4b072b036..05950531e217 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category/Flat.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/Flat.php @@ -173,7 +173,7 @@ public function getMainTable() public function getMainStoreTable($storeId = \Magento\Store\Model\Store::DEFAULT_STORE_ID) { if (is_string($storeId)) { - $storeId = intval($storeId); + $storeId = (int) $storeId; } if ($storeId) { @@ -699,8 +699,20 @@ public function getProductsPosition($category) $this->getTable('catalog_category_product'), ['product_id', 'position'] )->where( - 'category_id = :category_id' + "{$this->getTable('catalog_category_product')}.category_id = ?", + $category->getId() ); + $websiteId = $category->getStore()->getWebsiteId(); + if ($websiteId) { + $select->join( + ['product_website' => $this->getTable('catalog_product_website')], + "product_website.product_id = {$this->getTable('catalog_category_product')}.product_id", + [] + )->where( + 'product_website.website_id = ?', + $websiteId + ); + } $bind = ['category_id' => (int)$category->getId()]; return $this->getConnection()->fetchPairs($select, $bind); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php b/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php index 9ab863cde270..3a0d47fe573f 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php @@ -7,6 +7,7 @@ /** * Catalog EAV collection resource abstract model + * * Implement using different stores for retrieve attribute values * * @api @@ -42,7 +43,6 @@ class AbstractCollection extends \Magento\Eav\Model\Entity\Collection\AbstractCo * @param \Magento\Framework\Validator\UniversalFactory $universalFactory * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection - * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -205,10 +205,7 @@ protected function _getLoadAttributesSelect($table, $attributeIds = []) } /** - * @param \Magento\Framework\DB\Select $select - * @param string $table - * @param string $type - * @return \Magento\Framework\DB\Select + * @inheritdoc */ protected function _addLoadAttributesSelectValues($select, $table, $type) { diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php index 8f8e9f6bfedf..d56cc40ad0fc 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php @@ -167,6 +167,8 @@ public function __construct( } /** + * Init model + * * @return void */ protected function _construct() @@ -234,6 +236,8 @@ public function afterSave() ) { $this->_indexerEavProcessor->markIndexerAsInvalid(); } + + $this->_source = null; return parent::afterSave(); } @@ -362,6 +366,7 @@ public function getStoreId() /** * Retrieve apply to products array + * * Return empty array if applied to all products * * @return string[] @@ -478,7 +483,7 @@ protected function _isOriginalIndexable() $backendType = $this->getOrigData('backend_type'); $frontendInput = $this->getOrigData('frontend_input'); - if ($backendType == 'int' && $frontendInput == 'select') { + if ($backendType == 'int' && ($frontendInput == 'select' || $frontendInput == 'boolean')) { return true; } elseif ($backendType == 'varchar' && $frontendInput == 'multiselect') { return true; @@ -507,8 +512,8 @@ public function getIndexType() } /** + * @inheritdoc * @codeCoverageIgnoreStart - * {@inheritdoc} */ public function getIsWysiwygEnabled() { @@ -516,7 +521,7 @@ public function getIsWysiwygEnabled() } /** - * {@inheritdoc} + * @inheritdoc */ public function getIsHtmlAllowedOnFront() { @@ -524,7 +529,7 @@ public function getIsHtmlAllowedOnFront() } /** - * {@inheritdoc} + * @inheritdoc */ public function getUsedForSortBy() { @@ -532,7 +537,7 @@ public function getUsedForSortBy() } /** - * {@inheritdoc} + * @inheritdoc */ public function getIsFilterable() { @@ -540,7 +545,7 @@ public function getIsFilterable() } /** - * {@inheritdoc} + * @inheritdoc */ public function getIsFilterableInSearch() { @@ -548,7 +553,7 @@ public function getIsFilterableInSearch() } /** - * {@inheritdoc} + * @inheritdoc */ public function getIsUsedInGrid() { @@ -556,7 +561,7 @@ public function getIsUsedInGrid() } /** - * {@inheritdoc} + * @inheritdoc */ public function getIsVisibleInGrid() { @@ -564,7 +569,7 @@ public function getIsVisibleInGrid() } /** - * {@inheritdoc} + * @inheritdoc */ public function getIsFilterableInGrid() { @@ -572,7 +577,7 @@ public function getIsFilterableInGrid() } /** - * {@inheritdoc} + * @inheritdoc */ public function getPosition() { @@ -580,7 +585,7 @@ public function getPosition() } /** - * {@inheritdoc} + * @inheritdoc */ public function getIsSearchable() { @@ -588,7 +593,7 @@ public function getIsSearchable() } /** - * {@inheritdoc} + * @inheritdoc */ public function getIsVisibleInAdvancedSearch() { @@ -596,7 +601,7 @@ public function getIsVisibleInAdvancedSearch() } /** - * {@inheritdoc} + * @inheritdoc */ public function getIsComparable() { @@ -604,7 +609,7 @@ public function getIsComparable() } /** - * {@inheritdoc} + * @inheritdoc */ public function getIsUsedForPromoRules() { @@ -612,7 +617,7 @@ public function getIsUsedForPromoRules() } /** - * {@inheritdoc} + * @inheritdoc */ public function getIsVisibleOnFront() { @@ -620,7 +625,7 @@ public function getIsVisibleOnFront() } /** - * {@inheritdoc} + * @inheritdoc */ public function getUsedInProductListing() { @@ -628,7 +633,7 @@ public function getUsedInProductListing() } /** - * {@inheritdoc} + * @inheritdoc */ public function getIsVisible() { @@ -638,7 +643,7 @@ public function getIsVisible() //@codeCoverageIgnoreEnd /** - * {@inheritdoc} + * @inheritdoc */ public function getScope() { @@ -720,7 +725,7 @@ public function setPosition($position) /** * Set apply to value for the element * - * @param string []|string + * @param string[]|string $applyTo * @return $this */ public function setApplyTo($applyTo) @@ -829,7 +834,7 @@ public function setScope($scope) } /** - * {@inheritdoc} + * @inheritdoc */ public function afterDelete() { @@ -840,9 +845,14 @@ public function afterDelete() /** * @inheritdoc * @since 100.0.9 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + $this->unsetData('entity_type'); return array_diff( parent::__sleep(), @@ -853,9 +863,14 @@ public function __sleep() /** * @inheritdoc * @since 100.0.9 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + parent::__wakeup(); $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); $this->_indexerEavProcessor = $objectManager->get(\Magento\Catalog\Model\Indexer\Product\Flat\Processor::class); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Layer/Filter/Price.php b/app/code/Magento/Catalog/Model/ResourceModel/Layer/Filter/Price.php index 3699e29f8201..585da2af529a 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Layer/Filter/Price.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Layer/Filter/Price.php @@ -112,7 +112,7 @@ public function getCount($range) /** * Check and set correct variable values to prevent SQL-injections */ - $range = floatval($range); + $range = (float)$range; if ($range == 0) { $range = 1; } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product.php b/app/code/Magento/Catalog/Model/ResourceModel/Product.php index d71ec2388198..24174391be82 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product.php @@ -8,6 +8,7 @@ use Magento\Catalog\Model\ResourceModel\Product\Website\Link as ProductWebsiteLink; use Magento\Framework\App\ObjectManager; use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; +use Magento\Eav\Model\Entity\Attribute\UniqueValidationInterface; /** * Product entity resource model @@ -101,6 +102,7 @@ class Product extends AbstractResource * @param \Magento\Catalog\Model\Product\Attribute\DefaultAttributes $defaultAttributes * @param array $data * @param TableMaintainer|null $tableMaintainer + * @param UniqueValidationInterface|null $uniqueValidator * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -115,7 +117,8 @@ public function __construct( \Magento\Eav\Model\Entity\TypeFactory $typeFactory, \Magento\Catalog\Model\Product\Attribute\DefaultAttributes $defaultAttributes, $data = [], - TableMaintainer $tableMaintainer = null + TableMaintainer $tableMaintainer = null, + UniqueValidationInterface $uniqueValidator = null ) { $this->_categoryCollectionFactory = $categoryCollectionFactory; $this->_catalogCategory = $catalogCategory; @@ -127,7 +130,8 @@ public function __construct( $context, $storeManager, $modelFactory, - $data + $data, + $uniqueValidator ); $this->connectionName = 'catalog'; $this->tableMaintainer = $tableMaintainer ?: ObjectManager::getInstance()->get(TableMaintainer::class); @@ -289,7 +293,7 @@ protected function _afterSave(\Magento\Framework\DataObject $product) } /** - * {@inheritdoc} + * @inheritdoc */ public function delete($object) { @@ -593,7 +597,7 @@ public function countAll() } /** - * {@inheritdoc} + * @inheritdoc */ public function validate($object) { @@ -633,7 +637,7 @@ public function load($object, $entityId, $attributes = []) } /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.UnusedLocalVariable) * @since 101.0.0 */ @@ -675,6 +679,8 @@ public function save(\Magento\Framework\Model\AbstractModel $object) } /** + * Retrieve entity manager object + * * @return \Magento\Framework\EntityManager\EntityManager */ private function getEntityManager() @@ -687,6 +693,8 @@ private function getEntityManager() } /** + * Retrieve ProductWebsiteLink object + * * @deprecated 101.1.0 * @return ProductWebsiteLink */ @@ -696,6 +704,8 @@ private function getProductWebsiteLink() } /** + * Retrieve CategoryLink object + * * @deprecated 101.1.0 * @return \Magento\Catalog\Model\ResourceModel\Product\CategoryLink */ @@ -710,9 +720,10 @@ private function getProductCategoryLink() /** * Extends parent method to be appropriate for product. + * * Store id is required to correctly identify attribute value we are working with. * - * {@inheritdoc} + * @inheritdoc * @since 101.1.0 */ protected function getAttributeRow($entity, $object, $attribute) diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/BaseSelectProcessorInterface.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/BaseSelectProcessorInterface.php index d97f6bebf4e9..da3c4fb4417f 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/BaseSelectProcessorInterface.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/BaseSelectProcessorInterface.php @@ -9,6 +9,7 @@ /** * Interface BaseSelectProcessorInterface + * * @api * @since 101.0.3 */ @@ -20,6 +21,8 @@ interface BaseSelectProcessorInterface const PRODUCT_TABLE_ALIAS = 'child'; /** + * Process the select statement + * * @param Select $select * @return Select * @since 101.0.3 diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php index b54c19a11150..cf5760b0c33a 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php @@ -93,6 +93,8 @@ public function saveCategoryLinks(ProductInterface $product, array $categoryLink } /** + * Get category link metadata + * * @return \Magento\Framework\EntityManager\EntityMetadataInterface */ private function getCategoryLinkMetadata() @@ -114,16 +116,22 @@ private function getCategoryLinkMetadata() private function processCategoryLinks($newCategoryPositions, &$oldCategoryPositions) { $result = ['changed' => [], 'updated' => []]; + + $oldCategoryPositions = array_values($oldCategoryPositions); foreach ($newCategoryPositions as $newCategoryPosition) { - $key = array_search( - $newCategoryPosition['category_id'], - array_column($oldCategoryPositions, 'category_id') - ); + $key = false; + + foreach ($oldCategoryPositions as $oldKey => $oldCategoryPosition) { + if ((int)$oldCategoryPosition['category_id'] === (int)$newCategoryPosition['category_id']) { + $key = $oldKey; + break; + } + } if ($key === false) { $result['changed'][] = $newCategoryPosition; } elseif ($oldCategoryPositions[$key]['position'] != $newCategoryPosition['position']) { - $result['updated'][] = $newCategoryPositions[$key]; + $result['updated'][] = $newCategoryPosition; unset($oldCategoryPositions[$key]); } } @@ -132,6 +140,8 @@ private function processCategoryLinks($newCategoryPositions, &$oldCategoryPositi } /** + * Update category links + * * @param ProductInterface $product * @param array $insertLinks * @param bool $insert @@ -175,6 +185,8 @@ private function updateCategoryLinks(ProductInterface $product, array $insertLin } /** + * Delete category links + * * @param ProductInterface $product * @param array $deleteLinks * @return array diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php index 4384effc4e35..0cdf8b39f7d5 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php @@ -7,6 +7,8 @@ namespace Magento\Catalog\Model\ResourceModel\Product; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; +use Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver; use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus; use Magento\Catalog\Model\Product\Gallery\ReadHandler as GalleryReadHandler; use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; @@ -16,11 +18,10 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\DB\Select; use Magento\Framework\EntityManager\MetadataPool; -use Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver; +use Magento\Framework\Indexer\DimensionFactory; +use Magento\Framework\Model\ResourceModel\ResourceModelPoolInterface; use Magento\Store\Model\Indexer\WebsiteDimensionProvider; use Magento\Store\Model\Store; -use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; -use Magento\Framework\Indexer\DimensionFactory; /** * Product collection @@ -31,6 +32,7 @@ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.NumberOfChildren) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @since 100.0.2 */ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\AbstractCollection @@ -290,8 +292,14 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac */ private $dimensionFactory; + /** + * @var \Magento\Framework\DataObject + */ + private $emptyItem; + /** * Collection constructor + * * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory * @param \Psr\Log\LoggerInterface $logger * @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy @@ -317,6 +325,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac * @param TableMaintainer|null $tableMaintainer * @param PriceTableResolver|null $priceTableResolver * @param DimensionFactory|null $dimensionFactory + * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -434,7 +443,7 @@ protected function _preparePriceExpressionParameters($select) */ public function getPriceExpression($select) { - //@todo: Add caching of price expresion + //@todo: Add caching of price expression $this->_preparePriceExpressionParameters($select); return $this->_priceExpression; } @@ -474,8 +483,7 @@ public function getFlatState() } /** - * Retrieve is flat enabled flag - * Return always false if magento run admin + * Retrieve is flat enabled. Return always false if magento run admin. * * @return bool */ @@ -506,8 +514,7 @@ protected function _construct() } /** - * Standard resource collection initialization - * Needed for child classes + * Standard resource collection initialization. Needed for child classes. * * @param string $model * @param string $entityModel @@ -546,14 +553,16 @@ protected function _prepareStaticFields() } /** - * Retrieve collection empty item - * Redeclared for specifying id field name without getting resource model inside model + * Get collection empty item. Redeclared for specifying id field name without getting resource model inside model. * * @return \Magento\Framework\DataObject */ public function getNewEmptyItem() { - $object = parent::getNewEmptyItem(); + if (null === $this->emptyItem) { + $this->emptyItem = parent::getNewEmptyItem(); + } + $object = clone $this->emptyItem; if ($this->isEnabledFlat()) { $object->setIdFieldName($this->getEntity()->getIdFieldName()); } @@ -633,8 +642,7 @@ public function _loadAttributes($printQuery = false, $logQuery = false) } /** - * Add attribute to entities in collection - * If $attribute=='*' select all attributes + * Add attribute to entities in collection. If $attribute=='*' select all attributes. * * @param array|string|integer|\Magento\Framework\App\Config\Element $attribute * @param bool|string $joinType @@ -670,8 +678,7 @@ public function addAttributeToSelect($attribute, $joinType = false) } /** - * Processing collection items after loading - * Adding url rewrites, minimal prices, final prices, tax percents + * Processing collection items after loading. Adding url rewrites, minimal prices, final prices, tax percents. * * @return $this */ @@ -682,6 +689,7 @@ protected function _afterLoad() } $this->_prepareUrlDataObject(); + $this->prepareStoreId(); if (count($this)) { $this->_eventManager->dispatch('catalog_product_collection_load_after', ['collection' => $this]); @@ -690,6 +698,23 @@ protected function _afterLoad() return $this; } + /** + * Add Store ID to products from collection. + * + * @return $this + */ + protected function prepareStoreId() + { + if ($this->getStoreId() !== null) { + /** @var $item \Magento\Catalog\Model\Product */ + foreach ($this->_items as $item) { + $item->setStoreId($this->getStoreId()); + } + } + + return $this; + } + /** * Prepare Url Data object * @@ -756,8 +781,7 @@ public function addIdFilter($productId, $exclude = false) } /** - * Adding product website names to result collection - * Add for each product websites information + * Adding product website names to result collection. Add for each product websites information. * * @return $this */ @@ -768,7 +792,7 @@ public function addWebsiteNamesToResult() } /** - * {@inheritdoc} + * @inheritdoc */ public function load($printQuery = false, $logQuery = false) { @@ -819,14 +843,14 @@ protected function doAddWebsiteNamesToResult() foreach ($this as $product) { if (isset($productWebsites[$product->getId()])) { $product->setData('websites', $productWebsites[$product->getId()]); + $product->setData('website_ids', $productWebsites[$product->getId()]); } } return $this; } /** - * Add store availability filter. Include availability product - * for store website + * Add store availability filter. Include availability product for store website. * * @param null|string|bool|int|Store $store * @return $this @@ -1115,11 +1139,11 @@ public function getSelectCountSql() /** * Get SQL for get record count * - * @param null $select + * @param Select $select * @param bool $resetLeftJoins - * @return \Magento\Framework\DB\Select + * @return Select */ - protected function _getSelectCountSql($select = null, $resetLeftJoins = true) + protected function _getSelectCountSql(?Select $select = null, $resetLeftJoins = true) { $this->_renderFilters(); $countSelect = $select === null ? $this->_getClearSelect() : $this->_buildClearSelect($select); @@ -1357,8 +1381,7 @@ public function joinUrlRewrite() } /** - * Add URL rewrites data to product - * If collection loadded - run processing else set flag + * Add URL rewrites data to product. If collection loadded - run processing else set flag. * * @param int|string $categoryId * @return $this @@ -1412,6 +1435,11 @@ protected function _addUrlRewrite() ['cu' => $this->getTable('catalog_url_rewrite_product_category')], 'u.url_rewrite_id=cu.url_rewrite_id' )->where('cu.category_id IN (?)', $this->_urlRewriteCategory); + } else { + $select->joinLeft( + ['cu' => $this->getTable('catalog_url_rewrite_product_category')], + 'u.url_rewrite_id=cu.url_rewrite_id' + )->where('cu.url_rewrite_id IS NULL'); } // more priority is data with category id @@ -1518,7 +1546,7 @@ public function addPriceData($customerGroupId = null, $websiteId = null) /** * Add attribute to filter * - * @param \Magento\Eav\Model\Entity\Attribute\AbstractAttribute|string $attribute + * @param \Magento\Eav\Model\Entity\Attribute\AbstractAttribute|string|array $attribute * @param array $condition * @param string $joinType * @return $this @@ -1581,7 +1609,8 @@ public function addAttributeToFilter($attribute, $condition = null, $joinType = } /** - * {@inheritdoc} + * @inheritdoc + * * @since 101.0.0 */ protected function getEntityPkName(\Magento\Eav\Model\Entity\AbstractEntity $entity) @@ -1792,7 +1821,8 @@ protected function _productLimitationJoinWebsite() } $conditions[] = $this->getConnection()->quoteInto( 'product_website.website_id IN(?)', - $filters['website_ids'] + $filters['website_ids'], + 'int' ); } elseif (isset( $filters['store_id'] @@ -1804,7 +1834,7 @@ protected function _productLimitationJoinWebsite() ) { $joinWebsite = true; $websiteId = $this->_storeManager->getStore($filters['store_id'])->getWebsiteId(); - $conditions[] = $this->getConnection()->quoteInto('product_website.website_id = ?', $websiteId); + $conditions[] = $this->getConnection()->quoteInto('product_website.website_id = ?', $websiteId, 'int'); } $fromPart = $this->getSelect()->getPart(\Magento\Framework\DB\Select::FROM); @@ -1947,6 +1977,7 @@ protected function _productLimitationPrice($joinLeft = false) } // Set additional field filters foreach ($this->_priceDataFieldFilters as $filterData) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $select->where(call_user_func_array('sprintf', $filterData)); } } else { @@ -2000,12 +2031,16 @@ protected function _applyProductLimitations() $conditions = [ 'cat_index.product_id=e.entity_id', - $this->getConnection()->quoteInto('cat_index.store_id=?', $filters['store_id']), + $this->getConnection()->quoteInto('cat_index.store_id=?', $filters['store_id'], 'int'), ]; if (isset($filters['visibility']) && !isset($filters['store_table'])) { - $conditions[] = $this->getConnection()->quoteInto('cat_index.visibility IN(?)', $filters['visibility']); + $conditions[] = $this->getConnection()->quoteInto( + 'cat_index.visibility IN(?)', + $filters['visibility'], + 'int' + ); } - $conditions[] = $this->getConnection()->quoteInto('cat_index.category_id=?', $filters['category_id']); + $conditions[] = $this->getConnection()->quoteInto('cat_index.category_id=?', $filters['category_id'], 'int'); if (isset($filters['category_is_anchor'])) { $conditions[] = $this->getConnection()->quoteInto('cat_index.is_parent=?', $filters['category_is_anchor']); } @@ -2188,7 +2223,7 @@ private function getTierPriceSelect(array $productIds) $this->getLinkField() . ' IN(?)', $productIds )->order( - $this->getLinkField() + 'qty' ); return $select; } @@ -2248,6 +2283,7 @@ private function getBackend() public function addPriceDataFieldFilter($comparisonFormat, $fields) { if (!preg_match('/^%s( (<|>|=|<=|>=|<>) %s)*$/', $comparisonFormat)) { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception('Invalid comparison format.'); } @@ -2343,7 +2379,10 @@ private function getGalleryReadHandler() } /** + * Retrieve Media gallery resource. + * * @deprecated 101.0.1 + * * @return \Magento\Catalog\Model\ResourceModel\Product\Gallery */ private function getMediaGalleryResource() 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 7c78dbca5a00..aa6fb8c1f882 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item/Collection.php @@ -12,6 +12,7 @@ * @author Magento Core Team <core@magentocommerce.com> * @SuppressWarnings(PHPMD.LongVariable) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @since 100.0.2 */ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection @@ -75,7 +76,6 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection * @param \Magento\Catalog\Model\ResourceModel\Product\Compare\Item $catalogProductCompareItem * @param \Magento\Catalog\Helper\Product\Compare $catalogProductCompare * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection - * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -403,6 +403,7 @@ public function clear() /** * Retrieve is flat enabled flag + * * Overwrite disable flat for compared item if required EAV resource * * @return bool diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php index 2868392f8528..a9741cd8e1ec 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Catalog\Model\ResourceModel\Product; use Magento\Store\Model\Store; @@ -49,7 +50,8 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc + * * @since 101.0.0 */ protected function _construct() @@ -58,7 +60,8 @@ protected function _construct() } /** - * {@inheritdoc} + * @inheritdoc + * * @since 101.0.0 */ public function getConnection() @@ -67,6 +70,8 @@ public function getConnection() } /** + * Load data from table by valueId + * * @param string $tableNameAlias * @param array $ids * @param int|null $storeId @@ -111,6 +116,8 @@ public function loadDataFromTableByValueId( } /** + * Load product gallery by attributeId + * * @param \Magento\Catalog\Model\Product $product * @param int $attributeId * @return array @@ -132,6 +139,8 @@ public function loadProductGalleryByAttributeId($product, $attributeId) } /** + * Create base load select + * * @param int $entityId * @param int $storeId * @param int $attributeId @@ -141,7 +150,7 @@ public function loadProductGalleryByAttributeId($product, $attributeId) */ protected function createBaseLoadSelect($entityId, $storeId, $attributeId) { - $select = $this->createBatchBaseSelect($storeId, $attributeId); + $select = $this->createBatchBaseSelect($storeId, $attributeId); $select = $select->where( 'entity.' . $this->metadata->getLinkField() . ' = ?', @@ -151,6 +160,8 @@ protected function createBaseLoadSelect($entityId, $storeId, $attributeId) } /** + * Create batch base select + * * @param int $storeId * @param int $attributeId * @return \Magento\Framework\DB\Select @@ -190,7 +201,7 @@ public function createBatchBaseSelect($storeId, $attributeId) 'value.' . $linkField . ' = entity.' . $linkField, ] ), - ['label', 'position', 'disabled'] + [] )->joinLeft( ['default_value' => $this->getTable(self::GALLERY_VALUE_TABLE)], implode( @@ -201,8 +212,15 @@ public function createBatchBaseSelect($storeId, $attributeId) 'default_value.' . $linkField . ' = entity.' . $linkField, ] ), - ['label_default' => 'label', 'position_default' => 'position', 'disabled_default' => 'disabled'] - )->where( + [] + )->columns([ + 'label' => $this->getConnection()->getIfNullSql('`value`.`label`', '`default_value`.`label`'), + 'position' => $this->getConnection()->getIfNullSql('`value`.`position`', '`default_value`.`position`'), + 'disabled' => $this->getConnection()->getIfNullSql('`value`.`disabled`', '`default_value`.`disabled`'), + 'label_default' => 'default_value.label', + 'position_default' => 'default_value.position', + 'disabled_default' => 'default_value.disabled' + ])->where( $mainTableAlias . '.attribute_id = ?', $attributeId )->where( @@ -240,6 +258,8 @@ protected function removeDuplicates(&$result) } /** + * Get main table alias + * * @return string * @since 101.0.0 */ @@ -249,6 +269,8 @@ public function getMainTableAlias() } /** + * Bind value to entity + * * @param int $valueId * @param int $entityId * @return int @@ -266,6 +288,8 @@ public function bindValueToEntity($valueId, $entityId) } /** + * Save data row + * * @param string $table * @param array $data * @param array $fields @@ -355,9 +379,9 @@ public function deleteGalleryValueInStore($valueId, $entityId, $storeId) $conditions = implode( ' AND ', [ - $this->getConnection()->quoteInto('value_id = ?', (int) $valueId), - $this->getConnection()->quoteInto($this->metadata->getLinkField() . ' = ?', (int) $entityId), - $this->getConnection()->quoteInto('store_id = ?', (int) $storeId) + $this->getConnection()->quoteInto('value_id = ?', (int)$valueId), + $this->getConnection()->quoteInto($this->metadata->getLinkField() . ' = ?', (int)$entityId), + $this->getConnection()->quoteInto('store_id = ?', (int)$storeId) ] ); @@ -385,7 +409,7 @@ public function duplicate($attributeId, $newFiles, $originalProductId, $newProdu $select = $this->getConnection()->select()->from( [$this->getMainTableAlias() => $this->getMainTable()], - ['value_id', 'value'] + ['value_id', 'value', 'media_type', 'disabled'] )->joinInner( ['entity' => $this->getTable(self::GALLERY_VALUE_TO_ENTITY_TABLE)], $this->getMainTableAlias() . '.value_id = entity.value_id', @@ -402,16 +426,16 @@ public function duplicate($attributeId, $newFiles, $originalProductId, $newProdu // Duplicate main entries of gallery foreach ($this->getConnection()->fetchAll($select) as $row) { - $data = [ - 'attribute_id' => $attributeId, - 'value' => isset($newFiles[$row['value_id']]) ? $newFiles[$row['value_id']] : $row['value'], - ]; + $data = $row; + $data['attribute_id'] = $attributeId; + $data['value'] = $newFiles[$row['value_id']] ?? $row['value']; + unset($data['value_id']); $valueIdMap[$row['value_id']] = $this->insertGallery($data); $this->bindValueToEntity($valueIdMap[$row['value_id']], $newProductId); } - if (count($valueIdMap) == 0) { + if (count($valueIdMap) === 0) { return []; } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Image.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Image.php index 123f358be40c..77f67480619e 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Image.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Image.php @@ -12,6 +12,9 @@ use Magento\Framework\DB\Select; use Magento\Framework\App\ResourceConnection; +/** + * Class for retrieval of all product images + */ class Image { /** @@ -73,15 +76,24 @@ public function getAllProductImages(): \Generator /** * Get the number of unique pictures of products + * * @return int */ public function getCountAllProductImages(): int { - $select = $this->getVisibleImagesSelect()->reset('columns')->columns('count(*)'); + $select = $this->getVisibleImagesSelect() + ->reset('columns') + ->reset('distinct') + ->columns( + new \Zend_Db_Expr('count(distinct value)') + ); + return (int) $this->connection->fetchOne($select); } /** + * Return Select to fetch all products images + * * @return Select */ private function getVisibleImagesSelect(): Select diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php index c33ea7c781aa..e024f0d30f1d 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php @@ -24,13 +24,11 @@ abstract class AbstractEav extends \Magento\Catalog\Model\ResourceModel\Product\ protected $_eventManager = null; /** - * AbstractEav constructor. * @param \Magento\Framework\Model\ResourceModel\Db\Context $context * @param \Magento\Framework\Indexer\Table\StrategyInterface $tableStrategy * @param \Magento\Eav\Model\Config $eavConfig * @param \Magento\Framework\Event\ManagerInterface $eventManager - * @param null $connectionName - * @param \Magento\Indexer\Model\Indexer\StateFactory|null $stateFactory + * @param string $connectionName */ public function __construct( \Magento\Framework\Model\ResourceModel\Db\Context $context, @@ -70,7 +68,6 @@ public function reindexAll() /** * Rebuild index data by entities * - * * @param int|array $processIds * @return $this * @throws \Exception @@ -88,8 +85,8 @@ public function reindexEntities($processIds) /** * Rebuild index data by attribute id - * If attribute is not indexable remove data by attribute * + * If attribute is not indexable remove data by attribute * * @param int $attributeId * @param bool $isIndexable @@ -245,7 +242,8 @@ protected function _prepareRelationIndex($parentIds = null) /** * Retrieve condition for retrieve indexable attribute select - * the catalog/eav_attribute table must have alias is ca + * + * The catalog/eav_attribute table must have alias is ca * * @return string */ diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php index 5b68730209b4..7730d7cc9a7f 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php @@ -7,6 +7,7 @@ use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\Data\ProductAttributeInterface; /** * Catalog Product Eav Select and Multiply Select Attributes Indexer resource model @@ -24,6 +25,16 @@ class Source extends AbstractEav */ protected $_resourceHelper; + /** + * @var \Magento\Eav\Api\AttributeRepositoryInterface + */ + private $attributeRepository; + + /** + * @var \Magento\Framework\Api\SearchCriteriaBuilder + */ + private $criteriaBuilder; + /** * Construct * @@ -33,6 +44,8 @@ class Source extends AbstractEav * @param \Magento\Framework\Event\ManagerInterface $eventManager * @param \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper * @param null|string $connectionName + * @param \Magento\Eav\Api\AttributeRepositoryInterface|null $attributeRepository + * @param \Magento\Framework\Api\SearchCriteriaBuilder|null $criteriaBuilder */ public function __construct( \Magento\Framework\Model\ResourceModel\Db\Context $context, @@ -40,7 +53,9 @@ public function __construct( \Magento\Eav\Model\Config $eavConfig, \Magento\Framework\Event\ManagerInterface $eventManager, \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper, - $connectionName = null + $connectionName = null, + \Magento\Eav\Api\AttributeRepositoryInterface $attributeRepository = null, + \Magento\Framework\Api\SearchCriteriaBuilder $criteriaBuilder = null ) { parent::__construct( $context, @@ -50,6 +65,12 @@ public function __construct( $connectionName ); $this->_resourceHelper = $resourceHelper; + $this->attributeRepository = $attributeRepository + ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Eav\Api\AttributeRepositoryInterface::class); + $this->criteriaBuilder = $criteriaBuilder + ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Framework\Api\SearchCriteriaBuilder::class); } /** @@ -84,7 +105,7 @@ protected function _getIndexableAttributes($multiSelect) if ($multiSelect == true) { $select->where('ea.backend_type = ?', 'varchar')->where('ea.frontend_input = ?', 'multiselect'); } else { - $select->where('ea.backend_type = ?', 'int')->where('ea.frontend_input = ?', 'select'); + $select->where('ea.backend_type = ?', 'int')->where('ea.frontend_input IN( ? )', ['select', 'boolean']); } return $this->getConnection()->fetchCol($select); @@ -234,6 +255,10 @@ protected function _prepareMultiselectIndex($entityIds = null, $attributeId = nu $options[$row['attribute_id']][$row['option_id']] = true; } + // Retrieve any custom source model options + $sourceModelOptions = $this->getMultiSelectAttributeWithSourceModels($attrIds); + $options = array_replace_recursive($options, $sourceModelOptions); + // prepare get multiselect values query $productValueExpression = $connection->getCheckSql('pvs.value_id > 0', 'pvs.value', 'pvd.value'); $select = $connection->select()->from( @@ -297,6 +322,39 @@ protected function _prepareMultiselectIndex($entityIds = null, $attributeId = nu return $this; } + /** + * Get options for multiselect attributes using custom source models + * Based on @maderlock's fix from: + * https://github.com/magento/magento2/issues/417#issuecomment-265146285 + * + * @param array $attrIds + * + * @return array + */ + private function getMultiSelectAttributeWithSourceModels($attrIds) + { + // Add options from custom source models + $this->criteriaBuilder + ->addFilter('attribute_id', $attrIds, 'in') + ->addFilter('source_model', true, 'notnull'); + $criteria = $this->criteriaBuilder->create(); + $attributes = $this->attributeRepository->getList( + ProductAttributeInterface::ENTITY_TYPE_CODE, + $criteria + )->getItems(); + + $options = []; + foreach ($attributes as $attribute) { + $sourceModelOptions = $attribute->getOptions(); + // Add options to list used below + foreach ($sourceModelOptions as $option) { + $options[$attribute->getAttributeId()][$option->getValue()] = true; + } + } + + return $options; + } + /** * Save a data to temporary source index table * @@ -330,6 +388,8 @@ public function getIdxTable($table = null) } /** + * Save data from select + * * @param \Magento\Framework\DB\Select $select * @param array $options * @return void 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 47fc6802d7ea..463da8762b7c 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 168fa8f50acc..3b4c3408e742 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php @@ -10,6 +10,7 @@ /** * Default Product Type Price Indexer Resource model + * * For correctly work need define product type id * * @api @@ -208,6 +209,8 @@ public function reindexEntity($entityIds) } /** + * Reindex prices. + * * @param null|int|array $entityIds * @return \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice */ @@ -604,7 +607,7 @@ protected function _applyCustomOption() [] )->joinLeft( ['otps' => $this->getTable('catalog_product_option_type_price')], - 'otps.option_type_id = otpd.option_type_id AND otpd.store_id = cs.store_id', + 'otps.option_type_id = otpd.option_type_id AND otps.store_id = cs.store_id', [] )->group( ['i.entity_id', 'i.customer_group_id', 'i.website_id', 'o.option_id'] @@ -802,6 +805,8 @@ public function getIdxTable($table = null) } /** + * Check if product exists. + * * @return bool */ protected function hasEntity() @@ -823,6 +828,8 @@ protected function hasEntity() } /** + * Get total tier price expression. + * * @param \Zend_Db_Expr $priceExpression * @return \Zend_Db_Expr */ @@ -863,6 +870,8 @@ private function getTotalTierPriceExpression(\Zend_Db_Expr $priceExpression) } /** + * Get tier price expression for table. + * * @param string $tableAlias * @param \Zend_Db_Expr $priceExpression * @return \Zend_Db_Expr diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php index 0005ac8dea58..95fecc832fa2 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/Query/BaseFinalPrice.php @@ -16,6 +16,7 @@ /** * Prepare base select for Product Price index limited by specified dimensions: website and customer group + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class BaseFinalPrice @@ -66,10 +67,11 @@ class BaseFinalPrice private $metadataPool; /** - * BaseFinalPrice constructor. * @param \Magento\Framework\App\ResourceConnection $resource * @param JoinAttributeProcessor $joinAttributeProcessor * @param \Magento\Framework\Module\Manager $moduleManager + * @param \Magento\Framework\Event\ManagerInterface $eventManager + * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool * @param string $connectionName */ public function __construct( @@ -89,6 +91,8 @@ public function __construct( } /** + * Build query for base final price. + * * @param Dimension[] $dimensions * @param string $productType * @param array $entityIds @@ -285,7 +289,7 @@ private function getTotalTierPriceExpression(\Zend_Db_Expr $priceExpression) /** * Get tier price expression for table * - * @param $tableAlias + * @param string $tableAlias * @param \Zend_Db_Expr $priceExpression * @return \Zend_Db_Expr */ @@ -305,7 +309,7 @@ private function getTierPriceExpressionForTable($tableAlias, \Zend_Db_Expr $pric /** * Get connection * - * return \Magento\Framework\DB\Adapter\AdapterInterface + * @return \Magento\Framework\DB\Adapter\AdapterInterface * @throws \DomainException */ private function getConnection(): \Magento\Framework\DB\Adapter\AdapterInterface diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/TemporaryTableStrategy.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/TemporaryTableStrategy.php index 54673cb01bb1..89daab288597 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/TemporaryTableStrategy.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/TemporaryTableStrategy.php @@ -30,7 +30,7 @@ class TemporaryTableStrategy implements \Magento\Framework\Indexer\Table\Strateg /** * TemporaryTableStrategy constructor. - * @param \Magento\Framework\Indexer\Table\Strategy $strategy + * @param \Magento\Framework\Indexer\Table\StrategyInterface $strategy * @param \Magento\Framework\App\ResourceConnection $resource */ public function __construct( @@ -66,9 +66,10 @@ public function getTableName($tablePrefix) } /** - * Create temporary index table based on memory table + * Create temporary index table based on memory table{@inheritdoc} * - * {@inheritdoc} + * @param string $tablePrefix + * @return string */ public function prepareTableName($tablePrefix) { diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/DeleteHandler.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/DeleteHandler.php index 024c87c9fc88..a554ff2641df 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/DeleteHandler.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/DeleteHandler.php @@ -60,9 +60,11 @@ public function __construct( } /** + * Delete linked product. + * * @param string $entityType * @param object $entity - * @return object + * @return void * @throws CouldNotDeleteException * @throws NoSuchEntityException * @SuppressWarnings(PHPMD.UnusedFormalParameter) diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByBasePrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByBasePrice.php index 8841b6059c46..841fe17bdcf0 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByBasePrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByBasePrice.php @@ -11,6 +11,9 @@ use Magento\Framework\DB\Select; use Magento\Store\Model\Store; +/** + * Provide Select object for retrieve product id with minimal price. + */ class LinkedProductSelectBuilderByBasePrice implements LinkedProductSelectBuilderInterface { /** @@ -69,7 +72,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function build($productId) { @@ -85,7 +88,7 @@ public function build($productId) [] )->joinInner( [BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS => $productTable], - sprintf('%s.entity_id = link.child_id', BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS, $linkField), + sprintf('%s.entity_id = link.child_id', BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS), ['entity_id'] )->joinInner( ['t' => $priceAttribute->getBackendTable()], diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php index ce0a9b6e461c..494dbac02d79 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value.php @@ -17,11 +17,12 @@ use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; +use Magento\Catalog\Helper\Data; /** * Catalog product custom option resource model * - * @author Magento Core Team <core@magentocommerce.com> + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Value extends AbstractDb { @@ -51,6 +52,11 @@ class Value extends AbstractDb */ private $localeFormat; + /** + * @var Data + */ + private $dataHelper; + /** * Class constructor * @@ -59,17 +65,21 @@ class Value extends AbstractDb * @param StoreManagerInterface $storeManager * @param ScopeConfigInterface $config * @param string $connectionName + * @param Data $dataHelper */ public function __construct( Context $context, CurrencyFactory $currencyFactory, StoreManagerInterface $storeManager, ScopeConfigInterface $config, - $connectionName = null + $connectionName = null, + Data $dataHelper = null ) { $this->_currencyFactory = $currencyFactory; $this->_storeManager = $storeManager; $this->_config = $config; + $this->dataHelper = $dataHelper ?: ObjectManager::getInstance() + ->get(Data::class); parent::__construct($context, $connectionName); } @@ -85,6 +95,7 @@ protected function _construct() /** * Proceed operations after object is saved + * * Save options store data * * @param AbstractModel $object @@ -130,7 +141,7 @@ protected function _saveValuePrices(AbstractModel $object) $optionTypeId = $this->getConnection()->fetchOne($select); if ($optionTypeId) { - if ($object->getStoreId() == '0') { + if ($object->getStoreId() == '0' || $this->dataHelper->isPriceGlobal()) { $bind = ['price' => $price, 'price_type' => $priceType]; $where = [ 'option_type_id = ?' => $optionTypeId, @@ -160,19 +171,22 @@ protected function _saveValuePrices(AbstractModel $object) && isset($objectPrice) && $object->getStoreId() != Store::DEFAULT_STORE_ID ) { - $baseCurrency = $this->_config->getValue( + $website = $this->_storeManager->getStore($object->getStoreId())->getWebsite(); + + $websiteBaseCurrency = $this->_config->getValue( Currency::XML_PATH_CURRENCY_BASE, - 'default' + ScopeInterface::SCOPE_WEBSITE, + $website ); - $storeIds = $this->_storeManager->getStore($object->getStoreId())->getWebsite()->getStoreIds(); + $storeIds = $website->getStoreIds(); if (is_array($storeIds)) { foreach ($storeIds as $storeId) { if ($priceType == 'fixed') { $storeCurrency = $this->_storeManager->getStore($storeId)->getBaseCurrencyCode(); /** @var $currencyModel Currency */ $currencyModel = $this->_currencyFactory->create(); - $currencyModel->load($baseCurrency); + $currencyModel->load($websiteBaseCurrency); $rate = $currencyModel->getRate($storeCurrency); if (!$rate) { $rate = 1; diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/StatusBaseSelectProcessor.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/StatusBaseSelectProcessor.php index c7829ab3a31d..c5c656b72652 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/StatusBaseSelectProcessor.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/StatusBaseSelectProcessor.php @@ -13,6 +13,7 @@ use Magento\Framework\EntityManager\MetadataPool; use Magento\Store\Api\StoreResolverInterface; use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; /** * Class StatusBaseSelectProcessor @@ -30,28 +31,32 @@ class StatusBaseSelectProcessor implements BaseSelectProcessorInterface private $metadataPool; /** - * @var StoreResolverInterface + * @var StoreManagerInterface */ - private $storeResolver; + private $storeManager; /** * @param Config $eavConfig * @param MetadataPool $metadataPool * @param StoreResolverInterface $storeResolver + * @param StoreManagerInterface $storeManager + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( Config $eavConfig, MetadataPool $metadataPool, - StoreResolverInterface $storeResolver + StoreResolverInterface $storeResolver, + StoreManagerInterface $storeManager = null ) { $this->eavConfig = $eavConfig; $this->metadataPool = $metadataPool; - $this->storeResolver = $storeResolver; + $this->storeManager = $storeManager ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(StoreManagerInterface::class); } /** - * @param Select $select - * @return Select + * @inheritdoc */ public function process(Select $select) { @@ -70,7 +75,7 @@ public function process(Select $select) ['status_attr' => $statusAttribute->getBackendTable()], "status_attr.{$linkField} = " . self::PRODUCT_TABLE_ALIAS . ".{$linkField}" . ' AND status_attr.attribute_id = ' . (int)$statusAttribute->getAttributeId() - . ' AND status_attr.store_id = ' . $this->storeResolver->getCurrentStoreId(), + . ' AND status_attr.store_id = ' . $this->storeManager->getStore()->getId(), [] ); diff --git a/app/code/Magento/Catalog/Model/Rss/Category.php b/app/code/Magento/Catalog/Model/Rss/Category.php index a58569d1b59d..653d86b177a5 100644 --- a/app/code/Magento/Catalog/Model/Rss/Category.php +++ b/app/code/Magento/Catalog/Model/Rss/Category.php @@ -6,8 +6,7 @@ namespace Magento\Catalog\Model\Rss; /** - * Class Category - * @package Magento\Catalog\Model\Rss + * Rss Category model. */ class Category { @@ -42,9 +41,11 @@ public function __construct( } /** + * Get products for given category. + * * @param \Magento\Catalog\Model\Category $category * @param int $storeId - * @return $this + * @return \Magento\Catalog\Model\ResourceModel\Product\Collection */ public function getProductCollection(\Magento\Catalog\Model\Category $category, $storeId) { diff --git a/app/code/Magento/Catalog/Model/Template/Filter.php b/app/code/Magento/Catalog/Model/Template/Filter.php index 1eb30ff95a40..8cd61415b958 100644 --- a/app/code/Magento/Catalog/Model/Template/Filter.php +++ b/app/code/Magento/Catalog/Model/Template/Filter.php @@ -66,7 +66,7 @@ public function __construct( * Set use absolute links flag * * @param bool $flag - * @return \Magento\Email\Model\Template\Filter + * @return $this */ public function setUseAbsoluteLinks($flag) { @@ -76,10 +76,11 @@ public function setUseAbsoluteLinks($flag) /** * Setter whether SID is allowed in store directive + * * Doesn't set anything intentionally, since SID is not allowed in any kind of emails * * @param bool $flag - * @return \Magento\Email\Model\Template\Filter + * @return $this */ public function setUseSessionInUrl($flag) { @@ -132,6 +133,7 @@ public function mediaDirective($construction) /** * Retrieve store URL directive + * * Support url and direct_url properties * * @param array $construction diff --git a/app/code/Magento/Catalog/Observer/CategoryProductIndexer.php b/app/code/Magento/Catalog/Observer/CategoryProductIndexer.php new file mode 100644 index 000000000000..ca87efaa8749 --- /dev/null +++ b/app/code/Magento/Catalog/Observer/CategoryProductIndexer.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Observer; + +use Magento\Catalog\Model\Indexer\Category\Product\Processor; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; + +/** + * Checks if a category has changed products and depends on indexer configuration. + */ +class CategoryProductIndexer implements ObserverInterface +{ + /** + * @var Processor + */ + private $processor; + + /** + * @param Processor $processor + */ + public function __construct(Processor $processor) + { + $this->processor = $processor; + } + + /** + * @inheritdoc + */ + public function execute(Observer $observer): void + { + $productIds = $observer->getEvent()->getProductIds(); + if (!empty($productIds) && $this->processor->isIndexerScheduled()) { + $this->processor->markIndexerAsInvalid(); + } + } +} diff --git a/app/code/Magento/Catalog/Observer/SetSpecialPriceStartDate.php b/app/code/Magento/Catalog/Observer/SetSpecialPriceStartDate.php index a597b8fddda9..ed9f89efc689 100644 --- a/app/code/Magento/Catalog/Observer/SetSpecialPriceStartDate.php +++ b/app/code/Magento/Catalog/Observer/SetSpecialPriceStartDate.php @@ -3,9 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Observer; use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; /** * Set value for Special Price start date @@ -13,21 +16,20 @@ class SetSpecialPriceStartDate implements ObserverInterface { /** - * @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface + * @var TimezoneInterface */ private $localeDate; /** - * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate - * @codeCoverageIgnore + * @param TimezoneInterface $localeDate */ - public function __construct(\Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate) + public function __construct(TimezoneInterface $localeDate) { $this->localeDate = $localeDate; } /** - * Set the current date to Special Price From attribute if it empty + * Set the current date to Special Price From attribute if it's empty. * * @param \Magento\Framework\Event\Observer $observer * @return $this @@ -36,8 +38,8 @@ public function execute(\Magento\Framework\Event\Observer $observer) { /** @var $product \Magento\Catalog\Model\Product */ $product = $observer->getEvent()->getProduct(); - if ($product->getSpecialPrice() && !$product->getSpecialFromDate()) { - $product->setData('special_from_date', $this->localeDate->date()); + if ($product->getSpecialPrice() && ! $product->getSpecialFromDate()) { + $product->setData('special_from_date', $this->localeDate->date()->setTime(0, 0)); } return $this; diff --git a/app/code/Magento/Catalog/Plugin/Framework/App/Action/ContextPlugin.php b/app/code/Magento/Catalog/Plugin/Framework/App/Action/ContextPlugin.php new file mode 100644 index 000000000000..6add542b1555 --- /dev/null +++ b/app/code/Magento/Catalog/Plugin/Framework/App/Action/ContextPlugin.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Plugin\Framework\App\Action; + +use Magento\Catalog\Model\Product\ProductList\Toolbar as ToolbarModel; +use Magento\Catalog\Model\Product\ProductList\ToolbarMemorizer; +use Magento\Catalog\Model\Session as CatalogSession; +use Magento\Framework\App\Http\Context as HttpContext; + +/** + * Before dispatch plugin for all frontend controllers to update http context. + */ +class ContextPlugin +{ + /** + * @var ToolbarMemorizer + */ + private $toolbarMemorizer; + + /** + * @var CatalogSession + */ + private $catalogSession; + + /** + * @var HttpContext + */ + private $httpContext; + + /** + * @param ToolbarMemorizer $toolbarMemorizer + * @param CatalogSession $catalogSession + * @param HttpContext $httpContext + */ + public function __construct( + ToolbarMemorizer $toolbarMemorizer, + CatalogSession $catalogSession, + HttpContext $httpContext + ) { + $this->toolbarMemorizer = $toolbarMemorizer; + $this->catalogSession = $catalogSession; + $this->httpContext = $httpContext; + } + + /** + * Update http context with catalog sensitive information. + * + * @return void + */ + public function beforeDispatch() + { + if ($this->toolbarMemorizer->isMemorizingAllowed()) { + $params = [ + ToolbarModel::ORDER_PARAM_NAME, + ToolbarModel::DIRECTION_PARAM_NAME, + ToolbarModel::MODE_PARAM_NAME, + ToolbarModel::LIMIT_PARAM_NAME + ]; + foreach ($params as $param) { + $paramValue = $this->catalogSession->getData($param); + if ($paramValue) { + $this->httpContext->setValue($param, $paramValue, false); + } + } + } + } +} diff --git a/app/code/Magento/Catalog/Plugin/Model/Attribute/Backend/AttributeValidation.php b/app/code/Magento/Catalog/Plugin/Model/Attribute/Backend/AttributeValidation.php index 597a1466a125..eca4d468950e 100644 --- a/app/code/Magento/Catalog/Plugin/Model/Attribute/Backend/AttributeValidation.php +++ b/app/code/Magento/Catalog/Plugin/Model/Attribute/Backend/AttributeValidation.php @@ -7,6 +7,9 @@ use Magento\Store\Model\Store; +/** + * Attribute validation + */ class AttributeValidation { /** @@ -14,6 +17,11 @@ class AttributeValidation */ private $storeManager; + /** + * @var array + */ + private $allowedEntityTypes; + /** * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param array $allowedEntityTypes @@ -27,9 +35,12 @@ public function __construct( } /** + * Around validate + * * @param \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend $subject * @param \Closure $proceed * @param \Magento\Framework\DataObject $entity + * @throws \Magento\Framework\Exception\NoSuchEntityException * @return bool */ public function aroundValidate( @@ -41,7 +52,7 @@ public function aroundValidate( return $entity instanceof $allowedEntity; }, $this->allowedEntityTypes))); - if ($isAllowedType && $this->storeManager->getStore()->getId() !== Store::DEFAULT_STORE_ID) { + if ($isAllowedType && (int) $this->storeManager->getStore()->getId() !== Store::DEFAULT_STORE_ID) { $attrCode = $subject->getAttribute()->getAttributeCode(); // Null is meaning "no value" which should be overridden by value from default scope if (array_key_exists($attrCode, $entity->getData()) && $entity->getData($attrCode) === null) { diff --git a/app/code/Magento/Catalog/Plugin/Model/Product/Option/UpdateProductCustomOptionsAttributes.php b/app/code/Magento/Catalog/Plugin/Model/Product/Option/UpdateProductCustomOptionsAttributes.php new file mode 100644 index 000000000000..dd750cfbc696 --- /dev/null +++ b/app/code/Magento/Catalog/Plugin/Model/Product/Option/UpdateProductCustomOptionsAttributes.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Plugin\Model\Product\Option; + +/** + * Plugin for updating product 'has_options' and 'required_options' attributes + */ +class UpdateProductCustomOptionsAttributes +{ + /** + * @var \Magento\Catalog\Api\ProductRepositoryInterface + */ + private $productRepository; + + /** + * @param \Magento\Catalog\Api\ProductRepositoryInterface $productRepository + */ + public function __construct(\Magento\Catalog\Api\ProductRepositoryInterface $productRepository) + { + $this->productRepository = $productRepository; + } + + /** + * Update product 'has_options' and 'required_options' attributes after option save + * + * @param \Magento\Catalog\Api\ProductCustomOptionRepositoryInterface $subject + * @param \Magento\Catalog\Api\Data\ProductCustomOptionInterface $option + * + * @return \Magento\Catalog\Api\Data\ProductCustomOptionInterface + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterSave( + \Magento\Catalog\Api\ProductCustomOptionRepositoryInterface $subject, + \Magento\Catalog\Api\Data\ProductCustomOptionInterface $option + ) { + $product = $this->productRepository->get($option->getProductSku()); + if (!$product->getHasOptions() || + ($option->getIsRequire() && !$product->getRequiredOptions())) { + $product->setCanSaveCustomOptions(true); + $product->setOptionsSaved(true); + $currentOptions = array_filter($product->getOptions(), function ($iOption) use ($option) { + return $option->getOptionId() != $iOption->getOptionId(); + }); + $currentOptions[] = $option; + $product->setOptions($currentOptions); + $product->save(); + } + + return $option; + } +} diff --git a/app/code/Magento/Catalog/Plugin/Model/ResourceModel/Config.php b/app/code/Magento/Catalog/Plugin/Model/ResourceModel/Config.php index dfa06b6ebe6c..b942f5570f57 100644 --- a/app/code/Magento/Catalog/Plugin/Model/ResourceModel/Config.php +++ b/app/code/Magento/Catalog/Plugin/Model/ResourceModel/Config.php @@ -8,6 +8,9 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\Serialize\SerializerInterface; +/** + * Config cache plugin. + */ class Config { /**#@+ @@ -46,8 +49,10 @@ public function __construct( } /** + * Cache attribute used in listing. + * * @param \Magento\Catalog\Model\ResourceModel\Config $config - * @param callable $proceed + * @param \Closure $proceed * @return array */ public function aroundGetAttributesUsedInListing( @@ -73,8 +78,10 @@ public function aroundGetAttributesUsedInListing( } /** + * Cache attributes used for sorting. + * * @param \Magento\Catalog\Model\ResourceModel\Config $config - * @param callable $proceed + * @param \Closure $proceed * @return array */ public function aroundGetAttributesUsedForSortBy( diff --git a/app/code/Magento/Catalog/Pricing/Price/BasePrice.php b/app/code/Magento/Catalog/Pricing/Price/BasePrice.php index 54a13be864db..77368517a315 100644 --- a/app/code/Magento/Catalog/Pricing/Price/BasePrice.php +++ b/app/code/Magento/Catalog/Pricing/Price/BasePrice.php @@ -30,7 +30,7 @@ public function getValue() $this->value = false; foreach ($this->priceInfo->getPrices() as $price) { if ($price instanceof BasePriceProviderInterface && $price->getValue() !== false) { - $this->value = min($price->getValue(), $this->value ?: $price->getValue()); + $this->value = min($price->getValue(), $this->value !== false ? $this->value: $price->getValue()); } } } diff --git a/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php b/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php index 387ef9416ef6..a5e573caa381 100644 --- a/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php +++ b/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php @@ -29,8 +29,10 @@ public function __construct(CalculatorInterface $calculator) } /** - * Get raw value of "as low as" as a minimal among tier prices - * {@inheritdoc} + * Get raw value of "as low as" as a minimal among tier prices{@inheritdoc} + * + * @param SaleableInterface $saleableItem + * @return float|null */ public function getValue(SaleableInterface $saleableItem) { @@ -49,8 +51,10 @@ public function getValue(SaleableInterface $saleableItem) } /** - * Return calculated amount object that keeps "as low as" value - * {@inheritdoc} + * Return calculated amount object that keeps "as low as" value{@inheritdoc} + * + * @param SaleableInterface $saleableItem + * @return AmountInterface|null */ public function getAmount(SaleableInterface $saleableItem) { @@ -58,6 +62,6 @@ public function getAmount(SaleableInterface $saleableItem) return $value === null ? null - : $this->calculator->getAmount($value, $saleableItem); + : $this->calculator->getAmount($value, $saleableItem, 'tax'); } } diff --git a/app/code/Magento/Catalog/Pricing/Price/RegularPrice.php b/app/code/Magento/Catalog/Pricing/Price/RegularPrice.php index 1397ceb6bf71..2c4e332e7123 100644 --- a/app/code/Magento/Catalog/Pricing/Price/RegularPrice.php +++ b/app/code/Magento/Catalog/Pricing/Price/RegularPrice.php @@ -29,7 +29,7 @@ public function getValue() if ($this->value === null) { $price = $this->product->getPrice(); $priceInCurrentCurrency = $this->priceCurrency->convertAndRound($price); - $this->value = $priceInCurrentCurrency ? floatval($priceInCurrentCurrency) : 0; + $this->value = $priceInCurrentCurrency ? (float)$priceInCurrentCurrency : 0; } return $this->value; } diff --git a/app/code/Magento/Catalog/Pricing/Price/SpecialPrice.php b/app/code/Magento/Catalog/Pricing/Price/SpecialPrice.php index b1bfc6ff4ad6..77c48fdb1667 100644 --- a/app/code/Magento/Catalog/Pricing/Price/SpecialPrice.php +++ b/app/code/Magento/Catalog/Pricing/Price/SpecialPrice.php @@ -11,6 +11,7 @@ use Magento\Framework\Pricing\Price\AbstractPrice; use Magento\Framework\Pricing\Price\BasePriceProviderInterface; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Store\Api\Data\WebsiteInterface; /** * Special price model @@ -46,6 +47,8 @@ public function __construct( } /** + * Retrieve special price. + * * @return bool|float */ public function getValue() @@ -96,19 +99,19 @@ public function getSpecialToDate() } /** - * @return bool + * @inheritdoc */ public function isScopeDateInInterval() { return $this->localeDate->isScopeDateInInterval( - $this->product->getStore(), + WebsiteInterface::ADMIN_CODE, $this->getSpecialFromDate(), $this->getSpecialToDate() ); } /** - * @return bool + * @inheritdoc */ public function isPercentageDiscount() { diff --git a/app/code/Magento/Catalog/Pricing/Price/TierPrice.php b/app/code/Magento/Catalog/Pricing/Price/TierPrice.php index 74f98c2e66a5..f250927889c2 100644 --- a/app/code/Magento/Catalog/Pricing/Price/TierPrice.php +++ b/app/code/Magento/Catalog/Pricing/Price/TierPrice.php @@ -80,7 +80,7 @@ public function __construct( GroupManagementInterface $groupManagement, CustomerGroupRetrieverInterface $customerGroupRetriever = null ) { - $quantity = floatval($quantity) ? $quantity : 1; + $quantity = (float)$quantity ? $quantity : 1; parent::__construct($saleableItem, $quantity, $calculator, $priceCurrency); $this->customerSession = $customerSession; $this->groupManagement = $groupManagement; diff --git a/app/code/Magento/Catalog/Setup/CategorySetup.php b/app/code/Magento/Catalog/Setup/CategorySetup.php index 271387932829..f8542454bef9 100644 --- a/app/code/Magento/Catalog/Setup/CategorySetup.php +++ b/app/code/Magento/Catalog/Setup/CategorySetup.php @@ -10,7 +10,6 @@ use Magento\Catalog\Block\Adminhtml\Category\Helper\Pricestep; use Magento\Catalog\Block\Adminhtml\Category\Helper\Sortby\Available; use Magento\Catalog\Block\Adminhtml\Category\Helper\Sortby\DefaultSortby; -use Magento\Catalog\Block\Adminhtml\Product\Helper\Form\BaseImage; use Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Category as CategoryFormHelper; use Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Weight as WeightFormHelper; use Magento\Catalog\Model\Attribute\Backend\Customlayoutupdate; @@ -54,6 +53,8 @@ use Magento\Theme\Model\Theme\Source\Theme; /** + * Setup category with default entities. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CategorySetup extends EavSetup @@ -593,7 +594,6 @@ public function getDefaultEntities() 'label' => 'Base Image', 'input' => 'media_image', 'frontend' => ImageFrontendModel::class, - 'input_renderer' => BaseImage::class, 'required' => false, 'sort_order' => 0, 'global' => ScopedAttributeInterface::SCOPE_STORE, @@ -626,7 +626,6 @@ public function getDefaultEntities() 'type' => 'varchar', 'label' => 'Media Gallery', 'input' => 'gallery', - 'backend' => Media::class, 'required' => false, 'sort_order' => 4, 'group' => 'Images', diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductToCartActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductToCartActionGroup.xml index 692487c1d60c..a544be434f9c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductToCartActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductToCartActionGroup.xml @@ -11,9 +11,13 @@ <arguments> <argument name="product" defaultValue="product"/> </arguments> - <amOnPage url="/{{product.name}}.html" stepKey="navigateProductPage"/> + <amOnPage url="{{StorefrontProductPage.url(product.custom_attributes[url_key])}}" stepKey="goToProductPage"/> <click selector="{{StorefrontProductPageSection.addToCartBtn}}" stepKey="addToCart"/> - <waitForElementVisible selector="{{StorefrontProductPageSection.successMsg}}" time="30" stepKey="waitForProductAdded"/> - <see selector="{{StorefrontCategoryMainSection.SuccessMsg}}" userInput="You added {{product.name}} to your shopping cart." stepKey="seeAddedToCartMessage"/> + <waitForElementNotVisible selector="{{StorefrontProductActionSection.addToCartButtonTitleIsAdding}}" stepKey="waitForElementNotVisibleAddToCartButtonTitleIsAdding"/> + <waitForElementNotVisible selector="{{StorefrontProductActionSection.addToCartButtonTitleIsAdded}}" stepKey="waitForElementNotVisibleAddToCartButtonTitleIsAdded"/> + <waitForElementVisible selector="{{StorefrontProductActionSection.addToCartButtonTitleIsAddToCart}}" stepKey="waitForElementVisibleAddToCartButtonTitleIsAddToCart"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" time="30" stepKey="waitForProductAddedMessage"/> + <see selector="{{StorefrontMessagesSection.success}}" userInput="You added {{product.name}} to your shopping cart." stepKey="seeAddToCartSuccessMessage"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignImageRolesActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignImageRolesActionGroup.xml new file mode 100644 index 000000000000..90ceb1e4a1f9 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignImageRolesActionGroup.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="AdminAssignImageRolesActionGroup"> + <arguments> + <argument name="image"/> + </arguments> + <conditionalClick selector="{{AdminProductImagesSection.productImagesToggleState('closed')}}" dependentSelector="{{AdminProductImagesSection.productImagesToggleState('open')}}" visible="false" stepKey="clickSectionImage"/> + <click selector="{{AdminProductImagesSection.imageFile(image.fileName)}}" stepKey="clickProductImage"/> + <waitForElementVisible selector="{{AdminProductImagesSection.altText}}" stepKey="seeAltTextSection"/> + <checkOption selector="{{AdminProductImagesSection.roleBase}}" stepKey="checkRoleBase"/> + <checkOption selector="{{AdminProductImagesSection.roleSmall}}" stepKey="checkRoleSmall"/> + <checkOption selector="{{AdminProductImagesSection.roleThumbnail}}" stepKey="checkRoleThumbnail"/> + <checkOption selector="{{AdminProductImagesSection.roleSwatch}}" stepKey="checkRoleSwatch"/> + <click selector="{{AdminSlideOutDialogSection.closeButton}}" stepKey="clickCloseButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml index 76f65381f43f..90d732c9654e 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,14 +231,89 @@ <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"/> </arguments> <amOnPage url="{{AdminCategoryPage.page}}" stepKey="amOnCategoryPage"/> <waitForPageLoad stepKey="waitForPageLoad1"/> - <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(Category.Name)}}" stepKey="navigateToCreatedCategory" /> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandAll"/> <waitForPageLoad stepKey="waitForPageLoad2"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(Category.Name)}}" stepKey="navigateToCreatedCategory" /> <waitForLoadingMaskToDisappear stepKey="waitForSpinner" /> </actionGroup> + + <actionGroup name="ChangeSeoUrlKey"> + <arguments> + <argument name="value" type="string"/> + </arguments> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSeoSection"/> + <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="{{value}}" stepKey="enterURLKey"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategory"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccessMessage"/> + </actionGroup> + + <actionGroup name="ChangeSeoUrlKeyForSubCategory"> + <arguments> + <argument name="value" type="string"/> + </arguments> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSeoSection"/> + <uncheckOption selector="{{AdminCategorySEOSection.UrlKeyDefaultValueCheckbox}}" stepKey="uncheckDefaultValue"/> + <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="{{value}}" stepKey="enterURLKey"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategory"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccessMessage"/> + </actionGroup> + <actionGroup name="OpenCategoryFromCategoryTree"> + <arguments> + <argument name="category" type="string"/> + </arguments> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(category)}}" stepKey="selectCategory"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <waitForElementVisible selector="{{AdminCategoryContentSection.categoryPageTitle}}" stepKey="waitForCategoryTitle"/> + </actionGroup> + <actionGroup name="AdminAssignProductToCategory"> + <arguments> + <argument name="productId" type="string"/> + <argument name="categoryName" type="string"/> + </arguments> + <amOnPage url="{{AdminProductEditPage.url(productId)}}" stepKey="amOnPage"/> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[{{categoryName}}]" stepKey="selectCategory"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the product." stepKey="seeSaveProductMessage"/> + </actionGroup> + <actionGroup name="FillCategoryNameAndUrlKeyAndSave"> + <arguments> + <argument name="categoryName" type="string"/> + <argument name="categoryUrlKey" type="string"/> + </arguments> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{categoryName}}" stepKey="enterCategoryName"/> + <scrollTo selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="scrollToSearchEngineOptimization"/> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSEO"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="{{categoryUrlKey}}" stepKey="enterURLKey"/> + <scrollToTopOfPage stepKey="scrollToTheTopOfPage"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategory"/> + <waitForPageLoad stepKey="waitForPageToLoad1"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml new file mode 100644 index 000000000000..dd66919640a7 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCreateRecentlyProductsWidgetActionGroup" extends="AdminCreateWidgetActionGroup"> + <selectOption selector="{{AdminCatalogProductWidgetSection.productAttributesToShow}}" parameterArray="['Name', 'Image', 'Price']" stepKey="selectAllProductAttributes"/> + <selectOption selector="{{AdminCatalogProductWidgetSection.productButtonsToShow}}" parameterArray="['Add to Cart', 'Add to Compare', 'Add to Wishlist']" stepKey="selectAllProductButtons"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveWidget"/> + <waitForElementVisible selector="{{AdminMessagesSection.successMessage}}" stepKey="waitForSuccessMessageAppears"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="The widget instance has been saved" stepKey="seeSuccess"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenNewProductFormPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenNewProductFormPageActionGroup.xml new file mode 100644 index 000000000000..fe859fab5266 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenNewProductFormPageActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenNewProductFormPageActionGroup"> + <arguments> + <argument name="productType" type="string" defaultValue="simple" /> + <argument name="attributeSetId" type="string" defaultValue="{{defaultAttributeSet.attribute_set_id}}" /> + </arguments> + + <amOnPage url="{{AdminProductCreatePage.url(attributeSetId, productType)}}" stepKey="openProductNewPage" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml index 84231473b685..3c44a8f1898a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml @@ -15,12 +15,20 @@ <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> <waitForElementVisible selector="{{AdminProductGridActionSection.addTypeProduct(product.type_id)}}" stepKey="waitForAddProductDropdown" time="30"/> <click selector="{{AdminProductGridActionSection.addTypeProduct(product.type_id)}}" stepKey="clickAddProductType"/> - <waitForPageLoad stepKey="waitForCreateProductPageLoad"/> + <waitForPageLoad time="30" stepKey="waitForCreateProductPageLoad"/> <seeInCurrentUrl url="{{AdminProductCreatePage.url(AddToDefaultSet.attributeSetId, product.type_id)}}" stepKey="seeNewProductUrl"/> <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Product" stepKey="seeNewProductTitle"/> </actionGroup> + + <!--Navigate to create product page directly via ID--> + <actionGroup name="goToProductPageViaID"> + <arguments> + <argument name="productId" type="string"/> + </arguments> + <amOnPage url="{{AdminProductEditPage.url(productId)}}" stepKey="goToProduct"/> + </actionGroup> - <!--Fill main fields in create product form--> + <!-- Fill main fields in create product form using a product entity --> <actionGroup name="fillMainProductForm"> <arguments> <argument name="product" defaultValue="_defaultProduct"/> @@ -34,6 +42,25 @@ <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{product.weight}}" stepKey="fillProductWeight"/> </actionGroup> + <!-- Fill main fields in create product form using strings for flexibility --> + <actionGroup name="FillMainProductFormByString"> + <arguments> + <argument name="productName" type="string"/> + <argument name="productSku" type="string"/> + <argument name="productPrice" type="string"/> + <argument name="productQuantity" type="string"/> + <argument name="productStatus" type="string"/> + <argument name="productWeight" type="string"/> + </arguments> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{productName}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{productSku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{productPrice}}" stepKey="fillProductPrice"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{productQuantity}}" stepKey="fillProductQty"/> + <selectOption selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{productStatus}}" stepKey="selectStockStatus"/> + <selectOption selector="{{AdminProductFormSection.productWeightSelect}}" userInput="This item has weight" stepKey="selectWeight"/> + <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{productWeight}}" stepKey="fillProductWeight"/> + </actionGroup> + <!--Fill main fields in create product form with no weight, useful for virtual and downloadable products --> <actionGroup name="fillMainProductFormNoWeight"> <arguments> @@ -70,10 +97,21 @@ <!--Save product and see success message--> <actionGroup name="saveProductForm"> + <scrollToTopOfPage stepKey="scrollTopPageProduct"/> + <waitForElementVisible selector="{{AdminProductFormActionSection.saveButton}}" stepKey="waitForSaveProductButton" /> <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> + <waitForPageLoad stepKey="waitForProductToSave"/> <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="You saved the product." stepKey="seeSaveConfirmation"/> </actionGroup> + <actionGroup name="toggleProductEnabled"> + <click selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="toggleEnabled"/> + </actionGroup> + <!-- Save product but do not expect a success message --> + <actionGroup name="SaveProductFormNoSuccessCheck" extends="saveProductForm"> + <remove keyForRemoval="seeSaveConfirmation"/> + </actionGroup> + <!--Upload image for product--> <actionGroup name="addProductImage"> <arguments> @@ -159,7 +197,7 @@ <click selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" stepKey="openCustomOptionsSection"/> <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOption"/> - <fillField userInput="option1" selector="{{AdminProductCustomizableOptionsSection.optionTitleInput}}" stepKey="fillOptionTitle"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('0')}}" userInput="option1" stepKey="fillOptionTitle"/> <click selector="{{AdminProductCustomizableOptionsSection.optionTypeOpenDropDown}}" stepKey="openTypeDropDown"/> <click selector="{{AdminProductCustomizableOptionsSection.optionTypeTextField}}" stepKey="selectTypeTextField"/> <fillField userInput="20" selector="{{AdminProductCustomizableOptionsSection.maxCharactersInput}}" stepKey="fillMaxChars"/> @@ -173,6 +211,44 @@ <seeInField userInput="{{simpleProduct.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="assertFieldUrlKey"/> </actionGroup> + <actionGroup name="ProductSetWebsite"> + <arguments> + <argument name="website" type="string"/> + </arguments> + <scrollTo selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="scrollToWebsites"/> + <conditionalClick selector="{{ProductInWebsitesSection.sectionHeader}}" dependentSelector="{{ProductInWebsitesSection.website(website)}}" visible="false" stepKey="clickToOpenProductInWebsite"/> + <waitForPageLoad stepKey="waitForPageOpened"/> + <click selector="{{ProductInWebsitesSection.website(website)}}" stepKey="selectWebsite"/> + <click selector="{{AdminProductFormAdvancedPricingSection.save}}" stepKey="clickSaveProduct"/> + <waitForPageLoad time='60' stepKey="waitForProducrSaved"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForSaveSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the product." stepKey="seeSaveSuccessMessage"/> + </actionGroup> + + <actionGroup name="ProductSetAdvancedPricing"> + <arguments> + <argument name="website" type="string" defaultValue=""/> + <argument name="group" type="string" defaultValue="Retailer"/> + <argument name="quantity" type="string" defaultValue="1"/> + <argument name="price" type="string" defaultValue="Discount"/> + <argument name="amount" type="string" defaultValue="45"/> + </arguments> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingButton"/> + <waitForElement selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="waitForCustomerGroupPriceAddButton"/> + <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="addCustomerGroupAllGroupsQty1PriceDiscountAnd10percent"/> + <waitForElement selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" stepKey="waitForSelectCustomerGroupNameAttribute2"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{website}}" stepKey="selectProductWebsiteValue"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{group}}" stepKey="selectProductCustomGroupValue"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{quantity}}" stepKey="fillProductTierPriceQtyInput"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceValueTypeSelect('0')}}" userInput="{{price}}" stepKey="selectProductTierPriceValueType"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPricePercentageValuePriceInput('0')}}" userInput="{{amount}}" stepKey="selectProductTierPricePriceInput"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> + <waitForPageLoad stepKey="WaitForProductSave"/> + <click selector="{{AdminProductFormAdvancedPricingSection.save}}" stepKey="clickSaveProduct1"/> + <waitForPageLoad time="60" stepKey="WaitForProductSave1"/> + <see userInput="You saved the product." stepKey="seeSaveConfirmation"/> + </actionGroup> + <!--Assert text in Related, Up-Sell or Cross-Sell section in Admin Product page--> <actionGroup name="AssertTextInAdminProductRelatedUpSellCrossSellSection"> <arguments> @@ -198,10 +274,29 @@ <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{sku}}" stepKey="fillProductSkuFilter"/> <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> <waitForPageLoad stepKey="waitForPageToLoad"/> - <click selector="{{AdminProductModalSlideGridSection.productGridXRowYColumnButton('1', '1')}}" stepKey="selectProduct"/> + <checkOption selector="{{AdminProductModalSlideGridSection.productRowCheckboxBySku(sku)}}" stepKey="selectProduct"/> <click selector="{{AdminAddRelatedProductsModalSection.AddSelectedProductsButton}}" stepKey="addRelatedProductSelected"/> </actionGroup> + <!--Click AddCrossSellProducts and adds product by SKU--> + <actionGroup name="addCrossSellProductBySku"> + <arguments> + <argument name="sku"/> + </arguments> + <!--Scroll up to avoid error--> + <scrollTo selector="{{AdminProductFormRelatedUpSellCrossSellSection.relatedDropdown}}" x="0" y="-100" stepKey="scrollTo"/> + <conditionalClick selector="{{AdminProductFormRelatedUpSellCrossSellSection.relatedDropdown}}" dependentSelector="{{AdminProductFormRelatedUpSellCrossSellSection.relatedDependent}}" visible="false" stepKey="openDropDownIfClosedRelatedUpSellCrossSell"/> + <click selector="{{AdminProductFormRelatedUpSellCrossSellSection.AddCrossSellProductsButton}}" stepKey="clickAddCrossSellButton"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{sku}}" stepKey="fillProductSkuFilter"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{AdminProductModalSlideGridSection.productGridXRowYColumnButton('1', '1')}}" stepKey="selectProduct"/> + <click selector="{{AdminProductCrossSellModalSection.addSelectedProducts}}" stepKey="addRelatedProductSelected"/> + <waitForPageLoad stepKey="waitForModalDisappear"/> + </actionGroup> + <!--Add special price to product in Admin product page--> <actionGroup name="AddSpecialPriceToProductActionGroup"> <arguments> @@ -214,4 +309,178 @@ <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDone"/> <waitForElementNotVisible selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" stepKey="waitForCloseModalWindow"/> </actionGroup> + + <!--Select Product In Websites--> + <actionGroup name="SelectProductInWebsitesActionGroup"> + <arguments> + <argument name="website" type="string"/> + </arguments> + <scrollTo selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="scrollToWebsites"/> + <conditionalClick selector="{{ProductInWebsitesSection.sectionHeader}}" dependentSelector="{{AdminProductContentSection.sectionHeaderShow}}" visible="false" stepKey="expandSection"/> + <waitForPageLoad stepKey="waitForPageOpened"/> + <checkOption selector="{{ProductInWebsitesSection.website(website)}}" stepKey="selectWebsite"/> + </actionGroup> + + <actionGroup name="AdminProductAddSpecialPrice"> + <arguments> + <argument name="specialPrice" type="string"/> + </arguments> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink1"/> + <waitForElementVisible selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" stepKey="waitSpecialPrice1"/> + <click selector="{{AdminProductFormAdvancedPricingSection.useDefaultPrice}}" stepKey="checkUseDefault"/> + <fillField userInput="{{specialPrice}}" selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" stepKey="fillSpecialPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDone"/> + <waitForElementNotVisible selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" stepKey="waitForCloseModalWindow"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessage"/> + </actionGroup> + + <!--Switch to New Store view--> + <actionGroup name="SwitchToTheNewStoreView"> + <arguments> + <argument name="storeViewName" type="string"/> + </arguments> + <scrollTo selector="{{AdminProductContentSection.pageHeader}}" stepKey="scrollToUp"/> + <waitForElementVisible selector="{{AdminProductFormActionSection.changeStoreButton}}" stepKey="waitForElementBecomeVisible"/> + <click selector="{{AdminProductFormActionSection.changeStoreButton}}" stepKey="clickStoreviewSwitcher"/> + <click selector="{{AdminProductFormActionSection.selectStoreView(storeViewName)}}" stepKey="chooseStoreView"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="acceptStoreSwitchingMessage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> + + <!--Create a Simple Product--> + <actionGroup name="createSimpleProductAndAddToWebsite"> + <arguments> + <argument name="product"/> + <argument name="website" type="string"/> + </arguments> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToCatalogProductGrid"/> + <waitForPageLoad stepKey="waitForProductGrid"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductDropdown"/> + <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="clickAddSimpleProduct"/> + <fillField userInput="{{product.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="fillProductName"/> + <fillField userInput="{{product.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="fillProductSKU"/> + <fillField userInput="{{product.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="fillProductPrice"/> + <fillField userInput="{{product.quantity}}" selector="{{AdminProductFormSection.productQuantity}}" stepKey="fillProductQuantity"/> + <click selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="openProductInWebsites"/> + <click selector="{{ProductInWebsitesSection.website(website)}}" stepKey="selectWebsite"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave"/> + <waitForLoadingMaskToDisappear stepKey="waitForProductPageSave"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessage"/> + </actionGroup> + + <actionGroup name="CreatedProductConnectToWebsite"> + <arguments> + <argument name="website"/> + <argument name="product"/> + </arguments> + <click stepKey="openProduct" selector="{{AdminProductGridActionSection.productName(product.sku)}}"/> + <waitForPageLoad stepKey="waitForProductPage"/> + <scrollTo selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="ScrollToWebsites"/> + <click selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="openWebsitesList"/> + <waitForPageLoad stepKey="waitForWebsitesList"/> + <click selector="{{ProductInWebsitesSection.website(website.name)}}" stepKey="SelectWebsite"/> + <click selector="{{AdminProductFormAdvancedPricingSection.save}}" stepKey="clickSaveProduct"/> + <waitForPageLoad stepKey="waitForSave"/> + </actionGroup> + + <!--Check tier price with a discount percentage on product--> + <actionGroup name="AssertDiscountsPercentageOfProducts"> + <arguments> + <argument name="amount" type="string" defaultValue="45"/> + </arguments> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingButton"/> + <waitForElement selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="waitForCustomerGroupPriceAddButton"/> + <grabValueFrom selector="{{AdminProductFormAdvancedPricingSection.productTierPricePercentageValuePriceInput('0')}}" stepKey="grabProductTierPriceInput"/> + <assertEquals stepKey="assertProductTierPriceInput"> + <expectedResult type="string">{{amount}}</expectedResult> + <actualResult type="string">$grabProductTierPriceInput</actualResult> + </assertEquals> + </actionGroup> + + <!-- This action group goes to the product index page, opens the drop down and clicks the specified product type for adding a product --> + <actionGroup name="GoToSpecifiedCreateProductPage"> + <arguments> + <argument type="string" name="productType" defaultValue="simple"/> + </arguments> + <comment userInput="actionGroup:GoToSpecifiedCreateProductPage" stepKey="actionGroupComment"/> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductDropdown"/> + <click selector="{{AdminProductGridActionSection.addTypeProduct(productType)}}" stepKey="clickAddProduct"/> + <waitForPageLoad stepKey="waitForFormToLoad"/> + </actionGroup> + + <!-- This action group simply navigates to the product catalog page --> + <actionGroup name="GoToProductCatalogPage"> + <comment userInput="actionGroup:GoToProductCatalogPage" stepKey="actionGroupComment"/> + <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="GoToCatalogProductPage"/> + <waitForPageLoad stepKey="WaitForPageToLoad"/> + </actionGroup> + + <actionGroup name="SetProductUrlKey"> + <arguments> + <argument name="product" defaultValue="_defaultProduct"/> + </arguments> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSection"/> + <fillField userInput="{{product.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="fillUrlKey"/> + </actionGroup> + + <actionGroup name="SetProductUrlKeyByString"> + <arguments> + <argument name="urlKey" type="string"/> + </arguments> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSection"/> + <fillField userInput="{{urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="fillUrlKey"/> + </actionGroup> + + <actionGroup name="SetCategoryByName"> + <arguments> + <argument name="categoryName" type="string"/> + </arguments> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[{{categoryName}}]" stepKey="searchAndSelectCategory"/> + </actionGroup> + + <actionGroup name="expandAdminProductSection"> + <arguments> + <argument name="sectionSelector" defaultValue="{{AdminProductContentSection.sectionHeader}}" type="string"/> + <argument name="sectionDependentSelector" defaultValue="{{AdminProductContentSection.sectionHeaderShow}}" type="string"/> + </arguments> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <waitForElementVisible time="30" selector="{{sectionSelector}}" stepKey="waitForSection"/> + <conditionalClick selector="{{sectionSelector}}" dependentSelector="{{sectionDependentSelector}}" visible="false" stepKey="expandSection"/> + <waitForPageLoad time="30" stepKey="waitForSectionToExpand"/> + </actionGroup> + <actionGroup name="navigateToCreatedProductEditPage"> + <arguments> + <argument name="product" defaultValue="_defaultProduct"/> + </arguments> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToAdminProductIndexPage"/> + <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <waitForPageLoad stepKey="waitForClearFilters"/> + <dontSeeElement selector="{{AdminProductGridFilterSection.clearFilters}}" stepKey="dontSeeClearFilters"/> + <click selector="{{AdminProductGridFilterSection.viewDropdown}}" stepKey="openViewBookmarksTab"/> + <click selector="{{AdminProductGridFilterSection.viewBookmark('Default View')}}" stepKey="resetToDefaultGridView"/> + <waitForPageLoad stepKey="waitForResetToDefaultView"/> + <see selector="{{AdminProductGridFilterSection.viewDropdown}}" userInput="Default View" stepKey="seeDefaultViewSelected"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{product.sku}}" stepKey="fillProductSkuFilter"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + <waitForPageLoad stepKey="waitForFilterOnGrid"/> + <click selector="{{AdminProductGridSection.selectRowBasedOnName(product.name)}}" stepKey="clickProduct"/> + <waitForPageLoad stepKey="waitForProductEditPageLoad"/> + <waitForElementVisible selector="{{AdminProductFormBundleSection.productSku}}" stepKey="waitForProductSKUField"/> + <seeInField selector="{{AdminProductFormBundleSection.productSku}}" userInput="{{product.sku}}" stepKey="seeProductSKU"/> + </actionGroup> + <actionGroup name="addUpSellProductBySku" extends="addRelatedProductBySku"> + <click selector="{{AdminProductFormRelatedUpSellCrossSellSection.AddUpSellProductsButton}}" stepKey="clickAddRelatedProductButton"/> + <conditionalClick selector="{{AdminAddUpSellProductsModalSection.Modal}} {{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminAddUpSellProductsModalSection.Modal}} {{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <click selector="{{AdminAddUpSellProductsModalSection.Modal}} {{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> + <fillField selector="{{AdminAddUpSellProductsModalSection.Modal}} {{AdminProductGridFilterSection.skuFilter}}" userInput="{{sku}}" stepKey="fillProductSkuFilter"/> + <click selector="{{AdminAddUpSellProductsModalSection.Modal}} {{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{AdminAddUpSellProductsModalSection.Modal}}{{AdminProductModalSlideGridSection.productGridXRowYColumnButton('1', '1')}}" stepKey="selectProduct"/> + <click selector="{{AdminAddUpSellProductsModalSection.AddSelectedProductsButton}}" stepKey="addRelatedProductSelected"/> + <waitForPageLoad stepKey="waitForPageToLoad1"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml index fd8083869206..86158aba68f8 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml @@ -13,8 +13,255 @@ <argument name="ProductAttribute"/> </arguments> <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" + userInput="{{ProductAttribute.attribute_code}}" stepKey="setAttributeCode"/> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> + <click selector="{{AdminProductAttributeGridSection.FirstRow}}" stepKey="clickOnAttributeRow"/> + <waitForPageLoad stepKey="waitForPageLoad2" /> + </actionGroup> + <actionGroup name="navigateToEditProductAttribute"> + <arguments> + <argument name="ProductAttribute" type="string"/> + </arguments> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <fillField selector="{{AdminProductAttributeGridSection.GridFilterFrontEndLabel}}" userInput="{{ProductAttribute}}" stepKey="navigateToAttributeEditPage1" /> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="navigateToAttributeEditPage2" /> + <waitForPageLoad stepKey="waitForPageLoad2" /> + <click selector="{{AdminProductAttributeGridSection.FirstRow}}" stepKey="navigateToAttributeEditPage3" /> + <waitForPageLoad stepKey="waitForPageLoad3" /> + </actionGroup> + + <actionGroup name="AdminCreateAttributeFromProductPage"> + <arguments> + <argument name="attributeName" type="string"/> + <argument name="attributeType" type="string" defaultValue="TextField"/> + </arguments> + <click selector="{{AdminProductFormSection.addAttributeBtn}}" stepKey="clickAddAttributeBtn"/> + <see userInput="Select Attribute" stepKey="checkNewAttributePopUpAppeared"/> + <click selector="{{AdminProductFormAttributeSection.createNewAttribute}}" stepKey="clickCreateNewAttribute"/> + <fillField selector="{{AdminProductFormNewAttributeSection.attributeLabel}}" userInput="{{attributeName}}" stepKey="fillAttributeLabel"/> + <selectOption selector="{{AdminProductFormNewAttributeSection.attributeType}}" userInput="{{attributeType}}" stepKey="selectAttributeType"/> + <click selector="{{AdminProductFormNewAttributeSection.saveAttribute}}" stepKey="saveAttribute"/> + </actionGroup> + + <actionGroup name="AdminCreateAttributeWithValueWithTwoStoreViesFromProductPage" extends="AdminCreateAttributeFromProductPage"> + <remove keyForRemoval="saveAttribute"/> + <arguments> + <argument name="firstStoreViewName" type="string"/> + <argument name="secondStoreViewName" type="string"/> + </arguments> + <click selector="{{AdminProductFormNewAttributeSection.addValue}}" stepKey="addValue" after="selectAttributeType"/> + <seeElement selector="{{AdminProductFormNewAttributeSection.optionViewName(firstStoreViewName))}}" stepKey="seeFirstStoreView"/> + <seeElement selector="{{AdminProductFormNewAttributeSection.optionViewName(firstStoreViewName))}}" stepKey="seeSecondStoreView"/> + <fillField selector="{{AdminProductFormNewAttributeSection.optionValue('1'))}}" userInput="default" stepKey="fillDefaultStoreView"/> + <fillField selector="{{AdminProductFormNewAttributeSection.optionValue('2'))}}" userInput="admin" stepKey="fillAdminStoreView"/> + <fillField selector="{{AdminProductFormNewAttributeSection.optionValue('3'))}}" userInput="view1" stepKey="fillFirstStoreView"/> + <fillField selector="{{AdminProductFormNewAttributeSection.optionValue('4'))}}" userInput="view2" stepKey="fillSecondStoreView"/> + + <!--Check store view in Manage Titles section--> + <click selector="{{AdminProductFormNewAttributeSection.manageTitlesHeader}}" stepKey="openManageTitlesSection"/> + <seeElement selector="{{AdminProductFormNewAttributeSection.manageTitlesViewName(customStoreEN.name)}}" stepKey="seeFirstStoreViewName"/> + <seeElement selector="{{AdminProductFormNewAttributeSection.manageTitlesViewName(customStoreFR.name)}}" stepKey="seeSecondStoreViewName"/> + <click selector="{{AdminProductFormNewAttributeSection.saveAttribute}}" stepKey="saveAttribute1"/> + </actionGroup> + + <!-- Creates attribute and attribute set from the product page--> + <actionGroup name="AdminProductPageCreateAttributeSetWithAttribute"> + <arguments> + <argument name="attributeName" type="string"/> + <argument name="attributeSetName" type="string"/> + <argument name="attributeType" type="string" defaultValue="TextField"/> + </arguments> + <click selector="{{AdminProductFormSection.addAttributeBtn}}" stepKey="clickAddAttributeBtn"/> + <waitForPageLoad stepKey="waitForSidePanel"/> + <see userInput="Select Attribute" stepKey="checkNewAttributePopUpAppeared"/> + <click selector="{{AdminProductFormAttributeSection.createNewAttribute}}" stepKey="clickCreateNewAttribute"/> + <fillField selector="{{AdminProductFormNewAttributeSection.attributeLabel}}" userInput="{{attributeName}}" stepKey="fillAttributeLabel"/> + <selectOption selector="{{AdminProductFormNewAttributeSection.attributeType}}" userInput="{{attributeType}}" stepKey="selectAttributeType"/> + <click selector="{{AdminProductFormNewAttributeSection.saveInNewSet}}" stepKey="saveAttribute"/> + <fillField selector="{{AdminProductFormNewAttributeNewSetSection.setName}}" userInput="{{attributeSetName}}" stepKey="fillSetName"/> + <click selector="{{AdminProductFormNewAttributeNewSetSection.accept}}" stepKey="acceptPopup"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingToFinish"/> + <!-- Product page will hang if there is no reload--> + <reloadPage stepKey="reloadPage"/> + <waitForPageLoad stepKey="waitForReload"/> + </actionGroup> + + <!-- Create attribute and set with given search weight and defaultValue from the Edit Product Page --> + <actionGroup name="AdminCreateAttributeWithSearchWeight" extends="AdminProductPageCreateAttributeSetWithAttribute" insertAfter="selectAttributeType"> + <arguments> + <argument name="weight" type="string" defaultValue="1"/> + <argument name="defaultValue" type="string" defaultValue="default"/> + </arguments> + <click selector="{{AdminProductFormNewAttributeAdvancedSection.sectionHeader}}" stepKey="openAdvancedSection"/> + <fillField selector="{{AdminProductFormNewAttributeAdvancedSection.defaultValue}}" userInput="{{defaultValue}}" stepKey="inputDefault"/> + <click selector="{{AdminProductFormNewAttributeStorefrontSection.sectionHeader}}" stepKey="openStorefrontSection"/> + <checkOption selector="{{AdminProductFormNewAttributeStorefrontSection.useInSearch}}" stepKey="checkUseInSearch"/> + <waitForElementVisible selector="{{AdminProductFormNewAttributeStorefrontSection.searchWeight}}" stepKey="waitForSearchWeight"/> + <selectOption selector="{{AdminProductFormNewAttributeStorefrontSection.searchWeight}}" userInput="{{weight}}" stepKey="selectWeight"/> + </actionGroup> + + <actionGroup name="AdminProductPageSelectAttributeSet"> + <arguments> + <argument name="attributeSetName" type="string"/> + </arguments> + <click stepKey="openDropdown" selector="{{AdminProductFormSection.attributeSet}}"/> + <fillField stepKey="filter" selector="{{AdminProductFormSection.attributeSetFilter}}" userInput="{{attributeSetName}}"/> + <click stepKey="clickResult" selector="{{AdminProductFormSection.attributeSetFilterResult}}"/> + </actionGroup> + + <actionGroup name="AdminProductPageFillTextAttributeValueByName"> + <arguments> + <argument name="attributeName" type="string"/> + <argument name="value" type="string"/> + </arguments> + <click stepKey="openSection" selector="{{AdminProductAttributeSection.attributeSectionHeader}}"/> + <fillField stepKey="fillValue" selector="{{AdminProductAttributeSection.textAttributeByName(attributeName)}}" userInput="{{value}}"/> + </actionGroup> + + <actionGroup name="changeUseForPromoRuleConditionsProductAttribute"> + <arguments> + <argument name="option" type="string"/> + </arguments> + <click selector="{{StorefrontPropertiesSection.StoreFrontPropertiesTab}}" stepKey="clickStoreFrontPropertiesTab"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <selectOption selector="{{StorefrontPropertiesSection.useForPromoRuleConditions}}" userInput="{{option}}" stepKey="changeOption"/> + <click selector="{{AttributePropertiesSection.Save}}" stepKey="saveAttribute"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the product attribute." stepKey="successMessage"/> + </actionGroup> + <actionGroup name="deleteProductAttribute" extends="navigateToCreatedProductAttribute"> + <click selector="{{AttributePropertiesSection.DeleteAttribute}}" stepKey="deleteAttribute"/> + <click selector="{{ModalConfirmationSection.OkButton}}" stepKey="ClickOnDeleteButton"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" + stepKey="waitForSuccessMessage"/> + </actionGroup> + <actionGroup name="deleteProductAttributeByLabel"> + <arguments> + <argument name="ProductAttribute"/> + </arguments> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="{{ProductAttribute.default_label}}" stepKey="setAttributeCode"/> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> + <click selector="{{AdminProductAttributeGridSection.FirstRow}}" stepKey="clickOnAttributeRow"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <click selector="{{AttributePropertiesSection.DeleteAttribute}}" stepKey="deleteAttribute"/> + <click selector="{{ModalConfirmationSection.OkButton}}" stepKey="ClickOnDeleteButton"/> <waitForPageLoad stepKey="waitForPageLoad1"/> - <click selector="{{AdminProductAttributeGridSection.AttributeCode(ProductAttribute.attribute_code)}}" stepKey="navigateToAttributeEditPage1" /> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="waitForSuccessMessage"/> + </actionGroup> + <!-- Delete product attribute by Attribute Code --> + <actionGroup name="deleteProductAttributeByAttributeCode"> + <arguments> + <argument name="ProductAttributeCode" type="string"/> + </arguments> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="{{ProductAttributeCode}}" stepKey="setAttributeCode"/> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> + <click selector="{{AdminProductAttributeGridSection.FirstRow}}" stepKey="clickOnAttributeRow"/> <waitForPageLoad stepKey="waitForPageLoad2" /> + <click selector="{{AttributePropertiesSection.DeleteAttribute}}" stepKey="deleteAttribute"/> + <click selector="{{ModalConfirmationSection.OkButton}}" stepKey="ClickOnDeleteButton"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="waitForSuccessMessage"/> + </actionGroup> + <!--Filter product attribute by Attribute Code --> + <actionGroup name="filterProductAttributeByAttributeCode"> + <arguments> + <argument name="ProductAttributeCode" type="string"/> + </arguments> + <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGrid"/> + <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="{{ProductAttributeCode}}" stepKey="setAttributeCode"/> + <waitForPageLoad stepKey="waitForUserInput"/> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> + </actionGroup> + <!--Filter product attribute by Default Label --> + <actionGroup name="filterProductAttributeByDefaultLabel"> + <arguments> + <argument name="productAttributeLabel" type="string"/> + </arguments> + <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGrid"/> + <fillField selector="{{AdminProductAttributeGridSection.GridFilterFrontEndLabel}}" userInput="{{productAttributeLabel}}" stepKey="setDefaultLabel"/> + <waitForPageLoad stepKey="waitForUserInput"/> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> + </actionGroup> + <actionGroup name="saveProductAttribute"> + <waitForElementVisible selector="{{AttributePropertiesSection.Save}}" stepKey="waitForSaveButton"/> + <click selector="{{AttributePropertiesSection.Save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForAttributeToSave"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSuccessMessage"/> + </actionGroup> + <actionGroup name="confirmChangeInputTypeModal"> + <waitForElementVisible selector="{{AdminEditProductAttributesSection.ProductDataMayBeLostConfirmButton}}" stepKey="waitForChangeInputTypeButton"/> + <click selector="{{AdminEditProductAttributesSection.ProductDataMayBeLostConfirmButton}}" stepKey="clickChangeInputTypeButton"/> + <waitForElementNotVisible selector="{{AdminEditProductAttributesSection.ProductDataMayBeLostModal}}" stepKey="waitForChangeInputTypeModalGone"/> + </actionGroup> + <actionGroup name="saveProductAttributeInUse"> + <waitForElementVisible selector="{{AttributePropertiesSection.Save}}" stepKey="waitForSaveButton"/> + <click selector="{{AttributePropertiesSection.Save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForAttributeToSave"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSuccessMessage"/> + </actionGroup> + + <!--Clicks Add Attribute and adds the given attribute--> + <actionGroup name="addProductAttributeInProductModal"> + <arguments> + <argument name="attributeCode" type="string"/> + </arguments> + <click stepKey="addAttribute" selector="{{AdminProductFormActionSection.addAttributeButton}}"/> + <conditionalClick selector="{{AdminProductAddAttributeModalSection.clearFilters}}" dependentSelector="{{AdminProductAddAttributeModalSection.clearFilters}}" visible="true" stepKey="clearFilters"/> + <click stepKey="clickFilters" selector="{{AdminProductAddAttributeModalSection.filters}}"/> + <fillField stepKey="fillCode" selector="{{AdminProductAddAttributeModalSection.attributeCodeFilter}}" userInput="{{attributeCode}}"/> + <click stepKey="clickApply" selector="{{AdminProductAddAttributeModalSection.applyFilters}}"/> + <waitForPageLoad stepKey="waitForFilters"/> + <checkOption selector="{{AdminProductAddAttributeModalSection.firstRowCheckBox}}" stepKey="checkAttribute"/> + <click stepKey="addSelected" selector="{{AdminProductAddAttributeModalSection.addSelected}}"/> + </actionGroup> + + <!--Clicks createNewAttribute and fills out form--> + <actionGroup name="createProductAttribute"> + <arguments> + <argument name="attribute" type="entity" defaultValue="productAttributeWysiwyg"/> + </arguments> + <click stepKey="createNewAttribute" selector="{{AdminProductAttributeGridSection.createNewAttributeBtn}}"/> + <fillField stepKey="fillDefaultLabel" selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{attribute.attribute_code}}"/> + <selectOption selector="{{AttributePropertiesSection.InputType}}" stepKey="checkInputType" userInput="{{attribute.frontend_input}}"/> + <selectOption selector="{{AttributePropertiesSection.ValueRequired}}" stepKey="checkRequired" userInput="{{attribute.is_required_admin}}"/> + <click stepKey="saveAttribute" selector="{{AttributePropertiesSection.Save}}"/> + </actionGroup> + + <!-- Inputs text default value and attribute code--> + <actionGroup name="createProductAttributeWithTextField" extends="createProductAttribute" insertAfter="checkRequired"> + <click stepKey="openAdvancedProperties" selector="{{AdvancedAttributePropertiesSection.AdvancedAttributePropertiesSectionToggle}}"/> + <fillField stepKey="fillCode" selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" userInput="{{attribute.attribute_code}}"/> + <fillField stepKey="fillDefaultValue" selector="{{AdvancedAttributePropertiesSection.DefaultValueText}}" userInput="{{attribute.default_value}}"/> + </actionGroup> + + <!-- Inputs date default value and attribute code--> + <actionGroup name="createProductAttributeWithDateField" extends="createProductAttribute" insertAfter="checkRequired"> + <arguments> + <argument name="date" type="string"/> + </arguments> + <click stepKey="openAdvancedProperties" selector="{{AdvancedAttributePropertiesSection.AdvancedAttributePropertiesSectionToggle}}"/> + <fillField stepKey="fillCode" selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" userInput="{{attribute.attribute_code}}"/> + <fillField stepKey="fillDefaultValue" selector="{{AdvancedAttributePropertiesSection.DefaultValueDate}}" userInput="{{date}}"/> + </actionGroup> + + <!-- Creates dropdown option at row without saving--> + <actionGroup name="createAttributeDropdownNthOption"> + <arguments> + <argument name="row" type="string"/> + <argument name="adminName" type="string"/> + <argument name="frontName" type="string"/> + </arguments> + <click stepKey="clickAddOptions" selector="{{AttributePropertiesSection.dropdownAddOptions}}"/> + <waitForPageLoad stepKey="waitForNewOption"/> + <fillField stepKey="fillAdmin" selector="{{AttributePropertiesSection.dropdownNthOptionAdmin(row)}}" userInput="{{adminName}}"/> + <fillField stepKey="fillStoreView" selector="{{AttributePropertiesSection.dropdownNthOptionDefaultStoreView(row)}}" userInput="{{frontName}}"/> + </actionGroup> + + <!-- Creates dropdown option at row as default--> + <actionGroup name="createAttributeDropdownNthOptionAsDefault" extends="createAttributeDropdownNthOption"> + <checkOption selector="{{AttributePropertiesSection.dropdownNthOptionIsDefault(row)}}" stepKey="setAsDefault" after="fillStoreView"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetActionGroup.xml index 5948ca12dcf0..019d103a31cf 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetActionGroup.xml @@ -45,4 +45,53 @@ <fillField selector="{{AdminProductAttributeSetSection.name}}" userInput="{{label}}" stepKey="fillName"/> <click selector="{{AdminProductAttributeSetSection.saveBtn}}" stepKey="clickSave1"/> </actionGroup> + <actionGroup name="goToAttributeGridPage"> + <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSets"/> + </actionGroup> + <actionGroup name="goToAttributeSetByName"> + <arguments> + <argument name="name" type="string"/> + </arguments> + <click selector="{{AdminProductAttributeSetGridSection.resetFilter}}" stepKey="clickResetButton"/> + <fillField selector="{{AdminProductAttributeSetGridSection.filter}}" userInput="{{name}}" stepKey="filterByName"/> + <click selector="{{AdminProductAttributeSetGridSection.searchBtn}}" stepKey="clickSearch"/> + <click selector="{{AdminProductAttributeSetGridSection.nthRow('1')}}" stepKey="clickFirstRow"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> + <!-- Filter By Attribute Label --> + <actionGroup name="filterProductAttributeByAttributeLabel"> + <arguments> + <argument name="productAttributeLabel" type="string"/> + </arguments> + <fillField selector="{{AdminProductAttributeGridSection.attributeLabelFilter}}" userInput="{{productAttributeLabel}}" stepKey="setAttributeLabel"/> + <waitForPageLoad stepKey="waitForUserInput"/> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> + </actionGroup> + <actionGroup name="FilterProductAttributeSetGridByAttributeSetName"> + <arguments> + <argument name="name" type="string"/> + </arguments> + <click selector="{{AdminProductAttributeSetGridSection.resetFilter}}" stepKey="clickResetButton"/> + <fillField selector="{{AdminProductAttributeSetGridSection.filter}}" userInput="{{name}}" stepKey="filterByName"/> + <click selector="{{AdminProductAttributeSetGridSection.searchBtn}}" stepKey="clickSearch"/> + <click selector="{{AdminProductAttributeSetGridSection.nthRow('1')}}" stepKey="clickFirstRow"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad1"/> + </actionGroup> + + <!-- Delete attribute set --> + <actionGroup name="deleteAttributeSetByLabel"> + <arguments> + <argument name="label" type="string"/> + </arguments> + <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSets"/> + <waitForPageLoad stepKey="waitForAttributeSetPageLoad"/> + <fillField selector="{{AdminProductAttributeSetGridSection.filter}}" userInput="{{label}}" stepKey="filterByName"/> + <click selector="{{AdminProductAttributeSetGridSection.searchBtn}}" stepKey="clickSearch"/> + <click selector="{{AdminProductAttributeSetGridSection.nthRow('1')}}" stepKey="clickFirstRow"/> + <waitForPageLoad stepKey="waitForClick"/> + <click selector="{{AdminProductAttributeSetSection.deleteBtn}}" stepKey="clickDelete"/> + <click selector="{{AdminProductAttributeSetSection.modalOk}}" stepKey="confirmDelete"/> + <waitForPageLoad stepKey="waitForDeleteToFinish"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="The attribute set has been removed." stepKey="seeDeleteMessage"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml index 1bd9bb4a09c8..ad32b8edbd24 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml @@ -144,6 +144,18 @@ <click selector="{{AdminProductGridFilterSection.clearFilters}}" stepKey="clickClearFiltersAfter"/> </actionGroup> + <!-- Filter product grid by sku, name --> + <actionGroup name="filterProductGridBySkuAndName"> + <arguments> + <argument name="product" defaultValue="_defaultProduct"/> + </arguments> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{product.sku}}" stepKey="fillProductSkuFilter"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{product.name}}" stepKey="fillProductNameFilter"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + </actionGroup> + <!--Delete a product by filtering grid and using delete action--> <actionGroup name="deleteProductUsingProductGrid"> <arguments> @@ -155,6 +167,7 @@ <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{product.sku}}" stepKey="fillProductSkuFilter"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{product.name}}" stepKey="fillProductNameFilter"/> <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> <see selector="{{AdminProductGridSection.productGridCell('1', 'SKU')}}" userInput="{{product.sku}}" stepKey="seeProductSkuInGrid"/> <click selector="{{AdminProductGridSection.multicheckDropdown}}" stepKey="openMulticheckDropdown"/> @@ -164,6 +177,13 @@ <waitForElementVisible selector="{{AdminProductGridConfirmActionSection.title}}" stepKey="waitForConfirmModal"/> <click selector="{{AdminProductGridConfirmActionSection.ok}}" stepKey="confirmProductDelete"/> </actionGroup> + <!--Delete all products by filtering grid and using mass delete action--> + <actionGroup name="deleteAllDuplicateProductUsingProductGrid" extends="deleteProductUsingProductGrid"> + <arguments> + <argument name="product"/> + </arguments> + <remove keyForRemoval="seeProductSkuInGrid"/> + </actionGroup> <!--Delete a product by filtering grid and using delete action--> <actionGroup name="deleteProductBySku"> @@ -172,8 +192,7 @@ </arguments> <!--TODO use other action group for filtering grid when MQE-539 is implemented --> <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> - <waitForPageLoad time="60" stepKey="waitForPageLoadInitial"/> - <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{sku}}" stepKey="fillProductSkuFilter"/> <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> @@ -182,8 +201,20 @@ <click selector="{{AdminProductGridSection.multicheckOption('Select All')}}" stepKey="selectAllProductInFilteredGrid"/> <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickActionDropdown"/> <click selector="{{AdminProductGridSection.bulkActionOption('Delete')}}" stepKey="clickDeleteAction"/> - <waitForElementVisible selector="{{AdminProductGridConfirmActionSection.title}}" stepKey="waitForConfirmModal"/> - <click selector="{{AdminProductGridConfirmActionSection.ok}}" stepKey="confirmProductDelete"/> + <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForConfirmModal"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmProductDelete"/> + <see selector="{{AdminMessagesSection.success}}" userInput="record(s) have been deleted." stepKey="seeSuccessMessage"/> + </actionGroup> + + <actionGroup name="deleteProductByName" extends="deleteProductBySku"> + <arguments> + <argument name="sku" type="string" defaultValue=""/> + <argument name="name" type="string"/> + </arguments> + <remove keyForRemoval="fillProductSkuFilter"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{name}}" stepKey="fillProductSkuFilter" after="openProductFilters"/> + <remove keyForRemoval="seeProductSkuInGrid"/> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Name')}}" userInput="{{name}}" stepKey="seeProductNameInGrid" after="clickApplyFilters"/> </actionGroup> <!--Open product for edit by clicking row X and column Y in product grid--> @@ -206,4 +237,65 @@ <conditionalClick selector="{{AdminProductGridTableHeaderSection.id('descend')}}" dependentSelector="{{AdminProductGridTableHeaderSection.id('ascend')}}" visible="false" stepKey="sortById"/> <waitForPageLoad stepKey="waitForPageLoad"/> </actionGroup> + + <!--Disabled a product by filtering grid and using change status action--> + <actionGroup name="ChangeStatusProductUsingProductGridActionGroup"> + <arguments> + <argument name="product"/> + <argument name="status" defaultValue="Enable" type="string" /> + </arguments> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <waitForPageLoad time="60" stepKey="waitForPageLoadInitial"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{product.sku}}" stepKey="fillProductSkuFilter"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + <see selector="{{AdminProductGridSection.productGridCell('1', 'SKU')}}" userInput="{{product.sku}}" stepKey="seeProductSkuInGrid"/> + <click selector="{{AdminProductGridSection.multicheckDropdown}}" stepKey="openMulticheckDropdown"/> + <click selector="{{AdminProductGridSection.multicheckOption('Select All')}}" stepKey="selectAllProductInFilteredGrid"/> + + <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickActionDropdown"/> + <click selector="{{AdminProductGridSection.bulkActionOption('Change status')}}" stepKey="clickChangeStatusAction"/> + <click selector="{{AdminProductGridSection.changeStatus('status')}}" stepKey="clickChangeStatusDisabled" parameterized="true"/> + <waitForPageLoad stepKey="waitForStatusToBeChanged"/> + <see selector="{{AdminMessagesSection.success}}" userInput="A total of 1 record(s) have been updated." stepKey="seeSuccessMessage"/> + <waitForLoadingMaskToDisappear stepKey="waitForMaskToDisappear"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial2"/> + </actionGroup> + + <actionGroup name="NavigateToAndResetProductGridToDefaultView" extends="resetProductGridToDefaultView"> + <amOnPage url="{{AdminProductIndexPage.url}}" before="clickClearFilters" stepKey="goToAdminProductIndexPage"/> + <waitForPageLoad after="goToAdminProductIndexPage" stepKey="waitForProductIndexPageToLoad"/> + </actionGroup> + <actionGroup name="NavigateToAndResetProductAttributeGridToDefaultView"> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <waitForPageLoad stepKey="waitForGridLoad"/> + </actionGroup> + <!--Filter and select the the product --> + <actionGroup name="filterAndSelectProduct"> + <arguments> + <argument name="productSku" type="string"/> + </arguments> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{productSku}}" stepKey="fillProductSkuFilter"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + <waitForElementNotVisible selector="{{AdminProductGridSection.loadingMask}}" stepKey="waitForFilteredGridLoad" time="30"/> + <click stepKey="openSelectedProduct" selector="{{AdminProductGridSection.productRowBySku(productSku)}}"/> + <waitForPageLoad stepKey="waitForProductToLoad"/> + <waitForElementVisible selector="{{AdminHeaderSection.pageTitle}}" stepKey="waitForProductTitle"/> + </actionGroup> + + <actionGroup name="deleteProductsIfTheyExist"> + <conditionalClick selector="{{AdminProductGridSection.multicheckDropdown}}" dependentSelector="{{AdminProductGridSection.firstProductRow}}" visible="true" stepKey="openMulticheckDropdown"/> + <conditionalClick selector="{{AdminProductGridSection.multicheckOption('Select All')}}" dependentSelector="{{AdminProductGridSection.firstProductRow}}" visible="true" stepKey="selectAllProductInFilteredGrid"/> + <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickActionDropdown"/> + <click selector="{{AdminProductGridSection.bulkActionOption('Delete')}}" stepKey="clickDeleteAction"/> + <waitForElementVisible selector="{{AdminProductGridConfirmActionSection.ok}}" stepKey="waitForModalPopUp"/> + <click selector="{{AdminProductGridConfirmActionSection.ok}}" stepKey="confirmProductDelete"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAttributeDeletionErrorMessageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAttributeDeletionErrorMessageActionGroup.xml new file mode 100644 index 000000000000..9402ac05d79a --- /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 000000000000..25390e6b4a80 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductAttributePresenceInCatalogProductGridActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertProductAttributePresenceInCatalogProductGridActionGroup"> + <arguments> + <argument name="productAttribute" type="entity"/> + </arguments> + <waitForPageLoad stepKey="waitForCatalogProductGridPageLoad"/> + <seeElement selector="{{AdminGridHeaders.headerByName(productAttribute.label)}}" stepKey="seeAttributeInHeaders"/> + </actionGroup> +</actionGroups> + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductInfoOnEditPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductInfoOnEditPageActionGroup.xml new file mode 100644 index 000000000000..7917fe68aaeb --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductInfoOnEditPageActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertProductInfoOnEditPageActionGroup" extends="OpenEditProductOnBackendActionGroup"> + <arguments> + <argument name="product" type="entity"/> + </arguments> + <waitForPageLoad stepKey="waitForProductToLoad"/> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{product.name}}" stepKey="seeProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{product.sku}}" stepKey="seeProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{product.price}}" stepKey="seeProductPrice"/> + <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{product.quantity}}" stepKey="seeProductQuantity"/> + <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{product.status}}" stepKey="seeProductStockStatus"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductOnAdminGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductOnAdminGridActionGroup.xml new file mode 100644 index 000000000000..963c9d9f1c91 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductOnAdminGridActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertProductOnAdminGridActionGroup" extends="viewProductInAdminGrid"> + <arguments> + <argument name="product" defaultValue="_defaultProduct"/> + </arguments> + <remove keyForRemoval="clickClearFiltersAfter"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckProductsOrderActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckProductsOrderActionGroup.xml new file mode 100644 index 000000000000..f7cd2e707628 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckProductsOrderActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + + <actionGroup name="CompareTwoProductsOrder"> + <arguments> + <argument name="product_1"/> + <argument name="product_2"/> + </arguments> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToHomePage"/> + <waitForPageLoad stepKey="waitForPageLoad5"/> + <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByNumber('1')}}" userInput="alt" stepKey="grabFirstProductName1_1"/> + <assertEquals expected="{{product_1.name}}" actual="($grabFirstProductName1_1)" message="notExpectedOrder" stepKey="compare1"/> + <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByNumber('2')}}" userInput="alt" stepKey="grabFirstProductName2_2"/> + <assertEquals expected="{{product_2.name}}" actual="($grabFirstProductName2_2)" message="notExpectedOrder" stepKey="compare2"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/CreateNewProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateNewProductActionGroup.xml similarity index 86% rename from app/code/Magento/Braintree/Test/Mftf/ActionGroup/CreateNewProductActionGroup.xml rename to app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateNewProductActionGroup.xml index 19de3e859ae9..53de47f81060 100644 --- a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/CreateNewProductActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateNewProductActionGroup.xml @@ -5,10 +5,10 @@ * See COPYING.txt for license details. */ --> + <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="CreateNewProductActionGroup"> - <click stepKey="openCatalog" selector="{{AdminMenuSection.catalog}}"/> <waitForPageLoad stepKey="waitForCatalogSubmenu" time="5"/> <click stepKey="clickOnProducts" selector="{{CatalogSubmenuSection.products}}"/> @@ -22,4 +22,4 @@ <waitForElementVisible stepKey="waitForSuccessfullyCreatedMessage" selector="{{NewProductPageSection.createdSuccessMessage}}" time="10"/> <waitForPageLoad stepKey="waitForPageLoad" time="10"/> </actionGroup> -</actionGroups> \ No newline at end of file +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CustomOptionsActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CustomOptionsActionGroup.xml index 7373d5baea0c..b914d5e20712 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CustomOptionsActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CustomOptionsActionGroup.xml @@ -15,7 +15,6 @@ <argument name="productOption"/> <argument name="productOption2"/> </arguments> - <click stepKey="clickAddOptions" selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}"/> <waitForPageLoad stepKey="waitForAddProductPageLoad"/> @@ -48,10 +47,56 @@ <fillField selector="{{AdminProductCustomizableOptionsSection.lastOptionTitle}}" userInput="{{option.title}}" stepKey="fillTitle"/> <click selector="{{AdminProductCustomizableOptionsSection.lastOptionTypeParent}}" stepKey="openTypeSelect"/> <click selector="{{AdminProductCustomizableOptionsSection.optionType('File')}}" stepKey="selectTypeFile"/> - <waitForElementVisible selector="{{AdminProductCustomizableOptionsSection.optionPrice}}" stepKey="waitForElements"/> - <fillField selector="{{AdminProductCustomizableOptionsSection.optionPrice}}" userInput="{{option.price}}" stepKey="fillPrice"/> - <selectOption selector="{{AdminProductCustomizableOptionsSection.optionPriceType}}" userInput="{{option.price_type}}" stepKey="selectPriceType"/> - <fillField selector="{{AdminProductCustomizableOptionsSection.optionFileExtensions}}" userInput="{{option.file_extension}}" stepKey="fillCompatibleExtensions"/> + <waitForElementVisible selector="{{AdminProductCustomizableOptionsSection.optionPrice('0')}}" stepKey="waitForElements"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionPrice('0')}}" userInput="{{option.price}}" stepKey="fillPrice"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.optionPriceType('0')}}" userInput="{{option.price_type}}" stepKey="selectPriceType"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionFileExtensions('0')}}" userInput="{{option.file_extension}}" stepKey="fillCompatibleExtensions"/> + </actionGroup> + <actionGroup name="AddProductCustomOptionField"> + <arguments> + <argument name="option" defaultValue="ProductOptionField"/> + <argiment name="optionIndex" type="string"/> + </arguments> + <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" visible="false" stepKey="openCustomOptionSection"/> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOption"/> + <waitForElementVisible selector="{{AdminProductCustomizableOptionsSection.lastOptionTitle}}" stepKey="waitForOption"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.lastOptionTitle}}" userInput="{{option.title}}" stepKey="fillTitle"/> + <click selector="{{AdminProductCustomizableOptionsSection.lastOptionTypeParent}}" stepKey="openTypeSelect"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionType('Field')}}" stepKey="selectTypeFile"/> + <waitForElementVisible selector="{{AdminProductCustomizableOptionsSection.optionPrice(optionIndex)}}" stepKey="waitForElements"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionPrice(optionIndex)}}" userInput="{{option.price}}" stepKey="fillPrice"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.optionPriceType(optionIndex)}}" userInput="{{option.price_type}}" stepKey="selectPriceType"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionSku(optionIndex)}}" userInput="{{option.title}}" stepKey="fillSku"/> + </actionGroup> + <actionGroup name="importProductCustomizableOptions"> + <arguments> + <argument name="productName" type="string"/> + </arguments> + <click selector="{{AdminProductCustomizableOptionsSection.importOptions}}" stepKey="clickImportOptions"/> + <waitForElementVisible selector="{{AdminProductImportOptionsSection.selectProductTitle}}" stepKey="waitForTitleVisible"/> + <conditionalClick selector="{{AdminProductImportOptionsSection.resetFiltersButton}}" dependentSelector="{{AdminProductImportOptionsSection.resetFiltersButton}}" visible="true" stepKey="clickResetFilters"/> + <click selector="{{AdminProductImportOptionsSection.filterButton}}" stepKey="clickFilterButton"/> + <waitForElementVisible selector="{{AdminProductImportOptionsSection.nameField}}" stepKey="waitForNameField"/> + <fillField selector="{{AdminProductImportOptionsSection.nameField}}" userInput="{{productName}}" stepKey="fillProductName"/> + <click selector="{{AdminProductImportOptionsSection.applyFiltersButton}}" stepKey="clickApplyFilters"/> + <checkOption selector="{{AdminProductImportOptionsSection.firstRowItemCheckbox}}" stepKey="checkProductCheckbox"/> + <click selector="{{AdminProductImportOptionsSection.importButton}}" stepKey="clickImport"/> + </actionGroup> + <actionGroup name="resetImportOptionFilter"> + <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" visible="false" stepKey="openCustomOptionSection"/> + <click selector="{{AdminProductCustomizableOptionsSection.importOptions}}" stepKey="clickImportOptions"/> + <click selector="{{AdminProductImportOptionsSection.resetFiltersButton}}" stepKey="clickResetFilterButton"/> + </actionGroup> + <actionGroup name="checkCustomizableOptionImport"> + <arguments> + <argument name="option" defaultValue="ProductOptionField"/> + <argiment name="optionIndex" type="string"/> + </arguments> + <grabValueFrom selector="{{AdminProductCustomizableOptionsSection.optionTitleInput(optionIndex)}}" stepKey="grabOptionTitle"/> + <grabValueFrom selector="{{AdminProductCustomizableOptionsSection.optionPrice(optionIndex)}}" stepKey="grabOptionPrice"/> + <grabValueFrom selector="{{AdminProductCustomizableOptionsSection.optionSku(optionIndex)}}" stepKey="grabOptionSku"/> + <assertEquals expected="{{option.title}}" expectedType="string" actual="$grabOptionTitle" stepKey="assertOptionTitle"/> + <assertEquals expected="{{option.price}}" expectedType="string" actual="$grabOptionPrice" stepKey="assertOptionPrice"/> + <assertEquals expected="{{option.title}}" expectedType="string" actual="$grabOptionSku" stepKey="assertOptionSku"/> </actionGroup> - </actionGroups> diff --git a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/DeleteProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductActionGroup.xml similarity index 85% rename from app/code/Magento/Braintree/Test/Mftf/ActionGroup/DeleteProductActionGroup.xml rename to app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductActionGroup.xml index 724c6d92846c..7491b39aa8f2 100644 --- a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/DeleteProductActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductActionGroup.xml @@ -5,8 +5,9 @@ * See COPYING.txt for license details. */ --> + <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="DeleteProductActionGroup"> <arguments> <argument name="productName" defaultValue=""/> @@ -23,4 +24,4 @@ <click stepKey="clickOnOk" selector="{{ProductsPageSection.ok}}"/> <waitForElementVisible stepKey="waitForSuccessfullyDeletedMessage" selector="{{ProductsPageSection.deletedSuccessMessage}}" time="10"/> </actionGroup> -</actionGroups> \ No newline at end of file +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductAttributeByAttributeCodeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductAttributeByAttributeCodeActionGroup.xml new file mode 100644 index 000000000000..575cbdcd9b6d --- /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/OpenEditProductOnBackendActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenEditProductOnBackendActionGroup.xml index 8f89a85e1489..ea2543cd5c2a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenEditProductOnBackendActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenEditProductOnBackendActionGroup.xml @@ -11,7 +11,7 @@ <arguments> <argument name="product" defaultValue="product"/> </arguments> - <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductRow"/> + <click selector="{{AdminProductGridSection.productRowBySku(product.sku)}}" stepKey="clickOnProductRow"/> <waitForPageLoad time="30" stepKey="waitForProductPageLoad"/> <seeInField userInput="{{product.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="seeProductSkuOnEditProductPage"/> </actionGroup> 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 000000000000..31b024c82a9a --- /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 000000000000..67cdd8192fcb --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchAttributeByCodeOnProductAttributeGridActionGroup.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="SearchAttributeByCodeOnProductAttributeGridActionGroup"> + <arguments> + <argument name="productAttributeCode" type="string"/> + </arguments> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <waitForPageLoad stepKey="waitForAdminProductAttributeGridLoad"/> + <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGrid"/> + <waitForPageLoad stepKey="waitForAdminProductAttributeGridSectionLoad"/> + <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="{{productAttributeCode}}" stepKey="setAttributeCode"/> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskOnGridToDisappear"/> + <see selector="{{AdminProductAttributeGridSection.attributeCodeColumn}}" userInput="{{productAttributeCode}}" stepKey="seeAttributeCodeInGrid"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchForProductOnBackendActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchForProductOnBackendActionGroup.xml index 113e108577aa..aca9ba24c116 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchForProductOnBackendActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchForProductOnBackendActionGroup.xml @@ -12,10 +12,24 @@ <argument name="product" defaultValue="product"/> </arguments> <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad time="30" stepKey="waitForProductsPageToLoad"/> + <waitForPageLoad time="60" stepKey="waitForProductsPageToLoad"/> <click selector="{{AdminProductFiltersSection.filtersButton}}" stepKey="openFiltersSectionOnProductsPage"/> <conditionalClick selector="{{AdminProductFiltersSection.clearFiltersButton}}" dependentSelector="{{AdminProductFiltersSection.clearFiltersButton}}" visible="true" stepKey="cleanFiltersIfTheySet"/> <fillField userInput="{{product.sku}}" selector="{{AdminProductFiltersSection.skuInput}}" stepKey="fillSkuFieldOnFiltersSection"/> <click selector="{{AdminProductFiltersSection.apply}}" stepKey="clickApplyFiltersButton"/> </actionGroup> + + <actionGroup name="SearchForProductOnBackendByNameActionGroup" extends="SearchForProductOnBackendActionGroup"> + <arguments> + <argument name="productName" type="string"/> + </arguments> + <remove keyForRemoval="fillSkuFieldOnFiltersSection"/> + <fillField userInput="{{productName}}" selector="{{AdminProductFiltersSection.nameInput}}" after="cleanFiltersIfTheySet" stepKey="fillNameFieldOnFiltersSection"/> + </actionGroup> + + <actionGroup name="ClearProductsFilterActionGroup"> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad time="30" stepKey="waitForProductsPageToLoad"/> + <conditionalClick selector="{{AdminProductFiltersSection.clearFiltersButton}}" dependentSelector="{{AdminProductFiltersSection.clearFiltersButton}}" visible="true" stepKey="cleanFiltersIfTheySet"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductImagesOnProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductImagesOnProductPageActionGroup.xml new file mode 100644 index 000000000000..1bb7c179dfca --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductImagesOnProductPageActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAssertProductImagesOnProductPageActionGroup"> + <arguments> + <argument name="productImage" type="string" defaultValue="Magento_Catalog/images/product/placeholder/image.jpg" /> + </arguments> + <waitForElementNotVisible selector="{{StorefrontProductMediaSection.gallerySpinner}}" stepKey="waitGallerySpinnerDisappear" /> + <seeElement selector="{{StorefrontProductMediaSection.gallery}}" stepKey="seeProductGallery" /> + <seeElement selector="{{StorefrontProductMediaSection.productImage(productImage)}}" stepKey="seeProductImage" /> + <click selector="{{StorefrontProductMediaSection.productImage(productImage)}}" stepKey="openFullscreenImage" /> + <waitForPageLoad stepKey="waitForGalleryLoaded" /> + <seeElement selector="{{StorefrontProductMediaSection.productImageFullscreen(productImage)}}" stepKey="seeFullscreenProductImage" /> + <click selector="{{StorefrontProductMediaSection.closeFullscreenImage}}" stepKey="closeFullScreenImage" /> + <waitForPageLoad stepKey="waitForGalleryDisappear" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductInWidgetActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductInWidgetActionGroup.xml new file mode 100644 index 000000000000..c25b73bab21a --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductInWidgetActionGroup.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Check the product in recently viewed widget --> + <actionGroup name="StorefrontAssertProductInRecentlyViewedWidgetActionGroup"> + <arguments> + <argument name="product"/> + </arguments> + <waitForElementVisible selector="{{StorefrontWidgetsSection.widgetRecentlyViewedProductsGrid}}" stepKey="waitWidgetRecentlyViewedProductsGrid"/> + <see selector="{{StorefrontWidgetsSection.widgetRecentlyViewedProductsGrid}}" userInput="{{product.name}}" stepKey="seeProductInRecentlyViewedWidget"/> + </actionGroup> + + <!-- Check the product in recently compared widget --> + <actionGroup name="StorefrontAssertProductInRecentlyComparedWidgetActionGroup"> + <arguments> + <argument name="product"/> + </arguments> + <waitForElementVisible selector="{{StorefrontWidgetsSection.widgetRecentlyComparedProductsGrid}}" stepKey="waitWidgetRecentlyComparedProductsGrid"/> + <see selector="{{StorefrontWidgetsSection.widgetRecentlyComparedProductsGrid}}" userInput="{{product.name}}" stepKey="seeProductInRecentlyComparedWidget"/> + </actionGroup> + + <!-- Check the product in recently ordered widget --> + <actionGroup name="StorefrontAssertProductInRecentlyOrderedWidgetActionGroup"> + <arguments> + <argument name="product"/> + </arguments> + <waitForElementVisible selector="{{StorefrontWidgetsSection.widgetRecentlyOrderedProductsGrid}}" stepKey="waitWidgetRecentlyOrderedProductsGrid"/> + <see selector="{{StorefrontWidgetsSection.widgetRecentlyOrderedProductsGrid}}" userInput="{{product.name}}" stepKey="seeProductInRecentlyOrderedWidget"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductNameOnProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductNameOnProductPageActionGroup.xml new file mode 100644 index 000000000000..6cb156723b28 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductNameOnProductPageActionGroup.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAssertProductNameOnProductPageActionGroup"> + <arguments> + <argument name="productName" type="string"/> + </arguments> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{productName}}" stepKey="seeProductName" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductPriceOnProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductPriceOnProductPageActionGroup.xml new file mode 100644 index 000000000000..3c62ef89e584 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductPriceOnProductPageActionGroup.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAssertProductPriceOnProductPageActionGroup"> + <arguments> + <argument name="productPrice" type="string"/> + </arguments> + <see selector="{{StorefrontProductInfoMainSection.price}}" userInput="{{productPrice}}" stepKey="seeProductPrice" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductSkuOnProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductSkuOnProductPageActionGroup.xml new file mode 100644 index 000000000000..85d3927a6d6d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductSkuOnProductPageActionGroup.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAssertProductSkuOnProductPageActionGroup"> + <arguments> + <argument name="productSku" type="string"/> + </arguments> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{productSku}}" stepKey="seeProductSku" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml index c980c43b8f3a..4c7c011028c9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml @@ -15,12 +15,28 @@ <argument name="mode" type="string"/> <argument name="numOfProductsPerPage" type="string"/> <argument name="sortBy" type="string" defaultValue="position"/> + <argument name="sort" type="string" defaultValue="asc"/> </arguments> <!-- Go to storefront category page --> - <amOnPage url="{{StorefrontCategoryPage.url(category)}}?product_list_limit={{numOfProductsPerPage}}&product_list_mode={{mode}}&product_list_order={{sortBy}}" stepKey="onCategoryPage"/> + <amOnPage url="{{StorefrontCategoryPage.url(category)}}?product_list_limit={{numOfProductsPerPage}}&product_list_mode={{mode}}&product_list_order={{sortBy}}&product_list_dir={{sort}}" stepKey="onCategoryPage"/> <waitForPageLoad stepKey="waitForPageLoad"/> </actionGroup> + <actionGroup name="VerifyCategoryPageParameters"> + <arguments> + <argument name="category"/> + <argument name="mode" type="string"/> + <argument name="numOfProductsPerPage" type="string"/> + <argument name="sortBy" type="string" defaultValue="position"/> + </arguments> + <seeInCurrentUrl url="/{{category.custom_attributes[url_key]}}.html" stepKey="checkUrl"/> + <seeInTitle userInput="{{category.name}}" stepKey="assertCategoryNameInTitle"/> + <see userInput="{{category.name}}" selector="{{StorefrontCategoryMainSection.CategoryTitle}}" stepKey="assertCategoryName"/> + <see userInput="{{mode}}" selector="{{StorefrontCategoryMainSection.modeGridIsActive}}" stepKey="assertViewMode"/> + <see userInput="{{numOfProductsPerPage}}" selector="{{StorefrontCategoryMainSection.perPage}}" stepKey="assertNumberOfProductsPerPage"/> + <see userInput="{{sortBy}}" selector="{{StorefrontCategoryMainSection.sortedBy}}" stepKey="assertSortedBy"/> + </actionGroup> + <!-- Check the category page --> <actionGroup name="StorefrontCheckCategoryActionGroup"> <arguments> @@ -50,4 +66,19 @@ <click selector="{{StorefrontCategoryMainSection.modeListButton}}" stepKey="switchCategoryViewToListMode"/> <waitForElement selector="{{StorefrontCategoryMainSection.CategoryTitle}}" time="30" stepKey="waitForCategoryReload"/> </actionGroup> + + <actionGroup name="GoToSubCategoryPage"> + <arguments> + <argument name="parentCategory"/> + <argument name="subCategory"/> + <argument name="urlPath" type="string"/> + </arguments> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName(parentCategory.name)}}" stepKey="moveMouseOnMainCategory"/> + <waitForElementVisible selector="{{StorefrontHeaderSection.NavigationCategoryByName(subCategory.name)}}" stepKey="waitForSubCategoryVisible"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(subCategory.name)}}" stepKey="goToCategory"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <seeInCurrentUrl url="{{urlPath}}.html" stepKey="checkUrl"/> + <seeInTitle userInput="{{subCategory.name}}" stepKey="assertCategoryNameInTitle"/> + <see userInput="{{subCategory.name}}" selector="{{StorefrontCategoryMainSection.CategoryTitle}}" stepKey="assertCategoryName"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontClickAddToCartOnProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontClickAddToCartOnProductPageActionGroup.xml new file mode 100644 index 000000000000..fb2065d228d5 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontClickAddToCartOnProductPageActionGroup.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontClickAddToCartOnProductPageActionGroup"> + <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addToCart" /> + <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenProductPageActionGroup.xml new file mode 100644 index 000000000000..f5fabae5fc4c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenProductPageActionGroup.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontOpenProductPageActionGroup"> + <arguments> + <argument name="productUrl" type="string"/> + </arguments> + <amOnPage url="{{StorefrontProductPage.url(productUrl)}}" stepKey="openProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoaded"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/VerifyProductTypeOrderActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/VerifyProductTypeOrderActionGroup.xml new file mode 100644 index 000000000000..aec21f3bc48c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/VerifyProductTypeOrderActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="VerifyProductTypeOrder"> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductDropdown"/> + <waitForPageLoad stepKey="waitForLoad"/> + <seeElement stepKey="seeSimpleInOrder" selector="{{AdminProductDropdownOrderSection.simpleProduct}}"/> + <seeElement stepKey="seeVirtualInOrder" selector="{{AdminProductDropdownOrderSection.virtualProduct}}"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 000000000000..24e1fe9cf5ec --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminMenuCatalog"> + <data key="pageTitle">Catalog</data> + <data key="title">Catalog</data> + <data key="dataUiId">magento-catalog-catalog</data> + </entity> + <entity name="AdminMenuCatalogCategories"> + <data key="pageTitle">Default Category (ID: 2)</data> + <data key="title">Categories</data> + <data key="dataUiId">magento-catalog-catalog-categories</data> + </entity> + <entity name="AdminMenuCatalogProducts"> + <data key="pageTitle">Products</data> + <data key="title">Products</data> + <data key="dataUiId">magento-catalog-catalog-products</data> + </entity> + <entity name="AdminMenuStoresAttributesAttributeSet"> + <data key="pageTitle">Attribute Sets</data> + <data key="title">Attribute Set</data> + <data key="dataUiId">magento-catalog-catalog-attributes-sets</data> + </entity> + <entity name="AdminMenuStoresAttributesProduct"> + <data key="pageTitle">Product Attributes</data> + <data key="title">Product</data> + <data key="dataUiId">magento-catalog-catalog-attributes-attributes</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/AttributeSetData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/AttributeSetData.xml new file mode 100644 index 000000000000..6e1b25fb9cdc --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/AttributeSetData.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="defaultAttributeSet"> + <data key="attribute_set_id">4</data> + <data key="attribute_set_name">Default</data> + <data key="sort_order">0</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogAttributeSetData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogAttributeSetData.xml new file mode 100644 index 000000000000..d78c03a51dd7 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogAttributeSetData.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="CatalogAttributeSet" type="CatalogAttributeSet"> + <data key="attribute_set_name" unique="suffix">test_set_</data> + <data key="attributeGroupId">7</data> + <data key="skeletonId">4</data> + </entity> +</entities> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogPriceData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogPriceData.xml new file mode 100644 index 000000000000..0f7f4da1b68c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogPriceData.xml @@ -0,0 +1,32 @@ +<?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="CatalogPriceScopeWebsite" type="catalog_price_config_state"> + <requiredEntity type="scope">scopeWebsite</requiredEntity> + <requiredEntity type="default_product_price">defaultProductPrice</requiredEntity> + </entity> + <entity name="scopeWebsite" type="scope"> + <data key="value">1</data> + </entity> + <entity name="defaultProductPrice" type="default_product_price"> + <data key="value">0</data> + </entity> + + <entity name="DefaultConfigCatalogPrice" type="catalog_price_config_state"> + <requiredEntity type="scope">scopeGlobal</requiredEntity> + <requiredEntity type="default_product_price">defaultProductPrice</requiredEntity> + </entity> + <entity name="scopeGlobal" type="scope"> + <data key="value">0</data> + </entity> + <entity name="defaultProductPrice" type="default_product_price"> + <data key="value"/> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogRecentlyProductsConfigData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogRecentlyProductsConfigData.xml new file mode 100644 index 000000000000..d1e469deaebb --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogRecentlyProductsConfigData.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="EnableSynchronizeWidgetProductsWithBackendStorage" type="catalog_recently_products"> + <requiredEntity type="synchronize_with_backend">EnableCatalogRecentlyProductsSynchronize</requiredEntity> + </entity> + + <entity name="EnableCatalogRecentlyProductsSynchronize" type="synchronize_with_backend"> + <data key="value">1</data> + </entity> + + <entity name="DisableSynchronizeWidgetProductsWithBackendStorage" type="catalog_recently_products"> + <requiredEntity type="synchronize_with_backend">DefaultCatalogRecentlyProductsSynchronize</requiredEntity> + </entity> + + <entity name="DefaultCatalogRecentlyProductsSynchronize" type="synchronize_with_backend"> + <data key="value">0</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogSpecialPriceData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogSpecialPriceData.xml new file mode 100644 index 000000000000..31783526932b --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogSpecialPriceData.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="specialProductPrice" type="catalogSpecialPrice"> + <data key="price">99.99</data> + <data key="store_id">0</data> + <var key="sku" entityType="product2" entityKey="sku" /> + </entity> + <entity name="specialProductPrice2" type="catalogSpecialPrice"> + <data key="price">55.55</data> + <data key="store_id">0</data> + <var key="sku" entityType="product" entityKey="sku" /> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogStorefrontConfigData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogStorefrontConfigData.xml new file mode 100644 index 000000000000..abf01f00dbbc --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogStorefrontConfigData.xml @@ -0,0 +1,63 @@ +<?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="RememberPaginationCatalogStorefrontConfig" type="catalog_storefront_config"> + <requiredEntity type="grid_per_page_values">GridPerPageValues</requiredEntity> + <requiredEntity type="remember_pagination">RememberCategoryPagination</requiredEntity> + </entity> + + <entity name="GridPerPageValues" type="grid_per_page_values"> + <data key="value">9,12,20,24</data> + </entity> + + <entity name="RememberCategoryPagination" type="remember_pagination"> + <data key="value">1</data> + </entity> + + <entity name="DefaultCatalogStorefrontConfiguration" type="default_catalog_storefront_config"> + <requiredEntity type="catalogStorefrontFlagZero">DefaultCatalogStorefrontFlagZero</requiredEntity> + <data key="list_allow_all">DefaultListAllowAll</data> + <data key="flat_catalog_product">DefaultFlatCatalogProduct</data> + </entity> + + <entity name="DefaultCatalogStorefrontFlagZero" type="catalogStorefrontFlagZero"> + <data key="value">0</data> + </entity> + + <entity name="DefaultListAllowAll" type="list_allow_all"> + <data key="value">0</data> + </entity> + + <entity name="DefaultFlatCatalogProduct" type="flat_catalog_product"> + <data key="value">0</data> + </entity> + + <entity name="UseFlatCatalogCategoryAndProduct" type="catalog_storefront_config"> + <requiredEntity type="flat_catalog_product">UseFlatCatalogProduct</requiredEntity> + <requiredEntity type="flat_catalog_category">UseFlatCatalogCategory</requiredEntity> + </entity> + + <entity name="UseFlatCatalogProduct" type="flat_catalog_product"> + <data key="value">1</data> + </entity> + + <entity name="UseFlatCatalogCategory" type="flat_catalog_category"> + <data key="value">1</data> + </entity> + + <entity name="DefaultFlatCatalogCategoryAndProduct" type="catalog_storefront_config"> + <requiredEntity type="flat_catalog_product">DefaultFlatCatalogProduct</requiredEntity> + <requiredEntity type="flat_catalog_category">DefaultFlatCatalogCategory</requiredEntity> + </entity> + + <entity name="DefaultFlatCatalogCategory" type="flat_catalog_category"> + <data key="value">0</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml index 5c79c321c943..27167d03d528 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml @@ -42,4 +42,72 @@ <data key="is_active">true</data> <var key="parent_id" entityType="category" entityKey="id" /> </entity> + <entity name="FirstLevelSubCat" type="category"> + <data key="name" unique="suffix">FirstLevelSubCategory</data> + <data key="name_lwr" unique="suffix">firstlevelsubcategory</data> + </entity> + <entity name="SecondLevelSubCat" type="category"> + <data key="name" unique="suffix">SecondLevelSubCategory</data> + <data key="name_lwr" unique="suffix">secondlevelsubcategory</data> + </entity> + <entity name="ThirdLevelSubCat" type="category"> + <data key="name" unique="suffix">ThirdLevelSubCategory</data> + <data key="name_lwr" unique="suffix">subcategory</data> + </entity> + <entity name="FourthLevelSubCat" type="category"> + <data key="name" unique="suffix">FourthLevelSubCategory</data> + <data key="name_lwr" unique="suffix">subcategory</data> + </entity> + <entity name="FifthLevelCat" type="category"> + <data key="name" unique="suffix">FifthLevelCategory</data> + <data key="name_lwr" unique="suffix">category</data> + </entity> + <entity name="SimpleRootSubCategory" type="category"> + <data key="name" unique="suffix">SimpleRootSubCategory</data> + <data key="name_lwr" unique="suffix">simplerootsubcategory</data> + <data key="is_active">true</data> + <data key="include_in_menu">true</data> + <data key="url_key" unique="suffix">simplerootsubcategory</data> + <var key="parent_id" entityType="category" entityKey="id" /> + </entity> + <entity name="SubCategory" type="category"> + <data key="name" unique="suffix">SubCategory</data> + <data key="name_lwr" unique="suffix">subcategory</data> + <data key="is_active">true</data> + <data key="include_in_menu">true</data> + </entity> + <entity name="Two_nested_categories" type="category"> + <data key="name" unique="suffix">SecondLevel</data> + <data key="url_key" unique="suffix">secondlevel</data> + <data key="name_lwr" unique="suffix">secondlevel</data> + <data key="is_active">true</data> + <data key="include_in_menu">true</data> + <var key="parent_id" entityType="category" entityKey="id" /> + </entity> + <entity name="Three_nested_categories" type="category"> + <data key="name" unique="suffix">ThirdLevel</data> + <data key="url_key" unique="suffix">thirdlevel</data> + <data key="name_lwr" unique="suffix">thirdlevel</data> + <data key="is_active">true</data> + <data key="include_in_menu">true</data> + <var key="parent_id" entityType="category" entityKey="id" /> + </entity> + <entity name="CatNotIncludeInMenu" type="category"> + <data key="name" unique="suffix">NotInclMenu</data> + <data key="name_lwr" unique="suffix">notinclemenu</data> + <data key="is_active">true</data> + <data key="include_in_menu">false</data> + </entity> + <entity name="CatNotActive" type="category"> + <data key="name" unique="suffix">NotActive</data> + <data key="name_lwr" unique="suffix">notactive</data> + <data key="is_active">false</data> + <data key="include_in_menu">true</data> + </entity> + <entity name="CatInactiveNotInMenu" type="category"> + <data key="name" unique="suffix">InactiveNotInMenu</data> + <data key="name_lwr" unique="suffix">inactivenotinmenu</data> + <data key="is_active">false</data> + <data key="include_in_menu">false</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ConstData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ConstData.xml index 8a26b6babdbb..d09880f14afb 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ConstData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ConstData.xml @@ -13,4 +13,8 @@ <data key="one">1</data> <data key="two">2</data> </entity> + <entity name="prodNameWithSpecChars"> + <data key="trademark">"Pursuit Lumaflex™ Tone Band"</data> + <data key="skumark">"x™"</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/FrontendLabelData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/FrontendLabelData.xml index a46d40c62c76..a2bdaa7dbc62 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/FrontendLabelData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/FrontendLabelData.xml @@ -16,4 +16,8 @@ <data key="store_id">0</data> <data key="label" unique="suffix">attributeTwo</data> </entity> + <entity name="ProductAttributeFrontendLabelThree" type="FrontendLabel"> + <data key="store_id">0</data> + <data key="label" unique="suffix">attributeThree</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ImageData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ImageData.xml new file mode 100644 index 000000000000..a2391dda5480 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ImageData.xml @@ -0,0 +1,28 @@ +<?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="placeholderBaseImage" type="imageFile"> + <data key="file">adobe-base.jpg</data> + <data key="name">adobe-base</data> + <data key="extension">jpg</data> + </entity> + + <entity name="placeholderSmallImage" type="imageFile"> + <data key="file">adobe-small.jpg</data> + <data key="name">adobe-small</data> + <data key="extension">jpg</data> + </entity> + + <entity name="placeholderThumbnailImage" type="imageFile"> + <data key="file">adobe-thumb.jpg</data> + <data key="name">adobe-thumb</data> + <data key="extension">jpg</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/NewProductData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/NewProductData.xml new file mode 100644 index 000000000000..4479805cb12f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/NewProductData.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="NewProductData" type="braintree_config_state"> + <data key="ProductName">ProductTest</data> + <data key="Price">100</data> + <data key="Quantity">100</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml index b367cdcab9d8..817dd637f81d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml @@ -52,6 +52,41 @@ <data key="used_for_sort_by">true</data> <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> </entity> + <entity name="productAttributeWithTwoOptionsNotVisible" type="ProductAttribute"> + <data key="attribute_code" unique="suffix">test_attr_</data> + <data key="frontend_input">select</data> + <data key="scope">global</data> + <data key="is_required">false</data> + <data key="is_unique">false</data> + <data key="is_searchable">false</data> + <data key="is_visible">true</data> + <data key="is_visible_in_advanced_search">false</data> + <data key="is_visible_on_front">false</data> + <data key="is_filterable">false</data> + <data key="is_filterable_in_search">false</data> + <data key="used_in_product_listing">false</data> + <data key="is_used_for_promo_rules">false</data> + <data key="is_comparable">false</data> + <data key="is_used_in_grid">false</data> + <data key="is_visible_in_grid">false</data> + <data key="is_filterable_in_grid">false</data> + <data key="used_for_sort_by">false</data> + <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> + </entity> + <entity name="hiddenDropdownAttributeWithOptions" extends="productAttributeWithTwoOptions"> + <data key="is_searchable">false</data> + <data key="is_visible_in_advanced_search">false</data> + <data key="is_visible_on_front">false</data> + <data key="is_filterable">false</data> + <data key="is_filterable_in_search">false</data> + <data key="used_in_product_listing">false</data> + <data key="is_used_for_promo_rules">false</data> + <data key="is_comparable">false</data> + <data key="is_used_in_grid">false</data> + <data key="is_visible_in_grid">false</data> + <data key="is_filterable_in_grid">false</data> + <data key="used_for_sort_by">false</data> + </entity> <entity name="productDropDownAttribute" type="ProductAttribute"> <data key="attribute_code" unique="suffix">attribute</data> <data key="frontend_input">select</data> @@ -73,6 +108,27 @@ <data key="used_for_sort_by">true</data> <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> </entity> + <entity name="productDropDownAttributeNotSearchable" type="ProductAttribute"> + <data key="attribute_code" unique="suffix">attribute</data> + <data key="frontend_input">select</data> + <data key="scope">global</data> + <data key="is_required">false</data> + <data key="is_unique">false</data> + <data key="is_searchable">false</data> + <data key="is_visible">true</data> + <data key="is_visible_in_advanced_search">true</data> + <data key="is_visible_on_front">true</data> + <data key="is_filterable">true</data> + <data key="is_filterable_in_search">true</data> + <data key="used_in_product_listing">true</data> + <data key="is_used_for_promo_rules">true</data> + <data key="is_comparable">true</data> + <data key="is_used_in_grid">true</data> + <data key="is_visible_in_grid">true</data> + <data key="is_filterable_in_grid">true</data> + <data key="used_for_sort_by">true</data> + <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> + </entity> <entity name="productAttributeWithDropdownTwoOptions" type="ProductAttribute"> <data key="attribute_code">testattribute</data> <data key="frontend_input">select</data> @@ -115,4 +171,153 @@ <data key="used_for_sort_by">true</data> <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> </entity> + <entity name="productAttributeMultiselectTwoOptionsNotSearchable" type="ProductAttribute"> + <data key="attribute_code" unique="suffix">attribute</data> + <data key="frontend_input">multiselect</data> + <data key="scope">global</data> + <data key="is_required">false</data> + <data key="is_unique">false</data> + <data key="is_searchable">false</data> + <data key="is_visible">true</data> + <data key="is_visible_in_advanced_search">true</data> + <data key="is_visible_on_front">true</data> + <data key="is_filterable">true</data> + <data key="is_filterable_in_search">true</data> + <data key="used_in_product_listing">true</data> + <data key="is_used_for_promo_rules">true</data> + <data key="is_comparable">true</data> + <data key="is_used_in_grid">true</data> + <data key="is_visible_in_grid">true</data> + <data key="is_filterable_in_grid">true</data> + <data key="used_for_sort_by">true</data> + <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> + </entity> + <entity name="newsFromDate" type="ProductAttribute"> + <data key="attribute_code">news_from_date</data> + <data key="default_frontend_label">Set Product as New from Date</data> + <data key="frontend_input">date</data> + <data key="is_required">false</data> + <data key="is_user_defined">true</data> + <data key="scope">website</data> + <data key="is_unique">false</data> + <data key="is_searchable">false</data> + <data key="is_visible">false</data> + <data key="is_visible_on_front">false</data> + <data key="is_filterable">false</data> + <data key="is_filterable_in_search">false</data> + <data key="used_in_product_listing">true</data> + <data key="is_used_for_promo_rules">false</data> + <data key="is_comparable">false</data> + <data key="is_used_in_grid">true</data> + <data key="is_filterable_in_grid">true</data> + <data key="used_for_sort_by">false</data> + <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> + </entity> + <entity name="newProductAttribute" type="ProductAttribute"> + <data key="attribute_code" unique="suffix">attribute</data> + <data key="frontend_input">text</data> + <data key="scope">global</data> + <data key="is_required">false</data> + <data key="is_unique">false</data> + <data key="is_searchable">true</data> + <data key="is_visible">true</data> + <data key="is_visible_in_advanced_search">true</data> + <data key="is_visible_on_front">true</data> + <data key="is_filterable">true</data> + <data key="is_filterable_in_search">true</data> + <data key="used_in_product_listing">true</data> + <data key="is_used_for_promo_rules">true</data> + <data key="is_comparable">true</data> + <data key="is_used_in_grid">true</data> + <data key="is_visible_in_grid">true</data> + <data key="is_filterable_in_grid">true</data> + <data key="used_for_sort_by">true</data> + <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> + </entity> + <entity name="productYesNoAttribute" type="ProductAttribute"> + <data key="attribute_code" unique="suffix">attribute</data> + <data key="frontend_input">boolean</data> + <data key="scope">global</data> + <data key="is_required">false</data> + <data key="is_unique">false</data> + <data key="is_searchable">true</data> + <data key="is_visible">true</data> + <data key="is_visible_in_advanced_search">true</data> + <data key="is_visible_on_front">true</data> + <data key="is_filterable">true</data> + <data key="is_filterable_in_search">true</data> + <data key="used_in_product_listing">true</data> + <data key="is_used_for_promo_rules">true</data> + <data key="is_comparable">true</data> + <data key="is_used_in_grid">true</data> + <data key="is_visible_in_grid">true</data> + <data key="is_filterable_in_grid">true</data> + <data key="used_for_sort_by">true</data> + <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> + </entity> + <entity name="productAttributeText" type="ProductAttribute"> + <data key="attribute_code" unique="suffix">attribute</data> + <data key="frontend_input">text</data> + <data key="scope">global</data> + <data key="is_required">false</data> + <data key="is_unique">false</data> + <data key="is_searchable">false</data> + <data key="is_visible">true</data> + <data key="backend_type">text</data> + <data key="is_wysiwyg_enabled">false</data> + <data key="is_visible_in_advanced_search">false</data> + <data key="is_visible_on_front">true</data> + <data key="is_filterable">false</data> + <data key="is_filterable_in_search">false</data> + <data key="used_in_product_listing">false</data> + <data key="is_used_for_promo_rules">false</data> + <data key="is_comparable">true</data> + <data key="is_used_in_grid">false</data> + <data key="is_visible_in_grid">false</data> + <data key="is_filterable_in_grid">false</data> + <data key="used_for_sort_by">false</data> + <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> + </entity> + <entity name="textProductAttribute" extends="productAttributeWysiwyg" type="ProductAttribute"> + <data key="frontend_input">text</data> + <data key="default_value" unique="suffix">defaultValue</data> + <data key="is_required_admin">No</data> + </entity> + <entity name="dateProductAttribute" extends="productAttributeWysiwyg" type="ProductAttribute"> + <data key="frontend_input">date</data> + <data key="is_required_admin">No</data> + </entity> + <entity name="priceProductAttribute" extends="productAttributeWysiwyg" type="ProductAttribute"> + <data key="frontend_input">date</data> + <data key="is_required_admin">No</data> + </entity> + <entity name="dropdownProductAttribute" extends="productAttributeWysiwyg" type="ProductAttribute"> + <data key="frontend_input">select</data> + <data key="frontend_input_admin">Dropdown</data> + <data key="is_required_admin">No</data> + <data key="option1_admin" unique="suffix">opt1Admin</data> + <data key="option1_frontend" unique="suffix">opt1Front</data> + <data key="option2_admin" unique="suffix">opt2Admin</data> + <data key="option2_frontend" unique="suffix">opt2Front</data> + <data key="option3_admin" unique="suffix">opt3Admin</data> + <data key="option3_frontend" unique="suffix">opt3Front</data> + </entity> + <entity name="multiselectProductAttribute" extends="productAttributeWysiwyg" type="ProductAttribute"> + <data key="frontend_input">multiselect</data> + <data key="frontend_input_admin">Multiple Select</data> + <data key="is_required_admin">No</data> + <data key="option1_admin" unique="suffix">opt1Admin</data> + <data key="option1_frontend" unique="suffix">opt1Front</data> + <data key="option2_admin" unique="suffix">opt2Admin</data> + <data key="option2_frontend" unique="suffix">opt2Front</data> + <data key="option3_admin" unique="suffix">opt3Admin</data> + <data key="option3_frontend" unique="suffix">opt3Front</data> + </entity> + <entity name="dropdownProductAttributeWithQuote" extends="productAttributeWysiwyg" type="ProductAttribute"> + <data key="frontend_input">select</data> + <data key="frontend_input_admin">Dropdown</data> + <data key="is_required_admin">No</data> + <data key="option1_admin" unique="suffix">opt1'Admin</data> + <data key="option1_frontend" unique="suffix">opt1'Front</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml index c575f1a5db82..fcb56cf298a9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml @@ -65,4 +65,25 @@ <data key="is_default">false</data> <data key="sort_order">0</data> </entity> + <entity name="ProductAttributeOption7" type="ProductAttributeOption"> + <var key="attribute_code" entityKey="attribute_code" entityType="ProductAttribute"/> + <data key="label" unique="suffix">Green</data> + <data key="is_default">false</data> + <data key="sort_order">3</data> + <requiredEntity type="StoreLabel">Option7Store0</requiredEntity> + <requiredEntity type="StoreLabel">Option8Store1</requiredEntity> + </entity> + <entity name="ProductAttributeOption8" type="ProductAttributeOption"> + <var key="attribute_code" entityKey="attribute_code" entityType="ProductAttribute"/> + <data key="label" unique="suffix">Red</data> + <data key="is_default">false</data> + <data key="sort_order">3</data> + <requiredEntity type="StoreLabel">Option9Store0</requiredEntity> + <requiredEntity type="StoreLabel">Option10Store1</requiredEntity> + </entity> + <entity name="ProductAttributeOption8" type="ProductAttributeOption"> + <var key="attribute_code" entityKey="attribute_code" entityType="ProductAttribute"/> + <data key="label" unique="suffix">White</data> + <data key="value" unique="suffix">white</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeSetData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeSetData.xml index 68f51559a9f3..6d4314a6d865 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeSetData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeSetData.xml @@ -14,4 +14,16 @@ <data key="attributeGroupId">7</data> <data key="sortOrder">0</data> </entity> + <entity name="AddToDefaultSetSortOrder1" type="ProductAttributeSet"> + <var key="attributeCode" entityKey="attribute_code" entityType="ProductAttribute"/> + <data key="attributeSetId">4</data> + <data key="attributeGroupId">7</data> + <data key="sortOrder">1</data> + </entity> + <entity name="AddToSetBlank" type="ProductAttributeSet"> + <var key="attributeCode" entityKey="attribute_code" entityType="ProductAttribute"/> + <data key="attributeSetId">0</data> + <data key="attributeGroupId">0</data> + <data key="sortOrder">0</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml index 9ae551b69d38..b8d6ec8e63e7 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml @@ -18,6 +18,7 @@ <data key="urlKey" unique="suffix">testurlkey</data> <data key="status">1</data> <data key="quantity">100</data> + <data key="weight">1</data> <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> </entity> @@ -34,6 +35,13 @@ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> </entity> + <entity name="ApiSimpleProductWithSpecCharInName" type="product" extends="ApiSimpleProduct"> + <data key="name">Pursuit Lumaflex&trade; Tone Band</data> + <data key="sku" unique="suffix">x&trade;</data> + </entity> + <entity name="ApiSimpleProductWithCustomPrice" type="product" extends="ApiSimpleProduct"> + <data key="price">100</data> + </entity> <entity name="ApiSimpleProductUpdateDescription" type="product2"> <requiredEntity type="custom_attribute">ApiProductDescription</requiredEntity> <requiredEntity type="custom_attribute">ApiProductShortDescription</requiredEntity> @@ -56,6 +64,48 @@ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> </entity> + <entity name="SimpleProductAfterImport1" type="product"> + <data key="sku">SimpleProductForTest1</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="name">SimpleProductAfterImport1</data> + <data key="price">250.00</data> + <data key="visibility">4</data> + <data key="status">1</data> + <data key="quantity">100</data> + <data key="urlKey">simple-product-for-test-1</data> + <data key="weight">1</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> + </entity> + <entity name="SimpleProductAfterImport2" type="product"> + <data key="sku">SimpleProductForTest2</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="name">SimpleProductAfterImport2</data> + <data key="price">300.00</data> + <data key="visibility">4</data> + <data key="status">1</data> + <data key="quantity">100</data> + <data key="urlKey">simple-product-for-test-2</data> + <data key="weight">1</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> + </entity> + <entity name="SimpleProductAfterImport3" type="product"> + <data key="sku">SimpleProductForTest3</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="name">SimpleProductAfterImport3</data> + <data key="price">350.00</data> + <data key="visibility">4</data> + <data key="status">1</data> + <data key="quantity">100</data> + <data key="urlKey">simple-product-for-test-3</data> + <data key="weight">1</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> + </entity> <entity name="SimpleProduct2" type="product"> <data key="sku" unique="suffix">SimpleProduct</data> <data key="type_id">simple</data> @@ -92,6 +142,31 @@ <data key="quantity">0</data> <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> </entity> + <entity name="SimpleOutOfStockProduct" type="product"> + <data key="sku" unique="suffix">testSku</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">OutOfStockProduct</data> + <data key="price">123.00</data> + <data key="urlKey" unique="suffix">testurlkey</data> + <data key="status">1</data> + <data key="quantity">0</data> + </entity> + <!-- Simple Product Disabled --> + <entity name="SimpleProductOffline" type="product2"> + <data key="sku" unique="suffix">testSku</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">SimpleOffline</data> + <data key="price">123.00</data> + <data key="status">2</data> + <data key="quantity">100</data> + <data key="urlKey" unique="suffix">testurlkey</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute">CustomAttributeProductAttribute</requiredEntity> + </entity> <entity name="NewSimpleProduct" type="product"> <data key="price">321.00</data> </entity> @@ -106,6 +181,18 @@ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> <requiredEntity type="custom_attribute">CustomAttributeProductAttribute</requiredEntity> </entity> + <entity name="ApiSimpleOutOfStock" type="product2"> + <data key="sku" unique="suffix">api-simple-product</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">Api Simple Out Of Stock Product</data> + <data key="price">123.00</data> + <data key="urlKey" unique="suffix">api-simple-product</data> + <data key="status">1</data> + <data key="quantity">100</data> + <requiredEntity type="custom_attribute">CustomAttributeProductAttribute</requiredEntity> + </entity> <entity name="ApiSimpleOne" type="product2"> <data key="sku" unique="suffix">api-simple-product</data> <data key="type_id">simple</data> @@ -145,6 +232,15 @@ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> <requiredEntity type="custom_attribute">CustomAttributeProductAttribute</requiredEntity> </entity> + <entity name="ApiSimpleProductWithPrice50" type="product2" extends="ApiSimpleOne"> + <data key="price">50</data> + </entity> + <entity name="ApiSimpleProductWithPrice60" type="product2" extends="ApiSimpleTwo"> + <data key="price">60</data> + </entity> + <entity name="ApiSimpleProductWithPrice70" type="product2" extends="SimpleOne"> + <data key="price">70</data> + </entity> <entity name="ApiSimpleTwoHidden" type="product2"> <data key="sku" unique="suffix">api-simple-product-two</data> <data key="type_id">simple</data> @@ -180,6 +276,9 @@ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> <requiredEntity type="custom_attribute">CustomAttributeProductUrlKey</requiredEntity> </entity> + <entity name="SetProductVisibilityHidden" type="product2"> + <data key="visibility">1</data> + </entity> <entity name="ProductImage" type="uploadImage"> <data key="title" unique="suffix">Image1</data> <data key="price">1.00</data> @@ -206,6 +305,15 @@ <data key="filename">magento-again</data> <data key="file_extension">jpg</data> </entity> + <entity name="TestImageAdobe" type="image"> + <data key="title" unique="suffix">magento-adobe</data> + <data key="price">1.00</data> + <data key="file_type">Upload File</data> + <data key="shareable">Yes</data> + <data key="file">adobe-base.jpg</data> + <data key="filename">adobe-base</data> + <data key="file_extension">jpg</data> + </entity> <entity name="ProductWithUnicode" type="product"> <data key="sku" unique="suffix">霁产品</data> <data key="type_id">simple</data> @@ -256,7 +364,7 @@ <data key="status">1</data> <data key="quantity">100</data> <data key="weight">0</data> - <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> </entity> <entity name="productWithDescription" type="product"> @@ -301,6 +409,20 @@ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> </entity> + <entity name="SimpleProductWithCustomAttributeSet" type="product"> + <data key="sku" unique="suffix">testSku</data> + <data key="type_id">simple</data> + <var key="attribute_set_id" entityKey="attribute_set_id" entityType="CatalogAttributeSet"/> + <data key="visibility">4</data> + <data key="name" unique="suffix">testProductName</data> + <data key="price">123.00</data> + <data key="urlKey" unique="suffix">testurlkey</data> + <data key="status">1</data> + <data key="weight">1</data> + <data key="quantity">100</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> + </entity> <entity name="productWithOptions" type="product"> <var key="sku" entityType="product" entityKey="sku" /> <data key="file">magento.jpg</data> @@ -319,6 +441,11 @@ <var key="sku" entityType="product" entityKey="sku" /> <requiredEntity type="product_option">ProductOptionDropDownWithLongValuesTitle</requiredEntity> </entity> + <entity name="ProductWithTextFieldAndAreaOptions" type="product"> + <var key="sku" entityType="product" entityKey="sku" /> + <requiredEntity type="product_option">ProductOptionField</requiredEntity> + <requiredEntity type="product_option">ProductOptionArea</requiredEntity> + </entity> <entity name="ApiVirtualProductWithDescription" type="product"> <data key="sku" unique="suffix">api-virtual-product</data> <data key="type_id">virtual</data> @@ -419,4 +546,453 @@ <data key="status">1</data> <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> </entity> + <entity name="simpleProductForMassUpdate" 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">massUpdateProductName</data> + <data key="keyword">massUpdateProductName</data> + <data key="price">123.00</data> + <data key="urlKey" unique="suffix">masstesturlkey</data> + <data key="status">1</data> + <data key="quantity">100</data> + <data key="weight">1</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> + </entity> + <entity name="simpleProductForMassUpdate2" 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">massUpdateProductName</data> + <data key="keyword">massUpdateProductName</data> + <data key="price">123.00</data> + <data key="urlKey" unique="suffix">masstesturlkey</data> + <data key="status">1</data> + <data key="quantity">100</data> + <data key="weight">1</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> + </entity> + <entity name="ApiSimpleSingleQty" 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 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">1</data> + <requiredEntity type="product_extension_attribute">EavStock1</requiredEntity> + <requiredEntity type="custom_attribute">CustomAttributeProductAttribute</requiredEntity> + </entity> + <entity name="virtualProductWithRequiredFields" type="product"> + <data key="name" unique="suffix">virtualProduct</data> + <data key="sku" unique="suffix">virtualsku</data> + <data key="price">10</data> + <data key="visibility">Catalog, Search</data> + <data key="urlKey" unique="suffix">virtualproduct</data> + <data key="type_id">virtual</data> + </entity> + <entity name="virtualProductBigQty" type="product"> + <data key="name" unique="suffix">VirtualProduct</data> + <data key="sku" unique="suffix">virtual_sku</data> + <data key="price">100.00</data> + <data key="productTaxClass">None</data> + <data key="quantity">999</data> + <data key="status">In Stock</data> + <data key="visibility">Catalog, Search</data> + <data key="urlKey" unique="suffix">virtual-product</data> + <data key="type_id">virtual</data> + </entity> + <entity name="virtualProductGeneralGroup" type="product"> + <data key="name" unique="suffix">VirtualProduct</data> + <data key="sku" unique="suffix">virtual_sku</data> + <data key="price">100.00</data> + <data key="productTaxClass">None</data> + <data key="quantity">999</data> + <data key="status">In Stock</data> + <data key="visibility">Catalog, Search</data> + <data key="urlKey" unique="suffix">virtual-product</data> + <data key="type_id">virtual</data> + </entity> + <entity name="virtualProductCustomImportOptions" type="product"> + <data key="name" unique="suffix">VirtualProduct</data> + <data key="sku" unique="suffix">virtual_sku</data> + <data key="price">9,000.00</data> + <data key="quantity">999</data> + <data key="status">In Stock</data> + <data key="visibility">Catalog, Search</data> + <data key="urlKey" unique="suffix">virtual-product</data> + <data key="storefrontStatus">IN STOCK</data> + <data key="type_id">virtual</data> + </entity> + <entity name="virtualProductWithoutManageStock" type="product"> + <data key="name" unique="suffix">VirtualProduct</data> + <data key="sku" unique="suffix">virtual_sku</data> + <data key="price">100.00</data> + <data key="quantity">999</data> + <data key="urlKey" unique="suffix">virtual-product</data> + <data key="special_price">90.00</data> + <data key="storefrontStatus">IN STOCK</data> + <data key="type_id">virtual</data> + </entity> + <entity name="virtualProductOutOfStock" type="product"> + <data key="name" unique="suffix">VirtualProduct</data> + <data key="sku" unique="suffix">virtual_sku</data> + <data key="price">9,000.00</data> + <data key="quantity">999</data> + <data key="status">Out of Stock</data> + <data key="visibility">Catalog, Search</data> + <data key="urlKey" unique="suffix">virtual-product</data> + <data key="storefrontStatus">OUT OF STOCK</data> + <data key="type_id">virtual</data> + </entity> + <entity name="virtualProductAssignToCategory" type="product"> + <data key="name" unique="suffix">VirtualProduct</data> + <data key="sku" unique="suffix">virtual_sku</data> + <data key="price">10.00</data> + <data key="quantity">999</data> + <data key="urlKey" unique="suffix">virtual-product</data> + <data key="type_id">virtual</data> + </entity> + <entity name="updateVirtualProductRegularPriceInStock" type="product"> + <data key="name" unique="suffix">VirtualProduct</data> + <data key="sku" unique="suffix">virtual_sku</data> + <data key="price">120.00</data> + <data key="productTaxClass">None</data> + <data key="quantity">999</data> + <data key="status">In Stock</data> + <data key="storefrontStatus">IN STOCK</data> + <data key="visibility">Search</data> + <data key="urlKey" unique="suffix">virtual-product</data> + <data key="type_id">virtual</data> + </entity> + <entity name="updateVirtualProductWithTierPriceInStock" type="product"> + <data key="name" unique="suffix">VirtualProduct</data> + <data key="sku" unique="suffix">virtual_sku</data> + <data key="price">99.99</data> + <data key="productTaxClass">None</data> + <data key="quantity">999</data> + <data key="status">In Stock</data> + <data key="storefrontStatus">IN STOCK</data> + <data key="visibility">Catalog</data> + <data key="urlKey" unique="suffix">virtual-product</data> + <data key="type_id">virtual</data> + </entity> + <entity name="updateVirtualProductRegularPrice99OutOfStock" type="product"> + <data key="name" unique="suffix">VirtualProduct</data> + <data key="sku" unique="suffix">virtual_sku</data> + <data key="price">99.99</data> + <data key="productTaxClass">Taxable Goods</data> + <data key="status">Out of Stock</data> + <data key="storefrontStatus">OUT OF STOCK</data> + <data key="visibility">Search</data> + <data key="urlKey" unique="suffix">virtual-product</data> + <data key="type_id">virtual</data> + </entity> + <entity name="defaultSimpleProduct" type="product"> + <data key="name" unique="suffix">Testp</data> + <data key="sku" unique="suffix">testsku</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="price">560.00</data> + <data key="urlKey" unique="suffix">testurl-</data> + <data key="status">1</data> + <data key="quantity">25</data> + <data key="weight">1</data> + <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> + </entity> + <entity name="ProductWithLongNameSku" extends="ApiSimpleProduct"> + <data key="name" unique="suffix">Product With Long Name And Sku - But not too long</data> + <data key="sku" unique="suffix">Product With Long Name And Sku - But not too long</data> + </entity> + <entity name="PaginationProduct" type="product"> + <data key="name" unique="suffix">pagi</data> + <data key="sku" unique="suffix">pagisku</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="price">780.00</data> + <data key="urlKey" unique="suffix">pagiurl-</data> + <data key="status">1</data> + <data key="quantity">50</data> + <data key="weight">5</data> + <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> + </entity> + <entity name="Magento3" type="image"> + <data key="title" unique="suffix">Magento3</data> + <data key="price">1.00</data> + <data key="file_type">Upload File</data> + <data key="shareable">Yes</data> + <data key="file">magento3.jpg</data> + <data key="filename">magento3</data> + <data key="file_extension">jpg</data> + </entity> + <entity name="updateVirtualProductRegularPrice" type="product"> + <data key="name" unique="suffix">VirtualProduct</data> + <data key="sku" unique="suffix">virtual_sku</data> + <data key="price">99.99</data> + <data key="productTaxClass">None</data> + <data key="quantity">999</data> + <data key="status">In Stock</data> + <data key="storefrontStatus">IN STOCK</data> + <data key="visibility">Catalog</data> + <data key="urlKey" unique="suffix">virtual-product</data> + <data key="type_id">virtual</data> + </entity> + <entity name="updateVirtualProductRegularPrice5OutOfStock" type="product"> + <data key="name" unique="suffix">VirtualProduct</data> + <data key="sku" unique="suffix">virtual_sku</data> + <data key="price">5.00</data> + <data key="productTaxClass">None</data> + <data key="status">Out of Stock</data> + <data key="storefrontStatus">OUT OF STOCK</data> + <data key="visibility">Catalog</data> + <data key="urlKey" unique="suffix">virtual-product</data> + <data key="type_id">virtual</data> + </entity> + <entity name="updateVirtualProductSpecialPrice" type="product"> + <data key="name" unique="suffix">VirtualProduct</data> + <data key="sku" unique="suffix">virtual_sku</data> + <data key="price">120.00</data> + <data key="productTaxClass">Taxable Goods</data> + <data key="quantity">999</data> + <data key="status">In Stock</data> + <data key="storefrontStatus">IN STOCK</data> + <data key="visibility">Catalog, Search</data> + <data key="urlKey" unique="suffix">virtual-product</data> + <data key="special_price">45.00</data> + <data key="type_id">virtual</data> + </entity> + <entity name="updateVirtualProductSpecialPriceOutOfStock" type="product"> + <data key="name" unique="suffix">VirtualProduct</data> + <data key="sku" unique="suffix">virtual_sku</data> + <data key="price">99.99</data> + <data key="productTaxClass">None</data> + <data key="status">Out of Stock</data> + <data key="storefrontStatus">OUT OF STOCK</data> + <data key="visibility">Catalog, Search</data> + <data key="urlKey" unique="suffix">virtual-product</data> + <data key="special_price">45.00</data> + <data key="type_id">virtual</data> + </entity> + <entity name="updateVirtualProductTierPriceInStock" type="product"> + <data key="name" unique="suffix">VirtualProduct</data> + <data key="sku" unique="suffix">virtual_sku</data> + <data key="price">145.00</data> + <data key="productTaxClass">Taxable Goods</data> + <data key="quantity">999</data> + <data key="status">In Stock</data> + <data key="storefrontStatus">IN STOCK</data> + <data key="visibility">Catalog, Search</data> + <data key="urlKey" unique="suffix">virtual-product</data> + <data key="type_id">virtual</data> + </entity> + <entity name="updateVirtualTierPriceOutOfStock" type="product"> + <data key="name" unique="suffix">VirtualProduct</data> + <data key="sku" unique="suffix">virtual_sku</data> + <data key="price">185.00</data> + <data key="productTaxClass">None</data> + <data key="quantity">999</data> + <data key="status">Out of Stock</data> + <data key="storefrontStatus">OUT OF STOCK</data> + <data key="visibility">Catalog, Search</data> + <data key="urlKey" unique="suffix">virtual-product</data> + <data key="type_id">virtual</data> + </entity> + <entity name="simpleProductRegularPrice325InStock" type="product"> + <data key="urlKey" unique="suffix">test-simple-product</data> + <data key="name" unique="suffix">TestSimpleProduct</data> + <data key="sku" unique="suffix">test_simple_product_sku</data> + <data key="price">325.02</data> + <data key="quantity">89</data> + <data key="status">In Stock</data> + <data key="storefrontStatus">IN STOCK</data> + <data key="weight">89.0000</data> + <data key="visibility">Search</data> + <data key="type_id">simple</data> + <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> + </entity> + <entity name="simpleProductRegularPrice32503OutOfStock" type="product"> + <data key="urlKey" unique="suffix">test-simple-product</data> + <data key="name" unique="suffix">TestSimpleProduct</data> + <data key="sku" unique="suffix">test_simple_product_sku</data> + <data key="price">325.03</data> + <data key="quantity">25</data> + <data key="status">Out of Stock</data> + <data key="storefrontStatus">OUT OF STOCK</data> + <data key="weight">125.0000</data> + <data key="type_id">simple</data> + <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> + </entity> + <entity name="simpleProductRegularPrice245InStock" type="product"> + <data key="urlKey" unique="suffix">test-simple-product</data> + <data key="name" unique="suffix">TestSimpleProduct</data> + <data key="sku" unique="suffix">test_simple_product_sku</data> + <data key="price">245.00</data> + <data key="quantity">200</data> + <data key="status">In Stock</data> + <data key="storefrontStatus">IN STOCK</data> + <data key="weight">120.0000</data> + <data key="visibility">Catalog, Search</data> + <data key="type_id">simple</data> + <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> + </entity> + <entity name="simpleProductRegularPrice32501InStock" type="product"> + <data key="urlKey" unique="suffix">test-simple-product</data> + <data key="name" unique="suffix">TestSimpleProduct</data> + <data key="sku" unique="suffix">test_simple_product_sku</data> + <data key="price">325.01</data> + <data key="quantity">125</data> + <data key="status">In Stock</data> + <data key="storefrontStatus">IN STOCK</data> + <data key="weight">25.0000</data> + <data key="visibility">Catalog</data> + <data key="type_id">simple</data> + <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> + </entity> + <entity name="simpleProductTierPrice300InStock" type="product"> + <data key="urlKey" unique="suffix">test-simple-product</data> + <data key="name" unique="suffix">TestSimpleProduct</data> + <data key="sku" unique="suffix">test_simple_product_sku</data> + <data key="price">300.00</data> + <data key="quantity">34</data> + <data key="status">In Stock</data> + <data key="storefrontStatus">IN STOCK</data> + <data key="weight">1</data> + <data key="weightSelect">This item has weight</data> + <data key="type_id">simple</data> + <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> + </entity> + <entity name="simpleProductEnabledFlat" type="product"> + <data key="urlKey" unique="suffix">test-simple-product</data> + <data key="name" unique="suffix">TestSimpleProduct</data> + <data key="sku" unique="suffix">test_simple_product_sku</data> + <data key="price">1.99</data> + <data key="productTaxClass">Taxable Goods</data> + <data key="quantity">1000</data> + <data key="status">1</data> + <data key="storefrontStatus">IN STOCK</data> + <data key="weight">1</data> + <data key="weightSelect">This item has weight</data> + <data key="visibility">Catalog, Search</data> + <data key="type_id">simple</data> + <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> + </entity> + <entity name="simpleProductRegularPriceCustomOptions" type="product"> + <data key="urlKey" unique="suffix">test-simple-product</data> + <data key="name" unique="suffix">TestSimpleProduct</data> + <data key="sku" unique="suffix">test_simple_product_sku</data> + <data key="price">245.00</data> + <data key="storefront_new_cartprice">343.00</data> + <data key="quantity">200</data> + <data key="status">In Stock</data> + <data key="storefrontStatus">IN STOCK</data> + <data key="weight">120.0000</data> + <data key="type_id">simple</data> + <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> + </entity> + <entity name="simpleProductDisabled" type="product"> + <data key="urlKey" unique="suffix">test-simple-product</data> + <data key="name" unique="suffix">TestSimpleProduct</data> + <data key="sku" unique="suffix">test_simple_product_sku</data> + <data key="price">74.00</data> + <data key="quantity">87</data> + <data key="status">In Stock</data> + <data key="weight">333.0000</data> + <data key="type_id">simple</data> + <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> + </entity> + <entity name="simpleProductNotVisibleIndividually" type="product"> + <data key="urlKey" unique="suffix">test-simple-product</data> + <data key="name" unique="suffix">TestSimpleProduct</data> + <data key="sku" unique="suffix">test_simple_product_sku</data> + <data key="price">325.00</data> + <data key="quantity">123</data> + <data key="status">In Stock</data> + <data key="weight">129.0000</data> + <data key="visibility">Not Visible Individually</data> + <data key="type_id">simple</data> + <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> + </entity> + <entity name="simpleProductDataOverriding" type="product"> + <data key="urlKey" unique="suffix">test-simple-product</data> + <data key="name" unique="suffix">TestSimpleProduct</data> + <data key="sku" unique="suffix">test_simple_product_sku</data> + <data key="price">9.99</data> + <data key="type_id">simple</data> + <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> + </entity> + <entity name="nameAndAttributeSkuMaskSimpleProduct" type="product"> + <data key="urlKey" unique="suffix">simple-product</data> + <data key="name" unique="suffix">SimpleProduct</data> + <data key="price">10000.00</data> + <data key="quantity">657</data> + <data key="weight">50</data> + <data key="country_of_manufacture">UA</data> + <data key="country_of_manufacture_label">Ukraine</data> + <data key="type_id">simple</data> + <data key="status">1</data> + <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> + </entity> + <entity name="ProductShortDescription" type="ProductAttribute"> + <data key="attribute_code">short_description</data> + </entity> + <entity name="AddToDefaultSetTopOfContentSection" type="ProductAttributeSet"> + <var key="attributeCode" entityKey="attribute_code" entityType="ProductAttribute"/> + <data key="attributeSetId">4</data> + <data key="attributeGroupId">13</data> + <data key="sortOrder">0</data> + </entity> + <entity name="productAlphabeticalA" type="product" extends="_defaultProduct"> + <data key="name" unique="suffix">AAA Product</data> + </entity> + <entity name="productAlphabeticalB" type="product" extends="_defaultProduct"> + <data key="name" unique="suffix">BBB Product</data> + </entity> + <entity name="productWithSpecialCharacters" type="product" extends="_defaultProduct"> + <data key="name" unique="suffix">Product "!@#$%^&*()+:;\|}{][?=~` </data> + <data key="nameWithSafeChars" unique="suffix">|}{][?=~` </data> + </entity> + <entity name="productWith130CharName" type="product" extends="_defaultProduct"> + <data key="name" unique="suffix">ProductWith128Chars 1234567891123456789112345678911234567891123456789112345678911234567891123456789112345678 endnums</data> + </entity> + <entity name="simpleProductDefault" type="product"> + <data key="sku" unique="suffix">sku_simple_product_</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">Simple Product </data> + <data key="price">560</data> + <data key="urlKey" unique="suffix">simple-product-</data> + <data key="status">1</data> + <data key="quantity">25</data> + <data key="weight">1</data> + <data key="product_has_weight">1</data> + <data key="is_in_stock">1</data> + <data key="tax_class_id">2</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> + </entity> + <entity name="simpleProductWithoutCategory" type="product"> + <data key="sku" unique="suffix">sku_simple_product_</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">SimpleProduct</data> + <data key="price">560</data> + <data key="urlKey" unique="suffix">simple-product-</data> + <data key="status">1</data> + <data key="quantity">25</data> + <data key="weight">1</data> + <data key="product_has_weight">1</data> + <data key="is_in_stock">1</data> + <data key="tax_class_id">2</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductExtensionAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductExtensionAttributeData.xml index 6e532637fb6d..e9e9e4375236 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductExtensionAttributeData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductExtensionAttributeData.xml @@ -17,4 +17,7 @@ <entity name="EavStock10" type="product_extension_attribute"> <requiredEntity type="stock_item">Qty_10</requiredEntity> </entity> + <entity name="EavStock1" type="product_extension_attribute"> + <requiredEntity type="stock_item">Qty_1</requiredEntity> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductGridData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductGridData.xml index ea0bcafe56c4..71c8af318e9b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductGridData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductGridData.xml @@ -12,4 +12,7 @@ <data key="from">10</data> <data key="to">100</data> </entity> + <entity name="ProductPerPage"> + <data key="productCount">1</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductLinkData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductLinkData.xml new file mode 100644 index 000000000000..000bb2095002 --- /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 000000000000..bd4f807880ab --- /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 000000000000..157a4d410263 --- /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/StockItemData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/StockItemData.xml index 39ecc2d440fc..7cba4c3c76fe 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/StockItemData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/StockItemData.xml @@ -28,4 +28,8 @@ <data key="qty">101</data> <data key="is_in_stock">true</data> </entity> + <entity name="Qty_1" type="stock_item"> + <data key="qty">1</data> + <data key="is_in_stock">true</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/StoreLabelData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/StoreLabelData.xml index ce964e2d7150..0e51995ac72e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/StoreLabelData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/StoreLabelData.xml @@ -56,4 +56,20 @@ <data key="store_id">1</data> <data key="label">option6</data> </entity> + <entity name="Option7Store0" type="StoreLabel"> + <data key="store_id">0</data> + <data key="label">Green</data> + </entity> + <entity name="Option8Store1" type="StoreLabel"> + <data key="store_id">1</data> + <data key="label">Green</data> + </entity> + <entity name="Option9Store0" type="StoreLabel"> + <data key="store_id">0</data> + <data key="label">Red</data> + </entity> + <entity name="Option10Store1" type="StoreLabel"> + <data key="store_id">1</data> + <data key="label">Red</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml new file mode 100644 index 000000000000..e5070340421a --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml @@ -0,0 +1,59 @@ +<?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="testDataTierPrice" type="data"> + <data key="goldenPrice1">$676.50</data> + <data key="goldenPrice2">$615.00</data> + </entity> + <entity name="customStoreTierPrice" type="data"> + <data key="name">secondStore</data> + <data key="code">second_store</data> + </entity> + <entity name="customStoreView" type="data"> + <data key="name">secondStoreView</data> + <data key="code">second_store_view</data> + </entity> + <entity name="tierPriceOnVirtualProduct" type="data"> + <data key="website">All Websites [USD]</data> + <data key="customer_group">ALL GROUPS</data> + <data key="price">90.00</data> + <data key="qty">2</data> + </entity> + <entity name="tierPriceOnGeneralGroup" type="data"> + <data key="website">All Websites [USD]</data> + <data key="customer_group">General</data> + <data key="price">80.00</data> + <data key="qty">2</data> + </entity> + <entity name="tierPriceOnDefault" type="data"> + <data key="website_0">All Websites [USD]</data> + <data key="customer_group_0">ALL GROUPS</data> + <data key="price_0">15.00</data> + <data key="qty_0">3</data> + <data key="website_1">All Websites [USD]</data> + <data key="customer_group_1">ALL GROUPS</data> + <data key="price_1">24.00</data> + <data key="qty_1">15</data> + </entity> + <entity name="tierPriceHighCostSimpleProduct" type="data"> + <data key="website">All Websites [USD]</data> + <data key="customer_group">ALL GROUPS</data> + <data key="price">500,000.00</data> + <data key="qty">1</data> + </entity> + <entity name="tierProductPrice" type="catalogTierPrice"> + <data key="price">90.00</data> + <data key="price_type">fixed</data> + <data key="website_id">0</data> + <data key="customer_group">ALL GROUPS</data> + <data key="quantity">2</data> + <var key="sku" entityType="product2" entityKey="sku" /> + </entity> +</entities> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/VirtualProductOptionData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/VirtualProductOptionData.xml new file mode 100644 index 000000000000..fe1d49e4daad --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/VirtualProductOptionData.xml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="virtualProductCustomizableOption1"> + <data key="title" unique="suffix">Test1 option </data> + <data key="is_required">1</data> + <data key="type">Field</data> + <data key="option_0_price">120.03</data> + <data key="option_0_price_type">Fixed</data> + <data key="option_0_sku" unique="suffix">sku1_</data> + <data key="option_0_max_characters">45</data> + </entity> + <entity name="virtualProductCustomizableOption2"> + <data key="title" unique="suffix">Test2 option </data> + <data key="is_required">1</data> + <data key="type">Field</data> + <data key="option_0_price">120.03</data> + <data key="option_0_price_type">Fixed</data> + <data key="option_0_sku" unique="suffix">sku2_</data> + <data key="option_0_max_characters">45</data> + </entity> + <entity name="virtualProductCustomizableOption3"> + <data key="title" unique="suffix">Test3 option </data> + <data key="is_required">1</data> + <data key="type">Drop-down</data> + <data key="option_0_title" unique="suffix">Test3-1 </data> + <data key="option_0_price">110.01</data> + <data key="option_0_expected_price">9,900.90</data> + <data key="option_0_price_type">Percent</data> + <data key="option_0_sku" unique="suffix">sku3-1_</data> + <data key="option_0_sort_order">0</data> + <data key="option_1_title" unique="suffix">Test3-2 </data> + <data key="option_1_price">210.02</data> + <data key="option_1_price_type">Fixed</data> + <data key="option_1_sku" unique="suffix">sku3-2_</data> + <data key="option_1_sort_order">1</data> + </entity> + <entity name="virtualProductCustomizableOption4"> + <data key="title" unique="suffix">Test4 option </data> + <data key="is_required">1</data> + <data key="type">Drop-down</data> + <data key="option_0_title" unique="suffix">Test4-1 </data> + <data key="option_0_price">10.01</data> + <data key="option_0_price_type">Percent</data> + <data key="option_0_sku" unique="suffix">sku4-1_</data> + <data key="option_0_sort_order">0</data> + <data key="option_1_title" unique="suffix">Test4-2 </data> + <data key="option_1_price">20.02</data> + <data key="option_1_price_type">Fixed</data> + <data key="option_1_sku" unique="suffix">sku4-2_</data> + <data key="option_1_sort_order">1</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/WidgetsData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/WidgetsData.xml new file mode 100644 index 000000000000..18564ff101fd --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/WidgetsData.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="ProductLinkWidget" extends="ProductsListWidget"> + <data key="type">Catalog Product Link</data> + <data key="template">Product Link Block Template</data> + </entity> + <entity name="RecentlyComparedProductsWidget" type="widget"> + <data key="type">Recently Compared Products</data> + <data key="design_theme">Magento Luma</data> + <data key="name" unique="suffix">Recently Compared Products</data> + <array key="store_ids"> + <item>All Store Views</item> + </array> + <data key="display_on">All Pages</data> + <data key="container">Sidebar Additional</data> + </entity> + <entity name="RecentlyViewedProductsWidget" type="widget"> + <data key="type">Recently Viewed Products</data> + <data key="design_theme">Magento Luma</data> + <data key="name" unique="suffix">Recently Viewed Products</data> + <array key="store_ids"> + <item>All Store Views</item> + </array> + <data key="display_on">All Pages</data> + <data key="container">Sidebar Additional</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_attribute_set-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_attribute_set-meta.xml new file mode 100644 index 000000000000..9ef7b507812a --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_attribute_set-meta.xml @@ -0,0 +1,25 @@ +<?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="AddCatalogAttributeToAttributeSet" dataType="CatalogAttributeSet" type="create" auth="adminOauth" url="/V1/products/attribute-sets" method="POST"> + <contentType>application/json</contentType> + <object key="attributeSet" dataType="CatalogAttributeSet"> + <field key="attribute_set_name">string</field> + <field key="sort_order">integer</field> + </object> + <field key="skeletonId">integer</field> + </operation> + <operation name="DeleteCatalogAttributeFromAttributeSet" dataType="CatalogAttributeSet" type="delete" auth="adminOauth" url="/V1/products/attribute-sets/{attribute_set_id}" method="DELETE"> + <contentType>application/json</contentType> + </operation> + <operation name="GetCatalogAttributesFromDefaultSet" dataType="CatalogAttributeSet" type="get" auth="adminOauth" url="/V1/products/attribute-sets/{attribute_set_id}" method="GET"> + <contentType>application/json</contentType> + </operation> +</operations> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_configuration-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_configuration-meta.xml new file mode 100644 index 000000000000..b1f2b43220b3 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_configuration-meta.xml @@ -0,0 +1,118 @@ +<?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="CatalogStorefrontConfiguration" dataType="catalog_storefront_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/catalog/" method="POST"> + <object key="groups" dataType="catalog_storefront_config"> + <object key="frontend" dataType="catalog_storefront_config"> + <object key="fields" dataType="catalog_storefront_config"> + <object key="list_mode" dataType="list_mode"> + <field key="value">string</field> + </object> + <object key="grid_per_page_values" dataType="grid_per_page_values"> + <field key="value">string</field> + </object> + <object key="grid_per_page" dataType="grid_per_page"> + <field key="value">string</field> + </object> + <object key="list_per_page_values" dataType="list_per_page_values"> + <field key="value">string</field> + </object> + <object key="list_per_page" dataType="list_per_page"> + <field key="value">string</field> + </object> + <object key="default_sort_by" dataType="default_sort_by"> + <field key="value">string</field> + </object> + <object key="list_allow_all" dataType="list_allow_all"> + <field key="value">integer</field> + </object> + <object key="remember_pagination" dataType="remember_pagination"> + <field key="value">integer</field> + </object> + <object key="flat_catalog_category" dataType="flat_catalog_category"> + <field key="value">integer</field> + </object> + <object key="flat_catalog_product" dataType="flat_catalog_product"> + <field key="value">integer</field> + </object> + <object key="swatches_per_product" dataType="swatches_per_product"> + <field key="value">string</field> + </object> + <object key="show_swatches_in_product_list" dataType="show_swatches_in_product_list"> + <field key="value">integer</field> + </object> + </object> + </object> + </object> + </operation> + + <operation name="DefaultCatalogStorefrontConfiguration" dataType="default_catalog_storefront_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/catalog/" method="POST"> + <object key="groups" dataType="default_catalog_storefront_config"> + <object key="frontend" dataType="default_catalog_storefront_config"> + <object key="fields" dataType="default_catalog_storefront_config"> + <object key="list_mode" dataType="default_catalog_storefront_config"> + <object key="inherit" dataType="catalogStorefrontFlagZero"> + <field key="value">integer</field> + </object> + </object> + <object key="grid_per_page_values" dataType="default_catalog_storefront_config"> + <object key="inherit" dataType="catalogStorefrontFlagZero"> + <field key="value">integer</field> + </object> + </object> + <object key="grid_per_page" dataType="default_catalog_storefront_config"> + <object key="inherit" dataType="catalogStorefrontFlagZero"> + <field key="value">integer</field> + </object> + </object> + <object key="list_per_page_values" dataType="default_catalog_storefront_config"> + <object key="inherit" dataType="catalogStorefrontFlagZero"> + <field key="value">integer</field> + </object> + </object> + <object key="list_per_page" dataType="default_catalog_storefront_config"> + <object key="inherit" dataType="catalogStorefrontFlagZero"> + <field key="value">integer</field> + </object> + </object> + <object key="default_sort_by" dataType="default_catalog_storefront_config"> + <object key="inherit" dataType="catalogStorefrontFlagZero"> + <field key="value">integer</field> + </object> + </object> + <object key="remember_pagination" dataType="default_catalog_storefront_config"> + <object key="inherit" dataType="catalogStorefrontFlagZero"> + <field key="value">integer</field> + </object> + </object> + <object key="flat_catalog_category" dataType="default_catalog_storefront_config"> + <object key="inherit" dataType="catalogStorefrontFlagZero"> + <field key="value">integer</field> + </object> + </object> + <object key="swatches_per_product" dataType="default_catalog_storefront_config"> + <object key="inherit" dataType="catalogStorefrontFlagZero"> + <field key="value">integer</field> + </object> + </object> + <object key="show_swatches_in_product_list" dataType="default_catalog_storefront_config"> + <object key="inherit" dataType="catalogStorefrontFlagZero"> + <field key="value">integer</field> + </object> + </object> + <object key="list_allow_all" dataType="list_allow_all"> + <field key="value">integer</field> + </object> + <object key="flat_catalog_product" dataType="flat_catalog_product"> + <field key="value">integer</field> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_price-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_price-meta.xml new file mode 100644 index 000000000000..1ee57c89b2b3 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_price-meta.xml @@ -0,0 +1,25 @@ +<?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="CatalogPriceConfigState" dataType="catalog_price_config_state" type="create" auth="adminFormKey" url="/admin/system_config/save/section/catalog/" method="POST"> + <object key="groups" dataType="catalog_price_config_state"> + <object key="price" dataType="catalog_price_config_state"> + <object key="fields" dataType="catalog_price_config_state"> + <object key="scope" dataType="scope"> + <field key="value">string</field> + </object> + <object key="default_product_price" dataType="default_product_price"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_recently_products-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_recently_products-meta.xml new file mode 100644 index 000000000000..0fe4f154d5ef --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_recently_products-meta.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CatalogRecentlyProductsConfiguration" dataType="catalog_recently_products" type="create" + auth="adminFormKey" url="/admin/system_config/save/section/catalog/" method="POST" successRegex="/messages-message-success/"> + <object key="groups" dataType="catalog_recently_products"> + <object key="recently_products" dataType="catalog_recently_products"> + <object key="fields" dataType="catalog_recently_products"> + <object key="synchronize_with_backend" dataType="synchronize_with_backend"> + <field key="value">integer</field> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_special_price-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/catalog_special_price-meta.xml new file mode 100644 index 000000000000..354277ad056f --- /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 000000000000..7aa7530b0fda --- /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/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Framework/Metadata/image_content-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/image_content-meta.xml similarity index 100% rename from dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Framework/Metadata/image_content-meta.xml rename to app/code/Magento/Catalog/Test/Mftf/Metadata/image_content-meta.xml diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryPage.xml index 9349e188430f..f7d8abf8b2fe 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryPage.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryPage.xml @@ -8,7 +8,7 @@ <pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> - <page name="AdminCategoryPage" url="catalog/category/" area="admin" module="Catalog"> + <page name="AdminCategoryPage" url="catalog/category/" area="admin" module="Magento_Catalog"> <section name="AdminCategorySidebarActionSection"/> <section name="AdminCategoryMainActionsSection"/> <section name="AdminCategorySidebarTreeSection"/> 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 000000000000..dd5d5aef08a7 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminNewWidgetPage.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminNewWidgetPage" url="admin/widget_instance/new/" area="admin" module="Magento_Widget"> + <section name="AdminNewWidgetSelectProductPopupSection"/> + <section name="AdminCatalogProductWidgetSection"/> + </page> +</pages> diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductCreatePage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductCreatePage.xml index fc776b49ba21..e4c4ece5ac6c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductCreatePage.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductCreatePage.xml @@ -15,7 +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 000000000000..1ce53a0ebd54 --- /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/Page/StorefrontProductPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontProductPage.xml index 5aaa78822af0..75e3210cad7d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontProductPage.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontProductPage.xml @@ -8,7 +8,7 @@ <pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> - <page name="StorefrontProductPage" url="/{{var1}}.html" area="storefront" module="Catalog" parameterized="true"> + <page name="StorefrontProductPage" url="/{{var1}}.html" area="storefront" module="Magento_Catalog" parameterized="true"> <section name="StorefrontProductInfoMainSection" /> <section name="StorefrontProductInfoDetailsSection" /> <section name="WYSIWYGToolbarSection"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCatalogProductWidgetSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCatalogProductWidgetSection.xml new file mode 100644 index 000000000000..3261db1f63f2 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCatalogProductWidgetSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCatalogProductWidgetSection"> + <element name="productAttributesToShow" type="multiselect" selector="select[name='parameters[show_attributes][]']"/> + <element name="productButtonsToShow" type="multiselect" selector="select[name='parameters[show_buttons][]']"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryBasicFieldSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryBasicFieldSection.xml index 8b69a44993f1..977e63b9ec92 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryBasicFieldSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryBasicFieldSection.xml @@ -22,6 +22,7 @@ <element name="ContentTab" type="input" selector="input[name='name']"/> <element name="FieldError" type="text" selector=".admin__field-error[data-bind='attr: {for: {{field}}}, text: error']" parameterized="true"/> <element name="panelFieldControl" type="input" selector='//aside//div[@data-index="{{arg1}}"]/descendant::*[@name="{{arg2}}"]' parameterized="true"/> + <element name="productsInCategory" type="input" selector="div[data-index='assign_products']" timeout="30"/> </section> <section name="CategoryContentSection"> <element name="SelectFromGalleryBtn" type="button" selector="//label[text()='Select from Gallery']"/> @@ -32,6 +33,20 @@ <element name="DesignTab" type="button" selector="//strong[@class='admin__collapsible-title']//span[text()='Design']"/> <element name="LayoutDropdown" type="select" selector="select[name='page_layout']"/> </section> + <section name="CategoryDisplaySettingsSection"> + <element name="DisplaySettingTab" type="button" selector="//strong[@class='admin__collapsible-title']//span[text()='Display Settings']"/> + <element name="layeredNavigationPriceInput" type="input" selector="input[name='filter_price_range']"/> + <element name="FieldError" type="text" selector=".admin__field-error[data-bind='attr: {for: {{field}}}, text: error']" parameterized="true"/> + <element name="filterPriceRangeUseConfig" type="checkbox" selector="input[name='use_config[filter_price_range]']"/> + <element name="RequiredFieldIndicator" type="text" selector=" return window.getComputedStyle(document.querySelector('._required[data-index={{arg1}}]>.admin__field-label span'), ':after').getPropertyValue('content');" parameterized="true"/> + <element name="displayMode" type="button" selector="select[name='display_mode']"/> + <element name="anchor" type="checkbox" selector="input[name='is_anchor']"/> + <element name="productListCheckBox" type="checkbox" selector="input[name='use_config[available_sort_by]']" /> + <element name="productList" type="text" selector="select[name='available_sort_by']"/> + <element name="defaultProductLisCheckBox" type="checkbox" selector="input[name='use_config[default_sort_by]']"/> + <element name="defaultProductList" type="text" selector="select[name='default_sort_by']"/> + <element name="layeredNavigationPriceCheckBox" type="checkbox" selector="input[name='use_config[filter_price_range]']"/> + </section> <section name="CatalogWYSIWYGSection"> <element name="ShowHideBtn" type="button" selector="#togglecategory_form_description"/> <element name="TinyMCE4" type="text" selector=".mce-branding-powered-by"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryContentSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryContentSection.xml index ed0325394d59..e3d224904671 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryContentSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryContentSection.xml @@ -15,5 +15,14 @@ <element name="uploadImageFile" type="input" selector=".file-uploader-area>input"/> <element name="imageFileName" type="text" selector=".file-uploader-filename"/> <element name="removeImageButton" type="button" selector=".file-uploader-summary .action-remove"/> + <element name="AddCMSBlock" type="select" selector="//*[@name='landing_page']"/> + <element name="description" type="input" selector="//*[@name='description']"/> + <element name="content" type="button" selector="div[data-index='content'"/> + <element name="categoryInTree" type="button" selector="//li[contains(@class, 'x-tree-node')]//div[contains(.,'{{categoryName}}') and contains(@class, 'no-active-category')]" parameterized="true" /> + <element name="categoryPageTitle" type="text" selector="h1.page-title" /> + <element name="activeCategoryInTree" type="button" selector="//li[contains(@class, 'x-tree-node')]//div[contains(.,'{{categoryName}}') and contains(@class, 'active-category')]" parameterized="true" /> + <element name="productTableColumnName" type="input" selector="#catalog_category_products_filter_name"/> + <element name="productTableRow" type="button" selector="#catalog_category_products_table tbody tr"/> + <element name="productSearch" type="button" selector="//button[@data-action='grid-filter-apply']" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryDisplaySettingsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryDisplaySettingsSection.xml new file mode 100644 index 000000000000..daa00eb0a27b --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryDisplaySettingsSection.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="AdminCategoryDisplaySettingsSection"> + <element name="settingsHeader" type="button" selector="//*[contains(text(),'Display Settings')]" timeout="30"/> + <element name="displayMode" type="button" selector="//*[@name='display_mode']"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMainActionsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMainActionsSection.xml index 009110a729bd..e8adede5b2de 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMainActionsSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMainActionsSection.xml @@ -14,5 +14,7 @@ <element name="CategoryStoreViewDropdownToggle" type="button" selector="#store-change-button"/> <element name="CategoryStoreViewOption" type="button" selector="//div[contains(@class, 'store-switcher')]//a[normalize-space()='{{store}}']" parameterized="true"/> <element name="CategoryStoreViewModalAccept" type="button" selector=".modal-popup.confirm._show .action-accept"/> + <element name="allStoreViews" type="button" selector=".store-switcher .store-switcher-all" timeout="30"/> + <element name="storeSwitcher" type="text" selector=".store-switcher"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMessagesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMessagesSection.xml index fee86ca1caa2..ea4f4bf53eb7 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMessagesSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMessagesSection.xml @@ -10,5 +10,6 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminCategoryMessagesSection"> <element name="SuccessMessage" type="text" selector=".message-success"/> + <element name="errorMessage" type="text" selector="//div[@class='message message-error error']"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsGridSection.xml index 6b754dcc5d48..df79ec61ef73 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsGridSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryProductsGridSection.xml @@ -14,5 +14,8 @@ <element name="rowProductSku" type="text" selector="#catalog_category_products_table tbody tr:nth-of-type({{row}}) .col-sku" parameterized="true"/> <element name="rowPrice" type="text" selector="#catalog_category_products_table tbody tr:nth-of-type({{row}}) .col-price" parameterized="true"/> <element name="rowPosition" type="input" selector="#catalog_category_products_table tbody tr:nth-of-type({{row}}) .col-position .position input" timeout="30" parameterized="true"/> + <element name="productGridNameProduct" type="text" selector="//table[@id='catalog_category_products_table']//td[contains(., '{{productName}}')]" parameterized="true"/> + <element name="productVisibility" type="select" selector="//*[@name='product[visibility]']"/> + <element name="productSelectAll" type="checkbox" selector="input.admin__control-checkbox"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml index ef6fb99e88ee..fba28b3feaff 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml @@ -13,8 +13,9 @@ <element name="expandAll" type="button" selector=".tree-actions a:last-child"/> <element name="categoryTreeRoot" type="text" selector="div.x-tree-root-node>li.x-tree-node:first-of-type>div.x-tree-node-el:first-of-type" timeout="30"/> <element name="categoryInTree" type="text" selector="//a/span[contains(text(), '{{name}}')]" parameterized="true" timeout="30"/> - <element name="categoryInTreeUnderRoot" type="text" selector="//div[@class='x-tree-root-node']/li/ul/li[@class='x-tree-node']/div/a/span[contains(text(), '{{name}}')]" parameterized="true"/> + <element name="categoryInTreeUnderRoot" type="text" selector="//li/ul/li[@class='x-tree-node']/div/a/span[contains(text(), '{{name}}')]" parameterized="true"/> <element name="lastCreatedCategory" type="block" selector=".x-tree-root-ct li li:last-child" /> <element name="treeContainer" type="block" selector=".tree-holder" /> + <element name="expandRootCategory" type="text" selector="img.x-tree-elbow-end-plus"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateNewProductAttributeSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateNewProductAttributeSection.xml new file mode 100644 index 000000000000..2de7bf19fd37 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateNewProductAttributeSection.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCreateNewProductAttributeSection"> + <element name="saveAttribute" type="button" selector="#save"/> + <element name="defaultLabel" type="input" selector="input[name='frontend_label[0]']"/> + <element name="inputType" type="select" selector="select[name='frontend_input']" timeout="30"/> + <element name="addValue" type="button" selector="//button[contains(@data-action,'add_new_row')]" timeout="30"/> + <element name="defaultStoreView" type="input" selector="//input[contains(@name,'option[value][option_{{row}}][1]')]" parameterized="true"/> + <element name="adminOption" type="input" selector="//input[contains(@name,'option[value][option_{{row}}][0]')]" parameterized="true"/> + <element name="defaultRadioButton" type="radio" selector="//tr[{{row}}]//input[contains(@name,'default[]')]" parameterized="true"/> + <element name="isRequired" type="checkbox" selector="//input[contains(@name,'is_required')]/..//label"/> + <element name="advancedAttributeProperties" type="text" selector="//div[contains(@data-index,'advanced_fieldset')]"/> + <element name="attributeCode" type="input" selector="//*[@class='admin__fieldset-wrapper-content admin__collapsible-content _show']//input[@name='attribute_code']"/> + <element name="scope" type="select" selector="//*[@class='admin__fieldset-wrapper-content admin__collapsible-content _show']//select[@name='is_global']" timeout="30"/> + <element name="defaultValue" type="input" selector="//*[@class='admin__fieldset-wrapper-content admin__collapsible-content _show']//input[@name='default_value_text']"/> + <element name="isUnique" type="checkbox" selector="//input[contains(@name, 'is_unique')]/..//label"/> + <element name="storefrontProperties" type="text" selector="//div[contains(@data-index,'front_fieldset')]"/> + <element name="inSearch" type="checkbox" selector="//input[contains(@name, 'is_searchable')]/..//label"/> + <element name="advancedSearch" type="checkbox" selector="//input[contains(@name, 'is_visible_in_advanced_search')]/..//label"/> + <element name="isComparable" type="checkbox" selector="//input[contains(@name, 'is_comparable')]/..//label"/> + <element name="allowHtmlTags" type="checkbox" selector="//input[contains(@name, 'is_html_allowed_on_front')]/..//label"/> + <element name="visibleOnStorefront" type="checkbox" selector="//input[contains(@name, 'is_visible_on_front')]/..//label"/> + <element name="sortProductListing" type="checkbox" selector="//input[contains(@name, 'is_visible_on_front')]/..//label"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml index b83676c2e103..d24c501152b7 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml @@ -17,15 +17,36 @@ <element name="DefaultValue" type="input" selector="#default_value_text"/> <element name="Scope" type="select" selector="#is_global"/> <element name="Save" type="button" selector="#save" timeout="30"/> + <element name="DeleteAttribute" type="button" selector="#delete" timeout="30"/> <element name="SaveAndEdit" type="button" selector="#save_and_edit_button" timeout="30"/> <element name="TinyMCE4" type="button" selector="//span[text()='Default Value']/parent::label/following-sibling::div//div[@class='mce-branding-powered-by']"/> <element name="checkIfTabOpen" selector="//div[@id='advanced_fieldset-wrapper' and not(contains(@class,'opened'))]" type="button"/> <element name="useInLayeredNavigation" type="select" selector="#is_filterable"/> + <element name="addSwatch" type="button" selector="#add_new_swatch_text_option_button"/> + <element name="dropdownAddOptions" type="button" selector="#add_new_option_button"/> + <!-- Manage Options nth child--> + <element name="dropdownNthOptionIsDefault" type="checkbox" selector="tbody[data-role='options-container'] tr:nth-child({{var}}) .input-radio" parameterized="true"/> + <element name="dropdownNthOptionAdmin" type="textarea" selector="tbody[data-role='options-container'] tr:nth-child({{var}}) td:nth-child(3) input" parameterized="true"/> + <element name="dropdownNthOptionDefaultStoreView" type="textarea" selector="tbody[data-role='options-container'] tr:nth-child({{var}}) td:nth-child(4) input" parameterized="true"/> + <element name="dropdownNthOptionDelete" type="button" selector="tbody[data-role='options-container'] tr:nth-child({{var}}) button[title='Delete']" parameterized="true"/> + </section> + <section name="AttributeDeleteModalSection"> + <element name="confirm" type="button" selector=".modal-popup.confirm .action-accept"/> + <element name="cancel" type="button" selector=".modal-popup.confirm .action-dismiss"/> + </section> + <section name="AttributeManageSwatchSection"> + <element name="swatchField" type="input" selector="//th[contains(@class, 'col-swatch')]/span[contains(text(), '{{arg}}')]/ancestor::thead/following-sibling::tbody//input[@placeholder='Swatch']" parameterized="true"/> + <element name="descriptionField" type="input" selector="//th[contains(@class, 'col-swatch')]/span[contains(text(), '{{arg}}')]/ancestor::thead/following-sibling::tbody//input[@placeholder='Description']" parameterized="true"/> + </section> + <section name="AttributeOptionsSection"> + <element name="AddOption" type="button" selector="#add_new_option_button"/> </section> <section name="StorefrontPropertiesSection"> + <element name="PageTitle" type="text" selector="//span[text()='Storefront Properties']" /> <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"/> @@ -63,4 +84,20 @@ <element name="SpecialCharacter" type="button" selector=".mce-i-charmap" /> <element name="TextArea" type="input" selector="#default_value_textarea" /> </section> + <section name="AdvancedAttributePropertiesSection"> + <element name="AdvancedAttributePropertiesSectionToggle" + type="button" selector="#advanced_fieldset-wrapper"/> + <element name="AttributeCode" type="text" selector="#attribute_code"/> + <element name="DefaultValueText" type="textarea" selector="#default_value_text"/> + <element name="DefaultValueTextArea" type="textarea" selector="#default_value_textarea"/> + <element name="DefaultValueDate" type="textarea" selector="#default_value_date"/> + <element name="DefaultValueYesNo" type="textarea" selector="#default_value_yesno"/> + <element name="Scope" type="select" selector="#is_global"/> + <element name="UniqueValue" type="select" selector="#is_unique"/> + <element name="AddToColumnOptions" type="select" selector="#is_used_in_grid"/> + <element name="UseInFilterOptions" type="select" selector="#is_filterable_in_grid"/> + <element name="UseInProductListing" type="select" selector="#used_in_product_listing"/> + <element name="UseInSearch" type="select" selector="#is_searchable"/> + <element name="VisibleInAdvancedSearch" type="select" selector="#is_visible_in_advanced_search"/> + </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminEditProductAttributesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminEditProductAttributesSection.xml index 99dfddcc776d..63bdcd52cdd2 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminEditProductAttributesSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminEditProductAttributesSection.xml @@ -18,5 +18,8 @@ <element name="AttributeDescription" type="text" selector="#description"/> <element name="ChangeAttributeDescriptionToggle" type="checkbox" selector="#toggle_description"/> <element name="Save" type="button" selector="button[title=Save]" timeout="30"/> + <element name="ProductDataMayBeLostModal" type="button" selector="//aside[contains(@class,'_show')]//header[contains(.,'Product data may be lost')]"/> + <element name="ProductDataMayBeLostConfirmButton" type="button" selector="//aside[contains(@class,'_show')]//button[.='Change Input Type']"/> + <element name="defaultLabel" type="text" selector="//td[contains(text(), '{{attributeName}}')]/following-sibling::td[contains(@class, 'col-frontend_label')]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminNewWidgetSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminNewWidgetSection.xml new file mode 100644 index 000000000000..5329ad48c8f4 --- /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 000000000000..0da67849f85c --- /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 000000000000..a3c98e43b451 --- /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 160948f8f1f2..5efd04eacb71 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml @@ -10,11 +10,18 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminProductAttributeGridSection"> <element name="AttributeCode" type="text" selector="//td[contains(text(),'{{var1}}')]" parameterized="true" timeout="30"/> - <element name="createNewAttributeBtn" type="button" selector="button[data-index='add_new_attribute_button']"/> + <element name="createNewAttributeBtn" type="button" selector="#add"/> <element name="GridFilterFrontEndLabel" type="input" selector="#attributeGrid_filter_frontend_label"/> <element name="Search" type="button" selector="button[data-action=grid-filter-apply]" timeout="30"/> <element name="ResetFilter" type="button" selector="button[data-action='grid-filter-reset']" timeout="30"/> <element name="FirstRow" type="button" selector="//*[@id='attributeGrid_table']/tbody/tr[1]" timeout="30"/> <element name="FilterByAttributeCode" type="input" selector="#attributeGrid_filter_attribute_code"/> + <element name="attributeLabelFilter" type="input" selector="//input[@name='frontend_label']"/> + <element name="attributeCodeColumn" type="text" selector="//div[@id='attributeGrid']//td[contains(@class,'col-attr-code col-attribute_code')]"/> + <element name="defaultLabelColumn" type="text" selector="//div[@id='attributeGrid']//td[contains(@class,'col-label col-frontend_label')]"/> + <element name="isVisibleColumn" type="text" selector="//div[@id='attributeGrid']//td[contains(@class,'a-center col-is_visible')]"/> + <element name="scopeColumn" type="text" selector="//div[@id='attributeGrid']//td[contains(@class,'a-center col-is_global')]"/> + <element name="isSearchableColumn" type="text" selector="//div[@id='attributeGrid']//td[contains(@class,'a-center col-is_searchable')]"/> + <element name="isComparableColumn" type="text" selector="//div[@id='attributeGrid']//td[contains(@class,'a-center col-is_comparable')]"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeOptionsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeOptionsSection.xml new file mode 100644 index 000000000000..5f1112eef362 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeOptionsSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<!-- +/** + * 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="DropdownAttributeOptionsSection"> + <element name="nthOptionAdminLabel" type="input" + selector="(//*[@id='manage-options-panel']//tr[{{var}}]//input[contains(@name, 'option[value]')])[1]" parameterized="true"/> + <element name="deleteButton" type="button" selector="(//td[@class='col-delete'])[1]" timeout="30"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeSetGridSection.xml index b906e2fa9084..3fad50adb771 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 000000000000..46a516b538f0 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributesSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminProductAttributesSection"> + <element name="sectionHeader" type="button" selector="div[data-index='attributes']" timeout="30"/> + <element name="attributeLabelByCode" type="text" selector="div[data-index='{{var}}'] .admin__field-label span" parameterized="true"/> + <element name="attributeTextInputByCode" type="text" selector="div[data-index='{{var}}'] .admin__field-control input" parameterized="true"/> + <element name="attributeDropdownByCode" type="text" selector="div[data-index='{{var}}'] .admin__field-control select" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCategoryCreationSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCategoryCreationSection.xml index 337cf0527dd4..755add18ec1c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCategoryCreationSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCategoryCreationSection.xml @@ -11,12 +11,10 @@ <section name="AdminProductCategoryCreationSection"> <element name="firstExampleProduct" type="button" selector=".data-row:nth-of-type(1)"/> <element name="newCategory" type="button" selector="//button/span[text()='New Category']"/> - <element name="nameInput" type="input" selector="input[name='name']"/> - <element name="parentCategory" type="block" selector=".product_form_product_form_create_category_modal div[data-role='selected-option']"/> <element name="parentSearch" type="input" selector="aside input[data-role='advanced-select-text']"/> <element name="parentSearchResult" type="block" selector="aside .admin__action-multiselect-menu-inner"/> - <element name="createCategory" type="button" selector="#save"/> + <element name="createCategory" type="button" selector="#save" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductContentSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductContentSection.xml index a2ad155672a1..fafae5d53554 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductContentSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductContentSection.xml @@ -10,8 +10,10 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminProductContentSection"> <element name="sectionHeader" type="button" selector="div[data-index='content']" timeout="30"/> + <element name="sectionHeaderShow" type="button" selector="div[data-index='content']._show" timeout="30"/> <element name="descriptionTextArea" type="textarea" selector="#product_form_description"/> <element name="shortDescriptionTextArea" type="textarea" selector="#product_form_short_description"/> <element name="sectionHeaderIfNotShowing" type="button" selector="//div[@data-index='content']//div[contains(@class, '_hide')]"/> + <element name="pageHeader" type="textarea" selector="//*[@class='page-header row']"/> </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 000000000000..803d72d7a7ec --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCrossSellModalSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminProductCrossSellModalSection"> + <element name="addSelectedProducts" type="button" selector=".product_form_product_form_related_crosssell_modal .action-primary"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsSection.xml index 052195ec1aaa..352d219351fb 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsSection.xml @@ -13,13 +13,15 @@ <element name="customizableOptions" type="text" selector="//strong[contains(@class, 'admin__collapsible-title')]/span[text()='Customizable Options']"/> <element name="useDefaultOptionTitle" type="text" selector="[data-index='options'] tr.data-row [data-index='title'] [name^='options_use_default']"/> <element name="useDefaultOptionTitleByIndex" type="text" selector="[data-index='options'] [data-index='values'] tr[data-repeat-index='{{var1}}'] [name^='options_use_default']" parameterized="true"/> - <element name="addOptionBtn" type="button" selector="button[data-index='button_add']"/> + <element name="addOptionBtn" type="button" selector="button[data-index='button_add']" timeout="30"/> <element name="fillOptionTitle" type="input" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//span[text()='Option Title']/parent::label/parent::div/parent::div//input[@class='admin__control-text']" parameterized="true"/> - <element name="optionTitleInput" type="input" selector="input[name='product[options][0][title]']"/> - <element name="optionTypeOpenDropDown" type="button" selector=".admin__dynamic-rows[data-index='options'] .action-select"/> - <element name="optionTypeTextField" type="button" selector=".admin__dynamic-rows[data-index='options'] .action-menu._active li li"/> + <element name="optionTitleInput" type="input" selector="input[name='product[options][{{index}}][title]']" parameterized="true"/> + <element name="optionTypeOpenDropDown" type="button" selector=".admin__dynamic-rows[data-index='options'] .action-select" timeout="30"/> + <element name="optionTypeTextField" type="button" selector=".admin__dynamic-rows[data-index='options'] .action-menu._active li li" timeout="30"/> <element name="maxCharactersInput" type="input" selector="input[name='product[options][0][max_characters]']"/> + <element name="optionTypeDropDown" type="select" selector="//table[@data-index='options']//tr[{{index}}]//div[@data-index='type']//div[contains(@class, 'action-select-wrap')]" parameterized="true" /> + <element name="optionTypeItem" type="select" selector="//table[@data-index='options']//tr[{{index}}]//div[@data-index='type']//*[contains(@class, 'action-menu-item')]//*[contains(., '{{optionValue}}')]" parameterized="true" /> <element name="checkSelect" type="select" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//span[text()='Option Type']/parent::label/parent::div/parent::div//div[@data-role='selected-option']" parameterized="true"/> <element name="checkDropDown" type="select" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//parent::label/parent::div/parent::div//li[@class='admin__action-multiselect-menu-inner-item']//label[text()='Drop-down']" parameterized="true"/> <element name="clickAddValue" type="button" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//tfoot//button" parameterized="true"/> @@ -28,19 +30,30 @@ <element name="clickSelectPriceType" type="select" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//tbody//tr[@data-repeat-index='{{var2}}']//span[text()='Price Type']/parent::label/parent::div/parent::div//select" parameterized="true"/> <element name="checkboxUseDefaultTitle" type="checkbox" selector="//span[text()='Option Title']/parent::label/parent::div/parent::div/div//input[@type='checkbox']"/> <element name="checkboxUseDefaultOption" type="checkbox" selector="//table[@data-index='values']//tbody//tr[@data-repeat-index='{{var1}}']//div[@class='admin__field-control']//input[@type='checkbox']" parameterized="true"/> + <element name="requiredCheckBox" type="checkbox" selector="input[name='product[options][{{index}}][is_require]']" parameterized="true" /> + <element name="fillOptionValueSku" type="input" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//tbody/tr[@data-repeat-index='{{var2}}']//span[text()='SKU']/parent::label/parent::div/parent::div//div[@class='admin__field-control']/input" parameterized="true"/> <!-- Elements that make it easier to select the most recently added element --> <element name="lastOptionTitle" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[contains(@class, '_required')]//input" /> <element name="lastOptionTypeParent" type="block" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[contains(@class, 'admin__action-multiselect-text')]" /> <!-- var 1 represents the option type that you want to select, i.e "radio buttons" --> <element name="optionType" type="block" selector="//*[@data-index='custom_options']//label[text()='{{var1}}'][ancestor::*[contains(@class, '_active')]]" parameterized="true" /> - <element name="addValue" type="button" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[@data-action='add_new_row']" /> + <element name="addValue" type="button" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[@data-action='add_new_row']" timeout="30"/> <element name="valueTitle" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[contains(@class, 'admin__control-table')]//tbody/tr[last()]//*[@data-index='title']//input" /> <element name="valuePrice" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[contains(@class, 'admin__control-table')]//tbody/tr[last()]//*[@data-index='price']//input" /> - - <element name="optionPrice" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[@name='product[options][0][price]']"/> - <element name="optionPriceType" type="select" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[@name='product[options][0][price_type]']"/> - <element name="optionSku" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[@name='product[options][0][sku]']"/> - <element name="optionFileExtensions" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[@name='product[options][0][file_extension]']"/> + <element name="optionPrice" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr//*[@name='product[options][{{index}}][price]']" parameterized="true"/> + <element name="optionPriceType" type="select" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr//*[@name='product[options][{{var}}][price_type]']" parameterized="true"/> + <element name="optionSku" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr//*[@name='product[options][{{index}}][sku]']" parameterized="true"/> + <element name="optionFileExtensions" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr//*[@name='product[options][{{index}}][file_extension]']" parameterized="true"/> + <element name="importOptions" type="button" selector="//button[@data-index='button_import']" timeout="30"/> + </section> + <section name="AdminProductImportOptionsSection"> + <element name="selectProductTitle" type="text" selector="//h1[contains(text(), 'Select Product')]" timeout="30"/> + <element name="filterButton" type="button" selector="//button[@data-action='grid-filter-expand']" timeout="30"/> + <element name="nameField" type="input" selector="//input[@name='name']" timeout="30"/> + <element name="applyFiltersButton" type="button" selector="//button[@data-action='grid-filter-apply']" timeout="30"/> + <element name="resetFiltersButton" type="button" selector="//button[@data-action='grid-filter-reset']" timeout="30"/> + <element name="firstRowItemCheckbox" type="input" selector="//input[@data-action='select-row']" timeout="30"/> + <element name="importButton" type="button" selector="//button[contains(@class, 'action-primary')]/span[contains(text(), 'Import')]" timeout="30"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductDropdownOrderSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductDropdownOrderSection.xml new file mode 100644 index 000000000000..b58fb2316f91 --- /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/AdminProductFiltersSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFiltersSection.xml index ed08c84cdb6f..06ff54b2a399 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFiltersSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFiltersSection.xml @@ -15,7 +15,7 @@ <element name="apply" type="button" selector="button[data-action=grid-filter-apply]" timeout="30"/> <element name="filter" type="button" selector="//div[@class='data-grid-filters-action-wrap']/button" timeout="30"/> <element name="typeDropDown" type="multiselect" selector="//select[@name='type_id']" timeout="30"/> - <element name="bundleOption" type="multiselect" selector="//select[@name='type_id']/option[4]" timeout="30"/> + <element name="bundleOption" type="multiselect" selector="//select[@name='type_id']/option[@value='bundle']" timeout="30"/> <element name="applyFilters" type="button" selector="//button[@class='action-secondary']" timeout="30"/> <element name="allCheckbox" type="checkbox" selector="//div[@data-role='grid-wrapper']//label[@data-bind='attr: {for: ko.uid}']" timeout="30"/> <element name="actions" type="button" selector="//div[@class='action-select-wrap']/button" timeout="30"/> @@ -28,5 +28,7 @@ <element name="priceOfFirstRow" type="text" selector="//tr[@data-repeat-index='0']//div[contains(., '{{var1}}')]" parameterized="true"/> <element name="AllProductsNotOfBundleType" type="text" selector="//td[5]/div[text() != 'Bundle Product']"/> <element name="attributeSetOfFirstRow" type="text" selector="//tr[@data-repeat-index='0']//div[contains(., '{{var1}}')]" parameterized="true"/> + <element name="storeViewDropDown" type="multiselect" selector="//select[@name='store_id']" timeout="30"/> + <element name="storeViewOption" type="multiselect" selector="//select[@name='store_id']/option[contains(text(),'{{var1}}')]" parameterized="true" timeout="30"/> </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 afbaba41a9bb..1652546b0acb 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormActionSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormActionSection.xml @@ -10,10 +10,13 @@ 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"/> <element name="changeStoreButton" type="button" selector="#store-change-button" timeout="10"/> <element name="selectStoreView" type="button" selector="//ul[@data-role='stores-list']/li/a[normalize-space(.)='{{var1}}']" timeout="10" parameterized="true"/> + <element name="selectTaxClass" type="select" selector="select[name='product[tax_class_id]']"/> + <element name="saveAndDuplicate" type="button" selector="span[id='save_and_duplicate']" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml new file mode 100644 index 000000000000..bc7c472df6ea --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml @@ -0,0 +1,32 @@ +<?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="AdminProductFormAdvancedInventorySection"> + <element name="enableQtyIncrements" type="select" selector="//*[@name='product[stock_data][enable_qty_increments]']"/> + <element name="enableQtyIncrementsOptions" type="select" selector="//*[@name='product[stock_data][enable_qty_increments]']//option[contains(@value, '{{var1}}')]" parameterized="true"/> + <element name="enableQtyIncrementsUseConfigSettings" type="checkbox" selector="//input[@name='product[stock_data][use_config_enable_qty_inc]']"/> + <element name="qtyUsesDecimals" type="select" selector="//*[@name='product[stock_data][is_qty_decimal]']"/> + <element name="qtyUsesDecimalsOptions" type="select" selector="//*[@name='product[stock_data][is_qty_decimal]']//option[contains(@value, '{{var1}}')]" parameterized="true"/> + <element name="qtyIncrements" type="input" selector="//input[@name='product[stock_data][qty_increments]']"/> + <element name="qtyIncrementsUseConfigSettings" type="checkbox" selector="//input[@name='product[stock_data][use_config_qty_increments]']"/> + <element name="doneButton" type="button" selector="//aside[contains(@class,'product_form_product_form_advanced_inventory_modal')]//button[contains(@data-role,'action')]" timeout="5"/> + <element name="useConfigSettings" type="checkbox" selector="//input[@name='product[stock_data][use_config_manage_stock]']"/> + <element name="manageStock" type="select" selector="//*[@name='product[stock_data][manage_stock]']"/> + <element name="advancedInventoryCloseButton" type="button" selector=".product_form_product_form_advanced_inventory_modal button.action-close" timeout="30"/> + <element name="miniQtyConfigSetting" type="checkbox" selector="//*[@name='product[stock_data][use_config_min_sale_qty]']"/> + <element name="miniQtyAllowedInCart" type="input" selector="//*[@name='product[stock_data][min_sale_qty]']"/> + <element name="maxiQtyConfigSetting" type="checkbox" selector="//*[@name='product[stock_data][use_config_max_sale_qty]']"/> + <element name="maxiQtyAllowedInCart" type="input" selector="//*[@name='product[stock_data][max_sale_qty]']"/> + <element name="notifyBelowQtyConfigSetting" type="checkbox" selector="//*[@name='product[stock_data][use_config_notify_stock_qty]']"/> + <element name="notifyBelowQty" type="input" selector="//*[@name='product[stock_data][notify_stock_qty]']"/> + <element name="advancedInventoryQty" type="input" selector="//div[@class='modal-inner-wrap']//input[@name='product[quantity_and_stock_status][qty]']"/> + <element name="advancedInventoryStockStatus" type="select" selector="//div[@class='modal-inner-wrap']//select[@name='product[quantity_and_stock_status][is_in_stock]']"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml index 0a1804aa284d..3ef78a3fe8f4 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml @@ -14,12 +14,14 @@ <element name="advancedPricingCloseButton" type="button" selector=".product_form_product_form_advanced_pricing_modal button.action-close" timeout="30"/> <element name="productTierPriceWebsiteSelect" type="select" selector="[name='product[tier_price][{{var1}}][website_id]']" parameterized="true"/> <element name="productTierPriceCustGroupSelect" type="select" selector="[name='product[tier_price][{{var1}}][cust_group]']" parameterized="true"/> + <element name="productTierPriceCustGroupSelectOptions" type="select" selector="[name='product[tier_price][{{var1}}][cust_group]'] option" parameterized="true"/> <element name="productTierPriceQtyInput" type="input" selector="[name='product[tier_price][{{var1}}][price_qty]']" parameterized="true"/> <element name="productTierPriceValueTypeSelect" type="select" selector="[name='product[tier_price][{{var1}}][value_type]']" parameterized="true"/> <element name="productTierPriceFixedPriceInput" type="input" selector="[name='product[tier_price][{{var1}}][price]']" parameterized="true"/> <element name="productTierPricePercentageValuePriceInput" type="input" selector="[name='product[tier_price][{{var1}}][percentage_value]']" parameterized="true"/> <element name="specialPrice" type="input" selector="input[name='product[special_price]']"/> <element name="doneButton" type="button" selector=".product_form_product_form_advanced_pricing_modal button.action-primary" timeout="5"/> + <element name="msrp" type="input" selector="//input[@name='product[msrp]']" timeout="30"/> <element name="save" type="button" selector="#save-button"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAttributeSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAttributeSection.xml new file mode 100644 index 000000000000..a2a349ed6761 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAttributeSection.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminProductFormAttributeSection"> + <element name="createNewAttribute" type="button" selector="//button[@data-index='add_new_attribute_button']" timeout="30"/> + </section> + <section name="AdminProductFormNewAttributeSection"> + <element name="attributeLabel" type="button" selector="//input[@name='frontend_label[0]']" timeout="30"/> + <element name="attributeType" type="select" selector="//select[@name='frontend_input']" timeout="30"/> + <element name="addValue" type="button" selector="//button[@data-action='add_new_row']" timeout="30"/> + <element name="optionViewName" type="text" selector="//table[@data-index='attribute_options_select']//span[contains(text(), '{{arg}}')]" parameterized="true" timeout="30"/> + <element name="optionValue" type="input" selector="(//input[contains(@name, 'option[value]')])[{{arg}}]" timeout="30" parameterized="true"/> + <element name="manageTitlesHeader" type="button" selector="//div[@class='fieldset-wrapper-title']//span[contains(text(), 'Manage Titles')]" timeout="30"/> + <element name="manageTitlesViewName" type="text" selector="//div[@data-index='manage-titles']//span[contains(text(), '{{arg}}')]" timeout="30" parameterized="true"/> + <element name="saveAttribute" type="button" selector="button#save" timeout="30"/> + <element name="saveInNewSet" type="button" selector="button#saveInNewSet" timeout="10"/> + </section> + <section name="AdminProductFormNewAttributeAdvancedSection"> + <element name="sectionHeader" type="button" selector="div[data-index='advanced_fieldset']"/> + <element name="defaultValue" type="textarea" selector="input[name='default_value_text']"/> + </section> + <section name="AdminProductFormNewAttributeStorefrontSection"> + <element name="sectionHeader" type="button" selector="div[data-index='front_fieldset']"/> + <element name="useInSearch" type="checkbox" selector="div[data-index='is_searchable'] .admin__field-control label"/> + <element name="searchWeight" type="select" selector="select[name='search_weight']"/> + </section> + <section name="AdminProductFormNewAttributeNewSetSection"> + <element name="setName" type="button" selector="//div[contains(@class, 'modal-inner-wrap') and .//*[contains(., 'Enter Name for New Attribute Set')]]//input[contains(@class, 'admin__control-text')]"/> + <element name="accept" type="button" selector="//div[contains(@class, 'modal-inner-wrap') and .//*[contains(., 'Enter Name for New Attribute Set')]]//button[contains(@class, 'action-accept')]"/> + <element name="cancel" type="button" selector="//div[contains(@class, 'modal-inner-wrap') and .//*[contains(., 'Enter Name for New Attribute Set')]]//button[contains(@class, 'action-dismiss')]"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml index cfd7e5ed3b8d..f515171e835d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml @@ -13,21 +13,32 @@ <element name="attributeSetFilterResult" type="input" selector="div[data-index='attribute_set_id'] .action-menu-item._last" timeout="30"/> <element name="attributeSetFilterResultByName" type="text" selector="//label/span[text() = '{{var}}']" timeout="30" parameterized="true"/> <element name="productName" type="input" selector=".admin__field[data-index=name] input"/> + <element name="productNameDisabled" type="input" selector=".admin__field[data-index=name] input[disabled=true]"/> <element name="RequiredNameIndicator" type="text" selector=" return window.getComputedStyle(document.querySelector('._required[data-index=name]>.admin__field-label span'), ':after').getPropertyValue('content');"/> <element name="RequiredSkuIndicator" type="text" selector=" return window.getComputedStyle(document.querySelector('._required[data-index=sku]>.admin__field-label span'), ':after').getPropertyValue('content');"/> <element name="productSku" type="input" selector=".admin__field[data-index=sku] input"/> + <element name="productSkuDisabled" type="input" selector=".admin__field[data-index=sku] input[disabled=true]"/> <element name="enableProductAttributeLabel" type="text" selector="//span[text()='Enable Product']/parent::label"/> <element name="enableProductAttributeLabelWrapper" type="text" selector="//span[text()='Enable Product']/parent::label/parent::div"/> <element name="productStatus" type="checkbox" selector="input[name='product[status]']"/> + <element name="productStatusDisabled" type="checkbox" selector="input[name='product[status]'][disabled]"/> <element name="enableProductLabel" type="checkbox" selector="input[name='product[status]']+label"/> <element name="productStatusUseDefault" type="checkbox" selector="input[name='use_default[status]']"/> <element name="productNameUseDefault" type="checkbox" selector="input[name='use_default[name]']"/> <element name="productPrice" type="input" selector=".admin__field[data-index=price] input"/> + <element name="productPriceDisabled" type="input" selector=".admin__field[data-index=price] input[disabled=true]"/> + <element name="productPriceUseDefault" type="checkbox" selector=".admin__field[data-index=price] [name='use_default[price]']"/> + <element name="productTaxClass" type="select" selector="//*[@name='product[tax_class_id]']"/> + <element name="productTaxClassDisabled" type="select" selector="select[name='product[tax_class_id]'][disabled=true]"/> <element name="productTaxClassUseDefault" type="checkbox" selector="input[name='use_default[tax_class_id]']"/> - <element name="advancedPricingLink" type="button" selector="button[data-index='advanced_pricing_button']"/> + <element name="advancedPricingLink" type="button" selector="button[data-index='advanced_pricing_button']" timeout="30"/> <element name="categoriesDropdown" type="multiselect" selector="div[data-index='category_ids']"/> + <element name="unselectCategories" type="button" selector="//span[@class='admin__action-multiselect-crumb']/span[contains(.,'{{category}}')]/../button[@data-action='remove-selected-item']" parameterized="true" timeout="30"/> <element name="productQuantity" type="input" selector=".admin__field[data-index=qty] input"/> + <element name="advancedInventoryLink" type="button" selector="//button[contains(@data-index, 'advanced_inventory_button')]" timeout="30"/> <element name="productStockStatus" type="select" selector="select[name='product[quantity_and_stock_status][is_in_stock]']"/> + <element name="productStockStatusDisabled" type="select" selector="select[name='product[quantity_and_stock_status][is_in_stock]'][disabled=true]"/> + <element name="stockStatus" type="select" selector="[data-index='product-details'] select[name='product[quantity_and_stock_status][is_in_stock]']"/> <element name="productWeight" type="input" selector=".admin__field[data-index=weight] input"/> <element name="productWeightSelect" type="select" selector="select[name='product[product_has_weight]']"/> <element name="contentTab" type="button" selector="//strong[contains(@class, 'admin__collapsible-title')]/span[text()='Content']"/> @@ -35,18 +46,30 @@ <element name="priceFieldError" type="text" selector="//input[@name='product[price]']/parent::div/parent::div/label[@class='admin__field-error']"/> <element name="addAttributeBtn" type="button" selector="#addAttribute"/> <element name="createNewAttributeBtn" type="button" selector="button[data-index='add_new_attribute_button']"/> - <element name="save" type="button" selector="#save"/> + <element name="save" type="button" selector="#save-button"/> + <element name="saveNewAttribute" type="button" selector="//aside[contains(@class, 'create_new_attribute_modal')]//button[@id='save']"/> + <element name="successMessage" type="text" selector="#messages"/> <element name="attributeTab" type="button" selector="//strong[contains(@class, 'admin__collapsible-title')]/span[text()='Attributes']"/> <element name="attributeLabel" type="input" selector="//input[@name='frontend_label[0]']"/> <element name="frontendInput" type="select" selector="select[name = 'frontend_input']"/> <element name="productFormTab" type="button" selector="//strong[@class='admin__collapsible-title']/span[contains(text(), '{{tabName}}')]" parameterized="true"/> <element name="productFormTabState" type="text" selector="//strong[@class='admin__collapsible-title']/span[contains(text(), '{{tabName}}')]/parent::*/parent::*[@data-state-collapsible='{{state}}']" parameterized="true"/> <element name="visibility" type="select" selector="//select[@name='product[visibility]']"/> + <element name="visibilityDisabled" type="select" selector="select[name='product[visibility]'][disabled=true]"/> <element name="visibilityUseDefault" type="checkbox" selector="//input[@name='use_default[visibility]']"/> <element name="divByDataIndex" type="input" selector="div[data-index='{{var}}']" parameterized="true"/> <element name="setProductAsNewFrom" type="input" selector="input[name='product[news_from_date]']"/> <element name="setProductAsNewTo" type="input" selector="input[name='product[news_to_date]']"/> <element name="attributeLabelByText" type="text" selector="//*[@class='admin__field']//span[text()='{{attributeLabel}}']" parameterized="true"/> + <element name="attributeRequiredInput" type="input" selector="//input[contains(@name, 'product[{{attributeCode}}]')]" parameterized="true"/> + <element name="attributeFieldError" type="text" selector="//*[@class='admin__field _required _error']/..//label[contains(.,'This is a required field.')]"/> + <element name="customSelectField" type="select" selector="//select[@name='product[{{var}}]']" parameterized="true"/> + <element name="searchCategory" type="input" selector="//*[@data-index='category_ids']//input[contains(@class, 'multiselect-search')]"/> + <element name="selectCategory" type="input" selector="//*[@data-index='category_ids']//label[contains(., '{{categoryName}}')]" parameterized="true"/> + <element name="done" type="button" selector="//*[@data-index='category_ids']//button[@data-action='close-advanced-select']" timeout="30"/> + <element name="selectMultipleCategories" type="input" selector="//*[@data-index='container_category_ids']//*[contains(@class, '_selected')]"/> + <element name="countryOfManufacture" type="select" selector="select[name='product[country_of_manufacture]']"/> + <element name="newAddedAttribute" type="text" selector="//fieldset[@class='admin__fieldset']//div[contains(@data-index,'{{attributeCode}}')]" parameterized="true"/> </section> <section name="ProductInWebsitesSection"> <element name="sectionHeader" type="button" selector="div[data-index='websites']" timeout="30"/> @@ -57,10 +80,13 @@ <element name="LayoutDropdown" type="select" selector="select[name='product[page_layout]']"/> </section> <section name="AdminProductFormRelatedUpSellCrossSellSection"> + <element name="relatedProductsHeader" type="button" selector=".admin__collapsible-block-wrapper[data-index='related']" timeout="30"/> <element name="AddRelatedProductsButton" type="button" selector="button[data-index='button_related']" timeout="30"/> + <element name="addUpSellProduct" type="button" selector="button[data-index='button_upsell']" timeout="30"/> </section> <section name="AdminAddRelatedProductsModalSection"> - <element name="AddSelectedProductsButton" type="button" selector="//aside[contains(@class, 'product_form_product_form_related_related_modal')]//button/span[contains(text(), 'Add Selected Products')]" timeout="30"/> + <element name="AddSelectedProductsButton" type="button" selector="//aside[contains(@class, 'related_modal')]//button[contains(@class, 'action-primary')]" timeout="30"/> + <element name="AddUpSellProductsButton" type="button" selector="//aside[contains(@class, 'upsell_modal')]//button[contains(@class, 'action-primary')]" timeout="30"/> </section> <section name="ProductWYSIWYGSection"> <element name="Switcher" type="button" selector="//select[@id='dropdown-switcher']"/> @@ -104,24 +130,24 @@ <element name="Numlist" type="button" selector="//div[@id='editorproduct_form_description']//i[@class='mce-ico mce-i-bullist']" /> <element name="Bullet" type="button" selector="//div[@id='editorproduct_form_description']//i[@class='mce-ico mce-i-numlist']" /> <element name="InsertLink" type="button" selector="//div[@id='editorproduct_form_description']//i[@class='mce-ico mce-i-link']" /> - <element name="InsertImageIcon" type="button" selector="//div[@id='editorproduct_form_description']//i[@class='mce-ico mce-i-image']" /> + <element name="InsertImageIcon" type="button" selector="//div[@id='editorproduct_form_description']//i[@class='mce-ico mce-i-image']" timeout="30"/> <element name="InsertTable" type="button" selector="//div[@id='editorproduct_form_description']//i[@class='mce-ico mce-i-table']" /> <element name="SpecialCharacter" type="button" selector="//div[@id='editorproduct_form_description']//i[@class='mce-ico mce-i-charmap']" /> - <element name="Browse" type="button" selector=".mce-i-browse"/> - <element name="BrowseUploadImage" type="file" selector=".fileupload" /> + <element name="Browse" type="button" selector=".mce-i-browse" timeout="30"/> + <element name="BrowseUploadImage" type="file" selector=".fileupload" timeout="30"/> <element name="image" type="text" selector="//small[text()='{{var1}}']" parameterized="true"/> <element name="imageSelected" type="text" selector="//small[text()='{{var1}}']/parent::*[@class='filecnt selected']" parameterized="true"/> <element name="ImageSource" type="input" selector=".mce-combobox.mce-abs-layout-item.mce-last.mce-has-open" /> <element name="ImageDescription" type="input" selector=".mce-textbox.mce-abs-layout-item.mce-last" /> <element name="Height" type="input" selector=".mce-textbox.mce-abs-layout-item.mce-first" /> <element name="UploadImage" type="file" selector=".fileupload" /> - <element name="OkBtn" type="button" selector="//span[text()='Ok']"/> - <element name="InsertFile" type="text" selector="#insert_files"/> - <element name="CreateFolder" type="button" selector="#new_folder" /> - <element name="DeleteSelectedBtn" type="text" selector="#delete_files"/> - <element name="CancelBtn" type="button" selector="#cancel" /> + <element name="OkBtn" type="button" selector="//button//span[text()='Ok']"/> + <element name="InsertFile" type="text" selector="#insert_files" timeout="30"/> + <element name="CreateFolder" type="button" selector="#new_folder" timeout="30"/> + <element name="DeleteSelectedBtn" type="text" selector="#delete_files" timeout="30"/> + <element name="CancelBtn" type="button" selector=".page-actions #cancel" /> <element name="FolderName" type="button" selector="input[data-role='promptField']" /> - <element name="AcceptFolderName" type="button" selector=".action-primary.action-accept" /> + <element name="AcceptFolderName" type="button" selector=".action-primary.action-accept" timeout="30"/> <element name="StorageRootArrow" type="button" selector="#root > .jstree-icon" /> <element name="checkIfArrowExpand" type="button" selector="//li[@id='root' and contains(@class,'jstree-closed')]" /> <element name="WysiwygArrow" type="button" selector="#d3lzaXd5Zw-- > .jstree-icon" /> @@ -142,21 +168,21 @@ <element name="Numlist" type="button" selector="//div[@id='editorproduct_form_short_description']//i[@class='mce-ico mce-i-bullist']" /> <element name="Bullet" type="button" selector="//div[@id='editorproduct_form_short_description']//i[@class='mce-ico mce-i-numlist']" /> <element name="InsertLink" type="button" selector="//div[@id='editorproduct_form_short_description']//i[@class='mce-ico mce-i-link']" /> - <element name="InsertImageIcon" type="button" selector="//div[@id='editorproduct_form_short_description']//i[@class='mce-ico mce-i-image']" /> + <element name="InsertImageIcon" type="button" selector="//div[@id='editorproduct_form_short_description']//i[@class='mce-ico mce-i-image']" timeout="30"/> <element name="InsertTable" type="button" selector="//div[@id='editorproduct_form_short_description']//i[@class='mce-ico mce-i-table']" /> <element name="SpecialCharacter" type="button" selector="//div[@id='editorproduct_form_short_description']//i[@class='mce-ico mce-i-charmap']"/> <element name="Browse" type="button" selector=".mce-i-browse"/> - <element name="BrowseUploadImage" type="file" selector=".fileupload" /> + <element name="BrowseUploadImage" type="file" selector=".fileupload" timeout="30" /> <element name="image" type="text" selector="//small[text()='{{var1}}']" parameterized="true"/> <element name="imageSelected" type="text" selector="//small[text()='{{var1}}']/parent::*[@class='filecnt selected']" parameterized="true"/> <element name="ImageSource" type="input" selector=".mce-combobox.mce-abs-layout-item.mce-last.mce-has-open" /> <element name="ImageDescription" type="input" selector=".mce-textbox.mce-abs-layout-item.mce-last" /> <element name="Height" type="input" selector=".mce-textbox.mce-abs-layout-item.mce-first" /> <element name="UploadImage" type="file" selector=".fileupload" /> - <element name="OkBtn" type="button" selector="//span[text()='Ok']"/> + <element name="OkBtn" type="button" selector="//span[text()='Ok']" timeout="30"/> <element name="InsertFile" type="text" selector="#insert_files"/> <element name="CreateFolder" type="button" selector="#new_folder" /> - <element name="DeleteSelectedBtn" type="text" selector="#delete_files"/> + <element name="DeleteSelectedBtn" type="text" selector="#delete_files" timeout="30"/> <element name="CancelBtn" type="button" selector="#cancel" /> <element name="FolderName" type="button" selector="input[data-role='promptField']" /> <element name="AcceptFolderName" type="button" selector=".action-primary.action-accept" /> @@ -164,8 +190,22 @@ <element name="checkIfArrowExpand" type="button" selector="//li[@id='root' and contains(@class,'jstree-closed')]" /> <element name="confirmDelete" type="button" selector=".action-primary.action-accept" /> </section> + <section name="ProductDescriptionWysiwygSection"> + <element name="EditArea" type="text" selector="#editorproduct_form_description .mce-edit-area"/> + </section> + <section name="ProductShortDescriptionWysiwygSection"> + <element name="EditArea" type="text" selector="#editorproduct_form_short_description .mce-edit-area"/> + </section> <section name="AdminProductFormAdvancedPricingSection"> <element name="specialPrice" type="input" selector="input[name='product[special_price]']"/> <element name="doneButton" type="button" selector=".product_form_product_form_advanced_pricing_modal button.action-primary"/> + <element name="useDefaultPrice" type="checkbox" selector="//input[@name='product[special_price]']/parent::div/following-sibling::div/input[@name='use_default[special_price]']"/> + </section> + <section name="AdminProductAttributeSection"> + <element name="attributeSectionHeader" type="button" selector="//div[@data-index='attributes']" timeout="30"/> + <element name="textAttributeByCode" type="text" selector="//input[@name='product[{{arg}}]']" parameterized="true"/> + <element name="textAttributeByName" type="text" selector="//div[@data-index='attributes']//fieldset[contains(@class, 'admin__field') and .//*[contains(.,'{{var}}')]]//input" parameterized="true"/> + <element name="dropDownAttribute" type="select" selector="//select[@name='product[{{arg}}]']" parameterized="true" timeout="30"/> + <element name="attributeSection" type="block" selector="//div[@data-index='attributes']/div[contains(@class, 'admin__collapsible-content _show')]" timeout="30"/> </section> -</sections> +</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 611f12a39b51..939974248aab 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridFilterSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridFilterSection.xml @@ -28,8 +28,12 @@ <element name="priceFilterTo" type="input" selector="input.admin__control-text[name='price[to]']"/> <element name="typeFilter" type="select" selector="select.admin__control-select[name='type_id']"/> <element name="statusFilter" type="select" selector="select.admin__control-select[name='status']"/> + <element name="firstRowBySku" type="button" selector="//div[text()='{{var}}']/ancestor::tr" parameterized="true" timeout="30"/> <element name="newFromDateFilter" type="input" selector="input.admin__control-text[name='news_from_date[from]']"/> <element name="keywordSearch" type="input" selector="input#fulltext"/> <element name="keywordSearchButton" type="button" selector=".data-grid-search-control-wrap button.action-submit" timeout="30"/> + <element name="nthRow" type="block" selector=".data-row:nth-of-type({{var}})" parameterized="true" timeout="30"/> + <element name="productCount" type="text" selector="#catalog_category_products-total-count"/> + <element name="productPerPage" type="select" selector="#catalog_category_products_page-limit"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml index a7e20e22f1dd..07dd26381fe0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml @@ -8,6 +8,8 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminProductGridSection"> + <element name="productRowBySku" type="block" selector="//td[count(../../..//th[./*[.='SKU']]/preceding-sibling::th) + 1][./*[.='{{sku}}']]" parameterized="true" /> + <element name="productRowCheckboxBySku" type="block" selector="//td[count(../../..//th[./*[.='SKU']]/preceding-sibling::th) + 1][./*[.='{{sku}}']]/../td//input[@data-action='select-row']" parameterized="true" /> <element name="loadingMask" type="text" selector=".admin__data-grid-loading-mask[data-component*='product_listing']"/> <element name="columnHeader" type="button" selector="//div[@data-role='grid-wrapper']//table[contains(@class, 'data-grid')]/thead/tr/th[contains(@class, 'data-grid-th')]/span[text() = '{{label}}']" parameterized="true" timeout="30"/> <element name="column" type="text" selector="//tr//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., '{{column}}')]/preceding-sibling::th) +1 ]" parameterized="true"/> @@ -15,6 +17,8 @@ <element name="productGridElement2" type="text" selector="#addselector" /> <element name="productGridRows" type="text" selector="table.data-grid tr.data-row"/> <element name="firstProductRow" type="text" selector="table.data-grid tr.data-row:first-of-type"/> + <element name="firstProductRowName" type="text" selector="table.data-grid tr.data-row:first-of-type > td:nth-of-type(4)"/> + <element name="firstProductRowEditButton" type="button" selector="table.data-grid tr.data-row td .action-menu-item:first-of-type"/> <element name="productThumbnail" type="text" selector="table.data-grid tr:nth-child({{row}}) td.data-grid-thumbnail-cell > img" parameterized="true"/> <element name="productThumbnailBySrc" type="text" selector="img.admin__control-thumbnail[src*='{{pattern}}']" parameterized="true"/> <element name="productGridCell" type="text" selector="//tr[{{row}}]//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., '{{column}}')]/preceding-sibling::th) +1 ]" parameterized="true"/> @@ -28,6 +32,8 @@ <element name="firstRow" type="button" selector="tr.data-row:nth-of-type(1)"/> <element name="productGridCheckboxOnRow" type="checkbox" selector="//*[@id='container']//tr[{{row}}]/td[1]//input" parameterized="true"/> <element name="productGridNameProduct" type="input" selector="//tbody//tr//td//div[contains(., '{{var1}}')]" parameterized="true" timeout="30"/> + <element name="productGridContentsOnRow" type="checkbox" selector="//*[@id='container']//tr[{{row}}]/td" parameterized="true"/> <element name="selectRowBasedOnName" type="input" selector="//td/div[text()='{{var1}}']" parameterized="true"/> + <element name="changeStatus" type="button" selector="//div[contains(@class,'admin__data-grid-header-row') and contains(@class, 'row')]//div[contains(@class, 'action-menu-item')]//ul/li/span[text() = '{{status}}']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagePlaceholderConfigSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagePlaceholderConfigSection.xml new file mode 100644 index 000000000000..7558b13d624b --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagePlaceholderConfigSection.xml @@ -0,0 +1,37 @@ +<?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="AdminProductImagePlaceholderConfigSection"> + <element name="sectionHeader" type="text" selector="#catalog_placeholder-head"/> + <!--Base image placeholder--> + <element name="baseImageInput" type="file" selector="#catalog_placeholder_image_placeholder" timeout="10"/> + <element name="baseImageDelete" type="checkbox" selector="#catalog_placeholder_image_placeholder_delete"/> + <element name="baseImage" type="text" selector="#catalog_placeholder_image_placeholder_image"/> + <element name="baseImageBySrc" type="text" selector="#catalog_placeholder_image_placeholder_image[src*='{{var}}']" parameterized="true"/> + + <!--Small image placeholder--> + <element name="smallImageInput" type="file" selector="#catalog_placeholder_small_image_placeholder" timeout="10"/> + <element name="smallImageDelete" type="checkbox" selector="#catalog_placeholder_small_image_placeholder_delete"/> + <element name="smallImage" type="text" selector="#catalog_placeholder_small_image_placeholder_image"/> + <element name="smallImageBySrc" type="text" selector="#catalog_placeholder_small_image_placeholder_image[src*='{{var}}']" parameterized="true"/> + + <!--Swatch image placeholder--> + <element name="swatchImageInput" type="file" selector="#catalog_placeholder_swatch_image_placeholder" timeout="10"/> + <element name="swatchImageDelete" type="checkbox" selector="#catalog_placeholder_swatch_image_placeholder_delete"/> + <element name="swatchImage" type="text" selector="#catalog_placeholder_swatch_image_placeholder_image"/> + <element name="swatchImageBySrc" type="text" selector="#catalog_placeholder_swatch_image_placeholder_image[src*='{{var}}']" parameterized="true"/> + + <!--Thumbnail image placeholder--> + <element name="thumbnailImageInput" type="file" selector="#catalog_placeholder_thumbnail_placeholder" timeout="10"/> + <element name="thumbnailImageDelete" type="checkbox" selector="#catalog_placeholder_thumbnail_placeholder_delete"/> + <element name="thumbnailImage" type="text" selector="#catalog_placeholder_thumbnail_placeholder_image"/> + <element name="thumbnailImageBySrc" type="text" selector="#catalog_placeholder_thumbnail_placeholder_image[src*='{{var}}']" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagesSection.xml index eca0cb6f02ea..89eb1ed678cc 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagesSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagesSection.xml @@ -16,6 +16,7 @@ <element name="removeImageButton" type="button" selector=".action-remove"/> <element name="modalOkBtn" type="button" selector="button.action-primary.action-accept"/> <element name="uploadProgressBar" type="text" selector=".uploader .file-row"/> + <element name="productImagesToggleState" type="button" selector="[data-index='gallery'] > [data-state-collapsible='{{status}}']" parameterized="true"/> <element name="nthProductImage" type="button" selector="#media_gallery_content > div:nth-child({{var}}) img.product-image" parameterized="true"/> <element name="nthRemoveImageBtn" type="button" selector="#media_gallery_content > div:nth-child({{var}}) button.action-remove" parameterized="true"/> @@ -32,4 +33,4 @@ <element name="isThumbnailSelected" type="button" selector="//div[contains(@class, 'field-image-role')]//ul/li[contains(@class, 'selected')]/label[normalize-space(.) = 'Thumbnail']"/> <element name="isSwatchSelected" type="button" selector="//div[contains(@class, 'field-image-role')]//ul/li[contains(@class, 'selected')]/label[normalize-space(.) = 'Swatch']"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductModalSlideGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductModalSlideGridSection.xml index adc3a753f06f..dbdc82026947 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductModalSlideGridSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductModalSlideGridSection.xml @@ -10,5 +10,6 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminProductModalSlideGridSection"> <element name="productGridXRowYColumnButton" type="input" selector=".modal-slide table.data-grid tr.data-row:nth-child({{row}}) td:nth-child({{column}})" parameterized="true" timeout="30"/> + <element name="productRowCheckboxBySku" type="input" selector="//td[count(../../..//th[./*[.='SKU']]/preceding-sibling::th) + 1][./*[.='{{sku}}']]/../td//input[@data-action='select-row']" parameterized="true" /> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductRelatedUpSellCrossSellSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductRelatedUpSellCrossSellSection.xml index e90d806805f7..ef596bed186e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductRelatedUpSellCrossSellSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductRelatedUpSellCrossSellSection.xml @@ -9,7 +9,10 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminProductFormRelatedUpSellCrossSellSection"> + <element name="sectionHeader" type="block" selector=".fieldset-wrapper.admin__collapsible-block-wrapper[data-index='related']"/> <element name="AddRelatedProductsButton" type="button" selector="button[data-index='button_related']" timeout="30"/> + <element name="AddUpSellProductsButton" type="button" selector="button[data-index='button_upsell']" timeout="30"/> + <element name="AddCrossSellProductsButton" type="button" selector="button[data-index='button_crosssell']" timeout="30"/> <element name="relatedProductSectionText" type="text" selector=".fieldset-wrapper.admin__fieldset-section[data-index='related']"/> <element name="upSellProductSectionText" type="text" selector=".fieldset-wrapper.admin__fieldset-section[data-index='upsell']"/> <element name="crossSellProductSectionText" type="text" selector=".fieldset-wrapper.admin__fieldset-section[data-index='crosssell']"/> @@ -18,4 +21,8 @@ <element name="selectedRelatedProduct" type="block" selector="//span[@data-index='name']"/> <element name="removeRelatedProduct" type="button" selector="//span[text()='Related Products']//..//..//..//span[text()='{{productName}}']//..//..//..//..//..//button[@class='action-delete']" parameterized="true"/> </section> + <section name="AdminAddUpSellProductsModalSection"> + <element name="Modal" type="button" selector=".product_form_product_form_related_upsell_modal"/> + <element name="AddSelectedProductsButton" type="button" selector="//aside[contains(@class, 'product_form_product_form_related_upsell_modal')]//button/span[contains(text(), 'Add Selected Products')]" timeout="30"/> + </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductSEOSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductSEOSection.xml index c545fcd40883..53231a2a6863 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductSEOSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductSEOSection.xml @@ -11,5 +11,6 @@ <section name="AdminProductSEOSection"> <element name="sectionHeader" type="button" selector="div[data-index='search-engine-optimization']" timeout="30"/> <element name="urlKeyInput" type="input" selector="input[name='product[url_key]']"/> + <element name="useDefaultUrl" type="checkbox" selector="input[name='use_default[url_key]']"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection.xml index bf8812b3acef..53af1d5bd6eb 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection.xml @@ -38,4 +38,8 @@ <element name="description" type="input" selector="#description"/> </section> + <section name="AdminUpdateAttributesWebsiteSection"> + <element name="website" type="button" selector="#attributes_update_tabs_websites"/> + <element name="addProductToWebsite" type="checkbox" selector="#add-products-to-website-content .website-checkbox"/> + </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/CatalogSubmenuSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/CatalogSubmenuSection.xml new file mode 100644 index 000000000000..84a81c5204ac --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/CatalogSubmenuSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="CatalogSubmenuSection"> + <element name="products" type="button" selector="//li[@id='menu-magento-catalog-catalog']//li[@data-ui-id='menu-magento-catalog-catalog-products']"/> + </section> +</sections> diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/NewProductPageSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/NewProductPageSection.xml similarity index 81% rename from app/code/Magento/Braintree/Test/Mftf/Section/NewProductPageSection.xml rename to app/code/Magento/Catalog/Test/Mftf/Section/NewProductPageSection.xml index 42e451940c91..b98bd47b5413 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Section/NewProductPageSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/NewProductPageSection.xml @@ -7,7 +7,7 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="NewProductPageSection"> <element name="productName" type="input" selector="//input[@name='product[name]']"/> <element name="sku" type="input" selector="//input[@name='product[sku]']"/> diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/ProductsPageSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/ProductsPageSection.xml similarity index 84% rename from app/code/Magento/Braintree/Test/Mftf/Section/ProductsPageSection.xml rename to app/code/Magento/Catalog/Test/Mftf/Section/ProductsPageSection.xml index 267efdf3d0e5..ea37eb59b67f 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Section/ProductsPageSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/ProductsPageSection.xml @@ -7,7 +7,7 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="ProductsPageSection"> <element name="addProductButton" type="button" selector="//button[@id='add_new_product-button']"/> <element name="checkboxForProduct" type="button" selector="//*[contains(text(),'{{args}}')]/parent::td/preceding-sibling::td/label[@class='data-grid-checkbox-cell-inner']" parameterized="true"/> @@ -15,6 +15,5 @@ <element name="delete" type="button" selector="//*[contains(@class,'admin__data-grid-header-row row row-gutter')]//*[text()='Delete']"/> <element name="ok" type="button" selector="//button[@data-role='action']//span[text()='OK']"/> <element name="deletedSuccessMessage" type="button" selector="//*[@class='message message-success success']"/> - </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryBottomToolbarSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryBottomToolbarSection.xml new file mode 100644 index 000000000000..7ce795c78f25 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryBottomToolbarSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCategoryBottomToolbarSection"> + <element name="nextPage" type="button" selector=".//*[@class='toolbar toolbar-products'][2]//a[contains(@class, 'next')]" timeout="30"/> + <element name="previousPage" type="button" selector=".//*[@class='toolbar toolbar-products'][2]//a[contains(@class, 'previous')]" timeout="30"/> + <element name="pageNumber" type="text" selector="//*[@class='toolbar toolbar-products'][2]//a[contains(@class, 'page')]//span[2][contains(text() ,'{{var1}}')]" parameterized="true"/> + <element name="perPage" type="select" selector="//*[@class='toolbar toolbar-products'][2]//select[@id='limiter']"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml index d484abf2069f..1cd64544d963 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml @@ -9,11 +9,15 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontCategoryMainSection"> + <element name="perPage" type="select" selector="//*[@id='authenticationPopup']/following-sibling::div[3]//*[@id='limiter']"/> + <element name="sortedBy" type="select" selector="//*[@id='authenticationPopup']/following-sibling::div[1]//*[@id='sorter']"/> + <element name="modeGridIsActive" type="text" selector="//*[@id='authenticationPopup']/following-sibling::div[1]//*[@class='modes']/strong[@class='modes-mode active mode-grid']/span"/> <element name="modeListButton" type="button" selector="#mode-list"/> <element name="CategoryTitle" type="text" selector="#page-title-heading span"/> <element name="ProductItemInfo" type="button" selector=".product-item-info"/> <element name="specifiedProductItemInfo" type="button" selector="//a[@class='product-item-link'][contains(text(), '{{var1}}')]" parameterized="true"/> <element name="AddToCartBtn" type="button" selector="button.action.tocart.primary"/> + <element name="addToCartProductBySku" type="button" selector="//form[@data-product-sku='{{productSku}}']//button[contains(@class, 'tocart')]" parameterized="true" /> <element name="SuccessMsg" type="button" selector="div.message-success"/> <element name="productCount" type="text" selector="#toolbar-amount"/> <element name="CatalogDescription" type="text" selector="//div[@class='category-description']//p"/> @@ -22,9 +26,13 @@ <element name="productImage" type="text" selector="img.product-image-photo"/> <element name="productLink" type="text" selector="a.product-item-link"/> <element name="productLinkByHref" type="text" selector="a.product-item-link[href$='{{var1}}.html']" parameterized="true"/> - <element name="productPrice" type="text" selector="div.price-box.price-final_price"/> + <element name="productPrice" type="text" selector=".price-final_price"/> <element name="categoryImage" type="text" selector=".category-image"/> <element name="emptyProductMessage" type="block" selector=".message.info.empty>div"/> <element name="lineProductName" type="text" selector=".products.list.items.product-items li:nth-of-type({{line}}) .product-item-link" timeout="30" parameterized="true"/> + <element name="asLowAs" type="input" selector="//*[@class='price-box price-final_price']/a/span[@class='price-container price-final_price tax weee']"/> + <element name="productsList" type="block" selector="#maincontent .column.main"/> + <element name="productName" type="text" selector=".product-item-name"/> + <element name="productOptionList" type="text" selector="#narrow-by-list"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml index 7ea74d791375..51b5a0242d97 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml @@ -16,6 +16,7 @@ <element name="ProductInfoByNumber" type="text" selector="//main//li[{{var1}}]//div[@class='product-item-info']" parameterized="true"/> <element name="ProductAddToCompareByNumber" type="text" selector="//main//li[{{var1}}]//a[contains(@class, 'tocompare')]" parameterized="true"/> <element name="listedProduct" type="block" selector="ol li:nth-child({{productPositionInList}}) img" parameterized="true"/> + <element name="ProductImageByNumber" type="button" selector="//main//li[{{var1}}]//img" parameterized="true"/> <element name="categoryListView" type="button" selector="a[title='List']" timeout="30"/> <element name="ProductTitleByName" type="button" selector="//main//li//a[contains(text(), '{{var1}}')]" parameterized="true"/> @@ -28,7 +29,9 @@ <element name="productPriceLabel" type="text" selector="//span[@class='price-label'][contains(text(),'{{var1}}')]" parameterized="true"/> <element name="productPriceLinkAfterLabel" type="text" selector="//span[@class='price-label'][contains(text(),'{{var1}}')]/following::span[contains(text(), '{{var2}}')]" parameterized="true"/> <element name="ProductAddToCartByName" type="button" selector="//main//li[.//a[contains(text(), '{{var1}}')]]//a[contains(@class, 'tocart')]" parameterized="true"/> - <element name="ProductAddToCompareByName" type="text" selector="//main//li[.//a[contains(text(), '{{var1}}')]]//a[contains(@class, 'tocompare')]" parameterized="true"/> + <!--<element name="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/StorefrontCategorySidebarSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection.xml index 0599a42bfd7a..1b7bbd58eea9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection.xml @@ -13,4 +13,7 @@ <element name="filterOption" type="text" selector=".filter-options-content .item"/> <element name="optionQty" type="text" selector=".filter-options-content .item .count"/> </section> + <section name="StorefrontCategorySidebarMobileSection"> + <element name="shopByButton" type="button" selector="//div[contains(@class, 'filter-title')]/strong[contains(text(), 'Shop By')]"/> + </section> </sections> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontHeaderSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontHeaderSection.xml index 509ad2b8f849..52a377ad264c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontHeaderSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontHeaderSection.xml @@ -9,6 +9,6 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontHeaderSection"> - <element name="NavigationCategoryByName" type="button" selector="//nav//a[span[contains(., '{{var1}}')]]" parameterized="true"/> + <element name="NavigationCategoryByName" type="button" selector="//nav//a[span[contains(., '{{var1}}')]]" parameterized="true" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMessagesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMessagesSection.xml index dea1b2a5af75..c58479a7b73e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMessagesSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMessagesSection.xml @@ -10,5 +10,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontMessagesSection"> <element name="success" type="text" selector="div.message-success.success.message"/> + <element name="error" type="text" selector="div.message-error.error.message"/> + <element name="noticeMessage" type="text" selector="div.message.notice div"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMiniCartSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMiniCartSection.xml new file mode 100644 index 000000000000..c6ea96715cf8 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontMiniCartSection.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="StorefrontMiniCartSection"> + <element name="quantity" type="button" selector="span.counter-number"/> + <element name="show" type="button" selector="a.showcart"/> + <element name="goToCheckout" type="button" selector="#top-cart-btn-checkout" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontNavigationSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontNavigationSection.xml index e8f35fc6787b..292b2d7008bc 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontNavigationSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontNavigationSection.xml @@ -11,5 +11,7 @@ <element name="topCategory" type="button" selector="//a[contains(@class,'level-top')]/span[contains(text(),'{{var1}}')]" parameterized="true"/> <element name="subCategory" type="button" selector="//ul[contains(@class,'submenu')]//span[contains(text(),'{{var1}}')]" parameterized="true"/> <element name="breadcrumbs" type="textarea" selector=".items"/> + <element name="categoryBreadcrumbs" type="textarea" selector=".breadcrumbs li"/> + <element name="categoryBreadcrumbsByNumber" type="textarea" selector=".breadcrumbs li:nth-of-type({{number}})" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml index b93a70559fc4..8393cee57996 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -13,13 +13,15 @@ <element name="productName" type="text" selector=".base"/> <element name="productSku" type="text" selector=".product.attribute.sku>.value"/> <element name="productPriceLabel" type="text" selector=".price-label"/> - <element name="productPrice" type="text" selector="div.price-box.price-final_price"/> + <element name="price" type="text" selector=".product-info-main [data-price-type='finalPrice']"/> + <element name="productPrice" type="text" selector=".price-final_price"/> <element name="qty" type="input" selector="#qty"/> <element name="specialPrice" type="text" selector=".special-price"/> + <element name="specialPriceAmount" type="text" selector=".special-price span.price"/> <element name="updatedPrice" type="text" selector="div.price-box.price-final_price [data-price-type='finalPrice'] .price"/> <element name="oldPrice" type="text" selector=".old-price"/> <element name="oldPriceTag" type="text" selector=".old-price .price-label"/> - <element name="oldPriceAmount" type="text" selector=".old-price .price"/> + <element name="oldPriceAmount" type="text" selector=".old-price span.price"/> <element name="productStockStatus" type="text" selector=".stock[title=Availability]>span"/> <element name="productImage" type="text" selector="//*[@id='maincontent']//div[@class='gallery-placeholder']//img[@class='fotorama__img']"/> <element name="productImageSrc" type="text" selector="//*[@id='maincontent']//div[@class='gallery-placeholder']//img[contains(@src, '{{src}}')]" parameterized="true"/> @@ -28,14 +30,17 @@ <element name="productOptionAreaInput" type="textarea" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//label[contains(.,'{{var1}}')]/../div[@class='control']//textarea" parameterized="true"/> <element name="productOptionFile" type="file" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//label[contains(.,'OptionFile')]/../div[@class='control']//input[@type='file']" parameterized="true"/> <element name="productOptionSelect" type="select" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//label[contains(.,'{{var1}}')]/../div[@class='control']//select" parameterized="true"/> - + <element name="asLowAs" type="input" selector="span[class='price-wrapper '] "/> + <element name="specialPriceValue" type="text" selector="//span[@class='special-price']//span[@class='price']"/> + <element name="mapPrice" type="text" selector="//div[@class='price-box price-final_price']//span[contains(@class, 'price-msrp_price')]"/> + <element name="clickForPriceLink" type="text" selector="//div[@class='price-box price-final_price']//a[contains(text(), 'Click for price')]"/> <!-- The parameter is the nth custom option that you want to get --> <element name="nthCustomOption" type="block" selector="//*[@id='product-options-wrapper']/*[@class='fieldset']/*[contains(@class, 'field')][{{customOptionNum}}]" parameterized="true" /> + <!-- The 1st parameter is the nth custom option, the 2nd parameter is the nth value in the option --> <element name="nthCustomOptionInput" type="radio" selector="//*[@id='product-options-wrapper']/*[@class='fieldset']/*[contains(@class, 'field')][{{customOptionNum}}]//*[contains(@class, 'admin__field-option')][{{customOptionValueNum}}]//input" parameterized="true" /> <element name="productOptionRadioButtonsCheckbox" type="checkbox" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//label[contains(.,'{{var1}}')]/../div[@class='control']//input[@price='{{var2}}']" parameterized="true"/> - <element name="productOptionDataMonth" type="date" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//legend[contains(.,'{{var1}}')]/../div[@class='control']//select[@data-calendar-role='month']" parameterized="true"/> <element name="productOptionDataDay" type="date" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//legend[contains(.,'{{var1}}')]/../div[@class='control']//select[@data-calendar-role='day']" parameterized="true"/> <element name="productOptionDataYear" type="date" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//legend[contains(.,'{{var1}}')]/../div[@class='control']//select[@data-calendar-role='year']" parameterized="true"/> @@ -49,7 +54,6 @@ <!-- Only one of Upload/Url Inputs are available for File and Sample depending on the value of the corresponding TypeSelector --> <element name="addLinkFileUploadFile" type="file" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//label[contains(.,'{{var1}}')]/../div[@class='control']//input[@type='file']" parameterized="true" /> - <element name="productShortDescription" type="text" selector="//div[@class='product attribute overview']//div[@class='value']"/> <element name="productAttributeTitle1" type="text" selector="#product-options-wrapper div[tabindex='0'] label"/> <element name="productAttributeOptions1" type="select" selector="#product-options-wrapper div[tabindex='0'] option"/> @@ -66,8 +70,21 @@ <element name="productOptionDropDownOptionTitle" type="text" selector="//label[contains(.,'{{var1}}')]/../div[@class='control']//select//option[contains(.,'{{var2}}')]" parameterized="true"/> <!-- Tier price selectors --> + <element name="tierPriceText" type="text" selector=".prices-tier li[class='item']" /> <element name="productTierPriceByForTextLabel" type="text" selector="//ul[contains(@class, 'prices-tier')]//li[{{var1}}][contains(text(),'Buy {{var2}} for')]" parameterized="true"/> <element name="productTierPriceAmount" type="text" selector="//ul[contains(@class, 'prices-tier')]//li[{{var1}}]//span[contains(text(), '{{var2}}')]" parameterized="true"/> <element name="productTierPriceSavePercentageAmount" type="text" selector="//ul[contains(@class, 'prices-tier')]//li[{{var1}}]//span[contains(@class, 'percent')][contains(text(), '{{var2}}')]" parameterized="true"/> + + <!-- Special price selectors --> + <element name="productSpecialPrice" type="text" selector="//span[@data-price-type='finalPrice']/span"/> + <element name="specialProductText" type="text" selector="//span[text()='Regular Price']"/> + <element name="oldProductPrice" type="text" selector="//span[@data-price-type='oldPrice']/span"/> + + <!-- Customizable Option selectors --> + <element name="allCustomOptionLabels" type="text" selector="#product-options-wrapper label"/> + <element name="customOptionLabel" type="text" selector="//label[contains(., '{{customOptionTitle}}')]" parameterized="true"/> + <element name="customSelectOptions" type="select" selector="#{{selectId}} option" parameterized="true"/> + <element name="requiredCustomInput" type="text" selector="//div[contains(.,'{{customOptionTitle}}') and contains(@class, 'required') and .//input[@aria-required='true']]" parameterized="true"/> + <element name="requiredCustomSelect" type="select" selector="//div[contains(.,'{{customOptionTitle}}') and contains(@class, 'required') and .//select[@aria-required='true']]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml index 83c3ca534860..ea10e12fb73f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml @@ -9,6 +9,12 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontProductMediaSection"> + <element name="gallerySpinner" type="block" selector="#maincontent .fotorama__spinner--show" /> + <element name="gallery" type="block" selector="[data-gallery-role='gallery']" /> + <element name="productImage" type="text" selector="//*[@data-gallery-role='gallery' and not(contains(@class, 'fullscreen'))]//img[contains(@src, '{{filename}}') and not(contains(@class, 'full'))]" parameterized="true" /> + <element name="productImageFullscreen" type="text" selector="//*[@data-gallery-role='gallery' and contains(@class, 'fullscreen')]//img[contains(@src, '{{filename}}') and contains(@class, 'full')]" parameterized="true" /> + <element name="closeFullscreenImage" type="button" selector="//*[@data-gallery-role='gallery' and contains(@class, 'fullscreen')]//*[@data-gallery-role='fotorama__fullscreen-icon']" /> <element name="imageFile" type="text" selector="//*[@class='product media']//img[contains(@src, '{{filename}}')]" parameterized="true"/> + <element name="productImageActive" type="text" selector=".product.media div[data-active=true] > img[src*='{{filename}}']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml index c87af1224ed3..8055ecfe00cd 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml @@ -9,14 +9,19 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontProductPageSection"> - <element name="qtyInput" type="button" selector="input.input-text.qty"/> + <element name="qtyInput" type="button" selector="input.input-text.qty" timeout="30"/> <element name="addToCartBtn" type="button" selector="button.action.tocart.primary" timeout="30"/> - <element name="successMsg" type="button" selector="div.message-success"/> - <element name="errorMsg" type="button" selector="div.message-error"/> + <element name="successMsg" type="button" selector="div.message-success" timeout="30"/> + <element name="errorMsg" type="button" selector="div.message-error" timeout="30"/> <element name="alertMessage" type="text" selector=".page.messages [role=alert]"/> - <element name="messagesBlock" type="text" selector=".page.messages"/> + <element name="messagesBlock" type="text" selector=".page.messages" timeout="30"/> <element name="addToWishlist" type="button" selector="//a[@class='action towishlist']" timeout="30"/> <element name="customTextOptionInput" type="input" selector=".input-text.product-custom-option"/> <element name="charCounter" type="text" selector=".character-counter"/> + <element name="tax" type="input" selector=".totals-tax .amount .price"/> + <element name="subTotal" type="input" selector="span[data-th='Subtotal']"/> + <element name="shipping" type="input" selector="span[data-th='Shipping']"/> + <element name="orderTotal" type="input" selector=".grand.totals .amount .price"/> + <element name="customOptionDropDown" type="select" selector="//*[@id='product-options-wrapper']//select[contains(@class, 'product-custom-option admin__control-select')]"/> </section> -</sections> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductUpSellProductsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductUpSellProductsSection.xml new file mode 100644 index 000000000000..f00abbe3c58c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductUpSellProductsSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontProductUpSellProductsSection"> + <element name="upSellHeading" type="text" selector="#block-upsell-heading"/> + <element name="upSellProducts" type="text" selector="div.upsell .product-item-name"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontWidgetsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontWidgetsSection.xml new file mode 100644 index 000000000000..87aab45bd8cb --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontWidgetsSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontWidgetsSection"> + <element name="widgetRecentlyViewedProductsGrid" type="block" selector=".block.widget.block-viewed-products-grid"/> + <element name="widgetRecentlyComparedProductsGrid" type="block" selector=".block.widget.block-compared-products-grid"/> + <element name="widgetRecentlyOrderedProductsGrid" type="block" selector=".block.block-reorder"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AddOutOfStockProductToCompareListTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AddOutOfStockProductToCompareListTest.xml new file mode 100644 index 000000000000..4f66395bd0fb --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AddOutOfStockProductToCompareListTest.xml @@ -0,0 +1,94 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AddOutOfStockProductToCompareListTest"> + <annotations> + <features value="Catalog"/> + <title value="Add out of stock product to compare list"/> + <description value="Add out of stock product to compare list"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-98644"/> + <useCaseId value="MAGETWO-98522"/> + <group value="Catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <magentoCLI command="config:set cataloginventory/options/show_out_of_stock 0" stepKey="displayOutOfStockNo"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <createData entity="SimpleSubCategory" stepKey="category"/> + <createData entity="SimpleProduct4" stepKey="product"> + <requiredEntity createDataKey="category"/> + </createData> + </before> + <after> + <magentoCLI command="config:set cataloginventory/options/show_out_of_stock 0" stepKey="displayOutOfStockNo2"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <deleteData createDataKey="product" stepKey="deleteProduct"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Open product page--> + <comment userInput="Open product page" stepKey="openProdPage"/> + <amOnPage url="{{StorefrontProductPage.url($$product.name$$)}}" stepKey="goToSimpleProductPage"/> + <waitForPageLoad stepKey="waitForSimpleProductPage"/> + <!--'Add to compare' link is not available--> + <comment userInput="'Add to compare' link is not available" stepKey="addToCompareLinkAvailability"/> + <dontSeeElement selector="{{StorefrontProductInfoMainSection.productAddToCompare}}" stepKey="dontSeeAddToCompareLink"/> + <!--Turn on 'out on stock' config--> + <comment userInput="Turn on 'out of stock' config" stepKey="onOutOfStockConfig"/> + <magentoCLI command="config:set cataloginventory/options/show_out_of_stock 1" stepKey="displayOutOfStockYes"/> + <!--Clear cache and reindex--> + <comment userInput="Clear cache and reindex" stepKey="cleanCache"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!--Open product page--> + <comment userInput="Open product page" stepKey="openProductPage"/> + <amOnPage url="{{StorefrontProductPage.url($$product.name$$)}}" stepKey="goToSimpleProductPage2"/> + <waitForPageLoad stepKey="waitForSimpleProductPage2"/> + <!--Click on 'Add to Compare' link--> + <comment userInput="Click on 'Add to Compare' link" stepKey="clickOnAddToCompareLink"/> + <click selector="{{StorefrontProductInfoMainSection.productAddToCompare}}" stepKey="clickOnAddToCompare"/> + <waitForPageLoad stepKey="waitForProdAddToCmpList"/> + <!--Assert success message--> + <comment userInput="Assert success message" stepKey="assertSuccessMsg"/> + <grabTextFrom selector="{{AdminProductMessagesSection.successMessage}}" stepKey="grabTextFromSuccessMessage"/> + <assertEquals expected='You added product $$product.name$$ to the comparison list.' expectedType="string" actual="($grabTextFromSuccessMessage)" stepKey="assertSuccessMessage"/> + <!--See product in the comparison list--> + <comment userInput="See product in the comparison list" stepKey="seeProductInComparisonList"/> + <amOnPage url="{{StorefrontProductComparePage.url}}" stepKey="navigateToComparePage"/> + <waitForPageLoad stepKey="waitForStorefrontProductComparePageLoad"/> + <seeElement selector="{{StorefrontProductCompareMainSection.ProductLinkByName($product.name$)}}" stepKey="seeProductInCompareList"/> + <!--Go to Category page and delete product from comparison list--> + <comment userInput="Go to Category page and delete prduct from comparison list" stepKey="deletProdFromCmpList"/> + <amOnPage url="{{StorefrontCategoryPage.url($$category.name$$)}}" stepKey="onCategoryPage"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad1"/> + <click selector="{{StorefrontComparisonSidebarSection.ClearAll}}" stepKey="clickClearAll"/> + <waitForPageLoad time="30" stepKey="waitForConfirmPageLoad"/> + <click selector="{{AdminDeleteRoleSection.confirm}}" stepKey="confirmProdDelate"/> + <waitForPageLoad time="30" stepKey="waitForConfirmLoad"/> + <!--Add product to compare list from Category page--> + <comment userInput="Add product to compare list fom Category page" stepKey="addToCmpFromCategPage"/> + <moveMouseOver selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" stepKey="hoverOverProduct"/> + <click selector="{{StorefrontProductInfoMainSection.productAddToCompare}}" stepKey="clickAddToCompare"/> + <waitForPageLoad stepKey="waitProdAddingToCmpList"/> + <!--Assert success message--> + <comment userInput="Assert success message" stepKey="assertSuccessMsg2"/> + <grabTextFrom selector="{{AdminProductMessagesSection.successMessage}}" stepKey="grabTextFromSuccessMessage2"/> + <assertEquals expected='You added product $$product.name$$ to the comparison list.' expectedType="string" actual="($grabTextFromSuccessMessage)" stepKey="assertSuccessMessage2"/> + <!--Check that product displays on add to compare widget--> + <comment userInput="Check that product displays on add to compare widget" stepKey="checkProdNameOnWidget"/> + <seeElement selector="{{StorefrontComparisonSidebarSection.ProductTitleByName($$product.name$$)}}" stepKey="seeProdNameOnCmpWidget"/> + <!--See product in the compare page--> + <comment userInput="See product in the compare page" stepKey="seeProductInComparePage"/> + <amOnPage url="{{StorefrontProductComparePage.url}}" stepKey="navigateToComparePage2"/> + <waitForPageLoad stepKey="waitForStorefrontProductComparePageLoad2"/> + <seeElement selector="{{StorefrontProductCompareMainSection.ProductLinkByName($product.name$)}}" stepKey="seeProductInCompareList2"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AddToCartCrossSellTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AddToCartCrossSellTest.xml new file mode 100644 index 000000000000..53bb12fda483 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AddToCartCrossSellTest.xml @@ -0,0 +1,93 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AddToCartCrossSellTest"> + <annotations> + <features value="Catalog"/> + <stories value="Promote Products as Cross-Sells"/> + <title value="Admin should be able to add cross-sell to products."/> + <description value="Create products, add products to cross sells, and check that they appear in the Shopping Cart page."/> + <severity value="MAJOR"/> + <testCaseId value="MC-9143"/> + <group value="Catalog"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category1"/> + <createData entity="_defaultProduct" stepKey="simpleProduct1"> + <requiredEntity createDataKey="category1"/> + </createData> + <createData entity="_defaultProduct" stepKey="simpleProduct2"> + <requiredEntity createDataKey="category1"/> + </createData> + <createData entity="_defaultProduct" stepKey="simpleProduct3"> + <requiredEntity createDataKey="category1"/> + </createData> + + <actionGroup ref="LoginAsAdmin" stepKey="logInAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + + <deleteData createDataKey="simpleProduct1" stepKey="deleteSimp1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteSimp2"/> + <deleteData createDataKey="simpleProduct3" stepKey="deleteSimp3"/> + <deleteData createDataKey="category1" stepKey="deleteCategory"/> + </after> + + <!-- Go to simpleProduct1, add simpleProduct2 and simpleProduct3 as cross-sell--> + <amOnPage url="{{AdminProductEditPage.url($simpleProduct1.id$)}}" stepKey="goToProduct1"/> + <click stepKey="openHeader1" selector="{{AdminProductFormRelatedUpSellCrossSellSection.sectionHeader}}"/> + + <actionGroup ref="addCrossSellProductBySku" stepKey="addProduct2ToSimp1"> + <argument name="sku" value="$simpleProduct2.sku$"/> + </actionGroup> + <actionGroup ref="addCrossSellProductBySku" stepKey="addProduct3ToSimp1"> + <argument name="sku" value="$simpleProduct3.sku$"/> + </actionGroup> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + + <!-- Go to simpleProduct3, add simpleProduct1 and simpleProduct2 as cross-sell--> + <amOnPage url="{{AdminProductEditPage.url($simpleProduct3.id$)}}" stepKey="goToProduct3"/> + <click stepKey="openHeader2" selector="{{AdminProductFormRelatedUpSellCrossSellSection.sectionHeader}}"/> + + <actionGroup ref="addCrossSellProductBySku" stepKey="addProduct1ToSimp3"> + <argument name="sku" value="$simpleProduct1.sku$"/> + </actionGroup> + <actionGroup ref="addCrossSellProductBySku" stepKey="addProduct2ToSimp3"> + <argument name="sku" value="$simpleProduct2.sku$"/> + </actionGroup> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave2"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + + <!-- Go to frontend, add simpleProduct1 to cart--> + <actionGroup ref="AddSimpleProductToCart" stepKey="addSimp1ToCart"> + <argument name="product" value="$simpleProduct1$"/> + </actionGroup> + + <!-- Check that cart page contains cross-sell to simpleProduct2 and simpleProduct3--> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCart1"/> + <waitForPageLoad stepKey="waitForCartToLoad"/> + <waitForElementVisible selector="{{CheckoutCartCrossSellSection.products}}" stepKey="waitForCrossSellLoading"/> + <see stepKey="seeProduct2InCrossSell" selector="{{CheckoutCartCrossSellSection.products}}" userInput="$simpleProduct2.name$"/> + <see stepKey="seeProduct3InCrossSell" selector="{{CheckoutCartCrossSellSection.products}}" userInput="$simpleProduct3.name$"/> + + <!-- Add simpleProduct3 to cart, check cross-sell contains product2 but not product3--> + <click stepKey="addSimp3ToCart" selector="{{CheckoutCartCrossSellSection.productRowByName($simpleProduct3.name$)}}{{CheckoutCartCrossSellSection.addToCart}}"/> + <waitForPageLoad stepKey="waitForCartToLoad2"/> + <see stepKey="seeProduct2StillInCrossSell" selector="{{CheckoutCartCrossSellSection.products}}" userInput="$simpleProduct2.name$"/> + <dontSee stepKey="dontSeeProduct3InCrossSell" selector="{{CheckoutCartCrossSellSection.products}}" userInput="$simpleProduct3.name$"/> + + <!-- Add simpleProduct2 to cart, check cross-sell doesn't contain product 2 anymore.--> + <click stepKey="addSimp2ToCart" selector="{{CheckoutCartCrossSellSection.productRowByName($simpleProduct2.name$)}}{{CheckoutCartCrossSellSection.addToCart}}"/> + <waitForPageLoad stepKey="waitForCartToLoad3"/> + <dontSee stepKey="dontSeeProduct2InCrossSell" selector="{{CheckoutCartCrossSellSection.products}}" userInput="$simpleProduct2.name$"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml index 88a39a9087bb..117f094ee060 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml @@ -43,7 +43,9 @@ <actionGroup ref="saveProductForm" stepKey="saveSimpleProduct"/> <!-- Assert product image in admin product form --> - <actionGroup ref="assertProductImageAdminProductPage" stepKey="assertProductImageAdminProductPage"/> + <actionGroup ref="assertProductImageAdminProductPage" stepKey="assertProductImageAdminProductPage"> + <argument name="image" value="MagentoLogo"/> + </actionGroup> <!-- Assert product in storefront product page --> <actionGroup ref="AssertProductInStorefrontProductPage" stepKey="AssertProductInStorefrontProductPage"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultVideoSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultVideoSimpleProductTest.xml index 5456cb02e74c..f657fbbdae60 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultVideoSimpleProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultVideoSimpleProductTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminAddDefaultVideoSimpleProductTest"> <annotations> <features value="Catalog"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultVideoVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultVideoVirtualProductTest.xml index f48c352c5290..eab36bc90dc1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultVideoVirtualProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultVideoVirtualProductTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminAddDefaultVideoVirtualProductTest" extends="AdminAddDefaultVideoSimpleProductTest"> <annotations> <features value="Catalog"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml index a280433e315f..03f3e93bb30e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml @@ -16,85 +16,77 @@ <description value="Admin should be able to add image to WYSIWYG Editor on Product Page"/> <severity value="CRITICAL"/> <testCaseId value="MAGETWO-84375"/> - <skip> - <issueId value="MAGETWO-94438"/> - </skip> </annotations> <before> <actionGroup ref="LoginActionGroup" stepKey="login"/> <actionGroup ref="EnabledWYSIWYG" stepKey="enableWYSIWYG"/> <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> </before> + <after> + <actionGroup ref="DisabledWYSIWYG" stepKey="disableWYSIWYG"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <amOnPage url="{{AdminProductCreatePage.url(AddToDefaultSet.attributeSetId, 'simple')}}" stepKey="navigateToNewProduct"/> - <waitForPageLoad stepKey="waitForPageLoad"/> - <fillField userInput="{{_defaultProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="fillName"/> - <fillField userInput="{{_defaultProduct.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="fillPrice"/> - <fillField userInput="{{_defaultProduct.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="fillSKU"/> - <fillField userInput="{{_defaultProduct.quantity}}" selector="{{AdminProductFormSection.productQuantity}}" stepKey="fillQuantity"/> - <scrollTo selector="{{AdminProductFormSection.productQuantity}}" stepKey="scrollToQty" /> + <waitForPageLoad stepKey="waitForPageLoadProductCreatePage"/> + <actionGroup ref="fillMainProductForm" stepKey="fillBasicProductInfo" /> + <click selector="{{AdminProductFormSection.contentTab}}" stepKey="clickContentTab" /> <waitForElementVisible selector="{{ProductDescriptionWYSIWYGToolbarSection.TinyMCE4}}" stepKey="waitForDescription" /> <click selector="{{ProductDescriptionWYSIWYGToolbarSection.InsertImageIcon}}" stepKey="clickInsertImageIcon1" /> - <waitForPageLoad stepKey="waitForPageLoad1" /> <click selector="{{ProductDescriptionWYSIWYGToolbarSection.Browse}}" stepKey="clickBrowse1" /> - <waitForPageLoad stepKey="waitForPageLoad2" /> - <waitForLoadingMaskToDisappear stepKey="waitForLoading1" /> - <waitForLoadingMaskToDisappear stepKey="waitForLoading2" /> + <waitForLoadingMaskToDisappear stepKey="waitForBrowseModal" /> <see selector="{{ProductDescriptionWYSIWYGToolbarSection.CancelBtn}}" userInput="Cancel" stepKey="seeCancelBtn1" /> <see selector="{{ProductDescriptionWYSIWYGToolbarSection.CreateFolder}}" userInput="Create Folder" stepKey="seeCreateFolderBtn1" /> - <dontSeeElement selector="{{ProductDescriptionWYSIWYGToolbarSection.InsertFile}}" stepKey="dontSeeAddSelectedBtn" /> - <click selector="{{ProductDescriptionWYSIWYGToolbarSection.CreateFolder}}" stepKey="createFolder1"/> - <waitForElementVisible selector="{{ProductDescriptionWYSIWYGToolbarSection.FolderName}}" stepKey="waitForPopUp1" /> + <dontSeeElement selector="{{ProductDescriptionWYSIWYGToolbarSection.InsertFile}}" stepKey="dontSeeAddSelectedBtn1" /> + <click selector="{{ProductDescriptionWYSIWYGToolbarSection.CreateFolder}}" stepKey="createFolder1" /> + <waitForElement selector="{{ProductDescriptionWYSIWYGToolbarSection.FolderName}}" stepKey="waitForPopUp1" /> <fillField selector="{{ProductDescriptionWYSIWYGToolbarSection.FolderName}}" userInput="{{ImageFolder.name}}" stepKey="fillFolderName1" /> <click selector="{{ProductDescriptionWYSIWYGToolbarSection.AcceptFolderName}}" stepKey="acceptFolderName11" /> - <waitForLoadingMaskToDisappear stepKey="waitForLoading3" /> <conditionalClick selector="{{ProductDescriptionWYSIWYGToolbarSection.StorageRootArrow}}" dependentSelector="{{ProductDescriptionWYSIWYGToolbarSection.checkIfArrowExpand}}" stepKey="clickStorageRootArrowIfClosed" visible="true"/> <conditionalClick selector="{{ProductDescriptionWYSIWYGToolbarSection.WysiwygArrow}}" dependentSelector="{{ProductDescriptionWYSIWYGToolbarSection.checkIfWysiwygArrowExpand}}" stepKey="clickWysiwygArrowIfClosed" visible="true"/> <waitForText userInput="{{ImageFolder.name}}" stepKey="waitForNewFolder1" /> <click userInput="{{ImageFolder.name}}" stepKey="clickOnCreatedFolder1" /> - <waitForLoadingMaskToDisappear stepKey="waitForLoading4" /> + <waitForLoadingMaskToDisappear stepKey="waitForLoading4" timeout="45"/> <attachFile selector="{{ProductDescriptionWYSIWYGToolbarSection.BrowseUploadImage}}" userInput="{{ImageUpload1.value}}" stepKey="uploadImage1"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoading5" /> + <waitForLoadingMaskToDisappear stepKey="waitForFileUpload1" timeout="30"/> <waitForElementVisible selector="{{ProductDescriptionWYSIWYGToolbarSection.image(ImageUpload1.value)}}" stepKey="waitForUploadImage1" /> <seeElement selector="{{ProductDescriptionWYSIWYGToolbarSection.imageSelected(ImageUpload1.value)}}" stepKey="seeImageSelected1" /> <see selector="{{ProductDescriptionWYSIWYGToolbarSection.DeleteSelectedBtn}}" userInput="Delete Selected" stepKey="seeDeleteBtn1"/> <click selector="{{ProductDescriptionWYSIWYGToolbarSection.DeleteSelectedBtn}}" stepKey="clickDeleteSelected1" /> - <waitForText userInput="OK" stepKey="waitForConfirm1" /> - <click selector="{{ProductDescriptionWYSIWYGToolbarSection.confirmDelete}}" stepKey="confirmDelete1" /> + <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForConfirmDelete1"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmDelete1" /> <waitForElementNotVisible selector="{{ProductDescriptionWYSIWYGToolbarSection.image(ImageUpload1.value)}}" stepKey="waitForImageDeleted1" /> <dontSeeElement selector="{{ProductDescriptionWYSIWYGToolbarSection.image(ImageUpload1.value)}}" stepKey="dontSeeImage1" /> - <dontSeeElement selector="{{ProductDescriptionWYSIWYGToolbarSection.InsertFile}}" stepKey="dontSeeAddSelectedBtn1" /> + <dontSeeElement selector="{{ProductDescriptionWYSIWYGToolbarSection.InsertFile}}" stepKey="dontSeeAddSelectedBtn2" /> <attachFile selector="{{ProductDescriptionWYSIWYGToolbarSection.BrowseUploadImage}}" userInput="{{ImageUpload1.value}}" stepKey="uploadImage2"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoading6" /> + <waitForLoadingMaskToDisappear stepKey="waitForFileUpload2" timeout="45"/> <waitForElementVisible selector="{{ProductDescriptionWYSIWYGToolbarSection.image(ImageUpload1.value)}}" stepKey="waitForUploadImage2" /> <click selector="{{ProductDescriptionWYSIWYGToolbarSection.InsertFile}}" stepKey="clickInsertBtn1" /> - <waitForLoadingMaskToDisappear stepKey="waitForLoading7" /> - <waitForElementVisible selector="{{ProductDescriptionWYSIWYGToolbarSection.OkBtn}}" stepKey="waitForOkBtn1" /> + <waitForElementVisible selector="{{ProductDescriptionWYSIWYGToolbarSection.ImageDescription}}" stepKey="waitForImageDescriptionButton1" /> <fillField selector="{{ProductDescriptionWYSIWYGToolbarSection.ImageDescription}}" userInput="{{ImageUpload1.content}}" stepKey="fillImageDescription1" /> <fillField selector="{{ProductDescriptionWYSIWYGToolbarSection.Height}}" userInput="{{ImageUpload1.height}}" stepKey="fillImageHeight1" /> <click selector="{{ProductDescriptionWYSIWYGToolbarSection.OkBtn}}" stepKey="clickOkBtn1" /> - <waitForPageLoad stepKey="waitForPageLoad3"/> <scrollTo selector="{{ProductDescriptionWYSIWYGToolbarSection.TinyMCE4}}" stepKey="scrollToTinyMCE4" /> <click selector="{{ProductShortDescriptionWYSIWYGToolbarSection.InsertImageIcon}}" stepKey="clickInsertImageIcon2" /> - <waitForPageLoad stepKey="waitForPageLoad4" /> <click selector="{{ProductShortDescriptionWYSIWYGToolbarSection.Browse}}" stepKey="clickBrowse2" /> - <waitForPageLoad stepKey="waitForPageLoad5" /> - <waitForLoadingMaskToDisappear stepKey="waitForLoading8" /> + <waitForElementVisible selector="{{ProductDescriptionWYSIWYGToolbarSection.CancelBtn}}" stepKey="waitForCancelButton2"/> <see selector="{{ProductShortDescriptionWYSIWYGToolbarSection.CancelBtn}}" userInput="Cancel" stepKey="seeCancelBtn2" /> + <waitForLoadingMaskToDisappear stepKey="waitForLoading13" timeout="30"/> <see selector="{{ProductShortDescriptionWYSIWYGToolbarSection.CreateFolder}}" userInput="Create Folder" stepKey="seeCreateFolderBtn2" /> - <dontSeeElement selector="{{ProductShortDescriptionWYSIWYGToolbarSection.InsertFile}}" stepKey="dontSeeAddSelectedBtn2" /> + <waitForLoadingMaskToDisappear stepKey="waitForLoading14" timeout="40"/> + <dontSeeElement selector="{{ProductShortDescriptionWYSIWYGToolbarSection.InsertFile}}" stepKey="dontSeeAddSelectedBtn3" /> <attachFile selector="{{ProductShortDescriptionWYSIWYGToolbarSection.BrowseUploadImage}}" userInput="{{ImageUpload3.value}}" stepKey="uploadImage3"/> + <waitForLoadingMaskToDisappear stepKey="waitForFileUpload3" timeout="45"/> <waitForElementVisible selector="{{ProductShortDescriptionWYSIWYGToolbarSection.image(ImageUpload3.value)}}" stepKey="waitForUploadImage3" /> - <waitForLoadingMaskToDisappear stepKey="waitForLoading9" /> - <wait time="3" stepKey="waitMore" /> <waitForElement selector="{{ProductShortDescriptionWYSIWYGToolbarSection.DeleteSelectedBtn}}" stepKey="waitForDeletebtn" /> <see selector="{{ProductShortDescriptionWYSIWYGToolbarSection.DeleteSelectedBtn}}" userInput="Delete Selected" stepKey="seeDeleteBtn2"/> <click selector="{{ProductShortDescriptionWYSIWYGToolbarSection.DeleteSelectedBtn}}" stepKey="clickDeleteSelected2" /> - <waitForText userInput="OK" stepKey="waitForConfirm3" /> - <click selector="{{ProductShortDescriptionWYSIWYGToolbarSection.confirmDelete}}" stepKey="confirmDelete2" /> - <dontSeeElement selector="{{ProductDescriptionWYSIWYGToolbarSection.InsertFile}}" stepKey="dontSeeAddSelectedBtn3" /> + <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForConfirm3"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmDelete2" /> + <dontSeeElement selector="{{ProductDescriptionWYSIWYGToolbarSection.InsertFile}}" stepKey="dontSeeAddSelectedBtn4" /> <attachFile selector="{{ProductShortDescriptionWYSIWYGToolbarSection.BrowseUploadImage}}" userInput="{{ImageUpload3.value}}" stepKey="uploadImage4"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoading10" /> + <waitForLoadingMaskToDisappear stepKey="waitForFileUpload4" timeout="45"/> <waitForElementVisible selector="{{ProductShortDescriptionWYSIWYGToolbarSection.image(ImageUpload3.value)}}" stepKey="waitForUploadImage4" /> <click selector="{{ProductShortDescriptionWYSIWYGToolbarSection.InsertFile}}" stepKey="clickInsertBtn" /> <waitForLoadingMaskToDisappear stepKey="waitForLoading11" /> @@ -110,9 +102,5 @@ <seeElement selector="{{StorefrontProductInfoMainSection.mediaDescription}}" stepKey="assertMediaDescription"/> <seeElementInDOM selector="{{StorefrontCategoryMainSection.imageSource(ImageUpload3.fileName)}}" stepKey="assertMediaSource3"/> <seeElementInDOM selector="{{StorefrontCategoryMainSection.imageSource(ImageUpload1.fileName)}}" stepKey="assertMediaSource1"/> - <after> - <actionGroup ref="DisabledWYSIWYG" stepKey="disableWYSIWYG"/> - <actionGroup ref="logout" stepKey="logout"/> - </after> </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 000000000000..e3f4d6cbdde0 --- /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/AdminApplyTierPriceToProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest.xml index 72c0a4a51901..545e7c10379b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest.xml @@ -5,8 +5,7 @@ * See COPYING.txt for license details. */ --> -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminApplyTierPriceToProductTest"> <annotations> <features value="Catalog"/> @@ -236,9 +235,10 @@ <scrollToTopOfPage stepKey="scrollToTopOfPage5"/> <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingButton5"/> <waitForElement selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceDeleteButton}}" stepKey="waitForcustomerGroupPriceDeleteButton"/> - <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceDeleteButton}}" stepKey="deleteFirstRowOfCustomerGroupPrice"/> - <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceDeleteButton}}" stepKey="deleteSecondRowOfCustomerGroupPrice"/> - <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton5"/> + <scrollTo selector="(//*[contains(@class, 'product_form_product_form_advanced_pricing_modal')]//tr//button[@data-action='remove_row'])[1]" x="30" y="0" stepKey="scrollToDeleteFirstRowOfCustomerGroupPrice" /> + <click selector="(//tr//button[@data-action='remove_row'])[1]" userInput=".product_form_product_form_advanced_pricing_modal" stepKey="deleteFirstRowOfCustomerGroupPrice"/> + <click selector="//tr//button[@data-action='remove_row']" userInput=".product_form_product_form_advanced_pricing_modal" stepKey="deleteSecondRowOfCustomerGroupPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" userInput=".product_form_product_form_advanced_pricing_modal" stepKey="clickDoneButton5"/> <actionGroup ref="saveProductForm" stepKey="saveProduct5"/> <scrollToTopOfPage stepKey="scrollToTopOfPage6"/> <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingButton6"/> @@ -269,4 +269,56 @@ <actualResult type="variable">grabTextFromMiniCartSubtotalField2</actualResult> </assertEquals> </test> + <test name="AdminApplyTierPriceToProductWithPercentageDiscountTest"> + <annotations> + <features value="Catalog"/> + <stories value="MC-5517 - System tries to save 0 in Advanced Pricing which is invalid for Discount field"/> + <title value="You should be able to apply tier price to a product with float percent discount."/> + <description value="You should be able to apply tier price to a product with float percent discount."/> + <severity value="AVERAGE"/> + <testCaseId value="MAGETWO-96881"/> + <group value="product"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">100</field> + </createData> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad time="30" stepKey="waitForProductIndexPageLoad"/> + <actionGroup ref="resetProductGridToDefaultView" stepKey="resetGridToDefaultKeywordSearch"/> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProduct"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct1"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingButton"/> + <waitForElement selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="waitForCustomerGroupPriceAddButton"/> + <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="addCustomerGroupAllGroupsQty1PriceDiscountAndpercent"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="1" stepKey="fillProductTierPriceQtyInput"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceValueTypeSelect('0')}}" userInput="Discount" stepKey="selectProductTierPriceValueType"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPricePercentageValuePriceInput('0')}}" userInput="0.1" stepKey="selectProductTierPricePriceInput"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct1"/> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.sku$$)}}" stepKey="goProductPageOnStorefront"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad1"/> + <seeElement selector="{{StorefrontCategoryProductSection.productPriceFinal('99.90')}}" stepKey="assertProductFinalPriceProductPage"/> + <seeElement selector="{{StorefrontCategoryProductSection.productPriceLabel('Regular Price')}}" stepKey="assertRegularPriceProductPage"/> + <seeElement selector="{{StorefrontCategoryProductSection.productPriceOld('100')}}" stepKey="assertRegularPriceAmountProductPage"/> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="navigateToCategoryPage"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad2"/> + <seeElement selector="{{StorefrontCategoryProductSection.productPriceFinal('99.90')}}" stepKey="assertProductFinalPriceCategoryPage"/> + <seeElement selector="{{StorefrontCategoryProductSection.productPriceLabel('Regular Price')}}" stepKey="assertRegularPriceLabelCategoryPage"/> + <seeElement selector="{{StorefrontCategoryProductSection.productPriceOld('100')}}" stepKey="assertRegularPriceAmountCategoryPage"/> + </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogCategoriesNavigateMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogCategoriesNavigateMenuTest.xml new file mode 100644 index 000000000000..a51df86d0327 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogCategoriesNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCatalogCategoriesNavigateMenuTest"> + <annotations> + <features value="Catalog"/> + <stories value="Menu Navigation"/> + <title value="Admin catalog categories navigate menu test"/> + <description value="Admin should be able to navigate to Catalog > Categories"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14131"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToCategoriesPage"> + <argument name="menuUiId" value="{{AdminMenuCatalog.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuCatalogCategories.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuCatalogCategories.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogProductsNavigateMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogProductsNavigateMenuTest.xml new file mode 100644 index 000000000000..1d9400bf81e4 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogProductsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCatalogProductsNavigateMenuTest"> + <annotations> + <features value="Catalog"/> + <stories value="Menu Navigation"/> + <title value="Admin catalog products navigate menu test"/> + <description value="Admin should be able to navigate to Catalog > Products"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14130"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToCatalogProductsPage"> + <argument name="menuUiId" value="{{AdminMenuCatalog.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuCatalogProducts.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuCatalogProducts.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeSet.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeSet.xml new file mode 100644 index 000000000000..bcfab6ccfdf1 --- /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 000000000000..86978a4121a4 --- /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 000000000000..8d41b276334a --- /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 000000000000..fd22142fcb09 --- /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 000000000000..b6c76d657721 --- /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 000000000000..c9cd9acd9708 --- /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 000000000000..ee8b48a94b20 --- /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 000000000000..e1cb45be22b4 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml @@ -0,0 +1,73 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCheckOutOfStockProductIsVisibleInCategoryTest"> + <annotations> + <stories value="Manage products"/> + <title value="Out of Stock Product is Visible in Category"/> + <description value="Login as admin and check out of stock product is visible in category"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-11067"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!--Set Display out of stock product--> + <magentoCLI stepKey="setDisplayOutOfStockProduct" command="config:set cataloginventory/options/show_out_of_stock 1" /> + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <!--Create Category--> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <!--Create Simple Product--> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <!-- Delete created entity --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + <magentoCLI stepKey="setDisplayOutOfStockProduct" command="config:set cataloginventory/options/show_out_of_stock 0" /> + </after> + <!--Open Product Index Page and filter the product--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + <actionGroup ref="filterProductGridBySku" stepKey="filterProduct"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> + <!-- Update product Advanced Inventory Setting --> + <click stepKey="openSelectedProduct" selector="{{AdminProductGridSection.productRowBySku($$createSimpleProduct.sku$$)}}"/> + <waitForPageLoad stepKey="waitForProductToLoad"/> + <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickOnAdvancedInventoryLink"/> + <waitForPageLoad stepKey="waitForAdvancedInventoryPageToLoad"/> + <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.useConfigSettings}}" stepKey="uncheckConfigSetting"/> + <selectOption selector="{{AdminProductFormAdvancedInventorySection.manageStock}}" userInput="Yes" stepKey="clickOnManageStock"/> + <fillField selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryQty}}" userInput="5" stepKey="fillProductQty"/> + <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.miniQtyConfigSetting}}" stepKey="uncheckMiniQtyCheckBox"/> + <fillField selector="{{AdminProductFormAdvancedInventorySection.miniQtyAllowedInCart}}" userInput="1" stepKey="fillMiniAllowedQty"/> + <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.maxiQtyConfigSetting}}" stepKey="uncheckMaxQtyCheckBox"/> + <fillField selector="{{AdminProductFormAdvancedInventorySection.maxiQtyAllowedInCart}}" userInput="10000" stepKey="fillMaxAllowedQty"/> + <selectOption selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimals}}" userInput="Yes" stepKey="selectQuantityUsesDecimal"/> + <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQtyConfigSetting}}" stepKey="uncheckNotifyBelowQtyCheckBox"/> + <fillField selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQty}}" userInput="1" stepKey="fillNotifyBelowQty"/> + <selectOption selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryStockStatus}}" userInput="Out of Stock" stepKey="selectOutOfStock"/> + <click stepKey="clickOnDoneButton" selector="{{AdminProductFormAdvancedInventorySection.doneButton}}"/> + <waitForPageLoad stepKey="waitForProductPageToLoad"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> + <!--Verify product is visible in category front page --> + <amOnPage url="$$createCategory.name$$.html" stepKey="openCategoryStoreFrontPage"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="seeCategoryInFrontPage"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="clickOnCategory"/> + <see selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="seeProductNameInCategoryPage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml new file mode 100644 index 000000000000..f40a62c164ec --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml @@ -0,0 +1,176 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCheckPaginationInStorefrontTest"> + <annotations> + <stories value="Create flat catalog product"/> + <title value="Verify that pagination works when Flat Category is enabled"/> + <description value="Login as admin, create flat catalog product and check pagination"/> + <testCaseId value="MC-6051"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + <group value="Catalog"/> + </annotations> + <before> + <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 1 "/> + <magentoCLI stepKey="setFlatCatalogProduct" command="config:set catalog/frontend/flat_catalog_product 1 "/> + <createData entity="_defaultCategory" stepKey="createDefaultCategory"/> + <createData entity="PaginationProduct" stepKey="simpleProduct1"/> + <createData entity="PaginationProduct" stepKey="simpleProduct2"/> + <createData entity="PaginationProduct" stepKey="simpleProduct3"/> + <createData entity="PaginationProduct" stepKey="simpleProduct4"/> + <createData entity="PaginationProduct" stepKey="simpleProduct5"/> + <createData entity="PaginationProduct" stepKey="simpleProduct6"/> + <createData entity="PaginationProduct" stepKey="simpleProduct7"/> + <createData entity="PaginationProduct" stepKey="simpleProduct8"/> + <createData entity="PaginationProduct" stepKey="simpleProduct9"/> + <createData entity="PaginationProduct" stepKey="simpleProduct10"/> + <createData entity="PaginationProduct" stepKey="simpleProduct11"/> + <createData entity="PaginationProduct" stepKey="simpleProduct12"/> + <createData entity="PaginationProduct" stepKey="simpleProduct13"/> + <createData entity="PaginationProduct" stepKey="simpleProduct14"/> + <createData entity="PaginationProduct" stepKey="simpleProduct15"/> + <createData entity="PaginationProduct" stepKey="simpleProduct16"/> + <createData entity="PaginationProduct" stepKey="simpleProduct17"/> + <createData entity="PaginationProduct" stepKey="simpleProduct18"/> + <createData entity="PaginationProduct" stepKey="simpleProduct19"/> + <createData entity="PaginationProduct" stepKey="simpleProduct20"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + </before> + <after> + <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0" /> + <magentoCLI stepKey="setFlatCatalogProduct" command="config:set catalog/frontend/flat_catalog_product 0" /> + <deleteData createDataKey="createDefaultCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> + <deleteData createDataKey="simpleProduct3" stepKey="deleteSimpleProduct3"/> + <deleteData createDataKey="simpleProduct4" stepKey="deleteSimpleProduct4"/> + <deleteData createDataKey="simpleProduct5" stepKey="deleteSimpleProduct5"/> + <deleteData createDataKey="simpleProduct6" stepKey="deleteSimpleProduct6"/> + <deleteData createDataKey="simpleProduct7" stepKey="deleteSimpleProduct7"/> + <deleteData createDataKey="simpleProduct8" stepKey="deleteSimpleProduct8"/> + <deleteData createDataKey="simpleProduct9" stepKey="deleteSimpleProduct9"/> + <deleteData createDataKey="simpleProduct10" stepKey="deleteSimpleProduct10"/> + <deleteData createDataKey="simpleProduct11" stepKey="deleteSimpleProduct11"/> + <deleteData createDataKey="simpleProduct12" stepKey="deleteSimpleProduct12"/> + <deleteData createDataKey="simpleProduct13" stepKey="deleteSimpleProduct13"/> + <deleteData createDataKey="simpleProduct14" stepKey="deleteSimpleProduct14"/> + <deleteData createDataKey="simpleProduct15" stepKey="deleteSimpleProduct15"/> + <deleteData createDataKey="simpleProduct16" stepKey="deleteSimpleProduct16"/> + <deleteData createDataKey="simpleProduct17" stepKey="deleteSimpleProduct17"/> + <deleteData createDataKey="simpleProduct18" stepKey="deleteSimpleProduct18"/> + <deleteData createDataKey="simpleProduct19" stepKey="deleteSimpleProduct19"/> + <deleteData createDataKey="simpleProduct20" stepKey="deleteSimpleProduct20"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open Category Page and select created category--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoad1"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <waitForPageLoad stepKey="waitForPageToLoad0"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCreatedCategory"/> + <waitForPageLoad stepKey="waitForPageToLoaded2"/> + + <!--Select Products--> + <scrollTo selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" x="0" y="-80" stepKey="scrollToProductInCategory"/> + <click selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" stepKey="clickOnProductInCategory"/> + <waitForPageLoad stepKey="waitForProductsToLoad"/> + <scrollTo selector="{{CatalogProductsSection.resetFilter}}" stepKey="scrollToResetFilter"/> + <waitForElementVisible selector="{{CatalogProductsSection.resetFilter}}" time="30" stepKey="waitForResetButtonToVisible"/> + <click selector="{{CatalogProductsSection.resetFilter}}" stepKey="clickOnResetFilter"/> + <waitForPageLoad stepKey="waitForPageToLoad3"/> + <selectOption selector="{{AdminProductGridFilterSection.productPerPage}}" userInput="20" stepKey="selectPagePerView"/> + <fillField selector="{{AdminCategoryContentSection.productTableColumnName}}" userInput="pagi" stepKey="selectProduct1"/> + <click selector="{{AdminCategoryContentSection.productSearch}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitFroPageToLoad1"/> + <see selector="{{AdminProductGridFilterSection.productCount}}" userInput="20" stepKey="seeNumberOfProductsFound"/> + <click selector="{{AdminCategoryProductsGridSection.productSelectAll}}" stepKey="selectSelectAll"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForCategorySaved"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the category." stepKey="assertSuccessMessage"/> + <waitForPageLoad stepKey="waitForPageTitleToBeSaved"/> + + <!--Open Category Store Front Page--> + <amOnPage url="{{_defaultCategory.name}}.html" stepKey="goToStorefront"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="seeCategoryOnNavigation"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="selectCategory"/> + <waitForPageLoad stepKey="waitForProductToLoad"/> + + <!--Select 9 items per page and verify number of products displayed in each page --> + <conditionalClick selector="{{StorefrontCategoryTopToolbarSection.gridMode}}" visible="true" dependentSelector="{{StorefrontCategoryTopToolbarSection.gridMode}}" stepKey="seeProductGridIsActive"/> + <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.perPage}}" stepKey="scrollToBottomToolbarSection"/> + <selectOption selector="{{StorefrontCategoryBottomToolbarSection.perPage}}" userInput="9" stepKey="selectPerPageOption"/> + + <!--Verify number of products displayed in First Page --> + <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="9" stepKey="seeNumberOfProductsInFirstPage"/> + + <!--Verify number of products displayed in Second Page --> + <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="scrollToNextButton"/> + <click selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="clickOnNextPage"/> + <waitForPageLoad stepKey="waitForPageToLoad4"/> + <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="9" stepKey="seeNumberOfProductsInSecondPage"/> + + <!--Verify number of products displayed in third Page --> + <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="scrollToNextButton1"/> + <click selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="clickOnNextPage1"/> + <waitForPageLoad stepKey="waitForPageToLoad2"/> + <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="2" stepKey="seeNumberOfProductsInThirdPage"/> + + <!--Change Pages using Previous Page selector and verify number of products displayed in each page--> + <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.previousPage}}" stepKey="scrollToPreviousPage"/> + <click selector="{{StorefrontCategoryBottomToolbarSection.previousPage}}" stepKey="clickOnPreviousPage1"/> + <waitForPageLoad stepKey="waitForPageToLoad5"/> + <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="9" stepKey="seeNumberOfProductsInSecondPage1"/> + <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.previousPage}}" stepKey="scrollToPreviousPage1"/> + <click selector="{{StorefrontCategoryBottomToolbarSection.previousPage}}" stepKey="clickOnPreviousPage2"/> + <waitForPageLoad stepKey="waitForPageToLoad6"/> + <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="9" stepKey="seeNumberOfProductsInFirstPage1"/> + + <!--Select Pages by using page Number and verify number of products displayed--> + <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="scrollToPreviousPage2"/> + <click selector="{{StorefrontCategoryBottomToolbarSection.pageNumber('2')}}" stepKey="clickOnPage2"/> + <waitForPageLoad stepKey="waitForPageToLoad7"/> + <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="9" stepKey="seeNumberOfProductsInSecondPage2"/> + + <!--Select Third Page using page number--> + <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="scrollToPreviousPage3"/> + <click selector="{{StorefrontCategoryBottomToolbarSection.pageNumber('3')}}" stepKey="clickOnThirdPage"/> + <waitForPageLoad stepKey="waitForPageToLoad8"/> + <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="2" stepKey="seeNumberOfProductsInThirdPage2"/> + + <!--Select First Page using page number--> + <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.previousPage}}" stepKey="scrollToPreviousPage4"/> + <click selector="{{StorefrontCategoryBottomToolbarSection.pageNumber('1')}}" stepKey="clickOnFirstPage"/> + <waitForPageLoad stepKey="waitForPageToLoad9"/> + <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="9" stepKey="seeNumberOfProductsFirstPage2"/> + + <!--Select 15 items per page and verify number of products displayed in each page --> + <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.perPage}}" stepKey="scrollToPerPage"/> + <selectOption selector="{{StorefrontCategoryBottomToolbarSection.perPage}}" userInput="15" stepKey="selectPerPageOption1"/> + <waitForPageLoad stepKey="waitForPageToLoad10"/> + <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="15" stepKey="seeNumberOfProductsInFirstPage3"/> + <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="scrollToNextButton2"/> + <click selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="clickOnNextPage2"/> + <waitForPageLoad stepKey="waitForPageToLoad11"/> + <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="5" stepKey="seeNumberOfProductsInSecondPage3"/> + + <!--Select First Page using page number--> + <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.pageNumber('1')}}" stepKey="scrollToPreviousPage5"/> + <click selector="{{StorefrontCategoryBottomToolbarSection.pageNumber('1')}}" stepKey="clickOnFirstPage2"/> + <waitForPageLoad stepKey="waitForPageToLoad13"/> + + <!--Select 30 items per page and verify number of products displayed in each page --> + <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.perPage}}" stepKey="scrollToPerPage4"/> + <selectOption selector="{{StorefrontCategoryBottomToolbarSection.perPage}}" userInput="30" stepKey="selectPerPageOption2"/> + <waitForPageLoad stepKey="waitForPageToLoad12"/> + <seeNumberOfElements selector="{{StorefrontCategoryMainSection.productLink}}" userInput="20" stepKey="seeNumberOfProductsInFirstPage4"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml new file mode 100644 index 000000000000..f5872ac3efca --- /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/AdminConfigureProductImagePlaceholderTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminConfigureProductImagePlaceholderTest.xml new file mode 100644 index 000000000000..4d97dee56f05 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminConfigureProductImagePlaceholderTest.xml @@ -0,0 +1,145 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminConfigureProductImagePlaceholderTest"> + + <annotations> + <features value="Configuration"/> + <stories value="Configure product placeholder images"/> + <title value="Admin is able to configure product placeholder images"/> + <description value="Admin should be able to configure the images used for product image placeholders. The configured placeholders should be seen on the frontend when an image has no image."/> + <severity value="MAJOR"/> + <testCaseId value="MC-5005"/> + <group value="configuration"/> + </annotations> + + <before> + <createData entity="ApiCategory" stepKey="category"/> + <!--Create product with no images--> + <createData entity="ApiSimpleProduct" stepKey="productNoImages"> + <requiredEntity createDataKey="category"/> + </createData> + <!--Create product with small, base, and thumbnail image--> + <createData entity="ApiSimpleProduct" stepKey="productWithImages"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="ApiProductAttributeMediaGalleryEntryTestImage" stepKey="productImage"> + <requiredEntity createDataKey="productWithImages"/> + </createData> + </before> + + <after> + <!--Unset product image placeholders--> + <amOnPage url="{{CatalogConfigPage.url}}" stepKey="goToCatalogConfigurationPageAfter"/> + <waitForPageLoad stepKey="waitForConfigurationPageLoadAfter"/> + <conditionalClick selector="{{AdminProductImagePlaceholderConfigSection.sectionHeader}}" dependentSelector="{{AdminProductImagePlaceholderConfigSection.baseImageInput}}" visible="false" stepKey="openPlaceholderSectionAfter"/> + <waitForElementVisible selector="{{AdminProductImagePlaceholderConfigSection.baseImageInput}}" stepKey="waitForPlaceholderSectionOpenAfter"/> + <!--Delete base placeholder--> + <checkOption selector="{{AdminProductImagePlaceholderConfigSection.baseImageDelete}}" stepKey="checkDeleteBasePlaceholder"/> + <!--Delete small placeholder--> + <checkOption selector="{{AdminProductImagePlaceholderConfigSection.smallImageDelete}}" stepKey="checkDeleteSmallPlaceholder"/> + <!--Delete thumbnail placeholder--> + <checkOption selector="{{AdminProductImagePlaceholderConfigSection.thumbnailImageDelete}}" stepKey="checkDeleteThumbnailPlaceholder"/> + <!--Save config to delete placeholders--> + <click selector="{{AdminMainActionsSection.save}}" stepKey="saveConfigWithPlaceholders"/> + <!--See placeholders are empty--> + <conditionalClick selector="{{AdminProductImagePlaceholderConfigSection.sectionHeader}}" dependentSelector="{{AdminProductImagePlaceholderConfigSection.baseImageInput}}" visible="false" stepKey="openPlaceholderSection2"/> + <waitForElementVisible selector="{{AdminProductImagePlaceholderConfigSection.baseImageInput}}" stepKey="waitForPlaceholderSectionOpen2"/> + <dontSeeElement selector="{{AdminProductImagePlaceholderConfigSection.baseImage}}" stepKey="dontSeeBaseImageSet"/> + <dontSeeElement selector="{{AdminProductImagePlaceholderConfigSection.smallImage}}" stepKey="dontSeeSmallImageSet"/> + <dontSeeElement selector="{{AdminProductImagePlaceholderConfigSection.thumbnailImage}}" stepKey="dontSeeThumbnailImageSet"/> + <dontSeeElement selector="{{AdminProductImagePlaceholderConfigSection.swatchImage}}" stepKey="dontSeeSwatchImageSet"/> + + <!--Delete prerequisite entities--> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <deleteData createDataKey="productNoImages" stepKey="deleteProductNoImages"/> + <deleteData createDataKey="productWithImages" stepKey="deleteProductWithImages"/> + </after> + + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminArea"/> + + <!--Admin area: configure Product Image Placeholders--> + <comment userInput="Configure product image placeholders in store config" stepKey="configurePlaceholderComment"/> + <amOnPage url="{{CatalogConfigPage.url}}" stepKey="goToCatalogConfigurationPage"/> + <waitForPageLoad stepKey="waitForConfigurationPageLoad1"/> + <conditionalClick selector="{{AdminProductImagePlaceholderConfigSection.sectionHeader}}" dependentSelector="{{AdminProductImagePlaceholderConfigSection.baseImageInput}}" visible="false" stepKey="openPlaceholderSection1"/> + <waitForElementVisible selector="{{AdminProductImagePlaceholderConfigSection.baseImageInput}}" stepKey="waitForPlaceholderSectionOpen1"/> + <!--Set base placeholder--> + <attachFile selector="{{AdminProductImagePlaceholderConfigSection.baseImageInput}}" userInput="{{placeholderBaseImage.file}}" stepKey="uploadBasePlaceholder"/> + <!--Set small placeholder--> + <attachFile selector="{{AdminProductImagePlaceholderConfigSection.smallImageInput}}" userInput="{{placeholderSmallImage.file}}" stepKey="uploadSmallPlaceholder"/> + <!--Set thumbnail placeholder--> + <attachFile selector="{{AdminProductImagePlaceholderConfigSection.thumbnailImageInput}}" userInput="{{placeholderThumbnailImage.file}}" stepKey="uploadThumbnailPlaceholder"/> + <!--Save config with placeholders--> + <click selector="{{AdminMainActionsSection.save}}" stepKey="saveConfigWithPlaceholders"/> + <!--See images are saved--> + <conditionalClick selector="{{AdminProductImagePlaceholderConfigSection.sectionHeader}}" dependentSelector="{{AdminProductImagePlaceholderConfigSection.baseImageInput}}" visible="false" stepKey="openPlaceholderSection2"/> + <waitForElementVisible selector="{{AdminProductImagePlaceholderConfigSection.baseImageInput}}" stepKey="waitForPlaceholderSectionOpen2"/> + <seeElement selector="{{AdminProductImagePlaceholderConfigSection.baseImageBySrc(placeholderBaseImage.name)}}" stepKey="seeBasePlaceholderSet"/> + <seeElement selector="{{AdminProductImagePlaceholderConfigSection.smallImageBySrc(placeholderSmallImage.name)}}" stepKey="seeSmallPlaceholderSet"/> + <seeElement selector="{{AdminProductImagePlaceholderConfigSection.thumbnailImageBySrc(placeholderThumbnailImage.name)}}" stepKey="seeThumbnailPlaceholderSet"/> + <dontSeeElement selector="{{AdminProductImagePlaceholderConfigSection.swatchImage}}" stepKey="dontSeeSwatchImageSet"/> + + <!--See correct placeholder images on category page--> + <comment userInput="Check placeholder images on the storefront" stepKey="checkStorefrontComment"/> + <amOnPage url="$$category.name$$.html" stepKey="goToCategoryStorefront1"/> + <waitForPageLoad stepKey="waitForStorefrontCategory1"/> + <!--Product with no images uses placeholder--> + <seeElement selector="{{StorefrontCategoryProductSection.ProductImageByName($$productNoImages.name$$)}}" stepKey="seeProductNoImagesInCategory"/> + <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByName($$productNoImages.name$$)}}" userInput="src" stepKey="getSmallPlaceholderImageSrc"/> + <assertContains stepKey="checkSmallPlaceholderImage"> + <actualResult type="variable">$getSmallPlaceholderImageSrc</actualResult> + <expectedResult type="string">{{placeholderSmallImage.name}}</expectedResult> + </assertContains> + <!--Product with images does not use placeholder--> + <seeElement selector="{{StorefrontCategoryProductSection.ProductTitleByName($$productWithImages.name$$)}}" stepKey="seeProductWithImagesInCategory"/> + <grabAttributeFrom selector="{{StorefrontCategoryProductSection.ProductImageByName($$productWithImages.name$$)}}" userInput="src" stepKey="getSmallNonPlaceholderImageSrc"/> + <assertNotContains stepKey="checkSmallPlaceholderImageNotUsed"> + <actualResult type="variable">$getSmallNonPlaceholderImageSrc</actualResult> + <expectedResult type="string">{{placeholderSmallImage.name}}</expectedResult> + </assertNotContains> + + <!--Check base image on product page--> + <!--Product which is using placeholder--> + <click selector="{{StorefrontCategoryProductSection.ProductImageByName($$productNoImages.name$$)}}" stepKey="goToProductNoImages"/> + <waitForPageLoad stepKey="waitForProductPageLoad1"/> + <seeInCurrentUrl url="$$productNoImages.sku$$" stepKey="seeCorrectProductPage1"/> + <seeElement selector="{{StorefrontProductMediaSection.imageFile(placeholderBaseImage.name)}}" stepKey="seeBasePlaceholderImage"/> + <click selector="{{StorefrontProductPageSection.addToCartBtn}}" stepKey="addProductToCart1"/> + <waitForElementVisible selector="{{StorefrontProductPageSection.successMsg}}" stepKey="waitForProductAdded1"/> + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="openMiniCart1"/> + <grabAttributeFrom selector="{{StorefrontMinicartSection.productImageByName($$productNoImages.name$$)}}" userInput="src" stepKey="getThumbnailPlaceholderImageSrc"/> + <assertContains stepKey="checkThumbnailPlaceholderImage"> + <actualResult type="variable">$getThumbnailPlaceholderImageSrc</actualResult> + <expectedResult type="string">{{placeholderThumbnailImage.name}}</expectedResult> + </assertContains> + <actionGroup ref="removeProductFromMiniCart" stepKey="removeProductFromCart1"> + <argument name="productName" value="$$productNoImages.name$$"/> + </actionGroup> + <!--Product which is NOT using placeholder--> + <amOnPage url="$$category.name$$.html" stepKey="goToCategoryStorefront2"/> + <waitForPageLoad stepKey="waitForStorefrontCategory2"/> + <click selector="{{StorefrontCategoryProductSection.ProductImageByName($$productWithImages.name$$)}}" stepKey="goToProductWithImages"/> + <waitForPageLoad stepKey="waitForProductPageLoad2"/> + <seeInCurrentUrl url="$$productWithImages.sku$$" stepKey="seeCorrectProductPage2"/> + <dontSeeElement selector="{{StorefrontProductMediaSection.imageFile(placeholderBaseImage.name)}}" stepKey="dontSeeBasePlaceholderImage"/> + <click selector="{{StorefrontProductPageSection.addToCartBtn}}" stepKey="addProductToCart2"/> + <waitForElementVisible selector="{{StorefrontProductPageSection.successMsg}}" stepKey="waitForProductAdded2"/> + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="openMiniCart2"/> + <grabAttributeFrom selector="{{StorefrontMinicartSection.productImageByName($$productWithImages.name$$)}}" userInput="src" stepKey="getThumbnailImageSrc"/> + <assertNotContains stepKey="checkThumbnailImage"> + <actualResult type="variable">$getThumbnailImageSrc</actualResult> + <expectedResult type="string">{{placeholderThumbnailImage.name}}</expectedResult> + </assertNotContains> + <actionGroup ref="removeProductFromMiniCart" stepKey="removeProductFromCart2"> + <argument name="productName" value="$$productWithImages.name$$"/> + </actionGroup> + </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 000000000000..4deca7350467 --- /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 000000000000..d9e410a9a300 --- /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/AdminCreateCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest.xml index bfb955791064..8806612c0f5d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest.xml @@ -69,4 +69,35 @@ <waitForElementVisible selector="{{CategoryDesignSection.LayoutDropdown}}" stepKey="waitForLayoutDropDown" /> <seeOptionIsSelected selector="{{CategoryDesignSection.LayoutDropdown}}" userInput="2 columns with right bar" stepKey="see2ColumnsSelected" /> </test> + <test name="AdminCategoryFormDisplaySettingsUIValidationTest"> + <annotations> + <features value="Catalog"/> + <stories value="Default layout configuration MAGETWO-88793"/> + <title value="Category should not be saved once layered navigation price step field is left empty"/> + <description value="Once the Config setting is unchecked Category should not be saved with layered navigation price field left empty"/> + <severity value="AVERAGE"/> + <testCaseId value="MAGETWO-95797"/> + <group value="category"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="navigateToCategoryPage"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad1"/> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategory"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="enterCategoryName"/> + <click selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" stepKey="clickOnDisplaySettingsTab"/> + <waitForElementVisible selector="{{CategoryDisplaySettingsSection.filterPriceRangeUseConfig}}" stepKey="wait"/> + <scrollTo selector="{{CategoryDisplaySettingsSection.layeredNavigationPriceInput}}" stepKey="scrollToLayeredNavigationField"/> + <uncheckOption selector="{{CategoryDisplaySettingsSection.filterPriceRangeUseConfig}}" stepKey="uncheckConfigSetting"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategory"/> + <see selector="{{AdminCategoryBasicFieldSection.FieldError('uid')}}" userInput="This is a required field." stepKey="seeErrorMessage"/> + <!-- Verify that the Layered navigation price step field has the required indicator --> + <comment userInput="Check if Layered navigation price field has required indictor icon" stepKey="comment" /> + <executeJS function="{{CategoryDisplaySettingsSection.RequiredFieldIndicator('filter_price_range')}}" stepKey="getRequiredFieldIndicator"/> + <assertEquals expected='"*"' expectedType="string" actualType="variable" actual="getRequiredFieldIndicator" message="pass" stepKey="assertRequiredFieldIndicator1"/> + </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml new file mode 100644 index 000000000000..9115004ad958 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.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="AdminCreateCategoryWithAnchorFieldTest"> + <annotations> + <stories value="Create categories"/> + <title value="Create anchor subcategory with all fields"/> + <description value="Login as admin and create anchor subcategory with all fields"/> + <testCaseId value="MC-5267"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="_defaultBlock" stepKey="createDefaultCMSBlock"/> + <createData entity="defaultSimpleProduct" stepKey="simpleProduct" /> + </before> + <after> + <actionGroup ref="DeleteCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + <deleteData createDataKey="createDefaultCMSBlock" stepKey="deleteDefaultCMSBlock"/> + <deleteData stepKey="deleteSimpleProduct" createDataKey="simpleProduct"/> + </after> + <!--Create SubCategory--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoaded"/> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="fillCategoryName"/> + <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> + <!--Select Content and fill the options--> + <scrollTo selector="{{AdminCategoryContentSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToContent"/> + <click selector="{{AdminCategoryContentSection.sectionHeader}}" stepKey="selectContent"/> + <scrollTo selector="{{AdminCategoryContentSection.AddCMSBlock}}" x="0" y="-80" stepKey="scrollToAddCMSBlock"/> + <selectOption selector="{{AdminCategoryContentSection.AddCMSBlock}}" userInput="$$createDefaultCMSBlock.title$$" stepKey="selectCMSBlock"/> + <!--Select Display Setting and fill the options--> + <scrollTo selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" x="0" y="-80" stepKey="scrollToDisplaySetting"/> + <click selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" stepKey="selectDisplaySetting"/> + <selectOption selector="{{CategoryDisplaySettingsSection.displayMode}}" userInput="PRODUCTS_AND_PAGE" stepKey="selectdisplayMode"/> + <checkOption selector="{{CategoryDisplaySettingsSection.anchor}}" stepKey="enableAnchor"/> + <click selector="{{CategoryDisplaySettingsSection.productListCheckBox}}" stepKey="enableTheAvailableProductList"/> + <selectOption selector="{{CategoryDisplaySettingsSection.productList}}" parameterArray="['Position', 'Product Name', 'Price']" stepKey="selectPrice"/> + <scrollTo selector="{{CategoryDisplaySettingsSection.defaultProductLisCheckBox}}" x="0" y="-80" stepKey="scrollToDefaultProductList"/> + <click selector="{{CategoryDisplaySettingsSection.defaultProductLisCheckBox}}" stepKey="enableTheDefaultProductList"/> + <selectOption selector="{{CategoryDisplaySettingsSection.defaultProductList}}" userInput="name" stepKey="selectProductName"/> + <scrollTo selector="{{CategoryDisplaySettingsSection.layeredNavigationPriceCheckBox}}" x="0" y="-80" stepKey="scrollToLayeredNavPrice"/> + <click selector="{{CategoryDisplaySettingsSection.layeredNavigationPriceCheckBox}}" stepKey="enableLayeredNavigationPrice"/> + <fillField selector="{{CategoryDisplaySettingsSection.layeredNavigationPriceInput}}" userInput="5.5" stepKey="fillThePrice"/> + <!--Search the products and select the category products--> + <scrollTo selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" x="0" y="-80" stepKey="scrollToProductInCategory"/> + <click selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" stepKey="clickOnProductInCategory"/> + <fillField selector="{{AdminCategoryContentSection.productTableColumnName}}" userInput="$$simpleProduct.name$$" stepKey="selectProduct"/> + <click selector="{{AdminCategoryContentSection.productSearch}}" stepKey="clickSearchButton"/> + <click selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="selectProductFromTableRow"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForCategorySaved"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the category." stepKey="assertSuccessMessage"/> + <waitForPageLoad stepKey="waitForPageTitleToBeSaved"/> + <!--Verify the Category Title--> + <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seePageTitle" /> + <!--Verify Product in store front page--> + <amOnPage url="{{StorefrontCategoryPage.url(_defaultCategory.name_lwr)}}" stepKey="amOnCategoryPage"/> + <waitForPageLoad stepKey="waitForPageToBeLoaded"/> + <see selector="{{StorefrontCategoryMainSection.CategoryTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seeCategoryPageTitle"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="seeCategoryOnNavigation"/> + <waitForPageLoad stepKey="waitForProductToLoad"/> + <seeElement selector="{{StorefrontCategoryMainSection.productLinkByHref($$simpleProduct.urlKey$$)}}" stepKey="seeProductInCategory"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml new file mode 100644 index 000000000000..e8c6da476a3d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.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="AdminCreateCategoryWithCustomRootCategoryTest"> + <annotations> + <stories value="Create categories"/> + <title value="Create category in the custom root category that is used for custom website"/> + <description value="Login as admin and create a root category with nested sub category and verify category in store front "/> + <testCaseId value="MC-5272"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + <group value="Catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + </before> + <after> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="navigateToStoresIndex"/> + <waitForPageLoad stepKey="waitStoreIndexPageLoad" /> + <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCustomStore"> + <argument name="storeGroupName" value="customStore.name"/> + </actionGroup> + <actionGroup ref="DeleteCategory" stepKey="deleteCreatedNewRootCategory"> + <argument name="categoryEntity" value="NewRootCategory"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForCategoryIndexPageToBeLoaded"/> + <!--Create Root Category--> + <actionGroup ref="AdminCreateRootCategory" stepKey="createNewRootCategory"> + <argument name="categoryEntity" value="NewRootCategory"/> + </actionGroup> + <!--Create subcategory--> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(NewRootCategory.name)}}" stepKey="clickOnCreatedNewRootCategory"/> + <scrollToTopOfPage stepKey="scrollToTopOfPage1"/> + <actionGroup ref="CreateCategory" stepKey="createSubcategory"> + <argument name="categoryEntity" value="SimpleSubCategory"/> + </actionGroup> + <!--Create a Store--> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <waitForPageLoad stepKey="waitForSystemStorePage"/> + <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> + <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" stepKey="fillStoreName"/> + <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" stepKey="fillStoreCode"/> + <selectOption userInput="{{NewRootCategory.name}}" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectStoreStatus"/> + <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreButton"/> + <!--Create a Store View--> + <click selector="{{AdminStoresMainActionsSection.createStoreViewButton}}" stepKey="selectCreateStoreView"/> + <click selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="clickDropDown"/> + <selectOption userInput="{{customStore.name}}" selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="selectStoreViewStatus"/> + <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreSection.storeNameTextField}}" stepKey="fillStoreViewName"/> + <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreSection.storeCodeTextField}}" stepKey="fillStoreViewCode"/> + <selectOption selector="{{AdminNewStoreSection.statusDropdown}}" userInput="Enabled" stepKey="enableStatus"/> + <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreViewButton"/> + <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForModal" /> + <see selector="{{AdminConfirmationModalSection.title}}" userInput="Warning message" stepKey="seeWarning" /> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="dismissModal" /> + <waitForElementNotVisible selector="{{AdminNewStoreViewActionsSection.loadingMask}}" stepKey="waitForElementVisible"/> + <!--Go to store front page--> + <amOnPage url="/{{NewRootCategory.name}}/{{SimpleSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> + <waitForPageLoad time="60" stepKey="waitForStoreFrontPageLoad"/> + <!--Verify subcategory displayed in store front page--> + <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="selectMainWebsite"/> + <click selector="{{StorefrontFooterSection.storeLink(customStore.name)}}" stepKey="selectCustomStore"/> + <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="seeSubCategoryInStoreFrontPage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithFiveNestingTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithFiveNestingTest.xml new file mode 100644 index 000000000000..530bafaef24c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithFiveNestingTest.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="AdminCreateCategoryWithFiveNestingTest"> + <annotations> + <stories value="Create categories"/> + <title value="Create category with five nesting"/> + <description value="Login as admin and create nested sub category and verify the subcategory displayed in store front page "/> + <testCaseId value="MC-5271"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + </before> + <after> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="goToCategoryPage"/> + <waitForPageLoad time="60" stepKey="waitForCategoryPageLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(FirstLevelSubCat.name)}}" stepKey="clickCategoryLink"/> + <click selector="{{AdminCategoryMainActionsSection.DeleteButton}}" stepKey="clickDelete"/> + <waitForElementVisible selector="{{AdminCategoryModalSection.message}}" stepKey="waitForConfirmationModal"/> + <see selector="{{AdminCategoryModalSection.message}}" userInput="Are you sure you want to delete this category?" stepKey="seeDeleteConfirmationMessage"/> + <click selector="{{AdminCategoryModalSection.ok}}" stepKey="confirmDelete"/> + <waitForPageLoad time="60" stepKey="waitForDeleteToFinish"/> + <see selector="You deleted the category." stepKey="seeDeleteSuccess"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories"/> + <dontSee selector="{{AdminCategorySidebarTreeSection.categoryInTree(FirstLevelSubCat.name)}}" stepKey="dontSeeCategoryInTree"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Create Category with Five Nesting --> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForCategoryIndexPageToBeLoaded"/> + <!--Create Nested First Category--> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> + <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{FirstLevelSubCat.name}}" stepKey="fillFirstSubCategoryName"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveFirstSubCategory"/> + <waitForPageLoad stepKey="waitForSFirstSubCategorySaved"/> + <!-- Verify success message --> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the category." stepKey="assertSuccessMessage"/> + <!--Create Nested Second Sub Category--> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton1"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SecondLevelSubCat.name}}" stepKey="fillSecondSubCategoryName"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSecondSubCategory"/> + <waitForPageLoad stepKey="waitForSecondCategory"/> + <!-- Verify success message --> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the category." stepKey="assertSuccessMessage1"/> + <!--Create Nested Third Sub Category/>--> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton2"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{ThirdLevelSubCat.name}}" stepKey="fillThirdSubCategoryName"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveThirdSubCategory"/> + <waitForPageLoad stepKey="waitForThirdCategorySaved"/> + <!-- Verify success message --> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the category." stepKey="assertSuccessMessage2"/> + <!--Create Nested fourth Sub Category />--> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton3"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{FourthLevelSubCat.name}}" stepKey="fillFourthSubCategoryName"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveFourthSubCategory"/> + <waitForPageLoad stepKey="waitForFourthCategorySaved"/> + <!-- Verify success message --> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the category." stepKey="assertSuccessMessage3"/> + <!--Create Nested fifth Sub Category />--> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton4"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{FifthLevelCat.name}}" stepKey="fillFifthSubCategoryName"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveFifthLevelCategory"/> + <waitForPageLoad stepKey="waitForFifthCategorySaved"/> + <!-- Verify success message --> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the category." stepKey="assertSuccessMessage4"/> + <amOnPage url="/{{FirstLevelSubCat.name}}/{{SecondLevelSubCat.name}}/{{ThirdLevelSubCat.name}}/{{FourthLevelSubCat.name}}/{{FifthLevelCat.name}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> + <waitForPageLoad time="60" stepKey="waitForStoreFrontPageLoad"/> + <!--<Verify category displayed in store front page--> + <grabMultiple selector=".breadcrumbs li" stepKey="breadcrumbs"/> + <assertEquals stepKey="verifyTheCategoryInStoreFrontPage"> + <expectedResult type="array">['Home', {{FirstLevelSubCat.name}}, {{SecondLevelSubCat.name}}, {{ThirdLevelSubCat.name}}, {{FourthLevelSubCat.name}}, {{FifthLevelCat.name}} ]</expectedResult> + <actualResult type="variable">breadcrumbs</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveCategoryTest.xml new file mode 100644 index 000000000000..96f945da138b --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveCategoryTest.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateCategoryWithInactiveCategoryTest"> + <annotations> + <stories value="Create categories"/> + <title value="Create disabled subcategory"/> + <description value="Login as admin and create category with inactivated enable category option"/> + <testCaseId value="MC-5268"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + </before> + <after> + <actionGroup ref="DeleteCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Create In active Category --> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForCategoryIndexPageToBeLoaded"/> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> + <click selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="disableCategory"/> + <checkOption selector="{{AdminCategoryBasicFieldSection.IncludeInMenu}}" stepKey="enableIncludeInMenu"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="fillCategoryName"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForCategorySaved"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the category." stepKey="assertSuccessMessage"/> + <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seePageTitle" /> + <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="dontCategoryIsChecked"/> + <!--Verify InActive Category is created--> + <seeElement selector="{{AdminCategoryContentSection.categoryInTree(_defaultCategory.name)}}" stepKey="seeCategoryInTree" /> + <!--Verify Category is not listed store front page--> + <amOnPage url="{{StorefrontCategoryPage.url(_defaultCategory.name)}}" stepKey="amOnCategoryPage"/> + <waitForPageLoad stepKey="waitForPageToBeLoaded"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="dontSeeCategoryOnStoreFrontPage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveIncludeInMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveIncludeInMenuTest.xml new file mode 100644 index 000000000000..c983089163f7 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveIncludeInMenuTest.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="AdminCreateCategoryWithInactiveIncludeInMenuTest"> + <annotations> + <stories value="Create categories"/> + <title value="Create not included in menu subcategory"/> + <description value="Login as admin and create category with inactivated include in menu option"/> + <testCaseId value="MC-5269"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + </before> + <after> + <actionGroup ref="DeleteCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Create Category with not included in menu Subcategory --> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForCategoryIndexPageToBeLoaded"/> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> + <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> + <click selector="{{AdminCategoryBasicFieldSection.includeInMenuLabel}}" stepKey="disableIncludeInMenu"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="fillCategoryName"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForCategorySaved"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the category." stepKey="assertSuccessMessage"/> + <waitForPageLoad stepKey="waitForPageSaved"/> + <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seePageTitle" /> + <!--Verify Category is created/>--> + <seeElement selector="{{AdminCategoryContentSection.activeCategoryInTree(_defaultCategory.name)}}" stepKey="seeCategoryInTree" /> + <!--Verify Category in store front page menu/>--> + <amOnPage url="{{StorefrontCategoryPage.url(_defaultCategory.name)}}" stepKey="amOnCategoryPage"/> + <waitForPageLoad stepKey="waitForPageToBeLoaded"/> + <see selector="{{StorefrontCategoryMainSection.CategoryTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seeCategoryPageTitle"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="dontSeeCategoryOnNavigation"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithProductsGridFilter.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithProductsGridFilter.xml new file mode 100644 index 000000000000..79eec02a828f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithProductsGridFilter.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="AdminCreateCategoryWithProductsGridFilter"> + <annotations> + <stories value="Create categories"/> + <title value="Apply category products grid filter"/> + <description value="Login as admin and create default product and product with grid filter"/> + <testCaseId value="MC-5273"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + <group value="Catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + </before> + <after> + <actionGroup ref="DeleteCategory" stepKey="deleteCategory"/> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct1"> + <argument name="product" value="defaultSimpleProduct"/> + </actionGroup> + <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"/> + <waitForPageLoad stepKey="waitForProductList"/> + <!--Create Default Product--> + <click selector="{{AdminProductGridActionSection.addProductBtn}}" stepKey="clickAddDefaultProduct"/> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="fillDefaultProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{SimpleProduct.sku}}" stepKey="fillDefaultProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{SimpleProduct.price}}" stepKey="fillDefaultProductPrice"/> + <scrollTo selector="{{AdminProductFormBundleSection.seoDropdown}}" stepKey="scrollToSearchEngine"/> + <click selector="{{AdminProductFormBundleSection.seoDropdown}}" stepKey="selectSearchEngineOptimization"/> + <fillField selector="{{AdminProductFormBundleSection.urlKey}}" userInput="{{SimpleProduct.urlKey}}" stepKey="fillUrlKey"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveDefaultProduct"/> + <waitForPageLoad stepKey="waitForPDefaultProductSaved"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="successMessageYouSavedTheProductIsShown"/> + <!--Create product with grid filter Not Visible Individually--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="ProductList"/> + <waitForPageLoad stepKey="waitForProductPage"/> + <click selector="{{AdminProductGridActionSection.addProductBtn}}" stepKey="clickAddFilterProduct"/> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{defaultSimpleProduct.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{defaultSimpleProduct.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{defaultSimpleProduct.price}}" stepKey="fillProductPrice"/> + <selectOption selector="{{AdminCategoryProductsGridSection.productVisibility}}" userInput="Not Visible Individually" stepKey="selectProductVisibility"/> + <scrollTo selector="{{AdminProductFormBundleSection.seoDropdown}}" stepKey="scrollToSearchEngineOptimization"/> + <click selector="{{AdminProductFormBundleSection.seoDropdown}}" stepKey="selectSearchEngineOptimization1"/> + <fillField selector="{{AdminProductFormBundleSection.urlKey}}" userInput="{{defaultSimpleProduct.urlKey}}" stepKey="fillUrlKey1"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> + <waitForPageLoad stepKey="waitForProductSaved"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoaded"/> + <!--Create sub category--> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> + <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="fillCategoryName"/> + <!--Select the default product and product with grid filter--> + <scrollTo selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" x="0" y="-80" stepKey="scrollToProductInCategory"/> + <click selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" stepKey="clickOnProductInCategory"/> + <fillField selector="{{AdminCategoryContentSection.productTableColumnName}}" userInput="{{SimpleProduct.name}}" stepKey="selectProduct"/> + <click selector="{{AdminCategoryContentSection.productSearch}}" stepKey="clickSearchButton"/> + <click selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="selectProductFromRow"/> + <fillField selector="{{AdminCategoryContentSection.productTableColumnName}}" userInput="{{defaultSimpleProduct.name}}" stepKey="selectDefaultProduct"/> + <click selector="{{AdminCategoryContentSection.productSearch}}" stepKey="clickSearchButton1"/> + <click selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="selectDefaultProductFromTableRow"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="WaitForCategorySaved"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the category." stepKey="successMessageYouSavedTheCategory"/> + <!--Verify product with grid filter is not not visible--> + <amOnPage url="{{StorefrontProductPage.url(defaultSimpleProduct.urlKey)}}" stepKey="seeOnProductPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageToLoad"/> + <dontSee selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{defaultSimpleProduct.name}}" stepKey="dontSeeProductInStoreFrontPage"/> + <!--Verify product in Store Front Page--> + <amOnPage url="{{StorefrontProductPage.url(SimpleProduct.urlKey)}}" stepKey="seeDefaultProductPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageToLoad1"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="seeProductInStoreFrontPage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithRequiredFieldsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithRequiredFieldsTest.xml new file mode 100644 index 000000000000..1b6c9707b065 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithRequiredFieldsTest.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateCategoryWithRequiredFieldsTest"> + <annotations> + <stories value="Create categories"/> + <title value="Create Category from Category page with Required Fields Only"/> + <description value="Login as an admin and create a category with required fields."/> + <testCaseId value="MC-5265"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + </before> + <after> + <actionGroup ref="DeleteCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Create subcategory with required fields --> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForCategoryIndexPageToBeLoaded"/> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="fillCategoryName"/> + <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForCategorySaved"/> + <!-- Verify success message --> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the category." stepKey="assertSuccessMessage"/> + <!-- Verify subcategory created with required fields --> + <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seePageTitle" /> + <seeElement selector="{{AdminCategoryContentSection.activeCategoryInTree(_defaultCategory.name)}}" stepKey="seeCategoryInTree" /> + <!--Verify Category is listed in store front page menu/>--> + <amOnPage url="{{StorefrontCategoryPage.url(_defaultCategory.name)}}" stepKey="amOnCategoryPage"/> + <waitForPageLoad stepKey="waitForPageToBeLoaded"/> + <see selector="{{StorefrontCategoryMainSection.CategoryTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seeCategoryPageTitle"/> + </test> +</tests> 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 000000000000..a3f543e9cf32 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml @@ -0,0 +1,142 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateCustomProductAttributeWithDropdownFieldTest"> + <annotations> + <stories value="Create product Attribute"/> + <title value="Create Custom Product Attribute Dropdown Field (Not Required) from Product Page"/> + <description value="login as admin and create configurable product attribute with Dropdown field"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10905"/> + <group value="mtf_migrated"/> + <skip> + <issueId value="MC-15474"/> + </skip> + </annotations> + + <before> + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + + <!--Create Category--> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + + <!--Create Configurable Product--> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <!--Delete created entity --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + + <actionGroup ref="deleteProductAttribute" stepKey="deleteCreatedAttribute"> + <argument name="ProductAttribute" value="newProductAttribute"/> + </actionGroup> + + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Open Product Index Page--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + + <!-- Select Created Product--> + <actionGroup ref="filterProductGridBySku" stepKey="filterProductGridBySku"> + <argument name="product" value="$$createConfigProduct$$"/> + </actionGroup> + <click stepKey="openFirstProduct" selector="{{AdminProductGridSection.productRowBySku($$createConfigProduct.sku$$)}}"/> + <waitForPageLoad stepKey="waitForProductToLoad"/> + + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="100" stepKey="fillProductQty"/> + <selectOption selector="{{AdminProductFormSection.productStockStatus}}" userInput="In Stock" stepKey="selectStockStatus"/> + + <!-- Create New Product Attribute --> + <click selector="{{AdminProductFormSection.addAttributeBtn}}" stepKey="clickOnAddAttribute"/> + <waitForPageLoad stepKey="waitForAttributePageToLoad"/> + <click selector="{{AdminProductFormSection.createNewAttributeBtn}}" stepKey="clickCreateNewAttributeButton"/> + <waitForPageLoad stepKey="waitForNewAttributePageToLoad"/> + <waitForElementVisible selector="{{AdminCreateNewProductAttributeSection.defaultLabel}}" stepKey="waitForDefaultLabelToBeVisible"/> + <fillField selector="{{AdminCreateNewProductAttributeSection.defaultLabel}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="fillAttributeLabel"/> + <selectOption selector="{{AdminCreateNewProductAttributeSection.inputType}}" userInput="Dropdown" stepKey="selectInputType"/> + <waitForElementVisible selector="{{AdminCreateNewProductAttributeSection.addValue}}" stepKey="waitForAddValueButtonToVisible"/> + <click selector="{{AdminCreateNewProductAttributeSection.addValue}}" stepKey="clickOnAddValueButton"/> + <waitForElementVisible selector="{{AdminCreateNewProductAttributeSection.defaultStoreView('0')}}" stepKey="waitForDefaultStoreViewToVisible"/> + <fillField selector="{{AdminCreateNewProductAttributeSection.defaultStoreView('0')}}" userInput="{{ProductAttributeOption8.label}}" stepKey="fillDefaultStoreView"/> + <fillField selector="{{AdminCreateNewProductAttributeSection.adminOption('0')}}" userInput="{{ProductAttributeOption8.value}}" stepKey="fillAdminField"/> + <checkOption selector="{{AdminCreateNewProductAttributeSection.defaultRadioButton('1')}}" stepKey="selectRadioButton"/> + <click selector="{{AdminCreateNewProductAttributeSection.advancedAttributeProperties}}" stepKey="clickOnAdvancedAttributeProperties"/> + <waitForElementVisible selector="{{AdminCreateNewProductAttributeSection.attributeCode}}" stepKey="waitForAttributeCodeToVisible"/> + <scrollTo selector="{{AdminCreateNewProductAttributeSection.attributeCode}}" stepKey="scrollToAttributeCode"/> + <fillField selector="{{AdminCreateNewProductAttributeSection.attributeCode}}" userInput="{{newProductAttribute.attribute_code}}" stepKey="fillAttributeCode"/> + <scrollTo selector="{{AdminCreateNewProductAttributeSection.isUnique}}" stepKey="scrollToIsUniqueOption"/> + <checkOption selector="{{AdminCreateNewProductAttributeSection.isUnique}}" stepKey="enableIsUniqueOption"/> + <scrollTo selector="{{AdminCreateNewProductAttributeSection.advancedAttributeProperties}}" stepKey="scrollToAdvancedAttributeProperties"/> + <click selector="{{AdminCreateNewProductAttributeSection.advancedAttributeProperties}}" stepKey="clickOnAdvancedAttributeProperties1"/> + <scrollTo selector="{{AdminCreateNewProductAttributeSection.storefrontProperties}}" stepKey="scrollToStorefrontProperties"/> + <click selector="{{AdminCreateNewProductAttributeSection.storefrontProperties}}" stepKey="clickOnStorefrontProperties"/> + <waitForPageLoad stepKey="waitForStoreFrontPropertiesTodiaplay"/> + <scrollTo selector="{{AdminCreateNewProductAttributeSection.sortProductListing}}" x="0" y="-80" stepKey="scroll1ToSortProductListing"/> + <checkOption selector="{{AdminCreateNewProductAttributeSection.inSearch}}" stepKey="enableInSearchOption"/> + <checkOption selector="{{AdminCreateNewProductAttributeSection.advancedSearch}}" stepKey="enableAdvancedSearch"/> + <checkOption selector="{{AdminCreateNewProductAttributeSection.isComparable}}" stepKey="enableComparableOption"/> + <checkOption selector="{{AdminCreateNewProductAttributeSection.allowHtmlTags}}" stepKey="enableAllowHtmlTags"/> + <checkOption selector="{{AdminCreateNewProductAttributeSection.visibleOnStorefront}}" stepKey="enableVisibleOnStorefront"/> + <checkOption selector="{{AdminCreateNewProductAttributeSection.sortProductListing}}" stepKey="enableSortProductListing"/> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <click selector="{{AdminCreateNewProductAttributeSection.saveAttribute}}" stepKey="clickOnSaveAttribute"/> + <waitForPageLoad stepKey="waitForAttributeToSave"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="saveTheProduct"/> + <waitForPageLoad stepKey="waitForProductToSave"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> + + <!--Verify product attribute added in product form --> + <scrollTo selector="{{AdminProductFormSection.contentTab}}" stepKey="scrollToContentTab"/> + <waitForElementVisible selector="{{AdminProductFormSection.attributeTab}}" stepKey="waitForAttributeToVisible"/> + <click selector="{{AdminProductFormSection.attributeTab}}" stepKey="clickOnAttribute"/> + <seeElement selector="{{AdminProductFormSection.attributeLabelByText(ProductAttributeFrontendLabel.label)}}" stepKey="seeAttributeLabelInProductForm"/> + + <!--Verify Product Attribute in Attribute Form --> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="{{newProductAttribute.attribute_code}}" stepKey="setAttributeCode"/> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> + <waitForPageLoad stepKey="waitForPageLoad" /> + <see selector="{{AdminProductAttributeGridSection.attributeCodeColumn}}" userInput="{{newProductAttribute.attribute_code}}" stepKey="seeAttributeCode"/> + <see selector="{{AdminProductAttributeGridSection.defaultLabelColumn}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="seeDefaultLabel"/> + <see selector="{{AdminProductAttributeGridSection.isVisibleColumn}}" userInput="Yes" stepKey="seeIsVisibleColumn"/> + <see selector="{{AdminProductAttributeGridSection.isSearchableColumn}}" userInput="Yes" stepKey="seeSearchableColumn"/> + <see selector="{{AdminProductAttributeGridSection.isComparableColumn}}" userInput="Yes" stepKey="seeComparableColumn"/> + + <!--Verify Product Attribute is present in Category Store Front Page --> + <amOnPage url="$$createCategory.name$$.html" stepKey="goToStorefrontPage"/> + <waitForPageLoad stepKey="waitForProductFrontPageToLoad"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="clickOnCategory"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{StorefrontCategoryMainSection.productLink}}" stepKey="openSearchedProduct"/> + <waitForPageLoad stepKey="waitForProductToLoad1"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createConfigProduct.name$$" stepKey="seeProductNameInStoreFront"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createConfigProduct.price$$" stepKey="seeProductPriceInStoreFront"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="$$createConfigProduct.sku$$" stepKey="seeProductSkuInStoreFront"/> + <scrollTo selector="{{StorefrontProductMoreInformationSection.moreInformation}}" stepKey="scrollToMoreInformation"/> + <see selector="{{StorefrontProductMoreInformationSection.attributeLabel}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="seeAttributeLabel"/> + <see selector="{{StorefrontProductMoreInformationSection.attributeValue}}" userInput="{{ProductAttributeOption8.value}}" stepKey="seeAttributeValue"/> + + <!--Verify Product Attribute present in search page --> + <amOnPage url="$$createCategory.name$$.html" stepKey="goToStorefrontPage1"/> + <waitForPageLoad stepKey="waitForProductFrontPageToLoad1"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{ProductAttributeOption8.value}}" stepKey="fillAttribute"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearchResultToLoad"/> + <see selector="{{StorefrontCategoryMainSection.productName}}" userInput="$$createConfigProduct.name$$" stepKey="seeProductNameInCategoryPage"/> + <see selector="{{StorefrontCategoryMainSection.productOptionList}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="seeProductAttributeOptionInCategoryPage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeTest.xml new file mode 100644 index 000000000000..525f81de6c48 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeTest.xml @@ -0,0 +1,72 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateDropdownProductAttributeTest"> + <annotations> + <features value="Catalog"/> + <stories value="Create/configure Dropdown product attribute"/> + <title value="Admin should be able to create dropdown product attribute"/> + <description value="Admin should be able to create dropdown product attribute"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-4982"/> + <group value="Catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <!-- Remove attribute --> + <actionGroup ref="deleteProductAttribute" stepKey="deleteAttribute"> + <argument name="ProductAttribute" value="productDropDownAttribute"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <amOnPage url="{{ProductAttributePage.url}}" stepKey="navigateToNewProductAttributePage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + + <!-- Set attribute properties --> + <fillField selector="{{AttributePropertiesSection.DefaultLabel}}" + userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="fillDefaultLabel"/> + <selectOption selector="{{AttributePropertiesSection.InputType}}" + userInput="{{productDropDownAttribute.frontend_input}}" stepKey="fillInputType"/> + + <!-- Set advanced attribute properties --> + <click selector="{{AdvancedAttributePropertiesSection.AdvancedAttributePropertiesSectionToggle}}" + stepKey="showAdvancedAttributePropertiesSection"/> + <waitForElementVisible selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" + stepKey="waitForSlideOut"/> + <fillField selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" + userInput="{{productDropDownAttribute.attribute_code}}" + stepKey="fillAttributeCode"/> + + <!-- Add new attribute options --> + <click selector="{{AttributeOptionsSection.AddOption}}" stepKey="clickAddOption1"/> + <fillField selector="{{DropdownAttributeOptionsSection.nthOptionAdminLabel('1')}}" + userInput="Fish and Chips" stepKey="fillAdminValue1"/> + + <click selector="{{AttributeOptionsSection.AddOption}}" stepKey="clickAddOption2"/> + <fillField selector="{{DropdownAttributeOptionsSection.nthOptionAdminLabel('2')}}" + userInput="Fish & Chips" stepKey="fillAdminValue2"/> + + <!-- Save the new product attribute --> + <click selector="{{AttributePropertiesSection.Save}}" stepKey="clickSave1"/> + <waitForPageLoad stepKey="waitForGridPageLoad1"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" + stepKey="waitForSuccessMessage"/> + + <actionGroup ref="navigateToCreatedProductAttribute" stepKey="navigateToAttribute"> + <argument name="ProductAttribute" value="productDropDownAttribute"/> + </actionGroup> + <!-- Check attribute data --> + <grabValueFrom selector="{{DropdownAttributeOptionsSection.nthOptionAdminLabel('2')}}" + stepKey="secondOptionAdminLabel"/> + <assertEquals actual="$secondOptionAdminLabel" expected="'Fish & Chips'" + stepKey="assertSecondOption"/> + </test> +</tests> \ No newline at end of file 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 000000000000..1bc69be642a3 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml @@ -0,0 +1,87 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest"> + <annotations> + <features value="Catalog"/> + <stories value="Create product Dropdown attribute and check its visibility on frontend in Advanced Search form"/> + <title value="AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest"/> + <description value="Admin should able to create product Dropdown attribute and check its visibility on frontend in Advanced Search form"/> + <testCaseId value="MC-10827"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <!-- Create product attribute with 2 options --> + <createData entity="productDropDownAttributeNotSearchable" stepKey="attribute"/> + <createData entity="productAttributeOption1" stepKey="option1"> + <requiredEntity createDataKey="attribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="option2"> + <requiredEntity createDataKey="attribute"/> + </createData> + + <!-- Create product attribute set --> + <createData entity="CatalogAttributeSet" stepKey="createAttributeSet"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Filter product attribute set by attribute set name --> + <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="amOnAttributeSetPage"/> + <actionGroup ref="FilterProductAttributeSetGridByAttributeSetName" stepKey="filterProductAttrSetGridByAttrSetName"> + <argument name="name" value="$$createAttributeSet.attribute_set_name$$"/> + </actionGroup> + + <!-- Assert created attribute in an unassigned attributes --> + <see userInput="$$attribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.unassignedAttributesTree}}" stepKey="seeAttributeInUnassignedAttr"/> + + <!-- Assign attribute in the group --> + <actionGroup ref="AssignAttributeToGroup" stepKey="assignAttributeToGroup"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="$$attribute.attribute_code$$"/> + </actionGroup> + <see userInput="$$attribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.groupTree}}" stepKey="seeAttributeInGroup"/> + <actionGroup ref="SaveAttributeSet" stepKey="saveAttributeSet"/> + + <!-- Go to Product Attribute Grid page --> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="$$attribute.attribute_code$$" stepKey="fillAttrCodeField" /> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearchBtn" /> + <click selector="{{AdminProductAttributeGridSection.FirstRow}}" stepKey="chooseFirstRow" /> + + <!-- Change attribute property: Frontend Label --> + <fillField selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{productDropDownAttribute.attribute_code}}" stepKey="fillDefaultLabel"/> + + <!-- Change attribute property: Use in Search >Yes --> + <scrollToTopOfPage stepKey="scrollToTabs"/> + <click selector="{{StorefrontPropertiesSection.StoreFrontPropertiesTab}}" stepKey="clickStorefrontPropertiesTab"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <selectOption selector="{{AdvancedAttributePropertiesSection.UseInSearch}}" userInput="Yes" stepKey="seeInSearch"/> + + <!-- Change attribute property: Visible In Advanced Search >No --> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <selectOption selector="{{AdvancedAttributePropertiesSection.VisibleInAdvancedSearch}}" userInput="No" stepKey="dontSeeInAdvancedSearch"/> + + <!-- Save the new product attributes --> + <click selector="{{AttributePropertiesSection.Save}}" stepKey="clickSave"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSuccessMessage"/> + + <!-- Flash cache --> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + + <!-- Go to store's advanced catalog search page --> + <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> + <dontSeeElement selector="{{StorefrontCatalogSearchAdvancedFormSection.AttributeByCode('$$attribute.attribute_code$$')}}" stepKey="dontSeeAttribute"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateCategoryTest.xml new file mode 100644 index 000000000000..37ec4e0d3252 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateCategoryTest.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateDuplicateCategoryTest"> + <annotations> + <stories value="Create category"/> + <title value="Create Duplicate Category With Already Existed UrlKey"/> + <description value="Login as admin and create duplicate category"/> + <testCaseId value="MC-14702"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="SimpleSubCategory" stepKey="category"/> + </before> + <after> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Open Category Page and select Add category --> + <actionGroup ref="goToCreateCategoryPage" stepKey="goToCategoryPage"/> + + <!-- Fill the Category form with same name and urlKey as initially created category(SimpleSubCategory) --> + <actionGroup ref="FillCategoryNameAndUrlKeyAndSave" stepKey="fillCategoryForm"> + <argument name="categoryName" value="$$category.name$$"/> + <argument name="categoryUrlKey" value="$$category.custom_attributes[url_key]$$"/> + </actionGroup> + + <!-- Assert error message --> + <see selector="{{AdminCategoryMessagesSection.errorMessage}}" userInput="The value specified in the URL Key field would generate a URL that already exists." stepKey="seeErrorMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateProductTest.xml new file mode 100644 index 000000000000..575bb56912b2 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateProductTest.xml @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateDuplicateProductTest"> + <annotations> + <stories value="Create Product"/> + <title value="Create Duplicate Product With Existed Subcategory Name And UrlKey"/> + <description value="Login as admin and create duplicate Product"/> + <testCaseId value="MC-14714"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="SubCategory" stepKey="category"/> + <createData entity="Two_nested_categories" stepKey="subCategory"> + <requiredEntity createDataKey="category"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + + <after> + <deleteData createDataKey="subCategory" stepKey="deleteSubCategory"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to new simple product page --> + <actionGroup ref="GoToSpecifiedCreateProductPage" stepKey="goToCreateProductPage"/> + + <!-- Fill the main fields in the form --> + <actionGroup ref="FillMainProductFormByString" stepKey="fillMainProductForm"> + <argument name="productName" value="$$subCategory.name$$"/> + <argument name="productSku" value="{{defaultSimpleProduct.sku}}"/> + <argument name="productPrice" value="{{defaultSimpleProduct.price}}"/> + <argument name="productQuantity" value="{{defaultSimpleProduct.quantity}}"/> + <argument name="productStatus" value="{{defaultSimpleProduct.status}}"/> + <argument name="productWeight" value="{{defaultSimpleProduct.weight}}"/> + </actionGroup> + + <!-- Select the category that we created in the before block --> + <actionGroup ref="SetCategoryByName" stepKey="setCategory"> + <argument name="categoryName" value="$$category.name$$"/> + </actionGroup> + + <!-- Set the url key to match the subcategory created in the before block --> + <actionGroup ref="SetProductUrlKeyByString" stepKey="fillUrlKey"> + <argument name="urlKey" value="$$subCategory.custom_attributes[url_key]$$"/> + </actionGroup> + + <!-- Save the product and expect to see an error message --> + <actionGroup ref="SaveProductFormNoSuccessCheck" stepKey="tryToSaveProduct"/> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="The value specified in the URL Key field would generate a URL that already exists." stepKey="seeErrorMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml new file mode 100644 index 000000000000..21b3dba7140c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml @@ -0,0 +1,90 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest"> + <annotations> + <stories value="Create category"/> + <title value="Flat Catalog - Update Inactive Category as Inactive, Should Not be Visible on Storefront"/> + <description value="Login as admin and create inactive flat category and update category as inactive and verify category is not visible in store front"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-11009"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!--Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <!--Create category--> + <createData entity="CatNotActive" stepKey="createCategory"/> + <!-- Create First StoreView --> + <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewEn"> + <argument name="storeView" value="customStoreEN"/> + </actionGroup> + <!-- Create Second StoreView --> + <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewFr"> + <argument name="storeView" value="customStoreFR"/> + </actionGroup> + <!--Run full reindex and clear caches --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!--Enable Flat Catalog Category --> + <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 1"/> + <!--Open Index Management Page and Select Index mode "Update by Schedule" --> + <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + </before> + <after> + <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> + <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="realtime" /> + <magentoCLI stepKey="indexerReindex" command="indexer:reindex" /> + <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewFr"> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Select created category and make category inactive--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(CatNotActive.name)}}" stepKey="selectCreatedCategory"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> + <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{CatNotActive.name}}" stepKey="seeUpdatedCategoryTitle"/> + <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="verifyInactiveCategory"/> + <!--Run full reindex and clear caches --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!--Open Index Management Page --> + <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> + <waitForPageLoad stepKey="waitForIndexPageToBeLoaded"/> + <see stepKey="seeIndexStatus" selector="{{AdminIndexManagementSection.indexerStatus('Category Flat Data')}}" userInput="Ready"/> + <!--Verify Category In Store Front--> + <amOnPage url="/$$createCategory.name$$.html" stepKey="openCategoryPage1"/> + <waitForPageLoad stepKey="waitForCategoryStoreFrontPageToLoad"/> + <seeElement selector="{{StorefrontBundledSection.pageNotFound}}" stepKey="seeWhoopsOurBadMessage"/> + <!--Verify category is not visible in First Store View --> + <click stepKey="selectStoreSwitcher" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> + <click stepKey="selectForstStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreEN.name)}}"/> + <waitForPageLoad stepKey="waitForFirstStoreView"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="dontSeeCategoryOnNavigation"/> + <!--Verify category is not visible in Second Store View --> + <click stepKey="selectStoreSwitcher1" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> + <click stepKey="selectSecondStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreFR.name)}}"/> + <waitForPageLoad stepKey="waitForSecondStoreView"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="dontSeeCategoryOnNavigation1"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml new file mode 100644 index 000000000000..aa3dba85dfad --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateInactiveFlatCategoryTest"> + <annotations> + <stories value="Create category"/> + <title value="Flat Catalog - Create Category as Inactive, Should Not be Visible on Storefront"/> + <description value="Login as admin and create flat Inactive category and verify category is not visible in store front"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-11007"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!--Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <!--Create category--> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <!-- Create First StoreView --> + <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewEn"> + <argument name="storeView" value="customStoreEN"/> + </actionGroup> + <!-- Create Second StoreView --> + <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewFr"> + <argument name="storeView" value="customStoreFR"/> + </actionGroup> + <!--Run full reindex and clear caches --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!--Enable Flat Catalog Category --> + <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 1"/> + <!--Open Index Management Page and Select Index mode "Update by Schedule" --> + <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + </before> + <after> + <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> + <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="realtime" /> + <magentoCLI stepKey="indexerReindex" command="indexer:reindex" /> + <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewFr"> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Select created category and make category inactive--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="selectCreatedCategory"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> + <click selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="disableActiveCategory"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> + <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{SimpleSubCategory.name}}" stepKey="seeUpdatedCategoryTitle"/> + <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="verifyInactiveIncludeInMenu"/> + <!--Run full reindex and clear caches --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!--Open Index Management Page --> + <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> + <waitForPageLoad stepKey="waitForIndexPageToBeLoaded"/> + <see stepKey="seeIndexStatus" selector="{{AdminIndexManagementSection.indexerStatus('Category Flat Data')}}" userInput="Ready"/> + <!--Verify Category In Store Front--> + <amOnPage url="/$$createCategory.name$$.html" stepKey="openCategoryPage1"/> + <waitForPageLoad stepKey="waitForCategoryStoreFrontPageToLoad"/> + <!--Verify category is not visible in First Store View --> + <click stepKey="selectStoreSwitcher" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> + <click stepKey="selectForstStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreEN.name)}}"/> + <waitForPageLoad stepKey="waitForFirstStoreView"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="dontSeeCategoryOnNavigation"/> + <seeElement selector="{{StorefrontBundledSection.pageNotFound}}" stepKey="seeWhoopsOurBadMessage"/> + <!--Verify category is not visible in Second Store View --> + <click stepKey="selectStoreSwitcher1" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> + <click stepKey="selectSecondStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreFR.name)}}"/> + <waitForPageLoad stepKey="waitForSecondstoreView"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="dontSeeCategoryOnNavigation1"/> + <seeElement selector="{{StorefrontBundledSection.pageNotFound}}" stepKey="seeWhoopsOurBadMessage1"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml new file mode 100644 index 000000000000..37417cd7fdb8 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml @@ -0,0 +1,91 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateInactiveInMenuFlatCategoryTest"> + <annotations> + <stories value="Create category"/> + <title value="Flat Catalog - Exclude Category from Navigation Menu"/> + <description value="Login as admin and create inactive Include In Menu flat category and verify category is not displayed in Navigation Menu"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-11008"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!--Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <!--Create category--> + <createData entity="SimpleSubCategory" stepKey="category"/> + <!-- Create First StoreView --> + <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewEn"> + <argument name="storeView" value="customStoreEN"/> + </actionGroup> + <!-- Create Second StoreView --> + <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewFr"> + <argument name="storeView" value="customStoreFR"/> + </actionGroup> + <!--Run full reindex and clear caches --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!--Enable Flat Catalog Category --> + <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 1"/> + <!--Open Index Management Page and Select Index mode "Update by Schedule" --> + <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + </before> + <after> + <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> + <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="realtime" /> + <magentoCLI stepKey="indexerReindex" command="indexer:reindex" /> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewFr"> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Select created category and disable Include In Menu option--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="selectCreatedCategory"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> + <click selector="{{AdminCategoryBasicFieldSection.includeInMenuLabel}}" stepKey="disableIcludeInMenuOption"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> + <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <!--Verify category is saved and Include In Menu Option is disabled in Category Page --> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{SimpleSubCategory.name}}" stepKey="seeUpdatedCategoryTitle"/> + <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.includeInMenuLabel}}" stepKey="verifyInactiveIncludeInMenu"/> + <!--Run full reindex and clear caches --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!--Open Index Management Page --> + <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> + <waitForPageLoad stepKey="waitForIndexPageToBeLoaded"/> + <see stepKey="seeIndexStatus" selector="{{AdminIndexManagementSection.indexerStatus('Category Flat Data')}}" userInput="Ready"/> + <!--Verify Category In Store Front--> + <amOnPage url="/$$category.name$$.html" stepKey="openCategoryPage1"/> + <waitForPageLoad stepKey="waitForCategoryStoreFrontPageToLoad"/> + <!--Verify category is not displayed in navigation menu in First Store View --> + <click stepKey="selectStoreSwitcher" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> + <click stepKey="selectForstStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreEN.name)}}"/> + <waitForPageLoad stepKey="waitForFirstStoreView"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$category.name$$)}}" stepKey="seeCategoryOnNavigation"/> + <!--Verify category is not displayed in navigation menu in Second Store View --> + <click stepKey="selectStoreSwitcher1" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> + <click stepKey="selectSecondStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreFR.name)}}"/> + <waitForPageLoad stepKey="waitForSecondstoreView"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$category.name$$)}}" stepKey="seeCategoryOnNavigation1"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml new file mode 100644 index 000000000000..1f558568e924 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml @@ -0,0 +1,87 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest"> + <annotations> + <features value="Catalog"/> + <stories value="Create Multiple Select product attribute and check its visibility in Advanced Search form"/> + <title value="Create product attribute of type Multiple Select and check its visibility on frontend in Advanced Search form"/> + <description value="Admin should be able to create product attribute of type Multiple Select and check its visibility on frontend in Advanced Search form"/> + <testCaseId value="MC-10828"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <!-- Create a multiple select product attribute with two options --> + <createData entity="productAttributeMultiselectTwoOptionsNotSearchable" stepKey="attribute"/> + <createData entity="productAttributeOption1" stepKey="option1"> + <requiredEntity createDataKey="attribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="option2"> + <requiredEntity createDataKey="attribute"/> + </createData> + + <!-- Create product attribute set --> + <createData entity="CatalogAttributeSet" stepKey="createAttributeSet"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Filter product attribute set by attribute set name --> + <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="amOnAttributeSetPage"/> + <actionGroup ref="FilterProductAttributeSetGridByAttributeSetName" stepKey="filterProductAttrSetGridByAttrSetName"> + <argument name="name" value="$$createAttributeSet.attribute_set_name$$"/> + </actionGroup> + + <!-- Assert created attribute in an unassigned attributes --> + <see userInput="$$attribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.unassignedAttributesTree}}" stepKey="seeAttributeInUnassignedAttr"/> + + <!-- Assign attribute in the group --> + <actionGroup ref="AssignAttributeToGroup" stepKey="assignAttributeToGroup"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="$$attribute.attribute_code$$"/> + </actionGroup> + <see userInput="$$attribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.groupTree}}" stepKey="seeAttributeInGroup"/> + <actionGroup ref="SaveAttributeSet" stepKey="saveAttributeSet"/> + + <!-- Go to Product Attribute Grid page --> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="$$attribute.attribute_code$$" stepKey="fillAttrCodeField" /> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearchBtn" /> + <click selector="{{AdminProductAttributeGridSection.FirstRow}}" stepKey="chooseFirstRow" /> + + <!-- Change attribute property: Frontend Label --> + <fillField selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{productDropDownAttribute.attribute_code}}" stepKey="fillDefaultLabel"/> + + <!-- Change attribute property: Use in Search >Yes --> + <scrollToTopOfPage stepKey="scrollToTabs"/> + <click selector="{{StorefrontPropertiesSection.StoreFrontPropertiesTab}}" stepKey="clickStorefrontPropertiesTab"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <selectOption selector="{{AdvancedAttributePropertiesSection.UseInSearch}}" userInput="Yes" stepKey="seeInSearch"/> + + <!-- Change attribute property: Visible In Advanced Search >No --> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <selectOption selector="{{AdvancedAttributePropertiesSection.VisibleInAdvancedSearch}}" userInput="No" stepKey="dontSeeInAdvancedSearch"/> + + <!-- Save the new product attributes --> + <click selector="{{AttributePropertiesSection.Save}}" stepKey="clickSave"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSuccessMessage"/> + + <!-- Flash cache --> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + + <!-- Go to store's advanced catalog search page --> + <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> + <dontSeeElement selector="{{StorefrontCatalogSearchAdvancedFormSection.AttributeByCode('$$attribute.attribute_code$$')}}" stepKey="dontSeeAttribute"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductTest.xml new file mode 100644 index 000000000000..282331924bca --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductTest.xml @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateNewAttributeFromProductTest"> + <annotations> + <features value="Catalog"/> + <title value="Check that New Attribute from Product is create"/> + <description value="Check that New Attribute from Product is create"/> + <severity value="MAJOR"/> + <testCaseId value="MC-12296"/> + <useCaseId value="MAGETWO-59055"/> + <group value="Catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + + <!--Create product--> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + </before> + <after> + <!--Delete create data--> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + + <!--Delete store views--> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteFirstStoreView"> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteSecondStoreView"> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> + + <!--Delete Attribute--> + <actionGroup ref="deleteProductAttribute" stepKey="deleteAttribute"> + <argument name="ProductAttribute" value="productDropDownAttribute"/> + </actionGroup> + + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create 2 store views--> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createFirstStoreView"> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createSecondStoreView"> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> + + <!--Go to created product page and create new attribute--> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="openAdminEditPage"/> + <actionGroup ref="AdminCreateAttributeWithValueWithTwoStoreViesFromProductPage" stepKey="createAttribute"> + <argument name="attributeName" value="{{productDropDownAttribute.attribute_code}}"/> + <argument name="attributeType" value="Dropdown"/> + <argument name="firstStoreViewName" value="{{customStoreEN.name}}"/> + <argument name="secondStoreViewName" value="{{customStoreFR.name}}"/> + </actionGroup> + + <!--Check attribute existence in product page attribute section--> + <conditionalClick selector="{{AdminProductAttributeSection.attributeSectionHeader}}" dependentSelector="{{AdminProductAttributeSection.attributeSection}}" visible="false" stepKey="openAttributeSection"/> + <seeElement selector="{{AdminProductAttributeSection.dropDownAttribute(productDropDownAttribute.attribute_code)}}" stepKey="seeNewAttributeInProductPage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml new file mode 100644 index 000000000000..5c798db29b97 --- /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 000000000000..d4d6496e018f --- /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/AdminCreateProductDuplicateUrlkeyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest.xml index 95d74b965311..6658ad36d715 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest.xml @@ -40,4 +40,46 @@ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> <see userInput="The value specified in the URL Key field would generate a URL that already exists" selector="{{AdminProductMessagesSection.errorMessage}}" stepKey="assertErrorMessage"/> </test> + <test name="AdminCreateProductDuplicateProductTest"> + <annotations> + <features value="Catalog"/> + <stories value="Validation Errors"/> + <title value="No validation errors when trying to duplicate product twice"/> + <description value="No validation errors when trying to duplicate product twice"/> + <severity value="MAJOR"/> + <testCaseId value="MC-5472"/> + <group value="product"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <!--Delete all products by filtering grid and using mass delete action--> + <actionGroup ref="deleteAllDuplicateProductUsingProductGrid" stepKey="deleteAllDuplicateProducts"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <deleteData createDataKey="createCategory" stepKey="deletePreReqCatalog" /> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProduct1"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct1"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <!--Save and duplicated the product once--> + <actionGroup ref="AdminFormSaveAndDuplicate" stepKey="saveAndDuplicateProductForm1"/> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProduct2"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct2"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <!--Save and duplicated the product second time--> + <actionGroup ref="AdminFormSaveAndDuplicate" stepKey="saveAndDuplicateProductForm2"/> + </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryRequiredFieldsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryRequiredFieldsTest.xml new file mode 100644 index 000000000000..f98f9acc4696 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryRequiredFieldsTest.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateRootCategoryRequiredFieldsTest"> + <annotations> + <stories value="Create categories"/> + <features value="Catalog"/> + <title value="Create Root Category from Category Page"/> + <description value="Create Root Category from Category Page"/> + <testCaseId value="MC-5263"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="LoginToAdminPanel"/> + </before> + <after> + <actionGroup ref="DeleteCategory" stepKey="deleteCategory"> + <argument name="categoryEntity" value="_defaultCategory" /> + </actionGroup> + <actionGroup ref="logout" stepKey="logout" /> + </after> + + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="OpenAdminCatergoryIndexPage"/> + <click selector="{{AdminCategorySidebarActionSection.AddRootCategoryButton}}" stepKey="ClickOnAddRootButton"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="FillCategoryField"/> + <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="EnableCheckOption"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="ClickSaveButton"/> + <waitForPageLoad stepKey="WaitForCategorySaved"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the category." stepKey="AssertSuccessMessage"/> + <seeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="SeeCheckBoxisSelected"/> + <seeInField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="SeedFieldInput"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithCountryOfManufactureAttributeSKUMaskTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithCountryOfManufactureAttributeSKUMaskTest.xml new file mode 100644 index 000000000000..3487de656173 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithCountryOfManufactureAttributeSKUMaskTest.xml @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateSimpleProductWithCountryOfManufactureAttributeSKUMaskTest"> + <annotations> + <stories value="Create simple product"/> + <title value="Create simple product with (Country of Manufacture) Attribute SKU Mask"/> + <description value="Test log in to Create simple product and Create simple product with (Country of Manufacture) Attribute SKU Mask"/> + <testCaseId value="MC-11024"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <magentoCLI stepKey="setCountryOfManufacture" command="config:set catalog/fields_masks/sku" arguments="{{name}}-{{country_of_manufacture}}"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <magentoCLI stepKey="setName" command="config:set catalog/fields_masks/sku" arguments="{{name}}"/> + <actionGroup ref="deleteProductBySku" stepKey="deleteCreatedProduct"> + <argument name="sku" value="{{nameAndAttributeSkuMaskSimpleProduct.name}}-{{nameAndAttributeSkuMaskSimpleProduct.country_of_manufacture_label}}" /> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="openProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> + <waitForPageLoad stepKey="waitForProductToggleToSelectSimpleProduct"/> + <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="clickSimpleProductFromDropDownList"/> + + <!-- Create simple product with country of manufacture attribute --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{nameAndAttributeSkuMaskSimpleProduct.name}}" stepKey="fillSimpleProductName"/> + <selectOption selector="{{AdminProductFormSection.countryOfManufacture}}" userInput="{{nameAndAttributeSkuMaskSimpleProduct.country_of_manufacture_label}}" stepKey="selectCountryOfManufacture"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{nameAndAttributeSkuMaskSimpleProduct.price}}" stepKey="fillSimpleProductPrice"/> + <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{nameAndAttributeSkuMaskSimpleProduct.weight}}" stepKey="fillSimpleProductWeight"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{nameAndAttributeSkuMaskSimpleProduct.quantity}}" stepKey="fillSimpleProductQuantity"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForSimpleProductToSave"/> + <!-- Verify customer see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> + + <!-- Search created simple product(from above step) in the grid page to verify sku masked as name and country of manufacture --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchCreatedSimpleProduct"/> + <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{nameAndAttributeSkuMaskSimpleProduct.name}}-{{nameAndAttributeSkuMaskSimpleProduct.country_of_manufacture_label}}" stepKey="fillSkuFilterFieldWithNameAndCountryOfManufactureInput" /> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <waitForPageLoad stepKey="waitForProductSearchAfterApplyingFilters"/> + <see selector="{{AdminProductGridSection.firstProductRow}}" userInput="{{nameAndAttributeSkuMaskSimpleProduct.name}}-{{nameAndAttributeSkuMaskSimpleProduct.country_of_manufacture_label}}" stepKey="seeSimpleProductSkuMaskedAsNameAndCountryOfManufacture"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductFillingRequiredFieldsOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductFillingRequiredFieldsOnlyTest.xml new file mode 100644 index 000000000000..c3fe666c84fd --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductFillingRequiredFieldsOnlyTest.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateVirtualProductFillingRequiredFieldsOnlyTest"> + <annotations> + <stories value="Create virtual product"/> + <title value="Create virtual product filling required fields only"/> + <description value="Test log in to Create virtual product and Create virtual product filling required fields only"/> + <testCaseId value="MC-6031"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> + <waitForPageLoad stepKey="waitForProductToggleToSelectProduct"/> + <click selector="{{AdminProductGridActionSection.addVirtualProduct}}" stepKey="clickVirtualProduct"/> + + <!-- Create virtual product with required fields only --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{virtualProductWithRequiredFields.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{virtualProductWithRequiredFields.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{virtualProductWithRequiredFields.price}}" stepKey="fillProductPrice"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForVirtualProductSaved" /> + + <!-- Verify we see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSuccessMessage"/> + + <!-- Verify we see created virtual product(from the above step) on the product grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> + <waitForPageLoad stepKey="waitForProductCatalogPage1"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickSelector"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFilter"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{virtualProductWithRequiredFields.name}}" stepKey="fillProductName1"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{virtualProductWithRequiredFields.sku}}" stepKey="fillVirtualProductSku"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickSearch2"/> + <waitForPageLoad stepKey="waitForProductSearch"/> + <seeInField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{virtualProductWithRequiredFields.name}}" stepKey="seeVirtualProductName"/> + <seeInField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{virtualProductWithRequiredFields.sku}}" stepKey="seeVirtualProductSku"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductOutOfStockWithTierPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductOutOfStockWithTierPriceTest.xml new file mode 100644 index 000000000000..26ad7a46a73d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductOutOfStockWithTierPriceTest.xml @@ -0,0 +1,97 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateVirtualProductOutOfStockWithTierPriceTest"> + <annotations> + <stories value="Create virtual product"/> + <title value="Create virtual product out of stock with tier price"/> + <description value="Test log in to Create virtual product and Create virtual product out of stock with tier price"/> + <testCaseId value="MC-6036"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="categoryEntity"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> + <waitForPageLoad stepKey="waitForProductToggleToSelectProduct"/> + <click selector="{{AdminProductGridActionSection.addVirtualProduct}}" stepKey="clickVirtualProduct"/> + + <!-- Create virtual product out of stock with tier price --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{virtualProductOutOfStock.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{virtualProductOutOfStock.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{virtualProductOutOfStock.price}}" stepKey="fillProductPrice"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> + <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="clickCustomerGroupPriceAddButton"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceOnDefault.website_0}}" stepKey="selectProductTierPriceWebsite"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceOnDefault.customer_group_0}}" stepKey="selectProductTierPriceCustGroup"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnDefault.qty_0}}" stepKey="fillProductTierPriceQuantityInput"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnDefault.price_0}}" stepKey="selectProductTierPriceFixedPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="clickCustomerGroupPriceAddButtonToAddAnotherRow"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('1')}}" userInput="{{tierPriceOnDefault.website_1}}" stepKey="clickProductTierPriceWebsite1"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('1')}}" userInput="{{tierPriceOnDefault.customer_group_1}}" stepKey="clickProductTierPriceCustGroup1"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('1')}}" userInput="{{tierPriceOnDefault.qty_1}}" stepKey="fillProductTierPriceQuantityInput1"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('1')}}" userInput="{{tierPriceOnDefault.price_1}}" stepKey="selectProductTierPriceFixedPrice1"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{virtualProductOutOfStock.quantity}}" stepKey="fillVirtualProductQuantity"/> + <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{virtualProductOutOfStock.status}}" stepKey="selectStockStatusOutOfStock"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{virtualProductOutOfStock.urlKey}}" stepKey="fillUrlKey"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + + <!-- Verify we see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSuccessMessage"/> + + <!-- Verify we see created virtual product out of stock with tier price on the storefront page --> + <amOnPage url="{{StorefrontProductPage.url(virtualProductOutOfStock.urlKey)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageToLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{virtualProductOutOfStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{virtualProductOutOfStock.sku}}" stepKey="seeVirtualProductSku"/> + + <!-- Verify customer see product tier price on product page --> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productTierPriceByForTextLabel('1', tierPriceOnDefault.qty_0)}}" stepKey="firstTierPriceText"/> + <assertEquals stepKey="assertTierPriceTextOnProductPage1"> + <expectedResult type="string">Buy {{tierPriceOnDefault.qty_0}} for ${{tierPriceOnDefault.price_0}} each and save 100%</expectedResult> + <actualResult type="variable">firstTierPriceText</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productTierPriceByForTextLabel('2', tierPriceOnDefault.qty_1)}}" stepKey="secondTierPriceText"/> + <assertEquals stepKey="assertTierPriceTextOnProductPage2"> + <expectedResult type="string">Buy {{tierPriceOnDefault.qty_1}} for ${{tierPriceOnDefault.price_1}} each and save 100%</expectedResult> + <actualResult type="variable">secondTierPriceText</actualResult> + </assertEquals> + + <!-- Verify customer see product out of stock status on product page --> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> + <assertEquals stepKey="assertStockAvailableOnProductPage"> + <expectedResult type="string">{{virtualProductOutOfStock.storefrontStatus}}</expectedResult> + <actualResult type="variable">productStockAvailableStatus</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">${{virtualProductOutOfStock.price}}</expectedResult> + <actualResult type="variable">productPriceAmount</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml new file mode 100644 index 000000000000..17769c79677f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml @@ -0,0 +1,160 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest"> + <annotations> + <stories value="Create virtual product"/> + <title value="Create virtual product with custom options suite and import options"/> + <description value="Test log in to Create virtual product and Create virtual product with custom options suite and import options"/> + <testCaseId value="MC-6034"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="categoryEntity"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> + <waitForPageLoad stepKey="waitForProductToggleToSelectProduct"/> + <click selector="{{AdminProductGridActionSection.addVirtualProduct}}" stepKey="clickVirtualProduct"/> + + <!-- Create virtual product with custom options suite and import options --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{virtualProductCustomImportOptions.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{virtualProductCustomImportOptions.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{virtualProductCustomImportOptions.price}}" stepKey="fillProductPrice"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{virtualProductCustomImportOptions.quantity}}" stepKey="fillProductQuantity"/> + <click selector="{{AdminProductFormSection.productStockStatus}}" stepKey="clickProductStockStatus"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{virtualProductCustomImportOptions.urlKey}}" stepKey="fillUrlKey"/> + <click selector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" stepKey="clickAdminProductCustomizableOption"/> + + <!-- Create virtual product with customizable options dataSet1 --> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButton"/> + <waitForPageLoad stepKey="waitForFirstOption"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('0')}}" userInput="{{virtualProductCustomizableOption1.title}}" stepKey="fillOptionTitleForFirstDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('1')}}" stepKey="selectOptionTypeDropDownFirstDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('1', virtualProductCustomizableOption1.type)}}" stepKey="selectOptionFieldFromDropDownForFirstDataSet"/> + <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('0')}}" stepKey="checkRequiredCheckBoxForFirstDataSet"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionPrice('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_price}}" stepKey="fillOptionPriceForFirstDataSet"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.optionPriceType('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_price_type}}" stepKey="selectOptionPriceTypeForFirstDataSet"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionSku('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_sku}}" stepKey="fillOptionSkuForFirstDataSet"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.maxCharactersInput('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_max_characters}}" stepKey="fillOptionMaxCharactersForFirstDataSet"/> + + <!-- Create virtual product with customizable options dataSet2 --> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButtonForSecondDataSet"/> + <waitForPageLoad stepKey="waitForSecondDataSetToLoad"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('1')}}" userInput="{{virtualProductCustomizableOption2.title}}" stepKey="fillOptionTitleForSecondDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('2')}}" stepKey="selectOptionTypeDropDownSecondDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('2', virtualProductCustomizableOption2.type)}}" stepKey="selectOptionFieldFromDropDownForSecondDataSet"/> + <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('1')}}" stepKey="checkRequiredCheckBoxForSecondDataSet"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionPrice('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_price}}" stepKey="fillOptionPriceForSecondDataSet"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.optionPriceType('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_price_type}}" stepKey="selectOptionPriceTypeForSecondDataSet"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionSku('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_sku}}" stepKey="fillOptionSkuForSecondDataSet"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.maxCharactersInput('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_max_characters}}" stepKey="fillOptionMaxCharactersForSecondDataSet"/> + + <!-- Create virtual product with customizable options dataSet3 --> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButtonForThirdSetOfData"/> + <waitForPageLoad stepKey="waitForThirdSetOfDataToLoad"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('2')}}" userInput="{{virtualProductCustomizableOption3.title}}" stepKey="fillOptionTitleForThirdDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('3')}}" stepKey="selectOptionTypeDropDownForThirdDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('3', virtualProductCustomizableOption3.type)}}" stepKey="selectOptionFieldFromDropDownForThirdDataSet"/> + <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('2')}}" stepKey="checkRequiredCheckBoxForThirdDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.addValue}}" stepKey="clickAddOptionButtonForThirdDataSetToAddFirstRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_title}}" stepKey="fillOptionTitleForThirdDataSetFirstRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_price}}" stepKey="fillOptionPriceForThirdDataSetFirstRow"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_price_type}}" stepKey="selectOptionPriceTypeForThirdDataSetFirstRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_sku}}" stepKey="fillOptionSkuForThirdDataSetFirstRow"/> + <click selector="{{AdminProductCustomizableOptionsSection.addValue}}" stepKey="clickAddOptionButtonForThirdDataSetToAddSecondRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_title}}" stepKey="fillOptionTitleForThirdDataSetSecondRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_price}}" stepKey="fillOptionPriceForThirdDataSetSecondRow"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_price_type}}" stepKey="selectOptionPriceTypeForThirdDataSetSecondRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_sku}}" stepKey="fillOptionSkuForThirdDataSetSecondRow"/> + + <!-- Create virtual product with customizable options dataSet4 --> + <scrollToTopOfPage stepKey="scrollToAddOptionButton"/> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButtonForFourthDataSet"/> + <waitForPageLoad stepKey="waitForFourthDataSetToLoad"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('3')}}" userInput="{{virtualProductCustomizableOption4.title}}" stepKey="fillOptionTitleForFourthDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('4')}}" stepKey="selectOptionTypeDropDownForFourthSetOfData"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('4', virtualProductCustomizableOption4.type)}}" stepKey="selectOptionFieldFromDropDownForFourthDataSet"/> + <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('3')}}" stepKey="checkRequiredCheckBoxForFourthDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.addValue}}" stepKey="clickAddOptionButtonForFourthDataSetToAddFirstRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_title}}" stepKey="fillOptionTitleForFourthDataSetFirstRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_price}}" stepKey="fillOptionPriceForFourthDataSetFirstRow"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_price_type}}" stepKey="selectOptionPriceTypeForFourthDataSetFirstRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_sku}}" stepKey="fillOptionSkuForFourthDataSetFirstRow"/> + <click selector="{{AdminProductCustomizableOptionsSection.addValue}}" stepKey="clickAddOptionButtonForFourthDataSetToAddSecondRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_title}}" stepKey="fillOptionTitleForFourthDataSetSecondRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_price}}" stepKey="fillOptionPriceForFourthDataSetSecondRow"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_price_type}}" stepKey="selectOptionPriceTypeForFourthDataSetSecondRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_sku}}" stepKey="fillOptionSkuForFourthDataSetSecondRow"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + + <!-- Verify we see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSuccessMessage"/> + + <!-- Verify customer see created virtual product with custom options suite and import options(from above step) on storefront page and is searchable by sku --> + <amOnPage url="{{StorefrontProductPage.url(virtualProductCustomImportOptions.urlKey)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{virtualProductCustomImportOptions.sku}}" stepKey="fillVirtualProductName"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <see selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{virtualProductCustomImportOptions.name}}" stepKey="seeVirtualProductName"/> + <click selector="{{StorefrontQuickSearchResultsSection.productLink}}" stepKey="openSearchedProduct"/> + + <!-- Verify we see created virtual product with custom options suite and import options on the storefront page --> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{virtualProductCustomImportOptions.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{virtualProductCustomImportOptions.sku}}" stepKey="seeVirtualProductSku"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> + <assertEquals stepKey="assertStockAvailableOnProductPage"> + <expectedResult type="string">{{virtualProductCustomImportOptions.storefrontStatus}}</expectedResult> + <actualResult type="variable">productStockAvailableStatus</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">${{virtualProductCustomImportOptions.price}}</expectedResult> + <actualResult type="variable">productPriceAmount</actualResult> + </assertEquals> + + <!--Verify we see customizable options are Required --> + <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomInput(virtualProductCustomizableOption1.title)}}" stepKey="verifyFirstCustomOptionIsRequired" /> + <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomInput(virtualProductCustomizableOption2.title)}}" stepKey="verifySecondCustomOptionIsRequired" /> + <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomSelect(virtualProductCustomizableOption3.title)}}" stepKey="verifyThirdCustomOptionIsRequired" /> + <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomSelect(virtualProductCustomizableOption4.title)}}" stepKey="verifyFourthCustomOptionIsRequired" /> + + <!--Verify we see customizable option titles and prices --> + <grabMultiple selector="{{StorefrontProductInfoMainSection.allCustomOptionLabels}}" stepKey="allCustomOptionLabels" /> + <assertEquals stepKey="verifyLabels"> + <actualResult type="variable">allCustomOptionLabels</actualResult> + <expectedResult type="array">[{{virtualProductCustomizableOption1.title}} + ${{virtualProductCustomizableOption1.option_0_price}}, {{virtualProductCustomizableOption2.title}} + ${{virtualProductCustomizableOption2.option_0_price}}, {{virtualProductCustomizableOption3.title}}, {{virtualProductCustomizableOption4.title}}]</expectedResult> + </assertEquals> + <grabAttributeFrom userInput="for" selector="{{StorefrontProductInfoMainSection.customOptionLabel(virtualProductCustomizableOption4.title)}}" stepKey="fourthOptionId" /> + <grabMultiple selector="{{StorefrontProductInfoMainSection.customSelectOptions({$fourthOptionId})}}" stepKey="grabFourthOptions" /> + <assertEquals stepKey="assertFourthSelectOptions"> + <actualResult type="variable">grabFourthOptions</actualResult> + <expectedResult type="array">['-- Please Select --', {{virtualProductCustomizableOption4.option_0_title}} +$900.90, {{virtualProductCustomizableOption4.option_1_title}} +$20.02]</expectedResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceForGeneralGroupTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceForGeneralGroupTest.xml new file mode 100644 index 000000000000..78247f494359 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceForGeneralGroupTest.xml @@ -0,0 +1,132 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateVirtualProductWithTierPriceForGeneralGroupTest"> + <annotations> + <stories value="Create virtual product"/> + <title value="Create virtual product with tier price for General group"/> + <description value="Test log in to Create virtual product and Create virtual product with tier price for General group"/> + <testCaseId value="MC-6033"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> + <createData entity="Simple_US_CA_Customer" stepKey="customer" /> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="categoryEntity"/> + <deleteData stepKey="deleteCustomer" createDataKey="customer"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> + <waitForPageLoad stepKey="waitForProductToggleToSelectProduct"/> + <click selector="{{AdminProductGridActionSection.addVirtualProduct}}" stepKey="clickVirtualProduct"/> + + <!-- Create virtual product with tier price for general group --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{virtualProductGeneralGroup.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{virtualProductGeneralGroup.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{virtualProductGeneralGroup.price}}" stepKey="fillProductPrice"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> + <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="clickCustomerGroupPriceAddButton"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceOnGeneralGroup.website}}" stepKey="selectProductTierPriceWebsite"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceOnGeneralGroup.customer_group}}" stepKey="selectProductTierPriceGroup"/> + <scrollTo selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" x="50" y="0" stepKey="scrollToProductTierPriceQuantityInputTextBox"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnGeneralGroup.qty}}" stepKey="fillProductTierPriceQuantityInput"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnGeneralGroup.price}}" stepKey="fillProductTierPriceFixedPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> + <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{virtualProductGeneralGroup.productTaxClass}}" stepKey="selectProductTaxClass"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{virtualProductGeneralGroup.quantity}}" stepKey="fillProductQuantity"/> + <click selector="{{AdminProductFormSection.productStockStatus}}" stepKey="clickProductStockStatus"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{virtualProductGeneralGroup.visibility}}" stepKey="selectVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSectionHeader"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{virtualProductGeneralGroup.urlKey}}" stepKey="fillUrlKeyInput"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + + <!-- Verify we see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSuccessMessage"/> + + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> + <waitForPageLoad stepKey="waitForProductCatalogPage1"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="checkRetailCustomerTaxClass" /> + <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="{{virtualProductGeneralGroup.name}}" stepKey="fillVirtualProductName"/> + <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> + <waitForPageLoad stepKey="waitForProductSearch"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + + <!-- Verify we see created virtual product with tier price for general group(from the above step) in the product form page --> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{virtualProductGeneralGroup.name}}" stepKey="seeProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{virtualProductGeneralGroup.sku}}" stepKey="seeProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{virtualProductGeneralGroup.price}}" stepKey="seeProductPrice"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink1"/> + <seeOptionIsSelected selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceOnGeneralGroup.website}}" stepKey="seeProductTierPriceWebsite"/> + <seeOptionIsSelected selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceOnGeneralGroup.customer_group}}" stepKey="seeProductTierPriceGroup"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnGeneralGroup.qty}}" stepKey="seeProductTierPriceQuantityInput"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnGeneralGroup.price}}" stepKey="seeProductTierPriceFixedPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.advancedPricingCloseButton}}" stepKey="clickAdvancedPricingCloseButton"/> + <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{virtualProductGeneralGroup.productTaxClass}}" stepKey="seeProductTaxClass"/> + <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{virtualProductGeneralGroup.quantity}}" stepKey="seeProductQuantity"/> + <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{virtualProductGeneralGroup.status}}" stepKey="seeProductStockStatus"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> + <grabMultiple selector="{{AdminProductFormSection.selectMultipleCategories}}" stepKey="selectedCategories" /> + <assertEquals stepKey="assertSelectedCategories"> + <actualResult type="variable">selectedCategories</actualResult> + <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> + </assertEquals> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{virtualProductGeneralGroup.visibility}}" stepKey="seeVisibility"/> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> + <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{virtualProductGeneralGroup.urlKey}}" stepKey="seeUrlKey"/> + + <!--Verify customer see created virtual product on category page --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> + <see selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{virtualProductGeneralGroup.name}}" stepKey="seeVirtualProductNameOnCategoryPage"/> + + <!-- Verify customer see created virtual product with tier price for general group(from above step) in storefront page with customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> + <argument name="Customer" value="$$customer$$" /> + </actionGroup> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefront"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{virtualProductGeneralGroup.name}}" stepKey="fillVirtualProductNameInSearchTextBox"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <see selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{virtualProductGeneralGroup.name}}" stepKey="seeVirtualProductName"/> + <grabTextFrom selector="{{StorefrontQuickSearchResultsSection.asLowAsLabel}}" stepKey="tierPriceTextOnStorefrontPage"/> + + <!-- Verify customer see created virtual product with tier price --> + <assertEquals stepKey="assertTierPriceTextOnCategoryPage"> + <expectedResult type="string">As low as ${{tierPriceOnGeneralGroup.price}}</expectedResult> + <actualResult type="variable">tierPriceTextOnStorefrontPage</actualResult> + </assertEquals> + <click selector="{{StorefrontQuickSearchResultsSection.productLink}}" stepKey="openSearchedProduct"/> + <waitForPageLoad stepKey="waitForProductPageToBeLoaded"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.tierPriceText}}" stepKey="tierPriceText"/> + <assertEquals stepKey="assertTierPriceTextOnProductPage"> + <expectedResult type="string">Buy {{tierPriceOnGeneralGroup.qty}} for ${{tierPriceOnGeneralGroup.price}} each and save 20%</expectedResult> + <actualResult type="variable">tierPriceText</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml new file mode 100644 index 000000000000..6ef2569945fa --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml @@ -0,0 +1,123 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateVirtualProductWithTierPriceTest"> + <annotations> + <stories value="Create virtual product"/> + <title value="Create virtual product with tier price"/> + <description value="Test log in to Create virtual product and Create virtual product with tier price"/> + <testCaseId value="MC-6032"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="categoryEntity"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> + <waitForPageLoad stepKey="waitForProductToggleToSelectProduct"/> + <click selector="{{AdminProductGridActionSection.addVirtualProduct}}" stepKey="clickVirtualProduct"/> + + <!-- Create virtual product with tier price --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{virtualProductBigQty.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{virtualProductBigQty.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{virtualProductBigQty.price}}" stepKey="fillProductPrice"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> + <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="clickCustomerGroupPriceAddButton"/> + <scrollTo selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" x="50" y="0" stepKey="scrollToProductTierPriceQuantityInputTextBox"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnVirtualProduct.qty}}" stepKey="fillProductTierPriceQuantityInput"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnVirtualProduct.price}}" stepKey="selectProductTierPriceFixedPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> + <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{virtualProductBigQty.productTaxClass}}" stepKey="selectProductTaxClass"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{virtualProductBigQty.quantity}}" stepKey="fillProductQuantity"/> + <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{virtualProductBigQty.status}}" stepKey="selectStockStatusInStock"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{virtualProductBigQty.visibility}}" stepKey="selectVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{virtualProductBigQty.urlKey}}" stepKey="fillUrlKey"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + + <!-- Verify we see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSuccessMessage"/> + + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> + <waitForPageLoad stepKey="waitForProductCatalogPage1"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="checkRetailCustomerTaxClass" /> + <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="{{virtualProductBigQty.name}}" stepKey="fillProductName1"/> + <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> + <waitForPageLoad stepKey="waitForProductSearch"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened" /> + + <!-- Verify we see created virtual product with tier price(from the above step) in the product form page --> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{virtualProductBigQty.name}}" stepKey="seeProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{virtualProductBigQty.sku}}" stepKey="seeProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{virtualProductBigQty.price}}" stepKey="seeProductPrice"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink1"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnVirtualProduct.qty}}" stepKey="seeProductTierPriceQuantityInput"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnVirtualProduct.price}}" stepKey="seeProductTierPriceFixedPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.advancedPricingCloseButton}}" stepKey="clickAdvancedPricingCloseButton"/> + <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{virtualProductBigQty.productTaxClass}}" stepKey="seeProductTaxClass"/> + <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{virtualProductBigQty.quantity}}" stepKey="seeProductQuantity"/> + <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{virtualProductBigQty.status}}" stepKey="seeProductStockStatus"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> + <grabMultiple selector="{{AdminProductFormSection.selectMultipleCategories}}" stepKey="selectedCategories" /> + <assertEquals stepKey="assertSelectedCategories"> + <actualResult type="variable">selectedCategories</actualResult> + <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> + </assertEquals> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{virtualProductBigQty.visibility}}" stepKey="seeVisibility"/> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> + <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{virtualProductBigQty.urlKey}}" stepKey="seeUrlKey"/> + + <!--Verify customer see created virtual product on category page --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> + <see selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{virtualProductBigQty.name}}" stepKey="seeVirtualProductNameOnCategoryPage"/> + + <!-- Verify customer see created virtual product with tier price(from above step) on storefront page and is searchable by sku --> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefront"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{virtualProductBigQty.sku}}" stepKey="fillVirtualProductName"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <see selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{virtualProductBigQty.name}}" stepKey="seeVirtualProductName"/> + <grabTextFrom selector="{{StorefrontQuickSearchResultsSection.asLowAsLabel}}" stepKey="tierPriceTextOnStorefrontPage"/> + <assertEquals stepKey="assertTierPriceTextOnCategoryPage"> + <expectedResult type="string">As low as ${{tierPriceOnVirtualProduct.price}}</expectedResult> + <actualResult type="variable">tierPriceTextOnStorefrontPage</actualResult> + </assertEquals> + <click selector="{{StorefrontQuickSearchResultsSection.productLink}}" stepKey="openSearchedProduct"/> + <waitForPageLoad stepKey="waitForProductPageToBeLoaded" /> + + <!-- Verify customer see product tier price on product page --> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.tierPriceText}}" stepKey="tierPriceText"/> + <assertEquals stepKey="assertTierPriceTextOnProductPage"> + <expectedResult type="string">Buy {{tierPriceOnVirtualProduct.qty}} for ${{tierPriceOnVirtualProduct.price}} each and save 10%</expectedResult> + <actualResult type="variable">tierPriceText</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithoutManageStockTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithoutManageStockTest.xml new file mode 100644 index 000000000000..cb41b0292d33 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithoutManageStockTest.xml @@ -0,0 +1,88 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateVirtualProductWithoutManageStockTest"> + <annotations> + <stories value="Create virtual product"/> + <title value="Create virtual product without manage stock"/> + <description value="Test log in to Create virtual product and Create virtual product without manage stock"/> + <testCaseId value="MC-6035"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="categoryEntity"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> + <waitForPageLoad stepKey="waitForProductToggleToSelectProduct"/> + <click selector="{{AdminProductGridActionSection.addVirtualProduct}}" stepKey="clickVirtualProduct"/> + + <!-- Create virtual product without manage stock --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{virtualProductWithoutManageStock.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{virtualProductWithoutManageStock.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{virtualProductWithoutManageStock.price}}" stepKey="fillProductPrice"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" userInput="{{virtualProductWithoutManageStock.special_price}}" stepKey="fillSpecialPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{virtualProductWithoutManageStock.quantity}}" stepKey="fillProductQuantity"/> + <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickAdvancedInventoryLink"/> + <click selector="{{AdminProductFormAdvancedInventorySection.manageStock}}" stepKey="clickManageStock"/> + <checkOption selector="{{AdminProductFormAdvancedInventorySection.useConfigSettings}}" stepKey="CheckUseConfigSettingsCheckBox"/> + <click selector="{{AdminProductFormAdvancedInventorySection.doneButton}}" stepKey="clickDoneButtonOnAdvancedInventorySection"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{virtualProductWithoutManageStock.urlKey}}" stepKey="fillUrlKey"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + + <!-- Verify we see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSuccessMessage"/> + + <!-- Verify customer see created virtual product without manage stock on the storefront page --> + <amOnPage url="{{StorefrontProductPage.url(virtualProductWithoutManageStock.urlKey)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForStoreFrontPageToLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{virtualProductWithoutManageStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{virtualProductWithoutManageStock.sku}}" stepKey="seeVirtualProductSku"/> + + <!-- Verify customer see product special price on the storefront page --> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.specialPriceAmount}}" stepKey="specialPriceAmount"/> + <assertEquals stepKey="assertSpecialPriceTextOnProductPage"> + <expectedResult type="string">${{virtualProductWithoutManageStock.special_price}}</expectedResult> + <actualResult type="variable">specialPriceAmount</actualResult> + </assertEquals> + + <!-- Verify customer see product old price on the storefront page --> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.oldPriceAmount}}" stepKey="oldPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">${{virtualProductWithoutManageStock.price}}</expectedResult> + <actualResult type="variable">oldPriceAmount</actualResult> + </assertEquals> + + <!-- Verify customer see product in stock status on the storefront page --> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> + <assertEquals stepKey="assertStockAvailableOnProductPage"> + <expectedResult type="string">{{virtualProductWithoutManageStock.storefrontStatus}}</expectedResult> + <actualResult type="variable">productStockAvailableStatus</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteAttributeSetTest.xml new file mode 100644 index 000000000000..4d28ccbd44d2 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteAttributeSetTest.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminDeleteAttributeSetTest"> + <annotations> + <features value="Catalog"/> + <title value="Delete Attribute Set"/> + <description value="Admin should be able to delete an attribute set"/> + <testCaseId value="MC-4413"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="CatalogAttributeSet" stepKey="createAttributeSet"/> + <createData entity="SimpleProductWithCustomAttributeSet" stepKey="SimpleProductWithCustomAttributeSet"> + <requiredEntity createDataKey="createCategory"/> + <requiredEntity createDataKey="createAttributeSet"/> + </createData> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSetsPage"/> + <fillField selector="{{AdminProductAttributeSetGridSection.filter}}" userInput="$$createAttributeSet.attribute_set_name$$" stepKey="filterByAttributeName"/> + <!-- Filter the grid to find created below attribute set --> + <click selector="{{AdminProductAttributeSetGridSection.searchBtn}}" stepKey="clickSearch"/> + <click selector="{{AdminProductAttributeSetGridSection.nthRow('1')}}" stepKey="clickFirstRow"/> + <!-- Delete attribute set and confirm the modal --> + <click selector="{{AdminProductAttributeSetSection.deleteBtn}}" stepKey="clickDelete"/> + <click selector="{{AdminProductAttributeSetSection.modalOk}}" stepKey="confirmDelete"/> + <waitForPageLoad stepKey="waitForDeleteToFinish"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="The attribute set has been removed." stepKey="deleteMessage"/> + <!-- Assert the attribute set is not in the grid --> + <fillField selector="{{AdminProductAttributeSetGridSection.filter}}" userInput="$$createAttributeSet.attribute_set_name$$" stepKey="filterByAttributeName2"/> + <click selector="{{AdminProductAttributeSetGridSection.searchBtn}}" stepKey="clickSearch2"/> + <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage"/> + <!-- Search for the product by sku and name on the product page --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToAdminProductIndex"/> + <waitForPageLoad stepKey="waitForAdminProductIndex"/> + <actionGroup ref="filterProductGridBySkuAndName" stepKey="filerProductsBySkuAndName"> + <argument name="product" value="SimpleProductWithCustomAttributeSet"/> + </actionGroup> + <!-- Should not see the product --> + <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage2"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml new file mode 100644 index 000000000000..0df9dd0b5754 --- /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 000000000000..3841c061c262 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteDropdownProductAttributeFromAttributeSetTest.xml @@ -0,0 +1,72 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminDeleteDropdownProductAttributeFromAttributeSetTest"> + <annotations> + <stories value="Delete product attributes"/> + <title value="Delete Product Attribute, Dropdown Type, from Attribute Set"/> + <description value="Login as admin and delete dropdown type product attribute from attribute set"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10885"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <!-- Create Dropdown Product Attribute --> + <createData entity="productDropDownAttribute" stepKey="attribute"/> + <!-- Create Attribute set --> + <createData entity="CatalogAttributeSet" stepKey="createAttributeSet"/> + </before> + <after> + <!--Delete Created Data --> + <deleteData createDataKey="createAttributeSet" stepKey="deleteAttributeSet"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Open Product Attribute Set Page --> + <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSets"/> + <waitForPageLoad stepKey="waitForProductAttributeSetPageToLoad"/> + <click selector="{{AdminProductAttributeSetGridSection.resetFilter}}" stepKey="clickOnResetFilter"/> + <!-- Filter created Product Attribute Set --> + <fillField selector="{{AdminProductAttributeSetGridSection.filter}}" userInput="$$createAttributeSet.attribute_set_name$$" stepKey="fillAttributeSetName"/> + <click selector="{{AdminProductAttributeSetGridSection.searchBtn}}" stepKey="clickOnSearchButton"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{AdminProductAttributeSetGridSection.AttributeSetName($$createAttributeSet.attribute_set_name$$)}}" stepKey="clickOnAttributeSet"/> + <waitForPageLoad stepKey="waitForAttributeSetEditPageToLoad"/> + <!--Assign Attribute to the Group and save the attribute set --> + <actionGroup ref="AssignAttributeToGroup" stepKey="assignAttribute"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="$$attribute.attribute_code$$"/> + </actionGroup> + <click selector="{{AdminProductAttributeSetActionSection.save}}" stepKey="clickOnSaveButton"/> + <waitForPageLoad stepKey="waitForPageToSave"/> + <see userInput="You saved the attribute set" selector="{{AdminMessagesSection.success}}" stepKey="successMessage"/> + <!--Delete product attribute from product attribute grid --> + <actionGroup ref="deleteProductAttributeByAttributeCode" stepKey="deleteProductAttribute"> + <argument name="ProductAttributeCode" value="$$attribute.attribute_code$$"/> + </actionGroup> + <!--Confirm Attribute is not present in Product Attribute Grid --> + <actionGroup ref="filterProductAttributeByAttributeCode" stepKey="filterAttribute"> + <argument name="ProductAttributeCode" value="$$attribute.attribute_code$$"/> + </actionGroup> + <see selector="{{AdminProductAttributeGridSection.FirstRow}}" userInput="We couldn't find any records." stepKey="seeEmptyRow"/> + <!-- Verify Attribute is not present in Product Attribute Set Page --> + <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSets1"/> + <waitForPageLoad stepKey="waitForProductAttributeSetPageToLoad1"/> + <click selector="{{AdminProductAttributeSetGridSection.resetFilter}}" stepKey="clickOnResetFilter1"/> + <fillField selector="{{AdminProductAttributeSetGridSection.filter}}" userInput="$$createAttributeSet.attribute_set_name$$" stepKey="fillAttributeSetName1"/> + <click selector="{{AdminProductAttributeSetGridSection.searchBtn}}" stepKey="clickOnSearchButton1"/> + <waitForPageLoad stepKey="waitForPageToLoad1"/> + <click selector="{{AdminProductAttributeSetGridSection.AttributeSetName($$createAttributeSet.attribute_set_name$$)}}" stepKey="clickOnAttributeSet1"/> + <waitForPageLoad stepKey="waitForAttributeSetEditPageToLoad1"/> + <dontSee userInput="$$attribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.groupTree}}" stepKey="dontSeeAttributeInAttributeGroupTree"/> + <dontSee userInput="$$attribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.unassignedAttributesTree}}" stepKey="dontSeeAttributeInUnassignedAttributeTree"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductAttributeTest.xml new file mode 100644 index 000000000000..54b83e034fb1 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductAttributeTest.xml @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="DeleteProductAttributeTest"> + <annotations> + <features value="Catalog"/> + <title value="Delete Product Attribute"/> + <description value="Admin should able to delete a product attribute"/> + <testCaseId value="MC-10887"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="productAttributeWysiwyg" stepKey="createProductAttribute"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="deleteProductAttributeByAttributeCode" stepKey="deleteProductAttribute"> + <argument name="ProductAttributeCode" value="$$createProductAttribute.attribute_code$$"/> + </actionGroup> + <!-- Assert the product attribute is not in the grid by Attribute code --> + <actionGroup ref="filterProductAttributeByAttributeCode" stepKey="filterByAttributeCode"> + <argument name="ProductAttributeCode" value="$$createProductAttribute.attribute_code$$"/> + </actionGroup> + <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage"/> + <!--Assert the product attribute is not in the grid by Default Label --> + <actionGroup ref="filterProductAttributeByDefaultLabel" stepKey="filterByDefaultLabel"> + <argument name="productAttributeLabel" value="$$createProductAttribute.default_frontend_label$$"/> + </actionGroup> + <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage2"/> + <!--Go to the Catalog > Products page and create Simple Product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductList"/> + <waitForPageLoad stepKey="waitForProductList"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="toggleAddProductBtn"/> + <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="chooseAddSimpleProduct"/> + <waitForPageLoad stepKey="waitForProductAdded"/> + <!-- Press Add Attribute button --> + <click selector="{{AdminProductFormSection.addAttributeBtn}}" stepKey="clickAddAttributeBtn"/> + <waitForPageLoad stepKey="waitForAttributeAdded"/> + <!-- Filter By Attribute Label on Add Attribute Page --> + <click selector="{{AdminProductFiltersSection.filter}}" stepKey="clickOnFilter"/> + <actionGroup ref="filterProductAttributeByAttributeLabel" stepKey="filterByAttributeLabel"> + <argument name="productAttributeLabel" value="$$createProductAttribute.default_frontend_label$$"/> + </actionGroup> + <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage3"/> + <!-- Filter By Attribute Code on Export > Products page --> + <amOnPage url="{{AdminExportIndexPage.url}}" stepKey="navigateToSystemExport"/> + <selectOption selector="{{AdminExportMainSection.entityType}}" userInput="Products" stepKey="selectProductsOption"/> + <waitForElementVisible selector="{{AdminExportMainSection.entityAttributes}}" stepKey="waitForElementVisible"/> + <click selector="{{AdminExportAttributeSection.resetFilter}}" stepKey="resetFilter"/> + <fillField selector="{{AdminExportAttributeSection.filterByAttributeCode}}" userInput="$$createProductAttribute.attribute_code$$" stepKey="setAttributeCode"/> + <waitForPageLoad stepKey="waitForUserInput"/> + <click selector="{{AdminExportAttributeSection.search}}" stepKey="searchForAttribute"/> + <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage4"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml new file mode 100644 index 000000000000..7f6a1333b721 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminDeleteProductWithCustomOptionTest"> + <annotations> + <features value="Catalog"/> + <stories value="Delete products"/> + <title value="Delete Product with Custom Option"/> + <description value="Admin should be able to delete a product with custom option"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-11015"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <updateData createDataKey="createSimpleProduct" entity="productWithOptions2" stepKey="updateProductWithCustomOption"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteSimpleProductFilteredBySkuAndName"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="A total of 1 record(s) have been deleted." stepKey="deleteMessage"/> + <!--Verify product on product page --> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.name$$)}}" stepKey="amOnSimpleProductPage"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> + <!-- Search for the product by sku --> + <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createSimpleProduct.sku$$" stepKey="fillSearchBarByProductSku"/> + <waitForPageLoad stepKey="waitForSearchButton"/> + <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForSearchResults"/> + <!-- Should not see any search results --> + <dontSee userInput="$$createSimpleProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> + <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> + <!-- Go to the category page that we created in the before block --> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> + <!-- Should not see the product --> + <dontSee userInput="$$createSimpleProduct.name$$" selector="{{StorefrontCategoryMainSection.productsList}}" stepKey="dontSeeProductInCategory"/> + <see selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" userInput="We can't find products matching the selection." stepKey="seeEmptyProductMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryAssignedToStoreTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryAssignedToStoreTest.xml new file mode 100644 index 000000000000..e4b269dff96b --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryAssignedToStoreTest.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminDeleteRootCategoryAssignedToStoreTest"> + <annotations> + <stories value="Delete categories"/> + <title value="Cannot delete root category assigned to some store"/> + <description value="Login as admin and root category can not be deleted when category is assigned with any store."/> + <testCaseId value="MC-6050"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="NewRootCategory" stepKey="rootCategory" /> + </before> + <after> + <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCreatedStore"> + <argument name="storeGroupName" value="customStore.code"/> + </actionGroup> + <deleteData createDataKey="rootCategory" stepKey="deleteRootCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <waitForPageLoad stepKey="waitForSystemStorePage"/> + <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> + <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" stepKey="fillStoreName"/> + <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" stepKey="fillStoreCode"/> + <selectOption userInput="{{NewRootCategory.name}}" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectStoreStatus"/> + <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreButton"/> + <see userInput="You saved the store." stepKey="seeSaveMessage"/> + + <!--Verify Delete Root Category can not be deleted--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage1"/> + <waitForPageLoad stepKey="waitForCategoryIndexPageToBeLoaded1"/> + <scrollToTopOfPage stepKey="scrollToTopOfPage2"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(NewRootCategory.name))}}" stepKey="clickRootCategoryInTree"/> + + <!--Verify Delete button is not displayed--> + <dontSeeElement selector="{{AdminCategoryMainActionsSection.DeleteButton}}" stepKey="dontSeeDeleteButton"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml new file mode 100644 index 000000000000..e7ab14c77945 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminDeleteRootCategoryTest"> + <annotations> + <stories value="Delete categories"/> + <title value="Can delete a root category not assigned to any store"/> + <description value="Login as admin and delete a root category not assigned to any store"/> + <testCaseId value="MC-6048"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="NewRootCategory" stepKey="rootCategory" /> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Verify Created root Category--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForCategoryIndexPageToBeLoaded"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <seeElement selector="{{AdminCategoryBasicFieldSection.CategoryNameInput(NewRootCategory.name)}}" stepKey="seeRootCategory"/> + + <!--Delete Root Category--> + <deleteData createDataKey="rootCategory" stepKey="deleteRootCategory"/> + + <!--Verify Root Category is not listed in backend--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage1"/> + <waitForPageLoad stepKey="waitForCategoryIndexPageToBeLoaded1"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories1"/> + <dontSee selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{NewRootCategory.name}}" stepKey="dontSeeRootCategory"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml new file mode 100644 index 000000000000..6df571f403ac --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml @@ -0,0 +1,90 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminDeleteRootSubCategoryTest"> + <annotations> + <stories value="Delete categories"/> + <title value="Can delete a subcategory"/> + <description value="Login as admin and delete a root sub category"/> + <testCaseId value="MC-6049"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="NewRootCategory" stepKey="rootCategory" /> + <createData entity="SimpleRootSubCategory" stepKey="category"> + <requiredEntity createDataKey="rootCategory"/> + </createData> + </before> + <after> + <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCreatedStore"> + <argument name="storeGroupName" value="customStore.code"/> + </actionGroup> + <deleteData createDataKey="rootCategory" stepKey="deleteRootCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create a Store--> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <waitForPageLoad stepKey="waitForSystemStorePage"/> + <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> + <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" stepKey="fillStoreName"/> + <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" stepKey="fillStoreCode"/> + <selectOption userInput="{{NewRootCategory.name}}" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectStoreStatus"/> + <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreButton"/> + <see userInput="You saved the store." stepKey="seeSaveMessage"/> + + <!--Create a Store View--> + <click selector="{{AdminStoresMainActionsSection.createStoreViewButton}}" stepKey="selectCreateStoreView"/> + <click selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="clickDropDown"/> + <selectOption userInput="{{customStore.name}}" selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="selectStoreViewStatus"/> + <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreSection.storeNameTextField}}" stepKey="fillStoreViewName"/> + <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreSection.storeCodeTextField}}" stepKey="fillStoreViewCode"/> + <selectOption selector="{{AdminNewStoreSection.statusDropdown}}" userInput="Enabled" stepKey="enableStatus"/> + <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreViewButton"/> + <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForModal" /> + <see selector="{{AdminConfirmationModalSection.title}}" userInput="Warning message" stepKey="seeWarning" /> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="dismissModal" /> + <waitForElementNotVisible selector="{{AdminNewStoreViewActionsSection.loadingMask}}" stepKey="waitForElementVisible"/> + <see userInput="You saved the store view." stepKey="seeSaveMessage1"/> + + <!--Go To store front page--> + <amOnPage url="/{{NewRootCategory.name}}/{{SimpleRootSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> + <waitForPageLoad time="60" stepKey="waitForStoreFrontPageLoad"/> + + <!--Verify subcategory displayed in store front--> + <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="selectMainWebsite"/> + <click selector="{{StorefrontFooterSection.storeLink(customStore.name)}}" stepKey="selectMainWebsite1"/> + <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="seeSubCategoryInStoreFront"/> + + <!--Delete SubCategory--> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + + <!--Verify Sub Category is absent in backend --> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForCategoryIndexPageToBeLoaded"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories2"/> + <dontSee selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleRootSubCategory.name)}}" stepKey="dontSeeCategoryInTree"/> + + <!--Verify Sub Category is not present in Store Front--> + <amOnPage url="/{{NewRootCategory.name}}/{{SimpleSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage1"/> + <waitForPageLoad time="60" stepKey="waitForStoreFrontPageLoad2"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="dontSeeSubCategoryInStoreFront"/> + + <!--Verify in Category is not in Url Rewrite grid--> + <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="openUrlRewriteIndexPage"/> + <waitForPageLoad stepKey="waitForUrlRewritePageTopLoad"/> + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="{{SimpleRootSubCategory.url_key}}" stepKey="fillRequestPath"/> + <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton"/> + <see selector="{{AdminUrlRewriteIndexSection.emptyRecordMessage}}" userInput="We couldn't find any records." stepKey="seeEmptyRow"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml new file mode 100644 index 000000000000..7c460a3dfc51 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminDeleteSimpleProductTest"> + <annotations> + <features value="Catalog"/> + <stories value="Delete products"/> + <title value="Delete Simple Product"/> + <description value="Admin should be able to delete a simple product"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-11013"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteSimpleProductFilteredBySkuAndName"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="A total of 1 record(s) have been deleted." stepKey="deleteMessage"/> + <!--Verify product on Product Page --> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.name$$)}}" stepKey="amOnSimpleProductPage"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> + <!-- Search for the product by sku --> + <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createSimpleProduct.sku$$" stepKey="fillSearchBarByProductSku"/> + <waitForPageLoad stepKey="waitForSearchButton"/> + <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForSearchResults"/> + <!-- Should not see any search results --> + <dontSee userInput="$$createSimpleProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> + <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> + <!-- Go to the category page that we created in the before block --> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> + <!-- Should not see the product --> + <dontSee userInput="$$createSimpleProduct.name$$" selector="{{StorefrontCategoryMainSection.productsList}}" stepKey="dontSeeProductInCategory"/> + <see selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" userInput="We can't find products matching the selection." stepKey="seeEmptyProductMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSystemProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSystemProductAttributeTest.xml new file mode 100644 index 000000000000..6de1a5cd359c --- /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 000000000000..c3cafb17c5ea --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteTextFieldProductAttributeFromAttributeSetTest.xml @@ -0,0 +1,88 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminDeleteTextFieldProductAttributeFromAttributeSetTest"> + <annotations> + <stories value="Delete product attributes"/> + <title value="Delete Product Attribute, Text Field, from Attribute Set"/> + <description value="Login as admin and delete Text Field type product attribute from attribute set"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10886"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <!-- Create Product Attribute and assign to Default Product Attribute Set --> + <createData entity="newProductAttribute" stepKey="attribute"/> + <createData entity="AddToDefaultSet" stepKey="addToDefaultAttributeSet"> + <requiredEntity createDataKey="attribute"/> + </createData> + <!-- Create Simple Product --> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> + </before> + <after> + <!--Delete cteated Data --> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimplaeProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Open Product Attribute Set Page --> + <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSets"/> + <waitForPageLoad stepKey="waitForProductAttributeSetPageToLoad"/> + <click selector="{{AdminProductAttributeSetGridSection.resetFilter}}" stepKey="clickOnResetFilter"/> + <!--Select Default Product Attribute Set --> + <fillField selector="{{AdminProductAttributeSetGridSection.filter}}" userInput="Default" stepKey="fillAttributeSetName"/> + <click selector="{{AdminProductAttributeSetGridSection.searchBtn}}" stepKey="clickOnSearchButton"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{AdminProductAttributeSetGridSection.nthRow('1')}}" stepKey="clickFirstRow"/> + <waitForPageLoad stepKey="waitForAttributeSetEditPageToLoad"/> + <see selector="{{AdminProductAttributeSetEditSection.groupTree}}" userInput="$$attribute.attribute_code$$" stepKey="seeAttributeInAttributeGroupTree"/> + <!--Open Product Index Page and filter the product--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + <actionGroup ref="filterProductGridBySku" stepKey="filterProduct"> + <argument name="product" value="SimpleProduct2"/> + </actionGroup> + <!--Verify Created Product Attribute displayed in Product page --> + <click stepKey="openSelectedProduct" selector="{{AdminProductGridSection.productRowBySku($$createSimpleProduct.sku$$)}}"/> + <waitForPageLoad stepKey="waitForProductToLoad"/> + <seeElement selector="{{AdminProductFormSection.newAddedAttribute($$attribute.attribute_code$$)}}" stepKey="seeProductAttributeIsAdded"/> + <!--Delete product attribute from product attribute grid --> + <actionGroup ref="deleteProductAttributeByAttributeCode" stepKey="deleteProductAttribute"> + <argument name="ProductAttributeCode" value="$$attribute.attribute_code$$"/> + </actionGroup> + <!-- Confirm attribute is not present in product attribute grid --> + <actionGroup ref="filterProductAttributeByAttributeCode" stepKey="filterAttribute"> + <argument name="ProductAttributeCode" value="$$attribute.attribute_code$$"/> + </actionGroup> + <see stepKey="seeEmptyRow" selector="{{AdminProductAttributeGridSection.FirstRow}}" userInput="We couldn't find any records."/> + <!-- Verify Attribute is not present in Product Attribute Set Page --> + <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSets1"/> + <waitForPageLoad stepKey="waitForProductAttributeSetPageToLoad1"/> + <click selector="{{AdminProductAttributeSetGridSection.resetFilter}}" stepKey="clickOnResetFilter1"/> + <fillField selector="{{AdminProductAttributeSetGridSection.filter}}" userInput="Default" stepKey="fillAttributeSetName1"/> + <click selector="{{AdminProductAttributeSetGridSection.searchBtn}}" stepKey="clickOnSearchButton1"/> + <waitForPageLoad stepKey="waitForPageToLoad1"/> + <click selector="{{AdminProductAttributeSetGridSection.nthRow('1')}}" stepKey="clickFirstRow1"/> + <waitForPageLoad stepKey="waitForAttributeSetEditPageToLoad1"/> + <dontSee userInput="$$attribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.groupTree}}" stepKey="dontSeeAttributeInAttributeGroupTree"/> + <dontSee userInput="$$attribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.unassignedAttributesTree}}" stepKey="dontSeeAttributeInUnassignedAttributeTree"/> + <!--Verify Product Attribute is not present in Product Index Page --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="openProductIndexPage"/> + <waitForPageLoad stepKey="waitForProductIndexPageToLoad1"/> + <actionGroup ref="filterProductGridBySku" stepKey="filterProduct1"> + <argument name="product" value="SimpleProduct2"/> + </actionGroup> + <!--Verify Product Attribute is not present in Product page --> + <click selector="{{AdminProductGridSection.productRowBySku($$createSimpleProduct.sku$$)}}" stepKey="openSelectedProduct1"/> + <waitForPageLoad stepKey="waitForProductPageToLoad"/> + <dontSeeElement selector="{{AdminProductFormSection.newAddedAttribute($$attribute.attribute_code$$)}}" stepKey="dontSeeProductAttribute"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml new file mode 100644 index 000000000000..413d53d1c374 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminDeleteVirtualProductTest"> + <annotations> + <features value="Catalog"/> + <stories value="Delete products"/> + <title value="Delete Virtual Product"/> + <description value="Admin should be able to delete a virtual product"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-11014"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="defaultVirtualProduct" stepKey="createVirtualProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteVirtualProductFilteredBySkuAndName"> + <argument name="product" value="$$createVirtualProduct$$"/> + </actionGroup> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="A total of 1 record(s) have been deleted." stepKey="deleteMessage"/> + <!--Verify product on product page --> + <amOnPage url="{{StorefrontProductPage.url($$createVirtualProduct.name$$)}}" stepKey="amOnVirtualProductPage"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> + <!-- Search for the product by sku --> + <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createVirtualProduct.sku$$" stepKey="fillSearchBarByProductSku"/> + <waitForPageLoad stepKey="waitForSearchButton"/> + <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForSearchResults"/> + <!-- Should not see any search results --> + <dontSee userInput="$$createVirtualProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> + <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> + <!-- Go to the category page that we created in the before block --> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> + <!-- Should not see the product --> + <dontSee userInput="$$createVirtualProduct.name$$" selector="{{StorefrontCategoryMainSection.productsList}}" stepKey="dontSeeProductInCategory"/> + <see selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" userInput="We can't find products matching the selection." stepKey="seeEmptyProductMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml index 7603400ba8dc..e914b8c96d03 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml @@ -26,10 +26,9 @@ <requiredEntity createDataKey="myProductAttributeCreation"/> </createData> </before> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid1"/> - <waitForPageLoad stepKey="waitForPageLoad1"/> - <click selector="{{AdminProductAttributeGridSection.AttributeCode($$myProductAttributeCreation.attribute_code$$)}}" stepKey="navigateToAttributeEditPage1" /> - <waitForPageLoad stepKey="waitForPageLoad2" /> + <actionGroup ref="navigateToCreatedProductAttribute" stepKey="navigateToAttribute"> + <argument name="ProductAttribute" value="productAttributeWysiwyg"/> + </actionGroup> <seeOptionIsSelected selector="{{AttributePropertiesSection.InputType}}" userInput="Text Editor" stepKey="seeTextEditorSelected" /> <see selector="{{AttributePropertiesSection.InputType}}" userInput="Text Area" stepKey="seeTextArea1" /> <selectOption selector="{{AttributePropertiesSection.InputType}}" userInput="Text Area" stepKey="selectTextArea" /> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml new file mode 100644 index 000000000000..f3ec225540c7 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminFilterByNameByStoreViewOnProductGridTest"> + <annotations> + <features value="Catalog"/> + <stories value="Filter products"/> + <title value="Product grid filtering by store view level attribute"/> + <description value="Verify that products grid can be filtered on all store view level by attribute"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-98755"/> + <useCaseId value="MAGETWO-98335"/> + <group value="catalog"/> + </annotations> + <before> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="ClearProductsFilterActionGroup" stepKey="clearProductsFilter"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <amOnPage url="{{AdminProductEditPage.url($$createSimpleProduct.id$$)}}" stepKey="goToEditPage"/> + <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="switchToDefaultStoreView"> + <argument name="storeView" value="_defaultStore.name"/> + </actionGroup> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.productNameUseDefault}}" stepKey="uncheckUseDefault"/> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="fillNewName"/> + <actionGroup ref="saveProductForm" stepKey="saveSimpleProduct"/> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <actionGroup ref="filterProductGridByName" stepKey="filterGridByName"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> + <see selector="{{AdminProductGridSection.firstProductRow}}" userInput="{{SimpleProduct2.name}}" stepKey="seeProductNameInGrid"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml new file mode 100644 index 000000000000..5c434ecabf80 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml @@ -0,0 +1,160 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminFilteringCategoryProductsUsingScopeSelectorTest"> + <annotations> + <stories value="Filtering Category Products"/> + <title value="Filtering Category Products using scope selector"/> + <description value="Filtering Category Products using scope selector"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-48850"/> + <group value="catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create website, Store and Store View--> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createSecondWebsite"> + <argument name="newWebsiteName" value="{{secondCustomWebsite.name}}"/> + <argument name="websiteCode" value="{{secondCustomWebsite.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createSecondStoreGroup"> + <argument name="website" value="{{secondCustomWebsite.name}}"/> + <argument name="storeGroupName" value="{{SecondStoreGroupUnique.name}}"/> + <argument name="storeGroupCode" value="{{SecondStoreGroupUnique.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createSecondStoreView"> + <argument name="StoreGroup" value="SecondStoreGroupUnique"/> + <argument name="customStore" value="SecondStoreUnique"/> + </actionGroup> + + <!--Create Simple Product and Category --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct0"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createProduct1"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createProduct2"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createProduct12"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Set filter to product name and product0 not assigned to any website--> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForProduct0"> + <argument name="product" value="$$createProduct0$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="clickOpenProductForEdit0"> + <argument name="product" value="$$createProduct0$$"/> + </actionGroup> + <scrollTo selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="scrollToWebsitesSection"/> + <click selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="clickToOpenWebsiteSection"/> + <waitForPageLoad stepKey="waitForToOpenedWebsiteSection"/> + <uncheckOption selector="{{ProductInWebsitesSection.website('Main Website')}}" stepKey="uncheckWebsite"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct1"/> + + <!-- Set filter to product name and product2 in website 2 only --> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForProduct2"> + <argument name="product" value="$$createProduct2$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="clickOpenProductForEdit2"> + <argument name="product" value="$$createProduct2$$"/> + </actionGroup> + <actionGroup ref="SelectProductInWebsitesActionGroup" stepKey="selectProductInWebsites"> + <argument name="website" value="{{secondCustomWebsite.name}}"/> + </actionGroup> + <uncheckOption selector="{{ProductInWebsitesSection.website('Main Website')}}" stepKey="uncheckWebsite1"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct2"/> + + <!-- Set filter to product name and product12 assigned to both websites 1 and 2 --> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForProduct12"> + <argument name="product" value="$$createProduct12$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="clickOpenProductForEdit12"> + <argument name="product" value="$$createProduct12$$"/> + </actionGroup> + <actionGroup ref="SelectProductInWebsitesActionGroup" stepKey="selectProductInWebsites1"> + <argument name="website" value="{{secondCustomWebsite.name}}"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProduct3"/> + </before> + <after> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="{{secondCustomWebsite.name}}"/> + </actionGroup> + <actionGroup ref="ClearProductsFilterActionGroup" stepKey="clearProductsFilter"/> + <deleteData createDataKey="createProduct0" stepKey="deleteProduct"/> + <deleteData createDataKey="createProduct1" stepKey="deleteProduct1"/> + <deleteData createDataKey="createProduct2" stepKey="deleteProduct2"/> + <deleteData createDataKey="createProduct12" stepKey="deleteProduct3"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Step 1-2: Open Category page and Set scope selector to All Store Views--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="goToCategoryPage"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createCategory.name$$)}}" + stepKey="clickCategoryName"/> + <click selector="{{AdminCategoryProductsSection.sectionHeader}}" stepKey="openProductSection"/> + <grabTextFrom selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createCategory.name$$)}}" + stepKey="grabTextFromCategory"/> + <assertRegExp expected="/\(4\)$/" expectedType="string" actual="$grabTextFromCategory" actualType="variable" + message="wrongCountProductOnAllStoreViews" stepKey="checkCountProducts"/> + <see selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct0.name$$)}}" + userInput="$$createProduct0.name$$" stepKey="seeProductName"/> + <see selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct1.name$$)}}" + userInput="$$createProduct1.name$$" stepKey="seeProductName1"/> + <see selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct2.name$$)}}" + userInput="$$createProduct2.name$$" stepKey="seeProductName2"/> + <see selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct12.name$$)}}" + userInput="$$createProduct12.name$$" stepKey="seeProductName3"/> + + <!-- Step 3: Set scope selector to Website1( Storeview for the Website 1) --> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="swichToDefaultStoreView"> + <argument name="storeView" value="_defaultStore.name"/> + </actionGroup> + <grabTextFrom selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createCategory.name$$)}}" + stepKey="grabTextFromCategory1"/> + <assertRegExp expected="/\(2\)$/" expectedType="string" actual="$grabTextFromCategory1" actualType="variable" + message="wrongCountProductOnWebsite1" stepKey="checkCountProducts1"/> + <click selector="{{AdminCategoryProductsSection.sectionHeader}}" stepKey="openProductSection1"/> + <see selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct1.name$$)}}" + userInput="$$createProduct1.name$$" stepKey="seeProductName4"/> + <see selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct12.name$$)}}" + userInput="$$createProduct12.name$$" stepKey="seeProductName5"/> + <waitForText userInput="$$createCategory.name$$ (2)" stepKey="seeCorrectProductCount"/> + <dontSee selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct0.name$$)}}" + userInput="$$createProduct0.name$$" stepKey="dontSeeProductName"/> + <dontSee selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct2.name$$)}}" + userInput="$$createProduct2.name$$" stepKey="dontSeeProductName1"/> + + <!-- Step 4: Set scope selector to Website2 ( StoreView for Website 2) --> + <scrollToTopOfPage stepKey="scrollToTopOfPage1"/> + <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="swichToSecondStoreView"> + <argument name="storeView" value="SecondStoreUnique.name"/> + </actionGroup> + <click selector="{{AdminCategoryProductsSection.sectionHeader}}" stepKey="openProductSection2"/> + <grabTextFrom selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createCategory.name$$)}}" + stepKey="grabTextFromCategory2"/> + <assertRegExp expected="/\(2\)$/" expectedType="string" actual="$grabTextFromCategory2" actualType="variable" + message="wrongCountProductOnWebsite2" stepKey="checkCountProducts2"/> + <see selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct2.name$$)}}" + userInput="$$createProduct2.name$$" stepKey="seeProductName6"/> + <see selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct12.name$$)}}" + userInput="$$createProduct12.name$$" stepKey="seeProductName7"/> + <waitForText userInput="$$createCategory.name$$ (2)" stepKey="seeCorrectProductCount2"/> + <dontSee selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct0.name$$)}}" + userInput="$$createProduct0.name$$" stepKey="dontSeeProductName2"/> + <dontSee selector="{{AdminCategoryProductsGridSection.productGridNameProduct($$createProduct2.name$$)}}" + userInput="$$createProduct1.name$$" stepKey="dontSeeProductName3"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml new file mode 100644 index 000000000000..b24ed7f9c9a8 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminGridPageNumberAfterSaveAndCloseActionTest"> + <annotations> + <features value="Catalog"/> + <title value="Checking Catalog grid page number after Save and Close action"/> + <description value="Checking Catalog grid page number after Save and Close action"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-96164"/> + <useCaseId value="MAGETWO-96127"/> + <group value="Catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Clear product grid--> + <comment userInput="Clear product grid" stepKey="commentClearProductGrid"/> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="goToProductCatalog"/> + <waitForPageLoad stepKey="waitForProductIndexPage"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" + dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <waitForLoadingMaskToDisappear stepKey="waitForGridLoad"/> + <actionGroup ref="deleteProductsIfTheyExist" stepKey="deleteProductIfTheyExist"/> + <createData stepKey="category1" entity="SimpleSubCategory"/> + <createData stepKey="product1" entity="SimpleProduct"> + <requiredEntity createDataKey="category1"/> + </createData> + <createData stepKey="category2" entity="SimpleSubCategory"/> + <createData stepKey="product2" entity="SimpleProduct"> + <requiredEntity createDataKey="category2"/> + </createData> + </before> + <after> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="goToProductCatalog"/> + <waitForPageLoad stepKey="waitForProductIndexPage"/> + <click selector="{{AdminDataGridPaginationSection.previousPage}}" stepKey="clickPrevPageOrderGrid"/> + <actionGroup ref="adminDataGridDeleteCustomPerPage" stepKey="deleteCustomAddedPerPage"> + <argument name="perPage" value="ProductPerPage.productCount"/> + </actionGroup> + <deleteData stepKey="deleteCategory1" createDataKey="category1"/> + <deleteData stepKey="deleteProduct1" createDataKey="product1"/> + <deleteData stepKey="deleteCategory2" createDataKey="category2"/> + <deleteData stepKey="deleteProduct2" createDataKey="product2"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="goToProductCatalog"/> + <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="adminDataGridSelectCustomPerPage" stepKey="select1OrderPerPage"> + <argument name="perPage" value="ProductPerPage.productCount"/> + </actionGroup> + <!--Go to the next page and edit the product--> + <comment userInput="Go to the next page and edit the product" stepKey="commentEdiProduct"/> + <click selector="{{AdminDataGridPaginationSection.nextPage}}" stepKey="clickNextPageOrderGrid"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct2"> + <argument name="product" value="$$product2$$"/> + </actionGroup> + <actionGroup ref="AdminFormSaveAndClose" stepKey="saveAndCloseProduct"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <seeInField selector="{{AdminDataGridPaginationSection.currentPage}}" userInput="2" stepKey="seeOnSecondPageOrderGrid"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml new file mode 100644 index 000000000000..79ff7bcade77 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml @@ -0,0 +1,82 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminImportCustomizableOptionToProductWithSKUTest"> + <annotations> + <features value="Catalog"/> + <title value="Import customizable options to a product with existing SKU"/> + <description value="Import customizable options to a product with existing SKU"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-98211"/> + <useCaseId value="MAGETWO-70232"/> + <group value="catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create category--> + <comment userInput="Create category" stepKey="commentCreateCategory"/> + <createData entity="ApiCategory" stepKey="createCategory"/> + <!-- Create two product --> + <comment userInput="Create two product" stepKey="commentCreateTwoProduct"/> + <createData entity="SimpleProduct2" stepKey="createFirstProduct"/> + <createData entity="ApiSimpleProduct" stepKey="createSecondProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <!--Delete second product with changed sku--> + <comment userInput="Delete second product with changed sku" stepKey="commentDeleteProduct"/> + <actionGroup ref="deleteProductBySku" stepKey="deleteSecondProduct"> + <argument name="sku" value="$$createFirstProduct.sku$$-1"/> + </actionGroup> + <!--Delete created data--> + <comment userInput="Delete created data" stepKey="commentDeleteCreatedData"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + <!--Go to product page --> + <comment userInput="Go to product page" stepKey="commentGoToProductPage"/> + <amOnPage url="{{AdminProductEditPage.url($$createFirstProduct.id$$)}}" stepKey="goToProductEditPage"/> + <waitForPageLoad stepKey="waitForProductEditPageLoad"/> + <actionGroup ref="AddProductCustomOptionField" stepKey="addCutomOption1"> + <argument name="option" value="ProductOptionField"/> + <argument name="optionIndex" value="0"/> + </actionGroup> + <actionGroup ref="AddProductCustomOptionField" stepKey="addCutomOption2"> + <argument name="option" value="ProductOptionField2"/> + <argument name="optionIndex" value="1"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + <!--Change second product sku to first product sku--> + <comment userInput="Change second product sku to first product sku" stepKey="commentChangeSecondProduct"/> + <amOnPage url="{{AdminProductEditPage.url($$createSecondProduct.id$$)}}" stepKey="goToProductEditPage1"/> + <waitForPageLoad stepKey="waitForProductEditPageLoad1"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="$$createFirstProduct.sku$$" stepKey="fillProductSku1"/> + <!--Import customizable options and check--> + <comment userInput="Import customizable options and check" stepKey="commentImportOptions"/> + <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" visible="false" stepKey="openCustomOptionSection"/> + <actionGroup ref="importProductCustomizableOptions" stepKey="importOptions"> + <argument name="productName" value="$$createFirstProduct.name$$"/> + </actionGroup> + <actionGroup ref="checkCustomizableOptionImport" stepKey="checkFirstOptionImport"> + <argument name="option" value="ProductOptionField"/> + <argument name="optionIndex" value="0"/> + </actionGroup> + <actionGroup ref="checkCustomizableOptionImport" stepKey="checkSecondOptionImport"> + <argument name="option" value="ProductOptionField2"/> + <argument name="optionIndex" value="1"/> + </actionGroup> + <!--Save product and check sku changed message--> + <comment userInput="Save product and check sku changed message" stepKey="commentSAveProductAndCheck"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct1"/> + <see userInput="SKU for product $$createSecondProduct.name$$ has been changed to $$createFirstProduct.sku$$-1." stepKey="seeSkuChangedMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml new file mode 100644 index 000000000000..4d581bae700d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml @@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMassProductPriceUpdateTest"> + <annotations> + <stories value="Mass product update "/> + <features value="Catalog"/> + <title value="Mass update simple product price"/> + <description value="Login as admin and update mass product price"/> + <testCaseId value="MC-8510"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="defaultSimpleProduct" stepKey="simpleProduct1"/> + <createData entity="defaultSimpleProduct" stepKey="simpleProduct2"/> + </before> + <after> + <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open Product Index Page--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + + <!--Search products using keyword --> + <actionGroup ref="searchProductGridByKeyword2" stepKey="searchByKeyword"> + <argument name="keyword" value="Testp"/> + </actionGroup> + + <!--Sort Products by ID in descending order--> + <actionGroup ref="sortProductsByIdDescending" stepKey="sortProductsByIdDescending"/> + + <!--Select products--> + <checkOption selector="{{AdminProductGridSection.productRowCheckboxBySku($$simpleProduct1.sku$$)}}" stepKey="selectFirstProduct"/> + <checkOption selector="{{AdminProductGridSection.productRowCheckboxBySku($$simpleProduct2.sku$$)}}" stepKey="selectSecondProduct"/> + + <!-- Update product price--> + <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickDropdown"/> + <click selector="{{AdminProductGridSection.bulkActionOption('Update attributes')}}" stepKey="clickChangeStatus"/> + <waitForPageLoad stepKey="waitForProductAttributePageToLoad"/> + <scrollTo stepKey="scrollToPriceCheckBox" selector="{{AdminEditProductAttributesSection.ChangeAttributePriceToggle}}" x="0" y="-160"/> + <click selector="{{AdminEditProductAttributesSection.ChangeAttributePriceToggle}}" stepKey="selectPriceCheckBox"/> + <fillField stepKey="fillPrice" selector="{{AdminEditProductAttributesSection.AttributePrice}}" userInput="90.99"/> + <click stepKey="clickOnSaveButton" selector="{{AdminEditProductAttributesSection.Save}}"/> + <waitForPageLoad stepKey="waitForUpdatedProductToSave" /> + <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="Message is added to queue" stepKey="seeAttributeUpateSuccessMsg"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitFormToReload1"/> + + <!--Verify product name, sku and updated price--> + <click stepKey="openFirstProduct" selector="{{AdminProductGridSection.productRowBySku($$simpleProduct1.sku$$)}}"/> + <waitForPageLoad stepKey="waitForFirstProductToLoad"/> + <seeInField stepKey="seeFirstProductNameInField" selector="{{AdminProductFormSection.productName}}" userInput="$$simpleProduct1.name$$"/> + <seeInField stepKey="seeFirstProductSkuInField" selector="{{AdminProductFormSection.productSku}}" userInput="$$simpleProduct1.sku$$"/> + <seeInField stepKey="seeFirstProductPriceInField" selector="{{AdminProductFormSection.productPrice}}" userInput="90.99"/> + <click stepKey="clickOnBackButton" selector="{{AdminGridMainControls.back}}"/> + <waitForPageLoad stepKey="waitForProductsToLoad"/> + <click stepKey="openSecondProduct" selector="{{AdminProductGridSection.productRowBySku($$simpleProduct2.sku$$)}}"/> + <waitForPageLoad stepKey="waitForSecondProductToLoad"/> + <seeInField stepKey="seeSecondProductNameInField" selector="{{AdminProductFormSection.productName}}" userInput="$$simpleProduct2.name$$"/> + <seeInField stepKey="seeSecondProductSkuInField" selector="{{AdminProductFormSection.productSku}}" userInput="$$simpleProduct2.sku$$"/> + <seeInField stepKey="seeSecondProductPriceInField" selector="{{AdminProductFormSection.productPrice}}" userInput="90.99"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml index c0eebd1512d6..8a44c8093ca5 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml @@ -58,7 +58,13 @@ <click selector="{{AdminEditProductAttributesSection.ChangeAttributePriceToggle}}" stepKey="toggleToChangePrice"/> <fillField selector="{{AdminEditProductAttributesSection.AttributePrice}}" userInput="$$createProductOne.price$$0" stepKey="fillAttributeNameField"/> <click selector="{{AdminEditProductAttributesSection.Save}}" stepKey="save"/> - <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="A total of 2 record(s) were updated." stepKey="seeAttributeUpateSuccessMsg"/> + <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="Message is added to queue" stepKey="seeAttributeUpateSuccessMsg"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitFormToReload1"/> <!-- Assert on storefront default view --> <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroupDefault"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest.xml index 845c47c0e4c2..bee13bec370d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest.xml @@ -52,7 +52,13 @@ <click selector="{{AdminEditProductAttributesSection.ChangeAttributeDescriptionToggle}}" stepKey="toggleToChangeDescription"/> <fillField selector="{{AdminEditProductAttributesSection.AttributeDescription}}" userInput="Updated $$createProductOne.custom_attributes[description]$$" stepKey="fillAttributeDescriptionField"/> <click selector="{{AdminEditProductAttributesSection.Save}}" stepKey="save"/> - <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="A total of 2 record(s) were updated." stepKey="seeAttributeUpateSuccessMsg"/> + <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="Message is added to queue" stepKey="seeAttributeUpateSuccessMsg"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitFormToReload1"/> <!-- Assert on storefront default view --> <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroupDefault"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductStatusStoreViewScopeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductStatusStoreViewScopeTest.xml new file mode 100644 index 000000000000..e9b54e3f1a3d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductStatusStoreViewScopeTest.xml @@ -0,0 +1,151 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMassUpdateProductStatusStoreViewScopeTest"> + <annotations> + <features value="Catalog"/> + <stories value="Mass update product status"/> + <title value="Admin should be able to mass update product statuses in store view scope"/> + <description value="Admin should be able to mass update product statuses in store view scope"/> + <severity value="AVERAGE"/> + <testCaseId value="MAGETWO-59361"/> + <group value="Catalog"/> + <group value="Product Attributes"/> + </annotations> + <before> + <actionGroup ref="LoginActionGroup" stepKey="loginAsAdmin"/> + + <!--Create Website --> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createAdditionalWebsite"> + <argument name="newWebsiteName" value="Second Website"/> + <argument name="websiteCode" value="second_website"/> + </actionGroup> + + <!--Create Store --> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStore"> + <argument name="website" value="Second Website"/> + <argument name="storeGroupName" value="Second Store"/> + <argument name="storeGroupCode" value="second_store"/> + </actionGroup> + + <!--Create Store view --> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <waitForPageLoad stepKey="waitForSystemStorePage"/> + <click selector="{{AdminStoresMainActionsSection.createStoreViewButton}}" stepKey="createStoreViewButton"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <waitForElementVisible selector="//legend[contains(., 'Store View Information')]" stepKey="waitForNewStorePageToOpen"/> + <selectOption userInput="Second Store" selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="selectStoreGroup"/> + <fillField userInput="Second Store View" selector="{{AdminNewStoreSection.storeNameTextField}}" stepKey="fillStoreViewName"/> + <fillField userInput="second_store_view" selector="{{AdminNewStoreSection.storeCodeTextField}}" stepKey="fillStoreViewCode"/> + <selectOption selector="{{AdminNewStoreSection.statusDropdown}}" userInput="1" stepKey="enableStoreViewStatus"/> + <click selector="{{AdminNewStoreViewActionsSection.saveButton}}" stepKey="clickSaveStoreView" /> + <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForModal" /> + <see selector="{{AdminConfirmationModalSection.title}}" userInput="Warning message" stepKey="seeWarning" /> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="dismissModal" /> + <waitForPageLoad stepKey="waitForPageLoad2" time="180" /> + <waitForElementVisible selector="{{AdminStoresGridSection.storeFilterTextField}}" time="150" stepKey="waitForPageReolad"/> + <see userInput="You saved the store view." stepKey="seeSavedMessage" /> + + <!--Create a Simple Product 1 --> + <actionGroup ref="createSimpleProductAndAddToWebsite" stepKey="createSimpleProduct1"> + <argument name="product" value="simpleProductForMassUpdate"/> + <argument name="website" value="Second Website"/> + </actionGroup> + + <!--Create a Simple Product 2 --> + <actionGroup ref="createSimpleProductAndAddToWebsite" stepKey="createSimpleProduct2"> + <argument name="product" value="simpleProductForMassUpdate2"/> + <argument name="website" value="Second Website"/> + </actionGroup> + </before> + <after> + <!--Delete website --> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteSecondWebsite"> + <argument name="websiteName" value="Second Website"/> + </actionGroup> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + + <!--Delete Products --> + <actionGroup ref="DeleteProductActionGroup" stepKey="deleteProduct1"> + <argument name="productName" value="simpleProductForMassUpdate.name"/> + </actionGroup> + <actionGroup ref="DeleteProductActionGroup" stepKey="deleteProduct2"> + <argument name="productName" value="simpleProductForMassUpdate2.name"/> + </actionGroup> + <actionGroup ref="logout" stepKey="amOnLogoutPage"/> + </after> + + <!-- Search and select products --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> + <actionGroup ref="searchProductGridByKeyword2" stepKey="searchByKeyword"> + <argument name="keyword" value="{{simpleProductForMassUpdate.keyword}}"/> + </actionGroup> + <actionGroup ref="sortProductsByIdDescending" stepKey="sortProductsByIdDescending"/> + + <!-- Filter to Second Store View --> + <actionGroup ref="AdminFilterStoreViewActionGroup" stepKey="filterStoreView" > + <argument name="customStore" value="'Second Store View'" /> + </actionGroup> + + <!-- Select Product 2 --> + <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('2')}}" stepKey="clickCheckbox2"/> + + <!-- Mass update attributes --> + <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickDropdown"/> + <click selector="{{AdminProductGridSection.bulkActionOption('Change status')}}" stepKey="clickOption"/> + <click selector="{{AdminProductGridSection.bulkActionOption('Disable')}}" stepKey="clickDisabled"/> + <waitForPageLoad stepKey="waitForBulkUpdatePage"/> + + <!-- Verify Product Statuses --> + <see selector="{{AdminProductGridSection.productGridContentsOnRow('1')}}" userInput="Enabled" stepKey="checkIfProduct1IsEnabled"/> + <see selector="{{AdminProductGridSection.productGridContentsOnRow('2')}}" userInput="Disabled" stepKey="checkIfProduct2IsDisabled"/> + + <!-- Filter to Default Store View --> + <actionGroup ref="AdminFilterStoreViewActionGroup" stepKey="filterDefaultStoreView"> + <argument name="customStore" value="'Default'" /> + </actionGroup> + + <!-- Verify Product Statuses --> + <see selector="{{AdminProductGridSection.productGridContentsOnRow('1')}}" userInput="Enabled" stepKey="checkIfDefaultViewProduct1IsEnabled"/> + <see selector="{{AdminProductGridSection.productGridContentsOnRow('2')}}" userInput="Enabled" stepKey="checkIfDefaultViewProduct2IsEnabled"/> + + <!-- Assert on storefront default view --> + <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroupDefault"/> + <actionGroup ref="StorefrontAdvancedCatalogSearchByProductNameAndDescriptionActionGroup" stepKey="searchByNameDefault"> + <argument name="name" value="{{simpleProductForMassUpdate.keyword}}"/> + <argument name="description" value=""/> + </actionGroup> + <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResultDefault"/> + <see userInput="2 items" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="seeInDefault"/> + + <!-- Enable the product in Default store view --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex2"/> + <waitForPageLoad stepKey="waitForProductIndexPageLoad2"/> + <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('1')}}" stepKey="clickCheckboxDefaultStoreView"/> + <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('2')}}" stepKey="clickCheckboxDefaultStoreView2"/> + + <!-- Mass update attributes --> + <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickDropdownDefaultStoreView"/> + <click selector="{{AdminProductGridSection.bulkActionOption('Change status')}}" stepKey="clickOptionDefaultStoreView"/> + <click selector="{{AdminProductGridSection.bulkActionOption('Disable')}}" stepKey="clickDisabledDefaultStoreView"/> + <waitForPageLoad stepKey="waitForBulkUpdatePageDefaultStoreView"/> + <see selector="{{AdminProductGridSection.productGridContentsOnRow('1')}}" userInput="Disabled" stepKey="checkIfProduct2IsDisabledDefaultStoreView"/> + + <!-- Assert on storefront default view --> + <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroupDefault2"/> + <actionGroup ref="StorefrontAdvancedCatalogSearchByProductNameAndDescriptionActionGroup" stepKey="searchByNameDefault2"> + <argument name="name" value="{{simpleProductForMassUpdate.name}}"/> + <argument name="description" value=""/> + </actionGroup> + <actionGroup ref="StorefrontCheckAdvancedSearchResultActionGroup" stepKey="StorefrontCheckAdvancedSearchResultDefault2"/> + <see userInput="We can't find any items matching these search criteria." selector="{{StorefrontCatalogSearchAdvancedResultMainSection.message}}" stepKey="seeInDefault2"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml new file mode 100644 index 000000000000..247711295a55 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml @@ -0,0 +1,123 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMoveAnchoredCategoryToDefaultCategoryTest"> + <annotations> + <stories value="Move categories"/> + <title value="Move default anchored subcategory with anchored parent to default subcategory"/> + <description value="Login as admin,move anchored subcategory with anchored parent to default subcategory"/> + <testCaseId value="MC-6493"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + <features value="Catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="_defaultCategory" stepKey="createDefaultCategory"/> + <createData entity="defaultSimpleProduct" stepKey="simpleProduct"/> + </before> + <after> + <deleteData createDataKey="createDefaultCategory" stepKey="deleteDefaultCategory"/> + <deleteData createDataKey="simpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open Category Page--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoaded"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCategory"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + + <!--Enable Anchor for _defaultCategory Category--> + <scrollTo selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" x="0" y="-80" stepKey="scrollToDisplaySetting"/> + <click selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" stepKey="selectDisplaySetting"/> + <checkOption selector="{{CategoryDisplaySettingsSection.anchor}}" stepKey="enableAnchor"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> + <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + + <!--Enable Anchor for FirstLevelSubCat Category--> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{FirstLevelSubCat.name}}" stepKey="addSubCategoryName"/> + <scrollTo selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" x="0" y="-80" stepKey="scrollToDisplaySetting1"/> + <click selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" stepKey="selectDisplaySetting1"/> + <checkOption selector="{{CategoryDisplaySettingsSection.anchor}}" stepKey="enableAnchor1"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory1"/> + <waitForPageLoad stepKey="waitForSecondCategoryToSave1"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage1"/> + + <!--Enable Anchor for SimpleSubCategory Category and add products to the Category--> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton1"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName1"/> + <scrollTo selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" x="0" y="-80" stepKey="scrollToDisplaySetting2"/> + <click selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" stepKey="selectDisplaySetting2"/> + <checkOption selector="{{CategoryDisplaySettingsSection.anchor}}" stepKey="enableAnchor2"/> + <scrollTo selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" x="0" y="-80" stepKey="scrollToProductInCategory1"/> + <click selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" stepKey="clickOnProductInCategory"/> + <fillField selector="{{AdminCategoryContentSection.productTableColumnName}}" userInput="$$simpleProduct.name$$" stepKey="selectProduct"/> + <click selector="{{AdminCategoryContentSection.productSearch}}" stepKey="clickSearchButton"/> + <click selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="selectProductFromTableRow"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory2"/> + <waitForPageLoad stepKey="waitForSecondCategoryToSave2"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage2"/> + + <!--Open Category in store front page--> + <amOnPage url="/$$createDefaultCategory.name$$/{{FirstLevelSubCat.name}}/{{SimpleSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> + <waitForPageLoad stepKey="waitForStoreFrontPageLoad"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="seeDefaultCategoryOnStoreNavigationBar"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="dontSeeSubCategoryOnStoreNavigationBar"/> + + <!--<Verify breadcrumbs in store front page--> + <grabMultiple selector="{{StorefrontNavigationSection.categoryBreadcrumbs}}" stepKey="breadcrumbs"/> + <assertEquals stepKey="verifyTheCategoryInStoreFrontPage"> + <expectedResult type="array">['Home', $$createDefaultCategory.name$$,{{FirstLevelSubCat.name}}, {{SimpleSubCategory.name}}]</expectedResult> + <actualResult type="variable">breadcrumbs</actualResult> + </assertEquals> + + <!--Verify Product displayed in category store front page--> + <click selector="{{StorefrontCategoryMainSection.productLink}}" stepKey="openSearchedProduct"/> + <waitForPageLoad stepKey="waitForProductToLoad1"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{defaultSimpleProduct.name}}" stepKey="assertProductName"/> + + <!--Open Category Page--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage1"/> + <waitForPageLoad stepKey="waitForPageToLoad1"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree2"/> + <waitForPageLoad stepKey="waitForPageToLoad2"/> + + <!--Move SubCategory under Default Category--> + <dragAndDrop selector1="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" selector2="{{AdminCategorySidebarTreeSection.categoryInTree('Default Category')}}" stepKey="moveCategory"/> + <see selector="{{AdminCategoryModalSection.message}}" userInput="This operation can take a long time" stepKey="seeWarningMessage"/> + <click selector="{{AdminCategoryModalSection.ok}}" stepKey="clickOkButtonOnWarningPopup"/> + <waitForPageLoad stepKey="waitForPageToLoad3"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You moved the category." stepKey="seeSuccessMoveMessage"/> + <amOnPage url="/{{SimpleSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage1"/> + <waitForPageLoad stepKey="waitForStoreFrontPageLoad1"/> + + <!--Verify breadcrumbs in store front page after the move--> + <grabMultiple selector="{{StorefrontNavigationSection.categoryBreadcrumbs}}" stepKey="breadcrumbsAfterMove"/> + <assertEquals stepKey="verifyBreadcrumbsInFrontPageAfterMove"> + <expectedResult type="array">['Home',{{SimpleSubCategory.name}}]</expectedResult> + <actualResult type="variable">breadcrumbsAfterMove</actualResult> + </assertEquals> + + <!--Open Category in store front--> + <amOnPage url="{{StorefrontCategoryPage.url(SimpleSubCategory.name)}}" stepKey="amOnCategoryPage"/> + <waitForPageLoad stepKey="waitForPageToBeLoaded"/> + <seeElement selector="{{StorefrontCategoryMainSection.CategoryTitle(SimpleSubCategory.name)}}" stepKey="seeCategoryInTitle"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="seeCategoryOnStoreNavigationBarAfterMove"/> + <click selector="{{StorefrontCategoryMainSection.productLink}}" stepKey="openSearchedProduct1"/> + <waitForPageLoad stepKey="waitForProductToLoad2"/> + + <!--Verify product name on Store Front--> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{defaultSimpleProduct.name}}" stepKey="assertProductNameAfterMove"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml new file mode 100644 index 000000000000..ba6e6a43674c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml @@ -0,0 +1,113 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMoveCategoryAndCheckUrlRewritesTest"> + <annotations> + <stories value="Move categories"/> + <title value="URL Rewrites for subcategories during creation and move"/> + <description value="Login as admin, move category from one to another and check category url rewrites"/> + <testCaseId value="MC-6494"/> + <features value="Catalog"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="_defaultCategory" stepKey="createDefaultCategory"/> + </before> + <after> + <deleteData createDataKey="createDefaultCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open category page--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoaded"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCategory"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + + <!--Create second level category--> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SubCategory.name}}" stepKey="addSubCategoryName"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory1"/> + <waitForPageLoad stepKey="waitForSecondCategoryToSave1"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage1"/> + + <!--Create third level category under second level category--> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton1"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName1"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory2"/> + <waitForPageLoad stepKey="waitForSecondCategoryToSave2"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage2"/> + <grabFromCurrentUrl stepKey="categoryId" regex="#\/([0-9]*)?\/$#" /> + + <!--Open Url Rewrite Page--> + <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="openUrlRewriteIndexPage"/> + <waitForPageLoad stepKey="waitForUrlRewritePage"/> + + <!--Search third level category Redirect Path, Target Path and Redirect Type--> + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="{{SimpleSubCategory.name_lwr}}" stepKey="fillRedirectPathFilter"/> + <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton"/> + <waitForPageLoad stepKey="waitForPageToLoad0"/> + + <!--Verify Category RedirectType--> + <see stepKey="verifyTheRedirectType" selector="{{AdminUrlRewriteIndexSection.redirectTypeColumn('1')}}" userInput="No" /> + + <!--Verify Redirect Path --> + <see selector="{{AdminUrlRewriteIndexSection.requestPathColumn('1')}}" userInput="{{_defaultCategory.name_lwr}}2/{{SubCategory.name_lwr}}/{{SimpleSubCategory.name_lwr}}.html" stepKey="verifyTheRedirectPath"/> + + <!--Verify Category Target Path--> + <see stepKey="verifyTheTargetPath" selector="{{AdminUrlRewriteIndexSection.targetPathColumn('1')}}" userInput="catalog/category/view/id/{$categoryId}"/> + + <!--Open Category Page --> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage1"/> + <waitForPageLoad stepKey="waitForPageToLoad1"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree2"/> + <waitForPageLoad stepKey="waitForPageToLoad2"/> + + <!--Move the third level category under first level category --> + <dragAndDrop selector1="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" selector2="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="moveCategory"/> + <see selector="{{AdminCategoryModalSection.message}}" userInput="This operation can take a long time" stepKey="seeWarningMessage"/> + <click selector="{{AdminCategoryModalSection.ok}}" stepKey="clickOkButtonOnWarningPopup"/> + <waitForPageLoad stepKey="waitForPageToLoad3"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You moved the category." stepKey="seeSuccessMoveMessage"/> + + <!--Open Url Rewrite page --> + <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="openUrlRewriteIndexPage1"/> + <waitForPageLoad stepKey="waitForUrlRewritePage1"/> + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="{{_defaultCategory.name_lwr}}2/{{SimpleSubCategory.name_lwr}}.html" stepKey="fillCategoryUrlKey1"/> + <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton1"/> + <waitForPageLoad stepKey="waitForPageToLoad4"/> + + <!--Verify new Redirect Path after move --> + <see stepKey="verifyTheRequestPathAfterMove" selector="{{AdminUrlRewriteIndexSection.requestPathColumn('1')}}" userInput="{{_defaultCategory.name_lwr}}2/{{SimpleSubCategory.name_lwr}}.html" /> + + <!--Verify new Target Path after move --> + <see stepKey="verifyTheTargetPathAfterMove" selector="{{AdminUrlRewriteIndexSection.targetPathColumn('1')}}" userInput="catalog/category/view/id/{$categoryId}" /> + + <!--Verify new RedirectType after move --> + <see stepKey="verifyTheRedirectTypeAfterMove" selector="{{AdminUrlRewriteIndexSection.redirectTypeColumn('1')}}" userInput="No" /> + + <!--Verify before move Redirect Path displayed with associated Target Path and Redirect Type--> + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="{{SimpleSubCategory.name_lwr}}" stepKey="fillCategoryUrlKey2"/> + <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton2"/> + <waitForPageLoad stepKey="waitForPageToLoad5"/> + <see stepKey="verifyTheRedirectTypeAfterMove1" selector="{{AdminUrlRewriteIndexSection.redirectTypeColumn('1')}}" userInput="Permanent (301)" /> + <see stepKey="verifyTheRequestPathAfterMove1" selector="{{AdminUrlRewriteIndexSection.requestPathColumn('1')}}" userInput="{{_defaultCategory.name_lwr}}2/{{SubCategory.name_lwr}}/{{SimpleSubCategory.name_lwr}}.html" /> + <see stepKey="verifyTheTargetPathAfterMove1" selector="{{AdminUrlRewriteIndexSection.targetPathColumn('1')}}" userInput="{{_defaultCategory.name_lwr}}2/{{SimpleSubCategory.name_lwr}}.html" /> + + <!--Verify before move Redirect Path directs to the category page--> + <amOnPage url="{{_defaultCategory.name_lwr}}2/{{SubCategory.name_lwr}}/{{SimpleSubCategory.name_lwr}}.html" stepKey="openCategoryStoreFrontPage"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="seeCategoryOnStoreNavigationBar"/> + <seeElement selector="{{StorefrontCategoryMainSection.CategoryTitle(SimpleSubCategory.name)}}" stepKey="seeCategoryInTitle"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml new file mode 100644 index 000000000000..d17078d794b4 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml @@ -0,0 +1,117 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMoveCategoryFromParentAnchoredCategoryTest"> + <annotations> + <stories value="Move categories"/> + <title value="Move default subcategory with anchored parent to default subcategory"/> + <description value="Login as admin,move subcategory with anchored parent to default subcategory"/> + <testCaseId value="MC-6492"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + <features value="Catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="defaultSimpleProduct" stepKey="simpleProduct"/> + <createData entity="_defaultCategory" stepKey="createDefaultCategory"/> + </before> + <after> + <deleteData createDataKey="simpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createDefaultCategory" stepKey="deleteDefaultCategory"/> + <actionGroup ref="DeleteCategory" stepKey="deleteCategory"> + <argument name="categoryEntity" value="SimpleSubCategory"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open Category page--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoaded"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCategory"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + + <!--Enable Anchor for _defaultCategory category --> + <scrollTo selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" x="0" y="-80" stepKey="scrollToDisplaySetting"/> + <click selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" stepKey="selectDisplaySetting"/> + <checkOption selector="{{CategoryDisplaySettingsSection.anchor}}" stepKey="enableAnchor"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> + + <!--Create a Subcategory under _defaultCategory category--> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName"/> + + <!--Add a product to SimpleSubCategory category--> + <scrollTo selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" x="0" y="-80" stepKey="scrollToProductInCategory"/> + <click selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" stepKey="clickOnProductInCategory"/> + <fillField selector="{{AdminCategoryContentSection.productTableColumnName}}" userInput="$$simpleProduct.name$$" stepKey="selectProduct"/> + <click selector="{{AdminCategoryContentSection.productSearch}}" stepKey="clickSearchButton"/> + <click selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="selectProductFromTableRow"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory1"/> + <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + + <!--Verify category displayed in store front page--> + <amOnPage url="/$$createDefaultCategory.name$$/{{SimpleSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> + <waitForPageLoad stepKey="waitForStoreFrontPageLoad"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="seeDefaultCategoryOnStoreNavigationBar"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="dontSeeSubCategoryOnStoreNavigationBar"/> + + <!--Check category breadcrumbs in store front page--> + <grabMultiple selector="{{StorefrontNavigationSection.categoryBreadcrumbs}}" stepKey="breadcrumbs"/> + <assertEquals stepKey="verifyTheCategoryInStoreFrontPage"> + <expectedResult type="array">['Home', $$createDefaultCategory.name$$,{{SimpleSubCategory.name}}]</expectedResult> + <actualResult type="variable">breadcrumbs</actualResult> + </assertEquals> + + <!--Verify Product displayed in category store front page--> + <click selector="{{StorefrontCategoryMainSection.productLink}}" stepKey="openSearchedProduct"/> + <waitForPageLoad stepKey="waitForProductToLoad1"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{defaultSimpleProduct.name}}" stepKey="assertProductName"/> + + <!--Open Category Page--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage1"/> + <waitForPageLoad stepKey="waitForPageToLoad1"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree2"/> + <waitForPageLoad stepKey="waitForPageToLoad2"/> + + <!--Move SubCategory under Default Category--> + <dragAndDrop selector1="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" selector2="{{AdminCategorySidebarTreeSection.categoryInTree('Default Category')}}" stepKey="moveCategory"/> + <see selector="{{AdminCategoryModalSection.message}}" userInput="This operation can take a long time" stepKey="seeWarningMessage"/> + <click selector="{{AdminCategoryModalSection.ok}}" stepKey="clickOkButtonOnWarningPopup"/> + <waitForPageLoad stepKey="waitForPageToLoad3"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You moved the category." stepKey="seeSuccessMoveMessage"/> + + <!--Open category in store front page--> + <amOnPage url="/{{SimpleSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage1"/> + <waitForPageLoad stepKey="waitForStoreFrontPageLoad1"/> + + <!--Verify breadcrumbs after the move in store front page--> + <grabMultiple selector="{{StorefrontNavigationSection.categoryBreadcrumbs}}" stepKey="breadcrumbsAfterMove"/> + <assertEquals stepKey="verifyBreadcrumbsInFrontPageAfterMove"> + <expectedResult type="array">['Home',{{SimpleSubCategory.name}}]</expectedResult> + <actualResult type="variable">breadcrumbsAfterMove</actualResult> + </assertEquals> + + <!--Open category store front page --> + <amOnPage url="{{StorefrontCategoryPage.url(SimpleSubCategory.name)}}" stepKey="amOnCategoryPage"/> + <waitForPageLoad stepKey="waitForPageToBeLoaded"/> + + <!--Verify Category in store front--> + <seeElement selector="{{StorefrontCategoryMainSection.CategoryTitle(SimpleSubCategory.name)}}" stepKey="seeCategoryInTitle"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="seeSubCategoryOnStoreNavigationBarAfterMove"/> + <click selector="{{StorefrontCategoryMainSection.productLink}}" stepKey="openSearchedProduct1"/> + <waitForPageLoad stepKey="waitForProductToLoad2"/> + + <!--Verify product name on Store Front--> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{defaultSimpleProduct.name}}" stepKey="assertProductNameAfterMove"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml new file mode 100644 index 000000000000..9831f73e0787 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml @@ -0,0 +1,116 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMoveCategoryToAnotherPositionInCategoryTreeTest"> + <annotations> + <stories value="Move categories"/> + <title value="Move Category to Another Position in Category Tree"/> + <description value="Test log in to Move Category and Move Category to Another Position in Category Tree"/> + <testCaseId value="MC-13612"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="_defaultCategory" stepKey="createDefaultCategory"/> + </before> + <after> + <deleteData createDataKey="createDefaultCategory" stepKey="deleteDefaultCategory"/> + <actionGroup ref="DeleteCategory" stepKey="SecondLevelSubCat"> + <argument name="categoryEntity" value="SecondLevelSubCat"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Open Category Page --> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoaded"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickExpandTree"/> + <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCategory"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <!-- Create three level deep sub Category --> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickAddSubCategoryButton"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{FirstLevelSubCat.name}}" stepKey="fillSubCategoryName"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveFirstLevelSubCategory"/> + <waitForPageLoad stepKey="waitForFirstLevelCategoryToSave"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButtonAgain"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SecondLevelSubCat.name}}" stepKey="fillSecondLevelSubCategoryName"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSecondLevelSubCategory"/> + <waitForPageLoad stepKey="waitForSecondLevelCategoryToSave"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSaveSuccessMessage"/> + <grabFromCurrentUrl stepKey="categoryId" regex="#\/([0-9]*)?\/$#" /> + + <!-- Move Category to another position in category tree, but click cancel button --> + <dragAndDrop selector1="{{AdminCategorySidebarTreeSection.categoryInTree(SecondLevelSubCat.name)}}" selector2="{{AdminCategorySidebarTreeSection.categoryInTree('Default Category')}}" stepKey="moveCategory"/> + <see selector="{{AdminCategoryModalSection.message}}" userInput="This operation can take a long time" stepKey="seeWarningMessage"/> + <click selector="{{AdminCategoryModalSection.cancel}}" stepKey="clickCancelButtonOnWarningPopup"/> + <!-- Verify Category in store front page after clicking cancel button --> + <amOnPage url="/$$createDefaultCategory.name$$/{{FirstLevelSubCat.name}}/{{SecondLevelSubCat.name}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> + <waitForPageLoad stepKey="waitForStoreFrontPageLoad"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="seeDefaultCategoryOnStoreNavigationBar"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="dontSeeSubCategoryOnStoreNavigationBar"/> + <!-- Verify breadcrumbs in store front page after clicking cancel button --> + <grabMultiple selector="{{StorefrontNavigationSection.categoryBreadcrumbs}}" stepKey="breadcrumbs"/> + <assertEquals stepKey="verifyTheCategoryInStoreFrontPage"> + <expectedResult type="array">['Home', $$createDefaultCategory.name$$,{{FirstLevelSubCat.name}},{{SecondLevelSubCat.name}}]</expectedResult> + <actualResult type="variable">breadcrumbs</actualResult> + </assertEquals> + + <!-- Move Category to another position in category tree and click ok button--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openTheAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitTillPageLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <dragAndDrop selector1="{{AdminCategorySidebarTreeSection.categoryInTree(SecondLevelSubCat.name)}}" selector2="{{AdminCategorySidebarTreeSection.categoryInTree('Default Category')}}" stepKey="DragCategory"/> + <see selector="{{AdminCategoryModalSection.message}}" userInput="This operation can take a long time" stepKey="seeWarningMessageForOneMoreTime"/> + <click selector="{{AdminCategoryModalSection.ok}}" stepKey="clickOkButtonOnWarningPopup"/> + <waitForPageLoad stepKey="waitTheForPageToLoad"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You moved the category." stepKey="seeSuccessMoveMessage"/> + <amOnPage url="/{{SimpleSubCategory.name}}.html" stepKey="seeCategoryNameInStoreFrontPage"/> + <waitForPageLoad stepKey="waitForStoreFrontPageToLoad"/> + <!-- Verify Category in store front after moving category to another position in category tree --> + <amOnPage url="{{StorefrontCategoryPage.url(SecondLevelSubCat.name)}}" stepKey="amOnCategoryPage"/> + <waitForPageLoad stepKey="waitForPageToBeLoaded"/> + <seeElement selector="{{StorefrontCategoryMainSection.CategoryTitle(SecondLevelSubCat.name)}}" stepKey="seeCategoryInTitle"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SecondLevelSubCat.name)}}" stepKey="seeCategoryOnStoreNavigationBarAfterMove"/> + <!-- Verify breadcrumbs in store front page after moving category to another position in category tree --> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(SecondLevelSubCat.name)}}" stepKey="clickCategoryOnNavigation"/> + <waitForPageLoad stepKey="waitForCategoryLoad"/> + <grabMultiple selector="{{StorefrontNavigationSection.categoryBreadcrumbs}}" stepKey="breadcrumbsAfterMove"/> + <assertEquals stepKey="verifyBreadcrumbsInFrontPageAfterMove"> + <expectedResult type="array">['Home',{{SecondLevelSubCat.name}}]</expectedResult> + <actualResult type="variable">breadcrumbsAfterMove</actualResult> + </assertEquals> + + <!-- Open Url Rewrite page and see the url rewrite for the moved category --> + <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="openUrlRewriteIndexPage"/> + <waitForPageLoad stepKey="waitForUrlRewritePageLoad"/> + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="{{SecondLevelSubCat.name_lwr}}.html" stepKey="fillCategoryUrlKey"/> + <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton"/> + <waitForPageLoad stepKey="waitForUrlPageToLoad"/> + <!-- Verify new Redirect Path after move --> + <see selector="{{AdminUrlRewriteIndexSection.requestPathColumn('2')}}" userInput="{{SecondLevelSubCat.name_lwr}}.html" stepKey="verifyTheRequestPathAfterMove"/> + <!-- Verify new Target Path after move --> + <see selector="{{AdminUrlRewriteIndexSection.targetPathColumn('2')}}" userInput="catalog/category/view/id/{$categoryId}" stepKey="verifyTheTargetPathAfterMove"/> + <!-- Verify new RedirectType after move --> + <see selector="{{AdminUrlRewriteIndexSection.redirectTypeColumn('2')}}" userInput="No" stepKey="verifyTheRedirectTypeAfterMove"/> + <!-- Verify before move Redirect Path displayed with associated Target Path and Redirect Type--> + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="{{SecondLevelSubCat.name_lwr}}" stepKey="fillTheCategoryUrlKey"/> + <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton2"/> + <waitForPageLoad stepKey="waitForSearch"/> + <see selector="{{AdminUrlRewriteIndexSection.redirectTypeColumn('1')}}" userInput="Permanent (301)" stepKey="verifyTheRedirectTypeBeforeMove"/> + <see selector="{{AdminUrlRewriteIndexSection.requestPathColumn('1')}}" userInput="{{_defaultCategory.name_lwr}}2/{{FirstLevelSubCat.name_lwr}}/{{SecondLevelSubCat.name_lwr}}.html" stepKey="verifyTheRequestPathBeforeMove"/> + <see selector="{{AdminUrlRewriteIndexSection.targetPathColumn('1')}}" userInput="{{SecondLevelSubCat.name_lwr}}.html" stepKey="verifyTheTargetPathBeforeMove"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml new file mode 100644 index 000000000000..bcd4ca853120 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml @@ -0,0 +1,183 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminNavigateMultipleUpSellProductsTest"> + <annotations> + <stories value="Up Sell products"/> + <title value="Promote Multiple Products (Simple, Configurable) as Up-Sell Products"/> + <description value="Login as admin and add simple and configurable Products as Up-Sell products"/> + <testCaseId value="MC-8902"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <!--Create Simple Products--> + <createData entity="SimpleSubCategory" stepKey="createCategory1"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory1"/> + </createData> + <createData entity="SimpleSubCategory" stepKey="createCategory2"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct1"> + <requiredEntity createDataKey="createCategory2"/> + </createData> + + <!-- Create the configurable product with product Attribute options--> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="delete"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + + <!--Login as admin--> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <!--Logout as admin--> + <actionGroup ref="logout" stepKey="logout"/> + + <!--Delete created data--> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createSimpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createCategory1" stepKey="deleteSubCategory1"/> + <deleteData createDataKey="createCategory2" stepKey="deleteCategory2"/> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deletecreateConfigChildProduct2"/> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deletecreateConfigChildProduct1"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + </after> + + <!--Open Product Index Page--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + + <!--Select SimpleProduct --> + <actionGroup ref="filterProductGridBySku" stepKey="findCreatedProduct"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <click stepKey="openFirstProduct" selector="{{AdminProductGridSection.productRowBySku($$createSimpleProduct.sku$$)}}"/> + <waitForPageLoad stepKey="waitForProductToLoad"/> + + <!--Add SimpleProduct1 and ConfigProduct as Up sell products--> + <click stepKey="clickOnRelatedProducts" selector="{{AdminProductFormRelatedUpSellCrossSellSection.relatedProductsHeader}}"/> + <click stepKey="clickOnAddUpSellProducts" selector="{{AdminProductFormRelatedUpSellCrossSellSection.addUpSellProduct}}"/> + <actionGroup ref="filterProductGridBySku2" stepKey="filterProduct"> + <argument name="sku" value="$$createSimpleProduct1.sku$$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForTheProductToLoad"/> + <checkOption selector="{{AdminAddProductsToGroupPanel.firstCheckbox}}" stepKey="selectTheSimpleProduct2"/> + <click stepKey="addSelectedProduct" selector="{{AdminAddRelatedProductsModalSection.AddUpSellProductsButton}}"/> + <waitForPageLoad stepKey="waitForProductToBeAdded"/> + <click stepKey="clickOnAddUpSellProductsButton" selector="{{AdminProductFormRelatedUpSellCrossSellSection.addUpSellProduct}}"/> + <actionGroup ref="filterProductGridBySku2" stepKey="filterConfigurableProduct"> + <argument name="sku" value="$$createConfigProduct.sku$$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForTheConfigProductToLoad"/> + <checkOption selector="{{AdminAddProductsToGroupPanel.firstCheckbox}}" stepKey="selectTheConfigProduct"/> + <click stepKey="addSelectedProductButton" selector="{{AdminAddRelatedProductsModalSection.AddUpSellProductsButton}}"/> + <waitForPageLoad stepKey="waitForConfigProductToBeAdded"/> + <click stepKey="clickOnRelatedProducts1" selector="{{AdminProductFormRelatedUpSellCrossSellSection.relatedProductsHeader}}"/> + <click stepKey="clickOnSaveButton" selector="{{AdminProductFormActionSection.saveButton}}"/> + <waitForPageLoad stepKey="waitForLoading1"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> + + <!--Go to Product Index Page --> + <click stepKey="clickOnBackButton" selector="{{AdminGridMainControls.back}}"/> + <waitForPageLoad stepKey="waitForProductsToBeLoaded"/> + + <!--Select Configurable Product--> + <actionGroup ref="filterProductGridBySku" stepKey="findConfigProduct"> + <argument name="product" value="$$createConfigProduct$$"/> + </actionGroup> + <click stepKey="openConfigProduct" selector="{{AdminProductGridSection.productRowBySku($$createConfigProduct.sku$$)}}"/> + <waitForPageLoad stepKey="waitForConfigProductToLoad"/> + + <!--Add SimpleProduct1 as Up Sell Product--> + <click stepKey="clickOnRelatedProductsHeader" selector="{{AdminProductFormRelatedUpSellCrossSellSection.relatedProductsHeader}}"/> + <click stepKey="clickOnAddUpSellProductsButton1" selector="{{AdminProductFormRelatedUpSellCrossSellSection.addUpSellProduct}}"/> + <actionGroup ref="filterProductGridBySku2" stepKey="filterSimpleProduct2"> + <argument name="sku" value="$$createSimpleProduct1.sku$$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForTheSimpleProduct2ToBeLoaded"/> + <checkOption selector="{{AdminAddProductsToGroupPanel.firstCheckbox}}" stepKey="selectSimpleProduct1"/> + <click stepKey="addSimpleProduct2" selector="{{AdminAddRelatedProductsModalSection.AddUpSellProductsButton}}"/> + <waitForPageLoad stepKey="waitForSimpleProductToBeAdded"/> + <scrollTo selector="{{AdminProductFormActionSection.saveButton}}" stepKey="scrollToTheSaveButton"/> + <click stepKey="clickOnSaveButton1" selector="{{AdminProductFormActionSection.saveButton}}"/> + <waitForPageLoad stepKey="waitForLoading2"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown1"/> + <waitForPageLoad stepKey="waitForUpdatesTobeSaved1"/> + + <!--Go to SimpleProduct store front page--> + <amOnPage url="$$createSimpleProduct.sku$$.html" stepKey="goToSimpleProductFrontPage"/> + <waitForPageLoad stepKey="waitForProduct"/> + <see stepKey="seeProductName" userInput="$$createSimpleProduct.sku$$" selector="{{StorefrontProductInfoMainSection.productName}}"/> + <scrollTo stepKey="scrollToTheUpSellHeading" selector="{{StorefrontProductUpSellProductsSection.upSellHeading}}"/> + + <!--Verify Up Sell Products displayed in SimpleProduct page--> + <see stepKey="seeTheUpSellHeading" selector="{{StorefrontProductUpSellProductsSection.upSellHeading}}" userInput="We found other products you might like!"/> + <see stepKey="seeSimpleProduct1" selector="{{StorefrontProductUpSellProductsSection.upSellProducts}}" userInput="$$createSimpleProduct1.name$$"/> + <see stepKey="seeConfigProduct" selector="{{StorefrontProductUpSellProductsSection.upSellProducts}}" userInput="$$createConfigProduct.name$$"/> + + <!--Go to Config Product store front page--> + <amOnPage url="$$createConfigProduct.sku$$.html" stepKey="goToConfigProductFrontPage"/> + <waitForPageLoad stepKey="waitForConfigProductToBeLoaded"/> + <scrollTo stepKey="scrollToTheUpSellHeading1" selector="{{StorefrontProductUpSellProductsSection.upSellHeading}}"/> + + <!--Verify Up Sell Products displayed in ConfigProduct page--> + <see stepKey="seeTheUpSellHeading1" selector="{{StorefrontProductUpSellProductsSection.upSellHeading}}" userInput="We found other products you might like!"/> + <see stepKey="seeSimpleProduct2" selector="{{StorefrontProductUpSellProductsSection.upSellProducts}}" userInput="$$createSimpleProduct1.name$$"/> + + <!--Go to SimpleProduct1 store front page--> + <amOnPage url="$$createSimpleProduct1.sku$$.html" stepKey="goToSimpleProduct1FrontPage"/> + <waitForPageLoad stepKey="waitForSimpleProduct1ToBeLoaded"/> + + <!--Verify No Up Sell Products displayed in SimplProduct1 page--> + <dontSee stepKey="dontSeeTheUpSellHeading1" selector="{{StorefrontProductUpSellProductsSection.upSellHeading}}" userInput="We found other products you might like!"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductImageAssignmentForMultipleStoresTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductImageAssignmentForMultipleStoresTest.xml new file mode 100644 index 000000000000..8149bc34087f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductImageAssignmentForMultipleStoresTest.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="AdminProductImageAssignmentForMultipleStoresTest"> + <annotations> + <features value="Catalog"/> + <stories value="Product image assignment for multiple stores"/> + <title value="Product image assignment for multiple stores"/> + <description value="Product image assignment for multiple stores"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-58718"/> + <group value="product"/> + <group value="WYSIWYGDisabled"/> + <skip> + <issueId value="MC-13841"/> + </skip> + </annotations> + <before> + <!-- Login Admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!-- Create Store View English --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreViewEn"> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + <!-- Create Store View France --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreViewFr"> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> + <!-- Create Category and Simple Product --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">100</field> + </createData> + </before> + <after> + <!-- Delete Store View English --> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + <!-- Delete Store View France --> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewFr"> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> + <!-- Clear Filter Store --> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="resetFiltersOnStorePage"/> + <!-- Delete Category and Simple Product --> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <!-- Clear Filter Product --> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductFilters"/> + <!-- Logout Admin --> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + <!-- Search Product and Open Edit --> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchProduct"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + + <!-- Switch to the English store view --> + <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="switchStoreViewEnglishProduct"> + <argument name="storeView" value="customStoreEN.name"/> + </actionGroup> + + <!-- Upload Image English --> + <actionGroup ref="addProductImage" stepKey="uploadImageEnglish"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct1"/> + + <!-- Switch to the French store view --> + <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="switchStoreViewFrenchProduct"> + <argument name="storeView" value="customStoreFR.name"/> + </actionGroup> + + <!-- Upload Image French --> + <actionGroup ref="addProductImage" stepKey="uploadImageFrench"> + <argument name="image" value="Magento3"/> + </actionGroup> + <actionGroup ref="AdminAssignImageRolesActionGroup" stepKey="assignImageRole1"> + <argument name="image" value="Magento3"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProduct2"/> + + <!-- Switch to the All store view --> + <actionGroup ref="AdminSwitchToAllStoreViewActionGroup" stepKey="switchAllStoreViewProduct"/> + + <!-- Upload Image All Store View --> + <actionGroup ref="addProductImage" stepKey="uploadImageAllStoreView"> + <argument name="image" value="TestImageNew"/> + </actionGroup> + <actionGroup ref="AdminAssignImageRolesActionGroup" stepKey="assignImageRole"> + <argument name="image" value="TestImageNew"/> + </actionGroup> + + <!-- Change any product data product description --> + <click selector="{{AdminProductContentSection.sectionHeader}}" stepKey="openDescriptionDropDown"/> + <fillField selector="{{AdminProductContentSection.descriptionTextArea}}" userInput="This is the long description" stepKey="fillLongDescription"/> + <fillField selector="{{AdminProductContentSection.shortDescriptionTextArea}}" userInput="This is the short description" stepKey="fillShortDescription"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + + <!-- Go to Product Page and see Default Store View--> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.custom_attributes[url_key]$$)}}" stepKey="goToDefaultStorefrontProductPage"/> + <waitForElementVisible selector="{{StorefrontProductMediaSection.productImageActive(TestImageNew.filename)}}" time="30" stepKey="waitImageToBeLoaded"/> + <seeElement selector="{{StorefrontProductMediaSection.productImageActive(TestImageNew.filename)}}" stepKey="seeActiveImageDefault"/> + + <!-- English Switch Store View and see English Store View --> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchStoreViewEnglish"> + <argument name="storeView" value="customStoreEN"/> + </actionGroup> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="openCategoryPage"/> + <waitForPageLoad time="30" stepKey="waitForCategoryPage"/> + <seeElement selector="{{StorefrontCategoryProductSection.ProductImageBySrc(ProductImage.fileName)}}" stepKey="seeThumb"/> + <click selector="{{StorefrontCategoryProductSection.ProductTitleByName($$createSimpleProduct.name$$)}}" stepKey="openProductPage"/> + <waitForPageLoad time="30" stepKey="waitForProductPage"/> + <waitForElementVisible selector="{{StorefrontProductMediaSection.productImageActive(ProductImage.filename)}}" time="30" stepKey="waitImageToBeLoaded2"/> + <seeElement selector="{{StorefrontProductMediaSection.productImageActive(ProductImage.filename)}}" stepKey="seeActiveImageEnglish"/> + + <!-- Switch France Store View and see France Store View --> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchStoreViewFrance"> + <argument name="storeView" value="customStoreFR"/> + </actionGroup> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="openCategoryPage1"/> + <waitForPageLoad time="30" stepKey="waitForCategoryPage1"/> + <seeElement selector="{{StorefrontCategoryProductSection.ProductImageBySrc(Magento3.fileName)}}" stepKey="seeThumb1"/> + <click selector="{{StorefrontCategoryProductSection.ProductTitleByName($$createSimpleProduct.name$$)}}" stepKey="openProductPage1"/> + <waitForPageLoad time="30" stepKey="waitForProductPage1"/> + <waitForElementVisible selector="{{StorefrontProductMediaSection.productImageActive(Magento3.filename)}}" time="30" stepKey="waitImageToBeLoaded3"/> + <seeElement selector="{{StorefrontProductMediaSection.productImageActive(Magento3.filename)}}" stepKey="seeActiveImageFrance"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultVideoSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultVideoSimpleProductTest.xml index 1bd218d18c27..876eedb9347c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultVideoSimpleProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultVideoSimpleProductTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminRemoveDefaultVideoSimpleProductTest"> <annotations> <features value="Catalog"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultVideoVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultVideoVirtualProductTest.xml index e6d3978cad7b..8b3b38d0ece3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultVideoVirtualProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultVideoVirtualProductTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminRemoveDefaultVideoVirtualProductTest" extends="AdminRemoveDefaultVideoSimpleProductTest"> <annotations> <features value="Catalog"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml new file mode 100644 index 000000000000..8316f54c15a5 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml @@ -0,0 +1,131 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminRemoveImageAffectsAllScopesTest"> + <annotations> + <features value="Catalog"/> + <stories value="MAGETWO-66442: Changes in default scope not effect product images in other scopes"/> + <title value="Effect of product images changes in default scope to other scopes"/> + <description value="Product image should be deleted from all scopes"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-94265"/> + <group value="Catalog"/> + </annotations> + <before> + <!--Create 2 websites (with stores, store views)--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="_defaultCategory" stepKey="category"/> + <createData entity="_defaultProduct" stepKey="product"> + <requiredEntity createDataKey="category"/> + </createData> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> + <argument name="newWebsiteName" value="FirstWebSite"/> + <argument name="websiteCode" value="FirstWebSiteCode"/> + </actionGroup> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStore" after="createWebsite"> + <argument name="website" value="FirstWebSite"/> + <argument name="storeGroupName" value="NewStore"/> + <argument name="storeGroupCode" value="Base1"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView" after="createNewStore"> + <argument name="StoreGroup" value="staticFirstStoreGroup"/> + <argument name="customStore" value="staticStore"/> + </actionGroup> + + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createSecondWebsite" after="createCustomStoreView"> + <argument name="newWebsiteName" value="SecondWebSite"/> + <argument name="websiteCode" value="SecondWebSiteCode"/> + </actionGroup> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createSecondStore" after="createSecondWebsite"> + <argument name="website" value="SecondWebSite"/> + <argument name="storeGroupName" value="SecondStore"/> + <argument name="storeGroupCode" value="Base2"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView2" after="createSecondStore"> + <argument name="StoreGroup" value="staticStoreGroup"/> + <argument name="customStore" value="staticSecondStore"/> + </actionGroup> + </before> + + <after> + <actionGroup ref="ResetWebUrlOptions" stepKey="resetUrlOption"/> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="FirstWebSite"/> + </actionGroup> + + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteSecondWebsite"> + <argument name="websiteName" value="SecondWebSite"/> + </actionGroup> + <deleteData createDataKey="category" stepKey="deletePreReqCategory"/> + <deleteData createDataKey="product" stepKey="deleteFirstProduct"/> + <magentoCLI stepKey="reindex" command="indexer:reindex"/> + <magentoCLI stepKey="flushCache" command="cache:flush"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create product--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="resetProductGridToDefaultView" stepKey="resetProductGridColumnsInitial"/> + + <!--Open created product--> + <click selector="{{AdminProductGridSection.productGridNameProduct($$product.name$$)}}" stepKey="createdProduct"/> + <waitForPageLoad stepKey="waitForOpenedCreatedProduct"/> + + <!-- Add image to product --> + <actionGroup ref="addProductImage" stepKey="addFirstImageForProduct"> + <argument name="image" value="TestImageNew"/> + </actionGroup> + + <!-- Add second image to product --> + <actionGroup ref="addProductImage" stepKey="addSecondImageForProduct"> + <argument name="image" value="MagentoLogo"/> + </actionGroup> + <!--"Product in Websites": select both Websites--> + <actionGroup ref="ProductSetWebsite" stepKey="ProductSetWebsite1"> + <argument name="website" value="FirstWebSite"/> + </actionGroup> + <actionGroup ref="ProductSetWebsite" stepKey="ProductSetWebsite2"> + <argument name="website" value="SecondWebSite"/> + </actionGroup> + + <!--Go to "Catalog" -> "Products". Open created product--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoaded"/> + <click selector="{{AdminProductGridSection.productGridNameProduct($$product.name$$)}}" stepKey="openCreatedProduct"/> + <waitForPageLoad stepKey="waitForCreatedProductOpened"/> + + <!--Delete Image 1--> + <actionGroup ref="removeProductImage" stepKey="removeProductImage"/> + + <!--Click "Save" in the upper right corner--> + <actionGroup ref="saveProductForm" stepKey="saveProductFormAfterRemove"/> + + <!--Switch to "Store view 1"--> + <actionGroup ref="SwitchToTheNewStoreView" stepKey="selectStoreView"> + <argument name="storeViewName" value="Store View"/> + </actionGroup> + + <!-- Assert product first image not in admin product form --> + <actionGroup ref="assertProductImageNotInAdminProductPage" stepKey="assertProductImageNotInAdminProductPage"> + <argument name="image" value="TestImageNew"/> + </actionGroup> + + <!--Switch to "Store view 2"--> + <actionGroup ref="SwitchToTheNewStoreView" stepKey="selectSecondStoreView"> + <argument name="storeViewName" value="Second Store View"/> + </actionGroup> + + <!-- Verify that Image 1 is deleted from the Second Store View list --> + <actionGroup ref="assertProductImageNotInAdminProductPage" stepKey="assertProductImageNotInSecondStoreViewPage"> + <argument name="image" value="TestImageNew"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.xml new file mode 100644 index 000000000000..234a7c69913c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.xml @@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminSortingByWebsitesTest"> + <annotations> + <stories value="View sorting by websites"/> + <title value="Sorting by websites in Admin"/> + <description value="Sorting products by websites in Admin"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="productAssignedToCustomWebsite"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="productAssignedToMainWebsite"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create new website --> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createAdditionalWebsite"> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> + </actionGroup> + <actionGroup ref="EnableWebUrlOptions" stepKey="addStoreCodeToUrls"/> + <magentoCLI command="cache:flush" stepKey="flushCacheAfterEnableWebUrlOptions"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="productAssignedToCustomWebsite" stepKey="deleteProductAssignedToCustomWebsite"/> + <deleteData createDataKey="productAssignedToMainWebsite" stepKey="deleteProductAssignedToMainWebsite"/> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteTestWebsite"> + <argument name="websiteName" value="{{customWebsite.name}}"/> + </actionGroup> + <actionGroup ref="ResetWebUrlOptions" stepKey="resetUrlOption"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Assign Custom Website to Simple Product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToCatalogProductGrid"/> + <waitForPageLoad stepKey="waitForCatalogProductGrid"/> + + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="assignCustomWebsiteToProduct"> + <argument name="product" value="$$productAssignedToCustomWebsite$$"/> + </actionGroup> + <scrollTo selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="scrollToWebsites"/> + <conditionalClick selector="{{ProductInWebsitesSection.sectionHeader}}" dependentSelector="{{AdminProductContentSection.sectionHeaderShow}}" visible="false" stepKey="expandSection"/> + <waitForPageLoad stepKey="waitForPageOpened"/> + <uncheckOption selector="{{ProductInWebsitesSection.website(_defaultWebsite.name)}}" stepKey="deselectMainWebsite"/> + <checkOption selector="{{ProductInWebsitesSection.website(customWebsite.name)}}" stepKey="selectWebsite"/> + + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave"/> + <waitForLoadingMaskToDisappear stepKey="waitForProductPageToSaveAgain"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessageAgain"/> + + <!--Navigate To Product Grid To Check Website Sorting--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToCatalogProductGridToSortByWebsite"/> + <waitForPageLoad stepKey="waitForCatalogProductGridLoaded"/> + + <!--Sorting works (By Websites) ASC--> + <click selector="{{AdminProductGridSection.columnHeader('Websites')}}" stepKey="clickWebsitesHeaderToSortAsc"/> + <see selector="{{AdminProductGridSection.productGridContentsOnRow('1')}}" userInput="Main Website" stepKey="checkIfProduct1WebsitesAsc"/> + + <!--Sorting works (By Websites) DESC--> + <click selector="{{AdminProductGridSection.columnHeader('Websites')}}" stepKey="clickWebsitesHeaderToSortDesc"/> + <see selector="{{AdminProductGridSection.productGridContentsOnRow('1')}}" userInput="{{customWebsite.name}}" stepKey="checkIfProduct1WebsitesDesc"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresAttributeSetNavigateMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresAttributeSetNavigateMenuTest.xml new file mode 100644 index 000000000000..ed29c281b804 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresAttributeSetNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminStoresAttributeSetNavigateMenuTest"> + <annotations> + <features value="Catalog"/> + <stories value="Menu Navigation"/> + <title value="Admin stores attribute set navigate menu test"/> + <description value="Admin should be able to navigate to Stores > Attribute Set"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14133"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToAttributeSetPage"> + <argument name="menuUiId" value="{{AdminMenuStores.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuStoresAttributesAttributeSet.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuStoresAttributesAttributeSet.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresProductNavigateMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresProductNavigateMenuTest.xml new file mode 100644 index 000000000000..28a33c4f20c0 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresProductNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminStoresProductNavigateMenuTest"> + <annotations> + <features value="Catalog"/> + <stories value="Menu Navigation"/> + <title value="Admin stores product navigate menu test"/> + <description value="Admin should be able to navigate to Stores > Product"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14132"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToProductAttributePage"> + <argument name="menuUiId" value="{{AdminMenuStores.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuStoresAttributesProduct.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuStoresAttributesProduct.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminTierPriceNotAvailableForProductOptionsWithoutTierPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminTierPriceNotAvailableForProductOptionsWithoutTierPriceTest.xml new file mode 100644 index 000000000000..51af9d78dfd4 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminTierPriceNotAvailableForProductOptionsWithoutTierPriceTest.xml @@ -0,0 +1,94 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminTierPriceNotAvailableForProductOptionsWithoutTierPriceTest"> + <annotations> + <features value="Catalog"/> + <title value="Check that 'tier price' block not available for simple product from options without 'tier price'"/> + <description value="Check that 'tier price' block not available for simple product from options without 'tier price'"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-97050"/> + <useCaseId value="MAGETWO-96842"/> + <group value="catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!--Create category--> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + + <!-- Create the configurable product based on the data in the /data folder --> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Make the configurable product have two options, that are children of the default attribute set --> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create the 2 children that will be a part of the configurable product --> + <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + + <!-- Assign the two products to the configurable product --> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + </before> + <after> + <!--Delete created data--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + + <!--Go to storefront product page an check price box css--> + <amOnPage url="{{StorefrontProductPage.url($$createConfigProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToSimpleProductPage"/> + <waitForPageLoad stepKey="waitForStoreFrontLoad"/> + <selectOption selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" userInput="$$getConfigAttributeOption1.value$$" stepKey="selectOption"/> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="class" stepKey="grabGrabPriceClass"/> + <assertNotContains actual="$grabGrabPriceClass" expected=".price-box .price-tier_price" expectedType="string" stepKey="assertNotEquals"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUnassignProductAttributeFromAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUnassignProductAttributeFromAttributeSetTest.xml index dc6cf012840e..a33c7bb12879 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUnassignProductAttributeFromAttributeSetTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUnassignProductAttributeFromAttributeSetTest.xml @@ -17,33 +17,25 @@ <severity value="CRITICAL"/> <testCaseId value="MC-194"/> <group value="Catalog"/> - <skip> - <issueId value="MAGETWO-92780"/> - </skip> </annotations> <before> <createData entity="productDropDownAttribute" stepKey="attribute"/> - <createData entity="productAttributeOption1" stepKey="option1"> <requiredEntity createDataKey="attribute"/> </createData> <createData entity="productAttributeOption2" stepKey="option2"> <requiredEntity createDataKey="attribute"/> </createData> - <createData entity="AddToDefaultSet" stepKey="addToDefaultSet"> <requiredEntity createDataKey="attribute"/> </createData> - <createData entity="ApiProductWithDescription" stepKey="product"/> - <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> </before> <after> <deleteData createDataKey="attribute" stepKey="deleteAttribute"/> <actionGroup ref="logout" stepKey="logout"/> </after> - <!-- Assert attribute presence in storefront product additional information --> <amOnPage url="/$$product.custom_attributes[url_key]$$.html" stepKey="onProductPage1"/> <waitForPageLoad stepKey="wait1"/> @@ -64,6 +56,8 @@ <see userInput="$$attribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.unassignedAttributesTree}}" stepKey="seeAttributeInUnassigned"/> <!-- Save attribute set --> <actionGroup ref="SaveAttributeSet" stepKey="SaveAttributeSet"/> + <!-- Clear cache --> + <actionGroup ref="ClearPageCacheActionGroup" stepKey="clearPageCacheActionGroup"/> <!-- Go to create new product page --> <amOnPage url="{{AdminProductCreatePage.url(AddToDefaultSet.attributeSetId, 'simple')}}" stepKey="navigateToNewProduct"/> <waitForPageLoad stepKey="wait2"/> @@ -72,8 +66,7 @@ <!-- Assert removed attribute not presence in storefront product additional information --> <amOnPage url="/$$product.custom_attributes[url_key]$$.html" stepKey="onProductPage2"/> <waitForPageLoad stepKey="wait3"/> - <actionGroup ref="checkAttributeNotInMoreInformationTab" stepKey="checkAttributeNotInMoreInformationTab"> - <argument name="attributeLabel" value="$$attribute.attribute[frontend_labels][0][label]$$"/> - </actionGroup> + <dontSeeElement selector="{{StorefrontProductMoreInformationSection.moreInformation}}" stepKey="dontSeeProductAttribute"/> + <dontSee userInput="$$attribute.attribute[frontend_labels][0][label]$$" selector="{{StorefrontProductMoreInformationSection.moreInformationTextArea}}" stepKey="dontSeeAttributeLabel"/> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml new file mode 100644 index 000000000000..d8d462f850f8 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml @@ -0,0 +1,77 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest"> + <annotations> + <stories value="Update categories"/> + <title value="Update category, check default URL key on the custom store view"/> + <description value="Login as admin and update category and check default URL Key on custom store view"/> + <testCaseId value="MC-6063"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="NewRootCategory" stepKey="rootCategory"/> + <createData entity="SimpleRootSubCategory" stepKey="category"> + <requiredEntity createDataKey="rootCategory"/> + </createData> + </before> + <after> + <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCustomStore"> + <argument name="storeGroupName" value="customStore.name"/> + </actionGroup> + <deleteData createDataKey="rootCategory" stepKey="deleteRootCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open Store Page --> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <waitForPageLoad stepKey="waitForSystemStorePage"/> + + <!--Create Custom Store --> + <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> + <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" stepKey="fillStoreName"/> + <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" stepKey="fillStoreCode"/> + <selectOption userInput="{{NewRootCategory.name}}" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectStoreStatus"/> + <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreButton"/> + + <!--Create Store View--> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> + <argument name="StoreGroup" value="customStore"/> + <argument name="customStore" value="customStore"/> + </actionGroup> + + <!--Update Category--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoaded"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleRootSubCategory.name)}}" stepKey="selectCategory"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="updateCategoryName"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveUpdatedCategory"/> + <waitForPageLoad stepKey="waitForCateforyToSave"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + <waitForPageLoad stepKey="waitForPageToLoad1"/> + <scrollTo selector="{{AdminCategorySEOSection.SectionHeader}}" x="0" y="-80" stepKey="scrollToSearchEngineOptimization1"/> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="selectSearchEngineOptimization1"/> + <waitForPageLoad stepKey="waitForPageToLoad2"/> + <seeInField selector="{{AdminCategorySEOSection.UrlKeyInput}}" stepKey="seeCategoryUrlKey" userInput="{{SimpleRootSubCategory.name_lwr}}2" /> + <!--Open Category in Store Front Page--> + <amOnPage url="/{{NewRootCategory.name}}/{{_defaultCategory.name}}.html" stepKey="seeTheCategoryInStoreFront"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> + <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="clickSwitchStoreButtonOnDefaultStore"/> + <click selector="{{StorefrontFooterSection.storeLink(customStore.name)}}" stepKey="selectSecondStoreToSwitchOn"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="seeUpdatedCatergoryInStoreFront"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="selectCategoryOnStoreFront"/> + <waitForPageLoad stepKey="waitForProductToLoad"/> + <seeElement selector="{{StorefrontCategoryMainSection.CategoryTitle(_defaultCategory.name)}}" stepKey="seeTheUpdatedCategoryTitle"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml new file mode 100644 index 000000000000..479249ca678d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateCategoryAndMakeInactiveTest"> + <annotations> + <stories value="Update categories"/> + <title value="Update category, make inactive"/> + <description value="Login as admin and update category and make it Inactive"/> + <testCaseId value="MC-6060"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="_defaultCategory" stepKey="createDefaultCategory"/> + </before> + <after> + <deleteData createDataKey="createDefaultCategory" stepKey="deleteCreatedCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open category page--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoaded"/> + + <!--Update category and make category inactive--> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCreatedCategory"/> + <click selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="disableCategory"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForCategorySaved"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the category." stepKey="assertSuccessMessage"/> + <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seePageTitle" /> + <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="dontCategoryIsChecked"/> + + <!--Verify Inactive Category is store front page--> + <amOnPage url="{{StorefrontCategoryPage.url(_defaultCategory.name)}}" stepKey="amOnCategoryPage"/> + <waitForPageLoad stepKey="waitForPageToBeLoaded"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="dontSeeCategoryOnStoreNavigationBar"/> + <waitForPageLoad time="15" stepKey="wait"/> + + <!--Verify Inactive Category in category page --> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage1"/> + <waitForPageLoad stepKey="waitForPageToLoaded1"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree1"/> + <seeElement selector="{{AdminCategoryContentSection.categoryInTree(_defaultCategory.name)}}" stepKey="assertCategoryInTree" /> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCreatedCategory1"/> + <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seeCategoryPageTitle1" /> + <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="assertCategoryIsInactive"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml new file mode 100644 index 000000000000..2cb4a6b6dd43 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml @@ -0,0 +1,82 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateCategoryNameWithStoreViewTest"> + <annotations> + <stories value="Update categories"/> + <title value="Update category, with custom store view"/> + <description value="Login as admin and update category name with custom Store View"/> + <testCaseId value="MC-6061"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="NewRootCategory" stepKey="rootCategory"/> + <createData entity="SimpleRootSubCategory" stepKey="category"> + <requiredEntity createDataKey="rootCategory"/> + </createData> + </before> + <after> + <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCustomStore"> + <argument name="storeGroupName" value="customStore.name"/> + </actionGroup> + <deleteData createDataKey="rootCategory" stepKey="deleteRootCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open store page --> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <waitForPageLoad stepKey="waitForSystemStorePage"/> + + <!--Create Custom Store --> + <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> + <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" stepKey="fillStoreName"/> + <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" stepKey="fillStoreCode"/> + <selectOption userInput="{{NewRootCategory.name}}" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectStoreStatus"/> + <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreButton"/> + + <!--Create Store View--> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> + <argument name="StoreGroup" value="customStore"/> + <argument name="customStore" value="customStore"/> + </actionGroup> + + <!--Verify created SubCAtegory is present on Store Front --> + <amOnPage url="/{{NewRootCategory.name}}/{{SimpleRootSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFront"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="ClickSwitchStoreButtonOnDefaultStore"/> + <click selector="{{StorefrontFooterSection.storeLink(customStore.name)}}" stepKey="SelectSecondStoreToSwitchOn"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="seeCatergoryInStoreFront"/> + + <!--Open Category Page--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoaded"/> + + <!--Update Category--> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTreeUnderRoot(SimpleRootSubCategory.name)}}" stepKey="clickOnSubcategoryIsUndeRootCategory"/> + <waitForPageLoad stepKey="waitForPageToLoad1"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="updateCategoryName"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveUpdatedCategory"/> + <waitForPageLoad stepKey="waitForCateforyToSave"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + + <!--Verify the Category is not present in Store Front--> + <amOnPage url="/{{NewRootCategory.name}}/{{SimpleRootSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFront1"/> + <waitForPageLoad stepKey="waitForPageToLoaded2"/> + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="dontSeeCatergoryInStoreFront"/> + + <!--Verify the Updated Category is present in Store Front--> + <amOnPage url="/{{NewRootCategory.name}}/{{_defaultCategory.name}}.html" stepKey="seeTheUpdatedCategoryInStoreFront"/> + <waitForPageLoad stepKey="waitForPageToLoaded3"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="seeUpdatedCatergoryInStoreFront"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryUrlKeyWithStoreViewTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryUrlKeyWithStoreViewTest.xml new file mode 100644 index 000000000000..e7c4a8a093e1 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryUrlKeyWithStoreViewTest.xml @@ -0,0 +1,84 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateCategoryUrlKeyWithStoreViewTest"> + <annotations> + <stories value="Update categories"/> + <title value="Update category, URL key with custom store view"/> + <description value="Login as admin and update category URL Key with store view"/> + <testCaseId value="MC-6062"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="NewRootCategory" stepKey="rootCategory"/> + <createData entity="SimpleRootSubCategory" stepKey="category"> + <requiredEntity createDataKey="rootCategory"/> + </createData> + </before> + <after> + <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCustomStore"> + <argument name="storeGroupName" value="customStore.name"/> + </actionGroup> + <deleteData createDataKey="rootCategory" stepKey="deleteRootCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open Store Page --> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <waitForPageLoad stepKey="waitForSystemStorePage"/> + + <!--Create Custom Store --> + <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> + <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" stepKey="fillStoreName"/> + <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" stepKey="fillStoreCode"/> + <selectOption userInput="{{NewRootCategory.name}}" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectStoreStatus"/> + <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreButton"/> + + <!--Create Store View--> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> + <argument name="StoreGroup" value="customStore"/> + <argument name="customStore" value="customStore"/> + </actionGroup> + + <!--Verify Category in Store View--> + <amOnPage url="/{{NewRootCategory.name}}/{{SimpleRootSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFront"/> + <waitForPageLoad stepKey="waitForSystemStorePage1"/> + <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="ClickSwitchStoreButtonOnDefaultStore"/> + <click selector="{{StorefrontFooterSection.storeLink(customStore.name)}}" stepKey="SelectSecondStoreToSwitchOn"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="seeCatergoryInStoreFront"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="selectCategory"/> + <waitForPageLoad stepKey="waitForProductToLoad"/> + + <!--Update URL Key--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoaded2"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleRootSubCategory.name)}}" stepKey="selectCategory1"/> + <scrollTo selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="scrollToSearchEngineOptimization"/> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSeoSection"/> + <clearField selector="{{AdminCategorySEOSection.UrlKeyInput}}" stepKey="clearUrlKeyField"/> + <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="newurlkey" stepKey="enterURLKey"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategoryAfterFirstSeoUpdate"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccessMessage"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + + <!--Open Category Store Front Page--> + <amOnPage url="/{{NewRootCategory.name}}/{{SimpleRootSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFront1"/> + <waitForPageLoad stepKey="waitForSystemStorePage3"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="seeCategoryOnNavigation1"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="selectCategory2"/> + <waitForPageLoad stepKey="waitForProductToLoad1"/> + + <!--Verify Updated URLKey is present--> + <seeInCurrentUrl stepKey="verifyUpdatedUrlKey" url="newurlkey.html"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithInactiveIncludeInMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithInactiveIncludeInMenuTest.xml new file mode 100644 index 000000000000..3fea9c0eed7c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithInactiveIncludeInMenuTest.xml @@ -0,0 +1,83 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateCategoryWithInactiveIncludeInMenuTest"> + <annotations> + <stories value="Update categories"/> + <title value="Update category, name description urlkey metatitle exclude from menu"/> + <description value="Login as admin and update category name, description, urlKey, metatitle and exclude from menu"/> + <testCaseId value="MC-6058"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + <group value="WYSIWYGDisabled"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="_defaultCategory" stepKey="createDefaultCategory"/> + </before> + <after> + <deleteData createDataKey="createDefaultCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open Category Page--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoaded"/> + + <!--Update Category name,description, urlKey, meta title and disable Include in Menu--> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCreatedCategory"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleRootSubCategory.name}}" stepKey="fillCategoryName"/> + <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> + <click selector="{{AdminCategoryBasicFieldSection.includeInMenuLabel}}" stepKey="disableIncludeInMenu"/> + <scrollTo selector="{{AdminCategoryContentSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToContent"/> + <click selector="{{AdminCategoryContentSection.sectionHeader}}" stepKey="selectContent"/> + <fillField selector="{{AdminCategoryContentSection.description}}" userInput="Updated category Description Fields" stepKey="fillUpdatedDescription"/> + <scrollTo selector="{{AdminCategorySEOSection.SectionHeader}}" x="0" y="-80" stepKey="scrollToSearchEngineOptimization"/> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="selectSearchEngineOptimization"/> + <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="{{SimpleRootSubCategory.url_key}}" stepKey="fillUpdatedUrlKey"/> + <fillField selector="{{AdminCategorySEOSection.MetaTitleInput}}" userInput="{{SimpleRootSubCategory.name}}" stepKey="fillUpdatedMetaTitle"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForCategorySaved"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the category." stepKey="assertSuccessMessage"/> + + <!--Open UrlRewrite Page--> + <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="openUrlRewriteIndexPage"/> + <waitForPageLoad stepKey="waitForUrlRewritePage"/> + + <!--Verify Updated Category UrlKey--> + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="{{SimpleRootSubCategory.url_key}}" stepKey="fillUpdatedCategoryUrlKey"/> + <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <see stepKey="seeCategoryUrlKey" selector="{{AdminUrlRewriteIndexSection.requestPathColumn('1')}}" userInput="{{SimpleRootSubCategory.url_key}}.html" /> + <!--Verify Updated Category UrlKey directs to category Store Front--> + <amOnPage url="{{SimpleRootSubCategory.url_key}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> + <waitForPageLoad time="60" stepKey="waitForStoreFrontPageLoad"/> + <seeElement selector="{{StorefrontCategoryMainSection.CategoryTitle(SimpleRootSubCategory.name)}}" stepKey="seeUpdatedCategoryInStoreFrontPage"/> + + <!--Verify Updated fields in Category Page--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage1"/> + <waitForPageLoad stepKey="waitForPageToLoaded1"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree1"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleRootSubCategory.name)}}" stepKey="selectCreatedCategory1"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> + <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{SimpleRootSubCategory.name}}" stepKey="seeUpdatedCategoryTitle"/> + <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.includeInMenuLabel}}" stepKey="verifyInactiveIncludeInMenu"/> + <seeInField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleRootSubCategory.name}}" stepKey="seeUpdatedCategoryName"/> + <scrollTo selector="{{AdminCategoryContentSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToContent1"/> + <click selector="{{AdminCategoryContentSection.sectionHeader}}" stepKey="selectContent1"/> + <scrollTo selector="{{AdminCategoryContentSection.description}}" stepKey="scrollToDescription1"/> + <seeInField stepKey="seeUpdatedDiscription" selector="{{AdminCategoryContentSection.description}}" userInput="Updated category Description Fields"/> + <scrollTo selector="{{AdminCategorySEOSection.SectionHeader}}" x="0" y="-80" stepKey="scrollToSearchEngineOptimization1"/> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="selectSearchEngineOptimization1"/> + <seeInField stepKey="seeUpdatedUrlKey" selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="{{SimpleRootSubCategory.url_key}}"/> + <seeInField stepKey="seeUpdatedMetaTitleInput" selector="{{AdminCategorySEOSection.MetaTitleInput}}" userInput="{{SimpleRootSubCategory.name}}"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsTest.xml new file mode 100644 index 000000000000..1cb01ac11cb8 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsTest.xml @@ -0,0 +1,84 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateCategoryWithProductsTest"> + <annotations> + <stories value="Update categories"/> + <title value="Update category, sort products by default sorting"/> + <description value="Login as admin, update category and sort products"/> + <testCaseId value="MC-6059"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <createData entity="defaultSimpleProduct" stepKey="simpleProduct" /> + <createData entity="_defaultCategory" stepKey="createCategory"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="simpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open Category Page--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoaded"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCreatedCategory"/> + <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> + + <!--Update Product Display Setting--> + <scrollTo selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" x="0" y="-80" stepKey="scrollToDisplaySetting"/> + <click selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" stepKey="selectDisplaySetting"/> + <scrollToTopOfPage stepKey="scfrollToTop"/> + <click selector="{{CategoryDisplaySettingsSection.productListCheckBox}}" stepKey="enableTheAvailableProductList"/> + <selectOption selector="{{CategoryDisplaySettingsSection.productList}}" parameterArray="['Product Name', 'Price']" stepKey="selectPrice"/> + <scrollTo selector="{{CategoryDisplaySettingsSection.defaultProductLisCheckBox}}" x="0" y="-80" stepKey="scrollToDefaultProductList"/> + <click selector="{{CategoryDisplaySettingsSection.defaultProductLisCheckBox}}" stepKey="enableTheDefaultProductList"/> + <selectOption selector="{{CategoryDisplaySettingsSection.defaultProductList}}" userInput="name" stepKey="selectProductName"/> + + <!--Add Products in Category--> + <scrollTo selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" x="0" y="-80" stepKey="scrollToProductInCategory"/> + <click selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" stepKey="clickOnProductInCategory"/> + <scrollToTopOfPage stepKey="scrollOnTopOfPage"/> + <click selector="{{CatalogProductsSection.resetFilter}}" stepKey="clickOnResetFilter"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <fillField selector="{{AdminCategoryContentSection.productTableColumnName}}" userInput="$$simpleProduct.name$$" stepKey="selectProduct1"/> + <click selector="{{AdminCategoryContentSection.productSearch}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitFroPageToLoad1"/> + <scrollTo selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="scrollToTableRow"/> + <click selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="selectProduct1FromTableRow"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForCategorySaved"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the category." stepKey="assertSuccessMessage"/> + <waitForPageLoad stepKey="waitForPageTitleToBeSaved"/> + + <!--Verify Category Title--> + <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seePageTitle" /> + + <!--Verify Category in store front page--> + <amOnPage url="{{StorefrontCategoryPage.url(_defaultCategory.name)}}" stepKey="seeDefaultProductPage"/> + <waitForPageLoad stepKey="waitForPageToBeLoaded"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="seeCategoryOnNavigation"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="selectCategory"/> + <waitForPageLoad stepKey="waitForProductToLoad"/> + + <!--Verify Product in Category--> + <seeElement stepKey="seeProductsInCategory" selector="{{StorefrontCategoryMainSection.productLink}}"/> + <click selector="{{StorefrontCategoryMainSection.productLink}}" stepKey="openSearchedProduct"/> + <waitForPageLoad stepKey="waitForProductToLoad1"/> + + <!--Verify product name and price on Store Front--> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{defaultSimpleProduct.name}}" stepKey="assertProductName"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{defaultSimpleProduct.price}}" stepKey="assertProductPrice"/> + </test> +</tests> + diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryAndAddProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryAndAddProductsTest.xml new file mode 100644 index 000000000000..8872ea98eb50 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryAndAddProductsTest.xml @@ -0,0 +1,103 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateFlatCategoryAndAddProductsTest"> + <annotations> + <stories value="Update category"/> + <title value="Flat Catalog - Assign Simple Product to Category"/> + <description value="Login as admin, update flat category by adding a simple product"/> + <testCaseId value="MC-11012"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <!-- Create Simple Product --> + <createData entity="SimpleSubCategory" stepKey="category"/> + <!-- Create category --> + <createData entity="defaultSimpleProduct" stepKey="createSimpleProduct"/> + <!-- Create First StoreView --> + <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewEn"> + <argument name="storeView" value="customStoreEN"/> + </actionGroup> + <!-- Create Second StoreView --> + <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewFr"> + <argument name="storeView" value="customStoreFR"/> + </actionGroup> + <!--Run full reindex and clear caches --> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <!--Enable Flat Catalog Category --> + <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 1"/> + <!--Open Index Management Page and Select Index mode "Update by Schedule" --> + <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + </before> + <after> + <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> + <magentoCLI stepKey="setIndexersMode" command="indexer:set-mode" arguments="realtime" /> + <magentoCLI stepKey="indexerReindex" command="indexer:reindex" /> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewFr"> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Select Created Category--> + <magentoCLI command="indexer:reindex" stepKey="reindexBeforeFlow"/> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="selectCreatedCategory"/> + <waitForPageLoad stepKey="waitForTheCategoryPageToLoaded"/> + <!--Add Products in Category--> + <scrollTo selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" x="0" y="-80" stepKey="scrollToProductInCategory"/> + <click selector="{{AdminCategoryBasicFieldSection.productsInCategory}}" stepKey="clickOnProductInCategory"/> + <scrollToTopOfPage stepKey="scrollOnTopOfPage"/> + <conditionalClick selector="{{CatalogProductsSection.resetFilter}}" dependentSelector="{{CatalogProductsSection.resetFilter}}" visible="true" stepKey="clickOnResetFilter"/> + <waitForPageLoad stepKey="waitForProductsToLoad"/> + <fillField selector="{{AdminCategoryContentSection.productTableColumnName}}" userInput="$$createSimpleProduct.name$$" stepKey="selectProduct"/> + <click selector="{{AdminCategoryContentSection.productSearch}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitFroPageToLoad1"/> + <scrollTo selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="scrollToTableRow"/> + <click selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="selectProductFromTableRow"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> + <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + <!--Open Index Management Page and verify flat categoryIndex status--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!--Open Index Management Page --> + <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> + <waitForPageLoad stepKey="waitForIndexPageToLoad"/> + <see stepKey="seeCategoryIndexStatus" selector="{{AdminIndexManagementSection.indexerStatus('Category Flat Data')}}" userInput="Ready"/> + <!--Verify Product In Store Front--> + <amOnPage url="$$createSimpleProduct.name$$.html" stepKey="goToStorefrontPage"/> + <waitForPageLoad stepKey="waitForPageToBeLoaded"/> + <!--Verify product and category is visible in First Store View --> + <click stepKey="selectStoreSwitcher" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> + <click stepKey="selectFirstStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreEN.name)}}"/> + <waitForPageLoad stepKey="waitForFirstStoreView"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$category.name$$)}}" stepKey="seeCategoryOnNavigation"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{defaultSimpleProduct.name}}" stepKey="assertProductName"/> + <!--Verify product and category is visible in Second Store View --> + <click stepKey="selectStoreSwitcher1" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> + <click stepKey="selectSecondStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreFR.name)}}"/> + <waitForPageLoad stepKey="waitForSecondStoreView"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$category.name$$)}}" stepKey="seeCategoryOnNavigation1"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{defaultSimpleProduct.name}}" stepKey="seeProductName"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml new file mode 100644 index 000000000000..552730337062 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml @@ -0,0 +1,91 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateFlatCategoryAndIncludeInMenuTest"> + <annotations> + <stories value="Update category"/> + <title value="Flat Catalog - Update Category, Include in Navigation Menu"/> + <description value="Login as admin and update flat category by enabling Include in Menu"/> + <testCaseId value="MC-11011"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <!--Create category--> + <createData entity="CatNotIncludeInMenu" stepKey="createCategory"/> + <!-- Create First StoreView --> + <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewEn"> + <argument name="storeView" value="customStoreEN"/> + </actionGroup> + <!-- Create Second StoreView --> + <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewFr"> + <argument name="storeView" value="customStoreFR"/> + </actionGroup> + <!--Run full reindex and clear caches --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!--Enable Flat Catalog Category --> + <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 1"/> + <!--Open Index Management Page and Select Index mode "Update by Schedule" --> + <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + </before> + <after> + <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> + <magentoCLI stepKey="setIndexersMode" command="indexer:set-mode" arguments="realtime" /> + <magentoCLI stepKey="indexerReindex" command="indexer:reindex" /> + <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewFr"> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Verify Category is not listed in navigation menu--> + <amOnPage url="/{{CatNotIncludeInMenu.name_lwr}}.html" stepKey="openCategoryPage"/> + <waitForPageLoad time="60" stepKey="waitForPageToBeLoaded"/> + <dontSee selector="{{StorefrontHeaderSection.NavigationCategoryByName(CatNotIncludeInMenu.name)}}" stepKey="dontSeeCategoryOnNavigation"/> + <!-- Select created category and enable Include In Menu option--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(CatNotIncludeInMenu.name)}}" stepKey="selectCreatedCategory"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> + <click selector="{{AdminCategoryBasicFieldSection.includeInMenuLabel}}" stepKey="enableIncludeInMenuOption"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> + <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + <!--Run full reindex and clear caches --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!--Open Index Management Page --> + <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> + <waitForPageLoad stepKey="waitForIndexPageToBeLoaded"/> + <see stepKey="seeIndexStatus" selector="{{AdminIndexManagementSection.indexerStatus('Category Flat Data')}}" userInput="Ready"/> + <!--Verify Category In Store Front--> + <amOnPage url="/$$createCategory.name$$.html" stepKey="openCategoryPage1"/> + <waitForPageLoad stepKey="waitForCategoryStoreFrontPageToLoad"/> + <!--Verify category is visible in First Store View --> + <click stepKey="selectStoreSwitcher" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> + <click stepKey="selectForstStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreEN.name)}}"/> + <waitForPageLoad stepKey="waitForFirstStoreView"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="seeCategoryOnNavigation"/> + <!--Verify category is visible in Second Store View --> + <click stepKey="selectStoreSwitcher1" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> + <click stepKey="selectSecondStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreFR.name)}}"/> + <waitForPageLoad stepKey="waitForSecondstoreView"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="seeCategoryOnNavigation1"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryNameAndDescriptionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryNameAndDescriptionTest.xml new file mode 100644 index 000000000000..fcbc0cb20526 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryNameAndDescriptionTest.xml @@ -0,0 +1,102 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateFlatCategoryNameAndDescriptionTest"> + <annotations> + <stories value="Update category"/> + <title value="Flat Catalog - Update Category Name and Description"/> + <description value="Login as admin and update flat category name and description"/> + <testCaseId value="MC-11010"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + <group value="WYSIWYGDisabled"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + <!--Create category--> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <!-- Create First StoreView --> + <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewEn"> + <argument name="storeView" value="customStoreEN"/> + </actionGroup> + <!-- Create Second StoreView --> + <actionGroup ref="CreateStoreView" stepKey="createCustomStoreViewFr"> + <argument name="storeView" value="customStoreFR"/> + </actionGroup> + <!--Run full reindex and clear caches --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!--Enable Flat Catalog Category --> + <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 1"/> + <!--Open Index Management Page and Select Index mode "Update by Schedule" --> + <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + </before> + <after> + <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> + <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="realtime" /> + <magentoCLI stepKey="indexerReindex" command="indexer:reindex" /> + <deleteData stepKey="deleteCategory" createDataKey="createCategory" /> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewFr"> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Select Created Category--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCreatedCategory"/> + <waitForPageLoad stepKey="waitForPageToLoaded"/> + <!--Update Category Name and Description --> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName"/> + <scrollTo selector="{{AdminCategoryContentSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToContent"/> + <click selector="{{AdminCategoryContentSection.sectionHeader}}" stepKey="selectContent"/> + <fillField selector="{{AdminCategoryContentSection.description}}" userInput="Updated category Description Fields" stepKey="fillUpdatedDescription"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> + <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + <!--Run full reindex and clear caches --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!--Open Index Management Page --> + <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> + <waitForPageLoad stepKey="waitForIndexPageToLoad"/> + <see stepKey="seeIndexStatus" selector="{{AdminIndexManagementSection.indexerStatus('Category Flat Data')}}" userInput="READY"/> + <!--Verify Category In Store Front--> + <amOnPage url="{{SimpleSubCategory.name}}.html" stepKey="goToStorefrontPage"/> + <waitForPageLoad stepKey="waitForPageToBeLoaded"/> + <!--Verify category is visible in First Store View --> + <click stepKey="selectStoreSwitcher" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> + <click stepKey="selectFirstStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreEN.name)}}"/> + <waitForPageLoad stepKey="waitForFirstStoreView"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="seeCategoryOnNavigation"/> + <!--Verify category is visible in Second Store View --> + <click stepKey="selectStoreSwitcher1" selector="{{StorefrontHeaderSection.storeViewSwitcher}}"/> + <click stepKey="selectSecondStoreView" selector="{{StorefrontHeaderSection.storeViewList(customStoreFR.name)}}"/> + <waitForPageLoad stepKey="waitForSecondStoreView"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="seeCategoryOnNavigation1"/> + <!-- Verify Updated Category Name and description on Category Page--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage1"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree1"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="selectUpdatedCategory"/> + <waitForPageLoad stepKey="waitForUpdatedCategoryPageToLoad"/> + <seeInField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="seeUpdatedSubCategoryName"/> + <scrollTo selector="{{AdminCategoryContentSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToContent1"/> + <click selector="{{AdminCategoryContentSection.sectionHeader}}" stepKey="selectContent1"/> + <seeInField selector="{{AdminCategoryContentSection.description}}" userInput="Updated category Description Fields" stepKey="seeUpdatedDescription"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml new file mode 100644 index 000000000000..18e4ff9ee2c9 --- /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 000000000000..d5fc981b5b2e --- /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 000000000000..2c3aa5db7517 --- /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 000000000000..6e8f1ba6f12a --- /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 000000000000..a042c4d60ae4 --- /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 000000000000..d08ef9c93999 --- /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 000000000000..3433a0911732 --- /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 000000000000..a695982921cf --- /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 000000000000..ba52c6d2bc26 --- /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 000000000000..cb5c24839e38 --- /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 000000000000..318ab6555235 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml @@ -0,0 +1,159 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest"> + <annotations> + <stories value="Update Simple Product"/> + <title value="Update Simple Product with Regular Price (In Stock) with Custom Options"/> + <description value="Test log in to Update Simple Product and Update Simple Product with Regular Price (In Stock) with Custom Options"/> + <testCaseId value="MC-10819"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> + <createData entity="defaultSimpleProduct" stepKey="initialSimpleProduct"> + <requiredEntity createDataKey="initialCategoryEntity"/> + </createData> + <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> + <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> + <actionGroup ref="deleteProductBySku" stepKey="deleteCreatedProduct"> + <argument name="sku" value="{{simpleProductRegularPriceCustomOptions.sku}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search default simple product in the grid --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGrid"> + <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + </actionGroup> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + + <!-- Update simple product with regular price --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductRegularPriceCustomOptions.name}}" stepKey="fillSimpleProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductRegularPriceCustomOptions.sku}}" stepKey="fillSimpleProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductRegularPriceCustomOptions.price}}" stepKey="fillSimpleProductPrice"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductRegularPriceCustomOptions.quantity}}" stepKey="fillSimpleProductQuantity"/> + <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{simpleProductRegularPriceCustomOptions.status}}" stepKey="selectStockStatusInStock"/> + <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductRegularPriceCustomOptions.weight}}" stepKey="fillSimpleProductWeight"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory"/> + <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory"/> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPriceCustomOptions.urlKey}}" stepKey="fillUrlKey"/> + <click selector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" stepKey="clickAdminProductCustomizableOption"/> + + <!-- Create simple product with customizable option --> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButton"/> + <waitForPageLoad stepKey="waitForDataToLoad"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('0')}}" userInput="{{simpleProductCustomizableOption.title}}" stepKey="fillOptionTitle"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('1')}}" stepKey="selectOptionTypeDropDown"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('1', simpleProductCustomizableOption.type)}}" stepKey="selectOptionFieldFromDropDown"/> + <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('0')}}" stepKey="checkRequiredCheckBox"/> + <click selector="{{AdminProductCustomizableOptionsSection.addValue}}" stepKey="clickAddValueButton"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(simpleProductCustomizableOption.title,'0')}}" userInput="{{simpleProductCustomizableOption.option_0_title}}" stepKey="fillOptionTitleForCustomizableOption"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(simpleProductCustomizableOption.title,'0')}}" userInput="{{simpleProductCustomizableOption.option_0_price}}" stepKey="fillOptionPrice"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(simpleProductCustomizableOption.title,'0')}}" userInput="{{simpleProductCustomizableOption.option_0_price_type}}" stepKey="selectOptionPriceType"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(simpleProductCustomizableOption.title,'0')}}" userInput="{{simpleProductCustomizableOption.option_0_sku}}" stepKey="fillOptionSku"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForSimpleProductSave"/> + + <!--Verify customer see success message--> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> + + <!--Search updated simple product(from above step) in the grid page--> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> + <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductRegularPriceCustomOptions.name}}" stepKey="fillSimpleProductNameInNameFilter"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{simpleProductRegularPriceCustomOptions.sku}}" stepKey="fillProductSku"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedSimpleProductVisibleInGrid"/> + <waitForPageLoad stepKey="waitUntilSimpleProductPageIsOpened"/> + + <!-- Verify customer see updated simple product in the product form page --> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductRegularPriceCustomOptions.name}}" stepKey="seeSimpleProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductRegularPriceCustomOptions.sku}}" stepKey="seeSimpleProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductRegularPriceCustomOptions.price}}" stepKey="seeSimpleProductPrice"/> + <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductRegularPriceCustomOptions.quantity}}" stepKey="seeSimpleProductQuantity"/> + <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{simpleProductRegularPriceCustomOptions.status}}" stepKey="seeSimpleProductStockStatus"/> + <seeInField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductRegularPriceCustomOptions.weight}}" stepKey="seeSimpleProductWeight"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> + <see selector="{{AdminProductFormSection.selectMultipleCategories}}" userInput="$$categoryEntity.name$$" stepKey="selectedCategories" /> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> + <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPriceCustomOptions.urlKey}}" stepKey="seeUrlKey"/> + <click selector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" stepKey="clickAdminProductCustomizableOptionToSeeValues"/> + + <!-- Verify simple product with customizable options --> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButtonForCustomizableOption"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('0')}}" userInput="{{simpleProductCustomizableOption.title}}" stepKey="seeOptionTitleForCustomizableOption"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('1')}}" stepKey="selectOptionTypeDropDownForCustomizableOption"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('1', simpleProductCustomizableOption.type)}}" stepKey="selectOptionFieldFromDropDownForCustomizableOption"/> + <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('0')}}" stepKey="checkRequiredCheckBoxForTheThirdDataSet"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(simpleProductCustomizableOption.title,'0')}}" userInput="{{simpleProductCustomizableOption.option_0_title}}" stepKey="seeOptionTitle"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(simpleProductCustomizableOption.title,'0')}}" userInput="{{simpleProductCustomizableOption.option_0_price}}" stepKey="seeOptionPrice"/> + <seeOptionIsSelected selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(simpleProductCustomizableOption.title,'0')}}" userInput="{{simpleProductCustomizableOption.option_0_price_type}}" stepKey="selectOptionValuePriceType"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(simpleProductCustomizableOption.title,'0')}}" userInput="{{simpleProductCustomizableOption.option_0_sku}}" stepKey="seeOptionSku"/> + + <!-- Verify customer see updated simple product (from the above step) on the storefront page --> + <amOnPage url="{{StorefrontProductPage.url(simpleProductRegularPriceCustomOptions.urlKey)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{simpleProductRegularPriceCustomOptions.name}}" stepKey="seeSimpleProductNameOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{simpleProductRegularPriceCustomOptions.price}}" stepKey="seeSimpleProductPriceOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{simpleProductRegularPriceCustomOptions.sku}}" stepKey="seeSimpleProductSkuOnStoreFrontPage"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> + <assertEquals stepKey="assertStockAvailableOnProductPage"> + <expectedResult type="string">{{simpleProductRegularPriceCustomOptions.storefrontStatus}}</expectedResult> + <actualResult type="variable">productStockAvailableStatus</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">${{simpleProductRegularPriceCustomOptions.price}}</expectedResult> + <actualResult type="variable">productPriceAmount</actualResult> + </assertEquals> + + <!--Verify customer see customizable options are Required --> + <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomSelect(simpleProductCustomizableOption.title)}}" stepKey="verifyFirstCustomOptionIsRequired"/> + + <!--Verify customer see customizable option titles and prices --> + <grabAttributeFrom userInput="for" selector="{{StorefrontProductInfoMainSection.customOptionLabel(simpleProductCustomizableOption.title)}}" stepKey="simpleOptionId"/> + <grabMultiple selector="{{StorefrontProductInfoMainSection.customSelectOptions({$simpleOptionId})}}" stepKey="grabFourthOptions"/> + <assertEquals stepKey="assertFourthSelectOptions"> + <actualResult type="variable">grabFourthOptions</actualResult> + <expectedResult type="array">['-- Please Select --', {{simpleProductCustomizableOption.option_0_title}} +$98.00]</expectedResult> + </assertEquals> + + <!-- Verify added Product in cart --> + <selectOption selector="{{StorefrontProductPageSection.customOptionDropDown}}" userInput="{{simpleProductCustomizableOption.option_0_title}} +$98.00" stepKey="selectCustomOption"/> + <fillField selector="{{StorefrontProductPageSection.qtyInput}}" userInput="1" stepKey="fillProductQuantity"/> + <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="clickOnAddToCartButton"/> + <waitForPageLoad stepKey="waitForProductToAddInCart"/> + <seeElement selector="{{StorefrontProductPageSection.successMsg}}" stepKey="seeYouAddedSimpleprod4ToYourShoppingCartSuccessSaveMessage"/> + <seeElement selector="{{StorefrontMinicartSection.quantity(1)}}" stepKey="seeAddedProductQuantityInCart"/> + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickOnMiniCart"/> + <see selector="{{StorefrontMinicartSection.miniCartItemsText}}" userInput="{{simpleProductRegularPriceCustomOptions.name}}" stepKey="seeProductNameInMiniCart"/> + <see selector="{{StorefrontMinicartSection.miniCartItemsText}}" userInput="{{simpleProductRegularPriceCustomOptions.storefront_new_cartprice}}" stepKey="seeProductPriceInMiniCart"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml new file mode 100644 index 000000000000..54ed753b80a1 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml @@ -0,0 +1,124 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateSimpleProductWithRegularPriceOutOfStockTest"> + <annotations> + <stories value="Update Simple Product"/> + <title value="Update Simple Product with Regular Price (Out of Stock)"/> + <description value="Test log in to Update Simple Product and Update Simple Product with Regular Price (Out of Stock)"/> + <testCaseId value="MC-10806"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> + <createData entity="defaultSimpleProduct" stepKey="initialSimpleProduct"> + <requiredEntity createDataKey="initialCategoryEntity"/> + </createData> + <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> + <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> + <actionGroup ref="deleteProductBySku" stepKey="deleteCreatedProduct"> + <argument name="sku" value="{{simpleProductRegularPrice32503OutOfStock.sku}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search default simple product in the grid --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGrid"> + <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + </actionGroup> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + + <!-- Update simple product with regular price(out of stock) --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductRegularPrice32503OutOfStock.name}}" stepKey="fillSimpleProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductRegularPrice32503OutOfStock.sku}}" stepKey="fillSimpleProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductRegularPrice32503OutOfStock.price}}" stepKey="fillSimpleProductPrice"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductRegularPrice32503OutOfStock.quantity}}" stepKey="fillSimpleProductQuantity"/> + <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{simpleProductRegularPrice32503OutOfStock.status}}" stepKey="selectStockStatusInStock"/> + <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductRegularPrice32503OutOfStock.weight}}" stepKey="fillSimpleProductWeight"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory"/> + <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory"/> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice32503OutOfStock.urlKey}}" stepKey="fillUrlKey"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForSimpleProductSave"/> + + <!-- Verify customer see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> + + <!-- Search updated simple product(from above step) in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> + <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductRegularPrice32503OutOfStock.name}}" stepKey="fillSimpleProductNameInNameFilter"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{simpleProductRegularPrice32503OutOfStock.sku}}" stepKey="fillProductSku"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedSimpleProductVisibleInGrid"/> + <waitForPageLoad stepKey="waitUntilSimpleProductPageIsOpened"/> + + <!-- Verify customer see updated simple product in the product form page --> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductRegularPrice32503OutOfStock.name}}" stepKey="seeSimpleProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{simpleProductRegularPrice32503OutOfStock.sku}}" stepKey="seeSimpleProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductRegularPrice32503OutOfStock.price}}" stepKey="seeSimpleProductPrice"/> + <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductRegularPrice32503OutOfStock.quantity}}" stepKey="seeSimpleProductQuantity"/> + <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{simpleProductRegularPrice32503OutOfStock.status}}" stepKey="seeSimpleProductStockStatus"/> + <seeInField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductRegularPrice32503OutOfStock.weight}}" stepKey="seeSimpleProductWeight"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> + <see selector="{{AdminProductFormSection.selectMultipleCategories}}" userInput="$$categoryEntity.name$$" stepKey="selectedCategories" /> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> + <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice32503OutOfStock.urlKey}}" stepKey="seeUrlKey"/> + + <!--Verify customer don't see updated simple product link on category page --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <dontSee selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{simpleProductRegularPrice32503OutOfStock.name}}" stepKey="dontSeeSimpleProductNameOnCategoryPage"/> + + <!-- Verify customer see updated simple product (from the above step) on the storefront page --> + <amOnPage url="{{StorefrontProductPage.url(simpleProductRegularPrice32503OutOfStock.urlKey)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{simpleProductRegularPrice32503OutOfStock.name}}" stepKey="seeSimpleProductNameOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{simpleProductRegularPrice32503OutOfStock.price}}" stepKey="seeSimpleProductPriceOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{simpleProductRegularPrice32503OutOfStock.sku}}" stepKey="seeSimpleProductSkuOnStoreFrontPage"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> + <assertEquals stepKey="assertStockAvailableOnProductPage"> + <expectedResult type="string">{{simpleProductRegularPrice32503OutOfStock.storefrontStatus}}</expectedResult> + <actualResult type="variable">productStockAvailableStatus</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">${{simpleProductRegularPrice32503OutOfStock.price}}</expectedResult> + <actualResult type="variable">productPriceAmount</actualResult> + </assertEquals> + + <!--Verify customer don't see updated simple product link on magento storefront page and is searchable by sku --> + <amOnPage url="{{StorefrontProductPage.url(simpleProductRegularPrice32503OutOfStock.urlKey)}}" stepKey="goToMagentoStorefrontPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{simpleProductRegularPrice32503OutOfStock.sku}}" stepKey="fillSimpleProductSkuInSearchTextBox"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <dontSee selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{simpleProductRegularPrice32503OutOfStock.name}}" stepKey="dontSeeProductName"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithNoRedirectTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithNoRedirectTest.xml new file mode 100644 index 000000000000..4dea6663e61b --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithNoRedirectTest.xml @@ -0,0 +1,88 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateTopCategoryUrlWithNoRedirectTest"> + <annotations> + <stories value="Update category"/> + <title value="Update top category url and do not create redirect"/> + <description value="Login as admin and update top category url and do not create redirect"/> + <testCaseId value="MC-6056"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <!-- Create three level nested category --> + <createData entity="_defaultCategory" stepKey="createDefaultCategory"/> + <createData entity="Two_nested_categories" stepKey="createTwoLevelNestedCategories"> + <requiredEntity createDataKey="createDefaultCategory"/> + </createData> + <createData entity="Three_nested_categories" stepKey="createThreeLevelNestedCategories"> + <requiredEntity createDataKey="createTwoLevelNestedCategories"/> + </createData> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + + <deleteData createDataKey="createThreeLevelNestedCategories" stepKey="deleteThreeNestedCategories"/> + <deleteData createDataKey="createTwoLevelNestedCategories" stepKey="deleteTwoLevelNestedCategory"/> + <deleteData createDataKey="createDefaultCategory" stepKey="deleteDefaultCategory"/> + </after> + + <!-- Open Category page --> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoaded"/> + + <!-- Open 3rd Level category --> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createThreeLevelNestedCategories.name$$)}}" stepKey="selectCategory"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + + <!--Update category UrlKey and uncheck permanent redirect for old URL --> + <scrollTo selector="{{AdminCategorySEOSection.SectionHeader}}" x="0" y="-80" stepKey="scrollToSearchEngineOptimization1"/> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="selectSearchEngineOptimization"/> + <waitForPageLoad stepKey="waitForPageToLoad1"/> + <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="updatedurl" stepKey="updateUrlKey"/> + <uncheckOption selector="{{AdminCategorySEOSection.UrlKeyRedirectCheckbox}}" stepKey="uncheckPermanentRedirectCheckBox"/> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="selectSearchEngineOptimization1"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveUpdatedCategory"/> + <waitForPageLoad stepKey="waitForCategoryToSave"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + + <!-- Get Category Id --> + <grabFromCurrentUrl stepKey="categoryId" regex="#\/([0-9]*)?\/$#"/> + + <!-- Open Url Rewrite Page --> + <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="openUrlRewriteIndexPage"/> + <waitForPageLoad stepKey="waitForUrlRewritePage"/> + + <!-- Verify third level category's Redirect Path, Target Path and Redirect Type after the URL Update --> + <click selector="{{AdminUrlRewriteIndexSection.resetButton}}" stepKey="clickOnResetButton"/> + <waitForPageLoad stepKey="waitForPageToLoad0"/> + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="updatedurl" stepKey="fillUpdatedUrlInRedirectPathFilter"/> + <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton"/> + <waitForPageLoad stepKey="waitForPageToLoad2"/> + <see stepKey="seeTheRedirectType" selector="{{AdminUrlRewriteIndexSection.redirectTypeColumn('1')}}" userInput="No" /> + <see stepKey="seeTheTargetPath" selector="{{AdminUrlRewriteIndexSection.targetPathColumn('1')}}" userInput="catalog/category/view/id/{$categoryId}"/> + <see selector="{{AdminUrlRewriteIndexSection.requestPathColumn('1')}}" userInput="$$createDefaultCategory.name$$/$$createTwoLevelNestedCategories.name$$/updatedurl.html" stepKey="seeTheRedirectPath"/> + + <!-- Verify third level category's old URL path doesn't show redirect path--> + <click selector="{{AdminUrlRewriteIndexSection.resetButton}}" stepKey="clickOnResetButton1"/> + <waitForPageLoad stepKey="waitForPageToLoad3"/> + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="{{Three_nested_categories.name_lwr}}" stepKey="fillOldUrlInRedirectPathFilter"/> + <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton1"/> + <waitForPageLoad stepKey="waitForPageToLoad4"/> + <see stepKey="seeEmptyRecodsMessage" selector="{{AdminUrlRewriteIndexSection.emptyRecords}}" userInput="We couldn't find any records."/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithRedirectTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithRedirectTest.xml new file mode 100644 index 000000000000..ee1ed5f97edf --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithRedirectTest.xml @@ -0,0 +1,88 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateTopCategoryUrlWithRedirectTest"> + <annotations> + <stories value="Update category"/> + <title value="Update top category url and create redirect"/> + <description value="Login as admin and update top category url and create redirect"/> + <testCaseId value="MC-6057"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <!-- Create three level nested category --> + <createData entity="_defaultCategory" stepKey="createDefaultCategory"/> + <createData entity="Two_nested_categories" stepKey="createTwoLevelNestedCategories"> + <requiredEntity createDataKey="createDefaultCategory"/> + </createData> + <createData entity="Three_nested_categories" stepKey="createThreeLevelNestedCategories"> + <requiredEntity createDataKey="createTwoLevelNestedCategories"/> + </createData> + </before> + <after> + <deleteData createDataKey="createThreeLevelNestedCategories" stepKey="deleteThreeNestedCategories"/> + <deleteData createDataKey="createTwoLevelNestedCategories" stepKey="deleteTwoLevelNestedCategory"/> + <deleteData createDataKey="createDefaultCategory" stepKey="deleteDefaultCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Open Category page --> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="openAdminCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForPageToLoaded"/> + + <!-- Open 3rd Level category --> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createThreeLevelNestedCategories.name$$)}}" stepKey="selectCategory"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + + <!--Update category UrlKey and check permanent redirect for old URL --> + <scrollTo selector="{{AdminCategorySEOSection.SectionHeader}}" x="0" y="-80" stepKey="scrollToSearchEngineOptimization1"/> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="selectSearchEngineOptimization"/> + <waitForPageLoad stepKey="waitForPageToLoad1"/> + <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="updateredirecturl" stepKey="updateUrlKey"/> + <checkOption selector="{{AdminCategorySEOSection.UrlKeyRedirectCheckbox}}" stepKey="checkPermanentRedirectCheckBox"/> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="selectSearchEngineOptimization1"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveUpdatedCategory"/> + <waitForPageLoad stepKey="waitForCategoryToSave"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + + <!-- Get Category ID --> + <grabFromCurrentUrl stepKey="categoryId" regex="#\/([0-9]*)?\/$#"/> + + <!-- Open Url Rewrite Page --> + <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="openUrlRewriteIndexPage"/> + <waitForPageLoad stepKey="waitForUrlRewritePage"/> + + <!-- Verify third level category's Redirect Path, Target Path and Redirect Type after the URL update --> + <click selector="{{AdminUrlRewriteIndexSection.resetButton}}" stepKey="clickOnResetButton"/> + <waitForPageLoad stepKey="waitForPageToLoad2"/> + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="updateredirecturl" stepKey="fillUpdatedURLInRedirectPathFilter"/> + <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton"/> + <waitForPageLoad stepKey="waitForPageToLoad3"/> + <see selector="{{AdminUrlRewriteIndexSection.redirectTypeColumn('1')}}" userInput="No" stepKey="seeTheRedirectType"/> + <see selector="{{AdminUrlRewriteIndexSection.targetPathColumn('1')}}" userInput="catalog/category/view/id/{$categoryId}" stepKey="seeTheTargetPath"/> + <see selector="{{AdminUrlRewriteIndexSection.requestPathColumn('1')}}" userInput="$$createDefaultCategory.name$$/$$createTwoLevelNestedCategories.name$$/updateredirecturl.html" stepKey="seeTheRedirectPath"/> + + <!-- Verify third level category's Redirect path, Target Path and Redirect type for old URL --> + <click selector="{{AdminUrlRewriteIndexSection.resetButton}}" stepKey="clickOnResetButton1"/> + <waitForPageLoad stepKey="waitForPageToLoad4"/> + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="$$createThreeLevelNestedCategories.name$$" stepKey="fillOldUrlInRedirectPathFilter"/> + <click selector="{{AdminUrlRewriteIndexSection.searchButton}}" stepKey="clickOnSearchButton1"/> + <waitForPageLoad stepKey="waitForPageToLoad5"/> + <see selector="{{AdminUrlRewriteIndexSection.requestPathColumn('1')}}" userInput="$$createDefaultCategory.name$$/$$createTwoLevelNestedCategories.name$$/$$createThreeLevelNestedCategories.name$$.html" stepKey="seeTheRedirectPathForOldUrl"/> + <see selector="{{AdminUrlRewriteIndexSection.targetPathColumn('1')}}" userInput="$$createDefaultCategory.name$$/$$createTwoLevelNestedCategories.name$$/updateredirecturl.html" stepKey="seeTheTargetPathForOldUrl"/> + <see selector="{{AdminUrlRewriteIndexSection.redirectTypeColumn('1')}}" userInput="Permanent (301)" stepKey="seeTheRedirectTypeForOldUrl"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml new file mode 100644 index 000000000000..9bdc93e61e49 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml @@ -0,0 +1,155 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest"> + <annotations> + <stories value="Update Virtual Product"/> + <title value="Update Virtual Product with Regular Price (In Stock) Visible in Category Only"/> + <description value="Test log in to Update Virtual Product and Update Virtual Product with Regular Price (In Stock) Visible in Category Only"/> + <testCaseId value="MC-6495"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> + <createData entity="defaultVirtualProduct" stepKey="initialVirtualProduct"> + <requiredEntity createDataKey="initialCategoryEntity"/> + </createData> + <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> + <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search default virtual product in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> + <waitForPageLoad stepKey="waitForProductCatalogPage1"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> + <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> + <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> + <waitForPageLoad stepKey="waitForProductSearch"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + + <!-- Update virtual product with regular price --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductRegularPrice.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductRegularPrice.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice.price}}" stepKey="fillProductPrice"/> + <!-- Press enter to validate advanced pricing link --> + <pressKey selector="{{AdminProductFormSection.productPrice}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ENTER]" stepKey="pressEnterKey"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> + <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="clickCustomerGroupPriceAddButton"/> + <scrollTo selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" x="50" y="0" stepKey="scrollToProductTierPriceQuantityInputTextBox"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.website}}" stepKey="selectProductTierPriceWebsiteInput"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.customer_group}}" stepKey="selectProductTierPriceCustomerGroupInput"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnVirtualProduct.qty}}" stepKey="fillProductTierPriceQuantityInput"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnVirtualProduct.price}}" stepKey="selectProductTierPriceFixedPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> + <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductRegularPrice.productTaxClass}}" stepKey="selectProductStockClass"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualProductRegularPrice.quantity}}" stepKey="fillProductQuantity"/> + <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{updateVirtualProductRegularPrice.status}}" stepKey="selectStockStatusInStock"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPrice.visibility}}" stepKey="selectVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPrice.urlKey}}" stepKey="fillUrlKey"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + <!-- Verify we see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> + + <!-- Search updated virtual product(from above step) in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> + <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductRegularPrice.name}}" stepKey="fillVirtualProductNameInNameFilter"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{updateVirtualProductRegularPrice.sku}}" stepKey="fillVirtualProductSku"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedVirtualProductVisibleInGrid"/> + <waitForPageLoad stepKey="waitUntilVirtualProductPageIsOpened"/> + + <!-- Verify we see created virtual product with tier price(from the above step) in the product form page --> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductRegularPrice.name}}" stepKey="seeProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductRegularPrice.sku}}" stepKey="seeProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice.price}}" stepKey="seeProductPrice"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink1"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.website}}" stepKey="seeProductTierPriceWebsiteInput"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.customer_group}}" stepKey="seeProductTierPriceCustomerGroupInput"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnVirtualProduct.qty}}" stepKey="seeProductTierPriceQuantityInput"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnVirtualProduct.price}}" stepKey="seeProductTierPriceFixedPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.advancedPricingCloseButton}}" stepKey="clickAdvancedPricingCloseButton"/> + <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductRegularPrice.productTaxClass}}" stepKey="seeProductTaxClass"/> + <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualProductRegularPrice.quantity}}" stepKey="seeProductQuantity"/> + <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{updateVirtualProductRegularPrice.status}}" stepKey="seeProductStockStatus"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> + <grabMultiple selector="{{AdminProductFormSection.selectMultipleCategories}}" stepKey="selectedCategories" /> + <assertEquals stepKey="assertSelectedCategories"> + <actualResult type="variable">selectedCategories</actualResult> + <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> + </assertEquals> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPrice.visibility}}" stepKey="seeVisibility"/> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> + <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPrice.urlKey}}" stepKey="seeUrlKey"/> + + <!-- Verify customer don't see updated virtual product link on storefront page and is searchable by sku --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductRegularPrice.urlKey)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{updateVirtualProductRegularPrice.sku}}" stepKey="fillVirtualProductSkuOnStorefrontPage"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <dontSee selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{updateVirtualProductRegularPrice.name}}" stepKey="dontSeeVirtualProductName"/> + + <!-- Verify customer see updated virtual product in category page --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <see selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{updateVirtualProductRegularPrice.name}}" stepKey="seeVirtualProductLinkOnCategoryPage"/> + <grabTextFrom selector="{{StorefrontCategoryMainSection.asLowAs}}" stepKey="tierPriceTextOnCategoryPage"/> + <assertEquals stepKey="assertTierPriceTextOnCategoryPage"> + <expectedResult type="string">As low as ${{tierPriceOnVirtualProduct.price}}</expectedResult> + <actualResult type="variable">tierPriceTextOnCategoryPage</actualResult> + </assertEquals> + + <!-- Verify customer see updated virtual product and tier price on product page --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductRegularPrice.urlKey)}}" stepKey="goToStorefrontProductPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageToLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductRegularPrice.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductRegularPrice.sku}}" stepKey="seeVirtualProductSku"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.tierPriceText}}" stepKey="tierPriceText"/> + <assertEquals stepKey="assertTierPriceTextOnProductPage"> + <expectedResult type="string">Buy {{tierPriceOnVirtualProduct.qty}} for ${{tierPriceOnVirtualProduct.price}} each and save 10%</expectedResult> + <actualResult type="variable">tierPriceText</actualResult> + </assertEquals> + <assertEquals stepKey="assertStockAvailableOnProductPage"> + <expectedResult type="string">{{updateVirtualProductRegularPrice.storefrontStatus}}</expectedResult> + <actualResult type="variable">productStockAvailableStatus</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">${{updateVirtualProductRegularPrice.price}}</expectedResult> + <actualResult type="variable">productPriceAmount</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml new file mode 100644 index 000000000000..34d85e7b4685 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml @@ -0,0 +1,249 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest"> + <annotations> + <stories value="Update Virtual Product"/> + <title value="Update Virtual Product with Regular Price (In Stock) with Custom Options Visible in Search Only"/> + <description value="Test log in to Update Virtual Product and Update Virtual Product with Regular Price (In Stock) with Custom Options Visible in Search Only"/> + <testCaseId value="MC-6641"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> + <createData entity="defaultVirtualProduct" stepKey="initialVirtualProduct"> + <requiredEntity createDataKey="initialCategoryEntity"/> + </createData> + <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> + <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search default virtual product in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> + <waitForPageLoad stepKey="waitForProductCatalogPage1"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> + <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> + <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> + <waitForPageLoad stepKey="waitForProductSearch"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + + <!-- Update virtual product with regular price(in stock) --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductRegularPriceInStock.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductRegularPriceInStock.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductRegularPriceInStock.price}}" stepKey="fillProductPrice"/> + <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductRegularPriceInStock.productTaxClass}}" stepKey="selectProductTaxClass"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualProductRegularPriceInStock.quantity}}" stepKey="fillProductQuantity"/> + <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{updateVirtualProductRegularPriceInStock.status}}" stepKey="selectStockStatusInStock"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPriceInStock.visibility}}" stepKey="selectVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPriceInStock.urlKey}}" stepKey="fillUrlKey"/> + <click selector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" stepKey="clickAdminProductCustomizableOption"/> + <!-- Create virtual product with customizable options dataSet1 --> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButton"/> + <waitForPageLoad stepKey="waitForFirstOption"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('0')}}" userInput="{{virtualProductCustomizableOption1.title}}" stepKey="fillOptionTitleForFirstDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('1')}}" stepKey="selectOptionTypeDropDownFirstDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('1', virtualProductCustomizableOption1.type)}}" stepKey="selectOptionFieldFromDropDownForFirstDataSet"/> + <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('0')}}" stepKey="checkRequiredCheckBoxForFirstDataSet"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionPrice('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_price}}" stepKey="fillOptionPriceForFirstDataSet"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.optionPriceType('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_price_type}}" stepKey="selectOptionPriceTypeForFirstDataSet"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionSku('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_sku}}" stepKey="fillOptionSkuForFirstDataSet"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.maxCharactersInput('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_max_characters}}" stepKey="fillOptionMaxCharactersForFirstDataSet"/> + <!--Create virtual product with customizable options dataSet2 --> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButtonForSecondDataSet"/> + <waitForPageLoad stepKey="waitForSecondDataSetToLoad"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('1')}}" userInput="{{virtualProductCustomizableOption2.title}}" stepKey="fillOptionTitleForSecondDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('2')}}" stepKey="selectOptionTypeDropDownSecondDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('2', virtualProductCustomizableOption2.type)}}" stepKey="selectOptionFieldFromDropDownForSecondDataSet"/> + <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('1')}}" stepKey="checkRequiredCheckBoxForSecondDataSet"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionPrice('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_price}}" stepKey="fillOptionPriceForSecondDataSet"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.optionPriceType('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_price_type}}" stepKey="selectOptionPriceTypeForSecondDataSet"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionSku('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_sku}}" stepKey="fillOptionSkuForSecondDataSet"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.maxCharactersInput('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_max_characters}}" stepKey="fillOptionMaxCharactersForSecondDataSet"/> + <!-- Create virtual product with customizable options dataSet3 --> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButtonForThirdSetOfData"/> + <waitForPageLoad stepKey="waitForThirdSetOfDataToLoad"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('2')}}" userInput="{{virtualProductCustomizableOption3.title}}" stepKey="fillOptionTitleForThirdDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('3')}}" stepKey="selectOptionTypeDropDownForThirdDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('3', virtualProductCustomizableOption3.type)}}" stepKey="selectOptionFieldFromDropDownForThirdDataSet"/> + <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('2')}}" stepKey="checkRequiredCheckBoxForThirdDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.addValue}}" stepKey="clickAddOptionButtonForThirdDataSetToAddFirstRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_title}}" stepKey="fillOptionTitleForThirdDataSetFirstRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_price}}" stepKey="fillOptionPriceForThirdDataSetFirstRow"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_price_type}}" stepKey="selectOptionPriceTypeForThirdDataSetFirstRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_sku}}" stepKey="fillOptionSkuForThirdDataSetFirstRow"/> + <click selector="{{AdminProductCustomizableOptionsSection.addValue}}" stepKey="clickAddOptionButtonForThirdDataSetToAddSecondRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_title}}" stepKey="fillOptionTitleForThirdDataSetSecondRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_price}}" stepKey="fillOptionPriceForThirdDataSetSecondRow"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_price_type}}" stepKey="selectOptionPriceTypeForThirdDataSetSecondRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_sku}}" stepKey="fillOptionSkuForThirdDataSetSecondRow"/> + <!-- Create virtual product with customizable options dataSet4 --> + <scrollToTopOfPage stepKey="scrollToAddOptionButton"/> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButtonForFourthDataSet"/> + <waitForPageLoad stepKey="waitForFourthDataSetToLoad"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('3')}}" userInput="{{virtualProductCustomizableOption4.title}}" stepKey="fillOptionTitleForFourthDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('4')}}" stepKey="selectOptionTypeDropDownForFourthSetOfData"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('4', virtualProductCustomizableOption4.type)}}" stepKey="selectOptionFieldFromDropDownForFourthDataSet"/> + <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('3')}}" stepKey="checkRequiredCheckBoxForFourthDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.addValue}}" stepKey="clickAddOptionButtonForFourthDataSetToAddFirstRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_title}}" stepKey="fillOptionTitleForFourthDataSetFirstRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_price}}" stepKey="fillOptionPriceForFourthDataSetFirstRow"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_price_type}}" stepKey="selectOptionPriceTypeForFourthDataSetFirstRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_sku}}" stepKey="fillOptionSkuForFourthDataSetFirstRow"/> + <click selector="{{AdminProductCustomizableOptionsSection.addValue}}" stepKey="clickAddOptionButtonForFourthDataSetToAddSecondRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_title}}" stepKey="fillOptionTitleForFourthDataSetSecondRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_price}}" stepKey="fillOptionPriceForFourthDataSetSecondRow"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_price_type}}" stepKey="selectOptionPriceTypeForFourthDataSetSecondRow"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_sku}}" stepKey="fillOptionSkuForFourthDataSetSecondRow"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + <!-- Verify we see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> + + <!-- Search updated virtual product(from above step) in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> + <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductRegularPriceInStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{updateVirtualProductRegularPriceInStock.sku}}" stepKey="fillVirtualProductSku"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedVirtualProductVisibleInGrid"/> + <waitForPageLoad stepKey="waitUntilVirtualProductPageIsOpened"/> + + <!-- Verify we customer see updated virtual product in the product form page --> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductRegularPriceInStock.name}}" stepKey="seeProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductRegularPriceInStock.sku}}" stepKey="seeProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductRegularPriceInStock.price}}" stepKey="seeProductPrice"/> + <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductRegularPriceInStock.productTaxClass}}" stepKey="seeProductTaxClass"/> + <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualProductRegularPriceInStock.quantity}}" stepKey="seeProductQuantity"/> + <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{updateVirtualProductRegularPriceInStock.status}}" stepKey="seeProductStockStatus"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> + <grabMultiple selector="{{AdminProductFormSection.selectMultipleCategories}}" stepKey="selectedCategories" /> + <assertEquals stepKey="assertSelectedCategories"> + <actualResult type="variable">selectedCategories</actualResult> + <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> + </assertEquals> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPriceInStock.visibility}}" stepKey="seeVisibility"/> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> + <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPriceInStock.urlKey}}" stepKey="seeUrlKey"/> + <click selector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" stepKey="clickAdminProductCustomizableOptionToSeeValues"/> + <!-- Create virtual product with customizable options dataSet1 --> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButton1"/> + <waitForPageLoad stepKey="waitForFirstOptionToLoad"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('0')}}" userInput="{{virtualProductCustomizableOption1.title}}" stepKey="seeOptionTitleForFirstDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('1')}}" stepKey="selectOptionTypeDropDownFirstDataSet1"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('1', virtualProductCustomizableOption1.type)}}" stepKey="selectOptionFieldFromDropDownForFirstDataSet1"/> + <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('0')}}" stepKey="checkRequiredCheckBoxForFirstDataSet1"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.optionPrice('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_price}}" stepKey="seeOptionPriceForFirstDataSet"/> + <seeOptionIsSelected selector="{{AdminProductCustomizableOptionsSection.optionPriceType('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_price_type}}" stepKey="selectOptionPriceTypeForFirstDataSet1"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.optionSku('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_sku}}" stepKey="seeOptionSkuForFirstDataSet"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.maxCharactersInput('0')}}" userInput="{{virtualProductCustomizableOption1.option_0_max_characters}}" stepKey="seeOptionMaxCharactersForFirstDataSet"/> + <!--Create virtual product with customizable options dataSet2 --> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButtonForSecondDataSetToSeeFields"/> + <waitForPageLoad stepKey="waitForTheSecondDataSetToLoad"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('1')}}" userInput="{{virtualProductCustomizableOption2.title}}" stepKey="seeOptionTitleForSecondDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('2')}}" stepKey="selectOptionTypeDropDownSecondDataSet2"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('2', virtualProductCustomizableOption2.type)}}" stepKey="selectOptionFieldFromDropDownForSecondDataSet2"/> + <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('1')}}" stepKey="checkRequiredCheckBoxForTheSecondDataSet"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.optionPrice('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_price}}" stepKey="seeOptionPriceForSecondDataSet"/> + <seeOptionIsSelected selector="{{AdminProductCustomizableOptionsSection.optionPriceType('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_price_type}}" stepKey="selectOptionPriceTypeForTheSecondDataSet"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.optionSku('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_sku}}" stepKey="seeOptionSkuForSecondDataSet"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.maxCharactersInput('1')}}" userInput="{{virtualProductCustomizableOption2.option_0_max_characters}}" stepKey="seeOptionMaxCharactersForSecondDataSet"/> + <!-- Create virtual product with customizable options dataSet3 --> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButtonForTheThirdSetOfData"/> + <waitForPageLoad stepKey="waitForTheThirdSetOfDataToLoad"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('2')}}" userInput="{{virtualProductCustomizableOption3.title}}" stepKey="seeOptionTitleForThirdDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('3')}}" stepKey="selectOptionTypeDropDownForTheThirdDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('3', virtualProductCustomizableOption3.type)}}" stepKey="selectOptionFieldFromDropDownForTheThirdDataSet"/> + <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('2')}}" stepKey="checkRequiredCheckBoxForTheThirdDataSet"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_title}}" stepKey="seeOptionTitleForThirdDataSetFirstRow"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_price}}" stepKey="seeOptionPriceForThirdDataSetFirstRow"/> + <seeOptionIsSelected selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_price_type}}" stepKey="selectOptionPriceTypeForTheThirdDataSetFirstRow"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption3.title,'0')}}" userInput="{{virtualProductCustomizableOption3.option_0_sku}}" stepKey="seeOptionSkuForThirdDataSetFirstRow"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_title}}" stepKey="seeOptionTitleForThirdDataSetSecondRow"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_price}}" stepKey="seeOptionPriceForThirdDataSetSecondRow"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_price_type}}" stepKey="selectOptionPriceTypeForTheThirdDataSetSecondRow"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption3.title,'1')}}" userInput="{{virtualProductCustomizableOption3.option_1_sku}}" stepKey="seeOptionSkuForThirdDataSetSecondRow"/> + <!-- Create virtual product with customizable options dataSet4 --> + <scrollToTopOfPage stepKey="scrollToTheAddOptionButton"/> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOptionButtonForTheFourthDataSet"/> + <waitForPageLoad stepKey="waitForTheFourthDataSetToLoad"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('3')}}" userInput="{{virtualProductCustomizableOption4.title}}" stepKey="fillOptionTitleForTheFourthDataSet"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeDropDown('4')}}" stepKey="selectOptionTypeDropDownForTheFourthSetOfData"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeItem('4', virtualProductCustomizableOption4.type)}}" stepKey="selectOptionFieldFromDropDownForTheFourthDataSet"/> + <checkOption selector="{{AdminProductCustomizableOptionsSection.requiredCheckBox('3')}}" stepKey="checkRequiredCheckBoxForTheFourthDataSet"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_title}}" stepKey="seeOptionTitleForFourthDataSetFirstRow"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_price}}" stepKey="seeOptionPriceForFourthDataSetFirstRow"/> + <seeOptionIsSelected selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_price_type}}" stepKey="selectOptionPriceTypeForTheFourthDataSetFirstRow"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption4.title,'0')}}" userInput="{{virtualProductCustomizableOption4.option_0_sku}}" stepKey="seeOptionSkuForFourthDataSetFirstRow"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_title}}" stepKey="seeOptionTitleForFourthDataSetSecondRow"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_price}}" stepKey="seeOptionPriceForFourthDataSetSecondRow"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_price_type}}" stepKey="selectOptionPriceTypeForTheFourthDataSetSecondRow"/> + <seeInField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_sku}}" stepKey="seeOptionSkuForFourthDataSetSecondRow"/> + + <!--Verify customer don't see updated virtual product link on category page --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <dontSee selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{updateVirtualProductRegularPriceInStock.name}}" stepKey="dontSeeVirtualProductNameOnCategoryPage"/> + + <!-- Verify customer see updated virtual product (from the above step) on the storefront page --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductRegularPriceInStock.urlKey)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductRegularPriceInStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductRegularPriceInStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductRegularPriceInStock.sku}}" stepKey="seeVirtualProductSku"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> + <assertEquals stepKey="assertStockAvailableOnProductPage"> + <expectedResult type="string">{{updateVirtualProductRegularPriceInStock.storefrontStatus}}</expectedResult> + <actualResult type="variable">productStockAvailableStatus</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">${{updateVirtualProductRegularPriceInStock.price}}</expectedResult> + <actualResult type="variable">productPriceAmount</actualResult> + </assertEquals> + <!--Verify we customer see customizable options are Required --> + <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomInput(virtualProductCustomizableOption1.title)}}" stepKey="verifyFirstCustomOptionIsRequired" /> + <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomInput(virtualProductCustomizableOption2.title)}}" stepKey="verifySecondCustomOptionIsRequired" /> + <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomSelect(virtualProductCustomizableOption3.title)}}" stepKey="verifyThirdCustomOptionIsRequired" /> + <seeElement selector="{{StorefrontProductInfoMainSection.requiredCustomSelect(virtualProductCustomizableOption4.title)}}" stepKey="verifyFourthCustomOptionIsRequired" /> + <!--Verify customer see customizable option titles and prices --> + <grabMultiple selector="{{StorefrontProductInfoMainSection.allCustomOptionLabels}}" stepKey="allCustomOptionLabels" /> + <assertEquals stepKey="verifyLabels"> + <actualResult type="variable">allCustomOptionLabels</actualResult> + <expectedResult type="array">[{{virtualProductCustomizableOption1.title}} + ${{virtualProductCustomizableOption1.option_0_price}}, {{virtualProductCustomizableOption2.title}} + ${{virtualProductCustomizableOption2.option_0_price}}, {{virtualProductCustomizableOption3.title}}, {{virtualProductCustomizableOption4.title}}]</expectedResult> + </assertEquals> + <grabAttributeFrom userInput="for" selector="{{StorefrontProductInfoMainSection.customOptionLabel(virtualProductCustomizableOption4.title)}}" stepKey="fourthOptionId" /> + <grabMultiple selector="{{StorefrontProductInfoMainSection.customSelectOptions({$fourthOptionId})}}" stepKey="grabFourthOptions" /> + <assertEquals stepKey="assertFourthSelectOptions"> + <actualResult type="variable">grabFourthOptions</actualResult> + <expectedResult type="array">['-- Please Select --', {{virtualProductCustomizableOption4.option_0_title}} +$12.01, {{virtualProductCustomizableOption4.option_1_title}} +$20.02]</expectedResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryAndSearchTest.xml new file mode 100644 index 000000000000..a2a4f6586025 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryAndSearchTest.xml @@ -0,0 +1,100 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryAndSearchTest"> + <annotations> + <stories value="Update Virtual Product"/> + <title value="Update Virtual Product with Regular Price (Out of Stock) Visible in Category and Search"/> + <description value="Test log in to Update Virtual Product and Update Virtual Product with Regular Price (Out of Stock) Visible in Category and Search"/> + <testCaseId value="MC-7433"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> + <createData entity="defaultVirtualProduct" stepKey="initialVirtualProduct"> + <requiredEntity createDataKey="initialCategoryEntity"/> + </createData> + <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> + <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search default virtual product in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> + <waitForPageLoad stepKey="waitForProductCatalogPage1"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> + <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> + <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> + <waitForPageLoad stepKey="waitForProductSearch"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + + <!-- Update virtual product with regular price(out of stock) --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.price}}" stepKey="fillProductPrice"/> + <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.productTaxClass}}" stepKey="selectProductStockClass"/> + <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.status}}" stepKey="selectStockStatusInStock"/> + <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.visibility}}" stepKey="selectVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.urlKey}}" stepKey="fillUrlKey"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + <!-- Verify we see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> + + <!-- Search updated virtual product(from above step) in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> + <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.sku}}" stepKey="fillVirtualProductSku"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedVirtualProductVisibleInGrid"/> + <waitForPageLoad stepKey="waitUntilVirtualProductPageIsOpened"/> + + <!-- Verify customer see updated virtual product with regular price(out of stock) in the product form page --> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="seeProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.sku}}" stepKey="seeProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.price}}" stepKey="seeProductPrice"/> + <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.productTaxClass}}" stepKey="seeProductTaxClass"/> + <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.status}}" stepKey="seeProductStockStatus"/> + <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.visibility}}" stepKey="seeVisibility"/> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> + <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.urlKey}}" stepKey="seeUrlKey"/> + + <!--Verify customer see updated virtual product with regular price(out of stock) on product storefront page --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductRegularPrice5OutOfStock.urlKey)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.sku}}" stepKey="seeVirtualProductSku"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> + <assertEquals stepKey="assertStockAvailableOnProductPage"> + <expectedResult type="string">{{updateVirtualProductRegularPrice5OutOfStock.storefrontStatus}}</expectedResult> + <actualResult type="variable">productStockAvailableStatus</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">${{updateVirtualProductRegularPrice5OutOfStock.price}}</expectedResult> + <actualResult type="variable">productPriceAmount</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryOnlyTest.xml new file mode 100644 index 000000000000..e64022b31161 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryOnlyTest.xml @@ -0,0 +1,127 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryOnlyTest"> + <annotations> + <stories value="Update Virtual Product"/> + <title value="Update Virtual Product with Regular Price (Out of Stock) Visible in Category Only"/> + <description value="Test log in to Update Virtual Product and Update Virtual Product with Regular Price (Out of Stock) Visible in Category Only"/> + <testCaseId value="MC-6503"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> + <createData entity="defaultVirtualProduct" stepKey="initialVirtualProduct"> + <requiredEntity createDataKey="initialCategoryEntity"/> + </createData> + <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> + <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search default virtual product in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> + <waitForPageLoad stepKey="waitForProductCatalogPage1"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickclearAllFilter" /> + <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> + <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> + <waitForPageLoad stepKey="waitForProductSearch"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + + <!-- Update virtual product with regular price(out of stock) --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.price}}" stepKey="fillProductPrice"/> + <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.productTaxClass}}" stepKey="selectProductStockClass"/> + <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.status}}" stepKey="selectStockStatusInStock"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.visibility}}" stepKey="selectVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.urlKey}}" stepKey="fillUrlKey"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + <!-- Verify we see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> + + <!-- Search updated virtual product(from above step) in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> + <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.sku}}" stepKey="fillVirtualProductSku"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedVirtualProductVisibleInGrid"/> + <waitForPageLoad stepKey="waitUntilVirtualProductPageIsOpened"/> + + <!-- Verify we see updated virtual product with regular price(from the above step) in the product form page --> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="seeProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.sku}}" stepKey="seeProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.price}}" stepKey="seeProductPrice"/> + <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.productTaxClass}}" stepKey="seeProductTaxClass"/> + <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.status}}" stepKey="seeProductStockStatus"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> + <grabMultiple selector="{{AdminProductFormSection.selectMultipleCategories}}" stepKey="selectedCategories" /> + <assertEquals stepKey="assertSelectedCategories"> + <actualResult type="variable">selectedCategories</actualResult> + <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> + </assertEquals> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.visibility}}" stepKey="seeVisibility"/> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> + <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.urlKey}}" stepKey="seeUrlKey"/> + + <!--Verify customer don't see updated virtual product link(from above step) on category page --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <dontSee selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="dontseeVirtualProductNameOnCategoryPage"/> + + <!--Verify customer see updated virtual product (from above step) on product storefront page --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductRegularPrice5OutOfStock.urlKey)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.sku}}" stepKey="seeVirtualProductSku"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> + <assertEquals stepKey="assertStockAvailableOnProductPage"> + <expectedResult type="string">{{updateVirtualProductRegularPrice5OutOfStock.storefrontStatus}}</expectedResult> + <actualResult type="variable">productStockAvailableStatus</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">${{updateVirtualProductRegularPrice5OutOfStock.price}}</expectedResult> + <actualResult type="variable">productPriceAmount</actualResult> + </assertEquals> + + <!--Verify customer don't see updated virtual product link(from above step) on magento storefront page and is searchable by sku --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductRegularPrice5OutOfStock.urlKey)}}" stepKey="goToMagentoStorefrontPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.sku}}" stepKey="fillVirtualProductName"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <dontSee selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="dontSeeVirtualProductName"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInSearchOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInSearchOnlyTest.xml new file mode 100644 index 000000000000..aa3184994daf --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInSearchOnlyTest.xml @@ -0,0 +1,112 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInSearchOnlyTest"> + <annotations> + <stories value="Update Virtual Product"/> + <title value="Update Virtual Product with Regular Price (Out of Stock) Visible in Search Only"/> + <description value="Test log in to Update Virtual Product and Update Virtual Product with Regular Price (Out of Stock) Visible in Search Only"/> + <testCaseId value="MC-6498"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> + <createData entity="defaultVirtualProduct" stepKey="initialVirtualProduct"> + <requiredEntity createDataKey="initialCategoryEntity"/> + </createData> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search default virtual product in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> + <waitForPageLoad stepKey="waitForProductCatalogPage1"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> + <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> + <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> + <waitForPageLoad stepKey="waitForProductSearch"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + + <!-- Update virtual product with regular price(out of stock) --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.price}}" stepKey="fillProductPrice"/> + <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.productTaxClass}}" stepKey="selectProductStockClass"/> + <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.status}}" stepKey="selectStockStatusInStock"/> + <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.visibility}}" stepKey="selectVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.urlKey}}" stepKey="fillUrlKey"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + <!-- Verify we see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> + + <!-- Search updated virtual product(from above step) in the grid --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> + <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.sku}}" stepKey="fillVirtualProductSku"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedVirtualProductVisibleInGrid"/> + <waitForPageLoad stepKey="waitUntilVirtualProductPageIsOpened"/> + + <!-- Verify customer see updated virtual product with regular price in the product form page --> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.name}}" stepKey="seeProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.sku}}" stepKey="seeProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.price}}" stepKey="seeProductPrice"/> + <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.productTaxClass}}" stepKey="seeProductTaxClass"/> + <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.status}}" stepKey="seeProductStockStatus"/> + <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.visibility}}" stepKey="seeVisibility"/> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> + <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.urlKey}}" stepKey="seeUrlKey"/> + + <!--Verify customer see updated virtual product on storefront page by url key --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductRegularPrice99OutOfStock.urlKey)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.sku}}" stepKey="seeVirtualProductSku"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> + <assertEquals stepKey="assertStockAvailableOnProductPage"> + <expectedResult type="string">{{updateVirtualProductRegularPrice99OutOfStock.storefrontStatus}}</expectedResult> + <actualResult type="variable">productStockAvailableStatus</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">${{updateVirtualProductRegularPrice99OutOfStock.price}}</expectedResult> + <actualResult type="variable">productPriceAmount</actualResult> + </assertEquals> + + <!--Verify customer don't see updated virtual product link on magento storefront page --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductRegularPrice99OutOfStock.urlKey)}}" stepKey="goToMagentoStorefrontPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.sku}}" stepKey="fillVirtualProductName"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <dontSee selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.name}}" stepKey="dontSeeVirtualProductLinkOnStorefrontPage"/> + + <!--Verify customer don't see updated virtual product link on category page --> + <amOnPage url="{{StorefrontCategoryPage.url($$initialCategoryEntity.name$$)}}" stepKey="openCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <dontSee selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.name}}" stepKey="dontSeeVirtualProductLinkOnCategoryPage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceInStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceInStockVisibleInCategoryAndSearchTest.xml new file mode 100644 index 000000000000..9b6a56d6f81d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceInStockVisibleInCategoryAndSearchTest.xml @@ -0,0 +1,143 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateVirtualProductWithSpecialPriceInStockVisibleInCategoryAndSearchTest"> + <annotations> + <stories value="Update Virtual Product"/> + <title value="Update Virtual Product with Special Price (In Stock) Visible in Category and Search"/> + <description value="Test log in to Update Virtual Product and Update Virtual Product with Special Price (In Stock) Visible in Category and Search"/> + <testCaseId value="MC-6496"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> + <createData entity="defaultVirtualProduct" stepKey="initialVirtualProduct"> + <requiredEntity createDataKey="initialCategoryEntity"/> + </createData> + <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> + <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search default virtual product in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> + <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> + <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> + <waitForPageLoad stepKey="waitForProductSearch"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + + <!-- Update virtual product with special price --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductSpecialPrice.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductSpecialPrice.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductSpecialPrice.price}}" stepKey="fillProductPrice"/> + <!-- Press enter to validate advanced pricing link --> + <pressKey selector="{{AdminProductFormSection.productPrice}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ENTER]" stepKey="pressEnterKey"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" userInput="{{updateVirtualProductSpecialPrice.special_price}}" stepKey="fillSpecialPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> + <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductSpecialPrice.productTaxClass}}" stepKey="selectProductStockClass"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualProductSpecialPrice.quantity}}" stepKey="fillProductQuantity"/> + <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{updateVirtualProductSpecialPrice.status}}" stepKey="selectStockStatusInStock"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductSpecialPrice.visibility}}" stepKey="selectVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductSpecialPrice.urlKey}}" stepKey="fillUrlKey"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + <!-- Verify we see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> + + <!-- Search updated virtual product(from above step) in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> + <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductSpecialPrice.name}}" stepKey="fillVirtualProductNameInNameFilter"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{updateVirtualProductSpecialPrice.sku}}" stepKey="fillVirtualProductSku"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedVirtualProductVisibleInGrid"/> + <waitForPageLoad stepKey="waitUntilVirtualProductPageIsOpened"/> + <!-- Verify customer see updated virtual product with special price(from the above step) in the product form page --> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductSpecialPrice.name}}" stepKey="seeProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductSpecialPrice.sku}}" stepKey="seeProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductSpecialPrice.price}}" stepKey="seeProductPrice"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink1"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" userInput="{{updateVirtualProductSpecialPrice.special_price}}" stepKey="seeSpecialPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.advancedPricingCloseButton}}" stepKey="clickAdvancedPricingCloseButton"/> + <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductSpecialPrice.productTaxClass}}" stepKey="seeProductTaxClass"/> + <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualProductSpecialPrice.quantity}}" stepKey="seeProductQuantity"/> + <see selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{updateVirtualProductSpecialPrice.status}}" stepKey="seeProductStockStatus"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> + <grabMultiple selector="{{AdminProductFormSection.selectMultipleCategories}}" stepKey="selectedCategories" /> + <assertEquals stepKey="assertSelectedCategories"> + <actualResult type="variable">selectedCategories</actualResult> + <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> + </assertEquals> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <see selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductSpecialPrice.visibility}}" stepKey="seeVisibility"/> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> + <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductSpecialPrice.urlKey}}" stepKey="seeUrlKey"/> + + <!--Verify customer see updated virtual product link on category page --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <see selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{updateVirtualProductSpecialPrice.name}}" stepKey="seeVirtualProductNameOnCategoryPage"/> + + <!-- Verify customer see updated virtual product on the magento storefront page and is searchable by sku --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductSpecialPrice.urlKey)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{updateVirtualProductSpecialPrice.sku}}" stepKey="fillVirtualProductName"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <see selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{updateVirtualProductSpecialPrice.name}}" stepKey="seeVirtualProductName"/> + + <!--Verify customer see updated virtual product with special price(from above step) on product storefront page by url key --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductSpecialPrice.urlKey)}}" stepKey="goToProductStorefrontPage"/> + <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductSpecialPrice.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductSpecialPrice.sku}}" stepKey="seeVirtualProductSku"/> + <!-- Verify customer see virtual product special price on the storefront page --> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.specialPriceAmount}}" stepKey="specialPriceAmount"/> + <assertEquals stepKey="assertSpecialPriceTextOnProductPage"> + <expectedResult type="string">${{updateVirtualProductSpecialPrice.special_price}}</expectedResult> + <actualResult type="variable">specialPriceAmount</actualResult> + </assertEquals> + <!-- Verify customer see virtual product old price on the storefront page --> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.oldPriceAmount}}" stepKey="oldPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">${{updateVirtualProductSpecialPrice.price}}</expectedResult> + <actualResult type="variable">oldPriceAmount</actualResult> + </assertEquals> + <!-- Verify customer see virtual product in stock status on the storefront page --> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> + <assertEquals stepKey="assertStockAvailableOnProductPage"> + <expectedResult type="string">{{updateVirtualProductSpecialPrice.storefrontStatus}}</expectedResult> + <actualResult type="variable">productStockAvailableStatus</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceOutOfStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceOutOfStockVisibleInCategoryAndSearchTest.xml new file mode 100644 index 000000000000..920a0a494bae --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceOutOfStockVisibleInCategoryAndSearchTest.xml @@ -0,0 +1,135 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="UpdateVirtualProductWithSpecialPriceOutOfStockVisibleInCategoryAndSearchTest"> + <annotations> + <stories value="Update Virtual Product"/> + <title value="Update Virtual Product with Special Price (Out of Stock) Visible in Category and Search"/> + <description value="Test log in to Update Virtual Product and Update Virtual Product with Special Price (Out of Stock) Visible in Category and Search"/> + <testCaseId value="MC-6505"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> + <createData entity="defaultVirtualProduct" stepKey="initialVirtualProduct"> + <requiredEntity createDataKey="initialCategoryEntity"/> + </createData> + <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> + <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search default virtual product in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> + <waitForPageLoad stepKey="waitForProductCatalogPage1"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> + <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> + <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> + <waitForPageLoad stepKey="waitForProductSearch"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + + <!-- Update virtual product with special price(out of stock) --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.price}}" stepKey="fillProductPrice"/> + <!-- Press enter to validate advanced pricing link --> + <pressKey selector="{{AdminProductFormSection.productPrice}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ENTER]" stepKey="pressEnterKey"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.special_price}}" stepKey="fillSpecialPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> + <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.productTaxClass}}" stepKey="selectProductStockClass"/> + <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.status}}" stepKey="selectStockStatusInStock"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.visibility}}" stepKey="selectVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.urlKey}}" stepKey="fillUrlKey"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + <!-- Verify we see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> + + <!-- Search updated virtual product with special price(out of stock) in the grid --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> + <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.sku}}" stepKey="fillVirtualProductSku"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedVirtualProductVisibleInGrid"/> + <waitForPageLoad stepKey="waitUntilVirtualProductPageIsOpened"/> + <!-- Verify customer see updated virtual product with special price(out of stock) in the product form page --> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.name}}" stepKey="seeProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.sku}}" stepKey="seeProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.price}}" stepKey="seeProductPrice"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink1"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.special_price}}" stepKey="seeSpecialPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.advancedPricingCloseButton}}" stepKey="clickAdvancedPricingCloseButton"/> + <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.productTaxClass}}" stepKey="seeProductTaxClass"/> + <see selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.status}}" stepKey="seeProductStockStatus"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> + <grabMultiple selector="{{AdminProductFormSection.selectMultipleCategories}}" stepKey="selectedCategories" /> + <assertEquals stepKey="assertSelectedCategories"> + <actualResult type="variable">selectedCategories</actualResult> + <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> + </assertEquals> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <see selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.visibility}}" stepKey="seeVisibility"/> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> + <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.urlKey}}" stepKey="seeUrlKey"/> + + <!--Verify customer see updated virtual product with special price(out of stock) on product storefront page --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductSpecialPriceOutOfStock.urlKey)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.sku}}" stepKey="seeVirtualProductSku"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> + <assertEquals stepKey="assertStockAvailableOnProductPage"> + <expectedResult type="string">{{updateVirtualProductSpecialPriceOutOfStock.storefrontStatus}}</expectedResult> + <actualResult type="variable">productStockAvailableStatus</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.oldPriceAmount}}" stepKey="productPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">${{updateVirtualProductSpecialPriceOutOfStock.price}}</expectedResult> + <actualResult type="variable">productPriceAmount</actualResult> + </assertEquals> + <!--Verify customer see virtual product with special price on the storefront page--> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.specialPriceAmount}}" stepKey="specialPriceAmount"/> + <assertEquals stepKey="assertSpecialPriceTextOnProductPage"> + <expectedResult type="string">${{updateVirtualProductSpecialPriceOutOfStock.special_price}}</expectedResult> + <actualResult type="variable">specialPriceAmount</actualResult> + </assertEquals> + + <!--Verify customer don't see updated virtual product link on magento storefront page and is searchable by sku --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductSpecialPriceOutOfStock.urlKey)}}" stepKey="goToMagentoStorefrontPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.sku}}" stepKey="fillVirtualProductName"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <dontSee selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.name}}" stepKey="dontSeeVirtualProductNameOnStorefrontPage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryAndSearchTest.xml new file mode 100644 index 000000000000..d4ec5e410d9f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryAndSearchTest.xml @@ -0,0 +1,155 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="UpdateVirtualProductWithTierPriceInStockVisibleInCategoryAndSearchTest"> + <annotations> + <stories value="Update Virtual Product"/> + <title value="Update Virtual Product with Tier Price (In Stock) Visible in Category and Search"/> + <description value="Test log in to Update Virtual Product and Update Virtual Product with Tier Price (In Stock) Visible in Category and Search"/> + <testCaseId value="MC-6504"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> + <createData entity="defaultVirtualProduct" stepKey="initialVirtualProduct"> + <requiredEntity createDataKey="initialCategoryEntity"/> + </createData> + <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> + <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search default virtual product in the grid --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> + <waitForPageLoad stepKey="waitForProductCatalogPage1"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> + <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> + <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> + <waitForPageLoad stepKey="waitForProductSearch"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + + <!-- Update virtual product with tier price --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductTierPriceInStock.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductTierPriceInStock.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductTierPriceInStock.price}}" stepKey="fillProductPrice"/> + <!-- Press enter to validate advanced pricing link --> + <pressKey selector="{{AdminProductFormSection.productPrice}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ENTER]" stepKey="pressEnterKey"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> + <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="clickCustomerGroupPriceAddButton"/> + <scrollTo selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" x="50" y="0" stepKey="scrollToProductTierPriceQuantityInputTextBox"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.website}}" stepKey="selectProductTierPriceWebsiteInput"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.customer_group}}" stepKey="selectProductTierPriceCustomerGroupInput"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnVirtualProduct.qty}}" stepKey="fillProductTierPriceQuantityInput"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnVirtualProduct.price}}" stepKey="selectProductTierPriceFixedPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> + <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductTierPriceInStock.productTaxClass}}" stepKey="selectProductStockClass"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualProductTierPriceInStock.quantity}}" stepKey="fillProductQuantity"/> + <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{updateVirtualProductTierPriceInStock.status}}" stepKey="selectStockStatusInStock"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductTierPriceInStock.visibility}}" stepKey="selectVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductTierPriceInStock.urlKey}}" stepKey="fillUrlKey"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + <!-- Verify we see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> + + <!-- Search updated virtual product(from above step) in the grid --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> + <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductTierPriceInStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{updateVirtualProductTierPriceInStock.sku}}" stepKey="fillVirtualProductSku"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedVirtualProductVisibleInGrid"/> + <waitForPageLoad stepKey="waitUntilVirtualProductPageIsOpened"/> + + <!-- Verify customer see updated virtual product with tier price(from the above step) in the product form page --> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductTierPriceInStock.name}}" stepKey="seeProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductTierPriceInStock.sku}}" stepKey="seeProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductTierPriceInStock.price}}" stepKey="seeProductPrice"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink1"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.website}}" stepKey="seeProductTierPriceWebsiteInput"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.customer_group}}" stepKey="seeProductTierPriceCustomerGroupInput"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnVirtualProduct.qty}}" stepKey="seeProductTierPriceQuantityInput"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnVirtualProduct.price}}" stepKey="seeProductTierPriceFixedPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.advancedPricingCloseButton}}" stepKey="clickAdvancedPricingCloseButton"/> + <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductTierPriceInStock.productTaxClass}}" stepKey="seeProductTaxClass"/> + <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualProductTierPriceInStock.quantity}}" stepKey="seeProductQuantity"/> + <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{updateVirtualProductTierPriceInStock.status}}" stepKey="seeProductStockStatus"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> + <grabMultiple selector="{{AdminProductFormSection.selectMultipleCategories}}" stepKey="selectedCategories" /> + <assertEquals stepKey="assertSelectedCategories"> + <actualResult type="variable">selectedCategories</actualResult> + <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> + </assertEquals> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductTierPriceInStock.visibility}}" stepKey="seeVisibility"/> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> + <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductTierPriceInStock.urlKey}}" stepKey="seeUrlKey"/> + + <!--Verify customer see updated virtual product link on category page --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <see selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{updateVirtualProductTierPriceInStock.name}}" stepKey="seeVirtualProductLinkOnCategoryPage"/> + + <!--Verify customer see updated virtual product with tier price on product storefront page --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductTierPriceInStock.urlKey)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductTierPriceInStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductTierPriceInStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductTierPriceInStock.sku}}" stepKey="seeVirtualProductSku"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> + <assertEquals stepKey="assertStockAvailableOnProductPage"> + <expectedResult type="string">{{updateVirtualProductTierPriceInStock.storefrontStatus}}</expectedResult> + <actualResult type="variable">productStockAvailableStatus</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">${{updateVirtualProductTierPriceInStock.price}}</expectedResult> + <actualResult type="variable">productPriceAmount</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.tierPriceText}}" stepKey="tierPriceText"/> + <assertEquals stepKey="assertTierPriceTextOnProductPage"> + <expectedResult type="string">Buy {{tierPriceOnVirtualProduct.qty}} for ${{tierPriceOnVirtualProduct.price}} each and save 38%</expectedResult> + <actualResult type="variable">tierPriceText</actualResult> + </assertEquals> + + <!--Verify customer see updated virtual product link on magento storefront page and is searchable by sku --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductTierPriceInStock.urlKey)}}" stepKey="goToMagentoStorefrontPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{updateVirtualProductTierPriceInStock.sku}}" stepKey="fillVirtualProductName"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <see selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{updateVirtualProductTierPriceInStock.name}}" stepKey="seeVirtualProductName"/> + <grabTextFrom selector="{{StorefrontQuickSearchResultsSection.asLowAsLabel}}" stepKey="tierPriceTextOnStorefrontPage"/> + <assertEquals stepKey="assertTierPriceTextOnCategoryPage"> + <expectedResult type="string">As low as ${{tierPriceOnVirtualProduct.price}}</expectedResult> + <actualResult type="variable">tierPriceTextOnStorefrontPage</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest.xml new file mode 100644 index 000000000000..717d710b4a28 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest.xml @@ -0,0 +1,151 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest"> + <annotations> + <stories value="Update Virtual Product"/> + <title value="Update Virtual Product with Tier Price (In Stock) Visible in Category Only"/> + <description value="Test log in to Update Virtual Product and Update Virtual Product with Tier Price (In Stock) Visible in Category Only"/> + <testCaseId value="MC-7508"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> + <createData entity="defaultVirtualProduct" stepKey="initialVirtualProduct"> + <requiredEntity createDataKey="initialCategoryEntity"/> + </createData> + <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> + <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search default virtual product in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> + <waitForPageLoad stepKey="waitForProductCatalogPage1"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> + <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> + <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> + <waitForPageLoad stepKey="waitForProductSearch"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + + <!-- Update virtual product with tier price(in stock) --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductWithTierPriceInStock.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductWithTierPriceInStock.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductWithTierPriceInStock.price}}" stepKey="fillProductPrice"/> + <!-- Press enter to validate advanced pricing link --> + <pressKey selector="{{AdminProductFormSection.productPrice}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ENTER]" stepKey="pressEnterKey"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> + <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="clickCustomerGroupPriceAddButton"/> + <scrollTo selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" x="50" y="0" stepKey="scrollToProductTierPriceQuantityInputTextBox"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.website}}" stepKey="selectProductTierPriceWebsiteInput"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.customer_group}}" stepKey="selectProductTierPriceCustomerGroupInput"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnVirtualProduct.qty}}" stepKey="fillProductTierPriceQuantityInput"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnVirtualProduct.price}}" stepKey="selectProductTierPriceFixedPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> + <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductWithTierPriceInStock.productTaxClass}}" stepKey="selectProductStockClass"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualProductWithTierPriceInStock.quantity}}" stepKey="fillProductQuantity"/> + <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{updateVirtualProductWithTierPriceInStock.status}}" stepKey="selectStockStatusInStock"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductWithTierPriceInStock.visibility}}" stepKey="selectVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductWithTierPriceInStock.urlKey}}" stepKey="fillUrlKey"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + <!-- Verify we see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> + + <!-- Search updated virtual product(from above step) in the grid --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> + <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductWithTierPriceInStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{updateVirtualProductWithTierPriceInStock.sku}}" stepKey="fillVirtualProductSku"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedVirtualProductVisibleInGrid"/> + <waitForPageLoad stepKey="waitUntilVirtualProductPageIsOpened"/> + + <!-- Verify customer see updated virtual product with tier price(from the above step) in the product form page --> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualProductWithTierPriceInStock.name}}" stepKey="seeProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualProductWithTierPriceInStock.sku}}" stepKey="seeProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualProductWithTierPriceInStock.price}}" stepKey="seeProductPrice"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink1"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.website}}" stepKey="seeProductTierPriceWebsiteInput"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.customer_group}}" stepKey="seeProductTierPriceCustomerGroupInput"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnVirtualProduct.qty}}" stepKey="seeProductTierPriceQuantityInput"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnVirtualProduct.price}}" stepKey="seeProductTierPriceFixedPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.advancedPricingCloseButton}}" stepKey="clickAdvancedPricingCloseButton"/> + <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualProductWithTierPriceInStock.productTaxClass}}" stepKey="seeProductTaxClass"/> + <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualProductWithTierPriceInStock.quantity}}" stepKey="seeProductQuantity"/> + <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{updateVirtualProductWithTierPriceInStock.status}}" stepKey="seeProductStockStatus"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> + <grabMultiple selector="{{AdminProductFormSection.selectMultipleCategories}}" stepKey="selectedCategories" /> + <assertEquals stepKey="assertSelectedCategories"> + <actualResult type="variable">selectedCategories</actualResult> + <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> + </assertEquals> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductWithTierPriceInStock.visibility}}" stepKey="seeVisibility"/> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> + <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductWithTierPriceInStock.urlKey}}" stepKey="seeUrlKey"/> + + <!--Verify customer see updated virtual product link on category page --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <see selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{updateVirtualProductWithTierPriceInStock.name}}" stepKey="seeVirtualProductNameOnCategoryPage"/> + + <!--Verify customer see updated virtual product with tier price(from above step) on product storefront page --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductWithTierPriceInStock.urlKey)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductWithTierPriceInStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductWithTierPriceInStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductWithTierPriceInStock.sku}}" stepKey="seeVirtualProductSku"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> + <assertEquals stepKey="assertStockAvailableOnProductPage"> + <expectedResult type="string">{{updateVirtualProductWithTierPriceInStock.storefrontStatus}}</expectedResult> + <actualResult type="variable">productStockAvailableStatus</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">${{updateVirtualProductWithTierPriceInStock.price}}</expectedResult> + <actualResult type="variable">productPriceAmount</actualResult> + </assertEquals> + <!-- Verify customer see product tier price on product page --> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.tierPriceText}}" stepKey="tierPriceText"/> + <assertEquals stepKey="assertTierPriceTextOnProductPage"> + <expectedResult type="string">Buy {{tierPriceOnVirtualProduct.qty}} for ${{tierPriceOnVirtualProduct.price}} each and save 10%</expectedResult> + <actualResult type="variable">tierPriceText</actualResult> + </assertEquals> + + <!--Verify customer don't see updated virtual product link on magento storefront page and is searchable by sku --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductWithTierPriceInStock.urlKey)}}" stepKey="goToMagentoStorefrontPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{updateVirtualProductTierPriceInStock.sku}}" stepKey="fillVirtualProductName"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <dontSee selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{updateVirtualProductWithTierPriceInStock.name}}" stepKey="dontSeeVirtualProductName"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest.xml new file mode 100644 index 000000000000..703a4e24cdca --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest.xml @@ -0,0 +1,151 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest"> + <annotations> + <stories value="Update Virtual Product"/> + <title value="Update Virtual Product with Tier Price (Out of Stock) Visible in Category and Search"/> + <description value="Test log in to Update Virtual Product and Update Virtual Product with Tier Price (Out of Stock) Visible in Category and Search"/> + <testCaseId value="MC-6499"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> + <createData entity="defaultVirtualProduct" stepKey="initialVirtualProduct"> + <requiredEntity createDataKey="initialCategoryEntity"/> + </createData> + <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> + <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search default virtual product in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> + <waitForPageLoad stepKey="waitForProductCatalogPage1"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> + <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> + <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> + <waitForPageLoad stepKey="waitForProductSearch"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + + <!-- Update virtual product with tier price(out of stock) --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualTierPriceOutOfStock.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualTierPriceOutOfStock.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualTierPriceOutOfStock.price}}" stepKey="fillProductPrice"/> + <!-- Press enter to validate advanced pricing link --> + <pressKey selector="{{AdminProductFormSection.productPrice}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ENTER]" stepKey="pressEnterKey"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink"/> + <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="clickCustomerGroupPriceAddButton"/> + <scrollTo selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" x="50" y="0" stepKey="scrollToProductTierPriceQuantityInputTextBox"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.website}}" stepKey="selectProductTierPriceWebsiteInput"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.customer_group}}" stepKey="selectProductTierPriceCustomerGroupInput"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnVirtualProduct.qty}}" stepKey="fillProductTierPriceQuantityInput"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnVirtualProduct.price}}" stepKey="selectProductTierPriceFixedPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> + <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualTierPriceOutOfStock.productTaxClass}}" stepKey="selectProductStockClass"/> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualTierPriceOutOfStock.quantity}}" stepKey="fillProductQuantity"/> + <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{updateVirtualTierPriceOutOfStock.status}}" stepKey="selectStockStatusInStock"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$initialCategoryEntity.name$$" stepKey="fillSearchForInitialCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$initialCategoryEntity.name$$)}}" stepKey="unselectInitialCategory"/> + <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> + <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualTierPriceOutOfStock.visibility}}" stepKey="selectVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualTierPriceOutOfStock.urlKey}}" stepKey="fillUrlKey"/> + <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + <!-- Verify we see success message --> + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> + + <!-- Search updated virtual product(from above step) in the grid page --> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> + <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualTierPriceOutOfStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{updateVirtualTierPriceOutOfStock.sku}}" stepKey="fillVirtualProductSku"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyUpdatedVirtualProductVisibleInGrid"/> + <waitForPageLoad stepKey="waitUntilVirtualProductPageIsOpened"/> + + <!-- Verify we customer see updated virtual product with tier price(from the above step) in the product form page --> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{updateVirtualTierPriceOutOfStock.name}}" stepKey="seeProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{updateVirtualTierPriceOutOfStock.sku}}" stepKey="seeProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{updateVirtualTierPriceOutOfStock.price}}" stepKey="seeProductPrice"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickAdvancedPricingLink1"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.website}}" stepKey="seeProductTierPriceWebsiteInput"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect('0')}}" userInput="{{tierPriceOnVirtualProduct.customer_group}}" stepKey="seeProductTierPriceCustomerGroupInput"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="{{tierPriceOnVirtualProduct.qty}}" stepKey="seeProductTierPriceQuantityInput"/> + <seeInField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceFixedPriceInput('0')}}" userInput="{{tierPriceOnVirtualProduct.price}}" stepKey="seeProductTierPriceFixedPrice"/> + <click selector="{{AdminProductFormAdvancedPricingSection.advancedPricingCloseButton}}" stepKey="clickAdvancedPricingCloseButton"/> + <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{updateVirtualTierPriceOutOfStock.productTaxClass}}" stepKey="seeProductTaxClass"/> + <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{updateVirtualTierPriceOutOfStock.quantity}}" stepKey="seeProductQuantity"/> + <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{updateVirtualTierPriceOutOfStock.status}}" stepKey="seeProductStockStatus"/> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> + <grabMultiple selector="{{AdminProductFormSection.selectMultipleCategories}}" stepKey="selectedCategories" /> + <assertEquals stepKey="assertSelectedCategories"> + <actualResult type="variable">selectedCategories</actualResult> + <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> + </assertEquals> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualTierPriceOutOfStock.visibility}}" stepKey="seeVisibility"/> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> + <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualTierPriceOutOfStock.urlKey}}" stepKey="seeUrlKey"/> + + <!--Verify customer don't see updated virtual product link on category page --> + <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <dontSee selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{updateVirtualTierPriceOutOfStock.name}}" stepKey="dontSeeVirtualProductNameOnCategoryPage"/> + + <!--Verify customer see updated virtual product with tier price(from above step) on product storefront page --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualTierPriceOutOfStock.urlKey)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualTierPriceOutOfStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualTierPriceOutOfStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualTierPriceOutOfStock.sku}}" stepKey="seeVirtualProductSku"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> + <assertEquals stepKey="assertStockAvailableOnProductPage"> + <expectedResult type="string">{{updateVirtualTierPriceOutOfStock.storefrontStatus}}</expectedResult> + <actualResult type="variable">productStockAvailableStatus</actualResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="productPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">${{updateVirtualTierPriceOutOfStock.price}}</expectedResult> + <actualResult type="variable">productPriceAmount</actualResult> + </assertEquals> + <!-- Verify customer see product tier price on product page --> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.tierPriceText}}" stepKey="tierPriceText"/> + <assertEquals stepKey="assertTierPriceTextOnProductPage"> + <expectedResult type="string">Buy {{tierPriceOnVirtualProduct.qty}} for ${{tierPriceOnVirtualProduct.price}} each and save 51%</expectedResult> + <actualResult type="variable">tierPriceText</actualResult> + </assertEquals> + + <!--Verify customer don't see updated virtual product link on magento storefront page and is searchable by sku --> + <amOnPage url="{{StorefrontProductPage.url(updateVirtualTierPriceOutOfStock.urlKey)}}" stepKey="goToMagentoStorefrontPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{updateVirtualTierPriceOutOfStock.sku}}" stepKey="fillVirtualProductName"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <dontSee selector="{{StorefrontQuickSearchResultsSection.productLink}}" userInput="{{updateVirtualTierPriceOutOfStock.name}}" stepKey="dontSeeVirtualProductName"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyProductOrderTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyProductOrderTest.xml new file mode 100644 index 000000000000..a81c26b6e6ea --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyProductOrderTest.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminVerifyProductOrder"> + <annotations> + <features value="Catalog"/> + <stories value="Verify Product Order"/> + <title value="Admin should see product types in specified order"/> + <description value="Product Type Order should be Simple -> Configurable -> Grouped -> Virtual -> Bundle -> Downloadable -> EE"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14207"/> + <group value="mtf_migrated"/> + <group value="product"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="GoToProductCatalogPage" stepKey="goToProductCatalogPage"/> + <actionGroup ref="VerifyProductTypeOrder" stepKey="verifyProductTypeOrder"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest.xml index 0eb8f5668751..84c3f81ef6db 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdvanceCatalogSearchVirtualProductByNameTest" extends="AdvanceCatalogSearchSimpleProductByNameTest"> <annotations> <features value="Catalog"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml new file mode 100644 index 000000000000..cee40241185b --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml @@ -0,0 +1,338 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="CheckTierPricingOfProductsTest"> + <annotations> + <features value="Shopping Cart"/> + <stories value="MAGETWO-91697 - [Magento Cloud] 'Tier Pricing' of Products changes to 'Price' (without discount) after Updated Items and Quantities in the Order of B2B Store View."/> + <title value="Checking 'Tier Pricing' of Products and 'Price' (without discount) in the Order of B2B Store View"/> + <description value="Checking 'Tier Pricing' of Products and 'Price' (without discount) in the Order of B2B Store View"/> + <testCaseId value="MAGETWO-94111"/> + <severity value="CRITICAL"/> + <group value="Shopping Cart"/> + </annotations> + + <before> + <createData entity="_defaultCategory" stepKey="category"/> + <createData entity="SimpleProduct" stepKey="product1"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="SimpleProduct" stepKey="product2"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="SimpleProduct" stepKey="product3"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="SimpleProduct" stepKey="product4"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="customer"/> + <!--Login as admin--> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + + <!--Create website, Sore adn Store View--> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="AdminCreateWebsite"> + <argument name="newWebsiteName" value="secondWebsite"/> + <argument name="websiteCode" value="second_website"/> + </actionGroup> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="AdminCreateStore"> + <argument name="website" value="secondWebsite"/> + <argument name="storeGroupName" value="secondStore"/> + <argument name="storeGroupCode" value="second_store"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="AdminCreateStoreView"> + <argument name="StoreGroup" value="customStoreTierPrice"/> + <argument name="customStore" value="customStoreView"/> + </actionGroup> + <!--Set Configuration--> + <createData entity="CatalogPriceScopeWebsite" stepKey="paymentMethodsSettingConfig"/> + <!--Set advanced pricing for all 4 products--> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProduct1"> + <argument name="product" value="$$product1$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct1"> + <argument name="product" value="$$product1$$"/> + </actionGroup> + <actionGroup ref="ProductSetWebsite" stepKey="ProductSetWebsite"> + <argument name="website" value="secondWebsite"/> + </actionGroup> + <actionGroup ref="ProductSetAdvancedPricing" stepKey="ProductSetAdvancedPricing1"> + <argument name="website" value="secondWebsite"/> + </actionGroup> + + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProduct2"> + <argument name="product" value="$$product2$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct2"> + <argument name="product" value="$$product2$$"/> + </actionGroup> + <actionGroup ref="ProductSetWebsite" stepKey="ProductSetWebsite2"> + <argument name="website" value="secondWebsite"/> + </actionGroup> + <actionGroup ref="ProductSetAdvancedPricing" stepKey="ProductSetAdvancedPricing2"> + <argument name="website" value="secondWebsite"/> + </actionGroup> + + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProduct3"> + <argument name="product" value="$$product3$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct3"> + <argument name="product" value="$$product3$$"/> + </actionGroup> + <actionGroup ref="ProductSetWebsite" stepKey="ProductSetWebsite3"> + <argument name="website" value="secondWebsite"/> + </actionGroup> + <actionGroup ref="ProductSetAdvancedPricing" stepKey="ProductSetAdvancedPricing3"> + <argument name="website" value="secondWebsite"/> + </actionGroup> + + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProduct4"> + <argument name="product" value="$$product4$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct4"> + <argument name="product" value="$$product4$$"/> + </actionGroup> + <actionGroup ref="ProductSetWebsite" stepKey="ProductSetWebsite4"> + <argument name="website" value="secondWebsite"/> + </actionGroup> + <actionGroup ref="ProductSetAdvancedPricing" stepKey="ProductSetAdvancedPricing4"> + <argument name="website" value="secondWebsite"/> + </actionGroup> + <actionGroup ref="ClearProductsFilterActionGroup" stepKey="ClearProductsFilterActionGroup"/> + + <!--Flush cache--> + <magentoCLI command="cache:flush" stepKey="cleanCache"/> + + + <!--Edit customer info--> + <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="OpenEditCustomerFrom"> + <argument name="customer" value="$$customer$$"/> + </actionGroup> + <click selector="{{AdminCustomerAccountInformationSection.accountInformationButton}}" stepKey="ClickOnAccountInformationSection"/> + <waitForPageLoad stepKey="waitForPageOpened1"/> + <selectOption selector="{{AdminCustomerAccountInformationSection.group}}" userInput="Retailer" stepKey="Group"/> + <selectOption selector="{{AdminCustomerAccountInformationSection.storeView}}" userInput="secondStoreView" stepKey="clickToSelectStore"/> + <click selector="{{AdminCustomerAccountInformationSection.saveCustomer}}" stepKey="save"/> + <waitForPageLoad stepKey="waitForCustomersPage"/> + <see userInput="You saved the customer." stepKey="CustomerIsSaved"/> + + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomers"/> + <waitForPageLoad stepKey="waitForPageLoad1" /> + <click selector="{{AdminCustomerFiltersSection.clearAll}}" stepKey="ClearFilters"/> + <waitForPageLoad stepKey="waitForFiltersClear"/> + + <!--Create Cart Price Rule--> + <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> + <waitForPageLoad stepKey="waitForPriceList"/> + <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> + <waitForPageLoad stepKey="waitForPageDiscountPageIsLoaded"/> + <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="ship" stepKey="fillRuleName"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="secondWebsite" stepKey="selectWebsites"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.customerGroups}}" userInput="Retailer" stepKey="selectCustomerGroup"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.coupon}}" userInput="Specific Coupon" stepKey="selectCouponType"/> + <fillField selector="{{AdminCartPriceRulesFormSection.couponCode}}" userInput="ship" stepKey="setCode"/> + <fillField selector="{{AdminCartPriceRulesFormSection.userPerCustomer}}" userInput="0" stepKey="setUserPerCustomer"/> + <fillField selector="{{AdminCartPriceRulesFormSection.userPerCoupon}}" userInput="0" stepKey="setUserPerCoupon"/> + <fillField selector="{{AdminCartPriceRulesFormSection.priority}}" userInput="0" stepKey="setPriority"/> + <click selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="clickToExpandActions"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.freeShipping}}" userInput="For shipment with matching items" stepKey="selectFreeShippingType"/> + <click selector="{{AdminCartPriceRulesFormSection.saveAndContinue}}" stepKey="clickSaveAndContinueButton"/> + <waitForPageLoad stepKey="waitForCartPriceRuleSaved"/> + <see userInput="You saved the rule." stepKey="RuleSaved"/> + + <!--Create new order--> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="CreateNewOrder"> + <argument name="customer" value="Simple_US_Customer"/> + <argument name="storeView" value="customStoreView"/> + </actionGroup> + + <click selector="{{OrdersGridSection.addProducts}}" stepKey="clickToAddProduct"/> + <waitForPageLoad stepKey="waitForProductsOpened"/> + <!--TEST CASE #1--> + <!--Add 3 products to order with specified quantity--> + <click selector="{{OrdersGridSection.selectProduct($$product1.name$$)}}" stepKey="selectProduct1"/> + <fillField selector="{{OrdersGridSection.setQuantity($$product1.name$$)}}" userInput="10" stepKey="AddProductQuantity1"/> + + <click selector="{{OrdersGridSection.selectProduct($$product2.name$$)}}" stepKey="selectProduct2"/> + <fillField selector="{{OrdersGridSection.setQuantity($$product2.name$$)}}" userInput="10" stepKey="AddProductQuantity2"/> + + <click selector="{{OrdersGridSection.selectProduct($$product3.name$$)}}" stepKey="selectProduct3"/> + <fillField selector="{{OrdersGridSection.setQuantity($$product3.name$$)}}" userInput="10" stepKey="AddProductQuantity3"/> + <click stepKey="addProductsToOrder" selector="{{OrdersGridSection.addProductsToOrder}}"/> + <waitForLoadingMaskToDisappear stepKey="wait6"/> + <!--Verify tier price values--> + <grabTextFrom selector="{{OrdersGridSection.productPrice($$product1.name$$)}}" stepKey="checkProductPrice1"/> + <assertEquals stepKey="verifyPrice1"> + <expectedResult type="string">{{testDataTierPrice.goldenPrice1}}</expectedResult> + <actualResult type="variable">$checkProductPrice1</actualResult> + </assertEquals> + + <grabTextFrom selector="{{OrdersGridSection.productPrice($$product2.name$$)}}" stepKey="checkProductPrice2"/> + <assertEquals stepKey="verifyPrice2"> + <expectedResult type="string">{{testDataTierPrice.goldenPrice1}}</expectedResult> + <actualResult type="variable">$checkProductPrice2</actualResult> + </assertEquals> + + <grabTextFrom selector="{{OrdersGridSection.productPrice($$product3.name$$)}}" stepKey="checkProductPrice3"/> + <assertEquals stepKey="verifyPrice3"> + <expectedResult type="string">{{testDataTierPrice.goldenPrice1}}</expectedResult> + <actualResult type="variable">$checkProductPrice3</actualResult> + </assertEquals> + + <!--Edit order and verify values--> + <waitForPageLoad stepKey="waitForPageLoaded2"/> + <click selector="{{OrdersGridSection.customPrice($$product1.name$$)}}" stepKey="ClickOnCustomPrice"/> + <fillField selector="{{OrdersGridSection.customQuantity($$product1.name$$)}}" userInput="5" stepKey="ClickOnQuantity"/> + <waitForLoadingMaskToDisappear stepKey="wait1"/> + <click selector="{{OrdersGridSection.update}}" stepKey="ClickToUpdate"/> + <waitForLoadingMaskToDisappear stepKey="wait2"/> + <grabTextFrom selector="{{OrdersGridSection.productPrice($$product1.name$$)}}" stepKey="checkProductPrice4"/> + <assertEquals stepKey="verifyPrice4"> + <expectedResult type="string">{{testDataTierPrice.goldenPrice2}}</expectedResult> + <actualResult type="variable">$checkProductPrice4</actualResult> + </assertEquals> + + <grabTextFrom selector="{{OrdersGridSection.productPrice($$product2.name$$)}}" stepKey="checkProductPrice5"/> + <assertEquals stepKey="verifyPrice5"> + <expectedResult type="string">{{testDataTierPrice.goldenPrice1}}</expectedResult> + <actualResult type="variable">$checkProductPrice5</actualResult> + </assertEquals> + <grabTextFrom selector="{{OrdersGridSection.productPrice($$product3.name$$)}}" stepKey="checkProductPrice6"/> + <assertEquals stepKey="verifyPrice6"> + <expectedResult type="string">{{testDataTierPrice.goldenPrice1}}</expectedResult> + <actualResult type="variable">$checkProductPrice3</actualResult> + </assertEquals> + + <!--Remove products from order--> + <selectOption selector="{{OrdersGridSection.removeItems($$product1.name$$)}}" userInput="Remove" stepKey="clickToRemove1"/> + <selectOption selector="{{OrdersGridSection.removeItems($$product2.name$$)}}" userInput="Remove" stepKey="clickToRemove2"/> + <selectOption selector="{{OrdersGridSection.removeItems($$product3.name$$)}}" userInput="Remove" stepKey="clickToRemove3"/> + <waitForLoadingMaskToDisappear stepKey="wait3"/> + <click selector="{{OrdersGridSection.update}}" stepKey="ClickToUpdate1"/> + <waitForPageLoad stepKey="WaitProductsDeleted"/> + + <!--TEST CASE #2--> + <!--Add 3 products to order with specified quantity--> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <click stepKey="clickToAddProduct1" selector="{{OrdersGridSection.addProducts}}"/> + <click selector="{{OrdersGridSection.selectProduct($$product1.name$$)}}" stepKey="selectProduct5"/> + <fillField selector="{{OrdersGridSection.setQuantity($$product1.name$$)}}" userInput="10" stepKey="AddProductQuantity5"/> + + <click selector="{{OrdersGridSection.selectProduct($$product2.name$$)}}" stepKey="selectProduct6"/> + <fillField selector="{{OrdersGridSection.setQuantity($$product2.name$$)}}" userInput="10" stepKey="AddProductQuantity6"/> + + <click selector="{{OrdersGridSection.selectProduct($$product3.name$$)}}" stepKey="selectProduct7"/> + <fillField selector="{{OrdersGridSection.setQuantity($$product3.name$$)}}" userInput="10" stepKey="AddProductQuantity7"/> + <click stepKey="addProductsToOrder1" selector="{{OrdersGridSection.addProductsToOrder}}"/> + <waitForLoadingMaskToDisappear stepKey="wait7"/> + <!--Verify tier price values--> + <grabTextFrom selector="{{OrdersGridSection.productPrice($$product1.name$$)}}" stepKey="checkProductPrice7"/> + <assertEquals stepKey="verifyPrice7"> + <expectedResult type="string">{{testDataTierPrice.goldenPrice1}}</expectedResult> + <actualResult type="variable">$checkProductPrice7</actualResult> + </assertEquals> + + <grabTextFrom selector="{{OrdersGridSection.productPrice($$product2.name$$)}}" stepKey="checkProductPrice8"/> + <assertEquals stepKey="verifyPrice8"> + <expectedResult type="string">{{testDataTierPrice.goldenPrice1}}</expectedResult> + <actualResult type="variable">$checkProductPrice8</actualResult> + </assertEquals> + + <grabTextFrom selector="{{OrdersGridSection.productPrice($$product3.name$$)}}" stepKey="checkProductPrice9"/> + <assertEquals stepKey="verifyPrice9"> + <expectedResult type="string">{{testDataTierPrice.goldenPrice1}}</expectedResult> + <actualResult type="variable">$checkProductPrice9</actualResult> + </assertEquals> + + <!--Add one more product and verify values--> + <waitForPageLoad stepKey="waitForPgeLoaded3"/> + <click selector="{{OrdersGridSection.addProducts}}" stepKey="clickToAddProduct2"/> + <waitForLoadingMaskToDisappear stepKey="wait8"/> + <click selector="{{OrdersGridSection.selectProduct($$product4.name$$)}}" stepKey="selectProduct8"/> + <fillField selector="{{OrdersGridSection.setQuantity($$product4.name$$)}}" userInput="10" stepKey="AddProductQuantity9"/> + <click selector="{{OrdersGridSection.addProductsToOrder}}" stepKey="addProductsToOrder2"/> + <waitForLoadingMaskToDisappear stepKey="wait9"/> + <grabTextFrom selector="{{OrdersGridSection.productPrice($$product4.name$$)}}" stepKey="checkProductPrice10"/> + <assertEquals stepKey="verifyPrice10"> + <expectedResult type="string">{{testDataTierPrice.goldenPrice1}}</expectedResult> + <actualResult type="variable">$checkProductPrice10</actualResult> + </assertEquals> + + <grabTextFrom selector="{{OrdersGridSection.productPrice($$product1.name$$)}}" stepKey="checkProductPrice11"/> + <assertEquals stepKey="verifyPrice11"> + <expectedResult type="string">{{testDataTierPrice.goldenPrice1}}</expectedResult> + <actualResult type="variable">$checkProductPrice11</actualResult> + </assertEquals> + + <grabTextFrom selector="{{OrdersGridSection.productPrice($$product2.name$$)}}" stepKey="checkProductPrice12"/> + <assertEquals stepKey="verifyPrice12"> + <expectedResult type="string">{{testDataTierPrice.goldenPrice1}}</expectedResult> + <actualResult type="variable">$checkProductPrice12</actualResult> + </assertEquals> + + <grabTextFrom selector="{{OrdersGridSection.productPrice($$product3.name$$)}}" stepKey="checkProductPrice13"/> + <assertEquals stepKey="verifyPrice13"> + <expectedResult type="string">{{testDataTierPrice.goldenPrice1}}</expectedResult> + <actualResult type="variable">$checkProductPrice13</actualResult> + </assertEquals> + + <selectOption selector="{{OrdersGridSection.removeItems($$product1.name$$)}}" userInput="Remove" stepKey="clickToRemove4"/> + <selectOption selector="{{OrdersGridSection.removeItems($$product2.name$$)}}" userInput="Remove" stepKey="clickToRemove5"/> + <selectOption selector="{{OrdersGridSection.removeItems($$product3.name$$)}}" userInput="Remove" stepKey="clickToRemove6"/> + <waitForLoadingMaskToDisappear stepKey="wait4"/> + <click selector="{{OrdersGridSection.update}}" stepKey="ClickToUpdate2"/> + <waitForLoadingMaskToDisappear stepKey="wait10"/> + + <!--TEST CASE #3--> + <waitForPageLoad stepKey="WaitProductsDeleted1"/> + <scrollToTopOfPage stepKey="scrollToTopOfPage1"/> + <click selector="{{OrdersGridSection.addProducts}}" stepKey="clickToAddProduct4" /> + <click selector="{{OrdersGridSection.selectProduct($$product1.name$$)}}" stepKey="selectProduct9"/> + <fillField selector="{{OrdersGridSection.setQuantity($$product1.name$$)}}" userInput="10" stepKey="AddProductQuantity10"/> + <click selector="{{OrdersGridSection.addProductsToOrder}}" stepKey="addProductsToOrder3"/> + <waitForLoadingMaskToDisappear stepKey="wait11"/> + <fillField selector="{{OrdersGridSection.applyCoupon}}" userInput="ship" stepKey="AddCouponCode"/> + <waitForLoadingMaskToDisappear stepKey="wait5"/> + <click selector="{{OrdersGridSection.update}}" stepKey="ClickToUpdate3"/> + <waitForLoadingMaskToDisappear stepKey="wait12"/> + <grabTextFrom selector="{{OrdersGridSection.productPrice($$product1.name$$)}}" stepKey="checkProductPrice14"/> + <grabTextFrom selector="{{OrdersGridSection.productPrice($$product4.name$$)}}" stepKey="checkProductPrice15"/> + <assertEquals stepKey="verifyPrice14"> + <expectedResult type="string">{{testDataTierPrice.goldenPrice1}}</expectedResult> + <actualResult type="variable">$checkProductPrice14</actualResult> + </assertEquals> + <assertEquals stepKey="verifyPrice15"> + <expectedResult type="string">{{testDataTierPrice.goldenPrice1}}</expectedResult> + <actualResult type="variable">$checkProductPrice15</actualResult> + </assertEquals> + + <after> + <deleteData createDataKey="product1" stepKey="deleteProduct1"/> + <deleteData createDataKey="product2" stepKey="deleteProduct2"/> + <deleteData createDataKey="product3" stepKey="deleteProduct3"/> + <deleteData createDataKey="product4" stepKey="deleteProduct4"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + <createData entity="DefaultConfigCatalogPrice" stepKey="defaultConfigCatalogPrice"/> + <actionGroup ref="DeleteCartPriceRuleByName" stepKey="cleanUpRule"> + <argument name="ruleName" value="ship"/> + </actionGroup> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="DeleteWebsite"> + <argument name="websiteName" value="secondWebsite"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + + <!--Do reindex and flush cache--> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </after> + </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 000000000000..d895993217e3 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest.xml @@ -0,0 +1,425 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="CreateProductAttributeEntityTextFieldTest"> + <annotations> + <features value="Catalog"/> + <stories value="Create Product Attributes"/> + <title value="Admin should be able to create a TextField product attribute"/> + <description value="Admin should be able to create a TextField product attribute"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10894"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage"> + <argument name="ProductAttribute" value="{{textProductAttribute.attribute_code}}"/> + </actionGroup> + <click stepKey="clickDelete" selector="{{AttributePropertiesSection.DeleteAttribute}}"/> + <click stepKey="clickOk" selector="{{AttributeDeleteModalSection.confirm}}"/> + <waitForPageLoad stepKey="waitForDeletion"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Navigate to Stores > Attributes > Product.--> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> + + <!--Create new Product Attribute as TextField, with code and default value.--> + <actionGroup ref="createProductAttributeWithTextField" stepKey="createAttribute"> + <argument name="attribute" value="textProductAttribute"/> + </actionGroup> + + <!--Navigate to Product Attribute.--> + <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage"> + <argument name="ProductAttribute" value="{{textProductAttribute.attribute_code}}"/> + </actionGroup> + + <!--Perform appropriate assertions against textProductAttribute entity--> + <seeInField stepKey="assertLabel" selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{textProductAttribute.attribute_code}}"/> + <seeOptionIsSelected stepKey="assertInputType" selector="{{AttributePropertiesSection.InputType}}" userInput="{{textProductAttribute.frontend_input}}"/> + <seeOptionIsSelected stepKey="assertRequired" selector="{{AttributePropertiesSection.ValueRequired}}" userInput="{{textProductAttribute.is_required_admin}}"/> + <seeInField stepKey="assertAttrCode" selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" userInput="{{textProductAttribute.attribute_code}}"/> + <seeInField stepKey="assertDefaultValue" selector="{{AdvancedAttributePropertiesSection.DefaultValueText}}" userInput="{{textProductAttribute.default_value}}"/> + + <!--Go to New Product page, add Attribute and check values--> + <amOnPage url="{{AdminProductCreatePage.url('4', 'simple')}}" stepKey="goToCreateSimpleProductPage"/> + <actionGroup ref="addProductAttributeInProductModal" stepKey="addAttributeToProduct"> + <argument name="attributeCode" value="{{textProductAttribute.attribute_code}}"/> + </actionGroup> + <click stepKey="openAttributes" selector="{{AdminProductAttributesSection.sectionHeader}}"/> + <waitForElementVisible selector="{{AdminProductAttributesSection.attributeTextInputByCode(textProductAttribute.attribute_code)}}" stepKey="waitforLabel"/> + <seeInField stepKey="checkDefaultValue" selector="{{AdminProductAttributesSection.attributeTextInputByCode(textProductAttribute.attribute_code)}}" userInput="{{textProductAttribute.default_value}}"/> + <see stepKey="checkLabel" selector="{{AdminProductAttributesSection.attributeLabelByCode(textProductAttribute.attribute_code)}}" userInput="{{textProductAttribute.attribute_code}}"/> + </test> + + <test name="CreateProductAttributeEntityDateTest"> + <annotations> + <features value="Catalog"/> + <stories value="Create Product Attributes"/> + <title value="Admin should be able to create a Date product attribute"/> + <description value="Admin should be able to create a Date product attribute"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10895"/> + <group value="Catalog"/> + <skip> + <issueId value="MC-13817"/> + </skip> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage"> + <argument name="ProductAttribute" value="{{dateProductAttribute.attribute_code}}"/> + </actionGroup> + <click stepKey="clickDelete" selector="{{AttributePropertiesSection.DeleteAttribute}}"/> + <click stepKey="clickOk" selector="{{AttributeDeleteModalSection.confirm}}"/> + <waitForPageLoad stepKey="waitForDeletion"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Generate date for use as default value, needs to be MM/d/YYYY --> + <generateDate date="now" format="m/j/Y" stepKey="generateDefaultDate"/> + + <!--Navigate to Stores > Attributes > Product.--> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> + + <!--Create new Product Attribute as TextField, with code and default value.--> + <actionGroup ref="createProductAttributeWithDateField" stepKey="createAttribute"> + <argument name="attribute" value="dateProductAttribute"/> + <argument name="date" value="{$generateDefaultDate}"/> + </actionGroup> + + <!--Navigate to Product Attribute.--> + <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage"> + <argument name="ProductAttribute" value="{{dateProductAttribute.attribute_code}}"/> + </actionGroup> + + <!--Perform appropriate assertions against textProductAttribute entity--> + <seeInField stepKey="assertLabel" selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{dateProductAttribute.attribute_code}}"/> + <seeOptionIsSelected stepKey="assertInputType" selector="{{AttributePropertiesSection.InputType}}" userInput="{{dateProductAttribute.frontend_input}}"/> + <seeOptionIsSelected stepKey="assertRequired" selector="{{AttributePropertiesSection.ValueRequired}}" userInput="{{dateProductAttribute.is_required_admin}}"/> + <seeInField stepKey="assertAttrCode" selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" userInput="{{dateProductAttribute.attribute_code}}"/> + <seeInField stepKey="assertDefaultValue" selector="{{AdvancedAttributePropertiesSection.DefaultValueDate}}" userInput="{$generateDefaultDate}"/> + + <!--Go to New Product page, add Attribute and check values--> + <amOnPage url="{{AdminProductCreatePage.url('4', 'simple')}}" stepKey="goToCreateSimpleProductPage"/> + <actionGroup ref="addProductAttributeInProductModal" stepKey="addAttributeToProduct"> + <argument name="attributeCode" value="{{dateProductAttribute.attribute_code}}"/> + </actionGroup> + <click stepKey="openAttributes" selector="{{AdminProductAttributesSection.sectionHeader}}"/> + <waitForElementVisible selector="{{AdminProductAttributesSection.attributeTextInputByCode(dateProductAttribute.attribute_code)}}" stepKey="waitforLabel"/> + <seeInField stepKey="checkDefaultValue" selector="{{AdminProductAttributesSection.attributeTextInputByCode(dateProductAttribute.attribute_code)}}" userInput="{$generateDefaultDate}"/> + <see stepKey="checkLabel" selector="{{AdminProductAttributesSection.attributeLabelByCode(dateProductAttribute.attribute_code)}}" userInput="{{dateProductAttribute.attribute_code}}"/> + </test> + + <test name="CreateProductAttributeEntityPriceTest"> + <annotations> + <features value="Catalog"/> + <stories value="Create Product Attributes"/> + <title value="Admin should be able to create a Price product attribute"/> + <description value="Admin should be able to create a Price product attribute"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10897"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage"> + <argument name="ProductAttribute" value="{{priceProductAttribute.attribute_code}}"/> + </actionGroup> + <click stepKey="clickDelete" selector="{{AttributePropertiesSection.DeleteAttribute}}"/> + <click stepKey="clickOk" selector="{{AttributeDeleteModalSection.confirm}}"/> + <waitForPageLoad stepKey="waitForDeletion"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Navigate to Stores > Attributes > Product.--> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> + + <!--Create new Product Attribute with Price--> + <actionGroup ref="createProductAttribute" stepKey="createAttribute"> + <argument name="attribute" value="priceProductAttribute"/> + </actionGroup> + + <!--Navigate to Product Attribute.--> + <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage"> + <argument name="ProductAttribute" value="{{priceProductAttribute.attribute_code}}"/> + </actionGroup> + + <!--Perform appropriate assertions against priceProductAttribute entity--> + <seeInField stepKey="assertLabel" selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{priceProductAttribute.attribute_code}}"/> + <seeOptionIsSelected stepKey="assertInputType" selector="{{AttributePropertiesSection.InputType}}" userInput="{{priceProductAttribute.frontend_input}}"/> + <seeOptionIsSelected stepKey="assertRequired" selector="{{AttributePropertiesSection.ValueRequired}}" userInput="{{priceProductAttribute.is_required_admin}}"/> + <seeInField stepKey="assertAttrCode" selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" userInput="{{priceProductAttribute.attribute_code}}"/> + + <!--Go to New Product page, add Attribute and check values--> + <amOnPage url="{{AdminProductCreatePage.url('4', 'simple')}}" stepKey="goToCreateSimpleProductPage"/> + <actionGroup ref="addProductAttributeInProductModal" stepKey="addAttributeToProduct"> + <argument name="attributeCode" value="{{priceProductAttribute.attribute_code}}"/> + </actionGroup> + <click stepKey="openAttributes" selector="{{AdminProductAttributesSection.sectionHeader}}"/> + <waitForElementVisible selector="{{AdminProductAttributesSection.attributeTextInputByCode(priceProductAttribute.attribute_code)}}" stepKey="waitforLabel"/> + <see stepKey="checkLabel" selector="{{AdminProductAttributesSection.attributeLabelByCode(priceProductAttribute.attribute_code)}}" userInput="{{priceProductAttribute.attribute_code}}"/> + </test> + + <test name="CreateProductAttributeEntityDropdownTest"> + <annotations> + <features value="Catalog"/> + <stories value="Create Product Attributes"/> + <title value="Admin should be able to create a Dropdown product attribute"/> + <description value="Admin should be able to create a Dropdown product attribute"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10896"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage"> + <argument name="ProductAttribute" value="{{dropdownProductAttribute.attribute_code}}"/> + </actionGroup> + <click stepKey="clickDelete" selector="{{AttributePropertiesSection.DeleteAttribute}}"/> + <click stepKey="clickOk" selector="{{AttributeDeleteModalSection.confirm}}"/> + <waitForPageLoad stepKey="waitForDeletion"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Navigate to Stores > Attributes > Product.--> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> + + <!--Create new Product Attribute as TextField, with code and default value.--> + <actionGroup ref="createProductAttribute" stepKey="createAttribute"> + <argument name="attribute" value="dropdownProductAttribute"/> + </actionGroup> + + <!--Navigate to Product Attribute, add Product Options and Save - 1--> + <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage1"> + <argument name="ProductAttribute" value="{{dropdownProductAttribute.attribute_code}}"/> + </actionGroup> + <actionGroup ref="createAttributeDropdownNthOption" stepKey="createOption1"> + <argument name="adminName" value="{{dropdownProductAttribute.option1_admin}}"/> + <argument name="frontName" value="{{dropdownProductAttribute.option1_frontend}}"/> + <argument name="row" value="1"/> + </actionGroup> + <actionGroup ref="createAttributeDropdownNthOption" stepKey="createOption2"> + <argument name="adminName" value="{{dropdownProductAttribute.option2_admin}}"/> + <argument name="frontName" value="{{dropdownProductAttribute.option2_frontend}}"/> + <argument name="row" value="2"/> + </actionGroup> + <actionGroup ref="createAttributeDropdownNthOptionAsDefault" stepKey="createOption3"> + <argument name="adminName" value="{{dropdownProductAttribute.option3_admin}}"/> + <argument name="frontName" value="{{dropdownProductAttribute.option3_frontend}}"/> + <argument name="row" value="3"/> + </actionGroup> + <click stepKey="saveAttribute" selector="{{AttributePropertiesSection.Save}}"/> + + <!--Perform appropriate assertions against dropdownProductAttribute entity--> + <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPageForAssertions"> + <argument name="ProductAttribute" value="{{dropdownProductAttribute.attribute_code}}"/> + </actionGroup> + <seeInField stepKey="assertLabel" selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{dropdownProductAttribute.attribute_code}}"/> + <seeOptionIsSelected stepKey="assertInputType" selector="{{AttributePropertiesSection.InputType}}" userInput="{{dropdownProductAttribute.frontend_input_admin}}"/> + <seeOptionIsSelected stepKey="assertRequired" selector="{{AttributePropertiesSection.ValueRequired}}" userInput="{{dropdownProductAttribute.is_required_admin}}"/> + <seeInField stepKey="assertAttrCode" selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" userInput="{{dropdownProductAttribute.attribute_code}}"/> + + <!--Assert options are in order and with correct attributes--> + <seeInField stepKey="seeOption1Admin" selector="{{AttributePropertiesSection.dropdownNthOptionAdmin('1')}}" userInput="{{dropdownProductAttribute.option1_admin}}"/> + <seeInField stepKey="seeOption1StoreView" selector="{{AttributePropertiesSection.dropdownNthOptionDefaultStoreView('1')}}" userInput="{{dropdownProductAttribute.option1_frontend}}"/> + <dontSeeCheckboxIsChecked stepKey="dontSeeOption1Default" selector="{{AttributePropertiesSection.dropdownNthOptionIsDefault('1')}}"/> + <seeInField stepKey="seeOption2Admin" selector="{{AttributePropertiesSection.dropdownNthOptionAdmin('2')}}" userInput="{{dropdownProductAttribute.option2_admin}}"/> + <seeInField stepKey="seeOption2StoreView" selector="{{AttributePropertiesSection.dropdownNthOptionDefaultStoreView('2')}}" userInput="{{dropdownProductAttribute.option2_frontend}}"/> + <dontSeeCheckboxIsChecked stepKey="dontSeeOption2Default" selector="{{AttributePropertiesSection.dropdownNthOptionIsDefault('2')}}"/> + <seeInField stepKey="seeOption3Admin" selector="{{AttributePropertiesSection.dropdownNthOptionAdmin('3')}}" userInput="{{dropdownProductAttribute.option3_admin}}"/> + <seeInField stepKey="seeOption3StoreView" selector="{{AttributePropertiesSection.dropdownNthOptionDefaultStoreView('3')}}" userInput="{{dropdownProductAttribute.option3_frontend}}"/> + <seeCheckboxIsChecked stepKey="seeOption3Default" selector="{{AttributePropertiesSection.dropdownNthOptionIsDefault('3')}}"/> + + <!--Go to New Product page, add Attribute and check dropdown values--> + <amOnPage url="{{AdminProductCreatePage.url('4', 'simple')}}" stepKey="goToCreateSimpleProductPage"/> + <actionGroup ref="addProductAttributeInProductModal" stepKey="addAttributeToProduct"> + <argument name="attributeCode" value="{{dropdownProductAttribute.attribute_code}}"/> + </actionGroup> + <click stepKey="openAttributes" selector="{{AdminProductAttributesSection.sectionHeader}}"/> + <waitForElementVisible selector="{{AdminProductAttributesSection.attributeDropdownByCode(dropdownProductAttribute.attribute_code)}}" stepKey="waitforLabel"/> + <seeOptionIsSelected selector="{{AdminProductAttributesSection.attributeDropdownByCode(dropdownProductAttribute.attribute_code)}}" userInput="{{dropdownProductAttribute.option3_admin}}" stepKey="seeDefaultIsCorrect"/> + <see stepKey="seeOption1Available" selector="{{AdminProductAttributesSection.attributeDropdownByCode(dropdownProductAttribute.attribute_code)}}" userInput="{{dropdownProductAttribute.option1_admin}}"/> + <see stepKey="seeOption2Available" selector="{{AdminProductAttributesSection.attributeDropdownByCode(dropdownProductAttribute.attribute_code)}}" userInput="{{dropdownProductAttribute.option2_admin}}"/> + <see stepKey="seeOption3Available" selector="{{AdminProductAttributesSection.attributeDropdownByCode(dropdownProductAttribute.attribute_code)}}" userInput="{{dropdownProductAttribute.option3_admin}}"/> + </test> + + <test name="CreateProductAttributeEntityDropdownWithSingleQuoteTest"> + <annotations> + <features value="Catalog"/> + <stories value="Create Product Attributes"/> + <title value="Admin should be able to create a Dropdown product attribute containing a single quote"/> + <description value="Admin should be able to create a Dropdown product attribute containing a single quote"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10898"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage"> + <argument name="ProductAttribute" value="{{dropdownProductAttributeWithQuote.attribute_code}}"/> + </actionGroup> + <click stepKey="clickDelete" selector="{{AttributePropertiesSection.DeleteAttribute}}"/> + <click stepKey="clickOk" selector="{{AttributeDeleteModalSection.confirm}}"/> + <waitForPageLoad stepKey="waitForDeletion"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Navigate to Stores > Attributes > Product.--> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> + + <!--Create new Product Attribute as TextField, with code and default value.--> + <actionGroup ref="createProductAttribute" stepKey="createAttribute"> + <argument name="attribute" value="dropdownProductAttributeWithQuote"/> + </actionGroup> + + <!--Navigate to Product Attribute, add Product Option and Save - 1--> + <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage1"> + <argument name="ProductAttribute" value="{{dropdownProductAttributeWithQuote.attribute_code}}"/> + </actionGroup> + <actionGroup ref="createAttributeDropdownNthOptionAsDefault" stepKey="createOption1"> + <argument name="adminName" value="{{dropdownProductAttributeWithQuote.option1_admin}}"/> + <argument name="frontName" value="{{dropdownProductAttributeWithQuote.option1_frontend}}"/> + <argument name="row" value="1"/> + </actionGroup> + <click stepKey="saveAttribute" selector="{{AttributePropertiesSection.Save}}"/> + + <!--Perform appropriate assertions against dropdownProductAttribute entity--> + <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPageForAssertions"> + <argument name="ProductAttribute" value="{{dropdownProductAttributeWithQuote.attribute_code}}"/> + </actionGroup> + <seeInField stepKey="assertLabel" selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{dropdownProductAttributeWithQuote.attribute_code}}"/> + <seeOptionIsSelected stepKey="assertInputType" selector="{{AttributePropertiesSection.InputType}}" userInput="{{dropdownProductAttributeWithQuote.frontend_input_admin}}"/> + <seeOptionIsSelected stepKey="assertRequired" selector="{{AttributePropertiesSection.ValueRequired}}" userInput="{{dropdownProductAttributeWithQuote.is_required_admin}}"/> + <seeInField stepKey="assertAttrCode" selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" userInput="{{dropdownProductAttributeWithQuote.attribute_code}}"/> + + <!--Assert options are in order and with correct attributes--> + <seeInField stepKey="seeOption1Admin" selector="{{AttributePropertiesSection.dropdownNthOptionAdmin('1')}}" userInput="{{dropdownProductAttributeWithQuote.option1_admin}}"/> + <seeInField stepKey="seeOption1StoreView" selector="{{AttributePropertiesSection.dropdownNthOptionDefaultStoreView('1')}}" userInput="{{dropdownProductAttributeWithQuote.option1_frontend}}"/> + <seeCheckboxIsChecked stepKey="seeOption1Default" selector="{{AttributePropertiesSection.dropdownNthOptionIsDefault('1')}}"/> + + <!--Go to New Product page, add Attribute and check dropdown values--> + <amOnPage url="{{AdminProductCreatePage.url('4', 'simple')}}" stepKey="goToCreateSimpleProductPage"/> + <actionGroup ref="addProductAttributeInProductModal" stepKey="addAttributeToProduct"> + <argument name="attributeCode" value="{{dropdownProductAttributeWithQuote.attribute_code}}"/> + </actionGroup> + <click stepKey="openAttributes" selector="{{AdminProductAttributesSection.sectionHeader}}"/> + <waitForElementVisible selector="{{AdminProductAttributesSection.attributeDropdownByCode(dropdownProductAttributeWithQuote.attribute_code)}}" stepKey="waitforLabel"/> + <seeOptionIsSelected selector="{{AdminProductAttributesSection.attributeDropdownByCode(dropdownProductAttributeWithQuote.attribute_code)}}" userInput="{{dropdownProductAttributeWithQuote.option1_admin}}" stepKey="seeDefaultIsCorrect"/> + <see stepKey="seeOption1Available" selector="{{AdminProductAttributesSection.attributeDropdownByCode(dropdownProductAttributeWithQuote.attribute_code)}}" userInput="{{dropdownProductAttributeWithQuote.option1_admin}}"/> + </test> + + <test name="CreateProductAttributeEntityMultiSelectTest"> + <annotations> + <features value="Catalog"/> + <stories value="Create Product Attributes"/> + <title value="Admin should be able to create a MultiSelect product attribute"/> + <description value="Admin should be able to create a MultiSelect product attribute"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10888"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage"> + <argument name="ProductAttribute" value="{{multiselectProductAttribute.attribute_code}}"/> + </actionGroup> + <click stepKey="clickDelete" selector="{{AttributePropertiesSection.DeleteAttribute}}"/> + <click stepKey="clickOk" selector="{{AttributeDeleteModalSection.confirm}}"/> + <waitForPageLoad stepKey="waitForDeletion"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Navigate to Stores > Attributes > Product.--> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> + + <!--Create new Product Attribute as TextField, with code and default value.--> + <actionGroup ref="createProductAttribute" stepKey="createAttribute"> + <argument name="attribute" value="multiselectProductAttribute"/> + </actionGroup> + + <!--Navigate to Product Attribute, add Product Options and Save - 1--> + <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPage1"> + <argument name="ProductAttribute" value="{{multiselectProductAttribute.attribute_code}}"/> + </actionGroup> + <actionGroup ref="createAttributeDropdownNthOption" stepKey="createOption1"> + <argument name="adminName" value="{{multiselectProductAttribute.option1_admin}}"/> + <argument name="frontName" value="{{multiselectProductAttribute.option1_frontend}}"/> + <argument name="row" value="1"/> + </actionGroup> + <actionGroup ref="createAttributeDropdownNthOption" stepKey="createOption2"> + <argument name="adminName" value="{{multiselectProductAttribute.option2_admin}}"/> + <argument name="frontName" value="{{multiselectProductAttribute.option2_frontend}}"/> + <argument name="row" value="2"/> + </actionGroup> + <actionGroup ref="createAttributeDropdownNthOptionAsDefault" stepKey="createOption3"> + <argument name="adminName" value="{{multiselectProductAttribute.option3_admin}}"/> + <argument name="frontName" value="{{multiselectProductAttribute.option3_frontend}}"/> + <argument name="row" value="3"/> + </actionGroup> + <click stepKey="saveAttribute" selector="{{AttributePropertiesSection.Save}}"/> + + <!--Perform appropriate assertions against multiselectProductAttribute entity--> + <actionGroup ref="navigateToEditProductAttribute" stepKey="goToEditPageForAssertions"> + <argument name="ProductAttribute" value="{{multiselectProductAttribute.attribute_code}}"/> + </actionGroup> + <seeInField stepKey="assertLabel" selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{multiselectProductAttribute.attribute_code}}"/> + <seeOptionIsSelected stepKey="assertInputType" selector="{{AttributePropertiesSection.InputType}}" userInput="{{multiselectProductAttribute.frontend_input_admin}}"/> + <seeOptionIsSelected stepKey="assertRequired" selector="{{AttributePropertiesSection.ValueRequired}}" userInput="{{multiselectProductAttribute.is_required_admin}}"/> + <seeInField stepKey="assertAttrCode" selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" userInput="{{multiselectProductAttribute.attribute_code}}"/> + + <!--Assert options are in order and with correct attributes--> + <seeInField stepKey="seeOption1Admin" selector="{{AttributePropertiesSection.dropdownNthOptionAdmin('1')}}" userInput="{{multiselectProductAttribute.option1_admin}}"/> + <seeInField stepKey="seeOption1StoreView" selector="{{AttributePropertiesSection.dropdownNthOptionDefaultStoreView('1')}}" userInput="{{multiselectProductAttribute.option1_frontend}}"/> + <dontSeeCheckboxIsChecked stepKey="dontSeeOption1Default" selector="{{AttributePropertiesSection.dropdownNthOptionIsDefault('1')}}"/> + <seeInField stepKey="seeOption2Admin" selector="{{AttributePropertiesSection.dropdownNthOptionAdmin('2')}}" userInput="{{multiselectProductAttribute.option2_admin}}"/> + <seeInField stepKey="seeOption2StoreView" selector="{{AttributePropertiesSection.dropdownNthOptionDefaultStoreView('2')}}" userInput="{{multiselectProductAttribute.option2_frontend}}"/> + <dontSeeCheckboxIsChecked stepKey="dontSeeOption2Default" selector="{{AttributePropertiesSection.dropdownNthOptionIsDefault('2')}}"/> + <seeInField stepKey="seeOption3Admin" selector="{{AttributePropertiesSection.dropdownNthOptionAdmin('3')}}" userInput="{{multiselectProductAttribute.option3_admin}}"/> + <seeInField stepKey="seeOption3StoreView" selector="{{AttributePropertiesSection.dropdownNthOptionDefaultStoreView('3')}}" userInput="{{multiselectProductAttribute.option3_frontend}}"/> + <seeCheckboxIsChecked stepKey="seeOption3Default" selector="{{AttributePropertiesSection.dropdownNthOptionIsDefault('3')}}"/> + + <!--Go to New Product page, add Attribute and check multiselect values--> + <amOnPage url="{{AdminProductCreatePage.url('4', 'simple')}}" stepKey="goToCreateSimpleProductPage"/> + <actionGroup ref="addProductAttributeInProductModal" stepKey="addAttributeToProduct"> + <argument name="attributeCode" value="{{multiselectProductAttribute.attribute_code}}"/> + </actionGroup> + <click stepKey="openAttributes" selector="{{AdminProductAttributesSection.sectionHeader}}"/> + <waitForElementVisible selector="{{AdminProductAttributesSection.attributeDropdownByCode(multiselectProductAttribute.attribute_code)}}" stepKey="waitforLabel"/> + <seeOptionIsSelected selector="{{AdminProductAttributesSection.attributeDropdownByCode(multiselectProductAttribute.attribute_code)}}" userInput="{{multiselectProductAttribute.option3_admin}}" stepKey="seeDefaultIsCorrect"/> + <see stepKey="seeOption1Available" selector="{{AdminProductAttributesSection.attributeDropdownByCode(multiselectProductAttribute.attribute_code)}}" userInput="{{multiselectProductAttribute.option1_admin}}"/> + <see stepKey="seeOption2Available" selector="{{AdminProductAttributesSection.attributeDropdownByCode(multiselectProductAttribute.attribute_code)}}" userInput="{{multiselectProductAttribute.option2_admin}}"/> + <see stepKey="seeOption3Available" selector="{{AdminProductAttributesSection.attributeDropdownByCode(multiselectProductAttribute.attribute_code)}}" userInput="{{multiselectProductAttribute.option3_admin}}"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/DeleteUsedInConfigurableProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/DeleteUsedInConfigurableProductAttributeTest.xml new file mode 100644 index 000000000000..e79e4cea408f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/DeleteUsedInConfigurableProductAttributeTest.xml @@ -0,0 +1,105 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="DeleteUsedInConfigurableProductAttributeTest"> + <annotations> + <stories value="Delete product attribute"/> + <title value="Admin should not be able to delete product attribute used in configurable product"/> + <description value="Admin should not be able to delete product attribute used in configurable product"/> + <testCaseId value="MC-14061"/> + <severity value="CRITICAL"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create category --> + <createData entity="SimpleSubCategory" stepKey="categoryHandle"/> + + <!-- Create base configurable product--> + <createData entity="BaseConfigurableProduct" stepKey="baseConfigProductHandle"> + <requiredEntity createDataKey="categoryHandle"/> + </createData> + + <!-- Create Dropdown Product Attribute --> + <createData entity="productDropDownAttribute" stepKey="productAttributeHandle"/> + + <!--Create attribute options --> + <createData entity="productAttributeOption1" stepKey="productAttributeOption1Handle"> + <requiredEntity createDataKey="productAttributeHandle"/> + </createData> + <createData entity="productAttributeOption2" stepKey="productAttributeOption2Handle"> + <requiredEntity createDataKey="productAttributeHandle"/> + </createData> + + <!--Add to attribute to Default attribute set--> + <createData entity="AddToDefaultSet" stepKey="addToAttributeSetHandle"> + <requiredEntity createDataKey="productAttributeHandle"/> + </createData> + + <!-- Get handle of the attribute options --> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getAttributeOption1Handle"> + <requiredEntity createDataKey="productAttributeHandle"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getAttributeOption2Handle"> + <requiredEntity createDataKey="productAttributeHandle"/> + </getData> + + <!--Create configurable product with the options --> + <createData entity="ConfigurableProductTwoOptions" stepKey="configProductOptionHandle"> + <requiredEntity createDataKey="baseConfigProductHandle"/> + <requiredEntity createDataKey="productAttributeHandle"/> + <requiredEntity createDataKey="getAttributeOption1Handle"/> + <requiredEntity createDataKey="getAttributeOption2Handle"/> + </createData> + + <!-- Login As Admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!-- Delete the configurable product created in the before block --> + <deleteData createDataKey="baseConfigProductHandle" stepKey="deleteConfig"/> + + <!-- Delete the category created in the before block --> + <deleteData createDataKey="categoryHandle" stepKey="deleteCategory"/> + + <!-- Delete configurable product attribute created in the before block --> + <deleteData createDataKey="productAttributeHandle" stepKey="deleteProductAttribute"/> + + <!-- Logout --> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + <!-- Go to Stores > Attributes > Products. Search and select the product attribute that was used to create the configurable product--> + <actionGroup ref="OpenProductAttributeFromSearchResultInGridActionGroup" stepKey="openProductAttributeFromSearchResultInGrid"> + <argument name="productAttributeCode" value="$$productAttributeHandle.attribute_code$$"/> + </actionGroup> + + <!-- Click Delete Attribute button --> + <actionGroup ref="DeleteProductAttributeByAttributeCodeActionGroup" stepKey="deleteProductAttributeByAttributeCode"> + <argument name="productAttributeCode" value="$$productAttributeHandle.attribute_code$$"/> + </actionGroup> + + <!-- Should see error message: This attribute is used in configurable products. --> + <actionGroup ref="AssertAttributeDeletionErrorMessageActionGroup" stepKey="assertAttributeDeletionErrorMessage"/> + + <!-- Go back to the product attribute grid. Should see the product attribute in the grid --> + <actionGroup ref="SearchAttributeByCodeOnProductAttributeGridActionGroup" stepKey="searchAttributeByCodeOnProductAttributeGrid"> + <argument name="productAttributeCode" value="$$productAttributeHandle.attribute_code$$"/> + </actionGroup> + + <!-- Go to the Catalog > Products page and search configurable product created in before block.--> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForProductOnBackend"> + <argument name="product" value="$$baseConfigProductHandle$$"/> + </actionGroup> + + <!--Should see the product attributes as expected --> + <actionGroup ref="AssertProductAttributePresenceInCatalogProductGridActionGroup" stepKey="assertProductAttributePresenceInCatalogProductGrid"> + <argument name="productAttribute" value="$$productAttributeHandle$$"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml new file mode 100644 index 000000000000..3dd55a9dfee9 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="ProductAvailableAfterEnablingSubCategoriesTest"> + <annotations> + <features value="Catalog"/> + <title value="Check that parent categories are showing products after enabling subcategories after fully reindex"/> + <description value="Check that parent categories are showing products after enabling subcategories after fully reindex"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-97370"/> + <useCaseId value="MAGETWO-96846"/> + <group value="Catalog"/> + </annotations> + <before> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="SubCategoryWithParent" stepKey="simpleSubCategory"> + <requiredEntity createDataKey="createCategory"/> + <field key="is_active">false</field> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="simpleSubCategory"/> + </createData> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <amOnPage url="$$createCategory.name$$.html" stepKey="goToCategoryStorefront2"/> + <waitForPageLoad stepKey="waitForCategoryStorefront"/> + <dontSeeElement selector="{{StorefrontCategoryProductSection.ProductImageByName($$createSimpleProduct.name$$)}}" stepKey="dontSeeCreatedProduct"/> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="onCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoadAddProducts"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandAll"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$simpleSubCategory.name$$)}}" stepKey="clickOnCreatedSimpleSubCategoryBeforeDelete"/> + <waitForPageLoad stepKey="AdminCategoryEditPageLoad"/> + <click selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="EnableCategory"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategoryWithProducts"/> + <waitForPageLoad stepKey="waitForCategorySaved"/> + <see userInput="You saved the category." stepKey="seeSuccessMessage"/> + <amOnPage url="$$createCategory.name$$.html" stepKey="goToCategoryStorefront"/> + <waitForPageLoad stepKey="waitForCategoryStorefrontPage"/> + <seeElement selector="{{StorefrontCategoryProductSection.ProductImageByName($$createSimpleProduct.name$$)}}" stepKey="seeCreatedProduct"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuote.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuote.xml index 569eb290bae3..386633f0e947 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuote.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuote.xml @@ -17,10 +17,6 @@ <severity value="CRITICAL"/> <group value="product"/> <testCaseId value="MAGETWO-92384"/> - <group value="skip"/> - <skip> - <issueId value="MAGETWO-93261"/> - </skip> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViews.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViews.xml deleted file mode 100644 index 1df6ae654001..000000000000 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViews.xml +++ /dev/null @@ -1,292 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest"> - <annotations> - <features value="Catalog"/> - <stories value="Custom options different storeviews"/> - <title value="Admin should be able to sell products with different variants of their own"/> - <description value="Admin should be able to sell products with different variants of their own"/> - <severity value="CRITICAL"/> - <testCaseId value="MAGETWO-58184"/> - <group value="product"/> - </annotations> - - <before> - <!-- Create Customer --> - - <createData entity="Simple_US_Customer" stepKey="createCustomer"/> - - <!--Create Simple Product --> - - <createData entity="_defaultCategory" stepKey="createCategory"/> - <createData entity="_defaultProduct" stepKey="createProduct"> - <requiredEntity createDataKey="createCategory"/> - <field key="price">100</field> - </createData> - - <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> - - <!--Create storeView 1--> - - <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView1"> - <argument name="customStore" value="customStoreEN"/> - </actionGroup> - - <!--Create storeView 2--> - - <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView2"> - <argument name="customStore" value="customStoreFR"/> - </actionGroup> - </before> - - <after> - <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> - <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> - - <!-- Delete Store View EN --> - - <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView1"> - <argument name="customStore" value="customStoreEN"/> - </actionGroup> - - <!-- Delete Store View FR --> - - <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView2"> - <argument name="customStore" value="customStoreFR"/> - </actionGroup> - </after> - - <!-- Open Product Grid, Filter product and open --> - - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad"/> - - <actionGroup ref="filterProductGridBySku" stepKey="filterGroupedProductOptions"> - <argument name="product" value="_defaultProduct"/> - </actionGroup> - <click selector="{{AdminProductGridSection.productGridXRowYColumnButton('1', '2')}}" stepKey="openProductForEdit"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad1"/> - - <!-- Update Product with Option Value DropDown 1--> - - <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" visible="true" stepKey="clickIfContentTabCloses2"/> - <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="checkAddOption1"/> - <waitForPageLoad time="10" stepKey="waitForPageLoad7"/> - <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionTitle('New Option')}}" userInput="Custom Options 1" stepKey="fillOptionTitle1"/> - <click selector="{{AdminProductCustomizableOptionsSection.checkSelect('Custom Options 1')}}" stepKey="clickSelect1"/> - <click selector="{{AdminProductCustomizableOptionsSection.checkDropDown('Custom Options 1')}}" stepKey="clickDropDown1"/> - <click selector="{{AdminProductCustomizableOptionsSection.clickAddValue('Custom Options 1')}}" stepKey="clickAddValue1"/> - <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('Custom Options 1', '0')}}" userInput="option1" stepKey="fillOptionValueTitle1"/> - <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice('Custom Options 1', '0')}}" userInput="5" stepKey="fillOptionValuePrice1"/> - - <!-- Update Product with Option Value 1 DropDown 1--> - - <click selector="{{AdminProductCustomizableOptionsSection.clickAddValue('Custom Options 1')}}" stepKey="clickAddValue2"/> - <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('Custom Options 1', '1')}}" userInput="option2" stepKey="fillOptionValueTitle2"/> - <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice('Custom Options 1', '1')}}" userInput="50" stepKey="fillOptionValuePrice2"/> - <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType('Custom Options 1', '1')}}" userInput="percent" stepKey="clickSelectPriceType"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton1"/> - - <!-- Switcher to Store FR--> - - <click selector="{{AdminProductFormActionSection.changeStoreButton}}" stepKey="clickStoreSwitcher"/> - <click selector="{{AdminProductFormActionSection.selectStoreView(customStoreFR.name)}}" stepKey="clickStoreView"/> - <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="acceptMessage"/> - - <!-- Open tab Customizable Options --> - - <waitForPageLoad time="10" stepKey="waitForPageLoad2"/> - <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" visible="true" stepKey="clickIfContentTabCloses3"/> - - <!-- Update Option Customizable Options and Option Value 1--> - - <waitForPageLoad time="30" stepKey="waitForPageLoad8"/> - <uncheckOption selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitle}}" stepKey="uncheckUseDefaultOptionTitle"/> - <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionTitle('Custom Options 1')}}" userInput="FR Custom Options 1" stepKey="fillOptionTitle2"/> - <uncheckOption selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitleByIndex('0')}}" stepKey="uncheckUseDefaultOptionValueTitle1"/> - <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('FR Custom Options 1', '0')}}" userInput="FR option1" stepKey="fillOptionValueTitle3"/> - - <!-- Update Product with Option Value 1 DropDown 1--> - - <click selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitleByIndex('1')}}" stepKey="clickHiddenRequireMessage"/> - <uncheckOption selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitleByIndex('1')}}" stepKey="uncheckUseDefaultOptionValueTitle2"/> - <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('FR Custom Options 1', '1')}}" userInput="FR option2" stepKey="fillOptionValueTitle4"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton2"/> - - <!-- Login Customer Storefront --> - - <amOnPage url="{{StorefrontCustomerSignInPage.url}}" stepKey="amOnSignInPage"/> - <waitForPageLoad stepKey="waitForSignInPage"/> - <fillField userInput="$$createCustomer.email$$" selector="{{StorefrontCustomerSignInFormSection.emailField}}" stepKey="fillEmail"/> - <fillField userInput="$$createCustomer.password$$" selector="{{StorefrontCustomerSignInFormSection.passwordField}}" stepKey="fillPassword"/> - <click selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}" stepKey="clickSignInAccountButton"/> - - <!-- Go to Product Page --> - - <amOnPage url="{{StorefrontHomePage.url}}$$createProduct.custom_attributes[url_key]$$.html" stepKey="amOnProduct1Page"/> - <waitForPageLoad stepKey="waitForProductPage"/> - <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownTitle('Custom Options 1')}}" stepKey="seeProductOptionDropDownTitle"/> - <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('Custom Options 1', 'option1')}}" stepKey="seeproductOptionDropDownOptionTitle1"/> - <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('Custom Options 1', 'option2')}}" stepKey="seeproductOptionDropDownOptionTitle2"/> - - <selectOption userInput="5" selector="{{StorefrontProductInfoMainSection.productOptionSelect('Custom Options 1')}}" stepKey="selectProductOptionDropDown"/> - - <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage1"> - <argument name="productName" value="$$createProduct.name$$"/> - </actionGroup> - - <selectOption userInput="50" selector="{{StorefrontProductInfoMainSection.productOptionSelect('Custom Options 1')}}" stepKey="selectProductOptionDropDown1"/> - - <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> - <argument name="productName" value="$$createProduct.name$$"/> - </actionGroup> - - <!-- Checking the correctness of displayed custom options for user parameters on checkout --> - - <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart" /> - - <conditionalClick selector="{{CheckoutPaymentSection.cartItemsArea}}" dependentSelector="{{CheckoutPaymentSection.cartItemsArea}}" visible="true" stepKey="exposeMiniCart"/> - - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskForCartItem"/> - <waitForElement selector="{{CheckoutPaymentSection.cartItemsAreaActive}}" time="30" stepKey="waitForCartItemsAreaActive"/> - - <see selector="{{CheckoutPaymentSection.cartItems}}" userInput="$$createProduct.name$$" stepKey="seeProductInCart"/> - - <!-- See Custom options are displayed as option1 --> - - <conditionalClick selector="{{CheckoutPaymentSection.productOptionsByProductItemPrice('105')}}" dependentSelector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('105')}}" visible="false" stepKey="exposeProductOptions"/> - <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('105')}}" userInput="option1" stepKey="seeProductOptionValueDropdown1Input1"/> - - <!-- See Custom options are displayed as option2 --> - - <conditionalClick selector="{{CheckoutPaymentSection.productOptionsByProductItemPrice('150')}}" dependentSelector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('150')}}" visible="false" stepKey="exposeProductOptions1"/> - <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('150')}}" userInput="option2" stepKey="seeProductOptionValueDropdown1Input2"/> - <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> - - <!-- Place Order --> - - <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> - <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> - - <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> - - <!-- Open Order --> - - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearOnOrdersPage"/> - <fillField selector="{{AdminOrdersGridSection.search}}" userInput="{$grabOrderNumber}" stepKey="fillOrderNum"/> - <click selector="{{AdminOrdersGridSection.submitSearch}}" stepKey="submitSearchOrderNum"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearOnSearch"/> - <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> - - <!-- Checking the correctness of displayed custom options for user parameters on Order --> - - <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="option1" stepKey="seeAdminOrderProductOptionValueDropdown1"/> - <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="option2" stepKey="seeAdminOrderProductOptionValueDropdown2"/> - - <!-- Switch to FR Store View Storefront --> - - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnProduct4Page"/> - <waitForPageLoad stepKey="waitForStorefrontHomePage"/> - <click selector="{{StorefrontHeaderSection.storeViewSwitcher}}" stepKey="clickStoreViewSwitcher1"/> - <waitForElementVisible selector="{{StorefrontHeaderSection.storeViewDropdown}}" stepKey="waitForStoreViewDropdown1"/> - <click selector="{{StorefrontHeaderSection.storeViewOption(customStoreFR.code)}}" stepKey="selectStoreView1"/> - <waitForPageLoad stepKey="waitForPageLoad4"/> - - <amOnPage url="{{StorefrontHomePage.url}}$$createProduct.custom_attributes[url_key]$$.html" stepKey="amOnProduct2Page"/> - <waitForPageLoad stepKey="waitForProductPage2"/> - <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownTitle('FR Custom Options 1')}}" stepKey="seeProductFrOptionDropDownTitle"/> - <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('FR Custom Options 1', 'FR option1')}}" stepKey="productFrOptionDropDownOptionTitle1"/> - <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('FR Custom Options 1', 'FR option2')}}" stepKey="productFrOptionDropDownOptionTitle2"/> - - <selectOption userInput="5" selector="{{StorefrontProductInfoMainSection.productOptionSelect('FR Custom Options 1')}}" stepKey="seeProductFrOptionDropDown"/> - - <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage2"> - <argument name="productName" value="$$createProduct.name$$"/> - </actionGroup> - - <selectOption userInput="50" selector="{{StorefrontProductInfoMainSection.productOptionSelect('FR Custom Options 1')}}" stepKey="seeProductFrOptionDropDown1"/> - - <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage3"> - <argument name="productName" value="$$createProduct.name$$"/> - </actionGroup> - - <!-- Checking the correctness of displayed custom options for user parameters on checkout --> - - <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart1" /> - - <conditionalClick selector="{{CheckoutPaymentSection.cartItemsArea}}" dependentSelector="{{CheckoutPaymentSection.cartItemsArea}}" visible="true" stepKey="exposeMiniCart1"/> - - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskForCartItem1"/> - <waitForElement selector="{{CheckoutPaymentSection.cartItemsAreaActive}}" time="30" stepKey="waitForCartItemsAreaActive1"/> - - <see selector="{{CheckoutPaymentSection.cartItems}}" userInput="$$createProduct.name$$" stepKey="seeProductInCar1t"/> - - <!-- See Custom options are displayed as option1 --> - - <conditionalClick selector="{{CheckoutPaymentSection.productOptionsByProductItemPrice('105')}}" dependentSelector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('105')}}" visible="false" stepKey="exposeProductOptions2"/> - <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('105')}}" userInput="FR option1" stepKey="seeProductFrOptionValueDropdown1Input2"/> - - <!-- See Custom options are displayed as option2 --> - - <conditionalClick selector="{{CheckoutPaymentSection.productOptionsByProductItemPrice('150')}}" dependentSelector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('150')}}" visible="false" stepKey="exposeProductOptions3"/> - <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('150')}}" userInput="FR option2" stepKey="seeProductFrOptionValueDropdown1Input3"/> - <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext1"/> - - <!-- Place Order --> - - <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton1"/> - <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder1"/> - - <!-- Open Product Grid, Filter product and open --> - - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage1"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad5"/> - - <actionGroup ref="filterProductGridBySku" stepKey="filterGroupedProductOptions1"> - <argument name="product" value="_defaultProduct"/> - </actionGroup> - <click selector="{{AdminProductGridSection.productGridXRowYColumnButton('1', '2')}}" stepKey="openProductForEdit1"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad6"/> - - <!-- Switcher to Store FR--> - - <click selector="{{AdminProductFormActionSection.changeStoreButton}}" stepKey="clickStoreSwitcher1"/> - <click selector="{{AdminProductFormActionSection.selectStoreView(customStoreFR.name)}}" stepKey="clickStoreView1"/> - <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="acceptMessage1"/> - - <!-- Open tab Customizable Options --> - - <waitForPageLoad time="30" stepKey="waitForPageLoad9"/> - <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" visible="true" stepKey="clickIfContentTabCloses4" /> - - <!-- Update Option Customizable Options and Option Value 1--> - - <waitForPageLoad time="30" stepKey="waitForPageLoad10"/> - <checkOption selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitle}}" stepKey="checkUseDefaultOptionTitle"/> - <checkOption selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitleByIndex('0')}}" stepKey="checkUseDefaultOptionValueTitle1"/> - - <!-- Update Product with Option Value 1 DropDown 1--> - - <waitForPageLoad time="30" stepKey="waitForPageLoad11"/> - <checkOption selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitleByIndex('1')}}" stepKey="checkUseDefaultOptionValueTitle2"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton3"/> - - <!--Go to Product Page--> - - <amOnPage url="{{StorefrontHomePage.url}}$$createProduct.custom_attributes[url_key]$$.html" stepKey="amOnProduct2Page2"/> - <waitForPageLoad stepKey="waitForProductPage3"/> - <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownTitle('Custom Options 1')}}" stepKey="seeProductOptionDropDownTitle1"/> - <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('Custom Options 1', 'option1')}}" stepKey="seeProductOptionDropDownOptionTitle3"/> - <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('Custom Options 1', 'option2')}}" stepKey="seeProductOptionDropDownOptionTitle4"/> - </test> -</tests> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml new file mode 100644 index 000000000000..fb95fc3f57bc --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml @@ -0,0 +1,317 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest"> + <annotations> + <features value="Catalog"/> + <stories value="Custom options different storeviews"/> + <title value="Admin should be able to sell products with different variants of their own"/> + <description value="Admin should be able to sell products with different variants of their own"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-58184"/> + <group value="product"/> + </annotations> + + <before> + <!-- Create Customer --> + + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + + <!--Create Simple Product --> + + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">100</field> + </createData> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + + <!--Create storeView 1--> + + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView1"> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + + <!--Create storeView 2--> + + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView2"> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> + </before> + + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + + <!-- Delete Store View EN --> + + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView1"> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + + <!-- Delete Store View FR --> + + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView2"> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearWebsitesGridFilters"/> + + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrdersGridFilter"/> + + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearProductsGridFilters"/> + <actionGroup ref="logout" stepKey="logout"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogoutStorefront"/> + </after> + + <!-- Open Product Grid, Filter product and open --> + + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad1"/> + + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="filterGroupedProductOptions"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProductPage"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + + <!-- Update Product with Option Value DropDown 1--> + <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" visible="true" stepKey="clickIfContentTabCloses2"/> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="checkAddOption1"/> + <waitForPageLoad time="10" stepKey="waitForPageLoad3"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionTitle('New Option')}}" userInput="Custom Options 1" stepKey="fillOptionTitle1"/> + <click selector="{{AdminProductCustomizableOptionsSection.checkSelect('Custom Options 1')}}" stepKey="clickSelect1"/> + <click selector="{{AdminProductCustomizableOptionsSection.checkDropDown('Custom Options 1')}}" stepKey="clickDropDown1"/> + <click selector="{{AdminProductCustomizableOptionsSection.clickAddValue('Custom Options 1')}}" stepKey="clickAddValue1"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('Custom Options 1', '0')}}" userInput="option1" stepKey="fillOptionValueTitle1"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice('Custom Options 1', '0')}}" userInput="5" stepKey="fillOptionValuePrice1"/> + + <!-- Update Product with Option Value 1 DropDown 1--> + + <click selector="{{AdminProductCustomizableOptionsSection.clickAddValue('Custom Options 1')}}" stepKey="clickAddValue2"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('Custom Options 1', '1')}}" userInput="option2" stepKey="fillOptionValueTitle2"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice('Custom Options 1', '1')}}" userInput="50" stepKey="fillOptionValuePrice2"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType('Custom Options 1', '1')}}" userInput="percent" stepKey="clickSelectPriceType"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton1"/> + + <!-- Switcher to Store FR--> + <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="switchToStoreFR"> + <argument name="storeView" value="customStoreFR.name"/> + </actionGroup> + + <!-- Open tab Customizable Options --> + + <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" visible="true" stepKey="clickIfContentTabCloses3"/> + + <!-- Update Option Customizable Options and Option Value 1--> + + <waitForPageLoad time="30" stepKey="waitForPageLoad5"/> + <uncheckOption selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitle}}" stepKey="uncheckUseDefaultOptionTitle"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionTitle('Custom Options 1')}}" userInput="FR Custom Options 1" stepKey="fillOptionTitle2"/> + <uncheckOption selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitleByIndex('0')}}" stepKey="uncheckUseDefaultOptionValueTitle1"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('FR Custom Options 1', '0')}}" userInput="FR option1" stepKey="fillOptionValueTitle3"/> + + <!-- Update Product with Option Value 1 DropDown 1--> + + <click selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitleByIndex('1')}}" stepKey="clickHiddenRequireMessage"/> + <uncheckOption selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitleByIndex('1')}}" stepKey="uncheckUseDefaultOptionValueTitle2"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('FR Custom Options 1', '1')}}" userInput="FR option2" stepKey="fillOptionValueTitle4"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton2"/> + + <!-- Login Customer Storefront --> + + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> + <argument name="Customer" value="$$createCustomer$$" /> + </actionGroup> + + <!-- Go to Product Page --> + + <amOnPage url="{{StorefrontHomePage.url}}$$createProduct.custom_attributes[url_key]$$.html" stepKey="amOnProduct1Page"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad7"/> + + <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownTitle('Custom Options 1')}}" stepKey="seeProductOptionDropDownTitle"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('Custom Options 1', 'option1')}}" stepKey="seeproductOptionDropDownOptionTitle1"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('Custom Options 1', 'option2')}}" stepKey="seeproductOptionDropDownOptionTitle2"/> + + <selectOption userInput="5" selector="{{StorefrontProductInfoMainSection.productOptionSelect('Custom Options 1')}}" stepKey="selectProductOptionDropDown"/> + + <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage1"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + + <selectOption userInput="50" selector="{{StorefrontProductInfoMainSection.productOptionSelect('Custom Options 1')}}" stepKey="selectProductOptionDropDown1"/> + + <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + + <!-- Checking the correctness of displayed custom options for user parameters on checkout --> + + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart" /> + + <conditionalClick selector="{{CheckoutPaymentSection.cartItemsArea}}" dependentSelector="{{CheckoutPaymentSection.cartItemsArea}}" visible="true" stepKey="exposeMiniCart"/> + + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskForCartItem"/> + <waitForElement selector="{{CheckoutPaymentSection.cartItemsAreaActive}}" time="30" stepKey="waitForCartItemsAreaActive"/> + + <see selector="{{CheckoutPaymentSection.cartItems}}" userInput="$$createProduct.name$$" stepKey="seeProductInCart"/> + + <!-- See Custom options are displayed as option1 --> + + <conditionalClick selector="{{CheckoutPaymentSection.productOptionsByProductItemPrice('105')}}" dependentSelector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('105')}}" visible="false" stepKey="exposeProductOptions"/> + <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('105')}}" userInput="option1" stepKey="seeProductOptionValueDropdown1Input1"/> + + <!-- See Custom options are displayed as option2 --> + + <conditionalClick selector="{{CheckoutPaymentSection.productOptionsByProductItemPrice('150')}}" dependentSelector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('150')}}" visible="false" stepKey="exposeProductOptions1"/> + <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('150')}}" userInput="option2" stepKey="seeProductOptionValueDropdown1Input2"/> + + <!-- Place Order --> + + <!--Select shipping method--> + <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> + <waitForElementVisible selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterClickNext"/> + <!--Select payment method--> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod"/> + <!-- Place Order --> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="customerPlaceOrder"> + <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage"/> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage"/> + </actionGroup> + + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> + + <!-- Open Order --> + + <actionGroup ref="filterOrderGridById" stepKey="openOrdersGrid"> + <argument name="orderId" value="{$grabOrderNumber}"/> + </actionGroup> + <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad10"/> + + <!-- Checking the correctness of displayed custom options for user parameters on Order --> + + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="option1" stepKey="seeAdminOrderProductOptionValueDropdown1"/> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="option2" stepKey="seeAdminOrderProductOptionValueDropdown2"/> + + <!-- Switch to FR Store View Storefront --> + + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnProduct4Page"/> + + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchStore"> + <argument name="storeView" value="customStoreFR"/> + </actionGroup> + + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="amOnProduct2Page"/> + + <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownTitle('FR Custom Options 1')}}" stepKey="seeProductFrOptionDropDownTitle"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('FR Custom Options 1', 'FR option1')}}" stepKey="productFrOptionDropDownOptionTitle1"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('FR Custom Options 1', 'FR option2')}}" stepKey="productFrOptionDropDownOptionTitle2"/> + + <selectOption userInput="5" selector="{{StorefrontProductInfoMainSection.productOptionSelect('FR Custom Options 1')}}" stepKey="seeProductFrOptionDropDown"/> + + <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage2"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + + <selectOption userInput="50" selector="{{StorefrontProductInfoMainSection.productOptionSelect('FR Custom Options 1')}}" stepKey="seeProductFrOptionDropDown1"/> + + <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage3"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + + <!-- Checking the correctness of displayed custom options for user parameters on checkout --> + + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart1" /> + + <conditionalClick selector="{{CheckoutPaymentSection.cartItemsArea}}" dependentSelector="{{CheckoutPaymentSection.cartItemsArea}}" visible="true" stepKey="exposeMiniCart1"/> + + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskForCartItem1"/> + <waitForElement selector="{{CheckoutPaymentSection.cartItemsAreaActive}}" time="30" stepKey="waitForCartItemsAreaActive1"/> + + <see selector="{{CheckoutPaymentSection.cartItems}}" userInput="$$createProduct.name$$" stepKey="seeProductInCar1t"/> + + <!-- See Custom options are displayed as option1 --> + + <conditionalClick selector="{{CheckoutPaymentSection.productOptionsByProductItemPrice('105')}}" dependentSelector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('105')}}" visible="false" stepKey="exposeProductOptions2"/> + <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('105')}}" userInput="FR option1" stepKey="seeProductFrOptionValueDropdown1Input2"/> + + <!-- See Custom options are displayed as option2 --> + + <conditionalClick selector="{{CheckoutPaymentSection.productOptionsByProductItemPrice('150')}}" dependentSelector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('150')}}" visible="false" stepKey="exposeProductOptions3"/> + <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('150')}}" userInput="FR option2" stepKey="seeProductFrOptionValueDropdown1Input3"/> + + <!-- Place Order --> + + <!--Select shipping method--> + <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod2"/> + <waitForElementVisible selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton2"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext2"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterClickNext2"/> + + <!--Select payment method--> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod2"/> + <!-- Place Order --> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="customerPlaceOrder2"> + <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage"/> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage"/> + </actionGroup> + + <!-- Open Product Grid, Filter product and open --> + + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage1"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad15"/> + + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="filterGroupedProductOptions1"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + + <click selector="{{AdminProductGridSection.productGridXRowYColumnButton('1', '2')}}" stepKey="openProductForEdit1"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad16"/> + + <!-- Switcher to Store FR--> + + <scrollToTopOfPage stepKey="scrollToTopOfPage2"/> + <click selector="{{AdminProductFormActionSection.changeStoreButton}}" stepKey="clickStoreSwitcher1"/> + <click selector="{{AdminProductFormActionSection.selectStoreView(customStoreFR.name)}}" stepKey="clickStoreView1"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="acceptMessage1"/> + + <!-- Open tab Customizable Options --> + + <waitForPageLoad time="30" stepKey="waitForPageLoad17"/> + <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" visible="true" stepKey="clickIfContentTabCloses4" /> + + <!-- Update Option Customizable Options and Option Value 1--> + + <waitForPageLoad time="30" stepKey="waitForPageLoad18"/> + <checkOption selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitle}}" stepKey="checkUseDefaultOptionTitle"/> + <checkOption selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitleByIndex('0')}}" stepKey="checkUseDefaultOptionValueTitle1"/> + + <!-- Update Product with Option Value 1 DropDown 1--> + + <waitForPageLoad time="30" stepKey="waitForPageLoad19"/> + <checkOption selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitleByIndex('1')}}" stepKey="checkUseDefaultOptionValueTitle2"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton3"/> + + <!--Go to Product Page--> + + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="amOnProduct2Page2"/> + + <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownTitle('Custom Options 1')}}" stepKey="seeProductOptionDropDownTitle1"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('Custom Options 1', 'option1')}}" stepKey="seeProductOptionDropDownOptionTitle3"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('Custom Options 1', 'option2')}}" stepKey="seeProductOptionDropDownOptionTitle4"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptions.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptions.xml deleted file mode 100644 index 6b444f1f6663..000000000000 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptions.xml +++ /dev/null @@ -1,182 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="StorefrontPurchaseProductWithCustomOptions"> - <annotations> - <features value="Catalog"/> - <stories value="Purchase a product with Custom Options of different types"/> - <title value="Admin should be able to sell products with different variants of their own"/> - <description value="Admin should be able to sell products with different variants of their own"/> - <severity value="CRITICAL"/> - <testCaseId value="MAGETWO-61717"/> - <group value="Catalog"/> - </annotations> - <before> - <createData entity="Simple_US_Customer" stepKey="createCustomer"/> - </before> - <after> - <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> - </after> - - <!--Create Simple Product with Custom Options--> - - <createData entity="_defaultCategory" stepKey="createCategory"/> - <createData entity="_defaultProduct" stepKey="createProduct"> - <requiredEntity createDataKey="createCategory"/> - <field key="price">17</field> - </createData> - <updateData createDataKey="createProduct" entity="productWithOptions" stepKey="updateProductWithOption"/> - - <!-- Login Customer Storeront --> - - <amOnPage url="{{StorefrontCustomerSignInPage.url}}" stepKey="amOnSignInPage"/> - <fillField userInput="$$createCustomer.email$$" selector="{{StorefrontCustomerSignInFormSection.emailField}}" stepKey="fillEmail"/> - <fillField userInput="$$createCustomer.password$$" selector="{{StorefrontCustomerSignInFormSection.passwordField}}" stepKey="fillPassword"/> - <click selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}" stepKey="clickSignInAccountButton"/> - - <!-- Checking the correctness of displayed prices for user parameters --> - - <amOnPage url="{{StorefrontHomePage.url}}$createProduct.custom_attributes[url_key]$.html" stepKey="amOnProduct3Page"/> - - <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsPrice(ProductOptionField.title, ProductOptionField.price)}}" stepKey="checkFieldProductOption"/> - <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsPrice(ProductOptionArea.title, '1.7')}}" stepKey="checkAreaProductOption"/> - <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsDropDown(ProductOptionDropDown.title, ProductOptionValueDropdown1.price)}}" stepKey="checkDropDownProductOption"/> - <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsRadioButtons(ProductOptionRadiobutton.title, ProductOptionValueRadioButtons1.price)}}" stepKey="checkButtonsProductOption"/> - <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsCheckbox(ProductOptionCheckbox.title, '20.91')}}" stepKey="checkCheckboxProductOptiozn"/> - <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsMultiselect(ProductOptionMultiSelect.title, ProductOptionValueMultiSelect1.price)}}" stepKey="checkMultiSelectProductOption"/> - <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsData(ProductOptionDate.title, ProductOptionDate.price)}}" stepKey="checkDataProductOption"/> - - <!-- Adding items to the checkout --> - - <fillField userInput="OptionField" selector="{{StorefrontProductInfoMainSection.productOptionFieldInput(ProductOptionField.title)}}" stepKey="fillProductOptionInputField"/> - <fillField userInput="OptionArea" selector="{{StorefrontProductInfoMainSection.productOptionAreaInput(ProductOptionArea.title)}}" stepKey="fillProductOptionInputArea"/> - <attachFile userInput="{{productWithOptions.file}}" selector="{{StorefrontProductInfoMainSection.addLinkFileUploadFile(ProductOptionFile.title)}}" stepKey="fillUploadFile"/> - <selectOption userInput="{{ProductOptionValueDropdown1.price}}" selector="{{StorefrontProductInfoMainSection.productOptionSelect(ProductOptionDropDown.title)}}" stepKey="seeProductOptionDropDown"/> - <checkOption selector="{{StorefrontProductInfoMainSection.productOptionRadioButtonsCheckbox(ProductOptionRadiobutton.title, ProductOptionValueRadioButtons1.price)}}" stepKey="seeProductOptionRadioButtons"/> - <checkOption selector="{{StorefrontProductInfoMainSection.productOptionRadioButtonsCheckbox(ProductOptionCheckbox.title, '20.91')}}" stepKey="seeProductOptionCheckbox"/> - <selectOption userInput="{{ProductOptionValueMultiSelect1.price}}" selector="{{StorefrontProductInfoMainSection.productOptionSelect(ProductOptionMultiSelect.title)}}" stepKey="selectProductOptionMultiSelect"/> - <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionDataMonth(ProductOptionDate.title)}}" stepKey="selectProductOptionDate"/> - <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionDataDay(ProductOptionDate.title)}}" stepKey="selectProductOptionDate1"/> - <selectOption userInput="2018" selector="{{StorefrontProductInfoMainSection.productOptionDataYear(ProductOptionDate.title)}}" stepKey="selectProductOptionDate2"/> - <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionDateAndTimeMonth(ProductOptionDateTime.title)}}" stepKey="selectProductOptionDateAndTimeMonth"/> - <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionDateAndTimeDay(ProductOptionDateTime.title)}}" stepKey="selectProductOptionDateAndTimeDay"/> - <selectOption userInput="2018" selector="{{StorefrontProductInfoMainSection.productOptionDateAndTimeYear(ProductOptionDateTime.title)}}" stepKey="selectProductOptionDateAndTimeYear"/> - <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionDateAndTimeHour(ProductOptionDateTime.title)}}" stepKey="selectProductOptionDateAndTimeHour"/> - <selectOption userInput="00" selector="{{StorefrontProductInfoMainSection.productOptionDateAndTimeMinute(ProductOptionDateTime.title)}}" stepKey="selectProductOptionDateAndTimeMinute"/> - <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionTimeHour(ProductOptionTime.title)}}" stepKey="selectProductOptionTimeHour"/> - <selectOption userInput="00" selector="{{StorefrontProductInfoMainSection.productOptionTimeMinute(ProductOptionTime.title)}}" stepKey="selectProductOptionTimeMinute"/> - - <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="finalProductPrice"/> - - <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> - <argument name="productName" value="$createProduct.name$"/> - </actionGroup> - - <!-- Checking the correctness of displayed custom options for user parameters on checkout --> - - <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart" /> - - <conditionalClick selector="{{CheckoutPaymentSection.cartItemsArea}}" dependentSelector="{{CheckoutPaymentSection.cartItemsArea}}" visible="true" stepKey="exposeMiniCart"/> - - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskForCartItem"/> - <waitForElement selector="{{CheckoutPaymentSection.cartItemsAreaActive}}" time="30" stepKey="waitForCartItemsAreaActive"/> - - <see selector="{{CheckoutPaymentSection.cartItems}}" userInput="$createProduct.name$" stepKey="seeProductInCart"/> - - <conditionalClick selector="{{CheckoutPaymentSection.ProductOptionsByProductItemName($createProduct.name$)}}" dependentSelector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($createProduct.name$)}}" visible="false" stepKey="exposeProductOptions"/> - - <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($createProduct.name$)}}" userInput="{{ProductOptionField.title}}" stepKey="seeProductOptionFieldInput1"/> - <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($createProduct.name$)}}" userInput="{{ProductOptionArea.title}}" stepKey="seeProductOptionAreaInput1"/> - <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($createProduct.name$)}}" userInput="{{productWithOptions.file}}" stepKey="seeProductOptionFileInput1"/> - <seeElement selector="{{CheckoutPaymentSection.ProductOptionLinkActiveByProductItemName($createProduct.name$, productWithOptions.file)}}" stepKey="seeProductOptionFileInputLink1"/> - <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($createProduct.name$)}}" userInput="{{ProductOptionValueDropdown1.title}}" stepKey="seeProductOptionValueDropdown1Input1"/> - <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($createProduct.name$)}}" userInput="{{ProductOptionValueRadioButtons1.title}}" stepKey="seeProductOptionValueRadioButtons1Input1"/> - <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($createProduct.name$)}}" userInput="{{ProductOptionValueCheckbox.title}}" stepKey="seeProductOptionValueCheckboxInput1" /> - <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($createProduct.name$)}}" userInput="{{ProductOptionValueMultiSelect1.title}}" stepKey="seeproductAttributeOptionsMultiselect1Input1" /> - <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($createProduct.name$)}}" userInput="Jan 1, 2018" stepKey="seeProductOptionDateAndTimeInput" /> - <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($createProduct.name$)}}" userInput="1/1/18, 1:00 AM" stepKey="seeProductOptionDataInput" /> - <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($createProduct.name$)}}" userInput="1:00 AM" stepKey="seeProductOptionTimeInput" /> - - <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> - - <!-- Place Order --> - - <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> - <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> - - <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> - - <!-- Login to Admin and open Order --> - - <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> - - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearOnOrdersPage"/> - <fillField selector="{{AdminOrdersGridSection.search}}" userInput="{$grabOrderNumber}" stepKey="fillOrderNum"/> - <click selector="{{AdminOrdersGridSection.submitSearch}}" stepKey="submitSearchOrderNum"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearOnSearch"/> - <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> - - <!-- Checking the correctness of displayed custom options for user parameters on Order --> - - <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionField.title}}" stepKey="seeAdminOrderProductOptionField" /> - <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionArea.title}}" stepKey="seeAdminOrderProductOptionArea"/> - <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{productWithOptions.file}}" stepKey="seeAdminOrderProductOptionFile"/> - <seeElement selector="{{AdminOrderItemsOrderedSection.productNameOptionsLink(productWithOptions.file)}}" stepKey="seeAdminOrderProductOptionFileLink"/> - <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueDropdown1.title}}" stepKey="seeAdminOrderProductOptionValueDropdown1"/> - <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueRadioButtons1.title}}" stepKey="seeAdminOrderProductOptionValueRadioButton1"/> - <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueCheckbox.title}}" stepKey="seeAdminOrderProductOptionValueCheckbox" /> - <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueMultiSelect1.title}}" stepKey="seeAdminOrderproductAttributeOptionsMultiselect1" /> - <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="Jan 1, 2018" stepKey="seeAdminOrderProductOptionDateAndTime" /> - <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="1/1/18, 1:00 AM" stepKey="seeAdminOrderProductOptionData" /> - <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="1:00 AM" stepKey="seeAdminOrderProductOptionTime" /> - - <!-- Reorder and Checking the correctness of displayed custom options for user parameters on Order and correctness of displayed price Subtotal--> - - <click selector="{{AdminOrderDetailsMainActionsSection.reorder}}" stepKey="clickReorder"/> - <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="trySubmitOrder"/> - - <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionField.title}}" stepKey="seeAdminOrderProductOptionField1" /> - <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionArea.title}}" stepKey="seeAdminOrderProductOptionArea1"/> - <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{productWithOptions.file}}" stepKey="seeAdminOrderProductOptionFile1"/> - <seeElement selector="{{AdminOrderItemsOrderedSection.productNameOptionsLink(productWithOptions.file)}}" stepKey="seeAdminOrderProductOptionFileLink1"/> - <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueDropdown1.title}}" stepKey="seeAdminOrderProductOptionValueDropdown11"/> - <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueRadioButtons1.title}}" stepKey="seeAdminOrderProductOptionValueRadioButton11"/> - <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueCheckbox.title}}" stepKey="seeAdminOrderProductOptionValueCheckbox1" /> - <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueMultiSelect1.title}}" stepKey="seeAdminOrderproductAttributeOptionsMultiselect11" /> - <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="Jan 1, 2018" stepKey="seeAdminOrderProductOptionDateAndTime1" /> - <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="1/1/18, 1:00 AM" stepKey="seeAdminOrderProductOptionData1" /> - <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="1:00 AM" stepKey="seeAdminOrderProductOptionTime1" /> - - <see selector="{{AdminOrderTotalSection.subTotal}}" userInput="{$finalProductPrice}" stepKey="seeOrderSubTotal"/> - - <!-- Go to Customer Order Page and Checking the correctness of displayed custom options for user parameters on Order --> - - <amOnPage url="{{StorefrontCustomerOrderViewPage.url({$grabOrderNumber})}}" stepKey="amOnProduct4Page"/> - - <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($createProduct.name$, ProductOptionField.title, ProductOptionField.title)}}" userInput="{{ProductOptionField.title}}" stepKey="seeStorefontOrderProductOptionField1" /> - <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($createProduct.name$, ProductOptionArea.title, ProductOptionArea.title)}}" userInput="{{ProductOptionArea.title}}" stepKey="seeStorefontOrderProductOptionArea1"/> - <see selector="{{StorefrontCustomerOrderSection.productCustomOptionsFile($createProduct.name$, ProductOptionFile.title, productWithOptions.file)}}" userInput="{{productWithOptions.file}}" stepKey="seeStorefontOrderProductOptionFile1"/> - <seeElement selector="{{StorefrontCustomerOrderSection.productCustomOptionsLink($createProduct.name$, ProductOptionFile.title, productWithOptions.file)}}" stepKey="seeStorefontOrderProductOptionFileLink1"/> - <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($createProduct.name$, ProductOptionDropDown.title, ProductOptionValueDropdown1.title)}}" userInput="{{ProductOptionValueDropdown1.title}}" stepKey="seeStorefontOrderProductOptionValueDropdown11"/> - <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($createProduct.name$, ProductOptionRadiobutton.title, ProductOptionValueRadioButtons1.title)}}" userInput="{{ProductOptionValueRadioButtons1.title}}" stepKey="seeStorefontOrderProductOptionValueRadioButtons11"/> - <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($createProduct.name$, ProductOptionCheckbox.title, ProductOptionValueCheckbox.title)}}" userInput="{{ProductOptionValueCheckbox.title}}" stepKey="seeStorefontOrderProductOptionValueCheckbox1" /> - <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($createProduct.name$, ProductOptionMultiSelect.title, ProductOptionValueMultiSelect1.title)}}" userInput="{{ProductOptionValueMultiSelect1.title}}" stepKey="seeStorefontOrderproductAttributeOptionsMultiselect11" /> - <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($createProduct.name$, ProductOptionDate.title, 'Jan 1, 2018')}}" userInput="Jan 1, 2018" stepKey="seeStorefontOrderProductOptionDateAndTime1" /> - <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($createProduct.name$, ProductOptionDateTime.title, '1/1/18, 1:00 AM')}}" userInput="1/1/18, 1:00 AM" stepKey="seeStorefontOrderProductOptionData1" /> - <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($createProduct.name$, ProductOptionTime.title, '1:00 AM')}}" userInput="1:00 AM" stepKey="seeStorefontOrderProductOptionTime1" /> - - <!-- Delete product and category --> - - <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> - <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - - </test> -</tests> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml new file mode 100644 index 000000000000..a3bce2d4fe2f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml @@ -0,0 +1,189 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontPurchaseProductWithCustomOptionsTest"> + <annotations> + <features value="Catalog"/> + <stories value="Purchase a product with Custom Options of different types"/> + <title value="Admin should be able to sell products with different variants of their own"/> + <description value="Admin should be able to sell products with different variants of their own"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-61717"/> + <group value="Catalog"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <!--Create Simple Product with Custom Options--> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">17</field> + </createData> + <updateData createDataKey="createProduct" entity="productWithOptions" stepKey="updateProductWithOption"/> + <!-- Logout customer before in case of it logged in from previous test --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogoutStorefront"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <!-- Delete product and category --> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderListingFilters"/> + <actionGroup ref="logout" stepKey="logoutAdmin"/> + <!-- Logout customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogoutStorefront"/> + </after> + + <!-- Login Customer Storefront --> + + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginCustomerOnStorefront"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!-- Checking the correctness of displayed prices for user parameters --> + + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToProductPage"/> + + <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsPrice(ProductOptionField.title, ProductOptionField.price)}}" stepKey="checkFieldProductOption"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsPrice(ProductOptionArea.title, '1.7')}}" stepKey="checkAreaProductOption"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsDropDown(ProductOptionDropDown.title, ProductOptionValueDropdown1.price)}}" stepKey="checkDropDownProductOption"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsRadioButtons(ProductOptionRadiobutton.title, ProductOptionValueRadioButtons1.price)}}" stepKey="checkButtonsProductOption"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsCheckbox(ProductOptionCheckbox.title, '20.91')}}" stepKey="checkCheckboxProductOptiozn"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsMultiselect(ProductOptionMultiSelect.title, ProductOptionValueMultiSelect1.price)}}" stepKey="checkMultiSelectProductOption"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsData(ProductOptionDate.title, ProductOptionDate.price)}}" stepKey="checkDataProductOption"/> + + <!--Generate year--> + <generateDate date="Now" format="Y" stepKey="year"/> + <generateDate date="Now" format="y" stepKey="shortYear"/> + + <!-- Adding items to the checkout --> + + <fillField userInput="OptionField" selector="{{StorefrontProductInfoMainSection.productOptionFieldInput(ProductOptionField.title)}}" stepKey="fillProductOptionInputField"/> + <fillField userInput="OptionArea" selector="{{StorefrontProductInfoMainSection.productOptionAreaInput(ProductOptionArea.title)}}" stepKey="fillProductOptionInputArea"/> + <attachFile userInput="{{productWithOptions.file}}" selector="{{StorefrontProductInfoMainSection.addLinkFileUploadFile(ProductOptionFile.title)}}" stepKey="fillUploadFile"/> + <selectOption userInput="{{ProductOptionValueDropdown1.price}}" selector="{{StorefrontProductInfoMainSection.productOptionSelect(ProductOptionDropDown.title)}}" stepKey="seeProductOptionDropDown"/> + <checkOption selector="{{StorefrontProductInfoMainSection.productOptionRadioButtonsCheckbox(ProductOptionRadiobutton.title, ProductOptionValueRadioButtons1.price)}}" stepKey="seeProductOptionRadioButtons"/> + <checkOption selector="{{StorefrontProductInfoMainSection.productOptionRadioButtonsCheckbox(ProductOptionCheckbox.title, '20.91')}}" stepKey="seeProductOptionCheckbox"/> + <selectOption userInput="{{ProductOptionValueMultiSelect1.price}}" selector="{{StorefrontProductInfoMainSection.productOptionSelect(ProductOptionMultiSelect.title)}}" stepKey="selectProductOptionMultiSelect"/> + <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionDataMonth(ProductOptionDate.title)}}" stepKey="selectProductOptionDate"/> + <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionDataDay(ProductOptionDate.title)}}" stepKey="selectProductOptionDate1"/> + <selectOption userInput="$year" selector="{{StorefrontProductInfoMainSection.productOptionDataYear(ProductOptionDate.title)}}" stepKey="selectProductOptionDate2"/> + <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionDateAndTimeMonth(ProductOptionDateTime.title)}}" stepKey="selectProductOptionDateAndTimeMonth"/> + <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionDateAndTimeDay(ProductOptionDateTime.title)}}" stepKey="selectProductOptionDateAndTimeDay"/> + <selectOption userInput="$year" selector="{{StorefrontProductInfoMainSection.productOptionDateAndTimeYear(ProductOptionDateTime.title)}}" stepKey="selectProductOptionDateAndTimeYear"/> + <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionDateAndTimeHour(ProductOptionDateTime.title)}}" stepKey="selectProductOptionDateAndTimeHour"/> + <selectOption userInput="00" selector="{{StorefrontProductInfoMainSection.productOptionDateAndTimeMinute(ProductOptionDateTime.title)}}" stepKey="selectProductOptionDateAndTimeMinute"/> + <selectOption userInput="01" selector="{{StorefrontProductInfoMainSection.productOptionTimeHour(ProductOptionTime.title)}}" stepKey="selectProductOptionTimeHour"/> + <selectOption userInput="00" selector="{{StorefrontProductInfoMainSection.productOptionTimeMinute(ProductOptionTime.title)}}" stepKey="selectProductOptionTimeMinute"/> + + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="finalProductPrice"/> + + <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + + <!-- Checking the correctness of displayed custom options for user parameters on checkout --> + + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart" /> + + <conditionalClick selector="{{CheckoutPaymentSection.cartItemsArea}}" dependentSelector="{{CheckoutPaymentSection.cartItemsArea}}" visible="true" stepKey="exposeMiniCart"/> + + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskForCartItem"/> + <waitForElement selector="{{CheckoutPaymentSection.cartItemsAreaActive}}" time="30" stepKey="waitForCartItemsAreaActive"/> + + <see selector="{{CheckoutPaymentSection.cartItems}}" userInput="$$createProduct.name$$" stepKey="seeProductInCart"/> + + <conditionalClick selector="{{CheckoutPaymentSection.ProductOptionsByProductItemName($$createProduct.name$$)}}" dependentSelector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" visible="false" stepKey="exposeProductOptions"/> + + <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="{{ProductOptionField.title}}" stepKey="seeProductOptionFieldInput1"/> + <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="{{ProductOptionArea.title}}" stepKey="seeProductOptionAreaInput1"/> + <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="{{productWithOptions.file}}" stepKey="seeProductOptionFileInput1"/> + <seeElement selector="{{CheckoutPaymentSection.ProductOptionLinkActiveByProductItemName($$createProduct.name$$, productWithOptions.file)}}" stepKey="seeProductOptionFileInputLink1"/> + <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="{{ProductOptionValueDropdown1.title}}" stepKey="seeProductOptionValueDropdown1Input1"/> + <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="{{ProductOptionValueRadioButtons1.title}}" stepKey="seeProductOptionValueRadioButtons1Input1"/> + <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="{{ProductOptionValueCheckbox.title}}" stepKey="seeProductOptionValueCheckboxInput1" /> + <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="{{ProductOptionValueMultiSelect1.title}}" stepKey="seeproductAttributeOptionsMultiselect1Input1" /> + <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="Jan 1, $year" stepKey="seeProductOptionDateAndTimeInput" /> + <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="1/1/$shortYear, 1:00 AM" stepKey="seeProductOptionDataInput" /> + <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="1:00 AM" stepKey="seeProductOptionTimeInput" /> + <!--Select shipping method--> + <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> + <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterClickNext"/> + <!--Select payment method--> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod"/> + <!-- Place Order --> + <waitForElementVisible selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> + + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> + + <!-- Login to Admin and open Order --> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + + <actionGroup ref="filterOrderGridById" stepKey="filterByOrderId"> + <argument name="orderId" value="$grabOrderNumber"/> + </actionGroup> + <click selector="{{AdminDataGridTableSection.firstRow}}" stepKey="clickOrderRow"/> + <waitForPageLoad stepKey="waitForOrderPageOpened"/> + + <!-- Checking the correctness of displayed custom options for user parameters on Order --> + + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionField.title}}" stepKey="seeAdminOrderProductOptionField" /> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionArea.title}}" stepKey="seeAdminOrderProductOptionArea"/> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{productWithOptions.file}}" stepKey="seeAdminOrderProductOptionFile"/> + <seeElement selector="{{AdminOrderItemsOrderedSection.productNameOptionsLink(productWithOptions.file)}}" stepKey="seeAdminOrderProductOptionFileLink"/> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueDropdown1.title}}" stepKey="seeAdminOrderProductOptionValueDropdown1"/> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueRadioButtons1.title}}" stepKey="seeAdminOrderProductOptionValueRadioButton1"/> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueCheckbox.title}}" stepKey="seeAdminOrderProductOptionValueCheckbox" /> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueMultiSelect1.title}}" stepKey="seeAdminOrderproductAttributeOptionsMultiselect1" /> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="Jan 1, $year" stepKey="seeAdminOrderProductOptionDateAndTime" /> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="1/1/$shortYear, 1:00 AM" stepKey="seeAdminOrderProductOptionData" /> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="1:00 AM" stepKey="seeAdminOrderProductOptionTime" /> + + <!-- Reorder and Checking the correctness of displayed custom options for user parameters on Order and correctness of displayed price Subtotal--> + + <click selector="{{AdminOrderDetailsMainActionsSection.reorder}}" stepKey="clickReorder"/> + <actionGroup ref="AdminCheckoutSelectCheckMoneyOrderBillingMethodActionGroup" stepKey="selectBillingMethod"/> + <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="trySubmitOrder"/> + + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionField.title}}" stepKey="seeAdminOrderProductOptionField1" /> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionArea.title}}" stepKey="seeAdminOrderProductOptionArea1"/> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{productWithOptions.file}}" stepKey="seeAdminOrderProductOptionFile1"/> + <seeElement selector="{{AdminOrderItemsOrderedSection.productNameOptionsLink(productWithOptions.file)}}" stepKey="seeAdminOrderProductOptionFileLink1"/> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueDropdown1.title}}" stepKey="seeAdminOrderProductOptionValueDropdown11"/> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueRadioButtons1.title}}" stepKey="seeAdminOrderProductOptionValueRadioButton11"/> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueCheckbox.title}}" stepKey="seeAdminOrderProductOptionValueCheckbox1" /> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueMultiSelect1.title}}" stepKey="seeAdminOrderproductAttributeOptionsMultiselect11" /> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="Jan 1, $year" stepKey="seeAdminOrderProductOptionDateAndTime1" /> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="1/1/$shortYear, 1:00 AM" stepKey="seeAdminOrderProductOptionData1" /> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="1:00 AM" stepKey="seeAdminOrderProductOptionTime1" /> + + <see selector="{{AdminOrderTotalSection.subTotal}}" userInput="{$finalProductPrice}" stepKey="seeOrderSubTotal"/> + + <!-- Go to Customer Order Page and Checking the correctness of displayed custom options for user parameters on Order --> + + <amOnPage url="{{StorefrontCustomerOrderViewPage.url({$grabOrderNumber})}}" stepKey="amOnOrderPage"/> + + <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionField.title, ProductOptionField.title)}}" userInput="{{ProductOptionField.title}}" stepKey="seeStorefontOrderProductOptionField1" /> + <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionArea.title, ProductOptionArea.title)}}" userInput="{{ProductOptionArea.title}}" stepKey="seeStorefontOrderProductOptionArea1"/> + <see selector="{{StorefrontCustomerOrderSection.productCustomOptionsFile($$createProduct.name$$, ProductOptionFile.title, productWithOptions.file)}}" userInput="{{productWithOptions.file}}" stepKey="seeStorefontOrderProductOptionFile1"/> + <seeElement selector="{{StorefrontCustomerOrderSection.productCustomOptionsLink($createProduct.name$, ProductOptionFile.title, productWithOptions.file)}}" stepKey="seeStorefontOrderProductOptionFileLink1"/> + <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionDropDown.title, ProductOptionValueDropdown1.title)}}" userInput="{{ProductOptionValueDropdown1.title}}" stepKey="seeStorefontOrderProductOptionValueDropdown11"/> + <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionRadiobutton.title, ProductOptionValueRadioButtons1.title)}}" userInput="{{ProductOptionValueRadioButtons1.title}}" stepKey="seeStorefontOrderProductOptionValueRadioButtons11"/> + <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionCheckbox.title, ProductOptionValueCheckbox.title)}}" userInput="{{ProductOptionValueCheckbox.title}}" stepKey="seeStorefontOrderProductOptionValueCheckbox1" /> + <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionMultiSelect.title, ProductOptionValueMultiSelect1.title)}}" userInput="{{ProductOptionValueMultiSelect1.title}}" stepKey="seeStorefontOrderproductAttributeOptionsMultiselect11" /> + <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionDate.title, 'Jan 1, $year')}}" userInput="Jan 1, $year" stepKey="seeStorefontOrderProductOptionDateAndTime1" /> + <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionDateTime.title, '1/1/$shortYear, 1:00 AM')}}" userInput="1/1/$shortYear, 1:00 AM" stepKey="seeStorefontOrderProductOptionData1" /> + <see selector="{{StorefrontCustomerOrderSection.productCustomOptions($$createProduct.name$$, ProductOptionTime.title, '1:00 AM')}}" userInput="1:00 AM" stepKey="seeStorefontOrderProductOptionTime1" /> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitle.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitle.xml index 9d7c61623845..04cb813ec0ef 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitle.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitle.xml @@ -17,11 +17,6 @@ <description value="Admin should be able to see the full title of the selected custom option value in the order"/> <severity value="MAJOR"/> <testCaseId value="MC-3043"/> - <group value="skip"/> - <!-- Skip due to MQE-1128 --> - <skip> - <issueId value="MQE-1128"/> - </skip> </annotations> <before> <!--Create Simple Product with Custom Options--> @@ -38,6 +33,8 @@ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearFilters"/> + <actionGroup ref="logout" stepKey="logout"/> </after> <!-- Login Customer Storefront --> @@ -76,11 +73,20 @@ <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="{{ProductOptionValueDropdownLongTitle1.title}}" stepKey="seeProductOptionValueDropdown1Input1"/> + <!--Select shipping method--> + + <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> + <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterClickNext"/> + + <!-- Checkout select Check/Money Order payment --> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> <!-- Place Order --> - <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> @@ -91,6 +97,7 @@ <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearOnOrdersPage"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearGridFilter"/> <fillField selector="{{AdminOrdersGridSection.search}}" userInput="{$grabOrderNumber}" stepKey="fillOrderNum"/> <click selector="{{AdminOrdersGridSection.submitSearch}}" stepKey="submitSearchOrderNum"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearOnSearch"/> @@ -106,6 +113,6 @@ </assertEquals> <moveMouseOver selector="{{AdminOrderItemsOrderedSection.productNameOptions}} dd" stepKey="hoverProduct"/> <waitForElementVisible selector="{{AdminOrderItemsOrderedSection.productNameOptions}} dd:nth-child(2)" stepKey="waitForCustomOptionValueFullName"/> - <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueDropdownLongTitle1.title}}" stepKey="seeAdminOrderProductOptionValueDropdown1"/> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="Optisfvdklvfnkljvnfdklpvnfdjklfdvnjkvfdkjnvfdjkfvndj111 11Optisfvdklvfnkljvnfdklpvnfdjklfdvnjkvfdkjnvfdjkfvndj11111" stepKey="seeAdminOrderProductOptionValueDropdown1"/> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.xml new file mode 100644 index 000000000000..0ed61b8636c4 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.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="StorefrontRememberCategoryPaginationTest"> + <annotations> + <title value="Verify that Number of Products per page retained when visiting a different category"/> + <stories value="MAGETWO-61478: Number of Products displayed per page not retained when visiting a different category"/> + <description value="Verify that Number of Products per page retained when visiting a different category"/> + <features value="Catalog"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-94210"/> + <group value="Catalog"/> + </annotations> + + <before> + <createData entity="_defaultCategory" stepKey="defaultCategory1"/> + <createData entity="SimpleProduct" stepKey="simpleProduct1"> + <requiredEntity createDataKey="defaultCategory1"/> + </createData> + + <createData entity="_defaultCategory" stepKey="defaultCategory2"/> + <createData entity="SimpleProduct" stepKey="simpleProduct2"> + <requiredEntity createDataKey="defaultCategory2"/> + </createData> + + <createData entity="RememberPaginationCatalogStorefrontConfig" stepKey="setRememberPaginationCatalogStorefrontConfig"/> + </before> + + <actionGroup ref="GoToStorefrontCategoryPageByParameters" stepKey="GoToStorefrontCategory1Page"> + <argument name="category" value="$$defaultCategory1.custom_attributes[url_key]$$"/> + <argument name="mode" value="grid"/> + <argument name="numOfProductsPerPage" value="12"/> + </actionGroup> + + <actionGroup ref="VerifyCategoryPageParameters" stepKey="verifyCategory1PageParameters"> + <argument name="category" value="$$defaultCategory1$$"/> + <argument name="mode" value="grid"/> + <argument name="numOfProductsPerPage" value="12"/> + </actionGroup> + + <amOnPage url="{{StorefrontCategoryPage.url($$defaultCategory2.name$$)}}" stepKey="navigateToCategory2Page"/> + <waitForPageLoad stepKey="waitForCategory2PageToLoad"/> + + <actionGroup ref="VerifyCategoryPageParameters" stepKey="verifyCategory2PageParameters"> + <argument name="category" value="$$defaultCategory2$$"/> + <argument name="mode" value="grid"/> + <argument name="numOfProductsPerPage" value="12"/> + </actionGroup> + + <after> + <createData entity="DefaultCatalogStorefrontConfiguration" stepKey="setDefaultCatalogStorefrontConfiguration"/> + + <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> + <deleteData createDataKey="defaultCategory1" stepKey="deleteCategory1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteProduct2"/> + <deleteData createDataKey="defaultCategory2" stepKey="deleteCategory2"/> + + <magentoCLI command="cache:flush" stepKey="flushCache"/> + + </after> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest.xml new file mode 100644 index 000000000000..268e18d2b4ef --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest.xml @@ -0,0 +1,94 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest"> + <annotations> + <features value="Catalog"/> + <title value="Check that special price displayed when 'default config' scope timezone does not match 'website' scope timezone"/> + <description value="Check that special price displayed when 'default config' scope timezone does not match 'website' scope timezone"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-97508"/> + <useCaseId value="MAGETWO-96847"/> + <group value="Catalog"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + + <!--Create product--> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + + <!--Create customer--> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + <after> + <!--Delete create data--> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Set timezone for default config--> + <amOnPage url="{{GeneralConfigurationPage.url}}" stepKey="goToGeneralConfig"/> + <waitForPageLoad stepKey="waitForConfigPage"/> + <conditionalClick selector="{{LocaleOptionsSection.sectionHeader}}" dependentSelector="{{LocaleOptionsSection.timezone}}" visible="false" stepKey="openLocaleSection"/> + <grabValueFrom selector="{{LocaleOptionsSection.timezone}}" stepKey="originalTimezone"/> + <selectOption selector="{{LocaleOptionsSection.timezone}}" userInput="Central European Standard Time (Europe/Paris)" stepKey="setTimezone"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="saveConfig"/> + + <!--Set timezone for Main Website--> + <amOnPage url="{{GeneralConfigurationPage.url}}" stepKey="goToGeneralConfig1"/> + <waitForPageLoad stepKey="waitForConfigPage1"/> + <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="AdminSwitchStoreViewActionGroup"> + <argument name="website" value="_defaultWebsite"/> + </actionGroup> + <conditionalClick selector="{{LocaleOptionsSection.sectionHeader}}" dependentSelector="{{LocaleOptionsSection.timezone}}" visible="false" stepKey="openLocaleSection1"/> + <uncheckOption selector="{{LocaleOptionsSection.useDefault}}" stepKey="uncheckUseDefault"/> + <grabValueFrom selector="{{LocaleOptionsSection.timezone}}" stepKey="originalTimezone1"/> + <selectOption selector="{{LocaleOptionsSection.timezone}}" userInput="Greenwich Mean Time (Africa/Abidjan)" stepKey="setTimezone1"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="saveConfig1"/> + + <!--Set special price to created product--> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="openAdminEditPage"/> + <actionGroup ref="AddSpecialPriceToProductActionGroup" stepKey="setSpecialPriceToCreatedProduct"> + <argument name="price" value="15"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProductForm"/> + + <!--Login to storefront from customer and check price--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="logInFromCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!--Go to the product page and check special price--> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="amOnSimpleProductPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.specialPriceValue}}" stepKey="grabSpecialPrice"/> + <assertEquals expected='$15.00' expectedType="string" actual="$grabSpecialPrice" stepKey="assertSpecialPrice"/> + + <!--Reset timezone--> + <amOnPage url="{{GeneralConfigurationPage.url}}" stepKey="goToGeneralConfigReset"/> + <waitForPageLoad stepKey="waitForConfigPageReset"/> + <conditionalClick selector="{{LocaleOptionsSection.sectionHeader}}" dependentSelector="{{LocaleOptionsSection.timezone}}" visible="false" stepKey="openLocaleSectionReset"/> + <selectOption selector="{{LocaleOptionsSection.timezone}}" userInput="$originalTimezone" stepKey="resetTimezone"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="saveConfigReset"/> + + <!--Reset timezone--> + <amOnPage url="{{GeneralConfigurationPage.url}}" stepKey="goToGeneralConfigReset1"/> + <waitForPageLoad stepKey="waitForConfigPageReset1"/> + <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="AdminSwitchStoreViewActionGroup1"> + <argument name="website" value="_defaultWebsite"/> + </actionGroup> + <conditionalClick selector="{{LocaleOptionsSection.sectionHeader}}" dependentSelector="{{LocaleOptionsSection.timezone}}" visible="false" stepKey="openLocaleSectionReset1"/> + <uncheckOption selector="{{LocaleOptionsSection.useDefault}}" stepKey="uncheckUseDefault1"/> + <selectOption selector="{{LocaleOptionsSection.timezone}}" userInput="$originalTimezone" stepKey="resetTimezone1"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="saveConfigReset1"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/TieredPricingAndQuantityIncrementsWorkWithDecimalinventoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/TieredPricingAndQuantityIncrementsWorkWithDecimalinventoryTest.xml new file mode 100644 index 000000000000..4d7c97b26457 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/TieredPricingAndQuantityIncrementsWorkWithDecimalinventoryTest.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="TieredPricingAndQuantityIncrementsWorkWithDecimalinventoryTest"> + <annotations> + <features value="Catalog"/> + <stories value="Tiered pricing and quantity increments work with decimal inventory"/> + <title value="Tiered pricing and quantity increments work with decimal inventory"/> + <description value="Tiered pricing and quantity increments work with decimal inventory"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-93973"/> + <group value="Catalog"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + <createData entity="SimpleProduct" stepKey="createPreReqSimpleProduct"> + <requiredEntity createDataKey="createPreReqCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> + <deleteData createDataKey="createPreReqSimpleProduct" stepKey="deletePreReqSimpleProduct"/> + </after> + <!--Step1. Login as admin. Go to Catalog > Products page. Filtering *prod1*. Open *prod1* to edit--> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin" /> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="filterGroupedProductOptions"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> + <click selector="{{AdminProductGridSection.productGridNameProduct('$$createPreReqSimpleProduct.name$$')}}" + stepKey="clickOpenProductForEdit"/> + <waitForPageLoad time="30" stepKey="waitForProductEditOpen"/> + <!--Step2. Open *Advanced Inventory* pop-up (Click on *Advanced Inventory* link). Set *Qty Uses Decimals* to *Yes*. Click on button *Done* --> + <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickOnAdvancedInventoryLink"/> + <scrollTo selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimals}}" stepKey="scrollToQtyUsesDecimalsDropBox"/> + <click selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimals}}" stepKey="clickOnQtyUsesDecimalsDropBox"/> + <click selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimalsOptions('1')}}" stepKey="chooseYesOnQtyUsesDecimalsDropBox"/> + <click selector="{{AdminProductFormAdvancedInventorySection.doneButton}}" stepKey="clickOnDoneButton"/> + <!--Step3. Open *Advanced Pricing* pop-up (Click on *Advanced Pricing* link). Click on *Add* button. Fill *0.5* in *Quantity*--> + <scrollTo selector="{{AdminProductFormSection.productName}}" stepKey="scrollToProductName"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingLink1"/> + <waitForElement selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="waitForAddButton"/> + <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="clickOnCustomerGroupPriceAddButton"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="0.5" stepKey="fillProductTierPriceQty"/> + <!--Step4. Close *Advanced Pricing* (Click on button *Done*). Save *prod1* (Click on button *Save*)--> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickOnDoneButton2"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> + + <!--The code should be uncommented after fix MAGETWO-96016--> + <!--<click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingLink2"/>--> + <!--<seeInField userInput="0.5" selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" stepKey="seeInField1"/>--> + <!--<click selector="{{AdminProductFormAdvancedPricingSection.advancedPricingCloseButton}}" stepKey="clickOnCloseButton"/>--> + + <!--Step5. Open *Advanced Inventory* pop-up. Set *Enable Qty Increments* to *Yes*. Fill *.5* in *Qty Increments*--> + <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickOnAdvancedInventoryLink2"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <scrollTo selector="{{AdminProductFormAdvancedInventorySection.enableQtyIncrements}}" stepKey="scrollToEnableQtyIncrements"/> + <click selector="{{AdminProductFormAdvancedInventorySection.enableQtyIncrementsUseConfigSettings}}" stepKey="clickOnEnableQtyIncrementsUseConfigSettingsCheckbox"/> + <click selector="{{AdminProductFormAdvancedInventorySection.enableQtyIncrements}}" stepKey="clickOnEnableQtyIncrements"/> + <click selector="{{AdminProductFormAdvancedInventorySection.enableQtyIncrementsOptions(('1'))}}" stepKey="chooseYesOnEnableQtyIncrements"/> + <scrollTo selector="{{AdminProductFormAdvancedInventorySection.qtyIncrementsUseConfigSettings}}" stepKey="scrollToQtyIncrementsUseConfigSettings"/> + <click selector="{{AdminProductFormAdvancedInventorySection.qtyIncrementsUseConfigSettings}}" stepKey="clickOnQtyIncrementsUseConfigSettings"/> + <scrollTo selector="{{AdminProductFormAdvancedInventorySection.qtyIncrements}}" stepKey="scrollToQtyIncrements"/> + <fillField selector="{{AdminProductFormAdvancedInventorySection.qtyIncrements}}" userInput=".5" stepKey="fillQtyIncrements"/> + <!--Step6. Close *Advanced Inventory* (Click on button *Done*). Save *prod1* (Click on button *Save*) --> + <click selector="{{AdminProductFormAdvancedInventorySection.doneButton}}" stepKey="clickOnDoneButton3"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton2"/> + <!--Step7. Open *Customer view* (Go to *Store Front*). Open *prod1* page (Find via search and click on product name) --> + <amOnPage url="{{StorefrontHomePage.url}}$$createPreReqSimpleProduct.custom_attributes[url_key]$$.html" stepKey="amOnProductPage"/> + <!--Step8. Fill *1.5* in *Qty*. Click on button *Add to Cart*--> + <fillField selector="{{StorefrontProductPageSection.qtyInput}}" userInput="1.5" stepKey="fillQty"/> + <click selector="{{StorefrontProductPageSection.addToCartBtn}}" stepKey="clickOnAddToCart"/> + <waitForElementVisible selector="{{StorefrontProductPageSection.successMsg}}" time="30" stepKey="waitForProductAdded"/> + <see selector="{{StorefrontCategoryMainSection.SuccessMsg}}" userInput="You added $$createPreReqSimpleProduct.name$$ to your shopping cart." stepKey="seeAddedToCartMessage"/> + <!--Step9. Click on *Cart* icon. Click on *View and Edit Cart* link. Change *Qty* value to *5.5*--> + <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="goToShoppingCartFromMinicart"/> + <fillField selector="{{CheckoutCartProductSection.ProductQuantityByName(('$$createPreReqSimpleProduct.name$$'))}}" userInput="5.5" stepKey="fillQty2"/> + <click selector="{{CheckoutCartProductSection.updateShoppingCartButton}}" stepKey="clickOnUpdateShoppingCartButton"/> + <seeInField userInput="5.5" selector="{{CheckoutCartProductSection.ProductQuantityByName(('$$createPreReqSimpleProduct.name$$'))}}" stepKey="seeInField2"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnProductTest.xml index 26dd94b19de6..29ed3af4f01d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnProductTest.xml @@ -32,16 +32,19 @@ <click selector="{{AdminProductFormSection.contentTab}}" stepKey="clickContentTab" /> <waitForElementVisible selector="{{ProductDescriptionWYSIWYGToolbarSection.TinyMCE4}}" stepKey="waitForDescription" /> <seeElement selector="{{ProductDescriptionWYSIWYGToolbarSection.TinyMCE4}}" stepKey="TinyMCE4Description" /> - <waitForElementVisible selector="{{ProductShortDescriptionWYSIWYGToolbarSection.TinyMCE4}}" stepKey="waitForShortDescription" /> - <seeElement selector="{{ProductShortDescriptionWYSIWYGToolbarSection.TinyMCE4}}" stepKey="TinyMCE4ShortDescription" /> + <click selector="{{ProductDescriptionWysiwygSection.EditArea}}" stepKey="focusProductDescriptionWysiwyg"/> <executeJS function="tinyMCE.get('product_form_description').setContent('Hello World!');" stepKey="executeJSFillContent1"/> + <waitForElementVisible selector="{{ProductShortDescriptionWYSIWYGToolbarSection.TinyMCE4}}" stepKey="waitForShortDescription" /> + <seeElement selector="{{ProductShortDescriptionWYSIWYGToolbarSection.TinyMCE4}}" stepKey="TinyMCE4ShortDescription" /> + <click selector="{{ProductShortDescriptionWysiwygSection.EditArea}}" stepKey="focusProductShortDescriptionWysiwyg"/> <executeJS function="tinyMCE.get('product_form_short_description').setContent('Hello World! Short Content');" stepKey="executeJSFillContent2"/> + <scrollTo selector="{{ProductDescriptionWYSIWYGToolbarSection.showHideBtn}}" y="-150" x="0" stepKey="scrollToDesShowHideBtn1" /> <click selector="{{ProductDescriptionWYSIWYGToolbarSection.showHideBtn}}" stepKey="clickShowHideBtn1" /> <waitForElementVisible selector="{{ProductDescriptionWYSIWYGToolbarSection.InsertImageBtn}}" stepKey="waitForInsertImage1" /> <see selector="{{ProductDescriptionWYSIWYGToolbarSection.InsertImageBtn}}" userInput="Insert Image..." stepKey="seeInsertImage1"/> <dontSee selector="{{TinyMCESection.InsertWidgetBtn}}" stepKey="insertWidget1" /> <dontSee selector="{{TinyMCESection.InsertVariableBtn}}" stepKey="insertVariable1" /> - <scrollTo selector="{{ProductDescriptionWYSIWYGToolbarSection.showHideBtn}}" stepKey="scrollToDesShowHideBtn" /> + <scrollTo selector="{{ProductDescriptionWYSIWYGToolbarSection.showHideBtn}}" stepKey="scrollToDesShowHideBtn2" /> <click selector="{{ProductShortDescriptionWYSIWYGToolbarSection.showHideBtn}}" stepKey="clickShowHideBtn2" /> <waitForElementVisible selector="{{ProductShortDescriptionWYSIWYGToolbarSection.InsertImageBtn}}" stepKey="waitForInsertImage2" /> <see selector="{{ProductShortDescriptionWYSIWYGToolbarSection.InsertImageBtn}}" userInput="Insert Image..." stepKey="seeInsertImage2"/> diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php index 804eef25ebdd..249c32ff276c 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php @@ -6,9 +6,13 @@ namespace Magento\Catalog\Test\Unit\Block\Adminhtml\Product\Helper\Form\Gallery; use Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery\Content; -use Magento\Framework\Filesystem; +use Magento\Catalog\Model\Entity\Attribute; +use Magento\Catalog\Model\Product; use Magento\Framework\Phrase; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class ContentTest extends \PHPUnit\Framework\TestCase { /** @@ -219,4 +223,146 @@ public function testGetImagesJsonWithException() $this->assertSame(json_encode($imagesResult), $this->content->getImagesJson()); } + + /** + * Test GetImageTypes() will return value for given attribute from data persistor. + * + * @return void + */ + public function testGetImageTypesFromDataPersistor() + { + $attributeCode = 'thumbnail'; + $value = 'testImageValue'; + $scopeLabel = 'testScopeLabel'; + $label = 'testLabel'; + $name = 'testName'; + $expectedTypes = [ + $attributeCode => [ + 'code' => $attributeCode, + 'value' => $value, + 'label' => $label, + 'name' => $name, + ], + ]; + $product = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->getMock(); + $product->expects($this->once()) + ->method('getData') + ->with($this->identicalTo($attributeCode)) + ->willReturn(null); + $mediaAttribute = $this->getMediaAttribute($label, $attributeCode); + $product->expects($this->once()) + ->method('getMediaAttributes') + ->willReturn([$mediaAttribute]); + $this->galleryMock->expects($this->exactly(2)) + ->method('getDataObject') + ->willReturn($product); + $this->galleryMock->expects($this->once()) + ->method('getImageValue') + ->with($this->identicalTo($attributeCode)) + ->willReturn($value); + $this->galleryMock->expects($this->once()) + ->method('getScopeLabel') + ->with($this->identicalTo($mediaAttribute)) + ->willReturn($scopeLabel); + $this->galleryMock->expects($this->once()) + ->method('getAttributeFieldName') + ->with($this->identicalTo($mediaAttribute)) + ->willReturn($name); + $this->getImageTypesAssertions($attributeCode, $scopeLabel, $expectedTypes); + } + + /** + * Test GetImageTypes() will return value for given attribute from product. + * + * @return void + */ + public function testGetImageTypesFromProduct() + { + $attributeCode = 'thumbnail'; + $value = 'testImageValue'; + $scopeLabel = 'testScopeLabel'; + $label = 'testLabel'; + $name = 'testName'; + $expectedTypes = [ + $attributeCode => [ + 'code' => $attributeCode, + 'value' => $value, + 'label' => $label, + 'name' => $name, + ], + ]; + $product = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->getMock(); + $product->expects($this->once()) + ->method('getData') + ->with($this->identicalTo($attributeCode)) + ->willReturn($value); + $mediaAttribute = $this->getMediaAttribute($label, $attributeCode); + $product->expects($this->once()) + ->method('getMediaAttributes') + ->willReturn([$mediaAttribute]); + $this->galleryMock->expects($this->exactly(2)) + ->method('getDataObject') + ->willReturn($product); + $this->galleryMock->expects($this->never()) + ->method('getImageValue'); + $this->galleryMock->expects($this->once()) + ->method('getScopeLabel') + ->with($this->identicalTo($mediaAttribute)) + ->willReturn($scopeLabel); + $this->galleryMock->expects($this->once()) + ->method('getAttributeFieldName') + ->with($this->identicalTo($mediaAttribute)) + ->willReturn($name); + $this->getImageTypesAssertions($attributeCode, $scopeLabel, $expectedTypes); + } + + /** + * Perform assertions. + * + * @param string $attributeCode + * @param string $scopeLabel + * @param array $expectedTypes + * @return void + */ + private function getImageTypesAssertions(string $attributeCode, string $scopeLabel, array $expectedTypes) + { + $this->content->setElement($this->galleryMock); + $result = $this->content->getImageTypes(); + $scope = $result[$attributeCode]['scope']; + $this->assertSame($scopeLabel, $scope->getText()); + unset($result[$attributeCode]['scope']); + $this->assertSame($expectedTypes, $result); + } + + /** + * Get media attribute mock. + * + * @param string $label + * @param string $attributeCode + * @return \PHPUnit_Framework_MockObject_MockObject + */ + private function getMediaAttribute(string $label, string $attributeCode) + { + $frontend = $this->getMockBuilder(Product\Attribute\Frontend\Image::class) + ->disableOriginalConstructor() + ->getMock(); + $frontend->expects($this->once()) + ->method('getLabel') + ->willReturn($label); + $mediaAttribute = $this->getMockBuilder(Attribute::class) + ->disableOriginalConstructor() + ->getMock(); + $mediaAttribute->expects($this->any()) + ->method('getAttributeCode') + ->willReturn($attributeCode); + $mediaAttribute->expects($this->once()) + ->method('getFrontend') + ->willReturn($frontend); + + return $mediaAttribute; + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/GalleryTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/GalleryTest.php index 06e2368f3080..1e04680676eb 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/GalleryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/GalleryTest.php @@ -5,6 +5,8 @@ */ namespace Magento\Catalog\Test\Unit\Block\Adminhtml\Product\Helper\Form; +use Magento\Framework\App\Request\DataPersistorInterface; + class GalleryTest extends \PHPUnit\Framework\TestCase { /** @@ -32,18 +34,27 @@ class GalleryTest extends \PHPUnit\Framework\TestCase */ protected $objectManager; + /** + * @var DataPersistorInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $dataPersistorMock; + public function setUp() { $this->registryMock = $this->createMock(\Magento\Framework\Registry::class); $this->productMock = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['getData']); $this->formMock = $this->createMock(\Magento\Framework\Data\Form::class); - + $this->dataPersistorMock = $this->getMockBuilder(DataPersistorInterface::class) + ->disableOriginalConstructor() + ->setMethods(['get']) + ->getMockForAbstractClass(); $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->gallery = $this->objectManager->getObject( \Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery::class, [ 'registry' => $this->registryMock, - 'form' => $this->formMock + 'form' => $this->formMock, + 'dataPersistor' => $this->dataPersistorMock ] ); } @@ -70,6 +81,68 @@ public function testGetImages() $this->assertSame($mediaGallery, $this->gallery->getImages()); } + /** + * Test getImages() will try get data from data persistor, if it's absent in registry. + * + * @return void + */ + public function testGetImagesWithDataPersistor() + { + $product = [ + 'product' => [ + 'media_gallery' => [ + 'images' => [ + [ + 'value_id' => '1', + 'file' => 'image_1.jpg', + 'media_type' => 'image', + ], + [ + 'value_id' => '2', + 'file' => 'image_2.jpg', + 'media_type' => 'image', + ], + ], + ], + ], + ]; + $this->registryMock->expects($this->once())->method('registry')->willReturn($this->productMock); + $this->productMock->expects($this->once())->method('getData')->willReturn(null); + $this->dataPersistorMock->expects($this->once()) + ->method('get') + ->with($this->identicalTo('catalog_product')) + ->willReturn($product); + + $this->assertSame($product['product']['media_gallery'], $this->gallery->getImages()); + } + + /** + * Test get image value from data persistor in case it's absent in product from registry. + * + * @return void + */ + public function testGetImageValue() + { + $product = [ + 'product' => [ + 'media_gallery' => [ + 'images' => [ + 'value_id' => '1', + 'file' => 'image_1.jpg', + 'media_type' => 'image', + ], + ], + 'small' => 'testSmallImage', + 'thumbnail' => 'testThumbnail' + ] + ]; + $this->dataPersistorMock->expects($this->once()) + ->method('get') + ->with($this->identicalTo('catalog_product')) + ->willReturn($product); + $this->assertSame($product['product']['small'], $this->gallery->getImageValue('small')); + } + public function testGetDataObject() { $this->registryMock->expects($this->once())->method('registry')->willReturn($this->productMock); diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Rss/NotifyStockTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Rss/NotifyStockTest.php index 1dd866f1fe2c..da35d845468d 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Rss/NotifyStockTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Rss/NotifyStockTest.php @@ -96,7 +96,12 @@ public function testGetRssData() $this->urlBuilder->expects($this->once())->method('getUrl') ->with('catalog/product/edit', ['id' => 1, '_secure' => true, '_nosecret' => true]) ->will($this->returnValue('http://magento.com/catalog/product/edit/id/1')); - $this->assertEquals($this->rssFeed, $this->block->getRssData()); + + $data = $this->block->getRssData(); + $this->assertTrue(is_string($data['title'])); + $this->assertTrue(is_string($data['description'])); + $this->assertTrue(is_string($data['entries'][0]['description'])); + $this->assertEquals($this->rssFeed, $data); } public function testGetCacheLifetime() diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Product/ImageFactoryTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/ImageFactoryTest.php index 8a42865a3fe4..95b06e40602b 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Product/ImageFactoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Product/ImageFactoryTest.php @@ -145,7 +145,8 @@ private function getTestDataWithoutAttributes(): array 'label' => 'test_image_label', 'ratio' => 1, 'custom_attributes' => '', - 'product_id' => null + 'product_id' => null, + 'class' => 'product-image-photo' ], ], ]; @@ -190,6 +191,7 @@ private function getTestDataWithAttributes(): array 'custom_attributes' => [ 'name_1' => 'value_1', 'name_2' => 'value_2', + 'class' => 'my-class' ], ], 'expected' => [ @@ -201,7 +203,8 @@ private function getTestDataWithAttributes(): array 'label' => 'test_product_name', 'ratio' => 0.5, // <== 'custom_attributes' => 'name_1="value_1" name_2="value_2"', - 'product_id' => null + 'product_id' => null, + 'class' => 'my-class' ], ], ]; diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Product/ProductList/ToolbarTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/ProductList/ToolbarTest.php index ac963326dbfa..884f4c543c8b 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Product/ProductList/ToolbarTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Product/ProductList/ToolbarTest.php @@ -18,6 +18,11 @@ class ToolbarTest extends \PHPUnit\Framework\TestCase */ protected $model; + /** + * @var \Magento\Catalog\Model\Product\ProductList\ToolbarMemorizer | \PHPUnit_Framework_MockObject_MockObject + */ + private $memorizer; + /** * @var \Magento\Framework\Url | \PHPUnit_Framework_MockObject_MockObject */ @@ -62,6 +67,16 @@ protected function setUp() 'getLimit', 'getCurrentPage' ]); + $this->memorizer = $this->createPartialMock( + \Magento\Catalog\Model\Product\ProductList\ToolbarMemorizer::class, + [ + 'getDirection', + 'getOrder', + 'getMode', + 'getLimit', + 'isMemorizingAllowed' + ] + ); $this->layout = $this->createPartialMock(\Magento\Framework\View\Layout::class, ['getChildName', 'getBlock']); $this->pagerBlock = $this->createPartialMock(\Magento\Theme\Block\Html\Pager::class, [ 'setUseContainer', @@ -116,6 +131,7 @@ protected function setUp() 'context' => $context, 'catalogConfig' => $this->catalogConfig, 'toolbarModel' => $this->model, + 'toolbarMemorizer' => $this->memorizer, 'urlEncoder' => $this->urlEncoder, 'productListHelper' => $this->productListHelper ] @@ -155,7 +171,7 @@ public function testGetPagerEncodedUrl() public function testGetCurrentOrder() { $order = 'price'; - $this->model->expects($this->once()) + $this->memorizer->expects($this->once()) ->method('getOrder') ->will($this->returnValue($order)); $this->catalogConfig->expects($this->once()) @@ -169,7 +185,7 @@ public function testGetCurrentDirection() { $direction = 'desc'; - $this->model->expects($this->once()) + $this->memorizer->expects($this->once()) ->method('getDirection') ->will($this->returnValue($direction)); @@ -183,7 +199,7 @@ public function testGetCurrentMode() $this->productListHelper->expects($this->once()) ->method('getAvailableViewMode') ->will($this->returnValue(['list' => 'List'])); - $this->model->expects($this->once()) + $this->memorizer->expects($this->once()) ->method('getMode') ->will($this->returnValue($mode)); @@ -232,11 +248,11 @@ public function testGetLimit() $mode = 'list'; $limit = 10; - $this->model->expects($this->once()) + $this->memorizer->expects($this->once()) ->method('getMode') ->will($this->returnValue($mode)); - $this->model->expects($this->once()) + $this->memorizer->expects($this->once()) ->method('getLimit') ->will($this->returnValue($limit)); $this->productListHelper->expects($this->once()) @@ -266,7 +282,7 @@ public function testGetPagerHtml() $this->productListHelper->expects($this->exactly(2)) ->method('getAvailableLimit') ->will($this->returnValue([10 => 10, 20 => 20])); - $this->model->expects($this->once()) + $this->memorizer->expects($this->once()) ->method('getLimit') ->will($this->returnValue($limit)); $this->pagerBlock->expects($this->once()) diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryOptionsTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryOptionsTest.php new file mode 100644 index 000000000000..7ed8b13fce75 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryOptionsTest.php @@ -0,0 +1,223 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Catalog\Test\Unit\Block\Product\View; + +use Magento\Catalog\Block\Product\Context; +use Magento\Catalog\Block\Product\View\Gallery; +use Magento\Catalog\Block\Product\View\GalleryOptions; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\Escaper; +use Magento\Framework\View\Config; +use Magento\Framework\Config\View; +use Magento\Framework\Serialize\Serializer\Json; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class GalleryOptionsTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var GalleryOptions + */ + private $model; + + /** + * @var Gallery|\PHPUnit_Framework_MockObject_MockObject + */ + private $gallery; + + /** + * @var Context|\PHPUnit_Framework_MockObject_MockObject + */ + private $context; + + /** + * @var Json + */ + private $jsonSerializer; + + /** + * @var View|\PHPUnit_Framework_MockObject_MockObject + */ + private $configView; + + /** + * @var Config|\PHPUnit_Framework_MockObject_MockObject + */ + private $viewConfig; + + /** + * @var Escaper + */ + private $escaper; + + protected function setUp() + { + $objectManager = new ObjectManager($this); + + $this->escaper = $objectManager->getObject(Escaper::class); + $this->configView = $this->createMock(View::class); + + $this->viewConfig = $this->createConfiguredMock( + Config::class, + [ + 'getViewConfig' => $this->configView + ] + ); + + $this->context = $this->createConfiguredMock( + Context::class, + [ + 'getEscaper' => $this->escaper, + 'getViewConfig' => $this->viewConfig + ] + ); + + $this->gallery = $this->createMock(Gallery::class); + + $this->jsonSerializer = $objectManager->getObject( + Json::class + ); + + $this->model = $objectManager->getObject(GalleryOptions::class, [ + 'context' => $this->context, + 'jsonSerializer' => $this->jsonSerializer, + 'gallery' => $this->gallery + ]); + } + + public function testGetOptionsJson() + { + $configMap = [ + ['Magento_Catalog', 'gallery/nav', 'thumbs'], + ['Magento_Catalog', 'gallery/loop', false], + ['Magento_Catalog', 'gallery/keyboard', true], + ['Magento_Catalog', 'gallery/arrows', true], + ['Magento_Catalog', 'gallery/caption', false], + ['Magento_Catalog', 'gallery/allowfullscreen', true], + ['Magento_Catalog', 'gallery/navdir', 'horizontal'], + ['Magento_Catalog', 'gallery/navarrows', true], + ['Magento_Catalog', 'gallery/navtype', 'slides'], + ['Magento_Catalog', 'gallery/thumbmargin', '5'], + ['Magento_Catalog', 'gallery/transition/effect', 'slide'], + ['Magento_Catalog', 'gallery/transition/duration', '500'], + ]; + + $imageAttributesMap = [ + ['product_page_image_medium','height',null, 100], + ['product_page_image_medium','width',null, 200], + ['product_page_image_small','height',null, 300], + ['product_page_image_small','width',null, 400] + ]; + + $this->configView->expects($this->any()) + ->method('getVarValue') + ->will($this->returnValueMap($configMap)); + $this->gallery->expects($this->any()) + ->method('getImageAttribute') + ->will($this->returnValueMap($imageAttributesMap)); + + $json = $this->model->getOptionsJson(); + + $decodedJson = $this->jsonSerializer->unserialize($json); + + $this->assertSame('thumbs', $decodedJson['nav']); + $this->assertSame(false, $decodedJson['loop']); + $this->assertSame(true, $decodedJson['keyboard']); + $this->assertSame(true, $decodedJson['arrows']); + $this->assertSame(false, $decodedJson['showCaption']); + $this->assertSame(true, $decodedJson['allowfullscreen']); + $this->assertSame('horizontal', $decodedJson['navdir']); + $this->assertSame(true, $decodedJson['navarrows']); + $this->assertSame('slides', $decodedJson['navtype']); + $this->assertSame(5, $decodedJson['thumbmargin']); + $this->assertSame('slide', $decodedJson['transition']); + $this->assertSame(500, $decodedJson['transitionduration']); + $this->assertSame(100, $decodedJson['height']); + $this->assertSame(200, $decodedJson['width']); + $this->assertSame(300, $decodedJson['thumbheight']); + $this->assertSame(400, $decodedJson['thumbwidth']); + } + + public function testGetFSOptionsJson() + { + $configMap = [ + ['Magento_Catalog', 'gallery/fullscreen/nav', false], + ['Magento_Catalog', 'gallery/fullscreen/loop', true], + ['Magento_Catalog', 'gallery/fullscreen/keyboard', true], + ['Magento_Catalog', 'gallery/fullscreen/arrows', false], + ['Magento_Catalog', 'gallery/fullscreen/caption', true], + ['Magento_Catalog', 'gallery/fullscreen/navdir', 'vertical'], + ['Magento_Catalog', 'gallery/fullscreen/navarrows', false], + ['Magento_Catalog', 'gallery/fullscreen/navtype', 'thumbs'], + ['Magento_Catalog', 'gallery/fullscreen/thumbmargin', '10'], + ['Magento_Catalog', 'gallery/fullscreen/transition/effect', 'dissolve'], + ['Magento_Catalog', 'gallery/fullscreen/transition/duration', '300'] + ]; + + $this->configView->expects($this->any()) + ->method('getVarValue') + ->will($this->returnValueMap($configMap)); + + $json = $this->model->getFSOptionsJson(); + + $decodedJson = $this->jsonSerializer->unserialize($json); + + //Note, this tests the special case for nav variable set to false. It + //Should not be converted to boolean. + $this->assertSame('false', $decodedJson['nav']); + $this->assertSame(true, $decodedJson['loop']); + $this->assertSame(false, $decodedJson['arrows']); + $this->assertSame(true, $decodedJson['keyboard']); + $this->assertSame(true, $decodedJson['showCaption']); + $this->assertSame('vertical', $decodedJson['navdir']); + $this->assertSame(false, $decodedJson['navarrows']); + $this->assertSame(10, $decodedJson['thumbmargin']); + $this->assertSame('thumbs', $decodedJson['navtype']); + $this->assertSame('dissolve', $decodedJson['transition']); + $this->assertSame(300, $decodedJson['transitionduration']); + } + + public function testGetOptionsJsonOptionals() + { + $configMap = [ + ['Magento_Catalog', 'gallery/fullscreen/thumbmargin', false], + ['Magento_Catalog', 'gallery/fullscreen/transition/duration', false] + ]; + + $this->configView->expects($this->any()) + ->method('getVarValue') + ->will($this->returnValueMap($configMap)); + + $json = $this->model->getOptionsJson(); + + $decodedJson = $this->jsonSerializer->unserialize($json); + + $this->assertArrayNotHasKey('thumbmargin', $decodedJson); + $this->assertArrayNotHasKey('transitionduration', $decodedJson); + } + + public function testGetFSOptionsJsonOptionals() + { + $configMap = [ + ['Magento_Catalog', 'gallery/fullscreen/keyboard', false], + ['Magento_Catalog', 'gallery/fullscreen/thumbmargin', false], + ['Magento_Catalog', 'gallery/fullscreen/transition/duration', false] + ]; + + $this->configView->expects($this->any()) + ->method('getVarValue') + ->will($this->returnValueMap($configMap)); + + $json = $this->model->getFSOptionsJson(); + + $decodedJson = $this->jsonSerializer->unserialize($json); + + $this->assertArrayNotHasKey('thumbmargin', $decodedJson); + $this->assertArrayNotHasKey('keyboard', $decodedJson); + $this->assertArrayNotHasKey('transitionduration', $decodedJson); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryTest.php index c9b7dc50beb9..a81d8b1c9fc3 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryTest.php @@ -91,6 +91,94 @@ protected function setUp() ]); } + public function testGetGalleryImagesJsonWithLabel() + { + $this->prepareGetGalleryImagesJsonMocks(); + $json = $this->model->getGalleryImagesJson(); + $decodedJson = json_decode($json, true); + $this->assertEquals('product_page_image_small_url', $decodedJson[0]['thumb']); + $this->assertEquals('product_page_image_medium_url', $decodedJson[0]['img']); + $this->assertEquals('product_page_image_large_url', $decodedJson[0]['full']); + $this->assertEquals('test_label', $decodedJson[0]['caption']); + $this->assertEquals('2', $decodedJson[0]['position']); + $this->assertEquals(false, $decodedJson[0]['isMain']); + $this->assertEquals('test_media_type', $decodedJson[0]['type']); + $this->assertEquals('test_video_url', $decodedJson[0]['videoUrl']); + } + + public function testGetGalleryImagesJsonWithoutLabel() + { + $this->prepareGetGalleryImagesJsonMocks(false); + $json = $this->model->getGalleryImagesJson(); + $decodedJson = json_decode($json, true); + $this->assertEquals('test_product_name', $decodedJson[0]['caption']); + } + + private function prepareGetGalleryImagesJsonMocks($hasLabel = true) + { + $storeMock = $this->getMockBuilder(\Magento\Store\Model\Store::class) + ->disableOriginalConstructor() + ->getMock(); + + $productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) + ->disableOriginalConstructor() + ->getMock(); + + $productTypeMock = $this->getMockBuilder(\Magento\Catalog\Model\Product\Type\AbstractType::class) + ->disableOriginalConstructor() + ->getMock(); + $productTypeMock->expects($this->any()) + ->method('getStoreFilter') + ->with($productMock) + ->willReturn($storeMock); + + $productMock->expects($this->any()) + ->method('getTypeInstance') + ->willReturn($productTypeMock); + $productMock->expects($this->any()) + ->method('getMediaGalleryImages') + ->willReturn($this->getImagesCollectionWithPopulatedDataObject($hasLabel)); + $productMock->expects($this->any()) + ->method('getName') + ->willReturn('test_product_name'); + + $this->registry->expects($this->any()) + ->method('registry') + ->with('product') + ->willReturn($productMock); + + $this->imageHelper = $this->getMockBuilder(\Magento\Catalog\Helper\Image::class) + ->setMethods(['init', 'setImageFile', 'getUrl']) + ->disableOriginalConstructor() + ->getMock(); + + $this->imageHelper->expects($this->any()) + ->method('init') + ->willReturnMap([ + [$productMock, 'product_page_image_small', [], $this->imageHelper], + [$productMock, 'product_page_image_medium_no_frame', [], $this->imageHelper], + [$productMock, 'product_page_image_large_no_frame', [], $this->imageHelper], + ]) + ->willReturnSelf(); + $this->imageHelper->expects($this->any()) + ->method('setImageFile') + ->with('test_file') + ->willReturnSelf(); + $this->urlBuilder->expects($this->at(0)) + ->method('getUrl') + ->willReturn('product_page_image_small_url'); + $this->urlBuilder->expects($this->at(1)) + ->method('getUrl') + ->willReturn('product_page_image_medium_url'); + $this->urlBuilder->expects($this->at(2)) + ->method('getUrl') + ->willReturn('product_page_image_large_url'); + + $this->galleryImagesConfigMock->expects($this->exactly(2)) + ->method('getItems') + ->willReturn($this->getGalleryImagesConfigItems()); + } + public function testGetGalleryImages() { $productMock = $this->createMock(Product::class); @@ -163,4 +251,30 @@ private function getGalleryImagesConfigItems() ]) ]; } + + /** + * @return \Magento\Framework\Data\Collection + */ + private function getImagesCollectionWithPopulatedDataObject($hasLabel) + { + $collectionMock = $this->getMockBuilder(\Magento\Framework\Data\Collection::class) + ->disableOriginalConstructor() + ->getMock(); + + $items = [ + new \Magento\Framework\DataObject([ + 'file' => 'test_file', + 'label' => ($hasLabel ? 'test_label' : ''), + 'position' => '2', + 'media_type' => 'external-test_media_type', + "video_url" => 'test_video_url' + ]), + ]; + + $collectionMock->expects($this->any()) + ->method('getIterator') + ->willReturn(new \ArrayIterator($items)); + + return $collectionMock; + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/RefreshPathTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/RefreshPathTest.php index 45de62e218cf..adf00333721b 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/RefreshPathTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Category/RefreshPathTest.php @@ -66,8 +66,8 @@ private function setObjectProperty($object, string $propertyName, $value) : void */ public function testExecute() : void { - $value = ['id' => 3, 'path' => '1/2/3', 'parentId' => 2]; - $result = '{"id":3,"path":"1/2/3","parentId":"2"}'; + $value = ['id' => 3, 'path' => '1/2/3', 'parentId' => 2, 'level' => 2]; + $result = '{"id":3,"path":"1/2/3","parentId":"2","level":"2"}'; $requestMock = $this->getMockForAbstractClass(\Magento\Framework\App\RequestInterface::class); diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/SaveTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/SaveTest.php deleted file mode 100644 index de44af7f58af..000000000000 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Action/Attribute/SaveTest.php +++ /dev/null @@ -1,258 +0,0 @@ -<?php -/** - * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Action\Attribute; - -/** - * @SuppressWarnings(PHPMD.TooManyFields) - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class SaveTest extends \PHPUnit\Framework\TestCase -{ - /** @var \Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute\Save */ - protected $object; - - /** @var \Magento\Catalog\Helper\Product\Edit\Action\Attribute|\PHPUnit_Framework_MockObject_MockObject */ - protected $attributeHelper; - - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $dataObjectHelperMock; - - /** @var \Magento\CatalogInventory\Model\Indexer\Stock\Processor|\PHPUnit_Framework_MockObject_MockObject */ - protected $stockIndexerProcessor; - - /** @var \Magento\Backend\App\Action\Context|\PHPUnit_Framework_MockObject_MockObject */ - protected $context; - - /** @var \Magento\Framework\App\Request\Http|\PHPUnit_Framework_MockObject_MockObject */ - protected $request; - - /** @var \Magento\Framework\App\Response\Http|\PHPUnit_Framework_MockObject_MockObject */ - protected $response; - - /** @var \Magento\Framework\ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $objectManager; - - /** @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $eventManager; - - /** @var \Magento\Framework\UrlInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $url; - - /** @var \Magento\Framework\App\Response\RedirectInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $redirect; - - /** @var \Magento\Framework\App\ActionFlag|\PHPUnit_Framework_MockObject_MockObject */ - protected $actionFlag; - - /** @var \Magento\Framework\App\ViewInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $view; - - /** @var \Magento\Framework\Message\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $messageManager; - - /** @var \Magento\Backend\Model\Session|\PHPUnit_Framework_MockObject_MockObject */ - protected $session; - - /** @var \Magento\Framework\AuthorizationInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $authorization; - - /** @var \Magento\Backend\Model\Auth|\PHPUnit_Framework_MockObject_MockObject */ - protected $auth; - - /** @var \Magento\Backend\Helper\Data|\PHPUnit_Framework_MockObject_MockObject */ - protected $helper; - - /** @var \Magento\Backend\Model\UrlInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $backendUrl; - - /** @var \Magento\Framework\Data\Form\FormKey\Validator|\PHPUnit_Framework_MockObject_MockObject */ - protected $formKeyValidator; - - /** @var \Magento\Framework\Locale\ResolverInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $localeResolver; - - /** @var \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject */ - protected $product; - - /** @var \Magento\CatalogInventory\Api\StockRegistryInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $stockItemService; - - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $stockItem; - - /** @var \Magento\CatalogInventory\Api\StockConfigurationInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $stockConfig; - - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $stockItemRepository; - - /** - * @var \Magento\Backend\Model\View\Result\RedirectFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $resultRedirectFactory; - - protected function setUp() - { - $this->attributeHelper = $this->createPartialMock( - \Magento\Catalog\Helper\Product\Edit\Action\Attribute::class, - ['getProductIds', 'getSelectedStoreId', 'getStoreWebsiteId'] - ); - - $this->dataObjectHelperMock = $this->getMockBuilder(\Magento\Framework\Api\DataObjectHelper::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->stockIndexerProcessor = $this->createPartialMock( - \Magento\CatalogInventory\Model\Indexer\Stock\Processor::class, - ['reindexList'] - ); - - $resultRedirect = $this->getMockBuilder(\Magento\Backend\Model\View\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultRedirectFactory = $this->getMockBuilder(\Magento\Backend\Model\View\Result\RedirectFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - $this->resultRedirectFactory->expects($this->atLeastOnce()) - ->method('create') - ->willReturn($resultRedirect); - - $this->prepareContext(); - - $this->object = (new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this))->getObject( - \Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute\Save::class, - [ - 'context' => $this->context, - 'attributeHelper' => $this->attributeHelper, - 'stockIndexerProcessor' => $this->stockIndexerProcessor, - 'dataObjectHelper' => $this->dataObjectHelperMock, - ] - ); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - protected function prepareContext() - { - $this->stockItemRepository = $this->getMockBuilder( - \Magento\CatalogInventory\Api\StockItemRepositoryInterface::class - )->disableOriginalConstructor()->getMock(); - - $this->request = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) - ->disableOriginalConstructor()->getMock(); - $this->response = $this->createMock(\Magento\Framework\App\Response\Http::class); - $this->objectManager = $this->createMock(\Magento\Framework\ObjectManagerInterface::class); - $this->eventManager = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); - $this->url = $this->createMock(\Magento\Framework\UrlInterface::class); - $this->redirect = $this->createMock(\Magento\Framework\App\Response\RedirectInterface::class); - $this->actionFlag = $this->createMock(\Magento\Framework\App\ActionFlag::class); - $this->view = $this->createMock(\Magento\Framework\App\ViewInterface::class); - $this->messageManager = $this->createMock(\Magento\Framework\Message\ManagerInterface::class); - $this->session = $this->createMock(\Magento\Backend\Model\Session::class); - $this->authorization = $this->createMock(\Magento\Framework\AuthorizationInterface::class); - $this->auth = $this->createMock(\Magento\Backend\Model\Auth::class); - $this->helper = $this->createMock(\Magento\Backend\Helper\Data::class); - $this->backendUrl = $this->createMock(\Magento\Backend\Model\UrlInterface::class); - $this->formKeyValidator = $this->createMock(\Magento\Framework\Data\Form\FormKey\Validator::class); - $this->localeResolver = $this->createMock(\Magento\Framework\Locale\ResolverInterface::class); - - $this->context = $this->context = $this->createPartialMock(\Magento\Backend\App\Action\Context::class, [ - 'getRequest', - 'getResponse', - 'getObjectManager', - 'getEventManager', - 'getUrl', - 'getRedirect', - 'getActionFlag', - 'getView', - 'getMessageManager', - 'getSession', - 'getAuthorization', - 'getAuth', - 'getHelper', - 'getBackendUrl', - 'getFormKeyValidator', - 'getLocaleResolver', - 'getResultRedirectFactory' - ]); - $this->context->expects($this->any())->method('getRequest')->willReturn($this->request); - $this->context->expects($this->any())->method('getResponse')->willReturn($this->response); - $this->context->expects($this->any())->method('getObjectManager')->willReturn($this->objectManager); - $this->context->expects($this->any())->method('getEventManager')->willReturn($this->eventManager); - $this->context->expects($this->any())->method('getUrl')->willReturn($this->url); - $this->context->expects($this->any())->method('getRedirect')->willReturn($this->redirect); - $this->context->expects($this->any())->method('getActionFlag')->willReturn($this->actionFlag); - $this->context->expects($this->any())->method('getView')->willReturn($this->view); - $this->context->expects($this->any())->method('getMessageManager')->willReturn($this->messageManager); - $this->context->expects($this->any())->method('getSession')->willReturn($this->session); - $this->context->expects($this->any())->method('getAuthorization')->willReturn($this->authorization); - $this->context->expects($this->any())->method('getAuth')->willReturn($this->auth); - $this->context->expects($this->any())->method('getHelper')->willReturn($this->helper); - $this->context->expects($this->any())->method('getBackendUrl')->willReturn($this->backendUrl); - $this->context->expects($this->any())->method('getFormKeyValidator')->willReturn($this->formKeyValidator); - $this->context->expects($this->any())->method('getLocaleResolver')->willReturn($this->localeResolver); - $this->context->expects($this->any()) - ->method('getResultRedirectFactory') - ->willReturn($this->resultRedirectFactory); - - $this->product = $this->createPartialMock( - \Magento\Catalog\Model\Product::class, - ['isProductsHasSku', '__wakeup'] - ); - - $this->stockItemService = $this->getMockBuilder(\Magento\CatalogInventory\Api\StockRegistryInterface::class) - ->disableOriginalConstructor() - ->setMethods(['getStockItem', 'saveStockItem']) - ->getMockForAbstractClass(); - $this->stockItem = $this->getMockBuilder(\Magento\CatalogInventory\Api\Data\StockItemInterface::class) - ->setMethods(['getId', 'getProductId']) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - - $this->stockConfig = $this->getMockBuilder(\Magento\CatalogInventory\Api\StockConfigurationInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - - $this->objectManager->expects($this->any())->method('create')->will($this->returnValueMap([ - [\Magento\Catalog\Model\Product::class, [], $this->product], - [\Magento\CatalogInventory\Api\StockRegistryInterface::class, [], $this->stockItemService], - [\Magento\CatalogInventory\Api\StockItemRepositoryInterface::class, [], $this->stockItemRepository], - ])); - - $this->objectManager->expects($this->any())->method('get')->will($this->returnValueMap([ - [\Magento\CatalogInventory\Api\StockConfigurationInterface::class, $this->stockConfig], - ])); - } - - public function testExecuteThatProductIdsAreObtainedFromAttributeHelper() - { - $this->attributeHelper->expects($this->any())->method('getProductIds')->will($this->returnValue([5])); - $this->attributeHelper->expects($this->any())->method('getSelectedStoreId')->will($this->returnValue([1])); - $this->attributeHelper->expects($this->any())->method('getStoreWebsiteId')->will($this->returnValue(1)); - $this->stockConfig->expects($this->any())->method('getConfigItemOptions')->will($this->returnValue([])); - $this->dataObjectHelperMock->expects($this->any()) - ->method('populateWithArray') - ->with($this->stockItem, $this->anything(), \Magento\CatalogInventory\Api\Data\StockItemInterface::class) - ->willReturnSelf(); - $this->product->expects($this->any())->method('isProductsHasSku')->with([5])->will($this->returnValue(true)); - $this->stockItemService->expects($this->any())->method('getStockItem')->with(5, 1) - ->will($this->returnValue($this->stockItem)); - $this->stockIndexerProcessor->expects($this->any())->method('reindexList')->with([5]); - - $this->request->expects($this->any())->method('getParam')->will($this->returnValueMap([ - ['inventory', [], [7]], - ])); - - $this->messageManager->expects($this->never())->method('addErrorMessage'); - $this->messageManager->expects($this->never())->method('addExceptionMessage'); - - $this->object->execute(); - } -} diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php index f493cbc88f18..30d3503e4640 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php @@ -5,7 +5,10 @@ */ namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Attribute; +use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Catalog\Controller\Adminhtml\Product\Attribute\Save; +use Magento\Eav\Model\Validator\Attribute\Code as AttributeCodeValidator; +use Magento\Framework\Serialize\Serializer\FormData; use Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\AttributeTest; use Magento\Catalog\Model\Product\AttributeSet\BuildFactory; use Magento\Catalog\Model\Product\AttributeSet\Build; @@ -13,11 +16,14 @@ use Magento\Eav\Api\Data\AttributeSetInterface; use Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype\ValidatorFactory; use Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\CollectionFactory; +use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Filter\FilterManager; use Magento\Catalog\Helper\Product as ProductHelper; +use Magento\Framework\View\Element\Messages; use Magento\Framework\View\LayoutFactory; use Magento\Backend\Model\View\Result\Redirect as ResultRedirect; use Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype\Validator as InputTypeValidator; +use Magento\Framework\View\LayoutInterface; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -79,6 +85,21 @@ class SaveTest extends AttributeTest */ protected $inputTypeValidatorMock; + /** + * @var FormData|\PHPUnit_Framework_MockObject_MockObject + */ + private $formDataSerializerMock; + + /** + * @var ProductAttributeInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $productAttributeMock; + + /** + * @var AttributeCodeValidator|\PHPUnit_Framework_MockObject_MockObject + */ + private $attributeCodeValidatorMock; + protected function setUp() { parent::setUp(); @@ -108,6 +129,7 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); $this->redirectMock = $this->getMockBuilder(ResultRedirect::class) + ->setMethods(['setData', 'setPath']) ->disableOriginalConstructor() ->getMock(); $this->attributeSetMock = $this->getMockBuilder(AttributeSetInterface::class) @@ -119,6 +141,15 @@ protected function setUp() $this->inputTypeValidatorMock = $this->getMockBuilder(InputTypeValidator::class) ->disableOriginalConstructor() ->getMock(); + $this->formDataSerializerMock = $this->getMockBuilder(FormData::class) + ->disableOriginalConstructor() + ->getMock(); + $this->attributeCodeValidatorMock = $this->getMockBuilder(AttributeCodeValidator::class) + ->disableOriginalConstructor() + ->getMock(); + $this->productAttributeMock = $this->getMockBuilder(ProductAttributeInterface::class) + ->setMethods(['getId', 'get']) + ->getMockForAbstractClass(); $this->buildFactoryMock->expects($this->any()) ->method('create') @@ -126,6 +157,9 @@ protected function setUp() $this->validatorFactoryMock->expects($this->any()) ->method('create') ->willReturn($this->inputTypeValidatorMock); + $this->attributeFactoryMock + ->method('create') + ->willReturn($this->productAttributeMock); } /** @@ -145,11 +179,24 @@ protected function getModel() 'validatorFactory' => $this->validatorFactoryMock, 'groupCollectionFactory' => $this->groupCollectionFactoryMock, 'layoutFactory' => $this->layoutFactoryMock, + 'formDataSerializer' => $this->formDataSerializerMock, + 'attributeCodeValidator' => $this->attributeCodeValidatorMock ]); } public function testExecuteWithEmptyData() { + $this->requestMock->expects($this->any()) + ->method('getParam') + ->willReturnMap([ + ['isAjax', null, null], + ['serialized_options', '[]', ''], + ]); + $this->formDataSerializerMock + ->expects($this->once()) + ->method('unserialize') + ->with('') + ->willReturn([]); $this->requestMock->expects($this->once()) ->method('getPostValue') ->willReturn([]); @@ -170,6 +217,27 @@ public function testExecute() 'frontend_input' => 'test_frontend_input', ]; + $this->requestMock->expects($this->any()) + ->method('getParam') + ->willReturnMap([ + ['isAjax', null, null], + ['serialized_options', '[]', ''], + ]); + $this->formDataSerializerMock + ->expects($this->once()) + ->method('unserialize') + ->with('') + ->willReturn([]); + $this->productAttributeMock + ->method('getId') + ->willReturn(1); + $this->productAttributeMock + ->method('getAttributeCode') + ->willReturn('test_code'); + $this->attributeCodeValidatorMock + ->method('isValid') + ->with('test_code') + ->willReturn(true); $this->requestMock->expects($this->once()) ->method('getPostValue') ->willReturn($data); @@ -203,4 +271,80 @@ public function testExecute() $this->assertInstanceOf(ResultRedirect::class, $this->getModel()->execute()); } + + /** + * @throws \Magento\Framework\Exception\NotFoundException + */ + public function testExecuteWithOptionsDataError() + { + $serializedOptions = '{"key":"value"}'; + $message = "The attribute couldn't be saved due to an error. Verify your information and try again. " + . "If the error persists, please try again later."; + + $this->requestMock->expects($this->any()) + ->method('getParam') + ->willReturnMap([ + ['isAjax', null, true], + ['serialized_options', '[]', $serializedOptions], + ]); + $this->formDataSerializerMock + ->expects($this->once()) + ->method('unserialize') + ->with($serializedOptions) + ->willThrowException(new \InvalidArgumentException('Some exception')); + $this->messageManager + ->expects($this->once()) + ->method('addErrorMessage') + ->with($message); + $this->addReturnResultConditions('catalog/*/edit', ['_current' => true], ['error' => true]); + + $this->getModel()->execute(); + } + + /** + * @param string $path + * @param array $params + * @param array $response + * @return mixed + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + private function addReturnResultConditions(string $path = '', array $params = [], array $response = []) + { + $layoutMock = $this->getMockBuilder(LayoutInterface::class) + ->setMethods(['initMessages', 'getMessagesBlock']) + ->getMockForAbstractClass(); + $this->layoutFactoryMock + ->expects($this->once()) + ->method('create') + ->with() + ->willReturn($layoutMock); + $layoutMock + ->method('initMessages') + ->with(); + $messageBlockMock = $this->getMockBuilder(Messages::class) + ->disableOriginalConstructor() + ->getMock(); + $layoutMock + ->expects($this->once()) + ->method('getMessagesBlock') + ->willReturn($messageBlockMock); + $messageBlockMock + ->expects($this->once()) + ->method('getGroupedHtml') + ->willReturn('message1'); + $this->resultFactoryMock + ->expects($this->once()) + ->method('create') + ->with(ResultFactory::TYPE_JSON) + ->willReturn($this->redirectMock); + $response = array_merge($response, [ + 'messages' => ['message1'], + 'params' => $params, + ]); + $this->redirectMock + ->expects($this->once()) + ->method('setData') + ->with($response) + ->willReturnSelf(); + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php index 9c747393cc72..742148b1bf7f 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php @@ -6,6 +6,8 @@ namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Attribute; use Magento\Catalog\Controller\Adminhtml\Product\Attribute\Validate; +use Magento\Eav\Model\Validator\Attribute\Code as AttributeCodeValidator; +use Magento\Framework\Serialize\Serializer\FormData; use Magento\Catalog\Model\ResourceModel\Eav\Attribute; use Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\AttributeTest; use Magento\Eav\Model\Entity\Attribute\Set as AttributeSet; @@ -61,6 +63,16 @@ class ValidateTest extends AttributeTest */ protected $layoutMock; + /** + * @var FormData|\PHPUnit_Framework_MockObject_MockObject + */ + private $formDataSerializerMock; + + /** + * @var AttributeCodeValidator|\PHPUnit_Framework_MockObject_MockObject + */ + private $attributeCodeValidatorMock; + protected function setUp() { parent::setUp(); @@ -86,6 +98,12 @@ protected function setUp() ->getMock(); $this->layoutMock = $this->getMockBuilder(LayoutInterface::class) ->getMockForAbstractClass(); + $this->formDataSerializerMock = $this->getMockBuilder(FormData::class) + ->disableOriginalConstructor() + ->getMock(); + $this->attributeCodeValidatorMock = $this->getMockBuilder(AttributeCodeValidator::class) + ->disableOriginalConstructor() + ->getMock(); $this->contextMock->expects($this->any()) ->method('getObjectManager') @@ -100,25 +118,29 @@ protected function getModel() return $this->objectManager->getObject( Validate::class, [ - 'context' => $this->contextMock, - 'attributeLabelCache' => $this->attributeLabelCacheMock, - 'coreRegistry' => $this->coreRegistryMock, - 'resultPageFactory' => $this->resultPageFactoryMock, - 'resultJsonFactory' => $this->resultJsonFactoryMock, - 'layoutFactory' => $this->layoutFactoryMock, - 'multipleAttributeList' => ['select' => 'option'] + 'context' => $this->contextMock, + 'attributeLabelCache' => $this->attributeLabelCacheMock, + 'coreRegistry' => $this->coreRegistryMock, + 'resultPageFactory' => $this->resultPageFactoryMock, + 'resultJsonFactory' => $this->resultJsonFactoryMock, + 'layoutFactory' => $this->layoutFactoryMock, + 'multipleAttributeList' => ['select' => 'option'], + 'formDataSerializer' => $this->formDataSerializerMock, + 'attributeCodeValidator' => $this->attributeCodeValidatorMock, ] ); } public function testExecute() { + $serializedOptions = '{"key":"value"}'; $this->requestMock->expects($this->any()) ->method('getParam') ->willReturnMap([ ['frontend_label', null, 'test_frontend_label'], ['attribute_code', null, 'test_attribute_code'], ['new_attribute_set_name', null, 'test_attribute_set_name'], + ['serialized_options', '[]', $serializedOptions], ]); $this->objectManagerMock->expects($this->exactly(2)) ->method('create') @@ -129,6 +151,12 @@ public function testExecute() $this->attributeMock->expects($this->once()) ->method('loadByCode') ->willReturnSelf(); + + $this->attributeCodeValidatorMock->expects($this->once()) + ->method('isValid') + ->with('test_attribute_code') + ->willReturn(true); + $this->requestMock->expects($this->once()) ->method('has') ->with('new_attribute_set_name') @@ -160,6 +188,7 @@ public function testExecute() */ public function testUniqueValidation(array $options, $isError) { + $serializedOptions = '{"key":"value"}'; $countFunctionCalls = ($isError) ? 6 : 5; $this->requestMock->expects($this->exactly($countFunctionCalls)) ->method('getParam') @@ -167,10 +196,21 @@ public function testUniqueValidation(array $options, $isError) ['frontend_label', null, null], ['attribute_code', null, "test_attribute_code"], ['new_attribute_set_name', null, 'test_attribute_set_name'], - ['option', null, $options], - ['message_key', null, Validate::DEFAULT_MESSAGE_KEY] + ['message_key', null, Validate::DEFAULT_MESSAGE_KEY], + ['serialized_options', '[]', $serializedOptions], ]); + $this->formDataSerializerMock + ->expects($this->once()) + ->method('unserialize') + ->with($serializedOptions) + ->willReturn($options); + + $this->attributeCodeValidatorMock->expects($this->once()) + ->method('isValid') + ->with('test_attribute_code') + ->willReturn(true); + $this->objectManagerMock->expects($this->once()) ->method('create') ->willReturn($this->attributeMock); @@ -203,67 +243,77 @@ public function provideUniqueData() return [ 'no values' => [ [ - 'delete' => [ - "option_0" => "", - "option_1" => "", - "option_2" => "", - ] + 'option' => [ + 'delete' => [ + "option_0" => "", + "option_1" => "", + "option_2" => "", + ], + ], ], false ], 'valid options' => [ [ - 'value' => [ - "option_0" => [1, 0], - "option_1" => [2, 0], - "option_2" => [3, 0], + 'option' => [ + 'value' => [ + "option_0" => [1, 0], + "option_1" => [2, 0], + "option_2" => [3, 0], + ], + 'delete' => [ + "option_0" => "", + "option_1" => "", + "option_2" => "", + ], ], - 'delete' => [ - "option_0" => "", - "option_1" => "", - "option_2" => "", - ] ], false ], 'duplicate options' => [ [ - 'value' => [ - "option_0" => [1, 0], - "option_1" => [1, 0], - "option_2" => [3, 0], + 'option' => [ + 'value' => [ + "option_0" => [1, 0], + "option_1" => [1, 0], + "option_2" => [3, 0], + ], + 'delete' => [ + "option_0" => "", + "option_1" => "", + "option_2" => "", + ], ], - 'delete' => [ - "option_0" => "", - "option_1" => "", - "option_2" => "", - ] ], true ], 'duplicate and deleted' => [ [ - 'value' => [ - "option_0" => [1, 0], - "option_1" => [1, 0], - "option_2" => [3, 0], + 'option' => [ + 'value' => [ + "option_0" => [1, 0], + "option_1" => [1, 0], + "option_2" => [3, 0], + ], + 'delete' => [ + "option_0" => "", + "option_1" => "1", + "option_2" => "", + ], ], - 'delete' => [ - "option_0" => "", - "option_1" => "1", - "option_2" => "", - ] ], false ], 'empty and deleted' => [ [ - 'value' => [ - "option_0" => [1, 0], - "option_1" => [2, 0], - "option_2" => ["", ""], + 'option' => [ + 'value' => [ + "option_0" => [1, 0], + "option_1" => [2, 0], + "option_2" => ["", ""], + ], + 'delete' => [ + "option_0" => "", + "option_1" => "", + "option_2" => "1", + ], ], - 'delete' => [ - "option_0" => "", - "option_1" => "", - "option_2" => "1", - ] ], false ], ]; @@ -278,6 +328,7 @@ public function provideUniqueData() */ public function testEmptyOption(array $options, $result) { + $serializedOptions = '{"key":"value"}'; $this->requestMock->expects($this->any()) ->method('getParam') ->willReturnMap([ @@ -285,10 +336,16 @@ public function testEmptyOption(array $options, $result) ['frontend_input', 'select', 'multipleselect'], ['attribute_code', null, "test_attribute_code"], ['new_attribute_set_name', null, 'test_attribute_set_name'], - ['option', null, $options], ['message_key', Validate::DEFAULT_MESSAGE_KEY, 'message'], + ['serialized_options', '[]', $serializedOptions], ]); + $this->formDataSerializerMock + ->expects($this->once()) + ->method('unserialize') + ->with($serializedOptions) + ->willReturn($options); + $this->objectManagerMock->expects($this->once()) ->method('create') ->willReturn($this->attributeMock); @@ -297,6 +354,11 @@ public function testEmptyOption(array $options, $result) ->method('loadByCode') ->willReturnSelf(); + $this->attributeCodeValidatorMock->expects($this->once()) + ->method('isValid') + ->with('test_attribute_code') + ->willReturn(true); + $this->resultJsonFactoryMock->expects($this->once()) ->method('create') ->willReturn($this->resultJson); @@ -320,8 +382,10 @@ public function provideEmptyOption() return [ 'empty admin scope options' => [ [ - 'value' => [ - "option_0" => [''], + 'option' => [ + 'value' => [ + "option_0" => [''], + ], ], ], (object) [ @@ -331,8 +395,10 @@ public function provideEmptyOption() ], 'not empty admin scope options' => [ [ - 'value' => [ - "option_0" => ['asdads'], + 'option' => [ + 'value' => [ + "option_0" => ['asdads'], + ], ], ], (object) [ @@ -341,11 +407,13 @@ public function provideEmptyOption() ], 'empty admin scope options and deleted' => [ [ - 'value' => [ - "option_0" => [''], - ], - 'delete' => [ - 'option_0' => '1', + 'option' => [ + 'value' => [ + "option_0" => [''], + ], + 'delete' => [ + 'option_0' => '1', + ], ], ], (object) [ @@ -354,11 +422,13 @@ public function provideEmptyOption() ], 'empty admin scope options and not deleted' => [ [ - 'value' => [ - "option_0" => [''], - ], - 'delete' => [ - 'option_0' => '0', + 'option' => [ + 'value' => [ + "option_0" => [''], + ], + 'delete' => [ + 'option_0' => '0', + ], ], ], (object) [ @@ -368,4 +438,136 @@ public function provideEmptyOption() ], ]; } + + /** + * @throws \Magento\Framework\Exception\NotFoundException + */ + public function testExecuteWithOptionsDataError() + { + $serializedOptions = '{"key":"value"}'; + $message = "The attribute couldn't be validated due to an error. Verify your information and try again. " + . "If the error persists, please try again later."; + $this->requestMock->expects($this->any()) + ->method('getParam') + ->willReturnMap([ + ['frontend_label', null, 'test_frontend_label'], + ['attribute_code', null, 'test_attribute_code'], + ['new_attribute_set_name', null, 'test_attribute_set_name'], + ['message_key', Validate::DEFAULT_MESSAGE_KEY, 'message'], + ['serialized_options', '[]', $serializedOptions], + ]); + + $this->formDataSerializerMock + ->expects($this->once()) + ->method('unserialize') + ->with($serializedOptions) + ->willThrowException(new \InvalidArgumentException('Some exception')); + + $this->objectManagerMock + ->method('create') + ->willReturnMap([ + [\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class, [], $this->attributeMock], + [\Magento\Eav\Model\Entity\Attribute\Set::class, [], $this->attributeSetMock] + ]); + + $this->attributeCodeValidatorMock + ->method('isValid') + ->willReturn(true); + + $this->attributeMock + ->method('loadByCode') + ->willReturnSelf(); + $this->attributeSetMock + ->method('setEntityTypeId') + ->willReturnSelf(); + $this->resultJsonFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->resultJson); + $this->resultJson->expects($this->once()) + ->method('setJsonData') + ->with(json_encode([ + 'error' => true, + 'message' => $message + ])) + ->willReturnSelf(); + + $this->getModel()->execute(); + } + + /** + * Test execute with an invalid attribute code + * + * @dataProvider provideInvalidAttributeCodes + * @param string $attributeCode + * @param $result + * @throws \Magento\Framework\Exception\NotFoundException + */ + public function testExecuteWithInvalidAttributeCode($attributeCode, $result) + { + $serializedOptions = '{"key":"value"}'; + $this->requestMock->expects($this->any()) + ->method('getParam') + ->willReturnMap([ + ['frontend_label', null, null], + ['frontend_input', 'select', 'multipleselect'], + ['attribute_code', null, $attributeCode], + ['new_attribute_set_name', null, 'test_attribute_set_name'], + ['message_key', Validate::DEFAULT_MESSAGE_KEY, 'message'], + ['serialized_options', '[]', $serializedOptions], + ]); + + $this->formDataSerializerMock + ->expects($this->once()) + ->method('unserialize') + ->with($serializedOptions) + ->willReturn(["key" => "value"]); + + $this->objectManagerMock->expects($this->once()) + ->method('create') + ->willReturn($this->attributeMock); + + $this->attributeMock->expects($this->once()) + ->method('loadByCode') + ->willReturnSelf(); + + $this->attributeCodeValidatorMock->expects($this->once()) + ->method('isValid') + ->with($attributeCode) + ->willReturn(false); + + $this->attributeCodeValidatorMock->expects($this->once()) + ->method('getMessages') + ->willReturn(['Invalid Attribute Code.']); + + $this->resultJsonFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->resultJson); + + $this->resultJson->expects($this->once()) + ->method('setJsonData') + ->willReturnArgument(0); + + $response = $this->getModel()->execute(); + $responseObject = json_decode($response); + + $this->assertEquals($responseObject, $result); + } + + /** + * Providing invalid attribute codes + * + * @return array + */ + public function provideInvalidAttributeCodes() + { + return [ + 'invalid attribute code' => [ + '.attribute_code', + (object) [ + 'error' => true, + 'message' => 'Invalid Attribute Code.', + ] + ] + ]; + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/AttributeTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/AttributeTest.php index b85b03852b62..2a75773754fc 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/AttributeTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/AttributeTest.php @@ -9,8 +9,10 @@ use Magento\Catalog\Controller\Adminhtml\Product\Attribute; use Magento\Framework\App\RequestInterface; use Magento\Framework\Cache\FrontendInterface; +use Magento\Framework\Message\ManagerInterface; +use Magento\Framework\ObjectManager\ObjectManager; use Magento\Framework\Registry; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\Framework\View\Result\PageFactory; use Magento\Framework\Controller\ResultFactory; @@ -20,7 +22,7 @@ class AttributeTest extends \PHPUnit\Framework\TestCase { /** - * @var ObjectManager + * @var ObjectManagerHelper */ protected $objectManager; @@ -54,9 +56,14 @@ class AttributeTest extends \PHPUnit\Framework\TestCase */ protected $resultFactoryMock; + /** + * @var ManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $messageManager; + protected function setUp() { - $this->objectManager = new ObjectManager($this); + $this->objectManager = new ObjectManagerHelper($this); $this->contextMock = $this->getMockBuilder(Context::class) ->disableOriginalConstructor() ->getMock(); @@ -74,6 +81,9 @@ protected function setUp() $this->resultFactoryMock = $this->getMockBuilder(ResultFactory::class) ->disableOriginalConstructor() ->getMock(); + $this->messageManager = $this->getMockBuilder(ManagerInterface::class) + ->disableOriginalConstructor() + ->getMock(); $this->contextMock->expects($this->any()) ->method('getRequest') @@ -81,6 +91,9 @@ protected function setUp() $this->contextMock->expects($this->any()) ->method('getResultFactory') ->willReturn($this->resultFactoryMock); + $this->contextMock + ->method('getMessageManager') + ->willReturn($this->messageManager); } /** diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php index ff44a91a6499..c889c58e3df3 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php @@ -95,6 +95,11 @@ class HelperTest extends \PHPUnit\Framework\TestCase */ protected $attributeFilterMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $dateTimeFilterMock; + /** * @inheritdoc */ @@ -170,6 +175,11 @@ protected function setUp() $resolverProperty = $helperReflection->getProperty('linkResolver'); $resolverProperty->setAccessible(true); $resolverProperty->setValue($this->helper, $this->linkResolverMock); + + $this->dateTimeFilterMock = $this->createMock(\Magento\Framework\Stdlib\DateTime\Filter\DateTime::class); + $dateTimeFilterProperty = $helperReflection->getProperty('dateTimeFilter'); + $dateTimeFilterProperty->setAccessible(true); + $dateTimeFilterProperty->setValue($this->helper, $this->dateTimeFilterMock); } /** @@ -211,6 +221,12 @@ public function testInitialize( if (!empty($tierPrice)) { $productData = array_merge($productData, ['tier_price' => $tierPrice]); } + + $this->dateTimeFilterMock->expects($this->once()) + ->method('filter') + ->with($specialFromDate) + ->willReturn($specialFromDate); + $attributeNonDate = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Category/ViewTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Category/ViewTest.php index d93520297e48..60c6f2f1bd82 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Category/ViewTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Category/ViewTest.php @@ -124,7 +124,7 @@ protected function setUp() ->disableOriginalConstructor()->getMock(); $this->pageConfig->expects($this->any())->method('addBodyClass')->will($this->returnSelf()); - $this->page = $this->getMockBuilder(\Magento\Framework\View\Page::class) + $this->page = $this->getMockBuilder(\Magento\Framework\View\Result\Page::class) ->setMethods(['getConfig', 'initLayout', 'addPageLayoutHandles', 'getLayout', 'addUpdate']) ->disableOriginalConstructor()->getMock(); $this->page->expects($this->any())->method('getConfig')->will($this->returnValue($this->pageConfig)); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/TierPrice/SaveHandlerTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/TierPrice/SaveHandlerTest.php new file mode 100644 index 000000000000..fbb96933db51 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/TierPrice/SaveHandlerTest.php @@ -0,0 +1,178 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Model\Attribute\Backend\TierPrice; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Catalog\Model\Product\Attribute\Backend\TierPrice\SaveHandler; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Customer\Api\GroupManagementInterface; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice; + +/** + * Unit tests for \Magento\Catalog\Model\Product\Attribute\Backend\TierPrice\SaveHandler + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class SaveHandlerTest extends \PHPUnit\Framework\TestCase +{ + /** + * Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + private $objectManager; + + /** + * @var SaveHandler|\PHPUnit_Framework_MockObject_MockObject + */ + private $saveHandler; + + /** + * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeManager; + + /** + * @var ProductAttributeRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $attributeRepository; + + /** + * @var GroupManagementInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $groupManagement; + + /** + * @var MetadataPool|\PHPUnit_Framework_MockObject_MockObject + */ + private $metadataPoll; + + /** + * @var Tierprice|\PHPUnit_Framework_MockObject_MockObject + */ + private $tierPriceResource; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = new ObjectManager($this); + $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getStore']) + ->getMockForAbstractClass(); + $this->attributeRepository = $this->getMockBuilder(ProductAttributeRepositoryInterface::class) + ->disableOriginalConstructor() + ->setMethods(['get']) + ->getMockForAbstractClass(); + $this->groupManagement = $this->getMockBuilder(GroupManagementInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getAllCustomersGroup']) + ->getMockForAbstractClass(); + $this->metadataPoll = $this->getMockBuilder(MetadataPool::class) + ->disableOriginalConstructor() + ->setMethods(['getMetadata']) + ->getMock(); + $this->tierPriceResource = $this->getMockBuilder(Tierprice::class) + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + + $this->saveHandler = $this->objectManager->getObject( + \Magento\Catalog\Model\Product\Attribute\Backend\TierPrice\SaveHandler::class, + [ + 'storeManager' => $this->storeManager, + 'attributeRepository' => $this->attributeRepository, + 'groupManagement' => $this->groupManagement, + 'metadataPoll' => $this->metadataPoll, + 'tierPriceResource' => $this->tierPriceResource + ] + ); + } + + public function testExecute(): void + { + $tierPrices = [ + ['website_id' => 0, 'price_qty' => 2, 'cust_group' => 0, 'price' => 10], + ['website_id' => 0, 'price_qty' => 3, 'cust_group' => 3200, 'price' => null, 'percentage_value' => 20] + ]; + $linkField = 'entity_id'; + $productId = 10; + + /** @var \PHPUnit_Framework_MockObject_MockObject $product */ + $product = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getData','setData', 'getStoreId']) + ->getMockForAbstractClass(); + $product->expects($this->atLeastOnce())->method('getData')->willReturnMap( + [ + ['tier_price', $tierPrices], + ['entity_id', $productId] + ] + ); + $product->expects($this->atLeastOnce())->method('getStoreId')->willReturn(0); + $product->expects($this->atLeastOnce())->method('setData')->with('tier_price_changed', 1); + $store = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getWebsiteId']) + ->getMockForAbstractClass(); + $store->expects($this->atLeastOnce())->method('getWebsiteId')->willReturn(0); + $this->storeManager->expects($this->atLeastOnce())->method('getStore')->willReturn($store); + /** @var \PHPUnit_Framework_MockObject_MockObject $attribute */ + $attribute = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductAttributeInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getName', 'isScopeGlobal']) + ->getMockForAbstractClass(); + $attribute->expects($this->atLeastOnce())->method('getName')->willReturn('tier_price'); + $attribute->expects($this->atLeastOnce())->method('isScopeGlobal')->willReturn(true); + $this->attributeRepository->expects($this->atLeastOnce())->method('get')->with('tier_price') + ->willReturn($attribute); + $productMetadata = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadataInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getLinkField']) + ->getMockForAbstractClass(); + $productMetadata->expects($this->atLeastOnce())->method('getLinkField')->willReturn($linkField); + $this->metadataPoll->expects($this->atLeastOnce())->method('getMetadata') + ->with(\Magento\Catalog\Api\Data\ProductInterface::class) + ->willReturn($productMetadata); + $customerGroup = $this->getMockBuilder(\Magento\Customer\Api\Data\GroupInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getId']) + ->getMockForAbstractClass(); + $customerGroup->expects($this->atLeastOnce())->method('getId')->willReturn(3200); + $this->groupManagement->expects($this->atLeastOnce())->method('getAllCustomersGroup') + ->willReturn($customerGroup); + $this->tierPriceResource->expects($this->atLeastOnce())->method('savePriceData')->willReturnSelf(); + + $this->assertEquals($product, $this->saveHandler->execute($product)); + } + + /** + * @expectedException \Magento\Framework\Exception\InputException + * @expectedExceptionMessage Tier prices data should be array, but actually other type is received + */ + public function testExecuteWithException(): void + { + /** @var \PHPUnit_Framework_MockObject_MockObject $attribute */ + $attribute = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductAttributeInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getName', 'isScopeGlobal']) + ->getMockForAbstractClass(); + $attribute->expects($this->atLeastOnce())->method('getName')->willReturn('tier_price'); + $this->attributeRepository->expects($this->atLeastOnce())->method('get')->with('tier_price') + ->willReturn($attribute); + /** @var \PHPUnit_Framework_MockObject_MockObject $product */ + $product = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getData','setData', 'getStoreId', 'getOrigData']) + ->getMockForAbstractClass(); + $product->expects($this->atLeastOnce())->method('getData')->with('tier_price')->willReturn(1); + + $this->saveHandler->execute($product); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/TierPrice/UpdateHandlerTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/TierPrice/UpdateHandlerTest.php new file mode 100644 index 000000000000..cce00c50d37a --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/TierPrice/UpdateHandlerTest.php @@ -0,0 +1,192 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Model\Attribute\Backend\TierPrice; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Catalog\Model\Product\Attribute\Backend\TierPrice\UpdateHandler; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Customer\Api\GroupManagementInterface; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice; + +/** + * Unit tests for \Magento\Catalog\Model\Product\Attribute\Backend\TierPrice\UpdateHandler + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class UpdateHandlerTest extends \PHPUnit\Framework\TestCase +{ + /** + * Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + private $objectManager; + + /** + * @var UpdateHandler|\PHPUnit_Framework_MockObject_MockObject + */ + private $updateHandler; + + /** + * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeManager; + + /** + * @var ProductAttributeRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $attributeRepository; + + /** + * @var GroupManagementInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $groupManagement; + + /** + * @var MetadataPool|\PHPUnit_Framework_MockObject_MockObject + */ + private $metadataPoll; + + /** + * @var Tierprice|\PHPUnit_Framework_MockObject_MockObject + */ + private $tierPriceResource; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = new ObjectManager($this); + $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getStore']) + ->getMockForAbstractClass(); + $this->attributeRepository = $this->getMockBuilder(ProductAttributeRepositoryInterface::class) + ->disableOriginalConstructor() + ->setMethods(['get']) + ->getMockForAbstractClass(); + $this->groupManagement = $this->getMockBuilder(GroupManagementInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getAllCustomersGroup']) + ->getMockForAbstractClass(); + $this->metadataPoll = $this->getMockBuilder(MetadataPool::class) + ->disableOriginalConstructor() + ->setMethods(['getMetadata']) + ->getMock(); + $this->tierPriceResource = $this->getMockBuilder(Tierprice::class) + ->disableOriginalConstructor() + ->setMethods([]) + ->getMock(); + + $this->updateHandler = $this->objectManager->getObject( + \Magento\Catalog\Model\Product\Attribute\Backend\TierPrice\UpdateHandler::class, + [ + 'storeManager' => $this->storeManager, + 'attributeRepository' => $this->attributeRepository, + 'groupManagement' => $this->groupManagement, + 'metadataPoll' => $this->metadataPoll, + 'tierPriceResource' => $this->tierPriceResource + ] + ); + } + + public function testExecute(): void + { + $newTierPrices = [ + ['website_id' => 0, 'price_qty' => 2, 'cust_group' => 0, 'price' => 15], + ['website_id' => 0, 'price_qty' => 3, 'cust_group' => 3200, 'price' => null, 'percentage_value' => 20] + ]; + $priceIdToDelete = 2; + $originalTierPrices = [ + ['price_id' => 1, 'website_id' => 0, 'price_qty' => 2, 'cust_group' => 0, 'price' => 10], + ['price_id' => $priceIdToDelete, 'website_id' => 0, 'price_qty' => 4, 'cust_group' => 0, 'price' => 20], + ]; + $linkField = 'entity_id'; + $productId = 10; + + /** @var \PHPUnit_Framework_MockObject_MockObject $product */ + $product = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getData','setData', 'getStoreId', 'getOrigData']) + ->getMockForAbstractClass(); + $product->expects($this->atLeastOnce())->method('getData')->willReturnMap( + [ + ['tier_price', $newTierPrices], + ['entity_id', $productId] + ] + ); + $product->expects($this->atLeastOnce())->method('getOrigData') + ->willReturnMap( + [ + ['tier_price', $originalTierPrices], + ['entity_id', $productId] + ] + ); + $product->expects($this->atLeastOnce())->method('getStoreId')->willReturn(0); + $product->expects($this->atLeastOnce())->method('setData')->with('tier_price_changed', 1); + $store = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getWebsiteId']) + ->getMockForAbstractClass(); + $store->expects($this->atLeastOnce())->method('getWebsiteId')->willReturn(0); + $this->storeManager->expects($this->atLeastOnce())->method('getStore')->willReturn($store); + /** @var \PHPUnit_Framework_MockObject_MockObject $attribute */ + $attribute = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductAttributeInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getName', 'isScopeGlobal']) + ->getMockForAbstractClass(); + $attribute->expects($this->atLeastOnce())->method('getName')->willReturn('tier_price'); + $attribute->expects($this->atLeastOnce())->method('isScopeGlobal')->willReturn(true); + $this->attributeRepository->expects($this->atLeastOnce())->method('get')->with('tier_price') + ->willReturn($attribute); + $productMetadata = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadataInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getLinkField']) + ->getMockForAbstractClass(); + $productMetadata->expects($this->atLeastOnce())->method('getLinkField')->willReturn($linkField); + $this->metadataPoll->expects($this->atLeastOnce())->method('getMetadata') + ->with(\Magento\Catalog\Api\Data\ProductInterface::class) + ->willReturn($productMetadata); + $customerGroup = $this->getMockBuilder(\Magento\Customer\Api\Data\GroupInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getId']) + ->getMockForAbstractClass(); + $customerGroup->expects($this->atLeastOnce())->method('getId')->willReturn(3200); + $this->groupManagement->expects($this->atLeastOnce())->method('getAllCustomersGroup') + ->willReturn($customerGroup); + $this->tierPriceResource->expects($this->exactly(2))->method('savePriceData')->willReturnSelf(); + $this->tierPriceResource->expects($this->once())->method('deletePriceData') + ->with($productId, null, $priceIdToDelete); + + $this->assertEquals($product, $this->updateHandler->execute($product)); + } + + /** + * @expectedException \Magento\Framework\Exception\InputException + * @expectedExceptionMessage Tier prices data should be array, but actually other type is received + */ + public function testExecuteWithException(): void + { + /** @var \PHPUnit_Framework_MockObject_MockObject $attribute */ + $attribute = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductAttributeInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getName', 'isScopeGlobal']) + ->getMockForAbstractClass(); + $attribute->expects($this->atLeastOnce())->method('getName')->willReturn('tier_price'); + $this->attributeRepository->expects($this->atLeastOnce())->method('get')->with('tier_price') + ->willReturn($attribute); + /** @var \PHPUnit_Framework_MockObject_MockObject $product */ + $product = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getData','setData', 'getStoreId', 'getOrigData']) + ->getMockForAbstractClass(); + $product->expects($this->atLeastOnce())->method('getData')->with('tier_price')->willReturn(1); + + $this->updateHandler->execute($product); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Category/Product/PositionResolverTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Category/Product/PositionResolverTest.php index 1ff3a1bae5c2..7ad8b1a0ab3f 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Category/Product/PositionResolverTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/Product/PositionResolverTest.php @@ -107,7 +107,7 @@ public function testGetPositions() $this->select->expects($this->once()) ->method('where') ->willReturnSelf(); - $this->select->expects($this->once()) + $this->select->expects($this->exactly(2)) ->method('order') ->willReturnSelf(); $this->select->expects($this->once()) 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 9fb2adb2b8ec..97c098ba0ff2 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Category/TreeTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/TreeTest.php @@ -43,6 +43,11 @@ class TreeTest extends \PHPUnit\Framework\TestCase */ protected $node; + /** + * @var \Magento\Catalog\Model\ResourceModel\Category\TreeFactory + */ + private $treeResourceFactoryMock; + protected function setUp() { $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -59,6 +64,12 @@ protected function setUp() \Magento\Store\Model\StoreManagerInterface::class )->disableOriginalConstructor()->getMock(); + $this->treeResourceFactoryMock = $this->createMock( + \Magento\Catalog\Model\ResourceModel\Category\TreeFactory::class + ); + $this->treeResourceFactoryMock->method('create') + ->willReturn($this->categoryTreeMock); + $methods = ['create']; $this->treeFactoryMock = $this->createPartialMock(\Magento\Catalog\Api\Data\CategoryTreeInterfaceFactory::class, $methods); @@ -70,7 +81,8 @@ protected function setUp() 'categoryCollection' => $this->categoryCollection, 'categoryTree' => $this->categoryTreeMock, 'storeManager' => $this->storeManagerMock, - 'treeFactory' => $this->treeFactoryMock + 'treeFactory' => $this->treeFactoryMock, + 'treeResourceFactory' => $this->treeResourceFactoryMock, ] ); } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php index 64eedbce2d98..b4042d6b02c1 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php @@ -383,13 +383,16 @@ public function testReindexFlatEnabled( public function reindexFlatDisabledTestDataProvider() { return [ - [false, null, null, null, 0], - [true, null, null, null, 0], - [false, [], null, null, 0], - [false, ["1", "2"], null, null, 1], - [false, null, 1, null, 1], - [false, ["1", "2"], 0, 1, 1], - [false, null, 1, 1, 0], + [false, null, null, null, null, null, 0], + [true, null, null, null, null, null, 0], + [false, [], null, null, null, null, 0], + [false, ["1", "2"], null, null, null, null, 1], + [false, null, 1, null, null, null, 1], + [false, ["1", "2"], 0, 1, null, null, 1], + [false, null, 1, 1, null, null, 0], + [false, ["1", "2"], null, null, 0, 1, 1], + [false, ["1", "2"], null, null, 1, 0, 1], + ]; } @@ -407,11 +410,16 @@ public function testReindexFlatDisabled( $affectedIds, $isAnchorOrig, $isAnchor, + $isActiveOrig, + $isActive, $expectedProductReindexCall ) { $this->category->setAffectedProductIds($affectedIds); $this->category->setData('is_anchor', $isAnchor); $this->category->setOrigData('is_anchor', $isAnchorOrig); + $this->category->setData('is_active', $isActive); + $this->category->setOrigData('is_active', $isActiveOrig); + $this->category->setAffectedProductIds($affectedIds); $pathIds = ['path/1/2', 'path/2/3']; @@ -422,7 +430,7 @@ public function testReindexFlatDisabled( ->method('isFlatEnabled') ->will($this->returnValue(false)); - $this->productIndexer->expects($this->exactly(1)) + $this->productIndexer ->method('isScheduled') ->willReturn($productScheduled); $this->productIndexer->expects($this->exactly($expectedProductReindexCall)) diff --git a/app/code/Magento/Catalog/Test/Unit/Model/CollectionProviderTest.php b/app/code/Magento/Catalog/Test/Unit/Model/CollectionProviderTest.php index d8931cbbfcf7..f0e17c7938b2 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/CollectionProviderTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/CollectionProviderTest.php @@ -57,10 +57,14 @@ public function testGetCollection() $linkedProductOneMock = $this->createMock(Product::class); $linkedProductTwoMock = $this->createMock(Product::class); $linkedProductThreeMock = $this->createMock(Product::class); + $linkedProductFourMock = $this->createMock(Product::class); + $linkedProductFiveMock = $this->createMock(Product::class); $linkedProductOneMock->expects($this->once())->method('getId')->willReturn(1); $linkedProductTwoMock->expects($this->once())->method('getId')->willReturn(2); $linkedProductThreeMock->expects($this->once())->method('getId')->willReturn(3); + $linkedProductFourMock->expects($this->once())->method('getId')->willReturn(4); + $linkedProductFiveMock->expects($this->once())->method('getId')->willReturn(5); $this->converterPoolMock->expects($this->once()) ->method('getConverter') @@ -71,9 +75,11 @@ public function testGetCollection() [$linkedProductOneMock, ['name' => 'Product One', 'position' => 10]], [$linkedProductTwoMock, ['name' => 'Product Two', 'position' => 2]], [$linkedProductThreeMock, ['name' => 'Product Three', 'position' => 2]], + [$linkedProductFourMock, ['name' => 'Product Four', 'position' => null]], + [$linkedProductFiveMock, ['name' => 'Product Five']], ]; - $this->converterMock->expects($this->exactly(3))->method('convert')->willReturnMap($map); + $this->converterMock->expects($this->exactly(5))->method('convert')->willReturnMap($map); $this->providerMock->expects($this->once()) ->method('getLinkedProducts') @@ -82,14 +88,18 @@ public function testGetCollection() [ $linkedProductOneMock, $linkedProductTwoMock, - $linkedProductThreeMock + $linkedProductThreeMock, + $linkedProductFourMock, + $linkedProductFiveMock, ] ); $expectedResult = [ - 2 => ['name' => 'Product Two', 'position' => 2], - 3 => ['name' => 'Product Three', 'position' => 2], - 10 => ['name' => 'Product One', 'position' => 10], + 0 => ['name' => 'Product Four', 'position' => 0], + 1 => ['name' => 'Product Five', 'position' => 0], + 2 => ['name' => 'Product Three', 'position' => 2], + 3 => ['name' => 'Product Two', 'position' => 2], + 4 => ['name' => 'Product One', 'position' => 10], ]; $actualResult = $this->model->getCollection($this->productMock, 'crosssell'); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Config/CatalogClone/Media/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Config/CatalogClone/Media/ImageTest.php index 5b1d3bf7943f..23f0aec5b69a 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Config/CatalogClone/Media/ImageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Config/CatalogClone/Media/ImageTest.php @@ -9,6 +9,11 @@ use Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +/** + * Tests \Magento\Catalog\Model\Config\CatalogClone\Media\Image. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class ImageTest extends \PHPUnit\Framework\TestCase { /** @@ -36,6 +41,14 @@ class ImageTest extends \PHPUnit\Framework\TestCase */ private $attribute; + /** + * @var \Magento\Framework\Escaper|\PHPUnit_Framework_MockObject_MockObject + */ + private $escaperMock; + + /** + * @inheritdoc + */ protected function setUp() { $this->eavConfig = $this->getMockBuilder(\Magento\Eav\Model\Config::class) @@ -62,54 +75,79 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); + $this->escaperMock = $this->getMockBuilder( + \Magento\Framework\Escaper::class + ) + ->disableOriginalConstructor() + ->setMethods(['escapeHtml']) + ->getMock(); + $helper = new ObjectManager($this); $this->model = $helper->getObject( \Magento\Catalog\Model\Config\CatalogClone\Media\Image::class, [ 'eavConfig' => $this->eavConfig, - 'attributeCollectionFactory' => $this->attributeCollectionFactory + 'attributeCollectionFactory' => $this->attributeCollectionFactory, + 'escaper' => $this->escaperMock, ] ); } - public function testGetPrefixes() + /** + * @param string $actualLabel + * @param string $expectedLabel + * @return void + * + * @dataProvider getPrefixesDataProvider + */ + public function testGetPrefixes(string $actualLabel, string $expectedLabel): void { $entityTypeId = 3; /** @var \Magento\Eav\Model\Entity\Type|\PHPUnit_Framework_MockObject_MockObject $entityType */ $entityType = $this->getMockBuilder(\Magento\Eav\Model\Entity\Type::class) ->disableOriginalConstructor() ->getMock(); - $entityType->expects($this->once())->method('getId')->will($this->returnValue($entityTypeId)); + $entityType->expects($this->once())->method('getId')->willReturn($entityTypeId); /** @var AbstractFrontend|\PHPUnit_Framework_MockObject_MockObject $frontend */ $frontend = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend::class) ->setMethods(['getLabel']) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $frontend->expects($this->once())->method('getLabel')->will($this->returnValue('testLabel')); + $frontend->expects($this->once())->method('getLabel')->willReturn($actualLabel); - $this->attributeCollection->expects($this->once())->method('setEntityTypeFilter')->with( - $this->equalTo($entityTypeId) - ); - $this->attributeCollection->expects($this->once())->method('setFrontendInputTypeFilter')->with( - $this->equalTo('media_image') - ); + $this->attributeCollection->expects($this->once())->method('setEntityTypeFilter')->with($entityTypeId); + $this->attributeCollection->expects($this->once())->method('setFrontendInputTypeFilter')->with('media_image'); - $this->attribute->expects($this->once())->method('getAttributeCode')->will( - $this->returnValue('attributeCode') - ); - $this->attribute->expects($this->once())->method('getFrontend')->will( - $this->returnValue($frontend) - ); + $this->attribute->expects($this->once())->method('getAttributeCode')->willReturn('attributeCode'); + $this->attribute->expects($this->once())->method('getFrontend')->willReturn($frontend); - $this->attributeCollection->expects($this->any())->method('getIterator')->will( - $this->returnValue(new \ArrayIterator([$this->attribute])) - ); + $this->attributeCollection->expects($this->any())->method('getIterator') + ->willReturn(new \ArrayIterator([$this->attribute])); + + $this->eavConfig->expects($this->any())->method('getEntityType')->with(Product::ENTITY) + ->willReturn($entityType); - $this->eavConfig->expects($this->any())->method('getEntityType')->with( - $this->equalTo(Product::ENTITY) - )->will($this->returnValue($entityType)); + $this->escaperMock->expects($this->once())->method('escapeHtml')->with($actualLabel) + ->willReturn($expectedLabel); - $this->assertEquals([['field' => 'attributeCode_', 'label' => 'testLabel']], $this->model->getPrefixes()); + $this->assertEquals([['field' => 'attributeCode_', 'label' => $expectedLabel]], $this->model->getPrefixes()); + } + + /** + * @return array + */ + public function getPrefixesDataProvider(): array + { + return [ + [ + 'actual_label' => 'testLabel', + 'expected_label' => 'testLabel', + ], + [ + 'actual_label' => '<media-image-attributelabel', + 'expected_label' => '<media-image-attributelabel', + ], + ]; } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ImageUploaderTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ImageUploaderTest.php index c989f2dd4746..6552e8544000 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ImageUploaderTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ImageUploaderTest.php @@ -69,10 +69,17 @@ class ImageUploaderTest extends \PHPUnit\Framework\TestCase /** * Allowed extensions * - * @var string + * @var array */ private $allowedExtensions; + /** + * Allowed mime types + * + * @var array + */ + private $allowedMimeTypes; + protected function setUp() { $this->coreFileStorageDatabaseMock = $this->createMock( @@ -97,6 +104,7 @@ protected function setUp() $this->baseTmpPath = 'base/tmp/'; $this->basePath = 'base/real/'; $this->allowedExtensions = ['.jpg']; + $this->allowedMimeTypes = ['image/jpg', 'image/jpeg', 'image/gif', 'image/png']; $this->imageUploader = new \Magento\Catalog\Model\ImageUploader( @@ -107,7 +115,8 @@ protected function setUp() $this->loggerMock, $this->baseTmpPath, $this->basePath, - $this->allowedExtensions + $this->allowedExtensions, + $this->allowedMimeTypes ); } 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 90c3f999a6a8..2e1cff834fd3 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/Indexer/Product/Flat/Action/EraserTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Action/EraserTest.php index cc6f5d84ef00..e1e2816d4422 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Action/EraserTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Action/EraserTest.php @@ -53,8 +53,14 @@ public function testRemoveDeletedProducts() { $productsToDeleteIds = [1, 2]; $select = $this->createMock(\Magento\Framework\DB\Select::class); - $select->expects($this->once())->method('from')->with('catalog_product_entity')->will($this->returnSelf()); - $select->expects($this->once())->method('where')->with('entity_id IN(?)', $productsToDeleteIds) + $select->expects($this->once()) + ->method('from') + ->with(['product_table' => 'catalog_product_entity']) + ->will($this->returnSelf()); + $select->expects($this->once())->method('columns')->with('entity_id')->will($this->returnSelf()); + $select->expects($this->once()) + ->method('where') + ->with('product_table.entity_id IN(?)', $productsToDeleteIds) ->will($this->returnSelf()); $products = [['entity_id' => 2]]; $statement = $this->createMock(\Zend_Db_Statement_Interface::class); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Action/RowTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Action/RowTest.php index 7b2a2ff304b8..11d07872fef9 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Action/RowTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Flat/Action/RowTest.php @@ -6,8 +6,13 @@ namespace Magento\Catalog\Test\Unit\Model\Indexer\Product\Flat\Action; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class RowTest extends \PHPUnit\Framework\TestCase { /** @@ -16,76 +21,116 @@ class RowTest extends \PHPUnit\Framework\TestCase protected $model; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var MockObject */ protected $storeManager; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var MockObject */ protected $store; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var MockObject */ protected $productIndexerHelper; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var MockObject */ protected $resource; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var MockObject */ protected $connection; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var MockObject */ protected $flatItemWriter; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var MockObject */ protected $flatItemEraser; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var MockObject */ protected $flatTableBuilder; + /** + * @inheritdoc + */ protected function setUp() { $objectManager = new ObjectManager($this); + $attributeTable = 'catalog_product_entity_int'; + $statusId = 22; $this->connection = $this->createMock(\Magento\Framework\DB\Adapter\AdapterInterface::class); $this->resource = $this->createMock(\Magento\Framework\App\ResourceConnection::class); $this->resource->expects($this->any())->method('getConnection') ->with('default') - ->will($this->returnValue($this->connection)); + ->willReturn($this->connection); $this->storeManager = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); $this->store = $this->createMock(\Magento\Store\Model\Store::class); - $this->store->expects($this->any())->method('getId')->will($this->returnValue('store_id_1')); - $this->storeManager->expects($this->any())->method('getStores')->will($this->returnValue([$this->store])); - $this->productIndexerHelper = $this->createMock(\Magento\Catalog\Helper\Product\Flat\Indexer::class); + $this->store->expects($this->any())->method('getId')->willReturn('store_id_1'); + $this->storeManager->expects($this->any())->method('getStores')->willReturn([$this->store]); $this->flatItemEraser = $this->createMock(\Magento\Catalog\Model\Indexer\Product\Flat\Action\Eraser::class); $this->flatItemWriter = $this->createMock(\Magento\Catalog\Model\Indexer\Product\Flat\Action\Indexer::class); $this->flatTableBuilder = $this->createMock( \Magento\Catalog\Model\Indexer\Product\Flat\FlatTableBuilder::class ); + $this->productIndexerHelper = $this->createMock(\Magento\Catalog\Helper\Product\Flat\Indexer::class); + $statusAttributeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute::class) + ->disableOriginalConstructor() + ->getMock(); + $this->productIndexerHelper->expects($this->any())->method('getAttribute') + ->with('status') + ->willReturn($statusAttributeMock); + $backendMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend::class) + ->disableOriginalConstructor() + ->getMock(); + $backendMock->expects($this->any())->method('getTable')->willReturn($attributeTable); + $statusAttributeMock->expects($this->any())->method('getBackend')->willReturn($backendMock); + $statusAttributeMock->expects($this->any())->method('getId')->willReturn($statusId); + $selectMock = $this->getMockBuilder(\Magento\Framework\DB\Select::class) + ->disableOriginalConstructor() + ->getMock(); + $this->connection->expects($this->any())->method('select')->willReturn($selectMock); + $selectMock->method('from') + ->willReturnSelf(); + $selectMock->method('joinLeft') + ->willReturnSelf(); + $selectMock->expects($this->any())->method('where')->willReturnSelf(); + $selectMock->expects($this->any())->method('order')->willReturnSelf(); + $selectMock->expects($this->any())->method('limit')->willReturnSelf(); + $pdoMock = $this->createMock(\Zend_Db_Statement_Pdo::class); + $this->connection->expects($this->any())->method('query')->with($selectMock)->willReturn($pdoMock); + $pdoMock->expects($this->any())->method('fetchColumn')->willReturn('1'); + + $metadataPool = $this->createMock(\Magento\Framework\EntityManager\MetadataPool::class); + $productMetadata = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadataInterface::class) + ->getMockForAbstractClass(); + $metadataPool->expects($this->any())->method('getMetadata')->with(ProductInterface::class) + ->willReturn($productMetadata); + $productMetadata->expects($this->any())->method('getLinkField')->willReturn('entity_id'); $this->model = $objectManager->getObject( \Magento\Catalog\Model\Indexer\Product\Flat\Action\Row::class, [ - 'resource' => $this->resource, - 'storeManager' => $this->storeManager, - 'productHelper' => $this->productIndexerHelper, - 'flatItemEraser' => $this->flatItemEraser, - 'flatItemWriter' => $this->flatItemWriter, - 'flatTableBuilder' => $this->flatTableBuilder + 'resource' => $this->resource, + 'storeManager' => $this->storeManager, + 'productHelper' => $this->productIndexerHelper, + 'flatItemEraser' => $this->flatItemEraser, + 'flatItemWriter' => $this->flatItemWriter, + 'flatTableBuilder' => $this->flatTableBuilder, ] ); + + $objectManager->setBackwardCompatibleProperty($this->model, 'metadataPool', $metadataPool); } /** @@ -100,9 +145,9 @@ public function testExecuteWithEmptyId() public function testExecuteWithNonExistingFlatTablesCreatesTables() { $this->productIndexerHelper->expects($this->any())->method('getFlatTableName') - ->will($this->returnValue('store_flat_table')); + ->willReturn('store_flat_table'); $this->connection->expects($this->any())->method('isTableExists')->with('store_flat_table') - ->will($this->returnValue(false)); + ->willReturn(false); $this->flatItemEraser->expects($this->never())->method('removeDeletedProducts'); $this->flatTableBuilder->expects($this->once())->method('build')->with('store_id_1', ['product_id_1']); $this->flatItemWriter->expects($this->once())->method('write')->with('store_id_1', 'product_id_1'); @@ -112,9 +157,9 @@ public function testExecuteWithNonExistingFlatTablesCreatesTables() public function testExecuteWithExistingFlatTablesCreatesTables() { $this->productIndexerHelper->expects($this->any())->method('getFlatTableName') - ->will($this->returnValue('store_flat_table')); + ->willReturn('store_flat_table'); $this->connection->expects($this->any())->method('isTableExists')->with('store_flat_table') - ->will($this->returnValue(true)); + ->willReturn(true); $this->flatItemEraser->expects($this->once())->method('removeDeletedProducts'); $this->flatTableBuilder->expects($this->never())->method('build')->with('store_id_1', ['product_id_1']); $this->model->execute('product_id_1'); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Layer/Filter/DataProvider/PriceTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Layer/Filter/DataProvider/PriceTest.php index f2c77627e38d..8ca23df31cde 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Layer/Filter/DataProvider/PriceTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Layer/Filter/DataProvider/PriceTest.php @@ -152,7 +152,7 @@ public function testGetMaxPrice() $this->productCollection->expects($this->once()) ->method('getMaxPrice') ->will($this->returnValue($maxPrice)); - $this->assertSame(floatval($maxPrice), $this->target->getMaxPrice()); + $this->assertSame((float)$maxPrice, $this->target->getMaxPrice()); } /** diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/CopierTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/CopierTest.php index 31fd0696db32..80b6db2a516b 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/CopierTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/CopierTest.php @@ -6,9 +6,12 @@ namespace Magento\Catalog\Test\Unit\Model\Product; use Magento\Catalog\Api\Data\ProductInterface; -use \Magento\Catalog\Model\Product\Copier; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Copier; /** + * Test for Magento\Catalog\Model\Product\Copier class. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CopierTest extends \PHPUnit\Framework\TestCase @@ -54,7 +57,7 @@ protected function setUp() \Magento\Catalog\Model\Product\Option\Repository::class ); $this->optionRepositoryMock; - $this->productMock = $this->createMock(\Magento\Catalog\Model\Product::class); + $this->productMock = $this->createMock(Product::class); $this->productMock->expects($this->any())->method('getEntityId')->willReturn(1); $this->metadata = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadata::class) @@ -75,6 +78,9 @@ protected function setUp() ]); } + /** + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ public function testCopy() { $stockItem = $this->getMockBuilder(\Magento\CatalogInventory\Api\Data\StockItemInterface::class) @@ -102,11 +108,47 @@ public function testCopy() ['linkField', null, '1'], ]); - $resourceMock = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product::class); - $this->productMock->expects($this->once())->method('getResource')->will($this->returnValue($resourceMock)); + $entityMock = $this->getMockForAbstractClass( + \Magento\Eav\Model\Entity\AbstractEntity::class, + [], + '', + false, + true, + true, + ['checkAttributeUniqueValue'] + ); + $entityMock->expects($this->any()) + ->method('checkAttributeUniqueValue') + ->willReturn(true); + + $attributeMock = $this->getMockForAbstractClass( + \Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class, + [], + '', + false, + true, + true, + ['getEntity'] + ); + $attributeMock->expects($this->any()) + ->method('getEntity') + ->willReturn($entityMock); + + $resourceMock = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Product::class) + ->disableOriginalConstructor() + ->setMethods(['getAttributeRawValue', 'duplicate', 'getAttribute']) + ->getMock(); + $resourceMock->expects($this->any()) + ->method('getAttributeRawValue') + ->willReturn('urk-key-1'); + $resourceMock->expects($this->any()) + ->method('getAttribute') + ->willReturn($attributeMock); + + $this->productMock->expects($this->any())->method('getResource')->will($this->returnValue($resourceMock)); $duplicateMock = $this->createPartialMock( - \Magento\Catalog\Model\Product::class, + Product::class, [ '__wakeup', 'setData', @@ -118,11 +160,11 @@ public function testCopy() 'setCreatedAt', 'setUpdatedAt', 'setId', - 'setStoreId', 'getEntityId', 'save', 'setUrlKey', - 'getUrlKey', + 'setStoreId', + 'getStoreIds', ] ); $this->productFactoryMock->expects($this->once())->method('create')->will($this->returnValue($duplicateMock)); @@ -137,27 +179,22 @@ public function testCopy() )->with( \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_DISABLED ); + $duplicateMock->expects($this->atLeastOnce())->method('setStoreId'); $duplicateMock->expects($this->once())->method('setCreatedAt')->with(null); $duplicateMock->expects($this->once())->method('setUpdatedAt')->with(null); $duplicateMock->expects($this->once())->method('setId')->with(null); - $duplicateMock->expects( - $this->once() - )->method( - 'setStoreId' - )->with( - \Magento\Store\Model\Store::DEFAULT_STORE_ID - ); - $duplicateMock->expects($this->once())->method('setData')->with($productData); + $duplicateMock->expects($this->atLeastOnce())->method('getStoreIds')->willReturn([]); + $duplicateMock->expects($this->atLeastOnce())->method('setData')->willReturn($duplicateMock); $this->copyConstructorMock->expects($this->once())->method('build')->with($this->productMock, $duplicateMock); - $duplicateMock->expects($this->once())->method('getUrlKey')->willReturn('urk-key-1'); - $duplicateMock->expects($this->once())->method('setUrlKey')->with('urk-key-2'); + $duplicateMock->expects($this->once())->method('setUrlKey')->with('urk-key-2')->willReturn($duplicateMock); $duplicateMock->expects($this->once())->method('save'); $this->metadata->expects($this->any())->method('getLinkField')->willReturn('linkField'); $duplicateMock->expects($this->any())->method('getData')->willReturnMap([ ['linkField', null, '2'], - ]); $this->optionRepositoryMock->expects($this->once()) + ]); + $this->optionRepositoryMock->expects($this->once()) ->method('duplicate') ->with($this->productMock, $duplicateMock); $resourceMock->expects($this->once())->method('duplicate')->with(1, 2); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Gallery/GalleryManagementTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Gallery/GalleryManagementTest.php index 9fafbc9d9675..1d12645019d1 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Gallery/GalleryManagementTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Gallery/GalleryManagementTest.php @@ -266,7 +266,7 @@ public function testGetWithNonExistingProduct() /** * @expectedException \Magento\Framework\Exception\NoSuchEntityException - * @expectedExceptionText The image doesn't exist. Verify and try again. + * @expectedExceptionMessage The image doesn't exist. Verify and try again. */ public function testGetWithNonExistingImage() { diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/DefaultValidatorTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/DefaultValidatorTest.php index da6b790fedfa..7c2ec8abb768 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/DefaultValidatorTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/DefaultValidatorTest.php @@ -18,6 +18,11 @@ class DefaultValidatorTest extends \PHPUnit\Framework\TestCase */ protected $valueMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $localeFormatMock; + /** * @inheritdoc */ @@ -26,6 +31,8 @@ protected function setUp() $configMock = $this->createMock(\Magento\Catalog\Model\ProductOptions\ConfigInterface::class); $storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); $priceConfigMock = new \Magento\Catalog\Model\Config\Source\Product\Options\Price($storeManagerMock); + $this->localeFormatMock = $this->createMock(\Magento\Framework\Locale\FormatInterface::class); + $config = [ [ 'label' => 'group label 1', @@ -51,7 +58,8 @@ protected function setUp() $configMock->expects($this->once())->method('getAll')->will($this->returnValue($config)); $this->validator = new \Magento\Catalog\Model\Product\Option\Validator\DefaultValidator( $configMock, - $priceConfigMock + $priceConfigMock, + $this->localeFormatMock ); } @@ -63,10 +71,10 @@ public function isValidTitleDataProvider() { $mess = ['option required fields' => 'Missed values for option required fields']; return [ - ['option_title', 'name 1.1', 'fixed', new \Magento\Framework\DataObject(['store_id' => 1]), [], true], - ['option_title', 'name 1.1', 'fixed', new \Magento\Framework\DataObject(['store_id' => 0]), [], true], - [null, 'name 1.1', 'fixed', new \Magento\Framework\DataObject(['store_id' => 1]), [], true], - [null, 'name 1.1', 'fixed', new \Magento\Framework\DataObject(['store_id' => 0]), $mess, false], + ['option_title', 'name 1.1', 'fixed', 10, new \Magento\Framework\DataObject(['store_id' => 1]), [], true], + ['option_title', 'name 1.1', 'fixed', 10, new \Magento\Framework\DataObject(['store_id' => 0]), [], true], + [null, 'name 1.1', 'fixed', 10, new \Magento\Framework\DataObject(['store_id' => 1]), [], true], + [null, 'name 1.1', 'fixed', 10, new \Magento\Framework\DataObject(['store_id' => 0]), $mess, false], ]; } @@ -79,15 +87,18 @@ public function isValidTitleDataProvider() * @param bool $result * @dataProvider isValidTitleDataProvider */ - public function testIsValidTitle($title, $type, $priceType, $product, $messages, $result) + public function testIsValidTitle($title, $type, $priceType, $price, $product, $messages, $result) { - $methods = ['getTitle', 'getType', 'getPriceType', '__wakeup', 'getProduct']; + $methods = ['getTitle', 'getType', 'getPriceType', 'getPrice', '__wakeup', 'getProduct']; $valueMock = $this->createPartialMock(\Magento\Catalog\Model\Product\Option::class, $methods); $valueMock->expects($this->once())->method('getTitle')->will($this->returnValue($title)); $valueMock->expects($this->any())->method('getType')->will($this->returnValue($type)); $valueMock->expects($this->once())->method('getPriceType')->will($this->returnValue($priceType)); - // $valueMock->expects($this->once())->method('getPrice')->will($this->returnValue($price)); + $valueMock->expects($this->once())->method('getPrice')->will($this->returnValue($price)); $valueMock->expects($this->once())->method('getProduct')->will($this->returnValue($product)); + + $this->localeFormatMock->expects($this->once())->method('getNumber')->will($this->returnValue($price)); + $this->assertEquals($result, $this->validator->isValid($valueMock)); $this->assertEquals($messages, $this->validator->getMessages()); } @@ -126,4 +137,43 @@ public function testIsValidFail($product) $this->assertFalse($this->validator->isValid($valueMock)); $this->assertEquals($messages, $this->validator->getMessages()); } + + /** + * Data provider for testValidationNegativePrice + * @return array + */ + public function validationPriceDataProvider() + { + return [ + ['option_title', 'name 1.1', 'fixed', -12, new \Magento\Framework\DataObject(['store_id' => 1])], + ['option_title', 'name 1.1', 'fixed', -12, new \Magento\Framework\DataObject(['store_id' => 0])], + ['option_title', 'name 1.1', 'fixed', 12, new \Magento\Framework\DataObject(['store_id' => 1])], + ['option_title', 'name 1.1', 'fixed', 12, new \Magento\Framework\DataObject(['store_id' => 0])] + ]; + } + + /** + * @param $title + * @param $type + * @param $priceType + * @param $price + * @param $product + * @dataProvider validationPriceDataProvider + */ + public function testValidationPrice($title, $type, $priceType, $price, $product) + { + $methods = ['getTitle', 'getType', 'getPriceType', 'getPrice', '__wakeup', 'getProduct']; + $valueMock = $this->createPartialMock(\Magento\Catalog\Model\Product\Option::class, $methods); + $valueMock->expects($this->once())->method('getTitle')->will($this->returnValue($title)); + $valueMock->expects($this->exactly(2))->method('getType')->will($this->returnValue($type)); + $valueMock->expects($this->once())->method('getPriceType')->will($this->returnValue($priceType)); + $valueMock->expects($this->once())->method('getPrice')->will($this->returnValue($price)); + $valueMock->expects($this->once())->method('getProduct')->will($this->returnValue($product)); + + $this->localeFormatMock->expects($this->once())->method('getNumber')->will($this->returnValue($price)); + + $messages = []; + $this->assertTrue($this->validator->isValid($valueMock)); + $this->assertEquals($messages, $this->validator->getMessages()); + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/FileTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/FileTest.php index 2de993c07551..e688da1c6aa1 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/FileTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/FileTest.php @@ -18,6 +18,11 @@ class FileTest extends \PHPUnit\Framework\TestCase */ protected $valueMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $localeFormatMock; + /** * @inheritdoc */ @@ -26,6 +31,8 @@ protected function setUp() $configMock = $this->createMock(\Magento\Catalog\Model\ProductOptions\ConfigInterface::class); $storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); $priceConfigMock = new \Magento\Catalog\Model\Config\Source\Product\Options\Price($storeManagerMock); + $this->localeFormatMock = $this->createMock(\Magento\Framework\Locale\FormatInterface::class); + $config = [ [ 'label' => 'group label 1', @@ -53,7 +60,8 @@ protected function setUp() $this->valueMock = $this->createPartialMock(\Magento\Catalog\Model\Product\Option::class, $methods); $this->validator = new \Magento\Catalog\Model\Product\Option\Validator\File( $configMock, - $priceConfigMock + $priceConfigMock, + $this->localeFormatMock ); } @@ -70,6 +78,15 @@ public function testIsValidSuccess() ->willReturn(10); $this->valueMock->expects($this->once())->method('getImageSizeX')->will($this->returnValue(10)); $this->valueMock->expects($this->once())->method('getImageSizeY')->will($this->returnValue(15)); + $this->localeFormatMock->expects($this->at(0)) + ->method('getNumber') + ->with($this->equalTo(10)) + ->will($this->returnValue(10)); + $this->localeFormatMock + ->expects($this->at(2)) + ->method('getNumber') + ->with($this->equalTo(15)) + ->will($this->returnValue(15)); $this->assertEmpty($this->validator->getMessages()); $this->assertTrue($this->validator->isValid($this->valueMock)); } @@ -87,6 +104,16 @@ public function testIsValidWithNegativeImageSize() ->willReturn(10); $this->valueMock->expects($this->once())->method('getImageSizeX')->will($this->returnValue(-10)); $this->valueMock->expects($this->never())->method('getImageSizeY'); + $this->localeFormatMock->expects($this->at(0)) + ->method('getNumber') + ->with($this->equalTo(10)) + ->will($this->returnValue(10)); + $this->localeFormatMock + ->expects($this->at(1)) + ->method('getNumber') + ->with($this->equalTo(-10)) + ->will($this->returnValue(-10)); + $messages = [ 'option values' => 'Invalid option value', ]; @@ -107,6 +134,15 @@ public function testIsValidWithNegativeImageSizeY() ->willReturn(10); $this->valueMock->expects($this->once())->method('getImageSizeX')->will($this->returnValue(10)); $this->valueMock->expects($this->once())->method('getImageSizeY')->will($this->returnValue(-10)); + $this->localeFormatMock->expects($this->at(0)) + ->method('getNumber') + ->with($this->equalTo(10)) + ->will($this->returnValue(10)); + $this->localeFormatMock + ->expects($this->at(2)) + ->method('getNumber') + ->with($this->equalTo(-10)) + ->will($this->returnValue(-10)); $messages = [ 'option values' => 'Invalid option value', ]; diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/SelectTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/SelectTest.php index b97783edf856..7fad5592a2d2 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/SelectTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/SelectTest.php @@ -18,6 +18,11 @@ class SelectTest extends \PHPUnit\Framework\TestCase */ protected $valueMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $localeFormatMock; + /** * @inheritdoc */ @@ -26,6 +31,7 @@ protected function setUp() $configMock = $this->createMock(\Magento\Catalog\Model\ProductOptions\ConfigInterface::class); $storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); $priceConfigMock = new \Magento\Catalog\Model\Config\Source\Product\Options\Price($storeManagerMock); + $this->localeFormatMock = $this->createMock(\Magento\Framework\Locale\FormatInterface::class); $config = [ [ 'label' => 'group label 1', @@ -53,7 +59,8 @@ protected function setUp() $this->valueMock = $this->createPartialMock(\Magento\Catalog\Model\Product\Option::class, $methods, []); $this->validator = new \Magento\Catalog\Model\Product\Option\Validator\Select( $configMock, - $priceConfigMock + $priceConfigMock, + $this->localeFormatMock ); } @@ -69,6 +76,12 @@ public function testIsValidSuccess($expectedResult, array $value) $this->valueMock->expects($this->never())->method('getPriceType'); $this->valueMock->expects($this->never())->method('getPrice'); $this->valueMock->expects($this->any())->method('getData')->with('values')->will($this->returnValue([$value])); + if (isset($value['price'])) { + $this->localeFormatMock + ->expects($this->once()) + ->method('getNumber') + ->will($this->returnValue($value['price'])); + } $this->assertEquals($expectedResult, $this->validator->isValid($this->valueMock)); } @@ -117,6 +130,7 @@ public function testIsValidateWithInvalidOptionValues() ->method('getData') ->with('values') ->will($this->returnValue('invalid_data')); + $messages = [ 'option values' => 'Invalid option value', ]; @@ -159,6 +173,7 @@ public function testIsValidateWithInvalidData($priceType, $price, $title) $this->valueMock->expects($this->never())->method('getPriceType'); $this->valueMock->expects($this->never())->method('getPrice'); $this->valueMock->expects($this->any())->method('getData')->with('values')->will($this->returnValue([$value])); + $this->localeFormatMock->expects($this->any())->method('getNumber')->will($this->returnValue($price)); $messages = [ 'option values' => 'Invalid option value', ]; diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/TextTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/TextTest.php index 4881154728dd..a3e6189f7492 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/TextTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/TextTest.php @@ -18,6 +18,11 @@ class TextTest extends \PHPUnit\Framework\TestCase */ protected $valueMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $localeFormatMock; + /** * @inheritdoc */ @@ -26,6 +31,7 @@ protected function setUp() $configMock = $this->createMock(\Magento\Catalog\Model\ProductOptions\ConfigInterface::class); $storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); $priceConfigMock = new \Magento\Catalog\Model\Config\Source\Product\Options\Price($storeManagerMock); + $this->localeFormatMock = $this->createMock(\Magento\Framework\Locale\FormatInterface::class); $config = [ [ 'label' => 'group label 1', @@ -53,7 +59,8 @@ protected function setUp() $this->valueMock = $this->createPartialMock(\Magento\Catalog\Model\Product\Option::class, $methods); $this->validator = new \Magento\Catalog\Model\Product\Option\Validator\Text( $configMock, - $priceConfigMock + $priceConfigMock, + $this->localeFormatMock ); } @@ -69,6 +76,10 @@ public function testIsValidSuccess() $this->valueMock->method('getPrice') ->willReturn(10); $this->valueMock->expects($this->once())->method('getMaxCharacters')->will($this->returnValue(10)); + $this->localeFormatMock->expects($this->exactly(2)) + ->method('getNumber') + ->with($this->equalTo(10)) + ->will($this->returnValue(10)); $this->assertTrue($this->validator->isValid($this->valueMock)); $this->assertEmpty($this->validator->getMessages()); } @@ -85,6 +96,15 @@ public function testIsValidWithNegativeMaxCharacters() $this->valueMock->method('getPrice') ->willReturn(10); $this->valueMock->expects($this->once())->method('getMaxCharacters')->will($this->returnValue(-10)); + $this->localeFormatMock->expects($this->at(0)) + ->method('getNumber') + ->with($this->equalTo(10)) + ->will($this->returnValue(10)); + $this->localeFormatMock + ->expects($this->at(1)) + ->method('getNumber') + ->with($this->equalTo(-10)) + ->will($this->returnValue(-10)); $messages = [ 'option values' => 'Invalid option value', ]; diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/TierPriceStorageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/TierPriceStorageTest.php index c9288790ed6e..a97f2281125a 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/TierPriceStorageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/TierPriceStorageTest.php @@ -152,6 +152,30 @@ public function testGet() $this->assertEquals(2, count($prices)); } + /** + * Test get method without tierprices. + * + * @return void + */ + public function testGetWithoutTierPrices() + { + $skus = ['simple', 'virtual']; + $this->tierPriceValidator + ->expects($this->once()) + ->method('validateSkus') + ->with($skus) + ->willReturn($skus); + $this->productIdLocator->expects($this->atLeastOnce()) + ->method('retrieveProductIdsBySkus') + ->with(['simple', 'virtual']) + ->willReturn(['simple' => ['2' => 'simple'], 'virtual' => ['3' => 'virtual']]); + $this->tierPricePersistence->expects($this->once())->method('get')->willReturn([]); + $this->tierPricePersistence->expects($this->never())->method('getEntityLinkField'); + $this->tierPriceFactory->expects($this->never())->method('create'); + $prices = $this->tierPriceStorage->get($skus); + $this->assertEmpty($prices); + } + /** * Test update method. * diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/PriceModifierTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/PriceModifierTest.php index 754d80302d41..6029a2b82008 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/PriceModifierTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/PriceModifierTest.php @@ -54,7 +54,7 @@ protected function setUp() /** * @expectedException \Magento\Framework\Exception\NoSuchEntityException - * @expectedMessage Tier price is unavailable for this product. + * @expectedExceptionMessage Product hasn't group price with such data: customerGroupId = '1', website = 1, qty = 3 */ public function testRemoveWhenTierPricesNotExists() { @@ -70,7 +70,7 @@ public function testRemoveWhenTierPricesNotExists() /** * @expectedException \Magento\Framework\Exception\NoSuchEntityException - * @expectedMessage For current customerGroupId = '10' with 'qty' = 15 any tier price exist'. + * @expectedExceptionMessage Product hasn't group price with such data: customerGroupId = '10', website = 1, qty = 5 */ public function testRemoveTierPriceForNonExistingCustomerGroup() { @@ -81,7 +81,7 @@ public function testRemoveTierPriceForNonExistingCustomerGroup() ->will($this->returnValue($this->prices)); $this->productMock->expects($this->never())->method('setData'); $this->productRepositoryMock->expects($this->never())->method('save'); - $this->priceModifier->removeTierPrice($this->productMock, 10, 15, 1); + $this->priceModifier->removeTierPrice($this->productMock, 10, 5, 1); } public function testSuccessfullyRemoveTierPriceSpecifiedForAllGroups() diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductFrontendAction/SynchronizerTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductFrontendAction/SynchronizerTest.php index fce4a02622d9..38bed83cb950 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductFrontendAction/SynchronizerTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductFrontendAction/SynchronizerTest.php @@ -80,6 +80,7 @@ protected function setUp() public function testFilterProductActions() { + $typeId = 'recently_compared_product'; $productsData = [ 1 => [ 'added_at' => 12, @@ -87,7 +88,7 @@ public function testFilterProductActions() ], 2 => [ 'added_at' => 13, - 'product_id' => 2, + 'product_id' => '2', ], 3 => [ 'added_at' => 14, @@ -126,10 +127,12 @@ public function testFilterProductActions() $collection->expects($this->once()) ->method('addFilterByUserIdentities') ->with(1, 34); - $collection->expects($this->any()) + $collection->expects($this->at(1)) ->method('addFieldToFilter') - ->withConsecutive(['type_id'], ['product_id']); - + ->with('type_id', $typeId); + $collection->expects($this->at(2)) + ->method('addFieldToFilter') + ->with('product_id', [1, 2]); $iterator = new \IteratorIterator(new \ArrayIterator([$frontendAction])); $collection->expects($this->once()) ->method('getIterator') diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/TierPriceManagementTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/TierPriceManagementTest.php index f340d0b204b6..ae479a9b34d4 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/TierPriceManagementTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/TierPriceManagementTest.php @@ -195,7 +195,7 @@ public function testSuccessDeleteTierPrice() /** * @expectedException \Magento\Framework\Exception\NoSuchEntityException - * @message The product doesn't exist. Verify and try again. + * @expectedExceptionMessage No such entity. */ public function testDeleteTierPriceFromNonExistingProduct() { diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductIdLocatorTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductIdLocatorTest.php index b730e12ca820..b9cb82274c80 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductIdLocatorTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductIdLocatorTest.php @@ -58,7 +58,16 @@ public function testRetrieveProductIdsBySkus() { $skus = ['sku_1', 'sku_2']; $collection = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Product\Collection::class) - ->setMethods(['getIterator', 'addFieldToFilter']) + ->setMethods( + [ + 'getItems', + 'addFieldToFilter', + 'setPageSize', + 'getLastPageNumber', + 'setCurPage', + 'clear' + ] + ) ->disableOriginalConstructor()->getMock(); $product = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) ->setMethods(['getSku', 'getData', 'getTypeId']) @@ -69,7 +78,11 @@ public function testRetrieveProductIdsBySkus() $this->collectionFactory->expects($this->once())->method('create')->willReturn($collection); $collection->expects($this->once())->method('addFieldToFilter') ->with(\Magento\Catalog\Api\Data\ProductInterface::SKU, ['in' => $skus])->willReturnSelf(); - $collection->expects($this->once())->method('getIterator')->willReturn(new \ArrayIterator([$product])); + $collection->expects($this->atLeastOnce())->method('getItems')->willReturn([$product]); + $collection->expects($this->atLeastOnce())->method('setPageSize')->willReturnSelf(); + $collection->expects($this->atLeastOnce())->method('getLastPageNumber')->willReturn(1); + $collection->expects($this->atLeastOnce())->method('setCurPage')->with(1)->willReturnSelf(); + $collection->expects($this->atLeastOnce())->method('clear')->willReturnSelf(); $this->metadataPool ->expects($this->once()) ->method('getMetadata') 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 034b04b6a757..cfb54c3aefd0 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/ProductRepositoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php index c729a0c58e1e..cb92cc6c2d52 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -9,30 +8,41 @@ namespace Magento\Catalog\Test\Unit\Model; use Magento\Catalog\Api\Data\ProductExtensionInterface; +use Magento\Catalog\Api\Data\ProductSearchResultsInterface; use Magento\Catalog\Api\Data\ProductSearchResultsInterfaceFactory; use Magento\Catalog\Api\ProductAttributeRepositoryInterface; use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Gallery\MimeTypeExtensionMap; +use Magento\Catalog\Model\Product\Gallery\Processor; use Magento\Catalog\Model\Product\LinkTypeProvider; +use Magento\Catalog\Model\Product\Media\Config; +use Magento\Catalog\Model\Product\Option; +use Magento\Catalog\Model\Product\Option\Value; use Magento\Catalog\Model\ProductFactory; +use Magento\Catalog\Model\ProductLink\Link; use Magento\Catalog\Model\ProductRepository; +use Magento\Catalog\Model\ResourceModel\Product\Collection; use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; +use Magento\Framework\Api\Data\ImageContentInterface; use Magento\Framework\Api\Data\ImageContentInterfaceFactory; use Magento\Framework\Api\ExtensibleDataObjectConverter; use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\ImageContent; use Magento\Framework\Api\ImageContentValidator; use Magento\Framework\Api\ImageContentValidatorInterface; use Magento\Framework\Api\ImageProcessorInterface; -use Magento\Framework\Api\Data\ImageContentInterface; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\DB\Adapter\ConnectionException; use Magento\Framework\Filesystem; use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; +use PHPUnit\Framework\TestCase; use PHPUnit_Framework_MockObject_MockObject as MockObject; /** @@ -42,12 +52,12 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ -class ProductRepositoryTest extends \PHPUnit\Framework\TestCase +class ProductRepositoryTest extends TestCase { /** * @var Product|MockObject */ - protected $product; + private $product; /** * @var Product|MockObject @@ -153,12 +163,12 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase private $storeManager; /** - * @var \Magento\Catalog\Model\Product\Gallery\Processor|\PHPUnit_Framework_MockObject_MockObject + * @var Processor|MockObject */ - private $mediaGalleryProcessor; + private $processor; /** - * @var CollectionProcessorInterface|\PHPUnit_Framework_MockObject_MockObject + * @var CollectionProcessorInterface|MockObject */ private $collectionProcessor; @@ -168,7 +178,7 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase private $productExtension; /** - * @var Json|\PHPUnit_Framework_MockObject_MockObject + * @var Json|MockObject */ private $serializerMock; @@ -185,12 +195,12 @@ class ProductRepositoryTest extends \PHPUnit\Framework\TestCase protected function setUp() { $this->productFactory = $this->createPartialMock( - \Magento\Catalog\Model\ProductFactory::class, + ProductFactory::class, ['create', 'setData'] ); $this->product = $this->createPartialMock( - \Magento\Catalog\Model\Product::class, + Product::class, [ 'getId', 'getSku', @@ -200,12 +210,13 @@ protected function setUp() 'setData', 'getStoreId', 'getMediaGalleryEntries', - 'getExtensionAttributes' + 'getExtensionAttributes', + 'getCategoryIds' ] ); $this->initializedProduct = $this->createPartialMock( - \Magento\Catalog\Model\Product::class, + Product::class, [ 'getWebsiteIds', 'setProductOptions', @@ -220,7 +231,8 @@ protected function setUp() 'validate', 'save', 'getMediaGalleryEntries', - 'getExtensionAttributes' + 'getExtensionAttributes', + 'getCategoryIds' ] ); $this->initializedProduct->expects($this->any()) @@ -232,7 +244,7 @@ protected function setUp() $this->searchCriteriaBuilder = $this->createMock(SearchCriteriaBuilder::class); $this->metadataService = $this->createMock(ProductAttributeRepositoryInterface::class); $this->searchResultsFactory = $this->createPartialMock( - \Magento\Catalog\Api\Data\ProductSearchResultsInterfaceFactory::class, + ProductSearchResultsInterfaceFactory::class, ['create'] ); $this->resourceModel = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product::class); @@ -269,15 +281,21 @@ protected function setUp() $this->initializedProduct ->method('getExtensionAttributes') ->willReturn($this->productExtension); + $this->product + ->method('getCategoryIds') + ->willReturn([1, 2, 3, 4]); + $this->initializedProduct + ->method('getCategoryIds') + ->willReturn([1, 2, 3, 4]); $storeMock = $this->getMockBuilder(StoreInterface::class) ->disableOriginalConstructor() ->setMethods([]) ->getMockForAbstractClass(); $storeMock->expects($this->any())->method('getWebsiteId')->willReturn('1'); - $storeMock->expects($this->any())->method('getCode')->willReturn(\Magento\Store\Model\Store::ADMIN_CODE); + $storeMock->expects($this->any())->method('getCode')->willReturn(Store::ADMIN_CODE); $this->storeManager->expects($this->any())->method('getStore')->willReturn($storeMock); - $this->mediaGalleryProcessor = $this->createMock(\Magento\Catalog\Model\Product\Gallery\Processor::class); + $this->processor = $this->createMock(Processor::class); $this->collectionProcessor = $this->getMockBuilder(CollectionProcessorInterface::class) ->getMock(); @@ -293,6 +311,14 @@ function ($value) { ) ); + $mediaProcessor = $this->objectManager->getObject( + ProductRepository\MediaGalleryProcessor::class, + [ + 'processor' => $this->processor, + 'contentFactory' => $this->contentFactory, + 'imageProcessor' => $this->imageProcessor, + ] + ); $this->model = $this->objectManager->getObject( ProductRepository::class, [ @@ -307,17 +333,16 @@ function ($value) { 'extensibleDataObjectConverter' => $this->extensibleDataObjectConverter, 'contentValidator' => $this->contentValidator, 'fileSystem' => $this->fileSystem, - 'contentFactory' => $this->contentFactory, 'mimeTypeExtensionMap' => $this->mimeTypeExtensionMap, 'linkTypeProvider' => $this->linkTypeProvider, - 'imageProcessor' => $this->imageProcessor, 'storeManager' => $this->storeManager, - 'mediaGalleryProcessor' => $this->mediaGalleryProcessor, + 'mediaGalleryProcessor' => $this->processor, 'collectionProcessor' => $this->collectionProcessor, 'serializer' => $this->serializerMock, 'cacheLimit' => $this->cacheLimit ] ); + $this->objectManager->setBackwardCompatibleProperty($this->model, 'mediaProcessor', $mediaProcessor); } /** @@ -500,7 +525,7 @@ private function getProductMocksForReducedCache($productsCount) $productMocks = []; for ($i = 1; $i <= $productsCount; $i++) { - $productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) + $productMock = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() ->setMethods([ 'getId', @@ -753,8 +778,8 @@ public function testDeleteById() public function testGetList() { - $searchCriteriaMock = $this->createMock(\Magento\Framework\Api\SearchCriteriaInterface::class); - $collectionMock = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product\Collection::class); + $searchCriteriaMock = $this->createMock(SearchCriteriaInterface::class); + $collectionMock = $this->createMock(Collection::class); $this->collectionFactory->expects($this->once())->method('create')->willReturn($collectionMock); $this->product->method('getSku')->willReturn('simple'); $collectionMock->expects($this->once())->method('addAttributeToSelect')->with('*'); @@ -769,7 +794,7 @@ public function testGetList() $collectionMock->expects($this->once())->method('addCategoryIds'); $collectionMock->expects($this->atLeastOnce())->method('getItems')->willReturn([$this->product]); $collectionMock->expects($this->once())->method('getSize')->willReturn(128); - $searchResultsMock = $this->createMock(\Magento\Catalog\Api\Data\ProductSearchResultsInterface::class); + $searchResultsMock = $this->createMock(ProductSearchResultsInterface::class); $searchResultsMock->expects($this->once())->method('setSearchCriteria')->with($searchCriteriaMock); $searchResultsMock->expects($this->once())->method('setItems')->with([$this->product]); $this->searchResultsFactory->expects($this->once())->method('create')->willReturn($searchResultsMock); @@ -903,8 +928,8 @@ public function saveExistingWithOptionsDataProvider() ], ]; - /** @var \Magento\Catalog\Model\Product\Option|\PHPUnit_Framework_MockObject_MockObject $existingOption1 */ - $existingOption1 = $this->getMockBuilder(\Magento\Catalog\Model\Product\Option::class) + /** @var Option|MockObject $existingOption1 */ + $existingOption1 = $this->getMockBuilder(Option::class) ->disableOriginalConstructor() ->setMethods(null) ->getMock(); @@ -914,8 +939,8 @@ public function saveExistingWithOptionsDataProvider() "type" => "drop_down", ] ); - /** @var \Magento\Catalog\Model\Product\Option\Value $existingOptionValue1 */ - $existingOptionValue1 = $this->getMockBuilder(\Magento\Catalog\Model\Product\Option\Value::class) + /** @var Value $existingOptionValue1 */ + $existingOptionValue1 = $this->getMockBuilder(Value::class) ->disableOriginalConstructor() ->setMethods(null) ->getMock(); @@ -926,7 +951,7 @@ public function saveExistingWithOptionsDataProvider() "price" => 5, ] ); - $existingOptionValue2 = $this->getMockBuilder(\Magento\Catalog\Model\Product\Option\Value::class) + $existingOptionValue2 = $this->getMockBuilder(Value::class) ->disableOriginalConstructor() ->setMethods(null) ->getMock(); @@ -943,7 +968,7 @@ public function saveExistingWithOptionsDataProvider() "9" => $existingOptionValue2, ] ); - $existingOption2 = $this->getMockBuilder(\Magento\Catalog\Model\Product\Option::class) + $existingOption2 = $this->getMockBuilder(Option::class) ->disableOriginalConstructor() ->setMethods(null) ->getMock(); @@ -1008,8 +1033,8 @@ public function saveExistingWithOptionsDataProvider() * @param array $existingLinks * @param array $expectedData * @dataProvider saveWithLinksDataProvider - * @throws \Magento\Framework\Exception\CouldNotSaveException - * @throws \Magento\Framework\Exception\InputException + * @throws CouldNotSaveException + * @throws InputException */ public function testSaveWithLinks(array $newLinks, array $existingLinks, array $expectedData) { @@ -1037,7 +1062,7 @@ public function testSaveWithLinks(array $newLinks, array $existingLinks, array $ ->expects($this->any())->method('getProductsIdsBySkus') ->willReturn([$newLinks['linked_product_sku'] => $newLinks['linked_product_sku']]); - $inputLink = $this->objectManager->getObject(\Magento\Catalog\Model\ProductLink\Link::class); + $inputLink = $this->objectManager->getObject(Link::class); $inputLink->setProductSku($newLinks['product_sku']); $inputLink->setLinkType($newLinks['link_type']); $inputLink->setLinkedProductSku($newLinks['linked_product_sku']); @@ -1081,7 +1106,7 @@ public function testSaveWithLinks(array $newLinks, array $existingLinks, array $ $outputLinks = []; if (!empty($expectedData)) { foreach ($expectedData as $link) { - $outputLink = $this->objectManager->getObject(\Magento\Catalog\Model\ProductLink\Link::class); + $outputLink = $this->objectManager->getObject(Link::class); $outputLink->setProductSku($link['product_sku']); $outputLink->setLinkType($link['link_type']); $outputLink->setLinkedProductSku($link['linked_product_sku']); @@ -1235,10 +1260,10 @@ public function testSaveExistingWithNewMediaGalleryEntries() $mediaTmpPath = '/tmp'; $absolutePath = '/a/b/filename.jpg'; - $this->mediaGalleryProcessor->expects($this->once())->method('clearMediaAttribute') + $this->processor->expects($this->once())->method('clearMediaAttribute') ->with($this->initializedProduct, ['image', 'small_image']); - $mediaConfigMock = $this->getMockBuilder(\Magento\Catalog\Model\Product\Media\Config::class) + $mediaConfigMock = $this->getMockBuilder(Config::class) ->disableOriginalConstructor() ->getMock(); $mediaConfigMock->expects($this->once()) @@ -1250,7 +1275,7 @@ public function testSaveExistingWithNewMediaGalleryEntries() ->willReturn($mediaConfigMock); //verify new entries - $contentDataObject = $this->getMockBuilder(\Magento\Framework\Api\ImageContent::class) + $contentDataObject = $this->getMockBuilder(ImageContent::class) ->disableOriginalConstructor() ->setMethods(null) ->getMock(); @@ -1263,10 +1288,10 @@ public function testSaveExistingWithNewMediaGalleryEntries() ->willReturn($absolutePath); $imageFileUri = "imageFileUri"; - $this->mediaGalleryProcessor->expects($this->once())->method('addImage') + $this->processor->expects($this->once())->method('addImage') ->with($this->initializedProduct, $mediaTmpPath . $absolutePath, ['image', 'small_image'], true, false) ->willReturn($imageFileUri); - $this->mediaGalleryProcessor->expects($this->once())->method('updateImage') + $this->processor->expects($this->once())->method('updateImage') ->with( $this->initializedProduct, $imageFileUri, @@ -1386,9 +1411,9 @@ public function testSaveExistingWithMediaGalleryEntries() ->method('getMediaAttributes') ->willReturn(["image" => "filename1", "small_image" => "filename2"]); - $this->mediaGalleryProcessor->expects($this->once())->method('clearMediaAttribute') + $this->processor->expects($this->once())->method('clearMediaAttribute') ->with($this->initializedProduct, ['image', 'small_image']); - $this->mediaGalleryProcessor->expects($this->once()) + $this->processor->expects($this->once()) ->method('setMediaAttribute') ->with($this->initializedProduct, ['image', 'small_image'], 'filename1'); $this->initializedProduct->expects($this->atLeastOnce()) diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php index c95b812705ad..22ba6bfa9f7f 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php @@ -8,11 +8,11 @@ use Magento\Catalog\Api\Data\ProductExtensionInterface; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\Framework\Api\Data\ImageContentInterface; use Magento\Framework\Api\ExtensibleDataInterface; use Magento\Framework\Api\ExtensionAttributesFactory; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -use Magento\Catalog\Model\Product\Attribute\Source\Status; /** * Product Test @@ -180,7 +180,7 @@ class ProductTest extends \PHPUnit\Framework\TestCase /** * @var \PHPUnit_Framework_MockObject_MockObject */ - private $extensionAttrbutes; + private $extensionAttributes; /** * @var \PHPUnit_Framework_MockObject_MockObject @@ -200,7 +200,7 @@ class ProductTest extends \PHPUnit\Framework\TestCase /** * @var ProductExtensionInterface|\PHPUnit_Framework_MockObject_MockObject */ - private $extensionAttributes; + private $productExtAttributes; /** * @var \Magento\Eav\Model\Config|\PHPUnit_Framework_MockObject_MockObject @@ -218,7 +218,7 @@ protected function setUp() \Magento\Framework\Module\Manager::class, ['isEnabled'] ); - $this->extensionAttrbutes = $this->getMockBuilder(\Magento\Framework\Api\ExtensionAttributesInterface::class) + $this->extensionAttributes = $this->getMockBuilder(\Magento\Framework\Api\ExtensionAttributesInterface::class) ->setMethods(['getWebsiteIds', 'setWebsiteIds']) ->disableOriginalConstructor() ->getMock(); @@ -372,13 +372,13 @@ protected function setUp() $this->mediaConfig = $this->createMock(\Magento\Catalog\Model\Product\Media\Config::class); $this->eavConfig = $this->createMock(\Magento\Eav\Model\Config::class); - $this->extensionAttributes = $this->getMockBuilder(ProductExtensionInterface::class) + $this->productExtAttributes = $this->getMockBuilder(ProductExtensionInterface::class) ->setMethods(['getStockItem']) ->getMockForAbstractClass(); $this->extensionAttributesFactory ->expects($this->any()) ->method('create') - ->willReturn($this->extensionAttributes); + ->willReturn($this->productExtAttributes); $this->filterCustomAttribute = $this->createTestProxy( \Magento\Catalog\Model\FilterProductCustomAttribute::class @@ -549,6 +549,7 @@ public function testSetCategoryCollection() public function testGetCategory() { + $this->model->setData('category_ids', [10]); $this->category->expects($this->any())->method('getId')->will($this->returnValue(10)); $this->registry->expects($this->any())->method('registry')->will($this->returnValue($this->category)); $this->categoryRepository->expects($this->any())->method('get')->will($this->returnValue($this->category)); @@ -557,7 +558,8 @@ public function testGetCategory() public function testGetCategoryId() { - $this->category->expects($this->once())->method('getId')->will($this->returnValue(10)); + $this->model->setData('category_ids', [10]); + $this->category->expects($this->any())->method('getId')->will($this->returnValue(10)); $this->registry->expects($this->at(0))->method('registry'); $this->registry->expects($this->at(1))->method('registry')->will($this->returnValue($this->category)); 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 e1847bea53fc..868252da8190 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 7edbc399a947..701338774baa 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/CategoryLinkTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CategoryLinkTest.php index 9e2b19660299..5a1a5906ec4b 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CategoryLinkTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CategoryLinkTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Catalog\Test\Unit\Model\ResourceModel\Product; use Magento\Catalog\Model\ResourceModel\Product\CategoryLink; @@ -129,9 +130,20 @@ public function testSaveCategoryLinks($newCategoryLinks, $dbCategoryLinks, $affe ); } + $expectedResult = []; + + foreach ($affectedIds as $type => $ids) { + $expectedResult = array_merge($expectedResult, $ids); + // Verify if the correct insert, update and/or delete actions are performed: + $this->setupExpectationsForConnection($type, $ids); + } + $actualResult = $this->model->saveCategoryLinks($product, $newCategoryLinks); + sort($actualResult); - $this->assertEquals($affectedIds, $actualResult); + sort($expectedResult); + + $this->assertEquals($expectedResult, $actualResult); } /** @@ -151,7 +163,11 @@ public function getCategoryLinksDataProvider() ['category_id' => 3, 'position' => 10], ['category_id' => 4, 'position' => 20], ], - [], // Nothing to update - data not changed + [ + 'update' => [], + 'insert' => [], + 'delete' => [], + ], ], [ [ @@ -162,7 +178,11 @@ public function getCategoryLinksDataProvider() ['category_id' => 3, 'position' => 10], ['category_id' => 4, 'position' => 20], ], - [3, 4, 5], // 4 - updated position, 5 - added, 3 - deleted + [ + 'update' => [4], + 'insert' => [5], + 'delete' => [3], + ], ], [ [ @@ -173,7 +193,11 @@ public function getCategoryLinksDataProvider() ['category_id' => 3, 'position' => 10], ['category_id' => 4, 'position' => 20], ], - [3, 4], // 3 - updated position, 4 - deleted + [ + 'update' => [3], + 'insert' => [], + 'delete' => [4], + ], ], [ [], @@ -181,8 +205,80 @@ public function getCategoryLinksDataProvider() ['category_id' => 3, 'position' => 10], ['category_id' => 4, 'position' => 20], ], - [3, 4], // 3, 4 - deleted + [ + 'update' => [], + 'insert' => [], + 'delete' => [3, 4], + ], ], + [ + [ + ['category_id' => 3, 'position' => 10], + ['category_id' => 4, 'position' => 20], + ], + [ + ['category_id' => 3, 'position' => 20], // swapped positions + ['category_id' => 4, 'position' => 10], // swapped positions + ], + [ + 'update' => [3, 4], + 'insert' => [], + 'delete' => [], + ], + ] ]; } + + /** + * @param $type + * @param $ids + */ + private function setupExpectationsForConnection($type, $ids): void + { + switch ($type) { + case 'insert': + $this->connectionMock + ->expects($this->exactly(empty($ids) ? 0 : 1)) + ->method('insertArray') + ->with( + $this->anything(), + $this->anything(), + $this->callback(function ($data) use ($ids) { + $foundIds = []; + foreach ($data as $row) { + $foundIds[] = $row['category_id']; + } + return $ids === $foundIds; + }) + ); + break; + case 'update': + $this->connectionMock + ->expects($this->exactly(empty($ids) ? 0 : 1)) + ->method('insertOnDuplicate') + ->with( + $this->anything(), + $this->callback(function ($data) use ($ids) { + $foundIds = []; + foreach ($data as $row) { + $foundIds[] = $row['category_id']; + } + return $ids === $foundIds; + }) + ); + break; + case 'delete': + $this->connectionMock + ->expects($this->exactly(empty($ids) ? 0 : 1)) + ->method('delete') + // Verify that the correct category ID's are touched: + ->with( + $this->anything(), + $this->callback(function ($data) use ($ids) { + return array_values($data)[1] === $ids; + }) + ); + break; + } + } } 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 dbbb3fb29513..0316b2e374d2 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 @@ -58,13 +58,18 @@ class CollectionTest extends \PHPUnit\Framework\TestCase */ private $storeManager; + /** + * @var \Magento\Framework\Data\Collection\EntityFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $entityFactory; + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ protected function setUp() { $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $entityFactory = $this->createMock(\Magento\Framework\Data\Collection\EntityFactory::class); + $this->entityFactory = $this->createMock(\Magento\Framework\Data\Collection\EntityFactory::class); $logger = $this->getMockBuilder(\Psr\Log\LoggerInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); @@ -120,32 +125,25 @@ protected function setUp() $groupManagement = $this->getMockBuilder(\Magento\Customer\Api\GroupManagementInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->connectionMock = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) ->setMethods(['getId']) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->selectMock = $this->getMockBuilder(\Magento\Framework\DB\Select::class) ->disableOriginalConstructor() ->getMock(); - $this->entityMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\AbstractEntity::class) ->disableOriginalConstructor() ->getMock(); - $this->galleryResourceMock = $this->getMockBuilder( \Magento\Catalog\Model\ResourceModel\Product\Gallery::class )->disableOriginalConstructor()->getMock(); - $this->metadataPoolMock = $this->getMockBuilder( \Magento\Framework\EntityManager\MetadataPool::class )->disableOriginalConstructor()->getMock(); - $this->galleryReadHandlerMock = $this->getMockBuilder( \Magento\Catalog\Model\Product\Gallery\ReadHandler::class )->disableOriginalConstructor()->getMock(); - $this->storeManager->expects($this->any())->method('getId')->willReturn(1); $this->storeManager->expects($this->any())->method('getStore')->willReturnSelf(); $universalFactory->expects($this->exactly(1))->method('create')->willReturnOnConsecutiveCalls( @@ -168,7 +166,7 @@ protected function setUp() $this->collection = $this->objectManager->getObject( \Magento\Catalog\Model\ResourceModel\Product\Collection::class, [ - 'entityFactory' => $entityFactory, + 'entityFactory' => $this->entityFactory, 'logger' => $logger, 'fetchStrategy' => $fetchStrategy, 'eventManager' => $eventManager, @@ -318,7 +316,7 @@ public function testAddTierPriceDataByGroupId() [ '(customer_group_id=? AND all_groups=0) OR all_groups=1', $customerGroupId] ) ->willReturnSelf(); - $select->expects($this->once())->method('order')->with('entity_id')->willReturnSelf(); + $select->expects($this->once())->method('order')->with('qty')->willReturnSelf(); $this->connectionMock->expects($this->once()) ->method('fetchAll') ->with($select) @@ -370,7 +368,7 @@ public function testAddTierPriceData() $select->expects($this->exactly(1))->method('where') ->with('entity_id IN(?)', [1]) ->willReturnSelf(); - $select->expects($this->once())->method('order')->with('entity_id')->willReturnSelf(); + $select->expects($this->once())->method('order')->with('qty')->willReturnSelf(); $this->connectionMock->expects($this->once()) ->method('fetchAll') ->with($select) @@ -379,4 +377,20 @@ public function testAddTierPriceData() $this->assertSame($this->collection, $this->collection->addTierPriceData()); } + + /** + * Test for getNewEmptyItem() method + * + * @return void + */ + public function testGetNewEmptyItem() + { + $item = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) + ->disableOriginalConstructor() + ->getMock(); + $this->entityFactory->expects($this->once())->method('create')->willReturn($item); + $firstItem = $this->collection->getNewEmptyItem(); + $secondItem = $this->collection->getNewEmptyItem(); + $this->assertEquals($firstItem, $secondItem); + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/GalleryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/GalleryTest.php index dfed4e4f3738..47ef3c999125 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/GalleryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/GalleryTest.php @@ -281,6 +281,9 @@ public function testBindValueToEntityRecordExists() $this->resource->bindValueToEntity($valueId, $entityId); } + /** + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ public function testLoadGallery() { $productId = 5; @@ -329,7 +332,8 @@ public function testLoadGallery() 'main.value_id = entity.value_id', ['entity_id'] )->willReturnSelf(); - $this->product->expects($this->at(0))->method('getData')->with('entity_id')->willReturn($productId); + $this->product->expects($this->at(0))->method('getData') + ->with('entity_id')->willReturn($productId); $this->product->expects($this->at(1))->method('getStoreId')->will($this->returnValue($storeId)); $this->connection->expects($this->exactly(2))->method('quoteInto')->withConsecutive( ['value.store_id = ?'], @@ -338,26 +342,50 @@ public function testLoadGallery() 'value.store_id = ' . $storeId, 'default_value.store_id = ' . 0 ); + $this->connection->expects($this->any())->method('getIfNullSql')->will( + $this->returnValueMap([ + [ + '`value`.`label`', + '`default_value`.`label`', + 'IFNULL(`value`.`label`, `default_value`.`label`)' + ], + [ + '`value`.`position`', + '`default_value`.`position`', + 'IFNULL(`value`.`position`, `default_value`.`position`)' + ], + [ + '`value`.`disabled`', + '`default_value`.`disabled`', + 'IFNULL(`value`.`disabled`, `default_value`.`disabled`)' + ] + ]) + ); $this->select->expects($this->at(2))->method('joinLeft')->with( ['value' => $getTableReturnValue], $quoteInfoReturnValue, - [ - 'label', - 'position', - 'disabled' - ] + [] )->willReturnSelf(); $this->select->expects($this->at(3))->method('joinLeft')->with( ['default_value' => $getTableReturnValue], $quoteDefaultInfoReturnValue, - ['label_default' => 'label', 'position_default' => 'position', 'disabled_default' => 'disabled'] + [] )->willReturnSelf(); - $this->select->expects($this->at(4))->method('where')->with( + $this->select->expects($this->at(4))->method('columns')->with([ + 'label' => 'IFNULL(`value`.`label`, `default_value`.`label`)', + 'position' => 'IFNULL(`value`.`position`, `default_value`.`position`)', + 'disabled' => 'IFNULL(`value`.`disabled`, `default_value`.`disabled`)', + 'label_default' => 'default_value.label', + 'position_default' => 'default_value.position', + 'disabled_default' => 'default_value.disabled' + ])->willReturnSelf(); + $this->select->expects($this->at(5))->method('where')->with( 'main.attribute_id = ?', $attributeId )->willReturnSelf(); - $this->select->expects($this->at(5))->method('where')->with('main.disabled = 0')->willReturnSelf(); - $this->select->expects($this->at(7))->method('where') + $this->select->expects($this->at(6))->method('where') + ->with('main.disabled = 0')->willReturnSelf(); + $this->select->expects($this->at(8))->method('where') ->with('entity.entity_id = ?', $productId) ->willReturnSelf(); $this->select->expects($this->once())->method('order') diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/ImageTest.php new file mode 100644 index 000000000000..4fce12dc2de8 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/ImageTest.php @@ -0,0 +1,237 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Model\ResourceModel\Product; + +use Magento\Catalog\Model\ResourceModel\Product\Image; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Query\Generator; +use Magento\Framework\DB\Select; +use Magento\Framework\App\ResourceConnection; +use Magento\Catalog\Model\ResourceModel\Product\Gallery; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +use Magento\Framework\DB\Query\BatchIteratorInterface; + +class ImageTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + protected $objectManager; + + /** + * @var AdapterInterface | MockObject + */ + protected $connectionMock; + + /** + * @var Generator | MockObject + */ + protected $generatorMock; + + /** + * @var ResourceConnection | MockObject + */ + protected $resourceMock; + + protected function setUp(): void + { + $this->objectManager = + new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->connectionMock = $this->createMock(AdapterInterface::class); + $this->resourceMock = $this->createMock(ResourceConnection::class); + $this->resourceMock->method('getConnection') + ->willReturn($this->connectionMock); + $this->resourceMock->method('getTableName') + ->willReturnArgument(0); + $this->generatorMock = $this->createMock(Generator::class); + } + + /** + * @return MockObject + */ + protected function getVisibleImagesSelectMock(): MockObject + { + $selectMock = $this->getMockBuilder(Select::class) + ->disableOriginalConstructor() + ->getMock(); + $selectMock->expects($this->once()) + ->method('distinct') + ->willReturnSelf(); + $selectMock->expects($this->once()) + ->method('from') + ->with( + ['images' => Gallery::GALLERY_TABLE], + 'value as filepath' + )->willReturnSelf(); + $selectMock->expects($this->once()) + ->method('where') + ->with('disabled = 0') + ->willReturnSelf(); + + return $selectMock; + } + + /** + * @param int $imagesCount + * @dataProvider dataProvider + */ + public function testGetCountAllProductImages(int $imagesCount): void + { + $selectMock = $this->getVisibleImagesSelectMock(); + $selectMock->expects($this->exactly(2)) + ->method('reset') + ->withConsecutive( + ['columns'], + ['distinct'] + )->willReturnSelf(); + $selectMock->expects($this->once()) + ->method('columns') + ->with(new \Zend_Db_Expr('count(distinct value)')) + ->willReturnSelf(); + + $this->connectionMock->expects($this->once()) + ->method('select') + ->willReturn($selectMock); + $this->connectionMock->expects($this->once()) + ->method('fetchOne') + ->with($selectMock) + ->willReturn($imagesCount); + + $imageModel = $this->objectManager->getObject( + Image::class, + [ + 'generator' => $this->generatorMock, + 'resourceConnection' => $this->resourceMock + ] + ); + + $this->assertSame( + $imagesCount, + $imageModel->getCountAllProductImages() + ); + } + + /** + * @param int $imagesCount + * @param int $batchSize + * @dataProvider dataProvider + */ + public function testGetAllProductImages( + int $imagesCount, + int $batchSize + ): void { + $this->connectionMock->expects($this->once()) + ->method('select') + ->willReturn($this->getVisibleImagesSelectMock()); + + $batchCount = (int)ceil($imagesCount / $batchSize); + $fetchResultsCallback = $this->getFetchResultCallbackForBatches($imagesCount, $batchSize); + $this->connectionMock->expects($this->exactly($batchCount)) + ->method('fetchAll') + ->will($this->returnCallback($fetchResultsCallback)); + + /** @var Select | MockObject $selectMock */ + $selectMock = $this->getMockBuilder(Select::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->generatorMock->expects($this->once()) + ->method('generate') + ->with( + 'value_id', + $selectMock, + $batchSize, + BatchIteratorInterface::NON_UNIQUE_FIELD_ITERATOR + )->will( + $this->returnCallback( + $this->getBatchIteratorCallback($selectMock, $batchCount) + ) + ); + + $imageModel = $this->objectManager->getObject( + Image::class, + [ + 'generator' => $this->generatorMock, + 'resourceConnection' => $this->resourceMock, + 'batchSize' => $batchSize + ] + ); + + $this->assertCount($imagesCount, $imageModel->getAllProductImages()); + } + + /** + * @param int $imagesCount + * @param int $batchSize + * @return \Closure + */ + protected function getFetchResultCallbackForBatches( + int $imagesCount, + int $batchSize + ): \Closure { + $fetchResultsCallback = function () use (&$imagesCount, $batchSize) { + $batchSize = + ($imagesCount >= $batchSize) ? $batchSize : $imagesCount; + $imagesCount -= $batchSize; + + $getFetchResults = function ($batchSize): array { + $result = []; + $count = $batchSize; + while ($count) { + $count--; + $result[$count] = $count; + } + + return $result; + }; + + return $getFetchResults($batchSize); + }; + + return $fetchResultsCallback; + } + + /** + * @param Select | MockObject $selectMock + * @param int $batchCount + * @return \Closure + */ + protected function getBatchIteratorCallback( + MockObject $selectMock, + int $batchCount + ): \Closure { + $iteratorCallback = function () use ($batchCount, $selectMock): array { + $result = []; + $count = $batchCount; + while ($count) { + $count--; + $result[$count] = $selectMock; + } + + return $result; + }; + + return $iteratorCallback; + } + + /** + * Data Provider + * @return array + */ + public function dataProvider(): array + { + return [ + [300, 300], + [300, 100], + [139, 100], + [67, 10], + [154, 47], + [0, 100] + ]; + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Indexer/Price/CompositeProductRowSizeEstimatorTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Indexer/Price/CompositeProductRowSizeEstimatorTest.php index 1c4764433814..728044b89caf 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Indexer/Price/CompositeProductRowSizeEstimatorTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Indexer/Price/CompositeProductRowSizeEstimatorTest.php @@ -49,7 +49,6 @@ protected function setUp() public function testEstimateRowSize() { - $this->markTestSkipped('Unskip after MAGETWO-89738'); $expectedResult = 40000000; $maxRelatedProductCount = 10; diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/StatusBaseSelectProcessorTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/StatusBaseSelectProcessorTest.php index a21883eb4a18..ee487041600b 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/StatusBaseSelectProcessorTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/StatusBaseSelectProcessorTest.php @@ -16,7 +16,7 @@ use Magento\Framework\EntityManager\EntityMetadataInterface; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\Store\Api\StoreResolverInterface; +use Magento\Store\Model\StoreManagerInterface; use Magento\Store\Model\Store; /** @@ -35,9 +35,9 @@ class StatusBaseSelectProcessorTest extends \PHPUnit\Framework\TestCase private $metadataPool; /** - * @var StoreResolverInterface|\PHPUnit_Framework_MockObject_MockObject + * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - private $storeResolver; + private $storeManager; /** * @var Select|\PHPUnit_Framework_MockObject_MockObject @@ -53,13 +53,13 @@ protected function setUp() { $this->eavConfig = $this->getMockBuilder(Config::class)->disableOriginalConstructor()->getMock(); $this->metadataPool = $this->getMockBuilder(MetadataPool::class)->disableOriginalConstructor()->getMock(); - $this->storeResolver = $this->getMockBuilder(StoreResolverInterface::class)->getMock(); + $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class)->getMock(); $this->select = $this->getMockBuilder(Select::class)->disableOriginalConstructor()->getMock(); $this->statusBaseSelectProcessor = (new ObjectManager($this))->getObject(StatusBaseSelectProcessor::class, [ 'eavConfig' => $this->eavConfig, 'metadataPool' => $this->metadataPool, - 'storeResolver' => $this->storeResolver, + 'storeManager' => $this->storeManager, ]); } @@ -94,8 +94,14 @@ public function testProcess() ->with(Product::ENTITY, ProductInterface::STATUS) ->willReturn($statusAttribute); - $this->storeResolver->expects($this->once()) - ->method('getCurrentStoreId') + $storeMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class)->getMock(); + + $this->storeManager->expects($this->once()) + ->method('getStore') + ->willReturn($storeMock); + + $storeMock->expects($this->once()) + ->method('getId') ->willReturn($currentStoreId); $this->select->expects($this->at(0)) diff --git a/app/code/Magento/Catalog/Test/Unit/Model/_files/converted_view.php b/app/code/Magento/Catalog/Test/Unit/Model/_files/converted_view.php index 37b0e15cac65..e225ec0daef6 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/_files/converted_view.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/_files/converted_view.php @@ -11,7 +11,41 @@ "type" => "swatch_thumb", "width" => 75, "height" => 75, - "background" => [255, 25, 2] + "constrain" => false, + "aspect_ratio" => false, + "frame" => false, + "transparency" => false, + "background" => [255, 25, 2], + ], + "swatch_thumb_medium" => [ + "type" => "swatch_medium", + "width" => 750, + "height" => 750, + "constrain" => true, + "aspect_ratio" => true, + "frame" => true, + "transparency" => true, + "background" => [255, 25, 2], + ], + "swatch_thumb_large" => [ + "type" => "swatch_large", + "width" => 1080, + "height" => 720, + "constrain" => false, + "aspect_ratio" => false, + "frame" => false, + "transparency" => false, + "background" => [255, 25, 2], + ], + "swatch_thumb_small" => [ + "type" => "swatch_small", + "width" => 100, + "height" => 100, + "constrain" => true, + "aspect_ratio" => true, + "frame" => true, + "transparency" => true, + "background" => [255, 25, 2], ] ] ] diff --git a/app/code/Magento/Catalog/Test/Unit/Model/_files/valid_view.xml b/app/code/Magento/Catalog/Test/Unit/Model/_files/valid_view.xml index 253abc5e2e48..ee4ddaad5342 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/_files/valid_view.xml +++ b/app/code/Magento/Catalog/Test/Unit/Model/_files/valid_view.xml @@ -11,6 +11,37 @@ <image id="swatch_thumb_base" type="swatch_thumb"> <width>75</width> <height>75</height> + <constrain>false</constrain> + <aspect_ratio>false</aspect_ratio> + <frame>false</frame> + <transparency>false</transparency> + <background>[255, 25, 2]</background> + </image> + <image id="swatch_thumb_medium" type="swatch_medium"> + <width>750</width> + <height>750</height> + <constrain>true</constrain> + <aspect_ratio>true</aspect_ratio> + <frame>true</frame> + <transparency>true</transparency> + <background>[255, 25, 2]</background> + </image> + <image id="swatch_thumb_large" type="swatch_large"> + <width>1080</width> + <height>720</height> + <constrain>0</constrain> + <aspect_ratio>0</aspect_ratio> + <frame>0</frame> + <transparency>0</transparency> + <background>[255, 25, 2]</background> + </image> + <image id="swatch_thumb_small" type="swatch_small"> + <width>100</width> + <height>100</height> + <constrain>1</constrain> + <aspect_ratio>1</aspect_ratio> + <frame>1</frame> + <transparency>1</transparency> <background>[255, 25, 2]</background> </image> </images> diff --git a/app/code/Magento/Catalog/Test/Unit/Plugin/Model/Attribute/Backend/AttributeValidationTest.php b/app/code/Magento/Catalog/Test/Unit/Plugin/Model/Attribute/Backend/AttributeValidationTest.php new file mode 100644 index 000000000000..463ecf881977 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Plugin/Model/Attribute/Backend/AttributeValidationTest.php @@ -0,0 +1,154 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Plugin\Model\Attribute\Backend; + +use Magento\Catalog\Plugin\Model\Attribute\Backend\AttributeValidation; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend; +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Framework\DataObject; + +class AttributeValidationTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var AttributeValidation + */ + private $attributeValidation; + + /** + * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeManagerMock; + + /** + * @var StoreInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeMock; + + /** + * @var array + */ + private $allowedEntityTypes; + + /** + * @var \Callable + */ + private $proceedMock; + + /** + * @var bool + */ + private $isProceedMockCalled = false; + + /** + * @var AbstractBackend|\PHPUnit_Framework_MockObject_MockObject + */ + private $subjectMock; + + /** + * @var AbstractAttribute|\PHPUnit_Framework_MockObject_MockObject + */ + private $attributeMock; + + /** + * @var DataObject|\PHPUnit_Framework_MockObject_MockObject + */ + private $entityMock; + + protected function setUp() + { + $objectManager = new ObjectManager($this); + + $this->attributeMock = $this->getMockBuilder(AbstractBackend::class) + ->setMethods(['getAttributeCode']) + ->getMockForAbstractClass(); + $this->subjectMock = $this->getMockBuilder(AbstractBackend::class) + ->setMethods(['getAttribute']) + ->getMockForAbstractClass(); + $this->subjectMock->expects($this->any()) + ->method('getAttribute') + ->willReturn($this->attributeMock); + + $this->storeMock = $this->getMockBuilder(StoreInterface::class) + ->setMethods(['getId']) + ->getMockForAbstractClass(); + $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class) + ->setMethods(['getStore']) + ->getMockForAbstractClass(); + $this->storeManagerMock->expects($this->any()) + ->method('getStore') + ->willReturn($this->storeMock); + + $this->entityMock = $this->getMockBuilder(DataObject::class) + ->setMethods(['getData']) + ->getMock(); + + $this->allowedEntityTypes = [$this->entityMock]; + + $this->proceedMock = function () { + $this->isProceedMockCalled = true; + }; + + $this->attributeValidation = $objectManager->getObject( + AttributeValidation::class, + [ + 'storeManager' => $this->storeManagerMock, + 'allowedEntityTypes' => $this->allowedEntityTypes, + ] + ); + } + + /** + * @param bool $shouldProceedRun + * @param bool $defaultStoreUsed + * @param null|int|string $storeId + * @dataProvider aroundValidateDataProvider + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @return void + */ + public function testAroundValidate(bool $shouldProceedRun, bool $defaultStoreUsed, $storeId) + { + $this->isProceedMockCalled = false; + $attributeCode = 'code'; + + $this->storeMock->expects($this->once()) + ->method('getId') + ->willReturn($storeId); + if ($defaultStoreUsed) { + $this->attributeMock->expects($this->once()) + ->method('getAttributeCode') + ->willReturn($attributeCode); + $this->entityMock->expects($this->at(0)) + ->method('getData') + ->willReturn([$attributeCode => null]); + $this->entityMock->expects($this->at(1)) + ->method('getData') + ->with($attributeCode) + ->willReturn(null); + } + + $this->attributeValidation->aroundValidate($this->subjectMock, $this->proceedMock, $this->entityMock); + $this->assertSame($shouldProceedRun, $this->isProceedMockCalled); + } + + /** + * Data provider for testAroundValidate + * @return array + */ + public function aroundValidateDataProvider(): array + { + return [ + [true, false, '0'], + [true, false, 0], + [true, false, null], + [false, true, 1], + ]; + } +} 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 000000000000..774edcfeb6b6 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Ui/Component/ColumnFactoryTest.php @@ -0,0 +1,156 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Ui\Component; + +use PHPUnit\Framework\TestCase; +use Magento\Catalog\Ui\Component\ColumnFactory; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Ui\Component\Listing\Columns\ColumnInterface; +use Magento\Ui\Component\Filters\FilterModifier; + +/** + * ColumnFactory test. + */ +class ColumnFactoryTest extends TestCase +{ + /** + * @var ColumnFactory + */ + private $columnFactory; + + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var ProductAttributeInterface|\PHPUnit\Framework\MockObject\MockObject + */ + private $attribute; + + /** + * @var ContextInterface|\PHPUnit\Framework\MockObject\MockObject + */ + private $context; + + /** + * @var UiComponentFactory|\PHPUnit\Framework\MockObject\MockObject + */ + private $uiComponentFactory; + + /** + * @var ColumnInterface|\PHPUnit\Framework\MockObject\MockObject + */ + private $column; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManager = new ObjectManager($this); + + $this->attribute = $this->getMockBuilder(ProductAttributeInterface::class) + ->setMethods(['usesSource']) + ->getMockForAbstractClass(); + $this->context = $this->createMock(ContextInterface::class); + $this->uiComponentFactory = $this->createMock(UiComponentFactory::class); + $this->column = $this->getMockForAbstractClass(ColumnInterface::class); + $this->uiComponentFactory->method('create') + ->willReturn($this->column); + + $this->columnFactory = $this->objectManager->getObject(ColumnFactory::class, [ + 'componentFactory' => $this->uiComponentFactory + ]); + } + + /** + * Tests the create method will return correct object. + * + * @return void + */ + public function testCreatedObject(): void + { + $this->context->method('getRequestParam') + ->with(FilterModifier::FILTER_MODIFIER, []) + ->willReturn([]); + + $object = $this->columnFactory->create($this->attribute, $this->context); + $this->assertEquals( + $this->column, + $object, + 'Object must be the same which the ui component factory creates.' + ); + } + + /** + * Tests create method with not filterable in grid attribute. + * + * @param array $filterModifiers + * @param null|string $filter + * + * @return void + * @dataProvider filterModifiersProvider + */ + public function testCreateWithNotFilterableInGridAttribute(array $filterModifiers, ?string $filter): void + { + $componentFactoryArgument = [ + 'data' => [ + 'config' => [ + 'label' => __(null), + 'dataType' => 'text', + 'add_field' => true, + 'visible' => null, + 'filter' => $filter, + 'component' => 'Magento_Ui/js/grid/columns/column', + ], + ], + 'context' => $this->context, + ]; + + $this->context->method('getRequestParam') + ->with(FilterModifier::FILTER_MODIFIER, []) + ->willReturn($filterModifiers); + $this->attribute->method('getIsFilterableInGrid') + ->willReturn(false); + $this->attribute->method('getAttributeCode') + ->willReturn('color'); + + $this->uiComponentFactory->expects($this->once()) + ->method('create') + ->with($this->anything(), $this->anything(), $componentFactoryArgument); + + $this->columnFactory->create($this->attribute, $this->context); + } + + /** + * Filter modifiers data provider. + * + * @return array + */ + public function filterModifiersProvider(): array + { + return [ + 'without' => [ + 'filter_modifiers' => [], + 'filter' => null, + ], + 'with' => [ + 'filter_modifiers' => [ + 'color' => [ + 'condition_type' => 'notnull', + ], + ], + 'filter' => 'text', + ], + ]; + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php index cd6565f32ed1..a2d81854607a 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php @@ -154,38 +154,4 @@ public function modifyMetaLockedDataProvider() { return [[true], [false]]; } - - public function testModifyMetaWithCaching() - { - $this->arrayManagerMock->expects($this->exactly(2)) - ->method('findPath') - ->willReturn(true); - $cacheManager = $this->getMockBuilder(CacheInterface::class) - ->getMockForAbstractClass(); - $cacheManager->expects($this->once()) - ->method('load') - ->with(Categories::CATEGORY_TREE_ID . '_'); - $cacheManager->expects($this->once()) - ->method('save'); - - $modifier = $this->createModel(); - $cacheContextProperty = new \ReflectionProperty( - Categories::class, - 'cacheManager' - ); - $cacheContextProperty->setAccessible(true); - $cacheContextProperty->setValue($modifier, $cacheManager); - - $groupCode = 'test_group_code'; - $meta = [ - $groupCode => [ - 'children' => [ - 'category_ids' => [ - 'sortOrder' => 10, - ], - ], - ], - ]; - $modifier->modifyMeta($meta); - } } diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/WebsitesTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/WebsitesTest.php index c3096770729a..829dc4824416 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/WebsitesTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/WebsitesTest.php @@ -74,6 +74,9 @@ class WebsitesTest extends AbstractModifierTest */ protected $storeViewMock; + /** + * @inheritdoc + */ protected function setUp() { parent::setUp(); @@ -90,14 +93,11 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); $this->websiteRepositoryMock = $this->getMockBuilder(\Magento\Store\Api\WebsiteRepositoryInterface::class) - ->setMethods(['getList', 'getDefault']) + ->setMethods(['getList']) ->getMockForAbstractClass(); $this->websiteRepositoryMock->expects($this->any()) ->method('getDefault') ->willReturn($this->websiteMock); - $this->websiteRepositoryMock->expects($this->any()) - ->method('getList') - ->willReturn([$this->websiteMock, $this->secondWebsiteMock]); $this->groupRepositoryMock = $this->getMockBuilder(\Magento\Store\Api\GroupRepositoryInterface::class) ->setMethods(['getList']) ->getMockForAbstractClass(); @@ -111,8 +111,10 @@ protected function setUp() ->method('getWebsiteIds') ->willReturn($this->assignedWebsites); $this->storeManagerMock = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) - ->setMethods(['isSingleStoreMode']) + ->setMethods(['isSingleStoreMode', 'getWesites']) ->getMockForAbstractClass(); + $this->storeManagerMock->method('getWebsites') + ->willReturn([$this->websiteMock, $this->secondWebsiteMock]); $this->storeManagerMock->expects($this->any()) ->method('isSingleStoreMode') ->willReturn(false); diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Listing/Collector/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Listing/Collector/ImageTest.php index 12bc9acfa4c5..009cd690d4cd 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Listing/Collector/ImageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Listing/Collector/ImageTest.php @@ -15,6 +15,7 @@ use Magento\Catalog\Helper\ImageFactory; use Magento\Catalog\Api\Data\ProductRender\ImageInterface; use Magento\Catalog\Helper\Image as ImageHelper; +use Magento\Framework\View\DesignLoader; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -33,6 +34,9 @@ class ImageTest extends \PHPUnit\Framework\TestCase /** @var DesignInterface | \PHPUnit_Framework_MockObject_MockObject */ private $design; + /** @var DesignLoader | \PHPUnit_Framework_MockObject_MockObject*/ + private $designLoader; + /** @var Image */ private $model; @@ -60,13 +64,15 @@ public function setUp() ->getMock(); $this->storeManager = $this->createMock(StoreManagerInterface::class); $this->design = $this->createMock(DesignInterface::class); + $this->designLoader = $this->createMock(DesignLoader::class); $this->model = new Image( $this->imageFactory, $this->state, $this->storeManager, $this->design, $this->imageInterfaceFactory, - $this->imageCodes + $this->imageCodes, + $this->designLoader ); } 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 6d7c8814bd47..0e0cb676cdf3 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/ProductCustomOptionsDataProviderTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/ProductCustomOptionsDataProviderTest.php @@ -54,7 +54,16 @@ protected function setUp() ->getMockForAbstractClass(); $this->collectionMock = $this->getMockBuilder(AbstractCollection::class) ->disableOriginalConstructor() - ->setMethods(['load', 'getSelect', 'getTable', 'getIterator', 'isLoaded', 'toArray', 'getSize']) + ->setMethods([ + 'load', + 'getSelect', + 'getTable', + 'getIterator', + 'isLoaded', + 'toArray', + 'getSize', + 'setStoreId' + ]) ->getMockForAbstractClass(); $this->dbSelectMock = $this->getMockBuilder(DbSelect::class) ->disableOriginalConstructor() diff --git a/app/code/Magento/Catalog/Test/Unit/ViewModel/Product/BreadcrumbsTest.php b/app/code/Magento/Catalog/Test/Unit/ViewModel/Product/BreadcrumbsTest.php index dbf1292e5736..a4ccaffc8fb6 100644 --- a/app/code/Magento/Catalog/Test/Unit/ViewModel/Product/BreadcrumbsTest.php +++ b/app/code/Magento/Catalog/Test/Unit/ViewModel/Product/BreadcrumbsTest.php @@ -152,21 +152,21 @@ public function productJsonEncodeDataProvider() : array return [ [ $this->getObjectManager()->getObject(Product::class, ['data' => ['name' => 'Test ™']]), - '{"breadcrumbs":{"categoryUrlSuffix":"."html","userCategoryPathInUrl":0,"product":"Test \u2122"}}', + '{"breadcrumbs":{"categoryUrlSuffix":"."html","useCategoryPathInUrl":0,"product":"Test \u2122"}}', ], [ $this->getObjectManager()->getObject(Product::class, ['data' => ['name' => 'Test "']]), - '{"breadcrumbs":{"categoryUrlSuffix":"."html","userCategoryPathInUrl":0,"product":"Test ""}}', + '{"breadcrumbs":{"categoryUrlSuffix":"."html","useCategoryPathInUrl":0,"product":"Test ""}}', ], [ $this->getObjectManager()->getObject(Product::class, ['data' => ['name' => 'Test <b>x</b>']]), - '{"breadcrumbs":{"categoryUrlSuffix":"."html","userCategoryPathInUrl":0,"product":' + '{"breadcrumbs":{"categoryUrlSuffix":"."html","useCategoryPathInUrl":0,"product":' . '"Test <b>x<\/b>"}}', ], [ $this->getObjectManager()->getObject(Product::class, ['data' => ['name' => 'Test \'abc\'']]), '{"breadcrumbs":' - . '{"categoryUrlSuffix":"."html","userCategoryPathInUrl":0,"product":"Test 'abc'"}}' + . '{"categoryUrlSuffix":"."html","useCategoryPathInUrl":0,"product":"Test 'abc'"}}' ], ]; } diff --git a/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php b/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php index cbc67fee8a5a..ea6b1fd47a0a 100644 --- a/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php +++ b/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php @@ -5,7 +5,11 @@ */ namespace Magento\Catalog\Ui\Component; +use Magento\Ui\Component\Filters\FilterModifier; + /** + * Column Factory + * * @api * @since 100.0.2 */ @@ -47,20 +51,26 @@ public function __construct(\Magento\Framework\View\Element\UiComponentFactory $ } /** + * Create Factory + * * @param \Magento\Catalog\Api\Data\ProductAttributeInterface $attribute * @param \Magento\Framework\View\Element\UiComponent\ContextInterface $context * @param array $config + * * @return \Magento\Ui\Component\Listing\Columns\ColumnInterface + * @throws \Magento\Framework\Exception\LocalizedException */ 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); @@ -82,7 +92,10 @@ public function create($attribute, $context, array $config = []) } /** + * Get Js Component + * * @param string $dataType + * * @return string */ protected function getJsComponent($dataType) @@ -91,14 +104,15 @@ protected function getJsComponent($dataType) } /** + * Get Data Type + * * @param \Magento\Catalog\Api\Data\ProductAttributeInterface $attribute + * * @return string */ protected function getDataType($attribute) { - return isset($this->dataTypeMap[$attribute->getFrontendInput()]) - ? $this->dataTypeMap[$attribute->getFrontendInput()] - : $this->dataTypeMap['default']; + return $this->dataTypeMap[$attribute->getFrontendInput()] ?? $this->dataTypeMap['default']; } /** @@ -111,6 +125,6 @@ protected function getFilterType($frontendInput) { $filtersMap = ['date' => 'dateRange']; $result = array_replace_recursive($this->dataTypeMap, $filtersMap); - return isset($result[$frontendInput]) ? $result[$frontendInput] : $result['default']; + return $result[$frontendInput] ?? $result['default']; } } diff --git a/app/code/Magento/Catalog/Ui/Component/FilterFactory.php b/app/code/Magento/Catalog/Ui/Component/FilterFactory.php index fcc500c89160..dd8eaffb0a65 100644 --- a/app/code/Magento/Catalog/Ui/Component/FilterFactory.php +++ b/app/code/Magento/Catalog/Ui/Component/FilterFactory.php @@ -71,8 +71,6 @@ public function create($attribute, $context, $config = []) */ protected function getFilterType($attribute) { - return isset($this->filterMap[$attribute->getFrontendInput()]) - ? $this->filterMap[$attribute->getFrontendInput()] - : $this->filterMap['default']; + return $this->filterMap[$attribute->getFrontendInput()] ?? $this->filterMap['default']; } } diff --git a/app/code/Magento/Catalog/Ui/Component/Listing/Columns.php b/app/code/Magento/Catalog/Ui/Component/Listing/Columns.php index c96498b054d2..8ea6d8b9e5a0 100644 --- a/app/code/Magento/Catalog/Ui/Component/Listing/Columns.php +++ b/app/code/Magento/Catalog/Ui/Component/Listing/Columns.php @@ -80,6 +80,6 @@ public function prepare() */ protected function getFilterType($frontendInput) { - return isset($this->filterMap[$frontendInput]) ? $this->filterMap[$frontendInput] : $this->filterMap['default']; + return $this->filterMap[$frontendInput] ?? $this->filterMap['default']; } } diff --git a/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Thumbnail.php b/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Thumbnail.php index d4dc9ddd7ca3..09c9782fc0e3 100644 --- a/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Thumbnail.php +++ b/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Thumbnail.php @@ -9,6 +9,8 @@ use Magento\Framework\View\Element\UiComponent\ContextInterface; /** + * Class Thumbnail + * * @api * @since 100.0.2 */ @@ -67,6 +69,8 @@ public function prepareDataSource(array $dataSource) } /** + * Get Alt + * * @param array $row * * @return null|string @@ -74,6 +78,6 @@ public function prepareDataSource(array $dataSource) protected function getAlt($row) { $altField = $this->getData('config/altField') ?: self::ALT_FIELD; - return isset($row[$altField]) ? $row[$altField] : null; + return $row[$altField] ?? null; } } diff --git a/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Websites.php b/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Websites.php index 5af0d71dc246..494b77724e5b 100644 --- a/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Websites.php +++ b/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Websites.php @@ -3,13 +3,19 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Catalog\Ui\Component\Listing\Columns; -use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Framework\DB\Helper; use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; use Magento\Store\Model\StoreManagerInterface; /** + * Websites listing column component. + * * @api * @since 100.0.2 */ @@ -20,6 +26,11 @@ class Websites extends \Magento\Ui\Component\Listing\Columns\Column */ const NAME = 'websites'; + /** + * Data for concatenated website names value. + */ + private $websiteNames = 'website_names'; + /** * Store manager * @@ -27,26 +38,36 @@ class Websites extends \Magento\Ui\Component\Listing\Columns\Column */ protected $storeManager; + /** + * @var \Magento\Framework\DB\Helper + */ + private $resourceHelper; + /** * @param ContextInterface $context * @param UiComponentFactory $uiComponentFactory * @param StoreManagerInterface $storeManager * @param array $components * @param array $data + * @param Helper $resourceHelper */ public function __construct( ContextInterface $context, UiComponentFactory $uiComponentFactory, StoreManagerInterface $storeManager, array $components = [], - array $data = [] + array $data = [], + Helper $resourceHelper = null ) { parent::__construct($context, $uiComponentFactory, $components, $data); + $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); $this->storeManager = $storeManager; + $this->resourceHelper = $resourceHelper ?: $objectManager->get(Helper::class); } /** - * {@inheritdoc} + * @inheritdoc + * * @deprecated 101.0.0 */ public function prepareDataSource(array $dataSource) @@ -71,9 +92,10 @@ public function prepareDataSource(array $dataSource) return $dataSource; } - + /** - * Prepare component configuration + * Prepare component configuration. + * * @return void */ public function prepare() @@ -83,4 +105,46 @@ public function prepare() $this->_data['config']['componentDisabled'] = true; } } + + /** + * Apply sorting. + * + * @return void + */ + protected function applySorting() + { + $sorting = $this->getContext()->getRequestParam('sorting'); + $isSortable = $this->getData('config/sortable'); + if ($isSortable !== false + && !empty($sorting['field']) + && !empty($sorting['direction']) + && $sorting['field'] === $this->getName() + ) { + $collection = $this->getContext()->getDataProvider()->getCollection(); + $collection + ->joinField( + 'websites_ids', + 'catalog_product_website', + 'website_id', + 'product_id=entity_id', + null, + 'left' + ) + ->joinTable( + 'store_website', + 'website_id = websites_ids', + ['name'], + null, + 'left' + ) + ->groupByAttribute('entity_id'); + $this->resourceHelper->addGroupConcatColumn( + $collection->getSelect(), + $this->websiteNames, + 'name' + ); + + $collection->getSelect()->order($this->websiteNames . ' ' . $sorting['direction']); + } + } } diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php index 336aeffa1058..00132c6ad89e 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php @@ -139,7 +139,8 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc + * * @since 101.0.0 */ public function modifyMeta(array $meta) @@ -158,7 +159,8 @@ public function modifyMeta(array $meta) } /** - * {@inheritdoc} + * @inheritdoc + * * @since 101.0.0 */ public function modifyData(array $data) @@ -381,11 +383,15 @@ private function addAdvancedPriceLink() ); $advancedPricingButton['arguments']['data']['config'] = [ + 'dataScope' => 'advanced_pricing_button', 'displayAsLink' => true, 'formElement' => Container::NAME, 'componentType' => Container::NAME, 'component' => 'Magento_Ui/js/form/components/button', 'template' => 'ui/form/components/button/container', + 'imports' => [ + 'childError' => $this->scopeName . '.advanced_pricing_modal.advanced-pricing:error', + ], 'actions' => [ [ 'targetName' => $this->scopeName . '.advanced_pricing_modal', diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php index ed737df708ab..800ead0e4030 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Ui\DataProvider\Product\Form\Modifier; use Magento\Catalog\Model\Locator\LocatorInterface; @@ -11,6 +13,7 @@ use Magento\Framework\App\CacheInterface; use Magento\Framework\DB\Helper as DbHelper; use Magento\Catalog\Model\Category as CategoryModel; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Serialize\SerializerInterface; use Magento\Framework\UrlInterface; use Magento\Framework\Stdlib\ArrayManager; @@ -118,7 +121,7 @@ private function getCacheManager() } /** - * {@inheritdoc} + * @inheritdoc * @since 101.0.0 */ public function modifyMeta(array $meta) @@ -130,7 +133,7 @@ public function modifyMeta(array $meta) } /** - * {@inheritdoc} + * @inheritdoc * @since 101.0.0 */ public function modifyData(array $data) @@ -202,6 +205,7 @@ protected function createNewCategoryModal(array $meta) * * @param array $meta * @return array + * @throws LocalizedException * @since 101.0.0 */ protected function customizeCategoriesField(array $meta) @@ -289,6 +293,7 @@ protected function customizeCategoriesField(array $meta) 'source' => 'product_details', 'displayArea' => 'insideGroup', 'sortOrder' => 20, + 'dataScope' => $fieldCode, ], ], ] @@ -305,20 +310,64 @@ protected function customizeCategoriesField(array $meta) * * @param string|null $filter * @return array + * @throws LocalizedException * @since 101.0.0 */ protected function getCategoriesTree($filter = null) { - $categoryTree = $this->getCacheManager()->load(self::CATEGORY_TREE_ID . '_' . $filter); - if ($categoryTree) { - return $this->serializer->unserialize($categoryTree); + $storeId = (int) $this->locator->getStore()->getId(); + + $cachedCategoriesTree = $this->getCacheManager() + ->load($this->getCategoriesTreeCacheId($storeId, (string) $filter)); + if (!empty($cachedCategoriesTree)) { + return $this->serializer->unserialize($cachedCategoriesTree); } - $storeId = $this->locator->getStore()->getId(); + $categoriesTree = $this->retrieveCategoriesTree( + $storeId, + $this->retrieveShownCategoriesIds($storeId, (string) $filter) + ); + + $this->getCacheManager()->save( + $this->serializer->serialize($categoriesTree), + $this->getCategoriesTreeCacheId($storeId, (string) $filter), + [ + \Magento\Catalog\Model\Category::CACHE_TAG, + \Magento\Framework\App\Cache\Type\Block::CACHE_TAG + ] + ); + + return $categoriesTree; + } + + /** + * Get cache id for categories tree. + * + * @param int $storeId + * @param string $filter + * @return string + */ + private function getCategoriesTreeCacheId(int $storeId, string $filter = '') : string + { + return self::CATEGORY_TREE_ID + . '_' . (string) $storeId + . '_' . $filter; + } + + /** + * Retrieve filtered list of categories id. + * + * @param int $storeId + * @param string $filter + * @return array + * @throws LocalizedException + */ + private function retrieveShownCategoriesIds(int $storeId, string $filter = '') : array + { /* @var $matchingNamesCollection \Magento\Catalog\Model\ResourceModel\Category\Collection */ $matchingNamesCollection = $this->categoryCollectionFactory->create(); - if ($filter !== null) { + if (!empty($filter)) { $matchingNamesCollection->addAttributeToFilter( 'name', ['like' => $this->dbHelper->addLikeEscape($filter, ['position' => 'any'])] @@ -338,6 +387,19 @@ protected function getCategoriesTree($filter = null) } } + return $shownCategoriesIds; + } + + /** + * Retrieve tree of categories with attributes. + * + * @param int $storeId + * @param array $shownCategoriesIds + * @return array|null + * @throws LocalizedException + */ + private function retrieveCategoriesTree(int $storeId, array $shownCategoriesIds) : ?array + { /* @var $collection \Magento\Catalog\Model\ResourceModel\Category\Collection */ $collection = $this->categoryCollectionFactory->create(); @@ -364,15 +426,6 @@ protected function getCategoriesTree($filter = null) $categoryById[$category->getParentId()]['optgroup'][] = &$categoryById[$category->getId()]; } - $this->getCacheManager()->save( - $this->serializer->serialize($categoryById[CategoryModel::TREE_ROOT_ID]['optgroup']), - self::CATEGORY_TREE_ID . '_' . $filter, - [ - \Magento\Catalog\Model\Category::CACHE_TAG, - \Magento\Framework\App\Cache\Type\Block::CACHE_TAG - ] - ); - return $categoryById[CategoryModel::TREE_ROOT_ID]['optgroup']; } } diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CustomOptions.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CustomOptions.php index 86f1db2022cc..af43c84501f6 100755 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CustomOptions.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CustomOptions.php @@ -11,6 +11,7 @@ use Magento\Catalog\Model\Config\Source\Product\Options\Price as ProductOptionsPrice; use Magento\Framework\UrlInterface; use Magento\Framework\Stdlib\ArrayManager; +use Magento\Ui\Component\Form\Element\Hidden; use Magento\Ui\Component\Modal; use Magento\Ui\Component\Container; use Magento\Ui\Component\DynamicRows; @@ -867,7 +868,7 @@ protected function getPositionFieldConfig($sortOrder) 'data' => [ 'config' => [ 'componentType' => Field::NAME, - 'formElement' => Input::NAME, + 'formElement' => Hidden::NAME, 'dataScope' => static::FIELD_SORT_ORDER_NAME, 'dataType' => Number::NAME, 'visible' => false, diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php old mode 100755 new mode 100644 index b60f1592633b..8326c3b53189 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php @@ -11,6 +11,7 @@ use Magento\Catalog\Model\Attribute\ScopeOverriddenValue; use Magento\Catalog\Model\Locator\LocatorInterface; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Type as ProductType; use Magento\Catalog\Model\ResourceModel\Eav\Attribute as EavAttribute; use Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory as EavAttributeFactory; use Magento\Catalog\Ui\DataProvider\CatalogEavValidationRules; @@ -290,7 +291,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc * @since 101.0.0 */ public function modifyMeta(array $meta) @@ -401,7 +402,7 @@ public function getContainerChildren(ProductAttributeInterface $attribute, $grou } /** - * {@inheritdoc} + * @inheritdoc * @since 101.0.0 */ public function modifyData(array $data) @@ -419,7 +420,7 @@ public function modifyData(array $data) foreach ($attributes as $attribute) { if (null !== ($attributeValue = $this->setupAttributeData($attribute))) { - if ($attribute->getFrontendInput() === 'price' && is_scalar($attributeValue)) { + if ($this->isPriceAttribute($attribute, $attributeValue)) { $attributeValue = $this->formatPrice($attributeValue); } $data[$productId][self::DATA_SOURCE_DEFAULT][$attribute->getAttributeCode()] = $attributeValue; @@ -430,6 +431,32 @@ public function modifyData(array $data) return $data; } + /** + * Obtain if given attribute is a price + * + * @param \Magento\Catalog\Api\Data\ProductAttributeInterface $attribute + * @param string|integer $attributeValue + * @return bool + */ + private function isPriceAttribute(ProductAttributeInterface $attribute, $attributeValue) + { + return $attribute->getFrontendInput() === 'price' + && is_scalar($attributeValue) + && !$this->isBundleSpecialPrice($attribute); + } + + /** + * Obtain if current product is bundle and given attribute is special_price + * + * @param \Magento\Catalog\Api\Data\ProductAttributeInterface $attribute + * @return bool + */ + private function isBundleSpecialPrice(ProductAttributeInterface $attribute) + { + return $this->locator->getProduct()->getTypeId() === ProductType::TYPE_BUNDLE + && $attribute->getAttributeCode() === ProductAttributeInterface::CODE_SPECIAL_PRICE; + } + /** * Resolve data persistence * @@ -532,8 +559,8 @@ private function getAttributes() /** * Loads attributes for specified groups at once * - * @param AttributeGroupInterface[] ...$groups - * @return @return ProductAttributeInterface[] + * @param AttributeGroupInterface[] $groups + * @return ProductAttributeInterface[] */ private function loadAttributesForGroups(array $groups) { @@ -649,7 +676,7 @@ public function setupAttributeMeta(ProductAttributeInterface $attribute, $groupC // TODO: Refactor to $attribute->getOptions() when MAGETWO-48289 is done $attributeModel = $this->getAttributeModel($attribute); if ($attributeModel->usesSource()) { - $options = $attributeModel->getSource()->getAllOptions(); + $options = $attributeModel->getSource()->getAllOptions(true, true); $meta = $this->arrayManager->merge($configPath, $meta, [ 'options' => $this->convertOptionsValueToString($options), ]); @@ -707,7 +734,8 @@ public function setupAttributeMeta(ProductAttributeInterface $attribute, $groupC } /** - * Returns attribute default value, based on db setting or setting in the system configuration + * Returns attribute default value, based on db setting or setting in the system configuration. + * * @param ProductAttributeInterface $attribute * @return null|string */ @@ -742,6 +770,8 @@ private function convertOptionsValueToString(array $options) : array } /** + * Adds 'use default value' checkbox. + * * @param ProductAttributeInterface $attribute * @param array $meta * @return array @@ -890,7 +920,7 @@ private function getFormElementsMapValue($value) { $valueMap = $this->formElementMapper->getMappings(); - return isset($valueMap[$value]) ? $valueMap[$value] : $value; + return $valueMap[$value] ?? $value; } /** @@ -944,6 +974,9 @@ private function canDisplayUseDefault(ProductAttributeInterface $attribute) $attributeCode = $attribute->getAttributeCode(); /** @var Product $product */ $product = $this->locator->getProduct(); + if ($product->isLockedAttribute($attributeCode)) { + return false; + } if (isset($this->canDisplayUseDefault[$attributeCode])) { return $this->canDisplayUseDefault[$attributeCode]; diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/CompositeConfigProcessor.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/CompositeConfigProcessor.php index 5513af9d98e7..fed94193225f 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/CompositeConfigProcessor.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/CompositeConfigProcessor.php @@ -10,6 +10,9 @@ use Psr\Log\LoggerInterface as Logger; +/** + * Process config for Wysiwyg. + */ class CompositeConfigProcessor implements WysiwygConfigDataProcessorInterface { /** @@ -24,6 +27,7 @@ class CompositeConfigProcessor implements WysiwygConfigDataProcessorInterface /** * CompositeConfigProcessor constructor. + * @param Logger $logger * @param array $eavWysiwygDataProcessors */ public function __construct(Logger $logger, array $eavWysiwygDataProcessors) @@ -33,7 +37,7 @@ public function __construct(Logger $logger, array $eavWysiwygDataProcessors) } /** - * {@inheritdoc} + * @inheritdoc */ public function process(\Magento\Catalog\Api\Data\ProductAttributeInterface $attribute) { diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/General.php index 98de8ea34767..26044eb91a30 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 @@ -58,8 +58,11 @@ public function __construct( } /** - * {@inheritdoc} + * Customize number fields for advanced price and weight fields. + * * @since 101.0.0 + * @param array $data + * @return array * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function modifyData(array $data) @@ -130,8 +133,11 @@ protected function customizeAdvancedPriceFormat(array $data) } /** - * {@inheritdoc} + * Customize product form fields. + * * @since 101.0.0 + * @param array $meta + * @return array */ public function modifyMeta(array $meta) { @@ -349,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); @@ -361,7 +369,8 @@ protected function customizeNameListeners(array $meta) $skuPath . static::META_CONFIG_PATH, $meta, [ - 'autoImportIfEmpty' => true + 'autoImportIfEmpty' => true, + 'validation' => ['no-marginal-whitespace' => true] ] ); diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php index 0eddca332220..a529580e2923 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php @@ -45,7 +45,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc * @since 101.1.0 */ public function modifyData(array $data) @@ -54,8 +54,11 @@ public function modifyData(array $data) } /** - * {@inheritdoc} + * Add tier price info to meta array. + * * @since 101.1.0 + * @param array $meta + * @return array */ public function modifyMeta(array $meta) { @@ -150,8 +153,8 @@ private function getUpdatedTierPriceStructure(array $priceMeta) 'dataType' => Price::NAME, 'addbefore' => '%', 'validation' => [ - 'validate-number' => true, - 'less-than-equals-to' => 100 + 'required-entry' => true, + 'validate-positive-percent-decimal' => true ], 'visible' => $firstOption && $firstOption['value'] == ProductPriceOptionsInterface::VALUE_PERCENT, diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Websites.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Websites.php index bab36ce5fc4d..9cbbb86a2c55 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Websites.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Websites.php @@ -89,7 +89,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc * @since 101.0.0 */ public function modifyData(array $data) @@ -117,7 +117,7 @@ public function modifyData(array $data) } /** - * {@inheritdoc} + * @inheritdoc * @since 101.0.0 */ public function modifyMeta(array $meta) @@ -175,9 +175,11 @@ protected function getFieldsForFieldset() $label = __('Websites'); $defaultWebsiteId = $this->websiteRepository->getDefault()->getId(); + $isOnlyOneWebsiteAvailable = count($websitesList) === 1; foreach ($websitesList as $website) { $isChecked = in_array($website['id'], $websiteIds) - || ($defaultWebsiteId == $website['id'] && $isNewProduct); + || ($defaultWebsiteId == $website['id'] && $isNewProduct) + || $isOnlyOneWebsiteAvailable; $children[$website['id']] = [ 'arguments' => [ 'data' => [ @@ -331,6 +333,8 @@ protected function getWebsitesOptions() } /** + * Returns websites options list. + * * @return array * @since 101.0.0 */ @@ -397,8 +401,9 @@ protected function getWebsitesList() $this->websitesList = []; $groupList = $this->groupRepository->getList(); $storesList = $this->storeRepository->getList(); + $websiteList = $this->storeManager->getWebsites(true); - foreach ($this->websiteRepository->getList() as $website) { + foreach ($websiteList as $website) { $websiteId = $website->getId(); if (!$websiteId) { continue; diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php index 216bc16968fc..4fcb87ab1396 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php @@ -14,15 +14,18 @@ use Magento\Catalog\Model\Product\Image\NotLoadInfoImageException; use Magento\Catalog\Ui\DataProvider\Product\ProductRenderCollectorInterface; use Magento\Framework\App\State; +use Magento\Framework\View\Design\ThemeInterface; use Magento\Framework\View\DesignInterface; use Magento\Store\Model\StoreManager; use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\View\DesignLoader; /** * Collect enough information about image rendering on front * If you want to add new image, that should render on front you need * to configure this class in di.xml * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Image implements ProductRenderCollectorInterface { @@ -51,6 +54,7 @@ class Image implements ProductRenderCollectorInterface /** * @var DesignInterface + * @deprecated 2.3.0 DesignLoader is used for design theme loading */ private $design; @@ -59,6 +63,11 @@ class Image implements ProductRenderCollectorInterface */ private $imageRenderInfoFactory; + /** + * @var DesignLoader + */ + private $designLoader; + /** * Image constructor. * @param ImageFactory $imageFactory @@ -67,6 +76,7 @@ class Image implements ProductRenderCollectorInterface * @param DesignInterface $design * @param ImageInterfaceFactory $imageRenderInfoFactory * @param array $imageCodes + * @param DesignLoader $designLoader */ public function __construct( ImageFactory $imageFactory, @@ -74,7 +84,8 @@ public function __construct( StoreManagerInterface $storeManager, DesignInterface $design, ImageInterfaceFactory $imageRenderInfoFactory, - array $imageCodes = [] + array $imageCodes = [], + DesignLoader $designLoader = null ) { $this->imageFactory = $imageFactory; $this->imageCodes = $imageCodes; @@ -82,6 +93,8 @@ public function __construct( $this->storeManager = $storeManager; $this->design = $design; $this->imageRenderInfoFactory = $imageRenderInfoFactory; + $this->designLoader = $designLoader ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(DesignLoader::class); } /** @@ -92,6 +105,8 @@ public function __construct( public function collect(ProductInterface $product, ProductRenderInterface $productRender) { $images = []; + /** @var ThemeInterface $currentTheme */ + $currentTheme = $this->design->getDesignTheme(); foreach ($this->imageCodes as $imageCode) { /** @var ImageInterface $image */ @@ -120,10 +135,13 @@ public function collect(ProductInterface $product, ProductRenderInterface $produ $images[] = $image; } + $this->design->setDesignTheme($currentTheme); $productRender->setImages($images); } /** + * Callback for emulating image creation + * * Callback in which we emulate initialize default design theme, depends on current store, be settings store id * from render info * @@ -136,7 +154,7 @@ public function collect(ProductInterface $product, ProductRenderInterface $produ public function emulateImageCreating(ProductInterface $product, $imageCode, $storeId, ImageInterface $image) { $this->storeManager->setCurrentStore($storeId); - $this->design->setDefaultDesignTheme(); + $this->designLoader->load(); $imageHelper = $this->imageFactory->create(); $imageHelper->init($product, $imageCode); diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/DataProvider.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/DataProvider.php index 3090734df014..4de0b94d0680 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/DataProvider.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/DataProvider.php @@ -24,8 +24,6 @@ class DataProvider extends \Magento\Framework\View\Element\UiComponent\DataProvi /** * @param string $name - * @param string $primaryFieldName - * @param string $requestFieldName * @param Reporting $reporting * @param SearchCriteriaBuilder $searchCriteriaBuilder * @param RequestInterface $request @@ -61,7 +59,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getData() { diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php index f4334bc25efd..e5451c8e4984 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php @@ -5,10 +5,16 @@ */ namespace Magento\Catalog\Ui\DataProvider\Product; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\Framework\Exception\LocalizedException; +use Magento\Eav\Model\Entity\Attribute\AttributeInterface; + /** * Collection which is used for rendering product list in the backend. * * Used for product grid and customizes behavior of the default Product collection for grid needs. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class ProductCollection extends \Magento\Catalog\Model\ResourceModel\Product\Collection { @@ -25,4 +31,63 @@ protected function _productLimitationJoinPrice() $this->_productLimitationFilters->setUsePriceIndex(false); return $this->_productLimitationPrice(true); } + + /** + * Add attribute filter to collection + * + * @param AttributeInterface|integer|string|array $attribute + * @param null|string|array $condition + * @param string $joinType + * @return $this + * @throws LocalizedException + */ + public function addAttributeToFilter($attribute, $condition = null, $joinType = 'inner') + { + $storeId = (int)$this->getStoreId(); + if ($attribute === 'is_saleable' + || is_array($attribute) + || $storeId !== $this->getDefaultStoreId() + ) { + return parent::addAttributeToFilter($attribute, $condition, $joinType); + } + + if ($attribute instanceof AttributeInterface) { + $attributeModel = $attribute; + } else { + $attributeModel = $this->getEntity()->getAttribute($attribute); + if ($attributeModel === false) { + throw new LocalizedException( + __('Invalid attribute identifier for filter (%1)', get_class($attribute)) + ); + } + } + + if ($attributeModel->isScopeGlobal() || $attributeModel->getBackend()->isStatic()) { + return parent::addAttributeToFilter($attribute, $condition, $joinType); + } + + $this->addAttributeToFilterAllStores($attributeModel, $condition); + + return $this; + } + + /** + * Add attribute to filter by all stores + * + * @param Attribute $attributeModel + * @param array $condition + * @return void + */ + private function addAttributeToFilterAllStores(Attribute $attributeModel, array $condition): void + { + $tableName = $this->getTable($attributeModel->getBackendTable()); + $entity = $this->getEntity(); + $fKey = 'e.' . $this->getEntityPkName($entity); + $pKey = $tableName . '.' . $this->getEntityPkName($entity); + $condition = "({$pKey} = {$fKey}) AND (" + . $this->_getConditionSql("{$tableName}.value", $condition) + . ')'; + $selectExistsInAllStores = $this->getConnection()->select()->from($tableName); + $this->getSelect()->exists($selectExistsInAllStores, $condition); + } } diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductDataProvider.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductDataProvider.php index 200ecf89641f..a518afc576d6 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductDataProvider.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductDataProvider.php @@ -7,6 +7,7 @@ use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; use Magento\Framework\App\ObjectManager; +use Magento\Store\Model\Store; use Magento\Ui\DataProvider\Modifier\ModifierInterface; use Magento\Ui\DataProvider\Modifier\PoolInterface; @@ -67,6 +68,7 @@ public function __construct( $this->addFieldStrategies = $addFieldStrategies; $this->addFilterStrategies = $addFilterStrategies; $this->modifiersPool = $modifiersPool ?: ObjectManager::getInstance()->get(PoolInterface::class); + $this->collection->setStoreId(Store::DEFAULT_STORE_ID); } /** @@ -110,7 +112,7 @@ public function addField($field, $alias = null) } /** - * {@inheritdoc} + * @inheritdoc */ public function addFilter(\Magento\Framework\Api\Filter $filter) { diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductRenderCollectorInterface.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductRenderCollectorInterface.php index 5d14cd21f7b9..3f16e0a6617d 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductRenderCollectorInterface.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductRenderCollectorInterface.php @@ -21,7 +21,6 @@ interface ProductRenderCollectorInterface * * @param ProductInterface $product * @param ProductRenderInterface $productRender - * @param array $data * @return void * @since 101.1.0 */ diff --git a/app/code/Magento/Catalog/ViewModel/Product/Breadcrumbs.php b/app/code/Magento/Catalog/ViewModel/Product/Breadcrumbs.php index 95f2531e5fdc..d1424d637937 100644 --- a/app/code/Magento/Catalog/ViewModel/Product/Breadcrumbs.php +++ b/app/code/Magento/Catalog/ViewModel/Product/Breadcrumbs.php @@ -105,7 +105,7 @@ public function getJsonConfigurationHtmlEscaped() : string [ 'breadcrumbs' => [ 'categoryUrlSuffix' => $this->escaper->escapeHtml($this->getCategoryUrlSuffix()), - 'userCategoryPathInUrl' => (int)$this->isCategoryUsedInProductUrl(), + 'useCategoryPathInUrl' => (int)$this->isCategoryUsedInProductUrl(), 'product' => $this->escaper->escapeHtml($this->getProductName()) ] ], diff --git a/app/code/Magento/Catalog/ViewModel/Product/Checker/AddToCompareAvailability.php b/app/code/Magento/Catalog/ViewModel/Product/Checker/AddToCompareAvailability.php new file mode 100644 index 000000000000..27829155af29 --- /dev/null +++ b/app/code/Magento/Catalog/ViewModel/Product/Checker/AddToCompareAvailability.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\ViewModel\Product\Checker; + +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\CatalogInventory\Api\StockConfigurationInterface; + +/** + * Check is available add to compare. + */ +class AddToCompareAvailability implements ArgumentInterface +{ + /** + * @var StockConfigurationInterface + */ + private $stockConfiguration; + + /** + * @param StockConfigurationInterface $stockConfiguration + */ + public function __construct(StockConfigurationInterface $stockConfiguration) + { + $this->stockConfiguration = $stockConfiguration; + } + + /** + * Is product available for comparison. + * + * @param ProductInterface $product + * @return bool + */ + public function isAvailableForCompare(ProductInterface $product): bool + { + return $this->isInStock($product) || $this->stockConfiguration->isShowOutOfStock(); + } + + /** + * Get is in stock status. + * + * @param ProductInterface $product + * @return bool + */ + private function isInStock(ProductInterface $product): bool + { + $quantityAndStockStatus = $product->getQuantityAndStockStatus(); + if (!$quantityAndStockStatus) { + return $product->isSalable(); + } + + return isset($quantityAndStockStatus['is_in_stock']) && $quantityAndStockStatus['is_in_stock']; + } +} diff --git a/app/code/Magento/Catalog/composer.json b/app/code/Magento/Catalog/composer.json index 44d051933909..5c3ee3da8ca8 100644 --- a/app/code/Magento/Catalog/composer.json +++ b/app/code/Magento/Catalog/composer.json @@ -7,6 +7,8 @@ "require": { "php": "~7.1.3||~7.2.0", "magento/framework": "*", + "magento/module-authorization": "*", + "magento/module-asynchronous-operations": "*", "magento/module-backend": "*", "magento/module-catalog-inventory": "*", "magento/module-catalog-rule": "*", diff --git a/app/code/Magento/Catalog/etc/adminhtml/di.xml b/app/code/Magento/Catalog/etc/adminhtml/di.xml index 10251d35dffc..c04cfb2dce00 100644 --- a/app/code/Magento/Catalog/etc/adminhtml/di.xml +++ b/app/code/Magento/Catalog/etc/adminhtml/di.xml @@ -78,7 +78,7 @@ <type name="Magento\Catalog\Model\ResourceModel\Attribute"> <plugin name="invalidate_pagecache_after_attribute_save" type="Magento\Catalog\Plugin\Model\ResourceModel\Attribute\Save" /> </type> - <virtualType name="\Magento\Catalog\Ui\DataProvider\Product\ProductCollectionFactory" type="\Magento\Catalog\Model\ResourceModel\Product\CollectionFactory"> + <virtualType name="Magento\Catalog\Ui\DataProvider\Product\ProductCollectionFactory" type="Magento\Catalog\Model\ResourceModel\Product\CollectionFactory"> <arguments> <argument name="instanceName" xsi:type="string">\Magento\Catalog\Ui\DataProvider\Product\ProductCollection</argument> </arguments> @@ -220,4 +220,16 @@ <argument name="filter" xsi:type="object">Magento\Catalog\Ui\DataProvider\Product\AddSearchKeyConditionToCollection</argument> </arguments> </type> + <type name="Magento\Catalog\Block\Adminhtml\Product\Edit\Action\Attribute\Tab\Attributes"> + <arguments> + <argument name="excludeFields" xsi:type="array"> + <item name="0" xsi:type="string">category_ids</item> + <item name="1" xsi:type="string">gallery</item> + <item name="2" xsi:type="string">image</item> + <item name="3" xsi:type="string">media_gallery</item> + <item name="4" xsi:type="string">quantity_and_stock_status</item> + <item name="5" xsi:type="string">tier_price</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Catalog/etc/adminhtml/events.xml b/app/code/Magento/Catalog/etc/adminhtml/events.xml index f4fd7fc30398..ad83f5898237 100644 --- a/app/code/Magento/Catalog/etc/adminhtml/events.xml +++ b/app/code/Magento/Catalog/etc/adminhtml/events.xml @@ -9,4 +9,7 @@ <event name="cms_wysiwyg_images_static_urls_allowed"> <observer name="catalog_wysiwyg" instance="Magento\Catalog\Observer\CatalogCheckIsUsingStaticUrlsAllowedObserver" /> </event> + <event name="catalog_category_change_products"> + <observer name="category_product_indexer" instance="Magento\Catalog\Observer\CategoryProductIndexer"/> + </event> </config> diff --git a/app/code/Magento/Catalog/etc/adminhtml/system.xml b/app/code/Magento/Catalog/etc/adminhtml/system.xml index 71a799fd2242..1d563244f143 100644 --- a/app/code/Magento/Catalog/etc/adminhtml/system.xml +++ b/app/code/Magento/Catalog/etc/adminhtml/system.xml @@ -10,6 +10,9 @@ <tab id="catalog" translate="label" sortOrder="200"> <label>Catalog</label> </tab> + <tab id="advanced" translate="label" sortOrder="999999"> + <label>Advanced</label> + </tab> <section id="catalog" translate="label" type="text" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1"> <class>separator-top</class> <label>Catalog</label> @@ -56,7 +59,7 @@ <field id="grid_per_page_values" translate="label comment" type="text" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Products per Page on Grid Allowed Values</label> <comment>Comma-separated.</comment> - <validate>validate-per-page-value-list</validate> + <validate>validate-per-page-value-list required-entry</validate> </field> <field id="grid_per_page" translate="label comment" type="text" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Products per Page on Grid Default Value</label> @@ -66,7 +69,7 @@ <field id="list_per_page_values" translate="label comment" type="text" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Products per Page on List Allowed Values</label> <comment>Comma-separated.</comment> - <validate>validate-per-page-value-list</validate> + <validate>validate-per-page-value-list required-entry</validate> </field> <field id="list_per_page" translate="label comment" type="text" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Products per Page on List Default Value</label> @@ -83,8 +86,9 @@ <backend_model>Magento\Catalog\Model\Indexer\Product\Flat\System\Config\Mode</backend_model> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> - <field id="default_sort_by" translate="label" type="select" sortOrder="6" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="default_sort_by" translate="label comment" type="select" sortOrder="6" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Product Listing Sort by</label> + <comment>Applies to category pages</comment> <source_model>Magento\Catalog\Model\Config\Source\ListSort</source_model> </field> <field id="list_allow_all" translate="label comment" type="select" sortOrder="6" showInDefault="1" showInWebsite="1" showInStore="1"> @@ -92,6 +96,11 @@ <comment>Whether to show "All" option in the "Show X Per Page" dropdown</comment> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> + <field id="remember_pagination" translate="label comment" type="select" sortOrder="7" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> + <label>Remember Category Pagination</label> + <comment>Changing may affect SEO and cache storage consumption.</comment> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + </field> </group> <group id="placeholder" translate="label" sortOrder="300" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Product Image Placeholders</label> @@ -193,5 +202,19 @@ </field> </group> </section> + <section id="system" translate="label" type="text" sortOrder="900" showInDefault="1" showInWebsite="1" showInStore="1"> + <class>separator-top</class> + <label>System</label> + <tab>advanced</tab> + <resource>Magento_Config::config_system</resource> + <group id="upload_configuration" translate="label" type="text" sortOrder="1000" showInDefault="1" showInWebsite="1" showInStore="1"> + <label>Images Upload Configuration</label> + <field id="jpeg_quality" translate="label comment" type="text" sortOrder="100" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> + <label>Quality</label> + <validate>validate-digits validate-digits-range digits-range-1-100 required-entry</validate> + <comment>Jpeg quality for resized images 1-100%.</comment> + </field> + </group> + </section> </system> </config> diff --git a/app/code/Magento/Catalog/etc/communication.xml b/app/code/Magento/Catalog/etc/communication.xml new file mode 100644 index 000000000000..1a957f6ac9fe --- /dev/null +++ b/app/code/Magento/Catalog/etc/communication.xml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Communication/etc/communication.xsd"> + <topic name="product_action_attribute.update" request="Magento\AsynchronousOperations\Api\Data\OperationInterface"> + <handler name="product_action_attribute.update" type="Magento\Catalog\Model\Attribute\Backend\Consumer" method="process" /> + </topic> + <topic name="product_action_attribute.website.update" request="Magento\AsynchronousOperations\Api\Data\OperationInterface"> + <handler name="product_action_attribute.website.update" type="Magento\Catalog\Model\Attribute\Backend\ConsumerWebsiteAssign" method="process" /> + </topic> +</config> \ No newline at end of file diff --git a/app/code/Magento/Catalog/etc/config.xml b/app/code/Magento/Catalog/etc/config.xml index f52760aa5074..3a842166a382 100644 --- a/app/code/Magento/Catalog/etc/config.xml +++ b/app/code/Magento/Catalog/etc/config.xml @@ -30,6 +30,7 @@ <flat_catalog_category>0</flat_catalog_category> <default_sort_by>position</default_sort_by> <parse_url_directives>1</parse_url_directives> + <remember_pagination>0</remember_pagination> </frontend> <product> <flat> @@ -66,6 +67,9 @@ <product_custom_options_fodler>custom_options</product_custom_options_fodler> </allowed_resources> </media_storage_configuration> + <upload_configuration> + <jpeg_quality>80</jpeg_quality> + </upload_configuration> </system> <design> <watermark> diff --git a/app/code/Magento/Catalog/etc/db_schema.xml b/app/code/Magento/Catalog/etc/db_schema.xml index 7b4e1a96adda..17e3dddc41c3 100644 --- a/app/code/Magento/Catalog/etc/db_schema.xml +++ b/app/code/Magento/Catalog/etc/db_schema.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="catalog_product_entity" resource="default" engine="innodb" comment="Catalog Product Table"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="smallint" name="attribute_set_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Attribute Set ID"/> <column xsi:type="varchar" name="type_id" nullable="false" length="32" default="simple" comment="Type ID"/> @@ -22,13 +22,13 @@ comment="Creation Time"/> <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" comment="Update Time"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> - <index name="CATALOG_PRODUCT_ENTITY_ATTRIBUTE_SET_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_ENTITY_ATTRIBUTE_SET_ID" indexType="btree"> <column name="attribute_set_id"/> </index> - <index name="CATALOG_PRODUCT_ENTITY_SKU" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_ENTITY_SKU" indexType="btree"> <column name="sku"/> </index> </table> @@ -41,29 +41,29 @@ <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Store ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0"/> + default="0" comment="Entity ID"/> <column xsi:type="datetime" name="value" on_update="false" nullable="true" comment="Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="foreign" name="CAT_PRD_ENTT_DTIME_ATTR_ID_EAV_ATTR_ATTR_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_DTIME_ATTR_ID_EAV_ATTR_ATTR_ID" table="catalog_product_entity_datetime" column="attribute_id" referenceTable="eav_attribute" referenceColumn="attribute_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CAT_PRD_ENTT_DTIME_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_DTIME_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_product_entity_datetime" column="entity_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CATALOG_PRODUCT_ENTITY_DATETIME_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="CATALOG_PRODUCT_ENTITY_DATETIME_STORE_ID_STORE_STORE_ID" table="catalog_product_entity_datetime" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CATALOG_PRODUCT_ENTITY_DATETIME_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> + <constraint xsi:type="unique" referenceId="CATALOG_PRODUCT_ENTITY_DATETIME_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> <column name="entity_id"/> <column name="attribute_id"/> <column name="store_id"/> </constraint> - <index name="CATALOG_PRODUCT_ENTITY_DATETIME_ATTRIBUTE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_ENTITY_DATETIME_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> - <index name="CATALOG_PRODUCT_ENTITY_DATETIME_STORE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_ENTITY_DATETIME_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -76,30 +76,30 @@ <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Store ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0"/> + default="0" comment="Entity ID"/> <column xsi:type="decimal" name="value" scale="4" precision="12" unsigned="false" nullable="true" comment="Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="foreign" name="CAT_PRD_ENTT_DEC_ATTR_ID_EAV_ATTR_ATTR_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_DEC_ATTR_ID_EAV_ATTR_ATTR_ID" table="catalog_product_entity_decimal" column="attribute_id" referenceTable="eav_attribute" referenceColumn="attribute_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CAT_PRD_ENTT_DEC_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_DEC_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_product_entity_decimal" column="entity_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CATALOG_PRODUCT_ENTITY_DECIMAL_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="CATALOG_PRODUCT_ENTITY_DECIMAL_STORE_ID_STORE_STORE_ID" table="catalog_product_entity_decimal" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CATALOG_PRODUCT_ENTITY_DECIMAL_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> + <constraint xsi:type="unique" referenceId="CATALOG_PRODUCT_ENTITY_DECIMAL_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> <column name="entity_id"/> <column name="attribute_id"/> <column name="store_id"/> </constraint> - <index name="CATALOG_PRODUCT_ENTITY_DECIMAL_STORE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_ENTITY_DECIMAL_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="CATALOG_PRODUCT_ENTITY_DECIMAL_ATTRIBUTE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_ENTITY_DECIMAL_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> </table> @@ -112,30 +112,30 @@ <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Store ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0"/> + default="0" comment="Entity ID"/> <column xsi:type="int" name="value" padding="11" unsigned="false" nullable="true" identity="false" comment="Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="foreign" name="CAT_PRD_ENTT_INT_ATTR_ID_EAV_ATTR_ATTR_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_INT_ATTR_ID_EAV_ATTR_ATTR_ID" table="catalog_product_entity_int" column="attribute_id" referenceTable="eav_attribute" referenceColumn="attribute_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CAT_PRD_ENTT_INT_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_INT_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_product_entity_int" column="entity_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CATALOG_PRODUCT_ENTITY_INT_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="CATALOG_PRODUCT_ENTITY_INT_STORE_ID_STORE_STORE_ID" table="catalog_product_entity_int" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CATALOG_PRODUCT_ENTITY_INT_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> + <constraint xsi:type="unique" referenceId="CATALOG_PRODUCT_ENTITY_INT_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> <column name="entity_id"/> <column name="attribute_id"/> <column name="store_id"/> </constraint> - <index name="CATALOG_PRODUCT_ENTITY_INT_ATTRIBUTE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_ENTITY_INT_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> - <index name="CATALOG_PRODUCT_ENTITY_INT_STORE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_ENTITY_INT_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -148,29 +148,29 @@ <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Store ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0"/> + default="0" comment="Entity ID"/> <column xsi:type="text" name="value" nullable="true" comment="Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="foreign" name="CAT_PRD_ENTT_TEXT_ATTR_ID_EAV_ATTR_ATTR_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_TEXT_ATTR_ID_EAV_ATTR_ATTR_ID" table="catalog_product_entity_text" column="attribute_id" referenceTable="eav_attribute" referenceColumn="attribute_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CAT_PRD_ENTT_TEXT_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_TEXT_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_product_entity_text" column="entity_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CATALOG_PRODUCT_ENTITY_TEXT_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="CATALOG_PRODUCT_ENTITY_TEXT_STORE_ID_STORE_STORE_ID" table="catalog_product_entity_text" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CATALOG_PRODUCT_ENTITY_TEXT_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> + <constraint xsi:type="unique" referenceId="CATALOG_PRODUCT_ENTITY_TEXT_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> <column name="entity_id"/> <column name="attribute_id"/> <column name="store_id"/> </constraint> - <index name="CATALOG_PRODUCT_ENTITY_TEXT_ATTRIBUTE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_ENTITY_TEXT_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> - <index name="CATALOG_PRODUCT_ENTITY_TEXT_STORE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_ENTITY_TEXT_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -183,29 +183,29 @@ <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Store ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0"/> + default="0" comment="Entity ID"/> <column xsi:type="varchar" name="value" nullable="true" length="255" comment="Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="foreign" name="CAT_PRD_ENTT_VCHR_ATTR_ID_EAV_ATTR_ATTR_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_VCHR_ATTR_ID_EAV_ATTR_ATTR_ID" table="catalog_product_entity_varchar" column="attribute_id" referenceTable="eav_attribute" referenceColumn="attribute_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CAT_PRD_ENTT_VCHR_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_VCHR_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_product_entity_varchar" column="entity_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CATALOG_PRODUCT_ENTITY_VARCHAR_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="CATALOG_PRODUCT_ENTITY_VARCHAR_STORE_ID_STORE_STORE_ID" table="catalog_product_entity_varchar" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CATALOG_PRODUCT_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> + <constraint xsi:type="unique" referenceId="CATALOG_PRODUCT_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> <column name="entity_id"/> <column name="attribute_id"/> <column name="store_id"/> </constraint> - <index name="CATALOG_PRODUCT_ENTITY_VARCHAR_ATTRIBUTE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_ENTITY_VARCHAR_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> - <index name="CATALOG_PRODUCT_ENTITY_VARCHAR_STORE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_ENTITY_VARCHAR_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -218,40 +218,40 @@ <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Store ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0"/> + default="0" comment="Entity ID"/> <column xsi:type="int" name="position" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Position"/> <column xsi:type="varchar" name="value" nullable="true" length="255" comment="Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="foreign" name="CAT_PRD_ENTT_GLR_ATTR_ID_EAV_ATTR_ATTR_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_GLR_ATTR_ID_EAV_ATTR_ATTR_ID" table="catalog_product_entity_gallery" column="attribute_id" referenceTable="eav_attribute" referenceColumn="attribute_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CAT_PRD_ENTT_GLR_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_GLR_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_product_entity_gallery" column="entity_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CATALOG_PRODUCT_ENTITY_GALLERY_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="CATALOG_PRODUCT_ENTITY_GALLERY_STORE_ID_STORE_STORE_ID" table="catalog_product_entity_gallery" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CATALOG_PRODUCT_ENTITY_GALLERY_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> + <constraint xsi:type="unique" referenceId="CATALOG_PRODUCT_ENTITY_GALLERY_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> <column name="entity_id"/> <column name="attribute_id"/> <column name="store_id"/> </constraint> - <index name="CATALOG_PRODUCT_ENTITY_GALLERY_ENTITY_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_ENTITY_GALLERY_ENTITY_ID" indexType="btree"> <column name="entity_id"/> </index> - <index name="CATALOG_PRODUCT_ENTITY_GALLERY_ATTRIBUTE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_ENTITY_GALLERY_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> - <index name="CATALOG_PRODUCT_ENTITY_GALLERY_STORE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_ENTITY_GALLERY_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> <table name="catalog_category_entity" resource="default" engine="innodb" comment="Catalog Category Table"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="smallint" name="attribute_set_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Attribute Set ID"/> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="false" identity="false" @@ -267,13 +267,13 @@ comment="Tree Level"/> <column xsi:type="int" name="children_count" padding="11" unsigned="false" nullable="false" identity="false" comment="Child Count"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> - <index name="CATALOG_CATEGORY_ENTITY_LEVEL" indexType="btree"> + <index referenceId="CATALOG_CATEGORY_ENTITY_LEVEL" indexType="btree"> <column name="level"/> </index> - <index name="CATALOG_CATEGORY_ENTITY_PATH" indexType="btree"> + <index referenceId="CATALOG_CATEGORY_ENTITY_PATH" indexType="btree"> <column name="path"/> </index> </table> @@ -286,32 +286,32 @@ <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Store ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0"/> + default="0" comment="Entity ID"/> <column xsi:type="datetime" name="value" on_update="false" nullable="true" comment="Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="foreign" name="CAT_CTGR_ENTT_DTIME_ATTR_ID_EAV_ATTR_ATTR_ID" + <constraint xsi:type="foreign" referenceId="CAT_CTGR_ENTT_DTIME_ATTR_ID_EAV_ATTR_ATTR_ID" table="catalog_category_entity_datetime" column="attribute_id" referenceTable="eav_attribute" referenceColumn="attribute_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CAT_CTGR_ENTT_DTIME_ENTT_ID_CAT_CTGR_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="CAT_CTGR_ENTT_DTIME_ENTT_ID_CAT_CTGR_ENTT_ENTT_ID" table="catalog_category_entity_datetime" column="entity_id" referenceTable="catalog_category_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CATALOG_CATEGORY_ENTITY_DATETIME_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="CATALOG_CATEGORY_ENTITY_DATETIME_STORE_ID_STORE_STORE_ID" table="catalog_category_entity_datetime" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CATALOG_CATEGORY_ENTITY_DATETIME_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> + <constraint xsi:type="unique" referenceId="CATALOG_CATEGORY_ENTITY_DATETIME_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> <column name="entity_id"/> <column name="attribute_id"/> <column name="store_id"/> </constraint> - <index name="CATALOG_CATEGORY_ENTITY_DATETIME_ENTITY_ID" indexType="btree"> + <index referenceId="CATALOG_CATEGORY_ENTITY_DATETIME_ENTITY_ID" indexType="btree"> <column name="entity_id"/> </index> - <index name="CATALOG_CATEGORY_ENTITY_DATETIME_ATTRIBUTE_ID" indexType="btree"> + <index referenceId="CATALOG_CATEGORY_ENTITY_DATETIME_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> - <index name="CATALOG_CATEGORY_ENTITY_DATETIME_STORE_ID" indexType="btree"> + <index referenceId="CATALOG_CATEGORY_ENTITY_DATETIME_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -324,33 +324,33 @@ <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Store ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0"/> + default="0" comment="Entity ID"/> <column xsi:type="decimal" name="value" scale="4" precision="12" unsigned="false" nullable="true" comment="Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="foreign" name="CAT_CTGR_ENTT_DEC_ATTR_ID_EAV_ATTR_ATTR_ID" + <constraint xsi:type="foreign" referenceId="CAT_CTGR_ENTT_DEC_ATTR_ID_EAV_ATTR_ATTR_ID" table="catalog_category_entity_decimal" column="attribute_id" referenceTable="eav_attribute" referenceColumn="attribute_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CAT_CTGR_ENTT_DEC_ENTT_ID_CAT_CTGR_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="CAT_CTGR_ENTT_DEC_ENTT_ID_CAT_CTGR_ENTT_ENTT_ID" table="catalog_category_entity_decimal" column="entity_id" referenceTable="catalog_category_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CATALOG_CATEGORY_ENTITY_DECIMAL_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="CATALOG_CATEGORY_ENTITY_DECIMAL_STORE_ID_STORE_STORE_ID" table="catalog_category_entity_decimal" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CATALOG_CATEGORY_ENTITY_DECIMAL_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> + <constraint xsi:type="unique" referenceId="CATALOG_CATEGORY_ENTITY_DECIMAL_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> <column name="entity_id"/> <column name="attribute_id"/> <column name="store_id"/> </constraint> - <index name="CATALOG_CATEGORY_ENTITY_DECIMAL_ENTITY_ID" indexType="btree"> + <index referenceId="CATALOG_CATEGORY_ENTITY_DECIMAL_ENTITY_ID" indexType="btree"> <column name="entity_id"/> </index> - <index name="CATALOG_CATEGORY_ENTITY_DECIMAL_ATTRIBUTE_ID" indexType="btree"> + <index referenceId="CATALOG_CATEGORY_ENTITY_DECIMAL_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> - <index name="CATALOG_CATEGORY_ENTITY_DECIMAL_STORE_ID" indexType="btree"> + <index referenceId="CATALOG_CATEGORY_ENTITY_DECIMAL_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -363,33 +363,33 @@ <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Store ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0"/> + default="0" comment="Entity ID"/> <column xsi:type="int" name="value" padding="11" unsigned="false" nullable="true" identity="false" comment="Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="foreign" name="CAT_CTGR_ENTT_INT_ATTR_ID_EAV_ATTR_ATTR_ID" + <constraint xsi:type="foreign" referenceId="CAT_CTGR_ENTT_INT_ATTR_ID_EAV_ATTR_ATTR_ID" table="catalog_category_entity_int" column="attribute_id" referenceTable="eav_attribute" referenceColumn="attribute_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CAT_CTGR_ENTT_INT_ENTT_ID_CAT_CTGR_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="CAT_CTGR_ENTT_INT_ENTT_ID_CAT_CTGR_ENTT_ENTT_ID" table="catalog_category_entity_int" column="entity_id" referenceTable="catalog_category_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CATALOG_CATEGORY_ENTITY_INT_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="CATALOG_CATEGORY_ENTITY_INT_STORE_ID_STORE_STORE_ID" table="catalog_category_entity_int" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CATALOG_CATEGORY_ENTITY_INT_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> + <constraint xsi:type="unique" referenceId="CATALOG_CATEGORY_ENTITY_INT_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> <column name="entity_id"/> <column name="attribute_id"/> <column name="store_id"/> </constraint> - <index name="CATALOG_CATEGORY_ENTITY_INT_ENTITY_ID" indexType="btree"> + <index referenceId="CATALOG_CATEGORY_ENTITY_INT_ENTITY_ID" indexType="btree"> <column name="entity_id"/> </index> - <index name="CATALOG_CATEGORY_ENTITY_INT_ATTRIBUTE_ID" indexType="btree"> + <index referenceId="CATALOG_CATEGORY_ENTITY_INT_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> - <index name="CATALOG_CATEGORY_ENTITY_INT_STORE_ID" indexType="btree"> + <index referenceId="CATALOG_CATEGORY_ENTITY_INT_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -402,32 +402,32 @@ <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Store ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0"/> + default="0" comment="Entity ID"/> <column xsi:type="text" name="value" nullable="true" comment="Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="foreign" name="CAT_CTGR_ENTT_TEXT_ATTR_ID_EAV_ATTR_ATTR_ID" + <constraint xsi:type="foreign" referenceId="CAT_CTGR_ENTT_TEXT_ATTR_ID_EAV_ATTR_ATTR_ID" table="catalog_category_entity_text" column="attribute_id" referenceTable="eav_attribute" referenceColumn="attribute_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CAT_CTGR_ENTT_TEXT_ENTT_ID_CAT_CTGR_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="CAT_CTGR_ENTT_TEXT_ENTT_ID_CAT_CTGR_ENTT_ENTT_ID" table="catalog_category_entity_text" column="entity_id" referenceTable="catalog_category_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CATALOG_CATEGORY_ENTITY_TEXT_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="CATALOG_CATEGORY_ENTITY_TEXT_STORE_ID_STORE_STORE_ID" table="catalog_category_entity_text" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CATALOG_CATEGORY_ENTITY_TEXT_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> + <constraint xsi:type="unique" referenceId="CATALOG_CATEGORY_ENTITY_TEXT_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> <column name="entity_id"/> <column name="attribute_id"/> <column name="store_id"/> </constraint> - <index name="CATALOG_CATEGORY_ENTITY_TEXT_ENTITY_ID" indexType="btree"> + <index referenceId="CATALOG_CATEGORY_ENTITY_TEXT_ENTITY_ID" indexType="btree"> <column name="entity_id"/> </index> - <index name="CATALOG_CATEGORY_ENTITY_TEXT_ATTRIBUTE_ID" indexType="btree"> + <index referenceId="CATALOG_CATEGORY_ENTITY_TEXT_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> - <index name="CATALOG_CATEGORY_ENTITY_TEXT_STORE_ID" indexType="btree"> + <index referenceId="CATALOG_CATEGORY_ENTITY_TEXT_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -440,32 +440,32 @@ <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Store ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0"/> + default="0" comment="Entity ID"/> <column xsi:type="varchar" name="value" nullable="true" length="255" comment="Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="foreign" name="CAT_CTGR_ENTT_VCHR_ATTR_ID_EAV_ATTR_ATTR_ID" + <constraint xsi:type="foreign" referenceId="CAT_CTGR_ENTT_VCHR_ATTR_ID_EAV_ATTR_ATTR_ID" table="catalog_category_entity_varchar" column="attribute_id" referenceTable="eav_attribute" referenceColumn="attribute_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CAT_CTGR_ENTT_VCHR_ENTT_ID_CAT_CTGR_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="CAT_CTGR_ENTT_VCHR_ENTT_ID_CAT_CTGR_ENTT_ENTT_ID" table="catalog_category_entity_varchar" column="entity_id" referenceTable="catalog_category_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CATALOG_CATEGORY_ENTITY_VARCHAR_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="CATALOG_CATEGORY_ENTITY_VARCHAR_STORE_ID_STORE_STORE_ID" table="catalog_category_entity_varchar" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CATALOG_CATEGORY_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> + <constraint xsi:type="unique" referenceId="CATALOG_CATEGORY_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> <column name="entity_id"/> <column name="attribute_id"/> <column name="store_id"/> </constraint> - <index name="CATALOG_CATEGORY_ENTITY_VARCHAR_ENTITY_ID" indexType="btree"> + <index referenceId="CATALOG_CATEGORY_ENTITY_VARCHAR_ENTITY_ID" indexType="btree"> <column name="entity_id"/> </index> - <index name="CATALOG_CATEGORY_ENTITY_VARCHAR_ATTRIBUTE_ID" indexType="btree"> + <index referenceId="CATALOG_CATEGORY_ENTITY_VARCHAR_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> - <index name="CATALOG_CATEGORY_ENTITY_VARCHAR_STORE_ID" indexType="btree"> + <index referenceId="CATALOG_CATEGORY_ENTITY_VARCHAR_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -479,22 +479,22 @@ default="0" comment="Product ID"/> <column xsi:type="int" name="position" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Position"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> <column name="category_id"/> <column name="product_id"/> </constraint> - <constraint xsi:type="foreign" name="CAT_CTGR_PRD_PRD_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_category_product" + <constraint xsi:type="foreign" referenceId="CAT_CTGR_PRD_PRD_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_category_product" column="product_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CAT_CTGR_PRD_CTGR_ID_CAT_CTGR_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="CAT_CTGR_PRD_CTGR_ID_CAT_CTGR_ENTT_ENTT_ID" table="catalog_category_product" column="category_id" referenceTable="catalog_category_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CATALOG_CATEGORY_PRODUCT_CATEGORY_ID_PRODUCT_ID"> + <constraint xsi:type="unique" referenceId="CATALOG_CATEGORY_PRODUCT_CATEGORY_ID_PRODUCT_ID"> <column name="category_id"/> <column name="product_id"/> </constraint> - <index name="CATALOG_CATEGORY_PRODUCT_PRODUCT_ID" indexType="btree"> + <index referenceId="CATALOG_CATEGORY_PRODUCT_PRODUCT_ID" indexType="btree"> <column name="product_id"/> </index> </table> @@ -512,18 +512,18 @@ default="0" comment="Store ID"/> <column xsi:type="smallint" name="visibility" padding="5" unsigned="true" nullable="false" identity="false" comment="Visibility"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="category_id"/> <column name="product_id"/> <column name="store_id"/> </constraint> - <index name="CAT_CTGR_PRD_IDX_PRD_ID_STORE_ID_CTGR_ID_VISIBILITY" indexType="btree"> + <index referenceId="CAT_CTGR_PRD_IDX_PRD_ID_STORE_ID_CTGR_ID_VISIBILITY" indexType="btree"> <column name="product_id"/> <column name="store_id"/> <column name="category_id"/> <column name="visibility"/> </index> - <index name="CAT_CTGR_PRD_IDX_STORE_ID_CTGR_ID_VISIBILITY_IS_PARENT_POSITION" indexType="btree"> + <index referenceId="CAT_CTGR_PRD_IDX_STORE_ID_CTGR_ID_VISIBILITY_IS_PARENT_POSITION" indexType="btree"> <column name="store_id"/> <column name="category_id"/> <column name="visibility"/> @@ -542,29 +542,29 @@ default="0" comment="Product ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" comment="Store ID"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="catalog_compare_item_id"/> </constraint> - <constraint xsi:type="foreign" name="CATALOG_COMPARE_ITEM_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="CATALOG_COMPARE_ITEM_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID" table="catalog_compare_item" column="customer_id" referenceTable="customer_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CATALOG_COMPARE_ITEM_PRODUCT_ID_CATALOG_PRODUCT_ENTITY_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="CATALOG_COMPARE_ITEM_PRODUCT_ID_CATALOG_PRODUCT_ENTITY_ENTITY_ID" table="catalog_compare_item" column="product_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CATALOG_COMPARE_ITEM_STORE_ID_STORE_STORE_ID" table="catalog_compare_item" + <constraint xsi:type="foreign" referenceId="CATALOG_COMPARE_ITEM_STORE_ID_STORE_STORE_ID" table="catalog_compare_item" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="SET NULL"/> - <index name="CATALOG_COMPARE_ITEM_PRODUCT_ID" indexType="btree"> + <index referenceId="CATALOG_COMPARE_ITEM_PRODUCT_ID" indexType="btree"> <column name="product_id"/> </index> - <index name="CATALOG_COMPARE_ITEM_VISITOR_ID_PRODUCT_ID" indexType="btree"> + <index referenceId="CATALOG_COMPARE_ITEM_VISITOR_ID_PRODUCT_ID" indexType="btree"> <column name="visitor_id"/> <column name="product_id"/> </index> - <index name="CATALOG_COMPARE_ITEM_CUSTOMER_ID_PRODUCT_ID" indexType="btree"> + <index referenceId="CATALOG_COMPARE_ITEM_CUSTOMER_ID_PRODUCT_ID" indexType="btree"> <column name="customer_id"/> <column name="product_id"/> </index> - <index name="CATALOG_COMPARE_ITEM_STORE_ID" indexType="btree"> + <index referenceId="CATALOG_COMPARE_ITEM_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -574,17 +574,17 @@ comment="Product ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Website ID"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="product_id"/> <column name="website_id"/> </constraint> - <constraint xsi:type="foreign" name="CATALOG_PRODUCT_WEBSITE_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID" + <constraint xsi:type="foreign" referenceId="CATALOG_PRODUCT_WEBSITE_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID" table="catalog_product_website" column="website_id" referenceTable="store_website" referenceColumn="website_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CAT_PRD_WS_PRD_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_product_website" + <constraint xsi:type="foreign" referenceId="CAT_PRD_WS_PRD_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_product_website" column="product_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <index name="CATALOG_PRODUCT_WEBSITE_WEBSITE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_WEBSITE_WEBSITE_ID" indexType="btree"> <column name="website_id"/> </index> </table> @@ -593,7 +593,7 @@ <column xsi:type="smallint" name="link_type_id" padding="5" unsigned="true" nullable="false" identity="true" comment="Link Type ID"/> <column xsi:type="varchar" name="code" nullable="true" length="32" comment="Code"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="link_type_id"/> </constraint> </table> @@ -607,27 +607,27 @@ default="0" comment="Linked Product ID"/> <column xsi:type="smallint" name="link_type_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Link Type ID"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="link_id"/> </constraint> - <constraint xsi:type="foreign" name="CAT_PRD_LNK_LNKED_PRD_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_product_link" + <constraint xsi:type="foreign" referenceId="CAT_PRD_LNK_LNKED_PRD_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_product_link" column="linked_product_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CATALOG_PRODUCT_LINK_PRODUCT_ID_CATALOG_PRODUCT_ENTITY_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="CATALOG_PRODUCT_LINK_PRODUCT_ID_CATALOG_PRODUCT_ENTITY_ENTITY_ID" table="catalog_product_link" column="product_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CAT_PRD_LNK_LNK_TYPE_ID_CAT_PRD_LNK_TYPE_LNK_TYPE_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_LNK_LNK_TYPE_ID_CAT_PRD_LNK_TYPE_LNK_TYPE_ID" table="catalog_product_link" column="link_type_id" referenceTable="catalog_product_link_type" referenceColumn="link_type_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CATALOG_PRODUCT_LINK_LINK_TYPE_ID_PRODUCT_ID_LINKED_PRODUCT_ID"> + <constraint xsi:type="unique" referenceId="CATALOG_PRODUCT_LINK_LINK_TYPE_ID_PRODUCT_ID_LINKED_PRODUCT_ID"> <column name="link_type_id"/> <column name="product_id"/> <column name="linked_product_id"/> </constraint> - <index name="CATALOG_PRODUCT_LINK_PRODUCT_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_LINK_PRODUCT_ID" indexType="btree"> <column name="product_id"/> </index> - <index name="CATALOG_PRODUCT_LINK_LINKED_PRODUCT_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_LINK_LINKED_PRODUCT_ID" indexType="btree"> <column name="linked_product_id"/> </index> </table> @@ -640,13 +640,13 @@ <column xsi:type="varchar" name="product_link_attribute_code" nullable="true" length="32" comment="Product Link Attribute Code"/> <column xsi:type="varchar" name="data_type" nullable="true" length="32" comment="Data Type"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="product_link_attribute_id"/> </constraint> - <constraint xsi:type="foreign" name="CAT_PRD_LNK_ATTR_LNK_TYPE_ID_CAT_PRD_LNK_TYPE_LNK_TYPE_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_LNK_ATTR_LNK_TYPE_ID_CAT_PRD_LNK_TYPE_LNK_TYPE_ID" table="catalog_product_link_attribute" column="link_type_id" referenceTable="catalog_product_link_type" referenceColumn="link_type_id" onDelete="CASCADE"/> - <index name="CATALOG_PRODUCT_LINK_ATTRIBUTE_LINK_TYPE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_LINK_ATTRIBUTE_LINK_TYPE_ID" indexType="btree"> <column name="link_type_id"/> </index> </table> @@ -660,21 +660,21 @@ comment="Link ID"/> <column xsi:type="decimal" name="value" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="foreign" name="CAT_PRD_LNK_ATTR_DEC_LNK_ID_CAT_PRD_LNK_LNK_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_LNK_ATTR_DEC_LNK_ID_CAT_PRD_LNK_LNK_ID" table="catalog_product_link_attribute_decimal" column="link_id" referenceTable="catalog_product_link" referenceColumn="link_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="FK_AB2EFA9A14F7BCF1D5400056203D14B6" + <constraint xsi:type="foreign" referenceId="FK_AB2EFA9A14F7BCF1D5400056203D14B6" table="catalog_product_link_attribute_decimal" column="product_link_attribute_id" referenceTable="catalog_product_link_attribute" referenceColumn="product_link_attribute_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CAT_PRD_LNK_ATTR_DEC_PRD_LNK_ATTR_ID_LNK_ID"> + <constraint xsi:type="unique" referenceId="CAT_PRD_LNK_ATTR_DEC_PRD_LNK_ATTR_ID_LNK_ID"> <column name="product_link_attribute_id"/> <column name="link_id"/> </constraint> - <index name="CATALOG_PRODUCT_LINK_ATTRIBUTE_DECIMAL_LINK_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_LINK_ATTRIBUTE_DECIMAL_LINK_ID" indexType="btree"> <column name="link_id"/> </index> </table> @@ -688,21 +688,21 @@ comment="Link ID"/> <column xsi:type="int" name="value" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="foreign" name="CAT_PRD_LNK_ATTR_INT_LNK_ID_CAT_PRD_LNK_LNK_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_LNK_ATTR_INT_LNK_ID_CAT_PRD_LNK_LNK_ID" table="catalog_product_link_attribute_int" column="link_id" referenceTable="catalog_product_link" referenceColumn="link_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="FK_D6D878F8BA2A4282F8DDED7E6E3DE35C" + <constraint xsi:type="foreign" referenceId="FK_D6D878F8BA2A4282F8DDED7E6E3DE35C" table="catalog_product_link_attribute_int" column="product_link_attribute_id" referenceTable="catalog_product_link_attribute" referenceColumn="product_link_attribute_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CAT_PRD_LNK_ATTR_INT_PRD_LNK_ATTR_ID_LNK_ID"> + <constraint xsi:type="unique" referenceId="CAT_PRD_LNK_ATTR_INT_PRD_LNK_ATTR_ID_LNK_ID"> <column name="product_link_attribute_id"/> <column name="link_id"/> </constraint> - <index name="CATALOG_PRODUCT_LINK_ATTRIBUTE_INT_LINK_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_LINK_ATTRIBUTE_INT_LINK_ID" indexType="btree"> <column name="link_id"/> </index> </table> @@ -715,21 +715,21 @@ <column xsi:type="int" name="link_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Link ID"/> <column xsi:type="varchar" name="value" nullable="true" length="255" comment="Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="foreign" name="CAT_PRD_LNK_ATTR_VCHR_LNK_ID_CAT_PRD_LNK_LNK_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_LNK_ATTR_VCHR_LNK_ID_CAT_PRD_LNK_LNK_ID" table="catalog_product_link_attribute_varchar" column="link_id" referenceTable="catalog_product_link" referenceColumn="link_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="FK_DEE9C4DA61CFCC01DFCF50F0D79CEA51" + <constraint xsi:type="foreign" referenceId="FK_DEE9C4DA61CFCC01DFCF50F0D79CEA51" table="catalog_product_link_attribute_varchar" column="product_link_attribute_id" referenceTable="catalog_product_link_attribute" referenceColumn="product_link_attribute_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CAT_PRD_LNK_ATTR_VCHR_PRD_LNK_ATTR_ID_LNK_ID"> + <constraint xsi:type="unique" referenceId="CAT_PRD_LNK_ATTR_VCHR_PRD_LNK_ATTR_ID_LNK_ID"> <column name="product_link_attribute_id"/> <column name="link_id"/> </constraint> - <index name="CATALOG_PRODUCT_LINK_ATTRIBUTE_VARCHAR_LINK_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_LINK_ATTRIBUTE_VARCHAR_LINK_ID" indexType="btree"> <column name="link_id"/> </index> </table> @@ -738,7 +738,7 @@ <column xsi:type="int" name="value_id" padding="11" unsigned="false" nullable="false" identity="true" comment="Value ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0"/> + default="0" comment="Entity ID"/> <column xsi:type="smallint" name="all_groups" padding="5" unsigned="true" nullable="false" identity="false" default="1" comment="Is Applicable To All Customer Groups"/> <column xsi:type="int" name="customer_group_id" padding="10" unsigned="true" nullable="false" identity="false" @@ -751,29 +751,29 @@ comment="Website ID"/> <column xsi:type="decimal" name="percentage_value" scale="2" precision="5" unsigned="false" nullable="true" comment="Percentage value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="foreign" name="CAT_PRD_ENTT_TIER_PRICE_CSTR_GROUP_ID_CSTR_GROUP_CSTR_GROUP_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_TIER_PRICE_CSTR_GROUP_ID_CSTR_GROUP_CSTR_GROUP_ID" table="catalog_product_entity_tier_price" column="customer_group_id" referenceTable="customer_group" referenceColumn="customer_group_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CAT_PRD_ENTT_TIER_PRICE_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_TIER_PRICE_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_product_entity_tier_price" column="entity_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CAT_PRD_ENTT_TIER_PRICE_WS_ID_STORE_WS_WS_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_TIER_PRICE_WS_ID_STORE_WS_WS_ID" table="catalog_product_entity_tier_price" column="website_id" referenceTable="store_website" referenceColumn="website_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="UNQ_E8AB433B9ACB00343ABB312AD2FAB087"> + <constraint xsi:type="unique" referenceId="UNQ_E8AB433B9ACB00343ABB312AD2FAB087"> <column name="entity_id"/> <column name="all_groups"/> <column name="customer_group_id"/> <column name="qty"/> <column name="website_id"/> </constraint> - <index name="CATALOG_PRODUCT_ENTITY_TIER_PRICE_CUSTOMER_GROUP_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_ENTITY_TIER_PRICE_CUSTOMER_GROUP_ID" indexType="btree"> <column name="customer_group_id"/> </index> - <index name="CATALOG_PRODUCT_ENTITY_TIER_PRICE_WEBSITE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_ENTITY_TIER_PRICE_WEBSITE_ID" indexType="btree"> <column name="website_id"/> </index> </table> @@ -788,13 +788,13 @@ comment="Media entry type"/> <column xsi:type="smallint" name="disabled" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Visibility status"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="foreign" name="CAT_PRD_ENTT_MDA_GLR_ATTR_ID_EAV_ATTR_ATTR_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_MDA_GLR_ATTR_ID_EAV_ATTR_ATTR_ID" table="catalog_product_entity_media_gallery" column="attribute_id" referenceTable="eav_attribute" referenceColumn="attribute_id" onDelete="CASCADE"/> - <index name="CATALOG_PRODUCT_ENTITY_MEDIA_GALLERY_ATTRIBUTE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_ENTITY_MEDIA_GALLERY_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> </table> @@ -805,7 +805,7 @@ <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Store ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0"/> + default="0" comment="Entity ID"/> <column xsi:type="varchar" name="label" nullable="true" length="255" comment="Label"/> <column xsi:type="int" name="position" padding="10" unsigned="true" nullable="true" identity="false" comment="Position"/> @@ -813,28 +813,33 @@ default="0" comment="Is Disabled"/> <column xsi:type="int" name="record_id" padding="10" unsigned="true" nullable="false" identity="true" comment="Record Id"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="record_id"/> </constraint> - <constraint xsi:type="foreign" name="CAT_PRD_ENTT_MDA_GLR_VAL_VAL_ID_CAT_PRD_ENTT_MDA_GLR_VAL_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_MDA_GLR_VAL_VAL_ID_CAT_PRD_ENTT_MDA_GLR_VAL_ID" table="catalog_product_entity_media_gallery_value" column="value_id" referenceTable="catalog_product_entity_media_gallery" referenceColumn="value_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CAT_PRD_ENTT_MDA_GLR_VAL_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_MDA_GLR_VAL_STORE_ID_STORE_STORE_ID" table="catalog_product_entity_media_gallery_value" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CAT_PRD_ENTT_MDA_GLR_VAL_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_MDA_GLR_VAL_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_product_entity_media_gallery_value" column="entity_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <index name="CATALOG_PRODUCT_ENTITY_MEDIA_GALLERY_VALUE_STORE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_ENTITY_MEDIA_GALLERY_VALUE_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="CATALOG_PRODUCT_ENTITY_MEDIA_GALLERY_VALUE_ENTITY_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_ENTITY_MEDIA_GALLERY_VALUE_ENTITY_ID" indexType="btree"> <column name="entity_id"/> </index> - <index name="CATALOG_PRODUCT_ENTITY_MEDIA_GALLERY_VALUE_VALUE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_ENTITY_MEDIA_GALLERY_VALUE_VALUE_ID" indexType="btree"> <column name="value_id"/> </index> + <index referenceId="CAT_PRD_ENTT_MDA_GLR_VAL_ENTT_ID_VAL_ID_STORE_ID" indexType="btree"> + <column name="entity_id"/> + <column name="value_id"/> + <column name="store_id"/> + </index> </table> <table name="catalog_product_option" resource="default" engine="innodb" comment="Catalog Product Option Table"> <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="true" @@ -854,13 +859,13 @@ comment="Image Size Y"/> <column xsi:type="int" name="sort_order" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Sort Order"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="option_id"/> </constraint> - <constraint xsi:type="foreign" name="CAT_PRD_OPT_PRD_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_product_option" + <constraint xsi:type="foreign" referenceId="CAT_PRD_OPT_PRD_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_product_option" column="product_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <index name="CATALOG_PRODUCT_OPTION_PRODUCT_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_OPTION_PRODUCT_ID" indexType="btree"> <column name="product_id"/> </index> </table> @@ -875,20 +880,20 @@ <column xsi:type="decimal" name="price" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Price"/> <column xsi:type="varchar" name="price_type" nullable="false" length="7" default="fixed" comment="Price Type"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="option_price_id"/> </constraint> - <constraint xsi:type="foreign" name="CAT_PRD_OPT_PRICE_OPT_ID_CAT_PRD_OPT_OPT_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_OPT_PRICE_OPT_ID_CAT_PRD_OPT_OPT_ID" table="catalog_product_option_price" column="option_id" referenceTable="catalog_product_option" referenceColumn="option_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CATALOG_PRODUCT_OPTION_PRICE_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="CATALOG_PRODUCT_OPTION_PRICE_STORE_ID_STORE_STORE_ID" table="catalog_product_option_price" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CATALOG_PRODUCT_OPTION_PRICE_OPTION_ID_STORE_ID"> + <constraint xsi:type="unique" referenceId="CATALOG_PRODUCT_OPTION_PRICE_OPTION_ID_STORE_ID"> <column name="option_id"/> <column name="store_id"/> </constraint> - <index name="CATALOG_PRODUCT_OPTION_PRICE_STORE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_OPTION_PRICE_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -901,20 +906,20 @@ <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Store ID"/> <column xsi:type="varchar" name="title" nullable="true" length="255" comment="Title"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="option_title_id"/> </constraint> - <constraint xsi:type="foreign" name="CAT_PRD_OPT_TTL_OPT_ID_CAT_PRD_OPT_OPT_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_OPT_TTL_OPT_ID_CAT_PRD_OPT_OPT_ID" table="catalog_product_option_title" column="option_id" referenceTable="catalog_product_option" referenceColumn="option_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CATALOG_PRODUCT_OPTION_TITLE_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="CATALOG_PRODUCT_OPTION_TITLE_STORE_ID_STORE_STORE_ID" table="catalog_product_option_title" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CATALOG_PRODUCT_OPTION_TITLE_OPTION_ID_STORE_ID"> + <constraint xsi:type="unique" referenceId="CATALOG_PRODUCT_OPTION_TITLE_OPTION_ID_STORE_ID"> <column name="option_id"/> <column name="store_id"/> </constraint> - <index name="CATALOG_PRODUCT_OPTION_TITLE_STORE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_OPTION_TITLE_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -927,13 +932,13 @@ <column xsi:type="varchar" name="sku" nullable="true" length="64" comment="SKU"/> <column xsi:type="int" name="sort_order" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Sort Order"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="option_type_id"/> </constraint> - <constraint xsi:type="foreign" name="CAT_PRD_OPT_TYPE_VAL_OPT_ID_CAT_PRD_OPT_OPT_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_OPT_TYPE_VAL_OPT_ID_CAT_PRD_OPT_OPT_ID" table="catalog_product_option_type_value" column="option_id" referenceTable="catalog_product_option" referenceColumn="option_id" onDelete="CASCADE"/> - <index name="CATALOG_PRODUCT_OPTION_TYPE_VALUE_OPTION_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_OPTION_TYPE_VALUE_OPTION_ID" indexType="btree"> <column name="option_id"/> </index> </table> @@ -948,21 +953,21 @@ <column xsi:type="decimal" name="price" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Price"/> <column xsi:type="varchar" name="price_type" nullable="false" length="7" default="fixed" comment="Price Type"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="option_type_price_id"/> </constraint> - <constraint xsi:type="foreign" name="FK_B523E3378E8602F376CC415825576B7F" + <constraint xsi:type="foreign" referenceId="FK_B523E3378E8602F376CC415825576B7F" table="catalog_product_option_type_price" column="option_type_id" referenceTable="catalog_product_option_type_value" referenceColumn="option_type_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CATALOG_PRODUCT_OPTION_TYPE_PRICE_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="CATALOG_PRODUCT_OPTION_TYPE_PRICE_STORE_ID_STORE_STORE_ID" table="catalog_product_option_type_price" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CATALOG_PRODUCT_OPTION_TYPE_PRICE_OPTION_TYPE_ID_STORE_ID"> + <constraint xsi:type="unique" referenceId="CATALOG_PRODUCT_OPTION_TYPE_PRICE_OPTION_TYPE_ID_STORE_ID"> <column name="option_type_id"/> <column name="store_id"/> </constraint> - <index name="CATALOG_PRODUCT_OPTION_TYPE_PRICE_STORE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_OPTION_TYPE_PRICE_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -975,21 +980,21 @@ <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Store ID"/> <column xsi:type="varchar" name="title" nullable="true" length="255" comment="Title"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="option_type_title_id"/> </constraint> - <constraint xsi:type="foreign" name="FK_C085B9CF2C2A302E8043FDEA1937D6A2" + <constraint xsi:type="foreign" referenceId="FK_C085B9CF2C2A302E8043FDEA1937D6A2" table="catalog_product_option_type_title" column="option_type_id" referenceTable="catalog_product_option_type_value" referenceColumn="option_type_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CATALOG_PRODUCT_OPTION_TYPE_TITLE_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="CATALOG_PRODUCT_OPTION_TYPE_TITLE_STORE_ID_STORE_STORE_ID" table="catalog_product_option_type_title" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CATALOG_PRODUCT_OPTION_TYPE_TITLE_OPTION_TYPE_ID_STORE_ID"> + <constraint xsi:type="unique" referenceId="CATALOG_PRODUCT_OPTION_TYPE_TITLE_OPTION_TYPE_ID_STORE_ID"> <column name="option_type_id"/> <column name="store_id"/> </constraint> - <index name="CATALOG_PRODUCT_OPTION_TYPE_TITLE_STORE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_OPTION_TYPE_TITLE_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -1037,16 +1042,16 @@ identity="false" default="0" comment="Is Visible in Grid"/> <column xsi:type="smallint" name="is_filterable_in_grid" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Is Filterable in Grid"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="attribute_id"/> </constraint> - <constraint xsi:type="foreign" name="CATALOG_EAV_ATTRIBUTE_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" + <constraint xsi:type="foreign" referenceId="CATALOG_EAV_ATTRIBUTE_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" table="catalog_eav_attribute" column="attribute_id" referenceTable="eav_attribute" referenceColumn="attribute_id" onDelete="CASCADE"/> - <index name="CATALOG_EAV_ATTRIBUTE_USED_FOR_SORT_BY" indexType="btree"> + <index referenceId="CATALOG_EAV_ATTRIBUTE_USED_FOR_SORT_BY" indexType="btree"> <column name="used_for_sort_by"/> </index> - <index name="CATALOG_EAV_ATTRIBUTE_USED_IN_PRODUCT_LISTING" indexType="btree"> + <index referenceId="CATALOG_EAV_ATTRIBUTE_USED_IN_PRODUCT_LISTING" indexType="btree"> <column name="used_in_product_listing"/> </index> </table> @@ -1055,17 +1060,17 @@ comment="Parent ID"/> <column xsi:type="int" name="child_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Child ID"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="parent_id"/> <column name="child_id"/> </constraint> - <constraint xsi:type="foreign" name="CAT_PRD_RELATION_CHILD_ID_CAT_PRD_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_RELATION_CHILD_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_product_relation" column="child_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CAT_PRD_RELATION_PARENT_ID_CAT_PRD_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_RELATION_PARENT_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_product_relation" column="parent_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <index name="CATALOG_PRODUCT_RELATION_CHILD_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_RELATION_CHILD_ID" indexType="btree"> <column name="child_id"/> </index> </table> @@ -1081,20 +1086,20 @@ comment="Value"/> <column xsi:type="int" name="source_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Original entity Id for attribute value"/> - <constraint xsi:type="primary" name="CAT_PRD_IDX_EAV_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> + <constraint xsi:type="primary" referenceId="CAT_PRD_IDX_EAV_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> <column name="entity_id"/> <column name="attribute_id"/> <column name="store_id"/> <column name="value"/> <column name="source_id"/> </constraint> - <index name="CATALOG_PRODUCT_INDEX_EAV_ATTRIBUTE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> - <index name="CATALOG_PRODUCT_INDEX_EAV_STORE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="CATALOG_PRODUCT_INDEX_EAV_VALUE" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_VALUE" indexType="btree"> <column name="value"/> </index> </table> @@ -1110,20 +1115,20 @@ comment="Value"/> <column xsi:type="int" name="source_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Original entity Id for attribute value"/> - <constraint xsi:type="primary" name="CAT_PRD_IDX_EAV_DEC_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> + <constraint xsi:type="primary" referenceId="CAT_PRD_IDX_EAV_DEC_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> <column name="entity_id"/> <column name="attribute_id"/> <column name="store_id"/> <column name="value"/> <column name="source_id"/> </constraint> - <index name="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_ATTRIBUTE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> - <index name="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_STORE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_VALUE" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_VALUE" indexType="btree"> <column name="value"/> </index> </table> @@ -1147,18 +1152,18 @@ comment="Max Price"/> <column xsi:type="decimal" name="tier_price" scale="4" precision="12" unsigned="false" nullable="true" comment="Tier Price"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> <column name="customer_group_id"/> <column name="website_id"/> </constraint> - <index name="CATALOG_PRODUCT_INDEX_PRICE_CUSTOMER_GROUP_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_INDEX_PRICE_CUSTOMER_GROUP_ID" indexType="btree"> <column name="customer_group_id"/> </index> - <index name="CATALOG_PRODUCT_INDEX_PRICE_MIN_PRICE" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_INDEX_PRICE_MIN_PRICE" indexType="btree"> <column name="min_price"/> </index> - <index name="CAT_PRD_IDX_PRICE_WS_ID_CSTR_GROUP_ID_MIN_PRICE" indexType="btree"> + <index referenceId="CAT_PRD_IDX_PRICE_WS_ID_CSTR_GROUP_ID_MIN_PRICE" indexType="btree"> <column name="website_id"/> <column name="customer_group_id"/> <column name="min_price"/> @@ -1174,24 +1179,24 @@ comment="Website ID"/> <column xsi:type="decimal" name="min_price" scale="4" precision="12" unsigned="false" nullable="true" comment="Min Price"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> <column name="customer_group_id"/> <column name="website_id"/> </constraint> - <constraint xsi:type="foreign" name="CAT_PRD_IDX_TIER_PRICE_CSTR_GROUP_ID_CSTR_GROUP_CSTR_GROUP_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_IDX_TIER_PRICE_CSTR_GROUP_ID_CSTR_GROUP_CSTR_GROUP_ID" table="catalog_product_index_tier_price" column="customer_group_id" referenceTable="customer_group" referenceColumn="customer_group_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CAT_PRD_IDX_TIER_PRICE_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_IDX_TIER_PRICE_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_product_index_tier_price" column="entity_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CAT_PRD_IDX_TIER_PRICE_WS_ID_STORE_WS_WS_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_IDX_TIER_PRICE_WS_ID_STORE_WS_WS_ID" table="catalog_product_index_tier_price" column="website_id" referenceTable="store_website" referenceColumn="website_id" onDelete="CASCADE"/> - <index name="CATALOG_PRODUCT_INDEX_TIER_PRICE_CUSTOMER_GROUP_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_INDEX_TIER_PRICE_CUSTOMER_GROUP_ID" indexType="btree"> <column name="customer_group_id"/> </index> - <index name="CATALOG_PRODUCT_INDEX_TIER_PRICE_WEBSITE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_INDEX_TIER_PRICE_WEBSITE_ID" indexType="btree"> <column name="website_id"/> </index> </table> @@ -1203,12 +1208,12 @@ comment="Default store id for website"/> <column xsi:type="date" name="website_date" comment="Website Date"/> <column xsi:type="float" name="rate" unsigned="false" nullable="true" default="1" comment="Rate"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="website_id"/> </constraint> - <constraint xsi:type="foreign" name="CAT_PRD_IDX_WS_WS_ID_STORE_WS_WS_ID" table="catalog_product_index_website" + <constraint xsi:type="foreign" referenceId="CAT_PRD_IDX_WS_WS_ID_STORE_WS_WS_ID" table="catalog_product_index_website" column="website_id" referenceTable="store_website" referenceColumn="website_id" onDelete="CASCADE"/> - <index name="CATALOG_PRODUCT_INDEX_WEBSITE_WEBSITE_DATE" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_INDEX_WEBSITE_WEBSITE_DATE" indexType="btree"> <column name="website_date"/> </index> </table> @@ -1226,7 +1231,7 @@ comment="Price"/> <column xsi:type="decimal" name="tier_price" scale="4" precision="12" unsigned="false" nullable="true" comment="Tier Price"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="parent_id"/> <column name="child_id"/> <column name="customer_group_id"/> @@ -1247,7 +1252,7 @@ comment="Price"/> <column xsi:type="decimal" name="tier_price" scale="4" precision="12" unsigned="false" nullable="true" comment="Tier Price"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="parent_id"/> <column name="child_id"/> <column name="customer_group_id"/> @@ -1268,7 +1273,7 @@ comment="Max Price"/> <column xsi:type="decimal" name="tier_price" scale="4" precision="12" unsigned="false" nullable="true" comment="Tier Price"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> <column name="customer_group_id"/> <column name="website_id"/> @@ -1288,7 +1293,7 @@ comment="Max Price"/> <column xsi:type="decimal" name="tier_price" scale="4" precision="12" unsigned="false" nullable="true" comment="Tier Price"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> <column name="customer_group_id"/> <column name="website_id"/> @@ -1316,7 +1321,7 @@ comment="Tier Price"/> <column xsi:type="decimal" name="base_tier" scale="4" precision="12" unsigned="false" nullable="true" comment="Base Tier"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> <column name="customer_group_id"/> <column name="website_id"/> @@ -1344,7 +1349,7 @@ comment="Tier Price"/> <column xsi:type="decimal" name="base_tier" scale="4" precision="12" unsigned="false" nullable="true" comment="Base Tier"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> <column name="customer_group_id"/> <column name="website_id"/> @@ -1364,7 +1369,7 @@ comment="Max Price"/> <column xsi:type="decimal" name="tier_price" scale="4" precision="12" unsigned="false" nullable="true" comment="Tier Price"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> <column name="customer_group_id"/> <column name="website_id"/> @@ -1384,7 +1389,7 @@ comment="Max Price"/> <column xsi:type="decimal" name="tier_price" scale="4" precision="12" unsigned="false" nullable="true" comment="Tier Price"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> <column name="customer_group_id"/> <column name="website_id"/> @@ -1406,7 +1411,7 @@ comment="Max Price"/> <column xsi:type="decimal" name="tier_price" scale="4" precision="12" unsigned="false" nullable="true" comment="Tier Price"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> <column name="customer_group_id"/> <column name="website_id"/> @@ -1429,7 +1434,7 @@ comment="Max Price"/> <column xsi:type="decimal" name="tier_price" scale="4" precision="12" unsigned="false" nullable="true" comment="Tier Price"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> <column name="customer_group_id"/> <column name="website_id"/> @@ -1448,20 +1453,20 @@ comment="Value"/> <column xsi:type="int" name="source_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Original entity Id for attribute value"/> - <constraint xsi:type="primary" name="CAT_PRD_IDX_EAV_IDX_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> + <constraint xsi:type="primary" referenceId="CAT_PRD_IDX_EAV_IDX_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> <column name="entity_id"/> <column name="attribute_id"/> <column name="store_id"/> <column name="value"/> <column name="source_id"/> </constraint> - <index name="CATALOG_PRODUCT_INDEX_EAV_IDX_ATTRIBUTE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_IDX_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> - <index name="CATALOG_PRODUCT_INDEX_EAV_IDX_STORE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_IDX_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="CATALOG_PRODUCT_INDEX_EAV_IDX_VALUE" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_IDX_VALUE" indexType="btree"> <column name="value"/> </index> </table> @@ -1477,20 +1482,20 @@ comment="Value"/> <column xsi:type="int" name="source_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Original entity Id for attribute value"/> - <constraint xsi:type="primary" name="CAT_PRD_IDX_EAV_TMP_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> + <constraint xsi:type="primary" referenceId="CAT_PRD_IDX_EAV_TMP_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> <column name="entity_id"/> <column name="attribute_id"/> <column name="store_id"/> <column name="value"/> <column name="source_id"/> </constraint> - <index name="CATALOG_PRODUCT_INDEX_EAV_TMP_ATTRIBUTE_ID" indexType="hash"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_TMP_ATTRIBUTE_ID" indexType="hash"> <column name="attribute_id"/> </index> - <index name="CATALOG_PRODUCT_INDEX_EAV_TMP_STORE_ID" indexType="hash"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_TMP_STORE_ID" indexType="hash"> <column name="store_id"/> </index> - <index name="CATALOG_PRODUCT_INDEX_EAV_TMP_VALUE" indexType="hash"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_TMP_VALUE" indexType="hash"> <column name="value"/> </index> </table> @@ -1506,20 +1511,20 @@ comment="Value"/> <column xsi:type="int" name="source_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Original entity Id for attribute value"/> - <constraint xsi:type="primary" name="CAT_PRD_IDX_EAV_DEC_IDX_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> + <constraint xsi:type="primary" referenceId="CAT_PRD_IDX_EAV_DEC_IDX_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> <column name="entity_id"/> <column name="attribute_id"/> <column name="store_id"/> <column name="value"/> <column name="source_id"/> </constraint> - <index name="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_IDX_ATTRIBUTE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_IDX_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> - <index name="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_IDX_STORE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_IDX_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_IDX_VALUE" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_IDX_VALUE" indexType="btree"> <column name="value"/> </index> </table> @@ -1535,20 +1540,20 @@ comment="Value"/> <column xsi:type="int" name="source_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Original entity Id for attribute value"/> - <constraint xsi:type="primary" name="CAT_PRD_IDX_EAV_DEC_TMP_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> + <constraint xsi:type="primary" referenceId="CAT_PRD_IDX_EAV_DEC_TMP_ENTT_ID_ATTR_ID_STORE_ID_VAL_SOURCE_ID"> <column name="entity_id"/> <column name="attribute_id"/> <column name="store_id"/> <column name="value"/> <column name="source_id"/> </constraint> - <index name="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_TMP_ATTRIBUTE_ID" indexType="hash"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_TMP_ATTRIBUTE_ID" indexType="hash"> <column name="attribute_id"/> </index> - <index name="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_TMP_STORE_ID" indexType="hash"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_TMP_STORE_ID" indexType="hash"> <column name="store_id"/> </index> - <index name="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_TMP_VALUE" indexType="hash"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_TMP_VALUE" indexType="hash"> <column name="value"/> </index> </table> @@ -1572,18 +1577,18 @@ comment="Max Price"/> <column xsi:type="decimal" name="tier_price" scale="4" precision="12" unsigned="false" nullable="true" comment="Tier Price"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> <column name="customer_group_id"/> <column name="website_id"/> </constraint> - <index name="CATALOG_PRODUCT_INDEX_PRICE_IDX_CUSTOMER_GROUP_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_INDEX_PRICE_IDX_CUSTOMER_GROUP_ID" indexType="btree"> <column name="customer_group_id"/> </index> - <index name="CATALOG_PRODUCT_INDEX_PRICE_IDX_WEBSITE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_INDEX_PRICE_IDX_WEBSITE_ID" indexType="btree"> <column name="website_id"/> </index> - <index name="CATALOG_PRODUCT_INDEX_PRICE_IDX_MIN_PRICE" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_INDEX_PRICE_IDX_MIN_PRICE" indexType="btree"> <column name="min_price"/> </index> </table> @@ -1607,18 +1612,18 @@ comment="Max Price"/> <column xsi:type="decimal" name="tier_price" scale="4" precision="12" unsigned="false" nullable="true" comment="Tier Price"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> <column name="customer_group_id"/> <column name="website_id"/> </constraint> - <index name="CATALOG_PRODUCT_INDEX_PRICE_TMP_CUSTOMER_GROUP_ID" indexType="hash"> + <index referenceId="CATALOG_PRODUCT_INDEX_PRICE_TMP_CUSTOMER_GROUP_ID" indexType="hash"> <column name="customer_group_id"/> </index> - <index name="CATALOG_PRODUCT_INDEX_PRICE_TMP_WEBSITE_ID" indexType="hash"> + <index referenceId="CATALOG_PRODUCT_INDEX_PRICE_TMP_WEBSITE_ID" indexType="hash"> <column name="website_id"/> </index> - <index name="CATALOG_PRODUCT_INDEX_PRICE_TMP_MIN_PRICE" indexType="hash"> + <index referenceId="CATALOG_PRODUCT_INDEX_PRICE_TMP_MIN_PRICE" indexType="hash"> <column name="min_price"/> </index> </table> @@ -1636,12 +1641,12 @@ default="0" comment="Store ID"/> <column xsi:type="smallint" name="visibility" padding="5" unsigned="true" nullable="false" identity="false" comment="Visibility"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="category_id"/> <column name="product_id"/> <column name="store_id"/> </constraint> - <index name="CAT_CTGR_PRD_IDX_TMP_PRD_ID_CTGR_ID_STORE_ID" indexType="hash"> + <index referenceId="CAT_CTGR_PRD_IDX_TMP_PRD_ID_CTGR_ID_STORE_ID" indexType="hash"> <column name="product_id"/> <column name="category_id"/> <column name="store_id"/> @@ -1651,15 +1656,16 @@ comment="Link Media value to Product entity table"> <column xsi:type="int" name="value_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Value media Entry ID"/> - <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false"/> - <constraint xsi:type="foreign" name="FK_A6C6C8FAA386736921D3A7C4B50B1185" + <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" + comment="Product Entity ID"/> + <constraint xsi:type="foreign" referenceId="FK_A6C6C8FAA386736921D3A7C4B50B1185" table="catalog_product_entity_media_gallery_value_to_entity" column="value_id" referenceTable="catalog_product_entity_media_gallery" referenceColumn="value_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CAT_PRD_ENTT_MDA_GLR_VAL_TO_ENTT_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_MDA_GLR_VAL_TO_ENTT_ENTT_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_product_entity_media_gallery_value_to_entity" column="entity_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CAT_PRD_ENTT_MDA_GLR_VAL_TO_ENTT_VAL_ID_ENTT_ID"> + <constraint xsi:type="unique" referenceId="CAT_PRD_ENTT_MDA_GLR_VAL_TO_ENTT_VAL_ID_ENTT_ID"> <column name="value_id"/> <column name="entity_id"/> </constraint> @@ -1676,20 +1682,20 @@ comment="Value"/> <column xsi:type="int" name="source_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Original entity Id for attribute value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> <column name="attribute_id"/> <column name="store_id"/> <column name="value"/> <column name="source_id"/> </constraint> - <index name="CATALOG_PRODUCT_INDEX_EAV_ATTRIBUTE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> - <index name="CATALOG_PRODUCT_INDEX_EAV_STORE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="CATALOG_PRODUCT_INDEX_EAV_VALUE" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_VALUE" indexType="btree"> <column name="value"/> </index> </table> @@ -1705,20 +1711,20 @@ comment="Value"/> <column xsi:type="int" name="source_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Original entity Id for attribute value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> <column name="attribute_id"/> <column name="store_id"/> <column name="value"/> <column name="source_id"/> </constraint> - <index name="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_ATTRIBUTE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> - <index name="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_STORE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_VALUE" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_INDEX_EAV_DECIMAL_VALUE" indexType="btree"> <column name="value"/> </index> </table> @@ -1742,18 +1748,18 @@ comment="Max Price"/> <column xsi:type="decimal" name="tier_price" scale="4" precision="12" unsigned="false" nullable="true" comment="Tier Price"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> <column name="customer_group_id"/> <column name="website_id"/> </constraint> - <index name="CATALOG_PRODUCT_INDEX_PRICE_CUSTOMER_GROUP_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_INDEX_PRICE_CUSTOMER_GROUP_ID" indexType="btree"> <column name="customer_group_id"/> </index> - <index name="CATALOG_PRODUCT_INDEX_PRICE_MIN_PRICE" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_INDEX_PRICE_MIN_PRICE" indexType="btree"> <column name="min_price"/> </index> - <index name="CAT_PRD_IDX_PRICE_WS_ID_CSTR_GROUP_ID_MIN_PRICE" indexType="btree"> + <index referenceId="CAT_PRD_IDX_PRICE_WS_ID_CSTR_GROUP_ID_MIN_PRICE" indexType="btree"> <column name="website_id"/> <column name="customer_group_id"/> <column name="min_price"/> @@ -1773,18 +1779,18 @@ default="0" comment="Store ID"/> <column xsi:type="smallint" name="visibility" padding="5" unsigned="true" nullable="false" identity="false" comment="Visibility"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="category_id"/> <column name="product_id"/> <column name="store_id"/> </constraint> - <index name="CAT_CTGR_PRD_IDX_PRD_ID_STORE_ID_CTGR_ID_VISIBILITY" indexType="btree"> + <index referenceId="CAT_CTGR_PRD_IDX_PRD_ID_STORE_ID_CTGR_ID_VISIBILITY" indexType="btree"> <column name="product_id"/> <column name="store_id"/> <column name="category_id"/> <column name="visibility"/> </index> - <index name="CAT_CTGR_PRD_IDX_STORE_ID_CTGR_ID_VISIBILITY_IS_PARENT_POSITION" indexType="btree"> + <index referenceId="CAT_CTGR_PRD_IDX_STORE_ID_CTGR_ID_VISIBILITY_IS_PARENT_POSITION" indexType="btree"> <column name="store_id"/> <column name="category_id"/> <column name="visibility"/> @@ -1805,21 +1811,21 @@ comment="Product Id"/> <column xsi:type="bigint" name="added_at" padding="20" unsigned="false" nullable="false" identity="false" comment="Added At"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="action_id"/> </constraint> - <constraint xsi:type="foreign" name="CAT_PRD_FRONTEND_ACTION_CSTR_ID_CSTR_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_FRONTEND_ACTION_CSTR_ID_CSTR_ENTT_ENTT_ID" table="catalog_product_frontend_action" column="customer_id" referenceTable="customer_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CAT_PRD_FRONTEND_ACTION_PRD_ID_CAT_PRD_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_FRONTEND_ACTION_PRD_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_product_frontend_action" column="product_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE" /> - <constraint xsi:type="unique" name="CATALOG_PRODUCT_FRONTEND_ACTION_VISITOR_ID_PRODUCT_ID_TYPE_ID"> + <constraint xsi:type="unique" referenceId="CATALOG_PRODUCT_FRONTEND_ACTION_VISITOR_ID_PRODUCT_ID_TYPE_ID"> <column name="visitor_id"/> <column name="product_id"/> <column name="type_id"/> </constraint> - <constraint xsi:type="unique" name="CATALOG_PRODUCT_FRONTEND_ACTION_CUSTOMER_ID_PRODUCT_ID_TYPE_ID"> + <constraint xsi:type="unique" referenceId="CATALOG_PRODUCT_FRONTEND_ACTION_CUSTOMER_ID_PRODUCT_ID_TYPE_ID"> <column name="customer_id"/> <column name="product_id"/> <column name="type_id"/> diff --git a/app/code/Magento/Catalog/etc/db_schema_whitelist.json b/app/code/Magento/Catalog/etc/db_schema_whitelist.json index b1543a6a007f..d4bd6927d434 100644 --- a/app/code/Magento/Catalog/etc/db_schema_whitelist.json +++ b/app/code/Magento/Catalog/etc/db_schema_whitelist.json @@ -484,7 +484,8 @@ "index": { "CATALOG_PRODUCT_ENTITY_MEDIA_GALLERY_VALUE_STORE_ID": true, "CATALOG_PRODUCT_ENTITY_MEDIA_GALLERY_VALUE_ENTITY_ID": true, - "CATALOG_PRODUCT_ENTITY_MEDIA_GALLERY_VALUE_VALUE_ID": true + "CATALOG_PRODUCT_ENTITY_MEDIA_GALLERY_VALUE_VALUE_ID": true, + "CAT_PRD_ENTT_MDA_GLR_VAL_ENTT_ID_VAL_ID_STORE_ID": true }, "constraint": { "PRIMARY": true, @@ -1121,4 +1122,4 @@ "CATALOG_PRODUCT_FRONTEND_ACTION_CUSTOMER_ID_PRODUCT_ID_TYPE_ID": true } } -} \ No newline at end of file +} diff --git a/app/code/Magento/Catalog/etc/di.xml b/app/code/Magento/Catalog/etc/di.xml index 32cc01f3036a..49447447622f 100644 --- a/app/code/Magento/Catalog/etc/di.xml +++ b/app/code/Magento/Catalog/etc/di.xml @@ -72,6 +72,7 @@ <preference for="Magento\Catalog\Model\Indexer\Product\Price\UpdateIndexInterface" type="Magento\Catalog\Model\Indexer\Product\Price\InvalidateIndex" /> <preference for="Magento\Catalog\Model\Product\Gallery\ImagesConfigFactoryInterface" type="Magento\Catalog\Model\Product\Gallery\ImagesConfigFactory" /> <preference for="Magento\Catalog\Model\Product\Configuration\Item\ItemResolverInterface" type="Magento\Catalog\Model\Product\Configuration\Item\ItemResolverComposite" /> + <preference for="Magento\Catalog\Api\Data\MassActionInterface" type="\Magento\Catalog\Model\MassAction" /> <type name="Magento\Customer\Model\ResourceModel\Visitor"> <plugin name="catalogLog" type="Magento\Catalog\Model\Plugin\Log" /> </type> @@ -220,6 +221,12 @@ <item name="gif" xsi:type="string">gif</item> <item name="png" xsi:type="string">png</item> </argument> + <argument name="allowedMimeTypes" xsi:type="array"> + <item name="jpg" xsi:type="string">image/jpg</item> + <item name="jpeg" xsi:type="string">image/jpeg</item> + <item name="gif" xsi:type="string">image/gif</item> + <item name="png" xsi:type="string">image/png</item> + </argument> </arguments> </virtualType> <type name="Magento\Catalog\Controller\Adminhtml\Category\Image\Upload"> @@ -596,6 +603,13 @@ </argument> </arguments> </type> + <type name="Magento\Sales\Model\Order\ProductOption"> + <arguments> + <argument name="processorPool" xsi:type="array"> + <item name="custom_options" xsi:type="object">Magento\Catalog\Model\ProductOptionProcessor</item> + </argument> + </arguments> + </type> <type name="Magento\Framework\Model\Entity\RepositoryFactory"> <arguments> <argument name="entities" xsi:type="array"> @@ -657,12 +671,14 @@ <item name="mediaGalleryCreate" xsi:type="string">Magento\Catalog\Model\Product\Gallery\CreateHandler</item> <item name="categoryProductLinksSave" xsi:type="string">Magento\Catalog\Model\Category\Link\SaveHandler</item> <item name="websitePersistor" xsi:type="string">Magento\Catalog\Model\Product\Website\SaveHandler</item> + <item name="tierPriceCreator" xsi:type="string">Magento\Catalog\Model\Product\Attribute\Backend\TierPrice\SaveHandler</item> </item> <item name="update" xsi:type="array"> <item name="optionUpdater" xsi:type="string">Magento\Catalog\Model\Product\Option\SaveHandler</item> <item name="mediaGalleryUpdate" xsi:type="string">Magento\Catalog\Model\Product\Gallery\UpdateHandler</item> <item name="categoryProductLinksSave" xsi:type="string">Magento\Catalog\Model\Category\Link\SaveHandler</item> <item name="websitePersistor" xsi:type="string">Magento\Catalog\Model\Product\Website\SaveHandler</item> + <item name="tierPriceUpdater" xsi:type="string">Magento\Catalog\Model\Product\Attribute\Backend\TierPrice\UpdateHandler</item> </item> </item> </argument> diff --git a/app/code/Magento/Catalog/etc/frontend/di.xml b/app/code/Magento/Catalog/etc/frontend/di.xml index 55098037191e..ee9c5b29da89 100644 --- a/app/code/Magento/Catalog/etc/frontend/di.xml +++ b/app/code/Magento/Catalog/etc/frontend/di.xml @@ -116,4 +116,8 @@ <plugin name="get_catalog_category_product_index_table_name" type="Magento\Catalog\Model\Indexer\Category\Product\Plugin\TableResolver"/> <plugin name="get_catalog_product_price_index_table_name" type="Magento\Catalog\Model\Indexer\Product\Price\Plugin\TableResolver"/> </type> + <type name="Magento\Framework\App\Action\AbstractAction"> + <plugin name="catalog_app_action_dispatch_controller_context_plugin" + type="Magento\Catalog\Plugin\Framework\App\Action\ContextPlugin" /> + </type> </config> diff --git a/app/code/Magento/Catalog/etc/product_options.xsd b/app/code/Magento/Catalog/etc/product_options.xsd index 3bc24a909926..734c8f378d5d 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 6cc35fd7bee3..dec952bcf492 100644 --- a/app/code/Magento/Catalog/etc/product_types_base.xsd +++ b/app/code/Magento/Catalog/etc/product_types_base.xsd @@ -92,11 +92,11 @@ <xs:simpleType name="modelName"> <xs:annotation> <xs:documentation> - Model name can contain only [a-zA-Z_\\]. + Model name can contain only ([\\]?[a-zA-Z_][a-zA-Z0-9_]*)+. </xs:documentation> </xs:annotation> <xs:restriction base="xs:string"> - <xs:pattern value="[a-zA-Z_\\]+" /> + <xs:pattern value="([\\]?[a-zA-Z_][a-zA-Z0-9_]*)+" /> </xs:restriction> </xs:simpleType> </xs:schema> diff --git a/app/code/Magento/Catalog/etc/queue.xml b/app/code/Magento/Catalog/etc/queue.xml new file mode 100644 index 000000000000..137f34a5c1e2 --- /dev/null +++ b/app/code/Magento/Catalog/etc/queue.xml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/queue.xsd"> + <broker topic="product_action_attribute.update" exchange="magento-db" type="db"> + <queue name="product_action_attribute.update" consumer="product_action_attribute.update" consumerInstance="Magento\Framework\MessageQueue\Consumer" handler="Magento\Catalog\Model\Attribute\Backend\Consumer::process"/> + </broker> + <broker topic="product_action_attribute.website.update" exchange="magento-db" type="db"> + <queue name="product_action_attribute.website.update" consumer="product_action_attribute.website.update" consumerInstance="Magento\Framework\MessageQueue\Consumer" handler="Magento\Catalog\Model\Attribute\Backend\ConsumerWebsiteAssign::process"/> + </broker> +</config> \ No newline at end of file diff --git a/app/code/Magento/Catalog/etc/queue_consumer.xml b/app/code/Magento/Catalog/etc/queue_consumer.xml new file mode 100644 index 000000000000..d9e66ae69c10 --- /dev/null +++ b/app/code/Magento/Catalog/etc/queue_consumer.xml @@ -0,0 +1,11 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/consumer.xsd"> + <consumer name="product_action_attribute.update" queue="product_action_attribute.update" connection="db" maxMessages="5000" consumerInstance="Magento\Framework\MessageQueue\Consumer" handler="Magento\Catalog\Model\Attribute\Backend\Consumer::process" /> + <consumer name="product_action_attribute.website.update" queue="product_action_attribute.website.update" connection="db" maxMessages="5000" consumerInstance="Magento\Framework\MessageQueue\Consumer" handler="Magento\Catalog\Model\Attribute\Backend\ConsumerWebsiteAssign::process" /> +</config> \ No newline at end of file diff --git a/app/code/Magento/Catalog/etc/queue_publisher.xml b/app/code/Magento/Catalog/etc/queue_publisher.xml new file mode 100644 index 000000000000..1606ea42ec0b --- /dev/null +++ b/app/code/Magento/Catalog/etc/queue_publisher.xml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/publisher.xsd"> + <publisher topic="product_action_attribute.update"> + <connection name="db" exchange="magento-db" /> + </publisher> + <publisher topic="product_action_attribute.website.update"> + <connection name="db" exchange="magento-db" /> + </publisher> +</config> \ No newline at end of file diff --git a/app/code/Magento/Catalog/etc/queue_topology.xml b/app/code/Magento/Catalog/etc/queue_topology.xml new file mode 100644 index 000000000000..bdac891afbdb --- /dev/null +++ b/app/code/Magento/Catalog/etc/queue_topology.xml @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/topology.xsd"> + <exchange name="magento-db" type="topic" connection="db"> + <binding id="updateBinding" topic="product_action_attribute.update" destinationType="queue" destination="product_action_attribute.update"/> + <binding id="updateBindingWebsite" topic="product_action_attribute.website.update" destinationType="queue" destination="product_action_attribute.website.update"/> + </exchange> +</config> \ No newline at end of file diff --git a/app/code/Magento/Catalog/etc/webapi_rest/di.xml b/app/code/Magento/Catalog/etc/webapi_rest/di.xml index 2a5d60222e9f..44cdd473bf74 100644 --- a/app/code/Magento/Catalog/etc/webapi_rest/di.xml +++ b/app/code/Magento/Catalog/etc/webapi_rest/di.xml @@ -19,4 +19,7 @@ <plugin name="get_catalog_category_product_index_table_name" type="Magento\Catalog\Model\Indexer\Category\Product\Plugin\TableResolver"/> <plugin name="get_catalog_product_price_index_table_name" type="Magento\Catalog\Model\Indexer\Product\Price\Plugin\TableResolver"/> </type> + <type name="Magento\Catalog\Api\ProductCustomOptionRepositoryInterface"> + <plugin name="updateProductCustomOptionsAttributes" type="Magento\Catalog\Plugin\Model\Product\Option\UpdateProductCustomOptionsAttributes"/> + </type> </config> diff --git a/app/code/Magento/Catalog/etc/webapi_soap/di.xml b/app/code/Magento/Catalog/etc/webapi_soap/di.xml index 2a5d60222e9f..44cdd473bf74 100644 --- a/app/code/Magento/Catalog/etc/webapi_soap/di.xml +++ b/app/code/Magento/Catalog/etc/webapi_soap/di.xml @@ -19,4 +19,7 @@ <plugin name="get_catalog_category_product_index_table_name" type="Magento\Catalog\Model\Indexer\Category\Product\Plugin\TableResolver"/> <plugin name="get_catalog_product_price_index_table_name" type="Magento\Catalog\Model\Indexer\Product\Price\Plugin\TableResolver"/> </type> + <type name="Magento\Catalog\Api\ProductCustomOptionRepositoryInterface"> + <plugin name="updateProductCustomOptionsAttributes" type="Magento\Catalog\Plugin\Model\Product\Option\UpdateProductCustomOptionsAttributes"/> + </type> </config> diff --git a/app/code/Magento/Catalog/i18n/en_US.csv b/app/code/Magento/Catalog/i18n/en_US.csv index f2a3ab8f83f2..ed27dfd646cb 100644 --- a/app/code/Magento/Catalog/i18n/en_US.csv +++ b/app/code/Magento/Catalog/i18n/en_US.csv @@ -233,7 +233,6 @@ Products,Products "This attribute set no longer exists.","This attribute set no longer exists." "You saved the attribute set.","You saved the attribute set." "Something went wrong while saving the attribute set.","Something went wrong while saving the attribute set." -"You added product %1 to the comparison list.","You added product %1 to the comparison list." "You cleared the comparison list.","You cleared the comparison list." "Something went wrong clearing the comparison list.","Something went wrong clearing the comparison list." "You removed product %1 from the comparison list.","You removed product %1 from the comparison list." @@ -808,4 +807,5 @@ Details,Details "Product Name or SKU", "Product Name or SKU" "Start typing to find products", "Start typing to find products" "Product with ID: (%1) doesn't exist", "Product with ID: (%1) doesn't exist" -"Category with ID: (%1) doesn't exist", "Category with ID: (%1) doesn't exist" \ No newline at end of file +"Category with ID: (%1) doesn't exist", "Category with ID: (%1) doesn't exist" +"You added product %1 to the <a href=""%2"">comparison list</a>.","You added product %1 to the <a href=""%2"">comparison list</a>." \ No newline at end of file diff --git a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_crosssell.xml b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_crosssell.xml index 4a27158be5f7..d7aa058a1f44 100644 --- a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_crosssell.xml +++ b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_crosssell.xml @@ -5,6 +5,10 @@ * See COPYING.txt for license details. */ --> +<!-- +@deprecated Not used since cross-sell products grid moved to UI components. +@see Magento_Catalog::view/adminhtml/ui_component/crosssell_product_listing.xml +--> <layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd"> <container name="root" label="Root"> <block class="Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Crosssell" name="catalog.product.edit.tab.crosssell"/> diff --git a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_crosssellgrid.xml b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_crosssellgrid.xml index b5efecf0d03c..3ba4562c9d3d 100644 --- a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_crosssellgrid.xml +++ b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_crosssellgrid.xml @@ -5,6 +5,10 @@ * See COPYING.txt for license details. */ --> +<!-- +@deprecated Not used since cross-sell products grid moved to UI components. +@see Magento_Catalog::view/adminhtml/ui_component/crosssell_product_listing.xml +--> <layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd"> <container name="root" label="Root"> <block class="Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Crosssell" name="catalog.product.edit.tab.crosssell"/> diff --git a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_related.xml b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_related.xml index 1340d40ef4f9..c40c4a2818ef 100644 --- a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_related.xml +++ b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_related.xml @@ -5,6 +5,10 @@ * See COPYING.txt for license details. */ --> +<!-- +@deprecated Not used since related products grid moved to UI components. +@see Magento_Catalog::view/adminhtml/ui_component/related_product_listing.xml +--> <layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd"> <container name="root" label="Root"> <block class="Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Related" name="catalog.product.edit.tab.related"/> diff --git a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_relatedgrid.xml b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_relatedgrid.xml index 1ae83419ae64..38b791d88a00 100644 --- a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_relatedgrid.xml +++ b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_relatedgrid.xml @@ -5,6 +5,10 @@ * See COPYING.txt for license details. */ --> +<!-- +@deprecated Not used since related products grid moved to UI components. +@see Magento_Catalog::view/adminhtml/ui_component/related_product_listing.xml +--> <layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd"> <container name="root" label="Root"> <block class="Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Related" name="catalog.product.edit.tab.related"/> diff --git a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_set_block.xml b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_set_block.xml index 44884897461a..4e7396608826 100644 --- a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_set_block.xml +++ b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_set_block.xml @@ -11,7 +11,7 @@ <block class="Magento\Backend\Block\Widget\Grid" name="adminhtml.catalog.product.set.grid" as="grid"> <arguments> <argument name="id" xsi:type="string">setGrid</argument> - <argument name="dataSource" xsi:type="object">Magento\Eav\Model\ResourceModel\Entity\Attribute\Grid\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Eav\Model\ResourceModel\Entity\Attribute\Grid\Collection</argument> <argument name="default_sort" xsi:type="string">set_name</argument> <argument name="default_dir" xsi:type="string">ASC</argument> <argument name="save_parameters_in_session" xsi:type="string">1</argument> diff --git a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_upsell.xml b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_upsell.xml index a9770ed3c182..eea445041194 100644 --- a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_upsell.xml +++ b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_upsell.xml @@ -5,6 +5,10 @@ * See COPYING.txt for license details. */ --> +<!-- +@deprecated Not used since upsell products grid moved to UI components. +@see Magento_Catalog::view/adminhtml/ui_component/upsell_product_listing.xml +--> <layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd"> <container name="root" label="Root"> <block class="Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Upsell" name="catalog.product.edit.tab.upsell"/> diff --git a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_upsellgrid.xml b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_upsellgrid.xml index a4acf572caeb..2c400746c64f 100644 --- a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_upsellgrid.xml +++ b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_upsellgrid.xml @@ -5,6 +5,10 @@ * See COPYING.txt for license details. */ --> +<!-- +@deprecated Not used since upsell products grid moved to UI components. +@see Magento_Catalog::view/adminhtml/ui_component/upsell_product_listing.xml +--> <layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd"> <container name="root" label="Root"> <block class="Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Upsell" name="catalog.product.edit.tab.upsell"/> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/checkboxes/tree.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/checkboxes/tree.phtml index 00a1580923a7..ee67acd0ebd4 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/checkboxes/tree.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/checkboxes/tree.phtml @@ -20,7 +20,7 @@ "categoryCheckboxTree": { "dataUrl": "<?= $block->escapeUrl($block->getLoadTreeUrl()) ?>", "divId": "<?= /* @noEscape */ $divId ?>", - "rootVisible": <?= /* @noEscape */ $block->getRoot()->getIsVisible() ? 'true' : 'false' ?>, + "rootVisible": false, "useAjax": <?= $block->escapeHtml($block->getUseAjax()) ?>, "currentNodeId": <?= (int)$block->getCategoryId() ?>, "jsFormObject": "<?= /* @noEscape */ $block->getJsFormObject() ?>", @@ -28,7 +28,7 @@ "checked": "<?= $block->escapeHtml($block->getRoot()->getChecked()) ?>", "allowdDrop": <?= /* @noEscape */ $block->getRoot()->getIsVisible() ? 'true' : 'false' ?>, "rootId": <?= (int)$block->getRoot()->getId() ?>, - "expanded": <?= (int)$block->getIsWasExpanded() ?>, + "expanded": true, "categoryId": <?= (int)$block->getCategoryId() ?>, "treeJson": <?= /* @noEscape */ $block->getTreeJson() ?> } diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml index 9865589556e7..f448edc692ce 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml @@ -248,7 +248,7 @@ try { response = JSON.parse(transport.responseText); } catch (e) { - console.warn('An error occured while parsing response'); + console.warn('An error occurred while parsing response'); } if (!response || !response['parameters']) { @@ -302,6 +302,7 @@ } <?php endif;?> //updateContent(url); //commented since ajax requests replaced with http ones to load a category + jQuery('#tree-div').find('.x-tree-node-el').first().remove(); } jQuery(function () { diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/widget/tree.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/widget/tree.phtml index dbe66ef1aecd..69737b8a37c1 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/widget/tree.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/widget/tree.phtml @@ -160,7 +160,7 @@ jQuery(function() loader: categoryLoader, enableDD: false, containerScroll: true, - rootVisible: '<?= /* @escapeNotVerified */ $block->getRoot()->getIsVisible() ?>', + rootVisible: false, useAjax: true, currentNodeId: <?= (int) $block->getCategoryId() ?>, addNodeTo: false @@ -177,7 +177,7 @@ jQuery(function() text: 'Psw', draggable: false, id: <?= (int) $block->getRoot()->getId() ?>, - expanded: <?= (int) $block->getIsWasExpanded() ?>, + expanded: true, category_id: <?= (int) $block->getCategoryId() ?> }; diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/form.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/form.phtml index 74cf8f5f3a70..124194519b97 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/form.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/form.phtml @@ -21,13 +21,13 @@ <input name="form_key" type="hidden" value="<?= $block->escapeHtml($block->getFormKey()) ?>" /> <?= $block->getChildHtml('form') ?> </form> - - -<script> -require(['jquery', "mage/mage"], function(jQuery){ - - jQuery('#edit_form').mage('form').mage('validation', {validationUrl: '<?= /* @escapeNotVerified */ $block->getValidationUrl() ?>'}); - -}); +<script type="text/x-magento-init"> + { + "#edit_form": { + "Magento_Catalog/catalog/product/edit/attribute": { + "validationUrl": "<?= /* @escapeNotVerified */ $block->getValidationUrl() ?>" + } + } + } </script> <?= /* @escapeNotVerified */ $block->getFormScripts() ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml index 8a5f1919f78b..195ac9242271 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml @@ -40,13 +40,16 @@ function getFrontTab() { function checkOptionsPanelVisibility(){ if($('manage-options-panel')){ - var panel = $('manage-options-panel').up('.fieldset'); + var panel = $('manage-options-panel').up('.fieldset'), + activePanelClass = 'selected-type-options'; if($('frontend_input') && ($('frontend_input').value=='select' || $('frontend_input').value=='multiselect')){ panel.show(); + panel.addClass(activePanelClass); } else { panel.hide(); + panel.removeClass(activePanelClass); } } } @@ -55,7 +58,7 @@ function bindAttributeInputType() { checkOptionsPanelVisibility(); switchDefaultValueField(); - if($('frontend_input') && ($('frontend_input').value=='select' || $('frontend_input').value=='multiselect' || $('frontend_input').value=='price')){ + if($('frontend_input') && ($('frontend_input').value=='boolean' || $('frontend_input').value=='select' || $('frontend_input').value=='multiselect' || $('frontend_input').value=='price')){ if($('is_filterable') && !$('is_filterable').getAttribute('readonly')){ $('is_filterable').disabled = false; } @@ -331,7 +334,7 @@ if ($('is_required')) { jQuery(function($) { bindAttributeInputType(); - // @todo: refactor collapsable component + // @todo: refactor collapsible component $('.attribute-popup .collapse, [data-role="advanced_fieldset-content"]') .collapsable() .collapse('hide'); diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main.phtml index 54b945b48c10..9621b9a57168 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main.phtml @@ -80,7 +80,7 @@ // set the root node this.root = new Ext.tree.TreeNode({ text: 'ROOT', - allowDrug:false, + allowDrag:false, allowDrop:true, id:'1' }); @@ -188,6 +188,15 @@ for( j in config[i].children ) { if(config[i].children[j].id) { newNode = new Ext.tree.TreeNode(config[i].children[j]); + + if (typeof newNode.ui.onTextChange === 'function') { + newNode.ui.onTextChange = function (_3, _4, _5) { + if (this.rendered) { + this.textNode.innerText = _4; + } + } + } + } node.appendChild(newNode); newNode.addListener('click', editSet.unregister); } @@ -195,13 +204,20 @@ } } } - } - editSet = function() { - return { - register : function(node) { - editSet.currentNode = node; - }, + + editSet = function () { + return { + register: function (node) { + editSet.currentNode = node; + if (typeof node.ui.onTextChange === 'function') { + node.ui.onTextChange = function (_3, _4, _5) { + if (this.rendered) { + this.textNode.innerText = _4; + } + } + } + }, unregister : function() { editSet.currentNode = false; @@ -293,6 +309,14 @@ allowDrag : true }); + if (typeof newNode.ui.onTextChange === 'function') { + newNode.ui.onTextChange = function (_3, _4, _5) { + if (this.rendered) { + this.textNode.innerText = _4; + } + } + } + TreePanels.root.appendChild(newNode); newNode.addListener('beforemove', editSet.groupBeforeMove); newNode.addListener('beforeinsert', editSet.groupBeforeInsert); diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main/tree/attribute.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main/tree/attribute.phtml index 223b3e9888ee..75f04eae8215 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main/tree/attribute.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main/tree/attribute.phtml @@ -2,4 +2,4 @@ /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. - */; + */ diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/attribute.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/attribute.phtml index d1591d70945c..a3b0b32e4c29 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/attribute.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/attribute.phtml @@ -11,11 +11,12 @@ <form action="<?= /* @escapeNotVerified */ $block->getSaveUrl() ?>" method="post" id="attributes-edit-form" class="attributes-edit-form" enctype="multipart/form-data"> <?= $block->getBlockHtml('formkey') ?> </form> -<script> -require(['jquery', "mage/mage"], function(jQuery){ - - jQuery('#attributes-edit-form').mage('form') - .mage('validation', {validationUrl: '<?= /* @escapeNotVerified */ $block->getValidationUrl() ?>'}); - -}); +<script type="text/x-magento-init"> + { + "#attributes-edit-form": { + "Magento_Catalog/catalog/product/edit/attribute": { + "validationUrl": "<?= /* @escapeNotVerified */ $block->getValidationUrl() ?>" + } + } + } </script> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml index efc06d675c36..64c8ba7dcf49 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml @@ -30,6 +30,13 @@ }); </script> +<?php +$defaultMinSaleQty = $block->getDefaultConfigValue('min_sale_qty'); +if (!is_numeric($defaultMinSaleQty)) { + $defaultMinSaleQty = json_decode($defaultMinSaleQty, true); + $defaultMinSaleQty = (float) $defaultMinSaleQty[\Magento\Customer\Api\Data\GroupInterface::CUST_GROUP_ALL] ?? 1; +} +?> <div class="fieldset-wrapper form-inline advanced-inventory-edit"> <div class="fieldset-wrapper-title"> <strong class="title"> @@ -132,7 +139,7 @@ <div class="field"> <input type="text" class="input-text validate-number" id="inventory_min_sale_qty" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[min_sale_qty]" - value="<?= /* @escapeNotVerified */ $block->getDefaultConfigValue('min_sale_qty') * 1 ?>" + value="<?= /* @escapeNotVerified */ $defaultMinSaleQty ?>" disabled="disabled"/> </div> <div class="field choice"> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/attribute_set.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/attribute_set.phtml index 6a5f6c464849..a7e8564e7a1d 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/attribute_set.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/attribute_set.phtml @@ -14,7 +14,7 @@ <% } %> <ul data-mage-init='{"menu":[]}'> <% _.each(data.items, function(value) { %> - <li <%- data.optionData(value) %>><a href="#"><%- value.label %></a></li> + <li <%= data.optionData(value) %>><a href="#"><%- value.label %></a></li> <% }); %> </ul> <% if (!data.term && data.items.length && !data.allShown()) { %> diff --git a/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml b/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml index dafea71f872d..90d6e0b48400 100644 --- a/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml +++ b/app/code/Magento/Catalog/view/adminhtml/ui_component/category_form.xml @@ -77,6 +77,13 @@ <dataType>text</dataType> </settings> </field> + <field name="level" formElement="hidden"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="source" xsi:type="string">category</item> + </item> + </argument> + </field> <field name="store_id" formElement="hidden"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> @@ -331,7 +338,6 @@ <item name="type" xsi:type="string">group</item> <item name="config" xsi:type="array"> <item name="breakLine" xsi:type="boolean">true</item> - <item name="required" xsi:type="boolean">true</item> </item> </argument> <field name="filter_price_range" formElement="input"> @@ -341,6 +347,9 @@ </item> </argument> <settings> + <validation> + <rule name="required-entry" xsi:type="boolean">true</rule> + </validation> <additionalClasses> <class name="admin__field-small">true</class> </additionalClasses> diff --git a/app/code/Magento/Catalog/view/adminhtml/ui_component/design_config_form.xml b/app/code/Magento/Catalog/view/adminhtml/ui_component/design_config_form.xml index 1e6082392977..cb0beb67c271 100644 --- a/app/code/Magento/Catalog/view/adminhtml/ui_component/design_config_form.xml +++ b/app/code/Magento/Catalog/view/adminhtml/ui_component/design_config_form.xml @@ -18,7 +18,7 @@ <level>2</level> <label translate="true">Base</label> </settings> - <field name="watermark_image_image" formElement="fileUploader"> + <field name="watermark_image_image" formElement="imageUploader"> <settings> <notice translate="true">Allowed file types: jpeg, gif, png.</notice> <label translate="true">Image</label> @@ -78,7 +78,7 @@ <level>2</level> <label translate="true">Thumbnail</label> </settings> - <field name="watermark_thumbnail_image" formElement="fileUploader"> + <field name="watermark_thumbnail_image" formElement="imageUploader"> <settings> <label translate="true">Image</label> <componentType>imageUploader</componentType> @@ -137,7 +137,7 @@ <level>2</level> <label translate="true">Small</label> </settings> - <field name="watermark_small_image_image" formElement="fileUploader"> + <field name="watermark_small_image_image" formElement="imageUploader"> <settings> <label translate="true">Image</label> <componentType>imageUploader</componentType> diff --git a/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml b/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml index 65090fa3ac46..d689daef4bca 100644 --- a/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml +++ b/app/code/Magento/Catalog/view/adminhtml/ui_component/product_listing.xml @@ -132,6 +132,7 @@ <settings> <addField>true</addField> <filter>text</filter> + <bodyTmpl>ui/grid/cells/html</bodyTmpl> <label translate="true">Name</label> </settings> </column> @@ -154,6 +155,7 @@ <column name="sku" sortOrder="60"> <settings> <filter>text</filter> + <bodyTmpl>ui/grid/cells/html</bodyTmpl> <label translate="true">SKU</label> </settings> </column> @@ -190,6 +192,13 @@ <label translate="true">Websites</label> </settings> </column> + <column name="cost" class="Magento\Catalog\Ui\Component\Listing\Columns\Price" sortOrder="120"> + <settings> + <addField>true</addField> + <filter>textRange</filter> + <label translate="true">Cost</label> + </settings> + </column> <actionsColumn name="actions" class="Magento\Catalog\Ui\Component\Listing\Columns\ProductActions" sortOrder="200"> <settings> <indexField>entity_id</indexField> diff --git a/app/code/Magento/Catalog/view/adminhtml/web/catalog/category/edit.js b/app/code/Magento/Catalog/view/adminhtml/web/catalog/category/edit.js index 75ee3019cf4b..41f7a874c26f 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/catalog/category/edit.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/catalog/category/edit.js @@ -82,7 +82,7 @@ define([ return function (config, element) { config = config || {}; jQuery(element).on('click', function () { - categorySubmit(config.url, config.ajax); + categorySubmit(); }); }; }); diff --git a/app/code/Magento/Catalog/view/adminhtml/web/catalog/category/form.js b/app/code/Magento/Catalog/view/adminhtml/web/catalog/category/form.js index 0a04358e4112..76aaddf55ac9 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/catalog/category/form.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/catalog/category/form.js @@ -15,6 +15,7 @@ define([ categoryIdSelector: 'input[name="id"]', categoryPathSelector: 'input[name="path"]', categoryParentSelector: 'input[name="parent"]', + categoryLevelSelector: 'input[name="level"]', refreshUrl: config.refreshUrl }, @@ -47,6 +48,7 @@ define([ $(this.options.categoryIdSelector).val(data.id).change(); $(this.options.categoryPathSelector).val(data.path).change(); $(this.options.categoryParentSelector).val(data.parentId).change(); + $(this.options.categoryLevelSelector).val(data.level).change(); } } }; diff --git a/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/composite/configure.js b/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/composite/configure.js index 6903a17bcdcc..1ac2a4ffadaa 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/composite/configure.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/composite/configure.js @@ -91,8 +91,8 @@ define([ /** * Add product list types as scope and their urls - * expamle: addListType('product_to_add', {urlFetch: 'http://magento...'}) - * expamle: addListType('wishlist', {urlSubmit: 'http://magento...'}) + * example: addListType('product_to_add', {urlFetch: 'http://magento...'}) + * example: addListType('wishlist', {urlSubmit: 'http://magento...'}) * * @param type types as scope * @param urls obj can be @@ -112,7 +112,7 @@ define([ /** * Adds complex list type - that is used to submit several list types at once * Only urlSubmit is possible for this list type - * expamle: addComplexListType(['wishlist', 'product_list'], 'http://magento...') + * example: addComplexListType(['wishlist', 'product_list'], 'http://magento...') * * @param type types as scope * @param urls obj can be @@ -469,26 +469,6 @@ define([ } }, - /** - * toggles Selects states (for IE) except those to be shown in popup - */ - /*_toggleSelectsExceptBlock: function(flag) { - if(Prototype.Browser.IE){ - if (this.blockForm) { - var states = new Array; - var selects = this.blockForm.getElementsByTagName("select"); - for(var i=0; i<selects.length; i++){ - states[i] = selects[i].style.visibility - } - } - if (this.blockForm) { - for(i=0; i<selects.length; i++){ - selects[i].style.visibility = states[i] - } - } - } - },*/ - /** * Close configuration window */ 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 new file mode 100644 index 000000000000..e1923dc46d68 --- /dev/null +++ b/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/edit/attribute.js @@ -0,0 +1,18 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'mage/mage', + 'validation' +], function ($) { + 'use strict'; + + return function (config, element) { + $(element).mage('form').validation({ + validationUrl: config.validationUrl + }); + }; +}); diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/category-checkbox-tree.js b/app/code/Magento/Catalog/view/adminhtml/web/js/category-checkbox-tree.js index 2359d1499f83..0ec404a769f4 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/js/category-checkbox-tree.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/category-checkbox-tree.js @@ -225,6 +225,7 @@ define([ categoryLoader.on('beforeload', function (treeLoader, node) { treeLoader.baseParams.id = node.attributes.id; + treeLoader.baseParams.selected = options.jsFormObject.updateElement.value; }); categoryLoader.on('load', function () { diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/edit-tree.js b/app/code/Magento/Catalog/view/adminhtml/web/js/edit-tree.js index 3544767b8d77..7c947195f331 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/js/edit-tree.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/edit-tree.js @@ -18,7 +18,10 @@ require([ /** * Delete some category * This routine get categoryId explicitly, so even if currently selected tree node is out of sync - * with this form, we surely delete same category in the tree and at backend + * with this form, we surely delete same category in the tree and at backend. + * + * @deprecated + * @see deleteConfirm */ function categoryDelete(url) { confirm({ diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/form/element/action-delete.js b/app/code/Magento/Catalog/view/adminhtml/web/js/form/element/action-delete.js index 97f978de47b6..f829c66c4011 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/js/form/element/action-delete.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/form/element/action-delete.js @@ -5,10 +5,10 @@ define([ 'underscore', 'Magento_Ui/js/form/element/abstract' -], function (_, Acstract) { +], function (_, Abstract) { 'use strict'; - return Acstract.extend({ + return Abstract.extend({ defaults: { prefixName: '', prefixElementName: '', diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/form/element/input.js b/app/code/Magento/Catalog/view/adminhtml/web/js/form/element/input.js index 2f6703cc92ea..4bbdea066b76 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/js/form/element/input.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/form/element/input.js @@ -5,10 +5,10 @@ define([ 'underscore', 'Magento_Ui/js/form/element/abstract' -], function (_, Acstract) { +], function (_, Abstract) { 'use strict'; - return Acstract.extend({ + return Abstract.extend({ defaults: { prefixName: '', prefixElementName: '', diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/options.js b/app/code/Magento/Catalog/view/adminhtml/web/js/options.js index 6ea005915763..7adc0dcfdf40 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/js/options.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/options.js @@ -20,7 +20,6 @@ define([ return function (config) { var optionPanel = jQuery('#manage-options-panel'), - optionsValues = [], editForm = jQuery('#edit_form'), attributeOption = { table: $('attribute-options-table'), @@ -145,7 +144,9 @@ define([ return optionDefaultInputType; } - }; + }, + tableBody = jQuery(), + activePanelClass = 'selected-type-options'; if ($('add_new_option_button')) { Event.observe('add_new_option_button', 'click', attributeOption.add.bind(attributeOption, {}, true)); @@ -180,30 +181,32 @@ define([ }); }); } - editForm.on('submit', function () { - optionPanel.find('input') - .each(function () { - if (this.disabled) { - return; + editForm.on('beforeSubmit', function () { + var optionContainer = optionPanel.find('table tbody'), + optionsValues; + + if (optionPanel.hasClass(activePanelClass)) { + optionsValues = jQuery.map( + optionContainer.find('tr'), + function (row) { + return jQuery(row).find('input, select, textarea').serialize(); } - - if (this.type === 'checkbox' || this.type === 'radio') { - if (this.checked) { - optionsValues.push(this.name + '=' + jQuery(this).val()); - } - } else { - optionsValues.push(this.name + '=' + jQuery(this).val()); - } - }); - jQuery('<input>') - .attr({ - type: 'hidden', - name: 'serialized_options' - }) - .val(JSON.stringify(optionsValues)) - .prependTo(editForm); - optionPanel.find('table') - .replaceWith(jQuery('<div>').text(jQuery.mage.__('Sending attribute values as package.'))); + ); + jQuery('<input>') + .attr({ + type: 'hidden', + name: 'serialized_options' + }) + .val(JSON.stringify(optionsValues)) + .prependTo(editForm); + } + tableBody = optionContainer.detach(); + }); + editForm.on('afterValidate.error highlight.validate', function () { + if (optionPanel.hasClass(activePanelClass)) { + optionPanel.find('table').append(tableBody); + jQuery('input[name="serialized_options"]').remove(); + } }); window.attributeOption = attributeOption; window.optionDefaultInputType = attributeOption.getOptionInputType(); diff --git a/app/code/Magento/Catalog/view/base/templates/product/composite/fieldset/options/view/checkable.phtml b/app/code/Magento/Catalog/view/base/templates/product/composite/fieldset/options/view/checkable.phtml new file mode 100644 index 000000000000..0f3b4f481a28 --- /dev/null +++ b/app/code/Magento/Catalog/view/base/templates/product/composite/fieldset/options/view/checkable.phtml @@ -0,0 +1,100 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +// @codingStandardsIgnoreFile +use Magento\Catalog\Model\Product\Option; + +/** + * @var \Magento\Catalog\Block\Product\View\Options\View\Checkable $block + */ +$option = $block->getOption(); +if ($option) : ?> + <?php + $configValue = $block->getPreconfiguredValue($option); + $optionType = $option->getType(); + $arraySign = $optionType === Option::OPTION_TYPE_CHECKBOX ? '[]' : ''; + $count = 1; + ?> + +<div class="options-list nested" id="options-<?php echo /* @noEscape */ +$option->getId() ?>-list"> + <?php if ($optionType === Option::OPTION_TYPE_RADIO && !$option->getIsRequire()): ?> + <div class="field choice admin__field admin__field-option"> + <input type="radio" + id="options_<?php echo /* @noEscape */ + $option->getId() ?>" + class="radio admin__control-radio product-custom-option" + name="options[<?php echo /* @noEscape */ + $option->getId() ?>]" + data-selector="options[<?php echo /* @noEscape */ + $option->getId() ?>]" + onclick="<?php echo $block->getSkipJsReloadPrice() ? '' : 'opConfig.reloadPrice()' ?>" + value="" + checked="checked" + /> + <label class="label admin__field-label" for="options_<?php echo /* @noEscape */ + $option->getId() ?>"> + <span> + <?php echo /* @noEscape */ + __('None') ?> + </span> + </label> + </div> +<?php endif; ?> + + <?php foreach ($option->getValues() as $value) : ?> + <?php + $checked = ''; + $count++; + if ($arraySign) { + $checked = is_array($configValue) && in_array($value->getOptionTypeId(), $configValue) ? 'checked' : ''; + } else { + $checked = $configValue == $value->getOptionTypeId() ? 'checked' : ''; + } + $dataSelector = 'options[' . $option->getId() . ']'; + if ($arraySign) { + $dataSelector .= '[' . $value->getOptionTypeId() . ']'; + } + ?> + + <div class="field choice admin__field admin__field-option <?php echo /* @noEscape */ + $option->getIsRequire() ? 'required': '' ?>"> + <input type="<?php echo /* @noEscape */ + $optionType ?>" + class="<?php /** @noinspection DisconnectedForeachInstructionInspection */ + echo /* @noEscape */ + $optionType === Option::OPTION_TYPE_RADIO ? + 'radio admin__control-radio' : + 'checkbox admin__control-checkbox' ?> <?php echo /* @noEscape */ + $option->getIsRequire() ? 'required': '' ?> + product-custom-option + <?php echo $block->getSkipJsReloadPrice() ? '' : 'opConfig.reloadPrice()' ?>" + name="options[<?php echo $option->getId() ?>]<?php echo /* @noEscape */ + $arraySign ?>" + id="options_<?php echo /* @noEscape */ + $option->getId() . '_' . $count ?>" + value="<?php echo /* @noEscape */ + $value->getOptionTypeId() ?>" + <?php echo /* @noEscape */ + $checked ?> + data-selector="<?php echo /* @noEscape */ + $dataSelector ?>" + price="<?php echo /* @noEscape */ + $block->getCurrencyByStore($value) ?>" + /> + <label class="label admin__field-label" + for="options_<?php echo /* @noEscape */ + $option->getId() . '_' . $count ?>"> + <span> + <?php echo $block->escapeHtml($value->getTitle()) ?> + </span> + <?php echo /* @noEscape */ + $block->formatPrice($value) ?> + </label> + </div> + <?php endforeach; ?> + </div> +<?php endif; ?> \ No newline at end of file diff --git a/app/code/Magento/Catalog/view/base/web/js/price-utils.js b/app/code/Magento/Catalog/view/base/web/js/price-utils.js index e2ea42f7d5fe..7b83d12cc980 100644 --- a/app/code/Magento/Catalog/view/base/web/js/price-utils.js +++ b/app/code/Magento/Catalog/view/base/web/js/price-utils.js @@ -60,7 +60,7 @@ define([ pattern = pattern.indexOf('{sign}') < 0 ? s + pattern : pattern.replace('{sign}', s); // we're avoiding the usage of to fixed, and using round instead with the e representation to address - // numbers like 1.005 = 1.01. Using ToFixed to only provide trailig zeroes in case we have a whole number + // numbers like 1.005 = 1.01. Using ToFixed to only provide trailing zeroes in case we have a whole number i = parseInt( amount = Number(Math.round(Math.abs(+amount || 0) + 'e+' + precision) + ('e-' + precision)), 10 diff --git a/app/code/Magento/Catalog/view/frontend/layout/catalog_product_view.xml b/app/code/Magento/Catalog/view/frontend/layout/catalog_product_view.xml index 3630fddb326a..13e2d998f6cd 100644 --- a/app/code/Magento/Catalog/view/frontend/layout/catalog_product_view.xml +++ b/app/code/Magento/Catalog/view/frontend/layout/catalog_product_view.xml @@ -91,7 +91,11 @@ <container name="product.info.social" label="Product social links container" htmlTag="div" htmlClass="product-social-links"> <block class="Magento\Catalog\Block\Product\View" name="product.info.addto" as="addto" template="Magento_Catalog::product/view/addto.phtml"> <block class="Magento\Catalog\Block\Product\View\AddTo\Compare" name="view.addto.compare" after="view.addto.wishlist" - template="Magento_Catalog::product/view/addto/compare.phtml" /> + template="Magento_Catalog::product/view/addto/compare.phtml" > + <arguments> + <argument name="addToCompareViewModel" xsi:type="object">Magento\Catalog\ViewModel\Product\Checker\AddToCompareAvailability</argument> + </arguments> + </block> </block> <block class="Magento\Catalog\Block\Product\View" name="product.info.mailto" template="Magento_Catalog::product/view/mailto.phtml"/> </container> @@ -121,7 +125,12 @@ </arguments> </block> </container> - <block class="Magento\Catalog\Block\Product\View\Gallery" name="product.info.media.image" template="Magento_Catalog::product/view/gallery.phtml"/> + <block class="Magento\Catalog\Block\Product\View\Gallery" name="product.info.media.image" template="Magento_Catalog::product/view/gallery.phtml"> + <arguments> + <argument name="gallery_options" xsi:type="object">Magento\Catalog\Block\Product\View\GalleryOptions</argument> + <argument name="imageHelper" xsi:type="object">Magento\Catalog\Helper\Image</argument> + </arguments> + </block> <container name="skip_gallery_after.wrapper" htmlTag="div" htmlClass="action-skip-wrapper"> <block class="Magento\Framework\View\Element\Template" after="product.info.media.image" name="skip_gallery_after" template="Magento_Theme::html/skip.phtml"> <arguments> @@ -136,7 +145,7 @@ </arguments> </block> </container> - <block class="Magento\Catalog\Block\Product\View\Description" name="product.info.details" template="Magento_Catalog::product/view/details.phtml" after="product.info.media"> + <block class="Magento\Catalog\Block\Product\View\Details" name="product.info.details" template="Magento_Catalog::product/view/details.phtml" after="product.info.media"> <block class="Magento\Catalog\Block\Product\View\Description" name="product.info.description" as="description" template="Magento_Catalog::product/view/attribute.phtml" group="detailed_info"> <arguments> <argument name="at_call" xsi:type="string">getDescription</argument> @@ -144,11 +153,13 @@ <argument name="css_class" xsi:type="string">description</argument> <argument name="at_label" xsi:type="string">none</argument> <argument name="title" translate="true" xsi:type="string">Details</argument> + <argument name="sort_order" xsi:type="string">10</argument> </arguments> </block> <block class="Magento\Catalog\Block\Product\View\Attributes" name="product.attributes" as="additional" template="Magento_Catalog::product/view/attributes.phtml" group="detailed_info"> <arguments> <argument translate="true" name="title" xsi:type="string">More Information</argument> + <argument name="sort_order" xsi:type="string">20</argument> </arguments> </block> </block> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml index 949d365e7899..7daf04998036 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/compare/list.phtml @@ -116,7 +116,9 @@ <?php $block->getImage($item, 'product_small_image')->toHtml(); ?> <?php break; default: ?> - <?= /* @escapeNotVerified */ $helper->productAttribute($item, $block->getProductAttributeValue($item, $attribute), $attribute->getAttributeCode()) ?> + <?php if (is_string($block->getProductAttributeValue($item, $attribute))): ?> + <?= /* @escapeNotVerified */ $helper->productAttribute($item, $block->getProductAttributeValue($item, $attribute), $attribute->getAttributeCode()) ?> + <?php endif; ?> <?php break; } ?> </div> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml index 74a0b2d7cf1a..8a907bd54aa6 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml @@ -10,7 +10,7 @@ style="width:<?= /* @escapeNotVerified */ $block->getWidth() ?>px;"> <span class="product-image-wrapper" style="padding-bottom: <?= /* @escapeNotVerified */ ($block->getRatio() * 100) ?>%;"> - <img class="product-image-photo" + <img class="<?= /* @escapeNotVerified */ $block->getClass() ?>" <?= /* @escapeNotVerified */ $block->getCustomAttributes() ?> src="<?= /* @escapeNotVerified */ $block->getImageUrl() ?>" max-width="<?= /* @escapeNotVerified */ $block->getWidth() ?>" diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml index f43440234608..ecc9700802d2 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml @@ -169,7 +169,7 @@ switch ($type = $block->getType()) { <?php if ($type == 'related' && $canItemsAddToCart): ?> <div class="block-actions"> <?= /* @escapeNotVerified */ __('Check items to add to the cart or') ?> - <button type="button" class="action select" role="select-all"><span><?= /* @escapeNotVerified */ __('select all') ?></span></button> + <button type="button" class="action select" role="button"><span><?= /* @escapeNotVerified */ __('select all') ?></span></button> </div> <?php endif; ?> <div class="products wrapper grid products-grid products-<?= /* @escapeNotVerified */ $type ?>"> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/addto/compare.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/addto/compare.phtml index adf0f44d0c83..194a472d81d5 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/addto/compare.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/addto/compare.phtml @@ -9,6 +9,10 @@ /** @var $block \Magento\Catalog\Block\Product\View\Addto\Compare */ ?> +<?php $viewModel = $block->getData('addToCompareViewModel'); ?> +<?php if ($viewModel->isAvailableForCompare($block->getProduct())): ?> <a href="#" data-post='<?= /* @escapeNotVerified */ $block->getPostDataParams() ?>' data-role="add-to-links" class="action tocompare"><span><?= /* @escapeNotVerified */ __('Add to Compare') ?></span></a> +<?php endif; ?> + diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/addtocart.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/addtocart.phtml index 9c18a18ff583..71452a2d65e9 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/addtocart.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/addtocart.phtml @@ -20,6 +20,7 @@ <input type="number" name="qty" id="qty" + min="0" value="<?= /* @escapeNotVerified */ $block->getProductDefaultQty() * 1 ?>" title="<?= /* @escapeNotVerified */ __('Qty') ?>" class="input-text qty" @@ -32,7 +33,7 @@ <button type="submit" title="<?= /* @escapeNotVerified */ $buttonTitle ?>" class="action primary tocart" - id="product-addtocart-button"> + id="product-addtocart-button" disabled> <span><?= /* @escapeNotVerified */ $buttonTitle ?></span> </button> <?= $block->getChildHtml('', true) ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/attributes.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/attributes.phtml index c930d2195a01..1c4a37fedebe 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/attributes.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/attributes.phtml @@ -23,8 +23,8 @@ <tbody> <?php foreach ($_additional as $_data): ?> <tr> - <th class="col label" scope="row"><?= $block->escapeHtml(__($_data['label'])) ?></th> - <td class="col data" data-th="<?= $block->escapeHtml(__($_data['label'])) ?>"><?= /* @escapeNotVerified */ $_helper->productAttribute($_product, $_data['value'], $_data['code']) ?></td> + <th class="col label" scope="row"><?= $block->escapeHtml($_data['label']) ?></th> + <td class="col data" data-th="<?= $block->escapeHtml($_data['label']) ?>"><?= /* @escapeNotVerified */ $_helper->productAttribute($_product, $_data['value'], $_data['code']) ?></td> </tr> <?php endforeach; ?> </tbody> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/details.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/details.phtml index b1af46b80552..57eabbf1d8c8 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/details.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/details.phtml @@ -6,8 +6,9 @@ // @codingStandardsIgnoreFile +/** @var \Magento\Catalog\Block\Product\View\Details $block */ ?> -<?php if ($detailedInfoGroup = $block->getGroupChildNames('detailed_info', 'getChildHtml')):?> +<?php if ($detailedInfoGroup = $block->getGroupSortedChildNames('detailed_info', 'getChildHtml')):?> <div class="product info detailed"> <?php $layout = $block->getLayout(); ?> <div class="product data items" data-mage-init='{"tabs":{"openedState":"active"}}'> @@ -21,17 +22,17 @@ $label = $block->getChildData($alias, 'title'); ?> <div class="data item title" - aria-labeledby="tab-label-<?= /* @escapeNotVerified */ $alias ?>-title" data-role="collapsible" id="tab-label-<?= /* @escapeNotVerified */ $alias ?>"> <a class="data switch" tabindex="-1" - data-toggle="switch" + data-toggle="trigger" href="#<?= /* @escapeNotVerified */ $alias ?>" id="tab-label-<?= /* @escapeNotVerified */ $alias ?>-title"> <?= /* @escapeNotVerified */ $label ?> </a> </div> - <div class="data item content" id="<?= /* @escapeNotVerified */ $alias ?>" data-role="content"> + <div class="data item content" + aria-labelledby="tab-label-<?= /* @escapeNotVerified */ $alias ?>-title" id="<?= /* @escapeNotVerified */ $alias ?>" data-role="content"> <?= /* @escapeNotVerified */ $html ?> </div> <?php endforeach;?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/gallery.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/gallery.phtml index 1bfa30478df8..1f06b90758d0 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/gallery.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/gallery.phtml @@ -12,32 +12,32 @@ * @var $block \Magento\Catalog\Block\Product\View\Gallery */ ?> -<div class="gallery-placeholder _block-content-loading" data-gallery-role="gallery-placeholder"> - <div data-role="loader" class="loading-mask"> - <div class="loader"> - <img src="<?= /* @escapeNotVerified */ $block->getViewFileUrl('images/loader-1.gif') ?>" - alt="<?= /* @escapeNotVerified */ __('Loading...') ?>"> - </div> - </div> -</div> -<!--Fix for jumping content. Loader must be the same size as gallery.--> -<script> - var config = { - "width": <?= /* @escapeNotVerified */ $block->getImageAttribute('product_page_image_medium', 'width') ?>, - "thumbheight": <?php /* @escapeNotVerified */ echo $block->getImageAttribute('product_page_image_small', 'height') - ?: $block->getImageAttribute('product_page_image_small', 'width'); ?>, - "navtype": "<?= /* @escapeNotVerified */ $block->getVar("gallery/navtype") ?>", - "height": <?= /* @escapeNotVerified */ $block->getImageAttribute('product_page_image_medium', 'height') ?> - }, - thumbBarHeight = 0, - loader = document.querySelectorAll('[data-gallery-role="gallery-placeholder"] [data-role="loader"]')[0]; - if (config.navtype === 'horizontal') { - thumbBarHeight = config.thumbheight; +<?php + $images = $block->getGalleryImages()->getItems(); + $mainImage = current(array_filter($images, function ($img) use ($block) { + return $block->isMainImage($img); + })); + + if (!empty($images) && empty($mainImage)) { + $mainImage = $block->getGalleryImages()->getFirstItem(); } - loader.style.paddingBottom = ( config.height / config.width * 100) + "%"; -</script> + $helper = $block->getData('imageHelper'); + $mainImageData = $mainImage ? + $mainImage->getData('medium_image_url') : + $helper->getDefaultPlaceholderUrl('image'); + +?> + +<div class="gallery-placeholder _block-content-loading" data-gallery-role="gallery-placeholder"> + <img + alt="main product photo" + class="gallery-placeholder__image" + src="<?= /* @noEscape */ $mainImageData ?>" + /> +</div> + <script type="text/x-magento-init"> { "[data-gallery-role=gallery-placeholder]": { @@ -45,44 +45,8 @@ "mixins":["magnifier/magnify"], "magnifierOpts": <?= /* @escapeNotVerified */ $block->getMagnifier() ?>, "data": <?= /* @escapeNotVerified */ $block->getGalleryImagesJson() ?>, - "options": { - "nav": "<?= /* @escapeNotVerified */ $block->getVar("gallery/nav") ?>", - "loop": <?= /* @escapeNotVerified */ $block->getVar("gallery/loop") ? 'true' : 'false' ?>, - "keyboard": <?= /* @escapeNotVerified */ $block->getVar("gallery/keyboard") ? 'true' : 'false' ?>, - "arrows": <?= /* @escapeNotVerified */ $block->getVar("gallery/arrows") ? 'true' : 'false' ?>, - "allowfullscreen": <?= /* @escapeNotVerified */ $block->getVar("gallery/allowfullscreen") ? 'true' : 'false' ?>, - "showCaption": <?= /* @escapeNotVerified */ $block->getVar("gallery/caption") ? 'true' : 'false' ?>, - "width": "<?= /* @escapeNotVerified */ $block->getImageAttribute('product_page_image_medium', 'width') ?>", - "thumbwidth": "<?= /* @escapeNotVerified */ $block->getImageAttribute('product_page_image_small', 'width') ?>", - <?php if ($block->getImageAttribute('product_page_image_small', 'height') || $block->getImageAttribute('product_page_image_small', 'width')): ?> - "thumbheight": <?php /* @escapeNotVerified */ echo $block->getImageAttribute('product_page_image_small', 'height') - ?: $block->getImageAttribute('product_page_image_small', 'width'); ?>, - <?php endif; ?> - <?php if ($block->getImageAttribute('product_page_image_medium', 'height') || $block->getImageAttribute('product_page_image_medium', 'width')): ?> - "height": <?php /* @escapeNotVerified */ echo $block->getImageAttribute('product_page_image_medium', 'height') - ?: $block->getImageAttribute('product_page_image_medium', 'width'); ?>, - <?php endif; ?> - <?php if ($block->getVar("gallery/transition/duration")): ?> - "transitionduration": <?= /* @escapeNotVerified */ $block->getVar("gallery/transition/duration") ?>, - <?php endif; ?> - "transition": "<?= /* @escapeNotVerified */ $block->getVar("gallery/transition/effect") ?>", - "navarrows": <?= /* @escapeNotVerified */ $block->getVar("gallery/navarrows") ? 'true' : 'false' ?>, - "navtype": "<?= /* @escapeNotVerified */ $block->getVar("gallery/navtype") ?>", - "navdir": "<?= /* @escapeNotVerified */ $block->getVar("gallery/navdir") ?>" - }, - "fullscreen": { - "nav": "<?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/nav") ?>", - "loop": <?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/loop") ? 'true' : 'false' ?>, - "navdir": "<?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/navdir") ?>", - "navarrows": <?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/navarrows") ? 'true' : 'false' ?>, - "navtype": "<?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/navtype") ?>", - "arrows": <?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/arrows") ? 'true' : 'false' ?>, - "showCaption": <?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/caption") ? 'true' : 'false' ?>, - <?php if ($block->getVar("gallery/fullscreen/transition/duration")): ?> - "transitionduration": <?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/transition/duration") ?>, - <?php endif; ?> - "transition": "<?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/transition/effect") ?>" - }, + "options": <?= /* @noEscape */ $block->getGalleryOptions()->getOptionsJson() ?>, + "fullscreen": <?= /* @noEscape */ $block->getGalleryOptions()->getFSOptionsJson() ?>, "breakpoints": <?= /* @escapeNotVerified */ $block->getBreakpoints() ?> } } diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/general.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/general.phtml index a2b91a5eeb99..40f86c7e68d6 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/templates/product/widget/new/column/new_default_list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_default_list.phtml index a4ae3ba90798..45a206f3f92b 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_default_list.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/column/new_default_list.phtml @@ -33,7 +33,7 @@ <div class="product-item-actions"> <div class="actions-primary"> <?php if ($_product->isSaleable()): ?> - <?php if ($_product->getTypeInstance()->hasRequiredOptions($_product)): ?> + <?php if (!$_product->getTypeInstance()->isPossibleBuyFromList($_product)): ?> <button type="button" title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>" class="action tocart primary" data-mage-init='{"redirectUrl":{"url":"<?= /* @escapeNotVerified */ $block->getAddToCartUrl($_product) ?>"}}'> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_grid.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_grid.phtml index 11bbfea1ac8e..93542c4c9095 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_grid.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_grid.phtml @@ -66,7 +66,7 @@ if ($exist = ($block->getProductCollection() && $block->getProductCollection()-> <?php if ($showCart): ?> <div class="actions-primary"> <?php if ($_item->isSaleable()): ?> - <?php if ($_item->getTypeInstance()->hasRequiredOptions($_item)): ?> + <?php if (!$_item->getTypeInstance()->isPossibleBuyFromList($_item)): ?> <button class="action tocart primary" data-mage-init='{"redirectUrl":{"url":"<?= /* @escapeNotVerified */ $block->getAddToCartUrl($_item) ?>"}}' type="button" title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>"> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_list.phtml index 615cd13fb6d3..ad75a3a6f074 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_list.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/widget/new/content/new_list.phtml @@ -65,7 +65,7 @@ if ($exist = ($block->getProductCollection() && $block->getProductCollection()-> <?php if ($showCart): ?> <div class="actions-primary"> <?php if ($_item->isSaleable()): ?> - <?php if ($_item->getTypeInstance()->hasRequiredOptions($_item)): ?> + <?php if (!$_item->getTypeInstance()->isPossibleBuyFromList($_item)): ?> <button class="action tocart primary" data-mage-init='{"redirectUrl":{"url":"<?= /* @escapeNotVerified */ $block->getAddToCartUrl($_item) ?>"}}' type="button" title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>"> diff --git a/app/code/Magento/Catalog/view/frontend/web/js/catalog-add-to-cart.js b/app/code/Magento/Catalog/view/frontend/web/js/catalog-add-to-cart.js index 3fb641733eb2..bcb7c668657d 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/catalog-add-to-cart.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/catalog-add-to-cart.js @@ -6,8 +6,10 @@ define([ 'jquery', 'mage/translate', + 'underscore', + 'Magento_Catalog/js/product/view/product-ids-resolver', 'jquery/ui' -], function ($, $t) { +], function ($, $t, _, idsResolver) { 'use strict'; $.widget('mage.catalogAddToCart', { @@ -76,17 +78,18 @@ define([ /** * Handler for the form 'submit' event * - * @param {Object} form + * @param {jQuery} form */ submitForm: function (form) { this.ajaxSubmit(form); }, /** - * @param {String} form + * @param {jQuery} form */ ajaxSubmit: function (form) { var self = this, + productIds = idsResolver(form), formData; $(self.options.minicartSelector).trigger('contentLoading'); @@ -115,6 +118,7 @@ define([ $(document).trigger('ajax:addToCart', { 'sku': form.data().productSku, + 'productIds': productIds, 'form': form, 'response': res }); @@ -161,6 +165,16 @@ define([ self.enableAddToCartButton(form); }, + /** @inheritdoc */ + error: function (res) { + $(document).trigger('ajax:addToCart:error', { + 'sku': form.data().productSku, + 'productIds': productIds, + 'form': form, + 'response': res + }); + }, + /** @inheritdoc */ complete: function (res) { if (res.state() === 'rejected') { diff --git a/app/code/Magento/Catalog/view/frontend/web/js/product/list/toolbar.js b/app/code/Magento/Catalog/view/frontend/web/js/product/list/toolbar.js index 88be03a04e71..b8b6ff65be2b 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/product/list/toolbar.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/product/list/toolbar.js @@ -27,7 +27,9 @@ define([ directionDefault: 'asc', orderDefault: 'position', limitDefault: '9', - url: '' + url: '', + formKey: '', + post: false }, /** @inheritdoc */ @@ -89,7 +91,7 @@ define([ baseUrl = urlPaths[0], urlParams = urlPaths[1] ? urlPaths[1].split('&') : [], paramData = {}, - parameters, i; + parameters, i, form, params, key, input, formKey; for (i = 0; i < urlParams.length; i++) { parameters = urlParams[i].split('='); @@ -99,12 +101,38 @@ define([ } paramData[paramName] = paramValue; - if (paramValue == defaultValue) { //eslint-disable-line eqeqeq - delete paramData[paramName]; - } - paramData = $.param(paramData); + if (this.options.post) { + form = document.createElement('form'); + params = [this.options.mode, this.options.direction, this.options.order, this.options.limit]; + + for (key in paramData) { + if (params.indexOf(key) !== -1) { //eslint-disable-line max-depth + input = document.createElement('input'); + input.name = key; + input.value = paramData[key]; + form.appendChild(input); + delete paramData[key]; + } + } + formKey = document.createElement('input'); + formKey.name = 'form_key'; + formKey.value = this.options.formKey; + form.appendChild(formKey); + + paramData = $.param(paramData); + baseUrl += paramData.length ? '?' + paramData : ''; - location.href = baseUrl + (paramData.length ? '?' + paramData : ''); + form.action = baseUrl; + form.method = 'POST'; + document.body.appendChild(form); + form.submit(); + } else { + if (paramValue == defaultValue) { //eslint-disable-line eqeqeq + delete paramData[paramName]; + } + paramData = $.param(paramData); + location.href = baseUrl + (paramData.length ? '?' + paramData : ''); + } } }); diff --git a/app/code/Magento/Catalog/view/frontend/web/js/product/provider.js b/app/code/Magento/Catalog/view/frontend/web/js/product/provider.js index c53b2fa6e2a7..b29ebe7d57d1 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/product/provider.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/product/provider.js @@ -5,11 +5,13 @@ define([ 'underscore', + 'jquery', 'mageUtils', 'uiElement', 'Magento_Catalog/js/product/storage/storage-service', - 'Magento_Customer/js/customer-data' -], function (_, utils, Element, storage, customerData) { + 'Magento_Customer/js/customer-data', + 'Magento_Catalog/js/product/view/product-ids-resolver' +], function (_, $, utils, Element, storage, customerData, productResolver) { 'use strict'; return Element.extend({ @@ -135,11 +137,16 @@ define([ */ filterIds: function (ids) { var _ids = {}, - currentTime = new Date().getTime() / 1000; + currentTime = new Date().getTime() / 1000, + currentProductIds = productResolver($('#product_addtocart_form')); _.each(ids, function (id) { - if (currentTime - id['added_at'] < ~~this.idsStorage.lifetime) { + if ( + currentTime - id['added_at'] < ~~this.idsStorage.lifetime && + !_.contains(currentProductIds, id['product_id']) + ) { _ids[id['product_id']] = id; + } }, this); diff --git a/app/code/Magento/Catalog/view/frontend/web/js/product/storage/data-storage.js b/app/code/Magento/Catalog/view/frontend/web/js/product/storage/data-storage.js index ab566a70a756..3dc9f3e84451 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/product/storage/data-storage.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/product/storage/data-storage.js @@ -34,6 +34,21 @@ define([ }; } + /** + * Set data to localStorage with support check. + * + * @param {String} namespace + * @param {Object} data + */ + function setLocalStorageItem(namespace, data) { + try { + window.localStorage.setItem(namespace, JSON.stringify(data)); + } catch (e) { + console.warn('localStorage is unavailable - skipping local caching of product data'); + console.error(e); + } + } + return { /** @@ -118,7 +133,7 @@ define([ if (_.isEmpty(data)) { this.localStorage.removeAll(); } else { - this.localStorage.set(data); + setLocalStorageItem(this.namespace, data); } }, diff --git a/app/code/Magento/Catalog/view/frontend/web/js/product/storage/ids-storage.js b/app/code/Magento/Catalog/view/frontend/web/js/product/storage/ids-storage.js index 7eafbad8299d..ec07c19a2c1b 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/product/storage/ids-storage.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/product/storage/ids-storage.js @@ -11,6 +11,21 @@ define([ ], function ($, _, ko, utils) { 'use strict'; + /** + * Set data to localStorage with support check. + * + * @param {String} namespace + * @param {Object} data + */ + function setLocalStorageItem(namespace, data) { + try { + window.localStorage.setItem(namespace, JSON.stringify(data)); + } catch (e) { + console.warn('localStorage is unavailable - skipping local caching of product data'); + console.error(e); + } + } + return { /** @@ -94,11 +109,7 @@ define([ * Initializes handler to "data" property update */ internalDataHandler: function (data) { - var localStorage = this.localStorage.get(); - - if (!utils.compare(data, localStorage).equal) { - this.localStorage.set(data); - } + setLocalStorageItem(this.namespace, data); }, /** diff --git a/app/code/Magento/Catalog/view/frontend/web/js/product/storage/storage-service.js b/app/code/Magento/Catalog/view/frontend/web/js/product/storage/storage-service.js index e571baa23e49..b35ab867bb0e 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/product/storage/storage-service.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/product/storage/storage-service.js @@ -47,7 +47,7 @@ define([ * @param {*} data */ add: function (data) { - if (!_.isEmpty(data) && !utils.compare(data, this.data()).equal) { + if (!_.isEmpty(data)) { this.data(_.extend(utils.copy(this.data()), data)); } }, diff --git a/app/code/Magento/Catalog/view/frontend/web/js/product/view/product-ids-resolver.js b/app/code/Magento/Catalog/view/frontend/web/js/product/view/product-ids-resolver.js new file mode 100644 index 000000000000..f13e8f84a1b1 --- /dev/null +++ b/app/code/Magento/Catalog/view/frontend/web/js/product/view/product-ids-resolver.js @@ -0,0 +1,29 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'underscore', + 'Magento_Catalog/js/product/view/product-ids' +], function (_, productIds) { + 'use strict'; + + /** + * Returns id's of products in form. + * + * @param {jQuery} $form + * @return {Array} + */ + return function ($form) { + var idSet = productIds(), + product = _.findWhere($form.serializeArray(), { + name: 'product' + }); + + if (!_.isUndefined(product)) { + idSet.push(product.value); + } + + return _.uniq(idSet); + }; +}); diff --git a/app/code/Magento/Catalog/view/frontend/web/js/product/view/product-ids.js b/app/code/Magento/Catalog/view/frontend/web/js/product/view/product-ids.js new file mode 100644 index 000000000000..2198b7b8e48b --- /dev/null +++ b/app/code/Magento/Catalog/view/frontend/web/js/product/view/product-ids.js @@ -0,0 +1,12 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'ko' +], function (ko) { + 'use strict'; + + return ko.observableArray([]); +}); diff --git a/app/code/Magento/Catalog/view/frontend/web/js/related-products.js b/app/code/Magento/Catalog/view/frontend/web/js/related-products.js index 0c37f9ff4f00..66df48c28bfa 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/related-products.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/related-products.js @@ -17,7 +17,7 @@ define([ relatedProductsField: '#related-products-field', // Hidden input field that stores related products. selectAllMessage: $.mage.__('select all'), unselectAllMessage: $.mage.__('unselect all'), - selectAllLink: '[role="select-all"]', + selectAllLink: '[role="button"]', elementsSelector: '.item.product' }, diff --git a/app/code/Magento/Catalog/view/frontend/web/js/validate-product.js b/app/code/Magento/Catalog/view/frontend/web/js/validate-product.js index c0637cb672dc..755e777a01f7 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/validate-product.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/validate-product.js @@ -13,7 +13,8 @@ define([ $.widget('mage.productValidate', { options: { bindSubmit: false, - radioCheckboxClosest: '.nested' + radioCheckboxClosest: '.nested', + addToCartButtonSelector: '.action.tocart' }, /** @@ -41,6 +42,7 @@ define([ return false; } }); + $(this.options.addToCartButtonSelector).attr('disabled', false); } }); diff --git a/app/code/Magento/CatalogAnalytics/README.md b/app/code/Magento/CatalogAnalytics/README.md index df125446117a..f93b223c342d 100644 --- a/app/code/Magento/CatalogAnalytics/README.md +++ b/app/code/Magento/CatalogAnalytics/README.md @@ -1,3 +1,3 @@ # Magento_CatalogAnalytics module -The Magento_CatalogAnalytics module configures data definitions for a data collection related to the Catalog module entities to be used in [Advanced Reporting](http://devdocs.magento.com/guides/v2.2/advanced-reporting/modules.html). +The Magento_CatalogAnalytics module configures data definitions for a data collection related to the Catalog module entities to be used in [Advanced Reporting](https://devdocs.magento.com/guides/v2.2/advanced-reporting/modules.html). diff --git a/app/code/Magento/CatalogAnalytics/composer.json b/app/code/Magento/CatalogAnalytics/composer.json index 5c97261d483d..805be8a17765 100644 --- a/app/code/Magento/CatalogAnalytics/composer.json +++ b/app/code/Magento/CatalogAnalytics/composer.json @@ -4,7 +4,8 @@ "require": { "php": "~7.1.3||~7.2.0", "magento/framework": "*", - "magento/module-catalog": "*" + "magento/module-catalog": "*", + "magento/module-analytics": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php b/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php index ebd8671de02f..d57154c42992 100644 --- a/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php +++ b/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php @@ -15,6 +15,11 @@ */ class AttributesJoiner { + /** + * @var array + */ + private $queryFields = []; + /** * Join fields attached to field node to collection's select. * @@ -24,17 +29,33 @@ class AttributesJoiner */ public function join(FieldNode $fieldNode, AbstractCollection $collection) : void { - $query = $fieldNode->selectionSet->selections; - - /** @var FieldNode $field */ - foreach ($query as $field) { - if ($field->kind === 'InlineFragment') { - continue; + foreach ($this->getQueryFields($fieldNode) as $field) { + if (!$collection->isAttributeAdded($field)) { + $collection->addAttributeToSelect($field); } + } + } - if (!$collection->isAttributeAdded($field->name->value)) { - $collection->addAttributeToSelect($field->name->value); + /** + * Get an array of queried fields. + * + * @param FieldNode $fieldNode + * @return string[] + */ + public function getQueryFields(FieldNode $fieldNode) + { + if (!isset($this->queryFields[$fieldNode->name->value])) { + $this->queryFields[$fieldNode->name->value] = []; + $query = $fieldNode->selectionSet->selections; + /** @var FieldNode $field */ + foreach ($query as $field) { + if ($field->kind === 'InlineFragment') { + continue; + } + $this->queryFields[$fieldNode->name->value][] = $field->name->value; } } + + return $this->queryFields[$fieldNode->name->value]; } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php b/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php index dbe58a9c77cd..b5d02511da4e 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php +++ b/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php @@ -26,7 +26,7 @@ public function calculate(FieldNode $fieldNode) : int $depth = count($selections) ? 1 : 0; $childrenDepth = [0]; foreach ($selections as $node) { - if ($node->kind === 'InlineFragment') { + if ($node->kind === 'InlineFragment' || null !== $node->alias) { continue; } diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/Hydrator.php b/app/code/Magento/CatalogGraphQl/Model/Category/Hydrator.php index da4ec37c51da..d2c1fc8f7be9 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Category/Hydrator.php +++ b/app/code/Magento/CatalogGraphQl/Model/Category/Hydrator.php @@ -8,6 +8,7 @@ namespace Magento\CatalogGraphQl\Model\Category; use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Model\Category; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CustomAttributesFlattener; use Magento\Framework\Reflection\DataObjectProcessor; @@ -41,16 +42,21 @@ public function __construct( /** * Hydrate and flatten category object to flat array * - * @param CategoryInterface $category + * @param Category $category + * @param bool $basicFieldsOnly Set to false to avoid expensive hydration, used for performance optimization * @return array */ - public function hydrateCategory(CategoryInterface $category) : array + public function hydrateCategory(Category $category, $basicFieldsOnly = false) : array { - $categoryData = $this->dataObjectProcessor->buildOutputDataArray($category, CategoryInterface::class); + if ($basicFieldsOnly) { + $categoryData = $category->getData(); + } else { + $categoryData = $this->dataObjectProcessor->buildOutputDataArray($category, CategoryInterface::class); + } $categoryData['id'] = $category->getId(); - $categoryData['product_count'] = $category->getProductCount(); $categoryData['children'] = []; $categoryData['available_sort_by'] = $category->getAvailableSortBy(); + $categoryData['model'] = $category; return $this->flattener->flatten($categoryData); } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/LevelCalculator.php b/app/code/Magento/CatalogGraphQl/Model/Category/LevelCalculator.php index 0401e1c42331..f587be245c99 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Category/LevelCalculator.php +++ b/app/code/Magento/CatalogGraphQl/Model/Category/LevelCalculator.php @@ -15,6 +15,16 @@ */ class LevelCalculator { + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var Category + */ + private $resourceCategory; + /** * @param ResourceConnection $resourceConnection * @param Category $resourceCategory @@ -39,6 +49,7 @@ public function calculate(int $rootCategoryId) : int $select = $connection->select() ->from($this->resourceConnection->getTableName('catalog_category_entity'), 'level') ->where($this->resourceCategory->getLinkField() . " = ?", $rootCategoryId); + return (int) $connection->fetchOne($select); } } diff --git a/app/code/Magento/CatalogGraphQl/Model/LayerFilterItemTypeResolverComposite.php b/app/code/Magento/CatalogGraphQl/Model/LayerFilterItemTypeResolverComposite.php index 20e559011755..02594ecfaf8e 100644 --- a/app/code/Magento/CatalogGraphQl/Model/LayerFilterItemTypeResolverComposite.php +++ b/app/code/Magento/CatalogGraphQl/Model/LayerFilterItemTypeResolverComposite.php @@ -31,7 +31,7 @@ public function __construct(array $typeResolvers = []) } /** - * {@inheritdoc} + * @inheritdoc */ public function resolveType(array $data) : string { @@ -42,10 +42,6 @@ public function resolveType(array $data) : string return $resolvedType; } } - if (empty($resolvedType)) { - throw new GraphQlInputException( - __('Concrete type for %1 not implemented', ['ProductLinksInterface']) - ); - } + throw new GraphQlInputException(__('Cannot resolve layered filter type')); } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Product/Option/DateType.php b/app/code/Magento/CatalogGraphQl/Model/Product/Option/DateType.php new file mode 100644 index 000000000000..e1106a3f696e --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Product/Option/DateType.php @@ -0,0 +1,67 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Product\Option; + +use Magento\Catalog\Model\Product\Option\Type\Date as ProductDateOptionType; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Stdlib\DateTime; + +/** + * @inheritdoc + */ +class DateType extends ProductDateOptionType +{ + /** + * Make valid string as a value of date option type for GraphQl queries + * + * @param array $values All product option values, i.e. array (option_id => mixed, option_id => mixed...) + * @return ProductDateOptionType + */ + public function validateUserValue($values) + { + if ($this->_dateExists() || $this->_timeExists()) { + return parent::validateUserValue($this->formatValues($values)); + } + + return $this; + } + + /** + * Format date value from string to date array + * + * @param [] $values + * @return [] + * @throws LocalizedException + */ + private function formatValues($values) + { + if (isset($values[$this->getOption()->getId()])) { + $value = $values[$this->getOption()->getId()]; + $dateTime = \DateTime::createFromFormat(DateTime::DATETIME_PHP_FORMAT, $value); + $values[$this->getOption()->getId()] = [ + 'date' => $value, + 'year' => $dateTime->format('Y'), + 'month' => $dateTime->format('m'), + 'day' => $dateTime->format('d'), + 'hour' => $dateTime->format('H'), + 'minute' => $dateTime->format('i'), + 'day_part' => $dateTime->format('a'), + ]; + } + + return $values; + } + + /** + * @inheritdoc + */ + public function useCalendar() + { + return false; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/ProductDataProvider.php b/app/code/Magento/CatalogGraphQl/Model/ProductDataProvider.php new file mode 100644 index 000000000000..0d38490407e7 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/ProductDataProvider.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model; + +use Magento\Catalog\Api\ProductRepositoryInterface; + +/** + * Product data provider + * + * TODO: will be replaces on deferred mechanism + */ +class ProductDataProvider +{ + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @param ProductRepositoryInterface $productRepository + */ + public function __construct(ProductRepositoryInterface $productRepository) + { + $this->productRepository = $productRepository; + } + + /** + * Get product data by id + * + * @param int $productId + * @return array + */ + public function getProductDataById(int $productId): array + { + $product = $this->productRepository->getById($productId); + $productData = $product->toArray(); + $productData['model'] = $product; + return $productData; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/ProductLinkTypeResolverComposite.php b/app/code/Magento/CatalogGraphQl/Model/ProductLinkTypeResolverComposite.php index 937e3921758d..c1bf5c0b7bb1 100644 --- a/app/code/Magento/CatalogGraphQl/Model/ProductLinkTypeResolverComposite.php +++ b/app/code/Magento/CatalogGraphQl/Model/ProductLinkTypeResolverComposite.php @@ -11,7 +11,7 @@ use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; /** - * {@inheritdoc} + * @inheritdoc */ class ProductLinkTypeResolverComposite implements TypeResolverInterface { @@ -29,8 +29,7 @@ public function __construct(array $productLinksTypeNameResolvers = []) } /** - * {@inheritdoc} - * @throws GraphQlInputException + * @inheritdoc */ public function resolveType(array $data) : string { @@ -48,11 +47,6 @@ public function resolveType(array $data) : string return $resolvedType; } } - - if (!$resolvedType) { - throw new GraphQlInputException( - __('Concrete type for %1 not implemented', ['ProductLinksInterface']) - ); - } + throw new GraphQlInputException(__('Cannot resolve type')); } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php index 378e7cb4c367..cb392a7b2295 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php @@ -7,6 +7,7 @@ namespace Magento\CatalogGraphQl\Model\Resolver; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Catalog\Api\Data\CategoryInterface; use Magento\Catalog\Model\ResourceModel\Category\Collection; @@ -15,9 +16,8 @@ use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CustomAttributesFlattener; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; -use Magento\Framework\GraphQl\Query\Resolver\Value; use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; -use Magento\Framework\Reflection\DataObjectProcessor; +use Magento\CatalogGraphQl\Model\Category\Hydrator as CategoryHydrator; /** * Resolver for category objects the product is assigned to. @@ -36,11 +36,6 @@ class Categories implements ResolverInterface */ private $categoryIds = []; - /** - * @var DataObjectProcessor - */ - private $dataObjectProcessor; - /** * @var AttributesJoiner */ @@ -57,33 +52,42 @@ class Categories implements ResolverInterface private $valueFactory; /** - * Category constructor. + * @var CategoryHydrator + */ + private $categoryHydrator; + + /** * @param CollectionFactory $collectionFactory - * @param DataObjectProcessor $dataObjectProcessor * @param AttributesJoiner $attributesJoiner * @param CustomAttributesFlattener $customAttributesFlattener * @param ValueFactory $valueFactory + * @param CategoryHydrator $categoryHydrator */ public function __construct( CollectionFactory $collectionFactory, - DataObjectProcessor $dataObjectProcessor, AttributesJoiner $attributesJoiner, CustomAttributesFlattener $customAttributesFlattener, - ValueFactory $valueFactory + ValueFactory $valueFactory, + CategoryHydrator $categoryHydrator ) { $this->collection = $collectionFactory->create(); - $this->dataObjectProcessor = $dataObjectProcessor; $this->attributesJoiner = $attributesJoiner; $this->customAttributesFlattener = $customAttributesFlattener; $this->valueFactory = $valueFactory; + $this->categoryHydrator = $categoryHydrator; } /** - * {@inheritdoc} + * @inheritdoc + * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) : Value + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + /** @var \Magento\Catalog\Model\Product $product */ $product = $value['model']; $categoryIds = $product->getCategoryIds(); @@ -97,19 +101,24 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value } if (!$this->collection->isLoaded()) { - $that->attributesJoiner->join($info->fieldASTs[0], $this->collection); + $that->attributesJoiner->join($info->fieldNodes[0], $this->collection); $this->collection->addIdFilter($this->categoryIds); } /** @var CategoryInterface | \Magento\Catalog\Model\Category $item */ foreach ($this->collection as $item) { if (in_array($item->getId(), $categoryIds)) { - $categories[$item->getId()] = $this->dataObjectProcessor->buildOutputDataArray( - $item, - CategoryInterface::class - ); - $categories[$item->getId()] = $this->customAttributesFlattener - ->flatten($categories[$item->getId()]); - $categories[$item->getId()]['product_count'] = $item->getProductCount(); + // Try to extract all requested fields from the loaded collection data + $categories[$item->getId()] = $this->categoryHydrator->hydrateCategory($item, true); + $categories[$item->getId()]['model'] = $item; + $requestedFields = $that->attributesJoiner->getQueryFields($info->fieldNodes[0]); + $extractedFields = array_keys($categories[$item->getId()]); + $foundFields = array_intersect($requestedFields, $extractedFields); + if (count($requestedFields) === count($foundFields)) { + continue; + } + + // If not all requested fields were extracted from the collection, start more complex extraction + $categories[$item->getId()] = $this->categoryHydrator->hydrateCategory($item); } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Breadcrumbs.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Breadcrumbs.php index d49c7f5e5674..b93c7e279153 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Breadcrumbs.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Breadcrumbs.php @@ -7,12 +7,11 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Category; -use \Magento\CatalogGraphQl\Model\Resolver\Category\DataProvider\Breadcrumbs as BreadcrumbsDataProvider; +use Magento\CatalogGraphQl\Model\Resolver\Category\DataProvider\Breadcrumbs as BreadcrumbsDataProvider; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; -use Magento\Framework\GraphQl\Query\Resolver\Value; -use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; /** * Retrieves breadcrumbs @@ -24,39 +23,25 @@ class Breadcrumbs implements ResolverInterface */ private $breadcrumbsDataProvider; - /** - * @var ValueFactory - */ - private $valueFactory; - /** * @param BreadcrumbsDataProvider $breadcrumbsDataProvider - * @param ValueFactory $valueFactory */ public function __construct( - BreadcrumbsDataProvider $breadcrumbsDataProvider, - ValueFactory $valueFactory + BreadcrumbsDataProvider $breadcrumbsDataProvider ) { $this->breadcrumbsDataProvider = $breadcrumbsDataProvider; - $this->valueFactory = $valueFactory; } /** * @inheritdoc */ - public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null): Value + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) { if (!isset($value['path'])) { - $result = function () { - return null; - }; - return $this->valueFactory->create($result); + throw new LocalizedException(__('"path" value should be specified')); } - $result = function () use ($value) { - $breadcrumbsData = $this->breadcrumbsDataProvider->getData($value['path']); - return count($breadcrumbsData) ? $breadcrumbsData : null; - }; - return $this->valueFactory->create($result); + $breadcrumbsData = $this->breadcrumbsDataProvider->getData($value['path']); + return count($breadcrumbsData) ? $breadcrumbsData : null; } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/CategoriesIdentity.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/CategoriesIdentity.php new file mode 100644 index 000000000000..aba2d7b198db --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/CategoriesIdentity.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Category; + +use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface; + +/** + * Identity for multiple resolved categories + */ +class CategoriesIdentity implements IdentityInterface +{ + /** + * Get category IDs from resolved data + * + * @param array $resolvedData + * @return string[] + */ + public function getIdentities(array $resolvedData): array + { + $ids = []; + if (!empty($resolvedData)) { + foreach ($resolvedData as $category) { + $ids[] = $category['id']; + } + } + return $ids; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/CategoryHtmlAttribute.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/CategoryHtmlAttribute.php new file mode 100644 index 000000000000..7ccb46c3a293 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/CategoryHtmlAttribute.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Category; + +use Magento\Catalog\Model\Category; +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\Catalog\Helper\Output as OutputHelper; + +/** + * Resolve rendered content for category attributes where HTML content is allowed + */ +class CategoryHtmlAttribute implements ResolverInterface +{ + /** + * @var OutputHelper + */ + private $outputHelper; + + /** + * @param OutputHelper $outputHelper + */ + public function __construct( + OutputHelper $outputHelper + ) { + $this->outputHelper = $outputHelper; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + + /* @var $category Category */ + $category = $value['model']; + $fieldName = $field->getName(); + $renderedValue = $this->outputHelper->categoryAttribute($category, $category->getData($fieldName), $fieldName); + + return $renderedValue; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/CategoryTreeIdentity.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/CategoryTreeIdentity.php new file mode 100644 index 000000000000..e4970f08b3eb --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/CategoryTreeIdentity.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Category; + +use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface; + +/** + * Identity for resolved category + */ +class CategoryTreeIdentity implements IdentityInterface +{ + /** + * Get category ID from resolved data + * + * @param array $resolvedData + * @return string[] + */ + public function getIdentities(array $resolvedData): array + { + return empty($resolvedData['id']) ? [] : [$resolvedData['id']]; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php index 406b4173e68e..7a41f8fc94e7 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php @@ -9,8 +9,6 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Query\Resolver\Value; -use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\Builder; @@ -22,38 +20,38 @@ */ class Products implements ResolverInterface { - /** @var \Magento\Catalog\Api\ProductRepositoryInterface */ + /** + * @var \Magento\Catalog\Api\ProductRepositoryInterface + */ private $productRepository; - /** @var Builder */ + /** + * @var Builder + */ private $searchCriteriaBuilder; - /** @var Filter */ + /** + * @var Filter + */ private $filterQuery; - /** @var ValueFactory */ - private $valueFactory; - /** * @param ProductRepositoryInterface $productRepository * @param Builder $searchCriteriaBuilder * @param Filter $filterQuery - * @param ValueFactory $valueFactory */ public function __construct( ProductRepositoryInterface $productRepository, Builder $searchCriteriaBuilder, - Filter $filterQuery, - ValueFactory $valueFactory + Filter $filterQuery ) { $this->productRepository = $productRepository; $this->searchCriteriaBuilder = $searchCriteriaBuilder; $this->filterQuery = $filterQuery; - $this->valueFactory = $valueFactory; } /** - * {@inheritdoc} + * @inheritdoc */ public function resolve( Field $field, @@ -61,7 +59,7 @@ public function resolve( ResolveInfo $info, array $value = null, array $args = null - ): Value { + ) { $args['filter'] = [ 'category_id' => [ 'eq' => $value['id'] @@ -94,14 +92,10 @@ public function resolve( 'items' => $searchResult->getProductsSearchResult(), 'page_info' => [ 'page_size' => $searchCriteria->getPageSize(), - 'current_page' => $currentPage + 'current_page' => $currentPage, + 'total_pages' => $maxPages ] ]; - - $result = function () use ($data) { - return $data; - }; - - return $this->valueFactory->create($result); + return $data; } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/ProductsCount.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/ProductsCount.php new file mode 100644 index 000000000000..397fd12b7e71 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/ProductsCount.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Category; + +use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\Product\Visibility; +use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessor\StockProcessor; +use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; + +/** + * Retrieves products count for a category + */ +class ProductsCount implements ResolverInterface +{ + /** + * @var Visibility + */ + private $catalogProductVisibility; + + /** + * @var StockProcessor + */ + private $stockProcessor; + + /** + * @var SearchCriteriaInterface + */ + private $searchCriteria; + + /** + * @param Visibility $catalogProductVisibility + * @param SearchCriteriaInterface $searchCriteria + * @param StockProcessor $stockProcessor + */ + public function __construct( + Visibility $catalogProductVisibility, + SearchCriteriaInterface $searchCriteria, + StockProcessor $stockProcessor + ) { + $this->catalogProductVisibility = $catalogProductVisibility; + $this->searchCriteria = $searchCriteria; + $this->stockProcessor = $stockProcessor; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($value['model'])) { + throw new GraphQlInputException(__('"model" value should be specified')); + } + /** @var Category $category */ + $category = $value['model']; + $productsCollection = $category->getProductCollection(); + $productsCollection->setVisibility($this->catalogProductVisibility->getVisibleInSiteIds()); + $productsCollection = $this->stockProcessor->process($productsCollection, $this->searchCriteria, []); + + return $productsCollection->getSize(); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/SortFields.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/SortFields.php index ca68b2991011..cb5553bb0370 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/SortFields.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/SortFields.php @@ -9,8 +9,6 @@ use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Query\Resolver\Value; -use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; /** @@ -18,11 +16,6 @@ */ class SortFields implements ResolverInterface { - /** - * @var ValueFactory - */ - private $valueFactory; - /** * @var \Magento\Catalog\Model\Config */ @@ -39,27 +32,24 @@ class SortFields implements ResolverInterface private $sortbyAttributeSource; /** - * @param ValueFactory $valueFactory * @param \Magento\Catalog\Model\Config $catalogConfig * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @oaram \Magento\Catalog\Model\Category\Attribute\Source\Sortby $sortbyAttributeSource */ public function __construct( - ValueFactory $valueFactory, \Magento\Catalog\Model\Config $catalogConfig, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Catalog\Model\Category\Attribute\Source\Sortby $sortbyAttributeSource ) { - $this->valueFactory = $valueFactory; $this->catalogConfig = $catalogConfig; $this->storeManager = $storeManager; $this->sortbyAttributeSource = $sortbyAttributeSource; } /** - * {@inheritDoc} + * @inheritdoc */ - public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) : Value + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) { $sortFieldsOptions = $this->sortbyAttributeSource->getAllOptions(); array_walk( @@ -73,10 +63,6 @@ function (&$option) { 'options' => $sortFieldsOptions, ]; - $result = function () use ($data) { - return $data; - }; - - return $this->valueFactory->create($result); + return $data; } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryTree.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryTree.php index f631e5ff61d2..1783a5cd9a7e 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryTree.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryTree.php @@ -7,12 +7,12 @@ namespace Magento\CatalogGraphQl\Model\Resolver; +use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\ExtractDataFromCategoryTree; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; use Magento\Framework\GraphQl\Query\ResolverInterface; -use Magento\Framework\GraphQl\Query\Resolver\Value; -use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; /** * Category tree field resolver, used for GraphQL request processing. @@ -25,60 +25,60 @@ class CategoryTree implements ResolverInterface const CATEGORY_INTERFACE = 'CategoryInterface'; /** - * @var Products\DataProvider\CategoryTree + * @var \Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CategoryTree */ private $categoryTree; /** - * @var ValueFactory + * @var ExtractDataFromCategoryTree */ - private $valueFactory; + private $extractDataFromCategoryTree; /** - * @param Products\DataProvider\CategoryTree $categoryTree - * @param ValueFactory $valueFactory + * @param \Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CategoryTree $categoryTree + * @param ExtractDataFromCategoryTree $extractDataFromCategoryTree */ public function __construct( \Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CategoryTree $categoryTree, - ValueFactory $valueFactory + ExtractDataFromCategoryTree $extractDataFromCategoryTree ) { $this->categoryTree = $categoryTree; - $this->valueFactory = $valueFactory; + $this->extractDataFromCategoryTree = $extractDataFromCategoryTree; } /** - * Assert that filters from search criteria are valid and retrieve root category id + * Get category id * * @param array $args * @return int * @throws GraphQlInputException */ - private function assertFiltersAreValidAndGetCategoryRootIds(array $args) : int + private function getCategoryId(array $args) : int { if (!isset($args['id'])) { throw new GraphQlInputException(__('"id for category should be specified')); } - return (int) $args['id']; + return (int)$args['id']; } /** - * {@inheritdoc} + * @inheritdoc */ - public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) : Value + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) { - return $this->valueFactory->create(function () use ($value, $args, $field, $info) { - if (isset($value[$field->getName()])) { - return $value[$field->getName()]; - } + if (isset($value[$field->getName()])) { + return $value[$field->getName()]; + } + + $rootCategoryId = $this->getCategoryId($args); + $categoriesTree = $this->categoryTree->getTree($info, $rootCategoryId); + + if (empty($categoriesTree) || ($categoriesTree->count() == 0)) { + throw new GraphQlNoSuchEntityException(__('Category doesn\'t exist')); + } - $rootCategoryId = $this->assertFiltersAreValidAndGetCategoryRootIds($args); - $categoriesTree = $this->categoryTree->getTree($info, $rootCategoryId); - if (!empty($categoriesTree)) { - return current($categoriesTree); - } else { - return null; - } - }); + $result = $this->extractDataFromCategoryTree->execute($categoriesTree); + return current($result); } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/LayerFilters.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/LayerFilters.php new file mode 100644 index 000000000000..0ec7e12e42d5 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/LayerFilters.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Layered navigation filters resolver, used for GraphQL request processing. + */ +class LayerFilters implements ResolverInterface +{ + /** + * @var Layer\DataProvider\Filters + */ + private $filtersDataProvider; + + /** + * @param \Magento\CatalogGraphQl\Model\Resolver\Layer\DataProvider\Filters $filtersDataProvider + */ + public function __construct( + \Magento\CatalogGraphQl\Model\Resolver\Layer\DataProvider\Filters $filtersDataProvider + ) { + $this->filtersDataProvider = $filtersDataProvider; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['layer_type'])) { + return null; + } + + return $this->filtersDataProvider->getData($value['layer_type']); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product.php index 8bf3335bbb9d..40aa54fd9387 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product.php @@ -17,7 +17,7 @@ use Magento\Framework\GraphQl\Query\ResolverInterface; /** - * {@inheritdoc} + * @inheritdoc */ class Product implements ResolverInterface { @@ -52,9 +52,9 @@ public function __construct( } /** - * {@inheritDoc} + * @inheritdoc */ - public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) : Value + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) { if (!isset($value['sku'])) { throw new GraphQlInputException(__('No child sku found for product link.')); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CanonicalUrl.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CanonicalUrl.php index d2675848c2d2..9047eaee4b56 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CanonicalUrl.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CanonicalUrl.php @@ -8,9 +8,8 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Product; use Magento\Catalog\Model\Product; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Query\Resolver\Value; -use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; @@ -20,21 +19,7 @@ class CanonicalUrl implements ResolverInterface { /** - * @var ValueFactory - */ - private $valueFactory; - - /** - * @param ValueFactory $valueFactory - */ - public function __construct( - ValueFactory $valueFactory - ) { - $this->valueFactory = $valueFactory; - } - - /** - * {@inheritdoc} + * @inheritdoc */ public function resolve( Field $field, @@ -42,21 +27,15 @@ public function resolve( ResolveInfo $info, array $value = null, array $args = null - ): Value { + ) { if (!isset($value['model'])) { - $result = function () { - return null; - }; - return $this->valueFactory->create($result); + throw new LocalizedException(__('"model" value should be specified')); } /* @var $product Product */ $product = $value['model']; $url = $product->getUrlModel()->getUrl($product, ['_ignore_category' => true]); - $result = function () use ($url) { - return $url; - }; - - return $this->valueFactory->create($result); + + return $url; } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/EntityIdToId.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/EntityIdToId.php index 4c101f68eb4d..ada3caad5f9f 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/EntityIdToId.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/EntityIdToId.php @@ -7,19 +7,18 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Product; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Product; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Query\Resolver\Value; -use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; /** - * Fixed the id related data in the product data + * @inheritdoc * - * {@inheritdoc} + * Fixed the id related data in the product data */ class EntityIdToId implements ResolverInterface { @@ -28,23 +27,16 @@ class EntityIdToId implements ResolverInterface */ private $metadataPool; - /** - * @var ValueFactory - */ - private $valueFactory; - /** * @param MetadataPool $metadataPool - * @param ValueFactory $valueFactory */ - public function __construct(MetadataPool $metadataPool, ValueFactory $valueFactory) + public function __construct(MetadataPool $metadataPool) { $this->metadataPool = $metadataPool; - $this->valueFactory = $valueFactory; } /** - * {@inheritDoc} + * @inheritdoc */ public function resolve( Field $field, @@ -52,12 +44,9 @@ public function resolve( ResolveInfo $info, array $value = null, array $args = null - ): Value { + ) { if (!isset($value['model'])) { - $result = function () { - return null; - }; - return $this->valueFactory->create($result); + throw new LocalizedException(__('"model" value should be specified')); } /** @var Product $product */ @@ -67,10 +56,6 @@ public function resolve( $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField() ); - $result = function () use ($productId) { - return $productId; - }; - - return $this->valueFactory->create($result); + return $productId; } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Identity.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Identity.php new file mode 100644 index 000000000000..198b1c112dca --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Identity.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Product; + +use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface; + +/** + * Identity for resolved products + */ +class Identity implements IdentityInterface +{ + /** + * Get product ids for cache tag + * + * @param array $resolvedData + * @return string[] + */ + public function getIdentities(array $resolvedData): array + { + $ids = []; + $items = $resolvedData['items'] ?? []; + foreach ($items as $item) { + $ids[] = $item['entity_id']; + } + + return $ids; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGalleryEntries.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGalleryEntries.php index ac028eef1fb1..c8f167da583d 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGalleryEntries.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGalleryEntries.php @@ -7,35 +7,32 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Product; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Catalog\Model\Product; use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Query\Resolver\Value; -use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; /** + * @inheritdoc + * * Format a product's media gallery information to conform to GraphQL schema representation */ class MediaGalleryEntries implements ResolverInterface { /** - * @var ValueFactory - */ - private $valueFactory; - - /** - * @param ValueFactory $valueFactory - */ - public function __construct(ValueFactory $valueFactory) - { - $this->valueFactory = $valueFactory; - } - - /** + * @inheritdoc + * * Format product's media gallery entry data to conform to GraphQL schema * - * {@inheritdoc} + * @param \Magento\Framework\GraphQl\Config\Element\Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * @throws \Exception + * @return array */ public function resolve( Field $field, @@ -43,12 +40,9 @@ public function resolve( ResolveInfo $info, array $value = null, array $args = null - ): Value { + ) { if (!isset($value['model'])) { - $result = function () { - return null; - }; - return $this->valueFactory->create($result); + throw new LocalizedException(__('"model" value should be specified')); } /** @var Product $product */ @@ -64,11 +58,6 @@ public function resolve( } } } - - $result = function () use ($mediaGalleryEntries) { - return $mediaGalleryEntries; - }; - - return $this->valueFactory->create($result); + return $mediaGalleryEntries; } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/NewFromTo.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/NewFromTo.php index 34fb58b97b15..12016282a308 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/NewFromTo.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/NewFromTo.php @@ -7,35 +7,31 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Product; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Catalog\Model\Product; use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Query\Resolver\Value; -use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; /** + * @inheritdoc + * * Format the new from and to typo of legacy fields news_from_date and news_to_date */ class NewFromTo implements ResolverInterface { /** - * @var ValueFactory - */ - private $valueFactory; - - /** - * @param ValueFactory $valueFactory - */ - public function __construct(ValueFactory $valueFactory) - { - $this->valueFactory = $valueFactory; - } - - /** + * @inheritdoc + * * Transfer data from legacy news_from_date and news_to_date to new names corespondent fields * - * {@inheritdoc} + * @param \Magento\Framework\GraphQl\Config\Element\Field $field + * @param \Magento\Framework\GraphQl\Query\Resolver\ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * @throws \Exception + * @return null|array */ public function resolve( Field $field, @@ -43,12 +39,9 @@ public function resolve( ResolveInfo $info, array $value = null, array $args = null - ): Value { + ) { if (!isset($value['model'])) { - $result = function () { - return null; - }; - return $this->valueFactory->create($result); + throw new LocalizedException(__('"model" value should be specified')); } /** @var Product $product */ @@ -60,10 +53,6 @@ public function resolve( $data = $product->getData($attributeName); } - $result = function () use ($data) { - return $data; - }; - - return $this->valueFactory->create($result); + return $data; } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Options.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Options.php index 8e06877452ff..76602288039c 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Options.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Options.php @@ -7,12 +7,12 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Product; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Option; use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Query\Resolver\Value; -use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; /** @@ -21,22 +21,17 @@ class Options implements ResolverInterface { /** - * @var ValueFactory - */ - private $valueFactory; - - /** - * @param ValueFactory $valueFactory - */ - public function __construct(ValueFactory $valueFactory) - { - $this->valueFactory = $valueFactory; - } - - /** + * @inheritdoc + * * Format product's option data to conform to GraphQL schema * - * {@inheritdoc} + * @param \Magento\Framework\GraphQl\Config\Element\Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * @throws \Exception + * @return null|array */ public function resolve( Field $field, @@ -44,12 +39,9 @@ public function resolve( ResolveInfo $info, array $value = null, array $args = null - ): Value { + ) { if (!isset($value['model'])) { - $result = function () { - return null; - }; - return $this->valueFactory->create($result); + throw new LocalizedException(__('"model" value should be specified')); } /** @var Product $product */ @@ -80,10 +72,6 @@ public function resolve( } } - $result = function () use ($options) { - return $options; - }; - - return $this->valueFactory->create($result); + return $options; } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price.php index 29b693abc266..55d930101fb6 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price.php @@ -7,13 +7,13 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Product; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Catalog\Model\Product; use Magento\Catalog\Pricing\Price\FinalPrice; use Magento\Catalog\Pricing\Price\RegularPrice; use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Query\Resolver\Value; -use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\Pricing\Adjustment\AdjustmentInterface; use Magento\Framework\Pricing\Amount\AmountInterface; @@ -35,30 +35,30 @@ class Price implements ResolverInterface */ private $priceInfoFactory; - /** - * @var ValueFactory - */ - private $valueFactory; - /** * @param StoreManagerInterface $storeManager * @param PriceInfoFactory $priceInfoFactory - * @param ValueFactory $valueFactory */ public function __construct( StoreManagerInterface $storeManager, - PriceInfoFactory $priceInfoFactory, - ValueFactory $valueFactory + PriceInfoFactory $priceInfoFactory ) { $this->storeManager = $storeManager; $this->priceInfoFactory = $priceInfoFactory; - $this->valueFactory = $valueFactory; } /** + * @inheritdoc + * * Format product's tier price data to conform to GraphQL schema * - * {@inheritdoc} + * @param \Magento\Framework\GraphQl\Config\Element\Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * @throws \Exception + * @return array */ public function resolve( Field $field, @@ -66,12 +66,9 @@ public function resolve( ResolveInfo $info, array $value = null, array $args = null - ): Value { + ) { if (!isset($value['model'])) { - $result = function () { - return null; - }; - return $this->valueFactory->create($result); + throw new LocalizedException(__('"model" value should be specified')); } /** @var Product $product */ @@ -90,11 +87,7 @@ public function resolve( 'maximalPrice' => $this->createAdjustmentsArray($priceInfo->getAdjustments(), $maximalPriceAmount) ]; - $result = function () use ($prices) { - return $prices; - }; - - return $this->valueFactory->create($result); + return $prices; } /** diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductComplexTextAttribute.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductComplexTextAttribute.php new file mode 100644 index 000000000000..2573e92e564b --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductComplexTextAttribute.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Product; + +use Magento\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\Catalog\Model\Product; +use Magento\Catalog\Helper\Output as OutputHelper; + +/** + * Resolve rendered content for attributes where HTML content is allowed + */ +class ProductComplexTextAttribute implements ResolverInterface +{ + /** + * @var OutputHelper + */ + private $outputHelper; + + /** + * @param OutputHelper $outputHelper + */ + public function __construct( + OutputHelper $outputHelper + ) { + $this->outputHelper = $outputHelper; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ): array { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + + /* @var $product Product */ + $product = $value['model']; + $fieldName = $field->getName(); + $renderedValue = $this->outputHelper->productAttribute($product, $product->getData($fieldName), $fieldName); + + return ['html' => $renderedValue ?? '']; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage.php new file mode 100644 index 000000000000..d1566162472b --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Product; + +use Magento\Catalog\Model\Product; +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; + +/** + * Returns product's image data + */ +class ProductImage implements ResolverInterface +{ + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ): array { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + + /** @var Product $product */ + $product = $value['model']; + $imageType = $field->getName(); + + return [ + 'model' => $product, + 'image_type' => $imageType, + ]; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage/Label.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage/Label.php new file mode 100644 index 000000000000..f971e3574262 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage/Label.php @@ -0,0 +1,96 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Product\ProductImage; + +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ResourceModel\Product as ProductResourceModel; +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\Store\Model\StoreManagerInterface; + +/** + * Returns product's image label + */ +class Label implements ResolverInterface +{ + /** + * @var ProductResourceModel + */ + private $productResource; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @param ProductResourceModel $productResource + * @param StoreManagerInterface $storeManager + */ + public function __construct( + ProductResourceModel $productResource, + StoreManagerInterface $storeManager + ) { + $this->productResource = $productResource; + $this->storeManager = $storeManager; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['image_type'])) { + throw new LocalizedException(__('"image_type" value should be specified')); + } + + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + + /** @var Product $product */ + $product = $value['model']; + $imageType = $value['image_type']; + $imagePath = $product->getData($imageType); + $productId = (int)$product->getEntityId(); + + // null if image is not set + if (null === $imagePath) { + return $this->getAttributeValue($productId, 'name'); + } + + $imageLabel = $this->getAttributeValue($productId, $imageType . '_label'); + if (null === $imageLabel) { + $imageLabel = $this->getAttributeValue($productId, 'name'); + } + + return $imageLabel; + } + + /** + * Get attribute value + * + * @param int $productId + * @param string $attributeCode + * @return null|string Null if attribute value is not exists + */ + private function getAttributeValue(int $productId, string $attributeCode): ?string + { + $storeId = $this->storeManager->getStore()->getId(); + + $value = $this->productResource->getAttributeRawValue($productId, $attributeCode, $storeId); + return is_array($value) && empty($value) ? null : $value; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage/Url.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage/Url.php new file mode 100644 index 000000000000..23a8c2d15c09 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage/Url.php @@ -0,0 +1,89 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Product\ProductImage; + +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; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Returns product's image url + */ +class Url implements ResolverInterface +{ + /** + * @var ImageFactory + */ + private $productImageFactory; + /** + * @var PlaceholderProvider + */ + private $placeholderProvider; + + /** + * @param ImageFactory $productImageFactory + * @param PlaceholderProvider $placeholderProvider + */ + public function __construct( + ImageFactory $productImageFactory, + PlaceholderProvider $placeholderProvider + ) { + $this->productImageFactory = $productImageFactory; + $this->placeholderProvider = $placeholderProvider; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['image_type'])) { + throw new LocalizedException(__('"image_type" value should be specified')); + } + + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + + /** @var Product $product */ + $product = $value['model']; + $imagePath = $product->getData($value['image_type']); + + return $this->getImageUrl($value['image_type'], $imagePath); + } + + /** + * Get image URL + * + * @param string $imageType + * @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); + + if ($image->isBaseFilePlaceholder()) { + return $this->placeholderProvider->getPlaceholder($imageType); + } + + return $image->getUrl(); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductLinks.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductLinks.php index 181371f16d3a..4d1b11a74b9d 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductLinks.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductLinks.php @@ -7,18 +7,18 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Product; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\ProductLink\Link; use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Query\Resolver\Value; -use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; /** - * Format the product links information to conform to GraphQL schema representation + * @inheritdoc * - * {@inheritdoc} + * Format the product links information to conform to GraphQL schema representation */ class ProductLinks implements ResolverInterface { @@ -28,22 +28,17 @@ class ProductLinks implements ResolverInterface private $linkTypes = ['related', 'upsell', 'crosssell']; /** - * @var ValueFactory - */ - private $valueFactory; - - /** - * @param ValueFactory $valueFactory - */ - public function __construct(ValueFactory $valueFactory) - { - $this->valueFactory = $valueFactory; - } - - /** + * @inheritdoc + * * Format product links data to conform to GraphQL schema * - * {@inheritdoc} + * @param \Magento\Framework\GraphQl\Config\Element\Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * @throws \Exception + * @return null|array */ public function resolve( Field $field, @@ -51,12 +46,9 @@ public function resolve( ResolveInfo $info, array $value = null, array $args = null - ): Value { + ) { if (!isset($value['model'])) { - $result = function () { - return null; - }; - return $this->valueFactory->create($result); + throw new LocalizedException(__('"model" value should be specified')); } /** @var Product $product */ @@ -73,10 +65,6 @@ public function resolve( } } - $result = function () use ($links) { - return $links; - }; - - return $this->valueFactory->create($result); + return $links; } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/TierPrices.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/TierPrices.php index 0b7811d4f743..726ef91c5688 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/TierPrices.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/TierPrices.php @@ -7,38 +7,33 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Product; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\TierPrice; use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Query\Resolver\Value; -use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; /** - * Format a product's tier price information to conform to GraphQL schema representation + * @inheritdoc * - * {@inheritdoc} + * Format a product's tier price information to conform to GraphQL schema representation */ class TierPrices implements ResolverInterface { /** - * @var ValueFactory - */ - private $valueFactory; - - /** - * @param ValueFactory $valueFactory - */ - public function __construct(ValueFactory $valueFactory) - { - $this->valueFactory = $valueFactory; - } - - /** + * @inheritdoc + * * Format product's tier price data to conform to GraphQL schema * - * {@inheritdoc} + * @param \Magento\Framework\GraphQl\Config\Element\Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * @throws \Exception + * @return null|array */ public function resolve( Field $field, @@ -46,12 +41,9 @@ public function resolve( ResolveInfo $info, array $value = null, array $args = null - ): Value { + ) { if (!isset($value['model'])) { - $result = function () { - return null; - }; - return $this->valueFactory->create($result); + throw new LocalizedException(__('"model" value should be specified')); } /** @var Product $product */ @@ -66,10 +58,6 @@ public function resolve( } } - $result = function () use ($tierPrices) { - return $tierPrices; - }; - - return $this->valueFactory->create($result); + return $tierPrices; } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Websites.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Websites.php index 867f1fa0d0b0..070c564713a9 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Websites.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Websites.php @@ -7,10 +7,9 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Product; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\Catalog\Model\Product; use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Query\Resolver\Value; use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\CatalogGraphQl\Model\Resolver\Product\Websites\Collection; @@ -43,15 +42,12 @@ public function __construct( } /** - * {@inheritDoc} + * @inheritdoc */ - public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) : Value + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) { if (!isset($value['entity_id'])) { - $result = function () { - return null; - }; - return $this->valueFactory->create($result); + throw new LocalizedException(__('"model" value should be specified')); } $this->productWebsitesCollection->addIdFilters((int)$value['entity_id']); $result = function () use ($value) { diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php index cc791ce780c1..24c5e664831e 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php @@ -14,8 +14,6 @@ use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\Builder; use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\SearchFilter; -use Magento\Framework\GraphQl\Query\Resolver\Value; -use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Catalog\Model\Layer\Resolver; @@ -44,40 +42,26 @@ class Products implements ResolverInterface */ private $searchFilter; - /** - * @var ValueFactory - */ - private $valueFactory; - - /** - * @var Layer\DataProvider\Filters - */ - private $filtersDataProvider; - /** * @param Builder $searchCriteriaBuilder * @param Search $searchQuery * @param Filter $filterQuery - * @param ValueFactory $valueFactory + * @param SearchFilter $searchFilter */ public function __construct( Builder $searchCriteriaBuilder, Search $searchQuery, Filter $filterQuery, - SearchFilter $searchFilter, - ValueFactory $valueFactory, - \Magento\CatalogGraphQl\Model\Resolver\Layer\DataProvider\Filters $filtersDataProvider + SearchFilter $searchFilter ) { $this->searchCriteriaBuilder = $searchCriteriaBuilder; $this->searchQuery = $searchQuery; $this->filterQuery = $filterQuery; $this->searchFilter = $searchFilter; - $this->valueFactory = $valueFactory; - $this->filtersDataProvider = $filtersDataProvider; } /** - * {@inheritdoc} + * @inheritdoc */ public function resolve( Field $field, @@ -85,7 +69,7 @@ public function resolve( ResolveInfo $info, array $value = null, array $args = null - ) : Value { + ) { $searchCriteria = $this->searchCriteriaBuilder->build($field->getName(), $args); $searchCriteria->setCurrentPage($args['currentPage']); $searchCriteria->setPageSize($args['pageSize']); @@ -110,10 +94,10 @@ public function resolve( $currentPage = $searchCriteria->getCurrentPage(); if ($searchCriteria->getCurrentPage() > $maxPages && $searchResult->getTotalCount() > 0) { - $currentPage = new GraphQlInputException( + throw new GraphQlInputException( __( - 'currentPage value %1 specified is greater than the number of pages available.', - [$maxPages] + 'currentPage value %1 specified is greater than the %2 page(s) available.', + [$currentPage, $maxPages] ) ); } @@ -123,15 +107,12 @@ public function resolve( 'items' => $searchResult->getProductsSearchResult(), 'page_info' => [ 'page_size' => $searchCriteria->getPageSize(), - 'current_page' => $currentPage + 'current_page' => $currentPage, + 'total_pages' => $maxPages ], - 'filters' => $this->filtersDataProvider->getData($layerType) + 'layer_type' => $layerType ]; - $result = function () use ($data) { - return $data; - }; - - return $this->valueFactory->create($result); + return $data; } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php index d2fc174fb1ed..fc5a563c82b4 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php @@ -9,7 +9,6 @@ use GraphQL\Language\AST\FieldNode; use Magento\CatalogGraphQl\Model\Category\DepthCalculator; -use Magento\CatalogGraphQl\Model\Category\Hydrator; use Magento\CatalogGraphQl\Model\Category\LevelCalculator; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; @@ -17,6 +16,7 @@ use Magento\Catalog\Model\ResourceModel\Category\Collection; use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory; use Magento\CatalogGraphQl\Model\AttributesJoiner; +use Magento\Catalog\Model\Category; /** * Category tree data provider @@ -53,81 +53,75 @@ class CategoryTree */ private $metadata; - /** - * @var Hydrator - */ - private $hydrator; - /** * @param CollectionFactory $collectionFactory * @param AttributesJoiner $attributesJoiner * @param DepthCalculator $depthCalculator * @param LevelCalculator $levelCalculator * @param MetadataPool $metadata - * @param Hydrator $hydrator */ public function __construct( CollectionFactory $collectionFactory, AttributesJoiner $attributesJoiner, DepthCalculator $depthCalculator, LevelCalculator $levelCalculator, - MetadataPool $metadata, - Hydrator $hydrator + MetadataPool $metadata ) { $this->collectionFactory = $collectionFactory; $this->attributesJoiner = $attributesJoiner; $this->depthCalculator = $depthCalculator; $this->levelCalculator = $levelCalculator; $this->metadata = $metadata; - $this->hydrator = $hydrator; } /** + * Returns categories tree starting from parent $rootCategoryId + * * @param ResolveInfo $resolveInfo * @param int $rootCategoryId - * @return array + * @return \Iterator */ - public function getTree(ResolveInfo $resolveInfo, int $rootCategoryId) : array + public function getTree(ResolveInfo $resolveInfo, int $rootCategoryId): \Iterator { - $categoryQuery = $resolveInfo->fieldASTs[0]; + $categoryQuery = $resolveInfo->fieldNodes[0]; $collection = $this->collectionFactory->create(); $this->joinAttributesRecursively($collection, $categoryQuery); $depth = $this->depthCalculator->calculate($categoryQuery); $level = $this->levelCalculator->calculate($rootCategoryId); + + // If root category is being filter, we've to remove first slash + if ($rootCategoryId == Category::TREE_ROOT_ID) { + $regExpPathFilter = sprintf('.*%s/[/0-9]*$', $rootCategoryId); + } else { + $regExpPathFilter = sprintf('.*/%s/[/0-9]*$', $rootCategoryId); + } + //Search for desired part of category tree - $collection->addPathFilter(sprintf('.*/%s/[/0-9]*$', $rootCategoryId)); + $collection->addPathFilter($regExpPathFilter); + $collection->addFieldToFilter('level', ['gt' => $level]); $collection->addFieldToFilter('level', ['lteq' => $level + $depth - self::DEPTH_OFFSET]); + $collection->addAttributeToFilter('is_active', 1, "left"); $collection->setOrder('level'); + $collection->setOrder( + 'position', + $collection::SORT_ORDER_DESC + ); $collection->getSelect()->orWhere( - $this->metadata->getMetadata(CategoryInterface::class)->getIdentifierField() . ' = ?', + $collection->getSelect() + ->getConnection() + ->quoteIdentifier( + 'e.' . $this->metadata->getMetadata(CategoryInterface::class)->getIdentifierField() + ) . ' = ?', $rootCategoryId ); - return $this->processTree($collection->getIterator()); - } - - /** - * @param \Iterator $iterator - * @return array - */ - private function processTree(\Iterator $iterator) : array - { - $tree = []; - while ($iterator->valid()) { - /** @var CategoryInterface $category */ - $category = $iterator->current(); - $iterator->next(); - $nextCategory = $iterator->current(); - $tree[$category->getId()] = $this->hydrator->hydrateCategory($category); - if ($nextCategory && (int) $nextCategory->getLevel() !== (int) $category->getLevel()) { - $tree[$category->getId()]['children'] = $this->processTree($iterator); - } - } - return $tree; + return $collection->getIterator(); } /** + * Join attributes recursively + * * @param Collection $collection * @param FieldNode $fieldNode * @return void diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ExtractDataFromCategoryTree.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ExtractDataFromCategoryTree.php new file mode 100644 index 000000000000..3525ccbb6a2d --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ExtractDataFromCategoryTree.php @@ -0,0 +1,111 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider; + +use Magento\CatalogGraphQl\Model\Category\Hydrator; +use Magento\Catalog\Api\Data\CategoryInterface; + +/** + * Extract data from category tree + */ +class ExtractDataFromCategoryTree +{ + /** + * @var Hydrator + */ + private $categoryHydrator; + + /** + * @var CategoryInterface + */ + private $iteratingCategory; + + /** + * @var int + */ + private $startCategoryFetchLevel = 1; + + /** + * @param Hydrator $categoryHydrator + */ + public function __construct( + Hydrator $categoryHydrator + ) { + $this->categoryHydrator = $categoryHydrator; + } + + /** + * Extract data from category tree + * + * @param \Iterator $iterator + * @return array + */ + public function execute(\Iterator $iterator): array + { + $tree = []; + while ($iterator->valid()) { + /** @var CategoryInterface $category */ + $category = $iterator->current(); + $iterator->next(); + $pathElements = explode("/", $category->getPath()); + if (empty($tree)) { + $this->startCategoryFetchLevel = count($pathElements) - 1; + } + $this->iteratingCategory = $category; + $currentLevelTree = $this->explodePathToArray($pathElements, $this->startCategoryFetchLevel); + if (empty($tree)) { + $tree = $currentLevelTree; + } + $tree = $this->mergeCategoriesTrees($currentLevelTree, $tree); + } + return $tree; + } + + /** + * Merge together complex categories trees + * + * @param array $tree1 + * @param array $tree2 + * @return array + */ + private function mergeCategoriesTrees(array &$tree1, array &$tree2): array + { + $mergedTree = $tree1; + foreach ($tree2 as $currentKey => &$value) { + if (is_array($value) && isset($mergedTree[$currentKey]) && is_array($mergedTree[$currentKey])) { + $mergedTree[$currentKey] = $this->mergeCategoriesTrees($mergedTree[$currentKey], $value); + } else { + $mergedTree[$currentKey] = $value; + } + } + return $mergedTree; + } + + /** + * Recursive method to generate tree for one category path + * + * @param array $pathElements + * @param int $index + * @return array + */ + private function explodePathToArray(array $pathElements, int $index): array + { + $tree = []; + $tree[$pathElements[$index]]['id'] = $pathElements[$index]; + if ($index === count($pathElements) - 1) { + $tree[$pathElements[$index]] = $this->categoryHydrator->hydrateCategory($this->iteratingCategory); + $tree[$pathElements[$index]]['model'] = $this->iteratingCategory; + } + $currentIndex = $index; + $index++; + if (isset($pathElements[$index])) { + $tree[$pathElements[$currentIndex]]['children'] = $this->explodePathToArray($pathElements, $index); + } + return $tree; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Image/Placeholder.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Image/Placeholder.php new file mode 100644 index 000000000000..f5cf2a9ef82f --- /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 000000000000..dc48c5ef6934 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Image/Placeholder/Theme.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Image\Placeholder; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\View\Design\Theme\ThemeProviderInterface; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Theme provider + */ +class Theme +{ + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var ThemeProviderInterface + */ + private $themeProvider; + + /** + * @param ScopeConfigInterface $scopeConfig + * @param StoreManagerInterface $storeManager + * @param ThemeProviderInterface $themeProvider + */ + public function __construct( + ScopeConfigInterface $scopeConfig, + StoreManagerInterface $storeManager, + ThemeProviderInterface $themeProvider + ) { + $this->scopeConfig = $scopeConfig; + $this->storeManager = $storeManager; + $this->themeProvider = $themeProvider; + } + + /** + * Get theme model + * + * @return array + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function getThemeData(): array + { + $themeId = $this->scopeConfig->getValue( + \Magento\Framework\View\DesignInterface::XML_PATH_THEME_ID, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $this->storeManager->getStore()->getId() + ); + + /** @var $theme \Magento\Framework\View\Design\ThemeInterface */ + $theme = $this->themeProvider->getThemeById($themeId); + + $data = $theme->getData(); + $data['themeModel'] = $theme; + + return $data; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php index 2c73d7a07917..7f1fd7194225 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php @@ -86,8 +86,12 @@ public function getList( $collection->load(); // Methods that perform extra fetches post-load - $collection->addMediaGalleryData(); - $collection->addOptionsToResult(); + if (in_array('media_gallery_entries', $attributes)) { + $collection->addMediaGalleryData(); + } + if (in_array('options', $attributes)) { + $collection->addOptionsToResult(); + } $searchResult = $this->searchResultsFactory->create(); $searchResult->setSearchCriteria($searchCriteria); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php index c4da59fd2ced..f9b64f3a3c69 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php @@ -12,7 +12,6 @@ use Magento\CatalogGraphQl\Model\Resolver\Products\SearchCriteria\Helper\Filter as FilterHelper; use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResult; use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResultFactory; -use Magento\Framework\EntityManager\EntityManager; use Magento\Search\Api\SearchInterface; /** @@ -45,31 +44,42 @@ class Search */ private $metadataPool; + /** + * @var \Magento\Search\Model\Search\PageSizeProvider + */ + private $pageSizeProvider; + /** * @param SearchInterface $search * @param FilterHelper $filterHelper * @param Filter $filterQuery * @param SearchResultFactory $searchResultFactory + * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool + * @param \Magento\Search\Model\Search\PageSizeProvider $pageSize */ public function __construct( SearchInterface $search, FilterHelper $filterHelper, Filter $filterQuery, SearchResultFactory $searchResultFactory, - \Magento\Framework\EntityManager\MetadataPool $metadataPool + \Magento\Framework\EntityManager\MetadataPool $metadataPool, + \Magento\Search\Model\Search\PageSizeProvider $pageSize ) { $this->search = $search; $this->filterHelper = $filterHelper; $this->filterQuery = $filterQuery; $this->searchResultFactory = $searchResultFactory; $this->metadataPool = $metadataPool; + $this->pageSizeProvider = $pageSize; } /** * Return results of full text catalog search of given term, and will return filtered results if filter is specified * * @param SearchCriteriaInterface $searchCriteria + * @param ResolveInfo $info * @return SearchResult + * @throws \Exception */ public function getResult(SearchCriteriaInterface $searchCriteria, ResolveInfo $info) : SearchResult { @@ -79,7 +89,8 @@ public function getResult(SearchCriteriaInterface $searchCriteria, ResolveInfo $ $realPageSize = $searchCriteria->getPageSize(); $realCurrentPage = $searchCriteria->getCurrentPage(); // Current page must be set to 0 and page size to max for search to grab all ID's as temporary workaround - $searchCriteria->setPageSize(PHP_INT_MAX); + $pageSize = $this->pageSizeProvider->getMaxPageSize(); + $searchCriteria->setPageSize($pageSize); $searchCriteria->setCurrentPage(0); $itemsResults = $this->search->search($searchCriteria); diff --git a/app/code/Magento/CatalogGraphQl/Model/Search/Adapter/Mysql/Query/Builder/Match.php b/app/code/Magento/CatalogGraphQl/Model/Search/Adapter/Mysql/Query/Builder/Match.php new file mode 100644 index 000000000000..4490cf031e5e --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Search/Adapter/Mysql/Query/Builder/Match.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Search\Adapter\Mysql\Query\Builder; + +use Magento\Framework\DB\Helper\Mysql\Fulltext; +use Magento\Framework\Search\Adapter\Mysql\Field\ResolverInterface; +use Magento\Framework\Search\Adapter\Mysql\Query\Builder\Match as BuilderMatch; +use Magento\Framework\Search\Adapter\Preprocessor\PreprocessorInterface; +use Magento\Framework\Search\Request\Query\BoolExpression; +use Magento\Search\Helper\Data; + +/** + * @inheritdoc + */ +class Match extends BuilderMatch +{ + /** + * @var Data + */ + private $searchHelper; + + /** + * @param ResolverInterface $resolver + * @param Fulltext $fulltextHelper + * @param Data $searchHelper + * @param string $fulltextSearchMode + * @param PreprocessorInterface[] $preprocessors + */ + public function __construct( + ResolverInterface $resolver, + Fulltext $fulltextHelper, + Data $searchHelper, + $fulltextSearchMode = Fulltext::FULLTEXT_MODE_BOOLEAN, + array $preprocessors = [] + ) { + parent::__construct($resolver, $fulltextHelper, $fulltextSearchMode, $preprocessors); + $this->searchHelper = $searchHelper; + } + + /** + * @inheritdoc + */ + protected function prepareQuery($queryValue, $conditionType) + { + $replaceSymbols = str_split(self::SPECIAL_CHARACTERS, 1); + $queryValue = str_replace($replaceSymbols, ' ', $queryValue); + foreach ($this->preprocessors as $preprocessor) { + $queryValue = $preprocessor->process($queryValue); + } + + $stringPrefix = ''; + if ($conditionType === BoolExpression::QUERY_CONDITION_MUST) { + $stringPrefix = '+'; + } elseif ($conditionType === BoolExpression::QUERY_CONDITION_NOT) { + $stringPrefix = '-'; + } + + $queryValues = explode(' ', $queryValue); + + foreach ($queryValues as $queryKey => $queryValue) { + if (empty($queryValue)) { + unset($queryValues[$queryKey]); + } else { + $stringSuffix = $this->searchHelper->getMinQueryLength() > strlen($queryValue) ? '' : '*'; + $queryValues[$queryKey] = $stringPrefix . $queryValue . $stringSuffix; + } + } + + $queryValue = implode(' ', $queryValues); + + return $queryValue; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Product/CanonicalUrlTest.php b/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Product/CanonicalUrlTest.php deleted file mode 100644 index ae01c67eb522..000000000000 --- a/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Product/CanonicalUrlTest.php +++ /dev/null @@ -1,92 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\CatalogGraphQl\Test\Unit\Model\Resolver\Product; - -use Magento\CatalogGraphQl\Model\Resolver\Product\CanonicalUrl; -use Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator; -use Magento\Framework\GraphQl\Query\Resolver\Value; -use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\Store\Model\StoreManagerInterface; -use PHPUnit\Framework\TestCase; - -class CanonicalUrlTest extends TestCase -{ - /** - * @var ObjectManager - */ - private $objectManager; - - /** - * @var CanonicalUrl - */ - private $subject; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $mockValueFactory; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $mockStoreManager; - - public function testReturnsNullWhenNoProductAvailable() - { - $mockField = $this->getMockBuilder(\Magento\Framework\GraphQl\Config\Element\Field::class) - ->disableOriginalConstructor() - ->getMock(); - $mockInfo = $this->getMockBuilder(\Magento\Framework\GraphQl\Schema\Type\ResolveInfo::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->mockValueFactory->method('create')->with( - $this->callback( - function ($param) { - return $param() === null; - } - ) - ); - - $this->subject->resolve($mockField, '', $mockInfo, [], []); - } - - protected function setUp() - { - parent::setUp(); - $this->objectManager = new ObjectManager($this); - $this->mockStoreManager = $this->getMockBuilder(StoreManagerInterface::class)->getMock(); - $this->mockValueFactory = $this->getMockBuilder(ValueFactory::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->mockValueFactory->method('create')->willReturn( - $this->objectManager->getObject( - Value::class, - ['callback' => function () { - return ''; - }] - ) - ); - - $mockProductUrlPathGenerator = $this->getMockBuilder(ProductUrlPathGenerator::class) - ->disableOriginalConstructor() - ->getMock(); - $mockProductUrlPathGenerator->method('getUrlPathWithSuffix')->willReturn('product_url.html'); - - $this->subject = $this->objectManager->getObject( - CanonicalUrl::class, - [ - 'valueFactory' => $this->mockValueFactory, - 'storeManager' => $this->mockStoreManager, - 'productUrlPathGenerator' => $mockProductUrlPathGenerator - ] - ); - } -} diff --git a/app/code/Magento/CatalogGraphQl/composer.json b/app/code/Magento/CatalogGraphQl/composer.json index eb86ac634412..950b496263ff 100644 --- a/app/code/Magento/CatalogGraphQl/composer.json +++ b/app/code/Magento/CatalogGraphQl/composer.json @@ -14,6 +14,7 @@ }, "suggest": { "magento/module-graph-ql": "*", + "magento/module-graph-ql-cache": "*", "magento/module-store-graph-ql": "*" }, "license": [ diff --git a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml index 68a292ede6b4..a5bd42860ded 100644 --- a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml @@ -6,6 +6,7 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\Catalog\Model\Product\Option\Type\Date" type="Magento\CatalogGraphQl\Model\Product\Option\DateType" /> <type name="Magento\CatalogGraphQl\Model\ProductInterfaceTypeResolverComposite"> <arguments> <argument name="productTypeNameResolvers" xsi:type="array"> @@ -37,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> @@ -73,4 +78,6 @@ </argument> </arguments> </virtualType> + <preference for="Magento\Framework\Search\Adapter\Mysql\Query\Builder\Match" + type="Magento\CatalogGraphQl\Model\Search\Adapter\Mysql\Query\Builder\Match" /> </config> diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index 9235ec271a3c..9f102a1c6a15 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -9,184 +9,11 @@ type Query { currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), sort: ProductSortInput @doc(description: "Specifies which attribute to sort on, and whether to return the results in ascending or descending order.") ): Products - @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Products") @doc(description: "The products query searches for products that match the criteria specified in the search and filter attributes") + @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Products") @doc(description: "The products query searches for products that match the criteria specified in the search and filter attributes") @cache(cacheTag: "cat_p", cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Identity") category ( id: Int @doc(description: "Id of the category") ): CategoryTree - @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoryTree") -} - -enum CurrencyEnum @doc(description: "The list of available currency codes") { - AFN - ALL - AZN - DZD - AOA - ARS - AMD - AWG - AUD - BSD - BHD - BDT - BBD - BYR - BZD - BMD - BTN - BOB - BAM - BWP - BRL - GBP - BND - BGN - BUK - BIF - KHR - CAD - CVE - CZK - KYD - GQE - CLP - CNY - COP - KMF - CDF - CRC - HRK - CUP - DKK - DJF - DOP - XCD - EGP - SVC - ERN - EEK - ETB - EUR - FKP - FJD - GMD - GEK - GEL - GHS - GIP - GTQ - GNF - GYD - HTG - HNL - HKD - HUF - ISK - INR - IDR - IRR - IQD - ILS - JMD - JPY - JOD - KZT - KES - KWD - KGS - LAK - LVL - LBP - LSL - LRD - LYD - LTL - MOP - MKD - MGA - MWK - MYR - MVR - LSM - MRO - MUR - MXN - MDL - MNT - MAD - MZN - MMK - NAD - NPR - ANG - YTL - NZD - NIC - NGN - KPW - NOK - OMR - PKR - PAB - PGK - PYG - PEN - PHP - PLN - QAR - RHD - RON - RUB - RWF - SHP - STD - SAR - RSD - SCR - SLL - SGD - SKK - SBD - SOS - ZAR - KRW - LKR - SDG - SRD - SZL - SEK - CHF - SYP - TWD - TJS - TZS - THB - TOP - TTD - TND - TMM - USD - UGX - UAH - AED - UYU - UZS - VUV - VEB - VEF - VND - CHE - CHW - XOF - WST - YER - ZMK - ZWD - TRY - AZM - ROL - TRL - XPF + @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoryTree") @doc(description: "The category query searches for categories that match the criteria specified in the search and filter attributes") @cache(cacheTag: "cat_c", cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoryTreeIdentity") } type Price @doc(description: "The Price object defines the price of a product as well as any tax-related adjustments.") { @@ -214,11 +41,6 @@ enum PriceTypeEnum @doc(description: "This enumeration the price type.") { DYNAMIC } -type Money @doc(description: "A Money object defines a monetary value, including a numeric value and a currency code.") { - value: Float @doc(description: "A number expressing a monetary value") - currency: CurrencyEnum @doc(description: "A three-letter currency code, such as USD or EUR") -} - type ProductPrices @doc(description: "The ProductPrices object contains the regular price of an item, as well as its minimum and maximum prices. Only composite products, which include bundle, configurable, and grouped products, can contain a minimum and maximum price.") { minimalPrice: Price @doc(description: "The lowest possible final price for all the options defined within a composite product. If you are specifying a price range, this would be the from value.") maximalPrice: Price @doc(description: "The highest possible final price for all the options defined within a composite product. If you are specifying a price range, this would be the to value.") @@ -248,8 +70,8 @@ interface ProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\ id: Int @doc(description: "The ID number assigned to the product") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\EntityIdToId") name: String @doc(description: "The product name. Customers use this name to identify the product.") sku: String @doc(description: "A number or code assigned to a product to identify the product, options, price, and manufacturer") - description: String @doc(description: "Detailed information about the product. The value can include simple HTML tags.") - short_description: String @doc(description: "A short description of the product. Its use depends on the theme.") + description: ComplexTextValue @doc(description: "Detailed information about the product. The value can include simple HTML tags.") @resolver(class: "\\Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductComplexTextAttribute") + short_description: ComplexTextValue @doc(description: "A short description of the product. Its use depends on the theme.") @resolver(class: "\\Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductComplexTextAttribute") special_price: Float @doc(description: "The discounted price of the product") special_from_date: String @doc(description: "The beginning date that a product has a special price") special_to_date: String @doc(description: "The end date that a product has a special price") @@ -257,16 +79,13 @@ interface ProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\ meta_title: String @doc(description: "A string that is displayed in the title bar and tab of the browser and in search results lists") meta_keyword: String @doc(description: "A comma-separated list of keywords that are visible only to search engines") meta_description: String @doc(description: "A brief overview of the product for search results listings, maximum 255 characters") - image: String @doc(description: "The relative path to the main image on the product page") - small_image: String @doc(description: "The relative path to the small image, which is used on catalog pages") - thumbnail: String @doc(description: "The relative path to the product's thumbnail image") + image: ProductImage @doc(description: "The relative path to the main image on the product page") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductImage") + small_image: ProductImage @doc(description: "The relative path to the small image, which is used on catalog pages") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductImage") + thumbnail: ProductImage @doc(description: "The relative path to the product's thumbnail image") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductImage") new_from_date: String @doc(description: "The beginning date for new product listings, and determines if the product is featured as a new product") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\NewFromTo") new_to_date: String @doc(description: "The end date for new product listings") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\NewFromTo") tier_price: Float @doc(description: "The price when tier pricing is in effect and the items purchased threshold has been reached") options_container: String @doc(description: "If the product has multiple options, determines where they appear on the product page") - image_label: String @doc(description: "The label assigned to a product image") - small_image_label: String @doc(description: "The label assigned to a product's small image") - thumbnail_label: String @doc(description: "The label assigned to a product's thumbnail image") created_at: String @doc(description: "Timestamp indicating when the product was created") updated_at: String @doc(description: "Timestamp indicating when the product was updated") country_of_manufacture: String @doc(description: "The product's country of origin") @@ -278,7 +97,7 @@ interface ProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\ price: ProductPrices @doc(description: "A ProductPrices object, indicating the price of an item") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Price") gift_message_available: String @doc(description: "Indicates whether a gift message is available") manufacturer: Int @doc(description: "A number representing the product's manufacturer") - categories: [CategoryInterface] @doc(description: "The categories assigned to a product") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Categories") + categories: [CategoryInterface] @doc(description: "The categories assigned to a product") @cache(cacheTag: "cat_c", cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoriesIdentity") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Categories") canonical_url: String @doc(description: "Canonical URL") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CanonicalUrl") } @@ -326,6 +145,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") @@ -352,10 +184,16 @@ type CustomizableFileValue @doc(description: "CustomizableFileValue defines the image_size_y: Int @doc(description: "The maximum height of an image") } +type ProductImage @doc(description: "Product image information. Contains image relative path, URL and label") { + url: String @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductImage\\Url") + label: String @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductImage\\Label") +} + interface CustomizableOptionInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\CustomizableOptionTypeResolver") @doc(description: "The CustomizableOptionInterface contains basic information about a customizable option. It can be implemented by several types of configurable options.") { title: String @doc(description: "The display name for this option") required: Boolean @doc(description: "Indicates whether the option is required") sort_order: Int @doc(description: "The order in which the option is displayed") + option_id: Int @doc(description: "Option ID") } interface CustomizableProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\ProductInterfaceTypeResolverComposite") @doc(description: "CustomizableProductInterface contains information about customizable product options.") { @@ -364,7 +202,7 @@ interface CustomizableProductInterface @typeResolver(class: "Magento\\CatalogGra interface CategoryInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\CategoryInterfaceTypeResolver") @doc(description: "CategoryInterface contains the full set of attributes that can be returned in a category search") { id: Int @doc(description: "An ID that uniquely identifies the category") - description: String @doc(description: "An optional description of the category") + description: String @doc(description: "An optional description of the category") @resolver(class: "\\Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoryHtmlAttribute") name: String @doc(description: "The display name of the category") path: String @doc(description: "Category Path") path_in_store: String @doc(description: "Category path in store") @@ -374,13 +212,13 @@ interface CategoryInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model level: Int @doc(description: "Indicates the depth of the category within the tree") created_at: String @doc(description: "Timestamp indicating when the category was created") updated_at: String @doc(description: "Timestamp indicating when the category was updated") - product_count: Int @doc(description: "The number of products in the category") + product_count: Int @doc(description: "The number of products in the category") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\ProductsCount") default_sort_by: String @doc(description: "The attribute to use for sorting") products( pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. This attribute is optional."), currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), sort: ProductSortInput @doc(description: "Specifies which attribute to sort on, and whether to return the results in ascending or descending order.") - ): CategoryProducts @doc(description: "The list of products assigned to the category") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\Products") + ): CategoryProducts @doc(description: "The list of products assigned to the category") @cache(cacheTag: "cat_p", cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Identity") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\Products") breadcrumbs: [Breadcrumb] @doc(description: "Breadcrumbs, parent categories info") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\Breadcrumbs") } @@ -404,6 +242,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") { } @@ -415,7 +266,7 @@ type Products @doc(description: "The Products object is the top-level object ret items: [ProductInterface] @doc(description: "An array of products that match the specified search criteria") page_info: SearchResultPageInfo @doc(description: "An object that includes the page_info and currentPage values specified in the query") total_count: Int @doc(description: "The number of products returned") - filters: [LayerFilter] @doc(description: "Layered navigation filters array") + filters: [LayerFilter] @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\LayerFilters") @doc(description: "Layered navigation filters array") sort_fields: SortFields @doc(description: "An object that includes the default sort field and all available sort fields") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\SortFields") } @@ -431,7 +282,7 @@ input ProductFilterInput @doc(description: "ProductFilterInput defines the filte description: FilterTypeInput @doc(description: "Detailed information about the product. The value can include simple HTML tags.") short_description: FilterTypeInput @doc(description: "A short description of the product. Its use depends on the theme.") price: FilterTypeInput @doc(description: "The price of an item") - special_price: FilterTypeInput @doc(description: "The discounted price of the product") + special_price: FilterTypeInput @doc(description: "The discounted price of the product. Do not include the currency code.") special_from_date: FilterTypeInput @doc(description: "The beginning date that a product has a special price") special_to_date: FilterTypeInput @doc(description: "The end date that a product has a special price") weight: FilterTypeInput @doc(description: "The weight of the item, in units defined by the store") @@ -448,7 +299,6 @@ input ProductFilterInput @doc(description: "ProductFilterInput defines the filte custom_layout_update: FilterTypeInput @doc(description: "XML code that is applied as a layout update to the product page") min_price: FilterTypeInput @doc(description:"The numeric minimal price of the product. Do not include the currency code.") max_price: FilterTypeInput @doc(description:"The numeric maximal price of the product. Do not include the currency code.") - special_price: FilterTypeInput @doc(description:"The numeric special price of the product. Do not include the currency code.") category_id: FilterTypeInput @doc(description: "Category ID the product belongs to") options_container: FilterTypeInput @doc(description: "If the product has multiple options, determines where they appear on the product page") required_options: FilterTypeInput @doc(description: "Indicates whether the product has required options") @@ -548,6 +398,6 @@ type SortField { } type SortFields @doc(description: "SortFields contains a default value for sort fields and all available sort fields") { - default: String @doc(description: "Default value of sort fields") + default: String @doc(description: "Default value of sort fields") options: [SortField] @doc(description: "Available sort fields") } diff --git a/app/code/Magento/CatalogImportExport/Model/Export/Product.php b/app/code/Magento/CatalogImportExport/Model/Export/Product.php index 23aa8d65ddb0..75249e490786 100644 --- a/app/code/Magento/CatalogImportExport/Model/Export/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Export/Product.php @@ -20,6 +20,7 @@ * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @since 100.0.2 */ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity @@ -349,12 +350,14 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity private $productEntityLinkField; /** + * Product constructor. + * * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate * @param \Magento\Eav\Model\Config $config * @param \Magento\Framework\App\ResourceConnection $resource * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Psr\Log\LoggerInterface $logger - * @param \Magento\Catalog\Model\ResourceModel\Product\Collection $collection + * @param \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $collectionFactory * @param \Magento\ImportExport\Model\Export\ConfigInterface $exportConfig * @param \Magento\Catalog\Model\ResourceModel\ProductFactory $productFactory * @param \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory $attrSetColFactory @@ -363,10 +366,10 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity * @param \Magento\Catalog\Model\ResourceModel\Product\Option\CollectionFactory $optionColFactory * @param \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory $attributeColFactory * @param Product\Type\Factory $_typeFactory - * @param \Magento\Catalog\Model\Product\LinkTypeProvider $linkTypeProvider - * @param \Magento\CatalogImportExport\Model\Export\RowCustomizerInterface $rowCustomizer + * @param ProductEntity\LinkTypeProvider $linkTypeProvider + * @param RowCustomizerInterface $rowCustomizer * @param array $dateAttrCodes - * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * @throws \Magento\Framework\Exception\LocalizedException */ public function __construct( \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, @@ -520,10 +523,13 @@ protected function getMediaGallery(array $productIds) if (empty($productIds)) { return []; } + + $productEntityJoinField = $this->getProductEntityLinkField(); + $select = $this->_connection->select()->from( ['mgvte' => $this->_resourceModel->getTableName('catalog_product_entity_media_gallery_value_to_entity')], [ - "mgvte.{$this->getProductEntityLinkField()}", + "mgvte.$productEntityJoinField", 'mgvte.value_id' ] )->joinLeft( @@ -535,7 +541,7 @@ protected function getMediaGallery(array $productIds) ] )->joinLeft( ['mgv' => $this->_resourceModel->getTableName('catalog_product_entity_media_gallery_value')], - '(mg.value_id = mgv.value_id)', + "(mg.value_id = mgv.value_id) and (mgvte.$productEntityJoinField = mgv.$productEntityJoinField)", [ 'mgv.label', 'mgv.position', @@ -543,14 +549,14 @@ protected function getMediaGallery(array $productIds) 'mgv.store_id', ] )->where( - "mgvte.{$this->getProductEntityLinkField()} IN (?)", + "mgvte.$productEntityJoinField IN (?)", $productIds ); $rowMediaGallery = []; $stmt = $this->_connection->query($select); while ($mediaRow = $stmt->fetch()) { - $rowMediaGallery[$mediaRow[$this->getProductEntityLinkField()]][] = [ + $rowMediaGallery[$mediaRow[$productEntityJoinField]][] = [ '_media_attribute_id' => $mediaRow['attribute_id'], '_media_image' => $mediaRow['filename'], '_media_label' => $mediaRow['label'], @@ -692,7 +698,9 @@ protected function updateDataWithCategoryColumns(&$dataRow, &$rowCategories, $pr } /** - * {@inheritdoc} + * Get header columns + * + * @return string[] */ public function _getHeaderColumns() { @@ -751,7 +759,10 @@ protected function _getExportMainAttrCodes() } /** - * {@inheritdoc} + * Get entity collection + * + * @param bool $resetCollection + * @return \Magento\Framework\Data\Collection\AbstractDb */ protected function _getEntityCollection($resetCollection = false) { @@ -796,7 +807,7 @@ protected function getItemsPerPage() // Maximal Products limit $maxProductsLimit = 5000; - $this->_itemsPerPage = intval( + $this->_itemsPerPage = (int)( ($memoryLimit * $memoryUsagePercent - memory_get_usage(true)) / $memoryPerProduct ); if ($this->_itemsPerPage < $minProductsLimit) { @@ -858,7 +869,10 @@ public function export() } /** - * {@inheritdoc} + * Apply filter to collection and add not skipped attributes to select. + * + * @param \Magento\Eav\Model\Entity\Collection\AbstractCollection $collection + * @return \Magento\Eav\Model\Entity\Collection\AbstractCollection * @since 100.2.0 */ protected function _prepareEntityCollection(\Magento\Eav\Model\Entity\Collection\AbstractCollection $collection) @@ -920,8 +934,7 @@ protected function getExportData() } /** - * Load products' data from the collection - * and filter it (if needed). + * Load products' data from the collection and filter it (if needed). * * @return array Keys are product IDs, values arrays with keys as store IDs * and values as store-specific versions of Product entity. @@ -929,15 +942,17 @@ protected function getExportData() protected function loadCollection(): array { $data = []; - $collection = $this->_getEntityCollection(); foreach (array_keys($this->_storeIdToCode) as $storeId) { + $collection->setOrder('entity_id', 'asc'); + $this->_prepareEntityCollection($collection); $collection->setStoreId($storeId); + $collection->load(); foreach ($collection as $itemId => $item) { $data[$itemId][$storeId] = $item; } + $collection->clear(); } - $collection->clear(); return $data; } @@ -1063,6 +1078,8 @@ private function wrapValue($value) } /** + * Collect multi raw data from + * * @return array */ protected function collectMultirawData() @@ -1104,6 +1121,8 @@ protected function collectMultirawData() } /** + * Check the current data has multiselect value + * * @param \Magento\Catalog\Model\Product $item * @param int $storeId * @return bool @@ -1116,6 +1135,8 @@ protected function hasMultiselectData($item, $storeId) } /** + * Collect multiselect values based on value + * * @param \Magento\Catalog\Model\Product $item * @param string $attrCode * @param int $storeId @@ -1140,6 +1161,8 @@ protected function collectMultiselectValues($item, $attrCode, $storeId) } /** + * Check attribute is valid. + * * @param string $code * @param mixed $value * @return bool @@ -1155,10 +1178,16 @@ protected function isValidAttributeValue($code, $value) $isValid = false; } + if (is_array($value)) { + $isValid = false; + } + return $isValid; } /** + * Append multi row data + * * @param array $dataRow * @param array $multiRawData * @return array @@ -1268,11 +1297,23 @@ private function appendMultirowData(&$dataRow, $multiRawData) } if (!empty($multiRawData['customOptionsData'][$productLinkId][$storeId])) { + $shouldBeMerged = true; $customOptionsRows = $multiRawData['customOptionsData'][$productLinkId][$storeId]; - $multiRawData['customOptionsData'][$productLinkId][$storeId] = []; - $customOptions = implode(ImportProduct::PSEUDO_MULTI_LINE_SEPARATOR, $customOptionsRows); - $dataRow = array_merge($dataRow, ['custom_options' => $customOptions]); + if ($storeId != Store::DEFAULT_STORE_ID + && !empty($multiRawData['customOptionsData'][$productLinkId][Store::DEFAULT_STORE_ID]) + ) { + $defaultCustomOptions = $multiRawData['customOptionsData'][$productLinkId][Store::DEFAULT_STORE_ID]; + if (!array_diff($defaultCustomOptions, $customOptionsRows)) { + $shouldBeMerged = false; + } + } + + if ($shouldBeMerged) { + $multiRawData['customOptionsData'][$productLinkId][$storeId] = []; + $customOptions = implode(ImportProduct::PSEUDO_MULTI_LINE_SEPARATOR, $customOptionsRows); + $dataRow = array_merge($dataRow, ['custom_options' => $customOptions]); + } } if (empty($dataRow)) { @@ -1288,6 +1329,8 @@ private function appendMultirowData(&$dataRow, $multiRawData) } /** + * Add multi row data to export + * * @deprecated 100.1.0 * @param array $dataRow * @param array $multiRawData @@ -1335,6 +1378,8 @@ protected function _customHeadersMapping($rowData) } /** + * Convert option row to cell string + * * @param array $option * @return string */ @@ -1364,6 +1409,7 @@ protected function optionRowToCellString($option) protected function getCustomOptionsData($productIds) { $customOptionsData = []; + $defaultOptionsData = []; foreach (array_keys($this->_storeIdToCode) as $storeId) { $options = $this->_optionColFactory->create(); @@ -1376,38 +1422,42 @@ protected function getCustomOptionsData($productIds) ->addValuesToResult($storeId); foreach ($options as $option) { + $optionData = $option->toArray(); $row = []; $productId = $option['product_id']; $row['name'] = $option['title']; $row['type'] = $option['type']; - if (Store::DEFAULT_STORE_ID === $storeId) { - $row['required'] = $option['is_require']; - $row['price'] = $option['price']; - $row['price_type'] = ($option['price_type'] === 'percent') ? 'percent' : 'fixed'; - $row['sku'] = $option['sku']; - if ($option['max_characters']) { - $row['max_characters'] = $option['max_characters']; - } - - foreach (['file_extension', 'image_size_x', 'image_size_y'] as $fileOptionKey) { - if (!isset($option[$fileOptionKey])) { - continue; - } - $row[$fileOptionKey] = $option[$fileOptionKey]; + $row['required'] = $this->getOptionValue('is_require', $defaultOptionsData, $optionData); + $row['price'] = $this->getOptionValue('price', $defaultOptionsData, $optionData); + $row['sku'] = $this->getOptionValue('sku', $defaultOptionsData, $optionData); + if (array_key_exists('max_characters', $optionData) + || array_key_exists('max_characters', $defaultOptionsData) + ) { + $row['max_characters'] = $this->getOptionValue('max_characters', $defaultOptionsData, $optionData); + } + foreach (['file_extension', 'image_size_x', 'image_size_y'] as $fileOptionKey) { + if (isset($option[$fileOptionKey]) || isset($defaultOptionsData[$fileOptionKey])) { + $row[$fileOptionKey] = $this->getOptionValue($fileOptionKey, $defaultOptionsData, $optionData); } } + $percentType = $this->getOptionValue('price_type', $defaultOptionsData, $optionData); + $row['price_type'] = ($percentType === 'percent') ? 'percent' : 'fixed'; + + if (Store::DEFAULT_STORE_ID === $storeId) { + $optionId = $option['option_id']; + $defaultOptionsData[$optionId] = $option->toArray(); + } + $values = $option->getValues(); if ($values) { foreach ($values as $value) { $row['option_title'] = $value['title']; - if (Store::DEFAULT_STORE_ID === $storeId) { - $row['option_title'] = $value['title']; - $row['price'] = $value['price']; - $row['price_type'] = ($value['price_type'] === 'percent') ? 'percent' : 'fixed'; - $row['sku'] = $value['sku']; - } + $row['option_title'] = $value['title']; + $row['price'] = $value['price']; + $row['price_type'] = ($value['price_type'] === 'percent') ? 'percent' : 'fixed'; + $row['sku'] = $value['sku']; $customOptionsData[$productId][$storeId][] = $this->optionRowToCellString($row); } } else { @@ -1421,6 +1471,31 @@ protected function getCustomOptionsData($productIds) return $customOptionsData; } + /** + * Get value for custom option according to store or default value + * + * @param string $optionName + * @param array $defaultOptionsData + * @param array $optionData + * @return mixed + */ + private function getOptionValue($optionName, $defaultOptionsData, $optionData) + { + $optionId = $optionData['option_id']; + + if (array_key_exists($optionName, $optionData) && $optionData[$optionName] !== null) { + return $optionData[$optionName]; + } + + if (array_key_exists($optionId, $defaultOptionsData) + && array_key_exists($optionName, $defaultOptionsData[$optionId]) + ) { + return $defaultOptionsData[$optionId][$optionName]; + } + + return null; + } + /** * Clean up already loaded attribute collection. * diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index 6c3bef2a52b0..edeb955b19c9 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -3,18 +3,21 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\CatalogImportExport\Model\Import; +use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Config as CatalogConfig; use Magento\Catalog\Model\Product\Visibility; -use Magento\CatalogImportExport\Model\Import\Product\MediaGalleryProcessor; use Magento\CatalogImportExport\Model\Import\Product\ImageTypeProcessor; +use Magento\CatalogImportExport\Model\Import\Product\MediaGalleryProcessor; use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface as ValidatorInterface; -use Magento\CatalogInventory\Api\Data\StockItemInterface; use Magento\CatalogImportExport\Model\StockItemImporterInterface; +use Magento\CatalogInventory\Api\Data\StockItemInterface; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Filesystem; use Magento\Framework\Intl\DateTimeFactory; use Magento\Framework\Model\ResourceModel\Db\ObjectRelationProcessor; @@ -295,7 +298,8 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity ValidatorInterface::ERROR_MEDIA_URL_NOT_ACCESSIBLE => 'Imported resource (image) could not be downloaded from external resource due to timeout or access permissions', ValidatorInterface::ERROR_INVALID_WEIGHT => 'Product weight is invalid', ValidatorInterface::ERROR_DUPLICATE_URL_KEY => 'Url key: \'%s\' was already generated for an item with the SKU: \'%s\'. You need to specify the unique URL key manually', - ValidatorInterface::ERROR_DUPLICATE_MULTISELECT_VALUES => "Value for multiselect attribute %s contains duplicated values", + ValidatorInterface::ERROR_DUPLICATE_MULTISELECT_VALUES => 'Value for multiselect attribute %s contains duplicated values', + 'invalidNewToDateValue' => 'Make sure new_to_date is later than or the same as new_from_date', ]; //@codingStandardsIgnoreEnd @@ -732,6 +736,11 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity */ private $dateTimeFactory; + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + /** * @param \Magento\Framework\Json\Helper\Data $jsonHelper * @param \Magento\ImportExport\Helper\Data $importExportData @@ -776,7 +785,11 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity * @param MediaGalleryProcessor $mediaProcessor * @param StockItemImporterInterface|null $stockItemImporter * @param DateTimeFactory $dateTimeFactory + * @param ProductRepositoryInterface|null $productRepository + * @throws LocalizedException + * @throws \Magento\Framework\Exception\FileSystemException * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function __construct( \Magento\Framework\Json\Helper\Data $jsonHelper, @@ -821,7 +834,8 @@ public function __construct( ImageTypeProcessor $imageTypeProcessor = null, MediaGalleryProcessor $mediaProcessor = null, StockItemImporterInterface $stockItemImporter = null, - DateTimeFactory $dateTimeFactory = null + DateTimeFactory $dateTimeFactory = null, + ProductRepositoryInterface $productRepository = null ) { $this->_eventManager = $eventManager; $this->stockRegistry = $stockRegistry; @@ -875,6 +889,8 @@ public function __construct( ->initImagesArrayKeys(); $this->validator->init($this); $this->dateTimeFactory = $dateTimeFactory ?? ObjectManager::getInstance()->get(DateTimeFactory::class); + $this->productRepository = $productRepository ?? ObjectManager::getInstance() + ->get(ProductRepositoryInterface::class); } /** @@ -890,7 +906,7 @@ public function isAttributeValid($attrCode, array $attrParams, array $rowData, $ { if (!$this->validator->isAttributeValid($attrCode, $attrParams, $rowData)) { foreach ($this->validator->getMessages() as $message) { - $this->addRowError($message, $rowNum, $attrCode); + $this->skipRow($rowNum, $message, ProcessingError::ERROR_LEVEL_NOT_CRITICAL, $attrCode); } return false; } @@ -898,8 +914,8 @@ public function isAttributeValid($attrCode, array $attrParams, array $rowData, $ } /** - * * Multiple value separator getter. + * * @return string */ public function getMultipleValueSeparator() @@ -949,6 +965,8 @@ public function getMediaGalleryAttributeId() } /** + * Retrieve product type by name. + * * @param string $name * @return Product\Type\AbstractType */ @@ -1189,8 +1207,10 @@ protected function _initErrorTemplates() } /** - * Set valid attribute set and product type to rows with all scopes - * to ensure that existing products doesn't changed. + * Set valid attribute set and product type to rows. + * + * Set valid attribute set and product type to rows with all + * scopes to ensure that existing products doesn't changed. * * @param array $rowData * @return array @@ -1220,12 +1240,14 @@ protected function _prepareRowForDb(array $rowData) /** * Gather and save information about product links. + * * Must be called after ALL products saving done. * * @return $this * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * phpcs:disable Generic.Metrics.NestingLevel */ protected function _saveLinks() { @@ -1235,7 +1257,7 @@ protected function _saveLinks() $nextLinkId = $this->_resourceHelper->getNextAutoincrement($mainTable); // pre-load 'position' attributes ID for each link type once - foreach ($this->_linkNameToId as $linkName => $linkId) { + foreach ($this->_linkNameToId as $linkId) { $select = $this->_connection->select()->from( $resource->getTable('catalog_product_link_attribute'), ['id' => 'product_link_attribute_id'] @@ -1353,6 +1375,7 @@ protected function _saveLinks() } return $this; } + // phpcs:enable /** * Save product attributes. @@ -1406,7 +1429,7 @@ protected function _saveProductCategories(array $categoriesData) $delProductId[] = $productId; foreach (array_keys($categories) as $categoryId) { - $categoriesIn[] = ['product_id' => $productId, 'category_id' => $categoryId, 'position' => 1]; + $categoriesIn[] = ['product_id' => $productId, 'category_id' => $categoryId, 'position' => 0]; } } if (Import::BEHAVIOR_APPEND != $this->getBehavior()) { @@ -1468,6 +1491,7 @@ public function saveProductEntity(array $entityRowsIn, array $entityRowsUp) /** * Return additional data, needed to select. + * * @return array */ private function getOldSkuFieldsForSelect() @@ -1477,6 +1501,7 @@ private function getOldSkuFieldsForSelect() /** * Adds newly created products to _oldSku + * * @param array $newProducts * @return void */ @@ -1514,6 +1539,7 @@ private function getNewSkuFieldsForSelect() /** * Init media gallery resources + * * @return void * @since 100.0.4 * @deprecated @@ -1544,6 +1570,8 @@ protected function getExistingImages($bunch) } /** + * Retrieve image from row. + * * @param array $rowData * @return array */ @@ -1582,6 +1610,7 @@ public function getImagesFromRow(array $rowData) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @SuppressWarnings(PHPMD.UnusedLocalVariable) * @throws LocalizedException + * phpcs:disable Generic.Metrics.NestingLevel */ protected function _saveProducts() { @@ -1599,6 +1628,7 @@ protected function _saveProducts() $tierPrices = []; $mediaGallery = []; $labelsForUpdate = []; + $imagesForChangeVisibility = []; $uploadedImages = []; $previousType = null; $prevAttributeSet = null; @@ -1617,14 +1647,25 @@ protected function _saveProducts() } $rowScope = $this->getRowScope($rowData); - $rowData[self::URL_KEY] = $this->getUrlKey($rowData); + $urlKey = $this->getUrlKey($rowData); + if (!empty($rowData[self::URL_KEY])) { + // If url_key column and its value were in the CSV file + $rowData[self::URL_KEY] = $urlKey; + } elseif ($this->isNeedToChangeUrlKey($rowData)) { + // If url_key column was empty or even not declared in the CSV file but by the rules it is need to + // be setteed. In case when url_key is generating from name column we have to ensure that the bunch + // of products will pass for the event with url_key column. + $bunch[$rowNum][self::URL_KEY] = $rowData[self::URL_KEY] = $urlKey; + } $rowSku = $rowData[self::COL_SKU]; if (null === $rowSku) { $this->getErrorAggregator()->addRowToSkip($rowNum); continue; - } elseif (self::SCOPE_STORE == $rowScope) { + } + + if (self::SCOPE_STORE == $rowScope) { // set necessary data from SCOPE_DEFAULT row $rowData[self::COL_TYPE] = $this->skuProcessor->getNewSku($rowSku)['type_id']; $rowData['attribute_set_id'] = $this->skuProcessor->getNewSku($rowSku)['attr_set_id']; @@ -1687,6 +1728,14 @@ protected function _saveProducts() $websiteId = $this->storeResolver->getWebsiteCodeToId($websiteCode); $this->websitesCache[$rowSku][$websiteId] = true; } + } else { + $product = $this->retrieveProductBySku($rowSku); + if ($product) { + $websiteIds = $product->getWebsiteIds(); + foreach ($websiteIds as $websiteId) { + $this->websitesCache[$rowSku][$websiteId] = true; + } + } } // 3. Categories phase @@ -1718,21 +1767,24 @@ protected function _saveProducts() } // 5. Media gallery phase - $disabledImages = []; list($rowImages, $rowLabels) = $this->getImagesFromRow($rowData); $storeId = !empty($rowData[self::COL_STORE]) ? $this->getStoreIdByCode($rowData[self::COL_STORE]) : Store::DEFAULT_STORE_ID; - if (isset($rowData['_media_is_disabled']) && strlen(trim($rowData['_media_is_disabled']))) { - $disabledImages = array_flip( - explode($this->getMultipleValueSeparator(), $rowData['_media_is_disabled']) - ); + $imageHiddenStates = $this->getImagesHiddenStates($rowData); + foreach (array_keys($imageHiddenStates) as $image) { + if (array_key_exists($rowSku, $existingImages) + && array_key_exists($image, $existingImages[$rowSku]) + ) { + $rowImages[self::COL_MEDIA_IMAGE][] = $image; + $uploadedImages[$image] = $image; + } + if (empty($rowImages)) { - foreach (array_keys($disabledImages) as $disabledImage) { - $rowImages[self::COL_MEDIA_IMAGE][] = $disabledImage; - } + $rowImages[self::COL_MEDIA_IMAGE][] = $image; } } + $rowData[self::COL_MEDIA_IMAGE] = []; /* @@ -1748,6 +1800,7 @@ protected function _saveProducts() if ($uploadedFile) { $uploadedImages[$columnImage] = $uploadedFile; } else { + unset($rowData[$column]); $this->addRowError( ValidatorInterface::ERROR_MEDIA_URL_NOT_ACCESSIBLE, $rowNum, @@ -1766,13 +1819,23 @@ protected function _saveProducts() if ($uploadedFile && !isset($mediaGallery[$storeId][$rowSku][$uploadedFile])) { if (isset($existingImages[$rowSku][$uploadedFile])) { + $currentFileData = $existingImages[$rowSku][$uploadedFile]; if (isset($rowLabels[$column][$columnImageKey]) && $rowLabels[$column][$columnImageKey] != - $existingImages[$rowSku][$uploadedFile]['label'] + $currentFileData['label'] ) { $labelsForUpdate[] = [ 'label' => $rowLabels[$column][$columnImageKey], - 'imageData' => $existingImages[$rowSku][$uploadedFile] + 'imageData' => $currentFileData + ]; + } + + if (array_key_exists($uploadedFile, $imageHiddenStates) + && $currentFileData['disabled'] != $imageHiddenStates[$uploadedFile] + ) { + $imagesForChangeVisibility[] = [ + 'disabled' => $imageHiddenStates[$uploadedFile], + 'imageData' => $currentFileData ]; } } else { @@ -1785,7 +1848,8 @@ protected function _saveProducts() ? $rowLabels[$column][$columnImageKey] : '', 'position' => ++$position, - 'disabled' => isset($disabledImages[$columnImage]) ? '1' : '0', + 'disabled' => isset($imageHiddenStates[$columnImage]) + ? $imageHiddenStates[$columnImage] : '0', 'value' => $uploadedFile, ]; } @@ -1905,6 +1969,8 @@ protected function _saveProducts() $mediaGallery )->_saveProductAttributes( $attributes + )->updateMediaGalleryVisibility( + $imagesForChangeVisibility )->updateMediaGalleryLabels( $labelsForUpdate ); @@ -1917,8 +1983,37 @@ protected function _saveProducts() return $this; } + // phpcs:enable + + /** + * Prepare array with image states (visible or hidden from product page) + * + * @param array $rowData + * @return array + */ + private function getImagesHiddenStates($rowData) + { + $statesArray = []; + $mappingArray = [ + '_media_is_disabled' => '1' + ]; + + foreach ($mappingArray as $key => $value) { + if (isset($rowData[$key]) && strlen(trim($rowData[$key]))) { + $items = explode($this->getMultipleValueSeparator(), $rowData[$key]); + + foreach ($items as $item) { + $statesArray[$item] = $value; + } + } + } + + return $statesArray; + } /** + * Resolve valid category ids from provided row data. + * * @param array $rowData * @return array */ @@ -1941,11 +2036,18 @@ protected function processRowCategories($rowData) . ' ' . $error['exception']->getMessage() ); } + } else { + $product = $this->retrieveProductBySku($rowData['sku']); + if ($product) { + $categoryIds = $product->getCategoryIds(); + } } return $categoryIds; } /** + * Get product websites. + * * @param string $productSku * @return array */ @@ -1955,6 +2057,8 @@ public function getProductWebsites($productSku) } /** + * Retrieve product categories. + * * @param string $productSku * @return array */ @@ -1964,6 +2068,8 @@ public function getProductCategories($productSku) } /** + * Get store id by code. + * * @param string $storeCode * @return array|int|null|string */ @@ -2055,6 +2161,8 @@ protected function _getUploader() } /** + * Retrieve uploader. + * * @return Uploader * @throws \Magento\Framework\Exception\LocalizedException */ @@ -2065,6 +2173,7 @@ public function getUploader() /** * Uploading files into the "catalog/product" media folder. + * * Return a new file name if the same file is already exists. * * @param string $fileName @@ -2259,7 +2368,7 @@ public function getEntityTypeCode() * Returns array of new products data with SKU as key. All SKU keys are in lowercase for avoiding creation of * new products with the same SKU in different letter cases. * - * @var string $sku + * @param string $sku * @return array */ public function getNewSku($sku = null) @@ -2323,6 +2432,7 @@ public function getRowScope(array $rowData) * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @throws \Zend_Validate_Exception */ public function validateRow(array $rowData, $rowNum) { @@ -2338,32 +2448,35 @@ public function validateRow(array $rowData, $rowNum) // BEHAVIOR_DELETE and BEHAVIOR_REPLACE use specific validation logic if (Import::BEHAVIOR_REPLACE == $this->getBehavior()) { if (self::SCOPE_DEFAULT == $rowScope && !$this->isSkuExist($sku)) { - $this->addRowError(ValidatorInterface::ERROR_SKU_NOT_FOUND_FOR_DELETE, $rowNum); + $this->skipRow($rowNum, ValidatorInterface::ERROR_SKU_NOT_FOUND_FOR_DELETE); return false; } } if (Import::BEHAVIOR_DELETE == $this->getBehavior()) { if (self::SCOPE_DEFAULT == $rowScope && !$this->isSkuExist($sku)) { - $this->addRowError(ValidatorInterface::ERROR_SKU_NOT_FOUND_FOR_DELETE, $rowNum); + $this->skipRow($rowNum, ValidatorInterface::ERROR_SKU_NOT_FOUND_FOR_DELETE); return false; } return true; } + // if product doesn't exist, need to throw critical error else all errors should be not critical. + $errorLevel = $this->getValidationErrorLevel($sku); + if (!$this->validator->isValid($rowData)) { foreach ($this->validator->getMessages() as $message) { - $this->addRowError($message, $rowNum, $this->validator->getInvalidAttribute()); + $this->skipRow($rowNum, $message, $errorLevel, $this->validator->getInvalidAttribute()); } } if (null === $sku) { - $this->addRowError(ValidatorInterface::ERROR_SKU_IS_EMPTY, $rowNum); + $this->skipRow($rowNum, ValidatorInterface::ERROR_SKU_IS_EMPTY, $errorLevel); } elseif (false === $sku) { - $this->addRowError(ValidatorInterface::ERROR_ROW_IS_ORPHAN, $rowNum); + $this->skipRow($rowNum, ValidatorInterface::ERROR_ROW_IS_ORPHAN, $errorLevel); } elseif (self::SCOPE_STORE == $rowScope && !$this->storeResolver->getStoreCodeToId($rowData[self::COL_STORE]) ) { - $this->addRowError(ValidatorInterface::ERROR_INVALID_STORE, $rowNum); + $this->skipRow($rowNum, ValidatorInterface::ERROR_INVALID_STORE, $errorLevel); } // SKU is specified, row is SCOPE_DEFAULT, new product block begins @@ -2378,16 +2491,15 @@ public function validateRow(array $rowData, $rowNum) $this->prepareNewSkuData($sku) ); } else { - $this->addRowError(ValidatorInterface::ERROR_TYPE_UNSUPPORTED, $rowNum); + $this->skipRow($rowNum, ValidatorInterface::ERROR_TYPE_UNSUPPORTED, $errorLevel); } } else { // validate new product type and attribute set - if (!isset($rowData[self::COL_TYPE]) || !isset($this->_productTypeModels[$rowData[self::COL_TYPE]])) { - $this->addRowError(ValidatorInterface::ERROR_INVALID_TYPE, $rowNum); - } elseif (!isset($rowData[self::COL_ATTR_SET]) - || !isset($this->_attrSetNameToId[$rowData[self::COL_ATTR_SET]]) + if (!isset($rowData[self::COL_TYPE], $this->_productTypeModels[$rowData[self::COL_TYPE]])) { + $this->skipRow($rowNum, ValidatorInterface::ERROR_INVALID_TYPE, $errorLevel); + } elseif (!isset($rowData[self::COL_ATTR_SET], $this->_attrSetNameToId[$rowData[self::COL_ATTR_SET]]) ) { - $this->addRowError(ValidatorInterface::ERROR_INVALID_ATTR_SET, $rowNum); + $this->skipRow($rowNum, ValidatorInterface::ERROR_INVALID_ATTR_SET, $errorLevel); } elseif ($this->skuProcessor->getNewSku($sku) === null) { $this->skuProcessor->addNewSku( $sku, @@ -2443,15 +2555,35 @@ public function validateRow(array $rowData, $rowNum) ValidatorInterface::ERROR_DUPLICATE_URL_KEY, $rowNum, $rowData[self::COL_NAME], - $message - ); + $message, + $errorLevel + ) + ->getErrorAggregator() + ->addRowToSkip($rowNum); } } } + + if (!empty($rowData['new_from_date']) && !empty($rowData['new_to_date']) + ) { + $newFromTimestamp = strtotime($this->dateTime->formatDate($rowData['new_from_date'], false)); + $newToTimestamp = strtotime($this->dateTime->formatDate($rowData['new_to_date'], false)); + if ($newFromTimestamp > $newToTimestamp) { + $this->skipRow( + $rowNum, + 'invalidNewToDateValue', + $errorLevel, + $rowData['new_to_date'] + ); + } + } + return !$this->getErrorAggregator()->isRowInvalid($rowNum); } /** + * Check if need to validate url key. + * * @param array $rowData * @return bool */ @@ -2459,8 +2591,8 @@ private function isNeedToValidateUrlKey($rowData) { return (!empty($rowData[self::URL_KEY]) || !empty($rowData[self::COL_NAME])) && (empty($rowData[self::COL_VISIBILITY]) - || $rowData[self::COL_VISIBILITY] - !== (string)Visibility::getOptionArray()[Visibility::VISIBILITY_NOT_VISIBLE]); + || $rowData[self::COL_VISIBILITY] + !== (string)Visibility::getOptionArray()[Visibility::VISIBILITY_NOT_VISIBLE]); } /** @@ -2763,14 +2895,17 @@ protected function getProductUrlSuffix($storeId = null) } /** + * Retrieve url key from provided row data. + * * @param array $rowData * @return string + * * @since 100.0.3 */ protected function getUrlKey($rowData) { if (!empty($rowData[self::URL_KEY])) { - return strtolower($rowData[self::URL_KEY]); + return $this->productUrl->formatUrlKey($rowData[self::URL_KEY]); } if (!empty($rowData[self::COL_NAME])) { @@ -2781,7 +2916,10 @@ protected function getUrlKey($rowData) } /** + * Retrieve resource. + * * @return Proxy\Product\ResourceModel + * * @since 100.0.3 */ protected function getResource() @@ -2792,6 +2930,26 @@ protected function getResource() return $this->_resource; } + /** + * Whether a url key is needed to be change. + * + * @param array $rowData + * @return bool + */ + private function isNeedToChangeUrlKey(array $rowData): bool + { + $urlKey = $this->getUrlKey($rowData); + $productExists = $this->isSkuExist($rowData[self::COL_SKU]); + $markedToEraseUrlKey = isset($rowData[self::URL_KEY]); + // The product isn't new and the url key index wasn't marked for change. + if (!$urlKey && $productExists && !$markedToEraseUrlKey) { + // Seems there is no need to change the url key + return false; + } + + return true; + } + /** * Get product entity link field * @@ -2835,6 +2993,21 @@ private function updateMediaGalleryLabels(array $labels) } } + /** + * Update 'disabled' field for media gallery entity + * + * @param array $images + * @return $this + */ + private function updateMediaGalleryVisibility(array $images) + { + if (!empty($images)) { + $this->mediaProcessor->updateMediaGalleryVisibility($images); + } + + return $this; + } + /** * Parse values from multiple attributes fields * @@ -2897,9 +3070,7 @@ private function formatStockDataForRow(array $rowData): array if ($this->stockConfiguration->isQty($this->skuProcessor->getNewSku($sku)['type_id'])) { $stockItemDo->setData($row); - $row['is_in_stock'] = isset($row['is_in_stock']) && $stockItemDo->getBackorders() - ? $row['is_in_stock'] - : $this->stockStateProvider->verifyStock($stockItemDo); + $row['is_in_stock'] = $row['is_in_stock'] ?? $this->stockStateProvider->verifyStock($stockItemDo); if ($this->stockStateProvider->verifyNotification($stockItemDo)) { $date = $this->dateTimeFactory->create('now', new \DateTimeZone('UTC')); $row['low_stock_date'] = $date->format(DateTime::DATETIME_PHP_FORMAT); @@ -2911,4 +3082,54 @@ private function formatStockDataForRow(array $rowData): array return $row; } + + /** + * Retrieve product by sku. + * + * @param string $sku + * @return \Magento\Catalog\Api\Data\ProductInterface|null + */ + private function retrieveProductBySku($sku) + { + try { + $product = $this->productRepository->get($sku); + } catch (NoSuchEntityException $e) { + return null; + } + return $product; + } + + /** + * Add row as skipped + * + * @param int $rowNum + * @param string $errorCode Error code or simply column name + * @param string $errorLevel error level + * @param string|null $colName optional column name + * @return $this + */ + private function skipRow( + $rowNum, + string $errorCode, + string $errorLevel = ProcessingError::ERROR_LEVEL_NOT_CRITICAL, + $colName = null + ): self { + $this->addRowError($errorCode, $rowNum, $colName, null, $errorLevel); + $this->getErrorAggregator() + ->addRowToSkip($rowNum); + return $this; + } + + /** + * Returns errorLevel for validation + * + * @param string $sku + * @return string + */ + private function getValidationErrorLevel($sku): string + { + return (!$this->isSkuExist($sku) && Import::BEHAVIOR_REPLACE !== $this->getBehavior()) + ? ProcessingError::ERROR_LEVEL_CRITICAL + : ProcessingError::ERROR_LEVEL_NOT_CRITICAL; + } } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/CategoryProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/CategoryProcessor.php index db5b07279ed1..951989146e67 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/CategoryProcessor.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/CategoryProcessor.php @@ -66,6 +66,8 @@ public function __construct( } /** + * Initialize categories + * * @return $this */ protected function initCategories() @@ -104,7 +106,6 @@ protected function initCategories() * * @param string $name * @param int $parentId - * * @return int */ protected function createCategory($name, $parentId) @@ -120,13 +121,8 @@ protected function createCategory($name, $parentId) $category->setIsActive(true); $category->setIncludeInMenu(true); $category->setAttributeSetId($category->getDefaultAttributeSetId()); - try { - $category->save(); - $this->categoriesCache[$category->getId()] = $category; - } catch (\Exception $e) { - $this->addFailedCategory($category, $e); - } - + $category->save(); + $this->categoriesCache[$category->getId()] = $category; return $category->getId(); } @@ -134,7 +130,6 @@ protected function createCategory($name, $parentId) * Returns ID of category by string path creating nonexistent ones. * * @param string $categoryPath - * * @return int */ protected function upsertCategory($categoryPath) @@ -165,7 +160,6 @@ protected function upsertCategory($categoryPath) * * @param string $categoriesString * @param string $categoriesSeparator - * * @return array */ public function upsertCategories($categoriesString, $categoriesSeparator) @@ -234,7 +228,7 @@ public function clearFailedCategories() */ public function getCategoryById($categoryId) { - return isset($this->categoriesCache[$categoryId]) ? $this->categoriesCache[$categoryId] : null; + return $this->categoriesCache[$categoryId] ?? null; } /** diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php index ec7c6a117299..d43dc11a68fc 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php @@ -103,7 +103,7 @@ public function __construct( /** * Save product media gallery. * - * @param $mediaGalleryData + * @param array $mediaGalleryData * @return void */ public function saveMediaGallery(array $mediaGalleryData) @@ -152,14 +152,37 @@ public function saveMediaGallery(array $mediaGalleryData) * @return void */ public function updateMediaGalleryLabels(array $labels) + { + $this->updateMediaGalleryField($labels, 'label'); + } + + /** + * Update 'disabled' field for media gallery entity + * + * @param array $images + * @return void + */ + public function updateMediaGalleryVisibility(array $images) + { + $this->updateMediaGalleryField($images, 'disabled'); + } + + /** + * Update value for requested field in media gallery entities + * + * @param array $data + * @param string $field + * @return void + */ + private function updateMediaGalleryField(array $data, $field) { $insertData = []; - foreach ($labels as $label) { - $imageData = $label['imageData']; + foreach ($data as $datum) { + $imageData = $datum['imageData']; - if ($imageData['label'] === null) { + if ($imageData[$field] === null) { $insertData[] = [ - 'label' => $label['label'], + $field => $datum[$field], $this->getProductEntityLinkField() => $imageData[$this->getProductEntityLinkField()], 'value_id' => $imageData['value_id'], 'store_id' => Store::DEFAULT_STORE_ID, @@ -168,7 +191,7 @@ public function updateMediaGalleryLabels(array $labels) $this->connection->update( $this->mediaGalleryValueTableName, [ - 'label' => $label['label'], + $field => $datum[$field], ], [ $this->getProductEntityLinkField() . ' = ?' => $imageData[$this->getProductEntityLinkField()], @@ -224,6 +247,7 @@ public function getExistingImages(array $bunch) ), [ 'label' => 'mgv.label', + 'disabled' => 'mgv.disabled', ] )->joinInner( ['pe' => $this->productEntityTableName], @@ -263,7 +287,7 @@ private function initMediaGalleryResources() /** * Save media gallery data per store. * - * @param $storeId + * @param int $storeId * @param array $mediaGalleryData * @param array $newMediaValues * @param array $valueToProductId @@ -339,6 +363,8 @@ private function getProductEntityLinkField() } /** + * Get resource. + * * @return \Magento\CatalogImportExport\Model\Import\Proxy\Product\ResourceModel */ private function getResource() diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php index ae29fd2ef4bd..7435c0bebfc1 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php @@ -333,6 +333,11 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity */ private $optionTypeTitles; + /** + * @var array + */ + private $lastOptionTitle; + /** * @param \Magento\ImportExport\Model\ResourceModel\Import\Data $importData * @param ResourceConnection $resource @@ -426,7 +431,10 @@ protected function _initMessageTemplates() ); $this->_productEntity->addMessageTemplate( self::ERROR_INVALID_TYPE, - __('Value for \'type\' sub attribute in \'custom_options\' attribute contains incorrect value, acceptable values are: \'dropdown\', \'checkbox\'') + __( + 'Value for \'type\' sub attribute in \'custom_options\' attribute contains incorrect value, acceptable values are: %1', + '\''.implode('\', \'', array_keys($this->_specificTypes)).'\'' + ) ); $this->_productEntity->addMessageTemplate(self::ERROR_EMPTY_TITLE, __('Please enter a value for title.')); $this->_productEntity->addMessageTemplate( @@ -624,7 +632,7 @@ public function validateAmbiguousData() $this->_addRowsErrors(self::ERROR_AMBIGUOUS_NEW_NAMES, $errorRows); return false; } - if ($this->getBehavior() == \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND) { + if ($this->getBehavior() == Import::BEHAVIOR_APPEND) { $errorRows = $this->_findOldOptionsWithTheSameTitles(); if ($errorRows) { $this->_addRowsErrors(self::ERROR_AMBIGUOUS_OLD_NAMES, $errorRows); @@ -962,11 +970,10 @@ public function validateRow(array $rowData, $rowNumber) return false; } } - return true; } } - return false; + return true; } /** @@ -1117,6 +1124,8 @@ protected function _getMultiRowFormat($rowData) } /** + * Process option row. + * * @param string $name * @param array $optionRow * @return array @@ -1198,6 +1207,7 @@ private function addFileOptions($result, $optionRow) /** * Import data rows. + * * Additional store view data (option titles) will be sought in store view specified import file rows * * @return boolean @@ -1206,7 +1216,6 @@ private function addFileOptions($result, $optionRow) protected function _importData() { $this->_initProductsSku(); - $nextOptionId = $this->_resourceHelper->getNextAutoincrement($this->_tables['catalog_product_option']); $nextValueId = $this->_resourceHelper->getNextAutoincrement( $this->_tables['catalog_product_option_type_value'] @@ -1225,7 +1234,6 @@ protected function _importData() $parentCount = []; $childCount = []; $optionsToRemove = []; - foreach ($bunch as $rowNumber => $rowData) { if (isset($optionId, $valueId) && empty($rowData[PRODUCT::COL_STORE_VIEW_CODE])) { $nextOptionId = $optionId; @@ -1273,17 +1281,26 @@ protected function _importData() $parentCount, $childCount ); + $this->_collectOptionTitle($combinedData, $prevOptionId, $titles); + $this->checkOptionTitles( + $options, + $titles, + $combinedData, + $prevOptionId, + $optionId, + $products, + $prices + ); } } - $this->removeExistingOptions($products, $optionsToRemove); - $types = [ 'values' => $typeValues, 'prices' => $typePrices, 'titles' => $typeTitles, ]; + $this->setLastOptionTitle($titles); //Save prepared custom options data. $this->savePreparedCustomOptions( $products, @@ -1293,11 +1310,69 @@ protected function _importData() $types ); } - return true; } /** + * Check options titles. + * + * If products were split up between bunches, + * this function will add needed option for option titles + * + * @param array $options + * @param array $titles + * @param array $combinedData + * @param int $prevOptionId + * @param int $optionId + * @param array $products + * @param array $prices + * @return void + */ + private function checkOptionTitles( + array &$options, + array &$titles, + array $combinedData, + int &$prevOptionId, + int &$optionId, + array $products, + array $prices + ) : void { + $titlesCount = count($titles); + if ($titlesCount > 0 && count($options) !== $titlesCount) { + $combinedData[Product::COL_STORE_VIEW_CODE] = ''; + $optionId--; + $option = $this->_collectOptionMainData( + $combinedData, + $prevOptionId, + $optionId, + $products, + $prices + ); + if ($option) { + $options[] = $option; + } + } + } + + /** + * Setting last Custom Option Title + * to use it later in _collectOptionTitle + * to set correct title for default store view + * + * @param array $titles + */ + private function setLastOptionTitle(array &$titles) : void + { + if (count($titles) > 0) { + end($titles); + $key = key($titles); + $this->lastOptionTitle[$key] = $titles[$key]; + } + } + + /** + * Remove existing options. + * * Remove all existing options if import behaviour is APPEND * in other case remove options for products with empty "custom_options" row only. * @@ -1308,7 +1383,7 @@ protected function _importData() */ private function removeExistingOptions(array $products, array $optionsToRemove): void { - if ($this->getBehavior() != \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND) { + if ($this->getBehavior() != Import::BEHAVIOR_APPEND) { $this->_deleteEntities(array_keys($products)); } elseif (!empty($optionsToRemove)) { // Remove options for products with empty "custom_options" row @@ -1447,8 +1522,12 @@ protected function _collectOptionTitle(array $rowData, $prevOptionId, array &$ti $defaultStoreId = Store::DEFAULT_STORE_ID; if (!empty($rowData[self::COLUMN_TITLE])) { if (!isset($titles[$prevOptionId][$defaultStoreId])) { - // ensure default title is set - $titles[$prevOptionId][$defaultStoreId] = $rowData[self::COLUMN_TITLE]; + if (isset($this->lastOptionTitle[$prevOptionId])) { + $titles[$prevOptionId] = $this->lastOptionTitle[$prevOptionId]; + unset($this->lastOptionTitle); + } else { + $titles[$prevOptionId][$defaultStoreId] = $rowData[self::COLUMN_TITLE]; + } } $titles[$prevOptionId][$this->_rowStoreId] = $rowData[self::COLUMN_TITLE]; } @@ -1547,6 +1626,8 @@ private function getExistingOptionTypeId($optionId, $storeId, $optionTypeTitle) } /** + * Rarse required data. + * * Parse required data from current row and store to class internal variables some data * for underlying dependent rows * @@ -1715,7 +1796,8 @@ protected function _getSpecificTypeData(array $rowData, $optionTypeId, $defaultS ]; $priceData = false; - if (!empty($rowData[self::COLUMN_ROW_PRICE])) { + $customOptionRowPrice = $rowData[self::COLUMN_ROW_PRICE]; + if (!empty($customOptionRowPrice) || $customOptionRowPrice === '0') { $priceData = [ 'price' => (double)rtrim($rowData[self::COLUMN_ROW_PRICE], '%'), 'price_type' => 'fixed', @@ -2028,7 +2110,7 @@ private function savePreparedCustomOptions( array $types ): void { if ($this->_isReadyForSaving($options, $titles, $types['values'])) { - if ($this->getBehavior() == \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND) { + if ($this->getBehavior() == Import::BEHAVIOR_APPEND) { $this->_compareOptionsWithExisting($options, $titles, $prices, $types['values']); $this->restoreOriginalOptionTypeIds($types['values'], $types['prices'], $types['titles']); } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/SkuProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/SkuProcessor.php index addd1523f87a..ea6049ba651a 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/SkuProcessor.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/SkuProcessor.php @@ -142,7 +142,7 @@ public function getNewSku($sku = null) { if ($sku !== null) { $sku = strtolower($sku); - return isset($this->newSkus[$sku]) ? $this->newSkus[$sku] : null; + return $this->newSkus[$sku] ?? null; } return $this->newSkus; } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/StoreResolver.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/StoreResolver.php index be5644e22b05..3dff0188a7db 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/StoreResolver.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/StoreResolver.php @@ -75,7 +75,7 @@ public function getWebsiteCodeToId($code = null) $this->_initWebsites(); } if ($code) { - return isset($this->websiteCodeToId[$code]) ? $this->websiteCodeToId[$code] : null; + return $this->websiteCodeToId[$code] ?? null; } return $this->websiteCodeToId; } @@ -90,7 +90,7 @@ public function getWebsiteCodeToStoreIds($code = null) $this->_initWebsites(); } if ($code) { - return isset($this->websiteCodeToStoreIds[$code]) ? $this->websiteCodeToStoreIds[$code] : null; + return $this->websiteCodeToStoreIds[$code] ?? null; } return $this->websiteCodeToStoreIds; } @@ -119,7 +119,7 @@ public function getStoreCodeToId($code = null) $this->_initStores(); } if ($code) { - return isset($this->storeCodeToId[$code]) ? $this->storeCodeToId[$code] : null; + return $this->storeCodeToId[$code] ?? null; } return $this->storeCodeToId; } @@ -134,7 +134,7 @@ public function getStoreIdToWebsiteStoreIds($code = null) $this->_initStores(); } if ($code) { - return isset($this->storeIdToWebsiteStoreIds[$code]) ? $this->storeIdToWebsiteStoreIds[$code] : null; + return $this->storeIdToWebsiteStoreIds[$code] ?? null; } return $this->storeIdToWebsiteStoreIds; } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php index afd018f077d2..3b6caef66ce6 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php @@ -195,6 +195,8 @@ public function __construct( } /** + * Initialize template for error message. + * * @param array $templateCollection * @return $this */ @@ -377,6 +379,8 @@ public function retrieveAttributeFromCache($attributeCode) } /** + * Adding attribute option. + * * In case we've dynamically added new attribute option during import we need to add it to our cache * in order to keep it up to date. * @@ -508,8 +512,10 @@ public function isSuitable() } /** - * Prepare attributes values for save: exclude non-existent, static or with empty values attributes; - * set default values if needed + * Adding default attribute to product before save. + * + * Prepare attributes values for save: exclude non-existent, static or with empty values attributes, + * set default values if needed. * * @param array $rowData * @param bool $withDefaultValue @@ -537,9 +543,9 @@ public function prepareAttributesWithDefaultValueForSave(array $rowData, $withDe } else { $resultAttrs[$attrCode] = $rowData[$attrCode]; } - } elseif (array_key_exists($attrCode, $rowData)) { + } elseif (array_key_exists($attrCode, $rowData) && empty($rowData['_store'])) { $resultAttrs[$attrCode] = $rowData[$attrCode]; - } elseif ($withDefaultValue && null !== $attrParams['default_value']) { + } elseif ($withDefaultValue && null !== $attrParams['default_value'] && empty($rowData['_store'])) { $resultAttrs[$attrCode] = $attrParams['default_value']; } } @@ -611,7 +617,8 @@ protected function getProductEntityLinkField() } /** - * Clean cached values + * Clean cached values. + * * @since 100.2.0 */ public function __destruct() diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php index 2aa210599188..4b7416f6ad9a 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php @@ -7,6 +7,7 @@ use Magento\CatalogImportExport\Model\Import\Product; use Magento\Framework\Validator\AbstractValidator; +use Magento\Catalog\Model\Product\Attribute\Backend\Sku; /** * Class Validator @@ -60,6 +61,8 @@ public function __construct( } /** + * Text validation + * * @param mixed $attrCode * @param string $type * @return bool @@ -69,6 +72,8 @@ protected function textValidation($attrCode, $type) $val = $this->string->cleanString($this->_rowData[$attrCode]); if ($type == 'text') { $valid = $this->string->strlen($val) < Product::DB_MAX_TEXT_LENGTH; + } else if ($attrCode == Product::COL_SKU) { + $valid = $this->string->strlen($val) <= SKU::SKU_MAX_LENGTH; } else { $valid = $this->string->strlen($val) < Product::DB_MAX_VARCHAR_LENGTH; } @@ -105,6 +110,8 @@ private function validateOption($attrCode, $possibleOptions, $value) } /** + * Numeric validation + * * @param mixed $attrCode * @param string $type * @return bool @@ -132,6 +139,8 @@ protected function numericValidation($attrCode, $type) } /** + * Is required attribute valid + * * @param string $attrCode * @param array $attributeParams * @param array $rowData @@ -159,6 +168,8 @@ public function isRequiredAttributeValid($attrCode, array $attributeParams, arra } /** + * Is attribute valid + * * @param string $attrCode * @param array $attrParams * @param array $rowData @@ -255,6 +266,8 @@ public function isAttributeValid($attrCode, array $attrParams, array $rowData) } /** + * Set invalid attribute + * * @param string|null $attribute * @return void * @since 100.1.0 @@ -265,6 +278,8 @@ protected function setInvalidAttribute($attribute) } /** + * Get invalid attribute + * * @return string * @since 100.1.0 */ @@ -274,6 +289,8 @@ public function getInvalidAttribute() } /** + * Is valid attributes + * * @return bool * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ @@ -300,7 +317,7 @@ protected function isValidAttributes() } /** - * {@inheritdoc} + * @inheritdoc */ public function isValid($value) { @@ -331,6 +348,8 @@ public function getRowScope(array $rowData) } /** + * Init + * * @param \Magento\CatalogImportExport\Model\Import\Product $context * @return $this */ diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php b/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php index e7ffe408cc73..3ac7f98818d7 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php @@ -101,7 +101,7 @@ class Uploader extends \Magento\MediaStorage\Model\File\Uploader * @param \Magento\MediaStorage\Model\File\Validator\NotProtectedExtension $validator * @param \Magento\Framework\Filesystem $filesystem * @param \Magento\Framework\Filesystem\File\ReadFactory $readFactory - * @param null $filePath + * @param string|null $filePath * @throws \Magento\Framework\Exception\LocalizedException */ public function __construct( @@ -113,15 +113,15 @@ public function __construct( \Magento\Framework\Filesystem\File\ReadFactory $readFactory, $filePath = null ) { - if ($filePath !== null) { - $this->_setUploadFile($filePath); - } $this->_imageFactory = $imageFactory; $this->_coreFileStorageDb = $coreFileStorageDb; $this->_coreFileStorage = $coreFileStorage; $this->_validator = $validator; $this->_directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); $this->_readFactory = $readFactory; + if ($filePath !== null) { + $this->_setUploadFile($filePath); + } } /** @@ -146,20 +146,24 @@ public function init() * @param string $fileName * @param bool $renameFileOff * @return array + * @throws \Magento\Framework\Exception\LocalizedException */ public function move($fileName, $renameFileOff = false) { if ($renameFileOff) { $this->setAllowRenameFiles(false); } + + if ($this->getTmpDir()) { + $filePath = $this->getTmpDir() . '/'; + } else { + $filePath = ''; + } + if (preg_match('/\bhttps?:\/\//i', $fileName, $matches)) { $url = str_replace($matches[0], '', $fileName); - - if ($matches[0] === $this->httpScheme) { - $read = $this->_readFactory->create($url, DriverPool::HTTP); - } else { - $read = $this->_readFactory->create($url, DriverPool::HTTPS); - } + $driver = $matches[0] === $this->httpScheme ? DriverPool::HTTP : DriverPool::HTTPS; + $read = $this->_readFactory->create($url, $driver); //only use filename (for URI with query parameters) $parsedUrlPath = parse_url($url, PHP_URL_PATH); @@ -170,24 +174,19 @@ public function move($fileName, $renameFileOff = false) } } - if ($this->getTmpDir()) { - $filePath = $this->getTmpDir() . '/'; - } else { - $filePath = ''; + $fileExtension = pathinfo($fileName, PATHINFO_EXTENSION); + if ($fileExtension && !$this->checkAllowedExtension($fileExtension)) { + throw new \Magento\Framework\Exception\LocalizedException(__('Disallowed file type.')); } + $fileName = preg_replace('/[^a-z0-9\._-]+/i', '', $fileName); - $filePath = $this->_directory->getRelativePath($filePath . $fileName); + $relativePath = $this->_directory->getRelativePath($filePath . $fileName); $this->_directory->writeFile( - $filePath, + $relativePath, $read->readAll() ); } - if ($this->getTmpDir()) { - $filePath = $this->getTmpDir() . '/'; - } else { - $filePath = ''; - } $filePath = $this->_directory->getRelativePath($filePath . $fileName); $this->_setUploadFile($filePath); $destDir = $this->_directory->getAbsolutePath($this->getDestDir()); @@ -353,7 +352,7 @@ protected function _moveFile($tmpPath, $destPath) } /** - * {@inheritdoc} + * @inheritdoc */ protected function chmod($file) { diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/ActionGroup/AdminExportActionGroup.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/ActionGroup/AdminExportActionGroup.xml new file mode 100644 index 000000000000..b9eea2b11463 --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/ActionGroup/AdminExportActionGroup.xml @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + + <!-- Export products using filtering by attribute --> + <actionGroup name="exportProductsFilterByAttribute"> + <arguments> + <argument name="attribute" type="string"/> + <argument name="attributeData" type="string"/> + </arguments> + <selectOption selector="{{AdminExportMainSection.entityType}}" userInput="Products" stepKey="selectProductsOption"/> + <waitForElementVisible selector="{{AdminExportMainSection.entityAttributes}}" stepKey="waitForElementVisible"/> + <scrollTo selector="{{AdminExportAttributeSection.chooseAttribute('attribute')}}" stepKey="scrollToAttribute" /> + <checkOption selector="{{AdminExportAttributeSection.chooseAttribute('attribute')}}" stepKey="selectAttribute"/> + <fillField selector="{{AdminExportAttributeSection.fillFilter('attribute')}}" userInput="{{attributeData}}" stepKey="setDataInField"/> + <waitForPageLoad stepKey="waitForUserInput"/> + <scrollTo selector="{{AdminExportAttributeSection.continueBtn}}" stepKey="scrollToContinue" /> + <click selector="{{AdminExportAttributeSection.continueBtn}}" stepKey="clickContinueButton"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="Message is added to queue, wait to get your file soon" stepKey="seeSuccessMessage"/> + </actionGroup> + + <!-- Export products without filtering --> + <actionGroup name="exportAllProducts"> + <selectOption selector="{{AdminExportMainSection.entityType}}" userInput="Products" stepKey="selectProductsOption"/> + <waitForElementVisible selector="{{AdminExportMainSection.entityAttributes}}" stepKey="waitForElementVisible" time="5"/> + <scrollTo selector="{{AdminExportAttributeSection.continueBtn}}" stepKey="scrollToContinue"/> + <wait stepKey="waitForScroll" time="5"/> + <click selector="{{AdminExportAttributeSection.continueBtn}}" stepKey="clickContinueButton"/> + <wait stepKey="waitForClick" time="5"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="Message is added to queue, wait to get your file soon" stepKey="seeSuccessMessage"/> + </actionGroup> + + <!-- Download first file in the grid --> + <actionGroup name="downloadFileByRowIndex"> + <arguments> + <argument name="rowIndex" type="string"/> + </arguments> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitFormReload"/> + <click stepKey="clickSelectBtn" selector="{{AdminExportAttributeSection.selectByIndex(rowIndex)}}"/> + <click stepKey="clickOnDownload" selector="{{AdminExportAttributeSection.download(rowIndex)}}" after="clickSelectBtn"/> + </actionGroup> + + <!-- Delete exported file --> + <actionGroup name="deleteExportedFile"> + <arguments> + <argument name="rowIndex" type="string"/> + </arguments> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitFormReload"/> + <click stepKey="clickSelectBtn" selector="{{AdminExportAttributeSection.selectByIndex(rowIndex)}}"/> + <click stepKey="clickOnDelete" selector="{{AdminExportAttributeSection.delete(rowIndex)}}" after="clickSelectBtn"/> + <waitForElementVisible selector="{{AdminProductGridConfirmActionSection.title}}" stepKey="waitForConfirmModal"/> + <click selector="{{AdminProductGridConfirmActionSection.ok}}" stepKey="confirmDelete"/> + <waitForPageLoad stepKey="waitForExportDataDeleted" /> + <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportBundleProductTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportBundleProductTest.xml new file mode 100644 index 000000000000..1f5ae6b6905b --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportBundleProductTest.xml @@ -0,0 +1,126 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminExportBundleProductTest"> + <annotations> + <features value="CatalogImportExport"/> + <stories value="Export products"/> + <title value="Export Bundle Product"/> + <description value="Admin should be able to export Bundle Product"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14008"/> + <group value="catalog_import_export"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!--Create bundle product with dynamic price with two simple products --> + <createData entity="SimpleProduct2" stepKey="firstSimpleProductForDynamic"/> + <createData entity="SimpleProduct2" stepKey="secondSimpleProductForDynamic"/> + <createData entity="ApiBundleProduct" stepKey="createDynamicBundleProduct"/> + <createData entity="DropDownBundleOption" stepKey="createFirstBundleOption"> + <requiredEntity createDataKey="createDynamicBundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="firstLinkOptionToDynamicProduct"> + <requiredEntity createDataKey="createDynamicBundleProduct"/> + <requiredEntity createDataKey="createFirstBundleOption"/> + <requiredEntity createDataKey="firstSimpleProductForDynamic"/> + </createData> + <createData entity="ApiBundleLink" stepKey="secondLinkOptionToDynamicProduct"> + <requiredEntity createDataKey="createDynamicBundleProduct"/> + <requiredEntity createDataKey="createFirstBundleOption"/> + <requiredEntity createDataKey="secondSimpleProductForDynamic"/> + </createData> + + <!-- Create bundle product with fixed price with two simple products --> + <createData entity="SimpleProduct2" stepKey="firstSimpleProductForFixed"/> + <createData entity="SimpleProduct2" stepKey="secondSimpleProductForFixed"/> + <createData entity="ApiFixedBundleProduct" stepKey="createFixedBundleProduct"/> + <createData entity="DropDownBundleOption" stepKey="createSecondBundleOption"> + <requiredEntity createDataKey="createFixedBundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="firstLinkOptionToFixedProduct"> + <requiredEntity createDataKey="createFixedBundleProduct"/> + <requiredEntity createDataKey="createSecondBundleOption"/> + <requiredEntity createDataKey="firstSimpleProductForFixed"/> + </createData> + <createData entity="ApiBundleLink" stepKey="secondLinkOptionToFixedProduct"> + <requiredEntity createDataKey="createFixedBundleProduct"/> + <requiredEntity createDataKey="createSecondBundleOption"/> + <requiredEntity createDataKey="secondSimpleProductForFixed"/> + </createData> + + <!-- Create bundle product with custom textarea attribute with two simple products --> + <createData entity="productAttributeWysiwyg" stepKey="createProductAttribute"/> + <createData entity="AddToDefaultSet" stepKey="addToDefaultAttributeSet"> + <requiredEntity createDataKey="createProductAttribute"/> + </createData> + <createData entity="ApiFixedBundleProduct" stepKey="createFixedBundleProductWithAttribute"> + <requiredEntity createDataKey="addToDefaultAttributeSet"/> + </createData> + <createData entity="SimpleProduct2" stepKey="firstSimpleProductForFixedWithAttribute"/> + <createData entity="SimpleProduct2" stepKey="secondSimpleProductForFixedWithAttribute"/> + <createData entity="DropDownBundleOption" stepKey="createBundleOptionWithAttribute"> + <requiredEntity createDataKey="createFixedBundleProductWithAttribute"/> + </createData> + <createData entity="ApiBundleLink" stepKey="firstLinkOptionToFixedProductWithAttribute"> + <requiredEntity createDataKey="createFixedBundleProductWithAttribute"/> + <requiredEntity createDataKey="createBundleOptionWithAttribute"/> + <requiredEntity createDataKey="firstSimpleProductForFixedWithAttribute"/> + </createData> + <createData entity="ApiBundleLink" stepKey="secondLinkOptionToFixedProductWithAttribute"> + <requiredEntity createDataKey="createFixedBundleProductWithAttribute"/> + <requiredEntity createDataKey="createBundleOptionWithAttribute"/> + <requiredEntity createDataKey="secondSimpleProductForFixedWithAttribute"/> + </createData> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + </before> + <after> + <!-- Delete exported file --> + <actionGroup ref="deleteExportedFile" stepKey="deleteExportedFile"> + <argument name="rowIndex" value="0"/> + </actionGroup> + + <!-- Delete products creations --> + <deleteData createDataKey="createDynamicBundleProduct" stepKey="deleteDynamicBundleProduct"/> + <deleteData createDataKey="firstSimpleProductForDynamic" stepKey="deleteFirstSimpleProductForDynamic"/> + <deleteData createDataKey="secondSimpleProductForDynamic" stepKey="deleteSecondSimpleProductForDynamic"/> + <deleteData createDataKey="createFixedBundleProduct" stepKey="deleteFixedBundleProduct"/> + <deleteData createDataKey="firstSimpleProductForFixed" stepKey="deleteFirstSimpleProductForFixed"/> + <deleteData createDataKey="secondSimpleProductForFixed" stepKey="deleteSecondSimpleProductForFixed"/> + <deleteData createDataKey="createFixedBundleProductWithAttribute" stepKey="deleteFixedBundleProductWithAttribute"/> + <deleteData createDataKey="firstSimpleProductForFixedWithAttribute" stepKey="deleteFirstSimpleProductForFixedWithAttribute"/> + <deleteData createDataKey="secondSimpleProductForFixedWithAttribute" stepKey="deleteSecondSimpleProductForFixedWithAttribute"/> + <deleteData createDataKey="createProductAttribute" stepKey="deleteProductAttribute"/> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to export page --> + <amOnPage url="{{AdminExportIndexPage.url}}" stepKey="goToExportIndexPage"/> + + <!-- Export created below products --> + <actionGroup ref="exportAllProducts" stepKey="exportCreatedProducts"/> + + <!-- Run cron --> + <magentoCLI command="cron:run" stepKey="runCron3"/> + + <!-- Download product --> + <actionGroup ref="downloadFileByRowIndex" stepKey="downloadCreatedProducts"> + <argument name="rowIndex" value="0"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml new file mode 100644 index 000000000000..a587d71ba0e6 --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml @@ -0,0 +1,91 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminExportGroupedProductWithSpecialPriceTest"> + <annotations> + <features value="CatalogImportExport"/> + <stories value="Export products"/> + <title value="Export grouped product with special price"/> + <description value="Admin should be able to export grouped product with special price"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14009"/> + <group value="catalog_import_export"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create first simple product and add special price --> + <createData entity="SimpleProduct2" stepKey="createFirstSimpleProduct"/> + <createData entity="specialProductPrice2" stepKey="specialPriceToFirstProduct"> + <requiredEntity createDataKey="createFirstSimpleProduct"/> + </createData> + + <!-- Create second simple product and add special price--> + <createData entity="SimpleProduct2" stepKey="createSecondSimpleProduct"/> + <createData entity="specialProductPrice2" stepKey="specialPriceToSecondProduct"> + <requiredEntity createDataKey="createSecondSimpleProduct"/> + </createData> + + <!-- Create category --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + + <!-- Create group product with created below simple products --> + <createData entity="ApiGroupedProduct2" stepKey="createGroupedProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="OneSimpleProductLink" stepKey="addFirstProduct"> + <requiredEntity createDataKey="createGroupedProduct"/> + <requiredEntity createDataKey="createFirstSimpleProduct"/> + </createData> + <updateData entity="OneMoreSimpleProductLink" createDataKey="addFirstProduct" stepKey="addSecondProduct"> + <requiredEntity createDataKey="createGroupedProduct"/> + <requiredEntity createDataKey="createSecondSimpleProduct"/> + </updateData> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + </before> + <after> + <!-- Delete exported file --> + <actionGroup ref="deleteExportedFile" stepKey="deleteExportedFile"> + <argument name="rowIndex" value="0"/> + </actionGroup> + + <!-- Deleted created products --> + <deleteData createDataKey="createFirstSimpleProduct" stepKey="deleteFirstSimpleProduct"/> + <deleteData createDataKey="createSecondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> + <deleteData createDataKey="createGroupedProduct" stepKey="deleteGroupedProduct"/> + + <!-- Delete category --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to export page --> + <amOnPage url="{{AdminExportIndexPage.url}}" stepKey="goToExportIndexPage"/> + <waitForPageLoad stepKey="waitForExportIndexPageLoad"/> + + <!-- Export created below products --> + <actionGroup ref="exportAllProducts" stepKey="exportCreatedProducts"/> + + <!-- Run cron --> + <magentoCLI command="cron:run" stepKey="runCron3"/> + + <!-- Download product --> + <actionGroup ref="downloadFileByRowIndex" stepKey="downloadCreatedProducts"> + <argument name="rowIndex" value="0"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml new file mode 100644 index 000000000000..6f64da469369 --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml @@ -0,0 +1,118 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest"> + <annotations> + <features value="CatalogImportExport"/> + <stories value="Export products"/> + <title value="Export Simple and Configurable products with custom options"/> + <description value="Admin should be able to export Simple and Configurable products with custom options"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14005"/> + <group value="catalog_import_export"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create category --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + + <!-- Create configurable product with two attributes --> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeFirstOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeSecondOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeFirstOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeSecondOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Add custom options to configurable product --> + <updateData createDataKey="createConfigProduct" entity="productWithOptions" stepKey="updateProductWithOptions"/> + + <!-- Create two simple product which will be the part of configurable product --> + <createData entity="ApiSimpleOne" stepKey="createConfigFirstChildProduct"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeFirstOption"/> + </createData> + <createData entity="ApiSimpleTwo" stepKey="createConfigSecondChildProduct"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeSecondOption"/> + </createData> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeFirstOption"/> + <requiredEntity createDataKey="getConfigAttributeSecondOption"/> + </createData> + + <!-- Add created below children products to configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddFirstChild"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigFirstChildProduct"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddSecondChild"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigSecondChildProduct"/> + </createData> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + </before> + <after> + <!-- Delete exported file --> + <actionGroup ref="deleteExportedFile" stepKey="deleteExportedFile"> + <argument name="rowIndex" value="0"/> + </actionGroup> + + <!-- Delete configurable product creation --> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigFirstChildProduct" stepKey="deleteConfigFirstChildProduct"/> + <deleteData createDataKey="createConfigSecondChildProduct" stepKey="deleteConfigSecondChildProduct"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to export page --> + <amOnPage url="{{AdminExportIndexPage.url}}" stepKey="goToExportIndexPage"/> + <waitForPageLoad stepKey="waitForExportIndexPageLoad"/> + + <!-- Fill entity attributes data --> + <actionGroup ref="exportProductsFilterByAttribute" stepKey="exportProductBySku"> + <argument name="attribute" value="sku"/> + <argument name="attributeData" value="$$createConfigProduct.sku$$"/> + </actionGroup> + + <!-- Run cron --> + <magentoCLI command="cron:run" stepKey="runCron3"/> + + <!-- Download product --> + <actionGroup ref="downloadFileByRowIndex" stepKey="downloadCreatedProducts"> + <argument name="rowIndex" value="0"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml new file mode 100644 index 000000000000..993f1c9cd9da --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml @@ -0,0 +1,134 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest"> + <annotations> + <features value="CatalogImportExport"/> + <stories value="Export products"/> + <title value="Export Simple product and Configurable products with assigned images"/> + <description value="Admin should be able to export Simple and Configurable products with assigned images"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14004"/> + <group value="catalog_import_export"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create category --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + + <!-- Create configurable product with two attributes --> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeFirstOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeSecondOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeFirstOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeSecondOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create first simple product which will be the part of configurable product --> + <createData entity="ApiSimpleOne" stepKey="createConfigFirstChildProduct"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeFirstOption"/> + </createData> + + <!-- Add image to first simple product --> + <createData entity="ApiProductAttributeMediaGalleryEntryTestImage" stepKey="createConfigChildFirstProductImage"> + <requiredEntity createDataKey="createConfigFirstChildProduct"/> + </createData> + + <!-- Create second simple product which will be the part of configurable product --> + <createData entity="ApiSimpleTwo" stepKey="createConfigSecondChildProduct"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeSecondOption"/> + </createData> + + <!-- Add image to second simple product --> + <createData entity="ApiProductAttributeMediaGalleryEntryMagentoLogo" stepKey="createConfigSecondChildProductImage"> + <requiredEntity createDataKey="createConfigSecondChildProduct"/> + </createData> + + <!-- Add two options to configurable product --> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeFirstOption"/> + <requiredEntity createDataKey="getConfigAttributeSecondOption"/> + </createData> + + <!-- Add created below children products to configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddFirstChild"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigFirstChildProduct"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddSecondChild"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigSecondChildProduct"/> + </createData> + + <!-- Add image to configurable product --> + <createData entity="ApiProductAttributeMediaGalleryEntryTestImage" stepKey="createConfigProductImage"> + <requiredEntity createDataKey="createConfigProduct"/> + </createData> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + </before> + <after> + <!-- Delete exported file --> + <actionGroup ref="deleteExportedFile" stepKey="deleteExportedFile"> + <argument name="rowIndex" value="0"/> + </actionGroup> + + <!-- Delete configurable product creation --> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigFirstChildProduct" stepKey="deleteConfigFirstChildProduct"/> + <deleteData createDataKey="createConfigSecondChildProduct" stepKey="deleteConfigSecondChildProduct"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to export page --> + <amOnPage url="{{AdminExportIndexPage.url}}" stepKey="goToExportIndexPage"/> + <waitForPageLoad stepKey="waitForExportIndexPageLoad"/> + + <!-- Fill entity attributes data --> + <actionGroup ref="exportProductsFilterByAttribute" stepKey="exportProductBySku"> + <argument name="attribute" value="sku"/> + <argument name="attributeData" value="$$createConfigProduct.sku$$"/> + </actionGroup> + + <!-- Run cron --> + <magentoCLI command="cron:run" stepKey="runCron3"/> + + <!-- Download product --> + <actionGroup ref="downloadFileByRowIndex" stepKey="downloadCreatedProducts"> + <argument name="rowIndex" value="0"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest.xml new file mode 100644 index 000000000000..491d20604a08 --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest.xml @@ -0,0 +1,116 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest"> + <annotations> + <features value="CatalogImportExport"/> + <stories value="Export products"/> + <title value="Export Simple Product assigned to Main Website and Configurable Product assigned to Custom Website"/> + <description value="Admin should be able to export Simple Product assigned to Main Website and Configurable Product assigned to Custom Website"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14006"/> + <group value="catalog_import_export"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create simple product --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Create configurable product --> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeFirstOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeSecondOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeFirstOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeSecondOption"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create two simple product which will be the part of configurable product --> + <createData entity="ApiSimpleOne" stepKey="createConfigFirstChildProduct"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeFirstOption"/> + </createData> + <createData entity="ApiSimpleTwo" stepKey="createConfigSecondChildProduct"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeSecondOption"/> + </createData> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeFirstOption"/> + <requiredEntity createDataKey="getConfigAttributeSecondOption"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddFirstChild"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigFirstChildProduct"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddSecondChild"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigSecondChildProduct"/> + </createData> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + </before> + <after> + <!-- Delete exported file --> + <actionGroup ref="deleteExportedFile" stepKey="deleteExportedFile"> + <argument name="rowIndex" value="0"/> + </actionGroup> + + <!-- Delete simple product --> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + + <!-- Delete configurable product creation --> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigFirstChildProduct" stepKey="deleteConfigFirstChildProduct"/> + <deleteData createDataKey="createConfigSecondChildProduct" stepKey="deleteConfigSecondChildProduct"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to export page --> + <amOnPage url="{{AdminExportIndexPage.url}}" stepKey="goToExportIndexPage"/> + <waitForPageLoad stepKey="waitForExportIndexPageLoad"/> + + <!-- Export created below products --> + <actionGroup ref="exportAllProducts" stepKey="exportCreatedProducts"/> + + <!-- Run cron --> + <magentoCLI command="cron:run" stepKey="runCron3"/> + + <!-- Download product --> + <actionGroup ref="downloadFileByRowIndex" stepKey="downloadCreatedProducts"> + <argument name="rowIndex" value="0"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml new file mode 100644 index 000000000000..f671b54803e3 --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminExportSimpleProductWithCustomAttributeTest"> + <annotations> + <features value="CatalogImportExport"/> + <stories value="Export products"/> + <title value="Export Simple Product with custom attribute"/> + <description value="Admin should be able to export Simple Product with custom attribute"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14007"/> + <group value="catalog_import_export"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create simple product with custom attribute set --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="CatalogAttributeSet" stepKey="createAttributeSet"/> + <createData entity="SimpleProductWithCustomAttributeSet" stepKey="createSimpleProductWithCustomAttributeSet"> + <requiredEntity createDataKey="createCategory"/> + <requiredEntity createDataKey="createAttributeSet"/> + </createData> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + </before> + <after> + <!-- Delete exported file --> + <actionGroup ref="deleteExportedFile" stepKey="deleteExportedFile"> + <argument name="rowIndex" value="0"/> + </actionGroup> + + <!-- Delete product creations --> + <deleteData createDataKey="createSimpleProductWithCustomAttributeSet" stepKey="deleteSimpleProductWithCustomAttributeSet"/> + <deleteData createDataKey="createAttributeSet" stepKey="deleteAttributeSet"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to export page --> + <amOnPage url="{{AdminExportIndexPage.url}}" stepKey="goToExportIndexPage"/> + <waitForPageLoad stepKey="waitForExportIndexPageLoad"/> + + <!-- Export created below products --> + <actionGroup ref="exportAllProducts" stepKey="exportCreatedProducts"/> + + <!-- Run cron --> + <magentoCLI command="cron:run" stepKey="runCron3"/> + + <!-- Download product --> + <actionGroup ref="downloadFileByRowIndex" stepKey="downloadCreatedProducts"> + <argument name="rowIndex" value="0"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/CategoryProcessorTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/CategoryProcessorTest.php index 69f4a465e330..919f0cfda7cb 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/CategoryProcessorTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/CategoryProcessorTest.php @@ -34,41 +34,49 @@ class CategoryProcessorTest extends \PHPUnit\Framework\TestCase */ protected $product; + /** + * @var \Magento\Catalog\Model\Category + */ + private $childCategory; + + /** + * \Magento\Catalog\Model\Category + */ + private $parentCategory; + protected function setUp() { $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->objectManagerHelper = new ObjectManagerHelper($this); - $childCategory = $this->getMockBuilder(\Magento\Catalog\Model\Category::class) + $this->childCategory = $this->getMockBuilder(\Magento\Catalog\Model\Category::class) ->disableOriginalConstructor() ->getMock(); - $childCategory->method('getId')->will($this->returnValue(self::CHILD_CATEGORY_ID)); - $childCategory->method('getName')->will($this->returnValue(self::CHILD_CATEGORY_NAME)); - $childCategory->method('getPath')->will($this->returnValue( + $this->childCategory->method('getId')->will($this->returnValue(self::CHILD_CATEGORY_ID)); + $this->childCategory->method('getName')->will($this->returnValue(self::CHILD_CATEGORY_NAME)); + $this->childCategory->method('getPath')->will($this->returnValue( self::PARENT_CATEGORY_ID . CategoryProcessor::DELIMITER_CATEGORY . self::CHILD_CATEGORY_ID )); - $childCategory->method('save')->willThrowException(new \Exception()); - - $parentCategory = $this->getMockBuilder(\Magento\Catalog\Model\Category::class) + $this->parentCategory = $this->getMockBuilder(\Magento\Catalog\Model\Category::class) ->disableOriginalConstructor() ->getMock(); - $parentCategory->method('getId')->will($this->returnValue(self::PARENT_CATEGORY_ID)); - $parentCategory->method('getName')->will($this->returnValue('Parent')); - $parentCategory->method('getPath')->will($this->returnValue(self::PARENT_CATEGORY_ID)); + $this->parentCategory->method('getId')->will($this->returnValue(self::PARENT_CATEGORY_ID)); + $this->parentCategory->method('getName')->will($this->returnValue('Parent')); + $this->parentCategory->method('getPath')->will($this->returnValue(self::PARENT_CATEGORY_ID)); $categoryCollection = $this->objectManagerHelper->getCollectionMock( \Magento\Catalog\Model\ResourceModel\Category\Collection::class, [ - self::PARENT_CATEGORY_ID => $parentCategory, - self::CHILD_CATEGORY_ID => $childCategory, + self::PARENT_CATEGORY_ID => $this->parentCategory, + self::CHILD_CATEGORY_ID => $this->childCategory, ] ); $map = [ - [self::PARENT_CATEGORY_ID, $parentCategory], - [self::CHILD_CATEGORY_ID, $childCategory], + [self::PARENT_CATEGORY_ID, $this->parentCategory], + [self::CHILD_CATEGORY_ID, $this->childCategory], ]; $categoryCollection->expects($this->any()) ->method('getItemById') @@ -91,7 +99,7 @@ protected function setUp() $categoryFactory = $this->createPartialMock(\Magento\Catalog\Model\CategoryFactory::class, ['create']); - $categoryFactory->method('create')->will($this->returnValue($childCategory)); + $categoryFactory->method('create')->will($this->returnValue($this->childCategory)); $this->categoryProcessor = new \Magento\CatalogImportExport\Model\Import\Product\CategoryProcessor( @@ -110,11 +118,13 @@ public function testUpsertCategories() /** * Tests case when newly created category save throws exception. */ - public function testCreateCategoryException() + public function testUpsertCategoriesWithAlreadyExistsException() { - $method = new \ReflectionMethod(CategoryProcessor::class, 'createCategory'); - $method->setAccessible(true); - $method->invoke($this->categoryProcessor, self::CHILD_CATEGORY_NAME, self::PARENT_CATEGORY_ID); + $exception = new \Magento\Framework\Exception\AlreadyExistsException(); + $categoriesSeparator = '/'; + $categoryName = 'Exception Category'; + $this->childCategory->method('save')->willThrowException($exception); + $this->categoryProcessor->upsertCategories($categoryName, $categoriesSeparator); $this->assertNotEmpty($this->categoryProcessor->getFailedCategories()); } diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php index 98e434f21748..f0a52a67e009 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php @@ -702,7 +702,7 @@ public function testValidateRowNoCustomOption() { $rowData = include __DIR__ . '/_files/row_data_no_custom_option.php'; $this->_bypassModelMethodGetMultiRowFormat($rowData); - $this->assertFalse($this->modelMock->validateRow($rowData, 0)); + $this->assertTrue($this->modelMock->validateRow($rowData, 0)); } /** diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php index a562916b3b4b..f85d33edb5d8 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php @@ -3,11 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\CatalogImportExport\Test\Unit\Model\Import; +use Magento\CatalogImportExport\Model\Import\Product; use Magento\CatalogImportExport\Model\Import\Product\ImageTypeProcessor; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\ImportExport\Model\Import; +use PHPUnit\Framework\MockObject\MockObject; /** * Class ProductTest @@ -26,126 +29,126 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI const ENTITY_ID = 13; - /** @var \Magento\Framework\DB\Adapter\AdapterInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\DB\Adapter\AdapterInterface| MockObject */ protected $_connection; - /** @var \Magento\Framework\Json\Helper\Data|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Json\Helper\Data| MockObject */ protected $jsonHelper; - /** @var \Magento\ImportExport\Model\ResourceModel\Import\Data|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\ImportExport\Model\ResourceModel\Import\Data| MockObject */ protected $_dataSourceModel; - /** @var \Magento\Framework\App\ResourceConnection|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\App\ResourceConnection| MockObject */ protected $resource; - /** @var \Magento\ImportExport\Model\ResourceModel\Helper|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\ImportExport\Model\ResourceModel\Helper| MockObject */ protected $_resourceHelper; - /** @var \Magento\Framework\Stdlib\StringUtils|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Stdlib\StringUtils|MockObject */ protected $string; - /** @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Event\ManagerInterface|MockObject */ protected $_eventManager; - /** @var \Magento\CatalogInventory\Api\StockRegistryInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogInventory\Api\StockRegistryInterface|MockObject */ protected $stockRegistry; - /** @var \Magento\CatalogImportExport\Model\Import\Product\OptionFactory|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Product\OptionFactory|MockObject */ protected $optionFactory; - /** @var \Magento\CatalogInventory\Api\StockConfigurationInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogInventory\Api\StockConfigurationInterface|MockObject */ protected $stockConfiguration; - /** @var \Magento\CatalogInventory\Model\Spi\StockStateProviderInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogInventory\Model\Spi\StockStateProviderInterface|MockObject */ protected $stockStateProvider; - /** @var \Magento\CatalogImportExport\Model\Import\Product\Option|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Product\Option|MockObject */ protected $optionEntity; - /** @var \Magento\Framework\Stdlib\DateTime|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Stdlib\DateTime|MockObject */ protected $dateTime; /** @var array */ protected $data; - /** @var \Magento\ImportExport\Helper\Data|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\ImportExport\Helper\Data|MockObject */ protected $importExportData; - /** @var \Magento\ImportExport\Model\ResourceModel\Import\Data|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\ImportExport\Model\ResourceModel\Import\Data|MockObject */ protected $importData; - /** @var \Magento\Eav\Model\Config|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Eav\Model\Config|MockObject */ protected $config; - /** @var \Magento\ImportExport\Model\ResourceModel\Helper|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\ImportExport\Model\ResourceModel\Helper|MockObject */ protected $resourceHelper; - /** @var \Magento\Catalog\Helper\Data|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Catalog\Helper\Data|MockObject */ protected $_catalogData; - /** @var \Magento\ImportExport\Model\Import\Config|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\ImportExport\Model\Import\Config|MockObject */ protected $_importConfig; - /** @var \PHPUnit_Framework_MockObject_MockObject */ + /** @var MockObject */ protected $_resourceFactory; // @codingStandardsIgnoreStart - /** @var \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory|MockObject */ protected $_setColFactory; - /** @var \Magento\CatalogImportExport\Model\Import\Product\Type\Factory|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Product\Type\Factory|MockObject */ protected $_productTypeFactory; - /** @var \Magento\Catalog\Model\ResourceModel\Product\LinkFactory|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Catalog\Model\ResourceModel\Product\LinkFactory|MockObject */ protected $_linkFactory; - /** @var \Magento\CatalogImportExport\Model\Import\Proxy\ProductFactory|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Proxy\ProductFactory|MockObject */ protected $_proxyProdFactory; - /** @var \Magento\CatalogImportExport\Model\Import\UploaderFactory|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\UploaderFactory|MockObject */ protected $_uploaderFactory; - /** @var \Magento\Framework\Filesystem|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Filesystem|MockObject */ protected $_filesystem; - /** @var \Magento\Framework\Filesystem\Directory\WriteInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Filesystem\Directory\WriteInterface|MockObject */ protected $_mediaDirectory; - /** @var \Magento\CatalogInventory\Model\ResourceModel\Stock\ItemFactory|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogInventory\Model\ResourceModel\Stock\ItemFactory|MockObject */ protected $_stockResItemFac; - /** @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface|MockObject */ protected $_localeDate; - /** @var \Magento\Framework\Indexer\IndexerRegistry|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Indexer\IndexerRegistry|MockObject */ protected $indexerRegistry; - /** @var \Psr\Log\LoggerInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Psr\Log\LoggerInterface|MockObject */ protected $_logger; - /** @var \Magento\CatalogImportExport\Model\Import\Product\StoreResolver|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Product\StoreResolver|MockObject */ protected $storeResolver; - /** @var \Magento\CatalogImportExport\Model\Import\Product\SkuProcessor|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Product\SkuProcessor|MockObject */ protected $skuProcessor; - /** @var \Magento\CatalogImportExport\Model\Import\Product\CategoryProcessor|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Product\CategoryProcessor|MockObject */ protected $categoryProcessor; - /** @var \Magento\CatalogImportExport\Model\Import\Product\Validator|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Product\Validator|MockObject */ protected $validator; - /** @var \Magento\Framework\Model\ResourceModel\Db\ObjectRelationProcessor|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Model\ResourceModel\Db\ObjectRelationProcessor|MockObject */ protected $objectRelationProcessor; - /** @var \Magento\Framework\Model\ResourceModel\Db\TransactionManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Model\ResourceModel\Db\TransactionManagerInterface|MockObject */ protected $transactionManager; - /** @var \Magento\CatalogImportExport\Model\Import\Product\TaxClassProcessor|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Product\TaxClassProcessor|MockObject */ // @codingStandardsIgnoreEnd protected $taxClassProcessor; - /** @var \Magento\CatalogImportExport\Model\Import\Product */ + /** @var Product */ protected $importProduct; /** @@ -153,13 +156,13 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI */ protected $errorAggregator; - /** @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject*/ + /** @var \Magento\Framework\App\Config\ScopeConfigInterface|MockObject */ protected $scopeConfig; - /** @var \Magento\Catalog\Model\Product\Url|\PHPUnit_Framework_MockObject_MockObject*/ + /** @var \Magento\Catalog\Model\Product\Url|MockObject */ protected $productUrl; - /** @var ImageTypeProcessor|\PHPUnit_Framework_MockObject_MockObject */ + /** @var ImageTypeProcessor|MockObject */ protected $imageTypeProcessor; /** @@ -343,7 +346,7 @@ protected function setUp() $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->importProduct = $objectManager->getObject( - \Magento\CatalogImportExport\Model\Import\Product::class, + Product::class, [ 'jsonHelper' => $this->jsonHelper, 'importExportData' => $this->importExportData, @@ -385,7 +388,7 @@ protected function setUp() 'imageTypeProcessor' => $this->imageTypeProcessor ] ); - $reflection = new \ReflectionClass(\Magento\CatalogImportExport\Model\Import\Product::class); + $reflection = new \ReflectionClass(Product::class); $reflectionProperty = $reflection->getProperty('metadataPool'); $reflectionProperty->setAccessible(true); $reflectionProperty->setValue($this->importProduct, $metadataPoolMock); @@ -627,7 +630,7 @@ public function testGetEmptyAttributeValueConstantFromParameters() public function testDeleteProductsForReplacement() { - $importProduct = $this->getMockBuilder(\Magento\CatalogImportExport\Model\Import\Product::class) + $importProduct = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() ->setMethods([ 'setParameters', '_deleteProducts' @@ -693,7 +696,7 @@ public function testValidateRowIsAlreadyValidated() */ public function testValidateRow($rowScope, $oldSku, $expectedResult, $behaviour = Import::BEHAVIOR_DELETE) { - $importProduct = $this->getMockBuilder(\Magento\CatalogImportExport\Model\Import\Product::class) + $importProduct = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() ->setMethods(['getBehavior', 'getRowScope', 'getErrorAggregator']) ->getMock(); @@ -705,7 +708,7 @@ public function testValidateRow($rowScope, $oldSku, $expectedResult, $behaviour ->method('getErrorAggregator') ->willReturn($this->getErrorAggregatorObject()); $importProduct->expects($this->once())->method('getRowScope')->willReturn($rowScope); - $skuKey = \Magento\CatalogImportExport\Model\Import\Product::COL_SKU; + $skuKey = Product::COL_SKU; $rowData = [ $skuKey => 'sku', ]; @@ -717,18 +720,22 @@ public function testValidateRow($rowScope, $oldSku, $expectedResult, $behaviour public function testValidateRowDeleteBehaviourAddRowErrorCall() { - $importProduct = $this->getMockBuilder(\Magento\CatalogImportExport\Model\Import\Product::class) + $importProduct = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() - ->setMethods(['getBehavior', 'getRowScope', 'addRowError']) + ->setMethods(['getBehavior', 'getRowScope', 'addRowError', 'getErrorAggregator']) ->getMock(); $importProduct->expects($this->exactly(2))->method('getBehavior') ->willReturn(\Magento\ImportExport\Model\Import::BEHAVIOR_DELETE); $importProduct->expects($this->once())->method('getRowScope') - ->willReturn(\Magento\CatalogImportExport\Model\Import\Product::SCOPE_DEFAULT); + ->willReturn(Product::SCOPE_DEFAULT); $importProduct->expects($this->once())->method('addRowError'); + $importProduct->method('getErrorAggregator') + ->willReturn( + $this->getErrorAggregatorObject(['addRowToSkip']) + ); $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => 'sku', + Product::COL_SKU => 'sku', ]; $importProduct->validateRow($rowData, 0); @@ -739,7 +746,7 @@ public function testValidateRowValidatorCheck() $messages = ['validator message']; $this->validator->expects($this->once())->method('getMessages')->willReturn($messages); $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => 'sku', + Product::COL_SKU => 'sku', ]; $rowNum = 0; $this->importProduct->validateRow($rowData, $rowNum); @@ -841,7 +848,7 @@ public function getStoreIdByCodeDataProvider() return [ [ '$storeCode' => null, - '$expectedResult' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_DEFAULT, + '$expectedResult' => Product::SCOPE_DEFAULT, ], [ '$storeCode' => 'value', @@ -856,17 +863,17 @@ public function getStoreIdByCodeDataProvider() public function testValidateRowCheckSpecifiedSku($sku, $expectedError) { $importProduct = $this->createModelMockWithErrorAggregator( - [ 'addRowError', 'getOptionEntity', 'getRowScope'], + ['addRowError', 'getOptionEntity', 'getRowScope'], ['isRowInvalid' => true] ); $rowNum = 0; $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, - \Magento\CatalogImportExport\Model\Import\Product::COL_STORE => '', + Product::COL_SKU => $sku, + Product::COL_STORE => '', ]; - $this->storeResolver->expects($this->any())->method('getStoreCodeToId')->willReturn(null); + $this->storeResolver->method('getStoreCodeToId')->willReturn(null); $this->setPropertyValue($importProduct, 'storeResolver', $this->storeResolver); $this->setPropertyValue($importProduct, 'skuProcessor', $this->skuProcessor); @@ -875,7 +882,7 @@ public function testValidateRowCheckSpecifiedSku($sku, $expectedError) $importProduct ->expects($this->once()) ->method('getRowScope') - ->willReturn(\Magento\CatalogImportExport\Model\Import\Product::SCOPE_STORE); + ->willReturn(Product::SCOPE_STORE); $importProduct->expects($this->at(1))->method('addRowError')->with($expectedError, $rowNum)->willReturn(null); $importProduct->validateRow($rowData, $rowNum); @@ -889,7 +896,7 @@ public function testValidateRowProcessEntityIncrement() $errorAggregator->method('isRowInvalid')->willReturn(true); $this->setPropertyValue($this->importProduct, '_processedEntitiesCount', $count); $this->setPropertyValue($this->importProduct, 'errorAggregator', $errorAggregator); - $rowData = [\Magento\CatalogImportExport\Model\Import\Product::COL_SKU => false]; + $rowData = [Product::COL_SKU => false]; //suppress validator $this->_setValidatorMockInImportProduct($this->importProduct); $this->importProduct->validateRow($rowData, $rowNum); @@ -899,14 +906,14 @@ public function testValidateRowProcessEntityIncrement() public function testValidateRowValidateExistingProductTypeAddNewSku() { $importProduct = $this->createModelMockWithErrorAggregator( - [ 'addRowError', 'getOptionEntity'], + ['addRowError', 'getOptionEntity'], ['isRowInvalid' => true] ); $sku = 'sku'; $rowNum = 0; $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, + Product::COL_SKU => $sku, ]; $oldSku = [ $sku => [ @@ -929,7 +936,7 @@ public function testValidateRowValidateExistingProductTypeAddNewSku() $this->setPropertyValue($importProduct, '_oldSku', $oldSku); $expectedData = [ - 'entity_id' => $oldSku[$sku]['entity_id'], //entity_id_val + 'entity_id' => $oldSku[$sku]['entity_id'], //entity_id_val 'type_id' => $oldSku[$sku]['type_id'],// type_id_val 'attr_set_id' => $oldSku[$sku]['attr_set_id'], //attr_set_id_val 'attr_set_code' => $_attrSetIdToName[$oldSku[$sku]['attr_set_id']],//attr_set_id_val @@ -947,7 +954,7 @@ public function testValidateRowValidateExistingProductTypeAddErrorRowCall() $sku = 'sku'; $rowNum = 0; $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, + Product::COL_SKU => $sku, ]; $oldSku = [ $sku => [ @@ -972,6 +979,11 @@ public function testValidateRowValidateExistingProductTypeAddErrorRowCall() /** * @dataProvider validateRowValidateNewProductTypeAddRowErrorCallDataProvider + * @param string $colType + * @param string $productTypeModelsColType + * @param string $colAttrSet + * @param string $attrSetNameToIdColAttrSet + * @param string $error */ public function testValidateRowValidateNewProductTypeAddRowErrorCall( $colType, @@ -983,15 +995,15 @@ public function testValidateRowValidateNewProductTypeAddRowErrorCall( $sku = 'sku'; $rowNum = 0; $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, - \Magento\CatalogImportExport\Model\Import\Product::COL_TYPE => $colType, - \Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET => $colAttrSet, + Product::COL_SKU => $sku, + Product::COL_TYPE => $colType, + Product::COL_ATTR_SET => $colAttrSet, ]; $_attrSetNameToId = [ - $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET] => $attrSetNameToIdColAttrSet, + $rowData[Product::COL_ATTR_SET] => $attrSetNameToIdColAttrSet, ]; $_productTypeModels = [ - $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_TYPE] => $productTypeModelsColType, + $rowData[Product::COL_TYPE] => $productTypeModelsColType, ]; $oldSku = [ $sku => null, @@ -1019,29 +1031,25 @@ public function testValidateRowValidateNewProductTypeGetNewSkuCall() $sku = 'sku'; $rowNum = 0; $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, - \Magento\CatalogImportExport\Model\Import\Product::COL_TYPE => 'value', - \Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET => 'value', + Product::COL_SKU => $sku, + Product::COL_TYPE => 'value', + Product::COL_ATTR_SET => 'value', ]; $_productTypeModels = [ - $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_TYPE] => 'value', + $rowData[Product::COL_TYPE] => 'value', ]; $oldSku = [ $sku => null, ]; $_attrSetNameToId = [ - $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET] => 'attr_set_code_val' + $rowData[Product::COL_ATTR_SET] => 'attr_set_code_val' ]; $expectedData = [ 'entity_id' => null, - 'type_id' => $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_TYPE],//value + 'type_id' => $rowData[Product::COL_TYPE],//value //attr_set_id_val - 'attr_set_id' => $_attrSetNameToId[ - $rowData[ - \Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET - ] - ], - 'attr_set_code' => $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET],//value + 'attr_set_id' => $_attrSetNameToId[$rowData[Product::COL_ATTR_SET]], + 'attr_set_code' => $rowData[Product::COL_ATTR_SET],//value 'row_id' => null ]; $importProduct = $this->createModelMockWithErrorAggregator( @@ -1077,8 +1085,8 @@ public function testValidateRowSetAttributeSetCodeIntoRowData() $sku = 'sku'; $rowNum = 0; $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, - \Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET => 'col_attr_set_val', + Product::COL_SKU => $sku, + Product::COL_ATTR_SET => 'col_attr_set_val', ]; $expectedAttrSetCode = 'new_attr_set_code'; $newSku = [ @@ -1086,8 +1094,8 @@ public function testValidateRowSetAttributeSetCodeIntoRowData() 'type_id' => 'new_type_id_val', ]; $expectedRowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, - \Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET => $newSku['attr_set_code'], + Product::COL_SKU => $sku, + Product::COL_ATTR_SET => $newSku['attr_set_code'], ]; $oldSku = [ $sku => [ @@ -1121,8 +1129,8 @@ public function testValidateValidateOptionEntity() $sku = 'sku'; $rowNum = 0; $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, - \Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET => 'col_attr_set_val', + Product::COL_SKU => $sku, + Product::COL_ATTR_SET => 'col_attr_set_val', ]; $oldSku = [ $sku => [ @@ -1374,7 +1382,7 @@ public function validateRowDataProvider() { return [ [ - '$rowScope' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_DEFAULT, + '$rowScope' => Product::SCOPE_DEFAULT, '$oldSku' => null, '$expectedResult' => false, ], @@ -1389,12 +1397,12 @@ public function validateRowDataProvider() '$expectedResult' => true, ], [ - '$rowScope' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_DEFAULT, + '$rowScope' => Product::SCOPE_DEFAULT, '$oldSku' => true, '$expectedResult' => true, ], [ - '$rowScope' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_DEFAULT, + '$rowScope' => Product::SCOPE_DEFAULT, '$oldSku' => null, '$expectedResult' => false, '$behaviour' => Import::BEHAVIOR_REPLACE @@ -1415,7 +1423,7 @@ public function isAttributeValidAssertAttrValidDataProvider() '$rowData' => [ 'code' => str_repeat( 'a', - \Magento\CatalogImportExport\Model\Import\Product::DB_MAX_VARCHAR_LENGTH - 1 + Product::DB_MAX_VARCHAR_LENGTH - 1 ), ], ], @@ -1468,7 +1476,7 @@ public function isAttributeValidAssertAttrValidDataProvider() '$rowData' => [ 'code' => str_repeat( 'a', - \Magento\CatalogImportExport\Model\Import\Product::DB_MAX_TEXT_LENGTH - 1 + Product::DB_MAX_TEXT_LENGTH - 1 ), ], ], @@ -1488,7 +1496,7 @@ public function isAttributeValidAssertAttrInvalidDataProvider() '$rowData' => [ 'code' => str_repeat( 'a', - \Magento\CatalogImportExport\Model\Import\Product::DB_MAX_VARCHAR_LENGTH + 1 + Product::DB_MAX_VARCHAR_LENGTH + 1 ), ], ], @@ -1541,7 +1549,7 @@ public function isAttributeValidAssertAttrInvalidDataProvider() '$rowData' => [ 'code' => str_repeat( 'a', - \Magento\CatalogImportExport\Model\Import\Product::DB_MAX_TEXT_LENGTH + 1 + Product::DB_MAX_TEXT_LENGTH + 1 ), ], ], @@ -1553,8 +1561,8 @@ public function isAttributeValidAssertAttrInvalidDataProvider() */ public function getRowScopeDataProvider() { - $colSku = \Magento\CatalogImportExport\Model\Import\Product::COL_SKU; - $colStore = \Magento\CatalogImportExport\Model\Import\Product::COL_STORE; + $colSku = Product::COL_SKU; + $colStore = Product::COL_STORE; return [ [ @@ -1562,21 +1570,21 @@ public function getRowScopeDataProvider() $colSku => null, $colStore => 'store', ], - '$expectedResult' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_STORE + '$expectedResult' => Product::SCOPE_STORE ], [ '$rowData' => [ $colSku => 'sku', $colStore => null, ], - '$expectedResult' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_DEFAULT + '$expectedResult' => Product::SCOPE_DEFAULT ], [ '$rowData' => [ $colSku => 'sku', $colStore => 'store', ], - '$expectedResult' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_STORE + '$expectedResult' => Product::SCOPE_STORE ], ]; } @@ -1653,9 +1661,9 @@ protected function overrideMethod(&$object, $methodName, array $parameters = []) * * @see _rewriteGetOptionEntityInImportProduct() * @see _setValidatorMockInImportProduct() - * @param \Magento\CatalogImportExport\Model\Import\Product + * @param Product * Param should go with rewritten getOptionEntity method. - * @return \Magento\CatalogImportExport\Model\Import\Product\Option|\PHPUnit_Framework_MockObject_MockObject + * @return \Magento\CatalogImportExport\Model\Import\Product\Option|MockObject */ private function _suppressValidateRowOptionValidatorInvalidRows($importProduct) { @@ -1671,8 +1679,8 @@ private function _suppressValidateRowOptionValidatorInvalidRows($importProduct) * Used in group of validateRow method's tests. * Set validator mock in importProduct, return true for isValid method. * - * @param \Magento\CatalogImportExport\Model\Import\Product - * @return \Magento\CatalogImportExport\Model\Import\Product\Validator|\PHPUnit_Framework_MockObject_MockObject + * @param Product + * @return \Magento\CatalogImportExport\Model\Import\Product\Validator|MockObject */ private function _setValidatorMockInImportProduct($importProduct) { @@ -1686,9 +1694,9 @@ private function _setValidatorMockInImportProduct($importProduct) * Used in group of validateRow method's tests. * Make getOptionEntity return option mock. * - * @param \Magento\CatalogImportExport\Model\Import\Product + * @param Product * Param should go with rewritten getOptionEntity method. - * @return \Magento\CatalogImportExport\Model\Import\Product\Option|\PHPUnit_Framework_MockObject_MockObject + * @return \Magento\CatalogImportExport\Model\Import\Product\Option|MockObject */ private function _rewriteGetOptionEntityInImportProduct($importProduct) { @@ -1703,12 +1711,12 @@ private function _rewriteGetOptionEntityInImportProduct($importProduct) /** * @param array $methods * @param array $errorAggregatorMethods - * @return \PHPUnit_Framework_MockObject_MockObject + * @return MockObject */ protected function createModelMockWithErrorAggregator(array $methods = [], array $errorAggregatorMethods = []) { $methods[] = 'getErrorAggregator'; - $importProduct = $this->getMockBuilder(\Magento\CatalogImportExport\Model\Import\Product::class) + $importProduct = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() ->setMethods($methods) ->getMock(); diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php index 262593377aa2..f734596de014 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php @@ -93,14 +93,14 @@ protected function setUp() $this->filesystem, $this->readFactory, ]) - ->setMethods(['_setUploadFile', 'save', 'getTmpDir']) + ->setMethods(['_setUploadFile', 'save', 'getTmpDir', 'checkAllowedExtension']) ->getMock(); } /** * @dataProvider moveFileUrlDataProvider */ - public function testMoveFileUrl($fileUrl, $expectedHost, $expectedFileName) + public function testMoveFileUrl($fileUrl, $expectedHost, $expectedFileName, $checkAllowedExtension) { $destDir = 'var/dest/dir'; $expectedRelativeFilePath = $expectedFileName; @@ -128,6 +128,9 @@ public function testMoveFileUrl($fileUrl, $expectedHost, $expectedFileName) $this->uploader->expects($this->once())->method('_setUploadFile')->will($this->returnSelf()); $this->uploader->expects($this->once())->method('save')->with($destDir . '/' . $expectedFileName) ->willReturn(['name' => $expectedFileName, 'path' => 'absPath']); + $this->uploader->expects($this->exactly($checkAllowedExtension)) + ->method('checkAllowedExtension') + ->willReturn(true); $this->uploader->setDestDir($destDir); $result = $this->uploader->move($fileUrl); @@ -224,31 +227,37 @@ public function moveFileUrlDataProvider() '$fileUrl' => 'http://test_uploader_file', '$expectedHost' => 'test_uploader_file', '$expectedFileName' => 'test_uploader_file', + '$checkAllowedExtension' => 0 ], [ '$fileUrl' => 'https://!:^&`;file', '$expectedHost' => '!:^&`;file', '$expectedFileName' => 'file', + '$checkAllowedExtension' => 0 ], [ '$fileUrl' => 'https://www.google.com/image.jpg', '$expectedHost' => 'www.google.com/image.jpg', '$expectedFileName' => 'image.jpg', + '$checkAllowedExtension' => 1 ], [ '$fileUrl' => 'https://www.google.com/image.jpg?param=1', '$expectedHost' => 'www.google.com/image.jpg?param=1', '$expectedFileName' => 'image.jpg', + '$checkAllowedExtension' => 1 ], [ '$fileUrl' => 'https://www.google.com/image.jpg?param=1¶m=2', '$expectedHost' => 'www.google.com/image.jpg?param=1¶m=2', '$expectedFileName' => 'image.jpg', + '$checkAllowedExtension' => 1 ], [ '$fileUrl' => 'http://www.google.com/image.jpg?param=1¶m=2', '$expectedHost' => 'www.google.com/image.jpg?param=1¶m=2', '$expectedFileName' => 'image.jpg', + '$checkAllowedExtension' => 1 ], ]; } diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockCollectionInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockCollectionInterface.php index c2521f77ca24..9ae22e5e1a36 100644 --- a/app/code/Magento/CatalogInventory/Api/Data/StockCollectionInterface.php +++ b/app/code/Magento/CatalogInventory/Api/Data/StockCollectionInterface.php @@ -15,6 +15,10 @@ * Interface StockCollectionInterface * @api * @since 100.0.2 + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ interface StockCollectionInterface extends SearchResultsInterface { diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockInterface.php index 53e95921ea95..087fae6e6568 100644 --- a/app/code/Magento/CatalogInventory/Api/Data/StockInterface.php +++ b/app/code/Magento/CatalogInventory/Api/Data/StockInterface.php @@ -11,6 +11,10 @@ * Interface Stock * @api * @since 100.0.2 + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ interface StockInterface extends ExtensibleDataInterface { diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockItemCollectionInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockItemCollectionInterface.php index 038174c8e52b..59a1f58b74c2 100644 --- a/app/code/Magento/CatalogInventory/Api/Data/StockItemCollectionInterface.php +++ b/app/code/Magento/CatalogInventory/Api/Data/StockItemCollectionInterface.php @@ -15,6 +15,10 @@ * Interface StockItemCollectionInterface * @api * @since 100.0.2 + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ interface StockItemCollectionInterface extends SearchResultsInterface { diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockItemInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockItemInterface.php index b876615468ba..e2e8a744c4bc 100644 --- a/app/code/Magento/CatalogInventory/Api/Data/StockItemInterface.php +++ b/app/code/Magento/CatalogInventory/Api/Data/StockItemInterface.php @@ -11,6 +11,10 @@ * Interface StockItem * @api * @since 100.0.2 + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ interface StockItemInterface extends ExtensibleDataInterface { diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockStatusCollectionInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockStatusCollectionInterface.php index 70a2c29ff9a6..1cc045745a0c 100644 --- a/app/code/Magento/CatalogInventory/Api/Data/StockStatusCollectionInterface.php +++ b/app/code/Magento/CatalogInventory/Api/Data/StockStatusCollectionInterface.php @@ -11,6 +11,10 @@ * Stock Status collection interface * @api * @since 100.0.2 + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ interface StockStatusCollectionInterface extends SearchResultsInterface { diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockStatusInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockStatusInterface.php index c9ae6a96a367..66b639fb088d 100644 --- a/app/code/Magento/CatalogInventory/Api/Data/StockStatusInterface.php +++ b/app/code/Magento/CatalogInventory/Api/Data/StockStatusInterface.php @@ -11,6 +11,10 @@ * Interface StockStatusInterface * @api * @since 100.0.2 + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ interface StockStatusInterface extends ExtensibleDataInterface { diff --git a/app/code/Magento/CatalogInventory/Api/RegisterProductSaleInterface.php b/app/code/Magento/CatalogInventory/Api/RegisterProductSaleInterface.php index 9122fb003864..6fd1e7466970 100644 --- a/app/code/Magento/CatalogInventory/Api/RegisterProductSaleInterface.php +++ b/app/code/Magento/CatalogInventory/Api/RegisterProductSaleInterface.php @@ -12,6 +12,10 @@ /** * @api + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ interface RegisterProductSaleInterface { diff --git a/app/code/Magento/CatalogInventory/Api/RevertProductSaleInterface.php b/app/code/Magento/CatalogInventory/Api/RevertProductSaleInterface.php index ed496f3882fc..552e30da8923 100644 --- a/app/code/Magento/CatalogInventory/Api/RevertProductSaleInterface.php +++ b/app/code/Magento/CatalogInventory/Api/RevertProductSaleInterface.php @@ -9,6 +9,10 @@ /** * @api + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ interface RevertProductSaleInterface { diff --git a/app/code/Magento/CatalogInventory/Api/StockConfigurationInterface.php b/app/code/Magento/CatalogInventory/Api/StockConfigurationInterface.php index a23d5030b824..5019e86b7af4 100644 --- a/app/code/Magento/CatalogInventory/Api/StockConfigurationInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockConfigurationInterface.php @@ -9,6 +9,10 @@ * Interface StockConfigurationInterface * @api * @since 100.0.2 + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ interface StockConfigurationInterface { diff --git a/app/code/Magento/CatalogInventory/Api/StockCriteriaInterface.php b/app/code/Magento/CatalogInventory/Api/StockCriteriaInterface.php index 969af6481cb4..eb6fb2e812f2 100644 --- a/app/code/Magento/CatalogInventory/Api/StockCriteriaInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockCriteriaInterface.php @@ -9,6 +9,10 @@ * Interface StockCriteriaInterface * @api * @since 100.0.2 + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ interface StockCriteriaInterface extends \Magento\Framework\Api\CriteriaInterface { diff --git a/app/code/Magento/CatalogInventory/Api/StockIndexInterface.php b/app/code/Magento/CatalogInventory/Api/StockIndexInterface.php index 1521b34c715b..18bab6571c20 100644 --- a/app/code/Magento/CatalogInventory/Api/StockIndexInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockIndexInterface.php @@ -9,6 +9,10 @@ * Interface StockIndexInterface * @api * @since 100.0.2 + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ interface StockIndexInterface { diff --git a/app/code/Magento/CatalogInventory/Api/StockItemCriteriaInterface.php b/app/code/Magento/CatalogInventory/Api/StockItemCriteriaInterface.php index a2fc7801b1d1..1d2cabbb48a1 100644 --- a/app/code/Magento/CatalogInventory/Api/StockItemCriteriaInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockItemCriteriaInterface.php @@ -9,6 +9,10 @@ * Interface StockItemCriteriaInterface * @api * @since 100.0.2 + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ interface StockItemCriteriaInterface extends \Magento\Framework\Api\CriteriaInterface { diff --git a/app/code/Magento/CatalogInventory/Api/StockItemRepositoryInterface.php b/app/code/Magento/CatalogInventory/Api/StockItemRepositoryInterface.php index 2732048d1445..eecf6cbe0763 100644 --- a/app/code/Magento/CatalogInventory/Api/StockItemRepositoryInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockItemRepositoryInterface.php @@ -9,6 +9,10 @@ * Interface StockItemRepository * @api * @since 100.0.2 + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ interface StockItemRepositoryInterface { diff --git a/app/code/Magento/CatalogInventory/Api/StockManagementInterface.php b/app/code/Magento/CatalogInventory/Api/StockManagementInterface.php index ddb84abf3d1a..8796953e32fd 100644 --- a/app/code/Magento/CatalogInventory/Api/StockManagementInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockManagementInterface.php @@ -9,6 +9,10 @@ * Interface StockManagementInterface * @api * @since 100.0.2 + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ interface StockManagementInterface { diff --git a/app/code/Magento/CatalogInventory/Api/StockRegistryInterface.php b/app/code/Magento/CatalogInventory/Api/StockRegistryInterface.php index 2d011e24f410..5478f90fb7d9 100644 --- a/app/code/Magento/CatalogInventory/Api/StockRegistryInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockRegistryInterface.php @@ -9,6 +9,10 @@ * Interface StockRegistryInterface * @api * @since 100.0.2 + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ interface StockRegistryInterface { diff --git a/app/code/Magento/CatalogInventory/Api/StockRepositoryInterface.php b/app/code/Magento/CatalogInventory/Api/StockRepositoryInterface.php index 80a7e79289cf..3cfdf4550634 100644 --- a/app/code/Magento/CatalogInventory/Api/StockRepositoryInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockRepositoryInterface.php @@ -9,6 +9,10 @@ * Interface StockRepositoryInterface * @api * @since 100.0.2 + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ interface StockRepositoryInterface { diff --git a/app/code/Magento/CatalogInventory/Api/StockStateInterface.php b/app/code/Magento/CatalogInventory/Api/StockStateInterface.php index 8a1f7da5158e..8be7f5be79f2 100644 --- a/app/code/Magento/CatalogInventory/Api/StockStateInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockStateInterface.php @@ -9,6 +9,10 @@ * Interface StockStateInterface * @api * @since 100.0.2 + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ interface StockStateInterface { diff --git a/app/code/Magento/CatalogInventory/Api/StockStatusCriteriaInterface.php b/app/code/Magento/CatalogInventory/Api/StockStatusCriteriaInterface.php index e504e6355a15..99ad7005d9da 100644 --- a/app/code/Magento/CatalogInventory/Api/StockStatusCriteriaInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockStatusCriteriaInterface.php @@ -9,6 +9,10 @@ * Interface StockStatusCriteriaInterface * @api * @since 100.0.2 + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ interface StockStatusCriteriaInterface extends \Magento\Framework\Api\CriteriaInterface { diff --git a/app/code/Magento/CatalogInventory/Api/StockStatusRepositoryInterface.php b/app/code/Magento/CatalogInventory/Api/StockStatusRepositoryInterface.php index 94d4998c1e31..d29171f557f0 100644 --- a/app/code/Magento/CatalogInventory/Api/StockStatusRepositoryInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockStatusRepositoryInterface.php @@ -9,6 +9,10 @@ * Interface StockStatusRepositoryInterface * @api * @since 100.0.2 + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ interface StockStatusRepositoryInterface { diff --git a/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Customergroup.php b/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Customergroup.php index dc992378d128..f349e94235a9 100644 --- a/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Customergroup.php +++ b/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Customergroup.php @@ -84,7 +84,7 @@ protected function _getCustomerGroups($groupId = null) $this->_customerGroups[$notLoggedInGroup->getId()] = $notLoggedInGroup->getCode(); } if ($groupId !== null) { - return isset($this->_customerGroups[$groupId]) ? $this->_customerGroups[$groupId] : null; + return $this->_customerGroups[$groupId] ?? null; } return $this->_customerGroups; } diff --git a/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Minsaleqty.php b/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Minsaleqty.php index 15dfbd122e95..d3c165bbde1a 100644 --- a/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Minsaleqty.php +++ b/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Minsaleqty.php @@ -10,6 +10,10 @@ * * @api * @since 100.0.2 + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ class Minsaleqty extends \Magento\Config\Block\System\Config\Form\Field\FieldArray\AbstractFieldArray { diff --git a/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Stock.php b/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Stock.php index 2c41e1798921..5378801b6c24 100644 --- a/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Stock.php +++ b/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Stock.php @@ -14,6 +14,10 @@ /** * @api * @since 100.0.2 + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ class Stock extends \Magento\Framework\Data\Form\Element\Select { diff --git a/app/code/Magento/CatalogInventory/Block/Qtyincrements.php b/app/code/Magento/CatalogInventory/Block/Qtyincrements.php index adaa762f3279..a12b72cd0a97 100644 --- a/app/code/Magento/CatalogInventory/Block/Qtyincrements.php +++ b/app/code/Magento/CatalogInventory/Block/Qtyincrements.php @@ -14,6 +14,10 @@ * * @api * @since 100.0.2 + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ class Qtyincrements extends Template implements IdentityInterface { diff --git a/app/code/Magento/CatalogInventory/Block/Stockqty/AbstractStockqty.php b/app/code/Magento/CatalogInventory/Block/Stockqty/AbstractStockqty.php index 568fa600ec52..4c8f356519e2 100644 --- a/app/code/Magento/CatalogInventory/Block/Stockqty/AbstractStockqty.php +++ b/app/code/Magento/CatalogInventory/Block/Stockqty/AbstractStockqty.php @@ -131,7 +131,9 @@ public function getPlaceholderId() */ public function isMsgVisible() { - return $this->getStockQty() > 0 && $this->getStockQtyLeft() <= $this->getThresholdQty(); + return $this->getStockQty() > 0 + && $this->getStockQtyLeft() > 0 + && $this->getStockQtyLeft() <= $this->getThresholdQty(); } /** diff --git a/app/code/Magento/CatalogInventory/Block/Stockqty/DefaultStockqty.php b/app/code/Magento/CatalogInventory/Block/Stockqty/DefaultStockqty.php index c315338be0de..5a3a3ca6ee98 100644 --- a/app/code/Magento/CatalogInventory/Block/Stockqty/DefaultStockqty.php +++ b/app/code/Magento/CatalogInventory/Block/Stockqty/DefaultStockqty.php @@ -11,6 +11,10 @@ * * @api * @since 100.0.2 + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ class DefaultStockqty extends AbstractStockqty implements \Magento\Framework\DataObject\IdentityInterface { diff --git a/app/code/Magento/CatalogInventory/Helper/Stock.php b/app/code/Magento/CatalogInventory/Helper/Stock.php index 494d440eeed8..798ac4074c18 100644 --- a/app/code/Magento/CatalogInventory/Helper/Stock.php +++ b/app/code/Magento/CatalogInventory/Helper/Stock.php @@ -18,6 +18,10 @@ * Class Stock * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ class Stock { diff --git a/app/code/Magento/CatalogInventory/Model/AddStockStatusToCollection.php b/app/code/Magento/CatalogInventory/Model/AddStockStatusToCollection.php index 2c52b49c1a03..0a02d4eb6a9a 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/Adminhtml/Stock/Item.php b/app/code/Magento/CatalogInventory/Model/Adminhtml/Stock/Item.php index 36721db87488..145b0d1454ae 100644 --- a/app/code/Magento/CatalogInventory/Model/Adminhtml/Stock/Item.php +++ b/app/code/Magento/CatalogInventory/Model/Adminhtml/Stock/Item.php @@ -20,6 +20,10 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api * @since 100.0.2 + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ class Item extends \Magento\CatalogInventory\Model\Stock\Item implements IdentityInterface { diff --git a/app/code/Magento/CatalogInventory/Model/Configuration.php b/app/code/Magento/CatalogInventory/Model/Configuration.php index 2f0415b40dc0..8b0849c8874b 100644 --- a/app/code/Magento/CatalogInventory/Model/Configuration.php +++ b/app/code/Magento/CatalogInventory/Model/Configuration.php @@ -9,6 +9,7 @@ use Magento\CatalogInventory\Helper\Minsaleqty as MinsaleqtyHelper; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Catalog\Model\ProductTypes\ConfigInterface; +use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\StoreManagerInterface; /** @@ -131,6 +132,8 @@ class Configuration implements StockConfigurationInterface protected $storeManager; /** + * Configuration constructor. + * * @param ConfigInterface $config * @param ScopeConfigInterface $scopeConfig * @param MinsaleqtyHelper $minsaleqtyHelper @@ -149,7 +152,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getDefaultScopeId() { @@ -159,6 +162,8 @@ public function getDefaultScopeId() } /** + * Is Qty Type Ids + * * @param int|null $filter * @return array */ @@ -182,6 +187,8 @@ public function getIsQtyTypeIds($filter = null) } /** + * Is Qty + * * @param int $productTypeId * @return bool */ @@ -201,12 +208,14 @@ public function canSubtractQty($store = null) { return $this->scopeConfig->isSetFlag( self::XML_PATH_CAN_SUBTRACT, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $store ); } /** + * Get Min Qty + * * @param null|string|bool|int|\Magento\Store\Model\Store $store * @return float */ @@ -214,12 +223,14 @@ public function getMinQty($store = null) { return (float)$this->scopeConfig->getValue( self::XML_PATH_MIN_QTY, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $store ); } /** + * Get Min Sale Qty + * * @param null|string|bool|int|\Magento\Store\Model\Store $store * @param int $customerGroupId * @return float @@ -230,6 +241,8 @@ public function getMinSaleQty($store = null, $customerGroupId = null) } /** + * Get Max Sale Qty + * * @param null|string|bool|int|\Magento\Store\Model\Store $store * @return float|null */ @@ -237,12 +250,14 @@ public function getMaxSaleQty($store = null) { return (float)$this->scopeConfig->getValue( self::XML_PATH_MAX_SALE_QTY, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $store ); } /** + * Get Notify Stock Qty + * * @param null|string|bool|int|\Magento\Store\Model\Store $store * @return float */ @@ -250,7 +265,7 @@ public function getNotifyStockQty($store = null) { return (float) $this->scopeConfig->getValue( self::XML_PATH_NOTIFY_STOCK_QTY, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $store ); } @@ -264,14 +279,16 @@ public function getNotifyStockQty($store = null) */ public function getEnableQtyIncrements($store = null) { - return (bool) $this->scopeConfig->getValue( + return $this->scopeConfig->isSetFlag( self::XML_PATH_ENABLE_QTY_INCREMENTS, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $store ); } /** + * Get Qty Increments + * * @param null|string|bool|int|\Magento\Store\Model\Store $store * @return float */ @@ -279,7 +296,7 @@ public function getQtyIncrements($store = null) { return (float)$this->scopeConfig->getValue( self::XML_PATH_QTY_INCREMENTS, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $store ); } @@ -294,7 +311,7 @@ public function getBackorders($store = null) { return (int) $this->scopeConfig->getValue( self::XML_PATH_BACKORDERS, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $store ); } @@ -309,7 +326,7 @@ public function getManageStock($store = null) { return (int) $this->scopeConfig->isSetFlag( self::XML_PATH_MANAGE_STOCK, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $store ); } @@ -325,7 +342,7 @@ public function getCanBackInStock($store = null) { return $this->scopeConfig->isSetFlag( self::XML_PATH_CAN_BACK_IN_STOCK, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $store ); } @@ -340,7 +357,7 @@ public function isShowOutOfStock($store = null) { return $this->scopeConfig->isSetFlag( self::XML_PATH_SHOW_OUT_OF_STOCK, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $store ); } @@ -355,14 +372,13 @@ public function isAutoReturnEnabled($store = null) { return $this->scopeConfig->isSetFlag( self::XML_PATH_ITEM_AUTO_RETURN, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $store ); } /** - * Get 'Display product stock status' option value - * Shows if it is necessary to show product stock status ('in stock'/'out of stock') + * Display product stock status. Shows if it is necessary to show product stock status in stock/out of stock. * * @param null|string|bool|int|\Magento\Store\Model\Store $store * @return bool @@ -371,12 +387,14 @@ public function isDisplayProductStockStatus($store = null) { return $this->scopeConfig->isSetFlag( self::XML_PATH_DISPLAY_PRODUCT_STOCK_STATUS, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $store ); } /** + * Get Default Config Value + * * @param string $field * @param null|string|bool|int|\Magento\Store\Model\Store $store * @return string|null @@ -385,12 +403,14 @@ public function getDefaultConfigValue($field, $store = null) { return $this->scopeConfig->getValue( self::XML_PATH_ITEM . $field, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $store ); } /** + * Get Stock Threshold Qty + * * @param null|string|bool|int|\Magento\Store\Model\Store $store * @return string|null */ @@ -398,7 +418,7 @@ public function getStockThresholdQty($store = null) { return $this->scopeConfig->getValue( self::XML_PATH_STOCK_THRESHOLD_QTY, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $store ); } diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/ProductPriceIndexFilter.php b/app/code/Magento/CatalogInventory/Model/Indexer/ProductPriceIndexFilter.php index 5e7210c1b744..f9a49d4f8d12 100644 --- a/app/code/Magento/CatalogInventory/Model/Indexer/ProductPriceIndexFilter.php +++ b/app/code/Magento/CatalogInventory/Model/Indexer/ProductPriceIndexFilter.php @@ -9,11 +9,11 @@ use Magento\CatalogInventory\Api\StockConfigurationInterface; use Magento\CatalogInventory\Model\ResourceModel\Stock\Item; -use Magento\CatalogInventory\Model\Stock; use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\PriceModifierInterface; use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\IndexTableStructure; use Magento\Framework\App\ResourceConnection; use Magento\Framework\App\ObjectManager; +use Magento\Framework\DB\Query\Generator; /** * Class for filter product price index. @@ -40,22 +40,38 @@ class ProductPriceIndexFilter implements PriceModifierInterface */ private $connectionName; + /** + * @var Generator + */ + private $batchQueryGenerator; + + /** + * @var int + */ + private $batchSize; + /** * @param StockConfigurationInterface $stockConfiguration * @param Item $stockItem * @param ResourceConnection $resourceConnection * @param string $connectionName + * @param Generator $batchQueryGenerator + * @param int $batchSize */ public function __construct( StockConfigurationInterface $stockConfiguration, Item $stockItem, ResourceConnection $resourceConnection = null, - $connectionName = 'indexer' + $connectionName = 'indexer', + Generator $batchQueryGenerator = null, + $batchSize = 100 ) { $this->stockConfiguration = $stockConfiguration; $this->stockItem = $stockItem; $this->resourceConnection = $resourceConnection ?: ObjectManager::getInstance()->get(ResourceConnection::class); $this->connectionName = $connectionName; + $this->batchQueryGenerator = $batchQueryGenerator ?: ObjectManager::getInstance()->get(Generator::class); + $this->batchSize = $batchSize; } /** @@ -76,32 +92,37 @@ public function modifyPrice(IndexTableStructure $priceTable, array $entityIds = $connection = $this->resourceConnection->getConnection($this->connectionName); $select = $connection->select(); + $select->from( - ['price_index' => $priceTable->getTableName()], - [] - ); - $select->joinInner( ['stock_item' => $this->stockItem->getMainTable()], - 'stock_item.product_id = price_index.' . $priceTable->getEntityField() - . ' AND stock_item.stock_id = ' . Stock::DEFAULT_STOCK_ID, - [] + ['stock_item.product_id', 'MAX(stock_item.is_in_stock) as max_is_in_stock'] ); + if ($this->stockConfiguration->getManageStock()) { - $stockStatus = $connection->getCheckSql( - 'use_config_manage_stock = 0 AND manage_stock = 0', - Stock::STOCK_IN_STOCK, - 'is_in_stock' - ); + $select->where('stock_item.use_config_manage_stock = 1 OR stock_item.manage_stock = 1'); } else { - $stockStatus = $connection->getCheckSql( - 'use_config_manage_stock = 0 AND manage_stock = 1', - 'is_in_stock', - Stock::STOCK_IN_STOCK - ); + $select->where('stock_item.use_config_manage_stock = 0 AND stock_item.manage_stock = 1'); } - $select->where($stockStatus . ' = ?', Stock::STOCK_OUT_OF_STOCK); - $query = $select->deleteFromSelect('price_index'); - $connection->query($query); + $select->group('stock_item.product_id'); + $select->having('max_is_in_stock = 0'); + + $batchSelectIterator = $this->batchQueryGenerator->generate( + 'product_id', + $select, + $this->batchSize, + \Magento\Framework\DB\Query\BatchIteratorInterface::UNIQUE_FIELD_ITERATOR + ); + + foreach ($batchSelectIterator as $select) { + $productIds = null; + foreach ($connection->query($select)->fetchAll() as $row) { + $productIds[] = $row['product_id']; + } + if ($productIds !== null) { + $where = [$priceTable->getEntityField() .' IN (?)' => $productIds]; + $connection->delete($priceTable->getTableName(), $where); + } + } } } 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 bc10d38173b4..43a5aabee977 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/Indexer/Stock/CacheCleaner.php b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php index a32faa4640a8..b3fa07479a71 100644 --- a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php +++ b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php @@ -10,8 +10,10 @@ use Magento\CatalogInventory\Api\StockConfigurationInterface; use Magento\Framework\App\ResourceConnection; +use Magento\Framework\App\ObjectManager; use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Indexer\CacheContext; use Magento\CatalogInventory\Model\Stock; use Magento\Catalog\Model\Product; @@ -46,25 +48,35 @@ class CacheCleaner */ private $connection; + /** + * @var MetadataPool + */ + private $metadataPool; + /** * @param ResourceConnection $resource * @param StockConfigurationInterface $stockConfiguration * @param CacheContext $cacheContext * @param ManagerInterface $eventManager + * @param MetadataPool|null $metadataPool */ public function __construct( ResourceConnection $resource, StockConfigurationInterface $stockConfiguration, CacheContext $cacheContext, - ManagerInterface $eventManager + ManagerInterface $eventManager, + MetadataPool $metadataPool = null ) { $this->resource = $resource; $this->stockConfiguration = $stockConfiguration; $this->cacheContext = $cacheContext; $this->eventManager = $eventManager; + $this->metadataPool = $metadataPool ?: ObjectManager::getInstance()->get(MetadataPool::class); } /** + * Clean cache by product ids. + * * @param array $productIds * @param callable $reindex * @return void @@ -76,22 +88,37 @@ public function clean(array $productIds, callable $reindex) $productStatusesAfter = $this->getProductStockStatuses($productIds); $productIds = $this->getProductIdsForCacheClean($productStatusesBefore, $productStatusesAfter); if ($productIds) { - $this->cacheContext->registerEntities(Product::CACHE_TAG, $productIds); + $this->cacheContext->registerEntities(Product::CACHE_TAG, array_unique($productIds)); $this->eventManager->dispatch('clean_cache_by_tags', ['object' => $this->cacheContext]); } } /** + * Get current stock statuses for product ids. + * * @param array $productIds * @return array */ private function getProductStockStatuses(array $productIds) { + $linkField = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class) + ->getLinkField(); $select = $this->getConnection()->select() ->from( - $this->resource->getTableName('cataloginventory_stock_status'), + ['css' => $this->resource->getTableName('cataloginventory_stock_status')], ['product_id', 'stock_status', 'qty'] - )->where('product_id IN (?)', $productIds) + ) + ->joinLeft( + ['cpr' => $this->resource->getTableName('catalog_product_relation')], + 'css.product_id = cpr.child_id', + [] + ) + ->joinLeft( + ['cpe' => $this->resource->getTableName('catalog_product_entity')], + 'cpr.parent_id = cpe.' . $linkField, + ['parent_id' => 'cpe.entity_id'] + ) + ->where('product_id IN (?)', $productIds) ->where('stock_id = ?', Stock::DEFAULT_STOCK_ID) ->where('website_id = ?', $this->stockConfiguration->getDefaultScopeId()); @@ -125,6 +152,9 @@ private function getProductIdsForCacheClean(array $productStatusesBefore, array if ($statusBefore['stock_status'] !== $statusAfter['stock_status'] || ($stockThresholdQty && $statusAfter['qty'] <= $stockThresholdQty)) { $productIds[] = $productId; + if (isset($statusAfter['parent_id'])) { + $productIds[] = $statusAfter['parent_id']; + } } } @@ -132,6 +162,8 @@ private function getProductIdsForCacheClean(array $productStatusesBefore, array } /** + * Get database connection. + * * @return AdapterInterface */ private function getConnection() diff --git a/app/code/Magento/CatalogInventory/Model/Plugin/Layer.php b/app/code/Magento/CatalogInventory/Model/Plugin/Layer.php index b8e8e47bb1fb..168e947b8fa5 100644 --- a/app/code/Magento/CatalogInventory/Model/Plugin/Layer.php +++ b/app/code/Magento/CatalogInventory/Model/Plugin/Layer.php @@ -3,8 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\CatalogInventory\Model\Plugin; +use Magento\Framework\Search\EngineResolverInterface; +use Magento\Search\Model\EngineResolver; + +/** + * Catalog inventory plugin for layer. + */ class Layer { /** @@ -21,16 +28,24 @@ class Layer */ protected $scopeConfig; + /** + * @var EngineResolverInterface + */ + private $engineResolver; + /** * @param \Magento\CatalogInventory\Helper\Stock $stockHelper * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + * @param EngineResolverInterface $engineResolver */ public function __construct( \Magento\CatalogInventory\Helper\Stock $stockHelper, - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, + EngineResolverInterface $engineResolver ) { $this->stockHelper = $stockHelper; $this->scopeConfig = $scopeConfig; + $this->engineResolver = $engineResolver; } /** @@ -46,12 +61,22 @@ public function beforePrepareProductCollection( \Magento\Catalog\Model\Layer $subject, \Magento\Catalog\Model\ResourceModel\Collection\AbstractCollection $collection ) { - if ($this->_isEnabledShowOutOfStock()) { + if (!$this->isCurrentEngineMysql() || $this->_isEnabledShowOutOfStock()) { return; } $this->stockHelper->addIsInStockFilterToCollection($collection); } + /** + * Check if current engine is MYSQL. + * + * @return bool + */ + private function isCurrentEngineMysql() + { + return $this->engineResolver->getCurrentSearchEngine() === EngineResolver::CATALOG_SEARCH_MYSQL_ENGINE; + } + /** * Get config value for 'display out of stock' option * diff --git a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php index 0cc77bb7caf3..502d9532e8a0 100644 --- a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php +++ b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php @@ -17,10 +17,18 @@ use Magento\CatalogInventory\Model\Stock; use Magento\Framework\Event\Observer; use Magento\Framework\Exception\LocalizedException; +use Magento\Quote\Model\Quote\Item; /** + * Quote item quantity validator. + * * @api * @since 100.0.2 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ class QuantityValidator { @@ -67,8 +75,7 @@ public function __construct( * Add error information to Quote Item * * @param \Magento\Framework\DataObject $result - * @param \Magento\Quote\Model\Quote\Item $quoteItem - * @param bool $removeError + * @param Item $quoteItem * @return void */ private function addErrorInfoToQuote($result, $quoteItem) @@ -100,7 +107,7 @@ private function addErrorInfoToQuote($result, $quoteItem) */ public function validate(Observer $observer) { - /* @var $quoteItem \Magento\Quote\Model\Quote\Item */ + /* @var $quoteItem Item */ $quoteItem = $observer->getEvent()->getItem(); if (!$quoteItem || !$quoteItem->getProductId() || @@ -175,35 +182,11 @@ public function validate(Observer $observer) $qty = $product->getTypeInstance()->prepareQuoteItemQty($qty, $product); $quoteItem->setData('qty', $qty); if ($stockStatus) { - $result = $this->stockState->checkQtyIncrements( - $product->getId(), - $qty, - $product->getStore()->getWebsiteId() - ); - if ($result->getHasError()) { - $quoteItem->addErrorInfo( - 'cataloginventory', - Data::ERROR_QTY_INCREMENTS, - $result->getMessage() - ); - - $quoteItem->getQuote()->addErrorInfo( - $result->getQuoteMessageIndex(), - 'cataloginventory', - Data::ERROR_QTY_INCREMENTS, - $result->getQuoteMessage() - ); - } else { - // Delete error from item and its quote, if it was set due to qty problems - $this->_removeErrorsFromQuoteAndItem( - $quoteItem, - Data::ERROR_QTY_INCREMENTS - ); - } + $this->checkOptionsQtyIncrements($quoteItem, $options); } + // variable to keep track if we have previously encountered an error in one of the options $removeError = true; - foreach ($options as $option) { $result = $option->getStockStateResult(); if ($result->getHasError()) { @@ -228,10 +211,47 @@ public function validate(Observer $observer) } } + /** + * Verifies product options quantity increments. + * + * @param Item $quoteItem + * @param array $options + * @return void + */ + private function checkOptionsQtyIncrements(Item $quoteItem, array $options): void + { + $removeErrors = true; + foreach ($options as $option) { + $result = $this->stockState->checkQtyIncrements( + $option->getProduct()->getId(), + $quoteItem->getData('qty'), + $option->getProduct()->getStore()->getWebsiteId() + ); + if ($result->getHasError()) { + $quoteItem->getQuote()->addErrorInfo( + $result->getQuoteMessageIndex(), + 'cataloginventory', + Data::ERROR_QTY_INCREMENTS, + $result->getQuoteMessage() + ); + + $removeErrors = false; + } + } + + if ($removeErrors) { + // Delete error from item and its quote, if it was set due to qty problems + $this->_removeErrorsFromQuoteAndItem( + $quoteItem, + Data::ERROR_QTY_INCREMENTS + ); + } + } + /** * Removes error statuses from quote and item, set by this observer * - * @param \Magento\Quote\Model\Quote\Item $item + * @param Item $item * @param int $code * @return void */ diff --git a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/Initializer/Option.php b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/Initializer/Option.php index b99e43d52f47..a48b9ae5f080 100644 --- a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/Initializer/Option.php +++ b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/Initializer/Option.php @@ -9,6 +9,9 @@ use Magento\CatalogInventory\Api\StockStateInterface; use Magento\CatalogInventory\Model\Quote\Item\QuantityValidator\QuoteItemQtyList; +/** + * Quote item option initializer. + */ class Option { /** @@ -67,10 +70,6 @@ public function getStockItem( * define that stock item is child for composite product */ $stockItem->setIsChildItem(true); - /** - * don't check qty increments value for option product - */ - $stockItem->setSuppressCheckQtyIncrements(true); return $stockItem; } @@ -121,7 +120,7 @@ public function initialize( /** * if option's qty was updates we also need to update quote item qty */ - $quoteItem->setData('qty', intval($qty)); + $quoteItem->setData('qty', (int) $qty); } if ($result->getMessage() !== null) { $option->setMessage($result->getMessage()); diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php index 366cb1c3902a..ba3b62f55476 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php @@ -18,6 +18,10 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api * @since 100.0.2 + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ class DefaultStock extends AbstractIndexer implements StockInterface { diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/QueryProcessorInterface.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/QueryProcessorInterface.php index abe473bd9682..115002b23764 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/QueryProcessorInterface.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/QueryProcessorInterface.php @@ -11,6 +11,10 @@ /** * @api * @since 100.1.0 + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ interface QueryProcessorInterface { diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/StockInterface.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/StockInterface.php index 5ced55edf208..24ed49637281 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/StockInterface.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/StockInterface.php @@ -9,6 +9,10 @@ * CatalogInventory Stock Indexer Interface * @api * @since 100.0.2 + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ interface StockInterface { diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/StockFactory.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/StockFactory.php index f9738484766b..0ee162e429f4 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/StockFactory.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/StockFactory.php @@ -12,6 +12,10 @@ /** * @api * @since 100.0.2 + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ class StockFactory { diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item.php index ce8930ad4f7a..edccad60231e 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item.php @@ -263,6 +263,12 @@ public function updateLowStockDate(int $websiteId) $connection->update($this->getMainTable(), $value, $where); } + /** + * Get Manage Stock Expression + * + * @param string $tableAlias + * @return \Zend_Db_Expr + */ public function getManageStockExpr(string $tableAlias = ''): \Zend_Db_Expr { if ($tableAlias) { @@ -277,6 +283,12 @@ public function getManageStockExpr(string $tableAlias = ''): \Zend_Db_Expr return $manageStock; } + /** + * Get Backorders Expression + * + * @param string $tableAlias + * @return \Zend_Db_Expr + */ public function getBackordersExpr(string $tableAlias = ''): \Zend_Db_Expr { if ($tableAlias) { @@ -291,6 +303,12 @@ public function getBackordersExpr(string $tableAlias = ''): \Zend_Db_Expr return $itemBackorders; } + /** + * Get Minimum Sale Quantity Expression + * + * @param string $tableAlias + * @return \Zend_Db_Expr + */ public function getMinSaleQtyExpr(string $tableAlias = ''): \Zend_Db_Expr { if ($tableAlias) { diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php index bc5fda4939ad..402ce5f2f611 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php @@ -13,6 +13,10 @@ * CatalogInventory Stock Status per website Resource Model * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ class Status extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb { diff --git a/app/code/Magento/CatalogInventory/Model/Source/Backorders.php b/app/code/Magento/CatalogInventory/Model/Source/Backorders.php index 1d200d27a444..0bffb9a9888c 100644 --- a/app/code/Magento/CatalogInventory/Model/Source/Backorders.php +++ b/app/code/Magento/CatalogInventory/Model/Source/Backorders.php @@ -9,6 +9,10 @@ * Back orders source class * @api * @since 100.0.2 + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ class Backorders implements \Magento\Framework\Option\ArrayInterface { diff --git a/app/code/Magento/CatalogInventory/Model/Source/Stock.php b/app/code/Magento/CatalogInventory/Model/Source/Stock.php index 9ed891d1dcc0..7d44ab782de6 100644 --- a/app/code/Magento/CatalogInventory/Model/Source/Stock.php +++ b/app/code/Magento/CatalogInventory/Model/Source/Stock.php @@ -11,6 +11,10 @@ * CatalogInventory Stock source model * @api * @since 100.0.2 + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ class Stock extends AbstractSource { diff --git a/app/code/Magento/CatalogInventory/Model/Spi/StockRegistryProviderInterface.php b/app/code/Magento/CatalogInventory/Model/Spi/StockRegistryProviderInterface.php index f711268bc793..0fa4b919c40f 100644 --- a/app/code/Magento/CatalogInventory/Model/Spi/StockRegistryProviderInterface.php +++ b/app/code/Magento/CatalogInventory/Model/Spi/StockRegistryProviderInterface.php @@ -7,16 +7,24 @@ /** * Interface StockRegistryProviderInterface + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ interface StockRegistryProviderInterface { /** + * Get stock. + * * @param int $scopeId * @return \Magento\CatalogInventory\Api\Data\StockInterface */ public function getStock($scopeId); /** + * Get stock item. + * * @param int $productId * @param int $scopeId * @return \Magento\CatalogInventory\Api\Data\StockItemInterface @@ -24,6 +32,8 @@ public function getStock($scopeId); public function getStockItem($productId, $scopeId); /** + * Get stock status. + * * @param int $productId * @param int $scopeId * @return \Magento\CatalogInventory\Api\Data\StockStatusInterface diff --git a/app/code/Magento/CatalogInventory/Model/Spi/StockStateProviderInterface.php b/app/code/Magento/CatalogInventory/Model/Spi/StockStateProviderInterface.php index 89fb54e7e496..30f703b5b928 100644 --- a/app/code/Magento/CatalogInventory/Model/Spi/StockStateProviderInterface.php +++ b/app/code/Magento/CatalogInventory/Model/Spi/StockStateProviderInterface.php @@ -9,22 +9,32 @@ /** * Interface StockStateProviderInterface + * + * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html + * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ interface StockStateProviderInterface { /** + * Verify stock. + * * @param StockItemInterface $stockItem * @return bool */ public function verifyStock(StockItemInterface $stockItem); /** + * Verify notification. + * * @param StockItemInterface $stockItem * @return bool */ public function verifyNotification(StockItemInterface $stockItem); /** + * Validate quote qty. + * * @param StockItemInterface $stockItem * @param int|float $itemQty * @param int|float $qtyToCheck @@ -44,8 +54,9 @@ public function checkQuoteItemQty(StockItemInterface $stockItem, $itemQty, $qtyT public function checkQty(StockItemInterface $stockItem, $qty); /** - * Returns suggested qty that satisfies qty increments and minQty/maxQty/minSaleQty/maxSaleQty conditions - * or original qty if such value does not exist + * Returns suggested qty or original qty if such value does not exist. + * + * Suggested qty satisfies qty increments and minQty/maxQty/minSaleQty/maxSaleQty conditions. * * @param StockItemInterface $stockItem * @param int|float $qty @@ -54,6 +65,8 @@ public function checkQty(StockItemInterface $stockItem, $qty); public function suggestQty(StockItemInterface $stockItem, $qty); /** + * Check qty increments. + * * @param StockItemInterface $stockItem * @param int|float $qty * @return \Magento\Framework\DataObject diff --git a/app/code/Magento/CatalogInventory/Model/Stock/Item.php b/app/code/Magento/CatalogInventory/Model/Stock/Item.php index bcab2c622a5b..553ea7393c94 100644 --- a/app/code/Magento/CatalogInventory/Model/Stock/Item.php +++ b/app/code/Magento/CatalogInventory/Model/Stock/Item.php @@ -206,7 +206,7 @@ public function getStockStatusChangedAuto() */ public function getQty() { - return null === $this->_getData(static::QTY) ? null : floatval($this->_getData(static::QTY)); + return null === $this->_getData(static::QTY) ? null : (float)$this->_getData(static::QTY); } /** diff --git a/app/code/Magento/CatalogInventory/Model/Stock/Status.php b/app/code/Magento/CatalogInventory/Model/Stock/Status.php index 899056d8f083..4941d5d333bd 100644 --- a/app/code/Magento/CatalogInventory/Model/Stock/Status.php +++ b/app/code/Magento/CatalogInventory/Model/Stock/Status.php @@ -72,6 +72,8 @@ protected function _construct() //@codeCoverageIgnoreStart /** + * Retrieve product ID + * * @return int */ public function getProductId() @@ -80,6 +82,8 @@ public function getProductId() } /** + * Retrieve website ID + * * @return int */ public function getWebsiteId() @@ -88,6 +92,8 @@ public function getWebsiteId() } /** + * Retrieve stock ID + * * @return int */ public function getStockId() @@ -96,6 +102,8 @@ public function getStockId() } /** + * Retrieve qty + * * @return int */ public function getQty() @@ -104,16 +112,20 @@ public function getQty() } /** + * Retrieve stock status + * * @return int */ - public function getStockStatus() + public function getStockStatus(): int { - return $this->getData(self::KEY_STOCK_STATUS); + return (int)$this->getData(self::KEY_STOCK_STATUS); } //@codeCoverageIgnoreEnd /** + * Retrieve stock item + * * @return StockItemInterface */ public function getStockItem() @@ -124,6 +136,8 @@ public function getStockItem() //@codeCoverageIgnoreStart /** + * Set product ID + * * @param int $productId * @return $this */ @@ -133,6 +147,8 @@ public function setProductId($productId) } /** + * Set web website ID + * * @param int $websiteId * @return $this */ @@ -142,6 +158,8 @@ public function setWebsiteId($websiteId) } /** + * Set stock ID + * * @param int $stockId * @return $this */ @@ -151,6 +169,8 @@ public function setStockId($stockId) } /** + * Set qty + * * @param int $qty * @return $this */ @@ -160,6 +180,8 @@ public function setQty($qty) } /** + * Set stock status + * * @param int $stockStatus * @return $this */ @@ -169,7 +191,7 @@ public function setStockStatus($stockStatus) } /** - * {@inheritdoc} + * Retrieve existing extension attributes object or create a new one. * * @return \Magento\CatalogInventory\Api\Data\StockStatusExtensionInterface|null */ @@ -179,7 +201,7 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * Set an extension attributes object. * * @param \Magento\CatalogInventory\Api\Data\StockStatusExtensionInterface $extensionAttributes * @return $this diff --git a/app/code/Magento/CatalogInventory/Model/StockManagement.php b/app/code/Magento/CatalogInventory/Model/StockManagement.php index b3939f2e5149..5d7d099dc01a 100644 --- a/app/code/Magento/CatalogInventory/Model/StockManagement.php +++ b/app/code/Magento/CatalogInventory/Model/StockManagement.php @@ -85,6 +85,7 @@ public function __construct( /** * Subtract product qtys from stock. + * * Return array of items that require full save. * * @param string[] $items @@ -141,17 +142,25 @@ public function registerProductsSale($items, $websiteId = null) } /** - * @param string[] $items - * @param int $websiteId - * @return bool + * @inheritdoc */ public function revertProductsSale($items, $websiteId = null) { //if (!$websiteId) { $websiteId = $this->stockConfiguration->getDefaultScopeId(); //} - $this->qtyCounter->correctItemsQty($items, $websiteId, '+'); - return true; + $revertItems = []; + foreach ($items as $productId => $qty) { + $stockItem = $this->stockRegistryProvider->getStockItem($productId, $websiteId); + $canSubtractQty = $stockItem->getItemId() && $this->canSubtractQty($stockItem); + if (!$canSubtractQty || !$this->stockConfiguration->isQty($stockItem->getTypeId())) { + continue; + } + $revertItems[$productId] = $qty; + } + $this->qtyCounter->correctItemsQty($revertItems, $websiteId, '+'); + + return $revertItems; } /** @@ -195,6 +204,8 @@ protected function getProductType($productId) } /** + * Get stock resource. + * * @return ResourceStock */ protected function getResource() diff --git a/app/code/Magento/CatalogInventory/Model/StockRegistryStorage.php b/app/code/Magento/CatalogInventory/Model/StockRegistryStorage.php index 0a54adfe91c5..8238c1e8f6b2 100644 --- a/app/code/Magento/CatalogInventory/Model/StockRegistryStorage.php +++ b/app/code/Magento/CatalogInventory/Model/StockRegistryStorage.php @@ -68,7 +68,7 @@ public function removeStock($scopeId = null) */ public function getStockItem($productId, $scopeId) { - return isset($this->stockItems[$productId][$scopeId]) ? $this->stockItems[$productId][$scopeId] : null; + return $this->stockItems[$productId][$scopeId] ?? null; } /** @@ -103,7 +103,7 @@ public function removeStockItem($productId, $scopeId = null) */ public function getStockStatus($productId, $scopeId) { - return isset($this->stockStatuses[$productId][$scopeId]) ? $this->stockStatuses[$productId][$scopeId] : null; + return $this->stockStatuses[$productId][$scopeId] ?? null; } /** diff --git a/app/code/Magento/CatalogInventory/Model/StockStateProvider.php b/app/code/Magento/CatalogInventory/Model/StockStateProvider.php index fb6fc3be6137..6851b05aa56a 100644 --- a/app/code/Magento/CatalogInventory/Model/StockStateProvider.php +++ b/app/code/Magento/CatalogInventory/Model/StockStateProvider.php @@ -9,9 +9,9 @@ use Magento\Catalog\Model\ProductFactory; use Magento\CatalogInventory\Api\Data\StockItemInterface; use Magento\CatalogInventory\Model\Spi\StockStateProviderInterface; +use Magento\Framework\DataObject\Factory as ObjectFactory; use Magento\Framework\Locale\FormatInterface; use Magento\Framework\Math\Division as MathDivision; -use Magento\Framework\DataObject\Factory as ObjectFactory; /** * Interface StockStateProvider @@ -65,6 +65,8 @@ public function __construct( } /** + * Validate stock + * * @param StockItemInterface $stockItem * @return bool */ @@ -82,6 +84,8 @@ public function verifyStock(StockItemInterface $stockItem) } /** + * Verify notification + * * @param StockItemInterface $stockItem * @return bool */ @@ -91,6 +95,8 @@ public function verifyNotification(StockItemInterface $stockItem) } /** + * Validate quote qty + * * @param StockItemInterface $stockItem * @param int|float $qty * @param int|float $summaryQty @@ -113,14 +119,12 @@ public function checkQuoteItemQty(StockItemInterface $stockItem, $qty, $summaryQ $result->setItemIsQtyDecimal($stockItem->getIsQtyDecimal()); if (!$stockItem->getIsQtyDecimal()) { $result->setHasQtyOptionUpdate(true); - $qty = intval($qty); + $qty = (int) $qty ?: 1; /** * Adding stock data to quote item */ $result->setItemQty($qty); - $qty = $this->getNumber($qty); - $origQty = intval($origQty); - $result->setOrigQty($origQty); + $result->setOrigQty((int)$this->getNumber($origQty) ?: 1); } if ($stockItem->getMinSaleQty() && $qty < $stockItem->getMinSaleQty()) { @@ -254,6 +258,8 @@ public function checkQty(StockItemInterface $stockItem, $qty) } /** + * Returns suggested qty + * * Returns suggested qty that satisfies qty increments and minQty/maxQty/minSaleQty/maxSaleQty conditions * or original qty if such value does not exist * @@ -294,6 +300,8 @@ public function suggestQty(StockItemInterface $stockItem, $qty) } /** + * Check Qty Increments + * * @param StockItemInterface $stockItem * @param float|int $qty * @return \Magento\Framework\DataObject @@ -369,6 +377,8 @@ public function getStockQty(StockItemInterface $stockItem) } /** + * Get numeric qty + * * @param string|float|int|null $qty * @return float|null */ diff --git a/app/code/Magento/CatalogInventory/Observer/CancelOrderItemObserver.php b/app/code/Magento/CatalogInventory/Observer/CancelOrderItemObserver.php index 1e99794d68a4..098e254d785a 100644 --- a/app/code/Magento/CatalogInventory/Observer/CancelOrderItemObserver.php +++ b/app/code/Magento/CatalogInventory/Observer/CancelOrderItemObserver.php @@ -6,15 +6,21 @@ namespace Magento\CatalogInventory\Observer; -use Magento\Framework\Event\ObserverInterface; use Magento\CatalogInventory\Api\StockManagementInterface; +use Magento\CatalogInventory\Model\Configuration; use Magento\Framework\Event\Observer as EventObserver; +use Magento\Framework\Event\ObserverInterface; /** * Catalog inventory module observer */ class CancelOrderItemObserver implements ObserverInterface { + /** + * @var \Magento\CatalogInventory\Model\Configuration + */ + protected $configuration; + /** * @var StockManagementInterface */ @@ -26,13 +32,16 @@ class CancelOrderItemObserver implements ObserverInterface protected $priceIndexer; /** + * @param Configuration $configuration * @param StockManagementInterface $stockManagement * @param \Magento\Catalog\Model\Indexer\Product\Price\Processor $priceIndexer */ public function __construct( + Configuration $configuration, StockManagementInterface $stockManagement, \Magento\Catalog\Model\Indexer\Product\Price\Processor $priceIndexer ) { + $this->configuration = $configuration; $this->stockManagement = $stockManagement; $this->priceIndexer = $priceIndexer; } @@ -49,7 +58,8 @@ public function execute(EventObserver $observer) $item = $observer->getEvent()->getItem(); $children = $item->getChildrenItems(); $qty = $item->getQtyOrdered() - max($item->getQtyShipped(), $item->getQtyInvoiced()) - $item->getQtyCanceled(); - if ($item->getId() && $item->getProductId() && empty($children) && $qty) { + if ($item->getId() && $item->getProductId() && empty($children) && $qty && $this->configuration + ->getCanBackInStock()) { $this->stockManagement->backItemQty($item->getProductId(), $qty, $item->getStore()->getWebsiteId()); } $this->priceIndexer->reindexRow($item->getProductId()); diff --git a/app/code/Magento/CatalogInventory/Observer/ProcessInventoryDataObserver.php b/app/code/Magento/CatalogInventory/Observer/ProcessInventoryDataObserver.php index b831af53d4af..6aad119694a9 100644 --- a/app/code/Magento/CatalogInventory/Observer/ProcessInventoryDataObserver.php +++ b/app/code/Magento/CatalogInventory/Observer/ProcessInventoryDataObserver.php @@ -12,8 +12,7 @@ use Magento\Framework\Event\Observer as EventObserver; /** - * This observer prepares stock data for saving by combining stock data from the stock data property - * and quantity_and_stock_status attribute and setting it to the single point represented by stock data property. + * Prepares stock data for saving * * @deprecated 100.2.0 Stock data should be processed using the module API * @see StockItemInterface when you want to change the stock data @@ -74,7 +73,6 @@ private function processStockData(Product $product) $this->setStockDataToProduct($product, $stockItem, $quantityAndStockStatus); } } - $product->unsetData('quantity_and_stock_status'); } /** diff --git a/app/code/Magento/CatalogInventory/Observer/RevertQuoteInventoryObserver.php b/app/code/Magento/CatalogInventory/Observer/RevertQuoteInventoryObserver.php index 93a50cc9a7a4..ab21f32b3f62 100644 --- a/app/code/Magento/CatalogInventory/Observer/RevertQuoteInventoryObserver.php +++ b/app/code/Magento/CatalogInventory/Observer/RevertQuoteInventoryObserver.php @@ -64,8 +64,8 @@ public function execute(EventObserver $observer) { $quote = $observer->getEvent()->getQuote(); $items = $this->productQty->getProductQty($quote->getAllItems()); - $this->stockManagement->revertProductsSale($items, $quote->getStore()->getWebsiteId()); - $productIds = array_keys($items); + $revertedItems = $this->stockManagement->revertProductsSale($items, $quote->getStore()->getWebsiteId()); + $productIds = array_keys($revertedItems); if (!empty($productIds)) { $this->stockIndexerProcessor->reindexList($productIds); $this->priceIndexer->reindexList($productIds); diff --git a/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php b/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php new file mode 100644 index 000000000000..334d2b22edbf --- /dev/null +++ b/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php @@ -0,0 +1,159 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\CatalogInventory\Plugin; + +use Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute\Save; +use Magento\CatalogInventory\Api\Data\StockItemInterface; + +/** + * MassUpdate product attribute. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class MassUpdateProductAttribute +{ + /** + * @var \Magento\CatalogInventory\Model\Indexer\Stock\Processor + */ + private $stockIndexerProcessor; + + /** + * @var \Magento\Framework\Api\DataObjectHelper + */ + private $dataObjectHelper; + + /** + * @var \Magento\CatalogInventory\Api\StockRegistryInterface + */ + private $stockRegistry; + + /** + * @var \Magento\CatalogInventory\Api\StockItemRepositoryInterface + */ + private $stockItemRepository; + + /** + * @var \Magento\CatalogInventory\Api\StockConfigurationInterface + */ + private $stockConfiguration; + + /** + * @var \Magento\Catalog\Helper\Product\Edit\Action\Attribute + */ + private $attributeHelper; + + /** + * @var \Magento\Framework\Message\ManagerInterface + */ + private $messageManager; + + /** + * @param \Magento\CatalogInventory\Model\Indexer\Stock\Processor $stockIndexerProcessor + * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper + * @param \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry + * @param \Magento\CatalogInventory\Api\StockItemRepositoryInterface $stockItemRepository + * @param \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration + * @param \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeHelper + * @param \Magento\Framework\Message\ManagerInterface $messageManager + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + \Magento\CatalogInventory\Model\Indexer\Stock\Processor $stockIndexerProcessor, + \Magento\Framework\Api\DataObjectHelper $dataObjectHelper, + \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry, + \Magento\CatalogInventory\Api\StockItemRepositoryInterface $stockItemRepository, + \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration, + \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeHelper, + \Magento\Framework\Message\ManagerInterface $messageManager + ) { + $this->stockIndexerProcessor = $stockIndexerProcessor; + $this->dataObjectHelper = $dataObjectHelper; + $this->stockRegistry = $stockRegistry; + $this->stockItemRepository = $stockItemRepository; + $this->stockConfiguration = $stockConfiguration; + $this->attributeHelper = $attributeHelper; + $this->messageManager = $messageManager; + } + + /** + * Around execute plugin + * + * @param Save $subject + * @param callable $proceed + * + * @return \Magento\Framework\Controller\ResultInterface + */ + public function aroundExecute(Save $subject, callable $proceed) + { + try { + /** @var \Magento\Framework\App\RequestInterface $request */ + $request = $subject->getRequest(); + $inventoryData = $request->getParam('inventory', []); + $inventoryData = $this->addConfigSettings($inventoryData); + + $storeId = $this->attributeHelper->getSelectedStoreId(); + $websiteId = $this->attributeHelper->getStoreWebsiteId($storeId); + $productIds = $this->attributeHelper->getProductIds(); + + if (!empty($inventoryData)) { + $this->updateInventoryInProducts($productIds, $websiteId, $inventoryData); + } + + return $proceed(); + } catch (\Magento\Framework\Exception\LocalizedException $e) { + $this->messageManager->addErrorMessage($e->getMessage()); + return $proceed(); + } catch (\Exception $e) { + $this->messageManager->addExceptionMessage( + $e, + __('Something went wrong while updating the product(s) attributes.') + ); + return $proceed(); + } + } + + /** + * Add config settings + * + * @param array $inventoryData + * + * @return array + */ + private function addConfigSettings($inventoryData) + { + $options = $this->stockConfiguration->getConfigItemOptions(); + foreach ($options as $option) { + $useConfig = 'use_config_' . $option; + if (isset($inventoryData[$option]) && !isset($inventoryData[$useConfig])) { + $inventoryData[$useConfig] = 0; + } + } + return $inventoryData; + } + + /** + * Update inventory in products + * + * @param array $productIds + * @param int $websiteId + * @param array $inventoryData + * + * @return void + */ + private function updateInventoryInProducts($productIds, $websiteId, $inventoryData): void + { + foreach ($productIds as $productId) { + $stockItemDo = $this->stockRegistry->getStockItem($productId, $websiteId); + if (!$stockItemDo->getProductId()) { + $inventoryData['product_id'] = $productId; + } + $stockItemId = $stockItemDo->getId(); + $this->dataObjectHelper->populateWithArray($stockItemDo, $inventoryData, StockItemInterface::class); + $stockItemDo->setItemId($stockItemId); + $this->stockItemRepository->save($stockItemDo); + } + $this->stockIndexerProcessor->reindexList($productIds); + } +} diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/DisplayOutOfStockProductActionGroup.xml b/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/DisplayOutOfStockProductActionGroup.xml index c7c9126f4680..2850b8d06920 100644 --- a/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/DisplayOutOfStockProductActionGroup.xml +++ b/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/DisplayOutOfStockProductActionGroup.xml @@ -21,6 +21,7 @@ <actionGroup name="noDisplayOutOfStockProduct"> <amOnPage url="{{InventoryConfigurationPage.url}}" stepKey="navigateToInventoryConfigurationPage"/> <waitForPageLoad stepKey="waitForConfigPageToLoad"/> + <conditionalClick stepKey="expandProductStockOptions" selector="{{InventoryConfigSection.ProductStockOptionsTab}}" dependentSelector="{{InventoryConfigSection.CheckIfProductStockOptionsTabExpanded}}" visible="true" /> <uncheckOption selector="{{InventoryConfigSection.DisplayOutOfStockSystemValue}}" stepKey="uncheckUseSystemValue"/> <waitForElementVisible selector="{{InventoryConfigSection.DisplayOutOfStockDropdown}}" stepKey="waitForSwitcherDropdown" /> <selectOption selector="{{InventoryConfigSection.DisplayOutOfStockDropdown}}" userInput="No" stepKey="switchToNo" /> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml new file mode 100644 index 000000000000..706df79b1ef8 --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.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="AssociatedProductToConfigurableOutOfStockTest"> + <annotations> + <features value="CatalogInventory"/> + <stories value="Add/remove images and videos for all product types and category"/> + <title value="Out of stock associated products to configurable are not full page cache cleaned "/> + <description value="After last configurable product was ordered it becomes out of stock"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-94135"/> + <group value="CatalogInventory"/> + </annotations> + + <before> + <createData entity="SimpleSubCategory" stepKey="simplecategory"/> + + <!-- Create configurable product with two options --> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="simplecategory"/> + </createData> + + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create child product with single quantity --> + <createData entity="ApiSimpleSingleQty" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + + <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + + <!-- Create customer --> + <createData entity="Simple_US_Customer" stepKey="createSimpleUsCustomer"> + <field key="group_id">1</field> + </createData> + </before> + + <after> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="simplecategory" stepKey="deleteSimpleCategory"/> + <deleteData createDataKey="createSimpleUsCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + </after> + + <!-- Login as a customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="signUpNewUser"> + <argument name="Customer" value="$$createSimpleUsCustomer$$"/> + </actionGroup> + <amOnPage url="/{{ApiConfigurableProduct.urlKey}}2.html" stepKey="goToConfigProductPage"/> + <!-- Go to configurable product page --> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + + <!-- Order product with single quantity --> + <selectOption userInput="$$createConfigProductAttributeOption1.option[store_labels][1][label]$$" selector="{{StorefrontProductInfoMainSection.optionByAttributeId($$createConfigProductAttribute.attribute_id$$)}}" stepKey="configProductFillOption" /> + <click stepKey="addSimpleProductToCart" selector="{{StorefrontProductActionSection.addToCart}}"/> + <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" time="30" stepKey="waitForProductAdded"/> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToShoppingCartPage"/> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <!-- Checkout select Check/Money Order payment --> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> + <waitForPageLoad stepKey="waitForOrderSuccessPage1"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> + <actionGroup ref="StorefrontSignOutActionGroup" stepKey="StorefrontSignOutActionGroup"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask3"/> + <!-- Reset admin order filter --> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderFilters"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask5"/> + <fillField selector="{{AdminOrdersGridSection.search}}" userInput="{$grabOrderNumber}" stepKey="searchOrderNum"/> + <click selector="{{AdminOrdersGridSection.submitSearch}}" stepKey="submitSearch"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask4"/> + + <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> + <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoice"/> + <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeSuccessMessage"/> + <click selector="{{AdminOrderDetailsMainActionsSection.ship}}" stepKey="clickShip"/> + <waitForLoadingMaskToDisappear stepKey="waitForShipLoadingMask"/> + <click selector="{{AdminShipmentMainActionsSection.submitShipment}}" stepKey="submitShipment"/> + <waitForPageLoad stepKey="waitShipmentCreated"/> + <actionGroup ref="logout" stepKey="logout"/> + <magentoCLI stepKey="runCron" command="cron:run --group='index'"/> + + <!-- Wait till cron job runs for schedule updates --> + <wait time="60" stepKey="waitForUpdateStarts"/> + + <!-- Assert that product with single quantity is not available for order --> + <amOnPage url="/{{ApiConfigurableProduct.urlKey}}2.html" stepKey="goToConfigProductPage2"/> + <waitForPageLoad stepKey="waitForProductPageLoad2"/> + <dontSee userInput="$$createConfigProductAttributeOption1.option[store_labels][1][label]$$" selector="{{StorefrontProductInfoMainSection.optionByAttributeId($$createConfigProductAttribute.attribute_id$$)}}" stepKey="assertOptionNotAvailable" /> + </test> +</tests> diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/AddStockStatusToCollectionTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/AddStockStatusToCollectionTest.php index af35666ced3e..906df5473277 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/ConfigurationTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/ConfigurationTest.php index d2779b79b30d..cefc4ada7d21 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/ConfigurationTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/ConfigurationTest.php @@ -144,7 +144,7 @@ public function testGetEnableQtyIncrements() $store = 1; $this->scopeConfigMock->expects($this->once()) - ->method('getValue') + ->method('isSetFlag') ->with( Configuration::XML_PATH_ENABLE_QTY_INCREMENTS, \Magento\Store\Model\ScopeInterface::SCOPE_STORE, diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/CacheCleanerTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/CacheCleanerTest.php index 5e4249685f8d..755e54a919b6 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/CacheCleanerTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/CacheCleanerTest.php @@ -12,6 +12,7 @@ use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Select; use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Indexer\CacheContext; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Catalog\Model\Product; @@ -43,6 +44,11 @@ class CacheCleanerTest extends \PHPUnit\Framework\TestCase */ private $cacheContextMock; + /** + * @var MetadataPool |\PHPUnit_Framework_MockObject_MockObject + */ + private $metadataPoolMock; + /** * @var StockConfigurationInterface|\PHPUnit_Framework_MockObject_MockObject */ @@ -61,6 +67,8 @@ protected function setUp() ->setMethods(['getStockThresholdQty'])->getMockForAbstractClass(); $this->cacheContextMock = $this->getMockBuilder(CacheContext::class)->disableOriginalConstructor()->getMock(); $this->eventManagerMock = $this->getMockBuilder(ManagerInterface::class)->getMock(); + $this->metadataPoolMock = $this->getMockBuilder(MetadataPool::class) + ->setMethods(['getMetadata', 'getLinkField'])->disableOriginalConstructor()->getMock(); $this->selectMock = $this->getMockBuilder(Select::class)->disableOriginalConstructor()->getMock(); $this->resourceMock->expects($this->any()) @@ -73,7 +81,8 @@ protected function setUp() 'resource' => $this->resourceMock, 'stockConfiguration' => $this->stockConfigurationMock, 'cacheContext' => $this->cacheContextMock, - 'eventManager' => $this->eventManagerMock + 'eventManager' => $this->eventManagerMock, + 'metadataPool' => $this->metadataPoolMock ] ); } @@ -90,6 +99,7 @@ public function testClean($stockStatusBefore, $stockStatusAfter, $qtyAfter, $sto $productId = 123; $this->selectMock->expects($this->any())->method('from')->willReturnSelf(); $this->selectMock->expects($this->any())->method('where')->willReturnSelf(); + $this->selectMock->expects($this->any())->method('joinLeft')->willReturnSelf(); $this->connectionMock->expects($this->exactly(2))->method('select')->willReturn($this->selectMock); $this->connectionMock->expects($this->exactly(2))->method('fetchAll')->willReturnOnConsecutiveCalls( [ @@ -105,7 +115,10 @@ public function testClean($stockStatusBefore, $stockStatusAfter, $qtyAfter, $sto ->with(Product::CACHE_TAG, [$productId]); $this->eventManagerMock->expects($this->once())->method('dispatch') ->with('clean_cache_by_tags', ['object' => $this->cacheContextMock]); - + $this->metadataPoolMock->expects($this->exactly(2))->method('getMetadata') + ->willReturnSelf(); + $this->metadataPoolMock->expects($this->exactly(2))->method('getLinkField') + ->willReturn('row_id'); $callback = function () { }; $this->unit->clean([], $callback); @@ -136,6 +149,7 @@ public function testNotCleanCache($stockStatusBefore, $stockStatusAfter, $qtyAft $productId = 123; $this->selectMock->expects($this->any())->method('from')->willReturnSelf(); $this->selectMock->expects($this->any())->method('where')->willReturnSelf(); + $this->selectMock->expects($this->any())->method('joinLeft')->willReturnSelf(); $this->connectionMock->expects($this->exactly(2))->method('select')->willReturn($this->selectMock); $this->connectionMock->expects($this->exactly(2))->method('fetchAll')->willReturnOnConsecutiveCalls( [ @@ -149,6 +163,10 @@ public function testNotCleanCache($stockStatusBefore, $stockStatusAfter, $qtyAft ->willReturn($stockThresholdQty); $this->cacheContextMock->expects($this->never())->method('registerEntities'); $this->eventManagerMock->expects($this->never())->method('dispatch'); + $this->metadataPoolMock->expects($this->exactly(2))->method('getMetadata') + ->willReturnSelf(); + $this->metadataPoolMock->expects($this->exactly(2))->method('getLinkField') + ->willReturn('row_id'); $callback = function () { }; 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 287459bd8cbc..b64563a35176 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/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/OptionTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/OptionTest.php index eb32c30ab4f8..87233b4048b2 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/OptionTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/OptionTest.php @@ -151,7 +151,6 @@ public function testInitializeWhenResultIsDecimalGetBackordersMessageHasOptionQt $this->optionMock->expects($this->any())->method('getProduct')->will($this->returnValue($this->productMock)); $this->stockItemMock->expects($this->once())->method('setIsChildItem')->with(true); - $this->stockItemMock->expects($this->once())->method('setSuppressCheckQtyIncrements')->with(true); $this->stockItemMock->expects($this->once())->method('getItemId')->will($this->returnValue(true)); $this->stockRegistry @@ -222,7 +221,6 @@ public function testInitializeWhenResultNotDecimalGetBackordersMessageHasOptionQ $this->optionMock->expects($this->any())->method('getProduct')->will($this->returnValue($this->productMock)); $this->stockItemMock->expects($this->once())->method('setIsChildItem')->with(true); - $this->stockItemMock->expects($this->once())->method('setSuppressCheckQtyIncrements')->with(true); $this->stockItemMock->expects($this->once())->method('getItemId')->will($this->returnValue(true)); $this->stockRegistry diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/QuantityValidatorTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/QuantityValidatorTest.php index 7e2bad0b9635..633505a5609b 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/QuantityValidatorTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/QuantityValidatorTest.php @@ -278,11 +278,13 @@ public function testValidateWithOptions() { $optionMock = $this->getMockBuilder(OptionItem::class) ->disableOriginalConstructor() - ->setMethods(['setHasError', 'getStockStateResult']) + ->setMethods(['setHasError', 'getStockStateResult', 'getProduct']) ->getMock(); $optionMock->expects($this->once()) ->method('getStockStateResult') ->willReturn($this->resultMock); + $optionMock->method('getProduct') + ->willReturn($this->productMock); $this->stockRegistryMock->expects($this->at(0)) ->method('getStockItem') ->willReturn($this->stockItemMock); @@ -319,7 +321,7 @@ public function testValidateWithOptionsAndError() { $optionMock = $this->getMockBuilder(OptionItem::class) ->disableOriginalConstructor() - ->setMethods(['setHasError', 'getStockStateResult']) + ->setMethods(['setHasError', 'getStockStateResult', 'getProduct']) ->getMock(); $this->stockRegistryMock->expects($this->at(0)) ->method('getStockItem') @@ -330,6 +332,8 @@ public function testValidateWithOptionsAndError() $optionMock->expects($this->once()) ->method('getStockStateResult') ->willReturn($this->resultMock); + $optionMock->method('getProduct') + ->willReturn($this->productMock); $options = [$optionMock]; $this->createInitialStub(1); $this->setUpStubForQuantity(1, true); @@ -360,7 +364,7 @@ public function testValidateAndRemoveErrorsFromQuote() { $optionMock = $this->getMockBuilder(OptionItem::class) ->disableOriginalConstructor() - ->setMethods(['setHasError', 'getStockStateResult']) + ->setMethods(['setHasError', 'getStockStateResult', 'getProduct']) ->getMock(); $quoteItem = $this->getMockBuilder(Item::class) ->disableOriginalConstructor() @@ -369,6 +373,8 @@ public function testValidateAndRemoveErrorsFromQuote() $optionMock->expects($this->once()) ->method('getStockStateResult') ->willReturn($this->resultMock); + $optionMock->method('getProduct') + ->willReturn($this->productMock); $this->stockRegistryMock->expects($this->at(0)) ->method('getStockItem') ->willReturn($this->stockItemMock); 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 000000000000..d66a783c6720 --- /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 8b55b6f32798..eb6239ea87ef 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": "*", @@ -27,5 +28,6 @@ "psr-4": { "Magento\\CatalogInventory\\": "" } - } + }, + "abandoned": "magento/inventory-composer-metapackage" } diff --git a/app/code/Magento/CatalogInventory/etc/adminhtml/di.xml b/app/code/Magento/CatalogInventory/etc/adminhtml/di.xml index 3397ef25918c..28035de29bc2 100644 --- a/app/code/Magento/CatalogInventory/etc/adminhtml/di.xml +++ b/app/code/Magento/CatalogInventory/etc/adminhtml/di.xml @@ -23,6 +23,7 @@ <arguments> <argument name="addFieldStrategies" xsi:type="array"> <item name="qty" xsi:type="object">Magento\CatalogInventory\Ui\DataProvider\Product\AddQuantityFieldToCollection</item> + <item name="quantity_and_stock_status" xsi:type="object">Magento\CatalogInventory\Ui\DataProvider\Product\AddQuantityAndStockStatusFieldToCollection</item> </argument> <argument name="addFilterStrategies" xsi:type="array"> <item name="qty" xsi:type="object">Magento\CatalogInventory\Ui\DataProvider\Product\AddQuantityFilterToCollection</item> diff --git a/app/code/Magento/CatalogInventory/etc/db_schema.xml b/app/code/Magento/CatalogInventory/etc/db_schema.xml index 8a6ae8d2d93c..5ac7fedc5aa1 100644 --- a/app/code/Magento/CatalogInventory/etc/db_schema.xml +++ b/app/code/Magento/CatalogInventory/etc/db_schema.xml @@ -13,10 +13,10 @@ <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Website Id"/> <column xsi:type="varchar" name="stock_name" nullable="true" length="255" comment="Stock Name"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="stock_id"/> </constraint> - <index name="CATALOGINVENTORY_STOCK_WEBSITE_ID" indexType="btree"> + <index referenceId="CATALOGINVENTORY_STOCK_WEBSITE_ID" indexType="btree"> <column name="website_id"/> </index> </table> @@ -71,23 +71,23 @@ identity="false" default="0" comment="Is Divided into Multiple Boxes for Shipping"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Website ID"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="item_id"/> </constraint> - <constraint xsi:type="foreign" name="CATINV_STOCK_ITEM_PRD_ID_CAT_PRD_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="CATINV_STOCK_ITEM_PRD_ID_CAT_PRD_ENTT_ENTT_ID" table="cataloginventory_stock_item" column="product_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CATINV_STOCK_ITEM_STOCK_ID_CATINV_STOCK_STOCK_ID" + <constraint xsi:type="foreign" referenceId="CATINV_STOCK_ITEM_STOCK_ID_CATINV_STOCK_STOCK_ID" table="cataloginventory_stock_item" column="stock_id" referenceTable="cataloginventory_stock" referenceColumn="stock_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CATALOGINVENTORY_STOCK_ITEM_PRODUCT_ID_STOCK_ID"> + <constraint xsi:type="unique" referenceId="CATALOGINVENTORY_STOCK_ITEM_PRODUCT_ID_STOCK_ID"> <column name="product_id"/> <column name="stock_id"/> </constraint> - <index name="CATALOGINVENTORY_STOCK_ITEM_WEBSITE_ID" indexType="btree"> + <index referenceId="CATALOGINVENTORY_STOCK_ITEM_WEBSITE_ID" indexType="btree"> <column name="website_id"/> </index> - <index name="CATALOGINVENTORY_STOCK_ITEM_STOCK_ID" indexType="btree"> + <index referenceId="CATALOGINVENTORY_STOCK_ITEM_STOCK_ID" indexType="btree"> <column name="stock_id"/> </index> </table> @@ -103,18 +103,18 @@ comment="Qty"/> <column xsi:type="smallint" name="stock_status" padding="5" unsigned="true" nullable="false" identity="false" comment="Stock Status"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="product_id"/> <column name="website_id"/> <column name="stock_id"/> </constraint> - <index name="CATALOGINVENTORY_STOCK_STATUS_STOCK_ID" indexType="btree"> + <index referenceId="CATALOGINVENTORY_STOCK_STATUS_STOCK_ID" indexType="btree"> <column name="stock_id"/> </index> - <index name="CATALOGINVENTORY_STOCK_STATUS_WEBSITE_ID" indexType="btree"> + <index referenceId="CATALOGINVENTORY_STOCK_STATUS_WEBSITE_ID" indexType="btree"> <column name="website_id"/> </index> - <index name="CATALOGINVENTORY_STOCK_STATUS_STOCK_STATUS" indexType="btree"> + <index referenceId="CATALOGINVENTORY_STOCK_STATUS_STOCK_STATUS" indexType="btree"> <column name="stock_status"/> </index> </table> @@ -130,15 +130,15 @@ comment="Qty"/> <column xsi:type="smallint" name="stock_status" padding="5" unsigned="true" nullable="false" identity="false" comment="Stock Status"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="product_id"/> <column name="website_id"/> <column name="stock_id"/> </constraint> - <index name="CATALOGINVENTORY_STOCK_STATUS_IDX_STOCK_ID" indexType="btree"> + <index referenceId="CATALOGINVENTORY_STOCK_STATUS_IDX_STOCK_ID" indexType="btree"> <column name="stock_id"/> </index> - <index name="CATALOGINVENTORY_STOCK_STATUS_IDX_WEBSITE_ID" indexType="btree"> + <index referenceId="CATALOGINVENTORY_STOCK_STATUS_IDX_WEBSITE_ID" indexType="btree"> <column name="website_id"/> </index> </table> @@ -154,15 +154,15 @@ comment="Qty"/> <column xsi:type="smallint" name="stock_status" padding="5" unsigned="true" nullable="false" identity="false" comment="Stock Status"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="product_id"/> <column name="website_id"/> <column name="stock_id"/> </constraint> - <index name="CATALOGINVENTORY_STOCK_STATUS_TMP_STOCK_ID" indexType="hash"> + <index referenceId="CATALOGINVENTORY_STOCK_STATUS_TMP_STOCK_ID" indexType="hash"> <column name="stock_id"/> </index> - <index name="CATALOGINVENTORY_STOCK_STATUS_TMP_WEBSITE_ID" indexType="hash"> + <index referenceId="CATALOGINVENTORY_STOCK_STATUS_TMP_WEBSITE_ID" indexType="hash"> <column name="website_id"/> </index> </table> @@ -178,18 +178,18 @@ comment="Qty"/> <column xsi:type="smallint" name="stock_status" padding="5" unsigned="true" nullable="false" identity="false" comment="Stock Status"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="product_id"/> <column name="website_id"/> <column name="stock_id"/> </constraint> - <index name="CATALOGINVENTORY_STOCK_STATUS_STOCK_ID" indexType="btree"> + <index referenceId="CATALOGINVENTORY_STOCK_STATUS_STOCK_ID" indexType="btree"> <column name="stock_id"/> </index> - <index name="CATALOGINVENTORY_STOCK_STATUS_WEBSITE_ID" indexType="btree"> + <index referenceId="CATALOGINVENTORY_STOCK_STATUS_WEBSITE_ID" indexType="btree"> <column name="website_id"/> </index> - <index name="CATALOGINVENTORY_STOCK_STATUS_STOCK_STATUS" indexType="btree"> + <index referenceId="CATALOGINVENTORY_STOCK_STATUS_STOCK_STATUS" indexType="btree"> <column name="stock_status"/> </index> </table> diff --git a/app/code/Magento/CatalogInventory/etc/di.xml b/app/code/Magento/CatalogInventory/etc/di.xml index ace72bb11c37..e7d79c593b8c 100644 --- a/app/code/Magento/CatalogInventory/etc/di.xml +++ b/app/code/Magento/CatalogInventory/etc/di.xml @@ -44,7 +44,7 @@ </type> <type name="Magento\CatalogInventory\Observer\UpdateItemsStockUponConfigChangeObserver"> <arguments> - <argument name="resourceStock" xsi:type="object">Magento\CatalogInventory\Model\ResourceModel\Stock\Proxy</argument> + <argument name="resourceStockItem" xsi:type="object">Magento\CatalogInventory\Model\ResourceModel\Stock\Item\Proxy</argument> </arguments> </type> <type name="Magento\Catalog\Model\Layer"> @@ -111,7 +111,7 @@ <argument name="batchSizeManagement" xsi:type="object">Magento\CatalogInventory\Model\Indexer\Stock\BatchSizeManagement</argument> </arguments> </type> - <type name="\Magento\Framework\Data\CollectionModifier"> + <type name="Magento\Framework\Data\CollectionModifier"> <arguments> <argument name="conditions" xsi:type="array"> <item name="stockStatusCondition" xsi:type="object">Magento\CatalogInventory\Model\ProductCollectionStockCondition</item> @@ -135,4 +135,7 @@ <type name="Magento\CatalogInventory\Model\ResourceModel\Stock\Item"> <plugin name="priceIndexUpdater" type="Magento\CatalogInventory\Model\Plugin\PriceIndexUpdater" /> </type> + <type name="Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute\Save"> + <plugin name="massAction" type="Magento\CatalogInventory\Plugin\MassUpdateProductAttribute" /> + </type> </config> diff --git a/app/code/Magento/CatalogInventory/view/adminhtml/ui_component/product_form.xml b/app/code/Magento/CatalogInventory/view/adminhtml/ui_component/product_form.xml index f8571b6e332b..0a7f0fdc32d4 100644 --- a/app/code/Magento/CatalogInventory/view/adminhtml/ui_component/product_form.xml +++ b/app/code/Magento/CatalogInventory/view/adminhtml/ui_component/product_form.xml @@ -288,7 +288,7 @@ <settings> <scopeLabel>[GLOBAL]</scopeLabel> <validation> - <rule name="validate-number" xsi:type="boolean">true</rule> + <rule name="validate-greater-than-zero" xsi:type="boolean">true</rule> </validation> <label translate="true">Maximum Qty Allowed in Shopping Cart</label> <dataScope>max_sale_qty</dataScope> diff --git a/app/code/Magento/CatalogInventoryGraphQl/Model/Resolver/OnlyXLeftInStockResolver.php b/app/code/Magento/CatalogInventoryGraphQl/Model/Resolver/OnlyXLeftInStockResolver.php new file mode 100644 index 000000000000..9e10f0b44850 --- /dev/null +++ b/app/code/Magento/CatalogInventoryGraphQl/Model/Resolver/OnlyXLeftInStockResolver.php @@ -0,0 +1,100 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogInventoryGraphQl\Model\Resolver; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\CatalogInventory\Api\StockRegistryInterface; +use Magento\CatalogInventory\Model\Configuration; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Store\Model\ScopeInterface; + +/** + * @inheritdoc + */ +class OnlyXLeftInStockResolver implements ResolverInterface +{ + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var StockRegistryInterface + */ + private $stockRegistry; + + /** + * @param ScopeConfigInterface $scopeConfig + * @param StockRegistryInterface $stockRegistry + */ + public function __construct( + ScopeConfigInterface $scopeConfig, + StockRegistryInterface $stockRegistry + ) { + $this->scopeConfig = $scopeConfig; + $this->stockRegistry = $stockRegistry; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!array_key_exists('model', $value) || !$value['model'] instanceof ProductInterface) { + throw new LocalizedException(__('"model" value should be specified')); + } + + /* @var $product ProductInterface */ + $product = $value['model']; + $onlyXLeftQty = $this->getOnlyXLeftQty($product); + + return $onlyXLeftQty; + } + + /** + * Get product qty left when "Catalog > Inventory > Stock Options > Only X left Threshold" is greater than 0 + * + * @param ProductInterface $product + * + * @return null|float + */ + private function getOnlyXLeftQty(ProductInterface $product): ?float + { + $thresholdQty = (float)$this->scopeConfig->getValue( + Configuration::XML_PATH_STOCK_THRESHOLD_QTY, + ScopeInterface::SCOPE_STORE + ); + if ($thresholdQty === 0) { + return null; + } + + $stockItem = $this->stockRegistry->getStockItem($product->getId()); + + $stockCurrentQty = $this->stockRegistry->getStockStatus( + $product->getId(), + $product->getStore()->getWebsiteId() + )->getQty(); + + $stockLeft = $stockCurrentQty - $stockItem->getMinQty(); + + $thresholdQty = (float)$this->scopeConfig->getValue( + Configuration::XML_PATH_STOCK_THRESHOLD_QTY, + ScopeInterface::SCOPE_STORE + ); + + if ($stockCurrentQty > 0 && $stockLeft <= $thresholdQty) { + return (float)$stockLeft; + } + + return null; + } +} diff --git a/app/code/Magento/CatalogInventoryGraphQl/Model/Resolver/StockStatusProvider.php b/app/code/Magento/CatalogInventoryGraphQl/Model/Resolver/StockStatusProvider.php new file mode 100644 index 000000000000..354e053efa90 --- /dev/null +++ b/app/code/Magento/CatalogInventoryGraphQl/Model/Resolver/StockStatusProvider.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogInventoryGraphQl\Model\Resolver; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\CatalogInventory\Api\Data\StockStatusInterface; +use Magento\CatalogInventory\Api\StockStatusRepositoryInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; + +/** + * @inheritdoc + */ +class StockStatusProvider implements ResolverInterface +{ + /** + * @var StockStatusRepositoryInterface + */ + private $stockStatusRepository; + + /** + * @param StockStatusRepositoryInterface $stockStatusRepository + */ + public function __construct(StockStatusRepositoryInterface $stockStatusRepository) + { + $this->stockStatusRepository = $stockStatusRepository; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!array_key_exists('model', $value) || !$value['model'] instanceof ProductInterface) { + throw new LocalizedException(__('"model" value should be specified')); + } + + /* @var $product ProductInterface */ + $product = $value['model']; + + $stockStatus = $this->stockStatusRepository->get($product->getId()); + $productStockStatus = (int)$stockStatus->getStockStatus(); + + return $productStockStatus === StockStatusInterface::STATUS_IN_STOCK ? 'IN_STOCK' : 'OUT_OF_STOCK'; + } +} diff --git a/app/code/Magento/CatalogInventoryGraphQl/README.md b/app/code/Magento/CatalogInventoryGraphQl/README.md new file mode 100644 index 000000000000..ef4302e8da36 --- /dev/null +++ b/app/code/Magento/CatalogInventoryGraphQl/README.md @@ -0,0 +1,4 @@ +# CatalogInventoryGraphQl + +**CatalogInventoryGraphQl** provides type information for the GraphQl module +to generate inventory stock fields for product information endpoints. diff --git a/app/code/Magento/CatalogInventoryGraphQl/composer.json b/app/code/Magento/CatalogInventoryGraphQl/composer.json new file mode 100644 index 000000000000..5e85c3ae12f9 --- /dev/null +++ b/app/code/Magento/CatalogInventoryGraphQl/composer.json @@ -0,0 +1,24 @@ +{ + "name": "magento/module-catalog-inventory-graph-ql", + "description": "N/A", + "type": "magento2-module", + "require": { + "php": "~7.1.3||~7.2.0", + "magento/framework": "*", + "magento/module-store": "*", + "magento/module-catalog": "*", + "magento/module-catalog-inventory": "*" + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\CatalogInventoryGraphQl\\": "" + } + } +} diff --git a/app/code/Magento/CatalogInventoryGraphQl/etc/module.xml b/app/code/Magento/CatalogInventoryGraphQl/etc/module.xml new file mode 100644 index 000000000000..776e4e165333 --- /dev/null +++ b/app/code/Magento/CatalogInventoryGraphQl/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_CatalogInventoryGraphQl"> + <sequence> + <module name="Magento_Store"/> + <module name="Magento_Catalog"/> + <module name="Magento_CatalogInventory"/> + </sequence> + </module> +</config> diff --git a/app/code/Magento/CatalogInventoryGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogInventoryGraphQl/etc/schema.graphqls new file mode 100644 index 000000000000..6a6c7e75dbd9 --- /dev/null +++ b/app/code/Magento/CatalogInventoryGraphQl/etc/schema.graphqls @@ -0,0 +1,12 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +interface ProductInterface { + only_x_left_in_stock: Float @doc(description: "Product stock only x left count") @resolver(class: "Magento\\CatalogInventoryGraphQl\\Model\\Resolver\\OnlyXLeftInStockResolver") + stock_status: ProductStockStatus @doc(description: "Stock status of the product") @resolver(class: "Magento\\CatalogInventoryGraphQl\\Model\\Resolver\\StockStatusProvider") +} + +enum ProductStockStatus @doc(description: "This enumeration states whether a product stock status is in stock or out of stock") { + IN_STOCK + OUT_OF_STOCK +} diff --git a/app/code/Magento/CatalogInventoryGraphQl/registration.php b/app/code/Magento/CatalogInventoryGraphQl/registration.php new file mode 100644 index 000000000000..6ef9d3b1f90b --- /dev/null +++ b/app/code/Magento/CatalogInventoryGraphQl/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_CatalogInventoryGraphQl', __DIR__); diff --git a/app/code/Magento/CatalogRule/Block/Adminhtml/Edit/DeleteButton.php b/app/code/Magento/CatalogRule/Block/Adminhtml/Edit/DeleteButton.php index 6390822b58f4..184cb6419294 100644 --- a/app/code/Magento/CatalogRule/Block/Adminhtml/Edit/DeleteButton.php +++ b/app/code/Magento/CatalogRule/Block/Adminhtml/Edit/DeleteButton.php @@ -25,7 +25,7 @@ public function getButtonData() 'class' => 'delete', 'on_click' => 'deleteConfirm(\'' . __( 'Are you sure you want to do this?' - ) . '\', \'' . $this->urlBuilder->getUrl('*/*/delete', ['id' => $ruleId]) . '\')', + ) . '\', \'' . $this->urlBuilder->getUrl('*/*/delete', ['id' => $ruleId]) . '\', {data: {}})', 'sort_order' => 20, ]; } diff --git a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Delete.php b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Delete.php index 3500506d8d6c..998d45b839c7 100644 --- a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Delete.php +++ b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Delete.php @@ -6,9 +6,10 @@ */ namespace Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\Exception\LocalizedException; -class Delete extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog +class Delete extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog implements HttpPostActionInterface { /** * @return void diff --git a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Edit.php b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Edit.php index 945c28b2088f..2c2abcef8b25 100644 --- a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Edit.php +++ b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Edit.php @@ -6,7 +6,9 @@ */ namespace Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog; -class Edit extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Edit extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog implements HttpGetActionInterface { /** * @return void diff --git a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Index.php b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Index.php index 8c6a38819512..a9dcd1f6383a 100644 --- a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Index.php +++ b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Index.php @@ -6,7 +6,9 @@ */ namespace Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog; -class Index extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog implements HttpGetActionInterface { /** * @return void diff --git a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/NewAction.php b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/NewAction.php index c677c75c5953..d86c56402d25 100644 --- a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/NewAction.php +++ b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/NewAction.php @@ -6,7 +6,9 @@ */ namespace Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog; -class NewAction extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class NewAction extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog implements HttpGetActionInterface { /** * @return void diff --git a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/NewConditionHtml.php b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/NewConditionHtml.php index b790d90d3804..a845c104f943 100644 --- a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/NewConditionHtml.php +++ b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/NewConditionHtml.php @@ -6,9 +6,12 @@ */ namespace Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Rule\Model\Condition\AbstractCondition; +use Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog as CatalogAction; -class NewConditionHtml extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog +class NewConditionHtml extends CatalogAction implements HttpPostActionInterface, HttpGetActionInterface { /** * @return void diff --git a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Save.php b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Save.php index f3046c58a389..0ff12faf54cb 100644 --- a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Save.php +++ b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Save.php @@ -6,6 +6,7 @@ */ namespace Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Backend\App\Action\Context; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Registry; @@ -13,9 +14,11 @@ use Magento\Framework\App\Request\DataPersistorInterface; /** + * Save action for catalog rule + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Save extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog +class Save extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog implements HttpPostActionInterface { /** * @var DataPersistorInterface @@ -39,7 +42,9 @@ public function __construct( } /** - * @return void + * Execute save action from catalog rule + * + * @return \Magento\Framework\App\ResponseInterface|\Magento\Framework\Controller\ResultInterface|void * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function execute() @@ -60,6 +65,17 @@ public function execute() ['request' => $this->getRequest()] ); $data = $this->getRequest()->getPostValue(); + + $filterValues = ['from_date' => $this->_dateFilter]; + if ($this->getRequest()->getParam('to_date')) { + $filterValues['to_date'] = $this->_dateFilter; + } + $inputFilter = new \Zend_Filter_Input( + $filterValues, + [], + $data + ); + $data = $inputFilter->getUnescaped(); $id = $this->getRequest()->getParam('rule_id'); if ($id) { $model = $ruleRepository->get($id); diff --git a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Widget/CategoriesJson.php b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Widget/CategoriesJson.php index d049d74bd260..18a58c0f8542 100644 --- a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Widget/CategoriesJson.php +++ b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Widget/CategoriesJson.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -9,8 +8,12 @@ use Magento\Backend\App\Action\Context; use Magento\Catalog\Model\Category; use Magento\Framework\Registry; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; -class CategoriesJson extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Widget +/** + * Categories json widget for catalog rule + */ +class CategoriesJson extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Widget implements HttpPostActionInterface { /** * Core registry @@ -77,10 +80,11 @@ public function execute() if (!($category = $this->_initCategory())) { return; } + $selected = $this->getRequest()->getPost('selected', ''); $block = $this->_view->getLayout()->createBlock( \Magento\Catalog\Block\Adminhtml\Category\Checkboxes\Tree::class )->setCategoryIds( - [$categoryId] + explode(',', $selected) ); $this->getResponse()->representJson( $block->getTreeJson($category) diff --git a/app/code/Magento/CatalogRule/Model/Indexer/AbstractIndexer.php b/app/code/Magento/CatalogRule/Model/Indexer/AbstractIndexer.php index 5d93e6f21686..6b7c12dfdf46 100644 --- a/app/code/Magento/CatalogRule/Model/Indexer/AbstractIndexer.php +++ b/app/code/Magento/CatalogRule/Model/Indexer/AbstractIndexer.php @@ -10,6 +10,9 @@ use Magento\Framework\DataObject\IdentityInterface; use Magento\Framework\Indexer\CacheContext; +/** + * Abstract class for CatalogRule indexers. + */ abstract class AbstractIndexer implements IndexerActionInterface, MviewActionInterface, IdentityInterface { /** @@ -66,7 +69,6 @@ public function executeFull() { $this->indexBuilder->reindexFull(); $this->_eventManager->dispatch('clean_cache_by_tags', ['object' => $this]); - //TODO: remove after fix fpc. MAGETWO-50668 $this->getCacheManager()->clean($this->getIdentities()); } @@ -137,8 +139,9 @@ public function executeRow($id) abstract protected function doExecuteRow($id); /** - * @return \Magento\Framework\App\CacheInterface|mixed + * Get cache manager * + * @return \Magento\Framework\App\CacheInterface|mixed * @deprecated 100.0.7 */ private function getCacheManager() diff --git a/app/code/Magento/CatalogRule/Model/Rule/Condition/ConditionsToSearchCriteriaMapper.php b/app/code/Magento/CatalogRule/Model/Rule/Condition/ConditionsToSearchCriteriaMapper.php index 6d343fe149d2..fabe504fbe31 100644 --- a/app/code/Magento/CatalogRule/Model/Rule/Condition/ConditionsToSearchCriteriaMapper.php +++ b/app/code/Magento/CatalogRule/Model/Rule/Condition/ConditionsToSearchCriteriaMapper.php @@ -71,6 +71,8 @@ public function mapConditionsToSearchCriteria(CombinedCondition $conditions): Se } /** + * Convert condition to filter group + * * @param ConditionInterface $condition * @return null|\Magento\Framework\Api\CombinedFilterGroup|\Magento\Framework\Api\Filter * @throws InputException @@ -89,6 +91,8 @@ private function mapConditionToFilterGroup(ConditionInterface $condition) } /** + * Convert combined condition to filter group + * * @param Combine $combinedCondition * @return null|\Magento\Framework\Api\CombinedFilterGroup * @throws InputException @@ -121,6 +125,8 @@ private function mapCombinedConditionToFilterGroup(CombinedCondition $combinedCo } /** + * Convert simple condition to filter group + * * @param ConditionInterface $productCondition * @return FilterGroup|Filter * @throws InputException @@ -139,6 +145,8 @@ private function mapSimpleConditionToFilterGroup(ConditionInterface $productCond } /** + * Convert simple condition with array value to filter group + * * @param ConditionInterface $productCondition * @return FilterGroup * @throws InputException @@ -161,6 +169,8 @@ private function processSimpleConditionWithArrayValue(ConditionInterface $produc } /** + * Get glue for multiple values by operator + * * @param string $operator * @return string */ @@ -211,6 +221,8 @@ private function reverseSqlOperatorInFilter(Filter $filter) } /** + * Convert filters array into combined filter group + * * @param array $filters * @param string $combinationMode * @return FilterGroup @@ -227,6 +239,8 @@ private function createCombinedFilterGroup(array $filters, string $combinationMo } /** + * Creating of filter object by filtering params + * * @param string $field * @param string $value * @param string $conditionType @@ -264,6 +278,7 @@ private function mapRuleOperatorToSQLCondition(string $ruleOperator): string '!{}' => 'nlike', // does not contains '()' => 'in', // is one of '!()' => 'nin', // is not one of + '<=>' => 'is_null' ]; if (!array_key_exists($ruleOperator, $operatorsMap)) { diff --git a/app/code/Magento/CatalogRule/Model/Rule/Condition/Product.php b/app/code/Magento/CatalogRule/Model/Rule/Condition/Product.php index ab650c94a0f0..0db178b2a0a6 100644 --- a/app/code/Magento/CatalogRule/Model/Rule/Condition/Product.php +++ b/app/code/Magento/CatalogRule/Model/Rule/Condition/Product.php @@ -4,12 +4,11 @@ * See COPYING.txt for license details. */ -/** - * Catalog Rule Product Condition data model - */ namespace Magento\CatalogRule\Model\Rule\Condition; /** + * Catalog Rule Product Condition data model + * * @method string getAttribute() Returns attribute code */ class Product extends \Magento\Rule\Model\Condition\Product\AbstractProduct @@ -29,6 +28,9 @@ public function validate(\Magento\Framework\Model\AbstractModel $model) $oldAttrValue = $model->getData($attrCode); if ($oldAttrValue === null) { + if ($this->getOperator() === '<=>') { + return true; + } return false; } diff --git a/app/code/Magento/CatalogRule/Observer/RulePricesStorage.php b/app/code/Magento/CatalogRule/Observer/RulePricesStorage.php index 8a3a82170762..321780eca563 100644 --- a/app/code/Magento/CatalogRule/Observer/RulePricesStorage.php +++ b/app/code/Magento/CatalogRule/Observer/RulePricesStorage.php @@ -22,7 +22,7 @@ class RulePricesStorage */ public function getRulePrice($id) { - return isset($this->rulePrices[$id]) ? $this->rulePrices[$id] : false; + return $this->rulePrices[$id] ?? false; } /** diff --git a/app/code/Magento/CatalogRule/Pricing/Price/CatalogRulePrice.php b/app/code/Magento/CatalogRule/Pricing/Price/CatalogRulePrice.php index a45a548264a4..c71b51317fd5 100644 --- a/app/code/Magento/CatalogRule/Pricing/Price/CatalogRulePrice.php +++ b/app/code/Magento/CatalogRule/Pricing/Price/CatalogRulePrice.php @@ -89,7 +89,7 @@ public function getValue() { if (null === $this->value) { if ($this->product->hasData(self::PRICE_CODE)) { - $this->value = floatval($this->product->getData(self::PRICE_CODE)) ?: false; + $this->value = (float)$this->product->getData(self::PRICE_CODE) ?: false; } else { $this->value = $this->getRuleResource() ->getRulePrice( @@ -98,7 +98,7 @@ public function getValue() $this->customerSession->getCustomerGroupId(), $this->product->getId() ); - $this->value = $this->value ? floatval($this->value) : false; + $this->value = $this->value ? (float)$this->value : false; } if ($this->value) { $this->value = $this->priceCurrency->convertAndRound($this->value); diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminOpenNewCatalogPriceRuleFormPageActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminOpenNewCatalogPriceRuleFormPageActionGroup.xml new file mode 100644 index 000000000000..072e8b24b033 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminOpenNewCatalogPriceRuleFormPageActionGroup.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenNewCatalogPriceRuleFormPageActionGroup"> + <amOnPage url="{{CatalogRuleNewPage.url}}" stepKey="openNewCatalogPriceRulePage" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AssertCustomerGroupNotOnCatalogPriceRuleFormActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AssertCustomerGroupNotOnCatalogPriceRuleFormActionGroup.xml new file mode 100644 index 000000000000..93a2a8a61095 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AssertCustomerGroupNotOnCatalogPriceRuleFormActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertCustomerGroupNotOnCatalogPriceRuleFormActionGroup"> + <arguments> + <argument name="customerGroup" type="entity" /> + </arguments> + <grabMultiple selector="{{AdminNewCatalogPriceRule.customerGroupsOptions}}" stepKey="customerGroups" /> + <assertNotContains stepKey="assertCustomerGroupNotInOptions"> + <actualResult type="variable">customerGroups</actualResult> + <expectedResult type="string">{{customerGroup.code}}</expectedResult> + </assertNotContains> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml index bfc059ccb247..b0c4f2d8a609 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml @@ -17,7 +17,6 @@ <amOnPage stepKey="goToPriceRulePage" url="{{CatalogRulePage.url}}"/> <waitForPageLoad stepKey="waitForPriceRulePage"/> <click stepKey="addNewRule" selector="{{AdminGridMainControls.add}}"/> - <waitForPageLoad stepKey="waitForIndividualRulePage"/> <!-- Fill the form according the attributes of the entity --> <fillField stepKey="fillName" selector="{{AdminNewCatalogPriceRule.ruleName}}" userInput="{{catalogRule.name}}"/> @@ -37,6 +36,41 @@ <waitForPageLoad stepKey="waitForApplied"/> </actionGroup> + + <actionGroup name="createCatalogPriceRule"> + <arguments> + <argument name="catalogRule" defaultValue="_defaultCatalogRule"/> + </arguments> + + <click stepKey="addNewRule" selector="{{AdminGridMainControls.add}}"/> + <fillField selector="{{AdminNewCatalogPriceRule.ruleName}}" userInput="{{catalogRule.name}}" stepKey="fillName" /> + <fillField selector="{{AdminNewCatalogPriceRule.description}}" userInput="{{catalogRule.description}}" stepKey="fillDescription" /> + <selectOption selector="{{AdminNewCatalogPriceRule.websites}}" parameterArray="{{catalogRule.website_ids}}" stepKey="selectSite" /> + <click stepKey="openActionDropdown" selector="{{AdminNewCatalogPriceRule.actionsTab}}"/> + <fillField stepKey="fillDiscountValue" selector="{{AdminNewCatalogPriceRuleActions.discountAmount}}" userInput="{{catalogRule.discount_amount}}"/> + + <scrollToTopOfPage stepKey="scrollToTop"/> + <waitForPageLoad stepKey="waitForApplied"/> + </actionGroup> + + <actionGroup name="CreateCatalogPriceRuleConditionWithAttribute"> + <arguments> + <argument name="attributeName" type="string"/> + <argument name="targetValue" type="string"/> + <argument name="targetSelectValue" type="string"/> + </arguments> + + <click selector="{{AdminNewCatalogPriceRule.conditionsTab}}" stepKey="openConditionsTab"/> + <waitForPageLoad stepKey="waitForConditionTabOpened"/> + <click selector="{{AdminNewCatalogPriceRuleConditions.newCondition}}" stepKey="addNewCondition"/> + <selectOption selector="{{AdminNewCatalogPriceRuleConditions.conditionSelect('1')}}" userInput="{{attributeName}}" stepKey="selectTypeCondition"/> + <waitForElement selector="{{AdminNewCatalogPriceRuleConditions.targetEllipsisValue('1', targetValue)}}" stepKey="waitForIsTarget"/> + <click selector="{{AdminNewCatalogPriceRuleConditions.targetEllipsisValue('1', 'is')}}" stepKey="clickOnIs"/> + <selectOption selector="{{AdminNewCatalogPriceRuleConditions.targetSelect('1')}}" userInput="{{targetSelectValue}}" stepKey="selectTargetCondition"/> + <click selector="{{AdminNewCatalogPriceRule.fromDateButton}}" stepKey="clickFromCalender"/> + <click selector="{{AdminNewCatalogPriceRule.todayDate}}" stepKey="clickFromToday"/> + </actionGroup> + <!-- Apply all of the saved catalog price rules --> <actionGroup name="applyCatalogPriceRules"> <amOnPage stepKey="goToPriceRulePage" url="{{CatalogRulePage.url}}"/> @@ -44,4 +78,42 @@ <click stepKey="applyRules" selector="{{AdminCatalogPriceRuleGrid.applyRules}}"/> <see stepKey="assertSuccess" selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="Updated rules applied."/> </actionGroup> + + <!--Add Catalog Rule Condition With product SKU--> + <actionGroup name="newCatalogPriceRuleByUIWithConditionIsSKU" extends="newCatalogPriceRuleByUI"> + <arguments> + <argument name="productSku"/> + </arguments> + <click selector="{{AdminNewCatalogPriceRule.conditionsTab}}" after="discardSubsequentRules" stepKey="openConditionsTab"/> + <waitForPageLoad after="openConditionsTab" stepKey="waitForConditionTabOpened"/> + <click selector="{{AdminNewCatalogPriceRuleConditions.newCondition}}" after="waitForConditionTabOpened" stepKey="addNewCondition"/> + <selectOption selector="{{AdminNewCatalogPriceRuleConditions.conditionSelect('1')}}" userInput="Magento\CatalogRule\Model\Rule\Condition\Product|sku" after="addNewCondition" stepKey="selectTypeCondition"/> + <waitForPageLoad after="selectTypeCondition" stepKey="waitForConditionChosed"/> + <click selector="{{AdminNewCatalogPriceRuleConditions.targetEllipsis('1')}}" after="waitForConditionChosed" stepKey="clickEllipsis"/> + <fillField selector="{{AdminNewCatalogPriceRuleConditions.targetInput('1', '1')}}" userInput="{{productSku}}" after="clickEllipsis" stepKey="fillProductSku"/> + <click selector="{{AdminNewCatalogPriceRuleConditions.applyButton('1', '1')}}" after="fillProductSku" stepKey="clickApply"/> + </actionGroup> + + <!--Add Catalog Rule Condition With Category--> + <actionGroup name="newCatalogPriceRuleByUIWithConditionIsCategory" extends="newCatalogPriceRuleByUI"> + <arguments> + <argument name="categoryId"/> + </arguments> + <click selector="{{AdminNewCatalogPriceRule.conditionsTab}}" after="discardSubsequentRules" stepKey="openConditionsTab"/> + <waitForPageLoad after="openConditionsTab" stepKey="waitForConditionTabOpened"/> + <click selector="{{AdminNewCatalogPriceRuleConditions.newCondition}}" after="waitForConditionTabOpened" stepKey="addNewCondition"/> + <selectOption selector="{{AdminNewCatalogPriceRuleConditions.conditionSelect('1')}}" userInput="Magento\CatalogRule\Model\Rule\Condition\Product|category_ids" after="addNewCondition" stepKey="selectTypeCondition"/> + <waitForPageLoad after="selectTypeCondition" stepKey="waitForConditionChosed"/> + <click selector="{{AdminNewCatalogPriceRuleConditions.targetEllipsis('1')}}" after="waitForConditionChosed" stepKey="clickEllipsis"/> + <fillField selector="{{AdminNewCatalogPriceRuleConditions.targetInput('1', '1')}}" userInput="{{categoryId}}" after="clickEllipsis" stepKey="fillCategoryId"/> + <click selector="{{AdminNewCatalogPriceRuleConditions.applyButton('1', '1')}}" after="fillCategoryId" stepKey="clickApply"/> + </actionGroup> + + <actionGroup name="selectGeneralCustomerGroupActionGroup"> + <selectOption selector="{{AdminNewCatalogPriceRule.customerGroups}}" userInput="General" stepKey="selectCustomerGroup"/> + </actionGroup> + + <actionGroup name="selectNotLoggedInCustomerGroupActionGroup"> + <selectOption selector="{{AdminNewCatalogPriceRule.customerGroups}}" userInput="NOT LOGGED IN" stepKey="selectCustomerGroup"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/CatalogRule/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 000000000000..eb9cac1401c3 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminMenuMarketing"> + <data key="pageTitle">Marketing</data> + <data key="title">Marketing</data> + <data key="dataUiId">magento-backend-marketing</data> + </entity> + <entity name="AdminMenuMarketingPromotionsCatalogPriceRule"> + <data key="pageTitle">Catalog Price Rule</data> + <data key="title">Catalog Price Rule</data> + <data key="dataUiId">magento-catalogrule-promo-catalog</data> + </entity> +</entities> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleData.xml b/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleData.xml index b1cb1920f39f..5b75708d1ae0 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleData.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleData.xml @@ -63,4 +63,35 @@ <data key="simple_action">to_fixed</data> <data key="discount_amount">110.7</data> </entity> + + <entity name="CatalogRuleByPercentWith96Amount" type="catalogRule"> + <data key="name" unique="suffix">CatalogPriceRule</data> + <data key="description">Catalog Price Rule Description</data> + <data key="is_active">1</data> + <array key="customer_group_ids"> + <item>0</item> + </array> + <array key="website_ids"> + <item>1</item> + </array> + <data key="simple_action">by_percent</data> + <data key="discount_amount">96</data> + </entity> + + <entity name="CatalogRuleWithAllCustomerGroups" type="catalogRule"> + <data key="name" unique="suffix">CatalogPriceRule</data> + <data key="description">Catalog Price Rule Description</data> + <data key="is_active">1</data> + <array key="customer_group_ids"> + <item>0</item> + <item>1</item> + <item>2</item> + <item>3</item> + </array> + <array key="website_ids"> + <item>1</item> + </array> + <data key="simple_action">by_percent</data> + <data key="discount_amount">10</data> + </entity> </entities> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Page/CatalogRuleNewPage.xml b/app/code/Magento/CatalogRule/Test/Mftf/Page/CatalogRuleNewPage.xml new file mode 100644 index 000000000000..ad3e40b37c5b --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Page/CatalogRuleNewPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="CatalogRuleNewPage" url="catalog_rule/promo_catalog/new/" module="Magento_CatalogRule" area="admin"> + <section name="AdminNewCatalogPriceRule"/> + </page> +</pages> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminNewCatalogPriceRuleSection.xml b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminNewCatalogPriceRuleSection.xml index ab2c6eb89d26..635260888e7f 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminNewCatalogPriceRuleSection.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminNewCatalogPriceRuleSection.xml @@ -38,9 +38,11 @@ </section> <section name="AdminNewCatalogPriceRuleConditions"> - <element name="newCondition" type="button" selector="span.rule-param-new-child"/> + <element name="newCondition" type="button" selector=".rule-param.rule-param-new-child"/> <element name="conditionSelect" type="select" selector="select#conditions__{{var}}__new_child" parameterized="true"/> <element name="targetEllipsis" type="button" selector="//li[{{var}}]//a[@class='label'][text() = '...']" parameterized="true"/> + <element name="targetEllipsisValue" type="button" selector="//ul[@id='conditions__{{var}}__children']//a[contains(text(), '{{var1}}')]" parameterized="true" timeout="30"/> + <element name="targetSelect" type="select" selector="//ul[@id='conditions__{{var}}__children']//select" parameterized="true" timeout="30"/> <element name="targetInput" type="input" selector="input#conditions__{{var1}}--{{var2}}__value" parameterized="true"/> <element name="applyButton" type="button" selector="#conditions__{{var1}}__children li:nth-of-type({{var2}}) a.rule-param-apply" parameterized="true"/> </section> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleByCategoryTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleByCategoryTest.xml index 716a363ec5d7..875a7842f21f 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleByCategoryTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleByCategoryTest.xml @@ -17,7 +17,7 @@ <testCaseId value="MC-74"/> <group value="CatalogRule"/> <skip> - <issueId value="DEVOPS-3618"/> + <issueId value="MC-5777"/> </skip> </annotations> <before> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest.xml index 072385c46645..10db68e9053d 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest.xml @@ -18,7 +18,7 @@ <testCaseId value="MC-65"/> <group value="CatalogRule"/> <skip> - <issueId value="DEVOPS-3618"/> + <issueId value="MC-5777"/> </skip> </annotations> <before> @@ -78,7 +78,7 @@ <testCaseId value="MC-93"/> <group value="CatalogRule"/> <skip> - <issueId value="DEVOPS-3618"/> + <issueId value="MC-5777"/> </skip> </annotations> <before> @@ -104,7 +104,7 @@ <testCaseId value="MC-69"/> <group value="CatalogRule"/> <skip> - <issueId value="DEVOPS-3618"/> + <issueId value="MC-5777"/> </skip> </annotations> <before> @@ -130,7 +130,7 @@ <testCaseId value="MC-60"/> <group value="CatalogRule"/> <skip> - <issueId value="DEVOPS-3618"/> + <issueId value="MC-5777"/> </skip> </annotations> <before> @@ -156,7 +156,7 @@ <testCaseId value="MC-71"/> <group value="CatalogRule"/> <skip> - <issueId value="DEVOPS-3618"/> + <issueId value="MC-5777"/> </skip> </annotations> <before> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleTest.xml index 83770ffff5ea..06f3682aedd8 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleTest.xml @@ -18,7 +18,7 @@ <testCaseId value="MC-160"/> <group value="CatalogRule"/> <skip> - <issueId value="DEVOPS-3618"/> + <issueId value="MC-5777"/> </skip> </annotations> <before> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml new file mode 100644 index 000000000000..053a8c33e640 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml @@ -0,0 +1,136 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminEnableAttributeIsUndefinedCatalogPriceRuleTest"> + <annotations> + <features value="CatalogRule"/> + <title value="Enable 'is undefined' condition to Scope Catalog Price rules by custom product attribute"/> + <description value="Enable 'is undefined' condition to Scope Catalog Price rules by custom product attribute"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-13654"/> + <useCaseId value="MC-10971"/> + <group value="CatalogRule"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + + <createData entity="ApiCategory" stepKey="createFirstCategory"/> + <createData entity="ApiSimpleProduct" stepKey="createFirstProduct"> + <requiredEntity createDataKey="createFirstCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSecondProduct"> + <requiredEntity createDataKey="createFirstCategory"/> + </createData> + <createData entity="productYesNoAttribute" stepKey="createProductAttribute"/> + <createData entity="AddToDefaultSet" stepKey="addToAttributeSetHandle"> + <requiredEntity createDataKey="createProductAttribute"/> + </createData> + + <createData entity="SimpleSubCategory" stepKey="createSecondCategory"/> + <createData entity="SimpleProduct3" stepKey="createThirdProduct"> + <requiredEntity createDataKey="createSecondCategory"/> + </createData> + <createData entity="SimpleProduct4" stepKey="createForthProduct"> + <requiredEntity createDataKey="createSecondCategory"/> + </createData> + <createData entity="productDropDownAttribute" stepKey="createSecondProductAttribute"> + <field key="scope">website</field> + </createData> + </before> + <after> + + <!--Delete created data--> + <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToCatalogPriceRulePage"/> + <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRule"> + <argument name="name" value="{{CatalogRuleWithAllCustomerGroups.name}}"/> + <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> + </actionGroup> + <click stepKey="resetFilters" selector="{{AdminSecondaryGridSection.resetFilters}}"/> + <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> + <deleteData createDataKey="createSecondProduct" stepKey="deleteSecondProduct"/> + <deleteData createDataKey="createFirstCategory" stepKey="deleteFirstCategory"/> + <deleteData createDataKey="createThirdProduct" stepKey="deleteThirdProduct"/> + <deleteData createDataKey="createForthProduct" stepKey="deleteForthProduct"/> + <deleteData createDataKey="createSecondCategory" stepKey="deleteSecondCategory"/> + <deleteData createDataKey="createSecondProductAttribute" stepKey="deleteSecondProductAttribute"/> + + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create catalog price rule--> + <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToPriceRulePage"/> + <waitForPageLoad stepKey="waitForPriceRulePage"/> + <actionGroup ref="createCatalogPriceRule" stepKey="createCatalogPriceRule"> + <argument name="catalogRule" value="CatalogRuleWithAllCustomerGroups"/> + </actionGroup> + <actionGroup ref="selectNotLoggedInCustomerGroupActionGroup" stepKey="selectCustomerGroup"/> + <actionGroup ref="CreateCatalogPriceRuleConditionWithAttribute" stepKey="createCatalogPriceRuleCondition"> + <argument name="attributeName" value="$$createProductAttribute.attribute[frontend_labels][0][label]$$"/> + <argument name="targetValue" value="is"/> + <argument name="targetSelectValue" value="is undefined"/> + </actionGroup> + <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="clickSaveAndApplyRules"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + + <!--Check Catalog Price Rule for first product--> + <amOnPage url="{{StorefrontProductPage.url($$createFirstProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToFirstProductPage"/> + <waitForPageLoad stepKey="waitForFirstProductPageLoad"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.updatedPrice}}" stepKey="grabFirstProductUpdatedPrice"/> + <assertEquals expected='$110.70' expectedType="string" actual="($grabFirstProductUpdatedPrice)" stepKey="assertFirstProductUpdatedPrice"/> + + <!--Check Catalog Price Rule for second product--> + <amOnPage url="{{StorefrontProductPage.url($$createSecondProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToSecondProductPage"/> + <waitForPageLoad stepKey="waitForSecondProductPageLoad"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.updatedPrice}}" stepKey="grabSecondProductUpdatedPrice"/> + <assertEquals expected='$110.70' expectedType="string" actual="($grabFirstProductUpdatedPrice)" stepKey="assertSecondProductUpdatedPrice"/> + + <!--Delete previous attribute and Catalog Price Rule--> + <deleteData createDataKey="createProductAttribute" stepKey="deleteProductAttribute"/> + <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToCatalogPriceRulePage"/> + <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRule"> + <argument name="name" value="{{CatalogRuleWithAllCustomerGroups.name}}"/> + <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> + </actionGroup> + + <!--Add new attribute to Default set--> + <createData entity="AddToDefaultSet" stepKey="addToAttributeSetHandle1"> + <requiredEntity createDataKey="createSecondProductAttribute"/> + </createData> + + <!--Create new Catalog Price Rule--> + <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToPriceRulePage1"/> + <waitForPageLoad stepKey="waitForPriceRulePage1"/> + <actionGroup ref="createCatalogPriceRule" stepKey="createCatalogPriceRule1"> + <argument name="catalogRule" value="CatalogRuleWithAllCustomerGroups"/> + </actionGroup> + <actionGroup ref="selectNotLoggedInCustomerGroupActionGroup" stepKey="selectCustomerGroup1"/> + <actionGroup ref="CreateCatalogPriceRuleConditionWithAttribute" stepKey="createCatalogPriceRuleCondition1"> + <argument name="attributeName" value="$$createSecondProductAttribute.attribute[frontend_labels][0][label]$$"/> + <argument name="targetValue" value="is"/> + <argument name="targetSelectValue" value="is undefined"/> + </actionGroup> + <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="clickSaveAndApplyRules1"/> + <magentoCLI command="indexer:reindex" stepKey="reindex1"/> + <magentoCLI command="cache:flush" stepKey="flushCache1"/> + + <!--Check Catalog Price Rule for third product--> + <amOnPage url="{{StorefrontProductPage.url($$createThirdProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToThirdProductPage"/> + <waitForPageLoad stepKey="waitForThirdProductPageLoad"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.updatedPrice}}" stepKey="grabThirdProductUpdatedPrice"/> + <assertEquals expected='$110.70' expectedType="string" actual="($grabThirdProductUpdatedPrice)" stepKey="assertThirdProductUpdatedPrice"/> + + <!--Check Catalog Price Rule for forth product--> + <amOnPage url="{{StorefrontProductPage.url($$createForthProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToForthProductPage"/> + <waitForPageLoad stepKey="waitForForthProductPageLoad"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.updatedPrice}}" stepKey="grabForthProductUpdatedPrice"/> + <assertEquals expected='$110.70' expectedType="string" actual="($grabForthProductUpdatedPrice)" stepKey="assertForthProductUpdatedPrice"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminMarketingCatalogPriceRuleNavigateMenuTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminMarketingCatalogPriceRuleNavigateMenuTest.xml new file mode 100644 index 000000000000..0fe35419aaf3 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminMarketingCatalogPriceRuleNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMarketingCatalogPriceRuleNavigateMenuTest"> + <annotations> + <features value="CatalogRule"/> + <stories value="Menu Navigation"/> + <title value="Admin marketing catalog price rule navigate menu test"/> + <description value="Admin should be able to navigate to Marketing > Catalog Price Rule"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14134"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToCatalogPriceRulePage"> + <argument name="menuUiId" value="{{AdminMenuMarketing.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuMarketingPromotionsCatalogPriceRule.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuMarketingPromotionsCatalogPriceRule.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/CatalogPriceRuleAndCustomerGroupMembershipArePersistedUnderLongTermCookieTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/CatalogPriceRuleAndCustomerGroupMembershipArePersistedUnderLongTermCookieTest.xml new file mode 100644 index 000000000000..e3eac52a8d40 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/CatalogPriceRuleAndCustomerGroupMembershipArePersistedUnderLongTermCookieTest.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="CatalogPriceRuleAndCustomerGroupMembershipArePersistedUnderLongTermCookieTest"> + <annotations> + <features value="Persistent"/> + <stories value="Check the price"/> + <title value="Verify that Catalog Price Rule and Customer Group Membership are persisted under long-term cookie"/> + <description value="Verify that Catalog Price Rule and Customer Group Membership are persisted under long-term cookie"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-69455"/> + <group value="persistent"/> + <skip> + <issueId value="MC-5777"/> + </skip> + </annotations> + <before> + <createData entity="PersistentConfigEnabled" stepKey="enablePersistent"/> + <createData entity="PersistentLogoutClearDisable" stepKey="persistentLogoutClearDisable"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">50</field> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer"> + <field key="group_id">1</field> + </createData> + + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <!--Create Catalog Rule--> + <actionGroup ref="newCatalogPriceRuleByUIWithConditionIsCategory" stepKey="createCatalogPriceRule"> + <argument name="catalogRule" value="_defaultCatalogRule"/> + <argument name="categoryId" value="$$createCategory.id$$"/> + </actionGroup> + <actionGroup ref="selectGeneralCustomerGroupActionGroup" stepKey="selectCustomerGroup"/> + <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="clickSaveAndApplyRules"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the rule." stepKey="assertSuccess"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <!-- Delete the rule --> + <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToCatalogPriceRulePage"/> + <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRule"> + <argument name="name" value="{{_defaultCatalogRule.name}}"/> + <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + <createData entity="PersistentConfigDefault" stepKey="setDefaultPersistentState"/> + <createData entity="PersistentLogoutClearEnabled" stepKey="persistentLogoutClearEnabled"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + + <!--Go to category and check price--> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onStorefrontCategoryPage"/> + <see selector="{{StorefrontCategoryProductSection.ProductPriceByNumber('1')}}" userInput="$$createProduct.price$$" stepKey="checkPriceSimpleProduct"/> + + <!--Login to storefront from customer and check price--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="logInFromCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onStorefrontCategoryPage2"/> + <see userInput="Welcome, $$createCustomer.firstname$$ $$createCustomer.lastname$$!" selector="{{StorefrontPanelHeaderSection.WelcomeMessage}}" stepKey="homeCheckWelcome"/> + <see selector="{{StorefrontCategoryProductSection.ProductSpecialPriceByNumber('1')}}" userInput="45.00" stepKey="checkPriceSimpleProduct2"/> + + <!--Click *Sign Out* and check the price of the Simple Product--> + <actionGroup ref="StorefrontSignOutActionGroup" stepKey="storefrontSignOut"/> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onStorefrontCategoryPage3"/> + <see userInput="Welcome, $$createCustomer.firstname$$ $$createCustomer.lastname$$!" selector="{{StorefrontPanelHeaderSection.WelcomeMessage}}" stepKey="homeCheckWelcome2"/> + <seeElement selector="{{StorefrontPanelHeaderSection.notYouLink}}" stepKey="checkLinkNotYoy"/> + <see selector="{{StorefrontCategoryProductSection.ProductSpecialPriceByNumber('1')}}" userInput="45.00" stepKey="checkPriceSimpleProduct3"/> + + <!--Click the *Not you?* link and check the price for Simple Product--> + <click selector="{{StorefrontPanelHeaderSection.notYouLink}}" stepKey="clickNext"/> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onStorefrontCategoryPage4"/> + <see userInput="Default welcome msg!" selector="{{StorefrontPanelHeaderSection.WelcomeMessage}}" stepKey="homeCheckWelcome3"/> + <see selector="{{StorefrontCategoryProductSection.ProductPriceByNumber('1')}}" userInput="$$createProduct.price$$" stepKey="checkPriceSimpleProduct4"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/DeleteCustomerGroupTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/DeleteCustomerGroupTest.xml new file mode 100644 index 000000000000..75223fcfc4c4 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/DeleteCustomerGroupTest.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="DeleteCustomerGroupTest"> + <actionGroup ref="AdminOpenNewCatalogPriceRuleFormPageActionGroup" stepKey="openNewCatalogPriceRuleForm" /> + <actionGroup ref="AssertCustomerGroupNotOnCatalogPriceRuleFormActionGroup" stepKey="assertCustomerGroupNotOnCatalogPriceRuleForm"> + <argument name="customerGroup" value="$$customerGroup$$" /> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml index 55f775e40fea..c6ecc1c6d965 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml @@ -18,7 +18,7 @@ <testCaseId value="MC-79"/> <group value="CatalogRule"/> <skip> - <issueId value="DEVOPS-3618"/> + <issueId value="MC-5777"/> </skip> </annotations> <before> diff --git a/app/code/Magento/CatalogRule/Test/Unit/Block/Adminhtml/Edit/DeleteButtonTest.php b/app/code/Magento/CatalogRule/Test/Unit/Block/Adminhtml/Edit/DeleteButtonTest.php index 6178d51644fd..f969b342f826 100644 --- a/app/code/Magento/CatalogRule/Test/Unit/Block/Adminhtml/Edit/DeleteButtonTest.php +++ b/app/code/Magento/CatalogRule/Test/Unit/Block/Adminhtml/Edit/DeleteButtonTest.php @@ -24,13 +24,16 @@ class DeleteButtonTest extends \PHPUnit\Framework\TestCase */ protected $registryMock; + /** + * @inheritDoc + */ protected function setUp() { $this->urlBuilderMock = $this->createMock(\Magento\Framework\UrlInterface::class); $this->registryMock = $this->createMock(\Magento\Framework\Registry::class); $contextMock = $this->createMock(\Magento\Backend\Block\Widget\Context::class); - $contextMock->expects($this->once())->method('getUrlBuilder')->willReturn($this->urlBuilderMock); + $contextMock->expects($this->any())->method('getUrlBuilder')->willReturn($this->urlBuilderMock); $this->model = new \Magento\CatalogRule\Block\Adminhtml\Edit\DeleteButton( $contextMock, @@ -38,33 +41,9 @@ protected function setUp() ); } - public function testGetButtonData() - { - $ruleId = 42; - $deleteUrl = 'http://magento.com/rule/delete/' . $ruleId; - $ruleMock = new \Magento\Framework\DataObject(['id' => $ruleId]); - - $this->registryMock->expects($this->once()) - ->method('registry') - ->with(RegistryConstants::CURRENT_CATALOG_RULE_ID) - ->willReturn($ruleMock); - $this->urlBuilderMock->expects($this->once()) - ->method('getUrl') - ->with('*/*/delete', ['id' => $ruleId]) - ->willReturn($deleteUrl); - - $data = [ - 'label' => __('Delete Rule'), - 'class' => 'delete', - 'on_click' => 'deleteConfirm(\'' . __( - 'Are you sure you want to do this?' - ) . '\', \'' . $deleteUrl . '\')', - 'sort_order' => 20, - ]; - - $this->assertEquals($data, $this->model->getButtonData()); - } - + /** + * Test empty response without a present rule. + */ public function testGetButtonDataWithoutRule() { $this->assertEquals([], $this->model->getButtonData()); diff --git a/app/code/Magento/CatalogRule/etc/db_schema.xml b/app/code/Magento/CatalogRule/etc/db_schema.xml index f4c40a6930cc..894f057ba73d 100644 --- a/app/code/Magento/CatalogRule/etc/db_schema.xml +++ b/app/code/Magento/CatalogRule/etc/db_schema.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="catalogrule" resource="default" engine="innodb" comment="CatalogRule"> <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="varchar" name="name" nullable="true" length="255" comment="Name"/> <column xsi:type="text" name="description" nullable="true" comment="Description"/> <column xsi:type="date" name="from_date" comment="From"/> @@ -23,12 +23,12 @@ <column xsi:type="int" name="sort_order" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Sort Order"/> <column xsi:type="varchar" name="simple_action" nullable="true" length="32" comment="Simple Action"/> - <column xsi:type="decimal" name="discount_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="discount_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Discount Amount"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rule_id"/> </constraint> - <index name="CATALOGRULE_IS_ACTIVE_SORT_ORDER_TO_DATE_FROM_DATE" indexType="btree"> + <index referenceId="CATALOGRULE_IS_ACTIVE_SORT_ORDER_TO_DATE_FROM_DATE" indexType="btree"> <column name="is_active"/> <column name="sort_order"/> <column name="to_date"/> @@ -39,7 +39,7 @@ <column xsi:type="int" name="rule_product_id" padding="10" unsigned="true" nullable="false" identity="true" comment="Rule Product Id"/> <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" - comment="Rule Id"/> + comment="Rule ID"/> <column xsi:type="int" name="from_time" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="From Time"/> <column xsi:type="int" name="to_time" padding="10" unsigned="true" nullable="false" identity="false" default="0" @@ -49,7 +49,7 @@ default="0" comment="Product Id"/> <column xsi:type="varchar" name="action_operator" nullable="true" length="10" default="to_fixed" comment="Action Operator"/> - <column xsi:type="decimal" name="action_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="action_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Action Amount"/> <column xsi:type="smallint" name="action_stop" padding="6" unsigned="false" nullable="false" identity="false" default="0" comment="Action Stop"/> @@ -57,10 +57,10 @@ default="0" comment="Sort Order"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Website Id"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rule_product_id"/> </constraint> - <constraint xsi:type="unique" name="UNQ_EAA51B56FF092A0DCB795D1CEF812B7B"> + <constraint xsi:type="unique" referenceId="UNQ_EAA51B56FF092A0DCB795D1CEF812B7B"> <column name="rule_id"/> <column name="from_time"/> <column name="to_time"/> @@ -69,19 +69,19 @@ <column name="product_id"/> <column name="sort_order"/> </constraint> - <index name="CATALOGRULE_PRODUCT_CUSTOMER_GROUP_ID" indexType="btree"> + <index referenceId="CATALOGRULE_PRODUCT_CUSTOMER_GROUP_ID" indexType="btree"> <column name="customer_group_id"/> </index> - <index name="CATALOGRULE_PRODUCT_WEBSITE_ID" indexType="btree"> + <index referenceId="CATALOGRULE_PRODUCT_WEBSITE_ID" indexType="btree"> <column name="website_id"/> </index> - <index name="CATALOGRULE_PRODUCT_FROM_TIME" indexType="btree"> + <index referenceId="CATALOGRULE_PRODUCT_FROM_TIME" indexType="btree"> <column name="from_time"/> </index> - <index name="CATALOGRULE_PRODUCT_TO_TIME" indexType="btree"> + <index referenceId="CATALOGRULE_PRODUCT_TO_TIME" indexType="btree"> <column name="to_time"/> </index> - <index name="CATALOGRULE_PRODUCT_PRODUCT_ID" indexType="btree"> + <index referenceId="CATALOGRULE_PRODUCT_PRODUCT_ID" indexType="btree"> <column name="product_id"/> </index> </table> @@ -98,78 +98,80 @@ comment="Website Id"/> <column xsi:type="date" name="latest_start_date" comment="Latest StartDate"/> <column xsi:type="date" name="earliest_end_date" comment="Earliest EndDate"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rule_product_price_id"/> </constraint> - <constraint xsi:type="unique" name="CATRULE_PRD_PRICE_RULE_DATE_WS_ID_CSTR_GROUP_ID_PRD_ID"> + <constraint xsi:type="unique" referenceId="CATRULE_PRD_PRICE_RULE_DATE_WS_ID_CSTR_GROUP_ID_PRD_ID"> <column name="rule_date"/> <column name="website_id"/> <column name="customer_group_id"/> <column name="product_id"/> </constraint> - <index name="CATALOGRULE_PRODUCT_PRICE_CUSTOMER_GROUP_ID" indexType="btree"> + <index referenceId="CATALOGRULE_PRODUCT_PRICE_CUSTOMER_GROUP_ID" indexType="btree"> <column name="customer_group_id"/> </index> - <index name="CATALOGRULE_PRODUCT_PRICE_WEBSITE_ID" indexType="btree"> + <index referenceId="CATALOGRULE_PRODUCT_PRICE_WEBSITE_ID" indexType="btree"> <column name="website_id"/> </index> - <index name="CATALOGRULE_PRODUCT_PRICE_PRODUCT_ID" indexType="btree"> + <index referenceId="CATALOGRULE_PRODUCT_PRICE_PRODUCT_ID" indexType="btree"> <column name="product_id"/> </index> </table> <table name="catalogrule_group_website" resource="default" engine="innodb" comment="CatalogRule Group Website"> <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" - comment="Rule Id"/> + comment="Rule ID"/> <column xsi:type="int" name="customer_group_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Customer Group Id"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Website Id"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rule_id"/> <column name="customer_group_id"/> <column name="website_id"/> </constraint> - <index name="CATALOGRULE_GROUP_WEBSITE_CUSTOMER_GROUP_ID" indexType="btree"> + <index referenceId="CATALOGRULE_GROUP_WEBSITE_CUSTOMER_GROUP_ID" indexType="btree"> <column name="customer_group_id"/> </index> - <index name="CATALOGRULE_GROUP_WEBSITE_WEBSITE_ID" indexType="btree"> + <index referenceId="CATALOGRULE_GROUP_WEBSITE_WEBSITE_ID" indexType="btree"> <column name="website_id"/> </index> </table> <table name="catalogrule_website" resource="default" engine="innodb" comment="Catalog Rules To Websites Relations"> - <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false"/> + <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" + comment="Rule ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Website Id"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rule_id"/> <column name="website_id"/> </constraint> - <constraint xsi:type="foreign" name="CATALOGRULE_WEBSITE_RULE_ID_CATALOGRULE_RULE_ID" + <constraint xsi:type="foreign" referenceId="CATALOGRULE_WEBSITE_RULE_ID_CATALOGRULE_RULE_ID" table="catalogrule_website" column="rule_id" referenceTable="catalogrule" referenceColumn="rule_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CATALOGRULE_WEBSITE_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID" + <constraint xsi:type="foreign" referenceId="CATALOGRULE_WEBSITE_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID" table="catalogrule_website" column="website_id" referenceTable="store_website" referenceColumn="website_id" onDelete="CASCADE"/> - <index name="CATALOGRULE_WEBSITE_WEBSITE_ID" indexType="btree"> + <index referenceId="CATALOGRULE_WEBSITE_WEBSITE_ID" indexType="btree"> <column name="website_id"/> </index> </table> <table name="catalogrule_customer_group" resource="default" engine="innodb" comment="Catalog Rules To Customer Groups Relations"> - <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false"/> + <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" + comment="Rule ID"/> <column xsi:type="int" name="customer_group_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Customer Group Id"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rule_id"/> <column name="customer_group_id"/> </constraint> - <constraint xsi:type="foreign" name="CATALOGRULE_CUSTOMER_GROUP_RULE_ID_CATALOGRULE_RULE_ID" + <constraint xsi:type="foreign" referenceId="CATALOGRULE_CUSTOMER_GROUP_RULE_ID_CATALOGRULE_RULE_ID" table="catalogrule_customer_group" column="rule_id" referenceTable="catalogrule" referenceColumn="rule_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CATRULE_CSTR_GROUP_CSTR_GROUP_ID_CSTR_GROUP_CSTR_GROUP_ID" + <constraint xsi:type="foreign" referenceId="CATRULE_CSTR_GROUP_CSTR_GROUP_ID_CSTR_GROUP_CSTR_GROUP_ID" table="catalogrule_customer_group" column="customer_group_id" referenceTable="customer_group" referenceColumn="customer_group_id" onDelete="CASCADE"/> - <index name="CATALOGRULE_CUSTOMER_GROUP_CUSTOMER_GROUP_ID" indexType="btree"> + <index referenceId="CATALOGRULE_CUSTOMER_GROUP_CUSTOMER_GROUP_ID" indexType="btree"> <column name="customer_group_id"/> </index> </table> @@ -177,7 +179,7 @@ <column xsi:type="int" name="rule_product_id" padding="10" unsigned="true" nullable="false" identity="true" comment="Rule Product Id"/> <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" - comment="Rule Id"/> + comment="Rule ID"/> <column xsi:type="int" name="from_time" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="From Time"/> <column xsi:type="int" name="to_time" padding="10" unsigned="true" nullable="false" identity="false" default="0" @@ -195,10 +197,10 @@ default="0" comment="Sort Order"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Website Id"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rule_product_id"/> </constraint> - <constraint xsi:type="unique" name="UNQ_EAA51B56FF092A0DCB795D1CEF812B7B"> + <constraint xsi:type="unique" referenceId="UNQ_EAA51B56FF092A0DCB795D1CEF812B7B"> <column name="rule_id"/> <column name="from_time"/> <column name="to_time"/> @@ -207,19 +209,19 @@ <column name="product_id"/> <column name="sort_order"/> </constraint> - <index name="CATALOGRULE_PRODUCT_CUSTOMER_GROUP_ID" indexType="btree"> + <index referenceId="CATALOGRULE_PRODUCT_CUSTOMER_GROUP_ID" indexType="btree"> <column name="customer_group_id"/> </index> - <index name="CATALOGRULE_PRODUCT_WEBSITE_ID" indexType="btree"> + <index referenceId="CATALOGRULE_PRODUCT_WEBSITE_ID" indexType="btree"> <column name="website_id"/> </index> - <index name="CATALOGRULE_PRODUCT_FROM_TIME" indexType="btree"> + <index referenceId="CATALOGRULE_PRODUCT_FROM_TIME" indexType="btree"> <column name="from_time"/> </index> - <index name="CATALOGRULE_PRODUCT_TO_TIME" indexType="btree"> + <index referenceId="CATALOGRULE_PRODUCT_TO_TIME" indexType="btree"> <column name="to_time"/> </index> - <index name="CATALOGRULE_PRODUCT_PRODUCT_ID" indexType="btree"> + <index referenceId="CATALOGRULE_PRODUCT_PRODUCT_ID" indexType="btree"> <column name="product_id"/> </index> </table> @@ -237,42 +239,42 @@ comment="Website Id"/> <column xsi:type="date" name="latest_start_date" comment="Latest StartDate"/> <column xsi:type="date" name="earliest_end_date" comment="Earliest EndDate"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rule_product_price_id"/> </constraint> - <constraint xsi:type="unique" name="CATRULE_PRD_PRICE_RULE_DATE_WS_ID_CSTR_GROUP_ID_PRD_ID"> + <constraint xsi:type="unique" referenceId="CATRULE_PRD_PRICE_RULE_DATE_WS_ID_CSTR_GROUP_ID_PRD_ID"> <column name="rule_date"/> <column name="website_id"/> <column name="customer_group_id"/> <column name="product_id"/> </constraint> - <index name="CATALOGRULE_PRODUCT_PRICE_CUSTOMER_GROUP_ID" indexType="btree"> + <index referenceId="CATALOGRULE_PRODUCT_PRICE_CUSTOMER_GROUP_ID" indexType="btree"> <column name="customer_group_id"/> </index> - <index name="CATALOGRULE_PRODUCT_PRICE_WEBSITE_ID" indexType="btree"> + <index referenceId="CATALOGRULE_PRODUCT_PRICE_WEBSITE_ID" indexType="btree"> <column name="website_id"/> </index> - <index name="CATALOGRULE_PRODUCT_PRICE_PRODUCT_ID" indexType="btree"> + <index referenceId="CATALOGRULE_PRODUCT_PRICE_PRODUCT_ID" indexType="btree"> <column name="product_id"/> </index> </table> <table name="catalogrule_group_website_replica" resource="default" engine="innodb" comment="CatalogRule Group Website"> <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" - comment="Rule Id"/> + comment="Rule ID"/> <column xsi:type="int" name="customer_group_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Customer Group Id"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Website Id"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rule_id"/> <column name="customer_group_id"/> <column name="website_id"/> </constraint> - <index name="CATALOGRULE_GROUP_WEBSITE_CUSTOMER_GROUP_ID" indexType="btree"> + <index referenceId="CATALOGRULE_GROUP_WEBSITE_CUSTOMER_GROUP_ID" indexType="btree"> <column name="customer_group_id"/> </index> - <index name="CATALOGRULE_GROUP_WEBSITE_WEBSITE_ID" indexType="btree"> + <index referenceId="CATALOGRULE_GROUP_WEBSITE_WEBSITE_ID" indexType="btree"> <column name="website_id"/> </index> </table> diff --git a/app/code/Magento/CatalogRule/etc/mview.xml b/app/code/Magento/CatalogRule/etc/mview.xml index 35efe33461af..9e5a1c866a84 100644 --- a/app/code/Magento/CatalogRule/etc/mview.xml +++ b/app/code/Magento/CatalogRule/etc/mview.xml @@ -16,7 +16,6 @@ <table name="catalog_product_entity" entity_column="entity_id" /> <table name="catalog_product_entity_datetime" entity_column="entity_id" /> <table name="catalog_product_entity_decimal" entity_column="entity_id" /> - <table name="catalog_product_entity_gallery" entity_column="entity_id" /> <table name="catalog_product_entity_int" entity_column="entity_id" /> <table name="catalog_product_entity_text" entity_column="entity_id" /> <table name="catalog_product_entity_tier_price" entity_column="entity_id" /> diff --git a/app/code/Magento/CatalogRule/view/adminhtml/layout/catalog_rule_promo_catalog_block.xml b/app/code/Magento/CatalogRule/view/adminhtml/layout/catalog_rule_promo_catalog_block.xml index 99d64ed7a635..f38f6e0fcd85 100644 --- a/app/code/Magento/CatalogRule/view/adminhtml/layout/catalog_rule_promo_catalog_block.xml +++ b/app/code/Magento/CatalogRule/view/adminhtml/layout/catalog_rule_promo_catalog_block.xml @@ -11,7 +11,7 @@ <block class="Magento\Backend\Block\Widget\Grid" name="promo.catalog.grid" as="grid"> <arguments> <argument name="id" xsi:type="string">promo_catalog_grid</argument> - <argument name="dataSource" xsi:type="object">Magento\CatalogRule\Model\ResourceModel\Grid\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\CatalogRule\Model\ResourceModel\Grid\Collection</argument> <argument name="default_sort" xsi:type="string">name</argument> <argument name="default_dir" xsi:type="string">ASC</argument> <argument name="save_parameters_in_session" xsi:type="string">1</argument> diff --git a/app/code/Magento/CatalogSearch/Block/Advanced/Form.php b/app/code/Magento/CatalogSearch/Block/Advanced/Form.php index 4d1957991d1b..681b7ecfb02d 100644 --- a/app/code/Magento/CatalogSearch/Block/Advanced/Form.php +++ b/app/code/Magento/CatalogSearch/Block/Advanced/Form.php @@ -4,11 +4,6 @@ * See COPYING.txt for license details. */ -/** - * Advanced search form - * - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\CatalogSearch\Block\Advanced; use Magento\CatalogSearch\Model\Advanced; @@ -21,6 +16,8 @@ use Magento\Framework\View\Element\Template\Context; /** + * Advanced search form + * * @api * @since 100.0.2 */ @@ -58,7 +55,7 @@ public function __construct( } /** - * @return AbstractBlock + * @inheritdoc */ public function _prepareLayout() { @@ -286,6 +283,8 @@ public function getAttributeYesNoElement($attribute) } /** + * Get select block. + * * @return BlockInterface */ protected function _getSelectBlock() @@ -299,6 +298,8 @@ protected function _getSelectBlock() } /** + * Get date block. + * * @return BlockInterface|mixed */ protected function _getDateBlock() diff --git a/app/code/Magento/CatalogSearch/Block/Advanced/Result.php b/app/code/Magento/CatalogSearch/Block/Advanced/Result.php index 7c8e65b24913..9f25990594a4 100644 --- a/app/code/Magento/CatalogSearch/Block/Advanced/Result.php +++ b/app/code/Magento/CatalogSearch/Block/Advanced/Result.php @@ -63,7 +63,7 @@ public function __construct( } /** - * @return AbstractBlock + * @inheritdoc */ protected function _prepareLayout() { @@ -125,6 +125,8 @@ public function setListModes() } /** + * Initialize list collection. + * * @return void */ public function setListCollection() @@ -133,6 +135,8 @@ public function setListCollection() } /** + * Get product collection. + * * @return Collection */ protected function _getProductCollection() @@ -141,6 +145,8 @@ protected function _getProductCollection() } /** + * Set search model. + * * @return Advanced */ public function getSearchModel() @@ -149,6 +155,8 @@ public function getSearchModel() } /** + * Get result count. + * * @return mixed */ public function getResultCount() @@ -161,6 +169,8 @@ public function getResultCount() } /** + * Get product list HTML. + * * @return string */ public function getProductListHtml() @@ -169,6 +179,8 @@ public function getProductListHtml() } /** + * Get form URL. + * * @return string */ public function getFormUrl() @@ -182,6 +194,8 @@ public function getFormUrl() } /** + * Get search criteria. + * * @return array */ public function getSearchCriterias() diff --git a/app/code/Magento/CatalogSearch/Block/Plugin/FrontTabPlugin.php b/app/code/Magento/CatalogSearch/Block/Plugin/FrontTabPlugin.php index 94cb9c3c8fcf..85ad66013cf3 100644 --- a/app/code/Magento/CatalogSearch/Block/Plugin/FrontTabPlugin.php +++ b/app/code/Magento/CatalogSearch/Block/Plugin/FrontTabPlugin.php @@ -11,7 +11,7 @@ use Magento\Framework\Data\Form\Element\Fieldset; /** - * Plugin for Magento\Catalog\Block\Adminhtml\Product\Attribute\Edit\Tab\Front + * Add Search Weight field to the product attribute add/edit tab */ class FrontTabPlugin { diff --git a/app/code/Magento/CatalogSearch/Block/SearchTermsLog.php b/app/code/Magento/CatalogSearch/Block/SearchTermsLog.php index 0be43ce6ff1f..005c7860cfe5 100644 --- a/app/code/Magento/CatalogSearch/Block/SearchTermsLog.php +++ b/app/code/Magento/CatalogSearch/Block/SearchTermsLog.php @@ -9,7 +9,7 @@ use Magento\Framework\View\Element\Block\ArgumentInterface; /** - * Class for logging search terms on cached pages + * Provider of the information on whether the page is cacheable, so that AJAX-based logging of terms can be triggered */ class SearchTermsLog implements ArgumentInterface { diff --git a/app/code/Magento/CatalogSearch/Controller/Advanced/Index.php b/app/code/Magento/CatalogSearch/Controller/Advanced/Index.php index b20e36016d20..4942b36743c1 100644 --- a/app/code/Magento/CatalogSearch/Controller/Advanced/Index.php +++ b/app/code/Magento/CatalogSearch/Controller/Advanced/Index.php @@ -6,12 +6,17 @@ */ namespace Magento\CatalogSearch\Controller\Advanced; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\Controller\ResultFactory; -class Index extends \Magento\Framework\App\Action\Action +/** + * Advanced search controller. + */ +class Index extends \Magento\Framework\App\Action\Action implements HttpGetActionInterface, HttpPostActionInterface { /** - * @return \Magento\Framework\Controller\ResultInterface + * @inheritdoc */ public function execute() { diff --git a/app/code/Magento/CatalogSearch/Controller/Advanced/Result.php b/app/code/Magento/CatalogSearch/Controller/Advanced/Result.php index d6a30f4f3141..384132415a10 100644 --- a/app/code/Magento/CatalogSearch/Controller/Advanced/Result.php +++ b/app/code/Magento/CatalogSearch/Controller/Advanced/Result.php @@ -6,12 +6,22 @@ */ namespace Magento\CatalogSearch\Controller\Advanced; +use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\CatalogSearch\Model\Advanced as ModelAdvanced; use Magento\Framework\App\Action\Context; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\UrlFactory; -class Result extends \Magento\Framework\App\Action\Action +/** + * Advanced search result. + */ +class Result extends \Magento\Framework\App\Action\Action implements HttpGetActionInterface, HttpPostActionInterface { + /** + * No results default handle. + */ + const DEFAULT_NO_RESULT_HANDLE = 'catalogsearch_advanced_result_noresults'; + /** * Url factory * @@ -44,13 +54,16 @@ public function __construct( } /** - * @return \Magento\Framework\Controller\Result\Redirect + * @inheritdoc */ public function execute() { try { $this->_catalogSearchAdvanced->addFilters($this->getRequest()->getQueryValue()); - $this->_view->loadLayout(); + $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) { $this->messageManager->addError($e->getMessage()); diff --git a/app/code/Magento/CatalogSearch/Controller/Result/Index.php b/app/code/Magento/CatalogSearch/Controller/Result/Index.php index 22958b64d444..975c6ba1e7eb 100644 --- a/app/code/Magento/CatalogSearch/Controller/Result/Index.php +++ b/app/code/Magento/CatalogSearch/Controller/Result/Index.php @@ -6,15 +6,25 @@ */ namespace Magento\CatalogSearch\Controller\Result; +use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Catalog\Model\Layer\Resolver; use Magento\Catalog\Model\Session; use Magento\Framework\App\Action\Context; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Store\Model\StoreManagerInterface; use Magento\Search\Model\QueryFactory; use Magento\Search\Model\PopularSearchTerms; -class Index extends \Magento\Framework\App\Action\Action +/** + * Search result. + */ +class Index extends \Magento\Framework\App\Action\Action implements HttpGetActionInterface, HttpPostActionInterface { + /** + * No results default handle. + */ + const DEFAULT_NO_RESULT_HANDLE = 'catalogsearch_result_index_noresults'; + /** * Catalog session * @@ -85,12 +95,19 @@ public function execute() $getAdditionalRequestParameters = $this->getRequest()->getParams(); unset($getAdditionalRequestParameters[QueryFactory::QUERY_VAR_NAME]); + $handles = null; + if ($query->getNumResults() == 0) { + $this->_view->getPage()->initLayout(); + $handles = $this->_view->getLayout()->getUpdate()->getHandles(); + $handles[] = static::DEFAULT_NO_RESULT_HANDLE; + } + if (empty($getAdditionalRequestParameters) && $this->_objectManager->get(PopularSearchTerms::class)->isCacheable($queryText, $storeId) ) { - $this->getCacheableResult($catalogSearchHelper, $query); + $this->getCacheableResult($catalogSearchHelper, $query, $handles); } else { - $this->getNotCacheableResult($catalogSearchHelper, $query); + $this->getNotCacheableResult($catalogSearchHelper, $query, $handles); } } else { $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl()); @@ -102,9 +119,10 @@ public function execute() * * @param \Magento\CatalogSearch\Helper\Data $catalogSearchHelper * @param \Magento\Search\Model\Query $query + * @param array $handles * @return void */ - private function getCacheableResult($catalogSearchHelper, $query) + private function getCacheableResult($catalogSearchHelper, $query, $handles) { if (!$catalogSearchHelper->isMinQueryLength()) { $redirect = $query->getRedirect(); @@ -116,7 +134,7 @@ private function getCacheableResult($catalogSearchHelper, $query) $catalogSearchHelper->checkNotes(); - $this->_view->loadLayout(); + $this->_view->loadLayout($handles); $this->_view->renderLayout(); } @@ -125,11 +143,12 @@ private function getCacheableResult($catalogSearchHelper, $query) * * @param \Magento\CatalogSearch\Helper\Data $catalogSearchHelper * @param \Magento\Search\Model\Query $query + * @param array $handles * @return void * * @throws \Magento\Framework\Exception\LocalizedException */ - private function getNotCacheableResult($catalogSearchHelper, $query) + private function getNotCacheableResult($catalogSearchHelper, $query, $handles) { if ($catalogSearchHelper->isMinQueryLength()) { $query->setId(0)->setIsActive(1)->setIsProcessed(1); @@ -144,7 +163,7 @@ private function getNotCacheableResult($catalogSearchHelper, $query) $catalogSearchHelper->checkNotes(); - $this->_view->loadLayout(); + $this->_view->loadLayout($handles); $this->getResponse()->setNoCacheHeaders(); $this->_view->renderLayout(); } diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Aggregation/AggregationResolver.php b/app/code/Magento/CatalogSearch/Model/Adapter/Aggregation/AggregationResolver.php index 8ca0c0eeddf1..8a484d4cc790 100644 --- a/app/code/Magento/CatalogSearch/Model/Adapter/Aggregation/AggregationResolver.php +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Aggregation/AggregationResolver.php @@ -14,6 +14,9 @@ use Magento\Framework\Search\RequestInterface; use Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection as AttributeCollection; +/** + * Aggregation resolver. + */ class AggregationResolver implements AggregationResolverInterface { /** @@ -75,7 +78,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function resolve(RequestInterface $request, array $documentIds) { diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Aggregation/Checker/Query/AdvancedSearch.php b/app/code/Magento/CatalogSearch/Model/Adapter/Aggregation/Checker/Query/AdvancedSearch.php index 8f1f3fde1424..cea42347dc3f 100644 --- a/app/code/Magento/CatalogSearch/Model/Adapter/Aggregation/Checker/Query/AdvancedSearch.php +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Aggregation/Checker/Query/AdvancedSearch.php @@ -29,7 +29,7 @@ public function __construct($name) } /** - * {@inheritdoc} + * @inheritdoc */ public function isApplicable(RequestInterface $request) { diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Aggregation/Checker/Query/CatalogView.php b/app/code/Magento/CatalogSearch/Model/Adapter/Aggregation/Checker/Query/CatalogView.php index 01e95c4676af..42da02f8ca4f 100644 --- a/app/code/Magento/CatalogSearch/Model/Adapter/Aggregation/Checker/Query/CatalogView.php +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Aggregation/Checker/Query/CatalogView.php @@ -51,7 +51,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function isApplicable(RequestInterface $request) { diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Aggregation/RequestCheckerComposite.php b/app/code/Magento/CatalogSearch/Model/Adapter/Aggregation/RequestCheckerComposite.php index 9e4f93b45985..75910e172073 100644 --- a/app/code/Magento/CatalogSearch/Model/Adapter/Aggregation/RequestCheckerComposite.php +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Aggregation/RequestCheckerComposite.php @@ -9,6 +9,9 @@ use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Store\Model\StoreManagerInterface; +/** + * Composite request checker. + */ class RequestCheckerComposite implements RequestCheckerInterface { /** @@ -53,7 +56,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function isApplicable(RequestInterface $request) { diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider.php index 64a776e7354c..66f5ad7a7192 100644 --- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider.php +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider.php @@ -16,7 +16,14 @@ use Magento\Framework\DB\Select; use Magento\Framework\Search\Adapter\Mysql\Aggregation\DataProviderInterface; use Magento\Framework\Search\Request\BucketInterface; +use Magento\Framework\Event\Manager; +/** + * Data Provider for catalog search. + * + * @deprecated + * @see \Magento\ElasticSearch + */ class DataProvider implements DataProviderInterface { /** @@ -39,12 +46,18 @@ class DataProvider implements DataProviderInterface */ private $selectBuilderForAttribute; + /** + * @var Manager + */ + private $eventManager; + /** * @param Config $eavConfig * @param ResourceConnection $resource * @param ScopeResolverInterface $scopeResolver * @param null $customerSession @deprecated * @param SelectBuilderForAttribute|null $selectBuilderForAttribute + * @param Manager|null $eventManager * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -53,17 +66,19 @@ public function __construct( ResourceConnection $resource, ScopeResolverInterface $scopeResolver, $customerSession, - SelectBuilderForAttribute $selectBuilderForAttribute = null + SelectBuilderForAttribute $selectBuilderForAttribute = null, + Manager $eventManager = null ) { $this->eavConfig = $eavConfig; $this->connection = $resource->getConnection(); $this->scopeResolver = $scopeResolver; $this->selectBuilderForAttribute = $selectBuilderForAttribute ?: ObjectManager::getInstance()->get(SelectBuilderForAttribute::class); + $this->eventManager = $eventManager ?: ObjectManager::getInstance()->get(Manager::class); } /** - * {@inheritdoc} + * @inheritdoc */ public function getDataSet( BucketInterface $bucket, @@ -79,13 +94,17 @@ public function getDataSet( 'main_table.entity_id = entities.entity_id', [] ); + $this->eventManager->dispatch( + 'catalogsearch_query_add_filter_after', + ['bucket' => $bucket, 'select' => $select] + ); $select = $this->selectBuilderForAttribute->build($select, $attribute, $currentScope); return $select; } /** - * {@inheritdoc} + * @inheritdoc */ public function execute(Select $select) { @@ -93,6 +112,8 @@ public function execute(Select $select) } /** + * Get select. + * * @return Select */ private function getSelect() diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider/QueryBuilder.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider/QueryBuilder.php index 7ebf71f42443..26837448f2df 100644 --- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider/QueryBuilder.php +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider/QueryBuilder.php @@ -22,6 +22,9 @@ /** * Attribute query builder * + * @deprecated + * @see \Magento\ElasticSearch + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class QueryBuilder 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 be0a98339134..00012a78d100 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 @@ -21,6 +21,9 @@ /** * Build select for attribute. + * + * @deprecated + * @see \Magento\ElasticSearch */ class SelectBuilderForAttribute { @@ -71,6 +74,8 @@ public function __construct( } /** + * Build select for attribute search + * * @param Select $select * @param AbstractAttribute $attribute * @param int $currentScope @@ -98,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); @@ -113,6 +118,8 @@ public function build(Select $select, AbstractAttribute $attribute, int $current } /** + * Is add stock filter + * * @return bool */ private function isAddStockFilter() diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider/SelectBuilderForAttribute/ApplyStockConditionToSelect.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider/SelectBuilderForAttribute/ApplyStockConditionToSelect.php index 83f9c6f9c304..be572793f1ec 100644 --- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider/SelectBuilderForAttribute/ApplyStockConditionToSelect.php +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider/SelectBuilderForAttribute/ApplyStockConditionToSelect.php @@ -13,6 +13,9 @@ /** * Join stock table with stock condition to select. + * + * @deprecated + * @see \Magento\ElasticSearch */ class ApplyStockConditionToSelect { diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/BaseSelectStrategy/BaseSelectAttributesSearchStrategy.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/BaseSelectStrategy/BaseSelectAttributesSearchStrategy.php index 87fc896f5095..27a784f8609b 100644 --- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/BaseSelectStrategy/BaseSelectAttributesSearchStrategy.php +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/BaseSelectStrategy/BaseSelectAttributesSearchStrategy.php @@ -18,6 +18,9 @@ * * The main idea of this strategy is using eav index table as main table for query * in case when search request requires search by attributes + * + * @deprecated + * @see \Magento\ElasticSearch */ class BaseSelectAttributesSearchStrategy implements BaseSelectStrategyInterface { diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/BaseSelectStrategy/BaseSelectFullTextSearchStrategy.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/BaseSelectStrategy/BaseSelectFullTextSearchStrategy.php index 0732df7bd9f5..bff878122c8c 100644 --- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/BaseSelectStrategy/BaseSelectFullTextSearchStrategy.php +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/BaseSelectStrategy/BaseSelectFullTextSearchStrategy.php @@ -17,6 +17,9 @@ * * The main idea of this strategy is using fulltext search index table as main table for query * in case when search request does not requires any search by attributes + * + * @deprecated + * @see \Magento\ElasticSearch */ class BaseSelectFullTextSearchStrategy implements BaseSelectStrategyInterface { diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Dynamic/DataProvider.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Dynamic/DataProvider.php index d2e653ac9b9a..eb4761adf830 100644 --- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Dynamic/DataProvider.php +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Dynamic/DataProvider.php @@ -24,6 +24,8 @@ /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @deprecated + * @see \Magento\ElasticSearch */ class DataProvider implements DataProviderInterface { diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Field/Resolver.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Field/Resolver.php index a187ccc8f968..c24acf4610e0 100644 --- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Field/Resolver.php +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Field/Resolver.php @@ -10,6 +10,10 @@ use Magento\Framework\Search\Adapter\Mysql\Field\FieldInterface; use Magento\Framework\Search\Adapter\Mysql\Field\ResolverInterface; +/** + * @deprecated + * @see \Magento\ElasticSearch + */ class Resolver implements ResolverInterface { /** diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/AliasResolver.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/AliasResolver.php index 547122125b1d..bf431396cc0c 100644 --- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/AliasResolver.php +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/AliasResolver.php @@ -12,6 +12,8 @@ * Purpose of class is to resolve table alias for Search Request filter * @api * @since 100.1.6 + * @deprecated + * @see \Magento\ElasticSearch */ class AliasResolver { diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php index 35addc3fafd4..c758e773f43c 100644 --- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php @@ -8,7 +8,9 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\CatalogSearch\Model\Search\FilterMapper\VisibilityFilter; use Magento\CatalogSearch\Model\Search\TableMapper; +use Magento\Customer\Model\Session; use Magento\Eav\Model\Config; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; @@ -20,11 +22,14 @@ use Magento\Framework\Search\Adapter\Mysql\Filter\PreprocessorInterface; use Magento\Framework\Search\Request\FilterInterface; use Magento\Store\Model\Store; -use Magento\Customer\Model\Session; -use Magento\CatalogSearch\Model\Search\FilterMapper\VisibilityFilter; /** + * ElasticSearch search filter pre-processor. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @deprecated + * @see \Magento\ElasticSearch */ class Preprocessor implements PreprocessorInterface { @@ -126,7 +131,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function process(FilterInterface $filter, $isNegation, $query) { @@ -134,10 +139,13 @@ public function process(FilterInterface $filter, $isNegation, $query) } /** + * Process query with field. + * * @param FilterInterface $filter * @param bool $isNegation * @param string $query * @return string + * @throws \Magento\Framework\Exception\LocalizedException */ private function processQueryWithField(FilterInterface $filter, $isNegation, $query) { @@ -168,7 +176,7 @@ private function processQueryWithField(FilterInterface $filter, $isNegation, $qu } elseif ($filter->getField() === VisibilityFilter::VISIBILITY_FILTER_FIELD) { return ''; } elseif ($filter->getType() === FilterInterface::TYPE_TERM && - in_array($attribute->getFrontendInput(), ['select', 'multiselect'], true) + in_array($attribute->getFrontendInput(), ['select', 'multiselect', 'boolean'], true) ) { $resultQuery = $this->processTermSelect($filter, $isNegation); } elseif ($filter->getType() === FilterInterface::TYPE_RANGE && @@ -202,19 +210,23 @@ private function processQueryWithField(FilterInterface $filter, $isNegation, $qu ->where('main_table.store_id = ?', Store::DEFAULT_STORE_ID) ->having($query); - $resultQuery = 'search_index.entity_id IN ( - select entity_id from ' . $this->conditionManager->wrapBrackets($select) . ' as filter - )'; + $resultQuery = 'search_index.entity_id IN (' + . 'select entity_id from ' + . $this->conditionManager->wrapBrackets($select) + . ' as filter)'; } return $resultQuery; } /** + * Process range numeric. + * * @param FilterInterface $filter * @param string $query * @param Attribute $attribute * @return string + * @throws \Exception */ private function processRangeNumeric(FilterInterface $filter, $query, $attribute) { @@ -236,14 +248,17 @@ private function processRangeNumeric(FilterInterface $filter, $query, $attribute ->where('main_table.store_id = ?', $currentStoreId) ->having($query); - $resultQuery = 'search_index.entity_id IN ( - select entity_id from ' . $this->conditionManager->wrapBrackets($select) . ' as filter - )'; + $resultQuery = 'search_index.entity_id IN (' + . 'select entity_id from ' + . $this->conditionManager->wrapBrackets($select) + . ' as filter)'; return $resultQuery; } /** + * Process term select. + * * @param FilterInterface $filter * @param bool $isNegation * @return string diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Plugin/Aggregation/Category/DataProvider.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Plugin/Aggregation/Category/DataProvider.php index 94432bbfe4a7..a5650cac7339 100644 --- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Plugin/Aggregation/Category/DataProvider.php +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Plugin/Aggregation/Category/DataProvider.php @@ -18,6 +18,8 @@ /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @deprecated + * @see \Magento\ElasticSearch */ class DataProvider { diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Options.php b/app/code/Magento/CatalogSearch/Model/Adapter/Options.php index 425e6c004161..f2e1c3c4c42d 100644 --- a/app/code/Magento/CatalogSearch/Model/Adapter/Options.php +++ b/app/code/Magento/CatalogSearch/Model/Adapter/Options.php @@ -10,6 +10,8 @@ use Magento\Store\Model\ScopeInterface; /** + * Catalog search config. + * * @api * @since 100.0.2 */ @@ -33,7 +35,7 @@ public function __construct(ScopeConfigInterface $scopeConfig) } /** - * {@inheritdoc} + * @inheritdoc */ public function get() { diff --git a/app/code/Magento/CatalogSearch/Model/Adminhtml/System/Config/Backend/Engine.php b/app/code/Magento/CatalogSearch/Model/Adminhtml/System/Config/Backend/Engine.php index 2964f836ab9d..1f11d2650334 100644 --- a/app/code/Magento/CatalogSearch/Model/Adminhtml/System/Config/Backend/Engine.php +++ b/app/code/Magento/CatalogSearch/Model/Adminhtml/System/Config/Backend/Engine.php @@ -6,7 +6,8 @@ namespace Magento\CatalogSearch\Model\Adminhtml\System\Config\Backend; /** - * @author Magento Core Team <core@magentocommerce.com> + * Backend model for catalog search engine system config + * * @api * @since 100.0.2 */ @@ -43,6 +44,7 @@ public function __construct( /** * After save call + * * Invalidate catalog search index if engine was changed * * @return $this diff --git a/app/code/Magento/CatalogSearch/Model/Advanced.php b/app/code/Magento/CatalogSearch/Model/Advanced.php index 28f67a7829e7..5b96a8c21cbe 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,9 +21,11 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Registry; use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\App\ObjectManager; /** * Catalog advanced search model + * * @method int getEntityTypeId() * @method \Magento\CatalogSearch\Model\Advanced setEntityTypeId(int $value) * @method int getAttributeSetId() @@ -63,6 +67,7 @@ class Advanced extends \Magento\Framework\Model\AbstractModel /** * Initialize dependencies * + * @deprecated * @var Config */ protected $_catalogConfig; @@ -105,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 * @@ -123,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( @@ -137,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; @@ -146,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 ); } @@ -265,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; @@ -276,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 * @@ -284,18 +319,14 @@ 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; } /** + * Add search criteria. + * * @param EntityAttribute $attribute * @param mixed $value * @return void 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 000000000000..e62de9c689fc --- /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 000000000000..23719a6713a3 --- /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 000000000000..6e963ea1aa8a --- /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/Advanced/Request/Builder.php b/app/code/Magento/CatalogSearch/Model/Advanced/Request/Builder.php index 429c29fd3d2e..9e7737123b2e 100644 --- a/app/code/Magento/CatalogSearch/Model/Advanced/Request/Builder.php +++ b/app/code/Magento/CatalogSearch/Model/Advanced/Request/Builder.php @@ -8,12 +8,16 @@ use Magento\Framework\Search\Request\Builder as RequestBuilder; /** + * Catalog search advanced request builder. + * * @api * @since 100.0.2 */ class Builder extends RequestBuilder { /** + * Bind value to query. + * * @param string $attributeCode * @param array|string $attributeValue * @return void diff --git a/app/code/Magento/CatalogSearch/Model/Attribute/SearchWeight.php b/app/code/Magento/CatalogSearch/Model/Attribute/SearchWeight.php index d6110f4b3b2c..fa42cadb8a2f 100644 --- a/app/code/Magento/CatalogSearch/Model/Attribute/SearchWeight.php +++ b/app/code/Magento/CatalogSearch/Model/Attribute/SearchWeight.php @@ -7,8 +7,9 @@ namespace Magento\CatalogSearch\Model\Attribute; /** - * This plugin is responsible for processing of search_weight property of a product attribute, - * which is used to boost matches by specific attributes. + * This plugin is responsible for processing of search_weight property of a product attribute. + * + * 'search_weight' is used to boost matches by specific attributes. * * This is part of search accuracy customization functionality. */ diff --git a/app/code/Magento/CatalogSearch/Model/Autocomplete/DataProvider.php b/app/code/Magento/CatalogSearch/Model/Autocomplete/DataProvider.php index c1c9997bc83e..f014c6d13318 100644 --- a/app/code/Magento/CatalogSearch/Model/Autocomplete/DataProvider.php +++ b/app/code/Magento/CatalogSearch/Model/Autocomplete/DataProvider.php @@ -13,6 +13,9 @@ use Magento\Framework\App\Config\ScopeConfigInterface as ScopeConfig; use Magento\Store\Model\ScopeInterface; +/** + * Catalog search auto-complete data provider. + */ class DataProvider implements DataProviderInterface { /** @@ -44,6 +47,7 @@ class DataProvider implements DataProviderInterface /** * @param QueryFactory $queryFactory * @param ItemFactory $itemFactory + * @param ScopeConfig $scopeConfig */ public function __construct( QueryFactory $queryFactory, @@ -60,7 +64,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getItems() { diff --git a/app/code/Magento/CatalogSearch/Model/Fulltext.php b/app/code/Magento/CatalogSearch/Model/Fulltext.php index 1792fd21fb8d..398d6e9dd18d 100644 --- a/app/code/Magento/CatalogSearch/Model/Fulltext.php +++ b/app/code/Magento/CatalogSearch/Model/Fulltext.php @@ -21,6 +21,9 @@ * @method \Magento\CatalogSearch\Model\Fulltext setStoreId(int $value) * @method string getDataIndex() * @method \Magento\CatalogSearch\Model\Fulltext setDataIndex(string $value) + * + * @deprecated + * @see \Magento\ElasticSearch */ class Fulltext extends \Magento\Framework\Model\AbstractModel { diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php index 37208ed896e8..21d8b7297da7 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php @@ -117,7 +117,8 @@ public function execute($entityIds) } /** - * {@inheritdoc} + * @inheritdoc + * * @throws \InvalidArgumentException */ public function executeByDimensions(array $dimensions, \Traversable $entityIds = null) @@ -145,8 +146,10 @@ public function executeByDimensions(array $dimensions, \Traversable $entityIds = $productIds = array_unique( array_merge($entityIds, $this->fulltextResource->getRelationsByChild($entityIds)) ); - $saveHandler->deleteIndex($dimensions, new \ArrayIterator($productIds)); - $saveHandler->saveIndex($dimensions, $this->fullAction->rebuildStoreIndex($storeId, $productIds)); + if ($saveHandler->isAvailable($dimensions)) { + $saveHandler->deleteIndex($dimensions, new \ArrayIterator($productIds)); + $saveHandler->saveIndex($dimensions, $this->fullAction->rebuildStoreIndex($storeId, $productIds)); + } } } diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php index a8d46911193a..39cb95747c2c 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php @@ -12,6 +12,8 @@ use Magento\Store\Model\Store; /** + * Catalog search full test search data provider. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) * @api @@ -568,8 +570,8 @@ public function prepareProductIndex($indexData, $productData, $storeId) } } foreach ($indexData as $entityId => $attributeData) { - foreach ($attributeData as $attributeId => $attributeValue) { - $value = $this->getAttributeValue($attributeId, $attributeValue, $storeId); + foreach ($attributeData as $attributeId => $attributeValues) { + $value = $this->getAttributeValue($attributeId, $attributeValues, $storeId); if (!empty($value)) { if (isset($index[$attributeId])) { $index[$attributeId][$entityId] = $value; @@ -600,16 +602,16 @@ public function prepareProductIndex($indexData, $productData, $storeId) * Retrieve attribute source value for search * * @param int $attributeId - * @param mixed $valueId + * @param mixed $valueIds * @param int $storeId * @return string */ - private function getAttributeValue($attributeId, $valueId, $storeId) + private function getAttributeValue($attributeId, $valueIds, $storeId) { $attribute = $this->getSearchableAttribute($attributeId); - $value = $this->engine->processAttributeValue($attribute, $valueId); + $value = $this->engine->processAttributeValue($attribute, $valueIds); if (false !== $value) { - $optionValue = $this->getAttributeOptionValue($attributeId, $valueId, $storeId); + $optionValue = $this->getAttributeOptionValue($attributeId, $valueIds, $storeId); if (null === $optionValue) { $value = $this->filterAttributeValue($value); } else { @@ -624,13 +626,15 @@ private function getAttributeValue($attributeId, $valueId, $storeId) * Get attribute option value * * @param int $attributeId - * @param int $valueId + * @param int|string $valueIds * @param int $storeId * @return null|string */ - private function getAttributeOptionValue($attributeId, $valueId, $storeId) + private function getAttributeOptionValue($attributeId, $valueIds, $storeId) { $optionKey = $attributeId . '-' . $storeId; + $attributeValueIds = explode(',', $valueIds); + $attributeOptionValue = ''; if (!array_key_exists($optionKey, $this->attributeOptions) ) { $attribute = $this->getSearchableAttribute($attributeId); @@ -648,8 +652,12 @@ private function getAttributeOptionValue($attributeId, $valueId, $storeId) $this->attributeOptions[$optionKey] = null; } } - - return $this->attributeOptions[$optionKey][$valueId] ?? null; + foreach ($attributeValueIds as $attrValueId) { + if (isset($this->attributeOptions[$optionKey][$attrValueId])) { + $attributeOptionValue .= $this->attributeOptions[$optionKey][$attrValueId] . ' '; + } + } + return empty($attributeOptionValue) ? null : trim($attributeOptionValue); } /** diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php index 8a18c1bfcc57..f56a4fe4d1b7 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php @@ -397,7 +397,7 @@ public function rebuildStoreIndex($storeId, $productIds = null) } $products = $this->dataProvider ->getSearchableProducts($storeId, $staticFields, $productIds, $lastProductId, $this->batchSize); - }; + } } /** diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/IndexIterator.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/IndexIterator.php index abd65c71cad6..a2c39deff189 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/IndexIterator.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/IndexIterator.php @@ -14,6 +14,9 @@ * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\Full * @api * @since 100.0.3 + * + * @deprecated + * @see \Magento\ElasticSearch */ class IndexIterator implements \Iterator { diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Attribute.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Attribute.php index 83ad7acca84d..86dccf8cfe55 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Attribute.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Attribute.php @@ -7,6 +7,9 @@ use Magento\CatalogSearch\Model\Indexer\Fulltext; +/** + * Catalog search indexer plugin for catalog attribute. + */ class Attribute extends AbstractPlugin { /** diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Category.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Category.php index e9c0e06ac38a..1218e3da9a78 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Category.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Category.php @@ -10,8 +10,7 @@ use Magento\Framework\Model\AbstractModel; /** - * Class Category - * @package Magento\CatalogSearch\Model\Indexer\Fulltext\Plugin + * Catalog search indexer plugin for catalog category. */ class Category extends AbstractPlugin { @@ -30,6 +29,8 @@ public function aroundSave(ResourceCategory $resourceCategory, \Closure $proceed } /** + * Reindex catalog search. + * * @param ResourceCategory $resourceCategory * @param \Closure $proceed * @param AbstractModel $category diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Product.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Product.php index 949033bb338e..e250d1123937 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Product.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Product.php @@ -9,15 +9,19 @@ use Magento\Catalog\Model\ResourceModel\Product as ResourceProduct; use Magento\Framework\Model\AbstractModel; +/** + * Catalog search indexer plugin for catalog product. + */ class Product extends AbstractPlugin { /** - * Reindex on product save + * Reindex on product save. * * @param ResourceProduct $productResource * @param \Closure $proceed * @param AbstractModel $product * @return ResourceProduct + * @throws \Exception */ public function aroundSave(ResourceProduct $productResource, \Closure $proceed, AbstractModel $product) { @@ -31,6 +35,7 @@ public function aroundSave(ResourceProduct $productResource, \Closure $proceed, * @param \Closure $proceed * @param AbstractModel $product * @return ResourceProduct + * @throws \Exception */ public function aroundDelete(ResourceProduct $productResource, \Closure $proceed, AbstractModel $product) { @@ -38,6 +43,8 @@ public function aroundDelete(ResourceProduct $productResource, \Closure $proceed } /** + * Reindex catalog search. + * * @param ResourceProduct $productResource * @param \Closure $proceed * @param AbstractModel $product diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Store.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Store.php index e971f59cf10f..23ab52012f2e 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Store.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Store.php @@ -11,6 +11,9 @@ use Magento\Framework\Indexer\ConfigInterface; use Magento\Framework\Event\ObserverInterface; +/** + * Catalog search indexer plugin for store. + */ class Store implements ObserverInterface { /** @@ -44,6 +47,8 @@ public function __construct( } /** + * Reindex catalog search. + * * @param \Magento\Store\Model\Store $store * @return void */ @@ -59,6 +64,8 @@ private function clearIndex(\Magento\Store\Model\Store $store) } /** + * Reindex catalog search on store modification. + * * @param \Magento\Framework\Event\Observer $observer * @return void */ diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/IndexStructure.php b/app/code/Magento/CatalogSearch/Model/Indexer/IndexStructure.php index c46f062c1e6d..0d226acdc3d7 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/IndexStructure.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/IndexStructure.php @@ -15,6 +15,8 @@ use Magento\Framework\Search\Request\IndexScopeResolverInterface; /** + * Catalog search index structure. + * * @api * @since 100.0.2 */ @@ -43,9 +45,7 @@ public function __construct( } /** - * @param string $index - * @param Dimension[] $dimensions - * @return void + * @inheritdoc */ public function delete($index, array $dimensions = []) { @@ -56,11 +56,7 @@ public function delete($index, array $dimensions = []) } /** - * @param string $index - * @param array $fields - * @param array $dimensions - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * @return void + * @inheritdoc */ public function create($index, array $fields, array $dimensions = []) { @@ -68,6 +64,8 @@ public function create($index, array $fields, array $dimensions = []) } /** + * Create fulltext index table. + * * @param string $tableName * @throws \Zend_Db_Exception * @return void diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/IndexStructureFactory.php b/app/code/Magento/CatalogSearch/Model/Indexer/IndexStructureFactory.php index d8b3c19ddb91..d54d6c939ccc 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/IndexStructureFactory.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/IndexStructureFactory.php @@ -10,6 +10,8 @@ use Magento\Framework\Search\EngineResolverInterface; /** + * Index structure factory + * * @api * @since 100.1.0 */ diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/IndexStructureProxy.php b/app/code/Magento/CatalogSearch/Model/Indexer/IndexStructureProxy.php index 0fb8af514456..c62d2a033565 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/IndexStructureProxy.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/IndexStructureProxy.php @@ -7,6 +7,9 @@ use Magento\Framework\Indexer\IndexStructureInterface; +/** + * Catalog search index structure proxy. + */ class IndexStructureProxy implements IndexStructureInterface { /** @@ -29,7 +32,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function delete( $index, @@ -39,7 +42,7 @@ public function delete( } /** - * {@inheritdoc} + * @inheritdoc */ public function create( $index, diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/IndexSwitcherInterface.php b/app/code/Magento/CatalogSearch/Model/Indexer/IndexSwitcherInterface.php index bdb663a0b616..f45ef11a8638 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/IndexSwitcherInterface.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/IndexSwitcherInterface.php @@ -6,7 +6,8 @@ namespace Magento\CatalogSearch\Model\Indexer; /** - * Provides a functionality to replace main index with its temporary representation + * Provides a functionality to replace main index with its temporary representation. + * * @api * @since 100.2.0 */ diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/IndexSwitcherProxy.php b/app/code/Magento/CatalogSearch/Model/Indexer/IndexSwitcherProxy.php index e5c801cf0b7d..e4a20cc188fb 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/IndexSwitcherProxy.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/IndexSwitcherProxy.php @@ -51,7 +51,7 @@ public function __construct( } /** - * {@inheritDoc} + * @inheritDoc * * As index switcher is an optional part of the search SPI, it may be not defined by a search engine. * It is especially reasonable for search engines with pre-defined indexes declaration (like Sphinx) diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/IndexerHandler.php b/app/code/Magento/CatalogSearch/Model/Indexer/IndexerHandler.php index 931d7571a901..9f105bd3ea46 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/IndexerHandler.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/IndexerHandler.php @@ -14,8 +14,12 @@ use Magento\Framework\Indexer\SaveHandler\Batch; /** + * Catalog search indexer handler. + * * @api * @since 100.0.2 + * @deprecated + * @see \Magento\ElasticSearch */ class IndexerHandler implements IndexerInterface { @@ -90,7 +94,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function saveIndex($dimensions, \Traversable $documents) { @@ -100,7 +104,7 @@ public function saveIndex($dimensions, \Traversable $documents) } /** - * {@inheritdoc} + * @inheritdoc */ public function deleteIndex($dimensions, \Traversable $documents) { @@ -111,7 +115,7 @@ public function deleteIndex($dimensions, \Traversable $documents) } /** - * {@inheritdoc} + * @inheritdoc */ public function cleanIndex($dimensions) { @@ -120,14 +124,20 @@ public function cleanIndex($dimensions) } /** - * {@inheritdoc} + * @inheritdoc */ - public function isAvailable() + public function isAvailable($dimensions = []) { - return true; + if (empty($dimensions)) { + return true; + } + + return $this->resource->getConnection()->isTableExists($this->getTableName($dimensions)); } /** + * Returns table name. + * * @param Dimension[] $dimensions * @return string */ @@ -137,6 +147,8 @@ private function getTableName($dimensions) } /** + * Returns index name. + * * @return string */ private function getIndexName() @@ -145,6 +157,8 @@ private function getIndexName() } /** + * Add documents to storage. + * * @param array $documents * @param Dimension[] $dimensions * @return void @@ -163,6 +177,8 @@ private function insertDocuments(array $documents, array $dimensions) } /** + * Searchable filter preparation. + * * @param array $documents * @return array */ @@ -183,6 +199,8 @@ private function prepareSearchableFields(array $documents) } /** + * Prepare fields. + * * @return void */ private function prepareFields() diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/IndexerHandlerFactory.php b/app/code/Magento/CatalogSearch/Model/Indexer/IndexerHandlerFactory.php index b9b44df6f404..841ee8708f26 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/IndexerHandlerFactory.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/IndexerHandlerFactory.php @@ -10,6 +10,8 @@ use Magento\Framework\Search\EngineResolverInterface; /** + * Indexer handler factory. + * * @api * @since 100.0.2 */ diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Mview/Action.php b/app/code/Magento/CatalogSearch/Model/Indexer/Mview/Action.php index 47a8681a73c6..b6639f760457 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Mview/Action.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Mview/Action.php @@ -9,6 +9,9 @@ use Magento\Framework\Mview\ActionInterface; use Magento\Framework\Indexer\IndexerInterfaceFactory; +/** + * Catalog search materialized view index action. + */ class Action implements ActionInterface { /** diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/ProductFieldset.php b/app/code/Magento/CatalogSearch/Model/Indexer/ProductFieldset.php index 343dc50d604b..6db063bde7d1 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/ProductFieldset.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/ProductFieldset.php @@ -13,6 +13,8 @@ /** * @api * @since 100.0.2 + * @deprecated + * @see \Magento\ElasticSearch */ class ProductFieldset implements \Magento\Framework\Indexer\FieldsetInterface { diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Scope/IndexSwitcher.php b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/IndexSwitcher.php index 86649cd1093d..ed2b1be5c703 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Scope/IndexSwitcher.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/IndexSwitcher.php @@ -11,6 +11,9 @@ /** * Provides a functionality to replace main index with its temporary representation + * + * @deprecated + * @see \Magento\ElasticSearch */ class IndexSwitcher implements IndexSwitcherInterface { diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Scope/IndexTableNotExistException.php b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/IndexTableNotExistException.php index a386b74084af..b01f3c50d500 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Scope/IndexTableNotExistException.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/IndexTableNotExistException.php @@ -14,6 +14,8 @@ * * @api * @since 100.2.0 + * @deprecated + * @see \Magento\ElasticSearch */ class IndexTableNotExistException extends LocalizedException { diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Scope/ScopeProxy.php b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/ScopeProxy.php index 9166ddbef60d..fcecb36e7d50 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Scope/ScopeProxy.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/ScopeProxy.php @@ -9,8 +9,7 @@ use Magento\Framework\Search\Request\Dimension; /** - * Implementation of IndexScopeResolverInterface which resolves index scope dynamically - * depending on current scope state + * Implementation of IndexScopeResolverInterface which resolves index scope dynamically depending on current scope state */ class ScopeProxy implements \Magento\Framework\Search\Request\IndexScopeResolverInterface { @@ -64,9 +63,7 @@ private function create($state) } /** - * @param string $index - * @param Dimension[] $dimensions - * @return string + * @inheritdoc */ public function resolve($index, array $dimensions) { diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Scope/State.php b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/State.php index 5f9a3e305995..a12ae8e69c31 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Scope/State.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/State.php @@ -31,6 +31,7 @@ class State /** * Set the state to use temporary Index + * * @return void */ public function useTemporaryIndex() @@ -40,6 +41,7 @@ public function useTemporaryIndex() /** * Set the state to use regular Index + * * @return void */ public function useRegularIndex() @@ -48,6 +50,8 @@ public function useRegularIndex() } /** + * Get state. + * * @return string */ public function getState() diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Scope/TemporaryResolver.php b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/TemporaryResolver.php index c52a0a058665..796559d1f703 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Scope/TemporaryResolver.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/TemporaryResolver.php @@ -11,6 +11,9 @@ /** * Resolves name of a temporary table for indexation + * + * @deprecated + * @see \Magento\ElasticSearch */ class TemporaryResolver implements \Magento\Framework\Search\Request\IndexScopeResolverInterface { diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Scope/UnknownStateException.php b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/UnknownStateException.php index b44a2d220545..8722cd52b618 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Scope/UnknownStateException.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/UnknownStateException.php @@ -13,6 +13,8 @@ * * @api * @since 100.2.0 + * @deprecated + * @see \Magento\ElasticSearch */ class UnknownStateException extends LocalizedException { diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Category/ItemCollectionProvider.php b/app/code/Magento/CatalogSearch/Model/Layer/Category/ItemCollectionProvider.php index 4ce286bf1592..c24665f4808d 100644 --- a/app/code/Magento/CatalogSearch/Model/Layer/Category/ItemCollectionProvider.php +++ b/app/code/Magento/CatalogSearch/Model/Layer/Category/ItemCollectionProvider.php @@ -9,6 +9,9 @@ use Magento\Catalog\Model\Layer\ItemCollectionProviderInterface; use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; +/** + * Catalog search category layer collection provider. + */ class ItemCollectionProvider implements ItemCollectionProviderInterface { /** @@ -25,8 +28,7 @@ public function __construct(CollectionFactory $collectionFactory) } /** - * @param \Magento\Catalog\Model\Category $category - * @return \Magento\Catalog\Model\ResourceModel\Product\Collection + * @inheritdoc */ public function getCollection(\Magento\Catalog\Model\Category $category) { diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php index 7aac6e98fc04..794d0ac97153 100644 --- a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php +++ b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php @@ -91,12 +91,10 @@ protected function _getItemsData() return $this->itemDataBuilder->build(); } - $productSize = $productCollection->getSize(); - $options = $attribute->getFrontend() ->getSelectOptions(); foreach ($options as $option) { - $this->buildOptionData($option, $isAttributeFilterable, $optionsFacetedData, $productSize); + $this->buildOptionData($option, $isAttributeFilterable, $optionsFacetedData); } return $this->itemDataBuilder->build(); @@ -108,17 +106,16 @@ protected function _getItemsData() * @param array $option * @param boolean $isAttributeFilterable * @param array $optionsFacetedData - * @param int $productSize * @return void */ - private function buildOptionData($option, $isAttributeFilterable, $optionsFacetedData, $productSize) + private function buildOptionData($option, $isAttributeFilterable, $optionsFacetedData) { $value = $this->getOptionValue($option); if ($value === false) { return; } $count = $this->getOptionCount($value, $optionsFacetedData); - if ($isAttributeFilterable && (!$this->isOptionReducesResults($count, $productSize) || $count === 0)) { + if ($isAttributeFilterable && $count === 0) { return; } @@ -156,4 +153,12 @@ private function getOptionCount($value, $optionsFacetedData) ? (int)$optionsFacetedData[$value]['count'] : 0; } + + /** + * @inheritdoc + */ + protected function isOptionReducesResults($optionCount, $totalSize) + { + return $optionCount <= $totalSize; + } } diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Category.php b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Category.php index 7c15514f211d..0998cf7a9b3a 100644 --- a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Category.php +++ b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Category.php @@ -24,14 +24,16 @@ class Category extends AbstractFilter private $dataProvider; /** + * Category constructor. + * * @param \Magento\Catalog\Model\Layer\Filter\ItemFactory $filterItemFactory * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Catalog\Model\Layer $layer * @param \Magento\Catalog\Model\Layer\Filter\Item\DataBuilder $itemDataBuilder - * @param \Magento\Catalog\Model\CategoryFactory $categoryFactory * @param \Magento\Framework\Escaper $escaper - * @param CategoryManagerFactory $categoryManager + * @param \Magento\Catalog\Model\Layer\Filter\DataProvider\CategoryFactory $categoryDataProviderFactory * @param array $data + * @throws \Magento\Framework\Exception\LocalizedException */ public function __construct( \Magento\Catalog\Model\Layer\Filter\ItemFactory $filterItemFactory, diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Decimal.php b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Decimal.php index e61a886a41d6..e9fb1070fedd 100644 --- a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Decimal.php +++ b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Decimal.php @@ -111,12 +111,9 @@ protected function _getItemsData() $from = ''; } if ($to == '*') { - $to = ''; + $to = null; } - $label = $this->renderRangeLabel( - empty($from) ? 0 : $from, - empty($to) ? 0 : $to - ); + $label = $this->renderRangeLabel(empty($from) ? 0 : $from, $to); $value = $from . '-' . $to; $data[] = [ @@ -141,7 +138,7 @@ protected function _getItemsData() protected function renderRangeLabel($fromPrice, $toPrice) { $formattedFromPrice = $this->priceCurrency->format($fromPrice); - if ($toPrice === '') { + if ($toPrice === null) { return __('%1 and above', $formattedFromPrice); } else { if ($fromPrice != $toPrice) { diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php index 108f1b9f4fd8..a19f53469ae0 100644 --- a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php +++ b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php @@ -86,6 +86,8 @@ public function __construct( } /** + * Get resource model. + * * @return \Magento\Catalog\Model\ResourceModel\Layer\Filter\Price */ public function getResource() @@ -223,6 +225,8 @@ protected function _getItemsData() } /** + * Get 'to' part of the filter. + * * @param float $from * @return float */ @@ -237,6 +241,8 @@ protected function getTo($from) } /** + * Get 'from' part of the filter. + * * @param float $from * @return float */ @@ -251,6 +257,8 @@ protected function getFrom($from) } /** + * Prepare filter data. + * * @param string $key * @param int $count * @return array diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Search/Plugin/CollectionFilter.php b/app/code/Magento/CatalogSearch/Model/Layer/Search/Plugin/CollectionFilter.php index 4ffd8ff4ba5e..eb901498c4ea 100644 --- a/app/code/Magento/CatalogSearch/Model/Layer/Search/Plugin/CollectionFilter.php +++ b/app/code/Magento/CatalogSearch/Model/Layer/Search/Plugin/CollectionFilter.php @@ -9,6 +9,9 @@ use Magento\Catalog\Model\Category; use Magento\Search\Model\QueryFactory; +/** + * Catalog search plugin for search collection filter in layered navigation. + */ class CollectionFilter { /** diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Search/StateKey.php b/app/code/Magento/CatalogSearch/Model/Layer/Search/StateKey.php index 4f14b7daba1d..98caccea2ae4 100644 --- a/app/code/Magento/CatalogSearch/Model/Layer/Search/StateKey.php +++ b/app/code/Magento/CatalogSearch/Model/Layer/Search/StateKey.php @@ -9,6 +9,9 @@ use Magento\Catalog\Model\Layer\StateKeyInterface; +/** + * Catalog search state key for layered navigation. + */ class StateKey extends \Magento\Catalog\Model\Layer\Category\StateKey implements StateKeyInterface { /** @@ -31,8 +34,7 @@ public function __construct( } /** - * @param \Magento\Catalog\Model\Category $category - * @return string|void + * @inheritdoc */ public function toString($category) { diff --git a/app/code/Magento/CatalogSearch/Model/Price/Interval.php b/app/code/Magento/CatalogSearch/Model/Price/Interval.php index db1d550c3724..ea2d24aeadfd 100644 --- a/app/code/Magento/CatalogSearch/Model/Price/Interval.php +++ b/app/code/Magento/CatalogSearch/Model/Price/Interval.php @@ -7,6 +7,9 @@ use Magento\Framework\Search\Dynamic\IntervalInterface; +/** + * Catalog search price interval. + */ class Interval implements IntervalInterface { /** @@ -23,7 +26,7 @@ public function __construct(\Magento\Catalog\Model\ResourceModel\Layer\Filter\Pr } /** - * {@inheritdoc} + * @inheritdoc */ public function load($limit, $offset = null, $lower = null, $upper = null) { @@ -32,7 +35,7 @@ public function load($limit, $offset = null, $lower = null, $upper = null) } /** - * {@inheritdoc} + * @inheritdoc */ public function loadPrevious($data, $index, $lower = null) { @@ -41,7 +44,7 @@ public function loadPrevious($data, $index, $lower = null) } /** - * {@inheritdoc} + * @inheritdoc */ public function loadNext($data, $rightIndex, $upper = null) { @@ -50,6 +53,8 @@ public function loadNext($data, $rightIndex, $upper = null) } /** + * Convert to float values. + * * @param array $prices * @return array */ diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced.php index 8a5a6372bb22..05254a50aadc 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced.php @@ -8,7 +8,6 @@ /** * Advanced Catalog Search resource model * - * @author Magento Core Team <core@magentocommerce.com> * @api * @since 100.0.2 */ diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php index 57896ba5a79f..7791dc761ae3 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php @@ -3,23 +3,36 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\CatalogSearch\Model\ResourceModel\Advanced; use Magento\Catalog\Model\Product; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverInterface; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierInterface; +use Magento\Framework\Search\EngineResolverInterface; +use Magento\Search\Model\EngineResolver; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverInterface; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverFactory; use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\DB\Select; use Magento\Framework\Api\Search\SearchCriteriaBuilder; use Magento\Framework\Api\Search\SearchResultFactory; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\Search\Adapter\Mysql\TemporaryStorage; use Magento\Framework\Search\Request\EmptyRequestDataException; use Magento\Framework\Search\Request\NonExistingRequestNameException; use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverFactory; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierFactory; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Api\Search\SearchResultInterface; /** - * Collection Advanced + * Advanced search collection + * + * This collection should be refactored to not have dependencies on MySQL-specific implementation. * - * @author Magento Core Team <core@magentocommerce.com> + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api * @since 100.0.2 @@ -39,6 +52,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection /** * @var \Magento\Framework\Search\Adapter\Mysql\TemporaryStorageFactory + * @deprecated There must be no dependencies on specific adapter in generic search implementation */ private $temporaryStorageFactory; @@ -57,6 +71,41 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection */ private $filterBuilder; + /** + * @var \Magento\Framework\Api\Search\SearchResultInterface + */ + private $searchResult; + + /** + * @var string + */ + private $searchRequestName; + + /** + * @var SearchCriteriaResolverFactory + */ + private $searchCriteriaResolverFactory; + + /** + * @var SearchResultApplierFactory + */ + private $searchResultApplierFactory; + + /** + * @var TotalRecordsResolverFactory + */ + private $totalRecordsResolverFactory; + + /** + * @var EngineResolverInterface + */ + private $engineResolver; + + /** + * @var array + */ + private $searchOrders; + /** * Collection constructor * @@ -86,7 +135,11 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection * @param SearchResultFactory|null $searchResultFactory * @param ProductLimitationFactory|null $productLimitationFactory * @param MetadataPool|null $metadataPool - * + * @param string $searchRequestName + * @param SearchCriteriaResolverFactory|null $searchCriteriaResolverFactory + * @param SearchResultApplierFactory|null $searchResultApplierFactory + * @param TotalRecordsResolverFactory|null $totalRecordsResolverFactory + * @param EngineResolverInterface|null $engineResolver * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -115,15 +168,29 @@ public function __construct( \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, SearchResultFactory $searchResultFactory = null, ProductLimitationFactory $productLimitationFactory = null, - MetadataPool $metadataPool = null + MetadataPool $metadataPool = null, + $searchRequestName = 'advanced_search_container', + SearchCriteriaResolverFactory $searchCriteriaResolverFactory = null, + SearchResultApplierFactory $searchResultApplierFactory = null, + TotalRecordsResolverFactory $totalRecordsResolverFactory = null, + EngineResolverInterface $engineResolver = null ) { $this->requestBuilder = $requestBuilder; $this->searchEngine = $searchEngine; $this->temporaryStorageFactory = $temporaryStorageFactory; + $this->searchRequestName = $searchRequestName; if ($searchResultFactory === null) { $this->searchResultFactory = \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Framework\Api\Search\SearchResultFactory::class); } + $this->searchCriteriaResolverFactory = $searchCriteriaResolverFactory ?: ObjectManager::getInstance() + ->get(SearchCriteriaResolverFactory::class); + $this->searchResultApplierFactory = $searchResultApplierFactory ?: ObjectManager::getInstance() + ->get(SearchResultApplierFactory::class); + $this->totalRecordsResolverFactory = $totalRecordsResolverFactory ?: ObjectManager::getInstance() + ->get(TotalRecordsResolverFactory::class); + $this->engineResolver = $engineResolver ?: ObjectManager::getInstance() + ->get(EngineResolverInterface::class); parent::__construct( $entityFactory, $logger, @@ -165,11 +232,89 @@ public function addFieldsToFilter($fields) return $this; } + /** + * @inheritdoc + */ + public function setOrder($attribute, $dir = Select::SQL_DESC) + { + $this->setSearchOrder($attribute, $dir); + if ($this->isCurrentEngineMysql()) { + parent::setOrder($attribute, $dir); + } + + return $this; + } + + /** + * @inheritdoc + */ + public function addCategoryFilter(\Magento\Catalog\Model\Category $category) + { + /** + * This changes need in backward compatible reasons for support dynamic improved algorithm + * for price aggregation process. + */ + if ($this->isCurrentEngineMysql()) { + parent::addCategoryFilter($category); + } else { + $this->addFieldToFilter('category_ids', $category->getId()); + $this->_productLimitationPrice(); + } + + return $this; + } + + /** + * @inheritdoc + */ + public function setVisibility($visibility) + { + /** + * This changes need in backward compatible reasons for support dynamic improved algorithm + * for price aggregation process. + */ + if ($this->isCurrentEngineMysql()) { + parent::setVisibility($visibility); + } else { + $this->addFieldToFilter('visibility', $visibility); + } + + return $this; + } + + /** + * Set sort order for search query. + * + * @param string $field + * @param string $direction + * @return void + */ + private function setSearchOrder($field, $direction) + { + $field = (string)$this->_getMappedField($field); + $direction = strtoupper($direction) == self::SORT_ORDER_ASC ? self::SORT_ORDER_ASC : self::SORT_ORDER_DESC; + + $this->searchOrders[$field] = $direction; + } + + /** + * Check if current engine is MYSQL. + * + * @return bool + */ + private function isCurrentEngineMysql() + { + return $this->engineResolver->getCurrentSearchEngine() === EngineResolver::CATALOG_SEARCH_MYSQL_ENGINE; + } + /** * @inheritdoc */ protected function _renderFiltersBefore() { + if ($this->isLoaded()) { + return; + } if ($this->filters) { foreach ($this->filters as $attributes) { foreach ($attributes as $attributeCode => $attributeValue) { @@ -177,34 +322,73 @@ 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. + * * @param string $attributeCode * @return string */ @@ -264,6 +448,8 @@ private function addRangeAttributeToSearch($attributeCode, $attributeValue) } /** + * Get search. + * * @return \Magento\Search\Api\SearchInterface */ private function getSearch() @@ -276,6 +462,8 @@ private function getSearch() } /** + * Get search criteria builder. + * * @return SearchCriteriaBuilder */ private function getSearchCriteriaBuilder() @@ -288,6 +476,8 @@ private function getSearchCriteriaBuilder() } /** + * Get filter builder. + * * @return FilterBuilder */ private function getFilterBuilder() diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Engine.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Engine.php index fac8c4d2a47f..93ae2c94e210 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Engine.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Engine.php @@ -7,6 +7,9 @@ /** * CatalogSearch Fulltext Index Engine resource model + * + * @deprecated + * @see \Magento\ElasticSearch */ class Engine implements EngineInterface { @@ -107,7 +110,9 @@ public function processAttributeValue($attribute, $value) && in_array($attribute->getFrontendInput(), ['text', 'textarea']) ) { $result = $value; - } elseif ($this->isTermFilterableAttribute($attribute)) { + } elseif ($this->isTermFilterableAttribute($attribute) + || ($attribute->getIsSearchable() && in_array($attribute->getFrontendInput(), ['select', 'multiselect'])) + ) { $result = ''; } @@ -116,6 +121,7 @@ public function processAttributeValue($attribute, $value) /** * Prepare index array as a string glued by separator + * * Support 2 level array gluing * * @param array $index diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/EngineInterface.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/EngineInterface.php index 0182b09bcacf..4b9db55105ea 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/EngineInterface.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/EngineInterface.php @@ -3,13 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -/** - * CatalogSearch Index Engine Interface - */ namespace Magento\CatalogSearch\Model\ResourceModel; /** + * CatalogSearch Index Engine Interface + * * @api * @since 100.0.2 */ diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/EngineProvider.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/EngineProvider.php index aa883aeb842e..d1259159606d 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/EngineProvider.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/EngineProvider.php @@ -4,15 +4,13 @@ * See COPYING.txt for license details. */ -/** - * Catalog Search engine provider - */ namespace Magento\CatalogSearch\Model\ResourceModel; -use Magento\CatalogSearch\Model\ResourceModel\EngineInterface; use Magento\Framework\Search\EngineResolverInterface; /** + * Catalog Search engine provider + * * @api * @since 100.0.2 */ diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php index 0408957e511b..59f6cd1c6e7e 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php @@ -3,13 +3,22 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\CatalogSearch\Model\ResourceModel\Fulltext; -use Magento\CatalogSearch\Model\Search\RequestGenerator; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverInterface; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverFactory; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverInterface; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverFactory; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierFactory; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierInterface; +use Magento\Framework\Search\EngineResolverInterface; +use Magento\Framework\Data\Collection\Db\SizeResolverInterfaceFactory; use Magento\Framework\DB\Select; +use Magento\Framework\Api\Search\SearchResultInterface; +use Magento\CatalogSearch\Model\Search\RequestGenerator; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Exception\StateException; -use Magento\Framework\Search\Adapter\Mysql\TemporaryStorage; use Magento\Framework\Search\Response\QueryResponse; use Magento\Framework\Search\Request\EmptyRequestDataException; use Magento\Framework\Search\Request\NonExistingRequestNameException; @@ -17,13 +26,18 @@ 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 - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * + * This collection should be refactored to not have dependencies on MySQL-specific implementation. * * @api * @since 100.0.2 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyFields) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection { @@ -58,11 +72,6 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection */ private $queryText; - /** - * @var string|null - */ - private $relevanceOrderDirection = null; - /** * @var string */ @@ -70,6 +79,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection /** * @var \Magento\Framework\Search\Adapter\Mysql\TemporaryStorageFactory + * @deprecated There must be no dependencies on specific adapter in generic search implementation */ private $temporaryStorageFactory; @@ -98,6 +108,31 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection */ private $filterBuilder; + /** + * @var SearchCriteriaResolverFactory + */ + private $searchCriteriaResolverFactory; + + /** + * @var SearchResultApplierFactory + */ + private $searchResultApplierFactory; + + /** + * @var TotalRecordsResolverFactory + */ + private $totalRecordsResolverFactory; + + /** + * @var EngineResolverInterface + */ + private $engineResolver; + + /** + * @var array + */ + private $searchOrders; + /** * Collection constructor * @@ -129,8 +164,15 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection * @param SearchResultFactory|null $searchResultFactory * @param ProductLimitationFactory|null $productLimitationFactory * @param MetadataPool|null $metadataPool - * + * @param \Magento\Search\Api\SearchInterface|null $search + * @param \Magento\Framework\Api\Search\SearchCriteriaBuilder|null $searchCriteriaBuilder + * @param \Magento\Framework\Api\FilterBuilder|null $filterBuilder + * @param SearchCriteriaResolverFactory|null $searchCriteriaResolverFactory + * @param SearchResultApplierFactory|null $searchResultApplierFactory + * @param TotalRecordsResolverFactory|null $totalRecordsResolverFactory + * @param EngineResolverInterface|null $engineResolver * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * @SuppressWarnings(PHPMD.NPathComplexity) */ public function __construct( \Magento\Framework\Data\Collection\EntityFactory $entityFactory, @@ -160,7 +202,14 @@ public function __construct( $searchRequestName = 'catalog_view_container', SearchResultFactory $searchResultFactory = null, ProductLimitationFactory $productLimitationFactory = null, - MetadataPool $metadataPool = null + MetadataPool $metadataPool = null, + \Magento\Search\Api\SearchInterface $search = null, + \Magento\Framework\Api\Search\SearchCriteriaBuilder $searchCriteriaBuilder = null, + \Magento\Framework\Api\FilterBuilder $filterBuilder = null, + SearchCriteriaResolverFactory $searchCriteriaResolverFactory = null, + SearchResultApplierFactory $searchResultApplierFactory = null, + TotalRecordsResolverFactory $totalRecordsResolverFactory = null, + EngineResolverInterface $engineResolver = null ) { $this->queryFactory = $catalogSearchData; if ($searchResultFactory === null) { @@ -195,9 +244,24 @@ public function __construct( $this->searchEngine = $searchEngine; $this->temporaryStorageFactory = $temporaryStorageFactory; $this->searchRequestName = $searchRequestName; + $this->search = $search ?: ObjectManager::getInstance()->get(\Magento\Search\Api\SearchInterface::class); + $this->searchCriteriaBuilder = $searchCriteriaBuilder ?: ObjectManager::getInstance() + ->get(\Magento\Framework\Api\Search\SearchCriteriaBuilder::class); + $this->filterBuilder = $filterBuilder ?: ObjectManager::getInstance() + ->get(\Magento\Framework\Api\FilterBuilder::class); + $this->searchCriteriaResolverFactory = $searchCriteriaResolverFactory ?: ObjectManager::getInstance() + ->get(SearchCriteriaResolverFactory::class); + $this->searchResultApplierFactory = $searchResultApplierFactory ?: ObjectManager::getInstance() + ->get(SearchResultApplierFactory::class); + $this->totalRecordsResolverFactory = $totalRecordsResolverFactory ?: ObjectManager::getInstance() + ->get(TotalRecordsResolverFactory::class); + $this->engineResolver = $engineResolver ?: ObjectManager::getInstance() + ->get(EngineResolverInterface::class); } /** + * Get search. + * * @deprecated 100.1.0 * @return \Magento\Search\Api\SearchInterface */ @@ -210,6 +274,8 @@ private function getSearch() } /** + * Test search. + * * @deprecated 100.1.0 * @param \Magento\Search\Api\SearchInterface $object * @return void @@ -221,6 +287,8 @@ public function setSearch(\Magento\Search\Api\SearchInterface $object) } /** + * Set search criteria builder. + * * @deprecated 100.1.0 * @return \Magento\Framework\Api\Search\SearchCriteriaBuilder */ @@ -234,6 +302,8 @@ private function getSearchCriteriaBuilder() } /** + * Set search criteria builder. + * * @deprecated 100.1.0 * @param \Magento\Framework\Api\Search\SearchCriteriaBuilder $object * @return void @@ -245,6 +315,8 @@ public function setSearchCriteriaBuilder(\Magento\Framework\Api\Search\SearchCri } /** + * Get filter builder. + * * @deprecated 100.1.0 * @return \Magento\Framework\Api\FilterBuilder */ @@ -257,6 +329,8 @@ private function getFilterBuilder() } /** + * Set filter builder. + * * @deprecated 100.1.0 * @param \Magento\Framework\Api\FilterBuilder $object * @return void @@ -271,7 +345,7 @@ public function setFilterBuilder(\Magento\Framework\Api\FilterBuilder $object) * Apply attribute filter to facet collection * * @param string $field - * @param null $condition + * @param mixed|null $condition * @return $this */ public function addFieldToFilter($field, $condition = null) @@ -316,32 +390,32 @@ public function addSearchFilter($query) /** * @inheritdoc */ - protected function _renderFiltersBefore() + public function setOrder($attribute, $dir = Select::SQL_DESC) { - $this->getSearchCriteriaBuilder(); - $this->getFilterBuilder(); - $this->getSearch(); - - if ($this->queryText) { - $this->filterBuilder->setField('search_term'); - $this->filterBuilder->setValue($this->queryText); - $this->searchCriteriaBuilder->addFilter($this->filterBuilder->create()); + $this->setSearchOrder($attribute, $dir); + if ($this->isCurrentEngineMysql()) { + parent::setOrder($attribute, $dir); } - $priceRangeCalculation = $this->_scopeConfig->getValue( - \Magento\Catalog\Model\Layer\Filter\Dynamic\AlgorithmFactory::XML_PATH_RANGE_CALCULATION, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ); - if ($priceRangeCalculation) { - $this->filterBuilder->setField('price_dynamic_algorithm'); - $this->filterBuilder->setValue($priceRangeCalculation); - $this->searchCriteriaBuilder->addFilter($this->filterBuilder->create()); + return $this; + } + + /** + * @inheritdoc + */ + protected function _renderFiltersBefore() + { + if ($this->isLoaded()) { + return; } - $searchCriteria = $this->searchCriteriaBuilder->create(); - $searchCriteria->setRequestName($this->searchRequestName); + $this->prepareSearchTermFilter(); + $this->preparePriceAggregation(); + + $searchCriteria = $this->getSearchCriteriaResolver()->resolve(); try { $this->searchResult = $this->getSearch()->search($searchCriteria); + $this->_totalRecords = $this->getTotalRecordsResolver($this->searchResult)->resolve(); } catch (EmptyRequestDataException $e) { /** @var \Magento\Framework\Api\Search\SearchResultInterface $searchResult */ $this->searchResult = $this->searchResultFactory->create()->setItems([]); @@ -350,23 +424,79 @@ protected function _renderFiltersBefore() throw new LocalizedException(__('An error occurred. For details, see the error log.')); } - $temporaryStorage = $this->temporaryStorageFactory->create(); - $table = $temporaryStorage->storeApiDocuments($this->searchResult->getItems()); + $this->getSearchResultApplier($this->searchResult)->apply(); + parent::_renderFiltersBefore(); + } - $this->getSelect()->joinInner( - [ - 'search_result' => $table->getName(), - ], - 'e.entity_id = search_result.' . TemporaryStorage::FIELD_ENTITY_ID, - [] - ); + /** + * Set sort order for search query. + * + * @param string $field + * @param string $direction + * @return void + */ + private function setSearchOrder($field, $direction) + { + $field = (string)$this->_getMappedField($field); + $direction = strtoupper($direction) == self::SORT_ORDER_ASC ? self::SORT_ORDER_ASC : self::SORT_ORDER_DESC; - if ($this->relevanceOrderDirection) { - $this->getSelect()->order( - 'search_result.'. TemporaryStorage::FIELD_SCORE . ' ' . $this->relevanceOrderDirection - ); - } - return parent::_renderFiltersBefore(); + $this->searchOrders[$field] = $direction; + } + + /** + * Check if current engine is MYSQL. + * + * @return bool + */ + private function isCurrentEngineMysql() + { + return $this->engineResolver->getCurrentSearchEngine() === EngineResolver::CATALOG_SEARCH_MYSQL_ENGINE; + } + + /** + * Get total records resolver. + * + * @param SearchResultInterface $searchResult + * @return TotalRecordsResolverInterface + */ + private function getTotalRecordsResolver(SearchResultInterface $searchResult): TotalRecordsResolverInterface + { + return $this->totalRecordsResolverFactory->create([ + 'searchResult' => $searchResult, + ]); + } + + /** + * Get search criteria resolver. + * + * @return SearchCriteriaResolverInterface + */ + private function getSearchCriteriaResolver(): SearchCriteriaResolverInterface + { + return $this->searchCriteriaResolverFactory->create([ + 'builder' => $this->getSearchCriteriaBuilder(), + 'collection' => $this, + 'searchRequestName' => $this->searchRequestName, + 'currentPage' => $this->_curPage, + 'size' => $this->getPageSize(), + 'orders' => $this->searchOrders, + ]); + } + + /** + * Get search result applier. + * + * @param SearchResultInterface $searchResult + * @return SearchResultApplierInterface + */ + private function getSearchResultApplier(SearchResultInterface $searchResult): SearchResultApplierInterface + { + return $this->searchResultApplierFactory->create([ + 'collection' => $this, + 'searchResult' => $searchResult, + /** This variable sets by serOrder method, but doesn't have a getter method. */ + 'orders' => $this->_orders, + ]); } /** @@ -384,6 +514,8 @@ protected function _beforeLoad() } /** + * Render filters. + * * @return $this */ protected function _renderFilters() @@ -392,24 +524,6 @@ protected function _renderFilters() return parent::_renderFilters(); } - /** - * Set Order field - * - * @param string $attribute - * @param string $dir - * @return $this - */ - public function setOrder($attribute, $dir = Select::SQL_DESC) - { - if ($attribute === 'relevance') { - $this->relevanceOrderDirection = $dir; - } else { - parent::setOrder($attribute, $dir); - } - - return $this; - } - /** * Stub method for compatibility with other search engines * @@ -456,7 +570,17 @@ public function getFacetedData($field) public function addCategoryFilter(\Magento\Catalog\Model\Category $category) { $this->addFieldToFilter('category_ids', $category->getId()); - return parent::addCategoryFilter($category); + /** + * This changes need in backward compatible reasons for support dynamic improved algorithm + * for price aggregation process. + */ + if ($this->isCurrentEngineMysql()) { + parent::addCategoryFilter($category); + } else { + $this->_productLimitationPrice(); + } + + return $this; } /** @@ -468,6 +592,46 @@ public function addCategoryFilter(\Magento\Catalog\Model\Category $category) public function setVisibility($visibility) { $this->addFieldToFilter('visibility', $visibility); - return parent::setVisibility($visibility); + /** + * This changes need in backward compatible reasons for support dynamic improved algorithm + * for price aggregation process. + */ + if ($this->isCurrentEngineMysql()) { + parent::setVisibility($visibility); + } + + return $this; + } + + /** + * Prepare search term filter for text query. + * + * @return void + */ + private function prepareSearchTermFilter(): void + { + if ($this->queryText) { + $this->filterBuilder->setField('search_term'); + $this->filterBuilder->setValue($this->queryText); + $this->searchCriteriaBuilder->addFilter($this->filterBuilder->create()); + } + } + + /** + * Prepare price aggregation algorithm. + * + * @return void + */ + private function preparePriceAggregation(): void + { + $priceRangeCalculation = $this->_scopeConfig->getValue( + \Magento\Catalog\Model\Layer\Filter\Dynamic\AlgorithmFactory::XML_PATH_RANGE_CALCULATION, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + if ($priceRangeCalculation) { + $this->filterBuilder->setField('price_dynamic_algorithm'); + $this->filterBuilder->setValue($priceRangeCalculation); + $this->searchCriteriaBuilder->addFilter($this->filterBuilder->create()); + } } } diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolver.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolver.php new file mode 100644 index 000000000000..632e1ab9270d --- /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 000000000000..047fa7f71e40 --- /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 000000000000..20c237646e52 --- /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 000000000000..1b3e2a6bbac7 --- /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 000000000000..12b6f8131391 --- /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 000000000000..190450f9606b --- /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 b958de91314f..e625ccbe51fe 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php @@ -10,6 +10,7 @@ * Search collection * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @api * @since 100.0.2 */ @@ -60,7 +61,6 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection * @param \Magento\Customer\Api\GroupManagementInterface $groupManagement * @param \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory $attributeCollectionFactory * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection - * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -269,6 +269,7 @@ protected function _getSearchEntityIdsSql($query, $searchOnlyInCurrentStore = tr $sql = $this->_getSearchInOptionSql($query); if ($sql) { + // phpcs:ignore Magento2.SQL.RawQuery $selects[] = "SELECT * FROM ({$sql}) AS inoptionsql"; // inherent unions may be inside } diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Setup/PropertyMapper.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Setup/PropertyMapper.php new file mode 100644 index 000000000000..f72e1536f471 --- /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/BaseSelectStrategy/BaseSelectStrategyInterface.php b/app/code/Magento/CatalogSearch/Model/Search/BaseSelectStrategy/BaseSelectStrategyInterface.php index c6495015fee4..2d8dfb922249 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/BaseSelectStrategy/BaseSelectStrategyInterface.php +++ b/app/code/Magento/CatalogSearch/Model/Search/BaseSelectStrategy/BaseSelectStrategyInterface.php @@ -8,8 +8,10 @@ use Magento\CatalogSearch\Model\Search\SelectContainer\SelectContainer; /** - * Interface BaseSelectStrategyInterface * This interface represents strategy that will be used to create base select for search request + * + * @deprecated + * @see \Magento\ElasticSearch */ interface BaseSelectStrategyInterface { diff --git a/app/code/Magento/CatalogSearch/Model/Search/BaseSelectStrategy/StrategyMapper.php b/app/code/Magento/CatalogSearch/Model/Search/BaseSelectStrategy/StrategyMapper.php index 65759685c2b6..e554d3c774a3 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/BaseSelectStrategy/StrategyMapper.php +++ b/app/code/Magento/CatalogSearch/Model/Search/BaseSelectStrategy/StrategyMapper.php @@ -10,8 +10,10 @@ use Magento\CatalogSearch\Model\Adapter\Mysql\BaseSelectStrategy\BaseSelectAttributesSearchStrategy; /** - * Class StrategyMapper * This class is responsible for deciding which BaseSelectStrategyInterface should be used for passed SelectContainer + * + * @deprecated + * @see \Magento\ElasticSearch */ class StrategyMapper { diff --git a/app/code/Magento/CatalogSearch/Model/Search/Catalog.php b/app/code/Magento/CatalogSearch/Model/Search/Catalog.php index 4572336d761e..31ac889b19e6 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/Catalog.php +++ b/app/code/Magento/CatalogSearch/Model/Search/Catalog.php @@ -9,8 +9,6 @@ /** * Search model for backend search - * - * @deprecated 100.2.0 */ class Catalog extends \Magento\Framework\DataObject { diff --git a/app/code/Magento/CatalogSearch/Model/Search/CustomAttributeFilterCheck.php b/app/code/Magento/CatalogSearch/Model/Search/CustomAttributeFilterCheck.php index 98bf1e5984a7..657c8540d7c6 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/CustomAttributeFilterCheck.php +++ b/app/code/Magento/CatalogSearch/Model/Search/CustomAttributeFilterCheck.php @@ -10,8 +10,10 @@ use Magento\Catalog\Model\Product; /** - * Class CustomAttributeFilterSelector * Checks if FilterInterface is by custom attribute + * + * @deprecated + * @see \Magento\ElasticSearch */ class CustomAttributeFilterCheck { @@ -42,7 +44,7 @@ public function isCustom(FilterInterface $filter) return $attribute && $filter->getType() === FilterInterface::TYPE_TERM - && in_array($attribute->getFrontendInput(), ['select', 'multiselect'], true); + && in_array($attribute->getFrontendInput(), ['select', 'multiselect', 'boolean'], true); } /** diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/CustomAttributeFilter.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/CustomAttributeFilter.php index fc93b86f5da5..8c796f877065 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/CustomAttributeFilter.php +++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/CustomAttributeFilter.php @@ -16,8 +16,10 @@ use Magento\Catalog\Model\Product; /** - * Class CustomAttributeFilter - * Applies filters by custom attributes to base select + * Applies filters by custom attributes to base select. + * + * @deprecated + * @see \Magento\ElasticSearch */ class CustomAttributeFilter { @@ -71,13 +73,13 @@ public function __construct( * Applies filters by custom attributes to base select * * @param Select $select - * @param FilterInterface[] ...$filters + * @param FilterInterface[] $filters * @return Select * @throws \Magento\Framework\Exception\LocalizedException * @throws \InvalidArgumentException * @throws \DomainException */ - public function apply(Select $select, FilterInterface ... $filters) + public function apply(Select $select, FilterInterface ...$filters) { $select = clone $select; $mainTableAlias = $this->extractTableAliasFromSelect($select); @@ -141,7 +143,6 @@ private function getJoinConditions($attrId, $mainTable, $joinTable) { return [ sprintf('`%s`.`entity_id` = `%s`.`entity_id`', $mainTable, $joinTable), - sprintf('`%s`.`source_id` = `%s`.`source_id`', $mainTable, $joinTable), $this->conditionManager->generateCondition( sprintf('%s.attribute_id', $joinTable), '=', diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/DimensionsProcessor.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/DimensionsProcessor.php index 67f427d4be47..3d2b9eed0376 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/DimensionsProcessor.php +++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/DimensionsProcessor.php @@ -16,6 +16,9 @@ /** * Class DimensionsProcessor * Adds dimension conditions to select query + * + * @deprecated + * @see \Magento\ElasticSearch */ class DimensionsProcessor { diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/ExclusionStrategy.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/ExclusionStrategy.php index 512dd69aad95..c382569338e2 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/ExclusionStrategy.php +++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/ExclusionStrategy.php @@ -21,6 +21,9 @@ /** * Strategy which processes exclusions from general rules * + * @deprecated + * @see \Magento\ElasticSearch + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ExclusionStrategy implements FilterStrategyInterface diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterContext.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterContext.php index 0c8233316338..67ed66da2a03 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterContext.php +++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterContext.php @@ -13,7 +13,10 @@ /** * FilterContext represents a Context of the Strategy pattern * Its responsibility is to choose appropriate strategy to apply passed filter to the Select + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @deprecated + * @see \Magento\ElasticSearch */ class FilterContext implements FilterStrategyInterface { diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterMapper.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterMapper.php index 27128327554d..7136fad5b19a 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterMapper.php +++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterMapper.php @@ -13,6 +13,9 @@ /** * Class FilterMapper * This class applies filters to Select based on SelectContainer configuration + * + * @deprecated + * @see \Magento\ElasticSearch */ class FilterMapper { diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterStrategyInterface.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterStrategyInterface.php index b59f7eeec977..a61c691c0d5c 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterStrategyInterface.php +++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterStrategyInterface.php @@ -10,6 +10,8 @@ * FilterStrategyInterface provides the interface to work with strategies * @api * @since 100.1.6 + * @deprecated + * @see \Magento\ElasticSearch */ interface FilterStrategyInterface { diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/StaticAttributeStrategy.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/StaticAttributeStrategy.php index 8544b463dbb1..3986cc617f06 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/StaticAttributeStrategy.php +++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/StaticAttributeStrategy.php @@ -12,6 +12,9 @@ /** * This strategy handles static attributes + * + * @deprecated + * @see \Magento\ElasticSearch */ class StaticAttributeStrategy implements FilterStrategyInterface { diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/StockStatusFilter.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/StockStatusFilter.php index 1cd9d1a3dd77..0e3ba0d4e669 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/StockStatusFilter.php +++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/StockStatusFilter.php @@ -15,6 +15,9 @@ /** * Class StockStatusFilter * Adds filter by stock status to base select + * + * @deprecated + * @see \Magento\ElasticSearch */ class StockStatusFilter { diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/TermDropdownStrategy.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/TermDropdownStrategy.php index 6b5dce5fde4e..9d7e31ee3b6d 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/TermDropdownStrategy.php +++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/TermDropdownStrategy.php @@ -15,6 +15,9 @@ * This strategy handles attributes which comply with two criteria: * - The filter for dropdown or multi-select attribute * - The filter is Term filter + * + * @deprecated + * @see \Magento\ElasticSearch */ class TermDropdownStrategy implements FilterStrategyInterface { diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/TermDropdownStrategy/ApplyStockConditionToSelect.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/TermDropdownStrategy/ApplyStockConditionToSelect.php index dee8b09a051e..c28bc3485cf4 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/TermDropdownStrategy/ApplyStockConditionToSelect.php +++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/TermDropdownStrategy/ApplyStockConditionToSelect.php @@ -13,6 +13,9 @@ /** * Apply stock condition to select. + * + * @deprecated + * @see \Magento\ElasticSearch */ class ApplyStockConditionToSelect { diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/TermDropdownStrategy/SelectBuilder.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/TermDropdownStrategy/SelectBuilder.php index fccbfa364d89..007647db39b3 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/TermDropdownStrategy/SelectBuilder.php +++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/TermDropdownStrategy/SelectBuilder.php @@ -16,6 +16,9 @@ /** * Add joins to select. + * + * @deprecated + * @see \Magento\ElasticSearch */ class SelectBuilder { diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/VisibilityFilter.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/VisibilityFilter.php index a615d76bef4d..690ef9115edf 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/VisibilityFilter.php +++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/VisibilityFilter.php @@ -16,6 +16,9 @@ /** * Class VisibilityFilter * Applies filter by visibility to base select + * + * @deprecated + * @see \Magento\ElasticSearch */ class VisibilityFilter { diff --git a/app/code/Magento/CatalogSearch/Model/Search/FiltersExtractor.php b/app/code/Magento/CatalogSearch/Model/Search/FiltersExtractor.php index 53c27eb66f6a..55c858297991 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/FiltersExtractor.php +++ b/app/code/Magento/CatalogSearch/Model/Search/FiltersExtractor.php @@ -12,6 +12,9 @@ /** * Class FiltersExtractor * Extracts filters from QueryInterface + * + * @deprecated + * @see \Magento\ElasticSearch */ class FiltersExtractor { diff --git a/app/code/Magento/CatalogSearch/Model/Search/IndexBuilder.php b/app/code/Magento/CatalogSearch/Model/Search/IndexBuilder.php index 890a0d400014..906220db28dc 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/IndexBuilder.php +++ b/app/code/Magento/CatalogSearch/Model/Search/IndexBuilder.php @@ -26,6 +26,8 @@ /** * Build base Query for Index * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @deprecated + * @see \Magento\ElasticSearch */ class IndexBuilder implements IndexBuilderInterface { 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 000000000000..f621bcbf9183 --- /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 000000000000..db02d5ac5f51 --- /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/Model/Search/QueryChecker/FullTextSearchCheck.php b/app/code/Magento/CatalogSearch/Model/Search/QueryChecker/FullTextSearchCheck.php index f6c2ac0a4f55..a47ea5437520 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/QueryChecker/FullTextSearchCheck.php +++ b/app/code/Magento/CatalogSearch/Model/Search/QueryChecker/FullTextSearchCheck.php @@ -12,6 +12,9 @@ /** * Class is responsible for checking if fulltext search is required for search query + * + * @deprecated + * @see \Magento\ElasticSearch */ class FullTextSearchCheck { @@ -22,7 +25,7 @@ class FullTextSearchCheck * to join catalog_eav_attribute table to search query or not * * In case when the $query does not requires full text search - * - we can skipp joining catalog_eav_attribute table because it becomes excessive + * - we can skip joining catalog_eav_attribute table because it becomes excessive * * @param QueryInterface $query * @return bool @@ -34,6 +37,8 @@ public function isRequiredForQuery(QueryInterface $query) } /** + * Process query + * * @param QueryInterface $query * @return bool * @throws \InvalidArgumentException @@ -59,6 +64,8 @@ private function processQuery(QueryInterface $query) } /** + * Process boolean query + * * @param BoolExpression $query * @return bool * @throws \InvalidArgumentException @@ -87,6 +94,8 @@ private function processBoolQuery(BoolExpression $query) } /** + * Process filter query + * * @param Filter $query * @return bool * @throws \InvalidArgumentException diff --git a/app/code/Magento/CatalogSearch/Model/Search/ReaderPlugin.php b/app/code/Magento/CatalogSearch/Model/Search/ReaderPlugin.php index 8ddc8408959e..916e03f47149 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/ReaderPlugin.php +++ b/app/code/Magento/CatalogSearch/Model/Search/ReaderPlugin.php @@ -5,6 +5,10 @@ */ namespace Magento\CatalogSearch\Model\Search; +/** + * @deprecated + * @see \Magento\ElasticSearch + */ class ReaderPlugin { /** diff --git a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php index 6f6a5eed642e..0adc2fcecbfa 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php +++ b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php @@ -14,6 +14,8 @@ use Magento\Framework\Search\Request\QueryInterface; /** + * Catalog search request generator. + * * @api * @since 100.0.2 */ diff --git a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Decimal.php b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Decimal.php index 3fb3021ff9db..b3d39a48fe9f 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Decimal.php +++ b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Decimal.php @@ -10,10 +10,13 @@ use Magento\Framework\Search\Request\BucketInterface; use Magento\Framework\Search\Request\FilterInterface; +/** + * Catalog search range request generator. + */ class Decimal implements GeneratorInterface { /** - * {@inheritdoc} + * @inheritdoc */ public function getFilterData(Attribute $attribute, $filterName) { @@ -27,7 +30,7 @@ public function getFilterData(Attribute $attribute, $filterName) } /** - * {@inheritdoc} + * @inheritdoc */ public function getAggregationData(Attribute $attribute, $bucketName) { diff --git a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/General.php b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/General.php index f2965bb9f981..63b09de7f08d 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/General.php +++ b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/General.php @@ -10,10 +10,13 @@ use Magento\Framework\Search\Request\BucketInterface; use Magento\Framework\Search\Request\FilterInterface; +/** + * Catalog search request generator. + */ class General implements GeneratorInterface { /** - * {@inheritdoc} + * @inheritdoc */ public function getFilterData(Attribute $attribute, $filterName) { @@ -26,7 +29,7 @@ public function getFilterData(Attribute $attribute, $filterName) } /** - * {@inheritdoc} + * @inheritdoc */ public function getAggregationData(Attribute $attribute, $bucketName) { diff --git a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorInterface.php b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorInterface.php index a7379eaa0bd2..22f829063fbe 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorInterface.php +++ b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorInterface.php @@ -9,13 +9,16 @@ use Magento\Catalog\Model\ResourceModel\Eav\Attribute; /** + * Catalog search reguest generator interface. + * * @api * @since 100.1.6 */ interface GeneratorInterface { /** - * Get filter data for specific attribute + * Get filter data for specific attribute. + * * @param Attribute $attribute * @param string $filterName * @return array @@ -24,7 +27,8 @@ interface GeneratorInterface public function getFilterData(Attribute $attribute, $filterName); /** - * Get aggregation data for specific attribute + * Get aggregation data for specific attribute. + * * @param Attribute $attribute * @param string $bucketName * @return array diff --git a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorResolver.php b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorResolver.php index d2841e5cdf32..68ca546b8191 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorResolver.php +++ b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorResolver.php @@ -9,6 +9,8 @@ /** * @api * @since 100.1.6 + * @deprecated + * @see \Magento\ElasticSearch */ class GeneratorResolver { diff --git a/app/code/Magento/CatalogSearch/Model/Search/SelectContainer/SelectContainer.php b/app/code/Magento/CatalogSearch/Model/Search/SelectContainer/SelectContainer.php index 5305ed276bf3..f0eade4bfbcf 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/SelectContainer/SelectContainer.php +++ b/app/code/Magento/CatalogSearch/Model/Search/SelectContainer/SelectContainer.php @@ -10,8 +10,10 @@ use Magento\Framework\Search\Request\FilterInterface; /** - * Class SelectContainer * This class is a container for all data that is required for creating select query by search request + * + * @deprecated + * @see \Magento\ElasticSearch */ class SelectContainer { diff --git a/app/code/Magento/CatalogSearch/Model/Search/SelectContainer/SelectContainerBuilder.php b/app/code/Magento/CatalogSearch/Model/Search/SelectContainer/SelectContainerBuilder.php index a18ef8e91d7f..d5b7be8bf010 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/SelectContainer/SelectContainerBuilder.php +++ b/app/code/Magento/CatalogSearch/Model/Search/SelectContainer/SelectContainerBuilder.php @@ -18,6 +18,8 @@ * Class SelectContainerBuilder * Class is responsible for SelectContainer creation and filling it with all required data * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @deprecated + * @see \Magento\ElasticSearch */ class SelectContainerBuilder { diff --git a/app/code/Magento/CatalogSearch/Model/Search/TableMapper.php b/app/code/Magento/CatalogSearch/Model/Search/TableMapper.php index eab79c22309f..6b18c4307f51 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/TableMapper.php +++ b/app/code/Magento/CatalogSearch/Model/Search/TableMapper.php @@ -25,6 +25,8 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api * @since 100.0.2 + * @deprecated + * @see \Magento\ElasticSearch */ class TableMapper { diff --git a/app/code/Magento/CatalogSearch/Plugin/EnableEavIndexer.php b/app/code/Magento/CatalogSearch/Plugin/EnableEavIndexer.php index c624f9d1c2e5..956a1b2360f8 100644 --- a/app/code/Magento/CatalogSearch/Plugin/EnableEavIndexer.php +++ b/app/code/Magento/CatalogSearch/Plugin/EnableEavIndexer.php @@ -9,6 +9,9 @@ /** * Enable Product EAV indexer in configuration for MySQL search engine + * + * @deprecated + * @see \Magento\ElasticSearch */ class EnableEavIndexer { diff --git a/app/code/Magento/CatalogSearch/Setup/Patch/Data/MySQLSearchDeprecationNotification.php b/app/code/Magento/CatalogSearch/Setup/Patch/Data/MySQLSearchDeprecationNotification.php new file mode 100644 index 000000000000..8fa9f56d7847 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Setup/Patch/Data/MySQLSearchDeprecationNotification.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogSearch\Setup\Patch\Data; + +/** + * Implementation of the notification about MySQL search being deprecated. + * + * @deprecated + * @see \Magento\ElasticSearch + */ +class MySQLSearchDeprecationNotification implements \Magento\Framework\Setup\Patch\DataPatchInterface +{ + /** + * @var \Magento\Framework\Search\EngineResolverInterface + */ + private $searchEngineResolver; + + /** + * @var \Magento\Framework\Notification\NotifierInterface + */ + private $notifier; + + /** + * @param \Magento\Framework\Search\EngineResolverInterface $searchEngineResolver + * @param \Magento\Framework\Notification\NotifierInterface $notifier + */ + public function __construct( + \Magento\Framework\Search\EngineResolverInterface $searchEngineResolver, + \Magento\Framework\Notification\NotifierInterface $notifier + ) { + $this->searchEngineResolver = $searchEngineResolver; + $this->notifier = $notifier; + } + + /** + * @inheritdoc + */ + public function apply() + { + if ($this->searchEngineResolver->getCurrentSearchEngine() === 'mysql') { + $message = <<<MESSAGE +Catalog Search is currently configured to use the MySQL engine, which has been deprecated. Consider migrating to one of +the Elasticsearch engines now to ensure there are no service interruptions during your next upgrade. +MESSAGE; + + $this->notifier->addNotice(__('Deprecation Notice'), __($message)); + } + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return []; + } +} diff --git a/app/code/Magento/CatalogSearch/Setup/Patch/Data/SetInitialSearchWeightForAttributes.php b/app/code/Magento/CatalogSearch/Setup/Patch/Data/SetInitialSearchWeightForAttributes.php index beff1c66d4f6..7f6dbe033e3a 100644 --- a/app/code/Magento/CatalogSearch/Setup/Patch/Data/SetInitialSearchWeightForAttributes.php +++ b/app/code/Magento/CatalogSearch/Setup/Patch/Data/SetInitialSearchWeightForAttributes.php @@ -13,8 +13,8 @@ use Magento\Catalog\Api\ProductAttributeRepositoryInterface; /** - * Class SetInitialSearchWeightForAttributes - * @package Magento\CatalogSearch\Setup\Patch + * @deprecated + * @see \Magento\ElasticSearch */ class SetInitialSearchWeightForAttributes implements DataPatchInterface, PatchVersionInterface { diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AdminCatalogSearchTermActionGroup.xml b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AdminCatalogSearchTermActionGroup.xml new file mode 100644 index 000000000000..33ffa4fe1b29 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AdminCatalogSearchTermActionGroup.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertSearchTermSaveSuccessMessage"> + <arguments> + <argument name="searchQuery" type="string"/> + <argument name="storeValue" type="string"/> + <argument name="redirectUrl" type="string"/> + <argument name="displayInSuggestedTerm" type="string"/> + </arguments> + <amOnPage url="{{AdminCatalogSearchTermIndexPage.url}}" stepKey="openAdminCatalogSearchTermIndexPage"/> + <waitForPageLoad stepKey="waitForAdminCatalogSearchTermIndexPageLoad"/> + <click selector="{{AdminCatalogSearchTermIndexSection.addNewSearchTermButton}}" stepKey="clickAddNewSearchTermButton"/> + <waitForPageLoad stepKey="waitForAdminCatalogSearchTermNewPageLoad"/> + <fillField selector="{{AdminCatalogSearchTermNewSection.searchQuery}}" userInput="{{searchQuery}}" stepKey="fillSearchQueryTextBox"/> + <selectOption selector="{{AdminCatalogSearchTermNewSection.store}}" userInput="{{storeValue}}" stepKey="selectStoreValue"/> + <fillField selector="{{AdminCatalogSearchTermNewSection.redirectUrl}}" userInput="{{redirectUrl}}" stepKey="fillRedirectUrl"/> + <selectOption selector="{{AdminCatalogSearchTermNewSection.displayInSuggestedTerm}}" userInput="{{displayInSuggestedTerm}}" stepKey="selectDisplayInSuggestedTerm"/> + <click selector="{{AdminCatalogSearchTermNewSection.saveSearchButton}}" stepKey="clickSaveSearchButton"/> + <see selector="{{AdminCatalogSearchTermMessagesSection.successMessage}}" userInput="You saved the search term." stepKey="seeSaveSuccessMessage"/> + </actionGroup> + <actionGroup name="AssertSearchTermSuccessDeleteMessage"> + <arguments> + <argument name="searchQuery" type="string"/> + </arguments> + <amOnPage url="{{AdminCatalogSearchTermIndexPage.url}}" stepKey="openCatalogSearchIndexPage"/> + <waitForPageLoad stepKey="waitForAdminCatalogSearchTermIndexPageLoad"/> + <click selector="{{AdminCatalogSearchTermIndexSection.resetFilterButton}}" stepKey="clickOnResetButton"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <fillField selector="{{AdminCatalogSearchTermIndexSection.searchQuery}}" userInput="{{searchQuery}}" stepKey="fillSearchQuery"/> + <click selector="{{AdminCatalogSearchTermIndexSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForSearchResultLoad"/> + <click selector="{{AdminCatalogSearchTermIndexSection.nthRow('1')}}" stepKey="checkFirstRow"/> + <selectOption selector="{{AdminCatalogSearchTermIndexSection.massActions}}" userInput="delete" stepKey="selectDeleteOption"/> + <click selector="{{AdminCatalogSearchTermIndexSection.submit}}" stepKey="clickSubmitButton"/> + <click selector="{{AdminCatalogSearchTermIndexSection.okButton}}" stepKey="clickOkButton"/> + <see selector="{{AdminCatalogSearchTermMessagesSection.successMessage}}" userInput="Total of 1 record(s) were deleted." stepKey="seeSuccessMessage"/> + </actionGroup> + <actionGroup name="AssertSearchTermNotInGrid"> + <arguments> + <argument name="searchQuery" type="string"/> + </arguments> + <amOnPage url="{{AdminCatalogSearchTermIndexPage.url}}" stepKey="openCatalogSearchIndexPage"/> + <waitForPageLoad stepKey="waitForAdminCatalogSearchTermIndexPageLoad"/> + <click selector="{{AdminCatalogSearchTermIndexSection.resetFilterButton}}" stepKey="clickOnResetButton"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <fillField selector="{{AdminCatalogSearchTermIndexSection.searchQuery}}" userInput="{{searchQuery}}" stepKey="fillSearchQuery"/> + <click selector="{{AdminCatalogSearchTermIndexSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForSearchResultToLoad"/> + <see selector="{{AdminCatalogSearchTermIndexSection.emptyRecords}}" userInput="We couldn't find any records." stepKey="seeEmptyRecordMessage"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AdminSetMinimalQueryLengthActionGroup.xml b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AdminSetMinimalQueryLengthActionGroup.xml new file mode 100644 index 000000000000..b9ef37cb4eff --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AdminSetMinimalQueryLengthActionGroup.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="SetMinimalQueryLengthActionGroup"> + <arguments> + <argument name="minLength" type="string" defaultValue="1"/> + </arguments> + <amOnPage url="{{AdminCatalogSearchConfigurationPage.url}}" stepKey="navigateToConfigurationPage"/> + <waitForPageLoad stepKey="wait1"/> + <scrollTo selector="{{AdminCatalogSearchConfigurationSection.catalogSearchTab}}" stepKey="scrollToCatalogSearchTab"/> + <conditionalClick selector="{{AdminCatalogSearchConfigurationSection.catalogSearchTab}}" dependentSelector="{{AdminCatalogSearchConfigurationSection.minQueryLength}}" visible="false" stepKey="expandCatalogSearchTab"/> + <waitForElementVisible selector="{{AdminCatalogSearchConfigurationSection.minQueryLength}}" stepKey="waitTabToCollapse"/> + <see userInput="{{MinMaxQueryLength.Hint}}" selector="{{AdminCatalogSearchConfigurationSection.minQueryLengthHint}}" stepKey="seeHint1"/> + <see userInput="{{MinMaxQueryLength.Hint}}" selector="{{AdminCatalogSearchConfigurationSection.maxQueryLengthHint}}" stepKey="seeHint2"/> + <uncheckOption selector="{{AdminCatalogSearchConfigurationSection.minQueryLengthInherit}}" stepKey="uncheckSystemValue"/> + <fillField selector="{{AdminCatalogSearchConfigurationSection.minQueryLength}}" userInput="{{minLength}}" stepKey="setMinQueryLength"/> + <click selector="{{AdminCatalogSearchConfigurationSection.catalogSearchTab}}" stepKey="collapseTab"/> + <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig"/> + <waitForPageLoad stepKey="waitForConfigSaved"/> + <see userInput="You saved the configuration." stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchActionGroup.xml b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchActionGroup.xml index 51dd8a80fcb4..067d76821d68 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchActionGroup.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchActionGroup.xml @@ -11,15 +11,66 @@ <!-- Quick search the phrase and check if the result page contains correct information --> <actionGroup name="StorefrontCheckQuickSearchActionGroup"> <arguments> - <argument name="phrase"/> + <argument name="phrase" /> </arguments> - <fillField userInput="{{phrase}}" selector="{{StorefrontQuickSearchSection.searchPhrase}}" stepKey="fillQuickSearch"/> - <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickQuickSearchButton" /> + <submitForm selector="{{StorefrontQuickSearchSection.searchMiniForm}}" parameterArray="['q' => {{phrase}}]" stepKey="fillQuickSearch" /> <seeInCurrentUrl url="{{StorefrontCatalogSearchPage.url}}" stepKey="checkUrl"/> + <dontSeeInCurrentUrl url="form_key=" stepKey="checkUrlFormKey"/> <seeInTitle userInput="Search results for: '{{phrase}}'" stepKey="assertQuickSearchTitle"/> <see userInput="Search results for: '{{phrase}}'" selector="{{StorefrontCatalogSearchMainSection.SearchTitle}}" stepKey="assertQuickSearchName"/> </actionGroup> + <!-- Quick search the phrase and check if the result page contains correct information, usable with type="string" --> + <actionGroup name="StorefrontCheckQuickSearchStringActionGroup"> + <arguments> + <argument name="phrase" type="string"/> + </arguments> + <fillField stepKey="fillInput" selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{phrase}}"/> + <submitForm selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" parameterArray="[]" stepKey="submitQuickSearch" /> + <seeInCurrentUrl url="{{StorefrontCatalogSearchPage.url}}" stepKey="checkUrl"/> + <dontSeeInCurrentUrl url="form_key=" stepKey="checkUrlFormKey"/> + <seeInTitle userInput="Search results for: '{{phrase}}'" stepKey="assertQuickSearchTitle"/> + <see userInput="Search results for: '{{phrase}}'" selector="{{StorefrontCatalogSearchMainSection.SearchTitle}}" stepKey="assertQuickSearchName"/> + </actionGroup> + + <!-- Opens product from QuickSearch and performs assertions--> + <actionGroup name="StorefrontOpenProductFromQuickSearch"> + <arguments> + <argument name="productName" type="string"/> + <argument name="productUrlKey" type="string"/> + </arguments> + <click stepKey="openProduct" selector="{{StorefrontQuickSearchResultsSection.productByName(productName)}}"/> + <waitForPageLoad stepKey="waitForProductLoad"/> + <seeInCurrentUrl url="{{productUrlKey}}" stepKey="checkUrl"/> + <see stepKey="checkName" selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{productName}}"/> + </actionGroup> + + <!-- Adds product from Quicksearch page and perform assertions--> + <actionGroup name="StorefrontAddToCartFromQuickSearch"> + <arguments> + <argument name="productName" type="string"/> + </arguments> + <moveMouseOver stepKey="hoverOverProduct" selector="{{StorefrontQuickSearchResultsSection.productByIndex('1')}}"/> + <click selector="{{StorefrontQuickSearchResultsSection.productByName(productName)}} {{StorefrontQuickSearchResultsSection.addToCartBtn}}" stepKey="addToCart"/> + <waitForElementVisible selector="{{StorefrontQuickSearchResultsSection.messageSection}}" time="30" stepKey="waitForProductAdded"/> + <see selector="{{StorefrontQuickSearchResultsSection.messageSection}}" userInput="You added {{productName}} to your shopping cart." stepKey="seeAddedToCartMessage"/> + </actionGroup> + + <actionGroup name="StorefrontQuickSearchCheckProductNameInGrid"> + <arguments> + <argument name="productName" type="string"/> + <argument name="index" type="string"/> + </arguments> + <see selector="{{StorefrontQuickSearchResultsSection.productByIndex(index)}}" userInput="{{productName}}" stepKey="seeProductName"/> + </actionGroup> + + <actionGroup name="StorefrontQuickSearchCheckProductNameNotInGrid"> + <arguments> + <argument name="productName" type="string"/> + </arguments> + <dontSee selector="{{StorefrontQuickSearchResultsSection.allResults}}" userInput="{{productName}}" stepKey="dontSeeProductName"/> + </actionGroup> + <!-- Open advanced search page --> <actionGroup name="StorefrontOpenAdvancedSearchActionGroup"> <click selector="{{StorefrontFooterSection.AdvancedSearch}}" stepKey="clickAdvancedSearchLink" /> @@ -48,7 +99,7 @@ <!-- Go to store's advanced catalog search page --> <actionGroup name="GoToStoreViewAdvancedCatalogSearchActionGroup"> <amOnPage url="{{StorefrontCatalogSearchAdvancedFormPage.url}}" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <waitForPageLoad time="90" stepKey="waitForPageLoad"/> </actionGroup> <!-- Storefront advanced catalog search by product name --> @@ -116,4 +167,9 @@ <click selector="{{StorefrontCatalogSearchAdvancedFormSection.SubmitButton}}" stepKey="clickSubmit"/> <waitForPageLoad stepKey="waitForPageLoad"/> </actionGroup> + + <!-- Asserts that search results do not contain any results--> + <actionGroup name="StorefrontCheckSearchIsEmpty"> + <see stepKey="checkEmpty" selector="{{StorefrontQuickSearchResultsSection.messageSection}}" userInput="Your search returned no results"/> + </actionGroup> </actionGroups> \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchTermActionGroup.xml b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchTermActionGroup.xml new file mode 100644 index 000000000000..83e4ac50a74e --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchTermActionGroup.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!--Verify AssertSearchTermNotOnFrontend--> + <actionGroup name="AssertSearchTermNotOnFrontend"> + <arguments> + <argument name="searchQuery" type="string"/> + <argument name="url_key" type="string"/> + </arguments> + <amOnPage url="{{StorefrontProductPage.url('url_key')}}" stepKey="goToMagentoStorefrontPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{searchQuery}}" stepKey="fillSearchQuery"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <see selector="{{StorefrontMessagesSection.noticeMessage}}" userInput="Your search returned no results." stepKey="seeAssertSearchTermNotOnFrontendNoticeMessage"/> + </actionGroup> + + <actionGroup name="AssertSearchTermOnFrontend"> + <arguments> + <argument name="searchQuery" type="string"/> + <argument name="redirectUrl" type="string"/> + </arguments> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="{{searchQuery}}" stepKey="fillSearchQuery"/> + <waitForPageLoad stepKey="waitForFillField"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <seeInCurrentUrl url="{{redirectUrl}}" stepKey="checkUrl"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 000000000000..df1c3db6e566 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminMenuMarketingSEOAndSearchSearchTerms"> + <data key="pageTitle">Search Terms</data> + <data key="title">Search Terms</data> + <data key="dataUiId">magento-search-search-terms</data> + </entity> + <entity name="AdminMenuReportsMarketingSearchTerms"> + <data key="pageTitle">Search Terms Report</data> + <data key="title">Search Terms</data> + <data key="dataUiId">magento-search-report-search-term</data> + </entity> +</entities> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Data/CatalogSearchData.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Data/CatalogSearchData.xml new file mode 100644 index 000000000000..686845607911 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Data/CatalogSearchData.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="SetMinQueryLengthToDefault" type="catalog_search_config_def"> + <requiredEntity type="enable">DefaultMinQueryLength</requiredEntity> + </entity> + <entity name="UncheckMinQueryLengthAndSet" type="catalog_search_config_query_length"> + <requiredEntity type="number">SetMinQueryLengthToOne</requiredEntity> + </entity> + <entity name="DefaultMinQueryLength" type="enable"> + <data key="inherit">true</data> + </entity> + <entity name="DefaultMinQueryLengthDisable" type="enable"> + <data key="inherit">0</data> + </entity> + <entity name="SetMinQueryLengthToOne" type="number"> + <data key="value">1</data> + </entity> + +</entities> \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Data/MinMaxQueryLengthHintsData.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Data/MinMaxQueryLengthHintsData.xml new file mode 100644 index 000000000000..6fb254afea34 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Data/MinMaxQueryLengthHintsData.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="MinMaxQueryLength" type="constant"> + <data key="Hint">This value must be compatible with the corresponding setting in the configured search engine</data> + </entity> +</entities> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Data/SearchTermData.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Data/SearchTermData.xml new file mode 100644 index 000000000000..995b860d107c --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Data/SearchTermData.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="SimpleTerm" type="searchTerm"> + <data key="search_query" unique="suffix">Query text</data> + <data key="store_id">Default Store View</data> + <data key="redirect" unique="suffix">http://example.com/</data> + <data key="display_in_terms">No</data> + </entity> +</entities> \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Metadata/catalog_search-meta.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Metadata/catalog_search-meta.xml new file mode 100644 index 000000000000..7405377249aa --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Metadata/catalog_search-meta.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CatalogSearchConfigDefault" dataType="catalog_search_config_def" type="create" auth="adminFormKey" url="/admin/system_config/save/section/catalog/" method="POST"> + <object key="groups" dataType="catalog_search_config_def"> + <object key="search" dataType="catalog_search_config_def"> + <object key="fields" dataType="catalog_search_config_def"> + <object key="min_query_length" dataType="enable"> + <field key="inherit">boolean</field> + </object> + </object> + </object> + </object> + </operation> + <operation name="CatalogSearchConfigQueryLength" dataType="catalog_search_config_query_length" type="create" auth="adminFormKey" url="/admin/system_config/save/section/catalog/" method="POST"> + <object key="groups" dataType="catalog_search_config_query_length"> + <object key="search" dataType="catalog_search_config_query_length"> + <object key="fields" dataType="catalog_search_config_query_length"> + <object key="min_query_length" dataType="number"> + <field key="value">integer</field> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Page/AdminCatalogSearchTermIndexPage.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Page/AdminCatalogSearchTermIndexPage.xml new file mode 100644 index 000000000000..bbafff8ad773 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Page/AdminCatalogSearchTermIndexPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminCatalogSearchTermIndexPage" url="/search/term/index/" area="admin" module="Magento_CatalogSearch"> + <section name="AdminCatalogSearchTermIndexSection"/> + </page> +</pages> \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Page/AdminCatalogSearchTermNewPage.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Page/AdminCatalogSearchTermNewPage.xml new file mode 100644 index 000000000000..de7491471741 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Page/AdminCatalogSearchTermNewPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminCatalogSearchTermNewPage" url="/search/term/new/" area="admin" module="Magento_CatalogSearch"> + <section name="AdminCatalogSearchTermNewSection"/> + </page> +</pages> \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermIndexSection.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermIndexSection.xml new file mode 100644 index 000000000000..ac316d060f6e --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermIndexSection.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCatalogSearchTermIndexSection"> + <element name="addNewSearchTermButton" type="button" selector="//div[@class='page-actions-buttons']/button[@id='add']" timeout="30"/> + <element name="resetFilterButton" type="button" selector="//button[@class='action-default scalable action-reset action-tertiary']" timeout="30"/> + <element name="searchButton" type="button" selector="//button[@class='action-default scalable action-secondary']" timeout="30"/> + <element name="massActions" type="text" selector="//div[@class='admin__grid-massaction-form']//select[@id='search_term_grid_massaction-select']"/> + <element name="submit" type="button" selector="//button[@class='action-default scalable']/span" timeout="30"/> + <element name="searchQuery" type="text" selector="//tr[@class='data-grid-filters']//td/input[@name='search_query']"/> + <element name="nthRow" type="checkbox" selector="//tbody/tr['{{rowNum}}']//input[@name='search']" parameterized="true"/> + <element name="searchTermRowCheckboxBySearchQuery" type="checkbox" selector="//*[normalize-space()='{{var1}}']/preceding-sibling::td//input[@name='search']" parameterized="true" timeout="30"/> + <element name="okButton" type="button" selector="//button[@class='action-primary action-accept']/span" timeout="30"/> + <element name="emptyRecords" type="text" selector="//tr[@class='data-grid-tr-no-data even']/td[@class='empty-text']"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermMessagesSection.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermMessagesSection.xml new file mode 100644 index 000000000000..5d19198a1b94 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermMessagesSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCatalogSearchTermMessagesSection"> + <element name="successMessage" type="text" selector="//div[@class='message message-success success']/div[@data-ui-id='messages-message-success']"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermNewSection.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermNewSection.xml new file mode 100644 index 000000000000..a7d577a7508c --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Section/AdminCatalogSearchTermNewSection.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCatalogSearchTermNewSection"> + <element name="searchQuery" type="text" selector="//div[@class='admin__field-control control']/input[@id='query_text']"/> + <element name="store" type="text" selector="//select[@id='store_id']"/> + <element name="redirectUrl" type="text" selector="//div[@class='admin__field-control control']/input[@id='redirect']"/> + <element name="displayInSuggestedTerm" type="select" selector="//select[@name='display_in_terms']"/> + <element name="saveSearchButton" type="button" selector="//button[@id='save']/span[@class='ui-button-text']" timeout="30"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Section/CatalogSearchAdminConfigSection.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Section/CatalogSearchAdminConfigSection.xml new file mode 100644 index 000000000000..605bcabb8a81 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Section/CatalogSearchAdminConfigSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCatalogSearchConfigurationSection"> + <element name="minQueryLength" type="input" selector="#catalog_search_min_query_length"/> + <element name="minQueryLengthInherit" type="checkbox" selector="#catalog_search_min_query_length_inherit"/> + <element name="minQueryLengthHint" type="text" selector="#row_catalog_search_min_query_length .value span"/> + <element name="maxQueryLengthHint" type="text" selector="#row_catalog_search_max_query_length .value span"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchMainSection.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchMainSection.xml index 8b35ef333617..667f08fea657 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchMainSection.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchMainSection.xml @@ -15,5 +15,6 @@ <element name="SuccessMsg" type="button" selector="div.message-success"/> <element name="productCount" type="text" selector="#toolbar-amount"/> <element name="message" type="text" selector="div.message div"/> + <element name="searchResults" type="block" selector="#maincontent .column.main"/> </section> </sections> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCreateSearchTermEntityTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCreateSearchTermEntityTest.xml new file mode 100644 index 000000000000..2b425f34f8a5 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCreateSearchTermEntityTest.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateSearchTermEntityTest"> + <annotations> + <stories value="Search terms"/> + <title value="Create search term test"/> + <description value="Admin should be able to create search term"/> + <testCaseId value="MC-13989"/> + <severity value="CRITICAL"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Create simple product --> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> + </before> + <after> + <!-- Delete created search term --> + <actionGroup ref="AssertSearchTermSuccessDeleteMessage" stepKey="deleteSearchTerm"> + <argument name="searchQuery" value="$$createSimpleProduct.sku$$"/> + </actionGroup> + + <!-- Delete created product --> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to the search terms page and create new search term --> + <actionGroup ref="AssertSearchTermSaveSuccessMessage" stepKey="createNewSearchTerm"> + <argument name="searchQuery" value="$$createSimpleProduct.sku$$"/> + <argument name="storeValue" value="{{SimpleTerm.store_id}}"/> + <argument name="redirectUrl" value="{{SimpleTerm.redirect}}"/> + <argument name="displayInSuggestedTerm" value="{{SimpleTerm.display_in_terms}}"/> + </actionGroup> + + <!-- Go to storefront --> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnStorefrontPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + + <!-- Assert created search term on storefront --> + <actionGroup ref="AssertSearchTermOnFrontend" stepKey="assertCreatedSearchTermOnFrontend"> + <argument name="searchQuery" value="$$createSimpleProduct.sku$$"/> + <argument name="redirectUrl" value="{{SimpleTerm.redirect}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminDeleteSearchTermTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminDeleteSearchTermTest.xml new file mode 100644 index 000000000000..c72ed424ef30 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminDeleteSearchTermTest.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminDeleteSearchTermTest"> + <annotations> + <stories value="Search terms"/> + <title value="Delete Search Term and Verify Storefront"/> + <description value="Test log in to SearchTerm and DeleteSearchTerm"/> + <testCaseId value="MC-13988"/> + <severity value="CRITICAL"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> + <createData entity="SimpleProduct" stepKey="simpleProduct"> + <requiredEntity createDataKey="initialCategoryEntity"/> + </createData> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> + <deleteData stepKey="deleteSimpleProduct" createDataKey="simpleProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Add new search term--> + <actionGroup ref="AssertSearchTermSaveSuccessMessage" stepKey="addNewSearchTerm"> + <argument name="searchQuery" value="{{SimpleTerm.search_query}}"/> + <argument name="storeValue" value="{{SimpleTerm.store_id}}"/> + <argument name="redirectUrl" value="{{SimpleTerm.redirect}}"/> + <argument name="displayInSuggestedTerm" value="{{SimpleTerm.display_in_terms}}"/> + </actionGroup> + + <!--Search and delete search term and AssertSearchTermSuccessDeleteMessage--> + <actionGroup ref="AssertSearchTermSuccessDeleteMessage" stepKey="deleteSearchTerm"> + <argument name="searchQuery" value="{{SimpleTerm.search_query}}"/> + </actionGroup> + + <!--Verify deleted search term in grid and AssertSearchTermNotInGrid--> + <actionGroup ref="AssertSearchTermNotInGrid" stepKey="verifyDeletedSearchTermNotInGrid"> + <argument name="searchQuery" value="{{SimpleTerm.search_query}}"/> + </actionGroup> + + <!--Go to storefront and Verify AssertSearchTermNotOnFrontend--> + <actionGroup ref="AssertSearchTermNotOnFrontend" stepKey="verifySearchTermNotOnFrontend"> + <argument name="searchQuery" value="{{SimpleTerm.search_query}}"/> + <argument name="url_key" value="$$simpleProduct.custom_attributes[url_key]$$"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminMarketingSearchTermsNavigateMenuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminMarketingSearchTermsNavigateMenuTest.xml new file mode 100644 index 000000000000..bc255020d98b --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminMarketingSearchTermsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMarketingSearchTermsNavigateMenuTest"> + <annotations> + <features value="CatalogSearch"/> + <stories value="Menu Navigation"/> + <title value="Admin marketing search terms navigate menu test"/> + <description value="Admin should be able to navigate to Marketing > Search Terms"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14135"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToMarketingSearchTermsPage"> + <argument name="menuUiId" value="{{AdminMenuMarketing.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuMarketingSEOAndSearchSearchTerms.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuMarketingSEOAndSearchSearchTerms.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminReportsSearchTermsNavigateMenuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminReportsSearchTermsNavigateMenuTest.xml new file mode 100644 index 000000000000..85cf0e3ba90e --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminReportsSearchTermsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminReportsSearchTermsNavigateMenuTest"> + <annotations> + <features value="CatalogSearch"/> + <stories value="Menu Navigation"/> + <title value="Admin reports search terms navigate menu test"/> + <description value="Admin should be able to navigate to Reports > Search Terms"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14136"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportSearchTermsPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsMarketingSearchTerms.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsMarketingSearchTerms.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml new file mode 100644 index 000000000000..2fea5c988bf9 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="MinimalQueryLengthForCatalogSearchTest"> + <annotations> + <features value="CatalogSearch"/> + <title value="Minimal query length for catalog search"/> + <description value="Minimal query length for catalog search"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-6325"/> + <useCaseId value="MAGETWO-58764"/> + <group value="CatalogSearch"/> + </annotations> + <before> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <createData entity="SetMinQueryLengthToDefault" stepKey="setMinimumQueryLengthToDefault"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="SetMinimalQueryLengthActionGroup" stepKey="setMinQueryLength"/> + <comment userInput="Go to Storefront and search for product" stepKey="searchProdUsingMinQueryLength"/> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToHomePage"/> + <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="s" stepKey="fillAttribute"/> + <waitForPageLoad stepKey="waitForSearchTextBox"/> + <click selector="{{StorefrontQuickSearchResultsSection.searchTextBoxButton}}" stepKey="clickSearchTextBoxButton"/> + <waitForPageLoad stepKey="waitForSearch"/> + <see selector="{{StorefrontCategoryMainSection.productName}}" userInput="$$createProduct.name$$" stepKey="seeProductNameInCategoryPage"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest.xml new file mode 100644 index 000000000000..19db201e91f4 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest.xml @@ -0,0 +1,629 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="QuickSearchProductBySku"> + <annotations> + <stories value="Search Product on Storefront"/> + <title value="User should be able to use Quick Search to find products"/> + <description value="Use Quick Search to find a product"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14783"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData stepKey="deleteProduct" createDataKey="createSimpleProduct"/> + <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + </after> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> + <argument name="phrase" value="$createSimpleProduct.sku$"/> + </actionGroup> + <actionGroup ref="StorefrontOpenProductFromQuickSearch" stepKey="openAndCheckProduct"> + <argument name="productName" value="$createSimpleProduct.name$"/> + <argument name="productUrlKey" value="$createSimpleProduct.custom_attributes[url_key]$"/> + </actionGroup> + </test> + <test name="QuickSearchProductByName" extends="QuickSearchProductBySku"> + <annotations> + <stories value="Search Product on Storefront"/> + <title value="User should be able to use Quick Search to find products via Name"/> + <description value="Use Quick Search to find a product"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14791"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + <!-- Overwrite search to use name --> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> + <argument name="phrase" value="$createSimpleProduct.name$"/> + </actionGroup> + </test> + <test name="QuickSearchProductByNameWithSpecialChars" extends="QuickSearchProductBySku"> + <annotations> + <stories value="Search Product on Storefront"/> + <title value="Quick Search can find products with names that contain special characters"/> + <description value="Use Quick Search to find a product by name"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14792"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="productWithSpecialCharacters" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <!-- Overwrite search to use name --> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> + <argument name="phrase" value="$createSimpleProduct.name$"/> + </actionGroup> + </test> + <test name="QuickSearchEmptyResults"> + <annotations> + <stories value="Search Product on Storefront"/> + <title value="User should not get search results on query that doesn't return anything"/> + <description value="Use invalid query to return no products"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14793"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData stepKey="deleteProduct" createDataKey="createSimpleProduct"/> + <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + </after> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> + <argument name="phrase" value="ThisShouldn'tReturnAnything"/> + </actionGroup> + <actionGroup ref="StorefrontCheckSearchIsEmpty" stepKey="checkEmpty"/> + </test> + <test name="QuickSearchWithTwoCharsEmptyResults" extends="QuickSearchEmptyResults"> + <annotations> + <stories value="Search Product on Storefront"/> + <title value="User should not get search results on query that only contains two characters"/> + <description value="Use of 2 character query to return no products"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14794"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + <skip> + <issueId value="MC-15827"/> + </skip> + </annotations> + <executeJS function="var s = '$createSimpleProduct.name$'; var ret=s.substring(0,2); return ret;" stepKey="getFirstTwoLetters" before="searchStorefront"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> + <argument name="phrase" value="{$getFirstTwoLetters}"/> + </actionGroup> + </test> + <test name="QuickSearchProductByNameWithThreeLetters" extends="QuickSearchProductBySku"> + <annotations> + <stories value="Search Product on Storefront"/> + <title value="User should be able to use Quick Search to find products by their first three letters"/> + <description value="Use Quick Search to find a product using only first three letters"/> + <severity value="MAJOR"/> + <testCaseId value="MC-15034"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + <executeJS function="var s = '$createSimpleProduct.name$'; var ret=s.substring(0,3); return ret;" stepKey="getFirstThreeLetters" before="searchStorefront"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> + <argument name="phrase" value="{$getFirstThreeLetters}"/> + </actionGroup> + </test> + <test name="QuickSearchProductBy128CharQuery" extends="QuickSearchProductBySku"> + <annotations> + <stories value="Search Product on Storefront"/> + <title value="User should be able to use Quick Search product with long names, using first 128 letters"/> + <description value="Use Quick Search to find a product with name of 130 length with query of only 128"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14795"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="productWith130CharName" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <executeJS function="var s = '$createSimpleProduct.name$'; var ret=s.substring(0,128); return ret;" stepKey="get128Letters" before="searchStorefront"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> + <argument name="phrase" value="{$get128Letters}"/> + </actionGroup> + </test> + + <test name="QuickSearchTwoProductsWithSameWeight"> + <annotations> + <stories value="Search Product on Storefront"/> + <title value="Quick Search should sort products with the same weight appropriately"/> + <description value="Use Quick Search to find a two products with the same weight"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14796"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="productAlphabeticalA" stepKey="product1"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="productAlphabeticalB" stepKey="product2"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + + + <!-- Create and Assign Attribute to product1--> + <actionGroup ref="goToProductPageViaID" stepKey="goToProduct1"> + <argument name="productId" value="$product1.id$"/> + </actionGroup> + <actionGroup ref="AdminCreateAttributeWithSearchWeight" stepKey="createProduct1Attribute"> + <argument name="attributeType" value="Text Field"/> + <argument name="attributeName" value="$product1.name$"/> + <argument name="attributeSetName" value="$product1.name$"/> + <argument name="weight" value="1"/> + <argument name="defaultValue" value="{{_defaultProduct.name}}"/> + </actionGroup> + <actionGroup ref="AdminProductPageSelectAttributeSet" stepKey="selectAttributeSet1"> + <argument name="attributeSetName" value="$product1.name$"/> + </actionGroup> + <!--fill in default--> + <actionGroup ref="saveProductForm" stepKey="saveProduct1a"/> + <actionGroup ref="AdminProductPageFillTextAttributeValueByName" stepKey="fillDefault1"> + <argument name="attributeName" value="$product1.name$"/> + <argument name="value" value="{{_defaultProduct.name}}"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProduct1b"/> + <!-- Create and Assign Attribute to product2--> + <actionGroup ref="goToProductPageViaID" stepKey="goToProduct2"> + <argument name="productId" value="$product2.id$"/> + </actionGroup> + <actionGroup ref="AdminCreateAttributeWithSearchWeight" stepKey="createProduct2Attribute"> + <argument name="attributeType" value="Text Field"/> + <argument name="attributeName" value="$product2.name$"/> + <argument name="attributeSetName" value="$product2.name$"/> + <argument name="weight" value="1"/> + <argument name="defaultValue" value="{{_defaultProduct.name}}"/> + </actionGroup> + <actionGroup ref="AdminProductPageSelectAttributeSet" stepKey="selectAttributeSet2"> + <argument name="attributeSetName" value="$product2.name$"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProduct2a"/> + <!--fill in default--> + <actionGroup ref="AdminProductPageFillTextAttributeValueByName" stepKey="fillDefault2"> + <argument name="attributeName" value="$product2.name$"/> + <argument name="value" value="{{_defaultProduct.name}}"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProduct2b"/> + </before> + <after> + <deleteData stepKey="deleteProduct1" createDataKey="product1"/> + <deleteData stepKey="deleteProduct2" createDataKey="product2"/> + <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + </after> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> + <argument name="phrase" value="{{_defaultProduct.name}}"/> + </actionGroup> + <actionGroup ref="StorefrontQuickSearchCheckProductNameInGrid" stepKey="assertProduct1Position"> + <argument name="productName" value="$product1.name$"/> + <argument name="index" value="2"/> + </actionGroup> + <actionGroup ref="StorefrontQuickSearchCheckProductNameInGrid" stepKey="assertProduct2Position"> + <argument name="productName" value="$product2.name$"/> + <argument name="index" value="1"/> + </actionGroup> + </test> + <test name="QuickSearchTwoProductsWithDifferentWeight" extends="QuickSearchTwoProductsWithSameWeight"> + <annotations> + <stories value="Search Product on Storefront"/> + <title value="Quick Search should sort products with the different weight appropriately"/> + <description value="Use Quick Search to find a two products with the different weight"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14797"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="AdminCreateAttributeWithSearchWeight" stepKey="createProduct1Attribute"> + <argument name="attributeType" value="Text Field"/> + <argument name="attributeName" value="$product1.name$"/> + <argument name="attributeSetName" value="$product1.name$"/> + <argument name="weight" value="5"/> + <argument name="defaultValue" value="{{_defaultProduct.name}}"/> + </actionGroup> + </before> + <actionGroup ref="StorefrontQuickSearchCheckProductNameInGrid" stepKey="assertProduct1Position"> + <argument name="productName" value="$product1.name$"/> + <argument name="index" value="1"/> + </actionGroup> + <actionGroup ref="StorefrontQuickSearchCheckProductNameInGrid" stepKey="assertProduct2Position"> + <argument name="productName" value="$product2.name$"/> + <argument name="index" value="2"/> + </actionGroup> + </test> + + <test name="QuickSearchAndAddToCart"> + <annotations> + <stories value="Search Product on Storefront"/> + <title value="User should be able to use Quick Search to find a simple product and add it to cart"/> + <description value="Use Quick Search to find simple Product and Add to Cart"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14784"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData stepKey="deleteProduct" createDataKey="createSimpleProduct"/> + <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + </after> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> + <argument name="phrase" value="$createSimpleProduct.name$"/> + </actionGroup> + <actionGroup ref="StorefrontAddToCartFromQuickSearch" stepKey="addProductToCart"> + <argument name="productName" value="$createSimpleProduct.name$"/> + </actionGroup> + </test> + <test name="QuickSearchAndAddToCartVirtual"> + <annotations> + <stories value="Search Product on Storefront"/> + <title value="User should be able to use Quick Search to find a virtual product and add it to cart"/> + <description value="Use Quick Search to find virtual Product and Add to Cart"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14785"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="VirtualProduct" stepKey="createVirtualProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData stepKey="deleteProduct" createDataKey="createVirtualProduct"/> + <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + </after> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> + <argument name="phrase" value="$createVirtualProduct.name$"/> + </actionGroup> + <actionGroup ref="StorefrontAddToCartFromQuickSearch" stepKey="addProductToCart"> + <argument name="productName" value="$createVirtualProduct.name$"/> + </actionGroup> + </test> + <test name="QuickSearchAndAddToCartConfigurable"> + <annotations> + <stories value="Search Product on Storefront"/> + <title value="User should be able to use Quick Search to find a configurable product and add it to cart"/> + <description value="Use Quick Search to find configurable Product and Add to Cart"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14786"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + <actionGroup ref="createConfigurableProduct" stepKey="createProduct"> + <argument name="product" value="_defaultProduct"/> + <argument name="category" value="$$createCategory$$"/> + </actionGroup> + </before> + <after> + <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + <actionGroup ref="deleteProductBySku" stepKey="deleteProduct"> + <argument name="sku" value="{{_defaultProduct.sku}}"/> + </actionGroup> + </after> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> + <argument name="phrase" value="{{_defaultProduct.name}}"/> + </actionGroup> + <actionGroup ref="StorefrontOpenProductFromQuickSearch" stepKey="openAndCheckProduct"> + <argument name="productName" value="{{_defaultProduct.name}}"/> + <argument name="productUrlKey" value="{{_defaultProduct.urlKey}}"/> + </actionGroup> + <actionGroup ref="SelectSingleAttributeAndAddToCart" stepKey="addProductToCart"> + <argument name="productName" value="{{_defaultProduct.name}}"/> + <argument name="attributeCode" value="{{colorProductAttribute.default_label}}"/> + <argument name="optionName" value="{{colorProductAttribute1.name}}"/> + </actionGroup> + </test> + <test name="QuickSearchAndAddToCartDownloadable"> + <annotations> + <stories value="Search Product on Storefront"/> + <title value="User should be able to use Quick Search to find a downloadable product and add it to cart"/> + <description value="Use Quick Search to find downloadable Product and Add to Cart"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14787"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="DownloadableProductWithOneLink" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="downloadableLink1" stepKey="addDownloadableLink1"> + <requiredEntity createDataKey="createProduct"/> + </createData> + </before> + <after> + <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> + <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + </after> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> + <argument name="phrase" value="$createProduct.name$"/> + </actionGroup> + <actionGroup ref="StorefrontAddToCartFromQuickSearch" stepKey="addProductToCart"> + <argument name="productName" value="$createProduct.name$"/> + </actionGroup> + </test> + <test name="QuickSearchAndAddToCartGrouped"> + <annotations> + <stories value="Search Product on Storefront"/> + <title value="User should be able to use Quick Search to find a grouped product and add it to cart"/> + <description value="Use Quick Search to find grouped Product and Add to Cart"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14788"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="simple1"/> + <createData entity="ApiGroupedProduct" stepKey="createProduct"/> + <createData entity="OneSimpleProductLink" stepKey="addProductOne"> + <requiredEntity createDataKey="createProduct"/> + <requiredEntity createDataKey="simple1"/> + </createData> + </before> + <after> + <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> + </after> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> + <argument name="phrase" value="$createProduct.name$"/> + </actionGroup> + <actionGroup ref="StorefrontAddToCartFromQuickSearch" stepKey="addProductToCart"> + <argument name="productName" value="$createProduct.name$"/> + </actionGroup> + </test> + <test name="QuickSearchAndAddToCartBundleDynamic"> + <annotations> + <stories value="Search Product on Storefront"/> + <title value="User should be able to use Quick Search to find a Bundle Dynamic product and add it to cart"/> + <description value="Use Quick Search to find Bundle Dynamic Product and Add to Cart"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14789"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!--Create dynamic product--> + <createData entity="ApiBundleProductPriceViewRange" stepKey="createBundleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="DropDownBundleOption" stepKey="bundleOption"> + <requiredEntity createDataKey="createBundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink1"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="bundleOption"/> + <requiredEntity createDataKey="createProduct"/> + <field key="qty">10</field> + </createData> + <!--Finish bundle creation--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <amOnPage url="{{AdminProductEditPage.url($$createBundleProduct.id$$)}}" stepKey="goToProductEditPage"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + </before> + <after> + <deleteData stepKey="deleteBundleProduct" createDataKey="createBundleProduct"/> + <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> + <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + </after> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> + <argument name="phrase" value="$createBundleProduct.name$"/> + </actionGroup> + <actionGroup ref="StorefrontOpenProductFromQuickSearch" stepKey="openAndCheckProduct"> + <argument name="productName" value="$createBundleProduct.name$"/> + <argument name="productUrlKey" value="$createBundleProduct.custom_attributes[url_key]$"/> + </actionGroup> + <actionGroup ref="StorefrontAddBundleProductFromProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="productName" value="$createBundleProduct.name$"/> + </actionGroup> + </test> + <test name="QuickSearchAndAddToCartBundleFixed"> + <annotations> + <stories value="Search Product on Storefront"/> + <title value="User should be able to use Quick Search to find a Bundle Fixed product and add it to cart"/> + <description value="Use Quick Search to find Bundle Fixed Product and Add to Cart"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14790"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!--Create fixed product--> + <!--Create 2 simple products--> + <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> + <!-- Create the bundle product based --> + <createData entity="ApiFixedBundleProduct" stepKey="createBundleProduct"/> + <createData entity="MultipleSelectOption" stepKey="createBundleOption1_1"> + <requiredEntity createDataKey="createBundleProduct"/> + <field key="required">false</field> + </createData> + <createData entity="CheckboxOption" stepKey="createBundleOption1_2"> + <requiredEntity createDataKey="createBundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkOptionToProduct"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createBundleOption1_1"/> + <requiredEntity createDataKey="simpleProduct1"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkOptionToProduct2"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createBundleOption1_1"/> + <requiredEntity createDataKey="simpleProduct2"/> + </createData> + + <!--Finish bundle creation--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <amOnPage url="{{AdminProductEditPage.url($$createBundleProduct.id$$)}}" stepKey="goToProductEditPage"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + </before> + <after> + <deleteData stepKey="deleteBundleProduct" createDataKey="createBundleProduct"/> + <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> + <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> + </after> + <comment userInput="$simpleProduct1.name$" stepKey="asdf"/> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> + <argument name="phrase" value="$createBundleProduct.name$"/> + </actionGroup> + <actionGroup ref="StorefrontOpenProductFromQuickSearch" stepKey="openAndCheckProduct"> + <argument name="productName" value="$createBundleProduct.name$"/> + <argument name="productUrlKey" value="$createBundleProduct.custom_attributes[url_key]$"/> + </actionGroup> + <actionGroup ref="StorefrontAddBundleProductFromProductToCartWithMultiOption" stepKey="addProductToCart"> + <argument name="productName" value="$createBundleProduct.name$"/> + <argument name="optionName" value="$createBundleOption1_1.name$"/> + <argument name="value" value="$simpleProduct1.name$"/> + </actionGroup> + </test> + + <test name="QuickSearchConfigurableChildren"> + <annotations> + <stories value="Search Product on Storefront"/> + <title value="User should be able to use Quick Search to a configurable product's child products"/> + <description value="Use Quick Search to find a configurable product with enabled/disable children"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14798"/> + <group value="CatalogSearch"/> + <group value="mtf_migrated"/> + <skip> + <issueId value="MC-15101"/> + </skip> + </annotations> + <before> + <!-- Create the category --> + <createData entity="ApiCategory" stepKey="createCategory"/> + + <!-- Create blank AttributeSet--> + <createData entity="CatalogAttributeSet" stepKey="attributeSet"/> + + <!-- Create an attribute with two options to be used in the first child product --> + <createData entity="hiddenDropdownAttributeWithOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + + <!-- Assign attribute to set --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="goToAttributeGridPage" stepKey="goToPage"/> + <actionGroup ref="goToAttributeSetByName" stepKey="goToSet"> + <argument name="name" value="$attributeSet.attribute_set_name$"/> + </actionGroup> + <actionGroup ref="AssignAttributeToGroup" stepKey="assignToAttributeSetAndGroup"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="$createConfigProductAttribute.attribute_code$"/> + </actionGroup> + <actionGroup ref="SaveAttributeSet" stepKey="savePage"/> + + <!-- Get the first option of the attribute we created --> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create a simple product,give it the attributeSet and attribute with the first option --> + <createData entity="ApiSimpleOneHidden" stepKey="createConfigChildProduct1"> + <field key="attribute_set_id">$attributeSet.attribute_set_id$</field> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <updateData entity="ApiSimpleProductUpdateDescription" stepKey="updateSimpleProduct1" createDataKey="createConfigChildProduct1"/> + + <!-- Create the configurable product, give it the attributeSet and add it to the category --> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <field key="attribute_set_id">$attributeSet.attribute_set_id$</field> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Create the configurable product --> + <createData entity="ConfigurableProductOneOption" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <!-- Add the first simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + </before> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> + <argument name="phrase" value="$createConfigProduct.name$"/> + </actionGroup> + <actionGroup ref="StorefrontQuickSearchCheckProductNameInGrid" stepKey="seeProductInGrid"> + <argument name="productName" value="$createConfigProduct.name$"/> + <argument name="index" value="1"/> + </actionGroup> + + <!-- Disable Child Product --> + <actionGroup ref="goToProductPageViaID" stepKey="goToChildProduct"> + <argument name="productId" value="$createConfigChildProduct1.id$"/> + </actionGroup> + <actionGroup ref="toggleProductEnabled" stepKey="disableProduct"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPageAgain"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefrontAgain"> + <argument name="phrase" value="$createConfigProduct.name$"/> + </actionGroup> + <actionGroup ref="StorefrontQuickSearchCheckProductNameNotInGrid" stepKey="dontSeeProductAnymore"> + <argument name="productName" value="$createConfigProduct.name$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Controller/Advanced/ResultTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Controller/Advanced/ResultTest.php index 891f008979e1..8d1dbbd6dd6d 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Controller/Advanced/ResultTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Controller/Advanced/ResultTest.php @@ -10,12 +10,27 @@ */ class ResultTest extends \PHPUnit\Framework\TestCase { + /** + * Test result action filters set before load layout scenario + * + * @return void + */ 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) { @@ -53,6 +68,11 @@ function ($added) use (&$filters) { $instance->execute(); } + /** + * Test url set on exception scenario + * + * @return void + */ public function testUrlSetOnException() { $redirectResultMock = $this->createMock(\Magento\Framework\Controller\Result\Redirect::class); @@ -131,11 +151,62 @@ public function testUrlSetOnException() /** @var \Magento\CatalogSearch\Controller\Advanced\Result $instance */ $instance = $objectManager->getObject( \Magento\CatalogSearch\Controller\Advanced\Result::class, - ['context' => $contextMock, - 'catalogSearchAdvanced' => $catalogSearchAdvanced, - 'urlFactory' => $urlFactoryMock + [ + 'context' => $contextMock, + 'catalogSearchAdvanced' => $catalogSearchAdvanced, + 'urlFactory' => $urlFactoryMock ] ); $this->assertEquals($redirectResultMock, $instance->execute()); } + + /** + * Test no result handle scenario + * + * @return void + */ + public function testNoResultsHandle() + { + $expectedQuery = 'notExistTerm'; + + $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)); + + $page = $this->createPartialMock(\Magento\Framework\View\Result\Page::class, ['initLayout']); + + $view = $this->createPartialMock( + \Magento\Framework\App\View::class, + ['loadLayout', 'renderLayout', 'getPage', 'getLayout'] + ); + + $view->expects($this->once())->method('loadLayout') + ->with([\Magento\CatalogSearch\Controller\Advanced\Result::DEFAULT_NO_RESULT_HANDLE]); + + $view->expects($this->once())->method('getPage')->will($this->returnValue($page)); + $view->expects($this->once())->method('getLayout')->will($this->returnValue($layout)); + + $request = $this->createPartialMock(\Magento\Framework\App\Console\Request::class, ['getQueryValue']); + $request->expects($this->once())->method('getQueryValue')->will($this->returnValue($expectedQuery)); + + $catalogSearchAdvanced = $this->createPartialMock( + \Magento\CatalogSearch\Model\Advanced::class, + ['addFilters', '__wakeup', 'getProductCollection'] + ); + + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $context = $objectManager->getObject( + \Magento\Framework\App\Action\Context::class, + ['view' => $view, 'request' => $request] + ); + + /** @var \Magento\CatalogSearch\Controller\Advanced\Result $instance */ + $instance = $objectManager->getObject( + \Magento\CatalogSearch\Controller\Advanced\Result::class, + ['context' => $context, 'catalogSearchAdvanced' => $catalogSearchAdvanced] + ); + $instance->execute(); + } } diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Aggregation/DataProvider/QueryBuilderTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Aggregation/DataProvider/QueryBuilderTest.php index 72379c3819de..949554506d5b 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Aggregation/DataProvider/QueryBuilderTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Aggregation/DataProvider/QueryBuilderTest.php @@ -20,6 +20,8 @@ * Test for Magento\CatalogSearch\Model\Adapter\Mysql\Aggregation\DataProvider\QueryBuilder. * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @deprecated + * @see \Magento\ElasticSearch */ class QueryBuilderTest extends \PHPUnit\Framework\TestCase { diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Aggregation/DataProviderTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Aggregation/DataProviderTest.php index 8eeceba8209b..e3cc3e1d1837 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Aggregation/DataProviderTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Aggregation/DataProviderTest.php @@ -20,9 +20,13 @@ use Magento\Eav\Model\Entity\Attribute; use Magento\Catalog\Model\Product; use Magento\Framework\DB\Ddl\Table; +use Magento\Framework\Event\Manager; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * + * @deprecated + * @see \Magento\ElasticSearch */ class DataProviderTest extends \PHPUnit\Framework\TestCase { @@ -61,6 +65,11 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase */ private $selectBuilderForAttribute; + /** + * @var Manager|\PHPUnit_Framework_MockObject_MockObject + */ + private $eventManager; + protected function setUp() { $this->eavConfigMock = $this->createMock(Config::class); @@ -70,12 +79,14 @@ protected function setUp() $this->adapterMock = $this->createMock(AdapterInterface::class); $this->resourceConnectionMock->expects($this->once())->method('getConnection')->willReturn($this->adapterMock); $this->selectBuilderForAttribute = $this->createMock(SelectBuilderForAttribute::class); + $this->eventManager = $this->createMock(Manager::class); $this->model = new DataProvider( $this->eavConfigMock, $this->resourceConnectionMock, $this->scopeResolverMock, $this->sessionMock, - $this->selectBuilderForAttribute + $this->selectBuilderForAttribute, + $this->eventManager ); } @@ -99,6 +110,7 @@ public function testGetDataSetUsesFrontendPriceIndexerTableIfAttributeIsPrice() $selectMock = $this->createMock(Select::class); $this->adapterMock->expects($this->atLeastOnce())->method('select')->willReturn($selectMock); + $this->eventManager->expects($this->once())->method('dispatch')->willReturn($selectMock); $tableMock = $this->createMock(Table::class); $this->model->getDataSet($bucketMock, ['scope' => $dimensionMock], $tableMock); @@ -126,6 +138,7 @@ public function testGetDataSetUsesFrontendPriceIndexerTableForDecimalAttributes( $selectMock = $this->createMock(Select::class); $this->selectBuilderForAttribute->expects($this->once())->method('build')->willReturn($selectMock); $this->adapterMock->expects($this->atLeastOnce())->method('select')->willReturn($selectMock); + $this->eventManager->expects($this->once())->method('dispatch')->willReturn($selectMock); $tableMock = $this->createMock(Table::class); $this->model->getDataSet($bucketMock, ['scope' => $dimensionMock], $tableMock); } diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Dynamic/DataProviderTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Dynamic/DataProviderTest.php index 1186dd6936cc..b064eec3338e 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Dynamic/DataProviderTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Dynamic/DataProviderTest.php @@ -19,8 +19,9 @@ use Magento\Store\Model\StoreManager; /** - * Class DataProviderTest * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @deprecated + * @see \Magento\ElasticSearch */ class DataProviderTest extends \PHPUnit\Framework\TestCase { diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Field/ResolverTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Field/ResolverTest.php index ca19e5e995c8..1e609cdeac28 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Field/ResolverTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Field/ResolverTest.php @@ -9,6 +9,9 @@ /** * Unit tests for Magento\CatalogSearch\Model\Adapter\Mysql\Field\Resolver class. + * + * @deprecated + * @see \Magento\ElasticSearch */ class ResolverTest extends \PHPUnit\Framework\TestCase { diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/AliasResolverTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/AliasResolverTest.php index 697fab650793..1690d9f39360 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/AliasResolverTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/AliasResolverTest.php @@ -9,6 +9,10 @@ use Magento\CatalogSearch\Model\Search\RequestGenerator; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +/** + * @deprecated + * @see \Magento\ElasticSearch + */ class AliasResolverTest extends \PHPUnit\Framework\TestCase { /** diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php index 01108358da2e..7e3de7534e8c 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php @@ -15,6 +15,8 @@ /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @deprecated + * @see \Magento\ElasticSearch */ class PreprocessorTest extends \PHPUnit\Framework\TestCase { diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/AdvancedTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/AdvancedTest.php index fc5915bb3cdf..a4f62ce83a1b 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/Indexer/FulltextTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/FulltextTest.php index d7129b9c224f..f70c61cdbafd 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/FulltextTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/FulltextTest.php @@ -116,6 +116,7 @@ public function testExecute() ->willReturn($ids); $this->saveHandler->expects($this->exactly(count($stores)))->method('deleteIndex'); $this->saveHandler->expects($this->exactly(2))->method('saveIndex'); + $this->saveHandler->expects($this->exactly(2))->method('isAvailable')->willReturn(true); $consecutiveStoreRebuildArguments = array_map( function ($store) use ($ids) { return [$store, $ids]; @@ -186,6 +187,7 @@ public function testExecuteList() ->willReturn($ids); $this->saveHandler->expects($this->exactly(count($stores)))->method('deleteIndex'); $this->saveHandler->expects($this->exactly(2))->method('saveIndex'); + $this->saveHandler->expects($this->exactly(2))->method('isAvailable')->willReturn(true); $this->fullAction->expects($this->exactly(2)) ->method('rebuildStoreIndex') ->willReturn(new \ArrayObject([$indexData, $indexData])); @@ -204,6 +206,7 @@ public function testExecuteRow() ->willReturn([$id]); $this->saveHandler->expects($this->exactly(count($stores)))->method('deleteIndex'); $this->saveHandler->expects($this->exactly(2))->method('saveIndex'); + $this->saveHandler->expects($this->exactly(2))->method('isAvailable')->willReturn(true); $this->fullAction->expects($this->exactly(2)) ->method('rebuildStoreIndex') ->willReturn(new \ArrayObject([$indexData, $indexData])); diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/AttributeTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/AttributeTest.php index abc0fdd1069f..69e2c33d02d1 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/AttributeTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/AttributeTest.php @@ -321,10 +321,6 @@ public function testGetItemsWithoutApply() ->method('build') ->will($this->returnValue($builtData)); - $this->fulltextCollection->expects($this->once()) - ->method('getSize') - ->will($this->returnValue(50)); - $expectedFilterItems = [ $this->createFilterItem(0, $builtData[0]['label'], $builtData[0]['value'], $builtData[0]['count']), $this->createFilterItem(1, $builtData[1]['label'], $builtData[1]['value'], $builtData[1]['count']), @@ -383,9 +379,6 @@ public function testGetItemsOnlyWithResults() $this->fulltextCollection->expects($this->once()) ->method('getFacetedData') ->willReturn($facetedData); - $this->fulltextCollection->expects($this->once()) - ->method('getSize') - ->will($this->returnValue(50)); $this->itemDataBuilder->expects($this->once()) ->method('addItemData') 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 21d67bdf53c5..683070c28623 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,12 +7,20 @@ 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 * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @deprecated + * @see \Magento\ElasticSearch */ class CollectionTest extends BaseCollection { @@ -77,6 +85,42 @@ protected function setUp() $productLimitationFactoryMock->method('create') ->willReturn($productLimitationMock); + $searchCriteriaResolver = $this->getMockBuilder(SearchCriteriaResolverInterface::class) + ->disableOriginalConstructor() + ->setMethods(['resolve']) + ->getMockForAbstractClass(); + $searchCriteriaResolverFactory = $this->getMockBuilder(SearchCriteriaResolverFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $searchCriteriaResolverFactory->expects($this->any()) + ->method('create') + ->willReturn($searchCriteriaResolver); + + $searchResultApplier = $this->getMockBuilder(SearchResultApplierInterface::class) + ->disableOriginalConstructor() + ->setMethods(['apply']) + ->getMockForAbstractClass(); + $searchResultApplierFactory = $this->getMockBuilder(SearchResultApplierFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $searchResultApplierFactory->expects($this->any()) + ->method('create') + ->willReturn($searchResultApplier); + + $totalRecordsResolver = $this->getMockBuilder(TotalRecordsResolverInterface::class) + ->disableOriginalConstructor() + ->setMethods(['resolve']) + ->getMockForAbstractClass(); + $totalRecordsResolverFactory = $this->getMockBuilder(TotalRecordsResolverFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $totalRecordsResolverFactory->expects($this->any()) + ->method('create') + ->willReturn($totalRecordsResolver); + $this->advancedCollection = $this->objectManager->getObject( \Magento\CatalogSearch\Model\ResourceModel\Advanced\Collection::class, [ @@ -88,6 +132,10 @@ protected function setUp() 'temporaryStorageFactory' => $this->temporaryStorageFactory, 'search' => $this->search, 'productLimitationFactory' => $productLimitationFactoryMock, + 'collectionProvider' => null, + 'searchCriteriaResolverFactory' => $searchCriteriaResolverFactory, + 'searchResultApplierFactory' => $searchResultApplierFactory, + 'totalRecordsResolverFactory' => $totalRecordsResolverFactory ] ); } @@ -115,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 e99d75c25f5c..9ea103e23d2a 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/BaseCollection.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/BaseCollection.php @@ -9,6 +9,9 @@ * Base class for Collection tests. * * Contains helper methods to get commonly used mocks used for collection tests. + * + * @deprecated + * @see \Magento\ElasticSearch **/ class BaseCollection extends \PHPUnit\Framework\TestCase { 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 a3b1d2fd0f2b..9170b81dc318 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Fulltext/CollectionTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Fulltext/CollectionTest.php @@ -5,6 +5,12 @@ */ namespace Magento\CatalogSearch\Test\Unit\Model\ResourceModel\Fulltext; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverFactory; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverInterface; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierFactory; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverFactory; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierInterface; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverInterface; use Magento\CatalogSearch\Test\Unit\Model\ResourceModel\BaseCollection; use Magento\Framework\Search\Adapter\Mysql\TemporaryStorageFactory; use PHPUnit_Framework_MockObject_MockObject as MockObject; @@ -97,6 +103,41 @@ protected function setUp() $temporaryStorageFactory->expects($this->any()) ->method('create') ->willReturn($this->temporaryStorage); + $searchCriteriaResolver = $this->getMockBuilder(SearchCriteriaResolverInterface::class) + ->disableOriginalConstructor() + ->setMethods(['resolve']) + ->getMockForAbstractClass(); + $searchCriteriaResolverFactory = $this->getMockBuilder(SearchCriteriaResolverFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $searchCriteriaResolverFactory->expects($this->any()) + ->method('create') + ->willReturn($searchCriteriaResolver); + + $searchResultApplier = $this->getMockBuilder(SearchResultApplierInterface::class) + ->disableOriginalConstructor() + ->setMethods(['apply']) + ->getMockForAbstractClass(); + $searchResultApplierFactory = $this->getMockBuilder(SearchResultApplierFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $searchResultApplierFactory->expects($this->any()) + ->method('create') + ->willReturn($searchResultApplier); + + $totalRecordsResolver = $this->getMockBuilder(TotalRecordsResolverInterface::class) + ->disableOriginalConstructor() + ->setMethods(['resolve']) + ->getMockForAbstractClass(); + $totalRecordsResolverFactory = $this->getMockBuilder(TotalRecordsResolverFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $totalRecordsResolverFactory->expects($this->any()) + ->method('create') + ->willReturn($totalRecordsResolver); $this->model = $this->objectManager->getObject( \Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection::class, @@ -106,6 +147,9 @@ protected function setUp() 'scopeConfig' => $this->scopeConfig, 'temporaryStorageFactory' => $temporaryStorageFactory, 'productLimitationFactory' => $productLimitationFactoryMock, + 'searchCriteriaResolverFactory' => $searchCriteriaResolverFactory, + 'searchResultApplierFactory' => $searchResultApplierFactory, + 'totalRecordsResolverFactory' => $totalRecordsResolverFactory, ] ); @@ -124,37 +168,10 @@ protected function tearDown() $reflectionProperty->setValue(null); } - /** - * @expectedException \Exception - * @expectedExceptionCode 333 - * @expectedExceptionMessage setRequestName - */ - public function testGetFacetedDataWithException() - { - $criteria = $this->createMock(\Magento\Framework\Api\Search\SearchCriteria::class); - $this->criteriaBuilder->expects($this->once())->method('create')->willReturn($criteria); - $criteria->expects($this->once()) - ->method('setRequestName') - ->withConsecutive(['catalog_view_container']) - ->willThrowException(new \Exception('setRequestName', 333)); - $this->model->getFacetedData('field'); - } - public function testGetFacetedDataWithEmptyAggregations() { - $criteria = $this->createMock(\Magento\Framework\Api\Search\SearchCriteria::class); - $this->criteriaBuilder->expects($this->once())->method('create')->willReturn($criteria); - $criteria->expects($this->once()) - ->method('setRequestName') - ->withConsecutive(['catalog_view_container']); $searchResult = $this->getMockBuilder(\Magento\Framework\Api\Search\SearchResultInterface::class) ->getMockForAbstractClass(); - $table = $this->getMockBuilder(\Magento\Framework\DB\Ddl\Table::class) - ->setMethods(['getName']) - ->getMock(); - $this->temporaryStorage->expects($this->once()) - ->method('storeApiDocuments') - ->willReturn($table); $this->search->expects($this->once()) ->method('search') ->willReturn($searchResult); diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Setup/PropertyMapperTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Setup/PropertyMapperTest.php new file mode 100644 index 000000000000..5c917b360f14 --- /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/Test/Unit/Model/Search/BaseSelectStrategy/StrategyMapperTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/BaseSelectStrategy/StrategyMapperTest.php index 5fa5b0333c6b..b168e7281176 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/BaseSelectStrategy/StrategyMapperTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/BaseSelectStrategy/StrategyMapperTest.php @@ -11,6 +11,10 @@ use \Magento\CatalogSearch\Model\Search\BaseSelectStrategy\StrategyMapper; use \Magento\CatalogSearch\Model\Search\SelectContainer\SelectContainer; +/** + * @deprecated + * @see \Magento\ElasticSearch + */ class StrategyMapperTest extends \PHPUnit\Framework\TestCase { /** @var BaseSelectAttributesSearchStrategy|\PHPUnit_Framework_MockObject_MockObject */ diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/ExclusionStrategyTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/ExclusionStrategyTest.php index 09591532f9f0..e693807760ea 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/ExclusionStrategyTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/ExclusionStrategyTest.php @@ -17,6 +17,8 @@ /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @deprecated + * @see \Magento\ElasticSearch */ class ExclusionStrategyTest extends \PHPUnit\Framework\TestCase { diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/FilterContextTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/FilterContextTest.php index e16f23ca09d2..12ce0d63ac68 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/FilterContextTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/FilterContextTest.php @@ -16,6 +16,10 @@ use Magento\Framework\Search\Request\FilterInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +/** + * @deprecated + * @see \Magento\ElasticSearch + */ class FilterContextTest extends \PHPUnit\Framework\TestCase { /** diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/TermDropdownStrategyTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/TermDropdownStrategyTest.php index 8771c92039f4..e064f46655d9 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/TermDropdownStrategyTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/TermDropdownStrategyTest.php @@ -18,6 +18,9 @@ /** * Class TermDropdownStrategyTest. * Unit test for \Magento\CatalogSearch\Model\Search\FilterMapper\TermDropdownStrategy. + * + * @deprecated + * @see \Magento\ElasticSearch */ class TermDropdownStrategyTest extends \PHPUnit\Framework\TestCase { diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/IndexBuilderTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/IndexBuilderTest.php index da7cfa1ea982..b066c118ef78 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/IndexBuilderTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/IndexBuilderTest.php @@ -27,6 +27,8 @@ * Test for \Magento\CatalogSearch\Model\Search\IndexBuilder * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @deprecated + * @see \Magento\ElasticSearch */ class IndexBuilderTest extends \PHPUnit\Framework\TestCase { diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/TableMapperTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/TableMapperTest.php index cfddc07bceec..db4675526153 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/TableMapperTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/TableMapperTest.php @@ -18,7 +18,10 @@ /** * Test for \Magento\CatalogSearch\Model\Search\TableMapper + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @deprecated + * @see \Magento\ElasticSearch */ class TableMapperTest extends \PHPUnit\Framework\TestCase { diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Plugin/EnableEavIndexerTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Plugin/EnableEavIndexerTest.php index 0eac2e3309ae..b20fdde7c521 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Plugin/EnableEavIndexerTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Plugin/EnableEavIndexerTest.php @@ -9,6 +9,10 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +/** + * @deprecated + * @see \Magento\ElasticSearch + */ class EnableEavIndexerTest extends \PHPUnit\Framework\TestCase { /** diff --git a/app/code/Magento/CatalogSearch/composer.json b/app/code/Magento/CatalogSearch/composer.json index 1000e349b6f9..7bcb91e94541 100644 --- a/app/code/Magento/CatalogSearch/composer.json +++ b/app/code/Magento/CatalogSearch/composer.json @@ -1,6 +1,6 @@ { "name": "magento/module-catalog-search", - "description": "N/A", + "description": "Catalog search", "config": { "sort-packages": true }, diff --git a/app/code/Magento/CatalogSearch/etc/adminhtml/system.xml b/app/code/Magento/CatalogSearch/etc/adminhtml/system.xml index b8f2863139e9..c358062b88a4 100644 --- a/app/code/Magento/CatalogSearch/etc/adminhtml/system.xml +++ b/app/code/Magento/CatalogSearch/etc/adminhtml/system.xml @@ -19,13 +19,15 @@ <field id="engine" canRestore="1"> <backend_model>Magento\CatalogSearch\Model\Adminhtml\System\Config\Backend\Engine</backend_model> </field> - <field id="min_query_length" translate="label" type="text" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="min_query_length" translate="label comment" type="text" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Minimal Query Length</label> <validate>validate-digits</validate> + <comment>This value must be compatible with the corresponding setting in the configured search engine. Be aware: a low query length limit may cause the performance impact.</comment> </field> - <field id="max_query_length" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="max_query_length" translate="label comment" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Maximum Query Length</label> <validate>validate-digits</validate> + <comment>This value must be compatible with the corresponding setting in the configured search engine.</comment> </field> <field id="max_count_cacheable_search_terms" translate="label comment" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Number of top search results to cache</label> diff --git a/app/code/Magento/CatalogSearch/etc/config.xml b/app/code/Magento/CatalogSearch/etc/config.xml index 66b79226c9f3..7ea15c6caa59 100644 --- a/app/code/Magento/CatalogSearch/etc/config.xml +++ b/app/code/Magento/CatalogSearch/etc/config.xml @@ -13,7 +13,7 @@ </seo> <search> <engine>mysql</engine> - <min_query_length>1</min_query_length> + <min_query_length>3</min_query_length> <max_query_length>128</max_query_length> <max_count_cacheable_search_terms>100</max_count_cacheable_search_terms> <autocomplete_limit>8</autocomplete_limit> diff --git a/app/code/Magento/CatalogSearch/etc/di.xml b/app/code/Magento/CatalogSearch/etc/di.xml index cc07384d4c52..7359bd6b454b 100644 --- a/app/code/Magento/CatalogSearch/etc/di.xml +++ b/app/code/Magento/CatalogSearch/etc/di.xml @@ -14,6 +14,10 @@ <preference for="Magento\CatalogSearch\Model\Search\FilterMapper\FilterStrategyInterface" type="Magento\CatalogSearch\Model\Search\FilterMapper\FilterContext"/> <preference for="Magento\CatalogSearch\Model\Indexer\IndexSwitcherInterface" type="Magento\CatalogSearch\Model\Indexer\IndexSwitcherProxy"/> <preference for="Magento\CatalogSearch\Model\Adapter\Aggregation\RequestCheckerInterface" type="Magento\CatalogSearch\Model\Adapter\Aggregation\RequestCheckerComposite"/> + <preference for="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverInterface" type="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolver"/> + <preference for="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierInterface" type="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplier"/> + <preference for="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverInterface" type="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolver"/> + <preference for="Magento\CatalogSearch\Model\Search\ItemCollectionProviderInterface" type="Magento\CatalogSearch\Model\Search\ItemCollectionProvider"/> <type name="Magento\CatalogSearch\Model\Indexer\IndexerHandlerFactory"> <arguments> <argument name="handlers" xsi:type="array"> @@ -176,8 +180,13 @@ <argument name="collectionFactory" xsi:type="object">Magento\CatalogSearch\Model\ResourceModel\Fulltext\SearchCollectionFactory</argument> </arguments> </virtualType> - <virtualType name="Magento\CatalogSearch\Model\ResourceModel\Advanced\CollectionFactory" - type="Magento\Catalog\Model\ResourceModel\Product\CollectionFactory"> + + <virtualType name="Magento\CatalogSearch\Model\Advanced\ItemCollectionProvider" type="Magento\Catalog\Model\Layer\Search\ItemCollectionProvider"> + <arguments> + <argument name="collectionFactory" xsi:type="object">Magento\CatalogSearch\Model\ResourceModel\Advanced\CollectionFactory</argument> + </arguments> + </virtualType> + <virtualType name="Magento\CatalogSearch\Model\ResourceModel\Advanced\CollectionFactory" type="Magento\Catalog\Model\ResourceModel\Product\CollectionFactory"> <arguments> <argument name="instanceName" xsi:type="string">Magento\CatalogSearch\Model\ResourceModel\Advanced\Collection</argument> </arguments> @@ -185,6 +194,21 @@ <type name="Magento\CatalogSearch\Model\Advanced"> <arguments> <argument name="productCollectionFactory" xsi:type="object">Magento\CatalogSearch\Model\ResourceModel\Advanced\CollectionFactory</argument> + <argument name="collectionProvider" xsi:type="object">Magento\CatalogSearch\Model\Search\ItemCollectionProviderInterface</argument> + </arguments> + </type> + <type name="Magento\CatalogSearch\Model\Search\ItemCollectionProvider"> + <arguments> + <argument name="factories" xsi:type="array"> + <item name="mysql" xsi:type="object">Magento\CatalogSearch\Model\ResourceModel\Advanced\CollectionFactory</item> + </argument> + </arguments> + </type> + <type name="Magento\CatalogSearch\Model\Advanced\ProductCollectionPrepareStrategyProvider"> + <arguments> + <argument name="strategies" xsi:type="array"> + <item name="mysql" xsi:type="object">Magento\CatalogSearch\Model\Advanced\ProductCollectionPrepareStrategy</item> + </argument> </arguments> </type> <virtualType name="Magento\CatalogSearch\Model\Layer\Category\Context" type="Magento\Catalog\Model\Layer\Category\Context"> @@ -340,4 +364,11 @@ <type name="Magento\Config\Model\Config"> <plugin name="config_enable_eav_indexer" type="Magento\CatalogSearch\Plugin\EnableEavIndexer" /> </type> + <type name="Magento\Eav\Model\Entity\Setup\PropertyMapper\Composite"> + <arguments> + <argument name="propertyMappers" xsi:type="array"> + <item name="catalog_search" xsi:type="string">Magento\CatalogSearch\Model\ResourceModel\Setup\PropertyMapper</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/CatalogSearch/i18n/en_US.csv b/app/code/Magento/CatalogSearch/i18n/en_US.csv index ba97dc9de1d3..f25d1a589d45 100644 --- a/app/code/Magento/CatalogSearch/i18n/en_US.csv +++ b/app/code/Magento/CatalogSearch/i18n/en_US.csv @@ -38,3 +38,5 @@ name,name "Maximum Query Length","Maximum Query Length" "Rebuild Catalog product fulltext search index","Rebuild Catalog product fulltext search index" "Please enter a valid price range.","Please enter a valid price range." +"This value must be compatible with the corresponding setting in the configured search engine. Be aware: a low query length limit may cause the performance impact.","This value must be compatible with the corresponding setting in the configured search engine. Be aware: a low query length limit may cause the performance impact." +"This value must be compatible with the corresponding setting in the configured search engine.","This value must be compatible with the corresponding setting in the configured search engine." \ No newline at end of file diff --git a/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/result.phtml b/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/result.phtml index 83808df5b95e..3f616ab791df 100644 --- a/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/result.phtml +++ b/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/result.phtml @@ -11,6 +11,8 @@ /** * @var $block \Magento\CatalogSearch\Block\Advanced\Result */ +/** this changes need for valid apply filters and configuration before search process is started */ +$productList = $block->getProductListHtml(); ?> <?php if ($results = $block->getResultCount()): ?> <div class="search found"> @@ -49,6 +51,6 @@ </div> <?php endif; ?> <?php if ($block->getResultCount()): ?> - <div class="search results"><?= $block->getProductListHtml() ?></div> + <div class="search results"><?= /* @escapeNotVerified */ $productList ?></div> <?php endif; ?> <?php $block->getSearchCriterias(); ?> diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenUrlRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenUrlRewriteGenerator.php index 6aa33f37cd31..d18034220cfa 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenUrlRewriteGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenUrlRewriteGenerator.php @@ -10,7 +10,11 @@ use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; use Magento\UrlRewrite\Model\MergeDataProviderFactory; use Magento\Framework\App\ObjectManager; +use Magento\Catalog\Api\CategoryRepositoryInterface; +/** + * Model for generate url rewrites for children categories + */ class ChildrenUrlRewriteGenerator { /** @@ -28,15 +32,22 @@ class ChildrenUrlRewriteGenerator */ private $mergeDataProviderPrototype; + /** + * @var CategoryRepositoryInterface + */ + private $categoryRepository; + /** * @param \Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider $childrenCategoriesProvider * @param \Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGeneratorFactory $categoryUrlRewriteGeneratorFactory * @param \Magento\UrlRewrite\Model\MergeDataProviderFactory|null $mergeDataProviderFactory + * @param CategoryRepositoryInterface|null $categoryRepository */ public function __construct( ChildrenCategoriesProvider $childrenCategoriesProvider, CategoryUrlRewriteGeneratorFactory $categoryUrlRewriteGeneratorFactory, - MergeDataProviderFactory $mergeDataProviderFactory = null + MergeDataProviderFactory $mergeDataProviderFactory = null, + CategoryRepositoryInterface $categoryRepository = null ) { $this->childrenCategoriesProvider = $childrenCategoriesProvider; $this->categoryUrlRewriteGeneratorFactory = $categoryUrlRewriteGeneratorFactory; @@ -44,6 +55,8 @@ public function __construct( $mergeDataProviderFactory = ObjectManager::getInstance()->get(MergeDataProviderFactory::class); } $this->mergeDataProviderPrototype = $mergeDataProviderFactory->create(); + $this->categoryRepository = $categoryRepository + ?: ObjectManager::getInstance()->get(CategoryRepositoryInterface::class); } /** @@ -53,18 +66,23 @@ public function __construct( * @param \Magento\Catalog\Model\Category $category * @param int|null $rootCategoryId * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function generate($storeId, Category $category, $rootCategoryId = null) { $mergeDataProvider = clone $this->mergeDataProviderPrototype; - foreach ($this->childrenCategoriesProvider->getChildren($category, true) as $childCategory) { - $childCategory->setStoreId($storeId); - $childCategory->setData('save_rewrites_history', $category->getData('save_rewrites_history')); - /** @var CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator */ - $categoryUrlRewriteGenerator = $this->categoryUrlRewriteGeneratorFactory->create(); - $mergeDataProvider->merge( - $categoryUrlRewriteGenerator->generate($childCategory, false, $rootCategoryId) - ); + $childrenIds = $this->childrenCategoriesProvider->getChildrenIds($category, true); + if ($childrenIds) { + foreach ($childrenIds as $childId) { + /** @var Category $childCategory */ + $childCategory = $this->categoryRepository->get($childId, $storeId); + $childCategory->setData('save_rewrites_history', $category->getData('save_rewrites_history')); + /** @var CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator */ + $categoryUrlRewriteGenerator = $this->categoryUrlRewriteGeneratorFactory->create(); + $mergeDataProvider->merge( + $categoryUrlRewriteGenerator->generate($childCategory, false, $rootCategoryId) + ); + } } return $mergeDataProvider->getData(); diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Category/Move.php b/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Category/Move.php index 17d12ba563eb..f3984bf7d62a 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Category/Move.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Category/Move.php @@ -6,9 +6,14 @@ namespace Magento\CatalogUrlRewrite\Model\Category\Plugin\Category; use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\CategoryFactory; use Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator; use Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider; +use Magento\Store\Model\Store; +/** + * Perform url updating for children categories. + */ class Move { /** @@ -21,16 +26,24 @@ class Move */ private $childrenCategoriesProvider; + /** + * @var CategoryFactory + */ + private $categoryFactory; + /** * @param CategoryUrlPathGenerator $categoryUrlPathGenerator * @param ChildrenCategoriesProvider $childrenCategoriesProvider + * @param CategoryFactory $categoryFactory */ public function __construct( CategoryUrlPathGenerator $categoryUrlPathGenerator, - ChildrenCategoriesProvider $childrenCategoriesProvider + ChildrenCategoriesProvider $childrenCategoriesProvider, + CategoryFactory $categoryFactory ) { $this->categoryUrlPathGenerator = $categoryUrlPathGenerator; $this->childrenCategoriesProvider = $childrenCategoriesProvider; + $this->categoryFactory = $categoryFactory; } /** @@ -51,20 +64,57 @@ public function afterChangeParent( Category $newParent, $afterCategoryId ) { - $category->setUrlPath($this->categoryUrlPathGenerator->getUrlPath($category)); - $category->getResource()->saveAttribute($category, 'url_path'); - $this->updateUrlPathForChildren($category); + $categoryStoreId = $category->getStoreId(); + foreach ($category->getStoreIds() as $storeId) { + $category->setStoreId($storeId); + if (!$this->isGlobalScope($storeId)) { + $this->updateCategoryUrlKeyForStore($category); + $category->unsUrlPath(); + $category->setUrlPath($this->categoryUrlPathGenerator->getUrlPath($category)); + $category->getResource()->saveAttribute($category, 'url_path'); + $this->updateUrlPathForChildren($category); + } + } + $category->setStoreId($categoryStoreId); return $result; } /** + * Set category url_key according to current category store id. + * + * @param Category $category + * @return void + */ + private function updateCategoryUrlKeyForStore(Category $category) + { + $item = $this->categoryFactory->create(); + $item->setStoreId($category->getStoreId()); + $item->load($category->getId()); + $category->setUrlKey($item->getUrlKey()); + } + + /** + * Check is global scope. + * + * @param int|null $storeId + * @return bool + */ + private function isGlobalScope($storeId) + { + return null === $storeId || $storeId == Store::DEFAULT_STORE_ID; + } + + /** + * Updates url_path for child categories. + * * @param Category $category * @return void */ - protected function updateUrlPathForChildren($category) + private function updateUrlPathForChildren($category) { foreach ($this->childrenCategoriesProvider->getChildren($category, true) as $childCategory) { + $childCategory->setStoreId($category->getStoreId()); $childCategory->unsUrlPath(); $childCategory->setUrlPath($this->categoryUrlPathGenerator->getUrlPath($childCategory)); $childCategory->getResource()->saveAttribute($childCategory, 'url_path'); diff --git a/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlPathGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlPathGenerator.php index ee20b0e934b5..cba9218ce7c7 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlPathGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlPathGenerator.php @@ -8,6 +8,9 @@ use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Catalog\Model\Category; +/** + * Class for generation category url_path + */ class CategoryUrlPathGenerator { /** @@ -61,9 +64,11 @@ public function __construct( * Build category URL path * * @param \Magento\Catalog\Api\Data\CategoryInterface|\Magento\Framework\Model\AbstractModel $category + * @param null|\Magento\Catalog\Api\Data\CategoryInterface|\Magento\Framework\Model\AbstractModel $parentCategory * @return string + * @throws \Magento\Framework\Exception\NoSuchEntityException */ - public function getUrlPath($category) + public function getUrlPath($category, $parentCategory = null) { if (in_array($category->getParentId(), [Category::ROOT_CATEGORY_ID, Category::TREE_ROOT_ID])) { return ''; @@ -77,15 +82,17 @@ public function getUrlPath($category) return $category->getUrlPath(); } if ($this->isNeedToGenerateUrlPathForParent($category)) { - $parentPath = $this->getUrlPath( - $this->categoryRepository->get($category->getParentId(), $category->getStoreId()) - ); + $parentCategory = $parentCategory === null ? + $this->categoryRepository->get($category->getParentId(), $category->getStoreId()) : $parentCategory; + $parentPath = $this->getUrlPath($parentCategory); $path = $parentPath === '' ? $path : $parentPath . '/' . $path; } return $path; } /** + * Define whether we should generate URL path for parent + * * @param \Magento\Catalog\Model\Category $category * @return bool */ diff --git a/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGenerator.php index b2da0ab39f31..a86604672e2b 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGenerator.php @@ -15,6 +15,9 @@ use Magento\Framework\App\ObjectManager; use Magento\UrlRewrite\Model\MergeDataProviderFactory; +/** + * Generate list of urls. + */ class CategoryUrlRewriteGenerator { /** Entity type code */ @@ -84,6 +87,8 @@ public function __construct( } /** + * Generate list of urls. + * * @param \Magento\Catalog\Model\Category $category * @param bool $overrideStoreUrls * @param int|null $rootCategoryId @@ -119,6 +124,7 @@ protected function generateForGlobalScope( $mergeDataProvider = clone $this->mergeDataProviderPrototype; $categoryId = $category->getId(); foreach ($category->getStoreIds() as $storeId) { + $category->setStoreId($storeId); if (!$this->isGlobalScope($storeId) && $this->isOverrideUrlsForStore($storeId, $categoryId, $overrideStoreUrls) ) { @@ -131,6 +137,8 @@ protected function generateForGlobalScope( } /** + * Checks if urls should be overridden for store. + * * @param int $storeId * @param int $categoryId * @param bool $overrideStoreUrls diff --git a/app/code/Magento/CatalogUrlRewrite/Model/ObjectRegistry.php b/app/code/Magento/CatalogUrlRewrite/Model/ObjectRegistry.php index 019959f9c1fe..a048c216139e 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/ObjectRegistry.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/ObjectRegistry.php @@ -5,6 +5,9 @@ */ namespace Magento\CatalogUrlRewrite\Model; +/** + * Class ObjectRegistry + */ class ObjectRegistry { /** @@ -26,15 +29,19 @@ public function __construct($entities) } /** + * Get Entity + * * @param int $entityId * @return \Magento\Framework\DataObject|null */ public function get($entityId) { - return isset($this->entitiesMap[$entityId]) ? $this->entitiesMap[$entityId] : null; + return $this->entitiesMap[$entityId] ?? null; } /** + * List Entities + * * @return \Magento\Framework\DataObject[] */ public function getList() diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Products/AdaptUrlRewritesToVisibilityAttribute.php b/app/code/Magento/CatalogUrlRewrite/Model/Products/AdaptUrlRewritesToVisibilityAttribute.php new file mode 100644 index 000000000000..f5851cf1e11b --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Model/Products/AdaptUrlRewritesToVisibilityAttribute.php @@ -0,0 +1,127 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogUrlRewrite\Model\Products; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; +use Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator; +use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; +use Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException; +use Magento\UrlRewrite\Model\UrlPersistInterface; +use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; + +/** + * Save/Delete UrlRewrites by Product ID's and visibility + */ +class AdaptUrlRewritesToVisibilityAttribute +{ + /** + * @var CollectionFactory + */ + private $productCollectionFactory; + + /** + * @var ProductUrlRewriteGenerator + */ + private $urlRewriteGenerator; + + /** + * @var UrlPersistInterface + */ + private $urlPersist; + + /** + * @var ProductUrlPathGenerator + */ + private $urlPathGenerator; + + /** + * @param CollectionFactory $collectionFactory + * @param ProductUrlRewriteGenerator $urlRewriteGenerator + * @param UrlPersistInterface $urlPersist + * @param ProductUrlPathGenerator|null $urlPathGenerator + */ + public function __construct( + CollectionFactory $collectionFactory, + ProductUrlRewriteGenerator $urlRewriteGenerator, + UrlPersistInterface $urlPersist, + ProductUrlPathGenerator $urlPathGenerator + ) { + $this->productCollectionFactory = $collectionFactory; + $this->urlRewriteGenerator = $urlRewriteGenerator; + $this->urlPersist = $urlPersist; + $this->urlPathGenerator = $urlPathGenerator; + } + + /** + * Process Url Rewrites according to the products visibility attribute + * + * @param array $productIds + * @param int $visibility + * @throws UrlAlreadyExistsException + */ + public function execute(array $productIds, int $visibility): void + { + $products = $this->getProductsByIds($productIds); + + /** @var Product $product */ + foreach ($products as $product) { + if ($visibility == Visibility::VISIBILITY_NOT_VISIBLE) { + $this->urlPersist->deleteByData( + [ + UrlRewrite::ENTITY_ID => $product->getId(), + UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, + ] + ); + } elseif ($visibility !== Visibility::VISIBILITY_NOT_VISIBLE) { + $product->setVisibility($visibility); + $productUrlPath = $this->urlPathGenerator->getUrlPath($product); + $productUrlRewrite = $this->urlRewriteGenerator->generate($product); + $product->unsUrlPath(); + $product->setUrlPath($productUrlPath); + + try { + $this->urlPersist->replace($productUrlRewrite); + } catch (UrlAlreadyExistsException $e) { + throw new UrlAlreadyExistsException( + __( + 'Can not change the visibility of the product with SKU equals "%1". ' + . 'URL key "%2" for specified store already exists.', + $product->getSku(), + $product->getUrlKey() + ), + $e, + $e->getCode(), + $e->getUrls() + ); + } + } + } + } + + /** + * Get Product Models by Id's + * + * @param array $productIds + * @return array + */ + private function getProductsByIds(array $productIds): array + { + $productCollection = $this->productCollectionFactory->create(); + $productCollection->addAttributeToSelect(ProductInterface::VISIBILITY); + $productCollection->addAttributeToSelect('url_key'); + $productCollection->addFieldToFilter( + 'entity_id', + ['in' => array_unique($productIds)] + ); + + return $productCollection->getItems(); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Model/ResourceModel/Category/Product.php b/app/code/Magento/CatalogUrlRewrite/Model/ResourceModel/Category/Product.php index 685a9d282874..311cc6de7611 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/ResourceModel/Category/Product.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/ResourceModel/Category/Product.php @@ -8,6 +8,9 @@ use Magento\Framework\Model\ResourceModel\Db\AbstractDb; use Magento\UrlRewrite\Model\Storage\DbStorage; +/** + * Product Resource Class + */ class Product extends AbstractDb { /** @@ -38,6 +41,8 @@ protected function _construct() } /** + * Save multiple data + * * @param array $insertData * @return int */ @@ -70,7 +75,10 @@ public function removeMultiple(array $removeData) } /** - * Removes multiple entities from url_rewrite table using entities from catalog_url_rewrite_product_category + * Removes multiple data by filter + * + * Removes multiple entities from url_rewrite table + * using entities from catalog_url_rewrite_product_category * Example: $filter = ['category_id' => [1, 2, 3], 'product_id' => [1, 2, 3]] * * @param array $filter @@ -78,10 +86,7 @@ public function removeMultiple(array $removeData) */ public function removeMultipleByProductCategory(array $filter) { - return $this->getConnection()->delete( - $this->getTable(self::TABLE_NAME), - ['url_rewrite_id in (?)' => $this->prepareSelect($filter)] - ); + return $this->getConnection()->deleteFromSelect($this->prepareSelect($filter), self::TABLE_NAME); } /** diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php index 022a78be0019..7b60c8504976 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php @@ -135,6 +135,7 @@ class AfterImportDataObserver implements ObserverInterface 'url_path', 'name', 'visibility', + 'save_rewrites_history' ]; /** @@ -199,6 +200,7 @@ public function __construct( /** * Action after data import. + * * Save new url rewrites and remove old if exist. * * @param Observer $observer @@ -261,12 +263,15 @@ protected function _populateForUrlGeneration($rowData) if ($this->isGlobalScope($product->getStoreId())) { $this->populateGlobalProduct($product); } else { + $this->storesCache[$product->getStoreId()] = true; $this->addProductToImport($product, $product->getStoreId()); } return $this; } /** + * Add store id to product data. + * * @param \Magento\Catalog\Model\Product $product * @param array $rowData * @return void @@ -436,6 +441,8 @@ protected function currentUrlRewritesRegenerate() } /** + * Generate url-rewrite for outogenerated url-rewirte. + * * @param UrlRewrite $url * @param Category $category * @return array @@ -470,6 +477,8 @@ protected function generateForAutogenerated($url, $category) } /** + * Generate url-rewrite for custom url-rewirte. + * * @param UrlRewrite $url * @param Category $category * @return array @@ -503,6 +512,8 @@ protected function generateForCustom($url, $category) } /** + * Retrieve category from url metadata. + * * @param UrlRewrite $url * @return Category|null|bool */ @@ -517,6 +528,8 @@ protected function retrieveCategoryFromMetadata($url) } /** + * Check, category suited for url-rewrite generation. + * * @param \Magento\Catalog\Model\Category $category * @param int $storeId * @return bool diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteSavingObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteSavingObserver.php index 5d7e323e8b2d..3cfd49b1d210 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteSavingObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteSavingObserver.php @@ -100,16 +100,19 @@ public function execute(\Magento\Framework\Event\Observer $observer) } $mapsGenerated = false; - if ($category->dataHasChangedFor('url_key') - || $category->dataHasChangedFor('is_anchor') - || !empty($category->getChangedProductIds()) - ) { + if ($this->isCategoryHasChanged($category)) { if ($category->dataHasChangedFor('url_key')) { $categoryUrlRewriteResult = $this->categoryUrlRewriteGenerator->generate($category); $this->urlRewriteBunchReplacer->doBunchReplace($categoryUrlRewriteResult); } - $productUrlRewriteResult = $this->urlRewriteHandler->generateProductUrlRewrites($category); - $this->urlRewriteBunchReplacer->doBunchReplace($productUrlRewriteResult); + if ($this->isChangedOnlyProduct($category)) { + $productUrlRewriteResult = + $this->urlRewriteHandler->updateProductUrlRewritesForChangedProduct($category); + $this->urlRewriteBunchReplacer->doBunchReplace($productUrlRewriteResult); + } else { + $productUrlRewriteResult = $this->urlRewriteHandler->generateProductUrlRewrites($category); + $this->urlRewriteBunchReplacer->doBunchReplace($productUrlRewriteResult); + } $mapsGenerated = true; } @@ -119,6 +122,38 @@ public function execute(\Magento\Framework\Event\Observer $observer) } } + /** + * Check is category changed changed. + * + * @param Category $category + * @return bool + */ + private function isCategoryHasChanged(Category $category): bool + { + if ($category->dataHasChangedFor('url_key') + || $category->dataHasChangedFor('is_anchor') + || !empty($category->getChangedProductIds())) { + return true; + } + return false; + } + + /** + * Check is only product changed. + * + * @param Category $category + * @return bool + */ + private function isChangedOnlyProduct(Category $category): bool + { + if (!empty($category->getChangedProductIds()) + && !$category->dataHasChangedFor('is_anchor') + && !$category->dataHasChangedFor('url_key')) { + return true; + } + return false; + } + /** * In case store_id is not set for category then we can assume that it was passed through product import. * Store group must have only one root category, so receiving category's path and checking if one of it parts diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php index eb54f0427c11..713dd6ac0c73 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php @@ -8,11 +8,15 @@ use Magento\Catalog\Model\Category; use Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator; use Magento\CatalogUrlRewrite\Service\V1\StoreViewService; +use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Framework\Event\Observer; use Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider; use Magento\Framework\Event\ObserverInterface; use Magento\Store\Model\Store; +/** + * Class for set or update url path. + */ class CategoryUrlPathAutogeneratorObserver implements ObserverInterface { /** @@ -30,22 +34,32 @@ class CategoryUrlPathAutogeneratorObserver implements ObserverInterface */ protected $storeViewService; + /** + * @var CategoryRepositoryInterface + */ + private $categoryRepository; + /** * @param CategoryUrlPathGenerator $categoryUrlPathGenerator * @param ChildrenCategoriesProvider $childrenCategoriesProvider * @param \Magento\CatalogUrlRewrite\Service\V1\StoreViewService $storeViewService + * @param CategoryRepositoryInterface $categoryRepository */ public function __construct( CategoryUrlPathGenerator $categoryUrlPathGenerator, ChildrenCategoriesProvider $childrenCategoriesProvider, - StoreViewService $storeViewService + StoreViewService $storeViewService, + CategoryRepositoryInterface $categoryRepository ) { $this->categoryUrlPathGenerator = $categoryUrlPathGenerator; $this->childrenCategoriesProvider = $childrenCategoriesProvider; $this->storeViewService = $storeViewService; + $this->categoryRepository = $categoryRepository; } /** + * Method for update/set url path. + * * @param \Magento\Framework\Event\Observer $observer * @return void * @throws \Magento\Framework\Exception\LocalizedException @@ -57,45 +71,69 @@ public function execute(\Magento\Framework\Event\Observer $observer) $useDefaultAttribute = !$category->isObjectNew() && !empty($category->getData('use_default')['url_key']); if ($category->getUrlKey() !== false && !$useDefaultAttribute) { $resultUrlKey = $this->categoryUrlPathGenerator->getUrlKey($category); - if (empty($resultUrlKey)) { - throw new \Magento\Framework\Exception\LocalizedException(__('Invalid URL key')); - } - $category->setUrlKey($resultUrlKey) - ->setUrlPath($this->categoryUrlPathGenerator->getUrlPath($category)); - if (!$category->isObjectNew()) { - $category->getResource()->saveAttribute($category, 'url_path'); - if ($category->dataHasChangedFor('url_path')) { - $this->updateUrlPathForChildren($category); - } + $this->updateUrlKey($category, $resultUrlKey); + } else if ($useDefaultAttribute) { + $resultUrlKey = $category->formatUrlKey($category->getOrigData('name')); + $this->updateUrlKey($category, $resultUrlKey); + $category->setUrlKey(null)->setUrlPath(null); + } + } + + /** + * Update Url Key + * + * @param Category $category + * @param string $urlKey + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function updateUrlKey($category, $urlKey) + { + if (empty($urlKey)) { + throw new \Magento\Framework\Exception\LocalizedException(__('Invalid URL key')); + } + $category->setUrlKey($urlKey) + ->setUrlPath($this->categoryUrlPathGenerator->getUrlPath($category)); + if (!$category->isObjectNew()) { + $category->getResource()->saveAttribute($category, 'url_path'); + if ($category->dataHasChangedFor('url_path')) { + $this->updateUrlPathForChildren($category); } } } /** + * Update url path for children category. + * * @param Category $category * @return void */ protected function updateUrlPathForChildren(Category $category) { - $children = $this->childrenCategoriesProvider->getChildren($category, true); - if ($this->isGlobalScope($category->getStoreId())) { - foreach ($children as $child) { + $childrenIds = $this->childrenCategoriesProvider->getChildrenIds($category, true); + foreach ($childrenIds as $childId) { foreach ($category->getStoreIds() as $storeId) { if ($this->storeViewService->doesEntityHaveOverriddenUrlPathForStore( $storeId, - $child->getId(), + $childId, Category::ENTITY )) { - $child->setStoreId($storeId); + $child = $this->categoryRepository->get($childId, $storeId); $this->updateUrlPathForCategory($child); } } } } else { + $children = $this->childrenCategoriesProvider->getChildren($category, true); foreach ($children as $child) { + /** @var Category $child */ $child->setStoreId($category->getStoreId()); - $this->updateUrlPathForCategory($child); + if ($child->getParentId() === $category->getId()) { + $this->updateUrlPathForCategory($child, $category); + } else { + $this->updateUrlPathForCategory($child); + } } } } @@ -112,13 +150,17 @@ protected function isGlobalScope($storeId) } /** + * Update url path for category. + * * @param Category $category + * @param Category|null $parentCategory * @return void + * @throws \Magento\Framework\Exception\NoSuchEntityException */ - protected function updateUrlPathForCategory(Category $category) + protected function updateUrlPathForCategory(Category $category, Category $parentCategory = null) { $category->unsUrlPath(); - $category->setUrlPath($this->categoryUrlPathGenerator->getUrlPath($category)); + $category->setUrlPath($this->categoryUrlPathGenerator->getUrlPath($category, $parentCategory)); $category->getResource()->saveAttribute($category, 'url_path'); } } diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/ProcessUrlRewriteOnChangeProductVisibilityObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/ProcessUrlRewriteOnChangeProductVisibilityObserver.php new file mode 100644 index 000000000000..2337bb3646bd --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Observer/ProcessUrlRewriteOnChangeProductVisibilityObserver.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogUrlRewrite\Observer; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\CatalogUrlRewrite\Model\Products\AdaptUrlRewritesToVisibilityAttribute; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException; + +/** + * Consider URL rewrites on change product visibility via mass action + */ +class ProcessUrlRewriteOnChangeProductVisibilityObserver implements ObserverInterface +{ + /** + * @var AdaptUrlRewritesToVisibilityAttribute + */ + private $adaptUrlRewritesToVisibility; + + /** + * @param AdaptUrlRewritesToVisibilityAttribute $adaptUrlRewritesToVisibility + */ + public function __construct(AdaptUrlRewritesToVisibilityAttribute $adaptUrlRewritesToVisibility) + { + $this->adaptUrlRewritesToVisibility = $adaptUrlRewritesToVisibility; + } + + /** + * Generate urls for UrlRewrites and save it in storage + * + * @param Observer $observer + * @return void + * @throws UrlAlreadyExistsException + */ + public function execute(Observer $observer) + { + $event = $observer->getEvent(); + $attrData = $event->getAttributesData(); + $productIds = $event->getProductIds(); + $visibility = $attrData[ProductInterface::VISIBILITY] ?? 0; + + if (!$visibility || !$productIds) { + return; + } + + $this->adaptUrlRewritesToVisibility->execute($productIds, (int)$visibility); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserver.php index c4d67f447e2c..6eda8dd0b61e 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserver.php @@ -6,11 +6,15 @@ namespace Magento\CatalogUrlRewrite\Observer; use Magento\Catalog\Model\Product; +use Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator; use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; +use Magento\Framework\App\ObjectManager; use Magento\UrlRewrite\Model\UrlPersistInterface; -use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; use Magento\Framework\Event\ObserverInterface; +/** + * Class ProductProcessUrlRewriteSavingObserver + */ class ProductProcessUrlRewriteSavingObserver implements ObserverInterface { /** @@ -23,22 +27,33 @@ class ProductProcessUrlRewriteSavingObserver implements ObserverInterface */ private $urlPersist; + /** + * @var ProductUrlPathGenerator + */ + private $productUrlPathGenerator; + /** * @param ProductUrlRewriteGenerator $productUrlRewriteGenerator * @param UrlPersistInterface $urlPersist + * @param ProductUrlPathGenerator|null $productUrlPathGenerator */ public function __construct( ProductUrlRewriteGenerator $productUrlRewriteGenerator, - UrlPersistInterface $urlPersist + UrlPersistInterface $urlPersist, + ProductUrlPathGenerator $productUrlPathGenerator = null ) { $this->productUrlRewriteGenerator = $productUrlRewriteGenerator; $this->urlPersist = $urlPersist; + $this->productUrlPathGenerator = $productUrlPathGenerator ?: ObjectManager::getInstance() + ->get(ProductUrlPathGenerator::class); } /** * Generate urls for UrlRewrite and save it in storage + * * @param \Magento\Framework\Event\Observer $observer * @return void + * @throws \Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException */ public function execute(\Magento\Framework\Event\Observer $observer) { @@ -51,6 +66,8 @@ public function execute(\Magento\Framework\Event\Observer $observer) || $product->dataHasChangedFor('visibility') ) { if ($product->isVisibleInSiteVisibility()) { + $product->unsUrlPath(); + $product->setUrlPath($this->productUrlPathGenerator->getUrlPath($product)); $this->urlPersist->replace($this->productUrlRewriteGenerator->generate($product)); } } diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/ProductToWebsiteChangeObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/ProductToWebsiteChangeObserver.php index fc2056e83ec7..44b47faf3d4b 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/ProductToWebsiteChangeObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/ProductToWebsiteChangeObserver.php @@ -13,7 +13,12 @@ use Magento\Store\Model\Store; use Magento\UrlRewrite\Model\UrlPersistInterface; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; +use Magento\Store\Api\StoreWebsiteRelationInterface; +use Magento\Framework\App\ObjectManager; +/** + * Observer to assign the products to website + */ class ProductToWebsiteChangeObserver implements ObserverInterface { /** @@ -36,22 +41,31 @@ class ProductToWebsiteChangeObserver implements ObserverInterface */ protected $request; + /** + * @var StoreWebsiteRelationInterface + */ + private $storeWebsiteRelation; + /** * @param ProductUrlRewriteGenerator $productUrlRewriteGenerator * @param UrlPersistInterface $urlPersist * @param ProductRepositoryInterface $productRepository * @param RequestInterface $request + * @param StoreWebsiteRelationInterface $storeWebsiteRelation */ public function __construct( ProductUrlRewriteGenerator $productUrlRewriteGenerator, UrlPersistInterface $urlPersist, ProductRepositoryInterface $productRepository, - RequestInterface $request + RequestInterface $request, + StoreWebsiteRelationInterface $storeWebsiteRelation = null ) { $this->productUrlRewriteGenerator = $productUrlRewriteGenerator; $this->urlPersist = $urlPersist; $this->productRepository = $productRepository; $this->request = $request; + $this->storeWebsiteRelation = $storeWebsiteRelation ?: + ObjectManager::getInstance()->get(StoreWebsiteRelationInterface::class); } /** @@ -69,12 +83,21 @@ public function execute(\Magento\Framework\Event\Observer $observer) $this->request->getParam('store_id', Store::DEFAULT_STORE_ID) ); - $this->urlPersist->deleteByData([ - UrlRewrite::ENTITY_ID => $product->getId(), - UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, - ]); - if ($product->getVisibility() != Visibility::VISIBILITY_NOT_VISIBLE) { - $this->urlPersist->replace($this->productUrlRewriteGenerator->generate($product)); + if (!empty($this->productUrlRewriteGenerator->generate($product))) { + if ($this->request->getParam('remove_website_ids')) { + foreach ($this->request->getParam('remove_website_ids') as $webId) { + foreach ($this->storeWebsiteRelation->getStoreByWebsiteId($webId) as $storeId) { + $this->urlPersist->deleteByData([ + UrlRewrite::ENTITY_ID => $product->getId(), + UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, + UrlRewrite::STORE_ID => $storeId + ]); + } + } + } + if ($product->getVisibility() != Visibility::VISIBILITY_NOT_VISIBLE) { + $this->urlPersist->replace($this->productUrlRewriteGenerator->generate($product)); + } } } } diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php b/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php index c4ec0bb3a74b..b4a35f323e1b 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php @@ -24,6 +24,8 @@ use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; /** + * Class for management url rewrites. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class UrlRewriteHandler @@ -156,6 +158,30 @@ public function generateProductUrlRewrites(Category $category): array } /** + * Update product url rewrites for changed product. + * + * @param Category $category + * @return array + */ + public function updateProductUrlRewritesForChangedProduct(Category $category): array + { + $mergeDataProvider = clone $this->mergeDataProviderPrototype; + $this->isSkippedProduct[$category->getEntityId()] = []; + $saveRewriteHistory = (bool)$category->getData('save_rewrites_history'); + $storeIds = $this->getCategoryStoreIds($category); + + if ($category->getChangedProductIds()) { + foreach ($storeIds as $storeId) { + $this->generateChangedProductUrls($mergeDataProvider, $category, (int)$storeId, $saveRewriteHistory); + } + } + + return $mergeDataProvider->getData(); + } + + /** + * Delete category rewrites for children. + * * @param Category $category * @return void */ @@ -184,6 +210,8 @@ public function deleteCategoryRewritesForChildren(Category $category) } /** + * Get category products url rewrites. + * * @param Category $category * @param int $storeId * @param bool $saveRewriteHistory diff --git a/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/InputParamsResolver.php b/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/InputParamsResolver.php new file mode 100644 index 000000000000..4e8e3840693a --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/InputParamsResolver.php @@ -0,0 +1,88 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\CatalogUrlRewrite\Plugin\Webapi\Controller\Rest; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Webapi\Rest\Request as RestRequest; + +/** + * Plugin for InputParamsResolver + * + * Used to modify product data with save_rewrites_history flag + */ +class InputParamsResolver +{ + /** + * @var RestRequest + */ + private $request; + + /** + * @param RestRequest $request + */ + public function __construct(RestRequest $request) + { + $this->request = $request; + } + + /** + * Add 'save_rewrites_history' param to the product data + * + * @see \Magento\CatalogUrlRewrite\Plugin\Catalog\Controller\Adminhtml\Product\Initialization\Helper + * @param \Magento\Webapi\Controller\Rest\InputParamsResolver $subject + * @param array $result + * @return array + */ + public function afterResolve(\Magento\Webapi\Controller\Rest\InputParamsResolver $subject, array $result): array + { + $route = $subject->getRoute(); + $serviceMethodName = $route->getServiceMethod(); + $serviceClassName = $route->getServiceClass(); + $requestBodyParams = $this->request->getBodyParams(); + + if ($this->isProductSaveCalled($serviceClassName, $serviceMethodName) + && $this->isCustomAttributesExists($requestBodyParams)) { + foreach ($requestBodyParams['product']['custom_attributes'] as $attribute) { + if ($attribute['attribute_code'] === 'save_rewrites_history') { + foreach ($result as $resultItem) { + if ($resultItem instanceof \Magento\Catalog\Model\Product) { + $resultItem->setData('save_rewrites_history', (bool)$attribute['value']); + break 2; + } + } + break; + } + } + } + return $result; + } + + /** + * Check that product save method called + * + * @param string $serviceClassName + * @param string $serviceMethodName + * @return bool + */ + private function isProductSaveCalled(string $serviceClassName, string $serviceMethodName): bool + { + return $serviceClassName === ProductRepositoryInterface::class && $serviceMethodName === 'save'; + } + + /** + * Check is any custom options exists in product data + * + * @param array $requestBodyParams + * @return bool + */ + private function isCustomAttributesExists(array $requestBodyParams): bool + { + return !empty($requestBodyParams['product']['custom_attributes']); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml new file mode 100644 index 000000000000..30a4290d882f --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml @@ -0,0 +1,82 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUrlForProductRewrittenCorrectlyTest"> + <annotations> + <features value="CatalogUrlRewrite"/> + <title value="Check that URL for product rewritten correctly"/> + <description value="Check that URL for product rewritten correctly"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-97224"/> + <useCaseId value="MAGETWO-64191"/> + <group value="CatalogUrlRewrite"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!--Create product--> + <createData entity="_defaultCategory" stepKey="category"/> + <createData entity="ApiSimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="category"/> + </createData> + </before> + <after> + <!--Delete created data--> + <deleteData createDataKey="createProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open Created product--> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="amOnEditPage"/> + <waitForPageLoad stepKey="waitForEditPage"/> + + <!--Switch to Default Store view--> + <actionGroup ref="SwitchToTheNewStoreView" stepKey="selectSecondStoreView"> + <argument name="storeViewName" value="Default Store View"/> + </actionGroup> + <waitForPageLoad stepKey="waitForStoreViewLoad"/> + + <!--Set use default url--> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickOnSearchEngineOptimization"/> + <waitForElementVisible selector="{{AdminProductSEOSection.useDefaultUrl}}" stepKey="waitForUseDefaultUrlCheckbox"/> + <click selector="{{AdminProductSEOSection.useDefaultUrl}}" stepKey="clickUseDefaultUrlCheckbox"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="$$createProduct.sku$$-new" stepKey="changeUrlKey"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + + <!--Select product and go toUpdate Attribute page--> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="GoToCatalogPageChangingView"/> + <waitForPageLoad stepKey="WaitForPageToLoadFullyChangingView"/> + <actionGroup ref="filterProductGridByName" stepKey="filterBundleProductOptionsDownToName"> + <argument name="product" value="ApiSimpleProduct"/> + </actionGroup> + <click selector="{{AdminProductFiltersSection.allCheckbox}}" stepKey="ClickOnSelectAllCheckBoxChangingView"/> + <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickActionDropdown"/> + <click selector="{{AdminProductGridSection.bulkActionOption('Update attributes')}}" stepKey="clickBulkUpdate"/> + <waitForPageLoad stepKey="waitForUpdateAttributesPageLoad"/> + <seeInCurrentUrl url="{{ProductAttributesEditPage.url}}" stepKey="seeInUrlAttributeUpdatePage"/> + <click selector="{{AdminUpdateAttributesWebsiteSection.website}}" stepKey="clickWebsiteTab"/> + <waitForAjaxLoad stepKey="waitForLoadWebSiteTab"/> + <click selector="{{AdminUpdateAttributesWebsiteSection.addProductToWebsite}}" stepKey="checkAddProductToWebsiteCheckbox"/> + <click selector="{{AdminUpdateAttributesSection.saveButton}}" stepKey="clickSave"/> + <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="Message is added to queue" stepKey="seeSaveSuccess"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitFormToReload1"/> + + <!--Got to Store front product page and check url--> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.sku$$-new)}}" stepKey="navigateToSimpleProductPage"/> + <seeInCurrentUrl url="{{StorefrontProductPage.url($$createProduct.sku$$-new)}}" stepKey="seeProductNewUrl"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/RewriteStoreLevelUrlKeyOfChildCategoryTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/RewriteStoreLevelUrlKeyOfChildCategoryTest.xml new file mode 100644 index 000000000000..67870c51140a --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/RewriteStoreLevelUrlKeyOfChildCategoryTest.xml @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="RewriteStoreLevelUrlKeyOfChildCategoryTest"> + <annotations> + <title value="Rewriting Store-level URL key of child category"/> + <stories value="MAGETWO-91649: #13513: Magento ignore store-level url_key of child category in URL rewrite process for global scope"/> + <description value="Rewriting Store-level URL key of child category"/> + <features value="CatalogUrlRewrite"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-94934"/> + <group value="CatalogUrlRewrite"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView" /> + + <createData entity="_defaultCategory" stepKey="defaultCategory"/> + <createData entity="SubCategoryWithParent" stepKey="subCategory"> + <requiredEntity createDataKey="defaultCategory"/> + </createData> + </before> + + <actionGroup ref="navigateToCreatedCategory" stepKey="navigateToCreatedSubCategory"> + <argument name="Category" value="$$subCategory$$"/> + </actionGroup> + + <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="AdminSwitchStoreViewForSubCategory"/> + + <actionGroup ref="ChangeSeoUrlKeyForSubCategory" stepKey="changeSeoUrlKeyForSubCategory"> + <argument name="value" value="bags-second"/> + </actionGroup> + + <actionGroup ref="navigateToCreatedCategory" stepKey="navigateToCreatedDefaultCategory"> + <argument name="Category" value="$$defaultCategory$$"/> + </actionGroup> + + <actionGroup ref="ChangeSeoUrlKey" stepKey="changeSeoUrlKeyForDefaultCategory"> + <argument name="value" value="gear-global"/> + </actionGroup> + + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefrontPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="storefrontSwitchStoreView"/> + + <actionGroup ref="GoToSubCategoryPage" stepKey="goToSubCategoryPage"> + <argument name="parentCategory" value="$$defaultCategory$$"/> + <argument name="subCategory" value="$$subCategory$$"/> + <argument name="urlPath" value="gear-global/bags-second"/> + </actionGroup> + + <after> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"/> + <actionGroup ref="logout" stepKey="logout"/> + + <deleteData createDataKey="subCategory" stepKey="deleteSubCategory"/> + <deleteData createDataKey="defaultCategory" stepKey="deleteNewRootCategory"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/ChildrenUrlRewriteGeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/ChildrenUrlRewriteGeneratorTest.php index 3f641256b125..f8422c7c05fa 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/ChildrenUrlRewriteGeneratorTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/ChildrenUrlRewriteGeneratorTest.php @@ -31,6 +31,9 @@ class ChildrenUrlRewriteGeneratorTest extends \PHPUnit\Framework\TestCase /** @var \PHPUnit_Framework_MockObject_MockObject */ private $serializerMock; + /** @var \PHPUnit_Framework_MockObject_MockObject */ + private $categoryRepository; + protected function setUp() { $this->serializerMock = $this->getMockBuilder(Json::class) @@ -47,6 +50,9 @@ protected function setUp() $this->categoryUrlRewriteGenerator = $this->getMockBuilder( \Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator::class )->disableOriginalConstructor()->getMock(); + $this->categoryRepository = $this->getMockBuilder( + \Magento\Catalog\Model\CategoryRepository::class + )->disableOriginalConstructor()->getMock(); $mergeDataProviderFactory = $this->createPartialMock( \Magento\UrlRewrite\Model\MergeDataProviderFactory::class, ['create'] @@ -59,14 +65,15 @@ protected function setUp() [ 'childrenCategoriesProvider' => $this->childrenCategoriesProvider, 'categoryUrlRewriteGeneratorFactory' => $this->categoryUrlRewriteGeneratorFactory, - 'mergeDataProviderFactory' => $mergeDataProviderFactory + 'mergeDataProviderFactory' => $mergeDataProviderFactory, + 'categoryRepository' => $this->categoryRepository ] ); } public function testNoChildrenCategories() { - $this->childrenCategoriesProvider->expects($this->once())->method('getChildren')->with($this->category, true) + $this->childrenCategoriesProvider->expects($this->once())->method('getChildrenIds')->with($this->category, true) ->will($this->returnValue([])); $this->assertEquals([], $this->childrenUrlRewriteGenerator->generate('store_id', $this->category)); @@ -76,14 +83,16 @@ public function testGenerate() { $storeId = 'store_id'; $saveRewritesHistory = 'flag'; + $childId = 2; $childCategory = $this->getMockBuilder(\Magento\Catalog\Model\Category::class) ->disableOriginalConstructor()->getMock(); - $childCategory->expects($this->once())->method('setStoreId')->with($storeId); $childCategory->expects($this->once())->method('setData') ->with('save_rewrites_history', $saveRewritesHistory); - $this->childrenCategoriesProvider->expects($this->once())->method('getChildren')->with($this->category, true) - ->will($this->returnValue([$childCategory])); + $this->childrenCategoriesProvider->expects($this->once())->method('getChildrenIds')->with($this->category, true) + ->will($this->returnValue([$childId])); + $this->categoryRepository->expects($this->once())->method('get') + ->with($childId, $storeId)->willReturn($childCategory); $this->category->expects($this->any())->method('getData')->with('save_rewrites_history') ->will($this->returnValue($saveRewritesHistory)); $this->categoryUrlRewriteGeneratorFactory->expects($this->once())->method('create') 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 fbc620a6d741..294cf8562906 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/Category/Plugin/Category/MoveTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/Plugin/Category/MoveTest.php index f91a55c11b97..85e883702715 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/Plugin/Category/MoveTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/Plugin/Category/MoveTest.php @@ -5,6 +5,7 @@ */ namespace Magento\CatalogUrlRewrite\Test\Unit\Model\Category\Plugin\Category; +use Magento\Catalog\Model\CategoryFactory; use Magento\CatalogUrlRewrite\Model\Category\Plugin\Category\Move as CategoryMovePlugin; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator; @@ -39,6 +40,11 @@ class MoveTest extends \PHPUnit\Framework\TestCase */ private $categoryMock; + /** + * @var CategoryFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $categoryFactory; + /** * @var CategoryMovePlugin */ @@ -55,28 +61,44 @@ protected function setUp() ->disableOriginalConstructor() ->setMethods(['getChildren']) ->getMock(); + $this->categoryFactory = $this->getMockBuilder(CategoryFactory::class) + ->disableOriginalConstructor() + ->getMock(); $this->subjectMock = $this->getMockBuilder(CategoryResourceModel::class) ->disableOriginalConstructor() ->getMock(); $this->categoryMock = $this->getMockBuilder(Category::class) ->disableOriginalConstructor() - ->setMethods(['getResource', 'setUrlPath']) + ->setMethods(['getResource', 'setUrlPath', 'getStoreIds', 'getStoreId', 'setStoreId']) ->getMock(); $this->plugin = $this->objectManager->getObject( CategoryMovePlugin::class, [ 'categoryUrlPathGenerator' => $this->categoryUrlPathGeneratorMock, - 'childrenCategoriesProvider' => $this->childrenCategoriesProviderMock + 'childrenCategoriesProvider' => $this->childrenCategoriesProviderMock, + 'categoryFactory' => $this->categoryFactory ] ); } + /** + * Tests url updating for children categories. + */ public function testAfterChangeParent() { $urlPath = 'test/path'; - $this->categoryMock->expects($this->once()) - ->method('getResource') + $storeIds = [1]; + $originalCategory = $this->getMockBuilder(Category::class) + ->disableOriginalConstructor() + ->getMock(); + $this->categoryFactory->method('create') + ->willReturn($originalCategory); + + $this->categoryMock->method('getResource') ->willReturn($this->subjectMock); + $this->categoryMock->expects($this->once()) + ->method('getStoreIds') + ->willReturn($storeIds); $this->childrenCategoriesProviderMock->expects($this->once()) ->method('getChildren') ->with($this->categoryMock, true) @@ -85,9 +107,6 @@ public function testAfterChangeParent() ->method('getUrlPath') ->with($this->categoryMock) ->willReturn($urlPath); - $this->categoryMock->expects($this->once()) - ->method('getResource') - ->willReturn($this->subjectMock); $this->categoryMock->expects($this->once()) ->method('setUrlPath') ->with($urlPath); 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 4855478b8488..c431743fc0b5 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 fd9ab10537f1..3984d949332d 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php @@ -694,7 +694,7 @@ protected function currentUrlRewritesRegeneratorGetCurrentRewritesMocks($current ->disableOriginalConstructor()->getMock(); foreach ($urlRewrite as $key => $value) { $url->expects($this->any()) - ->method('get' . str_replace(' ', '', ucwords(str_replace('_', ' ', $key)))) + ->method('get' . str_replace('_', '', ucwords($key, '_'))) ->will($this->returnValue($value)); } $rewrites[] = $url; diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteMovingObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteMovingObserverTest.php new file mode 100644 index 000000000000..b12da6243a90 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteMovingObserverTest.php @@ -0,0 +1,144 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogUrlRewrite\Test\Unit\Observer; + +use Magento\Catalog\Model\Category; +use Magento\CatalogUrlRewrite\Block\UrlKeyRenderer; +use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; +use Magento\CatalogUrlRewrite\Model\Map\DatabaseMapPool; +use Magento\CatalogUrlRewrite\Model\Map\DataCategoryUrlRewriteDatabaseMap; +use Magento\CatalogUrlRewrite\Model\Map\DataProductUrlRewriteDatabaseMap; +use Magento\CatalogUrlRewrite\Model\UrlRewriteBunchReplacer; +use Magento\CatalogUrlRewrite\Observer\CategoryProcessUrlRewriteMovingObserver; +use Magento\CatalogUrlRewrite\Observer\UrlRewriteHandler; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Event; +use Magento\Framework\Event\Observer; +use Magento\UrlRewrite\Model\UrlPersistInterface; + +/** + * Class CategoryProcessUrlRewriteMovingObserverTest + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class CategoryProcessUrlRewriteMovingObserverTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var CategoryProcessUrlRewriteMovingObserver + */ + private $observer; + + /** + * @var CategoryUrlRewriteGenerator|\PHPUnit_Framework_MockObject_MockObject + */ + private $categoryUrlRewriteGeneratorMock; + + /** + * @var UrlPersistInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $urlPersistMock; + + /** + * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $scopeConfigMock; + + /** + * @var UrlRewriteHandler|\PHPUnit_Framework_MockObject_MockObject + */ + private $urlRewriteHandlerMock; + + /** + * @var DatabaseMapPool|\PHPUnit_Framework_MockObject_MockObject + */ + private $databaseMapPoolMock; + + /** + * Set Up + */ + protected function setUp() + { + $this->categoryUrlRewriteGeneratorMock = $this->createMock(CategoryUrlRewriteGenerator::class); + $this->urlPersistMock = $this->createMock(UrlPersistInterface::class); + $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); + $this->urlRewriteHandlerMock = $this->createMock(UrlRewriteHandler::class); + /** @var UrlRewriteBunchReplacer|\PHPUnit_Framework_MockObject_MockObject $urlRewriteMock */ + $urlRewriteMock = $this->createMock(UrlRewriteBunchReplacer::class); + $this->databaseMapPoolMock = $this->createMock(DatabaseMapPool::class); + + $this->observer = new CategoryProcessUrlRewriteMovingObserver( + $this->categoryUrlRewriteGeneratorMock, + $this->urlPersistMock, + $this->scopeConfigMock, + $this->urlRewriteHandlerMock, + $urlRewriteMock, + $this->databaseMapPoolMock, + [ + DataCategoryUrlRewriteDatabaseMap::class, + DataProductUrlRewriteDatabaseMap::class + ] + ); + } + + /** + * Test category process rewrite url by changing the parent + * + * @return void + */ + public function testCategoryProcessUrlRewriteAfterMovingWithChangedParentId() + { + /** @var Observer|\PHPUnit_Framework_MockObject_MockObject $observerMock */ + $observerMock = $this->createMock(Observer::class); + $eventMock = $this->getMockBuilder(Event::class) + ->disableOriginalConstructor() + ->setMethods(['getCategory']) + ->getMock(); + $categoryMock = $this->createPartialMock(Category::class, [ + 'dataHasChangedFor', + 'getEntityId', + 'getStoreId', + 'setData' + ]); + + $categoryMock->expects($this->once())->method('dataHasChangedFor')->with('parent_id') + ->willReturn(true); + $eventMock->expects($this->once())->method('getCategory')->willReturn($categoryMock); + $observerMock->expects($this->once())->method('getEvent')->willReturn($eventMock); + $this->scopeConfigMock->expects($this->once())->method('isSetFlag') + ->with(UrlKeyRenderer::XML_PATH_SEO_SAVE_HISTORY)->willReturn(true); + $this->categoryUrlRewriteGeneratorMock->expects($this->once())->method('generate') + ->with($categoryMock, true)->willReturn(['category-url-rewrite']); + $this->urlRewriteHandlerMock->expects($this->once())->method('generateProductUrlRewrites') + ->with($categoryMock)->willReturn(['product-url-rewrite']); + $this->databaseMapPoolMock->expects($this->exactly(2))->method('resetMap')->willReturnSelf(); + + $this->observer->execute($observerMock); + } + + /** + * Test category process rewrite url without changing the parent + * + * @return void + */ + public function testCategoryProcessUrlRewriteAfterMovingWithinNotChangedParent() + { + /** @var Observer|\PHPUnit_Framework_MockObject_MockObject $observerMock */ + $observerMock = $this->createMock(Observer::class); + $eventMock = $this->getMockBuilder(Event::class) + ->disableOriginalConstructor() + ->setMethods(['getCategory']) + ->getMock(); + $categoryMock = $this->createPartialMock(Category::class, ['dataHasChangedFor']); + $observerMock->expects($this->once())->method('getEvent')->willReturn($eventMock); + $eventMock->expects($this->once())->method('getCategory')->willReturn($categoryMock); + $categoryMock->expects($this->once())->method('dataHasChangedFor')->with('parent_id') + ->willReturn(false); + + $this->observer->execute($observerMock); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Webapi/Controller/Rest/InputParamsResolverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Webapi/Controller/Rest/InputParamsResolverTest.php new file mode 100644 index 000000000000..8e705b2c09f6 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Webapi/Controller/Rest/InputParamsResolverTest.php @@ -0,0 +1,116 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\CatalogUrlRewrite\Test\Unit\Plugin\Webapi\Controller\Rest; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Webapi\Controller\Rest\InputParamsResolver; +use Magento\CatalogUrlRewrite\Plugin\Webapi\Controller\Rest\InputParamsResolver as InputParamsResolverPlugin; +use Magento\Framework\Webapi\Rest\Request as RestRequest; +use Magento\Catalog\Model\Product; +use Magento\Webapi\Controller\Rest\Router\Route; +use Magento\Catalog\Api\ProductRepositoryInterface; + +/** + * Unit test for InputParamsResolver plugin + */ +class InputParamsResolverTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var string + */ + private $saveRewritesHistory; + + /** + * @var array + */ + private $requestBodyParams; + + /** + * @var array + */ + private $result; + + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var InputParamsResolver|\PHPUnit_Framework_MockObject_MockObject + */ + private $subject; + + /** + * @var RestRequest|\PHPUnit_Framework_MockObject_MockObject + */ + private $request; + + /** + * @var Product|\PHPUnit_Framework_MockObject_MockObject + */ + private $product; + + /** + * @var Route|\PHPUnit_Framework_MockObject_MockObject + */ + private $route; + + /** + * @var InputParamsResolverPlugin + */ + private $plugin; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->saveRewritesHistory = 'save_rewrites_history'; + $this->requestBodyParams = [ + 'product' => [ + 'sku' => 'test', + 'custom_attributes' => [ + ['attribute_code' => $this->saveRewritesHistory, 'value' => 1] + ] + ] + ]; + + $this->route = $this->createPartialMock(Route::class, ['getServiceMethod', 'getServiceClass']); + $this->request = $this->createPartialMock(RestRequest::class, ['getBodyParams']); + $this->request->expects($this->any())->method('getBodyParams')->willReturn($this->requestBodyParams); + $this->subject = $this->createPartialMock(InputParamsResolver::class, ['getRoute']); + $this->subject->expects($this->any())->method('getRoute')->willReturn($this->route); + $this->product = $this->createPartialMock(Product::class, ['setData']); + + $this->result = [false, $this->product, 'test']; + + $this->objectManager = new ObjectManager($this); + $this->plugin = $this->objectManager->getObject( + InputParamsResolverPlugin::class, + [ + 'request' => $this->request + ] + ); + } + + public function testAfterResolve() + { + $this->route->expects($this->once()) + ->method('getServiceClass') + ->willReturn(ProductRepositoryInterface::class); + $this->route->expects($this->once()) + ->method('getServiceMethod') + ->willReturn('save'); + $this->product->expects($this->once()) + ->method('setData') + ->with($this->saveRewritesHistory, true); + + $this->plugin->afterResolve($this->subject, $this->result); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/composer.json b/app/code/Magento/CatalogUrlRewrite/composer.json index e373d8c8c175..b4ceff96b50b 100644 --- a/app/code/Magento/CatalogUrlRewrite/composer.json +++ b/app/code/Magento/CatalogUrlRewrite/composer.json @@ -16,6 +16,9 @@ "magento/module-ui": "*", "magento/module-url-rewrite": "*" }, + "suggest": { + "magento/module-webapi": "*" + }, "type": "magento2-module", "license": [ "OSL-3.0", diff --git a/app/code/Magento/CatalogUrlRewrite/etc/db_schema.xml b/app/code/Magento/CatalogUrlRewrite/etc/db_schema.xml index 174173fa2019..c8da5b59cf5f 100644 --- a/app/code/Magento/CatalogUrlRewrite/etc/db_schema.xml +++ b/app/code/Magento/CatalogUrlRewrite/etc/db_schema.xml @@ -15,16 +15,16 @@ comment="category_id"/> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="false" identity="false" comment="product_id"/> - <constraint xsi:type="foreign" name="CAT_URL_REWRITE_PRD_CTGR_PRD_ID_CAT_PRD_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="CAT_URL_REWRITE_PRD_CTGR_PRD_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_url_rewrite_product_category" column="product_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="FK_BB79E64705D7F17FE181F23144528FC8" + <constraint xsi:type="foreign" referenceId="FK_BB79E64705D7F17FE181F23144528FC8" table="catalog_url_rewrite_product_category" column="url_rewrite_id" referenceTable="url_rewrite" referenceColumn="url_rewrite_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CAT_URL_REWRITE_PRD_CTGR_CTGR_ID_CAT_CTGR_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="CAT_URL_REWRITE_PRD_CTGR_CTGR_ID_CAT_CTGR_ENTT_ENTT_ID" table="catalog_url_rewrite_product_category" column="category_id" referenceTable="catalog_category_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <index name="CATALOG_URL_REWRITE_PRODUCT_CATEGORY_CATEGORY_ID_PRODUCT_ID" indexType="btree"> + <index referenceId="CATALOG_URL_REWRITE_PRODUCT_CATEGORY_CATEGORY_ID_PRODUCT_ID" indexType="btree"> <column name="category_id"/> <column name="product_id"/> </index> diff --git a/app/code/Magento/CatalogUrlRewrite/etc/events.xml b/app/code/Magento/CatalogUrlRewrite/etc/events.xml index cc558fe81f16..728442acf7a4 100644 --- a/app/code/Magento/CatalogUrlRewrite/etc/events.xml +++ b/app/code/Magento/CatalogUrlRewrite/etc/events.xml @@ -27,6 +27,9 @@ <event name="catalog_product_save_after"> <observer name="process_url_rewrite_saving" instance="Magento\CatalogUrlRewrite\Observer\ProductProcessUrlRewriteSavingObserver"/> </event> + <event name="catalog_product_attribute_update_before"> + <observer name="process_url_rewrite_on_change_product_visibility" instance="Magento\CatalogUrlRewrite\Observer\ProcessUrlRewriteOnChangeProductVisibilityObserver"/> + </event> <event name="catalog_category_save_before"> <observer name="category_url_path_autogeneration" instance="Magento\CatalogUrlRewrite\Observer\CategoryUrlPathAutogeneratorObserver"/> </event> diff --git a/app/code/Magento/CatalogUrlRewrite/etc/webapi_rest/di.xml b/app/code/Magento/CatalogUrlRewrite/etc/webapi_rest/di.xml new file mode 100644 index 000000000000..9c5186a5ec0a --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/etc/webapi_rest/di.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:ObjectManager/etc/config.xsd"> + <type name="Magento\Webapi\Controller\Rest\InputParamsResolver"> + <plugin name="product_save_rewrites_history_rest_plugin" type="Magento\CatalogUrlRewrite\Plugin\Webapi\Controller\Rest\InputParamsResolver" sortOrder="1" disabled="false" /> + </type> +</config> diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/etc/module.xml b/app/code/Magento/CatalogUrlRewriteGraphQl/etc/module.xml index be4bb9fcd701..e6e0f6cf7210 100644 --- a/app/code/Magento/CatalogUrlRewriteGraphQl/etc/module.xml +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/etc/module.xml @@ -6,5 +6,10 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> - <module name="Magento_CatalogUrlRewriteGraphQl" /> + <module name="Magento_CatalogUrlRewriteGraphQl" > + <sequence> + <module name="Magento_UrlRewriteGraphQl"/> + <module name="Magento_CatalogGraphQl"/> + </sequence> + </module> </config> diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogUrlRewriteGraphQl/etc/schema.graphqls index b96cfcb03d41..f4ad2e930dda 100644 --- a/app/code/Magento/CatalogUrlRewriteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/etc/schema.graphqls @@ -4,6 +4,7 @@ interface ProductInterface { url_key: String @doc(description: "The part of the URL that identifies the product") url_path: String @doc(description: "The part of the URL that precedes the url_key") + url_rewrites: [UrlRewrite] @doc(description: "URL rewrites list") @resolver(class: "Magento\\UrlRewriteGraphQl\\Model\\Resolver\\UrlRewrite") } input ProductFilterInput { diff --git a/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php b/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php index 9a55f981b760..9e47830debfc 100644 --- a/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php +++ b/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php @@ -6,16 +6,21 @@ namespace Magento\CatalogWidget\Block\Product; +use Magento\Catalog\Model\Product; use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\ActionInterface; use Magento\Framework\DataObject\IdentityInterface; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\View\LayoutFactory; use Magento\Widget\Block\BlockInterface; +use Magento\Framework\Url\EncoderInterface; /** * Catalog Products List widget block - * Class ProductsList + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ class ProductsList extends \Magento\Catalog\Block\Product\AbstractProduct implements BlockInterface, IdentityInterface { @@ -94,6 +99,21 @@ class ProductsList extends \Magento\Catalog\Block\Product\AbstractProduct implem */ private $json; + /** + * @var LayoutFactory + */ + private $layoutFactory; + + /** + * @var \Magento\Framework\Url\EncoderInterface|null + */ + private $urlEncoder; + + /** + * @var \Magento\Framework\View\Element\RendererList + */ + private $rendererListBlock; + /** * @param \Magento\Catalog\Block\Product\Context $context * @param \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory @@ -104,6 +124,10 @@ class ProductsList extends \Magento\Catalog\Block\Product\AbstractProduct implem * @param \Magento\Widget\Helper\Conditions $conditionsHelper * @param array $data * @param Json|null $json + * @param LayoutFactory|null $layoutFactory + * @param \Magento\Framework\Url\EncoderInterface|null $urlEncoder + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Catalog\Block\Product\Context $context, @@ -114,7 +138,9 @@ public function __construct( \Magento\CatalogWidget\Model\Rule $rule, \Magento\Widget\Helper\Conditions $conditionsHelper, array $data = [], - Json $json = null + Json $json = null, + LayoutFactory $layoutFactory = null, + EncoderInterface $urlEncoder = null ) { $this->productCollectionFactory = $productCollectionFactory; $this->catalogProductVisibility = $catalogProductVisibility; @@ -123,6 +149,8 @@ public function __construct( $this->rule = $rule; $this->conditionsHelper = $conditionsHelper; $this->json = $json ?: ObjectManager::getInstance()->get(Json::class); + $this->layoutFactory = $layoutFactory ?: ObjectManager::getInstance()->get(LayoutFactory::class); + $this->urlEncoder = $urlEncoder ?: ObjectManager::getInstance()->get(EncoderInterface::class); parent::__construct( $context, $data @@ -130,7 +158,9 @@ public function __construct( } /** - * {@inheritdoc} + * Internal constructor, that is called from real constructor + * + * @return void */ protected function _construct() { @@ -151,6 +181,7 @@ protected function _construct() * Get key pieces for caching block content * * @return array + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getCacheKeyInfo() { @@ -164,8 +195,9 @@ public function getCacheKeyInfo() $this->_storeManager->getStore()->getId(), $this->_design->getDesignTheme()->getId(), $this->httpContext->getValue(\Magento\Customer\Model\Context::CONTEXT_GROUP), - intval($this->getRequest()->getParam($this->getData('page_var_name'), 1)), + (int) $this->getRequest()->getParam($this->getData('page_var_name'), 1), $this->getProductsPerPage(), + $this->getProductsCount(), $conditions, $this->json->serialize($this->getRequest()->getParams()), $this->getTemplate(), @@ -174,7 +206,7 @@ public function getCacheKeyInfo() } /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.NPathComplexity) */ public function getProductPriceHtml( @@ -196,22 +228,62 @@ public function getProductPriceHtml( ? $arguments['display_minimal_price'] : true; - /** @var \Magento\Framework\Pricing\Render $priceRender */ + /** @var \Magento\Framework\Pricing\Render $priceRender */ $priceRender = $this->getLayout()->getBlock('product.price.render.default'); - - $price = ''; - if ($priceRender) { - $price = $priceRender->render( - \Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE, - $product, - $arguments + if (!$priceRender) { + $priceRender = $this->getLayout()->createBlock( + \Magento\Framework\Pricing\Render::class, + 'product.price.render.default', + ['data' => ['price_render_handle' => 'catalog_product_prices']] ); } + + $price = $priceRender->render( + \Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE, + $product, + $arguments + ); + return $price; } /** - * {@inheritdoc} + * @inheritdoc + */ + protected function getDetailsRendererList() + { + if (empty($this->rendererListBlock)) { + /** @var $layout \Magento\Framework\View\LayoutInterface */ + $layout = $this->layoutFactory->create(['cacheable' => false]); + $layout->getUpdate()->addHandle('catalog_widget_product_list')->load(); + $layout->generateXml(); + $layout->generateElements(); + + $this->rendererListBlock = $layout->getBlock('category.product.type.widget.details.renderers'); + } + return $this->rendererListBlock; + } + + /** + * Get post parameters. + * + * @param Product $product + * @return array + */ + public function getAddToCartPostParams(Product $product) + { + $url = $this->getAddToCartUrl($product); + return [ + 'action' => $url, + 'data' => [ + 'product' => $product->getEntityId(), + ActionInterface::PARAM_NAME_URL_ENCODED => $this->urlEncoder->encode($url), + ] + ]; + } + + /** + * @inheritdoc */ protected function _beforeToHtml() { @@ -223,15 +295,22 @@ protected function _beforeToHtml() * Prepare and return product collection * * @return \Magento\Catalog\Model\ResourceModel\Product\Collection + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function createCollection() { /** @var $collection \Magento\Catalog\Model\ResourceModel\Product\Collection */ $collection = $this->productCollectionFactory->create(); + + if ($this->getData('store_id') !== null) { + $collection->setStoreId($this->getData('store_id')); + } + $collection->setVisibility($this->catalogProductVisibility->getVisibleInCatalogIds()); $collection = $this->_addProductAttributesAndPrices($collection) ->addStoreFilter() + ->addAttributeToSort('created_at', 'desc') ->setPageSize($this->getPageSize()) ->setCurPage($this->getRequest()->getParam($this->getData('page_var_name'), 1)); @@ -249,6 +328,8 @@ public function createCollection() } /** + * Get conditions + * * @return \Magento\Rule\Model\Condition\Combine */ protected function getConditions() @@ -338,7 +419,7 @@ public function getPagerHtml() if (!$this->pager) { $this->pager = $this->getLayout()->createBlock( \Magento\Catalog\Block\Product\Widget\Html\Pager::class, - 'widget.products.list.pager' + $this->getWidgetPagerBlockName() ); $this->pager->setUseContainer(true) @@ -386,8 +467,9 @@ public function getTitle() } /** - * @return PriceCurrencyInterface + * Get currency of product * + * @return PriceCurrencyInterface * @deprecated 100.2.0 */ private function getPriceCurrency() @@ -398,4 +480,37 @@ private function getPriceCurrency() } return $this->priceCurrency; } + + /** + * @inheritdoc + */ + public function getAddToCartUrl($product, $additional = []) + { + $requestingPageUrl = $this->getRequest()->getParam('requesting_page_url'); + + if (!empty($requestingPageUrl)) { + $additional['useUencPlaceholder'] = true; + $url = parent::getAddToCartUrl($product, $additional); + return str_replace('%25uenc%25', $this->urlEncoder->encode($requestingPageUrl), $url); + } + + return parent::getAddToCartUrl($product, $additional); + } + + /** + * Get widget block name + * + * @return string + */ + private function getWidgetPagerBlockName() + { + $pageName = $this->getData('page_var_name'); + $pagerBlockName = 'widget.products.list.pager'; + + if (!$pageName) { + return $pagerBlockName; + } + + return $pagerBlockName . '.' . $pageName; + } } diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/AdminCreateBlockWithWidgetActionGroup.xml b/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/AdminCreateBlockWithWidgetActionGroup.xml new file mode 100644 index 000000000000..1f54ff40283b --- /dev/null +++ b/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/AdminCreateBlockWithWidgetActionGroup.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See 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="AdminCreateBlockWithWidget"> + <arguments> + <argument name="addCondition" type="string"/> + <argument name="isCondition" type="string"/> + <argument name="fieldCondition" type="string"/> + </arguments> + + <click stepKey="clickShowHideButton" selector="{{BlockWYSIWYGSection.ShowHideBtn}}"/> + <waitForElementVisible stepKey="waitForInsertWidgetButton" selector="{{CatalogWidgetSection.insertWidgetButton}}"/> + + <selectOption stepKey="selectAllStoreView" userInput="All Store Views" selector="{{CatalogWidgetSection.storeViewOption}}"/> + <fillField selector="{{BlockContentSection.TextArea}}" userInput="" stepKey="makeContentFieldEmpty"/> + + <click selector="{{CatalogWidgetSection.insertWidgetButton}}" stepKey="clickInsertWidgetButton"/> + <waitForElementVisible stepKey="waitForInsertWidgetFrame" selector="{{InsertWidgetSection.widgetTypeDropDown}}" time="10"/> + + <selectOption selector="{{InsertWidgetSection.widgetTypeDropDown}}" userInput="Catalog Products List" stepKey="selectCatalogProductListOption"/> + <waitForElementVisible stepKey="waitForConditionsElementBecomeAvailable" selector="{{InsertWidgetSection.conditionsAddButton}}"/> + + <click selector="{{InsertWidgetSection.conditionsAddButton}}" stepKey="clickToAddCondition"/> + <waitForElementVisible stepKey="waitForSelectBoxOpened" selector="{{InsertWidgetSection.conditionsSelectBox}}"/> + + <selectOption selector="{{InsertWidgetSection.conditionsSelectBox}}" userInput="{{addCondition}}" stepKey="selectConditionsSelectBox"/> + <waitForElementVisible stepKey="seeConditionsAdded" selector="{{InsertWidgetSection.addCondition('1')}}"/> + + <click selector="{{InsertWidgetSection.conditionIs}}" stepKey="clickToConditionIs"/> + <selectOption selector="{{InsertWidgetSection.conditionOperator('1')}}" stepKey="selectOperatorGreaterThan" userInput="{{isCondition}}"/> + + <click selector="{{InsertWidgetSection.addCondition('1')}}" stepKey="clickAddConditionItem"/> + <waitForElementVisible stepKey="waitForConditionFieldOpened" selector="{{InsertWidgetSection.conditionField('1')}}"/> + + <fillField selector="{{InsertWidgetSection.conditionField('1')}}" stepKey="setOperator" userInput="{{fieldCondition}}"/> + <click selector="{{WidgetSection.InsertWidget}}" stepKey="clickInsertWidget"/> + + <waitForElementVisible stepKey="waitForInsertWidgetSaved" selector="{{InsertWidgetSection.save}}"/> + <click stepKey="clickSaveButton" selector="{{InsertWidgetSection.save}}"/> + <see userInput="You saved the block." stepKey="seeSavedBlockMsgOnForm"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/Section/CatalogWidgetSection.xml b/app/code/Magento/CatalogWidget/Test/Mftf/Section/CatalogWidgetSection.xml new file mode 100644 index 000000000000..855d325c9850 --- /dev/null +++ b/app/code/Magento/CatalogWidget/Test/Mftf/Section/CatalogWidgetSection.xml @@ -0,0 +1,26 @@ +<?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="CatalogWidgetSection"> + <element name="insertWidgetButton" type="button" selector=".scalable.action-add-widget.plugin"/> + <element name="storeViewOption" type="button" selector="//*[@name='store_id']"/> + </section> + + <section name="InsertWidgetSection"> + <element name="widgetTypeDropDown" type="select" selector="#select_widget_type"/> + <element name="conditionsAddButton" type="button" selector=".rule-param.rule-param-new-child"/> + <element name="conditionsSelectBox" type="button" selector="#conditions__1__new_child"/> + <element name="addCondition" type="button" selector="//*[@id='conditions__1--{{arg1}}__value']/../preceding-sibling::a" parameterized="true"/> + <element name="conditionField" type="button" selector="#conditions__1--{{arg2}}__value" parameterized="true"/> + <element name="save" type="button" selector="#save-button"/> + <element name="conditionIs" type="button" selector="//*[@id='conditions__1--1__attribute']/following-sibling::span[1]"/> + <element name="conditionOperator" type="button" selector="#conditions__1--{{arg3}}__operator" parameterized="true"/> + <element name="checkElementStorefrontByPrice" type="button" selector="//*[@class='product-items widget-product-grid']//*[contains(text(),'${{arg4}}.00')]" parameterized="true"/> + <element name="checkElementStorefrontByName" type="button" selector="//*[@class='product-items widget-product-grid']//*[@class='product-item'][{{productPosition}}]//a[contains(text(), '{{productName}}')]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/Section/ProductListWidgetSection.xml b/app/code/Magento/CatalogWidget/Test/Mftf/Section/ProductListWidgetSection.xml new file mode 100644 index 000000000000..03bef8ffa3b7 --- /dev/null +++ b/app/code/Magento/CatalogWidget/Test/Mftf/Section/ProductListWidgetSection.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="ProductListWidgetSection"> + <element name="AddToCartByName" type="button" selector="//*[contains(@class,'product-item-info')][descendant::a[contains(text(), '{{arg1}}')]]//button[contains(@class,'tocart')]" parameterized="true"/> + <element name="AddToCompareByName" type="button" selector="//*[contains(@class,'product-item-info')][descendant::a[contains(text(), '{{arg1}}')]]//button[contains(@class,'tocompare')]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOperatorsTest.xml b/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOperatorsTest.xml new file mode 100644 index 000000000000..32bea8b604cf --- /dev/null +++ b/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOperatorsTest.xml @@ -0,0 +1,157 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="CatalogProductListWidgetOperatorsTest"> + <annotations> + <features value="CatalogWidget"/> + <stories value="MAGETWO-91609: Problems with operator more/less in the 'catalog Products List' widget"/> + <title value="Checking operator more/less in the 'catalog Products List' widget"/> + <description value="Check 'less than', 'equals or greater than', 'equals or less than' operators"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-94479"/> + <group value="CatalogWidget"/> + <group value="WYSIWYGDisabled"/> + </annotations> + + <before> + <createData entity="SimpleSubCategory" stepKey="simplecategory"/> + <createData entity="SimpleProduct" stepKey="createFirstProduct"> + <requiredEntity createDataKey="simplecategory"/> + <field key="price">10</field> + </createData> + <createData entity="SimpleProduct" stepKey="createSecondProduct"> + <requiredEntity createDataKey="simplecategory"/> + <field key="price">50</field> + </createData> + <createData entity="SimpleProduct" stepKey="createThirdProduct"> + <requiredEntity createDataKey="simplecategory"/> + <field key="price">100</field> + </createData> + + <createData entity="_defaultBlock" stepKey="createPreReqBlock"/> + <!--User log in on back-end as admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="EnabledWYSIWYG" stepKey="enableWYSIWYG"/> + </before> + + <!--Open block with widget.--> + <actionGroup ref="navigateToCreatedCMSBlockPage" stepKey="navigateToCreatedCMSBlockPage1"> + <argument name="CMSBlockPage" value="$$createPreReqBlock$$"/> + </actionGroup> + + <actionGroup ref="AdminCreateBlockWithWidget" stepKey="adminCreateBlockWithWidget"> + <argument name="addCondition" value="Price"/> + <argument name="isCondition" value="greater than"/> + <argument name="fieldCondition" value="20"/> + </actionGroup> + + <!--Go to Catalog > Categories (choose category where created products)--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="onCategoryIndexPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoadAddProducts" after="onCategoryIndexPage"/> + <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickExpandAll" after="waitForCategoryPageLoadAddProducts"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="clickCategoryLink"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + + <!--Content > Add CMS Block: name saved block--> + <waitForElementVisible selector="{{AdminCategoryContentSection.sectionHeader}}" stepKey="waitForContentSection"/> + <conditionalClick selector="{{AdminCategoryContentSection.sectionHeader}}" dependentSelector="{{AdminCategoryContentSection.uploadButton}}" visible="false" stepKey="openContentSection"/> + <waitForPageLoad stepKey="waitForContentLoad"/> + + <selectOption selector="{{AdminCategoryContentSection.AddCMSBlock}}" stepKey="selectSavedBlock" userInput="{{_defaultBlock.title}}"/> + + <!--Display Settings > Display Mode: Static block only--> + <waitForElementVisible selector="{{AdminCategoryDisplaySettingsSection.settingsHeader}}" stepKey="waitForDisplaySettingsSection"/> + <conditionalClick selector="{{AdminCategoryDisplaySettingsSection.settingsHeader}}" dependentSelector="{{AdminCategoryDisplaySettingsSection.displayMode}}" visible="false" stepKey="openDisplaySettingsSection"/> + <waitForPageLoad stepKey="waitForDisplaySettingsLoad"/> + <selectOption stepKey="selectStaticBlockOnlyOption" userInput="Static block only" selector="{{AdminCategoryDisplaySettingsSection.displayMode}}"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategoryWithProducts"/> + <waitForPageLoad stepKey="waitForCategorySaved"/> + <see userInput="You saved the category." stepKey="seeSuccessMessage"/> + + <!--Go to Storefront > category--> + <amOnPage url="$$simplecategory.name$$.html" stepKey="goToStorefrontCategoryPage"/> + <waitForPageLoad stepKey="waitForStorefrontPageLoaded"/> + + <!--Check operators Greater than--> + <dontSeeElement selector="{{InsertWidgetSection.checkElementStorefrontByPrice('10')}}" stepKey="dontSeeElementByPrice20"/> + <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByPrice('50')}}" stepKey="seeElementByPrice50"/> + <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByPrice('100')}}" stepKey="seeElementByPrice100"/> + + <!--Open block with widget.--> + <actionGroup ref="navigateToCreatedCMSBlockPage" stepKey="navigateToCreatedCMSBlockPage2"> + <argument name="CMSBlockPage" value="$$createPreReqBlock$$"/> + </actionGroup> + + <actionGroup ref="AdminCreateBlockWithWidget" stepKey="adminCreateBlockWithWidgetLessThan"> + <argument name="addCondition" value="Price"/> + <argument name="isCondition" value="less than"/> + <argument name="fieldCondition" value="20"/> + </actionGroup> + + <!--Go to Storefront > category--> + <amOnPage url="$$simplecategory.name$$.html" stepKey="goToStorefrontCategoryPage2"/> + <waitForPageLoad stepKey="waitForStorefrontPageLoaded2"/> + + <!--Check operators Greater than--> + <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByPrice('10')}}" stepKey="seeElementByPrice20"/> + <dontSeeElement selector="{{InsertWidgetSection.checkElementStorefrontByPrice('50')}}" stepKey="dontSeeElementByPrice50"/> + <dontSeeElement selector="{{InsertWidgetSection.checkElementStorefrontByPrice('100')}}" stepKey="dontSeeElementByPrice100"/> + + <!--Open block with widget.--> + <actionGroup ref="navigateToCreatedCMSBlockPage" stepKey="navigateToCreatedCMSBlockPage3"> + <argument name="CMSBlockPage" value="$$createPreReqBlock$$"/> + </actionGroup> + + <actionGroup ref="AdminCreateBlockWithWidget" stepKey="adminCreateBlockWithWidgetEqualsOrGreaterThan"> + <argument name="addCondition" value="Price"/> + <argument name="isCondition" value="equals or greater than"/> + <argument name="fieldCondition" value="50"/> + </actionGroup> + + <!--Go to Storefront > category--> + <amOnPage url="$$simplecategory.name$$.html" stepKey="goToStorefrontCategoryPage3"/> + <waitForPageLoad stepKey="waitForStorefrontPageLoaded3"/> + + <!--Check operators Greater than--> + <dontSeeElement selector="{{InsertWidgetSection.checkElementStorefrontByPrice('10')}}" stepKey="dontSeeElementByPrice20s"/> + <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByPrice('50')}}" stepKey="seeElementByPrice50s"/> + <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByPrice('100')}}" stepKey="seeElementByPrice100s"/> + + <!--Open block with widget.--> + <actionGroup ref="navigateToCreatedCMSBlockPage" stepKey="navigateToCreatedCMSBlockPage4"> + <argument name="CMSBlockPage" value="$$createPreReqBlock$$"/> + </actionGroup> + + <actionGroup ref="AdminCreateBlockWithWidget" stepKey="adminCreateBlockWithWidgetEqualsOrLessThan"> + <argument name="addCondition" value="Price"/> + <argument name="isCondition" value="equals or less than"/> + <argument name="fieldCondition" value="50"/> + </actionGroup> + + <!--Go to Storefront > category--> + <amOnPage url="$$simplecategory.name$$.html" stepKey="goToStorefrontCategoryPage4"/> + <waitForPageLoad stepKey="waitForStorefrontPageLoaded4"/> + + <!--Check operators Greater than--> + <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByPrice('10')}}" stepKey="seeElementByPrice20s"/> + <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByPrice('50')}}" stepKey="seeElementByPrice50t"/> + <dontSeeElement selector="{{InsertWidgetSection.checkElementStorefrontByPrice('100')}}" stepKey="dontSeeElementByPrice100s"/> + + <after> + <actionGroup ref="DisabledWYSIWYG" stepKey="disableWYSIWYG"/> + <deleteData createDataKey="createPreReqBlock" stepKey="deletePreReqBlock" /> + <deleteData createDataKey="simplecategory" stepKey="deleteSimpleCategory"/> + <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> + <deleteData createDataKey="createSecondProduct" stepKey="deleteSecondProduct"/> + <deleteData createDataKey="createThirdProduct" stepKey="deleteThirdProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOrderTest.xml b/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOrderTest.xml new file mode 100644 index 000000000000..11586207c4d8 --- /dev/null +++ b/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOrderTest.xml @@ -0,0 +1,88 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="CatalogProductListWidgetOrderTest"> + <annotations> + <features value="CatalogWidget"/> + <stories value="MC-5905: Wrong sorting on Products component"/> + <title value="Checking order of products in the 'catalog Products List' widget"/> + <description value="Check that products are ordered with recently added products first"/> + <severity value="MAJOR"/> + <testCaseId value="MC-13794"/> + <group value="CatalogWidget"/> + <group value="WYSIWYGDisabled"/> + <skip> + <issueId value="MC-13923"/> + </skip> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="simplecategory"/> + <createData entity="SimpleProduct" stepKey="createFirstProduct"> + <requiredEntity createDataKey="simplecategory"/> + <field key="price">10</field> + </createData> + <createData entity="SimpleProduct" stepKey="createSecondProduct"> + <requiredEntity createDataKey="simplecategory"/> + <field key="price">20</field> + </createData> + <createData entity="SimpleProduct" stepKey="createThirdProduct"> + <requiredEntity createDataKey="simplecategory"/> + <field key="price">30</field> + </createData> + <createData entity="_defaultCmsPage" stepKey="createPreReqPage"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="EnabledWYSIWYG" stepKey="enableWYSIWYG"/> + </before> + <!--Open created cms page--> + <comment userInput="Open created cms page" stepKey="commentOpenCreatedCmsPage"/> + <actionGroup ref="navigateToCreatedCMSPage" stepKey="navigateToCreatedCMSPage1"> + <argument name="CMSPage" value="$$createPreReqPage$$"/> + </actionGroup> + <!--Add widget to cms page--> + <comment userInput="Add widget to cms page" stepKey="commentAddWidgetToCmsPage"/> + <click selector="{{TinyMCESection.InsertWidgetIcon}}" stepKey="clickInsertWidgetIcon" /> + <waitForPageLoad stepKey="waitForPageLoad1" /> + <selectOption selector="{{WidgetSection.WidgetType}}" userInput="Catalog Products List" stepKey="selectCatalogProductsList" /> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear1" /> + <click selector="{{WidgetSection.AddParam}}" stepKey="clickAddParamBtn" /> + <waitForElementVisible selector="{{WidgetSection.ConditionsDropdown}}" stepKey="waitForDropdownVisible"/> + <selectOption selector="{{WidgetSection.ConditionsDropdown}}" userInput="Category" stepKey="selectCategoryCondition" /> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear2" /> + <click selector="{{WidgetSection.RuleParam}}" stepKey="clickRuleParam" /> + <waitForElementVisible selector="{{WidgetSection.Chooser}}" stepKey="waitForElement" /> + <click selector="{{WidgetSection.Chooser}}" stepKey="clickChooser" /> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear3" /> + <click selector="{{WidgetSection.PreCreateCategory('$$simplecategory.name$$')}}" stepKey="selectCategory" /> + <click selector="{{WidgetSection.InsertWidget}}" stepKey="clickInsertWidget" /> + <waitForPageLoad stepKey="waitForPageLoad2" /> + <!--Save cms page and go to Storefront--> + <comment userInput="Save cms page and go to Storefront" stepKey="commentSaveCmsPageAndGoToStorefront"/> + <click selector="{{CmsNewPagePageActionsSection.expandSplitButton}}" stepKey="expandButtonMenu"/> + <waitForElementVisible selector="{{CmsNewPagePageActionsSection.splitButtonMenu}}" stepKey="waitForSplitButtonMenuVisible"/> + <click selector="{{CmsNewPagePageActionsSection.savePage}}" stepKey="clickSavePage"/> + <see userInput="You saved the page." stepKey="seeSuccessMessage"/> + <amOnPage url="$$createPreReqPage.identifier$$" stepKey="amOnPageTestPage"/> + <waitForPageLoad stepKey="waitForPageLoad3" /> + <!--Check order of products: recently added first--> + <comment userInput="Check order of products: recently added first" stepKey="commentCheckOrderOfProductsRecentlyAddedFirst"/> + <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByName('1','$$createThirdProduct.name$$')}}" stepKey="seeElementByName1"/> + <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByName('2','$$createSecondProduct.name$$')}}" stepKey="seeElementByName2"/> + <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByName('3','$$createFirstProduct.name$$')}}" stepKey="seeElementByName3"/> + <after> + <actionGroup ref="DisabledWYSIWYG" stepKey="disableWYSIWYG"/> + <deleteData createDataKey="createPreReqPage" stepKey="deletePreReqPage" /> + <deleteData createDataKey="simplecategory" stepKey="deleteSimpleCategory"/> + <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> + <deleteData createDataKey="createSecondProduct" stepKey="deleteSecondProduct"/> + <deleteData createDataKey="createThirdProduct" stepKey="deleteThirdProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/CatalogWidget/Test/Unit/Block/Product/ProductsListTest.php b/app/code/Magento/CatalogWidget/Test/Unit/Block/Product/ProductsListTest.php index 5de8b9d9632f..a78975379572 100644 --- a/app/code/Magento/CatalogWidget/Test/Unit/Block/Product/ProductsListTest.php +++ b/app/code/Magento/CatalogWidget/Test/Unit/Block/Product/ProductsListTest.php @@ -167,6 +167,7 @@ public function testGetCacheKeyInfo() 'context_group', 1, 5, + 10, 'some_serialized_conditions', json_encode('request_params'), 'test_template', @@ -274,6 +275,7 @@ public function testCreateCollection($pagerEnable, $productsCount, $productsPerP 'addAttributeToSelect', 'addUrlRewrite', 'addStoreFilter', + 'addAttributeToSort', 'setPageSize', 'setCurPage', 'distinct' @@ -288,6 +290,7 @@ public function testCreateCollection($pagerEnable, $productsCount, $productsPerP $collection->expects($this->once())->method('addAttributeToSelect')->willReturnSelf(); $collection->expects($this->once())->method('addUrlRewrite')->willReturnSelf(); $collection->expects($this->once())->method('addStoreFilter')->willReturnSelf(); + $collection->expects($this->once())->method('addAttributeToSort')->with('created_at', 'desc')->willReturnSelf(); $collection->expects($this->once())->method('setPageSize')->with($expectedPageSize)->willReturnSelf(); $collection->expects($this->once())->method('setCurPage')->willReturnSelf(); $collection->expects($this->once())->method('distinct')->willReturnSelf(); diff --git a/app/code/Magento/CatalogWidget/view/frontend/layout/catalog_widget_product_list.xml b/app/code/Magento/CatalogWidget/view/frontend/layout/catalog_widget_product_list.xml new file mode 100644 index 000000000000..db44d8b62dc1 --- /dev/null +++ b/app/code/Magento/CatalogWidget/view/frontend/layout/catalog_widget_product_list.xml @@ -0,0 +1,17 @@ +<!-- + ~ Copyright © Magento, Inc. All rights reserved. + ~ See COPYING.txt for license details. + --> + +<!-- + ~ Copyright © Magento, Inc. All rights reserved. + ~ See COPYING.txt for license details. + --> + +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <block class="Magento\Framework\View\Element\RendererList" name="category.product.type.widget.details.renderers"> + <block class="Magento\Framework\View\Element\Template" name="category.product.type.details.renderers.default" as="default"/> + </block> + </body> +</page> \ No newline at end of file diff --git a/app/code/Magento/CatalogWidget/view/frontend/templates/product/widget/content/grid.phtml b/app/code/Magento/CatalogWidget/view/frontend/templates/product/widget/content/grid.phtml index f2273f7d44ff..29efe8a8c1c6 100644 --- a/app/code/Magento/CatalogWidget/view/frontend/templates/product/widget/content/grid.phtml +++ b/app/code/Magento/CatalogWidget/view/frontend/templates/product/widget/content/grid.phtml @@ -3,13 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +use Magento\Framework\App\Action\Action; // @codingStandardsIgnoreFile /** @var \Magento\CatalogWidget\Block\Product\ProductsList $block */ ?> <?php if ($exist = ($block->getProductCollection() && $block->getProductCollection()->getSize())): ?> -<?php + <?php $type = 'widget-product-grid'; $mode = 'grid'; @@ -20,14 +21,14 @@ $showWishlist = true; $showCompare = true; $showCart = true; - $templateType = \Magento\Catalog\Block\Product\ReviewRendererInterface::DEFAULT_VIEW; + $templateType = \Magento\Catalog\Block\Product\ReviewRendererInterface::SHORT_VIEW; $description = false; -?> + ?> <div class="block widget block-products-list <?= /* @noEscape */ $mode ?>"> <?php if ($block->getTitle()): ?> - <div class="block-title"> - <strong><?= $block->escapeHtml(__($block->getTitle())) ?></strong> - </div> + <div class="block-title"> + <strong><?= $block->escapeHtml(__($block->getTitle())) ?></strong> + </div> <?php endif ?> <div class="block-content"> <?= /* @noEscape */ '<!-- ' . $image . '-->' ?> @@ -48,57 +49,57 @@ <?= $block->escapeHtml($_item->getName()) ?> </a> </strong> - <?php - echo $block->getProductPriceHtml($_item, $type); - ?> - <?php if ($templateType): ?> <?= $block->getReviewsSummaryHtml($_item, $templateType) ?> <?php endif; ?> + <?= $block->getProductPriceHtml($_item, $type) ?> + + <?= $block->getProductDetailsHtml($_item) ?> + <?php if ($showWishlist || $showCompare || $showCart): ?> - <div class="product-item-actions"> - <?php if ($showCart): ?> - <div class="actions-primary"> - <?php if ($_item->isSaleable()): ?> - <?php if ($_item->getTypeInstance()->hasRequiredOptions($_item)): ?> - <button class="action tocart primary" data-mage-init='{"redirectUrl":{"url":"<?= $block->escapeUrl($block->getAddToCartUrl($_item)) ?>"}}' type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"> - <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> - </button> + <div class="product-item-inner"> + <div class="product-item-actions"> + <?php if ($showCart): ?> + <div class="actions-primary"> + <?php if ($_item->isSaleable()): ?> + <?php $postParams = $block->getAddToCartPostParams($_item); ?> + <form data-role="tocart-form" data-product-sku="<?= $block->escapeHtml($_item->getSku()) ?>" action="<?= /* @NoEscape */ $postParams['action'] ?>" method="post"> + <input type="hidden" name="product" value="<?= /* @escapeNotVerified */ $postParams['data']['product'] ?>"> + <input type="hidden" name="<?= /* @escapeNotVerified */ Action::PARAM_NAME_URL_ENCODED ?>" value="<?= /* @escapeNotVerified */ $postParams['data'][Action::PARAM_NAME_URL_ENCODED] ?>"> + <?= $block->getBlockHtml('formkey') ?> + <button type="submit" + title="<?= $block->escapeHtml(__('Add to Cart')) ?>" + class="action tocart primary"> + <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> + </button> + </form> <?php else: ?> - <?php - $postDataHelper = $this->helper('Magento\Framework\Data\Helper\PostHelper'); - $postData = $postDataHelper->getPostData($block->getAddToCartUrl($_item), ['product' => $_item->getEntityId()]) - ?> - <button class="action tocart primary" data-post='<?= /* @noEscape */ $postData ?>' type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"> - <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> - </button> + <?php if ($_item->getIsSalable()): ?> + <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> + <?php else: ?> + <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> + <?php endif; ?> <?php endif; ?> - <?php else: ?> - <?php if ($_item->getIsSalable()): ?> - <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> - <?php else: ?> - <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> + </div> + <?php endif; ?> + <?php if ($showWishlist || $showCompare): ?> + <div class="actions-secondary" data-role="add-to-links"> + <?php if ($this->helper('Magento\Wishlist\Helper\Data')->isAllow() && $showWishlist): ?> + <a href="#" + data-post='<?= /* @noEscape */ $block->getAddToWishlistParams($_item) ?>' class="action towishlist" data-action="add-to-wishlist" title="<?= $block->escapeHtmlAttr(__('Add to Wish List')) ?>"> + <span><?= $block->escapeHtml(__('Add to Wish List')) ?></span> + </a> + <?php endif; ?> + <?php if ($block->getAddToCompareUrl() && $showCompare): ?> + <?php $compareHelper = $this->helper('Magento\Catalog\Helper\Product\Compare');?> + <a href="#" class="action tocompare" data-post='<?= /* @noEscape */ $compareHelper->getPostDataParams($_item) ?>' title="<?= $block->escapeHtmlAttr(__('Add to Compare')) ?>"> + <span><?= $block->escapeHtml(__('Add to Compare')) ?></span> + </a> <?php endif; ?> - <?php endif; ?> - </div> - <?php endif; ?> - <?php if ($showWishlist || $showCompare): ?> - <div class="actions-secondary" data-role="add-to-links"> - <?php if ($this->helper('Magento\Wishlist\Helper\Data')->isAllow() && $showWishlist): ?> - <a href="#" - data-post='<?= /* @noEscape */ $block->getAddToWishlistParams($_item) ?>' class="action towishlist" data-action="add-to-wishlist" title="<?= $block->escapeHtmlAttr(__('Add to Wish List')) ?>"> - <span><?= $block->escapeHtml(__('Add to Wish List')) ?></span> - </a> - <?php endif; ?> - <?php if ($block->getAddToCompareUrl() && $showCompare): ?> - <?php $compareHelper = $this->helper('Magento\Catalog\Helper\Product\Compare');?> - <a href="#" class="action tocompare" data-post='<?= /* @noEscape */ $compareHelper->getPostDataParams($_item) ?>' title="<?= $block->escapeHtmlAttr(__('Add to Compare')) ?>"> - <span><?= $block->escapeHtml(__('Add to Compare')) ?></span> - </a> - <?php endif; ?> - </div> - <?php endif; ?> + </div> + <?php endif; ?> + </div> </div> <?php endif; ?> </div> diff --git a/app/code/Magento/Checkout/Block/Cart/Sidebar.php b/app/code/Magento/Checkout/Block/Cart/Sidebar.php index 92ba6bf2bbbb..c5e309df3cad 100644 --- a/app/code/Magento/Checkout/Block/Cart/Sidebar.php +++ b/app/code/Magento/Checkout/Block/Cart/Sidebar.php @@ -82,11 +82,14 @@ public function getConfig() 'baseUrl' => $this->getBaseUrl(), 'minicartMaxItemsVisible' => $this->getMiniCartMaxItemsCount(), 'websiteId' => $this->_storeManager->getStore()->getWebsiteId(), - 'maxItemsToDisplay' => $this->getMaxItemsToDisplay() + 'maxItemsToDisplay' => $this->getMaxItemsToDisplay(), + 'storeId' => $this->_storeManager->getStore()->getId() ]; } /** + * Get serialized config + * * @return string * @since 100.2.0 */ @@ -96,6 +99,8 @@ public function getSerializedConfig() } /** + * Get image html template + * * @return string */ public function getImageHtmlTemplate() @@ -130,6 +135,7 @@ public function getShoppingCartUrl() * * @return string * @codeCoverageIgnore + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getUpdateItemQtyUrl() { @@ -141,6 +147,7 @@ public function getUpdateItemQtyUrl() * * @return string * @codeCoverageIgnore + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getRemoveItemUrl() { @@ -210,6 +217,7 @@ private function getMiniCartMaxItemsCount() /** * Returns maximum cart items to display + * * This setting regulates how many items will be displayed in minicart * * @return int diff --git a/app/code/Magento/Checkout/Block/Cart/Totals.php b/app/code/Magento/Checkout/Block/Cart/Totals.php index 375c564f2905..7ac5c1c149cc 100644 --- a/app/code/Magento/Checkout/Block/Cart/Totals.php +++ b/app/code/Magento/Checkout/Block/Cart/Totals.php @@ -3,12 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Checkout\Block\Cart; use Magento\Framework\View\Element\BlockInterface; use Magento\Checkout\Block\Checkout\LayoutProcessorInterface; /** + * Totals cart block. + * * @api */ class Totals extends \Magento\Checkout\Block\Cart\AbstractCart @@ -62,6 +65,8 @@ public function __construct( } /** + * Retrieve encoded js layout. + * * @return string */ public function getJsLayout() @@ -74,6 +79,8 @@ public function getJsLayout() } /** + * Retrieve totals from cache. + * * @return array */ public function getTotals() @@ -85,6 +92,8 @@ public function getTotals() } /** + * Set totals to cache. + * * @param array $value * @return $this * @codeCoverageIgnore @@ -96,6 +105,8 @@ public function setTotals($value) } /** + * Create totals block and set totals. + * * @param string $code * @return BlockInterface */ @@ -121,6 +132,8 @@ protected function _getTotalRenderer($code) } /** + * Get totals html. + * * @param mixed $total * @param int|null $area * @param int $colspan @@ -177,7 +190,7 @@ public function needDisplayBaseGrandtotal() } /** - * Get formated in base currency base grand total value + * Get formatted in base currency base grand total value * * @return string */ diff --git a/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php b/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php index de996bed0243..5dedf2c7e7eb 100644 --- a/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php +++ b/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php @@ -6,10 +6,18 @@ namespace Magento\Checkout\Block\Checkout; use Magento\Customer\Api\CustomerRepositoryInterface as CustomerRepository; +use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Helper\Address as AddressHelper; use Magento\Customer\Model\Session; use Magento\Directory\Helper\Data as DirectoryHelper; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +/** + * Fields attribute merger. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ class AttributeMerger { /** @@ -46,6 +54,7 @@ class AttributeMerger 'alpha' => 'validate-alpha', 'numeric' => 'validate-number', 'alphanumeric' => 'validate-alphanum', + 'alphanum-with-spaces' => 'validate-alphanum-with-spaces', 'url' => 'validate-url', 'email' => 'email2', 'length' => 'validate-length', @@ -67,7 +76,7 @@ class AttributeMerger private $customerRepository; /** - * @var \Magento\Customer\Api\Data\CustomerInterface + * @var CustomerInterface */ private $customer; @@ -269,6 +278,7 @@ protected function getMultilineFieldConfig($attributeCode, array $attributeConfi for ($lineIndex = 0; $lineIndex < (int)$attributeConfig['size']; $lineIndex++) { $isFirstLine = $lineIndex === 0; $line = [ + 'label' => __("%1: Line %2", $attributeConfig['label'], $lineIndex + 1), 'component' => 'Magento_Ui/js/form/element/abstract', 'config' => [ // customScope is used to group elements within a single form e.g. they can be validated separately @@ -309,10 +319,14 @@ protected function getMultilineFieldConfig($attributeCode, array $attributeConfi } /** + * Returns default attribute value. + * * @param string $attributeCode + * @throws NoSuchEntityException + * @throws LocalizedException * @return null|string */ - protected function getDefaultValue($attributeCode) + protected function getDefaultValue($attributeCode): ?string { if ($attributeCode === 'country_id') { return $this->directoryHelper->getDefaultCountry(); @@ -346,9 +360,13 @@ protected function getDefaultValue($attributeCode) } /** - * @return \Magento\Customer\Api\Data\CustomerInterface|null + * Returns logged customer. + * + * @throws NoSuchEntityException + * @throws LocalizedException + * @return CustomerInterface|null */ - protected function getCustomer() + protected function getCustomer(): ?CustomerInterface { if (!$this->customer) { if ($this->customerSession->isLoggedIn()) { diff --git a/app/code/Magento/Checkout/Block/Checkout/LayoutProcessor.php b/app/code/Magento/Checkout/Block/Checkout/LayoutProcessor.php index f47e514948d6..557f14335244 100644 --- a/app/code/Magento/Checkout/Block/Checkout/LayoutProcessor.php +++ b/app/code/Magento/Checkout/Block/Checkout/LayoutProcessor.php @@ -7,7 +7,7 @@ use Magento\Checkout\Helper\Data; use Magento\Framework\App\ObjectManager; -use Magento\Store\Api\StoreResolverInterface; +use Magento\Store\Model\StoreManagerInterface; /** * Class LayoutProcessor @@ -40,9 +40,9 @@ class LayoutProcessor implements \Magento\Checkout\Block\Checkout\LayoutProcesso private $checkoutDataHelper; /** - * @var StoreResolverInterface + * @var StoreManagerInterface */ - private $storeResolver; + private $storeManager; /** * @var \Magento\Shipping\Model\Config @@ -53,30 +53,36 @@ class LayoutProcessor implements \Magento\Checkout\Block\Checkout\LayoutProcesso * @param \Magento\Customer\Model\AttributeMetadataDataProvider $attributeMetadataDataProvider * @param \Magento\Ui\Component\Form\AttributeMapper $attributeMapper * @param AttributeMerger $merger + * @param \Magento\Customer\Model\Options|null $options + * @param Data|null $checkoutDataHelper + * @param \Magento\Shipping\Model\Config|null $shippingConfig + * @param StoreManagerInterface|null $storeManager */ public function __construct( \Magento\Customer\Model\AttributeMetadataDataProvider $attributeMetadataDataProvider, \Magento\Ui\Component\Form\AttributeMapper $attributeMapper, - AttributeMerger $merger + AttributeMerger $merger, + \Magento\Customer\Model\Options $options = null, + Data $checkoutDataHelper = null, + \Magento\Shipping\Model\Config $shippingConfig = null, + StoreManagerInterface $storeManager = null ) { $this->attributeMetadataDataProvider = $attributeMetadataDataProvider; $this->attributeMapper = $attributeMapper; $this->merger = $merger; + $this->options = $options ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Customer\Model\Options::class); + $this->checkoutDataHelper = $checkoutDataHelper ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(Data::class); + $this->shippingConfig = $shippingConfig ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Shipping\Model\Config::class); + $this->storeManager = $storeManager ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(StoreManagerInterface::class); } /** - * @deprecated 100.0.11 - * @return \Magento\Customer\Model\Options - */ - private function getOptions() - { - if (!is_object($this->options)) { - $this->options = ObjectManager::getInstance()->get(\Magento\Customer\Model\Options::class); - } - return $this->options; - } - - /** + * Get address attributes. + * * @return array */ private function getAddressAttributes() @@ -116,6 +122,7 @@ private function convertElementsToSelect($elements, $attributesToConvert) if (!in_array($code, $codes)) { continue; } + // phpcs:ignore Magento2.Functions.DiscouragedFunction $options = call_user_func($attributesToConvert[$code]); if (!is_array($options)) { continue; @@ -143,8 +150,8 @@ private function convertElementsToSelect($elements, $attributesToConvert) public function process($jsLayout) { $attributesToConvert = [ - 'prefix' => [$this->getOptions(), 'getNamePrefixOptions'], - 'suffix' => [$this->getOptions(), 'getNameSuffixOptions'], + 'prefix' => [$this->options, 'getNamePrefixOptions'], + 'suffix' => [$this->options, 'getNameSuffixOptions'], ]; $elements = $this->getAddressAttributes(); @@ -192,8 +199,8 @@ public function process($jsLayout) */ private function processShippingChildrenComponents($shippingRatesLayout) { - $activeCarriers = $this->getShippingConfig()->getActiveCarriers( - $this->getStoreResolver()->getCurrentStoreId() + $activeCarriers = $this->shippingConfig->getActiveCarriers( + $this->storeManager->getStore()->getId() ); foreach (array_keys($shippingRatesLayout) as $carrierName) { $carrierKey = str_replace('-rates-validation', '', $carrierName); @@ -206,6 +213,7 @@ private function processShippingChildrenComponents($shippingRatesLayout) /** * Appends billing address form component to payment layout + * * @param array $paymentLayout * @param array $elements * @return array @@ -221,7 +229,7 @@ private function processPaymentChildrenComponents(array $paymentLayout, array $e } // The if billing address should be displayed on Payment method or page - if ($this->getCheckoutDataHelper()->isDisplayBillingOnPaymentMethodAvailable()) { + if ($this->checkoutDataHelper->isDisplayBillingOnPaymentMethodAvailable()) { $paymentLayout['payments-list']['children'] = array_merge_recursive( $paymentLayout['payments-list']['children'], @@ -280,8 +288,14 @@ private function getBillingAddressComponent($paymentCode, $elements) 'provider' => 'checkoutProvider', 'deps' => 'checkoutProvider', 'dataScopePrefix' => 'billingAddress' . $paymentCode, + 'billingAddressListProvider' => '${$.name}.billingAddressList', 'sortOrder' => 1, 'children' => [ + 'billingAddressList' => [ + 'component' => 'Magento_Checkout/js/view/billing-address/list', + 'displayArea' => 'billing-address-list', + 'template' => 'Magento_Checkout/billing-address/list' + ], 'form-fields' => [ 'component' => 'uiComponent', 'displayArea' => 'additional-fieldsets', @@ -340,49 +354,4 @@ private function getBillingAddressComponent($paymentCode, $elements) ], ]; } - - /** - * Get checkout data helper instance - * - * @return Data - * @deprecated 100.1.4 - */ - private function getCheckoutDataHelper() - { - if (!$this->checkoutDataHelper) { - $this->checkoutDataHelper = ObjectManager::getInstance()->get(Data::class); - } - - return $this->checkoutDataHelper; - } - - /** - * Retrieve Shipping Configuration. - * - * @return \Magento\Shipping\Model\Config - * @deprecated 100.2.0 - */ - private function getShippingConfig() - { - if (!$this->shippingConfig) { - $this->shippingConfig = ObjectManager::getInstance()->get(\Magento\Shipping\Model\Config::class); - } - - return $this->shippingConfig; - } - - /** - * Get store resolver. - * - * @return StoreResolverInterface - * @deprecated 100.2.0 - */ - private function getStoreResolver() - { - if (!$this->storeResolver) { - $this->storeResolver = ObjectManager::getInstance()->get(StoreResolverInterface::class); - } - - return $this->storeResolver; - } } diff --git a/app/code/Magento/Checkout/Block/Onepage.php b/app/code/Magento/Checkout/Block/Onepage.php index ca6b045ddbb5..e01d5835b4cf 100644 --- a/app/code/Magento/Checkout/Block/Onepage.php +++ b/app/code/Magento/Checkout/Block/Onepage.php @@ -38,7 +38,7 @@ class Onepage extends \Magento\Framework\View\Element\Template protected $layoutProcessors; /** - * @var \Magento\Framework\Serialize\Serializer\Json + * @var \Magento\Framework\Serialize\SerializerInterface */ private $serializer; @@ -48,8 +48,9 @@ class Onepage extends \Magento\Framework\View\Element\Template * @param \Magento\Checkout\Model\CompositeConfigProvider $configProvider * @param array $layoutProcessors * @param array $data - * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer - * @throws \RuntimeException + * @param \Magento\Framework\Serialize\Serializer\Json $serializer + * @param \Magento\Framework\Serialize\SerializerInterface $serializerInterface + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, @@ -57,7 +58,8 @@ public function __construct( \Magento\Checkout\Model\CompositeConfigProvider $configProvider, array $layoutProcessors = [], array $data = [], - \Magento\Framework\Serialize\Serializer\Json $serializer = null + \Magento\Framework\Serialize\Serializer\Json $serializer = null, + \Magento\Framework\Serialize\SerializerInterface $serializerInterface = null ) { parent::__construct($context, $data); $this->formKey = $formKey; @@ -65,12 +67,12 @@ public function __construct( $this->jsLayout = isset($data['jsLayout']) && is_array($data['jsLayout']) ? $data['jsLayout'] : []; $this->configProvider = $configProvider; $this->layoutProcessors = $layoutProcessors; - $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Framework\Serialize\Serializer\Json::class); + $this->serializer = $serializerInterface ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Framework\Serialize\Serializer\JsonHexTag::class); } /** - * @return string + * @inheritdoc */ public function getJsLayout() { @@ -78,7 +80,7 @@ public function getJsLayout() $this->jsLayout = $processor->process($this->jsLayout); } - return json_encode($this->jsLayout, JSON_HEX_TAG); + return $this->serializer->serialize($this->jsLayout); } /** @@ -115,11 +117,13 @@ public function getBaseUrl() } /** + * Retrieve serialized checkout config. + * * @return bool|string * @since 100.2.0 */ public function getSerializedCheckoutConfig() { - return json_encode($this->getCheckoutConfig(), JSON_HEX_TAG); + return $this->serializer->serialize($this->getCheckoutConfig()); } } diff --git a/app/code/Magento/Checkout/Block/QuoteShortcutButtons.php b/app/code/Magento/Checkout/Block/QuoteShortcutButtons.php index 6b3774f7e38f..3b2f1604fae4 100644 --- a/app/code/Magento/Checkout/Block/QuoteShortcutButtons.php +++ b/app/code/Magento/Checkout/Block/QuoteShortcutButtons.php @@ -8,6 +8,8 @@ use Magento\Framework\View\Element\Template; /** + * Displays buttons on shopping cart page + * * @api */ class QuoteShortcutButtons extends \Magento\Catalog\Block\ShortcutButtons @@ -45,7 +47,8 @@ protected function _beforeToHtml() 'container' => $this, 'is_catalog_product' => $this->_isCatalogProduct, 'or_position' => $this->_orPosition, - 'checkout_session' => $this->_checkoutSession + 'checkout_session' => $this->_checkoutSession, + 'is_shopping_cart' => true ] ); return $this; diff --git a/app/code/Magento/Checkout/Controller/Account/DelegateCreate.php b/app/code/Magento/Checkout/Controller/Account/DelegateCreate.php index 6c4c8b053e2a..e4f909f9a813 100644 --- a/app/code/Magento/Checkout/Controller/Account/DelegateCreate.php +++ b/app/code/Magento/Checkout/Controller/Account/DelegateCreate.php @@ -7,6 +7,7 @@ namespace Magento\Checkout\Controller\Account; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Framework\App\Action\Action; use Magento\Framework\App\Action\Context; use Magento\Checkout\Model\Session; @@ -15,7 +16,7 @@ /** * Redirect guest customer for registration. */ -class DelegateCreate extends Action +class DelegateCreate extends Action implements HttpGetActionInterface { /** * @var OrderCustomerDelegateInterface diff --git a/app/code/Magento/Checkout/Controller/Cart.php b/app/code/Magento/Checkout/Controller/Cart.php index f244fe310af2..d7b09c17ee03 100644 --- a/app/code/Magento/Checkout/Controller/Cart.php +++ b/app/code/Magento/Checkout/Controller/Cart.php @@ -106,8 +106,7 @@ protected function _isInternalUrl($url) /** * Get resolved back url * - * @param null $defaultUrl - * + * @param string|null $defaultUrl * @return mixed|null|string */ protected function getBackUrl($defaultUrl = null) @@ -118,12 +117,7 @@ protected function getBackUrl($defaultUrl = null) return $returnUrl; } - $shouldRedirectToCart = $this->_scopeConfig->getValue( - 'checkout/cart/redirect_to_cart', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ); - - if ($shouldRedirectToCart || $this->getRequest()->getParam('in_cart')) { + if ($this->shouldRedirectToCart() || $this->getRequest()->getParam('in_cart')) { if ($this->getRequest()->getActionName() == 'add' && !$this->getRequest()->getParam('in_cart')) { $this->_checkoutSession->setContinueShoppingUrl($this->_redirect->getRefererUrl()); } @@ -132,4 +126,17 @@ protected function getBackUrl($defaultUrl = null) return $defaultUrl; } + + /** + * Is redirect should be performed after the product was added to cart. + * + * @return bool + */ + private function shouldRedirectToCart() + { + return $this->_scopeConfig->isSetFlag( + 'checkout/cart/redirect_to_cart', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + } } diff --git a/app/code/Magento/Checkout/Controller/Cart/Add.php b/app/code/Magento/Checkout/Controller/Cart/Add.php index 5f54ca339564..739f71caeb80 100644 --- a/app/code/Magento/Checkout/Controller/Cart/Add.php +++ b/app/code/Magento/Checkout/Controller/Cart/Add.php @@ -6,14 +6,17 @@ */ namespace Magento\Checkout\Controller\Cart; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Checkout\Model\Cart as CustomerCart; use Magento\Framework\Exception\NoSuchEntityException; /** + * Controller for processing add to cart action. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Add extends \Magento\Checkout\Controller\Cart +class Add extends \Magento\Checkout\Controller\Cart implements HttpPostActionInterface { /** * @var ProductRepositoryInterface @@ -125,11 +128,21 @@ public function execute() if (!$this->_checkoutSession->getNoCartRedirect(true)) { if (!$this->cart->getQuote()->getHasError()) { - $message = __( - 'You added %1 to your shopping cart.', - $product->getName() - ); - $this->messageManager->addSuccessMessage($message); + if ($this->shouldRedirectToCart()) { + $message = __( + 'You added %1 to your shopping cart.', + $product->getName() + ); + $this->messageManager->addSuccessMessage($message); + } else { + $this->messageManager->addComplexSuccessMessage( + 'addCartSuccessMessage', + [ + 'product_name' => $product->getName(), + 'cart_url' => $this->getCartUrl(), + ] + ); + } } return $this->goBack(null, $product); } @@ -150,8 +163,7 @@ public function execute() $url = $this->_checkoutSession->getRedirectUrl(true); if (!$url) { - $cartUrl = $this->_objectManager->get(\Magento\Checkout\Helper\Cart::class)->getCartUrl(); - $url = $this->_redirect->getRedirectUrl($cartUrl); + $url = $this->_redirect->getRedirectUrl($this->getCartUrl()); } return $this->goBack($url); @@ -194,4 +206,27 @@ protected function goBack($backUrl = null, $product = null) $this->_objectManager->get(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($result) ); } + + /** + * Returns cart url + * + * @return string + */ + private function getCartUrl() + { + return $this->_url->getUrl('checkout/cart', ['_secure' => true]); + } + + /** + * Is redirect should be performed after the product was added to cart. + * + * @return bool + */ + private function shouldRedirectToCart() + { + return $this->_scopeConfig->isSetFlag( + 'checkout/cart/redirect_to_cart', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + } } diff --git a/app/code/Magento/Checkout/Controller/Cart/Addgroup.php b/app/code/Magento/Checkout/Controller/Cart/Addgroup.php index c205f3c16072..7eb936203125 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/Controller/Cart/Configure.php b/app/code/Magento/Checkout/Controller/Cart/Configure.php index 19b2d2db345a..aa4ae755d794 100644 --- a/app/code/Magento/Checkout/Controller/Cart/Configure.php +++ b/app/code/Magento/Checkout/Controller/Cart/Configure.php @@ -7,13 +7,14 @@ namespace Magento\Checkout\Controller\Cart; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Framework; use Magento\Framework\Controller\ResultFactory; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Configure extends \Magento\Checkout\Controller\Cart +class Configure extends \Magento\Checkout\Controller\Cart implements HttpGetActionInterface { /** * @var \Magento\Framework\View\Result\PageFactory diff --git a/app/code/Magento/Checkout/Controller/Cart/CouponPost.php b/app/code/Magento/Checkout/Controller/Cart/CouponPost.php index 5f6833518117..fdfe817f354f 100644 --- a/app/code/Magento/Checkout/Controller/Cart/CouponPost.php +++ b/app/code/Magento/Checkout/Controller/Cart/CouponPost.php @@ -5,10 +5,12 @@ */ namespace Magento\Checkout\Controller\Cart; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; + /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class CouponPost extends \Magento\Checkout\Controller\Cart +class CouponPost extends \Magento\Checkout\Controller\Cart implements HttpPostActionInterface { /** * Sales quote repository diff --git a/app/code/Magento/Checkout/Controller/Cart/Delete.php b/app/code/Magento/Checkout/Controller/Cart/Delete.php index 4a6174e83fd0..e9c15bd7f8cc 100644 --- a/app/code/Magento/Checkout/Controller/Cart/Delete.php +++ b/app/code/Magento/Checkout/Controller/Cart/Delete.php @@ -1,12 +1,18 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Checkout\Controller\Cart; -class Delete extends \Magento\Checkout\Controller\Cart +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; + +/** + * Action Delete. + * + * Deletes item from cart. + */ +class Delete extends \Magento\Checkout\Controller\Cart implements HttpPostActionInterface { /** * Delete shopping cart item action @@ -22,7 +28,12 @@ public function execute() $id = (int)$this->getRequest()->getParam('id'); if ($id) { try { - $this->cart->removeItem($id)->save(); + $this->cart->removeItem($id); + // We should set Totals to be recollected once more because of Cart model as usually is loading + // before action executing and in case when triggerRecollect setted as true recollecting will + // executed and the flag will be true already. + $this->cart->getQuote()->setTotalsCollectedFlag(false); + $this->cart->save(); } catch (\Exception $e) { $this->messageManager->addErrorMessage(__('We can\'t remove the item.')); $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); diff --git a/app/code/Magento/Checkout/Controller/Cart/Index.php b/app/code/Magento/Checkout/Controller/Cart/Index.php index 3fb582d35e28..182ab6777776 100644 --- a/app/code/Magento/Checkout/Controller/Cart/Index.php +++ b/app/code/Magento/Checkout/Controller/Cart/Index.php @@ -7,7 +7,9 @@ namespace Magento\Checkout\Controller\Cart; -class Index extends \Magento\Checkout\Controller\Cart +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\Checkout\Controller\Cart implements HttpGetActionInterface { /** * @var \Magento\Framework\View\Result\PageFactory diff --git a/app/code/Magento/Checkout/Controller/Cart/UpdateItemOptions.php b/app/code/Magento/Checkout/Controller/Cart/UpdateItemOptions.php index 59bd6489bf92..40ce2252581c 100644 --- a/app/code/Magento/Checkout/Controller/Cart/UpdateItemOptions.php +++ b/app/code/Magento/Checkout/Controller/Cart/UpdateItemOptions.php @@ -7,7 +7,9 @@ namespace Magento\Checkout\Controller\Cart; -class UpdateItemOptions extends \Magento\Checkout\Controller\Cart +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; + +class UpdateItemOptions extends \Magento\Checkout\Controller\Cart implements HttpPostActionInterface { /** * Update product configuration for a cart item diff --git a/app/code/Magento/Checkout/Controller/Cart/UpdatePost.php b/app/code/Magento/Checkout/Controller/Cart/UpdatePost.php index 05ce48c9b46c..bfc408d920ad 100644 --- a/app/code/Magento/Checkout/Controller/Cart/UpdatePost.php +++ b/app/code/Magento/Checkout/Controller/Cart/UpdatePost.php @@ -1,14 +1,18 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Checkout\Controller\Cart; use Magento\Checkout\Model\Cart\RequestQuantityProcessor; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\App\Action\HttpGetActionInterface; -class UpdatePost extends \Magento\Checkout\Controller\Cart +/** + * Post update shopping cart. + */ +class UpdatePost extends \Magento\Checkout\Controller\Cart implements HttpGetActionInterface, HttpPostActionInterface { /** * @var RequestQuantityProcessor diff --git a/app/code/Magento/Checkout/Controller/Index/Index.php b/app/code/Magento/Checkout/Controller/Index/Index.php index 785c1f1473be..5acfab435b51 100644 --- a/app/code/Magento/Checkout/Controller/Index/Index.php +++ b/app/code/Magento/Checkout/Controller/Index/Index.php @@ -9,7 +9,9 @@ namespace Magento\Checkout\Controller\Index; -class Index extends \Magento\Checkout\Controller\Onepage +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\Checkout\Controller\Onepage implements HttpGetActionInterface { /** * Checkout page diff --git a/app/code/Magento/Checkout/Controller/Onepage/Success.php b/app/code/Magento/Checkout/Controller/Onepage/Success.php index ae9be42a89c8..a657b23cca4d 100644 --- a/app/code/Magento/Checkout/Controller/Onepage/Success.php +++ b/app/code/Magento/Checkout/Controller/Onepage/Success.php @@ -1,12 +1,16 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Checkout\Controller\Onepage; -class Success extends \Magento\Checkout\Controller\Onepage +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +/** + * Onepage checkout success controller class + */ +class Success extends \Magento\Checkout\Controller\Onepage implements HttpGetActionInterface { /** * Order success action @@ -24,7 +28,10 @@ public function execute() $resultPage = $this->resultPageFactory->create(); $this->_eventManager->dispatch( 'checkout_onepage_controller_success_action', - ['order_ids' => [$session->getLastOrderId()]] + [ + 'order_ids' => [$session->getLastOrderId()], + 'order' => $session->getLastRealOrder() + ] ); return $resultPage; } diff --git a/app/code/Magento/Checkout/Controller/Sidebar/RemoveItem.php b/app/code/Magento/Checkout/Controller/Sidebar/RemoveItem.php index c84aec336a58..f589e702de95 100644 --- a/app/code/Magento/Checkout/Controller/Sidebar/RemoveItem.php +++ b/app/code/Magento/Checkout/Controller/Sidebar/RemoveItem.php @@ -5,7 +5,9 @@ */ namespace Magento\Checkout\Controller\Sidebar; -class RemoveItem extends \Magento\Framework\App\Action\Action +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; + +class RemoveItem extends \Magento\Framework\App\Action\Action implements HttpPostActionInterface { /** * @var \Magento\Checkout\Model\Sidebar diff --git a/app/code/Magento/Checkout/CustomerData/Cart.php b/app/code/Magento/Checkout/CustomerData/Cart.php index ddb077462ef1..169be4cc62f0 100644 --- a/app/code/Magento/Checkout/CustomerData/Cart.php +++ b/app/code/Magento/Checkout/CustomerData/Cart.php @@ -10,6 +10,8 @@ /** * Cart source + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class Cart extends \Magento\Framework\DataObject implements SectionSourceInterface { @@ -82,7 +84,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getSectionData() { @@ -98,7 +100,8 @@ public function getSectionData() 'items' => $this->getRecentItems(), 'extra_actions' => $this->layout->createBlock(\Magento\Catalog\Block\ShortcutButtons::class)->toHtml(), 'isGuestCheckoutAllowed' => $this->isGuestCheckoutAllowed(), - 'website_id' => $this->getQuote()->getStore()->getWebsiteId() + 'website_id' => $this->getQuote()->getStore()->getWebsiteId(), + 'storeId' => $this->getQuote()->getStore()->getStoreId() ]; } @@ -158,11 +161,10 @@ protected function getRecentItems() : $item->getProduct(); $products = $this->catalogUrl->getRewriteByProductStore([$product->getId() => $item->getStoreId()]); - if (!isset($products[$product->getId()])) { - continue; + if (isset($products[$product->getId()])) { + $urlDataObject = new \Magento\Framework\DataObject($products[$product->getId()]); + $item->getProduct()->setUrlDataObject($urlDataObject); } - $urlDataObject = new \Magento\Framework\DataObject($products[$product->getId()]); - $item->getProduct()->setUrlDataObject($urlDataObject); } $items[] = $this->itemPoolInterface->getItemData($item); } diff --git a/app/code/Magento/Checkout/Helper/Data.php b/app/code/Magento/Checkout/Helper/Data.php index 636d4aaca21f..40bdf93d161e 100644 --- a/app/code/Magento/Checkout/Helper/Data.php +++ b/app/code/Magento/Checkout/Helper/Data.php @@ -59,6 +59,8 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper private $paymentFailures; /** + * Data constructor. + * * @param \Magento\Framework\App\Helper\Context $context * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Checkout\Model\Session $checkoutSession @@ -113,6 +115,8 @@ public function getQuote() } /** + * Format Price + * * @param float $price * @return string */ @@ -127,6 +131,8 @@ public function formatPrice($price) } /** + * Convert Price + * * @param float $price * @param bool $format * @return float @@ -145,9 +151,9 @@ public function convertPrice($price, $format = true) */ public function canOnepageCheckout() { - return (bool)$this->scopeConfig->getValue( + return $this->scopeConfig->isSetFlag( 'checkout/options/onepage_checkout_enabled', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE ); } @@ -164,7 +170,7 @@ public function getPriceInclTax($item) } $qty = $item->getQty() ? $item->getQty() : ($item->getQtyOrdered() ? $item->getQtyOrdered() : 1); $taxAmount = $item->getTaxAmount() + $item->getDiscountTaxCompensation(); - $price = floatval($qty) ? ($item->getRowTotal() + $taxAmount) / $qty : 0; + $price = (float)$qty ? ($item->getRowTotal() + $taxAmount) / $qty : 0; return $this->priceCurrency->round($price); } @@ -184,6 +190,8 @@ public function getSubtotalInclTax($item) } /** + * Get Base Price Incl Tax + * * @param AbstractItem $item * @return float */ @@ -191,11 +199,13 @@ public function getBasePriceInclTax($item) { $qty = $item->getQty() ? $item->getQty() : ($item->getQtyOrdered() ? $item->getQtyOrdered() : 1); $taxAmount = $item->getBaseTaxAmount() + $item->getBaseDiscountTaxCompensation(); - $price = floatval($qty) ? ($item->getBaseRowTotal() + $taxAmount) / $qty : 0; + $price = (float)$qty ? ($item->getBaseRowTotal() + $taxAmount) / $qty : 0; return $this->priceCurrency->round($price); } /** + * Get Base Subtotal Incl Tax + * * @param AbstractItem $item * @return float */ @@ -217,13 +227,15 @@ public function sendPaymentFailedEmail( \Magento\Quote\Model\Quote $checkout, string $message, string $checkoutType = 'onepage' - ): \Magento\Checkout\Helper\Data { + ): Data { $this->paymentFailures->handle((int)$checkout->getId(), $message, $checkoutType); return $this; } /** + * Get Emails + * * @param string $configPath * @param null|string|bool|int|Store $storeId * @return array|false @@ -232,7 +244,7 @@ protected function _getEmails($configPath, $storeId) { $data = $this->scopeConfig->getValue( $configPath, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $storeId ); if (!empty($data)) { @@ -242,8 +254,7 @@ protected function _getEmails($configPath, $storeId) } /** - * Check is allowed Guest Checkout - * Use config settings and observer + * Check is allowed Guest Checkout. Use config settings and observer * * @param \Magento\Quote\Model\Quote $quote * @param int|Store $store @@ -256,7 +267,7 @@ public function isAllowedGuestCheckout(\Magento\Quote\Model\Quote $quote, $store } $guestCheckout = $this->scopeConfig->isSetFlag( self::XML_PATH_GUEST_CHECKOUT, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $store ); @@ -295,13 +306,13 @@ public function isCustomerMustBeLogged() { return $this->scopeConfig->isSetFlag( self::XML_PATH_CUSTOMER_MUST_BE_LOGGED, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE ); } /** - * Checks if display billing address on payment method is available, otherwise - * billing address should be display on payment page + * If display billing address on payment method is available, otherwise should be display on payment page + * * @return bool */ public function isDisplayBillingOnPaymentMethodAvailable() diff --git a/app/code/Magento/Checkout/Model/Cart.php b/app/code/Magento/Checkout/Model/Cart.php index a18cba3f67c8..cec99909dc99 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,21 +366,15 @@ 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', + ['info' => $requestInfo, 'product' => $product] + ); $result = $this->getQuote()->addProduct($product, $request); } catch (\Magento\Framework\Exception\LocalizedException $e) { $this->_checkoutSession->setUseNotice(false); @@ -434,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; } @@ -615,6 +611,8 @@ public function truncate() } /** + * Get product ids. + * * @return int[] */ public function getProductIds() @@ -756,4 +754,27 @@ private function getRequestInfoFilter() } return $this->requestInfoFilter; } + + /** + * Get request quantity + * + * @param Product $product + * @param \Magento\Framework\DataObject|int|array $request + * @return int|DataObject + */ + private function getQtyRequest($product, $request = 0) + { + $request = $this->_getProductRequest($request); + $stockItem = $this->stockRegistry->getStockItem($product->getId(), $product->getStore()->getWebsiteId()); + $minimumQty = $stockItem->getMinSaleQty(); + //If product quantity is not specified in request and there is set minimal qty for it + if ($minimumQty + && $minimumQty > 0 + && !$request->getQty() + ) { + $request->setQty($minimumQty); + } + + return $request; + } } diff --git a/app/code/Magento/Checkout/Model/DefaultConfigProvider.php b/app/code/Magento/Checkout/Model/DefaultConfigProvider.php index 16f13511001e..470d4a3aca56 100644 --- a/app/code/Magento/Checkout/Model/DefaultConfigProvider.php +++ b/app/code/Magento/Checkout/Model/DefaultConfigProvider.php @@ -10,9 +10,12 @@ use Magento\Checkout\Model\Session as CheckoutSession; use Magento\Customer\Api\AddressMetadataInterface; use Magento\Customer\Api\CustomerRepositoryInterface as CustomerRepository; +use Magento\Customer\Model\Address\CustomerAddressDataProvider; use Magento\Customer\Model\Context as CustomerContext; use Magento\Customer\Model\Session as CustomerSession; use Magento\Customer\Model\Url as CustomerUrlManager; +use Magento\Eav\Api\AttributeOptionManagementInterface; +use Magento\Framework\Api\CustomAttributesDataInterface; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Http\Context as HttpContext; use Magento\Framework\App\ObjectManager; @@ -21,16 +24,26 @@ use Magento\Framework\UrlInterface; use Magento\Quote\Api\CartItemRepositoryInterface as QuoteItemRepository; use Magento\Quote\Api\CartTotalRepositoryInterface; +use Magento\Quote\Api\Data\AddressInterface; use Magento\Quote\Api\ShippingMethodManagementInterface as ShippingMethodManager; use Magento\Quote\Model\QuoteIdMaskFactory; use Magento\Store\Model\ScopeInterface; +use Magento\Ui\Component\Form\Element\Multiline; /** + * Default Config Provider + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class DefaultConfigProvider implements ConfigProviderInterface { + /** + * @var AttributeOptionManagementInterface + */ + private $attributeOptionManager; + /** * @var CheckoutHelper */ @@ -166,6 +179,11 @@ class DefaultConfigProvider implements ConfigProviderInterface */ private $addressMetadata; + /** + * @var CustomerAddressDataProvider + */ + private $customerAddressData; + /** * @param CheckoutHelper $checkoutHelper * @param Session $checkoutSession @@ -194,6 +212,8 @@ class DefaultConfigProvider implements ConfigProviderInterface * @param \Magento\Quote\Api\PaymentMethodManagementInterface $paymentMethodManagement * @param UrlInterface $urlBuilder * @param AddressMetadataInterface $addressMetadata + * @param AttributeOptionManagementInterface $attributeOptionManager + * @param CustomerAddressDataProvider|null $customerAddressData * @codeCoverageIgnore * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -224,7 +244,9 @@ public function __construct( \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Quote\Api\PaymentMethodManagementInterface $paymentMethodManagement, UrlInterface $urlBuilder, - AddressMetadataInterface $addressMetadata = null + AddressMetadataInterface $addressMetadata = null, + AttributeOptionManagementInterface $attributeOptionManager = null, + CustomerAddressDataProvider $customerAddressData = null ) { $this->checkoutHelper = $checkoutHelper; $this->checkoutSession = $checkoutSession; @@ -253,20 +275,41 @@ public function __construct( $this->paymentMethodManagement = $paymentMethodManagement; $this->urlBuilder = $urlBuilder; $this->addressMetadata = $addressMetadata ?: ObjectManager::getInstance()->get(AddressMetadataInterface::class); + $this->attributeOptionManager = $attributeOptionManager ?? + ObjectManager::getInstance()->get(AttributeOptionManagementInterface::class); + $this->customerAddressData = $customerAddressData ?: + ObjectManager::getInstance()->get(CustomerAddressDataProvider::class); } /** - * {@inheritdoc} + * Return configuration array + * + * @return array|mixed + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Magento\Framework\Exception\LocalizedException */ public function getConfig() { - $quoteId = $this->checkoutSession->getQuote()->getId(); + $quote = $this->checkoutSession->getQuote(); + $quoteId = $quote->getId(); + $email = $quote->getShippingAddress()->getEmail(); + $quoteItemData = $this->getQuoteItemData(); $output['formKey'] = $this->formKey->getFormKey(); $output['customerData'] = $this->getCustomerData(); $output['quoteData'] = $this->getQuoteData(); - $output['quoteItemData'] = $this->getQuoteItemData(); + $output['quoteItemData'] = $quoteItemData; + $output['quoteMessages'] = $this->getQuoteItemsMessages($quoteItemData); $output['isCustomerLoggedIn'] = $this->isCustomerLoggedIn(); $output['selectedShippingMethod'] = $this->getSelectedShippingMethod(); + if ($email && !$this->isCustomerLoggedIn()) { + $shippingAddressFromData = $this->getAddressFromData($quote->getShippingAddress()); + $billingAddressFromData = $this->getAddressFromData($quote->getBillingAddress()); + $output['shippingAddressFromData'] = $shippingAddressFromData; + if ($shippingAddressFromData != $billingAddressFromData) { + $output['billingAddressFromData'] = $billingAddressFromData; + } + $output['validatedEmailValue'] = $email; + } $output['storeCode'] = $this->getStoreCode(); $output['isGuestCheckoutAllowed'] = $this->isGuestCheckoutAllowed(); $output['isCustomerLoginRequired'] = $this->isCustomerLoginRequired(); @@ -278,14 +321,15 @@ public function getConfig() $output['staticBaseUrl'] = $this->getStaticBaseUrl(); $output['priceFormat'] = $this->localeFormat->getPriceFormat( null, - $this->checkoutSession->getQuote()->getQuoteCurrencyCode() + $quote->getQuoteCurrencyCode() ); $output['basePriceFormat'] = $this->localeFormat->getPriceFormat( null, - $this->checkoutSession->getQuote()->getBaseCurrencyCode() + $quote->getBaseCurrencyCode() ); $output['postCodes'] = $this->postCodesConfig->getPostCodes(); $output['imageData'] = $this->imageProvider->getImages($quoteId); + $output['totalsData'] = $this->getTotalsData(); $output['shippingPolicy'] = [ 'isEnabled' => $this->scopeConfig->isSetFlag( @@ -326,57 +370,18 @@ private function isAutocompleteEnabled() * * @return array */ - private function getCustomerData() + private function getCustomerData(): array { $customerData = []; if ($this->isCustomerLoggedIn()) { + /** @var \Magento\Customer\Api\Data\CustomerInterface $customer */ $customer = $this->customerRepository->getById($this->customerSession->getCustomerId()); $customerData = $customer->__toArray(); - foreach ($customer->getAddresses() as $key => $address) { - $customerData['addresses'][$key]['inline'] = $this->getCustomerAddressInline($address); - if ($address->getCustomAttributes()) { - $customerData['addresses'][$key]['custom_attributes'] = $this->filterNotVisibleAttributes( - $customerData['addresses'][$key]['custom_attributes'] - ); - } - } + $customerData['addresses'] = $this->customerAddressData->getAddressDataByCustomer($customer); } return $customerData; } - /** - * Filter not visible on storefront custom attributes. - * - * @param array $attributes - * @return array - */ - private function filterNotVisibleAttributes(array $attributes) - { - $attributesMetadata = $this->addressMetadata->getAllAttributesMetadata(); - foreach ($attributesMetadata as $attributeMetadata) { - if (!$attributeMetadata->isVisible()) { - unset($attributes[$attributeMetadata->getAttributeCode()]); - } - } - - return $attributes; - } - - /** - * Set additional customer address data - * - * @param \Magento\Customer\Api\Data\AddressInterface $address - * @return string - */ - private function getCustomerAddressInline($address) - { - $builtOutputAddressData = $this->addressMapper->toFlatArray($address); - return $this->addressConfig - ->getFormatByCode(\Magento\Customer\Model\Address\Config::DEFAULT_ADDRESS_FORMAT) - ->getRenderer() - ->renderArray($builtOutputAddressData); - } - /** * Retrieve quote data * @@ -420,6 +425,7 @@ private function getQuoteItemData() $quoteItem->getProduct(), 'product_thumbnail_image' )->getUrl(); + $quoteItemData[$index]['message'] = $quoteItem->getMessage(); } } return $quoteItemData; @@ -513,6 +519,38 @@ private function getSelectedShippingMethod() return $shippingMethodData; } + /** + * Create address data appropriate to fill checkout address form + * + * @param AddressInterface $address + * @return array + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function getAddressFromData(AddressInterface $address) + { + $addressData = []; + $attributesMetadata = $this->addressMetadata->getAllAttributesMetadata(); + foreach ($attributesMetadata as $attributeMetadata) { + if (!$attributeMetadata->isVisible()) { + continue; + } + $attributeCode = $attributeMetadata->getAttributeCode(); + $attributeData = $address->getData($attributeCode); + if ($attributeData) { + if ($attributeMetadata->getFrontendInput() === Multiline::NAME) { + $attributeData = \is_array($attributeData) ? $attributeData : explode("\n", $attributeData); + $attributeData = (object)$attributeData; + } + if ($attributeMetadata->isUserDefined()) { + $addressData[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES][$attributeCode] = $attributeData; + continue; + } + $addressData[$attributeCode] = $attributeData; + } + } + return $addressData; + } + /** * Retrieve store code * @@ -581,6 +619,7 @@ protected function getStaticBaseUrl() /** * Return quote totals data + * * @return array */ private function getTotalsData() @@ -612,6 +651,7 @@ private function getTotalsData() /** * Returns active carriers codes + * * @return array */ private function getActiveCarriers() @@ -625,6 +665,7 @@ private function getActiveCarriers() /** * Returns origin country code + * * @return string */ private function getOriginCountryCode() @@ -638,7 +679,9 @@ private function getOriginCountryCode() /** * Returns array of payment methods - * @return array + * + * @return array $paymentMethods + * @throws \Magento\Framework\Exception\NoSuchEntityException */ private function getPaymentMethods() { @@ -654,4 +697,22 @@ private function getPaymentMethods() } return $paymentMethods; } + + /** + * Get notification messages for the quote items + * + * @param array $quoteItemData + * @return array + */ + private function getQuoteItemsMessages(array $quoteItemData): array + { + $quoteItemsMessages = []; + if ($quoteItemData) { + foreach ($quoteItemData as $item) { + $quoteItemsMessages[$item['item_id']] = $item['message']; + } + } + + return $quoteItemsMessages; + } } diff --git a/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php b/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php index 333226b7d216..da29482f0123 100644 --- a/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php +++ b/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php @@ -14,6 +14,8 @@ use Magento\Quote\Model\Quote; /** + * Guest payment information management model. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class GuestPaymentInformationManagement implements \Magento\Checkout\Api\GuestPaymentInformationManagementInterface @@ -66,7 +68,7 @@ class GuestPaymentInformationManagement implements \Magento\Checkout\Api\GuestPa * @param \Magento\Checkout\Api\PaymentInformationManagementInterface $paymentInformationManagement * @param \Magento\Quote\Model\QuoteIdMaskFactory $quoteIdMaskFactory * @param CartRepositoryInterface $cartRepository - * @param ResourceConnection|null + * @param ResourceConnection $connectionPool * @codeCoverageIgnore */ public function __construct( @@ -88,7 +90,7 @@ public function __construct( } /** - * {@inheritDoc} + * @inheritdoc */ public function savePaymentInformationAndPlaceOrder( $cartId, @@ -129,7 +131,7 @@ public function savePaymentInformationAndPlaceOrder( } /** - * {@inheritDoc} + * @inheritdoc */ public function savePaymentInformation( $cartId, @@ -156,7 +158,7 @@ public function savePaymentInformation( } /** - * {@inheritDoc} + * @inheritdoc */ public function getPaymentInformation($cartId) { @@ -190,9 +192,8 @@ private function limitShippingCarrier(Quote $quote) : void { $shippingAddress = $quote->getShippingAddress(); if ($shippingAddress && $shippingAddress->getShippingMethod()) { - $shippingDataArray = explode('_', $shippingAddress->getShippingMethod()); - $shippingCarrier = array_shift($shippingDataArray); - $shippingAddress->setLimitCarrier($shippingCarrier); + $shippingRate = $shippingAddress->getShippingRateByCode($shippingAddress->getShippingMethod()); + $shippingAddress->setLimitCarrier($shippingRate->getCarrier()); } } } diff --git a/app/code/Magento/Checkout/Model/PaymentInformationManagement.php b/app/code/Magento/Checkout/Model/PaymentInformationManagement.php index 164109177d4e..e0de45a3f0de 100644 --- a/app/code/Magento/Checkout/Model/PaymentInformationManagement.php +++ b/app/code/Magento/Checkout/Model/PaymentInformationManagement.php @@ -9,6 +9,8 @@ use Magento\Framework\Exception\CouldNotSaveException; /** + * Payment information management + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class PaymentInformationManagement implements \Magento\Checkout\Api\PaymentInformationManagementInterface @@ -72,7 +74,7 @@ public function __construct( } /** - * {@inheritDoc} + * @inheritdoc */ public function savePaymentInformationAndPlaceOrder( $cartId, @@ -98,7 +100,7 @@ public function savePaymentInformationAndPlaceOrder( } /** - * {@inheritDoc} + * @inheritdoc */ public function savePaymentInformation( $cartId, @@ -115,9 +117,10 @@ public function savePaymentInformation( $quote->setDataChanges(true); $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 ? $shippingRate->getCarrier() : $shippingAddress->getShippingMethod() + ); } } $this->paymentMethodManagement->set($cartId, $paymentMethod); @@ -125,7 +128,7 @@ public function savePaymentInformation( } /** - * {@inheritDoc} + * @inheritdoc */ public function getPaymentInformation($cartId) { diff --git a/app/code/Magento/Checkout/Model/ShippingInformationManagement.php b/app/code/Magento/Checkout/Model/ShippingInformationManagement.php index 381ee2b9015c..cd3bd2c5a7de 100644 --- a/app/code/Magento/Checkout/Model/ShippingInformationManagement.php +++ b/app/code/Magento/Checkout/Model/ShippingInformationManagement.php @@ -19,6 +19,9 @@ use Magento\Framework\App\ObjectManager; /** + * Class ShippingInformationManagement + * + * @package Magento\Checkout\Model * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ShippingInformationManagement implements \Magento\Checkout\Api\ShippingInformationManagementInterface @@ -99,8 +102,8 @@ class ShippingInformationManagement implements \Magento\Checkout\Api\ShippingInf * @param \Magento\Customer\Api\AddressRepositoryInterface $addressRepository * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\Quote\Model\Quote\TotalsCollector $totalsCollector - * @param CartExtensionFactory|null $cartExtensionFactory, - * @param ShippingAssignmentFactory|null $shippingAssignmentFactory, + * @param CartExtensionFactory|null $cartExtensionFactory + * @param ShippingAssignmentFactory|null $shippingAssignmentFactory * @param ShippingFactory|null $shippingFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -136,7 +139,14 @@ public function __construct( } /** - * {@inheritDoc} + * Save address information. + * + * @param int $cartId + * @param \Magento\Checkout\Api\Data\ShippingInformationInterface $addressInformation + * @return \Magento\Checkout\Api\Data\PaymentDetailsInterface + * @throws InputException + * @throws NoSuchEntityException + * @throws StateException */ public function saveAddressInformation( $cartId, @@ -151,6 +161,10 @@ public function saveAddressInformation( $address->setCustomerAddressId(null); } + if ($billingAddress && !$billingAddress->getCustomerAddressId()) { + $billingAddress->setCustomerAddressId(null); + } + if (!$address->getCountryId()) { throw new StateException(__('The shipping address is missing. Set the address and try again.')); } @@ -208,6 +222,8 @@ protected function validateQuote(\Magento\Quote\Model\Quote $quote) } /** + * Prepare shipping assignment. + * * @param CartInterface $quote * @param AddressInterface $address * @param string $method diff --git a/app/code/Magento/Checkout/Observer/SalesQuoteSaveAfterObserver.php b/app/code/Magento/Checkout/Observer/SalesQuoteSaveAfterObserver.php index d926e33d5411..6bc7965ff5e3 100644 --- a/app/code/Magento/Checkout/Observer/SalesQuoteSaveAfterObserver.php +++ b/app/code/Magento/Checkout/Observer/SalesQuoteSaveAfterObserver.php @@ -7,6 +7,9 @@ use Magento\Framework\Event\ObserverInterface; +/** + * Class SalesQuoteSaveAfterObserver + */ class SalesQuoteSaveAfterObserver implements ObserverInterface { /** @@ -24,15 +27,18 @@ public function __construct(\Magento\Checkout\Model\Session $checkoutSession) } /** + * Assign quote to session + * * @param \Magento\Framework\Event\Observer $observer * @return void */ public function execute(\Magento\Framework\Event\Observer $observer) { + /* @var \Magento\Quote\Model\Quote $quote */ $quote = $observer->getEvent()->getQuote(); - /* @var $quote \Magento\Quote\Model\Quote */ + if ($quote->getIsCheckoutCart()) { - $this->checkoutSession->getQuoteId($quote->getId()); + $this->checkoutSession->setQuoteId($quote->getId()); } } } diff --git a/app/code/Magento/Checkout/Plugin/Model/Quote/ResetQuoteAddresses.php b/app/code/Magento/Checkout/Plugin/Model/Quote/ResetQuoteAddresses.php index 3791b28917e9..e7776b3dcbed 100644 --- a/app/code/Magento/Checkout/Plugin/Model/Quote/ResetQuoteAddresses.php +++ b/app/code/Magento/Checkout/Plugin/Model/Quote/ResetQuoteAddresses.php @@ -15,10 +15,11 @@ class ResetQuoteAddresses { /** + * Clears the quote addresses when all the items are removed from the cart + * * @param Quote $quote * @param Quote $result * @param mixed $itemId - * * @return Quote * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -28,6 +29,10 @@ public function afterRemoveItem(Quote $quote, Quote $result, $itemId): Quote foreach ($result->getAllAddresses() as $address) { $result->removeAddress($address->getId()); } + $extensionAttributes = $result->getExtensionAttributes(); + if (!$result->isVirtual() && $extensionAttributes && $extensionAttributes->getShippingAssignments()) { + $extensionAttributes->setShippingAssignments([]); + } } return $result; diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AdminCheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AdminCheckoutActionGroup.xml new file mode 100644 index 000000000000..a29564b2457a --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AdminCheckoutActionGroup.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"> + <!-- Checkout select Check/Money billing method --> + <actionGroup name="AdminCheckoutSelectCheckMoneyOrderBillingMethodActionGroup"> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <conditionalClick selector="{{AdminCheckoutPaymentSection.checkBillingMethodByName('Check / Money order')}}" dependentSelector="{{AdminCheckoutPaymentSection.checkBillingMethodByName('Check / Money order')}}" visible="true" stepKey="selectCheckmoBillingMethod"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterBillingMethodSelection"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertMiniShoppingCartSubTotalActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertMiniShoppingCartSubTotalActionGroup.xml new file mode 100644 index 000000000000..8c5c6f41fffa --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertMiniShoppingCartSubTotalActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertMiniShoppingCartSubTotalActionGroup"> + <arguments> + <argument name="dataQuote" type="entity" /> + </arguments> + <waitForPageLoad stepKey="waitForPageLoad" time="120"/> + <grabTextFrom selector="{{StorefrontMinicartSection.miniCartSubtotalField}}" stepKey="grabMiniCartTotal" /> + <assertContains stepKey="assertMiniCartTotal"> + <actualResult type="variable">$grabMiniCartTotal</actualResult> + <expectedResult type="string">{{dataQuote.subtotal}}</expectedResult> + </assertContains> + <assertContains stepKey="assertMiniCartCurrency"> + <actualResult type="variable">$grabMiniCartTotal</actualResult> + <expectedResult type="string">{{dataQuote.currency}}</expectedResult> + </assertContains> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontEmailNoteMessageOnCheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontEmailNoteMessageOnCheckoutActionGroup.xml new file mode 100644 index 000000000000..c4fc753e7371 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontEmailNoteMessageOnCheckoutActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStorefrontEmailNoteMessageOnCheckoutActionGroup"> + <arguments> + <argument name="message" type="string" defaultValue="You can create an account after checkout." /> + </arguments> + <waitForElementVisible selector="{{StorefrontCheckoutCheckoutCustomerLoginSection.emailNoteMessage}}" stepKey="waitForFormValidation"/> + <see selector="{{StorefrontCheckoutCheckoutCustomerLoginSection.emailNoteMessage}}" userInput="{{message}}" stepKey="seeTheNoteMessageIsDisplayed"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontEmailTooltipContentOnCheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontEmailTooltipContentOnCheckoutActionGroup.xml new file mode 100644 index 000000000000..f9c6771262cc --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontEmailTooltipContentOnCheckoutActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStorefrontEmailTooltipContentOnCheckoutActionGroup"> + <arguments> + <argument name="content" type="string" defaultValue="We'll send your order confirmation here." /> + </arguments> + <waitForElementVisible selector="{{StorefrontCheckoutCheckoutCustomerLoginSection.emailTooltipButton}}" stepKey="waitForTooltipButtonVisible" /> + <click selector="{{StorefrontCheckoutCheckoutCustomerLoginSection.emailTooltipButton}}" stepKey="clickEmailTooltipButton" /> + <see selector="{{StorefrontCheckoutCheckoutCustomerLoginSection.emailTooltipContent}}" userInput="{{content}}" stepKey="seeEmailTooltipContent" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontEmailValidationMessageOnCheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontEmailValidationMessageOnCheckoutActionGroup.xml new file mode 100644 index 000000000000..14b96ed46ce6 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontEmailValidationMessageOnCheckoutActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStorefrontEmailValidationMessageOnCheckoutActionGroup"> + <arguments> + <argument name="message" type="string" defaultValue="Please enter a valid email address (Ex: johndoe@domain.com)." /> + </arguments> + <waitForElementVisible selector="{{StorefrontCheckoutCheckoutCustomerLoginSection.emailErrorMessage}}" stepKey="waitForFormValidation"/> + <see selector="{{StorefrontCheckoutCheckoutCustomerLoginSection.emailErrorMessage}}" userInput="{{message}}" stepKey="seeTheErrorMessageIsDisplayed"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml index 41be15d0ca41..2e4b742ece8e 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml @@ -8,16 +8,60 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Checkout select Flat Rate shipping method --> + <actionGroup name="CheckoutSelectFlatRateShippingMethodActionGroup"> + <conditionalClick selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('Flat Rate')}}" dependentSelector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('Flat Rate')}}" visible="true" stepKey="selectFlatRateShippingMethod"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskForNextButton"/> + </actionGroup> + <!-- Go to checkout from minicart --> <actionGroup name="GoToCheckoutFromMinicartActionGroup"> - <wait stepKey="wait" time="10" /> + <waitForElementNotVisible selector="{{StorefrontMinicartSection.emptyCart}}" stepKey="waitUpdateQuantity" /> + <wait time="5" stepKey="waitMinicartRendering"/> <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickCart"/> <click selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="goToCheckout"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> + + <!-- Go to checkout from cart --> + <actionGroup name="GoToCheckoutFromCartActionGroup"> + <waitForElementNotVisible selector="{{StorefrontMinicartSection.emptyCart}}" stepKey="waitUpdateQuantity" /> + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickCart"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <seeInCurrentUrl url="{{CheckoutCartPage.url}}" stepKey="assertCheckoutCartUrl"/> + <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="goToCheckout"/> </actionGroup> <!-- Guest checkout filling shipping section --> <actionGroup name="GuestCheckoutFillingShippingSectionActionGroup"> + <arguments> + <argument name="customerVar" defaultValue="CustomerEntityOne"/> + <argument name="customerAddressVar" defaultValue="CustomerAddressSimple"/> + <!--First available shipping method will be selected if value is not passed for shippingMethod--> + <argument name="shippingMethod" defaultValue="" type="string"/> + </arguments> + <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{customerVar.email}}" stepKey="enterEmail"/> + <fillField selector="{{CheckoutShippingSection.firstName}}" userInput="{{customerVar.firstname}}" stepKey="enterFirstName"/> + <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{customerVar.lastname}}" stepKey="enterLastName"/> + <fillField selector="{{CheckoutShippingSection.street}}" userInput="{{customerAddressVar.street[0]}}" stepKey="enterStreet"/> + <fillField selector="{{CheckoutShippingSection.city}}" userInput="{{customerAddressVar.city}}" stepKey="enterCity"/> + <selectOption selector="{{CheckoutShippingSection.region}}" userInput="{{customerAddressVar.state}}" stepKey="selectRegion"/> + <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="{{customerAddressVar.postcode}}" stepKey="enterPostcode"/> + <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{customerAddressVar.telephone}}" stepKey="enterTelephone"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <click selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('shippingMethod')}}" stepKey="selectShippingMethod"/> + <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> + <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + </actionGroup> + + <actionGroup name="GuestCheckoutFillShippingNoWaitForPaymentActionGroup" extends="GuestCheckoutFillingShippingSectionActionGroup"> + <remove keyForRemoval="waitForPaymentSectionLoaded"/> + <remove keyForRemoval="assertCheckoutPaymentUrl"/> + </actionGroup> + + <!-- Guest checkout filling shipping section without region --> + <actionGroup name="GuestCheckoutFillingShippingSectionWithoutRegionActionGroup"> <arguments> <argument name="customerVar"/> <argument name="customerAddressVar"/> @@ -27,8 +71,8 @@ <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{customerVar.lastname}}" stepKey="enterLastName"/> <fillField selector="{{CheckoutShippingSection.street}}" userInput="{{customerAddressVar.street[0]}}" stepKey="enterStreet"/> <fillField selector="{{CheckoutShippingSection.city}}" userInput="{{customerAddressVar.city}}" stepKey="enterCity"/> - <selectOption selector="{{CheckoutShippingSection.region}}" userInput="{{customerAddressVar.state}}" stepKey="selectRegion"/> <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="{{customerAddressVar.postcode}}" stepKey="enterPostcode"/> + <selectOption selector="{{CheckoutShippingSection.country}}" userInput="{{customerAddressVar.country_id}}" stepKey="enterCountry"/> <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{customerAddressVar.telephone}}" stepKey="enterTelephone"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> <click selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> @@ -38,8 +82,59 @@ <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> </actionGroup> + <!-- Guest checkout filling shipping section with unavailable payments--> + <actionGroup name="GuestCheckoutFillingShippingSectionUnavailablePaymentActionGroup"> + <arguments> + <argument name="customerVar"/> + <argument name="customerAddressVar"/> + </arguments> + <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{customerVar.email}}" stepKey="enterEmail"/> + <fillField selector="{{CheckoutShippingSection.firstName}}" userInput="{{customerVar.firstname}}" stepKey="enterFirstName"/> + <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{customerVar.lastname}}" stepKey="enterLastName"/> + <fillField selector="{{CheckoutShippingSection.street}}" userInput="{{customerAddressVar.street[0]}}" stepKey="enterStreet"/> + <fillField selector="{{CheckoutShippingSection.city}}" userInput="{{customerAddressVar.city}}" stepKey="enterCity"/> + <selectOption selector="{{CheckoutShippingSection.region}}" userInput="{{customerAddressVar.state}}" stepKey="selectRegion"/> + <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="{{customerAddressVar.postcode}}" stepKey="enterPostcode"/> + <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{customerAddressVar.telephone}}" stepKey="enterTelephone"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <click selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> + <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.noQuotes}}" stepKey="waitMessage"/> + <see userInput="No Payment method available." stepKey="checkMessage"/> + </actionGroup> + + <actionGroup name="GuestCheckoutWithSpecificCountryOptionForPaymentMethodActionGroup" extends="GuestCheckoutFillingShippingSectionUnavailablePaymentActionGroup"> + <arguments> + <argument name="paymentMethod" type="string"/> + </arguments> + <remove keyForRemoval="checkMessage"/> + <dontSee selector="{{CheckoutPaymentSection.paymentMethodByName(paymentMethod)}}" stepKey="paymentMethodDoesNotAvailable"/> + </actionGroup> <!-- Logged in user checkout filling shipping section --> <actionGroup name="LoggedInUserCheckoutFillingShippingSectionActionGroup"> + <arguments> + <argument name="customerVar" defaultValue="CustomerEntityOne"/> + <argument name="customerAddressVar" defaultValue="CustomerAddressSimple"/> + </arguments> + <fillField selector="{{CheckoutShippingSection.firstName}}" userInput="{{customerVar.firstname}}" stepKey="enterFirstName"/> + <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{customerVar.lastname}}" stepKey="enterLastName"/> + <fillField selector="{{CheckoutShippingSection.street}}" userInput="{{customerAddressVar.street[0]}}" stepKey="enterStreet"/> + <fillField selector="{{CheckoutShippingSection.city}}" userInput="{{customerAddressVar.city}}" stepKey="enterCity"/> + <selectOption selector="{{CheckoutShippingSection.region}}" userInput="{{customerAddressVar.state}}" stepKey="selectRegion"/> + <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="{{customerAddressVar.postcode}}" stepKey="enterPostcode"/> + <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{customerAddressVar.telephone}}" stepKey="enterTelephone"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <click selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> + <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> + <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + </actionGroup> + + <!-- Logged in user checkout filling shipping section --> + <actionGroup name="LoggedInUserCheckoutAddNewShippingSectionWithoutRegionActionGroup"> <arguments> <argument name="customerVar"/> <argument name="customerAddressVar"/> @@ -48,9 +143,10 @@ <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{customerVar.lastname}}" stepKey="enterLastName"/> <fillField selector="{{CheckoutShippingSection.street}}" userInput="{{customerAddressVar.street[0]}}" stepKey="enterStreet"/> <fillField selector="{{CheckoutShippingSection.city}}" userInput="{{customerAddressVar.city}}" stepKey="enterCity"/> - <selectOption selector="{{CheckoutShippingSection.region}}" userInput="{{customerAddressVar.state}}" stepKey="selectRegion"/> <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="{{customerAddressVar.postcode}}" stepKey="enterPostcode"/> + <selectOption selector="{{CheckoutShippingSection.country}}" userInput="{{customerAddressVar.country_id}}" stepKey="enterCountry"/> <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{customerAddressVar.telephone}}" stepKey="enterTelephone"/> + <click selector="{{CheckoutShippingSection.saveAddress}}" stepKey="clickSaveAddress"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> <click selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> @@ -59,6 +155,24 @@ <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> </actionGroup> + <!-- Place order with logged the user --> + <actionGroup name="PlaceOrderWithLoggedUserActionGroup"> + <arguments> + <!--First available shipping method will be selected if value is not passed for shippingMethod--> + <argument name="shippingMethod" defaultValue="" type="string"/> + </arguments> + <waitForElementVisible selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="waitProceedToCheckout"/> + <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout"/> + <click selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('shippingMethod')}}" stepKey="selectShippingMethod"/> + <waitForElement selector="{{CheckoutShippingSection.next}}" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" stepKey="waitForPaymentSectionLoaded"/> + <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="waitForPlaceOrderButton"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> + <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="waitForLoadSuccessPage"/> + </actionGroup> + <!-- Check product in checkout cart items --> <actionGroup name="CheckProductInCheckoutCartItemsActionGroup"> <arguments> @@ -74,18 +188,26 @@ <!-- Check order summary in checkout --> <actionGroup name="CheckOrderSummaryInCheckoutActionGroup"> <arguments> - <argument name="subtotal"/> - <argument name="shippingTotal"/> - <argument name="shippingMethod"/> - <argument name="total"/> + <argument name="subtotal" type="string"/> + <argument name="shippingTotal" type="string"/> + <argument name="shippingMethod" type="string"/> + <argument name="total" type="string"/> </arguments> <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> - <see userInput="${{subtotal}}" selector="{{CheckoutPaymentSection.orderSummarySubtotal}}" stepKey="assertSubtotal"/> - <see userInput="${{shippingTotal}}" selector="{{CheckoutPaymentSection.orderSummaryShippingTotal}}" stepKey="assertShipping"/> + <see userInput="{{subtotal}}" selector="{{CheckoutPaymentSection.orderSummarySubtotal}}" stepKey="assertSubtotal"/> + <see userInput="{{shippingTotal}}" selector="{{CheckoutPaymentSection.orderSummaryShippingTotal}}" stepKey="assertShipping"/> <see userInput="{{shippingMethod}}" selector="{{CheckoutPaymentSection.orderSummaryShippingMethod}}" stepKey="assertShippingMethod"/> - <see userInput="${{total}}" selector="{{CheckoutPaymentSection.orderSummaryTotal}}" stepKey="assertTotal"/> + <see userInput="{{total}}" selector="{{CheckoutPaymentSection.orderSummaryTotal}}" stepKey="assertTotal"/> </actionGroup> + <actionGroup name="CheckTotalsSortOrderInSummarySection"> + <arguments> + <argument name="elementName" type="string"/> + <argument name="positionNumber" type="string"/> + </arguments> + <see userInput="{{elementName}}" selector="{{CheckoutCartSummarySection.elementPosition(positionNumber)}}" stepKey="assertElementPosition"/> + </actionGroup> + <!-- Check ship to information in checkout --> <actionGroup name="CheckShipToInformationInCheckoutActionGroup"> <arguments> @@ -113,8 +235,25 @@ <!-- Checkout select Check/Money Order payment --> <actionGroup name="CheckoutSelectCheckMoneyOrderPaymentActionGroup"> - <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> - <conditionalClick selector="{{CheckoutPaymentSection.checkMoneyOrderPayment}}" dependentSelector="{{CheckoutPaymentSection.billingAddress}}" visible="false" stepKey="clickCheckMoneyOrderPayment" /> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <conditionalClick selector="{{StorefrontCheckoutPaymentMethodSection.checkPaymentMethodByName('Check / Money order')}}" dependentSelector="{{StorefrontCheckoutPaymentMethodSection.checkPaymentMethodByName('Check / Money order')}}" visible="true" stepKey="selectCheckmoPaymentMethod"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterPaymentMethodSelection"/> + </actionGroup> + + <!-- Check selected shipping address information on shipping information step --> + <actionGroup name="CheckSelectedShippingAddressInCheckoutActionGroup"> + <arguments> + <argument name="customerVar"/> + <argument name="customerAddressVar"/> + </arguments> + <waitForElement selector="{{CheckoutShippingSection.shippingTab}}" time="30" stepKey="waitForShippingSectionLoaded"/> + <see stepKey="VerifyFirstNameInSelectedAddress" selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerVar.firstname}}" /> + <see stepKey="VerifyLastNameInSelectedAddress" selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerVar.lastname}}" /> + <see stepKey="VerifyStreetInSelectedAddress" selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerAddressVar.street[0]}}" /> + <see stepKey="VerifyCityInSelectedAddress" selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerAddressVar.city}}" /> + <see stepKey="VerifyZipInSelectedAddress" selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerAddressVar.postcode}}" /> + <see stepKey="VerifyPhoneInSelectedAddress" selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerAddressVar.telephone}}" /> </actionGroup> <!-- Check billing address in checkout --> @@ -133,13 +272,29 @@ <see userInput="{{customerAddressVar.telephone}}" selector="{{CheckoutPaymentSection.billingAddress}}" stepKey="assertBillingAddressTelephone"/> </actionGroup> + <!-- Check billing address in checkout with billing address on payment page --> + <actionGroup name="CheckBillingAddressInCheckoutWithBillingAddressOnPaymentPageActionGroup"> + <arguments> + <argument name="customerVar"/> + <argument name="customerAddressVar"/> + </arguments> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> + <see userInput="{{customerVar.firstName}}" selector="{{CheckoutPaymentWithDisplayBillingAddressOnPaymentPageSection.billingAddressDetails}}" stepKey="assertBillingAddressDetailsFirstName"/> + <see userInput="{{customerVar.lastName}}" selector="{{CheckoutPaymentWithDisplayBillingAddressOnPaymentPageSection.billingAddressDetails}}" stepKey="assertBillingAddressDetailsLastName"/> + <see userInput="{{customerAddressVar.street[0]}}" selector="{{CheckoutPaymentWithDisplayBillingAddressOnPaymentPageSection.billingAddressDetails}}" stepKey="assertBillingAddressDetailsStreet"/> + <see userInput="{{customerAddressVar.city}}" selector="{{CheckoutPaymentWithDisplayBillingAddressOnPaymentPageSection.billingAddressDetails}}" stepKey="assertBillingAddressDetailsCity"/> + <see userInput="{{customerAddressVar.state}}" selector="{{CheckoutPaymentWithDisplayBillingAddressOnPaymentPageSection.billingAddressDetails}}" stepKey="assertBillingAddressDetailsState"/> + <see userInput="{{customerAddressVar.postcode}}" selector="{{CheckoutPaymentWithDisplayBillingAddressOnPaymentPageSection.billingAddressDetails}}" stepKey="assertBillingAddressDetailsPostcode"/> + <see userInput="{{customerAddressVar.telephone}}" selector="{{CheckoutPaymentWithDisplayBillingAddressOnPaymentPageSection.billingAddressDetails}}" stepKey="assertBillingAddressDetailsTelephone"/> + </actionGroup> + <!-- Checkout place order --> <actionGroup name="CheckoutPlaceOrderActionGroup"> <arguments> <argument name="orderNumberMessage"/> <argument name="emailYouMessage"/> </arguments> - <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> <see selector="{{CheckoutSuccessMainSection.success}}" userInput="{{orderNumberMessage}}" stepKey="seeOrderNumber"/> <see selector="{{CheckoutSuccessMainSection.success}}" userInput="{{emailYouMessage}}" stepKey="seeEmailYou"/> @@ -154,4 +309,18 @@ <conditionalClick selector="{{CheckoutCartSummarySection.shippingHeading}}" dependentSelector="{{CheckoutCartSummarySection.country}}" visible="false" stepKey="openShippingDetails"/> <see selector="{{CheckoutCartSummarySection.countryParameterized('placeNumber')}}" userInput="{{country}}" stepKey="seeCountry"/> </actionGroup> -</actionGroups> \ No newline at end of file + + <actionGroup name="StorefrontSignOutActionGroup"> + <click selector="{{StoreFrontSignOutSection.customerAccount}}" stepKey="clickCustomerButton"/> + <click selector="{{StoreFrontSignOutSection.signOut}}" stepKey="clickToSignOut"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see userInput="You are signed out" stepKey="signOut"/> + </actionGroup> + + <!--Click Place Order button--> + <actionGroup name="ClickPlaceOrderActionGroup"> + <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> + <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="waitForLoadSuccessPage"/> + </actionGroup> +</actionGroups> 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 000000000000..0e6994e8feaa --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/ClearShippingAddressActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details.z + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="ClearShippingAddressActionGroup"> + <clearField selector="{{CheckoutShippingSection.firstName}}" stepKey="clearFieldFirstName"/> + <clearField selector="{{CheckoutShippingSection.company}}" stepKey="clearFieldCompany"/> + <clearField selector="{{CheckoutShippingSection.street}}" stepKey="clearFieldStreetAddress"/> + <clearField selector="{{CheckoutShippingSection.city}}" stepKey="clearFieldCityName"/> + <selectOption selector="{{CheckoutShippingSection.region}}" userInput="" stepKey="clearFieldRegion"/> + <clearField selector="{{CheckoutShippingSection.postcode}}" stepKey="clearFieldZip"/> + <selectOption selector="{{CheckoutShippingSection.country}}" userInput="" stepKey="clearFieldCounty"/> + <clearField selector="{{CheckoutShippingSection.telephone}}" stepKey="clearFieldPhoneNumber"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/FillNewShippingAddressModalActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/FillNewShippingAddressModalActionGroup.xml new file mode 100644 index 000000000000..7035855cc0ed --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/FillNewShippingAddressModalActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="FillNewShippingAddressModalActionGroup" extends="FillShippingAddressOneStreetActionGroup"> + <arguments> + <argument name="address"/> + </arguments> + <selectOption stepKey="selectRegion" selector="{{CheckoutShippingSection.region}}" + userInput="{{address.state}}" after="fillCityName"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/FillShippingAddressOneStreetActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/FillShippingAddressOneStreetActionGroup.xml new file mode 100644 index 000000000000..cbc6c5320b01 --- /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/FillShippingZipFormActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/FillShippingZipFormActionGroup.xml new file mode 100644 index 000000000000..f12bf4344ab1 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/FillShippingZipFormActionGroup.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="FillShippingZipForm"> + <arguments> + <argument name="address"/> + </arguments> + <conditionalClick stepKey="openShippingDetails" selector="{{CheckoutCartSummarySection.shippingHeading}}" dependentSelector="{{CheckoutCartSummarySection.country}}" visible="false"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <waitForElementVisible selector="{{CheckoutCartSummarySection.country}}" time="30" stepKey="waitForCountryFieldAppears"/> + <selectOption stepKey="selectCountry" selector="{{CheckoutCartSummarySection.country}}" userInput="{{address.country}}"/> + <selectOption stepKey="selectStateProvince" selector="{{CheckoutCartSummarySection.stateProvince}}" userInput="{{address.state}}"/> + <fillField stepKey="fillPostCode" selector="{{CheckoutCartSummarySection.postcode}}" userInput="{{address.postcode}}"/> + <waitForPageLoad stepKey="waitForFormUpdate"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewBillingAddressActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewBillingAddressActionGroup.xml index 5a71b82c15cd..34f2cfe7f7ff 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewBillingAddressActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewBillingAddressActionGroup.xml @@ -44,5 +44,32 @@ <selectOption stepKey="selectCounty" selector="{{classPrefix}} {{CheckoutShippingSection.country}}" userInput="{{Address.country_id}}"/> <waitForPageLoad stepKey="waitForFormUpdate2"/> </actionGroup> + <!--Filling address without second address field and without state field--> + <actionGroup name="LoggedInCheckoutWithOneAddressFieldWithoutStateField" extends="LoggedInCheckoutFillNewBillingAddressActionGroup"> + <remove keyForRemoval="fillStreetAddress2"/> + <remove keyForRemoval="selectState"/> + </actionGroup> -</actionGroups> \ No newline at end of file + <actionGroup name="clearCheckoutAddressPopupFieldsActionGroup"> + <arguments> + <argument name="classPrefix" type="string" defaultValue=""/> + </arguments> + <clearField selector="{{classPrefix}} {{CheckoutShippingSection.firstName}}" stepKey="clearFieldFirstName"/> + <clearField selector="{{classPrefix}} {{CheckoutShippingSection.lastName}}" stepKey="clearFieldLastName"/> + <clearField selector="{{classPrefix}} {{CheckoutShippingSection.company}}" stepKey="clearFieldCompany"/> + <clearField selector="{{classPrefix}} {{CheckoutShippingSection.street}}" stepKey="clearFieldStreetAddress1"/> + <clearField selector="{{classPrefix}} {{CheckoutShippingSection.street2}}" stepKey="clearFieldStreetAddress2"/> + <clearField selector="{{classPrefix}} {{CheckoutShippingSection.city}}" stepKey="clearFieldCityName"/> + <selectOption selector="{{classPrefix}} {{CheckoutShippingSection.region}}" userInput="" stepKey="clearFieldRegion"/> + <clearField selector="{{classPrefix}} {{CheckoutShippingSection.postcode}}" stepKey="clearFieldZip"/> + <selectOption selector="{{classPrefix}} {{CheckoutShippingSection.country}}" userInput="" stepKey="clearFieldCounty"/> + <clearField selector="{{classPrefix}} {{CheckoutShippingSection.telephone}}" stepKey="clearFieldPhoneNumber"/> + </actionGroup> + <actionGroup name="GuestCheckoutSelectPaymentAndFillNewBillingAddressActionGroup" extends="GuestCheckoutFillNewBillingAddressActionGroup"> + <arguments> + <argument name="paymentMethod" type="string"/> + </arguments> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" after="waitForLoading3" stepKey="waitForPaymentSectionLoaded"/> + <conditionalClick selector="{{CheckoutPaymentSection.paymentMethodByName(paymentMethod)}}" dependentSelector="{{CheckoutPaymentSection.billingAddress}}" visible="false" parametrized="true" before="enterFirstName" stepKey="clickCheckMoneyOrderPayment"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/IdentityOfDefaultBillingAndShippingAddressActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/IdentityOfDefaultBillingAndShippingAddressActionGroup.xml index 6e5f127eefc1..15c157a98264 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/IdentityOfDefaultBillingAndShippingAddressActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/IdentityOfDefaultBillingAndShippingAddressActionGroup.xml @@ -5,9 +5,9 @@ * See COPYING.txt for license details. */ --> -<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <!--Assert That Shipping And Billing Address are the same--> <actionGroup name="AssertThatShippingAndBillingAddressTheSame"> <!--Get shipping and billing addresses--> @@ -18,5 +18,4 @@ <see userInput="Billing Address" stepKey="seeBillingAddress"/> <assertEquals stepKey="assert" actual="$billingAddr" expected="$shippingAddr"/> </actionGroup> - </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAddSimpleProductToCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAddSimpleProductToCartActionGroup.xml new file mode 100644 index 000000000000..7bfc87cd8d6f --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAddSimpleProductToCartActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Add Product to Cart from the category page and check message --> + <actionGroup name="StorefrontAddSimpleProductToCartActionGroup"> + <arguments> + <argument name="product" type="entity"/> + </arguments> + <moveMouseOver selector="{{StorefrontCategoryProductSection.ProductInfoByName(product.name)}}" stepKey="moveMouseOverProduct" /> + <click selector="{{StorefrontCategoryProductSection.ProductAddToCartByName(product.name)}}" stepKey="clickAddToCart" /> + <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" stepKey="waitForSuccessMessage" /> + <see selector="{{StorefrontCategoryMainSection.SuccessMsg}}" userInput="You added {{product.name}} to your shopping cart." stepKey="assertSuccessMessage"/> + </actionGroup> +</actionGroups> + diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCartEstimateShippingAndTaxActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCartEstimateShippingAndTaxActionGroup.xml new file mode 100644 index 000000000000..0d6f34098c04 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCartEstimateShippingAndTaxActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Assert Estimate Shipping and Tax Data in Cart --> + <actionGroup name="StorefrontAssertCartEstimateShippingAndTaxActionGroup"> + <arguments> + <argument name="customerData" defaultValue="Simple_US_CA_Customer_For_Shipment"/> + </arguments> + <seeInField selector="{{CheckoutCartSummarySection.country}}" userInput="{{customerData.country}}" stepKey="assertCountryFieldInCartEstimateShippingAndTaxSection"/> + <seeInField selector="{{CheckoutCartSummarySection.stateProvinceInput}}" userInput="{{customerData.region}}" stepKey="assertStateProvinceInCartEstimateShippingAndTaxSection"/> + <seeInField selector="{{CheckoutCartSummarySection.postcode}}" userInput="{{customerData.postcode}}" stepKey="assertZipPostalCodeInCartEstimateShippingAndTaxSection"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCartShippingMethodSelectedActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCartShippingMethodSelectedActionGroup.xml new file mode 100644 index 000000000000..4061f97821cd --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCartShippingMethodSelectedActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Assert Shipping Method Is Checked on Cart --> + <actionGroup name="StorefrontAssertCartShippingMethodSelectedActionGroup"> + <arguments> + <argument name="carrierCode" type="string"/> + <argument name="methodCode" type="string"/> + </arguments> + <seeCheckboxIsChecked selector="{{CheckoutCartSummarySection.shippingMethodElementId(carrierCode, methodCode)}}" stepKey="assertShippingMethodIsChecked"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCheckoutEstimateShippingInformationActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCheckoutEstimateShippingInformationActionGroup.xml new file mode 100644 index 000000000000..82d7e12105b8 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCheckoutEstimateShippingInformationActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Assert Estimate Shipping and Tax Data on Checkout --> + <actionGroup name="StorefrontAssertCheckoutEstimateShippingInformationActionGroup"> + <arguments> + <argument name="customerData" defaultValue="Simple_US_CA_Customer_For_Shipment"/> + </arguments> + <seeInField selector="{{CheckoutShippingGuestInfoSection.country}}" userInput="{{customerData.country}}" stepKey="assertCountryField"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.region}}" userInput="{{customerData.region}}" stepKey="assertStateProvinceField"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.postcode}}" userInput="{{customerData.postcode}}" stepKey="assertZipPostalCodeField"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCheckoutShippingMethodSelectedActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCheckoutShippingMethodSelectedActionGroup.xml new file mode 100644 index 000000000000..33f2852f1f0a --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCheckoutShippingMethodSelectedActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Assert Shipping Method by Name Is Checked on Checkout --> + <actionGroup name="StorefrontAssertCheckoutShippingMethodSelectedActionGroup"> + <arguments> + <argument name="shippingMethod"/> + </arguments> + <seeCheckboxIsChecked selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('shippingMethod')}}" stepKey="assertShippingMethodByNameIsChecked"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertGuestShippingInfoActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertGuestShippingInfoActionGroup.xml new file mode 100644 index 000000000000..02c362bf3405 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertGuestShippingInfoActionGroup.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Assert guest shipping info on checkout --> + <actionGroup name="StorefrontAssertGuestShippingInfoActionGroup"> + <arguments> + <argument name="customerData" defaultValue="Simple_UK_Customer_For_Shipment"/> + </arguments> + <seeInField selector="{{CheckoutShippingGuestInfoSection.email}}" userInput="{{customerData.email}}" stepKey="assertEmailAddress"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.firstName}}" userInput="{{customerData.firstName}}" stepKey="assertFirstName"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.lastName}}" userInput="{{customerData.lastName}}" stepKey="assertLastName"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.company}}" userInput="{{customerData.company}}" stepKey="assertCompany"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.street}}" userInput="{{customerData.streetFirstLine}}" stepKey="assertAddressFirstLine"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.street2}}" userInput="{{customerData.streetSecondLine}}" stepKey="assertAddressSecondLine"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.city}}" userInput="{{customerData.city}}" stepKey="assertCity"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.country}}" userInput="{{customerData.country}}" stepKey="assertCountry"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.regionInput}}" userInput="{{customerData.region}}" stepKey="assertStateProvince"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.postcode}}" userInput="{{customerData.postcode}}" stepKey="assertZipPostalCode"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.telephone}}" userInput="{{customerData.telephone}}" stepKey="assertPhoneNumber"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertShippingMethodPresentInCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertShippingMethodPresentInCartActionGroup.xml new file mode 100644 index 000000000000..3d8530ae8370 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertShippingMethodPresentInCartActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Assert shipping method is present in cart --> + <actionGroup name="StorefrontAssertShippingMethodPresentInCartActionGroup"> + <arguments> + <argument name="shippingMethod" type="string"/> + </arguments> + <see selector="{{CheckoutCartSummarySection.shippingMethodLabel}}" userInput="{{shippingMethod}}" stepKey="assertShippingMethodIsPresentInCart"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCartEstimateShippingAndTaxActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCartEstimateShippingAndTaxActionGroup.xml new file mode 100644 index 000000000000..4176859f99f7 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCartEstimateShippingAndTaxActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Fill Estimate Shipping and Tax fields --> + <actionGroup name="StorefrontCartEstimateShippingAndTaxActionGroup"> + <arguments> + <argument name="estimateAddress" defaultValue="EstimateAddressCalifornia"/> + </arguments> + <conditionalClick selector="{{CheckoutCartSummarySection.estimateShippingAndTax}}" dependentSelector="{{CheckoutCartSummarySection.country}}" visible="false" stepKey="clickOnEstimateShippingAndTax"/> + <waitForElementVisible selector="{{CheckoutCartSummarySection.country}}" stepKey="waitForCountrySelectorIsVisible"/> + <selectOption selector="{{CheckoutCartSummarySection.country}}" userInput="{{estimateAddress.country}}" stepKey="selectCountry"/> + <waitForLoadingMaskToDisappear stepKey="waitForCountryLoadingMaskDisappear"/> + <selectOption selector="{{CheckoutCartSummarySection.stateProvince}}" userInput="{{estimateAddress.state}}" stepKey="selectStateProvince"/> + <waitForLoadingMaskToDisappear stepKey="waitForStateLoadingMaskDisappear"/> + <fillField selector="{{CheckoutCartSummarySection.postcode}}" userInput="{{estimateAddress.zipCode}}" stepKey="fillZipPostalCodeField"/> + <waitForLoadingMaskToDisappear stepKey="waitForZipLoadingMaskDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontFillEmailFieldOnCheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontFillEmailFieldOnCheckoutActionGroup.xml new file mode 100644 index 000000000000..fcac780a3677 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontFillEmailFieldOnCheckoutActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontFillEmailFieldOnCheckoutActionGroup"> + <arguments> + <argument name="email" type="string" /> + </arguments> + <fillField selector="{{StorefrontCheckoutCheckoutCustomerLoginSection.email}}" userInput="{{email}}" stepKey="fillCustomerEmailField"/> + <doubleClick selector="{{StorefrontCheckoutCheckoutCustomerLoginSection.emailTooltipButton}}" stepKey="clickToMoveFocusFromEmailInput" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontFillGuestShippingInfoActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontFillGuestShippingInfoActionGroup.xml new file mode 100644 index 000000000000..e7669d62c79a --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontFillGuestShippingInfoActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Fill data in checkout shipping section --> + <actionGroup name="StorefrontFillGuestShippingInfoActionGroup"> + <arguments> + <argument name="customerData" defaultValue="Simple_UK_Customer_For_Shipment"/> + </arguments> + <fillField selector="{{CheckoutShippingGuestInfoSection.email}}" userInput="{{customerData.email}}" stepKey="fillEmailAddressField"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.firstName}}" userInput="{{customerData.firstName}}" stepKey="fillFirstNameField"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.lastName}}" userInput="{{customerData.lastName}}" stepKey="fillLastNameField"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.company}}" userInput="{{customerData.company}}" stepKey="fillCompanyField"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.street}}" userInput="{{customerData.streetFirstLine}}" stepKey="fillStreetAddressFirstLineField"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.street2}}" userInput="{{customerData.streetSecondLine}}" stepKey="fillStreetAddressSecondLineField"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.city}}" userInput="{{customerData.city}}" stepKey="fillCityField"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.telephone}}" userInput="{{customerData.telephone}}" stepKey="fillPhoneNumberField"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontMiniCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontMiniCartActionGroup.xml index cb53a2a4f381..e4a388d2c3a5 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontMiniCartActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontMiniCartActionGroup.xml @@ -21,4 +21,25 @@ <waitForElementVisible selector="{{StorefrontMinicartSection.viewAndEditCart}}" stepKey="waitForViewAndEditCartVisible"/> <see selector="{{StorefrontMinicartSection.miniCartItemsText}}" userInput="{{productName}}" stepKey="seeInMiniCart"/> </actionGroup> + + <!--Remove an item from the cart using minicart--> + <actionGroup name="removeProductFromMiniCart"> + <arguments> + <argument name="productName" type="string"/> + </arguments> + <conditionalClick selector="{{StorefrontMinicartSection.showCart}}" dependentSelector="{{StorefrontMinicartSection.miniCartOpened}}" visible="false" stepKey="openMiniCart"/> + <waitForElementVisible selector="{{StorefrontMinicartSection.viewAndEditCart}}" stepKey="waitForMiniCartOpen"/> + <click selector="{{StorefrontMinicartSection.deleteMiniCartItemByName(productName)}}" stepKey="clickDelete"/> + <waitForElementVisible selector="{{StoreFrontRemoveItemModalSection.message}}" stepKey="waitForConfirmationModal"/> + <see selector="{{StoreFrontRemoveItemModalSection.message}}" userInput="Are you sure you would like to remove this item from the shopping cart?" stepKey="seeDeleteConfirmationMessage"/> + <click selector="{{StoreFrontRemoveItemModalSection.ok}}" stepKey="confirmDelete"/> + <waitForPageLoad stepKey="waitForDeleteToFinish"/> + </actionGroup> + + <!--Check that the minicart is empty--> + <actionGroup name="assertMiniCartEmpty"> + <dontSeeElement selector="{{StorefrontMinicartSection.productCount}}" stepKey="dontSeeMinicartProductCount"/> + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="expandMinicart"/> + <see selector="{{StorefrontMinicartSection.minicartContent}}" userInput="You have no items in your shopping cart." stepKey="seeEmptyCartMessage"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontOpenCheckoutPageActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontOpenCheckoutPageActionGroup.xml new file mode 100644 index 000000000000..b18d476c02c6 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontOpenCheckoutPageActionGroup.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontOpenCheckoutPageActionGroup"> + <amOnPage url="{{CheckoutPage.url}}" stepKey="openCheckoutPage" /> + <waitForPageLoad stepKey="waitForCheckoutPageLoaded" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml index 29a76675fa52..24ed05583b6f 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml @@ -16,8 +16,9 @@ </arguments> <moveMouseOver selector="{{StorefrontCategoryProductSection.ProductInfoByName(product.name)}}" stepKey="moveMouseOverProduct" /> <click selector="{{StorefrontCategoryProductSection.ProductAddToCartByName(product.name)}}" stepKey="clickAddToCart" /> - <!-- @TODO: Use general message selector after MQE-694 is fixed --> - <waitForElement selector="{{StorefrontMessagesSection.messageProductAddedToCart(product.name)}}" time="30" stepKey="assertMessage"/> + <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" stepKey="waitForSuccessMessage" /> + <see selector="{{StorefrontCategoryMainSection.SuccessMsg}}" userInput="You added {{product.name}} to your shopping cart." stepKey="assertSuccessMessage"/> + <seeLink stepKey="assertLinkToShoppingCart" url="{{_ENV.MAGENTO_BASE_URL}}checkout/cart/" userInput="shopping cart" /> <waitForText userInput="{{productCount}}" selector="{{StorefrontMinicartSection.productCount}}" time="30" stepKey="assertProductCount"/> </actionGroup> @@ -30,8 +31,9 @@ </arguments> <moveMouseOver selector="{{StorefrontCategoryProductSection.ProductInfoByName(product.name)}}" stepKey="moveMouseOverProduct" /> <click selector="{{StorefrontCategoryProductSection.ProductAddToCartByName(product.name)}}" stepKey="clickAddToCart" /> - <!-- @TODO: Use general message selector after MQE-694 is fixed --> - <waitForElement selector="{{StorefrontMessagesSection.messageProductAddedToCart(product.name)}}" time="30" stepKey="assertMessage"/> + <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" stepKey="waitForSuccessMessage" /> + <see selector="{{StorefrontCategoryMainSection.SuccessMsg}}" userInput="You added {{product.name}} to your shopping cart." stepKey="assertSuccessMessage"/> + <seeLink stepKey="assertLinkToShoppingCart" url="{{_ENV.MAGENTO_BASE_URL}}checkout/cart/" userInput="shopping cart" /> <waitForText userInput="{{checkQuantity}}" selector="{{StorefrontMinicartSection.productCount}}" time="30" stepKey="assertProductCount"/> <conditionalClick selector="{{StorefrontMinicartSection.showCart}}" dependentSelector="{{StorefrontMinicartSection.miniCartOpened}}" visible="false" stepKey="openMiniCart"/> <waitForElementVisible selector="{{StorefrontMinicartSection.viewAndEditCart}}" stepKey="waitForViewAndEditCartVisible"/> @@ -46,8 +48,9 @@ <argument name="productCount" type="string"/> </arguments> <click selector="{{StorefrontProductInfoMainSection.AddToCart}}" stepKey="clickAddToCart" /> - <!-- @TODO: Use general message selector after MQE-694 is fixed --> - <waitForElement selector="{{StorefrontMessagesSection.messageProductAddedToCart(product.name)}}" time="30" stepKey="assertMessage"/> + <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" stepKey="waitForSuccessMessage" /> + <see selector="{{StorefrontProductPageSection.messagesBlock}}" userInput="You added {{product.name}} to your shopping cart." stepKey="assertSuccessMessage"/> + <seeLink stepKey="assertLinkToShoppingCart" url="{{_ENV.MAGENTO_BASE_URL}}checkout/cart/" userInput="shopping cart" /> <waitForText userInput="{{productCount}}" selector="{{StorefrontMinicartSection.productCount}}" time="30" stepKey="assertProductCount"/> </actionGroup> @@ -75,17 +78,20 @@ <!-- Check the Cart --> <actionGroup name="StorefrontCheckCartActionGroup"> <arguments> - <argument name="subtotal"/> - <argument name="shipping"/> - <argument name="shippingMethod"/> - <argument name="total"/> + <argument name="subtotal" type="string"/> + <argument name="shipping" type="string"/> + <argument name="shippingMethod" type="string"/> + <argument name="total" type="string"/> </arguments> <seeInCurrentUrl url="{{CheckoutCartPage.url}}" stepKey="assertUrl"/> - <waitForText userInput="${{total}}" selector="{{CheckoutCartSummarySection.total}}" time="30" stepKey="waitForTotal"/> - <see userInput="${{subtotal}}" selector="{{CheckoutCartSummarySection.subtotal}}" stepKey="assertSubtotal"/> - <see userInput="${{shipping}}" selector="{{CheckoutCartSummarySection.shipping}}" stepKey="assertShipping"/> + <waitForPageLoad stepKey="waitForCartPage"/> + <conditionalClick selector="{{CheckoutCartSummarySection.shippingHeading}}" dependentSelector="{{CheckoutCartSummarySection.shippingMethodForm}}" visible="false" stepKey="openEstimateShippingSection"/> + <waitForElementVisible selector="{{CheckoutCartSummarySection.flatRateShippingMethod}}" stepKey="waitForShippingSection"/> + <checkOption selector="{{CheckoutCartSummarySection.flatRateShippingMethod}}" stepKey="selectShippingMethod"/> + <see userInput="{{subtotal}}" selector="{{CheckoutCartSummarySection.subtotal}}" stepKey="assertSubtotal"/> <see userInput="({{shippingMethod}})" selector="{{CheckoutCartSummarySection.shippingMethod}}" stepKey="assertShippingMethod"/> - <see userInput="${{total}}" selector="{{CheckoutCartSummarySection.total}}" stepKey="assertTotal"/> + <waitForText userInput="{{shipping}}" selector="{{CheckoutCartSummarySection.shipping}}" time="45" stepKey="assertShipping"/> + <see userInput="{{total}}" selector="{{CheckoutCartSummarySection.total}}" stepKey="assertTotal"/> </actionGroup> <!-- Open the Cart from Minicart--> @@ -106,4 +112,4 @@ <fillField stepKey="fillZip" selector="{{CheckoutCartSummarySection.postcode}}" userInput="{{taxCode.zip}}"/> <waitForPageLoad stepKey="waitForFormUpdate"/> </actionGroup> -</actionGroups> \ No newline at end of file +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontShippmentFromActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontShippmentFromActionGroup.xml new file mode 100644 index 000000000000..d3d96cb9c743 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontShippmentFromActionGroup.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!--Fill shipment form for free shipping--> + <actionGroup name="ShipmentFormFreeShippingActionGroup"> + <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{CustomerEntityOne.email}}" stepKey="setCustomerEmail"/> + <fillField selector="{{CheckoutShippingSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="SetCustomerFirstName"/> + <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="SetCustomerLastName"/> + <fillField selector="{{CheckoutShippingSection.street}}" userInput="{{CustomerAddressSimple.street}}" stepKey="SetCustomerStreetAddress"/> + <fillField selector="{{CheckoutShippingSection.city}}" userInput="{{CustomerAddressSimple.city}}" stepKey="SetCustomerCity"/> + <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="{{CustomerAddressSimple.postcode}}" stepKey="SetCustomerZipCode"/> + <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{CustomerAddressSimple.telephone}}" stepKey="SetCustomerPhoneNumber"/> + <click selector="{{CheckoutShippingSection.region}}" stepKey="clickToSetState"/> + <click selector="{{CheckoutShippingSection.state}}" stepKey="clickToChooseState"/> + <see userInput="$0.00 Free Free Shipping" selector="{{CheckoutShippingMethodsSection.shippingMethodRowByName('Free Shipping')}}" stepKey="seeShippingMethod" after="clickToChooseState"/> + <click selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('Free Shipping')}}" stepKey="selectFlatShippingMethod" after="seeShippingMethod"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask" after="selectFlatShippingMethod"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickToSaveShippingInfo"/> + <waitForPageLoad time="5" stepKey="waitForReviewAndPaymentsPageIsLoaded"/> + <seeInCurrentUrl url="payment" stepKey="reviewAndPaymentIsShown"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontUpdateProductQtyMiniShoppingCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontUpdateProductQtyMiniShoppingCartActionGroup.xml new file mode 100644 index 000000000000..ee8b761a452d --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontUpdateProductQtyMiniShoppingCartActionGroup.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontUpdateProductQtyMiniShoppingCartActionGroup"> + <arguments> + <argument name="product" type="entity" /> + <argument name="quote" type="entity" /> + </arguments> + + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="goToMiniShoppingCart"/> + + <!-- Clearing QTY field --> + <doubleClick selector="{{StorefrontMinicartSection.itemQuantityBySku(product.sku)}}" stepKey="doubleClickOnQtyInput" /> + <pressKey selector="{{StorefrontMinicartSection.itemQuantityBySku(product.sku)}}" parameterArray="[\WebDriverKeys::DELETE]" stepKey="clearQty"/> + <!-- Clearing QTY field --> + + <fillField selector="{{StorefrontMinicartSection.itemQuantityBySku(product.sku)}}" userInput="{{quote.qty}}" stepKey="changeQty"/> + <click selector="{{StorefrontMinicartSection.itemQuantityUpdateBySku(product.sku)}}" stepKey="clickUpdateButton"/> + <waitForPageLoad stepKey="waitForProductQtyUpdate" /> + + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/Data/CountryData.xml b/app/code/Magento/Checkout/Test/Mftf/Data/CountryData.xml index 26bc6ff641a9..7fc349bf9f05 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Data/CountryData.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Data/CountryData.xml @@ -7,10 +7,32 @@ --> <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> <entity name="Countries" type="countryArray"> <array key="country"> <item>Bahamas</item> </array> </entity> -</entities> \ No newline at end of file + <entity name="DefaultCountriesWithRequiredRegions" type="countryArray"> + <array key="country"> + <item>Australia</item> + <item>Brazil</item> + <item>Canada</item> + <item>Croatia</item> + <item>Estonia</item> + <item>India</item> + <item>Latvia</item> + <item>Lithuania</item> + <item>Romania</item> + <item>Spain</item> + <item>Switzerland</item> + <item>United States</item> + <item>Australia</item> + </array> + </entity> + <entity name="CustomCountryWithRequiredRegion" type="countryArray"> + <array key="country"> + <item>United Kingdom</item> + </array> + </entity> +</entities> diff --git a/app/code/Magento/Checkout/Test/Mftf/Data/EstimateAndTaxData.xml b/app/code/Magento/Checkout/Test/Mftf/Data/EstimateAndTaxData.xml new file mode 100644 index 000000000000..36dea5a521a0 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Data/EstimateAndTaxData.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="EstimateAddressCalifornia"> + <data key="country">United States</data> + <data key="state">California</data> + <data key="zipCode">90240</data> + </entity> +</entities> diff --git a/app/code/Magento/Checkout/Test/Mftf/Data/QuoteData.xml b/app/code/Magento/Checkout/Test/Mftf/Data/QuoteData.xml index 530157851191..e7a5992ad894 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Data/QuoteData.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Data/QuoteData.xml @@ -15,4 +15,26 @@ <data key="total">495.00</data> <data key="shippingMethod">Flat Rate - Fixed</data> </entity> + <entity name="simpleOrderQty2" type="Quote"> + <data key="price">560.00</data> + <data key="qty">2</data> + <data key="subtotal">1,120.00</data> + <data key="shipping">10.00</data> + <data key="total">1,130.00</data> + <data key="shippingMethod">Flat Rate - Fixed</data> + <data key="currency">$</data> + </entity> + <entity name="quoteQty3Price123" type="Quote"> + <data key="price">123.00</data> + <data key="qty">3</data> + <data key="subtotal">369.00</data> + <data key="currency">$</data> + </entity> + <entity name="quoteQty11Subtotal1320" type="Quote"> + <data key="price">100.00</data> + <data key="customOptionsPrice">20</data> + <data key="qty">11</data> + <data key="subtotal">1,320.00</data> + <data key="currency">$</data> + </entity> </entities> diff --git a/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutCartPage.xml b/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutCartPage.xml index b0acc64c7772..bf17800f29ad 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/Page/CheckoutShippingPage.xml b/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutShippingPage.xml index 07f9e9e6481f..c8641f7d8fbf 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutShippingPage.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutShippingPage.xml @@ -11,5 +11,6 @@ <page name="CheckoutShippingPage" url="/checkout/#shipping" module="Checkout" area="storefront"> <section name="CheckoutShippingGuestInfoSection"/> <section name="CheckoutShippingSection"/> + <section name="StorefrontCheckoutAddressPopupSection"/> </page> </pages> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/AdminCheckoutPaymentSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/AdminCheckoutPaymentSection.xml new file mode 100644 index 000000000000..2b5dd512bc4e --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/AdminCheckoutPaymentSection.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="AdminCheckoutPaymentSection"> + <element name="checkBillingMethodByName" type="radio" selector="//div[@id='order-billing_method']//dl[@class='admin__payment-methods']//dt//label[contains(., '{{methodName}}')]/..//input" parameterized="true"/> + </section> +</sections> 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 000000000000..aa23f3364771 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartCrossSellSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="CheckoutCartCrossSellSection"> + <element name="title" type="text" selector=".block.crosssell .block-title"/> + <element name="products" type="block" selector=".block.crosssell .block-content"/> + <element name="productRowByName" type="block" selector="//li[@class='item product product-item'and .//a[@title='{{name}}']]" parameterized="true"/> + <element name="addToCart" type="block" selector="//button[@title='Add to Cart']"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml index ab82d9fdd93b..dcfb12fd4e96 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml @@ -26,10 +26,12 @@ parameterized="true"/> <element name="RemoveItem" type="button" selector="//table[@id='shopping-cart-table']//tbody//tr[contains(@class,'item-actions')]//a[contains(@class,'action-delete')]"/> + <element name="productName" type="text" selector="//tbody[@class='cart item']//strong[@class='product-item-name']"/> <element name="nthItemOption" type="block" selector=".item:nth-of-type({{numElement}}) .item-options" parameterized="true"/> <element name="nthEditButton" type="block" selector=".item:nth-of-type({{numElement}}) .action-edit" parameterized="true"/> <element name="nthBundleOptionName" type="text" selector=".product-item-details .item-options:nth-of-type({{numOption}}) dt" parameterized="true"/> <element name="productSubtotalByName" type="input" selector="//main//table[@id='shopping-cart-table']//tbody//tr[..//strong[contains(@class, 'product-item-name')]//a/text()='{{var1}}'][1]//td[contains(@class, 'subtotal')]//span[@class='price']" parameterized="true"/> <element name="updateShoppingCartButton" type="button" selector="#form-validate button[type='submit'].update" timeout="30"/> + <element name="qty" type="input" selector="//input[@data-cart-item-id='{{var}}'][@title='Qty']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml index a1a8d2ba3ead..3100fae3b119 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml @@ -9,7 +9,9 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="CheckoutCartSummarySection"> + <element name="elementPosition" type="text" selector=".data.table.totals > tbody tr:nth-of-type({{value}}) > th" parameterized="true"/> <element name="subtotal" type="text" selector="//*[@id='cart-totals']//tr[@class='totals sub']//td//span[@class='price']"/> + <element name="shippingMethodForm" type="text" selector="#co-shipping-method-form"/> <element name="shippingMethod" type="text" selector="//*[@id='cart-totals']//tr[@class='totals shipping excl']//th//span[@class='value']"/> <element name="shipping" type="text" selector="//*[@id='cart-totals']//tr[@class='totals shipping excl']//td//span[@class='price']"/> <element name="total" type="text" selector="//*[@id='cart-totals']//tr[@class='grand totals']//td//span[@class='price']"/> @@ -18,9 +20,12 @@ <element name="shippingHeading" type="button" selector="#block-shipping-heading"/> <element name="postcode" type="input" selector="input[name='postcode']" timeout="10"/> <element name="stateProvince" type="select" selector="select[name='region_id']" timeout="10"/> + <element name="stateProvinceInput" type="input" selector="input[name='region']"/> <element name="country" type="select" selector="select[name='country_id']" timeout="10"/> <element name="countryParameterized" type="select" selector="select[name='country_id'] > option:nth-child({{var}})" timeout="10" parameterized="true"/> <element name="estimateShippingAndTax" type="text" selector="#block-shipping-heading" timeout="5"/> - <element name="flatRateShippingMethod" type="radio" selector="#s_method_flatrate_flatrate" timeout="30"/> + <element name="flatRateShippingMethod" type="input" selector="#s_method_flatrate_flatrate" timeout="30"/> + <element name="shippingMethodLabel" type="text" selector="#co-shipping-method-form dl dt span"/> + <element name="shippingMethodElementId" type="radio" selector="#s_method_{{carrierCode}}_{{methodCode}}" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutOrderSummarySection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutOrderSummarySection.xml index 3074f390306b..6e0032990175 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutOrderSummarySection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutOrderSummarySection.xml @@ -13,5 +13,9 @@ <element name="productItemName" type="text" selector=".product-item-name"/> <element name="productItemQty" type="text" selector=".value"/> <element name="productItemPrice" type="text" selector=".price"/> + <element name="orderNumber" type="text" selector="//div[@class='checkout-success']//a"/> + <element name="shippingAddress" type="textarea" selector="//*[@class='box box-address-shipping']//address"/> + <element name="billingAddress" type="textarea" selector="//*[@class='box box-address-billing']//address"/> + <element name="additionalAddress" type="text" selector=".block.block-addresses-list"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml index 259f672ae816..cbe71e9cffa6 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml @@ -13,6 +13,7 @@ <element name="availablePaymentSolutions" type="text" selector="#checkout-payment-method-load>div>div>div:nth-child(2)>div.payment-method-title.field.choice"/> <element name="notAvailablePaymentSolutions" type="text" selector="#checkout-payment-method-load>div>div>div.payment-method._active>div.payment-method-title.field.choice"/> <element name="billingNewAddressForm" type="text" selector="[data-form='billing-new-address']"/> + <element name="billingAddressNotSameCheckbox" type="checkbox" selector="#billing-address-same-as-shipping-checkmo"/> <element name="placeOrderDisabled" type="button" selector="#checkout-payment-method-load button.disabled"/> <element name="update" type="button" selector=".payment-method-billing-address .action.action-update"/> <element name="guestFirstName" type="input" selector=".billing-address-form input[name*='firstname']"/> @@ -22,12 +23,12 @@ <element name="guestRegion" type="select" selector=".billing-address-form select[name*='region_id']"/> <element name="guestPostcode" type="input" selector=".billing-address-form input[name*='postcode']"/> <element name="guestTelephone" type="input" selector=".billing-address-form input[name*='telephone']"/> - <element name="billingAddress" type="text" selector="div.billing-address-details"/> + <element name="billingAddress" type="text" selector=".payment-method._active div.billing-address-details"/> <element name="cartItems" type="text" selector="ol.minicart-items"/> <element name="cartItemsArea" type="button" selector="div.block.items-in-cart"/> - <element name="cartItemsAreaActive" type="textarea" selector="div.block.items-in-cart.active"/> + <element name="cartItemsAreaActive" type="textarea" selector="div.block.items-in-cart.active" timeout="30"/> <element name="checkMoneyOrderPayment" type="radio" selector="input#checkmo.radio" timeout="30"/> - <element name="placeOrder" type="button" selector="button.action.primary.checkout" timeout="30"/> + <element name="placeOrder" type="button" selector=".payment-method._active button.action.primary.checkout" timeout="30"/> <element name="paymentSectionTitle" type="text" selector="//*[@id='checkout-payment-method-load']//div[text()='Payment Method']" /> <element name="orderSummarySubtotal" type="text" selector="//tr[@class='totals sub']//span[@class='price']" /> <element name="orderSummaryShippingTotal" type="text" selector="//tr[@class='totals shipping excl']//span[@class='price']" /> @@ -40,12 +41,19 @@ <element name="shipToInformation" type="text" selector="//div[@class='ship-to']//div[@class='shipping-information-content']" /> <element name="shippingMethodInformation" type="text" selector="//div[@class='ship-via']//div[@class='shipping-information-content']" /> <element name="paymentMethodTitle" type="text" selector=".payment-method-title span" /> - <element name="productOptionsByProductItemPrice" type="text" selector="//div[@class='product-item-inner']//div[@class='subtotal']//span[@class='price'][contains(.,'{{var1}}')]//ancestor::div[@class='product-item-details']//div[@class='product options']" parameterized="true"/> - <element name="productOptionsActiveByProductItemPrice" type="text" selector="//div[@class='subtotal']//span[@class='price'][contains(.,'{{var1}}')]//ancestor::div[@class='product-item-details']//div[@class='product options active']" parameterized="true"/> + <element name="productOptionsByProductItemPrice" type="text" selector="//div[@class='product-item-inner']//div[@class='subtotal']//span[@class='price'][contains(.,'{{price}}')]//ancestor::div[@class='product-item-details']//div[@class='product options']" parameterized="true"/> + <element name="productOptionsActiveByProductItemPrice" type="text" selector="//div[@class='subtotal']//span[@class='price'][contains(.,'{{price}}')]//ancestor::div[@class='product-item-details']//div[@class='product options active']" parameterized="true"/> + <element name="productItemPriceByName" type="text" selector="//div[@class='product-item-details'][contains(., '{{ProductName}}')]//span[@class='price']" parameterized="true"/> <element name="tax" type="text" selector="[data-th='Tax'] span" timeout="30"/> <element name="taxPercentage" type="text" selector=".totals-tax-details .mark"/> <element name="orderSummaryTotalIncluding" type="text" selector="//tr[@class='grand totals incl']//span[@class='price']" /> <element name="orderSummaryTotalExcluding" type="text" selector="//tr[@class='grand totals excl']//span[@class='price']" /> + <element name="shippingAndBillingAddressSame" type="input" selector="#billing-address-same-as-shipping-braintree_cc_vault"/> + <element name="addressAction" type="button" selector="//span[text()='{{action}}']" parameterized="true"/> + <element name="addressBook" type="button" selector="//a[text()='Address Book']"/> + <element name="noQuotes" type="text" selector=".no-quotes-block"/> + <element name="paymentMethodByName" type="text" selector="//*[@id='checkout-payment-method-load']//*[contains(@class, 'payment-group')]//label[normalize-space(.)='{{var1}}']" parameterized="true"/> + <element name="billingAddressSelectShared" type="select" selector=".checkout-billing-address select[name='billing_address_id']"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentWithDisplayBillingAddressOnPaymentPageSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentWithDisplayBillingAddressOnPaymentPageSection.xml new file mode 100644 index 000000000000..42decd8d4322 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentWithDisplayBillingAddressOnPaymentPageSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="CheckoutPaymentWithDisplayBillingAddressOnPaymentPageSection"> + <element name="billingAddressDetails" type="text" selector="div.billing-address-details"/> + </section> +</sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingGuestInfoSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingGuestInfoSection.xml index ad2a43eb90c8..b19e365f2e32 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingGuestInfoSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingGuestInfoSection.xml @@ -9,15 +9,23 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="CheckoutShippingGuestInfoSection"> - <element name="email" type="input" selector="#customer-email"/> + <element name="email" type="input" selector="#checkout-customer-email"/> <element name="firstName" type="input" selector="input[name=firstname]"/> <element name="lastName" type="input" selector="input[name=lastname]"/> <element name="street" type="input" selector="input[name='street[0]']"/> + <element name="street2" type="input" selector="input[name='street[1]']"/> <element name="city" type="input" selector="input[name=city]"/> <element name="region" type="select" selector="select[name=region_id]"/> + <element name="regionInput" type="input" selector="input[name=region]"/> <element name="postcode" type="input" selector="input[name=postcode]"/> + <element name="country" type="select" selector="select[name=country_id]"/> + <element name="company" type="input" selector="input[name=company]"/> <element name="telephone" type="input" selector="input[name=telephone]"/> <element name="next" type="button" selector="button.button.action.continue.primary" timeout="30"/> <element name="firstShippingMethod" type="radio" selector=".row:nth-of-type(1) .col-method .radio"/> + + <!--Order Summary--> + <element name="itemInCart" type="button" selector="//div[@class='title']"/> + <element name="productName" type="text" selector="//strong[@class='product-item-name']"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingMethodsSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingMethodsSection.xml index ceb4505c7969..77d903eab393 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingMethodsSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingMethodsSection.xml @@ -13,8 +13,10 @@ <element name="firstShippingMethod" type="radio" selector="//*[@id='checkout-shipping-method-load']//input[@class='radio']"/> <element name="shippingMethodRow" type="text" selector=".form.methods-shipping table tbody tr"/> <element name="checkShippingMethodByName" type="radio" selector="//div[@id='checkout-shipping-method-load']//td[contains(., '{{var1}}')]/..//input" parameterized="true"/> + <element name="shippingMethodFlatRate" type="radio" selector="#checkout-shipping-method-load input[value='flatrate_flatrate']"/> <element name="shippingMethodRowByName" type="text" selector="//div[@id='checkout-shipping-method-load']//td[contains(., '{{var1}}')]/.." parameterized="true"/> - <element name="shipHereButton" type="button" selector="//button[contains(@class, 'action-select-shipping-item')]/parent::div/following-sibling::div/button[contains(@class, 'action-select-shipping-item')]"/> + <element name="shipHereButton" type="button" selector="//div/following-sibling::div/button[contains(@class, 'action-select-shipping-item')]"/> <element name="shippingMethodLoader" type="button" selector="//div[contains(@class, 'checkout-shipping-method')]/following-sibling::div[contains(@class, 'loading-mask')]"/> + <element name="freeShippingShippingMethod" type="input" selector="#s_method_freeshipping_freeshipping" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml index 494a365ffd50..9676f16f3a5c 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml @@ -15,7 +15,7 @@ <element name="editAddressButton" type="button" selector=".action-edit-address" timeout="30"/> <element name="addressDropdown" type="select" selector="[name=billing_address_id]"/> <element name="newAddressButton" type="button" selector=".action-show-popup" timeout="30"/> - <element name="email" type="input" selector="#customer-email"/> + <element name="email" type="input" selector="#checkout-customer-email"/> <element name="firstName" type="input" selector="input[name=firstname]"/> <element name="lastName" type="input" selector="input[name=lastname]"/> <element name="company" type="input" selector="input[name=company]"/> @@ -27,10 +27,14 @@ <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']"/> <element name="defaultShipping" type="button" selector=".billing-address-details"/> + <element name="state" type="button" selector="//*[text()='Alabama']"/> <element name="stateInput" type="input" selector="input[name=region]"/> + <element name="regionOptions" type="select" selector="select[name=region_id] option"/> + <element name="editActiveAddress" type="button" selector="//div[@class='shipping-address-item selected-item']//span[text()='Edit']" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessMainSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessMainSection.xml index 34819f641cbc..bc65f8a2c081 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessMainSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessMainSection.xml @@ -16,6 +16,7 @@ <element name="orderLink" type="text" selector="a[href*=order_id].order-number" timeout="30"/> <element name="orderNumberText" type="text" selector=".checkout-success > p:nth-child(1)"/> <element name="continueShoppingButton" type="button" selector=".action.primary.continue" timeout="30"/> + <element name="createAnAccount" type="button" selector="input[value='Create an Account']" timeout="30"/> <element name="printLink" type="button" selector=".print" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessRegisterSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessRegisterSection.xml index d0ef8d347efb..0d692e4ab143 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessRegisterSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessRegisterSection.xml @@ -12,5 +12,6 @@ <element name="registerMessage" type="text" selector="#registration p:nth-child(1)"/> <element name="customerEmail" type="text" selector="#registration p:nth-child(2)"/> <element name="createAccountButton" type="button" selector="#registration form input[type='submit']" timeout="30"/> + <element name="orderNumber" type="text" selector="//p[text()='Your order # is: ']//span"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/IdentityOfDefaultBillingAndShippingAddressSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/IdentityOfDefaultBillingAndShippingAddressSection.xml index 89b3a25b45e3..2039128ac2de 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/IdentityOfDefaultBillingAndShippingAddressSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/IdentityOfDefaultBillingAndShippingAddressSection.xml @@ -7,8 +7,7 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> - + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="ShipmentFormSection"> <element name="shippingAddress" type="textarea" selector="//*[@class='box box-billing-address']//address"/> <element name="billingAddress" type="textarea" selector="//*[@class='box box-shipping-address']//address"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutAddressPopupSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutAddressPopupSection.xml new file mode 100644 index 000000000000..6a27915768dd --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutAddressPopupSection.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="StorefrontCheckoutAddressPopupSection"> + <element name="newAddressModalPopup" type="block" selector=".modal-popup.modal-slide._inner-scroll"/> + <element name="closeAddressModalPopup" type="button" selector=".action-hide-popup"/> + </section> +</sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutCheckoutCustomerLoginSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutCheckoutCustomerLoginSection.xml new file mode 100644 index 000000000000..9772fa1993ac --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutCheckoutCustomerLoginSection.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCheckoutCheckoutCustomerLoginSection"> + <element name="email" type="input" selector="form[data-role='email-with-possible-login'] input[name='username']" /> + <element name="emailNoteMessage" type="text" selector="//form[@data-role='email-with-possible-login']//div[input[@name='username']]//*[contains(@class, 'note')]" /> + <element name="emailErrorMessage" type="text" selector="//form[@data-role='email-with-possible-login']//div[input[@name='username']]//*[@id='checkout-customer-email-error']" /> + <element name="emailTooltipButton" type="button" selector="//form[@data-role='email-with-possible-login']//div[input[@name='username']]//*[contains(@class, 'action-help')]" /> + <element name="emailTooltipContent" type="text" selector="//form[@data-role='email-with-possible-login']//div[input[@name='username']]//*[contains(@class, 'field-tooltip-content')]" /> + <element name="password" type="input" selector="form[data-role='email-with-possible-login'] input[name='password']" /> + <element name="passwordNoteMessage" type="text" selector="//form[@data-role='email-with-possible-login']//div[input[@name='password']]//*[contains(@class, 'note')]" /> + <element name="submit" type="button" selector="form[data-role='email-with-possible-login'] button[type='submit']" /> + <element name="forgotPassword" type="button" selector="form[data-role='email-with-possible-login'] a.remind" /> + </section> +</sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutPaymentMethodSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutPaymentMethodSection.xml new file mode 100644 index 000000000000..9d9a96d2ea5e --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCheckoutPaymentMethodSection.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="StorefrontCheckoutPaymentMethodSection"> + <element name="billingAddress" type="text" selector=".checkout-billing-address"/> + <element name="checkPaymentMethodByName" type="radio" selector="//div[@id='checkout-payment-method-load']//div[@class='payment-method']//label//span[contains(., '{{methodName}}')]/../..//input" parameterized="true"/> + <element name="billingAddressSameAsShipping" type="checkbox" selector=".payment-method._active [name='billing-address-same-as-shipping']"/> + <element name="billingAddressSameAsShippingShared" type="checkbox" selector="#billing-address-same-as-shipping-shared"/> + </section> +</sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMessagesSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMessagesSection.xml index e70ff2b44519..8bd6a59f5633 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMessagesSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMessagesSection.xml @@ -11,7 +11,7 @@ <section name="StorefrontMessagesSection"> <!-- @TODO: Use general message selector after MQE-694 is fixed --> <element name="messageProductAddedToCart" type="text" - selector="//main//div[contains(@class, 'messages')]//div[contains(@class, 'message')]/div[contains(text(), 'You added {{var1}} to your shopping cart.')]" + selector="//main//div[contains(@class, 'messages')]//div[contains(@class, 'message')]/div[contains(text(), 'You added {{var1}} to your') and a[contains(., 'shopping cart')]]" parameterized="true" /> </section> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml index 1c46b8677503..38c88bf4f80b 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml @@ -13,6 +13,7 @@ <element name="productLinkByName" type="button" selector="//header//ol[@id='mini-cart']//div[@class='product-item-details']//a[contains(text(), '{{var1}}')]" parameterized="true"/> <element name="productPriceByName" type="text" selector="//header//ol[@id='mini-cart']//div[@class='product-item-details'][.//a[contains(text(), '{{var1}}')]]//span[@class='price']" parameterized="true"/> <element name="productImageByName" type="text" selector="//header//ol[@id='mini-cart']//span[@class='product-image-container']//img[@alt='{{var1}}']" parameterized="true"/> + <element name="productName" type="text" selector=".product-item-name"/> <element name="productOptionsDetailsByName" type="button" selector="//header//ol[@id='mini-cart']//div[@class='product-item-details'][.//a[contains(text(), '{{var1}}')]]//span[.='See Details']" parameterized="true"/> <element name="productOptionByNameAndAttribute" type="text" selector="//header//ol[@id='mini-cart']//div[@class='product-item-details'][.//a[contains(text(), '{{var1}}')]]//dt[@class='label' and .='{{var2}}']/following-sibling::dd[@class='values']//span" parameterized="true"/> <element name="showCart" type="button" selector="a.showcart"/> @@ -22,8 +23,15 @@ <element name="viewAndEditCart" type="button" selector=".action.viewcart" timeout="30"/> <element name="miniCartItemsText" type="text" selector=".minicart-items"/> <element name="deleteMiniCartItem" type="button" selector=".action.delete" timeout="30"/> + <element name="deleteMiniCartItemByName" type="button" selector="//ol[@id='mini-cart']//div[contains(., '{{var}}')]//a[contains(@class, 'delete')]" parameterized="true"/> <element name="miniCartSubtotalField" type="text" selector=".block-minicart .amount span.price"/> + <element name="itemQuantityBySku" type="input" selector="#minicart-content-wrapper input[data-cart-item-id='{{productSku}}']" parameterized="true"/> + <element name="itemQuantityUpdateBySku" type="button" selector="//div[@id='minicart-content-wrapper']//input[@data-cart-item-id='{{productSku}}']/../button[contains(@class, 'update-cart-item')]" parameterized="true"/> <element name="itemQuantity" type="input" selector="//a[text()='{{productName}}']/../..//input[contains(@class,'cart-item-qty')]" parameterized="true"/> <element name="itemQuantityUpdate" type="button" selector="//a[text()='{{productName}}']/../..//span[text()='Update']" parameterized="true"/> + <element name="itemDiscount" type="text" selector="//tr[@class='totals']//td[@class='amount']/span"/> + <element name="subtotal" type="text" selector="//tr[@class='totals sub']//td[@class='amount']/span"/> + <element name="emptyCart" type="text" selector=".counter.qty.empty"/> + <element name="minicartContent" type="block" selector="#minicart-content-wrapper"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest.xml index a41b1afc7436..e19627e7435d 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest.xml @@ -55,8 +55,8 @@ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask2"/> <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton"/> <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> - <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoadedTest3"/> - + <!-- Checkout select Check/Money Order payment --> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> <!--Click Place Order button--> <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="seeSuccessTitle"/> @@ -81,7 +81,8 @@ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask3"/> <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton2"/> <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext2"/> - <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoadedTest4"/> + <!-- Checkout select Check/Money Order payment --> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment2"/> <!--Click Place Order button--> <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder2"/> @@ -105,7 +106,9 @@ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask4"/> <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton3"/> <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext3"/> - <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoadedTest5"/> + + <!-- Checkout select Check/Money Order payment --> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment3"/> <!--Click Place Order button--> <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder3"/> @@ -165,6 +168,9 @@ <argument name="customerAddressVar" value="CustomerAddressSimple" /> </actionGroup> + <!-- Checkout select Check/Money Order payment --> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> + <!--Click Place Order button--> <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="waitForLoadSuccessPage"/> @@ -198,6 +204,9 @@ <argument name="customerAddressVar" value="CustomerAddressSimple" /> </actionGroup> + <!-- Checkout select Check/Money Order payment --> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment2"/> + <!--Click Place Order button--> <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder2"/> <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="waitForLoadSuccessPage2"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/CheckNotVisibleProductInMinicartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/CheckNotVisibleProductInMinicartTest.xml new file mode 100644 index 000000000000..4b4ca1935fd7 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/CheckNotVisibleProductInMinicartTest.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="CheckNotVisibleProductInMinicartTest"> + <annotations> + <features value="Checkout"/> + <stories value="MAGETWO-96422: Hidden Products are absent in Storefront Mini-Cart" /> + <title value="Not visible individually product in mini-shopping cart."/> + <description value="To be sure that product in mini-shopping cart remains visible after admin makes it not visible individually"/> + <severity value="MAJOR"/> + <group value="checkout"/> + </annotations> + + <!--Create simple product1 and simple product2--> + <createData entity="SimpleTwo" stepKey="createSimpleProduct1"/> + <createData entity="SimpleTwo" stepKey="createSimpleProduct2"/> + + <!--Go to simple product1 page--> + <amOnPage url="$$createSimpleProduct1.custom_attributes[url_key]$$.html" stepKey="navigateToSimpleProductPage1"/> + <waitForPageLoad stepKey="waitForCatalogPageLoad"/> + + <!--Add simple product1 to Shopping Cart--> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage1"> + <argument name="productName" value="$$createSimpleProduct1.name$$"/> + </actionGroup> + + <!--Check simple product1 in minicart--> + <comment userInput="Check simple product 1 in minicart" stepKey="commentCheckSimpleProduct1InMinicart" after="addToCartFromStorefrontProductPage1"/> + <actionGroup ref="assertOneProductNameInMiniCart" stepKey="assertProduct1NameInMiniCart"> + <argument name="productName" value="$$createSimpleProduct1.name$$"/> + </actionGroup> + + <!--Make simple product1 not visible individually--> + <updateData entity="SetProductVisibilityHidden" createDataKey="createSimpleProduct1" stepKey="updateSimpleProduct1"> + <requiredEntity createDataKey="createSimpleProduct1"/> + </updateData> + + <!--Go to simple product2 page--> + <amOnPage url="$$createSimpleProduct2.custom_attributes[url_key]$$.html" stepKey="navigateToSimpleProductPage2"/> + <waitForPageLoad stepKey="waitForCatalogPageLoad2"/> + + <!--Add simple product2 to Shopping Cart for updating cart items--> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage2"> + <argument name="productName" value="$$createSimpleProduct2.name$$"/> + </actionGroup> + + <!--Check simple product1 in minicart--> + <comment userInput="Check hidden simple product 1 in minicart" stepKey="commentCheckHiddenSimpleProduct1InMinicart" after="addToCartFromStorefrontProductPage2"/> + <actionGroup ref="assertOneProductNameInMiniCart" stepKey="assertHiddenProduct1NameInMiniCart"> + <argument name="productName" value="$$createSimpleProduct1.name$$"/> + </actionGroup> + + <!--Check simple product2 in minicart--> + <comment userInput="Check hidden simple product 2 in minicart" stepKey="commentCheckSimpleProduct2InMinicart" after="addToCartFromStorefrontProductPage2"/> + <actionGroup ref="assertOneProductNameInMiniCart" stepKey="assertProduct2NameInMiniCart"> + <argument name="productName" value="$$createSimpleProduct2.name$$"/> + </actionGroup> + + <!--Delete simple product1 and simple product2--> + <deleteData createDataKey="createSimpleProduct1" stepKey="deleteProduct1"/> + <deleteData createDataKey="createSimpleProduct2" stepKey="deleteProduct2"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/CheckoutSpecificDestinationsTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/CheckoutSpecificDestinationsTest.xml index 269ca94b3f77..f3807388399b 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/CheckoutSpecificDestinationsTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/CheckoutSpecificDestinationsTest.xml @@ -1,86 +1,86 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> - <test name="CheckoutSpecificDestinationsTest"> - <annotations> - <title value="Check that top destinations can be removed after a selection was previously saved"/> - <stories value="MAGETWO-91511: Top destinations cannot be removed after a selection was previously saved"/> - <description value="Check that top destinations can be removed after a selection was previously saved"/> - <features value="Checkout"/> - <severity value="AVERAGE"/> - <testCaseId value="MAGETWO-94195"/> - <group value="Checkout"/> - </annotations> - - <before> - <createData entity="_defaultCategory" stepKey="defaultCategory"/> - <createData entity="_defaultProduct" stepKey="simpleProduct"> - <requiredEntity createDataKey="defaultCategory"/> - </createData> - - <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> - </before> - - <!--Go to configuration general page--> - <actionGroup ref="NavigateToConfigurationGeneralPage" stepKey="navigateToConfigurationGeneralPage"/> - - <!--Open country options section--> - <conditionalClick selector="{{CountryOptionsSection.countryOptions}}" dependentSelector="{{CountryOptionsSection.countryOptionsOpen}}" visible="false" stepKey="clickOnStoreInformation"/> - - <!--Select top destinations country--> - <actionGroup ref="SelectTopDestinationsCountry" stepKey="selectTopDestinationsCountry"> - <argument name="countries" value="Countries"/> - </actionGroup> - - <!--Go to product page--> - <amOnPage url="{{StorefrontProductPage.url($$simpleProduct.name$$)}}" stepKey="amOnStorefrontProductPage"/> - <waitForPageLoad stepKey="waitForProductPageLoad"/> - - <!--Add product to cart--> - <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCart"> - <argument name="productName" value="$$simpleProduct.name$$"/> - </actionGroup> - - <!--Go to shopping cart--> - <amOnPage url="{{CheckoutCartPage.url}}" stepKey="amOnPageShoppingCart"/> - - <!--Verify country options in checkout top destination section--> - <actionGroup ref="VerifyTopDestinationsCountry" stepKey="verifyTopDestinationsCountry"> - <argument name="country" value="Bahamas"/> - <argument name="placeNumber" value="2"/> - </actionGroup> - - <!--Go to configuration general page--> - <actionGroup ref="NavigateToConfigurationGeneralPage" stepKey="navigateToConfigurationGeneralPage2"/> - - <!--Open country options section--> - <conditionalClick selector="{{CountryOptionsSection.countryOptions}}" dependentSelector="{{CountryOptionsSection.countryOptionsOpen}}" visible="false" stepKey="clickOnStoreInformation2"/> - - <!--Deselect top destinations country--> - <actionGroup ref="UnSelectTopDestinationsCountry" stepKey="unSelectTopDestinationsCountry"> - <argument name="countries" value="Countries"/> - </actionGroup> - - <!--Go to shopping cart--> - <amOnPage url="{{CheckoutCartPage.url}}" stepKey="amOnPageShoppingCart2"/> - - <!--Verify country options is shown by default--> - <actionGroup ref="VerifyTopDestinationsCountry" stepKey="verifyTopDestinationsCountry2"> - <argument name="country" value="Afghanistan"/> - <argument name="placeNumber" value="2"/> - </actionGroup> - - <after> - <actionGroup ref="logout" stepKey="logout"/> - - <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> - <deleteData createDataKey="defaultCategory" stepKey="deleteCategory"/> - </after> - </test> -</tests> \ No newline at end of file +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="CheckoutSpecificDestinationsTest"> + <annotations> + <title value="Check that top destinations can be removed after a selection was previously saved"/> + <stories value="MAGETWO-91511: Top destinations cannot be removed after a selection was previously saved"/> + <description value="Check that top destinations can be removed after a selection was previously saved"/> + <features value="Checkout"/> + <severity value="AVERAGE"/> + <testCaseId value="MAGETWO-94195"/> + <group value="Checkout"/> + </annotations> + + <before> + <createData entity="_defaultCategory" stepKey="defaultCategory"/> + <createData entity="_defaultProduct" stepKey="simpleProduct"> + <requiredEntity createDataKey="defaultCategory"/> + </createData> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <!--Go to configuration general page--> + <actionGroup ref="NavigateToConfigurationGeneralPage" stepKey="navigateToConfigurationGeneralPage"/> + + <!--Open country options section--> + <conditionalClick selector="{{CountryOptionsSection.countryOptions}}" dependentSelector="{{CountryOptionsSection.countryOptionsOpen}}" visible="false" stepKey="clickOnStoreInformation"/> + + <!--Select top destinations country--> + <actionGroup ref="SelectTopDestinationsCountry" stepKey="selectTopDestinationsCountry"> + <argument name="countries" value="Countries"/> + </actionGroup> + + <!--Go to product page--> + <amOnPage url="{{StorefrontProductPage.url($$simpleProduct.name$$)}}" stepKey="amOnStorefrontProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + + <!--Add product to cart--> + <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCart"> + <argument name="productName" value="$$simpleProduct.name$$"/> + </actionGroup> + + <!--Go to shopping cart--> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="amOnPageShoppingCart"/> + + <!--Verify country options in checkout top destination section--> + <actionGroup ref="VerifyTopDestinationsCountry" stepKey="verifyTopDestinationsCountry"> + <argument name="country" value="Bahamas"/> + <argument name="placeNumber" value="2"/> + </actionGroup> + + <!--Go to configuration general page--> + <actionGroup ref="NavigateToConfigurationGeneralPage" stepKey="navigateToConfigurationGeneralPage2"/> + + <!--Open country options section--> + <conditionalClick selector="{{CountryOptionsSection.countryOptions}}" dependentSelector="{{CountryOptionsSection.countryOptionsOpen}}" visible="false" stepKey="clickOnStoreInformation2"/> + + <!--Deselect top destinations country--> + <actionGroup ref="UnSelectTopDestinationsCountry" stepKey="unSelectTopDestinationsCountry"> + <argument name="countries" value="Countries"/> + </actionGroup> + + <!--Go to shopping cart--> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="amOnPageShoppingCart2"/> + + <!--Verify country options is shown by default--> + <actionGroup ref="VerifyTopDestinationsCountry" stepKey="verifyTopDestinationsCountry2"> + <argument name="country" value="Afghanistan"/> + <argument name="placeNumber" value="2"/> + </actionGroup> + + <after> + <actionGroup ref="logout" stepKey="logout"/> + + <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="defaultCategory" stepKey="deleteCategory"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml new file mode 100644 index 000000000000..166f5022d5ae --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="DefaultBillingAddressShouldBeCheckedOnPaymentPageTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout via the Storefront"/> + <title value="The default billing address should be used on checkout"/> + <description value="Default billing address should be preselected on payments page on checkout if it exist"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-98892"/> + <useCaseId value="MAGETWO-70996"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <!--Go to Storefront as Customer--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> + <argument name="Customer" value="$$createCustomer$$" /> + </actionGroup> + </before> + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <!--Logout from customer account--> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + </after> + <!-- Add simple product to cart and go to checkout--> + <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <!-- Click "+ New Address" and Fill new address--> + <click selector="{{CheckoutShippingSection.newAddressButton}}" stepKey="addAddress"/> + <actionGroup ref="LoggedInCheckoutWithOneAddressFieldWithoutStateField" stepKey="changeAddress"> + <argument name="Address" value="UK_Not_Default_Address"/> + <argument name="classPrefix" value="._show"/> + </actionGroup> + <!--Click "Save Addresses" --> + <click selector="{{CheckoutShippingSection.saveAddress}}" stepKey="saveAddress"/> + <waitForPageLoad stepKey="waitForAddressSaved"/> + <dontSeeElement selector="{{StorefrontCheckoutAddressPopupSection.newAddressModalPopup}}" stepKey="dontSeeModalPopup"/> + <!--Select Shipping Rate "Flat Rate" and click "Next" button--> + <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShipping"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask2"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> + <!--Verify that "My billing and shipping address are the same" is unchecked and billing address is preselected--> + <dontSeeCheckboxIsChecked selector="{{CheckoutPaymentSection.billingAddressNotSameCheckbox}}" stepKey="shippingAndBillingAddressIsSameUnchecked"/> + <see selector="{{CheckoutPaymentSection.billingAddress}}" userInput="{{US_Address_TX.street[0]}}" stepKey="assertBillingAddress"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml new file mode 100644 index 000000000000..20015f76e08e --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="EditShippingAddressOnePageCheckoutTest"> + <annotations> + <features value="Checkout"/> + <stories value="Edit Shipping Address"/> + <title value="Edit Shipping Address on Checkout Page."/> + <description value="Edit Shipping Address on Checkout Page."/> + <severity value="MAJOR"/> + <testCaseId value="MC-14680"/> + <group value="shoppingCart"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="simpleProductDefault" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer_NY" stepKey="createCustomer"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + + <!-- Go to Frontend as Customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> + <argument name="Customer" value="$$createCustomer$$" /> + </actionGroup> + + <!-- Add product to cart --> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> + <actionGroup ref="StorefrontAddSimpleProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + + <!-- Go to checkout page --> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="customerGoToCheckoutFromMinicart" /> + + <!-- *New Address* button on 1st checkout step --> + <click selector="{{CheckoutShippingSection.newAddressButton}}" stepKey="addNewAddress"/> + + <!--Fill in required fields and click *Save address* button--> + <actionGroup ref="FillShippingAddressOneStreetActionGroup" stepKey="changeAddress"> + <argument name="address" value="UK_Not_Default_Address"/> + </actionGroup> + <click selector="{{CheckoutShippingSection.saveAddress}}" stepKey="saveNewAddress"/> + + <!--Select Shipping Rate--> + <scrollTo selector="{{CheckoutShippingMethodsSection.next}}" stepKey="scrollToShippingRate"/> + <click selector="{{CheckoutShippingMethodsSection.shippingMethodFlatRate}}" stepKey="selectShippingMethod"/> + + <!-- Click *Edit* button for the new address --> + <click selector="{{CheckoutShippingSection.editActiveAddress}}" stepKey="editNewAddress"/> + + <!--Remove values from required fields and click *Cancel* button --> + <actionGroup ref="ClearShippingAddressActionGroup" stepKey="clearRequiredFields"/> + <click selector="{{CheckoutShippingSection.cancelChangeAddress}}" stepKey="cancelEditAddress"/> + + <!-- Go to *Next* --> + <scrollTo selector="{{CheckoutShippingMethodsSection.next}}" stepKey="scrollToButtonNext"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="goNext"/> + + <!-- Select payment solution --> + <checkOption selector="{{CheckoutPaymentSection.billingAddressNotSameCheckbox}}" stepKey="selectPaymentSolution" /> + + <!--Refresh Page and Place Order--> + <reloadPage stepKey="reloadPage"/> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="placeOrder"/> + </test> + </tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml index 00d80cc2a94d..5335ec2ad775 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml @@ -96,14 +96,10 @@ <comment userInput="Check cart information" stepKey="commentCheckCartInformation" after="cartMinicartAssertSimpleProduct2PageImageNotDefault" /> <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="cartOpenCart" after="commentCheckCartInformation"/> <actionGroup ref="StorefrontCheckCartActionGroup" stepKey="cartAssertCart" after="cartOpenCart"> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="subtotal" value="E2EB2CQuote.subtotal"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="shipping" value="E2EB2CQuote.shipping"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="shippingMethod" value="E2EB2CQuote.shippingMethod"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="total" value="E2EB2CQuote.total"/> + <argument name="subtotal" value="480.00"/> + <argument name="shipping" value="15.00"/> + <argument name="shippingMethod" value="Flat Rate - Fixed"/> + <argument name="total" value="495.00"/> </actionGroup> <!-- Check simple product 1 in cart --> @@ -157,14 +153,10 @@ <!-- Check order summary in checkout --> <comment userInput="Check order summary in checkout" stepKey="commentCheckOrderSummaryInCheckout" after="guestCheckoutFillingShippingSection" /> <actionGroup ref="CheckOrderSummaryInCheckoutActionGroup" stepKey="guestCheckoutCheckOrderSummary" after="commentCheckOrderSummaryInCheckout"> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="subtotal" value="E2EB2CQuote.subtotal"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="shippingTotal" value="E2EB2CQuote.shipping"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="shippingMethod" value="E2EB2CQuote.shippingMethod"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="total" value="E2EB2CQuote.total"/> + <argument name="subtotal" value="480.00"/> + <argument name="shippingTotal" value="15.00"/> + <argument name="shippingMethod" value="Flat Rate - Fixed"/> + <argument name="total" value="495.00"/> </actionGroup> <!-- Check ship to information in checkout --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml index 05a6939941f3..65627787e2a0 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml @@ -96,14 +96,10 @@ <comment userInput="Check cart information" stepKey="commentCheckCartInformation" after="cartMinicartAssertSimpleProduct2PageImageNotDefault" /> <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="cartOpenCart" after="commentCheckCartInformation"/> <actionGroup ref="StorefrontCheckCartActionGroup" stepKey="cartAssertCart" after="cartOpenCart"> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="subtotal" value="E2EB2CQuote.subtotal"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="shipping" value="E2EB2CQuote.shipping"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="shippingMethod" value="E2EB2CQuote.shippingMethod"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="total" value="E2EB2CQuote.total"/> + <argument name="subtotal" value="480.00"/> + <argument name="shipping" value="15.00"/> + <argument name="shippingMethod" value="Flat Rate - Fixed"/> + <argument name="total" value="495.00"/> </actionGroup> <!-- Check simple product 1 in cart --> @@ -157,14 +153,10 @@ <!-- Check order summary in checkout --> <comment userInput="Check order summary in checkout" stepKey="commentCheckOrderSummaryInCheckout" after="checkoutFillingShippingSection" /> <actionGroup ref="CheckOrderSummaryInCheckoutActionGroup" stepKey="checkoutCheckOrderSummary" after="commentCheckOrderSummaryInCheckout"> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="subtotal" value="E2EB2CQuote.subtotal"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="shippingTotal" value="E2EB2CQuote.shipping"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="shippingMethod" value="E2EB2CQuote.shippingMethod"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="total" value="E2EB2CQuote.total"/> + <argument name="subtotal" value="480.00"/> + <argument name="shippingTotal" value="15.00"/> + <argument name="shippingMethod" value="Flat Rate - Fixed"/> + <argument name="total" value="495.00"/> </actionGroup> <!-- Check ship to information in checkout --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/IdentityOfDefaultBillingAndShippingAddressTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/IdentityOfDefaultBillingAndShippingAddressTest.xml index 9664ec47420c..89028e146c35 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/IdentityOfDefaultBillingAndShippingAddressTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/IdentityOfDefaultBillingAndShippingAddressTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="IdentityOfDefaultBillingAndShippingAddressTest"> <annotations> <features value="Customer"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontCheckCustomerInfoCreatedByGuestTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontCheckCustomerInfoCreatedByGuestTest.xml new file mode 100644 index 000000000000..693c05684f29 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontCheckCustomerInfoCreatedByGuestTest.xml @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StoreFrontCheckCustomerInfoCreatedByGuestTest"> + <annotations> + <features value="Checkout"/> + <stories value="Check customer information created by guest"/> + <title value="Check Customer Information Created By Guest"/> + <description value="Check customer information after placing the order as the guest who created an account"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-95932"/> + <useCaseId value="MAGETWO-95820"/> + <group value="checkout"/> + </annotations> + + <before> + <createData entity="_defaultCategory" stepKey="category"/> + <createData entity="_defaultProduct" stepKey="product"> + <requiredEntity createDataKey="category"/> + </createData> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + + <after> + <deleteData createDataKey="product" stepKey="deleteProduct" /> + <deleteData createDataKey="category" stepKey="deleteCategory" /> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + + <amOnPage url="$$product.name$$.html" stepKey="navigateToProductPage"/> + <waitForPageLoad stepKey="waitForProductPage"/> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$$product.name$$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShippingSection"> + <argument name="customerVar" value="CustomerEntityOne"/> + <argument name="customerAddressVar" value="CustomerAddressSimple"/> + </actionGroup> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="placeOrder"> + <argument name="orderNumberMessage" value="CONST.successGuestCheckoutOrderNumberMessage"/> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage" /> + </actionGroup> + <grabTextFrom selector="{{CheckoutSuccessRegisterSection.orderNumber}}" stepKey="grabOrderNumber"/> + <click selector="{{CheckoutSuccessRegisterSection.createAccountButton}}" stepKey="clickCreateAccountButton"/> + <fillField selector="{{StorefrontCustomerCreateFormSection.passwordField}}" userInput="{{CustomerEntityOne.password}}" stepKey="TypePassword"/> + <fillField selector="{{StorefrontCustomerCreateFormSection.confirmPasswordField}}" userInput="{{CustomerEntityOne.password}}" stepKey="TypeConfirmationPassword"/> + <click selector="{{StorefrontCustomerCreateFormSection.createAccountButton}}" stepKey="clickOnCreateAccount"/> + <see userInput="Thank you for registering" stepKey="verifyAccountCreated"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdmin"/> + <amOnPage url="{{AdminOrderPage.url({$grabOrderNumber})}}" stepKey="navigateToOrderPage"/> + <waitForPageLoad stepKey="waitForCreatedOrderPage"/> + <see stepKey="seeCustomerName" userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminShipmentOrderInformationSection.customerName}}"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml new file mode 100644 index 000000000000..330a026bb942 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml @@ -0,0 +1,97 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest"> + <annotations> + <title value="Checkout Free Shipping Recalculation after Coupon Code Added"/> + <stories value="Checkout Free Shipping Recalculation after Coupon Code Added"/> + <description value="User should be able to do checkout free shipping recalculation after adding coupon code"/> + <features value="Checkout"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-96537"/> + <useCaseId value="MAGETWO-96431"/> + <group value="Checkout"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="Simple_US_Customer" stepKey="createSimpleUsCustomer"> + <field key="group_id">1</field> + </createData> + <createData entity="_defaultCategory" stepKey="defaultCategory"/> + <createData entity="_defaultProduct" stepKey="simpleProduct"> + <field key="price">90</field> + <requiredEntity createDataKey="defaultCategory"/> + </createData> + <!--It is default for FlatRate--> + <createData entity="FlatRateShippingMethodConfig" stepKey="enableFlatRate"/> + <createData entity="FreeShippingMethodsSettingConfig" stepKey="freeShippingMethodsSettingConfig"/> + <createData entity="MinimumOrderAmount90" stepKey="minimumOrderAmount90"/> + <magentoCLI command="cache:flush" stepKey="flushCache1"/> + <actionGroup ref="AdminCreateCartPriceRuleWithCouponCode" stepKey="createCartPriceRule"> + <argument name="ruleName" value="CatPriceRule"/> + <argument name="couponCode" value="CatPriceRule.coupon_code"/> + </actionGroup> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStoreFront"> + <argument name="Customer" value="$$createSimpleUsCustomer$$"/> + </actionGroup> + <amOnPage url="$$simpleProduct.name$$.html" stepKey="navigateToProductPage"/> + <waitForPageLoad stepKey="waitForProductPage"/> + </before> + <after> + <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="defaultCategory" stepKey="deleteCategory"/> + <createData entity="DefaultShippingMethodsConfig" stepKey="defaultShippingMethodsConfig"/> + <createData entity="DefaultMinimumOrderAmount" stepKey="defaultMinimumOrderAmount"/> + <deleteData createDataKey="createSimpleUsCustomer" stepKey="deleteCustomer"/> + <magentoCLI command="cache:flush" stepKey="flushCache2"/> + <actionGroup ref="DeleteCartPriceRuleByName" stepKey="deleteCartPriceRule"> + <argument name="ruleName" value="{{CatPriceRule.name}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <actionGroup ref="ApplyCartRuleOnStorefrontActionGroup" stepKey="applyCartRule"> + <argument name="product" value="$$simpleProduct$$"/> + <argument name="couponCode" value="{{CatPriceRule.coupon_code}}"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart1"/> + <waitForPageLoad stepKey="waitForpageLoad1"/> + <dontSee selector="{{CheckoutShippingMethodsSection.shippingMethodRowByName('Free')}}" stepKey="dontSeeFreeShipping"/> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToShoppingCartPage"/> + <waitForPageLoad stepKey="waitForShoppingCartPage"/> + <conditionalClick selector="{{DiscountSection.DiscountTab}}" dependentSelector="{{DiscountSection.CouponInput}}" visible="false" stepKey="clickIfDiscountTabClosed1"/> + <waitForPageLoad stepKey="waitForCouponTabOpen1"/> + <click selector="{{DiscountSection.CancelCoupon}}" stepKey="cancelCoupon"/> + <waitForPageLoad stepKey="waitForCancel"/> + <see userInput='You canceled the coupon code.' stepKey="seeCancellationMessage"/> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart2"/> + <waitForPageLoad stepKey="waitForShippingMethods"/> + <click stepKey="chooseFreeShipping" selector="{{CheckoutShippingMethodsSection.shippingMethodRowByName('Free')}}"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext1"/> + <waitForPageLoad stepKey="waitForReviewAndPayments1"/> + <conditionalClick selector="{{DiscountSection.DiscountTab}}" dependentSelector="{{DiscountSection.CouponInput}}" visible="false" stepKey="clickIfDiscountTabClosed2"/> + <waitForPageLoad stepKey="waitForCouponTabOpen2"/> + <fillField selector="{{DiscountSection.DiscountInput}}" userInput="{{CatPriceRule.coupon_code}}" stepKey="fillCouponCode"/> + <click selector="{{DiscountSection.ApplyCodeBtn}}" stepKey="applyCode"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see userInput="Your coupon was successfully applied." stepKey="seeSuccessMessage"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder1"/> + <waitForPageLoad stepKey="waitForError"/> + <see stepKey="seeShippingMethodError" userInput="The shipping method is missing. Select the shipping method and try again."/> + <amOnPage stepKey="navigateToShippingPage" url="{{CheckoutShippingPage.url}}"/> + <waitForPageLoad stepKey="waitForShippingPageLoad"/> + <click stepKey="chooseFlatRateShipping" selector="{{CheckoutShippingMethodsSection.shippingMethodRowByName('Flat Rate')}}"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext2"/> + <waitForPageLoad stepKey="waitForReviewAndPayments2"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder2"/> + <waitForPageLoad stepKey="waitForSuccessfullyPlacedOrder"/> + <see stepKey="seeSuccessMessageForPlacedOrder" userInput="Thank you for your purchase!"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontUpdateShoppingCartWhileUpdateMinicartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontUpdateShoppingCartWhileUpdateMinicartTest.xml new file mode 100644 index 000000000000..fb80b4880a6f --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontUpdateShoppingCartWhileUpdateMinicartTest.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StoreFrontUpdateShoppingCartWhileUpdateMinicartTest"> + <annotations> + <stories value="Shopping Cart"/> + <title value="Check updating shopping cart while updating items from minicart"/> + <description value="Check updating shopping cart while updating items from minicart"/> + <severity value="AVERAGE"/> + <testCaseId value="MAGETWO-97280"/> + <useCaseId value="MAGETWO-71344"/> + <group value="checkout"/> + </annotations> + + <before> + <!--Create product--> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + </before> + + <after> + <!--Delete created data--> + <deleteData createDataKey="createProduct" stepKey="deleteProduct" /> + </after> + + <!--Add product to cart--> + <amOnPage url="$$createProduct.name$$.html" stepKey="navigateToProductPage"/> + <waitForPageLoad stepKey="waitForProductPage"/> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + + <!--Go to Shopping cart and check Qty--> + <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="goToShoppingCart"/> + <grabValueFrom selector="{{CheckoutCartProductSection.ProductQuantityByName($$createProduct.name$$)}}" stepKey="grabQtyShoppingCart"/> + <assertEquals expected="1" actual="$grabQtyShoppingCart" stepKey="assertQtyShoppingCart"/> + + <!--Open minicart and change Qty--> + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="openMiniCart"/> + <waitForElementVisible selector="{{StorefrontMinicartSection.quantity}}" stepKey="waitForElementQty"/> + <pressKey selector="{{StorefrontMinicartSection.itemQuantity($$createProduct.name$$)}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::BACKSPACE]" stepKey="deleteFiled"/> + <fillField selector="{{StorefrontMinicartSection.itemQuantity($$createProduct.name$$)}}" userInput="5" stepKey="changeQty"/> + <click selector="{{StorefrontMinicartSection.itemQuantityUpdate($$createProduct.name$$)}}" stepKey="updateQty"/> + <waitForAjaxLoad stepKey="waitForAjaxLoad"/> + + <!--Check Qty in shopping cart after updating--> + <grabValueFrom selector="{{CheckoutCartProductSection.ProductQuantityByName($$createProduct.name$$)}}" stepKey="grabQtyShoppingCart1"/> + <assertEquals expected="5" actual="$grabQtyShoppingCart1" stepKey="assertQtyShoppingCart1"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest.xml index 6ea8d8a16729..fadc9ec50ad8 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest.xml @@ -15,7 +15,7 @@ <title value="Customer Checkout"/> <description value="Should be able to place an order as a customer."/> <severity value="CRITICAL"/> - <testCaseId value="#"/> + <testCaseId value="MC-5922"/> <group value="checkout"/> </annotations> <before> @@ -26,10 +26,13 @@ <createData entity="Simple_US_Customer" stepKey="simpleuscustomer"/> </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <!--Clear filters--> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomers"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingCustomerFilters"/> + + <actionGroup ref="logout" stepKey="logout"/> <deleteData createDataKey="simpleproduct1" stepKey="deleteProduct1"/> <deleteData createDataKey="simplecategory" stepKey="deleteCategory"/> - <deleteData createDataKey="simpleuscustomer" stepKey="deleteCustomer"/> </after> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> @@ -48,6 +51,8 @@ <click stepKey="s35" selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}"/> <waitForElement stepKey="s36" selector="{{CheckoutShippingMethodsSection.next}}" time="30"/> <click stepKey="s37" selector="{{CheckoutShippingMethodsSection.next}}" /> + <!-- Checkout select Check/Money Order payment --> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> <waitForPageLoad stepKey="s39"/> <waitForElement stepKey="s41" selector="{{CheckoutPaymentSection.placeOrder}}" time="30" /> <see stepKey="s47" selector="{{CheckoutPaymentSection.billingAddress}}" userInput="{{US_Address_TX.street[0]}}" /> @@ -76,6 +81,7 @@ <amOnPage stepKey="s96" url="{{AdminCustomerPage.url}}"/> <waitForPageLoad stepKey="s97"/> + <waitForElementVisible selector="{{AdminCustomerFiltersSection.filtersButton}}" time="30" stepKey="waitFiltersButton"/> <click stepKey="s98" selector="{{AdminCustomerFiltersSection.filtersButton}}"/> <fillField stepKey="s99" selector="{{AdminCustomerFiltersSection.emailInput}}" userInput="$$simpleuscustomer.email$$"/> <click stepKey="s100" selector="{{AdminCustomerFiltersSection.apply}}"/> @@ -91,9 +97,7 @@ <description value="Should be able to place an order as a customer with multiple addresses and tax rates."/> <testCaseId value="MAGETWO-93109"/> <severity value="AVERAGE"/> - <skip> - <issueId value="MQE-1187" /> - </skip> + <group value="checkout"/> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="simplecategory"/> @@ -165,7 +169,8 @@ <click stepKey="selectFirstShippingMethod1" selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}"/> <waitForElement stepKey="waitForShippingMethodSelect1" selector="{{CheckoutShippingMethodsSection.next}}" time="30"/> <click stepKey="clickNextOnShippingMethodLoad1" selector="{{CheckoutShippingMethodsSection.next}}" /> - <waitForPageLoad stepKey="waitForPaymentLoad1"/> + <!-- Checkout select Check/Money Order payment --> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> <waitForElement stepKey="waitForPlaceOrderButton1" selector="{{CheckoutPaymentSection.placeOrder}}" time="30" /> <see stepKey="seeBillingAddressIsCorrect1" selector="{{CheckoutPaymentSection.billingAddress}}" userInput="{{US_Address_NY.street[0]}}" /> <click stepKey="clickPlaceOrderButton1" selector="{{CheckoutPaymentSection.placeOrder}}" /> @@ -181,16 +186,87 @@ <see selector="{{StorefrontMinicartSection.quantity}}" userInput="1" stepKey="seeCartQuantity2"/> <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart2" /> - <click stepKey="changeShippingAddress" selector="{{CheckoutShippingMethodsSection.shipHereButton}}"/> - <waitForElementNotVisible stepKey="waitForShippingMethodLoaderNotVisible" selector="{{CheckoutShippingMethodsSection.shippingMethodLoader}}" time="30"/> - <click stepKey="selectFirstShippingMethod2" selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}"/> - <waitForElement stepKey="waitForShippingMethodSelect2" selector="{{CheckoutShippingMethodsSection.next}}" time="30"/> - <click stepKey="clickNextOnShippingMethodLoad2" selector="{{CheckoutShippingMethodsSection.next}}" /> - <waitForPageLoad stepKey="waitForPaymentLoad2"/> - <waitForElement stepKey="waitForPlaceOrderButton2" selector="{{CheckoutPaymentSection.placeOrder}}" time="30" /> - <see stepKey="seeBillingAddressIsCorrect2" selector="{{CheckoutPaymentSection.billingAddress}}" userInput="{{UK_Not_Default_Address.street[0]}}" /> - <click stepKey="clickPlaceOrderButton2" selector="{{CheckoutPaymentSection.placeOrder}}" /> + <click selector="{{CheckoutShippingMethodsSection.shipHereButton}}" stepKey="changeShippingAddress"/> + <waitForElementNotVisible selector="{{CheckoutShippingMethodsSection.shippingMethodLoader}}" time="30" stepKey="waitForShippingMethodLoaderNotVisible"/> + <waitForElementVisible selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}" time="30" stepKey="waitForShippingMethodRadioToBeVisible"/> + <waitForPageLoad stepKey="waitForPageLoad23"/> + <click selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod2"/> + <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForShippingMethodSelect2"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNextOnShippingMethodLoad2"/> + <!-- Checkout select Check/Money Order payment --> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment2"/> + <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton2"/> + <see selector="{{CheckoutPaymentSection.billingAddress}}" userInput="{{US_Address_NY.street[0]}}" stepKey="seeBillingAddressIsCorrect2" /> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrderButton2"/> <waitForPageLoad stepKey="waitForOrderSuccessPage2"/> - <see stepKey="seeSuccessMessage2" selector="{{CheckoutSuccessMainSection.success}}" userInput="Your order number is:" /> + <see selector="{{CheckoutSuccessMainSection.success}}" userInput="Your order number is:" stepKey="seeSuccessMessage2"/> + </test> + <test name="StorefrontCustomerCheckoutTestWithRestrictedCountriesForPayment"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout flow if payment solutions are not available"/> + <title value="Checkout via Customer Checkout with restricted countries for payment"/> + <description value="Should be able to place an order as a Customer with restricted countries for payment."/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-42653"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <magentoCLI command="config:set checkout/options/display_billing_address_on 1" stepKey="setShowBillingAddressOnPaymentPage" /> + <magentoCLI command="config:set payment/checkmo/allowspecific 1" stepKey="allowSpecificValue" /> + <magentoCLI command="config:set payment/checkmo/specificcountry GB" stepKey="specificCountryValue" /> + <createData entity="Simple_US_Customer" stepKey="simpleuscustomer"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <magentoCLI command="config:set payment/checkmo/allowspecific 0" stepKey="allowSpecificValue" /> + <magentoCLI command="config:set payment/checkmo/specificcountry ''" stepKey="specificCountryValue" /> + <magentoCLI command="config:set checkout/options/display_billing_address_on 0" stepKey="setDisplayBillingAddressOnPaymentMethod" /> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + </after> + <!-- Login as Customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> + <argument name="Customer" value="$$simpleuscustomer$$" /> + </actionGroup> + + <!-- Add product to cart --> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <moveMouseOver selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" stepKey="hoverProduct"/> + <click selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" stepKey="addToCart"/> + <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" time="30" stepKey="waitForProductAdded"/> + <see selector="{{StorefrontCategoryMainSection.SuccessMsg}}" userInput="You added $$createProduct.name$$ to your shopping cart." stepKey="seeAddedToCartMessage"/> + <see selector="{{StorefrontMinicartSection.quantity}}" userInput="1" stepKey="seeCartQuantity"/> + + <!-- Go to checkout page --> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="customerGoToCheckoutFromMinicart" /> + + <!-- Select address --> + <click stepKey="selectAddress" selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}"/> + <waitForElement stepKey="waitNextButton" selector="{{CheckoutShippingMethodsSection.next}}" time="30"/> + <click stepKey="clickNextButton" selector="{{CheckoutShippingMethodsSection.next}}" /> + <waitForPageLoad stepKey="waitBillingForm"/> + <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + <dontsee selector="{{CheckoutPaymentSection.paymentMethodByName('Check / Money order')}}" stepKey="paymentMethodDoesNotAvailable"/> + + <!-- Fill UK Address and verify that payment available and checkout successful --> + <uncheckOption selector="{{StorefrontCheckoutPaymentMethodSection.billingAddressSameAsShippingShared}}" stepKey="uncheckBillingAddressSameAsShippingCheckCheckBox"/> + <selectOption selector="{{CheckoutPaymentSection.billingAddressSelectShared}}" userInput="New Address" stepKey="clickOnNewAddress"/> + <waitForPageLoad stepKey="waitNewAddressBillingForm"/> + <actionGroup ref="LoggedInCheckoutFillNewBillingAddressActionGroup" stepKey="changeAddress"> + <argument name="Address" value="updateCustomerUKAddress"/> + <argument name="classPrefix" value="[aria-hidden=false]"/> + </actionGroup> + <click selector="{{CheckoutPaymentSection.addressAction('Update')}}" stepKey="clickUpdateBillingAddressButton" /> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="customerSelectCheckMoneyOrderPayment" /> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="customerPlaceorder"> + <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage" /> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage" /> + </actionGroup> </test> -</tests> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutWithoutRegionTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutWithoutRegionTest.xml new file mode 100644 index 000000000000..0cc0dcf38e31 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutWithoutRegionTest.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCustomerCheckoutWithoutRegionTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout via the Admin"/> + <title value="Shipping address is not validated in checkout when proceeding step as logged in user with default shipping address"/> + <description value="Shouldn't be able to place an order as a customer without state if it's required."/> + <severity value="CRITICAL"/> + <testCaseId value="#"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_GB_Customer" stepKey="createCustomer"/> + <actionGroup stepKey="loginToAdminPanel" ref="LoginAsAdmin"/> + <actionGroup ref="SelectCountriesWithRequiredRegion" stepKey="setCustomCountryWithRequiredRegion"> + <argument name="countries" value="CustomCountryWithRequiredRegion"/> + </actionGroup> + </before> + <after> + <actionGroup ref="SelectCountriesWithRequiredRegion" stepKey="setDefaultCountriesWithRequiredRegion"> + <argument name="countries" value="DefaultCountriesWithRequiredRegions"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> + <argument name="Customer" value="$$createCustomer$$" /> + </actionGroup> + + <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="navigateToCheckoutPage"/> + + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNextButton"/> + <see selector="{{StorefrontMessagesSection.error}}" userInput='Please specify a regionId in shipping address.' stepKey="seeErrorMessages"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerPlaceOrderWithNewAddressesThatWasEditedTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerPlaceOrderWithNewAddressesThatWasEditedTest.xml new file mode 100644 index 000000000000..651c5bd8d437 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerPlaceOrderWithNewAddressesThatWasEditedTest.xml @@ -0,0 +1,96 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCustomerPlaceOrderWithNewAddressesThatWasEditedTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout via the Storefront"/> + <title value="Customer can place order with new addresses that was edited during checkout with several conditions"/> + <description value="Customer can place order with new addresses."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-67837"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + <after> + <!--Logout from customer account--> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + <!--Go to Storefront as Customer--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> + <argument name="Customer" value="$$createCustomer$$" /> + </actionGroup> + + <!-- Add simple product to cart and go to checkout--> + <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart" /> + + <!-- Click "+ New Address" and Fill new address--> + <click selector="{{CheckoutShippingSection.newAddressButton}}" stepKey="addAddress"/> + <actionGroup ref="LoggedInCheckoutWithOneAddressFieldWithoutStateField" stepKey="changeAddress"> + <argument name="Address" value="UK_Not_Default_Address"/> + <argument name="classPrefix" value="._show"/> + </actionGroup> + + <!--Click "Save Addresses" --> + <click selector="{{CheckoutShippingSection.saveAddress}}" stepKey="saveAddress"/> + <waitForPageLoad stepKey="waitForAddressSaved"/> + <dontSeeElement selector="{{StorefrontCheckoutAddressPopupSection.newAddressModalPopup}}" stepKey="dontSeeModalPopup"/> + + <!--Select Shipping Rate "Flat Rate"--> + <click selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('Flat Rate')}}" stepKey="selectFlatShippingMethod"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask2"/> + + <click selector="{{CheckoutShippingSection.editActiveAddress}}" stepKey="editNewAddress"/> + <actionGroup ref="clearCheckoutAddressPopupFieldsActionGroup" stepKey="clearRequiredFields"> + <argument name="classPrefix" value="._show"/> + </actionGroup> + + <!--Close Popup and click next--> + <click selector="{{StorefrontCheckoutAddressPopupSection.closeAddressModalPopup}}" stepKey="closePopup"/> + <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> + + <!--Refresh Page and Place Order--> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> + <reloadPage stepKey="reloadPage"/> + <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="waitForPlaceOrderButton"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> + <seeElement selector="{{CheckoutSuccessMainSection.success}}" stepKey="orderIsSuccessfullyPlaced"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderLink}}" stepKey="grabOrderNumber"/> + + <!--Verify New addresses in Customer's Address Book--> + <amOnPage url="{{StorefrontCustomerAddressesPage.url}}" stepKey="goToCustomerAddressBook"/> + <see userInput="{{UK_Not_Default_Address.street[0]}}" + selector="{{StorefrontCustomerAddressesSection.addressesList}}" stepKey="checkNewAddressesStreet"/> + <see userInput="{{UK_Not_Default_Address.city}}" + selector="{{StorefrontCustomerAddressesSection.addressesList}}" stepKey="checkNewAddressesCity"/> + <see userInput="{{UK_Not_Default_Address.postcode}}" + selector="{{StorefrontCustomerAddressesSection.addressesList}}" stepKey="checkNewAddressesPostcode"/> + <!--Order review page has address that was created during checkout--> + <amOnPage url="{{StorefrontCustomerOrderViewPage.url({$grabOrderNumber})}}" stepKey="goToOrderReviewPage"/> + <see userInput="{{UK_Not_Default_Address.street[0]}} {{UK_Not_Default_Address.city}}, {{UK_Not_Default_Address.postcode}}" + selector="{{StorefrontCustomerOrderViewSection.shippingAddress}}" stepKey="checkShippingAddress"/> + <see userInput="{{US_Address_TX_Default_Billing.street[0]}}" + selector="{{StorefrontCustomerOrderViewSection.billingAddress}}" stepKey="checkBillingAddress"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutDataPersistTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutDataPersistTest.xml new file mode 100644 index 000000000000..626f095604fa --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutDataPersistTest.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="StorefrontGuestCheckoutDataPersistTest"> + <annotations> + <features value="Checkout"/> + <stories value="MAGETWO-95068: Checkout data (shipping address etc) not persistant after cart update"/> + <title value="Check that checkout data persist after cart update"/> + <description value="Checkout data should be persist after updating cart"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-96979"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + </after> + + <!-- Add simple product to cart --> + <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <!-- Navigate to checkout --> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <!-- Fill shipping address --> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShipping"> + <argument name="shippingMethod" value="Flat Rate"/> + </actionGroup> + <!-- Add simple product to cart --> + <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart1"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <!-- Navigate to checkout --> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart1"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.email}}" userInput="{{CustomerEntityOne.email}}" stepKey="assertGuestEmail"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="assertGuestFirstName"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.lastName}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="assertGuestLastName"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.street}}" userInput="{{CustomerAddressSimple.street[0]}}" stepKey="assertGuestStreet"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.city}}" userInput="{{CustomerAddressSimple.city}}" stepKey="assertGuestCity"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.region}}" userInput="{{CustomerAddressSimple.state}}" stepKey="assertGuestRegion"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.postcode}}" userInput="{{CustomerAddressSimple.postcode}}" stepKey="assertGuestPostcode"/> + <seeInField selector="{{CheckoutShippingGuestInfoSection.telephone}}" userInput="{{CustomerAddressSimple.telephone}}" stepKey="assertGuestTelephone"/> + </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 a7b82d54afb3..ff61b3be08af 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest.xml @@ -57,15 +57,93 @@ <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearOnOrdersPage"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearGridFilter"/> <fillField selector="{{AdminOrdersGridSection.search}}" userInput="{$grabOrderNumber}" stepKey="fillOrderNum"/> <click selector="{{AdminOrdersGridSection.submitSearch}}" stepKey="submitSearchOrderNum"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearOnSearch"/> <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> <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"/> <see selector="{{AdminOrderDetailsInformationSection.itemsOrdered}}" userInput="$$createProduct.name$$" stepKey="seeAdminOrderProduct"/> </test> + <test name="StorefrontGuestCheckoutWithSidebarDisabledTest" extends="StorefrontGuestCheckoutTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout via Guest Checkout"/> + <title value="Guest Checkout when Cart sidebar disabled"/> + <description value="Should be able to place an order as a Guest when Cart sidebar is disabled"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-97001"/> + <group value="checkout"/> + </annotations> + <before> + <magentoCLI stepKey="disableSidebar" command="config:set checkout/sidebar/display 0" /> + </before> + <after> + <magentoCLI stepKey="enableSidebar" command="config:set checkout/sidebar/display 1" /> + </after> + <remove keyForRemoval="guestGoToCheckoutFromMinicart" /> + <actionGroup ref="GoToCheckoutFromCartActionGroup" stepKey="guestGoToCheckoutFromCart" after="seeCartQuantity" /> + </test> + <test name="StorefrontGuestCheckoutTestWithRestrictedCountriesForPayment"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout flow if payment solutions are not available"/> + <title value="Checkout via Guest Checkout with restricted countries for payment"/> + <description value="Should be able to place an order as a Guest with restricted countries for payment."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-42653"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <magentoCLI stepKey="allowSpecificValue" command="config:set payment/checkmo/allowspecific 1" /> + <magentoCLI stepKey="specificCountryValue" command="config:set payment/checkmo/specificcountry GB" /> + + </before> + <after> + <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <magentoCLI stepKey="allowSpecificValue" command="config:set payment/checkmo/allowspecific 0" /> + <magentoCLI stepKey="specificCountryValue" command="config:set payment/checkmo/specificcountry ''" /> + </after> + + <!-- Add product to cart --> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <moveMouseOver selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" stepKey="hoverProduct"/> + <click selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" stepKey="addToCart"/> + <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" time="30" stepKey="waitForProductAdded"/> + <see selector="{{StorefrontCategoryMainSection.SuccessMsg}}" userInput="You added $$createProduct.name$$ to your shopping cart." stepKey="seeAddedToCartMessage"/> + <see selector="{{StorefrontMinicartSection.quantity}}" userInput="1" stepKey="seeCartQuantity"/> + + <!-- Go to checkout page --> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="guestGoToCheckoutFromMinicart" /> + + <!-- Fill US Address and verify that no payment available --> + <actionGroup ref="GuestCheckoutWithSpecificCountryOptionForPaymentMethodActionGroup" stepKey="guestCheckoutFillingShippingSection"> + <argument name="customerVar" value="CustomerEntityOne" /> + <argument name="customerAddressVar" value="CustomerAddressSimple" /> + <argument name="paymentMethod" value="Check / Money order"/> + </actionGroup> + + <!-- Fill UK Address and verify that payment available and checkout successful --> + <click selector="{{CheckoutHeaderSection.shippingMethodStep}}" stepKey="goToShipping" /> + <actionGroup ref="GuestCheckoutFillingShippingSectionWithoutRegionActionGroup" stepKey="guestCheckoutFillingShippingSectionUK"> + <argument name="customerVar" value="CustomerEntityOne" /> + <argument name="customerAddressVar" value="UK_Not_Default_Address" /> + </actionGroup> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="guestSelectCheckMoneyOrderPayment" /> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="guestPlaceorder"> + <argument name="orderNumberMessage" value="CONST.successGuestCheckoutOrderNumberMessage" /> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage" /> + </actionGroup> + </test> </tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.xml new file mode 100644 index 000000000000..913eb34b34d0 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.xml @@ -0,0 +1,95 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontOnePageCheckoutDataWhenChangeQtyTest"> + <annotations> + <stories value="Checkout"/> + <title value="One page Checkout Customer data when changing Product Qty"/> + <description value="One page Checkout Customer data when changing Product Qty"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-96960"/> + <useCaseId value="MAGETWO-96850"/> + <group value="checkout"/> + </annotations> + <before> + <!--Create a product--> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + </before> + <after> + <!--Delete created data--> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + </after> + + <!--Add product to cart and checkout--> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="amOnSimpleProductPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$createProduct.name$$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{CustomerEntityOne.email}}" stepKey="enterEmail"/> + <fillField selector="{{CheckoutShippingSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="enterFirstName"/> + <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="enterLastName"/> + <fillField selector="{{CheckoutShippingSection.street}}" userInput="{{CustomerAddressSimple.street[0]}}" stepKey="enterStreet"/> + <fillField selector="{{CheckoutShippingSection.city}}" userInput="{{CustomerAddressSimple.city}}" stepKey="enterCity"/> + <selectOption selector="{{CheckoutShippingSection.region}}" userInput="{{CustomerAddressSimple.state}}" stepKey="selectRegion"/> + <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="{{CustomerAddressSimple.postcode}}" stepKey="enterPostcode"/> + <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{CustomerAddressSimple.telephone}}" stepKey="enterTelephone"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + + <!--Grab customer data to check it--> + <grabValueFrom selector="{{CheckoutShippingSection.email}}" stepKey="grabEmail"/> + <grabValueFrom selector="{{CheckoutShippingSection.firstName}}" stepKey="grabFirstName"/> + <grabValueFrom selector="{{CheckoutShippingSection.lastName}}" stepKey="grabLastName"/> + <grabValueFrom selector="{{CheckoutShippingSection.street}}" stepKey="grabStreet"/> + <grabValueFrom selector="{{CheckoutShippingSection.city}}" stepKey="grabCity"/> + <grabTextFrom selector="{{CheckoutShippingSection.region}}" stepKey="grabRegion"/> + <grabValueFrom selector="{{CheckoutShippingSection.postcode}}" stepKey="grabPostcode"/> + <grabValueFrom selector="{{CheckoutShippingSection.telephone}}" stepKey="grabTelephone"/> + + <!--Select shipping method and finalize checkout--> + <click selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> + <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> + <waitForPageLoad stepKey="waitForShippingMethodLoad"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> + <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + + <!--Go to cart page, update qty and proceed to checkout--> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCartPage"/> + <waitForPageLoad stepKey="waitForCartPageLoad"/> + <see userInput="Shopping Cart" stepKey="seeCartPageIsOpened"/> + <fillField selector="{{CheckoutCartProductSection.qty($$createProduct.name$$)}}" userInput="2" stepKey="updateProductQty"/> + <click selector="{{CheckoutCartProductSection.updateShoppingCartButton}}" stepKey="clickUpdateShoppingCart"/> + <waitForAjaxLoad stepKey="waitForAjaxLoad"/> + <grabValueFrom selector="{{CheckoutCartProductSection.qty($$createProduct.name$$)}}" stepKey="grabQty"/> + <assertEquals expected="2" actual="$grabQty" stepKey="assertQty"/> + <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout"/> + + <!--Check that form is filled with customer data--> + <grabValueFrom selector="{{CheckoutShippingSection.email}}" stepKey="grabEmail1"/> + <grabValueFrom selector="{{CheckoutShippingSection.firstName}}" stepKey="grabFirstName1"/> + <grabValueFrom selector="{{CheckoutShippingSection.lastName}}" stepKey="grabLastName1"/> + <grabValueFrom selector="{{CheckoutShippingSection.street}}" stepKey="grabStreet1"/> + <grabValueFrom selector="{{CheckoutShippingSection.city}}" stepKey="grabCity1"/> + <grabTextFrom selector="{{CheckoutShippingSection.region}}" stepKey="grabRegion1"/> + <grabValueFrom selector="{{CheckoutShippingSection.postcode}}" stepKey="grabPostcode1"/> + <grabValueFrom selector="{{CheckoutShippingSection.telephone}}" stepKey="grabTelephone1"/> + + <assertEquals expected="$grabEmail" actual="$grabEmail1" stepKey="assertEmail"/> + <assertEquals expected="$grabFirstName" actual="$grabFirstName1" stepKey="assertFirstName"/> + <assertEquals expected="$grabLastName" actual="$grabLastName1" stepKey="assertLastName"/> + <assertEquals expected="$grabStreet" actual="$grabStreet1" stepKey="assertStreet"/> + <assertEquals expected="$grabCity" actual="$grabCity1" stepKey="assertCity"/> + <assertEquals expected="$grabRegion" actual="$grabRegion1" stepKey="assertRegion"/> + <assertEquals expected="$grabPostcode" actual="$grabPostcode1" stepKey="assertPostcode"/> + <assertEquals expected="$grabTelephone" actual="$grabTelephone1" stepKey="assertTelephone"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml new file mode 100644 index 000000000000..20ff67a076e1 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml @@ -0,0 +1,90 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout via Guest Checkout"/> + <title value="Persistent Data for Guest Customer with physical quote"/> + <description value="One can use Persistent Data for Guest Customer with physical quote"/> + <severity value="MAJOR"/> + <testCaseId value="MC-13479"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="SimpleProduct2" stepKey="createProduct"> + <field key="price">10</field> + </createData> + <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefront"/> + <executeJS function="window.localStorage.clear();" stepKey="clearLocalStorage"/> + </before> + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShipping"/> + </after> + <!-- 1. Add simple product to cart and go to checkout--> + <actionGroup ref="AddSimpleProductToCart" stepKey="addSimpleProductToCart"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <!-- 2. Go to Shopping Cart --> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCheckoutCartIndexPage"/> + <!-- 3. Open "Estimate Shipping and Tax" section and input data --> + <actionGroup ref="StorefrontCartEstimateShippingAndTaxActionGroup" stepKey="fillEstimateShippingAndTaxSection"/> + <actionGroup ref="StorefrontAssertShippingMethodPresentInCartActionGroup" stepKey="assertShippingMethodFlatRateIsPresentInCart"> + <argument name="shippingMethod" value="Flat Rate"/> + </actionGroup> + <actionGroup ref="StorefrontAssertShippingMethodPresentInCartActionGroup" stepKey="assertShippingMethodFreeShippingIsPresentInCart"> + <argument name="shippingMethod" value="Free Shipping"/> + </actionGroup> + <!-- 4. Select Flat Rate as shipping --> + <checkOption selector="{{CheckoutCartSummarySection.flatRateShippingMethod}}" stepKey="selectFlatRateShippingMethod"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearAfterFlatRateSelection"/> + <see selector="{{CheckoutCartSummarySection.total}}" userInput="15" stepKey="assertOrderTotalField"/> + <!-- 5. Refresh browser page (F5) --> + <reloadPage stepKey="reloadPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="StorefrontAssertCartEstimateShippingAndTaxActionGroup" stepKey="assertCartEstimateShippingAndTaxAfterPageReload"/> + <actionGroup ref="StorefrontAssertCartShippingMethodSelectedActionGroup" stepKey="assertFlatRateShippingMethodIsChecked"> + <argument name="carrierCode" value="flatrate"/> + <argument name="methodCode" value="flatrate"/> + </actionGroup> + <!-- 6. Go to Checkout --> + <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout"/> + <actionGroup ref="StorefrontAssertCheckoutEstimateShippingInformationActionGroup" stepKey="assertCheckoutEstimateShippingInformationAfterGoingToCheckout"/> + <actionGroup ref="StorefrontAssertCheckoutShippingMethodSelectedActionGroup" stepKey="assertFlatRateShippingMethodIsCheckedAfterGoingToCheckout"> + <argument name="shippingMethod" value="Flat Rate"/> + </actionGroup> + <!-- 7. Change persisted data --> + <selectOption selector="{{CheckoutShippingGuestInfoSection.country}}" userInput="United Kingdom" stepKey="changeCountryField"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.regionInput}}" userInput="" stepKey="changeStateProvinceField"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.postcode}}" userInput="KW1 7NQ" stepKey="changeZipPostalCodeField"/> + <!-- 8. Change shipping rate, select Free Shipping --> + <checkOption selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('Free Shipping')}}" stepKey="checkFreeShippingAsShippingMethod"/> + <!-- 9. Fill other fields --> + <actionGroup ref="StorefrontFillGuestShippingInfoActionGroup" stepKey="fillOtherFieldsInCheckoutShippingSection"/> + <!-- 10. Refresh browser page(F5) --> + <reloadPage stepKey="reloadCheckoutPage"/> + <waitForPageLoad stepKey="waitForCheckoutPageLoad"/> + <actionGroup ref="StorefrontAssertGuestShippingInfoActionGroup" stepKey="assertGuestShippingPersistedInfoAfterReloadingCheckoutShippingPage"/> + <actionGroup ref="StorefrontAssertCheckoutShippingMethodSelectedActionGroup" stepKey="assertFreeShippingShippingMethodIsChecked"> + <argument name="shippingMethod" value="Free Shipping"/> + </actionGroup> + <!-- 11. Go back to the shopping cart --> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCheckoutCartIndexPage1"/> + <actionGroup ref="StorefrontAssertCartEstimateShippingAndTaxActionGroup" stepKey="assertCartEstimateShippingAndTaxAfterGoingBackToShoppingCart"> + <argument name="customerData" value="Simple_UK_Customer_For_Shipment"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCartShippingMethodSelectedActionGroup" stepKey="assertFreeShippingShippingMethodIsCheckedAfterGoingBackToShoppingCart"> + <argument name="carrierCode" value="freeshipping"/> + <argument name="methodCode" value="freeshipping"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest.xml new file mode 100644 index 000000000000..3401369a8c74 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest.xml @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest"> + <annotations> + <features value="Checkout"/> + <title value="Checking Product name in Minicart and on Checkout page with different store views"/> + <description value="Checking Product name in Minicart and on Checkout page with different store views"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-96466"/> + <useCaseId value="MAGETWO-96421"/> + <group value="checkout"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!--Create a product--> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + </before> + <after> + <!--Delete created data--> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"> + <argument name="customStore" value="customStore"/> + </actionGroup> + + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Create store view --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> + <argument name="customStore" value="customStore"/> + </actionGroup> + + <!--Go to created product page--> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="goToEditPage"/> + <waitForPageLoad stepKey="waitForProductPage"/> + + <!--Switch to second store view and change the product name--> + <actionGroup ref="SwitchToTheNewStoreView" stepKey="switchToCustomStoreView"> + <argument name="storeViewName" value="{{customStore.name}}"/> + </actionGroup> + <waitForPageLoad stepKey="waitForPageLoad"/> + <click selector="{{AdminProductFormSection.productNameUseDefault}}" stepKey="uncheckUseDefault"/> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="$$createProduct.name$$-new" stepKey="fillProductName"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + + <!--Add product to cart--> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="amOnSimpleProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$createProduct.name$$"/> + </actionGroup> + + <!--Switch to second store view--> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchStoreView"> + <argument name="storeView" value="customStore"/> + </actionGroup> + <waitForPageLoad stepKey="waitForStoreView"/> + + <!--Check product name in Minicart--> + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickCart"/> + <grabTextFrom selector="{{StorefrontMinicartSection.productName}}" stepKey="grabProductNameMinicart"/> + <assertContains expected="$$createProduct.name$$" actual="$grabProductNameMinicart" stepKey="assertProductNameMinicart"/> + <assertContains expectedType="string" expected="-new" actual="$grabProductNameMinicart" stepKey="assertProductNameMinicart1"/> + + <!--Check product name in Shopping Cart page--> + <click selector="{{StorefrontMinicartSection.viewAndEditCart}}" stepKey="clickViewAndEdit"/> + <waitForPageLoad stepKey="waitForShoppingCartPageLoad"/> + <grabTextFrom selector="{{CheckoutCartProductSection.productName}}" stepKey="grabProductNameCart"/> + <assertContains expected="$$createProduct.name$$" actual="$grabProductNameCart" stepKey="assertProductNameCart"/> + <assertContains expectedType="string" expected="-new" actual="$grabProductNameCart" stepKey="assertProductNameCart1"/> + + <!--Proceed to checkout and check product name in Order Summary area--> + <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="proceedToCheckout"/> + <waitForPageLoad stepKey="waitForShippingPageLoad"/> + <click selector="{{CheckoutShippingGuestInfoSection.itemInCart}}" stepKey="clickItemInCart"/> + <grabTextFrom selector="{{CheckoutShippingGuestInfoSection.productName}}" stepKey="grabProductNameShipping"/> + <assertContains expected="$$createProduct.name$$" actual="$grabProductNameShipping" stepKey="assertProductNameShipping"/> + <assertContains expectedType="string" expected="-new" actual="$grabProductNameShipping" stepKey="assertProductNameShipping1"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontShoppingCartCheckCustomerDefaultShippingAddressForVirtualQuoteTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontShoppingCartCheckCustomerDefaultShippingAddressForVirtualQuoteTest.xml new file mode 100644 index 000000000000..b0e1dead1fff --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontShoppingCartCheckCustomerDefaultShippingAddressForVirtualQuoteTest.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="StorefrontShoppingCartCheckCustomerDefaultShippingAddressForVirtualQuoteTest"> + <annotations> + <features value="Checkout"/> + <stories value="Estimator in Shopping cart must be pre-filled by Customer default shipping address for virtual quote"/> + <title value="Estimator in Shopping cart must be pre-filled by Customer default shipping address for virtual quote"/> + <description value="Estimator in Shopping cart must be pre-filled by Customer default shipping address for virtual quote"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-46795"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="VirtualProduct" stepKey="createVirtualProduct"/> + <createData entity="Customer_With_Different_Default_Billing_Shipping_Addresses" stepKey="createCustomer"/> + </before> + <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> + <deleteData createDataKey="createVirtualProduct" stepKey="deleteVirtualProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + <!-- Steps --> + <!-- Step 1: Go to Storefront as Customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> + <argument name="Customer" value="$$createCustomer$$" /> + </actionGroup> + <!-- Step 2: Add virtual product to cart --> + <amOnPage url="{{StorefrontProductPage.url($$createVirtualProduct.custom_attributes[url_key]$)}}" stepKey="amOnPage"/> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$createVirtualProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + <!-- Step 3: Go to Shopping Cart --> + <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="goToShoppingcart"/> + <!-- Step 4: Open Estimate Tax section --> + <click selector="{{CheckoutCartSummarySection.estimateShippingAndTax}}" stepKey="openEstimateTaxSection"/> + <seeOptionIsSelected selector="{{CheckoutCartSummarySection.country}}" userInput="{{US_Address_CA.country}}" stepKey="checkCountry"/> + <seeOptionIsSelected selector="{{CheckoutCartSummarySection.stateProvince}}" userInput="{{US_Address_CA.state}}" stepKey="checkState"/> + <scrollTo selector="{{CheckoutCartSummarySection.postcode}}" stepKey="scrollToPostCodeField"/> + <grabValueFrom selector="{{CheckoutCartSummarySection.postcode}}" stepKey="grabTextPostCode"/> + <assertEquals message="Customer postcode is invalid" stepKey="checkCustomerPostcode"> + <expectedResult type="string">{{US_Address_CA.postcode}}</expectedResult> + <actualResult type="variable">grabTextPostCode</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml new file mode 100644 index 000000000000..b4747a6bf727 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontUpdatePriceInShoppingCartAfterProductSaveTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout via the Storefront"/> + <title value="Update price in shopping cart after product save"/> + <description value="Price in shopping cart should be updated after product save with changed price"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-58179"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"> + <field key="price">100</field> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="SetCustomerDataLifetimeActionGroup" stepKey="setCustomerDataLifetime"> + <argument name="minutes" value="1"/> + </actionGroup> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <actionGroup ref="SetCustomerDataLifetimeActionGroup" stepKey="setDefaultCustomerDataLifetime"/> + <magentoCLI command="indexer:reindex customer_grid" stepKey="reindexCustomerGrid"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Go to product page--> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToSimpleProductPage"/> + + <!--Add Product to Shopping Cart--> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + </actionGroup> + + <!--Go to Checkout--> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShipping"/> + + <!--Check price--> + <conditionalClick selector="{{CheckoutPaymentSection.cartItemsArea}}" dependentSelector="{{CheckoutPaymentSection.cartItemsAreaActive}}" visible="false" stepKey="openItemProductBlock"/> + <see userInput="$100.00" selector="{{CheckoutPaymentSection.orderSummarySubtotal}}" stepKey="checkSummarySubtotal"/> + <see userInput="$100.00" selector="{{CheckoutPaymentSection.productItemPriceByName($$createSimpleProduct.name$$)}}" stepKey="checkItemPrice"/> + + <!--Edit product price via admin panel--> + <openNewTab stepKey="openNewTab"/> + <amOnPage url="{{AdminProductEditPage.url($$createSimpleProduct.id$$)}}" stepKey="goToProductEditPage"/> + <fillField userInput="120" selector="{{AdminProductFormSection.productPrice}}" stepKey="setNewPrice"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + <closeTab stepKey="closeTab"/> + + <!--Check price--> + <reloadPage stepKey="reloadPage"/> + <waitForPageLoad stepKey="waitForCheckoutPageReload"/> + <conditionalClick selector="{{CheckoutPaymentSection.cartItemsArea}}" dependentSelector="{{CheckoutPaymentSection.cartItemsAreaActive}}" visible="false" stepKey="openItemProductBlock1"/> + <see userInput="$120.00" selector="{{CheckoutPaymentSection.orderSummarySubtotal}}" stepKey="checkSummarySubtotal1"/> + <see userInput="$120.00" selector="{{CheckoutPaymentSection.productItemPriceByName($$createSimpleProduct.name$$)}}" stepKey="checkItemPrice1"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleProductQtyTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleProductQtyTest.xml new file mode 100644 index 000000000000..423f4049f672 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleProductQtyTest.xml @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontUpdateShoppingCartSimpleProductQtyTest"> + <annotations> + <features value="Checkout"/> + <title value="Check updating shopping cart while updating items qty"/> + <description value="Check updating shopping cart while updating items qty"/> + <testCaseId value="MC-14731" /> + <group value="shoppingCart"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Add the newly created product to the shopping cart --> + <actionGroup ref="AddSimpleProductToCart" stepKey="addToCartFromStorefrontProductPage"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + </before> + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + </after> + + <!-- Go to the shopping cart --> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="amOnPageShoppingCart"/> + <waitForPageLoad stepKey="waitForCheckoutPageLoad1"/> + + <!-- Change the product QTY --> + <fillField selector="{{CheckoutCartProductSection.ProductQuantityByName($$createProduct.name$$)}}" userInput="{{quoteQty3Price123.qty}}" stepKey="changeCartQty"/> + <click selector="{{CheckoutCartProductSection.updateShoppingCartButton}}" stepKey="openShoppingCart"/> + <waitForPageLoad stepKey="waitForCheckoutPageLoad2"/> + + <!-- The price and QTY values should be updated for the product --> + <grabValueFrom selector="{{CheckoutCartProductSection.ProductQuantityByName($$createProduct.name$$)}}" stepKey="grabProductQtyInCart"/> + <see userInput="{{quoteQty3Price123.currency}}{{quoteQty3Price123.subtotal}}" selector="{{CheckoutCartProductSection.productSubtotalByName($$createProduct.name$$)}}" stepKey="assertProductPrice"/> + <assertEquals stepKey="assertProductQtyInCart"> + <actualResult type="variable">grabProductQtyInCart</actualResult> + <expectedResult type="string">{{quoteQty3Price123.qty}}</expectedResult> + </assertEquals> + + <!-- Subtotal should be updated --> + <see userInput="{{quoteQty3Price123.currency}}{{quoteQty3Price123.subtotal}}" selector="{{CheckoutCartSummarySection.subtotal}}" stepKey="assertCartSubtotal"/> + + <!-- Minicart product price and subtotal should be updated --> + <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="openMinicart"/> + <grabValueFrom selector="{{StorefrontMinicartSection.itemQuantity($$createProduct.name$$)}}" stepKey="grabProductQtyInMinicart"/> + <assertEquals stepKey="assertProductQtyInMinicart"> + <actualResult type="variable">grabProductQtyInMinicart</actualResult> + <expectedResult type="string">{{quoteQty3Price123.qty}}</expectedResult> + </assertEquals> + <see userInput="{{quoteQty3Price123.currency}}{{quoteQty3Price123.subtotal}}" selector="{{StorefrontMinicartSection.subtotal}}" stepKey="assertMinicartSubtotal"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleWithCustomOptionsProductQtyTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleWithCustomOptionsProductQtyTest.xml new file mode 100644 index 000000000000..84080b04c80e --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleWithCustomOptionsProductQtyTest.xml @@ -0,0 +1,70 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontUpdateShoppingCartSimpleWithCustomOptionsProductQtyTest"> + <annotations> + <features value="Checkout"/> + <title value="Check updating shopping cart while updating qty of items with custom options"/> + <description value="Check updating shopping cart while updating qty of items with custom options"/> + <testCaseId value="MC-14732" /> + <group value="shoppingCart"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProductWithCustomPrice" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Add two custom options to the product: field and textarea --> + <updateData createDataKey="createProduct" entity="ProductWithTextFieldAndAreaOptions" stepKey="updateProductWithOption"/> + + <!-- Go to the product page, fill the custom options values and add the product to the shopping cart --> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="amOnProductPage"/> + <waitForPageLoad stepKey="waitForCatalogPageLoad"/> + <fillField userInput="OptionField" selector="{{StorefrontProductInfoMainSection.productOptionFieldInput(ProductOptionField.title)}}" stepKey="fillProductOptionInputField"/> + <fillField userInput="OptionArea" selector="{{StorefrontProductInfoMainSection.productOptionAreaInput(ProductOptionArea.title)}}" stepKey="fillProductOptionInputArea"/> + <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$createProduct.name$"/> + </actionGroup> + </before> + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + </after> + + <!-- Go to the shopping cart --> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="amOnPageShoppingCart"/> + <waitForPageLoad stepKey="waitForCheckoutPageLoad"/> + + <!-- Change the product QTY --> + <fillField selector="{{CheckoutCartProductSection.ProductQuantityByName($$createProduct.name$$)}}" userInput="{{quoteQty11Subtotal1320.qty}}" stepKey="changeCartQty"/> + <click selector="{{CheckoutCartProductSection.updateShoppingCartButton}}" stepKey="updateShoppingCart"/> + <waitForPageLoad stepKey="waitShoppingCartUpdated"/> + + <!-- The price and QTY values should be updated for the product --> + <grabValueFrom selector="{{CheckoutCartProductSection.ProductQuantityByName($$createProduct.name$$)}}" stepKey="grabProductQtyInCart"/> + <see userInput="{{quoteQty11Subtotal1320.currency}}{{quoteQty11Subtotal1320.subtotal}}" selector="{{CheckoutCartProductSection.productSubtotalByName($$createProduct.name$$)}}" stepKey="assertProductPrice"/> + <assertEquals stepKey="assertProductQtyInCart"> + <expectedResult type="string">{{quoteQty11Subtotal1320.qty}}</expectedResult> + <actualResult type="variable">grabProductQtyInCart</actualResult> + </assertEquals> + <see userInput="{{quoteQty11Subtotal1320.currency}}{{quoteQty11Subtotal1320.subtotal}}" selector="{{CheckoutCartSummarySection.subtotal}}" stepKey="assertSubtotal"/> + + <!-- Minicart product price and subtotal should be updated --> + <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="openMinicart"/> + <grabValueFrom selector="{{StorefrontMinicartSection.itemQuantity($$createProduct.name$$)}}" stepKey="grabProductQtyInMinicart"/> + <assertEquals stepKey="assertProductQtyInMinicart"> + <expectedResult type="string">{{quoteQty11Subtotal1320.qty}}</expectedResult> + <actualResult type="variable">grabProductQtyInMinicart</actualResult> + </assertEquals> + <see userInput="{{quoteQty11Subtotal1320.currency}}{{quoteQty11Subtotal1320.subtotal}}" selector="{{StorefrontMinicartSection.subtotal}}" stepKey="assertMinicartSubtotal"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontValidateEmailOnCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontValidateEmailOnCheckoutTest.xml new file mode 100644 index 000000000000..1b27e1d53ada --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontValidateEmailOnCheckoutTest.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontValidateEmailOnCheckoutTest"> + <annotations> + <features value="Checkout"/> + <title value="Email validation for Guest on checkout flow"/> + <description value="Email validation for Guest on checkout flow"/> + <stories value="Guest Checkout"/> + <testCaseId value="MC-14695" /> + <group value="checkout"/> + <group value="shoppingCart"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="SimpleTwo" stepKey="simpleProduct"/> + </before> + + <after> + <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> + </after> + + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductStorefront"> + <argument name="productUrl" value="$$simpleProduct.custom_attributes[url_key]$$" /> + </actionGroup> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage" /> + + <actionGroup ref="StorefrontOpenCheckoutPageActionGroup" stepKey="openCheckoutPage" /> + <actionGroup ref="AssertStorefrontEmailTooltipContentOnCheckoutActionGroup" stepKey="assertEmailTooltipContent" /> + <actionGroup ref="AssertStorefrontEmailNoteMessageOnCheckoutActionGroup" stepKey="assertEmailNoteMessage" /> + + <actionGroup ref="StorefrontFillEmailFieldOnCheckoutActionGroup" stepKey="fillIncorrectEmailFirstAttempt"> + <argument name="email" value="John" /> + </actionGroup> + <actionGroup ref="AssertStorefrontEmailValidationMessageOnCheckoutActionGroup" stepKey="verifyValidationErrorMessageFirstAttempt" /> + + <actionGroup ref="StorefrontFillEmailFieldOnCheckoutActionGroup" stepKey="fillIncorrectEmailSecondAttempt"> + <argument name="email" value="johndoe#example.com" /> + </actionGroup> + <actionGroup ref="AssertStorefrontEmailValidationMessageOnCheckoutActionGroup" stepKey="verifyValidationErrorMessageSecondAttempt" /> + + <actionGroup ref="StorefrontFillEmailFieldOnCheckoutActionGroup" stepKey="fillIncorrectEmailThirdAttempt"> + <argument name="email" value="johndoe@example.c" /> + </actionGroup> + <actionGroup ref="AssertStorefrontEmailValidationMessageOnCheckoutActionGroup" stepKey="verifyValidationErrorMessageThirdAttempt" /> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromMiniShoppingCartEntityTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromMiniShoppingCartEntityTest.xml new file mode 100644 index 000000000000..7318f865a0dc --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromMiniShoppingCartEntityTest.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="UpdateProductFromMiniShoppingCartEntityTest"> + <annotations> + <stories value="Shopping Cart"/> + <title value="Check updating product from mini shopping cart"/> + <description value="Update Product Qty on Mini Shopping Cart"/> + <severity value="MAJOR"/> + <testCaseId value="MC-15068"/> + <group value="shoppingCart"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <!--Create product according to dataset.--> + <createData entity="simpleProductWithoutCategory" stepKey="createProduct"/> + + <!--Add product to cart--> + <actionGroup ref="AddSimpleProductToCart" stepKey="addToCartFromStorefrontProductPage"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + </before> + + <after> + <!--Delete created data--> + <deleteData createDataKey="createProduct" stepKey="deleteProduct" /> + </after> + + <actionGroup ref="StorefrontUpdateProductQtyMiniShoppingCartActionGroup" stepKey="updateProductQty"> + <argument name="product" value="$$createProduct$$" /> + <argument name="quote" value="simpleOrderQty2" /> + </actionGroup> + + <!-- Perform all assertions --> + <actionGroup ref="AssertMiniShoppingCartSubTotalActionGroup" stepKey="checkSummary"> + <argument name="dataQuote" value="simpleOrderQty2"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/ZeroSubtotalOrdersWithProcessingStatusTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/ZeroSubtotalOrdersWithProcessingStatusTest.xml new file mode 100644 index 000000000000..4b3e18fb3187 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/ZeroSubtotalOrdersWithProcessingStatusTest.xml @@ -0,0 +1,103 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="ZeroSubtotalOrdersWithProcessingStatusTest"> + <annotations> + <features value="Checkout"/> + <stories value="MAGETWO-71375: Zero Subtotal Orders have incorrect status"/> + <title value="Checking status of Zero Subtotal Orders with 'Processing' New Order Status"/> + <description value="Created order should be in Processing status"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-94178"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="simplecategory"/> + <createData entity="SimpleProduct" stepKey="simpleproduct"> + <requiredEntity createDataKey="simplecategory"/> + </createData> + <createData entity="PaymentMethodsSettingConfig" stepKey="paymentMethodsSettingConfig"/> + <createData entity="FreeShippingMethodsSettingConfig" stepKey="freeShippingMethodsSettingConfig"/> + <!--Go to Admin page--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <after> + <actionGroup ref="DeleteCartPriceRuleByName" stepKey="deleteSalesRule"> + <argument name="ruleName" value="{{ApiSalesRule.name}}"/> + </actionGroup> + <createData entity="DefaultShippingMethodsConfig" stepKey="defaultShippingMethodsConfig"/> + <createData entity="DisableFreeShippingConfig" stepKey="disableFreeShippingConfig"/> + <createData entity="DisablePaymentMethodsSettingConfig" stepKey="disablePaymentMethodsSettingConfig"/> + <actionGroup ref="logout" stepKey="logout"/> + <deleteData createDataKey="simpleproduct" stepKey="deleteProduct"/> + <deleteData createDataKey="simplecategory" stepKey="deleteCategory"/> + </after> + + <!--Open MARKETING > Cart Price Rules--> + <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> + <waitForPageLoad stepKey="waitForRulesPage"/> + + <!--Add New Rule--> + <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> + <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{ApiSalesRule.name}}" stepKey="fillRuleName"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsite"/> + <actionGroup ref="selectNotLoggedInCustomerGroup" stepKey="chooseNotLoggedInCustomerGroup"/> + <generateDate date="-1 day" format="m/d/Y" stepKey="yesterdayDate"/> + <fillField selector="{{AdminCartPriceRulesFormSection.fromDate}}" userInput="{$yesterdayDate}" stepKey="fillFromDate"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.coupon}}" userInput="Specific Coupon" stepKey="selectCouponType"/> + <fillField selector="{{AdminCartPriceRulesFormSection.couponCode}}" userInput="{{_defaultCoupon.code}}" stepKey="fillCouponCode"/> + <fillField selector="{{AdminCartPriceRulesFormSection.userPerCoupon}}" userInput="99" stepKey="fillUserPerCoupon"/> + <click selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="clickToExpandActions"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.apply}}" userInput="Percent of product price discount" stepKey="selectActionType"/> + <fillField selector="{{AdminCartPriceRulesFormSection.discountAmount}}" userInput="100" stepKey="fillDiscountAmount"/> + <click selector="{{AdminCartPriceRulesFormSection.save}}" stepKey="clickSaveButton"/> + <see selector="{{AdminCartPriceRulesSection.messages}}" userInput="You saved the rule." stepKey="seeSuccessMessage"/> + + <!--Proceed to store front and place an order with free shipping using created coupon--> + <!--Add product to card--> + <actionGroup ref="AddSimpleProductToCart" stepKey="AddProductToCard"> + <argument name="product" value="$$simpleproduct$$"/> + </actionGroup> + + <!--Proceed to shipment--> + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickToOpenCard"/> + <click selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="clickToProceedToCheckout"/> + <waitForPageLoad stepKey="waitForTheFormIsOpened"/> + + <!--Fill shipping form--> + <actionGroup ref="ShipmentFormFreeShippingActionGroup" stepKey="shipmentFormFreeShippingActionGroup"/> + + <click selector="{{DiscountSection.DiscountTab}}" stepKey="clickToAddDiscount"/> + <fillField selector="{{DiscountSection.DiscountInput}}" userInput="{{_defaultCoupon.code}}" stepKey="TypeDiscountCode"/> + <click selector="{{DiscountSection.ApplyCodeBtn}}" stepKey="clickToApplyDiscount"/> + <waitForPageLoad stepKey="WaitForDiscountToBeAdded"/> + <see userInput="Your coupon was successfully applied." stepKey="verifyText"/> + + <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> + + <!--Proceed to Admin panel > SALES > Orders. Created order should be in Processing status--> + <amOnPage url="/admin/sales/order/" stepKey="navigateToSalesOrderPage"/> + <waitForPageLoad stepKey="waitForSalesOrderPageLoaded"/> + + <!-- Open Order --> + <actionGroup ref="filterOrderGridById" stepKey="filterOrderGridById"> + <argument name="orderId" value="$grabOrderNumber"/> + </actionGroup> + <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> + <waitForPageLoad stepKey="waitForCreatedOrderPageOpened"/> + + <!--Verify that Created order is in Processing status--> + <see selector="{{AdminShipmentOrderInformationSection.orderStatus}}" userInput="Processing" stepKey="seeShipmentOrderStatus"/> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderFilters"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Unit/Block/Cart/SidebarTest.php b/app/code/Magento/Checkout/Test/Unit/Block/Cart/SidebarTest.php index 1c5224d007ec..f69ced3b094c 100644 --- a/app/code/Magento/Checkout/Test/Unit/Block/Cart/SidebarTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Block/Cart/SidebarTest.php @@ -144,7 +144,8 @@ public function testGetConfig() 'baseUrl' => $baseUrl, 'minicartMaxItemsVisible' => 3, 'websiteId' => 100, - 'maxItemsToDisplay' => 8 + 'maxItemsToDisplay' => 8, + 'storeId' => null ]; $valueMap = [ @@ -161,7 +162,7 @@ public function testGetConfig() $this->urlBuilderMock->expects($this->exactly(4)) ->method('getUrl') ->willReturnMap($valueMap); - $this->storeManagerMock->expects($this->exactly(2))->method('getStore')->willReturn($storeMock); + $this->storeManagerMock->expects($this->any())->method('getStore')->willReturn($storeMock); $storeMock->expects($this->once())->method('getBaseUrl')->willReturn($baseUrl); $this->scopeConfigMock->expects($this->at(0)) diff --git a/app/code/Magento/Checkout/Test/Unit/Block/Checkout/AttributeMergerTest.php b/app/code/Magento/Checkout/Test/Unit/Block/Checkout/AttributeMergerTest.php new file mode 100644 index 000000000000..23840da97bd4 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Unit/Block/Checkout/AttributeMergerTest.php @@ -0,0 +1,121 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Test\Unit\Block\Checkout; + +use Magento\Customer\Api\CustomerRepositoryInterface as CustomerRepository; +use Magento\Customer\Helper\Address as AddressHelper; +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Directory\Helper\Data as DirectoryHelper; +use Magento\Checkout\Block\Checkout\AttributeMerger; +use PHPUnit\Framework\TestCase; + +class AttributeMergerTest extends TestCase +{ + /** + * @var CustomerRepository + */ + private $customerRepository; + + /** + * @var CustomerSession + */ + private $customerSession; + + /** + * @var AddressHelper + */ + private $addressHelper; + + /** + * @var DirectoryHelper + */ + private $directoryHelper; + + /** + * @var AttributeMerger + */ + private $attributeMerger; + + /** + * @inheritdoc + */ + protected function setUp() + { + + $this->customerRepository = $this->createMock(CustomerRepository::class); + $this->customerSession = $this->createMock(CustomerSession::class); + $this->addressHelper = $this->createMock(AddressHelper::class); + $this->directoryHelper = $this->createMock(DirectoryHelper::class); + + $this->attributeMerger = new AttributeMerger( + $this->addressHelper, + $this->customerSession, + $this->customerRepository, + $this->directoryHelper + ); + } + + /** + * Tests of element attributes merging. + * + * @param String $validationRule - validation rule. + * @param String $expectedValidation - expected mapped validation. + * @dataProvider validationRulesDataProvider + */ + public function testMerge(String $validationRule, String $expectedValidation): void + { + $elements = [ + 'field' => [ + 'visible' => true, + 'formElement' => 'input', + 'label' => __('City'), + 'value' => null, + 'sortOrder' => 1, + 'validation' => [ + 'input_validation' => $validationRule + ], + ] + ]; + + $actualResult = $this->attributeMerger->merge( + $elements, + 'provider', + 'dataScope', + ['field' => + [ + 'validation' => ['length' => true] + ] + ] + ); + + $expectedResult = [ + $expectedValidation => true, + 'length' => true + ]; + + self::assertEquals($expectedResult, $actualResult['field']['validation']); + } + + /** + * Provides possible validation types. + * + * @return array + */ + public function validationRulesDataProvider(): array + { + return [ + ['alpha', 'validate-alpha'], + ['numeric', 'validate-number'], + ['alphanumeric', 'validate-alphanum'], + ['alphanum-with-spaces', 'validate-alphanum-with-spaces'], + ['url', 'validate-url'], + ['email', 'email2'], + ['length', 'validate-length'] + ]; + } +} diff --git a/app/code/Magento/Checkout/Test/Unit/Block/Checkout/LayoutProcessorTest.php b/app/code/Magento/Checkout/Test/Unit/Block/Checkout/LayoutProcessorTest.php index b3e55bb418d3..31ca2a203301 100644 --- a/app/code/Magento/Checkout/Test/Unit/Block/Checkout/LayoutProcessorTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Block/Checkout/LayoutProcessorTest.php @@ -10,15 +10,12 @@ use Magento\Checkout\Helper\Data; use Magento\Customer\Model\AttributeMetadataDataProvider; use Magento\Customer\Model\Options; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + use Magento\Ui\Component\Form\AttributeMapper; use PHPUnit_Framework_MockObject_MockObject as MockObject; /** - * LayoutProcessorTest covers a list of variations for - * checkout layout processor - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * LayoutProcessorTest covers a list of variations for checkout layout processor */ class LayoutProcessorTest extends \PHPUnit\Framework\TestCase { @@ -50,12 +47,10 @@ class LayoutProcessorTest extends \PHPUnit\Framework\TestCase /** * @var MockObject */ - private $storeResolver; + private $storeManager; protected function setUp() { - $objectManager = new ObjectManager($this); - $this->attributeDataProvider = $this->getMockBuilder(AttributeMetadataDataProvider::class) ->disableOriginalConstructor() ->setMethods(['loadAttributesCollection']) @@ -80,17 +75,21 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); + $shippingConfig = $this->getMockBuilder(\Magento\Shipping\Model\Config::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->storeManager = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); + $this->layoutProcessor = new LayoutProcessor( $this->attributeDataProvider, $this->attributeMapper, - $this->attributeMerger + $this->attributeMerger, + $options, + $this->dataHelper, + $shippingConfig, + $this->storeManager ); - - $this->storeResolver = $this->createMock(\Magento\Store\Api\StoreResolverInterface::class); - - $objectManager->setBackwardCompatibleProperty($this->layoutProcessor, 'checkoutDataHelper', $this->dataHelper); - $objectManager->setBackwardCompatibleProperty($this->layoutProcessor, 'options', $options); - $objectManager->setBackwardCompatibleProperty($this->layoutProcessor, 'storeResolver', $this->storeResolver); } /** @@ -277,7 +276,7 @@ private function getBillingComponent($paymentCode) 'telephone' => [ 'config' => [ 'tooltip' => [ - 'description' => __('For delivery questions.'), + 'description' => ('For delivery questions.'), ], ], ], diff --git a/app/code/Magento/Checkout/Test/Unit/Block/OnepageTest.php b/app/code/Magento/Checkout/Test/Unit/Block/OnepageTest.php index 54f77c95148a..b54339aa2c1d 100644 --- a/app/code/Magento/Checkout/Test/Unit/Block/OnepageTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Block/OnepageTest.php @@ -35,7 +35,7 @@ class OnepageTest extends \PHPUnit\Framework\TestCase /** * @var \PHPUnit_Framework_MockObject_MockObject */ - private $serializer; + private $serializerMock; protected function setUp() { @@ -49,7 +49,7 @@ protected function setUp() \Magento\Checkout\Block\Checkout\LayoutProcessorInterface::class ); - $this->serializer = $this->createMock(\Magento\Framework\Serialize\Serializer\Json::class); + $this->serializerMock = $this->createMock(\Magento\Framework\Serialize\Serializer\JsonHexTag::class); $this->model = new \Magento\Checkout\Block\Onepage( $contextMock, @@ -57,7 +57,8 @@ protected function setUp() $this->configProviderMock, [$this->layoutProcessorMock], [], - $this->serializer + $this->serializerMock, + $this->serializerMock ); } @@ -93,6 +94,7 @@ public function testGetJsLayout() $processedLayout = ['layout' => ['processed' => true]]; $jsonLayout = '{"layout":{"processed":true}}'; $this->layoutProcessorMock->expects($this->once())->method('process')->with([])->willReturn($processedLayout); + $this->serializerMock->expects($this->once())->method('serialize')->willReturn($jsonLayout); $this->assertEquals($jsonLayout, $this->model->getJsLayout()); } @@ -101,6 +103,7 @@ public function testGetSerializedCheckoutConfig() { $checkoutConfig = ['checkout', 'config']; $this->configProviderMock->expects($this->once())->method('getConfig')->willReturn($checkoutConfig); + $this->serializerMock->expects($this->once())->method('serialize')->willReturn(json_encode($checkoutConfig)); $this->assertEquals(json_encode($checkoutConfig), $this->model->getSerializedCheckoutConfig()); } diff --git a/app/code/Magento/Checkout/Test/Unit/CustomerData/CartTest.php b/app/code/Magento/Checkout/Test/Unit/CustomerData/CartTest.php index 75e181cbabd0..e3e13cc5b1e6 100644 --- a/app/code/Magento/Checkout/Test/Unit/CustomerData/CartTest.php +++ b/app/code/Magento/Checkout/Test/Unit/CustomerData/CartTest.php @@ -113,7 +113,7 @@ public function testGetSectionData() $storeMock = $this->createPartialMock(\Magento\Store\Model\System\Store::class, ['getWebsiteId']); $storeMock->expects($this->once())->method('getWebsiteId')->willReturn($websiteId); - $quoteMock->expects($this->once())->method('getStore')->willReturn($storeMock); + $quoteMock->expects($this->any())->method('getStore')->willReturn($storeMock); $productMock = $this->createPartialMock( \Magento\Catalog\Model\Product::class, @@ -162,6 +162,7 @@ public function testGetSectionData() 'isGuestCheckoutAllowed' => 1, 'website_id' => $websiteId, 'subtotalAmount' => 200, + 'storeId' => null ]; $this->assertEquals($expectedResult, $this->model->getSectionData()); } @@ -199,7 +200,7 @@ public function testGetSectionDataWithCompositeProduct() $storeMock = $this->createPartialMock(\Magento\Store\Model\System\Store::class, ['getWebsiteId']); $storeMock->expects($this->once())->method('getWebsiteId')->willReturn($websiteId); - $quoteMock->expects($this->once())->method('getStore')->willReturn($storeMock); + $quoteMock->expects($this->any())->method('getStore')->willReturn($storeMock); $this->checkoutCartMock->expects($this->once())->method('getSummaryQty')->willReturn($summaryQty); $this->checkoutHelperMock->expects($this->once()) @@ -265,6 +266,7 @@ public function testGetSectionDataWithCompositeProduct() 'isGuestCheckoutAllowed' => 1, 'website_id' => $websiteId, 'subtotalAmount' => 200, + 'storeId' => null ]; $this->assertEquals($expectedResult, $this->model->getSectionData()); } diff --git a/app/code/Magento/Checkout/Test/Unit/Helper/DataTest.php b/app/code/Magento/Checkout/Test/Unit/Helper/DataTest.php index 53132ffaa748..089ea15726c6 100644 --- a/app/code/Magento/Checkout/Test/Unit/Helper/DataTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Helper/DataTest.php @@ -179,7 +179,7 @@ public function testConvertPrice() public function testCanOnepageCheckout() { - $this->scopeConfig->expects($this->once())->method('getValue')->with( + $this->scopeConfig->expects($this->once())->method('isSetFlag')->with( 'checkout/options/onepage_checkout_enabled', 'store' )->will($this->returnValue(true)); diff --git a/app/code/Magento/Checkout/Test/Unit/Model/CartTest.php b/app/code/Magento/Checkout/Test/Unit/Model/CartTest.php index 6bd0bdf258a0..bc66324c2986 100644 --- a/app/code/Magento/Checkout/Test/Unit/Model/CartTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Model/CartTest.php @@ -318,6 +318,12 @@ public function testAddProduct($productInfo, $requestInfo) $this->productRepository->expects($this->any()) ->method('getById') ->will($this->returnValue($product)); + + $this->eventManagerMock->expects($this->at(0))->method('dispatch')->with( + 'checkout_cart_product_add_before', + ['info' => $requestInfo, 'product' => $product] + ); + $this->quoteMock->expects($this->once()) ->method('addProduct') ->will($this->returnValue(1)); @@ -325,7 +331,7 @@ public function testAddProduct($productInfo, $requestInfo) ->method('getQuote') ->will($this->returnValue($this->quoteMock)); - $this->eventManagerMock->expects($this->at(0))->method('dispatch')->with( + $this->eventManagerMock->expects($this->at(1))->method('dispatch')->with( 'checkout_cart_product_add_after', ['quote_item' => 1, 'product' => $product] ); @@ -363,6 +369,12 @@ public function testAddProductException() $this->productRepository->expects($this->any()) ->method('getById') ->will($this->returnValue($product)); + + $this->eventManagerMock->expects($this->once())->method('dispatch')->with( + 'checkout_cart_product_add_before', + ['info' => 4, 'product' => $product] + ); + $this->quoteMock->expects($this->once()) ->method('addProduct') ->will($this->returnValue('error')); @@ -370,10 +382,6 @@ public function testAddProductException() ->method('getQuote') ->will($this->returnValue($this->quoteMock)); - $this->eventManagerMock->expects($this->never())->method('dispatch')->with( - 'checkout_cart_product_add_after', - ['quote_item' => 1, 'product' => $product] - ); $this->expectException(\Magento\Framework\Exception\LocalizedException::class); $this->cart->addProduct(4, 4); } @@ -399,6 +407,11 @@ public function testAddProductExceptionBadParams() ->method('getById') ->will($this->returnValue($product)); + $this->eventManagerMock->expects($this->never())->method('dispatch')->with( + 'checkout_cart_product_add_before', + ['info' => 'bad', 'product' => $product] + ); + $this->eventManagerMock->expects($this->never())->method('dispatch')->with( 'checkout_cart_product_add_after', ['quote_item' => 1, 'product' => $product] diff --git a/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php b/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php index 853ae0157e64..1de0ebce10f5 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/Test/Unit/Model/PaymentInformationManagementTest.php b/app/code/Magento/Checkout/Test/Unit/Model/PaymentInformationManagementTest.php index 77c15fccfa8a..ea841e86586b 100644 --- a/app/code/Magento/Checkout/Test/Unit/Model/PaymentInformationManagementTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Model/PaymentInformationManagementTest.php @@ -172,9 +172,11 @@ private function getMockForAssignBillingAddress($cartId, $billingAddressMock) $billingAddressId = 1; $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); $quoteBillingAddress = $this->createMock(\Magento\Quote\Model\Quote\Address::class); + $shippingRate = $this->createPartialMock(\Magento\Quote\Model\Quote\Address\Rate::class, []); + $shippingRate->setCarrier('flatrate'); $quoteShippingAddress = $this->createPartialMock( \Magento\Quote\Model\Quote\Address::class, - ['setLimitCarrier', 'getShippingMethod'] + ['setLimitCarrier', 'getShippingMethod', 'getShippingRateByCode'] ); $this->cartRepositoryMock->expects($this->any())->method('getActive')->with($cartId)->willReturn($quoteMock); $quoteMock->expects($this->once())->method('getBillingAddress')->willReturn($quoteBillingAddress); @@ -183,6 +185,7 @@ private function getMockForAssignBillingAddress($cartId, $billingAddressMock) $quoteMock->expects($this->once())->method('removeAddress')->with($billingAddressId); $quoteMock->expects($this->once())->method('setBillingAddress')->with($billingAddressMock); $quoteMock->expects($this->once())->method('setDataChanges')->willReturnSelf(); + $quoteShippingAddress->expects($this->any())->method('getShippingRateByCode')->willReturn($shippingRate); $quoteShippingAddress->expects($this->any())->method('getShippingMethod')->willReturn('flatrate_flatrate'); $quoteShippingAddress->expects($this->once())->method('setLimitCarrier')->with('flatrate')->willReturnSelf(); } diff --git a/app/code/Magento/Checkout/Test/Unit/Model/SidebarTest.php b/app/code/Magento/Checkout/Test/Unit/Model/SidebarTest.php index a196b10478c7..ff7340f87f32 100644 --- a/app/code/Magento/Checkout/Test/Unit/Model/SidebarTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Model/SidebarTest.php @@ -98,7 +98,7 @@ public function testCheckQuoteItem() /** * @expectedException \Magento\Framework\Exception\LocalizedException - * @exceptedExceptionMessage The quote item isn't found. Verify the item and try again. + * @expectedExceptionMessage The quote item isn't found. Verify the item and try again. */ public function testCheckQuoteItemWithException() { diff --git a/app/code/Magento/Checkout/Test/Unit/Observer/SalesQuoteSaveAfterObserverTest.php b/app/code/Magento/Checkout/Test/Unit/Observer/SalesQuoteSaveAfterObserverTest.php index 6070bb5d424c..dabaf173d90b 100644 --- a/app/code/Magento/Checkout/Test/Unit/Observer/SalesQuoteSaveAfterObserverTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Observer/SalesQuoteSaveAfterObserverTest.php @@ -30,13 +30,14 @@ protected function setUp() public function testSalesQuoteSaveAfter() { + $quoteId = 7; $observer = $this->createMock(\Magento\Framework\Event\Observer::class); $observer->expects($this->once())->method('getEvent')->will( $this->returnValue(new \Magento\Framework\DataObject( - ['quote' => new \Magento\Framework\DataObject(['is_checkout_cart' => 1, 'id' => 7])] + ['quote' => new \Magento\Framework\DataObject(['is_checkout_cart' => 1, 'id' => $quoteId])] )) ); - $this->checkoutSession->expects($this->once())->method('getQuoteId')->with(7); + $this->checkoutSession->expects($this->once())->method('setQuoteId')->with($quoteId); $this->object->execute($observer); } diff --git a/app/code/Magento/Checkout/etc/di.xml b/app/code/Magento/Checkout/etc/di.xml index 71dfd12bb477..4ebd594a2856 100644 --- a/app/code/Magento/Checkout/etc/di.xml +++ b/app/code/Magento/Checkout/etc/di.xml @@ -49,7 +49,4 @@ </argument> </arguments> </type> - <type name="Magento\Quote\Model\Quote"> - <plugin name="clear_addresses_after_product_delete" type="Magento\Checkout\Plugin\Model\Quote\ResetQuoteAddresses"/> - </type> </config> diff --git a/app/code/Magento/Checkout/etc/frontend/di.xml b/app/code/Magento/Checkout/etc/frontend/di.xml index f2aced01b394..8f35fe9f37ab 100644 --- a/app/code/Magento/Checkout/etc/frontend/di.xml +++ b/app/code/Magento/Checkout/etc/frontend/di.xml @@ -59,6 +59,7 @@ <item name="totalsSortOrder" xsi:type="object">Magento\Checkout\Block\Checkout\TotalsProcessor</item> <item name="directoryData" xsi:type="object">Magento\Checkout\Block\Checkout\DirectoryDataProcessor</item> </argument> + <argument name="serializer" xsi:type="object">Magento\Framework\Serialize\Serializer\JsonHexTag</argument> </arguments> </type> <type name="Magento\Checkout\Block\Cart\Totals"> @@ -83,4 +84,19 @@ </argument> </arguments> </type> + <type name="Magento\Framework\View\Element\Message\MessageConfigurationsPool"> + <arguments> + <argument name="configurationsMap" xsi:type="array"> + <item name="addCartSuccessMessage" xsi:type="array"> + <item name="renderer" xsi:type="const">\Magento\Framework\View\Element\Message\Renderer\BlockRenderer::CODE</item> + <item name="data" xsi:type="array"> + <item name="template" xsi:type="string">Magento_Checkout::messages/addCartSuccessMessage.phtml</item> + </item> + </item> + </argument> + </arguments> + </type> + <type name="Magento\Quote\Model\Quote"> + <plugin name="clear_addresses_after_product_delete" type="Magento\Checkout\Plugin\Model\Quote\ResetQuoteAddresses"/> + </type> </config> diff --git a/app/code/Magento/Checkout/etc/frontend/sections.xml b/app/code/Magento/Checkout/etc/frontend/sections.xml index 35733a6119a2..90c2878f501c 100644 --- a/app/code/Magento/Checkout/etc/frontend/sections.xml +++ b/app/code/Magento/Checkout/etc/frontend/sections.xml @@ -46,7 +46,6 @@ </action> <action name="rest/*/V1/guest-carts/*/payment-information"> <section name="cart"/> - <section name="checkout-data"/> </action> <action name="rest/*/V1/guest-carts/*/selected-payment-method"> <section name="cart"/> diff --git a/app/code/Magento/Checkout/i18n/en_US.csv b/app/code/Magento/Checkout/i18n/en_US.csv index a6ea2c13579a..7f2f0b439032 100644 --- a/app/code/Magento/Checkout/i18n/en_US.csv +++ b/app/code/Magento/Checkout/i18n/en_US.csv @@ -182,3 +182,4 @@ Payment,Payment "Items in Cart","Items in Cart" "Close","Close" "Show Cross-sell Items in the Shopping Cart","Show Cross-sell Items in the Shopping Cart" +"You added %1 to your <a href=""%2"">shopping cart</a>.","You added %1 to your <a href=""%2"">shopping cart</a>." \ No newline at end of file diff --git a/app/code/Magento/Checkout/view/adminhtml/email/failed_payment.html b/app/code/Magento/Checkout/view/adminhtml/email/failed_payment.html index fb55f9b601dc..03ad7d9e8d84 100644 --- a/app/code/Magento/Checkout/view/adminhtml/email/failed_payment.html +++ b/app/code/Magento/Checkout/view/adminhtml/email/failed_payment.html @@ -23,43 +23,43 @@ <h1>{{trans "Payment Transaction Failed"}}</h1> <ul> <li> - <b>{{trans "Reason"}}</b><br /> + <strong>{{trans "Reason"}}</strong><br /> {{var reason}} </li> <li> - <b>{{trans "Checkout Type"}}</b><br /> + <strong>{{trans "Checkout Type"}}</strong><br /> {{var checkoutType}} </li> <li> - <b>{{trans "Customer:"}}</b><br /> + <strong>{{trans "Customer:"}}</strong><br /> <a href="mailto:{{var customerEmail}}">{{var customer}}</a> <{{var customerEmail}}> </li> <li> - <b>{{trans "Items"}}</b><br /> + <strong>{{trans "Items"}}</strong><br /> {{var items|raw}} </li> <li> - <b>{{trans "Total:"}}</b><br /> + <strong>{{trans "Total:"}}</strong><br /> {{var total}} </li> <li> - <b>{{trans "Billing Address:"}}</b><br /> + <strong>{{trans "Billing Address:"}}</strong><br /> {{var billingAddress.format('html')|raw}} </li> <li> - <b>{{trans "Shipping Address:"}}</b><br /> + <strong>{{trans "Shipping Address:"}}</strong><br /> {{var shippingAddress.format('html')|raw}} </li> <li> - <b>{{trans "Shipping Method:"}}</b><br /> + <strong>{{trans "Shipping Method:"}}</strong><br /> {{var shippingMethod}} </li> <li> - <b>{{trans "Payment Method:"}}</b><br /> + <strong>{{trans "Payment Method:"}}</strong><br /> {{var paymentMethod}} </li> <li> - <b>{{trans "Date & Time:"}}</b><br /> + <strong>{{trans "Date & Time:"}}</strong><br /> {{var dateAndTime}} </li> </ul> diff --git a/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml b/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml index d4fadedf5d7a..a305413bcf1f 100644 --- a/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml +++ b/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml @@ -105,7 +105,7 @@ <item name="trigger" xsi:type="string">opc-new-shipping-address</item> <item name="buttons" xsi:type="array"> <item name="save" xsi:type="array"> - <item name="text" xsi:type="string" translate="true">Save Address</item> + <item name="text" xsi:type="string" translate="true">Ship here</item> <item name="class" xsi:type="string">action primary action-save-address</item> </item> <item name="cancel" xsi:type="array"> @@ -404,6 +404,10 @@ <item name="component" xsi:type="string">Magento_Checkout/js/view/summary/item/details/subtotal</item> <item name="displayArea" xsi:type="string">after_details</item> </item> + <item name="message" xsi:type="array"> + <item name="component" xsi:type="string">Magento_Checkout/js/view/summary/item/details/message</item> + <item name="displayArea" xsi:type="string">item_message</item> + </item> </item> </item> </item> 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 71b1392d5391..84ab9b13d8f3 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') ?> @@ -24,9 +25,9 @@ <?php endif ?> <table id="shopping-cart-table" class="cart items data table" - data-mage-init='{"shoppingCart":{"emptyCartButton": "action.clear", + data-mage-init='{"shoppingCart":{"emptyCartButton": ".action.clear", "updateCartActionContainer": "#update_cart_action_container"}}'> - <caption role="heading" aria-level="2" class="table-caption"><?= /* @escapeNotVerified */ __('Shopping Cart Items') ?></caption> + <caption class="table-caption"><?= /* @escapeNotVerified */ __('Shopping Cart Items') ?></caption> <thead> <tr> <th class="col item" scope="col"><span><?= /* @escapeNotVerified */ __('Item') ?></span></th> 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 c1db2f7775ca..bfb7ddc55cda 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/item/configure/updatecart.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/item/configure/updatecart.phtml @@ -20,6 +20,7 @@ <input type="number" name="qty" id="qty" + min="0" value="" title="<?= /* @escapeNotVerified */ __('Qty') ?>" class="input-text qty" diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/item/default.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/item/default.phtml index 0567c61f0db6..d15794fb761b 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/item/default.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/item/default.phtml @@ -49,7 +49,7 @@ $canApplyMsrp = $helper->isShowBeforeOrderConfirm($product) && $helper->isMinima <?php if (isset($_formatedOptionValue['full_view'])): ?> <?= /* @escapeNotVerified */ $_formatedOptionValue['full_view'] ?> <?php else: ?> - <?= /* @escapeNotVerified */ $_formatedOptionValue['value'] ?> + <?= $block->escapeHtml($_formatedOptionValue['value'], ['span']) ?> <?php endif; ?> </dd> <?php endforeach; ?> @@ -84,20 +84,20 @@ $canApplyMsrp = $helper->isShowBeforeOrderConfirm($product) && $helper->isMinima <?php endif; ?> <td class="col qty" data-th="<?= $block->escapeHtml(__('Qty')) ?>"> <div class="field qty"> - <label class="label" for="cart-<?= /* @escapeNotVerified */ $_item->getId() ?>-qty"> - <span><?= /* @escapeNotVerified */ __('Qty') ?></span> - </label> <div class="control qty"> - <input id="cart-<?= /* @escapeNotVerified */ $_item->getId() ?>-qty" - name="cart[<?= /* @escapeNotVerified */ $_item->getId() ?>][qty]" - data-cart-item-id="<?= $block->escapeHtml($_item->getSku()) ?>" - value="<?= /* @escapeNotVerified */ $block->getQty() ?>" - type="number" - size="4" - title="<?= $block->escapeHtml(__('Qty')) ?>" - class="input-text qty" - data-validate="{required:true,'validate-greater-than-zero':true}" - data-role="cart-item-qty"/> + <label for="cart-<?= /* @escapeNotVerified */ $_item->getId() ?>-qty"> + <span class="label"><?= /* @escapeNotVerified */ __('Qty') ?></span> + <input id="cart-<?= /* @escapeNotVerified */ $_item->getId() ?>-qty" + name="cart[<?= /* @escapeNotVerified */ $_item->getId() ?>][qty]" + data-cart-item-id="<?= $block->escapeHtml($_item->getSku()) ?>" + value="<?= /* @escapeNotVerified */ $block->getQty() ?>" + type="number" + size="4" + title="<?= $block->escapeHtml(__('Qty')) ?>" + class="input-text qty" + data-validate="{required:true,'validate-greater-than-zero':true}" + data-role="cart-item-qty"/> + </label> </div> </div> </td> @@ -111,7 +111,7 @@ $canApplyMsrp = $helper->isShowBeforeOrderConfirm($product) && $helper->isMinima </td> </tr> <tr class="item-actions"> - <td colspan="100"> + <td colspan="4"> <div class="actions-toolbar"> <?= /* @escapeNotVerified */ $block->getActions($_item) ?> </div> diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/item/renderer/actions/edit.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/item/renderer/actions/edit.phtml index 1876cf2edb78..da0a83f05ef6 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/item/renderer/actions/edit.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/item/renderer/actions/edit.phtml @@ -12,8 +12,6 @@ <a class="action action-edit" href="<?= /* @escapeNotVerified */ $block->getConfigureUrl() ?>" title="<?= $block->escapeHtml(__('Edit item parameters')) ?>"> - <span> - <?= /* @escapeNotVerified */ __('Edit') ?> - </span> - </a> + <span><?= /* @escapeNotVerified */ __('Edit') ?></span> + </a> <?php endif ?> diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/minicart.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/minicart.phtml index e6d0260cf230..20be9cd010c6 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/minicart.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/minicart.phtml @@ -41,6 +41,14 @@ </div> <?= $block->getChildHtml('minicart.addons') ?> </div> + <?php else: ?> + <script> + require(['jquery'], function ($) { + $('a.action.showcart').click(function() { + $(document.body).trigger('processStart'); + }); + }); + </script> <?php endif ?> <script> window.checkout = <?= /* @escapeNotVerified */ $block->getSerializedConfig() ?>; 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 1c0c221a550c..67ac4a933556 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/templates/messages/addCartSuccessMessage.phtml b/app/code/Magento/Checkout/view/frontend/templates/messages/addCartSuccessMessage.phtml new file mode 100644 index 000000000000..e835037b5fcb --- /dev/null +++ b/app/code/Magento/Checkout/view/frontend/templates/messages/addCartSuccessMessage.phtml @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +// @codingStandardsIgnoreFile +/** @var \Magento\Framework\View\Element\Template $block */ +?> + +<?= $block->escapeHtml(__( + 'You added %1 to your <a href="%2">shopping cart</a>.', + $block->getData('product_name'), + $block->getData('cart_url') +), ['a']); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/action/create-billing-address.js b/app/code/Magento/Checkout/view/frontend/web/js/action/create-billing-address.js index 7db0dc5ce747..c601bb8acf12 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/action/create-billing-address.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/action/create-billing-address.js @@ -12,6 +12,17 @@ define([ 'use strict'; return function (addressData) { - return addressConverter.formAddressDataToQuoteAddress(addressData); + var address = addressConverter.formAddressDataToQuoteAddress(addressData); + + /** + * Returns new customer billing address type. + * + * @returns {String} + */ + address.getType = function () { + return 'new-customer-billing-address'; + }; + + return address; }; }); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/action/update-shopping-cart.js b/app/code/Magento/Checkout/view/frontend/web/js/action/update-shopping-cart.js index ce1527b3d72d..1920bc4d7ac4 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/action/update-shopping-cart.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/action/update-shopping-cart.js @@ -14,7 +14,8 @@ define([ $.widget('mage.updateShoppingCart', { options: { validationURL: '', - eventName: 'updateCartItemQty' + eventName: 'updateCartItemQty', + updateCartActionContainer: '' }, /** @inheritdoc */ @@ -31,7 +32,9 @@ define([ * @return {Boolean} */ onSubmit: function (event) { - if (!this.options.validationURL) { + var action = this.element.find(this.options.updateCartActionContainer).val(); + + if (!this.options.validationURL || action === 'empty_cart') { return true; } diff --git a/app/code/Magento/Checkout/view/frontend/web/js/checkout-data.js b/app/code/Magento/Checkout/view/frontend/web/js/checkout-data.js index 22b37b2da0b2..1858ce946fb0 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/checkout-data.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/checkout-data.js @@ -10,7 +10,8 @@ */ define([ 'jquery', - 'Magento_Customer/js/customer-data' + 'Magento_Customer/js/customer-data', + 'jquery/jquery-storageapi' ], function ($, storage) { 'use strict'; @@ -23,6 +24,22 @@ define([ storage.set(cacheKey, data); }, + /** + * @return {*} + */ + initData = function () { + return { + 'selectedShippingAddress': null, //Selected shipping address pulled from persistence storage + 'shippingAddressFromData': null, //Shipping address pulled from persistence storage + 'newCustomerShippingAddress': null, //Shipping address pulled from persistence storage for customer + 'selectedShippingRate': null, //Shipping rate pulled from persistence storage + 'selectedPaymentMethod': null, //Payment method pulled from persistence storage + 'selectedBillingAddress': null, //Selected billing address pulled from persistence storage + 'billingAddressFromData': null, //Billing address pulled from persistence storage + 'newCustomerBillingAddress': null //Billing address pulled from persistence storage for new customer + }; + }, + /** * @return {*} */ @@ -30,17 +47,12 @@ define([ var data = storage.get(cacheKey)(); if ($.isEmptyObject(data)) { - data = { - 'selectedShippingAddress': null, //Selected shipping address pulled from persistence storage - 'shippingAddressFromData': null, //Shipping address pulled from persistence storage - 'newCustomerShippingAddress': null, //Shipping address pulled from persistence storage for customer - 'selectedShippingRate': null, //Shipping rate pulled from persistence storage - 'selectedPaymentMethod': null, //Payment method pulled from persistence storage - 'selectedBillingAddress': null, //Selected billing address pulled from persistence storage - 'billingAddressFromData': null, //Billing address pulled from persistence storage - 'newCustomerBillingAddress': null //Billing address pulled from persistence storage for new customer - }; - saveData(data); + data = $.initNamespaceStorage('mage-cache-storage').localStorage.get(cacheKey); + + if ($.isEmptyObject(data)) { + data = initData(); + saveData(data); + } } return data; diff --git a/app/code/Magento/Checkout/view/frontend/web/js/empty-cart.js b/app/code/Magento/Checkout/view/frontend/web/js/empty-cart.js new file mode 100644 index 000000000000..4b30ad807527 --- /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/address-converter.js b/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js index a1aacf6e8032..9b20a782c38d 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js @@ -58,6 +58,16 @@ define([ } delete addressData['region_id']; + if (addressData['custom_attributes']) { + addressData['custom_attributes'] = Object.entries(addressData['custom_attributes']) + .map(function (customAttribute) { + return { + 'attribute_code': customAttribute[0], + 'value': customAttribute[1] + }; + }); + } + return address(addressData); }, diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/cart/estimate-service.js b/app/code/Magento/Checkout/view/frontend/web/js/model/cart/estimate-service.js index 76e3d911e7d3..54e496131972 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/cart/estimate-service.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/cart/estimate-service.js @@ -14,55 +14,71 @@ define([ 'use strict'; var rateProcessors = [], - totalsProcessors = []; + totalsProcessors = [], - quote.shippingAddress.subscribe(function () { - var type = quote.shippingAddress().getType(); + /** + * Estimate totals for shipping address and update shipping rates. + */ + estimateTotalsAndUpdateRates = function () { + var type = quote.shippingAddress().getType(); - if ( - quote.isVirtual() || - window.checkoutConfig.activeCarriers && window.checkoutConfig.activeCarriers.length === 0 - ) { - // update totals block when estimated address was set - totalsProcessors['default'] = totalsDefaultProvider; - totalsProcessors[type] ? - totalsProcessors[type].estimateTotals(quote.shippingAddress()) : - totalsProcessors['default'].estimateTotals(quote.shippingAddress()); - } else { - // check if user data not changed -> load rates from cache - if (!cartCache.isChanged('address', quote.shippingAddress()) && - !cartCache.isChanged('cartVersion', customerData.get('cart')()['data_id']) && - cartCache.get('rates') + if ( + quote.isVirtual() || + window.checkoutConfig.activeCarriers && window.checkoutConfig.activeCarriers.length === 0 ) { - shippingService.setShippingRates(cartCache.get('rates')); + // update totals block when estimated address was set + totalsProcessors['default'] = totalsDefaultProvider; + totalsProcessors[type] ? + totalsProcessors[type].estimateTotals(quote.shippingAddress()) : + totalsProcessors['default'].estimateTotals(quote.shippingAddress()); + } else { + // check if user data not changed -> load rates from cache + if (!cartCache.isChanged('address', quote.shippingAddress()) && + !cartCache.isChanged('cartVersion', customerData.get('cart')()['data_id']) && + cartCache.get('rates') + ) { + shippingService.setShippingRates(cartCache.get('rates')); - return; + return; + } + + // update rates list when estimated address was set + rateProcessors['default'] = defaultProcessor; + rateProcessors[type] ? + rateProcessors[type].getRates(quote.shippingAddress()) : + rateProcessors['default'].getRates(quote.shippingAddress()); + + // save rates to cache after load + shippingService.getShippingRates().subscribe(function (rates) { + cartCache.set('rates', rates); + }); } + }, - // update rates list when estimated address was set - rateProcessors['default'] = defaultProcessor; - rateProcessors[type] ? - rateProcessors[type].getRates(quote.shippingAddress()) : - rateProcessors['default'].getRates(quote.shippingAddress()); + /** + * Estimate totals for shipping address. + */ + estimateTotalsShipping = function () { + totalsDefaultProvider.estimateTotals(quote.shippingAddress()); + }, - // save rates to cache after load - shippingService.getShippingRates().subscribe(function (rates) { - cartCache.set('rates', rates); - }); - } - }); - quote.shippingMethod.subscribe(function () { - totalsDefaultProvider.estimateTotals(quote.shippingAddress()); - }); - quote.billingAddress.subscribe(function () { - var type = quote.billingAddress().getType(); + /** + * Estimate totals for billing address. + */ + estimateTotalsBilling = function () { + var type = quote.billingAddress().getType(); + + if (quote.isVirtual()) { + // update totals block when estimated address was set + totalsProcessors['default'] = totalsDefaultProvider; + totalsProcessors[type] ? + totalsProcessors[type].estimateTotals(quote.billingAddress()) : + totalsProcessors['default'].estimateTotals(quote.billingAddress()); + } + }; - if (quote.isVirtual()) { - // update totals block when estimated address was set - totalsProcessors['default'] = totalsDefaultProvider; - totalsProcessors[type] ? - totalsProcessors[type].estimateTotals(quote.billingAddress()) : - totalsProcessors['default'].estimateTotals(quote.billingAddress()); - } - }); + quote.shippingAddress.subscribe(estimateTotalsAndUpdateRates); + quote.shippingMethod.subscribe(estimateTotalsShipping); + quote.billingAddress.subscribe(estimateTotalsBilling); + customerData.get('cart').subscribe(estimateTotalsShipping); }); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js b/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js index 73f4df567903..bc0ab59b622a 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/checkout-data-resolver.js @@ -60,14 +60,21 @@ define([ this.resolveBillingAddress(); } } - }, /** * Resolve shipping address. Used local storage */ resolveShippingAddress: function () { - var newCustomerShippingAddress = checkoutData.getNewCustomerShippingAddress(); + var newCustomerShippingAddress; + + if (!checkoutData.getShippingAddressFromData() && + window.checkoutConfig.shippingAddressFromData + ) { + checkoutData.setShippingAddressFromData(window.checkoutConfig.shippingAddressFromData); + } + + newCustomerShippingAddress = checkoutData.getNewCustomerShippingAddress(); if (newCustomerShippingAddress) { createShippingAddress(newCustomerShippingAddress); @@ -196,15 +203,24 @@ define([ * Resolve billing address. Used local storage */ resolveBillingAddress: function () { - var selectedBillingAddress = checkoutData.getSelectedBillingAddress(), - newCustomerBillingAddressData = checkoutData.getNewCustomerBillingAddress(); + var selectedBillingAddress, + newCustomerBillingAddressData; + + if (!checkoutData.getBillingAddressFromData() && + window.checkoutConfig.billingAddressFromData + ) { + checkoutData.setBillingAddressFromData(window.checkoutConfig.billingAddressFromData); + } + + selectedBillingAddress = checkoutData.getSelectedBillingAddress(); + newCustomerBillingAddressData = checkoutData.getNewCustomerBillingAddress(); if (selectedBillingAddress) { - if (selectedBillingAddress == 'new-customer-address' && newCustomerBillingAddressData) { //eslint-disable-line + if (selectedBillingAddress === 'new-customer-billing-address' && newCustomerBillingAddressData) { selectBillingAddress(createBillingAddress(newCustomerBillingAddressData)); } else { addressList.some(function (address) { - if (selectedBillingAddress == address.getKey()) { //eslint-disable-line eqeqeq + if (selectedBillingAddress === address.getKey()) { selectBillingAddress(address); } }); @@ -218,16 +234,31 @@ define([ * Apply resolved billing address to quote */ applyBillingAddress: function () { - var shippingAddress; + var shippingAddress, + isBillingAddressInitialized; if (quote.billingAddress()) { selectBillingAddress(quote.billingAddress()); return; } + + if (quote.isVirtual() || !quote.billingAddress()) { + isBillingAddressInitialized = addressList.some(function (addrs) { + if (addrs.isDefaultBilling()) { + selectBillingAddress(addrs); + + return true; + } + + return false; + }); + } + shippingAddress = quote.shippingAddress(); - if (shippingAddress && + if (!isBillingAddressInitialized && + shippingAddress && shippingAddress.canUseForBilling() && (shippingAddress.isDefaultShipping() || !quote.isVirtual()) ) { diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/error-processor.js b/app/code/Magento/Checkout/view/frontend/web/js/model/error-processor.js index 848a7daf71e1..42b692ff9dd8 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/error-processor.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/error-processor.js @@ -8,8 +8,9 @@ */ define([ 'mage/url', - 'Magento_Ui/js/model/messageList' -], function (url, globalMessageList) { + 'Magento_Ui/js/model/messageList', + 'mage/translate' +], function (url, globalMessageList, $t) { 'use strict'; return { @@ -25,7 +26,11 @@ define([ if (response.status == 401) { //eslint-disable-line eqeqeq window.location.replace(url.build('customer/account/login/')); } else { - error = JSON.parse(response.responseText); + try { + error = JSON.parse(response.responseText); + } catch (exception) { + error = $t('Something went wrong with your request. Please try again later.'); + } messageContainer.addErrorMessage(error); } } diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/new-customer-address.js b/app/code/Magento/Checkout/view/frontend/web/js/model/new-customer-address.js index a880bd423ab2..4ef39421440c 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/new-customer-address.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/new-customer-address.js @@ -22,6 +22,8 @@ define([ if (addressData.region && addressData.region['region_id']) { regionId = addressData.region['region_id']; + } else if (!addressData['region_id']) { + regionId = undefined; } else if ( /* eslint-disable */ addressData['country_id'] && addressData['country_id'] == window.checkoutConfig.defaultCountryId || diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/payment/additional-validators.js b/app/code/Magento/Checkout/view/frontend/web/js/model/payment/additional-validators.js index 1cb35a4cee2d..1337e1affd3d 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/payment/additional-validators.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/payment/additional-validators.js @@ -35,15 +35,17 @@ define([], function () { * * @returns {Boolean} */ - validate: function () { + validate: function (hideError) { var validationResult = true; + hideError = hideError || false; + if (validators.length <= 0) { return validationResult; } validators.forEach(function (item) { - if (item.validate() == false) { //eslint-disable-line eqeqeq + if (item.validate(hideError) == false) { //eslint-disable-line eqeqeq validationResult = false; return false; diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/place-order.js b/app/code/Magento/Checkout/view/frontend/web/js/model/place-order.js index c3c5b9d68cec..c07878fcaea9 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/place-order.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/place-order.js @@ -9,9 +9,10 @@ define( [ 'mage/storage', 'Magento_Checkout/js/model/error-processor', - 'Magento_Checkout/js/model/full-screen-loader' + 'Magento_Checkout/js/model/full-screen-loader', + 'Magento_Customer/js/customer-data' ], - function (storage, errorProcessor, fullScreenLoader) { + function (storage, errorProcessor, fullScreenLoader, customerData) { 'use strict'; return function (serviceUrl, payload, messageContainer) { @@ -23,6 +24,23 @@ define( function (response) { errorProcessor.process(response, messageContainer); } + ).success( + function (response) { + var clearData = { + 'selectedShippingAddress': null, + 'shippingAddressFromData': null, + 'newCustomerShippingAddress': null, + 'selectedShippingRate': null, + 'selectedPaymentMethod': null, + 'selectedBillingAddress': null, + 'billingAddressFromData': null, + 'newCustomerBillingAddress': null + }; + + if (response.responseType !== 'error') { + customerData.set('checkout-data', clearData); + } + } ).always( function () { fullScreenLoader.stopLoader(); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/postcode-validator.js b/app/code/Magento/Checkout/view/frontend/web/js/model/postcode-validator.js index a95471d90dab..0a5334a42c7e 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/postcode-validator.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/postcode-validator.js @@ -14,11 +14,13 @@ define([ /** * @param {*} postCode * @param {*} countryId + * @param {Array} postCodesPatterns * @return {Boolean} */ - validate: function (postCode, countryId) { - var patterns = window.checkoutConfig.postCodes[countryId], - pattern, regex; + validate: function (postCode, countryId, postCodesPatterns) { + var pattern, regex, + patterns = postCodesPatterns ? postCodesPatterns[countryId] : + window.checkoutConfig.postCodes[countryId]; this.validatedPostCodeExample = []; diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/quote.js b/app/code/Magento/Checkout/view/frontend/web/js/model/quote.js index 2510d1aced3d..3486a9273661 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/quote.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/quote.js @@ -7,7 +7,8 @@ */ define([ 'ko', - 'underscore' + 'underscore', + 'domReady!' ], function (ko, _) { 'use strict'; diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rates-validator.js b/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rates-validator.js index d31c0dca3811..8b07c02e4d38 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rates-validator.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rates-validator.js @@ -35,13 +35,14 @@ define([ var checkoutConfig = window.checkoutConfig, validators = [], observedElements = [], - postcodeElement = null, + postcodeElements = [], postcodeElementName = 'postcode'; validators.push(defaultValidator); return { validateAddressTimeout: 0, + validateZipCodeTimeout: 0, validateDelay: 2000, /** @@ -101,7 +102,7 @@ define([ if (element.index === postcodeElementName) { this.bindHandler(element, delay); - postcodeElement = element; + postcodeElements.push(element); } }, @@ -133,10 +134,20 @@ define([ }); } else { element.on('value', function () { + clearTimeout(self.validateZipCodeTimeout); + self.validateZipCodeTimeout = setTimeout(function () { + if (element.index === postcodeElementName) { + self.postcodeValidation(element); + } else { + $.each(postcodeElements, function (index, elem) { + self.postcodeValidation(elem); + }); + } + }, delay); + if (!formPopUpState.isVisible()) { clearTimeout(self.validateAddressTimeout); self.validateAddressTimeout = setTimeout(function () { - self.postcodeValidation(); self.validateFields(); }, delay); } @@ -148,8 +159,8 @@ define([ /** * @return {*} */ - postcodeValidation: function () { - var countryId = $('select[name="country_id"]').val(), + postcodeValidation: function (postcodeElement) { + var countryId = $('select[name="country_id"]:visible').val(), validationResult, warnMessage; @@ -178,8 +189,8 @@ define([ */ validateFields: function () { var addressFlat = addressConverter.formDataProviderToFlatData( - this.collectObservedData(), - 'shippingAddress' + this.collectObservedData(), + 'shippingAddress' ), address; 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 3ea49cd981d9..eecfa65b189d 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js @@ -14,7 +14,11 @@ define([ _create: function () { var items, i, reload; - $(this.options.emptyCartButton).on('click', $.proxy(function () { + $(this.options.emptyCartButton).on('click', $.proxy(function (event) { + if (event.detail === 0) { + return; + } + $(this.options.emptyCartButton).attr('name', 'update_cart_action_temp'); $(this.options.updateCartActionContainer) .attr('name', 'update_cart_action').attr('value', 'empty_cart'); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js b/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js index 3b5168453e11..e66c66006246 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js @@ -25,6 +25,7 @@ define([ } }, scrollHeight: 0, + shoppingCartUrl: window.checkout.shoppingCartUrl, /** * Create sidebar. @@ -226,7 +227,11 @@ define([ var productData = this._getProductById(Number(elem.data('cart-item'))); if (!_.isUndefined(productData)) { - $(document).trigger('ajax:updateCartItemQty', productData['product_sku']); + $(document).trigger('ajax:updateCartItemQty'); + + if (window.location.href === this.shoppingCartUrl) { + window.location.reload(false); + } } this._hideItemButton(elem); }, @@ -253,7 +258,9 @@ define([ var productData = this._getProductById(Number(elem.data('cart-item'))); if (!_.isUndefined(productData)) { - $(document).trigger('ajax:removeFromCart', productData['product_sku']); + $(document).trigger('ajax:removeFromCart', { + productIds: [productData['product_id']] + }); } }, 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 6b5d08c2641c..a552aa01da06 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 @@ -17,7 +17,8 @@ define([ 'Magento_Customer/js/customer-data', 'Magento_Checkout/js/action/set-billing-address', 'Magento_Ui/js/model/messageList', - 'mage/translate' + 'mage/translate', + 'Magento_Checkout/js/model/shipping-rates-validator' ], function ( ko, @@ -33,35 +34,29 @@ function ( customerData, setBillingAddressAction, globalMessageList, - $t + $t, + shippingRatesValidator ) { 'use strict'; var lastSelectedBillingAddress = null, - newAddressOption = { - /** - * Get new address label - * @returns {String} - */ - getAddressInline: function () { - return $t('New Address'); - }, - customerAddressId: null - }, countryData = customerData.get('directory-data'), addressOptions = addressList().filter(function (address) { - return address.getType() == 'customer-address'; //eslint-disable-line eqeqeq + return address.getType() === 'customer-address'; }); - addressOptions.push(newAddressOption); - return Component.extend({ defaults: { - template: 'Magento_Checkout/billing-address' + template: 'Magento_Checkout/billing-address', + actionsTemplate: 'Magento_Checkout/billing-address/actions', + formTemplate: 'Magento_Checkout/billing-address/form', + detailsTemplate: 'Magento_Checkout/billing-address/details', + links: { + isAddressFormVisible: '${$.billingAddressListProvider}:isNewAddressSelected' + } }, currentBillingAddress: quote.billingAddress, - addressOptions: addressOptions, - customerHasAddresses: addressOptions.length > 1, + customerHasAddresses: addressOptions.length > 0, /** * Init component @@ -71,6 +66,7 @@ function ( quote.paymentMethod.subscribe(function () { checkoutDataResolver.resolveBillingAddress(); }, this); + shippingRatesValidator.initFields(this.get('name') + '.form-fields'); }, /** @@ -81,7 +77,7 @@ function ( .observe({ selectedAddress: null, isAddressDetailsVisible: quote.billingAddress() != null, - isAddressFormVisible: !customer.isLoggedIn() || addressOptions.length === 1, + isAddressFormVisible: !customer.isLoggedIn() || !addressOptions.length, isAddressSameAsShipping: false, saveInAddressBook: 1 }); @@ -144,7 +140,7 @@ function ( updateAddress: function () { var addressData, newBillingAddress; - if (this.selectedAddress() && this.selectedAddress() != newAddressOption) { //eslint-disable-line eqeqeq + if (this.selectedAddress() && !this.isAddressFormVisible()) { selectBillingAddress(this.selectedAddress()); checkoutData.setSelectedBillingAddress(this.selectedAddress().getKey()); } else { @@ -199,6 +195,13 @@ function ( } }, + /** + * Manage cancel button visibility + */ + canUseCancelBillingAddress: ko.computed(function () { + return quote.billingAddress() || lastSelectedBillingAddress; + }), + /** * Restore billing address */ @@ -208,13 +211,6 @@ function ( } }, - /** - * @param {Object} address - */ - onAddressChange: function (address) { - this.isAddressFormVisible(address == newAddressOption); //eslint-disable-line eqeqeq - }, - /** * @param {Number} countryId * @return {*} diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address/list.js b/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address/list.js new file mode 100644 index 000000000000..ca3a267c0167 --- /dev/null +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address/list.js @@ -0,0 +1,77 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'uiComponent', + 'Magento_Customer/js/model/address-list', + 'mage/translate', + 'Magento_Customer/js/model/customer' +], function (Component, addressList, $t, customer) { + 'use strict'; + + var newAddressOption = { + /** + * Get new address label + * @returns {String} + */ + getAddressInline: function () { + return $t('New Address'); + }, + customerAddressId: null + }, + addressOptions = addressList().filter(function (address) { + return address.getType() === 'customer-address'; + }); + + return Component.extend({ + defaults: { + template: 'Magento_Checkout/billing-address', + selectedAddress: null, + isNewAddressSelected: false, + addressOptions: addressOptions, + exports: { + selectedAddress: '${ $.parentName }:selectedAddress' + } + }, + + /** + * @returns {Object} Chainable. + */ + initConfig: function () { + this._super(); + this.addressOptions.push(newAddressOption); + + return this; + }, + + /** + * @return {exports.initObservable} + */ + initObservable: function () { + this._super() + .observe('selectedAddress isNewAddressSelected') + .observe({ + isNewAddressSelected: !customer.isLoggedIn() || !addressOptions.length + }); + + return this; + }, + + /** + * @param {Object} address + * @return {*} + */ + addressOptionsText: function (address) { + return address.getAddressInline(); + }, + + /** + * @param {Object} address + */ + onAddressChange: function (address) { + this.isNewAddressSelected(address === newAddressOption); + } + }); +}); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/cart/shipping-estimation.js b/app/code/Magento/Checkout/view/frontend/web/js/view/cart/shipping-estimation.js index 39b5ea0299ab..a857d89a72b1 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/cart/shipping-estimation.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/cart/shipping-estimation.js @@ -55,6 +55,12 @@ define( checkoutDataResolver.resolveEstimationAddress(); address = quote.isVirtual() ? quote.billingAddress() : quote.shippingAddress(); + if (!address && quote.isVirtual()) { + address = addressConverter.formAddressDataToQuoteAddress( + checkoutData.getSelectedBillingAddress() + ); + } + if (address) { estimatedAddress = address.isEditable() ? addressConverter.quoteAddressToFormAddressData(address) : diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/form/element/email.js b/app/code/Magento/Checkout/view/frontend/web/js/view/form/element/email.js index 4a25778e754c..c0de643d3a22 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/form/element/email.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/form/element/email.js @@ -17,7 +17,16 @@ define([ ], function ($, Component, ko, customer, checkEmailAvailability, loginAction, quote, checkoutData, fullScreenLoader) { 'use strict'; - var validatedEmail = checkoutData.getValidatedEmailValue(); + var validatedEmail; + + if (!checkoutData.getValidatedEmailValue() && + window.checkoutConfig.validatedEmailValue + ) { + checkoutData.setInputFieldEmailValue(window.checkoutConfig.validatedEmailValue); + checkoutData.setValidatedEmailValue(window.checkoutConfig.validatedEmailValue); + } + + validatedEmail = checkoutData.getValidatedEmailValue(); if (validatedEmail && !customer.isLoggedIn()) { quote.guestEmail = validatedEmail; @@ -33,6 +42,9 @@ define([ listens: { email: 'emailHasChanged', emailFocused: 'validateEmail' + }, + ignoreTmpls: { + email: true } }, checkDelay: 2000, @@ -168,7 +180,7 @@ define([ }, /** - * Resolves an initial sate of a login form. + * Resolves an initial state of a login form. * * @returns {Boolean} - initial visibility state. */ diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js b/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js index a2f8c8c56ff3..5e29fa209a64 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js @@ -81,6 +81,7 @@ define([ maxItemsToDisplay: window.checkout.maxItemsToDisplay, cart: {}, + // jscs:disable requireCamelCaseOrUpperCaseIdentifiers /** * @override */ @@ -101,12 +102,16 @@ define([ self.isLoading(true); }); - if (cartData()['website_id'] !== window.checkout.websiteId) { + if (cartData().website_id !== window.checkout.websiteId || + cartData().store_id !== window.checkout.storeId + ) { customerData.reload(['cart'], false); } return this._super(); }, + //jscs:enable requireCamelCaseOrUpperCaseIdentifiers + isLoading: ko.observable(false), initSidebar: initSidebar, diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/payment.js b/app/code/Magento/Checkout/view/frontend/web/js/view/payment.js index c17e5e40d5c9..e8994c61b722 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/payment.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/payment.js @@ -66,9 +66,21 @@ define([ navigate: function () { var self = this; - getPaymentInformation().done(function () { - self.isVisible(true); - }); + if (!self.hasShippingMethod()) { + this.isVisible(false); + stepNavigator.setHash('shipping'); + } else { + getPaymentInformation().done(function () { + self.isVisible(true); + }); + } + }, + + /** + * @return {Boolean} + */ + hasShippingMethod: function () { + return window.checkoutConfig.selectedShippingMethod !== null; }, /** diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/progress-bar.js b/app/code/Magento/Checkout/view/frontend/web/js/view/progress-bar.js index 683a18d0e4ea..30ea9da1dd60 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/progress-bar.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/progress-bar.js @@ -23,11 +23,17 @@ define([ /** @inheritdoc */ initialize: function () { + var stepsValue; + this._super(); window.addEventListener('hashchange', _.bind(stepNavigator.handleHash, stepNavigator)); if (!window.location.hash) { - stepNavigator.setHash(stepNavigator.steps().sort(stepNavigator.sortItems)[0].code); + stepsValue = stepNavigator.steps(); + + if (stepsValue.length) { + stepNavigator.setHash(stepsValue.sort(stepNavigator.sortItems)[0].code); + } } stepNavigator.handleHash(); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js index a7cb7f7e7de8..c811d3a1e836 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js @@ -247,6 +247,8 @@ define([ */ setShippingInformation: function () { if (this.validateShippingInformation()) { + quote.billingAddress(null); + checkoutDataResolver.resolveBillingAddress(); setShippingInformationAction().done( function () { stepNavigator.next(); @@ -263,7 +265,11 @@ define([ addressData, loginFormSelector = 'form[data-role=email-with-possible-login]', emailValidationResult = customer.isLoggedIn(), - field; + field, + country = registry.get(this.parentName + '.shippingAddress.shipping-address-fieldset.country_id'), + countryIndexedOptions = country.indexedOptions, + option = countryIndexedOptions[quote.shippingAddress().countryId], + messageContainer = registry.get('checkout.errors').messageContainer; if (!quote.shippingMethod()) { this.errorValidationMessage( @@ -316,6 +322,16 @@ define([ shippingAddress['save_in_address_book'] = 1; } selectShippingAddress(shippingAddress); + } else if (customer.isLoggedIn() && + option && + option['is_region_required'] && + !quote.shippingAddress().region + ) { + messageContainer.addErrorMessage({ + message: $t('Please specify a regionId in shipping address.') + }); + + return false; } if (!emailValidationResult) { diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/summary/item/details/message.js b/app/code/Magento/Checkout/view/frontend/web/js/view/summary/item/details/message.js new file mode 100644 index 000000000000..ed41fd26c47e --- /dev/null +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/summary/item/details/message.js @@ -0,0 +1,30 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define(['uiComponent'], function (Component) { + 'use strict'; + + var quoteMessages = window.checkoutConfig.quoteMessages; + + return Component.extend({ + defaults: { + template: 'Magento_Checkout/summary/item/details/message' + }, + displayArea: 'item_message', + quoteMessages: quoteMessages, + + /** + * @param {Object} item + * @return {null} + */ + getMessage: function (item) { + if (this.quoteMessages[item['item_id']]) { + return this.quoteMessages[item['item_id']]; + } + + return null; + } + }); +}); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/summary/shipping.js b/app/code/Magento/Checkout/view/frontend/web/js/view/summary/shipping.js index 3fda26033925..10d49265e3bb 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/summary/shipping.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/summary/shipping.js @@ -21,14 +21,21 @@ define([ * @return {*} */ getShippingMethodTitle: function () { - var shippingMethod; + var shippingMethod = '', + shippingMethodTitle = ''; if (!this.isCalculated()) { return ''; } shippingMethod = quote.shippingMethod(); - return shippingMethod ? shippingMethod['carrier_title'] + ' - ' + shippingMethod['method_title'] : ''; + if (typeof shippingMethod['method_title'] !== 'undefined') { + shippingMethodTitle = ' - ' + shippingMethod['method_title']; + } + + return shippingMethod ? + shippingMethod['carrier_title'] + shippingMethodTitle : + shippingMethod['carrier_title']; }, /** diff --git a/app/code/Magento/Checkout/view/frontend/web/template/authentication.html b/app/code/Magento/Checkout/view/frontend/web/template/authentication.html index 406a7d899b67..5b8dde81dd93 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/authentication.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/authentication.html @@ -31,15 +31,15 @@ <div class="block block-customer-login" data-bind="attr: {'data-label': $t('or')}"> <div class="block-title"> - <strong id="block-customer-login-heading" - role="heading" - aria-level="2" - data-bind="i18n: 'Sign In'"></strong> + <strong id="block-customer-login-heading-checkout" + role="heading" + aria-level="2" + data-bind="i18n: 'Sign In'"></strong> </div> <!-- ko foreach: getRegion('messages') --> <!-- ko template: getTemplate() --><!-- /ko --> <!--/ko--> - <div class="block-content" aria-labelledby="block-customer-login-heading"> + <div class="block-content" aria-labelledby="block-customer-login-heading-checkout"> <form data-role="login" data-bind="submit:login" method="post"> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/billing-address.html b/app/code/Magento/Checkout/view/frontend/web/template/billing-address.html index 5f735fbb4daa..cabfcc9b3db0 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/billing-address.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/billing-address.html @@ -5,28 +5,18 @@ */ --> <div class="checkout-billing-address"> - <div class="billing-address-same-as-shipping-block field choice" data-bind="visible: canUseShippingAddress()"> <input type="checkbox" name="billing-address-same-as-shipping" data-bind="checked: isAddressSameAsShipping, click: useShippingAddress, attr: {id: 'billing-address-same-as-shipping-' + getCode($parent)}"/> <label data-bind="attr: {for: 'billing-address-same-as-shipping-' + getCode($parent)}"><span data-bind="i18n: 'My billing and shipping address are the same'"></span></label> </div> - - <!-- ko template: 'Magento_Checkout/billing-address/details' --><!-- /ko --> + <render args="detailsTemplate"/> <fieldset class="fieldset" data-bind="visible: !isAddressDetailsVisible()"> - <!-- ko template: 'Magento_Checkout/billing-address/list' --><!-- /ko --> - <!-- ko template: 'Magento_Checkout/billing-address/form' --><!-- /ko --> - <div class="actions-toolbar"> - <div class="primary"> - <button class="action action-update" type="button" data-bind="click: updateAddress"> - <span data-bind="i18n: 'Update'"></span> - </button> - <button class="action action-cancel" type="button" data-bind="click: cancelAddressEdit"> - <span data-bind="i18n: 'Cancel'"></span> - </button> - </div> + <each args="getRegion('billing-address-list')" render="" /> + <div data-bind="fadeVisible: isAddressFormVisible"> + <render args="formTemplate"/> </div> + <render args="actionsTemplate"/> </fieldset> - </div> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/billing-address/actions.html b/app/code/Magento/Checkout/view/frontend/web/template/billing-address/actions.html new file mode 100644 index 000000000000..860f340d3f7c --- /dev/null +++ b/app/code/Magento/Checkout/view/frontend/web/template/billing-address/actions.html @@ -0,0 +1,21 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="actions-toolbar"> + <div class="primary"> + <button class="action action-update" + type="button" + click="updateAddress"> + <span translate="'Update'"/> + </button> + <button class="action action-cancel" + type="button" + click="cancelAddressEdit" + visible="canUseCancelBillingAddress()"> + <span translate="'Cancel'"/> + </button> + </div> +</div> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html b/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html index fd994a4e8a95..ea521b3a8afd 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html @@ -4,28 +4,37 @@ * See COPYING.txt for license details. */ --> -<div class="billing-address-details" data-bind="if: isAddressDetailsVisible() && currentBillingAddress()"> - <!-- ko text: currentBillingAddress().prefix --><!-- /ko --> <!-- ko text: currentBillingAddress().firstname --><!-- /ko --> <!-- ko text: currentBillingAddress().middlename --><!-- /ko --> - <!-- ko text: currentBillingAddress().lastname --><!-- /ko --> <!-- ko text: currentBillingAddress().suffix --><!-- /ko --><br/> - <!-- ko text: _.values(currentBillingAddress().street).join(", ") --><!-- /ko --><br/> - <!-- ko text: currentBillingAddress().city --><!-- /ko -->, <span data-bind="html: currentBillingAddress().region"></span> <!-- ko text: currentBillingAddress().postcode --><!-- /ko --><br/> - <!-- ko text: getCountryName(currentBillingAddress().countryId) --><!-- /ko --><br/> - <!-- ko if: (currentBillingAddress().telephone) --> - <a data-bind="text: currentBillingAddress().telephone, attr: {'href': 'tel:' + currentBillingAddress().telephone}"></a> - <!-- /ko --><br/> - <!-- ko foreach: { data: currentBillingAddress().customAttributes, as: 'element' } --> - <!-- ko foreach: { data: Object.keys(element), as: 'attribute' } --> - <!-- ko if: (typeof element[attribute] === "object") --> - <!-- ko text: element[attribute].value --><!-- /ko --> - <!-- /ko --> - <!-- ko if: (typeof element[attribute] === "string") --> - <!-- ko text: element[attribute] --><!-- /ko --> - <!-- /ko --><br/> - <!-- /ko --> - <!-- /ko --> - <button type="button" +<div if="isAddressDetailsVisible() && currentBillingAddress()" class="billing-address-details"> + <text args="currentBillingAddress().prefix"/> <text args="currentBillingAddress().firstname"/> <text args="currentBillingAddress().middlename"/> + <text args="currentBillingAddress().lastname"/> <text args="currentBillingAddress().suffix"/><br/> + <text args="_.values(currentBillingAddress().street).join(', ')"/><br/> + <text args="currentBillingAddress().city "/>, <span text="currentBillingAddress().region"></span> <text args="currentBillingAddress().postcode"/><br/> + <text args="getCountryName(currentBillingAddress().countryId)"/><br/> + <a if="currentBillingAddress().telephone" attr="'href': 'tel:' + currentBillingAddress().telephone" text="currentBillingAddress().telephone"></a><br/> + + <each args="data: currentBillingAddress().customAttributes, as: 'element'"> + <each args="data: Object.keys(element), as: 'attribute'"> + <if args="typeof element[attribute] === 'object'"> + <if args="element[attribute].label"> + <text args="element[attribute].label"/> + </if> + <ifnot args="element[attribute].label"> + <if args="element[attribute].value"> + <text args="element[attribute].value"/> + </if> + </ifnot> + </if> + <if args="typeof element[attribute] === 'string'"> + <text args="element[attribute]"/> + </if><br/> + </each> + </each> + + <button visible="!isAddressSameAsShipping()" + type="button" class="action action-edit-address" - data-bind="visible: !isAddressSameAsShipping(), click: editAddress"> - <span data-bind="i18n: 'Edit'"></span> + click="editAddress"> + <span translate="'Edit'"></span> </button> </div> + diff --git a/app/code/Magento/Checkout/view/frontend/web/template/billing-address/form.html b/app/code/Magento/Checkout/view/frontend/web/template/billing-address/form.html index 54fe9a1f5939..e29ed99d17be 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/billing-address/form.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/billing-address/form.html @@ -4,7 +4,7 @@ * See COPYING.txt for license details. */ --> -<div class="billing-address-form" data-bind="fadeVisible: isAddressFormVisible"> +<div class="billing-address-form"> <!-- ko foreach: getRegion('before-fields') --> <!-- ko template: getTemplate() --><!-- /ko --> <!--/ko--> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/form/element/email.html b/app/code/Magento/Checkout/view/frontend/web/template/form/element/email.html index 8d6142e07fcf..6a784fa7a04c 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/form/element/email.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/form/element/email.html @@ -14,7 +14,7 @@ method="post"> <fieldset id="customer-email-fieldset" class="fieldset" data-bind="blockLoader: isLoading"> <div class="field required"> - <label class="label" for="customer-email"> + <label class="label" for="checkout-customer-email"> <span data-bind="i18n: 'Email Address'"></span> </label> <div class="control _with-tooltip"> @@ -26,7 +26,7 @@ mageInit: {'mage/trim-input':{}}" name="username" data-validate="{required:true, 'validate-email':true}" - id="customer-email" /> + id="checkout-customer-email" /> <!-- ko template: 'ui/form/element/helper/tooltip' --><!-- /ko --> <span class="note" data-bind="fadeVisible: isPasswordVisible() == false"><!-- ko i18n: 'You can create an account after checkout.'--><!-- /ko --></span> </div> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html b/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html index 2daca51a2f5d..fb128a891aea 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html @@ -97,7 +97,7 @@ </div> </div> - <div id="minicart-widgets" class="minicart-widgets"> + <div id="minicart-widgets" class="minicart-widgets" if="getRegion('promotion').length"> <each args="getRegion('promotion')" render=""/> </div> </div> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html b/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html index 357b0e550af0..41d442a76d51 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html @@ -45,7 +45,7 @@ <span data-bind="html: option.value.join('<br>')"></span> <!-- /ko --> <!-- ko ifnot: Array.isArray(option.value) --> - <span data-bind="html: option.value"></span> + <span data-bind="text: option.value"></span> <!-- /ko --> </dd> <!-- /ko --> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/shipping-address/address-renderer/default.html b/app/code/Magento/Checkout/view/frontend/web/template/shipping-address/address-renderer/default.html index 2e268461d1ee..cf64c0140b95 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/shipping-address/address-renderer/default.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/shipping-address/address-renderer/default.html @@ -4,33 +4,40 @@ * See COPYING.txt for license details. */ --> -<div class="shipping-address-item" data-bind="css: isSelected() ? 'selected-item' : 'not-selected-item'"> - <!-- ko text: address().prefix --><!-- /ko --> <!-- ko text: address().firstname --><!-- /ko --> <!-- ko text: address().middlename --><!-- /ko --> - <!-- ko text: address().lastname --><!-- /ko --> <!-- ko text: address().suffix --><!-- /ko --><br/> - <!-- ko text: _.values(address().street).join(", ") --><!-- /ko --><br/> - <!-- ko text: address().city --><!-- /ko -->, <span data-bind="html: address().region"></span> <!-- ko text: address().postcode --><!-- /ko --><br/> - <!-- ko text: getCountryName(address().countryId) --><!-- /ko --><br/> - <!-- ko if: (address().telephone) --> - <a data-bind="text: address().telephone, attr: {'href': 'tel:' + address().telephone}"></a> - <!-- /ko --><br/> - <!-- ko foreach: { data: address().customAttributes, as: 'element' } --> - <!-- ko foreach: { data: Object.keys(element), as: 'attribute' } --> - <!-- ko if: (typeof element[attribute] === "object") --> - <!-- ko text: element[attribute].value --><!-- /ko --> - <!-- /ko --> - <!-- ko if: (typeof element[attribute] === "string") --> - <!-- ko text: element[attribute] --><!-- /ko --> - <!-- /ko --><br/> - <!-- /ko --> - <!-- /ko --> - <!-- ko if: (address().isEditable()) --> - <button type="button" +<div class="shipping-address-item" css="'selected-item' : isSelected() , 'not-selected-item':!isSelected()"> + <text args="address().prefix"/> <text args="address().firstname"/> <text args="address().middlename"/> + <text args="address().lastname"/> <text args="address().suffix"/><br/> + <text args="_.values(address().street).join(', ')"/><br/> + <text args="address().city "/>, <span text="address().region"></span> <text args="address().postcode"/><br/> + <text args="getCountryName(address().countryId)"/><br/> + <a if="address().telephone" attr="'href': 'tel:' + address().telephone" text="address().telephone"></a><br/> + + <each args="data: address().customAttributes, as: 'element'"> + <each args="data: Object.keys(element), as: 'attribute'"> + <if args="typeof element[attribute] === 'object'"> + <if args="element[attribute].label"> + <text args="element[attribute].label"/> + </if> + <ifnot args="element[attribute].label"> + <if args="element[attribute].value"> + <text args="element[attribute].value"/> + </if> + </ifnot> + </if> + <if args="typeof element[attribute] === 'string'"> + <text args="element[attribute]"/> + </if><br/> + </each> + </each> + + <button visible="address().isEditable()" type="button" class="action edit-address-link" - data-bind="click: editAddress, visible: address().isEditable()"> - <span data-bind="i18n: 'Edit'"></span> + click="editAddress"> + <span translate="'Edit'"></span> </button> - <!-- /ko --> - <button type="button" data-bind="click: selectAddress" class="action action-select-shipping-item"> - <span data-bind="i18n: 'Ship Here'"></span> + <!-- ko if: (!isSelected()) --> + <button type="button" click="selectAddress" class="action action-select-shipping-item"> + <span translate="'Ship Here'"></span> </button> + <!-- /ko --> </div> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/shipping-information/address-renderer/default.html b/app/code/Magento/Checkout/view/frontend/web/template/shipping-information/address-renderer/default.html index b66526f660af..541413955cb4 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/shipping-information/address-renderer/default.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/shipping-information/address-renderer/default.html @@ -4,23 +4,29 @@ * See COPYING.txt for license details. */ --> -<!-- ko if: (visible()) --> - <!-- ko text: address().prefix --><!-- /ko --> <!-- ko text: address().firstname --><!-- /ko --> <!-- ko text: address().middlename --><!-- /ko --> - <!-- ko text: address().lastname --><!-- /ko --> <!-- ko text: address().suffix --><!-- /ko --><br/> - <!-- ko text: _.values(address().street).join(", ") --><!-- /ko --><br/> - <!-- ko text: address().city --><!-- /ko -->, <span data-bind="html: address().region"></span> <!-- ko text: address().postcode --><!-- /ko --><br/> - <!-- ko text: getCountryName(address().countryId) --><!-- /ko --><br/> - <!-- ko if: (address().telephone) --> - <a data-bind="text: address().telephone, attr: {'href': 'tel:' + address().telephone}"></a> - <!-- /ko --><br/> - <!-- ko foreach: { data: address().customAttributes, as: 'element' } --> - <!-- ko foreach: { data: Object.keys(element), as: 'attribute' } --> - <!-- ko if: (typeof element[attribute] === "object") --> - <!-- ko text: element[attribute].value --><!-- /ko --> - <!-- /ko --> - <!-- ko if: (typeof element[attribute] === "string") --> - <!-- ko text: element[attribute] --><!-- /ko --> - <!-- /ko --><br/> - <!-- /ko --> - <!-- /ko --> -<!-- /ko --> +<if args="visible()"> + <text args="address().prefix"/> <text args="address().firstname"/> <text args="address().middlename"/> + <text args="address().lastname"/> <text args="address().suffix"/><br/> + <text args="_.values(address().street).join(', ')"/><br/> + <text args="address().city "/>, <span text="address().region"></span> <text args="address().postcode"/><br/> + <text args="getCountryName(address().countryId)"/><br/> + <a if="address().telephone" attr="'href': 'tel:' + address().telephone" text="address().telephone"></a><br/> + + <each args="data: address().customAttributes, as: 'element'"> + <each args="data: Object.keys(element), as: 'attribute'"> + <if args="typeof element[attribute] === 'object'"> + <if args="element[attribute].label"> + <text args="element[attribute].label"/> + </if> + <ifnot args="element[attribute].label"> + <if args="element[attribute].value"> + <text args="element[attribute].value"/> + </if> + </ifnot> + </if> + <if args="typeof element[attribute] === 'string'"> + <text args="element[attribute]"/> + </if><br/> + </each> + </each> +</if> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/shipping.html b/app/code/Magento/Checkout/view/frontend/web/template/shipping.html index a1a5aa67a968..1fcfa4b3b134 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/shipping.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/shipping.html @@ -16,12 +16,14 @@ <!-- Address form pop up --> <if args="!isFormInline"> - <button type="button" - class="action action-show-popup" - click="showFormPopUp" - visible="!isNewAddressAdded()"> - <span translate="'New Address'" /> - </button> + <div class="new-address-popup"> + <button type="button" + class="action action-show-popup" + click="showFormPopUp" + visible="!isNewAddressAdded()"> + <span translate="'New Address'" /> + </button> + </div> <div id="opc-new-shipping-address" visible="isFormPopUpVisible()" render="shippingFormTemplate" /> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/summary/cart-items.html b/app/code/Magento/Checkout/view/frontend/web/template/summary/cart-items.html index 34ec91aa43c7..fc74a4691a2e 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/summary/cart-items.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/summary/cart-items.html @@ -6,7 +6,7 @@ --> <div class="block items-in-cart" data-bind="mageInit: {'collapsible':{'openedState': 'active', 'active': isItemsBlockExpanded()}}"> <div class="title" data-role="title"> - <strong role="heading"> + <strong role="heading" aria-level="1"> <translate args="maxCartItemsToDisplay" if="maxCartItemsToDisplay < getCartLineItemsCount()"/> <translate args="'of'" if="maxCartItemsToDisplay < getCartLineItemsCount()"/> <span data-bind="text: getCartLineItemsCount()"></span> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details.html b/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details.html index dd59bd78416c..2491ee12d263 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details.html @@ -43,3 +43,6 @@ </div> <!-- /ko --> </div> +<!-- ko foreach: getRegion('item_message') --> + <!-- ko template: getTemplate() --><!-- /ko --> +<!-- /ko --> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details/message.html b/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details/message.html new file mode 100644 index 000000000000..ea8f58cccd59 --- /dev/null +++ b/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details/message.html @@ -0,0 +1,9 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="cart item message notice" if="getMessage($parents[1])"> + <div data-bind="text: getMessage($parents[1])"></div> +</div> diff --git a/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Delete.php b/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Delete.php index f7b178df9962..d727107a86f4 100644 --- a/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Delete.php +++ b/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Delete.php @@ -12,8 +12,9 @@ use Magento\Framework\Registry; use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\App\Action\HttpPostActionInterface; -class Delete extends Agreement +class Delete extends Agreement implements HttpPostActionInterface { /** * @var CheckoutAgreementsRepositoryInterface diff --git a/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Edit.php b/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Edit.php index 8bec3b581cd5..1a82108a3da8 100644 --- a/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Edit.php +++ b/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Edit.php @@ -12,8 +12,9 @@ use Magento\Framework\Registry; use Magento\Framework\App\ObjectManager; use Magento\CheckoutAgreements\Block\Adminhtml\Agreement\Edit as BlockEdit; +use Magento\Framework\App\Action\HttpGetActionInterface; -class Edit extends Agreement +class Edit extends Agreement implements HttpGetActionInterface { /** * @var AgreementFactory diff --git a/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Index.php b/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Index.php index b1dfd8c304d7..d32ee7a4b752 100644 --- a/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Index.php +++ b/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Index.php @@ -6,7 +6,9 @@ */ namespace Magento\CheckoutAgreements\Controller\Adminhtml\Agreement; -class Index extends \Magento\CheckoutAgreements\Controller\Adminhtml\Agreement +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\CheckoutAgreements\Controller\Adminhtml\Agreement implements HttpGetActionInterface { /** * @return void diff --git a/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/NewAction.php b/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/NewAction.php index 4d482ebfc206..caa21c468230 100644 --- a/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/NewAction.php +++ b/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/NewAction.php @@ -6,7 +6,9 @@ */ namespace Magento\CheckoutAgreements\Controller\Adminhtml\Agreement; -class NewAction extends \Magento\CheckoutAgreements\Controller\Adminhtml\Agreement +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class NewAction extends \Magento\CheckoutAgreements\Controller\Adminhtml\Agreement implements HttpGetActionInterface { /** * @return void diff --git a/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Save.php b/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Save.php index 05a16d3dd426..4d4dd076ea16 100644 --- a/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Save.php +++ b/app/code/Magento/CheckoutAgreements/Controller/Adminhtml/Agreement/Save.php @@ -13,8 +13,9 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\App\Action\HttpPostActionInterface; -class Save extends Agreement +class Save extends Agreement implements HttpPostActionInterface { /** * @var AgreementFactory diff --git a/app/code/Magento/CheckoutAgreements/Model/AgreementsConfigProvider.php b/app/code/Magento/CheckoutAgreements/Model/AgreementsConfigProvider.php index 1f1b5be9683e..1217270d780e 100644 --- a/app/code/Magento/CheckoutAgreements/Model/AgreementsConfigProvider.php +++ b/app/code/Magento/CheckoutAgreements/Model/AgreementsConfigProvider.php @@ -67,17 +67,18 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getConfig() { $agreements = []; $agreements['checkoutAgreements'] = $this->getAgreementsConfig(); + return $agreements; } /** - * Returns agreements config + * Returns agreements config. * * @return array */ @@ -99,7 +100,7 @@ protected function getAgreementsConfig() 'content' => $agreement->getIsHtml() ? $agreement->getContent() : nl2br($this->escaper->escapeHtml($agreement->getContent())), - 'checkboxText' => $agreement->getCheckboxText(), + 'checkboxText' => $this->escaper->escapeHtml($agreement->getCheckboxText()), 'mode' => $agreement->getMode(), 'agreementId' => $agreement->getAgreementId() ]; diff --git a/app/code/Magento/CheckoutAgreements/Model/ResourceModel/Agreement/Grid/Collection.php b/app/code/Magento/CheckoutAgreements/Model/ResourceModel/Agreement/Grid/Collection.php index 70794d24a64e..bc055ca9f663 100644 --- a/app/code/Magento/CheckoutAgreements/Model/ResourceModel/Agreement/Grid/Collection.php +++ b/app/code/Magento/CheckoutAgreements/Model/ResourceModel/Agreement/Grid/Collection.php @@ -14,7 +14,7 @@ class Collection extends \Magento\CheckoutAgreements\Model\ResourceModel\Agreeme { /** - * {@inheritdoc} + * @inheritdoc */ public function load($printQuery = false, $logQuery = false) { @@ -30,6 +30,8 @@ public function load($printQuery = false, $logQuery = false) } /** + * Add stores to result + * * @return void */ private function addStoresToResult() @@ -56,6 +58,8 @@ private function addStoresToResult() } /** + * Get stores for agreements + * * @return array */ private function getStoresForAgreements() @@ -64,7 +68,7 @@ private function getStoresForAgreements() if (!empty($agreementId)) { $select = $this->getConnection()->select()->from( - ['agreement_store' => 'checkout_agreement_store'] + ['agreement_store' => $this->getResource()->getTable('checkout_agreement_store')] )->where( 'agreement_store.agreement_id IN (?)', $agreementId diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Data/AdminModuleData.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Data/AdminModuleData.xml new file mode 100644 index 000000000000..d42c2c813942 --- /dev/null +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Data/AdminModuleData.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminMenuStoresSettingsTermsAndConditions"> + <data key="pageTitle">Terms and Conditions</data> + <data key="title">Terms and Conditions</data> + <data key="dataUiId">magento-checkoutagreements-sales-checkoutagreement</data> + </entity> +</entities> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminStoresTermsAndConditionsNavigateMenuTest.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminStoresTermsAndConditionsNavigateMenuTest.xml new file mode 100644 index 000000000000..d2d4cb9138bd --- /dev/null +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminStoresTermsAndConditionsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminStoresTermsAndConditionsNavigateMenuTest"> + <annotations> + <features value="CheckoutAgreements"/> + <stories value="Menu Navigation"/> + <title value="Admin stores terms and conditions navigate menu test"/> + <description value="Admin should be able to navigate to Stores > Terms and Conditions"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14148"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToStoresTermsAndConditionsPage"> + <argument name="menuUiId" value="{{AdminMenuStores.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuStoresSettingsTermsAndConditions.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuStoresSettingsTermsAndConditions.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CheckoutAgreements/Test/Unit/Model/AgreementsConfigProviderTest.php b/app/code/Magento/CheckoutAgreements/Test/Unit/Model/AgreementsConfigProviderTest.php index c59a3d2433ec..c8309bacb0a8 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Unit/Model/AgreementsConfigProviderTest.php +++ b/app/code/Magento/CheckoutAgreements/Test/Unit/Model/AgreementsConfigProviderTest.php @@ -8,6 +8,9 @@ use Magento\CheckoutAgreements\Model\AgreementsProvider; use Magento\Store\Model\ScopeInterface; +/** + * Tests for AgreementsConfigProvider. + */ class AgreementsConfigProviderTest extends \PHPUnit\Framework\TestCase { /** @@ -35,6 +38,9 @@ class AgreementsConfigProviderTest extends \PHPUnit\Framework\TestCase */ private $agreementsFilterMock; + /** + * @inheritdoc + */ protected function setUp() { $this->scopeConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); @@ -59,10 +65,16 @@ protected function setUp() ); } + /** + * Test for getConfig if content is HTML. + * + * @return void + */ public function testGetConfigIfContentIsHtml() { $content = 'content'; $checkboxText = 'checkbox_text'; + $escapedCheckboxText = 'escaped_checkbox_text'; $mode = \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_AUTO; $agreementId = 100; $expectedResult = [ @@ -71,12 +83,12 @@ public function testGetConfigIfContentIsHtml() 'agreements' => [ [ 'content' => $content, - 'checkboxText' => $checkboxText, + 'checkboxText' => $escapedCheckboxText, 'mode' => $mode, - 'agreementId' => $agreementId - ] - ] - ] + 'agreementId' => $agreementId, + ], + ], + ], ]; $this->scopeConfigMock->expects($this->once()) @@ -94,6 +106,11 @@ public function testGetConfigIfContentIsHtml() ->with($searchCriteriaMock) ->willReturn([$agreement]); + $this->escaperMock->expects($this->once()) + ->method('escapeHtml') + ->with($checkboxText) + ->willReturn($escapedCheckboxText); + $agreement->expects($this->once())->method('getIsHtml')->willReturn(true); $agreement->expects($this->once())->method('getContent')->willReturn($content); $agreement->expects($this->once())->method('getCheckboxText')->willReturn($checkboxText); @@ -103,11 +120,17 @@ public function testGetConfigIfContentIsHtml() $this->assertEquals($expectedResult, $this->model->getConfig()); } + /** + * Test for getConfig if content is not HTML. + * + * @return void + */ public function testGetConfigIfContentIsNotHtml() { $content = 'content'; $escapedContent = 'escaped_content'; $checkboxText = 'checkbox_text'; + $escapedCheckboxText = 'escaped_checkbox_text'; $mode = \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_AUTO; $agreementId = 100; $expectedResult = [ @@ -116,12 +139,12 @@ public function testGetConfigIfContentIsNotHtml() 'agreements' => [ [ 'content' => $escapedContent, - 'checkboxText' => $checkboxText, + 'checkboxText' => $escapedCheckboxText, 'mode' => $mode, - 'agreementId' => $agreementId - ] - ] - ] + 'agreementId' => $agreementId, + ], + ], + ], ]; $this->scopeConfigMock->expects($this->once()) @@ -139,8 +162,11 @@ public function testGetConfigIfContentIsNotHtml() ->with($searchCriteriaMock) ->willReturn([$agreement]); - $this->escaperMock->expects($this->once())->method('escapeHtml')->with($content)->willReturn($escapedContent); - + $this->escaperMock->expects($this->at(0))->method('escapeHtml')->with($content)->willReturn($escapedContent); + $this->escaperMock->expects($this->at(1)) + ->method('escapeHtml') + ->with($checkboxText) + ->willReturn($escapedCheckboxText); $agreement->expects($this->once())->method('getIsHtml')->willReturn(false); $agreement->expects($this->once())->method('getContent')->willReturn($content); $agreement->expects($this->once())->method('getCheckboxText')->willReturn($checkboxText); diff --git a/app/code/Magento/CheckoutAgreements/etc/db_schema.xml b/app/code/Magento/CheckoutAgreements/etc/db_schema.xml index 31b3111df98e..09cd1c5b6396 100644 --- a/app/code/Magento/CheckoutAgreements/etc/db_schema.xml +++ b/app/code/Magento/CheckoutAgreements/etc/db_schema.xml @@ -20,7 +20,7 @@ default="0" comment="Is Html"/> <column xsi:type="smallint" name="mode" padding="6" unsigned="false" nullable="false" identity="false" default="0" comment="Applied mode"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="agreement_id"/> </constraint> </table> @@ -29,14 +29,14 @@ comment="Agreement Id"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Store Id"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="agreement_id"/> <column name="store_id"/> </constraint> - <constraint xsi:type="foreign" name="CHKT_AGRT_STORE_AGRT_ID_CHKT_AGRT_AGRT_ID" table="checkout_agreement_store" + <constraint xsi:type="foreign" referenceId="CHKT_AGRT_STORE_AGRT_ID_CHKT_AGRT_AGRT_ID" table="checkout_agreement_store" column="agreement_id" referenceTable="checkout_agreement" referenceColumn="agreement_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CHECKOUT_AGREEMENT_STORE_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="CHECKOUT_AGREEMENT_STORE_STORE_ID_STORE_STORE_ID" table="checkout_agreement_store" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> </table> diff --git a/app/code/Magento/CheckoutAgreements/etc/di.xml b/app/code/Magento/CheckoutAgreements/etc/di.xml index 081e3daa781f..a8ff8f5941f9 100644 --- a/app/code/Magento/CheckoutAgreements/etc/di.xml +++ b/app/code/Magento/CheckoutAgreements/etc/di.xml @@ -23,7 +23,7 @@ <type name="Magento\Checkout\Api\GuestPaymentInformationManagementInterface"> <plugin name="validate-guest-agreements" type="Magento\CheckoutAgreements\Model\Checkout\Plugin\GuestValidation"/> </type> - <type name="\Magento\CheckoutAgreements\Model\CheckoutAgreementsList"> + <type name="Magento\CheckoutAgreements\Model\CheckoutAgreementsList"> <arguments> <argument name="collectionProcessor" xsi:type="object">Magento\CheckoutAgreements\Model\Api\SearchCriteria\CollectionProcessor</argument> </arguments> diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/layout/multishipping_checkout_overview.xml b/app/code/Magento/CheckoutAgreements/view/frontend/layout/multishipping_checkout_overview.xml index 3f742de0177d..122160f1a10c 100644 --- a/app/code/Magento/CheckoutAgreements/view/frontend/layout/multishipping_checkout_overview.xml +++ b/app/code/Magento/CheckoutAgreements/view/frontend/layout/multishipping_checkout_overview.xml @@ -8,7 +8,7 @@ <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceBlock name="checkout_overview"> - <block class="Magento\CheckoutAgreements\Block\Agreements" name="checkout.multishipping.agreements" as="agreements" template="Magento_CheckoutAgreements::multishipping_agreements.phtml"/> + <block class="Magento\CheckoutAgreements\Block\Agreements" name="checkout.multishipping.agreements" as="agreements" template="Magento_CheckoutAgreements::additional_agreements.phtml"/> </referenceBlock> </body> </page> diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/templates/multishipping_agreements.phtml b/app/code/Magento/CheckoutAgreements/view/frontend/templates/multishipping_agreements.phtml index 3400770f5cee..33227f0cdce3 100644 --- a/app/code/Magento/CheckoutAgreements/view/frontend/templates/multishipping_agreements.phtml +++ b/app/code/Magento/CheckoutAgreements/view/frontend/templates/multishipping_agreements.phtml @@ -4,6 +4,7 @@ * See COPYING.txt for license details. */ +// @deprecated // @codingStandardsIgnoreFile ?> diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/web/js/model/agreement-validator.js b/app/code/Magento/CheckoutAgreements/view/frontend/web/js/model/agreement-validator.js index 157923323fd0..cbd06b51fe1b 100644 --- a/app/code/Magento/CheckoutAgreements/view/frontend/web/js/model/agreement-validator.js +++ b/app/code/Magento/CheckoutAgreements/view/frontend/web/js/model/agreement-validator.js @@ -19,7 +19,7 @@ define([ * * @returns {Boolean} */ - validate: function () { + validate: function (hideError) { var isValid = true; if (!agreementsConfig.isEnabled || $(agreementsInputPath).length === 0) { @@ -28,7 +28,8 @@ define([ $(agreementsInputPath).each(function (index, element) { if (!$.validator.validateSingleElement(element, { - errorElement: 'div' + errorElement: 'div', + hideError: hideError || false })) { isValid = false; } diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html b/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html index a448537d64e8..4b1a68624e54 100644 --- a/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html +++ b/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html @@ -5,17 +5,17 @@ */ --> <div data-role="checkout-agreements"> - <div class="checkout-agreements" data-bind="visible: isVisible"> + <div class="checkout-agreements fieldset" data-bind="visible: isVisible"> <!-- ko foreach: agreements --> <!-- ko if: ($parent.isAgreementRequired($data)) --> - <div class="checkout-agreement required"> + <div class="checkout-agreement field choice required"> <input type="checkbox" class="required-entry" data-bind="attr: { 'id': $parent.getCheckboxId($parentContext, agreementId), 'name': 'agreement[' + agreementId + ']', 'value': agreementId }"/> - <label data-bind="attr: {'for': $parent.getCheckboxId($parentContext, agreementId)}"> + <label class="label" data-bind="attr: {'for': $parent.getCheckboxId($parentContext, agreementId)}"> <button type="button" class="action action-show" data-bind="click: function(data, event) { return $parent.showContent(data, event) }" diff --git a/app/code/Magento/Cms/Block/Adminhtml/Block/Edit/DeleteButton.php b/app/code/Magento/Cms/Block/Adminhtml/Block/Edit/DeleteButton.php index a7410cac64d7..51a9313fdefd 100644 --- a/app/code/Magento/Cms/Block/Adminhtml/Block/Edit/DeleteButton.php +++ b/app/code/Magento/Cms/Block/Adminhtml/Block/Edit/DeleteButton.php @@ -13,7 +13,7 @@ class DeleteButton extends GenericButton implements ButtonProviderInterface { /** - * @return array + * @inheritDoc */ public function getButtonData() { @@ -24,7 +24,7 @@ public function getButtonData() 'class' => 'delete', 'on_click' => 'deleteConfirm(\'' . __( 'Are you sure you want to do this?' - ) . '\', \'' . $this->getDeleteUrl() . '\')', + ) . '\', \'' . $this->getDeleteUrl() . '\', {"data": {}})', 'sort_order' => 20, ]; } @@ -32,6 +32,8 @@ public function getButtonData() } /** + * URL to send delete requests to. + * * @return string */ public function getDeleteUrl() diff --git a/app/code/Magento/Cms/Block/Adminhtml/Page/Edit/DeleteButton.php b/app/code/Magento/Cms/Block/Adminhtml/Page/Edit/DeleteButton.php index 1fc599e4c856..b434fd3f5d34 100644 --- a/app/code/Magento/Cms/Block/Adminhtml/Page/Edit/DeleteButton.php +++ b/app/code/Magento/Cms/Block/Adminhtml/Page/Edit/DeleteButton.php @@ -13,7 +13,7 @@ class DeleteButton extends GenericButton implements ButtonProviderInterface { /** - * @return array + * @inheritDoc */ public function getButtonData() { @@ -24,7 +24,7 @@ public function getButtonData() 'class' => 'delete', 'on_click' => 'deleteConfirm(\'' . __( 'Are you sure you want to do this?' - ) . '\', \'' . $this->getDeleteUrl() . '\')', + ) . '\', \'' . $this->getDeleteUrl() . '\', {"data": {}})', 'sort_order' => 20, ]; } @@ -32,6 +32,8 @@ public function getButtonData() } /** + * Url to send delete requests to. + * * @return string */ public function getDeleteUrl() diff --git a/app/code/Magento/Cms/Block/Adminhtml/Page/Grid/Renderer/Action/UrlBuilder.php b/app/code/Magento/Cms/Block/Adminhtml/Page/Grid/Renderer/Action/UrlBuilder.php index 67ae137fb4d8..08ba2c3fff33 100644 --- a/app/code/Magento/Cms/Block/Adminhtml/Page/Grid/Renderer/Action/UrlBuilder.php +++ b/app/code/Magento/Cms/Block/Adminhtml/Page/Grid/Renderer/Action/UrlBuilder.php @@ -5,8 +5,9 @@ */ namespace Magento\Cms\Block\Adminhtml\Page\Grid\Renderer\Action; -use Magento\Store\Api\StoreResolverInterface; - +/** + * Url builder class used to compose dynamic urls. + */ class UrlBuilder { /** @@ -32,15 +33,25 @@ public function __construct(\Magento\Framework\UrlInterface $frontendUrlBuilder) */ public function getUrl($routePath, $scope, $store) { - $this->frontendUrlBuilder->setScope($scope); - $href = $this->frontendUrlBuilder->getUrl( - $routePath, - [ - '_current' => false, - '_nosid' => true, - '_query' => [StoreResolverInterface::PARAM_NAME => $store] - ] - ); + if ($scope) { + $this->frontendUrlBuilder->setScope($scope); + $href = $this->frontendUrlBuilder->getUrl( + $routePath, + [ + '_current' => false, + '_nosid' => true, + '_query' => [\Magento\Store\Model\StoreManagerInterface::PARAM_NAME => $store] + ] + ); + } else { + $href = $this->frontendUrlBuilder->getUrl( + $routePath, + [ + '_current' => false, + '_nosid' => true + ] + ); + } return $href; } diff --git a/app/code/Magento/Cms/Block/Block.php b/app/code/Magento/Cms/Block/Block.php index d0d75ea69119..c611f4b1e9f0 100644 --- a/app/code/Magento/Cms/Block/Block.php +++ b/app/code/Magento/Cms/Block/Block.php @@ -84,4 +84,14 @@ public function getIdentities() { return [\Magento\Cms\Model\Block::CACHE_TAG . '_' . $this->getBlockId()]; } + + /** + * @inheritdoc + */ + public function getCacheKeyInfo() + { + $cacheKeyInfo = parent::getCacheKeyInfo(); + $cacheKeyInfo[] = $this->_storeManager->getStore()->getId(); + return $cacheKeyInfo; + } } diff --git a/app/code/Magento/Cms/Block/Widget/Block.php b/app/code/Magento/Cms/Block/Widget/Block.php index d8bd483fae5e..c665f2afc5d3 100644 --- a/app/code/Magento/Cms/Block/Widget/Block.php +++ b/app/code/Magento/Cms/Block/Widget/Block.php @@ -3,14 +3,22 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Cms\Block\Widget; +use Magento\Framework\DataObject\IdentityInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Cms\Model\Block as CmsBlock; +use Magento\Widget\Block\BlockInterface; + /** * Cms Static Block Widget * - * @author Magento Core Team <core@magentocommerce.com> + * @author Magento Core Team <core@magentocommerce.com> */ -class Block extends \Magento\Framework\View\Element\Template implements \Magento\Widget\Block\BlockInterface +class Block extends \Magento\Framework\View\Element\Template implements BlockInterface, IdentityInterface { /** * @var \Magento\Cms\Model\Template\FilterProvider @@ -31,6 +39,11 @@ class Block extends \Magento\Framework\View\Element\Template implements \Magento */ protected $_blockFactory; + /** + * @var CmsBlock + */ + private $block; + /** * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Cms\Model\Template\FilterProvider $filterProvider @@ -49,8 +62,9 @@ public function __construct( } /** - * Prepare block text and determine whether block output enabled or not - * Prevent blocks recursion if needed + * Prepare block text and determine whether block output enabled or not. + * + * Prevent blocks recursion if needed. * * @return $this */ @@ -65,19 +79,63 @@ protected function _beforeToHtml() } self::$_widgetUsageMap[$blockHash] = true; - if ($blockId) { - $storeId = $this->_storeManager->getStore()->getId(); - /** @var \Magento\Cms\Model\Block $block */ - $block = $this->_blockFactory->create(); - $block->setStoreId($storeId)->load($blockId); - if ($block->isActive()) { + $block = $this->getBlock(); + + if ($block && $block->isActive()) { + try { + $storeId = $this->getData('store_id') ?? $this->_storeManager->getStore()->getId(); $this->setText( $this->_filterProvider->getBlockFilter()->setStoreId($storeId)->filter($block->getContent()) ); + } catch (NoSuchEntityException $e) { } } - unset(self::$_widgetUsageMap[$blockHash]); return $this; } + + /** + * Get identities of the Cms Block + * + * @return array + */ + public function getIdentities() + { + $block = $this->getBlock(); + + if ($block) { + return $block->getIdentities(); + } + + return []; + } + + /** + * Get block + * + * @return CmsBlock|null + */ + private function getBlock(): ?CmsBlock + { + if ($this->block) { + return $this->block; + } + + $blockId = $this->getData('block_id'); + + if ($blockId) { + try { + $storeId = $this->_storeManager->getStore()->getId(); + /** @var \Magento\Cms\Model\Block $block */ + $block = $this->_blockFactory->create(); + $block->setStoreId($storeId)->load($blockId); + $this->block = $block; + + return $block; + } catch (NoSuchEntityException $e) { + } + } + + return null; + } } diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Block/Delete.php b/app/code/Magento/Cms/Controller/Adminhtml/Block/Delete.php index 3aaf40e7d0ab..4af6b684496e 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Block/Delete.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Block/Delete.php @@ -6,7 +6,9 @@ */ namespace Magento\Cms\Controller\Adminhtml\Block; -class Delete extends \Magento\Cms\Controller\Adminhtml\Block +use Magento\Framework\App\Action\HttpPostActionInterface; + +class Delete extends \Magento\Cms\Controller\Adminhtml\Block implements HttpPostActionInterface { /** * Delete action diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Block/Edit.php b/app/code/Magento/Cms/Controller/Adminhtml/Block/Edit.php index 875608906323..655b3eb5b91b 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Block/Edit.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Block/Edit.php @@ -5,7 +5,12 @@ */ namespace Magento\Cms\Controller\Adminhtml\Block; -class Edit extends \Magento\Cms\Controller\Adminhtml\Block +use Magento\Framework\App\Action\HttpGetActionInterface; + +/** + * Edit CMS block action. + */ +class Edit extends \Magento\Cms\Controller\Adminhtml\Block implements HttpGetActionInterface { /** * @var \Magento\Framework\View\Result\PageFactory diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Block/Index.php b/app/code/Magento/Cms/Controller/Adminhtml/Block/Index.php index a4096e4d1a44..b7504a5c2b22 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Block/Index.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Block/Index.php @@ -1,12 +1,16 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Cms\Controller\Adminhtml\Block; -class Index extends \Magento\Cms\Controller\Adminhtml\Block +use Magento\Framework\App\Action\HttpGetActionInterface; + +/** + * Index action. + */ +class Index extends \Magento\Cms\Controller\Adminhtml\Block implements HttpGetActionInterface { /** * @var \Magento\Framework\View\Result\PageFactory diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Block/MassDelete.php b/app/code/Magento/Cms/Controller/Adminhtml/Block/MassDelete.php index 92bc7ad71f59..ccdddc7c2b59 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Block/MassDelete.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Block/MassDelete.php @@ -6,6 +6,7 @@ */ namespace Magento\Cms\Controller\Adminhtml\Block; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\Controller\ResultFactory; use Magento\Backend\App\Action\Context; use Magento\Ui\Component\MassAction\Filter; @@ -14,7 +15,7 @@ /** * Class MassDelete */ -class MassDelete extends \Magento\Backend\App\Action +class MassDelete extends \Magento\Backend\App\Action implements HttpPostActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Block/NewAction.php b/app/code/Magento/Cms/Controller/Adminhtml/Block/NewAction.php index cfac00915c97..5983594f876a 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Block/NewAction.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Block/NewAction.php @@ -1,12 +1,16 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Cms\Controller\Adminhtml\Block; -class NewAction extends \Magento\Cms\Controller\Adminhtml\Block +use Magento\Framework\App\Action\HttpGetActionInterface; + +/** + * Create CMS block action. + */ +class NewAction extends \Magento\Cms\Controller\Adminhtml\Block implements HttpGetActionInterface { /** * @var \Magento\Backend\Model\View\Result\ForwardFactory diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Block/Save.php b/app/code/Magento/Cms/Controller/Adminhtml/Block/Save.php index 40974b7a4b5c..0c6c6470398c 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Block/Save.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Block/Save.php @@ -1,11 +1,11 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Cms\Controller\Adminhtml\Block; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Backend\App\Action\Context; use Magento\Cms\Api\BlockRepositoryInterface; use Magento\Cms\Model\Block; @@ -14,7 +14,10 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Registry; -class Save extends \Magento\Cms\Controller\Adminhtml\Block +/** + * Save CMS block action. + */ +class Save extends \Magento\Cms\Controller\Adminhtml\Block implements HttpPostActionInterface { /** * @var DataPersistorInterface diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/Delete.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/Delete.php index 16c99e9857c3..15bb40930536 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Page/Delete.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/Delete.php @@ -1,12 +1,16 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Cms\Controller\Adminhtml\Page; -class Delete extends \Magento\Backend\App\Action +use Magento\Framework\App\Action\HttpPostActionInterface; + +/** + * Delete CMS page action. + */ +class Delete extends \Magento\Backend\App\Action implements HttpPostActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/Edit.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/Edit.php index 6d51c28b6aca..f50fb2b19c00 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Page/Edit.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/Edit.php @@ -1,14 +1,17 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Cms\Controller\Adminhtml\Page; +use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Backend\App\Action; -class Edit extends \Magento\Backend\App\Action +/** + * Edit CMS page action. + */ +class Edit extends \Magento\Backend\App\Action implements HttpGetActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/Index.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/Index.php index 75f9ad70dc40..04557ddaeec7 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Page/Index.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/Index.php @@ -1,15 +1,18 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Cms\Controller\Adminhtml\Page; +use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Backend\App\Action\Context; use Magento\Framework\View\Result\PageFactory; -class Index extends \Magento\Backend\App\Action +/** + * Index action. + */ +class Index extends \Magento\Backend\App\Action implements HttpGetActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/MassDelete.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/MassDelete.php index a1d32aa97a38..222849f97602 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Page/MassDelete.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/MassDelete.php @@ -5,6 +5,7 @@ */ namespace Magento\Cms\Controller\Adminhtml\Page; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\Controller\ResultFactory; use Magento\Backend\App\Action\Context; use Magento\Ui\Component\MassAction\Filter; @@ -13,7 +14,7 @@ /** * Class MassDelete */ -class MassDelete extends \Magento\Backend\App\Action +class MassDelete extends \Magento\Backend\App\Action implements HttpPostActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/MassDisable.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/MassDisable.php index a85b8ecd5e5a..0a8c667d4d7b 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Page/MassDisable.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/MassDisable.php @@ -5,6 +5,7 @@ */ namespace Magento\Cms\Controller\Adminhtml\Page; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\Controller\ResultFactory; use Magento\Backend\App\Action\Context; use Magento\Ui\Component\MassAction\Filter; @@ -13,7 +14,7 @@ /** * Class MassDisable */ -class MassDisable extends \Magento\Backend\App\Action +class MassDisable extends \Magento\Backend\App\Action implements HttpPostActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/MassEnable.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/MassEnable.php index 3f26769e4c9e..e2cb8d984e01 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Page/MassEnable.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/MassEnable.php @@ -5,6 +5,7 @@ */ namespace Magento\Cms\Controller\Adminhtml\Page; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\Controller\ResultFactory; use Magento\Backend\App\Action\Context; use Magento\Ui\Component\MassAction\Filter; @@ -13,7 +14,7 @@ /** * Class MassEnable */ -class MassEnable extends \Magento\Backend\App\Action +class MassEnable extends \Magento\Backend\App\Action implements HttpPostActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/NewAction.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/NewAction.php index 407657b05c1c..6a4f4951cef0 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Page/NewAction.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/NewAction.php @@ -6,7 +6,12 @@ */ namespace Magento\Cms\Controller\Adminhtml\Page; -class NewAction extends \Magento\Backend\App\Action +use Magento\Framework\App\Action\HttpGetActionInterface; + +/** + * Create CMS page action. + */ +class NewAction extends \Magento\Backend\App\Action implements HttpGetActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/Save.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/Save.php index ef4fda60c0f8..37cb45753174 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Page/Save.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/Save.php @@ -1,17 +1,20 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Cms\Controller\Adminhtml\Page; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Backend\App\Action; use Magento\Cms\Model\Page; use Magento\Framework\App\Request\DataPersistorInterface; use Magento\Framework\Exception\LocalizedException; -class Save extends \Magento\Backend\App\Action +/** + * Save CMS page action. + */ +class Save extends \Magento\Backend\App\Action implements HttpPostActionInterface { /** * Authorization level of a basic admin session @@ -127,8 +130,8 @@ public function execute() /** * Process result redirect * - * @param \Magento\Cms\Api\Data\PageInterface $model - * @param \Magento\Backend\Model\View\Result\Redirect $resultRedirect + * @param \Magento\Cms\Api\Data\PageInterface $model + * @param \Magento\Backend\Model\View\Result\Redirect $resultRedirect * @param array $data * @return \Magento\Backend\Model\View\Result\Redirect * @throws LocalizedException diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/Widget/Chooser.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/Widget/Chooser.php index 1c46ac6807ee..340f4b24dd16 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Page/Widget/Chooser.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/Widget/Chooser.php @@ -1,14 +1,18 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Cms\Controller\Adminhtml\Page\Widget; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Backend\App\Action; -class Chooser extends \Magento\Backend\App\Action +/** + * Chooser Source action. + */ +class Chooser extends Action implements HttpPostActionInterface, HttpGetActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFiles.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFiles.php index 890c9bf5eae5..6f57efad41e7 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFiles.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFiles.php @@ -6,11 +6,12 @@ namespace Magento\Cms\Controller\Adminhtml\Wysiwyg\Images; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\Action\HttpPostActionInterface; /** * Delete image files. */ -class DeleteFiles extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images +class DeleteFiles extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images implements HttpPostActionInterface { /** * @var \Magento\Framework\Controller\Result\JsonFactory @@ -79,7 +80,7 @@ public function execute() $filesystem = $this->_objectManager->get(\Magento\Framework\Filesystem::class); $dir = $filesystem->getDirectoryRead(DirectoryList::MEDIA); $filePath = $path . '/' . \Magento\Framework\File\Uploader::getCorrectFileName($file); - if ($dir->isFile($dir->getRelativePath($filePath))) { + if ($dir->isFile($dir->getRelativePath($filePath)) && !preg_match('#.htaccess#', $file)) { $this->getStorage()->deleteFile($filePath); } } diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php index a1de11c3c462..5344472a79a9 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php @@ -6,12 +6,13 @@ */ namespace Magento\Cms\Controller\Adminhtml\Wysiwyg\Images; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\App\Filesystem\DirectoryList; /** * Delete image folder. */ -class DeleteFolder extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images +class DeleteFolder extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images implements HttpPostActionInterface { /** * @var \Magento\Framework\Controller\Result\JsonFactory diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php index a7f49e8a431a..82d200beb6dc 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php @@ -6,12 +6,13 @@ */ namespace Magento\Cms\Controller\Adminhtml\Wysiwyg\Images; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\App\Filesystem\DirectoryList; /** * Creates new folder. */ -class NewFolder extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images +class NewFolder extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images implements HttpPostActionInterface { /** * @var \Magento\Framework\Controller\Result\JsonFactory diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php index 5c9aa2243bc6..9bad371aa84d 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php @@ -4,14 +4,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Cms\Controller\Adminhtml\Wysiwyg\Images; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\App\Filesystem\DirectoryList; /** * Upload image. */ -class Upload extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images +class Upload extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images implements HttpPostActionInterface { /** * @var \Magento\Framework\Controller\Result\JsonFactory @@ -57,13 +61,20 @@ public function execute() __('Directory %1 is not under storage root path.', $path) ); } - $result = $this->getStorage()->uploadFile($path, $this->getRequest()->getParam('type')); + $uploaded = $this->getStorage()->uploadFile($path, $this->getRequest()->getParam('type')); + $response = [ + 'name' => $uploaded['name'], + 'type' => $uploaded['type'], + 'error' => $uploaded['error'], + 'size' => $uploaded['size'], + 'file' => $uploaded['file'] + ]; } catch (\Exception $e) { - $result = ['error' => $e->getMessage(), 'errorcode' => $e->getCode()]; + $response = ['error' => $e->getMessage(), 'errorcode' => $e->getCode()]; } /** @var \Magento\Framework\Controller\Result\Json $resultJson */ $resultJson = $this->resultJsonFactory->create(); - return $resultJson->setData($result); + return $resultJson->setData($response); } } diff --git a/app/code/Magento/Cms/Controller/Index/Index.php b/app/code/Magento/Cms/Controller/Index/Index.php index c027bd1a2b71..59ed1a6248b7 100644 --- a/app/code/Magento/Cms/Controller/Index/Index.php +++ b/app/code/Magento/Cms/Controller/Index/Index.php @@ -5,6 +5,8 @@ */ namespace Magento\Cms\Controller\Index; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Framework\App\Action\Context; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; @@ -15,8 +17,12 @@ use Magento\Framework\View\Result\Page as ResultPage; use Magento\Cms\Helper\Page; use Magento\Store\Model\ScopeInterface; +use Magento\Framework\App\Action\Action; -class Index extends \Magento\Framework\App\Action\Action +/** + * Home page. Needs to be accessible by POST because of the store switching. + */ +class Index extends Action implements HttpGetActionInterface, HttpPostActionInterface { /** * @var ForwardFactory diff --git a/app/code/Magento/Cms/Controller/Noroute/Index.php b/app/code/Magento/Cms/Controller/Noroute/Index.php index db84ce9556da..b30beae73dce 100644 --- a/app/code/Magento/Cms/Controller/Noroute/Index.php +++ b/app/code/Magento/Cms/Controller/Noroute/Index.php @@ -6,6 +6,9 @@ */ namespace Magento\Cms\Controller\Noroute; +/** + * @SuppressWarnings(PHPMD.AllPurposeAction) + */ class Index extends \Magento\Framework\App\Action\Action { /** diff --git a/app/code/Magento/Cms/Controller/Page/View.php b/app/code/Magento/Cms/Controller/Page/View.php index ab02bc5e717a..9d5785450ec7 100644 --- a/app/code/Magento/Cms/Controller/Page/View.php +++ b/app/code/Magento/Cms/Controller/Page/View.php @@ -6,7 +6,14 @@ */ namespace Magento\Cms\Controller\Page; -class View extends \Magento\Framework\App\Action\Action +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\Action; + +/** + * Custom page for storefront. Needs to be accessible by POST because of the store switching. + */ +class View extends Action implements HttpGetActionInterface, HttpPostActionInterface { /** * @var \Magento\Framework\Controller\Result\ForwardFactory diff --git a/app/code/Magento/Cms/Helper/Page.php b/app/code/Magento/Cms/Helper/Page.php index abd260b260b9..70e9437235ac 100644 --- a/app/code/Magento/Cms/Helper/Page.php +++ b/app/code/Magento/Cms/Helper/Page.php @@ -187,7 +187,7 @@ public function getPageUrl($pageId = null) { /** @var \Magento\Cms\Model\Page $page */ $page = $this->_pageFactory->create(); - if ($pageId !== null && $pageId !== $page->getId()) { + if ($pageId !== null) { $page->setStoreId($this->_storeManager->getStore()->getId()); $page->load($pageId); } diff --git a/app/code/Magento/Cms/Model/Page/Source/PageLayout.php b/app/code/Magento/Cms/Model/Page/Source/PageLayout.php index fb759348759b..23a452c0fe58 100644 --- a/app/code/Magento/Cms/Model/Page/Source/PageLayout.php +++ b/app/code/Magento/Cms/Model/Page/Source/PageLayout.php @@ -20,6 +20,7 @@ class PageLayout implements OptionSourceInterface /** * @var array + * @deprecated since the cache is now handled by \Magento\Theme\Model\PageLayout\Config\Builder::$configFiles */ protected $options; @@ -34,16 +35,10 @@ public function __construct(BuilderInterface $pageLayoutBuilder) } /** - * Get options - * - * @return array + * @inheritdoc */ public function toOptionArray() { - if ($this->options !== null) { - return $this->options; - } - $configOptions = $this->pageLayoutBuilder->getPageLayoutsConfig()->getOptions(); $options = []; foreach ($configOptions as $key => $value) { @@ -54,6 +49,6 @@ public function toOptionArray() } $this->options = $options; - return $this->options; + return $options; } } diff --git a/app/code/Magento/Cms/Model/ResourceModel/Block.php b/app/code/Magento/Cms/Model/ResourceModel/Block.php index 9aab54b02bc1..30e817713755 100644 --- a/app/code/Magento/Cms/Model/ResourceModel/Block.php +++ b/app/code/Magento/Cms/Model/ResourceModel/Block.php @@ -95,9 +95,11 @@ protected function _beforeSave(AbstractModel $object) } /** + * Get block id. + * * @param AbstractModel $object * @param mixed $value - * @param null $field + * @param string $field * @return bool|int|string * @throws LocalizedException * @throws \Exception @@ -183,10 +185,12 @@ public function getIsUniqueBlockToStores(AbstractModel $object) $entityMetadata = $this->metadataPool->getMetadata(BlockInterface::class); $linkField = $entityMetadata->getLinkField(); - if ($this->_storeManager->isSingleStoreMode()) { - $stores = [Store::DEFAULT_STORE_ID]; - } else { - $stores = (array)$object->getData('store_id'); + $stores = (array)$object->getData('store_id'); + $isDefaultStore = $this->_storeManager->isSingleStoreMode() + || array_search(Store::DEFAULT_STORE_ID, $stores) !== false; + + if (!$isDefaultStore) { + $stores[] = Store::DEFAULT_STORE_ID; } $select = $this->getConnection()->select() @@ -196,8 +200,11 @@ public function getIsUniqueBlockToStores(AbstractModel $object) 'cb.' . $linkField . ' = cbs.' . $linkField, [] ) - ->where('cb.identifier = ?', $object->getData('identifier')) - ->where('cbs.store_id IN (?)', $stores); + ->where('cb.identifier = ? ', $object->getData('identifier')); + + if (!$isDefaultStore) { + $select->where('cbs.store_id IN (?)', $stores); + } if ($object->getId()) { $select->where('cb.' . $entityMetadata->getIdentifierField() . ' <> ?', $object->getId()); @@ -236,6 +243,8 @@ public function lookupStoreIds($id) } /** + * Save an object. + * * @param AbstractModel $object * @return $this * @throws \Exception diff --git a/app/code/Magento/Cms/Model/Wysiwyg/Config.php b/app/code/Magento/Cms/Model/Wysiwyg/Config.php index 5db3933dd116..1da7b99c6d88 100644 --- a/app/code/Magento/Cms/Model/Wysiwyg/Config.php +++ b/app/code/Magento/Cms/Model/Wysiwyg/Config.php @@ -89,8 +89,6 @@ class Config extends \Magento\Framework\DataObject implements ConfigInterface /** * @var array - * @deprecated - * @see \Magento\Cms\Model\Wysiwyg\Gallery\DefaultConfigProvider */ protected $_windowSize; @@ -207,6 +205,12 @@ public function getConfig($data = []) if ($this->_authorization->isAllowed('Magento_Cms::media_gallery')) { $this->configProvider->processGalleryConfig($config); + $config->addData( + [ + 'files_browser_window_width' => $this->_windowSize['width'], + 'files_browser_window_height' => $this->_windowSize['height'], + ] + ); } if ($config->getData('add_widgets')) { $this->configProvider->processWidgetConfig($config); diff --git a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php index dfd55f5346e5..dfbbce99b651 100644 --- a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php +++ b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php @@ -3,17 +3,24 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Cms\Model\Wysiwyg\Images; use Magento\Cms\Helper\Wysiwyg\Images; use Magento\Framework\App\Filesystem\DirectoryList; /** - * Wysiwyg Images model + * Wysiwyg Images model. + * + * Tightly connected with controllers responsible for managing files so it uses session and is (sort of) a part + * of the presentation layer. * * @SuppressWarnings(PHPMD.LongVariable) * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * * @api * @since 100.0.2 @@ -270,7 +277,8 @@ public function getDirsCollection($path) $collection = $this->getCollection($path) ->setCollectDirs(true) ->setCollectFiles(false) - ->setCollectRecursively(false); + ->setCollectRecursively(false) + ->setOrder('basename', \Magento\Framework\Data\Collection\Filesystem::SORT_ORDER_ASC); $conditions = $this->getConditionsForExcludeDirs(); @@ -501,14 +509,6 @@ public function uploadFile($targetPath, $type = null) // create thumbnail $this->resizeFile($targetPath . '/' . $uploader->getUploadedFileName(), true); - $result['cookie'] = [ - 'name' => $this->getSession()->getName(), - 'value' => $this->getSession()->getSessionId(), - 'lifetime' => $this->getSession()->getCookieLifetime(), - 'path' => $this->getSession()->getCookiePath(), - 'domain' => $this->getSession()->getCookieDomain(), - ]; - return $result; } diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertCMSPageContentActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertCMSPageContentActionGroup.xml index 58318660d2c4..dde623739025 100644 --- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertCMSPageContentActionGroup.xml +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertCMSPageContentActionGroup.xml @@ -23,4 +23,14 @@ <executeJS function="(el = document.querySelector('[name=\'identifier\']')) && el['se' + 'tAt' + 'tribute']('data-value', el.value.split('-')[0]);" stepKey="setAttribute" /> <seeElement selector="{{CmsNewPagePageBasicFieldsSection.duplicatedURLKey(_duplicatedCMSPage.title)}}" stepKey="see"/> </actionGroup> + <actionGroup name="AssertStoreFrontCMSPage"> + <arguments> + <argument name="cmsTitle" type="string"/> + <argument name="cmsContent" type="string"/> + <argument name="cmsContentHeading" type="string"/> + </arguments> + <see selector="{{StorefrontCMSPageSection.title}}" userInput="{{cmsTitle}}" stepKey="seeTitle"/> + <see selector="{{StorefrontCMSPageSection.mainTitle}}" userInput="{{cmsContentHeading}}" stepKey="seeContentHeading"/> + <see selector="{{StorefrontCMSPageSection.mainContent}}" userInput="{{cmsContent}}" stepKey="seeContent"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/CMSActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/CMSActionGroup.xml index 75e059f620c2..07e43347d9dd 100644 --- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/CMSActionGroup.xml +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/CMSActionGroup.xml @@ -44,4 +44,75 @@ <waitForPageLoad stepKey="waitForPageLoad3"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskOfStagingSection" /> </actionGroup> + <actionGroup name="DeleteCMSBlockActionGroup"> + <amOnPage url="{{CmsBlocksPage.url}}" stepKey="navigateToCMSPagesGrid"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <click selector="{{CmsPagesPageActionsSection.select(_defaultBlock.title)}}" stepKey="ClickOnSelect"/> + <click selector="{{CmsPagesPageActionsSection.delete(_defaultBlock.title)}}" stepKey="ClickOnEdit"/> + <waitForPageLoad stepKey="waitForPageLoad3"/> + <click selector="{{CmsPagesPageActionsSection.deleteConfirm}}" stepKey="ClickToConfirm"/> + <waitForPageLoad stepKey="waitForPageLoad4"/> + <see userInput="You deleted the block." stepKey="VerifyBlockIsDeleted"/> + </actionGroup> + <actionGroup name="AddStoreViewToCmsPage" extends="navigateToCreatedCMSPage"> + <arguments> + <argument name="storeViewName" type="string"/> + </arguments> + <remove keyForRemoval="clickExpandContentTabForPage"/> + <remove keyForRemoval="waitForLoadingMaskOfStagingSection"/> + <click selector="{{CmsNewPagePiwSection.header}}" stepKey="clickPageInWebsites" after="waitForPageLoad3"/> + <waitForElementVisible selector="{{CmsNewPagePiwSection.selectStoreView(storeViewName)}}" stepKey="waitForStoreGridReload"/> + <clickWithLeftButton selector="{{CmsNewPagePiwSection.selectStoreView(storeViewName)}}" stepKey="clickStoreView"/> + <click selector="{{CmsNewPagePageActionsSection.expandSplitButton}}" stepKey="expandButtonMenu"/> + <waitForElementVisible selector="{{CmsNewPagePageActionsSection.splitButtonMenu}}" stepKey="waitForSplitButtonMenuVisible"/> + <click selector="{{CmsNewPagePageActionsSection.savePage}}" stepKey="clickSavePage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see userInput="You saved the page." stepKey="seeMessage"/> + </actionGroup> + <actionGroup name="saveAndCloseCMSBlockWithSplitButton"> + <waitForElementVisible selector="{{BlockNewPagePageActionsSection.expandSplitButton}}" stepKey="waitForExpandSplitButtonToBeVisible" /> + <click selector="{{BlockNewPagePageActionsSection.expandSplitButton}}" stepKey="expandSplitButton"/> + <click selector="{{BlockNewPagePageActionsSection.saveAndClose}}" stepKey="clickSaveBlock"/> + <waitForPageLoad stepKey="waitForPageLoadAfterClickingSave" /> + <see userInput="You saved the block." stepKey="assertSaveBlockSuccessMessage"/> + </actionGroup> + <actionGroup name="navigateToStorefrontForCreatedPage"> + <arguments> + <argument name="page" type="string"/> + </arguments> + <amOnPage url="{{page}}" stepKey="goToStorefront"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> + <actionGroup name="saveCMSBlock"> + <waitForElementVisible selector="{{CmsNewBlockBlockActionsSection.savePage}}" stepKey="waitForSaveButton"/> + <click selector="{{CmsNewBlockBlockActionsSection.savePage}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see userInput="You saved the block." stepKey="seeSuccessfulSaveMessage"/> + </actionGroup> + <actionGroup name="saveAndContinueEditCmsPage"> + <waitForElementVisible time="10" selector="{{CmsNewPagePageActionsSection.saveAndContinueEdit}}" stepKey="waitForSaveAndContinueVisibility"/> + <click selector="{{CmsNewPagePageActionsSection.saveAndContinueEdit}}" stepKey="clickSaveAndContinueEditCmsPage"/> + <waitForPageLoad stepKey="waitForCmsPageLoad"/> + <waitForElementVisible time="1" selector="{{CmsNewPagePageActionsSection.cmsPageTitle}}" stepKey="waitForCmsPageSaveButton"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + </actionGroup> + <actionGroup name="saveCmsPage"> + <waitForElementVisible selector="{{CmsNewPagePageActionsSection.expandSplitButton}}" stepKey="waitForSplitButton"/> + <click selector="{{CmsNewPagePageActionsSection.expandSplitButton}}" stepKey="expandSplitButton"/> + <waitForElementVisible selector="{{CmsNewPagePageActionsSection.savePage}}" stepKey="waitForSaveCmsPage"/> + <click selector="{{CmsNewPagePageActionsSection.savePage}}" stepKey="clickSaveCmsPage"/> + <waitForElementVisible time="1" selector="{{CmsPagesPageActionsSection.addNewPageButton}}" stepKey="waitForCmsPageSaveButton"/> + <see userInput="You saved the page." selector="{{CmsPagesPageActionsSection.savePageSuccessMessage}}" stepKey="assertSavePageSuccessMessage"/> + </actionGroup> + <actionGroup name="setLayout"> + <arguments> + <argument name="designSection"/> + <argument name="layoutOption"/> + </arguments> + <waitForElementVisible selector="{{designSection.DesignTab}}" stepKey="waitForDesignTabVisible"/> + <conditionalClick selector="{{designSection.DesignTab}}" dependentSelector="{{designSection.LayoutDropdown}}" visible="false" stepKey="clickOnDesignTab"/> + <waitForPageLoad stepKey="waitForPageLoadDesignTab"/> + <waitForElementVisible selector="{{designSection.LayoutDropdown}}" stepKey="waitForLayoutDropDown" /> + <selectOption selector="{{designSection.LayoutDropdown}}" userInput="{{layoutOption}}" stepKey="selectLayout"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/ClearWidgetsFromCMSContentActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/ClearWidgetsFromCMSContentActionGroup.xml new file mode 100644 index 000000000000..2fa1b86a6157 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/ClearWidgetsFromCMSContentActionGroup.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="ClearWidgetsFromCMSContent"> + <amOnPage url="{{CmsPageEditPage.url('2')}}" stepKey="navigateToEditHomePagePage"/> + <waitForPageLoad stepKey="waitEditHomePagePageToLoad"/> + <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickContentTab" /> + <waitForElementNotVisible selector="{{CmsWYSIWYGSection.CheckIfTabExpand}}" stepKey="waitForTabExpand"/> + <executeJS function="jQuery('[id=\'cms_page_form_content_ifr\']').attr('name', 'preview-iframe')" stepKey="setPreviewFrameName"/> + <switchToIFrame selector="preview-iframe" stepKey="switchToIframe"/> + <fillField selector="{{TinyMCESection.EditorContent}}" userInput="Hello TinyMCE4!" stepKey="clearWidgets"/> + <switchToIFrame stepKey="switchOutFromIframe"/> + <executeJS function="tinyMCE.activeEditor.setContent('Hello TinyMCE4!');" stepKey="executeJSFillContent1"/> + <click selector="{{InsertWidgetSection.save}}" stepKey="saveWidget"/> + <waitForPageLoad stepKey="waitSaveToBeApplied"/> + <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="You saved the page." stepKey="seeSaveSuccess"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/CreateNewPageWithWidgetActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/CreateNewPageWithWidgetActionGroup.xml new file mode 100644 index 000000000000..a4b88c544de8 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/CreateNewPageWithWidgetActionGroup.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CreateNewPageWithWidget"> + <arguments> + <argument name="pageTitle" type="string" defaultValue="{{defaultCmsPage.title}}"/> + <argument name="category" type="string"/> + <argument name="condition" type="string"/> + <argument name="widgetType" type="string"/> + </arguments> + <amOnPage url="{{CmsNewPagePage.url}}" stepKey="amOnCMSNewPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <fillField selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" userInput="{{pageTitle}}" stepKey="fillFieldTitle"/> + <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickExpandContent"/> + <click selector="{{CmsNewPagePageActionsSection.insertWidget}}" stepKey="clickToInsertWidget"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <waitForElementVisible stepKey="waitForInsertWidgetTitle" selector="{{WidgetSection.InsertWidgetTitle}}"/> + <selectOption selector="{{WidgetSection.WidgetType}}" userInput="{{widgetType}}" stepKey="selectCatalogProductsList"/> + <waitForElementVisible selector="{{WidgetSection.AddParam}}" stepKey="waitForAddParam"/> + <scrollTo selector="{{WidgetSection.AddParam}}" stepKey="scrollToAddParamElement"/> + <click selector="{{WidgetSection.AddParam}}" stepKey="addParam"/> + <selectOption selector="{{WidgetSection.ConditionsDropdown}}" userInput="{{condition}}" stepKey="selectCategory"/> + <waitForElementVisible selector="{{WidgetSection.RuleParam}}" stepKey="waitForRuleParam"/> + <click selector="{{WidgetSection.RuleParam}}" stepKey="clickToAddRuleParam"/> + <click selector="{{WidgetSection.Chooser}}" stepKey="clickToSelectFromList"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <click selector="{{WidgetSection.PreCreateCategory(category)}}" stepKey="selectPreCategory" /> + <click selector="{{WidgetSection.InsertWidget}}" stepKey="clickToSaveInsertedWidget"/> + <waitForPageLoad stepKey="waitForPageLoad3"/> + <click selector="{{CmsNewBlockBlockActionsSection.savePage}}" stepKey="saveCMSPage"/> + <waitForElementVisible selector="{{CmsPagesPageActionsSection.savePageSuccessMessage}}" stepKey="waitForSuccessMessageLoggedOut" time="5"/> + <see userInput="You saved the page." stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/SearchBlockOnGridPageActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/SearchBlockOnGridPageActionGroup.xml index e31eb44146af..12205aeed8fa 100644 --- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/SearchBlockOnGridPageActionGroup.xml +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/SearchBlockOnGridPageActionGroup.xml @@ -16,4 +16,27 @@ <waitForLoadingMaskToDisappear stepKey="waitForSecondIdSortDescendingToFinish2" /> <waitForElementVisible selector="{{WidgetSection.BlockPage(Block.identifier)}}" stepKey="waitForBlockTitle" /> </actionGroup> + <actionGroup name ="deleteBlock"> + <arguments> + <argument name="Block" defaultValue=""/> + </arguments> + <amOnPage url="{{CmsBlocksPage.url}}" stepKey="navigateToCMSBlocksGrid"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <conditionalClick selector="{{BlockPageActionsSection.clearAll}}" dependentSelector="{{BlockPageActionsSection.activeFilters}}" stepKey="clickToResetFilter" visible="true"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <click selector="{{BlockPageActionsSection.FilterBtn}}" stepKey="clickFilterBtn"/> + <fillField selector="{{BlockPageActionsSection.URLKey}}" userInput="{{Block.identifier}}" stepKey="fillBlockIdentifierInput"/> + <click selector="{{BlockPageActionsSection.ApplyFiltersBtn}}" stepKey="applyFilter"/> + <waitForLoadingMaskToDisappear stepKey="waitForGridToLoadResults" /> + <waitForElementVisible selector="{{BlockPageActionsSection.select(Block.identifier)}}" stepKey="waitForCMSPageGrid" /> + <click selector="{{BlockPageActionsSection.select(Block.identifier)}}" stepKey="clickSelect" /> + <waitForElementVisible selector="{{BlockPageActionsSection.edit(Block.identifier)}}" stepKey="waitForEditLink" /> + <click selector="{{BlockPageActionsSection.edit(Block.identifier)}}" stepKey="clickEdit" /> + <waitForLoadingMaskToDisappear stepKey="waitForPageToLoad" /> + <click selector="{{CmsBlockBlockActionSection.deleteBlock}}" stepKey="deleteBlock"/> + <waitForElementVisible selector="{{CmsBlockBlockActionSection.deleteConfirm}}" stepKey="waitForOkButtonToBeVisible"/> + <click selector="{{CmsBlockBlockActionSection.deleteConfirm}}" stepKey="clickOkButton"/> + <waitForPageLoad stepKey="waitForPageLoad3"/> + <see userInput="You deleted the block." stepKey="seeSuccessMessage"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Cms/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 000000000000..3e227df56c90 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminMenuContent"> + <data key="pageTitle">Content</data> + <data key="title">Content</data> + <data key="dataUiId">magento-backend-content</data> + </entity> + <entity name="AdminMenuContentElementsPages"> + <data key="pageTitle">Pages</data> + <data key="title">Pages</data> + <data key="dataUiId">magento-cms-cms-page</data> + </entity> + <entity name="AdminMenuContentElementsBlocks"> + <data key="pageTitle">Blocks</data> + <data key="title">Blocks</data> + <data key="dataUiId">magento-cms-cms-block</data> + </entity> +</entities> diff --git a/app/code/Magento/Cms/Test/Mftf/Data/BlockData.xml b/app/code/Magento/Cms/Test/Mftf/Data/BlockData.xml new file mode 100644 index 000000000000..dea047ec4356 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Data/BlockData.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="Sales25offBlock" type="block"> + <data key="title" unique="suffix">Sales25off</data> + <data key="identifier" unique="suffix">Sales25off</data> + <data key="store_id">All Store Views</data> + <data key="content">sales25off everything!</data> + <data key="is_active">0</data> + </entity> +</entities> diff --git a/app/code/Magento/Cms/Test/Mftf/Data/CmsPageData.xml b/app/code/Magento/Cms/Test/Mftf/Data/CmsPageData.xml index d8a8401c31f1..2f8efac37cec 100644 --- a/app/code/Magento/Cms/Test/Mftf/Data/CmsPageData.xml +++ b/app/code/Magento/Cms/Test/Mftf/Data/CmsPageData.xml @@ -50,6 +50,7 @@ <data key="file_type">Upload File</data> <data key="shareable">Yes</data> <data key="file">magento-again.jpg</data> + <data key="fileName">magento-again</data> <data key="value">magento-again.jpg</data> <data key="content">Image content. Yeah.</data> <data key="height">1000</data> @@ -71,6 +72,7 @@ <data key="file_type">Upload File</data> <data key="shareable">Yes</data> <data key="value">magento3.jpg</data> + <data key="file">magento3.jpg</data> <data key="fileName">magento3</data> <data key="extension">jpg</data> <data key="content">Image content. Yeah.</data> @@ -86,4 +88,13 @@ <data key="content">1<br/>2<br/>3<br/>4<br/>5<br/>6<br/>7<br/>8<br/>9<br/>10<br/>11<br/>12<br/>13<br/>14<br/>15<br/>16<br/>17<br/>18<br/>19<br/>20<br/>line21<br/>22<br/>23<br/>24<br/>25<br/>26<br/>line27<br/>2<br/>3<br/>4<br/>5</data> <data key="identifier" unique="suffix">test-page-</data> </entity> + <entity name="_emptyCmsPage" type="cms_page"> + <data key="title" unique="suffix">Test CMS Page</data> + <data key="identifier" unique="suffix">test-page-</data> + </entity> + <entity name="_emptyCmsBlock" type="block"> + <data key="title" unique="suffix">Test CMS Block</data> + <data key="identifier" unique="suffix" >block</data> + <data key="active">true</data> + </entity> </entities> diff --git a/app/code/Magento/Cms/Test/Mftf/Data/NewCMSPageData.xml b/app/code/Magento/Cms/Test/Mftf/Data/NewCMSPageData.xml new file mode 100644 index 000000000000..61dfb051d101 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Data/NewCMSPageData.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="defaultCmsPage" type="block"> + <data key="title" unique="suffix">CMSpage</data> + </entity> +</entities> diff --git a/app/code/Magento/Cms/Test/Mftf/Page/AdminCmsEditBlockPage.xml b/app/code/Magento/Cms/Test/Mftf/Page/AdminCmsEditBlockPage.xml new file mode 100644 index 000000000000..3fd100ee02aa --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Page/AdminCmsEditBlockPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminEditBlockPage" url="cms/block/edit/block_id" area="admin" module="Magento_Cms"> + <section name="AdminUpdateBlockSection"/> + </page> +</pages> diff --git a/app/code/Magento/Cms/Test/Mftf/Page/CmsPageEditPage.xml b/app/code/Magento/Cms/Test/Mftf/Page/CmsPageEditPage.xml new file mode 100644 index 000000000000..73db6b61343b --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Page/CmsPageEditPage.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + --> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="CmsPageEditPage" area="admin" url="admin/cms_page/edit/page_id/{{var}}" parameterized="true" module="Magento_Cms"> + <section name="CmsNewPagePageActionsSection"/> + <section name="CmsNewPagePageBasicFieldsSection"/> + <section name="CmsNewPagePageContentSection"/> + <section name="CmsNewPagePageSeoSection"/> + </page> +</pages> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/AdminBlockGridSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/AdminBlockGridSection.xml new file mode 100644 index 000000000000..ab15570a01f4 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Section/AdminBlockGridSection.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminBlockGridSection"> + <element name="search" type="input" selector="//input[@placeholder='Search by keyword']"/> + <element name="searchButton" type="button" selector="//div[@class='data-grid-search-control-wrap']//label[@class='data-grid-search-label']/following-sibling::button[@class='action-submit']"/> + <element name="checkbox" type="checkbox" selector="//label[@class='data-grid-checkbox-cell-inner']//input[@class='admin__control-checkbox']"/> + <element name="select" type="select" selector="//tr[@class='data-row']//button[@class='action-select']"/> + <element name="editInSelect" type="text" selector="//a[contains(text(), 'Edit')]"/> + </section> +</sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection.xml index 2e783af6bfc6..2efa7f62fc4e 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection.xml @@ -17,6 +17,7 @@ <element name="saveAndDuplicate" type="button" selector="#save_and_duplicate" timeout="10"/> <element name="saveAndClose" type="button" selector="#save_and_close" timeout="10"/> <element name="expandSplitButton" type="button" selector="//button[@data-ui-id='save-button-dropdown']" timeout="10"/> + <element name="back" type="button" selector="#back"/> </section> <section name="BlockWYSIWYGSection"> <element name="ShowHideBtn" type="button" selector="#togglecms_block_form_content"/> @@ -24,4 +25,8 @@ <section name="BlockContentSection"> <element name="TextArea" type="input" selector="#cms_block_form_content"/> </section> + <section name="CmsBlockBlockActionSection"> + <element name="deleteBlock" type="button" selector="#delete" timeout="30"/> + <element name="deleteConfirm" type="button" selector=".action-primary.action-accept" timeout="60"/> + </section> </sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageActionsSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageActionsSection.xml index 42f8f4d00ee9..a340d0af1e7a 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageActionsSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageActionsSection.xml @@ -22,5 +22,6 @@ <element name="content" type="input" selector="//textarea[@name='content']"/> <element name="spinner" type="input" selector='//div[@data-component="cms_page_form.cms_page_form"]' /> <element name="saveAndClose" type="button" selector="#save_and_close" timeout="10"/> + <element name="insertWidget" type="button" selector="//span[contains(text(),'Insert Widget...')]"/> </section> </sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsPagesPageActionsSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsPagesPageActionsSection.xml index de8a2adccc36..11d8bb23313f 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/CmsPagesPageActionsSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsPagesPageActionsSection.xml @@ -17,7 +17,7 @@ <element name="addNewPageButton" type="button" selector="#add" timeout="30"/> <element name="select" type="button" selector="//div[text()='{{var1}}']/parent::td//following-sibling::td[@class='data-grid-actions-cell']//button[text()='Select']" parameterized="true"/> <element name="edit" type="button" selector="//div[text()='{{var1}}']/parent::td//following-sibling::td[@class='data-grid-actions-cell']//a[text()='Edit']" parameterized="true"/> - <element name="preview" type="button" selector="//div[text()='{{var1}}']/parent::td//following-sibling::td[@class='data-grid-actions-cell']//a[text()='Preview']" parameterized="true"/> + <element name="preview" type="button" selector="//div[text()='{{var1}}']/parent::td//following-sibling::td[@class='data-grid-actions-cell']//a[text()='View']" parameterized="true"/> <element name="clearAllButton" type="button" selector="//div[@class='admin__data-grid-header']//button[contains(text(), 'Clear all')]"/> <element name="activeFilters" type="button" selector="//div[@class='admin__data-grid-header']//span[contains(text(), 'Active filters:')]" /> <element name="spinner" type="input" selector='//div[@data-component="cms_page_listing.cms_page_listing.cms_page_columns"]'/> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/StorefrontCMSPageSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/StorefrontCMSPageSection.xml index 7a358c61263d..4ce8842c1ad8 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/StorefrontCMSPageSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/StorefrontCMSPageSection.xml @@ -10,9 +10,10 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontCMSPageSection"> <element name="mediaDescription" type="text" selector=".column.main>p>img"/> - <element name="imageSource" type="text" selector="//img[contains(@src,'{{var1}}')]" parameterized="true"/> + <element name="imageSource" type="text" selector="//img[contains(@src,'{{imageName}}')]" parameterized="true"/> <element name="mainTitle" type="text" selector="#maincontent .page-title"/> <element name="mainContent" type="text" selector="#maincontent"/> <element name="footerTop" type="text" selector="footer.page-footer"/> + <element name="title" type="text" selector="//div[@class='breadcrumbs']//ul/li[@class='item cms_page']"/> </section> </sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/StorefrontHeaderSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/StorefrontHeaderSection.xml deleted file mode 100644 index d26f7d83616d..000000000000 --- a/app/code/Magento/Cms/Test/Mftf/Section/StorefrontHeaderSection.xml +++ /dev/null @@ -1,13 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> - <section name="StorefrontHeaderSection"> - </section> -</sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection.xml b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection.xml index 75a399720cd6..ff6167ffc10e 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection.xml @@ -31,18 +31,23 @@ <element name="InsertImage" type="button" selector=".mce-i-image" /> <element name="InsertTable" type="button" selector=".mce-i-table" /> <element name="SpecialCharacter" type="button" selector=".mce-i-charmap" /> + <element name="WidgetButton" type="button" selector="span[class*='magento-widget mceNonEditable']"/> + <element name="EditorContent" type="input" selector="#tinymce"/> </section> <section name="MediaGallerySection"> <element name="Browse" type="button" selector=".mce-i-browse"/> + <element name="browseForImage" type="button" selector="//*[@id='srcbrowser']"/> <element name="BrowseUploadImage" type="file" selector=".fileupload" /> <element name="image" type="text" selector="//small[text()='{{var1}}']" parameterized="true"/> <element name="imageOrImageCopy" type="text" selector="//div[contains(@class,'media-gallery-modal')]//img[contains(@alt, '{{arg1}}.{{arg2}}')]|//img[contains(@alt,'{{arg1}}_') and contains(@alt,'.{{arg2}}')]" parameterized="true"/> <element name="imageSelected" type="text" selector="//small[text()='{{var1}}']/parent::*[@class='filecnt selected']" parameterized="true"/> <element name="ImageSource" type="input" selector=".mce-combobox.mce-abs-layout-item.mce-last.mce-has-open" /> <element name="ImageDescription" type="input" selector=".mce-textbox.mce-abs-layout-item.mce-last" /> + <element name="ImageDescriptionTinyMCE3" type="input" selector="#alt" /> <element name="Height" type="input" selector=".mce-textbox.mce-abs-layout-item.mce-first" /> <element name="UploadImage" type="file" selector=".fileupload" /> <element name="OkBtn" type="button" selector="//span[text()='Ok']"/> + <element name="insertBtn" type="button" selector="#insert"/> <element name="InsertFile" type="text" selector="#insert_files"/> <element name="CreateFolder" type="button" selector="#new_folder" /> <element name="DeleteSelectedBtn" type="text" selector="#delete_files"/> @@ -54,6 +59,7 @@ <element name="WysiwygArrow" type="button" selector="#d3lzaXd5Zw-- > .jstree-icon" /> <element name="checkIfWysiwygArrowExpand" type="button" selector="//li[@id='d3lzaXd5Zw--' and contains(@class,'jstree-closed')]" /> <element name="confirmDelete" type="button" selector=".action-primary.action-accept" /> + <element name="imageBlockByName" type="block" selector="//div[@data-row='file'][contains(., '{{imageName}}')]" parameterized="true"/> </section> <section name="VariableSection"> <element name="InsertWidget" type="button" selector="#insert_variable"/> @@ -95,6 +101,10 @@ <element name="AddParam" type="button" selector=".rule-param-add"/> <element name="ConditionsDropdown" type="select" selector="#conditions__1__new_child"/> <element name="RuleParam" type="button" selector="//a[text()='...']"/> + <element name="RuleParam1" type="button" selector="(//span[@class='rule-param']//a)[{{var}}]" parameterized="true"/> + <element name="RuleParamSelect" type="select" selector="//ul[contains(@class,'rule-param-children')]/li[{{arg1}}]//*[contains(@class,'rule-param')][{{arg2}}]//select" parameterized="true"/> + <element name="RuleParamInput" type="input" selector="//ul[contains(@class,'rule-param-children')]/li[{{arg1}}]//*[contains(@class,'rule-param')][{{arg2}}]//input" parameterized="true"/> + <element name="RuleParamLabel" type="input" selector="//ul[contains(@class,'rule-param-children')]/li[{{arg1}}]//*[contains(@class,'rule-param')][{{arg2}}]//a" parameterized="true"/> <element name="Chooser" type="button" selector="//img[@title='Open Chooser']"/> <element name="PageSize" type="input" selector="input[name='parameters[page_size]']"/> <element name="ProductAttribute" type="multiselect" selector="select[name='parameters[show_attributes][]']" /> @@ -104,5 +114,8 @@ <element name="CompareBtn" type="button" selector=".action.tocompare"/> <element name="ClearCompare" type="button" selector="#compare-clear-all"/> <element name="AcceptClear" type="button" selector=".action-primary.action-accept" /> + <element name="ChooserName" type="input" selector="input[name='chooser_name']" /> + <element name="SelectPageButton" type="button" selector="//button[@title='Select Page...']"/> + <element name="SelectPageFilterInput" type="input" selector="input.admin__control-text[name='{{filterName}}']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToCMSPageTinyMCE3Test.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToCMSPageTinyMCE3Test.xml new file mode 100644 index 000000000000..11bf03c1d5ee --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToCMSPageTinyMCE3Test.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="AdminAddImageToCMSPageTinyMCE3Test"> + <annotations> + <features value="Cms"/> + <stories value="Admin should be able to upload images with TinyMCE3 WYSIWYG"/> + <group value="Cms"/> + <title value="Verify that admin is able to upload image to a CMS Page with TinyMCE3 enabled"/> + <description value="Verify that admin is able to upload image to CMS Page with TinyMCE3 enabled"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-95725"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <!-- Choose TinyMCE3 as the default WYSIWYG editor--> + <magentoCLI command="config:set cms/wysiwyg/editor Magento_Tinymce3/tinymce3Adapter" stepKey="enableTinyMCE3"/> + </before> + <after> + <!-- Switch WYSIWYG editor to TinyMCE4--> + <comment userInput="Reset editor as TinyMCE4" stepKey="chooseTinyMCE4AsEditor"/> + <magentoCLI command="config:set cms/wysiwyg/editor mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter" stepKey="enableTinyMCE4"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <amOnPage url="{{CmsNewPagePage.url}}" stepKey="navigateToPage2"/> + <waitForPageLoad stepKey="wait5"/> + <fillField selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" userInput="{{_defaultCmsPage.title}}" stepKey="fillFieldTitle2"/> + <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickContentTab2" /> + <waitForElementVisible selector="{{TinyMCESection.TinyMCE3}}" stepKey="waitForTinyMCE3"/> + <seeElement selector="{{TinyMCESection.TinyMCE3}}" stepKey="seeTinyMCE3" /> + <wait time="3" stepKey="waiting"/> + <comment userInput="Click Insert image button" stepKey="clickImageButton"/> + <click selector="{{TinyMCESection.InsertImageBtnTinyMCE3}}" stepKey="clickInsertImage" /> + <waitForPageLoad stepKey="waitForiFrameToLoad" /> + <!-- Switch to the Edit/Insert Image iFrame --> + <comment userInput="Switching to iFrame" stepKey="insertImageiFrame"/> + <executeJS function="document.querySelector('.clearlooks2 iframe').setAttribute('name', 'insert-image');" stepKey="makeIFrameInteractable"/> + <switchToIFrame selector="insert-image" stepKey="switchToIFrame"/> + <click selector="{{MediaGallerySection.browseForImage}}" stepKey="clickBrowse"/> + <switchToIFrame stepKey="switchOutOfIFrame"/> + <waitForPageLoad stepKey="waitForPageToLoad" /> + <actionGroup ref="CreateImageFolder" stepKey="CreateImageFolder"> + <argument name="ImageFolder" value="ImageFolder"/> + </actionGroup> + <actionGroup ref="attachImage" stepKey="attachImage1"> + <argument name="Image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="saveImage" stepKey="insertImage"/> + <!-- Switching back to the Edit/Insert Image iFrame--> + <comment userInput="switching back to iFrame" stepKey="switchBackToIFrame"/> + <executeJS function="document.querySelector('.clearlooks2 iframe').setAttribute('name', 'insert-image');" stepKey="makeIFrameInteractable2"/> + <switchToIFrame selector="insert-image" stepKey="switchToIFrame2"/> + <waitForElementVisible selector="{{MediaGallerySection.insertBtn}}" stepKey="waitForInsertBtnOnIFrame" /> + <fillField selector="{{MediaGallerySection.ImageDescriptionTinyMCE3}}" userInput="{{ImageUpload.content}}" stepKey="fillImageDescription" /> + <click selector="{{MediaGallerySection.insertBtn}}" stepKey="clickInsertBtn" /> + <waitForPageLoad stepKey="wait3"/> + <click selector="{{CmsNewPagePageActionsSection.expandSplitButton}}" stepKey="expandButtonMenu"/> + <waitForElementVisible selector="{{CmsNewPagePageActionsSection.splitButtonMenu}}" stepKey="waitForSplitButtonMenuVisible"/> + <click selector="{{CmsNewPagePageActionsSection.savePage}}" stepKey="clickSavePage"/> + <see userInput="You saved the page." stepKey="seeSuccessMessage"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml index 9d19f7307dd2..03edc69e6d62 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml @@ -16,9 +16,6 @@ <description value="Admin should be able to add image to WYSIWYG content of Block"/> <severity value="CRITICAL"/> <testCaseId value="MAGETWO-84376"/> - <skip> - <issueId value="MQE-1187" /> - </skip> </annotations> <before> <createData entity="_defaultCmsPage" stepKey="createCMSPage" /> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCMSPageLinkTypeTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCMSPageLinkTypeTest.xml index ded94eab9204..1adb781a6753 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCMSPageLinkTypeTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCMSPageLinkTypeTest.xml @@ -36,7 +36,7 @@ <see userInput="Inserting a widget does not create a widget instance." stepKey="seeMessage" /> <!--see Insert Widget button disabled--> <see selector="{{WidgetSection.InsertWidgetBtnDisabled}}" userInput="Insert Widget" stepKey="seeInsertWidgetDisabled" /> - <!--see Cancel button enabed--> + <!--see Cancel button enabled--> <see selector="{{WidgetSection.CancelBtnEnabled}}" userInput="Cancel" stepKey="seeCancelBtnEnabled" /> <!--Select "Widget Type"--> <selectOption selector="{{WidgetSection.WidgetType}}" userInput="CMS Page Link" stepKey="selectCMSPageLink" /> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogCategoryLinkTypeTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogCategoryLinkTypeTest.xml index 5b3679bed77e..393e25e474f1 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogCategoryLinkTypeTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogCategoryLinkTypeTest.xml @@ -46,7 +46,9 @@ <selectOption selector="{{WidgetSection.WidgetTemplate}}" userInput="Category Link Block Template" stepKey="selectTemplate" /> <click selector="{{WidgetSection.BtnChooser}}" stepKey="clickSelectCategoryBtn" /> <waitForLoadingMaskToDisappear stepKey="wait3"/> - <click userInput="$$createPreReqCategory.name$$" stepKey="selectPreCreateCategory" /> + <click selector="{{AdminCategorySidebarTreeSection.expandRootCategory}}" stepKey="expandRootCategory" /> + <waitForElementVisible selector="{{WidgetSection.PreCreateCategory('$$createPreReqCategory.name$$')}}" stepKey="expandWait" /> + <click selector="{{WidgetSection.PreCreateCategory('$$createPreReqCategory.name$$')}}" stepKey="selectPreCreateCategory" /> <waitForElementNotVisible selector="{{WidgetSection.SelectCategoryTitle}}" stepKey="waitForSlideoutCloses1" /> <click selector="{{WidgetSection.InsertWidget}}" stepKey="clickInsertWidget" /> <waitForElementNotVisible selector="{{WidgetSection.InsertWidgetTitle}}" stepKey="waitForSlideOutCloses2" /> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductLinkTypeTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductLinkTypeTest.xml index 123d25f92b6b..9ee9d27de477 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductLinkTypeTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductLinkTypeTest.xml @@ -50,6 +50,8 @@ <selectOption selector="{{WidgetSection.WidgetTemplate}}" userInput="Product Link Block Template" stepKey="selectTemplate" /> <click selector="{{WidgetSection.BtnChooser}}" stepKey="clickSelectPageBtn" /> <waitForLoadingMaskToDisappear stepKey="wait4"/> + <click selector="{{AdminCategorySidebarTreeSection.expandRootCategory}}" stepKey="expandRootCategory" /> + <waitForElementVisible selector="{{WidgetSection.PreCreateCategory('$$createPreReqCategory.name$$')}}" stepKey="expandWait" /> <click selector="{{WidgetSection.PreCreateCategory('$$createPreReqCategory.name$$')}}" stepKey="selectPreCategory" /> <waitForLoadingMaskToDisappear stepKey="waitLoadingMask" /> <click selector="{{WidgetSection.PreCreateProduct('$$createPreReqProduct.name$$')}}" stepKey="selectPreProduct" /> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductListTypeTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductListTypeTest.xml index 705f2883f583..394d79bda1ab 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductListTypeTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductListTypeTest.xml @@ -42,7 +42,7 @@ <see userInput="Inserting a widget does not create a widget instance." stepKey="seeMessage" /> <!--see Insert Widget button disabled--> <see selector="{{WidgetSection.InsertWidgetBtnDisabled}}" userInput="Insert Widget" stepKey="seeInsertWidgetDisabled" /> - <!--see Cancel button enabed--> + <!--see Cancel button enabled--> <see selector="{{WidgetSection.CancelBtnEnabled}}" userInput="Cancel" stepKey="seeCancelBtnEnabled" /> <!--Select "Widget Type"--> <selectOption selector="{{WidgetSection.WidgetType}}" userInput="Catalog Products List" stepKey="selectCatalogProductsList" /> @@ -57,8 +57,29 @@ <click selector="{{WidgetSection.RuleParam}}" stepKey="clickRuleParam" /> <waitForElementVisible selector="{{WidgetSection.Chooser}}" stepKey="waitForElement" /> <click selector="{{WidgetSection.Chooser}}" stepKey="clickChooser" /> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear3" /> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear4" /> <click selector="{{WidgetSection.PreCreateCategory('$$createPreReqCategory.name$$')}}" stepKey="selectPreCategory" /> + + <!-- Test that the "<" operand functions correctly --> + <click selector="{{WidgetSection.AddParam}}" stepKey="clickAddParamBtn2" /> + <waitForElementVisible selector="{{WidgetSection.ConditionsDropdown}}" stepKey="waitForDropdownVisible2"/> + <selectOption selector="{{WidgetSection.ConditionsDropdown}}" userInput="Price" stepKey="selectPriceCondition"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear3"/> + <click selector="{{WidgetSection.RuleParamLabel('2','1')}}" stepKey="clickOperatorLabel"/> + <selectOption selector="{{WidgetSection.RuleParamSelect('2','1')}}" userInput="<" stepKey="selectLessThanCondition"/> + <click selector="{{WidgetSection.RuleParam}}" stepKey="clickRuleParam2"/> + <fillField selector="{{WidgetSection.RuleParamInput('2','2')}}" userInput="125" stepKey="fillMaxPrice"/> + + <!-- Test that the ">" operand functions correctly --> + <click selector="{{WidgetSection.AddParam}}" stepKey="clickAddParamBtn3" /> + <waitForElementVisible selector="{{WidgetSection.ConditionsDropdown}}" stepKey="waitForDropdownVisible3"/> + <selectOption selector="{{WidgetSection.ConditionsDropdown}}" userInput="Price" stepKey="selectPriceCondition2"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear5"/> + <click selector="{{WidgetSection.RuleParamLabel('3','1')}}" stepKey="clickOperatorLabel2"/> + <selectOption selector="{{WidgetSection.RuleParamSelect('3','1')}}" userInput=">" stepKey="selectLessThanCondition2"/> + <click selector="{{WidgetSection.RuleParam}}" stepKey="clickRuleParam3"/> + <fillField selector="{{WidgetSection.RuleParamInput('3','2')}}" userInput="1" stepKey="fillMinPrice"/> + <click selector="{{WidgetSection.InsertWidget}}" stepKey="clickInsertWidget" /> <waitForPageLoad stepKey="wait6" /> <scrollTo selector="{{CmsNewPagePageSeoSection.header}}" stepKey="scrollToSearchEngineTab" /> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyComparedProductsTypeTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyComparedProductsTypeTest.xml index c89aa27c10e7..862f51ea72fa 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyComparedProductsTypeTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyComparedProductsTypeTest.xml @@ -17,8 +17,6 @@ <description value="Admin should be able to create a CMS page with widget type: Recently Compared Products"/> <severity value="CRITICAL"/> <testCaseId value="MAGETWO-83792"/> - <!--Skip test due to MC-1284--> - <group value="skip"/> </annotations> <!--Main test--> <before> @@ -43,7 +41,7 @@ <waitForPageLoad stepKey="wait2"/> <!--see Insert Widget button disabled--> <see selector="{{WidgetSection.InsertWidgetBtnDisabled}}" userInput="Insert Widget" stepKey="seeInsertWidgetDisabled" /> - <!--see Cancel button enabed--> + <!--see Cancel button enabled--> <see selector="{{WidgetSection.CancelBtnEnabled}}" userInput="Cancel" stepKey="seeCancelBtnEnabled" /> <!--Select "Widget Type"--> <selectOption selector="{{WidgetSection.WidgetType}}" userInput="Recently Compared Products" stepKey="selectRecentlyComparedProducts" /> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyViewedProductsTypeTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyViewedProductsTypeTest.xml index 9cdbccd1f8c3..298aed917fc1 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyViewedProductsTypeTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyViewedProductsTypeTest.xml @@ -40,7 +40,7 @@ <see userInput="Inserting a widget does not create a widget instance." stepKey="seeMessage" /> <!--see Insert Widget button disabled--> <see selector="{{WidgetSection.InsertWidgetBtnDisabled}}" userInput="Insert Widget" stepKey="seeInsertWidgetDisabled" /> - <!--see Cancel button enabed--> + <!--see Cancel button enabled--> <see selector="{{WidgetSection.CancelBtnEnabled}}" userInput="Cancel" stepKey="seeCancelBtnEnabled" /> <!--Select "Widget Type"--> <selectOption selector="{{WidgetSection.WidgetType}}" userInput="Recently Viewed Products" stepKey="selectRecentlyViewedProducts" /> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminContentBlocksNavigateMenuTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminContentBlocksNavigateMenuTest.xml new file mode 100644 index 000000000000..19f501d6aa20 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminContentBlocksNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminContentBlocksNavigateMenuTest"> + <annotations> + <features value="Cms"/> + <stories value="Menu Navigation"/> + <title value="Admin content blocks navigate menu test"/> + <description value="Admin should be able to navigate to Content > Blocks"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14129"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToContentBlocksPage"> + <argument name="menuUiId" value="{{AdminMenuContent.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuContentElementsBlocks.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuContentElementsBlocks.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminContentPagesNavigateMenuTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminContentPagesNavigateMenuTest.xml new file mode 100644 index 000000000000..323a1de7b9a4 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminContentPagesNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminContentPagesNavigateMenuTest"> + <annotations> + <features value="Cms"/> + <stories value="Menu Navigation"/> + <title value="Admin content pages navigate menu test"/> + <description value="Admin should be able to navigate to Content > Pages"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14128"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToContentPagesPage"> + <argument name="menuUiId" value="{{AdminMenuContent.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuContentElementsPages.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuContentElementsPages.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateCmsBlockTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateCmsBlockTest.xml index e2c74823dfff..7ab0d9209dde 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateCmsBlockTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateCmsBlockTest.xml @@ -23,6 +23,9 @@ <actionGroup ref="DisabledWYSIWYG" stepKey="disableWYSIWYG"/> </before> <after> + <actionGroup ref="deleteBlock" stepKey="deleteCreatedBlock"> + <argument name="Block" value="_defaultBlock"/> + </actionGroup> <actionGroup ref="logout" stepKey="logout"/> </after> <amOnPage url="{{CmsNewBlock.url}}" stepKey="amOnBlocksCreationForm"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/CheckStaticBlocksTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/CheckStaticBlocksTest.xml new file mode 100644 index 000000000000..e6ab1c130606 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/CheckStaticBlocksTest.xml @@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="CheckStaticBlocksTest"> + <annotations> + <features value="Cms"/> + <stories value="MAGETWO-91559 - Static blocks with same ID appear in place of correct block"/> + <title value="Check static blocks: ID should be unique per Store View"/> + <description value="Check static blocks: ID should be unique per Store View"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-94229"/> + <group value="Cms"/> + </annotations> + <before> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="AdminCreateWebsite"> + <argument name="newWebsiteName" value="secondWebsite"/> + <argument name="websiteCode" value="second_website"/> + </actionGroup> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="AdminCreateStore"> + <argument name="website" value="secondWebsite"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="AdminCreateStoreView"> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStore"/> + </actionGroup> + </before> + + <!--Go to Cms blocks page--> + <amOnPage url="{{CmsBlocksPage.url}}" stepKey="navigateToCMSPagesGrid"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <seeInCurrentUrl url="cms/block/" stepKey="VerifyPageIsOpened"/> + <!--Click to create new block--> + <click selector="{{BlockPageActionsSection.addNewBlock}}" stepKey="ClickToAddNewBlock"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <seeInCurrentUrl url="cms/block/new" stepKey="VerifyNewBlockPageIsOpened"/> + <actionGroup ref="FillOutBlockContent" stepKey="FillOutBlockContent"/> + <click selector="{{BlockNewPagePageActionsSection.saveBlock}}" stepKey="ClickToSaveBlock"/> + <waitForPageLoad stepKey="waitForPageLoad3"/> + <see userInput="You saved the block." stepKey="VerifyBlockIsSaved"/> + <!--Click to go back and add new block--> + <click selector="{{BlockNewPagePageActionsSection.back}}" stepKey="ClickToGoBack"/> + <waitForPageLoad stepKey="waitForPageLoad4"/> + <click selector="{{BlockPageActionsSection.addNewBlock}}" stepKey="ClickToAddNewBlock1"/> + <waitForPageLoad stepKey="waitForPageLoad5"/> + <seeInCurrentUrl url="cms/block/new" stepKey="VerifyNewBlockPageIsOpened1"/> + <!--Add new BLock with the same data--> + <actionGroup ref="FillOutBlockContent" stepKey="FillOutBlockContent1"/> + <selectOption selector="{{BlockNewPageBasicFieldsSection.storeView}}" userInput="Default Store View" stepKey="selectDefaultStoreView" /> + <selectOption selector="{{BlockNewPageBasicFieldsSection.storeView}}" userInput="{{customStore.name}}" stepKey="selectSecondStoreView1" /> + <click selector="{{BlockNewPagePageActionsSection.saveBlock}}" stepKey="ClickToSaveBlock1"/> + <waitForPageLoad stepKey="waitForPageLoad6"/> + <!--Verify that corresponding message is displayed--> + <see userInput="A block identifier with the same properties already exists in the selected store." stepKey="VerifyBlockIsSaved1"/> + + <after> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="DeleteWebsite"> + <argument name="websiteName" value="secondWebsite"/> + </actionGroup> + <actionGroup ref="DeleteCMSBlockActionGroup" stepKey="DeleteCMSBlockActionGroup"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontMobileViewValidation.xml b/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontMobileViewValidation.xml index d1c44383b4ac..6165def067ef 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontMobileViewValidation.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontMobileViewValidation.xml @@ -11,6 +11,7 @@ <test name="StoreFrontMobileViewValidation"> <annotations> <features value="Cms"/> + <stories value="Mobile view page footer should stick to the bottom of page on Store front"/> <title value="Mobile view page footer should stick to the bottom of page on Store front"/> <description value="Mobile view page footer should stick to the bottom of page on Store front"/> <severity value="MAJOR"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/StoreViewLanguageCorrectSwitchTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/StoreViewLanguageCorrectSwitchTest.xml new file mode 100644 index 000000000000..2c351a12af72 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/StoreViewLanguageCorrectSwitchTest.xml @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StoreViewLanguageCorrectSwitchTest"> + <annotations> + <features value="Cms"/> + <title value="Check that Store View(language) switches correct"/> + <description value="Check that Store View(language) switches correct"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-96388"/> + <useCaseId value="MAGETWO-57337"/> + <group value="Cms"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Create Cms Pages --> + <createData entity="_newDefaultCmsPage" stepKey="createFirstCmsPage"/> + <createData entity="_newDefaultCmsPage" stepKey="createSecondCmsPage"/> + </before> + <after> + <deleteData createDataKey="createFirstCmsPage" stepKey="deleteFirstCmsPage"/> + <deleteData createDataKey="createSecondCmsPage" stepKey="deleteSecondCmsPage"/> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"> + <argument name="customStore" value="NewStoreViewData"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Create StoreView --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> + <argument name="customStore" value="NewStoreViewData"/> + </actionGroup> + + <!-- Add StoreView To Cms Page--> + <actionGroup ref="AddStoreViewToCmsPage" stepKey="gotToCmsPage"> + <argument name="CMSPage" value="$$createSecondCmsPage$$"/> + <argument name="storeViewName" value="{{NewStoreViewData.name}}"/> + </actionGroup> + + <!-- Check that Cms Page is open --> + <amOnPage url="{{StorefrontHomePage.url}}/$$createFirstCmsPage.identifier$$" stepKey="gotToFirstCmsPage"/> + <see userInput="$$createFirstCmsPage.title$$" stepKey="seePageTitle"/> + + <!-- Switch StoreView and check that Cms Page is open --> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchToCustomStoreView"> + <argument name="storeView" value="NewStoreViewData"/> + </actionGroup> + <amOnPage url="{{StorefrontHomePage.url}}/$$createSecondCmsPage.identifier$$" stepKey="gotToSecondCmsPage"/> + <see userInput="$$createSecondCmsPage.title$$" stepKey="seePageTitle1"/> + + <!--Open first Cms page on custom store view--> + <amOnPage url="{{StorefrontHomePage.url}}/$$createFirstCmsPage.identifier$$" stepKey="gotToFirstCmsPage1"/> + <see userInput="Whoops, our bad..." stepKey="seePageError"/> + + <!--Switch to default store view and check Cms page--> + <actionGroup ref="StorefrontSwitchDefaultStoreViewActionGroup" stepKey="switchToDefualtStoreView"/> + <see userInput="$$createFirstCmsPage.title$$" stepKey="seePageTitle2"/> + </test> +</tests> diff --git a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php index 2dc98bcefb96..7bec1e360146 100644 --- a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php +++ b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php @@ -417,6 +417,10 @@ protected function generalTestGetDirsCollection($path, $collectionArray = [], $e ->method('setCollectRecursively') ->with(false) ->willReturnSelf(); + $storageCollectionMock->expects($this->once()) + ->method('setOrder') + ->with('basename', \Magento\Framework\Data\Collection\Filesystem::SORT_ORDER_ASC) + ->willReturnSelf(); $storageCollectionMock->expects($this->once()) ->method('getIterator') ->willReturn(new \ArrayIterator($collectionArray)); @@ -440,14 +444,7 @@ public function testUploadFile() $thumbnailDestination = $thumbnailTargetPath . '/' . $fileName; $type = 'image'; $result = [ - 'result', - 'cookie' => [ - 'name' => 'session_name', - 'value' => '1', - 'lifetime' => '50', - 'path' => 'cookie/path', - 'domain' => 'cookie_domain', - ], + 'result' ]; $uploader = $this->getMockBuilder(\Magento\MediaStorage\Model\File\Uploader::class) ->disableOriginalConstructor() @@ -507,17 +504,6 @@ public function testUploadFile() $this->adapterFactoryMock->expects($this->atLeastOnce())->method('create')->willReturn($image); - $this->sessionMock->expects($this->atLeastOnce())->method('getName') - ->willReturn($result['cookie']['name']); - $this->sessionMock->expects($this->atLeastOnce())->method('getSessionId') - ->willReturn($result['cookie']['value']); - $this->sessionMock->expects($this->atLeastOnce())->method('getCookieLifetime') - ->willReturn($result['cookie']['lifetime']); - $this->sessionMock->expects($this->atLeastOnce())->method('getCookiePath') - ->willReturn($result['cookie']['path']); - $this->sessionMock->expects($this->atLeastOnce())->method('getCookieDomain') - ->willReturn($result['cookie']['domain']); - $this->assertEquals($result, $this->imagesStorage->uploadFile($targetPath, $type)); } } diff --git a/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/BlockActionsTest.php b/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/BlockActionsTest.php index 3dcf6c4a3fce..3095abef7bbe 100644 --- a/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/BlockActionsTest.php +++ b/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/BlockActionsTest.php @@ -95,6 +95,7 @@ public function testPrepareDataSource() 'title' => __('Delete %1', $title), 'message' => __('Are you sure you want to delete a %1 record?', $title) ], + 'post' => true ] ], ] diff --git a/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/PageActionsTest.php b/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/PageActionsTest.php index b0cc1bf061a4..9b3165a2c551 100644 --- a/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/PageActionsTest.php +++ b/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/Column/PageActionsTest.php @@ -70,6 +70,7 @@ public function testPrepareItemsByPageId() 'title' => __('Delete %1', $title), 'message' => __('Are you sure you want to delete a %1 record?', $title) ], + 'post' => true ] ], ] diff --git a/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/DataProviderTest.php b/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/DataProviderTest.php index 54e0e17ab7ad..a624823d02c1 100644 --- a/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/DataProviderTest.php +++ b/app/code/Magento/Cms/Test/Unit/Ui/Component/Listing/DataProviderTest.php @@ -118,7 +118,8 @@ public function testPrepareMetadata() 'config' => [ 'editorConfig' => [ 'enabled' => false - ] + ], + 'componentType' => \Magento\Ui\Component\Container::NAME ] ] ] diff --git a/app/code/Magento/Cms/Ui/Component/DataProvider.php b/app/code/Magento/Cms/Ui/Component/DataProvider.php index 5fc9c5a89603..b02dd6ba98ed 100644 --- a/app/code/Magento/Cms/Ui/Component/DataProvider.php +++ b/app/code/Magento/Cms/Ui/Component/DataProvider.php @@ -13,6 +13,9 @@ use Magento\Framework\AuthorizationInterface; use Magento\Framework\View\Element\UiComponent\DataProvider\Reporting; +/** + * DataProvider for cms ui. + */ class DataProvider extends \Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider { /** @@ -67,6 +70,8 @@ public function __construct( } /** + * Get authorization info. + * * @deprecated 101.0.7 * @return AuthorizationInterface|mixed */ @@ -95,7 +100,8 @@ public function prepareMetadata() 'config' => [ 'editorConfig' => [ 'enabled' => false - ] + ], + 'componentType' => \Magento\Ui\Component\Container::NAME ] ] ] diff --git a/app/code/Magento/Cms/Ui/Component/Listing/Column/BlockActions.php b/app/code/Magento/Cms/Ui/Component/Listing/Column/BlockActions.php index 60b9f34d29ae..f68ef35e534f 100644 --- a/app/code/Magento/Cms/Ui/Component/Listing/Column/BlockActions.php +++ b/app/code/Magento/Cms/Ui/Component/Listing/Column/BlockActions.php @@ -55,10 +55,7 @@ public function __construct( } /** - * Prepare Data Source - * - * @param array $dataSource - * @return array + * @inheritDoc */ public function prepareDataSource(array $dataSource) { @@ -87,7 +84,8 @@ public function prepareDataSource(array $dataSource) 'confirm' => [ 'title' => __('Delete %1', $title), 'message' => __('Are you sure you want to delete a %1 record?', $title) - ] + ], + 'post' => true ] ]; } @@ -99,6 +97,7 @@ public function prepareDataSource(array $dataSource) /** * Get instance of escaper + * * @return Escaper * @deprecated 101.0.7 */ diff --git a/app/code/Magento/Cms/Ui/Component/Listing/Column/PageActions.php b/app/code/Magento/Cms/Ui/Component/Listing/Column/PageActions.php index ea6882e21c85..26d31456bf61 100644 --- a/app/code/Magento/Cms/Ui/Component/Listing/Column/PageActions.php +++ b/app/code/Magento/Cms/Ui/Component/Listing/Column/PageActions.php @@ -67,10 +67,7 @@ public function __construct( } /** - * Prepare Data Source - * - * @param array $dataSource - * @return array + * @inheritDoc */ public function prepareDataSource(array $dataSource) { @@ -89,7 +86,8 @@ public function prepareDataSource(array $dataSource) 'confirm' => [ 'title' => __('Delete %1', $title), 'message' => __('Are you sure you want to delete a %1 record?', $title) - ] + ], + 'post' => true ]; } if (isset($item['identifier'])) { @@ -110,6 +108,7 @@ public function prepareDataSource(array $dataSource) /** * Get instance of escaper + * * @return Escaper * @deprecated 101.0.7 */ diff --git a/app/code/Magento/Cms/etc/db_schema.xml b/app/code/Magento/Cms/etc/db_schema.xml index 2b825544f56f..1e64c905badd 100644 --- a/app/code/Magento/Cms/etc/db_schema.xml +++ b/app/code/Magento/Cms/etc/db_schema.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="cms_block" resource="default" engine="innodb" comment="CMS Block Table"> <column xsi:type="smallint" name="block_id" padding="6" unsigned="false" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="varchar" name="title" nullable="false" length="255" comment="Block Title"/> <column xsi:type="varchar" name="identifier" nullable="false" length="255" comment="Block String Identifier"/> <column xsi:type="mediumtext" name="content" nullable="true" comment="Block Content"/> @@ -19,10 +19,10 @@ comment="Block Modification Time"/> <column xsi:type="smallint" name="is_active" padding="6" unsigned="false" nullable="false" identity="false" default="1" comment="Is Block Active"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="block_id"/> </constraint> - <index name="CMS_BLOCK_TITLE_IDENTIFIER_CONTENT" indexType="fulltext"> + <index referenceId="CMS_BLOCK_TITLE_IDENTIFIER_CONTENT" indexType="fulltext"> <column name="title"/> <column name="identifier"/> <column name="content"/> @@ -32,21 +32,21 @@ <column xsi:type="smallint" name="block_id" padding="6" unsigned="false" nullable="false" identity="false"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Store ID"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="block_id"/> <column name="store_id"/> </constraint> - <constraint xsi:type="foreign" name="CMS_BLOCK_STORE_BLOCK_ID_CMS_BLOCK_BLOCK_ID" table="cms_block_store" + <constraint xsi:type="foreign" referenceId="CMS_BLOCK_STORE_BLOCK_ID_CMS_BLOCK_BLOCK_ID" table="cms_block_store" column="block_id" referenceTable="cms_block" referenceColumn="block_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CMS_BLOCK_STORE_STORE_ID_STORE_STORE_ID" table="cms_block_store" + <constraint xsi:type="foreign" referenceId="CMS_BLOCK_STORE_STORE_ID_STORE_STORE_ID" table="cms_block_store" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <index name="CMS_BLOCK_STORE_STORE_ID" indexType="btree"> + <index referenceId="CMS_BLOCK_STORE_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> <table name="cms_page" resource="default" engine="innodb" comment="CMS Page Table"> <column xsi:type="smallint" name="page_id" padding="6" unsigned="false" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="varchar" name="title" nullable="true" length="255" comment="Page Title"/> <column xsi:type="varchar" name="page_layout" nullable="true" length="255" comment="Page Layout"/> <column xsi:type="text" name="meta_keywords" nullable="true" comment="Page Meta Keywords"/> @@ -71,13 +71,13 @@ <column xsi:type="date" name="custom_theme_from" comment="Page Custom Theme Active From Date"/> <column xsi:type="date" name="custom_theme_to" comment="Page Custom Theme Active To Date"/> <column xsi:type="varchar" name="meta_title" nullable="true" length="255" comment="Page Meta Title"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="page_id"/> </constraint> - <index name="CMS_PAGE_IDENTIFIER" indexType="btree"> + <index referenceId="CMS_PAGE_IDENTIFIER" indexType="btree"> <column name="identifier"/> </index> - <index name="CMS_PAGE_TITLE_META_KEYWORDS_META_DESCRIPTION_IDENTIFIER_CONTENT" indexType="fulltext"> + <index referenceId="CMS_PAGE_TITLE_META_KEYWORDS_META_DESCRIPTION_IDENTIFIER_CONTENT" indexType="fulltext"> <column name="title"/> <column name="meta_keywords"/> <column name="meta_description"/> @@ -86,18 +86,19 @@ </index> </table> <table name="cms_page_store" resource="default" engine="innodb" comment="CMS Page To Store Linkage Table"> - <column xsi:type="smallint" name="page_id" padding="6" unsigned="false" nullable="false" identity="false"/> + <column xsi:type="smallint" name="page_id" padding="6" unsigned="false" nullable="false" identity="false" + comment="Entity ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Store ID"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="page_id"/> <column name="store_id"/> </constraint> - <constraint xsi:type="foreign" name="CMS_PAGE_STORE_PAGE_ID_CMS_PAGE_PAGE_ID" table="cms_page_store" + <constraint xsi:type="foreign" referenceId="CMS_PAGE_STORE_PAGE_ID_CMS_PAGE_PAGE_ID" table="cms_page_store" column="page_id" referenceTable="cms_page" referenceColumn="page_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CMS_PAGE_STORE_STORE_ID_STORE_STORE_ID" table="cms_page_store" + <constraint xsi:type="foreign" referenceId="CMS_PAGE_STORE_STORE_ID_STORE_STORE_ID" table="cms_page_store" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <index name="CMS_PAGE_STORE_STORE_ID" indexType="btree"> + <index referenceId="CMS_PAGE_STORE_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> diff --git a/app/code/Magento/Cms/view/adminhtml/layout/cms_wysiwyg_images_index.xml b/app/code/Magento/Cms/view/adminhtml/layout/cms_wysiwyg_images_index.xml index 1bc8828ef6c8..6703b6c27712 100644 --- a/app/code/Magento/Cms/view/adminhtml/layout/cms_wysiwyg_images_index.xml +++ b/app/code/Magento/Cms/view/adminhtml/layout/cms_wysiwyg_images_index.xml @@ -9,7 +9,11 @@ <container name="root"> <block class="Magento\Cms\Block\Adminhtml\Wysiwyg\Images\Content" name="wysiwyg_images.content" template="Magento_Cms::browser/content.phtml"> <block class="Magento\Cms\Block\Adminhtml\Wysiwyg\Images\Tree" name="wysiwyg_images.tree" template="Magento_Cms::browser/tree.phtml"/> - <block class="Magento\Cms\Block\Adminhtml\Wysiwyg\Images\Content\Uploader" name="wysiwyg_images.uploader" template="Magento_Cms::browser/content/uploader.phtml"/> + <block class="Magento\Cms\Block\Adminhtml\Wysiwyg\Images\Content\Uploader" name="wysiwyg_images.uploader" template="Magento_Cms::browser/content/uploader.phtml"> + <arguments> + <argument name="image_upload_config_data" xsi:type="object">Magento\Backend\Block\DataProviders\ImageUploadConfig</argument> + </arguments> + </block> </block> </container> </layout> diff --git a/app/code/Magento/Cms/view/adminhtml/templates/browser/content/files.phtml b/app/code/Magento/Cms/view/adminhtml/templates/browser/content/files.phtml index da8999186992..44bd7d3ba3dd 100644 --- a/app/code/Magento/Cms/view/adminhtml/templates/browser/content/files.phtml +++ b/app/code/Magento/Cms/view/adminhtml/templates/browser/content/files.phtml @@ -21,7 +21,7 @@ $_height = $block->getImagesHeight(); data-size="<?= $block->escapeHtmlAttr($file->getSize()) ?>" data-mime-type="<?= $block->escapeHtmlAttr($file->getMimeType()) ?>" > - <p class="nm" style="height:<?= $block->escapeHtmlAttr($_height) ?>px;width:<?= $block->escapeHtmlAttr($_width) ?>px;"> + <p class="nm" style="height:<?= $block->escapeHtmlAttr($_height) ?>px;"> <?php if ($block->getFileThumbUrl($file)):?> <img src="<?= $block->escapeHtmlAttr($block->getFileThumbUrl($file)) ?>" alt="<?= $block->escapeHtmlAttr($block->getFileName($file)) ?>"/> <?php endif; ?> diff --git a/app/code/Magento/Cms/view/adminhtml/templates/browser/content/uploader.phtml b/app/code/Magento/Cms/view/adminhtml/templates/browser/content/uploader.phtml index 097235bc9fb7..414d42cb4538 100644 --- a/app/code/Magento/Cms/view/adminhtml/templates/browser/content/uploader.phtml +++ b/app/code/Magento/Cms/view/adminhtml/templates/browser/content/uploader.phtml @@ -17,6 +17,13 @@ foreach ($filters as $media_type) { }, $media_type['files'])); } +$resizeConfig = $block->getImageUploadConfigData()->getIsResizeEnabled() + ? "{action: 'resize', maxWidth: " + . $block->escapeHtml($block->getImageUploadMaxWidth()) + . ", maxHeight: " + . $block->escapeHtml($block->getImageUploadMaxHeight()) + . "}" + : "{action: 'resize'}"; ?> <div id="<?= $block->getHtmlId() ?>" class="uploader"> @@ -145,11 +152,9 @@ require([ action: 'load', fileTypes: /^image\/(gif|jpeg|png)$/, maxFileSize: <?= (int) $block->getFileSizeService()->getMaxFileSize() ?> * 10 - }, { - action: 'resize', - maxWidth: <?= (float) \Magento\Framework\File\Uploader::MAX_IMAGE_WIDTH ?> , - maxHeight: <?= (float) \Magento\Framework\File\Uploader::MAX_IMAGE_HEIGHT ?> - }, { + }, + <?= /* @noEscape */ $resizeConfig ?>, + { action: 'save' }] }); diff --git a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml index 9f886f6f1345..793fc7d26cb4 100644 --- a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml +++ b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml @@ -146,7 +146,6 @@ <editor> <validation> <rule name="required-entry" xsi:type="boolean">true</rule> - <rule name="validate-xml-identifier" xsi:type="boolean">true</rule> </validation> <editorType>text</editorType> </editor> diff --git a/app/code/Magento/CmsGraphQl/Model/Resolver/Block/Identity.php b/app/code/Magento/CmsGraphQl/Model/Resolver/Block/Identity.php new file mode 100644 index 000000000000..a40d23968c3c --- /dev/null +++ b/app/code/Magento/CmsGraphQl/Model/Resolver/Block/Identity.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CmsGraphQl\Model\Resolver\Block; + +use Magento\Cms\Api\Data\BlockInterface; +use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface; + +/** + * Identity for resolved CMS block + */ +class Identity implements IdentityInterface +{ + /** + * Get block identities from resolved data + * + * @param array $resolvedData + * @return string[] + */ + public function getIdentities(array $resolvedData): array + { + $ids = []; + $items = $resolvedData['items'] ?? []; + foreach ($items as $item) { + if (is_array($item) && !empty($item[BlockInterface::BLOCK_ID])) { + $ids[] = $item[BlockInterface::BLOCK_ID]; + $ids[] = $item[BlockInterface::IDENTIFIER]; + } + } + + return $ids; + } +} diff --git a/app/code/Magento/CmsGraphQl/Model/Resolver/Blocks.php b/app/code/Magento/CmsGraphQl/Model/Resolver/Blocks.php new file mode 100644 index 000000000000..e55db2a3fa42 --- /dev/null +++ b/app/code/Magento/CmsGraphQl/Model/Resolver/Blocks.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CmsGraphQl\Model\Resolver; + +use Magento\CmsGraphQl\Model\Resolver\DataProvider\Block as BlockDataProvider; +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; + +/** + * CMS blocks field resolver, used for GraphQL request processing + */ +class Blocks implements ResolverInterface +{ + /** + * @var BlockDataProvider + */ + private $blockDataProvider; + + /** + * @param BlockDataProvider $blockDataProvider + */ + public function __construct( + BlockDataProvider $blockDataProvider + ) { + $this->blockDataProvider = $blockDataProvider; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + + $blockIdentifiers = $this->getBlockIdentifiers($args); + $blocksData = $this->getBlocksData($blockIdentifiers); + + $resultData = [ + 'items' => $blocksData, + ]; + return $resultData; + } + + /** + * Get block identifiers + * + * @param array $args + * @return string[] + * @throws GraphQlInputException + */ + private function getBlockIdentifiers(array $args): array + { + if (!isset($args['identifiers']) || !is_array($args['identifiers']) || count($args['identifiers']) === 0) { + throw new GraphQlInputException(__('"identifiers" of CMS blocks should be specified')); + } + + return $args['identifiers']; + } + + /** + * Get blocks data + * + * @param array $blockIdentifiers + * @return array + * @throws GraphQlNoSuchEntityException + */ + private function getBlocksData(array $blockIdentifiers): array + { + $blocksData = []; + foreach ($blockIdentifiers as $blockIdentifier) { + try { + $blocksData[$blockIdentifier] = $this->blockDataProvider->getData($blockIdentifier); + } catch (NoSuchEntityException $e) { + $blocksData[$blockIdentifier] = new GraphQlNoSuchEntityException(__($e->getMessage()), $e); + } + } + return $blocksData; + } +} diff --git a/app/code/Magento/CmsGraphQl/Model/Resolver/DataProvider/Block.php b/app/code/Magento/CmsGraphQl/Model/Resolver/DataProvider/Block.php new file mode 100644 index 000000000000..fa4944381b85 --- /dev/null +++ b/app/code/Magento/CmsGraphQl/Model/Resolver/DataProvider/Block.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CmsGraphQl\Model\Resolver\DataProvider; + +use Magento\Cms\Api\BlockRepositoryInterface; +use Magento\Cms\Api\Data\BlockInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Widget\Model\Template\FilterEmulate; + +/** + * Cms block data provider + */ +class Block +{ + /** + * @var BlockRepositoryInterface + */ + private $blockRepository; + + /** + * @var FilterEmulate + */ + private $widgetFilter; + + /** + * @param BlockRepositoryInterface $blockRepository + * @param FilterEmulate $widgetFilter + */ + public function __construct( + BlockRepositoryInterface $blockRepository, + FilterEmulate $widgetFilter + ) { + $this->blockRepository = $blockRepository; + $this->widgetFilter = $widgetFilter; + } + + /** + * Get block data + * + * @param string $blockIdentifier + * @return array + * @throws NoSuchEntityException + */ + public function getData(string $blockIdentifier): array + { + $block = $this->blockRepository->getById($blockIdentifier); + + if (false === $block->isActive()) { + throw new NoSuchEntityException( + __('The CMS block with the "%1" ID doesn\'t exist.', $blockIdentifier) + ); + } + + $renderedContent = $this->widgetFilter->filter($block->getContent()); + + $blockData = [ + BlockInterface::BLOCK_ID => $block->getId(), + BlockInterface::IDENTIFIER => $block->getIdentifier(), + BlockInterface::TITLE => $block->getTitle(), + BlockInterface::CONTENT => $renderedContent, + ]; + return $blockData; + } +} diff --git a/app/code/Magento/CmsGraphQl/Model/Resolver/DataProvider/Page.php b/app/code/Magento/CmsGraphQl/Model/Resolver/DataProvider/Page.php new file mode 100644 index 000000000000..8e1e770b01e9 --- /dev/null +++ b/app/code/Magento/CmsGraphQl/Model/Resolver/DataProvider/Page.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CmsGraphQl\Model\Resolver\DataProvider; + +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Api\PageRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Widget\Model\Template\FilterEmulate; + +/** + * Cms page data provider + */ +class Page +{ + /** + * @var FilterEmulate + */ + private $widgetFilter; + + /** + * @var PageRepositoryInterface + */ + private $pageRepository; + + /** + * @param PageRepositoryInterface $pageRepository + * @param FilterEmulate $widgetFilter + */ + public function __construct( + PageRepositoryInterface $pageRepository, + FilterEmulate $widgetFilter + ) { + $this->pageRepository = $pageRepository; + $this->widgetFilter = $widgetFilter; + } + + /** + * Get the page data + * + * @param int $pageId + * @return array + * @throws NoSuchEntityException + */ + public function getData(int $pageId): array + { + $page = $this->pageRepository->getById($pageId); + + if (false === $page->isActive()) { + throw new NoSuchEntityException(); + } + + $renderedContent = $this->widgetFilter->filter($page->getContent()); + + $pageData = [ + PageInterface::PAGE_ID => $page->getId(), + 'url_key' => $page->getIdentifier(), + PageInterface::TITLE => $page->getTitle(), + PageInterface::CONTENT => $renderedContent, + PageInterface::CONTENT_HEADING => $page->getContentHeading(), + PageInterface::PAGE_LAYOUT => $page->getPageLayout(), + PageInterface::META_TITLE => $page->getMetaTitle(), + PageInterface::META_DESCRIPTION => $page->getMetaDescription(), + PageInterface::META_KEYWORDS => $page->getMetaKeywords(), + ]; + return $pageData; + } +} diff --git a/app/code/Magento/CmsGraphQl/Model/Resolver/Page.php b/app/code/Magento/CmsGraphQl/Model/Resolver/Page.php new file mode 100644 index 000000000000..1077ab81551c --- /dev/null +++ b/app/code/Magento/CmsGraphQl/Model/Resolver/Page.php @@ -0,0 +1,81 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CmsGraphQl\Model\Resolver; + +use Magento\CmsGraphQl\Model\Resolver\DataProvider\Page as PageDataProvider; +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; + +/** + * CMS page field resolver, used for GraphQL request processing + */ +class Page implements ResolverInterface +{ + /** + * @var PageDataProvider + */ + private $pageDataProvider; + + /** + * @param PageDataProvider $pageDataProvider + */ + public function __construct( + PageDataProvider $pageDataProvider + ) { + $this->pageDataProvider = $pageDataProvider; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + $pageId = $this->getPageId($args); + $pageData = $this->getPageData($pageId); + + return $pageData; + } + + /** + * @param array $args + * @return int + * @throws GraphQlInputException + */ + private function getPageId(array $args): int + { + if (!isset($args['id'])) { + throw new GraphQlInputException(__('"Page id should be specified')); + } + + return (int)$args['id']; + } + + /** + * @param int $pageId + * @return array + * @throws GraphQlNoSuchEntityException + */ + private function getPageData(int $pageId): array + { + try { + $pageData = $this->pageDataProvider->getData($pageId); + } catch (NoSuchEntityException $e) { + throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); + } + return $pageData; + } +} diff --git a/app/code/Magento/CmsGraphQl/Model/Resolver/Page/Identity.php b/app/code/Magento/CmsGraphQl/Model/Resolver/Page/Identity.php new file mode 100644 index 000000000000..abc306451e30 --- /dev/null +++ b/app/code/Magento/CmsGraphQl/Model/Resolver/Page/Identity.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CmsGraphQl\Model\Resolver\Page; + +use Magento\Cms\Api\Data\PageInterface; +use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface; + +/** + * Identity for resolved CMS page + */ +class Identity implements IdentityInterface +{ + /** + * Get page ID from resolved data + * + * @param array $resolvedData + * @return string[] + */ + public function getIdentities(array $resolvedData): array + { + return empty($resolvedData[PageInterface::PAGE_ID]) ? [] : [$resolvedData[PageInterface::PAGE_ID]]; + } +} diff --git a/app/code/Magento/CmsGraphQl/README.md b/app/code/Magento/CmsGraphQl/README.md new file mode 100644 index 000000000000..970444837f14 --- /dev/null +++ b/app/code/Magento/CmsGraphQl/README.md @@ -0,0 +1,4 @@ +# CmsGraphQl + +**CmsGraphQl** provides type information for the GraphQl module +to generate CMS fields for cms information endpoints. diff --git a/app/code/Magento/CmsGraphQl/composer.json b/app/code/Magento/CmsGraphQl/composer.json new file mode 100644 index 000000000000..18a6f1aa95e3 --- /dev/null +++ b/app/code/Magento/CmsGraphQl/composer.json @@ -0,0 +1,28 @@ +{ + "name": "magento/module-cms-graph-ql", + "description": "N/A", + "type": "magento2-module", + "require": { + "php": "~7.1.3||~7.2.0", + "magento/framework": "*", + "magento/module-cms": "*", + "magento/module-widget": "*" + }, + "suggest": { + "magento/module-graph-ql": "*", + "magento/module-graph-ql-cache": "*", + "magento/module-store-graph-ql": "*" + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\CmsGraphQl\\": "" + } + } +} diff --git a/app/code/Magento/CmsGraphQl/etc/graphql/di.xml b/app/code/Magento/CmsGraphQl/etc/graphql/di.xml new file mode 100644 index 000000000000..78c1071d8e07 --- /dev/null +++ b/app/code/Magento/CmsGraphQl/etc/graphql/di.xml @@ -0,0 +1,21 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\StoreGraphQl\Model\Resolver\Store\StoreConfigDataProvider"> + <arguments> + <argument name="extendedConfigData" xsi:type="array"> + <item name="front" xsi:type="string">web/default/front</item> + <item name="cms_home_page" xsi:type="string">web/default/cms_home_page</item> + <item name="no_route" xsi:type="string">web/default/no_route</item> + <item name="cms_no_route" xsi:type="string">web/default/cms_no_route</item> + <item name="cms_no_cookies" xsi:type="string">web/default/cms_no_cookies</item> + <item name="show_cms_breadcrumbs" xsi:type="string">web/default/show_cms_breadcrumbs</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/CmsGraphQl/etc/module.xml b/app/code/Magento/CmsGraphQl/etc/module.xml new file mode 100644 index 000000000000..4fca42430d16 --- /dev/null +++ b/app/code/Magento/CmsGraphQl/etc/module.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_CmsGraphQl"> + <sequence> + <module name="Magento_GraphQl"/> + </sequence> + </module> +</config> diff --git a/app/code/Magento/CmsGraphQl/etc/schema.graphqls b/app/code/Magento/CmsGraphQl/etc/schema.graphqls new file mode 100644 index 000000000000..04d2efa4c7cd --- /dev/null +++ b/app/code/Magento/CmsGraphQl/etc/schema.graphqls @@ -0,0 +1,41 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. +type StoreConfig @doc(description: "The type contains information about a store config") { + front : String @doc(description: "Default Web URL") + cms_home_page : String @doc(description: "CMS Home Page") + no_route : String @doc(description: "Default No-route URL") + cms_no_route : String @doc(description: "CMS No Route Page") + cms_no_cookies : String @doc(description: "CMS No Cookies Page") + show_cms_breadcrumbs : Int @doc(description: "Show Breadcrumbs for CMS Pages") +} + + +type Query { + cmsPage ( + id: Int @doc(description: "Id of the CMS page") + ): CmsPage @resolver(class: "Magento\\CmsGraphQl\\Model\\Resolver\\Page") @doc(description: "The CMS page query returns information about a CMS page") @cache(cacheTag: "cms_p", cacheIdentity: "Magento\\CmsGraphQl\\Model\\Resolver\\Page\\Identity") + cmsBlocks ( + identifiers: [String] @doc(description: "Identifiers of the CMS blocks") + ): CmsBlocks @resolver(class: "Magento\\CmsGraphQl\\Model\\Resolver\\Blocks") @doc(description: "The CMS block query returns information about CMS blocks") @cache(cacheTag: "cms_b", cacheIdentity: "Magento\\CmsGraphQl\\Model\\Resolver\\Block\\Identity") +} + +type CmsPage @doc(description: "CMS page defines all CMS page information") { + url_key: String @doc(description: "URL key of CMS page") + title: String @doc(description: "CMS page title") + content: String @doc(description: "CMS page content") + content_heading: String @doc(description: "CMS page content heading") + page_layout: String @doc(description: "CMS page content heading") + meta_title: String @doc(description: "CMS page meta title") + meta_description: String @doc(description: "CMS page meta description") + meta_keywords: String @doc(description: "CMS page meta keywords") +} + +type CmsBlocks @doc(description: "CMS blocks information") { + items: [CmsBlock] @doc(description: "An array of CMS blocks") +} + +type CmsBlock @doc(description: "CMS block defines all CMS block information") { + identifier: String @doc(description: "CMS block identifier") + title: String @doc(description: "CMS block title") + content: String @doc(description: "CMS block content") +} \ No newline at end of file diff --git a/app/code/Magento/CmsGraphQl/registration.php b/app/code/Magento/CmsGraphQl/registration.php new file mode 100644 index 000000000000..9a3fabf6c95e --- /dev/null +++ b/app/code/Magento/CmsGraphQl/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_CmsGraphQl', __DIR__); diff --git a/app/code/Magento/Config/App/Config/Type/System.php b/app/code/Magento/Config/App/Config/Type/System.php index 479b4f5c2173..c63ccae87165 100644 --- a/app/code/Magento/Config/App/Config/Type/System.php +++ b/app/code/Magento/Config/App/Config/Type/System.php @@ -3,27 +3,48 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Config\App\Config\Type; +use Magento\Framework\App\Config\ConfigSourceInterface; use Magento\Framework\App\Config\ConfigTypeInterface; +use Magento\Framework\App\Config\Spi\PostProcessorInterface; +use Magento\Framework\App\Config\Spi\PreProcessorInterface; use Magento\Framework\App\ObjectManager; use Magento\Config\App\Config\Type\System\Reader; +use Magento\Framework\App\ScopeInterface; +use Magento\Framework\Cache\FrontendInterface; +use Magento\Framework\Cache\LockGuardedCacheLoader; +use Magento\Framework\Lock\LockManagerInterface; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Store\Model\Config\Processor\Fallback; +use Magento\Framework\Encryption\Encryptor; +use Magento\Store\Model\ScopeInterface as StoreScope; /** * System configuration type + * * @api * @since 100.1.2 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.UnusedPrivateMethod) */ class System implements ConfigTypeInterface { + /** + * Config cache tag. + */ const CACHE_TAG = 'config_scopes'; + /** + * System config type. + */ const CONFIG_TYPE = 'system'; /** - * @var \Magento\Framework\App\Config\ConfigSourceInterface + * @var string */ - private $source; + private static $lockName = 'SYSTEM_CONFIG'; /** * @var array @@ -31,32 +52,17 @@ class System implements ConfigTypeInterface private $data = []; /** - * @var \Magento\Framework\App\Config\Spi\PostProcessorInterface + * @var PostProcessorInterface */ private $postProcessor; /** - * @var \Magento\Framework\App\Config\Spi\PreProcessorInterface - */ - private $preProcessor; - - /** - * @var \Magento\Framework\Cache\FrontendInterface + * @var FrontendInterface */ private $cache; /** - * @var int - */ - private $cachingNestedLevel; - - /** - * @var \Magento\Store\Model\Config\Processor\Fallback - */ - private $fallback; - - /** - * @var \Magento\Framework\Serialize\SerializerInterface + * @var SerializerInterface */ private $serializer; @@ -79,42 +85,62 @@ class System implements ConfigTypeInterface * * @var array */ - private $availableDataScopes = null; + private $availableDataScopes; /** - * @param \Magento\Framework\App\Config\ConfigSourceInterface $source - * @param \Magento\Framework\App\Config\Spi\PostProcessorInterface $postProcessor - * @param \Magento\Store\Model\Config\Processor\Fallback $fallback - * @param \Magento\Framework\Cache\FrontendInterface $cache - * @param \Magento\Framework\Serialize\SerializerInterface $serializer - * @param \Magento\Framework\App\Config\Spi\PreProcessorInterface $preProcessor + * @var Encryptor + */ + private $encryptor; + + /** + * @var LockGuardedCacheLoader + */ + private $lockQuery; + + /** + * @param ConfigSourceInterface $source + * @param PostProcessorInterface $postProcessor + * @param Fallback $fallback + * @param FrontendInterface $cache + * @param SerializerInterface $serializer + * @param PreProcessorInterface $preProcessor * @param int $cachingNestedLevel * @param string $configType - * @param Reader $reader + * @param Reader|null $reader + * @param Encryptor|null $encryptor + * @param LockManagerInterface|null $locker + * @param LockGuardedCacheLoader|null $lockQuery + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Framework\App\Config\ConfigSourceInterface $source, - \Magento\Framework\App\Config\Spi\PostProcessorInterface $postProcessor, - \Magento\Store\Model\Config\Processor\Fallback $fallback, - \Magento\Framework\Cache\FrontendInterface $cache, - \Magento\Framework\Serialize\SerializerInterface $serializer, - \Magento\Framework\App\Config\Spi\PreProcessorInterface $preProcessor, + ConfigSourceInterface $source, + PostProcessorInterface $postProcessor, + Fallback $fallback, + FrontendInterface $cache, + SerializerInterface $serializer, + PreProcessorInterface $preProcessor, $cachingNestedLevel = 1, $configType = self::CONFIG_TYPE, - Reader $reader = null + Reader $reader = null, + Encryptor $encryptor = null, + LockManagerInterface $locker = null, + LockGuardedCacheLoader $lockQuery = null ) { - $this->source = $source; $this->postProcessor = $postProcessor; - $this->preProcessor = $preProcessor; $this->cache = $cache; - $this->cachingNestedLevel = $cachingNestedLevel; - $this->fallback = $fallback; $this->serializer = $serializer; $this->configType = $configType; $this->reader = $reader ?: ObjectManager::getInstance()->get(Reader::class); + $this->encryptor = $encryptor + ?: ObjectManager::getInstance()->get(Encryptor::class); + $this->lockQuery = $lockQuery + ?: ObjectManager::getInstance()->get(LockGuardedCacheLoader::class); } /** + * Get configuration value by path + * * System configuration is separated by scopes (default, websites, stores). Configuration of a scope is inherited * from its parent scope (store inherits website). * @@ -136,68 +162,108 @@ public function get($path = '') { if ($path === '') { $this->data = array_replace_recursive($this->loadAllData(), $this->data); + return $this->data; } + + return $this->getWithParts($path); + } + + /** + * Proceed with parts extraction from path. + * + * @param string $path + * @return array|int|string|boolean + */ + private function getWithParts($path) + { $pathParts = explode('/', $path); - if (count($pathParts) === 1 && $pathParts[0] !== 'default') { + + if (count($pathParts) === 1 && $pathParts[0] !== ScopeInterface::SCOPE_DEFAULT) { if (!isset($this->data[$pathParts[0]])) { $data = $this->readData(); $this->data = array_replace_recursive($data, $this->data); } + return $this->data[$pathParts[0]]; } + $scopeType = array_shift($pathParts); - if ($scopeType === 'default') { + + if ($scopeType === ScopeInterface::SCOPE_DEFAULT) { if (!isset($this->data[$scopeType])) { $this->data = array_replace_recursive($this->loadDefaultScopeData($scopeType), $this->data); } + return $this->getDataByPathParts($this->data[$scopeType], $pathParts); } + $scopeId = array_shift($pathParts); + if (!isset($this->data[$scopeType][$scopeId])) { - $this->data = array_replace_recursive($this->loadScopeData($scopeType, $scopeId), $this->data); + $scopeData = $this->loadScopeData($scopeType, $scopeId); + + if (!isset($this->data[$scopeType][$scopeId])) { + $this->data = array_replace_recursive($scopeData, $this->data); + } } + return isset($this->data[$scopeType][$scopeId]) ? $this->getDataByPathParts($this->data[$scopeType][$scopeId], $pathParts) : null; } /** - * Load configuration data for all scopes + * Load configuration data for all scopes. * * @return array */ private function loadAllData() { - $cachedData = $this->cache->load($this->configType); - if ($cachedData === false) { - $data = $this->readData(); - } else { - $data = $this->serializer->unserialize($cachedData); - } - return $data; + $loadAction = function () { + $cachedData = $this->cache->load($this->configType); + $data = false; + if ($cachedData !== false) { + $data = $this->serializer->unserialize($this->encryptor->decrypt($cachedData)); + } + return $data; + }; + + return $this->lockQuery->lockedLoadData( + self::$lockName, + $loadAction, + \Closure::fromCallable([$this, 'readData']), + \Closure::fromCallable([$this, 'cacheData']) + ); } /** - * Load configuration data for default scope + * Load configuration data for default scope. * * @param string $scopeType * @return array */ private function loadDefaultScopeData($scopeType) { - $cachedData = $this->cache->load($this->configType . '_' . $scopeType); - if ($cachedData === false) { - $data = $this->readData(); - $this->cacheData($data); - } else { - $data = [$scopeType => $this->serializer->unserialize($cachedData)]; - } - return $data; + $loadAction = function () use ($scopeType) { + $cachedData = $this->cache->load($this->configType . '_' . $scopeType); + $scopeData = false; + if ($cachedData !== false) { + $scopeData = [$scopeType => $this->serializer->unserialize($this->encryptor->decrypt($cachedData))]; + } + return $scopeData; + }; + + return $this->lockQuery->lockedLoadData( + self::$lockName, + $loadAction, + \Closure::fromCallable([$this, 'readData']), + \Closure::fromCallable([$this, 'cacheData']) + ); } /** - * Load configuration data for a specified scope + * Load configuration data for a specified scope. * * @param string $scopeType * @param string $scopeId @@ -205,27 +271,39 @@ private function loadDefaultScopeData($scopeType) */ private function loadScopeData($scopeType, $scopeId) { - $cachedData = $this->cache->load($this->configType . '_' . $scopeType . '_' . $scopeId); - if ($cachedData === false) { - if ($this->availableDataScopes === null) { - $cachedScopeData = $this->cache->load($this->configType . '_scopes'); - if ($cachedScopeData !== false) { - $this->availableDataScopes = $this->serializer->unserialize($cachedScopeData); + $loadAction = function () use ($scopeType, $scopeId) { + $cachedData = $this->cache->load($this->configType . '_' . $scopeType . '_' . $scopeId); + $scopeData = false; + if ($cachedData === false) { + if ($this->availableDataScopes === null) { + $cachedScopeData = $this->cache->load($this->configType . '_scopes'); + if ($cachedScopeData !== false) { + $serializedCachedData = $this->encryptor->decrypt($cachedScopeData); + $this->availableDataScopes = $this->serializer->unserialize($serializedCachedData); + } } + if (is_array($this->availableDataScopes) && !isset($this->availableDataScopes[$scopeType][$scopeId])) { + $scopeData = [$scopeType => [$scopeId => []]]; + } + } else { + $serializedCachedData = $this->encryptor->decrypt($cachedData); + $scopeData = [$scopeType => [$scopeId => $this->serializer->unserialize($serializedCachedData)]]; } - if (is_array($this->availableDataScopes) && !isset($this->availableDataScopes[$scopeType][$scopeId])) { - return [$scopeType => [$scopeId => []]]; - } - $data = $this->readData(); - $this->cacheData($data); - } else { - $data = [$scopeType => [$scopeId => $this->serializer->unserialize($cachedData)]]; - } - return $data; + + return $scopeData; + }; + + return $this->lockQuery->lockedLoadData( + self::$lockName, + $loadAction, + \Closure::fromCallable([$this, 'readData']), + \Closure::fromCallable([$this, 'cacheData']) + ); } /** * Cache configuration data. + * * Caches data per scope to avoid reading data for all scopes on every request * * @param array $data @@ -234,35 +312,35 @@ private function loadScopeData($scopeType, $scopeId) private function cacheData(array $data) { $this->cache->save( - $this->serializer->serialize($data), + $this->encryptor->encryptWithFastestAvailableAlgorithm($this->serializer->serialize($data)), $this->configType, [self::CACHE_TAG] ); $this->cache->save( - $this->serializer->serialize($data['default']), + $this->encryptor->encryptWithFastestAvailableAlgorithm($this->serializer->serialize($data['default'])), $this->configType . '_default', [self::CACHE_TAG] ); $scopes = []; - foreach (['websites', 'stores'] as $curScopeType) { + foreach ([StoreScope::SCOPE_WEBSITES, StoreScope::SCOPE_STORES] as $curScopeType) { foreach ($data[$curScopeType] ?? [] as $curScopeId => $curScopeData) { $scopes[$curScopeType][$curScopeId] = 1; $this->cache->save( - $this->serializer->serialize($curScopeData), + $this->encryptor->encryptWithFastestAvailableAlgorithm($this->serializer->serialize($curScopeData)), $this->configType . '_' . $curScopeType . '_' . $curScopeId, [self::CACHE_TAG] ); } } $this->cache->save( - $this->serializer->serialize($scopes), - $this->configType . "_scopes", + $this->encryptor->encryptWithFastestAvailableAlgorithm($this->serializer->serialize($scopes)), + $this->configType . '_scopes', [self::CACHE_TAG] ); } /** - * Walk nested hash map by keys from $pathParts + * Walk nested hash map by keys from $pathParts. * * @param array $data to walk in * @param array $pathParts keys path @@ -279,6 +357,7 @@ private function getDataByPathParts($data, $pathParts) return null; } } + return $data; } @@ -298,7 +377,7 @@ private function readData(): array } /** - * Clean cache and global variables cache + * Clean cache and global variables cache. * * Next items cleared: * - Internal property intended to store already loaded configuration data @@ -310,6 +389,13 @@ private function readData(): array public function clean() { $this->data = []; - $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [self::CACHE_TAG]); + $cleanAction = function () { + $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [self::CACHE_TAG]); + }; + + $this->lockQuery->lockedCleanData( + self::$lockName, + $cleanAction + ); } } diff --git a/app/code/Magento/Config/Block/System/Config/Form.php b/app/code/Magento/Config/Block/System/Config/Form.php index 81e39a83296d..8378c058c195 100644 --- a/app/code/Magento/Config/Block/System/Config/Form.php +++ b/app/code/Magento/Config/Block/System/Config/Form.php @@ -134,6 +134,7 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic * @param \Magento\Config\Block\System\Config\Form\Fieldset\Factory $fieldsetFactory * @param \Magento\Config\Block\System\Config\Form\Field\Factory $fieldFactory * @param array $data + * @param SettingChecker|null $settingChecker */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -143,13 +144,15 @@ public function __construct( \Magento\Config\Model\Config\Structure $configStructure, \Magento\Config\Block\System\Config\Form\Fieldset\Factory $fieldsetFactory, \Magento\Config\Block\System\Config\Form\Field\Factory $fieldFactory, - array $data = [] + array $data = [], + SettingChecker $settingChecker = null ) { parent::__construct($context, $registry, $formFactory, $data); $this->_configFactory = $configFactory; $this->_configStructure = $configStructure; $this->_fieldsetFactory = $fieldsetFactory; $this->_fieldFactory = $fieldFactory; + $this->settingChecker = $settingChecker ?: ObjectManager::getInstance()->get(SettingChecker::class); $this->_scopeLabels = [ self::SCOPE_DEFAULT => __('[GLOBAL]'), @@ -158,18 +161,6 @@ public function __construct( ]; } - /** - * @deprecated 100.1.2 - * @return SettingChecker - */ - private function getSettingChecker() - { - if ($this->settingChecker === null) { - $this->settingChecker = ObjectManager::getInstance()->get(SettingChecker::class); - } - return $this->settingChecker; - } - /** * Initialize objects required to render config form * @@ -366,9 +357,8 @@ protected function _initElement( $sharedClass = $this->_getSharedCssClass($field); $requiresClass = $this->_getRequiresCssClass($field, $fieldPrefix); + $isReadOnly = $this->isReadOnly($field, $path); - $isReadOnly = $this->getElementVisibility()->isDisabled($field->getPath()) - ?: $this->getSettingChecker()->isReadOnly($path, $this->getScope(), $this->getStringScopeCode()); $formField = $fieldset->addField( $elementId, $field->getType(), @@ -417,7 +407,7 @@ private function getFieldData(\Magento\Config\Model\Config\Structure\Element\Fie { $data = $this->getAppConfigDataValue($path); - $placeholderValue = $this->getSettingChecker()->getPlaceholderValue( + $placeholderValue = $this->settingChecker->getPlaceholderValue( $path, $this->getScope(), $this->getStringScopeCode() @@ -434,6 +424,10 @@ private function getFieldData(\Magento\Config\Model\Config\Structure\Element\Fie $backendModel = $field->getBackendModel(); // Backend models which implement ProcessorInterface are processed by ScopeConfigInterface if (!$backendModel instanceof ProcessorInterface) { + if (array_key_exists($path, $this->_configData)) { + $data = $this->_configData[$path]; + } + $backendModel->setPath($path) ->setValue($data) ->setWebsite($this->getWebsiteCode()) @@ -541,7 +535,7 @@ public function getConfigValue($path) } /** - * @return \Magento\Backend\Block\Widget\Form|\Magento\Framework\View\Element\AbstractBlock + * @inheritdoc */ protected function _beforeToHtml() { @@ -718,6 +712,7 @@ protected function _getAdditionalElementTypes() * * @TODO delete this methods when {^see above^} is done * @return string + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getSectionCode() { @@ -729,6 +724,7 @@ public function getSectionCode() * * @TODO delete this methods when {^see above^} is done * @return string + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getWebsiteCode() { @@ -740,6 +736,7 @@ public function getWebsiteCode() * * @TODO delete this methods when {^see above^} is done * @return string + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getStoreCode() { @@ -797,6 +794,26 @@ private function getAppConfig() return $this->appConfig; } + /** + * Check Path is Readonly + * + * @param \Magento\Config\Model\Config\Structure\Element\Field $field + * @param string $path + * @return boolean + */ + private function isReadOnly(\Magento\Config\Model\Config\Structure\Element\Field $field, $path) + { + $isReadOnly = $this->settingChecker->isReadOnly( + $path, + ScopeConfigInterface::SCOPE_TYPE_DEFAULT + ); + if (!$isReadOnly) { + $isReadOnly = $this->getElementVisibility()->isDisabled($field->getPath()) + ?: $this->settingChecker->isReadOnly($path, $this->getScope(), $this->getStringScopeCode()); + } + return $isReadOnly; + } + /** * Retrieve deployment config data value by path * diff --git a/app/code/Magento/Config/Block/System/Config/Form/Field/Datetime.php b/app/code/Magento/Config/Block/System/Config/Form/Field/Datetime.php index 63dbb2b80e33..16b18e020008 100644 --- a/app/code/Magento/Config/Block/System/Config/Form/Field/Datetime.php +++ b/app/code/Magento/Config/Block/System/Config/Form/Field/Datetime.php @@ -33,6 +33,8 @@ public function __construct( } /** + * Returns element html + * * @param AbstractElement $element * @return string * @codeCoverageIgnore @@ -40,7 +42,7 @@ public function __construct( protected function _getElementHtml(AbstractElement $element) { return $this->dateTimeFormatter->formatObject( - $this->_localeDate->date(intval($element->getValue())), + $this->_localeDate->date((int) $element->getValue()), $this->_localeDate->getDateTimeFormat(\IntlDateFormatter::MEDIUM) ); } diff --git a/app/code/Magento/Config/Block/System/Config/Form/Field/Notification.php b/app/code/Magento/Config/Block/System/Config/Form/Field/Notification.php index 7f21bf4b92bf..2e79cec7088b 100644 --- a/app/code/Magento/Config/Block/System/Config/Form/Field/Notification.php +++ b/app/code/Magento/Config/Block/System/Config/Form/Field/Notification.php @@ -10,6 +10,7 @@ /** * Backend system config datetime field renderer + * * @api * @since 100.0.2 */ @@ -35,6 +36,8 @@ public function __construct( } /** + * Returns element html + * * @param AbstractElement $element * @return string */ @@ -44,6 +47,6 @@ protected function _getElementHtml(AbstractElement $element) $format = $this->_localeDate->getDateTimeFormat( \IntlDateFormatter::MEDIUM ); - return $this->dateTimeFormatter->formatObject($this->_localeDate->date(intval($element->getValue())), $format); + return $this->dateTimeFormatter->formatObject($this->_localeDate->date((int) $element->getValue()), $format); } } diff --git a/app/code/Magento/Config/Console/Command/ConfigSet/DefaultProcessor.php b/app/code/Magento/Config/Console/Command/ConfigSet/DefaultProcessor.php index d7d513bfad42..c622a48b7f2c 100644 --- a/app/code/Magento/Config/Console/Command/ConfigSet/DefaultProcessor.php +++ b/app/code/Magento/Config/Console/Command/ConfigSet/DefaultProcessor.php @@ -7,17 +7,19 @@ use Magento\Config\App\Config\Type\System; use Magento\Config\Console\Command\ConfigSetCommand; +use Magento\Config\Model\Config\Factory as ConfigFactory; use Magento\Framework\App\Config\ConfigPathResolver; use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Config\Model\PreparedValueFactory; -use Magento\Framework\App\Config\Value; /** * Processes default flow of config:set command. + * * This processor saves the value of configuration into database. * - * {@inheritdoc} + * @inheritdoc * @api * @since 100.2.0 */ @@ -44,26 +46,36 @@ class DefaultProcessor implements ConfigSetProcessorInterface */ private $preparedValueFactory; + /** + * @var ConfigFactory + */ + private $configFactory; + /** * @param PreparedValueFactory $preparedValueFactory The factory for prepared value * @param DeploymentConfig $deploymentConfig The deployment configuration reader * @param ConfigPathResolver $configPathResolver The resolver for configuration paths according to source type + * @param ConfigFactory|null $configFactory */ public function __construct( PreparedValueFactory $preparedValueFactory, DeploymentConfig $deploymentConfig, - ConfigPathResolver $configPathResolver + ConfigPathResolver $configPathResolver, + ConfigFactory $configFactory = null ) { $this->preparedValueFactory = $preparedValueFactory; $this->deploymentConfig = $deploymentConfig; $this->configPathResolver = $configPathResolver; + + $this->configFactory = $configFactory ?? ObjectManager::getInstance()->get(ConfigFactory::class); } /** * Processes database flow of config:set command. + * * Requires installed application. * - * {@inheritdoc} + * @inheritdoc * @since 100.2.0 */ public function process($path, $value, $scope, $scopeCode) @@ -78,12 +90,12 @@ public function process($path, $value, $scope, $scopeCode) } try { - /** @var Value $backendModel */ - $backendModel = $this->preparedValueFactory->create($path, $value, $scope, $scopeCode); - if ($backendModel instanceof Value) { - $resourceModel = $backendModel->getResource(); - $resourceModel->save($backendModel); - } + $config = $this->configFactory->create(['data' => [ + 'scope' => $scope, + 'scope_code' => $scopeCode, + ]]); + $config->setDataByPath($path, $value); + $config->save(); } catch (\Exception $exception) { throw new CouldNotSaveException(__('%1', $exception->getMessage()), $exception); } diff --git a/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php b/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php index 582f87508089..aeb57010e496 100644 --- a/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php +++ b/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php @@ -97,7 +97,7 @@ public function process($scope, $scopeCode, $value, $path) $field = $configStructure->getElementByConfigPath($path); /** @var Value $backendModel */ - $backendModel = $field && $field->hasBackendModel() + $backendModel = $field instanceof Field && $field->hasBackendModel() ? $field->getBackendModel() : $this->configValueFactory->create(); diff --git a/app/code/Magento/Config/Controller/Adminhtml/System/Config/Edit.php b/app/code/Magento/Config/Controller/Adminhtml/System/Config/Edit.php index b65f6e9d4e4c..12c6ecbab9fd 100644 --- a/app/code/Magento/Config/Controller/Adminhtml/System/Config/Edit.php +++ b/app/code/Magento/Config/Controller/Adminhtml/System/Config/Edit.php @@ -6,7 +6,9 @@ */ namespace Magento\Config\Controller\Adminhtml\System\Config; -class Edit extends AbstractScopeConfig +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Edit extends AbstractScopeConfig implements HttpGetActionInterface { /** * @var \Magento\Framework\View\Result\PageFactory diff --git a/app/code/Magento/Config/Controller/Adminhtml/System/Config/Index.php b/app/code/Magento/Config/Controller/Adminhtml/System/Config/Index.php index 66290a792612..03479085f3f6 100644 --- a/app/code/Magento/Config/Controller/Adminhtml/System/Config/Index.php +++ b/app/code/Magento/Config/Controller/Adminhtml/System/Config/Index.php @@ -6,7 +6,9 @@ */ namespace Magento\Config\Controller\Adminhtml\System\Config; -class Index extends AbstractScopeConfig +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends AbstractScopeConfig implements HttpGetActionInterface { /** * @var \Magento\Backend\Model\View\Result\ForwardFactory diff --git a/app/code/Magento/Config/Controller/Adminhtml/System/Config/Save.php b/app/code/Magento/Config/Controller/Adminhtml/System/Config/Save.php index 290a43c9cd62..2d4b20033806 100644 --- a/app/code/Magento/Config/Controller/Adminhtml/System/Config/Save.php +++ b/app/code/Magento/Config/Controller/Adminhtml/System/Config/Save.php @@ -5,6 +5,7 @@ */ namespace Magento\Config\Controller\Adminhtml\System\Config; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Config\Controller\Adminhtml\System\AbstractConfig; /** @@ -13,7 +14,7 @@ * @author Magento Core Team <core@magentocommerce.com> * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Save extends AbstractConfig +class Save extends AbstractConfig implements HttpPostActionInterface { /** * Backend Config Model Factory diff --git a/app/code/Magento/Config/Controller/Adminhtml/System/Config/State.php b/app/code/Magento/Config/Controller/Adminhtml/System/Config/State.php index 75716fa380be..5d74eb7ea4e2 100644 --- a/app/code/Magento/Config/Controller/Adminhtml/System/Config/State.php +++ b/app/code/Magento/Config/Controller/Adminhtml/System/Config/State.php @@ -6,7 +6,13 @@ */ namespace Magento\Config\Controller\Adminhtml\System\Config; -class State extends AbstractScopeConfig +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; + +/** + * Save current state of open tabs. GET is allowed for legacy reasons. + */ +class State extends AbstractScopeConfig implements HttpPostActionInterface, HttpGetActionInterface { /** * @var \Magento\Framework\Controller\Result\RawFactory diff --git a/app/code/Magento/Config/Model/Config.php b/app/code/Magento/Config/Model/Config.php index c6e2412f7e58..b5dbf97f7953 100644 --- a/app/code/Magento/Config/Model/Config.php +++ b/app/code/Magento/Config/Model/Config.php @@ -9,15 +9,32 @@ use Magento\Config\Model\Config\Structure\Element\Group; use Magento\Config\Model\Config\Structure\Element\Field; use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\ScopeInterface; +use Magento\Framework\App\ScopeResolverPool; +use Magento\Store\Model\ScopeInterface as StoreScopeInterface; +use Magento\Store\Model\ScopeTypeNormalizer; /** * Backend config model + * * Used to save configuration * * @author Magento Core Team <core@magentocommerce.com> * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api * @since 100.0.2 + * @method string getSection() + * @method void setSection(string $section) + * @method string getWebsite() + * @method void setWebsite(string $website) + * @method string getStore() + * @method void setStore(string $store) + * @method string getScope() + * @method void setScope(string $scope) + * @method int getScopeId() + * @method void setScopeId(int $scopeId) + * @method string getScopeCode() + * @method void setScopeCode(string $scopeCode) */ class Config extends \Magento\Framework\DataObject { @@ -87,6 +104,21 @@ class Config extends \Magento\Framework\DataObject */ private $settingChecker; + /** + * @var ScopeResolverPool + */ + private $scopeResolverPool; + + /** + * @var ScopeTypeNormalizer + */ + private $scopeTypeNormalizer; + + /** + * @var \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface + */ + private $pillPut; + /** * @param \Magento\Framework\App\Config\ReinitableConfigInterface $config * @param \Magento\Framework\Event\ManagerInterface $eventManager @@ -97,6 +129,10 @@ class Config extends \Magento\Framework\DataObject * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param Config\Reader\Source\Deployed\SettingChecker|null $settingChecker * @param array $data + * @param ScopeResolverPool|null $scopeResolverPool + * @param ScopeTypeNormalizer|null $scopeTypeNormalizer + * @param \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface|null $pillPut + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\App\Config\ReinitableConfigInterface $config, @@ -107,7 +143,10 @@ public function __construct( \Magento\Framework\App\Config\ValueFactory $configValueFactory, \Magento\Store\Model\StoreManagerInterface $storeManager, SettingChecker $settingChecker = null, - array $data = [] + array $data = [], + ScopeResolverPool $scopeResolverPool = null, + ScopeTypeNormalizer $scopeTypeNormalizer = null, + \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface $pillPut = null ) { parent::__construct($data); $this->_eventManager = $eventManager; @@ -117,11 +156,19 @@ public function __construct( $this->_configLoader = $configLoader; $this->_configValueFactory = $configValueFactory; $this->_storeManager = $storeManager; - $this->settingChecker = $settingChecker ?: ObjectManager::getInstance()->get(SettingChecker::class); + $this->settingChecker = $settingChecker + ?? ObjectManager::getInstance()->get(SettingChecker::class); + $this->scopeResolverPool = $scopeResolverPool + ?? ObjectManager::getInstance()->get(ScopeResolverPool::class); + $this->scopeTypeNormalizer = $scopeTypeNormalizer + ?? ObjectManager::getInstance()->get(ScopeTypeNormalizer::class); + $this->pillPut = $pillPut ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface::class); } /** * Save config section + * * Require set: section, website, store and groups * * @throws \Exception @@ -186,6 +233,8 @@ public function save() throw $e; } + $this->pillPut->put(); + return $this; } @@ -237,13 +286,14 @@ private function getField(string $sectionId, string $groupId, string $fieldId): * Get field path * * @param Field $field + * @param string $fieldId Need for support of clone_field feature * @param array &$oldConfig Need for compatibility with _processGroup() * @param array &$extraOldGroups Need for compatibility with _processGroup() * @return string */ - private function getFieldPath(Field $field, array &$oldConfig, array &$extraOldGroups): string + private function getFieldPath(Field $field, string $fieldId, array &$oldConfig, array &$extraOldGroups): string { - $path = $field->getGroupPath() . '/' . $field->getId(); + $path = $field->getGroupPath() . '/' . $fieldId; /** * Look for custom defined field path @@ -303,7 +353,7 @@ private function getChangedPaths( if (isset($groupData['fields'])) { foreach ($groupData['fields'] as $fieldId => $fieldData) { $field = $this->getField($sectionId, $groupId, $fieldId); - $path = $this->getFieldPath($field, $oldConfig, $extraOldGroups); + $path = $this->getFieldPath($field, $fieldId, $oldConfig, $extraOldGroups); if ($this->isValueChanged($oldConfig, $path, $fieldData)) { $changedPaths[] = $path; } @@ -385,6 +435,11 @@ protected function _processGroup( if (!isset($fieldData['value'])) { $fieldData['value'] = null; } + + if ($field->getType() == 'multiline' && is_array($fieldData['value'])) { + $fieldData['value'] = trim(implode(PHP_EOL, $fieldData['value'])); + } + $data = [ 'field' => $fieldId, 'groups' => $groups, @@ -398,7 +453,7 @@ protected function _processGroup( $backendModel->addData($data); $this->_checkSingleStoreMode($field, $backendModel); - $path = $this->getFieldPath($field, $extraOldGroups, $oldConfig); + $path = $this->getFieldPath($field, $fieldId, $extraOldGroups, $oldConfig); $backendModel->setPath($path)->setValue($fieldData['value']); $inherit = !empty($fieldData['inherit']); @@ -481,30 +536,35 @@ 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); } /** - * Get scope name and scopeId - * @todo refactor to scope resolver + * Set scope data + * * @return void */ private function initScope() @@ -512,31 +572,66 @@ private function initScope() if ($this->getSection() === null) { $this->setSection(''); } + + $scope = $this->retrieveScope(); + $this->setScope($this->scopeTypeNormalizer->normalize($scope->getScopeType())); + $this->setScopeCode($scope->getCode()); + $this->setScopeId($scope->getId()); + if ($this->getWebsite() === null) { - $this->setWebsite(''); + $this->setWebsite(StoreScopeInterface::SCOPE_WEBSITES === $this->getScope() ? $scope->getId() : ''); } if ($this->getStore() === null) { - $this->setStore(''); + $this->setStore(StoreScopeInterface::SCOPE_STORES === $this->getScope() ? $scope->getId() : ''); } + } - if ($this->getStore()) { - $scope = 'stores'; - $store = $this->_storeManager->getStore($this->getStore()); - $scopeId = (int)$store->getId(); - $scopeCode = $store->getCode(); - } elseif ($this->getWebsite()) { - $scope = 'websites'; - $website = $this->_storeManager->getWebsite($this->getWebsite()); - $scopeId = (int)$website->getId(); - $scopeCode = $website->getCode(); + /** + * Retrieve scope from initial data + * + * @return ScopeInterface + */ + private function retrieveScope(): ScopeInterface + { + $scopeType = $this->getScope(); + if (!$scopeType) { + switch (true) { + case $this->getStore(): + $scopeType = StoreScopeInterface::SCOPE_STORES; + $scopeIdentifier = $this->getStore(); + break; + case $this->getWebsite(): + $scopeType = StoreScopeInterface::SCOPE_WEBSITES; + $scopeIdentifier = $this->getWebsite(); + break; + default: + $scopeType = ScopeInterface::SCOPE_DEFAULT; + $scopeIdentifier = null; + break; + } } else { - $scope = 'default'; - $scopeId = 0; - $scopeCode = ''; + switch (true) { + case $this->getScopeId() !== null: + $scopeIdentifier = $this->getScopeId(); + break; + case $this->getScopeCode() !== null: + $scopeIdentifier = $this->getScopeCode(); + break; + case $this->getStore() !== null: + $scopeIdentifier = $this->getStore(); + break; + case $this->getWebsite() !== null: + $scopeIdentifier = $this->getWebsite(); + break; + default: + $scopeIdentifier = null; + break; + } } - $this->setScope($scope); - $this->setScopeId($scopeId); - $this->setScopeCode($scopeCode); + $scope = $this->scopeResolverPool->get($scopeType) + ->getScope($scopeIdentifier); + + return $scope; } /** diff --git a/app/code/Magento/Config/Model/Config/Backend/Admin/Usecustom.php b/app/code/Magento/Config/Model/Config/Backend/Admin/Usecustom.php index 9a483de6a695..f5d568f2f36b 100644 --- a/app/code/Magento/Config/Model/Config/Backend/Admin/Usecustom.php +++ b/app/code/Magento/Config/Model/Config/Backend/Admin/Usecustom.php @@ -10,6 +10,8 @@ namespace Magento\Config\Model\Config\Backend\Admin; /** + * Process custom admin url during configuration value save process. + * * @api * @since 100.0.2 */ @@ -56,8 +58,9 @@ public function beforeSave() { $value = $this->getValue(); if ($value == 1) { - $customUrl = $this->getData('groups/url/fields/custom/value'); - if (empty($customUrl)) { + $customUrlField = $this->getData('groups/url/fields/custom/value'); + $customUrlConfig = $this->_config->getValue('admin/url/custom'); + if (empty($customUrlField) && empty($customUrlConfig)) { throw new \Magento\Framework\Exception\LocalizedException(__('Please specify the admin custom URL.')); } } diff --git a/app/code/Magento/Config/Model/Config/Backend/Currency/AbstractCurrency.php b/app/code/Magento/Config/Model/Config/Backend/Currency/AbstractCurrency.php index 4ae66bfd9692..25303093ace5 100644 --- a/app/code/Magento/Config/Model/Config/Backend/Currency/AbstractCurrency.php +++ b/app/code/Magento/Config/Model/Config/Backend/Currency/AbstractCurrency.php @@ -14,6 +14,8 @@ namespace Magento\Config\Model\Config\Backend\Currency; /** + * Base currency class + * * @api * @since 100.0.2 */ @@ -26,18 +28,19 @@ abstract class AbstractCurrency extends \Magento\Framework\App\Config\Value */ protected function _getAllowedCurrencies() { - if (!$this->isFormData() || $this->getData('groups/options/fields/allow/inherit')) { - return explode( + $allowValue = $this->getData('groups/options/fields/allow/value'); + $allowedCurrencies = $allowValue === null || $this->getData('groups/options/fields/allow/inherit') + ? explode( ',', (string)$this->_config->getValue( \Magento\Directory\Model\Currency::XML_PATH_CURRENCY_ALLOW, $this->getScope(), $this->getScopeId() ) - ); - } + ) + : (array) $allowValue; - return (array)$this->getData('groups/options/fields/allow/value'); + return $allowedCurrencies; } /** diff --git a/app/code/Magento/Config/Model/Config/Backend/Currency/Cron.php b/app/code/Magento/Config/Model/Config/Backend/Currency/Cron.php index 3f80e01802b8..f29fa0611efa 100644 --- a/app/code/Magento/Config/Model/Config/Backend/Currency/Cron.php +++ b/app/code/Magento/Config/Model/Config/Backend/Currency/Cron.php @@ -10,6 +10,8 @@ namespace Magento\Config\Model\Config\Backend\Currency; /** + * Cron job configuration for currency + * * @api * @since 100.0.2 */ @@ -47,6 +49,8 @@ public function __construct( } /** + * After save handler + * * @return $this * @throws \Exception */ @@ -59,8 +63,8 @@ public function afterSave() $frequencyMonthly = \Magento\Cron\Model\Config\Source\Frequency::CRON_MONTHLY; $cronExprArray = [ - intval($time[1]), # Minute - intval($time[0]), # Hour + (int)$time[1], # Minute + (int)$time[0], # Hour $frequency == $frequencyMonthly ? '1' : '*', # Day of the Month '*', # Month of the Year $frequency == $frequencyWeekly ? '1' : '*', # Day of the Week diff --git a/app/code/Magento/Config/Model/Config/Backend/Encrypted.php b/app/code/Magento/Config/Model/Config/Backend/Encrypted.php index 1a91e403a679..ea3b1d4c74a5 100644 --- a/app/code/Magento/Config/Model/Config/Backend/Encrypted.php +++ b/app/code/Magento/Config/Model/Config/Backend/Encrypted.php @@ -9,6 +9,8 @@ namespace Magento\Config\Model\Config\Backend; /** + * Backend model for encrypted values. + * * @api * @since 100.0.2 */ @@ -48,9 +50,14 @@ public function __construct( * Magic method called during class serialization * * @return string[] + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + $properties = parent::__sleep(); return array_diff($properties, ['_encryptor']); } @@ -59,9 +66,14 @@ public function __sleep() * Magic method called during class un-serialization * * @return void + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + parent::__wakeup(); $this->_encryptor = \Magento\Framework\App\ObjectManager::getInstance()->get( \Magento\Framework\Encryption\EncryptorInterface::class diff --git a/app/code/Magento/Config/Model/Config/Backend/Log/Cron.php b/app/code/Magento/Config/Model/Config/Backend/Log/Cron.php index 3c36baf6f31f..cff6f54b3a7a 100644 --- a/app/code/Magento/Config/Model/Config/Backend/Log/Cron.php +++ b/app/code/Magento/Config/Model/Config/Backend/Log/Cron.php @@ -10,6 +10,8 @@ namespace Magento\Config\Model\Config\Backend\Log; /** + * Cron logger configuration + * * @api * @since 100.0.2 */ @@ -73,8 +75,8 @@ public function afterSave() if ($enabled) { $cronExprArray = [ - intval($time[1]), # Minute - intval($time[0]), # Hour + (int)$time[1], # Minute + (int)$time[0], # Hour $frequency == $frequencyMonthly ? '1' : '*', # Day of the Month '*', # Month of the Year $frequency == $frequencyWeekly ? '1' : '*', # Day of the Week diff --git a/app/code/Magento/Config/Model/Config/Backend/Serialized.php b/app/code/Magento/Config/Model/Config/Backend/Serialized.php index 3d5713357c39..6e0b6275db83 100644 --- a/app/code/Magento/Config/Model/Config/Backend/Serialized.php +++ b/app/code/Magento/Config/Model/Config/Backend/Serialized.php @@ -9,6 +9,8 @@ use Magento\Framework\Serialize\Serializer\Json; /** + * Serialized backend model + * * @api * @since 100.0.2 */ @@ -46,17 +48,32 @@ public function __construct( } /** + * Processing object after load data + * * @return void */ protected function _afterLoad() { $value = $this->getValue(); if (!is_array($value)) { - $this->setValue(empty($value) ? false : $this->serializer->unserialize($value)); + try { + $this->setValue(empty($value) ? false : $this->serializer->unserialize($value)); + } catch (\Exception $e) { + $this->_logger->critical( + sprintf( + 'Failed to unserialize %s config value. The error is: %s', + $this->getPath(), + $e->getMessage() + ) + ); + $this->setValue(false); + } } } /** + * Processing object before save data + * * @return $this */ public function beforeSave() diff --git a/app/code/Magento/Config/Model/Config/Source/Locale/Currency.php b/app/code/Magento/Config/Model/Config/Source/Locale/Currency.php index b3474674cf76..5beff0d043ad 100644 --- a/app/code/Magento/Config/Model/Config/Source/Locale/Currency.php +++ b/app/code/Magento/Config/Model/Config/Source/Locale/Currency.php @@ -4,12 +4,15 @@ * See COPYING.txt for license details. */ -/** - * Locale currency source - */ namespace Magento\Config\Model\Config\Source\Locale; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Locale\ListsInterface; + /** + * Locale currency source. + * * @api * @since 100.0.2 */ @@ -21,27 +24,70 @@ class Currency implements \Magento\Framework\Option\ArrayInterface protected $_options; /** - * @var \Magento\Framework\Locale\ListsInterface + * @var ListsInterface */ protected $_localeLists; /** - * @param \Magento\Framework\Locale\ListsInterface $localeLists + * @var ScopeConfigInterface */ - public function __construct(\Magento\Framework\Locale\ListsInterface $localeLists) - { + private $config; + + /** + * @var array + */ + private $installedCurrencies; + + /** + * @param ListsInterface $localeLists + * @param ScopeConfigInterface $config + */ + public function __construct( + ListsInterface $localeLists, + ScopeConfigInterface $config = null + ) { $this->_localeLists = $localeLists; + $this->config = $config ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); } /** - * @return array + * @inheritdoc */ public function toOptionArray() { if (!$this->_options) { $this->_options = $this->_localeLists->getOptionCurrencies(); } - $options = $this->_options; + + $selected = array_flip($this->getInstalledCurrencies()); + + $options = array_filter( + $this->_options, + function ($option) use ($selected) { + return isset($selected[$option['value']]); + } + ); + return $options; } + + /** + * Retrieve Installed Currencies. + * + * @return array + */ + private function getInstalledCurrencies() + { + if (!$this->installedCurrencies) { + $this->installedCurrencies = explode( + ',', + $this->config->getValue( + 'system/currency/installed', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ) + ); + } + + return $this->installedCurrencies; + } } diff --git a/app/code/Magento/Config/Model/Config/Structure.php b/app/code/Magento/Config/Model/Config/Structure.php index 5c74220051ba..a380dc82a7c5 100644 --- a/app/code/Magento/Config/Model/Config/Structure.php +++ b/app/code/Magento/Config/Model/Config/Structure.php @@ -337,7 +337,6 @@ protected function _getGroupFieldPathsByAttribute(array $fields, $parentPath, $a /** * Collects config paths and their structure paths from configuration files. * Returns the map of config paths and their structure paths. - * * All paths are declared in module's system.xml. * * ```xml @@ -394,7 +393,7 @@ private function getFieldsRecursively(array $elements = []) foreach ($elements as $element) { if (isset($element['children'])) { - $result = array_replace_recursive( + $result = array_merge_recursive( $result, $this->getFieldsRecursively($element['children']) ); diff --git a/app/code/Magento/Config/Model/Config/Structure/Mapper/Sorting.php b/app/code/Magento/Config/Model/Config/Structure/Mapper/Sorting.php index f6f3a0be187a..19e1acc6170f 100644 --- a/app/code/Magento/Config/Model/Config/Structure/Mapper/Sorting.php +++ b/app/code/Magento/Config/Model/Config/Structure/Mapper/Sorting.php @@ -10,6 +10,8 @@ namespace Magento\Config\Model\Config\Structure\Mapper; /** + * Sorting mapper + * * @api * @since 100.0.2 */ @@ -30,6 +32,8 @@ public function map(array $data) } /** + * Process config + * * @param array $data * @return array */ @@ -55,17 +59,13 @@ protected function _cmp($elementA, $elementB) { $sortIndexA = 0; if ($this->_hasValue('sortOrder', $elementA)) { - $sortIndexA = floatval($elementA['sortOrder']); + $sortIndexA = (float)$elementA['sortOrder']; } $sortIndexB = 0; if ($this->_hasValue('sortOrder', $elementB)) { - $sortIndexB = floatval($elementB['sortOrder']); - } - - if ($sortIndexA == $sortIndexB) { - return 0; + $sortIndexB = (float)$elementB['sortOrder']; } - return $sortIndexA < $sortIndexB ? -1 : 1; + return $sortIndexA <=> $sortIndexB; } } diff --git a/app/code/Magento/Config/Observer/Config/Backend/Admin/AfterCustomUrlChangedObserver.php b/app/code/Magento/Config/Observer/Config/Backend/Admin/AfterCustomUrlChangedObserver.php index bf414890d0ed..830b6376c94b 100644 --- a/app/code/Magento/Config/Observer/Config/Backend/Admin/AfterCustomUrlChangedObserver.php +++ b/app/code/Magento/Config/Observer/Config/Backend/Admin/AfterCustomUrlChangedObserver.php @@ -3,10 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Config\Observer\Config\Backend\Admin; use Magento\Framework\Event\ObserverInterface; +/** + * Class AfterCustomUrlChangedObserver redirects to new custom admin URL. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ class AfterCustomUrlChangedObserver implements ObserverInterface { /** @@ -56,7 +63,6 @@ public function __construct( * * @param \Magento\Framework\Event\Observer $observer * @return void - * @SuppressWarnings(PHPMD.ExitExpression) * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function execute(\Magento\Framework\Event\Observer $observer) @@ -68,6 +74,7 @@ public function execute(\Magento\Framework\Event\Observer $observer) $this->_authSession->destroy(); $adminUrl = $this->_backendData->getHomePageUrl(); $this->_response->setRedirect($adminUrl)->sendResponse(); + // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage exit(0); } } diff --git a/app/code/Magento/Config/Setup/ConfigOptionsList.php b/app/code/Magento/Config/Setup/ConfigOptionsList.php new file mode 100644 index 000000000000..c410eeae615e --- /dev/null +++ b/app/code/Magento/Config/Setup/ConfigOptionsList.php @@ -0,0 +1,135 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Config\Setup; + +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\Config\Data\ConfigData; +use Magento\Framework\Config\Data\ConfigDataFactory; +use Magento\Framework\Config\File\ConfigFilePool; +use Magento\Framework\Setup\ConfigOptionsListInterface; +use Magento\Framework\Setup\Option\SelectConfigOption; + +/** + * Deployment configuration options required for the Config module. + */ +class ConfigOptionsList implements ConfigOptionsListInterface +{ + /** + * Input key for the debug_logging option. + */ + const INPUT_KEY_DEBUG_LOGGING = 'enable-debug-logging'; + + /** + * Path to the debug_logging value in the deployment config. + */ + const CONFIG_PATH_DEBUG_LOGGING = 'dev/debug/debug_logging'; + + /** + * Input key for the syslog_logging option. + */ + const INPUT_KEY_SYSLOG_LOGGING = 'enable-syslog-logging'; + + /** + * Path to the syslog_logging value in the deployment config. + */ + const CONFIG_PATH_SYSLOG_LOGGING = 'dev/syslog/syslog_logging'; + + /** + * @var ConfigDataFactory + */ + private $configDataFactory; + + /** + * @param ConfigDataFactory $configDataFactory + */ + public function __construct(ConfigDataFactory $configDataFactory) + { + $this->configDataFactory = $configDataFactory; + } + + /** + * @inheritdoc + */ + public function getOptions() + { + return [ + new SelectConfigOption( + self::INPUT_KEY_DEBUG_LOGGING, + SelectConfigOption::FRONTEND_WIZARD_RADIO, + [true, false, 1, 0], + self::CONFIG_PATH_DEBUG_LOGGING, + 'Enable debug logging' + ), + new SelectConfigOption( + self::INPUT_KEY_SYSLOG_LOGGING, + SelectConfigOption::FRONTEND_WIZARD_RADIO, + [true, false, 1, 0], + self::CONFIG_PATH_SYSLOG_LOGGING, + 'Enable syslog logging' + ), + ]; + } + + /** + * @inheritdoc + */ + public function createConfig(array $options, DeploymentConfig $deploymentConfig) + { + $deploymentOption = [ + self::INPUT_KEY_DEBUG_LOGGING => self::CONFIG_PATH_DEBUG_LOGGING, + self::INPUT_KEY_SYSLOG_LOGGING => self::CONFIG_PATH_SYSLOG_LOGGING, + ]; + + $config = []; + foreach ($deploymentOption as $inputKey => $configPath) { + $configValue = $this->processBooleanConfigValue( + $inputKey, + $configPath, + $options + ); + if ($configValue) { + $config[] = $configValue; + } + } + + return $config; + } + + /** + * Provide config value from input. + * + * @param string $inputKey + * @param string $configPath + * @param array $options + * @return ConfigData|null + */ + private function processBooleanConfigValue(string $inputKey, string $configPath, array &$options): ?ConfigData + { + $configData = null; + if (isset($options[$inputKey])) { + $configData = $this->configDataFactory->create(ConfigFilePool::APP_ENV); + if ($options[$inputKey] === 'true' + || $options[$inputKey] === '1') { + $value = 1; + } else { + $value = 0; + } + $configData->set($configPath, $value); + } + + return $configData; + } + + /** + * @inheritdoc + */ + public function validate(array $options, DeploymentConfig $deploymentConfig) + { + return []; + } +} diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminConfigCreateNewAccountActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminConfigCreateNewAccountActionGroup.xml new file mode 100644 index 000000000000..084f4ce92f0f --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminConfigCreateNewAccountActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="SetGroupForValidVATIdIntraUnionActionGroup"> + <arguments> + <argument name="value" type="string"/> + </arguments> + <amOnPage url="{{AdminStoresCustomerConfigurationPage.url}}" stepKey="navigateToCustomerConfigurationPage" /> + <waitForPageLoad stepKey="waitForPageLoad"/> + <conditionalClick stepKey="expandCreateNewAccountOptionsTab" selector="{{AdminStoresCustomerConfigurationSection.createNewAccOpt}}" dependentSelector="{{AdminStoresCustomerConfigurationSection.enableAutoAssignCustomerGroup}}" visible="false"/> + <waitForElementVisible selector="{{AdminStoresCustomerConfigurationSection.createNewAccOpt}}" stepKey="waitForElementsAppeared"/> + <selectOption selector="{{AdminStoresCustomerConfigurationSection.groupForValidVATIdIntraUnion}}" userInput="{{value}}" stepKey="selectValue"/> + <click selector="{{AdminStoresCustomerConfigurationSection.createNewAccOpt}}" stepKey="collapseTab" /> + <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig"/> + <waitForPageLoad stepKey="waitForConfigSaved"/> + <see userInput="You saved the configuration." stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigAdminCatalogSearchActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigAdminCatalogSearchActionGroup.xml new file mode 100644 index 000000000000..2109b1cdbb56 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigAdminCatalogSearchActionGroup.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See 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="ChooseElasticSearchAsSearchEngine"> + <amOnPage url="{{AdminCatalogSearchConfigurationPage.url}}" stepKey="configureSearchEngine"/> + <waitForPageLoad stepKey="waitForConfigPage"/> + <scrollTo selector="{{AdminCatalogSearchConfigurationSection.catalogSearchTab}}" stepKey="scrollToCatalogSearchTab"/> + <conditionalClick selector="{{AdminCatalogSearchConfigurationSection.catalogSearchTab}}" dependentSelector="{{AdminCatalogSearchConfigurationSection.checkIfCatalogSearchTabExpand}}" visible="true" stepKey="expandCatalogSearchTab"/> + <waitForElementVisible selector="{{AdminCatalogSearchConfigurationSection.searchEngine}}" stepKey="waitForDropdownToBeVisible"/> + <uncheckOption selector="{{AdminCatalogSearchConfigurationSection.searchEngineDefaultSystemValue}}" stepKey="uncheckUseSystemValue"/> + <selectOption selector="{{AdminCatalogSearchConfigurationSection.searchEngine}}" userInput="elasticsearch5" stepKey="chooseES5"/> + <!--<scrollTo selector="{{AdminCatalogSearchConfigurationSection.catalogSearchTab}}" stepKey="scrollToCatalogSearchTab2"/>--> + <!--<click selector="{{AdminCatalogSearchConfigurationSection.catalogSearchTab}}" stepKey="collapseCatalogSearchTab"/>--> + <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfiguration"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the configuration." stepKey="seeConfigurationSuccessMessage"/> + </actionGroup> + <actionGroup name="ResetSearchEngineConfiguration"> + <amOnPage url="{{AdminCatalogSearchConfigurationPage.url}}" stepKey="resetSearchEngine"/> + <waitForPageLoad stepKey="waitForConfigPage2"/> + <scrollTo selector="{{AdminCatalogSearchConfigurationSection.catalogSearchTab}}" stepKey="scrollToCatalogSearchTab2"/> + <conditionalClick selector="{{AdminCatalogSearchConfigurationSection.catalogSearchTab}}" dependentSelector="{{AdminCatalogSearchConfigurationSection.checkIfCatalogSearchTabExpand}}" visible="true" stepKey="expandCatalogSearchTab2"/> + <waitForElementVisible selector="{{AdminCatalogSearchConfigurationSection.searchEngine}}" stepKey="waitForDropdownToBeVisible2"/> + <selectOption selector="{{AdminCatalogSearchConfigurationSection.searchEngine}}" userInput="mysql" stepKey="chooseMySQL"/> + <checkOption selector="{{AdminCatalogSearchConfigurationSection.searchEngineDefaultSystemValue}}" stepKey="checkUseSystemValue"/> + <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfiguration2"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the configuration." stepKey="seeConfigurationSuccessMessage2"/> + </actionGroup> + +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigSalesTaxClassActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigSalesTaxClassActionGroup.xml index 06c041fabeb3..4e9319351a13 100644 --- a/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigSalesTaxClassActionGroup.xml +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigSalesTaxClassActionGroup.xml @@ -28,4 +28,32 @@ <click selector="{{SalesConfigSection.TaxClassesTab}}" stepKey="collapseTaxClassesTab"/> <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfiguration"/> </actionGroup> - </actionGroups> \ No newline at end of file + <actionGroup name="SetTaxApplyOnSetting"> + <arguments> + <argument name="userInput" type="string"/> + </arguments> + <conditionalClick selector="{{AdminConfigureTaxSection.taxCalculationSettings}}" dependentSelector="{{AdminConfigureTaxSection.taxCalculationAlgorithm}}" visible="false" stepKey="openTaxCalcSettingsSection"/> + <scrollTo selector="{{AdminConfigureTaxSection.taxCalculationApplyTaxOnInherit}}" x="0" y="-80" stepKey="goToCheckbox"/> + <uncheckOption selector="{{AdminConfigureTaxSection.taxCalculationApplyTaxOnInherit}}" stepKey="enableApplyTaxOnSetting"/> + <selectOption selector="{{AdminConfigureTaxSection.taxCalculationApplyTaxOn}}" userInput="{{userInput}}" stepKey="setApplyTaxOn"/> + <scrollTo selector="{{SalesConfigSection.TaxClassesTab}}" stepKey="scrollToTop"/> + <click selector="{{AdminConfigureTaxSection.taxCalculationSettings}}" stepKey="collapseCalcSettingsTab"/> + <click selector="{{AdminConfigureTaxSection.save}}" stepKey="saveConfig"/> + <waitForPageLoad stepKey="waitForConfigSaved"/> + <see userInput="You saved the configuration." stepKey="seeSuccessMessage"/> + </actionGroup> + <actionGroup name="DisableTaxApplyOnOriginalPrice"> + <arguments> + <argument name="userInput" type="string"/> + </arguments> + <amOnPage url="{{AdminSalesTaxClassPage.url}}" stepKey="navigateToSalesTaxPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <conditionalClick selector="{{AdminConfigureTaxSection.taxCalculationSettings}}" dependentSelector="{{AdminConfigureTaxSection.taxCalculationAlgorithm}}" visible="false" stepKey="openTaxCalcSettingsSection"/> + <scrollTo selector="{{AdminConfigureTaxSection.taxCalculationApplyTaxOnInherit}}" x="0" y="-80" stepKey="goToCheckbox"/> + <selectOption selector="{{AdminConfigureTaxSection.taxCalculationApplyTaxOn}}" userInput="{{userInput}}" stepKey="setApplyTaxOff"/> + <checkOption selector="{{AdminConfigureTaxSection.taxCalculationApplyTaxOnInherit}}" stepKey="disableApplyTaxOnSetting"/> + <click selector="{{AdminConfigureTaxSection.save}}" stepKey="saveConfig"/> + <waitForPageLoad stepKey="waitForConfigSaved"/> + <see userInput="You saved the configuration." stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigWYSIWYGActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigWYSIWYGActionGroup.xml index 7062d092e720..eefaf5f3b539 100644 --- a/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigWYSIWYGActionGroup.xml +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigWYSIWYGActionGroup.xml @@ -9,25 +9,21 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="EnabledWYSIWYG"> - <amOnPage url="admin/admin/system_config/edit/section/cms/" stepKey="navigateToConfigurationPage" /> - <waitForPageLoad stepKey="wait1"/> - <conditionalClick stepKey="expandWYSIWYGOptions" selector="{{ContentManagementSection.WYSIWYGOptions}}" dependentSelector="{{ContentManagementSection.CheckIfTabExpand}}" visible="true" /> - <waitForElementVisible selector="{{ContentManagementSection.EnableWYSIWYG}}" stepKey="waitForEnableWYSIWYGDropdown1" /> - <waitForElementVisible selector="{{ContentManagementSection.EnableSystemValue}}" stepKey="waitForUseSystemValueVisible"/> - <uncheckOption selector="{{ContentManagementSection.EnableSystemValue}}" stepKey="uncheckUseSystemValue"/> - <selectOption selector="{{ContentManagementSection.EnableWYSIWYG}}" userInput="Enabled by Default" stepKey="selectOption1"/> + <magentoCLI stepKey="enableWYSIWYG" command="config:set cms/wysiwyg/enabled enabled"/> + </actionGroup> + <actionGroup name="SwitchToTinyMCE3"> + <comment userInput="Choose TinyMCE3 as the default editor" stepKey="chooseTinyMCE3AsEditor"/> + <conditionalClick stepKey="expandWYSIWYGOptions1" selector="{{ContentManagementSection.WYSIWYGOptions}}" dependentSelector="{{ContentManagementSection.CheckIfTabExpand}}" visible="true" /> + <waitForElementVisible selector="{{ContentManagementSection.SwitcherSystemValue}}" stepKey="waitForCheckbox2" /> + <uncheckOption selector="{{ContentManagementSection.SwitcherSystemValue}}" stepKey="uncheckUseSystemValue2"/> + <waitForElementVisible selector="{{ContentManagementSection.Switcher}}" stepKey="waitForSwitcherDropdown2" /> + <selectOption selector="{{ContentManagementSection.Switcher}}" userInput="TinyMCE 3" stepKey="switchToVersion3" /> <click selector="{{ContentManagementSection.WYSIWYGOptions}}" stepKey="collapseWYSIWYGOptions" /> <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig" /> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the configuration." stepKey="seeConfigurationSuccessMessage"/> </actionGroup> <actionGroup name="DisabledWYSIWYG"> - <amOnPage url="admin/admin/system_config/edit/section/cms/" stepKey="navigateToConfigurationPage" /> - <waitForPageLoad stepKey="wait3"/> - <conditionalClick stepKey="expandWYSIWYGOptions" selector="{{ContentManagementSection.WYSIWYGOptions}}" dependentSelector="{{ContentManagementSection.CheckIfTabExpand}}" visible="true" /> - <waitForElementVisible selector="{{ContentManagementSection.EnableWYSIWYG}}" stepKey="waitForEnableWYSIWYGDropdown2" time="30"/> - <uncheckOption selector="{{ContentManagementSection.EnableSystemValue}}" stepKey="uncheckUseSystemValue"/> - <selectOption selector="{{ContentManagementSection.EnableWYSIWYG}}" userInput="Disabled Completely" stepKey="selectOption2"/> - <click selector="{{ContentManagementSection.WYSIWYGOptions}}" stepKey="collapseWYSIWYGOptions" /> - <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig" /> + <magentoCLI stepKey="disableWYSIWYG" command="config:set cms/wysiwyg/enabled disabled"/> </actionGroup> <actionGroup name="UseStaticURLForMediaContentInWYSIWYG"> <arguments> @@ -42,4 +38,15 @@ <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig" /> <waitForPageLoad stepKey="waitForPageLoad2" /> </actionGroup> + <actionGroup name="EnabledWYSIWYGEditor"> + <amOnPage url="{{AdminContentManagementPage.url}}" stepKey="navigateToConfigurationPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <conditionalClick selector="{{ContentManagementSection.WYSIWYGOptions}}" dependentSelector="{{ContentManagementSection.EnableWYSIWYG}}" visible="false" stepKey="expandWYSIWYGOptionsTab"/> + <waitForElementVisible selector="{{ContentManagementSection.EnableWYSIWYG}}" stepKey="waitTabToExpand"/> + <uncheckOption selector="{{ContentManagementSection.EnableSystemValue}}" stepKey="enableEnableSystemValue"/> + <selectOption selector="{{ContentManagementSection.EnableWYSIWYG}}" userInput="Enabled by Default" stepKey="enableWYSIWYG"/> + <click selector="{{ContentManagementSection.WYSIWYGOptions}}" stepKey="collapseWYSIWYGOptionsTab"/> + <click selector="{{ContentManagementSection.Save}}" stepKey="clickSaveConfig" /> + <see stepKey="seeSuccess" selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the configuration."/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/GeneralConfigurationActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/GeneralConfigurationActionGroup.xml index fca48dfb49cf..f05cf5be3448 100644 --- a/app/code/Magento/Config/Test/Mftf/ActionGroup/GeneralConfigurationActionGroup.xml +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/GeneralConfigurationActionGroup.xml @@ -37,4 +37,17 @@ <click selector="#save" stepKey="saveConfig"/> <waitForPageLoad stepKey="waitForSavingConfig"/> </actionGroup> + + <actionGroup name="SelectCountriesWithRequiredRegion"> + <arguments> + <argument name="countries" type="countryArray"/> + </arguments> + <amOnPage url="{{AdminConfigGeneralPage.url}}" stepKey="navigateToAdminConfigGeneralPage"/> + <conditionalClick selector="{{StateOptionsSection.stateOptions}}" dependentSelector="{{StateOptionsSection.countriesWithRequiredRegions}}" visible="false" stepKey="expandStateOptionsTab" /> + <waitForAjaxLoad stepKey="waitForAjax"/> + <scrollTo selector="{{StateOptionsSection.countriesWithRequiredRegions}}" stepKey="scrollToForm"/> + <selectOption selector="{{StateOptionsSection.countriesWithRequiredRegions}}" parameterArray="[{{countries.country}}]" stepKey="selectCountriesWithRequiredRegion"/> + <click selector="#save" stepKey="saveConfig"/> + <waitForPageLoad stepKey="waitForSavingConfig"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/Data/AllowGuestCheckoutData.xml b/app/code/Magento/Config/Test/Mftf/Data/AllowGuestCheckoutData.xml new file mode 100644 index 000000000000..f89cdf1a87b3 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Data/AllowGuestCheckoutData.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="EnableAllowGuestCheckout" type="allow_guest_checkout_config"> + <requiredEntity type="guest_checkout">AllowGuestCheckoutYes</requiredEntity> + </entity> + <entity name="AllowGuestCheckoutYes" type="guest_checkout"> + <data key="value">1</data> + </entity> + + <entity name="DisableAllowGuestCheckout" type="allow_guest_checkout_config"> + <requiredEntity type="guest_checkout">AllowGuestCheckoutNo</requiredEntity> + </entity> + <entity name="AllowGuestCheckoutNo" type="guest_checkout"> + <data key="value">0</data> + </entity> +</entities> diff --git a/app/code/Magento/Config/Test/Mftf/Data/CountryOptionConfigData.xml b/app/code/Magento/Config/Test/Mftf/Data/CountryOptionConfigData.xml new file mode 100644 index 000000000000..53ca46e74620 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Data/CountryOptionConfigData.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="EnableAdminAccountAllowCountry" type="admin_account_country_options_config"> + <requiredEntity type="admin_account_country_options_value">AdminAccountAllowCountryUS</requiredEntity> + </entity> + <entity name="AdminAccountAllowCountryUS" type="admin_account_country_options_value"> + <data key="value">US</data> + </entity> + + <entity name="DisableAdminAccountAllowCountry" type="default_admin_account_country_options_config"> + <requiredEntity type="checkoutTotalFlagZero">DefaultAdminAccountAllowCountry</requiredEntity> + </entity> + <entity name="DefaultAdminAccountAllowCountry" type="checkoutTotalFlagZero"> + <data key="value">0</data> + </entity> +</entities> diff --git a/app/code/Magento/Config/Test/Mftf/Data/LocaleOptionsData.xml b/app/code/Magento/Config/Test/Mftf/Data/LocaleOptionsData.xml new file mode 100644 index 000000000000..5647283fae18 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Data/LocaleOptionsData.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="SetLocaleOptions" type="locale_options_config"> + <requiredEntity type="code">setLocaleOptionsFrance</requiredEntity> + </entity> + <entity name="setLocaleOptionsFrance" type="code"> + <data key="value">fr_FR</data> + </entity> + + <entity name="DefaultLocaleOptions" type="locale_options_config"> + <requiredEntity type="code">setLocaleOptionsUSA</requiredEntity> + </entity> + <entity name="setLocaleOptionsUSA" type="code"> + <data key="value">en_US</data> + </entity> +</entities> diff --git a/app/code/Magento/Config/Test/Mftf/Data/SystemConfigData.xml b/app/code/Magento/Config/Test/Mftf/Data/SystemConfigData.xml index 75dc19dc99c8..85188eb6e04c 100644 --- a/app/code/Magento/Config/Test/Mftf/Data/SystemConfigData.xml +++ b/app/code/Magento/Config/Test/Mftf/Data/SystemConfigData.xml @@ -7,7 +7,7 @@ --> <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> <entity name="AdminAccountSharingYes" type="admin_account_sharing_value"> <data key="value">Yes</data> </entity> diff --git a/app/code/Magento/Config/Test/Mftf/Data/WebUrlOptionsConfigData.xml b/app/code/Magento/Config/Test/Mftf/Data/WebUrlOptionsConfigData.xml new file mode 100644 index 000000000000..eda0eb904be8 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Data/WebUrlOptionsConfigData.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="DefaultWebUrlOptionsConfig" type="web_url_use_store"> + <requiredEntity type="url_use_store_value">DefaultConfigWebUrlOptions</requiredEntity> + </entity> + <entity name="DefaultConfigWebUrlOptions" type="url_use_store_value"> + <data key="value">0</data> + </entity> + + <entity name="EnableWebUrlOptionsConfig" type="web_url_use_store"> + <requiredEntity type="url_use_store_value">WebUrlOptionsYes</requiredEntity> + </entity> + <entity name="WebUrlOptionsYes" type="url_use_store_value"> + <data key="value">1</data> + </entity> +</entities> diff --git a/app/code/Magento/Config/Test/Mftf/Metadata/allow_guest_checkout-meta.xml b/app/code/Magento/Config/Test/Mftf/Metadata/allow_guest_checkout-meta.xml new file mode 100644 index 000000000000..052d9b657477 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Metadata/allow_guest_checkout-meta.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="AllowGuestCheckoutConfig" dataType="allow_guest_checkout_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/checkout/" method="POST"> + <object key="groups" dataType="allow_guest_checkout_config"> + <object key="options" dataType="allow_guest_checkout_config"> + <object key="fields" dataType="allow_guest_checkout_config"> + <object key="guest_checkout" dataType="guest_checkout"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Config/Test/Mftf/Metadata/locale_options_config-meta.xml b/app/code/Magento/Config/Test/Mftf/Metadata/locale_options_config-meta.xml new file mode 100644 index 000000000000..055a9896cd2d --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Metadata/locale_options_config-meta.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="GeneralLocaleOptionsConfig" dataType="locale_options_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/general/" method="POST"> + <object key="groups" dataType="locale_options_config"> + <object key="locale" dataType="locale_options_config"> + <object key="fields" dataType="locale_options_config"> + <object key="code" dataType="code"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Config/Test/Mftf/Metadata/system_config-countries-meta.xml b/app/code/Magento/Config/Test/Mftf/Metadata/system_config-countries-meta.xml new file mode 100644 index 000000000000..bd16c225af51 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Metadata/system_config-countries-meta.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="AdminAccountCountryOptionConfig" dataType="admin_account_country_options_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/general/" method="POST"> + <object key="groups" dataType="admin_account_country_options_config"> + <object key="country" dataType="admin_account_country_options_config"> + <object key="fields" dataType="admin_account_country_options_config"> + <object key="allow" dataType="admin_account_country_options_value"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> + + <operation name="DefaultAdminAccountCountryOptionConfig" dataType="default_admin_account_country_options_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/general/" method="POST"> + <object key="groups" dataType="default_admin_account_country_options_config"> + <object key="country" dataType="default_admin_account_country_options_config"> + <object key="fields" dataType="default_admin_account_country_options_config"> + <object key="allow" dataType="default_admin_account_country_options_config"> + <object key="inherit" dataType="checkoutTotalFlagZero"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Config/Test/Mftf/Metadata/system_config-meta.xml b/app/code/Magento/Config/Test/Mftf/Metadata/system_config-meta.xml index 37b8414d1f39..e7544c4e8ae2 100644 --- a/app/code/Magento/Config/Test/Mftf/Metadata/system_config-meta.xml +++ b/app/code/Magento/Config/Test/Mftf/Metadata/system_config-meta.xml @@ -5,8 +5,9 @@ * See COPYING.txt for license details. */ --> + <operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> <operation name="AdminAccountSharingConfig" dataType="admin_account_sharing_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/admin/" method="POST"> <object key="groups" dataType="admin_account_sharing_config"> <object key="security" dataType="admin_account_sharing_config"> diff --git a/app/code/Magento/Config/Test/Mftf/Metadata/web_url_options_config-meta.xml b/app/code/Magento/Config/Test/Mftf/Metadata/web_url_options_config-meta.xml new file mode 100644 index 000000000000..fc14ba7bbaba --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Metadata/web_url_options_config-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="WebUrlOptionsConfig" dataType="web_url_use_store" type="create" auth="adminFormKey" url="/admin/system_config/save/section/web/" + method="POST" successRegex="/messages-message-success/" returnRegex=""> + <object key="groups" dataType="web_url_use_store"> + <object key="url" dataType="web_url_use_store"> + <object key="fields" dataType="web_url_use_store"> + <object key="use_store" dataType="url_use_store_value"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Config/Test/Mftf/Page/AdminCatalogSearchConfigurationPage.xml b/app/code/Magento/Config/Test/Mftf/Page/AdminCatalogSearchConfigurationPage.xml new file mode 100644 index 000000000000..c3fa13f59697 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Page/AdminCatalogSearchConfigurationPage.xml @@ -0,0 +1,12 @@ +<?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="AdminCatalogSearchConfigurationPage" url="admin/system_config/edit/section/catalog/" area="admin" module="Magento_Config"> + <section name="AdminCatalogSearchConfigurationSection"/> + </page> +</pages> diff --git a/app/code/Magento/Config/Test/Mftf/Page/AdminStoresCustomerConfigurationPage.xml b/app/code/Magento/Config/Test/Mftf/Page/AdminStoresCustomerConfigurationPage.xml new file mode 100644 index 000000000000..6a4efb6b9e15 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Page/AdminStoresCustomerConfigurationPage.xml @@ -0,0 +1,12 @@ +<?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="AdminStoresCustomerConfigurationPage" url="admin/system_config/edit/section/customer/" area="admin" module="Magento_Config"> + <section name="AdminStoresCustomerConfigurationSection"/> + </page> +</pages> diff --git a/app/code/Magento/Config/Test/Mftf/Section/AdminConfigSection.xml b/app/code/Magento/Config/Test/Mftf/Section/AdminConfigSection.xml index dbede5491e01..b5bfe9cc2ea0 100644 --- a/app/code/Magento/Config/Test/Mftf/Section/AdminConfigSection.xml +++ b/app/code/Magento/Config/Test/Mftf/Section/AdminConfigSection.xml @@ -7,17 +7,11 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminConfigSection"> - <element name="advancedReportingMenuItem" type="text" selector="//a[contains(concat(' ',normalize-space(@class),' '),'item-nav')]/span[text()='Advanced Reporting']"/> - <element name="advancedReportingService" type="select" selector="#analytics_general_enabled"/> - <element name="advancedReportingServiceLabel" type="text" selector="#row_analytics_general_enabled>td.label>label>span"/> - <element name="advancedReportingServiceStatus" type="text" selector="#row_analytics_general_enabled>td.value>p>span"/> - <element name="advancedReportingIndustry" type="select" selector="#analytics_general_vertical"/> - <element name="advancedReportingIndustryLabel" type="text" selector=".config-vertical-label>label>span"/> - <element name="advancedReportingHour" type="select" selector="#row_analytics_general_collection_time>td:nth-child(2)>select:nth-child(2)"/> - <element name="advancedReportingMinute" type="select" selector="#row_analytics_general_collection_time>td:nth-child(2)>select:nth-child(3)"/> - <element name="advancedReportingSeconds" type="select" selector="#row_analytics_general_collection_time>td:nth-child(2)>select:nth-child(4)"/> - <element name="advancedReportingBlankIndustryError" type="text" selector=".message-error>div"/> - <element name="advancedReportingSuccessMessage" type="text" selector=".message-success>div"/> <element name="saveButton" type="button" selector="#save"/> + <element name="generalTab" type="text" selector="//div[@class='admin__page-nav-title title _collapsible']//strong[text()='General']"/> + <element name="generalTabClosed" type="text" selector="//div[@class='admin__page-nav-title title _collapsible' and @aria-expanded='false' or @aria-expanded='0']//strong[text()='General']"/> + <element name="generalTabOpened" type="text" selector="//div[@class='admin__page-nav-title title _collapsible' and @aria-expanded='true' or @aria-expanded='1']//strong[text()='General']"/> + <element name="defaultConfigButton" type="button" selector="#store-change-button" timeout="30"/> + <element name="defaultConfigDropdown" type="button" selector="//ul[@class='dropdown-menu']" timeout="30"/> </section> </sections> \ No newline at end of file diff --git a/app/code/Magento/Config/Test/Mftf/Section/AdminSalesConfigSection.xml b/app/code/Magento/Config/Test/Mftf/Section/AdminSalesConfigSection.xml index 55679fdb1524..1c2e2603566b 100644 --- a/app/code/Magento/Config/Test/Mftf/Section/AdminSalesConfigSection.xml +++ b/app/code/Magento/Config/Test/Mftf/Section/AdminSalesConfigSection.xml @@ -9,5 +9,9 @@ <section name="AdminSalesConfigSection"> <element name="enableMAPUseSystemValue" type="checkbox" selector="#sales_msrp_enabled_inherit"/> <element name="enableMAPSelect" type="select" selector="#sales_msrp_enabled"/> + <element name="giftOptions" type="select" selector="#sales_gift_options-head"/> + <element name="allowGiftReceipt" type="select" selector="#sales_gift_options_allow_gift_receipt"/> + <element name="allowPrintedCard" type="select" selector="#sales_gift_options_allow_printed_card"/> + <element name="go" type="select" selector="//a[@id='sales_gift_options-head']/ancestor::div[@class='entry-edit-head admin__collapsible-block']"/> </section> </sections> \ No newline at end of file diff --git a/app/code/Magento/Config/Test/Mftf/Section/AdminStoresCustomerConfigurationSection.xml b/app/code/Magento/Config/Test/Mftf/Section/AdminStoresCustomerConfigurationSection.xml new file mode 100644 index 000000000000..823be383ce12 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Section/AdminStoresCustomerConfigurationSection.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="AdminStoresCustomerConfigurationSection"> + <element name="createNewAccOpt" type="button" selector="#customer_create_account-head"/> + <element name="enableAutoAssignCustomerGroup" type="button" selector="#customer_create_account_auto_group_assign"/> + <element name="groupForValidVATIdIntraUnion" type="select" selector="#customer_create_account_viv_intra_union_group"/> + </section> +</sections> diff --git a/app/code/Magento/Config/Test/Mftf/Section/CatalogSearchAdminConfigSection.xml b/app/code/Magento/Config/Test/Mftf/Section/CatalogSearchAdminConfigSection.xml new file mode 100644 index 000000000000..e82ad4670f9b --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Section/CatalogSearchAdminConfigSection.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="AdminCatalogSearchConfigurationSection"> + <element name="catalogSearchTab" type="button" selector="#catalog_search-head"/> + <element name="checkIfCatalogSearchTabExpand" type="button" selector="#catalog_search-head:not(.open)"/> + <element name="searchEngineDefaultSystemValue" type="checkbox" selector="#catalog_search_engine_inherit"/> + <element name="searchEngine" type="select" selector="#catalog_search_engine"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Config/Test/Mftf/Section/CatalogSection.xml b/app/code/Magento/Config/Test/Mftf/Section/CatalogSection.xml index 3b1de96abc8a..e999dbc42a6a 100644 --- a/app/code/Magento/Config/Test/Mftf/Section/CatalogSection.xml +++ b/app/code/Magento/Config/Test/Mftf/Section/CatalogSection.xml @@ -11,5 +11,15 @@ <section name="CatalogSection"> <element name="storefront" type="select" selector="#catalog_frontend-head"/> <element name="CheckIfTabExpand" type="button" selector="#catalog_frontend-head:not(.open)"/> + <element name="price" type="button" selector="#catalog_price-head"/> + <element name="checkIfPriceExpand" type="button" selector="//a[@id='catalog_price-head' and @class='open']"/> + <element name="catalogPriceScope" type="select" selector="#catalog_price_scope"/> + <element name="catalogPriceScopeValue" type="select" selector="//select[@id='catalog_price_scope']/option[text()='{{args}}']" parameterized="true"/> + <element name="defaultProductPrice" type="input" selector="#catalog_price_default_product_price"/> + <element name="save" type="button" selector="#save"/> + <element name="flatCatalogCategoryCheckBox" type="checkbox" selector="#catalog_frontend_flat_catalog_category_inherit"/> + <element name="flatCatalogCategory" type="select" selector="#catalog_frontend_flat_catalog_category"/> + <element name="flatCatalogProduct" type="select" selector="#catalog_frontend_flat_catalog_product"/> + <element name="successMessage" type="text" selector="#messages"/> </section> </sections> diff --git a/app/code/Magento/Config/Test/Mftf/Section/GeneralSection.xml b/app/code/Magento/Config/Test/Mftf/Section/GeneralSection.xml index 85035c6b56f9..d007c860782a 100644 --- a/app/code/Magento/Config/Test/Mftf/Section/GeneralSection.xml +++ b/app/code/Magento/Config/Test/Mftf/Section/GeneralSection.xml @@ -16,7 +16,8 @@ <element name="SwitcherSystemValue" type="button" selector="#cms_wysiwyg_editor_inherit"/> <element name="Switcher" type="button" selector="#cms_wysiwyg_editor" /> <element name="StaticURL" type="button" selector="#cms_wysiwyg_use_static_urls_in_catalog" /> - <element name="Save" type="button" selector="#save"/> + <element name="Save" type="button" selector="#save" timeout="30"/> + <element name="StoreConfigurationPageSuccessMessage" type="text" selector="#messages [data-ui-id='messages-message-success']"/> </section> <section name="WebSection"> <element name="DefaultLayoutsTab" type="button" selector="#web_default_layouts-head"/> @@ -38,4 +39,9 @@ <element name="countryOptionsOpen" type="button" selector="#general_country-head.open"/> <element name="topDestinations" type="select" selector="#general_country_destinations"/> </section> + <section name="StateOptionsSection"> + <element name="stateOptions" type="button" selector="#general_region-head"/> + <element name="countriesWithRequiredRegions" type="select" selector="#general_region_state_required"/> + <element name="allowToChooseState" type="select" selector="general_region_display_all"/> + </section> </sections> diff --git a/app/code/Magento/Config/Test/Mftf/Test/CheckingCountryDropDownWithOneAllowedCountryTest.xml b/app/code/Magento/Config/Test/Mftf/Test/CheckingCountryDropDownWithOneAllowedCountryTest.xml new file mode 100644 index 000000000000..b0a7ee07ddad --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Test/CheckingCountryDropDownWithOneAllowedCountryTest.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="CheckingCountryDropDownWithOneAllowedCountryTest"> + <annotations> + <features value="Config"/> + <stories value="MAGETWO-96107: Additional blank option in country dropdown"/> + <title value="Checking country drop-down with one allowed country"/> + <description value="Check country drop-down with one allowed country"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-96133"/> + <group value="configuration"/> + </annotations> + <before> + <createData entity="EnableAdminAccountAllowCountry" stepKey="setAllowedCountries"/> + </before> + <after> + <createData entity="DisableAdminAccountAllowCountry" stepKey="setDefaultValueForAllowCountries"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> + <argument name="customerEmail" value="CustomerEntityOne.email"/> + </actionGroup> + <actionGroup ref="AdminClearCustomersFiltersActionGroup" stepKey="clearFilters"/> + <waitForPageLoad stepKey="WaitForPageToLoad"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Flush Magento Cache--> + <magentoCLI stepKey="flushCache" command="cache:flush"/> + <!--Create a customer account from Storefront--> + <actionGroup ref="SignUpNewUserFromStorefrontActionGroup" stepKey="createAnAccount"> + <argument name="Customer" value="CustomerEntityOne"/> + </actionGroup> + <click selector="{{CheckoutPaymentSection.addressBook}}" stepKey="goToAddressBook"/> + <click selector="{{StorefrontCustomerAddressSection.country}}" stepKey="clickToExpandCountryDropDown"/> + <see selector="{{StorefrontCustomerAddressSection.country}}" userInput="United States" stepKey="seeSelectedCountry"/> + <dontSee selector="{{StorefrontCustomerAddressSection.country}}" userInput="Brazil" stepKey="canNotSeeSelectedCountry"/> + </test> +</tests> diff --git a/app/code/Magento/Config/Test/Unit/App/Config/Type/SystemTest.php b/app/code/Magento/Config/Test/Unit/App/Config/Type/SystemTest.php deleted file mode 100644 index 40aa110382ed..000000000000 --- a/app/code/Magento/Config/Test/Unit/App/Config/Type/SystemTest.php +++ /dev/null @@ -1,181 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Config\Test\Unit\App\Config\Type; - -use Magento\Config\App\Config\Type\System; -use Magento\Framework\App\Config\ConfigSourceInterface; -use Magento\Framework\App\Config\Spi\PostProcessorInterface; -use Magento\Framework\App\Config\Spi\PreProcessorInterface; -use Magento\Framework\Cache\FrontendInterface; -use Magento\Framework\Serialize\SerializerInterface; -use Magento\Store\Model\Config\Processor\Fallback; -use Magento\Config\App\Config\Type\System\Reader; - -/** - * Test how Class process source, cache them and retrieve value by path - * @package Magento\Config\Test\Unit\App\Config\Type - */ -class SystemTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var ConfigSourceInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $source; - - /** - * @var PostProcessorInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $postProcessor; - - /** - * @var PreProcessorInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $preProcessor; - - /** - * @var Fallback|\PHPUnit_Framework_MockObject_MockObject - */ - private $fallback; - - /** - * @var FrontendInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $cache; - - /** - * @var System - */ - private $configType; - - /** - * @var SerializerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $serializer; - - /** - * @var Reader|\PHPUnit_Framework_MockObject_MockObject - */ - private $reader; - - public function setUp() - { - $this->source = $this->getMockBuilder(ConfigSourceInterface::class) - ->getMockForAbstractClass(); - $this->postProcessor = $this->getMockBuilder(PostProcessorInterface::class) - ->getMockForAbstractClass(); - $this->fallback = $this->getMockBuilder(Fallback::class) - ->disableOriginalConstructor() - ->getMock(); - $this->cache = $this->getMockBuilder(FrontendInterface::class) - ->getMockForAbstractClass(); - $this->preProcessor = $this->getMockBuilder(PreProcessorInterface::class) - ->getMockForAbstractClass(); - $this->serializer = $this->getMockBuilder(SerializerInterface::class) - ->getMock(); - $this->reader = $this->getMockBuilder(Reader::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->configType = new System( - $this->source, - $this->postProcessor, - $this->fallback, - $this->cache, - $this->serializer, - $this->preProcessor, - 1, - 'system', - $this->reader - ); - } - - public function testGetCachedWithLoadDefaultScopeData() - { - $path = 'default/dev/unsecure/url'; - $url = 'http://magento.test/'; - $data = [ - 'dev' => [ - 'unsecure' => [ - 'url' => $url - ] - ] - ]; - - $this->cache->expects($this->any()) - ->method('load') - ->willReturnOnConsecutiveCalls('1', serialize($data)); - $this->serializer->expects($this->once()) - ->method('unserialize') - ->willReturn($data); - $this->assertEquals($url, $this->configType->get($path)); - } - - public function testGetCachedWithLoadAllData() - { - $url = 'http://magento.test/'; - $data = [ - 'dev' => [ - 'unsecure' => [ - 'url' => $url - ] - ] - ]; - - $this->cache->expects($this->any()) - ->method('load') - ->willReturnOnConsecutiveCalls('1', serialize($data)); - $this->serializer->expects($this->once()) - ->method('unserialize') - ->willReturn($data); - $this->assertEquals($data, $this->configType->get('')); - } - - public function testGetNotCached() - { - $path = 'stores/default/dev/unsecure/url'; - $url = 'http://magento.test/'; - - $dataToCache = [ - 'unsecure' => [ - 'url' => $url - ] - ]; - $data = [ - 'default' => [], - 'websites' => [], - 'stores' => [ - 'default' => [ - 'dev' => [ - 'unsecure' => [ - 'url' => $url - ] - ] - ] - ] - ]; - $this->cache->expects($this->any()) - ->method('load') - ->willReturnOnConsecutiveCalls(false, false); - - $this->serializer->expects($this->atLeastOnce()) - ->method('serialize') - ->willReturn(serialize($dataToCache)); - $this->cache->expects($this->atLeastOnce()) - ->method('save') - ->willReturnSelf(); - $this->reader->expects($this->once()) - ->method('read') - ->willReturn($data); - $this->postProcessor->expects($this->once()) - ->method('process') - ->with($data) - ->willReturn($data); - - $this->assertEquals($url, $this->configType->get($path)); - $this->assertEquals($url, $this->configType->get($path)); - } -} diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/FileTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/FileTest.php index de18d45d2686..31215f1bdee2 100644 --- a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/FileTest.php +++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/FileTest.php @@ -40,7 +40,11 @@ protected function setUp() $this->file = $objectManager->getObject( \Magento\Config\Block\System\Config\Form\Field\File::class, - ['data' => $this->testData] + [ + '_escaper' => $objectManager->getObject(\Magento\Framework\Escaper::class), + 'data' => $this->testData, + + ] ); $formMock = new \Magento\Framework\DataObject(); diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php index 8a005a52ab61..b752f79f7344 100644 --- a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php +++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php @@ -34,6 +34,7 @@ protected function setUp() \Magento\Config\Block\System\Config\Form\Field\Image::class, [ 'urlBuilder' => $this->urlBuilderMock, + '_escaper' => $objectManager->getObject(\Magento\Framework\Escaper::class) ] ); diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/Select/AllowspecificTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/Select/AllowspecificTest.php index f5c65e848b3b..e7ba2e8aaa2e 100644 --- a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/Select/AllowspecificTest.php +++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/Select/AllowspecificTest.php @@ -21,7 +21,10 @@ protected function setUp() { $testHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->_object = $testHelper->getObject( - \Magento\Config\Block\System\Config\Form\Field\Select\Allowspecific::class + \Magento\Config\Block\System\Config\Form\Field\Select\Allowspecific::class, + [ + '_escaper' => $testHelper->getObject(\Magento\Framework\Escaper::class) + ] ); $this->_object->setData('html_id', 'spec_element'); $this->_formMock = $this->createPartialMock( diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/FormTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/FormTest.php index 93650dd62657..4e260b0fb2bb 100644 --- a/app/code/Magento/Config/Test/Unit/Block/System/Config/FormTest.php +++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/FormTest.php @@ -102,6 +102,9 @@ protected function setUp() \Magento\Config\Block\System\Config\Form\Fieldset\Factory::class ); $this->_fieldFactoryMock = $this->createMock(\Magento\Config\Block\System\Config\Form\Field\Factory::class); + $settingCheckerMock = $this->getMockBuilder(SettingChecker::class) + ->disableOriginalConstructor() + ->getMock(); $this->_coreConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); $this->_backendConfigMock = $this->createMock(\Magento\Config\Model\Config::class); @@ -153,6 +156,7 @@ protected function setUp() 'fieldsetFactory' => $this->_fieldsetFactoryMock, 'fieldFactory' => $this->_fieldFactoryMock, 'context' => $context, + 'settingChecker' => $settingCheckerMock, ]; $objectArguments = $helper->getConstructArguments(\Magento\Config\Block\System\Config\Form::class, $data); @@ -532,7 +536,7 @@ public function testInitFields( $elementVisibilityMock = $this->getMockBuilder(ElementVisibilityInterface::class) ->getMockForAbstractClass(); - $elementVisibilityMock->expects($this->once()) + $elementVisibilityMock->expects($this->any()) ->method('isDisabled') ->willReturn($isDisabled); diff --git a/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/DefaultProcessorTest.php b/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/DefaultProcessorTest.php index 984e0fe84268..35b2406b328c 100644 --- a/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/DefaultProcessorTest.php +++ b/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/DefaultProcessorTest.php @@ -7,13 +7,14 @@ use Magento\Config\App\Config\Type\System; use Magento\Config\Console\Command\ConfigSet\DefaultProcessor; +use Magento\Config\Model\Config; +use Magento\Config\Model\Config\Factory as ConfigFactory; use Magento\Framework\App\Config\ConfigPathResolver; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\DeploymentConfig; use Magento\Store\Model\ScopeInterface; use Magento\Config\Model\PreparedValueFactory; use Magento\Framework\App\Config\Value; -use Magento\Framework\App\Config\ValueInterface; use Magento\Framework\Model\ResourceModel\Db\AbstractDb; use PHPUnit_Framework_MockObject_MockObject as Mock; @@ -55,17 +56,18 @@ class DefaultProcessorTest extends \PHPUnit\Framework\TestCase */ private $resourceModelMock; + /** + * @var ConfigFactory|Mock + */ + private $configFactory; + /** * @inheritdoc */ protected function setUp() { - $this->deploymentConfigMock = $this->getMockBuilder(DeploymentConfig::class) - ->disableOriginalConstructor() - ->getMock(); - $this->configPathResolverMock = $this->getMockBuilder(ConfigPathResolver::class) - ->disableOriginalConstructor() - ->getMock(); + $this->deploymentConfigMock = $this->createMock(DeploymentConfig::class); + $this->configPathResolverMock = $this->createMock(ConfigPathResolver::class); $this->resourceModelMock = $this->getMockBuilder(AbstractDb::class) ->disableOriginalConstructor() ->setMethods(['save']) @@ -74,14 +76,14 @@ protected function setUp() ->disableOriginalConstructor() ->setMethods(['getResource']) ->getMock(); - $this->preparedValueFactoryMock = $this->getMockBuilder(PreparedValueFactory::class) - ->disableOriginalConstructor() - ->getMock(); + $this->preparedValueFactoryMock = $this->createMock(PreparedValueFactory::class); + $this->configFactory = $this->createMock(ConfigFactory::class); $this->model = new DefaultProcessor( $this->preparedValueFactoryMock, $this->deploymentConfigMock, - $this->configPathResolverMock + $this->configPathResolverMock, + $this->configFactory ); } @@ -98,15 +100,16 @@ public function testProcess($path, $value, $scope, $scopeCode) { $this->configMockForProcessTest($path, $scope, $scopeCode); - $this->preparedValueFactoryMock->expects($this->once()) + $config = $this->createMock(Config::class); + $this->configFactory->expects($this->once()) ->method('create') - ->willReturn($this->valueMock); - $this->valueMock->expects($this->once()) - ->method('getResource') - ->willReturn($this->resourceModelMock); - $this->resourceModelMock->expects($this->once()) + ->with(['data' => ['scope' => $scope, 'scope_code' => $scopeCode]]) + ->willReturn($config); + $config->expects($this->once()) + ->method('setDataByPath') + ->with($path, $value); + $config->expects($this->once()) ->method('save') - ->with($this->valueMock) ->willReturnSelf(); $this->model->process($path, $value, $scope, $scopeCode); @@ -124,28 +127,6 @@ public function processDataProvider() ]; } - public function testProcessWithWrongValueInstance() - { - $path = 'test/test/test'; - $scope = ScopeConfigInterface::SCOPE_TYPE_DEFAULT; - $scopeCode = null; - $value = 'value'; - $valueInterfaceMock = $this->getMockBuilder(ValueInterface::class) - ->getMockForAbstractClass(); - - $this->configMockForProcessTest($path, $scope, $scopeCode); - - $this->preparedValueFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($valueInterfaceMock); - $this->valueMock->expects($this->never()) - ->method('getResource'); - $this->resourceModelMock->expects($this->never()) - ->method('save'); - - $this->model->process($path, $value, $scope, $scopeCode); - } - /** * @param string $path * @param string $scope @@ -185,6 +166,9 @@ public function testProcessLockedValue() ->method('resolve') ->willReturn('system/default/test/test/test'); + $this->configFactory->expects($this->never()) + ->method('create'); + $this->model->process($path, $value, ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null); } } diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/Backend/SerializedTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/Backend/SerializedTest.php index bb1e0e022590..c2685e0a265c 100644 --- a/app/code/Magento/Config/Test/Unit/Model/Config/Backend/SerializedTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/Config/Backend/SerializedTest.php @@ -9,7 +9,11 @@ use Magento\Framework\Model\Context; use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Psr\Log\LoggerInterface; +/** + * Class SerializedTest + */ class SerializedTest extends \PHPUnit\Framework\TestCase { /** @var \Magento\Config\Model\Config\Backend\Serialized */ @@ -18,14 +22,20 @@ class SerializedTest extends \PHPUnit\Framework\TestCase /** @var Json|\PHPUnit_Framework_MockObject_MockObject */ private $serializerMock; + /** @var LoggerInterface|\PHPUnit_Framework_MockObject_MockObject */ + private $loggerMock; + protected function setUp() { $objectManager = new ObjectManager($this); $this->serializerMock = $this->createMock(Json::class); + $this->loggerMock = $this->createMock(LoggerInterface::class); $contextMock = $this->createMock(Context::class); $eventManagerMock = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); $contextMock->method('getEventDispatcher') ->willReturn($eventManagerMock); + $contextMock->method('getLogger') + ->willReturn($this->loggerMock); $this->serializedConfig = $objectManager->getObject( Serialized::class, [ @@ -72,6 +82,20 @@ public function afterLoadDataProvider() ]; } + public function testAfterLoadWithException() + { + $value = '{"key":'; + $expected = false; + $this->serializedConfig->setValue($value); + $this->serializerMock->expects($this->once()) + ->method('unserialize') + ->willThrowException(new \Exception()); + $this->loggerMock->expects($this->once()) + ->method('critical'); + $this->serializedConfig->afterLoad(); + $this->assertEquals($expected, $this->serializedConfig->getValue()); + } + /** * @param string $expected * @param int|double|string|array|boolean|null $value diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/StructureTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/StructureTest.php index 6c059f4b69b7..a17faf8f3588 100644 --- a/app/code/Magento/Config/Test/Unit/Model/Config/StructureTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/Config/StructureTest.php @@ -418,6 +418,7 @@ public function testGetFieldPaths() 'field_2' ], 'field_3' => [ + 'field_3', 'field_3' ], 'field_3_1' => [ diff --git a/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php b/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php index d0568f48ded1..66163e354cc0 100644 --- a/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php @@ -5,6 +5,8 @@ */ namespace Magento\Config\Test\Unit\Model; +use PHPUnit\Framework\MockObject\MockObject; + /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -13,130 +15,158 @@ class ConfigTest extends \PHPUnit\Framework\TestCase /** * @var \Magento\Config\Model\Config */ - protected $_model; + private $model; + + /** + * @var \Magento\Framework\Event\ManagerInterface|MockObject + */ + private $eventManagerMock; + + /** + * @var \Magento\Config\Model\Config\Structure\Reader|MockObject + */ + private $structureReaderMock; + + /** + * @var \Magento\Framework\DB\TransactionFactory|MockObject + */ + private $transFactoryMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\App\Config\ReinitableConfigInterface|MockObject */ - protected $_eventManagerMock; + private $appConfigMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Config\Model\Config\Loader|MockObject */ - protected $_structureReaderMock; + private $configLoaderMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\App\Config\ValueFactory|MockObject */ - protected $_transFactoryMock; + private $dataFactoryMock; /** - * @var \Magento\Framework\App\Config\ReinitableConfigInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Store\Model\StoreManagerInterface|MockObject */ - protected $_appConfigMock; + private $storeManager; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Config\Model\Config\Structure|MockObject */ - protected $_applicationMock; + private $configStructure; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Config\Model\Config\Reader\Source\Deployed\SettingChecker|MockObject */ - protected $_configLoaderMock; + private $settingsChecker; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\App\ScopeResolverPool|MockObject */ - protected $_dataFactoryMock; + private $scopeResolverPool; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var \Magento\Framework\App\ScopeResolverInterface|MockObject */ - protected $_storeManager; + private $scopeResolver; /** - * @var \Magento\Config\Model\Config\Structure + * @var \Magento\Framework\App\ScopeInterface|MockObject */ - protected $_configStructure; + private $scope; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Store\Model\ScopeTypeNormalizer|MockObject */ - private $_settingsChecker; + private $scopeTypeNormalizer; protected function setUp() { - $this->_eventManagerMock = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); - $this->_structureReaderMock = $this->createPartialMock( + $this->eventManagerMock = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); + $this->structureReaderMock = $this->createPartialMock( \Magento\Config\Model\Config\Structure\Reader::class, ['getConfiguration'] ); - $this->_configStructure = $this->createMock(\Magento\Config\Model\Config\Structure::class); + $this->configStructure = $this->createMock(\Magento\Config\Model\Config\Structure::class); - $this->_structureReaderMock->expects( + $this->structureReaderMock->expects( $this->any() )->method( 'getConfiguration' )->will( - $this->returnValue($this->_configStructure) + $this->returnValue($this->configStructure) ); - $this->_transFactoryMock = $this->createPartialMock( + $this->transFactoryMock = $this->createPartialMock( \Magento\Framework\DB\TransactionFactory::class, ['create', 'addObject'] ); - $this->_appConfigMock = $this->createMock(\Magento\Framework\App\Config\ReinitableConfigInterface::class); - $this->_configLoaderMock = $this->createPartialMock( + $this->appConfigMock = $this->createMock(\Magento\Framework\App\Config\ReinitableConfigInterface::class); + $this->configLoaderMock = $this->createPartialMock( \Magento\Config\Model\Config\Loader::class, ['getConfigByPath'] ); - $this->_dataFactoryMock = $this->createMock(\Magento\Framework\App\Config\ValueFactory::class); + $this->dataFactoryMock = $this->createMock(\Magento\Framework\App\Config\ValueFactory::class); - $this->_storeManager = $this->getMockForAbstractClass(\Magento\Store\Model\StoreManagerInterface::class); + $this->storeManager = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); - $this->_settingsChecker = $this + $this->settingsChecker = $this ->createMock(\Magento\Config\Model\Config\Reader\Source\Deployed\SettingChecker::class); - $this->_model = new \Magento\Config\Model\Config( - $this->_appConfigMock, - $this->_eventManagerMock, - $this->_configStructure, - $this->_transFactoryMock, - $this->_configLoaderMock, - $this->_dataFactoryMock, - $this->_storeManager, - $this->_settingsChecker + $this->scopeResolverPool = $this->createMock(\Magento\Framework\App\ScopeResolverPool::class); + $this->scopeResolver = $this->createMock(\Magento\Framework\App\ScopeResolverInterface::class); + $this->scopeResolverPool->method('get') + ->willReturn($this->scopeResolver); + $this->scope = $this->createMock(\Magento\Framework\App\ScopeInterface::class); + $this->scopeResolver->method('getScope') + ->willReturn($this->scope); + + $this->scopeTypeNormalizer = $this->createMock(\Magento\Store\Model\ScopeTypeNormalizer::class); + + $this->model = new \Magento\Config\Model\Config( + $this->appConfigMock, + $this->eventManagerMock, + $this->configStructure, + $this->transFactoryMock, + $this->configLoaderMock, + $this->dataFactoryMock, + $this->storeManager, + $this->settingsChecker, + [], + $this->scopeResolverPool, + $this->scopeTypeNormalizer ); } public function testSaveDoesNotDoAnythingIfGroupsAreNotPassed() { - $this->_configLoaderMock->expects($this->never())->method('getConfigByPath'); - $this->_model->save(); + $this->configLoaderMock->expects($this->never())->method('getConfigByPath'); + $this->model->save(); } public function testSaveEmptiesNonSetArguments() { - $this->_structureReaderMock->expects($this->never())->method('getConfiguration'); - $this->assertNull($this->_model->getSection()); - $this->assertNull($this->_model->getWebsite()); - $this->assertNull($this->_model->getStore()); - $this->_model->save(); - $this->assertSame('', $this->_model->getSection()); - $this->assertSame('', $this->_model->getWebsite()); - $this->assertSame('', $this->_model->getStore()); + $this->structureReaderMock->expects($this->never())->method('getConfiguration'); + $this->assertNull($this->model->getSection()); + $this->assertNull($this->model->getWebsite()); + $this->assertNull($this->model->getStore()); + $this->model->save(); + $this->assertSame('', $this->model->getSection()); + $this->assertSame('', $this->model->getWebsite()); + $this->assertSame('', $this->model->getStore()); } public function testSaveToCheckAdminSystemConfigChangedSectionEvent() { $transactionMock = $this->createMock(\Magento\Framework\DB\Transaction::class); - $this->_transFactoryMock->expects($this->any())->method('create')->will($this->returnValue($transactionMock)); + $this->transFactoryMock->expects($this->any())->method('create')->will($this->returnValue($transactionMock)); - $this->_configLoaderMock->expects($this->any())->method('getConfigByPath')->will($this->returnValue([])); + $this->configLoaderMock->expects($this->any())->method('getConfigByPath')->will($this->returnValue([])); - $this->_eventManagerMock->expects( + $this->eventManagerMock->expects( $this->at(0) )->method( 'dispatch' @@ -145,7 +175,7 @@ public function testSaveToCheckAdminSystemConfigChangedSectionEvent() $this->arrayHasKey('website') ); - $this->_eventManagerMock->expects( + $this->eventManagerMock->expects( $this->at(0) )->method( 'dispatch' @@ -154,20 +184,20 @@ public function testSaveToCheckAdminSystemConfigChangedSectionEvent() $this->arrayHasKey('store') ); - $this->_model->setGroups(['1' => ['data']]); - $this->_model->save(); + $this->model->setGroups(['1' => ['data']]); + $this->model->save(); } public function testDoNotSaveReadOnlyFields() { $transactionMock = $this->createMock(\Magento\Framework\DB\Transaction::class); - $this->_transFactoryMock->expects($this->any())->method('create')->will($this->returnValue($transactionMock)); + $this->transFactoryMock->expects($this->any())->method('create')->will($this->returnValue($transactionMock)); - $this->_settingsChecker->expects($this->any())->method('isReadOnly')->will($this->returnValue(true)); - $this->_configLoaderMock->expects($this->any())->method('getConfigByPath')->will($this->returnValue([])); + $this->settingsChecker->expects($this->any())->method('isReadOnly')->will($this->returnValue(true)); + $this->configLoaderMock->expects($this->any())->method('getConfigByPath')->will($this->returnValue([])); - $this->_model->setGroups(['1' => ['fields' => ['key' => ['data']]]]); - $this->_model->setSection('section'); + $this->model->setGroups(['1' => ['fields' => ['key' => ['data']]]]); + $this->model->setSection('section'); $group = $this->createMock(\Magento\Config\Model\Config\Structure\Element\Group::class); $group->method('getPath')->willReturn('section/1'); @@ -176,15 +206,15 @@ public function testDoNotSaveReadOnlyFields() $field->method('getGroupPath')->willReturn('section/1'); $field->method('getId')->willReturn('key'); - $this->_configStructure->expects($this->at(0)) + $this->configStructure->expects($this->at(0)) ->method('getElement') ->with('section/1') ->will($this->returnValue($group)); - $this->_configStructure->expects($this->at(1)) + $this->configStructure->expects($this->at(1)) ->method('getElement') ->with('section/1') ->will($this->returnValue($group)); - $this->_configStructure->expects($this->at(2)) + $this->configStructure->expects($this->at(2)) ->method('getElement') ->with('section/1/key') ->will($this->returnValue($field)); @@ -193,28 +223,28 @@ public function testDoNotSaveReadOnlyFields() \Magento\Framework\App\Config\Value::class, ['addData'] ); - $this->_dataFactoryMock->expects($this->any())->method('create')->will($this->returnValue($backendModel)); + $this->dataFactoryMock->expects($this->any())->method('create')->will($this->returnValue($backendModel)); - $this->_transFactoryMock->expects($this->never())->method('addObject'); + $this->transFactoryMock->expects($this->never())->method('addObject'); $backendModel->expects($this->never())->method('addData'); - $this->_model->save(); + $this->model->save(); } public function testSaveToCheckScopeDataSet() { $transactionMock = $this->createMock(\Magento\Framework\DB\Transaction::class); - $this->_transFactoryMock->expects($this->any())->method('create')->will($this->returnValue($transactionMock)); + $this->transFactoryMock->expects($this->any())->method('create')->will($this->returnValue($transactionMock)); - $this->_configLoaderMock->expects($this->any())->method('getConfigByPath')->will($this->returnValue([])); + $this->configLoaderMock->expects($this->any())->method('getConfigByPath')->will($this->returnValue([])); - $this->_eventManagerMock->expects($this->at(0)) + $this->eventManagerMock->expects($this->at(0)) ->method('dispatch') ->with( $this->equalTo('admin_system_config_changed_section_section'), $this->arrayHasKey('website') ); - $this->_eventManagerMock->expects($this->at(0)) + $this->eventManagerMock->expects($this->at(0)) ->method('dispatch') ->with( $this->equalTo('admin_system_config_changed_section_section'), @@ -228,36 +258,51 @@ public function testSaveToCheckScopeDataSet() $field->method('getGroupPath')->willReturn('section/1'); $field->method('getId')->willReturn('key'); - $this->_configStructure->expects($this->at(0)) + $this->configStructure->expects($this->at(0)) ->method('getElement') ->with('section/1') ->will($this->returnValue($group)); - $this->_configStructure->expects($this->at(1)) + $this->configStructure->expects($this->at(1)) ->method('getElement') ->with('section/1') ->will($this->returnValue($group)); - $this->_configStructure->expects($this->at(2)) + $this->configStructure->expects($this->at(2)) ->method('getElement') ->with('section/1/key') ->will($this->returnValue($field)); - $this->_configStructure->expects($this->at(3)) + $this->configStructure->expects($this->at(3)) ->method('getElement') ->with('section/1') ->will($this->returnValue($group)); - $this->_configStructure->expects($this->at(4)) + $this->configStructure->expects($this->at(4)) ->method('getElement') ->with('section/1/key') ->will($this->returnValue($field)); + $this->scopeResolver->expects($this->atLeastOnce()) + ->method('getScope') + ->with('1') + ->willReturn($this->scope); + $this->scope->expects($this->atLeastOnce()) + ->method('getScopeType') + ->willReturn('website'); + $this->scope->expects($this->atLeastOnce()) + ->method('getId') + ->willReturn(1); + $this->scope->expects($this->atLeastOnce()) + ->method('getCode') + ->willReturn('website_code'); + $this->scopeTypeNormalizer->expects($this->atLeastOnce()) + ->method('normalize') + ->with('website') + ->willReturn('websites'); $website = $this->createMock(\Magento\Store\Model\Website::class); - $website->expects($this->any())->method('getCode')->will($this->returnValue('website_code')); - $this->_storeManager->expects($this->any())->method('getWebsite')->will($this->returnValue($website)); - $this->_storeManager->expects($this->any())->method('getWebsites')->will($this->returnValue([$website])); - $this->_storeManager->expects($this->any())->method('isSingleStoreMode')->will($this->returnValue(true)); + $this->storeManager->expects($this->any())->method('getWebsites')->will($this->returnValue([$website])); + $this->storeManager->expects($this->any())->method('isSingleStoreMode')->will($this->returnValue(true)); - $this->_model->setWebsite('website'); - $this->_model->setSection('section'); - $this->_model->setGroups(['1' => ['fields' => ['key' => ['data']]]]); + $this->model->setWebsite('1'); + $this->model->setSection('section'); + $this->model->setGroups(['1' => ['fields' => ['key' => ['data']]]]); $backendModel = $this->createPartialMock( \Magento\Framework\App\Config\Value::class, @@ -270,7 +315,7 @@ public function testSaveToCheckScopeDataSet() 'groups' => [1 => ['fields' => ['key' => ['data']]]], 'group_id' => null, 'scope' => 'websites', - 'scope_id' => 0, + 'scope_id' => 1, 'scope_code' => 'website_code', 'field_config' => null, 'fieldset_data' => ['key' => null], @@ -280,27 +325,64 @@ public function testSaveToCheckScopeDataSet() ->with('section/1/key') ->will($this->returnValue($backendModel)); - $this->_dataFactoryMock->expects($this->any())->method('create')->will($this->returnValue($backendModel)); + $this->dataFactoryMock->expects($this->any())->method('create')->will($this->returnValue($backendModel)); + + $this->model->save(); + } - $this->_model->save(); + /** + * @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) + { + $this->model->setDataByPath($path, $value); + $this->assertEquals($section, $this->model->getData('section')); + $this->assertEquals($groups, $this->model->getData('groups')); } - public function testSetDataByPath() + /** + * @return array + */ + public function setDataByPathDataProvider(): array { - $value = 'value'; - $path = '<section>/<group>/<field>'; - $this->_model->setDataByPath($path, $value); - $expected = [ - 'section' => '<section>', - 'groups' => [ - '<group>' => [ - 'fields' => [ - '<field>' => ['value' => $value], + 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()); } /** @@ -309,34 +391,31 @@ public function testSetDataByPath() */ public function testSetDataByPathEmpty() { - $this->_model->setDataByPath('', 'value'); + $this->model->setDataByPath('', 'value'); } /** * @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); + $this->model->setDataByPath($path, $value); } /** * @return array */ - public function setDataByPathWrongDepthDataProvider() + public function setDataByPathWrongDepthDataProvider(): array { return [ - 'depth 2' => ['section/group', "Your configuration depth is 2 for path 'section/group'"], - 'depth 1' => ['section', "Your configuration depth is 1 for path 'section'"], - 'depth 4' => ['section/group/field/sub-field', "Your configuration depth is 4 for path" - . " 'section/group/field/sub-field'", ], + 'depth 2' => ['section/group'], + 'depth 1' => ['section'], ]; } } diff --git a/app/code/Magento/Config/etc/adminhtml/di.xml b/app/code/Magento/Config/etc/adminhtml/di.xml index 5e54f177776b..189fbdf69a7e 100644 --- a/app/code/Magento/Config/etc/adminhtml/di.xml +++ b/app/code/Magento/Config/etc/adminhtml/di.xml @@ -6,7 +6,6 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> - <preference for="Magento\Config\Model\Config\Structure\SearchInterface" type="Magento\Config\Model\Config\Structure" /> <preference for="Magento\Config\Model\Config\Backend\File\RequestData\RequestDataInterface" type="Magento\Config\Model\Config\Backend\File\RequestData" /> <preference for="Magento\Config\Model\Config\Structure\ElementVisibilityInterface" type="Magento\Config\Model\Config\Structure\ElementVisibilityComposite" /> <type name="Magento\Config\Model\Config\Structure\Element\Iterator\Tab" shared="false" /> diff --git a/app/code/Magento/Config/etc/db_schema.xml b/app/code/Magento/Config/etc/db_schema.xml index 3f55d582776c..8aeac802fbd9 100644 --- a/app/code/Magento/Config/etc/db_schema.xml +++ b/app/code/Magento/Config/etc/db_schema.xml @@ -15,10 +15,10 @@ default="0" comment="Config Scope Id"/> <column xsi:type="varchar" name="path" nullable="false" length="255" default="general" comment="Config Path"/> <column xsi:type="text" name="value" nullable="true" comment="Config Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="config_id"/> </constraint> - <constraint xsi:type="unique" name="CORE_CONFIG_DATA_SCOPE_SCOPE_ID_PATH"> + <constraint xsi:type="unique" referenceId="CORE_CONFIG_DATA_SCOPE_SCOPE_ID_PATH"> <column name="scope"/> <column name="scope_id"/> <column name="path"/> diff --git a/app/code/Magento/Config/etc/di.xml b/app/code/Magento/Config/etc/di.xml index a5dd18097fb4..920cac382fcb 100644 --- a/app/code/Magento/Config/etc/di.xml +++ b/app/code/Magento/Config/etc/di.xml @@ -77,6 +77,11 @@ </argument> </arguments> </type> + <type name="Magento\Framework\Lock\Backend\Cache"> + <arguments> + <argument name="cache" xsi:type="object">Magento\Framework\App\Cache\Type\Config</argument> + </arguments> + </type> <type name="Magento\Config\App\Config\Type\System"> <arguments> <argument name="source" xsi:type="object">systemConfigSourceAggregatedProxy</argument> @@ -85,8 +90,18 @@ <argument name="preProcessor" xsi:type="object">Magento\Framework\App\Config\PreProcessorComposite</argument> <argument name="serializer" xsi:type="object">Magento\Framework\Serialize\Serializer\Serialize</argument> <argument name="reader" xsi:type="object">Magento\Config\App\Config\Type\System\Reader\Proxy</argument> + <argument name="lockQuery" xsi:type="object">systemConfigQueryLocker</argument> </arguments> </type> + + <virtualType name="systemConfigQueryLocker" type="Magento\Framework\Cache\LockGuardedCacheLoader"> + <arguments> + <argument name="locker" xsi:type="object">Magento\Framework\Lock\Backend\Cache</argument> + <argument name="lockTimeout" xsi:type="number">42000</argument> + <argument name="delayTimeout" xsi:type="number">100</argument> + </arguments> + </virtualType> + <type name="Magento\Config\App\Config\Type\System\Reader"> <arguments> <argument name="source" xsi:type="object">systemConfigSourceAggregated</argument> diff --git a/app/code/Magento/ConfigurableImportExport/Model/Export/RowCustomizer.php b/app/code/Magento/ConfigurableImportExport/Model/Export/RowCustomizer.php index 58462b873d8b..7146108f61fe 100644 --- a/app/code/Magento/ConfigurableImportExport/Model/Export/RowCustomizer.php +++ b/app/code/Magento/ConfigurableImportExport/Model/Export/RowCustomizer.php @@ -3,14 +3,21 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\ConfigurableImportExport\Model\Export; -use Magento\CatalogImportExport\Model\Export\RowCustomizerInterface; use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; -use Magento\ConfigurableProduct\Model\Product\Type\Configurable as ConfigurableProductType; +use Magento\CatalogImportExport\Model\Export\RowCustomizerInterface; use Magento\CatalogImportExport\Model\Import\Product as ImportProduct; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable as ConfigurableProductType; use Magento\ImportExport\Model\Import; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +/** + * Customizes output during export + */ class RowCustomizer implements RowCustomizerInterface { /** @@ -36,6 +43,19 @@ class RowCustomizer implements RowCustomizerInterface self::CONFIGURABLE_VARIATIONS_LABELS_COLUMN ]; + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @param StoreManagerInterface $storeManager + */ + public function __construct(StoreManagerInterface $storeManager) + { + $this->storeManager = $storeManager; + } + /** * Prepare configurable data for export * @@ -49,6 +69,9 @@ public function prepareData($collection, $productIds) $productCollection->addAttributeToFilter('entity_id', ['in' => $productIds]) ->addAttributeToFilter('type_id', ['eq' => ConfigurableProductType::TYPE_CODE]); + // set global scope during export + $this->storeManager->setCurrentStore(Store::DEFAULT_STORE_ID); + while ($product = $productCollection->fetchItem()) { $productAttributesOptions = $product->getTypeInstance()->getConfigurableOptions($product); $this->configurableData[$product->getId()] = []; diff --git a/app/code/Magento/ConfigurableImportExport/Model/Import/Product/Type/Configurable.php b/app/code/Magento/ConfigurableImportExport/Model/Import/Product/Type/Configurable.php index fdbd8560c266..3f4565771e70 100644 --- a/app/code/Magento/ConfigurableImportExport/Model/Import/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableImportExport/Model/Import/Product/Type/Configurable.php @@ -248,7 +248,7 @@ public function __construct( */ protected function _addAttributeParams($attrSetName, array $attrParams, $attribute) { - // save super attributes for simplier and quicker search in future + // save super attributes for simpler and quicker search in future if ('select' == $attrParams['type'] && 1 == $attrParams['is_global']) { $this->_superAttributes[$attrParams['code']] = $attrParams; } diff --git a/app/code/Magento/ConfigurableImportExport/composer.json b/app/code/Magento/ConfigurableImportExport/composer.json index c1aab3e7a148..419a08e14b0a 100644 --- a/app/code/Magento/ConfigurableImportExport/composer.json +++ b/app/code/Magento/ConfigurableImportExport/composer.json @@ -11,7 +11,8 @@ "magento/module-catalog-import-export": "*", "magento/module-configurable-product": "*", "magento/module-eav": "*", - "magento/module-import-export": "*" + "magento/module-import-export": "*", + "magento/module-store": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/ConfigurableImportExport/etc/module.xml b/app/code/Magento/ConfigurableImportExport/etc/module.xml index 7ff81f8d6344..b59234ca0e7d 100644 --- a/app/code/Magento/ConfigurableImportExport/etc/module.xml +++ b/app/code/Magento/ConfigurableImportExport/etc/module.xml @@ -6,6 +6,9 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> - <module name="Magento_ConfigurableImportExport" > + <module name="Magento_ConfigurableImportExport"> + <sequence> + <module name="Magento_ConfigurableProduct"/> + </sequence> </module> </config> diff --git a/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Edit/Button/Save.php b/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Edit/Button/Save.php index 8848fc78dad6..f2de5e72211e 100644 --- a/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Edit/Button/Save.php +++ b/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Edit/Button/Save.php @@ -8,7 +8,6 @@ use Magento\Ui\Component\Control\Container; use Magento\Catalog\Block\Adminhtml\Product\Edit\Button\Generic; use Magento\ConfigurableProduct\Model\Product\Type\Configurable as ConfigurableType; -use Magento\Catalog\Model\Product\Type; /** * Class Save @@ -16,16 +15,7 @@ class Save extends Generic { /** - * @var array - */ - private static $availableProductTypes = [ - ConfigurableType::TYPE_CODE, - Type::TYPE_SIMPLE, - Type::TYPE_VIRTUAL - ]; - - /** - * {@inheritdoc} + * @inheritdoc */ public function getButtonData() { @@ -135,7 +125,8 @@ protected function getOptions() } /** - * Retrieve target for button + * Retrieve target for button. + * * @return string */ protected function getSaveTarget() @@ -148,7 +139,8 @@ protected function getSaveTarget() } /** - * Retrieve action for button + * Retrieve action for button. + * * @return string */ protected function getSaveAction() @@ -161,10 +153,12 @@ protected function getSaveAction() } /** + * Is configurable product. + * * @return boolean */ protected function isConfigurableProduct() { - return in_array($this->getProduct()->getTypeId(), self::$availableProductTypes); + return !$this->getProduct()->isComposite() || $this->getProduct()->getTypeId() === ConfigurableType::TYPE_CODE; } } diff --git a/app/code/Magento/ConfigurableProduct/Block/Cart/Item/Renderer/Configurable.php b/app/code/Magento/ConfigurableProduct/Block/Cart/Item/Renderer/Configurable.php index 3b657dd1ab2d..77110975401f 100644 --- a/app/code/Magento/ConfigurableProduct/Block/Cart/Item/Renderer/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Block/Cart/Item/Renderer/Configurable.php @@ -70,4 +70,14 @@ public function getIdentities() } return $identities; } + + /** + * Get price for exact simple product added to cart + * + * @inheritdoc + */ + public function getProductPriceHtml(\Magento\Catalog\Model\Product $product) + { + return parent::getProductPriceHtml($this->getChildProduct()); + } } diff --git a/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php index 2502b79921e9..e07879e93a6b 100644 --- a/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php @@ -15,6 +15,8 @@ use Magento\Framework\Pricing\PriceCurrencyInterface; /** + * Confugurable product view type + * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api @@ -276,6 +278,8 @@ protected function getOptionImages() } /** + * Collect price options + * * @return array */ protected function getOptionPrices() @@ -314,6 +318,11 @@ protected function getOptionPrices() ), ], 'tierPrices' => $tierPrices, + 'msrpPrice' => [ + 'amount' => $this->localeFormat->getNumber( + $product->getMsrp() + ), + ], ]; } return $prices; diff --git a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/AddAttribute.php b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/AddAttribute.php index 5991097e456f..34f10b59f3a9 100644 --- a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/AddAttribute.php +++ b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/AddAttribute.php @@ -6,10 +6,11 @@ */ namespace Magento\ConfigurableProduct\Controller\Adminhtml\Product; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Backend\App\Action; use Magento\Framework\Controller\ResultFactory; -class AddAttribute extends Action +class AddAttribute extends Action implements HttpGetActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/CreateOptions.php b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/CreateOptions.php index 6f5f106a8bb2..cfa2562d974f 100644 --- a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/CreateOptions.php +++ b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/CreateOptions.php @@ -6,10 +6,11 @@ */ namespace Magento\ConfigurableProduct\Controller\Adminhtml\Product\Attribute; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Backend\App\Action; use Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory; -class CreateOptions extends Action +class CreateOptions extends Action implements HttpPostActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/GetAttributes.php b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/GetAttributes.php index b6b34073db60..9f5d5062b536 100644 --- a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/GetAttributes.php +++ b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/GetAttributes.php @@ -6,10 +6,11 @@ */ namespace Magento\ConfigurableProduct\Controller\Adminhtml\Product\Attribute; +use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Backend\App\Action; use Magento\ConfigurableProduct\Model\AttributesListInterface; -class GetAttributes extends Action +class GetAttributes extends Action implements HttpGetActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Configurable.php b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Configurable.php index 5cd8b6a7d0b9..b5940e36aa79 100644 --- a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Configurable.php @@ -158,7 +158,7 @@ protected function getVariationMatrix() $configurableMatrix = json_decode($configurableMatrix, true); foreach ($configurableMatrix as $item) { - if ($item['newProduct']) { + if (isset($item['newProduct']) && $item['newProduct']) { $result[$item['variationKey']] = $this->mapData($item); if (isset($item['qty'])) { diff --git a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Wizard.php b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Wizard.php index 8adfdea96102..104181aed4fc 100644 --- a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Wizard.php +++ b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Wizard.php @@ -5,6 +5,8 @@ */ namespace Magento\ConfigurableProduct\Controller\Adminhtml\Product; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Backend\App\Action; use Magento\Framework\Controller\ResultFactory; use Magento\Catalog\Controller\Adminhtml\Product\Builder; @@ -13,7 +15,7 @@ /** * Class Wizard */ -class Wizard extends Action +class Wizard extends Action implements HttpPostActionInterface, HttpGetActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php b/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php index 79c2dd812acf..2f07f8b90ce7 100644 --- a/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php +++ b/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -11,6 +10,11 @@ use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\StateException; +/** + * Configurable product link management. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class LinkManagement implements \Magento\ConfigurableProduct\Api\LinkManagementInterface { /** @@ -68,7 +72,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getChildren($sku) { @@ -107,11 +111,15 @@ public function getChildren($sku) } /** - * {@inheritdoc} + * @inheritdoc + * @throws InputException + * @throws NoSuchEntityException + * @throws StateException + * @throws \Magento\Framework\Exception\CouldNotSaveException */ public function addChild($sku, $childSku) { - $product = $this->productRepository->get($sku); + $product = $this->productRepository->get($sku, true); $child = $this->productRepository->get($childSku); $childrenIds = array_values($this->configurableType->getChildrenIds($product->getId())[0]); @@ -150,7 +158,11 @@ public function addChild($sku, $childSku) } /** - * {@inheritdoc} + * @inheritdoc + * @throws InputException + * @throws NoSuchEntityException + * @throws StateException + * @throws \Magento\Framework\Exception\CouldNotSaveException */ public function removeChild($sku, $childSku) { diff --git a/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/ProductIdentitiesExtender.php b/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/ProductIdentitiesExtender.php new file mode 100644 index 000000000000..92b7ab0d88ea --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/ProductIdentitiesExtender.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Model\Plugin\Frontend; + +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Catalog\Model\Product; + +/** + * Extender of product identities for child of configurable products + */ +class ProductIdentitiesExtender +{ + /** + * @var Configurable + */ + private $configurableType; + + /** + * @param Configurable $configurableType + */ + public function __construct(Configurable $configurableType) + { + $this->configurableType = $configurableType; + } + + /** + * Add child identities to product identities + * + * @param Product $subject + * @param array $identities + * @return array + */ + public function afterGetIdentities(Product $subject, array $identities): array + { + foreach ($this->configurableType->getChildrenIds($subject->getId()) as $childIds) { + foreach ($childIds as $childId) { + $identities[] = Product::CACHE_TAG . '_' . $childId; + } + } + + return array_unique($identities); + } +} diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Configuration/Item/ItemProductResolver.php b/app/code/Magento/ConfigurableProduct/Model/Product/Configuration/Item/ItemProductResolver.php index 6c33ecc138ae..7de78b6612a1 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Configuration/Item/ItemProductResolver.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Configuration/Item/ItemProductResolver.php @@ -13,16 +13,17 @@ use Magento\Catalog\Model\Product\Configuration\Item\ItemResolverInterface; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Catalog\Model\Product; +use Magento\Store\Model\ScopeInterface; /** - * {@inheritdoc} + * Resolves the product from a configured item. */ class ItemProductResolver implements ItemResolverInterface { /** * Path in config to the setting which defines if parent or child product should be used to generate a thumbnail. */ - const CONFIG_THUMBNAIL_SOURCE = 'checkout/cart/configurable_product_image'; + public const CONFIG_THUMBNAIL_SOURCE = 'checkout/cart/configurable_product_image'; /** * @var ScopeConfigInterface @@ -38,27 +39,21 @@ public function __construct(ScopeConfigInterface $scopeConfig) } /** - * {@inheritdoc} + * Get the final product from a configured item by product type and selection. + * + * @param ItemInterface $item + * @return ProductInterface */ - public function getFinalProduct(ItemInterface $item) : ProductInterface + public function getFinalProduct(ItemInterface $item): ProductInterface { /** * Show parent product thumbnail if it must be always shown according to the related setting in system config * or if child thumbnail is not available. */ - $parentProduct = $item->getProduct(); - $finalProduct = $parentProduct; + $finalProduct = $item->getProduct(); $childProduct = $this->getChildProduct($item); - if ($childProduct !== $parentProduct) { - $configValue = $this->scopeConfig->getValue( - self::CONFIG_THUMBNAIL_SOURCE, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ); - $childThumb = $childProduct->getData('thumbnail'); - $finalProduct = - ($configValue == Thumbnail::OPTION_USE_PARENT_IMAGE) || (!$childThumb || $childThumb == 'no_selection') - ? $parentProduct - : $childProduct; + if ($childProduct !== null && $this->isUseChildProduct($childProduct)) { + $finalProduct = $childProduct; } return $finalProduct; } @@ -67,15 +62,30 @@ public function getFinalProduct(ItemInterface $item) : ProductInterface * Get item configurable child product. * * @param ItemInterface $item - * @return Product + * @return Product | null */ - private function getChildProduct(ItemInterface $item) : Product + private function getChildProduct(ItemInterface $item): ?Product { + /** @var \Magento\Quote\Model\Quote\Item\Option $option */ $option = $item->getOptionByCode('simple_product'); - $product = $item->getProduct(); - if ($option) { - $product = $option->getProduct(); - } - return $product; + return $option ? $option->getProduct() : null; + } + + /** + * Is need to use child product + * + * @param Product $childProduct + * @return bool + */ + private function isUseChildProduct(Product $childProduct): bool + { + $configValue = $this->scopeConfig->getValue( + self::CONFIG_THUMBNAIL_SOURCE, + ScopeInterface::SCOPE_STORE + ); + $childThumb = $childProduct->getData('thumbnail'); + return $configValue !== Thumbnail::OPTION_USE_PARENT_IMAGE + && $childThumb !== null + && $childThumb !== 'no_selection'; } } diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php index 19de63b7a976..a849d964eaed 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php @@ -24,6 +24,7 @@ * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @api * @since 100.0.2 */ @@ -453,6 +454,10 @@ public function getConfigurableAttributes($product) ['group' => 'CONFIGURABLE', 'method' => __METHOD__] ); if (!$product->hasData($this->_configurableAttributes)) { + // for new product do not load configurable attributes + if (!$product->getId()) { + return []; + } $configurableAttributes = $this->getConfigurableAttributeCollection($product); $this->extensionAttributesJoinProcessor->process($configurableAttributes); $configurableAttributes->orderByPosition()->load(); @@ -1227,6 +1232,8 @@ public function isPossibleBuyFromList($product) /** * Returns array of sub-products for specified configurable product * + * $requiredAttributeIds - one dimensional array, if provided + * * Result array contains all children for specified configurable product * * @param \Magento\Catalog\Model\Product $product @@ -1240,9 +1247,12 @@ public function getUsedProducts($product, $requiredAttributeIds = null) __METHOD__, $product->getData($metadata->getLinkField()), $product->getStoreId(), - $this->getCustomerSession()->getCustomerGroupId(), - $requiredAttributeIds + $this->getCustomerSession()->getCustomerGroupId() ]; + if ($requiredAttributeIds !== null) { + sort($requiredAttributeIds); + $keyParts[] = implode('', $requiredAttributeIds); + } $cacheKey = $this->getUsedProductsCacheKey($keyParts); return $this->loadUsedProducts($product, $cacheKey); } @@ -1381,7 +1391,7 @@ function ($item) { */ private function getUsedProductsCacheKey($keyParts) { - return md5(implode('_', $keyParts)); + return sha1(implode('_', $keyParts)); } /** @@ -1398,23 +1408,47 @@ private function getConfiguredUsedProductCollection( $skipStockFilter = true ) { $collection = $this->getUsedProductCollection($product); + if ($skipStockFilter) { $collection->setFlag('has_stock_status_filter', true); } + $collection - ->addAttributeToSelect($this->getCatalogConfig()->getProductAttributes()) + ->addAttributeToSelect($this->getAttributesForCollection($product)) ->addFilterByRequiredOptions() ->setStoreId($product->getStoreId()); - $requiredAttributes = ['name', 'price', 'weight', 'image', 'thumbnail', 'status', 'media_gallery']; - foreach ($requiredAttributes as $attributeCode) { - $collection->addAttributeToSelect($attributeCode); - } - foreach ($this->getUsedProductAttributes($product) as $usedProductAttribute) { - $collection->addAttributeToSelect($usedProductAttribute->getAttributeCode()); - } $collection->addMediaGalleryData(); $collection->addTierPriceData(); + return $collection; } + + /** + * @return array + */ + private function getAttributesForCollection(\Magento\Catalog\Model\Product $product) + { + $productAttributes = $this->getCatalogConfig()->getProductAttributes(); + + $requiredAttributes = [ + 'name', + 'price', + 'weight', + 'image', + 'thumbnail', + 'status', + 'visibility', + 'media_gallery' + ]; + + $usedAttributes = array_map( + function($attr) { + return $attr->getAttributeCode(); + }, + $this->getUsedProductAttributes($product) + ); + + return array_unique(array_merge($productAttributes, $requiredAttributes, $usedAttributes)); + } } diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Attribute.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Attribute.php index 7306942c3c49..4ead9ffe0fe7 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Attribute.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Attribute.php @@ -12,6 +12,8 @@ use Magento\Framework\EntityManager\MetadataPool; /** + * Configurable product attribute model. + * * @method Attribute setProductAttribute(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute $value) * @method \Magento\Eav\Model\Entity\Attribute\AbstractAttribute getProductAttribute() */ @@ -86,7 +88,7 @@ public function getOptions() } /** - * {@inheritdoc} + * @inheritdoc */ public function getLabel() { @@ -112,10 +114,10 @@ public function afterSave() } /** - * Load configurable attribute by product and product's attribute + * Load configurable attribute by product and product's attribute. * * @param \Magento\Catalog\Model\Product $product - * @param \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute + * @param \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute * @return void */ public function loadByProductAndAttribute($product, $attribute) @@ -144,7 +146,7 @@ public function deleteByProduct($product) } /** - * {@inheritdoc} + * @inheritdoc * @codeCoverageIgnore */ public function getAttributeId() @@ -153,7 +155,7 @@ public function getAttributeId() } /** - * {@inheritdoc} + * @inheritdoc * @codeCoverageIgnore */ public function getPosition() @@ -162,7 +164,7 @@ public function getPosition() } /** - * {@inheritdoc} + * @inheritdoc * @codeCoverageIgnore */ public function getIsUseDefault() @@ -171,7 +173,7 @@ public function getIsUseDefault() } /** - * {@inheritdoc} + * @inheritdoc * @codeCoverageIgnore */ public function getValues() @@ -182,8 +184,7 @@ public function getValues() //@codeCoverageIgnoreStart /** - * @param string $attributeId - * @return $this + * @inheritdoc */ public function setAttributeId($attributeId) { @@ -191,8 +192,7 @@ public function setAttributeId($attributeId) } /** - * @param string $label - * @return $this + * @inheritdoc */ public function setLabel($label) { @@ -200,8 +200,7 @@ public function setLabel($label) } /** - * @param int $position - * @return $this + * @inheritdoc */ public function setPosition($position) { @@ -209,8 +208,7 @@ public function setPosition($position) } /** - * @param bool $isUseDefault - * @return $this + * @inheritdoc */ public function setIsUseDefault($isUseDefault) { @@ -218,8 +216,7 @@ public function setIsUseDefault($isUseDefault) } /** - * @param \Magento\ConfigurableProduct\Api\Data\OptionValueInterface[] $values - * @return $this + * @inheritdoc */ public function setValues(array $values = null) { @@ -227,7 +224,7 @@ public function setValues(array $values = null) } /** - * {@inheritdoc} + * @inheritdoc * * @return \Magento\ConfigurableProduct\Api\Data\OptionExtensionInterface|null */ @@ -237,7 +234,7 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * @inheritdoc * * @param \Magento\ConfigurableProduct\Api\Data\OptionExtensionInterface $extensionAttributes * @return $this @@ -249,7 +246,7 @@ public function setExtensionAttributes( } /** - * {@inheritdoc} + * @inheritdoc */ public function getProductId() { @@ -257,7 +254,7 @@ public function getProductId() } /** - * {@inheritdoc} + * @inheritdoc */ public function setProductId($value) { @@ -268,9 +265,14 @@ public function setProductId($value) /** * @inheritdoc + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + return array_diff( parent::__sleep(), ['metadataPool'] @@ -279,9 +281,14 @@ public function __sleep() /** * @inheritdoc + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + parent::__wakeup(); $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); $this->metadataPool = $objectManager->get(MetadataPool::class); 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 bee334596e99..f2bf3116af9e 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Price.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Price.php @@ -7,14 +7,15 @@ */ namespace Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Catalog\Model\Product; + +/** + * Class Price for configurable product + */ class Price extends \Magento\Catalog\Model\Product\Type\Price { /** - * Get product final price - * - * @param float $qty - * @param \Magento\Catalog\Model\Product $product - * @return float + * @inheritdoc */ public function getFinalPrice($qty, $product) { @@ -22,7 +23,10 @@ public function getFinalPrice($qty, $product) return $product->getCalculatedFinalPrice(); } if ($product->getCustomOption('simple_product') && $product->getCustomOption('simple_product')->getProduct()) { - $finalPrice = parent::getFinalPrice($qty, $product->getCustomOption('simple_product')->getProduct()); + /** @var Product $simpleProduct */ + $simpleProduct = $product->getCustomOption('simple_product')->getProduct(); + $simpleProduct->setCustomerGroupId($product->getCustomerGroupId()); + $finalPrice = parent::getFinalPrice($qty, $simpleProduct); } else { $priceInfo = $product->getPriceInfo(); $finalPrice = $priceInfo->getPrice('final_price')->getAmount()->getValue(); @@ -35,7 +39,7 @@ public function getFinalPrice($qty, $product) } /** - * {@inheritdoc} + * @inheritdoc */ public function getPrice($product) { @@ -48,6 +52,7 @@ public function getPrice($product) } } } + return 0; } } diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php b/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php index 73e7f9053fa4..1bd8ef59f0d6 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php @@ -203,7 +203,9 @@ protected function fillSimpleProductData( $postData['stock_data'] = array_diff_key((array)$parentProduct->getStockData(), array_flip($keysFilter)); if (!isset($postData['stock_data']['is_in_stock'])) { $stockStatus = $parentProduct->getQuantityAndStockStatus(); - $postData['stock_data']['is_in_stock'] = $stockStatus['is_in_stock']; + if (isset($stockStatus['is_in_stock'])) { + $postData['stock_data']['is_in_stock'] = $stockStatus['is_in_stock']; + } } $postData = $this->processMediaGallery($product, $postData); $postData['status'] = isset($postData['status']) @@ -262,6 +264,8 @@ public function duplicateImagesForVariations($productsData) } /** + * Process media gallery for product + * * @param \Magento\Catalog\Model\Product $product * @param array $productData * diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Attribute/OptionSelectBuilder.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Attribute/OptionSelectBuilder.php index 5d9eed0a188f..8fbab8142fec 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Attribute/OptionSelectBuilder.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Attribute/OptionSelectBuilder.php @@ -40,7 +40,7 @@ public function __construct(Attribute $attributeResource, OptionProvider $attrib } /** - * {@inheritdoc} + * @inheritdoc */ public function getSelect(AbstractAttribute $superAttribute, int $productId, ScopeInterface $scope) { @@ -91,7 +91,7 @@ public function getSelect(AbstractAttribute $superAttribute, int $productId, Sco ] ), [] - )->joinInner( + )->joinLeft( ['attribute_option' => $this->attributeResource->getTable('eav_attribute_option')], 'attribute_option.option_id = entity_value.value', [] diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php index 81e2e99bfe93..b7bbf7aa1871 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php @@ -12,6 +12,11 @@ use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Query\BaseFinalPrice; use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\IndexTableStructureFactory; use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\IndexTableStructure; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Framework\App\ObjectManager; +use Magento\CatalogInventory\Model\Stock; +use Magento\CatalogInventory\Model\Configuration; /** * Configurable Products Price Indexer Resource model @@ -65,6 +70,11 @@ class Configurable implements DimensionalIndexerInterface */ private $basePriceModifier; + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + /** * @param BaseFinalPrice $baseFinalPrice * @param IndexTableStructureFactory $indexTableStructureFactory @@ -74,6 +84,7 @@ class Configurable implements DimensionalIndexerInterface * @param BasePriceModifier $basePriceModifier * @param bool $fullReindexAction * @param string $connectionName + * @param ScopeConfigInterface $scopeConfig */ public function __construct( BaseFinalPrice $baseFinalPrice, @@ -83,7 +94,8 @@ public function __construct( \Magento\Framework\App\ResourceConnection $resource, BasePriceModifier $basePriceModifier, $fullReindexAction = false, - $connectionName = 'indexer' + $connectionName = 'indexer', + ScopeConfigInterface $scopeConfig = null ) { $this->baseFinalPrice = $baseFinalPrice; $this->indexTableStructureFactory = $indexTableStructureFactory; @@ -93,10 +105,11 @@ public function __construct( $this->resource = $resource; $this->fullReindexAction = $fullReindexAction; $this->basePriceModifier = $basePriceModifier; + $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); } /** - * {@inheritdoc} + * @inheritdoc * * @throws \Exception */ @@ -184,7 +197,19 @@ private function fillTemporaryOptionsTable(string $temporaryOptionsTableName, ar ['le' => $this->getTable('catalog_product_entity')], 'le.' . $linkField . ' = l.parent_id', [] - )->columns( + ); + + // Does not make sense to extend query if out of stock products won't appear in tables for indexing + if ($this->isConfigShowOutOfStock()) { + $select->join( + ['si' => $this->getTable('cataloginventory_stock_item')], + 'si.product_id = l.product_id', + [] + ); + $select->where('si.is_in_stock = ?', Stock::STOCK_IN_STOCK); + } + + $select->columns( [ 'le.entity_id', 'customer_group_id', @@ -250,7 +275,7 @@ private function getMainTable($dimensions) /** * Get connection * - * return \Magento\Framework\DB\Adapter\AdapterInterface + * @return \Magento\Framework\DB\Adapter\AdapterInterface * @throws \DomainException */ private function getConnection(): \Magento\Framework\DB\Adapter\AdapterInterface @@ -272,4 +297,17 @@ private function getTable($tableName) { return $this->resource->getTableName($tableName, $this->connectionName); } + + /** + * Is flag Show Out Of Stock setted + * + * @return bool + */ + private function isConfigShowOutOfStock(): bool + { + return $this->scopeConfig->isSetFlag( + Configuration::XML_PATH_SHOW_OUT_OF_STOCK, + ScopeInterface::SCOPE_STORE + ); + } } diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable.php index ccff85dd9717..feffd22a0fb3 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable.php @@ -19,6 +19,9 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\DB\Adapter\AdapterInterface; +/** + * Configurable product resource model. + */ class Configurable extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb { /** @@ -173,10 +176,13 @@ public function getChildrenIds($parentId, $required = true) $parentId ); - $childrenIds = [0 => []]; - foreach ($this->getConnection()->fetchAll($select) as $row) { - $childrenIds[0][$row['product_id']] = $row['product_id']; - } + $childrenIds = [ + 0 => array_column( + $this->getConnection()->fetchAll($select), + 'product_id', + 'product_id' + ) + ]; return $childrenIds; } diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Attribute/Collection.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Attribute/Collection.php index 3c40d326be77..81cbbd06c523 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Attribute/Collection.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Attribute/Collection.php @@ -18,6 +18,8 @@ use Magento\Catalog\Api\Data\ProductInterface; /** + * Collection of configurable product attributes. + * * @api * @SuppressWarnings(PHPMD.LongVariable) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -302,6 +304,8 @@ protected function _loadLabels() } /** + * Load related options' data. + * * @return void */ protected function loadOptions() @@ -354,9 +358,14 @@ protected function getIncludedOptions(array $usedProducts, AbstractAttribute $pr /** * @inheritdoc * @since 100.0.6 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + return array_diff( parent::__sleep(), [ @@ -373,9 +382,14 @@ public function __sleep() /** * @inheritdoc * @since 100.0.6 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + parent::__wakeup(); $objectManager = ObjectManager::getInstance(); $this->_storeManager = $objectManager->get(\Magento\Store\Model\StoreManagerInterface::class); diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Product/Collection.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Product/Collection.php index 1460cc516811..3124a3b8cf0e 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Product/Collection.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Product/Collection.php @@ -41,6 +41,7 @@ protected function _construct() /** * Init select + * * @return $this|\Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\Collection */ protected function _initSelect() @@ -73,9 +74,9 @@ public function setProductFilter($product) * * @return $this */ - protected function _beforeLoad() + protected function _renderFilters() { - parent::_beforeLoad(); + parent::_renderFilters(); $metadata = $this->getProductEntityMetadata(); $parentIds = []; foreach ($this->products as $product) { @@ -88,8 +89,7 @@ protected function _beforeLoad() } /** - * Retrieve is flat enabled flag - * Return always false if magento run admin + * Retrieve is flat enabled flag. Return always false if magento run admin * * @return bool */ diff --git a/app/code/Magento/ConfigurableProduct/Plugin/Catalog/Model/Product/Pricing/Renderer/SalableResolver.php b/app/code/Magento/ConfigurableProduct/Plugin/Catalog/Model/Product/Pricing/Renderer/SalableResolver.php index efddb278df36..c828c0929b40 100644 --- a/app/code/Magento/ConfigurableProduct/Plugin/Catalog/Model/Product/Pricing/Renderer/SalableResolver.php +++ b/app/code/Magento/ConfigurableProduct/Plugin/Catalog/Model/Product/Pricing/Renderer/SalableResolver.php @@ -5,7 +5,7 @@ */ namespace Magento\ConfigurableProduct\Plugin\Catalog\Model\Product\Pricing\Renderer; -use Magento\ConfigurableProduct\Pricing\Price\LowestPriceOptionsProviderInterface; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable as TypeConfigurable; /** * A plugin for a salable resolver. @@ -13,29 +13,25 @@ class SalableResolver { /** - * @var LowestPriceOptionsProviderInterface + * @var TypeConfigurable */ - private $lowestPriceOptionsProvider; + private $typeConfigurable; /** - * @param LowestPriceOptionsProviderInterface $lowestPriceOptionsProvider + * @param TypeConfigurable $typeConfigurable */ - public function __construct( - LowestPriceOptionsProviderInterface $lowestPriceOptionsProvider - ) { - $this->lowestPriceOptionsProvider = $lowestPriceOptionsProvider; + public function __construct(TypeConfigurable $typeConfigurable) + { + $this->typeConfigurable = $typeConfigurable; } /** - * Performs an additional check whether given configurable product has - * at least one configuration in-stock. + * Performs an additional check whether given configurable product has at least one configuration in-stock. * * @param \Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolver $subject * @param bool $result * @param \Magento\Framework\Pricing\SaleableInterface $salableItem - * * @return bool - * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function afterIsSalable( @@ -43,10 +39,8 @@ public function afterIsSalable( $result, \Magento\Framework\Pricing\SaleableInterface $salableItem ) { - if ($salableItem->getTypeId() == 'configurable' && $result) { - if (!$this->lowestPriceOptionsProvider->getProducts($salableItem)) { - $result = false; - } + if ($salableItem->getTypeId() === TypeConfigurable::TYPE_CODE && $result) { + $result = $this->typeConfigurable->isSalable($salableItem); } return $result; diff --git a/app/code/Magento/ConfigurableProduct/Plugin/SalesRule/Model/Rule/Condition/Product.php b/app/code/Magento/ConfigurableProduct/Plugin/SalesRule/Model/Rule/Condition/Product.php new file mode 100644 index 000000000000..1ed4432347b7 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Plugin/SalesRule/Model/Rule/Condition/Product.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Plugin\SalesRule\Model\Rule\Condition; + +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; + +/** + * Class Product + * + * @package Magento\ConfigurableProduct\Plugin\SalesRule\Model\Rule\Condition + */ +class Product +{ + /** + * Prepare configurable product for validation. + * + * @param \Magento\SalesRule\Model\Rule\Condition\Product $subject + * @param \Magento\Framework\Model\AbstractModel $model + * @return array + */ + public function beforeValidate( + \Magento\SalesRule\Model\Rule\Condition\Product $subject, + \Magento\Framework\Model\AbstractModel $model + ) { + $product = $this->getProductToValidate($subject, $model); + if ($model->getProduct() !== $product) { + // We need to replace product only for validation and keep original product for all other cases. + $clone = clone $model; + $clone->setProduct($product); + $model = $clone; + } + + return [$model]; + } + + /** + * Select proper product for validation. + * + * @param \Magento\SalesRule\Model\Rule\Condition\Product $subject + * @param \Magento\Framework\Model\AbstractModel $model + * + * @return \Magento\Catalog\Api\Data\ProductInterface|\Magento\Catalog\Model\Product + */ + private function getProductToValidate( + \Magento\SalesRule\Model\Rule\Condition\Product $subject, + \Magento\Framework\Model\AbstractModel $model + ) { + /** @var \Magento\Catalog\Model\Product $product */ + $product = $model->getProduct(); + + $attrCode = $subject->getAttribute(); + + /* Check for attributes which are not available for configurable products */ + if ($product->getTypeId() == Configurable::TYPE_CODE && !$product->hasData($attrCode)) { + /** @var \Magento\Catalog\Model\AbstractModel $childProduct */ + $childProduct = current($model->getChildren())->getProduct(); + if ($childProduct->hasData($attrCode)) { + $product = $childProduct; + } + } + + return $product; + } +} diff --git a/app/code/Magento/ConfigurableProduct/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php b/app/code/Magento/ConfigurableProduct/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php new file mode 100644 index 000000000000..8bdde2aeb0cf --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Plugin\Tax\Model\Sales\Total\Quote; + +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Quote\Model\Quote\Item\AbstractItem; +use Magento\Tax\Api\Data\QuoteDetailsItemInterface; +use Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory; + +/** + * Plugin for CommonTaxCollector to apply Tax Class ID from child item for configurable product + */ +class CommonTaxCollector +{ + /** + * Apply Tax Class ID from child item for configurable product + * + * @param \Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector $subject + * @param QuoteDetailsItemInterface $result + * @param QuoteDetailsItemInterfaceFactory $itemDataObjectFactory + * @param AbstractItem $item + * @return QuoteDetailsItemInterface + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterMapItem( + \Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector $subject, + QuoteDetailsItemInterface $result, + QuoteDetailsItemInterfaceFactory $itemDataObjectFactory, + AbstractItem $item + ) : QuoteDetailsItemInterface { + if ($item->getProduct()->getTypeId() === Configurable::TYPE_CODE && $item->getHasChildren()) { + $childItem = $item->getChildren()[0]; + $result->getTaxClassKey()->setValue($childItem->getProduct()->getTaxClassId()); + } + + return $result; + } +} diff --git a/app/code/Magento/ConfigurableProduct/Pricing/Render/TierPriceBox.php b/app/code/Magento/ConfigurableProduct/Pricing/Render/TierPriceBox.php index 611523a60b06..447ba16d7271 100644 --- a/app/code/Magento/ConfigurableProduct/Pricing/Render/TierPriceBox.php +++ b/app/code/Magento/ConfigurableProduct/Pricing/Render/TierPriceBox.php @@ -5,6 +5,8 @@ */ namespace Magento\ConfigurableProduct\Pricing\Render; +use Magento\Catalog\Pricing\Price\TierPrice; + /** * Responsible for displaying tier price box on configurable product page. * @@ -17,9 +19,27 @@ class TierPriceBox extends FinalPriceBox */ public function toHtml() { - // Hide tier price block in case of MSRP. - if (!$this->isMsrpPriceApplicable()) { + // Hide tier price block in case of MSRP or in case when no options with tier price. + if (!$this->isMsrpPriceApplicable() && $this->isTierPriceApplicable()) { return parent::toHtml(); } } + + /** + * Check if at least one of simple products has tier price. + * + * @return bool + */ + private function isTierPriceApplicable() + { + $product = $this->getSaleableItem(); + foreach ($product->getTypeInstance()->getUsedProducts($product) as $simpleProduct) { + if ($simpleProduct->isSalable() && + !empty($simpleProduct->getPriceInfo()->getPrice(TierPrice::PRICE_CODE)->getTierPriceList()) + ) { + return true; + } + } + return false; + } } diff --git a/app/code/Magento/ConfigurableProduct/Setup/Patch/Data/InstallInitialConfigurableAttributes.php b/app/code/Magento/ConfigurableProduct/Setup/Patch/Data/InstallInitialConfigurableAttributes.php index f69d8529fb80..c6b173453f5e 100644 --- a/app/code/Magento/ConfigurableProduct/Setup/Patch/Data/InstallInitialConfigurableAttributes.php +++ b/app/code/Magento/ConfigurableProduct/Setup/Patch/Data/InstallInitialConfigurableAttributes.php @@ -6,16 +6,16 @@ namespace Magento\ConfigurableProduct\Setup\Patch\Data; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; use Magento\Eav\Setup\EavSetup; use Magento\Eav\Setup\EavSetupFactory; -use Magento\Framework\App\ResourceConnection; use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; -use Magento\ConfigurableProduct\Model\Product\Type\Configurable; /** * Class InstallInitialConfigurableAttributes + * * @package Magento\ConfigurableProduct\Setup\Patch */ class InstallInitialConfigurableAttributes implements DataPatchInterface, PatchVersionInterface @@ -24,6 +24,7 @@ class InstallInitialConfigurableAttributes implements DataPatchInterface, PatchV * @var ModuleDataSetupInterface */ private $moduleDataSetup; + /** * @var EavSetupFactory */ @@ -43,7 +44,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { @@ -64,24 +65,27 @@ public function apply() 'color' ]; foreach ($attributes as $attributeCode) { - $relatedProductTypes = explode( - ',', - $eavSetup->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attributeCode, 'apply_to') - ); - if (!in_array(Configurable::TYPE_CODE, $relatedProductTypes)) { - $relatedProductTypes[] = Configurable::TYPE_CODE; - $eavSetup->updateAttribute( - \Magento\Catalog\Model\Product::ENTITY, - $attributeCode, - 'apply_to', - implode(',', $relatedProductTypes) + $attribute = $eavSetup->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attributeCode, 'apply_to'); + if ($attribute) { + $relatedProductTypes = explode( + ',', + $attribute ); + if (!in_array(Configurable::TYPE_CODE, $relatedProductTypes)) { + $relatedProductTypes[] = Configurable::TYPE_CODE; + $eavSetup->updateAttribute( + \Magento\Catalog\Model\Product::ENTITY, + $attributeCode, + 'apply_to', + implode(',', $relatedProductTypes) + ); + } } } } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -89,7 +93,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -97,7 +101,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/ConfigurableProduct/Setup/Patch/Data/UpdateManufacturerAttribute.php b/app/code/Magento/ConfigurableProduct/Setup/Patch/Data/UpdateManufacturerAttribute.php index c9a2f7d373a6..1e085e0fdb38 100644 --- a/app/code/Magento/ConfigurableProduct/Setup/Patch/Data/UpdateManufacturerAttribute.php +++ b/app/code/Magento/ConfigurableProduct/Setup/Patch/Data/UpdateManufacturerAttribute.php @@ -15,8 +15,7 @@ use Magento\ConfigurableProduct\Model\Product\Type\Configurable; /** - * Class UpdateManufacturerAttribute - * @package Magento\ConfigurableProduct\Setup\Patch + * Update manufacturer attribute if it's presented in system. */ class UpdateManufacturerAttribute implements DataPatchInterface, PatchVersionInterface { @@ -31,7 +30,6 @@ class UpdateManufacturerAttribute implements DataPatchInterface, PatchVersionInt private $eavSetupFactory; /** - * UpdateTierPriceAttribute constructor. * @param ModuleDataSetupInterface $moduleDataSetup * @param EavSetupFactory $eavSetupFactory */ @@ -44,30 +42,37 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { /** @var EavSetup $eavSetup */ $eavSetup = $this->eavSetupFactory->create(['setup' => $this->moduleDataSetup]); - $relatedProductTypes = explode( - ',', - $eavSetup->getAttribute(\Magento\Catalog\Model\Product::ENTITY, 'manufacturer', 'apply_to') - ); - if (!in_array(Configurable::TYPE_CODE, $relatedProductTypes)) { - $relatedProductTypes[] = Configurable::TYPE_CODE; - $eavSetup->updateAttribute( - \Magento\Catalog\Model\Product::ENTITY, - 'manufacturer', - 'apply_to', - implode(',', $relatedProductTypes) + if ($manufacturer = $eavSetup->getAttribute( + \Magento\Catalog\Model\Product::ENTITY, + 'manufacturer', + 'apply_to' + )) { + $relatedProductTypes = explode( + ',', + $manufacturer ); + + if (!in_array(Configurable::TYPE_CODE, $relatedProductTypes)) { + $relatedProductTypes[] = Configurable::TYPE_CODE; + $eavSetup->updateAttribute( + \Magento\Catalog\Model\Product::ENTITY, + 'manufacturer', + 'apply_to', + implode(',', $relatedProductTypes) + ); + } } } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -77,7 +82,7 @@ public static function getDependencies() } /** - * {@inheritdoc}\ + * @inheritdoc */ public static function getVersion() { @@ -85,7 +90,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminAddOptionsToAttributeActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminAddOptionsToAttributeActionGroup.xml new file mode 100644 index 000000000000..4328159d6e93 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminAddOptionsToAttributeActionGroup.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="addOptionsToAttributeActionGroup"> + <arguments> + <argument name="option1" defaultValue="colorProductAttribute2"/> + <argument name="option2" defaultValue="colorDefaultProductAttribute1"/> + <argument name="option3" defaultValue="colorProductAttribute3"/> + <argument name="option4" defaultValue="colorProductAttribute1"/> + <argument name="option5" defaultValue="colorDefaultProductAttribute2"/> + </arguments> + <!--Add option 1 to attribute--> + <click selector="{{AdminNewAttributePanel.addOption}}" stepKey="clickAddOption1"/> + <waitForElementVisible selector="{{AdminNewAttributePanel.isDefault('1')}}" time="30" stepKey="waitForOptionRow1" after="clickAddOption1"/> + <fillField selector="{{AdminNewAttributePanel.optionAdminValue('0')}}" userInput="{{option1.name}}" stepKey="fillAdminLabel1" after="waitForOptionRow1"/> + <!--Add option 2 to attribute--> + <click selector="{{AdminNewAttributePanel.addOption}}" stepKey="clickAddOption2" after="fillAdminLabel1"/> + <waitForElementVisible selector="{{AdminNewAttributePanel.isDefault('2')}}" time="30" stepKey="waitForOptionRow2" after="clickAddOption2"/> + <fillField selector="{{AdminNewAttributePanel.optionAdminValue('1')}}" userInput="{{option2.name}}" stepKey="fillAdminLabel2" after="waitForOptionRow2"/> + <!--Add option 3 to attribute--> + <click selector="{{AdminNewAttributePanel.addOption}}" stepKey="clickAddOption3" after="fillAdminLabel2"/> + <waitForElementVisible selector="{{AdminNewAttributePanel.isDefault('3')}}" time="30" stepKey="waitForOptionRow3" after="clickAddOption3"/> + <fillField selector="{{AdminNewAttributePanel.optionAdminValue('2')}}" userInput="{{option3.name}}" stepKey="fillAdminLabel3" after="waitForOptionRow3"/> + <!--Add option 4 to attribute--> + <click selector="{{AdminNewAttributePanel.addOption}}" stepKey="clickAddOption4" after="fillAdminLabel3"/> + <waitForElementVisible selector="{{AdminNewAttributePanel.isDefault('4')}}" time="30" stepKey="waitForOptionRow4" after="clickAddOption4"/> + <fillField selector="{{AdminNewAttributePanel.optionAdminValue('3')}}" userInput="{{option4.name}}" stepKey="fillAdminLabel4" after="waitForOptionRow4"/> + <!--Add option 5 to attribute--> + <click selector="{{AdminNewAttributePanel.addOption}}" stepKey="clickAddOption5" after="fillAdminLabel4"/> + <waitForElementVisible selector="{{AdminNewAttributePanel.isDefault('5')}}" time="30" stepKey="waitForOptionRow5" after="clickAddOption5"/> + <fillField selector="{{AdminNewAttributePanel.optionAdminValue('4')}}" userInput="{{option5.name}}" stepKey="fillAdminLabel5" after="waitForOptionRow5"/> + <!--Save attribute--> + <click selector="{{AdminNewAttributePanel.saveAttribute}}" stepKey="clickSaveAttribute" after="fillAdminLabel5"/> + <waitForPageLoad stepKey="waitForSavingAttribute"/> + <see userInput="You saved the product attribute." stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml index 9ebadc236bb3..5a172ca5eabd 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminConfigurableProductActionGroup.xml @@ -105,7 +105,7 @@ <seeInTitle userInput="{{product.name}}" stepKey="seeProductNameInTitle"/> </actionGroup> - <actionGroup name="createConfigurationsForAttribute"> + <actionGroup name="generateConfigurationsByAttributeCode"> <arguments> <argument name="attributeCode" type="string" defaultValue="SomeString"/> </arguments> @@ -121,7 +121,185 @@ <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="99" stepKey="enterAttributeQuantity"/> <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton3"/> <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton4"/> + </actionGroup> + + <actionGroup name="createConfigurationsForAttribute" extends="generateConfigurationsByAttributeCode"> + <arguments> + <argument name="attributeCode" type="string" defaultValue="SomeString"/> + </arguments> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton2"/> + <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmInPopup"/> + </actionGroup> + + <actionGroup name="createConfigurationsForAttributeWithImages" extends="generateConfigurationsByAttributeCode"> + <arguments> + <argument name="attributeCode" type="string" defaultValue="SomeString"/> + <argument name="image" defaultValue="ProductImage"/> + </arguments> + + <click selector="{{AdminCreateProductConfigurationsPanel.applySingleSetOfImages}}" stepKey="clickOnApplySingleImageSetToAllSku" after="enterAttributeQuantity"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.imageUploadButton}}" stepKey="seeImageSectionIsReady" after="clickOnApplySingleImageSetToAllSku"/> + <attachFile selector="{{AdminCreateProductConfigurationsPanel.imageFileUpload}}" userInput="{{image.file}}" stepKey="uploadFile" after="seeImageSectionIsReady"/> + <waitForElementNotVisible selector="{{AdminCreateProductConfigurationsPanel.uploadProgressBar}}" stepKey="waitForUpload" after="uploadFile"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.imageFile(image.fileName)}}" stepKey="waitForThumbnail" after="waitForUpload"/> + + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton2" after="clickOnNextButton4"/> + <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmInPopup" after="clickOnSaveButton2"/> + </actionGroup> + + <actionGroup name="createConfigurationsForTwoAttribute" extends="generateConfigurationsByAttributeCode"> + <arguments> + <argument name="secondAttributeCode" type="string"/> + </arguments> + <remove keyForRemoval="clickOnSelectAll"/> + <remove keyForRemoval="clickFilters"/> + <remove keyForRemoval="fillFilterAttributeCodeField"/> + <remove keyForRemoval="clickApplyFiltersButton"/> + <remove keyForRemoval="clickOnFirstCheckbox"/> + + <click selector="{{AdminCreateProductConfigurationsPanel.attributeCheckbox(attributeCode)}}" stepKey="clickOnFirstAttributeCheckbox" after="clickCreateConfigurations"/> + <click selector="{{AdminCreateProductConfigurationsPanel.attributeCheckbox(secondAttributeCode)}}" stepKey="clickOnSecondAttributeCheckbox" after="clickOnFirstAttributeCheckbox"/> + <grabTextFrom selector="{{AdminCreateProductConfigurationsPanel.defaultLabel(attributeCode)}}" stepKey="grabFirstAttributeDefaultLabel" after="clickOnSecondAttributeCheckbox"/> + <grabTextFrom selector="{{AdminCreateProductConfigurationsPanel.defaultLabel(secondAttributeCode)}}" stepKey="grabSecondAttributeDefaultLabel" after="grabFirstAttributeDefaultLabel"/> + <click selector="{{AdminCreateProductConfigurationsPanel.selectAllByAttribute({$grabFirstAttributeDefaultLabel})}}" stepKey="clickOnSelectAllForFirstAttribute" after="clickOnNextButton1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.selectAllByAttribute({$grabSecondAttributeDefaultLabel})}}" stepKey="clickOnSelectAllForSecondAttribute" after="clickOnSelectAllForFirstAttribute"/> <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton2"/> <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmInPopup"/> </actionGroup> + + <actionGroup name="saveConfiguredProduct"> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton2"/> + <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmInPopup"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessage"/> + </actionGroup> + + <actionGroup name="addNewProductConfigurationAttribute"> + <arguments> + <argument name="attribute" type="entity"/> + <argument name="firstOption" type="entity"/> + <argument name="secondOption" type="entity"/> + </arguments> + <!-- Create new attribute --> + <click selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}" stepKey="clickOnNewAttribute"/> + <waitForPageLoad stepKey="waitForIFrame"/> + <switchToIFrame selector="{{AdminNewAttributePanel.newAttributeIFrame}}" stepKey="switchToNewAttributeIFrame"/> + <fillField selector="{{AdminNewAttributePanel.defaultLabel}}" userInput="{{attribute.default_label}}" stepKey="fillDefaultLabel"/> + <click selector="{{AdminNewAttributePanel.saveAttribute}}" stepKey="clickOnNewAttributePanel"/> + <waitForPageLoad stepKey="waitForSaveAttribute"/> + <switchToIFrame stepKey="switchOutOfIFrame"/> + <waitForPageLoad stepKey="waitForFilters"/> + <!-- Find created below attribute and add option; save attribute --> + <click selector="{{AdminCreateProductConfigurationsPanel.filters}}" stepKey="clickOnFilters"/> + <fillField userInput="{{attribute.default_label}}" selector="{{AdminCreateProductConfigurationsPanel.attributeCode}}" stepKey="fillFilterAttributeCodeField"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminCreateProductConfigurationsPanel.firstCheckbox}}" stepKey="clickOnFirstCheckbox"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton"/> + <click selector="{{AdminCreateProductConfigurationsPanel.createNewValue}}" stepKey="clickOnCreateFirstNewValue"/> + <fillField userInput="{{firstOption.name}}" selector="{{AdminCreateProductConfigurationsPanel.attributeName}}" stepKey="fillFieldForNewFirstOption"/> + <click selector="{{AdminCreateProductConfigurationsPanel.saveAttribute}}" stepKey="clickOnSaveNewAttribute"/> + <click selector="{{AdminCreateProductConfigurationsPanel.createNewValue}}" stepKey="clickOnCreateSecondNewValue"/> + <fillField userInput="{{secondOption.name}}" selector="{{AdminCreateProductConfigurationsPanel.attributeName}}" stepKey="fillFieldForNewSecondOption"/> + <click selector="{{AdminCreateProductConfigurationsPanel.saveAttribute}}" stepKey="clickOnSaveAttribute"/> + <click selector="{{AdminCreateProductConfigurationsPanel.selectAll}}" stepKey="clickOnSelectAll"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnSecondNextButton"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnThirdNextButton"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnFourthNextButton"/> + </actionGroup> + + <actionGroup name="changeProductConfigurationsInGrid"> + <arguments> + <argument name="firstOption" type="entity"/> + <argument name="secondOption" type="entity"/> + </arguments> + <fillField userInput="{{firstOption.name}}" selector="{{AdminProductFormConfigurationsSection.confProductNameCell(firstOption.name)}}" stepKey="fillFieldNameForFirstAttributeOption"/> + <fillField userInput="{{secondOption.name}}" selector="{{AdminProductFormConfigurationsSection.confProductNameCell(secondOption.name)}}" stepKey="fillFieldNameForSecondAttributeOption"/> + <fillField userInput="{{firstOption.sku}}" selector="{{AdminProductFormConfigurationsSection.confProductSkuCell(firstOption.name)}}" stepKey="fillFieldSkuForFirstAttributeOption"/> + <fillField userInput="{{secondOption.sku}}" selector="{{AdminProductFormConfigurationsSection.confProductSkuCell(secondOption.name)}}" stepKey="fillFieldSkuForSecondAttributeOption"/> + <fillField userInput="{{firstOption.price}}" selector="{{AdminProductFormConfigurationsSection.confProductPriceCell(firstOption.name)}}" stepKey="fillFieldPriceForFirstAttributeOption"/> + <fillField userInput="{{secondOption.price}}" selector="{{AdminProductFormConfigurationsSection.confProductPriceCell(secondOption.name)}}" stepKey="fillFieldPriceForSecondAttributeOption"/> + <fillField userInput="{{firstOption.quantity}}" selector="{{AdminProductFormConfigurationsSection.confProductQuantityCell(firstOption.name)}}" stepKey="fillFieldQuantityForFirstAttributeOption"/> + <fillField userInput="{{secondOption.quantity}}" selector="{{AdminProductFormConfigurationsSection.confProductQuantityCell(secondOption.name)}}" stepKey="fillFieldQuantityForSecondAttributeOption"/> + <fillField userInput="{{firstOption.weight}}" selector="{{AdminProductFormConfigurationsSection.confProductWeightCell(firstOption.name)}}" stepKey="fillFieldWeightForFirstAttributeOption"/> + <fillField userInput="{{secondOption.weight}}" selector="{{AdminProductFormConfigurationsSection.confProductWeightCell(secondOption.name)}}" stepKey="fillFieldWeightForSecondAttributeOption"/> + </actionGroup> + + <actionGroup name="changeProductConfigurationsInGridExceptSku" extends="changeProductConfigurationsInGrid"> + <remove keyForRemoval="fillFieldSkuForFirstAttributeOption"/> + <remove keyForRemoval="fillFieldSkuForSecondAttributeOption"/> + </actionGroup> + + <actionGroup name="addProductToConfigurationsGrid"> + <arguments> + <argument name="sku" type="string"/> + <argument name="name" type="string"/> + </arguments> + <click selector="{{AdminProductFormConfigurationsSection.actionsBtnByProductName(name)}}" stepKey="clickToExpandFirstActions"/> + <click selector="{{AdminProductFormConfigurationsSection.addProduct(name)}}" stepKey="clickChooseFirstDifferentProduct"/> + <switchToIFrame stepKey="switchOutOfIFrame"/> + <waitForPageLoad stepKey="waitForFilters"/> + <click selector="{{AdminCreateProductConfigurationsPanel.filters}}" stepKey="clickFilters"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{sku}}" stepKey="fillProductSkuFilter"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + <click selector="{{AdminProductGridFilterSection.firstRowBySku(sku)}}" stepKey="clickOnFirstRow"/> + </actionGroup> + + <actionGroup name="addUniqueImageToConfigurableProductOption"> + <arguments> + <argument name="image" defaultValue="ProductImage"/> + <argument name="frontend_label" type="string"/> + <argument name="label" type="string"/> + </arguments> + <click selector="{{AdminCreateProductConfigurationsPanel.applyUniqueImagesToEachSkus}}" stepKey="clickOnApplyUniqueImagesToEachSku"/> + <selectOption userInput="{{frontend_label}}" selector="{{AdminCreateProductConfigurationsPanel.selectImagesButton}}" stepKey="selectOption"/> + <attachFile selector="{{AdminCreateProductConfigurationsPanel.uploadImagesButton(label)}}" userInput="{{image.file}}" stepKey="uploadFile"/> + <waitForElementNotVisible selector="{{AdminCreateProductConfigurationsPanel.uploadProgressBar}}" stepKey="waitForUpload"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.imageFile(image.fileName)}}" stepKey="waitForThumbnail"/> + </actionGroup> + + <actionGroup name="addUniquePriceToConfigurableProductOption"> + <arguments> + <argument name="frontend_label" type="string"/> + <argument name="label" type="string"/> + <argument name="price" type="string"/> + </arguments> + <click selector="{{AdminCreateProductConfigurationsPanel.applyUniquePricesToEachSkus}}" stepKey="clickOnApplyUniquePricesToEachSku"/> + <selectOption userInput="{{frontend_label}}" selector="{{AdminCreateProductConfigurationsPanel.selectPriceButton}}" stepKey="selectOption"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.price(label)}}" userInput="{{price}}" stepKey="enterAttributeQuantity"/> + </actionGroup> + + <actionGroup name="saveConfigurableProductWithNewAttributeSet"> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveConfigurableProduct"/> + <waitForElementVisible selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" time="30" stepKey="waitForAttributeSetConfirmation"/> + <click selector="{{AdminChooseAffectedAttributeSetPopup.addNewAttrSet}}" stepKey="clickAddNewAttributeSet"/> + <fillField selector="{{AdminChooseAffectedAttributeSetPopup.createNewAttrSetName}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="fillFieldNewAttrSetName"/> + <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickConfirmAttributeSet"/> + <see selector="You saved the product" stepKey="seeConfigurableSaveConfirmation" after="clickConfirmAttributeSet"/> + </actionGroup> + + <actionGroup name="saveConfigurableProductAddToCurrentAttributeSet"> + <waitForElementVisible selector="{{AdminProductFormActionSection.saveButton}}" stepKey="waitForSaveBtnVisible"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProductAgain"/> + <waitForElementVisible selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="waitPopUpVisible"/> + <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmPopup"/> + <seeElement selector="{{AdminMessagesSection.success}}" stepKey="seeSaveProductMessage"/> + </actionGroup> + + <actionGroup name="assertConfigurableProductOnAdminProductPage"> + <arguments> + <argument name="product" type="entity"/> + </arguments> + <seeInField userInput="{{ApiConfigurableProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="seeNameRequired"/> + <seeInField userInput="{{ApiConfigurableProduct.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="seeSkuRequired"/> + <dontSeeInField userInput="{{ApiConfigurableProduct.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="dontSeePriceRequired"/> + </actionGroup> + + <!--Click in Next Step and see Title--> + <actionGroup name="AdminConfigurableWizardMoveToNextStepActionGroup"> + <arguments> + <argument name="title" type="string"/> + </arguments> + <click selector="{{ConfigurableProductSection.nextButton}}" stepKey="clickNextButton"/> + <waitForPageLoad stepKey="waitForNextStepLoaded"/> + <see userInput="{{title}}" selector="{{AdminProductFormConfigurationsSection.stepsWizardTitle}}" stepKey="seeStepTitle"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateApiConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateApiConfigurableProductActionGroup.xml new file mode 100644 index 000000000000..5feaab40a769 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateApiConfigurableProductActionGroup.xml @@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See 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="AdminCreateApiConfigurableProductActionGroup"> + <arguments> + <argument name="productName" defaultValue="{{ApiConfigurableProductWithOutCategory.name}}" type="string"/> + </arguments> + + <!-- Create the configurable product based on the data in the /data folder --> + <createData entity="ApiConfigurableProductWithOutCategory" stepKey="createConfigProduct"> + <field key="name">{{productName}}</field> + </createData> + + <!-- Create attribute with 2 options to be used in children products --> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="addAttributeToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create the 2 children that will be a part of the configurable product --> + <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + + <!-- Assign the two products to the configurable product --> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + </actionGroup> + + <!-- Create the configurable product, children are not visible individually --> + <actionGroup name="AdminCreateApiConfigurableProductWithHiddenChildActionGroup" extends="AdminCreateApiConfigurableProductActionGroup"> + <!-- Create the 2 children that will be a part of the configurable product --> + <createData entity="ApiSimpleOneHidden" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <createData entity="ApiSimpleTwoHidden" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/ConfigurableProductAttributeNameDesignActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/ConfigurableProductAttributeNameDesignActionGroup.xml index 95533057608f..c4ad02ee1413 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/ConfigurableProductAttributeNameDesignActionGroup.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/ConfigurableProductAttributeNameDesignActionGroup.xml @@ -7,8 +7,7 @@ --> <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> - + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="GotoCatalogProductsPage"> <!--Click on Catalog item--> @@ -168,5 +167,4 @@ <waitForPageLoad stepKey="waitForAllFilterReset"/> </actionGroup> - </actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml index 39c206e365a2..e2759fc0fd23 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 0a8d8e56426b..2c3e5716d6ad 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml @@ -24,4 +24,83 @@ <see userInput="{{product.custom_attributes[description]}}" selector="{{StorefrontProductInfoMainSection.productDescription}}" stepKey="assertProductDescription"/> <see userInput="{{product.custom_attributes[short_description]}}" selector="{{StorefrontProductInfoMainSection.productShortDescription}}" stepKey="assertProductShortDescription"/> </actionGroup> -</actionGroups> \ No newline at end of file + + <!-- Check Storefront Configurable Product Option --> + <actionGroup name="VerifyOptionInProductStorefront"> + <arguments> + <argument name="attributeCode" type="string"/> + <argument name="optionName" type="string"/> + </arguments> + <seeElement selector="{{StorefrontProductInfoMainSection.attributeOptionByAttributeID(attributeCode, optionName)}}" stepKey="verifyOptionExists"/> + </actionGroup> + + <!-- Adds Single Option Configurable Product to cart--> + <actionGroup name="SelectSingleAttributeAndAddToCart"> + <arguments> + <argument name="productName" type="string"/> + <argument name="attributeCode" type="string"/> + <argument name="optionName" type="string"/> + </arguments> + <selectOption selector="{{StorefrontProductInfoMainSection.attributeSelectByAttributeID(attributeCode)}}" userInput="{{optionName}}" stepKey="selectAttribute"/> + <click stepKey="addProduct" selector="{{StorefrontProductActionSection.addToCart}}"/> + <waitForElementVisible selector="{{StorefrontQuickSearchResultsSection.messageSection}}" time="30" stepKey="waitForProductAdded"/> + <see selector="{{StorefrontQuickSearchResultsSection.messageSection}}" userInput="You added {{productName}} to your shopping cart." stepKey="seeAddedToCartMessage"/> + </actionGroup> + + <!-- Verify configurable product options in storefront product view --> + <actionGroup name="storefrontCheckConfigurableProductOptions"> + <arguments> + <argument name="product" type="entity"/> + <argument name="firstOption" type="entity"/> + <argument name="secondOption" type="entity"/> + </arguments> + <selectOption userInput="{{firstOption.name}}" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="selectOption1"/> + <see userInput="{{product.name}}" selector="{{StorefrontProductInfoMainSection.productName}}" stepKey="seeConfigurableProductName"/> + <see userInput="{{firstOption.price}}" selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="assertProductPricePresent"/> + <see userInput="{{product.sku}}" selector="{{StorefrontProductInfoMainSection.productSku}}" stepKey="seeConfigurableProductSku"/> + <see userInput="IN STOCK" selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="assertInStock"/> + <see userInput="{{colorProductAttribute.default_label}}" selector="{{StorefrontProductInfoMainSection.productAttributeTitle1}}" stepKey="seeColorAttributeName"/> + <dontSee userInput="As low as" selector="{{StorefrontProductInfoMainSection.productPriceLabel}}" stepKey="dontSeeProductPriceLabel1"/> + <selectOption userInput="{{secondOption.name}}" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="selectOption2"/> + <dontSee userInput="As low as" selector="{{StorefrontProductInfoMainSection.productPriceLabel}}" stepKey="dontSeeProductPriceLabel2"/> + <see userInput="{{secondOption.price}}" selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="seeProductPrice2"/> + </actionGroup> + + <!-- Assert option image in storefront product page --> + <actionGroup name="assertOptionImageInStorefrontProductPage"> + <arguments> + <argument name="product" type="entity"/> + <argument name="label" type="string"/> + <argument name="image" defaultValue="MagentoLogo"/> + </arguments> + <seeInCurrentUrl url="/{{product.urlKey}}.html" stepKey="checkUrl"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <selectOption userInput="{{label}}" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="selectOption1"/> + <seeElement selector="{{StorefrontProductMediaSection.imageFile(image.filename)}}" stepKey="seeFirstImage"/> + </actionGroup> + + <!-- Assert option image and price in storefront product page --> + <actionGroup name="AssertOptionImageAndPriceInStorefrontProductActionGroup"> + <arguments> + <argument name="label" type="string"/> + <argument name="image" type="string"/> + <argument name="price" type="string"/> + </arguments> + <selectOption userInput="{{label}}" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="selectOption"/> + <seeElement selector="{{StorefrontProductMediaSection.imageFile(image)}}" stepKey="seeImage"/> + <see userInput="{{price}}" selector="{{StorefrontProductInfoMainSection.price}}" stepKey="seeProductPrice"/> + </actionGroup> + + <!-- Assert configurable product with special price in storefront product page --> + <actionGroup name="assertConfigurableProductWithSpecialPriceOnStorefrontProductPage"> + <arguments> + <argument name="option" type="string"/> + <argument name="price" type="string"/> + <argument name="specialPrice" defaultValue="specialProductPrice"/> + </arguments> + <selectOption userInput="{{option}}" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="selectOptionWithSpecialPrice"/> + <see userInput="{{specialProductPrice.price}}" selector="{{StorefrontProductInfoMainSection.productSpecialPrice}}" stepKey="seeSpecialProductPrice"/> + <see userInput="Regular Price" selector="{{StorefrontProductInfoMainSection.specialProductText}}" stepKey="seeText"/> + <see userInput="{{price}}" selector="{{StorefrontProductInfoMainSection.oldProductPrice}}" stepKey="seeOldProductPrice"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontProductAttributeActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontProductAttributeActionGroup.xml new file mode 100644 index 000000000000..778082738153 --- /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 000000000000..f3b078623606 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/VerifyProductTypeOrderActionGroup.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="VerifyProductTypeOrder"> + <seeElement stepKey="seeConfigurableInOrder" selector="{{AdminProductDropdownOrderSection.configurableProduct}}"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductAttributeNameDesignData.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductAttributeNameDesignData.xml index 73a668fd2fef..0018f5996c9b 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductAttributeNameDesignData.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductAttributeNameDesignData.xml @@ -7,7 +7,7 @@ --> <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> <entity name="NewProductsData" type="user"> <data key="productName" unique="prefix">Shoes</data> <data key="price">60</data> @@ -31,5 +31,4 @@ <data key="configurableProduct">configurable</data> <data key="errorMessage">element.disabled is not a function</data> </entity> - </entities> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductData.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductData.xml index 286c11ad5f30..e7d9d61491a3 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductData.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductData.xml @@ -29,11 +29,24 @@ <data key="visibility">4</data> <data key="name" unique="suffix">API Configurable Product</data> <data key="urlKey" unique="suffix">api-configurable-product</data> + <data key="price">123.00</data> + <data key="weight">2</data> <data key="status">1</data> <data key="quantity">100</data> <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> </entity> + <entity name="ApiConfigurableProductWithOutCategory" type="product"> + <data key="sku" unique="suffix">api-configurable-product-with-out-category</data> + <data key="type_id">configurable</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">API Configurable Product</data> + <data key="urlKey" unique="suffix">api-configurable-product</data> + <data key="status">1</data> + <data key="quantity">100</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + </entity> <entity name="ApiConfigurableProductWithDescription" type="product"> <data key="sku" unique="suffix">api-configurable-product</data> <data key="type_id">configurable</data> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductOptionData.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductOptionData.xml index 7555337db8e0..a1a499f33eda 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductOptionData.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductOptionData.xml @@ -8,10 +8,22 @@ <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="ConfigurableProductOneOption" type="ConfigurableProductOption"> + <var key="attribute_id" entityKey="attribute_id" entityType="ProductAttribute" /> + <data key="label">option</data> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + </entity> <entity name="ConfigurableProductTwoOptions" type="ConfigurableProductOption"> <var key="attribute_id" entityKey="attribute_id" entityType="ProductAttribute" /> <data key="label">option</data> <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> <requiredEntity type="ValueIndex">ValueIndex2</requiredEntity> </entity> + <entity name="ConfigurableProductThreeOptions" type="ConfigurableProductOption"> + <var key="attribute_id" entityKey="attribute_id" entityType="ProductAttribute" /> + <data key="label">option</data> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + <requiredEntity type="ValueIndex">ValueIndex2</requiredEntity> + <requiredEntity type="ValueIndex">ValueIndex3</requiredEntity> + </entity> </entities> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ProductConfigurableAttributeData.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ProductConfigurableAttributeData.xml index 9342172f7d4d..4c5f83ecebec 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/Data/ValueIndexData.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ValueIndexData.xml index 2e4788823a28..537ba2bbce08 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ValueIndexData.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ValueIndexData.xml @@ -14,4 +14,7 @@ <entity name="ValueIndex2" type="ValueIndex"> <var key="value_index" entityKey="value" entityType="ProductAttributeOption"/> </entity> + <entity name="ValueIndex3" type="ValueIndex"> + <var key="value_index" entityKey="value" entityType="ProductAttributeOption"/> + </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 428963835299..78e4c7bced8e 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminChooseAffectedAttributeSetSection.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminChooseAffectedAttributeSetSection.xml @@ -10,5 +10,8 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminChooseAffectedAttributeSetPopup"> <element name="confirm" type="button" selector="button[data-index='confirm_button']" timeout="30"/> + <element name="addNewAttrSet" type="radio" selector="//input[@data-index='affectedAttributeSetNew']" timeout="30"/> + <element name="createNewAttrSetName" type="input" selector="//input[@name='configurableNewAttributeSetName']" timeout="30"/> + <element name="closePopUp" type="button" selector="//*[contains(@class,'product_form_product_form_configurable_attribute_set')]//button[@data-role='closeBtn']" timeout="30"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml index 99e47baac37d..a5e74145c9fe 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml @@ -16,6 +16,8 @@ <element name="applyFilters" type="button" selector="button[data-action='grid-filter-apply']" timeout="30"/> <element name="firstCheckbox" type="input" selector="tr[data-repeat-index='0'] .admin__control-checkbox"/> <element name="id" type="text" selector="//tr[contains(@data-repeat-index, '0')]/td[2]/div"/> + <element name="attributeCheckbox" type="checkbox" selector="//div[contains(text(), '{{arg}}')]/ancestor::tr//input[@data-action='select-row']" parameterized="true"/> + <element name="defaultLabel" type="text" selector="//div[contains(text(), '{{arg}}')]/ancestor::tr//td[3]/div[@class='data-grid-cell-content']" parameterized="true"/> <element name="selectAll" type="button" selector=".action-select-all"/> <element name="selectAllByAttribute" type="button" selector="//div[@data-attribute-title='{{attr}}']//button[contains(@class, 'action-select-all')]" parameterized="true"/> @@ -24,6 +26,10 @@ <element name="saveAttribute" type="button" selector="li[data-attribute-option-title=''] .action-save" timeout="30"/> <element name="attributeCheckboxByIndex" type="input" selector="li.attribute-option:nth-of-type({{var1}}) input" parameterized="true"/> + <element name="applySingleSetOfImages" type="radio" selector=".admin__field-label[for='apply-single-set-radio']" timeout="30"/> + <element name="imageFileUpload" type="input" selector=".steps-wizard-section input[type='file'][name='image']"/> + <element name="imageUploadButton" type="button" selector=".steps-wizard-section div.gallery"/> + <element name="applyUniquePricesByAttributeToEachSku" type="radio" selector=".admin__field-label[for='apply-unique-prices-radio']"/> <element name="applySinglePriceToAllSkus" type="radio" selector=".admin__field-label[for='apply-single-price-radio']"/> <element name="singlePrice" type="input" selector="#apply-single-price-input"/> @@ -33,7 +39,18 @@ <element name="attribute3" type="input" selector="#apply-single-price-input-2"/> <element name="applySingleQuantityToEachSkus" type="radio" selector=".admin__field-label[for='apply-single-inventory-radio']" timeout="30"/> + <element name="applyUniqueImagesToEachSkus" type="radio" selector=".admin__field-label[for='apply-unique-images-radio']" timeout="30"/> + <element name="applyUniquePricesToEachSkus" type="radio" selector=".admin__field-label[for='apply-unique-prices-radio']" timeout="30"/> + <element name="selectImagesButton" type="select" selector="#apply-images-attributes" timeout="30"/> + <element name="uploadImagesButton" type="file" selector="//*[text()='{{option}}']/../../div[@data-role='gallery']//input[@type='file']" timeout="30" parameterized="true"/> + <element name="uploadProgressBar" type="text" selector=".uploader .file-row"/> + <element name="imageFile" type="text" selector="//*[@data-role='gallery']//img[contains(@src, '{{url}}')]" parameterized="true"/> + <element name="selectPriceButton" type="select" selector="#select-each-price" timeout="30"/> + <element name="price" type="input" selector="//*[text()='{{option}}']/../..//input[contains(@id, 'apply-single-price-input')]" parameterized="true"/> <element name="quantity" type="input" selector="#apply-single-inventory-input"/> <element name="gridLoadingMask" type="text" selector="[data-role='spinner'][data-component*='product_attributes_listing']"/> + <element name="attributeCheckboxByName" type="input" selector="//*[contains(@data-attribute-option-title,'{{arg}}')]//input[@type='checkbox']" parameterized="true"/> + <element name="attributeColorCheckbox" type="select" selector="//div[contains(text(),'color') and @class='data-grid-cell-content']/../preceding-sibling::td/label/input"/> + <element name="attributeRowByAttributeCode" type="block" selector="//td[count(../../..//th[./*[.='Attribute Code']]/preceding-sibling::th) + 1][./*[.='{{attribute_code}}']]/../td//input[@data-action='select-row']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminNewAttributePanelSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminNewAttributePanelSection.xml index 44077888f8bc..658e7a5fec9b 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminNewAttributePanelSection.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminNewAttributePanelSection.xml @@ -9,6 +9,15 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminNewAttributePanel"> + <element name="useInSearch" type="select" selector="#is_searchable"/> + <element name="visibleInAdvancedSearch" type="select" selector="#is_visible_in_advanced_search"/> + <element name="comparableOnStorefront" type="select" selector="#is_comparable"/> + <element name="useInLayeredNavigation" type="select" selector="#is_filterable"/> + <element name="visibleOnCatalogPagesOnStorefront" type="select" selector="#is_visible_on_front"/> + <element name="useInProductListing" type="select" selector="#used_in_product_listing"/> + <element name="usedForStoringInProductListing" type="select" selector="#used_for_sort_by"/> + <element name="storefrontPropertiesTab" selector="#front_fieldset-wrapper"/> + <element name="storefrontPropertiesTitle" selector="//span[text()='Storefront Properties']"/> <element name="container" type="text" selector="#create_new_attribute"/> <element name="saveAttribute" type="button" selector="#save"/> <element name="newAttributeIFrame" type="iframe" selector="create_new_attribute_container"/> @@ -20,5 +29,6 @@ <element name="optionAdminValue" type="input" selector="[data-role='options-container'] input[name='option[value][option_{{row}}][0]']" parameterized="true"/> <element name="optionDefaultStoreValue" type="input" selector="[data-role='options-container'] input[name='option[value][option_{{row}}][1]']" parameterized="true"/> <element name="deleteOption" type="button" selector="#delete_button_option_{{row}}" parameterized="true"/> + <element name="deleteOptionByName" type="button" selector="//*[contains(@value, '{{arg}}')]/../following-sibling::td[contains(@id, 'delete_button_container')]/button" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductDropdownOrderSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductDropdownOrderSection.xml new file mode 100644 index 000000000000..6056e7f3cbd0 --- /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 0de7bc00044c..f6828a3b8631 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml @@ -10,6 +10,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminProductFormConfigurationsSection"> <element name="sectionHeader" type="text" selector=".admin__collapsible-block-wrapper[data-index='configurable']"/> + <element name="createdConfigurationsBlock" type="text" selector="div.admin__field.admin__field-wide"/> <element name="createConfigurations" type="button" selector="button[data-index='create_configurable_products_button']" timeout="30"/> <element name="currentVariationsRows" type="button" selector=".data-row"/> <element name="currentVariationsNameCells" type="textarea" selector=".admin__control-fields[data-index='name_container']"/> @@ -18,10 +19,25 @@ <element name="currentVariationsQuantityCells" type="textarea" selector=".admin__control-fields[data-index='quantity_container']"/> <element name="currentVariationsAttributesCells" type="textarea" selector=".admin__control-fields[data-index='attributes']"/> <element name="currentVariationsStatusCells" type="textarea" selector="._no-header[data-index='status']"/> + <element name="firstSKUInConfigurableProductsGrid" type="input" selector="//input[@name='configurable-matrix[0][sku]']"/> <element name="actionsBtn" type="button" selector="(//button[@class='action-select']/span[contains(text(), 'Select')])[{{var1}}]" parameterized="true"/> + <element name="actionsBtnByProductName" type="textarea" selector="//*[.='Attributes']/ancestor::tr/td[@data-index='attributes']//span[contains(text(), '{{var}}')]/ancestor::tr//button[@class='action-select']" parameterized="true"/> + <element name="addProduct" type="button" selector="//*[.='Attributes']/ancestor::tr/td[@data-index='attributes']//span[contains(text(), '{{var}}')]/ancestor::tr//a[text()='Choose a different Product']" parameterized="true"/> <element name="removeProductBtn" type="button" selector="//a[text()='Remove Product']"/> <element name="disableProductBtn" type="button" selector="//a[text()='Disable Product']"/> <element name="enableProductBtn" type="button" selector="//a[text()='Enable Product']"/> + <element name="confProductSku" type="input" selector="//*[@name='configurable-matrix[{{arg}}][sku]']" parameterized="true"/> + <element name="confProductNameCell" type="input" selector="//*[.='Attributes']/ancestor::tr//span[contains(text(), '{{var}}')]/ancestor::tr/td[@data-index='name_container']//input" parameterized="true"/> + <element name="confProductSkuCell" type="input" selector="//*[.='Attributes']/ancestor::tr//span[contains(text(), '{{var}}')]/ancestor::tr/td[@data-index='sku_container']//input" parameterized="true"/> + <element name="confProductPriceCell" type="input" selector="//*[.='Attributes']/ancestor::tr//span[contains(text(), '{{var}}')]/ancestor::tr/td[@data-index='price_container']//input" parameterized="true"/> + <element name="confProductQuantityCell" type="input" selector="//*[.='Attributes']/ancestor::tr//span[contains(text(), '{{var}}')]/ancestor::tr/td[@data-index='quantity_container']//input" parameterized="true"/> + <element name="confProductWeightCell" type="input" selector="//*[.='Attributes']/ancestor::tr//span[contains(text(), '{{var}}')]/ancestor::tr/td[@data-index='price_weight']//input" parameterized="true"/> + <element name="confProductSkuMessage" type="text" selector="//*[@name='configurable-matrix[{{arg}}][sku]']/following-sibling::label" parameterized="true"/> + <element name="variationsSkuInputByRow" selector="[data-index='configurable-matrix'] table > tbody > tr:nth-of-type({{row}}) input[name*='sku']" type="input" parameterized="true"/> + <element name="variationsSkuInputErrorByRow" selector="[data-index='configurable-matrix'] table > tbody > tr:nth-of-type({{row}}) .admin__field-error" type="text" parameterized="true"/> + <element name="variationLabel" type="text" selector="//div[@data-index='configurable-matrix']/label"/> + <element name="stepsWizardTitle" type="text" selector="div.content:not([style='display: none;']) .steps-wizard-title"/> + <element name="attributeEntityByName" type="text" selector="//div[@class='attribute-entity']//div[normalize-space(.)='{{attributeLabel}}']" parameterized="true"/> </section> <section name="AdminConfigurableProductFormSection"> <element name="productWeight" type="input" selector=".admin__control-text[name='product[weight]']"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/ConfigurableProductAttributeNameDesignSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/ConfigurableProductAttributeNameDesignSection.xml index b3077d9d5d56..ea5638f6816c 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/ConfigurableProductAttributeNameDesignSection.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/ConfigurableProductAttributeNameDesignSection.xml @@ -7,7 +7,7 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="CatalogProductsSection"> <element name="catalogItem" type="button" selector="//*[@id='menu-magento-catalog-catalog']/a/span"/> <element name="productItem" type="button" selector="//*[@data-ui-id='menu-magento-catalog-catalog-products']/a"/> @@ -53,7 +53,6 @@ <element name="saveAttributeButton" type="button" selector="//*[@id='save']"/> <element name="advancedAttributeProperties" type="button" selector="//*[@id='advanced_fieldset-wrapper']//*[contains(text(),'Advanced Attribute Properties')]"/> <element name="attributeCodeField" type="input" selector="//*[@id='attribute_code']"/> - </section> <section name="CreateProductConfigurations"> @@ -64,5 +63,4 @@ <element name="checkboxBlack" type="input" selector="//fieldset[@class='admin__fieldset admin__fieldset-options']//*[contains(text(),'black')]/preceding-sibling::input"/> <element name="errorMessage" type="input" selector="//div[@data-ui-id='messages-message-error']"/> </section> - </sections> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontProductInfoMainSection.xml index b195c19f7bed..24cd9262b674 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontProductInfoMainSection.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -11,11 +11,14 @@ <section name="StorefrontProductInfoMainSection"> <element name="optionByAttributeId" type="input" selector="#attribute{{var1}}" parameterized="true"/> <element name="productAttributeTitle1" type="text" selector="#product-options-wrapper div[tabindex='0'] label"/> + <element name="productPrice" type="text" selector="div.price-box.price-final_price"/> <element name="productAttributeOptions1" type="select" selector="#product-options-wrapper div[tabindex='0'] option"/> <element name="productAttributeOptionsSelectButton" type="select" selector="#product-options-wrapper .super-attribute-select"/> <element name="productAttributeOptionsError" type="text" selector="//div[@class='mage-error']"/> <!-- Parameter is the order number of the attribute on the page (1 is the newest) --> <element name="nthAttributeOnPage" type="block" selector="tr:nth-of-type({{numElement}}) .data" parameterized="true"/> <element name="stockIndication" type="block" selector=".stock" /> + <element name="attributeSelectByAttributeID" type="select" selector="//div[@class='fieldset']//div[//span[text()='{{attribute_code}}']]//select" parameterized="true"/> + <element name="attributeOptionByAttributeID" type="select" selector="//div[@class='fieldset']//div[//span[text()='{{attribute_code}}']]//option[text()='{{optionName}}']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddingNewOptionsWithImagesAndPricesToConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddingNewOptionsWithImagesAndPricesToConfigurableProductTest.xml new file mode 100644 index 000000000000..52443a17dfe6 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddingNewOptionsWithImagesAndPricesToConfigurableProductTest.xml @@ -0,0 +1,124 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminAddingNewOptionsWithImagesAndPricesToConfigurableProductTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Update product"/> + <title value="Adding new options with images and prices to Configurable Product"/> + <description value="Test case verifies possibility to add new options for configurable attribute for existing configurable product."/> + <severity value="CRITICAL"/> + <testCaseId value="MC-13339"/> + <group value="configurableProduct"/> + </annotations> + + <before> + <actionGroup ref="AdminCreateApiConfigurableProductActionGroup" stepKey="createConfigurableProduct"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <after> + <deleteData createDataKey="createConfigProductCreateConfigurableProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigProductAttributeCreateConfigurableProduct" stepKey="deleteConfigProductAttribute"/> + <deleteData createDataKey="createConfigChildProduct1CreateConfigurableProduct" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigChildProduct2CreateConfigurableProduct" stepKey="deleteConfigChildProduct2"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open edit product page--> + <amOnPage url="{{AdminProductEditPage.url($$createConfigProductCreateConfigurableProduct.id$$)}}" stepKey="goToProductEditPage"/> + + <!--Open edit configuration wizard--> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickEditConfigurations"/> + <see userInput="Select Attributes" selector="{{AdminProductFormConfigurationsSection.stepsWizardTitle}}" stepKey="seeStepTitle"/> + + <!--Click Next button--> + <actionGroup ref="AdminConfigurableWizardMoveToNextStepActionGroup" stepKey="navigateToAttributeValuesStep"> + <argument name="title" value="Attribute Values"/> + </actionGroup> + <seeElement selector="{{AdminProductFormConfigurationsSection.attributeEntityByName($$createConfigProductAttributeCreateConfigurableProduct.default_frontend_label$$)}}" stepKey="seeAttribute"/> + + <!--Create one color option via "Create New Value" link--> + <click selector="{{AdminCreateProductConfigurationsPanel.createNewValue}}" stepKey="clickOnCreateNewValue"/> + <fillField userInput="{{colorDefaultProductAttribute1.name}}" selector="{{AdminCreateProductConfigurationsPanel.attributeName}}" stepKey="fillFieldForNewAttribute"/> + <click selector="{{AdminCreateProductConfigurationsPanel.saveAttribute}}" stepKey="clickOnSaveNewAttribute"/> + + <!--Click Next button--> + <actionGroup ref="AdminConfigurableWizardMoveToNextStepActionGroup" stepKey="navigateToBulkStep"> + <argument name="title" value="Bulk Images, Price and Quantity"/> + </actionGroup> + + <!--Select Apply unique images by attribute to each SKU and color attribute in dropdown in Images--> + <click selector="{{AdminCreateProductConfigurationsPanel.applyUniqueImagesToEachSkus}}" stepKey="clickOnApplyUniqueImagesToEachSku"/> + <selectOption userInput="$$createConfigProductAttributeCreateConfigurableProduct.default_frontend_label$$" + selector="{{AdminCreateProductConfigurationsPanel.selectImagesButton}}" stepKey="selectAttributeOption"/> + + <!-- Add images to configurable product attribute options --> + <actionGroup ref="addUniqueImageToConfigurableProductOption" stepKey="addImageToConfigurableProductOptionOne"> + <argument name="image" value="ImageUpload"/> + <argument name="frontend_label" value="$$createConfigProductAttributeCreateConfigurableProduct.default_frontend_label$$"/> + <argument name="label" value="$$getConfigAttributeOption1CreateConfigurableProduct.label$$"/> + </actionGroup> + <actionGroup ref="addUniqueImageToConfigurableProductOption" stepKey="addImageToConfigurableProductOptionTwo"> + <argument name="image" value="ImageUpload_1"/> + <argument name="frontend_label" value="$$createConfigProductAttributeCreateConfigurableProduct.default_frontend_label$$"/> + <argument name="label" value="$$getConfigAttributeOption2CreateConfigurableProduct.label$$"/> + </actionGroup> + <actionGroup ref="addUniqueImageToConfigurableProductOption" stepKey="addImageToConfigurableProductOptionThree"> + <argument name="image" value="ImageUpload3"/> + <argument name="frontend_label" value="$$createConfigProductAttributeCreateConfigurableProduct.default_frontend_label$$"/> + <argument name="label" value="{{colorDefaultProductAttribute1.name}}"/> + </actionGroup> + + <!--Add prices to configurable product attribute options--> + <click selector="{{AdminCreateProductConfigurationsPanel.applyUniquePricesToEachSkus}}" stepKey="clickOnApplyUniquePricesByAttributeToEachSku"/> + <selectOption userInput="$$createConfigProductAttributeCreateConfigurableProduct.default_frontend_label$$" + selector="{{AdminCreateProductConfigurationsPanel.selectAttribute}}" stepKey="selectAttributes"/> + <fillField userInput="10" selector="{{AdminCreateProductConfigurationsPanel.price($$getConfigAttributeOption1CreateConfigurableProduct.label$$)}}" stepKey="fillAttributePrice"/> + <fillField userInput="20" selector="{{AdminCreateProductConfigurationsPanel.price($$getConfigAttributeOption2CreateConfigurableProduct.label$$)}}" stepKey="fillAttributePrice1"/> + <fillField userInput="30" selector="{{AdminCreateProductConfigurationsPanel.price(colorDefaultProductAttribute1.name)}}" stepKey="fillAttributePrice2"/> + + <!-- Add quantity to product attribute options --> + <click selector="{{AdminCreateProductConfigurationsPanel.applySingleQuantityToEachSkus}}" stepKey="clickOnApplySingleQuantityToEachSku"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="100" stepKey="enterAttributeQuantity"/> + + <!--Click Next button--> + <actionGroup ref="AdminConfigurableWizardMoveToNextStepActionGroup" stepKey="navigateToSummaryStep"> + <argument name="title" value="Summary"/> + </actionGroup> + + <!--Click Generate Configure button--> + <click selector="{{ConfigurableProductSection.generateConfigure}}" stepKey="clickGenerateConfigure"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + + <!--Go to frontend and check image and price--> + <amOnPage url="{{StorefrontProductPage.url($$createConfigProductCreateConfigurableProduct.custom_attributes[url_key]$$)}}" stepKey="goToProductPage"/> + + <actionGroup ref="AssertOptionImageAndPriceInStorefrontProductActionGroup" stepKey="assertFirstOptionImageAndPriceInStorefrontProductPage"> + <argument name="label" value="$$getConfigAttributeOption1CreateConfigurableProduct.label$$"/> + <argument name="image" value="{{ImageUpload.filename}}"/> + <argument name="price" value="10"/> + </actionGroup> + + <actionGroup ref="AssertOptionImageAndPriceInStorefrontProductActionGroup" stepKey="assertSecondOptionImageAndPriceInStorefrontProductPage"> + <argument name="label" value="$$getConfigAttributeOption2CreateConfigurableProduct.label$$"/> + <argument name="image" value="{{ImageUpload_1.filename}}"/> + <argument name="price" value="20"/> + </actionGroup> + + <actionGroup ref="AssertOptionImageAndPriceInStorefrontProductActionGroup" stepKey="assertThirdOptionImageAndPriceInStorefrontProductPage"> + <argument name="label" value="{{colorDefaultProductAttribute1.name}}"/> + <argument name="image" value="{{ImageUpload3.filename}}"/> + <argument name="price" value="30"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAssertNoticeThatExistingSkuAutomaticallyChangedWhenSavingProductWithSameSkuTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAssertNoticeThatExistingSkuAutomaticallyChangedWhenSavingProductWithSameSkuTest.xml new file mode 100644 index 000000000000..68bf703ecdab --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAssertNoticeThatExistingSkuAutomaticallyChangedWhenSavingProductWithSameSkuTest.xml @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminAssertNoticeThatExistingSkuAutomaticallyChangedWhenSavingProductWithSameSkuTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Create configurable product"/> + <title value="Assert notice that existing sku automatically changed when saving product with same sku"/> + <description value="Admin should not be able to create configurable product and two new options with the same sku"/> + <testCaseId value="MC-13693"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!-- Delete configurable product --> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Delete product attribute --> + <actionGroup ref="deleteProductAttributeByLabel" stepKey="deleteProductAttribute"> + <argument name="ProductAttribute" value="colorProductAttribute"/> + </actionGroup> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Create configurable product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="goToCreateProductPage" stepKey="createConfigurableProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Fill configurable product values --> + <actionGroup ref="fillMainProductForm" stepKey="fillConfigurableProductValues"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!--Create product configurations--> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickCreateConfigurations" after="fillConfigurableProductValues"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}" time="30" stepKey="waitForConfigurationModalOpen" after="clickCreateConfigurations"/> + + <!--Create new attribute with two option --> + <actionGroup ref="addNewProductConfigurationAttribute" stepKey="createProductConfigurationAttribute"> + <argument name="attribute" value="colorProductAttribute"/> + <argument name="firstOption" value="colorConfigurableProductAttribute1"/> + <argument name="secondOption" value="colorConfigurableProductAttribute2"/> + </actionGroup> + + <!-- Change products sku configurations in grid --> + <fillField userInput="{{ApiConfigurableProduct.sku}}" selector="{{AdminProductFormConfigurationsSection.confProductSkuCell(colorConfigurableProductAttribute1.name)}}" stepKey="fillFieldSkuForFirstAttributeOption"/> + <fillField userInput="{{ApiConfigurableProduct.sku}}" selector="{{AdminProductFormConfigurationsSection.confProductSkuCell(colorConfigurableProductAttribute2.name)}}" stepKey="fillFieldSkuForSecondAttributeOption"/> + + <!-- Save product --> + <waitForElementVisible selector="{{AdminProductFormActionSection.saveButton}}" stepKey="waitForSaveBtnVisible"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProductAgain"/> + <waitForElementVisible selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="waitPopUpVisible"/> + <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmPopup"/> + + <!-- Assert product auto incremented sku notice message; see success message --> + <see selector="{{AdminMessagesSection.noticeMessage}}" stepKey="seeNoticeMessage" userInput="SKU for product {{ApiConfigurableProduct.name}} has been changed to {{ApiConfigurableProduct.sku}}-2."/> + <see selector="{{AdminMessagesSection.successMessage}}" stepKey="seeSuccessMessage" userInput="You saved the product."/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckValidatorConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckValidatorConfigurableProductTest.xml new file mode 100644 index 000000000000..dd641fd370ba --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckValidatorConfigurableProductTest.xml @@ -0,0 +1,126 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCheckValidatorConfigurableProductTest"> + <annotations> + <stories value="Configurable Product"/> + <title value="Check that validator works correctly when creating Configurations for Configurable Products"/> + <description value="Verify validator works correctly for Configurable Products"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-95995"/> + <useCaseId value="MAGETWO-95834"/> + <group value="ConfigurableProduct"/> + </annotations> + + <before> + <!--Login as admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create Category--> + <createData entity="ApiCategory" stepKey="createCategory"/> + <!--Create Configurable product--> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + + <after> + <!--Delete created data--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <actionGroup ref="deleteProductBySku" stepKey="deleteProduct"> + <argument name="sku" value="{{ApiConfigurableProduct.name}}-thisIsShortName"/> + </actionGroup> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" + dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <!-- Remove attribute --> + <actionGroup ref="deleteProductAttribute" stepKey="deleteAttribute"> + <argument name="ProductAttribute" value="productDropDownAttribute"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Find the product that we just created using the product grid --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <waitForPageLoad stepKey="waitForAdminProductPageLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" + dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> + <actionGroup ref="filterProductGridBySku" stepKey="findCreatedProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + <waitForPageLoad stepKey="waitForProductFilterLoad"/> + <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + + <!-- Create configurations based off the Text Swatch we created earlier --> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickCreateConfigurations"/> + + <!--Create new attribute--> + <waitForElementVisible stepKey="waitForNewAttributePageOpened" selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}"/> + <click selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}" stepKey="clickCreateNewAttribute" after="waitForNewAttributePageOpened"/> + <switchToIFrame selector="{{AdminNewAttributePanel.newAttributeIFrame}}" stepKey="enterAttributePanelIFrame" after="clickCreateNewAttribute"/> + <waitForElementVisible selector="{{AdminNewAttributePanel.defaultLabel}}" time="30" stepKey="waitForIframeLoad" after="enterAttributePanelIFrame"/> + <fillField selector="{{AdminNewAttributePanel.defaultLabel}}" userInput="{{productDropDownAttribute.attribute_code}}" stepKey="fillDefaultLabel" after="waitForIframeLoad"/> + <selectOption selector="{{AdminNewAttributePanel.inputType}}" userInput="{{colorProductAttribute.input_type}}" stepKey="selectAttributeInputType" after="fillDefaultLabel"/> + <!--Add option to attribute--> + <click selector="{{AdminNewAttributePanel.addOption}}" stepKey="clickAddOption1" after="selectAttributeInputType"/> + <waitForElementVisible selector="{{AdminNewAttributePanel.isDefault('1')}}" time="30" stepKey="waitForOptionRow1" after="clickAddOption1"/> + <fillField selector="{{AdminNewAttributePanel.optionAdminValue('0')}}" userInput="ThisIsLongNameNameLengthMoreThanSixtyFourThisIsLongNameNameLength" stepKey="fillAdminLabel1" after="waitForOptionRow1"/> + <fillField selector="{{AdminNewAttributePanel.optionDefaultStoreValue('0')}}" userInput="{{colorProductAttribute1.name}}" stepKey="fillDefaultLabel1" after="fillAdminLabel1"/> + + <!--Save attribute--> + <click selector="{{AdminNewAttributePanel.saveAttribute}}" stepKey="clickOnNewAttributePanel"/> + <waitForPageLoad stepKey="waitForSaveAttribute"/> + <switchToIFrame stepKey="switchOutOfIFrame"/> + + <!--Find attribute in grid and select--> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFilters"/> + <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="clickOnFilters"/> + <fillField selector="{{AdminDataGridHeaderSection.attributeCodeFilterInput}}" userInput="{{productDropDownAttribute.attribute_code}}" stepKey="fillFilterAttributeCodeField"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminDataGridTableSection.rowCheckbox('1')}}" stepKey="clickOnFirstCheckbox"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickNextStep1"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.selectAllByAttribute(productDropDownAttribute.attribute_code)}}" stepKey="waitForNextPageOpened"/> + <click selector="{{AdminCreateProductConfigurationsPanel.selectAllByAttribute(productDropDownAttribute.attribute_code)}}" stepKey="clickSelectAll"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickNextStep2"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.applySinglePriceToAllSkus}}" stepKey="waitForNextPageOpened2"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applySinglePriceToAllSkus}}" stepKey="clickOnApplySinglePriceToAllSkus"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.singlePrice}}" userInput="10" stepKey="enterAttributePrice"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applySingleQuantityToEachSkus}}" stepKey="clickOnApplySingleQuantityToEachSku"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="100" stepKey="enterAttributeQuantity"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextStep3"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="waitForNextPageOpened3"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="generateProducts"/> + <waitForElementVisible selector="{{AdminProductFormActionSection.saveButton}}" stepKey="waitForSaveButtonVisible"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <waitForElementVisible selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="waitForPopUpVisible"/> + <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmInPopup"/> + <dontSeeElement selector="{{AdminMessagesSection.success}}" stepKey="dontSeeSaveProductMessage"/> + + <!--Close modal window--> + <click selector="{{AdminChooseAffectedAttributeSetPopup.closePopUp}}" stepKey="clickOnClosePopup"/> + <waitForElementNotVisible selector="{{AdminChooseAffectedAttributeSetPopup.closePopUp}}" stepKey="waitForDialogClosed"/> + + <!--See that validation message is shown under the fields--> + <scrollTo selector="{{AdminProductFormConfigurationsSection.currentVariationsSkuCells}}" stepKey="scrollTConfigurationTab"/> + <see userInput="Please enter less or equal than 64 symbols." selector="{{AdminProductFormConfigurationsSection.confProductSkuMessage('0')}}" stepKey="SeeValidationMessage"/> + + <!--Edit "SKU" with valid quantity--> + <fillField stepKey="fillValidValue" selector="{{AdminProductFormConfigurationsSection.confProductSku('0')}}" userInput="{{ApiConfigurableProduct.name}}-thisIsShortName"/> + + <!--Click on "Save"--> + <waitForElementVisible selector="{{AdminProductFormActionSection.saveButton}}" stepKey="waitForSaveBtnVisible"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProductAgain"/> + + <!--Click on "Confirm". Product is saved, success message appears --> + <waitForElementVisible selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="waitPopUpVisible"/> + <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmPopup"/> + <seeElement selector="{{AdminMessagesSection.success}}" stepKey="seeSaveProductMessage"/> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest.xml index 48f46a1205ec..2af85e1bac04 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest.xml @@ -36,6 +36,7 @@ </actionGroup> <!-- assert color configurations on the admin create product page --> + <dontSee selector="{{AdminProductFormConfigurationsSection.variationLabel}}" stepKey="seeLabelNotVisible"/> <seeNumberOfElements selector="{{AdminProductFormConfigurationsSection.currentVariationsRows}}" userInput="3" stepKey="seeNumberOfRows"/> <see selector="{{AdminProductFormConfigurationsSection.currentVariationsNameCells}}" userInput="{{colorProductAttribute1.name}}" stepKey="seeAttributeName1InField"/> <see selector="{{AdminProductFormConfigurationsSection.currentVariationsNameCells}}" userInput="{{colorProductAttribute2.name}}" stepKey="seeAttributeName2InField"/> @@ -68,4 +69,71 @@ <see selector="{{StorefrontProductInfoMainSection.productAttributeOptions1}}" userInput="{{colorProductAttribute2.name}}" stepKey="seeInDropDown2"/> <see selector="{{StorefrontProductInfoMainSection.productAttributeOptions1}}" userInput="{{colorProductAttribute3.name}}" stepKey="seeInDropDown3"/> </test> + + <test name="AdminCreateConfigurableProductAfterGettingIncorrectSKUMessageTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Create, Read, Update, Delete"/> + <title value="admin should be able to create a configurable product after incorrect sku"/> + <description value="admin should be able to create a configurable product after incorrect sku"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-96365"/> + <useCaseId value="MAGETWO-94556"/> + <group value="ConfigurableProduct"/> + </annotations> + + <before> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <amOnPage url="{{AdminProductEditPage.url($$createConfigProduct.id$$)}}" stepKey="goToEditPage"/> + <waitForPageLoad stepKey="waitForProductPage"/> + <conditionalClick selector="{{AdminProductFormConfigurationsSection.sectionHeader}}" dependentSelector="{{AdminProductFormConfigurationsSection.createConfigurations}}" visible="false" stepKey="openConfigurationSection"/> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="openConfigurationPane"/> + <click selector="{{AdminCreateProductConfigurationsPanel.filters}}" stepKey="clickFilters"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.attributeCode}}" userInput="color" stepKey="fillFilterAttributeCodeField"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminCreateProductConfigurationsPanel.firstCheckbox}}" stepKey="clickOnFirstCheckbox"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.createNewValue}}" stepKey="clickOnCreateNewValue1"/> + <fillField userInput="{{colorProductAttribute2.name}}" selector="{{AdminCreateProductConfigurationsPanel.attributeName}}" stepKey="fillFieldForNewAttribute1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.saveAttribute}}" stepKey="clickOnSaveNewAttribute1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton2"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton3"/> + <click selector="{{ConfigurableProductSection.generateConfigure}}" stepKey="generateConfigure"/> + <waitForPageLoad stepKey="waitForGenerateConfigure"/> + <grabValueFrom selector="{{AdminProductFormConfigurationsSection.firstSKUInConfigurableProductsGrid}}" stepKey="grabTextFromContent"/> + <fillField stepKey="fillMoreThan64Symbols" selector="{{AdminProductFormConfigurationsSection.firstSKUInConfigurableProductsGrid}}" userInput="01234567890123456789012345678901234567890123456789012345678901234"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct1"/> + <conditionalClick selector="{{AdminChooseAffectedAttributeSetPopup.closePopUp}}" dependentSelector="{{AdminChooseAffectedAttributeSetPopup.closePopUp}}" visible="true" stepKey="clickOnCloseInPopup"/> + <see stepKey="seeErrorMessage" userInput="Please enter less or equal than 64 symbols."/> + <fillField stepKey="fillCorrectSKU" selector="{{AdminProductFormConfigurationsSection.firstSKUInConfigurableProductsGrid}}" userInput="$grabTextFromContent"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct2"/> + <conditionalClick selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" dependentSelector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" visible="true" stepKey="clickOnConfirmInPopup"/> + <see userInput="You saved the product." stepKey="seeSaveConfirmation"/> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> + <waitForPageLoad stepKey="waitForProductAttributes"/> + <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGrid1"/> + <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="color" stepKey="fillFilter"/> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearch"/> + <click selector="{{AdminProductAttributeGridSection.AttributeCode('color')}}" stepKey="clickRowToEdit"/> + <click selector="{{DropdownAttributeOptionsSection.deleteButton(1)}}" stepKey="deleteOption"/> + <click selector="{{AttributePropertiesSection.Save}}" stepKey="saveAttribute"/> + <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGrid2"/> + <actionGroup stepKey="deleteProduct1" ref="deleteProductBySku"> + <argument name="sku" value="$grabTextFromContent"/> + </actionGroup> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <waitForPageLoad time="60" stepKey="waitForPageLoadInitial"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> + </test> </tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml new file mode 100644 index 000000000000..c599a6a23f19 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml @@ -0,0 +1,108 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminConfigurableProductLongSkuTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Create configurable product"/> + <title value="Admin is able to create an product with a long sku below that is below the character limit"/> + <description value="Try to create a product with sku slightly less than char limit. Get client side SKU length error for child products. Correct SKUs and save product succeeds."/> + <severity value="MAJOR"/> + <testCaseId value="MC-5685"/> + <group value="ConfigurableProduct"/> + </annotations> + + <before> + <!--Create product attribute with 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="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <!--Create Category--> + <createData entity="ApiCategory" stepKey="createCategory"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + </before> + + <after> + <!--Clean up products--> + <actionGroup ref="deleteProductByName" stepKey="cleanUpProducts"> + <argument name="sku" value="{{ProductWithLongNameSku.sku}}"/> + <argument name="name" value="{{ProductWithLongNameSku.name}}"/> + </actionGroup> + <!--Clean up attribute--> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + <!--Clean up category--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + </after> + + <!--Create a configurable product with long name and sku--> + <amOnPage url="{{AdminProductCreatePage.url(AddToDefaultSet.attributeSetId, 'configurable')}}" stepKey="goToProductCreatePage"/> + <waitForPageLoad stepKey="waitForProductCreatePage"/> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{ProductWithLongNameSku.name}}" stepKey="fillProductName"/> + <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{ProductWithLongNameSku.sku}}" stepKey="fillProductSku"/> + <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{ProductWithLongNameSku.price}}" stepKey="fillProductPrice"/> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[$$createCategory.name$$]" stepKey="selectCategory"/> + <!--Setup configurations--> + <actionGroup ref="generateConfigurationsByAttributeCode" stepKey="setupConfigurations"> + <argument name="attributeCode" value="$$createConfigProductAttribute.attribute_code$$"/> + </actionGroup> + + <!--See SKU length errors in Current Variations grid--> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProductFail"/> + <seeInCurrentUrl url="{{AdminProductCreatePage.url(AddToDefaultSet.attributeSetId, 'configurable')}}" stepKey="seeRemainOnCreateProductPage"/> + <see selector="{{AdminProductFormConfigurationsSection.variationsSkuInputErrorByRow('1')}}" userInput="Please enter less or equal than 64 symbols." stepKey="seeSkuTooLongError1"/> + <see selector="{{AdminProductFormConfigurationsSection.variationsSkuInputErrorByRow('2')}}" userInput="Please enter less or equal than 64 symbols." stepKey="seeSkuTooLongError2"/> + <!--Fix SKU lengths--> + <fillField selector="{{AdminProductFormConfigurationsSection.variationsSkuInputByRow('1')}}" userInput="LongSku-$$getConfigAttributeOption1.label$$" stepKey="fixConfigurationSku1"/> + <fillField selector="{{AdminProductFormConfigurationsSection.variationsSkuInputByRow('2')}}" userInput="LongSku-$$getConfigAttributeOption2.label$$" stepKey="fixConfigurationSku2"/> + <!--Save product successfully--> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProductSuccess"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessage"/> + + <!--Assert configurations on the product edit pag--> + <seeNumberOfElements selector="{{AdminProductFormConfigurationsSection.currentVariationsRows}}" userInput="2" stepKey="seeNumberOfRows"/> + <see selector="{{AdminProductFormConfigurationsSection.currentVariationsNameCells}}" userInput="{{ProductWithLongNameSku.name}}-$$getConfigAttributeOption1.label$$" stepKey="seeChildProductName1"/> + <see selector="{{AdminProductFormConfigurationsSection.currentVariationsNameCells}}" userInput="{{ProductWithLongNameSku.name}}-$$getConfigAttributeOption2.label$$" stepKey="seeChildProductName2"/> + <see selector="{{AdminProductFormConfigurationsSection.currentVariationsSkuCells}}" userInput="LongSku-$$getConfigAttributeOption1.label$$" stepKey="seeChildProductSku1"/> + <see selector="{{AdminProductFormConfigurationsSection.currentVariationsSkuCells}}" userInput="LongSku-$$getConfigAttributeOption2.label$$" stepKey="seeChildProductSku2"/> + <see selector="{{AdminProductFormConfigurationsSection.currentVariationsPriceCells}}" userInput="{{ProductWithLongNameSku.price}}" stepKey="seeConfigurationsPrice"/> + + <!--Assert storefront category list page--> + <amOnPage url="/" stepKey="amOnStorefront"/> + <waitForPageLoad stepKey="waitForStorefrontLoad"/> + <click userInput="$$createCategory.name$$" stepKey="clickOnCategoryName"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <see userInput="{{ProductWithLongNameSku.name}}" stepKey="assertProductPresent"/> + <see userInput="{{ProductWithLongNameSku.price}}" stepKey="assertProductPricePresent"/> + + <!--Assert storefront product details page--> + <click selector="{{StorefrontCategoryProductSection.ProductTitleByName(ProductWithLongNameSku.name)}}" stepKey="clickOnProductName"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <seeInTitle userInput="{{ProductWithLongNameSku.name}}" stepKey="assertProductNameTitle"/> + <see userInput="{{ProductWithLongNameSku.name}}" selector="{{StorefrontProductInfoMainSection.productName}}" stepKey="assertProductName"/> + <see userInput="{{ProductWithLongNameSku.sku}}" selector="{{StorefrontProductInfoMainSection.productSku}}" stepKey="assertProductSku"/> + <see selector="{{StorefrontProductInfoMainSection.productAttributeTitle1}}" userInput="$$createConfigProductAttribute.default_frontend_label$$" stepKey="seeColorAttributeName1"/> + <see selector="{{StorefrontProductInfoMainSection.productAttributeOptions1}}" userInput="$$getConfigAttributeOption1.label$$" stepKey="seeInDropDown1"/> + <see selector="{{StorefrontProductInfoMainSection.productAttributeOptions1}}" userInput="$$getConfigAttributeOption2.label$$" stepKey="seeInDropDown2"/> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest.xml index 027c2ce72916..001d4d17ec21 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest.xml @@ -228,12 +228,12 @@ <waitForPageLoad stepKey="waitForAdminProductPageLoad"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> <actionGroup ref="filterProductGridBySku" stepKey="findCreatedProduct"> - <argument name="product" value="ApiConfigurableProduct"/> + <argument name="product" value="$$createConfigProduct$$"/> </actionGroup> <waitForPageLoad stepKey="waitForProductFilterLoad"/> - - <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage"/> - <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openProduct"> + <argument name="product" value="$$createConfigProduct$$" /> + </actionGroup> <!-- Open the wizard for editing configurations and fill out a new attribute --> <click stepKey="clickEditConfig" selector="{{AdminProductFormConfigurationsSection.createConfigurations}}"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest.xml index af12f49bf86e..39aa516077c5 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest.xml @@ -57,7 +57,13 @@ <click selector="{{AdminUpdateAttributesSection.toggleDescription}}" stepKey="clickToggleDescription"/> <fillField selector="{{AdminUpdateAttributesSection.description}}" userInput="MFTF automation!" stepKey="fillDescription"/> <click selector="{{AdminUpdateAttributesSection.saveButton}}" stepKey="clickSave"/> - <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="A total of 3 record(s) were updated." stepKey="seeSaveSuccess"/> + <see selector="{{AdminProductMessagesSection.successMessage}}" userInput="Message is added to queue" stepKey="seeSaveSuccess"/> + + <!-- Run cron twice --> + <magentoCLI command="cron:run" stepKey="runCron1"/> + <magentoCLI command="cron:run" stepKey="runCron2"/> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitFormToReload1"/> <!-- Check storefront for description --> <amOnPage url="$$createProduct1.sku$$.html" stepKey="gotoProduct1"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml new file mode 100755 index 000000000000..2cc71964042a --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml @@ -0,0 +1,192 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt 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"/> + </annotations> + <before> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + </before> + <after> + <deleteData stepKey="deleteAttribute" createDataKey="createConfigProductAttribute"/> + </after> + <actionGroup ref="GoToSpecifiedCreateProductPage" stepKey="openProductFillForm"> + <argument name="productType" value="virtual"/> + </actionGroup> + <actionGroup ref="fillMainProductForm" stepKey="fillProductForm"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <comment before="createConfiguration" stepKey="beforeCreateConfiguration" userInput="Adding Configuration to Product"/> + <actionGroup ref="generateConfigurationsByAttributeCode" stepKey="createConfiguration" after="fillProductForm"> + <argument name="attributeCode" value="$$createConfigProductAttribute.attribute_code$$"/> + </actionGroup> + <actionGroup ref="saveConfiguredProduct" stepKey="saveProductForm"/> + <see selector="{{AdminProductGridSection.productGridCell('2', 'Type')}}" userInput="Configurable Product" stepKey="seeProductTypeInGrid"/> + <actionGroup ref="VerifyOptionInProductStorefront" stepKey="verifyConfigurableOption" after="AssertProductInStorefrontProductPage"> + <argument name="attributeCode" value="$createConfigProductAttribute.default_frontend_label$"/> + <argument name="optionName" value="$createConfigProductAttributeOption1.option[store_labels][1][label]$"/> + </actionGroup> + </test> + <test name="AdminCreateSimpleProductSwitchToConfigurableTest" extends="AdminCreateSimpleProductSwitchToVirtualTest"> + <annotations> + <features value="Catalog"/> + <stories value="Product Type Switching"/> + <title value="Admin should be able to switch a new product from simple to configurable"/> + <description value="After selecting a simple product when adding Admin should be switch to configurable implicitly"/> + <severity value="CRITICAL"/> + <useCaseId value="MAGETWO-44165"/> + <testCaseId value="MAGETWO-29398"/> + <group value="catalog"/> + </annotations> + <before> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + </before> + <after> + <deleteData stepKey="deleteAttribute" createDataKey="createConfigProductAttribute"/> + </after> + <actionGroup ref="GoToSpecifiedCreateProductPage" stepKey="openProductFillForm"> + <argument name="productType" value="simple"/> + </actionGroup> + <!-- Create configurable product from simple product page--> + <comment userInput="Create configurable product" stepKey="commentCreateProduct"/> + <actionGroup ref="fillMainProductForm" stepKey="fillProductForm"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <comment before="createConfiguration" stepKey="beforeCreateConfiguration" userInput="Adding Configuration to Product"/> + <actionGroup ref="generateConfigurationsByAttributeCode" stepKey="createConfiguration" after="fillProductForm"> + <argument name="attributeCode" value="$$createConfigProductAttribute.attribute_code$$"/> + </actionGroup> + <actionGroup ref="saveConfiguredProduct" stepKey="saveProductForm"/> + <see selector="{{AdminProductGridSection.productGridCell('2', 'Type')}}" userInput="Configurable Product" stepKey="seeProductTypeInGrid"/> + <!-- Verify product on store front --> + <comment userInput="Verify product on store front" stepKey="commentVerifyProductGrid"/> + <actionGroup ref="VerifyOptionInProductStorefront" stepKey="verifyConfigurableOption" after="AssertProductInStorefrontProductPage"> + <argument name="attributeCode" value="$createConfigProductAttribute.default_frontend_label$"/> + <argument name="optionName" value="$createConfigProductAttributeOption1.option[store_labels][1][label]$"/> + </actionGroup> + </test> + <test name="AdminCreateDownloadableProductSwitchToConfigurableTest"> + <annotations> + <features value="Catalog"/> + <stories value="Product Type Switching"/> + <title value="Admin should be able to switch a new product from downloadable to configurable"/> + <description value="After selecting a downloadable product when adding Admin should be switch to configurable implicitly"/> + <severity value="CRITICAL"/> + <useCaseId value="MAGETWO-44165"/> + <testCaseId value="MAGETWO-29398"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + </before> + <after> + <actionGroup ref="GoToProductCatalogPage" stepKey="goToProductCatalogPage"/> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteConfigurableProduct"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <actionGroup ref="resetProductGridToDefaultView" stepKey="resetSearch"/> + <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> + <deleteData stepKey="deleteAttribute" createDataKey="createConfigProductAttribute"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Create configurable product from downloadable product page--> + <comment userInput="Create configurable product" stepKey="commentCreateProduct"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + <!-- Open Dropdown and select downloadable product option --> + <comment stepKey="beforeOpenProductFillForm" userInput="Selecting Product from the Add Product Dropdown"/> + <actionGroup ref="GoToSpecifiedCreateProductPage" stepKey="openProductFillForm"> + <argument name="productType" value="downloadable"/> + </actionGroup> + <scrollTo selector="{{AdminProductDownloadableSection.sectionHeader}}" stepKey="scrollToDownloadableInfo" /> + <uncheckOption selector="{{AdminProductDownloadableSection.isDownloadableProduct}}" stepKey="checkIsDownloadable"/> + <!-- Fill form for Downloadable Product Type --> + <comment stepKey="beforeFillProductForm" userInput="Filling Product Form"/> + <actionGroup ref="fillMainProductForm" stepKey="fillProductForm"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <actionGroup ref="SetProductUrlKey" stepKey="setProductUrl"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <comment before="createConfiguration" stepKey="beforeCreateConfiguration" userInput="Adding Configuration to Product"/> + <actionGroup ref="generateConfigurationsByAttributeCode" stepKey="createConfiguration"> + <argument name="attributeCode" value="$$createConfigProductAttribute.attribute_code$$"/> + </actionGroup> + <actionGroup ref="saveConfiguredProduct" stepKey="saveProductForm"/> + <!-- Check that product was added with implicit type change --> + <comment stepKey="beforeVerify" userInput="Verify Product Type Assigned Correctly"/> + <actionGroup ref="GoToProductCatalogPage" stepKey="goToProductCatalogPage"/> + <actionGroup ref="resetProductGridToDefaultView" stepKey="resetSearch"/> + <actionGroup ref="filterProductGridByName" stepKey="searchForProduct"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <see selector="{{AdminProductGridSection.productGridCell('2', 'Type')}}" userInput="Configurable Product" stepKey="seeProductTypeInGrid"/> + <actionGroup ref="AssertProductInStorefrontProductPage" stepKey="assertProductInStorefrontProductPage"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <actionGroup ref="VerifyOptionInProductStorefront" stepKey="verifyConfigurableOption"> + <argument name="attributeCode" value="$createConfigProductAttribute.default_frontend_label$"/> + <argument name="optionName" value="$createConfigProductAttributeOption1.option[store_labels][1][label]$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductBasedOnParentSkuTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductBasedOnParentSkuTest.xml new file mode 100644 index 000000000000..f4f607e9119b --- /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 000000000000..a7242b43c2b5 --- /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 000000000000..49f3f8b5ea93 --- /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 000000000000..925e7a890cea --- /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 000000000000..0b83fdc1788d --- /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 000000000000..e24ac07c30d1 --- /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 000000000000..51f4bf027994 --- /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 000000000000..1db9b3e5b79b --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsAssignedToCategoryTest.xml @@ -0,0 +1,154 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateConfigurableProductWithTwoOptionsAssignedToCategoryTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Create configurable product"/> + <title value="Create configurable product with two new options assigned to category with not visible child products"/> + <description value="Admin should be able to create configurable product with two new options, assigned to category, child products are not visible individually"/> + <testCaseId value="MC-13685"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create category --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!-- Delete configurable product --> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Delete children products --> + <actionGroup ref="deleteProductBySku" stepKey="deleteFirstChildProduct"> + <argument name="sku" value="{{colorConfigurableProductAttribute1.sku}}"/> + </actionGroup> + <actionGroup ref="deleteProductBySku" stepKey="deleteSecondChildProduct"> + <argument name="sku" value="{{colorConfigurableProductAttribute2.sku}}"/> + </actionGroup> + + <!-- Delete product attribute --> + <actionGroup ref="deleteProductAttributeByLabel" stepKey="deleteProductAttribute"> + <argument name="ProductAttribute" value="colorProductAttribute"/> + </actionGroup> + + <!-- Delete attribute set --> + <actionGroup ref="deleteAttributeSetByLabel" stepKey="deleteAttributeSet"> + <argument name="label" value="{{ProductAttributeFrontendLabel.label}}"/> + </actionGroup> + + <!-- Delete category --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Create configurable product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="goToCreateProductPage" stepKey="createConfigurableProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Fill configurable product values --> + <actionGroup ref="fillMainProductForm" stepKey="fillConfigurableProductValues"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Create product configurations --> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickCreateConfigurations" after="fillConfigurableProductValues"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}" time="30" stepKey="waitForConfigurationModalOpen" after="clickCreateConfigurations"/> + + <!--Create new attribute with two options --> + <actionGroup ref="addNewProductConfigurationAttribute" stepKey="createProductConfigurationAttribute"> + <argument name="attribute" value="colorProductAttribute"/> + <argument name="firstOption" value="colorConfigurableProductAttribute1"/> + <argument name="secondOption" value="colorConfigurableProductAttribute2"/> + </actionGroup> + + <!-- Change product configurations in grid --> + <actionGroup ref="changeProductConfigurationsInGrid" stepKey="changeProductConfigurationsInGrid"> + <argument name="firstOption" value="colorConfigurableProductAttribute1"/> + <argument name="secondOption" value="colorConfigurableProductAttribute2"/> + </actionGroup> + + <!-- Add configurable product to category --> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[$$createCategory.name$$]" stepKey="fillCategory" after="fillConfigurableProductValues"/> + + <!-- Save configurable product; add product to new attribute set --> + <actionGroup ref="saveConfigurableProductWithNewAttributeSet" stepKey="saveConfigurableProduct"/> + + <!-- Assert child products in grid --> + <actionGroup ref="viewProductInAdminGrid" stepKey="viewFirstChildProductInAdminGrid"> + <argument name="product" value="colorConfigurableProductAttribute1"/> + </actionGroup> + <actionGroup ref="viewProductInAdminGrid" stepKey="viewSecondChildProductInAdminGrid"> + <argument name="product" value="colorConfigurableProductAttribute2"/> + </actionGroup> + + <!-- Assert configurable product in grid --> + <actionGroup ref="filterProductGridBySkuAndName" stepKey="findCreatedConfigurableProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="{{ApiConfigurableProduct.type_id}}" stepKey="seeProductTypeInGrid"/> + <click selector="{{AdminProductGridFilterSection.clearFilters}}" stepKey="clickClearFiltersAfter"/> + + <!-- Flash cache --> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + + <!--Assert configurable product in category --> + <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <actionGroup ref="StorefrontCheckCategoryConfigurableProduct" stepKey="assertConfigurableProductInCategory"> + <argument name="product" value="ApiConfigurableProduct"/> + <argument name="optionProduct" value="colorConfigurableProductAttribute1"/> + </actionGroup> + + <!--Assert configurable product on product page --> + <amOnPage url="{{ApiConfigurableProduct.urlKey}}.html" stepKey="amOnProductPage" after="assertConfigurableProductInCategory"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="storefrontCheckConfigurableProductOptions" stepKey="checkConfigurableProductOptions"> + <argument name="product" value="ApiConfigurableProduct"/> + <argument name="firstOption" value="colorConfigurableProductAttribute1"/> + <argument name="secondOption" value="colorConfigurableProductAttribute2"/> + </actionGroup> + + <!-- Add configurable product to the cart with selected first option --> + <selectOption userInput="{{colorConfigurableProductAttribute1.name}}" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="selectOptionForAddingToCart"/> + <click selector="{{StorefrontProductInfoMainSection.AddToCart}}" stepKey="clickAddToCart"/> + <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" stepKey="waitForSuccessMessage"/> + + <!-- Assert configurable product in cart --> + <amOnPage url="/checkout/cart/" stepKey="amOnShoppingCartPage"/> + <waitForPageLoad stepKey="waitForShoppingCartPageLoad"/> + <actionGroup ref="StorefrontCheckCartConfigurableProductActionGroup" stepKey="storefrontCheckCartConfigurableProductActionGroup"> + <argument name="product" value="ApiConfigurableProduct"/> + <argument name="optionProduct" value="colorConfigurableProductAttribute1"/> + <argument name="productQuantity" value="CONST.one"/> + </actionGroup> + + <!-- Assert child products are not displayed separately: two next step --> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStoreFront"/> + <waitForPageLoad stepKey="waitForStoreFrontPageLoad"/> + + <!-- Quick search the storefront for the first attribute option --> + <submitForm selector="{{StorefrontQuickSearchSection.searchMiniForm}}" parameterArray="['q' => {{colorConfigurableProductAttribute1.sku}}]" stepKey="searchStorefrontFirstChildProduct"/> + <dontSee selector="{{StorefrontCategoryProductSection.ProductTitleByName(colorConfigurableProductAttribute1.name)}}" stepKey="dontSeeConfigurableProductFirstChild"/> + + <!-- Quick search the storefront for the second attribute option --> + <submitForm selector="{{StorefrontQuickSearchSection.searchMiniForm}}" parameterArray="['q' => {{colorConfigurableProductAttribute2.sku}}]" stepKey="searchStorefrontSecondChildProduct"/> + <dontSee selector="{{StorefrontCategoryProductSection.ProductTitleByName(colorConfigurableProductAttribute2.name)}}" stepKey="dontSeeConfigurableProductSecondChild"/> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsWithoutAssignedToCategoryTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsWithoutAssignedToCategoryTest.xml new file mode 100644 index 000000000000..934a410d58a8 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsWithoutAssignedToCategoryTest.xml @@ -0,0 +1,136 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateConfigurableProductWithTwoOptionsWithoutAssignedToCategoryTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Create configurable product"/> + <title value="Create configurable product with two new options without assigned to category with not visible child products"/> + <description value="Admin should be able to create configurable product with two options without assigned to category, child products are not visible individually"/> + <testCaseId value="MC-13686"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!-- Delete configurable product --> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Delete children products --> + <actionGroup ref="deleteProductBySku" stepKey="deleteFirstChildProduct"> + <argument name="sku" value="{{colorConfigurableProductAttribute1.sku}}"/> + </actionGroup> + <actionGroup ref="deleteProductBySku" stepKey="deleteSecondChildProduct"> + <argument name="sku" value="{{colorConfigurableProductAttribute2.sku}}"/> + </actionGroup> + + <!-- Delete product attribute --> + <actionGroup ref="deleteProductAttributeByLabel" stepKey="deleteProductAttribute"> + <argument name="ProductAttribute" value="colorProductAttribute"/> + </actionGroup> + + <!-- Delete attribute set --> + <actionGroup ref="deleteAttributeSetByLabel" stepKey="deleteAttributeSet"> + <argument name="label" value="{{ProductAttributeFrontendLabel.label}}"/> + </actionGroup> + + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Create configurable product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="goToCreateProductPage" stepKey="createConfigurableProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Fill configurable product values --> + <actionGroup ref="fillMainProductForm" stepKey="fillConfigurableProductValues"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!--Create product configurations--> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickCreateConfigurations" after="fillConfigurableProductValues"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}" time="30" stepKey="waitForConfigurationModalOpen" after="clickCreateConfigurations"/> + + <!--Create new attribute with two option --> + <actionGroup ref="addNewProductConfigurationAttribute" stepKey="createProductConfigurationAttribute"> + <argument name="attribute" value="colorProductAttribute"/> + <argument name="firstOption" value="colorConfigurableProductAttribute1"/> + <argument name="secondOption" value="colorConfigurableProductAttribute2"/> + </actionGroup> + + <!-- Change product configurations in grid --> + <actionGroup ref="changeProductConfigurationsInGrid" stepKey="changeProductConfigurationsInGrid"> + <argument name="firstOption" value="colorConfigurableProductAttribute1"/> + <argument name="secondOption" value="colorConfigurableProductAttribute2"/> + </actionGroup> + + <!-- Save configurable product; add product to new attribute set --> + <actionGroup ref="saveConfigurableProductWithNewAttributeSet" stepKey="saveConfigurableProduct"/> + + <!-- Assert Child Products in grid --> + <actionGroup ref="viewProductInAdminGrid" stepKey="viewFirstChildProductInAdminGrid"> + <argument name="product" value="colorConfigurableProductAttribute1"/> + </actionGroup> + <actionGroup ref="viewProductInAdminGrid" stepKey="viewSecondChildProductInAdminGrid"> + <argument name="product" value="colorConfigurableProductAttribute2"/> + </actionGroup> + + <!-- Assert Configurable Product in grid --> + <actionGroup ref="filterProductGridBySkuAndName" stepKey="findCreatedConfigurableProduct"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="{{ApiConfigurableProduct.type_id}}" stepKey="seeProductTypeInGrid"/> + <click selector="{{AdminProductGridFilterSection.clearFilters}}" stepKey="clickClearFiltersAfter"/> + + <!-- Flash cache --> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + + <!-- Assert configurable product on product page --> + <amOnPage url="{{ApiConfigurableProduct.urlKey}}.html" stepKey="amOnProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="storefrontCheckConfigurableProductOptions" stepKey="checkConfigurableProductOptions"> + <argument name="product" value="ApiConfigurableProduct"/> + <argument name="firstOption" value="colorConfigurableProductAttribute1"/> + <argument name="secondOption" value="colorConfigurableProductAttribute2"/> + </actionGroup> + + <!-- Add configurable product to the cart with selected first option --> + <selectOption userInput="{{colorConfigurableProductAttribute1.name}}" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="selectOptionForAddingToCart"/> + <click selector="{{StorefrontProductInfoMainSection.AddToCart}}" stepKey="clickAddToCart"/> + <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" stepKey="waitForSuccessMessage"/> + + <!-- Assert configurable product in cart --> + <amOnPage url="/checkout/cart/" stepKey="amOnShoppingCartPage"/> + <waitForPageLoad stepKey="waitForShoppingCartPageLoad"/> + <actionGroup ref="StorefrontCheckCartConfigurableProductActionGroup" stepKey="storefrontCheckCartConfigurableProductActionGroup"> + <argument name="product" value="ApiConfigurableProduct"/> + <argument name="optionProduct" value="colorConfigurableProductAttribute1"/> + <argument name="productQuantity" value="CONST.one"/> + </actionGroup> + + <!-- Assert child products are not displayed separately: two next step --> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStoreFront"/> + <waitForPageLoad stepKey="waitForStoreFrontPageLoad"/> + + <!-- Quick search the storefront for the first attribute option --> + <submitForm selector="{{StorefrontQuickSearchSection.searchMiniForm}}" parameterArray="['q' => {{colorConfigurableProductAttribute1.sku}}]" stepKey="searchStorefrontFirstChildProduct"/> + <dontSee selector="{{StorefrontCategoryProductSection.ProductTitleByName(colorConfigurableProductAttribute1.name)}}" stepKey="dontSeeConfigurableProductFirstChild"/> + + <!-- Quick search the storefront for the second attribute option --> + <submitForm selector="{{StorefrontQuickSearchSection.searchMiniForm}}" parameterArray="['q' => {{colorConfigurableProductAttribute2.sku}}]" stepKey="searchStorefrontSecondChildProduct"/> + <dontSee selector="{{StorefrontCategoryProductSection.ProductTitleByName(colorConfigurableProductAttribute2.name)}}" stepKey="dontSeeConfigurableProductSecondChild"/> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminDeleteConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminDeleteConfigurableProductTest.xml new file mode 100644 index 000000000000..fb2920be528b --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminDeleteConfigurableProductTest.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminDeleteConfigurableProductTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Delete products"/> + <title value="Delete configurable product test"/> + <description value="Admin should be able to delete a configurable product"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-11020"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="BaseConfigurableProduct" stepKey="createConfigurableProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteConfigurableProductFilteredBySkuAndName"> + <argument name="product" value="$$createConfigurableProduct$$"/> + </actionGroup> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="A total of 1 record(s) have been deleted." stepKey="deleteMessage"/> + <!-- Verify product on Product Page --> + <amOnPage url="{{StorefrontProductPage.url($$createConfigurableProduct.name$$)}}" stepKey="amOnConfigurableProductPage"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> + <!-- Search for the product by sku --> + <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createConfigurableProduct.sku$$" stepKey="fillSearchBarByProductSku"/> + <waitForPageLoad stepKey="waitForSearchButton"/> + <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForSearchResults"/> + <!-- Should not see any search results --> + <dontSee userInput="$$createConfigurableProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> + <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> + <!-- Go to the category page that we created in the before block --> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> + <!-- Should not see the product --> + <dontSee userInput="$$createConfigurableProduct.name$$" selector="{{StorefrontCategoryMainSection.productsList}}" stepKey="dontSeeProductInCategory"/> + <see selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" userInput="We can't find products matching the selection." stepKey="seeEmptyProductMessage"/> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdvanceCatalogSearchConfigurableTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdvanceCatalogSearchConfigurableTest.xml index 454f9f5f29a7..c303e4d19db8 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdvanceCatalogSearchConfigurableTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdvanceCatalogSearchConfigurableTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdvanceCatalogSearchConfigurableByNameTest" extends="AdvanceCatalogSearchSimpleProductByNameTest"> <annotations> <features value="ConfigurableProduct"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductPriceAdditionalStoreViewTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductPriceAdditionalStoreViewTest.xml index b06067a6d43e..232dfe839146 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductPriceAdditionalStoreViewTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductPriceAdditionalStoreViewTest.xml @@ -16,9 +16,7 @@ <description value="Configurable product price should not disappear for additional stores on frontEnd if disabled for default store"/> <severity value="CRITICAL"/> <testCaseId value="MAGETWO-92247"/> - <skip> - <issueId value="MAGETWO-92247"/> - </skip> + <group value="ConfigurableProduct"/> </annotations> <before> @@ -133,12 +131,13 @@ <click selector="{{AdminProductFormActionSection.changeStoreButton}}" stepKey="clickStoreviewSwitcher"/> <click selector="{{AdminProductFormActionSection.selectStoreView('Second Store View')}}" stepKey="chooseStoreView"/> <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="acceptStoreSwitchingMessage"/> - <!--<waitForPageLoad stepKey="waitForStoreViewSwitched"/>--> <waitForPageLoad time="30" stepKey="waitForPageLoad9"/> <see userInput="Second Store View" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewStoreViewName"/> <!-- enable the config product for the second store --> - <waitForElementVisible selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="waitForProductEnableSlider"/> + <waitForElementVisible selector="{{AdminProductFormSection.productStatusUseDefault}}" stepKey="waitForDefaultValueCheckBox"/> + <click selector="{{AdminProductFormSection.productStatusUseDefault}}" stepKey="unCheckUseDefaultValueCheckBox"/> + <waitForElementVisible selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="waitForProductEnableSlider"/> <click selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="enableProductForSecondStoreView"/> <seeCheckboxIsChecked selector="{{AdminProductFormSection.productStatus}}" stepKey="seeThatProductIsEnabled"/> <actionGroup ref="AdminFormSaveAndClose" stepKey="enabledConfigProductSecondStore"/> @@ -151,7 +150,6 @@ <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="acceptStoreSwitchingMessage1"/> <waitForPageLoad time="30" stepKey="waitForPageLoad8"/> <see userInput="Second Store View" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewStoreViewName1"/> - <click selector="{{AdminProductFormConfigurationsSection.actionsBtn('1')}}" stepKey="clickToExpandActionsForFirstVariation2"/> <click selector="{{AdminProductFormConfigurationsSection.enableProductBtn}}" stepKey="clickEnableChildProduct1"/> <click selector="{{AdminProductFormConfigurationsSection.actionsBtn('2')}}" stepKey="clickToExpandActionsForSecondVariation2"/> @@ -181,7 +179,6 @@ <waitForPageLoad time="30" stepKey="waitForStoreViewSwitchedP1"/> <see userInput="Second Store View" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewStoreViewNameP1"/> <waitForElementVisible selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="waitForProductEnableSliderP1"/> - <click selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="enableProductForSecondStoreViewP1"/> <seeCheckboxIsChecked selector="{{AdminProductFormSection.productStatus}}" stepKey="seeThatProduct1IsEnabled"/> <actionGroup ref="AdminFormSaveAndClose" stepKey="save2UpdatedChild1"/> @@ -201,7 +198,6 @@ <waitForPageLoad time="30" stepKey="waitForStoreViewSwitchedP2"/> <see userInput="Second Store View" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewStoreViewNameP2"/> <waitForElementVisible selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="waitForProductEnableSliderP2"/> - <click selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="enableProductForSecondStoreViewP2"/> <seeCheckboxIsChecked selector="{{AdminProductFormSection.productStatus}}" stepKey="seeThatProduct2IsEnabled"/> <actionGroup ref="AdminFormSaveAndClose" stepKey="save2UpdatedChild2"/> @@ -211,6 +207,6 @@ <click userInput="$$createCategory.name$$" stepKey="clickOnCategoryName"/> <waitForPageLoad stepKey="waitForPageLoad4"/> <see userInput="$$createConfigProduct.name$$" stepKey="assertProductPresent"/> - <dontSee userInput="$$createConfigChildProduct1.price$$" stepKey="assertProductPricePresent"/> + <See userInput="$$createConfigChildProduct1.price$$" stepKey="assertProductPricePresent"/> </test> </tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ProductsQtyReturnAfterOrderCancelTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ProductsQtyReturnAfterOrderCancelTest.xml new file mode 100644 index 000000000000..456be43f80b8 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ProductsQtyReturnAfterOrderCancelTest.xml @@ -0,0 +1,99 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="ProductsQtyReturnAfterOrderCancel"> + + <annotations> + <features value="ConfigurableProduct"/> + <title value="Product qunatity return after order cancel"/> + <description value="Check Product qunatity return after order cancel"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-97228"/> + <useCaseId value="MAGETWO-82221"/> + <group value="ConfigurableProduct"/> + </annotations> + + <before> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <actionGroup ref="logout" stepKey="amOnLogoutPage"/> + </after> + + <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="GoToCatalogProductPage1"/> + + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct"> + <argument name="product" value="$$createConfigProduct$$"/> + </actionGroup> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="100" stepKey="changeProductQuantity"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveChanges"/> + <waitForPageLoad stepKey="waitProductGridToBeLoaded"/> + + <amOnPage url="$$createConfigProduct.sku$$.html" stepKey="navigateToProductPage"/> + <waitForPageLoad stepKey="waitForProductPage"/> + <fillField selector="{{StorefrontProductInfoMainSection.qty}}" userInput="4" stepKey="fillQuantity"/> + + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$$createConfigProduct.name$$"/> + </actionGroup> + + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShippingSection"> + <argument name="customerVar" value="CustomerEntityOne"/> + <argument name="customerAddressVar" value="CustomerAddressSimple"/> + </actionGroup> + + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="placeOrder"> + <argument name="orderNumberMessage" value="CONST.successGuestCheckoutOrderNumberMessage"/> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage" /> + </actionGroup> + + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> + + <actionGroup ref="filterOrderGridById" stepKey="filterOrderGridById"> + <argument name="orderId" value="$grabOrderNumber"/> + </actionGroup> + + <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> + <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceButton"/> + <waitForPageLoad stepKey="waitForNewInvoicePageLoad"/> + <fillField selector="{{AdminInvoiceItemsSection.qtyToInvoiceColumn}}" userInput="1" stepKey="ChangeQtyToInvoice"/> + <click selector="{{AdminInvoiceItemsSection.updateQty}}" stepKey="updateQunatity"/> + <waitForPageLoad stepKey="waitPageToBeLoaded"/> + <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + <click selector="{{AdminOrderDetailsMainActionsSection.ship}}" stepKey="clickShipAction"/> + <waitForPageLoad stepKey="waitOrderDetailToLoad"/> + <fillField selector="{{AdminShipmentItemsSection.itemQtyToShip('1')}}" userInput="1" stepKey="changeItemQtyToShip"/> + <click selector="{{AdminShipmentMainActionsSection.submitShipment}}" stepKey="clickSubmitShipment"/> + <waitForPageLoad stepKey="waitShipmentSectionToLoad"/> + <actionGroup ref="cancelPendingOrder" stepKey="cancelPendingOption"> + <argument name="orderStatus" value="Complete"/> + </actionGroup> + + <see selector="{{AdminOrderItemsOrderedSection.itemQty('1')}}" userInput="Canceled 3" stepKey="seeCanceledQuantity"/> + + <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="GoToCatalogProductPage"/> + + <actionGroup ref="filterProductGridBySku2" stepKey="filterProductGridBySku"> + <argument name="sku" value="$$createConfigProduct.sku$$"/> + </actionGroup> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Quantity')}}" userInput="99" stepKey="seeProductSkuInGrid"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductFilters"/> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderFilters"/> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductCategoryViewChildOnlyTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductCategoryViewChildOnlyTest.xml new file mode 100644 index 000000000000..1959551f8de2 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductCategoryViewChildOnlyTest.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="StorefrontConfigurableProductCategoryViewChildOnlyTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="View configurable product child in storefront"/> + <title value="It should be possible to only view the child product of a configurable product"/> + <description value="Create configurable product, add to category such that only child variation is visible in category"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-5832"/> + <group value="ConfigurableProduct"/> + </annotations> + <before> + <!-- Create the category --> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiCategory" stepKey="secondCategory"/> + + <!-- 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 we just created to default attribute set --> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + + <!-- Get the first option of the attribute we created --> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Get the second option of the attribute we created --> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create the configurable product and add it to the category --> + <createData entity="ApiConfigurableProduct" 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> + + <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> + <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + <deleteData createDataKey="createCategory" stepKey="deleteApiCategory"/> + <deleteData createDataKey="secondCategory" stepKey="deleteSecondCategory"/> + </after> + + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + + <!-- Go to the product page for the first product --> + <amOnPage stepKey="goToProductGrid" url="{{ProductCatalogPage.url}}"/> + <waitForPageLoad stepKey="waitForProductGridLoad"/> + <actionGroup stepKey="searchForSimpleProduct" ref="filterProductGridBySku2"> + <argument name="sku" value="$$createConfigChildProduct1.sku$$"/> + </actionGroup> + <actionGroup stepKey="openProductEditPage" ref="openProducForEditByClickingRowXColumnYInProductGrid"/> + <!-- Edit the visibility the first simple product --> + <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="Catalog, Search" stepKey="selectVisibilityCatalogSearch"/> + <!--Add to category--> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[$$secondCategory.name$$]" stepKey="addProductToCategory"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="assertSaveMessageSuccess"/> + + <!-- Go to storefront to view child product --> + <amOnPage stepKey="goToStoreFront" url="{{StorefrontHomePage.url}}"/> + <waitForPageLoad stepKey="waitForStorefront"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$secondCategory.name$$)}}" stepKey="goToCategoryStorefront"/> + <waitForPageLoad stepKey="waitForStorefrontCategory"/> + <seeElement selector="{{StorefrontCategoryProductSection.ProductTitleByName($$createConfigChildProduct1.name$$)}}" stepKey="seeChildProductInCategory"/> + <dontSeeElement selector="{{StorefrontCategoryProductSection.ProductTitleByName($$createConfigChildProduct2.name$$)}}" stepKey="dontSeeOtherChildProduct"/> + <dontSeeElement selector="{{StorefrontCategoryProductSection.ProductTitleByName($$createConfigProduct.name$$)}}" stepKey="dontSeeParentProduct"/> + <click selector="{{StorefrontCategoryProductSection.ProductTitleByName($$createConfigChildProduct1.name$$)}}" stepKey="clickProductName"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <seeInCurrentUrl url="$$createConfigChildProduct1.custom_attributes[url_key]$$" stepKey="seeProductPageIsAccessible"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productName($$createConfigChildProduct1.name$$)}}" stepKey="seeProductNameOnProductPage"/> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml index 231ef553d2d4..1075f79aef18 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml @@ -144,18 +144,15 @@ <!-- Quick search the storefront for the first attribute option --> <amOnPage stepKey="goToStoreFront" url="{{StorefrontHomePage.url}}"/> <waitForPageLoad stepKey="waitForStorefront"/> - <fillField stepKey="searchStorefront1" selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createConfigProductAttributeSelectOption1.option[store_labels][0][label]$$"/> - <click stepKey="clickSearch1" selector="{{StorefrontQuickSearchSection.searchButton}}"/> + <submitForm selector="#search_mini_form" parameterArray="['q' => $$createConfigProductAttributeSelectOption1.option[store_labels][0][label]$$]" stepKey="searchStorefront1" /> <seeElement stepKey="seeProduct1" selector="{{StorefrontCategoryProductSection.ProductTitleByName('$$createConfigProduct.name$$')}}"/> <!-- Quick search the storefront for the second attribute option --> - <fillField stepKey="searchStorefront2" selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createConfigProductAttributeOption1Multiselect.option[store_labels][0][label]$$"/> - <click stepKey="clickSearch2" selector="{{StorefrontQuickSearchSection.searchButton}}"/> + <submitForm selector="#search_mini_form" parameterArray="['q' => $$createConfigProductAttributeOption1Multiselect.option[store_labels][0][label]$$]" stepKey="searchStorefront2" /> <seeElement stepKey="seeProduct2" selector="{{StorefrontCategoryProductSection.ProductTitleByName('$$createConfigProduct.name$$')}}"/> <!-- Quick search the storefront for the first product description --> - <fillField stepKey="searchStorefront3" selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="'$$createConfigChildProduct1.custom_attributes[short_description]$$'"/> - <click stepKey="clickSearch3" selector="{{StorefrontQuickSearchSection.searchButton}}"/> + <submitForm selector="#search_mini_form" parameterArray="['q' => $$createConfigChildProduct1.custom_attributes[short_description]$$]" stepKey="searchStorefront3" /> <seeElement stepKey="seeProduct3" selector="{{StorefrontCategoryProductSection.ProductTitleByName('$$createConfigProduct.name$$')}}"/> </test> </tests> \ No newline at end of file diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml index 7cf352dcb61c..d890d5985811 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml @@ -17,7 +17,6 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-93059"/> <group value="ConfigurableProduct"/> - <group value="skip"/><!-- MAGETWO-94170 --> </annotations> <before> @@ -41,7 +40,7 @@ <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> <!--Go to storefront--> - <amOnPage url="" stepKey="goToHomePage"/> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToHomePage"/> <waitForPageLoad stepKey="waitForHomePageLoad"/> <click selector="{{StorefrontNavigationSection.topCategory($$createCategory.name$$)}}" stepKey="goToCategoryStorefront"/> <waitForPageLoad stepKey="waitForCategoryPageLoad"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml new file mode 100644 index 000000000000..57c45ee1e899 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml @@ -0,0 +1,167 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontSortingByPriceForConfigurableProductWithCatalogRuleAppliedTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="View soting by price in storefront"/> + <title value="Sorting by price for Configurable with Catalog Rule applied"/> + <description value="Sort by price should be correct if the apply Catalog Rule to child product of configurable product"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-69988"/> + <skip> + <issueId value="MC-5777"/> + </skip> + <group value="сonfigurable_product"/> + </annotations> + <before> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">5.00</field> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProduct2"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">10.00</field> + </createData> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption3" stepKey="createConfigProductAttributeOption3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="3" stepKey="getConfigAttributeOption3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <field key="price">15.00</field> + </createData> + <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + <field key="price">20.00</field> + </createData> + <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption3"/> + <field key="price">25.00</field> + </createData> + <createData entity="ConfigurableProductThreeOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + <requiredEntity createDataKey="getConfigAttributeOption3"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild3"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct3"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--SKU Product Attribute is enabled for Promo Rule Conditions--> + <actionGroup ref="navigateToEditProductAttribute" stepKey="navigateToSkuProductAttribute"> + <argument name="ProductAttribute" value="sku"/> + </actionGroup> + <actionGroup ref="changeUseForPromoRuleConditionsProductAttribute" stepKey="changeUseForPromoRuleConditionsProductAttributeToYes"> + <argument name="option" value="Yes"/> + </actionGroup> + <magentoCLI command="indexer:reindex" stepKey="reindex1"/> + <magentoCLI command="cache:flush" stepKey="flushCache1"/> + </before> + + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createSimpleProduct2" stepKey="deleteSimpleProduct2"/> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> + <deleteData createDataKey="createConfigChildProduct3" stepKey="deleteConfigChildProduct3"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + + <!--SKU Product Attribute is disable for Promo Rule Conditions--> + <actionGroup ref="navigateToEditProductAttribute" stepKey="navigateToSkuProductAttribute"> + <argument name="ProductAttribute" value="sku"/> + </actionGroup> + <actionGroup ref="changeUseForPromoRuleConditionsProductAttribute" stepKey="changeUseForPromoRuleConditionsProductAttributeToNo"> + <argument name="option" value="No"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + + <!--Open category with products and Sort by price desc--> + <actionGroup ref="GoToStorefrontCategoryPageByParameters" stepKey="goToStorefrontCategoryPage"> + <argument name="category" value="$$createCategory.custom_attributes[url_key]$$"/> + <argument name="mode" value="grid"/> + <argument name="numOfProductsPerPage" value="25"/> + <argument name="sortBy" value="price"/> + <argument name="sort" value="desc"/> + </actionGroup> + <see selector="{{StorefrontCategoryMainSection.lineProductName('1')}}" userInput="$$createConfigProduct.name$$" stepKey="seeConfigurableProduct"/> + <see selector="{{StorefrontCategoryMainSection.lineProductName('2')}}" userInput="$$createSimpleProduct2.name$$" stepKey="seeSimpleProductTwo"/> + <see selector="{{StorefrontCategoryMainSection.lineProductName('3')}}" userInput="$$createSimpleProduct.name$$" stepKey="seeSimpleProduct"/> + + <!--Create and apply catalog price rule--> + <actionGroup ref="newCatalogPriceRuleByUIWithConditionIsSKU" stepKey="createCatalogPriceRule"> + <argument name="catalogRule" value="CatalogRuleByPercentWith96Amount" /> + <argument name="productSku" value="$$createConfigChildProduct3.sku$$" /> + </actionGroup> + <actionGroup ref="selectNotLoggedInCustomerGroup" stepKey="selectNotLoggedInCustomerGroup"/> + <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="clickSaveAndApplyRules"/> + + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + + <!--Reopen category with products and Sort by price desc--> + <actionGroup ref="GoToStorefrontCategoryPageByParameters" stepKey="goToStorefrontCategoryPage2"> + <argument name="category" value="$$createCategory.custom_attributes[url_key]$$"/> + <argument name="mode" value="grid"/> + <argument name="numOfProductsPerPage" value="9"/> + <argument name="sortBy" value="price"/> + <argument name="sort" value="desc"/> + </actionGroup> + <see selector="{{StorefrontCategoryMainSection.lineProductName('1')}}" userInput="$$createConfigProduct.name$$" stepKey="seeConfigurableProduct2"/> + <see selector="{{StorefrontCategoryMainSection.lineProductName('2')}}" userInput="$$createSimpleProduct2.name$$" stepKey="seeSimpleProductTwo2"/> + <see selector="{{StorefrontCategoryMainSection.lineProductName('3')}}" userInput="$$createSimpleProduct.name$$" stepKey="seeSimpleProduct2"/> + + <!-- Delete the rule --> + <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToPriceRulePage"/> + <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRule"> + <argument name="name" value="{{CatalogRuleByPercentWith96Amount.name}}"/> + <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> + </actionGroup> + </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 000000000000..bb69122dc0be --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml @@ -0,0 +1,149 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontVerifyConfigurableProductLayeredNavigationTest"> + <annotations> + <stories value="Create configurable product"/> + <title value="Out of stock configurable attribute option doesn't show in Layered Navigation"/> + <description value=" Login as admin and verify out of stock configurable attribute option doesn't show in Layered Navigation"/> + <testCaseId value="MC-13734"/> + <severity value="CRITICAL"/> + <group value="ConfigurableProduct"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <!-- Login as Admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + + <!-- Create Default Category --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + + <!-- Create an attribute with three options --> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption3" stepKey="createConfigProductAttributeOption3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + + <!-- Add the attribute just created to default attribute set --> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + + <!-- Get the first option of the attribute created --> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Get the second option of the attribute created --> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Get the third option of the attribute created --> + <getData entity="ProductAttributeOptionGetter" index="3" stepKey="getConfigAttributeOption3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create Configurable product --> + <createData entity="BaseConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Create a simple product and give it the attribute with the first option --> + <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + + <!--Create a simple product and give it the attribute with the second option --> + <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + + <!--Create a simple product and give it the attribute with the Third option --> + <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption3"/> + </createData> + + <!-- Create the configurable product --> + <createData entity="ConfigurableProductThreeOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + <requiredEntity createDataKey="getConfigAttributeOption3"/> + </createData> + + <!-- Add the first simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + + <!-- Add the second simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + + <!-- Add the third simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild3"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct3"/> + </createData> + </before> + <after> + <!-- Delete Created Data --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> + <deleteData createDataKey="createConfigChildProduct3" stepKey="deleteConfigChildProduct3"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteAttribute"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Open Product Index Page and Filter First Child product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + <actionGroup ref="filterProductGridBySku" stepKey="filterProduct"> + <argument name="product" value="ApiSimpleOne"/> + </actionGroup> + + <!-- Change the First Child Product stock status as 'Out Of Stock'--> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="selectFirstRow"/> + <waitForPageLoad stepKey="waitForProductPageToLoad"/> + <scrollTo selector="{{AdminProductFormSection.productQuantity}}" stepKey="scrollToProductQuantity"/> + <selectOption selector="{{AdminProductFormSection.productStockStatus}}" userInput="Out of Stock" stepKey="disableProduct"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> + + <!--Open Category in Store Front and select product attribute option from sidebar --> + <actionGroup ref="SelectStorefrontSideBarAttributeOption" stepKey="selectStorefrontProductAttributeOption"> + <argument name="categoryName" value="$$createCategory.name$$"/> + <argument name="attributeDefaultLabel" value="$$createConfigProductAttribute.default_value$$"/> + </actionGroup> + + <!--Assert Out Of Stock product is not visible in Storefront Page --> + <dontSee selector="{{StorefrontCategorySidebarSection.filterOption}}" userInput="$$getConfigAttributeOption1.label$$" stepKey="dontSeeOption1"/> + <see selector="{{StorefrontCategorySidebarSection.filterOption}}" userInput="$$getConfigAttributeOption2.label$$" stepKey="seeOption2"/> + <see selector="{{StorefrontCategorySidebarSection.filterOption}}" userInput="$$getConfigAttributeOption3.label$$" stepKey="seeOption3"/> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Adminhtml/Product/Edit/Button/SaveTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Adminhtml/Product/Edit/Button/SaveTest.php index 8df6df53cc06..2d73b61245a4 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Adminhtml/Product/Edit/Button/SaveTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Adminhtml/Product/Edit/Button/SaveTest.php @@ -38,7 +38,7 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); $this->productMock = $this->getMockBuilder(ProductInterface::class) - ->setMethods(['isReadonly', 'isDuplicable']) + ->setMethods(['isReadonly', 'isDuplicable', 'isComposite']) ->getMockForAbstractClass(); $this->registryMock->expects(static::any()) diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php index 25d8412c9105..c5c2368720b9 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php @@ -379,6 +379,9 @@ private function getExpectedArray($productId, $amount, $priceQty, $percentage): 'percentage' => $percentage, ], ], + 'msrpPrice' => [ + 'amount' => null , + ] ], ], 'priceFormat' => [], diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/Frontend/ProductIdentitiesExtenderTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/Frontend/ProductIdentitiesExtenderTest.php new file mode 100644 index 000000000000..b4fb5ccfaa55 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/Frontend/ProductIdentitiesExtenderTest.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Test\Unit\Model\Plugin\Frontend; + +use Magento\ConfigurableProduct\Model\Plugin\Frontend\ProductIdentitiesExtender; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Catalog\Model\Product; + +/** + * Class ProductIdentitiesExtenderTest + */ +class ProductIdentitiesExtenderTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject|Configurable + */ + private $configurableTypeMock; + + /** + * @var ProductIdentitiesExtender + */ + private $plugin; + + /** @var MockObject|\Magento\Catalog\Model\Product */ + private $product; + + protected function setUp() + { + $this->product = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->setMethods(['getId']) + ->getMock(); + + $this->configurableTypeMock = $this->getMockBuilder(Configurable::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->plugin = new ProductIdentitiesExtender($this->configurableTypeMock); + } + + public function testAfterGetIdentities() + { + $identities = [ + 'SomeCacheId', + 'AnotherCacheId', + ]; + $productId = 12345; + $childIdentities = [ + 0 => [1, 2, 5, 100500] + ]; + $expectedIdentities = [ + 'SomeCacheId', + 'AnotherCacheId', + Product::CACHE_TAG . '_' . 1, + Product::CACHE_TAG . '_' . 2, + Product::CACHE_TAG . '_' . 5, + Product::CACHE_TAG . '_' . 100500, + ]; + + $this->product->expects($this->once()) + ->method('getId') + ->willReturn($productId); + + $this->configurableTypeMock->expects($this->once()) + ->method('getChildrenIds') + ->with($productId) + ->willReturn($childIdentities); + + $productIdentities = $this->plugin->afterGetIdentities($this->product, $identities); + $this->assertEquals($expectedIdentities, $productIdentities); + } +} diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Configuration/Item/ItemProductResolverTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Configuration/Item/ItemProductResolverTest.php new file mode 100644 index 000000000000..8dac2dee10d3 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Configuration/Item/ItemProductResolverTest.php @@ -0,0 +1,143 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Test\Unit\Model\Product\Configuration\Item; + +use Magento\Catalog\Model\Config\Source\Product\Thumbnail; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Configuration\Item\ItemInterface; +use Magento\Catalog\Model\Product\Configuration\Item\Option\OptionInterface; +use Magento\ConfigurableProduct\Model\Product\Configuration\Item\ItemProductResolver; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Quote\Model\Quote\Item\Option; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class ItemProductResolverTest extends TestCase +{ + /** @var ItemProductResolver */ + private $model; + /** @var ItemInterface | MockObject */ + private $item; + /** @var Product | MockObject */ + private $parentProduct; + /** @var ScopeConfigInterface | MockObject */ + private $scopeConfig; + /** @var OptionInterface | MockObject */ + private $option; + /** @var Product | MockObject */ + private $childProduct; + + /** + * Set up method + */ + protected function setUp() + { + parent::setUp(); + + $this->scopeConfig = $this->getMockBuilder(ScopeConfigInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->parentProduct = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->getMock(); + $this->parentProduct + ->method('getSku') + ->willReturn('parent_product'); + + $this->childProduct = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->getMock(); + $this->childProduct + ->method('getSku') + ->willReturn('child_product'); + + $this->option = $this->getMockBuilder(Option::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->option + ->method('getProduct') + ->willReturn($this->childProduct); + + $this->item = $this->getMockBuilder(ItemInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->item + ->expects($this->once()) + ->method('getProduct') + ->willReturn($this->parentProduct); + + $this->model = new ItemProductResolver($this->scopeConfig); + } + + /** + * Test for deleted child product from configurable product + */ + public function testGetFinalProductChildIsNull(): void + { + $this->scopeConfig->expects($this->never())->method('getValue'); + $this->childProduct->expects($this->never())->method('getData'); + + $this->item->expects($this->once()) + ->method('getOptionByCode') + ->willReturn(null); + + $finalProduct = $this->model->getFinalProduct($this->item); + $this->assertEquals( + $this->parentProduct->getSku(), + $finalProduct->getSku() + ); + } + + /** + * Tests child product from configurable product + * + * @dataProvider provideScopeConfig + * @param string $expectedSku + * @param string $scopeValue + * @param string | null $thumbnail + */ + public function testGetFinalProductChild($expectedSku, $scopeValue, $thumbnail): void + { + $this->item->expects($this->once()) + ->method('getOptionByCode') + ->willReturn($this->option); + + $this->childProduct + ->expects($this->once()) + ->method('getData') + ->willReturn($thumbnail); + + $this->scopeConfig->expects($this->once()) + ->method('getValue') + ->willReturn($scopeValue); + + $finalProduct = $this->model->getFinalProduct($this->item); + $this->assertEquals($expectedSku, $finalProduct->getSku()); + } + + /** + * Dataprovider for scope test + * @return array + */ + public function provideScopeConfig(): array + { + return [ + ['child_product', Thumbnail::OPTION_USE_OWN_IMAGE, 'thumbnail'], + ['parent_product', Thumbnail::OPTION_USE_PARENT_IMAGE, 'thumbnail'], + + ['parent_product', Thumbnail::OPTION_USE_OWN_IMAGE, null], + ['parent_product', Thumbnail::OPTION_USE_OWN_IMAGE, 'no_selection'], + + ['parent_product', Thumbnail::OPTION_USE_PARENT_IMAGE, null], + ['parent_product', Thumbnail::OPTION_USE_PARENT_IMAGE, 'no_selection'], + ]; + } +} 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 64b9b3776442..ab52d4eb8602 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/Model/Product/Type/ConfigurableTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php index 5e9399ddd3d6..c351d12fa813 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php @@ -11,10 +11,13 @@ use Magento\Catalog\Model\Product\Configuration\Item\Option\OptionInterface; use Magento\ConfigurableProduct\Model\Product\Type\Collection\SalableProcessor; use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute; use Magento\ConfigurableProduct\Model\Product\Type\Configurable\AttributeFactory; use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Attribute\Collection; use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Attribute\CollectionFactory; use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\Collection as ProductCollection; +use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\CollectionFactory + as ProductCollectionFactory; use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\ConfigurableFactory; use Magento\Customer\Model\Session; use Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend; @@ -153,9 +156,7 @@ protected function setUp() ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $this->productCollectionFactory = $this->getMockBuilder( - \Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\CollectionFactory::class - ) + $this->productCollectionFactory = $this->getMockBuilder(ProductCollectionFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); @@ -197,11 +198,6 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $this->productFactory = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterfaceFactory::class) - ->setMethods(['create']) - ->disableOriginalConstructor() - ->getMock(); - $this->salableProcessor = $this->createMock(SalableProcessor::class); $this->model = $this->objectHelper->getObject( @@ -286,9 +282,7 @@ public function testSave() $product->expects($this->atLeastOnce()) ->method('getData') ->willReturnMap($dataMap); - $attribute = $this->getMockBuilder( - \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute::class - ) + $attribute = $this->getMockBuilder(Attribute::class) ->disableOriginalConstructor() ->setMethods(['addData', 'setStoreId', 'setProductId', 'save', '__wakeup', '__sleep']) ->getMock(); @@ -385,7 +379,7 @@ public function testGetUsedProducts() ['_cache_instance_used_product_attributes', null, []] ] ); - + $this->catalogConfig->expects($this->any())->method('getProductAttributes')->willReturn([]); $productCollection->expects($this->atLeastOnce())->method('addAttributeToSelect')->willReturnSelf(); $productCollection->expects($this->once())->method('setProductFilter')->willReturnSelf(); $productCollection->expects($this->atLeastOnce())->method('setFlag')->willReturnSelf(); @@ -470,9 +464,7 @@ public function testGetConfigurableAttributesAsArray($productStore) $eavAttribute->expects($this->once())->method('getSource')->willReturn($attributeSource); $eavAttribute->expects($this->atLeastOnce())->method('getStoreLabel')->willReturn('Store Label'); - $attribute = $this->getMockBuilder( - \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute::class - ) + $attribute = $this->getMockBuilder(Attribute::class) ->disableOriginalConstructor() ->setMethods(['getProductAttribute', '__wakeup', '__sleep']) ->getMock(); @@ -515,17 +507,34 @@ public function getConfigurableAttributesAsArrayDataProvider() ]; } + public function testGetConfigurableAttributesNewProduct() + { + $configurableAttributes = '_cache_instance_configurable_attributes'; + + /** @var \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject $product */ + $product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) + ->setMethods(['hasData', 'getId']) + ->disableOriginalConstructor() + ->getMock(); + + $product->expects($this->once())->method('hasData')->with($configurableAttributes)->willReturn(false); + $product->expects($this->once())->method('getId')->willReturn(null); + + $this->assertEquals([], $this->model->getConfigurableAttributes($product)); + } + public function testGetConfigurableAttributes() { $configurableAttributes = '_cache_instance_configurable_attributes'; /** @var \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject $product */ $product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) - ->setMethods(['getData', 'hasData', 'setData']) + ->setMethods(['getData', 'hasData', 'setData', 'getId']) ->disableOriginalConstructor() ->getMock(); $product->expects($this->once())->method('hasData')->with($configurableAttributes)->willReturn(false); + $product->expects($this->once())->method('getId')->willReturn(1); $attributeCollection = $this->getMockBuilder(Collection::class) ->setMethods(['setProductFilter', 'orderByPosition', 'load']) @@ -581,9 +590,7 @@ public function testHasOptionsConfigurableAttribute() ->setMethods(['__wakeup', 'getAttributeCode', 'getOptions', 'hasData', 'getData']) ->disableOriginalConstructor() ->getMock(); - $attributeMock = $this->getMockBuilder( - \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute::class - ) + $attributeMock = $this->getMockBuilder(Attribute::class) ->disableOriginalConstructor() ->getMock(); @@ -689,7 +696,7 @@ function ($value) { ->disableOriginalConstructor() ->getMock(); $usedAttributeMock = $this->getMockBuilder( - \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute::class + Attribute::class ) ->setMethods(['getProductAttribute']) ->disableOriginalConstructor() diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/ResourceModel/Attribute/OptionSelectBuilderTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/ResourceModel/Attribute/OptionSelectBuilderTest.php index 9802c97372bb..537288618b64 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/ResourceModel/Attribute/OptionSelectBuilderTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/ResourceModel/Attribute/OptionSelectBuilderTest.php @@ -112,8 +112,8 @@ public function testGetSelect() { $this->select->expects($this->exactly(1))->method('from')->willReturnSelf(); $this->select->expects($this->exactly(1))->method('columns')->willReturnSelf(); - $this->select->expects($this->exactly(6))->method('joinInner')->willReturnSelf(); - $this->select->expects($this->exactly(3))->method('joinLeft')->willReturnSelf(); + $this->select->expects($this->exactly(5))->method('joinInner')->willReturnSelf(); + $this->select->expects($this->exactly(4))->method('joinLeft')->willReturnSelf(); $this->select->expects($this->exactly(1))->method('order')->willReturnSelf(); $this->select->expects($this->exactly(2))->method('where')->willReturnSelf(); @@ -156,8 +156,8 @@ public function testGetSelectWithBackendModel() { $this->select->expects($this->exactly(1))->method('from')->willReturnSelf(); $this->select->expects($this->exactly(0))->method('columns')->willReturnSelf(); - $this->select->expects($this->exactly(6))->method('joinInner')->willReturnSelf(); - $this->select->expects($this->exactly(1))->method('joinLeft')->willReturnSelf(); + $this->select->expects($this->exactly(5))->method('joinInner')->willReturnSelf(); + $this->select->expects($this->exactly(2))->method('joinLeft')->willReturnSelf(); $this->select->expects($this->exactly(1))->method('order')->willReturnSelf(); $this->select->expects($this->exactly(2))->method('where')->willReturnSelf(); diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Catalog/Model/Product/Pricing/Renderer/SalableResolverTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Catalog/Model/Product/Pricing/Renderer/SalableResolverTest.php new file mode 100644 index 000000000000..7ec2ce370ac5 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Catalog/Model/Product/Pricing/Renderer/SalableResolverTest.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\ConfigurableProduct\Test\Unit\Plugin\Catalog\Model\Product\Pricing\Renderer; + +use Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolver; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable as TypeConfigurable; +use Magento\ConfigurableProduct\Plugin\Catalog\Model\Product\Pricing\Renderer\SalableResolver as SalableResolverPlugin; +use Magento\Framework\Pricing\SaleableInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class SalableResolverTest extends TestCase +{ + /** + * @var TypeConfigurable|MockObject + */ + private $typeConfigurable; + + /** + * @var SalableResolverPlugin + */ + private $salableResolver; + + protected function setUp() + { + $this->typeConfigurable = $this->createMock(TypeConfigurable::class); + $this->salableResolver = new SalableResolverPlugin($this->typeConfigurable); + } + + /** + * @param SaleableInterface|MockObject $salableItem + * @param bool $isSalable + * @param bool $typeIsSalable + * @param bool $expectedResult + * @return void + * @dataProvider afterIsSalableDataProvider + */ + public function testAfterIsSalable($salableItem, bool $isSalable, bool $typeIsSalable, bool $expectedResult): void + { + $salableResolver = $this->createMock(SalableResolver::class); + + $this->typeConfigurable->method('isSalable') + ->willReturn($typeIsSalable); + + $result = $this->salableResolver->afterIsSalable($salableResolver, $isSalable, $salableItem); + $this->assertEquals($expectedResult, $result); + } + + /** + * @return array + */ + public function afterIsSalableDataProvider(): array + { + $simpleSalableItem = $this->createMock(SaleableInterface::class); + $simpleSalableItem->expects($this->once()) + ->method('getTypeId') + ->willReturn('simple'); + + $configurableSalableItem = $this->createMock(SaleableInterface::class); + $configurableSalableItem->expects($this->once()) + ->method('getTypeId') + ->willReturn('configurable'); + + return [ + [ + $simpleSalableItem, + true, + false, + true, + ], + [ + $configurableSalableItem, + true, + false, + false, + ], + ]; + } +} diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/SalesRule/Model/Rule/Condition/ProductTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/SalesRule/Model/Rule/Condition/ProductTest.php new file mode 100644 index 000000000000..80979148c495 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/SalesRule/Model/Rule/Condition/ProductTest.php @@ -0,0 +1,226 @@ +<?php declare(strict_types=1); +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\ConfigurableProduct\Test\Unit\Plugin\SalesRule\Model\Rule\Condition; + +use Magento\Backend\Helper\Data; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\ProductFactory; +use Magento\Catalog\Model\ResourceModel\Product; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\ConfigurableProduct\Plugin\SalesRule\Model\Rule\Condition\Product as ValidatorPlugin; +use Magento\Directory\Model\CurrencyFactory; +use Magento\Eav\Model\Config; +use Magento\Eav\Model\Entity\AbstractEntity; +use Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\Collection; +use Magento\Framework\App\ScopeResolverInterface; +use Magento\Framework\Locale\Format; +use Magento\Framework\Locale\FormatInterface; +use Magento\Framework\Locale\ResolverInterface; +use Magento\Quote\Model\Quote\Item\AbstractItem; +use Magento\Rule\Model\Condition\Context; +use Magento\SalesRule\Model\Rule\Condition\Product as SalesRuleProduct; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.LongVariable) + */ +class ProductTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + private $objectManager; + + /** + * @var SalesRuleProduct + */ + private $validator; + + /** + * @var \Magento\ConfigurableProduct\Plugin\SalesRule\Model\Rule\Condition\Product + */ + private $validatorPlugin; + + public function setUp() + { + $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->validator = $this->createValidator(); + $this->validatorPlugin = $this->objectManager->getObject(ValidatorPlugin::class); + } + + /** + * @return \Magento\SalesRule\Model\Rule\Condition\Product + */ + private function createValidator(): SalesRuleProduct + { + /** @var Context|\PHPUnit_Framework_MockObject_MockObject $contextMock */ + $contextMock = $this->getMockBuilder(Context::class) + ->disableOriginalConstructor() + ->getMock(); + /** @var Data|\PHPUnit_Framework_MockObject_MockObject $backendHelperMock */ + $backendHelperMock = $this->getMockBuilder(Data::class) + ->disableOriginalConstructor() + ->getMock(); + /** @var Config|\PHPUnit_Framework_MockObject_MockObject $configMock */ + $configMock = $this->getMockBuilder(Config::class) + ->disableOriginalConstructor() + ->getMock(); + /** @var ProductFactory|\PHPUnit_Framework_MockObject_MockObject $productFactoryMock */ + $productFactoryMock = $this->getMockBuilder(ProductFactory::class) + ->disableOriginalConstructor() + ->getMock(); + /** @var ProductRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject $productRepositoryMock */ + $productRepositoryMock = $this->getMockBuilder(ProductRepositoryInterface::class) + ->getMockForAbstractClass(); + $attributeLoaderInterfaceMock = $this->getMockBuilder(AbstractEntity::class) + ->disableOriginalConstructor() + ->setMethods(['getAttributesByCode']) + ->getMock(); + $attributeLoaderInterfaceMock + ->expects($this->any()) + ->method('getAttributesByCode') + ->willReturn([]); + /** @var Product|\PHPUnit_Framework_MockObject_MockObject $productMock */ + $productMock = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->setMethods(['loadAllAttributes', 'getConnection', 'getTable']) + ->getMock(); + $productMock->expects($this->any()) + ->method('loadAllAttributes') + ->willReturn($attributeLoaderInterfaceMock); + /** @var Collection|\PHPUnit_Framework_MockObject_MockObject $collectionMock */ + $collectionMock = $this->getMockBuilder(Collection::class) + ->disableOriginalConstructor() + ->getMock(); + /** @var FormatInterface|\PHPUnit_Framework_MockObject_MockObject $formatMock */ + $formatMock = new Format( + $this->getMockBuilder(ScopeResolverInterface::class)->disableOriginalConstructor()->getMock(), + $this->getMockBuilder(ResolverInterface::class)->disableOriginalConstructor()->getMock(), + $this->getMockBuilder(CurrencyFactory::class)->disableOriginalConstructor()->getMock() + ); + + return new SalesRuleProduct( + $contextMock, + $backendHelperMock, + $configMock, + $productFactoryMock, + $productRepositoryMock, + $productMock, + $collectionMock, + $formatMock + ); + } + + public function testChildIsUsedForValidation() + { + $configurableProductMock = $this->createProductMock(); + $configurableProductMock + ->expects($this->any()) + ->method('getTypeId') + ->willReturn(Configurable::TYPE_CODE); + $configurableProductMock + ->expects($this->any()) + ->method('hasData') + ->with($this->equalTo('special_price')) + ->willReturn(false); + + /* @var AbstractItem|\PHPUnit_Framework_MockObject_MockObject $item */ + $item = $this->getMockBuilder(AbstractItem::class) + ->disableOriginalConstructor() + ->setMethods(['setProduct', 'getProduct', 'getChildren']) + ->getMockForAbstractClass(); + $item->expects($this->any()) + ->method('getProduct') + ->willReturn($configurableProductMock); + + $simpleProductMock = $this->createProductMock(); + $simpleProductMock + ->expects($this->any()) + ->method('getTypeId') + ->willReturn(Type::TYPE_SIMPLE); + $simpleProductMock + ->expects($this->any()) + ->method('hasData') + ->with($this->equalTo('special_price')) + ->willReturn(true); + + $childItem = $this->getMockBuilder(AbstractItem::class) + ->disableOriginalConstructor() + ->setMethods(['getProduct']) + ->getMockForAbstractClass(); + $childItem->expects($this->any()) + ->method('getProduct') + ->willReturn($simpleProductMock); + + $item->expects($this->any()) + ->method('getChildren') + ->willReturn([$childItem]); + $item->expects($this->once()) + ->method('setProduct') + ->with($simpleProductMock); + + $this->validator->setAttribute('special_price'); + + $this->validatorPlugin->beforeValidate($this->validator, $item); + } + + /** + * @return Product|\PHPUnit_Framework_MockObject_MockObject + */ + private function createProductMock(): \PHPUnit_Framework_MockObject_MockObject + { + $productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) + ->disableOriginalConstructor() + ->setMethods([ + 'getAttribute', + 'getId', + 'setQuoteItemQty', + 'setQuoteItemPrice', + 'getTypeId', + 'hasData', + ]) + ->getMock(); + $productMock + ->expects($this->any()) + ->method('setQuoteItemQty') + ->willReturnSelf(); + $productMock + ->expects($this->any()) + ->method('setQuoteItemPrice') + ->willReturnSelf(); + + return $productMock; + } + + public function testChildIsNotUsedForValidation() + { + $simpleProductMock = $this->createProductMock(); + $simpleProductMock + ->expects($this->any()) + ->method('getTypeId') + ->willReturn(Type::TYPE_SIMPLE); + $simpleProductMock + ->expects($this->any()) + ->method('hasData') + ->with($this->equalTo('special_price')) + ->willReturn(true); + + /* @var AbstractItem|\PHPUnit_Framework_MockObject_MockObject $item */ + $item = $this->getMockBuilder(AbstractItem::class) + ->disableOriginalConstructor() + ->setMethods(['setProduct', 'getProduct']) + ->getMockForAbstractClass(); + $item->expects($this->any()) + ->method('getProduct') + ->willReturn($simpleProductMock); + + $this->validator->setAttribute('special_price'); + + $this->validatorPlugin->beforeValidate($this->validator, $item); + } +} diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollectorTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollectorTest.php new file mode 100644 index 000000000000..1a5c6c0003bf --- /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 fbab25ff1bea..e0cc83922e03 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 b03a8a65702b..76096fd6bdf6 100644 --- a/app/code/Magento/ConfigurableProduct/composer.json +++ b/app/code/Magento/ConfigurableProduct/composer.json @@ -22,9 +22,11 @@ "magento/module-msrp": "*", "magento/module-webapi": "*", "magento/module-sales": "*", + "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/db_schema.xml b/app/code/Magento/ConfigurableProduct/etc/db_schema.xml index 7c6661a5f399..d6917e8c1845 100644 --- a/app/code/Magento/ConfigurableProduct/etc/db_schema.xml +++ b/app/code/Magento/ConfigurableProduct/etc/db_schema.xml @@ -17,13 +17,13 @@ default="0" comment="Attribute ID"/> <column xsi:type="smallint" name="position" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Position"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="product_super_attribute_id"/> </constraint> - <constraint xsi:type="foreign" name="CAT_PRD_SPR_ATTR_PRD_ID_CAT_PRD_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_SPR_ATTR_PRD_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_product_super_attribute" column="product_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CATALOG_PRODUCT_SUPER_ATTRIBUTE_PRODUCT_ID_ATTRIBUTE_ID"> + <constraint xsi:type="unique" referenceId="CATALOG_PRODUCT_SUPER_ATTRIBUTE_PRODUCT_ID_ATTRIBUTE_ID"> <column name="product_id"/> <column name="attribute_id"/> </constraint> @@ -39,21 +39,21 @@ <column xsi:type="smallint" name="use_default" padding="5" unsigned="true" nullable="true" identity="false" default="0" comment="Use Default Value"/> <column xsi:type="varchar" name="value" nullable="true" length="255" comment="Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="foreign" name="FK_309442281DF7784210ED82B2CC51E5D5" + <constraint xsi:type="foreign" referenceId="FK_309442281DF7784210ED82B2CC51E5D5" table="catalog_product_super_attribute_label" column="product_super_attribute_id" referenceTable="catalog_product_super_attribute" referenceColumn="product_super_attribute_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CATALOG_PRODUCT_SUPER_ATTRIBUTE_LABEL_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="CATALOG_PRODUCT_SUPER_ATTRIBUTE_LABEL_STORE_ID_STORE_STORE_ID" table="catalog_product_super_attribute_label" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CAT_PRD_SPR_ATTR_LBL_PRD_SPR_ATTR_ID_STORE_ID"> + <constraint xsi:type="unique" referenceId="CAT_PRD_SPR_ATTR_LBL_PRD_SPR_ATTR_ID_STORE_ID"> <column name="product_super_attribute_id"/> <column name="store_id"/> </constraint> - <index name="CATALOG_PRODUCT_SUPER_ATTRIBUTE_LABEL_STORE_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_SUPER_ATTRIBUTE_LABEL_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -65,20 +65,20 @@ default="0" comment="Product ID"/> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Parent ID"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="link_id"/> </constraint> - <constraint xsi:type="foreign" name="CAT_PRD_SPR_LNK_PRD_ID_CAT_PRD_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_SPR_LNK_PRD_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_product_super_link" column="product_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CAT_PRD_SPR_LNK_PARENT_ID_CAT_PRD_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_SPR_LNK_PARENT_ID_CAT_PRD_ENTT_ENTT_ID" table="catalog_product_super_link" column="parent_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CATALOG_PRODUCT_SUPER_LINK_PRODUCT_ID_PARENT_ID"> + <constraint xsi:type="unique" referenceId="CATALOG_PRODUCT_SUPER_LINK_PRODUCT_ID_PARENT_ID"> <column name="product_id"/> <column name="parent_id"/> </constraint> - <index name="CATALOG_PRODUCT_SUPER_LINK_PARENT_ID" indexType="btree"> + <index referenceId="CATALOG_PRODUCT_SUPER_LINK_PARENT_ID" indexType="btree"> <column name="parent_id"/> </index> </table> diff --git a/app/code/Magento/ConfigurableProduct/etc/di.xml b/app/code/Magento/ConfigurableProduct/etc/di.xml index dfbad0dd5a76..c3ffe988b00d 100644 --- a/app/code/Magento/ConfigurableProduct/etc/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/di.xml @@ -125,6 +125,13 @@ </argument> </arguments> </type> + <type name="Magento\Sales\Model\Order\ProductOption"> + <arguments> + <argument name="processorPool" xsi:type="array"> + <item name="configurable" xsi:type="object">Magento\ConfigurableProduct\Model\ProductOptionProcessor</item> + </argument> + </arguments> + </type> <virtualType name="ConfigurableFinalPriceResolver" type="Magento\ConfigurableProduct\Pricing\Price\ConfigurablePriceResolver"> <arguments> <argument name="priceResolver" xsi:type="object">Magento\ConfigurableProduct\Pricing\Price\FinalPriceResolver</argument> @@ -228,4 +235,24 @@ </argument> </arguments> </type> + <type name="Magento\SalesRule\Model\Quote\ChildrenValidationLocator"> + <arguments> + <argument name="productTypeChildrenValidationMap" xsi:type="array"> + <item name="configurable" xsi:type="boolean">false</item> + </argument> + </arguments> + </type> + <type name="Magento\SalesRule\Model\Rule\Condition\Product"> + <plugin name="apply_rule_on_configurable_children" type="Magento\ConfigurableProduct\Plugin\SalesRule\Model\Rule\Condition\Product" /> + </type> + <type name="Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector"> + <plugin name="apply_tax_class_id" type="Magento\ConfigurableProduct\Plugin\Tax\Model\Sales\Total\Quote\CommonTaxCollector" /> + </type> + <type name="Magento\Eav\Model\Entity\Attribute\Group"> + <arguments> + <argument name="reservedSystemNames" xsi:type="array"> + <item name="configurable" xsi:type="string">configurable</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml b/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml index bb830c36b929..df96829b354c 100644 --- a/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml @@ -10,4 +10,7 @@ <type name="Magento\ConfigurableProduct\Model\ResourceModel\Attribute\OptionSelectBuilderInterface"> <plugin name="Magento_ConfigurableProduct_Plugin_Model_ResourceModel_Attribute_InStockOptionSelectBuilder" type="Magento\ConfigurableProduct\Plugin\Model\ResourceModel\Attribute\InStockOptionSelectBuilder"/> </type> + <type name="Magento\Catalog\Model\Product"> + <plugin name="product_identities_extender" type="Magento\ConfigurableProduct\Model\Plugin\Frontend\ProductIdentitiesExtender" /> + </type> </config> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml index a8712cdc183d..ecc95cbe3d48 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml @@ -17,12 +17,11 @@ <legend class="legend admin__legend"> <span><?= /* @escapeNotVerified */ __('Associated Products') ?></span> </legend> - <div class="product-options"> - <div class="field admin__field _required required"> - <?php foreach ($_attributes as $_attribute): ?> - <label class="label admin__field-label"><?php - /* @escapeNotVerified */ echo $_attribute->getProductAttribute() - ->getStoreLabel($_product->getStoreId()); + <div class="product-options fieldset admin__fieldset"> + <?php foreach ($_attributes as $_attribute): ?> + <div class="field admin__field _required required"> + <label class="label admin__field-label"><?= + $block->escapeHtml($_attribute->getProductAttribute()->getStoreLabel($_product->getStoreId())); ?></label> <div class="control admin__field-control <?php if ($_attribute->getDecoratedIsLast()): @@ -34,8 +33,8 @@ <option><?= /* @escapeNotVerified */ __('Choose an Option...') ?></option> </select> </div> - <?php endforeach; ?> - </div> + </div> + <?php endforeach; ?> </div> </fieldset> <script> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/dynamic-rows-configurable.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/dynamic-rows-configurable.js index 94a24779450a..7b04bebd4d73 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/dynamic-rows-configurable.js +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/dynamic-rows-configurable.js @@ -6,8 +6,9 @@ define([ 'underscore', 'uiRegistry', - 'Magento_Ui/js/dynamic-rows/dynamic-rows' -], function (_, registry, dynamicRows) { + 'Magento_Ui/js/dynamic-rows/dynamic-rows', + 'jquery' +], function (_, registry, dynamicRows, $) { 'use strict'; return dynamicRows.extend({ @@ -217,6 +218,8 @@ define([ _.each(tmpData, function (row, index) { path = this.dataScope + '.' + this.index + '.' + (this.startIndex + index); + row.attributes = $('<i></i>').text(row.attributes).html(); + row.sku = $('<i></i>').text(row.sku).html(); this.source.set(path, row); }, this); @@ -401,8 +404,8 @@ define([ product = { 'id': row.productId, 'product_link': row.productUrl, - 'name': row.name, - 'sku': row.sku, + 'name': $('<i></i>').text(row.name).html(), + 'sku': $('<i></i>').text(row.sku).html(), 'status': row.status, 'price': row.price, 'price_currency': row.priceCurrency, diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/price-configurable.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/price-configurable.js index 28e775b984b0..b2ef35546eea 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/price-configurable.js +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/price-configurable.js @@ -11,9 +11,6 @@ define([ return Abstract.extend({ defaults: { - listens: { - isConfigurable: 'handlePriceValue' - }, imports: { isConfigurable: '!ns = ${ $.ns }, index = configurable-matrix:isEmpty' }, @@ -22,12 +19,15 @@ define([ } }, - /** - * Invokes initialize method of parent class, - * contains initialization logic - */ + /** @inheritdoc */ initialize: function () { this._super(); + // resolve initial disable state + this.handlePriceValue(this.isConfigurable); + // add listener to track "configurable" type + this.setListeners({ + isConfigurable: 'handlePriceValue' + }); return this; }, @@ -50,8 +50,9 @@ define([ * @param {String} isConfigurable */ handlePriceValue: function (isConfigurable) { + this.disabled(!!this.isUseDefault() || isConfigurable); + if (isConfigurable) { - this.disable(); this.clear(); } } diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js index cc565cab6b26..1fa65e03f54e 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/attributes_values.js @@ -233,7 +233,7 @@ define([ */ requestAttributes: function (attributeIds) { $.ajax({ - type: 'POST', + type: 'GET', url: this.optionsUrl, data: { attributes: attributeIds diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/variations.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/variations.js index e7c8aa6f7174..6e82fd42692f 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/variations.js +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/variations.js @@ -383,26 +383,48 @@ define([ * Chose action for the form save button */ saveFormHandler: function () { - this.serializeData(); + this.formElement().validate(); + + if (this.formElement().source.get('params.invalid') === false) { + this.serializeData(); + } if (this.checkForNewAttributes()) { this.formSaveParams = arguments; this.attributeSetHandlerModal().openModal(); } else { + if (this.validateForm(this.formElement())) { + this.clearOutdatedData(); + } this.formElement().save(arguments[0], arguments[1]); + + if (this.formElement().source.get('params.invalid')) { + this.unserializeData(); + } } }, + /** + * @param {Object} formElement + * + * Validates each form element and returns true, if all elements are valid. + */ + validateForm: function (formElement) { + formElement.validate(); + + return !formElement.additionalInvalid && !formElement.source.get('params.invalid'); + }, + /** * Serialize data for specific form fields * - * Get data from outdated fields, serialize it and produce new form fields. + * Serializes some complex data fields * - * Outdated fields: + * Original fields: * - configurable-matrix; * - associated_product_ids. * - * New fields: + * Serialized fields in request: * - configurable-matrix-serialized; * - associated_product_ids_serialized. */ @@ -410,18 +432,50 @@ define([ if (this.source.data['configurable-matrix']) { this.source.data['configurable-matrix-serialized'] = JSON.stringify(this.source.data['configurable-matrix']); - - delete this.source.data['configurable-matrix']; } if (this.source.data['associated_product_ids']) { this.source.data['associated_product_ids_serialized'] = JSON.stringify(this.source.data['associated_product_ids']); + } + }, + + /** + * Clear outdated data for specific form fields + * + * Outdated fields: + * - configurable-matrix; + * - associated_product_ids. + */ + clearOutdatedData: function () { + if (this.source.data['configurable-matrix']) { + delete this.source.data['configurable-matrix']; + } + if (this.source.data['associated_product_ids']) { delete this.source.data['associated_product_ids']; } }, + /** + * Unserialize data for specific form fields + * + * Unserializes some fields that were serialized this.serializeData + */ + unserializeData: function () { + if (this.source.data['configurable-matrix-serialized']) { + this.source.data['configurable-matrix'] = + JSON.parse(this.source.data['configurable-matrix-serialized']); + delete this.source.data['configurable-matrix-serialized']; + } + + if (this.source.data['associated_product_ids_serialized']) { + this.source.data['associated_product_ids'] = + JSON.parse(this.source.data['associated_product_ids_serialized']); + delete this.source.data['associated_product_ids_serialized']; + } + }, + /** * Check for newly added attributes * @returns {Boolean} @@ -445,20 +499,20 @@ define([ * @returns {Boolean} */ addNewAttributeSetHandler: function () { - var choosenAttributeSetOption; + var chosenAttributeSetOption; this.formElement().validate(); if (this.formElement().source.get('params.invalid') === false) { - choosenAttributeSetOption = this.attributeSetSelection; + chosenAttributeSetOption = this.attributeSetSelection; - if (choosenAttributeSetOption === 'new') { + if (chosenAttributeSetOption === 'new') { this.createNewAttributeSet(); return false; } - if (choosenAttributeSetOption === 'existing') { + if (chosenAttributeSetOption === 'existing') { this.set( 'skeletonAttributeSet', this.attributeSetId @@ -469,6 +523,10 @@ define([ return true; } + + this.unserializeData(); + + return false; }, /** diff --git a/app/code/Magento/ConfigurableProduct/view/base/templates/product/price/tier_price.phtml b/app/code/Magento/ConfigurableProduct/view/base/templates/product/price/tier_price.phtml index 18f96cfaaf39..325ee1d5d79b 100644 --- a/app/code/Magento/ConfigurableProduct/view/base/templates/product/price/tier_price.phtml +++ b/app/code/Magento/ConfigurableProduct/view/base/templates/product/price/tier_price.phtml @@ -15,10 +15,13 @@ + '</span>' + '</span>'; %> <li class="item"> - <%= $t('Buy %1 for %2 each and').replace('%1', item.qty).replace('%2', priceStr) %> - <strong class="benefit"> - <%= $t('save') %><span class="percent tier-<%= key %>"> <%= item.percentage %></span>% - </strong> + <%= '<?= $block->escapeHtml(__('Buy %1 for %2 each and', '%1', '%2')) ?>' + .replace('%1', item.qty) + .replace('%2', priceStr) %> + <strong class="benefit"> + <?= $block->escapeHtml(__('save')) ?><span + class="percent tier-<%= key %>"> <%= item.percentage %></span>% + </strong> </li> <% }); %> </ul> diff --git a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js index 8cabe71c1750..ef40dcb9a732 100644 --- a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js +++ b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js @@ -291,6 +291,8 @@ define([ images = this.options.spConfig.images[this.simpleProduct]; if (images) { + images = this._sortImages(images); + if (this.options.gallerySwitchStrategy === 'prepend') { images = images.concat(initialImages); } @@ -309,7 +311,17 @@ define([ $(this.options.mediaGallerySelector).AddFotoramaVideoEvents(); } - galleryObject.first(); + }, + + /** + * Sorting images array + * + * @private + */ + _sortImages: function (images) { + return _.sortBy(images, function (image) { + return image.position; + }); }, /** @@ -360,7 +372,12 @@ define([ index = 1, allowedProducts, i, - j; + j, + finalPrice = parseFloat(this.options.spConfig.prices.finalPrice.amount), + optionFinalPrice, + optionPriceDiff, + optionPrices = this.options.spConfig.optionPrices, + allowedProductMinPrice; this._clearSelect(element); element.options[0] = new Option('', ''); @@ -374,6 +391,7 @@ define([ if (options) { for (i = 0; i < options.length; i++) { allowedProducts = []; + optionPriceDiff = 0; /* eslint-disable max-depth */ if (prevConfig) { @@ -387,6 +405,20 @@ define([ } } else { allowedProducts = options[i].products.slice(0); + + if (typeof allowedProducts[0] !== 'undefined' && + typeof optionPrices[allowedProducts[0]] !== 'undefined') { + allowedProductMinPrice = this._getAllowedProductWithMinPrice(allowedProducts); + optionFinalPrice = parseFloat(optionPrices[allowedProductMinPrice].finalPrice.amount); + optionPriceDiff = optionFinalPrice - finalPrice; + + if (optionPriceDiff !== 0) { + options[i].label = options[i].label + ' ' + priceUtils.formatPrice( + optionPriceDiff, + this.options.priceFormat, + true); + } + } } if (allowedProducts.length > 0) { @@ -394,7 +426,7 @@ define([ element.options[index] = new Option(this._getOptionLabel(options[i]), options[i].id); if (typeof options[i].price !== 'undefined') { - element.options[index].setAttribute('price', options[i].prices); + element.options[index].setAttribute('price', options[i].price); } element.options[index].config = options[i]; @@ -458,24 +490,56 @@ define([ _getPrices: function () { var prices = {}, elements = _.toArray(this.options.settings), - hasProductPrice = false; + allowedProduct; _.each(elements, function (element) { var selected = element.options[element.selectedIndex], config = selected && selected.config, priceValue = {}; - if (config && config.allowedProducts.length === 1 && !hasProductPrice) { + if (config && config.allowedProducts.length === 1) { priceValue = this._calculatePrice(config); - hasProductPrice = true; + } else if (element.value) { + allowedProduct = this._getAllowedProductWithMinPrice(config.allowedProducts); + priceValue = this._calculatePrice({ + 'allowedProducts': [ + allowedProduct + ] + }); } - prices[element.attributeId] = priceValue; + if (!_.isEmpty(priceValue)) { + prices.prices = priceValue; + } }, this); return prices; }, + /** + * Get product with minimum price from selected options. + * + * @param {Array} allowedProducts + * @returns {String} + * @private + */ + _getAllowedProductWithMinPrice: function (allowedProducts) { + var optionPrices = this.options.spConfig.optionPrices, + product = {}, + optionMinPrice, optionFinalPrice; + + _.each(allowedProducts, function (allowedProduct) { + optionFinalPrice = parseFloat(optionPrices[allowedProduct].finalPrice.amount); + + if (_.isEmpty(product) || optionFinalPrice < optionMinPrice) { + optionMinPrice = optionFinalPrice; + product = allowedProduct; + } + }, this); + + return product; + }, + /** * Returns prices for configured products * @@ -545,6 +609,13 @@ define([ } else { $(this.options.slyOldPriceSelector).hide(); } + + $(document).trigger('updateMsrpPriceBlock', + [ + optionId, + this.options.spConfig.optionPrices + ] + ); }, /** diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/ConfigurableProductTypeResolver.php b/app/code/Magento/ConfigurableProductGraphQl/Model/ConfigurableProductTypeResolver.php index aae39800cdd3..eda2ce11daaf 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/ConfigurableProductTypeResolver.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/ConfigurableProductTypeResolver.php @@ -8,19 +8,25 @@ namespace Magento\ConfigurableProductGraphQl\Model; use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable as Type; /** - * {@inheritdoc} + * @inheritdoc */ class ConfigurableProductTypeResolver implements TypeResolverInterface { /** - * {@inheritdoc} + * Configurable product type resolver code */ - public function resolveType(array $data) : string + const TYPE_RESOLVER = 'ConfigurableProduct'; + + /** + * @inheritdoc + */ + public function resolveType(array $data): string { - if (isset($data['type_id']) && $data['type_id'] == 'configurable') { - return 'ConfigurableProduct'; + if (isset($data['type_id']) && $data['type_id'] == Type::TYPE_CODE) { + return self::TYPE_RESOLVER; } return ''; } diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php index 90ed5cf54892..36ee00d55339 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php @@ -124,6 +124,8 @@ private function fetch() : array $this->attributeMap[$productId][$attribute->getId()]['attribute_code'] = $attribute->getProductAttribute()->getAttributeCode(); $this->attributeMap[$productId][$attribute->getId()]['values'] = $attributeData['options']; + $this->attributeMap[$productId][$attribute->getId()]['label'] + = $attribute->getProductAttribute()->getStoreLabel(); } return $this->attributeMap; diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableVariant.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableVariant.php index e63c75d50032..3e07fecb2ebe 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableVariant.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableVariant.php @@ -16,12 +16,11 @@ use Magento\ConfigurableProductGraphQl\Model\Variant\Collection; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Query\Resolver\Value; use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; /** - * {@inheritdoc} + * @inheritdoc */ class ConfigurableVariant implements ResolverInterface { @@ -72,11 +71,9 @@ public function __construct( } /** - * Fetch and format configurable variants. - * - * {@inheritDoc} + * @inheritdoc */ - public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) : Value + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) { $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); if ($value['type_id'] !== Type::TYPE_CODE || !isset($value[$linkField])) { @@ -86,7 +83,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value return $this->valueFactory->create($result); } - $this->variantCollection->addParentId((int)$value[$linkField]); + $this->variantCollection->addParentProduct($value['model']); $fields = $this->getProductFields($info); $matchedFields = $this->attributeCollection->getRequestAttributes($fields); $this->variantCollection->addEavAttributes($matchedFields); diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Options.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Options.php index 53912f7029e5..aa7ed6f55254 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Options.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Options.php @@ -13,12 +13,11 @@ use Magento\ConfigurableProductGraphQl\Model\Options\Collection as OptionCollection; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Query\Resolver\Value; use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; /** - * {@inheritdoc} + * @inheritdoc */ class Options implements ResolverInterface { @@ -55,9 +54,9 @@ public function __construct( /** * Fetch and format configurable variants. * - * {@inheritDoc} + * {@inheritdoc} */ - public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) : Value + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) { $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); if ($value['type_id'] !== Type::TYPE_CODE || !isset($value[$linkField])) { diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes.php index 9c275de3f096..dd2b84e1da53 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes.php @@ -7,10 +7,11 @@ namespace Magento\ConfigurableProductGraphQl\Model\Resolver\Variant; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Catalog\Model\Product; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Query\Resolver\Value; -use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; /** @@ -19,22 +20,17 @@ class Attributes implements ResolverInterface { /** - * @var ValueFactory - */ - private $valueFactory; - - /** - * @param ValueFactory $valueFactory - */ - public function __construct(ValueFactory $valueFactory) - { - $this->valueFactory = $valueFactory; - } - - /** + * @inheritdoc + * * Format product's option data to conform to GraphQL schema * - * {@inheritdoc} + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * @throws \Exception + * @return mixed|Value */ public function resolve( Field $field, @@ -42,38 +38,32 @@ public function resolve( ResolveInfo $info, array $value = null, array $args = null - ): Value { + ) { if (!isset($value['options']) || !isset($value['product'])) { - $result = function () { - return null; - }; - return $this->valueFactory->create($result); + return null; } - $result = function () use ($value) { - $data = []; - foreach ($value['options'] as $option) { - $code = $option['attribute_code']; - if (!isset($value['product'][$code])) { - continue; - } + $data = []; + foreach ($value['options'] as $option) { + $code = $option['attribute_code']; + /** @var Product|null $model */ + $model = $value['product']['model'] ?? null; + if (!$model || !$model->getData($code)) { + continue; + } - foreach ($option['values'] as $optionValue) { - if ($optionValue['value_index'] != $value['product'][$code]) { - continue; - } - $data[] = [ - 'label' => $optionValue['label'], - 'code' => $code, - 'use_default_value' => $optionValue['use_default_value'], - 'value_index' => $optionValue['value_index'] - ]; + foreach ($option['values'] as $optionValue) { + if ($optionValue['value_index'] != $model->getData($code)) { + continue; } + $data[] = [ + 'label' => $optionValue['label'], + 'code' => $code, + 'use_default_value' => $optionValue['use_default_value'], + 'value_index' => $optionValue['value_index'] + ]; } - - return $data; - }; - - return $this->valueFactory->create($result); + } + return $data; } } diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php index 0d86e1657439..12571602878d 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php @@ -9,9 +9,9 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Product; -use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\CollectionFactory; -use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\Collection as ChildCollection; use Magento\Catalog\Model\ProductFactory; +use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\Collection as ChildCollection; +use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\CollectionFactory; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product as DataProvider; @@ -47,9 +47,9 @@ class Collection private $metadataPool; /** - * @var int[] + * @var Product[] */ - private $parentIds = []; + private $parentProducts = []; /** * @var array @@ -83,19 +83,24 @@ public function __construct( } /** - * Add parent Id to collection filter + * Add parent to collection filter * - * @param int $id + * @param Product $product * @return void */ - public function addParentId(int $id) : void + public function addParentProduct(Product $product) : void { - if (!in_array($id, $this->parentIds) && !empty($this->childrenMap)) { + $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); + $productId = $product->getData($linkField); + + if (isset($this->parentProducts[$productId])) { + return; + } + + if (!empty($this->childrenMap)) { $this->childrenMap = []; - $this->parentIds[] = $id; - } elseif (!in_array($id, $this->parentIds)) { - $this->parentIds[] = $id; } + $this->parentProducts[$productId] = $product; } /** @@ -130,21 +135,20 @@ public function getChildProductsByParentId(int $id) : array * Fetch all children products from parent id's. * * @return array + * @throws \Exception */ private function fetch() : array { - if (empty($this->parentIds) || !empty($this->childrenMap)) { + if (empty($this->parentProducts) || !empty($this->childrenMap)) { return $this->childrenMap; } - $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); - foreach ($this->parentIds as $id) { + foreach ($this->parentProducts as $product) { + $attributeData = $this->getAttributesCodes($product); /** @var ChildCollection $childCollection */ $childCollection = $this->childCollectionFactory->create(); - /** @var Product $product */ - $product = $this->productFactory->create(); - $product->setData($linkField, $id); $childCollection->setProductFilter($product); + $childCollection->addAttributeToSelect($attributeData); /** @var Product $childProduct */ foreach ($childCollection->getItems() as $childProduct) { @@ -160,4 +164,24 @@ private function fetch() : array return $this->childrenMap; } + + /** + * Get attributes code + * + * @param \Magento\Catalog\Model\Product $currentProduct + * @return array + */ + private function getAttributesCodes(Product $currentProduct): array + { + $attributeCodes = []; + $allowAttributes = $currentProduct->getTypeInstance()->getConfigurableAttributes($currentProduct); + foreach ($allowAttributes as $attribute) { + $productAttribute = $attribute->getProductAttribute(); + if (!\in_array($productAttribute->getAttributeCode(), $attributeCodes)) { + $attributeCodes[] = $productAttribute->getAttributeCode(); + } + } + + return $attributeCodes; + } } diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls index 267a94a1d434..d4780c5c0867 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls +++ b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls @@ -1,5 +1,8 @@ # Copyright © Magento, Inc. All rights reserved. # See COPYING.txt for license details. +type Mutation { + addConfigurableProductsToCart(input: AddConfigurableProductsToCartInput): AddConfigurableProductsToCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\AddSimpleProductsToCart") +} type ConfigurableProduct implements ProductInterface, PhysicalProductInterface, CustomizableProductInterface @doc(description: "ConfigurableProduct defines basic features of a configurable product and its simple product variants") { variants: [ConfigurableVariant] @doc(description: "An array of variants of products") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\ConfigurableVariant") @@ -35,3 +38,30 @@ type ConfigurableProductOptionsValues @doc(description: "ConfigurableProductOpti store_label: String @doc(description: "The label of the product on the current store") use_default_value: Boolean @doc(description: "Indicates whether to use the default_label") } + +input AddConfigurableProductsToCartInput { + cart_id: String! + cartItems: [ConfigurableProductCartItemInput!]! +} + +type AddConfigurableProductsToCartOutput { + cart: Cart! +} + +input ConfigurableProductCartItemInput { + data: CartItemInput! + variant_sku: String! + customizable_options:[CustomizableOptionInput!] +} + +type ConfigurableCartItem implements CartItemInterface { + customizable_options: [SelectedCustomizableOption]! + configurable_options: [SelectedConfigurableOption!]! +} + +type SelectedConfigurableOption { + id: Int! + option_label: String! + value_id: Int! + value_label: String! +} diff --git a/app/code/Magento/Contact/Controller/Index/Index.php b/app/code/Magento/Contact/Controller/Index/Index.php index 4b734c4f9b61..562b07708724 100644 --- a/app/code/Magento/Contact/Controller/Index/Index.php +++ b/app/code/Magento/Contact/Controller/Index/Index.php @@ -6,9 +6,10 @@ */ namespace Magento\Contact\Controller\Index; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Framework\Controller\ResultFactory; -class Index extends \Magento\Contact\Controller\Index +class Index extends \Magento\Contact\Controller\Index implements HttpGetActionInterface { /** * Show Contact Us page diff --git a/app/code/Magento/Contact/Controller/Index/Post.php b/app/code/Magento/Contact/Controller/Index/Post.php index bdc2fee088d9..cbe49a767262 100644 --- a/app/code/Magento/Contact/Controller/Index/Post.php +++ b/app/code/Magento/Contact/Controller/Index/Post.php @@ -7,6 +7,7 @@ namespace Magento\Contact\Controller\Index; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Contact\Model\ConfigInterface; use Magento\Contact\Model\MailInterface; use Magento\Framework\App\Action\Context; @@ -17,7 +18,7 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject; -class Post extends \Magento\Contact\Controller\Index +class Post extends \Magento\Contact\Controller\Index implements HttpPostActionInterface { /** * @var DataPersistorInterface diff --git a/app/code/Magento/Contact/Test/Mftf/ActionGroup/AssertMessageContactUsFormActionGroup.xml b/app/code/Magento/Contact/Test/Mftf/ActionGroup/AssertMessageContactUsFormActionGroup.xml new file mode 100644 index 000000000000..eec219482516 --- /dev/null +++ b/app/code/Magento/Contact/Test/Mftf/ActionGroup/AssertMessageContactUsFormActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertMessageContactUsFormActionGroup"> + <arguments> + <argument name="message" type="string" defaultValue="Thanks for contacting us with your comments and questions. We'll respond to you very soon." /> + <argument name="messageType" type="string" defaultValue="success" /> + </arguments> + <see userInput="{{message}}" selector="{{StorefrontContactUsMessagesSection.messageByType(messageType)}}" stepKey="verifyMessage" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Contact/Test/Mftf/ActionGroup/StorefrontFillContactUsFormActionGroup.xml b/app/code/Magento/Contact/Test/Mftf/ActionGroup/StorefrontFillContactUsFormActionGroup.xml new file mode 100644 index 000000000000..df4964ea0423 --- /dev/null +++ b/app/code/Magento/Contact/Test/Mftf/ActionGroup/StorefrontFillContactUsFormActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontFillContactUsFormActionGroup"> + <arguments> + <argument name="customer" type="entity" /> + <argument name="contactUsData" type="entity" /> + </arguments> + <fillField selector="{{StorefrontContactUsFormSection.nameField}}" userInput="{{customer.firstname}}" stepKey="fillName"/> + <fillField selector="{{StorefrontContactUsFormSection.emailField}}" userInput="{{customer.email}}" stepKey="fillEmail"/> + <fillField selector="{{StorefrontContactUsFormSection.commentField}}" userInput="{{contactUsData.comment}}" stepKey="fillComment"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Contact/Test/Mftf/ActionGroup/StorefrontOpenContactUsPageActionGroup.xml b/app/code/Magento/Contact/Test/Mftf/ActionGroup/StorefrontOpenContactUsPageActionGroup.xml new file mode 100644 index 000000000000..d333d5d99896 --- /dev/null +++ b/app/code/Magento/Contact/Test/Mftf/ActionGroup/StorefrontOpenContactUsPageActionGroup.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontOpenContactUsPageActionGroup"> + <amOnPage url="{{StorefrontContactUsPage.url}}" stepKey="amOnContactUpPage"/> + <waitForPageLoad stepKey="waitForContactUpPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Contact/Test/Mftf/ActionGroup/StorefrontSubmitContactUsFormActionGroup.xml b/app/code/Magento/Contact/Test/Mftf/ActionGroup/StorefrontSubmitContactUsFormActionGroup.xml new file mode 100644 index 000000000000..f3fe34f20c31 --- /dev/null +++ b/app/code/Magento/Contact/Test/Mftf/ActionGroup/StorefrontSubmitContactUsFormActionGroup.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontSubmitContactUsFormActionGroup"> + <click selector="{{StorefrontContactUsFormSection.submitFormButton}}" stepKey="clickSubmitFormButton"/> + <waitForPageLoad stepKey="waitForCommentSubmitted" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Contact/Test/Mftf/Data/ContactUsData.xml b/app/code/Magento/Contact/Test/Mftf/Data/ContactUsData.xml new file mode 100644 index 000000000000..eadf760776c5 --- /dev/null +++ b/app/code/Magento/Contact/Test/Mftf/Data/ContactUsData.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="DefaultContactUsData"> + <data key="comment" unique="suffix">Lorem ipsum dolor sit amet, ne enim aliquando eam, oblique deserunt no usu. Unique: </data> + </entity> +</entities> diff --git a/app/code/Magento/Contact/Test/Mftf/Page/StorefrontContactUsPage.xml b/app/code/Magento/Contact/Test/Mftf/Page/StorefrontContactUsPage.xml new file mode 100644 index 000000000000..5e793b233850 --- /dev/null +++ b/app/code/Magento/Contact/Test/Mftf/Page/StorefrontContactUsPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="StorefrontContactUsPage" url="/contact/" area="storefront" module="Magento_Contact"> + <section name="StorefrontContactUsFormSection"/> + </page> +</pages> diff --git a/app/code/Magento/Contact/Test/Mftf/Section/StorefrontContactUsFormSection.xml b/app/code/Magento/Contact/Test/Mftf/Section/StorefrontContactUsFormSection.xml new file mode 100644 index 000000000000..fdaddf33f517 --- /dev/null +++ b/app/code/Magento/Contact/Test/Mftf/Section/StorefrontContactUsFormSection.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontContactUsFormSection"> + <element name="nameField" type="input" selector="#contact-form input[name='name']" /> + <element name="emailField" type="input" selector="#contact-form input[name='email']" /> + <element name="phoneField" type="input" selector="#contact-form input[name='telephone']" /> + <element name="commentField" type="textarea" selector="#contact-form textarea[name='comment']" /> + <element name="submitFormButton" type="button" selector="#contact-form button[type='submit']" timeout="30" /> + </section> +</sections> diff --git a/app/code/Magento/Contact/Test/Mftf/Section/StorefrontContactUsMessagesSection.xml b/app/code/Magento/Contact/Test/Mftf/Section/StorefrontContactUsMessagesSection.xml new file mode 100644 index 000000000000..0970f1f8f6b2 --- /dev/null +++ b/app/code/Magento/Contact/Test/Mftf/Section/StorefrontContactUsMessagesSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontContactUsMessagesSection"> + <element name="messageByType" type="block" selector="#maincontent .message-{{messageType}}" parameterized="true" /> + </section> +</sections> diff --git a/app/code/Magento/Contact/view/frontend/email/submitted_form.html b/app/code/Magento/Contact/view/frontend/email/submitted_form.html index 1bce6159c586..17146257aeff 100644 --- a/app/code/Magento/Contact/view/frontend/email/submitted_form.html +++ b/app/code/Magento/Contact/view/frontend/email/submitted_form.html @@ -16,19 +16,19 @@ <table class="message-details"> <tr> - <td><b>{{trans "Name"}}</b></td> + <td><strong>{{trans "Name"}}</strong></td> <td>{{var data.name}}</td> </tr> <tr> - <td><b>{{trans "Email"}}</b></td> + <td><strong>{{trans "Email"}}</strong></td> <td>{{var data.email}}</td> </tr> <tr> - <td><b>{{trans "Phone"}}</b></td> + <td><strong>{{trans "Phone"}}</strong></td> <td>{{var data.telephone}}</td> </tr> </table> -<p><b>{{trans "Message"}}</b></p> +<p><strong>{{trans "Message"}}</strong></p> <p>{{var data.comment}}</p> {{template config_path="design/email/footer_template"}} diff --git a/app/code/Magento/Contact/view/frontend/web/css/source/_module.less b/app/code/Magento/Contact/view/frontend/web/css/source/_module.less new file mode 100644 index 000000000000..d79806eecbe9 --- /dev/null +++ b/app/code/Magento/Contact/view/frontend/web/css/source/_module.less @@ -0,0 +1,52 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. +*/ + +& when (@media-common = true) { + .contact-index-index { + .column:not(.sidebar-main) { + .form.contact { + float: none; + width: 50%; + } + } + + .column:not(.sidebar-additional) { + .form.contact { + float: none; + width: 50%; + } + } + } +} + +// +// Desktop +// _____________________________________________ + +.media-width(@extremum, @break) when (@extremum = 'min') and (@break = @screen__m) { + .contact-index-index .column:not(.sidebar-additional) .form.contact { + min-width: 600px; + } +} + +// Mobile +.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { + .contact-index-index { + .column:not(.sidebar-main) { + .form.contact { + float: none; + width: 100%; + } + } + + .column:not(.sidebar-additional) { + .form.contact { + float: none; + width: 100%; + } + } + } +} + diff --git a/app/code/Magento/Cookie/Helper/Cookie.php b/app/code/Magento/Cookie/Helper/Cookie.php index 05ab02d7a2a1..8bab596ab4c1 100644 --- a/app/code/Magento/Cookie/Helper/Cookie.php +++ b/app/code/Magento/Cookie/Helper/Cookie.php @@ -42,7 +42,8 @@ class Cookie extends \Magento\Framework\App\Helper\AbstractHelper * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param array $data * - * @throws \InvalidArgumentException + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function __construct( \Magento\Framework\App\Helper\Context $context, diff --git a/app/code/Magento/Cron/Model/Config/Backend/Product/Alert.php b/app/code/Magento/Cron/Model/Config/Backend/Product/Alert.php index 2fc0f0ab4c1a..eeef291fb6ad 100644 --- a/app/code/Magento/Cron/Model/Config/Backend/Product/Alert.php +++ b/app/code/Magento/Cron/Model/Config/Backend/Product/Alert.php @@ -11,6 +11,9 @@ */ namespace Magento\Cron\Model\Config\Backend\Product; +/** + * Cron job Alert configuration + */ class Alert extends \Magento\Framework\App\Config\Value { /** @@ -61,7 +64,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc * * @return $this * @throws \Exception @@ -72,8 +75,8 @@ public function afterSave() $frequency = $this->getData('groups/productalert_cron/fields/frequency/value'); $cronExprArray = [ - intval($time[1]), //Minute - intval($time[0]), //Hour + (int)$time[1], //Minute + (int)$time[0], //Hour $frequency == \Magento\Cron\Model\Config\Source\Frequency::CRON_MONTHLY ? '1' : '*', //Day of the Month '*', //Month of the Year $frequency == \Magento\Cron\Model\Config\Source\Frequency::CRON_WEEKLY ? '1' : '*', //Day of the Week diff --git a/app/code/Magento/Cron/Model/Config/Backend/Sitemap.php b/app/code/Magento/Cron/Model/Config/Backend/Sitemap.php index 681129916647..44ed4c001d23 100644 --- a/app/code/Magento/Cron/Model/Config/Backend/Sitemap.php +++ b/app/code/Magento/Cron/Model/Config/Backend/Sitemap.php @@ -11,6 +11,9 @@ */ namespace Magento\Cron\Model\Config\Backend; +/** + * Sitemap configuration + */ class Sitemap extends \Magento\Framework\App\Config\Value { /** @@ -61,6 +64,8 @@ public function __construct( } /** + * After save handler + * * @return $this * @throws \Exception */ @@ -70,8 +75,8 @@ public function afterSave() $frequency = $this->getData('groups/generate/fields/frequency/value'); $cronExprArray = [ - intval($time[1]), //Minute - intval($time[0]), //Hour + (int)$time[1], //Minute + (int)$time[0], //Hour $frequency == \Magento\Cron\Model\Config\Source\Frequency::CRON_MONTHLY ? '1' : '*', //Day of the Month '*', //Month of the Year $frequency == \Magento\Cron\Model\Config\Source\Frequency::CRON_WEEKLY ? '1' : '*', //# Day of the Week diff --git a/app/code/Magento/Cron/Model/Schedule.php b/app/code/Magento/Cron/Model/Schedule.php index b127ecae6f98..582c7c811b71 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,14 +71,16 @@ 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); } /** - * @return void + * @inheritdoc */ public function _construct() { @@ -79,6 +88,8 @@ public function _construct() } /** + * Set cron expression. + * * @param string $expr * @return $this * @throws \Magento\Framework\Exception\CronException @@ -87,7 +98,7 @@ public function setCronExpr($expr) { $e = preg_split('#\s+#', $expr, null, PREG_SPLIT_NO_EMPTY); if (sizeof($e) < 5 || sizeof($e) > 6) { - throw new CronException(__('The "%1" cron expression is invalid. Verify and try again.', $expr)); + throw new CronException(__('Invalid cron expression: %1', $expr)); } $this->setCronExprArr($e); @@ -95,7 +106,7 @@ public function setCronExpr($expr) } /** - * Checks the observer's cron expression against time + * Checks the observer's cron expression against time. * * Supports $this->setCronExpr('* 0-5,10-59/5 2-10,15-25 january-june/2 mon-fri') * @@ -109,22 +120,27 @@ 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; } /** + * Match cron expression. + * * @param string $expr * @param int $num * @return bool @@ -184,13 +200,15 @@ public function matchCronExpression($expr, $num) } if ($from === false || $to === false) { - throw new CronException(__('The "%1" cron expression is invalid. Verify and try again.', $expr)); + throw new CronException(__('Invalid cron expression: %1', $expr)); } return $num >= $from && $num <= $to && $num % $mod === 0; } /** + * Get number of a month. + * * @param int|string $value * @return bool|int|string */ diff --git a/app/code/Magento/Cron/Observer/ProcessCronQueueObserver.php b/app/code/Magento/Cron/Observer/ProcessCronQueueObserver.php index ed5e46d7a60f..4bb5dc196f98 100644 --- a/app/code/Magento/Cron/Observer/ProcessCronQueueObserver.php +++ b/app/code/Magento/Cron/Observer/ProcessCronQueueObserver.php @@ -13,8 +13,12 @@ use Magento\Framework\Console\Cli; use Magento\Framework\Event\ObserverInterface; use Magento\Cron\Model\Schedule; +use Magento\Framework\Profiler\Driver\Standard\Stat; +use Magento\Framework\Profiler\Driver\Standard\StatFactory; /** + * The observer for processing cron jobs. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ProcessCronQueueObserver implements ObserverInterface @@ -56,6 +60,16 @@ class ProcessCronQueueObserver implements ObserverInterface */ const SECONDS_IN_MINUTE = 60; + /** + * How long to wait for cron group to become unlocked + */ + const LOCK_TIMEOUT = 5; + + /** + * Static lock prefix for cron group locking + */ + const LOCK_PREFIX = 'CRON_GROUP_'; + /** * @var \Magento\Cron\Model\ResourceModel\Schedule\Collection */ @@ -116,15 +130,20 @@ class ProcessCronQueueObserver implements ObserverInterface */ private $state; + /** + * @var \Magento\Framework\Lock\LockManagerInterface + */ + private $lockManager; + /** * @var array */ private $invalid = []; /** - * @var array + * @var Stat */ - private $jobs; + private $statProfiler; /** * @param \Magento\Framework\ObjectManagerInterface $objectManager @@ -137,7 +156,9 @@ class ProcessCronQueueObserver implements ObserverInterface * @param \Magento\Framework\Stdlib\DateTime\DateTime $dateTime * @param \Magento\Framework\Process\PhpExecutableFinderFactory $phpExecutableFinderFactory * @param \Psr\Log\LoggerInterface $logger - * @param \Magento\Framework\App\State $state + * @param State $state + * @param StatFactory $statFactory + * @param \Magento\Framework\Lock\LockManagerInterface $lockManager * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -151,7 +172,9 @@ public function __construct( \Magento\Framework\Stdlib\DateTime\DateTime $dateTime, \Magento\Framework\Process\PhpExecutableFinderFactory $phpExecutableFinderFactory, \Psr\Log\LoggerInterface $logger, - \Magento\Framework\App\State $state + \Magento\Framework\App\State $state, + StatFactory $statFactory, + \Magento\Framework\Lock\LockManagerInterface $lockManager ) { $this->_objectManager = $objectManager; $this->_scheduleFactory = $scheduleFactory; @@ -164,6 +187,8 @@ public function __construct( $this->phpExecutableFinder = $phpExecutableFinderFactory->create(); $this->logger = $logger; $this->state = $state; + $this->statProfiler = $statFactory->create(); + $this->lockManager = $lockManager; } /** @@ -179,27 +204,26 @@ public function __construct( */ public function execute(\Magento\Framework\Event\Observer $observer) { - $pendingJobs = $this->_getPendingSchedules(); + $currentTime = $this->dateTime->gmtTimestamp(); $jobGroupsRoot = $this->_config->getJobs(); + // sort jobs groups to start from used in separated process + uksort( + $jobGroupsRoot, + function ($a, $b) { + return $this->getCronGroupConfigurationValue($b, 'use_separate_process') + - $this->getCronGroupConfigurationValue($a, 'use_separate_process'); + } + ); $phpPath = $this->phpExecutableFinder->find() ?: 'php'; foreach ($jobGroupsRoot as $groupId => $jobsRoot) { - $this->_cleanup($groupId); - $this->_generate($groupId); - if ($this->_request->getParam('group') !== null - && $this->_request->getParam('group') !== '\'' . ($groupId) . '\'' - && $this->_request->getParam('group') !== $groupId - ) { + if (!$this->isGroupInFilter($groupId)) { continue; } - if (($this->_request->getParam(self::STANDALONE_PROCESS_STARTED) !== '1') && ( - $this->_scopeConfig->getValue( - 'system/cron/' . $groupId . '/use_separate_process', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ) == 1 - ) + if ($this->_request->getParam(self::STANDALONE_PROCESS_STARTED) !== '1' + && $this->getCronGroupConfigurationValue($groupId, 'use_separate_process') == 1 ) { $this->_shell->execute( $phpPath . ' %s cron:run --group=' . $groupId . ' --' . Cli::INPUT_KEY_BOOTSTRAP . '=' @@ -211,42 +235,43 @@ public function execute(\Magento\Framework\Event\Observer $observer) continue; } - /** @var \Magento\Cron\Model\Schedule $schedule */ - foreach ($pendingJobs as $schedule) { - $jobConfig = isset($jobsRoot[$schedule->getJobCode()]) ? $jobsRoot[$schedule->getJobCode()] : null; - if (!$jobConfig) { - continue; + $this->lockGroup( + $groupId, + function ($groupId) use ($currentTime, $jobsRoot) { + $this->cleanupJobs($groupId, $currentTime); + $this->generateSchedules($groupId); + $this->processPendingJobs($groupId, $jobsRoot, $currentTime); } + ); + } + } - $scheduledTime = strtotime($schedule->getScheduledAt()); - if ($scheduledTime > $currentTime) { - continue; - } + /** + * Lock group + * + * It should be taken by standalone (child) process, not by the parent process. + * + * @param int $groupId + * @param callable $callback + * + * @return void + */ + private function lockGroup($groupId, callable $callback) + { - try { - if ($schedule->tryLockJob()) { - $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); - } - } catch (\Exception $e) { - $schedule->setMessages($e->getMessage()); - if ($schedule->getStatus() === Schedule::STATUS_ERROR) { - $this->logger->critical($e); - } - if ($schedule->getStatus() === Schedule::STATUS_MISSED - && $this->state->getMode() === State::MODE_DEVELOPER - ) { - $this->logger->info( - sprintf( - "%s Schedule Id: %s Job Code: %s", - $schedule->getMessages(), - $schedule->getScheduleId(), - $schedule->getJobCode() - ) - ); - } - } - $schedule->save(); - } + if (!$this->lockManager->lock(self::LOCK_PREFIX . $groupId, self::LOCK_TIMEOUT)) { + $this->logger->warning( + sprintf( + "Could not acquire lock for cron group: %s, skipping run", + $groupId + ) + ); + return; + } + try { + $callback($groupId); + } finally { + $this->lockManager->unlock(self::LOCK_PREFIX . $groupId); } } @@ -263,19 +288,17 @@ public function execute(\Magento\Framework\Event\Observer $observer) */ protected function _runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId) { - $scheduleLifetime = (int)$this->_scopeConfig->getValue( - 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_LIFETIME, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ); + $jobCode = $schedule->getJobCode(); + $scheduleLifetime = $this->getCronGroupConfigurationValue($groupId, self::XML_PATH_SCHEDULE_LIFETIME); $scheduleLifetime = $scheduleLifetime * self::SECONDS_IN_MINUTE; if ($scheduledTime < $currentTime - $scheduleLifetime) { $schedule->setStatus(Schedule::STATUS_MISSED); - throw new \Exception('Too late for the schedule'); + throw new \Exception(sprintf('Cron Job %s is missed at %s', $jobCode, $schedule->getScheduledAt())); } if (!isset($jobConfig['instance'], $jobConfig['method'])) { $schedule->setStatus(Schedule::STATUS_ERROR); - throw new \Exception('No callbacks found'); + throw new \Exception(sprintf('No callbacks found for cron job %s', $jobCode)); } $model = $this->_objectManager->create($jobConfig['instance']); $callback = [$model, $jobConfig['method']]; @@ -288,10 +311,18 @@ protected function _runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $schedule->setExecutedAt(strftime('%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp()))->save(); + $this->startProfiling(); try { + $this->logger->info(sprintf('Cron Job %s is run', $jobCode)); call_user_func_array($callback, [$schedule]); } catch (\Throwable $e) { $schedule->setStatus(Schedule::STATUS_ERROR); + $this->logger->error(sprintf( + 'Cron Job %s has an error: %s. Statistics: %s', + $jobCode, + $e->getMessage(), + $this->getProfilingStat() + )); if (!$e instanceof \Exception) { $e = new \RuntimeException( 'Error when running a cron job', @@ -300,28 +331,68 @@ protected function _runJob($scheduledTime, $currentTime, $jobConfig, $schedule, ); } throw $e; + } finally { + $this->stopProfiling(); } $schedule->setStatus(Schedule::STATUS_SUCCESS)->setFinishedAt(strftime( '%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp() )); + + $this->logger->info(sprintf( + 'Cron Job %s is successfully finished. Statistics: %s', + $jobCode, + $this->getProfilingStat() + )); + } + + /** + * Starts profiling + * + * @return void + */ + private function startProfiling() + { + $this->statProfiler->clear(); + $this->statProfiler->start('job', microtime(true), memory_get_usage(true), memory_get_usage()); + } + + /** + * Stops profiling + * + * @return void + */ + private function stopProfiling() + { + $this->statProfiler->stop('job', microtime(true), memory_get_usage(true), memory_get_usage()); + } + + /** + * Retrieves statistics in the JSON format + * + * @return string + */ + private function getProfilingStat() + { + $stat = $this->statProfiler->get('job'); + unset($stat[Stat::START]); + return json_encode($stat); } /** - * Return job collection from data base with status 'pending' + * Return job collection from data base with status 'pending'. * + * @param string $groupId * @return \Magento\Cron\Model\ResourceModel\Schedule\Collection */ - protected function _getPendingSchedules() + private function getPendingSchedules($groupId) { - if (!$this->_pendingSchedules) { - $this->_pendingSchedules = $this->_scheduleFactory->create()->getCollection()->addFieldToFilter( - 'status', - Schedule::STATUS_PENDING - )->load(); - } - return $this->_pendingSchedules; + $jobs = $this->_config->getJobs(); + $pendingJobs = $this->_scheduleFactory->create()->getCollection(); + $pendingJobs->addFieldToFilter('status', Schedule::STATUS_PENDING); + $pendingJobs->addFieldToFilter('job_code', ['in' => array_keys($jobs[$groupId])]); + return $pendingJobs; } /** @@ -330,22 +401,32 @@ protected function _getPendingSchedules() * @param string $groupId * @return $this */ - protected function _generate($groupId) + private function generateSchedules($groupId) { /** * check if schedule generation is needed */ $lastRun = (int)$this->_cache->load(self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT . $groupId); - $rawSchedulePeriod = (int)$this->_scopeConfig->getValue( - 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_GENERATE_EVERY, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE + $rawSchedulePeriod = (int)$this->getCronGroupConfigurationValue( + $groupId, + self::XML_PATH_SCHEDULE_GENERATE_EVERY ); $schedulePeriod = $rawSchedulePeriod * self::SECONDS_IN_MINUTE; if ($lastRun > $this->dateTime->gmtTimestamp() - $schedulePeriod) { return $this; } - $schedules = $this->_getPendingSchedules(); + /** + * save time schedules generation was ran with no expiration + */ + $this->_cache->save( + $this->dateTime->gmtTimestamp(), + self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT . $groupId, + ['crontab'], + null + ); + + $schedules = $this->getPendingSchedules($groupId); $exists = []; /** @var Schedule $schedule */ foreach ($schedules as $schedule) { @@ -355,21 +436,11 @@ protected function _generate($groupId) /** * generate global crontab jobs */ - $jobs = $this->getJobs(); + $jobs = $this->_config->getJobs(); $this->invalid = []; $this->_generateJobs($jobs[$groupId], $exists, $groupId); $this->cleanupScheduleMismatches(); - /** - * save time schedules generation was ran with no expiration - */ - $this->_cache->save( - $this->dateTime->gmtTimestamp(), - self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT . $groupId, - ['crontab'], - null - ); - return $this; } @@ -379,7 +450,7 @@ protected function _generate($groupId) * @param array $jobs * @param array $exists * @param string $groupId - * @return $this + * @return void */ protected function _generateJobs($jobs, $exists, $groupId) { @@ -392,80 +463,65 @@ protected function _generateJobs($jobs, $exists, $groupId) $timeInterval = $this->getScheduleTimeInterval($groupId); $this->saveSchedule($jobCode, $cronExpression, $timeInterval, $exists); } - return $this; } /** * Clean expired jobs * * @param string $groupId - * @return $this + * @param int $currentTime + * @return void */ - protected function _cleanup($groupId) + private function cleanupJobs($groupId, $currentTime) { - $this->cleanupDisabledJobs($groupId); - // check if history cleanup is needed $lastCleanup = (int)$this->_cache->load(self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId); - $historyCleanUp = (int)$this->_scopeConfig->getValue( - 'system/cron/' . $groupId . '/' . self::XML_PATH_HISTORY_CLEANUP_EVERY, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ); + $historyCleanUp = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_CLEANUP_EVERY); if ($lastCleanup > $this->dateTime->gmtTimestamp() - $historyCleanUp * self::SECONDS_IN_MINUTE) { return $this; } - - // check how long the record should stay unprocessed before marked as MISSED - $scheduleLifetime = (int)$this->_scopeConfig->getValue( - 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_LIFETIME, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE + // save time history cleanup was ran with no expiration + $this->_cache->save( + $this->dateTime->gmtTimestamp(), + self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId, + ['crontab'], + null ); - $scheduleLifetime = $scheduleLifetime * self::SECONDS_IN_MINUTE; - /** - * @var \Magento\Cron\Model\ResourceModel\Schedule\Collection $history - */ - $history = $this->_scheduleFactory->create()->getCollection()->addFieldToFilter( - 'status', - ['in' => [Schedule::STATUS_SUCCESS, Schedule::STATUS_MISSED, Schedule::STATUS_ERROR]] - )->load(); + $this->cleanupDisabledJobs($groupId); - $historySuccess = (int)$this->_scopeConfig->getValue( - 'system/cron/' . $groupId . '/' . self::XML_PATH_HISTORY_SUCCESS, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ); - $historyFailure = (int)$this->_scopeConfig->getValue( - 'system/cron/' . $groupId . '/' . self::XML_PATH_HISTORY_FAILURE, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ); + $historySuccess = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_SUCCESS); + $historyFailure = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_FAILURE); $historyLifetimes = [ Schedule::STATUS_SUCCESS => $historySuccess * self::SECONDS_IN_MINUTE, Schedule::STATUS_MISSED => $historyFailure * self::SECONDS_IN_MINUTE, Schedule::STATUS_ERROR => $historyFailure * self::SECONDS_IN_MINUTE, + Schedule::STATUS_PENDING => max($historyFailure, $historySuccess) * self::SECONDS_IN_MINUTE, ]; - $now = $this->dateTime->gmtTimestamp(); - /** @var Schedule $record */ - foreach ($history as $record) { - $checkTime = $record->getExecutedAt() ? strtotime($record->getExecutedAt()) : - strtotime($record->getScheduledAt()) + $scheduleLifetime; - if ($checkTime < $now - $historyLifetimes[$record->getStatus()]) { - $record->delete(); - } + $jobs = $this->_config->getJobs()[$groupId]; + $scheduleResource = $this->_scheduleFactory->create()->getResource(); + $connection = $scheduleResource->getConnection(); + $count = 0; + foreach ($historyLifetimes as $status => $time) { + $count += $connection->delete( + $scheduleResource->getMainTable(), + [ + 'status = ?' => $status, + 'job_code in (?)' => array_keys($jobs), + 'created_at < ?' => $connection->formatDate($currentTime - $time) + ] + ); } - // save time history cleanup was ran with no expiration - $this->_cache->save( - $this->dateTime->gmtTimestamp(), - self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId, - ['crontab'], - null - ); - - return $this; + if ($count) { + $this->logger->info(sprintf('%d cron jobs were cleaned', $count)); + } } /** + * Get config of schedule. + * * @param array $jobConfig * @return mixed */ @@ -480,6 +536,8 @@ protected function getConfigSchedule($jobConfig) } /** + * Save a schedule of cron job. + * * @param string $jobCode * @param string $cronExpression * @param int $timeInterval @@ -493,7 +551,7 @@ protected function saveSchedule($jobCode, $cronExpression, $timeInterval, $exist for ($time = $currentTime; $time < $timeAhead; $time += self::SECONDS_IN_MINUTE) { $scheduledAt = strftime('%Y-%m-%d %H:%M:00', $time); $alreadyScheduled = !empty($exists[$jobCode . '/' . $scheduledAt]); - $schedule = $this->generateSchedule($jobCode, $cronExpression, $time); + $schedule = $this->createSchedule($jobCode, $cronExpression, $time); $valid = $schedule->trySchedule(); if (!$valid) { if ($alreadyScheduled) { @@ -512,12 +570,14 @@ protected function saveSchedule($jobCode, $cronExpression, $timeInterval, $exist } /** + * Create a schedule of cron job. + * * @param string $jobCode * @param string $cronExpression * @param int $time * @return Schedule */ - protected function generateSchedule($jobCode, $cronExpression, $time) + protected function createSchedule($jobCode, $cronExpression, $time) { $schedule = $this->_scheduleFactory->create() ->setCronExpr($cronExpression) @@ -530,43 +590,55 @@ protected function generateSchedule($jobCode, $cronExpression, $time) } /** + * Get time interval for scheduling. + * * @param string $groupId * @return int */ protected function getScheduleTimeInterval($groupId) { - $scheduleAheadFor = (int)$this->_scopeConfig->getValue( - 'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_AHEAD_FOR, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ); + $scheduleAheadFor = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_SCHEDULE_AHEAD_FOR); $scheduleAheadFor = $scheduleAheadFor * self::SECONDS_IN_MINUTE; return $scheduleAheadFor; } /** - * Clean up scheduled jobs that are disabled in the configuration - * This can happen when you turn off a cron job in the config and flush the cache + * Clean up scheduled jobs that are disabled in the configuration. + * + * This can happen when you turn off a cron job in the config and flush the cache. * * @param string $groupId * @return void */ private function cleanupDisabledJobs($groupId) { - $jobs = $this->getJobs(); + $jobs = $this->_config->getJobs(); + $jobsToCleanup = []; foreach ($jobs[$groupId] as $jobCode => $jobConfig) { if (!$this->getCronExpression($jobConfig)) { /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ - $scheduleResource = $this->_scheduleFactory->create()->getResource(); - $scheduleResource->getConnection()->delete($scheduleResource->getMainTable(), [ - 'status=?' => Schedule::STATUS_PENDING, - 'job_code=?' => $jobCode, - ]); + $jobsToCleanup[] = $jobCode; } } + + if (count($jobsToCleanup) > 0) { + $scheduleResource = $this->_scheduleFactory->create()->getResource(); + $count = $scheduleResource->getConnection()->delete( + $scheduleResource->getMainTable(), + [ + 'status = ?' => Schedule::STATUS_PENDING, + 'job_code in (?)' => $jobsToCleanup, + ] + ); + + $this->logger->info(sprintf('%d cron jobs were cleaned', $count)); + } } /** + * Get cron expression of cron job. + * * @param array $jobConfig * @return null|string */ @@ -586,19 +658,20 @@ private function getCronExpression($jobConfig) } /** - * Clean up scheduled jobs that do not match their cron expression anymore - * This can happen when you change the cron expression and flush the cache + * Clean up scheduled jobs that do not match their cron expression anymore. + * + * This can happen when you change the cron expression and flush the cache. * * @return $this */ private function cleanupScheduleMismatches() { + /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ + $scheduleResource = $this->_scheduleFactory->create()->getResource(); foreach ($this->invalid as $jobCode => $scheduledAtList) { - /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */ - $scheduleResource = $this->_scheduleFactory->create()->getResource(); $scheduleResource->getConnection()->delete($scheduleResource->getMainTable(), [ - 'status=?' => Schedule::STATUS_PENDING, - 'job_code=?' => $jobCode, + 'status = ?' => Schedule::STATUS_PENDING, + 'job_code = ?' => $jobCode, 'scheduled_at in (?)' => $scheduledAtList, ]); } @@ -606,13 +679,90 @@ private function cleanupScheduleMismatches() } /** - * @return array + * Get CronGroup Configuration Value. + * + * @param string $groupId + * @param string $path + * @return int + */ + private function getCronGroupConfigurationValue($groupId, $path) + { + return $this->_scopeConfig->getValue( + 'system/cron/' . $groupId . '/' . $path, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + } + + /** + * Is Group In Filter. + * + * @param string $groupId + * @return bool + */ + private function isGroupInFilter($groupId): bool + { + return !($this->_request->getParam('group') !== null + && trim($this->_request->getParam('group'), "'") !== $groupId); + } + + /** + * Process pending jobs. + * + * @param string $groupId + * @param array $jobsRoot + * @param int $currentTime */ - private function getJobs() + private function processPendingJobs($groupId, $jobsRoot, $currentTime) { - if ($this->jobs === null) { - $this->jobs = $this->_config->getJobs(); + $procesedJobs = []; + $pendingJobs = $this->getPendingSchedules($groupId); + /** @var \Magento\Cron\Model\Schedule $schedule */ + foreach ($pendingJobs as $schedule) { + if (isset($procesedJobs[$schedule->getJobCode()])) { + // process only on job per run + continue; + } + $jobConfig = isset($jobsRoot[$schedule->getJobCode()]) ? $jobsRoot[$schedule->getJobCode()] : null; + if (!$jobConfig) { + continue; + } + + $scheduledTime = strtotime($schedule->getScheduledAt()); + if ($scheduledTime > $currentTime) { + continue; + } + + try { + if ($schedule->tryLockJob()) { + $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); + } + } catch (\Exception $e) { + $this->processError($schedule, $e); + } + if ($schedule->getStatus() === Schedule::STATUS_SUCCESS) { + $procesedJobs[$schedule->getJobCode()] = true; + } + $schedule->save(); + } + } + + /** + * Process error messages. + * + * @param Schedule $schedule + * @param \Exception $exception + * @return void + */ + private function processError(\Magento\Cron\Model\Schedule $schedule, \Exception $exception) + { + $schedule->setMessages($exception->getMessage()); + if ($schedule->getStatus() === Schedule::STATUS_ERROR) { + $this->logger->critical($exception); + } + if ($schedule->getStatus() === Schedule::STATUS_MISSED + && $this->state->getMode() === State::MODE_DEVELOPER + ) { + $this->logger->info($schedule->getMessages()); } - return $this->jobs; } } diff --git a/app/code/Magento/Cron/Test/Unit/Model/ScheduleTest.php b/app/code/Magento/Cron/Test/Unit/Model/ScheduleTest.php index dd1fa0e79dc6..da5539859a4b 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,24 +376,30 @@ 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 ['1/'], //Invalid cron expression, expecting numeric modulus: 1/ - ['-'], //The "-" cron expression is invalid. Verify and try again. + ['-'], //Invalid cron expression ['1-2-3'], //Invalid cron expression, expecting 'from-to' structure: 1-2-3 ]; } /** + * Test for GetNumeric + * * @param mixed $param * @param int $expectedResult + * + * @return void * @dataProvider getNumericDataProvider */ - public function testGetNumeric($param, $expectedResult) + public function testGetNumeric($param, $expectedResult): void { // 1. Create mocks /** @var \Magento\Cron\Model\Schedule $model */ @@ -335,9 +413,11 @@ public function testGetNumeric($param, $expectedResult) } /** + * Data provider + * * @return array */ - public function getNumericDataProvider() + public function getNumericDataProvider(): array { return [ [null, false], @@ -362,7 +442,12 @@ public function getNumericDataProvider() ]; } - public function testTryLockJobSuccess() + /** + * Test for tryLockJobSuccess + * + * @return void + */ + public function testTryLockJobSuccess(): void { $scheduleId = 1; @@ -386,7 +471,12 @@ public function testTryLockJobSuccess() $this->assertEquals(Schedule::STATUS_RUNNING, $model->getStatus()); } - public function testTryLockJobFailure() + /** + * Test for tryLockJobFailure + * + * @return void + */ + public function testTryLockJobFailure(): void { $scheduleId = 1; diff --git a/app/code/Magento/Cron/Test/Unit/Model/System/Config/Initial/ConverterTest.php b/app/code/Magento/Cron/Test/Unit/Model/System/Config/Initial/ConverterTest.php new file mode 100644 index 000000000000..703926b4c011 --- /dev/null +++ b/app/code/Magento/Cron/Test/Unit/Model/System/Config/Initial/ConverterTest.php @@ -0,0 +1,89 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Cron\Test\Unit\Model\System\Config\Initial; + +use Magento\Cron\Model\Groups\Config\Data as GroupsConfigModel; +use Magento\Cron\Model\System\Config\Initial\Converter as ConverterPlugin; +use Magento\Framework\App\Config\Initial\Converter; + +/** + * Class ConverterTest + * + * Unit test for \Magento\Cron\Model\System\Config\Initial\Converter + */ +class ConverterTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var GroupsConfigModel|\PHPUnit_Framework_MockObject_MockObject + */ + private $groupsConfigMock; + + /** + * @var Converter|\PHPUnit_Framework_MockObject_MockObject + */ + private $converterMock; + + /** + * @var ConverterPlugin + */ + private $converterPlugin; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->groupsConfigMock = $this->getMockBuilder( + GroupsConfigModel::class + )->disableOriginalConstructor()->getMock(); + $this->converterMock = $this->getMockBuilder(Converter::class)->getMock(); + $this->converterPlugin = new ConverterPlugin($this->groupsConfigMock); + } + + /** + * Tests afterConvert method with no $result['data']['default']['system'] set + */ + public function testAfterConvertWithNoData() + { + $expectedResult = ['test']; + $this->groupsConfigMock->expects($this->never()) + ->method('get'); + + $result = $this->converterPlugin->afterConvert($this->converterMock, $expectedResult); + + self::assertSame($expectedResult, $result); + } + + /** + * Tests afterConvert method with $result['data']['default']['system'] set + */ + public function testAfterConvertWithData() + { + $groups = [ + 'group1' => ['val1' => ['value' => '1']], + 'group2' => ['val2' => ['value' => '2']] + ]; + $expectedResult['data']['default']['system']['cron'] = [ + 'group1' => [ + 'val1' => '1' + ], + 'group2' => [ + 'val2' => '2' + ] + ]; + $result['data']['default']['system']['cron'] = '1'; + + $this->groupsConfigMock->expects($this->once()) + ->method('get') + ->willReturn($groups); + + $result = $this->converterPlugin->afterConvert($this->converterMock, $result); + + self::assertEquals($expectedResult, $result); + } +} diff --git a/app/code/Magento/Cron/Test/Unit/Observer/ProcessCronQueueObserverTest.php b/app/code/Magento/Cron/Test/Unit/Observer/ProcessCronQueueObserverTest.php index d8cb79af5213..462dde98f99f 100644 --- a/app/code/Magento/Cron/Test/Unit/Observer/ProcessCronQueueObserverTest.php +++ b/app/code/Magento/Cron/Test/Unit/Observer/ProcessCronQueueObserverTest.php @@ -8,6 +8,7 @@ use Magento\Cron\Model\Schedule; use Magento\Cron\Observer\ProcessCronQueueObserver as ProcessCronQueueObserver; use Magento\Framework\App\State; +use Magento\Framework\Profiler\Driver\Standard\StatFactory; /** * Class \Magento\Cron\Test\Unit\Model\ObserverTest @@ -84,6 +85,11 @@ class ProcessCronQueueObserverTest extends \PHPUnit\Framework\TestCase */ protected $appStateMock; + /** + * @var \Magento\Framework\Lock\LockManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $lockManagerMock; + /** * @var \Magento\Cron\Model\ResourceModel\Schedule|\PHPUnit_Framework_MockObject_MockObject */ @@ -116,6 +122,7 @@ protected function setUp() )->disableOriginalConstructor()->getMock(); $this->_collection->expects($this->any())->method('addFieldToFilter')->will($this->returnSelf()); $this->_collection->expects($this->any())->method('load')->will($this->returnSelf()); + $this->_scheduleFactory = $this->getMockBuilder( \Magento\Cron\Model\ScheduleFactory::class )->setMethods( @@ -135,6 +142,12 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); + $this->lockManagerMock = $this->getMockBuilder(\Magento\Framework\Lock\LockManagerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->lockManagerMock->method('lock')->willReturn(true); + $this->lockManagerMock->method('unlock')->willReturn(true); + $this->observer = $this->createMock(\Magento\Framework\Event\Observer::class); $this->dateTimeMock = $this->getMockBuilder(\Magento\Framework\Stdlib\DateTime\DateTime::class) @@ -159,6 +172,16 @@ protected function setUp() $this->scheduleResource->method('getConnection')->willReturn($connection); $connection->method('delete')->willReturn(1); + $this->statFactory = $this->getMockBuilder(StatFactory::class) + ->setMethods(['create']) + ->disableOriginalConstructor() + ->getMock(); + + $this->stat = $this->getMockBuilder(\Magento\Framework\Profiler\Driver\Standard\Stat::class) + ->disableOriginalConstructor() + ->getMock(); + $this->statFactory->expects($this->any())->method('create')->willReturn($this->stat); + $this->_observer = new ProcessCronQueueObserver( $this->_objectManager, $this->_scheduleFactory, @@ -170,41 +193,23 @@ protected function setUp() $this->dateTimeMock, $phpExecutableFinderFactory, $this->loggerMock, - $this->appStateMock + $this->appStateMock, + $this->statFactory, + $this->lockManagerMock ); } - /** - * Test case without saved cron jobs in data base - */ - public function testDispatchNoPendingJobs() - { - $lastRun = $this->time + 10000000; - $this->_cache->expects($this->any())->method('load')->will($this->returnValue($lastRun)); - $this->_scopeConfig->expects($this->any())->method('getValue')->will($this->returnValue(0)); - - $this->_config->expects($this->once())->method('getJobs')->will($this->returnValue([])); - - $scheduleMock = $this->getMockBuilder( - \Magento\Cron\Model\Schedule::class - )->disableOriginalConstructor()->getMock(); - $scheduleMock->expects($this->any())->method('getCollection')->will($this->returnValue($this->_collection)); - $this->_scheduleFactory->expects($this->once())->method('create')->will($this->returnValue($scheduleMock)); - - $this->_observer->execute($this->observer); - } - /** * Test case for not existed cron jobs in files but in data base is presented */ public function testDispatchNoJobConfig() { $lastRun = $this->time + 10000000; - $this->_cache->expects($this->any())->method('load')->will($this->returnValue($lastRun)); - $this->_scopeConfig->expects($this->any())->method('getValue')->will($this->returnValue(0)); + $this->_cache->expects($this->atLeastOnce())->method('load')->will($this->returnValue($lastRun)); + $this->_scopeConfig->expects($this->atLeastOnce())->method('getValue')->will($this->returnValue(0)); $this->_config->expects( - $this->any() + $this->atLeastOnce() )->method( 'getJobs' )->will( @@ -212,16 +217,21 @@ public function testDispatchNoJobConfig() ); $schedule = $this->createPartialMock(\Magento\Cron\Model\Schedule::class, ['getJobCode', '__wakeup']); - $schedule->expects($this->once())->method('getJobCode')->will($this->returnValue('not_existed_job_code')); + $schedule->expects($this->atLeastOnce()) + ->method('getJobCode') + ->will($this->returnValue('not_existed_job_code')); $this->_collection->addItem($schedule); $scheduleMock = $this->getMockBuilder( \Magento\Cron\Model\Schedule::class )->disableOriginalConstructor()->getMock(); - $scheduleMock->expects($this->any())->method('getCollection')->will($this->returnValue($this->_collection)); - $scheduleMock->expects($this->any())->method('getResource')->will($this->returnValue($this->scheduleResource)); - $this->_scheduleFactory->expects($this->any())->method('create')->will($this->returnValue($scheduleMock)); + $scheduleMock->expects($this->atLeastOnce()) + ->method('getCollection') + ->will($this->returnValue($this->_collection)); + $this->_scheduleFactory->expects($this->atLeastOnce()) + ->method('create') + ->will($this->returnValue($scheduleMock)); $this->_observer->execute($this->observer); } @@ -240,11 +250,13 @@ public function testDispatchCanNotLock() $schedule = $this->getMockBuilder( \Magento\Cron\Model\Schedule::class )->setMethods( - ['getJobCode', 'tryLockJob', 'getScheduledAt', '__wakeup', 'save'] + ['getJobCode', 'tryLockJob', 'getScheduledAt', '__wakeup', 'save', 'setFinishedAt'] )->disableOriginalConstructor()->getMock(); $schedule->expects($this->any())->method('getJobCode')->will($this->returnValue('test_job1')); - $schedule->expects($this->once())->method('getScheduledAt')->will($this->returnValue($dateScheduledAt)); + $schedule->expects($this->atLeastOnce())->method('getScheduledAt')->will($this->returnValue($dateScheduledAt)); $schedule->expects($this->once())->method('tryLockJob')->will($this->returnValue(false)); + $schedule->expects($this->never())->method('setFinishedAt'); + $abstractModel = $this->createMock(\Magento\Framework\Model\AbstractModel::class); $schedule->expects($this->any())->method('save')->will($this->returnValue($abstractModel)); $this->_collection->addItem($schedule); @@ -262,7 +274,9 @@ public function testDispatchCanNotLock() )->disableOriginalConstructor()->getMock(); $scheduleMock->expects($this->any())->method('getCollection')->will($this->returnValue($this->_collection)); $scheduleMock->expects($this->any())->method('getResource')->will($this->returnValue($this->scheduleResource)); - $this->_scheduleFactory->expects($this->exactly(2))->method('create')->will($this->returnValue($scheduleMock)); + $this->_scheduleFactory->expects($this->atLeastOnce()) + ->method('create') + ->will($this->returnValue($scheduleMock)); $this->_observer->execute($this->observer); } @@ -272,10 +286,8 @@ public function testDispatchCanNotLock() */ public function testDispatchExceptionTooLate() { - $exceptionMessage = 'Too late for the schedule'; - $scheduleId = 42; + $exceptionMessage = 'Cron Job test_job1 is missed at 2017-07-30 15:00:00'; $jobCode = 'test_job1'; - $exception = $exceptionMessage . ' Schedule Id: ' . $scheduleId . ' Job Code: ' . $jobCode; $lastRun = $this->time + 10000000; $this->_cache->expects($this->any())->method('load')->willReturn($lastRun); @@ -299,25 +311,25 @@ public function testDispatchExceptionTooLate() 'getScheduleId', ] )->disableOriginalConstructor()->getMock(); - $schedule->expects($this->any())->method('getJobCode')->willReturn($jobCode); - $schedule->expects($this->once())->method('getScheduledAt')->willReturn($dateScheduledAt); + $schedule->expects($this->atLeastOnce())->method('getJobCode')->willReturn($jobCode); + $schedule->expects($this->atLeastOnce())->method('getScheduledAt')->willReturn($dateScheduledAt); $schedule->expects($this->once())->method('tryLockJob')->willReturn(true); $schedule->expects( - $this->once() + $this->any() )->method( 'setStatus' )->with( $this->equalTo(\Magento\Cron\Model\Schedule::STATUS_MISSED) )->willReturnSelf(); $schedule->expects($this->once())->method('setMessages')->with($this->equalTo($exceptionMessage)); - $schedule->expects($this->any())->method('getStatus')->willReturn(Schedule::STATUS_MISSED); - $schedule->expects($this->once())->method('getMessages')->willReturn($exceptionMessage); - $schedule->expects($this->once())->method('getScheduleId')->willReturn($scheduleId); + $schedule->expects($this->atLeastOnce())->method('getStatus')->willReturn(Schedule::STATUS_MISSED); + $schedule->expects($this->atLeastOnce())->method('getMessages')->willReturn($exceptionMessage); $schedule->expects($this->once())->method('save'); $this->appStateMock->expects($this->once())->method('getMode')->willReturn(State::MODE_DEVELOPER); - $this->loggerMock->expects($this->once())->method('info')->with($exception); + $this->loggerMock->expects($this->once())->method('info') + ->with('Cron Job test_job1 is missed at 2017-07-30 15:00:00'); $this->_collection->addItem($schedule); @@ -333,7 +345,7 @@ public function testDispatchExceptionTooLate() ->disableOriginalConstructor()->getMock(); $scheduleMock->expects($this->any())->method('getCollection')->willReturn($this->_collection); $scheduleMock->expects($this->any())->method('getResource')->will($this->returnValue($this->scheduleResource)); - $this->_scheduleFactory->expects($this->exactly(2))->method('create')->willReturn($scheduleMock); + $this->_scheduleFactory->expects($this->atLeastOnce())->method('create')->willReturn($scheduleMock); $this->_observer->execute($this->observer); } @@ -343,7 +355,8 @@ public function testDispatchExceptionTooLate() */ public function testDispatchExceptionNoCallback() { - $exceptionMessage = 'No callbacks found'; + $jobName = 'test_job1'; + $exceptionMessage = 'No callbacks found for cron job ' . $jobName; $exception = new \Exception(__($exceptionMessage)); $dateScheduledAt = date('Y-m-d H:i:s', $this->time - 86400); @@ -372,7 +385,7 @@ public function testDispatchExceptionNoCallback() $this->loggerMock->expects($this->once())->method('critical')->with($exception); - $jobConfig = ['test_group' => ['test_job1' => ['instance' => 'Some_Class']]]; + $jobConfig = ['test_group' => [$jobName => ['instance' => 'Some_Class']]]; $this->_config->expects($this->exactly(2))->method('getJobs')->will($this->returnValue($jobConfig)); @@ -388,7 +401,7 @@ public function testDispatchExceptionNoCallback() )->disableOriginalConstructor()->getMock(); $scheduleMock->expects($this->any())->method('getCollection')->will($this->returnValue($this->_collection)); $scheduleMock->expects($this->any())->method('getResource')->will($this->returnValue($this->scheduleResource)); - $this->_scheduleFactory->expects($this->exactly(2))->method('create')->will($this->returnValue($scheduleMock)); + $this->_scheduleFactory->expects($this->once())->method('create')->will($this->returnValue($scheduleMock)); $this->_observer->execute($this->observer); } @@ -453,7 +466,7 @@ public function testDispatchExceptionInCallback( )->disableOriginalConstructor()->getMock(); $scheduleMock->expects($this->any())->method('getCollection')->will($this->returnValue($this->_collection)); $scheduleMock->expects($this->any())->method('getResource')->will($this->returnValue($this->scheduleResource)); - $this->_scheduleFactory->expects($this->exactly(2))->method('create')->will($this->returnValue($scheduleMock)); + $this->_scheduleFactory->expects($this->once())->method('create')->will($this->returnValue($scheduleMock)); $this->_objectManager ->expects($this->once()) ->method('create') @@ -469,7 +482,6 @@ public function testDispatchExceptionInCallback( public function dispatchExceptionInCallbackDataProvider() { $throwable = new \TypeError(); - return [ 'non-callable callback' => [ 'Not_Existed_Class', @@ -496,7 +508,7 @@ public function dispatchExceptionInCallbackDataProvider() 'Error when running a cron job', 0, $throwable - ), + ) ], ]; } @@ -530,23 +542,22 @@ public function testDispatchRunJob() $scheduleMethods )->disableOriginalConstructor()->getMock(); $schedule->expects($this->any())->method('getJobCode')->will($this->returnValue('test_job1')); - $schedule->expects($this->once())->method('getScheduledAt')->will($this->returnValue($dateScheduledAt)); - $schedule->expects($this->once())->method('tryLockJob')->will($this->returnValue(true)); + $schedule->expects($this->atLeastOnce())->method('getScheduledAt')->will($this->returnValue($dateScheduledAt)); + $schedule->expects($this->atLeastOnce())->method('tryLockJob')->will($this->returnValue(true)); + $schedule->expects($this->any())->method('setFinishedAt')->willReturnSelf(); // cron start to execute some job $schedule->expects($this->any())->method('setExecutedAt')->will($this->returnSelf()); - $schedule->expects($this->at(5))->method('save'); + $schedule->expects($this->atLeastOnce())->method('save'); // cron end execute some job $schedule->expects( - $this->at(6) + $this->atLeastOnce() )->method( 'setStatus' )->with( $this->equalTo(\Magento\Cron\Model\Schedule::STATUS_SUCCESS) - )->will( - $this->returnSelf() - ); + )->willReturnSelf(); $schedule->expects($this->at(8))->method('save'); @@ -565,7 +576,7 @@ public function testDispatchRunJob() )->disableOriginalConstructor()->getMock(); $scheduleMock->expects($this->any())->method('getCollection')->will($this->returnValue($this->_collection)); $scheduleMock->expects($this->any())->method('getResource')->will($this->returnValue($this->scheduleResource)); - $this->_scheduleFactory->expects($this->exactly(2))->method('create')->will($this->returnValue($scheduleMock)); + $this->_scheduleFactory->expects($this->once(2))->method('create')->will($this->returnValue($scheduleMock)); $testCronJob = $this->getMockBuilder('CronJob')->setMethods(['execute'])->getMock(); $testCronJob->expects($this->atLeastOnce())->method('execute')->with($schedule); @@ -600,6 +611,8 @@ public function testDispatchNotGenerate() )->will( $this->returnValue(['test_group' => []]) ); + $this->_config->expects($this->at(2))->method('getJobs')->will($this->returnValue($jobConfig)); + $this->_config->expects($this->at(3))->method('getJobs')->will($this->returnValue($jobConfig)); $this->_request->expects($this->any())->method('getParam')->will($this->returnValue('test_group')); $this->_cache->expects( $this->at(0) @@ -669,6 +682,8 @@ public function testDispatchGenerate() ]; $this->_config->expects($this->at(0))->method('getJobs')->willReturn($jobConfig); $this->_config->expects($this->at(1))->method('getJobs')->willReturn($jobs); + $this->_config->expects($this->at(2))->method('getJobs')->willReturn($jobs); + $this->_config->expects($this->at(3))->method('getJobs')->willReturn($jobs); $this->_request->expects($this->any())->method('getParam')->willReturn('default'); $this->_cache->expects( $this->at(0) @@ -745,7 +760,7 @@ public function testDispatchCleanup() $this->_request->expects($this->any())->method('getParam')->will($this->returnValue('test_group')); $this->_collection->addItem($schedule); - $this->_config->expects($this->exactly(2))->method('getJobs')->will($this->returnValue($jobConfig)); + $this->_config->expects($this->atLeastOnce())->method('getJobs')->will($this->returnValue($jobConfig)); $this->_cache->expects($this->at(0))->method('load')->will($this->returnValue($this->time + 10000000)); $this->_cache->expects($this->at(1))->method('load')->will($this->returnValue($this->time - 10000000)); @@ -772,7 +787,7 @@ public function testDispatchCleanup() )->setMethods(['getCollection', 'getResource'])->disableOriginalConstructor()->getMock(); $scheduleMock->expects($this->any())->method('getCollection')->will($this->returnValue($collection)); $scheduleMock->expects($this->any())->method('getResource')->will($this->returnValue($this->scheduleResource)); - $this->_scheduleFactory->expects($this->at(1))->method('create')->will($this->returnValue($scheduleMock)); + $this->_scheduleFactory->expects($this->any())->method('create')->will($this->returnValue($scheduleMock)); $this->_observer->execute($this->observer); } @@ -796,55 +811,17 @@ public function testMissedJobsCleanedInTime() $this->_cache->expects($this->at(2))->method('load')->will($this->returnValue($this->time + 10000000)); $this->_scheduleFactory->expects($this->at(2))->method('create')->will($this->returnValue($scheduleMock)); - // This item was scheduled 2 days and 2 hours ago - $dateScheduledAt = date('Y-m-d H:i:s', $this->time - 180000); - /** @var \Magento\Cron\Model\Schedule|\PHPUnit_Framework_MockObject_MockObject $schedule1 */ - $schedule1 = $this->getMockBuilder( - \Magento\Cron\Model\Schedule::class - )->disableOriginalConstructor()->setMethods( - ['getExecutedAt', 'getScheduledAt', 'getStatus', 'delete', '__wakeup'] - )->getMock(); - $schedule1->expects($this->any())->method('getExecutedAt')->will($this->returnValue(null)); - $schedule1->expects($this->any())->method('getScheduledAt')->will($this->returnValue($dateScheduledAt)); - $schedule1->expects($this->any())->method('getStatus')->will($this->returnValue(Schedule::STATUS_MISSED)); - //we expect this job be deleted from the list - $schedule1->expects($this->once())->method('delete')->will($this->returnValue(true)); - $this->_collection->addItem($schedule1); - - // This item was scheduled 1 day ago - $dateScheduledAt = date('Y-m-d H:i:s', $this->time - 86400); - $schedule2 = $this->getMockBuilder( - \Magento\Cron\Model\Schedule::class - )->disableOriginalConstructor()->setMethods( - ['getExecutedAt', 'getScheduledAt', 'getStatus', 'delete', '__wakeup'] - )->getMock(); - $schedule2->expects($this->any())->method('getExecutedAt')->will($this->returnValue(null)); - $schedule2->expects($this->any())->method('getScheduledAt')->will($this->returnValue($dateScheduledAt)); - $schedule2->expects($this->any())->method('getStatus')->will($this->returnValue(Schedule::STATUS_MISSED)); - //we don't expect this job be deleted from the list - $schedule2->expects($this->never())->method('delete'); - $this->_collection->addItem($schedule2); - - $this->_config->expects($this->exactly(2))->method('getJobs')->will($this->returnValue($jobConfig)); - - $this->_scopeConfig->expects($this->at(0))->method('getValue') - ->with($this->equalTo('system/cron/test_group/history_cleanup_every')) - ->will($this->returnValue(10)); - $this->_scopeConfig->expects($this->at(1))->method('getValue') - ->with($this->equalTo('system/cron/test_group/schedule_lifetime')) - ->will($this->returnValue(2*24*60)); - $this->_scopeConfig->expects($this->at(2))->method('getValue') - ->with($this->equalTo('system/cron/test_group/history_success_lifetime')) - ->will($this->returnValue(0)); - $this->_scopeConfig->expects($this->at(3))->method('getValue') - ->with($this->equalTo('system/cron/test_group/history_failure_lifetime')) - ->will($this->returnValue(0)); - $this->_scopeConfig->expects($this->at(4))->method('getValue') - ->with($this->equalTo('system/cron/test_group/schedule_generate_every')) - ->will($this->returnValue(0)); - $this->_scopeConfig->expects($this->at(5))->method('getValue') - ->with($this->equalTo('system/cron/test_group/use_separate_process')) - ->will($this->returnValue(0)); + $this->_config->expects($this->atLeastOnce())->method('getJobs')->will($this->returnValue($jobConfig)); + + $this->_scopeConfig->expects($this->any())->method('getValue') + ->willReturnMap([ + ['system/cron/test_group/use_separate_process', 0], + ['system/cron/test_group/history_cleanup_every', 10], + ['system/cron/test_group/schedule_lifetime', 2*24*60], + ['system/cron/test_group/history_success_lifetime', 0], + ['system/cron/test_group/history_failure_lifetime', 0], + ['system/cron/test_group/schedule_generate_every', 0], + ]); $this->_collection->expects($this->any())->method('addFieldToFilter')->will($this->returnSelf()); $this->_collection->expects($this->any())->method('load')->will($this->returnSelf()); diff --git a/app/code/Magento/Cron/etc/db_schema.xml b/app/code/Magento/Cron/etc/db_schema.xml index deff05d3eec9..b3061eefa631 100644 --- a/app/code/Magento/Cron/etc/db_schema.xml +++ b/app/code/Magento/Cron/etc/db_schema.xml @@ -18,13 +18,13 @@ <column xsi:type="timestamp" name="scheduled_at" on_update="false" nullable="true" comment="Scheduled At"/> <column xsi:type="timestamp" name="executed_at" on_update="false" nullable="true" comment="Executed At"/> <column xsi:type="timestamp" name="finished_at" on_update="false" nullable="true" comment="Finished At"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="schedule_id"/> </constraint> - <index name="CRON_SCHEDULE_JOB_CODE" indexType="btree"> + <index referenceId="CRON_SCHEDULE_JOB_CODE" indexType="btree"> <column name="job_code"/> </index> - <index name="CRON_SCHEDULE_SCHEDULED_AT_STATUS" indexType="btree"> + <index referenceId="CRON_SCHEDULE_SCHEDULED_AT_STATUS" indexType="btree"> <column name="scheduled_at"/> <column name="status"/> </index> diff --git a/app/code/Magento/Cron/etc/di.xml b/app/code/Magento/Cron/etc/di.xml index a37f3760b70a..3e3bdc205357 100644 --- a/app/code/Magento/Cron/etc/di.xml +++ b/app/code/Magento/Cron/etc/di.xml @@ -16,6 +16,18 @@ <type name="Magento\Framework\App\Config\Initial\Converter"> <plugin name="cron_system_config_initial_converter_plugin" type="Magento\Cron\Model\System\Config\Initial\Converter" /> </type> + <virtualType name="Magento\Cron\Model\VirtualLoggerHandler" type="Magento\Framework\Logger\Handler\Base"> + <arguments> + <argument name="fileName" xsi:type="string">/var/log/cron.log</argument> + </arguments> + </virtualType> + <virtualType name="Magento\Cron\Model\VirtualLogger" type="Magento\Framework\Logger\Monolog"> + <arguments> + <argument name="handlers" xsi:type="array"> + <item name="system" xsi:type="object">Magento\Cron\Model\VirtualLoggerHandler</item> + </argument> + </arguments> + </virtualType> <!-- @api --> <virtualType name="shellBackground" type="Magento\Framework\Shell"> <arguments> @@ -25,6 +37,7 @@ <type name="Magento\Cron\Observer\ProcessCronQueueObserver"> <arguments> <argument name="shell" xsi:type="object">shellBackground</argument> + <argument name="logger" xsi:type="object">Magento\Cron\Model\VirtualLogger</argument> </arguments> </type> <type name="Magento\Framework\Console\CommandListInterface"> diff --git a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/FetchRates.php b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/FetchRates.php index 38e20355b669..34d24a8b0a7a 100644 --- a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/FetchRates.php +++ b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/FetchRates.php @@ -7,10 +7,13 @@ namespace Magento\CurrencySymbol\Controller\Adminhtml\System\Currency; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Controller\ResultFactory; +use Magento\CurrencySymbol\Controller\Adminhtml\System\Currency as CurrencyAction; -class FetchRates extends \Magento\CurrencySymbol\Controller\Adminhtml\System\Currency +class FetchRates extends CurrencyAction implements HttpGetActionInterface, HttpPostActionInterface { /** * Fetch rates action diff --git a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/Index.php b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/Index.php index 9b07777a14dc..a9b1b78cbc66 100644 --- a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/Index.php +++ b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/Index.php @@ -6,7 +6,9 @@ */ namespace Magento\CurrencySymbol\Controller\Adminhtml\System\Currency; -class Index extends \Magento\CurrencySymbol\Controller\Adminhtml\System\Currency +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\CurrencySymbol\Controller\Adminhtml\System\Currency implements HttpGetActionInterface { /** * Currency management main page diff --git a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRates.php b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRates.php index ae13c4d399e4..8dd6b5e6fac4 100644 --- a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRates.php +++ b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRates.php @@ -7,7 +7,9 @@ namespace Magento\CurrencySymbol\Controller\Adminhtml\System\Currency; -class SaveRates extends \Magento\CurrencySymbol\Controller\Adminhtml\System\Currency +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; + +class SaveRates extends \Magento\CurrencySymbol\Controller\Adminhtml\System\Currency implements HttpPostActionInterface { /** * Save rates action diff --git a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Index.php b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Index.php index 808372bd3a69..1762a907a75a 100644 --- a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Index.php +++ b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Index.php @@ -6,7 +6,9 @@ */ namespace Magento\CurrencySymbol\Controller\Adminhtml\System\Currencysymbol; -class Index extends \Magento\CurrencySymbol\Controller\Adminhtml\System\Currencysymbol +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\CurrencySymbol\Controller\Adminhtml\System\Currencysymbol implements HttpGetActionInterface { /** * Show Currency Symbols Management dialog diff --git a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Save.php b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Save.php index eee7961b02f4..703117f34fce 100644 --- a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Save.php +++ b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Save.php @@ -6,7 +6,9 @@ */ namespace Magento\CurrencySymbol\Controller\Adminhtml\System\Currencysymbol; -class Save extends \Magento\CurrencySymbol\Controller\Adminhtml\System\Currencysymbol +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; + +class Save extends \Magento\CurrencySymbol\Controller\Adminhtml\System\Currencysymbol implements HttpPostActionInterface { /** * Save custom Currency symbol diff --git a/app/code/Magento/CurrencySymbol/Model/System/Currencysymbol.php b/app/code/Magento/CurrencySymbol/Model/System/Currencysymbol.php index fcde688a1e14..6c7019986cce 100644 --- a/app/code/Magento/CurrencySymbol/Model/System/Currencysymbol.php +++ b/app/code/Magento/CurrencySymbol/Model/System/Currencysymbol.php @@ -187,14 +187,16 @@ public function getCurrencySymbolsData() /** * Save currency symbol to config * - * @param $symbols array + * @param array $symbols * @return $this */ public function setCurrencySymbolsData($symbols = []) { - foreach ($this->getCurrencySymbolsData() as $code => $values) { - if (isset($symbols[$code]) && ($symbols[$code] == $values['parentSymbol'] || empty($symbols[$code]))) { - unset($symbols[$code]); + if (!$this->_storeManager->isSingleStoreMode()) { + foreach ($this->getCurrencySymbolsData() as $code => $values) { + if (isset($symbols[$code]) && ($symbols[$code] == $values['parentSymbol'] || empty($symbols[$code]))) { + unset($symbols[$code]); + } } } $value = []; diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 000000000000..9166c8745c9e --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminMenuStoresCurrencyCurrencyRates"> + <data key="pageTitle">Currency Rates</data> + <data key="title">Currency Rates</data> + <data key="dataUiId">magento-currencysymbol-system-currency-rates</data> + </entity> + <entity name="AdminMenuStoresCurrencyCurrencySymbols"> + <data key="pageTitle">Currency Symbols</data> + <data key="title">Currency Symbols</data> + <data key="dataUiId">magento-currencysymbol-system-currency-symbols</data> + </entity> +</entities> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Page/ConfigCurrencySetupPage.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Page/ConfigCurrencySetupPage.xml new file mode 100644 index 000000000000..f523cb58d3bb --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Page/ConfigCurrencySetupPage.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="ConfigCurrencySetupPage" url="admin/system_config/edit/section/currency" area="admin" module="Magento_Config"> + </page> +</pages> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Section/CurrencySetupSection.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Section/CurrencySetupSection.xml new file mode 100644 index 000000000000..20fcd1e89360 --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Section/CurrencySetupSection.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="CurrencySetupSection"> + <element name="allowCurrencies" type="select" selector="#currency_options_allow"/> + <element name="currencyOptions" type="select" selector="#currency_options-head"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencyRatesNavigateMenuTest.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencyRatesNavigateMenuTest.xml new file mode 100644 index 000000000000..4a33d40d2a35 --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencyRatesNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminStoresCurrencyRatesNavigateMenuTest"> + <annotations> + <features value="CurrencySymbol"/> + <stories value="Menu Navigation"/> + <title value="Admin stores currency rates navigate menu test"/> + <description value="Admin should be able to navigate to Stores > Currency Rates"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14150"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToStoresCurrencyRatesPage"> + <argument name="menuUiId" value="{{AdminMenuStores.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuStoresCurrencyCurrencyRates.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuStoresCurrencyCurrencyRates.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencySymbolsNavigateMenuTest.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencySymbolsNavigateMenuTest.xml new file mode 100644 index 000000000000..978917772f2d --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencySymbolsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminStoresCurrencySymbolsNavigateMenuTest"> + <annotations> + <features value="CurrencySymbol"/> + <stories value="Menu Navigation"/> + <title value="Admin stores currency symbols navigate menu test"/> + <description value="Admin should be able to navigate to Stores > Currency Symbols"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14151"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToStoresCurrencySymbolsPage"> + <argument name="menuUiId" value="{{AdminMenuStores.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuStoresCurrencyCurrencySymbols.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuStoresCurrencyCurrencySymbols.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CurrencySymbol/view/adminhtml/templates/grid.phtml b/app/code/Magento/CurrencySymbol/view/adminhtml/templates/grid.phtml index 0ba3c7ed2d7d..6e9b9a396ec2 100644 --- a/app/code/Magento/CurrencySymbol/view/adminhtml/templates/grid.phtml +++ b/app/code/Magento/CurrencySymbol/view/adminhtml/templates/grid.phtml @@ -23,10 +23,9 @@ </label> <div class="admin__field-control"> <input id="custom_currency_symbol<?= /* @escapeNotVerified */ $code ?>" - class="required-entry admin__control-text" + class="required-entry admin__control-text <?= $data['inherited'] ? 'disabled' : '' ?>" type="text" value="<?= $block->escapeHtmlAttr($data['displaySymbol']) ?>" - <?= $data['inherited'] ? ' disabled="disabled"' : '' ?> name="custom_currency_symbol[<?= /* @escapeNotVerified */ $code ?>]"> <div class="admin__field admin__field-option"> <input id="custom_currency_symbol_inherit<?= /* @escapeNotVerified */ $code ?>" @@ -49,16 +48,18 @@ require(['jquery', "mage/mage", 'prototype'], function(jQuery){ function toggleUseDefault(code, value) { - checkbox = $('custom_currency_symbol_inherit'+code); - input = $('custom_currency_symbol'+code); - if (checkbox.checked) { - input.value = value; - input.disabled = true; + checkbox = jQuery('#custom_currency_symbol_inherit'+code); + input = jQuery('#custom_currency_symbol'+code); + + if (checkbox.is(':checked')) { + input.addClass('disabled'); + input.val(value); + input.prop('readonly', true); } else { - input.disabled = false; + input.removeClass('disabled'); + input.prop('readonly', false); } } - window.toggleUseDefault = toggleUseDefault; }); </script> diff --git a/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rate/matrix.phtml b/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rate/matrix.phtml index 8e0abcb31976..8a16eb71e085 100644 --- a/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rate/matrix.phtml +++ b/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rate/matrix.phtml @@ -45,7 +45,7 @@ $_rates = ($_newRates) ? $_newRates : $_oldRates; class="admin__control-text" <?= ($_currencyCode == $_rate) ? ' disabled' : '' ?> /> <?php if (isset($_newRates) && $_currencyCode != $_rate && isset($_oldRates[$_currencyCode][$_rate])): ?> - <div class="admin__field-note"><?= /* @escapeNotVerified */ __('Old rate:') ?> <b><?= /* @escapeNotVerified */ $_oldRates[$_currencyCode][$_rate] ?></b></div> + <div class="admin__field-note"><?= /* @escapeNotVerified */ __('Old rate:') ?> <strong><?= /* @escapeNotVerified */ $_oldRates[$_currencyCode][$_rate] ?></strong></div> <?php endif; ?> </td> <?php else: ?> @@ -56,7 +56,7 @@ $_rates = ($_newRates) ? $_newRates : $_oldRates; class="admin__control-text" <?= ($_currencyCode == $_rate) ? ' disabled' : '' ?> /> <?php if (isset($_newRates) && $_currencyCode != $_rate && isset($_oldRates[$_currencyCode][$_rate])): ?> - <div class="admin__field-note"><?= /* @escapeNotVerified */ __('Old rate:') ?> <b><?= /* @escapeNotVerified */ $_oldRates[$_currencyCode][$_rate] ?></b></div> + <div class="admin__field-note"><?= /* @escapeNotVerified */ __('Old rate:') ?> <strong><?= /* @escapeNotVerified */ $_oldRates[$_currencyCode][$_rate] ?></strong></div> <?php endif; ?> </td> <?php endif; ?> diff --git a/app/code/Magento/Customer/Api/AccountManagementInterface.php b/app/code/Magento/Customer/Api/AccountManagementInterface.php index d2f9fb7ebc42..10fc2349968e 100644 --- a/app/code/Magento/Customer/Api/AccountManagementInterface.php +++ b/app/code/Magento/Customer/Api/AccountManagementInterface.php @@ -7,6 +7,8 @@ namespace Magento\Customer\Api; +use Magento\Framework\Exception\InputException; + /** * Interface for managing customers accounts. * @api @@ -144,19 +146,24 @@ public function initiatePasswordReset($email, $template, $websiteId = null); /** * Reset customer password. * - * @param string $email + * @param string $email If empty value given then the customer + * will be matched by the RP token. * @param string $resetToken * @param string $newPassword + * * @return bool true on success * @throws \Magento\Framework\Exception\LocalizedException + * @throws InputException */ public function resetPassword($email, $resetToken, $newPassword); /** * Check if password reset token is valid. * - * @param int $customerId + * @param int $customerId If null is given then a customer + * will be matched by the RP token. * @param string $resetPasswordLinkToken + * * @return bool True if the token is valid * @throws \Magento\Framework\Exception\State\InputMismatchException If token is mismatched * @throws \Magento\Framework\Exception\State\ExpiredException If token is expired diff --git a/app/code/Magento/Customer/Api/CustomerRepositoryInterface.php b/app/code/Magento/Customer/Api/CustomerRepositoryInterface.php index 2133ae5a323b..ca9bf4dc7afd 100644 --- a/app/code/Magento/Customer/Api/CustomerRepositoryInterface.php +++ b/app/code/Magento/Customer/Api/CustomerRepositoryInterface.php @@ -51,7 +51,7 @@ public function getById($customerId); * Retrieve customers which match a specified criteria. * * This call returns an array of objects, but detailed information about each object’s attributes might not be - * included. See http://devdocs.magento.com/codelinks/attributes.html#CustomerRepositoryInterface to determine + * included. See https://devdocs.magento.com/codelinks/attributes.html#CustomerRepositoryInterface to determine * which call to use to get detailed information about all attributes for an object. * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria diff --git a/app/code/Magento/Customer/Api/GroupRepositoryInterface.php b/app/code/Magento/Customer/Api/GroupRepositoryInterface.php index 2f5e637a7693..f6ba387e913b 100644 --- a/app/code/Magento/Customer/Api/GroupRepositoryInterface.php +++ b/app/code/Magento/Customer/Api/GroupRepositoryInterface.php @@ -42,7 +42,7 @@ public function getById($id); * be filtered by tax class. * * This call returns an array of objects, but detailed information about each object’s attributes might not be - * included. See http://devdocs.magento.com/codelinks/attributes.html#GroupRepositoryInterface to determine + * included. See https://devdocs.magento.com/codelinks/attributes.html#GroupRepositoryInterface to determine * which call to use to get detailed information about all attributes for an object. * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria diff --git a/app/code/Magento/Customer/Block/Address/Book.php b/app/code/Magento/Customer/Block/Address/Book.php index 8b38946a063d..04669446ffee 100644 --- a/app/code/Magento/Customer/Block/Address/Book.php +++ b/app/code/Magento/Customer/Block/Address/Book.php @@ -6,8 +6,8 @@ namespace Magento\Customer\Block\Address; use Magento\Customer\Api\AddressRepositoryInterface; -use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Model\Address\Mapper; +use Magento\Customer\Block\Address\Grid as AddressesGrid; /** * Customer address book block @@ -24,7 +24,7 @@ class Book extends \Magento\Framework\View\Element\Template protected $currentCustomer; /** - * @var CustomerRepositoryInterface + * @var \Magento\Customer\Api\CustomerRepositoryInterface */ protected $customerRepository; @@ -43,33 +43,44 @@ class Book extends \Magento\Framework\View\Element\Template */ protected $addressMapper; + /** + * @var AddressesGrid + */ + private $addressesGrid; + /** * @param \Magento\Framework\View\Element\Template\Context $context - * @param CustomerRepositoryInterface $customerRepository + * @param CustomerRepositoryInterface|null $customerRepository * @param AddressRepositoryInterface $addressRepository * @param \Magento\Customer\Helper\Session\CurrentCustomer $currentCustomer * @param \Magento\Customer\Model\Address\Config $addressConfig * @param Mapper $addressMapper * @param array $data + * @param AddressesGrid|null $addressesGrid + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, - CustomerRepositoryInterface $customerRepository, + \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository = null, AddressRepositoryInterface $addressRepository, \Magento\Customer\Helper\Session\CurrentCustomer $currentCustomer, \Magento\Customer\Model\Address\Config $addressConfig, Mapper $addressMapper, - array $data = [] + array $data = [], + Grid $addressesGrid = null ) { - $this->customerRepository = $customerRepository; $this->currentCustomer = $currentCustomer; $this->addressRepository = $addressRepository; $this->_addressConfig = $addressConfig; $this->addressMapper = $addressMapper; + $this->addressesGrid = $addressesGrid ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(AddressesGrid::class); parent::__construct($context, $data); } /** + * Prepare the Address Book section layout + * * @return $this */ protected function _prepareLayout() @@ -79,14 +90,20 @@ protected function _prepareLayout() } /** + * Generate and return "New Address" URL + * * @return string + * @deprecated not used in this block + * @see \Magento\Customer\Block\Address\Grid::getAddAddressUrl */ public function getAddAddressUrl() { - return $this->getUrl('customer/address/new', ['_secure' => true]); + return $this->addressesGrid->getAddAddressUrl(); } /** + * Generate and return "Back" URL + * * @return string */ public function getBackUrl() @@ -98,24 +115,37 @@ public function getBackUrl() } /** + * Generate and return "Delete" URL + * * @return string + * @deprecated not used in this block + * @see \Magento\Customer\Block\Address\Grid::getDeleteUrl */ public function getDeleteUrl() { - return $this->getUrl('customer/address/delete'); + return $this->addressesGrid->getDeleteUrl(); } /** + * Generate and return "Edit Address" URL. + * + * Address ID passed in parameters + * * @param int $addressId * @return string + * @deprecated not used in this block + * @see \Magento\Customer\Block\Address\Grid::getAddressEditUrl */ public function getAddressEditUrl($addressId) { - return $this->getUrl('customer/address/edit', ['_secure' => true, 'id' => $addressId]); + return $this->addressesGrid->getAddressEditUrl($addressId); } /** + * Determines is the address primary (billing or shipping) + * * @return bool + * @throws \Magento\Framework\Exception\LocalizedException */ public function hasPrimaryAddress() { @@ -123,22 +153,22 @@ public function hasPrimaryAddress() } /** + * Get current additional customer addresses + * + * Will return array of address interfaces if customer have additional addresses and false in other case. + * * @return \Magento\Customer\Api\Data\AddressInterface[]|bool + * @throws \Magento\Framework\Exception\LocalizedException + * @deprecated not used in this block + * @see \Magento\Customer\Block\Address\Grid::getAdditionalAddresses */ public function getAdditionalAddresses() { try { - $addresses = $this->customerRepository->getById($this->currentCustomer->getCustomerId())->getAddresses(); + $addresses = $this->addressesGrid->getAdditionalAddresses(); } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { - return false; - } - $primaryAddressIds = [$this->getDefaultBilling(), $this->getDefaultShipping()]; - foreach ($addresses as $address) { - if (!in_array($address->getId(), $primaryAddressIds)) { - $additional[] = $address; - } } - return empty($additional) ? false : $additional; + return empty($addresses) ? false : $addresses; } /** @@ -158,23 +188,23 @@ public function getAddressHtml(\Magento\Customer\Api\Data\AddressInterface $addr } /** + * Get current customer + * * @return \Magento\Customer\Api\Data\CustomerInterface|null */ public function getCustomer() { - $customer = $this->getData('customer'); - if ($customer === null) { - try { - $customer = $this->customerRepository->getById($this->currentCustomer->getCustomerId()); - } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { - return null; - } - $this->setData('customer', $customer); + $customer = null; + try { + $customer = $this->currentCustomer->getCustomer(); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { } return $customer; } /** + * Get customer's default billing address + * * @return int|null */ public function getDefaultBilling() @@ -188,8 +218,11 @@ public function getDefaultBilling() } /** + * Get customer address by ID + * * @param int $addressId * @return \Magento\Customer\Api\Data\AddressInterface|null + * @throws \Magento\Framework\Exception\LocalizedException */ public function getAddressById($addressId) { @@ -201,6 +234,8 @@ public function getAddressById($addressId) } /** + * Get customer's default shipping address + * * @return int|null */ public function getDefaultShipping() diff --git a/app/code/Magento/Customer/Block/Address/Edit.php b/app/code/Magento/Customer/Block/Address/Edit.php index 6a42e9670ccc..afefb1138dea 100644 --- a/app/code/Magento/Customer/Block/Address/Edit.php +++ b/app/code/Magento/Customer/Block/Address/Edit.php @@ -5,6 +5,7 @@ */ namespace Magento\Customer\Block\Address; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; /** @@ -46,6 +47,11 @@ class Edit extends \Magento\Directory\Block\Data */ protected $dataObjectHelper; + /** + * @var \Magento\Customer\Api\AddressMetadataInterface + */ + private $addressMetadata; + /** * Constructor * @@ -61,6 +67,7 @@ class Edit extends \Magento\Directory\Block\Data * @param \Magento\Customer\Helper\Session\CurrentCustomer $currentCustomer * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper * @param array $data + * @param \Magento\Customer\Api\AddressMetadataInterface|null $addressMetadata * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -76,13 +83,15 @@ public function __construct( \Magento\Customer\Api\Data\AddressInterfaceFactory $addressDataFactory, \Magento\Customer\Helper\Session\CurrentCustomer $currentCustomer, \Magento\Framework\Api\DataObjectHelper $dataObjectHelper, - array $data = [] + array $data = [], + \Magento\Customer\Api\AddressMetadataInterface $addressMetadata = null ) { $this->_customerSession = $customerSession; $this->_addressRepository = $addressRepository; $this->addressDataFactory = $addressDataFactory; $this->currentCustomer = $currentCustomer; $this->dataObjectHelper = $dataObjectHelper; + $this->addressMetadata = $addressMetadata; parent::__construct( $context, $directoryHelper, @@ -103,6 +112,32 @@ protected function _prepareLayout() { parent::_prepareLayout(); + $this->initAddressObject(); + + $this->pageConfig->getTitle()->set($this->getTitle()); + + if ($postedData = $this->_customerSession->getAddressFormData(true)) { + $postedData['region'] = [ + 'region_id' => isset($postedData['region_id']) ? $postedData['region_id'] : null, + 'region' => $postedData['region'], + ]; + $this->dataObjectHelper->populateWithArray( + $this->_address, + $postedData, + \Magento\Customer\Api\Data\AddressInterface::class + ); + } + $this->precheckRequiredAttributes(); + return $this; + } + + /** + * Initialize address object. + * + * @return void + */ + private function initAddressObject() + { // Init address object if ($addressId = $this->getRequest()->getParam('id')) { try { @@ -124,22 +159,26 @@ protected function _prepareLayout() $this->_address->setLastname($customer->getLastname()); $this->_address->setSuffix($customer->getSuffix()); } + } - $this->pageConfig->getTitle()->set($this->getTitle()); - - if ($postedData = $this->_customerSession->getAddressFormData(true)) { - $postedData['region'] = [ - 'region_id' => isset($postedData['region_id']) ? $postedData['region_id'] : null, - 'region' => $postedData['region'], - ]; - $this->dataObjectHelper->populateWithArray( - $this->_address, - $postedData, - \Magento\Customer\Api\Data\AddressInterface::class - ); + /** + * Precheck attributes that may be required in attribute configuration. + * + * @return void + */ + private function precheckRequiredAttributes() + { + $precheckAttributes = $this->getData('check_attributes_on_render'); + $requiredAttributesPrechecked = []; + if (!empty($precheckAttributes) && is_array($precheckAttributes)) { + foreach ($precheckAttributes as $attributeCode) { + $attributeMetadata = $this->addressMetadata->getAttributeMetadata($attributeCode); + if ($attributeMetadata && $attributeMetadata->isRequired()) { + $requiredAttributesPrechecked[$attributeCode] = $attributeCode; + } + } } - - return $this; + $this->setData('required_attributes_prechecked', $requiredAttributesPrechecked); } /** diff --git a/app/code/Magento/Customer/Block/Address/Grid.php b/app/code/Magento/Customer/Block/Address/Grid.php new file mode 100644 index 000000000000..963efc648d94 --- /dev/null +++ b/app/code/Magento/Customer/Block/Address/Grid.php @@ -0,0 +1,250 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\Address; + +use Magento\Customer\Model\ResourceModel\Address\CollectionFactory as AddressCollectionFactory; +use Magento\Directory\Model\CountryFactory; +use Magento\Framework\Exception\NoSuchEntityException; + +/** + * Customer address grid + * + * @api + */ +class Grid extends \Magento\Framework\View\Element\Template +{ + /** + * @var \Magento\Customer\Helper\Session\CurrentCustomer + */ + private $currentCustomer; + + /** + * @var \Magento\Customer\Model\ResourceModel\Address\CollectionFactory + */ + private $addressCollectionFactory; + + /** + * @var \Magento\Customer\Model\ResourceModel\Address\Collection + */ + private $addressCollection; + + /** + * @var CountryFactory + */ + private $countryFactory; + + /** + * @param \Magento\Framework\View\Element\Template\Context $context + * @param \Magento\Customer\Helper\Session\CurrentCustomer $currentCustomer + * @param AddressCollectionFactory $addressCollectionFactory + * @param CountryFactory $countryFactory + * @param array $data + */ + public function __construct( + \Magento\Framework\View\Element\Template\Context $context, + \Magento\Customer\Helper\Session\CurrentCustomer $currentCustomer, + AddressCollectionFactory $addressCollectionFactory, + CountryFactory $countryFactory, + array $data = [] + ) { + $this->currentCustomer = $currentCustomer; + $this->addressCollectionFactory = $addressCollectionFactory; + $this->countryFactory = $countryFactory; + + parent::__construct($context, $data); + } + + /** + * Prepare the Address Book section layout + * + * @return void + * @throws \Magento\Framework\Exception\LocalizedException + */ + protected function _prepareLayout(): void + { + parent::_prepareLayout(); + $this->preparePager(); + } + + /** + * Generate and return "New Address" URL + * + * @return string + */ + public function getAddAddressUrl(): string + { + return $this->getUrl('customer/address/new', ['_secure' => true]); + } + + /** + * Generate and return "Delete" URL + * + * @return string + */ + public function getDeleteUrl(): string + { + return $this->getUrl('customer/address/delete'); + } + + /** + * Generate and return "Edit Address" URL. + * + * Address ID passed in parameters + * + * @param int $addressId + * @return string + */ + public function getAddressEditUrl($addressId): string + { + return $this->getUrl('customer/address/edit', ['_secure' => true, 'id' => $addressId]); + } + + /** + * Get current additional customer addresses + * + * Return array of address interfaces if customer has additional addresses and false in other cases + * + * @return \Magento\Customer\Api\Data\AddressInterface[] + * @throws \Magento\Framework\Exception\LocalizedException + * @throws NoSuchEntityException + */ + public function getAdditionalAddresses(): array + { + $additional = []; + $addresses = $this->getAddressCollection(); + $primaryAddressIds = [$this->getDefaultBilling(), $this->getDefaultShipping()]; + foreach ($addresses as $address) { + if (!in_array((int)$address->getId(), $primaryAddressIds, true)) { + $additional[] = $address->getDataModel(); + } + } + return $additional; + } + + /** + * Get current customer + * + * Return stored customer or get it from session + * + * @return \Magento\Customer\Api\Data\CustomerInterface + */ + public function getCustomer(): \Magento\Customer\Api\Data\CustomerInterface + { + $customer = $this->getData('customer'); + if ($customer === null) { + $customer = $this->currentCustomer->getCustomer(); + $this->setData('customer', $customer); + } + return $customer; + } + + /** + * Get one string street address from the Address DTO passed in parameters + * + * @param \Magento\Customer\Api\Data\AddressInterface $address + * @return string + */ + public function getStreetAddress(\Magento\Customer\Api\Data\AddressInterface $address): string + { + $street = $address->getStreet(); + if (is_array($street)) { + $street = implode(', ', $street); + } + return $street; + } + + /** + * Get country name by $countryCode + * + * Using \Magento\Directory\Model\Country to get country name by $countryCode + * + * @param string $countryCode + * @return string + */ + public function getCountryByCode(string $countryCode): string + { + /** @var \Magento\Directory\Model\Country $country */ + $country = $this->countryFactory->create(); + return $country->loadByCode($countryCode)->getName(); + } + + /** + * Get default billing address + * + * Return address string if address found and null if not + * + * @return int + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function getDefaultBilling(): int + { + $customer = $this->getCustomer(); + + return (int)$customer->getDefaultBilling(); + } + + /** + * Get default shipping address + * + * Return address string if address found and null if not + * + * @return int + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function getDefaultShipping(): int + { + $customer = $this->getCustomer(); + + return (int)$customer->getDefaultShipping(); + } + + /** + * Get pager layout + * + * @return void + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function preparePager(): void + { + $addressCollection = $this->getAddressCollection(); + if (null !== $addressCollection) { + $pager = $this->getLayout()->createBlock( + \Magento\Theme\Block\Html\Pager::class, + 'customer.addresses.pager' + )->setCollection($addressCollection); + $this->setChild('pager', $pager); + } + } + + /** + * Get customer addresses collection. + * + * Filters collection by customer id + * + * @return \Magento\Customer\Model\ResourceModel\Address\Collection + * @throws NoSuchEntityException + */ + private function getAddressCollection(): \Magento\Customer\Model\ResourceModel\Address\Collection + { + if (null === $this->addressCollection) { + if (null === $this->getCustomer()) { + throw new NoSuchEntityException(__('Customer not logged in')); + } + /** @var \Magento\Customer\Model\ResourceModel\Address\Collection $collection */ + $collection = $this->addressCollectionFactory->create(); + $collection->setOrder('entity_id', 'desc'); + $collection->addFieldToFilter( + 'entity_id', + ['nin' => [$this->getDefaultBilling(), $this->getDefaultShipping()]] + ); + $collection->setCustomerFilter([$this->getCustomer()->getId()]); + $this->addressCollection = $collection; + } + return $this->addressCollection; + } +} diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Address/CancelButton.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Address/CancelButton.php new file mode 100644 index 000000000000..d94b95691837 --- /dev/null +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Address/CancelButton.php @@ -0,0 +1,42 @@ +<?php +declare(strict_types=1); +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Customer\Block\Adminhtml\Edit\Address; + +use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; +use Magento\Customer\Block\Adminhtml\Edit\GenericButton; + +/** + * Class CancelButton + */ +class CancelButton extends GenericButton implements ButtonProviderInterface +{ + /** + * @inheritdoc + * + * @return array + */ + public function getButtonData() + { + return [ + 'label' => __('Cancel'), + 'on_click' => '', + 'data_attribute' => [ + 'mage-init' => [ + 'Magento_Ui/js/form/button-adapter' => [ + 'actions' => [ + [ + 'targetName' => 'customer_form.areas.address.address.customer_address_update_modal', + 'actionName' => 'closeModal' + ], + ], + ], + ], + ], + 'sort_order' => 20 + ]; + } +} diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Address/DeleteButton.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Address/DeleteButton.php new file mode 100644 index 000000000000..da589a25df28 --- /dev/null +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Address/DeleteButton.php @@ -0,0 +1,65 @@ +<?php +declare(strict_types=1); +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Customer\Block\Adminhtml\Edit\Address; + +use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; +use Magento\Customer\Ui\Component\Listing\Address\Column\Actions; + +/** + * Delete button on edit customer address form + */ +class DeleteButton extends GenericButton implements ButtonProviderInterface +{ + /** + * Get delete button data. + * + * @return array + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function getButtonData() + { + $data = []; + if ($this->getAddressId()) { + $data = [ + 'label' => __('Delete'), + 'on_click' => '', + 'data_attribute' => [ + 'mage-init' => [ + 'Magento_Ui/js/form/button-adapter' => [ + 'actions' => [ + [ + 'targetName' => 'customer_address_form.customer_address_form', + 'actionName' => 'deleteAddress', + 'params' => [ + $this->getDeleteUrl(), + ], + + ] + ], + ], + ], + ], + 'sort_order' => 20 + ]; + } + return $data; + } + + /** + * Get delete button url. + * + * @return string + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function getDeleteUrl(): string + { + return $this->getUrl( + Actions::CUSTOMER_ADDRESS_PATH_DELETE, + ['parent_id' => $this->getCustomerId(), 'id' => $this->getAddressId()] + ); + } +} diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Address/GenericButton.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Address/GenericButton.php new file mode 100644 index 000000000000..ae09ee689689 --- /dev/null +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Address/GenericButton.php @@ -0,0 +1,110 @@ +<?php +declare(strict_types=1); +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Customer\Block\Adminhtml\Edit\Address; + +use Magento\Customer\Model\AddressFactory; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\UrlInterface; +use Magento\Customer\Model\ResourceModel\Address; +use Magento\Customer\Model\ResourceModel\AddressRepository; + +/** + * Class for common code for buttons on the create/edit address form + */ +class GenericButton +{ + /** + * @var AddressFactory + */ + private $addressFactory; + + /** + * @var UrlInterface + */ + private $urlBuilder; + + /** + * @var Address + */ + private $addressResourceModel; + + /** + * @var RequestInterface + */ + private $request; + + /** + * @var AddressRepository + */ + private $addressRepository; + + /** + * @param AddressFactory $addressFactory + * @param UrlInterface $urlBuilder + * @param Address $addressResourceModel + * @param RequestInterface $request + * @param AddressRepository $addressRepository + */ + public function __construct( + AddressFactory $addressFactory, + UrlInterface $urlBuilder, + Address $addressResourceModel, + RequestInterface $request, + AddressRepository $addressRepository + ) { + $this->addressFactory = $addressFactory; + $this->urlBuilder = $urlBuilder; + $this->addressResourceModel = $addressResourceModel; + $this->request = $request; + $this->addressRepository = $addressRepository; + } + + /** + * Return address Id. + * + * @return int|null + */ + public function getAddressId() + { + $address = $this->addressFactory->create(); + + $entityId = $this->request->getParam('entity_id'); + $this->addressResourceModel->load( + $address, + $entityId + ); + + return $address->getEntityId() ?: null; + } + + /** + * Get customer id. + * + * @return int|null + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function getCustomerId() + { + $addressId = $this->request->getParam('entity_id'); + + $address = $this->addressRepository->getById($addressId); + + return $address->getCustomerId() ?: null; + } + + /** + * Generate url by route and parameters + * + * @param string $route + * @param array $params + * @return string + */ + public function getUrl($route = '', array $params = []): string + { + return $this->urlBuilder->getUrl($route, $params); + } +} diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Address/SaveButton.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Address/SaveButton.php new file mode 100644 index 000000000000..9d403185ca88 --- /dev/null +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Address/SaveButton.php @@ -0,0 +1,34 @@ +<?php +declare(strict_types=1); +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Customer\Block\Adminhtml\Edit\Address; + +use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; +use Magento\Customer\Block\Adminhtml\Edit\GenericButton; + +/** + * Class SaveButton + */ +class SaveButton extends GenericButton implements ButtonProviderInterface +{ + /** + * @inheritdoc + * + * @return array + */ + public function getButtonData() + { + return [ + 'label' => __('Save'), + 'class' => 'save primary', + 'data_attribute' => [ + 'mage-init' => ['button' => ['event' => 'save']], + 'form-role' => 'save', + ], + 'sort_order' => 10 + ]; + } +} diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/DeleteButton.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/DeleteButton.php index e1bb6feb2369..38b2f410d2fa 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/DeleteButton.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/DeleteButton.php @@ -10,6 +10,7 @@ /** * Class DeleteButton + * * @package Magento\Customer\Block\Adminhtml\Edit */ class DeleteButton extends GenericButton implements ButtonProviderInterface @@ -36,6 +37,8 @@ public function __construct( } /** + * Get button data. + * * @return array */ public function getButtonData() @@ -53,12 +56,15 @@ public function getButtonData() ], 'on_click' => '', 'sort_order' => 20, + 'aclResource' => 'Magento_Customer::delete', ]; } return $data; } /** + * Get delete url. + * * @return string */ public function getDeleteUrl() diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/InvalidateTokenButton.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/InvalidateTokenButton.php index 180cb3d66ea3..ca24ac9356df 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/InvalidateTokenButton.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/InvalidateTokenButton.php @@ -9,11 +9,14 @@ /** * Class InvalidateTokenButton + * * @package Magento\Customer\Block\Adminhtml\Edit */ class InvalidateTokenButton extends GenericButton implements ButtonProviderInterface { /** + * Get button data. + * * @return array */ public function getButtonData() @@ -27,12 +30,15 @@ public function getButtonData() 'class' => 'invalidate-token', 'on_click' => 'deleteConfirm("' . $deleteConfirmMsg . '", "' . $this->getInvalidateTokenUrl() . '")', 'sort_order' => 65, + 'aclResource' => 'Magento_Customer::invalidate_tokens', ]; } return $data; } /** + * Get invalidate token url. + * * @return string */ public function getInvalidateTokenUrl() diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Renderer/Region.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Renderer/Region.php index 9a025211c9b0..0aeed1562c51 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Renderer/Region.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Renderer/Region.php @@ -48,7 +48,7 @@ public function render(\Magento\Framework\Data\Form\Element\AbstractElement $ele $regionId = $element->getForm()->getElement('region_id')->getValue(); - $html = '<div class="field field-state required admin__field _required">'; + $html = '<div class="field field-state admin__field">'; $element->setClass('input-text admin__control-text'); $element->setRequired(true); $html .= $element->getLabelHtml() . '<div class="control admin__field-control">'; diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/ResetPasswordButton.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/ResetPasswordButton.php index f2d1dd9f0a85..f8a6b3505ae6 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/ResetPasswordButton.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/ResetPasswordButton.php @@ -13,6 +13,8 @@ class ResetPasswordButton extends GenericButton implements ButtonProviderInterface { /** + * Retrieve button-specified settings + * * @return array */ public function getButtonData() @@ -25,12 +27,15 @@ public function getButtonData() 'class' => 'reset reset-password', 'on_click' => sprintf("location.href = '%s';", $this->getResetPasswordUrl()), 'sort_order' => 60, + 'aclResource' => 'Magento_Customer::reset_password', ]; } return $data; } /** + * Get reset password url + * * @return string */ public function getResetPasswordUrl() 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 bb190260e477..2fb59ec767e8 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Orders.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Orders.php @@ -57,13 +57,14 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ 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); } @@ -102,7 +103,7 @@ protected function _prepareCollection() } /** - * {@inheritdoc} + * @inheritdoc */ protected function _prepareColumns() { @@ -123,7 +124,8 @@ protected function _prepareColumns() 'header' => __('Order Total'), 'index' => 'grand_total', 'type' => 'currency', - 'currency' => 'order_currency_code' + 'currency' => 'order_currency_code', + 'rate' => 1 ] ); @@ -162,7 +164,7 @@ public function getRowUrl($row) } /** - * {@inheritdoc} + * @inheritdoc */ public function getGridUrl() { 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 3f2c7cda7608..e63c00ba18d2 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Cart.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Cart.php @@ -71,13 +71,14 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ protected function _construct() { parent::_construct(); $this->setId('customer_view_cart_grid'); - $this->setDefaultSort('added_at', 'desc'); + $this->setDefaultSort('added_at'); + $this->setDefaultDir('desc'); $this->setSortable(false); $this->setPagerVisibility(false); $this->setFilterVisibility(false); @@ -94,7 +95,7 @@ protected function _prepareCollection() $quote = $this->getQuote(); if ($quote) { - $collection = $quote->getItemsCollection(false); + $collection = $quote->getItemsCollection(true); } else { $collection = $this->_dataCollectionFactory->create(); } @@ -106,7 +107,7 @@ protected function _prepareCollection() } /** - * {@inheritdoc} + * @inheritdoc */ protected function _prepareColumns() { @@ -144,7 +145,7 @@ protected function _prepareColumns() } /** - * {@inheritdoc} + * @inheritdoc */ public function getRowUrl($row) { @@ -152,7 +153,7 @@ public function getRowUrl($row) } /** - * {@inheritdoc} + * @inheritdoc */ public function getHeadersVisibility() { diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/PersonalInfo.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/PersonalInfo.php index 81b7b8b3f96b..a6e0eb0bcbc5 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/PersonalInfo.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/PersonalInfo.php @@ -152,13 +152,12 @@ public function __construct( /** * Set customer registry * - * @param \Magento\Framework\Registry $coreRegistry + * @param \Magento\Framework\Registry $customerRegistry * @return void * @deprecated 100.1.0 */ public function setCustomerRegistry(\Magento\Customer\Model\CustomerRegistry $customerRegistry) { - $this->customerRegistry = $customerRegistry; } @@ -461,7 +460,7 @@ protected function getOnlineMinutesInterval() 'customer/online_customers/online_minutes_interval', \Magento\Store\Model\ScopeInterface::SCOPE_STORE ); - return intval($configValue) > 0 ? intval($configValue) : self::DEFAULT_ONLINE_MINUTES_INTERVAL; + return (int)$configValue > 0 ? (int)$configValue : self::DEFAULT_ONLINE_MINUTES_INTERVAL; } /** diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Sales.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Sales.php index 76c33f143e67..5eeacca4c73a 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Sales.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Sales.php @@ -147,10 +147,12 @@ public function _beforeToHtml() */ public function getWebsiteCount($websiteId) { - return isset($this->_websiteCounts[$websiteId]) ? $this->_websiteCounts[$websiteId] : 0; + return $this->_websiteCounts[$websiteId] ?? 0; } /** + * Returns Grouped Collection Rows + * * @return array */ public function getRows() @@ -159,6 +161,8 @@ public function getRows() } /** + * Return totals data + * * @return \Magento\Framework\DataObject */ public function getTotals() @@ -171,7 +175,9 @@ public function getTotals() * * @param float $price * @param null|int $websiteId + * * @return string + * @throws \Magento\Framework\Exception\LocalizedException */ public function formatCurrency($price, $websiteId = null) { diff --git a/app/code/Magento/Customer/Block/DataProviders/PostCodesPatternsAttributeData.php b/app/code/Magento/Customer/Block/DataProviders/PostCodesPatternsAttributeData.php new file mode 100644 index 000000000000..280948439e1f --- /dev/null +++ b/app/code/Magento/Customer/Block/DataProviders/PostCodesPatternsAttributeData.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\DataProviders; + +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\Directory\Model\Country\Postcode\Config as PostCodeConfig; + +/** + * Provides postcodes patterns into template. + */ +class PostCodesPatternsAttributeData implements ArgumentInterface +{ + /** + * @var PostCodeConfig + */ + private $postCodeConfig; + + /** + * @var SerializerInterface + */ + private $serializer; + + /** + * Constructor + * + * @param PostCodeConfig $postCodeConfig + * @param SerializerInterface $serializer + */ + public function __construct(PostCodeConfig $postCodeConfig, SerializerInterface $serializer) + { + $this->postCodeConfig = $postCodeConfig; + $this->serializer = $serializer; + } + + /** + * Get serialized post codes + * + * @return string + */ + public function getSerializedPostCodes(): string + { + return $this->serializer->serialize($this->postCodeConfig->getPostCodes()); + } +} diff --git a/app/code/Magento/Customer/Block/Form/Login.php b/app/code/Magento/Customer/Block/Form/Login.php index 7b265ae1f0f3..d3d3306a49b4 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 f31012a52a98..59966768a2ed 100644 --- a/app/code/Magento/Customer/Block/Form/Register.php +++ b/app/code/Magento/Customer/Block/Form/Register.php @@ -6,6 +6,7 @@ namespace Magento\Customer\Block\Form; use Magento\Customer\Model\AccountManagement; +use Magento\Newsletter\Observer\PredispatchNewsletterObserver; /** * Customer register form block @@ -85,15 +86,6 @@ public function getConfig($path) return $this->_scopeConfig->getValue($path, \Magento\Store\Model\ScopeInterface::SCOPE_STORE); } - /** - * @return $this - */ - protected function _prepareLayout() - { - $this->pageConfig->getTitle()->set(__('Create New Customer Account')); - return parent::_prepareLayout(); - } - /** * Retrieve form posting url * @@ -177,11 +169,13 @@ public function getRegion() */ public function isNewsletterEnabled() { - return $this->_moduleManager->isOutputEnabled('Magento_Newsletter'); + return $this->_moduleManager->isOutputEnabled('Magento_Newsletter') + && $this->getConfig(PredispatchNewsletterObserver::XML_PATH_NEWSLETTER_ACTIVE); } /** * Restore entity data from session + * * Entity and form code must be defined for the form * * @param \Magento\Customer\Model\Metadata\Form $form diff --git a/app/code/Magento/Customer/Block/Widget/Dob.php b/app/code/Magento/Customer/Block/Widget/Dob.php index 936563d51982..55101fb82afd 100644 --- a/app/code/Magento/Customer/Block/Widget/Dob.php +++ b/app/code/Magento/Customer/Block/Widget/Dob.php @@ -61,7 +61,7 @@ public function __construct( } /** - * @return void + * @inheritdoc */ public function _construct() { @@ -70,6 +70,8 @@ public function _construct() } /** + * Check if dob attribute enabled in system + * * @return bool */ public function isEnabled() @@ -79,6 +81,8 @@ public function isEnabled() } /** + * Check if dob attribute marked as required + * * @return bool */ public function isRequired() @@ -88,6 +92,8 @@ public function isRequired() } /** + * Set date + * * @param string $date * @return $this */ @@ -135,6 +141,8 @@ protected function applyOutputFilter($value) } /** + * Get day + * * @return string|bool */ public function getDay() @@ -143,6 +151,8 @@ public function getDay() } /** + * Get month + * * @return string|bool */ public function getMonth() @@ -151,6 +161,8 @@ public function getMonth() } /** + * Get year + * * @return string|bool */ public function getYear() @@ -168,6 +180,19 @@ public function getLabel() return __('Date of Birth'); } + /** + * Retrieve store attribute label + * + * @param string $attributeCode + * + * @return string + */ + public function getStoreLabel($attributeCode) + { + $attribute = $this->_getAttribute($attributeCode); + return $attribute ? __($attribute->getStoreLabel()) : ''; + } + /** * Create correct date field * diff --git a/app/code/Magento/Customer/Block/Widget/Gender.php b/app/code/Magento/Customer/Block/Widget/Gender.php index d03c64a54fb9..9df3f1072ce0 100644 --- a/app/code/Magento/Customer/Block/Widget/Gender.php +++ b/app/code/Magento/Customer/Block/Widget/Gender.php @@ -64,6 +64,7 @@ public function _construct() /** * Check if gender attribute enabled in system + * * @return bool */ public function isEnabled() @@ -73,6 +74,7 @@ public function isEnabled() /** * Check if gender attribute marked as required + * * @return bool */ public function isRequired() @@ -80,6 +82,19 @@ public function isRequired() return $this->_getAttribute('gender') ? (bool)$this->_getAttribute('gender')->isRequired() : false; } + /** + * Retrieve store attribute label + * + * @param string $attributeCode + * + * @return string + */ + public function getStoreLabel($attributeCode) + { + $attribute = $this->_getAttribute($attributeCode); + return $attribute ? __($attribute->getStoreLabel()) : ''; + } + /** * Get current customer from session * @@ -92,6 +107,7 @@ public function getCustomer() /** * Returns options from gender attribute + * * @return OptionInterface[] */ public function getGenderOptions() diff --git a/app/code/Magento/Customer/Block/Widget/Name.php b/app/code/Magento/Customer/Block/Widget/Name.php index d50045f4a409..6f1b051af746 100644 --- a/app/code/Magento/Customer/Block/Widget/Name.php +++ b/app/code/Magento/Customer/Block/Widget/Name.php @@ -55,7 +55,7 @@ public function __construct( } /** - * @return void + * @inheritdoc */ public function _construct() { @@ -245,10 +245,13 @@ public function getStoreLabel($attributeCode) */ public function getAttributeValidationClass($attributeCode) { - return $this->_addressHelper->getAttributeValidationClass($attributeCode); + $attributeMetadata = $this->_getAttribute($attributeCode); + return $attributeMetadata ? $attributeMetadata->getFrontendClass() : ''; } /** + * Check if attribute is required + * * @param string $attributeCode * @return bool */ @@ -259,6 +262,8 @@ private function _isAttributeRequired($attributeCode) } /** + * Check if attribute is visible + * * @param string $attributeCode * @return bool */ diff --git a/app/code/Magento/Customer/Block/Widget/Taxvat.php b/app/code/Magento/Customer/Block/Widget/Taxvat.php index e5c9c01dc3ac..e35f04f592a4 100644 --- a/app/code/Magento/Customer/Block/Widget/Taxvat.php +++ b/app/code/Magento/Customer/Block/Widget/Taxvat.php @@ -63,4 +63,17 @@ public function isRequired() { return $this->_getAttribute('taxvat') ? (bool)$this->_getAttribute('taxvat')->isRequired() : false; } + + /** + * Retrieve store attribute label + * + * @param string $attributeCode + * + * @return string + */ + public function getStoreLabel($attributeCode) + { + $attribute = $this->_getAttribute($attributeCode); + return $attribute ? __($attribute->getStoreLabel()) : ''; + } } diff --git a/app/code/Magento/Customer/Controller/Account/Create.php b/app/code/Magento/Customer/Controller/Account/Create.php index 8f1bc107547e..450fe461534a 100644 --- a/app/code/Magento/Customer/Controller/Account/Create.php +++ b/app/code/Magento/Customer/Controller/Account/Create.php @@ -6,12 +6,13 @@ */ namespace Magento\Customer\Controller\Account; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Customer\Model\Registration; use Magento\Customer\Model\Session; use Magento\Framework\View\Result\PageFactory; use Magento\Framework\App\Action\Context; -class Create extends \Magento\Customer\Controller\AbstractAccount +class Create extends \Magento\Customer\Controller\AbstractAccount implements HttpGetActionInterface { /** * @var \Magento\Customer\Model\Registration diff --git a/app/code/Magento/Customer/Controller/Account/CreatePassword.php b/app/code/Magento/Customer/Controller/Account/CreatePassword.php index fb2e3dd42908..124ac912a7cc 100644 --- a/app/code/Magento/Customer/Controller/Account/CreatePassword.php +++ b/app/code/Magento/Customer/Controller/Account/CreatePassword.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -8,10 +7,16 @@ use Magento\Customer\Api\AccountManagementInterface; use Magento\Customer\Model\Session; +use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Framework\View\Result\PageFactory; use Magento\Framework\App\Action\Context; -class CreatePassword extends \Magento\Customer\Controller\AbstractAccount +/** + * Class CreatePassword + * + * @package Magento\Customer\Controller\Account + */ +class CreatePassword extends \Magento\Customer\Controller\AbstractAccount implements HttpGetActionInterface { /** * @var \Magento\Customer\Api\AccountManagementInterface @@ -54,27 +59,27 @@ public function __construct( public function execute() { $resetPasswordToken = (string)$this->getRequest()->getParam('token'); - $customerId = (int)$this->getRequest()->getParam('id'); - $isDirectLink = $resetPasswordToken != '' && $customerId != 0; + $isDirectLink = $resetPasswordToken != ''; if (!$isDirectLink) { $resetPasswordToken = (string)$this->session->getRpToken(); - $customerId = (int)$this->session->getRpCustomerId(); } try { - $this->accountManagement->validateResetPasswordLinkToken($customerId, $resetPasswordToken); + $this->accountManagement->validateResetPasswordLinkToken(null, $resetPasswordToken); if ($isDirectLink) { $this->session->setRpToken($resetPasswordToken); - $this->session->setRpCustomerId($customerId); $resultRedirect = $this->resultRedirectFactory->create(); $resultRedirect->setPath('*/*/createpassword'); + return $resultRedirect; } else { /** @var \Magento\Framework\View\Result\Page $resultPage */ $resultPage = $this->resultPageFactory->create(); - $resultPage->getLayout()->getBlock('resetPassword')->setCustomerId($customerId) + $resultPage->getLayout() + ->getBlock('resetPassword') ->setResetPasswordLinkToken($resetPasswordToken); + return $resultPage; } } catch (\Exception $exception) { diff --git a/app/code/Magento/Customer/Controller/Account/CreatePost.php b/app/code/Magento/Customer/Controller/Account/CreatePost.php index bb94063226f4..79a575add734 100644 --- a/app/code/Magento/Customer/Controller/Account/CreatePost.php +++ b/app/code/Magento/Customer/Controller/Account/CreatePost.php @@ -5,6 +5,7 @@ */ namespace Magento\Customer\Controller\Account; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Customer\Model\Account\Redirect as AccountRedirect; use Magento\Customer\Api\Data\AddressInterface; use Magento\Framework\Api\DataObjectHelper; @@ -40,7 +41,7 @@ * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class CreatePost extends AbstractAccount implements CsrfAwareActionInterface +class CreatePost extends AbstractAccount implements CsrfAwareActionInterface, HttpPostActionInterface { /** * @var \Magento\Customer\Api\AccountManagementInterface diff --git a/app/code/Magento/Customer/Controller/Account/Edit.php b/app/code/Magento/Customer/Controller/Account/Edit.php index 3c1e60199399..7c2b7215a05e 100644 --- a/app/code/Magento/Customer/Controller/Account/Edit.php +++ b/app/code/Magento/Customer/Controller/Account/Edit.php @@ -6,13 +6,14 @@ */ namespace Magento\Customer\Controller\Account; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Framework\Api\DataObjectHelper; use Magento\Customer\Model\Session; use Magento\Framework\View\Result\PageFactory; use Magento\Framework\App\Action\Context; -class Edit extends \Magento\Customer\Controller\AbstractAccount +class Edit extends \Magento\Customer\Controller\AbstractAccount implements HttpGetActionInterface { /** * @var \Magento\Customer\Api\CustomerRepositoryInterface diff --git a/app/code/Magento/Customer/Controller/Account/EditPost.php b/app/code/Magento/Customer/Controller/Account/EditPost.php index aa5e088f9c89..4eb41cedea29 100644 --- a/app/code/Magento/Customer/Controller/Account/EditPost.php +++ b/app/code/Magento/Customer/Controller/Account/EditPost.php @@ -7,6 +7,9 @@ namespace Magento\Customer\Controller\Account; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\AddressRegistry; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Customer\Model\AuthenticationInterface; use Magento\Customer\Model\Customer\Mapper; use Magento\Customer\Model\EmailNotificationInterface; @@ -21,8 +24,10 @@ use Magento\Customer\Model\CustomerExtractor; use Magento\Customer\Model\Session; use Magento\Framework\App\Action\Context; +use Magento\Framework\Escaper; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\InvalidEmailOrPasswordException; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\State\UserLockedException; use Magento\Customer\Controller\AbstractAccount; use Magento\Framework\Phrase; @@ -31,7 +36,7 @@ * Class EditPost * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class EditPost extends AbstractAccount implements CsrfAwareActionInterface +class EditPost extends AbstractAccount implements CsrfAwareActionInterface, HttpPostActionInterface { /** * Form code for data extractor @@ -78,6 +83,16 @@ class EditPost extends AbstractAccount implements CsrfAwareActionInterface */ private $customerMapper; + /** + * @var Escaper + */ + private $escaper; + + /** + * @var AddressRegistry + */ + private $addressRegistry; + /** * @param Context $context * @param Session $customerSession @@ -85,6 +100,8 @@ class EditPost extends AbstractAccount implements CsrfAwareActionInterface * @param CustomerRepositoryInterface $customerRepository * @param Validator $formKeyValidator * @param CustomerExtractor $customerExtractor + * @param Escaper|null $escaper + * @param AddressRegistry|null $addressRegistry */ public function __construct( Context $context, @@ -92,7 +109,9 @@ public function __construct( AccountManagementInterface $customerAccountManagement, CustomerRepositoryInterface $customerRepository, Validator $formKeyValidator, - CustomerExtractor $customerExtractor + CustomerExtractor $customerExtractor, + ?Escaper $escaper = null, + AddressRegistry $addressRegistry = null ) { parent::__construct($context); $this->session = $customerSession; @@ -100,6 +119,8 @@ public function __construct( $this->customerRepository = $customerRepository; $this->formKeyValidator = $formKeyValidator; $this->customerExtractor = $customerExtractor; + $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class); + $this->addressRegistry = $addressRegistry ?: ObjectManager::getInstance()->get(AddressRegistry::class); } /** @@ -185,6 +206,9 @@ public function execute() // whether a customer enabled change password option $isPasswordChanged = $this->changeCustomerPassword($currentCustomerDataObject->getEmail()); + // No need to validate customer address while editing customer profile + $this->disableAddressValidation($customerCandidateDataObject); + $this->customerRepository->save($customerCandidateDataObject); $this->getEmailNotification()->credentialsChanged( $customerCandidateDataObject, @@ -195,7 +219,7 @@ public function execute() $this->messageManager->addSuccess(__('You saved the account information.')); return $resultRedirect->setPath('customer/account'); } catch (InvalidEmailOrPasswordException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($this->escaper->escapeHtml($e->getMessage())); } catch (UserLockedException $e) { $message = __( 'The account sign-in was incorrect or your account is disabled temporarily. ' @@ -206,9 +230,9 @@ public function execute() $this->messageManager->addError($message); return $resultRedirect->setPath('customer/account/login'); } catch (InputException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($this->escaper->escapeHtml($e->getMessage())); foreach ($e->getErrors() as $error) { - $this->messageManager->addError($error->getMessage()); + $this->messageManager->addErrorMessage($this->escaper->escapeHtml($error->getMessage())); } } catch (\Magento\Framework\Exception\LocalizedException $e) { $this->messageManager->addError($e->getMessage()); @@ -342,4 +366,18 @@ private function getCustomerMapper() } return $this->customerMapper; } + + /** + * Disable Customer Address Validation + * + * @param CustomerInterface $customer + * @throws NoSuchEntityException + */ + private function disableAddressValidation($customer) + { + foreach ($customer->getAddresses() as $address) { + $addressModel = $this->addressRegistry->retrieve($address->getId()); + $addressModel->setShouldIgnoreValidation(true); + } + } } diff --git a/app/code/Magento/Customer/Controller/Account/ForgotPassword.php b/app/code/Magento/Customer/Controller/Account/ForgotPassword.php index f115b64efebd..cfa605580777 100644 --- a/app/code/Magento/Customer/Controller/Account/ForgotPassword.php +++ b/app/code/Magento/Customer/Controller/Account/ForgotPassword.php @@ -1,16 +1,19 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Customer\Controller\Account; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Customer\Model\Session; use Magento\Framework\App\Action\Context; use Magento\Framework\View\Result\PageFactory; -class ForgotPassword extends \Magento\Customer\Controller\AbstractAccount +/** + * Forgot Password controller + */ +class ForgotPassword extends \Magento\Customer\Controller\AbstractAccount implements HttpGetActionInterface { /** * @var PageFactory @@ -40,10 +43,17 @@ public function __construct( /** * Forgot customer password page * - * @return \Magento\Framework\View\Result\Page + * @return \Magento\Framework\Controller\Result\Redirect|\Magento\Framework\View\Result\Page */ public function execute() { + if ($this->session->isLoggedIn()) { + /** @var \Magento\Framework\Controller\Result\Redirect $resultRedirect */ + $resultRedirect = $this->resultRedirectFactory->create(); + $resultRedirect->setPath('*/*/'); + return $resultRedirect; + } + /** @var \Magento\Framework\View\Result\Page $resultPage */ $resultPage = $this->resultPageFactory->create(); $resultPage->getLayout()->getBlock('forgotPassword')->setEmailValue($this->session->getForgottenEmail()); diff --git a/app/code/Magento/Customer/Controller/Account/ForgotPasswordPost.php b/app/code/Magento/Customer/Controller/Account/ForgotPasswordPost.php index f30247387308..3c7fca99184d 100644 --- a/app/code/Magento/Customer/Controller/Account/ForgotPasswordPost.php +++ b/app/code/Magento/Customer/Controller/Account/ForgotPasswordPost.php @@ -6,6 +6,7 @@ */ namespace Magento\Customer\Controller\Account; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Customer\Api\AccountManagementInterface; use Magento\Customer\Model\AccountManagement; use Magento\Customer\Model\Session; @@ -18,7 +19,7 @@ * ForgotPasswordPost controller * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class ForgotPasswordPost extends \Magento\Customer\Controller\AbstractAccount +class ForgotPasswordPost extends \Magento\Customer\Controller\AbstractAccount implements HttpPostActionInterface { /** * @var \Magento\Customer\Api\AccountManagementInterface diff --git a/app/code/Magento/Customer/Controller/Account/Index.php b/app/code/Magento/Customer/Controller/Account/Index.php index 2ecf79d35b11..301fd584cfab 100644 --- a/app/code/Magento/Customer/Controller/Account/Index.php +++ b/app/code/Magento/Customer/Controller/Account/Index.php @@ -6,10 +6,11 @@ */ namespace Magento\Customer\Controller\Account; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Framework\App\Action\Context; use Magento\Framework\View\Result\PageFactory; -class Index extends \Magento\Customer\Controller\AbstractAccount +class Index extends \Magento\Customer\Controller\AbstractAccount implements HttpGetActionInterface { /** * @var PageFactory diff --git a/app/code/Magento/Customer/Controller/Account/Login.php b/app/code/Magento/Customer/Controller/Account/Login.php index d685191bf43b..64cc24095f93 100644 --- a/app/code/Magento/Customer/Controller/Account/Login.php +++ b/app/code/Magento/Customer/Controller/Account/Login.php @@ -6,12 +6,17 @@ */ namespace Magento\Customer\Controller\Account; +use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Customer\Model\Session; use Magento\Framework\App\Action\Context; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\View\Result\PageFactory; use Magento\Customer\Controller\AbstractAccount; -class Login extends AbstractAccount +/** + * Login form page. Accepts POST for backward compatibility reasons. + */ +class Login extends AbstractAccount implements HttpGetActionInterface, HttpPostActionInterface { /** * @var Session diff --git a/app/code/Magento/Customer/Controller/Account/LoginPost.php b/app/code/Magento/Customer/Controller/Account/LoginPost.php index 5c7eee78e5f4..04051fbbf366 100644 --- a/app/code/Magento/Customer/Controller/Account/LoginPost.php +++ b/app/code/Magento/Customer/Controller/Account/LoginPost.php @@ -6,6 +6,7 @@ namespace Magento\Customer\Controller\Account; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Customer\Model\Account\Redirect as AccountRedirect; use Magento\Framework\App\Action\Context; use Magento\Customer\Model\Session; @@ -27,7 +28,7 @@ /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class LoginPost extends AbstractAccount implements CsrfAwareActionInterface +class LoginPost extends AbstractAccount implements CsrfAwareActionInterface, HttpPostActionInterface { /** * @var \Magento\Customer\Api\AccountManagementInterface diff --git a/app/code/Magento/Customer/Controller/Account/Logout.php b/app/code/Magento/Customer/Controller/Account/Logout.php index 19dabf9effa5..9344f482bd6e 100644 --- a/app/code/Magento/Customer/Controller/Account/Logout.php +++ b/app/code/Magento/Customer/Controller/Account/Logout.php @@ -6,6 +6,8 @@ */ namespace Magento\Customer\Controller\Account; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Customer\Model\Session; use Magento\Framework\App\Action\Context; use Magento\Framework\App\ObjectManager; @@ -13,7 +15,10 @@ use Magento\Framework\Stdlib\Cookie\PhpCookieManager; use Magento\Customer\Controller\AbstractAccount; -class Logout extends AbstractAccount +/** + * Sign out a customer. + */ +class Logout extends AbstractAccount implements HttpGetActionInterface, HttpPostActionInterface { /** * @var Session diff --git a/app/code/Magento/Customer/Controller/Account/LogoutSuccess.php b/app/code/Magento/Customer/Controller/Account/LogoutSuccess.php index c58416434c0b..e00494d221d2 100644 --- a/app/code/Magento/Customer/Controller/Account/LogoutSuccess.php +++ b/app/code/Magento/Customer/Controller/Account/LogoutSuccess.php @@ -6,10 +6,11 @@ */ namespace Magento\Customer\Controller\Account; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Framework\App\Action\Context; use Magento\Framework\View\Result\PageFactory; -class LogoutSuccess extends \Magento\Customer\Controller\AbstractAccount +class LogoutSuccess extends \Magento\Customer\Controller\AbstractAccount implements HttpGetActionInterface { /** * @var PageFactory diff --git a/app/code/Magento/Customer/Controller/Account/ResetPasswordPost.php b/app/code/Magento/Customer/Controller/Account/ResetPasswordPost.php index 3de44e35d244..27a00f86dd95 100644 --- a/app/code/Magento/Customer/Controller/Account/ResetPasswordPost.php +++ b/app/code/Magento/Customer/Controller/Account/ResetPasswordPost.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -10,11 +9,16 @@ use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Model\Session; use Magento\Framework\App\Action\Context; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\Exception\InputException; use Magento\Customer\Model\Customer\CredentialsValidator; -use Magento\Framework\App\ObjectManager; -class ResetPasswordPost extends \Magento\Customer\Controller\AbstractAccount +/** + * Class ResetPasswordPost + * + * @package Magento\Customer\Controller\Account + */ +class ResetPasswordPost extends \Magento\Customer\Controller\AbstractAccount implements HttpPostActionInterface { /** * @var \Magento\Customer\Api\AccountManagementInterface @@ -31,17 +35,14 @@ class ResetPasswordPost extends \Magento\Customer\Controller\AbstractAccount */ protected $session; - /** - * @var CredentialsValidator - */ - private $credentialsValidator; - /** * @param Context $context * @param Session $customerSession * @param AccountManagementInterface $accountManagement * @param CustomerRepositoryInterface $customerRepository * @param CredentialsValidator|null $credentialsValidator + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( Context $context, @@ -53,8 +54,6 @@ public function __construct( $this->session = $customerSession; $this->accountManagement = $accountManagement; $this->customerRepository = $customerRepository; - $this->credentialsValidator = $credentialsValidator ?: ObjectManager::getInstance() - ->get(CredentialsValidator::class); parent::__construct($context); } @@ -70,29 +69,32 @@ public function execute() /** @var \Magento\Framework\Controller\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); $resetPasswordToken = (string)$this->getRequest()->getQuery('token'); - $customerId = (int)$this->getRequest()->getQuery('id'); $password = (string)$this->getRequest()->getPost('password'); $passwordConfirmation = (string)$this->getRequest()->getPost('password_confirmation'); if ($password !== $passwordConfirmation) { $this->messageManager->addError(__("New Password and Confirm New Password values didn't match.")); - $resultRedirect->setPath('*/*/createPassword', ['id' => $customerId, 'token' => $resetPasswordToken]); + $resultRedirect->setPath('*/*/createPassword', ['token' => $resetPasswordToken]); + return $resultRedirect; } if (iconv_strlen($password) <= 0) { $this->messageManager->addError(__('Please enter a new password.')); - $resultRedirect->setPath('*/*/createPassword', ['id' => $customerId, 'token' => $resetPasswordToken]); + $resultRedirect->setPath('*/*/createPassword', ['token' => $resetPasswordToken]); + return $resultRedirect; } try { - $customerEmail = $this->customerRepository->getById($customerId)->getEmail(); - $this->credentialsValidator->checkPasswordDifferentFromEmail($customerEmail, $password); - $this->accountManagement->resetPassword($customerEmail, $resetPasswordToken, $password); + $this->accountManagement->resetPassword( + null, + $resetPasswordToken, + $password + ); $this->session->unsRpToken(); - $this->session->unsRpCustomerId(); $this->messageManager->addSuccess(__('You updated your password.')); $resultRedirect->setPath('*/*/login'); + return $resultRedirect; } catch (InputException $e) { $this->messageManager->addError($e->getMessage()); @@ -102,7 +104,8 @@ public function execute() } catch (\Exception $exception) { $this->messageManager->addError(__('Something went wrong while saving the new password.')); } - $resultRedirect->setPath('*/*/createPassword', ['id' => $customerId, 'token' => $resetPasswordToken]); + $resultRedirect->setPath('*/*/createPassword', ['token' => $resetPasswordToken]); + return $resultRedirect; } } diff --git a/app/code/Magento/Customer/Controller/Address/Delete.php b/app/code/Magento/Customer/Controller/Address/Delete.php index ef92bd2ef533..a30e15db4b3f 100644 --- a/app/code/Magento/Customer/Controller/Address/Delete.php +++ b/app/code/Magento/Customer/Controller/Address/Delete.php @@ -6,9 +6,16 @@ */ namespace Magento\Customer\Controller\Address; -class Delete extends \Magento\Customer\Controller\Address +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; + +/** + * Delete customer address controller action. + */ +class Delete extends \Magento\Customer\Controller\Address implements HttpPostActionInterface, HttpGetActionInterface { /** + * @inheritdoc * @return \Magento\Framework\Controller\Result\Redirect */ public function execute() diff --git a/app/code/Magento/Customer/Controller/Address/Edit.php b/app/code/Magento/Customer/Controller/Address/Edit.php index a30eb10b1152..0a5affefae34 100644 --- a/app/code/Magento/Customer/Controller/Address/Edit.php +++ b/app/code/Magento/Customer/Controller/Address/Edit.php @@ -6,7 +6,9 @@ */ namespace Magento\Customer\Controller\Address; -class Edit extends \Magento\Customer\Controller\Address +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Edit extends \Magento\Customer\Controller\Address implements HttpGetActionInterface { /** * Customer address edit action diff --git a/app/code/Magento/Customer/Controller/Address/File/Upload.php b/app/code/Magento/Customer/Controller/Address/File/Upload.php new file mode 100644 index 000000000000..adb4c7abd172 --- /dev/null +++ b/app/code/Magento/Customer/Controller/Address/File/Upload.php @@ -0,0 +1,146 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Controller\Address\File; + +use Magento\Framework\App\Action\Action; +use Magento\Framework\App\Action\Context; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Api\CustomAttributesDataInterface; +use Magento\Customer\Api\AddressMetadataInterface; +use Magento\Customer\Model\FileUploader; +use Magento\Customer\Model\FileUploaderFactory; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\LocalizedException; +use Psr\Log\LoggerInterface; +use Magento\Customer\Model\FileProcessorFactory; + +/** + * Class for upload files for customer custom address attributes + */ +class Upload extends Action implements HttpPostActionInterface +{ + /** + * @var FileUploaderFactory + */ + private $fileUploaderFactory; + + /** + * @var AddressMetadataInterface + */ + private $addressMetadataService; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var FileProcessorFactory + */ + private $fileProcessorFactory; + + /** + * @param Context $context + * @param FileUploaderFactory $fileUploaderFactory + * @param AddressMetadataInterface $addressMetadataService + * @param LoggerInterface $logger + * @param FileProcessorFactory $fileProcessorFactory + */ + public function __construct( + Context $context, + FileUploaderFactory $fileUploaderFactory, + AddressMetadataInterface $addressMetadataService, + LoggerInterface $logger, + FileProcessorFactory $fileProcessorFactory + ) { + $this->fileUploaderFactory = $fileUploaderFactory; + $this->addressMetadataService = $addressMetadataService; + $this->logger = $logger; + $this->fileProcessorFactory = $fileProcessorFactory; + parent::__construct($context); + } + + /** + * @inheritDoc + */ + public function execute() + { + try { + $requestedFiles = $this->getRequest()->getFiles('custom_attributes'); + if (empty($requestedFiles)) { + $result = $this->processError(__('No files for upload.')); + } else { + $attributeCode = key($requestedFiles); + $attributeMetadata = $this->addressMetadataService->getAttributeMetadata($attributeCode); + + /** @var FileUploader $fileUploader */ + $fileUploader = $this->fileUploaderFactory->create([ + 'attributeMetadata' => $attributeMetadata, + 'entityTypeCode' => AddressMetadataInterface::ENTITY_TYPE_ADDRESS, + 'scope' => CustomAttributesDataInterface::CUSTOM_ATTRIBUTES, + ]); + + $errors = $fileUploader->validate(); + if (true !== $errors) { + $errorMessage = implode('</br>', $errors); + $result = $this->processError(($errorMessage)); + } else { + $result = $fileUploader->upload(); + $this->moveTmpFileToSuitableFolder($result); + } + } + } catch (LocalizedException $e) { + $result = $this->processError($e->getMessage(), $e->getCode()); + } catch (\Exception $e) { + $this->logger->critical($e); + $result = $this->processError($e->getMessage(), $e->getCode()); + } + + /** @var \Magento\Framework\Controller\Result\Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $resultJson->setData($result); + return $resultJson; + } + + /** + * Move file from temporary folder to the 'customer_address' media folder + * + * @param array $fileInfo + * @throws LocalizedException + */ + private function moveTmpFileToSuitableFolder(&$fileInfo) + { + $fileName = $fileInfo['file']; + $fileProcessor = $this->fileProcessorFactory + ->create(['entityTypeCode' => AddressMetadataInterface::ENTITY_TYPE_ADDRESS]); + + $newFilePath = $fileProcessor->moveTemporaryFile($fileName); + $fileInfo['file'] = $newFilePath; + $fileInfo['url'] = $fileProcessor->getViewUrl( + $newFilePath, + 'file' + ); + } + + /** + * Prepare result array for errors + * + * @param string $message + * @param int $code + * @return array + */ + private function processError($message, $code = 0) + { + $result = [ + 'error' => $message, + 'errorcode' => $code, + ]; + + return $result; + } +} diff --git a/app/code/Magento/Customer/Controller/Address/Form.php b/app/code/Magento/Customer/Controller/Address/Form.php index fc62a6c1a572..9b3f4e36be0e 100644 --- a/app/code/Magento/Customer/Controller/Address/Form.php +++ b/app/code/Magento/Customer/Controller/Address/Form.php @@ -6,7 +6,9 @@ */ namespace Magento\Customer\Controller\Address; -class Form extends \Magento\Customer\Controller\Address +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Form extends \Magento\Customer\Controller\Address implements HttpGetActionInterface { /** * Address book form diff --git a/app/code/Magento/Customer/Controller/Address/FormPost.php b/app/code/Magento/Customer/Controller/Address/FormPost.php index 60e1f6eb172f..25618e312916 100644 --- a/app/code/Magento/Customer/Controller/Address/FormPost.php +++ b/app/code/Magento/Customer/Controller/Address/FormPost.php @@ -6,6 +6,7 @@ namespace Magento\Customer\Controller\Address; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Customer\Api\AddressRepositoryInterface; use Magento\Customer\Api\Data\AddressInterfaceFactory; use Magento\Customer\Api\Data\RegionInterface; @@ -25,9 +26,11 @@ use Magento\Framework\View\Result\PageFactory; /** + * Customer Address Form Post Controller + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class FormPost extends \Magento\Customer\Controller\Address +class FormPost extends \Magento\Customer\Controller\Address implements HttpPostActionInterface { /** * @var RegionFactory @@ -119,8 +122,18 @@ protected function _extractAddress() \Magento\Customer\Api\Data\AddressInterface::class ); $addressDataObject->setCustomerId($this->_getSession()->getCustomerId()) - ->setIsDefaultBilling($this->getRequest()->getParam('default_billing', false)) - ->setIsDefaultShipping($this->getRequest()->getParam('default_shipping', false)); + ->setIsDefaultBilling( + $this->getRequest()->getParam( + 'default_billing', + isset($existingAddressData['default_billing']) ? $existingAddressData['default_billing'] : false + ) + ) + ->setIsDefaultShipping( + $this->getRequest()->getParam( + 'default_shipping', + isset($existingAddressData['default_shipping']) ? $existingAddressData['default_shipping'] : false + ) + ); return $addressDataObject; } diff --git a/app/code/Magento/Customer/Controller/Address/Index.php b/app/code/Magento/Customer/Controller/Address/Index.php index ad04c7bd5c71..92c6078349d6 100644 --- a/app/code/Magento/Customer/Controller/Address/Index.php +++ b/app/code/Magento/Customer/Controller/Address/Index.php @@ -6,12 +6,13 @@ */ namespace Magento\Customer\Controller\Address; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Customer\Api\CustomerRepositoryInterface; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Index extends \Magento\Customer\Controller\Address +class Index extends \Magento\Customer\Controller\Address implements HttpGetActionInterface { /** * @var CustomerRepositoryInterface @@ -28,9 +29,9 @@ class Index extends \Magento\Customer\Controller\Address * @param \Magento\Customer\Api\Data\RegionInterfaceFactory $regionDataFactory * @param \Magento\Framework\Reflection\DataObjectProcessor $dataProcessor * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper - * @param CustomerRepositoryInterface $customerRepository * @param \Magento\Framework\Controller\Result\ForwardFactory $resultForwardFactory * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory + * @param CustomerRepositoryInterface $customerRepository * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( diff --git a/app/code/Magento/Customer/Controller/Address/NewAction.php b/app/code/Magento/Customer/Controller/Address/NewAction.php index e97c74605788..043c2b91db29 100644 --- a/app/code/Magento/Customer/Controller/Address/NewAction.php +++ b/app/code/Magento/Customer/Controller/Address/NewAction.php @@ -6,7 +6,9 @@ */ namespace Magento\Customer\Controller\Address; -class NewAction extends \Magento\Customer\Controller\Address +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class NewAction extends \Magento\Customer\Controller\Address implements HttpGetActionInterface { /** * @return \Magento\Framework\Controller\Result\Forward diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Address/DefaultBillingAddress.php b/app/code/Magento/Customer/Controller/Adminhtml/Address/DefaultBillingAddress.php new file mode 100644 index 000000000000..bf8ca4aeb5cd --- /dev/null +++ b/app/code/Magento/Customer/Controller/Adminhtml/Address/DefaultBillingAddress.php @@ -0,0 +1,109 @@ +<?php +declare(strict_types=1); +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Customer\Controller\Adminhtml\Address; + +use Magento\Backend\App\Action; +use Magento\Customer\Api\Data\AddressInterface; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\Result\JsonFactory; +use Psr\Log\LoggerInterface; + +/** + * Class to process set default billing address action + */ +class DefaultBillingAddress extends Action implements HttpPostActionInterface +{ + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Customer::manage'; + + /** + * @var AddressRepositoryInterface + */ + private $addressRepository; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var JsonFactory + */ + private $resultJsonFactory; + + /** + * @param Action\Context $context + * @param AddressRepositoryInterface $addressRepository + * @param LoggerInterface $logger + * @param JsonFactory $resultJsonFactory + */ + public function __construct( + Action\Context $context, + AddressRepositoryInterface $addressRepository, + LoggerInterface $logger, + JsonFactory $resultJsonFactory + ) { + parent::__construct($context); + $this->addressRepository = $addressRepository; + $this->logger = $logger; + $this->resultJsonFactory = $resultJsonFactory; + } + + /** + * Execute action to set customer default billing address + * + * @return Json + */ + public function execute(): Json + { + $customerId = $this->getRequest()->getParam('parent_id', false); + $addressId = $this->getRequest()->getParam('id', false); + $error = true; + $message = __('There is no address id in setting default billing address.'); + + if ($addressId) { + try { + $address = $this->addressRepository->getById($addressId)->setCustomerId($customerId); + $this->setAddressAsDefault($address); + $this->addressRepository->save($address); + $message = __('Default billing address has been changed.'); + $error = false; + } catch (\Exception $e) { + $message = __('We can\'t change default billing address right now.'); + $this->logger->critical($e); + } + } + + $resultJson = $this->resultJsonFactory->create(); + $resultJson->setData( + [ + 'message' => $message, + 'error' => $error, + ] + ); + + return $resultJson; + } + + /** + * Set address as default billing address + * + * @param AddressInterface $address + * @return void + */ + private function setAddressAsDefault(AddressInterface $address): void + { + $address->setIsDefaultBilling(true); + } +} diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Address/DefaultShippingAddress.php b/app/code/Magento/Customer/Controller/Adminhtml/Address/DefaultShippingAddress.php new file mode 100644 index 000000000000..81928ae2d28d --- /dev/null +++ b/app/code/Magento/Customer/Controller/Adminhtml/Address/DefaultShippingAddress.php @@ -0,0 +1,109 @@ +<?php +declare(strict_types=1); +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Customer\Controller\Adminhtml\Address; + +use Magento\Backend\App\Action; +use Magento\Customer\Api\Data\AddressInterface; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\Result\JsonFactory; +use Psr\Log\LoggerInterface; + +/** + * Class to process set default shipping address action + */ +class DefaultShippingAddress extends Action implements HttpPostActionInterface +{ + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Customer::manage'; + + /** + * @var AddressRepositoryInterface + */ + private $addressRepository; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var JsonFactory + */ + private $resultJsonFactory; + + /** + * @param Action\Context $context + * @param AddressRepositoryInterface $addressRepository + * @param LoggerInterface $logger + * @param JsonFactory $resultJsonFactory + */ + public function __construct( + Action\Context $context, + AddressRepositoryInterface $addressRepository, + LoggerInterface $logger, + JsonFactory $resultJsonFactory + ) { + parent::__construct($context); + $this->addressRepository = $addressRepository; + $this->logger = $logger; + $this->resultJsonFactory = $resultJsonFactory; + } + + /** + * Execute action to set customer default shipping address + * + * @return Json + */ + public function execute(): Json + { + $customerId = $this->getRequest()->getParam('parent_id', false); + $addressId = $this->getRequest()->getParam('id', false); + $error = true; + $message = __('There is no address id in setting default shipping address.'); + + if ($addressId) { + try { + $address = $this->addressRepository->getById($addressId)->setCustomerId($customerId); + $this->setAddressAsDefault($address); + $this->addressRepository->save($address); + $message = __('Default shipping address has been changed.'); + $error = false; + } catch (\Exception $e) { + $message = __('We can\'t change default shipping address right now.'); + $this->logger->critical($e); + } + } + + $resultJson = $this->resultJsonFactory->create(); + $resultJson->setData( + [ + 'message' => $message, + 'error' => $error, + ] + ); + + return $resultJson; + } + + /** + * Set address as default shipping address + * + * @param AddressInterface $address + * @return void + */ + private function setAddressAsDefault(AddressInterface $address): void + { + $address->setIsDefaultShipping(true); + } +} diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Address/Delete.php b/app/code/Magento/Customer/Controller/Adminhtml/Address/Delete.php new file mode 100644 index 000000000000..711cd2473cdc --- /dev/null +++ b/app/code/Magento/Customer/Controller/Adminhtml/Address/Delete.php @@ -0,0 +1,95 @@ +<?php +declare(strict_types=1); +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Customer\Controller\Adminhtml\Address; + +use Magento\Backend\App\Action; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\Result\JsonFactory; +use Magento\Customer\Api\AddressRepositoryInterface; +use Psr\Log\LoggerInterface; + +/** + * Button for deletion of customer address in admin + */ +class Delete extends Action implements HttpPostActionInterface +{ + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Customer::manage'; + + /** + * @var AddressRepositoryInterface + */ + private $addressRepository; + + /** + * @var JsonFactory + */ + private $resultJsonFactory; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param Action\Context $context + * @param AddressRepositoryInterface $addressRepository + * @param JsonFactory $resultJsonFactory + * @param LoggerInterface $logger + */ + public function __construct( + Action\Context $context, + AddressRepositoryInterface $addressRepository, + JsonFactory $resultJsonFactory, + LoggerInterface $logger + ) { + parent::__construct($context); + $this->addressRepository = $addressRepository; + $this->resultJsonFactory = $resultJsonFactory; + $this->logger = $logger; + } + + /** + * Delete customer address action + * + * @return Json + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function execute(): Json + { + $customerId = $this->getRequest()->getParam('parent_id', false); + $addressId = $this->getRequest()->getParam('id', false); + $error = false; + $message = ''; + if ($addressId && $this->addressRepository->getById($addressId)->getCustomerId() === $customerId) { + try { + $this->addressRepository->deleteById($addressId); + $message = __('You deleted the address.'); + } catch (\Exception $e) { + $error = true; + $message = __('We can\'t delete the address right now.'); + $this->logger->critical($e); + } + } + + $resultJson = $this->resultJsonFactory->create(); + $resultJson->setData( + [ + 'message' => $message, + 'error' => $error, + ] + ); + + return $resultJson; + } +} diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Address/MassDelete.php b/app/code/Magento/Customer/Controller/Adminhtml/Address/MassDelete.php new file mode 100644 index 000000000000..cd319d662a09 --- /dev/null +++ b/app/code/Magento/Customer/Controller/Adminhtml/Address/MassDelete.php @@ -0,0 +1,131 @@ +<?php +declare(strict_types=1); +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Customer\Controller\Adminhtml\Address; + +use Magento\Backend\App\Action; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Backend\App\Action\Context; +use Magento\Framework\Controller\Result\JsonFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Ui\Component\MassAction\Filter; +use Magento\Customer\Model\ResourceModel\Address\CollectionFactory; +use Magento\Customer\Api\AddressRepositoryInterface; +use Psr\Log\LoggerInterface; + +/** + * Class to delete selected customer addresses through massaction + */ +class MassDelete extends Action implements HttpPostActionInterface +{ + /** + * Authorization level of a basic admin session + * + * @see MassDelete::_isAllowed() + */ + const ADMIN_RESOURCE = 'Magento_Customer::manage'; + + /** + * @var Filter + */ + private $filter; + + /** + * @var CollectionFactory + */ + private $collectionFactory; + + /** + * @var AddressRepositoryInterface + */ + private $addressRepository; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var JsonFactory + */ + private $resultJsonFactory; + + /** + * @param Context $context + * @param Filter $filter + * @param CollectionFactory $collectionFactory + * @param AddressRepositoryInterface $addressRepository + * @param LoggerInterface $logger + * @param JsonFactory $resultJsonFactory + */ + public function __construct( + Context $context, + Filter $filter, + CollectionFactory $collectionFactory, + AddressRepositoryInterface $addressRepository, + LoggerInterface $logger, + JsonFactory $resultJsonFactory + ) { + $this->filter = $filter; + $this->collectionFactory = $collectionFactory; + $this->addressRepository = $addressRepository; + $this->logger = $logger; + $this->resultJsonFactory = $resultJsonFactory; + parent::__construct($context); + } + + /** + * Delete specified customer addresses using grid massaction + * + * @return Json + * @throws LocalizedException + */ + public function execute(): Json + { + $customerData = $this->_session->getData('customer_data'); + /** @var \Magento\Customer\Model\ResourceModel\Address\Collection $collection */ + $collection = $this->filter->getCollection($this->collectionFactory->create()); + $error = false; + + try { + if ($customerData && $customerData['customer_id']) { + $collection->addFieldToFilter('parent_id', $customerData['customer_id']); + } else { + throw new \Exception(); + } + $collectionSize = $collection->getSize(); + /** @var \Magento\Customer\Model\Address $address */ + foreach ($collection as $address) { + $this->addressRepository->deleteById($address->getId()); + } + $message = __('A total of %1 record(s) have been deleted.', $collectionSize); + } catch (NoSuchEntityException $e) { + $message = __('There is no such address entity to delete.'); + $error = true; + $this->logger->critical($e); + } catch (LocalizedException $e) { + $message = __($e->getMessage()); + $error = true; + $this->logger->critical($e); + } catch (\Exception $e) { + $message = __('We can\'t mass delete the addresses right now.'); + $error = true; + $this->logger->critical($e); + } + + $resultJson = $this->resultJsonFactory->create(); + $resultJson->setData( + [ + 'message' => $message, + 'error' => $error, + ] + ); + + return $resultJson; + } +} diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Address/Save.php b/app/code/Magento/Customer/Controller/Adminhtml/Address/Save.php new file mode 100644 index 000000000000..e4daea6f59fd --- /dev/null +++ b/app/code/Magento/Customer/Controller/Adminhtml/Address/Save.php @@ -0,0 +1,174 @@ +<?php +declare(strict_types=1); +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Customer\Controller\Adminhtml\Address; + +use Magento\Backend\App\Action; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\Result\JsonFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Psr\Log\LoggerInterface; + +/** + * Class for saving of customer address + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class Save extends Action implements HttpPostActionInterface +{ + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Customer::manage'; + + /** + * @var \Magento\Customer\Api\AddressRepositoryInterface + */ + private $addressRepository; + + /** + * @var \Magento\Customer\Model\Metadata\FormFactory + */ + private $formFactory; + + /** + * @var \Magento\Customer\Api\CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var \Magento\Framework\Api\DataObjectHelper + */ + private $dataObjectHelper; + + /** + * @var \Magento\Customer\Api\Data\AddressInterfaceFactory + */ + private $addressDataFactory; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var JsonFactory + */ + private $resultJsonFactory; + + /** + * @param Action\Context $context + * @param \Magento\Customer\Api\AddressRepositoryInterface $addressRepository + * @param \Magento\Customer\Model\Metadata\FormFactory $formFactory + * @param \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository + * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper + * @param \Magento\Customer\Api\Data\AddressInterfaceFactory $addressDataFactory + * @param LoggerInterface $logger + * @param JsonFactory $resultJsonFactory + */ + public function __construct( + Action\Context $context, + \Magento\Customer\Api\AddressRepositoryInterface $addressRepository, + \Magento\Customer\Model\Metadata\FormFactory $formFactory, + \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository, + \Magento\Framework\Api\DataObjectHelper $dataObjectHelper, + \Magento\Customer\Api\Data\AddressInterfaceFactory $addressDataFactory, + LoggerInterface $logger, + JsonFactory $resultJsonFactory + ) { + parent::__construct($context); + $this->addressRepository = $addressRepository; + $this->formFactory = $formFactory; + $this->customerRepository = $customerRepository; + $this->dataObjectHelper = $dataObjectHelper; + $this->addressDataFactory = $addressDataFactory; + $this->logger = $logger; + $this->resultJsonFactory = $resultJsonFactory; + } + + /** + * Save customer address action + * + * @return Json + */ + public function execute(): Json + { + $customerId = $this->getRequest()->getParam('parent_id', false); + $addressId = $this->getRequest()->getParam('entity_id', false); + + $error = false; + try { + /** @var \Magento\Customer\Api\Data\CustomerInterface $customer */ + $customer = $this->customerRepository->getById($customerId); + + $addressForm = $this->formFactory->create( + 'customer_address', + 'adminhtml_customer_address', + [], + false, + false + ); + $addressData = $addressForm->extractData($this->getRequest()); + $addressData = $addressForm->compactData($addressData); + + $addressData['region'] = [ + 'region' => $addressData['region'] ?? null, + 'region_id' => $addressData['region_id'] ?? null, + ]; + $addressToSave = $this->addressDataFactory->create(); + $this->dataObjectHelper->populateWithArray( + $addressToSave, + $addressData, + \Magento\Customer\Api\Data\AddressInterface::class + ); + $addressToSave->setCustomerId($customer->getId()); + $addressToSave->setIsDefaultBilling( + (bool)$this->getRequest()->getParam('default_billing', false) + ); + $addressToSave->setIsDefaultShipping( + (bool)$this->getRequest()->getParam('default_shipping', false) + ); + if ($addressId) { + $addressToSave->setId($addressId); + $message = __('Customer address has been updated.'); + } else { + $addressToSave->setId(null); + $message = __('New customer address has been added.'); + } + $savedAddress = $this->addressRepository->save($addressToSave); + $addressId = $savedAddress->getId(); + } catch (NoSuchEntityException $e) { + $this->logger->critical($e); + $error = true; + $message = __('There is no customer with such id.'); + } catch (LocalizedException $e) { + $error = true; + $message = __($e->getMessage()); + $this->logger->critical($e); + } catch (\Exception $e) { + $error = true; + $message = __('We can\'t change customer address right now.'); + $this->logger->critical($e); + } + + $addressId = empty($addressId) ? null : $addressId; + $resultJson = $this->resultJsonFactory->create(); + $resultJson->setData( + [ + 'message' => $message, + 'error' => $error, + 'data' => [ + 'entity_id' => $addressId + ] + ] + ); + + return $resultJson; + } +} diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Address/Validate.php b/app/code/Magento/Customer/Controller/Adminhtml/Address/Validate.php new file mode 100644 index 000000000000..16a40fd9016f --- /dev/null +++ b/app/code/Magento/Customer/Controller/Adminhtml/Address/Validate.php @@ -0,0 +1,99 @@ +<?php +declare(strict_types=1); +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Customer\Controller\Adminhtml\Address; + +use Magento\Backend\App\Action; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\DataObject; + +/** + * Class for validation of customer address form on admin. + */ +class Validate extends Action implements HttpPostActionInterface, HttpGetActionInterface +{ + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Customer::manage'; + + /** + * @var \Magento\Framework\Controller\Result\JsonFactory + */ + private $resultJsonFactory; + + /** + * @var \Magento\Customer\Model\Metadata\FormFactory + */ + private $formFactory; + + /** + * @param Action\Context $context + * @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory + * @param \Magento\Customer\Model\Metadata\FormFactory $formFactory + */ + public function __construct( + Action\Context $context, + \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory, + \Magento\Customer\Model\Metadata\FormFactory $formFactory + ) { + parent::__construct($context); + $this->resultJsonFactory = $resultJsonFactory; + $this->formFactory = $formFactory; + } + + /** + * AJAX customer address validation action + * + * @return Json + */ + public function execute(): Json + { + /** @var \Magento\Framework\DataObject $response */ + $response = new \Magento\Framework\DataObject(); + $response->setError(false); + + /** @var \Magento\Framework\DataObject $validatedResponse */ + $validatedResponse = $this->validateCustomerAddress($response); + $resultJson = $this->resultJsonFactory->create(); + if ($validatedResponse->getError()) { + $validatedResponse->setError(true); + $validatedResponse->setMessages($response->getMessages()); + } + + $resultJson->setData($validatedResponse); + + return $resultJson; + } + + /** + * Customer address validation. + * + * @param DataObject $response + * @return \Magento\Framework\DataObject + */ + private function validateCustomerAddress(DataObject $response): DataObject + { + $addressForm = $this->formFactory->create('customer_address', 'adminhtml_customer_address'); + $formData = $addressForm->extractData($this->getRequest()); + + $errors = $addressForm->validateData($formData); + if ($errors !== true) { + $messages = $response->hasMessages() ? $response->getMessages() : []; + foreach ($errors as $error) { + $messages[] = $error; + } + $response->setMessages($messages); + $response->setError(true); + } + + return $response; + } +} diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Customer/InvalidateToken.php b/app/code/Magento/Customer/Controller/Adminhtml/Customer/InvalidateToken.php index 7337d005a732..b69410ecbfce 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Customer/InvalidateToken.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Customer/InvalidateToken.php @@ -7,6 +7,7 @@ namespace Magento\Customer\Controller\Adminhtml\Customer; +use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Integration\Api\CustomerTokenServiceInterface; use Magento\Customer\Api\AccountManagementInterface; use Magento\Customer\Api\AddressRepositoryInterface; @@ -25,8 +26,15 @@ * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.NumberOfChildren) */ -class InvalidateToken extends \Magento\Customer\Controller\Adminhtml\Index +class InvalidateToken extends \Magento\Customer\Controller\Adminhtml\Index implements HttpGetActionInterface { + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + const ADMIN_RESOURCE = 'Magento_Customer::invalidate_tokens'; + /** * @var CustomerTokenServiceInterface */ diff --git a/app/code/Magento/Customer/Controller/Adminhtml/File/Address/Upload.php b/app/code/Magento/Customer/Controller/Adminhtml/File/Address/Upload.php index 506eac323020..e9034c805038 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/File/Address/Upload.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/File/Address/Upload.php @@ -10,11 +10,16 @@ use Magento\Customer\Api\AddressMetadataInterface; use Magento\Customer\Model\FileUploader; use Magento\Customer\Model\FileUploaderFactory; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Exception\LocalizedException; use Psr\Log\LoggerInterface; -class Upload extends Action +/** + * Uploads files for customer address + */ +class Upload extends Action implements HttpGetActionInterface, HttpPostActionInterface { /** * Authorization level of a basic admin session @@ -38,21 +43,29 @@ class Upload extends Action */ private $logger; + /** + * @var string + */ + private $scope; + /** * @param Context $context * @param FileUploaderFactory $fileUploaderFactory * @param AddressMetadataInterface $addressMetadataService * @param LoggerInterface $logger + * @param string $scope */ public function __construct( Context $context, FileUploaderFactory $fileUploaderFactory, AddressMetadataInterface $addressMetadataService, - LoggerInterface $logger + LoggerInterface $logger, + string $scope = 'address' ) { $this->fileUploaderFactory = $fileUploaderFactory; $this->addressMetadataService = $addressMetadataService; $this->logger = $logger; + $this->scope = $scope; parent::__construct($context); } @@ -69,14 +82,14 @@ public function execute() // Must be executed before any operations with $_FILES! $this->convertFilesArray(); - $attributeCode = key($_FILES['address']['name']); + $attributeCode = key($_FILES[$this->scope]['name']); $attributeMetadata = $this->addressMetadataService->getAttributeMetadata($attributeCode); /** @var FileUploader $fileUploader */ $fileUploader = $this->fileUploaderFactory->create([ 'attributeMetadata' => $attributeMetadata, 'entityTypeCode' => AddressMetadataInterface::ENTITY_TYPE_ADDRESS, - 'scope' => 'address', + 'scope' => $this->scope, ]); $errors = $fileUploader->validate(); @@ -114,14 +127,11 @@ public function execute() */ private function convertFilesArray() { - foreach ($_FILES['address'] as $itemKey => $item) { - foreach ($item as $value) { - if (is_array($value)) { - $_FILES['address'][$itemKey] = [ - key($value) => current($value), - ]; - } + foreach ($_FILES as $itemKey => $item) { + foreach ($item as $fieldName => $value) { + $_FILES[$this->scope][$fieldName] = [$itemKey => $value]; } + unset($_FILES[$itemKey]); } } } diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Group/Delete.php b/app/code/Magento/Customer/Controller/Adminhtml/Group/Delete.php index 571ef57702bc..ab32ea08a44a 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Group/Delete.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Group/Delete.php @@ -6,9 +6,10 @@ */ namespace Magento\Customer\Controller\Adminhtml\Group; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\Exception\NoSuchEntityException; -class Delete extends \Magento\Customer\Controller\Adminhtml\Group +class Delete extends \Magento\Customer\Controller\Adminhtml\Group implements HttpPostActionInterface { /** * Delete customer group. diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Group/Edit.php b/app/code/Magento/Customer/Controller/Adminhtml/Group/Edit.php index 9da132078c9c..221d01a112ea 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Group/Edit.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Group/Edit.php @@ -6,7 +6,9 @@ */ namespace Magento\Customer\Controller\Adminhtml\Group; -class Edit extends \Magento\Customer\Controller\Adminhtml\Group +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Edit extends \Magento\Customer\Controller\Adminhtml\Group implements HttpGetActionInterface { /** * Edit customer group action. Forward to new action. diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Group/Index.php b/app/code/Magento/Customer/Controller/Adminhtml/Group/Index.php index 1246b6a6d0bd..6da79d2c40f3 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Group/Index.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Group/Index.php @@ -6,7 +6,9 @@ */ namespace Magento\Customer\Controller\Adminhtml\Group; -class Index extends \Magento\Customer\Controller\Adminhtml\Group +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\Customer\Controller\Adminhtml\Group implements HttpGetActionInterface { /** * Customer groups list. diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Group/NewAction.php b/app/code/Magento/Customer/Controller/Adminhtml/Group/NewAction.php index 2c230cd0b3f7..a5c832bf0f1e 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Group/NewAction.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Group/NewAction.php @@ -6,9 +6,10 @@ */ namespace Magento\Customer\Controller\Adminhtml\Group; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Customer\Controller\RegistryConstants; -class NewAction extends \Magento\Customer\Controller\Adminhtml\Group +class NewAction extends \Magento\Customer\Controller\Adminhtml\Group implements HttpGetActionInterface { /** * Initialize current group and set it in the registry. diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php b/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php index 936d9cdbc170..7549315f9ffc 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php @@ -6,11 +6,12 @@ */ namespace Magento\Customer\Controller\Adminhtml\Group; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Customer\Api\Data\GroupInterfaceFactory; use Magento\Customer\Api\Data\GroupInterface; use Magento\Customer\Api\GroupRepositoryInterface; -class Save extends \Magento\Customer\Controller\Adminhtml\Group +class Save extends \Magento\Customer\Controller\Adminhtml\Group implements HttpPostActionInterface { /** * @var \Magento\Framework\Reflection\DataObjectProcessor diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/Delete.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/Delete.php index 7a981b82b7e1..ab39ca098162 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/Delete.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/Delete.php @@ -5,10 +5,21 @@ */ namespace Magento\Customer\Controller\Adminhtml\Index; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\Controller\ResultFactory; -class Delete extends \Magento\Customer\Controller\Adminhtml\Index +/** + * Delete customer action. + */ +class Delete extends \Magento\Customer\Controller\Adminhtml\Index implements HttpPostActionInterface { + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + const ADMIN_RESOURCE = 'Magento_Customer::delete'; + /** * Delete customer action * diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/Edit.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/Edit.php index 90417c1ad544..25b4ddd4e173 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/Edit.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/Edit.php @@ -5,10 +5,11 @@ */ namespace Magento\Customer\Controller\Adminhtml\Index; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Customer\Api\Data\CustomerInterface; use Magento\Framework\Exception\NoSuchEntityException; -class Edit extends \Magento\Customer\Controller\Adminhtml\Index +class Edit extends \Magento\Customer\Controller\Adminhtml\Index implements HttpGetActionInterface { /** * Customer edit action diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/Index.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/Index.php index 861ac93b262c..b1986c8b6f08 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/Index.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/Index.php @@ -5,7 +5,9 @@ */ namespace Magento\Customer\Controller\Adminhtml\Index; -class Index extends \Magento\Customer\Controller\Adminhtml\Index +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\Customer\Controller\Adminhtml\Index implements HttpGetActionInterface { /** * Customers list action diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/InlineEdit.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/InlineEdit.php index 2d0ee3ae13da..7220de035681 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/InlineEdit.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/InlineEdit.php @@ -6,16 +6,22 @@ namespace Magento\Customer\Controller\Adminhtml\Index; use Magento\Backend\App\Action; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\AddressRegistry; use Magento\Customer\Model\EmailNotificationInterface; -use Magento\Customer\Test\Block\Form\Login; use Magento\Customer\Ui\Component\Listing\AttributeRepository; -use Magento\Customer\Api\Data\CustomerInterface; -use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Message\MessageInterface; +use Magento\Framework\App\ObjectManager; /** + * Customer inline edit action + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class InlineEdit extends \Magento\Backend\App\Action +class InlineEdit extends \Magento\Backend\App\Action implements HttpPostActionInterface { /** * Authorization level of a basic admin session @@ -59,6 +65,11 @@ class InlineEdit extends \Magento\Backend\App\Action */ private $emailNotification; + /** + * @var AddressRegistry + */ + private $addressRegistry; + /** * @param Action\Context $context * @param CustomerRepositoryInterface $customerRepository @@ -66,6 +77,7 @@ class InlineEdit extends \Magento\Backend\App\Action * @param \Magento\Customer\Model\Customer\Mapper $customerMapper * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper * @param \Psr\Log\LoggerInterface $logger + * @param AddressRegistry|null $addressRegistry */ public function __construct( Action\Context $context, @@ -73,13 +85,15 @@ public function __construct( \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory, \Magento\Customer\Model\Customer\Mapper $customerMapper, \Magento\Framework\Api\DataObjectHelper $dataObjectHelper, - \Psr\Log\LoggerInterface $logger + \Psr\Log\LoggerInterface $logger, + AddressRegistry $addressRegistry = null ) { $this->customerRepository = $customerRepository; $this->resultJsonFactory = $resultJsonFactory; $this->customerMapper = $customerMapper; $this->dataObjectHelper = $dataObjectHelper; $this->logger = $logger; + $this->addressRegistry = $addressRegistry ?: ObjectManager::getInstance()->get(AddressRegistry::class); parent::__construct($context); } @@ -101,7 +115,11 @@ private function getEmailNotification() } /** + * Inline edit action execute + * * @return \Magento\Framework\Controller\Result\Json + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function execute() { @@ -139,7 +157,7 @@ public function execute() * Receive entity(customer|customer_address) data from request * * @param array $data - * @param null $isCustomerData + * @param mixed $isCustomerData * @return array */ protected function getData(array $data, $isCustomerData = null) @@ -212,6 +230,8 @@ protected function updateDefaultBilling(array $data) protected function saveCustomer(CustomerInterface $customer) { try { + // No need to validate customer address during inline edit action + $this->disableAddressValidation($customer); $this->customerRepository->save($customer); } catch (\Magento\Framework\Exception\InputException $e) { $this->getMessageManager()->addError($this->getErrorWithCustomerId($e->getMessage())); @@ -249,7 +269,7 @@ protected function processAddressData(array $data) protected function getErrorMessages() { $messages = []; - foreach ($this->getMessageManager()->getMessages()->getItems() as $error) { + foreach ($this->getMessageManager()->getMessages()->getErrors() as $error) { $messages[] = $error->getText(); } return $messages; @@ -262,7 +282,7 @@ protected function getErrorMessages() */ protected function isErrorExists() { - return (bool)$this->getMessageManager()->getMessages(true)->getCount(); + return (bool)$this->getMessageManager()->getMessages(true)->getCountByType(MessageInterface::TYPE_ERROR); } /** @@ -297,4 +317,18 @@ protected function getErrorWithCustomerId($errorText) { return '[Customer ID: ' . $this->getCustomer()->getId() . '] ' . __($errorText); } + + /** + * Disable Customer Address Validation + * + * @param CustomerInterface $customer + * @throws NoSuchEntityException + */ + private function disableAddressValidation($customer) + { + foreach ($customer->getAddresses() as $address) { + $addressModel = $this->addressRegistry->retrieve($address->getId()); + $addressModel->setShouldIgnoreValidation(true); + } + } } diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroup.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroup.php index 762b872b97b6..5a9c52bf9b1c 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroup.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroup.php @@ -5,6 +5,8 @@ */ namespace Magento\Customer\Controller\Adminhtml\Index; +use Magento\Customer\Model\Customer; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Backend\App\Action\Context; use Magento\Customer\Model\ResourceModel\Customer\CollectionFactory; use Magento\Eav\Model\Entity\Collection\AbstractCollection; @@ -15,7 +17,7 @@ /** * Class MassAssignGroup */ -class MassAssignGroup extends AbstractMassAction +class MassAssignGroup extends AbstractMassAction implements HttpPostActionInterface { /** * @var CustomerRepositoryInterface @@ -51,6 +53,8 @@ protected function massAction(AbstractCollection $collection) // Verify customer exists $customer = $this->customerRepository->getById($customerId); $customer->setGroupId($this->getRequest()->getParam('group')); + // No need to validate customer and customer address during assigning customer to the group + $this->setIgnoreValidationFlag($customer); $this->customerRepository->save($customer); $customersUpdated++; } @@ -64,4 +68,15 @@ protected function massAction(AbstractCollection $collection) return $resultRedirect; } + + /** + * Set ignore_validation_flag to skip unnecessary address and customer validation + * + * @param Customer $customer + * @return void + */ + private function setIgnoreValidationFlag($customer) + { + $customer->setData('ignore_validation_flag', true); + } } diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/MassDelete.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/MassDelete.php index 453585c881a0..edaeea6a15eb 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/MassDelete.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/MassDelete.php @@ -5,6 +5,7 @@ */ namespace Magento\Customer\Controller\Adminhtml\Index; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Backend\App\Action\Context; use Magento\Customer\Model\ResourceModel\Customer\CollectionFactory; use Magento\Eav\Model\Entity\Collection\AbstractCollection; @@ -15,8 +16,15 @@ /** * Class MassDelete */ -class MassDelete extends AbstractMassAction +class MassDelete extends AbstractMassAction implements HttpPostActionInterface { + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + const ADMIN_RESOURCE = 'Magento_Customer::delete'; + /** * @var CustomerRepositoryInterface */ @@ -39,8 +47,7 @@ public function __construct( } /** - * @param AbstractCollection $collection - * @return \Magento\Backend\Model\View\Result\Redirect + * @inheritdoc */ protected function massAction(AbstractCollection $collection) { diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/NewAction.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/NewAction.php index e6cf2aa234e0..19a16f01acfc 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/NewAction.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/NewAction.php @@ -5,7 +5,9 @@ */ namespace Magento\Customer\Controller\Adminhtml\Index; -class NewAction extends \Magento\Customer\Controller\Adminhtml\Index +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class NewAction extends \Magento\Customer\Controller\Adminhtml\Index implements HttpGetActionInterface { /** * Create new customer action diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/ResetPassword.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/ResetPassword.php index 37c8ed5a252f..1e4fa91cbf89 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/ResetPassword.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/ResetPassword.php @@ -5,11 +5,24 @@ */ namespace Magento\Customer\Controller\Adminhtml\Index; +use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\SecurityViolationException; -class ResetPassword extends \Magento\Customer\Controller\Adminhtml\Index +/** + * Reset password controller + * + * @package Magento\Customer\Controller\Adminhtml\Index + */ +class ResetPassword extends \Magento\Customer\Controller\Adminhtml\Index implements HttpGetActionInterface { + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + const ADMIN_RESOURCE = 'Magento_Customer::reset_password'; + /** * Reset password handler * diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php index 12732f81f78a..38ed688a835b 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php @@ -5,6 +5,16 @@ */ namespace Magento\Customer\Controller\Adminhtml\Index; +use Magento\Customer\Api\AccountManagementInterface; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Model\Address\Mapper; +use Magento\Customer\Model\AddressRegistry; +use Magento\Framework\Api\DataObjectHelper; +use Magento\Customer\Api\Data\AddressInterfaceFactory; +use Magento\Customer\Api\Data\CustomerInterfaceFactory; +use Magento\Framework\DataObjectFactory as ObjectFactory; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Customer\Api\AddressMetadataInterface; use Magento\Customer\Api\CustomerMetadataInterface; use Magento\Customer\Api\Data\CustomerInterface; @@ -12,17 +22,115 @@ use Magento\Customer\Model\EmailNotificationInterface; use Magento\Customer\Model\Metadata\Form; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\App\ObjectManager; /** + * Save customer action. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Save extends \Magento\Customer\Controller\Adminhtml\Index +class Save extends \Magento\Customer\Controller\Adminhtml\Index implements HttpPostActionInterface { /** * @var EmailNotificationInterface */ private $emailNotification; + /** + * @var AddressRegistry + */ + private $addressRegistry; + + /** + * Constructor + * + * @param \Magento\Backend\App\Action\Context $context + * @param \Magento\Framework\Registry $coreRegistry + * @param \Magento\Framework\App\Response\Http\FileFactory $fileFactory + * @param \Magento\Customer\Model\CustomerFactory $customerFactory + * @param \Magento\Customer\Model\AddressFactory $addressFactory + * @param \Magento\Customer\Model\Metadata\FormFactory $formFactory + * @param \Magento\Newsletter\Model\SubscriberFactory $subscriberFactory + * @param \Magento\Customer\Helper\View $viewHelper + * @param \Magento\Framework\Math\Random $random + * @param CustomerRepositoryInterface $customerRepository + * @param \Magento\Framework\Api\ExtensibleDataObjectConverter $extensibleDataObjectConverter + * @param Mapper $addressMapper + * @param AccountManagementInterface $customerAccountManagement + * @param AddressRepositoryInterface $addressRepository + * @param CustomerInterfaceFactory $customerDataFactory + * @param AddressInterfaceFactory $addressDataFactory + * @param \Magento\Customer\Model\Customer\Mapper $customerMapper + * @param \Magento\Framework\Reflection\DataObjectProcessor $dataObjectProcessor + * @param DataObjectHelper $dataObjectHelper + * @param ObjectFactory $objectFactory + * @param \Magento\Framework\View\LayoutFactory $layoutFactory + * @param \Magento\Framework\View\Result\LayoutFactory $resultLayoutFactory + * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory + * @param \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory + * @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory + * @param AddressRegistry|null $addressRegistry + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + \Magento\Backend\App\Action\Context $context, + \Magento\Framework\Registry $coreRegistry, + \Magento\Framework\App\Response\Http\FileFactory $fileFactory, + \Magento\Customer\Model\CustomerFactory $customerFactory, + \Magento\Customer\Model\AddressFactory $addressFactory, + \Magento\Customer\Model\Metadata\FormFactory $formFactory, + \Magento\Newsletter\Model\SubscriberFactory $subscriberFactory, + \Magento\Customer\Helper\View $viewHelper, + \Magento\Framework\Math\Random $random, + CustomerRepositoryInterface $customerRepository, + \Magento\Framework\Api\ExtensibleDataObjectConverter $extensibleDataObjectConverter, + Mapper $addressMapper, + AccountManagementInterface $customerAccountManagement, + AddressRepositoryInterface $addressRepository, + CustomerInterfaceFactory $customerDataFactory, + AddressInterfaceFactory $addressDataFactory, + \Magento\Customer\Model\Customer\Mapper $customerMapper, + \Magento\Framework\Reflection\DataObjectProcessor $dataObjectProcessor, + DataObjectHelper $dataObjectHelper, + ObjectFactory $objectFactory, + \Magento\Framework\View\LayoutFactory $layoutFactory, + \Magento\Framework\View\Result\LayoutFactory $resultLayoutFactory, + \Magento\Framework\View\Result\PageFactory $resultPageFactory, + \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory, + \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory, + AddressRegistry $addressRegistry = null + ) { + parent::__construct( + $context, + $coreRegistry, + $fileFactory, + $customerFactory, + $addressFactory, + $formFactory, + $subscriberFactory, + $viewHelper, + $random, + $customerRepository, + $extensibleDataObjectConverter, + $addressMapper, + $customerAccountManagement, + $addressRepository, + $customerDataFactory, + $addressDataFactory, + $customerMapper, + $dataObjectProcessor, + $dataObjectHelper, + $objectFactory, + $layoutFactory, + $resultLayoutFactory, + $resultPageFactory, + $resultForwardFactory, + $resultJsonFactory + ); + $this->addressRegistry = $addressRegistry ?: ObjectManager::getInstance()->get(AddressRegistry::class); + } + /** * Reformat customer account data to be compatible with customer service interface * @@ -107,6 +215,7 @@ protected function _extractData( /** * Saves default_billing and default_shipping flags for customer address * + * @deprecated must be removed because addresses are save separately for now * @param array $addressIdList * @param array $extractedCustomerData * @return array @@ -149,6 +258,7 @@ protected function saveDefaultFlags(array $addressIdList, array & $extractedCust /** * Reformat customer addresses data to be compatible with customer service interface * + * @deprecated addresses are saved separately for now * @param array $extractedCustomerData * @return array */ @@ -179,18 +289,17 @@ protected function _extractCustomerAddressData(array & $extractedCustomerData) public function execute() { $returnToEdit = false; - $originalRequestData = $this->getRequest()->getPostValue(); - $customerId = $this->getCurrentCustomerId(); - if ($originalRequestData) { + if ($this->getRequest()->getPostValue()) { try { // optional fields might be set in request for future processing by observers in other modules $customerData = $this->_extractCustomerData(); - $addressesData = $this->_extractCustomerAddressData($customerData); if ($customerId) { $currentCustomer = $this->_customerRepository->getById($customerId); + // No need to validate customer address while editing customer profile + $this->disableAddressValidation($currentCustomer); $customerData = array_merge( $this->customerMapper->toFlatArray($currentCustomer), $customerData @@ -205,28 +314,12 @@ public function execute() $customerData, \Magento\Customer\Api\Data\CustomerInterface::class ); - $addresses = []; - foreach ($addressesData as $addressData) { - $region = isset($addressData['region']) ? $addressData['region'] : null; - $regionId = isset($addressData['region_id']) ? $addressData['region_id'] : null; - $addressData['region'] = [ - 'region' => $region, - 'region_id' => $regionId, - ]; - $addressDataObject = $this->addressDataFactory->create(); - $this->dataObjectHelper->populateWithArray( - $addressDataObject, - $addressData, - \Magento\Customer\Api\Data\AddressInterface::class - ); - $addresses[] = $addressDataObject; - } $this->_eventManager->dispatch( 'adminhtml_customer_prepare_save', ['customer' => $customer, 'request' => $this->getRequest()] ); - $customer->setAddresses($addresses); + if (isset($customerData['sendemail_store_id'])) { $customer->setStoreId($customerData['sendemail_store_id']); } @@ -269,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(); @@ -278,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) { @@ -380,4 +474,43 @@ private function getCurrentCustomerId() return $customerId; } + + /** + * Disable Customer Address Validation + * + * @param CustomerInterface $customer + * @throws NoSuchEntityException + */ + private function disableAddressValidation($customer) + { + foreach ($customer->getAddresses() as $address) { + $addressModel = $this->addressRegistry->retrieve($address->getId()); + $addressModel->setShouldIgnoreValidation(true); + } + } + + /** + * Retrieve formatted form data + * + * @return array + */ + private function retrieveFormattedFormData(): array + { + $originalRequestData = $this->getRequest()->getPostValue(); + + /* Customer data filtration */ + if (isset($originalRequestData['customer'])) { + $customerData = $this->_extractData( + 'adminhtml_customer', + CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, + [], + 'customer' + ); + + $customerData = array_intersect_key($customerData, $originalRequestData['customer']); + $originalRequestData['customer'] = array_merge($originalRequestData['customer'], $customerData); + } + + return $originalRequestData; + } } diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/Validate.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/Validate.php index 8e098a3b7ee1..d91bc7424bff 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/Validate.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/Validate.php @@ -5,10 +5,16 @@ */ namespace Magento\Customer\Controller\Adminhtml\Index; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Customer\Api\Data\CustomerInterface; use Magento\Framework\Message\Error; +use Magento\Customer\Controller\Adminhtml\Index as CustomerAction; -class Validate extends \Magento\Customer\Controller\Adminhtml\Index +/** + * Class for validation of customer + */ +class Validate extends CustomerAction implements HttpPostActionInterface, HttpGetActionInterface { /** * Customer validation @@ -69,40 +75,6 @@ protected function _validateCustomer($response) return $customer; } - /** - * Customer address validation. - * - * @param \Magento\Framework\DataObject $response - * @return void - */ - protected function _validateCustomerAddress($response) - { - $addresses = $this->getRequest()->getPost('address'); - if (!is_array($addresses)) { - return; - } - foreach (array_keys($addresses) as $index) { - if ($index == '_template_') { - continue; - } - - $addressForm = $this->_formFactory->create('customer_address', 'adminhtml_customer_address'); - - $requestScope = sprintf('address/%s', $index); - $formData = $addressForm->extractData($this->getRequest(), $requestScope); - - $errors = $addressForm->validateData($formData); - if ($errors !== true) { - $messages = $response->hasMessages() ? $response->getMessages() : []; - foreach ($errors as $error) { - $messages[] = $error; - } - $response->setMessages($messages); - $response->setError(1); - } - } - } - /** * AJAX customer validation action * @@ -113,10 +85,7 @@ public function execute() $response = new \Magento\Framework\DataObject(); $response->setError(0); - $customer = $this->_validateCustomer($response); - if ($customer) { - $this->_validateCustomerAddress($response); - } + $this->_validateCustomer($response); $resultJson = $this->resultJsonFactory->create(); if ($response->getError()) { $response->setError(true); diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/Viewfile.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/Viewfile.php index 20d330354bce..02a045086224 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/Viewfile.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/Viewfile.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Customer\Controller\Adminhtml\Index; use Magento\Customer\Api\AccountManagementInterface; @@ -17,7 +19,10 @@ use Magento\Framework\DataObjectFactory; /** + * Class Viewfile serves to show file or image by file/image name provided in request parameters. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.AllPurposeAction) */ class Viewfile extends \Magento\Customer\Controller\Adminhtml\Index { @@ -127,8 +132,6 @@ public function __construct( * * @return \Magento\Framework\Controller\ResultInterface|void * @throws NotFoundException - * - * @SuppressWarnings(PHPMD.ExitExpression) */ public function execute() { @@ -146,6 +149,7 @@ public function execute() } if ($plain) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $extension = pathinfo($path, PATHINFO_EXTENSION); switch (strtolower($extension)) { case 'gif': @@ -175,6 +179,7 @@ public function execute() $resultRaw->setContents($directory->readFile($fileName)); return $resultRaw; } else { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $name = pathinfo($path, PATHINFO_BASENAME); $this->_fileFactory->create( $name, diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Online/Index.php b/app/code/Magento/Customer/Controller/Adminhtml/Online/Index.php index 3cf9a82b8b87..0262513d029f 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Online/Index.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Online/Index.php @@ -6,10 +6,11 @@ */ namespace Magento\Customer\Controller\Adminhtml\Online; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Backend\App\Action\Context; use Magento\Framework\View\Result\PageFactory; -class Index extends \Magento\Backend\App\Action +class Index extends \Magento\Backend\App\Action implements HttpGetActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Customer/Controller/Ajax/Login.php b/app/code/Magento/Customer/Controller/Ajax/Login.php index 73869bc3f295..5049c83e60f3 100644 --- a/app/code/Magento/Customer/Controller/Ajax/Login.php +++ b/app/code/Magento/Customer/Controller/Ajax/Login.php @@ -6,6 +6,7 @@ namespace Magento\Customer\Controller\Ajax; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Customer\Api\AccountManagementInterface; use Magento\Framework\Exception\EmailNotConfirmedException; use Magento\Framework\Exception\InvalidEmailOrPasswordException; @@ -23,12 +24,12 @@ * @method \Magento\Framework\App\Response\Http getResponse() * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Login extends \Magento\Framework\App\Action\Action +class Login extends \Magento\Framework\App\Action\Action implements HttpPostActionInterface { /** - * @var \Magento\Framework\Session\Generic + * @var \Magento\Customer\Model\Session */ - protected $session; + protected $customerSession; /** * @var AccountManagementInterface @@ -106,7 +107,6 @@ public function __construct( /** * Get account redirect. - * For release backward compatibility. * * @deprecated 100.0.10 * @return AccountRedirect @@ -132,6 +132,8 @@ public function setAccountRedirect($value) } /** + * Initializes config dependency. + * * @deprecated 100.0.10 * @return ScopeConfigInterface */ @@ -144,6 +146,8 @@ protected function getScopeConfig() } /** + * Sets config dependency. + * * @deprecated 100.0.10 * @param ScopeConfigInterface $value * @return void @@ -198,25 +202,15 @@ public function execute() $response['redirectUrl'] = $this->_redirect->success($redirectRoute); $this->getAccountRedirect()->clearRedirectCookie(); } - } catch (EmailNotConfirmedException $e) { - $response = [ - 'errors' => true, - 'message' => $e->getMessage() - ]; - } catch (InvalidEmailOrPasswordException $e) { - $response = [ - 'errors' => true, - 'message' => $e->getMessage() - ]; } catch (LocalizedException $e) { $response = [ 'errors' => true, - 'message' => $e->getMessage() + 'message' => $e->getMessage(), ]; } catch (\Exception $e) { $response = [ 'errors' => true, - 'message' => __('Invalid login or password.') + 'message' => __('Invalid login or password.'), ]; } /** @var \Magento\Framework\Controller\Result\Json $resultJson */ diff --git a/app/code/Magento/Customer/Controller/Ajax/Logout.php b/app/code/Magento/Customer/Controller/Ajax/Logout.php index 0edd41e3632b..c9eadacd12e6 100644 --- a/app/code/Magento/Customer/Controller/Ajax/Logout.php +++ b/app/code/Magento/Customer/Controller/Ajax/Logout.php @@ -7,18 +7,20 @@ namespace Magento\Customer\Controller\Ajax; +use Magento\Framework\App\Action\HttpGetActionInterface; + /** * Logout controller * * @method \Magento\Framework\App\RequestInterface getRequest() * @method \Magento\Framework\App\Response\Http getResponse() */ -class Logout extends \Magento\Framework\App\Action\Action +class Logout extends \Magento\Framework\App\Action\Action implements HttpGetActionInterface { /** - * @var \Magento\Framework\Session\Generic + * @var \Magento\Customer\Model\Session */ - protected $session; + protected $customerSession; /** * @var \Magento\Framework\Controller\Result\JsonFactory diff --git a/app/code/Magento/Customer/Controller/Section/Load.php b/app/code/Magento/Customer/Controller/Section/Load.php index 7a2345c91750..37cd071b1362 100644 --- a/app/code/Magento/Customer/Controller/Section/Load.php +++ b/app/code/Magento/Customer/Controller/Section/Load.php @@ -5,6 +5,7 @@ */ namespace Magento\Customer\Controller\Section; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Customer\CustomerData\Section\Identifier; use Magento\Customer\CustomerData\SectionPoolInterface; use Magento\Framework\App\Action\Context; @@ -13,7 +14,7 @@ /** * Customer section controller */ -class Load extends \Magento\Framework\App\Action\Action +class Load extends \Magento\Framework\App\Action\Action implements HttpGetActionInterface { /** * @var JsonFactory @@ -58,7 +59,7 @@ public function __construct( } /** - * @return \Magento\Framework\Controller\Result\Json + * @inheritdoc */ public function execute() { @@ -70,11 +71,11 @@ public function execute() $sectionNames = $this->getRequest()->getParam('sections'); $sectionNames = $sectionNames ? array_unique(\explode(',', $sectionNames)) : null; - $updateSectionId = $this->getRequest()->getParam('update_section_id'); - if ('false' === $updateSectionId) { - $updateSectionId = false; + $forceNewSectionTimestamp = $this->getRequest()->getParam('force_new_section_timestamp'); + if ('false' === $forceNewSectionTimestamp) { + $forceNewSectionTimestamp = false; } - $response = $this->sectionPool->getSectionsData($sectionNames, (bool)$updateSectionId); + $response = $this->sectionPool->getSectionsData($sectionNames, (bool)$forceNewSectionTimestamp); } catch (\Exception $e) { $resultJson->setStatusHeader( \Zend\Http\Response::STATUS_CODE_400, diff --git a/app/code/Magento/Customer/CustomerData/Plugin/SessionChecker.php b/app/code/Magento/Customer/CustomerData/Plugin/SessionChecker.php index aa73e275ee0c..f82a4d15ae8b 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/CustomerData/Section/Identifier.php b/app/code/Magento/Customer/CustomerData/Section/Identifier.php index 2a770925d1c3..a8bc2c8abc11 100644 --- a/app/code/Magento/Customer/CustomerData/Section/Identifier.php +++ b/app/code/Magento/Customer/CustomerData/Section/Identifier.php @@ -43,12 +43,12 @@ public function __construct( /** * Init mark(identifier) for sections * - * @param bool $forceUpdate + * @param bool $forceNewTimestamp * @return int */ - public function initMark($forceUpdate) + public function initMark($forceNewTimestamp) { - if ($forceUpdate) { + if ($forceNewTimestamp) { $this->markId = time(); return $this->markId; } @@ -67,19 +67,19 @@ public function initMark($forceUpdate) * Mark sections with data id * * @param array $sectionsData - * @param null $sectionNames - * @param bool $updateIds + * @param array|null $sectionNames + * @param bool $forceNewTimestamp * @return array */ - public function markSections(array $sectionsData, $sectionNames = null, $updateIds = false) + public function markSections(array $sectionsData, $sectionNames = null, $forceNewTimestamp = false) { if (!$sectionNames) { $sectionNames = array_keys($sectionsData); } - $markId = $this->initMark($updateIds); + $markId = $this->initMark($forceNewTimestamp); foreach ($sectionNames as $name) { - if ($updateIds || !array_key_exists(self::SECTION_KEY, $sectionsData[$name])) { + if ($forceNewTimestamp || !array_key_exists(self::SECTION_KEY, $sectionsData[$name])) { $sectionsData[$name][self::SECTION_KEY] = $markId; } } diff --git a/app/code/Magento/Customer/CustomerData/SectionPool.php b/app/code/Magento/Customer/CustomerData/SectionPool.php index 0e0d7b992e33..efea1762d9de 100644 --- a/app/code/Magento/Customer/CustomerData/SectionPool.php +++ b/app/code/Magento/Customer/CustomerData/SectionPool.php @@ -53,12 +53,12 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ - public function getSectionsData(array $sectionNames = null, $updateIds = false) + public function getSectionsData(array $sectionNames = null, $forceNewTimestamp = false) { $sectionsData = $sectionNames ? $this->getSectionDataByNames($sectionNames) : $this->getAllSectionData(); - $sectionsData = $this->identifier->markSections($sectionsData, $sectionNames, $updateIds); + $sectionsData = $this->identifier->markSections($sectionsData, $sectionNames, $forceNewTimestamp); return $sectionsData; } diff --git a/app/code/Magento/Customer/CustomerData/SectionPoolInterface.php b/app/code/Magento/Customer/CustomerData/SectionPoolInterface.php index c308804fd0f8..ad73b9722b13 100644 --- a/app/code/Magento/Customer/CustomerData/SectionPoolInterface.php +++ b/app/code/Magento/Customer/CustomerData/SectionPoolInterface.php @@ -14,8 +14,8 @@ interface SectionPoolInterface * Get section data by section names. If $sectionNames is null then return all sections data * * @param array $sectionNames - * @param bool $updateIds + * @param bool $forceNewTimestamp * @return array */ - public function getSectionsData(array $sectionNames = null, $updateIds = false); + public function getSectionsData(array $sectionNames = null, $forceNewTimestamp = false); } diff --git a/app/code/Magento/Customer/Helper/Address.php b/app/code/Magento/Customer/Helper/Address.php index c74c62dc6d98..765c13b28770 100644 --- a/app/code/Magento/Customer/Helper/Address.php +++ b/app/code/Magento/Customer/Helper/Address.php @@ -10,6 +10,8 @@ use Magento\Customer\Api\Data\AttributeMetadataInterface; use Magento\Directory\Model\Country\Format; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\View\Element\BlockInterface; +use Magento\Store\Model\ScopeInterface; /** * Customer address helper @@ -94,6 +96,8 @@ class Address extends \Magento\Framework\App\Helper\AbstractHelper protected $_addressConfig; /** + * Address constructor. + * * @param \Magento\Framework\App\Helper\Context $context * @param \Magento\Framework\View\Element\BlockFactory $blockFactory * @param \Magento\Store\Model\StoreManagerInterface $storeManager @@ -127,6 +131,8 @@ public function getBookUrl() } /** + * Retrieve edit url. + * * @return void */ public function getEditUrl() @@ -134,6 +140,8 @@ public function getEditUrl() } /** + * Retrieve delete url. + * * @return void */ public function getDeleteUrl() @@ -141,6 +149,8 @@ public function getDeleteUrl() } /** + * Retrieve create url. + * * @return void */ public function getCreateUrl() @@ -148,6 +158,8 @@ public function getCreateUrl() } /** + * Retrieve block renderer. + * * @param string $renderer * @return \Magento\Framework\View\Element\BlockInterface */ @@ -165,7 +177,9 @@ public function getRenderer($renderer) * * @param string $key * @param \Magento\Store\Model\Store|int|string $store + * * @return string|null + * @throws NoSuchEntityException */ public function getConfig($key, $store = null) { @@ -174,7 +188,7 @@ public function getConfig($key, $store = null) if (!isset($this->_config[$websiteId])) { $this->_config[$websiteId] = $this->scopeConfig->getValue( 'customer/address', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $store ); } @@ -185,7 +199,10 @@ public function getConfig($key, $store = null) * Return Number of Lines in a Street Address for store * * @param \Magento\Store\Model\Store|int|string $store + * * @return int + * @throws NoSuchEntityException + * @throws \Magento\Framework\Exception\LocalizedException */ public function getStreetLines($store = null) { @@ -204,6 +221,8 @@ public function getStreetLines($store = null) } /** + * Retrieve address format. + * * @param string $code * @return Format|string */ @@ -232,7 +251,9 @@ public function getFormatTypeRenderer($code) * Determine if specified address config value can be shown * * @param string $key + * * @return bool + * @throws NoSuchEntityException */ public function canShowConfig($key) { @@ -243,9 +264,11 @@ public function canShowConfig($key) * Get string with frontend validation classes for attribute * * @param string $attributeCode + * * @return string * * @SuppressWarnings(PHPMD.NPathComplexity) + * @throws \Magento\Framework\Exception\LocalizedException */ public function getAttributeValidationClass($attributeCode) { @@ -313,9 +336,9 @@ public function convertStreetLines($origStreets, $toCount) */ public function isVatValidationEnabled($store = null) { - return (bool)$this->scopeConfig->getValue( + return $this->scopeConfig->isSetFlag( self::XML_PATH_VAT_VALIDATION_ENABLED, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $store ); } @@ -327,9 +350,9 @@ public function isVatValidationEnabled($store = null) */ public function isDisableAutoGroupAssignDefaultValue() { - return (bool)$this->scopeConfig->getValue( + return $this->scopeConfig->isSetFlag( self::XML_PATH_VIV_DISABLE_AUTO_ASSIGN_DEFAULT, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE ); } @@ -341,9 +364,9 @@ public function isDisableAutoGroupAssignDefaultValue() */ public function hasValidateOnEachTransaction($store = null) { - return (bool)$this->scopeConfig->getValue( + return $this->scopeConfig->isSetFlag( self::XML_PATH_VIV_ON_EACH_TRANSACTION, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $store ); } @@ -358,7 +381,7 @@ public function getTaxCalculationAddressType($store = null) { return (string)$this->scopeConfig->getValue( self::XML_PATH_VIV_TAX_CALCULATION_ADDRESS_TYPE, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $store ); } @@ -370,9 +393,9 @@ public function getTaxCalculationAddressType($store = null) */ public function isVatAttributeVisible() { - return (bool)$this->scopeConfig->getValue( + return $this->scopeConfig->isSetFlag( self::XML_PATH_VAT_FRONTEND_VISIBILITY, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE ); } @@ -380,7 +403,10 @@ public function isVatAttributeVisible() * Retrieve attribute visibility * * @param string $code + * * @return bool + * @throws NoSuchEntityException + * @throws \Magento\Framework\Exception\LocalizedException * @since 100.2.0 */ public function isAttributeVisible($code) diff --git a/app/code/Magento/Customer/Model/AccountConfirmation.php b/app/code/Magento/Customer/Model/AccountConfirmation.php index 7d01ff0efc41..f29330af2587 100644 --- a/app/code/Magento/Customer/Model/AccountConfirmation.php +++ b/app/code/Magento/Customer/Model/AccountConfirmation.php @@ -10,8 +10,7 @@ use Magento\Framework\Registry; /** - * Class AccountConfirmation. - * Checks if email confirmation required for customer. + * Class AccountConfirmation. Checks if email confirmation required for customer. */ class AccountConfirmation { @@ -31,6 +30,8 @@ class AccountConfirmation private $registry; /** + * AccountConfirmation constructor. + * * @param ScopeConfigInterface $scopeConfig * @param Registry $registry */ @@ -56,7 +57,7 @@ public function isConfirmationRequired($websiteId, $customerId, $customerEmail): return false; } - return (bool)$this->scopeConfig->getValue( + return $this->scopeConfig->isSetFlag( self::XML_PATH_IS_CONFIRM, ScopeInterface::SCOPE_WEBSITES, $websiteId diff --git a/app/code/Magento/Customer/Model/AccountManagement.php b/app/code/Magento/Customer/Model/AccountManagement.php index fe17adcb09c0..250d190f8fae 100644 --- a/app/code/Magento/Customer/Model/AccountManagement.php +++ b/app/code/Magento/Customer/Model/AccountManagement.php @@ -18,8 +18,10 @@ use Magento\Customer\Model\Customer as CustomerModel; use Magento\Customer\Model\Customer\CredentialsValidator; use Magento\Customer\Model\Metadata\Validator; +use Magento\Customer\Model\ResourceModel\Visitor\CollectionFactory; use Magento\Eav\Model\Validator\Attribute\Backend; use Magento\Framework\Api\ExtensibleDataObjectConverter; +use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\App\Area; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; @@ -41,16 +43,16 @@ use Magento\Framework\Intl\DateTimeFactory; use Magento\Framework\Mail\Template\TransportBuilder; use Magento\Framework\Math\Random; +use Magento\Framework\Phrase; use Magento\Framework\Reflection\DataObjectProcessor; use Magento\Framework\Registry; +use Magento\Framework\Session\SaveHandlerInterface; +use Magento\Framework\Session\SessionManagerInterface; use Magento\Framework\Stdlib\DateTime; use Magento\Framework\Stdlib\StringUtils as StringHelper; use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\StoreManagerInterface; use Psr\Log\LoggerInterface as PsrLogger; -use Magento\Framework\Session\SessionManagerInterface; -use Magento\Framework\Session\SaveHandlerInterface; -use Magento\Customer\Model\ResourceModel\Visitor\CollectionFactory; /** * Handle various customer account actions @@ -58,6 +60,7 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class AccountManagement implements AccountManagementInterface { @@ -326,6 +329,16 @@ class AccountManagement implements AccountManagementInterface */ private $accountConfirmation; + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var AddressRegistry + */ + private $addressRegistry; + /** * @param CustomerFactory $customerFactory * @param ManagerInterface $eventManager @@ -356,7 +369,10 @@ class AccountManagement implements AccountManagementInterface * @param SessionManagerInterface|null $sessionManager * @param SaveHandlerInterface|null $saveHandler * @param CollectionFactory|null $visitorCollectionFactory + * @param SearchCriteriaBuilder|null $searchCriteriaBuilder + * @param AddressRegistry|null $addressRegistry * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * @SuppressWarnings(PHPMD.NPathComplexity) */ public function __construct( CustomerFactory $customerFactory, @@ -387,7 +403,9 @@ public function __construct( AccountConfirmation $accountConfirmation = null, SessionManagerInterface $sessionManager = null, SaveHandlerInterface $saveHandler = null, - CollectionFactory $visitorCollectionFactory = null + CollectionFactory $visitorCollectionFactory = null, + SearchCriteriaBuilder $searchCriteriaBuilder = null, + AddressRegistry $addressRegistry = null ) { $this->customerFactory = $customerFactory; $this->eventManager = $eventManager; @@ -423,6 +441,10 @@ public function __construct( ?: ObjectManager::getInstance()->get(SaveHandlerInterface::class); $this->visitorCollectionFactory = $visitorCollectionFactory ?: ObjectManager::getInstance()->get(CollectionFactory::class); + $this->searchCriteriaBuilder = $searchCriteriaBuilder + ?: ObjectManager::getInstance()->get(SearchCriteriaBuilder::class); + $this->addressRegistry = $addressRegistry + ?: ObjectManager::getInstance()->get(AddressRegistry::class); } /** @@ -503,6 +525,8 @@ private function activateCustomer($customer, $confirmationKey) } $customer->setConfirmation(null); + // No need to validate customer and customer address while activating customer + $this->setIgnoreValidationFlag($customer); $this->customerRepository->save($customer); $this->getEmailNotification()->newAccount( $customer, @@ -568,6 +592,9 @@ public function initiatePasswordReset($email, $template, $websiteId = null) // load customer by email $customer = $this->customerRepository->get($email, $websiteId); + // No need to validate customer address while saving customer reset password token + $this->disableAddressValidation($customer); + $newPasswordToken = $this->mathRandom->getUniqueHash(); $this->changeResetPasswordLinkToken($customer, $newPasswordToken); @@ -591,6 +618,42 @@ public function initiatePasswordReset($email, $template, $websiteId = null) return false; } + /** + * Match a customer by their RP token. + * + * @param string $rpToken + * @throws ExpiredException + * @throws NoSuchEntityException + * @return CustomerInterface + * @throws LocalizedException + */ + private function matchCustomerByRpToken(string $rpToken): CustomerInterface + { + $this->searchCriteriaBuilder->addFilter( + 'rp_token', + $rpToken + ); + $this->searchCriteriaBuilder->setPageSize(1); + $found = $this->customerRepository->getList( + $this->searchCriteriaBuilder->create() + ); + if ($found->getTotalCount() > 1) { + //Failed to generated unique RP token + throw new ExpiredException( + new Phrase('Reset password token expired.') + ); + } + if ($found->getTotalCount() === 0) { + //Customer with such token not found. + throw NoSuchEntityException::singleField( + 'rp_token', + $rpToken + ); + } + //Unique customer found. + return $found->getItems()[0]; + } + /** * Handle not supported template * @@ -615,18 +678,36 @@ private function handleUnknownTemplate($template) */ public function resetPassword($email, $resetToken, $newPassword) { - $customer = $this->customerRepository->get($email); + if (!$email) { + $customer = $this->matchCustomerByRpToken($resetToken); + $email = $customer->getEmail(); + } else { + $customer = $this->customerRepository->get($email); + } + + // No need to validate customer and customer address while saving customer reset password token + $this->disableAddressValidation($customer); + $this->setIgnoreValidationFlag($customer); + //Validate Token and new password strength $this->validateResetPasswordToken($customer->getId(), $resetToken); + $this->credentialsValidator->checkPasswordDifferentFromEmail( + $email, + $newPassword + ); $this->checkPasswordStrength($newPassword); //Update secure data $customerSecure = $this->customerRegistry->retrieveSecureData($customer->getId()); $customerSecure->setRpToken(null); $customerSecure->setRpTokenCreatedAt(null); $customerSecure->setPasswordHash($this->createPasswordHash($newPassword)); - $this->getAuthentication()->unlock($customer->getId()); - $this->sessionManager->destroy(); $this->destroyCustomerSessions($customer->getId()); + if ($this->sessionManager->isSessionExists()) { + //delete old session and move data to the new session + //use this instead of $this->sessionManager->regenerateId because last one doesn't delete old session + // phpcs:ignore Magento2.Functions.DiscouragedFunction + session_regenerate_id(true); + } $this->customerRepository->save($customer); return true; @@ -781,6 +862,7 @@ public function createAccountWithPasswordHash(CustomerInterface $customer, $hash if ($customer->getWebsiteId()) { $storeId = $this->storeManager->getWebsite($customer->getWebsiteId())->getDefaultStore()->getId(); } else { + $this->storeManager->setCurrentStore(null); $storeId = $this->storeManager->getStore()->getId(); } $customer->setStoreId($storeId); @@ -864,6 +946,8 @@ public function getDefaultShippingAddress($customerId) * @param CustomerInterface $customer * @param string $redirectUrl * @return void + * @throws LocalizedException + * @throws NoSuchEntityException */ protected function sendEmailConfirmation(CustomerInterface $customer, $redirectUrl) { @@ -918,7 +1002,10 @@ public function changePasswordById($customerId, $currentPassword, $newPassword) * @param string $newPassword * @return bool true on success * @throws InputException + * @throws InputMismatchException * @throws InvalidEmailOrPasswordException + * @throws LocalizedException + * @throws NoSuchEntityException * @throws UserLockedException */ private function changePasswordForCustomer($customer, $currentPassword, $newPassword) @@ -938,6 +1025,7 @@ private function changePasswordForCustomer($customer, $currentPassword, $newPass $this->checkPasswordStrength($newPassword); $customerSecure->setPasswordHash($this->createPasswordHash($newPassword)); $this->destroyCustomerSessions($customer->getId()); + $this->disableAddressValidation($customer); $this->customerRepository->save($customer); return true; @@ -955,6 +1043,8 @@ protected function createPasswordHash($password) } /** + * Get EAV validator + * * @return Backend */ private function getEavValidator() @@ -1033,10 +1123,11 @@ public function isCustomerInStore($customerWebsiteId, $storeId) * @throws \Magento\Framework\Exception\State\ExpiredException If token is expired * @throws \Magento\Framework\Exception\InputException If token or customer id is invalid * @throws \Magento\Framework\Exception\NoSuchEntityException If customer doesn't exist + * @throws LocalizedException */ private function validateResetPasswordToken($customerId, $resetPasswordLinkToken) { - if (empty($customerId) || $customerId < 0) { + if ($customerId !== null && $customerId <= 0) { throw new InputException( __( 'Invalid value of "%value" provided for the %fieldName field.', @@ -1044,21 +1135,24 @@ private function validateResetPasswordToken($customerId, $resetPasswordLinkToken ) ); } + + if ($customerId === null) { + //Looking for the customer. + $customerId = $this->matchCustomerByRpToken($resetPasswordLinkToken) + ->getId(); + } if (!is_string($resetPasswordLinkToken) || empty($resetPasswordLinkToken)) { $params = ['fieldName' => 'resetPasswordLinkToken']; throw new InputException(__('"%fieldName" is required. Enter and try again.', $params)); } - $customerSecureData = $this->customerRegistry->retrieveSecureData($customerId); $rpToken = $customerSecureData->getRpToken(); $rpTokenCreatedAt = $customerSecureData->getRpTokenCreatedAt(); - if (!Security::compareStrings($rpToken, $resetPasswordLinkToken)) { throw new InputMismatchException(__('The password token is mismatched. Reset and try again.')); } elseif ($this->isResetPasswordLinkTokenExpired($rpToken, $rpTokenCreatedAt)) { throw new ExpiredException(__('The password token is expired. Reset and try again.')); } - return true; } @@ -1127,6 +1221,8 @@ protected function sendNewAccountEmail( * * @param CustomerInterface $customer * @return $this + * @throws LocalizedException + * @throws NoSuchEntityException * @deprecated 100.1.0 */ protected function sendPasswordResetNotificationEmail($customer) @@ -1141,6 +1237,7 @@ protected function sendPasswordResetNotificationEmail($customer) * @param int|string|null $defaultStoreId * @return int * @deprecated 100.1.0 + * @throws LocalizedException */ protected function getWebsiteStoreId($customer, $defaultStoreId = null) { @@ -1153,6 +1250,8 @@ protected function getWebsiteStoreId($customer, $defaultStoreId = null) } /** + * Get template types + * * @return array * @deprecated 100.1.0 */ @@ -1186,6 +1285,7 @@ protected function getTemplateTypes() * @param int|null $storeId * @param string $email * @return $this + * @throws MailException * @deprecated 100.1.0 */ protected function sendEmailTemplate( @@ -1301,6 +1401,9 @@ public function isResetPasswordLinkTokenExpired($rpToken, $rpTokenCreatedAt) * @param string $passwordLinkToken * @return bool * @throws InputException + * @throws InputMismatchException + * @throws LocalizedException + * @throws NoSuchEntityException */ public function changeResetPasswordLinkToken($customer, $passwordLinkToken) { @@ -1318,6 +1421,7 @@ public function changeResetPasswordLinkToken($customer, $passwordLinkToken) $customerSecure->setRpTokenCreatedAt( $this->dateTimeFactory->create()->format(DateTime::DATETIME_PHP_FORMAT) ); + $this->setIgnoreValidationFlag($customer); $this->customerRepository->save($customer); } return true; @@ -1328,6 +1432,8 @@ public function changeResetPasswordLinkToken($customer, $passwordLinkToken) * * @param CustomerInterface $customer * @return $this + * @throws LocalizedException + * @throws NoSuchEntityException * @deprecated 100.1.0 */ public function sendPasswordReminderEmail($customer) @@ -1355,6 +1461,8 @@ public function sendPasswordReminderEmail($customer) * * @param CustomerInterface $customer * @return $this + * @throws LocalizedException + * @throws NoSuchEntityException * @deprecated 100.1.0 */ public function sendPasswordResetConfirmationEmail($customer) @@ -1399,6 +1507,7 @@ protected function getAddressById(CustomerInterface $customer, $addressId) * * @param CustomerInterface $customer * @return Data\CustomerSecure + * @throws NoSuchEntityException * @deprecated 100.1.0 */ protected function getFullCustomerObject($customer) @@ -1426,6 +1535,20 @@ public function getPasswordHash($password) return $this->encryptor->getHash($password); } + /** + * Disable Customer Address Validation + * + * @param CustomerInterface $customer + * @throws NoSuchEntityException + */ + private function disableAddressValidation($customer) + { + foreach ($customer->getAddresses() as $address) { + $addressModel = $this->addressRegistry->retrieve($address->getId()); + $addressModel->setShouldIgnoreValidation(true); + } + } + /** * Get email notification * @@ -1445,6 +1568,7 @@ private function getEmailNotification() /** * Destroy all active customer sessions by customer id (current session will not be destroyed). + * * Customer sessions which should be deleted are collecting from the "customer_visitor" table considering * configured session lifetime. * @@ -1471,4 +1595,15 @@ private function destroyCustomerSessions($customerId) $this->saveHandler->destroy($sessionId); } } + + /** + * Set ignore_validation_flag for reset password flow to skip unnecessary address and customer validation + * + * @param Customer $customer + * @return void + */ + private function setIgnoreValidationFlag($customer) + { + $customer->setData('ignore_validation_flag', true); + } } diff --git a/app/code/Magento/Customer/Model/Address.php b/app/code/Magento/Customer/Model/Address.php index 1dcd8516af69..ea9b103f4227 100644 --- a/app/code/Magento/Customer/Model/Address.php +++ b/app/code/Magento/Customer/Model/Address.php @@ -122,6 +122,8 @@ public function __construct( } /** + * Initialize address model + * * @return void */ protected function _construct() @@ -154,9 +156,6 @@ public function updateData(AddressInterface $address) // Need to explicitly set this due to discrepancy in the keys between model and data object $this->setIsDefaultBilling($address->isDefaultBilling()); $this->setIsDefaultShipping($address->isDefaultShipping()); - if (!$this->getAttributeSetId()) { - $this->setAttributeSetId(AddressMetadataInterface::ATTRIBUTE_SET_ID_ADDRESS); - } $customAttributes = $address->getCustomAttributes(); if ($customAttributes !== null) { foreach ($customAttributes as $attribute) { @@ -168,17 +167,21 @@ public function updateData(AddressInterface $address) } /** - * {@inheritdoc} + * Create address data object based on current address model. + * + * @param int|null $defaultBillingAddressId + * @param int|null $defaultShippingAddressId + * @return AddressInterface + * Use Api/Data/AddressInterface as a result of service operations. Don't rely on the model to provide + * the instance of Api/Data/AddressInterface + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function getDataModel($defaultBillingAddressId = null, $defaultShippingAddressId = null) { if ($this->getCustomerId() || $this->getParentId()) { - if ($this->getCustomer()->getDefaultBillingAddress()) { - $defaultBillingAddressId = $this->getCustomer()->getDefaultBillingAddress()->getId(); - } - if ($this->getCustomer()->getDefaultShippingAddress()) { - $defaultShippingAddressId = $this->getCustomer()->getDefaultShippingAddress()->getId(); - } + $customer = $this->getCustomer(); + $defaultBillingAddressId = $customer->getDefaultBilling() ?: $defaultBillingAddressId; + $defaultShippingAddressId = $customer->getDefaultShipping() ?: $defaultShippingAddressId; } return parent::getDataModel($defaultBillingAddressId, $defaultShippingAddressId); } @@ -261,6 +264,8 @@ public function getDefaultAttributeCodes() } /** + * Clone address + * * @return void */ public function __clone() @@ -301,6 +306,8 @@ public function setRegionId($regionId) } /** + * Create customer model + * * @return Customer */ protected function _createCustomer() @@ -356,7 +363,11 @@ public function reindex() } /** - * {@inheritdoc} + * Get a list of custom attribute codes. + * + * By default, entity can be extended only using extension attributes functionality. + * + * @return string[] * @since 100.0.6 */ protected function getCustomAttributesCodes() @@ -366,6 +377,7 @@ protected function getCustomAttributesCodes() /** * Get new AttributeList dependency for application code. + * * @return \Magento\Customer\Model\Address\CustomAttributeListInterface * @deprecated 100.0.6 */ diff --git a/app/code/Magento/Customer/Model/Address/AbstractAddress.php b/app/code/Magento/Customer/Model/Address/AbstractAddress.php index 6408276630c3..d8d0646b30bb 100644 --- a/app/code/Magento/Customer/Model/Address/AbstractAddress.php +++ b/app/code/Magento/Customer/Model/Address/AbstractAddress.php @@ -23,7 +23,7 @@ * @method string getFirstname() * @method string getMiddlename() * @method string getLastname() - * @method int getCountryId() + * @method string getCountryId() * @method string getCity() * @method string getTelephone() * @method string getCompany() @@ -222,7 +222,7 @@ public function getStreet() } /** - * Get steet line by number + * Get street line by number * * @param int $number * @return string @@ -230,7 +230,7 @@ public function getStreet() public function getStreetLine($number) { $lines = $this->getStreet(); - return isset($lines[$number - 1]) ? $lines[$number - 1] : ''; + return $lines[$number - 1] ?? ''; } /** @@ -271,7 +271,8 @@ public function setStreet($street) * Enforce format of the street field or other multiline custom attributes * * @param array|string $key - * @param null $value + * @param array|string|null $value + * * @return \Magento\Framework\DataObject */ public function setData($key, $value = null) @@ -286,6 +287,7 @@ public function setData($key, $value = null) /** * Check that address can have multiline attribute by this code (as street or some custom attribute) + * * @param string $code * @return bool */ @@ -403,6 +405,8 @@ public function getRegionCode() } /** + * Return Region ID + * * @return int */ public function getRegionId() @@ -425,7 +429,9 @@ public function getRegionId() } /** - * @return int + * Get country + * + * @return string */ public function getCountry() { @@ -502,6 +508,8 @@ public function getConfig() } /** + * Processing object before save data + * * @return $this */ public function beforeSave() @@ -516,10 +524,12 @@ public function beforeSave() * * @param int|null $defaultBillingAddressId * @param int|null $defaultShippingAddressId + * * @return AddressInterface * Use Api/Data/AddressInterface as a result of service operations. Don't rely on the model to provide * the instance of Api/Data/AddressInterface * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @throws \Magento\Framework\Exception\LocalizedException */ public function getDataModel($defaultBillingAddressId = null, $defaultShippingAddressId = null) { @@ -591,6 +601,8 @@ public function validate() } /** + * Create region instance + * * @return \Magento\Directory\Model\Region */ protected function _createRegionInstance() @@ -599,6 +611,8 @@ protected function _createRegionInstance() } /** + * Create country instance + * * @return \Magento\Directory\Model\Country */ protected function _createCountryInstance() @@ -608,6 +622,7 @@ protected function _createCountryInstance() /** * Unset Region from address + * * @return $this * @since 100.2.0 */ @@ -617,8 +632,11 @@ public function unsRegion() } /** + * Is company required + * * @return bool * @since 100.2.0 + * @throws \Magento\Framework\Exception\LocalizedException */ protected function isCompanyRequired() { @@ -626,8 +644,11 @@ protected function isCompanyRequired() } /** + * Is telephone required + * * @return bool * @since 100.2.0 + * @throws \Magento\Framework\Exception\LocalizedException */ protected function isTelephoneRequired() { @@ -635,8 +656,11 @@ protected function isTelephoneRequired() } /** + * Is fax required + * * @return bool * @since 100.2.0 + * @throws \Magento\Framework\Exception\LocalizedException */ protected function isFaxRequired() { diff --git a/app/code/Magento/Customer/Model/Address/AddressModelInterface.php b/app/code/Magento/Customer/Model/Address/AddressModelInterface.php index 0af36e877555..06de3a99a831 100644 --- a/app/code/Magento/Customer/Model/Address/AddressModelInterface.php +++ b/app/code/Magento/Customer/Model/Address/AddressModelInterface.php @@ -15,7 +15,7 @@ interface AddressModelInterface { /** - * Get steet line by number + * Get street line by number * * @param int $number * @return string diff --git a/app/code/Magento/Customer/Model/Address/CustomAttributesProcessor.php b/app/code/Magento/Customer/Model/Address/CustomAttributesProcessor.php new file mode 100644 index 000000000000..d6e63e11ee45 --- /dev/null +++ b/app/code/Magento/Customer/Model/Address/CustomAttributesProcessor.php @@ -0,0 +1,112 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model\Address; + +use Magento\Customer\Api\AddressMetadataInterface; +use Magento\Eav\Api\AttributeOptionManagementInterface; + +/** + * Provides customer address data. + */ +class CustomAttributesProcessor +{ + /** + * @var AddressMetadataInterface + */ + private $addressMetadata; + + /** + * @var AttributeOptionManagementInterface + */ + private $attributeOptionManager; + + /** + * @param AddressMetadataInterface $addressMetadata + * @param AttributeOptionManagementInterface $attributeOptionManager + */ + public function __construct( + AddressMetadataInterface $addressMetadata, + AttributeOptionManagementInterface $attributeOptionManager + ) { + $this->addressMetadata = $addressMetadata; + $this->attributeOptionManager = $attributeOptionManager; + } + + /** + * Set Labels to custom Attributes + * + * @param \Magento\Framework\Api\AttributeValue[] $customAttributes + * @return array $customAttributes + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Exception\StateException + */ + private function setLabelsForAttributes(array $customAttributes): array + { + if (!empty($customAttributes)) { + foreach ($customAttributes as $customAttributeCode => $customAttribute) { + $attributeOptionLabels = $this->getAttributeLabels($customAttribute, $customAttributeCode); + if (!empty($attributeOptionLabels)) { + $customAttributes[$customAttributeCode]['label'] = implode(', ', $attributeOptionLabels); + } + } + } + + return $customAttributes; + } + /** + * Get Labels by CustomAttribute and CustomAttributeCode + * + * @param array $customAttribute + * @param string $customAttributeCode + * @return array $attributeOptionLabels + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Exception\StateException + */ + private function getAttributeLabels(array $customAttribute, string $customAttributeCode) : array + { + $attributeOptionLabels = []; + + if (!empty($customAttribute['value'])) { + $customAttributeValues = explode(',', $customAttribute['value']); + $attributeOptions = $this->attributeOptionManager->getItems( + \Magento\Customer\Model\Indexer\Address\AttributeProvider::ENTITY, + $customAttributeCode + ); + + if (!empty($attributeOptions)) { + foreach ($attributeOptions as $attributeOption) { + $attributeOptionValue = $attributeOption->getValue(); + if (\in_array($attributeOptionValue, $customAttributeValues, false)) { + $attributeOptionLabels[] = $attributeOption->getLabel() ?? $attributeOptionValue; + } + } + } + } + + return $attributeOptionLabels; + } + + /** + * Filter not visible on storefront custom attributes. + * + * @param array $attributes + * @return array + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function filterNotVisibleAttributes(array $attributes): array + { + $attributesMetadata = $this->addressMetadata->getAllAttributesMetadata(); + foreach ($attributesMetadata as $attributeMetadata) { + if (!$attributeMetadata->isVisible()) { + unset($attributes[$attributeMetadata->getAttributeCode()]); + } + } + + return $this->setLabelsForAttributes($attributes); + } +} diff --git a/app/code/Magento/Customer/Model/Address/CustomerAddressDataFormatter.php b/app/code/Magento/Customer/Model/Address/CustomerAddressDataFormatter.php new file mode 100644 index 000000000000..9202d7492040 --- /dev/null +++ b/app/code/Magento/Customer/Model/Address/CustomerAddressDataFormatter.php @@ -0,0 +1,110 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model\Address; + +use Magento\Customer\Api\Data\AddressInterface; +use Magento\Customer\Model\Address\Mapper as AddressMapper; +use Magento\Customer\Model\Address\Config as AddressConfig; + +/** + * Provides method to format customer address data. + */ +class CustomerAddressDataFormatter +{ + /** + * @var AddressMapper + */ + private $addressMapper; + + /** + * @var AddressConfig + */ + private $addressConfig; + + /** + * @var CustomAttributesProcessor + */ + private $customAttributesProcessor; + + /** + * @param Mapper $addressMapper + * @param Config $addressConfig + * @param CustomAttributesProcessor $customAttributesProcessor + */ + public function __construct( + AddressMapper $addressMapper, + AddressConfig $addressConfig, + CustomAttributesProcessor $customAttributesProcessor + ) { + $this->addressMapper = $addressMapper; + $this->addressConfig = $addressConfig; + $this->customAttributesProcessor = $customAttributesProcessor; + } + + /** + * Prepare customer address data. + * + * @param AddressInterface $customerAddress + * @return array + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function prepareAddress(AddressInterface $customerAddress) + { + $resultAddress = [ + 'id' => $customerAddress->getId(), + 'customer_id' => $customerAddress->getCustomerId(), + 'company' => $customerAddress->getCompany(), + 'prefix' => $customerAddress->getPrefix(), + 'firstname' => $customerAddress->getFirstname(), + 'lastname' => $customerAddress->getLastname(), + 'middlename' => $customerAddress->getMiddlename(), + 'suffix' => $customerAddress->getSuffix(), + 'street' => $customerAddress->getStreet(), + 'city' => $customerAddress->getCity(), + 'region' => [ + 'region' => $customerAddress->getRegion()->getRegion(), + 'region_code' => $customerAddress->getRegion()->getRegionCode(), + 'region_id' => $customerAddress->getRegion()->getRegionId(), + ], + 'region_id' => $customerAddress->getRegionId(), + 'postcode' => $customerAddress->getPostcode(), + 'country_id' => $customerAddress->getCountryId(), + 'telephone' => $customerAddress->getTelephone(), + 'fax' => $customerAddress->getFax(), + 'default_billing' => $customerAddress->isDefaultBilling(), + 'default_shipping' => $customerAddress->isDefaultShipping(), + 'inline' => $this->getCustomerAddressInline($customerAddress), + 'custom_attributes' => [], + 'extension_attributes' => $customerAddress->getExtensionAttributes(), + ]; + + if ($customerAddress->getCustomAttributes()) { + $customerAddress = $customerAddress->__toArray(); + $resultAddress['custom_attributes'] = $this->customAttributesProcessor->filterNotVisibleAttributes( + $customerAddress['custom_attributes'] + ); + } + + return $resultAddress; + } + + /** + * Set additional customer address data + * + * @param AddressInterface $address + * @return string + */ + private function getCustomerAddressInline(AddressInterface $address): string + { + $builtOutputAddressData = $this->addressMapper->toFlatArray($address); + return $this->addressConfig + ->getFormatByCode(AddressConfig::DEFAULT_ADDRESS_FORMAT) + ->getRenderer() + ->renderArray($builtOutputAddressData); + } +} diff --git a/app/code/Magento/Customer/Model/Address/CustomerAddressDataProvider.php b/app/code/Magento/Customer/Model/Address/CustomerAddressDataProvider.php new file mode 100644 index 000000000000..04fdd2a7f726 --- /dev/null +++ b/app/code/Magento/Customer/Model/Address/CustomerAddressDataProvider.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model\Address; + +/** + * Provides customer address data. + */ +class CustomerAddressDataProvider +{ + /** + * Customer addresses. + * + * @var array + */ + private $customerAddresses = []; + + /** + * @var CustomerAddressDataFormatter + */ + private $customerAddressDataFormatter; + + /** + * @param CustomerAddressDataFormatter $customerAddressDataFormatter + */ + public function __construct( + CustomerAddressDataFormatter $customerAddressDataFormatter + ) { + $this->customerAddressDataFormatter = $customerAddressDataFormatter; + } + + /** + * Get addresses for customer. + * + * @param \Magento\Customer\Api\Data\CustomerInterface $customer + * @return array + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function getAddressDataByCustomer( + \Magento\Customer\Api\Data\CustomerInterface $customer + ): array { + if (!empty($this->customerAddresses)) { + return $this->customerAddresses; + } + + $customerOriginAddresses = $customer->getAddresses(); + if (!$customerOriginAddresses) { + return []; + } + + $customerAddresses = []; + foreach ($customerOriginAddresses as $address) { + $customerAddresses[$address->getId()] = $this->customerAddressDataFormatter->prepareAddress($address); + } + + $this->customerAddresses = $customerAddresses; + + return $this->customerAddresses; + } +} diff --git a/app/code/Magento/Customer/Model/Address/DataProvider.php b/app/code/Magento/Customer/Model/Address/DataProvider.php new file mode 100644 index 000000000000..e1dd68207cae --- /dev/null +++ b/app/code/Magento/Customer/Model/Address/DataProvider.php @@ -0,0 +1,233 @@ +<?php +declare(strict_types=1); +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Customer\Model\Address; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Model\ResourceModel\Address\CollectionFactory; +use Magento\Eav\Model\Config; +use Magento\Eav\Model\Entity\Type; +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Customer\Model\Address; +use Magento\Customer\Model\FileUploaderDataResolver; +use Magento\Customer\Model\AttributeMetadataResolver; +use Magento\Ui\Component\Form\Element\Multiline; + +/** + * Dataprovider of customer addresses for customer address grid. + * @property \Magento\Customer\Model\ResourceModel\Address\Collection $collection + */ +class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider +{ + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var array + */ + private $loadedData; + + /** + * Allow to manage attributes, even they are hidden on storefront + * + * @var bool + */ + private $allowToShowHiddenAttributes; + + /* + * @var ContextInterface + */ + private $context; + + /** + * @var array + */ + private $bannedInputTypes = ['media_image']; + + /** + * @var array + */ + private static $attributesToEliminate = [ + 'region', + 'vat_is_valid', + 'vat_request_date', + 'vat_request_id', + 'vat_request_success' + ]; + + /** + * @var FileUploaderDataResolver + */ + private $fileUploaderDataResolver; + + /** + * @var AttributeMetadataResolver + */ + private $attributeMetadataResolver; + + /** + * DataProvider constructor. + * @param string $name + * @param string $primaryFieldName + * @param string $requestFieldName + * @param CollectionFactory $addressCollectionFactory + * @param CustomerRepositoryInterface $customerRepository + * @param Config $eavConfig + * @param ContextInterface $context + * @param FileUploaderDataResolver $fileUploaderDataResolver + * @param AttributeMetadataResolver $attributeMetadataResolver + * @param array $meta + * @param array $data + * @param bool $allowToShowHiddenAttributes + * @throws \Magento\Framework\Exception\LocalizedException + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + $name, + $primaryFieldName, + $requestFieldName, + CollectionFactory $addressCollectionFactory, + CustomerRepositoryInterface $customerRepository, + Config $eavConfig, + ContextInterface $context, + FileUploaderDataResolver $fileUploaderDataResolver, + AttributeMetadataResolver $attributeMetadataResolver, + array $meta = [], + array $data = [], + $allowToShowHiddenAttributes = true + ) { + parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data); + $this->collection = $addressCollectionFactory->create(); + $this->collection->addAttributeToSelect('*'); + $this->customerRepository = $customerRepository; + $this->allowToShowHiddenAttributes = $allowToShowHiddenAttributes; + $this->context = $context; + $this->fileUploaderDataResolver = $fileUploaderDataResolver; + $this->attributeMetadataResolver = $attributeMetadataResolver; + $this->meta['general']['children'] = $this->getAttributesMeta( + $eavConfig->getEntityType('customer_address') + ); + } + + /** + * Get Addresses data and process customer default billing & shipping addresses + * + * @return array + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function getData(): array + { + if (null !== $this->loadedData) { + return $this->loadedData; + } + $items = $this->collection->getItems(); + /** @var Address $item */ + foreach ($items as $item) { + $addressId = $item->getEntityId(); + $item->load($addressId); + $this->loadedData[$addressId] = $item->getData(); + $customerId = $this->loadedData[$addressId]['parent_id']; + /** @var \Magento\Customer\Model\Customer $customer */ + $customer = $this->customerRepository->getById($customerId); + $defaultBilling = $customer->getDefaultBilling(); + $defaultShipping = $customer->getDefaultShipping(); + $this->prepareAddressData($addressId, $this->loadedData, $defaultBilling, $defaultShipping); + $this->fileUploaderDataResolver->overrideFileUploaderData($item, $this->loadedData[$addressId]); + } + + if (null === $this->loadedData) { + $this->loadedData[''] = $this->getDefaultData(); + } + + return $this->loadedData; + } + + /** + * Prepare address data + * + * @param int $addressId + * @param array $addresses + * @param string|null $defaultBilling + * @param string|null $defaultShipping + * @return void + */ + private function prepareAddressData($addressId, array &$addresses, $defaultBilling, $defaultShipping): void + { + if (null !== $defaultBilling && $addressId === $defaultBilling) { + $addresses[$addressId]['default_billing'] = '1'; + } + if (null !== $defaultShipping && $addressId === $defaultShipping) { + $addresses[$addressId]['default_shipping'] = '1'; + } + foreach ($this->meta['general']['children'] as $attributeName => $attributeMeta) { + if ($attributeMeta['arguments']['data']['config']['dataType'] === Multiline::NAME + && isset($this->loadedData[$addressId][$attributeName]) + && !\is_array($this->loadedData[$addressId][$attributeName]) + ) { + $this->loadedData[$addressId][$attributeName] = explode( + "\n", + $this->loadedData[$addressId][$attributeName] + ); + } + } + } + + /** + * Get default customer data for adding new address + * + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @return array + */ + private function getDefaultData(): array + { + $parentId = $this->context->getRequestParam('parent_id'); + $customer = $this->customerRepository->getById($parentId); + $data = [ + 'parent_id' => $parentId, + 'firstname' => $customer->getFirstname(), + 'lastname' => $customer->getLastname() + ]; + + return $data; + } + + /** + * Get attributes meta + * + * @param Type $entityType + * @return array + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function getAttributesMeta(Type $entityType): array + { + $meta = []; + $attributes = $entityType->getAttributeCollection(); + /* @var AbstractAttribute $attribute */ + foreach ($attributes as $attribute) { + if (\in_array($attribute->getFrontendInput(), $this->bannedInputTypes, true)) { + continue; + } + if (\in_array($attribute->getAttributeCode(), self::$attributesToEliminate, true)) { + continue; + } + + $meta[$attribute->getAttributeCode()] = $this->attributeMetadataResolver->getAttributesMeta( + $attribute, + $entityType, + $this->allowToShowHiddenAttributes, + $this->getRequestFieldName() + ); + } + $this->attributeMetadataResolver->processWebsiteMeta($meta); + + return $meta; + } +} diff --git a/app/code/Magento/Customer/Model/Address/Validator/Country.php b/app/code/Magento/Customer/Model/Address/Validator/Country.php index d7fb51dbbd44..fdc5510d1d2e 100644 --- a/app/code/Magento/Customer/Model/Address/Validator/Country.php +++ b/app/code/Magento/Customer/Model/Address/Validator/Country.php @@ -7,7 +7,8 @@ use Magento\Customer\Model\Address\AbstractAddress; use Magento\Customer\Model\Address\ValidatorInterface; -use Magento\Framework\App\ObjectManager; +use Magento\Directory\Helper\Data; +use Magento\Directory\Model\AllowedCountries; use Magento\Store\Model\ScopeInterface; /** @@ -16,35 +17,25 @@ class Country implements ValidatorInterface { /** - * @var \Magento\Directory\Helper\Data + * @var Data */ private $directoryData; /** - * @var \Magento\Directory\Model\AllowedCountries + * @var AllowedCountries */ private $allowedCountriesReader; /** - * @var \Magento\Customer\Model\Config\Share - */ - private $shareConfig; - - /** - * @param \Magento\Directory\Helper\Data $directoryData - * @param \Magento\Directory\Model\AllowedCountries|null $allowedCountriesReader - * @param \Magento\Customer\Model\Config\Share|null $shareConfig + * @param Data $directoryData + * @param AllowedCountries $allowedCountriesReader */ public function __construct( - \Magento\Directory\Helper\Data $directoryData, - \Magento\Directory\Model\AllowedCountries $allowedCountriesReader = null, - \Magento\Customer\Model\Config\Share $shareConfig = null + Data $directoryData, + AllowedCountries $allowedCountriesReader ) { $this->directoryData = $directoryData; - $this->allowedCountriesReader = $allowedCountriesReader - ?: ObjectManager::getInstance()->get(\Magento\Directory\Model\AllowedCountries::class); - $this->shareConfig = $shareConfig - ?: ObjectManager::getInstance()->get(\Magento\Customer\Model\Config\Share::class); + $this->allowedCountriesReader = $allowedCountriesReader; } /** @@ -128,12 +119,7 @@ private function validateRegion(AbstractAddress $address) */ private function getWebsiteAllowedCountries(AbstractAddress $address): array { - $websiteId = null; - - if (!$this->shareConfig->isGlobalScope()) { - $websiteId = $address->getCustomer() ? $address->getCustomer()->getWebsiteId() : null; - } - - return $this->allowedCountriesReader->getAllowedCountries(ScopeInterface::SCOPE_WEBSITE, $websiteId); + $storeId = $address->getData('store_id'); + return $this->allowedCountriesReader->getAllowedCountries(ScopeInterface::SCOPE_STORE, $storeId); } } diff --git a/app/code/Magento/Customer/Model/Attribute.php b/app/code/Magento/Customer/Model/Attribute.php index 98a97872f15f..ae714f993082 100644 --- a/app/code/Magento/Customer/Model/Attribute.php +++ b/app/code/Magento/Customer/Model/Attribute.php @@ -202,9 +202,14 @@ public function canBeFilterableInGrid() /** * @inheritdoc + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + $this->unsetData('entity_type'); return array_diff( parent::__sleep(), @@ -214,9 +219,14 @@ public function __sleep() /** * @inheritdoc + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + parent::__wakeup(); $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); $this->indexerRegistry = $objectManager->get(\Magento\Framework\Indexer\IndexerRegistry::class); diff --git a/app/code/Magento/Customer/Model/Attribute/Data/Postcode.php b/app/code/Magento/Customer/Model/Attribute/Data/Postcode.php index 380b8a4d3446..b1602e8ca193 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/AttributeMetadataResolver.php b/app/code/Magento/Customer/Model/AttributeMetadataResolver.php new file mode 100644 index 000000000000..c22cc9a4f23f --- /dev/null +++ b/app/code/Magento/Customer/Model/AttributeMetadataResolver.php @@ -0,0 +1,241 @@ +<?php +declare(strict_types=1); +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Customer\Model; + +use Magento\Customer\Model\ResourceModel\Address\Attribute\Source\CountryWithWebsites; +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Customer\Api\Data\AddressInterface; +use Magento\Ui\DataProvider\EavValidationRules; +use Magento\Ui\Component\Form\Field; +use Magento\Eav\Model\Entity\Type; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\Config\Share as ShareConfig; + +/** + * Class to build meta data of the customer or customer address attribute + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class AttributeMetadataResolver +{ + /** + * EAV attribute properties to fetch from meta storage + * @var array + */ + private static $metaProperties = [ + 'dataType' => 'frontend_input', + 'visible' => 'is_visible', + 'required' => 'is_required', + 'label' => 'frontend_label', + 'sortOrder' => 'sort_order', + 'notice' => 'note', + 'default' => 'default_value', + 'size' => 'multiline_count', + ]; + + /** + * Form element mapping + * + * @var array + */ + private static $formElement = [ + 'text' => 'input', + 'hidden' => 'input', + 'boolean' => 'checkbox', + ]; + + /** + * @var CountryWithWebsites + */ + private $countryWithWebsiteSource; + + /** + * @var EavValidationRules + */ + private $eavValidationRules; + + /** + * @var FileUploaderDataResolver + */ + private $fileUploaderDataResolver; + + /** + * @var ContextInterface + */ + private $context; + + /** + * @var ShareConfig + */ + private $shareConfig; + + /** + * @param CountryWithWebsites $countryWithWebsiteSource + * @param EavValidationRules $eavValidationRules + * @param \Magento\Customer\Model\FileUploaderDataResolver $fileUploaderDataResolver + * @param ContextInterface $context + * @param ShareConfig $shareConfig + */ + public function __construct( + CountryWithWebsites $countryWithWebsiteSource, + EavValidationRules $eavValidationRules, + fileUploaderDataResolver $fileUploaderDataResolver, + ContextInterface $context, + ShareConfig $shareConfig + ) { + $this->countryWithWebsiteSource = $countryWithWebsiteSource; + $this->eavValidationRules = $eavValidationRules; + $this->fileUploaderDataResolver = $fileUploaderDataResolver; + $this->context = $context; + $this->shareConfig = $shareConfig; + } + + /** + * Get meta data of the customer or customer address attribute + * + * @param AbstractAttribute $attribute + * @param Type $entityType + * @param bool $allowToShowHiddenAttributes + * @param string $requestFieldName + * @return array + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function getAttributesMeta( + AbstractAttribute $attribute, + Type $entityType, + bool $allowToShowHiddenAttributes, + string $requestFieldName + ): array { + $meta = $this->modifyBooleanAttributeMeta($attribute); + // use getDataUsingMethod, since some getters are defined and apply additional processing of returning value + foreach (self::$metaProperties as $metaName => $origName) { + $value = $attribute->getDataUsingMethod($origName); + $meta['arguments']['data']['config'][$metaName] = ($metaName === 'label') ? __($value) : $value; + if ('frontend_input' === $origName) { + $meta['arguments']['data']['config']['formElement'] = self::$formElement[$value] ?? $value; + } + } + + if ($attribute->usesSource()) { + if ($attribute->getAttributeCode() === AddressInterface::COUNTRY_ID) { + $meta['arguments']['data']['config']['options'] = $this->countryWithWebsiteSource + ->getAllOptions(); + } else { + $meta['arguments']['data']['config']['options'] = $attribute->getSource()->getAllOptions(); + } + } + + $rules = $this->eavValidationRules->build($attribute, $meta['arguments']['data']['config']); + if (!empty($rules)) { + $meta['arguments']['data']['config']['validation'] = $rules; + } + + $meta['arguments']['data']['config']['componentType'] = Field::NAME; + $meta['arguments']['data']['config']['visible'] = $this->canShowAttribute( + $attribute, + $requestFieldName, + $allowToShowHiddenAttributes + ); + + $this->fileUploaderDataResolver->overrideFileUploaderMetadata( + $entityType, + $attribute, + $meta['arguments']['data']['config'] + ); + + return $meta; + } + + /** + * Detect can we show attribute on specific form or not + * + * @param AbstractAttribute $customerAttribute + * @param string $requestFieldName + * @param bool $allowToShowHiddenAttributes + * @return bool + */ + private function canShowAttribute( + AbstractAttribute $customerAttribute, + string $requestFieldName, + bool $allowToShowHiddenAttributes + ) { + $userDefined = (bool)$customerAttribute->getIsUserDefined(); + if (!$userDefined) { + return $customerAttribute->getIsVisible(); + } + + $canShowOnForm = $this->canShowAttributeInForm($customerAttribute, $requestFieldName); + + return ($allowToShowHiddenAttributes && $canShowOnForm) || + (!$allowToShowHiddenAttributes && $canShowOnForm && $customerAttribute->getIsVisible()); + } + + /** + * Check whether the specific attribute can be shown in form: customer registration, customer edit, etc... + * + * @param AbstractAttribute $customerAttribute + * @param string $requestFieldName + * @return bool + */ + private function canShowAttributeInForm(AbstractAttribute $customerAttribute, string $requestFieldName): bool + { + $isRegistration = $this->context->getRequestParam($requestFieldName) === null; + + if ($customerAttribute->getEntityType()->getEntityTypeCode() === 'customer') { + return \is_array($customerAttribute->getUsedInForms()) && + ( + (\in_array('customer_account_create', $customerAttribute->getUsedInForms(), true) + && $isRegistration) || + (\in_array('customer_account_edit', $customerAttribute->getUsedInForms(), true) + && !$isRegistration) + ); + } + return \is_array($customerAttribute->getUsedInForms()) && + \in_array('customer_address_edit', $customerAttribute->getUsedInForms(), true); + } + + /** + * Modify boolean attribute meta data + * + * @param AttributeInterface $attribute + * @return array + */ + private function modifyBooleanAttributeMeta(AttributeInterface $attribute): array + { + $meta = []; + if ($attribute->getFrontendInput() === 'boolean') { + $meta['arguments']['data']['config']['prefer'] = 'toggle'; + $meta['arguments']['data']['config']['valueMap'] = [ + 'true' => '1', + 'false' => '0', + ]; + } + + return $meta; + } + + /** + * Add global scope parameter and filter options to website meta + * + * @param array $meta + * @return void + */ + public function processWebsiteMeta(&$meta): void + { + if (isset($meta[CustomerInterface::WEBSITE_ID]) && $this->shareConfig->isGlobalScope()) { + $meta[CustomerInterface::WEBSITE_ID]['arguments']['data']['config']['isGlobalScope'] = 1; + } + + if (isset($meta[AddressInterface::COUNTRY_ID]) && !$this->shareConfig->isGlobalScope()) { + $meta[AddressInterface::COUNTRY_ID]['arguments']['data']['config']['filterBy'] = [ + 'target' => 'customer_form.customer_form_data_source:data.customer.website_id', + 'field' => 'website_ids' + ]; + } + } +} diff --git a/app/code/Magento/Customer/Model/Authentication.php b/app/code/Magento/Customer/Model/Authentication.php index 0967f1a0189e..9a9a46306207 100644 --- a/app/code/Magento/Customer/Model/Authentication.php +++ b/app/code/Magento/Customer/Model/Authentication.php @@ -83,7 +83,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function processAuthenticationFailure($customerId) { @@ -120,7 +120,7 @@ public function processAuthenticationFailure($customerId) } /** - * {@inheritdoc} + * @inheritdoc */ public function unlock($customerId) { @@ -152,7 +152,7 @@ protected function getMaxFailures() } /** - * {@inheritdoc} + * @inheritdoc */ public function isLocked($customerId) { @@ -161,12 +161,12 @@ public function isLocked($customerId) } /** - * {@inheritdoc} + * @inheritdoc */ public function authenticate($customerId, $password) { $customerSecure = $this->customerRegistry->retrieveSecureData($customerId); - $hash = $customerSecure->getPasswordHash(); + $hash = $customerSecure->getPasswordHash() ?? ''; if (!$this->encryptor->validateHash($password, $hash)) { $this->processAuthenticationFailure($customerId); if ($this->isLocked($customerId)) { diff --git a/app/code/Magento/Customer/Model/Config/Backend/Address/Street.php b/app/code/Magento/Customer/Model/Config/Backend/Address/Street.php index fc0fa3ebc073..40a10a1db093 100644 --- a/app/code/Magento/Customer/Model/Config/Backend/Address/Street.php +++ b/app/code/Magento/Customer/Model/Config/Backend/Address/Street.php @@ -87,15 +87,20 @@ public function afterDelete() { $result = parent::afterDelete(); - if ($this->getScope() == \Magento\Store\Model\ScopeInterface::SCOPE_WEBSITES) { - $attribute = $this->_eavConfig->getAttribute('customer_address', 'street'); - $website = $this->_storeManager->getWebsite($this->getScopeCode()); - $attribute->setWebsite($website); - $attribute->load($attribute->getId()); - $attribute->setData('scope_multiline_count', null); - $attribute->save(); - } + $attribute = $this->_eavConfig->getAttribute('customer_address', 'street'); + switch ($this->getScope()) { + case \Magento\Store\Model\ScopeInterface::SCOPE_WEBSITES: + $website = $this->_storeManager->getWebsite($this->getScopeCode()); + $attribute->setWebsite($website); + $attribute->load($attribute->getId()); + $attribute->setData('scope_multiline_count', null); + break; + case ScopeConfigInterface::SCOPE_TYPE_DEFAULT: + $attribute->setData('multiline_count', 2); + break; + } + $attribute->save(); return $result; } } diff --git a/app/code/Magento/Customer/Model/Customer.php b/app/code/Magento/Customer/Model/Customer.php index 6d1c1549216e..1287dbe5df70 100644 --- a/app/code/Magento/Customer/Model/Customer.php +++ b/app/code/Magento/Customer/Model/Customer.php @@ -219,6 +219,13 @@ class Customer extends \Magento\Framework\Model\AbstractModel */ private $accountConfirmation; + /** + * Caching property to store customer address data models by the address ID. + * + * @var array + */ + private $storedAddress; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -314,7 +321,10 @@ public function getDataModel() $addressesData = []; /** @var \Magento\Customer\Model\Address $address */ foreach ($this->getAddresses() as $address) { - $addressesData[] = $address->getDataModel(); + if (!isset($this->storedAddress[$address->getId()])) { + $this->storedAddress[$address->getId()] = $address->getDataModel(); + } + $addressesData[] = $this->storedAddress[$address->getId()]; } $customerDataObject = $this->customerDataFactory->create(); $this->dataObjectHelper->populateWithArray( @@ -359,13 +369,6 @@ public function updateData($customer) $this->setId($customerId); } - // Need to use attribute set or future updates can cause data loss - if (!$this->getAttributeSetId()) { - $this->setAttributeSetId( - CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER - ); - } - return $this; } @@ -961,6 +964,16 @@ public function getSharedWebsiteIds() return $ids; } + /** + * Retrieve attribute set id for customer. + * + * @return int + */ + public function getAttributeSetId() + { + return parent::getAttributeSetId() ?: CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER; + } + /** * Set store to customer * @@ -1041,17 +1054,6 @@ public function resetErrors() return $this; } - /** - * Prepare customer for delete - * - * @return $this - */ - public function beforeDelete() - { - //TODO : Revisit and figure handling permissions in MAGETWO-11084 Implementation: Service Context Provider - return parent::beforeDelete(); - } - /** * Processing object after save data * @@ -1292,6 +1294,8 @@ public function getResetPasswordLinkExpirationPeriod() } /** + * Create Address from Factory + * * @return Address */ protected function _createAddressInstance() @@ -1300,6 +1304,8 @@ protected function _createAddressInstance() } /** + * Create Address Collection from Factory + * * @return \Magento\Customer\Model\ResourceModel\Address\Collection */ protected function _createAddressCollection() @@ -1308,6 +1314,8 @@ protected function _createAddressCollection() } /** + * Get Template Types + * * @return array */ protected function getTemplateTypes() diff --git a/app/code/Magento/Customer/Model/Customer/DataProvider.php b/app/code/Magento/Customer/Model/Customer/DataProvider.php index ce976d3f62c7..5bad0a41aadc 100644 --- a/app/code/Magento/Customer/Model/Customer/DataProvider.php +++ b/app/code/Magento/Customer/Model/Customer/DataProvider.php @@ -5,14 +5,11 @@ */ namespace Magento\Customer\Model\Customer; -use Magento\Customer\Api\AddressMetadataInterface; -use Magento\Customer\Api\CustomerMetadataInterface; use Magento\Customer\Api\Data\AddressInterface; use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Model\Address; use Magento\Customer\Model\Attribute; use Magento\Customer\Model\Customer; -use Magento\Customer\Model\FileProcessor; use Magento\Customer\Model\FileProcessorFactory; use Magento\Customer\Model\ResourceModel\Address\Attribute\Source\CountryWithWebsites; use Magento\Customer\Model\ResourceModel\Customer\Collection; @@ -25,12 +22,18 @@ use Magento\Framework\Session\SessionManagerInterface; use Magento\Framework\View\Element\UiComponent\ContextInterface; use Magento\Framework\View\Element\UiComponent\DataProvider\FilterPool; +use Magento\Ui\Component\Form\Element\Multiline; use Magento\Ui\Component\Form\Field; use Magento\Ui\DataProvider\EavValidationRules; +use Magento\Customer\Model\FileUploaderDataResolver; /** + * Supplies the data for the customer UI component + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyFields) * + * @deprecated \Magento\Customer\Model\Customer\DataProviderWithDefaultAddresses is used instead * @api * @since 100.0.2 */ @@ -108,21 +111,6 @@ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider */ protected $session; - /** - * @var FileProcessorFactory - */ - private $fileProcessorFactory; - - /** - * File types allowed for file_uploader UI component - * - * @var array - */ - private $fileUploaderTypes = [ - 'image', - 'file', - ]; - /** * Customer fields that must be removed * @@ -146,6 +134,11 @@ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider */ private $allowToShowHiddenAttributes; + /** + * @var FileUploaderDataResolver + */ + private $fileUploaderDataResolver; + /** * @param string $name * @param string $primaryFieldName @@ -155,11 +148,13 @@ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider * @param Config $eavConfig * @param FilterPool $filterPool * @param FileProcessorFactory $fileProcessorFactory - * @param ContextInterface $context * @param array $meta * @param array $data + * @param ContextInterface $context * @param bool $allowToShowHiddenAttributes + * @param FileUploaderDataResolver|null $fileUploaderDataResolver * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( $name, @@ -173,7 +168,8 @@ public function __construct( array $meta = [], array $data = [], ContextInterface $context = null, - $allowToShowHiddenAttributes = true + $allowToShowHiddenAttributes = true, + $fileUploaderDataResolver = null ) { parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data); $this->eavValidationRules = $eavValidationRules; @@ -181,9 +177,10 @@ public function __construct( $this->collection->addAttributeToSelect('*'); $this->eavConfig = $eavConfig; $this->filterPool = $filterPool; - $this->fileProcessorFactory = $fileProcessorFactory ?: $this->getFileProcessorFactory(); $this->context = $context ?: ObjectManager::getInstance()->get(ContextInterface::class); $this->allowToShowHiddenAttributes = $allowToShowHiddenAttributes; + $this->fileUploaderDataResolver = $fileUploaderDataResolver + ?: ObjectManager::getInstance()->get(FileUploaderDataResolver::class); $this->meta['customer']['children'] = $this->getAttributesMeta( $this->eavConfig->getEntityType('customer') ); @@ -224,7 +221,7 @@ public function getData() foreach ($items as $customer) { $result['customer'] = $customer->getData(); - $this->overrideFileUploaderData($customer, $result['customer']); + $this->fileUploaderDataResolver->overrideFileUploaderData($customer, $result['customer']); $result['customer'] = array_diff_key( $result['customer'], @@ -239,7 +236,7 @@ public function getData() $result['address'][$addressId] = $address->getData(); $this->prepareAddressData($addressId, $result['address'], $result['customer']); - $this->overrideFileUploaderData($address, $result['address'][$addressId]); + $this->fileUploaderDataResolver->overrideFileUploaderData($address, $result['address'][$addressId]); } $this->loadedData[$customer->getId()] = $result; } @@ -254,75 +251,6 @@ public function getData() return $this->loadedData; } - /** - * Override file uploader UI component data - * - * Overrides data for attributes with frontend_input equal to 'image' or 'file'. - * - * @param Customer|Address $entity - * @param array $entityData - * @return void - */ - private function overrideFileUploaderData($entity, array &$entityData) - { - $attributes = $entity->getAttributes(); - foreach ($attributes as $attribute) { - /** @var Attribute $attribute */ - if (in_array($attribute->getFrontendInput(), $this->fileUploaderTypes)) { - $entityData[$attribute->getAttributeCode()] = $this->getFileUploaderData( - $entity->getEntityType(), - $attribute, - $entityData - ); - } - } - } - - /** - * Retrieve array of values required by file uploader UI component - * - * @param Type $entityType - * @param Attribute $attribute - * @param array $customerData - * @return array - * @SuppressWarnings(PHPMD.NPathComplexity) - */ - private function getFileUploaderData( - Type $entityType, - Attribute $attribute, - array $customerData - ) { - $attributeCode = $attribute->getAttributeCode(); - - $file = isset($customerData[$attributeCode]) - ? $customerData[$attributeCode] - : ''; - - /** @var FileProcessor $fileProcessor */ - $fileProcessor = $this->getFileProcessorFactory()->create([ - 'entityTypeCode' => $entityType->getEntityTypeCode(), - ]); - - if (!empty($file) - && $fileProcessor->isExist($file) - ) { - $stat = $fileProcessor->getStat($file); - $viewUrl = $fileProcessor->getViewUrl($file, $attribute->getFrontendInput()); - - return [ - [ - 'file' => $file, - 'size' => isset($stat) ? $stat['size'] : 0, - 'url' => isset($viewUrl) ? $viewUrl : '', - 'name' => basename($file), - 'type' => $fileProcessor->getMimeType($file), - ], - ]; - } - - return []; - } - /** * Get attributes meta * @@ -368,7 +296,11 @@ protected function getAttributesMeta(Type $entityType) $meta[$code]['arguments']['data']['config']['componentType'] = Field::NAME; $meta[$code]['arguments']['data']['config']['visible'] = $this->canShowAttribute($attribute); - $this->overrideFileUploaderMetadata($entityType, $attribute, $meta[$code]['arguments']['data']['config']); + $this->fileUploaderDataResolver->overrideFileUploaderMetadata( + $entityType, + $attribute, + $meta[$code]['arguments']['data']['config'] + ); } $this->processWebsiteMeta($meta); @@ -466,97 +398,6 @@ private function processWebsiteMeta(&$meta) } } - /** - * Override file uploader UI component metadata - * - * Overrides metadata for attributes with frontend_input equal to 'image' or 'file'. - * - * @param Type $entityType - * @param AbstractAttribute $attribute - * @param array $config - * @return void - */ - private function overrideFileUploaderMetadata( - Type $entityType, - AbstractAttribute $attribute, - array &$config - ) { - if (in_array($attribute->getFrontendInput(), $this->fileUploaderTypes)) { - $maxFileSize = self::MAX_FILE_SIZE; - - if (isset($config['validation']['max_file_size'])) { - $maxFileSize = (int)$config['validation']['max_file_size']; - } - - $allowedExtensions = []; - - if (isset($config['validation']['file_extensions'])) { - $allowedExtensions = explode(',', $config['validation']['file_extensions']); - array_walk($allowedExtensions, function (&$value) { - $value = strtolower(trim($value)); - }); - } - - $allowedExtensions = implode(' ', $allowedExtensions); - - $entityTypeCode = $entityType->getEntityTypeCode(); - $url = $this->getFileUploadUrl($entityTypeCode); - - $config = [ - 'formElement' => 'fileUploader', - 'componentType' => 'fileUploader', - 'maxFileSize' => $maxFileSize, - 'allowedExtensions' => $allowedExtensions, - 'uploaderConfig' => [ - 'url' => $url, - ], - 'label' => $this->getMetadataValue($config, 'label'), - 'sortOrder' => $this->getMetadataValue($config, 'sortOrder'), - 'required' => $this->getMetadataValue($config, 'required'), - 'visible' => $this->getMetadataValue($config, 'visible'), - 'validation' => $this->getMetadataValue($config, 'validation'), - ]; - } - } - - /** - * Retrieve metadata value - * - * @param array $config - * @param string $name - * @param mixed $default - * @return mixed - */ - private function getMetadataValue($config, $name, $default = null) - { - $value = isset($config[$name]) ? $config[$name] : $default; - return $value; - } - - /** - * Retrieve URL to file upload - * - * @param string $entityTypeCode - * @return string - */ - private function getFileUploadUrl($entityTypeCode) - { - switch ($entityTypeCode) { - case CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER: - $url = 'customer/file/customer_upload'; - break; - - case AddressMetadataInterface::ENTITY_TYPE_ADDRESS: - $url = 'customer/file/address_upload'; - break; - - default: - $url = ''; - break; - } - return $url; - } - /** * Process attributes by frontend input type * @@ -596,23 +437,14 @@ protected function prepareAddressData($addressId, array &$addresses, array $cust ) { $addresses[$addressId]['default_shipping'] = $customer['default_shipping']; } - if (isset($addresses[$addressId]['street']) && !is_array($addresses[$addressId]['street'])) { - $addresses[$addressId]['street'] = explode("\n", $addresses[$addressId]['street']); - } - } - /** - * Get FileProcessorFactory instance - * - * @return FileProcessorFactory - * @deprecated 100.1.3 - */ - private function getFileProcessorFactory() - { - if ($this->fileProcessorFactory === null) { - $this->fileProcessorFactory = ObjectManager::getInstance() - ->get(\Magento\Customer\Model\FileProcessorFactory::class); + foreach ($this->meta['address']['children'] as $attributeName => $attributeMeta) { + if ($attributeMeta['arguments']['data']['config']['dataType'] === Multiline::NAME + && isset($addresses[$addressId][$attributeName]) + && !is_array($addresses[$addressId][$attributeName]) + ) { + $addresses[$addressId][$attributeName] = explode("\n", $addresses[$addressId][$attributeName]); + } } - return $this->fileProcessorFactory; } } diff --git a/app/code/Magento/Customer/Model/Customer/DataProviderWithDefaultAddresses.php b/app/code/Magento/Customer/Model/Customer/DataProviderWithDefaultAddresses.php new file mode 100644 index 000000000000..07b8681df91a --- /dev/null +++ b/app/code/Magento/Customer/Model/Customer/DataProviderWithDefaultAddresses.php @@ -0,0 +1,199 @@ +<?php +declare(strict_types=1); +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Customer\Model\Customer; + +use Magento\Customer\Model\Address; +use Magento\Customer\Model\Customer; +use Magento\Customer\Model\ResourceModel\Customer\CollectionFactory as CustomerCollectionFactory; +use Magento\Eav\Model\Config; +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Eav\Model\Entity\Type; +use Magento\Framework\Session\SessionManagerInterface; +use Magento\Customer\Model\FileUploaderDataResolver; +use Magento\Customer\Model\AttributeMetadataResolver; + +/** + * Refactored version of Magento\Customer\Model\Customer\DataProvider with eliminated usage of addresses collection. + */ +class DataProviderWithDefaultAddresses extends \Magento\Ui\DataProvider\AbstractDataProvider +{ + /** + * @var array + */ + private $loadedData = []; + + /** + * @var SessionManagerInterface + */ + private $session; + + /** + * Customer fields that must be removed + * + * @var array + */ + private static $forbiddenCustomerFields = [ + 'password_hash', + 'rp_token', + ]; + + /** + * Allow to manage attributes, even they are hidden on storefront + * + * @var bool + */ + private $allowToShowHiddenAttributes; + + /** + * @var \Magento\Directory\Model\CountryFactory + */ + private $countryFactory; + + /** + * @var FileUploaderDataResolver + */ + private $fileUploaderDataResolver; + + /** + * @var AttributeMetadataResolver + */ + private $attributeMetadataResolver; + + /** + * @param string $name + * @param string $primaryFieldName + * @param string $requestFieldName + * @param CustomerCollectionFactory $customerCollectionFactory + * @param Config $eavConfig + * @param \Magento\Directory\Model\CountryFactory $countryFactory + * @param SessionManagerInterface $session + * @param FileUploaderDataResolver $fileUploaderDataResolver + * @param AttributeMetadataResolver $attributeMetadataResolver + * @param bool $allowToShowHiddenAttributes + * @param array $meta + * @param array $data + * @throws \Magento\Framework\Exception\LocalizedException + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + string $name, + string $primaryFieldName, + string $requestFieldName, + CustomerCollectionFactory $customerCollectionFactory, + Config $eavConfig, + \Magento\Directory\Model\CountryFactory $countryFactory, + SessionManagerInterface $session, + FileUploaderDataResolver $fileUploaderDataResolver, + AttributeMetadataResolver $attributeMetadataResolver, + $allowToShowHiddenAttributes = true, + array $meta = [], + array $data = [] + ) { + parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data); + $this->collection = $customerCollectionFactory->create(); + $this->collection->addAttributeToSelect('*'); + $this->allowToShowHiddenAttributes = $allowToShowHiddenAttributes; + $this->session = $session; + $this->countryFactory = $countryFactory; + $this->fileUploaderDataResolver = $fileUploaderDataResolver; + $this->attributeMetadataResolver = $attributeMetadataResolver; + $this->meta['customer']['children'] = $this->getAttributesMeta( + $eavConfig->getEntityType('customer') + ); + } + + /** + * Get data + * + * @return array + */ + public function getData(): array + { + if (!empty($this->loadedData)) { + return $this->loadedData; + } + $items = $this->collection->getItems(); + /** @var Customer $customer */ + foreach ($items as $customer) { + $result['customer'] = $customer->getData(); + + $this->fileUploaderDataResolver->overrideFileUploaderData($customer, $result['customer']); + + $result['customer'] = array_diff_key( + $result['customer'], + array_flip(self::$forbiddenCustomerFields) + ); + unset($result['address']); + + $result['default_billing_address'] = $this->prepareDefaultAddress( + $customer->getDefaultBillingAddress() + ); + $result['default_shipping_address'] = $this->prepareDefaultAddress( + $customer->getDefaultShippingAddress() + ); + $result['customer_id'] = $customer->getId(); + + $this->loadedData[$customer->getId()] = $result; + } + + $data = $this->session->getCustomerFormData(); + if (!empty($data)) { + $customerId = $data['customer']['entity_id'] ?? null; + $this->loadedData[$customerId] = $data; + $this->session->unsCustomerFormData(); + } + + return $this->loadedData; + } + + /** + * Prepare default address data. + * + * @param Address|false $address + * @return array + */ + private function prepareDefaultAddress($address): array + { + $addressData = []; + + if (!empty($address)) { + $addressData = $address->getData(); + if (isset($addressData['street']) && !\is_array($address['street'])) { + $addressData['street'] = explode("\n", $addressData['street']); + } + $addressData['country'] = $this->countryFactory->create() + ->loadByCode($addressData['country_id'])->getName(); + } + + return $addressData; + } + + /** + * Get attributes meta + * + * @param Type $entityType + * @return array + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function getAttributesMeta(Type $entityType): array + { + $meta = []; + $attributes = $entityType->getAttributeCollection(); + /* @var AbstractAttribute $attribute */ + foreach ($attributes as $attribute) { + $meta[$attribute->getAttributeCode()] = $this->attributeMetadataResolver->getAttributesMeta( + $attribute, + $entityType, + $this->allowToShowHiddenAttributes, + $this->getRequestFieldName() + ); + } + $this->attributeMetadataResolver->processWebsiteMeta($meta); + + return $meta; + } +} diff --git a/app/code/Magento/Customer/Model/EmailNotification.php b/app/code/Magento/Customer/Model/EmailNotification.php index 4b65dcca0973..144c24f8e835 100644 --- a/app/code/Magento/Customer/Model/EmailNotification.php +++ b/app/code/Magento/Customer/Model/EmailNotification.php @@ -17,6 +17,8 @@ use Magento\Framework\Exception\LocalizedException; /** + * Customer email notification + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class EmailNotification implements EmailNotificationInterface diff --git a/app/code/Magento/Customer/Model/FileProcessor.php b/app/code/Magento/Customer/Model/FileProcessor.php index 6a8472758c16..c16faea28429 100644 --- a/app/code/Magento/Customer/Model/FileProcessor.php +++ b/app/code/Magento/Customer/Model/FileProcessor.php @@ -3,8 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Customer\Model; +/** + * Processor class for work with uploaded files + */ class FileProcessor { /** @@ -232,7 +237,7 @@ public function moveTemporaryFile($fileName) ); } - $fileName = $dispersionPath . '/' . $fileName; + $fileName = $dispersionPath . '/' . $destinationFileName; return $fileName; } diff --git a/app/code/Magento/Customer/Model/FileUploaderDataResolver.php b/app/code/Magento/Customer/Model/FileUploaderDataResolver.php new file mode 100644 index 000000000000..535bfe97bc45 --- /dev/null +++ b/app/code/Magento/Customer/Model/FileUploaderDataResolver.php @@ -0,0 +1,204 @@ +<?php +declare(strict_types=1); +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Customer\Model; + +use Magento\Eav\Model\Entity\Type; +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Customer\Api\AddressMetadataInterface; +use Magento\Customer\Api\CustomerMetadataInterface; + +/** + * Class to retrieve file uploader data for customer and customer address file & image attributes + */ +class FileUploaderDataResolver +{ + /** + * Maximum file size allowed for file_uploader UI component + * This constant was copied from deprecated data provider \Magento\Customer\Model\Customer\DataProvider + */ + private const MAX_FILE_SIZE = 2097152; + + /** + * @var FileProcessorFactory + */ + private $fileProcessorFactory; + + /** + * File types allowed for file_uploader UI component + * + * @var array + */ + private $fileUploaderTypes = [ + 'image', + 'file', + ]; + + /** + * @param FileProcessorFactory $fileProcessorFactory + */ + public function __construct( + FileProcessorFactory $fileProcessorFactory + ) { + $this->fileProcessorFactory = $fileProcessorFactory; + } + + /** + * Override file uploader UI component data + * + * Overrides data for attributes with frontend_input equal to 'image' or 'file'. + * + * @param Customer|Address $entity + * @param array $entityData + * @return void + */ + public function overrideFileUploaderData($entity, array &$entityData): void + { + $attributes = $entity->getAttributes(); + foreach ($attributes as $attribute) { + /** @var Attribute $attribute */ + if (\in_array($attribute->getFrontendInput(), $this->fileUploaderTypes, true)) { + $entityData[$attribute->getAttributeCode()] = $this->getFileUploaderData( + $entity->getEntityType(), + $attribute, + $entityData + ); + } + } + } + + /** + * Retrieve array of values required by file uploader UI component + * + * @param Type $entityType + * @param Attribute $attribute + * @param array $customerData + * @return array + */ + private function getFileUploaderData( + Type $entityType, + Attribute $attribute, + array $customerData + ): array { + $attributeCode = $attribute->getAttributeCode(); + + $file = $customerData[$attributeCode] ?? null; + + /** @var FileProcessor $fileProcessor */ + $fileProcessor = $this->fileProcessorFactory->create([ + 'entityTypeCode' => $entityType->getEntityTypeCode(), + ]); + + if (!empty($file) + && $fileProcessor->isExist($file) + ) { + $stat = $fileProcessor->getStat($file); + $viewUrl = $fileProcessor->getViewUrl($file, $attribute->getFrontendInput()); + + return [ + [ + 'file' => $file, + 'size' => null !== $stat ? $stat['size'] : 0, + 'url' => $viewUrl ?? '', + 'name' => basename($file), + 'type' => $fileProcessor->getMimeType($file), + ], + ]; + } + + return []; + } + + /** + * Override file uploader UI component metadata + * + * Overrides metadata for attributes with frontend_input equal to 'image' or 'file'. + * + * @param Type $entityType + * @param AbstractAttribute $attribute + * @param array $config + * @return void + */ + public function overrideFileUploaderMetadata( + Type $entityType, + AbstractAttribute $attribute, + array &$config + ): void { + if (\in_array($attribute->getFrontendInput(), $this->fileUploaderTypes, true)) { + $maxFileSize = self::MAX_FILE_SIZE; + + if (isset($config['validation']['max_file_size'])) { + $maxFileSize = (int)$config['validation']['max_file_size']; + } + + $allowedExtensions = []; + + if (isset($config['validation']['file_extensions'])) { + $allowedExtensions = explode(',', $config['validation']['file_extensions']); + array_walk($allowedExtensions, function (&$value) { + $value = strtolower(trim($value)); + }); + } + + $allowedExtensions = implode(' ', $allowedExtensions); + + $entityTypeCode = $entityType->getEntityTypeCode(); + $url = $this->getFileUploadUrl($entityTypeCode); + + $config = [ + 'formElement' => 'fileUploader', + 'componentType' => 'fileUploader', + 'maxFileSize' => $maxFileSize, + 'allowedExtensions' => $allowedExtensions, + 'uploaderConfig' => [ + 'url' => $url, + ], + 'label' => $this->getMetadataValue($config, 'label'), + 'sortOrder' => $this->getMetadataValue($config, 'sortOrder'), + 'required' => $this->getMetadataValue($config, 'required'), + 'visible' => $this->getMetadataValue($config, 'visible'), + 'validation' => $this->getMetadataValue($config, 'validation'), + ]; + } + } + + /** + * Retrieve metadata value + * + * @param array $config + * @param string $name + * @param mixed $default + * @return mixed + */ + private function getMetadataValue($config, $name, $default = null) + { + return $config[$name] ?? $default; + } + + /** + * Retrieve URL to file upload + * + * @param string $entityTypeCode + * @return string + */ + private function getFileUploadUrl($entityTypeCode): string + { + switch ($entityTypeCode) { + case CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER: + $url = 'customer/file/customer_upload'; + break; + + case AddressMetadataInterface::ENTITY_TYPE_ADDRESS: + $url = 'customer/file/address_upload'; + break; + + default: + $url = ''; + break; + } + return $url; + } +} diff --git a/app/code/Magento/Customer/Model/GroupManagement.php b/app/code/Magento/Customer/Model/GroupManagement.php index 47d7d7ad1ac4..48cb5d55061c 100644 --- a/app/code/Magento/Customer/Model/GroupManagement.php +++ b/app/code/Magento/Customer/Model/GroupManagement.php @@ -8,16 +8,19 @@ namespace Magento\Customer\Model; use Magento\Customer\Api\Data\GroupInterface; +use Magento\Customer\Api\Data\GroupInterfaceFactory; +use Magento\Customer\Api\GroupRepositoryInterface; +use Magento\Framework\Api\FilterBuilder; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Api\SortOrderBuilder; use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Store\Model\StoreManagerInterface; -use Magento\Customer\Api\GroupRepositoryInterface; -use Magento\Customer\Api\Data\GroupInterfaceFactory; -use Magento\Customer\Model\GroupFactory; /** + * The class contains methods for getting information about a customer group + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class GroupManagement implements \Magento\Customer\Api\GroupManagementInterface @@ -65,6 +68,11 @@ class GroupManagement implements \Magento\Customer\Api\GroupManagementInterface */ protected $filterBuilder; + /** + * @var SortOrderBuilder + */ + private $sortOrderBuilder; + /** * @param StoreManagerInterface $storeManager * @param ScopeConfigInterface $scopeConfig @@ -73,6 +81,7 @@ class GroupManagement implements \Magento\Customer\Api\GroupManagementInterface * @param GroupInterfaceFactory $groupDataFactory * @param SearchCriteriaBuilder $searchCriteriaBuilder * @param FilterBuilder $filterBuilder + * @param SortOrderBuilder $sortOrderBuilder */ public function __construct( StoreManagerInterface $storeManager, @@ -81,7 +90,8 @@ public function __construct( GroupRepositoryInterface $groupRepository, GroupInterfaceFactory $groupDataFactory, SearchCriteriaBuilder $searchCriteriaBuilder, - FilterBuilder $filterBuilder + FilterBuilder $filterBuilder, + SortOrderBuilder $sortOrderBuilder = null ) { $this->storeManager = $storeManager; $this->scopeConfig = $scopeConfig; @@ -90,10 +100,12 @@ public function __construct( $this->groupDataFactory = $groupDataFactory; $this->searchCriteriaBuilder = $searchCriteriaBuilder; $this->filterBuilder = $filterBuilder; + $this->sortOrderBuilder = $sortOrderBuilder ?: ObjectManager::getInstance() + ->get(SortOrderBuilder::class); } /** - * {@inheritdoc} + * @inheritdoc */ public function isReadonly($groupId) { @@ -107,7 +119,7 @@ public function isReadonly($groupId) } /** - * {@inheritdoc} + * @inheritdoc */ public function getDefaultGroup($storeId = null) { @@ -133,7 +145,7 @@ public function getDefaultGroup($storeId = null) } /** - * {@inheritdoc} + * @inheritdoc */ public function getNotLoggedInGroup() { @@ -141,7 +153,7 @@ public function getNotLoggedInGroup() } /** - * {@inheritdoc} + * @inheritdoc */ public function getLoggedInGroups() { @@ -155,15 +167,20 @@ public function getLoggedInGroups() ->setConditionType('neq') ->setValue(self::CUST_GROUP_ALL) ->create(); + $groupNameSortOrder = $this->sortOrderBuilder + ->setField('customer_group_code') + ->setAscendingDirection() + ->create(); $searchCriteria = $this->searchCriteriaBuilder ->addFilters($notLoggedInFilter) ->addFilters($groupAll) + ->addSortOrder($groupNameSortOrder) ->create(); return $this->groupRepository->getList($searchCriteria)->getItems(); } /** - * {@inheritdoc} + * @inheritdoc */ public function getAllCustomersGroup() { diff --git a/app/code/Magento/Customer/Model/Indexer/CustomerGroupDimensionProvider.php b/app/code/Magento/Customer/Model/Indexer/CustomerGroupDimensionProvider.php index d4e0c5cce340..336e7ab770b0 100644 --- a/app/code/Magento/Customer/Model/Indexer/CustomerGroupDimensionProvider.php +++ b/app/code/Magento/Customer/Model/Indexer/CustomerGroupDimensionProvider.php @@ -11,6 +11,9 @@ use Magento\Framework\Indexer\DimensionFactory; use Magento\Framework\Indexer\DimensionProviderInterface; +/** + * Class CustomerGroupDimensionProvider + */ class CustomerGroupDimensionProvider implements DimensionProviderInterface { /** @@ -34,12 +37,19 @@ class CustomerGroupDimensionProvider implements DimensionProviderInterface */ private $dimensionFactory; + /** + * @param CustomerGroupCollectionFactory $collectionFactory + * @param DimensionFactory $dimensionFactory + */ public function __construct(CustomerGroupCollectionFactory $collectionFactory, DimensionFactory $dimensionFactory) { $this->dimensionFactory = $dimensionFactory; $this->collectionFactory = $collectionFactory; } + /** + * @inheritdoc + */ public function getIterator(): \Traversable { foreach ($this->getCustomerGroups() as $customerGroup) { @@ -48,6 +58,8 @@ public function getIterator(): \Traversable } /** + * Get Customer Groups + * * @return array */ private function getCustomerGroups(): array diff --git a/app/code/Magento/Customer/Model/Indexer/Source.php b/app/code/Magento/Customer/Model/Indexer/Source.php index e4bf03e08a9a..a8878e2084ea 100644 --- a/app/code/Magento/Customer/Model/Indexer/Source.php +++ b/app/code/Magento/Customer/Model/Indexer/Source.php @@ -5,6 +5,7 @@ */ namespace Magento\Customer\Model\Indexer; +use Magento\Customer\Model\ResourceModel\Customer\Indexer\CollectionFactory; use Magento\Customer\Model\ResourceModel\Customer\Indexer\Collection; use Magento\Framework\App\ResourceConnection\SourceProviderInterface; use Traversable; @@ -25,11 +26,11 @@ class Source implements \IteratorAggregate, \Countable, SourceProviderInterface private $batchSize; /** - * @param \Magento\Customer\Model\ResourceModel\Customer\Indexer\CollectionFactory $collection + * @param CollectionFactory $collectionFactory * @param int $batchSize */ public function __construct( - \Magento\Customer\Model\ResourceModel\Customer\Indexer\CollectionFactory $collectionFactory, + CollectionFactory $collectionFactory, $batchSize = 10000 ) { $this->customerCollection = $collectionFactory->create(); @@ -37,7 +38,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getMainTable() { @@ -45,7 +46,7 @@ public function getMainTable() } /** - * {@inheritdoc} + * @inheritdoc */ public function getIdFieldName() { @@ -53,7 +54,7 @@ public function getIdFieldName() } /** - * {@inheritdoc} + * @inheritdoc */ public function addFieldToSelect($fieldName, $alias = null) { @@ -62,7 +63,7 @@ public function addFieldToSelect($fieldName, $alias = null) } /** - * {@inheritdoc} + * @inheritdoc */ public function getSelect() { @@ -70,7 +71,7 @@ public function getSelect() } /** - * {@inheritdoc} + * @inheritdoc */ public function addFieldToFilter($attribute, $condition = null) { @@ -79,7 +80,7 @@ public function addFieldToFilter($attribute, $condition = null) } /** - * @return int + * @inheritdoc */ public function count() { @@ -105,4 +106,28 @@ public function getIterator() $pageNumber++; } while ($pageNumber <= $lastPage); } + + /** + * Joins Attribute + * + * @param string $alias alias for the joined attribute + * @param string|\Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute + * @param string $bind attribute of the main entity to link with joined $filter + * @param string|null $filter primary key for the joined entity (entity_id default) + * @param string $joinType inner|left + * @param int|null $storeId + * @return void + * @throws \Magento\Framework\Exception\LocalizedException + * @see Collection::joinAttribute() + */ + public function joinAttribute( + string $alias, + $attribute, + string $bind, + ?string $filter = null, + string $joinType = 'inner', + ?int $storeId = null + ): void { + $this->customerCollection->joinAttribute($alias, $attribute, $bind, $filter, $joinType, $storeId); + } } diff --git a/app/code/Magento/Customer/Model/Metadata/AttributeMetadataCache.php b/app/code/Magento/Customer/Model/Metadata/AttributeMetadataCache.php index 5a46fdb9defc..8e64fba4a9b0 100644 --- a/app/code/Magento/Customer/Model/Metadata/AttributeMetadataCache.php +++ b/app/code/Magento/Customer/Model/Metadata/AttributeMetadataCache.php @@ -12,6 +12,7 @@ use Magento\Framework\App\Cache\StateInterface; use Magento\Framework\App\CacheInterface; use Magento\Framework\Serialize\SerializerInterface; +use Magento\Store\Model\StoreManagerInterface; /** * Cache for attribute metadata @@ -53,6 +54,11 @@ class AttributeMetadataCache */ private $serializer; + /** + * @var StoreManagerInterface + */ + private $storeManager; + /** * Constructor * @@ -60,17 +66,21 @@ class AttributeMetadataCache * @param StateInterface $state * @param SerializerInterface $serializer * @param AttributeMetadataHydrator $attributeMetadataHydrator + * @param StoreManagerInterface $storeManager */ public function __construct( CacheInterface $cache, StateInterface $state, SerializerInterface $serializer, - AttributeMetadataHydrator $attributeMetadataHydrator + AttributeMetadataHydrator $attributeMetadataHydrator, + StoreManagerInterface $storeManager = null ) { $this->cache = $cache; $this->state = $state; $this->serializer = $serializer; $this->attributeMetadataHydrator = $attributeMetadataHydrator; + $this->storeManager = $storeManager ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(StoreManagerInterface::class); } /** @@ -82,11 +92,12 @@ public function __construct( */ public function load($entityType, $suffix = '') { - if (isset($this->attributes[$entityType . $suffix])) { - return $this->attributes[$entityType . $suffix]; + $storeId = $this->storeManager->getStore()->getId(); + if (isset($this->attributes[$entityType . $suffix . $storeId])) { + return $this->attributes[$entityType . $suffix . $storeId]; } if ($this->isEnabled()) { - $cacheKey = self::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix; + $cacheKey = self::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix . $storeId; $serializedData = $this->cache->load($cacheKey); if ($serializedData) { $attributesData = $this->serializer->unserialize($serializedData); @@ -94,7 +105,7 @@ public function load($entityType, $suffix = '') foreach ($attributesData as $key => $attributeData) { $attributes[$key] = $this->attributeMetadataHydrator->hydrate($attributeData); } - $this->attributes[$entityType . $suffix] = $attributes; + $this->attributes[$entityType . $suffix . $storeId] = $attributes; return $attributes; } } @@ -111,9 +122,10 @@ public function load($entityType, $suffix = '') */ public function save($entityType, array $attributes, $suffix = '') { - $this->attributes[$entityType . $suffix] = $attributes; + $storeId = $this->storeManager->getStore()->getId(); + $this->attributes[$entityType . $suffix . $storeId] = $attributes; if ($this->isEnabled()) { - $cacheKey = self::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix; + $cacheKey = self::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix . $storeId; $attributesData = []; foreach ($attributes as $key => $attribute) { $attributesData[$key] = $this->attributeMetadataHydrator->extract($attribute); diff --git a/app/code/Magento/Customer/Model/Metadata/CustomerMetadata.php b/app/code/Magento/Customer/Model/Metadata/CustomerMetadata.php index 7ed806e657e8..38f3fbcbdbde 100644 --- a/app/code/Magento/Customer/Model/Metadata/CustomerMetadata.php +++ b/app/code/Magento/Customer/Model/Metadata/CustomerMetadata.php @@ -33,20 +33,30 @@ class CustomerMetadata implements CustomerMetadataInterface */ private $attributeMetadataDataProvider; + /** + * List of system attributes which should be available to the clients. + * + * @var string[] + */ + private $systemAttributes; + /** * @param AttributeMetadataConverter $attributeMetadataConverter * @param AttributeMetadataDataProvider $attributeMetadataDataProvider + * @param string[] $systemAttributes */ public function __construct( AttributeMetadataConverter $attributeMetadataConverter, - AttributeMetadataDataProvider $attributeMetadataDataProvider + AttributeMetadataDataProvider $attributeMetadataDataProvider, + array $systemAttributes = [] ) { $this->attributeMetadataConverter = $attributeMetadataConverter; $this->attributeMetadataDataProvider = $attributeMetadataDataProvider; + $this->systemAttributes = $systemAttributes; } /** - * {@inheritdoc} + * @inheritdoc */ public function getAttributes($formCode) { @@ -67,7 +77,7 @@ public function getAttributes($formCode) } /** - * {@inheritdoc} + * @inheritdoc */ public function getAttributeMetadata($attributeCode) { @@ -92,7 +102,7 @@ public function getAttributeMetadata($attributeCode) } /** - * {@inheritdoc} + * @inheritdoc */ public function getAllAttributesMetadata() { @@ -116,7 +126,7 @@ public function getAllAttributesMetadata() } /** - * {@inheritdoc} + * @inheritdoc */ public function getCustomAttributesMetadata($dataObjectClassName = self::DATA_INTERFACE_NAME) { @@ -134,9 +144,10 @@ public function getCustomAttributesMetadata($dataObjectClassName = self::DATA_IN $isDataObjectMethod = isset($this->customerDataObjectMethods['get' . $camelCaseKey]) || isset($this->customerDataObjectMethods['is' . $camelCaseKey]); - /** Even though disable_auto_group_change is system attribute, it should be available to the clients */ if (!$isDataObjectMethod - && (!$attributeMetadata->isSystem() || $attributeCode == 'disable_auto_group_change') + && (!$attributeMetadata->isSystem() + || in_array($attributeCode, $this->systemAttributes) + ) ) { $customAttributes[] = $attributeMetadata; } diff --git a/app/code/Magento/Customer/Model/Metadata/Form/AbstractData.php b/app/code/Magento/Customer/Model/Metadata/Form/AbstractData.php index 168f00be16e3..8e443e93354b 100644 --- a/app/code/Magento/Customer/Model/Metadata/Form/AbstractData.php +++ b/app/code/Magento/Customer/Model/Metadata/Form/AbstractData.php @@ -12,6 +12,8 @@ use Magento\Framework\Validator\EmailAddress; /** + * Form Element Abstract Data Model + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ abstract class AbstractData @@ -137,6 +139,7 @@ public function setRequestScope($scope) /** * Set scope visibility + * * Search value only in scope or search value in scope and global * * @param boolean $flag @@ -281,9 +284,14 @@ protected function _validateInputRule($value) ); if ($inputValidation !== null) { + $allowWhiteSpace = false; + switch ($inputValidation) { + case 'alphanum-with-spaces': + $allowWhiteSpace = true; + // Continue to alphanumeric validation case 'alphanumeric': - $validator = new \Zend_Validate_Alnum(true); + $validator = new \Zend_Validate_Alnum($allowWhiteSpace); $validator->setMessage(__('"%1" invalid type entered.', $label), \Zend_Validate_Alnum::INVALID); $validator->setMessage( __('"%1" contains non-alphabetic or non-numeric characters.', $label), diff --git a/app/code/Magento/Customer/Model/Metadata/Form/File.php b/app/code/Magento/Customer/Model/Metadata/Form/File.php index e6e9c2b50c06..227e85ed98f9 100644 --- a/app/code/Magento/Customer/Model/Metadata/Form/File.php +++ b/app/code/Magento/Customer/Model/Metadata/Form/File.php @@ -15,6 +15,8 @@ use Magento\Framework\Filesystem; /** + * Processes files that are save for customer. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class File extends AbstractData @@ -66,7 +68,7 @@ class File extends AbstractData * @param \Psr\Log\LoggerInterface $logger * @param \Magento\Customer\Api\Data\AttributeMetadataInterface $attribute * @param \Magento\Framework\Locale\ResolverInterface $localeResolver - * @param null $value + * @param string|array $value * @param string $entityTypeCode * @param bool $isAjax * @param \Magento\Framework\Url\EncoderInterface $urlEncoder @@ -101,7 +103,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function extractValue(\Magento\Framework\App\RequestInterface $request) @@ -109,7 +111,7 @@ public function extractValue(\Magento\Framework\App\RequestInterface $request) $extend = $this->_getRequestValue($request); $attrCode = $this->getAttribute()->getAttributeCode(); - if ($this->_requestScope) { + if ($this->_requestScope || !isset($_FILES[$attrCode])) { $value = []; if (strpos($this->_requestScope, '/') !== false) { $scopes = explode('/', $this->_requestScope); @@ -160,8 +162,7 @@ public function extractValue(\Magento\Framework\App\RequestInterface $request) } /** - * Validate file by attribute validate rules - * Return array of errors + * Validate file by attribute validate rules. Returns array of errors. * * @param array $value * @return string[] @@ -232,7 +233,7 @@ protected function _isUploadedFile($filename) } /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -273,7 +274,7 @@ public function validateValue($value) } /** - * {@inheritdoc} + * @inheritdoc * * @return ImageContentInterface|array|string|null */ @@ -358,7 +359,7 @@ protected function processInputFieldValue($value) } /** - * {@inheritdoc} + * @inheritdoc */ public function restoreValue($value) { @@ -366,7 +367,7 @@ public function restoreValue($value) } /** - * {@inheritdoc} + * @inheritdoc */ public function outputValue($format = \Magento\Customer\Model\Metadata\ElementFactory::OUTPUT_FORMAT_TEXT) { diff --git a/app/code/Magento/Customer/Model/Metadata/Form/Image.php b/app/code/Magento/Customer/Model/Metadata/Form/Image.php index 2104f941a6bc..33bdf827f80f 100644 --- a/app/code/Magento/Customer/Model/Metadata/Form/Image.php +++ b/app/code/Magento/Customer/Model/Metadata/Form/Image.php @@ -16,6 +16,8 @@ use Magento\Framework\Filesystem; /** + * Metadata for form image field + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Image extends File @@ -32,7 +34,7 @@ class Image extends File * @param \Psr\Log\LoggerInterface $logger * @param \Magento\Customer\Api\Data\AttributeMetadataInterface $attribute * @param \Magento\Framework\Locale\ResolverInterface $localeResolver - * @param null $value + * @param null|string $value * @param string $entityTypeCode * @param bool $isAjax * @param \Magento\Framework\Url\EncoderInterface $urlEncoder @@ -78,6 +80,7 @@ public function __construct( /** * Validate file by attribute validate rules + * * Return array of errors * * @param array $value @@ -133,7 +136,7 @@ protected function _validateByRules($value) $maxImageHeight = ArrayObjectSearch::getArrayElementByName( $rules, - 'max_image_heght' + 'max_image_height' ); if ($maxImageHeight !== null) { if ($maxImageHeight < $imageProp[1]) { diff --git a/app/code/Magento/Customer/Model/Metadata/Form/Text.php b/app/code/Magento/Customer/Model/Metadata/Form/Text.php index c8b9a1e46a12..c639b607e279 100644 --- a/app/code/Magento/Customer/Model/Metadata/Form/Text.php +++ b/app/code/Magento/Customer/Model/Metadata/Form/Text.php @@ -11,6 +11,9 @@ use Magento\Customer\Api\Data\AttributeMetadataInterface; use Magento\Framework\Api\ArrayObjectSearch; +/** + * Form Text metadata + */ class Text extends AbstractData { /** @@ -52,8 +55,6 @@ public function extractValue(\Magento\Framework\App\RequestInterface $request) /** * @inheritdoc - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) */ public function validateValue($value) { @@ -66,12 +67,12 @@ public function validateValue($value) $value = $this->_value; } - if ($attribute->isRequired() && empty($value) && $value !== '0') { - $errors[] = __('"%1" is a required value.', $label); + if (!$attribute->isRequired() && empty($value)) { + return true; } - if (!$errors && !$attribute->isRequired() && empty($value)) { - return true; + if (empty($value) && $value !== '0') { + $errors[] = __('"%1" is a required value.', $label); } $errors = $this->validateLength($value, $attribute, $errors); @@ -80,6 +81,7 @@ public function validateValue($value) if ($result !== true) { $errors = array_merge($errors, $result); } + if (count($errors) == 0) { return true; } diff --git a/app/code/Magento/Customer/Model/Options.php b/app/code/Magento/Customer/Model/Options.php index 7747e309d82a..71e70f8e1420 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/Renderer/Region.php b/app/code/Magento/Customer/Model/Renderer/Region.php index 5c7fcd38d6c5..a26cfb96fe02 100644 --- a/app/code/Magento/Customer/Model/Renderer/Region.php +++ b/app/code/Magento/Customer/Model/Renderer/Region.php @@ -54,6 +54,8 @@ public function __construct( } /** + * Render element + * * @param AbstractElement $element * @return string * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -80,7 +82,7 @@ public function render(AbstractElement $element) $regionCollection = self::$_regionCollections[$countryId]; } - $regionId = intval($element->getForm()->getElement('region_id')->getValue()); + $regionId = (int)$element->getForm()->getElement('region_id')->getValue(); $htmlAttributes = $element->getHtmlAttributes(); foreach ($htmlAttributes as $key => $attribute) { diff --git a/app/code/Magento/Customer/Model/ResourceModel/Address.php b/app/code/Magento/Customer/Model/ResourceModel/Address.php index a52c37231084..200eaabe6517 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Address.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Address.php @@ -14,6 +14,7 @@ /** * Class Address + * * @package Magento\Customer\Model\ResourceModel * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -31,8 +32,8 @@ class Address extends \Magento\Eav\Model\Entity\VersionControl\AbstractEntity /** * @param \Magento\Eav\Model\Entity\Context $context - * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot $entitySnapshot, - * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\RelationComposite $entityRelationComposite, + * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot $entitySnapshot + * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\RelationComposite $entityRelationComposite * @param \Magento\Framework\Validator\Factory $validatorFactory * @param \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository * @param array $data @@ -98,6 +99,9 @@ protected function _beforeSave(\Magento\Framework\DataObject $address) */ protected function _validate($address) { + if ($address->getDataByKey('should_ignore_validation')) { + return; + }; $validator = $this->_validatorFactory->createValidator('customer_address', 'save'); if (!$validator->isValid($address)) { @@ -110,7 +114,7 @@ protected function _validate($address) } /** - * {@inheritdoc} + * @inheritdoc */ public function delete($object) { @@ -120,6 +124,8 @@ public function delete($object) } /** + * Get instance of DeleteRelation class + * * @deprecated 100.2.0 * @return DeleteRelation */ @@ -129,6 +135,8 @@ private function getDeleteRelation() } /** + * Get instance of CustomerRegistry class + * * @deprecated 100.2.0 * @return CustomerRegistry */ @@ -138,6 +146,8 @@ private function getCustomerRegistry() } /** + * After delete entity process + * * @param \Magento\Customer\Model\Address $address * @return $this */ diff --git a/app/code/Magento/Customer/Model/ResourceModel/Address/Grid/Collection.php b/app/code/Magento/Customer/Model/ResourceModel/Address/Grid/Collection.php new file mode 100644 index 000000000000..4e0347059086 --- /dev/null +++ b/app/code/Magento/Customer/Model/ResourceModel/Address/Grid/Collection.php @@ -0,0 +1,143 @@ +<?php +declare(strict_types=1); +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Customer\Model\ResourceModel\Address\Grid; + +use Magento\Framework\Api\Search\SearchResultInterface; +use Magento\Framework\Api\Search\AggregationInterface; +use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection; + +/** + * Class getting collection of addresses assigned to customer + */ +class Collection extends AbstractCollection implements SearchResultInterface +{ + /** + * @var AggregationInterface + */ + private $aggregations; + + /** + * @param \Magento\Framework\Data\Collection\EntityFactoryInterface $entityFactory + * @param \Psr\Log\LoggerInterface $logger + * @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy + * @param \Magento\Framework\Event\ManagerInterface $eventManager + * @param string $mainTable + * @param string $eventPrefix + * @param string $eventObject + * @param string $resourceModel + * @param string $model + * @param \Magento\Framework\DB\Adapter\AdapterInterface|string|null $connection + * @param \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + \Magento\Framework\Data\Collection\EntityFactoryInterface $entityFactory, + \Psr\Log\LoggerInterface $logger, + \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy, + \Magento\Framework\Event\ManagerInterface $eventManager, + $mainTable, + $eventPrefix, + $eventObject, + $resourceModel, + $model = \Magento\Framework\View\Element\UiComponent\DataProvider\Document::class, + $connection = null, + \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource = null + ) { + $this->_eventPrefix = $eventPrefix; + $this->_eventObject = $eventObject; + $this->_init($model, $resourceModel); + $this->setMainTable($mainTable); + $this->_idFieldName = 'entity_id'; + parent::__construct( + $entityFactory, + $logger, + $fetchStrategy, + $eventManager, + $connection, + $resource + ); + } + + /** + * @inheritdoc + * + * @return AggregationInterface + */ + public function getAggregations() + { + return $this->aggregations; + } + + /** + * @inheritdoc + * + * @param AggregationInterface $aggregations + * @return $this + */ + public function setAggregations($aggregations) + { + $this->aggregations = $aggregations; + return $this; + } + + /** + * Get search criteria. + * + * @return \Magento\Framework\Api\SearchCriteriaInterface|null + */ + public function getSearchCriteria() + { + return null; + } + + /** + * Set search criteria. + * + * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria + * @return $this + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function setSearchCriteria(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria = null) + { + return $this; + } + + /** + * Get total count. + * + * @return int + */ + public function getTotalCount() + { + return $this->getSize(); + } + + /** + * Set total count. + * + * @param int $totalCount + * @return $this + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function setTotalCount($totalCount) + { + return $this; + } + + /** + * Set items list. + * + * @param \Magento\Framework\Api\ExtensibleDataInterface[] $items + * @return $this + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function setItems(array $items = null) + { + return $this; + } +} diff --git a/app/code/Magento/Customer/Model/ResourceModel/Address/Relation.php b/app/code/Magento/Customer/Model/ResourceModel/Address/Relation.php index d473a4dc0189..ae342a1b10dd 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Address/Relation.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Address/Relation.php @@ -7,6 +7,8 @@ */ namespace Magento\Customer\Model\ResourceModel\Address; +use Magento\Customer\Model\Address; +use Magento\Customer\Model\Customer; use Magento\Framework\Model\ResourceModel\Db\VersionControl\RelationInterface; /** @@ -36,19 +38,14 @@ public function __construct(\Magento\Customer\Model\CustomerFactory $customerFac public function processRelation(\Magento\Framework\Model\AbstractModel $object) { /** - * @var $object \Magento\Customer\Model\Address + * @var $object Address */ - if (!$object->getIsCustomerSaveTransaction() && $this->isAddressDefault($object)) { + if (!$object->getIsCustomerSaveTransaction() && $object->getId()) { $customer = $this->customerFactory->create()->load($object->getCustomerId()); - $changedAddresses = []; - - if ($object->getIsDefaultBilling()) { - $changedAddresses['default_billing'] = $object->getId(); - } - if ($object->getIsDefaultShipping()) { - $changedAddresses['default_shipping'] = $object->getId(); - } + $changedAddresses = []; + $changedAddresses = $this->getDefaultBillingChangedAddress($object, $customer, $changedAddresses); + $changedAddresses = $this->getDefaultShippingChangedAddress($object, $customer, $changedAddresses); if ($changedAddresses) { $customer->getResource()->getConnection()->update( @@ -60,9 +57,62 @@ public function processRelation(\Magento\Framework\Model\AbstractModel $object) } } + /** + * Get default billing changed address + * + * @param Address $object + * @param Customer $customer + * @param array $changedAddresses + * @return array + */ + private function getDefaultBillingChangedAddress( + Address $object, + Customer $customer, + array $changedAddresses + ): array { + if ($object->getIsDefaultBilling()) { + $changedAddresses['default_billing'] = $object->getId(); + } elseif ($customer->getDefaultBillingAddress() + && $object->getIsDefaultBilling() === false + && (int)$customer->getDefaultBillingAddress()->getId() === (int)$object->getId() + ) { + $changedAddresses['default_billing'] = null; + } + + return $changedAddresses; + } + + /** + * Get default shipping changed address + * + * @param Address $object + * @param Customer $customer + * @param array $changedAddresses + * @return array + */ + private function getDefaultShippingChangedAddress( + Address $object, + Customer $customer, + array $changedAddresses + ): array { + if ($object->getIsDefaultShipping()) { + $changedAddresses['default_shipping'] = $object->getId(); + } elseif ($customer->getDefaultShippingAddress() + && $object->getIsDefaultShipping() === false + && (int)$customer->getDefaultShippingAddress()->getId() === (int)$object->getId() + ) { + $changedAddresses['default_shipping'] = null; + } + + return $changedAddresses; + } + /** * Checks if address has chosen as default and has had an id * + * @deprecated Is not used anymore due to changes in logic of save of address. + * If address was default and becomes not default than default address id for customer must be + * set to null * @param \Magento\Framework\Model\AbstractModel $object * @return bool */ diff --git a/app/code/Magento/Customer/Model/ResourceModel/AddressRepository.php b/app/code/Magento/Customer/Model/ResourceModel/AddressRepository.php index 7f69ab3c02bc..3fe61785de89 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/AddressRepository.php +++ b/app/code/Magento/Customer/Model/ResourceModel/AddressRepository.php @@ -7,6 +7,7 @@ */ namespace Magento\Customer\Model\ResourceModel; +use Magento\Customer\Api\Data\AddressInterface; use Magento\Customer\Model\Address as CustomerAddressModel; use Magento\Customer\Model\Customer as CustomerModel; use Magento\Customer\Model\ResourceModel\Address\Collection; @@ -16,6 +17,8 @@ use Magento\Framework\Exception\InputException; /** + * Address repository. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class AddressRepository implements \Magento\Customer\Api\AddressRepositoryInterface @@ -123,6 +126,7 @@ public function save(\Magento\Customer\Api\Data\AddressInterface $address) } else { $addressModel->updateData($address); } + $addressModel->setStoreId($customerModel->getStoreId()); $errors = $addressModel->validate(); if ($errors !== true) { @@ -143,6 +147,8 @@ public function save(\Magento\Customer\Api\Data\AddressInterface $address) } /** + * Update address collection. + * * @param Customer $customer * @param Address $address * @throws \Magento\Framework\Exception\LocalizedException diff --git a/app/code/Magento/Customer/Model/ResourceModel/Customer.php b/app/code/Magento/Customer/Model/ResourceModel/Customer.php index f51020155968..2eb1ef897e70 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Customer.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Customer.php @@ -151,7 +151,9 @@ protected function _beforeSave(\Magento\Framework\DataObject $customer) $customer->setConfirmation(null); } - $this->_validate($customer); + if (!$customer->getData('ignore_validation_flag')) { + $this->_validate($customer); + } return $this; } diff --git a/app/code/Magento/Customer/Model/ResourceModel/Customer/Relation.php b/app/code/Magento/Customer/Model/ResourceModel/Customer/Relation.php index e55c5d443c9d..96f47154e874 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Customer/Relation.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Customer/Relation.php @@ -23,41 +23,43 @@ public function processRelation(\Magento\Framework\Model\AbstractModel $customer $defaultBillingId = $customer->getData('default_billing'); $defaultShippingId = $customer->getData('default_shipping'); - /** @var \Magento\Customer\Model\Address $address */ - foreach ($customer->getAddresses() as $address) { - if ($address->getData('_deleted')) { - if ($address->getId() == $defaultBillingId) { - $customer->setData('default_billing', null); - } + if (!$customer->getData('ignore_validation_flag')) { + /** @var \Magento\Customer\Model\Address $address */ + foreach ($customer->getAddresses() as $address) { + if ($address->getData('_deleted')) { + if ($address->getId() == $defaultBillingId) { + $customer->setData('default_billing', null); + } - if ($address->getId() == $defaultShippingId) { - $customer->setData('default_shipping', null); - } + if ($address->getId() == $defaultShippingId) { + $customer->setData('default_shipping', null); + } - $removedAddressId = $address->getId(); - $address->delete(); + $removedAddressId = $address->getId(); + $address->delete(); - // Remove deleted address from customer address collection - $customer->getAddressesCollection()->removeItemByKey($removedAddressId); - } else { - $address->setParentId( - $customer->getId() - )->setStoreId( - $customer->getStoreId() - )->setIsCustomerSaveTransaction( - true - )->save(); + // Remove deleted address from customer address collection + $customer->getAddressesCollection()->removeItemByKey($removedAddressId); + } else { + $address->setParentId( + $customer->getId() + )->setStoreId( + $customer->getStoreId() + )->setIsCustomerSaveTransaction( + true + )->save(); - if (($address->getIsPrimaryBilling() || - $address->getIsDefaultBilling()) && $address->getId() != $defaultBillingId - ) { - $customer->setData('default_billing', $address->getId()); - } + if (($address->getIsPrimaryBilling() || + $address->getIsDefaultBilling()) && $address->getId() != $defaultBillingId + ) { + $customer->setData('default_billing', $address->getId()); + } - if (($address->getIsPrimaryShipping() || - $address->getIsDefaultShipping()) && $address->getId() != $defaultShippingId - ) { - $customer->setData('default_shipping', $address->getId()); + if (($address->getIsPrimaryShipping() || + $address->getIsDefaultShipping()) && $address->getId() != $defaultShippingId + ) { + $customer->setData('default_shipping', $address->getId()); + } } } } diff --git a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php index 29e35c721a3b..ddc0576cb0da 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php +++ b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php @@ -8,69 +8,80 @@ use Magento\Customer\Api\CustomerMetadataInterface; use Magento\Customer\Api\Data\CustomerInterface; -use Magento\Customer\Model\Delegation\Data\NewOperation; +use Magento\Customer\Api\Data\CustomerSearchResultsInterfaceFactory; +use Magento\Framework\Api\ExtensibleDataObjectConverter; +use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; +use Magento\Customer\Model\CustomerFactory; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Customer\Model\Data\CustomerSecureFactory; use Magento\Customer\Model\Customer\NotificationStorage; +use Magento\Customer\Model\Delegation\Data\NewOperation; +use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Framework\Api\DataObjectHelper; use Magento\Framework\Api\ImageProcessorInterface; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\Framework\Api\Search\FilterGroup; +use Magento\Framework\Event\ManagerInterface; use Magento\Customer\Model\Delegation\Storage as DelegatedStorage; use Magento\Framework\App\ObjectManager; +use Magento\Store\Model\StoreManagerInterface; /** * Customer repository. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) */ -class CustomerRepository implements \Magento\Customer\Api\CustomerRepositoryInterface +class CustomerRepository implements CustomerRepositoryInterface { /** - * @var \Magento\Customer\Model\CustomerFactory + * @var CustomerFactory */ protected $customerFactory; /** - * @var \Magento\Customer\Model\Data\CustomerSecureFactory + * @var CustomerSecureFactory */ protected $customerSecureFactory; /** - * @var \Magento\Customer\Model\CustomerRegistry + * @var CustomerRegistry */ protected $customerRegistry; /** - * @var \Magento\Customer\Model\ResourceModel\AddressRepository + * @var AddressRepository */ protected $addressRepository; /** - * @var \Magento\Customer\Model\ResourceModel\Customer + * @var Customer */ protected $customerResourceModel; /** - * @var \Magento\Customer\Api\CustomerMetadataInterface + * @var CustomerMetadataInterface */ protected $customerMetadata; /** - * @var \Magento\Customer\Api\Data\CustomerSearchResultsInterfaceFactory + * @var CustomerSearchResultsInterfaceFactory */ protected $searchResultsFactory; /** - * @var \Magento\Framework\Event\ManagerInterface + * @var ManagerInterface */ protected $eventManager; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $storeManager; /** - * @var \Magento\Framework\Api\ExtensibleDataObjectConverter + * @var ExtensibleDataObjectConverter */ protected $extensibleDataObjectConverter; @@ -85,7 +96,7 @@ class CustomerRepository implements \Magento\Customer\Api\CustomerRepositoryInte protected $imageProcessor; /** - * @var \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface + * @var JoinProcessorInterface */ protected $extensionAttributesJoinProcessor; @@ -105,38 +116,38 @@ class CustomerRepository implements \Magento\Customer\Api\CustomerRepositoryInte private $delegatedStorage; /** - * @param \Magento\Customer\Model\CustomerFactory $customerFactory - * @param \Magento\Customer\Model\Data\CustomerSecureFactory $customerSecureFactory - * @param \Magento\Customer\Model\CustomerRegistry $customerRegistry - * @param \Magento\Customer\Model\ResourceModel\AddressRepository $addressRepository - * @param \Magento\Customer\Model\ResourceModel\Customer $customerResourceModel - * @param \Magento\Customer\Api\CustomerMetadataInterface $customerMetadata - * @param \Magento\Customer\Api\Data\CustomerSearchResultsInterfaceFactory $searchResultsFactory - * @param \Magento\Framework\Event\ManagerInterface $eventManager - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Api\ExtensibleDataObjectConverter $extensibleDataObjectConverter + * @param CustomerFactory $customerFactory + * @param CustomerSecureFactory $customerSecureFactory + * @param CustomerRegistry $customerRegistry + * @param AddressRepository $addressRepository + * @param Customer $customerResourceModel + * @param CustomerMetadataInterface $customerMetadata + * @param CustomerSearchResultsInterfaceFactory $searchResultsFactory + * @param ManagerInterface $eventManager + * @param StoreManagerInterface $storeManager + * @param ExtensibleDataObjectConverter $extensibleDataObjectConverter * @param DataObjectHelper $dataObjectHelper * @param ImageProcessorInterface $imageProcessor - * @param \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $extensionAttributesJoinProcessor + * @param JoinProcessorInterface $extensionAttributesJoinProcessor * @param CollectionProcessorInterface $collectionProcessor * @param NotificationStorage $notificationStorage * @param DelegatedStorage|null $delegatedStorage * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Customer\Model\CustomerFactory $customerFactory, - \Magento\Customer\Model\Data\CustomerSecureFactory $customerSecureFactory, - \Magento\Customer\Model\CustomerRegistry $customerRegistry, - \Magento\Customer\Model\ResourceModel\AddressRepository $addressRepository, - \Magento\Customer\Model\ResourceModel\Customer $customerResourceModel, - \Magento\Customer\Api\CustomerMetadataInterface $customerMetadata, - \Magento\Customer\Api\Data\CustomerSearchResultsInterfaceFactory $searchResultsFactory, - \Magento\Framework\Event\ManagerInterface $eventManager, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\Api\ExtensibleDataObjectConverter $extensibleDataObjectConverter, + CustomerFactory $customerFactory, + CustomerSecureFactory $customerSecureFactory, + CustomerRegistry $customerRegistry, + AddressRepository $addressRepository, + Customer $customerResourceModel, + CustomerMetadataInterface $customerMetadata, + CustomerSearchResultsInterfaceFactory $searchResultsFactory, + ManagerInterface $eventManager, + StoreManagerInterface $storeManager, + ExtensibleDataObjectConverter $extensibleDataObjectConverter, DataObjectHelper $dataObjectHelper, ImageProcessorInterface $imageProcessor, - \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $extensionAttributesJoinProcessor, + JoinProcessorInterface $extensionAttributesJoinProcessor, CollectionProcessorInterface $collectionProcessor, NotificationStorage $notificationStorage, DelegatedStorage $delegatedStorage = null @@ -156,12 +167,18 @@ public function __construct( $this->extensionAttributesJoinProcessor = $extensionAttributesJoinProcessor; $this->collectionProcessor = $collectionProcessor; $this->notificationStorage = $notificationStorage; - $this->delegatedStorage = $delegatedStorage - ?? ObjectManager::getInstance()->get(DelegatedStorage::class); + $this->delegatedStorage = $delegatedStorage ?? ObjectManager::getInstance()->get(DelegatedStorage::class); } /** - * {@inheritdoc} + * Create or update a customer. + * + * @param \Magento\Customer\Api\Data\CustomerInterface $customer + * @param string $passwordHash + * @return \Magento\Customer\Api\Data\CustomerInterface + * @throws \Magento\Framework\Exception\InputException If bad input is provided + * @throws \Magento\Framework\Exception\State\InputMismatchException If the provided email is already used + * @throws \Magento\Framework\Exception\LocalizedException * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -204,18 +221,19 @@ public function save(CustomerInterface $customer, $passwordHash = null) $customerModel->setRpToken(null); $customerModel->setRpTokenCreatedAt(null); } - if (!array_key_exists('default_billing', $customerArr) + if (!array_key_exists('addresses', $customerArr) && null !== $prevCustomerDataArr && array_key_exists('default_billing', $prevCustomerDataArr) ) { $customerModel->setDefaultBilling($prevCustomerDataArr['default_billing']); } - if (!array_key_exists('default_shipping', $customerArr) + if (!array_key_exists('addresses', $customerArr) && null !== $prevCustomerDataArr && array_key_exists('default_shipping', $prevCustomerDataArr) ) { $customerModel->setDefaultShipping($prevCustomerDataArr['default_shipping']); } + $this->setValidationFlag($customerArr, $customerModel); $customerModel->save(); $this->customerRegistry->push($customerModel); $customerId = $customerModel->getId(); @@ -225,7 +243,7 @@ public function save(CustomerInterface $customer, $passwordHash = null) ) { $customer->setAddresses($delegatedNewOperation->getCustomer()->getAddresses()); } - if ($customer->getAddresses() !== null) { + if ($customer->getAddresses() !== null && !$customerModel->getData('ignore_validation_flag')) { if ($customer->getId()) { $existingAddresses = $this->getById($customer->getId())->getAddresses(); $getIdFunc = function ($address) { @@ -293,7 +311,13 @@ private function populateCustomerWithSecureData($customerModel, $passwordHash = } /** - * {@inheritdoc} + * Retrieve customer. + * + * @param string $email + * @param int|null $websiteId + * @return \Magento\Customer\Api\Data\CustomerInterface + * @throws \Magento\Framework\Exception\NoSuchEntityException If customer with the specified email does not exist. + * @throws \Magento\Framework\Exception\LocalizedException */ public function get($email, $websiteId = null) { @@ -302,7 +326,12 @@ public function get($email, $websiteId = null) } /** - * {@inheritdoc} + * Get customer by Customer ID. + * + * @param int $customerId + * @return \Magento\Customer\Api\Data\CustomerInterface + * @throws \Magento\Framework\Exception\NoSuchEntityException If customer with the specified ID does not exist. + * @throws \Magento\Framework\Exception\LocalizedException */ public function getById($customerId) { @@ -311,7 +340,15 @@ public function getById($customerId) } /** - * {@inheritdoc} + * Retrieve customers which match a specified criteria. + * + * This call returns an array of objects, but detailed information about each object’s attributes might not be + * included. See http://devdocs.magento.com/codelinks/attributes.html#CustomerRepositoryInterface to determine + * which call to use to get detailed information about all attributes for an object. + * + * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria + * @return \Magento\Customer\Api\Data\CustomerSearchResultsInterface + * @throws \Magento\Framework\Exception\LocalizedException */ public function getList(SearchCriteriaInterface $searchCriteria) { @@ -335,7 +372,7 @@ public function getList(SearchCriteriaInterface $searchCriteria) ->joinAttribute('billing_telephone', 'customer_address/telephone', 'default_billing', null, 'left') ->joinAttribute('billing_region', 'customer_address/region', 'default_billing', null, 'left') ->joinAttribute('billing_country_id', 'customer_address/country_id', 'default_billing', null, 'left') - ->joinAttribute('company', 'customer_address/company', 'default_billing', null, 'left'); + ->joinAttribute('billing_company', 'customer_address/company', 'default_billing', null, 'left'); $this->collectionProcessor->process($searchCriteria, $collection); @@ -351,7 +388,11 @@ public function getList(SearchCriteriaInterface $searchCriteria) } /** - * {@inheritdoc} + * Delete customer. + * + * @param \Magento\Customer\Api\Data\CustomerInterface $customer + * @return bool true on success + * @throws \Magento\Framework\Exception\LocalizedException */ public function delete(CustomerInterface $customer) { @@ -359,7 +400,12 @@ public function delete(CustomerInterface $customer) } /** - * {@inheritdoc} + * Delete customer by Customer ID. + * + * @param int $customerId + * @return bool true on success + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Magento\Framework\Exception\LocalizedException */ public function deleteById($customerId) { @@ -375,15 +421,12 @@ public function deleteById($customerId) * Helper function that adds a FilterGroup to the collection. * * @deprecated 100.2.0 - * @param \Magento\Framework\Api\Search\FilterGroup $filterGroup - * @param \Magento\Customer\Model\ResourceModel\Customer\Collection $collection + * @param FilterGroup $filterGroup + * @param Collection $collection * @return void - * @throws \Magento\Framework\Exception\InputException */ - protected function addFilterGroupToCollection( - \Magento\Framework\Api\Search\FilterGroup $filterGroup, - \Magento\Customer\Model\ResourceModel\Customer\Collection $collection - ) { + protected function addFilterGroupToCollection(FilterGroup $filterGroup, Collection $collection) + { $fields = []; foreach ($filterGroup->getFilters() as $filter) { $condition = $filter->getConditionType() ? $filter->getConditionType() : 'eq'; @@ -393,4 +436,18 @@ protected function addFilterGroupToCollection( $collection->addFieldToFilter($fields); } } + + /** + * Set ignore_validation_flag to skip model validation + * + * @param array $customerArray + * @param Customer $customerModel + * @return void + */ + private function setValidationFlag($customerArray, $customerModel) + { + if (isset($customerArray['ignore_validation_flag'])) { + $customerModel->setData('ignore_validation_flag', true); + } + } } diff --git a/app/code/Magento/Customer/Model/ResourceModel/Group.php b/app/code/Magento/Customer/Model/ResourceModel/Group.php index 80203e742e09..987723c5c9f5 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Group.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Group.php @@ -29,8 +29,8 @@ class Group extends \Magento\Framework\Model\ResourceModel\Db\VersionControl\Abs /** * @param \Magento\Framework\Model\ResourceModel\Db\Context $context - * @param Snapshot $entitySnapshot, - * @param RelationComposite $entityRelationComposite, + * @param Snapshot $entitySnapshot + * @param RelationComposite $entityRelationComposite * @param \Magento\Customer\Api\GroupManagementInterface $groupManagement * @param Customer\CollectionFactory $customersFactory * @param string $connectionName @@ -110,6 +110,8 @@ protected function _afterDelete(\Magento\Framework\Model\AbstractModel $group) } /** + * Create customers collection. + * * @return \Magento\Customer\Model\ResourceModel\Customer\Collection */ protected function _createCustomersCollection() @@ -131,7 +133,7 @@ protected function _beforeSave(\Magento\Framework\Model\AbstractModel $group) } /** - * {@inheritdoc} + * @inheritdoc */ protected function _afterSave(\Magento\Framework\Model\AbstractModel $object) { diff --git a/app/code/Magento/Customer/Model/Vat.php b/app/code/Magento/Customer/Model/Vat.php index 9822e2ad1b80..123a9eef4b75 100644 --- a/app/code/Magento/Customer/Model/Vat.php +++ b/app/code/Magento/Customer/Model/Vat.php @@ -179,15 +179,23 @@ public function checkVatNumber($countryCode, $vatNumber, $requesterCountryCode = return $gatewayResponse; } + $countryCodeForVatNumber = $this->getCountryCodeForVatNumber($countryCode); + $requesterCountryCodeForVatNumber = $this->getCountryCodeForVatNumber($requesterCountryCode); + try { $soapClient = $this->createVatNumberValidationSoapClient(); $requestParams = []; - $requestParams['countryCode'] = $countryCode; - $requestParams['vatNumber'] = str_replace([' ', '-'], ['', ''], $vatNumber); - $requestParams['requesterCountryCode'] = $requesterCountryCode; - $requestParams['requesterVatNumber'] = str_replace([' ', '-'], ['', ''], $requesterVatNumber); - + $requestParams['countryCode'] = $countryCodeForVatNumber; + $vatNumberSanitized = $this->isCountryInEU($countryCode) + ? str_replace([' ', '-', $countryCodeForVatNumber], ['', '', ''], $vatNumber) + : str_replace([' ', '-'], ['', ''], $vatNumber); + $requestParams['vatNumber'] = $vatNumberSanitized; + $requestParams['requesterCountryCode'] = $requesterCountryCodeForVatNumber; + $reqVatNumSanitized = $this->isCountryInEU($requesterCountryCode) + ? str_replace([' ', '-', $requesterCountryCodeForVatNumber], ['', '', ''], $requesterVatNumber) + : str_replace([' ', '-'], ['', ''], $requesterVatNumber); + $requestParams['requesterVatNumber'] = $reqVatNumSanitized; // Send request to service $result = $soapClient->checkVatApprox($requestParams); @@ -296,4 +304,22 @@ public function isCountryInEU($countryCode, $storeId = null) ); return in_array($countryCode, $euCountries); } + + /** + * Returns the country code to use in the VAT number which is not always the same as the normal country code + * + * @param string $countryCode + * @return string + */ + private function getCountryCodeForVatNumber(string $countryCode): string + { + // Greece uses a different code for VAT numbers then its country code + // See: http://ec.europa.eu/taxation_customs/vies/faq.html#item_11 + // And https://en.wikipedia.org/wiki/VAT_identification_number: + // "The full identifier starts with an ISO 3166-1 alpha-2 (2 letters) country code + // (except for Greece, which uses the ISO 639-1 language code EL for the Greek language, + // instead of its ISO 3166-1 alpha-2 country code GR)" + + return $countryCode === 'GR' ? 'EL' : $countryCode; + } } diff --git a/app/code/Magento/Customer/Model/Visitor.php b/app/code/Magento/Customer/Model/Visitor.php index a0530389f902..4f129f05aa82 100644 --- a/app/code/Magento/Customer/Model/Visitor.php +++ b/app/code/Magento/Customer/Model/Visitor.php @@ -11,8 +11,10 @@ /** * Class Visitor + * * @package Magento\Customer\Model * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class Visitor extends \Magento\Framework\Model\AbstractModel { @@ -86,6 +88,7 @@ class Visitor extends \Magento\Framework\Model\AbstractModel * @param array $ignoredUserAgents * @param array $ignores * @param array $data + * @param RequestSafetyInterface|null $requestSafety * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -166,10 +169,6 @@ public function initByRequest($observer) $this->setLastVisitAt((new \DateTime())->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT)); - // prevent saving Visitor for safe methods, e.g. GET request - if ($this->requestSafety->isSafeMethod()) { - return $this; - } if (!$this->getId()) { $this->setSessionId($this->session->getSessionId()); $this->save(); @@ -277,6 +276,7 @@ public function bindQuoteCreate($observer) /** * Destroy binding of checkout quote + * * @param \Magento\Framework\Event\Observer $observer * @return \Magento\Customer\Model\Visitor */ @@ -320,11 +320,9 @@ public function clean() */ public function getOnlineInterval() { - $configValue = intval( - $this->scopeConfig->getValue( - static::XML_PATH_ONLINE_INTERVAL, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ) + $configValue = (int)$this->scopeConfig->getValue( + static::XML_PATH_ONLINE_INTERVAL, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE ); return $configValue ?: static::DEFAULT_ONLINE_MINUTES_INTERVAL; } diff --git a/app/code/Magento/Customer/Observer/UpgradeCustomerPasswordObserver.php b/app/code/Magento/Customer/Observer/UpgradeCustomerPasswordObserver.php index eb7e81009c92..26c4c50009bb 100644 --- a/app/code/Magento/Customer/Observer/UpgradeCustomerPasswordObserver.php +++ b/app/code/Magento/Customer/Observer/UpgradeCustomerPasswordObserver.php @@ -6,11 +6,15 @@ namespace Magento\Customer\Observer; +use Magento\Customer\Model\Customer; use Magento\Framework\Encryption\EncryptorInterface; use Magento\Framework\Event\ObserverInterface; use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Model\CustomerRegistry; +/** + * Class observer UpgradeCustomerPasswordObserver to upgrade customer password hash when customer has logged in + */ class UpgradeCustomerPasswordObserver implements ObserverInterface { /** @@ -61,7 +65,20 @@ public function execute(\Magento\Framework\Event\Observer $observer) if (!$this->encryptor->validateHashVersion($customerSecure->getPasswordHash(), true)) { $customerSecure->setPasswordHash($this->encryptor->getHash($password, true)); + // No need to validate customer and customer address while upgrading customer password + $this->setIgnoreValidationFlag($customer); $this->customerRepository->save($customer); } } + + /** + * Set ignore_validation_flag to skip unnecessary address and customer validation + * + * @param Customer $customer + * @return void + */ + private function setIgnoreValidationFlag($customer) + { + $customer->setData('ignore_validation_flag', true); + } } diff --git a/app/code/Magento/Customer/Setup/Patch/Data/MigrateStoresAllowedCountriesToWebsite.php b/app/code/Magento/Customer/Setup/Patch/Data/MigrateStoresAllowedCountriesToWebsite.php index 7488f3fd4a92..e4978070f53a 100644 --- a/app/code/Magento/Customer/Setup/Patch/Data/MigrateStoresAllowedCountriesToWebsite.php +++ b/app/code/Magento/Customer/Setup/Patch/Data/MigrateStoresAllowedCountriesToWebsite.php @@ -8,12 +8,14 @@ use Magento\Directory\Model\AllowedCountries; use Magento\Framework\Setup\ModuleDataSetupInterface; -use Magento\Directory\Model\AllowedCountriesFactory; use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\StoreManagerInterface; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; +/** + * Migrate store allowed countries to website. + */ class MigrateStoresAllowedCountriesToWebsite implements DataPatchInterface, PatchVersionInterface { /** @@ -27,7 +29,7 @@ class MigrateStoresAllowedCountriesToWebsite implements DataPatchInterface, Patc private $storeManager; /** - * @var AllowedCountriesFactory + * @var AllowedCountries */ private $allowedCountries; @@ -48,10 +50,11 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { + $this->moduleDataSetup->getConnection()->beginTransaction(); try { @@ -149,7 +152,7 @@ private function mergeAllowedCountries(array $countries, array $newCountries, $i } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -159,7 +162,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -167,7 +170,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { 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 000000000000..53dba774d6c4 --- /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 000000000000..a908d042fcc5 --- /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 000000000000..32b624706102 --- /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 000000000000..9d7c209121fd --- /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 000000000000..d7529b3bdd58 --- /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 000000000000..5557025c4b1d --- /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 000000000000..e33ebbb96ee1 --- /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 000000000000..390f723d91f1 --- /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/AdminConfigCustomerActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminConfigCustomerActionGroup.xml new file mode 100644 index 000000000000..8a3ab7068696 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminConfigCustomerActionGroup.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="SetCustomerDataLifetimeActionGroup"> + <arguments> + <argument name="minutes" defaultValue="60" type="string"/> + </arguments> + <amOnPage url="{{AdminCustomerConfigPage.url('#customer_online_customers-link')}}" stepKey="openCustomerConfigPage"/> + <fillField userInput="{{minutes}}" selector="{{AdminCustomerConfigSection.customerDataLifetime}}" stepKey="fillCustomerDataLifetime"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSave"/> + <seeElement selector="{{AdminMessagesSection.success}}" stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCreateCustomerGroupActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCreateCustomerGroupActionGroup.xml new file mode 100644 index 000000000000..ba984a4d8256 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCreateCustomerGroupActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCreateCustomerGroupActionGroup"> + <arguments> + <argument name="groupName" type="string"/> + <argument name="taxClass" type="string"/> + </arguments> + <amOnPage url="{{AdminNewCustomerGroupPage.url}}" stepKey="goToNewCustomerGroupPage"/> + <waitForPageLoad stepKey="waitForNewCustomerGroupPageLoad"/> + + <!--Set tax class for customer group--> + <fillField stepKey="fillGroupName" selector="{{AdminNewCustomerGroupSection.groupName}}" userInput="{{groupName}}"/> + <selectOption selector="{{AdminNewCustomerGroupSection.taxClass}}" userInput="{{taxClass}}" stepKey="selectTaxClassOption"/> + <click selector="{{AdminNewCustomerGroupSection.saveCustomerGroup}}" stepKey="clickToSaveCustomerGroup"/> + <waitForPageLoad stepKey="waitForCustomerGroupSaved"/> + <see stepKey="seeCustomerGroupSaveMessage" userInput="You saved the customer group."/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCreateCustomerWithWebsiteAndStoreViewActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCreateCustomerWithWebsiteAndStoreViewActionGroup.xml new file mode 100644 index 000000000000..37149e23dc87 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCreateCustomerWithWebsiteAndStoreViewActionGroup.xml @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See 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="AdminCreateCustomerWithWebsiteAndStoreViewActionGroup"> + <arguments> + <argument name="customerData"/> + <argument name="address"/> + <argument name="website" type="string"/> + <argument name="storeView" type="string"/> + </arguments> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="goToCustomersPage"/> + <click stepKey="addNewCustomer" selector="{{AdminCustomerGridMainActionsSection.addNewCustomer}}"/> + <selectOption stepKey="selectWebSite" selector="{{AdminCustomerAccountInformationSection.associateToWebsite}}" userInput="{{website}}"/> + <fillField stepKey="FillFirstName" selector="{{AdminCustomerAccountInformationSection.firstName}}" userInput="{{customerData.firstname}}"/> + <fillField stepKey="FillLastName" selector="{{AdminCustomerAccountInformationSection.lastName}}" userInput="{{customerData.lastname}}"/> + <fillField stepKey="FillEmail" selector="{{AdminCustomerAccountInformationSection.email}}" userInput="{{customerData.email}}"/> + <selectOption stepKey="selectStoreView" selector="{{AdminCustomerAccountInformationSection.storeView}}" userInput="{{storeView}}"/> + <scrollToTopOfPage stepKey="scrollToTopOfThePage"/> + <click stepKey="saveCustomer" selector="{{AdminCustomerAccountInformationSection.saveCustomerAndContinueEdit}}"/> + <waitForPageLoad stepKey="waitForCustomersPage"/> + <see stepKey="seeSuccessMessage" userInput="You saved the customer."/> + <click stepKey="goToAddresses" selector="{{AdminCustomerAccountInformationSection.addressesButton}}"/> + <waitForPageLoad stepKey="waitForAddresses"/> + <click stepKey="clickOnAddNewAddress" selector="{{AdminCustomerAddressesSection.addNewAddress}}"/> + <waitForPageLoad stepKey="waitForAddressFields"/> + <click stepKey="thickBillingAddress" selector="{{AdminCustomerAddressesSection.defaultBillingAddress}}"/> + <click stepKey="thickShippingAddress" selector="{{AdminCustomerAddressesSection.defaultShippingAddress}}"/> + <fillField stepKey="fillFirstNameForAddress" selector="{{AdminCustomerAddressesSection.firstNameForAddress}}" userInput="{{address.firstname}}"/> + <fillField stepKey="fillLastNameForAddress" selector="{{AdminCustomerAddressesSection.lastNameForAddress}}" userInput="{{address.lastname}}"/> + <fillField stepKey="fillStreetAddress" selector="{{AdminCustomerAddressesSection.streetAddress}}" userInput="{{address.street[0]}}"/> + <fillField stepKey="fillCity" selector="{{AdminCustomerAddressesSection.city}}" userInput="{{address.city}}"/> + <selectOption stepKey="selectCountry" selector="{{AdminCustomerAddressesSection.country}}" userInput="{{address.country}}"/> + <selectOption stepKey="selectState" selector="{{AdminCustomerAddressesSection.state}}" userInput="{{address.state}}"/> + <fillField stepKey="fillZip" selector="{{AdminCustomerAddressesSection.zip}}" userInput="{{address.postcode}}"/> + <fillField stepKey="fillPhoneNumber" selector="{{AdminCustomerAddressesSection.phoneNumber}}" userInput="{{address.telephone}}"/> + <click stepKey="saveAddress" selector="{{AdminCustomerAddressesSection.saveAddress}}"/> + <waitForPageLoad stepKey="waitForAddressSave"/> + </actionGroup> + + <actionGroup name="AdminCreateCustomerWithWebSiteAndGroup"> + <arguments> + <argument name="customerData" defaultValue="Simple_US_Customer"/> + <argument name="website" type="string" defaultValue="customWebsite"/> + <argument name="storeView" type="string" defaultValue="customStore"/> + </arguments> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="goToCustomersPage"/> + <click stepKey="addNewCustomer" selector="{{AdminCustomerGridMainActionsSection.addNewCustomer}}"/> + <selectOption stepKey="selectWebSite" selector="{{AdminCustomerAccountInformationSection.associateToWebsite}}" userInput="{{website}}"/> + <click selector="{{AdminCustomerAccountInformationSection.group}}" stepKey="ClickToExpandGroup"/> + <waitForElement selector="{{AdminProductFormAdvancedPricingSection.productTierPriceGroupOrCatalogOption('Default (General)')}}" stepKey="waitForCustomerGroupExpand"/> + <click selector="{{AdminCustomerAccountInformationSection.groupValue('Default (General)')}}" after="waitForCustomerGroupExpand" stepKey="ClickToSelectGroup"/> + <fillField stepKey="FillFirstName" selector="{{AdminCustomerAccountInformationSection.firstName}}" userInput="{{customerData.firstname}}"/> + <fillField stepKey="FillLastName" selector="{{AdminCustomerAccountInformationSection.lastName}}" userInput="{{customerData.lastname}}"/> + <fillField stepKey="FillEmail" selector="{{AdminCustomerAccountInformationSection.email}}" userInput="{{customerData.email}}"/> + <selectOption stepKey="selectStoreView" selector="{{AdminCustomerAccountInformationSection.storeView}}" userInput="{{storeView}}"/> + <waitForElement selector="{{AdminCustomerAccountInformationSection.storeView}}" stepKey="waitForCustomerStoreViewExpand"/> + <click stepKey="save" selector="{{AdminCustomerAccountInformationSection.saveCustomer}}"/> + <waitForPageLoad stepKey="waitForCustomersPage"/> + <see stepKey="seeSuccessMessage" userInput="You saved the customer."/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerGridActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerGridActionGroup.xml new file mode 100644 index 000000000000..86039056999b --- /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 000000000000..03b950a6dbe6 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerSaveAndContinueActionGroup.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Save Customer and Assert Success Message --> + <actionGroup name="AdminCustomerSaveAndContinue" > + <click selector="{{AdminCustomerMainActionsSection.saveAndContinue}}" stepKey="saveAndContinue"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerShopingCartActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerShopingCartActionGroup.xml new file mode 100644 index 000000000000..f5d5682e374f --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerShopingCartActionGroup.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAddProductToShoppingCartActionGroup"> + <arguments> + <argument name="productName" type="string"/> + </arguments> + <waitForElementVisible selector="{{AdminCustomerShoppingCartProductItemSection.productItem}}" stepKey="waitForElementVisible"/> + <click selector="{{AdminCustomerShoppingCartProductItemSection.productItem}}" stepKey="expandProductItem"/> + <waitForElementVisible selector="{{AdminCustomerShoppingCartProductItemSection.productNameFilter}}" stepKey="waitForProductFilterFieldVisible"/> + <fillField selector="{{AdminCustomerShoppingCartProductItemSection.productNameFilter}}" stepKey="setProductName" userInput="{{productName}}"/> + <click selector="{{AdminCustomerShoppingCartProductItemSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForAjaxLoad stepKey="waitForAjax"/> + <waitForElementVisible selector="{{AdminCustomerShoppingCartProductItemSection.firstProductCheckbox}}" stepKey="waitForElementCheckboxVisible"/> + <click selector="{{AdminCustomerShoppingCartProductItemSection.firstProductCheckbox}}" stepKey="selectFirstCheckbox"/> + <click selector="{{AdminCustomerShoppingCartProductItemSection.addSelectionsToMyCartButton}}" stepKey="clickAddSelectionsToMyCartButton" after="selectFirstCheckbox"/> + <waitForAjaxLoad stepKey="waitForAjax2"/> + <seeElement stepKey="seeAddedProduct" selector="{{AdminCustomerShoppingCartProductItemSection.addedProductName('productName')}}"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminDeleteAddressInCustomersAddressGridActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminDeleteAddressInCustomersAddressGridActionGroup.xml new file mode 100644 index 000000000000..b61b353714e1 --- /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/AdminDeleteCustomerActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminDeleteCustomerActionGroup.xml new file mode 100644 index 000000000000..d08f10b22419 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminDeleteCustomerActionGroup.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="AdminDeleteCustomerActionGroup"> + <arguments> + <argument name="customerEmail"/> + </arguments> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomersPage"/> + <conditionalClick selector="{{AdminCustomerFiltersSection.clearAll}}" dependentSelector="{{AdminCustomerFiltersSection.clearAll}}" visible="true" stepKey="clickClearFilters"/> + <click stepKey="chooseCustomer" selector="{{AdminCustomerGridMainActionsSection.customerCheckbox(customerEmail)}}"/> + <click stepKey="openActions" selector="{{AdminCustomerGridMainActionsSection.actions}}"/> + <waitForPageLoad stepKey="waitActions"/> + <click stepKey="delete" selector="{{AdminCustomerGridMainActionsSection.delete}}"/> + <waitForPageLoad stepKey="waitForConfirmationAlert"/> + <click stepKey="accept" selector="{{AdminCustomerGridMainActionsSection.ok}}"/> + <see stepKey="seeSuccessMessage" userInput="were deleted."/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminDeleteCustomerGroupActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminDeleteCustomerGroupActionGroup.xml new file mode 100644 index 000000000000..788e5f8967f4 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminDeleteCustomerGroupActionGroup.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminDeleteCustomerGroupActionGroup"> + <arguments> + <argument name="customerGroupName" type="string"/> + </arguments> + <amOnPage url="{{AdminCustomerGroupsIndexPage.url}}" stepKey="goToAdminCustomerGroupIndexPage"/> + <waitForPageLoad time="30" stepKey="waitForCustomerGroupIndexPageLoad"/> + <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openFiltersSectionOnCustomerGroupIndexPage"/> + <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="{{AdminConfirmationModalSection.ok}}" stepKey="confirmDeleteCustomerGroup"/> + <seeElement selector="{{AdminMessagesSection.success}}" stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> 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 000000000000..954b83bead1d --- /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 000000000000..0c1af1cb5b67 --- /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 000000000000..9c38f23739b1 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminEditCustomerAddressesFromActionGroup.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See 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> + <actionGroup name="SelectDropdownCustomerAddressAttributeValueActionGroup"> + <arguments> + <argument name="customerAddressAttribute"/> + <argument name="optionValue" type="string"/> + </arguments> + <selectOption selector="{{AdminEditCustomerAddressesSection.dropDownAttribute(customerAddressAttribute.code)}}" userInput="{{optionValue}}" stepKey="selectOptionValue"/> + <click selector="{{AdminEditCustomerAddressesSection.save}}" stepKey="saveAddress"/> + <waitForPageLoad stepKey="waitForAddressSaved"/> + </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 000000000000..ddeefeb3c374 --- /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 000000000000..2d0d44a4cc52 --- /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 000000000000..c49a0dbe20ae --- /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 000000000000..9cab8a790ff5 --- /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/AdminFilterCustomerGroupByNameActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminFilterCustomerGroupByNameActionGroup.xml new file mode 100644 index 000000000000..1681a8e850ca --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminFilterCustomerGroupByNameActionGroup.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="AdminFilterCustomerGroupByNameActionGroup"> + <arguments> + <argument name="customerGroupName" type="string"/> + </arguments> + <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openFiltersSectionOnCustomerGroupIndexPage"/> + <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"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminOpenCustomerEditPageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminOpenCustomerEditPageActionGroup.xml new file mode 100644 index 000000000000..8e6b56b19d67 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminOpenCustomerEditPageActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenCustomerEditPageActionGroup"> + <arguments> + <argument name="customerId" type="string" /> + </arguments> + <amOnPage url="{{AdminEditCustomerPage.url(customerId)}}" stepKey="openCustomerEditPage" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminResetFilterInCustomerAddressGridActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminResetFilterInCustomerAddressGridActionGroup.xml new file mode 100644 index 000000000000..135f01078419 --- /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 000000000000..5c6ff347d565 --- /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 000000000000..d3907e96b0d7 --- /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 000000000000..1a8b4da67e74 --- /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 000000000000..bb84d578fd9e --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminSelectCustomerByEmailActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSelectCustomerByEmail"> + <arguments> + <argument name="customerEmail" type="string"/> + </arguments> + <checkOption selector="{{AdminCustomerGridSection.customerCheckboxByEmail(customerEmail)}}" stepKey="checkCustomerBox"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminUpdateCustomerGroupActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminUpdateCustomerGroupActionGroup.xml new file mode 100644 index 000000000000..b1b82fb9fb74 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminUpdateCustomerGroupActionGroup.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminUpdateCustomerGroupByEmailActionGroup"> + <arguments> + <argument name="emailAddress"/> + <argument name="customerGroup" type="string"/> + </arguments> + + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="goToCustomerPage01"/> + + <!-- Start of Action Group: searchAdminDataGridByKeyword --> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clickClearFilters0"/> + <fillField selector="{{AdminDataGridHeaderSection.search}}" userInput="{{emailAddress}}" stepKey="fillKeywordSearchField01"/> + <click selector="{{AdminDataGridHeaderSection.submitSearch}}" stepKey="clickKeywordSearch01"/> + <waitForPageLoad stepKey="waitForPageLoad02"/> + <!-- End of Action Group: searchAdminDataGridByKeyword --> + + <click selector="{{AdminGridRow.editByValue(emailAddress)}}" stepKey="clickOnCustomer01"/> + <waitForPageLoad stepKey="waitForPageLoad03"/> + + <conditionalClick selector="{{AdminCustomerAccountInformationSection.accountInformationTab}}" dependentSelector="{{AdminCustomerAccountInformationSection.accountInformationTab}}" visible="true" stepKey="clickOnAccountInformation01"/> + <waitForPageLoad stepKey="waitForPageLoad04"/> + + <click selector="{{AdminCustomerAccountInformationSection.group}}" stepKey="clickOnCustomerGroup01"/> + <selectOption selector="{{AdminCustomerAccountInformationSection.group}}" userInput="{{customerGroup}}" stepKey="selectCustomerGroup01"/> + + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickOnSave01"/> + <waitForPageLoad stepKey="waitForPageLoad05"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertAuthorizationPopUpPasswordAutoCompleteOffActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertAuthorizationPopUpPasswordAutoCompleteOffActionGroup.xml new file mode 100644 index 000000000000..186d0244e8c7 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertAuthorizationPopUpPasswordAutoCompleteOffActionGroup.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertAuthorizationPopUpPasswordAutoCompleteOffActionGroup"> + <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.password}}" stepKey="waitPasswordFieldVisible"/> + <assertElementContainsAttribute selector="{{StorefrontCustomerSignInPopupFormSection.password}}" attribute="autocomplete" expectedValue="off" stepKey="assertAuthorizationPopupPasswordAutocompleteOff"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerAccountPageTitleActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerAccountPageTitleActionGroup.xml new file mode 100644 index 000000000000..132b5ca81886 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerAccountPageTitleActionGroup.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertCustomerAccountPageTitleActionGroup"> + <arguments> + <argument name="pageTitle" type="string" /> + </arguments> + <see selector="{{StorefrontCustomerAccountMainSection.pageTitle}}" userInput="{{pageTitle}}" stepKey="assertPageTitle" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerGroupNotInGridActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerGroupNotInGridActionGroup.xml new file mode 100644 index 000000000000..26c4f23fc9a7 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerGroupNotInGridActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertCustomerGroupNotInGridActionGroup"> + <arguments> + <argument name="customerGroup" type="entity" /> + </arguments> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="cleanFilters"/> + <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openFiltersSectionOnCustomerGroupIndexPage"/> + <fillField userInput="{{customerGroup.code}}" selector="{{AdminDataGridHeaderSection.filterFieldInput('customer_group_code')}}" stepKey="fillNameFieldOnFiltersSection"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerGroupNotOnProductFormActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerGroupNotOnProductFormActionGroup.xml new file mode 100644 index 000000000000..94e01db5c1ff --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerGroupNotOnProductFormActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertCustomerGroupNotOnProductFormActionGroup"> + <arguments> + <argument name="customerGroup" type="entity" /> + </arguments> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingButton"/> + <waitForElementVisible selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="waitForCustomerGroupPriceAddButton"/> + <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="addCustomerGroupAllGroupsQty1PriceDiscountAnd10percent"/> + <grabMultiple selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelectOptions('0')}}" stepKey="customerGroups" /> + <assertNotContains stepKey="assertCustomerGroupNotInOptions"> + <actualResult type="variable">customerGroups</actualResult> + <expectedResult type="string">{{customerGroup.code}}</expectedResult> + </assertNotContains> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerGroupOnCustomerFormActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerGroupOnCustomerFormActionGroup.xml new file mode 100644 index 000000000000..2c8e0081e5e9 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerGroupOnCustomerFormActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertCustomerGroupOnCustomerFormActionGroup"> + <arguments> + <argument name="customerGroup" type="entity" /> + </arguments> + <click selector="{{AdminCustomerAccountInformationSection.accountInformationTab}}" stepKey="clickOnAccountInfoTab" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + <seeOptionIsSelected userInput="{{customerGroup.code}}" selector="{{AdminCustomerAccountInformationSection.group}}" stepKey="verifyNeededCustomerGroupSelected" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerLoggedInActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerLoggedInActionGroup.xml new file mode 100644 index 000000000000..d9da950fe711 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerLoggedInActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertCustomerWelcomeMessageActionGroup"> + <arguments> + <argument name="customerFullName" type="string" /> + </arguments> + <see userInput="Welcome, {{customerFullName}}!" selector="{{StorefrontPanelHeaderSection.welcomeMessage}}" stepKey="verifyMessage" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerResetPasswordActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerResetPasswordActionGroup.xml new file mode 100644 index 000000000000..644254443d12 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerResetPasswordActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertCustomerResetPasswordActionGroup"> + <arguments> + <argument name="url" type="string"/> + <argument name="message" type="string" defaultValue="" /> + <argument name="messageType" type="string" defaultValue="success" /> + </arguments> + + <waitForElementVisible selector="{{StorefrontCustomerLoginMessagesSection.messageByType(messageType)}}" stepKey="waitForMessage" /> + <see stepKey="seeMessage" userInput="{{message}}" selector="{{StorefrontCustomerLoginMessagesSection.messageByType(messageType)}}"/> + <seeInCurrentUrl stepKey="seeCorrectCurrentUrl" url="{{url}}"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertMessageCustomerChangeAccountInfoActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertMessageCustomerChangeAccountInfoActionGroup.xml new file mode 100644 index 000000000000..ab48184ed98a --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertMessageCustomerChangeAccountInfoActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertMessageCustomerChangeAccountInfoActionGroup"> + <arguments> + <argument name="message" type="string" defaultValue="You saved the account information." /> + <argument name="messageType" type="string" defaultValue="success" /> + </arguments> + <see userInput="{{message}}" selector="{{StorefrontCustomerAccountMainSection.messageByType(messageType)}}" stepKey="verifyMessage" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertMessageCustomerCreateAccountActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertMessageCustomerCreateAccountActionGroup.xml new file mode 100644 index 000000000000..65c9b025a9c2 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertMessageCustomerCreateAccountActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertMessageCustomerCreateAccountActionGroup"> + <arguments> + <argument name="message" type="string" defaultValue="Thank you for registering with Main Website Store." /> + <argument name="messageType" type="string" defaultValue="success" /> + </arguments> + <see userInput="{{message}}" selector="{{StorefrontCustomerAccountMainSection.messageByType(messageType)}}" stepKey="verifyMessage" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertMessageCustomerLoginActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertMessageCustomerLoginActionGroup.xml new file mode 100644 index 000000000000..6b8866198587 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertMessageCustomerLoginActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertMessageCustomerLoginActionGroup"> + <arguments> + <argument name="message" type="string" defaultValue="The account sign-in was incorrect or your account is disabled temporarily. Please wait and try again later." /> + <argument name="messageType" type="string" defaultValue="error" /> + </arguments> + <see userInput="{{message}}" selector="{{StorefrontCustomerLoginMessagesSection.messageByType(messageType)}}" stepKey="verifyMessage" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStorefrontPasswordAutocompleteOffActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStorefrontPasswordAutocompleteOffActionGroup.xml new file mode 100644 index 000000000000..23a067cd94ee --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStorefrontPasswordAutocompleteOffActionGroup.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStorefrontPasswordAutoCompleteOffActionGroup"> + <amOnPage stepKey="amOnSignInPage" url="{{StorefrontCustomerSignInPage.url}}"/> + <assertElementContainsAttribute selector="{{StorefrontCustomerSignInFormSection.passwordField}}" attribute="autocomplete" expectedValue="off" stepKey="assertSignInPasswordAutocompleteOff"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/CreateCustomerActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/CreateCustomerActionGroup.xml similarity index 93% rename from app/code/Magento/Braintree/Test/Mftf/ActionGroup/CreateCustomerActionGroup.xml rename to app/code/Magento/Customer/Test/Mftf/ActionGroup/CreateCustomerActionGroup.xml index a68042127ec4..047f656f5eab 100644 --- a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/CreateCustomerActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/CreateCustomerActionGroup.xml @@ -5,8 +5,9 @@ * See COPYING.txt for license details. */ --> + <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="CreateCustomerActionGroup"> <click stepKey="openCustomers" selector="{{AdminMenuSection.customers}}"/> <waitForAjaxLoad stepKey="waitForCatalogSubmenu" time="5"/> @@ -39,6 +40,5 @@ <click stepKey="save" selector="{{NewCustomerPageSection.saveCustomer}}"/> <waitForPageLoad stepKey="waitForCustomersPage" time="10"/> <waitForElementVisible selector="{{NewCustomerPageSection.createdSuccessMessage}}" stepKey="waitForSuccessfullyCreatedMessage" time="20"/> - </actionGroup> -</actionGroups> \ No newline at end of file +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/DeleteCustomerActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/DeleteCustomerActionGroup.xml new file mode 100644 index 000000000000..06659dae156a --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/DeleteCustomerActionGroup.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="DeleteCustomerActionGroup"> + <arguments> + <argument name="lastName" defaultValue=""/> + </arguments> + <!--Clear filter if exist--> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomers"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingCustomerFilters"/> + + <click stepKey="chooseCustomer" selector="{{CustomersPageSection.customerCheckbox(lastName)}}"/> + <waitForAjaxLoad stepKey="waitForThick" time="2"/> + <click stepKey="OpenActions" selector="{{CustomersPageSection.actions}}"/> + <waitForAjaxLoad stepKey="waitForDelete" time="5"/> + <click stepKey="ChooseDelete" selector="{{CustomersPageSection.delete}}"/> + <waitForPageLoad stepKey="waitForDeleteItemPopup" time="10"/> + <click stepKey="clickOnOk" selector="{{CustomersPageSection.ok}}"/> + <waitForElementVisible stepKey="waitForSuccessfullyDeletedMessage" selector="{{CustomersPageSection.deletedSuccessMessage}}" time="10"/> + </actionGroup> + <actionGroup name="DeleteCustomerByEmailActionGroup"> + <arguments> + <argument name="email" type="string"/> + </arguments> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomers"/> + <waitForPageLoad stepKey="waitForAdminCustomerPageLoad"/> + <click selector="{{AdminCustomerFiltersSection.filtersButton}}" stepKey="clickFilterButton"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="cleanFiltersIfTheySet"/> + <waitForPageLoad stepKey="waitForClearFilters"/> + <fillField selector="{{AdminCustomerFiltersSection.emailInput}}" userInput="{{email}}" stepKey="filterEmail"/> + <click selector="{{AdminCustomerFiltersSection.apply}}" stepKey="applyFilter"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{AdminCustomerGridSection.selectFirstRow}}" stepKey="clickOnEditButton1"/> + <click selector="{{CustomersPageSection.actions}}" stepKey="clickActionsDropdown"/> + <click selector="{{CustomersPageSection.delete}}" stepKey="clickDelete"/> + <waitForElementVisible selector="{{CustomersPageSection.ok}}" stepKey="waitForOkToVisible"/> + <click selector="{{CustomersPageSection.ok}}" stepKey="clickOkConfirmationButton"/> + <waitForElementVisible stepKey="waitForSuccessfullyDeletedMessage" selector="{{CustomersPageSection.deletedSuccessMessage}}" time="30"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/EditCustomerAddressesFromAdminActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/EditCustomerAddressesFromAdminActionGroup.xml new file mode 100644 index 000000000000..617c895bc120 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/EditCustomerAddressesFromAdminActionGroup.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="EditCustomerAddressesFromAdminActionGroup" > + <arguments> + <argument name="customerAddress"/> + </arguments> + <click selector="{{AdminEditCustomerAddressesSection.addresses}}" stepKey="proceedToAddresses"/> + <click selector="{{AdminEditCustomerAddressesSection.addNewAddress}}" stepKey="addNewAddresses"/> + <waitForPageLoad time="60" stepKey="wait5678" /> + <fillField stepKey="fillPrefixName" userInput="{{customerAddress.prefix}}" selector="{{AdminEditCustomerAddressesSection.prefixName}}"/> + <fillField stepKey="fillMiddleName" userInput="{{customerAddress.middlename}}" selector="{{AdminEditCustomerAddressesSection.middleName}}"/> + <fillField stepKey="fillSuffixName" userInput="{{customerAddress.suffix}}" selector="{{AdminEditCustomerAddressesSection.suffixName}}"/> + <fillField stepKey="fillCompany" userInput="{{customerAddress.company}}" selector="{{AdminEditCustomerAddressesSection.company}}"/> + <fillField stepKey="fillStreetAddress" userInput="{{customerAddress.street}}" selector="{{AdminEditCustomerAddressesSection.streetAddress}}"/> + <fillField stepKey="fillCity" userInput="{{customerAddress.city}}" selector="{{AdminEditCustomerAddressesSection.city}}"/> + <selectOption stepKey="selectCountry" selector="{{AdminEditCustomerAddressesSection.country}}" userInput="{{US_Address_CA.country_id}}"/> + <selectOption stepKey="selectState" selector="{{AdminEditCustomerAddressesSection.state}}" userInput="{{US_Address_CA.state}}"/> + <fillField stepKey="fillZipCode" userInput="{{customerAddress.postcode}}" selector="{{AdminEditCustomerAddressesSection.zipCode}}"/> + <fillField stepKey="fillPhone" userInput="{{customerAddress.telephone}}" selector="{{AdminEditCustomerAddressesSection.phone}}"/> + <fillField stepKey="fillVAT" userInput="{{customerAddress.vat_id}}" selector="{{AdminEditCustomerAddressesSection.vat}}"/> + <click selector="{{AdminEditCustomerAddressesSection.save}}" stepKey="saveAddress"/> + <waitForPageLoad stepKey="waitForAddressSaved"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/LoginToStorefrontActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/LoginToStorefrontActionGroup.xml index 7be36ffbd9bc..703b9f542f81 100644 --- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/LoginToStorefrontActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/LoginToStorefrontActionGroup.xml @@ -11,9 +11,12 @@ <arguments> <argument name="Customer"/> </arguments> - <amOnPage stepKey="amOnSignInPage" url="{{StorefrontCustomerSignInPage.url}}"/> - <fillField stepKey="fillEmail" userInput="{{Customer.email}}" selector="{{StorefrontCustomerSignInFormSection.emailField}}"/> - <fillField stepKey="fillPassword" userInput="{{Customer.password}}" selector="{{StorefrontCustomerSignInFormSection.passwordField}}"/> - <click stepKey="clickSignInAccountButton" selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}"/> + <amOnPage url="{{StorefrontCustomerSignInPage.url}}" stepKey="amOnSignInPage"/> + <waitForPageLoad time="30" stepKey="waitPageFullyLoaded"/> + <waitForElementVisible selector="{{StorefrontCustomerSignInFormSection.emailField}}" stepKey="waitFormAppears"/> + <fillField userInput="{{Customer.email}}" selector="{{StorefrontCustomerSignInFormSection.emailField}}" stepKey="fillEmail"/> + <fillField userInput="{{Customer.password}}" selector="{{StorefrontCustomerSignInFormSection.passwordField}}" stepKey="fillPassword"/> + <click selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}" stepKey="clickSignInAccountButton"/> + <waitForPageLoad stepKey="waitForCustomerLoggedIn" /> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/LoginToStorefrontWithEmailAndPasswordActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/LoginToStorefrontWithEmailAndPasswordActionGroup.xml new file mode 100644 index 000000000000..071450001051 --- /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 000000000000..be639d245f02 --- /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 000000000000..076797f34910 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/NavigateCustomerGroupActionGroup.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="NavigateToCustomerGroupPage"> + <amOnPage url="{{AdminCustomerGroupPage.url}}" stepKey="openCustomersGridPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/NavigateThroughCustomerTabsActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/NavigateThroughCustomerTabsActionGroup.xml new file mode 100644 index 000000000000..5591bee52969 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/NavigateThroughCustomerTabsActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="NavigateThroughCustomerTabsActionGroup"> + <arguments> + <argument name="navigationItemName" type="string" /> + </arguments> + <click selector="{{StorefrontCustomerSidebarSection.sidebarTab(navigationItemName)}}" stepKey="clickOnDesiredNavItem" /> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenEditCustomerFromAdminActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenEditCustomerFromAdminActionGroup.xml old mode 100644 new mode 100755 index 639c64b773a2..208f4f51e38e --- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenEditCustomerFromAdminActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenEditCustomerFromAdminActionGroup.xml @@ -12,12 +12,51 @@ <argument name="customer"/> </arguments> <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomers"/> - <waitForPageLoad stepKey="waitForPageLoad1" /> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingOrderFilters"/> <click selector="{{AdminCustomerFiltersSection.filtersButton}}" stepKey="openFilter"/> <fillField userInput="{{customer.email}}" selector="{{AdminCustomerFiltersSection.emailInput}}" stepKey="filterEmail"/> <click selector="{{AdminCustomerFiltersSection.apply}}" stepKey="applyFilter"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickEdit"/> - <waitForPageLoad stepKey="waitForPageLoad2" /> + <waitForPageLoad stepKey="waitForPageLoad3"/> + </actionGroup> + <actionGroup name="OpenEditCustomerAddressFromAdminActionGroup"> + <arguments> + <argument name="address"/> + </arguments> + <click selector="{{AdminCustomerAccountInformationSection.addressesButton}}" stepKey="openAddressesTab"/> + <waitForElementVisible selector="{{AdminCustomerAddressFiltersSection.filtersButton}}" stepKey="waitForComponentLoad"/> + <click selector="{{AdminCustomerAddressFiltersSection.filtersButton}}" stepKey="openAddressesFilter"/> + <fillField userInput="{{address.firstname}}" selector="{{AdminCustomerAddressFiltersSection.firstnameInput}}" stepKey="fillFirstname"/> + <fillField userInput="{{address.lastname}}" selector="{{AdminCustomerAddressFiltersSection.lastnameInput}}" stepKey="fillLastname"/> + <fillField userInput="{{address.telephone}}" selector="{{AdminCustomerAddressFiltersSection.telephoneInput}}" stepKey="fillCountry"/> + <fillField userInput="{{address.postcode}}" selector="{{AdminCustomerAddressFiltersSection.postcodeInput}}" stepKey="fillPostcode"/> + <click selector="{{AdminCustomerAddressFiltersSection.applyFilter}}" stepKey="applyAddressesFilter"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + <click selector="{{AdminCustomerAddressGridSection.firstRowSelectActionLink}}" stepKey="clickAction"/> + <click selector="{{AdminCustomerAddressGridSection.firstRowEditActionLink}}" stepKey="clickEdit"/> + <waitForPageLoad stepKey="waitForModalWindow" /> + </actionGroup> + <actionGroup name="DeleteCustomerFromAdminActionGroup"> + <arguments> + <argument name="customer" defaultValue="CustomerEntityOne"/> + </arguments> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomers"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clickOnButtonToRemoveFiltersIfPresent"/> + <fillField selector="{{AdminDataGridHeaderSection.search}}" userInput="{{customer.email}}" stepKey="fillSearch"/> + <click selector="{{AdminDataGridHeaderSection.submitSearch}}" stepKey="clickSubmit"/> + <waitForAjaxLoad stepKey="waitForLoadAjax"/> + <click selector="{{AdminCustomerGridMainActionsSection.multicheck}}" stepKey="selectAll"/> + <click selector="{{AdminCustomerGridMainActionsSection.actions}}" stepKey="clickActions"/> + <click selector="{{AdminCustomerGridMainActionsSection.delete}}" stepKey="clickDelete"/> + <waitForAjaxLoad stepKey="waitForLoadConfirmation"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmDelete"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="A total of 1 record(s) were deleted" stepKey="seeSuccess"/> + </actionGroup> + <actionGroup name="AdminClearCustomersFiltersActionGroup"> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="amOnCustomersPage"/> + <waitForPageLoad stepKey="WaitForPageToLoad"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clickOnButtonToRemoveFiltersIfPresent"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenMyAccountPageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenMyAccountPageActionGroup.xml new file mode 100644 index 000000000000..6ca0f612deea --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenMyAccountPageActionGroup.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="OpenMyAccountPageActionGroup"> + <click selector="{{LoggedInCustomerHeaderLinksSection.customerDropdownMenu}}" stepKey="openCustomerDropdownMenu"/> + <click selector="{{LoggedInCustomerHeaderLinksSection.myAccount}}" stepKey="clickOnMyAccount"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/SetGroupCustomerActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/SetGroupCustomerActionGroup.xml new file mode 100644 index 000000000000..ca5e16c4ddb4 --- /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 79cc00d4474b..ef956293d367 100644 --- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/SignUpNewUserFromStorefrontActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/SignUpNewUserFromStorefrontActionGroup.xml @@ -9,10 +9,9 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="SignUpNewUserFromStorefrontActionGroup"> <arguments> - <argument name="Customer"/> + <argument name="Customer" defaultValue="CustomerEntityOne"/> </arguments> - <amOnPage stepKey="amOnStorefrontPage" url="/"/> - <waitForPageLoad stepKey="waitForStorefrontPage"/> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnStorefrontPage"/> <click stepKey="clickOnCreateAccountLink" selector="{{StorefrontPanelHeaderSection.createAnAccountLink}}"/> <fillField stepKey="fillFirstName" userInput="{{Customer.firstname}}" selector="{{StorefrontCustomerCreateFormSection.firstnameField}}"/> <fillField stepKey="fillLastName" userInput="{{Customer.lastname}}" selector="{{StorefrontCustomerCreateFormSection.lastnameField}}"/> @@ -25,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"/> @@ -40,10 +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> -</actionGroups> + <!-- 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"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAddCustomerAddressActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAddCustomerAddressActionGroup.xml new file mode 100644 index 000000000000..a45fcf31f7b3 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAddCustomerAddressActionGroup.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAddNewCustomerAddressActionGroup"> + <amOnPage url="customer/address/new/" stepKey="OpenCustomerAddNewAddress"/> + <arguments> + <argument name="Address"/> + </arguments> + <fillField stepKey="fillFirstName" userInput="{{Address.firstname}}" selector="{{StorefrontCustomerAddressFormSection.firstName}}"/> + <fillField stepKey="fillLastName" userInput="{{Address.lastname}}" selector="{{StorefrontCustomerAddressFormSection.lastName}}"/> + <fillField stepKey="fillCompanyName" userInput="{{Address.company}}" selector="{{StorefrontCustomerAddressFormSection.company}}"/> + <fillField stepKey="fillPhoneNumber" userInput="{{Address.telephone}}" selector="{{StorefrontCustomerAddressFormSection.phoneNumber}}"/> + <fillField stepKey="fillStreetAddress" userInput="{{Address.street[0]}}" selector="{{StorefrontCustomerAddressFormSection.streetAddress}}"/> + <fillField stepKey="fillCity" userInput="{{Address.city}}" selector="{{StorefrontCustomerAddressFormSection.city}}"/> + <selectOption stepKey="selectState" userInput="{{Address.state}}" selector="{{StorefrontCustomerAddressFormSection.state}}"/> + <fillField stepKey="fillZip" userInput="{{Address.postcode}}" selector="{{StorefrontCustomerAddressFormSection.zip}}"/> + <selectOption stepKey="selectCountry" userInput="{{Address.country}}" selector="{{StorefrontCustomerAddressFormSection.country}}"/> + <click stepKey="saveCustomerAddress" selector="{{StorefrontCustomerAddressFormSection.saveAddress}}"/> + <see userInput="You saved the address." stepKey="verifyAddressAdded"/> + </actionGroup> + <actionGroup name="StorefrontAddCustomerDefaultAddressActionGroup"> + <amOnPage url="customer/address/new/" stepKey="OpenCustomerAddNewAddress"/> + <arguments> + <argument name="Address"/> + </arguments> + <fillField stepKey="fillFirstName" userInput="{{Address.firstname}}" selector="{{StorefrontCustomerAddressFormSection.firstName}}"/> + <fillField stepKey="fillLastName" userInput="{{Address.lastname}}" selector="{{StorefrontCustomerAddressFormSection.lastName}}"/> + <fillField stepKey="fillCompanyName" userInput="{{Address.company}}" selector="{{StorefrontCustomerAddressFormSection.company}}"/> + <fillField stepKey="fillPhoneNumber" userInput="{{Address.telephone}}" selector="{{StorefrontCustomerAddressFormSection.phoneNumber}}"/> + <fillField stepKey="fillStreetAddress" userInput="{{Address.street[0]}}" selector="{{StorefrontCustomerAddressFormSection.streetAddress}}"/> + <fillField stepKey="fillCity" userInput="{{Address.city}}" selector="{{StorefrontCustomerAddressFormSection.city}}"/> + <selectOption stepKey="selectState" userInput="{{Address.state}}" selector="{{StorefrontCustomerAddressFormSection.state}}"/> + <fillField stepKey="fillZip" userInput="{{Address.postcode}}" selector="{{StorefrontCustomerAddressFormSection.zip}}"/> + <selectOption stepKey="selectCountry" userInput="{{Address.country}}" selector="{{StorefrontCustomerAddressFormSection.country}}"/> + <click stepKey="checkUseAsDefaultBillingAddressCheckBox" selector="{{StorefrontCustomerAddressFormSection.useAsDefaultBillingAddressCheckBox}}"/> + <click stepKey="checkUseAsDefaultShippingAddressCheckBox" selector="{{StorefrontCustomerAddressFormSection.useAsDefaultShippingAddressCheckBox}}"/> + <click stepKey="saveCustomerAddress" selector="{{StorefrontCustomerAddressFormSection.saveAddress}}"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see userInput="You saved the address." stepKey="verifyAddressAdded"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertSuccessLoginToStorefrontActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertSuccessLoginToStorefrontActionGroup.xml new file mode 100644 index 000000000000..475702ad6922 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertSuccessLoginToStorefrontActionGroup.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAssertSuccessLoginToStorefront" extends="LoginToStorefrontActionGroup"> + <arguments> + <argument name="Customer" type="entity"/> + </arguments> + <see stepKey="assertWelcome" userInput="{{Customer.firstname}}" selector="{{StorefrontPanelHeaderSection.customerWelcome}}" after="clickSignInAccountButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontClickCreateAnAccountCustomerAccountCreationFormActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontClickCreateAnAccountCustomerAccountCreationFormActionGroup.xml new file mode 100644 index 000000000000..dfb9d1b2c259 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontClickCreateAnAccountCustomerAccountCreationFormActionGroup.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontClickCreateAnAccountCustomerAccountCreationFormActionGroup"> + <click stepKey="clickCreateAccountButton" selector="{{StorefrontCustomerCreateFormSection.createAccountButton}}" /> + <waitForPageLoad stepKey="waitForCustomerSaved" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontClickSignInButtonActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontClickSignInButtonActionGroup.xml new file mode 100644 index 000000000000..b12858fc1037 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontClickSignInButtonActionGroup.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontClickSignInButtonActionGroup"> + <click stepKey="signIn" selector="{{StorefrontPanelHeaderSection.customerLoginLink}}" /> + <waitForPageLoad stepKey="waitForStorefrontSignInPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontClickSignOnCustomerLoginFormActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontClickSignOnCustomerLoginFormActionGroup.xml new file mode 100644 index 000000000000..9cd52b841fca --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontClickSignOnCustomerLoginFormActionGroup.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontClickSignOnCustomerLoginFormActionGroup"> + <click stepKey="clickSignInButton" selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}" /> + <waitForPageLoad stepKey="waitForCustomerSignIn" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerActionGroup.xml new file mode 100644 index 000000000000..c3b92b1af7f8 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CustomerLogoutStorefrontByMenuItemsActionGroup"> + <conditionalClick selector="{{StorefrontPanelHeaderSection.customerWelcomeMenu}}" + dependentSelector="{{StorefrontPanelHeaderSection.customerLogoutLink}}" + visible="false" + stepKey="clickHeaderCustomerMenuButton" /> + <click selector="{{StorefrontPanelHeaderSection.customerLogoutLink}}" stepKey="clickSignOutButton" /> + </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 000000000000..8385dc17ecf9 --- /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 000000000000..afef2d9a04e3 --- /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 000000000000..febc482d62e8 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerAddressBookNumberOfAddressesActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Go to Address Book --> + <actionGroup name="StorefrontCustomerAddressBookNumberOfAddresses"> + <arguments> + <argument name="number" type="string"/> + </arguments> + <see userInput="{{number}} Item" selector="{{StorefrontCustomerAddressesSection.numberOfAddresses}}" stepKey="checkNumberOfAddresses"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerChangeEmailActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerChangeEmailActionGroup.xml new file mode 100644 index 000000000000..844d13aa1fe4 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerChangeEmailActionGroup.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontCustomerChangeEmailActionGroup"> + <arguments> + <argument name="email" type="string" /> + <argument name="password" type="string" /> + </arguments> + <conditionalClick selector="{{StorefrontCustomerSidebarSection.sidebarTab('Account Information')}}" dependentSelector="{{StorefrontCustomerSidebarSection.sidebarTab('Account Information')}}" visible="true" stepKey="openAccountInfoTab" /> + <waitForPageLoad stepKey="waitForAccountInfoTabOpened" /> + <checkOption selector="{{StorefrontCustomerAccountInformationSection.changeEmail}}" stepKey="clickChangeEmailCheckbox" /> + <fillField selector="{{StorefrontCustomerAccountInformationSection.email}}" userInput="{{email}}" stepKey="fillEmail" /> + <fillField selector="{{StorefrontCustomerAccountInformationSection.currentPassword}}" userInput="{{password}}" stepKey="fillCurrentPassword" /> + <click selector="{{StorefrontCustomerAccountInformationSection.saveButton}}" stepKey="saveChange" /> + <waitForPageLoad stepKey="waitForPageLoaded" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerGoToSidebarMenuActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerGoToSidebarMenuActionGroup.xml new file mode 100644 index 000000000000..84d2f353b51d --- /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/StorefrontCustomerLogoutActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerLogoutActionGroup.xml new file mode 100644 index 000000000000..de97bb47de79 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerLogoutActionGroup.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="StorefrontCustomerLogoutActionGroup"> + <amOnPage url="{{StorefrontCustomerLogoutPage.url}}" stepKey="storefrontSignOut"/> + </actionGroup> + + <actionGroup name="StorefrontSignOutActionGroup"> + <click selector="{{StoreFrontSignOutSection.customerAccount}}" stepKey="clickCustomerButton"/> + <click selector="{{StoreFrontSignOutSection.signOut}}" stepKey="clickToSignOut"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see userInput="You are signed out" stepKey="signOut"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerResetPasswordActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerResetPasswordActionGroup.xml new file mode 100644 index 000000000000..a28593f1b77b --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerResetPasswordActionGroup.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontCustomerResetPasswordActionGroup"> + <arguments> + <argument name="email" type="string" /> + </arguments> + + <amOnPage stepKey="amOnSignInPage" url="{{StorefrontCustomerSignInPage.url}}"/> + <click stepKey="clickForgotPasswordLink" selector="{{StorefrontCustomerSignInFormSection.forgotPasswordLink}}"/> + <see stepKey="seePageTitle" userInput="Forgot Your Password" selector="{{StorefrontForgotPasswordSection.pageTitle}}"/> + <!-- Enter email and submit the forgot password form --> + <fillField stepKey="fillEmailField" userInput="{{email}}" selector="{{StorefrontForgotPasswordSection.email}}"/> + <click stepKey="clickResetPassword" selector="{{StorefrontForgotPasswordSection.resetMyPasswordButton}}"/> + <waitForPageLoad stepKey="waitForPageLoaded" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillCustomerAccountCreationFormActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillCustomerAccountCreationFormActionGroup.xml new file mode 100644 index 000000000000..62d77d4548cc --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillCustomerAccountCreationFormActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontFillCustomerAccountCreationFormActionGroup"> + <arguments> + <argument name="customer" type="entity" /> + </arguments> + + <fillField stepKey="fillFirstName" userInput="{{customer.firstname}}" selector="{{StorefrontCustomerCreateFormSection.firstnameField}}" /> + <fillField stepKey="fillLastName" userInput="{{customer.lastname}}" selector="{{StorefrontCustomerCreateFormSection.lastnameField}}" /> + <fillField stepKey="fillEmail" userInput="{{customer.email}}" selector="{{StorefrontCustomerCreateFormSection.emailField}}"/> + <fillField stepKey="fillPassword" userInput="{{customer.password}}" selector="{{StorefrontCustomerCreateFormSection.passwordField}}"/> + <fillField stepKey="fillConfirmPassword" userInput="{{customer.password}}" selector="{{StorefrontCustomerCreateFormSection.confirmPasswordField}}"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillCustomerLoginFormActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillCustomerLoginFormActionGroup.xml new file mode 100644 index 000000000000..22883ada7c2b --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillCustomerLoginFormActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontFillCustomerLoginFormActionGroup"> + <arguments> + <argument name="customer" type="entity" /> + </arguments> + + <fillField userInput="{{customer.email}}" selector="{{StorefrontCustomerSignInFormSection.emailField}}" stepKey="fillEmail"/> + <fillField userInput="{{customer.password}}" selector="{{StorefrontCustomerSignInFormSection.passwordField}}" stepKey="fillPassword"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerAccountCreatePageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerAccountCreatePageActionGroup.xml new file mode 100644 index 000000000000..0ae470d0497a --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerAccountCreatePageActionGroup.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontOpenCustomerAccountCreatePageActionGroup"> + <amOnPage url="{{StorefrontCustomerCreatePage.url}}" stepKey="goToCustomerAccountCreatePage"/> + <waitForPageLoad stepKey="waitForPageLoaded"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerAccountInfoEditPageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerAccountInfoEditPageActionGroup.xml new file mode 100644 index 000000000000..c1ea2da8a951 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerAccountInfoEditPageActionGroup.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontOpenCustomerAccountInfoEditPageActionGroup"> + <amOnPage url="{{StorefrontCustomerEditPage.url}}" stepKey="goToCustomerEditPage"/> + <waitForPageLoad stepKey="waitForEditPage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerLoginPageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerLoginPageActionGroup.xml new file mode 100644 index 000000000000..0a5c72265528 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerLoginPageActionGroup.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontOpenCustomerLoginPageActionGroup"> + <amOnPage url="{{StorefrontCustomerSignInPage.url}}" stepKey="amOnSignInPage"/> + <waitForPageLoad stepKey="waitForPageLoaded"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/SwitchAccountActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/SwitchAccountActionGroup.xml similarity index 84% rename from app/code/Magento/Braintree/Test/Mftf/ActionGroup/SwitchAccountActionGroup.xml rename to app/code/Magento/Customer/Test/Mftf/ActionGroup/SwitchAccountActionGroup.xml index 7c774a634b36..4c59edbcb805 100644 --- a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/SwitchAccountActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/SwitchAccountActionGroup.xml @@ -5,9 +5,9 @@ * See COPYING.txt for license details. */ --> -<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <!--Sign out--> <actionGroup name="SignOut"> <click selector="{{SignOutSection.admin}}" stepKey="clickToAdminProfile"/> @@ -24,5 +24,4 @@ <fillField userInput="{{NewAdmin.password}}" selector="{{LoginFormSection.password}}" stepKey="fillPassword"/> <click selector="{{LoginFormSection.signIn}}" stepKey="clickLogin"/> </actionGroup> - -</actionGroups> \ No newline at end of file +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/VerifyGroupCustomerActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/VerifyGroupCustomerActionGroup.xml new file mode 100644 index 000000000000..712d3a59a214 --- /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 old mode 100644 new mode 100755 index 6ed75861fd42..5d1a90016714 --- a/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml +++ b/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml @@ -13,7 +13,7 @@ <data key="customer_id">12</data> <requiredEntity type="region">CustomerRegionOne</requiredEntity> <data key="region_id">0</data> - <data key="country_id">USA</data> + <data key="country_id">US</data> <array key="street"> <item>7700 W Parmer Ln</item> <item>Bld D</item> @@ -32,6 +32,7 @@ <data key="vat_id">vatData</data> <data key="default_shipping">true</data> <data key="default_billing">true</data> + <data key="region_qty">66</data> </entity> <entity name="US_Address_TX" type="address"> <data key="firstname">John</data> @@ -43,12 +44,29 @@ <data key="city">Austin</data> <data key="state">Texas</data> <data key="country_id">US</data> + <data key="country">United States</data> <data key="postcode">78729</data> <data key="telephone">512-345-6789</data> <data key="default_billing">Yes</data> <data key="default_shipping">Yes</data> <requiredEntity type="region">RegionTX</requiredEntity> </entity> + <entity name="US_Address_TX_Default_Billing" type="address"> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="company">Magento</data> + <array key="street"> + <item>7700 West Parmer Lane</item> + </array> + <data key="city">Austin</data> + <data key="state">Texas</data> + <data key="country_id">US</data> + <data key="country">United States</data> + <data key="postcode">78729</data> + <data key="telephone">512-345-6789</data> + <data key="default_billing">Yes</data> + <requiredEntity type="region">RegionTX</requiredEntity> + </entity> <entity name="US_Address_NY" type="address"> <data key="firstname">John</data> <data key="lastname">Doe</data> @@ -65,6 +83,40 @@ <data key="default_billing">Yes</data> <data key="default_shipping">Yes</data> <requiredEntity type="region">RegionNY</requiredEntity> + <data key="country">United States</data> + </entity> + <entity name="US_Address_NY_Default_Shipping" type="address"> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="company">368</data> + <array key="street"> + <item>368 Broadway St.</item> + <item>113</item> + </array> + <data key="city">New York</data> + <data key="state">New York</data> + <data key="country_id">US</data> + <data key="postcode">10001</data> + <data key="telephone">512-345-6789</data> + <data key="default_shipping">Yes</data> + <requiredEntity type="region">RegionNY</requiredEntity> + <data key="country">United States</data> + </entity> + <entity name="US_Address_NY_Not_Default_Address" type="address"> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="company">368</data> + <array key="street"> + <item>368 Broadway St.</item> + <item>Apt. 113</item> + </array> + <data key="city">New York</data> + <data key="state">New York</data> + <data key="country_id">US</data> + <data key="postcode">10001</data> + <data key="telephone">512-345-6789</data> + <requiredEntity type="region">RegionNY</requiredEntity> + <data key="country">United States</data> </entity> <entity name="US_Address_CA" type="address"> <data key="firstname">John</data> @@ -77,6 +129,7 @@ <data key="city">Los Angeles</data> <data key="state">California</data> <data key="country_id">US</data> + <data key="country">United States</data> <data key="postcode">90001</data> <data key="telephone">512-345-6789</data> <data key="default_billing">Yes</data> @@ -106,4 +159,116 @@ <data key="country_id">GB</data> <data key="telephone">444-44-444-44</data> </entity> + <entity name="US_Address_Utah" type="address"> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="company">Magento</data> + <array key="street"> + <item>1234 Some Utah address</item> + </array> + <data key="city">Provo</data> + <data key="state">Utah</data> + <data key="country_id">US</data> + <data key="country">United States</data> + <data key="postcode">84001</data> + <data key="telephone">512-345-6789</data> + <data key="default_billing">Yes</data> + <data key="default_shipping">Yes</data> + <requiredEntity type="region">RegionUT</requiredEntity> + </entity> + <entity name="UK_Simple_Address" extends="UK_Not_Default_Address"> + <array key="street"> + <item>172, Westminster Bridge Rd</item> + <item>7700 xyz street</item> + </array> + <data key="state">California</data> + </entity> + <entity name="US_Default_Billing_Address_TX" type="address" extends="US_Address_TX"> + <data key="default_billing">false</data> + <data key="default_shipping">true</data> + </entity> + <entity name="US_Default_Shipping_Address_CA" type="address" extends="US_Address_CA"> + <data key="default_billing">true</data> + <data key="default_shipping">false</data> + </entity> + <entity name="addressNoZipNoState" type="address"> + <data key="country_id">United Kingdom</data> + <array key="street"> + <item>3962 Horner Street</item> + </array> + <data key="company">Magento</data> + <data key="telephone">334-200-4061</data> + <data key="city">London</data> + <data key="firstname">Fn</data> + <data key="lastname">Ln</data> + <data key="middlename">Mn</data> + <data key="prefix">Mr</data> + <data key="suffix">Sr</data> + <data key="vat_id">U1234567891</data> + <data key="default_shipping">true</data> + <data key="default_billing">true</data> + </entity> + <entity name="updateCustomerUKAddress" type="address"> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="company">Magento</data> + <data key="telephone">0123456789-02134567</data> + <array key="street"> + <item>172, Westminster Bridge Rd</item> + <item>7700 xyz street</item> + </array> + <data key="country_id">GB</data> + <data key="country">United Kingdom</data> + <data key="city">London</data> + <!-- State not required for UK address on frontend--> + <data key="state"> </data> + <data key="postcode">12345</data> + </entity> + <entity name="updateCustomerFranceAddress" type="address"> + <data key="firstname">Jaen</data> + <data key="lastname">Reno</data> + <data key="company">Magento</data> + <data key="telephone">555-888-111-999</data> + <array key="street"> + <item>18-20 Rue Maréchal Lecler</item> + <item>18-20 Rue Maréchal Lecler</item> + </array> + <data key="country_id">FR</data> + <data key="country">France</data> + <data key="city">Quintin</data> + <data key="state">Côtes-d'Armor</data> + <data key="postcode">12345</data> + </entity> + <entity name="updateCustomerNoXSSInjection" type="address"> + <data key="firstname">Jany</data> + <data key="lastname">Doe</data> + <data key="company">Magento</data> + <data key="telephone">555-888-111-999</data> + <array key="street"> + <item>7700 West Parmer Lane</item> + <item>7700 West Parmer Lane</item> + </array> + <data key="country_id">US</data> + <data key="country">United States</data> + <data key="city">Denver</data> + <data key="state">Colorado</data> + <data key="postcode">12345</data> + </entity> + <entity name="PolandAddress" type="address"> + <data key="firstname">Mag</data> + <data key="lastname">Ento</data> + <data key="company">Magento</data> + <array key="street"> + <item>Piwowarska 6</item> + </array> + <data key="city">Bielsko-Biała</data> + <data key="state"> Bielsko</data> + <data key="country_id">PL</data> + <data key="country">Poland</data> + <data key="postcode">43-310</data> + <data key="telephone">799885616</data> + <data key="default_billing">Yes</data> + <data key="default_shipping">Yes</data> + <requiredEntity type="region">RegionUT</requiredEntity> + </entity> </entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Customer/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 000000000000..4e433c76b3dd --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminMenuCustomers"> + <data key="pageTitle">Customers</data> + <data key="title">Customers</data> + <data key="dataUiId">magento-customer-customer</data> + </entity> + <entity name="AdminMenuCustomersAllCustomers"> + <data key="pageTitle">Customers</data> + <data key="title">All Customers</data> + <data key="dataUiId">magento-customer-customer-manage</data> + </entity> + <entity name="AdminMenuCustomersCustomerGroups"> + <data key="pageTitle">Customer Groups</data> + <data key="title">Customer Groups</data> + <data key="dataUiId">magento-customer-customer-group</data> + </entity> + <entity name="AdminMenuCustomersNowOnline"> + <data key="pageTitle">Customers Now Online</data> + <data key="title">Now Online</data> + <data key="dataUiId">magento-customer-customer-online</data> + </entity> +</entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/CustomerConfigData.xml b/app/code/Magento/Customer/Test/Mftf/Data/CustomerConfigData.xml new file mode 100644 index 000000000000..11a47459ab7b --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Data/CustomerConfigData.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="CustomerAccountSharingDefault" type="customer_account_sharing_config"> + <requiredEntity type="account_share_scope_value">CustomerAccountSharingPerWebsite</requiredEntity> + </entity> + <entity name="CustomerAccountSharingPerWebsite" type="account_share_scope_value"> + <data key="value">1</data> + </entity> + + <entity name="CustomerAccountSharingGlobal" type="customer_account_sharing_config"> + <requiredEntity type="account_share_scope_value">GlobalCustomerAccountSharing</requiredEntity> + </entity> + <entity name="GlobalCustomerAccountSharing" type="account_share_scope_value"> + <data key="value">0</data> + </entity> + + <entity name="CustomerAccountSharingSystemValue" type="customer_account_sharing_config_inherit"> + <requiredEntity type="account_share_scope_inherit">CustomerAccountSharingInherit</requiredEntity> + </entity> + <entity name="CustomerAccountSharingInherit" type="account_share_scope_inherit"> + <data key="inherit">true</data> + </entity> +</entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/CustomerConfigurationData.xml b/app/code/Magento/Customer/Test/Mftf/Data/CustomerConfigurationData.xml new file mode 100644 index 000000000000..60d8b13887ec --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Data/CustomerConfigurationData.xml @@ -0,0 +1,40 @@ +<?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="SetCustomerCreateNewAccountOptionsConfig" type="customer_create_new_account_config"> + <requiredEntity type="auto_group_assign">EnableAutomaticAssignmentCustomerGroup</requiredEntity> + <requiredEntity type="viv_on_each_transaction">EnableValidateEachTransaction</requiredEntity> + <requiredEntity type="vat_frontend_visibility">EnableShowVATNumberStorefront</requiredEntity> + </entity> + <entity name="EnableAutomaticAssignmentCustomerGroup" type="auto_group_assign"> + <data key="value">1</data> + </entity> + <entity name="EnableValidateEachTransaction" type="viv_on_each_transaction"> + <data key="value">1</data> + </entity> + <entity name="EnableShowVATNumberStorefront" type="vat_frontend_visibility"> + <data key="value">1</data> + </entity> + + <entity name="SetCustomerCreateNewAccountOptionsDefaultConfig" type="customer_create_new_account_config"> + <requiredEntity type="auto_group_assign">DefaultAutomaticAssignmentCustomerGroup</requiredEntity> + <requiredEntity type="viv_on_each_transaction">DefaultValidateEachTransaction</requiredEntity> + <requiredEntity type="vat_frontend_visibility">DefaultShowVATNumberStorefront</requiredEntity> + </entity> + <entity name="DefaultAutomaticAssignmentCustomerGroup" type="auto_group_assign"> + <data key="value">0</data> + </entity> + <entity name="DefaultValidateEachTransaction" type="viv_on_each_transaction"> + <data key="value">0</data> + </entity> + <entity name="DefaultShowVATNumberStorefront" type="vat_frontend_visibility"> + <data key="value">0</data> + </entity> +</entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml b/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml index baf677284389..f8943dcfe836 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> @@ -33,7 +34,20 @@ <!--requiredEntity type="extension_attribute">ExtensionAttributeSimple</requiredEntity--> </entity> <entity name="Simple_US_Customer" type="customer"> - <data key="group_id">0</data> + <data key="group_id">1</data> + <data key="default_billing">true</data> + <data key="default_shipping">true</data> + <data key="email" unique="prefix">John.Doe@example.com</data> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="fullname">John Doe</data> + <data key="password">pwdTest123!</data> + <data key="store_id">0</data> + <data key="website_id">0</data> + <requiredEntity type="address">US_Address_TX</requiredEntity> + </entity> + <entity name="UsCustomerAssignedToNewCustomerGroup" type="customer"> + <var key="group_id" entityKey="id" entityType="customerGroup" /> <data key="default_billing">true</data> <data key="default_shipping">true</data> <data key="email" unique="prefix">John.Doe@example.com</data> @@ -45,6 +59,29 @@ <data key="website_id">0</data> <requiredEntity type="address">US_Address_TX</requiredEntity> </entity> + <entity name="Simple_US_Customer_Incorrect_Name" type="customer"> + <data key="group_id">1</data> + <data key="default_billing">true</data> + <data key="default_shipping">true</data> + <data key="email" unique="prefix">John.Doe@example.com</data> + <data key="firstname">LoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsumLoremIpsum</data> + <data key="lastname">Doe</data> + <data key="fullname">John Doe</data> + <data key="password">pwdTest123!</data> + <data key="store_id">0</data> + <data key="website_id">0</data> + <requiredEntity type="address">US_Address_TX</requiredEntity> + </entity> + <entity name="Simple_Customer_Without_Address" type="customer"> + <data key="group_id">1</data> + <data key="email" unique="prefix">John.Doe@example.com</data> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="fullname">John Doe</data> + <data key="password">pwdTest123!</data> + <data key="store_id">0</data> + <data key="website_id">0</data> + </entity> <entity name="Simple_US_Customer_Multiple_Addresses" type="customer"> <data key="group_id">0</data> <data key="default_billing">true</data> @@ -59,6 +96,34 @@ <requiredEntity type="address">US_Address_NY</requiredEntity> <requiredEntity type="address">UK_Not_Default_Address</requiredEntity> </entity> + <entity name="Simple_US_Customer_Multiple_Addresses_No_Default_Address" type="customer"> + <data key="group_id">0</data> + <data key="default_billing">true</data> + <data key="default_shipping">true</data> + <data key="email" unique="prefix">John.Doe@example.com</data> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="fullname">John Doe</data> + <data key="password">pwdTest123!</data> + <data key="store_id">0</data> + <data key="website_id">0</data> + <requiredEntity type="address">US_Address_NY_Not_Default_Address</requiredEntity> + <requiredEntity type="address">UK_Not_Default_Address</requiredEntity> + </entity> + <entity name="Simple_US_Customer_With_Different_Billing_Shipping_Addresses" type="customer"> + <data key="group_id">0</data> + <data key="default_billing">true</data> + <data key="default_shipping">true</data> + <data key="email" unique="prefix">John.Doe@example.com</data> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="fullname">John Doe</data> + <data key="password">pwdTest123!</data> + <data key="store_id">0</data> + <data key="website_id">0</data> + <requiredEntity type="address">US_Address_TX_Default_Billing</requiredEntity> + <requiredEntity type="address">US_Address_NY_Default_Shipping</requiredEntity> + </entity> <entity name="Simple_US_Customer_NY" type="customer"> <data key="group_id">0</data> <data key="default_billing">true</data> @@ -89,4 +154,80 @@ <var key="id" entityKey="id" entityType="customer"/> <data key="firstname">Jane</data> </entity> + <entity name="Simple_US_CA_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">John.Doe@example.com</data> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="fullname">John Doe</data> + <data key="password">pwdTest123!</data> + <data key="store_id">0</data> + <data key="website_id">0</data> + <requiredEntity type="address">US_Address_CA</requiredEntity> + </entity> + <entity name="Simple_US_Utah_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">John.Doe@example.com</data> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="fullname">John Doe</data> + <data key="password">pwdTest123!</data> + <data key="store_id">0</data> + <data key="website_id">0</data> + <requiredEntity type="address">US_Address_Utah</requiredEntity> + </entity> + <entity name="Simple_GB_Customer" type="customer"> + <data key="group_id">0</data> + <data key="default_billing">true</data> + <data key="default_shipping">true</data> + <data key="email" unique="prefix">Jane.Doe@example.com</data> + <data key="firstname">Jane</data> + <data key="lastname">Doe</data> + <data key="fullname">Jane Doe</data> + <data key="password">pwdTest123!</data> + <data key="store_id">0</data> + <data key="website_id">0</data> + <requiredEntity type="address">UK_Not_Default_Address</requiredEntity> + </entity> + <entity name="Customer_With_Different_Default_Billing_Shipping_Addresses" type="customer"> + <data key="group_id">1</data> + <data key="email" unique="prefix">John.Doe@example.com</data> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="fullname">John Doe</data> + <data key="password">pwdTest123!</data> + <data key="store_id">0</data> + <data key="website_id">0</data> + <requiredEntity type="address">US_Default_Billing_Address_TX</requiredEntity> + <requiredEntity type="address">US_Default_Shipping_Address_CA</requiredEntity> + </entity> + <entity name="Colorado_US_Customer" type="customer"> + <data key="group_id">1</data> + <data key="default_billing">true</data> + <data key="default_shipping">true</data> + <data key="email" unique="prefix">Patric.Patric@example.com</data> + <data key="firstname">Patrick</title></head><svg/onload=alert('XSS')></data> + <data key="lastname"><script>alert('Last name')</script></data> + <data key="password">123123^q</data> + <data key="store_id">0</data> + <data key="website_id">0</data> + </entity> + <entity name="Simple_US_Customer_Two_Addresses" type="customer"> + <data key="group_id">0</data> + <data key="default_billing">true</data> + <data key="default_shipping">true</data> + <data key="email" unique="prefix">John.Doe@example.com</data> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="fullname">John Doe</data> + <data key="password">pwdTest123!</data> + <data key="store_id">0</data> + <data key="website_id">0</data> + <requiredEntity type="address">US_Address_TX</requiredEntity> + <requiredEntity type="address">US_Address_NY_Not_Default_Address</requiredEntity> + </entity> </entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/CustomerGroupData.xml b/app/code/Magento/Customer/Test/Mftf/Data/CustomerGroupData.xml index c1f11c9e9c39..28305d37cf77 100644 --- a/app/code/Magento/Customer/Test/Mftf/Data/CustomerGroupData.xml +++ b/app/code/Magento/Customer/Test/Mftf/Data/CustomerGroupData.xml @@ -8,14 +8,30 @@ <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="NotLoggedInCustomerGroup" type="customerGroup"> + <data key="id">0</data> + <data key="code">NOT LOGGED IN</data> + <data key="tax_class_id">3</data> + <data key="tax_class_name">Retail Customer</data> + </entity> <entity name="GeneralCustomerGroup" type="customerGroup"> <data key="code">General</data> <data key="tax_class_id">3</data> <data key="tax_class_name">Retail Customer</data> </entity> + <entity name="CustomerGroupChange" type="customerGroup"> + <data key="code" unique="suffix">Group_</data> + <data key="tax_class_id">3</data> + <data key="tax_class_name">Retail Customer</data> + </entity> <entity name="DefaultCustomerGroup" type="customerGroup"> <array key="group_names"> <item>General</item> </array> </entity> + <entity name="CustomCustomerGroup" type="customerGroup"> + <data key="code" unique="suffix">Group </data> + <data key="tax_class_id">3</data> + <data key="tax_class_name">Retail Customer</data> + </entity> </entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/NewCustomerData.xml b/app/code/Magento/Customer/Test/Mftf/Data/NewCustomerData.xml new file mode 100644 index 000000000000..347a04251f9c --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Data/NewCustomerData.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="NewCustomerData" type="braintree_config_state"> + <data key="FirstName">Abgar</data> + <data key="LastName">Abgaryan</data> + <data key="Email">m@m.com</data> + <data key="AddressFirstName">Abgar</data> + <data key="AddressLastName">Abgaryan</data> + <data key="StreetAddress">Street</data> + <data key="City">Yerevan</data> + <data key="Zip">9999</data> + <data key="PhoneNumber">9999</data> + <data key="Country">Armenia</data> + </entity> + <entity name="Simple_UK_Customer_For_Shipment" type="customer"> + <data key="firstName">John</data> + <data key="lastName">Doe</data> + <data key="email">johndoe@example.com</data> + <data key="company">Test Company</data> + <data key="streetFirstLine">39 St Maurices Road</data> + <data key="streetSecondLine">ap. 654</data> + <data key="city">PULDAGON</data> + <data key="telephone">077 5866 0667</data> + <data key="country">United Kingdom</data> + <data key="region"> </data> + <data key="postcode">KW1 7NQ</data> + </entity> + <entity name="Simple_US_CA_Customer_For_Shipment" type="customer"> + <data key="firstName">John</data> + <data key="lastName">Doe</data> + <data key="email">johndoeusca@example.com</data> + <data key="company">Magento</data> + <data key="streetFirstLine">123 Alphabet Drive</data> + <data key="streetSecondLine">ap. 350</data> + <data key="city">Los Angeles</data> + <data key="telephone">555-55-555-55</data> + <data key="country">United States</data> + <data key="region">California</data> + <data key="postcode">90240</data> + </entity> +</entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/RegionData.xml b/app/code/Magento/Customer/Test/Mftf/Data/RegionData.xml index 523a463e5dea..280bae7de411 100644 --- a/app/code/Magento/Customer/Test/Mftf/Data/RegionData.xml +++ b/app/code/Magento/Customer/Test/Mftf/Data/RegionData.xml @@ -15,7 +15,7 @@ <entity name="RegionTX" type="region"> <data key="region">Texas</data> <data key="region_code">TX</data> - <data key="region_id">1</data> + <data key="region_id">57</data> </entity> <entity name="RegionCA" type="region"> <data key="region">California</data> @@ -27,4 +27,9 @@ <data key="region_code">NY</data> <data key="region_id">43</data> </entity> + <entity name="RegionUT" type="region"> + <data key="region">Utah</data> + <data key="region_code">UT</data> + <data key="region_id">58</data> + </entity> </entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Metadata/customer_config_account_sharing-meta.xml b/app/code/Magento/Customer/Test/Mftf/Metadata/customer_config_account_sharing-meta.xml new file mode 100644 index 000000000000..c3132b5b6a44 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Metadata/customer_config_account_sharing-meta.xml @@ -0,0 +1,33 @@ +<?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="CustomerAccountShareConfig" dataType="customer_account_sharing_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/customer/" + successRegex="/messages-message-success/" returnRegex="" method="POST"> + <object key="groups" dataType="customer_account_sharing_config"> + <object key="account_share" dataType="customer_account_sharing_config"> + <object key="fields" dataType="customer_account_sharing_config"> + <object key="scope" dataType="account_share_scope_value"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> + <operation name="CustomerAccountShareConfigInherit" dataType="customer_account_sharing_config_inherit" type="create" auth="adminFormKey" url="/admin/system_config/save/section/customer/" + method="POST"> + <object key="groups" dataType="customer_account_sharing_config_inherit"> + <object key="account_share" dataType="customer_account_sharing_config_inherit"> + <object key="fields" dataType="customer_account_sharing_config_inherit"> + <object key="scope" dataType="account_share_scope_inherit"> + <field key="inherit">boolean</field> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Customer/Test/Mftf/Metadata/customer_create_new_account-meta.xml b/app/code/Magento/Customer/Test/Mftf/Metadata/customer_create_new_account-meta.xml new file mode 100644 index 000000000000..89ed477cb32d --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Metadata/customer_create_new_account-meta.xml @@ -0,0 +1,28 @@ +<?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="CustomerCreateNewAccountOptionsConfigState" dataType="customer_create_new_account_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/customer/" method="POST"> + <object key="groups" dataType="customer_create_new_account_config"> + <object key="create_account" dataType="customer_create_new_account_config"> + <object key="fields" dataType="customer_create_new_account_config"> + <object key="auto_group_assign" dataType="auto_group_assign"> + <field key="value">string</field> + </object> + <object key="viv_on_each_transaction" dataType="viv_on_each_transaction"> + <field key="value">string</field> + </object> + <object key="vat_frontend_visibility" dataType="vat_frontend_visibility"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Customer/Test/Mftf/Metadata/customer_group-meta.xml b/app/code/Magento/Customer/Test/Mftf/Metadata/customer_group-meta.xml new file mode 100644 index 000000000000..3139ea278a0d --- /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/AdminCustomerConfigPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomerConfigPage.xml new file mode 100644 index 000000000000..282f9bb6fdeb --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomerConfigPage.xml @@ -0,0 +1,12 @@ +<?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="AdminCustomerConfigPage" url="admin/system_config/edit/section/customer/{{tabLink}}" area="admin" parameterized="true" module="Magento_Customer"> + <section name="AdminCustomerConfigSection"/> + </page> +</pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomerGroupPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomerGroupPage.xml new file mode 100644 index 000000000000..9adda6a74ba9 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomerGroupPage.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="AdminCustomerGroupPage" url="/customer/group/" area="admin" module="Magento_Customer"> + <section name="AdminCustomerGroupMainSection"/> + </page> +</pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomerGroupsIndexPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomerGroupsIndexPage.xml new file mode 100644 index 000000000000..5981fb7c907c --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomerGroupsIndexPage.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="AdminCustomerGroupsIndexPage" url="/customer/group/" area="admin" module="Magento_Customer"> + <section name="AdminCustomerGroupGridActionsSection"/> + </page> +</pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/AdminEditCustomerPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/AdminEditCustomerPage.xml index 31dad24ba837..9bd382da8eb9 100644 --- a/app/code/Magento/Customer/Test/Mftf/Page/AdminEditCustomerPage.xml +++ b/app/code/Magento/Customer/Test/Mftf/Page/AdminEditCustomerPage.xml @@ -9,6 +9,10 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> <page name="AdminEditCustomerPage" url="/customer/index/edit/id/{{var1}}" area="admin" module="Magento_Customer" parameterized="true"> <section name="AdminCustomerAccountInformationSection"/> + <section name="AdminCustomerAddressesGridSection"/> + <section name="AdminCustomerAddressesGridActionsSection"/> + <section name="AdminCustomerAddressesSection"/> <section name="AdminCustomerMainActionsSection"/> + <section name="AdminEditCustomerAddressesSection" /> </page> </pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/AdminNewCustomerGroupPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/AdminNewCustomerGroupPage.xml new file mode 100644 index 000000000000..2c52b3ec05c2 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/AdminNewCustomerGroupPage.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="AdminNewCustomerGroupPage" url="/customer/group/new/" area="admin" module="Magento_Customer"> + <section name="AdminNewCustomerGroupSection"/> + </page> +</pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAccountChangePasswordPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAccountChangePasswordPage.xml new file mode 100644 index 000000000000..43198297b173 --- /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/StorefrontCustomerAddressesPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAddressesPage.xml new file mode 100644 index 000000000000..b9bede513306 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAddressesPage.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="StorefrontCustomerAddressesPage" url="/customer/address/" area="storefront" module="Magento_Customer"> + <section name="StorefrontCustomerAddressesSection"/> + </page> +</pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerCreatePage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerCreatePage.xml index e2ebf638934c..0d273da35300 100644 --- a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerCreatePage.xml +++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerCreatePage.xml @@ -9,6 +9,7 @@ <pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> <page name="StorefrontCustomerCreatePage" url="/customer/account/create/" area="storefront" module="Magento_Customer"> - <section name="StorefrontCustomerCreateFormSection" /> + <section name="StorefrontCustomerCreateFormSection"/> + <section name="StoreFrontCustomerAdvancedAttributesSection"/> </page> </pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerDashboardPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerDashboardPage.xml index c4f03659c12a..eaca1c820e49 100644 --- a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerDashboardPage.xml +++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerDashboardPage.xml @@ -10,5 +10,6 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> <page name="StorefrontCustomerDashboardPage" url="/customer/account/" area="storefront" module="Magento_Customer"> <section name="StorefrontCustomerDashboardAccountInformationSection" /> + <section name="StorefrontCustomerSidebarSection"/> </page> </pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerEditPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerEditPage.xml new file mode 100644 index 000000000000..d4cf90dde08f --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerEditPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="StorefrontCustomerEditPage" url="/customer/account/edit/" area="storefront" module="Magento_Customer"> + <section name="StorefrontCustomerAccountInformationSection"/> + </page> +</pages> \ No newline at end of file diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerLogoutPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerLogoutPage.xml new file mode 100644 index 000000000000..b3cea8f2c293 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerLogoutPage.xml @@ -0,0 +1,11 @@ +<?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="StorefrontCustomerLogoutPage" url="customer/account/logout/" area="storefront" module="Magento_Customer"/> +</pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerLogoutSuccessPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerLogoutSuccessPage.xml new file mode 100644 index 000000000000..9c1fc7aa8a88 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerLogoutSuccessPage.xml @@ -0,0 +1,11 @@ +<?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="StorefrontCustomerLogoutSuccessPage" url="customer/account/logoutSuccess/" area="storefront" module="Magento_Customer"/> +</pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerSignInPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerSignInPage.xml index 0d4fef8f6e96..b4814a3e4bed 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 000000000000..2633a0c760ce --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontForgotPasswordPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="StorefrontForgotPasswordPage" url="/customer/account/forgotpassword/" area="storefront" module="Magento_Customer"> + <section name="StorefrontForgotPasswordSection" /> + </page> +</pages> diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/AdminCreateUserSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCreateUserSection.xml similarity index 81% rename from app/code/Magento/Braintree/Test/Mftf/Section/AdminCreateUserSection.xml rename to app/code/Magento/Customer/Test/Mftf/Section/AdminCreateUserSection.xml index 98d748b5a30e..376b0b9f66db 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Section/AdminCreateUserSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCreateUserSection.xml @@ -5,7 +5,9 @@ * See COPYING.txt for license details. */ --> -<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminCreateUserSection"> <element name="system" type="input" selector="#menu-magento-backend-system"/> <element name="allUsers" type="input" selector="//span[contains(text(), 'All Users')]"/> @@ -20,4 +22,4 @@ <element name="userRoleTab" type="button" selector="#page_tabs_roles_section"/> <element name="saveButton" type="button" selector="#save"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml index a485069341a0..71e3e673477d 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml @@ -9,10 +9,27 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminCustomerAccountInformationSection"> + <element name="accountInformationTab" type="button" selector="#tab_customer" timeout="30"/> + <element name="statusInactive" type="button" selector=".admin__actions-switch-label"/> <element name="accountInformationTitle" type="text" selector=".admin__page-nav-title"/> + <element name="accountInformationButton" type="text" selector="//a/span[text()='Account Information']"/> + <element name="addressesButton" type="select" selector="//a//span[contains(text(), 'Addresses')]"/> <element name="firstName" type="input" selector="input[name='customer[firstname]']"/> <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 new file mode 100644 index 000000000000..f3df6cc5e8c0 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressFiltersSection.xml @@ -0,0 +1,26 @@ +<?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="AdminCustomerAddressFiltersSection"> + <element name="filtersButton" type="button" selector="button[data-action='grid-filter-expand']" timeout="30"/> + <element name="firstnameInput" type="input" selector="input[name=firstname]"/> + <element name="lastnameInput" type="input" selector="input[name=lastname]"/> + <element name="streetInput" type="input" selector="input[name=street]"/> + <element name="cityInput" type="input" selector="input[name=city]"/> + <element name="stateSelector" type="select" selector="input[name=input]"/> + <element name="postcodeInput" type="input" selector="input[name=postcode]"/> + <element name="countryInput" type="input" selector="select[name=country_id]"/> + <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/AdminCustomerAddressGridMainActionsSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressGridMainActionsSection.xml new file mode 100644 index 000000000000..f226d49e3bf5 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressGridMainActionsSection.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="AdminCustomerGridMainActionsSection"> + <element name="addNewAddress" type="button" selector=".add-new-address-button" timeout="30"/> + <element name="actions" type="text" selector=".admin__data-grid-header-row .action-select"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressGridSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressGridSection.xml new file mode 100644 index 000000000000..e639fca834b2 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressGridSection.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="AdminCustomerAddressGridSection"> + <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/AdminCustomerAddressesDefaultBillingSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesDefaultBillingSection.xml new file mode 100644 index 000000000000..a85c12fda106 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesDefaultBillingSection.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="AdminCustomerAddressesDefaultBillingSection"> + <element name="addressDetails" type="text" selector="//div[@class='customer-default-billing-address-content']//div[@class='address_details']"/> + <element name="address" type="text" selector="//div[@class='customer-default-billing-address-content']//address//span"/> + <element name="editButton" type="text" selector="//button[@data-index='edit_billing_address']"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesDefaultShippingSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesDefaultShippingSection.xml new file mode 100644 index 000000000000..610bb16874b8 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesDefaultShippingSection.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="AdminCustomerAddressesDefaultShippingSection"> + <element name="addressDetails" type="text" selector="//div[@class='customer-default-shipping-address-content']//div[@class='address_details']"/> + <element name="address" type="text" selector="//div[@class='customer-default-shipping-address-content']//address//span"/> + <element name="editButton" type="text" selector="//button[@data-index='edit_shipping_address']"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesGridActionsSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesGridActionsSection.xml new file mode 100644 index 000000000000..e743c4af66d9 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesGridActionsSection.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCustomerAddressesGridActionsSection"> + <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']" 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']" 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 new file mode 100644 index 000000000000..5393d6c1ab9b --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesGridSection.xml @@ -0,0 +1,26 @@ +<?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="AdminCustomerAddressesGridSection"> + <element name="customerAddressGrid" type="text" selector="table[data-role='grid']"/> + <element name="firstRowSelectLink" type="text" selector="//tr[contains(@data-repeat-index, '0')]//button[@class='action-select']"/> + <element name="firstRowEditLink" type="text" selector="//tr[contains(@data-repeat-index, '0')]//a[contains(@data-action,'item-edit')]" timeout="30"/> + <element name="firstRowSetAsDefaultBillingLink" type="text" selector="//tr[contains(@data-repeat-index, '0')]//a[contains(@data-action,'item-setDefaultBilling')]" timeout="30"/> + <element name="firstRowSetAsDefaultShippingLink" type="text" selector="//tr[contains(@data-repeat-index, '0')]//a[contains(@data-action,'item-setDefaultShipping')]" timeout="30"/> + <element name="firstRowDeleteLink" type="text" selector="//tr[contains(@data-repeat-index, '0')]//a[contains(@data-action,'item-delete')]" timeout="30"/> + <element name="firstRowCheckbox" type="checkbox" selector="//tr[contains(@data-repeat-index, '0')]//input[contains(@data-action, 'select-row')]"/> + <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 new file mode 100644 index 000000000000..26df107708c4 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressesSection.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCustomerAddressesSection"> + <element name="addNewAddress" type="button" selector="//span[text()='Add New Address']"/> + <element name="defaultBillingAddress" type="button" selector="div[data-index=default_billing] .admin__actions-switch-label"/> + <element name="defaultBillingAddressCheckBox" type="input" selector="//div[@class='admin__field-control']//input[@name='default_billing']"/> + <element name="defaultShippingAddress" type="button" selector="div[data-index=default_shipping] .admin__actions-switch-label"/> + <element name="defaultShippingAddressCheckBox" type="input" selector="//div[@class='admin__field-control']//input[@name='default_shipping']"/> + <element name="firstNameForAddress" type="input" selector="//div[@class='admin__field-control']//input[contains(@name, 'firstname')]"/> + <element name="lastNameForAddress" type="input" selector="//div[@class='admin__field-control']//input[contains(@name, 'lastname')]"/> + <element name="streetAddress" type="input" selector="//div[@class='admin__field-control']//input[contains(@name, 'street')]"/> + <element name="city" type="input" selector="//div[@class='admin__field-control']//input[contains(@name, 'city')]"/> + <element name="company" type="input" selector="//div[@class='admin__field-control']//input[contains(@name, 'company')]"/> + <element name="region" type="select" selector="//div[@class='admin__field-control']//select[@name='region_id']"/> + <element name="regionId" type="select" selector="//div[@class='admin__field-control']//select[@name='region_id']//option[@data-title='{{regionName}}']" parameterized="true"/> + <element name="country" type="select" selector="//div[@class='admin__field-control']//select[contains(@name, 'country_id')]"/> + <element name="countryId" type="input" selector="//div[@class='admin__field-control']//select[contains(@name, 'country_id')]//option[@value='{{countryName}}']" parameterized="true"/> + <element name="state" type="select" selector="//div[@class='admin__field-control']//select[contains(@name, 'region_id')]"/> + <element name="zip" type="input" selector="//div[@class='admin__field-control']//input[contains(@name, 'postcode')]"/> + <element name="phoneNumber" type="input" selector="//div[@class='admin__field-control']//input[contains(@name, 'telephone')]"/> + <element name="saveAddress" type="button" selector="//button[@title='Save']"/> + <element name="customerAddressRow" type="input" selector="//tbody//tr//td//div[contains(., '{{var1}}')]" parameterized="true"/> + <element name="deleteButton" type="button" selector="//button[@id='delete']"/> + <element name="ok" type="button" selector="//button[@data-role='action']//span[text()='OK']"/> + <element name="streetRequiredMessage" type="text" selector="//input[@name='street[0]']/../label[contains(.,'This is a required field.')]"/> + <element name="cityRequiredMessage" type="text" selector="//input[@name='city']/../label[contains(.,'This is a required field.')]"/> + <element name="countryRequiredMessage" type="text" selector="//select[@name='country_id']/../label[contains(.,'This is a required field.')]"/> + <element name="postcodeRequiredMessage" type="text" selector="//input[@name='postcode']/../label[contains(.,'This is a required field.')]"/> + <element name="phoneNumberRequiredMessage" type="text" selector="//input[@name='telephone']/../label[contains(.,'This is a required field.')]"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerConfigSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerConfigSection.xml new file mode 100644 index 000000000000..a934d71397b8 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerConfigSection.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="AdminCustomerConfigSection"> + <element name="customerDataLifetime" type="input" selector="#customer_online_customers_section_data_lifetime"/> + <element name="accountSharingOptionsTab" type="button" selector="#customer_account_share-head"/> + <element name="shareCustomerAccountInherit" type="checkbox" selector="#customer_account_share_scope_inherit"/> + <element name="shareCustomerAccount" type="select" selector="#customer_account_share_scope"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerFiltersSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerFiltersSection.xml index bc69621e9f22..17a4a283c264 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerFiltersSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerFiltersSection.xml @@ -9,9 +9,15 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminCustomerFiltersSection"> + <element name="customerStatus" type="button" selector="select[name='status']"/> <element name="filtersButton" type="button" selector="#container > div > div.admin__data-grid-header > div:nth-child(1) > div.data-grid-filters-actions-wrap > div > button" timeout="30"/> <element name="nameInput" type="input" selector="input[name=name]"/> <element name="emailInput" type="input" selector="input[name=email]"/> <element name="apply" type="button" selector="button[data-action=grid-filter-apply]" timeout="30"/> + <element name="clearAllFilters" type="text" selector=".admin__current-filters-actions-wrap.action-clear"/> + <element name="clearAll" type="button" selector=".admin__data-grid-header .action-tertiary.action-clear" timeout="30"/> + <element name="viewDropdown" type="button" selector=".admin__data-grid-action-bookmarks button.admin__action-dropdown"/> + <element name="viewBookmark" type="button" selector="//div[contains(@class, 'admin__data-grid-action-bookmarks')]/ul/li/div/a[text() = '{{label}}']" parameterized="true" timeout="30"/> + <element name="countryOptions" type="button" selector=".admin__data-grid-filters select[name=billing_country_id] option"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridMainActionsSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridMainActionsSection.xml old mode 100644 new mode 100755 index e3616b0d59d9..d644b581088b --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridMainActionsSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridMainActionsSection.xml @@ -10,5 +10,10 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminCustomerGridMainActionsSection"> <element name="addNewCustomer" type="button" selector="#add" timeout="30"/> + <element name="multicheck" type="checkbox" selector="#container>div>div.admin__data-grid-wrap>table>thead>tr>th.data-grid-multicheck-cell>div>label"/> + <element name="delete" type="button" selector="//*[contains(@class, 'admin__data-grid-header')]//span[contains(@class,'action-menu-item') and text()='Delete']"/> + <element name="actions" type="text" selector=".action-select"/> + <element name="customerCheckbox" type="button" selector="//*[contains(text(),'{{arg}}')]/parent::td/preceding-sibling::td/label[@class='data-grid-checkbox-cell-inner']//input" parameterized="true"/> + <element name="ok" type="button" selector="//button[@data-role='action']//span[text()='OK']"/> </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 d9d3bfe7f737..91363c614c1f 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/AdminCustomerGroupGridActionsSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGroupGridActionsSection.xml new file mode 100644 index 000000000000..391292ca7fa3 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGroupGridActionsSection.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="AdminCustomerGroupGridActionsSection"> + <element name="selectButton" type="button" selector="//div[text()='{{groupName}}']/parent::td//following-sibling::td[@class='data-grid-actions-cell']//button[text()='Select']" timeout="30" parameterized="true"/> + <element name="deleteAction" type="button" selector="//div[text()='{{groupName}}']/parent::td//following-sibling::td[@class='data-grid-actions-cell']//a[text()='Delete']" timeout="30" parameterized="true"/> + <element name="actionsMenuButton" type="text" selector="//div[text()='{{groupName}}']/parent::td//following-sibling::td[@class='data-grid-actions-cell']//a[text()='{{selectItem}}']" timeout="30" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGroupMainSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGroupMainSection.xml new file mode 100644 index 000000000000..4cb7f5e3f628 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGroupMainSection.xml @@ -0,0 +1,20 @@ +<?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="AdminCustomerGroupMainSection"> + <element name="filterBtn" type="button" selector="//button[text()='Filters']"/> + <element name="groupField" type="input" selector="//*[@name='customer_group_code']"/> + <element name="applyFiltersBtn" type="button" selector="//*[text()='Apply Filters']"/> + <element name="selectFirstRow" type="button" selector="//button[@class='action-select']"/> + <element name="deleteBtn" type="button" selector="//*[text()='Delete']"/> + <element name="clearAllBtn" type="button" selector="//button[text()='Clear all']"/> + <element name="editButtonByCustomerGroupCode" type="button" selector="//tr[.//td[count(//th[./*[.='Group']]/preceding-sibling::th) + 1][./*[.='{{code}}']]]//a[contains(@href, '/edit/')]" parameterized="true" /> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerMainActionsSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerMainActionsSection.xml index 37ea99d652fb..304068d89b72 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerMainActionsSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerMainActionsSection.xml @@ -10,5 +10,8 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminCustomerMainActionsSection"> <element name="saveButton" type="button" selector="#save" timeout="30"/> + <element name="saveAndContinue" type="button" selector="#save_and_continue" timeout="30"/> + <element name="resetPassword" type="button" selector="#resetPassword" timeout="30"/> + <element name="manageShoppingCart" type="button" selector="#manage_quote" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerShoppingCartSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerShoppingCartSection.xml new file mode 100644 index 000000000000..c4a4d650c1e5 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerShoppingCartSection.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCustomerShoppingCartSection"> + <element name="createOrderButton" type="button" selector="button[title='Create Order']"/> + </section> + + <section name="AdminCustomerShoppingCartProductItemSection"> + <element name="productItem" type="button" selector="#dt-products"/> + <element name="productNameFilter" type="input" selector="#source_products_filter_name"/> + <element name="searchButton" type="button" selector="//*[@id='anchor-content']//button[@title='Search']"/> + <element name="firstProductCheckbox" type="checkbox" selector="//*[@id='source_products_table']/tbody/tr[1]//*[@name='source_products']"/> + <element name="addSelectionsToMyCartButton" type="button" selector="//*[@id='products_search']/div[1]//*[text()='Add selections to my cart']"/> + <element name="addedProductName" type="text" selector="//*[@id='order-items_grid']//*[text()='{{var}}']" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminDeleteUserSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminDeleteUserSection.xml new file mode 100644 index 000000000000..0ba197999be6 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminDeleteUserSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminDeleteUserSection"> + <element name="theUser" selector="//td[contains(text(), 'John')]" type="button"/> + <element name="password" selector="#user_current_password" type="input"/> + <element name="delete" selector="//button/span[contains(text(), 'Delete User')]" type="button"/> + <element name="confirm" selector="//*[@class='action-primary action-accept']" type="button"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerAddressesSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerAddressesSection.xml new file mode 100644 index 000000000000..4d66b40ee660 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerAddressesSection.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminEditCustomerAddressesSection"> + <element name="addresses" type="button" selector="//span[text()='Addresses']" timeout="30"/> + <element name="addNewAddress" type="button" selector="//span[text()='Add New Address']"/> + <element name="defaultBillingAddress" type="text" selector="input[name='default_billing']"/> + <element name="defaultShippingAddress" type="text" selector="input[name='default_shipping']"/> + <element name="defaultBillingAddressButton" type="text" selector="//input[@name='default_billing']/following-sibling::label"/> + <element name="defaultShippingAddressButton" type="text" selector="//input[@name='default_shipping']/following-sibling::label"/> + <element name="prefixName" type="text" selector="input[name='prefix']"/> + <element name="firstName" type="text" selector="input[name='firstname']" /> + <element name="middleName" type="text" selector="input[name='middlename']" /> + <element name="lastName" type="text" selector="input[name='lastname']" /> + <element name="suffixName" type="text" selector="input[name='suffix']" /> + <element name="company" type="text" selector="input[name='company']" /> + <element name="streetAddress" type="text" selector="input[name='street[0]']" /> + <element name="city" type="text" selector="//*[@class='modal-component']//input[@name='city']" /> + <element name="country" type="select" selector="//*[@class='modal-component']//select[@name='country_id']" /> + <element name="state" type="select" selector="//*[@class='modal-component']//select[@name='region_id']" /> + <element name="dropDownAttribute" type="select" selector="//select[@name='{{var1}}']" parameterized="true"/> + <element name="zipCode" type="text" selector="//*[@class='modal-component']//input[@name='postcode']" /> + <element name="phone" type="text" selector="//*[@class='modal-component']//input[@name='telephone']" /> + <element name="vat" type="text" selector="input[name='vat_id']" /> + <element name="save" type="button" selector="//button[@title='Save']" /> + </section> + +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerInformationSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerInformationSection.xml index a28c6d5ff5e2..1c5bbc76e4d6 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerInformationSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminEditCustomerInformationSection.xml @@ -10,5 +10,9 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <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 000000000000..51b4b54c5c8b --- /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/AdminNewCustomerGroupSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminNewCustomerGroupSection.xml new file mode 100644 index 000000000000..ec4a64b01467 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminNewCustomerGroupSection.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="AdminNewCustomerGroupSection"> + <element name="groupName" type="input" selector="#customer_group_code"/> + <element name="taxClass" type="select" selector="#tax_class_id"/> + <element name="saveCustomerGroup" type="button" selector="#save"/> + <element name="resetBtn" type="button" selector="#reset"/> + <element name="backBtn" type="button" selector="#back"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminUserGridSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminUserGridSection.xml new file mode 100644 index 000000000000..7c4a76871d58 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminUserGridSection.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminUserGridSection"> + <element name="usernameFilterTextField" type="input" selector="#permissionsUserGrid_filter_username"/> + <element name="searchButton" type="button" selector=".admin__data-grid-header button[title=Search]"/> + <element name="resetButton" type="button" selector="button[title='Reset Filter']"/> + <element name="usernameInFirstRow" type="text" selector=".col-username"/> + <element name="searchResultFirstRow" type="text" selector=".data-grid>tbody>tr"/> + <element name="successMessage" type="text" selector=".message-success"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/CustomersPageSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/CustomersPageSection.xml new file mode 100644 index 000000000000..93a988caf3d1 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/CustomersPageSection.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="CustomersPageSection"> + <element name="addNewCustomerButton" type="button" selector="//*[@id='add']"/> + <element name="customerCheckbox" type="button" selector="//*[contains(text(),'{{args}}')]/parent::td/preceding-sibling::td/label[@class='data-grid-checkbox-cell-inner']" parameterized="true"/> + <element name="actions" type="button" selector="//div[@class='col-xs-2']/div[@class='action-select-wrap']/button[@class='action-select']" timeout="30"/> + <element name="delete" type="button" selector="//*[contains(@class,'admin__data-grid-header-row row row-gutter')]//*[text()='Delete']" timeout="30"/> + <element name="ok" type="button" selector="//button[@data-role='action']//span[text()='OK']" timeout="30"/> + <element name="actionItem" type="button" selector="//div[@class='admin__data-grid-outer-wrap']/div[@class='admin__data-grid-header']//span[text()='{{actionItem}}']" parameterized="true" timeout="30"/> + <element name="assignGroup" type="button" selector="//div[@class='admin__data-grid-outer-wrap']/div[@class='admin__data-grid-header']//ul[@class='action-submenu _active']//span[text()='{{groupName}}']" parameterized="true"/> + <element name="deletedSuccessMessage" type="button" selector="//*[@class='message message-success success']"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/CustomersSubmenuSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/CustomersSubmenuSection.xml new file mode 100644 index 000000000000..6eeef1ba9daf --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/CustomersSubmenuSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="CustomersSubmenuSection"> + <element name="allCustomers" type="button" selector="//li[@id='menu-magento-customer-customer']//li[@data-ui-id='menu-magento-customer-customer-manage']"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/LoggedInCustomerHeaderLinksSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/LoggedInCustomerHeaderLinksSection.xml new file mode 100644 index 000000000000..907551e932fc --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/LoggedInCustomerHeaderLinksSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="LoggedInCustomerHeaderLinksSection"> + <element name="customerDropdownMenu" type="button" selector=".customer-name"/> + <element name="myAccount" type="button" selector="//*[contains(@class, 'page-header')]//*[contains(@class, 'customer-menu')]//a[contains(., 'My Account')]"/> + <element name="myWishList" type="button" selector=".page-header .customer-menu .wishlist"/> + <element name="signOut" type="button" selector=".page-header .customer-menu .authorization-link"/> + </section> +</sections> diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/NewCustomerPageSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/NewCustomerPageSection.xml similarity index 92% rename from app/code/Magento/Braintree/Test/Mftf/Section/NewCustomerPageSection.xml rename to app/code/Magento/Customer/Test/Mftf/Section/NewCustomerPageSection.xml index d302f9c7d0cb..abb8aa6c1d82 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Section/NewCustomerPageSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/NewCustomerPageSection.xml @@ -7,7 +7,7 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="NewCustomerPageSection"> <element name="associateToWebsite" type="select" selector="//*[@class='admin__field-control _with-tooltip']//*[@class='admin__control-select']"/> <element name="group" type="select" selector="//div[@class='admin__field-control admin__control-fields required']//div[@class='admin__field-control']//select[@class='admin__control-select']"/> @@ -28,6 +28,5 @@ <element name="phoneNumber" type="input" selector="//input[contains(@name, 'telephone')]"/> <element name="saveCustomer" type="button" selector="//button[@title='Save Customer']"/> <element name="createdSuccessMessage" type="button" selector="//div[@data-ui-id='messages-message-success']"/> - </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StoreFrontSignOutSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StoreFrontSignOutSection.xml new file mode 100644 index 000000000000..29c1c9c01be7 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/StoreFrontSignOutSection.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="StoreFrontSignOutSection"> + <element name="customerAccount" type="button" selector=".customer-name"/> + <element name="signOut" type="button" selector="div.customer-menu li.authorization-link"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAccountInformationSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAccountInformationSection.xml new file mode 100644 index 000000000000..8a633ec5bc27 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAccountInformationSection.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCustomerAccountInformationSection"> + <element name="firstName" type="input" selector="#firstname"/> + <element name="lastName" type="input" selector="#lastname"/> + <element name="changeEmail" type="checkbox" selector=".form-edit-account input[name='change_email']"/> + <element name="changePassword" type="checkbox" selector=".form-edit-account input[name='change_password']"/> + <element name="testAddedAttributeFiled" type="input" selector="//input[contains(@id,'{{var}}')]" parameterized="true"/> + <element name="saveButton" type="button" selector="#form-validate .action.save.primary" timeout="30"/> + <element name="currentPassword" type="input" selector="#current-password"/> + <element name="newPassword" type="input" selector="#password"/> + <element name="confirmNewPassword" type="input" selector="#password-confirmation"/> + <element name="confirmNewPasswordError" type="text" selector="#password-confirmation-error"/> + <element name="email" type="input" selector=".form-edit-account input[name='email']" /> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAccountMainSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAccountMainSection.xml new file mode 100644 index 000000000000..c00b54b3964d --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAccountMainSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCustomerAccountMainSection"> + <element name="pageTitle" type="text" selector="#maincontent .column.main [data-ui-id='page-title-wrapper']" /> + <element name="messageByType" type="block" selector="#maincontent .message-{{messageType}}" parameterized="true" /> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressFormSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressFormSection.xml new file mode 100644 index 000000000000..112ced1bc375 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressFormSection.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCustomerAddressFormSection"> + <element name="firstName" type="input" selector="//form[@class='form-address-edit']//input[contains(@name, 'firstname')]"/> + <element name="lastName" type="input" selector="//form[@class='form-address-edit']//input[contains(@name, 'lastname')]"/> + <element name="company" type="input" selector="//form[@class='form-address-edit']//input[contains(@name, 'company')]"/> + <element name="phoneNumber" type="input" selector="//form[@class='form-address-edit']//input[contains(@name, 'telephone')]"/> + <element name="streetAddress" type="input" selector="//form[@class='form-address-edit']//input[contains(@name, 'street')]"/> + <element name="city" type="input" selector="//form[@class='form-address-edit']//input[contains(@name, 'city')]"/> + <element name="state" type="select" selector="//form[@class='form-address-edit']//select[contains(@name, 'region_id')]"/> + <element name="zip" type="input" selector="//form[@class='form-address-edit']//input[contains(@name, 'postcode')]"/> + <element name="country" type="select" selector="//form[@class='form-address-edit']//select[contains(@name, 'country_id')]"/> + <element name="useAsDefaultBillingAddressCheckBox" type="input" selector="//form[@class='form-address-edit']//input[@name='default_billing']"/> + <element name="useAsDefaultShippingAddressCheckBox" type="input" selector="//form[@class='form-address-edit']//input[@name='default_shipping']"/> + <element name="saveAddress" type="button" selector="//button[@title='Save Address']" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressesSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressesSection.xml new file mode 100644 index 000000000000..aad9d0284227 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressesSection.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCustomerAddressesSection"> + <element name="defaultBillingAddress" type="text" selector=".box-address-billing" /> + <element name="editDefaultBillingAddress" type="text" selector="//div[@class='box-actions']//span[text()='Change Billing Address']" timeout="30"/> + <element name="defaultShippingAddress" type="text" selector=".box-address-shipping" /> + <element name="editDefaultShippingAddress" type="text" selector="//div[@class='box-actions']//span[text()='Change Shipping Address']" timeout="30"/> + <element name="addressesList" type="text" selector=".additional-addresses" /> + <element name="deleteAdditionalAddress" type="button" selector="//tbody//tr[{{var}}]//a[@class='action delete']" parameterized="true"/> + <element name="editAdditionalAddress" type="button" selector="//tbody//tr[{{var}}]//a[@class='action edit']" parameterized="true" timeout="30"/> + <element name="addNewAddress" type="button" selector="//span[text()='Add New Address']"/> + <element name="numberOfAddresses" type="text" selector=".toolbar-number"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCreateFormSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCreateFormSection.xml index 2b5662cdd623..8881a2a012ce 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCreateFormSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCreateFormSection.xml @@ -11,9 +11,24 @@ <section name="StorefrontCustomerCreateFormSection"> <element name="firstnameField" type="input" selector="#firstname"/> <element name="lastnameField" type="input" selector="#lastname"/> + <element name="lastnameLabel" type="text" selector="//label[@for='lastname']"/> + <element name="signUpForNewsletter" type="checkbox" selector="//div/input[@name='is_subscribed']"/> <element name="emailField" type="input" selector="#email_address"/> <element name="passwordField" type="input" selector="#password"/> <element name="confirmPasswordField" type="input" selector="#password-confirmation"/> <element name="createAccountButton" type="button" selector="button.action.submit.primary" timeout="30"/> </section> + <section name="StoreFrontCustomerAdvancedAttributesSection"> + <element name="textFieldAttribute" type="input" selector="//input[@id='{{var}}']" parameterized="true" /> + <element name="textAreaAttribute" type="input" selector="//textarea[@id='{{var}}']" parameterized="true" /> + <element name="multiLineFirstAttribute" type="input" selector="//input[@id='{{var}}_0']" parameterized="true" /> + <element name="multiLineSecondAttribute" type="input" selector="//input[@id='{{var}}_1']" parameterized="true" /> + <element name="datedAttribute" type="input" selector="//input[@id='{{var}}']" parameterized="true" /> + <element name="dropDownAttribute" type="select" selector="//select[@id='{{var}}']" parameterized="true" /> + <element name="dropDownOptionAttribute" type="text" selector="//*[@id='{{var}}']/option[2]" parameterized="true" /> + <element name="multiSelectFirstOptionAttribute" type="text" selector="//select[@id='{{var}}']/option[3]" parameterized="true" /> + <element name="yesNoAttribute" type="select" selector="//select[@id='{{var}}']" parameterized="true" /> + <element name="yesNoOptionAttribute" type="select" selector="//select[@id='{{var}}']/option[2]" parameterized="true" /> + <element name="selectedOption" type="text" selector="//select[@id='{{var}}']/option[@selected='selected']" parameterized="true"/> + </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerDashboardAccountInformationSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerDashboardAccountInformationSection.xml index 70d1bb6675db..93e7bf71b089 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 000000000000..a9859cf58751 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerLoginMessagesSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCustomerLoginMessagesSection"> + <element name="successMessage" type="text" selector=".message-success"/> + <element name="errorMessage" type="text" selector=".message-error"/> + <element name="messageByType" type="block" selector="#maincontent .message-{{messageType}}" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerMessagesSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerMessagesSection.xml new file mode 100644 index 000000000000..07d044921c8e --- /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/StorefrontCustomerOrderSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderSection.xml index 253a6a83c943..e8b11b27ddc7 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderSection.xml @@ -9,6 +9,7 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontCustomerOrderSection"> + <element name="isMyOrdersSection" type="text" selector="//*[@class='page-title']//*[contains(text(), 'My Orders')]"/> <element name="productCustomOptions" type="text" selector="//strong[contains(@class, 'product-item-name') and normalize-space(.)='{{var1}}']/following-sibling::*[contains(@class, 'item-options')]/dt[normalize-space(.)='{{var2}}']/following-sibling::dd[normalize-space(.)='{{var3}}']" parameterized="true"/> <element name="productCustomOptionsFile" type="text" selector="//strong[contains(@class, 'product-item-name') and normalize-space(.)='{{var1}}']/following-sibling::*[contains(@class, 'item-options')]/dt[normalize-space(.)='{{var2}}']/following-sibling::dd[contains(.,'{{var3}}')]" parameterized="true"/> <element name="productCustomOptionsLink" type="text" selector="//strong[contains(@class, 'product-item-name') and normalize-space(.)='{{var1}}']/following-sibling::*[contains(@class, 'item-options')]/dt[normalize-space(.)='{{var2}}']/following-sibling::dd//a[text() = '{{var3}}']" parameterized="true"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderViewSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderViewSection.xml index 53b07b2b6a51..f831aabddd4e 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderViewSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderViewSection.xml @@ -13,5 +13,8 @@ <element name="myOrdersTable" type="text" selector="#my-orders-table"/> <element name="subtotal" type="text" selector=".subtotal .amount"/> <element name="paymentMethod" type="text" selector=".payment-method dt"/> + <element name="printOrderLink" type="text" selector="a.action.print" timeout="30"/> + <element name="shippingAddress" type="text" selector=".box.box-order-shipping-address"/> + <element name="billingAddress" type="text" selector=".box.box-order-billing-address"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSidebarSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSidebarSection.xml new file mode 100644 index 000000000000..407c6480e9dd --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSidebarSection.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="StorefrontCustomerSidebarSection"> + <element name="sidebarTab" type="text" selector="//div[@id='block-collapsible-nav']//a[text()='{{tabName}}']" parameterized="true"/> + <element name="sidebarCurrentTab" type="text" selector="//div[@id='block-collapsible-nav']//*[contains(text(), '{{var}}')]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml index 8480dc6e9d2a..7bc057b8be7b 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml @@ -12,5 +12,12 @@ <element name="emailField" type="input" selector="#email"/> <element name="passwordField" type="input" selector="#pass"/> <element name="signInAccountButton" type="button" selector="#send2" timeout="30"/> + <element name="forgotPasswordLink" type="button" selector=".action.remind" timeout="10"/> + </section> + <section name="StorefrontCustomerSignInPopupFormSection"> + <element name="errorMessage" type="input" selector="[data-ui-id='checkout-cart-validationmessages-message-error']"/> + <element name="email" type="input" selector="#customer-email"/> + <element name="password" type="input" selector="#pass"/> + <element name="signIn" type="button" selector="#send2" timeout="30"/> </section> </sections> 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 000000000000..bdae69c425db --- /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 06b82db767ab..3610532c5356 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontPanelHeaderSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontPanelHeaderSection.xml @@ -9,7 +9,15 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontPanelHeaderSection"> + <!-- Element name="WelcomeMessage" Deprecated due to incorrect naming convention please use name="welcomeMessage" --> <element name="WelcomeMessage" type="text" selector=".greet.welcome span"/> - <element name="createAnAccountLink" type="select" selector=".panel.header li:nth-child(3)" timeout="30"/> + + <element name="welcomeMessage" type="text" selector="header>.panel .greet.welcome" /> + <element name="createAnAccountLink" type="select" selector="//div[@class='panel wrapper']//li/a[contains(.,'Create an Account')]" timeout="30"/> + <element name="notYouLink" type="button" selector=".greet.welcome span a"/> + <element name="customerWelcome" type="text" selector=".panel.header .greet.welcome"/> + <element name="customerWelcomeMenu" type="text" selector=".panel.header .customer-welcome .customer-name"/> + <element name="customerLoginLink" type="button" selector=".panel.header .header.links .authorization-link a" timeout="30"/> + <element name="customerLogoutLink" type="text" selector=".panel.header .customer-welcome .customer-menu .authorization-link a" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/SwitchAccountSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/SwitchAccountSection.xml similarity index 78% rename from app/code/Magento/Braintree/Test/Mftf/Section/SwitchAccountSection.xml rename to app/code/Magento/Customer/Test/Mftf/Section/SwitchAccountSection.xml index 3a07cbc6dd14..4442e317694e 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Section/SwitchAccountSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/SwitchAccountSection.xml @@ -7,8 +7,7 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> - + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="LoginFormSection"> <element name="username" type="input" selector="#username"/> <element name="password" type="input" selector="#login"/> @@ -19,6 +18,4 @@ <element name="admin" type="button" selector=".admin__action-dropdown-text"/> <element name="logout" type="button" selector="//*[contains(text(), 'Sign Out')]"/> </section> - </sections> - diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminAddNewDefaultBillingShippingCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminAddNewDefaultBillingShippingCustomerAddressTest.xml new file mode 100644 index 000000000000..fba7ebd2d4d5 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminAddNewDefaultBillingShippingCustomerAddressTest.xml @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminAddNewDefaultBillingShippingCustomerAddressTest"> + <annotations> + <stories value="Add new default billing/shipping customer address"/> + <title value="Add new default billing/shipping customer address"/> + <description value="Add new default billing/shipping customer address on customer addresses tab"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-94814"/> + <group value="customer"/> + </annotations> + <before> + <createData entity="Simple_US_Customer_Multiple_Addresses_No_Default_Address" stepKey="customer"/> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- - + Step1. Login to admin and go to Customers > All Customers. + Step2. On *Customers* page choose customer from preconditions and open it to edit + Step3. Open *Addresses* tab on edit customer page and press *Add New Address* button + <!- --> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> + <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="openEditCustomerPage"> + <argument name="customer" value="Simple_US_Customer_Multiple_Addresses_No_Default_Address"/> + </actionGroup> + <click selector="{{AdminEditCustomerInformationSection.addresses}}" stepKey="openAddressesTab"/> + <waitForPageLoad stepKey="waitForAddresses"/> + <conditionalClick selector="{{AdminCustomerAddressFiltersSection.clearAll}}" dependentSelector="{{AdminCustomerAddressFiltersSection.clearAll}}" visible="true" stepKey="clickOnButtonToRemoveFiltersIfPresent"/> + <seeElement selector="{{AdminCustomerAddressesDefaultBillingSection.address}}" stepKey="seeDefaultBillingAddressSectionBeforeChangingDefaultAddress"/> + <see userInput="The customer does not have default billing address" selector="{{AdminCustomerAddressesDefaultBillingSection.address}}" stepKey="assertThereIsNoDefaultBillingAddressSet"/> + <seeElement selector="{{AdminCustomerAddressesDefaultShippingSection.address}}" stepKey="seeDefaultShippingAddressSectionBeforeChangingDefaultAddress"/> + <see userInput="The customer does not have default shipping address" selector="{{AdminCustomerAddressesDefaultShippingSection.address}}" stepKey="assertThereIsNoDefaultShippingAddressSet"/> + <click selector="{{AdminCustomerAddressesSection.addNewAddress}}" stepKey="clickAddNewAddressButton"/> + <waitForPageLoad stepKey="waitForAddUpdateCustomerAddressForm"/> + <!--Step4. Fill all the fields with test data and press *Save* button--> + <click selector="{{AdminCustomerAddressesSection.defaultBillingAddress}}" stepKey="enableDefaultBillingAddress"/> + <click selector="{{AdminCustomerAddressesSection.defaultShippingAddress}}" stepKey="enableDefaultShippingAddress"/> + <fillField userInput="{{US_Address_TX.firstname}}" selector="{{AdminCustomerAddressesSection.firstNameForAddress}}" stepKey="fillFirstName"/> + <fillField userInput="{{US_Address_TX.lastname}}" selector="{{AdminCustomerAddressesSection.lastNameForAddress}}" stepKey="fillLastName"/> + <fillField userInput="{{US_Address_TX.company}}" selector="{{AdminCustomerAddressesSection.company}}" stepKey="fillCompany"/> + <fillField userInput="{{US_Address_TX.street[0]}}" selector="{{AdminCustomerAddressesSection.streetAddress}}" stepKey="fillStreet"/> + <fillField userInput="{{US_Address_TX.city}}" selector="{{AdminCustomerAddressesSection.city}}" stepKey="fillCity"/> + <click selector="{{AdminCustomerAddressesSection.country}}" stepKey="clickCountryToOpenListOfCountries"/> + <click selector="{{AdminCustomerAddressesSection.countryId(US_Address_TX.country_id)}}" stepKey="fillCountry"/> + <fillField userInput="{{US_Address_TX.postcode}}" selector="{{AdminCustomerAddressesSection.zip}}" stepKey="fillPostcode"/> + <fillField userInput="{{US_Address_TX.telephone}}" selector="{{AdminCustomerAddressesSection.phoneNumber}}" stepKey="fillTelephone"/> + <click selector="{{AdminCustomerAddressesSection.region}}" stepKey="clickRegionToOpenListOfRegions"/> + <click selector="{{AdminCustomerAddressesSection.regionId(US_Address_TX.state)}}" stepKey="fillRegion"/> + <click selector="{{AdminCustomerAddressesSection.saveAddress}}" stepKey="clickSaveCustomerAddressOnAddUpdateAddressForm"/> + <waitForPageLoad stepKey="waitForNewAddressIsCreated"/> + <see userInput="{{US_Address_TX.street[0]}}" selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" stepKey="assertDefaultBillingAddressIsChanged"/> + <see userInput="{{US_Address_TX.street[0]}}" selector="{{AdminCustomerAddressesDefaultShippingSection.addressDetails}}" stepKey="assertDefaultShippingAddressIsChanged"/> + <click selector="{{AdminCustomerAddressesDefaultBillingSection.editButton}}" stepKey="clickEditDefaultBillingAddress"/> + <waitForPageLoad stepKey="waitForCustomerAddressAddUpdateFormLoad"/> + <assertElementContainsAttribute selector="{{AdminCustomerAddressesSection.defaultBillingAddressCheckBox}}" attribute="value" expectedValue="1" stepKey="assertDefaultBillingIsEnabledCustomerAddressAddUpdateForm"/> + <assertElementContainsAttribute selector="{{AdminCustomerAddressesSection.defaultShippingAddressCheckBox}}" attribute="value" expectedValue="1" stepKey="assertDefaultShippingIsEnabledOnCustomerAddressAddUpdateForm"/> + </test> +</tests> 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 000000000000..36592ab38e91 --- /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/AdminCreateCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml index 9dd2127fa28b..78bae7ad60dd 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml @@ -19,6 +19,9 @@ <group value="customer"/> <group value="create"/> </annotations> + <before> + <magentoCLI command="indexer:reindex customer_grid" stepKey="reindexCustomerGrid"/> + </before> <after> <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> </after> @@ -27,17 +30,17 @@ <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomers"/> <waitForPageLoad stepKey="waitForLoad1"/> <click selector="{{AdminCustomerGridMainActionsSection.addNewCustomer}}" stepKey="clickCreateCustomer"/> - <waitForElement selector="{{AdminCustomerAccountInformationSection.firstName}}" stepKey="wait1"/> <fillField userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminCustomerAccountInformationSection.firstName}}" stepKey="fillFirstName"/> <fillField userInput="{{CustomerEntityOne.lastname}}" selector="{{AdminCustomerAccountInformationSection.lastName}}" stepKey="fillLastName"/> <fillField userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerAccountInformationSection.email}}" stepKey="fillEmail"/> <click selector="{{AdminCustomerMainActionsSection.saveButton}}" stepKey="saveCustomer"/> - <waitForElementNotVisible selector="div [data-role='spinner']" time="10" stepKey="waitForSpinner1"/> <seeElement selector="{{AdminCustomerMessagesSection.successMessage}}" stepKey="assertSuccessMessage"/> + <magentoCLI stepKey="reindex" command="indexer:reindex"/> + <reloadPage stepKey="reloadPage"/> + <waitForPageLoad stepKey="waitForLoad2"/> <click selector="{{AdminCustomerFiltersSection.filtersButton}}" stepKey="openFilter"/> <fillField userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerFiltersSection.emailInput}}" stepKey="filterEmail"/> <click selector="{{AdminCustomerFiltersSection.apply}}" stepKey="applyFilter"/> - <waitForElementNotVisible selector="div [data-role='spinner']" time="10" stepKey="waitForSpinner2"/> <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"/> 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 000000000000..cbc8b89d3f24 --- /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 000000000000..43f2aa7f8de9 --- /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 000000000000..872da149ed0b --- /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 000000000000..1b901a7b3e1c --- /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 000000000000..fe4bb3ee59e6 --- /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 000000000000..22ad60ff5de3 --- /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 000000000000..fc65a271a819 --- /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 000000000000..de4ab9ffaa12 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerTest.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateNewCustomerTest"> + <annotations> + <stories value="Create customer"/> + <title value="Create customer, new via backend"/> + <description value="Login as admin and create a new customer"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-5312"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + </before> + <after> + <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteCustomer"> + <argument name="email" value="{{CustomerEntityOne.email}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open New Customer Page --> + <amOnPage url="{{AdminNewCustomerPage.url}}" stepKey="navigateToNewCustomerPage"/> + <waitForPageLoad stepKey="waitToCustomerPageLoad"/> + <fillField userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminCustomerAccountInformationSection.firstName}}" stepKey="fillFirstName"/> + <fillField userInput="{{CustomerEntityOne.lastname}}" selector="{{AdminCustomerAccountInformationSection.lastName}}" stepKey="fillLastName"/> + <fillField userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerAccountInformationSection.email}}" stepKey="fillEmail"/> + <click selector="{{AdminCustomerMainActionsSection.saveButton}}" stepKey="saveCustomer"/> + <seeElement selector="{{AdminCustomerMessagesSection.successMessage}}" stepKey="assertSuccessMessage"/> + <reloadPage stepKey="reloadPage"/> + + <!--Filter the customer From grid--> + <actionGroup ref="AdminFilterCustomerByEmail" stepKey="filterTheCustomerByEmail"> + <argument name="email" value="{{CustomerEntityOne.email}}"/> + </actionGroup> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickOnEditButton1"/> + <waitForPageLoad stepKey="waitForCustomerEditPageToLoad"/> + + <!-- Assert Customer Title --> + <click selector="{{AdminCustomerAccountInformationSection.accountInformationButton}}" stepKey="clickOnAccountInformation"/> + <waitForPageLoad stepKey="waitForCustomerInformationPageToLoad"/> + <see stepKey="seeCustomerTitle" selector="{{AdminEditCustomerInformationSection.customerTitle}}" userInput="{{CustomerEntityOne.firstname}} {{CustomerEntityOne.lastname}} "/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersAllCustomersNavigateMenuTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersAllCustomersNavigateMenuTest.xml new file mode 100644 index 000000000000..76e4407675e4 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersAllCustomersNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCustomersAllCustomersNavigateMenuTest"> + <annotations> + <features value="Customer"/> + <stories value="Menu Navigation"/> + <title value="Admin customers all customers navigate menu test"/> + <description value="Admin should be able to navigate to Customers > All Customers"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14113"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToAllCustomerPage"> + <argument name="menuUiId" value="{{AdminMenuCustomers.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuCustomersAllCustomers.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuCustomersAllCustomers.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersCustomerGroupsNavigateMenuTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersCustomerGroupsNavigateMenuTest.xml new file mode 100644 index 000000000000..13a4b1c71433 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersCustomerGroupsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCustomersCustomerGroupsNavigateMenuTest"> + <annotations> + <features value="Customer"/> + <stories value="Menu Navigation"/> + <title value="Admin customers customer groups navigate menu test"/> + <description value="Admin should be able to navigate to Customers > Customer Groups"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14115"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToCustomerGroupsPage"> + <argument name="menuUiId" value="{{AdminMenuCustomers.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuCustomersCustomerGroups.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuCustomersCustomerGroups.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersNowOnlineNavigateMenuTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersNowOnlineNavigateMenuTest.xml new file mode 100644 index 000000000000..e9eb7803e01e --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersNowOnlineNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCustomersNowOnlineNavigateMenuTest"> + <annotations> + <features value="Customer"/> + <stories value="Menu Navigation"/> + <title value="Admin customers now online navigate menu test"/> + <description value="Admin should be able to navigate to Customers > Now Online"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14114"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToNowOnlinePage"> + <argument name="menuUiId" value="{{AdminMenuCustomers.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuCustomersNowOnline.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuCustomersNowOnline.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridTest.xml new file mode 100644 index 000000000000..4f501c27352b --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridTest.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="AdminDeleteCustomerAddressesFromTheGridTest"> + <annotations> + <title value="Admin delete customer addresses from the grid"/> + <description value="Admin delete customer addresses from the grid"/> + <features value="Module/ Customer"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-94850"/> + <stories value="MAGETWO-94346: Implement handling of large number of addresses on admin edit customer page"/> + <group value="customer"/> + </annotations> + + <before> + <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- - + Step1. Login to admin and go to Customers > All Customerts. + Step2. On *Customers* page choose customer from preconditions and open it to edit + Step3. On edit customer page open *Addresses* tab and find a grid with the additional addresses + <!- --> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> + <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="openEditCustomerPage"> + <argument name="customer" value="Simple_US_Customer_Multiple_Addresses"/> + </actionGroup> + <click selector="{{AdminEditCustomerInformationSection.addresses}}" stepKey="openAddressesTab"/> + <!--Step4. Click *Select* link in *Actions* column for target additional address--> + <conditionalClick selector="{{AdminCustomerAddressFiltersSection.clearAll}}" dependentSelector="{{AdminCustomerAddressFiltersSection.clearAll}}" visible="true" stepKey="clickOnButtonToRemoveFiltersIfPresent"/> + <click selector="{{AdminCustomerAddressesGridSection.firstRowSelectLink}}" stepKey="clickOnSelectLinkInFirstRow"/> + <!--Step5. Click *Delete*--> + <click selector="{{AdminCustomerAddressesGridSection.firstRowDeleteLink}}" stepKey="chooseDeleteOptionInFirstRow"/> + <waitForPageLoad stepKey="waitForCustomerAddressesGridPageLoad1"/> + <!--Step6. Press *Ok* button on the pop-up--> + <click selector="{{AdminCustomerAddressesGridActionsSection.ok}}" stepKey="clickOkOnPopup"/> + <waitForPageLoad stepKey="waitForCustomerAddressesGridPageLoad2"/> + <seeNumberOfElements userInput="1" selector="{{AdminCustomerAddressesGridSection.rowsInGrid}}" stepKey="seeOnlyOneCustomerAddressesInGrid"/> + <!--Step7. Delete last customer address--> + <click selector="{{AdminCustomerAddressesGridSection.firstRowSelectLink}}" stepKey="clickOnSelectLinkInFirstRow2"/> + <click selector="{{AdminCustomerAddressesGridSection.firstRowDeleteLink}}" stepKey="chooseDeleteOptionInFirstRow2"/> + <waitForPageLoad stepKey="waitForCustomerAddressesGridPageLoad3"/> + <click selector="{{AdminCustomerAddressesGridActionsSection.ok}}" stepKey="clickOkOnPopup2"/> + <waitForPageLoad stepKey="waitForCustomerAddressesGridPageLoad4"/> + <see userInput="We couldn't find any records." selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" stepKey="checkThatCustomerAddressesGridHasNoRecords"/> +</test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridViaMassActionsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridViaMassActionsTest.xml new file mode 100644 index 000000000000..a703c5a7c5d9 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridViaMassActionsTest.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="AdminDeleteCustomerAddressesFromTheGridViaMassActionsTest"> + <annotations> + <title value="Admin delete customer addresses from the grid via mass actions"/> + <description value="Admin delete customer addresses from the grid via mass actions"/> + <features value="Module/ Customer"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-94951"/> + <stories value="MAGETWO-94346: Implement handling of large number of addresses on admin edit customer page"/> + <group value="customer"/> + </annotations> + + <before> + <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- - + Step1. Login to admin and go to Customers > All Customerts. + Step2. On *Customers* page choose customer from preconditions and open it to edit + Step3. On edit customer page open *Addresses* tab and find a grid with the additional addresses + <!- --> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> + <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="openEditCustomerPage"> + <argument name="customer" value="Simple_US_Customer_Multiple_Addresses"/> + </actionGroup> + <click selector="{{AdminEditCustomerInformationSection.addresses}}" stepKey="openAddressesTab"/> + <conditionalClick selector="{{AdminCustomerAddressFiltersSection.clearAll}}" dependentSelector="{{AdminCustomerAddressFiltersSection.clearAll}}" visible="true" stepKey="clickOnButtonToRemoveFiltersIfPresent"/> + <!-- - + Step4. Check checkboxes for several addresses open *Actions* dropdown at the top of addresses grid and select action *Delete* + Step5. Press *Ok* button on the pop-up + <!- --> + <click selector="{{AdminCustomerAddressesGridSection.firstRowCheckbox}}" stepKey="tickFirstRowCustomerAddressCheckbox"/> + <click selector="{{AdminCustomerAddressesGridSection.secondRowCheckbox}}" stepKey="tickSecondRowCustomerAddressCheckbox"/> + <click selector="{{AdminCustomerAddressesGridActionsSection.actions}}" stepKey="openActionsDropdown"/> + <click selector="{{AdminCustomerAddressesGridActionsSection.delete}}" stepKey="chooseDeleteOption"/> + <waitForPageLoad stepKey="waitForCustomerAddressesGridPageLoad1"/> + <click selector="{{AdminCustomerAddressesGridActionsSection.ok}}" stepKey="clickOkOnPopup"/> + <waitForPageLoad stepKey="waitForCustomerAddressesGridPageLoad2"/> + <see userInput="We couldn't find any records." selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" stepKey="checkThatCustomerAddressesGridHasNoRecords"/> +</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 000000000000..7fef916fc458 --- /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/AdminDeleteDefaultBillingCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteDefaultBillingCustomerAddressTest.xml new file mode 100644 index 000000000000..bb455677d5e9 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteDefaultBillingCustomerAddressTest.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="AdminDeleteDefaultBillingCustomerAddressTest"> + <annotations> + <title value="Admin delete default billing customer address"/> + <description value="Admin delete default billing customer address"/> + <features value="Module/ Customer"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-94816"/> + <stories value="MAGETWO-94346: Implement handling of large number of addresses on admin edit customer page"/> + <group value="customer"/> + </annotations> + + <before> + <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- - + Step1. Login to admin and go to Customers > All Customers. + Step2. On *Customers* page choose customer from preconditions and open it to edit + <!- --> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> + <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="openEditCustomerPage"> + <argument name="customer" value="Simple_US_Customer_Multiple_Addresses"/> + </actionGroup> + <!--Step3. Open *Addresses* tab on edit customer page and click *edit* link near *Default Billing Address* block--> + <click selector="{{AdminEditCustomerInformationSection.addresses}}" stepKey="openAddressesTab"/> + <conditionalClick selector="{{AdminCustomerAddressFiltersSection.clearAll}}" dependentSelector="{{AdminCustomerAddressFiltersSection.clearAll}}" visible="true" stepKey="clickOnButtonToRemoveFiltersIfPresent"/> + <seeNumberOfElements userInput="2" selector="{{AdminCustomerAddressesGridSection.rowsInGrid}}" stepKey="seeTwoCustomerAddressesInGrid"/> + <click selector="{{AdminCustomerAddressesDefaultBillingSection.editButton}}" stepKey="clickEditNearDefaultBillingAddress"/> + <waitForPageLoad stepKey="waitForDefaultBillingAddressPopupLoad"/> + <!--Step4. Press *Delete* button--> + <click selector="{{AdminCustomerAddressesSection.deleteButton}}" stepKey="clickDeleteButton"/> + <waitForPageLoad stepKey="waitForConfirmationPopupLoad"/> + <click selector="{{AdminCustomerAddressesSection.ok}}" stepKey="clickOkOnPopup"/> + <waitForPageLoad stepKey="waitForDefaultBillingAddressPopupLoad2"/> + <seeNumberOfElements userInput="1" selector="{{AdminCustomerAddressesGridSection.rowsInGrid}}" stepKey="seeOnlyOneCustomerAddressesInGrid"/> + <dontSee userInput="{{US_Address_NY.street[0]}}" stepKey="assertDefaultBillingIsSet"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminEditDefaultBillingShippingCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminEditDefaultBillingShippingCustomerAddressTest.xml new file mode 100644 index 000000000000..df317c8bf701 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminEditDefaultBillingShippingCustomerAddressTest.xml @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminEditDefaultBillingShippingCustomerAddressTest"> + <annotations> + <stories value="Edit default billing/shipping customer address"/> + <title value="Edit default billing/shipping customer address"/> + <description value="Edit default billing/shipping customer address on customer addresses tab"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-94815"/> + <group value="customer"/> + </annotations> + <before> + <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="customer"/> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- - + Step1. Login to admin and go to Customers > All Customers. + Step2. On *Customers* page choose customer from preconditions and open it to edit + Step3. Open *Addresses* tab on edit customer page and press *Add New Address* button + <!- --> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> + <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="openEditCustomerPage"> + <argument name="customer" value="Simple_US_Customer_Multiple_Addresses"/> + </actionGroup> + <click selector="{{AdminEditCustomerInformationSection.addresses}}" stepKey="openAddressesTab"/> + <waitForPageLoad stepKey="waitForAddresses"/> + <conditionalClick selector="{{AdminCustomerAddressFiltersSection.clearAll}}" dependentSelector="{{AdminCustomerAddressFiltersSection.clearAll}}" visible="true" stepKey="clickOnButtonToRemoveFiltersIfPresent"/> + <seeElement selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" stepKey="seeDefaultBillingAddressSectionBeforeChangingDefaultAddress"/> + <see userInput="{{US_Address_NY.street[0]}}" selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" stepKey="assertDefaultBillingAddressIsSetBeforeChangingDefaultAddress"/> + <seeElement selector="{{AdminCustomerAddressesDefaultShippingSection.addressDetails}}" stepKey="seeDefaultShippingAddressSectionBeforeChangingDefaultAddress"/> + <see userInput="{{US_Address_NY.street[0]}}" selector="{{AdminCustomerAddressesDefaultShippingSection.addressDetails}}" stepKey="assertDefaultShippingAddressIsSetBeforeChangingDefaultAddress"/> + <click selector="{{AdminCustomerAddressesSection.addNewAddress}}" stepKey="clickAddNewAddressButton"/> + <waitForPageLoad stepKey="waitForAddUpdateCustomerAddressForm"/> + <!--Step4. Fill all the fields with test data and press *Save* button--> + <click selector="{{AdminCustomerAddressesSection.defaultBillingAddress}}" stepKey="enableDefaultBillingAddress"/> + <click selector="{{AdminCustomerAddressesSection.defaultShippingAddress}}" stepKey="enableDefaultShippingAddress"/> + <fillField userInput="{{US_Address_TX.firstname}}" selector="{{AdminCustomerAddressesSection.firstNameForAddress}}" stepKey="fillFirstName"/> + <fillField userInput="{{US_Address_TX.lastname}}" selector="{{AdminCustomerAddressesSection.lastNameForAddress}}" stepKey="fillLastName"/> + <fillField userInput="{{US_Address_TX.company}}" selector="{{AdminCustomerAddressesSection.company}}" stepKey="fillCompany"/> + <fillField userInput="{{US_Address_TX.street[0]}}" selector="{{AdminCustomerAddressesSection.streetAddress}}" stepKey="fillStreet"/> + <fillField userInput="{{US_Address_TX.city}}" selector="{{AdminCustomerAddressesSection.city}}" stepKey="fillCity"/> + <click selector="{{AdminCustomerAddressesSection.country}}" stepKey="clickCountryToOpenListOfCountries"/> + <click selector="{{AdminCustomerAddressesSection.countryId(US_Address_TX.country_id)}}" stepKey="fillCountry"/> + <fillField userInput="{{US_Address_TX.postcode}}" selector="{{AdminCustomerAddressesSection.zip}}" stepKey="fillPostcode"/> + <fillField userInput="{{US_Address_TX.telephone}}" selector="{{AdminCustomerAddressesSection.phoneNumber}}" stepKey="fillTelephone"/> + <click selector="{{AdminCustomerAddressesSection.region}}" stepKey="clickRegionToOpenListOfRegions"/> + <click selector="{{AdminCustomerAddressesSection.regionId(US_Address_TX.state)}}" stepKey="fillRegion"/> + <click selector="{{AdminCustomerAddressesSection.saveAddress}}" stepKey="clickSaveCustomerAddressOnAddUpdateAddressForm"/> + <waitForPageLoad stepKey="waitForNewAddressIsCreated"/> + <see userInput="{{US_Address_TX.street[0]}}" selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" stepKey="assertDefaultBillingAddressIsChanged"/> + <see userInput="{{US_Address_TX.street[0]}}" selector="{{AdminCustomerAddressesDefaultShippingSection.addressDetails}}" stepKey="assertDefaultShippingAddressIsChanged"/> + <click selector="{{AdminCustomerAddressesDefaultBillingSection.editButton}}" stepKey="clickEditDefaultBillingAddress"/> + <waitForPageLoad stepKey="waitForCustomerAddressAddUpdateFormLoad"/> + <assertElementContainsAttribute selector="{{AdminCustomerAddressesSection.defaultBillingAddressCheckBox}}" attribute="value" expectedValue="1" stepKey="assertDefaultBillingIsEnabledCustomerAddressAddUpdateForm"/> + <assertElementContainsAttribute selector="{{AdminCustomerAddressesSection.defaultShippingAddressCheckBox}}" attribute="value" expectedValue="1" stepKey="assertDefaultShippingIsEnabledOnCustomerAddressAddUpdateForm"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminResetCustomerPasswordTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminResetCustomerPasswordTest.xml new file mode 100644 index 000000000000..fb67838e941b --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminResetCustomerPasswordTest.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="AdminResetCustomerPasswordTest"> + <annotations> + <stories value="Reset password"/> + <title value="Admin should be able to reset customer password"/> + <description value="Admin should be able to reset customer password"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-30875"/> + <group value="customer"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="customer"/> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!--Edit customer info--> + <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="OpenEditCustomerFrom"> + <argument name="customer" value="$$customer$$"/> + </actionGroup> + <click selector="{{AdminCustomerMainActionsSection.resetPassword}}" stepKey="resetPassword"/> + <see userInput="The customer will receive an email with a link to reset password." stepKey="messageThatLinkToPasswordResetIsSent"/> + </test> +</tests> + + diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminSearchCustomerAddressByKeywordTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminSearchCustomerAddressByKeywordTest.xml new file mode 100644 index 000000000000..9f1c5e8cd923 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminSearchCustomerAddressByKeywordTest.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="AdminSearchCustomerAddressByKeywordTest"> + <annotations> + <title value="Admin search customer address by keyword"/> + <description value="Admin search customer address by keyword"/> + <features value="Module/ Customer"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-94954"/> + <stories value="MAGETWO-94346: Implement handling of large number of addresses on admin edit customer page"/> + <group value="customer"/> + </annotations> + + <before> + <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- - + Step1. Login to admin and go to Customers > All Customerts. + Step2. On *Customers* page choose customer from preconditions and open it to edit + Step3. On edit customer page open *Addresses* tab and find a grid with the additional addresses + <!- --> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> + <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="openEditCustomerPage"> + <argument name="customer" value="Simple_US_Customer_Multiple_Addresses"/> + </actionGroup> + <click selector="{{AdminEditCustomerInformationSection.addresses}}" stepKey="openAddressesTab"/> + <conditionalClick selector="{{AdminCustomerAddressFiltersSection.clearAll}}" dependentSelector="{{AdminCustomerAddressFiltersSection.clearAll}}" visible="true" stepKey="clickOnButtonToRemoveFiltersIfPresent"/> + <!--Step4. Fill *Search by keyword* filed with the query and press enter or clock on the magnifier icon--> + <fillField userInput="{{US_Address_NY.street[0]}}" selector="{{AdminCustomerAddressesGridActionsSection.search}}" stepKey="FillCustomerAddressStreetInSearchByKeyword"/> + <pressKey parameterArray="[\Facebook\WebDriver\WebDriverKeys::ENTER]" selector="{{AdminCustomerAddressesGridActionsSection.search}}" stepKey="pressEnterKey"/> + <waitForPageLoad stepKey="waitForCustomerAddressesGridPageLoad"/> + <seeNumberOfElements userInput="1" selector="{{AdminCustomerAddressesGridSection.rowsInGrid}}" stepKey="seeOnlyOneCustomerAddressesInGrid"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultBillingAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultBillingAddressTest.xml new file mode 100644 index 000000000000..db8d4e1ee1ee --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultBillingAddressTest.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="AdminSetCustomerDefaultBillingAddressTest"> + <annotations> + <stories value="Set customer default billing address"/> + <title value="Admin should be able to set customer default billing address"/> + <description value="Admin should be able to set customer default billing address from customer addresses grid row actions"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-94952"/> + <group value="customer"/> + </annotations> + <before> + <createData entity="Simple_US_Customer_Multiple_Addresses_No_Default_Address" stepKey="customer"/> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- - + Step1. Login to admin and go to Customers > All Customers. + Step2. On *Customers* page choose customer from preconditions and open it to edit + Step3. On edit customer page open *Addresses* tab and find a grid with the additional addresses + <!- --> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> + <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="openEditCustomerPage"> + <argument name="customer" value="Simple_US_Customer_Multiple_Addresses_No_Default_Address"/> + </actionGroup> + <click selector="{{AdminEditCustomerInformationSection.addresses}}" stepKey="openAddressesTab"/> + <waitForPageLoad stepKey="waitForAddresses"/> + <conditionalClick selector="{{AdminCustomerAddressFiltersSection.clearAll}}" dependentSelector="{{AdminCustomerAddressFiltersSection.clearAll}}" visible="true" stepKey="clickOnButtonToRemoveFiltersIfPresent"/> + <fillField userInput="{{US_Address_NY_Not_Default_Address.street[0]}}" selector="{{AdminCustomerAddressesGridActionsSection.search}}" stepKey="fillCustomerAddressStreetInSearchByKeyword"/> + <pressKey parameterArray="[\Facebook\WebDriver\WebDriverKeys::ENTER]" selector="{{AdminCustomerAddressesGridActionsSection.search}}" stepKey="pressEnterKey"/> + <waitForPageLoad stepKey="waitForCustomerAddressesGridPageLoad"/> + <see userInput="The customer does not have default billing address" selector="{{AdminCustomerAddressesDefaultBillingSection.address}}" stepKey="assertThatThereIsNoDefaultBillingAddress"/> + <seeNumberOfElements userInput="1" selector="{{AdminCustomerAddressesGridSection.rowsInGrid}}" stepKey="seeOnlyOneCustomerAddressesInGrid"/> + <!--Step4. Click *Select* link in *Actions* column for target additional address--> + <click selector="{{AdminCustomerAddressesGridSection.firstRowSelectLink}}" stepKey="clickSelectElementFromRow" /> + <!--Step4. Click *Set as default billing*--> + <click selector="{{AdminCustomerAddressesGridSection.firstRowSetAsDefaultBillingLink}}" stepKey="clickOnSetAddressAsDefaultBilling"/> + <!--Step5. Press *Ok* button on the pop-up--> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmSetAddressAsDefaultBilling"/> + <seeElement selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" stepKey="seeDefaultBillingAddressSection"/> + <see userInput="{{US_Address_NY_Not_Default_Address.street[0]}}" selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" stepKey="assertDefaultBillingAddressIsSet"/> + <click selector="{{AdminCustomerAddressesDefaultBillingSection.editButton}}" stepKey="clickEditDefaultBillingAddress"/> + <waitForPageLoad stepKey="waitForCustomerAddressAddUpdateFormLoad"/> + <assertElementContainsAttribute selector="{{AdminCustomerAddressesSection.defaultBillingAddressCheckBox}}" attribute="value" expectedValue="1" stepKey="assertDefaultBillingCheckboxIsCheckedOnCustomerAddressAddUpdateForm"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultShippingAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultShippingAddressTest.xml new file mode 100644 index 000000000000..6e8321817690 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultShippingAddressTest.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="AdminSetCustomerDefaultShippingAddressTest"> + <annotations> + <stories value="Set customer default shipping address"/> + <title value="Admin should be able to set customer default shipping address"/> + <description value="Admin should be able to set customer default shipping address from customer addresses grid row actions"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-94953"/> + <group value="customer"/> + </annotations> + <before> + <createData entity="Simple_US_Customer_Multiple_Addresses_No_Default_Address" stepKey="customer"/> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- - + Step1. Login to admin and go to Customers > All Customers. + Step2. On *Customers* page choose customer from preconditions and open it to edit + Step3. On edit customer page open *Addresses* tab and find a grid with the additional addresses + <!- --> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> + <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="openEditCustomerPage"> + <argument name="customer" value="Simple_US_Customer_Multiple_Addresses_No_Default_Address"/> + </actionGroup> + <click selector="{{AdminEditCustomerInformationSection.addresses}}" stepKey="openAddressesTab"/> + <waitForPageLoad stepKey="waitForAddresses"/> + <conditionalClick selector="{{AdminCustomerAddressFiltersSection.clearAll}}" dependentSelector="{{AdminCustomerAddressFiltersSection.clearAll}}" visible="true" stepKey="clickOnButtonToRemoveFiltersIfPresent"/> + <fillField userInput="{{US_Address_NY_Not_Default_Address.street[0]}}" selector="{{AdminCustomerAddressesGridActionsSection.search}}" stepKey="fillCustomerAddressStreetInSearchByKeyword"/> + <pressKey parameterArray="[\Facebook\WebDriver\WebDriverKeys::ENTER]" selector="{{AdminCustomerAddressesGridActionsSection.search}}" stepKey="pressEnterKey"/> + <waitForPageLoad stepKey="waitForCustomerAddressesGridPageLoad"/> + <see userInput="The customer does not have default shipping address" selector="{{AdminCustomerAddressesDefaultShippingSection.address}}" stepKey="assertThatThereIsNoDefaultShippingAddress"/> + <seeNumberOfElements userInput="1" selector="{{AdminCustomerAddressesGridSection.rowsInGrid}}" stepKey="seeOnlyOneCustomerAddressesInGrid"/> + <!--Step4. Click *Select* link in *Actions* column for target additional address--> + <click selector="{{AdminCustomerAddressesGridSection.firstRowSelectLink}}" stepKey="clickSelectElementFromRow" /> + <!--Step4. Click *Set as default shipping*--> + <click selector="{{AdminCustomerAddressesGridSection.firstRowSetAsDefaultShippingLink}}" stepKey="clickOnSetAddressAsDefaultShipping"/> + <!--Step5. Press *Ok* button on the pop-up--> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmSetAddressAsDefaultShipping"/> + <seeElement selector="{{AdminCustomerAddressesDefaultShippingSection.addressDetails}}" stepKey="seeDefaultShippingAddressSection"/> + <see userInput="{{US_Address_NY_Not_Default_Address.street[0]}}" selector="{{AdminCustomerAddressesDefaultShippingSection.addressDetails}}" stepKey="assertDefaultShippingAddressIsSet"/> + <click selector="{{AdminCustomerAddressesDefaultShippingSection.editButton}}" stepKey="clickEditDefaultShippingAddress"/> + <waitForPageLoad stepKey="waitForCustomerAddressAddUpdateFormLoad"/> + <assertElementContainsAttribute selector="{{AdminCustomerAddressesSection.defaultShippingAddressCheckBox}}" attribute="value" expectedValue="1" stepKey="assertDefaultShippingCheckboxIsCheckedOnCustomerAddressAddUpdateForm"/> + </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 000000000000..f58f23dee423 --- /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 000000000000..7dab6eefde8e --- /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 000000000000..bfb47dc9e191 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressRequiredFieldsTest.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminVerifyCustomerAddressRequiredFieldsTest"> + <annotations> + <stories value="Create customer"/> + <title value="Create customer, verify required fields on Addresses tab"/> + <description value="Login as admin and verify required fields on Address tab"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-5315"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="Simple_Customer_Without_Address" stepKey="createCustomer"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open Created Customer --> + <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="editCustomerForm"> + <argument name="customer" value="Simple_Customer_Without_Address"/> + </actionGroup> + <click selector="{{AdminCustomerAccountInformationSection.addressesButton}}" stepKey="openAddressesTab"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{AdminEditCustomerAddressesSection.addNewAddress}}" stepKey="ClickOnAddNewAddressButton"/> + <waitForPageLoad stepKey="waitForAdressPageToLoad"/> + <click selector="{{AdminEditCustomerAddressesSection.save}}" stepKey="clickOnSaveButton"/> + <waitForPageLoad stepKey="waitForPageToBeSaved"/> + + <!--Assert Required Field Messages --> + <seeElement selector="{{AdminCustomerAddressesSection.streetRequiredMessage}}" stepKey="seeStreetRequiredMessage"/> + <seeElement selector="{{AdminCustomerAddressesSection.cityRequiredMessage}}" stepKey="seeCityRequiredMessage"/> + <scrollTo selector="{{AdminEditCustomerAddressesSection.phone}}" x="0" y="-80" stepKey="scrollToPhone"/> + <seeElement selector="{{AdminCustomerAddressesSection.countryRequiredMessage}}" stepKey="seeCountryRequiredMessage"/> + <seeElement selector="{{AdminCustomerAddressesSection.postcodeRequiredMessage}}" stepKey="seePostcodeRequiredMessage"/> + <seeElement selector="{{AdminCustomerAddressesSection.phoneNumberRequiredMessage}}" stepKey="seePhoneNumberRequiredMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml new file mode 100644 index 000000000000..f39394ef312e --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml @@ -0,0 +1,118 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AllowedCountriesRestrictionApplyOnBackendTest"> + <annotations> + <title value="Country filter on Customers page when allowed countries restriction for a default website is applied"/> + <description value="Country filter on Customers page when allowed countries restriction for a default website is applied"/> + <features value="Customer"/> + <severity value="MAJOR"/> + <testCaseId value="MC-6441"/> + <useCaseId value="MAGETWO-91523"/> + <group value="customer"/> + </annotations> + <before> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="LoginActionGroup" stepKey="login"/> + <!--Create new website,store and store view--> + <comment userInput="Create new website,store and store view" stepKey="createWebsite"/> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="goToAdminSystemStorePage"/> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="adminCreateNewWebsite"> + <argument name="newWebsiteName" value="{{NewWebSiteData.name}}"/> + <argument name="websiteCode" value="{{NewWebSiteData.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="adminCreateNewStore"> + <argument name="website" value="{{NewWebSiteData.name}}"/> + <argument name="storeGroupName" value="{{NewStoreData.name}}"/> + <argument name="storeGroupCode" value="{{NewStoreData.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="adminCreateNewStoreView"> + <argument name="StoreGroup" value="NewStoreData"/> + </actionGroup> + <!--Set account sharing option - Default value is 'Per Website'--> + <comment userInput="Set account sharing option - Default value is 'Per Website'" stepKey="setAccountSharingOption"/> + <createData entity="CustomerAccountSharingDefault" stepKey="setToAccountSharingToDefault"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + </before> + <after> + <!--delete all created data and set main website country options to default--> + <comment userInput="Delete all created data and set main website country options to default" stepKey="resetConfigToDefault"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteTestWebsite"> + <argument name="websiteName" value="{{NewWebSiteData.name}}"/> + </actionGroup> + <actionGroup ref="NavigateToConfigurationGeneralPage" stepKey="navigateToConfigGeneralPage2"/> + <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="adminSwitchWebsiteActionGroup"> + <argument name="website" value="_defaultWebsite"/> + </actionGroup> + <actionGroup ref="SetWebsiteCountryOptionsToDefaultActionGroup" stepKey="setCountryOptionsToDefault"/> + <createData entity="CustomerAccountSharingSystemValue" stepKey="setAccountSharingToSystemValue"/> + <actionGroup ref="logout" stepKey="logout"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </after> + <!--Check that all countries are allowed initially and get amount--> + <comment userInput="Check that all countries are allowed initially and get amount" stepKey="checkAllCountriesAreAllowed"/> + <actionGroup ref="NavigateToConfigurationGeneralPage" stepKey="navigateToConfigGeneralPage"/> + <createData entity="DisableAdminAccountAllowCountry" stepKey="setDefaultValueForAllowCountries"/> + <executeJS function="return document.querySelectorAll('{{CountryOptionsSection.allowedCountries}} option').length" stepKey="countriesAmount"/> + <!-- Create customer for US --> + <comment userInput="Create customer for US" stepKey="createUSCustomer"/> + <createData entity="Simple_US_CA_Customer" stepKey="createCustomer"/> + <!-- Switch to first website, allow only Canada and set Canada as default country --> + <comment userInput="Switch to first website, allow only Canada and set Canada as default country" stepKey="setCanadaAsDefaultCountry"/> + <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="adminSwitchWebsiteActionGroup"> + <argument name="website" value="_defaultWebsite"/> + </actionGroup> + <conditionalClick selector="{{CountryOptionsSection.countryOptions}}" dependentSelector="{{CountryOptionsSection.countryOptionsOpen}}" visible="false" stepKey="clickOnStoreInformation2"/> + <waitForElementVisible selector="{{CountryOptionsSection.allowedCountries}}" stepKey="waitAllowedCountriesToBeVisible"/> + <uncheckOption selector="{{CountryOptionsSection.generalCountryDefaultInherit}}" stepKey="uncheckCheckbox1"/> + <selectOption selector="{{CountryOptionsSection.generalCountryDefault}}" userInput="Canada" stepKey="chooseCanada1"/> + <uncheckOption selector="{{CountryOptionsSection.generalCountryAllowInherit}}" stepKey="uncheckCheckbox2"/> + <selectOption selector="{{CountryOptionsSection.allowedCountries}}" userInput="Canada" stepKey="chooseCanada2"/> + <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig2"/> + <waitForPageLoad stepKey="waitForSavingSystemConfiguration2"/> + <!--Switch to second website and allow all countries except Canada--> + <comment userInput="Switch to second website and allow all countries except Canada" stepKey="switchToWebsiteAndAllowOnlyCanada"/> + <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="adminSwitchWebsiteActionGroup2"> + <argument name="website" value="NewWebSiteData"/> + </actionGroup> + <conditionalClick selector="{{CountryOptionsSection.countryOptions}}" dependentSelector="{{CountryOptionsSection.countryOptionsOpen}}" visible="false" stepKey="clickOnStoreInformation3"/> + <waitForElementVisible selector="{{CountryOptionsSection.allowedCountries}}" stepKey="waitAllowedCountriesToBeVisible2"/> + <uncheckOption selector="{{CountryOptionsSection.generalCountryAllowInherit}}" stepKey="uncheckCheckbox3"/> + <unselectOption selector="{{CountryOptionsSection.allowedCountries}}" userInput="Canada" stepKey="unselectCanada"/> + <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig3"/> + <waitForPageLoad stepKey="waitForSavingSystemConfiguration3"/> + <amOnPage url="{{AdminEditCustomerPage.url($$createCustomer.id$$)}}" stepKey="goToCustomerEditPage"/> + <waitForPageLoad stepKey="waitPageToLoad"/> + <!--Open created customer details page and change US address to Canada address--> + <comment userInput="Open created customer details page and change US address to Canada address" stepKey="changeCustomerAddressToCanada"/> + <actionGroup ref="OpenEditCustomerAddressFromAdminActionGroup" stepKey="editCustomerAddress"> + <argument name="address" value="US_Address_CA"/> + </actionGroup> + <selectOption selector="{{AdminEditCustomerAddressesSection.country}}" userInput="Canada" stepKey="selectCountry"/> + <selectOption selector="{{AdminEditCustomerAddressesSection.state}}" userInput="Quebec" stepKey="selectState"/> + <click selector="{{AdminEditCustomerAddressesSection.save}}" stepKey="saveAddress"/> + <waitForPageLoad stepKey="waitForAddressSaved"/> + <click stepKey="saveCustomer" selector="{{AdminCustomerAccountInformationSection.saveCustomerAndContinueEdit}}"/> + <waitForPageLoad stepKey="waitForCustomersPage"/> + <!--Go to Customers grid and check that filter countries amount is the same as initial allowed countries amount--> + <comment userInput="Go to Customers grid and check that filter countries amount is the same as initial allowed countries amount" stepKey="compareCountriesAmount"/> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="goToCustomersGrid"/> + <waitForPageLoad stepKey="waitForCustomersGrid"/> + <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openFiltersSectionOnCustomersGrid"/> + <executeJS function="var len = document.querySelectorAll('{{AdminCustomerFiltersSection.countryOptions}}').length; return len-1;" stepKey="countriesAmount2"/> + <assertEquals expected='($countriesAmount)' expectedType="integer" actual="($countriesAmount2)" stepKey="assertCountryAmounts"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/ChangeCustomerGroupTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/ChangeCustomerGroupTest.xml new file mode 100644 index 000000000000..fb083f39ad38 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/ChangeCustomerGroupTest.xml @@ -0,0 +1,76 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="ChangingSingleCustomerGroupViaGrid"> + <annotations> + <title value="Change a single customer group via grid"/> + <description value="From the selection of All Customers select a single customer to change their group"/> + <severity value="MAJOR"/> + <testCaseId value="MC-10921"/> + <stories value="Change Customer Group"/> + <group value="customer"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!--Delete created product--> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="NavigateToCustomerGroupPage" stepKey="navToCustomers"/> + <actionGroup ref="AdminDeleteCustomerGroupActionGroup" stepKey="deleteCustomerGroup"> + <argument name="customerGroupName" value="{{CustomerGroupChange.code}}"/> + </actionGroup> + + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <actionGroup ref="AdminCreateCustomerGroupActionGroup" stepKey="createCustomerGroup"> + <argument name="groupName" value="{{CustomerGroupChange.code}}"/> + <argument name="taxClass" value="{{CustomerGroupChange.tax_class_name}}"/> + </actionGroup> + <actionGroup ref="NavigateToAllCustomerPage" stepKey="navToCustomers"/> + <actionGroup ref="AdminFilterCustomerByName" stepKey="filterCustomer"> + <argument name="customerName" value="{{Simple_US_Customer.fullname}}"/> + </actionGroup> + <actionGroup ref="AdminSelectCustomerByEmail" stepKey="selectCustomer"> + <argument name="customerEmail" value="$$createCustomer.email$$"/> + </actionGroup> + <actionGroup ref="SetCustomerGroupForSelectedCustomersViaGrid" stepKey="setCustomerGroup"> + <argument name="groupName" value="{{CustomerGroupChange.code}}"/> + </actionGroup> + <actionGroup ref="AdminFilterCustomerByName" stepKey="filterCustomerAfterGroupChange"> + <argument name="customerName" value="{{Simple_US_Customer.fullname}}"/> + </actionGroup> + <actionGroup ref="VerifyCustomerGroupForCustomer" stepKey="verifyCustomerGroupSet"> + <argument name="customerEmail" value="$$createCustomer.email$$"/> + <argument name="groupName" value="{{CustomerGroupChange.code}}"/> + </actionGroup> + </test> + + <test name="ChangingAllCustomerGroupViaGrid" extends="ChangingSingleCustomerGroupViaGrid"> + <annotations> + <title value="Change all customers' group via grid"/> + <description value="Select All customers to change their group"/> + <severity value="MAJOR"/> + <testCaseId value="MC-10924"/> + <stories value="Change Customer Group"/> + <group value="customer"/> + <group value="mtf_migrated"/> + </annotations> + + <remove keyForRemoval="filterCustomer"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearFilters" before="selectCustomer"/> + <actionGroup ref="AdminSelectAllCustomers" stepKey="selectCustomer"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/DeleteCustomerGroupTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/DeleteCustomerGroupTest.xml new file mode 100644 index 000000000000..a085a67167f6 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/DeleteCustomerGroupTest.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="DeleteCustomerGroupTest"> + <annotations> + <title value="Delete customer group entity test"/> + <description value="Delete a customer group"/> + <stories value="Delete Customer Group"/> + <testCaseId value="MC-14590" /> + <group value="customers"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="CustomCustomerGroup" stepKey="customerGroup" /> + <createData entity="UsCustomerAssignedToNewCustomerGroup" stepKey="customer"> + <requiredEntity createDataKey="customerGroup" /> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Customer Group success delete message--> + <actionGroup ref="AdminDeleteCustomerGroupActionGroup" stepKey="deleteCustomerGroup"> + <argument name="customerGroupName" value="$$customerGroup.code$$"/> + </actionGroup> + <actionGroup ref="AssertCustomerGroupNotInGridActionGroup" stepKey="assertCustomerGroupNotInGrid"> + <argument name="customerGroup" value="$$customerGroup$$" /> + </actionGroup> + + <actionGroup ref="AdminOpenCustomerEditPageActionGroup" stepKey="openCustomerEditPage"> + <argument name="customerId" value="$$customer.id$$" /> + </actionGroup> + + <actionGroup ref="AssertCustomerGroupOnCustomerFormActionGroup" stepKey="assertCustomerGroupOnCustomerForm"> + <argument name="customerGroup" value="GeneralCustomerGroup" /> + </actionGroup> + + <actionGroup ref="AdminOpenNewProductFormPageActionGroup" stepKey="openNewProductForm" /> + + <actionGroup ref="AssertCustomerGroupNotOnProductFormActionGroup" stepKey="assertCustomerGroupNotOnProductForm"> + <argument name="customerGroup" value="$$customerGroup$$" /> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/PasswordAutocompleteOffTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/PasswordAutocompleteOffTest.xml new file mode 100644 index 000000000000..f364d24806b9 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/PasswordAutocompleteOffTest.xml @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="PasswordAutocompleteOffTest"> + <annotations> + <features value="Customer"/> + <stories value="Customer Password Autocomplete"/> + <title value="[Security] Autocomplete attribute with off value is added to password input"/> + <description value="[Security] Autocomplete attribute with off value is added to password input"/> + <testCaseId value="MC-13678"/> + <severity value="CRITICAL"/> + <group value="customers"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Configure Magento via CLI: disable_guest_checkout --> + <magentoCLI command="config:set checkout/options/guest_checkout 0" stepKey="disableGuestCheckout"/> + + <!-- Configure Magento via CLI: password_autocomplete_off--> + <magentoCLI command="config:set customer/password/autocomplete_on_storefront 0" stepKey="turnPasswordAutocompleteOff"/> + + <!-- Create a simple product --> + <createData entity="SimpleSubCategory" stepKey="category"/> + <createData entity="SimpleProduct" stepKey="product"> + <requiredEntity createDataKey="category"/> + </createData> + </before> + <after> + <!-- Set Magento configuration back to default values --> + <magentoCLI command="config:set checkout/options/guest_checkout 1" stepKey="disableGuestCheckoutRollback"/> + <magentoCLI command="config:set customer/password/autocomplete_on_storefront 1" stepKey="turnPasswordAutocompleteOffRollback"/> + + <!-- Delete the simple product created in the before block --> + <deleteData createDataKey="product" stepKey="deleteProduct"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + + <!-- Go to the created product page and add it to the cart--> + <actionGroup ref="AddSimpleProductToCart" stepKey="cartAddSimpleProductToCart"> + <argument name="product" value="$$product$$"/> + </actionGroup> + + <!--Click Sign in - on the top right of the page --> + <actionGroup ref="StorefrontClickSignInButtonActionGroup" stepKey="storeFrontClickSignInButton"/> + + <!--Verify if the password field on store front sign-in page has the autocomplete attribute set to off --> + <actionGroup ref="AssertStorefrontPasswordAutoCompleteOffActionGroup" stepKey="assertStorefrontPasswordAutoCompleteOff"/> + + <!--Proceed to checkout--> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + + <!--Verify if the password field on the authorization popup has the autocomplete attribute set to off --> + <actionGroup ref="AssertAuthorizationPopUpPasswordAutoCompleteOffActionGroup" stepKey="assertAuthorizationPopUpPasswordAutoCompleteOff"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest.xml new file mode 100644 index 000000000000..413bbfd06a53 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest.xml @@ -0,0 +1,121 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontAddNewCustomerAddressTest"> + <annotations> + <features value="Customer address"/> + <stories value="Implement handling of large number of addresses on storefront Address book"/> + <title value="Storefront - My account - Address Book - add new address"/> + <description value="Storefront user should be able to create a new address via the storefront"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-97364"/> + <group value="customer"/> + <group value="create"/> + </annotations> + <before> + <createData entity="Simple_Customer_Without_Address" stepKey="createCustomer"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="DeleteCustomer"/> + <amOnPage url="admin/admin/auth/logout/" stepKey="AmOnLogoutPage"/> + </after> + + <!--Log in to Storefront as Customer 1 --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="signUp"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddNewCustomerAddressActionGroup" stepKey="AddNewAddress"> + <argument name="Address" value="US_Address_TX"/> + </actionGroup> + <see userInput="{{US_Address_TX.street[0]}}" + selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" stepKey="checkNewAddressesStreetOnDefaultBilling"/> + <see userInput="{{US_Address_TX.city}}" + selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" stepKey="checkNewAddressesCityOnDefaultBilling"/> + <see userInput="{{US_Address_TX.postcode}}" + selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" stepKey="checkNewAddressesPostcodeOnDefaultBilling"/> + <see userInput="{{US_Address_TX.street[0]}}" + selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" stepKey="checkNewAddressesStreetOnDefaultShipping"/> + <see userInput="{{US_Address_TX.city}}" + selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" stepKey="checkNewAddressesCityOnDefaultShipping"/> + <see userInput="{{US_Address_TX.postcode}}" + selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" stepKey="checkNewAddressesPostcodeOnDefaultShipping"/> + </test> + <test name="StorefrontAddCustomerDefaultAddressTest"> + <annotations> + <features value="Customer address"/> + <stories value="Implement handling of large number of addresses on storefront Address book"/> + <title value="Storefront - My account - Address Book - add new default billing/shipping address"/> + <description value="Storefront user should be able to create a new default address via the storefront"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-97364"/> + <group value="customer"/> + <group value="create"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="DeleteCustomer"/> + </after> + + <!--Log in to Storefront as Customer 1 --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="signUp"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddCustomerDefaultAddressActionGroup" stepKey="AddNewDefaultAddress"> + <argument name="Address" value="US_Address_TX"/> + </actionGroup> + <see userInput="{{US_Address_TX.street[0]}}" + selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" stepKey="checkNewAddressesStreetOnDefaultBilling"/> + <see userInput="{{US_Address_TX.city}}" + selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" stepKey="checkNewAddressesCityOnDefaultBilling"/> + <see userInput="{{US_Address_TX.postcode}}" + selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" stepKey="checkNewAddressesPostcodeOnDefaultBilling"/> + <see userInput="{{US_Address_TX.street[0]}}" + selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" stepKey="checkNewAddressesStreetOnDefaultShipping"/> + <see userInput="{{US_Address_TX.city}}" + selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" stepKey="checkNewAddressesCityOnDefaultShipping"/> + <see userInput="{{US_Address_TX.postcode}}" + selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" stepKey="checkNewAddressesPostcodeOnDefaultShipping"/> + </test> + <test name="StorefrontAddCustomerNonDefaultAddressTest"> + <annotations> + <features value="Customer address"/> + <stories value="Implement handling of large number of addresses on storefront Address book"/> + <title value="Storefront - My account - Address Book - add new non default billing/shipping address"/> + <description value="Storefront user should be able to create a new non default address via the storefront"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-97500"/> + <group value="customer"/> + <group value="create"/> + </annotations> + <before> + <createData entity="Simple_US_Customer_NY" stepKey="createCustomer"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="DeleteCustomer"/> + <amOnPage url="admin/admin/auth/logout/" stepKey="AmOnLogoutPage"/> + </after> + + <!--Log in to Storefront as Customer 1 --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="signUp"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddNewCustomerAddressActionGroup" stepKey="AddNewNonDefaultAddress"> + <argument name="Address" value="US_Address_TX"/> + </actionGroup> + <see userInput="{{US_Address_TX.street[0]}}" + selector="{{StorefrontCustomerAddressesSection.addressesList}}" stepKey="checkNewAddressesStreetOnDefaultShipping"/> + <see userInput="{{US_Address_TX.city}}" + selector="{{StorefrontCustomerAddressesSection.addressesList}}" stepKey="checkNewAddressesCityOnDefaultShipping"/> + <see userInput="{{US_Address_TX.postcode}}" + selector="{{StorefrontCustomerAddressesSection.addressesList}}" stepKey="checkNewAddressesPostcodeOnDefaultShipping"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCheckTaxAddingValidVATIdTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCheckTaxAddingValidVATIdTest.xml new file mode 100644 index 000000000000..ab805193854b --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCheckTaxAddingValidVATIdTest.xml @@ -0,0 +1,148 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCheckTaxAddingValidVATIdTest"> + <annotations> + <features value="Customer"/> + <stories value="MAGETWO-91639: Tax is added despite customer group changes"/> + <title value="Check tax adding when it's changed to 'Valid VAT ID - Intra-Union'"/> + <description value="Tax should be applied"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-95028"/> + <group value="customer"/> + </annotations> + <before> + <!--Log In--> + <actionGroup ref="LoginAsAdmin" stepKey="logIn"/> + <!--Create category--> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <!--Create product--> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + + <!--Add new tax rates. Go to tax rule page --> + <actionGroup ref="addNewTaxRuleActionGroup" stepKey="addFirstTaxRuleActionGroup"/> + <fillField stepKey="fillRuleName" selector="{{AdminTaxRulesSection.ruleName}}" userInput="{{TaxRule.name}}"/> + + <!-- Add NY and CA tax rules --> + <actionGroup ref="addNewTaxRateNoZip" stepKey="addSimpleTaxUK"> + <argument name="taxCode" value="SimpleTaxUK"/> + </actionGroup> + <click stepKey="clickSave" selector="{{AdminStoresMainActionsSection.saveButton}}"/> + <waitForPageLoad stepKey="waitForNewTaxRuleCreated"/> + + <!-- Go to tax rule page to create second Tax Rule--> + <actionGroup ref="addNewTaxRuleActionGroup" stepKey="addSecondTaxRuleActionGroup"/> + <fillField stepKey="fillSecondRuleName" selector="{{AdminTaxRulesSection.ruleName}}" userInput="{{TaxRuleZeroRate.name}}"/> + <actionGroup ref="addNewTaxRateNoZip" stepKey="addSimpleTaxUKZeroRate"> + <argument name="taxCode" value="SimpleTaxUKZeroRate"/> + </actionGroup> + <actionGroup ref="addCustomerTaxClass" stepKey="addCustomerTaxClass"> + <argument name="customerTaxClassName" value="UK_zero"/> + </actionGroup> + <click stepKey="disableDefaultProdTaxClass" selector="{{AdminTaxRulesSection.defaultCustomerTaxClass}}"/> + <wait stepKey="waitForDisableDefaultProdTaxClass" time="2"/> + <click stepKey="clickSaveBtn" selector="{{AdminStoresMainActionsSection.saveButton}}"/> + <waitForPageLoad stepKey="waitForSecondTaxRuleCreated"/> + + <!--Create a Customer Group (CUSTOMERS > Customer Groups)--> + <actionGroup ref="AdminCreateCustomerGroupActionGroup" stepKey="createCustomerGroup"> + <argument name="groupName" value="test_UK"/> + <argument name="taxClass" value="UK_zero"/> + </actionGroup> + + <!--Set Customer Create New Account Options Config--> + <createData entity="SetCustomerCreateNewAccountOptionsConfig" stepKey="setCustomerCreateNewAccountOptionsConfig"/> + <actionGroup ref="SetGroupForValidVATIdIntraUnionActionGroup" stepKey="setGroupForValidVATIdIntraUnionActionGroup" after="setCustomerCreateNewAccountOptionsConfig"> + <argument name="value" value="test_UK"/> + </actionGroup> + + <!--Register customer on storefront--> + <actionGroup ref="SignUpNewCustomerStorefrontActionGroup" stepKey="createAnAccount"> + <argument name="Customer" value="CustomerEntityOne"/> + </actionGroup> + + <!--Go to My account > Address book--> + <actionGroup ref="EnterCustomerAddressInfoFillState" stepKey="enterAddressInfo"> + <argument name="Address" value="UK_Simple_Address"/> + </actionGroup> + + <!-- Go to product visible --> + <amOnPage url="$$createProduct.name$$.html" stepKey="navigateToProductPageOnDefaultStore"/> + <see userInput="$$createProduct.name$$" selector="{{StorefrontProductInfoMainSection.productName}}" stepKey="assertFirstProductNameTitle"/> + + <!--Add a product to the cart--> + <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> + <waitForPageLoad stepKey="waitForAddProductToCart"/> + <!--Proceed to checkout--> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="GoToCheckoutFromMinicartActionGroup"/> + <!-- Click next button to open payment section --> + <click selector="{{CheckoutShippingGuestInfoSection.next}}" stepKey="clickNext"/> + <waitForPageLoad stepKey="waitForShipmentPageLoad"/> + + <!-- Check order summary in checkout --> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> + <!--Verify that Tax 50% is applied --> + <see userInput="$123.00" selector="{{CheckoutPaymentSection.orderSummarySubtotal}}" stepKey="assertSubtotal"/> + <see userInput="$5.00" selector="{{CheckoutPaymentSection.orderSummaryShippingTotal}}" stepKey="assertShipping"/> + <see userInput="Flat Rate - Fixed" selector="{{CheckoutPaymentSection.orderSummaryShippingMethod}}" stepKey="assertShippingMethod"/> + <see userInput="$61.50" selector="{{CheckoutPaymentSection.tax}}" stepKey="assertTax"/> + <see userInput="$189.50" selector="{{CheckoutPaymentSection.orderSummaryTotal}}" stepKey="assertTotal"/> + + <after> + <!-- Go to the tax rule page and delete the row we created--> + <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulesPage"/> + <waitForPageLoad stepKey="waitForRulesPage"/> + + <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteRule"> + <argument name="name" value="{{TaxRule.name}}"/> + <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> + </actionGroup> + + <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteSecondRule"> + <argument name="name" value="{{TaxRuleZeroRate.name}}"/> + <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> + </actionGroup> + + <!-- Go to the tax rate page --> + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRatesPage"/> + <waitForPageLoad stepKey="waitForRatesPage"/> + + <!-- Delete the two tax rates that were created --> + <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteNYRate"> + <argument name="name" value="{{SimpleTaxUK.state}}-{{SimpleTaxUK.rate}}"/> + <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> + </actionGroup> + + <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteCARate"> + <argument name="name" value="{{SimpleTaxUKZeroRate.state}}-{{SimpleTaxUKZeroRate.rate}}"/> + <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> + </actionGroup> + + <!--Delete created customer group--> + <actionGroup ref="AdminDeleteCustomerGroupActionGroup" stepKey="deleteCustomerGroup"> + <argument name="customerGroupName" value="test_UK"/> + </actionGroup> + + <createData entity="SetCustomerCreateNewAccountOptionsDefaultConfig" stepKey="setCustomerCreateNewAccountOptionsDefaultConfig"/> + <deleteData createDataKey="createProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategoryFirst"/> + + <actionGroup ref="deleteProductTaxClass" stepKey="deleteFirstProductTaxClass"> + <argument name="taxClassName" value="UK_zero"/> + </actionGroup> + + <!--Log Out--> + <actionGroup ref="logout" stepKey="logout"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontDeleteCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontDeleteCustomerAddressTest.xml new file mode 100644 index 000000000000..7a9661688546 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontDeleteCustomerAddressTest.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontDeleteCustomerAddressTest"> + <annotations> + <stories value="Delete customer address from storefront"/> + <title value="User should be able to delete Customer address successfully from storefront"/> + <description value="User should be able to delete Customer address successfully from storefront"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-5713"/> + <group value="Customer"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + <amOnPage stepKey="amOnSignInPage" url="{{StorefrontCustomerSignInPage.url}}"/> + <fillField stepKey="fillEmail" userInput="$$createCustomer.email$$" selector="{{StorefrontCustomerSignInFormSection.emailField}}"/> + <fillField stepKey="fillPassword" userInput="$$createCustomer.password$$" selector="{{StorefrontCustomerSignInFormSection.passwordField}}"/> + <click stepKey="clickSignInAccountButton" selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}"/> + <actionGroup ref="EnterCustomerAddressInfo" stepKey="enterAddressInfo"> + <argument name="Address" value="US_Address_NY"/> + </actionGroup> + <see userInput="You saved the address." stepKey="verifyAddressCreated"/> + <click selector="{{StorefrontCustomerAddressesSection.deleteAdditionalAddress('1')}}" stepKey="deleteAdditionalAddress"/> + <waitForElementVisible selector="{{ModalConfirmationSection.modalContent}}" stepKey="waitFortheConfirmationModal"/> + <see selector="{{ModalConfirmationSection.modalContent}}" userInput="Are you sure you want to delete this address?" stepKey="seeAddressDeleteConfirmationMessage"/> + <click selector="{{ModalConfirmationSection.OkButton}}" stepKey="confirmDelete"/> + <waitForPageLoad stepKey="waitForDeleteToFinish"/> + <see userInput="You deleted the address." stepKey="verifyDeleteAddress"/> + </test> +</tests> 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 000000000000..8d4be5fda3c7 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontForgotPasswordTest.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCustomerForgotPasswordTest"> + <annotations> + <features value="Customer"/> + <stories value="Customer Login"/> + <title value="Forgot Password on Storefront validates customer email input"/> + <description value="Forgot Password on Storefront validates customer email input"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-13679"/> + <group value="Customer"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaDisableConfigData.path}} {{StorefrontCustomerCaptchaDisableConfigData.value}}" stepKey="disableCaptcha"/> + <createData stepKey="customer" entity="Simple_US_Customer"/> + </before> + <after> + <magentoCLI command="config:set {{StorefrontCustomerCaptchaEnableConfigData.path}} {{StorefrontCustomerCaptchaEnableConfigData.value}}" stepKey="enableCaptcha"/> + <deleteData stepKey="deleteCustomer" createDataKey="customer" /> + </after> + + <amOnPage stepKey="amOnSignInPage" url="{{StorefrontCustomerSignInPage.url}}"/> + <fillField stepKey="fillEmail" userInput="$$customer.email$$" selector="{{StorefrontCustomerSignInFormSection.emailField}}"/> + <fillField stepKey="fillPassword" userInput="something" selector="{{StorefrontCustomerSignInFormSection.passwordField}}"/> + <click stepKey="clickForgotPasswordLink" selector="{{StorefrontCustomerSignInFormSection.forgotPasswordLink}}"/> + <see stepKey="seePageTitle" userInput="Forgot Your Password" selector="{{StorefrontForgotPasswordSection.pageTitle}}"/> + <fillField stepKey="enterEmail" userInput="$$customer.email$$" selector="{{StorefrontForgotPasswordSection.email}}"/> + <click stepKey="clickResetPassword" selector="{{StorefrontForgotPasswordSection.resetMyPasswordButton}}"/> + <seeInCurrentUrl stepKey="seeInSignInPage" url="account/login"/> + <see stepKey="seeSuccessMessage" userInput="If there is an account associated with $$customer.email$$ you will receive an email with a link to reset your password." selector="{{StorefrontCustomerLoginMessagesSection.successMessage}}"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginWithIncorrectCredentialsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginWithIncorrectCredentialsTest.xml new file mode 100644 index 000000000000..104b5d56314b --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginWithIncorrectCredentialsTest.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontLoginWithIncorrectCredentialsTest"> + <annotations> + <features value="Customer"/> + <stories value="Customer Login"/> + <title value="Customer Login on Storefront with Incorrect Credentials"/> + <description value="Customer Login on Storefront with Incorrect Credentials"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10913"/> + <group value="Customer"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData stepKey="customer" entity="Simple_US_Customer"/> + </before> + <after> + <deleteData stepKey="deleteCustomer" createDataKey="customer" /> + </after> + + <amOnPage stepKey="amOnSignInPage" url="{{StorefrontCustomerSignInPage.url}}"/> + <fillField stepKey="fillEmail" userInput="$$customer.email$$" selector="{{StorefrontCustomerSignInFormSection.emailField}}"/> + <fillField stepKey="fillPassword" userInput="$$customer.password$$INVALID" selector="{{StorefrontCustomerSignInFormSection.passwordField}}"/> + <click stepKey="clickSignInAccountButton" selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}"/> + <see stepKey="seeErrorMessage" selector="{{StorefrontCustomerLoginMessagesSection.errorMessage}}" userInput="The account sign-in was incorrect or your account is disabled temporarily. Please wait and try again later."/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontResetCustomerPasswordFailedTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontResetCustomerPasswordFailedTest.xml new file mode 100644 index 000000000000..3121bd0da9d2 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontResetCustomerPasswordFailedTest.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontResetCustomerPasswordFailedTest"> + <annotations> + <features value="Customer"/> + <title value="Customer tries to reset password several times"/> + <description value="Customer tries to reset password several times"/> + <severity value="CRITICAL" /> + <testCaseId value="MC-14374" /> + <group value="Customer"/> + <group value="security"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData stepKey="customer" entity="Simple_US_Customer"/> + </before> + <after> + <deleteData stepKey="deleteCustomer" createDataKey="customer" /> + </after> + + <actionGroup ref="StorefrontCustomerResetPasswordActionGroup" stepKey="resetPasswordFirstAttempt"> + <argument name="email" value="$$customer.email$$" /> + </actionGroup> + <actionGroup ref="AssertCustomerResetPasswordActionGroup" stepKey="seePageWithSuccessMessage"> + <argument name="url" value="{{StorefrontCustomerSignInPage.url}}"/> + <argument name="message" value="If there is an account associated with $$customer.email$$ you will receive an email with a link to reset your password."/> + </actionGroup> + <actionGroup ref="StorefrontCustomerResetPasswordActionGroup" stepKey="resetPasswordSecondAttempt"> + <argument name="email" value="$$customer.email$$" /> + </actionGroup> + <actionGroup ref="AssertCustomerResetPasswordActionGroup" stepKey="seePageWithErrorMessage"> + <argument name="url" value="{{StorefrontForgotPasswordPage.url}}"/> + <argument name="message" value="We received too many requests for password resets. Please wait and try again later or contact hello@example.com."/> + <argument name="messageType" value="error" /> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressFranceTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressFranceTest.xml new file mode 100644 index 000000000000..dae456c96a67 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressFranceTest.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontUpdateCustomerAddressFranceTest"> + <annotations> + <stories value="Update Customer Address"/> + <title value="Update Customer Address (France) in Storefront"/> + <description value="Test log in to Storefront and Update Customer Address (France) in Storefront"/> + <testCaseId value="MC-10912"/> + <severity value="CRITICAL"/> + <group value="customer"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="SignUpNewUserFromStorefrontActionGroup" stepKey="SignUpNewUser"> + <argument name="Customer" value="CustomerEntityOne"/> + </actionGroup> + </before> + <after> + <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteNewUser"> + <argument name="email" value="{{CustomerEntityOne.email}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Update customer address France in storefront--> + <actionGroup ref="EnterCustomerAddressInfo" stepKey="enterAddress"> + <argument name="Address" value="updateCustomerFranceAddress"/> + </actionGroup> + <!--Verify customer address save success message--> + <see selector="{{AdminCustomerMessagesSection.successMessage}}" userInput="You saved the address." stepKey="seeAssertCustomerAddressSuccessSaveMessage"/> + + <!--Verify customer default billing address--> + <actionGroup ref="VerifyCustomerBillingAddressWithState" stepKey="verifyBillingAddress"> + <argument name="address" value="updateCustomerFranceAddress"/> + </actionGroup> + + <!--Verify customer default shipping address--> + <actionGroup ref="VerifyCustomerShippingAddressWithState" stepKey="verifyShippingAddress"> + <argument name="address" value="updateCustomerFranceAddress"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest.xml new file mode 100644 index 000000000000..d9d1c9f2e05a --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest.xml @@ -0,0 +1,122 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontUpdateCustomerDefaultBillingAddressFromBlockTest"> + <annotations> + <features value="Customer address"/> + <stories value="Implement handling of large number of addresses on storefront Address book"/> + <title value="Add default customer address via the Storefront6"/> + <description value="Storefront user should be able to create a new default address via the storefront"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-97501"/> + <group value="customer"/> + <group value="update"/> + </annotations> + <before> + <createData entity="Simple_US_Customer_With_Different_Billing_Shipping_Addresses" stepKey="createCustomer"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="DeleteCustomer"/> + </after> + + <!--Log in to Storefront as Customer 1 --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="signUp"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <amOnPage url="customer/address/" stepKey="OpenCustomerAddNewAddress"/> + <click stepKey="ClickEditDefaultBillingAddress" selector="{{StorefrontCustomerAddressesSection.editDefaultBillingAddress}}"/> + <fillField stepKey="fillFirstName" userInput="EditedFirstNameBilling" selector="{{StorefrontCustomerAddressFormSection.firstName}}"/> + <fillField stepKey="fillLastName" userInput="EditedLastNameBilling" selector="{{StorefrontCustomerAddressFormSection.lastName}}"/> + <click stepKey="saveCustomerAddress" selector="{{StorefrontCustomerAddressFormSection.saveAddress}}"/> + <see userInput="You saved the address." stepKey="verifyAddressAdded"/> + <see userInput="EditedFirstNameBilling" + selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" stepKey="checkNewAddressesFirstNameOnDefaultBilling"/> + <see userInput="EditedLastNameBilling" + selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" stepKey="checkNewAddressesLastNameOnDefaultBilling"/> + <see userInput="{{US_Address_NY_Default_Shipping.firstname}}" + selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" stepKey="checkNewAddressesFirstNameOnDefaultShipping"/> + <see userInput="{{US_Address_NY_Default_Shipping.lastname}}" + selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" stepKey="checkNewAddressesLastNameOnDefaultShipping"/> + </test> + <test name="StorefrontUpdateCustomerDefaultShippingAddressFromBlockTest"> + <annotations> + <features value="Customer address"/> + <stories value="Implement handling of large number of addresses on storefront Address book"/> + <title value="Add default customer address via the Storefront611"/> + <description value="Storefront user should be able to create a new default address via the storefront"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-97501"/> + <group value="customer"/> + <group value="update"/> + <skip> + <issueId value="MAGETWO-97504"/> + </skip> + </annotations> + <before> + <createData entity="Simple_US_Customer_With_Different_Billing_Shipping_Addresses" stepKey="createCustomer"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="DeleteCustomer"/> + </after> + + <!--Log in to Storefront as Customer 1 --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="signUp"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <amOnPage url="customer/address/" stepKey="OpenCustomerAddNewAddress"/> + <click stepKey="ClickEditDefaultShippingAddress" selector="{{StorefrontCustomerAddressesSection.editDefaultShippingAddress}}"/> + <fillField stepKey="fillFirstName" userInput="EditedFirstNameShipping" selector="{{StorefrontCustomerAddressFormSection.firstName}}"/> + <fillField stepKey="fillLastName" userInput="EditedLastNameShipping" selector="{{StorefrontCustomerAddressFormSection.lastName}}"/> + <click stepKey="saveCustomerAddress" selector="{{StorefrontCustomerAddressFormSection.saveAddress}}"/> + <see userInput="You saved the address." stepKey="verifyAddressAdded"/> + <see userInput="EditedFirstNameShipping" + selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" stepKey="checkNewAddressesFirstNameOnDefaultShipping"/> + <see userInput="EditedLastNameShipping" + selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" stepKey="checkNewAddressesLastNameOnDefaultShipping"/> + <see userInput="{{US_Address_TX_Default_Billing.firstname}}" + selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" stepKey="checkNewAddressesFirstNameOnDefaultBilling"/> + <see userInput="{{US_Address_TX_Default_Billing.lastname}}" + selector="{{StorefrontCustomerAddressesSection.defaultBillingAddress}}" stepKey="checkNewAddressesLastNameOnDefaultBilling"/> + </test> + <test name="StorefrontUpdateCustomerAddressFromGridTest"> + <annotations> + <features value="Customer address"/> + <stories value="Add default customer address via the Storefront7"/> + <title value="Add default customer address via the Storefront7"/> + <description value="Storefront user should be able to create a new default address via the storefront2"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-97502"/> + <group value="customer"/> + <group value="update"/> + </annotations> + <before> + <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="DeleteCustomer"/> + </after> + + <!--Log in to Storefront as Customer 1 --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="signUp"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <amOnPage url="customer/address/" stepKey="OpenCustomerAddNewAddress"/> + <click selector="{{StorefrontCustomerAddressesSection.editAdditionalAddress('1')}}" stepKey="editAdditionalAddress"/> + <fillField stepKey="fillFirstName" userInput="EditedFirstName" selector="{{StorefrontCustomerAddressFormSection.firstName}}"/> + <fillField stepKey="fillLastName" userInput="EditedLastName" selector="{{StorefrontCustomerAddressFormSection.lastName}}"/> + <click stepKey="saveCustomerAddress" selector="{{StorefrontCustomerAddressFormSection.saveAddress}}"/> + <see userInput="You saved the address." stepKey="verifyAddressAdded"/> + <see userInput="EditedFirstName" + selector="{{StorefrontCustomerAddressesSection.addressesList}}" stepKey="checkNewAddressFirstNameOnGrid"/> + <see userInput="EditedLastName" + selector="{{StorefrontCustomerAddressesSection.addressesList}}" stepKey="checkNewAddressLastNameOnGrid"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressUKTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressUKTest.xml new file mode 100644 index 000000000000..7b6e695aa8dc --- /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 000000000000..9bc253c91af9 --- /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 000000000000..e11404db9a9a --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontVerifyNoXssInjectionOnUpdateCustomerInformationAddAddressTest.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontVerifyNoXssInjectionOnUpdateCustomerInformationAddAddressTest"> + <annotations> + <stories value="Update Customer Address"/> + <title value="[Security] Verify No XSS Injection on Update Customer Information Add Address"/> + <description value="Test log in to Storefront and Verify No XSS Injection on Update Customer Information Add Address"/> + <testCaseId value="MC-10910"/> + <severity value="CRITICAL"/> + <group value="customer"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="SignUpNewUserFromStorefrontActionGroup" stepKey="SignUpNewUser"> + <argument name="Customer" value="Colorado_US_Customer"/> + </actionGroup> + </before> + <after> + <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteNewUser"> + <argument name="email" value="{{Colorado_US_Customer.email}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Update customer address in storefront--> + <actionGroup ref="EnterCustomerAddressInfo" stepKey="enterAddress"> + <argument name="Address" value="updateCustomerNoXSSInjection"/> + </actionGroup> + <!--Verify customer address save success message--> + <see selector="{{AdminCustomerMessagesSection.successMessage}}" userInput="You saved the address." stepKey="seeAssertCustomerAddressSuccessSaveMessage"/> + + <!--Verify customer default billing address--> + <actionGroup ref="VerifyCustomerBillingAddressWithState" stepKey="verifyBillingAddress"> + <argument name="address" value="updateCustomerNoXSSInjection"/> + </actionGroup> + + <!--Verify customer default shipping address--> + <actionGroup ref="VerifyCustomerShippingAddressWithState" stepKey="verifyShippingAddress"> + <argument name="address" value="updateCustomerNoXSSInjection"/> + </actionGroup> + + <!--Verify customer name on frontend--> + <actionGroup ref="VerifyCustomerNameOnFrontend" stepKey="verifyVerifyCustomerName"> + <argument name="customer" value="Colorado_US_Customer"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Customer/Test/Mftf/Test/VerifyDisabledCustomerGroupFieldTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/VerifyDisabledCustomerGroupFieldTest.xml new file mode 100644 index 000000000000..648c30b1ca0b --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/VerifyDisabledCustomerGroupFieldTest.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="VerifyDisabledCustomerGroupFieldTest"> + <annotations> + <stories value="Check that field is disabled in system Customer Group"/> + <title value="Check that field is disabled in system Customer Group"/> + <description value="Checks that customer_group_code field is disabled in NOT LOGGED IN Customer Group"/> + <testCaseId value="MC-14206"/> + <severity value="CRITICAL"/> + <group value="customers"/> + <group value="mtf_migrated"/> + </annotations> + + <!-- Steps --> + <!-- 1. Login to backend as admin user --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <waitForPageLoad stepKey="waitForAdminPageLoad" /> + + <!-- 2. Navigate to Customers > Customer Groups --> + <amOnPage url="{{AdminCustomerGroupPage.url}}" stepKey="amOnCustomerGroupPage" /> + <waitForPageLoad stepKey="waitForCustomerGroupsPageLoad" /> + + <!-- 3. Select system Customer Group specified in data set from grid --> + <click selector="{{AdminCustomerGroupMainSection.editButtonByCustomerGroupCode(NotLoggedInCustomerGroup.code)}}" stepKey="clickOnEditCustomerGroup" /> + + <!-- 4. Perform all assertions --> + <seeInField selector="{{AdminNewCustomerGroupSection.groupName}}" userInput="{{NotLoggedInCustomerGroup.code}}" stepKey="seeNotLoggedInTextInGroupName" /> + <assertElementContainsAttribute selector="{{AdminNewCustomerGroupSection.groupName}}" attribute="disabled" expectedValue="true" stepKey="checkIfGroupNameIsDisabled" /> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Unit/Block/Address/GridTest.php b/app/code/Magento/Customer/Test/Unit/Block/Address/GridTest.php new file mode 100644 index 000000000000..47f96b132b3d --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Block/Address/GridTest.php @@ -0,0 +1,198 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Customer\Test\Unit\Block\Address; + +use Magento\Customer\Model\ResourceModel\Address\CollectionFactory; + +/** + * Unit tests for \Magento\Customer\Block\Address\Grid class + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class GridTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager + */ + private $objectManager; + + /** + * @var \Magento\Customer\Helper\Session\CurrentCustomer|\PHPUnit_Framework_MockObject_MockObject + */ + private $addressCollectionFactory; + + /** + * @var \Magento\Customer\Model\ResourceModel\Address\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $currentCustomer; + + /** + * @var \Magento\Directory\Model\CountryFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $countryFactory; + + /** + * @var \Magento\Framework\UrlInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $urlBuilder; + + /** + * @var \Magento\Customer\Block\Address\Grid + */ + private $gridBlock; + + protected function setUp() + { + $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->currentCustomer = $this->getMockBuilder(\Magento\Customer\Helper\Session\CurrentCustomer::class) + ->disableOriginalConstructor() + ->setMethods(['getCustomer']) + ->getMock(); + + $this->addressCollectionFactory = $this->getMockBuilder(CollectionFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $this->countryFactory = $this->getMockBuilder(\Magento\Directory\Model\CountryFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $this->urlBuilder = $this->getMockForAbstractClass(\Magento\Framework\UrlInterface::class); + + $this->gridBlock = $this->objectManager->getObject( + \Magento\Customer\Block\Address\Grid::class, + [ + 'addressCollectionFactory' => $this->addressCollectionFactory, + 'currentCustomer' => $this->currentCustomer, + 'countryFactory' => $this->countryFactory, + '_urlBuilder' => $this->urlBuilder + ] + ); + } + + /** + * Test for \Magento\Customer\Block\Address\Book::getChildHtml method with 'pager' argument + */ + public function testGetChildHtml() + { + $customerId = 1; + $outputString = 'OutputString'; + /** @var \Magento\Framework\View\Element\BlockInterface|\PHPUnit_Framework_MockObject_MockObject $block */ + $block = $this->getMockBuilder(\Magento\Framework\View\Element\BlockInterface::class) + ->setMethods(['setCollection']) + ->getMockForAbstractClass(); + /** @var $layout \Magento\Framework\View\LayoutInterface|\PHPUnit_Framework_MockObject_MockObject */ + $layout = $this->getMockForAbstractClass(\Magento\Framework\View\LayoutInterface::class); + /** @var \Magento\Customer\Api\Data\CustomerInterface|\PHPUnit_Framework_MockObject_MockObject $customer */ + $customer = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\CustomerInterface::class); + /** @var \PHPUnit_Framework_MockObject_MockObject */ + $addressCollection = $this->getMockBuilder(\Magento\Customer\Model\ResourceModel\Address\Collection::class) + ->disableOriginalConstructor() + ->setMethods(['setOrder', 'setCustomerFilter', 'load','addFieldToFilter']) + ->getMock(); + + $layout->expects($this->atLeastOnce())->method('getChildName')->with('NameInLayout', 'pager') + ->willReturn('ChildName'); + $layout->expects($this->atLeastOnce())->method('renderElement')->with('ChildName', true) + ->willReturn('OutputString'); + $layout->expects($this->atLeastOnce())->method('createBlock') + ->with(\Magento\Theme\Block\Html\Pager::class, 'customer.addresses.pager')->willReturn($block); + $customer->expects($this->atLeastOnce())->method('getId')->willReturn($customerId); + $this->currentCustomer->expects($this->atLeastOnce())->method('getCustomer')->willReturn($customer); + $addressCollection->expects($this->atLeastOnce())->method('setOrder')->with('entity_id', 'desc') + ->willReturnSelf(); + $addressCollection->expects($this->atLeastOnce())->method('setCustomerFilter')->with([$customerId]) + ->willReturnSelf(); + $addressCollection->expects(static::any())->method('addFieldToFilter')->willReturnSelf(); + $this->addressCollectionFactory->expects($this->atLeastOnce())->method('create') + ->willReturn($addressCollection); + $block->expects($this->atLeastOnce())->method('setCollection')->with($addressCollection)->willReturnSelf(); + $this->gridBlock->setNameInLayout('NameInLayout'); + $this->gridBlock->setLayout($layout); + $this->assertEquals($outputString, $this->gridBlock->getChildHtml('pager')); + } + + /** + * Test for \Magento\Customer\Block\Address\Grid::getAddressEditUrl method + */ + public function testGetAddAddressUrl() + { + $addressId = 1; + $expectedUrl = 'expected_url'; + $this->urlBuilder->expects($this->atLeastOnce())->method('getUrl') + ->with('customer/address/edit', ['_secure' => true, 'id' => $addressId]) + ->willReturn($expectedUrl); + $this->assertEquals($expectedUrl, $this->gridBlock->getAddressEditUrl($addressId)); + } + + public function testGetAdditionalAddresses() + { + $customerId = 1; + /** @var \Magento\Customer\Api\Data\CustomerInterface|\PHPUnit_Framework_MockObject_MockObject $customer */ + $customer = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\CustomerInterface::class); + /** @var \PHPUnit_Framework_MockObject_MockObject */ + $addressCollection = $this->getMockBuilder(\Magento\Customer\Model\ResourceModel\Address\Collection::class) + ->disableOriginalConstructor() + ->setMethods(['setOrder', 'setCustomerFilter', 'load', 'getIterator','addFieldToFilter']) + ->getMock(); + $addressDataModel = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\AddressInterface::class); + $address = $this->getMockBuilder(\Magento\Customer\Model\Address::class) + ->disableOriginalConstructor() + ->setMethods(['getId', 'getDataModel']) + ->getMock(); + $collection = [$address, $address, $address]; + $address->expects($this->exactly(3))->method('getId') + ->willReturnOnConsecutiveCalls(1, 2, 3); + $address->expects($this->atLeastOnce())->method('getDataModel')->willReturn($addressDataModel); + $customer->expects($this->atLeastOnce())->method('getId')->willReturn($customerId); + $customer->expects($this->atLeastOnce())->method('getDefaultBilling')->willReturn('1'); + $customer->expects($this->atLeastOnce())->method('getDefaultShipping')->willReturn('2'); + + $this->currentCustomer->expects($this->atLeastOnce())->method('getCustomer')->willReturn($customer); + $addressCollection->expects($this->atLeastOnce())->method('setOrder')->with('entity_id', 'desc') + ->willReturnSelf(); + $addressCollection->expects($this->atLeastOnce())->method('setCustomerFilter')->with([$customerId]) + ->willReturnSelf(); + $addressCollection->expects(static::any())->method('addFieldToFilter')->willReturnSelf(); + $addressCollection->expects($this->atLeastOnce())->method('getIterator') + ->willReturn(new \ArrayIterator($collection)); + $this->addressCollectionFactory->expects($this->atLeastOnce())->method('create') + ->willReturn($addressCollection); + + $this->assertEquals($addressDataModel, $this->gridBlock->getAdditionalAddresses()[0]); + } + + /** + * Test for \Magento\Customer\ViewModel\CustomerAddress::getStreetAddress method + */ + public function testGetStreetAddress() + { + $street = ['Line 1', 'Line 2']; + $expectedAddress = 'Line 1, Line 2'; + $address = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\AddressInterface::class); + $address->expects($this->atLeastOnce())->method('getStreet')->willReturn($street); + $this->assertEquals($expectedAddress, $this->gridBlock->getStreetAddress($address)); + } + + /** + * Test for \Magento\Customer\ViewModel\CustomerAddress::getCountryByCode method + */ + public function testGetCountryByCode() + { + $countryId = 'US'; + $countryName = 'United States'; + $country = $this->getMockBuilder(\Magento\Directory\Model\Country::class) + ->disableOriginalConstructor() + ->setMethods(['loadByCode', 'getName']) + ->getMock(); + $this->countryFactory->expects($this->atLeastOnce())->method('create')->willReturn($country); + $country->expects($this->atLeastOnce())->method('loadByCode')->with($countryId)->willReturnSelf(); + $country->expects($this->atLeastOnce())->method('getName')->willReturn($countryName); + $this->assertEquals($countryName, $this->gridBlock->getCountryByCode($countryId)); + } +} diff --git a/app/code/Magento/Customer/Test/Unit/Block/Adminhtml/Edit/Address/DeleteButtonTest.php b/app/code/Magento/Customer/Test/Unit/Block/Adminhtml/Edit/Address/DeleteButtonTest.php new file mode 100644 index 000000000000..7b0da3bd422a --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Block/Adminhtml/Edit/Address/DeleteButtonTest.php @@ -0,0 +1,106 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Customer\Test\Unit\Block\Adminhtml\Edit\Address; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +/** + * Class for \Magento\Customer\Block\Adminhtml\Edit\Address\DeleteButton unit tests + */ +class DeleteButtonTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Customer\Model\AddressFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $addressFactory; + + /** + * @var \Magento\Framework\UrlInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $urlBuilder; + + /** + * @var \Magento\Customer\Model\ResourceModel\Address|\PHPUnit_Framework_MockObject_MockObject + */ + private $addressResourceModel; + + /** + * @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $request; + + /** + * @var \Magento\Customer\Model\ResourceModel\AddressRepository|\PHPUnit_Framework_MockObject_MockObject + */ + private $addressRepository; + + /** + * @var \Magento\Customer\Block\Adminhtml\Edit\Address\DeleteButton + */ + private $deleteButton; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->addressFactory = $this->getMockBuilder(\Magento\Customer\Model\AddressFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->urlBuilder = $this->getMockForAbstractClass(\Magento\Framework\UrlInterface::class); + $this->addressResourceModel = $this->getMockBuilder(\Magento\Customer\Model\ResourceModel\Address::class) + ->disableOriginalConstructor() + ->getMock(); + $this->request = $this->getMockForAbstractClass(\Magento\Framework\App\RequestInterface::class); + $this->addressRepository = $this->getMockBuilder( + \Magento\Customer\Model\ResourceModel\AddressRepository::class + ) + ->disableOriginalConstructor() + ->getMock(); + $objectManagerHelper = new ObjectManagerHelper($this); + + $this->deleteButton = $objectManagerHelper->getObject( + \Magento\Customer\Block\Adminhtml\Edit\Address\DeleteButton::class, + [ + 'addressFactory' => $this->addressFactory, + 'urlBuilder' => $this->urlBuilder, + 'addressResourceModel' => $this->addressResourceModel, + 'request' => $this->request, + 'addressRepository' => $this->addressRepository, + ] + ); + } + + /** + * Unit test for \Magento\Customer\Block\Adminhtml\Edit\Address\DeleteButton::getButtonData() method + */ + public function testGetButtonData() + { + $addressId = 1; + $customerId = 2; + + /** @var \Magento\Customer\Model\Address|\PHPUnit_Framework_MockObject_MockObject $address */ + $address = $this->getMockBuilder(\Magento\Customer\Model\Address::class) + ->disableOriginalConstructor() + ->getMock(); + $address->expects($this->atLeastOnce())->method('getEntityId')->willReturn($addressId); + $address->expects($this->atLeastOnce())->method('getCustomerId')->willReturn($customerId); + $this->addressFactory->expects($this->atLeastOnce())->method('create')->willReturn($address); + $this->request->expects($this->atLeastOnce())->method('getParam')->with('entity_id') + ->willReturn($addressId); + $this->addressResourceModel->expects($this->atLeastOnce())->method('load')->with($address, $addressId); + $this->addressRepository->expects($this->atLeastOnce())->method('getById')->with($addressId) + ->willReturn($address); + $this->urlBuilder->expects($this->atLeastOnce())->method('getUrl') + ->with( + \Magento\Customer\Ui\Component\Listing\Address\Column\Actions::CUSTOMER_ADDRESS_PATH_DELETE, + ['parent_id' => $customerId, 'id' => $addressId] + )->willReturn('url'); + + $buttonData = $this->deleteButton->getButtonData(); + $this->assertEquals('Delete', (string)$buttonData['label']); + } +} diff --git a/app/code/Magento/Customer/Test/Unit/Block/Form/RegisterTest.php b/app/code/Magento/Customer/Test/Unit/Block/Form/RegisterTest.php index f1629d61fe92..d234ebfb334d 100644 --- a/app/code/Magento/Customer/Test/Unit/Block/Form/RegisterTest.php +++ b/app/code/Magento/Customer/Test/Unit/Block/Form/RegisterTest.php @@ -7,6 +7,7 @@ use Magento\Customer\Block\Form\Register; use Magento\Customer\Model\AccountManagement; +use Magento\Newsletter\Observer\PredispatchNewsletterObserver; /** * Test class for \Magento\Customer\Block\Form\Register. @@ -274,12 +275,13 @@ public function testGetRegionNull() } /** - * @param $isNewsletterEnabled - * @param $expectedValue + * @param boolean $isNewsletterEnabled + * @param string $isNewsletterActive + * @param boolean $expectedValue * * @dataProvider isNewsletterEnabledProvider */ - public function testIsNewsletterEnabled($isNewsletterEnabled, $expectedValue) + public function testIsNewsletterEnabled($isNewsletterEnabled, $isNewsletterActive, $expectedValue) { $this->_moduleManager->expects( $this->once() @@ -290,6 +292,17 @@ public function testIsNewsletterEnabled($isNewsletterEnabled, $expectedValue) )->will( $this->returnValue($isNewsletterEnabled) ); + + $this->_scopeConfig->expects( + $this->any() + )->method( + 'getValue' + )->with( + PredispatchNewsletterObserver::XML_PATH_NEWSLETTER_ACTIVE + )->will( + $this->returnValue($isNewsletterActive) + ); + $this->assertEquals($expectedValue, $this->_block->isNewsletterEnabled()); } @@ -298,7 +311,7 @@ public function testIsNewsletterEnabled($isNewsletterEnabled, $expectedValue) */ public function isNewsletterEnabledProvider() { - return [[true, true], [false, false]]; + return [[true, true, true], [true, false, false], [false, true, false], [false, false, false]]; } /** diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Account/CreatePasswordTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Account/CreatePasswordTest.php deleted file mode 100644 index 77f41024ba02..000000000000 --- a/app/code/Magento/Customer/Test/Unit/Controller/Account/CreatePasswordTest.php +++ /dev/null @@ -1,233 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Customer\Test\Unit\Controller\Account; - -use Magento\Framework\Controller\Result\Redirect; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; - -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class CreatePasswordTest extends \PHPUnit\Framework\TestCase -{ - /** @var \Magento\Customer\Controller\Account\CreatePassword */ - protected $model; - - /** @var ObjectManagerHelper */ - protected $objectManagerHelper; - - /** @var \Magento\Customer\Model\Session|\PHPUnit_Framework_MockObject_MockObject */ - protected $sessionMock; - - /** @var \Magento\Framework\View\Result\PageFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $pageFactoryMock; - - /** @var \Magento\Customer\Api\AccountManagementInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $accountManagementMock; - - /** @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $requestMock; - - /** @var \Magento\Framework\Controller\Result\RedirectFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $redirectFactoryMock; - - /** @var \Magento\Framework\Message\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $messageManagerMock; - - protected function setUp() - { - $this->sessionMock = $this->getMockBuilder(\Magento\Customer\Model\Session::class) - ->disableOriginalConstructor() - ->setMethods(['setRpToken', 'setRpCustomerId', 'getRpToken', 'getRpCustomerId']) - ->getMock(); - $this->pageFactoryMock = $this->getMockBuilder(\Magento\Framework\View\Result\PageFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $this->accountManagementMock = $this->getMockBuilder(\Magento\Customer\Api\AccountManagementInterface::class) - ->getMockForAbstractClass(); - $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) - ->getMockForAbstractClass(); - $this->redirectFactoryMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\RedirectFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $this->messageManagerMock = $this->getMockBuilder(\Magento\Framework\Message\ManagerInterface::class) - ->getMockForAbstractClass(); - - $this->objectManagerHelper = new ObjectManagerHelper($this); - $this->model = $this->objectManagerHelper->getObject( - \Magento\Customer\Controller\Account\CreatePassword::class, - [ - 'customerSession' => $this->sessionMock, - 'resultPageFactory' => $this->pageFactoryMock, - 'accountManagement' => $this->accountManagementMock, - 'request' => $this->requestMock, - 'resultRedirectFactory' => $this->redirectFactoryMock, - 'messageManager' => $this->messageManagerMock, - ] - ); - } - - public function testExecuteWithLink() - { - $token = 'token'; - $customerId = '11'; - - $this->requestMock->expects($this->exactly(2)) - ->method('getParam') - ->willReturnMap( - [ - ['token', null, $token], - ['id', null, $customerId], - ] - ); - - $this->accountManagementMock->expects($this->once()) - ->method('validateResetPasswordLinkToken') - ->with($customerId, $token) - ->willReturn(true); - - $this->sessionMock->expects($this->once()) - ->method('setRpToken') - ->with($token); - $this->sessionMock->expects($this->once()) - ->method('setRpCustomerId') - ->with($customerId); - - /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->redirectFactoryMock->expects($this->once()) - ->method('create') - ->with([]) - ->willReturn($redirectMock); - - $redirectMock->expects($this->once()) - ->method('setPath') - ->with('*/*/createpassword', []) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } - - public function testExecuteWithSession() - { - $token = 'token'; - $customerId = '11'; - - $this->requestMock->expects($this->exactly(2)) - ->method('getParam') - ->willReturnMap( - [ - ['token', null, null], - ['id', null, $customerId], - ] - ); - - $this->sessionMock->expects($this->once()) - ->method('getRpToken') - ->willReturn($token); - $this->sessionMock->expects($this->once()) - ->method('getRpCustomerId') - ->willReturn($customerId); - - $this->accountManagementMock->expects($this->once()) - ->method('validateResetPasswordLinkToken') - ->with($customerId, $token) - ->willReturn(true); - - /** @var \Magento\Framework\View\Result\Page|\PHPUnit_Framework_MockObject_MockObject $pageMock */ - $pageMock = $this->getMockBuilder(\Magento\Framework\View\Result\Page::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->pageFactoryMock->expects($this->once()) - ->method('create') - ->with(false, []) - ->willReturn($pageMock); - - /** @var \Magento\Framework\View\Layout|\PHPUnit_Framework_MockObject_MockObject $layoutMock */ - $layoutMock = $this->getMockBuilder(\Magento\Framework\View\Layout::class) - ->disableOriginalConstructor() - ->getMock(); - - $pageMock->expects($this->once()) - ->method('getLayout') - ->willReturn($layoutMock); - - /** @var \Magento\Customer\Block\Account\Resetpassword|\PHPUnit_Framework_MockObject_MockObject $layoutMock */ - $blockMock = $this->getMockBuilder(\Magento\Customer\Block\Account\Resetpassword::class) - ->disableOriginalConstructor() - ->setMethods(['setCustomerId', 'setResetPasswordLinkToken']) - ->getMock(); - - $layoutMock->expects($this->once()) - ->method('getBlock') - ->with('resetPassword') - ->willReturn($blockMock); - - $blockMock->expects($this->once()) - ->method('setCustomerId') - ->with($customerId) - ->willReturnSelf(); - $blockMock->expects($this->once()) - ->method('setResetPasswordLinkToken') - ->with($token) - ->willReturnSelf(); - - $this->assertEquals($pageMock, $this->model->execute()); - } - - public function testExecuteWithException() - { - $token = 'token'; - $customerId = '11'; - - $this->requestMock->expects($this->exactly(2)) - ->method('getParam') - ->willReturnMap( - [ - ['token', null, $token], - ['id', null, null], - ] - ); - - $this->sessionMock->expects($this->once()) - ->method('getRpToken') - ->willReturn($token); - $this->sessionMock->expects($this->once()) - ->method('getRpCustomerId') - ->willReturn($customerId); - - $this->accountManagementMock->expects($this->once()) - ->method('validateResetPasswordLinkToken') - ->with($customerId, $token) - ->willThrowException(new \Exception('Exception.')); - - $this->messageManagerMock->expects($this->once()) - ->method('addError') - ->with(__('Your password reset link has expired.')) - ->willReturnSelf(); - - /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->redirectFactoryMock->expects($this->once()) - ->method('create') - ->with([]) - ->willReturn($redirectMock); - - $redirectMock->expects($this->once()) - ->method('setPath') - ->with('*/*/forgotpassword', []) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } -} diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Account/ResetPasswordPostTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Account/ResetPasswordPostTest.php deleted file mode 100644 index b79ad008e5e4..000000000000 --- a/app/code/Magento/Customer/Test/Unit/Controller/Account/ResetPasswordPostTest.php +++ /dev/null @@ -1,379 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Customer\Test\Unit\Controller\Account; - -use Magento\Framework\Controller\Result\Redirect; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; - -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class ResetPasswordPostTest extends \PHPUnit\Framework\TestCase -{ - /** @var \Magento\Customer\Controller\Account\ResetPasswordPost */ - protected $model; - - /** @var ObjectManagerHelper */ - protected $objectManagerHelper; - - /** @var \Magento\Customer\Model\Session|\PHPUnit_Framework_MockObject_MockObject */ - protected $sessionMock; - - /** @var \Magento\Framework\View\Result\PageFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $pageFactoryMock; - - /** @var \Magento\Customer\Api\AccountManagementInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $accountManagementMock; - - /** @var \Magento\Customer\Api\CustomerRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $customerRepositoryMock; - - /** @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $requestMock; - - /** @var \Magento\Framework\Controller\Result\RedirectFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $redirectFactoryMock; - - /** @var \Magento\Framework\Message\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $messageManagerMock; - - protected function setUp() - { - $this->sessionMock = $this->getMockBuilder(\Magento\Customer\Model\Session::class) - ->disableOriginalConstructor() - ->setMethods(['unsRpToken', 'unsRpCustomerId']) - ->getMock(); - $this->pageFactoryMock = $this->getMockBuilder(\Magento\Framework\View\Result\PageFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $this->accountManagementMock = $this->getMockBuilder(\Magento\Customer\Api\AccountManagementInterface::class) - ->getMockForAbstractClass(); - $this->customerRepositoryMock = $this->getMockBuilder(\Magento\Customer\Api\CustomerRepositoryInterface::class) - ->getMockForAbstractClass(); - $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) - ->setMethods(['getQuery', 'getPost']) - ->getMockForAbstractClass(); - $this->redirectFactoryMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\RedirectFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $this->messageManagerMock = $this->getMockBuilder(\Magento\Framework\Message\ManagerInterface::class) - ->getMockForAbstractClass(); - - $this->objectManagerHelper = new ObjectManagerHelper($this); - $this->model = $this->objectManagerHelper->getObject( - \Magento\Customer\Controller\Account\ResetPasswordPost::class, - [ - 'customerSession' => $this->sessionMock, - 'resultPageFactory' => $this->pageFactoryMock, - 'accountManagement' => $this->accountManagementMock, - 'customerRepository' => $this->customerRepositoryMock, - 'request' => $this->requestMock, - 'resultRedirectFactory' => $this->redirectFactoryMock, - 'messageManager' => $this->messageManagerMock, - ] - ); - } - - public function testExecute() - { - $token = 'token'; - $customerId = '11'; - $password = 'password'; - $passwordConfirmation = 'password'; - $email = 'email@email.com'; - - $this->requestMock->expects($this->exactly(2)) - ->method('getQuery') - ->willReturnMap( - [ - ['token', $token], - ['id', $customerId], - ] - ); - $this->requestMock->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap( - [ - ['password', $password], - ['password_confirmation', $passwordConfirmation], - ] - ); - - /** @var \Magento\Customer\Api\Data\CustomerInterface|\PHPUnit_Framework_MockObject_MockObject $customerMock */ - $customerMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) - ->getMockForAbstractClass(); - - $this->customerRepositoryMock->expects($this->once()) - ->method('getById') - ->with($customerId) - ->willReturn($customerMock); - - $customerMock->expects($this->once()) - ->method('getEmail') - ->willReturn($email); - - $this->accountManagementMock->expects($this->once()) - ->method('resetPassword') - ->with($email, $token, $password) - ->willReturn(true); - - $this->sessionMock->expects($this->once()) - ->method('unsRpToken'); - $this->sessionMock->expects($this->once()) - ->method('unsRpCustomerId'); - - $this->messageManagerMock->expects($this->once()) - ->method('addSuccess') - ->with(__('You updated your password.')) - ->willReturnSelf(); - - /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->redirectFactoryMock->expects($this->once()) - ->method('create') - ->with([]) - ->willReturn($redirectMock); - - $redirectMock->expects($this->once()) - ->method('setPath') - ->with('*/*/login', []) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } - - public function testExecuteWithException() - { - $token = 'token'; - $customerId = '11'; - $password = 'password'; - $passwordConfirmation = 'password'; - $email = 'email@email.com'; - - $this->requestMock->expects($this->exactly(2)) - ->method('getQuery') - ->willReturnMap( - [ - ['token', $token], - ['id', $customerId], - ] - ); - $this->requestMock->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap( - [ - ['password', $password], - ['password_confirmation', $passwordConfirmation], - ] - ); - - /** @var \Magento\Customer\Api\Data\CustomerInterface|\PHPUnit_Framework_MockObject_MockObject $customerMock */ - $customerMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) - ->getMockForAbstractClass(); - - $this->customerRepositoryMock->expects($this->once()) - ->method('getById') - ->with($customerId) - ->willReturn($customerMock); - - $customerMock->expects($this->once()) - ->method('getEmail') - ->willReturn($email); - - $this->accountManagementMock->expects($this->once()) - ->method('resetPassword') - ->with($email, $token, $password) - ->willThrowException(new \Exception('Exception.')); - - $this->messageManagerMock->expects($this->once()) - ->method('addError') - ->with(__('Something went wrong while saving the new password.')) - ->willReturnSelf(); - - /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->redirectFactoryMock->expects($this->once()) - ->method('create') - ->with([]) - ->willReturn($redirectMock); - - $redirectMock->expects($this->once()) - ->method('setPath') - ->with('*/*/createPassword', ['id' => $customerId, 'token' => $token]) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } - - /** - * Test for InputException - */ - public function testExecuteWithInputException() - { - $token = 'token'; - $customerId = '11'; - $password = 'password'; - $passwordConfirmation = 'password'; - $email = 'email@email.com'; - - $this->requestMock->expects($this->exactly(2)) - ->method('getQuery') - ->willReturnMap( - [ - ['token', $token], - ['id', $customerId], - ] - ); - $this->requestMock->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap( - [ - ['password', $password], - ['password_confirmation', $passwordConfirmation], - ] - ); - - /** @var \Magento\Customer\Api\Data\CustomerInterface|\PHPUnit_Framework_MockObject_MockObject $customerMock */ - $customerMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) - ->getMockForAbstractClass(); - - $this->customerRepositoryMock->expects($this->once()) - ->method('getById') - ->with($customerId) - ->willReturn($customerMock); - - $customerMock->expects($this->once()) - ->method('getEmail') - ->willReturn($email); - - $this->accountManagementMock->expects($this->once()) - ->method('resetPassword') - ->with($email, $token, $password) - ->willThrowException(new \Magento\Framework\Exception\InputException(__('InputException.'))); - - $this->messageManagerMock->expects($this->once()) - ->method('addError') - ->with(__('InputException.')) - ->willReturnSelf(); - - /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->redirectFactoryMock->expects($this->once()) - ->method('create') - ->with([]) - ->willReturn($redirectMock); - - $redirectMock->expects($this->once()) - ->method('setPath') - ->with('*/*/createPassword', ['id' => $customerId, 'token' => $token]) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } - - public function testExecuteWithWrongConfirmation() - { - $token = 'token'; - $customerId = '11'; - $password = 'password'; - $passwordConfirmation = 'wrong_password'; - - $this->requestMock->expects($this->exactly(2)) - ->method('getQuery') - ->willReturnMap( - [ - ['token', $token], - ['id', $customerId], - ] - ); - $this->requestMock->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap( - [ - ['password', $password], - ['password_confirmation', $passwordConfirmation], - ] - ); - - $this->messageManagerMock->expects($this->once()) - ->method('addError') - ->with(__('New Password and Confirm New Password values didn\'t match.')) - ->willReturnSelf(); - - /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->redirectFactoryMock->expects($this->once()) - ->method('create') - ->with([]) - ->willReturn($redirectMock); - - $redirectMock->expects($this->once()) - ->method('setPath') - ->with('*/*/createPassword', ['id' => $customerId, 'token' => $token]) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } - - public function testExecuteWithEmptyPassword() - { - $token = 'token'; - $customerId = '11'; - $password = ''; - $passwordConfirmation = ''; - - $this->requestMock->expects($this->exactly(2)) - ->method('getQuery') - ->willReturnMap( - [ - ['token', $token], - ['id', $customerId], - ] - ); - $this->requestMock->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap( - [ - ['password', $password], - ['password_confirmation', $passwordConfirmation], - ] - ); - - $this->messageManagerMock->expects($this->once()) - ->method('addError') - ->with(__('Please enter a new password.')) - ->willReturnSelf(); - - /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->redirectFactoryMock->expects($this->once()) - ->method('create') - ->with([]) - ->willReturn($redirectMock); - - $redirectMock->expects($this->once()) - ->method('setPath') - ->with('*/*/createPassword', ['id' => $customerId, 'token' => $token]) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } -} diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Address/FormPostTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Address/FormPostTest.php index c2a795fc9501..7ae55f44421c 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Address/FormPostTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Address/FormPostTest.php @@ -455,14 +455,20 @@ public function testExecute( $regionCode, $newRegionId, $newRegion, - $newRegionCode + $newRegionCode, + $existingDefaultBilling = false, + $existingDefaultShipping = false, + $setDefaultBilling = false, + $setDefaultShipping = false ): void { $existingAddressData = [ 'country_id' => $countryId, 'region_id' => $regionId, 'region' => $region, 'region_code' => $regionCode, - 'customer_id' => $customerId + 'customer_id' => $customerId, + 'default_billing' => $existingDefaultBilling, + 'default_shipping' => $existingDefaultShipping, ]; $newAddressData = [ 'country_id' => $countryId, @@ -486,8 +492,8 @@ public function testExecute( ->method('getParam') ->willReturnMap([ ['id', null, $addressId], - ['default_billing', false, $addressId], - ['default_shipping', false, $addressId], + ['default_billing', $existingDefaultBilling, $setDefaultBilling], + ['default_shipping', $existingDefaultShipping, $setDefaultShipping], ]); $this->addressRepository->expects($this->once()) @@ -565,11 +571,11 @@ public function testExecute( ->willReturnSelf(); $this->addressData->expects($this->once()) ->method('setIsDefaultBilling') - ->with() + ->with($setDefaultBilling) ->willReturnSelf(); $this->addressData->expects($this->once()) ->method('setIsDefaultShipping') - ->with() + ->with($setDefaultShipping) ->willReturnSelf(); $this->messageManager->expects($this->once()) @@ -628,11 +634,11 @@ public function dataProviderTestExecute(): array [1, 1, 1, 2, null, null, 12, null, null], [1, 1, 1, 2, 'Alaska', null, 12, null, 'CA'], - [1, 1, 1, 2, 'Alaska', 'AK', 12, 'California', null], + [1, 1, 1, 2, 'Alaska', 'AK', 12, 'California', null, true, true, true, false], - [1, 1, 1, 2, null, null, 12, null, null], - [1, 1, 1, 2, 'Alaska', null, 12, null, 'CA'], - [1, 1, 1, 2, 'Alaska', 'AK', 12, 'California', null], + [1, 1, 1, 2, null, null, 12, null, null, false, false, true, false], + [1, 1, 1, 2, 'Alaska', null, 12, null, 'CA', true, false, true, false], + [1, 1, 1, 2, 'Alaska', 'AK', 12, 'California', null, true, true, true, true], ]; } diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Address/SaveTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Address/SaveTest.php new file mode 100644 index 000000000000..1bf881ff0a93 --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Controller/Address/SaveTest.php @@ -0,0 +1,213 @@ +<?php +declare(strict_types=1); +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Customer\Test\Unit\Controller\Address; + +use Magento\Customer\Api\Data\AddressInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\Result\JsonFactory; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class SaveTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Customer\Controller\Adminhtml\Address\Save + */ + private $model; + + /** + * @var \Magento\Customer\Api\AddressRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $addressRepositoryMock; + + /** + * @var \Magento\Customer\Model\Metadata\FormFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $formFactoryMock; + + /** + * @var \Magento\Customer\Api\CustomerRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $customerRepositoryMock; + + /** + * @var \Magento\Framework\Api\DataObjectHelper|\PHPUnit_Framework_MockObject_MockObject + */ + private $dataObjectHelperMock; + + /** + * @var \Magento\Customer\Api\Data\AddressInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $addressDataFactoryMock; + + /** + * @var \Psr\Log\LoggerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $loggerMock; + + /** + * @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $requestMock; + + /** + * @var AddressInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $address; + + /** + * @var JsonFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $resultJsonFactory; + + /** + * @var Json|\PHPUnit_Framework_MockObject_MockObject + */ + private $json; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->addressRepositoryMock = $this->createMock(\Magento\Customer\Api\AddressRepositoryInterface::class); + $this->formFactoryMock = $this->createMock(\Magento\Customer\Model\Metadata\FormFactory::class); + $this->customerRepositoryMock = $this->createMock(\Magento\Customer\Api\CustomerRepositoryInterface::class); + $this->dataObjectHelperMock = $this->createMock(\Magento\Framework\Api\DataObjectHelper ::class); + $this->addressDataFactoryMock = $this->createMock(\Magento\Customer\Api\Data\AddressInterfaceFactory::class); + $this->loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class); + $this->requestMock = $this->createMock(\Magento\Framework\App\RequestInterface::class); + $this->address = $this->getMockBuilder(AddressInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->resultJsonFactory = $this->getMockBuilder(JsonFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->json = $this->getMockBuilder(Json::class) + ->disableOriginalConstructor() + ->getMock(); + + $objectManager = new ObjectManagerHelper($this); + + $this->model = $objectManager->getObject( + \Magento\Customer\Controller\Adminhtml\Address\Save::class, + [ + 'addressRepository' => $this->addressRepositoryMock, + 'formFactory' => $this->formFactoryMock, + 'customerRepository' => $this->customerRepositoryMock, + 'dataObjectHelper' => $this->dataObjectHelperMock, + 'addressDataFactory' => $this->addressDataFactoryMock, + 'logger' => $this->loggerMock, + 'request' => $this->requestMock, + 'resultJsonFactory' => $this->resultJsonFactory + ] + ); + } + + public function testExecute(): void + { + $addressId = 11; + $customerId = 22; + + $addressExtractedData = [ + 'entity_id' => $addressId, + 'code' => 'value', + 'coolness' => false, + 'region' => 'region', + 'region_id' => 'region_id', + ]; + + $addressCompactedData = [ + 'entity_id' => $addressId, + 'default_billing' => 'true', + 'default_shipping' => 'true', + 'code' => 'value', + 'coolness' => false, + 'region' => 'region', + 'region_id' => 'region_id', + ]; + + $mergedAddressData = [ + 'entity_id' => $addressId, + 'default_billing' => true, + 'default_shipping' => true, + 'code' => 'value', + 'region' => [ + 'region' => 'region', + 'region_id' => 'region_id', + ], + 'region_id' => 'region_id', + 'id' => $addressId, + ]; + + $this->requestMock->method('getParam') + ->withConsecutive(['parent_id'], ['entity_id']) + ->willReturnOnConsecutiveCalls(22, 1); + + $customerMock = $this->getMockBuilder( + \Magento\Customer\Api\Data\CustomerInterface::class + )->disableOriginalConstructor()->getMock(); + + $this->customerRepositoryMock->expects($this->atLeastOnce()) + ->method('getById') + ->with($customerId) + ->willReturn($customerMock); + + $customerAddressFormMock = $this->createMock(\Magento\Customer\Model\Metadata\Form::class); + $customerAddressFormMock->expects($this->atLeastOnce()) + ->method('extractData') + ->with($this->requestMock) + ->willReturn($addressExtractedData); + $customerAddressFormMock->expects($this->once()) + ->method('compactData') + ->with($addressExtractedData) + ->willReturn($addressCompactedData); + + $this->formFactoryMock->expects($this->exactly(1)) + ->method('create') + ->willReturn($customerAddressFormMock); + + $addressMock = $this->getMockBuilder(AddressInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->addressDataFactoryMock->expects($this->once())->method('create')->willReturn($addressMock); + + $this->dataObjectHelperMock->expects($this->atLeastOnce()) + ->method('populateWithArray') + ->willReturn( + [ + $addressMock, + $mergedAddressData, AddressInterface::class, + $this->dataObjectHelperMock, + ] + ); + $this->addressRepositoryMock->expects($this->once())->method('save')->willReturn($this->address); + $this->address->expects($this->once())->method('getId')->willReturn($addressId); + + $this->resultJsonFactory->expects($this->once()) + ->method('create') + ->willReturn($this->json); + $this->json->expects($this->once()) + ->method('setData') + ->with( + [ + 'message' => __('Customer address has been updated.'), + 'error' => false, + 'data' => [ + 'entity_id' => $addressId + ] + ] + )->willReturnSelf(); + + $this->assertEquals($this->json, $this->model->execute()); + } +} diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Address/ValidateTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Address/ValidateTest.php new file mode 100644 index 000000000000..a724bdd24959 --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Controller/Address/ValidateTest.php @@ -0,0 +1,118 @@ +<?php +declare(strict_types=1); +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Customer\Test\Unit\Controller\Address; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ValidateTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Customer\Controller\Adminhtml\Address\Validate + */ + private $model; + + /** + * @var \Magento\Customer\Model\Metadata\FormFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $formFactoryMock; + + /** + * @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $requestMock; + + /** + * @var \Magento\Backend\Model\View\Result\RedirectFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $resultRedirectFactoryMock; + + /** + * @var \Magento\Framework\Controller\Result\JsonFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $resultJsonFactoryMock; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->formFactoryMock = $this->createMock(\Magento\Customer\Model\Metadata\FormFactory::class); + $this->requestMock = $this->createMock(\Magento\Framework\App\RequestInterface::class); + $this->resultJsonFactoryMock = $this->createMock(\Magento\Framework\Controller\Result\JsonFactory::class); + $this->resultRedirectFactoryMock = $this->createMock(\Magento\Backend\Model\View\Result\RedirectFactory::class); + + $objectManager = new ObjectManagerHelper($this); + + $this->model = $objectManager->getObject( + \Magento\Customer\Controller\Adminhtml\Address\Validate::class, + [ + 'formFactory' => $this->formFactoryMock, + 'request' => $this->requestMock, + 'resultRedirectFactory' => $this->resultRedirectFactoryMock, + 'resultJsonFactory' => $this->resultJsonFactoryMock, + ] + ); + } + + /** + * Test method \Magento\Customer\Controller\Adminhtml\Address\Save::execute + * + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function testExecute() + { + $addressId = 11; + $errors = ['Error Message 1', 'Error Message 2']; + + $addressExtractedData = [ + 'entity_id' => $addressId, + 'default_billing' => true, + 'default_shipping' => true, + 'code' => 'value', + 'region' => [ + 'region' => 'region', + 'region_id' => 'region_id', + ], + 'region_id' => 'region_id', + 'id' => $addressId, + ]; + + $customerAddressFormMock = $this->createMock(\Magento\Customer\Model\Metadata\Form::class); + + $customerAddressFormMock->expects($this->atLeastOnce()) + ->method('extractData') + ->with($this->requestMock) + ->willReturn($addressExtractedData); + $customerAddressFormMock->expects($this->once()) + ->method('validateData') + ->with($addressExtractedData) + ->willReturn($errors); + + $this->formFactoryMock->expects($this->exactly(1)) + ->method('create') + ->willReturn($customerAddressFormMock); + + $resultJson = $this->createMock(\Magento\Framework\Controller\Result\Json::class); + $this->resultJsonFactoryMock->method('create') + ->willReturn($resultJson); + + $validateResponseMock = $this->createPartialMock( + \Magento\Framework\DataObject::class, + ['getError', 'setMessages'] + ); + $validateResponseMock->method('setMessages')->willReturnSelf(); + $validateResponseMock->method('getError')->willReturn(1); + + $resultJson->method('setData')->willReturnSelf(); + + $this->assertEquals($resultJson, $this->model->execute()); + } +} diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/File/Address/UploadTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/File/Address/UploadTest.php index 20177ab0b0db..8f8ed0e37a46 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/File/Address/UploadTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/File/Address/UploadTest.php @@ -70,7 +70,8 @@ protected function setUp() $this->context, $this->fileUploaderFactory, $this->addressMetadataService, - $this->logger + $this->logger, + 'address' ); } @@ -104,27 +105,27 @@ public function testExecuteEmptyFiles() public function testExecute() { - $attributeCode = 'attribute_code'; + $attributeCode = 'file_address_attribute'; + $resultFileSize = 20000; + $resultFileName = 'text.txt'; + $resultType = 'text/plain'; $_FILES = [ - 'address' => [ - 'name' => [ - 'new_0' => [ - $attributeCode => 'filename', - ], - ], + $attributeCode => [ + 'name' => $resultFileName, + 'type' => $resultType, + 'size' => $resultFileSize ], ]; - $resultFileName = '/filename.ext1'; $resultFilePath = 'filepath'; $resultFileUrl = 'viewFileUrl'; $result = [ 'name' => $resultFileName, - 'file' => $resultFileName, - 'path' => $resultFilePath, - 'tmp_name' => $resultFilePath . $resultFileName, + 'type' => $resultType, + 'size' => $resultFileSize, + 'tmp_name' => $resultFilePath . '/' . $resultFileName, 'url' => $resultFileUrl, ]; @@ -173,15 +174,16 @@ public function testExecute() public function testExecuteWithErrors() { - $attributeCode = 'attribute_code'; + $attributeCode = 'file_address_attribute'; + $resultFileSize = 20000; + $resultFileName = 'text.txt'; + $resultType = 'text/plain'; $_FILES = [ - 'address' => [ - 'name' => [ - 'new_0' => [ - $attributeCode => 'filename', - ], - ], + $attributeCode => [ + 'name' => $resultFileName, + 'type' => $resultType, + 'size' => $resultFileSize ], ]; diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/InlineEditTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/InlineEditTest.php index 913c41070856..45e64f6557d5 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/InlineEditTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/InlineEditTest.php @@ -5,9 +5,14 @@ */ namespace Magento\Customer\Test\Unit\Controller\Adminhtml\Index; +use Magento\Customer\Model\AddressRegistry; use Magento\Customer\Model\EmailNotificationInterface; +use Magento\Framework\DataObject; +use Magento\Framework\Message\MessageInterface; /** + * Unit tests for Inline customer edit + * * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -67,14 +72,27 @@ class InlineEditTest extends \PHPUnit\Framework\TestCase /** @var EmailNotificationInterface|\PHPUnit_Framework_MockObject_MockObject */ private $emailNotification; + /** @var AddressRegistry|\PHPUnit_Framework_MockObject_MockObject */ + private $addressRegistry; + /** @var array */ private $items; + /** + * Sets up mocks + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ protected function setUp() { $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->request = $this->getMockForAbstractClass(\Magento\Framework\App\RequestInterface::class, [], '', false); + $this->request = $this->getMockForAbstractClass( + \Magento\Framework\App\RequestInterface::class, + [], + '', + false + ); $this->messageManager = $this->getMockForAbstractClass( \Magento\Framework\Message\ManagerInterface::class, [], @@ -124,8 +142,12 @@ protected function setUp() '', false ); - $this->logger = $this->getMockForAbstractClass(\Psr\Log\LoggerInterface::class, [], '', false); - + $this->logger = $this->getMockForAbstractClass( + \Psr\Log\LoggerInterface::class, + [], + '', + false + ); $this->emailNotification = $this->getMockBuilder(EmailNotificationInterface::class) ->disableOriginalConstructor() ->getMock(); @@ -137,6 +159,7 @@ protected function setUp() 'messageManager' => $this->messageManager, ] ); + $this->addressRegistry = $this->createMock(\Magento\Customer\Model\AddressRegistry::class); $this->controller = $objectManager->getObject( \Magento\Customer\Controller\Adminhtml\Index\InlineEdit::class, [ @@ -149,6 +172,7 @@ protected function setUp() 'addressDataFactory' => $this->addressDataFactory, 'addressRepository' => $this->addressRepository, 'logger' => $this->logger, + 'addressRegistry' => $this->addressRegistry ] ); $reflection = new \ReflectionClass(get_class($this->controller)); @@ -165,6 +189,8 @@ protected function setUp() } /** + * Prepare mocks for tests + * * @param int $populateSequence */ protected function prepareMocksForTesting($populateSequence = 0) @@ -203,6 +229,9 @@ protected function prepareMocksForTesting($populateSequence = 0) ->willReturn(12); } + /** + * Prepare mocks for update customers default billing address use case + */ protected function prepareMocksForUpdateDefaultBilling() { $this->prepareMocksForProcessAddressData(); @@ -211,12 +240,15 @@ protected function prepareMocksForUpdateDefaultBilling() 'firstname' => 'Firstname', 'lastname' => 'Lastname', ]; - $this->customerData->expects($this->once()) + $this->customerData->expects($this->exactly(2)) ->method('getAddresses') ->willReturn([$this->address]); $this->address->expects($this->once()) ->method('isDefaultBilling') ->willReturn(true); + $this->addressRegistry->expects($this->once()) + ->method('retrieve') + ->willReturn(new DataObject()); $this->dataObjectHelper->expects($this->at(0)) ->method('populateWithArray') ->with( @@ -226,6 +258,9 @@ protected function prepareMocksForUpdateDefaultBilling() ); } + /** + * Prepare mocks for processing customers address data use case + */ protected function prepareMocksForProcessAddressData() { $this->customerData->expects($this->once()) @@ -236,16 +271,20 @@ protected function prepareMocksForProcessAddressData() ->willReturn('Lastname'); } + /** + * Prepare mocks for error messages processing test + */ protected function prepareMocksForErrorMessagesProcessing() { $this->messageManager->expects($this->atLeastOnce()) ->method('getMessages') ->willReturn($this->messageCollection); $this->messageCollection->expects($this->once()) - ->method('getItems') + ->method('getErrors') ->willReturn([$this->message]); $this->messageCollection->expects($this->once()) - ->method('getCount') + ->method('getCountByType') + ->with(MessageInterface::TYPE_ERROR) ->willReturn(1); $this->message->expects($this->once()) ->method('getText') @@ -259,6 +298,9 @@ protected function prepareMocksForErrorMessagesProcessing() ->willReturnSelf(); } + /** + * Unit test for updating customers billing address use case + */ public function testExecuteWithUpdateBilling() { $this->prepareMocksForTesting(1); @@ -279,6 +321,9 @@ public function testExecuteWithUpdateBilling() $this->assertSame($this->resultJson, $this->controller->execute()); } + /** + * Unit test for creating customer with empty data use case + */ public function testExecuteWithoutItems() { $this->resultJsonFactory->expects($this->once()) @@ -303,6 +348,9 @@ public function testExecuteWithoutItems() $this->assertSame($this->resultJson, $this->controller->execute()); } + /** + * Unit test for verifying Localized Exception during inline edit + */ public function testExecuteLocalizedException() { $exception = new \Magento\Framework\Exception\LocalizedException(__('Exception message')); @@ -310,6 +358,9 @@ public function testExecuteLocalizedException() $this->customerData->expects($this->once()) ->method('getDefaultBilling') ->willReturn(false); + $this->customerData->expects($this->once()) + ->method('getAddresses') + ->willReturn([]); $this->customerRepository->expects($this->once()) ->method('save') ->with($this->customerData) @@ -325,6 +376,9 @@ public function testExecuteLocalizedException() $this->assertSame($this->resultJson, $this->controller->execute()); } + /** + * Unit test for verifying Execute Exception during inline edit + */ public function testExecuteException() { $exception = new \Exception('Exception message'); @@ -332,6 +386,9 @@ public function testExecuteException() $this->customerData->expects($this->once()) ->method('getDefaultBilling') ->willReturn(false); + $this->customerData->expects($this->once()) + ->method('getAddresses') + ->willReturn([]); $this->customerRepository->expects($this->once()) ->method('save') ->with($this->customerData) diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassAssignGroupTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassAssignGroupTest.php index 884aab711d16..10144bdc318c 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassAssignGroupTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/MassAssignGroupTest.php @@ -11,6 +11,7 @@ /** * Class MassAssignGroupTest + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class MassAssignGroupTest extends \PHPUnit\Framework\TestCase @@ -70,12 +71,17 @@ class MassAssignGroupTest extends \PHPUnit\Framework\TestCase */ protected $customerRepositoryMock; + /** + * @inheritdoc + */ protected function setUp() { $objectManagerHelper = new ObjectManagerHelper($this); $this->contextMock = $this->createMock(\Magento\Backend\App\Action\Context::class); - $resultRedirectFactory = $this->createMock(\Magento\Backend\Model\View\Result\RedirectFactory::class); + $resultRedirectFactory = $this->createMock( + \Magento\Backend\Model\View\Result\RedirectFactory::class + ); $this->responseMock = $this->createMock(\Magento\Framework\App\ResponseInterface::class); $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) ->disableOriginalConstructor()->getMock(); @@ -129,7 +135,8 @@ protected function setUp() $this->customerCollectionFactoryMock->expects($this->once()) ->method('create') ->willReturn($this->customerCollectionMock); - $this->customerRepositoryMock = $this->getMockBuilder(\Magento\Customer\Api\CustomerRepositoryInterface::class) + $this->customerRepositoryMock = $this + ->getMockBuilder(\Magento\Customer\Api\CustomerRepositoryInterface::class) ->getMockForAbstractClass(); $this->massAction = $objectManagerHelper->getObject( \Magento\Customer\Controller\Adminhtml\Index\MassAssignGroup::class, @@ -142,12 +149,18 @@ protected function setUp() ); } + /** + * Unit test to verify mass customer group assignment use case + * + * @throws \Magento\Framework\Exception\LocalizedException + */ public function testExecute() { $customersIds = [10, 11, 12]; - $customerMock = $this->getMockBuilder( - \Magento\Customer\Api\Data\CustomerInterface::class - )->getMockForAbstractClass(); + $customerMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) + ->setMethods(['setData']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); $this->customerCollectionMock->expects($this->any()) ->method('getAllIds') ->willReturn($customersIds); @@ -168,6 +181,11 @@ public function testExecute() $this->massAction->execute(); } + /** + * Unit test to verify expected error during mass customer group assignment use case + * + * @throws \Magento\Framework\Exception\LocalizedException + */ public function testExecuteWithException() { $customersIds = [10, 11, 12]; diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/SaveTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/SaveTest.php index 5372bb11a89b..57f384d32d98 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 @@ -5,7 +5,6 @@ */ namespace Magento\Customer\Test\Unit\Controller\Adminhtml\Index; -use Magento\Customer\Api\AddressMetadataInterface; use Magento\Customer\Api\CustomerMetadataInterface; use Magento\Customer\Api\Data\AttributeMetadataInterface; use Magento\Customer\Api\Data\CustomerInterface; @@ -15,6 +14,8 @@ use Magento\Framework\Controller\Result\Redirect; /** + * Testing Save Customer use case from admin page + * * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @covers \Magento\Customer\Controller\Adminhtml\Index\Save @@ -281,7 +282,6 @@ protected function setUp() public function testExecuteWithExistentCustomer() { $customerId = 22; - $addressId = 11; $subscription = 'true'; $postValue = [ 'customer' => [ @@ -290,18 +290,6 @@ public function testExecuteWithExistentCustomer() 'coolness' => false, 'disable_auto_group_change' => 'false', ], - 'address' => [ - '_template_' => '_template_', - $addressId => [ - 'entity_id' => $addressId, - 'default_billing' => 'true', - 'default_shipping' => 'true', - 'code' => 'value', - 'coolness' => false, - 'region' => 'region', - 'region_id' => 'region_id', - ], - ], 'subscription' => $subscription, ]; $extractedData = [ @@ -318,22 +306,6 @@ public function testExecuteWithExistentCustomer() CustomerInterface::DEFAULT_BILLING => 2, CustomerInterface::DEFAULT_SHIPPING => 2 ]; - $addressExtractedData = [ - 'entity_id' => $addressId, - 'code' => 'value', - 'coolness' => false, - 'region' => 'region', - 'region_id' => 'region_id', - ]; - $addressCompactedData = [ - 'entity_id' => $addressId, - 'default_billing' => 'true', - 'default_shipping' => 'true', - 'code' => 'value', - 'coolness' => false, - 'region' => 'region', - 'region_id' => 'region_id', - ]; $savedData = [ 'entity_id' => $customerId, 'darkness' => true, @@ -341,61 +313,40 @@ public function testExecuteWithExistentCustomer() CustomerInterface::DEFAULT_BILLING => false, CustomerInterface::DEFAULT_SHIPPING => false, ]; - $savedAddressData = [ - 'entity_id' => $addressId, - 'default_billing' => true, - 'default_shipping' => true, - ]; $mergedData = [ 'entity_id' => $customerId, 'darkness' => true, 'name' => 'Name', 'code' => 'value', 'disable_auto_group_change' => 0, - CustomerInterface::DEFAULT_BILLING => $addressId, - CustomerInterface::DEFAULT_SHIPPING => $addressId, 'confirmation' => false, 'sendemail_store_id' => '1', 'id' => $customerId, ]; - $mergedAddressData = [ - 'entity_id' => $addressId, - 'default_billing' => true, - 'default_shipping' => true, - 'code' => 'value', - 'region' => [ - 'region' => 'region', - 'region_id' => 'region_id', - ], - 'region_id' => 'region_id', - 'id' => $addressId, - ]; /** @var AttributeMetadataInterface|\PHPUnit_Framework_MockObject_MockObject $customerFormMock */ $attributeMock = $this->getMockBuilder( \Magento\Customer\Api\Data\AttributeMetadataInterface::class )->disableOriginalConstructor()->getMock(); - $attributeMock->expects($this->exactly(2)) + $attributeMock->expects($this->atLeastOnce()) ->method('getAttributeCode') ->willReturn('coolness'); - $attributeMock->expects($this->exactly(2)) + $attributeMock->expects($this->atLeastOnce()) ->method('getFrontendInput') ->willReturn('int'); $attributes = [$attributeMock]; - $this->requestMock->expects($this->any()) + $this->requestMock->expects($this->atLeastOnce()) ->method('getPostValue') ->willReturnMap([ [null, null, $postValue], [CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, null, $postValue['customer']], - ['address/' . $addressId, null, $postValue['address'][$addressId]], ]); - $this->requestMock->expects($this->exactly(3)) + $this->requestMock->expects($this->atLeastOnce()) ->method('getPost') ->willReturnMap( [ ['customer', null, $postValue['customer']], - ['address', null, $postValue['address']], ['subscription', null, $subscription], ] ); @@ -404,16 +355,15 @@ public function testExecuteWithExistentCustomer() $objectMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) ->disableOriginalConstructor() ->getMock(); - $objectMock->expects($this->exactly(2)) + $objectMock->expects($this->atLeastOnce()) ->method('getData') ->willReturnMap( [ ['customer', null, $postValue['customer']], - ['address/' . $addressId, null, $postValue['address'][$addressId]], ] ); - $this->objectFactoryMock->expects($this->exactly(2)) + $this->objectFactoryMock->expects($this->exactly(1)) ->method('create') ->with(['data' => $postValue]) ->willReturn($objectMock); @@ -432,23 +382,7 @@ public function testExecuteWithExistentCustomer() $customerFormMock->expects($this->once()) ->method('getAttributes') ->willReturn($attributes); - - $customerAddressFormMock = $this->getMockBuilder( - \Magento\Customer\Model\Metadata\Form::class - )->disableOriginalConstructor()->getMock(); - $customerAddressFormMock->expects($this->once()) - ->method('extractData') - ->with($this->requestMock, 'address/' . $addressId) - ->willReturn($addressExtractedData); - $customerAddressFormMock->expects($this->once()) - ->method('compactData') - ->with($addressExtractedData) - ->willReturn($addressCompactedData); - $customerAddressFormMock->expects($this->once()) - ->method('getAttributes') - ->willReturn($attributes); - - $this->formFactoryMock->expects($this->exactly(2)) + $this->formFactoryMock->expects($this->exactly(1)) ->method('create') ->willReturnMap( [ @@ -461,15 +395,6 @@ public function testExecuteWithExistentCustomer() [], $customerFormMock ], - [ - AddressMetadataInterface::ENTITY_TYPE_ADDRESS, - 'adminhtml_customer_address', - $savedAddressData, - false, - Form::DONT_IGNORE_INVISIBLE, - [], - $customerAddressFormMock - ], ] ); @@ -492,25 +417,7 @@ public function testExecuteWithExistentCustomer() ->with($customerMock) ->willReturn($savedData); - $addressMock = $this->getMockBuilder(\Magento\Customer\Api\Data\AddressInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->customerAddressRepositoryMock->expects($this->once()) - ->method('getById') - ->with($addressId) - ->willReturn($addressMock); - - $this->customerAddressMapperMock->expects($this->once()) - ->method('toFlatArray') - ->with($addressMock) - ->willReturn($savedAddressData); - - $this->addressDataFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($addressMock); - - $this->dataHelperMock->expects($this->exactly(2)) + $this->dataHelperMock->expects($this->atLeastOnce()) ->method('populateWithArray') ->willReturnMap( [ @@ -519,19 +426,9 @@ public function testExecuteWithExistentCustomer() $mergedData, \Magento\Customer\Api\Data\CustomerInterface::class, $this->dataHelperMock ], - [ - $addressMock, - $mergedAddressData, \Magento\Customer\Api\Data\AddressInterface::class, - $this->dataHelperMock - ], ] ); - $customerMock->expects($this->once()) - ->method('setAddresses') - ->with([$addressMock]) - ->willReturnSelf(); - $this->customerRepositoryMock->expects($this->once()) ->method('save') ->with($customerMock) @@ -540,6 +437,10 @@ public function testExecuteWithExistentCustomer() $customerEmail = 'customer@email.com'; $customerMock->expects($this->once())->method('getEmail')->willReturn($customerEmail); + $customerMock->expects($this->once()) + ->method('getAddresses') + ->willReturn([]); + $this->emailNotificationMock->expects($this->once()) ->method('credentialsChanged') ->with($customerMock, $customerEmail) @@ -608,63 +509,33 @@ public function testExecuteWithExistentCustomer() public function testExecuteWithNewCustomer() { $customerId = 22; - $addressId = 11; + $subscription = '0'; $postValue = [ 'customer' => [ 'coolness' => false, 'disable_auto_group_change' => 'false', ], - 'address' => [ - '_template_' => '_template_', - $addressId => [ - 'entity_id' => $addressId, - 'code' => 'value', - 'coolness' => false, - 'region' => 'region', - 'region_id' => 'region_id', - ], - ], 'subscription' => $subscription, ]; $extractedData = [ 'coolness' => false, 'disable_auto_group_change' => 'false', ]; - $addressExtractedData = [ - 'entity_id' => $addressId, - 'code' => 'value', - 'coolness' => false, - 'region' => 'region', - 'region_id' => 'region_id', - ]; $mergedData = [ 'disable_auto_group_change' => 0, CustomerInterface::DEFAULT_BILLING => null, CustomerInterface::DEFAULT_SHIPPING => null, 'confirmation' => false, ]; - $mergedAddressData = [ - 'entity_id' => $addressId, - 'default_billing' => false, - 'default_shipping' => false, - 'code' => 'value', - 'region' => [ - 'region' => 'region', - 'region_id' => 'region_id', - ], - 'region_id' => 'region_id', - 'id' => $addressId, - ]; - /** @var AttributeMetadataInterface|\PHPUnit_Framework_MockObject_MockObject $customerFormMock */ $attributeMock = $this->getMockBuilder( \Magento\Customer\Api\Data\AttributeMetadataInterface::class )->disableOriginalConstructor()->getMock(); - $attributeMock->expects($this->exactly(2)) + $attributeMock->expects($this->atLeastOnce()) ->method('getAttributeCode') ->willReturn('coolness'); - $attributeMock->expects($this->exactly(2)) + $attributeMock->expects($this->atLeastOnce()) ->method('getFrontendInput') ->willReturn('int'); $attributes = [$attributeMock]; @@ -674,14 +545,12 @@ public function testExecuteWithNewCustomer() ->willReturnMap([ [null, null, $postValue], [CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, null, $postValue['customer']], - ['address/' . $addressId, null, $postValue['address'][$addressId]], ]); - $this->requestMock->expects($this->exactly(3)) + $this->requestMock->expects($this->atLeastOnce()) ->method('getPost') ->willReturnMap( [ ['customer', null, $postValue['customer']], - ['address', null, $postValue['address']], ['subscription', null, $subscription], ] ); @@ -690,16 +559,15 @@ public function testExecuteWithNewCustomer() $objectMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) ->disableOriginalConstructor() ->getMock(); - $objectMock->expects($this->exactly(2)) + $objectMock->expects($this->atLeastOnce()) ->method('getData') ->willReturnMap( [ ['customer', null, $postValue['customer']], - ['address/' . $addressId, null, $postValue['address'][$addressId]], ] ); - $this->objectFactoryMock->expects($this->exactly(2)) + $this->objectFactoryMock->expects($this->atLeastOnce()) ->method('create') ->with(['data' => $postValue]) ->willReturn($objectMock); @@ -719,22 +587,7 @@ public function testExecuteWithNewCustomer() ->method('getAttributes') ->willReturn($attributes); - $customerAddressFormMock = $this->getMockBuilder( - \Magento\Customer\Model\Metadata\Form::class - )->disableOriginalConstructor()->getMock(); - $customerAddressFormMock->expects($this->once()) - ->method('extractData') - ->with($this->requestMock, 'address/' . $addressId) - ->willReturn($addressExtractedData); - $customerAddressFormMock->expects($this->once()) - ->method('compactData') - ->with($addressExtractedData) - ->willReturn($addressExtractedData); - $customerAddressFormMock->expects($this->once()) - ->method('getAttributes') - ->willReturn($attributes); - - $this->formFactoryMock->expects($this->exactly(2)) + $this->formFactoryMock->expects($this->exactly(1)) ->method('create') ->willReturnMap( [ @@ -747,15 +600,6 @@ public function testExecuteWithNewCustomer() [], $customerFormMock ], - [ - AddressMetadataInterface::ENTITY_TYPE_ADDRESS, - 'adminhtml_customer_address', - [], - false, - Form::DONT_IGNORE_INVISIBLE, - [], - $customerAddressFormMock - ], ] ); @@ -768,25 +612,7 @@ public function testExecuteWithNewCustomer() ->method('create') ->willReturn($customerMock); - $addressMock = $this->getMockBuilder(\Magento\Customer\Api\Data\AddressInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->addressDataFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($addressMock); - - $this->customerAddressRepositoryMock->expects($this->once()) - ->method('getById') - ->with($addressId) - ->willReturn($addressMock); - - $this->customerAddressMapperMock->expects($this->once()) - ->method('toFlatArray') - ->with($addressMock) - ->willReturn([]); - - $this->dataHelperMock->expects($this->exactly(2)) + $this->dataHelperMock->expects($this->atLeastOnce()) ->method('populateWithArray') ->willReturnMap( [ @@ -795,11 +621,6 @@ public function testExecuteWithNewCustomer() $mergedData, \Magento\Customer\Api\Data\CustomerInterface::class, $this->dataHelperMock ], - [ - $addressMock, - $mergedAddressData, \Magento\Customer\Api\Data\AddressInterface::class, - $this->dataHelperMock - ], ] ); @@ -878,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]; @@ -904,12 +727,11 @@ public function testExecuteWithNewCustomerAndValidationException() [null, null, $postValue], [CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, null, $postValue['customer']], ]); - $this->requestMock->expects($this->exactly(2)) + $this->requestMock->expects($this->atLeastOnce()) ->method('getPost') ->willReturnMap( [ ['customer', null, $postValue['customer']], - ['address', null, null], ] ); @@ -917,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); @@ -930,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, @@ -990,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) @@ -1021,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]; @@ -1047,12 +874,11 @@ public function testExecuteWithNewCustomerAndLocalizedException() [null, null, $postValue], [CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, null, $postValue['customer']], ]); - $this->requestMock->expects($this->exactly(2)) + $this->requestMock->expects($this->atLeastOnce()) ->method('getPost') ->willReturnMap( [ ['customer', null, $postValue['customer']], - ['address', null, null], ] ); @@ -1060,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); @@ -1074,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, @@ -1133,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) @@ -1164,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]; @@ -1190,12 +1021,11 @@ public function testExecuteWithNewCustomerAndException() [null, null, $postValue], [CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, null, $postValue['customer']], ]); - $this->requestMock->expects($this->exactly(2)) + $this->requestMock->expects($this->atLeastOnce()) ->method('getPost') ->willReturnMap( [ ['customer', null, $postValue['customer']], - ['address', null, null], ] ); @@ -1203,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); @@ -1216,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, @@ -1277,7 +1107,10 @@ public function testExecuteWithNewCustomerAndException() $this->sessionMock->expects($this->once()) ->method('setCustomerFormData') - ->with($postValue); + ->with([ + 'customer' => $extractedData, + 'subscription' => $subscription, + ]); /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ValidateTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ValidateTest.php index 7209ac9fd24b..5adb90260163 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ValidateTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ValidateTest.php @@ -141,12 +141,6 @@ protected function setUp() public function testExecute() { - $this->request->expects($this->once()) - ->method('getPost') - ->willReturn([ - '_template_' => null, - 'address_index' => null - ]); $customerEntityId = 2; $this->request->expects($this->once()) ->method('getParam') @@ -162,11 +156,6 @@ public function testExecute() $this->form->expects($this->once())->method('setInvisibleIgnored'); $this->form->expects($this->atLeastOnce())->method('extractData')->willReturn([]); - $error = $this->createMock(\Magento\Framework\Message\Error::class); - $this->form->expects($this->once()) - ->method('validateData') - ->willReturn([$error]); - $validationResult = $this->getMockForAbstractClass( \Magento\Customer\Api\Data\ValidationResultsInterface::class, [], @@ -188,9 +177,6 @@ public function testExecute() public function testExecuteWithoutAddresses() { - $this->request->expects($this->once()) - ->method('getPost') - ->willReturn(null); $this->form->expects($this->once()) ->method('setInvisibleIgnored'); $this->form->expects($this->atLeastOnce()) @@ -223,9 +209,6 @@ public function testExecuteWithoutAddresses() public function testExecuteWithException() { - $this->request->expects($this->once()) - ->method('getPost') - ->willReturn(null); $this->form->expects($this->once()) ->method('setInvisibleIgnored'); $this->form->expects($this->atLeastOnce()) @@ -265,12 +248,6 @@ public function testExecuteWithException() public function testExecuteWithNewCustomerAndNoEntityId() { - $this->request->expects($this->once()) - ->method('getPost') - ->willReturn([ - '_template_' => null, - 'address_index' => null - ]); $this->request->expects($this->once()) ->method('getParam') ->with('customer') @@ -282,11 +259,6 @@ public function testExecuteWithNewCustomerAndNoEntityId() $this->form->expects($this->once())->method('setInvisibleIgnored'); $this->form->expects($this->atLeastOnce())->method('extractData')->willReturn([]); - $error = $this->createMock(\Magento\Framework\Message\Error::class); - $this->form->expects($this->once()) - ->method('validateData') - ->willReturn([$error]); - $validationResult = $this->getMockForAbstractClass( \Magento\Customer\Api\Data\ValidationResultsInterface::class, [], diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Ajax/LoginTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Ajax/LoginTest.php index aaaa799a5e26..14ed09f73325 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Ajax/LoginTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Ajax/LoginTest.php @@ -3,13 +3,32 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); -/** - * Test customer ajax login controller - */ namespace Magento\Customer\Test\Unit\Controller\Ajax; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Controller\Ajax\Login; +use Magento\Customer\Model\Account\Redirect; +use Magento\Customer\Model\AccountManagement; +use Magento\Customer\Model\Session; +use Magento\Framework\App\Action\Context; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Request\Http; +use Magento\Framework\App\Response\RedirectInterface; +use Magento\Framework\App\ResponseInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\Result\JsonFactory; +use Magento\Framework\Controller\Result\Raw; +use Magento\Framework\Controller\Result\RawFactory; use Magento\Framework\Exception\InvalidEmailOrPasswordException; +use Magento\Framework\Json\Helper\Data; +use Magento\Framework\ObjectManager\ObjectManager as FakeObjectManager; +use Magento\Framework\Stdlib\Cookie\CookieMetadata; +use Magento\Framework\Stdlib\Cookie\CookieMetadataFactory; +use Magento\Framework\Stdlib\CookieManagerInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use PHPUnit_Framework_MockObject_MockObject as MockObject; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -17,223 +36,190 @@ class LoginTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Customer\Controller\Ajax\Login + * @var Login */ - protected $object; + private $controller; /** - * @var \Magento\Framework\App\Request\Http|\PHPUnit_Framework_MockObject_MockObject + * @var Http|MockObject */ - protected $request; + private $request; /** - * @var \Magento\Framework\App\ResponseInterface|\PHPUnit_Framework_MockObject_MockObject + * @var ResponseInterface|MockObject */ - protected $response; + private $response; /** - * @var \Magento\Customer\Model\Session|\PHPUnit_Framework_MockObject_MockObject + * @var Session|MockObject */ - protected $customerSession; + private $customerSession; /** - * @var \Magento\Framework\ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var FakeObjectManager|MockObject */ - protected $objectManager; + private $objectManager; /** - * @var \Magento\Customer\Api\AccountManagementInterface|\PHPUnit_Framework_MockObject_MockObject + * @var AccountManagement|MockObject */ - protected $customerAccountManagementMock; + private $accountManagement; /** - * @var \Magento\Framework\Json\Helper\Data|\PHPUnit_Framework_MockObject_MockObject + * @var Data|MockObject */ - protected $jsonHelperMock; + private $jsonHelper; /** - * @var \Magento\Framework\Controller\Result\Json|\PHPUnit_Framework_MockObject_MockObject + * @var Json|MockObject */ - protected $resultJson; + private $resultJson; /** - * @var \Magento\Framework\Controller\Result\JsonFactory| \PHPUnit_Framework_MockObject_MockObject + * @var JsonFactory|MockObject */ - protected $resultJsonFactory; + private $resultJsonFactory; /** - * @var \Magento\Framework\Controller\Result\Raw| \PHPUnit_Framework_MockObject_MockObject + * @var Raw|MockObject */ - protected $resultRaw; + private $resultRaw; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var RedirectInterface|MockObject */ - protected $redirectMock; + private $redirect; /** - * @var \Magento\Framework\Stdlib\CookieManagerInterface| \PHPUnit_Framework_MockObject_MockObject + * @var CookieManagerInterface|MockObject */ private $cookieManager; /** - * @var \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory| \PHPUnit_Framework_MockObject_MockObject + * @var CookieMetadataFactory|MockObject */ private $cookieMetadataFactory; /** - * @var \Magento\Framework\Stdlib\Cookie\CookieMetadata| \PHPUnit_Framework_MockObject_MockObject + * @inheritdoc */ - private $cookieMetadata; - - protected function setUp() + protected function setUp(): void { - $this->request = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) - ->disableOriginalConstructor()->getMock(); + $this->request = $this->getMockBuilder(Http::class) + ->disableOriginalConstructor() + ->getMock(); $this->response = $this->createPartialMock( - \Magento\Framework\App\ResponseInterface::class, + ResponseInterface::class, ['setRedirect', 'sendResponse', 'representJson', 'setHttpResponseCode'] ); $this->customerSession = $this->createPartialMock( - \Magento\Customer\Model\Session::class, + Session::class, [ 'isLoggedIn', 'getLastCustomerId', 'getBeforeAuthUrl', 'setBeforeAuthUrl', 'setCustomerDataAsLoggedIn', - 'regenerateId' + 'regenerateId', + 'getData' ] ); - $this->objectManager = $this->createPartialMock(\Magento\Framework\ObjectManager\ObjectManager::class, ['get']); - $this->customerAccountManagementMock = - $this->createPartialMock(\Magento\Customer\Model\AccountManagement::class, ['authenticate']); + $this->objectManager = $this->createPartialMock(FakeObjectManager::class, ['get']); + $this->accountManagement = $this->createPartialMock(AccountManagement::class, ['authenticate']); - $this->jsonHelperMock = $this->createPartialMock(\Magento\Framework\Json\Helper\Data::class, ['jsonDecode']); + $this->jsonHelper = $this->createPartialMock(Data::class, ['jsonDecode']); - $this->resultJson = $this->getMockBuilder(\Magento\Framework\Controller\Result\Json::class) + $this->resultJson = $this->getMockBuilder(Json::class) ->disableOriginalConstructor() ->getMock(); - $this->resultJsonFactory = $this->getMockBuilder(\Magento\Framework\Controller\Result\JsonFactory::class) + $this->resultJsonFactory = $this->getMockBuilder(JsonFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $this->cookieManager = $this->getMockBuilder(\Magento\Framework\Stdlib\CookieManagerInterface::class) + $this->cookieManager = $this->getMockBuilder(CookieManagerInterface::class) ->setMethods(['getCookie', 'deleteCookie']) ->getMockForAbstractClass(); - $this->cookieMetadataFactory = $this->getMockBuilder( - \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory::class - )->disableOriginalConstructor()->getMock(); - $this->cookieMetadata = $this->getMockBuilder(\Magento\Framework\Stdlib\Cookie\CookieMetadata::class) + $this->cookieMetadataFactory = $this->getMockBuilder(CookieMetadataFactory::class) ->disableOriginalConstructor() ->getMock(); - $this->resultRaw = $this->getMockBuilder(\Magento\Framework\Controller\Result\Raw::class) + $this->resultRaw = $this->getMockBuilder(Raw::class) ->disableOriginalConstructor() ->getMock(); - $resultRawFactory = $this->getMockBuilder(\Magento\Framework\Controller\Result\RawFactory::class) + $resultRawFactory = $this->getMockBuilder(RawFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $resultRawFactory->expects($this->atLeastOnce()) - ->method('create') + $resultRawFactory->method('create') ->willReturn($this->resultRaw); - $contextMock = $this->createMock(\Magento\Framework\App\Action\Context::class); - $this->redirectMock = $this->createMock(\Magento\Framework\App\Response\RedirectInterface::class); - $contextMock->expects($this->atLeastOnce())->method('getRedirect')->willReturn($this->redirectMock); - $contextMock->expects($this->atLeastOnce())->method('getRequest')->willReturn($this->request); - - $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->object = $objectManager->getObject( - \Magento\Customer\Controller\Ajax\Login::class, + /** @var Context|MockObject $context */ + $context = $this->createMock(Context::class); + $this->redirect = $this->createMock(RedirectInterface::class); + $context->method('getRedirect') + ->willReturn($this->redirect); + $context->method('getRequest') + ->willReturn($this->request); + + $objectManager = new ObjectManager($this); + $this->controller = $objectManager->getObject( + Login::class, [ - 'context' => $contextMock, + 'context' => $context, 'customerSession' => $this->customerSession, - 'helper' => $this->jsonHelperMock, + 'helper' => $this->jsonHelper, 'response' => $this->response, 'resultRawFactory' => $resultRawFactory, 'resultJsonFactory' => $this->resultJsonFactory, 'objectManager' => $this->objectManager, - 'customerAccountManagement' => $this->customerAccountManagementMock, + 'customerAccountManagement' => $this->accountManagement, 'cookieManager' => $this->cookieManager, 'cookieMetadataFactory' => $this->cookieMetadataFactory ] ); } - public function testLogin() + /** + * Checks successful login. + */ + public function testLogin(): void { $jsonRequest = '{"username":"customer@example.com", "password":"password"}'; $loginSuccessResponse = '{"errors": false, "message":"Login successful."}'; + $this->withRequest($jsonRequest); - $this->request - ->expects($this->any()) - ->method('getContent') - ->willReturn($jsonRequest); - - $this->request - ->expects($this->any()) - ->method('getMethod') - ->willReturn('POST'); - - $this->request - ->expects($this->any()) - ->method('isXmlHttpRequest') - ->willReturn(true); - - $this->resultJsonFactory->expects($this->atLeastOnce()) - ->method('create') + $this->resultJsonFactory->method('create') ->willReturn($this->resultJson); - $this->jsonHelperMock - ->expects($this->any()) - ->method('jsonDecode') + $this->jsonHelper->method('jsonDecode') ->with($jsonRequest) ->willReturn(['username' => 'customer@example.com', 'password' => 'password']); - $customerMock = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\CustomerInterface::class); - $this->customerAccountManagementMock - ->expects($this->any()) - ->method('authenticate') + /** @var CustomerInterface|MockObject $customer */ + $customer = $this->getMockForAbstractClass(CustomerInterface::class); + $this->accountManagement->method('authenticate') ->with('customer@example.com', 'password') - ->willReturn($customerMock); + ->willReturn($customer); - $this->customerSession->expects($this->once()) - ->method('setCustomerDataAsLoggedIn') - ->with($customerMock); + $this->customerSession->method('setCustomerDataAsLoggedIn') + ->with($customer); + $this->customerSession->method('regenerateId'); - $this->customerSession->expects($this->once())->method('regenerateId'); + /** @var Redirect|MockObject $redirect */ + $redirect = $this->createMock(Redirect::class); + $this->controller->setAccountRedirect($redirect); + $redirect->method('getRedirectCookie') + ->willReturn('some_url1'); - $redirectMock = $this->createMock(\Magento\Customer\Model\Account\Redirect::class); - $this->object->setAccountRedirect($redirectMock); - $redirectMock->expects($this->once())->method('getRedirectCookie')->willReturn('some_url1'); + $this->withCookieManager(); - $this->cookieManager->expects($this->once()) - ->method('getCookie') - ->with('mage-cache-sessid') - ->willReturn(true); - $this->cookieMetadataFactory->expects($this->once()) - ->method('createCookieMetadata') - ->willReturn($this->cookieMetadata); - $this->cookieMetadata->expects($this->once()) - ->method('setPath') - ->with('/') - ->willReturnSelf(); - $this->cookieManager->expects($this->once()) - ->method('deleteCookie') - ->with('mage-cache-sessid', $this->cookieMetadata) - ->willReturnSelf(); + $this->withScopeConfig(); - $scopeConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); - $this->object->setScopeConfig($scopeConfigMock); - $scopeConfigMock->expects($this->once())->method('getValue') - ->with('customer/startup/redirect_dashboard') - ->willReturn(0); - - $this->redirectMock->expects($this->once())->method('success')->willReturn('some_url2'); - $this->resultRaw->expects($this->never())->method('setHttpResponseCode'); + $this->redirect->method('success') + ->willReturn('some_url2'); + $this->resultRaw->expects(self::never()) + ->method('setHttpResponseCode'); $result = [ 'errors' => false, @@ -241,67 +227,99 @@ public function testLogin() 'redirectUrl' => 'some_url2', ]; - $this->resultJson - ->expects($this->once()) - ->method('setData') + $this->resultJson->method('setData') ->with($result) ->willReturn($loginSuccessResponse); - $this->assertEquals($loginSuccessResponse, $this->object->execute()); + self::assertEquals($loginSuccessResponse, $this->controller->execute()); } - public function testLoginFailure() + /** + * Checks unsuccessful login. + */ + public function testLoginFailure(): void { $jsonRequest = '{"username":"invalid@example.com", "password":"invalid"}'; $loginFailureResponse = '{"message":"Invalid login or password."}'; + $this->withRequest($jsonRequest); - $this->request - ->expects($this->any()) - ->method('getContent') - ->willReturn($jsonRequest); - - $this->request - ->expects($this->any()) - ->method('getMethod') - ->willReturn('POST'); - - $this->request - ->expects($this->any()) - ->method('isXmlHttpRequest') - ->willReturn(true); - - $this->resultJsonFactory->expects($this->once()) - ->method('create') + $this->resultJsonFactory->method('create') ->willReturn($this->resultJson); - $this->jsonHelperMock - ->expects($this->any()) - ->method('jsonDecode') + $this->jsonHelper->method('jsonDecode') ->with($jsonRequest) ->willReturn(['username' => 'invalid@example.com', 'password' => 'invalid']); - $customerMock = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\CustomerInterface::class); - $this->customerAccountManagementMock - ->expects($this->any()) - ->method('authenticate') + /** @var CustomerInterface|MockObject $customer */ + $customer = $this->getMockForAbstractClass(CustomerInterface::class); + $this->accountManagement->method('authenticate') ->with('invalid@example.com', 'invalid') ->willThrowException(new InvalidEmailOrPasswordException(__('Invalid login or password.'))); - $this->customerSession->expects($this->never()) + $this->customerSession->expects(self::never()) ->method('setCustomerDataAsLoggedIn') - ->with($customerMock); - - $this->customerSession->expects($this->never())->method('regenerateId'); + ->with($customer); + $this->customerSession->expects(self::never()) + ->method('regenerateId'); $result = [ 'errors' => true, 'message' => __('Invalid login or password.') ]; - $this->resultJson - ->expects($this->once()) - ->method('setData') + $this->resultJson->method('setData') ->with($result) ->willReturn($loginFailureResponse); - $this->assertEquals($loginFailureResponse, $this->object->execute()); + self::assertEquals($loginFailureResponse, $this->controller->execute()); + } + + /** + * Emulates request behavior. + * + * @param string $jsonRequest + */ + private function withRequest(string $jsonRequest): void + { + $this->request->method('getContent') + ->willReturn($jsonRequest); + + $this->request->method('getMethod') + ->willReturn('POST'); + + $this->request->method('isXmlHttpRequest') + ->willReturn(true); + } + + /** + * Emulates cookie manager behavior. + */ + private function withCookieManager(): void + { + $this->cookieManager->method('getCookie') + ->with('mage-cache-sessid') + ->willReturn(true); + $cookieMetadata = $this->getMockBuilder(CookieMetadata::class) + ->disableOriginalConstructor() + ->getMock(); + $this->cookieMetadataFactory->method('createCookieMetadata') + ->willReturn($cookieMetadata); + $cookieMetadata->method('setPath') + ->with('/') + ->willReturnSelf(); + $this->cookieManager->method('deleteCookie') + ->with('mage-cache-sessid', $cookieMetadata) + ->willReturnSelf(); + } + + /** + * Emulates config behavior. + */ + private function withScopeConfig(): void + { + /** @var ScopeConfigInterface|MockObject $scopeConfig */ + $scopeConfig = $this->createMock(ScopeConfigInterface::class); + $this->controller->setScopeConfig($scopeConfig); + $scopeConfig->method('getValue') + ->with('customer/startup/redirect_dashboard') + ->willReturn(0); } } diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Section/LoadTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Section/LoadTest.php index f4bf184f9ebf..5a7cf42be2c7 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Section/LoadTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Section/LoadTest.php @@ -83,13 +83,13 @@ protected function setUp() } /** - * @param $sectionNames - * @param $updateSectionID - * @param $sectionNamesAsArray - * @param $updateIds + * @param string $sectionNames + * @param bool $forceNewSectionTimestamp + * @param string[] $sectionNamesAsArray + * @param bool $forceNewTimestamp * @dataProvider executeDataProvider */ - public function testExecute($sectionNames, $updateSectionID, $sectionNamesAsArray, $updateIds) + public function testExecute($sectionNames, $forceNewSectionTimestamp, $sectionNamesAsArray, $forceNewTimestamp) { $this->resultJsonFactoryMock->expects($this->once()) ->method('create') @@ -103,12 +103,12 @@ public function testExecute($sectionNames, $updateSectionID, $sectionNamesAsArra $this->httpRequestMock->expects($this->exactly(2)) ->method('getParam') - ->withConsecutive(['sections'], ['update_section_id']) - ->willReturnOnConsecutiveCalls($sectionNames, $updateSectionID); + ->withConsecutive(['sections'], ['force_new_section_timestamp']) + ->willReturnOnConsecutiveCalls($sectionNames, $forceNewSectionTimestamp); $this->sectionPoolMock->expects($this->once()) ->method('getSectionsData') - ->with($sectionNamesAsArray, $updateIds) + ->with($sectionNamesAsArray, $forceNewTimestamp) ->willReturn([ 'message' => 'some message', 'someKey' => 'someValue' @@ -133,15 +133,15 @@ public function executeDataProvider() return [ [ 'sectionNames' => 'sectionName1,sectionName2,sectionName3', - 'updateSectionID' => 'updateSectionID', + 'forceNewSectionTimestamp' => 'forceNewSectionTimestamp', 'sectionNamesAsArray' => ['sectionName1', 'sectionName2', 'sectionName3'], - 'updateIds' => true + 'forceNewTimestamp' => true ], [ 'sectionNames' => null, - 'updateSectionID' => null, + 'forceNewSectionTimestamp' => null, 'sectionNamesAsArray' => null, - 'updateIds' => false + 'forceNewTimestamp' => false ], ]; } diff --git a/app/code/Magento/Customer/Test/Unit/CustomerData/SectionPoolTest.php b/app/code/Magento/Customer/Test/Unit/CustomerData/SectionPoolTest.php index 98fee70e335f..2b67df1aee29 100644 --- a/app/code/Magento/Customer/Test/Unit/CustomerData/SectionPoolTest.php +++ b/app/code/Magento/Customer/Test/Unit/CustomerData/SectionPoolTest.php @@ -63,7 +63,7 @@ public function testGetSectionsDataAllSections() $this->identifierMock->expects($this->once()) ->method('markSections') - //check also default value for $updateIds = false + //check also default value for $forceTimestamp = false ->with($allSectionsData, $sectionNames, false) ->willReturn($identifierResult); $modelResult = $this->model->getSectionsData($sectionNames); diff --git a/app/code/Magento/Customer/Test/Unit/Helper/AddressTest.php b/app/code/Magento/Customer/Test/Unit/Helper/AddressTest.php index 74af4ec57c77..0818d94afe57 100644 --- a/app/code/Magento/Customer/Test/Unit/Helper/AddressTest.php +++ b/app/code/Magento/Customer/Test/Unit/Helper/AddressTest.php @@ -212,7 +212,7 @@ public function getConvertStreetLinesDataProvider() public function testIsVatValidationEnabled($store, $result) { $this->scopeConfig->expects($this->once()) - ->method('getValue') + ->method('isSetFlag') ->with( \Magento\Customer\Helper\Address::XML_PATH_VAT_VALIDATION_ENABLED, \Magento\Store\Model\ScopeInterface::SCOPE_STORE, @@ -242,7 +242,7 @@ public function getVatValidationEnabledDataProvider() public function testHasValidateOnEachTransaction($store, $result) { $this->scopeConfig->expects($this->once()) - ->method('getValue') + ->method('isSetFlag') ->with( \Magento\Customer\Helper\Address::XML_PATH_VIV_ON_EACH_TRANSACTION, \Magento\Store\Model\ScopeInterface::SCOPE_STORE, @@ -297,7 +297,7 @@ public function getTaxCalculationAddressTypeDataProvider() public function testIsDisableAutoGroupAssignDefaultValue() { $this->scopeConfig->expects($this->once()) - ->method('getValue') + ->method('isSetFlag') ->with( \Magento\Customer\Helper\Address::XML_PATH_VIV_DISABLE_AUTO_ASSIGN_DEFAULT, \Magento\Store\Model\ScopeInterface::SCOPE_STORE @@ -309,7 +309,7 @@ public function testIsDisableAutoGroupAssignDefaultValue() public function testIsVatAttributeVisible() { $this->scopeConfig->expects($this->once()) - ->method('getValue') + ->method('isSetFlag') ->with( \Magento\Customer\Helper\Address::XML_PATH_VAT_FRONTEND_VISIBILITY, \Magento\Store\Model\ScopeInterface::SCOPE_STORE @@ -414,4 +414,17 @@ public function isAttributeVisibleDataProvider() ['invalid_code', false] ]; } + + /** + * Data provider for test testIsAttributeRequire + * + * @return array + */ + public function isAttributeRequiredDataProvider() + { + return [ + ['fax', true], + ['invalid_code', false] + ]; + } } diff --git a/app/code/Magento/Customer/Test/Unit/Model/AccountConfirmationTest.php b/app/code/Magento/Customer/Test/Unit/Model/AccountConfirmationTest.php index ae246665b28e..1ce80d9d1e99 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/AccountConfirmationTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/AccountConfirmationTest.php @@ -59,7 +59,7 @@ public function testIsConfirmationRequired( $websiteId = 1; $this->scopeConfig->expects($this->any()) - ->method('getValue') + ->method('isSetFlag') ->with( $this->accountConfirmation::XML_PATH_IS_CONFIRM, ScopeInterface::SCOPE_WEBSITES, diff --git a/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php b/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php index aad20f757e91..209a9b077a30 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php @@ -6,10 +6,13 @@ namespace Magento\Customer\Test\Unit\Model; -use Magento\Customer\Model\AccountManagement; +use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Model\AccountConfirmation; +use Magento\Customer\Model\AccountManagement; use Magento\Customer\Model\AuthenticationInterface; +use Magento\Customer\Model\Data\Customer; use Magento\Customer\Model\EmailNotificationInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\App\Area; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Intl\DateTimeFactory; @@ -142,6 +145,16 @@ class AccountManagementTest extends \PHPUnit\Framework\TestCase */ private $saveHandler; + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Customer\Model\AddressRegistry + */ + private $addressRegistryMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|SearchCriteriaBuilder + */ + private $searchCriteriaBuilderMock; + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -176,6 +189,7 @@ protected function setUp() $this->dateTime = $this->createMock(\Magento\Framework\Stdlib\DateTime::class); $this->customer = $this->createMock(\Magento\Customer\Model\Customer::class); $this->objectFactory = $this->createMock(\Magento\Framework\DataObjectFactory::class); + $this->addressRegistryMock = $this->createMock(\Magento\Customer\Model\AddressRegistry::class); $this->extensibleDataObjectConverter = $this->createMock( \Magento\Framework\Api\ExtensibleDataObjectConverter::class ); @@ -193,6 +207,7 @@ protected function setUp() $this->dateTimeFactory = $this->createMock(DateTimeFactory::class); $this->accountConfirmation = $this->createMock(AccountConfirmation::class); + $this->searchCriteriaBuilderMock = $this->createMock(SearchCriteriaBuilder::class); $this->visitorCollectionFactory = $this->getMockBuilder( \Magento\Customer\Model\ResourceModel\Visitor\CollectionFactory::class @@ -239,6 +254,8 @@ protected function setUp() 'sessionManager' => $this->sessionManager, 'saveHandler' => $this->saveHandler, 'visitorCollectionFactory' => $this->visitorCollectionFactory, + 'searchCriteriaBuilder' => $this->searchCriteriaBuilderMock, + 'addressRegistry' => $this->addressRegistryMock, ] ); $this->objectManagerHelper->setBackwardCompatibleProperty( @@ -268,7 +285,7 @@ public function testCreateAccountWithPasswordHashWithExistingCustomer() $website->expects($this->once()) ->method('getStoreIds') ->willReturn([1, 2, 3]); - $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class)->getMock(); + $customer = $this->getMockBuilder(Customer::class)->disableOriginalConstructor()->getMock(); $customer->expects($this->once()) ->method('getId') ->willReturn($customerId); @@ -324,7 +341,7 @@ public function testCreateAccountWithPasswordHashWithCustomerWithoutStoreId() $website->expects($this->once()) ->method('getDefaultStore') ->willReturn($store); - $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class)->getMock(); + $customer = $this->getMockBuilder(Customer::class)->disableOriginalConstructor()->getMock(); $customer->expects($this->atLeastOnce()) ->method('getId') ->willReturn($customerId); @@ -400,7 +417,7 @@ public function testCreateAccountWithPasswordHashWithLocalizedException() $website->expects($this->once()) ->method('getDefaultStore') ->willReturn($store); - $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class)->getMock(); + $customer = $this->getMockBuilder(Customer::class)->disableOriginalConstructor()->getMock(); $customer->expects($this->atLeastOnce()) ->method('getId') ->willReturn($customerId); @@ -479,7 +496,7 @@ public function testCreateAccountWithPasswordHashWithAddressException() $website->expects($this->once()) ->method('getDefaultStore') ->willReturn($store); - $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class)->getMock(); + $customer = $this->getMockBuilder(Customer::class)->disableOriginalConstructor()->getMock(); $customer->expects($this->atLeastOnce()) ->method('getId') ->willReturn($customerId); @@ -548,8 +565,9 @@ public function testCreateAccountWithPasswordHashWithNewCustomerAndLocalizedExce $websiteId = 1; $hash = '4nj54lkj5jfi03j49f8bgujfgsd'; - $customerMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) - ->getMockForAbstractClass(); + $customerMock = $this->getMockBuilder(Customer::class) + ->disableOriginalConstructor() + ->getMock(); $customerMock->expects($this->atLeastOnce()) ->method('getId') @@ -640,7 +658,7 @@ public function testCreateAccountWithoutPassword() $website->expects($this->once()) ->method('getDefaultStore') ->willReturn($store); - $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class)->getMock(); + $customer = $this->getMockBuilder(Customer::class)->disableOriginalConstructor()->getMock(); $customer->expects($this->atLeastOnce()) ->method('getId') ->willReturn($customerId); @@ -785,7 +803,7 @@ public function testCreateAccountWithPasswordInputException( $minCharacterSetsNum . '. Classes of characters: Lower Case, Upper Case, Digits, Special Characters.'); } - $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class)->getMock(); + $customer = $this->getMockBuilder(Customer::class)->disableOriginalConstructor()->getMock(); $this->accountManagement->createAccount($customer, $password); } @@ -806,7 +824,7 @@ public function testCreateAccountInputExceptionExtraLongPassword() $this->expectException(\Magento\Framework\Exception\InputException::class); $this->expectExceptionMessage('Please enter a password with at most 256 characters.'); - $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class)->getMock(); + $customer = $this->getMockBuilder(Customer::class)->disableOriginalConstructor()->getMock(); $this->accountManagement->createAccount($customer, $password); } @@ -885,7 +903,7 @@ public function testCreateAccountWithPassword() $website->expects($this->once()) ->method('getDefaultStore') ->willReturn($store); - $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class)->getMock(); + $customer = $this->getMockBuilder(Customer::class)->disableOriginalConstructor()->getMock(); $customer->expects($this->atLeastOnce()) ->method('getId') ->willReturn($customerId); @@ -969,7 +987,8 @@ public function testSendPasswordReminderEmail() $templateIdentifier = 'Template Identifier'; $sender = 'Sender'; - $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) + $customer = $this->getMockBuilder(Customer::class) + ->disableOriginalConstructor() ->getMock(); $customer->expects($this->any()) ->method('getStoreId') @@ -1001,7 +1020,7 @@ public function testSendPasswordReminderEmail() $this->dataObjectProcessor->expects($this->once()) ->method('buildOutputDataArray') - ->with($customer, \Magento\Customer\Api\Data\CustomerInterface::class) + ->with($customer, CustomerInterface::class) ->willReturn($customerData); $this->customerViewHelper->expects($this->any()) @@ -1071,9 +1090,8 @@ public function testSendPasswordReminderEmail() protected function prepareInitiatePasswordReset($email, $templateIdentifier, $sender, $storeId, $customerId, $hash) { $websiteId = 1; - + $addressId = 5; $datetime = $this->prepareDateTimeFactory(); - $customerData = ['key' => 'value']; $customerName = 'Customer Name'; @@ -1083,12 +1101,23 @@ protected function prepareInitiatePasswordReset($email, $templateIdentifier, $se $this->store->expects($this->any()) ->method('getId') ->willReturn($storeId); - $this->storeManager->expects($this->any()) ->method('getStore') ->willReturn($this->store); - $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) + /** @var \Magento\Customer\Model\Address|\PHPUnit_Framework_MockObject_MockObject $addressModel */ + $addressModel = $this->getMockBuilder(\Magento\Customer\Model\Address::class)->disableOriginalConstructor() + ->setMethods(['setShouldIgnoreValidation'])->getMock(); + + /** @var \Magento\Customer\Api\Data\AddressInterface|\PHPUnit_Framework_MockObject_MockObject $customer */ + $address = $this->createMock(\Magento\Customer\Api\Data\AddressInterface::class); + $address->expects($this->once()) + ->method('getId') + ->willReturn($addressId); + + /** @var Customer|\PHPUnit_Framework_MockObject_MockObject $customer */ + $customer = $this->getMockBuilder(Customer::class) + ->disableOriginalConstructor() ->getMock(); $customer->expects($this->any()) ->method('getEmail') @@ -1099,7 +1128,19 @@ protected function prepareInitiatePasswordReset($email, $templateIdentifier, $se $customer->expects($this->any()) ->method('getStoreId') ->willReturn($storeId); - + $customer->expects($this->any()) + ->method('getAddresses') + ->willReturn([$address]); + $this->customerRepository->expects($this->once()) + ->method('get') + ->willReturn($customer); + $this->addressRegistryMock->expects($this->once()) + ->method('retrieve') + ->with($addressId) + ->willReturn($addressModel); + $addressModel->expects($this->once()) + ->method('setShouldIgnoreValidation') + ->with(true); $this->customerRepository->expects($this->once()) ->method('get') ->with($email, $websiteId) @@ -1108,16 +1149,13 @@ protected function prepareInitiatePasswordReset($email, $templateIdentifier, $se ->method('save') ->with($customer) ->willReturnSelf(); - $this->random->expects($this->once()) ->method('getUniqueHash') ->willReturn($hash); - $this->customerViewHelper->expects($this->any()) ->method('getCustomerName') ->with($customer) ->willReturn($customerName); - $this->customerSecure->expects($this->any()) ->method('setRpToken') ->with($hash) @@ -1134,15 +1172,13 @@ protected function prepareInitiatePasswordReset($email, $templateIdentifier, $se ->method('setData') ->with('name', $customerName) ->willReturnSelf(); - $this->customerRegistry->expects($this->any()) ->method('retrieveSecureData') ->with($customerId) ->willReturn($this->customerSecure); - $this->dataObjectProcessor->expects($this->any()) ->method('buildOutputDataArray') - ->with($customer, \Magento\Customer\Api\Data\CustomerInterface::class) + ->with($customer, Customer::class) ->willReturn($customerData); $this->prepareEmailSend($email, $templateIdentifier, $sender, $storeId, $customerName); @@ -1202,8 +1238,7 @@ public function testInitiatePasswordResetEmailReminder() $storeId = 1; - mt_srand(mt_rand() + (100000000 * (float)microtime()) % PHP_INT_MAX); - $hash = md5(uniqid(microtime() . mt_rand(0, mt_getrandmax()), true)); + $hash = hash('sha256', microtime() . random_int(PHP_INT_MIN, PHP_INT_MAX)); $this->emailNotificationMock->expects($this->once()) ->method('passwordReminder') @@ -1227,8 +1262,7 @@ public function testInitiatePasswordResetEmailReset() $templateIdentifier = 'Template Identifier'; $sender = 'Sender'; - mt_srand(mt_rand() + (100000000 * (float)microtime()) % PHP_INT_MAX); - $hash = md5(uniqid(microtime() . mt_rand(0, mt_getrandmax()), true)); + $hash = hash('sha256', microtime() . random_int(PHP_INT_MIN, PHP_INT_MAX)); $this->emailNotificationMock->expects($this->once()) ->method('passwordResetConfirmation') @@ -1252,8 +1286,7 @@ public function testInitiatePasswordResetNoTemplate() $templateIdentifier = 'Template Identifier'; $sender = 'Sender'; - mt_srand(mt_rand() + (100000000 * (float)microtime()) % PHP_INT_MAX); - $hash = md5(uniqid(microtime() . mt_rand(0, mt_getrandmax()), true)); + $hash = hash('sha256', microtime() . random_int(PHP_INT_MIN, PHP_INT_MAX)); $this->prepareInitiatePasswordReset($email, $templateIdentifier, $sender, $storeId, $customerId, $hash); @@ -1266,11 +1299,11 @@ public function testInitiatePasswordResetNoTemplate() /** * @expectedException \Magento\Framework\Exception\InputException - * @expectedExceptionMessage Invalid value of "" provided for the customerId field + * @expectedExceptionMessage Invalid value of "0" provided for the customerId field */ public function testValidateResetPasswordTokenBadCustomerId() { - $this->accountManagement->validateResetPasswordLinkToken(null, ''); + $this->accountManagement->validateResetPasswordLinkToken(0, ''); } /** @@ -1413,6 +1446,7 @@ private function reInitModel() 'encryptor' => $this->encryptor, 'dataProcessor' => $this->dataObjectProcessor, 'storeManager' => $this->storeManager, + 'addressRegistry' => $this->addressRegistryMock, 'transportBuilder' => $this->transportBuilder, ] ); @@ -1435,11 +1469,15 @@ public function testChangePassword() $passwordHash = '1a2b3f4c'; $this->reInitModel(); - $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) + $customer = $this->getMockBuilder(CustomerInterface::class) + ->disableOriginalConstructor() ->getMock(); $customer->expects($this->any()) ->method('getId') ->willReturn($customerId); + $customer->expects($this->once()) + ->method('getAddresses') + ->willReturn([]); $this->customerRepository ->expects($this->once()) @@ -1525,12 +1563,34 @@ public function testResetPassword() { $customerEmail = 'customer@example.com'; $customerId = '1'; + $addressId = 5; $resetToken = 'newStringToken'; $newPassword = 'new_password'; $this->reInitModel(); - $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class)->getMock(); + /** @var \Magento\Customer\Model\Address|\PHPUnit_Framework_MockObject_MockObject $addressModel */ + $addressModel = $this->getMockBuilder(\Magento\Customer\Model\Address::class)->disableOriginalConstructor() + ->setMethods(['setShouldIgnoreValidation'])->getMock(); + + /** @var \Magento\Customer\Api\Data\AddressInterface|\PHPUnit_Framework_MockObject_MockObject $customer */ + $address = $this->createMock(\Magento\Customer\Api\Data\AddressInterface::class); + $address->expects($this->any()) + ->method('getId') + ->willReturn($addressId); + + /** @var Customer|\PHPUnit_Framework_MockObject_MockObject $customer */ + $customer = $this->getMockBuilder(Customer::class)->disableOriginalConstructor()->getMock(); $customer->expects($this->any())->method('getId')->willReturn($customerId); + $customer->expects($this->any()) + ->method('getAddresses') + ->willReturn([$address]); + $this->addressRegistryMock->expects($this->once()) + ->method('retrieve') + ->with($addressId) + ->willReturn($addressModel); + $addressModel->expects($this->once()) + ->method('setShouldIgnoreValidation') + ->with(true); $this->customerRepository->expects($this->atLeastOnce())->method('get')->with($customerEmail) ->willReturn($customer); $this->customer->expects($this->atLeastOnce())->method('getResetPasswordLinkExpirationPeriod') @@ -1547,7 +1607,7 @@ function ($string) { $this->customerSecure->expects($this->once())->method('setRpTokenCreatedAt')->with(null); $this->customerSecure->expects($this->any())->method('setPasswordHash')->willReturn(null); - $this->sessionManager->expects($this->atLeastOnce())->method('destroy'); + $this->sessionManager->method('isSessionExists')->willReturn(false); $this->sessionManager->expects($this->atLeastOnce())->method('getSessionId'); $visitor = $this->getMockBuilder(\Magento\Customer\Model\Visitor::class) ->disableOriginalConstructor() @@ -1604,7 +1664,8 @@ public function testAuthenticate() $password = '1234567'; $passwordHash = '1a2b3f4c'; - $customerData = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) + $customerData = $this->getMockBuilder(Customer::class) + ->disableOriginalConstructor() ->getMock(); $customerModel = $this->getMockBuilder(\Magento\Customer\Model\Customer::class) @@ -1669,7 +1730,7 @@ public function testGetConfirmationStatus( $customerId = 1; $customerEmail = 'test1@example.com'; - $customerMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) + $customerMock = $this->getMockBuilder(Customer::class) ->disableOriginalConstructor() ->getMock(); $customerMock->expects($this->once()) @@ -1739,8 +1800,9 @@ public function testCreateAccountWithPasswordHashForGuest() ->method('getStore') ->willReturn($storeMock); - $customerMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) - ->getMockForAbstractClass(); + $customerMock = $this->getMockBuilder(Customer::class) + ->disableOriginalConstructor() + ->getMock(); $customerMock->expects($this->exactly(2)) ->method('getId') ->willReturn(null); @@ -1823,7 +1885,7 @@ public function testCreateAccountWithPasswordHashWithCustomerAddresses() ->method("setId") ->with(null); //Handle Customer calls - $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class)->getMock(); + $customer = $this->getMockBuilder(Customer::class)->disableOriginalConstructor()->getMock(); $customer ->expects($this->atLeastOnce()) ->method('getWebsiteId') @@ -1949,7 +2011,7 @@ public function testCreateAccountUnexpectedValueException(): void $website->expects($this->once()) ->method('getDefaultStore') ->willReturn($store); - $customer = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); + $customer = $this->createMock(Customer::class); $customer->expects($this->atLeastOnce()) ->method('getId') ->willReturn($customerId); @@ -2028,8 +2090,9 @@ public function testCreateAccountWithStoreNotInWebsite() $storeId = 1; $websiteId = 1; $hash = '4nj54lkj5jfi03j49f8bgujfgsd'; - $customerMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) - ->getMockForAbstractClass(); + $customerMock = $this->getMockBuilder(Customer::class) + ->disableOriginalConstructor() + ->getMock(); $customerMock->expects($this->atLeastOnce()) ->method('getId') ->willReturn(null); diff --git a/app/code/Magento/Customer/Test/Unit/Model/Address/DataProviderTest.php b/app/code/Magento/Customer/Test/Unit/Model/Address/DataProviderTest.php new file mode 100644 index 000000000000..4dafd305d619 --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Model/Address/DataProviderTest.php @@ -0,0 +1,351 @@ +<?php +declare(strict_types=1); +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Customer\Test\Unit\Model\Address; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Model\Address\DataProvider; +use Magento\Customer\Model\AttributeMetadataResolver; +use Magento\Customer\Model\FileUploaderDataResolver; +use Magento\Customer\Model\ResourceModel\Address\CollectionFactory; +use Magento\Customer\Model\ResourceModel\Address\Collection as AddressCollection; +use Magento\Eav\Model\Config; +use Magento\Eav\Model\Entity\Type; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Customer\Model\Address as AddressModel; +use Magento\Ui\Component\Form\Element\Multiline; +use Magento\Ui\Component\Form\Field; +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class DataProviderTest extends \PHPUnit\Framework\TestCase +{ + private const ATTRIBUTE_CODE = 'street'; + + /** + * @var CollectionFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $addressCollectionFactory; + + /** + * @var AddressCollection|\PHPUnit_Framework_MockObject_MockObject + */ + private $collection; + + /** + * @var CustomerRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $customerRepository; + + /** + * @var CustomerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $customer; + + /** + * @var Config|\PHPUnit_Framework_MockObject_MockObject + */ + private $eavConfig; + + /* + * @var ContextInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $context; + + /** + * @var AddressModel|\PHPUnit_Framework_MockObject_MockObject + */ + private $address; + + /** + * @var FileUploaderDataResolver|\PHPUnit_Framework_MockObject_MockObject + */ + private $fileUploaderDataResolver; + + /** + * @var AttributeMetadataResolver|\PHPUnit_Framework_MockObject_MockObject + */ + private $attributeMetadataResolver; + + /** + * @var DataProvider + */ + private $model; + + protected function setUp() + { + $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->fileUploaderDataResolver = $this->getMockBuilder(FileUploaderDataResolver::class) + ->disableOriginalConstructor() + ->getMock(); + $this->attributeMetadataResolver = $this->getMockBuilder(AttributeMetadataResolver::class) + ->disableOriginalConstructor() + ->getMock(); + $this->addressCollectionFactory = $this->getMockBuilder(CollectionFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->collection = $this->getMockBuilder(AddressCollection::class) + ->disableOriginalConstructor() + ->getMock(); + $this->customerRepository = $this->getMockForAbstractClass(CustomerRepositoryInterface::class); + $this->context = $this->getMockForAbstractClass(ContextInterface::class); + $this->addressCollectionFactory->expects($this->once()) + ->method('create') + ->willReturn($this->collection); + $this->eavConfig = $this->getMockBuilder(Config::class) + ->disableOriginalConstructor() + ->getMock(); + $this->eavConfig->expects($this->once()) + ->method('getEntityType') + ->with('customer_address') + ->willReturn($this->getTypeAddressMock([])); + $this->customer = $this->getMockForAbstractClass(CustomerInterface::class); + $this->address = $this->getMockBuilder(AddressModel::class) + ->disableOriginalConstructor() + ->getMock(); + $this->attributeMetadataResolver->expects($this->at(0)) + ->method('getAttributesMeta') + ->willReturn( + [ + 'arguments' => [ + 'data' => [ + 'config' => [ + 'dataType' => Multiline::NAME, + 'formElement' => 'frontend_input', + 'options' => 'test-options', + 'visible' => null, + 'required' => 'is_required', + 'label' => __('Street'), + 'sortOrder' => 'sort_order', + 'default' => 'default_value', + 'size' => 'multiline_count', + 'componentType' => Field::NAME, + ], + ], + ], + ] + ); + $this->attributeMetadataResolver->expects($this->at(1)) + ->method('getAttributesMeta') + ->willReturn( + [ + 'arguments' => [ + 'data' => [ + 'config' => [ + 'dataType' => 'frontend_input', + 'formElement' => 'frontend_input', + 'visible' => null, + 'required' => 'is_required', + 'label' => __('frontend_label'), + 'sortOrder' => 'sort_order', + 'default' => 'default_value', + 'size' => 'multiline_count', + 'componentType' => Field::NAME, + 'prefer' => 'toggle', + 'valueMap' => [ + 'true' => 1, + 'false' => 0, + ], + ], + ], + ], + ] + ); + $this->model = $objectManagerHelper->getObject( + DataProvider::class, + [ + 'name' => 'test-name', + 'primaryFieldName' => 'primary-field-name', + 'requestFieldName' => 'request-field-name', + 'addressCollectionFactory' => $this->addressCollectionFactory, + 'customerRepository' => $this->customerRepository, + 'eavConfig' => $this->eavConfig, + 'context' => $this->context, + 'fileUploaderDataResolver' => $this->fileUploaderDataResolver, + 'attributeMetadataResolver' => $this->attributeMetadataResolver, + [], + [], + true + ] + ); + } + + public function testGetDefaultData(): void + { + $expectedData = [ + '' => [ + 'parent_id' => 1, + 'firstname' => 'John', + 'lastname' => 'Doe' + ] + ]; + + $this->collection->expects($this->once()) + ->method('getItems') + ->willReturn([]); + + $this->context->expects($this->once()) + ->method('getRequestParam') + ->willReturn(1); + $this->customerRepository->expects($this->once()) + ->method('getById') + ->willReturn($this->customer); + $this->customer->expects($this->once()) + ->method('getFirstname') + ->willReturn('John'); + $this->customer->expects($this->once()) + ->method('getLastname') + ->willReturn('Doe'); + + $this->assertEquals($expectedData, $this->model->getData()); + } + + public function testGetData(): void + { + $expectedData = [ + '1' => [ + 'parent_id' => '1', + 'default_billing' => '1', + 'default_shipping' => '1', + 'firstname' => 'John', + 'lastname' => 'Doe', + 'street' => [ + '42000 Ave W 55 Cedar City', + 'Apt. 33' + ] + ] + ]; + + $this->collection->expects($this->once()) + ->method('getItems') + ->willReturn([ + $this->address + ]); + + $this->customerRepository->expects($this->once()) + ->method('getById') + ->willReturn($this->customer); + $this->customer->expects($this->once()) + ->method('getDefaultBilling') + ->willReturn('1'); + $this->customer->expects($this->once()) + ->method('getDefaultShipping') + ->willReturn('1'); + + $this->address->expects($this->once()) + ->method('getEntityId') + ->willReturn('1'); + $this->address->expects($this->once()) + ->method('load') + ->with('1') + ->willReturnSelf(); + $this->address->expects($this->once()) + ->method('getData') + ->willReturn([ + 'parent_id' => '1', + 'firstname' => 'John', + 'lastname' => 'Doe', + 'street' => "42000 Ave W 55 Cedar City\nApt. 33" + ]); + $this->fileUploaderDataResolver->expects($this->once()) + ->method('overrideFileUploaderData') + ->willReturnSelf(); + + $this->assertEquals($expectedData, $this->model->getData()); + } + + /** + * Get customer address type mock + * + * @param array $customerAttributes + * @return Type|\PHPUnit_Framework_MockObject_MockObject + */ + protected function getTypeAddressMock($customerAttributes = []) + { + $typeAddressMock = $this->getMockBuilder(Type::class) + ->disableOriginalConstructor() + ->getMock(); + $attributesCollection = !empty($customerAttributes) ? $customerAttributes : $this->getAttributeMock(); + foreach ($attributesCollection as $attribute) { + $attribute->expects($this->any()) + ->method('getEntityType') + ->willReturn($typeAddressMock); + } + + $typeAddressMock->expects($this->once()) + ->method('getAttributeCollection') + ->willReturn($attributesCollection); + + return $typeAddressMock; + } + + /** + * Get attribute mock + * + * @param array $options + * @return AbstractAttribute[]|\PHPUnit_Framework_MockObject_MockObject[] + */ + protected function getAttributeMock($options = []): array + { + $attributeMock = $this->getMockBuilder(AbstractAttribute::class) + ->setMethods( + [ + 'getAttributeCode', + 'getDataUsingMethod', + 'getFrontendInput', + 'getIsVisible', + 'getSource', + 'getIsUserDefined', + 'getUsedInForms', + 'getEntityType', + ] + ) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $attributeCode = self::ATTRIBUTE_CODE; + if (isset($options[self::ATTRIBUTE_CODE]['specific_code_prefix'])) { + $attributeCode .= $options[self::ATTRIBUTE_CODE]['specific_code_prefix']; + } + + $attributeMock->expects($this->exactly(2)) + ->method('getAttributeCode') + ->willReturn($attributeCode); + + $attributeBooleanMock = $this->getMockBuilder(AbstractAttribute::class) + ->setMethods( + [ + 'getAttributeCode', + 'getDataUsingMethod', + 'getFrontendInput', + 'getIsVisible', + 'getIsUserDefined', + 'getUsedInForms', + 'getSource', + 'getEntityType', + ] + ) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $booleanAttributeCode = 'test-code-boolean'; + if (isset($options['test-code-boolean']['specific_code_prefix'])) { + $booleanAttributeCode .= $options['test-code-boolean']['specific_code_prefix']; + } + + $attributeBooleanMock->expects($this->exactly(2)) + ->method('getAttributeCode') + ->willReturn($booleanAttributeCode); + + $mocks = [$attributeMock, $attributeBooleanMock]; + return $mocks; + } +} diff --git a/app/code/Magento/Customer/Test/Unit/Model/Address/Validator/CountryTest.php b/app/code/Magento/Customer/Test/Unit/Model/Address/Validator/CountryTest.php index d8148543a55d..f26a5ba2dbb7 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Address/Validator/CountryTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Address/Validator/CountryTest.php @@ -27,11 +27,6 @@ class CountryTest extends \PHPUnit\Framework\TestCase */ private $allowedCountriesReaderMock; - /** - * @var \Magento\Customer\Model\Config\Share|\PHPUnit_Framework_MockObject_MockObject - */ - private $shareConfigMock; - protected function setUp() { $this->directoryDataMock = $this->createMock(\Magento\Directory\Helper\Data::class); @@ -40,16 +35,11 @@ protected function setUp() \Magento\Directory\Model\AllowedCountries::class, ['getAllowedCountries'] ); - $this->shareConfigMock = $this->createPartialMock( - \Magento\Customer\Model\Config\Share::class, - ['isGlobalScope'] - ); $this->model = $this->objectManager->getObject( \Magento\Customer\Model\Address\Validator\Country::class, [ 'directoryData' => $this->directoryDataMock, 'allowedCountriesReader' => $this->allowedCountriesReaderMock, - 'shareConfig' => $this->shareConfigMock, ] ); } @@ -81,10 +71,9 @@ public function testValidate(array $data, array $countryIds, array $allowedRegio ->method('isRegionRequired') ->willReturn($data['regionRequired']); - $this->shareConfigMock->method('isGlobalScope')->willReturn(false); $this->allowedCountriesReaderMock ->method('getAllowedCountries') - ->with(ScopeInterface::SCOPE_WEBSITE, null) + ->with(ScopeInterface::SCOPE_STORE, null) ->willReturn($countryIds); $addressMock->method('getCountryId')->willReturn($data['country_id']); diff --git a/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php b/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php index 50c21379054b..ac87e2e336e3 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php @@ -49,14 +49,14 @@ class DataProviderTest extends \PHPUnit\Framework\TestCase protected $sessionMock; /** - * @var \Magento\Customer\Model\FileProcessorFactory|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Customer\Model\FileProcessor|\PHPUnit_Framework_MockObject_MockObject */ - protected $fileProcessorFactory; + protected $fileProcessor; /** - * @var \Magento\Customer\Model\FileProcessor|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Customer\Model\FileUploaderDataResolver|\PHPUnit_Framework_MockObject_MockObject */ - protected $fileProcessor; + private $fileUploaderDataResolver; /** * Set up @@ -84,10 +84,9 @@ protected function setUp() $this->fileProcessor = $this->getMockBuilder(\Magento\Customer\Model\FileProcessor::class) ->disableOriginalConstructor() ->getMock(); - - $this->fileProcessorFactory = $this->getMockBuilder(\Magento\Customer\Model\FileProcessorFactory::class) + $this->fileUploaderDataResolver = $this->getMockBuilder(\Magento\Customer\Model\FileUploaderDataResolver::class) ->disableOriginalConstructor() - ->setMethods(['create']) + ->setMethods(['overrideFileUploaderMetadata', 'overrideFileUploaderData']) ->getMock(); } @@ -111,16 +110,11 @@ public function testGetAttributesMetaWithOptions(array $expected) 'requestFieldName' => 'request-field-name', 'eavValidationRules' => $this->eavValidationRulesMock, 'customerCollectionFactory' => $this->getCustomerCollectionFactoryMock(), - 'eavConfig' => $this->getEavConfigMock() + 'eavConfig' => $this->getEavConfigMock(), + 'fileUploaderDataResolver' => $this->fileUploaderDataResolver ] ); - $helper->setBackwardCompatibleProperty( - $dataProvider, - 'fileProcessorFactory', - $this->fileProcessorFactory - ); - $meta = $dataProvider->getMeta(); $this->assertNotEmpty($meta); $this->assertEquals($expected, $meta); @@ -591,10 +585,6 @@ public function testGetData() $customer->expects($this->once()) ->method('getAddresses') ->willReturn([$address]); - $customer->expects($this->once()) - ->method('getAttributes') - ->willReturn([]); - $address->expects($this->atLeastOnce()) ->method('getId') ->willReturn(2); @@ -605,9 +595,6 @@ public function testGetData() $address->expects($this->once()) ->method('getData') ->willReturn($addressData); - $address->expects($this->once()) - ->method('getAttributes') - ->willReturn([]); $helper = new ObjectManager($this); $dataProvider = $helper->getObject( @@ -618,7 +605,8 @@ public function testGetData() 'requestFieldName' => 'request-field-name', 'eavValidationRules' => $this->eavValidationRulesMock, 'customerCollectionFactory' => $this->customerCollectionFactoryMock, - 'eavConfig' => $this->getEavConfigMock() + 'eavConfig' => $this->getEavConfigMock(), + 'fileUploaderDataResolver' => $this->fileUploaderDataResolver ] ); @@ -631,12 +619,6 @@ public function testGetData() ->method('getCustomerFormData') ->willReturn(null); - $helper->setBackwardCompatibleProperty( - $dataProvider, - 'fileProcessorFactory', - $this->fileProcessorFactory - ); - $this->assertEquals( [ '' => [ @@ -649,10 +631,8 @@ public function testGetData() 2 => [ 'firstname' => 'firstname', 'lastname' => 'lastname', - 'street' => [ - 'street', - 'street', - ], + // Won't be an array because it isn't defined as a multiline field in this test + 'street' => "street\nstreet", 'default_billing' => 2, 'default_shipping' => 2, ] @@ -725,10 +705,6 @@ public function testGetDataWithCustomerFormData() $customer->expects($this->once()) ->method('getAddresses') ->willReturn([$address]); - $customer->expects($this->once()) - ->method('getAttributes') - ->willReturn([]); - $address->expects($this->atLeastOnce()) ->method('getId') ->willReturn(2); @@ -743,10 +719,6 @@ public function testGetDataWithCustomerFormData() 'lastname' => 'lastname', 'street' => "street\nstreet", ]); - $address->expects($this->once()) - ->method('getAttributes') - ->willReturn([]); - $helper = new ObjectManager($this); $dataProvider = $helper->getObject( \Magento\Customer\Model\Customer\DataProvider::class, @@ -756,7 +728,8 @@ public function testGetDataWithCustomerFormData() 'requestFieldName' => 'request-field-name', 'eavValidationRules' => $this->eavValidationRulesMock, 'customerCollectionFactory' => $this->customerCollectionFactoryMock, - 'eavConfig' => $this->getEavConfigMock() + 'eavConfig' => $this->getEavConfigMock(), + 'fileUploaderDataResolver' => $this->fileUploaderDataResolver ] ); @@ -771,12 +744,6 @@ public function testGetDataWithCustomerFormData() $this->sessionMock->expects($this->once()) ->method('unsCustomerFormData'); - $helper->setBackwardCompatibleProperty( - $dataProvider, - 'fileProcessorFactory', - $this->fileProcessorFactory - ); - $this->assertEquals([$customerId => $customerFormData], $dataProvider->getData()); } @@ -790,42 +757,6 @@ public function testGetDataWithCustomAttributeImage() $customerEmail = 'user1@example.com'; $filename = '/filename.ext1'; - $viewUrl = 'viewUrl'; - $mime = 'image/png'; - - $expectedData = [ - $customerId => [ - 'customer' => [ - 'email' => $customerEmail, - 'img1' => [ - [ - 'file' => $filename, - 'size' => 1, - 'url' => $viewUrl, - 'name' => 'filename.ext1', - 'type' => $mime, - ], - ], - ], - ], - ]; - - $attributeMock = $this->getMockBuilder(\Magento\Customer\Model\Attribute::class) - ->disableOriginalConstructor() - ->getMock(); - $attributeMock->expects($this->exactly(2)) - ->method('getFrontendInput') - ->willReturn('image'); - $attributeMock->expects($this->exactly(2)) - ->method('getAttributeCode') - ->willReturn('img1'); - - $entityTypeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Type::class) - ->disableOriginalConstructor() - ->getMock(); - $entityTypeMock->expects($this->once()) - ->method('getEntityTypeCode') - ->willReturn(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER); $customerMock = $this->getMockBuilder(\Magento\Customer\Model\Customer::class) ->disableOriginalConstructor() @@ -842,13 +773,6 @@ public function testGetDataWithCustomAttributeImage() $customerMock->expects($this->once()) ->method('getId') ->willReturn($customerId); - $customerMock->expects($this->once()) - ->method('getAttributes') - ->willReturn([$attributeMock]); - $customerMock->expects($this->once()) - ->method('getEntityType') - ->willReturn($entityTypeMock); - $collectionMock = $this->getMockBuilder(\Magento\Customer\Model\ResourceModel\Customer\Collection::class) ->disableOriginalConstructor() ->getMock(); @@ -864,30 +788,6 @@ public function testGetDataWithCustomAttributeImage() ->method('getCustomerFormData') ->willReturn([]); - $this->fileProcessorFactory->expects($this->any()) - ->method('create') - ->with([ - 'entityTypeCode' => CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, - ]) - ->willReturn($this->fileProcessor); - - $this->fileProcessor->expects($this->once()) - ->method('isExist') - ->with($filename) - ->willReturn(true); - $this->fileProcessor->expects($this->once()) - ->method('getStat') - ->with($filename) - ->willReturn(['size' => 1]); - $this->fileProcessor->expects($this->once()) - ->method('getViewUrl') - ->with('/filename.ext1', 'image') - ->willReturn($viewUrl); - $this->fileProcessor->expects($this->once()) - ->method('getMimeType') - ->with($filename) - ->willReturn($mime); - $objectManager = new ObjectManager($this); $dataProvider = $objectManager->getObject( \Magento\Customer\Model\Customer\DataProvider::class, @@ -897,7 +797,8 @@ public function testGetDataWithCustomAttributeImage() 'requestFieldName' => 'request-field-name', 'eavValidationRules' => $this->eavValidationRulesMock, 'customerCollectionFactory' => $this->customerCollectionFactoryMock, - 'eavConfig' => $this->getEavConfigMock() + 'eavConfig' => $this->getEavConfigMock(), + 'fileUploaderDataResolver' => $this->fileUploaderDataResolver ] ); @@ -907,108 +808,15 @@ public function testGetDataWithCustomAttributeImage() $this->sessionMock ); - $objectManager->setBackwardCompatibleProperty( - $dataProvider, - 'fileProcessorFactory', - $this->fileProcessorFactory - ); - - $this->assertEquals($expectedData, $dataProvider->getData()); - } - - public function testGetDataWithCustomAttributeImageNoData() - { - $customerId = 1; - $customerEmail = 'user1@example.com'; - - $expectedData = [ - $customerId => [ - 'customer' => [ + $this->fileUploaderDataResolver->expects($this->atLeastOnce())->method('overrideFileUploaderData') + ->with( + $customerMock, + [ 'email' => $customerEmail, - 'img1' => [], - ], - ], - ]; - - $attributeMock = $this->getMockBuilder(\Magento\Customer\Model\Attribute::class) - ->disableOriginalConstructor() - ->getMock(); - $attributeMock->expects($this->once()) - ->method('getFrontendInput') - ->willReturn('image'); - $attributeMock->expects($this->exactly(2)) - ->method('getAttributeCode') - ->willReturn('img1'); - - $entityTypeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Type::class) - ->disableOriginalConstructor() - ->getMock(); - $entityTypeMock->expects($this->once()) - ->method('getEntityTypeCode') - ->willReturn(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER); - - $customerMock = $this->getMockBuilder(\Magento\Customer\Model\Customer::class) - ->disableOriginalConstructor() - ->getMock(); - $customerMock->expects($this->once()) - ->method('getData') - ->willReturn([ - 'email' => $customerEmail, - ]); - $customerMock->expects($this->once()) - ->method('getAddresses') - ->willReturn([]); - $customerMock->expects($this->once()) - ->method('getId') - ->willReturn($customerId); - $customerMock->expects($this->once()) - ->method('getAttributes') - ->willReturn([$attributeMock]); - $customerMock->expects($this->once()) - ->method('getEntityType') - ->willReturn($entityTypeMock); - - $collectionMock = $this->getMockBuilder(\Magento\Customer\Model\ResourceModel\Customer\Collection::class) - ->disableOriginalConstructor() - ->getMock(); - $collectionMock->expects($this->once()) - ->method('getItems') - ->willReturn([$customerMock]); - - $this->customerCollectionFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($collectionMock); - - $this->sessionMock->expects($this->once()) - ->method('getCustomerFormData') - ->willReturn([]); - - $objectManager = new ObjectManager($this); - $dataProvider = $objectManager->getObject( - \Magento\Customer\Model\Customer\DataProvider::class, - [ - 'name' => 'test-name', - 'primaryFieldName' => 'primary-field-name', - 'requestFieldName' => 'request-field-name', - 'eavValidationRules' => $this->eavValidationRulesMock, - 'customerCollectionFactory' => $this->customerCollectionFactoryMock, - 'eavConfig' => $this->getEavConfigMock() - ] - ); - - $objectManager->setBackwardCompatibleProperty( - $dataProvider, - 'session', - $this->sessionMock - ); - - $objectManager->setBackwardCompatibleProperty( - $dataProvider, - 'fileProcessorFactory', - $this->fileProcessorFactory - ); - - $this->assertEquals($expectedData, $dataProvider->getData()); + 'img1' => $filename, + ] + ); + $dataProvider->getData(); } /** @@ -1099,13 +907,6 @@ function ($origName) { 'file_extensions' => 'ext1, eXt2 ', // Added spaces and upper-cases ]); - $this->fileProcessorFactory->expects($this->any()) - ->method('create') - ->with([ - 'entityTypeCode' => CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, - ]) - ->willReturn($this->fileProcessor); - $objectManager = new ObjectManager($this); $dataProvider = $objectManager->getObject( \Magento\Customer\Model\Customer\DataProvider::class, @@ -1115,8 +916,7 @@ function ($origName) { 'requestFieldName' => 'request-field-name', 'eavValidationRules' => $this->eavValidationRulesMock, 'customerCollectionFactory' => $this->customerCollectionFactoryMock, - 'eavConfig' => $this->eavConfigMock, - 'fileProcessorFactory' => $this->fileProcessorFactory, + 'eavConfig' => $this->eavConfigMock ] ); @@ -1211,16 +1011,11 @@ public function testGetDataWithVisibleAttributes() 'requestFieldName' => 'request-field-name', 'eavValidationRules' => $this->eavValidationRulesMock, 'customerCollectionFactory' => $this->getCustomerCollectionFactoryMock(), - 'eavConfig' => $this->getEavConfigMock(array_merge($firstAttributesBundle, $secondAttributesBundle)) + 'eavConfig' => $this->getEavConfigMock(array_merge($firstAttributesBundle, $secondAttributesBundle)), + 'fileUploaderDataResolver' => $this->fileUploaderDataResolver ] ); - $helper->setBackwardCompatibleProperty( - $dataProvider, - 'fileProcessorFactory', - $this->fileProcessorFactory - ); - $meta = $dataProvider->getMeta(); $this->assertNotEmpty($meta); $this->assertEquals($this->getExpectationForVisibleAttributes(), $meta); @@ -1284,14 +1079,11 @@ public function testGetDataWithVisibleAttributesWithAccountEdit() 'eavValidationRules' => $this->eavValidationRulesMock, 'customerCollectionFactory' => $this->getCustomerCollectionFactoryMock(), 'context' => $context, - 'eavConfig' => $this->getEavConfigMock(array_merge($firstAttributesBundle, $secondAttributesBundle)) + 'eavConfig' => $this->getEavConfigMock(array_merge($firstAttributesBundle, $secondAttributesBundle)), + 'fileUploaderDataResolver' => $this->fileUploaderDataResolver + ] ); - $helper->setBackwardCompatibleProperty( - $dataProvider, - 'fileProcessorFactory', - $this->fileProcessorFactory - ); $meta = $dataProvider->getMeta(); $this->assertNotEmpty($meta); diff --git a/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderWithDefaultAddressesTest.php b/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderWithDefaultAddressesTest.php new file mode 100644 index 000000000000..d5eaecb3ef35 --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderWithDefaultAddressesTest.php @@ -0,0 +1,421 @@ +<?php +declare(strict_types=1); +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Customer\Test\Unit\Model\Customer; + +use Magento\Customer\Model\AttributeMetadataResolver; +use Magento\Customer\Model\Customer\DataProviderWithDefaultAddresses; +use Magento\Customer\Model\FileUploaderDataResolver; +use Magento\Customer\Model\ResourceModel\Customer\CollectionFactory as CustomerCollectionFactory; +use Magento\Customer\Model\ResourceModel\Customer\Collection as CustomerCollection; +use Magento\Eav\Model\Config; +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Eav\Model\Entity\Type; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Ui\Component\Form\Field; + +/** + * Test for class \Magento\Customer\Model\Customer\DataProviderWithDefaultAddresses + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class DataProviderWithDefaultAddressesTest extends \PHPUnit\Framework\TestCase +{ + private const ATTRIBUTE_CODE = 'test-code'; + + /** + * @var Config|\PHPUnit_Framework_MockObject_MockObject + */ + private $eavConfigMock; + + /** + * @var CustomerCollectionFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $customerCollectionFactoryMock; + + /** + * @var \Magento\Framework\Session\SessionManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $sessionMock; + + /** + * @var \Magento\Directory\Model\CountryFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $countryFactoryMock; + + /** + * @var \Magento\Customer\Model\Customer|\PHPUnit_Framework_MockObject_MockObject + */ + private $customerMock; + + /** + * @var CustomerCollection|\PHPUnit_Framework_MockObject_MockObject + */ + private $customerCollectionMock; + + /** + * @var FileUploaderDataResolver|\PHPUnit_Framework_MockObject_MockObject + */ + private $fileUploaderDataResolver; + + /** + * @var AttributeMetadataResolver|\PHPUnit_Framework_MockObject_MockObject + */ + private $attributeMetadataResolver; + + /** + * @return void + */ + protected function setUp(): void + { + $this->eavConfigMock = $this->getMockBuilder(Config::class)->disableOriginalConstructor()->getMock(); + $this->customerCollectionFactoryMock = $this->createPartialMock(CustomerCollectionFactory::class, ['create']); + $this->sessionMock = $this->getMockBuilder(\Magento\Framework\Session\SessionManagerInterface::class) + ->setMethods(['getCustomerFormData', 'unsCustomerFormData']) + ->getMockForAbstractClass(); + $this->countryFactoryMock = $this->getMockBuilder(\Magento\Directory\Model\CountryFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create', 'loadByCode', 'getName']) + ->getMock(); + $this->customerMock = $this->getMockBuilder(\Magento\Customer\Model\Customer::class) + ->disableOriginalConstructor() + ->getMock(); + $this->customerCollectionMock = $this->getMockBuilder(CustomerCollection::class) + ->disableOriginalConstructor() + ->getMock(); + $this->customerCollectionMock->expects($this->once())->method('addAttributeToSelect')->with('*'); + $this->customerCollectionFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->customerCollectionMock); + $this->eavConfigMock->expects($this->atLeastOnce()) + ->method('getEntityType') + ->with('customer') + ->willReturn($this->getTypeCustomerMock([])); + $this->fileUploaderDataResolver = $this->getMockBuilder(FileUploaderDataResolver::class) + ->disableOriginalConstructor() + ->getMock(); + $this->attributeMetadataResolver = $this->getMockBuilder(AttributeMetadataResolver::class) + ->disableOriginalConstructor() + ->setMethods(['getAttributesMeta']) + ->getMock(); + $this->attributeMetadataResolver->expects($this->at(0)) + ->method('getAttributesMeta') + ->willReturn( + [ + 'arguments' => [ + 'data' => [ + 'config' => [ + 'dataType' => 'frontend_input', + 'formElement' => 'frontend_input', + 'options' => 'test-options', + 'visible' => null, + 'required' => 'is_required', + 'label' => __('frontend_label'), + 'sortOrder' => 'sort_order', + 'notice' => 'note', + 'default' => 'default_value', + 'size' => 'multiline_count', + 'componentType' => Field::NAME, + ], + ], + ], + ] + ); + $this->attributeMetadataResolver->expects($this->at(1)) + ->method('getAttributesMeta') + ->willReturn( + [ + 'arguments' => [ + 'data' => [ + 'config' => [ + 'dataType' => 'frontend_input', + 'formElement' => 'frontend_input', + 'visible' => null, + 'required' => 'is_required', + 'label' => __('frontend_label'), + 'sortOrder' => 'sort_order', + 'notice' => 'note', + 'default' => 'default_value', + 'size' => 'multiline_count', + 'componentType' => Field::NAME, + 'prefer' => 'toggle', + 'valueMap' => [ + 'true' => 1, + 'false' => 0, + ], + ], + ], + ], + ] + ); + $helper = new ObjectManager($this); + $this->dataProvider = $helper->getObject( + DataProviderWithDefaultAddresses::class, + [ + 'name' => 'test-name', + 'primaryFieldName' => 'primary-field-name', + 'requestFieldName' => 'request-field-name', + 'customerCollectionFactory' => $this->customerCollectionFactoryMock, + 'eavConfig' => $this->eavConfigMock, + 'countryFactory' => $this->countryFactoryMock, + 'session' => $this->sessionMock, + 'fileUploaderDataResolver' => $this->fileUploaderDataResolver, + 'attributeMetadataResolver' => $this->attributeMetadataResolver, + true + ] + ); + } + + /** + * Run test getAttributesMeta method + * + * @param array $expected + * @return void + * + * @dataProvider getAttributesMetaDataProvider + */ + public function testGetAttributesMetaWithOptions(array $expected): void + { + $meta = $this->dataProvider->getMeta(); + $this->assertNotEmpty($meta); + $this->assertEquals($expected, $meta); + } + + /** + * Data provider for testGetAttributesMeta + * + * @return array + */ + public function getAttributesMetaDataProvider(): array + { + return [ + [ + 'expected' => [ + 'customer' => [ + 'children' => [ + self::ATTRIBUTE_CODE => [ + 'arguments' => [ + 'data' => [ + 'config' => [ + 'dataType' => 'frontend_input', + 'formElement' => 'frontend_input', + 'options' => 'test-options', + 'visible' => null, + 'required' => 'is_required', + 'label' => __('frontend_label'), + 'sortOrder' => 'sort_order', + 'notice' => 'note', + 'default' => 'default_value', + 'size' => 'multiline_count', + 'componentType' => Field::NAME, + ], + ], + ], + ], + 'test-code-boolean' => [ + 'arguments' => [ + 'data' => [ + 'config' => [ + 'dataType' => 'frontend_input', + 'formElement' => 'frontend_input', + 'visible' => null, + 'required' => 'is_required', + 'label' => __('frontend_label'), + 'sortOrder' => 'sort_order', + 'notice' => 'note', + 'default' => 'default_value', + 'size' => 'multiline_count', + 'componentType' => Field::NAME, + 'prefer' => 'toggle', + 'valueMap' => [ + 'true' => 1, + 'false' => 0, + ], + ], + ], + ], + ], + ], + ], + ] + ] + ]; + } + + /** + * @param array $customerAttributes + * @return Type|\PHPUnit_Framework_MockObject_MockObject + */ + protected function getTypeCustomerMock($customerAttributes = []) + { + $typeCustomerMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Type::class) + ->disableOriginalConstructor() + ->getMock(); + $attributesCollection = !empty($customerAttributes) ? $customerAttributes : $this->getAttributeMock(); + foreach ($attributesCollection as $attribute) { + $attribute->expects($this->any()) + ->method('getEntityType') + ->willReturn($typeCustomerMock); + } + + $typeCustomerMock->expects($this->atLeastOnce()) + ->method('getAttributeCollection') + ->willReturn($attributesCollection); + + return $typeCustomerMock; + } + + /** + * @param array $options + * @return AbstractAttribute[]|\PHPUnit_Framework_MockObject_MockObject[] + */ + protected function getAttributeMock($options = []): array + { + $attributeMock = $this->getMockBuilder(AbstractAttribute::class) + ->setMethods( + [ + 'getAttributeCode', + 'getDataUsingMethod', + 'getFrontendInput', + 'getIsVisible', + 'getSource', + 'getIsUserDefined', + 'getUsedInForms', + 'getEntityType', + ] + ) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $attributeCode = self::ATTRIBUTE_CODE; + if (isset($options[self::ATTRIBUTE_CODE]['specific_code_prefix'])) { + $attributeCode .= $options[self::ATTRIBUTE_CODE]['specific_code_prefix']; + } + + $attributeMock->expects($this->once()) + ->method('getAttributeCode') + ->willReturn($attributeCode); + + $attributeBooleanMock = $this->getMockBuilder(AbstractAttribute::class) + ->setMethods( + [ + 'getAttributeCode', + 'getDataUsingMethod', + 'getFrontendInput', + 'getIsVisible', + 'getIsUserDefined', + 'getUsedInForms', + 'getSource', + 'getEntityType', + ] + ) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $booleanAttributeCode = 'test-code-boolean'; + if (isset($options['test-code-boolean']['specific_code_prefix'])) { + $booleanAttributeCode .= $options['test-code-boolean']['specific_code_prefix']; + } + + $attributeBooleanMock->expects($this->once()) + ->method('getAttributeCode') + ->willReturn($booleanAttributeCode); + + $mocks = [$attributeMock, $attributeBooleanMock]; + return $mocks; + } + + /** + * @return void + */ + public function testGetData(): void + { + $customerData = [ + 'email' => 'test@test.ua', + 'default_billing' => 2, + 'default_shipping' => 2, + 'password_hash' => 'password_hash', + 'rp_token' => 'rp_token', + ]; + + $address = $this->getMockBuilder(\Magento\Customer\Model\Address::class) + ->disableOriginalConstructor() + ->getMock(); + $this->customerCollectionMock->expects($this->once())->method('getItems')->willReturn([$this->customerMock]); + $this->customerMock->expects($this->once())->method('getData')->willReturn($customerData); + $this->customerMock->expects($this->atLeastOnce())->method('getId')->willReturn(1); + + $this->customerMock->expects($this->once())->method('getDefaultBillingAddress')->willReturn($address); + $this->countryFactoryMock->expects($this->once())->method('create')->willReturnSelf(); + $this->countryFactoryMock->expects($this->once())->method('loadByCode')->willReturnSelf(); + $this->countryFactoryMock->expects($this->once())->method('getName')->willReturn('Ukraine'); + + $this->sessionMock->expects($this->once()) + ->method('getCustomerFormData') + ->willReturn(null); + + $this->assertEquals( + [ + 1 => [ + 'customer' => [ + 'email' => 'test@test.ua', + 'default_billing' => 2, + 'default_shipping' => 2, + ], + 'default_billing_address' => [ + 'country' => 'Ukraine', + ], + 'default_shipping_address' => [], + 'customer_id' => 1 + ] + ], + $this->dataProvider->getData() + ); + } + + /** + * @return void + */ + public function testGetDataWithCustomerFormData(): void + { + $customerId = 11; + $customerFormData = [ + 'customer' => [ + 'email' => 'test1@test1.ua', + 'default_billing' => 3, + 'default_shipping' => 3, + 'entity_id' => $customerId, + ], + 'address' => [ + 3 => [ + 'firstname' => 'firstname1', + 'lastname' => 'lastname1', + 'street' => [ + 'street1', + 'street2', + ], + 'default_billing' => 3, + 'default_shipping' => 3, + ], + ], + ]; + + $this->customerCollectionMock->expects($this->once())->method('getItems')->willReturn([$this->customerMock]); + $this->customerMock->expects($this->once()) + ->method('getData') + ->willReturn([ + 'email' => 'test@test.ua', + 'default_billing' => 2, + 'default_shipping' => 2, + ]); + $this->customerMock->expects($this->atLeastOnce())->method('getId')->willReturn($customerId); + + $this->sessionMock->expects($this->once())->method('getCustomerFormData')->willReturn($customerFormData); + $this->sessionMock->expects($this->once())->method('unsCustomerFormData'); + + $this->assertEquals([$customerId => $customerFormData], $this->dataProvider->getData()); + } +} diff --git a/app/code/Magento/Customer/Test/Unit/Model/CustomerTest.php b/app/code/Magento/Customer/Test/Unit/Model/CustomerTest.php index 9848a09540cb..65831069aa1f 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/CustomerTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/CustomerTest.php @@ -13,9 +13,12 @@ use Magento\Customer\Model\Customer; use Magento\Customer\Model\AccountConfirmation; +use Magento\Customer\Model\ResourceModel\Address\CollectionFactory as AddressCollectionFactory; +use Magento\Customer\Api\Data\CustomerInterfaceFactory; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyFields) */ class CustomerTest extends \PHPUnit\Framework\TestCase { @@ -68,6 +71,21 @@ class CustomerTest extends \PHPUnit\Framework\TestCase */ private $accountConfirmation; + /** + * @var AddressCollectionFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $addressesFactory; + + /** + * @var CustomerInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $customerDataFactory; + + /** + * @var \Magento\Framework\Api\DataObjectHelper|\PHPUnit_Framework_MockObject_MockObject + */ + private $dataObjectHelper; + protected function setUp() { $this->_website = $this->createMock(\Magento\Store\Model\Website::class); @@ -100,6 +118,19 @@ protected function setUp() $this->_encryptor = $this->createMock(\Magento\Framework\Encryption\EncryptorInterface::class); $helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->accountConfirmation = $this->createMock(AccountConfirmation::class); + $this->addressesFactory = $this->getMockBuilder(AddressCollectionFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->customerDataFactory = $this->getMockBuilder(CustomerInterfaceFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $this->dataObjectHelper = $this->getMockBuilder(\Magento\Framework\Api\DataObjectHelper::class) + ->disableOriginalConstructor() + ->setMethods(['populateWithArray']) + ->getMock(); + $this->_model = $helper->getObject( \Magento\Customer\Model\Customer::class, [ @@ -112,7 +143,10 @@ protected function setUp() 'registry' => $this->registryMock, 'resource' => $this->resourceMock, 'dataObjectProcessor' => $this->dataObjectProcessor, - 'accountConfirmation' => $this->accountConfirmation + 'accountConfirmation' => $this->accountConfirmation, + '_addressesFactory' => $this->addressesFactory, + 'customerDataFactory' => $this->customerDataFactory, + 'dataObjectHelper' => $this->dataObjectHelper ] ); } @@ -186,13 +220,13 @@ public function testSendNewAccountEmailWithoutStoreId() ->will($this->returnValue($transportMock)); $this->_model->setData([ - 'website_id' => 1, - 'store_id' => 1, - 'email' => 'email@example.com', - 'firstname' => 'FirstName', - 'lastname' => 'LastName', - 'middlename' => 'MiddleName', - 'prefix' => 'Name Prefix', + 'website_id' => 1, + 'store_id' => 1, + 'email' => 'email@example.com', + 'firstname' => 'FirstName', + 'lastname' => 'LastName', + 'middlename' => 'MiddleName', + 'prefix' => 'Name Prefix', ]); $this->_model->sendNewAccountEmail('registered'); } @@ -307,9 +341,46 @@ public function testUpdateData() } $expectedResult[$attribute->getAttributeCode()] = $attribute->getValue(); - $expectedResult['attribute_set_id'] = - \Magento\Customer\Api\CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER; $this->assertEquals($this->_model->getData(), $expectedResult); } + + /** + * Test for the \Magento\Customer\Model\Customer::getDataModel() method + */ + public function testGetDataModel() + { + $customerId = 1; + $this->_model->setEntityId($customerId); + $this->_model->setId($customerId); + $addressDataModel = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\AddressInterface::class); + $address = $this->getMockBuilder(\Magento\Customer\Model\Address::class) + ->disableOriginalConstructor() + ->setMethods(['setCustomer', 'getDataModel']) + ->getMock(); + $address->expects($this->atLeastOnce())->method('getDataModel')->willReturn($addressDataModel); + $addresses = new \ArrayIterator([$address, $address]); + $addressCollection = $this->getMockBuilder(\Magento\Customer\Model\ResourceModel\Address\Collection::class) + ->disableOriginalConstructor() + ->setMethods(['setCustomerFilter', 'addAttributeToSelect', 'getIterator', 'getItems']) + ->getMock(); + $addressCollection->expects($this->atLeastOnce())->method('setCustomerFilter')->willReturnSelf(); + $addressCollection->expects($this->atLeastOnce())->method('addAttributeToSelect')->willReturnSelf(); + $addressCollection->expects($this->atLeastOnce())->method('getIterator') + ->willReturn($addresses); + $addressCollection->expects($this->atLeastOnce())->method('getItems') + ->willReturn($addresses); + $this->addressesFactory->expects($this->atLeastOnce())->method('create')->willReturn($addressCollection); + $customerDataObject = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\CustomerInterface::class); + $this->customerDataFactory->expects($this->atLeastOnce())->method('create')->willReturn($customerDataObject); + $this->dataObjectHelper->expects($this->atLeastOnce())->method('populateWithArray') + ->with($customerDataObject, $this->_model->getData(), \Magento\Customer\Api\Data\CustomerInterface::class) + ->willReturnSelf(); + $customerDataObject->expects($this->atLeastOnce())->method('setAddresses') + ->with([$addressDataModel, $addressDataModel]) + ->willReturnSelf(); + $customerDataObject->expects($this->atLeastOnce())->method('setId')->with($customerId)->willReturnSelf(); + $this->_model->getDataModel(); + $this->assertEquals($customerDataObject, $this->_model->getDataModel()); + } } diff --git a/app/code/Magento/Customer/Test/Unit/Model/Metadata/AttributeMetadataCacheTest.php b/app/code/Magento/Customer/Test/Unit/Model/Metadata/AttributeMetadataCacheTest.php index 658472d13ab9..83915731ea5a 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Metadata/AttributeMetadataCacheTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Metadata/AttributeMetadataCacheTest.php @@ -15,7 +15,14 @@ use Magento\Framework\App\CacheInterface; use Magento\Framework\Serialize\SerializerInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreManagerInterface; +/** + * AttributeMetadataCache Test + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class AttributeMetadataCacheTest extends \PHPUnit\Framework\TestCase { /** @@ -43,6 +50,16 @@ class AttributeMetadataCacheTest extends \PHPUnit\Framework\TestCase */ private $attributeMetadataCache; + /** + * @var StoreInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeMock; + + /** + * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeManagerMock; + protected function setUp() { $objectManager = new ObjectManager($this); @@ -50,13 +67,18 @@ protected function setUp() $this->stateMock = $this->createMock(StateInterface::class); $this->serializerMock = $this->createMock(SerializerInterface::class); $this->attributeMetadataHydratorMock = $this->createMock(AttributeMetadataHydrator::class); + $this->storeMock = $this->createMock(StoreInterface::class); + $this->storeManagerMock = $this->createMock(StoreManagerInterface::class); + $this->storeManagerMock->method('getStore')->willReturn($this->storeMock); + $this->storeMock->method('getId')->willReturn(1); $this->attributeMetadataCache = $objectManager->getObject( AttributeMetadataCache::class, [ 'cache' => $this->cacheMock, 'state' => $this->stateMock, 'serializer' => $this->serializerMock, - 'attributeMetadataHydrator' => $this->attributeMetadataHydratorMock + 'attributeMetadataHydrator' => $this->attributeMetadataHydratorMock, + 'storeManager' => $this->storeManagerMock ] ); } @@ -80,7 +102,8 @@ public function testLoadNoCache() { $entityType = 'EntityType'; $suffix = 'none'; - $cacheKey = AttributeMetadataCache::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix; + $storeId = 1; + $cacheKey = AttributeMetadataCache::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix . $storeId; $this->stateMock->expects($this->once()) ->method('isEnabled') ->with(Type::TYPE_IDENTIFIER) @@ -96,7 +119,8 @@ public function testLoad() { $entityType = 'EntityType'; $suffix = 'none'; - $cacheKey = AttributeMetadataCache::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix; + $storeId = 1; + $cacheKey = AttributeMetadataCache::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix . $storeId; $serializedString = 'serialized string'; $attributeMetadataOneData = [ 'attribute_code' => 'attribute_code', @@ -156,7 +180,8 @@ public function testSave() { $entityType = 'EntityType'; $suffix = 'none'; - $cacheKey = AttributeMetadataCache::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix; + $storeId = 1; + $cacheKey = AttributeMetadataCache::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix . $storeId; $serializedString = 'serialized string'; $attributeMetadataOneData = [ 'attribute_code' => 'attribute_code', diff --git a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/AbstractDataTest.php b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/AbstractDataTest.php index e4dc22ba40e3..5b4b50ca8211 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/AbstractDataTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/AbstractDataTest.php @@ -205,37 +205,32 @@ public function applyOutputFilterDataProvider() } /** + * Tests input validation rules. + * * @param null|string $value * @param null|string $label * @param null|string $inputValidation * @param bool|array $expectedOutput * @dataProvider validateInputRuleDataProvider */ - public function testValidateInputRule($value, $label, $inputValidation, $expectedOutput) + public function testValidateInputRule($value, $label, $inputValidation, $expectedOutput): void { $validationRule = $this->getMockBuilder(\Magento\Customer\Api\Data\ValidationRuleInterface::class) ->disableOriginalConstructor() ->setMethods(['getName', 'getValue']) ->getMockForAbstractClass(); - $validationRule->expects($this->any()) - ->method('getName') - ->will($this->returnValue('input_validation')); - $validationRule->expects($this->any()) - ->method('getValue') - ->will($this->returnValue($inputValidation)); - - $this->_attributeMock->expects($this->any())->method('getStoreLabel')->will($this->returnValue($label)); - $this->_attributeMock->expects( - $this->any() - )->method( - 'getValidationRules' - )->will( - $this->returnValue( - [ - $validationRule, - ] - ) - ); + + $validationRule->method('getName') + ->willReturn('input_validation'); + + $validationRule->method('getValue') + ->willReturn($inputValidation); + + $this->_attributeMock->method('getStoreLabel') + ->willReturn($label); + + $this->_attributeMock->method('getValidationRules') + ->willReturn([$validationRule]); $this->assertEquals($expectedOutput, $this->_model->validateInputRule($value)); } @@ -256,6 +251,16 @@ public function validateInputRuleDataProvider() \Zend_Validate_Alnum::NOT_ALNUM => '"mylabel" contains non-alphabetic or non-numeric characters.' ] ], + [ + 'abc qaz', + 'mylabel', + 'alphanumeric', + [ + \Zend_Validate_Alnum::NOT_ALNUM => '"mylabel" contains non-alphabetic or non-numeric characters.' + ] + ], + ['abcqaz', 'mylabel', 'alphanumeric', true], + ['abc qaz', 'mylabel', 'alphanum-with-spaces', true], [ '!@#$', 'mylabel', diff --git a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/ImageTest.php b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/ImageTest.php index 0278e2b2d791..31d2a31ceae4 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/ImageTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/ImageTest.php @@ -259,7 +259,7 @@ public function testValidateMaxImageHeight() )->getMockForAbstractClass(); $validationRuleMock->expects($this->any()) ->method('getName') - ->willReturn('max_image_heght'); + ->willReturn('max_image_height'); $validationRuleMock->expects($this->any()) ->method('getValue') ->willReturn($maxImageHeight); diff --git a/app/code/Magento/Customer/Test/Unit/Model/Renderer/RegionTest.php b/app/code/Magento/Customer/Test/Unit/Model/Renderer/RegionTest.php index c655ff7056ed..e67adc47b888 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Renderer/RegionTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Renderer/RegionTest.php @@ -5,6 +5,8 @@ */ namespace Magento\Customer\Test\Unit\Model\Renderer; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + class RegionTest extends \PHPUnit\Framework\TestCase { /** @@ -58,6 +60,14 @@ public function testRender($regionCollection) ] ) ); + + $objectManager = new ObjectManager($this); + $escaper = $objectManager->getObject(\Magento\Framework\Escaper::class); + $reflection = new \ReflectionClass($elementMock); + $reflection_property = $reflection->getProperty('_escaper'); + $reflection_property->setAccessible(true); + $reflection_property->setValue($elementMock, $escaper); + $formMock->expects( $this->any() )->method( diff --git a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/Address/RelationTest.php b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/Address/RelationTest.php index e81637cfb23b..319179c5e279 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/Address/RelationTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/Address/RelationTest.php @@ -6,6 +6,7 @@ namespace Magento\Customer\Test\Unit\Model\ResourceModel\Address; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Customer\Model\Address; /** * Class AddressTest @@ -40,7 +41,7 @@ protected function setUp() */ public function testProcessRelation($addressId, $isDefaultBilling, $isDefaultShipping) { - $addressModel = $this->createPartialMock(\Magento\Framework\Model\AbstractModel::class, [ + $addressModel = $this->createPartialMock(Address::class, [ '__wakeup', 'getId', 'getEntityTypeId', @@ -55,7 +56,17 @@ public function testProcessRelation($addressId, $isDefaultBilling, $isDefaultShi ]); $customerModel = $this->createPartialMock( \Magento\Customer\Model\Customer::class, - ['__wakeup', 'setDefaultBilling', 'setDefaultShipping', 'save', 'load', 'getResource', 'getId'] + [ + '__wakeup', + 'setDefaultBilling', + 'setDefaultShipping', + 'save', + 'load', + 'getResource', + 'getId', + 'getDefaultShippingAddress', + 'getDefaultBillingAddress' + ] ); $customerResource = $this->getMockForAbstractClass( \Magento\Framework\Model\ResourceModel\Db\AbstractDb::class, @@ -88,6 +99,7 @@ public function testProcessRelation($addressId, $isDefaultBilling, $isDefaultShi $this->customerFactoryMock->expects($this->any()) ->method('create') ->willReturn($customerModel); + if ($addressId && ($isDefaultBilling || $isDefaultShipping)) { $customerId = 1; $customerResource->expects($this->exactly(2))->method('getConnection')->willReturn($connectionMock); diff --git a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/CustomerRepositoryTest.php b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/CustomerRepositoryTest.php index bd1dc774b531..05953b09b8c0 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/CustomerRepositoryTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/CustomerRepositoryTest.php @@ -195,35 +195,6 @@ public function testSave() $customerId = 1; $storeId = 2; - $region = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\RegionInterface::class, [], '', false); - $address = $this->getMockForAbstractClass( - \Magento\Customer\Api\Data\AddressInterface::class, - [], - '', - false, - false, - true, - [ - 'setCustomerId', - 'setRegion', - 'getRegion', - 'getId' - ] - ); - $address2 = $this->getMockForAbstractClass( - \Magento\Customer\Api\Data\AddressInterface::class, - [], - '', - false, - false, - true, - [ - 'setCustomerId', - 'setRegion', - 'getRegion', - 'getId' - ] - ); $customerModel = $this->createPartialMock(\Magento\Customer\Model\Customer::class, [ 'getId', 'setId', @@ -243,10 +214,6 @@ public function testSave() $origCustomer = $this->customer; - $this->customer->expects($this->atLeastOnce()) - ->method('__toArray') - ->willReturn(['default_billing', 'default_shipping']); - $customerAttributesMetaData = $this->getMockForAbstractClass( \Magento\Framework\Api\CustomAttributesDataInterface::class, [], @@ -273,6 +240,9 @@ public function testSave() $this->customer->expects($this->atLeastOnce()) ->method('getId') ->willReturn($customerId); + $this->customer->expects($this->atLeastOnce()) + ->method('__toArray') + ->willReturn([]); $this->customerRegistry->expects($this->atLeastOnce()) ->method('retrieve') ->with($customerId) @@ -287,28 +257,6 @@ public function testSave() $this->customerRegistry->expects($this->atLeastOnce()) ->method("remove") ->with($customerId); - $address->expects($this->once()) - ->method('setCustomerId') - ->with($customerId) - ->willReturnSelf(); - $address->expects($this->once()) - ->method('getRegion') - ->willReturn($region); - $address->expects($this->atLeastOnce()) - ->method('getId') - ->willReturn(7); - $address->expects($this->once()) - ->method('setRegion') - ->with($region); - $customerAttributesMetaData->expects($this->atLeastOnce()) - ->method('getAddresses') - ->willReturn([$address]); - $customerAttributesMetaData->expects($this->at(1)) - ->method('setAddresses') - ->with([]); - $customerAttributesMetaData->expects($this->at(2)) - ->method('setAddresses') - ->with([$address]); $this->extensibleDataObjectConverter->expects($this->once()) ->method('toNestedArray') ->with($customerAttributesMetaData, [], \Magento\Customer\Api\Data\CustomerInterface::class) @@ -334,12 +282,6 @@ public function testSave() $customerModel->expects($this->once()) ->method('setId') ->with($customerId); - $customerModel->expects($this->once()) - ->method('getAttributeSetId') - ->willReturn(null); - $customerModel->expects($this->once()) - ->method('setAttributeSetId') - ->with(\Magento\Customer\Api\CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER); $customerAttributesMetaData->expects($this->atLeastOnce()) ->method('getId') ->willReturn($customerId); @@ -399,12 +341,6 @@ public function testSave() $this->customerRegistry->expects($this->once()) ->method('push') ->with($customerModel); - $this->customer->expects($this->once()) - ->method('getAddresses') - ->willReturn([$address, $address2]); - $this->addressRepository->expects($this->once()) - ->method('save') - ->with($address); $customerAttributesMetaData->expects($this->once()) ->method('getEmail') ->willReturn('example@example.com'); @@ -446,47 +382,8 @@ public function testSaveWithPasswordHash() 'getFirstFailure', 'getLockExpires', ]); - $region = $this->getMockForAbstractClass( - \Magento\Customer\Api\Data\RegionInterface::class, - [], - '', - false - ); - $address = $this->getMockForAbstractClass( - \Magento\Customer\Api\Data\AddressInterface::class, - [], - '', - false, - false, - true, - [ - 'setCustomerId', - 'setRegion', - 'getRegion', - 'getId' - ] - ); - $address2 = $this->getMockForAbstractClass( - \Magento\Customer\Api\Data\AddressInterface::class, - [], - '', - false, - false, - true, - [ - 'setCustomerId', - 'setRegion', - 'getRegion', - 'getId' - ] - ); - $origCustomer = $this->customer; - $this->customer->expects($this->atLeastOnce()) - ->method('__toArray') - ->willReturn(['default_billing', 'default_shipping']); - $customerModel = $this->createPartialMock(\Magento\Customer\Model\Customer::class, [ 'getId', 'setId', @@ -554,6 +451,9 @@ public function testSaveWithPasswordHash() $this->customer->expects($this->atLeastOnce()) ->method('getId') ->willReturn($customerId); + $this->customer->expects($this->atLeastOnce()) + ->method('__toArray') + ->willReturn([]); $this->customerRegistry->expects($this->atLeastOnce()) ->method('retrieve') ->with($customerId) @@ -565,28 +465,6 @@ public function testSaveWithPasswordHash() ->method('save') ->with($this->customer, CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, $this->customer) ->willReturn($customerAttributesMetaData); - $address->expects($this->once()) - ->method('setCustomerId') - ->with($customerId) - ->willReturnSelf(); - $address->expects($this->once()) - ->method('getRegion') - ->willReturn($region); - $address->expects($this->atLeastOnce()) - ->method('getId') - ->willReturn(7); - $address->expects($this->once()) - ->method('setRegion') - ->with($region); - $customerAttributesMetaData->expects($this->any()) - ->method('getAddresses') - ->willReturn([$address]); - $customerAttributesMetaData->expects($this->at(1)) - ->method('setAddresses') - ->with([]); - $customerAttributesMetaData->expects($this->at(2)) - ->method('setAddresses') - ->with([$address]); $customerAttributesMetaData ->expects($this->atLeastOnce()) ->method('getId') @@ -616,12 +494,6 @@ public function testSaveWithPasswordHash() $customerModel->expects($this->once()) ->method('setId') ->with($customerId); - $customerModel->expects($this->once()) - ->method('getAttributeSetId') - ->willReturn(null); - $customerModel->expects($this->once()) - ->method('setAttributeSetId') - ->with(\Magento\Customer\Api\CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER); $customerModel->expects($this->atLeastOnce()) ->method('getId') ->willReturn($customerId); @@ -630,12 +502,6 @@ public function testSaveWithPasswordHash() $this->customerRegistry->expects($this->once()) ->method('push') ->with($customerModel); - $this->customer->expects($this->any()) - ->method('getAddresses') - ->willReturn([$address, $address2]); - $this->addressRepository->expects($this->once()) - ->method('save') - ->with($address); $customerAttributesMetaData->expects($this->once()) ->method('getEmail') ->willReturn('example@example.com'); @@ -752,7 +618,7 @@ public function testGetList() ->willReturnSelf(); $collection->expects($this->at(7)) ->method('joinAttribute') - ->with('company', 'customer_address/company', 'default_billing', null, 'left') + ->with('billing_company', 'customer_address/company', 'default_billing', null, 'left') ->willReturnSelf(); $this->collectionProcessorMock->expects($this->once()) ->method('process') diff --git a/app/code/Magento/Customer/Test/Unit/Observer/UpgradeCustomerPasswordObserverTest.php b/app/code/Magento/Customer/Test/Unit/Observer/UpgradeCustomerPasswordObserverTest.php index 8971f155f782..188bbde71c10 100644 --- a/app/code/Magento/Customer/Test/Unit/Observer/UpgradeCustomerPasswordObserverTest.php +++ b/app/code/Magento/Customer/Test/Unit/Observer/UpgradeCustomerPasswordObserverTest.php @@ -7,6 +7,9 @@ use Magento\Customer\Observer\UpgradeCustomerPasswordObserver; +/** + * Class UpgradeCustomerPasswordObserverTest for testing upgrade password observer + */ class UpgradeCustomerPasswordObserverTest extends \PHPUnit\Framework\TestCase { /** @@ -29,9 +32,13 @@ class UpgradeCustomerPasswordObserverTest extends \PHPUnit\Framework\TestCase */ protected $customerRegistry; + /** + * @inheritdoc + */ protected function setUp() { - $this->customerRepository = $this->getMockBuilder(\Magento\Customer\Api\CustomerRepositoryInterface::class) + $this->customerRepository = $this + ->getMockBuilder(\Magento\Customer\Api\CustomerRepositoryInterface::class) ->getMockForAbstractClass(); $this->customerRegistry = $this->getMockBuilder(\Magento\Customer\Model\CustomerRegistry::class) ->disableOriginalConstructor() @@ -47,6 +54,9 @@ protected function setUp() ); } + /** + * Unit test for verifying customers password upgrade observer + */ public function testUpgradeCustomerPassword() { $customerId = '1'; @@ -57,6 +67,8 @@ public function testUpgradeCustomerPassword() ->setMethods(['getId']) ->getMock(); $customer = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) + ->setMethods(['setData']) + ->disableOriginalConstructor() ->getMockForAbstractClass(); $customerSecure = $this->getMockBuilder(\Magento\Customer\Model\Data\CustomerSecure::class) ->disableOriginalConstructor() diff --git a/app/code/Magento/Customer/Test/Unit/Ui/Component/DataProvider/DocumentTest.php b/app/code/Magento/Customer/Test/Unit/Ui/Component/DataProvider/DocumentTest.php index 1d7905cca794..a9c6de72acbe 100644 --- a/app/code/Magento/Customer/Test/Unit/Ui/Component/DataProvider/DocumentTest.php +++ b/app/code/Magento/Customer/Test/Unit/Ui/Component/DataProvider/DocumentTest.php @@ -175,7 +175,7 @@ public function testGetConfirmationAttribute() $this->document->setData('original_website_id', $websiteId); $this->scopeConfig->expects(static::once()) - ->method('getValue') + ->method('isSetFlag') ->with() ->willReturn(true); diff --git a/app/code/Magento/Customer/Test/Unit/Ui/Component/Form/AddressFieldsetTest.php b/app/code/Magento/Customer/Test/Unit/Ui/Component/Form/AddressFieldsetTest.php new file mode 100644 index 000000000000..65a0443aed86 --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Ui/Component/Form/AddressFieldsetTest.php @@ -0,0 +1,69 @@ +<?php +declare(strict_types=1); +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Customer\Test\Unit\Ui\Component\Form; + +use Magento\Customer\Ui\Component\Form\AddressFieldset; +use Magento\Framework\View\Element\UiComponent\ContextInterface; + +/** + * Test for class \Magento\Customer\Ui\Component\Form\AddressFieldset + */ +class AddressFieldsetTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var AddressFieldset + */ + protected $fieldset; + + /** + * @var ContextInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $context; + + /** + * Set up + * + * @return void + */ + protected function setUp() + { + $this->context = $this->getMockForAbstractClass( + \Magento\Framework\View\Element\UiComponent\ContextInterface::class + ); + $this->fieldset = new AddressFieldset( + $this->context, + [], + [] + ); + } + + /** + * Run test for canShow() method + * + * @return void + * + */ + public function testCanShow() + { + $this->context->expects($this->atLeastOnce())->method('getRequestParam')->with('id') + ->willReturn(1); + $this->assertTrue($this->fieldset->isComponentVisible()); + } + + /** + * Run test for canShow() method without customer id in context + * + * @return void + * + */ + public function testCanShowWithoutId() + { + $this->context->expects($this->atLeastOnce())->method('getRequestParam')->with('id') + ->willReturn(null); + $this->assertEquals(false, $this->fieldset->isComponentVisible()); + } +} diff --git a/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/GroupActionsTest.php b/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/GroupActionsTest.php deleted file mode 100644 index fdd841ea88cf..000000000000 --- a/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/GroupActionsTest.php +++ /dev/null @@ -1,141 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -declare(strict_types=1); - -namespace Magento\Customer\Test\Unit\Ui\Component\Listing\Column; - -use Magento\Customer\Ui\Component\Listing\Column\GroupActions; -use Magento\Framework\Escaper; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\Framework\UrlInterface; -use Magento\Framework\View\Element\UiComponent\ContextInterface; -use Magento\Framework\View\Element\UiComponent\Processor; -use PHPUnit_Framework_MockObject_MockObject as MockObject; - -/** - * GroupActionsTest contains unit tests for \Magento\Customer\Ui\Component\Listing\Column\GroupActions class - */ -class GroupActionsTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var GroupActions - */ - protected $groupActions; - - /** - * @var Escaper|MockObject - */ - protected $escaper; - - /** - * @var UrlInterface|MockObject - */ - protected $urlBuilder; - - /** - * SetUp method - * - * @return void - */ - protected function setUp() - { - $objectManager = new ObjectManager($this); - - $context = $this->createMock(ContextInterface::class); - - $processor = $this->getMockBuilder(Processor::class) - ->disableOriginalConstructor() - ->getMock(); - $context->expects(static::never()) - ->method('getProcessor') - ->willReturn($processor); - - $this->urlBuilder = $this->createMock(UrlInterface::class); - - $this->escaper = $this->getMockBuilder(Escaper::class) - ->disableOriginalConstructor() - ->setMethods(['escapeHtml']) - ->getMock(); - - $this->groupActions = $objectManager->getObject(GroupActions::class, [ - 'context' => $context, - 'urlBuilder' => $this->urlBuilder, - 'escaper' => $this->escaper, - ]); - } - - /** - * @covers \Magento\Customer\Ui\Component\Listing\Column\GroupActions::prepareDataSource - */ - public function testPrepareDataSource() - { - $groupId = 1; - $groupCode = 'group code'; - $items = [ - 'data' => [ - 'items' => [ - [ - 'customer_group_id' => $groupId, - 'customer_group_code' => $groupCode - ] - ] - ] - ]; - $name = 'item_name'; - $expectedItems = [ - [ - 'customer_group_id' => $groupId, - 'customer_group_code' => $groupCode, - $name => [ - 'edit' => [ - 'href' => 'test/url/edit', - 'label' => __('Edit'), - ], - 'delete' => [ - 'href' => 'test/url/delete', - 'label' => __('Delete'), - 'confirm' => [ - 'title' => __('Delete %1', $groupCode), - 'message' => __('Are you sure you want to delete a %1 record?', $groupCode) - ], - ] - ], - ] - ]; - - $this->escaper->expects(static::once()) - ->method('escapeHtml') - ->with($groupCode) - ->willReturn($groupCode); - - $this->urlBuilder->expects(static::exactly(2)) - ->method('getUrl') - ->willReturnMap( - [ - [ - GroupActions::URL_PATH_EDIT, - [ - 'id' => $groupId - ], - 'test/url/edit', - ], - [ - GroupActions::URL_PATH_DELETE, - [ - 'id' => $groupId - ], - 'test/url/delete', - ], - ] - ); - - $this->groupActions->setData('name', $name); - - $actual = $this->groupActions->prepareDataSource($items); - static::assertEquals($expectedItems, $actual['data']['items']); - } -} diff --git a/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/ValidationRulesTest.php b/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/ValidationRulesTest.php index 130b3acd11e7..07b0a7604320 100644 --- a/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/ValidationRulesTest.php +++ b/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/Column/ValidationRulesTest.php @@ -18,12 +18,6 @@ class ValidationRulesTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->validationRules = $this->getMockBuilder( - \Magento\Customer\Ui\Component\Listing\Column\ValidationRules::class - ) - ->disableOriginalConstructor() - ->getMock(); - $this->validationRule = $this->getMockBuilder(\Magento\Customer\Api\Data\ValidationRuleInterface::class) ->disableOriginalConstructor() ->getMock(); @@ -31,20 +25,26 @@ protected function setUp() $this->validationRules = new ValidationRules(); } - public function testGetValidationRules() + /** + * Tests input validation rules + * + * @param String $validationRule - provided input validation rules + * @param String $validationClass - expected input validation class + * @dataProvider validationRulesDataProvider + */ + public function testGetValidationRules(String $validationRule, String $validationClass): void { $expectsRules = [ 'required-entry' => true, - 'validate-number' => true, + $validationClass => true, ]; - $this->validationRule->expects($this->atLeastOnce()) - ->method('getName') + $this->validationRule->method('getName') ->willReturn('input_validation'); - $this->validationRule->expects($this->atLeastOnce()) - ->method('getValue') - ->willReturn('numeric'); - $this->assertEquals( + $this->validationRule->method('getValue') + ->willReturn($validationRule); + + self::assertEquals( $expectsRules, $this->validationRules->getValidationRules( true, @@ -56,6 +56,23 @@ public function testGetValidationRules() ); } + /** + * Provides possible validation rules. + * + * @return array + */ + public function validationRulesDataProvider(): array + { + return [ + ['alpha', 'validate-alpha'], + ['numeric', 'validate-number'], + ['alphanumeric', 'validate-alphanum'], + ['alphanum-with-spaces', 'validate-alphanum-with-spaces'], + ['url', 'validate-url'], + ['email', 'validate-email'] + ]; + } + public function testGetValidationRulesWithOnlyRequiredRule() { $expectsRules = [ diff --git a/app/code/Magento/Customer/Ui/Component/DataProvider/Document.php b/app/code/Magento/Customer/Ui/Component/DataProvider/Document.php index a9a5c5b17744..468a9e7946f2 100644 --- a/app/code/Magento/Customer/Ui/Component/DataProvider/Document.php +++ b/app/code/Magento/Customer/Ui/Component/DataProvider/Document.php @@ -72,6 +72,7 @@ class Document extends \Magento\Framework\View\Element\UiComponent\DataProvider\ /** * Document constructor. + * * @param AttributeValueFactory $attributeValueFactory * @param GroupRepositoryInterface $groupRepository * @param CustomerMetadataInterface $customerMetadata @@ -118,9 +119,10 @@ public function getCustomAttribute($attributeCode) } /** - * Update customer gender value - * Method set gender label instead of id value + * Update customer gender value. Method set gender label instead of id value + * * @return void + * @throws \Magento\Framework\Exception\LocalizedException */ private function setGenderValue() { @@ -141,9 +143,10 @@ private function setGenderValue() } /** - * Update customer group value - * Method set group code instead id value + * Update customer group value. Method set group code instead id value + * * @return void + * @throws \Magento\Framework\Exception\LocalizedException */ private function setCustomerGroupValue() { @@ -157,8 +160,8 @@ private function setCustomerGroupValue() } /** - * Update website value - * Method set website name instead id value + * Update website value. Method set website name instead id value + * * @return void */ private function setWebsiteValue() @@ -170,22 +173,22 @@ private function setWebsiteValue() } /** - * Update confirmation value - * Method set confirmation text value to match what is shown in grid + * Update confirmation value. Method set confirmation text value to match what is shown in grid + * * @return void */ private function setConfirmationValue() { $value = $this->getData(self::$confirmationAttributeCode); $websiteId = $this->getData(self::$websiteIdAttributeCode) ?: $this->getData(self::$websiteAttributeCode); - $isConfirmationRequired = (bool)$this->scopeConfig->getValue( + $isConfirmRequired = $this->scopeConfig->isSetFlag( AccountManagement::XML_PATH_IS_CONFIRM, ScopeInterface::SCOPE_WEBSITES, $websiteId ); $valueText = __('Confirmation Not Required'); - if ($isConfirmationRequired) { + if ($isConfirmRequired) { $valueText = $value === null ? __('Confirmed') : __('Confirmation Required'); } @@ -193,8 +196,8 @@ private function setConfirmationValue() } /** - * Update lock expires value - * Method set account lock text value to match what is shown in grid + * Update lock expires value. Method set account lock text value to match what is shown in grid + * * @return void */ private function setAccountLockValue() diff --git a/app/code/Magento/Customer/Ui/Component/Form/AddressFieldset.php b/app/code/Magento/Customer/Ui/Component/Form/AddressFieldset.php new file mode 100644 index 000000000000..3e3a6e74166a --- /dev/null +++ b/app/code/Magento/Customer/Ui/Component/Form/AddressFieldset.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Ui\Component\Form; + +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\ComponentVisibilityInterface; + +/** + * Customer addresses fieldset class + */ +class AddressFieldset extends \Magento\Ui\Component\Form\Fieldset implements ComponentVisibilityInterface +{ + /** + * @param ContextInterface $context + * @param array $components + * @param array $data + */ + public function __construct( + ContextInterface $context, + array $components = [], + array $data = [] + ) { + $this->context = $context; + + parent::__construct($context, $components, $data); + } + + /** + * Can show customer addresses tab in tabs or not + * + * Will return false for not registered customer in a case when admin user created new customer account. + * Needed to hide addresses tab from create new customer page + * + * @return boolean + */ + public function isComponentVisible(): bool + { + $customerId = $this->context->getRequestParam('id'); + return (bool)$customerId; + } +} diff --git a/app/code/Magento/Customer/Ui/Component/Listing/Address/Column/Actions.php b/app/code/Magento/Customer/Ui/Component/Listing/Address/Column/Actions.php new file mode 100644 index 000000000000..6d1d7472a798 --- /dev/null +++ b/app/code/Magento/Customer/Ui/Component/Listing/Address/Column/Actions.php @@ -0,0 +1,128 @@ +<?php +declare(strict_types=1); +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Customer\Ui\Component\Listing\Address\Column; + +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Ui\Component\Listing\Columns\Column; +use Magento\Framework\UrlInterface; + +/** + * Prepare actions column for customer addresses grid + */ +class Actions extends Column +{ + const CUSTOMER_ADDRESS_PATH_DELETE = 'customer/address/delete'; + const CUSTOMER_ADDRESS_PATH_DEFAULT_SHIPPING = 'customer/address/defaultShippingAddress'; + const CUSTOMER_ADDRESS_PATH_DEFAULT_BILLING = 'customer/address/defaultBillingAddress'; + + /** + * @var UrlInterface + */ + private $urlBuilder; + + /** + * @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): array + { + if (isset($dataSource['data']['items'])) { + foreach ($dataSource['data']['items'] as &$item) { + $name = $this->getData('name'); + if (isset($item['entity_id'])) { + $item[$name]['edit'] = [ + 'callback' => [ + [ + 'provider' => 'customer_form.areas.address.address' + . '.customer_address_update_modal.update_customer_address_form_loader', + 'target' => 'destroyInserted', + ], + [ + 'provider' => 'customer_form.areas.address.address' + . '.customer_address_update_modal', + 'target' => 'openModal', + ], + [ + 'provider' => 'customer_form.areas.address.address' + . '.customer_address_update_modal.update_customer_address_form_loader', + 'target' => 'render', + 'params' => [ + 'entity_id' => $item['entity_id'], + ], + ] + ], + 'href' => '#', + 'label' => __('Edit'), + 'hidden' => false, + ]; + + $item[$name]['setDefaultBilling'] = [ + 'href' => $this->urlBuilder->getUrl( + self::CUSTOMER_ADDRESS_PATH_DEFAULT_BILLING, + ['parent_id' => $item['parent_id'], 'id' => $item['entity_id']] + ), + 'label' => __('Set as default billing'), + 'isAjax' => true, + 'confirm' => [ + 'title' => __('Set address as default billing'), + 'message' => __('Are you sure you want to set the address as default billing address?') + ] + ]; + + $item[$name]['setDefaultShipping'] = [ + 'href' => $this->urlBuilder->getUrl( + self::CUSTOMER_ADDRESS_PATH_DEFAULT_SHIPPING, + ['parent_id' => $item['parent_id'], 'id' => $item['entity_id']] + ), + 'label' => __('Set as default shipping'), + 'isAjax' => true, + 'confirm' => [ + 'title' => __('Set address as default shipping'), + 'message' => __('Are you sure you want to set the address as default shipping address?') + ] + ]; + + $item[$name]['delete'] = [ + 'href' => $this->urlBuilder->getUrl( + self::CUSTOMER_ADDRESS_PATH_DELETE, + ['parent_id' => $item['parent_id'], 'id' => $item['entity_id']] + ), + 'label' => __('Delete'), + 'isAjax' => true, + 'confirm' => [ + 'title' => __('Delete address'), + 'message' => __('Are you sure you want to delete the address?') + ] + ]; + } + } + } + + return $dataSource; + } +} diff --git a/app/code/Magento/Customer/Ui/Component/Listing/Address/Column/Countries.php b/app/code/Magento/Customer/Ui/Component/Listing/Address/Column/Countries.php new file mode 100644 index 000000000000..d05d5d1c592a --- /dev/null +++ b/app/code/Magento/Customer/Ui/Component/Listing/Address/Column/Countries.php @@ -0,0 +1,41 @@ +<?php +declare(strict_types=1); +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Customer\Ui\Component\Listing\Address\Column; + +use Magento\Framework\Data\OptionSourceInterface; + +/** + * Class for process countries in customer addresses grid + */ +class Countries implements OptionSourceInterface +{ + /** + * @var \Magento\Directory\Model\ResourceModel\Country\CollectionFactory + */ + private $countryCollectionFactory; + + /** + * @param \Magento\Directory\Model\ResourceModel\Country\CollectionFactory $collectionFactory + */ + public function __construct( + \Magento\Directory\Model\ResourceModel\Country\CollectionFactory $collectionFactory + ) { + $this->countryCollectionFactory = $collectionFactory; + } + + /** + * Get list of countries with country id as value and code as label + * + * @return array + */ + public function toOptionArray(): array + { + /** @var \Magento\Directory\Model\ResourceModel\Country\Collection $countryCollection */ + $countryCollection = $this->countryCollectionFactory->create(); + return $countryCollection->toOptionArray(); + } +} diff --git a/app/code/Magento/Customer/Ui/Component/Listing/Address/DataProvider.php b/app/code/Magento/Customer/Ui/Component/Listing/Address/DataProvider.php new file mode 100644 index 000000000000..c70e25ee99ec --- /dev/null +++ b/app/code/Magento/Customer/Ui/Component/Listing/Address/DataProvider.php @@ -0,0 +1,113 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Customer\Ui\Component\Listing\Address; + +use Magento\Customer\Model\ResourceModel\Address\Grid\CollectionFactory; +use Magento\Directory\Model\CountryFactory; +use Magento\Framework\Api\Filter; + +/** + * Custom DataProvider for customer addresses listing + */ +class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider +{ + /** + * @var \Magento\Framework\App\RequestInterface $request, + */ + private $request; + + /** + * @var CountryFactory + */ + private $countryDirectory; + + /** + * @param string $name + * @param string $primaryFieldName + * @param string $requestFieldName + * @param CollectionFactory $collectionFactory + * @param \Magento\Framework\App\RequestInterface $request + * @param CountryFactory $countryFactory + * @param array $meta + * @param array $data + */ + public function __construct( + $name, + $primaryFieldName, + $requestFieldName, + CollectionFactory $collectionFactory, + \Magento\Framework\App\RequestInterface $request, + CountryFactory $countryFactory, + array $meta = [], + array $data = [] + ) { + parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data); + $this->collection = $collectionFactory->create(); + $this->countryDirectory = $countryFactory->create(); + $this->request = $request; + } + + /** + * Add country key for default billing/shipping blocks on customer addresses tab + * + * @return array + */ + public function getData(): array + { + $collection = $this->getCollection(); + $data['items'] = []; + if ($this->request->getParam('parent_id')) { + $collection->addFieldToFilter('parent_id', $this->request->getParam('parent_id')); + $data = $collection->toArray(); + } + foreach ($data['items'] as $key => $item) { + if (isset($item['country_id']) && !isset($item['country'])) { + $data['items'][$key]['country'] = $this->countryDirectory->loadByCode($item['country_id'])->getName(); + } + } + + return $data; + } + + /** + * Add full text search filter to collection + * + * @param Filter $filter + * @return void + */ + public function addFilter(Filter $filter): void + { + if ($filter->getField() !== 'fulltext') { + $this->collection->addFieldToFilter( + $filter->getField(), + [$filter->getConditionType() => $filter->getValue()] + ); + } else { + $value = trim($filter->getValue()); + $this->collection->addFieldToFilter( + [ + ['attribute' => 'firstname'], + ['attribute' => 'lastname'], + ['attribute' => 'street'], + ['attribute' => 'city'], + ['attribute' => 'region'], + ['attribute' => 'postcode'], + ['attribute' => 'telephone'] + ], + [ + ['like' => "%{$value}%"], + ['like' => "%{$value}%"], + ['like' => "%{$value}%"], + ['like' => "%{$value}%"], + ['like' => "%{$value}%"], + ['like' => "%{$value}%"], + ['like' => "%{$value}%"], + ['like' => "%{$value}%"], + ] + ); + } + } +} diff --git a/app/code/Magento/Customer/Ui/Component/Listing/Column/GroupActions.php b/app/code/Magento/Customer/Ui/Component/Listing/Column/GroupActions.php index a8a3429ebadb..00c5f99fab46 100644 --- a/app/code/Magento/Customer/Ui/Component/Listing/Column/GroupActions.php +++ b/app/code/Magento/Customer/Ui/Component/Listing/Column/GroupActions.php @@ -98,7 +98,8 @@ public function prepareDataSource(array $dataSource) 'confirm' => [ 'title' => __('Delete %1', $title), 'message' => __('Are you sure you want to delete a %1 record?', $title) - ] + ], + 'post' => true ]; } } diff --git a/app/code/Magento/Customer/Ui/Component/Listing/Column/ValidationRules.php b/app/code/Magento/Customer/Ui/Component/Listing/Column/ValidationRules.php index b8f83421a6d6..6befec8e942a 100644 --- a/app/code/Magento/Customer/Ui/Component/Listing/Column/ValidationRules.php +++ b/app/code/Magento/Customer/Ui/Component/Listing/Column/ValidationRules.php @@ -7,6 +7,9 @@ use Magento\Customer\Api\Data\ValidationRuleInterface; +/** + * Provides validation classes according to corresponding rules. + */ class ValidationRules { /** @@ -16,6 +19,7 @@ class ValidationRules 'alpha' => 'validate-alpha', 'numeric' => 'validate-number', 'alphanumeric' => 'validate-alphanum', + 'alphanum-with-spaces' => 'validate-alphanum-with-spaces', 'url' => 'validate-url', 'email' => 'validate-email', ]; diff --git a/app/code/Magento/Customer/etc/acl.xml b/app/code/Magento/Customer/etc/acl.xml index e8e6219bef4f..1d45aa6445db 100644 --- a/app/code/Magento/Customer/etc/acl.xml +++ b/app/code/Magento/Customer/etc/acl.xml @@ -10,7 +10,13 @@ <resources> <resource id="Magento_Backend::admin"> <resource id="Magento_Customer::customer" title="Customers" translate="title" sortOrder="40"> - <resource id="Magento_Customer::manage" title="All Customers" translate="title" sortOrder="10" /> + <resource id="Magento_Customer::manage" title="All Customers" translate="title" sortOrder="10"> + <resource id="Magento_Customer::actions" title="Actions" translate="title" sortOrder="10"> + <resource id="Magento_Customer::delete" title="Delete" translate="title" sortOrder="10" /> + <resource id="Magento_Customer::reset_password" title="Reset password" translate="title" sortOrder="20" /> + <resource id="Magento_Customer::invalidate_tokens" title="Invalidate tokens" translate="title" sortOrder="30" /> + </resource> + </resource> <resource id="Magento_Customer::online" title="Now Online" translate="title" sortOrder="20" /> <resource id="Magento_Customer::group" title="Customer Groups" translate="title" sortOrder="30" /> </resource> diff --git a/app/code/Magento/Customer/etc/db_schema.xml b/app/code/Magento/Customer/etc/db_schema.xml index 368ca417432f..c699db06d30d 100644 --- a/app/code/Magento/Customer/etc/db_schema.xml +++ b/app/code/Magento/Customer/etc/db_schema.xml @@ -9,15 +9,15 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="customer_entity" resource="default" engine="innodb" comment="Customer Entity"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="true" identity="false" - comment="Website Id"/> + comment="Website ID"/> <column xsi:type="varchar" name="email" nullable="true" length="255" comment="Email"/> <column xsi:type="smallint" name="group_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Group Id"/> + default="0" comment="Group ID"/> <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment Id"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" - default="0" comment="Store Id"/> + default="0" comment="Store ID"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" @@ -50,37 +50,37 @@ <column xsi:type="timestamp" name="first_failure" on_update="false" nullable="true" comment="First Failure"/> <column xsi:type="timestamp" name="lock_expires" on_update="false" nullable="true" comment="Lock Expiration Date"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> - <constraint xsi:type="foreign" name="CUSTOMER_ENTITY_STORE_ID_STORE_STORE_ID" table="customer_entity" + <constraint xsi:type="foreign" referenceId="CUSTOMER_ENTITY_STORE_ID_STORE_STORE_ID" table="customer_entity" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="SET NULL"/> - <constraint xsi:type="foreign" name="CUSTOMER_ENTITY_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID" + <constraint xsi:type="foreign" referenceId="CUSTOMER_ENTITY_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID" table="customer_entity" column="website_id" referenceTable="store_website" referenceColumn="website_id" onDelete="SET NULL"/> - <constraint xsi:type="unique" name="CUSTOMER_ENTITY_EMAIL_WEBSITE_ID"> + <constraint xsi:type="unique" referenceId="CUSTOMER_ENTITY_EMAIL_WEBSITE_ID"> <column name="email"/> <column name="website_id"/> </constraint> - <index name="CUSTOMER_ENTITY_STORE_ID" indexType="btree"> + <index referenceId="CUSTOMER_ENTITY_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="CUSTOMER_ENTITY_WEBSITE_ID" indexType="btree"> + <index referenceId="CUSTOMER_ENTITY_WEBSITE_ID" indexType="btree"> <column name="website_id"/> </index> - <index name="CUSTOMER_ENTITY_FIRSTNAME" indexType="btree"> + <index referenceId="CUSTOMER_ENTITY_FIRSTNAME" indexType="btree"> <column name="firstname"/> </index> - <index name="CUSTOMER_ENTITY_LASTNAME" indexType="btree"> + <index referenceId="CUSTOMER_ENTITY_LASTNAME" indexType="btree"> <column name="lastname"/> </index> </table> <table name="customer_address_entity" resource="default" engine="innodb" comment="Customer Address Entity"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment Id"/> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Parent Id"/> + comment="Parent ID"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" @@ -111,13 +111,13 @@ comment="VAT number validation request ID"/> <column xsi:type="int" name="vat_request_success" padding="10" unsigned="true" nullable="true" identity="false" comment="VAT number validation request success"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> - <constraint xsi:type="foreign" name="CUSTOMER_ADDRESS_ENTITY_PARENT_ID_CUSTOMER_ENTITY_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="CUSTOMER_ADDRESS_ENTITY_PARENT_ID_CUSTOMER_ENTITY_ENTITY_ID" table="customer_address_entity" column="parent_id" referenceTable="customer_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <index name="CUSTOMER_ADDRESS_ENTITY_PARENT_ID" indexType="btree"> + <index referenceId="CUSTOMER_ADDRESS_ENTITY_PARENT_ID" indexType="btree"> <column name="parent_id"/> </index> </table> @@ -128,25 +128,25 @@ <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Attribute Id"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Id"/> + default="0" comment="Entity ID"/> <column xsi:type="datetime" name="value" on_update="false" nullable="true" comment="Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="foreign" name="CSTR_ADDR_ENTT_DTIME_ATTR_ID_EAV_ATTR_ATTR_ID" + <constraint xsi:type="foreign" referenceId="CSTR_ADDR_ENTT_DTIME_ATTR_ID_EAV_ATTR_ATTR_ID" table="customer_address_entity_datetime" column="attribute_id" referenceTable="eav_attribute" referenceColumn="attribute_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CSTR_ADDR_ENTT_DTIME_ENTT_ID_CSTR_ADDR_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="CSTR_ADDR_ENTT_DTIME_ENTT_ID_CSTR_ADDR_ENTT_ENTT_ID" table="customer_address_entity_datetime" column="entity_id" referenceTable="customer_address_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CUSTOMER_ADDRESS_ENTITY_DATETIME_ENTITY_ID_ATTRIBUTE_ID"> + <constraint xsi:type="unique" referenceId="CUSTOMER_ADDRESS_ENTITY_DATETIME_ENTITY_ID_ATTRIBUTE_ID"> <column name="entity_id"/> <column name="attribute_id"/> </constraint> - <index name="CUSTOMER_ADDRESS_ENTITY_DATETIME_ATTRIBUTE_ID" indexType="btree"> + <index referenceId="CUSTOMER_ADDRESS_ENTITY_DATETIME_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> - <index name="CUSTOMER_ADDRESS_ENTITY_DATETIME_ENTITY_ID_ATTRIBUTE_ID_VALUE" indexType="btree"> + <index referenceId="CUSTOMER_ADDRESS_ENTITY_DATETIME_ENTITY_ID_ATTRIBUTE_ID_VALUE" indexType="btree"> <column name="entity_id"/> <column name="attribute_id"/> <column name="value"/> @@ -159,26 +159,26 @@ <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Attribute Id"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Id"/> + default="0" comment="Entity ID"/> <column xsi:type="decimal" name="value" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="foreign" name="CSTR_ADDR_ENTT_DEC_ATTR_ID_EAV_ATTR_ATTR_ID" + <constraint xsi:type="foreign" referenceId="CSTR_ADDR_ENTT_DEC_ATTR_ID_EAV_ATTR_ATTR_ID" table="customer_address_entity_decimal" column="attribute_id" referenceTable="eav_attribute" referenceColumn="attribute_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CSTR_ADDR_ENTT_DEC_ENTT_ID_CSTR_ADDR_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="CSTR_ADDR_ENTT_DEC_ENTT_ID_CSTR_ADDR_ENTT_ENTT_ID" table="customer_address_entity_decimal" column="entity_id" referenceTable="customer_address_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CUSTOMER_ADDRESS_ENTITY_DECIMAL_ENTITY_ID_ATTRIBUTE_ID"> + <constraint xsi:type="unique" referenceId="CUSTOMER_ADDRESS_ENTITY_DECIMAL_ENTITY_ID_ATTRIBUTE_ID"> <column name="entity_id"/> <column name="attribute_id"/> </constraint> - <index name="CUSTOMER_ADDRESS_ENTITY_DECIMAL_ATTRIBUTE_ID" indexType="btree"> + <index referenceId="CUSTOMER_ADDRESS_ENTITY_DECIMAL_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> - <index name="CUSTOMER_ADDRESS_ENTITY_DECIMAL_ENTITY_ID_ATTRIBUTE_ID_VALUE" indexType="btree"> + <index referenceId="CUSTOMER_ADDRESS_ENTITY_DECIMAL_ENTITY_ID_ATTRIBUTE_ID_VALUE" indexType="btree"> <column name="entity_id"/> <column name="attribute_id"/> <column name="value"/> @@ -186,30 +186,30 @@ </table> <table name="customer_address_entity_int" resource="default" engine="innodb" comment="Customer Address Entity Int"> <column xsi:type="int" name="value_id" padding="11" unsigned="false" nullable="false" identity="true" - comment="Value Id"/> + comment="Value ID"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Attribute Id"/> + default="0" comment="Attribute ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Id"/> + default="0" comment="Entity ID"/> <column xsi:type="int" name="value" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="foreign" name="CSTR_ADDR_ENTT_INT_ATTR_ID_EAV_ATTR_ATTR_ID" + <constraint xsi:type="foreign" referenceId="CSTR_ADDR_ENTT_INT_ATTR_ID_EAV_ATTR_ATTR_ID" table="customer_address_entity_int" column="attribute_id" referenceTable="eav_attribute" referenceColumn="attribute_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CSTR_ADDR_ENTT_INT_ENTT_ID_CSTR_ADDR_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="CSTR_ADDR_ENTT_INT_ENTT_ID_CSTR_ADDR_ENTT_ENTT_ID" table="customer_address_entity_int" column="entity_id" referenceTable="customer_address_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CUSTOMER_ADDRESS_ENTITY_INT_ENTITY_ID_ATTRIBUTE_ID"> + <constraint xsi:type="unique" referenceId="CUSTOMER_ADDRESS_ENTITY_INT_ENTITY_ID_ATTRIBUTE_ID"> <column name="entity_id"/> <column name="attribute_id"/> </constraint> - <index name="CUSTOMER_ADDRESS_ENTITY_INT_ATTRIBUTE_ID" indexType="btree"> + <index referenceId="CUSTOMER_ADDRESS_ENTITY_INT_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> - <index name="CUSTOMER_ADDRESS_ENTITY_INT_ENTITY_ID_ATTRIBUTE_ID_VALUE" indexType="btree"> + <index referenceId="CUSTOMER_ADDRESS_ENTITY_INT_ENTITY_ID_ATTRIBUTE_ID_VALUE" indexType="btree"> <column name="entity_id"/> <column name="attribute_id"/> <column name="value"/> @@ -218,55 +218,55 @@ <table name="customer_address_entity_text" resource="default" engine="innodb" comment="Customer Address Entity Text"> <column xsi:type="int" name="value_id" padding="11" unsigned="false" nullable="false" identity="true" - comment="Value Id"/> + comment="Value ID"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Attribute Id"/> + default="0" comment="Attribute ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Id"/> + default="0" comment="Entity ID"/> <column xsi:type="text" name="value" nullable="false" comment="Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="foreign" name="CSTR_ADDR_ENTT_TEXT_ATTR_ID_EAV_ATTR_ATTR_ID" + <constraint xsi:type="foreign" referenceId="CSTR_ADDR_ENTT_TEXT_ATTR_ID_EAV_ATTR_ATTR_ID" table="customer_address_entity_text" column="attribute_id" referenceTable="eav_attribute" referenceColumn="attribute_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CSTR_ADDR_ENTT_TEXT_ENTT_ID_CSTR_ADDR_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="CSTR_ADDR_ENTT_TEXT_ENTT_ID_CSTR_ADDR_ENTT_ENTT_ID" table="customer_address_entity_text" column="entity_id" referenceTable="customer_address_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CUSTOMER_ADDRESS_ENTITY_TEXT_ENTITY_ID_ATTRIBUTE_ID"> + <constraint xsi:type="unique" referenceId="CUSTOMER_ADDRESS_ENTITY_TEXT_ENTITY_ID_ATTRIBUTE_ID"> <column name="entity_id"/> <column name="attribute_id"/> </constraint> - <index name="CUSTOMER_ADDRESS_ENTITY_TEXT_ATTRIBUTE_ID" indexType="btree"> + <index referenceId="CUSTOMER_ADDRESS_ENTITY_TEXT_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> </table> <table name="customer_address_entity_varchar" resource="default" engine="innodb" comment="Customer Address Entity Varchar"> <column xsi:type="int" name="value_id" padding="11" unsigned="false" nullable="false" identity="true" - comment="Value Id"/> + comment="Value ID"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Attribute Id"/> + default="0" comment="Attribute ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Id"/> + default="0" comment="Entity ID"/> <column xsi:type="varchar" name="value" nullable="true" length="255" comment="Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="foreign" name="CSTR_ADDR_ENTT_VCHR_ATTR_ID_EAV_ATTR_ATTR_ID" + <constraint xsi:type="foreign" referenceId="CSTR_ADDR_ENTT_VCHR_ATTR_ID_EAV_ATTR_ATTR_ID" table="customer_address_entity_varchar" column="attribute_id" referenceTable="eav_attribute" referenceColumn="attribute_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CSTR_ADDR_ENTT_VCHR_ENTT_ID_CSTR_ADDR_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="CSTR_ADDR_ENTT_VCHR_ENTT_ID_CSTR_ADDR_ENTT_ENTT_ID" table="customer_address_entity_varchar" column="entity_id" referenceTable="customer_address_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CUSTOMER_ADDRESS_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID"> + <constraint xsi:type="unique" referenceId="CUSTOMER_ADDRESS_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID"> <column name="entity_id"/> <column name="attribute_id"/> </constraint> - <index name="CUSTOMER_ADDRESS_ENTITY_VARCHAR_ATTRIBUTE_ID" indexType="btree"> + <index referenceId="CUSTOMER_ADDRESS_ENTITY_VARCHAR_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> - <index name="CUSTOMER_ADDRESS_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID_VALUE" indexType="btree"> + <index referenceId="CUSTOMER_ADDRESS_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID_VALUE" indexType="btree"> <column name="entity_id"/> <column name="attribute_id"/> <column name="value"/> @@ -274,29 +274,29 @@ </table> <table name="customer_entity_datetime" resource="default" engine="innodb" comment="Customer Entity Datetime"> <column xsi:type="int" name="value_id" padding="11" unsigned="false" nullable="false" identity="true" - comment="Value Id"/> + comment="Value ID"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Attribute Id"/> + default="0" comment="Attribute ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Id"/> + default="0" comment="Entity ID"/> <column xsi:type="datetime" name="value" on_update="false" nullable="true" comment="Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="foreign" name="CUSTOMER_ENTITY_DATETIME_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" + <constraint xsi:type="foreign" referenceId="CUSTOMER_ENTITY_DATETIME_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" table="customer_entity_datetime" column="attribute_id" referenceTable="eav_attribute" referenceColumn="attribute_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CUSTOMER_ENTITY_DATETIME_ENTITY_ID_CUSTOMER_ENTITY_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="CUSTOMER_ENTITY_DATETIME_ENTITY_ID_CUSTOMER_ENTITY_ENTITY_ID" table="customer_entity_datetime" column="entity_id" referenceTable="customer_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CUSTOMER_ENTITY_DATETIME_ENTITY_ID_ATTRIBUTE_ID"> + <constraint xsi:type="unique" referenceId="CUSTOMER_ENTITY_DATETIME_ENTITY_ID_ATTRIBUTE_ID"> <column name="entity_id"/> <column name="attribute_id"/> </constraint> - <index name="CUSTOMER_ENTITY_DATETIME_ATTRIBUTE_ID" indexType="btree"> + <index referenceId="CUSTOMER_ENTITY_DATETIME_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> - <index name="CUSTOMER_ENTITY_DATETIME_ENTITY_ID_ATTRIBUTE_ID_VALUE" indexType="btree"> + <index referenceId="CUSTOMER_ENTITY_DATETIME_ENTITY_ID_ATTRIBUTE_ID_VALUE" indexType="btree"> <column name="entity_id"/> <column name="attribute_id"/> <column name="value"/> @@ -304,30 +304,30 @@ </table> <table name="customer_entity_decimal" resource="default" engine="innodb" comment="Customer Entity Decimal"> <column xsi:type="int" name="value_id" padding="11" unsigned="false" nullable="false" identity="true" - comment="Value Id"/> + comment="Value ID"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Attribute Id"/> + default="0" comment="Attribute ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Id"/> + default="0" comment="Entity ID"/> <column xsi:type="decimal" name="value" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="foreign" name="CUSTOMER_ENTITY_DECIMAL_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" + <constraint xsi:type="foreign" referenceId="CUSTOMER_ENTITY_DECIMAL_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" table="customer_entity_decimal" column="attribute_id" referenceTable="eav_attribute" referenceColumn="attribute_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CUSTOMER_ENTITY_DECIMAL_ENTITY_ID_CUSTOMER_ENTITY_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="CUSTOMER_ENTITY_DECIMAL_ENTITY_ID_CUSTOMER_ENTITY_ENTITY_ID" table="customer_entity_decimal" column="entity_id" referenceTable="customer_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CUSTOMER_ENTITY_DECIMAL_ENTITY_ID_ATTRIBUTE_ID"> + <constraint xsi:type="unique" referenceId="CUSTOMER_ENTITY_DECIMAL_ENTITY_ID_ATTRIBUTE_ID"> <column name="entity_id"/> <column name="attribute_id"/> </constraint> - <index name="CUSTOMER_ENTITY_DECIMAL_ATTRIBUTE_ID" indexType="btree"> + <index referenceId="CUSTOMER_ENTITY_DECIMAL_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> - <index name="CUSTOMER_ENTITY_DECIMAL_ENTITY_ID_ATTRIBUTE_ID_VALUE" indexType="btree"> + <index referenceId="CUSTOMER_ENTITY_DECIMAL_ENTITY_ID_ATTRIBUTE_ID_VALUE" indexType="btree"> <column name="entity_id"/> <column name="attribute_id"/> <column name="value"/> @@ -335,30 +335,30 @@ </table> <table name="customer_entity_int" resource="default" engine="innodb" comment="Customer Entity Int"> <column xsi:type="int" name="value_id" padding="11" unsigned="false" nullable="false" identity="true" - comment="Value Id"/> + comment="Value ID"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Attribute Id"/> + default="0" comment="Attribute ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Id"/> + default="0" comment="Entity ID"/> <column xsi:type="int" name="value" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="foreign" name="CUSTOMER_ENTITY_INT_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" + <constraint xsi:type="foreign" referenceId="CUSTOMER_ENTITY_INT_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" table="customer_entity_int" column="attribute_id" referenceTable="eav_attribute" referenceColumn="attribute_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CUSTOMER_ENTITY_INT_ENTITY_ID_CUSTOMER_ENTITY_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="CUSTOMER_ENTITY_INT_ENTITY_ID_CUSTOMER_ENTITY_ENTITY_ID" table="customer_entity_int" column="entity_id" referenceTable="customer_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CUSTOMER_ENTITY_INT_ENTITY_ID_ATTRIBUTE_ID"> + <constraint xsi:type="unique" referenceId="CUSTOMER_ENTITY_INT_ENTITY_ID_ATTRIBUTE_ID"> <column name="entity_id"/> <column name="attribute_id"/> </constraint> - <index name="CUSTOMER_ENTITY_INT_ATTRIBUTE_ID" indexType="btree"> + <index referenceId="CUSTOMER_ENTITY_INT_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> - <index name="CUSTOMER_ENTITY_INT_ENTITY_ID_ATTRIBUTE_ID_VALUE" indexType="btree"> + <index referenceId="CUSTOMER_ENTITY_INT_ENTITY_ID_ATTRIBUTE_ID_VALUE" indexType="btree"> <column name="entity_id"/> <column name="attribute_id"/> <column name="value"/> @@ -366,54 +366,54 @@ </table> <table name="customer_entity_text" resource="default" engine="innodb" comment="Customer Entity Text"> <column xsi:type="int" name="value_id" padding="11" unsigned="false" nullable="false" identity="true" - comment="Value Id"/> + comment="Value ID"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Attribute Id"/> + default="0" comment="Attribute ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Id"/> + default="0" comment="Entity ID"/> <column xsi:type="text" name="value" nullable="false" comment="Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="foreign" name="CUSTOMER_ENTITY_TEXT_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" + <constraint xsi:type="foreign" referenceId="CUSTOMER_ENTITY_TEXT_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" table="customer_entity_text" column="attribute_id" referenceTable="eav_attribute" referenceColumn="attribute_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CUSTOMER_ENTITY_TEXT_ENTITY_ID_CUSTOMER_ENTITY_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="CUSTOMER_ENTITY_TEXT_ENTITY_ID_CUSTOMER_ENTITY_ENTITY_ID" table="customer_entity_text" column="entity_id" referenceTable="customer_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CUSTOMER_ENTITY_TEXT_ENTITY_ID_ATTRIBUTE_ID"> + <constraint xsi:type="unique" referenceId="CUSTOMER_ENTITY_TEXT_ENTITY_ID_ATTRIBUTE_ID"> <column name="entity_id"/> <column name="attribute_id"/> </constraint> - <index name="CUSTOMER_ENTITY_TEXT_ATTRIBUTE_ID" indexType="btree"> + <index referenceId="CUSTOMER_ENTITY_TEXT_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> </table> <table name="customer_entity_varchar" resource="default" engine="innodb" comment="Customer Entity Varchar"> <column xsi:type="int" name="value_id" padding="11" unsigned="false" nullable="false" identity="true" - comment="Value Id"/> + comment="Value ID"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Attribute Id"/> + default="0" comment="Attribute ID"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Id"/> + default="0" comment="Entity ID"/> <column xsi:type="varchar" name="value" nullable="true" length="255" comment="Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="foreign" name="CUSTOMER_ENTITY_VARCHAR_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" + <constraint xsi:type="foreign" referenceId="CUSTOMER_ENTITY_VARCHAR_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" table="customer_entity_varchar" column="attribute_id" referenceTable="eav_attribute" referenceColumn="attribute_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CUSTOMER_ENTITY_VARCHAR_ENTITY_ID_CUSTOMER_ENTITY_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="CUSTOMER_ENTITY_VARCHAR_ENTITY_ID_CUSTOMER_ENTITY_ENTITY_ID" table="customer_entity_varchar" column="entity_id" referenceTable="customer_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CUSTOMER_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID"> + <constraint xsi:type="unique" referenceId="CUSTOMER_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID"> <column name="entity_id"/> <column name="attribute_id"/> </constraint> - <index name="CUSTOMER_ENTITY_VARCHAR_ATTRIBUTE_ID" indexType="btree"> + <index referenceId="CUSTOMER_ENTITY_VARCHAR_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> - <index name="CUSTOMER_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID_VALUE" indexType="btree"> + <index referenceId="CUSTOMER_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID_VALUE" indexType="btree"> <column name="entity_id"/> <column name="attribute_id"/> <column name="value"/> @@ -425,7 +425,7 @@ comment="Customer Group Code"/> <column xsi:type="int" name="tax_class_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Tax Class Id"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="customer_group_id"/> </constraint> </table> @@ -451,10 +451,10 @@ identity="false" default="0" comment="Is Filterable in Grid"/> <column xsi:type="smallint" name="is_searchable_in_grid" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Is Searchable in Grid"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="attribute_id"/> </constraint> - <constraint xsi:type="foreign" name="CUSTOMER_EAV_ATTRIBUTE_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" + <constraint xsi:type="foreign" referenceId="CUSTOMER_EAV_ATTRIBUTE_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" table="customer_eav_attribute" column="attribute_id" referenceTable="eav_attribute" referenceColumn="attribute_id" onDelete="CASCADE"/> </table> @@ -462,14 +462,14 @@ <column xsi:type="varchar" name="form_code" nullable="false" length="32" comment="Form Code"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Attribute Id"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="form_code"/> <column name="attribute_id"/> </constraint> - <constraint xsi:type="foreign" name="CUSTOMER_FORM_ATTRIBUTE_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" + <constraint xsi:type="foreign" referenceId="CUSTOMER_FORM_ATTRIBUTE_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" table="customer_form_attribute" column="attribute_id" referenceTable="eav_attribute" referenceColumn="attribute_id" onDelete="CASCADE"/> - <index name="CUSTOMER_FORM_ATTRIBUTE_ATTRIBUTE_ID" indexType="btree"> + <index referenceId="CUSTOMER_FORM_ATTRIBUTE_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> </table> @@ -486,17 +486,17 @@ <column xsi:type="text" name="default_value" nullable="true" comment="Default Value"/> <column xsi:type="smallint" name="multiline_count" padding="5" unsigned="true" nullable="true" identity="false" comment="Multiline Count"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="attribute_id"/> <column name="website_id"/> </constraint> - <constraint xsi:type="foreign" name="CSTR_EAV_ATTR_WS_ATTR_ID_EAV_ATTR_ATTR_ID" + <constraint xsi:type="foreign" referenceId="CSTR_EAV_ATTR_WS_ATTR_ID_EAV_ATTR_ATTR_ID" table="customer_eav_attribute_website" column="attribute_id" referenceTable="eav_attribute" referenceColumn="attribute_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CSTR_EAV_ATTR_WS_WS_ID_STORE_WS_WS_ID" + <constraint xsi:type="foreign" referenceId="CSTR_EAV_ATTR_WS_WS_ID_STORE_WS_WS_ID" table="customer_eav_attribute_website" column="website_id" referenceTable="store_website" referenceColumn="website_id" onDelete="CASCADE"/> - <index name="CUSTOMER_EAV_ATTRIBUTE_WEBSITE_WEBSITE_ID" indexType="btree"> + <index referenceId="CUSTOMER_EAV_ATTRIBUTE_WEBSITE_WEBSITE_ID" indexType="btree"> <column name="website_id"/> </index> </table> @@ -506,15 +506,15 @@ <column xsi:type="int" name="customer_id" padding="11" unsigned="false" nullable="true" identity="false" comment="Customer Id"/> <column xsi:type="varchar" name="session_id" nullable="true" length="64" comment="Session ID"/> - <column xsi:type="timestamp" name="last_visit_at" on_update="true" nullable="true" default="CURRENT_TIMESTAMP" + <column xsi:type="timestamp" name="last_visit_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" comment="Last Visit Time"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="visitor_id"/> </constraint> - <index name="CUSTOMER_VISITOR_CUSTOMER_ID" indexType="btree"> + <index referenceId="CUSTOMER_VISITOR_CUSTOMER_ID" indexType="btree"> <column name="customer_id"/> </index> - <index name="CUSTOMER_VISITOR_LAST_VISIT_AT" indexType="btree"> + <index referenceId="CUSTOMER_VISITOR_LAST_VISIT_AT" indexType="btree"> <column name="last_visit_at"/> </index> </table> @@ -526,10 +526,10 @@ <column xsi:type="timestamp" name="last_login_at" on_update="false" nullable="true" comment="Last Login Time"/> <column xsi:type="timestamp" name="last_logout_at" on_update="false" nullable="true" comment="Last Logout Time"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="log_id"/> </constraint> - <constraint xsi:type="unique" name="CUSTOMER_LOG_CUSTOMER_ID"> + <constraint xsi:type="unique" referenceId="CUSTOMER_LOG_CUSTOMER_ID"> <column name="customer_id"/> </constraint> </table> diff --git a/app/code/Magento/Customer/etc/db_schema_whitelist.json b/app/code/Magento/Customer/etc/db_schema_whitelist.json index 4aada8f0d81f..ec7a53945aba 100644 --- a/app/code/Magento/Customer/etc/db_schema_whitelist.json +++ b/app/code/Magento/Customer/etc/db_schema_whitelist.json @@ -73,7 +73,8 @@ "vat_request_success": true }, "index": { - "CUSTOMER_ADDRESS_ENTITY_PARENT_ID": true + "CUSTOMER_ADDRESS_ENTITY_PARENT_ID": true, + "FTI_BA70344390184AC3F063AB2EB38BC0ED": true }, "constraint": { "PRIMARY": true, diff --git a/app/code/Magento/Customer/etc/di.xml b/app/code/Magento/Customer/etc/di.xml index 6e8c3dc68ed2..a181d6dd217f 100644 --- a/app/code/Magento/Customer/etc/di.xml +++ b/app/code/Magento/Customer/etc/di.xml @@ -127,6 +127,13 @@ <argument name="groupManagement" xsi:type="object">Magento\Customer\Api\GroupManagementInterface\Proxy</argument> </arguments> </type> + <type name="Magento\Customer\Model\Metadata\CustomerMetadata"> + <arguments> + <argument name="systemAttributes" xsi:type="array"> + <item name="disable_auto_group_change" xsi:type="string">disable_auto_group_change</item> + </argument> + </arguments> + </type> <virtualType name="SectionInvalidationConfigReader" type="Magento\Framework\Config\Reader\Filesystem"> <arguments> <argument name="idAttributes" xsi:type="array"> @@ -223,6 +230,7 @@ <item name="customer_listing_data_source" xsi:type="string">Magento\Customer\Model\ResourceModel\Grid\Collection</item> <item name="customer_online_grid_data_source" xsi:type="string">Magento\Customer\Model\ResourceModel\Online\Grid\Collection</item> <item name="customer_group_listing_data_source" xsi:type="string">Magento\Customer\Model\ResourceModel\Group\Grid\Collection</item> + <item name="customer_address_listing_data_source" xsi:type="string">Magento\Customer\Model\ResourceModel\Address\Grid\Collection</item> </argument> </arguments> </type> @@ -449,6 +457,14 @@ <argument name="resourceModel" xsi:type="string">Magento\Customer\Model\ResourceModel\Group</argument> </arguments> </type> + <type name="Magento\Customer\Model\ResourceModel\Address\Grid\Collection"> + <arguments> + <argument name="mainTable" xsi:type="string">customer_address_entity</argument> + <argument name="eventPrefix" xsi:type="string">customer_address_entity_grid_collection</argument> + <argument name="eventObject" xsi:type="string">customer_address_entity_grid_collection</argument> + <argument name="resourceModel" xsi:type="string">Magento\Customer\Model\ResourceModel\Address</argument> + </arguments> + </type> <preference for="Magento\Customer\Api\AccountDelegationInterface" type="Magento\Customer\Model\Delegation\AccountDelegation" /> diff --git a/app/code/Magento/Customer/etc/frontend/di.xml b/app/code/Magento/Customer/etc/frontend/di.xml index 4a45c4ad48d1..c31742519e58 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/i18n/en_US.csv b/app/code/Magento/Customer/i18n/en_US.csv index bf73d6361d4c..578267984f98 100644 --- a/app/code/Magento/Customer/i18n/en_US.csv +++ b/app/code/Magento/Customer/i18n/en_US.csv @@ -506,10 +506,10 @@ Strong,Strong "Rebuild Customer grid index","Rebuild Customer grid index" Group,Group "Add New Customer","Add New Customer" -"Are you sure to delete selected customers?","Are you sure to delete selected customers?" +"Are you sure you want to delete the selected customers?","Are you sure you want to delete the selected customers?" "Delete items","Delete items" "Subscribe to Newsletter","Subscribe to Newsletter" -"Are you sure to unsubscribe selected customers from newsletter?","Are you sure to unsubscribe selected customers from newsletter?" +"Are you sure you want to unsubscribe the selected customers from the newsletter?","Are you sure you want to unsubscribe the selected customers from the newsletter?" "Unsubscribe from Newsletter","Unsubscribe from Newsletter" "Assign a Customer Group","Assign a Customer Group" Phone,Phone diff --git a/app/code/Magento/Customer/view/adminhtml/layout/customer_address_edit.xml b/app/code/Magento/Customer/view/adminhtml/layout/customer_address_edit.xml new file mode 100644 index 000000000000..3acae3acec8a --- /dev/null +++ b/app/code/Magento/Customer/view/adminhtml/layout/customer_address_edit.xml @@ -0,0 +1,15 @@ +<?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" layout="admin-1column" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <update handle="styles"/> + <body> + <referenceContainer name="content"> + <uiComponent name="customer_address_form"/> + </referenceContainer> + </body> +</page> diff --git a/app/code/Magento/Customer/view/adminhtml/ui_component/customer_address_form.xml b/app/code/Magento/Customer/view/adminhtml/ui_component/customer_address_form.xml new file mode 100644 index 000000000000..35f0ed8f1dae --- /dev/null +++ b/app/code/Magento/Customer/view/adminhtml/ui_component/customer_address_form.xml @@ -0,0 +1,250 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<form xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd" component="Magento_Customer/js/form/components/form"> + <argument name="data" xsi:type="array"> + <item name="js_config" xsi:type="array"> + <item name="provider" xsi:type="string">customer_address_form.customer_address_form_data_source</item> + </item> + <item name="config" xsi:type="array"> + <item name="deleteConfirmationMessage" translate="true" xsi:type="string">Are you sure you want to delete this address?</item> + </item> + <item name="label" xsi:type="string" translate="true">Update Address</item> + <item name="reverseMetadataMerge" xsi:type="boolean">true</item> + <item name="template" xsi:type="string">templates/form/collapsible</item> + </argument> + <settings> + <buttons> + <button name="cancel" class="Magento\Customer\Block\Adminhtml\Edit\Address\CancelButton"/> + <button name="delete" class="Magento\Customer\Block\Adminhtml\Edit\Address\DeleteButton"/> + <button name="save" class="Magento\Customer\Block\Adminhtml\Edit\Address\SaveButton"/> + </buttons> + <namespace>customer_address_form</namespace> + <ajaxSave>true</ajaxSave> + <ajaxSaveType>simple</ajaxSaveType> + <dataScope>data</dataScope> + <deps> + <dep>customer_address_form.customer_address_form_data_source</dep> + </deps> + </settings> + <dataSource name="customer_address_form_data_source"> + <argument name="data" xsi:type="array"> + <item name="js_config" xsi:type="array"> + <item name="component" xsi:type="string">Magento_Ui/js/form/provider</item> + </item> + </argument> + <settings> + <submitUrl path="customer/address/save"/> + <validateUrl path="customer/address/validate"/> + </settings> + <aclResource>Magento_Customer::manage</aclResource> + <dataProvider class="Magento\Customer\Model\Address\DataProvider" name="customer_address_form_data_source"> + <settings> + <requestFieldName>entity_id</requestFieldName> + <primaryFieldName>entity_id</primaryFieldName> + </settings> + </dataProvider> + </dataSource> + <container name="messages" component="Magento_Ui/js/form/components/html"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="additionalClasses" xsi:type="string">message message-error</item> + <item name="visible" xsi:type="boolean">false</item> + <item name="imports" xsi:type="array"> + <item name="responseData" xsi:type="string">${ $.parentName }:responseData</item> + </item> + <item name="listens" xsi:type="array"> + <item name="responseData.error" xsi:type="string">visible</item> + <item name="responseData.messages" xsi:type="string">content</item> + </item> + </item> + </argument> + </container> + <fieldset name="general"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="is_collection" xsi:type="boolean">true</item> + </item> + </argument> + <settings> + <label/> + <dataScope/> + </settings> + + <field name="entity_id" formElement="hidden"> + <settings> + <dataType>text</dataType> + </settings> + </field> + <field name="default_billing" sortOrder="5" formElement="checkbox"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="default" xsi:type="number">0</item> + </item> + </argument> + <settings> + <dataType>boolean</dataType> + <label translate="true">Default Billing Address</label> + <dataScope>default_billing</dataScope> + </settings> + <formElements> + <checkbox> + <settings> + <valueMap> + <map name="false" xsi:type="number">0</map> + <map name="true" xsi:type="number">1</map> + </valueMap> + <prefer>toggle</prefer> + </settings> + </checkbox> + </formElements> + </field> + <field name="default_shipping" sortOrder="7" formElement="checkbox"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="default" xsi:type="number">0</item> + </item> + </argument> + <settings> + <dataType>boolean</dataType> + <label translate="true">Default Shipping Address</label> + <dataScope>default_shipping</dataScope> + </settings> + <formElements> + <checkbox> + <settings> + <valueMap> + <map name="false" xsi:type="number">0</map> + <map name="true" xsi:type="number">1</map> + </valueMap> + <prefer>toggle</prefer> + </settings> + </checkbox> + </formElements> + </field> + <field name="prefix" sortOrder="10" formElement="input"> + <settings> + <dataType>text</dataType> + <visible>true</visible> + <label translate="true">Name Prefix</label> + </settings> + </field> + <field name="firstname" sortOrder="20" formElement="input"> + <settings> + <dataType>text</dataType> + <visible>true</visible> + <label translate="true">First Name</label> + <validation> + <rule name="required-entry" xsi:type="boolean">true</rule> + </validation> + </settings> + </field> + <field name="lastname" sortOrder="30" formElement="input"> + <settings> + <dataType>text</dataType> + <visible>true</visible> + <label translate="true">Last Name</label> + <validation> + <rule name="required-entry" xsi:type="boolean">true</rule> + </validation> + </settings> + </field> + <field name="suffix" sortOrder="40" formElement="input"> + <settings> + <dataType>text</dataType> + <visible>true</visible> + <label translate="true">Name Suffix</label> + </settings> + </field> + <field name="middlename" sortOrder="50" formElement="input"> + <settings> + <dataType>text</dataType> + <visible>true</visible> + <label translate="true">Middle Name/Initial</label> + </settings> + </field> + <field name="company" sortOrder="60" formElement="input"> + <settings> + <dataType>text</dataType> + <visible>true</visible> + <label translate="true">Company</label> + </settings> + </field> + <field name="city" sortOrder="80" formElement="input"> + <settings> + <dataType>text</dataType> + <label translate="true">City</label> + <visible>true</visible> + <validation> + <rule name="required-entry" xsi:type="boolean">true</rule> + </validation> + </settings> + </field> + <field name="country_id" component="Magento_Customer/js/form/element/country" sortOrder="90" formElement="select"> + <settings> + <validation> + <rule name="required-entry" xsi:type="boolean">true</rule> + </validation> + <dataType>text</dataType> + </settings> + <formElements> + <select> + <settings> + <options class="Magento\Directory\Model\ResourceModel\Country\Collection"/> + </settings> + </select> + </formElements> + </field> + <field name="region_id" component="Magento_Customer/js/form/element/region" formElement="select"> + <settings> + <validation> + <rule name="required-entry" xsi:type="boolean">true</rule> + </validation> + <dataType>text</dataType> + <label translate="true">State/Province</label> + </settings> + <formElements> + <select> + <settings> + <filterBy> + <field>country_id</field> + <target>${ $.provider }:${ $.parentScope }.country_id</target> + </filterBy> + <customEntry>region</customEntry> + <options class="Magento\Directory\Model\ResourceModel\Region\Collection"/> + </settings> + </select> + </formElements> + </field> + <field name="postcode" component="Magento_Ui/js/form/element/post-code" sortOrder="120" formElement="input"> + <settings> + <dataType>text</dataType> + <visible>true</visible> + <label translate="true">Zip/Postal Code</label> + <validation> + <rule name="required-entry" xsi:type="boolean">true</rule> + </validation> + </settings> + </field> + <field name="telephone" sortOrder="130" formElement="input"> + <settings> + <dataType>text</dataType> + <visible>true</visible> + <label translate="true">Phone Number</label> + </settings> + </field> + <field name="vat_id" sortOrder="140" formElement="input"> + <settings> + <dataType>text</dataType> + <label translate="true">VAT Number</label> + <validation> + <rule name="validate-alphanum" xsi:type="boolean">true</rule> + </validation> + </settings> + </field> + </fieldset> +</form> diff --git a/app/code/Magento/Customer/view/adminhtml/ui_component/customer_address_listing.xml b/app/code/Magento/Customer/view/adminhtml/ui_component/customer_address_listing.xml new file mode 100644 index 000000000000..fb42a2c5a078 --- /dev/null +++ b/app/code/Magento/Customer/view/adminhtml/ui_component/customer_address_listing.xml @@ -0,0 +1,162 @@ +<?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">customer_address_listing.customer_address_listing_data_source</item> + </item> + </argument> + <settings> + <spinner>customer_address_columns</spinner> + <deps> + <dep>customer_address_listing.customer_address_listing_data_source</dep> + </deps> + </settings> + <dataSource name="customer_address_listing_data_source" component="Magento_Ui/js/grid/provider"> + <settings> + <filterUrlParams> + <param name="id">*</param> + </filterUrlParams> + <storageConfig> + <param name="indexField" xsi:type="string">entity_id</param> + </storageConfig> + <updateUrl path="mui/index/render"/> + </settings> + <aclResource>Magento_Customer::manage</aclResource> + <dataProvider class="Magento\Customer\Ui\Component\Listing\Address\DataProvider" name="customer_address_listing_data_source"> + <settings> + <requestFieldName>id</requestFieldName> + <primaryFieldName>entity_id</primaryFieldName> + </settings> + </dataProvider> + </dataSource> + <listingToolbar name="listing_top"> + <bookmark name="bookmarks"/> + <columnsControls name="columns_controls"/> + <filterSearch name="fulltext"/> + <filters name="listing_filters" component="Magento_Customer/js/grid/filters/filters"> + <settings> + <storageConfig> + <param name="provider" xsi:type="string">customer_address_listing.customer_address_listing.listing_top.bookmarks</param> + <param name="namespace" xsi:type="string">current.filters</param> + </storageConfig> + <childDefaults> + <param name="provider" xsi:type="string">customer_address_listing.customer_address_listing.listing_top.listing_filters</param> + <param name="imports" xsi:type="array"> + <item name="visible" xsi:type="string">customer_address_listing.customer_address_listing.listing_top.bookmarks:current.columns.${ $.index }.visible</item> + </param> + </childDefaults> + </settings> + </filters> + <massaction name="listing_massaction" component="Magento_Customer/js/grid/massactions"> + <action name="delete"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="isAjax" xsi:type="boolean">true</item> + </item> + </argument> + <settings> + <url path="customer/address/massDelete"/> + <type>delete</type> + <label translate="true">Delete</label> + <confirm> + <message translate="true">Are you sure to delete selected address?</message> + <title translate="true">Delete items + + + + + + + + + + + + false + + entity_id + true + customer_address_listing.customer_address_listing.address_columns.ids + + + + customer_address_listing.customer_address_listing.address_columns_editor + startEdit + + ${ $.$data.rowIndex } + true + + + + + + + entity_id + + + + + text + + + + + + text + + + + + + text + + + + + + text + + + + + + text + + + + + + text + + + + + + select + + select + + + + + + text + + text + + + + + + + entity_id + + + + diff --git a/app/code/Magento/Customer/view/adminhtml/ui_component/customer_listing.xml b/app/code/Magento/Customer/view/adminhtml/ui_component/customer_listing.xml index f8aa078f45e4..f845d407d401 100644 --- a/app/code/Magento/Customer/view/adminhtml/ui_component/customer_listing.xml +++ b/app/code/Magento/Customer/view/adminhtml/ui_component/customer_listing.xml @@ -49,7 +49,7 @@ - Are you sure to delete selected customers? + Are you sure you want to delete the selected customers? Delete items @@ -67,7 +67,7 @@ - Are you sure to unsubscribe selected customers from newsletter? + Are you sure you want to unsubscribe the selected customers from the newsletter? Unsubscribe from Newsletter @@ -174,6 +174,7 @@ + select select diff --git a/app/code/Magento/Customer/view/adminhtml/web/js/address/default-address.js b/app/code/Magento/Customer/view/adminhtml/web/js/address/default-address.js new file mode 100644 index 000000000000..07662cd9b105 --- /dev/null +++ b/app/code/Magento/Customer/view/adminhtml/web/js/address/default-address.js @@ -0,0 +1,50 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Ui/js/form/components/button', + 'underscore' +], function (Button, _) { + 'use strict'; + + return Button.extend({ + defaults: { + entityId: null, + parentId: null, + listens: { + entity: 'changeVisibility' + } + }, + + /** + * Apply action on target component, + * but previously create this component from template if it is not existed + * + * @param {Object} action - action configuration + */ + applyAction: function (action) { + if (action.params && action.params[0]) { + action.params[0]['entity_id'] = this.entityId; + action.params[0]['parent_id'] = this.parentId; + } else { + action.params = [{ + 'entity_id': this.entityId, + 'parent_id': this.parentId + }]; + } + + this._super(); + }, + + /** + * Change visibility of the default address shipping/billing blocks + * + * @param {Object} entity - customer address + */ + changeVisibility: function (entity) { + this.visible(!_.isEmpty(entity)); + } + }); +}); diff --git a/app/code/Magento/Customer/view/adminhtml/web/js/form/components/form.js b/app/code/Magento/Customer/view/adminhtml/web/js/form/components/form.js new file mode 100644 index 000000000000..812dbe5975c7 --- /dev/null +++ b/app/code/Magento/Customer/view/adminhtml/web/js/form/components/form.js @@ -0,0 +1,81 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'Magento_Ui/js/modal/alert', + 'Magento_Ui/js/modal/confirm', + 'Magento_Ui/js/form/form', + 'underscore', + 'mage/translate' +], function ($, uiAlert, uiConfirm, Form, _, $t) { + 'use strict'; + + return Form.extend({ + defaults: { + deleteConfirmationMessage: '', + ajaxSettings: { + method: 'POST', + dataType: 'json' + } + }, + + /** + * Delete customer address by provided url. + * Will call confirmation message to be sure that user is really wants to delete this address + * + * @param {String} url - ajax url + */ + deleteAddress: function (url) { + var that = this; + + uiConfirm({ + content: this.deleteConfirmationMessage, + actions: { + /** @inheritdoc */ + confirm: function () { + that._delete(url); + } + } + }); + }, + + /** + * Perform asynchronous DELETE request to server. + * @param {String} url - ajax url + * @returns {Deferred} + */ + _delete: function (url) { + var settings = _.extend({}, this.ajaxSettings, { + url: url, + data: { + 'form_key': window.FORM_KEY + } + }), + that = this; + + $('body').trigger('processStart'); + + return $.ajax(settings) + .done(function (response) { + if (response.error) { + uiAlert({ + content: response.message + }); + } else { + that.trigger('deleteAddressAction', that.source.get('data.entity_id')); + } + }) + .fail(function () { + uiAlert({ + content: $t('Sorry, there has been an error processing your request. Please try again later.') + }); + }) + .always(function () { + $('body').trigger('processStop'); + }); + + } + }); +}); diff --git a/app/code/Magento/Customer/view/adminhtml/web/js/form/components/insert-form.js b/app/code/Magento/Customer/view/adminhtml/web/js/form/components/insert-form.js new file mode 100644 index 000000000000..a1ffbde62d4d --- /dev/null +++ b/app/code/Magento/Customer/view/adminhtml/web/js/form/components/insert-form.js @@ -0,0 +1,80 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Ui/js/form/components/insert-form' +], function (Insert) { + 'use strict'; + + return Insert.extend({ + defaults: { + listens: { + responseData: 'onResponse' + }, + modules: { + addressListing: '${ $.addressListingProvider }', + addressModal: '${ $.addressModalProvider }' + } + }, + + /** + * Close modal, reload customer address listing and save customer address + * + * @param {Object} responseData + */ + onResponse: function (responseData) { + var data; + + if (!responseData.error) { + this.addressModal().closeModal(); + this.addressListing().reload({ + refresh: true + }); + data = this.externalSource().get('data'); + this.saveAddress(responseData, data); + } + }, + + /** + * Save customer address to customer form data source + * + * @param {Object} responseData + * @param {Object} data - customer address + */ + saveAddress: function (responseData, data) { + data['entity_id'] = responseData.data['entity_id']; + + if (parseFloat(data['default_billing'])) { + this.source.set('data.default_billing_address', data); + } else if ( + parseFloat(this.source.get('data.default_billing_address')['entity_id']) === data['entity_id'] + ) { + this.source.set('data.default_billing_address', []); + } + + if (parseFloat(data['default_shipping'])) { + this.source.set('data.default_shipping_address', data); + } else if ( + parseFloat(this.source.get('data.default_shipping_address')['entity_id']) === data['entity_id'] + ) { + this.source.set('data.default_shipping_address', []); + } + }, + + /** + * Event method that closes "Edit customer address" modal and refreshes grid after customer address + * was removed through "Delete" button on the "Edit customer address" modal + * + * @param {String} id - customer address ID to delete + */ + onAddressDelete: function (id) { + this.addressModal().closeModal(); + this.addressListing().reload({ + refresh: true + }); + this.addressListing()._delete([parseFloat(id)]); + } + }); +}); diff --git a/app/code/Magento/Customer/view/adminhtml/web/js/form/components/insert-listing.js b/app/code/Magento/Customer/view/adminhtml/web/js/form/components/insert-listing.js new file mode 100644 index 000000000000..912d4b32130e --- /dev/null +++ b/app/code/Magento/Customer/view/adminhtml/web/js/form/components/insert-listing.js @@ -0,0 +1,90 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Ui/js/form/components/insert-listing', + 'underscore' +], function (Insert, _) { + 'use strict'; + + return Insert.extend({ + + /** + * On action call + * + * @param {Object} data - customer address and actions + */ + onAction: function (data) { + this[data.action + 'Action'].call(this, data.data); + }, + + /** + * On mass action call + * + * @param {Object} data - customer address + */ + onMassAction: function (data) { + this[data.action + 'Massaction'].call(this, data.data); + }, + + /** + * Set default billing address + * + * @param {Object} data - customer address + */ + setDefaultBillingAction: function (data) { + this.source.set('data.default_billing_address', data); + }, + + /** + * Set default shipping address + * + * @param {Object} data - customer address + */ + setDefaultShippingAction: function (data) { + this.source.set('data.default_shipping_address', data); + }, + + /** + * Delete customer address + * + * @param {Object} data - customer address + */ + deleteAction: function (data) { + this._delete([parseFloat(data[data['id_field_name']])]); + }, + + /** + * Mass action delete + * + * @param {Object} data - customer address + */ + deleteMassaction: function (data) { + var ids = _.map(data, function (val) { + return parseFloat(val); + }); + + this._delete(ids); + }, + + /** + * Delete customer address by ids + * + * @param {Array} ids + */ + _delete: function (ids) { + var defaultShippingId = parseFloat(this.source.get('data.default_shipping_address.entity_id')), + defaultBillingId = parseFloat(this.source.get('data.default_billing_address.entity_id')); + + if (ids.indexOf(defaultShippingId) !== -1) { + this.source.set('data.default_shipping_address', []); + } + + if (ids.indexOf(defaultBillingId) !== -1) { + this.source.set('data.default_billing_address', []); + } + } + }); +}); diff --git a/app/code/Magento/Customer/view/adminhtml/web/js/form/element/country.js b/app/code/Magento/Customer/view/adminhtml/web/js/form/element/country.js new file mode 100644 index 000000000000..1ed30ae388b4 --- /dev/null +++ b/app/code/Magento/Customer/view/adminhtml/web/js/form/element/country.js @@ -0,0 +1,29 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Ui/js/form/element/country' +], function (Country) { + 'use strict'; + + return Country.extend({ + defaults: { + countryScope: 'data.country' + }, + + /** + * Set country to customer address form + * + * @param {String} value - country + */ + setDifferedFromDefault: function (value) { + this._super(); + + if (value) { + this.source.set(this.countryScope, this.indexedOptions[value].label); + } + } + }); +}); diff --git a/app/code/Magento/Customer/view/adminhtml/web/js/form/element/region.js b/app/code/Magento/Customer/view/adminhtml/web/js/form/element/region.js new file mode 100644 index 000000000000..755a8e6df3db --- /dev/null +++ b/app/code/Magento/Customer/view/adminhtml/web/js/form/element/region.js @@ -0,0 +1,29 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Ui/js/form/element/region' +], function (Region) { + 'use strict'; + + return Region.extend({ + defaults: { + regionScope: 'data.region' + }, + + /** + * Set region to customer address form + * + * @param {String} value - region + */ + setDifferedFromDefault: function (value) { + this._super(); + + if (parseFloat(value)) { + this.source.set(this.regionScope, this.indexedOptions[value].label); + } + } + }); +}); diff --git a/app/code/Magento/Customer/view/adminhtml/web/js/grid/columns/actions.js b/app/code/Magento/Customer/view/adminhtml/web/js/grid/columns/actions.js new file mode 100644 index 000000000000..66ef89c9413c --- /dev/null +++ b/app/code/Magento/Customer/view/adminhtml/web/js/grid/columns/actions.js @@ -0,0 +1,103 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Ui/js/grid/columns/actions', + 'Magento_Ui/js/modal/alert', + 'underscore', + 'jquery', + 'mage/translate' +], function (Actions, uiAlert, _, $, $t) { + 'use strict'; + + return Actions.extend({ + defaults: { + ajaxSettings: { + method: 'POST', + dataType: 'json' + }, + listens: { + action: 'onAction' + } + }, + + /** + * Reload customer address listing data source after customer address delete action + * + * @param {Object} data + */ + onAction: function (data) { + if (data.action === 'delete') { + this.source().reload({ + refresh: true + }); + } + }, + + /** + * Default action callback. Redirects to + * the specified in action's data url. + * + * @param {String} actionIndex - Action's identifier. + * @param {(Number|String)} recordId - Id of the record associated + * with a specified action. + * @param {Object} action - Action's data. + */ + defaultCallback: function (actionIndex, recordId, action) { + if (action.isAjax) { + this.request(action.href).done(function (response) { + var data; + + if (!response.error) { + data = _.findWhere(this.rows, { + _rowIndex: action.rowIndex + }); + + this.trigger('action', { + action: actionIndex, + data: data + }); + } + }.bind(this)); + + } else { + this._super(); + } + }, + + /** + * Send customer address listing ajax request + * + * @param {String} href + */ + request: function (href) { + var settings = _.extend({}, this.ajaxSettings, { + url: href, + data: { + 'form_key': window.FORM_KEY + } + }); + + $('body').trigger('processStart'); + + return $.ajax(settings) + .done(function (response) { + if (response.error) { + uiAlert({ + content: response.message + }); + } + }) + .fail(function () { + uiAlert({ + content: $t('Sorry, there has been an error processing your request. Please try again later.') + }); + }) + .always(function () { + $('body').trigger('processStop'); + }); + } + }); +}); diff --git a/app/code/Magento/Customer/view/adminhtml/web/js/grid/filters/chips.js b/app/code/Magento/Customer/view/adminhtml/web/js/grid/filters/chips.js new file mode 100644 index 000000000000..5dbf62a209b3 --- /dev/null +++ b/app/code/Magento/Customer/view/adminhtml/web/js/grid/filters/chips.js @@ -0,0 +1,23 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Ui/js/grid/filters/chips' +], function (Chips) { + 'use strict'; + + return Chips.extend({ + + /** + * Clear previous filters while initializing element to prevent filters sharing between customers + * + * @param {Object} elem + */ + initElement: function (elem) { + this.clear(); + this._super(elem); + } + }); +}); diff --git a/app/code/Magento/Customer/view/adminhtml/web/js/grid/filters/filters.js b/app/code/Magento/Customer/view/adminhtml/web/js/grid/filters/filters.js new file mode 100644 index 000000000000..f8bf7d3d7ec4 --- /dev/null +++ b/app/code/Magento/Customer/view/adminhtml/web/js/grid/filters/filters.js @@ -0,0 +1,20 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Ui/js/grid/filters/filters' +], function (Filters) { + 'use strict'; + + return Filters.extend({ + defaults: { + chipsConfig: { + name: '${ $.name }_chips', + provider: '${ $.chipsConfig.name }', + component: 'Magento_Customer/js/grid/filters/chips' + } + } + }); +}); diff --git a/app/code/Magento/Customer/view/adminhtml/web/js/grid/massactions.js b/app/code/Magento/Customer/view/adminhtml/web/js/grid/massactions.js new file mode 100644 index 000000000000..384f48554a91 --- /dev/null +++ b/app/code/Magento/Customer/view/adminhtml/web/js/grid/massactions.js @@ -0,0 +1,106 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Ui/js/grid/massactions', + 'Magento_Ui/js/modal/alert', + 'underscore', + 'jquery', + 'mage/translate' +], function (Massactions, uiAlert, _, $, $t) { + 'use strict'; + + return Massactions.extend({ + defaults: { + ajaxSettings: { + method: 'POST', + dataType: 'json' + }, + listens: { + massaction: 'onAction' + } + }, + + /** + * Reload customer addresses listing + * + * @param {Object} data + */ + onAction: function (data) { + if (data.action === 'delete') { + this.source.reload({ + refresh: true + }); + } + }, + + /** + * Default action callback. Send selections data + * via POST request. + * + * @param {Object} action - Action data. + * @param {Object} data - Selections data. + */ + defaultCallback: function (action, data) { + var itemsType, selections; + + if (action.isAjax) { + itemsType = data.excludeMode ? 'excluded' : 'selected'; + selections = {}; + + selections[itemsType] = data[itemsType]; + + if (!selections[itemsType].length) { + selections[itemsType] = false; + } + + _.extend(selections, data.params || {}); + + this.request(action.url, selections).done(function (response) { + if (!response.error) { + this.trigger('massaction', { + action: action.type, + data: selections + }); + } + }.bind(this)); + } else { + this._super(); + } + }, + + /** + * Send customer address listing mass action ajax request + * + * @param {String} href + * @param {Object} data + */ + request: function (href, data) { + var settings = _.extend({}, this.ajaxSettings, { + url: href, + data: data + }); + + $('body').trigger('processStart'); + + return $.ajax(settings) + .done(function (response) { + if (response.error) { + uiAlert({ + content: response.message + }); + } + }) + .fail(function () { + uiAlert({ + content: $t('Sorry, there has been an error processing your request. Please try again later.') + }); + }) + .always(function () { + $('body').trigger('processStop'); + }); + } + }); +}); diff --git a/app/code/Magento/Customer/view/adminhtml/web/template/default-address-wrapper.html b/app/code/Magento/Customer/view/adminhtml/web/template/default-address-wrapper.html new file mode 100644 index 000000000000..2af366b03342 --- /dev/null +++ b/app/code/Magento/Customer/view/adminhtml/web/template/default-address-wrapper.html @@ -0,0 +1,7 @@ + +
diff --git a/app/code/Magento/Customer/view/adminhtml/web/template/default-address.html b/app/code/Magento/Customer/view/adminhtml/web/template/default-address.html new file mode 100644 index 000000000000..8d68b93445b8 --- /dev/null +++ b/app/code/Magento/Customer/view/adminhtml/web/template/default-address.html @@ -0,0 +1,47 @@ +
+
+
+
+ + + + +
+
+ + + + + +
+ + +
+
+ + + +
+
+ + + + +
+
+ + +
T: +
+ +
F: +
+ +
VAT: +
+
+ +
+
+
diff --git a/app/code/Magento/Customer/view/base/ui_component/customer_form.xml b/app/code/Magento/Customer/view/base/ui_component/customer_form.xml index 4de6644b948f..e87997dbdb5e 100644 --- a/app/code/Magento/Customer/view/base/ui_component/customer_form.xml +++ b/app/code/Magento/Customer/view/base/ui_component/customer_form.xml @@ -43,7 +43,7 @@ - + id entity_id @@ -74,6 +74,17 @@ false + + + + customer + + + + text + false + + @@ -303,265 +314,196 @@ -
- - - true - Are you sure you want to delete this item? - - +
+ false + fieldset + + true + - - - - address - - - - number - false - - - - - - address - - - - text - true - - - - - - address - - - - - true - - text - - ${ $.provider }:data.customer.firstname - - - - - - - address - - - - text - true - - - - - - address + + + + + billing-address + Default Billing Address + customer-default-billing-address-content + The customer does not have default billing address + + true + + + + + + ${ $.provider}:data.default_billing_address + + + + + + + + + + shipping-address + Default Shipping Address + customer-default-shipping-address-content + The customer does not have default shipping address + + true + + + + + + ${ $.provider}:data.default_shipping_address + + + + + + + - - 0 - - text + + + - - - - - address - - - - - true - - text - - - - - - address - - - - - true - - text + + + + ns = customer_address_listing, index = customer_address_listing + ${ $.parentName } + + + + ajax + + customer_address_edit + 1 + + false + ${ $.parentName } + ${ $.ns }.customer_address_form_data_source + customer_address_form + + ${ $.externalProvider }:data.parent_id + + + ${ $.provider}:data.customer_id + ${ $.ns }.${ $.ns }:deleteAddressAction + + + + + + + + false + true + + customer_address_listing.customer_address_listing_data_source + customer_address_listing.customer_address_listing.customer_address_listing_columns.ids + true + customer_address_listing + customer_address_listing + + ${ $.externalProvider }:params.parent_id + - ${ $.provider }:data.customer.website_id + ${ $.provider }:data.customer.entity_id + ns = ${ $.ns }, index = actions:action + ns = ${ $.ns }, index = listing_massaction:massaction - - - - - address - - - - text - false - - - - - - address - - - - - true - - text - - - - - - - - - address - - - - - true - - text - - - - - - address - - - - - true - - text - - - - - - address - - - - - 0 - - text - - - - - - address - - - - text - - - - - - default_billing - value - - customer - default_billing - entity_id - - - - - ui/form/element/checkbox - boolean - - - - - Default Billing Address - - - - - - - - default_shipping - value - - customer - default_shipping - entity_id - - - - - ui/form/element/checkbox - boolean - - - - - Default Shipping Address - - - - +
diff --git a/app/code/Magento/Customer/view/frontend/email/password_reset_confirmation.html b/app/code/Magento/Customer/view/frontend/email/password_reset_confirmation.html index 6c17762a8822..59e7f16adfd5 100644 --- a/app/code/Magento/Customer/view/frontend/email/password_reset_confirmation.html +++ b/app/code/Magento/Customer/view/frontend/email/password_reset_confirmation.html @@ -21,7 +21,7 @@
- {{trans "Set a New Password"}} + {{trans "Set a New Password"}}
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 fd5ecbfa7f27..0c5af453f237 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 @@ */ --> + + 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 d49dae6dee58..3518df736c4a 100644 --- a/app/code/Magento/Customer/view/frontend/layout/customer_account_login.xml +++ b/app/code/Magento/Customer/view/frontend/layout/customer_account_login.xml @@ -6,6 +6,9 @@ */ --> + + Customer Login + diff --git a/app/code/Magento/Customer/view/frontend/layout/customer_address_form.xml b/app/code/Magento/Customer/view/frontend/layout/customer_address_form.xml index f053805409fe..f5ee2b347a5b 100644 --- a/app/code/Magento/Customer/view/frontend/layout/customer_address_form.xml +++ b/app/code/Magento/Customer/view/frontend/layout/customer_address_form.xml @@ -20,6 +20,7 @@ Magento\Customer\Block\DataProviders\AddressAttributeData + Magento\Customer\Block\DataProviders\PostCodesPatternsAttributeData diff --git a/app/code/Magento/Customer/view/frontend/layout/customer_address_index.xml b/app/code/Magento/Customer/view/frontend/layout/customer_address_index.xml index bad120e46277..2c5e5b98e5f7 100644 --- a/app/code/Magento/Customer/view/frontend/layout/customer_address_index.xml +++ b/app/code/Magento/Customer/view/frontend/layout/customer_address_index.xml @@ -13,6 +13,7 @@ + diff --git a/app/code/Magento/Customer/view/frontend/requirejs-config.js b/app/code/Magento/Customer/view/frontend/requirejs-config.js index 20dd53ded11c..f1bf5c1d1b67 100644 --- a/app/code/Magento/Customer/view/frontend/requirejs-config.js +++ b/app/code/Magento/Customer/view/frontend/requirejs-config.js @@ -7,11 +7,13 @@ var config = { map: { '*': { checkoutBalance: 'Magento_Customer/js/checkout-balance', - address: 'Magento_Customer/address', - changeEmailPassword: 'Magento_Customer/change-email-password', + address: 'Magento_Customer/js/address', + changeEmailPassword: 'Magento_Customer/js/change-email-password', passwordStrengthIndicator: 'Magento_Customer/js/password-strength-indicator', zxcvbn: 'Magento_Customer/js/zxcvbn', - addressValidation: 'Magento_Customer/js/addressValidation' + addressValidation: 'Magento_Customer/js/addressValidation', + 'Magento_Customer/address': 'Magento_Customer/js/address', + 'Magento_Customer/change-email-password': 'Magento_Customer/js/change-email-password' } } }; diff --git a/app/code/Magento/Customer/view/frontend/templates/account/customer.phtml b/app/code/Magento/Customer/view/frontend/templates/account/customer.phtml index 2bdb6ac044a9..b7b8796142de 100644 --- a/app/code/Magento/Customer/view/frontend/templates/account/customer.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/account/customer.phtml @@ -16,7 +16,6 @@ data-toggle="dropdown" data-trigger-keypress-button="true" data-bind="scope: 'customer'"> -
- - - -
-
- -
- -
- diff --git a/app/code/Magento/Customer/view/frontend/templates/address/edit.phtml b/app/code/Magento/Customer/view/frontend/templates/address/edit.phtml index 086b7423befe..df3f00041083 100644 --- a/app/code/Magento/Customer/view/frontend/templates/address/edit.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/address/edit.phtml @@ -104,7 +104,7 @@
+
@@ -184,7 +187,9 @@ diff --git a/app/code/Magento/Customer/view/frontend/templates/form/register.phtml b/app/code/Magento/Customer/view/frontend/templates/form/register.phtml index bb86c9f453df..f11177ae19f7 100644 --- a/app/code/Magento/Customer/view/frontend/templates/form/register.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/form/register.phtml @@ -94,7 +94,7 @@
- diff --git a/app/code/Magento/Customer/view/frontend/templates/form/resetforgottenpassword.phtml b/app/code/Magento/Customer/view/frontend/templates/form/resetforgottenpassword.phtml index 15e570da04be..e79cea80ac83 100644 --- a/app/code/Magento/Customer/view/frontend/templates/form/resetforgottenpassword.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/form/resetforgottenpassword.phtml @@ -8,7 +8,7 @@ /** @var \Magento\Customer\Block\Account\Resetpassword $block */ ?> -
isAutocompleteDisabled()) :?> autocomplete="off" id="form-validate" diff --git a/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml b/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml index 0a37896b810c..31510a65ef74 100644 --- a/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml @@ -29,7 +29,7 @@ $fieldCssClass = 'field date field-' . $block->getHtmlId(); $fieldCssClass .= $block->isRequired() ? ' required' : ''; ?>
- +
getFieldHtml() ?> getAdditionalDescription()) : ?> diff --git a/app/code/Magento/Customer/view/frontend/templates/widget/gender.phtml b/app/code/Magento/Customer/view/frontend/templates/widget/gender.phtml index d60f0968ad4f..8b45618a891e 100644 --- a/app/code/Magento/Customer/view/frontend/templates/widget/gender.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/widget/gender.phtml @@ -9,9 +9,9 @@ /** @var \Magento\Customer\Block\Widget\Gender $block */ ?>
- +
- isRequired()):?> class="validate-select" data-validate="{required:true}"> getGenderOptions(); ?> getGender();?> diff --git a/app/code/Magento/Customer/view/frontend/templates/widget/taxvat.phtml b/app/code/Magento/Customer/view/frontend/templates/widget/taxvat.phtml index 4b3681d4d8fd..bb60845a64e6 100644 --- a/app/code/Magento/Customer/view/frontend/templates/widget/taxvat.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/widget/taxvat.phtml @@ -9,8 +9,8 @@ /** @var \Magento\Customer\Block\Widget\Taxvat $block */ ?>
- +
- isRequired()) echo ' data-validate="{required:true}"' ?>> + isRequired()) echo ' data-validate="{required:true}"' ?>>
diff --git a/app/code/Magento/Customer/view/frontend/templates/widget/telephone.phtml b/app/code/Magento/Customer/view/frontend/templates/widget/telephone.phtml index 1b61dc45573b..6367bf10bbad 100644 --- a/app/code/Magento/Customer/view/frontend/templates/widget/telephone.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/widget/telephone.phtml @@ -19,7 +19,7 @@ escapeHtmlAttr( $this->helper('Magento\Customer\Helper\Address') - ->getAttributeValidationClass('fax') + ->getAttributeValidationClass('telephone') ); ?> 0) { _.each(dataProvider.getFromStorage(storage.keys()), function (sectionData, sectionName) { buffer.notify(sectionName, sectionData); @@ -233,7 +239,7 @@ define([ if (!_.isEmpty(privateContent)) { countryData = this.get('directory-data'); - if (_.isEmpty(countryData())) { + if (_.isEmpty(countryData()) && !isLoading) { customerData.reload(['directory-data'], false); } } @@ -323,11 +329,11 @@ define([ /** * @param {Array} sectionNames - * @param {Number} updateSectionId + * @param {Boolean} forceNewSectionTimestamp * @return {*} */ - reload: function (sectionNames, updateSectionId) { - return dataProvider.getFromServer(sectionNames, updateSectionId).done(function (sections) { + reload: function (sectionNames, forceNewSectionTimestamp) { + return dataProvider.getFromServer(sectionNames, forceNewSectionTimestamp).done(function (sections) { $(document).trigger('customer-data-reload', [sectionNames]); buffer.update(sections); }); diff --git a/app/code/Magento/Customer/view/frontend/web/js/password-strength-indicator.js b/app/code/Magento/Customer/view/frontend/web/js/password-strength-indicator.js index 89d9b320c049..9742e37f2df5 100644 --- a/app/code/Magento/Customer/view/frontend/web/js/password-strength-indicator.js +++ b/app/code/Magento/Customer/view/frontend/web/js/password-strength-indicator.js @@ -83,7 +83,7 @@ define([ } else { isValid = $.validator.validateSingleElement(this.options.cache.input); zxcvbnScore = zxcvbn(password).score; - displayScore = isValid ? zxcvbnScore : 1; + displayScore = isValid && zxcvbnScore > 0 ? zxcvbnScore : 1; } } diff --git a/app/code/Magento/CustomerAnalytics/README.md b/app/code/Magento/CustomerAnalytics/README.md index 8c64ce97629d..9658a8e7d90e 100644 --- a/app/code/Magento/CustomerAnalytics/README.md +++ b/app/code/Magento/CustomerAnalytics/README.md @@ -1,3 +1,3 @@ # Magento_CustomerAnalytics module -The Magento_CustomerAnalytics module configures data definitions for a data collection related to the Customer module entities to be used in [Advanced Reporting](http://devdocs.magento.com/guides/v2.2/advanced-reporting/modules.html). +The Magento_CustomerAnalytics module configures data definitions for a data collection related to the Customer module entities to be used in [Advanced Reporting](https://devdocs.magento.com/guides/v2.2/advanced-reporting/modules.html). diff --git a/app/code/Magento/CustomerAnalytics/composer.json b/app/code/Magento/CustomerAnalytics/composer.json index 7dec4279ee28..3840c534b196 100644 --- a/app/code/Magento/CustomerAnalytics/composer.json +++ b/app/code/Magento/CustomerAnalytics/composer.json @@ -4,7 +4,8 @@ "require": { "php": "~7.1.3||~7.2.0", "magento/framework": "*", - "magento/module-customer": "*" + "magento/module-customer": "*", + "magento/module-analytics": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/Address/CreateCustomerAddress.php b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/CreateCustomerAddress.php new file mode 100644 index 000000000000..388b6dc2ea94 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/CreateCustomerAddress.php @@ -0,0 +1,115 @@ +getAllowedAddressAttributes = $getAllowedAddressAttributes; + $this->addressFactory = $addressFactory; + $this->addressRepository = $addressRepository; + $this->dataObjectHelper = $dataObjectHelper; + } + + /** + * Create customer address + * + * @param int $customerId + * @param array $data + * @return AddressInterface + * @throws GraphQlInputException + */ + public function execute(int $customerId, array $data): AddressInterface + { + $this->validateData($data); + + /** @var AddressInterface $address */ + $address = $this->addressFactory->create(); + $this->dataObjectHelper->populateWithArray($address, $data, AddressInterface::class); + + if (isset($data['region']['region_id'])) { + $address->setRegionId($address->getRegion()->getRegionId()); + } + $address->setCustomerId($customerId); + + try { + $this->addressRepository->save($address); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage()), $e); + } + return $address; + } + + /** + * Validate customer address create data + * + * @param array $addressData + * @return void + * @throws GraphQlInputException + */ + public function validateData(array $addressData): void + { + $attributes = $this->getAllowedAddressAttributes->execute(); + $errorInput = []; + + foreach ($attributes as $attributeName => $attributeInfo) { + if ($attributeInfo->getIsRequired() + && (!isset($addressData[$attributeName]) || empty($addressData[$attributeName])) + ) { + $errorInput[] = $attributeName; + } + } + + if ($errorInput) { + throw new GraphQlInputException( + __('Required parameters are missing: %1', [implode(', ', $errorInput)]) + ); + } + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/Address/DeleteCustomerAddress.php b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/DeleteCustomerAddress.php new file mode 100644 index 000000000000..586fbebde703 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/DeleteCustomerAddress.php @@ -0,0 +1,60 @@ +addressRepository = $addressRepository; + } + + /** + * Delete customer address + * + * @param AddressInterface $address + * @return void + * @throws GraphQlInputException + */ + public function execute(AddressInterface $address): void + { + if ($address->isDefaultBilling()) { + throw new GraphQlInputException( + __('Customer Address %1 is set as default billing address and can not be deleted', [$address->getId()]) + ); + } + if ($address->isDefaultShipping()) { + throw new GraphQlInputException( + __('Customer Address %1 is set as default shipping address and can not be deleted', [$address->getId()]) + ); + } + + try { + $this->addressRepository->delete($address); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage()), $e); + } + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/Address/ExtractCustomerAddressData.php b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/ExtractCustomerAddressData.php new file mode 100644 index 000000000000..a4649bccc02e --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/ExtractCustomerAddressData.php @@ -0,0 +1,130 @@ +serviceOutputProcessor = $serviceOutputProcessor; + $this->jsonSerializer = $jsonSerializer; + $this->customerResourceModel = $customerResourceModel; + $this->customerFactory = $customerFactory; + } + + /** + * Curate shipping and billing default options + * + * @param array $address + * @param AddressInterface $addressObject + * @return array + */ + private function curateAddressDefaultValues(array $address, AddressInterface $addressObject) : array + { + $customerModel = $this->customerFactory->create(); + $this->customerResourceModel->load($customerModel, $addressObject->getCustomerId()); + $address[CustomerInterface::DEFAULT_BILLING] = + ($customerModel->getDefaultBillingAddress() + && $addressObject->getId() == $customerModel->getDefaultBillingAddress()->getId()); + $address[CustomerInterface::DEFAULT_SHIPPING] = + ($customerModel->getDefaultShippingAddress() + && $addressObject->getId() == $customerModel->getDefaultShippingAddress()->getId()); + return $address; + } + + /** + * Transform single customer address data from object to in array format + * + * @param AddressInterface $address + * @return array + */ + public function execute(AddressInterface $address): array + { + $addressData = $this->serviceOutputProcessor->process( + $address, + AddressRepositoryInterface::class, + 'getById' + ); + $addressData = $this->curateAddressDefaultValues($addressData, $address); + + if (isset($addressData[CustomAttributesDataInterface::EXTENSION_ATTRIBUTES_KEY])) { + $addressData = array_merge( + $addressData, + $addressData[CustomAttributesDataInterface::EXTENSION_ATTRIBUTES_KEY] + ); + } + $customAttributes = []; + if (isset($addressData[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES])) { + foreach ($addressData[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES] as $attribute) { + $isArray = false; + if (is_array($attribute['value'])) { + $isArray = true; + foreach ($attribute['value'] as $attributeValue) { + if (is_array($attributeValue)) { + $customAttributes[$attribute['attribute_code']] = $this->jsonSerializer->serialize( + $attribute['value'] + ); + continue; + } + $customAttributes[$attribute['attribute_code']] = implode(',', $attribute['value']); + continue; + } + } + if ($isArray) { + continue; + } + $customAttributes[$attribute['attribute_code']] = $attribute['value']; + } + } + $addressData = array_merge($addressData, $customAttributes); + + return $addressData; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/Address/GetAllowedAddressAttributes.php b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/GetAllowedAddressAttributes.php new file mode 100644 index 000000000000..87be76073238 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/GetAllowedAddressAttributes.php @@ -0,0 +1,49 @@ +eavConfig = $eavConfig; + } + + /** + * Get allowed address attributes + * + * @return AbstractAttribute[] + */ + public function execute(): array + { + $attributes = $this->eavConfig->getEntityAttributes( + AddressMetadataManagementInterface::ENTITY_TYPE_ADDRESS + ); + foreach ($attributes as $attributeCode => $attribute) { + if (false === $attribute->getIsVisibleOnFront()) { + unset($attributes[$attributeCode]); + } + } + return $attributes; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/Address/GetCustomerAddress.php b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/GetCustomerAddress.php new file mode 100644 index 000000000000..7258f2e726fd --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/GetCustomerAddress.php @@ -0,0 +1,68 @@ +addressRepository = $addressRepository; + } + + /** + * Get customer address + * + * @param int $addressId + * @param int $customerId + * @return AddressInterface + * @throws GraphQlInputException + * @throws GraphQlNoSuchEntityException + * @throws GraphQlAuthorizationException + */ + public function execute(int $addressId, int $customerId): AddressInterface + { + try { + $customerAddress = $this->addressRepository->getById($addressId); + } catch (NoSuchEntityException $e) { + throw new GraphQlNoSuchEntityException( + __('Could not find a address with ID "%address_id"', ['address_id' => $addressId]) + ); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage()), $e); + } + + if ((int)$customerAddress->getCustomerId() !== $customerId) { + throw new GraphQlAuthorizationException( + __( + 'Current customer does not have permission to address with ID "%address_id"', + ['address_id' => $addressId] + ) + ); + } + return $customerAddress; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/Address/UpdateCustomerAddress.php b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/UpdateCustomerAddress.php new file mode 100644 index 000000000000..65745a20bc8e --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/UpdateCustomerAddress.php @@ -0,0 +1,111 @@ +getAllowedAddressAttributes = $getAllowedAddressAttributes; + $this->addressRepository = $addressRepository; + $this->dataObjectHelper = $dataObjectHelper; + $this->restrictedKeys = $restrictedKeys; + } + + /** + * Update customer address + * + * @param AddressInterface $address + * @param array $data + * @return void + * @throws GraphQlInputException + */ + public function execute(AddressInterface $address, array $data): void + { + $this->validateData($data); + + $filteredData = array_diff_key($data, array_flip($this->restrictedKeys)); + $this->dataObjectHelper->populateWithArray($address, $filteredData, AddressInterface::class); + + if (isset($data['region']['region_id'])) { + $address->setRegionId($address->getRegion()->getRegionId()); + } + + try { + $this->addressRepository->save($address); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage()), $e); + } + } + + /** + * Validate customer address update data + * + * @param array $addressData + * @return void + * @throws GraphQlInputException + */ + public function validateData(array $addressData): void + { + $attributes = $this->getAllowedAddressAttributes->execute(); + $errorInput = []; + + foreach ($attributes as $attributeName => $attributeInfo) { + if ($attributeInfo->getIsRequired() + && (isset($addressData[$attributeName]) && empty($addressData[$attributeName])) + ) { + $errorInput[] = $attributeName; + } + } + + if ($errorInput) { + throw new GraphQlInputException( + __('Required parameters are missing: %1', [implode(', ', $errorInput)]) + ); + } + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/ChangeSubscriptionStatus.php b/app/code/Magento/CustomerGraphQl/Model/Customer/ChangeSubscriptionStatus.php new file mode 100644 index 000000000000..1acb418e7bba --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/ChangeSubscriptionStatus.php @@ -0,0 +1,48 @@ +subscriberFactory = $subscriberFactory; + } + + /** + * Change subscription status. Subscribe OR unsubscribe if required + * + * @param int $customerId + * @param bool $subscriptionStatus + * @return void + */ + public function execute(int $customerId, bool $subscriptionStatus): void + { + $subscriber = $this->subscriberFactory->create()->loadByCustomerId($customerId); + + if ($subscriptionStatus === true && !$subscriber->isSubscribed()) { + $this->subscriberFactory->create()->subscribeCustomerById($customerId); + } elseif ($subscriptionStatus === false && $subscriber->isSubscribed()) { + $this->subscriberFactory->create()->unsubscribeCustomerById($customerId); + } + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/CheckCustomerPassword.php b/app/code/Magento/CustomerGraphQl/Model/Customer/CheckCustomerPassword.php new file mode 100644 index 000000000000..3cc831e1ca40 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/CheckCustomerPassword.php @@ -0,0 +1,61 @@ +authentication = $authentication; + } + + /** + * Check customer password + * + * @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(__($e->getMessage()), $e); + } catch (UserLockedException $e) { + throw new GraphQlAuthenticationException(__($e->getMessage()), $e); + } catch (NoSuchEntityException $e) { + throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage()), $e); + } + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/CreateCustomerAccount.php b/app/code/Magento/CustomerGraphQl/Model/Customer/CreateCustomerAccount.php new file mode 100644 index 000000000000..b7b66df04246 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/CreateCustomerAccount.php @@ -0,0 +1,112 @@ +dataObjectHelper = $dataObjectHelper; + $this->customerFactory = $customerFactory; + $this->accountManagement = $accountManagement; + $this->storeManager = $storeManager; + $this->changeSubscriptionStatus = $changeSubscriptionStatus; + } + + /** + * Creates new customer account + * + * @param array $data + * @return CustomerInterface + * @throws GraphQlInputException + */ + public function execute(array $data): CustomerInterface + { + try { + $customer = $this->createAccount($data); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage())); + } + + if (isset($data['is_subscribed'])) { + $this->changeSubscriptionStatus->execute((int)$customer->getId(), (bool)$data['is_subscribed']); + } + return $customer; + } + + /** + * Create account + * + * @param array $data + * @return CustomerInterface + * @throws LocalizedException + */ + private function createAccount(array $data): CustomerInterface + { + $customerDataObject = $this->customerFactory->create(); + $this->dataObjectHelper->populateWithArray( + $customerDataObject, + $data, + CustomerInterface::class + ); + $store = $this->storeManager->getStore(); + $customerDataObject->setWebsiteId($store->getWebsiteId()); + $customerDataObject->setStoreId($store->getId()); + + $password = array_key_exists('password', $data) ? $data['password'] : null; + return $this->accountManagement->createAccount($customerDataObject, $password); + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/ExtractCustomerData.php b/app/code/Magento/CustomerGraphQl/Model/Customer/ExtractCustomerData.php new file mode 100644 index 000000000000..de37482aca05 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/ExtractCustomerData.php @@ -0,0 +1,108 @@ +serviceOutputProcessor = $serviceOutputProcessor; + $this->serializer = $serializer; + } + + /** + * Curate default shipping and default billing keys + * + * @param array $arrayAddress + * @return array + */ + private function curateAddressData(array $arrayAddress): array + { + foreach ($arrayAddress as $key => $address) { + if (!isset($address['default_shipping'])) { + $arrayAddress[$key]['default_shipping'] = false; + } + if (!isset($address['default_billing'])) { + $arrayAddress[$key]['default_billing'] = false; + } + } + return $arrayAddress; + } + + /** + * Transform single customer data from object to in array format + * + * @param CustomerInterface $customer + * @return array + * @throws LocalizedException + */ + public function execute(CustomerInterface $customer): array + { + $customerData = $this->serviceOutputProcessor->process( + $customer, + CustomerRepositoryInterface::class, + 'get' + ); + $customerData['addresses'] = $this->curateAddressData($customerData['addresses']); + if (isset($customerData['extension_attributes'])) { + $customerData = array_merge($customerData, $customerData['extension_attributes']); + } + $customAttributes = []; + if (isset($customerData['custom_attributes'])) { + foreach ($customerData['custom_attributes'] as $attribute) { + $isArray = false; + if (is_array($attribute['value'])) { + $isArray = true; + foreach ($attribute['value'] as $attributeValue) { + if (is_array($attributeValue)) { + $customAttributes[$attribute['attribute_code']] = $this->serializer->serialize( + $attribute['value'] + ); + continue; + } + $customAttributes[$attribute['attribute_code']] = implode(',', $attribute['value']); + continue; + } + } + if ($isArray) { + continue; + } + $customAttributes[$attribute['attribute_code']] = $attribute['value']; + } + } + $customerData = array_merge($customerData, $customAttributes); + + $customerData['model'] = $customer; + return $customerData; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/GetCustomer.php b/app/code/Magento/CustomerGraphQl/Model/Customer/GetCustomer.php new file mode 100644 index 000000000000..8bd5c9157493 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/GetCustomer.php @@ -0,0 +1,118 @@ +authentication = $authentication; + $this->customerRepository = $customerRepository; + $this->accountManagement = $accountManagement; + } + + /** + * Get customer + * + * @param ContextInterface $context + * @return CustomerInterface + * @throws GraphQlAuthenticationException + * @throws GraphQlAuthorizationException + * @throws GraphQlInputException + * @throws GraphQlNoSuchEntityException + */ + public function execute(ContextInterface $context): CustomerInterface + { + $currentUserId = $context->getUserId(); + $currentUserType = $context->getUserType(); + + if (true === $this->isUserGuest($currentUserId, $currentUserType)) { + throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.')); + } + + try { + $customer = $this->customerRepository->getById($currentUserId); + } catch (NoSuchEntityException $e) { + throw new GraphQlNoSuchEntityException( + __('Customer with id "%customer_id" does not exist.', ['customer_id' => $currentUserId]), + $e + ); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage())); + } + + if (true === $this->authentication->isLocked($currentUserId)) { + throw new GraphQlAuthenticationException(__('The account is locked.')); + } + + try { + $confirmationStatus = $this->accountManagement->getConfirmationStatus($currentUserId); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage())); + } + + if ($confirmationStatus === AccountManagementInterface::ACCOUNT_CONFIRMATION_REQUIRED) { + throw new GraphQlAuthenticationException(__("This account isn't confirmed. Verify and try again.")); + } + return $customer; + } + + /** + * Checking if current customer is guest + * + * @param int|null $customerId + * @param int|null $customerType + * @return bool + */ + private function isUserGuest(?int $customerId, ?int $customerType): bool + { + if (null === $customerId || null === $customerType) { + return true; + } + return 0 === (int)$customerId || (int)$customerType === UserContextInterface::USER_TYPE_GUEST; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/SaveCustomer.php b/app/code/Magento/CustomerGraphQl/Model/Customer/SaveCustomer.php new file mode 100644 index 000000000000..1605c63b62f4 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/SaveCustomer.php @@ -0,0 +1,58 @@ +customerRepository = $customerRepository; + } + + /** + * Save customer + * + * @param CustomerInterface $customer + * @throws GraphQlAlreadyExistsException + * @throws GraphQlAuthenticationException + * @throws GraphQlInputException + */ + public function execute(CustomerInterface $customer): void + { + try { + $this->customerRepository->save($customer); + } catch (AlreadyExistsException $e) { + throw new GraphQlAlreadyExistsException( + __('A customer with the same email address already exists in an associated website.'), + $e + ); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage()), $e); + } + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/UpdateCustomerAccount.php b/app/code/Magento/CustomerGraphQl/Model/Customer/UpdateCustomerAccount.php new file mode 100644 index 000000000000..8601d586b3c9 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/UpdateCustomerAccount.php @@ -0,0 +1,108 @@ +saveCustomer = $saveCustomer; + $this->storeManager = $storeManager; + $this->checkCustomerPassword = $checkCustomerPassword; + $this->dataObjectHelper = $dataObjectHelper; + $this->restrictedKeys = $restrictedKeys; + $this->changeSubscriptionStatus = $changeSubscriptionStatus; + } + + /** + * Update customer account data + * + * @param CustomerInterface $customer + * @param array $data + * @return void + * @throws GraphQlAlreadyExistsException + * @throws GraphQlAuthenticationException + * @throws GraphQlInputException + */ + public function execute(CustomerInterface $customer, array $data): void + { + if (isset($data['email']) && $customer->getEmail() !== $data['email']) { + if (!isset($data['password']) || empty($data['password'])) { + throw new GraphQlInputException(__('Provide the current "password" to change "email".')); + } + + $this->checkCustomerPassword->execute($data['password'], (int)$customer->getId()); + $customer->setEmail($data['email']); + } + + $filteredData = array_diff_key($data, array_flip($this->restrictedKeys)); + $this->dataObjectHelper->populateWithArray($customer, $filteredData, CustomerInterface::class); + + $customer->setStoreId($this->storeManager->getStore()->getId()); + + $this->saveCustomer->execute($customer); + + if (isset($data['is_subscribed'])) { + $this->changeSubscriptionStatus->execute((int)$customer->getId(), (bool)$data['is_subscribed']); + } + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/ChangePassword.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/ChangePassword.php new file mode 100644 index 000000000000..317b7725b026 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/ChangePassword.php @@ -0,0 +1,93 @@ +getCustomer = $getCustomer; + $this->checkCustomerPassword = $checkCustomerPassword; + $this->accountManagement = $accountManagement; + $this->extractCustomerData = $extractCustomerData; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($args['currentPassword']) || '' == trim($args['currentPassword'])) { + throw new GraphQlInputException(__('Specify the "currentPassword" value.')); + } + + if (!isset($args['newPassword']) || '' == trim($args['newPassword'])) { + throw new GraphQlInputException(__('Specify the "newPassword" value.')); + } + + $customer = $this->getCustomer->execute($context); + $customerId = (int)$customer->getId(); + + $this->checkCustomerPassword->execute($args['currentPassword'], $customerId); + + try { + $this->accountManagement->changePasswordById($customerId, $args['currentPassword'], $args['newPassword']); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage()), $e); + } + return $this->extractCustomerData->execute($customer); + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/CreateCustomer.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/CreateCustomer.php new file mode 100644 index 000000000000..1ae22bcc1279 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/CreateCustomer.php @@ -0,0 +1,67 @@ +extractCustomerData = $extractCustomerData; + $this->createCustomerAccount = $createCustomerAccount; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($args['input']) || !is_array($args['input']) || empty($args['input'])) { + throw new GraphQlInputException(__('"input" value should be specified')); + } + + $customer = $this->createCustomerAccount->execute($args['input']); + + $context->setUserId((int)$customer->getId()); + $context->setUserType(UserContextInterface::USER_TYPE_CUSTOMER); + + $data = $this->extractCustomerData->execute($customer); + return ['customer' => $data]; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/CreateCustomerAddress.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/CreateCustomerAddress.php new file mode 100644 index 000000000000..fd8122de961e --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/CreateCustomerAddress.php @@ -0,0 +1,72 @@ +getCustomer = $getCustomer; + $this->createCustomerAddress = $createCustomerAddress; + $this->extractCustomerAddressData = $extractCustomerAddressData; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($args['input']) || !is_array($args['input']) || empty($args['input'])) { + throw new GraphQlInputException(__('"input" value should be specified')); + } + + $customer = $this->getCustomer->execute($context); + + $address = $this->createCustomerAddress->execute((int)$customer->getId(), $args['input']); + return $this->extractCustomerAddressData->execute($address); + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/Customer.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/Customer.php index 1cf85381d346..91048d4836c8 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Resolver/Customer.php +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/Customer.php @@ -7,16 +7,10 @@ namespace Magento\CustomerGraphQl\Model\Resolver; -use Magento\Authorization\Model\UserContextInterface; +use Magento\CustomerGraphQl\Model\Customer\GetCustomer; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\CustomerGraphQl\Model\Resolver\Customer\CustomerDataProvider; -use Magento\Framework\Exception\NoSuchEntityException; +use Magento\CustomerGraphQl\Model\Customer\ExtractCustomerData; use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; -use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; -use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; -use Magento\Framework\GraphQl\Query\Resolver\Value; -use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; /** @@ -25,29 +19,29 @@ class Customer implements ResolverInterface { /** - * @var CustomerDataProvider + * @var GetCustomer */ - private $customerResolver; + private $getCustomer; /** - * @var ValueFactory + * @var ExtractCustomerData */ - private $valueFactory; + private $extractCustomerData; /** - * @param CustomerDataProvider $customerResolver - * @param ValueFactory $valueFactory + * @param GetCustomer $getCustomer + * @param ExtractCustomerData $extractCustomerData */ public function __construct( - CustomerDataProvider $customerResolver, - ValueFactory $valueFactory + GetCustomer $getCustomer, + ExtractCustomerData $extractCustomerData ) { - $this->customerResolver = $customerResolver; - $this->valueFactory = $valueFactory; + $this->getCustomer = $getCustomer; + $this->extractCustomerData = $extractCustomerData; } /** - * {@inheritdoc} + * @inheritdoc */ public function resolve( Field $field, @@ -55,25 +49,9 @@ public function resolve( ResolveInfo $info, array $value = null, array $args = null - ) : Value { - /** @var ContextInterface $context */ - if ((!$context->getUserId()) || $context->getUserType() == UserContextInterface::USER_TYPE_GUEST) { - throw new GraphQlAuthorizationException( - __( - 'Current customer does not have access to the resource "%1"', - [\Magento\Customer\Model\Customer::ENTITY] - ) - ); - } + ) { + $customer = $this->getCustomer->execute($context); - try { - $data = $this->customerResolver->getCustomerById($context->getUserId()); - $result = function () use ($data) { - return !empty($data) ? $data : []; - }; - return $this->valueFactory->create($result); - } catch (NoSuchEntityException $exception) { - throw new GraphQlNoSuchEntityException(__('Customer id %1 does not exist.', [$context->getUserId()])); - } + return $this->extractCustomerData->execute($customer); } } diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/Customer/CustomerDataProvider.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/Customer/CustomerDataProvider.php deleted file mode 100644 index 1a942a2ab149..000000000000 --- a/app/code/Magento/CustomerGraphQl/Model/Resolver/Customer/CustomerDataProvider.php +++ /dev/null @@ -1,113 +0,0 @@ -customerRepository = $customerRepository; - $this->serviceOutputProcessor = $serviceOutputProcessor; - $this->jsonSerializer = $jsonSerializer; - } - - /** - * Get customer data by Id or empty array - * - * @param int $customerId - * @return array - * @throws NoSuchEntityException|LocalizedException - */ - public function getCustomerById(int $customerId) : array - { - try { - $customerObject = $this->customerRepository->getById($customerId); - } catch (NoSuchEntityException $e) { - // No error should be thrown, null result should be returned - return []; - } - return $this->processCustomer($customerObject); - } - - /** - * Transform single customer data from object to in array format - * - * @param CustomerInterface $customerObject - * @return array - */ - private function processCustomer(CustomerInterface $customerObject) : array - { - $customer = $this->serviceOutputProcessor->process( - $customerObject, - CustomerRepositoryInterface::class, - 'get' - ); - if (isset($customer['extension_attributes'])) { - $customer = array_merge($customer, $customer['extension_attributes']); - } - $customAttributes = []; - if (isset($customer['custom_attributes'])) { - foreach ($customer['custom_attributes'] as $attribute) { - $isArray = false; - if (is_array($attribute['value'])) { - $isArray = true; - foreach ($attribute['value'] as $attributeValue) { - if (is_array($attributeValue)) { - $customAttributes[$attribute['attribute_code']] = $this->jsonSerializer->serialize( - $attribute['value'] - ); - continue; - } - $customAttributes[$attribute['attribute_code']] = implode(',', $attribute['value']); - continue; - } - } - if ($isArray) { - continue; - } - $customAttributes[$attribute['attribute_code']] = $attribute['value']; - } - } - $customer = array_merge($customer, $customAttributes); - - return $customer; - } -} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomerAddresses.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomerAddresses.php new file mode 100644 index 000000000000..e6e3887de423 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomerAddresses.php @@ -0,0 +1,71 @@ +getCustomer = $getCustomer; + $this->extractCustomerAddressData = $extractCustomerAddressData; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + /** @var Customer $customer */ + $customer = $value['model']; + + $addressesData = []; + $addresses = $customer->getAddresses(); + + if (count($addresses)) { + foreach ($addresses as $address) { + $addressesData[] = $this->extractCustomerAddressData->execute($address); + } + } + return $addressesData; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/DeleteCustomerAddress.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/DeleteCustomerAddress.php new file mode 100644 index 000000000000..08e82d930f26 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/DeleteCustomerAddress.php @@ -0,0 +1,73 @@ +getCustomer = $getCustomer; + $this->getCustomerAddress = $getCustomerAddress; + $this->deleteCustomerAddress = $deleteCustomerAddress; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($args['id']) || empty($args['id'])) { + throw new GraphQlInputException(__('Address "id" value should be specified')); + } + + $customer = $this->getCustomer->execute($context); + $address = $this->getCustomerAddress->execute((int)$args['id'], (int)$customer->getId()); + + $this->deleteCustomerAddress->execute($address); + return true; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/GenerateCustomerToken.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/GenerateCustomerToken.php new file mode 100644 index 000000000000..1bd77fe1cde8 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/GenerateCustomerToken.php @@ -0,0 +1,62 @@ +customerTokenService = $customerTokenService; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($args['email'])) { + throw new GraphQlInputException(__('Specify the "email" value.')); + } + + if (!isset($args['password'])) { + throw new GraphQlInputException(__('Specify the "password" value.')); + } + + try { + $token = $this->customerTokenService->createCustomerAccessToken($args['email'], $args['password']); + return ['token' => $token]; + } catch (AuthenticationException $e) { + throw new GraphQlAuthenticationException(__($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 000000000000..ddf1aec275ec --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/IsEmailAvailable.php @@ -0,0 +1,60 @@ +accountManagement = $accountManagement; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($args['email']) || empty($args['email'])) { + throw new GraphQlInputException(__('"Email should be specified')); + } + + try { + $isEmailAvailable = $this->accountManagement->isEmailAvailable($args['email']); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage()), $e); + } + + return [ + 'is_email_available' => $isEmailAvailable + ]; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/IsSubscribed.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/IsSubscribed.php new file mode 100644 index 000000000000..fc5691d97cbf --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/IsSubscribed.php @@ -0,0 +1,58 @@ +getCustomer = $getCustomer; + $this->subscriberFactory = $subscriberFactory; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + $customer = $this->getCustomer->execute($context); + + $status = $this->subscriberFactory->create()->loadByCustomerId((int)$customer->getId())->isSubscribed(); + return (bool)$status; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/RevokeCustomerToken.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/RevokeCustomerToken.php new file mode 100644 index 000000000000..92779597e5af --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/RevokeCustomerToken.php @@ -0,0 +1,57 @@ +getCustomer = $getCustomer; + $this->customerTokenService = $customerTokenService; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + $customer = $this->getCustomer->execute($context); + + return ['result' => $this->customerTokenService->revokeCustomerAccessToken((int)$customer->getId())]; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/UpdateCustomer.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/UpdateCustomer.php new file mode 100644 index 000000000000..7e06a2a063b4 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/UpdateCustomer.php @@ -0,0 +1,73 @@ +getCustomer = $getCustomer; + $this->updateCustomerAccount = $updateCustomerAccount; + $this->extractCustomerData = $extractCustomerData; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($args['input']) || !is_array($args['input']) || empty($args['input'])) { + throw new GraphQlInputException(__('"input" value should be specified')); + } + + $customer = $this->getCustomer->execute($context); + $this->updateCustomerAccount->execute($customer, $args['input']); + + $data = $this->extractCustomerData->execute($customer); + return ['customer' => $data]; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/UpdateCustomerAddress.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/UpdateCustomerAddress.php new file mode 100644 index 000000000000..bf41b7ddd10c --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/UpdateCustomerAddress.php @@ -0,0 +1,86 @@ +getCustomer = $getCustomer; + $this->getCustomerAddress = $getCustomerAddress; + $this->updateCustomerAddress = $updateCustomerAddress; + $this->extractCustomerAddressData = $extractCustomerAddressData; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($args['id']) || empty($args['id'])) { + throw new GraphQlInputException(__('Address "id" value should be specified')); + } + + if (!isset($args['input']) || !is_array($args['input']) || empty($args['input'])) { + throw new GraphQlInputException(__('"input" value should be specified')); + } + + $customer = $this->getCustomer->execute($context); + $address = $this->getCustomerAddress->execute((int)$args['id'], (int)$customer->getId()); + + $this->updateCustomerAddress->execute($address, $args['input']); + return $this->extractCustomerAddressData->execute($address); + } +} diff --git a/app/code/Magento/CustomerGraphQl/Test/Mftf/README.md b/app/code/Magento/CustomerGraphQl/Test/Mftf/README.md deleted file mode 100644 index ae023224f4d9..000000000000 --- a/app/code/Magento/CustomerGraphQl/Test/Mftf/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Customer Graph Ql Functional Tests - -The Functional Test Module for **Magento Customer Graph Ql** module. diff --git a/app/code/Magento/CustomerGraphQl/composer.json b/app/code/Magento/CustomerGraphQl/composer.json index 290d925215ec..722aba3aa065 100644 --- a/app/code/Magento/CustomerGraphQl/composer.json +++ b/app/code/Magento/CustomerGraphQl/composer.json @@ -6,6 +6,10 @@ "php": "~7.1.3||~7.2.0", "magento/module-customer": "*", "magento/module-authorization": "*", + "magento/module-eav": "*", + "magento/module-newsletter": "*", + "magento/module-integration": "*", + "magento/module-store": "*", "magento/framework": "*" }, "suggest": { 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 000000000000..f1bd3563fda3 --- /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/module.xml b/app/code/Magento/CustomerGraphQl/etc/module.xml index bde93c627650..eeed4862bbbf 100644 --- a/app/code/Magento/CustomerGraphQl/etc/module.xml +++ b/app/code/Magento/CustomerGraphQl/etc/module.xml @@ -6,11 +6,5 @@ */ --> - - - - - - - + diff --git a/app/code/Magento/CustomerGraphQl/etc/schema.graphqls b/app/code/Magento/CustomerGraphQl/etc/schema.graphqls index 91b7ef1f9be1..123818407505 100644 --- a/app/code/Magento/CustomerGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CustomerGraphQl/etc/schema.graphqls @@ -2,7 +2,78 @@ # See COPYING.txt for license details. type Query { - customer: Customer @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\Customer") @doc(description: "The customer query returns information about a customer account") + customer: Customer @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\Customer") @doc(description: "The customer query returns information about a customer account") @cache(cacheable: false) + isEmailAvailable ( + email: String! @doc(description: "The new customer email") + ): IsEmailAvailableOutput @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\IsEmailAvailable") +} + +type Mutation { + generateCustomerToken(email: String!, password: String!): CustomerToken @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\GenerateCustomerToken") @doc(description:"Retrieve the customer token") + changeCustomerPassword(currentPassword: String!, newPassword: String!): Customer @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\ChangePassword") @doc(description:"Changes the password for the logged-in customer") + createCustomer (input: CustomerInput!): CustomerOutput @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\CreateCustomer") @doc(description:"Create customer account") + updateCustomer (input: CustomerInput!): CustomerOutput @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\UpdateCustomer") @doc(description:"Update the customer's personal information") + revokeCustomerToken: RevokeCustomerTokenOutput @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\RevokeCustomerToken") @doc(description:"Revoke the customer token") + createCustomerAddress(input: CustomerAddressInput!): CustomerAddress @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\CreateCustomerAddress") @doc(description: "Create customer address") + updateCustomerAddress(id: Int!, input: CustomerAddressInput): CustomerAddress @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\UpdateCustomerAddress") @doc(description: "Update customer address") + deleteCustomerAddress(id: Int!): Boolean @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\DeleteCustomerAddress") @doc(description: "Delete customer address") +} + +input CustomerAddressInput { + firstname: String @doc(description: "The first name of the person associated with the shipping/billing address") + lastname: String @doc(description: "The family name of the person associated with the shipping/billing address") + company: String @doc(description: "The customer's company") + telephone: String @doc(description: "The telephone number") + street: [String] @doc(description: "An array of strings that define the street number and name") + city: String @doc(description: "The city or town") + region: CustomerAddressRegionInput @doc(description: "An object containing the region name, region code, and region ID") + postcode: String @doc(description: "The customer's ZIP or postal code") + country_id: CountryCodeEnum @doc(description: "The customer's country") + default_shipping: Boolean @doc(description: "Indicates whether the address is the default shipping address") + default_billing: Boolean @doc(description: "Indicates whether the address is the default billing address") + fax: String @doc(description: "The fax number") + middlename: String @doc(description: "The middle name of the person associated with the shipping/billing address") + prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.") + suffix: String @doc(description: "A value such as Sr., Jr., or III") + vat_id: String @doc(description: "The customer's Tax/VAT number (for corporate customers)") + custom_attributes: [CustomerAddressAttributeInput] @doc(description: "Address custom attributes") +} + +input CustomerAddressRegionInput @doc(description: "CustomerAddressRegionInput defines the customer's state or province") { + region_code: String @doc(description: "The address region code") + region: String @doc(description: "The state or province name") + region_id: Int @doc(description: "Uniquely identifies the region") +} + +input CustomerAddressAttributeInput { + attribute_code: String! @doc(description: "Attribute code") + value: String! @doc(description: "Attribute value") +} + +type CustomerToken { + token: String @doc(description: "The customer token") +} + +input CustomerInput { + prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.") + firstname: String @doc(description: "The customer's first name") + middlename: String @doc(description: "The customer's middle name") + lastname: String @doc(description: "The customer's family name") + suffix: String @doc(description: "A value such as Sr., Jr., or III") + email: String @doc(description: "The customer's email address. Required") + dob: String @doc(description: "The customer's date of birth") + taxvat: String @doc(description: "The customer's Tax/VAT number (for corporate customers)") + gender: Int @doc(description: "The customer's gender(Male - 1, Female - 2)") + password: String @doc(description: "The customer's password") + is_subscribed: Boolean @doc(description: "Indicates whether the customer is subscribed to the company's newsletter") +} + +type CustomerOutput { + customer: Customer! +} + +type RevokeCustomerTokenOutput { + result: Boolean! } type Customer @doc(description: "Customer defines the customer name and address and other details") { @@ -19,9 +90,10 @@ type Customer @doc(description: "Customer defines the customer name and address dob: String @doc(description: "The customer's date of birth") taxvat: String @doc(description: "The customer's Tax/VAT number (for corporate customers)") id: Int @doc(description: "The ID assigned to the customer") - is_subscribed: Boolean @doc(description: "Indicates whether the customer is subscribed to the company's newsletter") - addresses: [CustomerAddress] @doc(description: "An array containing the customer's shipping and billing addresses") -} + 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") @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\CustomerAddresses") + gender: Int @doc(description: "The customer's gender(Male - 1, Female - 2)") +} type CustomerAddress @doc(description: "CustomerAddress contains detailed information about a customer's billing and shipping addresses"){ id: Int @doc(description: "The ID assigned to the address object") @@ -43,6 +115,8 @@ type CustomerAddress @doc(description: "CustomerAddress contains detailed inform vat_id: String @doc(description: "The customer's Tax/VAT number (for corporate customers)") default_shipping: Boolean @doc(description: "Indicates whether the address is the default shipping address") default_billing: Boolean @doc(description: "Indicates whether the address is the default billing address") + custom_attributes: [CustomerAddressAttribute] @doc(description: "Address custom attributes") + extension_attributes: [CustomerAddressAttribute] @doc(description: "Address extension attributes") } type CustomerAddressRegion @doc(description: "CustomerAddressRegion defines the customer's state or province") { @@ -51,3 +125,259 @@ type CustomerAddressRegion @doc(description: "CustomerAddressRegion defines the region_id: Int @doc(description: "Uniquely identifies the region") } +type CustomerAddressAttribute { + attribute_code: String @doc(description: "Attribute code") + 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") + AL @doc(description: "Albania") + DZ @doc(description: "Algeria") + AS @doc(description: "American Samoa") + AD @doc(description: "Andorra") + AO @doc(description: "Angola") + AI @doc(description: "Anguilla") + AQ @doc(description: "Antarctica") + AG @doc(description: "Antigua & Barbuda") + AR @doc(description: "Argentina") + AM @doc(description: "Armenia") + AW @doc(description: "Aruba") + AU @doc(description: "Australia") + AT @doc(description: "Austria") + AZ @doc(description: "Azerbaijan") + BS @doc(description: "Bahamas") + BH @doc(description: "Bahrain") + BD @doc(description: "Bangladesh") + BB @doc(description: "Barbados") + BY @doc(description: "Belarus") + BE @doc(description: "Belgium") + BZ @doc(description: "Belize") + BJ @doc(description: "Benin") + BM @doc(description: "Bermuda") + BT @doc(description: "Bhutan") + BO @doc(description: "Bolivia") + BA @doc(description: "Bosnia & Herzegovina") + BW @doc(description: "Botswana") + BV @doc(description: "Bouvet Island") + BR @doc(description: "Brazil") + IO @doc(description: "British Indian Ocean Territory") + VG @doc(description: "British Virgin Islands") + BN @doc(description: "Brunei") + BG @doc(description: "Bulgaria") + BF @doc(description: "Burkina Faso") + BI @doc(description: "Burundi") + KH @doc(description: "Cambodia") + CM @doc(description: "Cameroon") + CA @doc(description: "Canada") + CV @doc(description: "Cape Verde") + KY @doc(description: "Cayman Islands") + CF @doc(description: "Central African Republic") + TD @doc(description: "Chad") + CL @doc(description: "Chile") + CN @doc(description: "China") + CX @doc(description: "Christmas Island") + CC @doc(description: "Cocos (Keeling) Islands") + CO @doc(description: "Colombia") + KM @doc(description: "Comoros") + CG @doc(description: "Congo -Brazzaville") + CD @doc(description: "Congo - Kinshasa") + CK @doc(description: "Cook Islands") + CR @doc(description: "Costa Rica") + CI @doc(description: "Côte d’Ivoire") + HR @doc(description: "Croatia") + CU @doc(description: "Cuba") + CY @doc(description: "Cyprus") + CZ @doc(description: "Czech Republic") + DK @doc(description: "Denmark") + DJ @doc(description: "Djibouti") + DM @doc(description: "Dominica") + DO @doc(description: "Dominican Republic") + EC @doc(description: "Ecuador") + EG @doc(description: "Egypt") + SV @doc(description: "El Salvador") + GQ @doc(description: "Equatorial Guinea") + ER @doc(description: "Eritrea") + EE @doc(description: "Estonia") + ET @doc(description: "Ethiopia") + FK @doc(description: "Falkland Islands") + FO @doc(description: "Faroe Islands") + FJ @doc(description: "Fiji") + FI @doc(description: "Finland") + FR @doc(description: "France") + GF @doc(description: "French Guiana") + PF @doc(description: "French Polynesia") + TF @doc(description: "French Southern Territories") + GA @doc(description: "Gabon") + GM @doc(description: "Gambia") + GE @doc(description: "Georgia") + DE @doc(description: "Germany") + GH @doc(description: "Ghana") + GI @doc(description: "Gibraltar") + GR @doc(description: "Greece") + GL @doc(description: "Greenland") + GD @doc(description: "Grenada") + GP @doc(description: "Guadeloupe") + GU @doc(description: "Guam") + GT @doc(description: "Guatemala") + GG @doc(description: "Guernsey") + GN @doc(description: "Guinea") + GW @doc(description: "Guinea-Bissau") + GY @doc(description: "Guyana") + HT @doc(description: "Haiti") + HM @doc(description: "Heard & McDonald Islands") + HN @doc(description: "Honduras") + HK @doc(description: "Hong Kong SAR China") + HU @doc(description: "Hungary") + IS @doc(description: "Iceland") + IN @doc(description: "India") + ID @doc(description: "Indonesia") + IR @doc(description: "Iran") + IQ @doc(description: "Iraq") + IE @doc(description: "Ireland") + IM @doc(description: "Isle of Man") + IL @doc(description: "Israel") + IT @doc(description: "Italy") + JM @doc(description: "Jamaica") + JP @doc(description: "Japan") + JE @doc(description: "Jersey") + JO @doc(description: "Jordan") + KZ @doc(description: "Kazakhstan") + KE @doc(description: "Kenya") + KI @doc(description: "Kiribati") + KW @doc(description: "Kuwait") + KG @doc(description: "Kyrgyzstan") + LA @doc(description: "Laos") + LV @doc(description: "Latvia") + LB @doc(description: "Lebanon") + LS @doc(description: "Lesotho") + LR @doc(description: "Liberia") + LY @doc(description: "Libya") + LI @doc(description: "Liechtenstein") + LT @doc(description: "Lithuania") + LU @doc(description: "Luxembourg") + MO @doc(description: "Macau SAR China") + MK @doc(description: "Macedonia") + MG @doc(description: "Madagascar") + MW @doc(description: "Malawi") + MY @doc(description: "Malaysia") + MV @doc(description: "Maldives") + ML @doc(description: "Mali") + MT @doc(description: "Malta") + MH @doc(description: "Marshall Islands") + MQ @doc(description: "Martinique") + MR @doc(description: "Mauritania") + MU @doc(description: "Mauritius") + YT @doc(description: "Mayotte") + MX @doc(description: "Mexico") + FM @doc(description: "Micronesia") + MD @doc(description: "Moldova") + MC @doc(description: "Monaco") + MN @doc(description: "Mongolia") + ME @doc(description: "Montenegro") + MS @doc(description: "Montserrat") + MA @doc(description: "Morocco") + MZ @doc(description: "Mozambique") + MM @doc(description: "Myanmar (Burma)") + NA @doc(description: "Namibia") + NR @doc(description: "Nauru") + NP @doc(description: "Nepal") + NL @doc(description: "Netherlands") + AN @doc(description: "Netherlands Antilles") + NC @doc(description: "New Caledonia") + NZ @doc(description: "New Zealand") + NI @doc(description: "Nicaragua") + NE @doc(description: "Niger") + NG @doc(description: "Nigeria") + NU @doc(description: "Niue") + NF @doc(description: "Norfolk Island") + MP @doc(description: "Northern Mariana Islands") + KP @doc(description: "North Korea") + NO @doc(description: "Norway") + OM @doc(description: "Oman") + PK @doc(description: "Pakistan") + PW @doc(description: "Palau") + PS @doc(description: "Palestinian Territories") + PA @doc(description: "Panama") + PG @doc(description: "Papua New Guinea") + PY @doc(description: "Paraguay") + PE @doc(description: "Peru") + PH @doc(description: "Philippines") + PN @doc(description: "Pitcairn Islands") + PL @doc(description: "Poland") + PT @doc(description: "Portugal") + QA @doc(description: "Qatar") + RE @doc(description: "Réunion") + RO @doc(description: "Romania") + RU @doc(description: "Russia") + RW @doc(description: "Rwanda") + WS @doc(description: "Samoa") + SM @doc(description: "San Marino") + ST @doc(description: "São Tomé & Príncipe") + SA @doc(description: "Saudi Arabia") + SN @doc(description: "Senegal") + RS @doc(description: "Serbia") + SC @doc(description: "Seychelles") + SL @doc(description: "Sierra Leone") + SG @doc(description: "Singapore") + SK @doc(description: "Slovakia") + SI @doc(description: "Slovenia") + SB @doc(description: "Solomon Islands") + SO @doc(description: "Somalia") + ZA @doc(description: "South Africa") + GS @doc(description: "South Georgia & South Sandwich Islands") + KR @doc(description: "South Korea") + ES @doc(description: "Spain") + LK @doc(description: "Sri Lanka") + BL @doc(description: "St. Barthélemy") + SH @doc(description: "St. Helena") + KN @doc(description: "St. Kitts & Nevis") + LC @doc(description: "St. Lucia") + MF @doc(description: "St. Martin") + PM @doc(description: "St. Pierre & Miquelon") + VC @doc(description: "St. Vincent & Grenadines") + SD @doc(description: "Sudan") + SR @doc(description: "Suriname") + SJ @doc(description: "Svalbard & Jan Mayen") + SZ @doc(description: "Swaziland") + SE @doc(description: "Sweden") + CH @doc(description: "Switzerland") + SY @doc(description: "Syria") + TW @doc(description: "Taiwan") + TJ @doc(description: "Tajikistan") + TZ @doc(description: "Tanzania") + TH @doc(description: "Thailand") + TL @doc(description: "Timor-Leste") + TG @doc(description: "Togo") + TK @doc(description: "Tokelau") + TO @doc(description: "Tonga") + TT @doc(description: "Trinidad & Tobago") + TN @doc(description: "Tunisia") + TR @doc(description: "Turkey") + TM @doc(description: "Turkmenistan") + TC @doc(description: "Turks & Caicos Islands") + TV @doc(description: "Tuvalu") + UG @doc(description: "Uganda") + UA @doc(description: "Ukraine") + AE @doc(description: "United Arab Emirates") + GB @doc(description: "United Kingdom") + US @doc(description: "United States") + UY @doc(description: "Uruguay") + UM @doc(description: "U.S. Outlying Islands") + VI @doc(description: "U.S. Virgin Islands") + UZ @doc(description: "Uzbekistan") + VU @doc(description: "Vanuatu") + VA @doc(description: "Vatican City") + VE @doc(description: "Venezuela") + VN @doc(description: "Vietnam") + WF @doc(description: "Wallis & Futuna") + EH @doc(description: "Western Sahara") + YE @doc(description: "Yemen") + ZM @doc(description: "Zambia") + ZW @doc(description: "Zimbabwe") +} diff --git a/app/code/Magento/CustomerImportExport/Model/Import/Address.php b/app/code/Magento/CustomerImportExport/Model/Import/Address.php index e1345edcf146..7a1a09efaa7b 100644 --- a/app/code/Magento/CustomerImportExport/Model/Import/Address.php +++ b/app/code/Magento/CustomerImportExport/Model/Import/Address.php @@ -101,6 +101,13 @@ class Address extends AbstractCustomer */ protected $_entityTable; + /** + * Region collection instance + * + * @var \Magento\Directory\Model\ResourceModel\Region\Collection + */ + private $_regionCollection; + /** * Countries and regions * @@ -781,7 +788,7 @@ public static function getDefaultAddressAttributeMapping() } /** - * check if address for import is empty (for customer composite mode) + * Check if address for import is empty (for customer composite mode) * * @param array $rowData * @return array @@ -940,7 +947,7 @@ protected function _checkRowDuplicate($customerId, $addressId) } /** - * set customer attributes + * Set customer attributes * * @param array $customerAttributes * @return $this diff --git a/app/code/Magento/CustomerImportExport/Model/Import/CustomerComposite.php b/app/code/Magento/CustomerImportExport/Model/Import/CustomerComposite.php index 956c9695623b..2086f41d27fa 100644 --- a/app/code/Magento/CustomerImportExport/Model/Import/CustomerComposite.php +++ b/app/code/Magento/CustomerImportExport/Model/Import/CustomerComposite.php @@ -299,8 +299,8 @@ public function validateData() $rows = []; foreach ($source as $row) { $rows[] = [ - Address::COLUMN_EMAIL => $row[Customer::COLUMN_EMAIL], - Address::COLUMN_WEBSITE => $row[Customer::COLUMN_WEBSITE], + Address::COLUMN_EMAIL => $row[Customer::COLUMN_EMAIL] ?? null, + Address::COLUMN_WEBSITE => $row[Customer::COLUMN_WEBSITE] ?? null ]; } $source->rewind(); diff --git a/app/code/Magento/CustomerImportExport/Model/ResourceModel/Import/Customer/Storage.php b/app/code/Magento/CustomerImportExport/Model/ResourceModel/Import/Customer/Storage.php index f779505a3801..43623019c005 100644 --- a/app/code/Magento/CustomerImportExport/Model/ResourceModel/Import/Customer/Storage.php +++ b/app/code/Magento/CustomerImportExport/Model/ResourceModel/Import/Customer/Storage.php @@ -13,6 +13,9 @@ use Magento\ImportExport\Model\ResourceModel\CollectionByPagesIteratorFactory; use Magento\ImportExport\Model\ResourceModel\CollectionByPagesIterator; +/** + * Storage to check existing customers. + */ class Storage { /** @@ -48,6 +51,11 @@ class Storage */ private $customerCollectionFactory; + /** + * @var CustomerCollection + */ + public $_customerCollection; + /** * @param CustomerCollectionFactory $collectionFactory * @param CollectionByPagesIteratorFactory $colIteratorFactory @@ -112,6 +120,8 @@ private function loadCustomersData(array $customerIdentifiers) } /** + * Add a customer by an array + * * @param array $customer * @return $this */ diff --git a/app/code/Magento/Deploy/Collector/Collector.php b/app/code/Magento/Deploy/Collector/Collector.php index b02c8e478d63..5974297a76cc 100644 --- a/app/code/Magento/Deploy/Collector/Collector.php +++ b/app/code/Magento/Deploy/Collector/Collector.php @@ -8,6 +8,8 @@ use Magento\Deploy\Source\SourcePool; use Magento\Deploy\Package\Package; use Magento\Deploy\Package\PackageFactory; +use Magento\Deploy\Package\PackageFile; +use Magento\Framework\Module\Manager; use Magento\Framework\View\Asset\PreProcessor\FileNameResolver; /** @@ -43,6 +45,9 @@ class Collector implements CollectorInterface * @var PackageFactory */ private $packageFactory; + + /** @var \Magento\Framework\Module\Manager */ + private $moduleManager; /** * Default values for package primary identifiers @@ -61,15 +66,19 @@ class Collector implements CollectorInterface * @param SourcePool $sourcePool * @param FileNameResolver $fileNameResolver * @param PackageFactory $packageFactory + * @param Manager|null $moduleManager */ public function __construct( SourcePool $sourcePool, FileNameResolver $fileNameResolver, - PackageFactory $packageFactory + PackageFactory $packageFactory, + Manager $moduleManager = null ) { $this->sourcePool = $sourcePool; $this->fileNameResolver = $fileNameResolver; $this->packageFactory = $packageFactory; + $this->moduleManager = $moduleManager ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Framework\Module\Manager::class); } /** @@ -81,19 +90,11 @@ public function collect() foreach ($this->sourcePool->getAll() as $source) { $files = $source->get(); foreach ($files as $file) { - $file->setDeployedFileName($this->fileNameResolver->resolve($file->getFileName())); - $params = [ - 'area' => $file->getArea(), - 'theme' => $file->getTheme(), - 'locale' => $file->getLocale(), - 'module' => $file->getModule(), - 'isVirtual' => (!$file->getLocale() || !$file->getTheme() || !$file->getArea()) - ]; - foreach ($this->packageDefaultValues as $name => $value) { - if (!isset($params[$name])) { - $params[$name] = $value; - } + if ($file->getModule() && !$this->moduleManager->isEnabled($file->getModule())) { + continue; } + $file->setDeployedFileName($this->fileNameResolver->resolve($file->getFileName())); + $params = $this->getParams($file); $packagePath = "{$params['area']}/{$params['theme']}/{$params['locale']}"; if (!isset($packages[$packagePath])) { $packages[$packagePath] = $this->packageFactory->create($params); @@ -105,4 +106,27 @@ public function collect() } return $packages; } + + /** + * Retrieve package params + * + * @param PackageFile $file + * @return array + */ + private function getParams(PackageFile $file) + { + $params = [ + 'area' => $file->getArea(), + 'theme' => $file->getTheme(), + 'locale' => $file->getLocale(), + 'module' => $file->getModule(), + 'isVirtual' => (!$file->getLocale() || !$file->getTheme() || !$file->getArea()) + ]; + foreach ($this->packageDefaultValues as $name => $value) { + if (!isset($params[$name])) { + $params[$name] = $value; + } + } + return $params; + } } diff --git a/app/code/Magento/Deploy/Console/DeployStaticOptions.php b/app/code/Magento/Deploy/Console/DeployStaticOptions.php index 9a73dd5d65fc..1c02d24f7e99 100644 --- a/app/code/Magento/Deploy/Console/DeployStaticOptions.php +++ b/app/code/Magento/Deploy/Console/DeployStaticOptions.php @@ -6,6 +6,7 @@ namespace Magento\Deploy\Console; +use Magento\Deploy\Process\Queue; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; @@ -57,6 +58,11 @@ class DeployStaticOptions */ const JOBS_AMOUNT = 'jobs'; + /** + * Key for max execution time option + */ + const MAX_EXECUTION_TIME = 'max-execution-time'; + /** * Force run of static deploy */ @@ -150,6 +156,7 @@ public function getOptionsList() * Basic options * * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ private function getBasicOptions() { @@ -216,6 +223,13 @@ private function getBasicOptions() 'Enable parallel processing using the specified number of jobs.', self::DEFAULT_JOBS_AMOUNT ), + new InputOption( + self::MAX_EXECUTION_TIME, + null, + InputOption::VALUE_OPTIONAL, + 'The maximum expected execution time of deployment static process (in seconds).', + Queue::DEFAULT_MAX_EXEC_TIME + ), new InputOption( self::SYMLINK_LOCALE, null, @@ -240,7 +254,7 @@ private function getBasicOptions() new InputArgument( self::LANGUAGES_ARGUMENT, InputArgument::IS_ARRAY, - 'Space-separated list of ISO-636 language codes for which to output static view files.' + 'Space-separated list of ISO-639 language codes for which to output static view files.' ), ]; } diff --git a/app/code/Magento/Deploy/Console/InputValidator.php b/app/code/Magento/Deploy/Console/InputValidator.php index b3301f60fec2..772410d58a46 100644 --- a/app/code/Magento/Deploy/Console/InputValidator.php +++ b/app/code/Magento/Deploy/Console/InputValidator.php @@ -9,6 +9,8 @@ use Magento\Deploy\Console\DeployStaticOptions as Options; use Magento\Framework\Validator\Locale; use Symfony\Component\Console\Input\InputInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Validator\RegexFactory; /** * Command input arguments validator class @@ -55,14 +57,24 @@ class InputValidator */ private $localeValidator; + /** + * @var RegexFactory + */ + private $versionValidatorFactory; + /** * InputValidator constructor * * @param Locale $localeValidator + * @param RegexFactory $versionValidatorFactory */ - public function __construct(Locale $localeValidator) - { + public function __construct( + Locale $localeValidator, + ?RegexFactory $versionValidatorFactory = null + ) { $this->localeValidator = $localeValidator; + $this->versionValidatorFactory = $versionValidatorFactory ?: + ObjectManager::getInstance()->get(RegexFactory::class); } /** @@ -85,6 +97,9 @@ public function validate(InputInterface $input) $input->getArgument(Options::LANGUAGES_ARGUMENT) ?: ['all'], $input->getOption(Options::EXCLUDE_LANGUAGE) ); + $this->checkVersionInput( + $input->getOption(Options::CONTENT_VERSION) ?: '' + ); } /** @@ -147,4 +162,29 @@ private function checkLanguagesInput(array $languagesInclude, array $languagesEx } } } + + /** + * Version input checks + * + * @param string $contentVersion + * @throws \InvalidArgumentException + */ + private function checkVersionInput(string $contentVersion): void + { + if ($contentVersion) { + $versionValidator = $this->versionValidatorFactory->create( + [ + 'pattern' => '/^[A-Za-z0-9_.]+$/' + ] + ); + + if (!$versionValidator->isValid($contentVersion)) { + throw new \InvalidArgumentException( + 'Argument "' . + Options::CONTENT_VERSION + . '" has invalid value, content version should contain only characters, digits and dots' + ); + } + } + } } diff --git a/app/code/Magento/Deploy/Model/DeploymentConfig/ImporterPool.php b/app/code/Magento/Deploy/Model/DeploymentConfig/ImporterPool.php index a4a9ef00c4a8..efcc12e08949 100644 --- a/app/code/Magento/Deploy/Model/DeploymentConfig/ImporterPool.php +++ b/app/code/Magento/Deploy/Model/DeploymentConfig/ImporterPool.php @@ -110,6 +110,8 @@ public function __construct( } /** + * Retrieves sections names + * * Retrieves names of sections for configuration files whose data is read from these files for import * to appropriate application sources. * @@ -187,14 +189,7 @@ public function getValidator($section) private function sort(array $data) { uasort($data, function (array $a, array $b) { - $a['sort_order'] = $this->getSortOrder($a); - $b['sort_order'] = $this->getSortOrder($b); - - if ($a['sort_order'] == $b['sort_order']) { - return 0; - } - - return ($a['sort_order'] < $b['sort_order']) ? -1 : 1; + return $this->getSortOrder($a) <=> $this->getSortOrder($b); }); return $data; diff --git a/app/code/Magento/Deploy/Model/Filesystem.php b/app/code/Magento/Deploy/Model/Filesystem.php index 59880d2d669b..bc19ee287858 100644 --- a/app/code/Magento/Deploy/Model/Filesystem.php +++ b/app/code/Magento/Deploy/Model/Filesystem.php @@ -29,8 +29,8 @@ class Filesystem * Access permissions to the files are set during deploy Magento 2, directly after * uploading code of Magento. Also it is possible to specify the value * of inverse mask for setting access permissions to files generated by Magento. - * @link http://devdocs.magento.com/guides/v2.0/install-gde/install/post-install-umask.html - * @link http://devdocs.magento.com/guides/v2.0/install-gde/prereq/file-system-perms.html + * @link https://devdocs.magento.com/guides/v2.0/install-gde/install/post-install-umask.html + * @link https://devdocs.magento.com/guides/v2.0/install-gde/prereq/file-system-perms.html */ const PERMISSIONS_FILE = 0640; @@ -41,8 +41,8 @@ class Filesystem * Access permissions to the directories are set during deploy Magento 2, directly after * uploading code of Magento. Also it is possible to specify the value * of inverse mask for setting access permissions to directories generated by Magento. - * @link http://devdocs.magento.com/guides/v2.0/install-gde/install/post-install-umask.html - * @link http://devdocs.magento.com/guides/v2.0/install-gde/prereq/file-system-perms.html + * @link https://devdocs.magento.com/guides/v2.0/install-gde/install/post-install-umask.html + * @link https://devdocs.magento.com/guides/v2.0/install-gde/prereq/file-system-perms.html */ const PERMISSIONS_DIR = 0750; @@ -150,6 +150,8 @@ public function __construct( * * @param OutputInterface $output * @return void + * @throws LocalizedException + * @throws \Exception */ public function regenerateStatic( OutputInterface $output @@ -164,9 +166,12 @@ public function regenerateStatic( DirectoryList::STATIC_VIEW ] ); - + + $this->reinitCacheDirectories(); // Trigger code generation $this->compile($output); + + $this->reinitCacheDirectories(); // Trigger static assets compilation and deployment $this->deployStaticContent($output); } @@ -217,6 +222,7 @@ private function getAdminUserInterfaceLocales() * * @return array * @throws \InvalidArgumentException if unknown locale is provided by the store configuration + * @throws \Magento\Framework\Exception\FileSystemException */ private function getUsedLocales() { @@ -249,13 +255,6 @@ function ($locale) { protected function compile(OutputInterface $output) { $output->writeln('Starting compilation'); - $this->cleanupFilesystem( - [ - DirectoryList::CACHE, - DirectoryList::GENERATED_CODE, - DirectoryList::GENERATED_METADATA, - ] - ); $cmd = $this->functionCallPath . 'setup:di:compile'; /** @@ -279,6 +278,7 @@ protected function compile(OutputInterface $output) * * @param array $directoryCodeList * @return void + * @throws \Magento\Framework\Exception\FileSystemException */ public function cleanupFilesystem($directoryCodeList) { @@ -320,8 +320,9 @@ public function cleanupFilesystem($directoryCodeList) * Access permissions to the files and directories are set during deploy Magento 2, directly after * uploading code of Magento. Also it is possible to specify the value * of inverse mask for setting access permissions to files and directories generated by Magento. - * @link http://devdocs.magento.com/guides/v2.0/install-gde/install/post-install-umask.html - * @link http://devdocs.magento.com/guides/v2.0/install-gde/prereq/file-system-perms.html + * @link https://devdocs.magento.com/guides/v2.0/install-gde/install/post-install-umask.html + * @link https://devdocs.magento.com/guides/v2.0/install-gde/prereq/file-system-perms.html + * @throws \Magento\Framework\Exception\FileSystemException */ protected function changePermissions($directoryCodeList, $dirPermissions, $filePermissions) { @@ -345,8 +346,9 @@ protected function changePermissions($directoryCodeList, $dirPermissions, $fileP * Access permissions to the files and directories are set during deploy Magento 2, directly after * uploading code of Magento. Also it is possible to specify the value * of inverse mask for setting access permissions to files and directories generated by Magento. - * @link http://devdocs.magento.com/guides/v2.0/install-gde/install/post-install-umask.html - * @link http://devdocs.magento.com/guides/v2.0/install-gde/prereq/file-system-perms.html + * @link https://devdocs.magento.com/guides/v2.0/install-gde/install/post-install-umask.html + * @link https://devdocs.magento.com/guides/v2.0/install-gde/prereq/file-system-perms.html + * @throws \Magento\Framework\Exception\FileSystemException */ public function lockStaticResources() { @@ -361,4 +363,15 @@ public function lockStaticResources() self::PERMISSIONS_FILE ); } + + /** + * Flush cache and restore the basic cache directories. + * + * @throws LocalizedException + */ + private function reinitCacheDirectories() + { + $command = $this->functionCallPath . 'cache:flush'; + $this->shell->execute($command); + } } diff --git a/app/code/Magento/Deploy/Process/Queue.php b/app/code/Magento/Deploy/Process/Queue.php index c4c31b8ff77b..fd7aad44e0a5 100644 --- a/app/code/Magento/Deploy/Process/Queue.php +++ b/app/code/Magento/Deploy/Process/Queue.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Deploy\Process; use Magento\Deploy\Package\Package; @@ -125,6 +127,8 @@ public function __construct( } /** + * Adds deployment package. + * * @param Package $package * @param Package[] $dependencies * @return bool true on success @@ -140,6 +144,8 @@ public function add(Package $package, array $dependencies = []) } /** + * Returns packages array. + * * @return Package[] */ public function getPackages() @@ -162,6 +168,7 @@ public function process() $this->assertAndExecute($name, $packages, $packageJob); } $this->logger->info('.'); + // phpcs:ignore Magento2.Functions.DiscouragedFunction sleep(3); foreach ($this->inProgress as $name => $package) { if ($this->isDeployed($package)) { @@ -209,6 +216,8 @@ private function assertAndExecute($name, array & $packages, array $packageJob) } /** + * Executes deployment package. + * * @param Package $package * @param string $name * @param array $packages @@ -244,6 +253,7 @@ private function awaitForAllProcesses() } } $this->logger->info('.'); + // phpcs:ignore Magento2.Functions.DiscouragedFunction sleep(5); } if ($this->isCanBeParalleled()) { @@ -253,6 +263,8 @@ private function awaitForAllProcesses() } /** + * Checks if can be parallel. + * * @return bool */ private function isCanBeParalleled() @@ -261,9 +273,11 @@ private function isCanBeParalleled() } /** + * Executes the process. + * * @param Package $package * @return bool true on success for main process and exit for child process - * @SuppressWarnings(PHPMD.ExitExpression) + * @throws \RuntimeException */ private function execute(Package $package) { @@ -291,6 +305,7 @@ function () use ($package) { ); if ($this->isCanBeParalleled()) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $pid = pcntl_fork(); if ($pid === -1) { throw new \RuntimeException('Unable to fork a new process'); @@ -305,6 +320,7 @@ function () use ($package) { // process child process $this->inProgress = []; $this->deployPackageService->deploy($package, $this->options, true); + // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage exit(0); } else { $this->deployPackageService->deploy($package, $this->options); @@ -313,6 +329,8 @@ function () use ($package) { } /** + * Checks if package is deployed. + * * @param Package $package * @return bool */ @@ -320,11 +338,13 @@ private function isDeployed(Package $package) { if ($this->isCanBeParalleled()) { if ($package->getState() === null) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $pid = pcntl_waitpid($this->getPid($package), $status, WNOHANG); if ($pid === $this->getPid($package)) { $package->setState(Package::STATE_COMPLETED); unset($this->inProgress[$package->getPath()]); + // phpcs:ignore Magento2.Functions.DiscouragedFunction return pcntl_wexitstatus($status) === 0; } return false; @@ -334,17 +354,19 @@ private function isDeployed(Package $package) } /** + * Returns process ID or null if not found. + * * @param Package $package * @return int|null */ private function getPid(Package $package) { - return isset($this->processIds[$package->getPath()]) - ? $this->processIds[$package->getPath()] - : null; + return isset($this->processIds[$package->getPath()]) ?? null; } /** + * Checks timeout. + * * @return bool */ private function checkTimeout() @@ -357,11 +379,13 @@ private function checkTimeout() * * Protect against zombie process * + * @throws \RuntimeException * @return void */ public function __destruct() { foreach ($this->inProgress as $package) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction if (pcntl_waitpid($this->getPid($package), $status) === -1) { throw new \RuntimeException( 'Error while waiting for package deployed: ' . $this->getPid($package) . '; Status: ' . $status diff --git a/app/code/Magento/Deploy/Service/DeployStaticContent.php b/app/code/Magento/Deploy/Service/DeployStaticContent.php index 66ec6e7418af..890399715991 100644 --- a/app/code/Magento/Deploy/Service/DeployStaticContent.php +++ b/app/code/Magento/Deploy/Service/DeployStaticContent.php @@ -9,6 +9,7 @@ use Magento\Deploy\Process\QueueFactory; use Magento\Deploy\Console\DeployStaticOptions as Options; use Magento\Framework\App\View\Deployment\Version\StorageInterface; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\ObjectManagerInterface; use Psr\Log\LoggerInterface; @@ -16,6 +17,7 @@ * Main service for static content deployment * * Aggregates services to deploy static files, static files bundles, translations and minified templates + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class DeployStaticContent { @@ -71,6 +73,7 @@ public function __construct( * Run deploy procedure * * @param array $options + * @throws LocalizedException * @return void */ public function deploy(array $options) @@ -85,24 +88,26 @@ public function deploy(array $options) return; } - $queue = $this->queueFactory->create( - [ - 'logger' => $this->logger, - 'options' => $options, - 'maxProcesses' => $this->getProcessesAmount($options), - 'deployPackageService' => $this->objectManager->create( - \Magento\Deploy\Service\DeployPackage::class, - [ - 'logger' => $this->logger - ] - ) - ] - ); + $queueOptions = [ + 'logger' => $this->logger, + 'options' => $options, + 'maxProcesses' => $this->getProcessesAmount($options), + 'deployPackageService' => $this->objectManager->create( + \Magento\Deploy\Service\DeployPackage::class, + [ + 'logger' => $this->logger + ] + ) + ]; + + if (isset($options[Options::MAX_EXECUTION_TIME])) { + $queueOptions['maxExecTime'] = (int)$options[Options::MAX_EXECUTION_TIME]; + } $deployStrategy = $this->deployStrategyFactory->create( $options[Options::STRATEGY], [ - 'queue' => $queue + 'queue' => $this->queueFactory->create($queueOptions) ] ); @@ -133,6 +138,8 @@ public function deploy(array $options) } /** + * Returns amount of parallel processes, returns zero if option wasn't set. + * * @param array $options * @return int */ @@ -142,6 +149,8 @@ private function getProcessesAmount(array $options) } /** + * Checks if need to refresh only version. + * * @param array $options * @return bool */ diff --git a/app/code/Magento/Deploy/Test/Unit/Console/InputValidatorTest.php b/app/code/Magento/Deploy/Test/Unit/Console/InputValidatorTest.php new file mode 100644 index 000000000000..f3991222dfa8 --- /dev/null +++ b/app/code/Magento/Deploy/Test/Unit/Console/InputValidatorTest.php @@ -0,0 +1,210 @@ +objectManagerHelper = new ObjectManagerHelper($this); + + $regexFactoryMock = $this->getMockBuilder(RegexFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $regexObject = new Regex('/^[A-Za-z0-9_.]+$/'); + + $regexFactoryMock->expects($this->any())->method('create') + ->willReturn($regexObject); + + $localeObjectMock = $this->getMockBuilder(Locale::class)->setMethods(['isValid']) + ->disableOriginalConstructor() + ->getMock(); + + $localeObjectMock->expects($this->any())->method('isValid') + ->with('en_US') + ->will($this->returnValue(true)); + + $this->inputValidator = $this->objectManagerHelper->getObject( + InputValidator::class, + [ + 'localeValidator' => $localeObjectMock, + 'versionValidatorFactory' => $regexFactoryMock + ] + ); + } + + /** + * @throws \Zend_Validate_Exception + */ + public function testValidate() + { + $input = $this->getMockBuilder(ArrayInput::class) + ->disableOriginalConstructor() + ->setMethods(['getOption', 'getArgument']) + ->getMock(); + + $input->expects($this->atLeastOnce())->method('getArgument')->willReturn(['all']); + + $input->expects($this->atLeastOnce())->method('getOption') + ->willReturnMap( + [ + [Options::AREA, ['all']], + [Options::EXCLUDE_AREA, ['none']], + [Options::THEME, ['all']], + [Options::EXCLUDE_THEME, ['none']], + [Options::EXCLUDE_LANGUAGE, ['none']], + [Options::CONTENT_VERSION, '12345'] + ] + ); + + /** @noinspection PhpParamsInspection */ + $this->inputValidator->validate($input); + } + + /** + * @covers \Magento\Deploy\Console\InputValidator::checkAreasInput() + */ + public function testCheckAreasInputException() + { + $options = [ + new InputOption(Options::AREA, null, 4, '', ['test']), + new InputOption(Options::EXCLUDE_AREA, null, 4, '', ['test']) + ]; + + $inputDefinition = new InputDefinition($options); + + try { + $this->inputValidator->validate( + new ArrayInput([], $inputDefinition) + ); + } catch (\Exception $e) { + $this->assertContains('--area (-a) and --exclude-area cannot be used at the same time', $e->getMessage()); + $this->assertInstanceOf(InvalidArgumentException::class, $e); + } + } + + /** + * @covers \Magento\Deploy\Console\InputValidator::checkThemesInput() + */ + public function testCheckThemesInputException() + { + $options = [ + new InputOption(Options::AREA, null, 4, '', ['all']), + new InputOption(Options::EXCLUDE_AREA, null, 4, '', ['none']), + new InputOption(Options::THEME, null, 4, '', ['blank']), + new InputOption(Options::EXCLUDE_THEME, null, 4, '', ['luma']) + ]; + + $inputDefinition = new InputDefinition($options); + + try { + $this->inputValidator->validate( + new ArrayInput([], $inputDefinition) + ); + } catch (\Exception $e) { + $this->assertContains('--theme (-t) and --exclude-theme cannot be used at the same time', $e->getMessage()); + $this->assertInstanceOf(InvalidArgumentException::class, $e); + } + } + + public function testCheckLanguagesInputException() + { + $options = [ + new InputOption(Options::AREA, null, 4, '', ['all']), + new InputOption(Options::EXCLUDE_AREA, '', 4, '', ['none']), + new InputOption(Options::THEME, null, 4, '', ['all']), + new InputOption(Options::EXCLUDE_THEME, null, 4, '', ['none']), + new InputArgument(Options::LANGUAGES_ARGUMENT, 2, '', ['en_US']), + new InputOption(Options::EXCLUDE_LANGUAGE, null, 4, '', ['all']) + ]; + + $inputDefinition = new InputDefinition($options); + + try { + $this->inputValidator->validate( + new ArrayInput([], $inputDefinition) + ); + } catch (\Exception $e) { + $this->assertContains( + '--language (-l) and --exclude-language cannot be used at the same time', + $e->getMessage() + ); + + $this->assertInstanceOf(InvalidArgumentException::class, $e); + } + } + + public function testCheckVersionInputException() + { + $options = [ + new InputOption(Options::AREA, null, 4, '', ['all']), + new InputOption(Options::EXCLUDE_AREA, null, 4, '', ['none']), + new InputOption(Options::THEME, null, 4, '', ['all']), + new InputOption(Options::EXCLUDE_THEME, null, 4, '', ['none']), + new InputArgument(Options::LANGUAGES_ARGUMENT, 2, '', ['en_US']), + new InputOption(Options::EXCLUDE_LANGUAGE, null, 4, '', ['none']), + new InputOption(Options::CONTENT_VERSION, null, 4, '', '/*!#') + ]; + + $inputDefinition = new InputDefinition($options); + + try { + $this->inputValidator->validate( + new ArrayInput([], $inputDefinition) + ); + } catch (\Exception $e) { + $this->assertContains( + 'Argument "' . + Options::CONTENT_VERSION + . '" has invalid value, content version should contain only characters, digits and dots', + $e->getMessage() + ); + + $this->assertInstanceOf(InvalidArgumentException::class, $e); + } + } +} diff --git a/app/code/Magento/Deploy/Test/Unit/Model/FilesystemTest.php b/app/code/Magento/Deploy/Test/Unit/Model/FilesystemTest.php index 00f70c6527a0..d3ff594fa612 100644 --- a/app/code/Magento/Deploy/Test/Unit/Model/FilesystemTest.php +++ b/app/code/Magento/Deploy/Test/Unit/Model/FilesystemTest.php @@ -69,6 +69,9 @@ class FilesystemTest extends \PHPUnit\Framework\TestCase */ private $cmdPrefix; + /** + * @inheritdoc + */ protected function setUp() { $objectManager = new ObjectManager($this); @@ -124,6 +127,9 @@ protected function setUp() $this->cmdPrefix = PHP_BINARY . ' -f ' . BP . DIRECTORY_SEPARATOR . 'bin' . DIRECTORY_SEPARATOR . 'magento '; } + /** + * @throws \Magento\Framework\Exception\LocalizedException + */ public function testRegenerateStatic() { $storeLocales = ['fr_FR', 'de_DE', 'nl_NL']; @@ -131,18 +137,16 @@ public function testRegenerateStatic() ->willReturn($storeLocales); $setupDiCompileCmd = $this->cmdPrefix . 'setup:di:compile'; - $this->shell->expects(self::at(0)) - ->method('execute') - ->with($setupDiCompileCmd); - $this->initAdminLocaleMock('en_US'); $usedLocales = ['fr_FR', 'de_DE', 'nl_NL', 'en_US']; + $cacheFlushCmd = $this->cmdPrefix . 'cache:flush'; $staticContentDeployCmd = $this->cmdPrefix . 'setup:static-content:deploy -f ' . implode(' ', $usedLocales); - $this->shell->expects(self::at(1)) + $this->shell + ->expects($this->exactly(4)) ->method('execute') - ->with($staticContentDeployCmd); + ->withConsecutive([$cacheFlushCmd], [$setupDiCompileCmd], [$cacheFlushCmd], [$staticContentDeployCmd]); $this->output->expects(self::at(0)) ->method('writeln') @@ -166,6 +170,7 @@ public function testRegenerateStatic() * @return void * @expectedException \InvalidArgumentException * @expectedExceptionMessage ;echo argument has invalid value, run info:language:list for list of available locales + * @throws \Magento\Framework\Exception\LocalizedException */ public function testGenerateStaticForNotAllowedStoreViewLocale() { @@ -184,6 +189,7 @@ public function testGenerateStaticForNotAllowedStoreViewLocale() * @return void * @expectedException \InvalidArgumentException * @expectedExceptionMessage ;echo argument has invalid value, run info:language:list for list of available locales + * @throws \Magento\Framework\Exception\LocalizedException */ public function testGenerateStaticForNotAllowedAdminLocale() { diff --git a/app/code/Magento/Deploy/Test/Unit/Service/DeployStaticContentTest.php b/app/code/Magento/Deploy/Test/Unit/Service/DeployStaticContentTest.php index 75edc8cb4f6e..396381960e54 100644 --- a/app/code/Magento/Deploy/Test/Unit/Service/DeployStaticContentTest.php +++ b/app/code/Magento/Deploy/Test/Unit/Service/DeployStaticContentTest.php @@ -5,6 +5,7 @@ */ namespace Magento\Deploy\Test\Unit\Service; +use Magento\Deploy\Console\DeployStaticOptions; use Magento\Deploy\Package\Package; use Magento\Deploy\Process\Queue; use Magento\Deploy\Service\Bundle; @@ -221,4 +222,35 @@ public function deployDataProvider() ] ]; } + + public function testMaxExecutionTimeOptionPassed() + { + $options = [ + DeployStaticOptions::MAX_EXECUTION_TIME => 100, + DeployStaticOptions::REFRESH_CONTENT_VERSION_ONLY => false, + DeployStaticOptions::JOBS_AMOUNT => 3, + DeployStaticOptions::STRATEGY => 'compact', + DeployStaticOptions::NO_JAVASCRIPT => true, + DeployStaticOptions::NO_HTML_MINIFY => true, + ]; + + $queueMock = $this->createMock(Queue::class); + $strategyMock = $this->createMock(CompactDeploy::class); + $this->queueFactory->expects($this->once()) + ->method('create') + ->with([ + 'logger' => $this->logger, + 'maxExecTime' => 100, + 'maxProcesses' => 3, + 'options' => $options, + 'deployPackageService' => null + ]) + ->willReturn($queueMock); + $this->deployStrategyFactory->expects($this->once()) + ->method('create') + ->with('compact', ['queue' => $queueMock]) + ->willReturn($strategyMock); + + $this->service->deploy($options); + } } diff --git a/app/code/Magento/Deploy/etc/di.xml b/app/code/Magento/Deploy/etc/di.xml index ce7c84c95538..0c32baebf12d 100644 --- a/app/code/Magento/Deploy/etc/di.xml +++ b/app/code/Magento/Deploy/etc/di.xml @@ -71,15 +71,4 @@ - - - - - - 0 - - - - - diff --git a/app/code/Magento/Developer/Console/Command/GeneratePatchCommand.php b/app/code/Magento/Developer/Console/Command/GeneratePatchCommand.php index cc3760b6846d..d224c13b5a6b 100644 --- a/app/code/Magento/Developer/Console/Command/GeneratePatchCommand.php +++ b/app/code/Magento/Developer/Console/Command/GeneratePatchCommand.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Developer\Console\Command; @@ -25,7 +26,7 @@ class GeneratePatchCommand extends Command /** * Command arguments and options */ - const COMMAND_NAME = 'dev:generate:patch'; + const COMMAND_NAME = 'setup:db-declaration:generate-patch'; const MODULE_NAME = 'module'; const INPUT_KEY_IS_REVERTABLE = 'revertable'; const INPUT_KEY_PATCH_TYPE = 'type'; @@ -47,7 +48,9 @@ public function __construct(ComponentRegistrar $componentRegistrar) } /** - * {@inheritdoc} + * Configure command + * + * @inheritdoc * @throws InvalidArgumentException */ protected function configure() @@ -89,16 +92,16 @@ protected function configure() * * @return string */ - private function getPatchTemplate() + private function getPatchTemplate() : string { return file_get_contents(__DIR__ . '/patch_template.php.dist'); } /** - * {@inheritdoc} + * @inheritdoc * @throws \InvalidArgumentException */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output) : int { $moduleName = $input->getArgument(self::MODULE_NAME); $patchName = $input->getArgument(self::INPUT_KEY_PATCH_NAME); diff --git a/app/code/Magento/Developer/Console/Command/SourceThemeDeployCommand.php b/app/code/Magento/Developer/Console/Command/SourceThemeDeployCommand.php index 25519e5c8305..1680aee38dcd 100644 --- a/app/code/Magento/Developer/Console/Command/SourceThemeDeployCommand.php +++ b/app/code/Magento/Developer/Console/Command/SourceThemeDeployCommand.php @@ -5,7 +5,6 @@ */ namespace Magento\Developer\Console\Command; -use Magento\Framework\App\State; use Magento\Framework\Validator\Locale; use Magento\Framework\View\Asset\Repository; use Symfony\Component\Console\Command\Command; @@ -81,7 +80,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ protected function configure() { @@ -129,7 +128,7 @@ protected function configure() } /** - * {@inheritdoc} + * @inheritdoc * @throws \InvalidArgumentException */ protected function execute(InputInterface $input, OutputInterface $output) diff --git a/app/code/Magento/Developer/Console/Command/TablesWhitelistGenerateCommand.php b/app/code/Magento/Developer/Console/Command/TablesWhitelistGenerateCommand.php index d283e693b8de..2155efa01709 100644 --- a/app/code/Magento/Developer/Console/Command/TablesWhitelistGenerateCommand.php +++ b/app/code/Magento/Developer/Console/Command/TablesWhitelistGenerateCommand.php @@ -3,14 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Developer\Console\Command; -use Magento\Framework\Component\ComponentRegistrar; +use Magento\Developer\Model\Setup\Declaration\Schema\WhitelistGenerator; use Magento\Framework\Config\FileResolverByModule; -use Magento\Framework\Module\Dir; -use Magento\Framework\Setup\Declaration\Schema\Diff\Diff; -use Magento\Framework\Setup\JsonPersistor; -use Magento\Framework\Setup\Declaration\Schema\Declaration\ReaderComposite; +use Magento\Framework\Exception\ConfigurationMismatchException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -18,6 +17,7 @@ /** * Command that allows to generate whitelist, that will be used, when declaration data is installed. + * * If whitelist already exists, new values will be added to existing whitelist. */ class TablesWhitelistGenerateCommand extends Command @@ -28,41 +28,20 @@ class TablesWhitelistGenerateCommand extends Command const MODULE_NAME_KEY = 'module-name'; /** - * @var ComponentRegistrar - */ - private $componentRegistrar; - - /** - * @var ReaderComposite - */ - private $readerComposite; - - /** - * @var JsonPersistor - */ - private $jsonPersistor; - - /** - * @var array + * @var WhitelistGenerator */ - private $primaryDbSchema; + private $whitelistGenerator; /** - * @param ComponentRegistrar $componentRegistrar - * @param ReaderComposite $readerComposite - * @param JsonPersistor $jsonPersistor + * @param WhitelistGenerator $whitelistGenerator * @param string|null $name */ public function __construct( - ComponentRegistrar $componentRegistrar, - ReaderComposite $readerComposite, - JsonPersistor $jsonPersistor, + WhitelistGenerator $whitelistGenerator, $name = null ) { + $this->whitelistGenerator = $whitelistGenerator; parent::__construct($name); - $this->componentRegistrar = $componentRegistrar; - $this->readerComposite = $readerComposite; - $this->jsonPersistor = $jsonPersistor; } /** @@ -72,7 +51,7 @@ public function __construct( */ protected function configure() { - $this->setName('declaration:generate:whitelist') + $this->setName('setup:db-declaration:generate-whitelist') ->setDescription( 'Generate whitelist of tables and columns that are allowed to be edited by declaration installer' ) @@ -90,116 +69,22 @@ protected function configure() parent::configure(); } - /** - * Update whitelist tables for all modules that are enabled on the moment. - * - * @param string $moduleName - * @return void - */ - private function persistModule($moduleName) - { - $content = []; - $modulePath = $this->componentRegistrar->getPath('module', $moduleName); - $whiteListFileName = $modulePath - . DIRECTORY_SEPARATOR - . Dir::MODULE_ETC_DIR - . DIRECTORY_SEPARATOR - . Diff::GENERATED_WHITELIST_FILE_NAME; - //We need to load whitelist file and update it with new revision of code. - if (file_exists($whiteListFileName)) { - $content = json_decode(file_get_contents($whiteListFileName), true); - } - - $newContent = $this->filterPrimaryTables($this->readerComposite->read($moduleName)); - - //Do merge between what we have before, and what we have now and filter to only certain attributes. - $content = array_replace_recursive( - $content, - $this->filterAttributeNames($newContent) - ); - if (!empty($content)) { - $this->jsonPersistor->persist($content, $whiteListFileName); - } - } - /** * @inheritdoc */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output) : int { $moduleName = $input->getOption(self::MODULE_NAME_KEY); try { - if ($moduleName === FileResolverByModule::ALL_MODULES) { - foreach (array_keys($this->componentRegistrar->getPaths('module')) as $moduleName) { - $this->persistModule($moduleName); - } - } else { - $this->persistModule($moduleName); - } + $this->whitelistGenerator->generate($moduleName); + } catch (ConfigurationMismatchException $e) { + $output->writeln($e->getMessage()); + return \Magento\Framework\Console\Cli::RETURN_FAILURE; } catch (\Exception $e) { return \Magento\Framework\Console\Cli::RETURN_FAILURE; } - //If script comes here, that we sucessfully write whitelist configuration return \Magento\Framework\Console\Cli::RETURN_SUCCESS; } - - /** - * As for whitelist we do not need any specific attributes like nullable or indexType, - * we need to choose only names. - * - * @param array $content - * @return array - */ - private function filterAttributeNames(array $content) - { - $names = []; - $types = ['column', 'index', 'constraint']; - - foreach ($content['table'] as $tableName => $tableContent) { - foreach ($types as $type) { - if (isset($tableContent[$type])) { - //Add elements to whitelist - foreach (array_keys($tableContent[$type]) as $elementName) { - //Depends on flag column will be whitelisted or not - $names[$tableName][$type][$elementName] = true; - } - } - } - } - - return $names; - } - - /** - * Load db_schema content from the primary scope app/etc/db_schema.xml. - * - * @return array - */ - private function getPrimaryDbSchema() - { - if (!$this->primaryDbSchema) { - $this->primaryDbSchema = $this->readerComposite->read('primary'); - } - return $this->primaryDbSchema; - } - - /** - * Filter tables from module db_schema.xml as they should not contain the primary system tables. - * - * @param array $moduleDbSchema - * @return array - * @SuppressWarnings(PHPMD.UnusedLocalVariable) - */ - private function filterPrimaryTables(array $moduleDbSchema) - { - $primaryDbSchema = $this->getPrimaryDbSchema(); - if (isset($moduleDbSchema['table']) && isset($primaryDbSchema['table'])) { - foreach ($primaryDbSchema['table'] as $tableNameKey => $tableContents) { - unset($moduleDbSchema['table'][$tableNameKey]); - } - } - return $moduleDbSchema; - } } diff --git a/app/code/Magento/Developer/Model/Logger/Handler/Debug.php b/app/code/Magento/Developer/Model/Logger/Handler/Debug.php index 9bfee42fa6a8..ba98524bb665 100644 --- a/app/code/Magento/Developer/Model/Logger/Handler/Debug.php +++ b/app/code/Magento/Developer/Model/Logger/Handler/Debug.php @@ -5,10 +5,10 @@ */ namespace Magento\Developer\Model\Logger\Handler; +use Magento\Config\Setup\ConfigOptionsList; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\State; use Magento\Framework\Filesystem\DriverInterface; -use Magento\Store\Model\ScopeInterface; use Magento\Framework\App\DeploymentConfig; /** @@ -37,6 +37,7 @@ class Debug extends \Magento\Framework\Logger\Handler\Debug * @param ScopeConfigInterface $scopeConfig * @param DeploymentConfig $deploymentConfig * @param string $filePath + * @throws \Exception */ public function __construct( DriverInterface $filesystem, @@ -53,16 +54,32 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function isHandling(array $record) { if ($this->deploymentConfig->isAvailable()) { return parent::isHandling($record) - && $this->scopeConfig->getValue('dev/debug/debug_logging', ScopeInterface::SCOPE_STORE); + && $this->isLoggingEnabled(); } return parent::isHandling($record); } + + /** + * Check that logging functionality is enabled. + * + * @return bool + */ + private function isLoggingEnabled(): bool + { + $configValue = $this->deploymentConfig->get(ConfigOptionsList::CONFIG_PATH_DEBUG_LOGGING); + if ($configValue === null) { + $isEnabled = $this->state->getMode() !== State::MODE_PRODUCTION; + } else { + $isEnabled = (bool)$configValue; + } + return $isEnabled; + } } diff --git a/app/code/Magento/Developer/Model/Logger/Handler/Syslog.php b/app/code/Magento/Developer/Model/Logger/Handler/Syslog.php index 6dc276a696f6..3f5ff5864031 100644 --- a/app/code/Magento/Developer/Model/Logger/Handler/Syslog.php +++ b/app/code/Magento/Developer/Model/Logger/Handler/Syslog.php @@ -7,22 +7,19 @@ namespace Magento\Developer\Model\Logger\Handler; +use Magento\Config\Setup\ConfigOptionsList; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\DeploymentConfig; /** - * Enable/disable syslog logging based on the store config setting. + * Enable/disable syslog logging based on the deployment config setting. */ class Syslog extends \Magento\Framework\Logger\Handler\Syslog { - public const CONFIG_PATH = 'dev/syslog/syslog_logging'; - /** - * Scope config. - * - * @var ScopeConfigInterface + * @deprecated configuration value has been removed. */ - private $scopeConfig; + public const CONFIG_PATH = 'dev/syslog/syslog_logging'; /** * Deployment config. @@ -35,6 +32,7 @@ class Syslog extends \Magento\Framework\Logger\Handler\Syslog * @param ScopeConfigInterface $scopeConfig Scope config * @param DeploymentConfig $deploymentConfig Deployment config * @param string $ident The string ident to be added to each message + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( ScopeConfigInterface $scopeConfig, @@ -42,8 +40,6 @@ public function __construct( string $ident ) { parent::__construct($ident); - - $this->scopeConfig = $scopeConfig; $this->deploymentConfig = $deploymentConfig; } @@ -53,7 +49,18 @@ public function __construct( public function isHandling(array $record): bool { return parent::isHandling($record) - && $this->deploymentConfig->isAvailable() - && $this->scopeConfig->getValue(self::CONFIG_PATH); + && $this->deploymentConfig->isDbAvailable() + && $this->isLoggingEnabled(); + } + + /** + * Check that logging functionality is enabled. + * + * @return bool + */ + private function isLoggingEnabled(): bool + { + $configValue = $this->deploymentConfig->get(ConfigOptionsList::CONFIG_PATH_SYSLOG_LOGGING); + return (bool)$configValue; } } diff --git a/app/code/Magento/Developer/Model/Setup/Declaration/Schema/WhitelistGenerator.php b/app/code/Magento/Developer/Model/Setup/Declaration/Schema/WhitelistGenerator.php new file mode 100644 index 000000000000..b752eaa111fa --- /dev/null +++ b/app/code/Magento/Developer/Model/Setup/Declaration/Schema/WhitelistGenerator.php @@ -0,0 +1,263 @@ +componentRegistrar = $componentRegistrar; + $this->jsonPersistor = $jsonPersistor; + $this->schemaConfig = $schemaConfig; + $this->readerComposite = $readerComposite; + $this->elementNameResolver = $elementNameResolver; + $this->deploymentConfig = $deploymentConfig; + } + + /** + * Generate whitelist declaration. + * + * @param string $moduleName + * @throws ConfigurationMismatchException + */ + public function generate(string $moduleName) + { + $this->checkMagentoInstallation(); + $schema = $this->schemaConfig->getDeclarationConfig(); + if ($moduleName === FileResolverByModule::ALL_MODULES) { + foreach (array_keys($this->componentRegistrar->getPaths('module')) as $moduleName) { + $this->persistModule($schema, $moduleName); + } + } else { + $this->persistModule($schema, $moduleName); + } + } + + /** + * Check the configuration of the installed instance. + * + * @throws ConfigurationMismatchException + */ + private function checkMagentoInstallation() + { + $tablePrefixLength = $this->deploymentConfig->get(ConfigOptionsListConstants::CONFIG_PATH_DB_PREFIX); + if ($tablePrefixLength) { + throw new ConfigurationMismatchException( + __('Magento was installed with a table prefix. Please re-install without prefix.') + ); + } + } + + /** + * Update whitelist tables for all modules that are enabled on the moment. + * + * @param Schema $schema + * @param string $moduleName + * @return void + */ + private function persistModule(Schema $schema, string $moduleName) + { + $content = []; + $modulePath = $this->componentRegistrar->getPath('module', $moduleName); + $whiteListFileName = $modulePath + . DIRECTORY_SEPARATOR + . Dir::MODULE_ETC_DIR + . DIRECTORY_SEPARATOR + . Diff::GENERATED_WHITELIST_FILE_NAME; + + //We need to load whitelist file and update it with new revision of code. + if (file_exists($whiteListFileName)) { + $content = json_decode(file_get_contents($whiteListFileName), true); + } + + $data = $this->filterPrimaryTables($this->readerComposite->read($moduleName)); + if (!empty($data['table'])) { + foreach ($data['table'] as $tableName => $tabledata) { + //Do merge between what we have before, and what we have now and filter to only certain attributes. + $content = array_replace_recursive( + $content, + [$tableName => $this->getElementsWithFixedName($tabledata)], + [$tableName => $this->getElementsWithAutogeneratedName( + $schema, + $tableName, + $tabledata + )] + ); + } + if (!empty($content)) { + $this->jsonPersistor->persist($content, $whiteListFileName); + } + } + } + + /** + * Provide immutable names of the table elements. + * + * @param array $tableData + * @return array + */ + private function getElementsWithFixedName(array $tableData): array + { + $declaredStructure = []; + if (!empty($tableData['column'])) { + $declaredColumns = array_keys($tableData['column']); + $declaredStructure['column'] = array_fill_keys($declaredColumns, true); + } + return $declaredStructure; + } + + /** + * Provide autogenerated names of the table elements. + * + * @param Schema $schema + * @param string $tableName + * @param array $tableData + * @return array + */ + private function getElementsWithAutogeneratedName(Schema $schema, string $tableName, array $tableData) : array + { + $declaredStructure = []; + $table = $schema->getTableByName($tableName); + + $elementType = 'index'; + if (!empty($tableData[$elementType])) { + foreach ($tableData[$elementType] as $tableElementData) { + $indexName = $this->elementNameResolver->getFullIndexName( + $table, + $tableElementData['column'], + $tableElementData['indexType'] ?? null + ); + $declaredStructure[$elementType][$indexName] = true; + } + } + + $elementType = 'constraint'; + if (!empty($tableData[$elementType])) { + foreach ($tableData[$elementType] as $tableElementData) { + if ($tableElementData['type'] === 'foreign') { + $referenceTable = $schema->getTableByName($tableElementData['referenceTable']); + $column = $table->getColumnByName($tableElementData['column']); + $referenceColumn = $referenceTable->getColumnByName($tableElementData['referenceColumn']); + $constraintName = ($column !== false && $referenceColumn !== false) ? + $this->elementNameResolver->getFullFKName( + $table, + $column, + $referenceTable, + $referenceColumn + ) : null; + } else { + $constraintName = $this->elementNameResolver->getFullIndexName( + $table, + $tableElementData['column'], + $tableElementData['type'] + ); + } + if ($constraintName) { + $declaredStructure[$elementType][$constraintName] = true; + } + } + } + + return $declaredStructure; + } + + /** + * Load db_schema content from the primary scope app/etc/db_schema.xml. + * + * @return array + */ + private function getPrimaryDbSchema(): array + { + if (!$this->primaryDbSchema) { + $this->primaryDbSchema = $this->readerComposite->read('primary'); + } + return $this->primaryDbSchema; + } + + /** + * Filter tables from module database schema as they should not contain the primary system tables. + * + * @param array $moduleDbSchema + * @return array + */ + private function filterPrimaryTables(array $moduleDbSchema): array + { + $primaryDbSchema = $this->getPrimaryDbSchema(); + if (isset($moduleDbSchema['table']) && isset($primaryDbSchema['table'])) { + foreach (array_keys($primaryDbSchema['table']) as $tableNameKey) { + unset($moduleDbSchema['table'][$tableNameKey]); + } + } + return $moduleDbSchema; + } +} diff --git a/app/code/Magento/Developer/Test/Unit/Console/Command/TablesWhitelistGenerateCommandTest.php b/app/code/Magento/Developer/Test/Unit/Console/Command/TablesWhitelistGenerateCommandTest.php deleted file mode 100644 index 5bfc5686b05f..000000000000 --- a/app/code/Magento/Developer/Test/Unit/Console/Command/TablesWhitelistGenerateCommandTest.php +++ /dev/null @@ -1,425 +0,0 @@ -componentRegistrarMock = $this->getMockBuilder(ComponentRegistrar::class) - ->disableOriginalConstructor() - ->getMock(); - $this->readerCompositeMock = $this->getMockBuilder(ReaderComposite::class) - ->disableOriginalConstructor() - ->getMock(); - $this->jsonPersistorMock = $this->getMockBuilder(JsonPersistor::class) - ->getMock(); - $this->objectManagerHelper = new ObjectManagerHelper($this); - $this->model = $this->objectManagerHelper->getObject( - TablesWhitelistGenerateCommand::class, - [ - 'componentRegistrar' => $this->componentRegistrarMock, - 'readerComposite' => $this->readerCompositeMock, - 'jsonPersistor' => $this->jsonPersistorMock - ] - ); - } - - /** - * @return array - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function whitelistTableProvider() - { - return [ - [ - 'moduleName' => 'SomeModule', - 'whitelist' => [ - 'primary' => [ - 'table' => - [ - 'patch_list' => - [ - 'column' => - [ - 'patch_id' => - [ - 'type' => 'int', - 'name' => 'patch_id', - 'identity' => 'true', - 'comment' => 'Patch Auto Increment', - ], - 'patch_name' => - [ - 'type' => 'varchar', - 'name' => 'patch_name', - 'length' => '1024', - 'nullable' => 'false', - 'comment' => 'Patch Class Name', - ], - ], - 'constraint' => - [ - 'PRIMARY' => - [ - 'column' => - [ - 'patch_id' => 'patch_id', - ], - 'type' => 'primary', - 'name' => 'PRIMARY', - ], - ], - 'name' => 'patch_list', - 'resource' => 'default', - 'comment' => 'List of data/schema patches', - ], - ], - ], - 'SomeModule' => [ - 'table' => [ - 'first_table' => [ - 'disabled' => false, - 'name' => 'first_table', - 'resource' => 'default', - 'engine' => 'innodb', - 'column' => [ - 'first_column' => [ - 'name' => 'first_column', - 'xsi:type' => 'integer', - 'nullable' => 1, - 'unsigned' => '0', - ], - 'second_column' => [ - 'name' => 'second_column', - 'xsi:type' => 'date', - 'nullable' => 0, - ] - ], - 'index' => [ - 'TEST_INDEX' => [ - 'name' => 'TEST_INDEX', - 'indexType' => 'btree', - 'columns' => [ - 'first_column' - ] - ] - ], - 'constraint' => [ - 'foreign' => [ - 'some_foreign_constraint' => [ - 'referenceTable' => 'table', - 'referenceColumn' => 'column', - 'table' => 'first_table', - 'column' => 'first_column' - ] - ], - 'primary' => [ - 'PRIMARY' => [ - 'xsi:type' => 'primary', - 'name' => 'PRIMARY', - 'columns' => [ - 'second_column' - ] - ] - ] - ] - ] - ] - ], - ], - 'expected' => [ - 'SomeModule' => [ - 'first_table' => [ - 'column' => [ - 'first_column' => true, - 'second_column' => true, - ], - 'index' => [ - 'TEST_INDEX' => true, - ], - 'constraint' => [ - 'foreign' => true, - 'primary' => true, - ] - ] - ] - ] - ], - [ - 'moduleName' => false, - 'whitelist' => [ - 'primary' => [ - 'table' => - [ - 'patch_list' => - [ - 'column' => - [ - 'patch_id' => - [ - 'type' => 'int', - 'name' => 'patch_id', - 'identity' => 'true', - 'comment' => 'Patch Auto Increment', - ], - 'patch_name' => - [ - 'type' => 'varchar', - 'name' => 'patch_name', - 'length' => '1024', - 'nullable' => 'false', - 'comment' => 'Patch Class Name', - ], - ], - 'constraint' => - [ - 'PRIMARY' => - [ - 'column' => - [ - 'patch_id' => 'patch_id', - ], - 'type' => 'primary', - 'name' => 'PRIMARY', - ], - ], - 'name' => 'patch_list', - 'resource' => 'default', - 'comment' => 'List of data/schema patches', - ], - ], - ], - 'SomeModule' => [ - 'table' => [ - 'first_table' => [ - 'disabled' => false, - 'name' => 'first_table', - 'resource' => 'default', - 'engine' => 'innodb', - 'column' => [ - 'first_column' => [ - 'name' => 'first_column', - 'xsi:type' => 'integer', - 'nullable' => 1, - 'unsigned' => '0', - ], - 'second_column' => [ - 'name' => 'second_column', - 'xsi:type' => 'date', - 'nullable' => 0, - ] - ], - 'index' => [ - 'TEST_INDEX' => [ - 'name' => 'TEST_INDEX', - 'indexType' => 'btree', - 'columns' => [ - 'first_column' - ] - ] - ], - 'constraint' => [ - 'foreign' => [ - 'some_foreign_constraint' => [ - 'referenceTable' => 'table', - 'referenceColumn' => 'column', - 'table' => 'first_table', - 'column' => 'first_column' - ] - ], - 'primary' => [ - 'PRIMARY' => [ - 'xsi:type' => 'primary', - 'name' => 'PRIMARY', - 'columns' => [ - 'second_column' - ] - ] - ] - ] - ] - ] - ], - 'Module2' => [ - 'table' => [ - 'second_table' => [ - 'disabled' => false, - 'name' => 'second_table', - 'resource' => 'default', - 'engine' => 'innodb', - 'column' => [ - 'first_column' => [ - 'name' => 'first_column', - 'xsi:type' => 'integer', - 'nullable' => 1, - 'unsigned' => '0', - ], - 'second_column' => [ - 'name' => 'second_column', - 'xsi:type' => 'date', - 'nullable' => 0, - ] - ], - 'index' => [ - 'TEST_INDEX' => [ - 'name' => 'TEST_INDEX', - 'indexType' => 'btree', - 'columns' => [ - 'first_column' - ] - ] - ], - 'constraint' => [ - 'foreign' => [ - 'some_foreign_constraint' => [ - 'referenceTable' => 'table', - 'referenceColumn' => 'column', - 'table' => 'second_table', - 'column' => 'first_column' - ] - ], - 'primary' => [ - 'PRIMARY' => [ - 'xsi:type' => 'primary', - 'name' => 'PRIMARY', - 'columns' => [ - 'second_column' - ] - ] - ] - ] - ] - ] - ] - ], - 'expected' => [ - 'SomeModule' => [ - 'first_table' => [ - 'column' => [ - 'first_column' => true, - 'second_column' => true, - ], - 'index' => [ - 'TEST_INDEX' => true, - ], - 'constraint' => [ - 'foreign' => true, - 'primary' => true, - ] - ] - ], - 'Module2' => [ - 'second_table' => [ - 'column' => [ - 'first_column' => true, - 'second_column' => true, - ], - 'index' => [ - 'TEST_INDEX' => true, - ], - 'constraint' => [ - 'foreign' => true, - 'primary' => true, - ] - ] - ] - ] - ] - ]; - } - - /** - * @dataProvider whitelistTableProvider - * @param string $moduleName - * @param array $whiteListTables - * @param array $expected - */ - public function testCommand($moduleName, array $whiteListTables, array $expected) - { - $commandTester = new CommandTester($this->model); - $options = !$moduleName ? [] : ['--module-name' => $moduleName]; - - if (!$moduleName) { - $this->componentRegistrarMock->expects(self::once()) - ->method('getPaths') - ->willReturn(['SomeModule' => 1, 'Module2' => 2]); - $this->readerCompositeMock->expects(self::exactly(3)) - ->method('read') - ->withConsecutive(['SomeModule'], ['primary'], ['Module2']) - ->willReturnOnConsecutiveCalls( - $whiteListTables['SomeModule'], - $whiteListTables['primary'], - $whiteListTables['Module2'] - ); - $this->jsonPersistorMock->expects(self::exactly(2)) - ->method('persist') - ->withConsecutive( - [ - $expected['SomeModule'], - '/etc/db_schema_whitelist.json' - ], - [ - $expected['Module2'], - '/etc/db_schema_whitelist.json' - ] - ); - } else { - $this->readerCompositeMock->expects(self::exactly(2)) - ->method('read') - ->withConsecutive([$moduleName], ['primary']) - ->willReturnOnConsecutiveCalls($whiteListTables['SomeModule'], $whiteListTables['primary']); - $this->jsonPersistorMock->expects(self::once()) - ->method('persist') - ->with( - $expected['SomeModule'], - '/etc/db_schema_whitelist.json' - ); - } - $commandTester->execute($options); - } -} diff --git a/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/DebugTest.php b/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/DebugTest.php index c116775d582b..1c729c933ec1 100644 --- a/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/DebugTest.php +++ b/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/DebugTest.php @@ -51,6 +51,9 @@ class DebugTest extends \PHPUnit\Framework\TestCase */ private $deploymentConfigMock; + /** + * @inheritdoc + */ protected function setUp() { $this->filesystemMock = $this->getMockBuilder(DriverInterface::class) @@ -80,70 +83,95 @@ protected function setUp() $this->model->setFormatter($this->formatterMock); } - public function testHandle() + /** + * @return void + */ + public function testHandleEnabledInDeveloperMode() { $this->deploymentConfigMock->expects($this->once()) ->method('isAvailable') ->willReturn(true); - $this->stateMock->expects($this->never()) - ->method('getMode'); - $this->scopeConfigMock->expects($this->once()) - ->method('getValue') - ->with('dev/debug/debug_logging', ScopeInterface::SCOPE_STORE, null) - ->willReturn(true); + $this->stateMock + ->expects($this->once()) + ->method('getMode') + ->willReturn(State::MODE_DEVELOPER); + $this->scopeConfigMock + ->expects($this->never()) + ->method('getValue'); $this->assertTrue($this->model->isHandling(['formatted' => false, 'level' => Logger::DEBUG])); } - public function testHandleDisabledByProduction() + /** + * @return void + */ + public function testHandleEnabledInDefaultMode() { $this->deploymentConfigMock->expects($this->once()) ->method('isAvailable') ->willReturn(true); - $this->stateMock->expects($this->never()) - ->method('getMode'); - $this->scopeConfigMock->expects($this->once()) + $this->stateMock + ->expects($this->once()) + ->method('getMode') + ->willReturn(State::MODE_DEFAULT); + $this->scopeConfigMock + ->expects($this->never()) ->method('getValue'); - $this->assertFalse($this->model->isHandling(['formatted' => false, 'level' => Logger::DEBUG])); + $this->assertTrue($this->model->isHandling(['formatted' => false, 'level' => Logger::DEBUG])); } - public function testHandleDisabledByConfig() + /** + * @return void + */ + public function testHandleDisabledByProduction() { $this->deploymentConfigMock->expects($this->once()) ->method('isAvailable') ->willReturn(true); - $this->stateMock->expects($this->never()) - ->method('getMode'); - $this->scopeConfigMock->expects($this->once()) - ->method('getValue') - ->with('dev/debug/debug_logging', ScopeInterface::SCOPE_STORE, null) - ->willReturn(false); + $this->stateMock + ->expects($this->once()) + ->method('getMode') + ->willReturn(State::MODE_PRODUCTION); + $this->scopeConfigMock + ->expects($this->never()) + ->method('getValue'); $this->assertFalse($this->model->isHandling(['formatted' => false, 'level' => Logger::DEBUG])); } + /** + * @return void + */ public function testHandleDisabledByLevel() { $this->deploymentConfigMock->expects($this->once()) ->method('isAvailable') ->willReturn(true); - $this->stateMock->expects($this->never()) - ->method('getMode'); - $this->scopeConfigMock->expects($this->never()) + $this->stateMock + ->expects($this->never()) + ->method('getMode') + ->willReturn(State::MODE_DEVELOPER); + $this->scopeConfigMock + ->expects($this->never()) ->method('getValue'); $this->assertFalse($this->model->isHandling(['formatted' => false, 'level' => Logger::API])); } + /** + * @return void + */ public function testDeploymentConfigIsNotAvailable() { $this->deploymentConfigMock->expects($this->once()) ->method('isAvailable') ->willReturn(false); - $this->stateMock->expects($this->never()) + $this->stateMock + ->expects($this->never()) ->method('getMode'); - $this->scopeConfigMock->expects($this->never()) + $this->scopeConfigMock + ->expects($this->never()) ->method('getValue'); $this->assertTrue($this->model->isHandling(['formatted' => false, 'level' => Logger::DEBUG])); diff --git a/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/SyslogTest.php b/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/SyslogTest.php index c744645b670b..06c19d3f2e83 100644 --- a/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/SyslogTest.php +++ b/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/SyslogTest.php @@ -7,6 +7,7 @@ namespace Magento\Developer\Test\Unit\Model\Logger\Handler; +use Magento\Config\Setup\ConfigOptionsList; use Magento\Developer\Model\Logger\Handler\Syslog; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\DeploymentConfig; @@ -34,6 +35,9 @@ class SyslogTest extends TestCase */ private $deploymentConfigMock; + /** + * @inheritdoc + */ protected function setUp() { $this->scopeConfigMock = $this->getMockForAbstractClass(ScopeConfigInterface::class); @@ -46,35 +50,48 @@ protected function setUp() ); } + /** + * @return void + */ public function testIsHandling(): void { $record = [ 'level' => Monolog::DEBUG, ]; - $this->scopeConfigMock->expects($this->once()) - ->method('getValue') - ->with(Syslog::CONFIG_PATH) - ->willReturn('1'); - $this->deploymentConfigMock->expects($this->once()) - ->method('isAvailable') + $this->scopeConfigMock + ->expects($this->never()) + ->method('getValue'); + $this->deploymentConfigMock + ->expects($this->once()) + ->method('isDbAvailable') ->willReturn(true); + $this->deploymentConfigMock + ->expects($this->once()) + ->method('get') + ->with(ConfigOptionsList::CONFIG_PATH_SYSLOG_LOGGING) + ->willReturn(1); $this->assertTrue( $this->model->isHandling($record) ); } + /** + * @return void + */ public function testIsHandlingNotInstalled(): void { $record = [ 'level' => Monolog::DEBUG, ]; - $this->scopeConfigMock->expects($this->never()) + $this->scopeConfigMock + ->expects($this->never()) ->method('getValue'); - $this->deploymentConfigMock->expects($this->once()) - ->method('isAvailable') + $this->deploymentConfigMock + ->expects($this->once()) + ->method('isDbAvailable') ->willReturn(false); $this->assertFalse( @@ -82,19 +99,27 @@ public function testIsHandlingNotInstalled(): void ); } + /** + * @return void + */ public function testIsHandlingDisabled(): void { $record = [ 'level' => Monolog::DEBUG, ]; - $this->scopeConfigMock->expects($this->once()) - ->method('getValue') - ->with(Syslog::CONFIG_PATH) - ->willReturn('0'); - $this->deploymentConfigMock->expects($this->once()) - ->method('isAvailable') + $this->scopeConfigMock + ->expects($this->never()) + ->method('getValue'); + $this->deploymentConfigMock + ->expects($this->once()) + ->method('isDbAvailable') ->willReturn(true); + $this->deploymentConfigMock + ->expects($this->once()) + ->method('get') + ->with(ConfigOptionsList::CONFIG_PATH_SYSLOG_LOGGING) + ->willReturn(0); $this->assertFalse( $this->model->isHandling($record) diff --git a/app/code/Magento/Developer/etc/adminhtml/system.xml b/app/code/Magento/Developer/etc/adminhtml/system.xml index 4ebc45f1a2ca..c64abd6eae72 100644 --- a/app/code/Magento/Developer/etc/adminhtml/system.xml +++ b/app/code/Magento/Developer/etc/adminhtml/system.xml @@ -25,20 +25,6 @@ Magento\Developer\Model\Config\Backend\AllowedIps - - - - Not available in production mode. - Magento\Config\Model\Config\Source\Yesno - - - - - - - Magento\Config\Model\Config\Source\Yesno - -
diff --git a/app/code/Magento/Developer/etc/di.xml b/app/code/Magento/Developer/etc/di.xml index 21ecf10c1b1e..98adcbb3a829 100644 --- a/app/code/Magento/Developer/etc/di.xml +++ b/app/code/Magento/Developer/etc/di.xml @@ -240,4 +240,12 @@ + + + + dev/debug/template_hints_storefront + dev/debug/template_hints_storefront_show_with_parameter + dev/debug/template_hints_parameter_value + + diff --git a/app/code/Magento/Developer/etc/frontend/di.xml b/app/code/Magento/Developer/etc/frontend/di.xml index 329c158d897a..aa4b34726020 100644 --- a/app/code/Magento/Developer/etc/frontend/di.xml +++ b/app/code/Magento/Developer/etc/frontend/di.xml @@ -9,11 +9,4 @@ - - - dev/debug/template_hints_storefront - dev/debug/template_hints_storefront_show_with_parameter - dev/debug/template_hints_parameter_value - - diff --git a/app/code/Magento/Developer/i18n/de_DE.csv b/app/code/Magento/Developer/i18n/de_DE.csv deleted file mode 100644 index e9c858101b71..000000000000 --- a/app/code/Magento/Developer/i18n/de_DE.csv +++ /dev/null @@ -1,5 +0,0 @@ -"Front-end development workflow","Front-end development workflow" -"Workflow type","Workflow type" -"Server side less compilation","Server side less compilation" -"Client side less compilation","Client side less compilation" -"Not available in production mode","Not available in production mode" diff --git a/app/code/Magento/Developer/i18n/es_ES.csv b/app/code/Magento/Developer/i18n/es_ES.csv deleted file mode 100644 index e9c858101b71..000000000000 --- a/app/code/Magento/Developer/i18n/es_ES.csv +++ /dev/null @@ -1,5 +0,0 @@ -"Front-end development workflow","Front-end development workflow" -"Workflow type","Workflow type" -"Server side less compilation","Server side less compilation" -"Client side less compilation","Client side less compilation" -"Not available in production mode","Not available in production mode" diff --git a/app/code/Magento/Developer/i18n/fr_FR.csv b/app/code/Magento/Developer/i18n/fr_FR.csv deleted file mode 100644 index e9c858101b71..000000000000 --- a/app/code/Magento/Developer/i18n/fr_FR.csv +++ /dev/null @@ -1,5 +0,0 @@ -"Front-end development workflow","Front-end development workflow" -"Workflow type","Workflow type" -"Server side less compilation","Server side less compilation" -"Client side less compilation","Client side less compilation" -"Not available in production mode","Not available in production mode" diff --git a/app/code/Magento/Developer/i18n/nl_NL.csv b/app/code/Magento/Developer/i18n/nl_NL.csv deleted file mode 100644 index e9c858101b71..000000000000 --- a/app/code/Magento/Developer/i18n/nl_NL.csv +++ /dev/null @@ -1,5 +0,0 @@ -"Front-end development workflow","Front-end development workflow" -"Workflow type","Workflow type" -"Server side less compilation","Server side less compilation" -"Client side less compilation","Client side less compilation" -"Not available in production mode","Not available in production mode" diff --git a/app/code/Magento/Developer/i18n/pt_BR.csv b/app/code/Magento/Developer/i18n/pt_BR.csv deleted file mode 100644 index e9c858101b71..000000000000 --- a/app/code/Magento/Developer/i18n/pt_BR.csv +++ /dev/null @@ -1,5 +0,0 @@ -"Front-end development workflow","Front-end development workflow" -"Workflow type","Workflow type" -"Server side less compilation","Server side less compilation" -"Client side less compilation","Client side less compilation" -"Not available in production mode","Not available in production mode" diff --git a/app/code/Magento/Developer/i18n/zh_Hans_CN.csv b/app/code/Magento/Developer/i18n/zh_Hans_CN.csv deleted file mode 100644 index e9c858101b71..000000000000 --- a/app/code/Magento/Developer/i18n/zh_Hans_CN.csv +++ /dev/null @@ -1,5 +0,0 @@ -"Front-end development workflow","Front-end development workflow" -"Workflow type","Workflow type" -"Server side less compilation","Server side less compilation" -"Client side less compilation","Client side less compilation" -"Not available in production mode","Not available in production mode" diff --git a/app/code/Magento/Dhl/Model/Carrier.php b/app/code/Magento/Dhl/Model/Carrier.php index d997db6ac1a3..1ad8b79ad12f 100644 --- a/app/code/Magento/Dhl/Model/Carrier.php +++ b/app/code/Magento/Dhl/Model/Carrier.php @@ -7,6 +7,7 @@ namespace Magento\Dhl\Model; use Magento\Catalog\Model\Product\Type; +use Magento\Framework\App\ProductMetadataInterface; use Magento\Framework\Module\Dir; use Magento\Sales\Exception\DocumentValidationException; use Magento\Sales\Model\Order\Shipment; @@ -56,6 +57,13 @@ class Carrier extends \Magento\Dhl\Model\AbstractDhl implements \Magento\Shippin */ const CODE = 'dhl'; + /** + * DHL service prefixes used for message reference + */ + private const SERVICE_PREFIX_QUOTE = 'QUOT'; + private const SERVICE_PREFIX_SHIPVAL = 'SHIP'; + private const SERVICE_PREFIX_TRACKING = 'TRCK'; + /** * Rate request data * @@ -206,6 +214,11 @@ class Carrier extends \Magento\Dhl\Model\AbstractDhl implements \Magento\Shippin */ private $xmlValidator; + /** + * @var ProductMetadataInterface + */ + private $productMetadata; + /** * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory $rateErrorFactory @@ -232,7 +245,8 @@ class Carrier extends \Magento\Dhl\Model\AbstractDhl implements \Magento\Shippin * @param \Magento\Framework\Stdlib\DateTime $dateTime * @param \Magento\Framework\HTTP\ZendClientFactory $httpClientFactory * @param array $data - * @param \Magento\Dhl\Model\Validator\XmlValidator $xmlValidator + * @param \Magento\Dhl\Model\Validator\XmlValidator|null $xmlValidator + * @param ProductMetadataInterface|null $productMetadata * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -261,7 +275,8 @@ public function __construct( \Magento\Framework\Stdlib\DateTime $dateTime, \Magento\Framework\HTTP\ZendClientFactory $httpClientFactory, array $data = [], - \Magento\Dhl\Model\Validator\XmlValidator $xmlValidator = null + \Magento\Dhl\Model\Validator\XmlValidator $xmlValidator = null, + ProductMetadataInterface $productMetadata = null ) { $this->readFactory = $readFactory; $this->_carrierHelper = $carrierHelper; @@ -295,6 +310,8 @@ public function __construct( } $this->xmlValidator = $xmlValidator ?: \Magento\Framework\App\ObjectManager::getInstance()->get(XmlValidator::class); + $this->productMetadata = $productMetadata + ?: \Magento\Framework\App\ObjectManager::getInstance()->get(ProductMetadataInterface::class); } /** @@ -983,18 +1000,29 @@ protected function _getQuotesFromServer($request) protected function _buildQuotesRequestXml() { $rawRequest = $this->_rawRequest; - $xmlStr = '' . - ''; + 'xsi:schemaLocation="http://www.dhl.com DCT-req_global-2.0.xsd"/>'; + $xml = $this->_xmlElFactory->create(['data' => $xmlStr]); $nodeGetQuote = $xml->addChild('GetQuote', '', ''); $nodeRequest = $nodeGetQuote->addChild('Request'); $nodeServiceHeader = $nodeRequest->addChild('ServiceHeader'); - $nodeServiceHeader->addChild('SiteID', (string)$this->getConfigData('id')); - $nodeServiceHeader->addChild('Password', (string)$this->getConfigData('password')); + $nodeServiceHeader->addChild('MessageTime', $this->buildMessageTimestamp()); + $nodeServiceHeader->addChild( + 'MessageReference', + $this->buildMessageReference(self::SERVICE_PREFIX_QUOTE) + ); + $nodeServiceHeader->addChild('SiteID', (string) $this->getConfigData('id')); + $nodeServiceHeader->addChild('Password', (string) $this->getConfigData('password')); + + $nodeMetaData = $nodeRequest->addChild('MetaData'); + $nodeMetaData->addChild('SoftwareName', $this->buildSoftwareName()); + $nodeMetaData->addChild('SoftwareVersion', $this->buildSoftwareVersion()); $nodeFrom = $nodeGetQuote->addChild('From'); $nodeFrom->addChild('CountryCode', $rawRequest->getOrigCountryId()); @@ -1386,44 +1414,37 @@ protected function _doRequest() { $rawRequest = $this->_request; - $originRegion = $this->getCountryParams( - $this->_scopeConfig->getValue( - Shipment::XML_PATH_STORE_COUNTRY_ID, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $this->getStore() - ) - )->getRegion(); - - if (!$originRegion) { - throw new \Magento\Framework\Exception\LocalizedException(__('Wrong Region')); - } - - if ($originRegion == 'AM') { - $originRegion = ''; - } - $xmlStr = '' . - ''; + ' xsi:schemaLocation="http://www.dhl.com ship-val-global-req-6.0.xsd"' . + ' schemaVersion="6.0" />'; $xml = $this->_xmlElFactory->create(['data' => $xmlStr]); $nodeRequest = $xml->addChild('Request', '', ''); $nodeServiceHeader = $nodeRequest->addChild('ServiceHeader'); + $nodeServiceHeader->addChild('MessageTime', $this->buildMessageTimestamp()); + // MessageReference must be 28 to 32 chars. + $nodeServiceHeader->addChild( + 'MessageReference', + $this->buildMessageReference(self::SERVICE_PREFIX_SHIPVAL) + ); $nodeServiceHeader->addChild('SiteID', (string)$this->getConfigData('id')); $nodeServiceHeader->addChild('Password', (string)$this->getConfigData('password')); - if (!$originRegion) { - $xml->addChild('RequestedPickupTime', 'N', ''); - } - if ($originRegion !== 'AP') { - $xml->addChild('NewShipper', 'N', ''); + $originRegion = $this->getCountryParams( + $this->_scopeConfig->getValue( + Shipment::XML_PATH_STORE_COUNTRY_ID, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $this->getStore() + ) + )->getRegion(); + if ($originRegion) { + $xml->addChild('RegionCode', $originRegion, ''); } + $xml->addChild('RequestedPickupTime', 'N', ''); + $xml->addChild('NewShipper', 'N', ''); $xml->addChild('LanguageCode', 'EN', ''); $xml->addChild('PiecesEnabled', 'Y', ''); @@ -1465,8 +1486,9 @@ protected function _doRequest() } $nodeConsignee->addChild('City', $rawRequest->getRecipientAddressCity()); - if ($originRegion !== 'AP') { - $nodeConsignee->addChild('Division', $rawRequest->getRecipientAddressStateOrProvinceCode()); + $recipientAddressStateOrProvinceCode = $rawRequest->getRecipientAddressStateOrProvinceCode(); + if ($recipientAddressStateOrProvinceCode) { + $nodeConsignee->addChild('Division', $recipientAddressStateOrProvinceCode); } $nodeConsignee->addChild('PostalCode', $rawRequest->getRecipientAddressPostalCode()); $nodeConsignee->addChild('CountryCode', $rawRequest->getRecipientAddressCountryCode()); @@ -1510,15 +1532,13 @@ protected function _doRequest() $nodeReference->addChild('ReferenceType', 'St'); /** Shipment Details */ - $this->_shipmentDetails($xml, $rawRequest, $originRegion); + $this->_shipmentDetails($xml, $rawRequest); /** Shipper */ $nodeShipper = $xml->addChild('Shipper', '', ''); $nodeShipper->addChild('ShipperID', (string)$this->getConfigData('account')); $nodeShipper->addChild('CompanyName', $rawRequest->getShipperContactCompanyName()); - if ($originRegion !== 'AP') { - $nodeShipper->addChild('RegisteredAccount', (string)$this->getConfigData('account')); - } + $nodeShipper->addChild('RegisteredAccount', (string)$this->getConfigData('account')); $address = $rawRequest->getShipperAddressStreet1() . ' ' . $rawRequest->getShipperAddressStreet2(); $address = $this->string->split($address, 35, false, true); @@ -1531,8 +1551,9 @@ protected function _doRequest() } $nodeShipper->addChild('City', $rawRequest->getShipperAddressCity()); - if ($originRegion !== 'AP') { - $nodeShipper->addChild('Division', $rawRequest->getShipperAddressStateOrProvinceCode()); + $shipperAddressStateOrProvinceCode = $rawRequest->getShipperAddressStateOrProvinceCode(); + if ($shipperAddressStateOrProvinceCode) { + $nodeShipper->addChild('Division', $shipperAddressStateOrProvinceCode); } $nodeShipper->addChild('PostalCode', $rawRequest->getShipperAddressPostalCode()); $nodeShipper->addChild('CountryCode', $rawRequest->getShipperAddressCountryCode()); @@ -1584,19 +1605,13 @@ protected function _doRequest() * @return void * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ protected function _shipmentDetails($xml, $rawRequest, $originRegion = '') { $nodeShipmentDetails = $xml->addChild('ShipmentDetails', '', ''); $nodeShipmentDetails->addChild('NumberOfPieces', count($rawRequest->getPackages())); - if ($originRegion) { - $nodeShipmentDetails->addChild( - 'CurrencyCode', - $this->_storeManager->getWebsite($this->_request->getWebsiteId())->getBaseCurrencyCode() - ); - } - $nodePieces = $nodeShipmentDetails->addChild('Pieces', '', ''); /* @@ -1615,18 +1630,12 @@ protected function _shipmentDetails($xml, $rawRequest, $originRegion = '') } $nodePiece->addChild('PieceID', ++$i); $nodePiece->addChild('PackageType', $packageType); - $nodePiece->addChild('Weight', sprintf('%.1f', $package['params']['weight'])); + $nodePiece->addChild('Weight', sprintf('%.3f', $package['params']['weight'])); $params = $package['params']; if ($params['width'] && $params['length'] && $params['height']) { - if (!$originRegion) { - $nodePiece->addChild('Width', round($params['width'])); - $nodePiece->addChild('Height', round($params['height'])); - $nodePiece->addChild('Depth', round($params['length'])); - } else { - $nodePiece->addChild('Depth', round($params['length'])); - $nodePiece->addChild('Width', round($params['width'])); - $nodePiece->addChild('Height', round($params['height'])); - } + $nodePiece->addChild('Width', round($params['width'])); + $nodePiece->addChild('Height', round($params['height'])); + $nodePiece->addChild('Depth', round($params['length'])); } $content = []; foreach ($package['items'] as $item) { @@ -1635,58 +1644,40 @@ protected function _shipmentDetails($xml, $rawRequest, $originRegion = '') $nodePiece->addChild('PieceContents', substr(implode(',', $content), 0, 34)); } - if (!$originRegion) { - $nodeShipmentDetails->addChild('Weight', sprintf('%.1f', $rawRequest->getPackageWeight())); - $nodeShipmentDetails->addChild('WeightUnit', substr($this->_getWeightUnit(), 0, 1)); - $nodeShipmentDetails->addChild('GlobalProductCode', $rawRequest->getShippingMethod()); - $nodeShipmentDetails->addChild('LocalProductCode', $rawRequest->getShippingMethod()); - $nodeShipmentDetails->addChild('Date', $this->_coreDate->date('Y-m-d')); - $nodeShipmentDetails->addChild('Contents', 'DHL Parcel'); - /** - * The DoorTo Element defines the type of delivery service that applies to the shipment. - * The valid values are DD (Door to Door), DA (Door to Airport) , AA and DC (Door to - * Door non-compliant) - */ - $nodeShipmentDetails->addChild('DoorTo', 'DD'); - $nodeShipmentDetails->addChild('DimensionUnit', substr($this->_getDimensionUnit(), 0, 1)); - if ($package['params']['container'] == self::DHL_CONTENT_TYPE_NON_DOC) { - $packageType = 'CP'; - } - $nodeShipmentDetails->addChild('PackageType', $packageType); - if ($this->isDutiable($rawRequest->getOrigCountryId(), $rawRequest->getDestCountryId())) { - $nodeShipmentDetails->addChild('IsDutiable', 'Y'); - } - $nodeShipmentDetails->addChild( - 'CurrencyCode', - $this->_storeManager->getWebsite($this->_request->getWebsiteId())->getBaseCurrencyCode() - ); - } else { - if ($package['params']['container'] == self::DHL_CONTENT_TYPE_NON_DOC) { - $packageType = 'CP'; - } - $nodeShipmentDetails->addChild('PackageType', $packageType); - $nodeShipmentDetails->addChild('Weight', sprintf('%.3f', $rawRequest->getPackageWeight())); - $nodeShipmentDetails->addChild('DimensionUnit', substr($this->_getDimensionUnit(), 0, 1)); - $nodeShipmentDetails->addChild('WeightUnit', substr($this->_getWeightUnit(), 0, 1)); - $nodeShipmentDetails->addChild('GlobalProductCode', $rawRequest->getShippingMethod()); - $nodeShipmentDetails->addChild('LocalProductCode', $rawRequest->getShippingMethod()); - - /** - * The DoorTo Element defines the type of delivery service that applies to the shipment. - * The valid values are DD (Door to Door), DA (Door to Airport) , AA and DC (Door to - * Door non-compliant) - */ - $nodeShipmentDetails->addChild('DoorTo', 'DD'); - $nodeShipmentDetails->addChild('Date', $this->_coreDate->date('Y-m-d')); - $nodeShipmentDetails->addChild('Contents', 'DHL Parcel TEST'); + $nodeShipmentDetails->addChild('Weight', sprintf('%.3f', $rawRequest->getPackageWeight())); + $nodeShipmentDetails->addChild('WeightUnit', substr($this->_getWeightUnit(), 0, 1)); + $nodeShipmentDetails->addChild('GlobalProductCode', $rawRequest->getShippingMethod()); + $nodeShipmentDetails->addChild('LocalProductCode', $rawRequest->getShippingMethod()); + $nodeShipmentDetails->addChild( + 'Date', + $this->_coreDate->date('Y-m-d', strtotime('now + 1day')) + ); + $nodeShipmentDetails->addChild('Contents', 'DHL Parcel'); + /** + * The DoorTo Element defines the type of delivery service that applies to the shipment. + * The valid values are DD (Door to Door), DA (Door to Airport) , AA and DC (Door to + * Door non-compliant) + */ + $nodeShipmentDetails->addChild('DoorTo', 'DD'); + $nodeShipmentDetails->addChild('DimensionUnit', substr($this->_getDimensionUnit(), 0, 1)); + if ($package['params']['container'] == self::DHL_CONTENT_TYPE_NON_DOC) { + $packageType = 'CP'; + } + $nodeShipmentDetails->addChild('PackageType', $packageType); + if ($this->isDutiable($rawRequest->getOrigCountryId(), $rawRequest->getDestCountryId())) { + $nodeShipmentDetails->addChild('IsDutiable', 'Y'); } + $nodeShipmentDetails->addChild( + 'CurrencyCode', + $this->_storeManager->getWebsite($this->_request->getWebsiteId())->getBaseCurrencyCode() + ); } /** * Get tracking * * @param string|string[] $trackings - * @return Result|null + * @return \Magento\Shipping\Model\Tracking\Result|null */ public function getTracking($trackings) { @@ -1710,12 +1701,15 @@ protected function _getXMLTracking($trackings) ''; + ' xsi:schemaLocation="http://www.dhl.com TrackingRequestKnown-1.0.xsd"' . + ' schemaVersion="1.0" />'; $xml = $this->_xmlElFactory->create(['data' => $xmlStr]); $requestNode = $xml->addChild('Request', '', ''); $serviceHeaderNode = $requestNode->addChild('ServiceHeader', '', ''); + $serviceHeaderNode->addChild('MessageTime', $this->buildMessageTimestamp()); + $serviceHeaderNode->addChild('MessageReference', $this->buildMessageReference(self::SERVICE_PREFIX_TRACKING)); $serviceHeaderNode->addChild('SiteID', (string)$this->getConfigData('id')); $serviceHeaderNode->addChild('Password', (string)$this->getConfigData('password')); @@ -1959,17 +1953,72 @@ protected function _prepareShippingLabelContent(\SimpleXMLElement $xml) } /** + * Verify if the shipment is dutiable + * * @param string $origCountryId * @param string $destCountryId * * @return bool */ - protected function isDutiable($origCountryId, $destCountryId) + protected function isDutiable($origCountryId, $destCountryId) : bool { $this->_checkDomesticStatus($origCountryId, $destCountryId); - return - self::DHL_CONTENT_TYPE_NON_DOC == $this->getConfigData('content_type') - || !$this->_isDomestic; + return !$this->_isDomestic; + } + + /** + * Builds a datetime string to be used as the MessageTime in accordance to the expected format. + * + * @param string|null $datetime + * @return string + */ + private function buildMessageTimestamp(string $datetime = null): string + { + return $this->_coreDate->date(\DATE_RFC3339, $datetime); + } + + /** + * Builds a string to be used as the MessageReference. + * + * @param string $servicePrefix + * @return string + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function buildMessageReference(string $servicePrefix): string + { + $validPrefixes = [ + self::SERVICE_PREFIX_QUOTE, + self::SERVICE_PREFIX_SHIPVAL, + self::SERVICE_PREFIX_TRACKING + ]; + + if (!in_array($servicePrefix, $validPrefixes)) { + throw new \Magento\Framework\Exception\LocalizedException( + __("Invalid service prefix \"$servicePrefix\" provided while attempting to build MessageReference") + ); + } + + return str_replace('.', '', uniqid("MAGE_{$servicePrefix}_", true)); + } + + /** + * Builds a string to be used as the request SoftwareName. + * + * @return string + */ + private function buildSoftwareName(): string + { + return substr($this->productMetadata->getName(), 0, 30); + } + + /** + * Builds a string to be used as the request SoftwareVersion. + * + * @return string + */ + private function buildSoftwareVersion(): string + { + return substr($this->productMetadata->getVersion(), 0, 10); } } diff --git a/app/code/Magento/Dhl/Test/Unit/Model/CarrierTest.php b/app/code/Magento/Dhl/Test/Unit/Model/CarrierTest.php index 96c76a17bc31..c3d82ef34a44 100644 --- a/app/code/Magento/Dhl/Test/Unit/Model/CarrierTest.php +++ b/app/code/Magento/Dhl/Test/Unit/Model/CarrierTest.php @@ -3,17 +3,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Dhl\Test\Unit\Model; use Magento\Dhl\Model\Carrier; use Magento\Dhl\Model\Validator\XmlValidator; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ProductMetadataInterface; use Magento\Framework\Filesystem\Directory\Read; use Magento\Framework\Filesystem\Directory\ReadFactory; use Magento\Framework\HTTP\ZendClient; use Magento\Framework\HTTP\ZendClientFactory; use Magento\Framework\Locale\ResolverInterface; use Magento\Framework\Module\Dir\Reader; +use Magento\Framework\Stdlib\DateTime\DateTime; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\Xml\Security; use Magento\Quote\Model\Quote\Address\RateRequest; @@ -32,7 +35,6 @@ use Magento\Store\Model\Website; use PHPUnit_Framework_MockObject_MockObject as MockObject; use Psr\Log\LoggerInterface; -use Magento\Store\Model\ScopeInterface; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -80,14 +82,19 @@ class CarrierTest extends \PHPUnit\Framework\TestCase private $xmlValidator; /** - * @var Request|MockObject + * @var LoggerInterface|MockObject */ - private $request; + private $logger; /** - * @var LoggerInterface|MockObject + * @var DateTime|MockObject */ - private $logger; + private $coreDateMock; + + /** + * @var ProductMetadataInterface + */ + private $productMetadataMock; /** * @inheritdoc @@ -96,35 +103,8 @@ protected function setUp() { $this->objectManager = new ObjectManager($this); - $this->request = $this->getMockBuilder(Request::class) - ->disableOriginalConstructor() - ->setMethods( - [ - 'getPackages', - 'getOrigCountryId', - 'setPackages', - 'setPackageWeight', - 'setPackageValue', - 'setValueWithDiscount', - 'setPackageCustomsValue', - 'setFreeMethodWeight', - 'getPackageWeight', - 'getFreeMethodWeight', - 'getOrderShipment', - ] - ) - ->getMock(); - $this->scope = $this->getMockForAbstractClass(ScopeConfigInterface::class); - $xmlElFactory = $this->getXmlFactory(); - $rateFactory = $this->getRateFactory(); - $rateMethodFactory = $this->getRateMethodFactory(); - $httpClientFactory = $this->getHttpClientFactory(); - $configReader = $this->getConfigReader(); - $readFactory = $this->getReadFactory(); - $storeManager = $this->getStoreManager(); - $this->error = $this->getMockBuilder(Error::class) ->setMethods(['setCarrier', 'setCarrierTitle', 'setErrorMessage']) ->getMock(); @@ -135,31 +115,45 @@ protected function setUp() $this->errorFactory->method('create') ->willReturn($this->error); - $carrierHelper = $this->getCarrierHelper(); - $this->xmlValidator = $this->getMockBuilder(XmlValidator::class) ->disableOriginalConstructor() ->getMock(); $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); + $this->coreDateMock = $this->getMockBuilder(DateTime::class) + ->disableOriginalConstructor() + ->getMock(); + $this->coreDateMock->method('date') + ->willReturn('currentTime'); + + $this->productMetadataMock = $this->getMockBuilder(ProductMetadataInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->productMetadataMock->method('getName') + ->willReturn('Software_Product_Name_30_Char_123456789'); + $this->productMetadataMock->method('getVersion') + ->willReturn('10Char_Ver123456789'); + $this->model = $this->objectManager->getObject( Carrier::class, [ 'scopeConfig' => $this->scope, - 'xmlSecurity' => new Security(), - 'logger' => $this->logger, - 'xmlElFactory' => $xmlElFactory, - 'rateFactory' => $rateFactory, 'rateErrorFactory' => $this->errorFactory, - 'rateMethodFactory' => $rateMethodFactory, - 'httpClientFactory' => $httpClientFactory, - 'readFactory' => $readFactory, - 'storeManager' => $storeManager, - 'configReader' => $configReader, - 'carrierHelper' => $carrierHelper, + 'logger' => $this->logger, + 'xmlSecurity' => new Security(), + 'xmlElFactory' => $this->getXmlFactory(), + 'rateFactory' => $this->getRateFactory(), + 'rateMethodFactory' => $this->getRateMethodFactory(), + 'carrierHelper' => $this->getCarrierHelper(), + 'configReader' => $this->getConfigReader(), + 'storeManager' => $this->getStoreManager(), + 'readFactory' => $this->getReadFactory(), + 'httpClientFactory' => $this->getHttpClientFactory(), 'data' => ['id' => 'dhl', 'store' => '1'], 'xmlValidator' => $this->xmlValidator, + 'coreDate' => $this->coreDateMock, + 'productMetadata' => $this->productMetadataMock ] ); } @@ -176,14 +170,14 @@ public function scopeConfigGetValue($path) 'carriers/dhl/shipment_days' => 'Mon,Tue,Wed,Thu,Fri,Sat', 'carriers/dhl/intl_shipment_days' => 'Mon,Tue,Wed,Thu,Fri,Sat', 'carriers/dhl/allowed_methods' => 'IE', - 'carriers/dhl/international_searvice' => 'IE', + 'carriers/dhl/international_service' => 'IE', 'carriers/dhl/gateway_url' => 'https://xmlpi-ea.dhl.com/XMLShippingServlet', 'carriers/dhl/id' => 'some ID', 'carriers/dhl/password' => 'some password', 'carriers/dhl/content_type' => 'N', 'carriers/dhl/nondoc_methods' => '1,3,4,8,P,Q,E,F,H,J,M,V,Y', 'carriers/dhl/showmethod' => 1, - 'carriers/dhl/title' => 'dhl Title', + 'carriers/dhl/title' => 'DHL Title', 'carriers/dhl/specificerrmsg' => 'dhl error message', 'carriers/dhl/unit_of_measure' => 'K', 'carriers/dhl/size' => '1', @@ -191,11 +185,16 @@ public function scopeConfigGetValue($path) 'carriers/dhl/width' => '1.6', 'carriers/dhl/depth' => '1.6', 'carriers/dhl/debug' => 1, - 'shipping/origin/country_id' => 'GB', + 'shipping/origin/country_id' => 'GB' ]; return isset($pathMap[$path]) ? $pathMap[$path] : null; } + /** + * Prepare shipping label content test + * + * @throws \ReflectionException + */ public function testPrepareShippingLabelContent() { $xml = simplexml_load_file( @@ -207,6 +206,8 @@ public function testPrepareShippingLabelContent() } /** + * Prepare shipping label content exception test + * * @dataProvider prepareShippingLabelContentExceptionDataProvider * @expectedException \Magento\Framework\Exception\LocalizedException * @expectedExceptionMessage Unable to retrieve shipping label @@ -217,6 +218,8 @@ public function testPrepareShippingLabelContentException(\SimpleXMLElement $xml) } /** + * Prepare shipping label content exception data provider + * * @return array */ public function prepareShippingLabelContentExceptionDataProvider() @@ -236,8 +239,11 @@ public function prepareShippingLabelContentExceptionDataProvider() } /** + * Invoke prepare shipping label content + * * @param \SimpleXMLElement $xml * @return \Magento\Framework\DataObject + * @throws \ReflectionException */ protected function _invokePrepareShippingLabelContent(\SimpleXMLElement $xml) { @@ -247,8 +253,14 @@ protected function _invokePrepareShippingLabelContent(\SimpleXMLElement $xml) return $method->invoke($model, $xml); } + /** + * Tests that valid rates are returned when sending a quotes request. + */ public function testCollectRates() { + $requestData = require __DIR__ . '/_files/dhl_quote_request_data.php'; + $responseXml = file_get_contents(__DIR__ . '/_files/dhl_quote_response.xml'); + $this->scope->method('getValue') ->willReturnCallback([$this, 'scopeConfigGetValue']); @@ -256,13 +268,14 @@ public function testCollectRates() ->willReturn(true); $this->httpResponse->method('getBody') - ->willReturn(file_get_contents(__DIR__ . '/_files/success_dhl_response_rates.xml')); + ->willReturn($responseXml); - /** @var RateRequest $request */ - $request = $this->objectManager->getObject( - RateRequest::class, - require __DIR__ . '/_files/rates_request_data_dhl.php' - ); + $this->coreDateMock->method('date') + ->willReturnCallback(function () { + return date(\DATE_RFC3339); + }); + + $request = $this->objectManager->getObject(RateRequest::class, $requestData); $reflectionClass = new \ReflectionObject($this->httpClient); $rawPostData = $reflectionClass->getProperty('raw_post_data'); @@ -272,13 +285,27 @@ public function testCollectRates() ->method('debug') ->with($this->stringContains('********')); - self::assertNotEmpty($this->model->collectRates($request)->getAllRates()); - self::assertContains('18.223', $rawPostData->getValue($this->httpClient)); - self::assertContains('0.630', $rawPostData->getValue($this->httpClient)); - self::assertContains('0.630', $rawPostData->getValue($this->httpClient)); - self::assertContains('0.630', $rawPostData->getValue($this->httpClient)); + $expectedRates = require __DIR__ . '/_files/dhl_quote_response_rates.php'; + $actualRates = $this->model->collectRates($request)->getAllRates(); + + self::assertEquals(count($expectedRates), count($actualRates)); + + foreach ($actualRates as $i => $actualRate) { + $actualRate = $actualRate->getData(); + unset($actualRate['method_title']); + self::assertEquals($expectedRates[$i], $actualRate); + } + + $requestXml = $rawPostData->getValue($this->httpClient); + self::assertContains('18.223', $requestXml); + self::assertContains('0.630', $requestXml); + self::assertContains('0.630', $requestXml); + self::assertContains('0.630', $requestXml); } + /** + * Tests that an error is returned when attempting to collect rates for an inactive shipping method. + */ public function testCollectRatesErrorMessage() { $this->scope->method('getValue') @@ -296,26 +323,81 @@ public function testCollectRatesErrorMessage() $this->assertSame($this->error, $this->model->collectRates($request)); } - public function testCollectRatesFail() + /** + * Test request to shipment sends valid xml values. + * + * @dataProvider requestToShipmentDataProvider + * @param string $origCountryId + * @param string $expectedRegionCode + * @param string $destCountryId + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \ReflectionException + */ + public function testRequestToShipment(string $origCountryId, string $expectedRegionCode, string $destCountryId) { - $this->scope->expects($this->once())->method('isSetFlag')->willReturn(true); + $scopeConfigValueMap = [ + ['carriers/dhl/account', 'store', null, '1234567890'], + ['carriers/dhl/gateway_url', 'store', null, 'https://xmlpi-ea.dhl.com/XMLShippingServlet'], + ['carriers/dhl/id', 'store', null, 'some ID'], + ['carriers/dhl/password', 'store', null, 'some password'], + ['carriers/dhl/content_type', 'store', null, 'N'], + ['carriers/dhl/nondoc_methods', 'store', null, '1,3,4,8,P,Q,E,F,H,J,M,V,Y'], + ['shipping/origin/country_id', 'store', null, $origCountryId], + ]; - $request = new RateRequest(); - $request->setPackageWeight(1); + $this->scope->method('getValue') + ->willReturnMap($scopeConfigValueMap); + + $this->httpResponse->method('getBody') + ->willReturn(utf8_encode(file_get_contents(__DIR__ . '/_files/response_shipping_label.xml'))); + + $request = $this->getRequest($origCountryId, $destCountryId); + + $this->logger->method('debug') + ->with($this->stringContains('********')); + + $result = $this->model->requestToShipment($request); - $this->assertFalse(false, $this->model->collectRates($request)); + $reflectionClass = new \ReflectionObject($this->httpClient); + $rawPostData = $reflectionClass->getProperty('raw_post_data'); + $rawPostData->setAccessible(true); + + $this->assertNotNull($result); + $requestXml = $rawPostData->getValue($this->httpClient); + $requestElement = new Element($requestXml); + + $messageReference = $requestElement->Request->ServiceHeader->MessageReference->__toString(); + $this->assertStringStartsWith('MAGE_SHIP_', $messageReference); + $this->assertGreaterThanOrEqual(28, strlen($messageReference)); + $this->assertLessThanOrEqual(32, strlen($messageReference)); + $requestElement->Request->ServiceHeader->MessageReference = 'MAGE_SHIP_28TO32_Char_CHECKED'; + + $this->assertXmlStringEqualsXmlString( + $this->getExpectedRequestXml($origCountryId, $destCountryId, $expectedRegionCode)->asXML(), + $requestElement->asXML() + ); } /** - * Test request to shipment sends valid xml values. + * Prepare and retrieve request object + * + * @param string $origCountryId + * @param string $destCountryId + * @return Request|MockObject */ - public function testRequestToShipment() + private function getRequest(string $origCountryId, string $destCountryId) { - $this->scope->method('getValue') - ->willReturnCallback([$this, 'scopeConfigGetValue']); + $order = $this->getMockBuilder(Order::class) + ->disableOriginalConstructor() + ->getMock(); + $order->method('getSubtotal') + ->willReturn('10.00'); - $this->httpResponse->method('getBody') - ->willReturn(utf8_encode(file_get_contents(__DIR__ . '/_files/response_shipping_label.xml'))); + $shipment = $this->getMockBuilder(Order\Shipment::class) + ->disableOriginalConstructor() + ->getMock(); + $shipment->method('getOrder') + ->willReturn($order); $packages = [ 'package' => [ @@ -337,52 +419,77 @@ public function testRequestToShipment() ], ]; - $order = $this->getMockBuilder(Order::class) - ->disableOriginalConstructor() - ->getMock(); - $order->method('getSubtotal') - ->willReturn('10.00'); + $methods = [ + 'getPackages' => $packages, + 'getOrigCountryId' => $origCountryId, + 'getDestCountryId' => $destCountryId, + 'getShipperAddressCountryCode' => $origCountryId, + 'getRecipientAddressCountryCode' => $destCountryId, + 'setPackages' => null, + 'setPackageWeight' => null, + 'setPackageValue' => null, + 'setValueWithDiscount' => null, + 'setPackageCustomsValue' => null, + 'setFreeMethodWeight' => null, + 'getPackageWeight' => '0.454000000001', + 'getFreeMethodWeight' => '0.454000000001', + 'getOrderShipment' => $shipment, + ]; - $shipment = $this->getMockBuilder(Order\Shipment::class) + /** @var Request|MockObject $request */ + $request = $this->getMockBuilder(Request::class) ->disableOriginalConstructor() + ->setMethods(array_keys($methods)) ->getMock(); - $shipment->method('getOrder') - ->willReturn($order); - $this->request->method('getPackages') - ->willReturn($packages); - $this->request->method('getOrigCountryId') - ->willReturn('GB'); - $this->request->method('setPackages') - ->willReturnSelf(); - $this->request->method('setPackageWeight') - ->willReturnSelf(); - $this->request->method('setPackageValue') - ->willReturnSelf(); - $this->request->method('setValueWithDiscount') - ->willReturnSelf(); - $this->request->method('setPackageCustomsValue') - ->willReturnSelf(); - $this->request->method('setFreeMethodWeight') - ->willReturnSelf(); - $this->request->method('getPackageWeight') - ->willReturn('0.454000000001'); - $this->request->method('getFreeMethodWeight') - ->willReturn('0.454000000001'); - $this->request->method('getOrderShipment') - ->willReturn($shipment); + foreach ($methods as $method => $return) { + $return ? $request->method($method)->willReturn($return) : $request->method($method)->willReturnSelf(); + } - $this->logger->method('debug') - ->with($this->stringContains('********')); + return $request; + } - $result = $this->model->requestToShipment($this->request); + /** + * Prepare and retrieve expected request xml element + * + * @param string $origCountryId + * @param string $destCountryId + * @return Element + */ + private function getExpectedRequestXml(string $origCountryId, string $destCountryId, string $regionCode) + { + $requestXmlPath = $origCountryId == $destCountryId + ? '/_files/domestic_shipment_request.xml' + : '/_files/shipment_request.xml'; - $reflectionClass = new \ReflectionObject($this->httpClient); - $rawPostData = $reflectionClass->getProperty('raw_post_data'); - $rawPostData->setAccessible(true); + $expectedRequestElement = new Element(file_get_contents(__DIR__ . $requestXmlPath)); - $this->assertNotNull($result); - $this->assertContains('0.454', $rawPostData->getValue($this->httpClient)); + $expectedRequestElement->Consignee->CountryCode = $destCountryId; + $expectedRequestElement->Consignee->CountryName = $this->getCountryName($destCountryId); + + $expectedRequestElement->Shipper->CountryCode = $origCountryId; + $expectedRequestElement->Shipper->CountryName = $this->getCountryName($origCountryId); + + $expectedRequestElement->RegionCode = $regionCode; + + return $expectedRequestElement; + } + + /** + * Get Country Name by Country Code + * + * @param string $countryCode + * @return string + */ + private function getCountryName($countryCode) + { + $countryNames = [ + 'US' => 'United States of America', + 'SG' => 'Singapore', + 'GB' => 'United Kingdom', + 'DE' => 'Germany', + ]; + return $countryNames[$countryCode]; } /** @@ -394,89 +501,21 @@ public function requestToShipmentDataProvider() { return [ [ - 'GB' + 'GB', 'EU', 'US' + ], + [ + 'SG', 'AP', 'US' ], [ - null + 'DE', 'EU', 'DE' ] ]; } /** - * Test that shipping label request for origin country from AP region doesn't contain restricted fields. + * Get DHL products test * - * @return void - */ - public function testShippingLabelRequestForAsiaPacificRegion() - { - $this->scope->method('getValue') - ->willReturnMap( - [ - ['shipping/origin/country_id', ScopeInterface::SCOPE_STORE, null, 'SG'], - ['carriers/dhl/gateway_url', ScopeInterface::SCOPE_STORE, null, 'https://xmlpi-ea.dhl.com'], - ] - ); - - $this->httpResponse->method('getBody') - ->willReturn(utf8_encode(file_get_contents(__DIR__ . '/_files/response_shipping_label.xml'))); - - $packages = [ - 'package' => [ - 'params' => [ - 'width' => '1', - 'length' => '1', - 'height' => '1', - 'dimension_units' => 'INCH', - 'weight_units' => 'POUND', - 'weight' => '0.45', - 'customs_value' => '10.00', - 'container' => Carrier::DHL_CONTENT_TYPE_NON_DOC, - ], - 'items' => [ - 'item1' => [ - 'name' => 'item_name', - ], - ], - ], - ]; - - $this->request->method('getPackages')->willReturn($packages); - $this->request->method('getOrigCountryId')->willReturn('SG'); - $this->request->method('setPackages')->willReturnSelf(); - $this->request->method('setPackageWeight')->willReturnSelf(); - $this->request->method('setPackageValue')->willReturnSelf(); - $this->request->method('setValueWithDiscount')->willReturnSelf(); - $this->request->method('setPackageCustomsValue')->willReturnSelf(); - - $result = $this->model->requestToShipment($this->request); - - $reflectionClass = new \ReflectionObject($this->httpClient); - $rawPostData = $reflectionClass->getProperty('raw_post_data'); - $rawPostData->setAccessible(true); - - $this->assertNotNull($result); - $requestXml = $rawPostData->getValue($this->httpClient); - - $this->assertNotContains( - 'NewShipper', - $requestXml, - 'NewShipper is restricted field for AP region' - ); - $this->assertNotContains( - 'Division', - $requestXml, - 'Division is restricted field for AP region' - ); - $this->assertNotContains( - 'RegisteredAccount', - $requestXml, - 'RegisteredAccount is restricted field for AP region' - ); - } - - /** * @dataProvider dhlProductsDataProvider - * * @param string $docType * @param array $products */ @@ -486,9 +525,11 @@ public function testGetDhlProducts(string $docType, array $products) } /** + * DHL products data provider + * * @return array */ - public function dhlProductsDataProvider() : array + public function dhlProductsDataProvider(): array { return [ 'doc' => [ @@ -537,6 +578,113 @@ public function dhlProductsDataProvider() : array ]; } + /** + * Tests that the built MessageReference string is of the appropriate format. + * + * @dataProvider buildMessageReferenceDataProvider + * @param $servicePrefix + * @throws \ReflectionException + */ + public function testBuildMessageReference($servicePrefix) + { + $method = new \ReflectionMethod($this->model, 'buildMessageReference'); + $method->setAccessible(true); + + $messageReference = $method->invoke($this->model, $servicePrefix); + $this->assertGreaterThanOrEqual(28, strlen($messageReference)); + $this->assertLessThanOrEqual(32, strlen($messageReference)); + } + + /** + * Build message reference data provider + * + * @return array + */ + public function buildMessageReferenceDataProvider() + { + return [ + 'quote_prefix' => ['QUOT'], + 'shipval_prefix' => ['SHIP'], + 'tracking_prefix' => ['TRCK'] + ]; + } + + /** + * Tests that an exception is thrown when an invalid service prefix is provided. + * + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Invalid service prefix + */ + public function testBuildMessageReferenceInvalidPrefix() + { + $method = new \ReflectionMethod($this->model, 'buildMessageReference'); + $method->setAccessible(true); + + $method->invoke($this->model, 'INVALID'); + } + + /** + * Tests that the built software name string is of the appropriate format. + * + * @dataProvider buildSoftwareNameDataProvider + * @param $productName + * @throws \ReflectionException + */ + public function testBuildSoftwareName($productName) + { + $method = new \ReflectionMethod($this->model, 'buildSoftwareName'); + $method->setAccessible(true); + + $this->productMetadataMock->method('getName')->willReturn($productName); + + $softwareName = $method->invoke($this->model); + $this->assertLessThanOrEqual(30, strlen($softwareName)); + } + + /** + * Data provider for testBuildSoftwareName + * + * @return array + */ + public function buildSoftwareNameDataProvider() + { + return [ + 'valid_length' => ['Magento'], + 'exceeds_length' => ['Product_Name_Longer_Than_30_Char'] + ]; + } + + /** + * Tests that the built software version string is of the appropriate format. + * + * @dataProvider buildSoftwareVersionProvider + * @param $productVersion + * @throws \ReflectionException + */ + public function testBuildSoftwareVersion($productVersion) + { + $method = new \ReflectionMethod($this->model, 'buildSoftwareVersion'); + $method->setAccessible(true); + + $this->productMetadataMock->method('getVersion')->willReturn($productVersion); + + $softwareVersion = $method->invoke($this->model); + $this->assertLessThanOrEqual(10, strlen($softwareVersion)); + } + + /** + * Data provider for testBuildSoftwareVersion + * + * @return array + */ + public function buildSoftwareVersionProvider() + { + return [ + 'valid_length' => ['2.3.1'], + 'exceeds_length' => ['dev-MC-1000'] + ]; + } + /** * Creates mock for XML factory. * @@ -595,19 +743,25 @@ private function getRateMethodFactory(): MockObject ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $rateMethod = $this->getMockBuilder(Method::class) - ->disableOriginalConstructor() - ->setMethods(['setPrice']) - ->getMock(); - $rateMethod->method('setPrice') - ->willReturnSelf(); + $rateMethodFactory->method('create') - ->willReturn($rateMethod); + ->willReturnCallback(function () { + $rateMethod = $this->getMockBuilder(Method::class) + ->disableOriginalConstructor() + ->setMethods(['setPrice']) + ->getMock(); + $rateMethod->method('setPrice') + ->willReturnSelf(); + + return $rateMethod; + }); return $rateMethodFactory; } /** + * Get config reader + * * @return MockObject */ private function getConfigReader(): MockObject @@ -622,6 +776,8 @@ private function getConfigReader(): MockObject } /** + * Get read factory + * * @return MockObject */ private function getReadFactory(): MockObject @@ -640,6 +796,8 @@ private function getReadFactory(): MockObject } /** + * Get store manager + * * @return MockObject */ private function getStoreManager(): MockObject @@ -661,6 +819,8 @@ private function getStoreManager(): MockObject } /** + * Get carrier helper + * * @return CarrierHelper */ private function getCarrierHelper(): CarrierHelper @@ -679,6 +839,8 @@ private function getCarrierHelper(): CarrierHelper } /** + * Get HTTP client factory + * * @return MockObject */ private function getHttpClientFactory(): MockObject diff --git a/app/code/Magento/Dhl/Test/Unit/Model/_files/countries.xml b/app/code/Magento/Dhl/Test/Unit/Model/_files/countries.xml index 3f28111f229d..792465ce4594 100644 --- a/app/code/Magento/Dhl/Test/Unit/Model/_files/countries.xml +++ b/app/code/Magento/Dhl/Test/Unit/Model/_files/countries.xml @@ -1,4 +1,4 @@ - + + + + + + 2014-01-09T12:13:29.498+00:00 + EvgeniyUSA + + + + + + NUQ + NUQ + + + BER + BER + + E + E + EXPRESS 9:00 + EXPRESS 9:00 NONDOC + TD + N + Y + 2014-01-09 + PT16H15M + PT15H15M + USD + 1.000000 + 42.060 + 0.000 + 2 + 0 + 0 + + 2014-01-13 11:59:00 + +00:00 + + PT9H + 2.205 + LB + 4 + 1 + + FF + FF + FUEL SURCHARGE + FUEL SURCHARGE + SCH + USD + 3.790 + + 3.790 + USD + BILLC + + + 3.790 + USD + PULCL + + + 3.790 + USD + BASEC + + + 2014-01-09 + 45.850 + 0.000 + + USD + BILLC + 42.060 + 45.850 + 0.000 + 0.000 + + + USD + PULCL + 42.060 + 45.850 + 0.000 + 0.000 + + + USD + BASEC + 42.060 + 45.850 + 0.000 + 0.000 + + 09:00:00 + 17:00:00 + PT1H + + + + NUQ + NUQ + + + BER + BER + + Q + Q + MEDICAL EXPRESS + MEDICAL EXPRESS + TD + Y + N + 2014-01-09 + PT16H15M + PT15H15M + USD + 1.000000 + 32.350 + 0.000 + 2 + 0 + 0 + + 2014-01-13 11:59:00 + +00:00 + + PT9H + 2.205 + LB + 4 + 1 + + FF + FF + FUEL SURCHARGE + FUEL SURCHARGE + SCH + USD + 2.910 + + 2.910 + USD + BILLC + + + 2.910 + USD + PULCL + + + 2.910 + USD + BASEC + + + 2014-01-09 + 35.260 + 0.000 + + USD + BILLC + 32.350 + 35.260 + 0.000 + 0.000 + + + USD + PULCL + 32.350 + 35.260 + 0.000 + 0.000 + + + USD + BASEC + 32.350 + 35.260 + 0.000 + 0.000 + + 09:00:00 + 17:00:00 + PT1H + + + + NUQ + NUQ + + + BER + BER + + Y + Y + EXPRESS 12:00 + EXPRESS 12:00 NONDOC + TD + N + Y + 2014-01-09 + PT16H15M + PT15H15M + USD + 1.000000 + 34.290 + 0.000 + 2 + 0 + 0 + + 2014-01-13 11:59:00 + +00:00 + + PT12H + 2.205 + LB + 4 + 1 + + FF + FF + FUEL SURCHARGE + FUEL SURCHARGE + SCH + USD + 3.090 + + 3.090 + USD + BILLC + + + 3.090 + USD + PULCL + + + 3.090 + USD + BASEC + + + 2014-01-09 + 37.380 + 0.000 + + USD + BILLC + 34.290 + 37.380 + 0.000 + 0.000 + + + USD + PULCL + 34.290 + 37.380 + 0.000 + 0.000 + + + USD + BASEC + 34.290 + 37.380 + 0.000 + 0.000 + + 09:00:00 + 17:00:00 + PT1H + + + + NUQ + NUQ + + + BER + BER + + 3 + 3 + B2C + EXPRESS WORLDWIDE (B2C) + TD + Y + N + 2014-01-09 + PT16H15M + PT15H15M + 1.000000 + 0 + 0.000 + 2 + 0 + 0 + + 2014-01-13 11:59:00 + +00:00 + + PT23H59M + 2.205 + LB + 4 + 1 + 2014-01-09 + 0.000 + 0.000 + + BILLC + 0 + 0.000 + 0.000 + 0.000 + + + USD + PULCL + 0 + 0.000 + 0.000 + 0.000 + + + USD + BASEC + 0 + 0.000 + 0.000 + 0.000 + + 09:00:00 + 17:00:00 + PT1H + + + + NUQ + NUQ + + + BER + BER + + P + P + EXPRESS WORLDWIDE + EXPRESS WORLDWIDE NONDOC + TD + N + Y + 2014-01-09 + PT16H15M + PT15H15M + USD + 1.000000 + 32.350 + 0.000 + 2 + 0 + 0 + + 2014-01-13 11:59:00 + +00:00 + + PT23H59M + 2.205 + LB + 4 + 1 + + FF + FF + FUEL SURCHARGE + FUEL SURCHARGE + SCH + USD + 2.910 + + 2.910 + USD + BILLC + + + 2.910 + USD + PULCL + + + 2.910 + USD + BASEC + + + 2014-01-09 + 35.260 + 0.000 + + USD + BILLC + 32.350 + 35.260 + 0.000 + 0.000 + + + USD + PULCL + 32.350 + 35.260 + 0.000 + 0.000 + + + USD + BASEC + 32.350 + 35.260 + 0.000 + 0.000 + + 09:00:00 + 17:00:00 + PT1H + + + + + E + + E + EXPRESS 9:00 + EXPRESS 9:00 NONDOC + TD + N + Y + + + FF + FUEL SURCHARGE + FUEL SURCHARGE + SCH + N + + + + Q + + Q + MEDICAL EXPRESS + MEDICAL EXPRESS + TD + Y + N + + + FF + FUEL SURCHARGE + FUEL SURCHARGE + SCH + N + + + + Y + + Y + EXPRESS 12:00 + EXPRESS 12:00 NONDOC + TD + N + Y + + + FF + FUEL SURCHARGE + FUEL SURCHARGE + SCH + N + + + + 3 + + 3 + B2C + EXPRESS WORLDWIDE (B2C) + TD + Y + N + + + + P + + P + EXPRESS WORLDWIDE + EXPRESS WORLDWIDE NONDOC + TD + N + Y + + + FF + FUEL SURCHARGE + FUEL SURCHARGE + SCH + N + + + + + diff --git a/app/code/Magento/Dhl/Test/Unit/Model/_files/dhl_quote_response_rates.php b/app/code/Magento/Dhl/Test/Unit/Model/_files/dhl_quote_response_rates.php new file mode 100644 index 000000000000..ddd7b2e4f97c --- /dev/null +++ b/app/code/Magento/Dhl/Test/Unit/Model/_files/dhl_quote_response_rates.php @@ -0,0 +1,31 @@ + 'dhl', + 'carrier_title' => 'DHL Title', + 'cost' => 45.85, + 'method' => 'E' + ], + [ + 'carrier' => 'dhl', + 'carrier_title' => 'DHL Title', + 'cost' => 35.26, + 'method' => 'Q' + ], + [ + 'carrier' => 'dhl', + 'carrier_title' => 'DHL Title', + 'cost' => 37.38, + 'method' => 'Y' + ], + [ + 'carrier' => 'dhl', + 'carrier_title' => 'DHL Title', + 'cost' => 35.26, + 'method' => 'P' + ] +]; diff --git a/app/code/Magento/Dhl/Test/Unit/Model/_files/domestic_shipment_request.xml b/app/code/Magento/Dhl/Test/Unit/Model/_files/domestic_shipment_request.xml new file mode 100644 index 000000000000..b71c2fa4a7dd --- /dev/null +++ b/app/code/Magento/Dhl/Test/Unit/Model/_files/domestic_shipment_request.xml @@ -0,0 +1,88 @@ + + + + + + currentTime + MAGE_SHIP_28TO32_Char_CHECKED + some ID + some password + + + CHECKED + N + N + EN + Y + + 1234567890 + S + 1234567890 + S + 1234567890 + + + + + + + + + + + + + + + 1 + + + shipment reference + St + + + 1 + + + 1 + CP + 0.454 + 3 + 3 + 3 + item_name + + + 0.454 + K + + + currentTime + DHL Parcel + DD + C + CP + USD + + + 1234567890 + + 1234567890 + + + + + + + + + + + PDF + \ No newline at end of file diff --git a/app/code/Magento/Dhl/Test/Unit/Model/_files/rates_request_data_dhl.php b/app/code/Magento/Dhl/Test/Unit/Model/_files/rates_request_data_dhl.php deleted file mode 100644 index 32e9c78886ce..000000000000 --- a/app/code/Magento/Dhl/Test/Unit/Model/_files/rates_request_data_dhl.php +++ /dev/null @@ -1,49 +0,0 @@ - [ - 'dest_country_id' => 'DE', - 'dest_region_id' => '82', - 'dest_region_code' => 'BER', - 'dest_street' => 'Turmstraße 17', - 'dest_city' => 'Berlin', - 'dest_postcode' => '10559', - 'dest_postal' => '10559', - 'package_value' => '5', - 'package_value_with_discount' => '5', - 'package_weight' => '8.2657', - 'package_qty' => '1', - 'package_physical_value' => '5', - 'free_method_weight' => '5', - 'store_id' => '1', - 'website_id' => '1', - 'free_shipping' => '0', - 'limit_carrier' => null, - 'base_subtotal_incl_tax' => '5', - 'orig_country_id' => 'US', - 'country_id' => 'US', - 'region_id' => '12', - 'city' => 'Fremont', - 'postcode' => '94538', - 'dhl_id' => 'MAGEN_8501', - 'dhl_password' => 'QR2GO1U74X', - 'dhl_account' => '799909537', - 'dhl_shipping_intl_key' => '54233F2B2C4E5C4B4C5E5A59565530554B405641475D5659', - 'girth' => null, - 'height' => null, - 'length' => null, - 'width' => null, - 'weight' => 1, - 'dhl_shipment_type' => 'P', - 'dhl_duitable' => 0, - 'dhl_duty_payment_type' => 'R', - 'dhl_content_desc' => 'Big Box', - 'limit_method' => 'IE', - 'ship_date' => '2014-01-09', - 'action' => 'RateEstimate', - 'all_items' => [], - ] -]; diff --git a/app/code/Magento/Dhl/Test/Unit/Model/_files/shipment_request.xml b/app/code/Magento/Dhl/Test/Unit/Model/_files/shipment_request.xml new file mode 100644 index 000000000000..d411041c9607 --- /dev/null +++ b/app/code/Magento/Dhl/Test/Unit/Model/_files/shipment_request.xml @@ -0,0 +1,93 @@ + + + + + + currentTime + MAGE_SHIP_28TO32_Char_CHECKED + some ID + some password + + + CHECKED + N + N + EN + Y + + 1234567890 + S + 1234567890 + S + 1234567890 + + + + + + + + + + + + + + + 1 + + + 10.00 + USD + + + shipment reference + St + + + 1 + + + 1 + CP + 0.454 + 3 + 3 + 3 + item_name + + + 0.454 + K + + + currentTime + DHL Parcel + DD + C + CP + Y + USD + + + 1234567890 + + 1234567890 + + + + + + + + + + + PDF + diff --git a/app/code/Magento/Dhl/Test/Unit/Model/_files/success_dhl_response_rates.xml b/app/code/Magento/Dhl/Test/Unit/Model/_files/success_dhl_response_rates.xml deleted file mode 100644 index b529e86ef154..000000000000 --- a/app/code/Magento/Dhl/Test/Unit/Model/_files/success_dhl_response_rates.xml +++ /dev/null @@ -1,478 +0,0 @@ - - - - - - - - 2014-01-09T12:13:29.498+00:00 - EvgeniyUSA - - - - - NUQ - NUQ - - - BER - BER - - - E - E - EXPRESS 9:00 - EXPRESS 9:00 NONDOC - TD - N - Y - 2014-01-09 - PT16H15M - PT15H15M - USD - 1.000000 - 42.060 - 0.000 - 2 - 0 - 0 - - - 2014-01-13 - PT9H - 2.205 - LB - 4 - 1 - - FF - FF - FUEL SURCHARGE - FUEL SURCHARGE - SCH - USD - 3.790 - - 3.790 - USD - BILLC - - - 3.790 - USD - PULCL - - - 3.790 - USD - BASEC - - - 2014-01-09 - 45.850 - 0.000 - - USD - BILLC - 42.060 - 45.850 - 0.000 - 0.000 - - - USD - PULCL - 42.060 - 45.850 - 0.000 - 0.000 - - - USD - BASEC - 42.060 - 45.850 - 0.000 - 0.000 - - - - Q - Q - MEDICAL EXPRESS - MEDICAL EXPRESS - TD - Y - N - 2014-01-09 - PT16H15M - PT15H15M - USD - 1.000000 - 32.350 - 0.000 - 2 - 0 - 0 - - - 2014-01-13 - PT9H - 2.205 - LB - 4 - 1 - - FF - FF - FUEL SURCHARGE - FUEL SURCHARGE - SCH - USD - 2.910 - - 2.910 - USD - BILLC - - - 2.910 - USD - PULCL - - - 2.910 - USD - BASEC - - - 2014-01-09 - 35.260 - 0.000 - - USD - BILLC - 32.350 - 35.260 - 0.000 - 0.000 - - - USD - PULCL - 32.350 - 35.260 - 0.000 - 0.000 - - - USD - BASEC - 32.350 - 35.260 - 0.000 - 0.000 - - - - Y - Y - EXPRESS 12:00 - EXPRESS 12:00 NONDOC - TD - N - Y - 2014-01-09 - PT16H15M - PT15H15M - USD - 1.000000 - 34.290 - 0.000 - 2 - 0 - 0 - - - 2014-01-13 - PT12H - 2.205 - LB - 4 - 1 - - FF - FF - FUEL SURCHARGE - FUEL SURCHARGE - SCH - USD - 3.090 - - 3.090 - USD - BILLC - - - 3.090 - USD - PULCL - - - 3.090 - USD - BASEC - - - 2014-01-09 - 37.380 - 0.000 - - USD - BILLC - 34.290 - 37.380 - 0.000 - 0.000 - - - USD - PULCL - 34.290 - 37.380 - 0.000 - 0.000 - - - USD - BASEC - 34.290 - 37.380 - 0.000 - 0.000 - - - - 3 - 3 - B2C - EXPRESS WORLDWIDE (B2C) - TD - Y - N - 2014-01-09 - PT16H15M - PT15H15M - 1.000000 - 0 - 0.000 - 2 - 0 - 0 - - - 2014-01-13 - PT23H59M - 2.205 - LB - 4 - 1 - 2014-01-09 - 0.000 - 0.000 - - BILLC - 0 - 0.000 - 0.000 - 0.000 - - - USD - PULCL - 0 - 0.000 - 0.000 - 0.000 - - - USD - BASEC - 0 - 0.000 - 0.000 - 0.000 - - - - P - P - EXPRESS WORLDWIDE - EXPRESS WORLDWIDE NONDOC - TD - N - Y - 2014-01-09 - PT16H15M - PT15H15M - USD - 1.000000 - 32.350 - 0.000 - 2 - 0 - 0 - - - 2014-01-13 - PT23H59M - 2.205 - LB - 4 - 1 - - FF - FF - FUEL SURCHARGE - FUEL SURCHARGE - SCH - USD - 2.910 - - 2.910 - USD - BILLC - - - 2.910 - USD - PULCL - - - 2.910 - USD - BASEC - - - 2014-01-09 - 35.260 - 0.000 - - USD - BILLC - 32.350 - 35.260 - 0.000 - 0.000 - - - USD - PULCL - 32.350 - 35.260 - 0.000 - 0.000 - - - USD - BASEC - 32.350 - 35.260 - 0.000 - 0.000 - - - - - - E - - E - EXPRESS 9:00 - EXPRESS 9:00 NONDOC - TD - N - Y - - - FF - FUEL SURCHARGE - FUEL SURCHARGE - SCH - N - - - - Q - - Q - MEDICAL EXPRESS - MEDICAL EXPRESS - TD - Y - N - - - FF - FUEL SURCHARGE - FUEL SURCHARGE - SCH - N - - - - Y - - Y - EXPRESS 12:00 - EXPRESS 12:00 NONDOC - TD - N - Y - - - FF - FUEL SURCHARGE - FUEL SURCHARGE - SCH - N - - - - 3 - - 3 - B2C - EXPRESS WORLDWIDE (B2C) - TD - Y - N - - - - P - - P - EXPRESS WORLDWIDE - EXPRESS WORLDWIDE NONDOC - TD - N - Y - - - FF - FUEL SURCHARGE - FUEL SURCHARGE - SCH - N - - - - - diff --git a/app/code/Magento/Dhl/etc/adminhtml/system.xml b/app/code/Magento/Dhl/etc/adminhtml/system.xml index 91ed6c6568a7..7ab37de2f365 100644 --- a/app/code/Magento/Dhl/etc/adminhtml/system.xml +++ b/app/code/Magento/Dhl/etc/adminhtml/system.xml @@ -31,8 +31,9 @@ - - + + + Whether to use Documents or NonDocuments service for non domestic shipments. (Shipments within the EU are classed as domestic) Magento\Dhl\Model\Source\Contenttype @@ -81,18 +82,12 @@ - + Magento\Dhl\Model\Source\Method\Doc - - D - - + Magento\Dhl\Model\Source\Method\Nondoc - - N - diff --git a/app/code/Magento/Dhl/etc/countries.xml b/app/code/Magento/Dhl/etc/countries.xml index 48837dbefb57..792465ce4594 100644 --- a/app/code/Magento/Dhl/etc/countries.xml +++ b/app/code/Magento/Dhl/etc/countries.xml @@ -83,7 +83,7 @@ EUR KG CM - EA + EU Austria 1 @@ -132,7 +132,7 @@ EUR KG CM - EA + EU Belgium 1 @@ -146,7 +146,7 @@ BGN KG CM - EA + EU Bulgaria 1 @@ -257,7 +257,7 @@ CHF KG CM - EA + EU Switzerland @@ -331,7 +331,7 @@ CZK KG CM - EA + EU Czech Republic, The 1 @@ -339,7 +339,7 @@ EUR KG CM - EA + EU Germany 1 @@ -353,7 +353,7 @@ DKK KG CM - EA + EU Denmark 1 @@ -389,7 +389,7 @@ EEK KG CM - EA + EU Estonia 1 @@ -410,7 +410,7 @@ EUR KG CM - EA + EU Spain 1 @@ -424,7 +424,7 @@ EUR KG CM - EA + EU Finland 1 @@ -457,7 +457,7 @@ EUR KG CM - EA + EU France 1 @@ -471,7 +471,7 @@ GBP KG CM - EA + EU United Kingdom 1 @@ -549,7 +549,7 @@ EUR KG CM - EA + EU Greece 1 @@ -612,7 +612,7 @@ HUF KG CM - EA + EU Hungary 1 @@ -633,7 +633,7 @@ EUR KG CM - EA + EU Ireland, Republic Of 1 @@ -668,14 +668,14 @@ ISK KG CM - EA + EU Iceland EUR KG CM - EA + EU Italy 1 @@ -834,7 +834,7 @@ LTL KG CM - EA + EU Lithuania 1 @@ -842,7 +842,7 @@ EUR KG CM - EA + EU Luxembourg 1 @@ -850,7 +850,7 @@ LVL KG CM - EA + EU Latvia 1 @@ -1039,7 +1039,7 @@ EUR KG CM - EA + EU Netherlands, The 1 @@ -1047,7 +1047,7 @@ NOK KG CM - EA + EU Norway @@ -1127,7 +1127,7 @@ PLN KG CM - EA + EU Poland 1 @@ -1142,7 +1142,7 @@ EUR KG CM - EA + EU Portugal 1 @@ -1177,7 +1177,7 @@ RON KG CM - EA + EU Romania 1 @@ -1231,7 +1231,7 @@ SEK KG CM - EA + EU Sweden 1 @@ -1246,7 +1246,7 @@ EUR KG CM - EA + EU Slovenia 1 @@ -1254,7 +1254,7 @@ EUR KG CM - EA + EU Slovakia 1 diff --git a/app/code/Magento/Dhl/i18n/en_US.csv b/app/code/Magento/Dhl/i18n/en_US.csv index a5532c2cea96..0e4c7a8385b9 100644 --- a/app/code/Magento/Dhl/i18n/en_US.csv +++ b/app/code/Magento/Dhl/i18n/en_US.csv @@ -61,6 +61,7 @@ Title,Title Password,Password "Account Number","Account Number" "Content Type","Content Type" +"Whether to use Documents or NonDocuments service for non domestic shipments. (Shipments within the EU are classed as domestic)","Whether to use Documents or NonDocuments service for non domestic shipments. (Shipments within the EU are classed as domestic)" "Calculate Handling Fee","Calculate Handling Fee" "Handling Applied","Handling Applied" """Per Order"" allows a single handling fee for the entire order. ""Per Package"" allows an individual handling fee for each package.","""Per Order"" allows a single handling fee for the entire order. ""Per Package"" allows an individual handling fee for each package." diff --git a/app/code/Magento/Directory/Block/Data.php b/app/code/Magento/Directory/Block/Data.php index ca2b4b95b557..333e9e03706b 100644 --- a/app/code/Magento/Directory/Block/Data.php +++ b/app/code/Magento/Directory/Block/Data.php @@ -6,6 +6,8 @@ namespace Magento\Directory\Block; /** + * Directory data block + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Data extends \Magento\Framework\View\Element\Template @@ -67,6 +69,8 @@ public function __construct( } /** + * Returns load url for regions + * * @return string */ public function getLoadrRegionUrl() @@ -75,6 +79,8 @@ public function getLoadrRegionUrl() } /** + * Returns country collection instance + * * @return \Magento\Directory\Model\ResourceModel\Country\Collection */ public function getCountryCollection() @@ -103,6 +109,8 @@ protected function getTopDestinations() } /** + * Returns country html select + * * @param null|string $defValue * @param string $name * @param string $id @@ -146,6 +154,8 @@ public function getCountryHtmlSelect($defValue = null, $name = 'country_id', $id } /** + * Returns region collection + * * @return \Magento\Directory\Model\ResourceModel\Region\Collection */ public function getRegionCollection() @@ -160,6 +170,8 @@ public function getRegionCollection() } /** + * Returns region html select + * * @return string */ public function getRegionHtmlSelect() @@ -184,7 +196,7 @@ public function getRegionHtmlSelect() )->setClass( 'required-entry validate-state' )->setValue( - intval($this->getRegionId()) + (int)$this->getRegionId() )->setOptions( $options )->getHtml(); @@ -193,6 +205,8 @@ public function getRegionHtmlSelect() } /** + * Returns country id + * * @return string */ public function getCountryId() @@ -205,6 +219,8 @@ public function getCountryId() } /** + * Returns regions js + * * @return string */ public function getRegionsJs() diff --git a/app/code/Magento/Directory/Helper/Data.php b/app/code/Magento/Directory/Helper/Data.php index 99e1d1ad5394..3a5558e6d6f3 100644 --- a/app/code/Magento/Directory/Helper/Data.php +++ b/app/code/Magento/Directory/Helper/Data.php @@ -6,7 +6,15 @@ namespace Magento\Directory\Helper; +use Magento\Directory\Model\Currency; +use Magento\Directory\Model\CurrencyFactory; +use Magento\Directory\Model\ResourceModel\Country\Collection; +use Magento\Directory\Model\ResourceModel\Region\CollectionFactory; +use Magento\Framework\App\Cache\Type\Config; +use Magento\Framework\App\Helper\Context; +use Magento\Framework\Json\Helper\Data as JsonData; use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; /** * Directory data helper @@ -53,7 +61,7 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper /** * Country collection * - * @var \Magento\Directory\Model\ResourceModel\Country\Collection + * @var Collection */ protected $_countryCollection; @@ -86,47 +94,49 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper protected $_optZipCountries = null; /** - * @var \Magento\Framework\App\Cache\Type\Config + * @var Config */ protected $_configCacheType; /** - * @var \Magento\Directory\Model\ResourceModel\Region\CollectionFactory + * @var CollectionFactory */ protected $_regCollectionFactory; /** - * @var \Magento\Framework\Json\Helper\Data + * @var JsonData */ protected $jsonHelper; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $_storeManager; /** - * @var \Magento\Directory\Model\CurrencyFactory + * @var CurrencyFactory */ protected $_currencyFactory; /** - * @param \Magento\Framework\App\Helper\Context $context - * @param \Magento\Framework\App\Cache\Type\Config $configCacheType - * @param \Magento\Directory\Model\ResourceModel\Country\Collection $countryCollection - * @param \Magento\Directory\Model\ResourceModel\Region\CollectionFactory $regCollectionFactory, - * @param \Magento\Framework\Json\Helper\Data $jsonHelper - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Directory\Model\CurrencyFactory $currencyFactory + * Data constructor. + * + * @param Context $context + * @param Config $configCacheType + * @param Collection $countryCollection + * @param CollectionFactory $regCollectionFactory + * @param JsonData $jsonHelper + * @param StoreManagerInterface $storeManager + * @param CurrencyFactory $currencyFactory */ public function __construct( - \Magento\Framework\App\Helper\Context $context, - \Magento\Framework\App\Cache\Type\Config $configCacheType, - \Magento\Directory\Model\ResourceModel\Country\Collection $countryCollection, - \Magento\Directory\Model\ResourceModel\Region\CollectionFactory $regCollectionFactory, - \Magento\Framework\Json\Helper\Data $jsonHelper, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Directory\Model\CurrencyFactory $currencyFactory + Context $context, + Config $configCacheType, + Collection $countryCollection, + CollectionFactory $regCollectionFactory, + JsonData $jsonHelper, + StoreManagerInterface $storeManager, + CurrencyFactory $currencyFactory ) { parent::__construct($context); $this->_configCacheType = $configCacheType; @@ -155,7 +165,7 @@ public function getRegionCollection() * Retrieve country collection * * @param null|int|string|\Magento\Store\Model\Store $store - * @return \Magento\Directory\Model\ResourceModel\Country\Collection + * @return Collection */ public function getCountryCollection($store = null) { @@ -169,6 +179,7 @@ public function getCountryCollection($store = null) * Retrieve regions data json * * @return string + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function getRegionJson() { @@ -197,8 +208,10 @@ public function getRegionJson() * @param float $amount * @param string $from * @param string $to + * * @return float * @SuppressWarnings(PHPMD.ShortVariable) + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function currencyConvert($amount, $from, $to = null) { @@ -251,7 +264,7 @@ public function isZipCodeOptional($countryCode) * Returns the list of countries, for which region is required * * @param boolean $asJson - * @return array + * @return array|string */ public function getCountriesWithStatesRequired($asJson = false) { @@ -275,7 +288,7 @@ public function getCountriesWithStatesRequired($asJson = false) */ public function isShowNonRequiredState() { - return (bool)$this->scopeConfig->getValue( + return $this->scopeConfig->isSetFlag( self::XML_PATH_DISPLAY_ALL_STATES, ScopeInterface::SCOPE_STORE ); @@ -303,7 +316,10 @@ public function isRegionRequired($countryId) */ public function getBaseCurrencyCode() { - return $this->scopeConfig->getValue(\Magento\Directory\Model\Currency::XML_PATH_CURRENCY_BASE, 'default'); + return $this->scopeConfig->getValue( + Currency::XML_PATH_CURRENCY_BASE, + 'default' + ); } /** diff --git a/app/code/Magento/Directory/Model/Config/Source/WeightUnit.php b/app/code/Magento/Directory/Model/Config/Source/WeightUnit.php index 4e2758b362a4..c8c42b952042 100644 --- a/app/code/Magento/Directory/Model/Config/Source/WeightUnit.php +++ b/app/code/Magento/Directory/Model/Config/Source/WeightUnit.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Directory\Model\Config\Source; /** @@ -14,10 +15,13 @@ class WeightUnit implements \Magento\Framework\Option\ArrayInterface { /** - * {@inheritdoc} + * @inheritdoc */ public function toOptionArray() { - return [['value' => 'lbs', 'label' => __('lbs')], ['value' => 'kgs', 'label' => __('kgs')]]; + return [ + ['value' => 'lbs', 'label' => __('lbs')], + ['value' => 'kgs', 'label' => __('kgs')] + ]; } } diff --git a/app/code/Magento/Directory/Model/Currency.php b/app/code/Magento/Directory/Model/Currency.php index f39c33408f90..6a2ebb453150 100644 --- a/app/code/Magento/Directory/Model/Currency.php +++ b/app/code/Magento/Directory/Model/Currency.php @@ -225,7 +225,7 @@ public function convert($price, $toCurrency = null) if ($toCurrency === null) { return $price; } elseif ($rate = $this->getRate($toCurrency)) { - return floatval($price) * floatval($rate); + return (float)$price * (float)$rate; } throw new \Exception(__( diff --git a/app/code/Magento/Directory/Model/CurrencyConfig.php b/app/code/Magento/Directory/Model/CurrencyConfig.php index fdb561c22417..f7230df6e86e 100644 --- a/app/code/Magento/Directory/Model/CurrencyConfig.php +++ b/app/code/Magento/Directory/Model/CurrencyConfig.php @@ -57,7 +57,7 @@ public function __construct( */ public function getConfigCurrencies(string $path) { - $result = $this->appState->getAreaCode() === Area::AREA_ADMINHTML + $result = in_array($this->appState->getAreaCode(), [Area::AREA_ADMINHTML, Area::AREA_CRONTAB]) ? $this->getConfigForAllStores($path) : $this->getConfigForCurrentStore($path); sort($result); diff --git a/app/code/Magento/Directory/Model/PriceCurrency.php b/app/code/Magento/Directory/Model/PriceCurrency.php index a211242d377f..08d320abd8b5 100644 --- a/app/code/Magento/Directory/Model/PriceCurrency.php +++ b/app/code/Magento/Directory/Model/PriceCurrency.php @@ -46,7 +46,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function convert($amount, $scope = null, $currency = null) { @@ -58,17 +58,15 @@ public function convert($amount, $scope = null, $currency = null) } /** - * {@inheritdoc} + * @inheritdoc */ public function convertAndRound($amount, $scope = null, $currency = null, $precision = self::DEFAULT_PRECISION) { - $currentCurrency = $this->getCurrency($scope, $currency); - $convertedValue = $this->getStore($scope)->getBaseCurrency()->convert($amount, $currentCurrency); - return round($convertedValue, $precision); + return $this->roundPrice($this->convert($amount, $scope, $currency), $precision); } /** - * {@inheritdoc} + * @inheritdoc */ public function format( $amount, @@ -82,7 +80,7 @@ public function format( } /** - * {@inheritdoc} + * @inheritdoc */ public function convertAndFormat( $amount, @@ -97,7 +95,7 @@ public function convertAndFormat( } /** - * {@inheritdoc} + * @inheritdoc */ public function getCurrency($scope = null, $currency = null) { @@ -118,6 +116,8 @@ public function getCurrency($scope = null, $currency = null) } /** + * Get currrency symbol + * * @param null|string|bool|int|\Magento\Framework\App\ScopeInterface $scope * @param \Magento\Framework\Model\AbstractModel|string|null $currency * @return string @@ -148,13 +148,22 @@ protected function getStore($scope = null) } /** - * Round price + * @inheritdoc + */ + public function round($price) + { + return round($price, 2); + } + + /** + * Round price with precision * * @param float $price + * @param int $precision * @return float */ - public function round($price) + public function roundPrice($price, $precision = self::DEFAULT_PRECISION) { - return round($price, 2); + return round($price, $precision); } } diff --git a/app/code/Magento/Directory/Model/ResourceModel/Country/Collection.php b/app/code/Magento/Directory/Model/ResourceModel/Country/Collection.php index 20140baae01b..4ec34a3842fa 100644 --- a/app/code/Magento/Directory/Model/ResourceModel/Country/Collection.php +++ b/app/code/Magento/Directory/Model/ResourceModel/Country/Collection.php @@ -129,8 +129,8 @@ public function __construct( /** * Add top destinition countries to head of option array * - * @param $emptyLabel - * @param $options + * @param string $emptyLabel + * @param array $options * @return array */ private function addForegroundCountriesToOptionArray($emptyLabel, $options) @@ -205,9 +205,10 @@ public function getItemById($countryId) /** * Add filter by country code to collection. + * * $countryCode can be either array of country codes or string representing one country code. * $iso can be either array containing 'iso2', 'iso3' values or string with containing one of that values directly. - * The collection will contain countries where at least one of contry $iso fields matches $countryCode. + * The collection will contain countries where at least one of country $iso fields matches $countryCode. * * @param string|string[] $countryCode * @param string|string[] $iso @@ -297,7 +298,7 @@ public function toOptionArray($emptyLabel = ' ') } $options[] = $option; } - if ($emptyLabel !== false && count($options) > 0) { + if ($emptyLabel !== false && count($options) > 1) { array_unshift($options, ['value' => '', 'label' => $emptyLabel]); } @@ -326,7 +327,7 @@ private function addDefaultCountryToOptions(array &$options) foreach ($options as $key => $option) { if (isset($defaultCountry[$option['value']])) { - $options[$key]['is_default'] = $defaultCountry[$option['value']]; + $options[$key]['is_default'] = !empty($defaultCountry[$option['value']]); } } } diff --git a/app/code/Magento/Directory/Model/ResourceModel/Currency.php b/app/code/Magento/Directory/Model/ResourceModel/Currency.php index ffbcce11cb4f..5339b0c9eb5b 100644 --- a/app/code/Magento/Directory/Model/ResourceModel/Currency.php +++ b/app/code/Magento/Directory/Model/ResourceModel/Currency.php @@ -216,7 +216,7 @@ protected function _getRatesByCode($code, $toCurrencies = null) $connection = $this->getConnection(); $bind = [':currency_from' => $code]; $select = $connection->select()->from( - $this->getTable('directory_currency_rate'), + $this->_currencyRateTable, ['currency_to', 'rate'] )->where( 'currency_from = :currency_from' diff --git a/app/code/Magento/Directory/Setup/Patch/Data/AddDataForAustralia.php b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForAustralia.php new file mode 100644 index 000000000000..20306d08a292 --- /dev/null +++ b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForAustralia.php @@ -0,0 +1,102 @@ +moduleDataSetup = $moduleDataSetup; + $this->dataInstallerFactory = $dataInstallerFactory; + } + + /** + * @inheritdoc + */ + public function apply() + { + /** @var DataInstaller $dataInstaller */ + $dataInstaller = $this->dataInstallerFactory->create(); + $dataInstaller->addCountryRegions( + $this->moduleDataSetup->getConnection(), + $this->getDataForAustralia() + ); + } + + /** + * Australian states data. + * + * @return array + */ + private function getDataForAustralia() + { + return [ + ['AU', 'ACT', 'Australian Capital Territory'], + ['AU', 'NSW', 'New South Wales'], + ['AU', 'VIC', 'Victoria'], + ['AU', 'QLD', 'Queensland'], + ['AU', 'SA', 'South Australia'], + ['AU', 'TAS', 'Tasmania'], + ['AU', 'WA', 'Western Australia'], + ['AU', 'NT', 'Northern Territory'] + ]; + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return [ + InitializeDirectoryData::class, + AddDataForCroatia::class, + AddDataForIndia::class, + ]; + } + + /** + * @inheritdoc + */ + public static function getVersion() + { + return '2.0.3'; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Directory/Setup/Patch/Data/AddDataForIndia.php b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForIndia.php index 69d500960d3f..47f4fb0a6c7f 100644 --- a/app/code/Magento/Directory/Setup/Patch/Data/AddDataForIndia.php +++ b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForIndia.php @@ -13,8 +13,7 @@ use Magento\Framework\Setup\Patch\PatchVersionInterface; /** - * Class AddDataForIndia - * @package Magento\Directory\Setup\Patch\Data + * Add Regions for India. */ class AddDataForIndia implements DataPatchInterface, PatchVersionInterface { @@ -29,7 +28,7 @@ class AddDataForIndia implements DataPatchInterface, PatchVersionInterface private $dataInstallerFactory; /** - * AddDataForCroatia constructor. + * AddDataForIndia constructor. * * @param ModuleDataSetupInterface $moduleDataSetup * @param \Magento\Directory\Setup\DataInstallerFactory $dataInstallerFactory @@ -43,7 +42,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { @@ -103,7 +102,7 @@ private function getDataForIndia() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -113,7 +112,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -121,7 +120,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/Directory/Setup/Patch/Data/AddDataForMexico.php b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForMexico.php new file mode 100644 index 000000000000..32bdf90800d6 --- /dev/null +++ b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForMexico.php @@ -0,0 +1,127 @@ +moduleDataSetup = $moduleDataSetup; + $this->dataInstallerFactory = $dataInstallerFactory; + } + + /** + * @inheritdoc + */ + public function apply() + { + /** @var DataInstaller $dataInstaller */ + $dataInstaller = $this->dataInstallerFactory->create(); + $dataInstaller->addCountryRegions( + $this->moduleDataSetup->getConnection(), + $this->getDataForMexico() + ); + } + + /** + * Mexican states data. + * + * @return array + */ + private function getDataForMexico() + { + return [ + ['MX', 'AGU', 'Aguascalientes'], + ['MX', 'BCN', 'Baja California'], + ['MX', 'BCS', 'Baja California Sur'], + ['MX', 'CAM', 'Campeche'], + ['MX', 'CHP', 'Chiapas'], + ['MX', 'CHH', 'Chihuahua'], + ['MX', 'CMX', 'Ciudad de México'], + ['MX', 'COA', 'Coahuila'], + ['MX', 'COL', 'Colima'], + ['MX', 'DUR', 'Durango'], + ['MX', 'MEX', 'Estado de México'], + ['MX', 'GUA', 'Guanajuato'], + ['MX', 'GRO', 'Guerrero'], + ['MX', 'HID', 'Hidalgo'], + ['MX', 'JAL', 'Jalisco'], + ['MX', 'MIC', 'Michoacán'], + ['MX', 'MOR', 'Morelos'], + ['MX', 'NAY', 'Nayarit'], + ['MX', 'NLE', 'Nuevo León'], + ['MX', 'OAX', 'Oaxaca'], + ['MX', 'PUE', 'Puebla'], + ['MX', 'QUE', 'Querétaro'], + ['MX', 'ROO', 'Quintana Roo'], + ['MX', 'SLP', 'San Luis Potosí'], + ['MX', 'SIN', 'Sinaloa'], + ['MX', 'SON', 'Sonora'], + ['MX', 'TAB', 'Tabasco'], + ['MX', 'TAM', 'Tamaulipas'], + ['MX', 'TLA', 'Tlaxcala'], + ['MX', 'VER', 'Veracruz'], + ['MX', 'YUC', 'Yucatán'], + ['MX', 'ZAC', 'Zacatecas'] + ]; + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return [ + InitializeDirectoryData::class, + AddDataForAustralia::class, + AddDataForCroatia::class, + AddDataForIndia::class, + ]; + } + + /** + * @inheritdoc + */ + public static function getVersion() + { + return '2.0.4'; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Directory/Test/Unit/Helper/DataTest.php b/app/code/Magento/Directory/Test/Unit/Helper/DataTest.php index 73e9f0f5fa1a..6ff0f8ea0f30 100644 --- a/app/code/Magento/Directory/Test/Unit/Helper/DataTest.php +++ b/app/code/Magento/Directory/Test/Unit/Helper/DataTest.php @@ -46,6 +46,7 @@ protected function setUp() { $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->scopeConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); + $this->scopeConfigMock->expects($this->any())->method('isSetFlag')->willReturn(false); $context = $this->createMock(\Magento\Framework\App\Helper\Context::class); $context->expects($this->any()) ->method('getScopeConfig') diff --git a/app/code/Magento/Directory/Test/Unit/Model/CurrencyConfigTest.php b/app/code/Magento/Directory/Test/Unit/Model/CurrencyConfigTest.php index 9b52bae26f90..e594be90b26d 100644 --- a/app/code/Magento/Directory/Test/Unit/Model/CurrencyConfigTest.php +++ b/app/code/Magento/Directory/Test/Unit/Model/CurrencyConfigTest.php @@ -68,7 +68,7 @@ protected function setUp() } /** - * Test get currency config for admin and storefront areas. + * Test get currency config for admin, crontab and storefront areas. * * @dataProvider getConfigCurrenciesDataProvider * @return void @@ -91,7 +91,7 @@ public function testGetConfigCurrencies(string $areCode) ->method('getCode') ->willReturn('testCode'); - if ($areCode === Area::AREA_ADMINHTML) { + if (in_array($areCode, [Area::AREA_ADMINHTML, Area::AREA_CRONTAB])) { $this->storeManager->expects(self::once()) ->method('getStores') ->willReturn([$store]); @@ -121,6 +121,7 @@ public function getConfigCurrenciesDataProvider() { return [ ['areaCode' => Area::AREA_ADMINHTML], + ['areaCode' => Area::AREA_CRONTAB], ['areaCode' => Area::AREA_FRONTEND], ]; } diff --git a/app/code/Magento/Directory/etc/db_schema.xml b/app/code/Magento/Directory/etc/db_schema.xml index 72fd929b98a0..c11e0ee525e3 100644 --- a/app/code/Magento/Directory/etc/db_schema.xml +++ b/app/code/Magento/Directory/etc/db_schema.xml @@ -11,7 +11,7 @@ - + @@ -21,10 +21,10 @@ - + - + @@ -36,10 +36,10 @@ comment="Country Id in ISO-2"/> - + - + @@ -49,14 +49,14 @@ - + - - + @@ -66,11 +66,11 @@ - + - + diff --git a/app/code/Magento/DirectoryGraphQl/Controller/HttpHeaderProcessor/CurrencyProcessor.php b/app/code/Magento/DirectoryGraphQl/Controller/HttpHeaderProcessor/CurrencyProcessor.php new file mode 100644 index 000000000000..b076b6fe9e76 --- /dev/null +++ b/app/code/Magento/DirectoryGraphQl/Controller/HttpHeaderProcessor/CurrencyProcessor.php @@ -0,0 +1,105 @@ +storeManager = $storeManager; + $this->httpContext = $httpContext; + $this->session = $session; + $this->logger = $logger; + } + + /** + * Handle the header 'Content-Currency' value. + * + * @param string $headerValue + * @return void + */ + public function processHeaderValue(string $headerValue) : void + { + try { + if (!empty($headerValue)) { + $headerCurrency = strtoupper(ltrim(rtrim($headerValue))); + /** @var \Magento\Store\Model\Store $currentStore */ + $currentStore = $this->storeManager->getStore(); + if (in_array($headerCurrency, $currentStore->getAvailableCurrencyCodes(true))) { + $currentStore->setCurrentCurrencyCode($headerCurrency); + } else { + /** @var \Magento\Store\Model\Store $store */ + $store = $this->storeManager->getStore() ?? $this->storeManager->getDefaultStoreView(); + //skip store not found exception as it will be handled in graphql validation + $this->logger->warning(__('Currency not allowed for store %1', [$store->getCode()])); + $this->httpContext->setValue( + HttpContext::CONTEXT_CURRENCY, + $headerCurrency, + $store->getDefaultCurrency()->getCode() + ); + } + } else { + if ($this->session->getCurrencyCode()) { + /** @var \Magento\Store\Model\Store $currentStore */ + $currentStore = $this->storeManager->getStore() ?? $this->storeManager->getDefaultStoreView(); + $currentStore->setCurrentCurrencyCode($this->session->getCurrencyCode()); + } else { + /** @var \Magento\Store\Model\Store $store */ + $store = $this->storeManager->getStore() ?? $this->storeManager->getDefaultStoreView(); + $this->httpContext->setValue( + HttpContext::CONTEXT_CURRENCY, + $store->getCurrentCurrency()->getCode(), + $store->getDefaultCurrency()->getCode() + ); + } + } + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + //skip store not found exception as it will be handled in graphql validation + $this->logger->warning($e->getMessage()); + } + } +} diff --git a/app/code/Magento/DirectoryGraphQl/Controller/HttpRequestValidator/CurrencyValidator.php b/app/code/Magento/DirectoryGraphQl/Controller/HttpRequestValidator/CurrencyValidator.php new file mode 100644 index 000000000000..7dab90802c20 --- /dev/null +++ b/app/code/Magento/DirectoryGraphQl/Controller/HttpRequestValidator/CurrencyValidator.php @@ -0,0 +1,62 @@ +storeManager = $storeManager; + } + + /** + * Validate the header 'Content-Currency' value. + * + * @param HttpRequestInterface $request + * @return void + * @throws GraphQlInputException + */ + public function validate(HttpRequestInterface $request): void + { + try { + $headerValue = $request->getHeader('Content-Currency'); + if (!empty($headerValue)) { + $headerCurrency = strtoupper(ltrim(rtrim($headerValue))); + /** @var \Magento\Store\Model\Store $currentStore */ + $currentStore = $this->storeManager->getStore(); + if (!in_array($headerCurrency, $currentStore->getAvailableCurrencyCodes(true))) { + throw new GraphQlInputException( + __('Please correct the target currency') + ); + } + } + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + $this->storeManager->setCurrentStore(null); + throw new GraphQlInputException( + __("Requested store is not found") + ); + } + } +} diff --git a/app/code/Magento/DirectoryGraphQl/Model/Resolver/Countries.php b/app/code/Magento/DirectoryGraphQl/Model/Resolver/Countries.php new file mode 100644 index 000000000000..dc788801f3e6 --- /dev/null +++ b/app/code/Magento/DirectoryGraphQl/Model/Resolver/Countries.php @@ -0,0 +1,63 @@ +dataProcessor = $dataProcessor; + $this->countryInformationAcquirer = $countryInformationAcquirer; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + $countries = $this->countryInformationAcquirer->getCountriesInfo(); + + $output = []; + foreach ($countries as $country) { + $output[] = $this->dataProcessor->buildOutputDataArray($country, CountryInformationInterface::class); + } + + return $output; + } +} diff --git a/app/code/Magento/DirectoryGraphQl/Model/Resolver/Country.php b/app/code/Magento/DirectoryGraphQl/Model/Resolver/Country.php new file mode 100644 index 000000000000..ea39f12a7bcb --- /dev/null +++ b/app/code/Magento/DirectoryGraphQl/Model/Resolver/Country.php @@ -0,0 +1,67 @@ +dataProcessor = $dataProcessor; + $this->countryInformationAcquirer = $countryInformationAcquirer; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + try { + $country = $this->countryInformationAcquirer->getCountryInfo($args['id']); + } catch (NoSuchEntityException $exception) { + throw new GraphQlNoSuchEntityException(__($exception->getMessage()), $exception); + } + + return $this->dataProcessor->buildOutputDataArray( + $country, + CountryInformationInterface::class + ); + } +} diff --git a/app/code/Magento/DirectoryGraphQl/Model/Resolver/Currency.php b/app/code/Magento/DirectoryGraphQl/Model/Resolver/Currency.php new file mode 100644 index 000000000000..fb2db6c312ac --- /dev/null +++ b/app/code/Magento/DirectoryGraphQl/Model/Resolver/Currency.php @@ -0,0 +1,59 @@ +dataProcessor = $dataProcessor; + $this->currencyInformationAcquirer = $currencyInformationAcquirer; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + return $this->dataProcessor->buildOutputDataArray( + $this->currencyInformationAcquirer->getCurrencyInfo(), + CurrencyInformationInterface::class + ); + } +} diff --git a/app/code/Magento/DirectoryGraphQl/README.md b/app/code/Magento/DirectoryGraphQl/README.md new file mode 100644 index 000000000000..1a5c969b39ed --- /dev/null +++ b/app/code/Magento/DirectoryGraphQl/README.md @@ -0,0 +1,4 @@ +# DirectoryGraphQl + +**DirectoryGraphQl** provides type and resolver information for the GraphQl module +to generate directory information endpoints. diff --git a/app/code/Magento/DirectoryGraphQl/Test/Mftf/README.md b/app/code/Magento/DirectoryGraphQl/Test/Mftf/README.md new file mode 100644 index 000000000000..8e2e188c1fe9 --- /dev/null +++ b/app/code/Magento/DirectoryGraphQl/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Directory Graph Ql Functional Tests + +The Functional Test Module for **Magento Directory Graph Ql** module. diff --git a/app/code/Magento/DirectoryGraphQl/composer.json b/app/code/Magento/DirectoryGraphQl/composer.json new file mode 100644 index 000000000000..d3c783e6c7bf --- /dev/null +++ b/app/code/Magento/DirectoryGraphQl/composer.json @@ -0,0 +1,24 @@ +{ + "name": "magento/module-directory-graph-ql", + "description": "N/A", + "type": "magento2-module", + "require": { + "php": "~7.1.3||~7.2.0", + "magento/module-directory": "*", + "magento/module-store": "*", + "magento/module-graph-ql": "*", + "magento/framework": "*" + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\DirectoryGraphQl\\": "" + } + } +} diff --git a/app/code/Magento/DirectoryGraphQl/etc/graphql/di.xml b/app/code/Magento/DirectoryGraphQl/etc/graphql/di.xml new file mode 100644 index 000000000000..63f501551f53 --- /dev/null +++ b/app/code/Magento/DirectoryGraphQl/etc/graphql/di.xml @@ -0,0 +1,19 @@ + + + + + + + Magento\DirectoryGraphQl\Controller\HttpHeaderProcessor\CurrencyProcessor + + + Magento\DirectoryGraphQl\Controller\HttpRequestValidator\CurrencyValidator + + + + diff --git a/app/code/Magento/DirectoryGraphQl/etc/module.xml b/app/code/Magento/DirectoryGraphQl/etc/module.xml new file mode 100644 index 000000000000..5d6ec613f36b --- /dev/null +++ b/app/code/Magento/DirectoryGraphQl/etc/module.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/code/Magento/DirectoryGraphQl/etc/schema.graphqls b/app/code/Magento/DirectoryGraphQl/etc/schema.graphqls new file mode 100644 index 000000000000..6daf13f567d4 --- /dev/null +++ b/app/code/Magento/DirectoryGraphQl/etc/schema.graphqls @@ -0,0 +1,39 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +type Query { + currency: Currency @resolver(class: "Magento\\DirectoryGraphQl\\Model\\Resolver\\Currency") @doc(description: "The currency query returns information about store currency.") @cache(cacheable: false) + countries: [Country] @resolver(class: "Magento\\DirectoryGraphQl\\Model\\Resolver\\Countries") @doc(description: "The countries query provides information for all countries.") @cache(cacheable: false) + country (id: String): Country @resolver(class: "Magento\\DirectoryGraphQl\\Model\\Resolver\\Country") @doc(description: "The countries query provides information for a single country.") @cache(cacheable: false) +} + +type Currency { + base_currency_code: String + base_currency_symbol: String + default_display_currecy_code: String @deprecated(reason: "Symbol was missed. Use `default_display_currency_code`.") + default_display_currency_code: String + default_display_currecy_symbol: String @deprecated(reason: "Symbol was missed. Use `default_display_currency_symbol`.") + default_display_currency_symbol: String + available_currency_codes: [String] + exchange_rates: [ExchangeRate] +} + +type ExchangeRate { + currency_to: String + rate: Float +} + +type Country { + id: String + two_letter_abbreviation: String + three_letter_abbreviation: String + full_name_locale: String + full_name_english: String + available_regions: [Region] +} + +type Region { + id: Int + code: String + name: String +} diff --git a/app/code/Magento/DirectoryGraphQl/registration.php b/app/code/Magento/DirectoryGraphQl/registration.php new file mode 100644 index 000000000000..6bb7fd8d4e44 --- /dev/null +++ b/app/code/Magento/DirectoryGraphQl/registration.php @@ -0,0 +1,9 @@ + - */ namespace Magento\Downloadable\Block\Adminhtml\Catalog\Product\Composite\Fieldset; /** + * Adminhtml block for fieldset of downloadable product + * * @api * @since 100.0.2 + * @deprecated + * @see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Composite */ class Downloadable extends \Magento\Downloadable\Block\Catalog\Product\Links { diff --git a/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable.php b/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable.php index e2694b3b93bb..8fdf1d395308 100644 --- a/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable.php +++ b/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable.php @@ -15,6 +15,8 @@ * Adminhtml catalog product downloadable items tab and form * * @author Magento Core Team + * @deprecated + * @see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Composite */ class Downloadable extends Widget implements TabInterface { @@ -134,6 +136,8 @@ public function isHidden() } /** + * Get group code + * * @return string */ public function getGroupCode() @@ -152,6 +156,8 @@ public function getContentTabId() } /** + * Is downloadable + * * @return bool */ public function isDownloadable() @@ -160,7 +166,7 @@ public function isDownloadable() } /** - * @return $this + * @inheritdoc */ protected function _prepareLayout() { diff --git a/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Links.php b/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Links.php index 947e6dc1e833..47c66c98fc8f 100644 --- a/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Links.php +++ b/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Links.php @@ -10,6 +10,9 @@ * * @author Magento Core Team * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * + * @deprecated + * @see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Links */ class Links extends \Magento\Backend\Block\Template { @@ -434,6 +437,8 @@ public function getConfig() } /** + * Is single store mode + * * @return bool */ public function isSingleStoreMode() @@ -442,8 +447,11 @@ public function isSingleStoreMode() } /** + * Get base currency code + * * @param null|string|bool|int|\Magento\Store\Model\Store $storeId $storeId * @return string + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function getBaseCurrencyCode($storeId) { @@ -451,8 +459,11 @@ public function getBaseCurrencyCode($storeId) } /** + * Get base currency symbol + * * @param null|string|bool|int|\Magento\Store\Model\Store $storeId $storeId * @return string + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function getBaseCurrencySymbol($storeId) { diff --git a/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Samples.php b/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Samples.php index 3c86bfb2f8d0..f245aeeeead6 100644 --- a/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Samples.php +++ b/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Samples.php @@ -9,6 +9,9 @@ * Adminhtml catalog product downloadable items tab links section * * @author Magento Core Team + * + * @deprecated + * @see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Samples */ class Samples extends \Magento\Backend\Block\Widget { diff --git a/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/File/Upload.php b/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/File/Upload.php index 38444de78ab9..83b2797050db 100644 --- a/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/File/Upload.php +++ b/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/File/Upload.php @@ -1,14 +1,19 @@ storageDatabase->saveFile($relativePath); } - - $result['cookie'] = [ - 'name' => $this->_getSession()->getName(), - 'value' => $this->_getSession()->getSessionId(), - 'lifetime' => $this->_getSession()->getCookieLifetime(), - 'path' => $this->_getSession()->getCookiePath(), - 'domain' => $this->_getSession()->getCookieDomain(), - ]; } catch (\Exception $e) { $result = ['error' => $e->getMessage(), 'errorcode' => $e->getCode()]; } + return $this->resultFactory->create(ResultFactory::TYPE_JSON)->setData($result); } } diff --git a/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Crosssell.php b/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Crosssell.php index e7b1fff27680..85f9ad39c4df 100644 --- a/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Crosssell.php +++ b/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Crosssell.php @@ -6,6 +6,16 @@ */ namespace Magento\Downloadable\Controller\Adminhtml\Downloadable\Product\Edit; -class Crosssell extends \Magento\Catalog\Controller\Adminhtml\Product\Crosssell +use Magento\Catalog\Controller\Adminhtml\Product\Crosssell as CatalogCrosssell; +use Magento\Framework\App\Action\HttpPostActionInterface; + +/** + * Class Crosssell + * + * @package Magento\Downloadable\Controller\Adminhtml\Downloadable\Product\Edit + * @deprecated Not used since cross-sell products grid moved to UI components. + * @see Magento_Catalog::view/adminhtml/ui_component/crosssell_product_listing.xml + */ +class Crosssell extends CatalogCrosssell implements HttpPostActionInterface { } diff --git a/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/CrosssellGrid.php b/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/CrosssellGrid.php index 8a77cb2c0101..b6c25bdec9f1 100644 --- a/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/CrosssellGrid.php +++ b/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/CrosssellGrid.php @@ -6,6 +6,16 @@ */ namespace Magento\Downloadable\Controller\Adminhtml\Downloadable\Product\Edit; -class CrosssellGrid extends \Magento\Catalog\Controller\Adminhtml\Product\CrosssellGrid +use Magento\Catalog\Controller\Adminhtml\Product\CrosssellGrid as CatalogCrosssellGrid; +use Magento\Framework\App\Action\HttpPostActionInterface; + +/** + * Class CrosssellGrid + * + * @package Magento\Downloadable\Controller\Adminhtml\Downloadable\Product\Edit + * @deprecated Not used since cross-sell products grid moved to UI components. + * @see Magento_Catalog::view/adminhtml/ui_component/crosssell_product_listing.xml + */ +class CrosssellGrid extends CatalogCrosssellGrid implements HttpPostActionInterface { } diff --git a/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Form.php b/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Form.php index 1ef72f1deecc..fe430566d63c 100644 --- a/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Form.php +++ b/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Form.php @@ -6,6 +6,13 @@ */ namespace Magento\Downloadable\Controller\Adminhtml\Downloadable\Product\Edit; +/** + * Class Form + * + * @deprecated since downloadable information rendering moved to UI components. + * @see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Composite + * @package Magento\Downloadable\Controller\Adminhtml\Downloadable\Product\Edit + */ class Form extends \Magento\Catalog\Controller\Adminhtml\Product\Edit { /** diff --git a/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Related.php b/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Related.php index 8c10377f74e8..0352d97bca93 100644 --- a/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Related.php +++ b/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Related.php @@ -6,6 +6,16 @@ */ namespace Magento\Downloadable\Controller\Adminhtml\Downloadable\Product\Edit; -class Related extends \Magento\Catalog\Controller\Adminhtml\Product\Related +use Magento\Catalog\Controller\Adminhtml\Product\Related as CatalogRelated; +use Magento\Framework\App\Action\HttpPostActionInterface; + +/** + * Class Related + * + * @package Magento\Downloadable\Controller\Adminhtml\Downloadable\Product\Edit + * @deprecated Not used since related products grid moved to UI components. + * @see Magento_Catalog::view/adminhtml/ui_component/related_product_listing.xml + */ +class Related extends CatalogRelated implements HttpPostActionInterface { } diff --git a/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/RelatedGrid.php b/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/RelatedGrid.php index 403b3828e39a..11e9c4eb5e3a 100644 --- a/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/RelatedGrid.php +++ b/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/RelatedGrid.php @@ -6,6 +6,16 @@ */ namespace Magento\Downloadable\Controller\Adminhtml\Downloadable\Product\Edit; -class RelatedGrid extends \Magento\Catalog\Controller\Adminhtml\Product\RelatedGrid +use Magento\Catalog\Controller\Adminhtml\Product\RelatedGrid as CatalogRelatedGrid; +use Magento\Framework\App\Action\HttpPostActionInterface; + +/** + * Class RelatedGrid + * + * @package Magento\Downloadable\Controller\Adminhtml\Downloadable\Product\Edit + * @deprecated Not used since related products grid moved to UI components. + * @see Magento_Catalog::view/adminhtml/ui_component/related_product_listing.xml + */ +class RelatedGrid extends CatalogRelatedGrid implements HttpPostActionInterface { } diff --git a/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Upsell.php b/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Upsell.php index d712a7ec80d1..a8581f38c419 100644 --- a/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Upsell.php +++ b/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Upsell.php @@ -6,6 +6,16 @@ */ namespace Magento\Downloadable\Controller\Adminhtml\Downloadable\Product\Edit; -class Upsell extends \Magento\Catalog\Controller\Adminhtml\Product\Upsell +use Magento\Catalog\Controller\Adminhtml\Product\Upsell as CatalogUpsell; +use Magento\Framework\App\Action\HttpPostActionInterface; + +/** + * Class Upsell + * + * @package Magento\Downloadable\Controller\Adminhtml\Downloadable\Product\Edit + * @deprecated Not used since upsell products grid moved to UI components. + * @see Magento_Catalog::view/adminhtml/ui_component/upsell_product_listing.xml + */ +class Upsell extends CatalogUpsell implements HttpPostActionInterface { } diff --git a/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/UpsellGrid.php b/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/UpsellGrid.php index 960909ca1b39..3e28a96948a7 100644 --- a/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/UpsellGrid.php +++ b/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/UpsellGrid.php @@ -6,6 +6,16 @@ */ namespace Magento\Downloadable\Controller\Adminhtml\Downloadable\Product\Edit; -class UpsellGrid extends \Magento\Catalog\Controller\Adminhtml\Product\UpsellGrid +use Magento\Catalog\Controller\Adminhtml\Product\UpsellGrid as CatalogUpsellGrid; +use Magento\Framework\App\Action\HttpPostActionInterface; + +/** + * Class UpsellGrid + * + * @package Magento\Downloadable\Controller\Adminhtml\Downloadable\Product\Edit + * @deprecated Not used since upsell products grid moved to UI components. + * @see Magento_Catalog::view/adminhtml/ui_component/upsell_product_listing.xml + */ +class UpsellGrid extends CatalogUpsellGrid implements HttpPostActionInterface { } diff --git a/app/code/Magento/Downloadable/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Downloadable.php b/app/code/Magento/Downloadable/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Downloadable.php index f56b219f72db..a283891afc40 100644 --- a/app/code/Magento/Downloadable/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Downloadable.php +++ b/app/code/Magento/Downloadable/Controller/Adminhtml/Product/Initialization/Helper/Plugin/Downloadable.php @@ -11,6 +11,9 @@ use Magento\Downloadable\Api\Data\SampleInterfaceFactory; use Magento\Downloadable\Api\Data\LinkInterfaceFactory; +/** + * Class for initialization downloadable info from request. + */ class Downloadable { /** @@ -92,6 +95,8 @@ public function afterInitialize( } } $extension->setDownloadableProductLinks($links); + } else { + $extension->setDownloadableProductLinks([]); } if (isset($downloadable['sample']) && is_array($downloadable['sample'])) { $samples = []; @@ -107,6 +112,8 @@ public function afterInitialize( } } $extension->setDownloadableProductSamples($samples); + } else { + $extension->setDownloadableProductSamples([]); } $product->setExtensionAttributes($extension); if ($product->getLinksPurchasedSeparately()) { diff --git a/app/code/Magento/Downloadable/Controller/Download/Link.php b/app/code/Magento/Downloadable/Controller/Download/Link.php index 765546d080e5..4766f1699afb 100644 --- a/app/code/Magento/Downloadable/Controller/Download/Link.php +++ b/app/code/Magento/Downloadable/Controller/Download/Link.php @@ -1,15 +1,21 @@ setStatus(PurchasedLink::LINK_STATUS_EXPIRED); } $linkPurchasedItem->save(); + // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage exit(0); } catch (\Exception $e) { $this->messageManager->addError(__('Something went wrong while getting the requested content.')); diff --git a/app/code/Magento/Downloadable/Controller/Download/LinkSample.php b/app/code/Magento/Downloadable/Controller/Download/LinkSample.php index 76ec791611c9..f40df744dd3e 100644 --- a/app/code/Magento/Downloadable/Controller/Download/LinkSample.php +++ b/app/code/Magento/Downloadable/Controller/Download/LinkSample.php @@ -1,21 +1,26 @@ _processDownload($resource, $resourceType); + // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage exit(0); } catch (\Exception $e) { $this->messageManager->addError( diff --git a/app/code/Magento/Downloadable/Controller/Download/Sample.php b/app/code/Magento/Downloadable/Controller/Download/Sample.php index 4a4f88d81b37..ac9eeac678f8 100644 --- a/app/code/Magento/Downloadable/Controller/Download/Sample.php +++ b/app/code/Magento/Downloadable/Controller/Download/Sample.php @@ -1,21 +1,26 @@ _processDownload($resource, $resourceType); + // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage exit(0); } catch (\Exception $e) { $this->messageManager->addError( diff --git a/app/code/Magento/Downloadable/Helper/Data.php b/app/code/Magento/Downloadable/Helper/Data.php index 96aa5bdfeffd..e9b2a5ce44c0 100644 --- a/app/code/Magento/Downloadable/Helper/Data.php +++ b/app/code/Magento/Downloadable/Helper/Data.php @@ -5,7 +5,9 @@ */ namespace Magento\Downloadable\Helper; +use Magento\Downloadable\Model\Link; use Magento\Downloadable\Model\Link\Purchased\Item; +use Magento\Store\Model\ScopeInterface; /** * Downloadable helper @@ -17,7 +19,7 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper /** * Check is link shareable or not * - * @param \Magento\Downloadable\Model\Link|Item $link + * @param Link|Item $link * @return bool * @SuppressWarnings(PHPMD.BooleanGetMethodName) */ @@ -25,14 +27,14 @@ public function getIsShareable($link) { $shareable = false; switch ($link->getIsShareable()) { - case \Magento\Downloadable\Model\Link::LINK_SHAREABLE_YES: - case \Magento\Downloadable\Model\Link::LINK_SHAREABLE_NO: + case Link::LINK_SHAREABLE_YES: + case Link::LINK_SHAREABLE_NO: $shareable = (bool)$link->getIsShareable(); break; - case \Magento\Downloadable\Model\Link::LINK_SHAREABLE_CONFIG: - $shareable = (bool)$this->scopeConfig->isSetFlag( - \Magento\Downloadable\Model\Link::XML_PATH_CONFIG_IS_SHAREABLE, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE + case Link::LINK_SHAREABLE_CONFIG: + $shareable = $this->scopeConfig->isSetFlag( + Link::XML_PATH_CONFIG_IS_SHAREABLE, + ScopeInterface::SCOPE_STORE ); } return $shareable; diff --git a/app/code/Magento/Downloadable/Helper/Download.php b/app/code/Magento/Downloadable/Helper/Download.php index 150a5ec474f3..6b7db3af5119 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 24d1d7831c9e..df8427bdde65 100644 --- a/app/code/Magento/Downloadable/Model/ResourceModel/Link.php +++ b/app/code/Magento/Downloadable/Model/ResourceModel/Link.php @@ -5,10 +5,6 @@ */ namespace Magento\Downloadable\Model\ResourceModel; -use Magento\Catalog\Api\Data\ProductInterface; -use Magento\Framework\App\ObjectManager; -use Magento\Framework\EntityManager\MetadataPool; - /** * Downloadable Product Samples resource model * @@ -17,11 +13,6 @@ */ class Link extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb { - /** - * @var MetadataPool - */ - private $metadataPool; - /** * Catalog data * @@ -210,10 +201,7 @@ public function getSearchableData($productId, $storeId) [] )->join( ['cpe' => $this->getTable('catalog_product_entity')], - sprintf( - 'cpe.entity_id = m.product_id', - $this->getMetadataPool()->getMetadata(ProductInterface::class)->getLinkField() - ), + 'cpe.entity_id = m.product_id', [] )->joinLeft( ['st' => $this->getTable('downloadable_link_title')], @@ -228,22 +216,12 @@ public function getSearchableData($productId, $storeId) } /** + * Get Currency model. + * * @return \Magento\Directory\Model\Currency */ protected function _createCurrency() { return $this->_currencyFactory->create(); } - - /** - * Get MetadataPool instance - * @return MetadataPool - */ - private function getMetadataPool() - { - if (!$this->metadataPool) { - $this->metadataPool = ObjectManager::getInstance()->get(MetadataPool::class); - } - return $this->metadataPool; - } } diff --git a/app/code/Magento/Downloadable/Model/SampleRepository.php b/app/code/Magento/Downloadable/Model/SampleRepository.php index 0c7d2b96f1b5..07c7631fade1 100644 --- a/app/code/Magento/Downloadable/Model/SampleRepository.php +++ b/app/code/Magento/Downloadable/Model/SampleRepository.php @@ -23,6 +23,7 @@ /** * Class SampleRepository + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class SampleRepository implements \Magento\Downloadable\Api\SampleRepositoryInterface @@ -100,7 +101,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getList($sku) { @@ -209,6 +210,8 @@ public function save( } /** + * Save sample. + * * @param \Magento\Catalog\Api\Data\ProductInterface $product * @param SampleInterface $sample * @param bool $isGlobalScopeContent @@ -257,6 +260,8 @@ protected function saveSample( } /** + * Update sample. + * * @param \Magento\Catalog\Api\Data\ProductInterface $product * @param SampleInterface $sample * @param bool $isGlobalScopeContent @@ -308,15 +313,18 @@ protected function updateSample( $existingSample->setTitle($sample->getTitle()); } - if ($sample->getSampleType() === 'file' && $sample->getSampleFileContent() === null) { - $sample->setSampleFile($existingSample->getSampleFile()); + if ($sample->getSampleType() === 'file' + && $sample->getSampleFileContent() === null + && $sample->getSampleFile() !== null + ) { + $existingSample->setSampleFile($sample->getSampleFile()); } $this->saveSample($product, $sample, $isGlobalScopeContent); return $existingSample->getId(); } /** - * {@inheritdoc} + * @inheritdoc */ public function delete($id) { diff --git a/app/code/Magento/Downloadable/Observer/SaveDownloadableOrderItemObserver.php b/app/code/Magento/Downloadable/Observer/SaveDownloadableOrderItemObserver.php index 64305cfce9b0..7c1d2748a3e9 100644 --- a/app/code/Magento/Downloadable/Observer/SaveDownloadableOrderItemObserver.php +++ b/app/code/Magento/Downloadable/Observer/SaveDownloadableOrderItemObserver.php @@ -9,6 +9,8 @@ use Magento\Store\Model\ScopeInterface; /** + * Saves data from order to purchased links. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class SaveDownloadableOrderItemObserver implements ObserverInterface @@ -92,9 +94,15 @@ public function execute(\Magento\Framework\Event\Observer $observer) if ($purchasedLink->getId()) { return $this; } + $storeId = $orderItem->getOrder()->getStoreId(); + $orderStatusToEnableItem = $this->_scopeConfig->getValue( + \Magento\Downloadable\Model\Link\Purchased\Item::XML_PATH_ORDER_ITEM_STATUS, + ScopeInterface::SCOPE_STORE, + $storeId + ); if (!$product) { $product = $this->_createProductModel()->setStoreId( - $orderItem->getOrder()->getStoreId() + $storeId )->load( $orderItem->getProductId() ); @@ -150,6 +158,8 @@ public function execute(\Magento\Framework\Event\Observer $observer) )->setNumberOfDownloadsBought( $numberOfDownloads )->setStatus( + \Magento\Sales\Model\Order\Item::STATUS_PENDING == $orderStatusToEnableItem ? + \Magento\Downloadable\Model\Link\Purchased\Item::LINK_STATUS_AVAILABLE : \Magento\Downloadable\Model\Link\Purchased\Item::LINK_STATUS_PENDING )->setCreatedAt( $orderItem->getCreatedAt() @@ -165,6 +175,8 @@ public function execute(\Magento\Framework\Event\Observer $observer) } /** + * Create purchased model. + * * @return \Magento\Downloadable\Model\Link\Purchased */ protected function _createPurchasedModel() @@ -173,6 +185,8 @@ protected function _createPurchasedModel() } /** + * Create product model. + * * @return \Magento\Catalog\Model\Product */ protected function _createProductModel() @@ -181,6 +195,8 @@ protected function _createProductModel() } /** + * Create purchased item model. + * * @return \Magento\Downloadable\Model\Link\Purchased\Item */ protected function _createPurchasedItemModel() @@ -189,6 +205,8 @@ protected function _createPurchasedItemModel() } /** + * Create items collection. + * * @return \Magento\Downloadable\Model\ResourceModel\Link\Purchased\Item\Collection */ protected function _createItemsCollection() diff --git a/app/code/Magento/Downloadable/Observer/UpdateLinkPurchasedObserver.php b/app/code/Magento/Downloadable/Observer/UpdateLinkPurchasedObserver.php new file mode 100644 index 000000000000..db391ccda686 --- /dev/null +++ b/app/code/Magento/Downloadable/Observer/UpdateLinkPurchasedObserver.php @@ -0,0 +1,80 @@ +scopeConfig = $scopeConfig; + $this->purchasedFactory = $purchasedFactory; + $this->objectCopyService = $objectCopyService; + } + + /** + * Re-save order data after order update. + * + * @param \Magento\Framework\Event\Observer $observer + * @return $this + */ + public function execute(\Magento\Framework\Event\Observer $observer) + { + $order = $observer->getEvent()->getOrder(); + + if (!$order->getId()) { + //order not saved in the database + return $this; + } + + $purchasedLinks = $this->purchasedFactory->create()->addFieldToFilter( + 'order_id', + ['eq' => $order->getId()] + ); + + foreach ($purchasedLinks as $linkPurchased) { + $this->objectCopyService->copyFieldsetToTarget( + \downloadable_sales_copy_order::class, + 'to_downloadable', + $order, + $linkPurchased + ); + $linkPurchased->save(); + } + + return $this; + } +} diff --git a/app/code/Magento/Downloadable/Setup/Patch/Data/UpdateLinksExistDefaultAttributeValue.php b/app/code/Magento/Downloadable/Setup/Patch/Data/UpdateLinksExistDefaultAttributeValue.php new file mode 100644 index 000000000000..bde9accc39d7 --- /dev/null +++ b/app/code/Magento/Downloadable/Setup/Patch/Data/UpdateLinksExistDefaultAttributeValue.php @@ -0,0 +1,82 @@ +moduleDataSetup = $moduleDataSetup; + $this->eavSetupFactory = $eavSetupFactory; + } + + /** + * @inheritdoc + */ + public function apply() + { + /** @var EavSetup $eavSetup */ + $eavSetup = $this->eavSetupFactory->create(['setup' => $this->moduleDataSetup]); + // remove default value + $eavSetup->updateAttribute( + \Magento\Catalog\Model\Product::ENTITY, + 'links_exist', + 'default_value', + null + ); + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return [InstallDownloadableAttributes::class]; + } + + /** + * @inheritdoc + */ + public static function getVersion() + { + return '2.0.3'; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} 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 000000000000..b84dbff1154c --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/ActionGroup/VerifyProductTypeOrderActionGroup.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/app/code/Magento/Downloadable/Test/Mftf/Data/LinkData.xml b/app/code/Magento/Downloadable/Test/Mftf/Data/LinkData.xml index 38ac2c99e475..08f1c2349357 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Data/LinkData.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Data/LinkData.xml @@ -31,6 +31,28 @@ magento-logo.png https://static.magento.com/sites/all/themes/mag_redesign/images/magento-logo.svg
+ + link-1 + 2.43 + 2 + url + http://example.com + url + http://example.com + 0 + 1 + + + link-2 + 3 + 3 + url + http://example.com + url + http://example.com + 1 + 2 + SampleFile Upload File diff --git a/app/code/Magento/Downloadable/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Downloadable/Test/Mftf/Data/ProductData.xml index 6a91b60dcb58..c836b0d90d13 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Data/ProductData.xml @@ -19,6 +19,35 @@ 1 downloadableproduct + + downloadableproduct + downloadable + 4 + DownloadableProduct + 50.99 + 100 + 0 + 1 + downloadableproduct + 4 + EavStockItem + CustomAttributeCategoryIds + downloadableLink1 + + + downloadableproduct + downloadable + 4 + DownloadableProduct + 50.99 + 100 + 0 + 1 + downloadableproduct + CustomAttributeCategoryIds + downloadableLink1 + downloadableLink2 + api-downloadable-product downloadable diff --git a/app/code/Magento/Downloadable/Test/Mftf/Section/AdminProductDropdownOrderSection.xml b/app/code/Magento/Downloadable/Test/Mftf/Section/AdminProductDropdownOrderSection.xml new file mode 100644 index 000000000000..39b4e303d516 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Section/AdminProductDropdownOrderSection.xml @@ -0,0 +1,14 @@ + + + + +
+ +
+
diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminAddDefaultVideoDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminAddDefaultVideoDownloadableProductTest.xml index 88dcca095871..a7acdfded29b 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminAddDefaultVideoDownloadableProductTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminAddDefaultVideoDownloadableProductTest.xml @@ -7,7 +7,7 @@ --> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml new file mode 100755 index 000000000000..55740af4d834 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml @@ -0,0 +1,31 @@ + + + + + + + + + + <description value="After selecting a downloadable product when adding Admin should be switch to simple implicitly"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-10929"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <actionGroup ref="GoToSpecifiedCreateProductPage" stepKey="openProductFillForm"> + <argument name="productType" value="downloadable"/> + </actionGroup> + <!-- Fill form for Virtual Product Type --> + <actionGroup ref="fillMainProductForm" stepKey="fillProductForm"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Simple Product" stepKey="seeProductTypeInGrid"/> + </test> +</tests> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminDeleteDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminDeleteDownloadableProductTest.xml new file mode 100644 index 000000000000..d7e93d3429b9 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminDeleteDownloadableProductTest.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminDeleteDownloadableProductTest"> + <annotations> + <features value="Downloadable"/> + <title value="Delete Downloadable Product"/> + <description value="Admin should be able to delete a downloadable product"/> + <testCaseId value="MC-11018"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="DownloadableProductWithTwoLink" stepKey="createDownloadableProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="downloadableLink1" stepKey="addDownloadableLink1"> + <requiredEntity createDataKey="createDownloadableProduct"/> + </createData> + <createData entity="downloadableLink2" stepKey="addDownloadableLink2"> + <requiredEntity createDataKey="createDownloadableProduct"/> + </createData> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteDownloadableProductFilteredBySkuAndName"> + <argument name="product" value="$$createDownloadableProduct$$"/> + </actionGroup> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="A total of 1 record(s) have been deleted." stepKey="deleteMessage"/> + <!--Verify product on Product Page --> + <amOnPage url="{{StorefrontProductPage.url($$createDownloadableProduct.name$$)}}" stepKey="amOnDownloadableProductPage"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> + <!-- Search for the product by sku --> + <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createDownloadableProduct.sku$$" stepKey="fillSearchBarByProductSku"/> + <waitForPageLoad stepKey="waitForSearchButton"/> + <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForSearchResults"/> + <!-- Should not see any search results --> + <dontSee userInput="$$createDownloadableProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> + <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> + <!-- Go to the category page that we created in the before block --> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> + <!-- Should not see the product --> + <dontSee userInput="$$createDownloadableProduct.name$$" selector="{{StorefrontCategoryMainSection.productsList}}" stepKey="dontSeeProductInCategory"/> + <see selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" userInput="We can't find products matching the selection." stepKey="seeEmptyProductMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminRemoveDefaultImageDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminRemoveDefaultImageDownloadableProductTest.xml index 94809563de3c..3ee6cef47738 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminRemoveDefaultImageDownloadableProductTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminRemoveDefaultImageDownloadableProductTest.xml @@ -17,9 +17,6 @@ <severity value="MAJOR"/> <testCaseId value="MC-201"/> <group value="Downloadable"/> - <skip> - <issueId value="MAGETWO-94795"/> - </skip> </annotations> <before> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminRemoveDefaultVideoDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminRemoveDefaultVideoDownloadableProductTest.xml index 83d859ee7421..d8bbbb2b4d62 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminRemoveDefaultVideoDownloadableProductTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminRemoveDefaultVideoDownloadableProductTest.xml @@ -17,9 +17,6 @@ <severity value="MAJOR"/> <testCaseId value="MC-207"/> <group value="Downloadable"/> - <skip> - <issueId value="MAGETWO-94795"/> - </skip> </annotations> <!-- Create a downloadable product --> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdvanceCatalogSearchDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdvanceCatalogSearchDownloadableProductTest.xml index af5d20b075d1..66177b6875dd 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdvanceCatalogSearchDownloadableProductTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdvanceCatalogSearchDownloadableProductTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdvanceCatalogSearchDownloadableByNameTest" extends="AdvanceCatalogSearchSimpleProductByNameTest"> <annotations> <features value="Downloadable"/> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/NewProductsListWidgetDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/NewProductsListWidgetDownloadableProductTest.xml index 1a9ad271d62c..4864d11c884b 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/NewProductsListWidgetDownloadableProductTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/NewProductsListWidgetDownloadableProductTest.xml @@ -18,9 +18,6 @@ <testCaseId value="MC-124"/> <group value="Downloadable"/> <group value="WYSIWYGDisabled"/> - <skip> - <issueId value="MQE-1187"/> - </skip> </annotations> <!-- A Cms page containing the New Products Widget gets created here via extends --> diff --git a/app/code/Magento/Downloadable/Test/Unit/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/LinksTest.php b/app/code/Magento/Downloadable/Test/Unit/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/LinksTest.php index 8b9900d747ce..06b29fce1cd1 100644 --- a/app/code/Magento/Downloadable/Test/Unit/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/LinksTest.php +++ b/app/code/Magento/Downloadable/Test/Unit/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/LinksTest.php @@ -5,6 +5,14 @@ */ namespace Magento\Downloadable\Test\Unit\Block\Adminhtml\Catalog\Product\Edit\Tab\Downloadable; +/** + * Class LinksTest + * + * @package Magento\Downloadable\Test\Unit\Block\Adminhtml\Catalog\Product\Edit\Tab\Downloadable + * + * @deprecated + * @see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Links + */ class LinksTest extends \PHPUnit\Framework\TestCase { /** diff --git a/app/code/Magento/Downloadable/Test/Unit/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/SamplesTest.php b/app/code/Magento/Downloadable/Test/Unit/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/SamplesTest.php index e850923bbd06..f0423606add5 100644 --- a/app/code/Magento/Downloadable/Test/Unit/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/SamplesTest.php +++ b/app/code/Magento/Downloadable/Test/Unit/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/SamplesTest.php @@ -5,6 +5,14 @@ */ namespace Magento\Downloadable\Test\Unit\Block\Adminhtml\Catalog\Product\Edit\Tab\Downloadable; +/** + * Class SamplesTest + * + * @package Magento\Downloadable\Test\Unit\Block\Adminhtml\Catalog\Product\Edit\Tab\Downloadable + * + * @deprecated + * @see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Samples + */ class SamplesTest extends \PHPUnit\Framework\TestCase { /** diff --git a/app/code/Magento/Downloadable/Test/Unit/Controller/Adminhtml/Downloadable/File/UploadTest.php b/app/code/Magento/Downloadable/Test/Unit/Controller/Adminhtml/Downloadable/File/UploadTest.php index df1f127fae31..281fb571ff56 100644 --- a/app/code/Magento/Downloadable/Test/Unit/Controller/Adminhtml/Downloadable/File/UploadTest.php +++ b/app/code/Magento/Downloadable/Test/Unit/Controller/Adminhtml/Downloadable/File/UploadTest.php @@ -59,11 +59,6 @@ class UploadTest extends \PHPUnit\Framework\TestCase */ protected $fileHelper; - /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Backend\Model\Session - */ - protected $session; - /** * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\Controller\ResultFactory */ @@ -81,9 +76,6 @@ protected function setUp() ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $this->session = $this->getMockBuilder(\Magento\Backend\Model\Session::class) - ->disableOriginalConstructor() - ->getMock(); $this->resultFactory = $this->getMockBuilder(\Magento\Framework\Controller\ResultFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) @@ -108,9 +100,6 @@ protected function setUp() $this->context->expects($this->any()) ->method('getRequest') ->will($this->returnValue($this->request)); - $this->context->expects($this->any()) - ->method('getSession') - ->will($this->returnValue($this->session)); $this->context->expects($this->any()) ->method('getResultFactory') ->will($this->returnValue($this->resultFactory)); @@ -154,11 +143,6 @@ public function testExecute() $this->uploaderFactory->expects($this->once())->method('create')->willReturn($uploader); $this->fileHelper->expects($this->once())->method('uploadFromTmp')->willReturn($data); $this->storageDatabase->expects($this->once())->method('saveFile'); - $this->session->expects($this->once())->method('getName')->willReturn('Name'); - $this->session->expects($this->once())->method('getSessionId')->willReturn('SessionId'); - $this->session->expects($this->once())->method('getCookieLifetime')->willReturn('CookieLifetime'); - $this->session->expects($this->once())->method('getCookiePath')->willReturn('CookiePath'); - $this->session->expects($this->once())->method('getCookieDomain')->willReturn('CookieDomain'); $this->resultFactory->expects($this->once())->method('create')->willReturn($resultJson); $resultJson->expects($this->once())->method('setData')->willReturnSelf(); diff --git a/app/code/Magento/Downloadable/Test/Unit/Helper/DownloadTest.php b/app/code/Magento/Downloadable/Test/Unit/Helper/DownloadTest.php index 7cb5b03b0385..9551cfe982bd 100644 --- a/app/code/Magento/Downloadable/Test/Unit/Helper/DownloadTest.php +++ b/app/code/Magento/Downloadable/Test/Unit/Helper/DownloadTest.php @@ -89,7 +89,7 @@ public function testSetResourceInvalidPath() /** * @expectedException \Magento\Framework\Exception\LocalizedException - * @exectedExceptionMessage Please set resource file and link type. + * @expectedExceptionMessage Please set resource file and link type. */ public function testGetFileSizeNoResource() { 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 a352c4bdf7bc..c4824f913daf 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 1587163ba812..81c5918eb24a 100644 --- a/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Samples.php +++ b/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Samples.php @@ -3,21 +3,23 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier; -use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier; use Magento\Catalog\Model\Locator\LocatorInterface; +use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier; use Magento\Downloadable\Model\Product\Type; use Magento\Downloadable\Model\Source\TypeUpload; -use Magento\Store\Model\StoreManagerInterface; use Magento\Framework\Stdlib\ArrayManager; -use Magento\Ui\Component\DynamicRows; use Magento\Framework\UrlInterface; +use Magento\Store\Model\StoreManagerInterface; use Magento\Ui\Component\Container; +use Magento\Ui\Component\DynamicRows; use Magento\Ui\Component\Form; /** - * Class adds a grid with samples + * Class adds a grid with samples. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Samples extends AbstractModifier @@ -77,7 +79,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function modifyData(array $data) { @@ -90,7 +92,7 @@ public function modifyData(array $data) } /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function modifyMeta(array $meta) @@ -135,6 +137,8 @@ public function modifyMeta(array $meta) } /** + * Returns configuration for dynamic rows + * * @return array */ protected function getDynamicRows() @@ -155,6 +159,8 @@ protected function getDynamicRows() } /** + * Returns Record column configuration + * * @return array */ protected function getRecord() @@ -192,6 +198,8 @@ protected function getRecord() } /** + * Returns Title column configuration + * * @return array */ protected function getTitleColumn() @@ -203,12 +211,14 @@ protected function getTitleColumn() 'showLabel' => false, 'label' => __('Title'), 'dataScope' => '', + 'sortOrder' => 10, ]; $titleField['arguments']['data']['config'] = [ 'formElement' => Form\Element\Input::NAME, 'componentType' => Form\Field::NAME, 'dataType' => Form\Element\DataType\Text::NAME, 'dataScope' => 'title', + 'labelVisible' => false, 'validation' => [ 'required-entry' => true, ], @@ -218,6 +228,8 @@ protected function getTitleColumn() } /** + * Returns Sample column configuration + * * @return array */ protected function getSampleColumn() @@ -229,6 +241,7 @@ protected function getSampleColumn() 'label' => __('File'), 'showLabel' => false, 'dataScope' => '', + 'sortOrder' => 20, ]; $sampleType['arguments']['data']['config'] = [ 'formElement' => Form\Element\Select::NAME, @@ -236,6 +249,7 @@ protected function getSampleColumn() 'component' => 'Magento_Downloadable/js/components/upload-type-handler', 'dataType' => Form\Element\DataType\Text::NAME, 'dataScope' => 'type', + 'labelVisible' => false, 'options' => $this->typeUpload->toOptionArray(), 'typeFile' => 'sample_file', 'typeUrl' => 'sample_url', @@ -246,6 +260,7 @@ protected function getSampleColumn() 'dataType' => Form\Element\DataType\Text::NAME, 'dataScope' => 'sample_url', 'placeholder' => 'URL', + 'labelVisible' => false, 'validation' => [ 'required-entry' => true, 'validate-url' => true, diff --git a/app/code/Magento/Downloadable/etc/db_schema.xml b/app/code/Magento/Downloadable/etc/db_schema.xml index ed25628bcffd..89d47644661a 100644 --- a/app/code/Magento/Downloadable/etc/db_schema.xml +++ b/app/code/Magento/Downloadable/etc/db_schema.xml @@ -24,13 +24,13 @@ <column xsi:type="varchar" name="sample_url" nullable="true" length="255" comment="Sample Url"/> <column xsi:type="varchar" name="sample_file" nullable="true" length="255" comment="Sample File"/> <column xsi:type="varchar" name="sample_type" nullable="true" length="20" comment="Sample Type"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="link_id"/> </constraint> - <constraint xsi:type="foreign" name="DOWNLOADABLE_LINK_PRODUCT_ID_CATALOG_PRODUCT_ENTITY_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="DOWNLOADABLE_LINK_PRODUCT_ID_CATALOG_PRODUCT_ENTITY_ENTITY_ID" table="downloadable_link" column="product_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <index name="DOWNLOADABLE_LINK_PRODUCT_ID_SORT_ORDER" indexType="btree"> + <index referenceId="DOWNLOADABLE_LINK_PRODUCT_ID_SORT_ORDER" indexType="btree"> <column name="product_id"/> <column name="sort_order"/> </index> @@ -44,19 +44,19 @@ default="0" comment="Website ID"/> <column xsi:type="decimal" name="price" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Price"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="price_id"/> </constraint> - <constraint xsi:type="foreign" name="DOWNLOADABLE_LINK_PRICE_LINK_ID_DOWNLOADABLE_LINK_LINK_ID" + <constraint xsi:type="foreign" referenceId="DOWNLOADABLE_LINK_PRICE_LINK_ID_DOWNLOADABLE_LINK_LINK_ID" table="downloadable_link_price" column="link_id" referenceTable="downloadable_link" referenceColumn="link_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="DOWNLOADABLE_LINK_PRICE_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID" + <constraint xsi:type="foreign" referenceId="DOWNLOADABLE_LINK_PRICE_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID" table="downloadable_link_price" column="website_id" referenceTable="store_website" referenceColumn="website_id" onDelete="CASCADE"/> - <index name="DOWNLOADABLE_LINK_PRICE_LINK_ID" indexType="btree"> + <index referenceId="DOWNLOADABLE_LINK_PRICE_LINK_ID" indexType="btree"> <column name="link_id"/> </index> - <index name="DOWNLOADABLE_LINK_PRICE_WEBSITE_ID" indexType="btree"> + <index referenceId="DOWNLOADABLE_LINK_PRICE_WEBSITE_ID" indexType="btree"> <column name="website_id"/> </index> </table> @@ -78,22 +78,22 @@ <column xsi:type="varchar" name="product_name" nullable="true" length="255" comment="Product name"/> <column xsi:type="varchar" name="product_sku" nullable="true" length="255" comment="Product sku"/> <column xsi:type="varchar" name="link_section_title" nullable="true" length="255" comment="Link_section_title"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="purchased_id"/> </constraint> - <constraint xsi:type="foreign" name="DL_LNK_PURCHASED_CSTR_ID_CSTR_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="DL_LNK_PURCHASED_CSTR_ID_CSTR_ENTT_ENTT_ID" table="downloadable_link_purchased" column="customer_id" referenceTable="customer_entity" referenceColumn="entity_id" onDelete="SET NULL"/> - <constraint xsi:type="foreign" name="DOWNLOADABLE_LINK_PURCHASED_ORDER_ID_SALES_ORDER_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="DOWNLOADABLE_LINK_PURCHASED_ORDER_ID_SALES_ORDER_ENTITY_ID" table="downloadable_link_purchased" column="order_id" referenceTable="sales_order" referenceColumn="entity_id" onDelete="SET NULL"/> - <index name="DOWNLOADABLE_LINK_PURCHASED_ORDER_ID" indexType="btree"> + <index referenceId="DOWNLOADABLE_LINK_PURCHASED_ORDER_ID" indexType="btree"> <column name="order_id"/> </index> - <index name="DOWNLOADABLE_LINK_PURCHASED_ORDER_ITEM_ID" indexType="btree"> + <index referenceId="DOWNLOADABLE_LINK_PURCHASED_ORDER_ITEM_ID" indexType="btree"> <column name="order_item_id"/> </index> - <index name="DOWNLOADABLE_LINK_PURCHASED_CUSTOMER_ID" indexType="btree"> + <index referenceId="DOWNLOADABLE_LINK_PURCHASED_CUSTOMER_ID" indexType="btree"> <column name="customer_id"/> </index> </table> @@ -125,22 +125,22 @@ comment="Creation Time"/> <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" comment="Update Time"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="item_id"/> </constraint> - <constraint xsi:type="foreign" name="DL_LNK_PURCHASED_ITEM_PURCHASED_ID_DL_LNK_PURCHASED_PURCHASED_ID" + <constraint xsi:type="foreign" referenceId="DL_LNK_PURCHASED_ITEM_PURCHASED_ID_DL_LNK_PURCHASED_PURCHASED_ID" table="downloadable_link_purchased_item" column="purchased_id" referenceTable="downloadable_link_purchased" referenceColumn="purchased_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="DL_LNK_PURCHASED_ITEM_ORDER_ITEM_ID_SALES_ORDER_ITEM_ITEM_ID" + <constraint xsi:type="foreign" referenceId="DL_LNK_PURCHASED_ITEM_ORDER_ITEM_ID_SALES_ORDER_ITEM_ITEM_ID" table="downloadable_link_purchased_item" column="order_item_id" referenceTable="sales_order_item" referenceColumn="item_id" onDelete="SET NULL"/> - <index name="DOWNLOADABLE_LINK_PURCHASED_ITEM_LINK_HASH" indexType="btree"> + <index referenceId="DOWNLOADABLE_LINK_PURCHASED_ITEM_LINK_HASH" indexType="btree"> <column name="link_hash"/> </index> - <index name="DOWNLOADABLE_LINK_PURCHASED_ITEM_ORDER_ITEM_ID" indexType="btree"> + <index referenceId="DOWNLOADABLE_LINK_PURCHASED_ITEM_ORDER_ITEM_ID" indexType="btree"> <column name="order_item_id"/> </index> - <index name="DOWNLOADABLE_LINK_PURCHASED_ITEM_PURCHASED_ID" indexType="btree"> + <index referenceId="DOWNLOADABLE_LINK_PURCHASED_ITEM_PURCHASED_ID" indexType="btree"> <column name="purchased_id"/> </index> </table> @@ -152,20 +152,20 @@ <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Store ID"/> <column xsi:type="varchar" name="title" nullable="true" length="255" comment="Title"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="title_id"/> </constraint> - <constraint xsi:type="foreign" name="DOWNLOADABLE_LINK_TITLE_LINK_ID_DOWNLOADABLE_LINK_LINK_ID" + <constraint xsi:type="foreign" referenceId="DOWNLOADABLE_LINK_TITLE_LINK_ID_DOWNLOADABLE_LINK_LINK_ID" table="downloadable_link_title" column="link_id" referenceTable="downloadable_link" referenceColumn="link_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="DOWNLOADABLE_LINK_TITLE_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="DOWNLOADABLE_LINK_TITLE_STORE_ID_STORE_STORE_ID" table="downloadable_link_title" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="DOWNLOADABLE_LINK_TITLE_LINK_ID_STORE_ID"> + <constraint xsi:type="unique" referenceId="DOWNLOADABLE_LINK_TITLE_LINK_ID_STORE_ID"> <column name="link_id"/> <column name="store_id"/> </constraint> - <index name="DOWNLOADABLE_LINK_TITLE_STORE_ID" indexType="btree"> + <index referenceId="DOWNLOADABLE_LINK_TITLE_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -179,13 +179,13 @@ <column xsi:type="varchar" name="sample_type" nullable="true" length="20" comment="Sample Type"/> <column xsi:type="int" name="sort_order" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Sort Order"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="sample_id"/> </constraint> - <constraint xsi:type="foreign" name="DOWNLOADABLE_SAMPLE_PRODUCT_ID_CATALOG_PRODUCT_ENTITY_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="DOWNLOADABLE_SAMPLE_PRODUCT_ID_CATALOG_PRODUCT_ENTITY_ENTITY_ID" table="downloadable_sample" column="product_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <index name="DOWNLOADABLE_SAMPLE_PRODUCT_ID" indexType="btree"> + <index referenceId="DOWNLOADABLE_SAMPLE_PRODUCT_ID" indexType="btree"> <column name="product_id"/> </index> </table> @@ -198,20 +198,20 @@ <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Store ID"/> <column xsi:type="varchar" name="title" nullable="true" length="255" comment="Title"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="title_id"/> </constraint> - <constraint xsi:type="foreign" name="DL_SAMPLE_TTL_SAMPLE_ID_DL_SAMPLE_SAMPLE_ID" + <constraint xsi:type="foreign" referenceId="DL_SAMPLE_TTL_SAMPLE_ID_DL_SAMPLE_SAMPLE_ID" table="downloadable_sample_title" column="sample_id" referenceTable="downloadable_sample" referenceColumn="sample_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="DOWNLOADABLE_SAMPLE_TITLE_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="DOWNLOADABLE_SAMPLE_TITLE_STORE_ID_STORE_STORE_ID" table="downloadable_sample_title" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="DOWNLOADABLE_SAMPLE_TITLE_SAMPLE_ID_STORE_ID"> + <constraint xsi:type="unique" referenceId="DOWNLOADABLE_SAMPLE_TITLE_SAMPLE_ID_STORE_ID"> <column name="sample_id"/> <column name="store_id"/> </constraint> - <index name="DOWNLOADABLE_SAMPLE_TITLE_STORE_ID" indexType="btree"> + <index referenceId="DOWNLOADABLE_SAMPLE_TITLE_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -227,7 +227,7 @@ default="0" comment="Minimum price"/> <column xsi:type="decimal" name="max_price" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Maximum price"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> <column name="customer_group_id"/> <column name="website_id"/> @@ -245,7 +245,7 @@ default="0" comment="Minimum price"/> <column xsi:type="decimal" name="max_price" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Maximum price"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> <column name="customer_group_id"/> <column name="website_id"/> diff --git a/app/code/Magento/Downloadable/etc/di.xml b/app/code/Magento/Downloadable/etc/di.xml index 932e48e88088..4e9b0b55afb0 100644 --- a/app/code/Magento/Downloadable/etc/di.xml +++ b/app/code/Magento/Downloadable/etc/di.xml @@ -77,6 +77,13 @@ </argument> </arguments> </type> + <type name="Magento\Sales\Model\Order\ProductOption"> + <arguments> + <argument name="processorPool" xsi:type="array"> + <item name="downloadable" xsi:type="object">Magento\Downloadable\Model\ProductOptionProcessor</item> + </argument> + </arguments> + </type> <preference for="Magento\Downloadable\Api\LinkRepositoryInterface" type="Magento\Downloadable\Model\LinkRepository" /> <preference for="Magento\Downloadable\Api\SampleRepositoryInterface" type="Magento\Downloadable\Model\SampleRepository" /> <preference for="Magento\Downloadable\Api\Data\LinkInterface" type="Magento\Downloadable\Model\Link" /> diff --git a/app/code/Magento/Downloadable/etc/events.xml b/app/code/Magento/Downloadable/etc/events.xml index e4f03ff238d4..21cc50ddc966 100644 --- a/app/code/Magento/Downloadable/etc/events.xml +++ b/app/code/Magento/Downloadable/etc/events.xml @@ -6,11 +6,12 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd"> - <event name="sales_order_item_save_commit_after"> + <event name="sales_order_item_save_after"> <observer name="downloadable_observer" instance="Magento\Downloadable\Observer\SaveDownloadableOrderItemObserver" /> </event> - <event name="sales_order_save_commit_after"> + <event name="sales_order_save_after"> <observer name="downloadable_observer" instance="Magento\Downloadable\Observer\SetLinkStatusObserver" /> + <observer name="downloadable_observer_assign_customer" instance="Magento\Downloadable\Observer\UpdateLinkPurchasedObserver" /> </event> <event name="sales_model_service_quote_submit_success"> <observer name="checkout_type_onepage_save_order_after" instance="Magento\Downloadable\Observer\SetHasDownloadableProductsObserver" /> diff --git a/app/code/Magento/Downloadable/view/adminhtml/layout/catalog_product_downloadable.xml b/app/code/Magento/Downloadable/view/adminhtml/layout/catalog_product_downloadable.xml index 19b485f0b782..0352c98bfa56 100644 --- a/app/code/Magento/Downloadable/view/adminhtml/layout/catalog_product_downloadable.xml +++ b/app/code/Magento/Downloadable/view/adminhtml/layout/catalog_product_downloadable.xml @@ -5,6 +5,10 @@ * See COPYING.txt for license details. */ --> +<!-- +@deprecated Adminhtml Blocks extending for Downloadable products have neen moved to UI components +@see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Composite +--> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <update handle="downloadable_items"/> <body> diff --git a/app/code/Magento/Downloadable/view/adminhtml/layout/catalog_product_simple.xml b/app/code/Magento/Downloadable/view/adminhtml/layout/catalog_product_simple.xml index 843f9b402564..d424db980f7a 100644 --- a/app/code/Magento/Downloadable/view/adminhtml/layout/catalog_product_simple.xml +++ b/app/code/Magento/Downloadable/view/adminhtml/layout/catalog_product_simple.xml @@ -5,6 +5,10 @@ * See COPYING.txt for license details. */ --> +<!-- +@deprecated Adminhtml Blocks extending for Downloadable products have neen moved to UI components +@see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Composite +--> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <update handle="downloadable_items"/> </page> diff --git a/app/code/Magento/Downloadable/view/adminhtml/layout/catalog_product_view_type_downloadable.xml b/app/code/Magento/Downloadable/view/adminhtml/layout/catalog_product_view_type_downloadable.xml index d1e551ff1c96..9c88e1ba15c4 100644 --- a/app/code/Magento/Downloadable/view/adminhtml/layout/catalog_product_view_type_downloadable.xml +++ b/app/code/Magento/Downloadable/view/adminhtml/layout/catalog_product_view_type_downloadable.xml @@ -5,6 +5,10 @@ * See COPYING.txt for license details. */ --> +<!-- +@deprecated Adminhtml Blocks extending for Downloadable products have neen moved to UI components +@see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Composite +--> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceBlock name="product.composite.fieldset"> diff --git a/app/code/Magento/Downloadable/view/adminhtml/layout/catalog_product_virtual.xml b/app/code/Magento/Downloadable/view/adminhtml/layout/catalog_product_virtual.xml index 843f9b402564..d424db980f7a 100644 --- a/app/code/Magento/Downloadable/view/adminhtml/layout/catalog_product_virtual.xml +++ b/app/code/Magento/Downloadable/view/adminhtml/layout/catalog_product_virtual.xml @@ -5,6 +5,10 @@ * See COPYING.txt for license details. */ --> +<!-- +@deprecated Adminhtml Blocks extending for Downloadable products have neen moved to UI components +@see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Composite +--> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <update handle="downloadable_items"/> </page> diff --git a/app/code/Magento/Downloadable/view/adminhtml/layout/downloadable_items.xml b/app/code/Magento/Downloadable/view/adminhtml/layout/downloadable_items.xml index 958e922334db..fec90e7049be 100644 --- a/app/code/Magento/Downloadable/view/adminhtml/layout/downloadable_items.xml +++ b/app/code/Magento/Downloadable/view/adminhtml/layout/downloadable_items.xml @@ -5,6 +5,10 @@ * See COPYING.txt for license details. */ --> +<!-- +@deprecated Adminhtml Blocks extending for Downloadable products have neen moved to UI components +@see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Composite +--> <layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_layout.xsd"> <!--<referenceContainer name="product_form">--> <!--<block name="downloadable_items" class="Magento\Downloadable\Block\Adminhtml\Catalog\Product\Edit\Tab\Downloadable">--> diff --git a/app/code/Magento/Downloadable/view/adminhtml/templates/product/composite/fieldset/downloadable.phtml b/app/code/Magento/Downloadable/view/adminhtml/templates/product/composite/fieldset/downloadable.phtml index c86eb56a3900..c2338e30ecd3 100644 --- a/app/code/Magento/Downloadable/view/adminhtml/templates/product/composite/fieldset/downloadable.phtml +++ b/app/code/Magento/Downloadable/view/adminhtml/templates/product/composite/fieldset/downloadable.phtml @@ -4,6 +4,7 @@ * See COPYING.txt for license details. */ +// @deprecated // @codingStandardsIgnoreFile ?> @@ -29,7 +30,7 @@ value="<?= /* @escapeNotVerified */ $_link->getId() ?>" <?= /* @escapeNotVerified */ $block->getLinkCheckedValue($_link) ?> price="<?= /* @escapeNotVerified */ $block->getCurrencyPrice($_link->getPrice()) ?>"/> <?php endif; ?> - <label for="links_<?= /* @escapeNotVerified */ $_link->getId() ?>" class="label"> + <label for="links_<?= /* @escapeNotVerified */ $_link->getId() ?>" class="label admin__field-label"> <?= $block->escapeHtml($_link->getTitle()) ?> <?php if ($_link->getSampleFile() || $_link->getSampleUrl()): ?>  (<a href="<?= /* @escapeNotVerified */ $block->getLinkSampleUrl($_link) ?>" <?= $block->getIsOpenInNewWindow()?'onclick="this.target=\'_blank\'"':'' ?>><?= /* @escapeNotVerified */ __('sample') ?></a>) diff --git a/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable.phtml b/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable.phtml index 7dc547c5e275..a4443edb08e6 100644 --- a/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable.phtml +++ b/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable.phtml @@ -4,6 +4,7 @@ * See COPYING.txt for license details. */ +// @deprecated // @codingStandardsIgnoreFile ?> diff --git a/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable/links.phtml b/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable/links.phtml index 3ec6010218fb..c86019d9cd20 100644 --- a/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable/links.phtml +++ b/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable/links.phtml @@ -4,6 +4,7 @@ * See COPYING.txt for license details. */ +// @deprecated // @codingStandardsIgnoreFile ?> diff --git a/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable/samples.phtml b/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable/samples.phtml index 3645a184df21..947d1d0b38be 100644 --- a/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable/samples.phtml +++ b/app/code/Magento/Downloadable/view/adminhtml/templates/product/edit/downloadable/samples.phtml @@ -4,6 +4,7 @@ * See COPYING.txt for license details. */ +// @deprecated // @codingStandardsIgnoreFile ?> diff --git a/app/code/Magento/Downloadable/view/frontend/layout/catalog_product_view_type_downloadable.xml b/app/code/Magento/Downloadable/view/frontend/layout/catalog_product_view_type_downloadable.xml index 45e5f0b8da72..f851558c1a56 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/Downloadable/view/frontend/requirejs-config.js b/app/code/Magento/Downloadable/view/frontend/requirejs-config.js index 59d0558decd1..f615966d801f 100644 --- a/app/code/Magento/Downloadable/view/frontend/requirejs-config.js +++ b/app/code/Magento/Downloadable/view/frontend/requirejs-config.js @@ -6,7 +6,8 @@ var config = { map: { '*': { - downloadable: 'Magento_Downloadable/downloadable' + downloadable: 'Magento_Downloadable/js/downloadable', + 'Magento_Downloadable/downloadable': 'Magento_Downloadable/js/downloadable' } } }; diff --git a/app/code/Magento/Downloadable/view/frontend/web/downloadable.js b/app/code/Magento/Downloadable/view/frontend/web/js/downloadable.js similarity index 100% rename from app/code/Magento/Downloadable/view/frontend/web/downloadable.js rename to app/code/Magento/Downloadable/view/frontend/web/js/downloadable.js diff --git a/app/code/Magento/DownloadableGraphQl/Model/DownloadableProductTypeResolver.php b/app/code/Magento/DownloadableGraphQl/Model/DownloadableProductTypeResolver.php index 59c007d91076..4bef5d3d57b0 100644 --- a/app/code/Magento/DownloadableGraphQl/Model/DownloadableProductTypeResolver.php +++ b/app/code/Magento/DownloadableGraphQl/Model/DownloadableProductTypeResolver.php @@ -8,19 +8,21 @@ namespace Magento\DownloadableGraphQl\Model; use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; +use Magento\Downloadable\Model\Product\Type as Type; /** - * {@inheritdoc} + * @inheritdoc */ class DownloadableProductTypeResolver implements TypeResolverInterface { + const DOWNLOADABLE_PRODUCT = 'DownloadableProduct'; /** - * {@inheritdoc} + * @inheritdoc */ public function resolveType(array $data) : string { - if (isset($data['type_id']) && $data['type_id'] == 'downloadable') { - return 'DownloadableProduct'; + if (isset($data['type_id']) && $data['type_id'] == Type::TYPE_DOWNLOADABLE) { + return self::DOWNLOADABLE_PRODUCT; } return ''; } diff --git a/app/code/Magento/DownloadableGraphQl/Model/Resolver/CustomerDownloadableProducts.php b/app/code/Magento/DownloadableGraphQl/Model/Resolver/CustomerDownloadableProducts.php new file mode 100644 index 000000000000..b981e0288566 --- /dev/null +++ b/app/code/Magento/DownloadableGraphQl/Model/Resolver/CustomerDownloadableProducts.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\DownloadableGraphQl\Model\Resolver; + +use Magento\DownloadableGraphQl\Model\ResourceModel\GetPurchasedDownloadableProducts; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\UrlInterface; + +/** + * @inheritdoc + * + * Returns available downloadable products for customer + */ +class CustomerDownloadableProducts implements ResolverInterface +{ + /** + * @var GetPurchasedDownloadableProducts + */ + private $getPurchasedDownloadableProducts; + + /** + * @var UrlInterface + */ + private $urlBuilder; + + /** + * @param GetPurchasedDownloadableProducts $getPurchasedDownloadableProducts + * @param UrlInterface $urlBuilder + */ + public function __construct( + GetPurchasedDownloadableProducts $getPurchasedDownloadableProducts, + UrlInterface $urlBuilder + ) { + $this->getPurchasedDownloadableProducts = $getPurchasedDownloadableProducts; + $this->urlBuilder = $urlBuilder; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + $currentUserId = $context->getUserId(); + $purchasedProducts = $this->getPurchasedDownloadableProducts->execute($currentUserId); + $productsData = []; + + /* The fields names are hardcoded since there's no existing name reference in the code */ + foreach ($purchasedProducts as $purchasedProduct) { + if ($purchasedProduct['number_of_downloads_bought']) { + $remainingDownloads = $purchasedProduct['number_of_downloads_bought'] - + $purchasedProduct['number_of_downloads_used']; + } else { + $remainingDownloads = __('Unlimited'); + } + + $productsData[] = [ + 'order_increment_id' => $purchasedProduct['order_increment_id'], + 'date' => $purchasedProduct['created_at'], + 'status' => $purchasedProduct['status'], + 'download_url' => $this->urlBuilder->getUrl( + 'downloadable/download/link', + ['id' => $purchasedProduct['link_hash'], '_secure' => true] + ), + 'remaining_downloads' => $remainingDownloads + ]; + } + + return ['items' => $productsData]; + } +} diff --git a/app/code/Magento/DownloadableGraphQl/Model/Resolver/Product/DownloadableOptions.php b/app/code/Magento/DownloadableGraphQl/Model/Resolver/Product/DownloadableOptions.php index cda853a16afa..a1e25663a9c3 100644 --- a/app/code/Magento/DownloadableGraphQl/Model/Resolver/Product/DownloadableOptions.php +++ b/app/code/Magento/DownloadableGraphQl/Model/Resolver/Product/DownloadableOptions.php @@ -7,6 +7,8 @@ namespace Magento\DownloadableGraphQl\Model\Resolver\Product; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Catalog\Model\Product; use Magento\Downloadable\Helper\Data as DownloadableHelper; @@ -16,14 +18,12 @@ use Magento\Framework\Data\Collection; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\EnumLookup; -use Magento\Framework\GraphQl\Query\Resolver\Value; -use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; /** - * Format for downloadable product types + * @inheritdoc * - * {@inheritdoc} + * Format for downloadable product types */ class DownloadableOptions implements ResolverInterface { @@ -47,36 +47,36 @@ class DownloadableOptions implements ResolverInterface */ private $linkCollection; - /** - * @var ValueFactory - */ - private $valueFactory; - /** * @param EnumLookup $enumLookup * @param DownloadableHelper $downloadableHelper * @param SampleCollection $sampleCollection * @param LinkCollection $linkCollection - * @param ValueFactory $valueFactory */ public function __construct( EnumLookup $enumLookup, DownloadableHelper $downloadableHelper, SampleCollection $sampleCollection, - LinkCollection $linkCollection, - ValueFactory $valueFactory + LinkCollection $linkCollection ) { $this->enumLookup = $enumLookup; $this->downloadableHelper = $downloadableHelper; $this->sampleCollection = $sampleCollection; $this->linkCollection = $linkCollection; - $this->valueFactory = $valueFactory; } /** + * @inheritdoc + * * Add downloadable options to configurable types * - * {@inheritdoc} + * @param \Magento\Framework\GraphQl\Config\Element\Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * @throws \Exception + * @return null|array */ public function resolve( Field $field, @@ -84,12 +84,9 @@ public function resolve( ResolveInfo $info, array $value = null, array $args = null - ): Value { + ) { if (!isset($value['model'])) { - $result = function () { - return null; - }; - return $this->valueFactory->create($result); + throw new LocalizedException(__('"model" value should be specified')); } /** @var Product $product */ @@ -113,11 +110,7 @@ public function resolve( } } - $result = function () use ($data) { - return $data; - }; - - return $this->valueFactory->create($result); + return $data; } /** diff --git a/app/code/Magento/DownloadableGraphQl/Model/ResourceModel/GetPurchasedDownloadableProducts.php b/app/code/Magento/DownloadableGraphQl/Model/ResourceModel/GetPurchasedDownloadableProducts.php new file mode 100644 index 000000000000..e8c29e90609f --- /dev/null +++ b/app/code/Magento/DownloadableGraphQl/Model/ResourceModel/GetPurchasedDownloadableProducts.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\DownloadableGraphQl\Model\ResourceModel; + +use Magento\Framework\App\ResourceConnection; +use Magento\Downloadable\Model\Link\Purchased\Item; + +/** + * Class GetPurchasedDownloadableProducts + * + * The model returns all purchased products for the specified customer + */ +class GetPurchasedDownloadableProducts +{ + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @param ResourceConnection $resourceConnection + */ + public function __construct( + ResourceConnection $resourceConnection + ) { + $this->resourceConnection = $resourceConnection; + } + + /** + * Return available purchased products for customer + * + * @param int $customerId + * @return array + */ + public function execute(int $customerId): array + { + $connection = $this->resourceConnection->getConnection(); + $allowedItemsStatuses = [Item::LINK_STATUS_PENDING_PAYMENT, Item::LINK_STATUS_PAYMENT_REVIEW]; + $downloadablePurchasedTable = $connection->getTableName('downloadable_link_purchased'); + + /* The fields names are hardcoded since there's no existing name reference in the code */ + $selectQuery = $connection->select() + ->from($downloadablePurchasedTable) + ->joinLeft( + ['item' => $connection->getTableName('downloadable_link_purchased_item')], + "$downloadablePurchasedTable.purchased_id = item.purchased_id" + ) + ->where("$downloadablePurchasedTable.customer_id = ?", $customerId) + ->where('item.status NOT IN (?)', $allowedItemsStatuses); + + return $connection->fetchAll($selectQuery); + } +} diff --git a/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls b/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls index 8e877ffe8360..788a5fc601ee 100644 --- a/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls +++ b/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls @@ -1,6 +1,22 @@ # Copyright © Magento, Inc. All rights reserved. # See COPYING.txt for license details. +type Query { + customerDownloadableProducts: CustomerDownloadableProducts @resolver(class: "Magento\\DownloadableGraphQl\\Model\\Resolver\\CustomerDownloadableProducts") @doc(description: "The query returns the contents of a customer's downloadable products") @cache(cacheable: false) +} + +type CustomerDownloadableProducts { + items: [CustomerDownloadableProduct] @doc(description: "List of purchased downloadable items") +} + +type CustomerDownloadableProduct { + order_increment_id: String + date: String + status: String + download_url: String + remaining_downloads: String +} + type DownloadableProduct implements ProductInterface, CustomizableProductInterface @doc(description: "DownloadableProduct defines a product that the customer downloads") { downloadable_product_samples: [DownloadableProductSamples] @resolver(class: "Magento\\DownloadableGraphQl\\Model\\Resolver\\Product\\DownloadableOptions") @doc(description: "An array containing information about samples of this downloadable product.") downloadable_product_links: [DownloadableProductLinks] @resolver(class: "Magento\\DownloadableGraphQl\\Model\\Resolver\\Product\\DownloadableOptions") @doc(description: "An array containing information about the links for this downloadable product") diff --git a/app/code/Magento/DownloadableImportExport/Helper/Uploader.php b/app/code/Magento/DownloadableImportExport/Helper/Uploader.php index 46081f77cc66..197250faaea9 100644 --- a/app/code/Magento/DownloadableImportExport/Helper/Uploader.php +++ b/app/code/Magento/DownloadableImportExport/Helper/Uploader.php @@ -38,6 +38,11 @@ class Uploader extends \Magento\Framework\App\Helper\AbstractHelper */ protected $parameters = []; + /** + * @var \Magento\Framework\DB\Adapter\AdapterInterface + */ + public $connection; + /** * Construct * diff --git a/app/code/Magento/Eav/Api/AttributeSetRepositoryInterface.php b/app/code/Magento/Eav/Api/AttributeSetRepositoryInterface.php index 8bd65ba85abe..be4b5a3fa0fe 100644 --- a/app/code/Magento/Eav/Api/AttributeSetRepositoryInterface.php +++ b/app/code/Magento/Eav/Api/AttributeSetRepositoryInterface.php @@ -17,7 +17,7 @@ interface AttributeSetRepositoryInterface * Retrieve list of Attribute Sets * * This call returns an array of objects, but detailed information about each object’s attributes might not be - * included. See http://devdocs.magento.com/codelinks/attributes.html#AttributeSetRepositoryInterface to determine + * included. See https://devdocs.magento.com/codelinks/attributes.html#AttributeSetRepositoryInterface to determine * which call to use to get detailed information about all attributes for an object. * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria diff --git a/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Main/AbstractMain.php b/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Main/AbstractMain.php index c5a18a3de99c..be9d2700664c 100644 --- a/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Main/AbstractMain.php +++ b/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Main/AbstractMain.php @@ -4,15 +4,13 @@ * See COPYING.txt for license details. */ -/** - * Product attribute add/edit form main tab - * - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Eav\Block\Adminhtml\Attribute\Edit\Main; use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +/** + * Product attribute add/edit form main tab + */ abstract class AbstractMain extends \Magento\Backend\Block\Widget\Form\Generic { /** @@ -110,7 +108,6 @@ protected function _prepareForm() /** @var \Magento\Framework\Data\Form $form */ $form = $this->_formFactory->create( - ['data' => ['id' => 'edit_form', 'action' => $this->getData('action'), 'method' => 'post']] ); @@ -280,10 +277,11 @@ protected function _initFormValues() } /** - * Processing block html after rendering + * Processing block html after rendering. + * * Adding js block to the end of this block * - * @param string $html + * @param string $html * @return string */ protected function _afterToHtml($html) diff --git a/app/code/Magento/Eav/Model/Attribute/Data/AbstractData.php b/app/code/Magento/Eav/Model/Attribute/Data/AbstractData.php index 3189041d7f71..3bc87ed97751 100644 --- a/app/code/Magento/Eav/Model/Attribute/Data/AbstractData.php +++ b/app/code/Magento/Eav/Model/Attribute/Data/AbstractData.php @@ -143,6 +143,7 @@ public function setRequestScope($scope) /** * Set scope visibility + * * Search value only in scope or search value in scope and global * * @param bool $flag @@ -296,7 +297,7 @@ protected function _applyOutputFilter($value) * Validate value by attribute input validation rule * * @param string $value - * @return string|true + * @return array|true * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -311,9 +312,13 @@ protected function _validateInputRule($value) if (!empty($validateRules['input_validation'])) { $label = $this->getAttribute()->getStoreLabel(); + $allowWhiteSpace = false; switch ($validateRules['input_validation']) { + case 'alphanum-with-spaces': + $allowWhiteSpace = true; + // Continue to alphanumeric validation case 'alphanumeric': - $validator = new \Zend_Validate_Alnum(true); + $validator = new \Zend_Validate_Alnum($allowWhiteSpace); $validator->setMessage(__('"%1" invalid type entered.', $label), \Zend_Validate_Alnum::INVALID); $validator->setMessage( __('"%1" contains non-alphabetic or non-numeric characters.', $label), diff --git a/app/code/Magento/Eav/Model/Attribute/Data/File.php b/app/code/Magento/Eav/Model/Attribute/Data/File.php index 5e2e2716e13d..a52c88261166 100644 --- a/app/code/Magento/Eav/Model/Attribute/Data/File.php +++ b/app/code/Magento/Eav/Model/Attribute/Data/File.php @@ -120,8 +120,7 @@ public function extractValue(RequestInterface $request) } /** - * Validate file by attribute validate rules - * Return array of errors + * Validate file by attribute validate rules and return array of errors * * @param array $value * @return string[] @@ -147,7 +146,7 @@ protected function _validateByRules($value) return $this->_fileValidator->getMessages(); } - if (!is_uploaded_file($value['tmp_name'])) { + if (!empty($value['tmp_name']) && !file_exists($value['tmp_name'])) { return [__('"%1" is not a valid file.', $label)]; } @@ -174,12 +173,22 @@ public function validateValue($value) if ($this->getIsAjaxRequest()) { return true; } + $fileData = $value; + + if (is_string($value) && !empty($value)) { + $dir = $this->_directory->getAbsolutePath($this->getAttribute()->getEntityType()->getEntityTypeCode()); + $fileData = [ + 'size' => filesize($dir . $value), + 'name' => $value, + 'tmp_name' => $dir . $value + ]; + } $errors = []; $attribute = $this->getAttribute(); $toDelete = !empty($value['delete']) ? true : false; - $toUpload = !empty($value['tmp_name']) ? true : false; + $toUpload = !empty($value['tmp_name']) || is_string($value) && !empty($value) ? true : false; if (!$toUpload && !$toDelete && $this->getEntity()->getData($attribute->getAttributeCode())) { return true; @@ -195,11 +204,13 @@ public function validateValue($value) } if ($toUpload) { - $errors = array_merge($errors, $this->_validateByRules($value)); + $errors = array_merge($errors, $this->_validateByRules($fileData)); } if (count($errors) == 0) { return true; + } elseif (is_string($value) && !empty($value)) { + $this->_directory->delete($dir . $value); } return $errors; diff --git a/app/code/Magento/Eav/Model/Attribute/Data/Image.php b/app/code/Magento/Eav/Model/Attribute/Data/Image.php index d04b7e9b940c..24cd0f4fcf61 100644 --- a/app/code/Magento/Eav/Model/Attribute/Data/Image.php +++ b/app/code/Magento/Eav/Model/Attribute/Data/Image.php @@ -14,6 +14,7 @@ class Image extends \Magento\Eav\Model\Attribute\Data\File { /** * Validate file by attribute validate rules + * * Return array of errors * * @param array $value @@ -54,9 +55,9 @@ protected function _validateByRules($value) $errors[] = __('"%1" width exceeds allowed value of %2 px.', $label, $r); } } - if (!empty($rules['max_image_heght'])) { - if ($rules['max_image_heght'] < $imageProp[1]) { - $r = $rules['max_image_heght']; + if (!empty($rules['max_image_height'])) { + if ($rules['max_image_height'] < $imageProp[1]) { + $r = $rules['max_image_height']; $errors[] = __('"%1" height exceeds allowed value of %2 px.', $label, $r); } } diff --git a/app/code/Magento/Eav/Model/Attribute/Data/Text.php b/app/code/Magento/Eav/Model/Attribute/Data/Text.php index f81fb2affd3b..c5167821fdfc 100644 --- a/app/code/Magento/Eav/Model/Attribute/Data/Text.php +++ b/app/code/Magento/Eav/Model/Attribute/Data/Text.php @@ -51,12 +51,12 @@ public function extractValue(RequestInterface $request) /** * Validate data + * * Return true or array of errors * * @param array|string $value * @return bool|array - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) + * @throws \Magento\Framework\Exception\LocalizedException */ public function validateValue($value) { @@ -68,24 +68,21 @@ public function validateValue($value) $value = $this->getEntity()->getDataUsingMethod($attribute->getAttributeCode()); } - if ($attribute->getIsRequired() && empty($value) && $value !== '0') { + if (!$attribute->getIsRequired() && empty($value)) { + return true; + } + + if (empty($value) && $value !== '0' && $attribute->getDefaultValue() === null) { $label = __($attribute->getStoreLabel()); $errors[] = __('"%1" is a required value.', $label); } - if (!$errors && !$attribute->getIsRequired() && empty($value)) { - return true; - } + $validateLengthResult = $this->validateLength($attribute, $value); + $errors = array_merge($errors, $validateLengthResult); - $result = $this->validateLength($attribute, $value); - if (count($result) !== 0) { - $errors = array_merge($errors, $result); - } + $validateInputRuleResult = $this->validateInputRule($value); + $errors = array_merge($errors, $validateInputRuleResult); - $result = $this->_validateInputRule($value); - if ($result !== true) { - $errors = array_merge($errors, $result); - } if (count($errors) == 0) { return true; } @@ -141,7 +138,7 @@ public function outputValue($format = \Magento\Eav\Model\AttributeDataFactory::O * @param string $value * @return array errors */ - private function validateLength(\Magento\Eav\Model\Attribute $attribute, $value): array + private function validateLength(\Magento\Eav\Model\Attribute $attribute, string $value): array { $errors = []; $length = $this->_string->strlen(trim($value)); @@ -162,4 +159,16 @@ private function validateLength(\Magento\Eav\Model\Attribute $attribute, $value) return $errors; } + + /** + * Validate value by attribute input validation rule. + * + * @param string $value + * @return array + */ + private function validateInputRule(string $value): array + { + $result = $this->_validateInputRule($value); + return \is_array($result) ? $result : []; + } } diff --git a/app/code/Magento/Eav/Model/Entity/AbstractEntity.php b/app/code/Magento/Eav/Model/Entity/AbstractEntity.php index 0522ea043217..1fd71e446e6b 100644 --- a/app/code/Magento/Eav/Model/Entity/AbstractEntity.php +++ b/app/code/Magento/Eav/Model/Entity/AbstractEntity.php @@ -10,6 +10,7 @@ use Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend; use Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend; use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource; +use Magento\Eav\Model\Entity\Attribute\UniqueValidationInterface; use Magento\Framework\App\Config\Element; use Magento\Framework\DataObject; use Magento\Framework\DB\Adapter\DuplicateException; @@ -215,12 +216,21 @@ abstract class AbstractEntity extends AbstractResource implements EntityInterfac */ protected $objectRelationProcessor; + /** + * @var UniqueValidationInterface + */ + private $uniqueValidator; + /** * @param Context $context * @param array $data + * @param UniqueValidationInterface|null $uniqueValidator */ - public function __construct(Context $context, $data = []) - { + public function __construct( + Context $context, + $data = [], + UniqueValidationInterface $uniqueValidator = null + ) { $this->_eavConfig = $context->getEavConfig(); $this->_resource = $context->getResource(); $this->_attrSetEntity = $context->getAttributeSetEntity(); @@ -229,6 +239,8 @@ public function __construct(Context $context, $data = []) $this->_universalFactory = $context->getUniversalFactory(); $this->transactionManager = $context->getTransactionManager(); $this->objectRelationProcessor = $context->getObjectRelationProcessor(); + $this->uniqueValidator = $uniqueValidator ?: + ObjectManager::getInstance()->get(UniqueValidationInterface::class); parent::__construct(); $properties = get_object_vars($this); foreach ($data as $key => $value) { @@ -488,6 +500,7 @@ public function addAttribute(AbstractAttribute $attribute, $object = null) /** * Get attributes by scope * + * @param string $suffix * @return array */ private function getAttributesByScope($suffix) @@ -958,12 +971,8 @@ public function checkAttributeUniqueValue(AbstractAttribute $attribute, $object) $data = $connection->fetchCol($select, $bind); - $objectId = $object->getData($entityIdField); - if ($objectId) { - if (isset($data[0])) { - return $data[0] == $objectId; - } - return true; + if ($object->getData($entityIdField)) { + return $this->uniqueValidator->validate($attribute, $object, $this, $entityIdField, $data); } return !count($data); @@ -1674,14 +1683,16 @@ public function saveAttribute(DataObject $object, $attributeCode) $connection->beginTransaction(); try { - $select = $connection->select()->from($table, 'value_id')->where($where); - $origValueId = $connection->fetchOne($select); + $select = $connection->select()->from($table, ['value_id', 'value'])->where($where); + $origRow = $connection->fetchRow($select); + $origValueId = $origRow['value_id'] ?? false; + $origValue = $origRow['value'] ?? null; if ($origValueId === false && $newValue !== null) { $this->_insertAttribute($object, $attribute, $newValue); } elseif ($origValueId !== false && $newValue !== null) { $this->_updateAttribute($object, $attribute, $origValueId, $newValue); - } elseif ($origValueId !== false && $newValue === null) { + } elseif ($origValueId !== false && $newValue === null && $origValue !== null) { $connection->delete($table, $where); } $this->_processAttributeValues(); @@ -1972,7 +1983,8 @@ public function afterDelete(DataObject $object) /** * Load attributes for object - * if the object will not pass all attributes for this entity type will be loaded + * + * If the object will not pass all attributes for this entity type will be loaded * * @param array $attributes * @param AbstractEntity|null $object diff --git a/app/code/Magento/Eav/Model/Entity/Attribute.php b/app/code/Magento/Eav/Model/Entity/Attribute.php index c605f3ce17e3..e23f81607a0c 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute.php @@ -3,10 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Eav\Model\Entity; +use Magento\Eav\Model\Validator\Attribute\Code as AttributeCodeValidator; use Magento\Framework\Api\AttributeValueFactory; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Stdlib\DateTime; use Magento\Framework\Stdlib\DateTime\DateTimeFormatterInterface; @@ -81,6 +82,11 @@ class Attribute extends \Magento\Eav\Model\Entity\Attribute\AbstractAttribute im */ protected $dateTimeFormatter; + /** + * @var AttributeCodeValidator|null + */ + private $attributeCodeValidator; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -101,6 +107,7 @@ class Attribute extends \Magento\Eav\Model\Entity\Attribute\AbstractAttribute im * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data + * @param AttributeCodeValidator|null $attributeCodeValidator * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @codeCoverageIgnore */ @@ -123,7 +130,8 @@ public function __construct( DateTimeFormatterInterface $dateTimeFormatter, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + AttributeCodeValidator $attributeCodeValidator = null ) { parent::__construct( $context, @@ -146,6 +154,9 @@ public function __construct( $this->_localeResolver = $localeResolver; $this->reservedAttributeList = $reservedAttributeList; $this->dateTimeFormatter = $dateTimeFormatter; + $this->attributeCodeValidator = $attributeCodeValidator ?: ObjectManager::getInstance()->get( + AttributeCodeValidator::class + ); } /** @@ -231,6 +242,13 @@ public function loadEntityAttributeIdBySet() */ public function beforeSave() { + if (isset($this->_data['attribute_code']) + && !$this->attributeCodeValidator->isValid($this->_data['attribute_code']) + ) { + $errorMessages = implode("\n", $this->attributeCodeValidator->getMessages()); + throw new LocalizedException(__($errorMessages)); + } + // prevent overriding product data if (isset($this->_data['attribute_code']) && $this->reservedAttributeList->isReservedAttribute($this)) { throw new LocalizedException( @@ -241,25 +259,6 @@ public function beforeSave() ); } - /** - * Check for maximum attribute_code length - */ - if (isset( - $this->_data['attribute_code'] - ) && !\Zend_Validate::is( - $this->_data['attribute_code'], - 'StringLength', - ['max' => self::ATTRIBUTE_CODE_MAX_LENGTH] - ) - ) { - throw new LocalizedException( - __( - 'The attribute code needs to be %1 characters or fewer. Re-enter the code and try again.', - self::ATTRIBUTE_CODE_MAX_LENGTH - ) - ); - } - $defaultValue = $this->getDefaultValue(); $hasDefaultValue = (string)$defaultValue != ''; @@ -295,6 +294,12 @@ public function beforeSave() } } + if ($this->getFrontendInput() == 'media_image') { + if (!$this->getFrontendModel()) { + $this->setFrontendModel(\Magento\Catalog\Model\Product\Attribute\Frontend\Image::class); + } + } + if ($this->getBackendType() == 'gallery') { if (!$this->getBackendModel()) { $this->setBackendModel(\Magento\Eav\Model\Entity\Attribute\Backend\DefaultBackend::class); @@ -305,9 +310,9 @@ public function beforeSave() } /** - * Save additional data + * @inheritdoc * - * @return $this + * Save additional data. */ public function afterSave() { @@ -316,7 +321,7 @@ public function afterSave() } /** - * @return $this + * @inheritdoc * @since 100.0.7 */ public function afterDelete() @@ -491,9 +496,14 @@ public function getIdentities() /** * @inheritdoc * @since 100.0.7 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + $this->unsetData('attribute_set_info'); return array_diff( parent::__sleep(), @@ -504,11 +514,16 @@ public function __sleep() /** * @inheritdoc * @since 100.0.7 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + parent::__wakeup(); - $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); + $objectManager = ObjectManager::getInstance(); $this->_localeDate = $objectManager->get(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class); $this->_localeResolver = $objectManager->get(\Magento\Framework\Locale\ResolverInterface::class); $this->reservedAttributeList = $objectManager->get(\Magento\Catalog\Model\Product\ReservedAttributeList::class); diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php b/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php index 6601c0505137..9ed4ac529368 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Eav\Model\Entity\Attribute; @@ -216,6 +217,7 @@ public function __construct( /** * Get Serializer instance. + * * @deprecated 100.2.0 * * @return Json @@ -244,8 +246,8 @@ protected function _construct() /** * Load attribute data by code * - * @param string|int|\Magento\Eav\Model\Entity\Type $entityType - * @param string $code + * @param string|int|\Magento\Eav\Model\Entity\Type $entityType + * @param string $code * @return $this * @throws LocalizedException */ @@ -296,7 +298,7 @@ public function setAttributeId($data) } /** - * {@inheritdoc} + * @inheritdoc * @codeCoverageIgnore */ public function getAttributeId() @@ -305,6 +307,8 @@ public function getAttributeId() } /** + * Set attribute code + * * @param string $data * @return $this * @codeCoverageIgnore @@ -315,7 +319,7 @@ public function setAttributeCode($data) } /** - * {@inheritdoc} + * @inheritdoc * @codeCoverageIgnore */ public function getAttributeCode() @@ -324,6 +328,8 @@ public function getAttributeCode() } /** + * Set attribute model + * * @param array $data * @return $this * @codeCoverageIgnore @@ -334,6 +340,8 @@ public function setAttributeModel($data) } /** + * Returns attribute model + * * @return array * @codeCoverageIgnore */ @@ -343,6 +351,8 @@ public function getAttributeModel() } /** + * Set backend type + * * @param string $data * @return $this * @codeCoverageIgnore @@ -353,7 +363,7 @@ public function setBackendType($data) } /** - * {@inheritdoc} + * @inheritdoc * @codeCoverageIgnore */ public function getBackendType() @@ -362,6 +372,8 @@ public function getBackendType() } /** + * Set backend model + * * @param string $data * @return $this * @codeCoverageIgnore @@ -372,7 +384,7 @@ public function setBackendModel($data) } /** - * {@inheritdoc} + * @inheritdoc * @codeCoverageIgnore */ public function getBackendModel() @@ -381,6 +393,8 @@ public function getBackendModel() } /** + * Set backend table + * * @param string $data * @return $this * @codeCoverageIgnore @@ -391,6 +405,8 @@ public function setBackendTable($data) } /** + * Returns is visible on front + * * @return bool * @SuppressWarnings(PHPMD.BooleanGetMethodName) * @codeCoverageIgnore @@ -401,7 +417,9 @@ public function getIsVisibleOnFront() } /** - * @return string|int|bool|float + * Returns default value + * + * @return string|null * @codeCoverageIgnore */ public function getDefaultValue() @@ -422,6 +440,8 @@ public function setDefaultValue($defaultValue) } /** + * Returns attribute set id + * * @return int * @codeCoverageIgnore */ @@ -431,6 +451,8 @@ public function getAttributeSetId() } /** + * Set attribute set id + * * @param int $id * @return $this * @codeCoverageIgnore @@ -443,7 +465,7 @@ public function setAttributeSetId($id) } /** - * {@inheritdoc} + * @inheritdoc * @codeCoverageIgnore */ public function getEntityTypeId() @@ -452,6 +474,8 @@ public function getEntityTypeId() } /** + * Set entity type id + * * @param int|string $id * @return $this * @codeCoverageIgnore @@ -464,6 +488,8 @@ public function setEntityTypeId($id) } /** + * Set entity type + * * @param string $type * @return $this * @codeCoverageIgnore @@ -495,7 +521,7 @@ public function getAlias($entity = null) /** * Set attribute name * - * @param string $name + * @param string $name * @return $this * @codeCoverageIgnore */ @@ -608,9 +634,11 @@ public function getSource() { if (empty($this->_source)) { if (!$this->getSourceModel()) { - $this->setSourceModel($this->_getDefaultSourceModel()); + $this->_source = $this->_getDefaultSourceModel(); + } else { + $this->_source = $this->getSourceModel(); } - $source = $this->_universalFactory->create($this->getSourceModel()); + $source = $this->_universalFactory->create($this->_source); if (!$source) { throw new LocalizedException( __( @@ -639,6 +667,8 @@ public function usesSource() } /** + * Returns default backend model + * * @return string * @codeCoverageIgnore */ @@ -648,6 +678,8 @@ protected function _getDefaultBackendModel() } /** + * Returns default frontend model + * * @return string * @codeCoverageIgnore */ @@ -657,6 +689,8 @@ protected function _getDefaultFrontendModel() } /** + * Returns default source model + * * @return string * @codeCoverageIgnore */ @@ -779,7 +813,7 @@ public function getBackendTable() if ($this->isStatic()) { $this->_dataTable = $this->getEntityType()->getValueTablePrefix(); } else { - $backendTable = trim($this->_getData('backend_table')); + $backendTable = trim((string)$this->_getData('backend_table')); if (empty($backendTable)) { $entityTable = [$this->getEntityType()->getEntityTablePrefix(), $this->getBackendType()]; $backendTable = $this->getResource()->getTable($entityTable); @@ -891,6 +925,7 @@ public function _getFlatColumnsDdlDefinition() /** * Retrieve flat columns definition in old format (before MMDB support) + * * Used in database compatible mode * * @deprecated 100.2.0 @@ -1066,8 +1101,8 @@ public function getFlatUpdateSelect($store = null) } /** + * @inheritdoc * @codeCoverageIgnoreStart - * {@inheritdoc} */ public function getIsUnique() { @@ -1086,7 +1121,7 @@ public function setIsUnique($isUnique) } /** - * {@inheritdoc} + * @inheritdoc */ public function getFrontendClass() { @@ -1105,7 +1140,7 @@ public function setFrontendClass($frontendClass) } /** - * {@inheritdoc} + * @inheritdoc */ public function getFrontendInput() { @@ -1113,7 +1148,7 @@ public function getFrontendInput() } /** - * {@inheritdoc} + * @inheritdoc */ public function setFrontendInput($frontendInput) { @@ -1121,7 +1156,7 @@ public function setFrontendInput($frontendInput) } /** - * {@inheritdoc} + * @inheritdoc */ public function getIsRequired() { @@ -1129,7 +1164,7 @@ public function getIsRequired() } /** - * {@inheritdoc} + * @inheritdoc */ public function setIsRequired($isRequired) { @@ -1139,7 +1174,7 @@ public function setIsRequired($isRequired) //@codeCoverageIgnoreEnd /** - * {@inheritdoc} + * @inheritdoc */ public function getOptions() { @@ -1200,8 +1235,8 @@ protected function convertToObjects(array $options) } /** + * @inheritdoc * @codeCoverageIgnoreStart - * {@inheritdoc} */ public function getIsUserDefined() { @@ -1220,7 +1255,7 @@ public function setIsUserDefined($isUserDefined) } /** - * {@inheritdoc} + * @inheritdoc */ public function getDefaultFrontendLabel() { @@ -1239,7 +1274,7 @@ public function setDefaultFrontendLabel($defaultFrontendLabel) } /** - * {@inheritdoc} + * @inheritdoc */ public function getFrontendLabels() { @@ -1271,7 +1306,7 @@ public function setFrontendLabels(array $frontendLabels = null) } /** - * {@inheritdoc} + * @inheritdoc */ public function getNote() { @@ -1290,7 +1325,7 @@ public function setNote($note) } /** - * {@inheritdoc} + * @inheritdoc */ public function getSourceModel() { @@ -1311,7 +1346,7 @@ public function setSourceModel($sourceModel) //@codeCoverageIgnoreEnd /** - * {@inheritdoc} + * @inheritdoc */ public function getValidationRules() { @@ -1338,7 +1373,7 @@ public function setValidationRules(array $validationRules = null) } /** - * {@inheritdoc} + * @inheritdoc * * @return \Magento\Eav\Api\Data\AttributeExtensionInterface|null * @codeCoverageIgnore @@ -1355,7 +1390,7 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * @inheritdoc * * @param \Magento\Eav\Api\Data\AttributeExtensionInterface $extensionAttributes * @return $this @@ -1369,9 +1404,14 @@ public function setExtensionAttributes(\Magento\Eav\Api\Data\AttributeExtensionI /** * @inheritdoc * @since 100.0.7 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + return array_diff( parent::__sleep(), [ @@ -1394,9 +1434,14 @@ public function __sleep() /** * @inheritdoc * @since 100.0.7 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + parent::__wakeup(); $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); $this->_eavConfig = $objectManager->get(\Magento\Eav\Model\Config::class); diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/Backend/AbstractBackend.php b/app/code/Magento/Eav/Model/Entity/Attribute/Backend/AbstractBackend.php index 53c32e1cbfba..86ab6f6ea6e9 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/Backend/AbstractBackend.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/Backend/AbstractBackend.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Eav\Model\Entity\Attribute\Backend; @@ -204,12 +205,12 @@ public function getEntityValueId($entity) /** * Retrieve default value * - * @return mixed + * @return string */ public function getDefaultValue() { if ($this->_defaultValue === null) { - if ($this->getAttribute()->getDefaultValue()) { + if ($this->getAttribute()->getDefaultValue() !== null) { $this->_defaultValue = $this->getAttribute()->getDefaultValue(); } else { $this->_defaultValue = ""; @@ -285,7 +286,7 @@ public function afterLoad($object) public function beforeSave($object) { $attrCode = $this->getAttribute()->getAttributeCode(); - if (!$object->hasData($attrCode) && $this->getDefaultValue()) { + if (!$object->hasData($attrCode) && $this->getDefaultValue() !== '') { $object->setData($attrCode, $this->getDefaultValue()); } @@ -348,9 +349,8 @@ public function getAffectedFields($object) } /** - * By default attribute value is considered scalar that can be stored in a generic way + * By default attribute value is considered scalar that can be stored in a generic way {@inheritdoc} * - * {@inheritdoc} * @codeCoverageIgnore */ public function isScalar() diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/Frontend/AbstractFrontend.php b/app/code/Magento/Eav/Model/Entity/Attribute/Frontend/AbstractFrontend.php index 3d4c9e89a035..2c7ea1ab9268 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/Frontend/AbstractFrontend.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/Frontend/AbstractFrontend.php @@ -20,6 +20,8 @@ use Magento\Eav\Model\Entity\Attribute\Source\BooleanFactory; /** + * EAV entity attribute form renderer. + * * @api * @since 100.0.2 */ @@ -234,6 +236,9 @@ protected function _getInputValidateClass() case 'alphanumeric': $class = 'validate-alphanum'; break; + case 'alphanum-with-spaces': + $class = 'validate-alphanum-with-spaces'; + break; case 'numeric': $class = 'validate-digits'; break; diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/Group.php b/app/code/Magento/Eav/Model/Entity/Attribute/Group.php index 0b6ac2b998de..2e5596456058 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/Group.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/Group.php @@ -3,11 +3,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Eav\Model\Entity\Attribute; +use Magento\Eav\Api\Data\AttributeGroupExtensionInterface; use Magento\Framework\Api\AttributeValueFactory; +use Magento\Framework\Exception\LocalizedException; /** + * Entity attribute group model + * * @api * @method int getSortOrder() * @method \Magento\Eav\Model\Entity\Attribute\Group setSortOrder(int $value) @@ -27,6 +32,11 @@ class Group extends \Magento\Framework\Model\AbstractExtensibleModel implements */ private $translitFilter; + /** + * @var array + */ + private $reservedSystemNames = []; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -35,7 +45,8 @@ class Group extends \Magento\Framework\Model\AbstractExtensibleModel implements * @param \Magento\Framework\Filter\Translit $translitFilter * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection - * @param array $data + * @param array $data (optional) + * @param array $reservedSystemNames (optional) */ public function __construct( \Magento\Framework\Model\Context $context, @@ -45,7 +56,8 @@ public function __construct( \Magento\Framework\Filter\Translit $translitFilter, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + array $reservedSystemNames = [] ) { parent::__construct( $context, @@ -56,6 +68,7 @@ public function __construct( $resourceCollection, $data ); + $this->reservedSystemNames = $reservedSystemNames; $this->translitFilter = $translitFilter; } @@ -74,6 +87,7 @@ protected function _construct() * Checks if current attribute group exists * * @return bool + * @throws LocalizedException * @codeCoverageIgnore */ public function itemExists() @@ -85,6 +99,7 @@ public function itemExists() * Delete groups * * @return $this + * @throws LocalizedException * @codeCoverageIgnore */ public function deleteGroups() @@ -110,9 +125,10 @@ public function beforeSave() ), '-' ); - if (empty($attributeGroupCode)) { + $isReservedSystemName = in_array(strtolower($attributeGroupCode), $this->reservedSystemNames); + if (empty($attributeGroupCode) || $isReservedSystemName) { // in the following code md5 is not used for security purposes - $attributeGroupCode = md5($groupName); + $attributeGroupCode = md5(strtolower($groupName)); } $this->setAttributeGroupCode($attributeGroupCode); } @@ -121,7 +137,8 @@ public function beforeSave() } /** - * {@inheritdoc} + * @inheritdoc + * * @codeCoverageIgnoreStart */ public function getAttributeGroupId() @@ -130,7 +147,7 @@ public function getAttributeGroupId() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAttributeGroupName() { @@ -138,7 +155,7 @@ public function getAttributeGroupName() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAttributeSetId() { @@ -146,7 +163,7 @@ public function getAttributeSetId() } /** - * {@inheritdoc} + * @inheritdoc */ public function setAttributeGroupId($attributeGroupId) { @@ -154,7 +171,7 @@ public function setAttributeGroupId($attributeGroupId) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAttributeGroupName($attributeGroupName) { @@ -162,7 +179,7 @@ public function setAttributeGroupName($attributeGroupName) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAttributeSetId($attributeSetId) { @@ -170,9 +187,9 @@ public function setAttributeSetId($attributeSetId) } /** - * {@inheritdoc} + * @inheritdoc * - * @return \Magento\Eav\Api\Data\AttributeGroupExtensionInterface|null + * @return AttributeGroupExtensionInterface|null */ public function getExtensionAttributes() { @@ -180,14 +197,13 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * @inheritdoc * - * @param \Magento\Eav\Api\Data\AttributeGroupExtensionInterface $extensionAttributes + * @param AttributeGroupExtensionInterface $extensionAttributes * @return $this */ - public function setExtensionAttributes( - \Magento\Eav\Api\Data\AttributeGroupExtensionInterface $extensionAttributes - ) { + public function setExtensionAttributes(AttributeGroupExtensionInterface $extensionAttributes) + { return $this->_setExtensionAttributes($extensionAttributes); } diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/OptionManagement.php b/app/code/Magento/Eav/Model/Entity/Attribute/OptionManagement.php index a2c9611d26f5..3c3bc083fdf8 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/OptionManagement.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/OptionManagement.php @@ -10,6 +10,9 @@ use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\StateException; +/** + * Eav Option Management + */ class OptionManagement implements \Magento\Eav\Api\AttributeOptionManagementInterface { /** @@ -36,7 +39,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function add($entityType, $attributeCode, $option) { @@ -79,7 +82,7 @@ public function add($entityType, $attributeCode, $option) } /** - * {@inheritdoc} + * @inheritdoc */ public function delete($entityType, $attributeCode, $optionId) { @@ -110,7 +113,7 @@ public function delete($entityType, $attributeCode, $optionId) } /** - * {@inheritdoc} + * @inheritdoc */ public function getItems($entityType, $attributeCode) { @@ -129,6 +132,8 @@ public function getItems($entityType, $attributeCode) } /** + * Validate option + * * @param \Magento\Eav\Api\Data\AttributeInterface $attribute * @param int $optionId * @throws NoSuchEntityException @@ -136,7 +141,7 @@ public function getItems($entityType, $attributeCode) */ protected function validateOption($attribute, $optionId) { - if (!$attribute->getSource()->getOptionText($optionId)) { + if ($attribute->getSource()->getOptionText($optionId) === false) { throw new NoSuchEntityException( __( 'The "%1" attribute doesn\'t include an option with "%2" ID.', @@ -148,15 +153,19 @@ protected function validateOption($attribute, $optionId) } /** + * Returns option id + * * @param \Magento\Eav\Api\Data\AttributeOptionInterface $option * @return string */ private function getOptionId(\Magento\Eav\Api\Data\AttributeOptionInterface $option) : string { - return $option->getValue() ?: 'new_option'; + return 'id_' . ($option->getValue() ?: 'new_option'); } /** + * Set option value + * * @param \Magento\Eav\Api\Data\AttributeOptionInterface $option * @param \Magento\Eav\Api\Data\AttributeInterface $attribute * @param string $optionLabel diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/Source/AbstractSource.php b/app/code/Magento/Eav/Model/Entity/Attribute/Source/AbstractSource.php index 0991b3f9f4b2..dd4cd4217a12 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/Source/AbstractSource.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/Source/AbstractSource.php @@ -3,11 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Eav\Model\Entity\Attribute\Source; /** * Entity/Attribute/Model - attribute selection source abstract - * + * phpcs:disable Magento2.Classes.AbstractApi * @api * @author Magento Core Team <core@magentocommerce.com> * @SuppressWarnings(PHPMD.NumberOfChildren) @@ -65,7 +66,7 @@ public function getOptionText($value) { $options = $this->getAllOptions(); // Fixed for tax_class_id and custom_design - if (sizeof($options) > 0) { + if (count($options) > 0) { foreach ($options as $option) { if (isset($option['value']) && $option['value'] == $value) { return isset($option['label']) ? $option['label'] : $option['value']; @@ -73,20 +74,22 @@ public function getOptionText($value) } } // End - if (isset($options[$value])) { + if (is_scalar($value) && isset($options[$value])) { return $options[$value]; } return false; } /** + * Get option id. + * * @param string $value * @return null|string */ public function getOptionId($value) { foreach ($this->getAllOptions() as $option) { - if (strcasecmp($option['label'], $value) == 0 || $option['value'] == $value) { + if ($this->mbStrcasecmp($option['label'], $value) == 0 || $option['value'] == $value) { return $option['value']; } } @@ -164,4 +167,20 @@ public function toOptionArray() { return $this->getAllOptions(); } + + /** + * Multibyte support strcasecmp function version. + * + * @param string $str1 + * @param string $str2 + * @return int + */ + private function mbStrcasecmp($str1, $str2) + { + $encoding = mb_internal_encoding(); + return strcmp( + mb_strtoupper($str1, $encoding), + mb_strtoupper($str2, $encoding) + ); + } } diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/Source/Table.php b/app/code/Magento/Eav/Model/Entity/Attribute/Source/Table.php index 8f8c39bf2604..f9aa1a9ed3ba 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/Source/Table.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/Source/Table.php @@ -9,6 +9,8 @@ use Magento\Store\Model\StoreManagerInterface; /** + * Eav attribute default source when values are coming from another table + * * @api * @since 100.0.2 */ @@ -127,12 +129,14 @@ public function getSpecificOptions($ids, $withEmpty = true) } /** + * Add an empty option to the array + * * @param array $options * @return array */ private function addEmptyOption(array $options) { - array_unshift($options, ['label' => $this->getAttribute()->getIsRequired() ? '' : ' ', 'value' => '']); + array_unshift($options, ['label' => ' ', 'value' => '']); return $options; } diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/UniqueValidationInterface.php b/app/code/Magento/Eav/Model/Entity/Attribute/UniqueValidationInterface.php new file mode 100644 index 000000000000..b68e79d7b7d2 --- /dev/null +++ b/app/code/Magento/Eav/Model/Entity/Attribute/UniqueValidationInterface.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Eav\Model\Entity\Attribute; + +use Magento\Framework\DataObject; +use Magento\Eav\Model\Entity\AbstractEntity; + +/** + * Interface for unique attribute validator + */ +interface UniqueValidationInterface +{ + /** + * Validate if attribute value is unique + * + * @param AbstractAttribute $attribute + * @param DataObject $object + * @param AbstractEntity $entity + * @param string $entityLinkField + * @param array $entityIds + * @return bool + */ + public function validate( + AbstractAttribute $attribute, + DataObject $object, + AbstractEntity $entity, + $entityLinkField, + array $entityIds + ); +} diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/UniqueValidator.php b/app/code/Magento/Eav/Model/Entity/Attribute/UniqueValidator.php new file mode 100644 index 000000000000..b1888b42bef9 --- /dev/null +++ b/app/code/Magento/Eav/Model/Entity/Attribute/UniqueValidator.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Eav\Model\Entity\Attribute; + +use Magento\Framework\DataObject; +use Magento\Eav\Model\Entity\AbstractEntity; + +/** + * Class for validate unique attribute value + */ +class UniqueValidator implements UniqueValidationInterface +{ + /** + * @inheritdoc + */ + public function validate( + AbstractAttribute $attribute, + DataObject $object, + AbstractEntity $entity, + $entityLinkField, + array $entityIds + ) { + if (isset($entityIds[0])) { + return $entityIds[0] == $object->getData($entityLinkField); + } + return true; + } +} diff --git a/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php b/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php index 076d79776230..e50abbc11e54 100644 --- a/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php +++ b/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php @@ -14,6 +14,7 @@ /** * Entity/Attribute/Model - collection abstract * + * phpcs:disable Magento2.Classes.AbstractApi * @api * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) @@ -125,6 +126,7 @@ abstract class AbstractCollection extends AbstractDb implements SourceProviderIn protected $_resourceHelper; /** + * * @var \Magento\Framework\Validator\UniversalFactory */ protected $_universalFactory; @@ -172,6 +174,7 @@ public function __construct( * Initialize collection * * @return void + * phpcs:disable Magento2.CodeAnalysis.EmptyBlock */ protected function _construct() { @@ -248,7 +251,7 @@ public function setEntity($entity) $this->_entity = $this->_eavEntityFactory->create()->setType($entity); } else { throw new LocalizedException( - __('The "%1" entity supplied is invalid. Verify the entity and try again.', print_r($entity, 1)) + __('The entity supplied to collection is invalid. Verify the entity and try again.') ); } return $this; @@ -282,7 +285,7 @@ public function getResource() /** * Set template object for the collection * - * @param \Magento\Framework\DataObject $object + * @param \Magento\Framework\DataObject $object * @return $this */ public function setObject($object = null) @@ -379,6 +382,7 @@ public function addAttributeToFilter($attribute, $condition = null, $joinType = if (!empty($conditionSql)) { $this->getSelect()->where($conditionSql, null, \Magento\Framework\DB\Select::TYPE_CONDITION); + $this->_totalRecords = null; } else { throw new \Magento\Framework\Exception\LocalizedException( __('Invalid attribute identifier for filter (%1)', get_class($attribute)) @@ -398,7 +402,7 @@ public function addAttributeToFilter($attribute, $condition = null, $joinType = */ public function addFieldToFilter($attribute, $condition = null) { - return $this->addAttributeToFilter($attribute, $condition); + return $this->addAttributeToFilter($attribute, $condition, 'left'); } /** @@ -652,7 +656,7 @@ public function groupByAttribute($attribute) * @param string $bind attribute of the main entity to link with joined $filter * @param string $filter primary key for the joined entity (entity_id default) * @param string $joinType inner|left - * @param null $storeId + * @param int|null $storeId * @return $this * @throws LocalizedException * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -946,8 +950,8 @@ public function load($printQuery = false, $logQuery = false) /** * Clone and reset collection * - * @param null $limit - * @param null $offset + * @param int|null $limit + * @param int|null $offset * @return Select */ protected function _getAllIdsSelect($limit = null, $offset = null) @@ -1044,6 +1048,7 @@ public function importFromArray($arr) $this->_items[$entityId]->addData($row); } } + $this->_setIsLoaded(); return $this; } @@ -1147,7 +1152,6 @@ public function _loadEntities($printQuery = false, $logQuery = false) * @param bool $printQuery * @param bool $logQuery * @return $this - * @throws LocalizedException * @throws \Exception * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) @@ -1242,7 +1246,7 @@ protected function _getLoadAttributesSelect($table, $attributeIds = []) if ($entity->getEntityTable() == \Magento\Eav\Model\Entity::DEFAULT_ENTITY_TABLE && $entity->getTypeId()) { $select->where( - 'entity_type_id =?', + 't_d.entity_type_id =?', $entity->getTypeId() ); } @@ -1354,8 +1358,8 @@ protected function _getAttributeFieldName($attributeCode) /** * Add attribute value table to the join if it wasn't added previously * - * @param string $attributeCode - * @param string $joinType inner|left + * @param string $attributeCode + * @param string $joinType inner|left * @return $this * @throws LocalizedException * @SuppressWarnings(PHPMD.NPathComplexity) @@ -1449,12 +1453,12 @@ protected function getEntityPkName(\Magento\Eav\Model\Entity\AbstractEntity $ent /** * Adding join statement to collection select instance * - * @param string $method - * @param object $attribute - * @param string $tableAlias - * @param array $condition - * @param string $fieldCode - * @param string $fieldAlias + * @param string $method + * @param object $attribute + * @param string $tableAlias + * @param array $condition + * @param string $fieldCode + * @param string $fieldAlias * @return $this */ protected function _joinAttributeToSelect($method, $attribute, $tableAlias, $condition, $fieldCode, $fieldAlias) @@ -1619,6 +1623,7 @@ public function getLoadedIds() /** * Clear collection + * * @return $this */ public function clear() @@ -1629,6 +1634,7 @@ public function clear() /** * Remove all items from collection + * * @return $this */ public function removeAllItems() @@ -1652,6 +1658,7 @@ public function removeItemByKey($key) } /** + * Returns main table. * Returns main table name - extracted from "module/table" style and * validated by db adapter * 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 e626ed35eb1e..631bfa3c2d2b 100644 --- a/app/code/Magento/Eav/Model/Entity/Collection/VersionControl/AbstractCollection.php +++ b/app/code/Magento/Eav/Model/Entity/Collection/VersionControl/AbstractCollection.php @@ -7,8 +7,11 @@ /** * Class Abstract Collection + * + * phpcs:disable Magento2.Classes.AbstractApi * @api * @since 100.0.2 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ abstract class AbstractCollection extends \Magento\Eav\Model\Entity\Collection\AbstractCollection { @@ -18,6 +21,7 @@ abstract class AbstractCollection extends \Magento\Eav\Model\Entity\Collection\A protected $entitySnapshot; /** + * AbstractCollection constructor. * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory * @param \Psr\Log\LoggerInterface $logger * @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy @@ -27,8 +31,9 @@ abstract class AbstractCollection extends \Magento\Eav\Model\Entity\Collection\A * @param \Magento\Eav\Model\EntityFactory $eavEntityFactory * @param \Magento\Eav\Model\ResourceModel\Helper $resourceHelper * @param \Magento\Framework\Validator\UniversalFactory $universalFactory - * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot $entitySnapshot, - * @param mixed $connection + * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot $entitySnapshot + * @param \Magento\Framework\DB\Adapter\AdapterInterface|null $connection + * * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @codeCoverageIgnore */ diff --git a/app/code/Magento/Eav/Model/Entity/Type.php b/app/code/Magento/Eav/Model/Entity/Type.php index 80fcfd4ab585..444d58bf546d 100644 --- a/app/code/Magento/Eav/Model/Entity/Type.php +++ b/app/code/Magento/Eav/Model/Entity/Type.php @@ -167,12 +167,8 @@ public function getAttributeCollection($setId = null) */ protected function _getAttributeCollection() { - $collection = $this->_attributeFactory->create()->getCollection(); - $objectsModel = $this->getAttributeModel(); - if ($objectsModel) { - $collection->setModel($objectsModel); - } - + $collection = $this->_universalFactory->create($this->getEntityAttributeCollection()); + $collection->setItemObjectClass($this->getAttributeModel()); return $collection; } diff --git a/app/code/Magento/Eav/Model/Form.php b/app/code/Magento/Eav/Model/Form.php index c8c50521f550..a34b53eede35 100644 --- a/app/code/Magento/Eav/Model/Form.php +++ b/app/code/Magento/Eav/Model/Form.php @@ -286,7 +286,8 @@ public function getFormCode() } /** - * Return entity type instance + * Return entity type instance. + * * Return EAV entity type if entity type is not defined * * @return \Magento\Eav\Model\Entity\Type @@ -323,6 +324,8 @@ public function getAttributes() if ($this->_attributes === null) { $this->_attributes = []; $this->_userAttributes = []; + $this->_systemAttributes = []; + $this->_allowedAttributes = []; /** @var $attribute \Magento\Eav\Model\Attribute */ foreach ($this->_getFilteredFormAttributeCollection() as $attribute) { $this->_attributes[$attribute->getAttributeCode()] = $attribute; diff --git a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php index 88f58c6d0111..5e7226e7a36d 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php +++ b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php @@ -119,7 +119,7 @@ public function loadByCode(AbstractModel $object, $entityTypeId, $code) */ private function _getMaxSortOrder(AbstractModel $object) { - if (intval($object->getAttributeGroupId()) > 0) { + if ((int)$object->getAttributeGroupId() > 0) { $connection = $this->getConnection(); $bind = [ ':attribute_set_id' => $object->getAttributeSetId(), @@ -225,6 +225,8 @@ protected function _afterDelete(\Magento\Framework\Model\AbstractModel $object) } /** + * Returns config instance + * * @return Config * @deprecated 100.0.7 */ @@ -297,10 +299,10 @@ protected function _saveAdditionalAttributeData(AbstractModel $object) * Save in set including * * @param AbstractModel $object - * @param null $attributeEntityId - * @param null $attributeSetId - * @param null $attributeGroupId - * @param null $attributeSortOrder + * @param int|null $attributeEntityId + * @param int|null $attributeSetId + * @param int|null $attributeGroupId + * @param int|null $attributeSortOrder * @return $this * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -636,6 +638,7 @@ public function getAdditionalAttributeTable($entityTypeId) /** * Load additional attribute data. + * * Load label of current active store * * @param EntityAttribute|AbstractModel $object @@ -722,9 +725,14 @@ public function getValidAttributeIds($attributeIds) * * @return array * @since 100.0.7 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + $properties = parent::__sleep(); $properties = array_diff($properties, ['_storeManager']); return $properties; @@ -735,9 +743,14 @@ public function __sleep() * * @return void * @since 100.0.7 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + parent::__wakeup(); $this->_storeManager = \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Store\Model\StoreManagerInterface::class); diff --git a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute/Collection.php b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute/Collection.php index cec415e51367..6fce6bd2dc44 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute/Collection.php +++ b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute/Collection.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Eav\Model\ResourceModel\Entity\Attribute; use Magento\Eav\Model\Entity\Type; @@ -128,7 +129,7 @@ public function setEntityTypeFilter($type) /** * Specify attribute set filter * - * @param int $setId + * @param int|int[] $setId * @return $this */ public function setAttributeSetFilter($setId) @@ -183,6 +184,7 @@ public function setAttributeSetFilterBySetName($attributeSetName, $entityTypeCod /** * Specify multiple attribute sets filter + * * Result will be ordered by sort_order * * @param array $setIds @@ -225,7 +227,6 @@ public function setInAllAttributeSetsFilter(array $setIds) ->having(new \Zend_Db_Expr('COUNT(*)') . ' = ' . count($setIds)); } - //$this->getSelect()->distinct(true); $this->setOrder('is_user_defined', self::SORT_ORDER_ASC); return $this; @@ -475,7 +476,7 @@ public function addStoreLabel($storeId) } /** - * {@inheritdoc} + * @inheritdoc */ public function getSelectCountSql() { diff --git a/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php b/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php index cd2fe7477ca6..7f6dfa2a5e9a 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php +++ b/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php @@ -5,13 +5,19 @@ */ namespace Magento\Eav\Model\ResourceModel; +use Magento\Eav\Model\Config; use Magento\Framework\DataObject; +use Magento\Framework\DB\Select; +use Magento\Framework\DB\Sql\UnionExpression; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\EntityManager\Operation\AttributeInterface; use Magento\Framework\Model\Entity\ScopeInterface; use Magento\Framework\Model\Entity\ScopeResolver; use Psr\Log\LoggerInterface; +/** + * EAV read handler + */ class ReadHandler implements AttributeInterface { /** @@ -30,23 +36,21 @@ class ReadHandler implements AttributeInterface private $logger; /** - * @var \Magento\Eav\Model\Config + * @var Config */ private $config; /** - * ReadHandler constructor. - * * @param MetadataPool $metadataPool * @param ScopeResolver $scopeResolver * @param LoggerInterface $logger - * @param \Magento\Eav\Model\Config $config + * @param Config $config */ public function __construct( MetadataPool $metadataPool, ScopeResolver $scopeResolver, LoggerInterface $logger, - \Magento\Eav\Model\Config $config + Config $config ) { $this->metadataPool = $metadataPool; $this->scopeResolver = $scopeResolver; @@ -86,6 +90,8 @@ private function getEntityAttributes(string $entityType, DataObject $entity): ar } /** + * Get context variables + * * @param ScopeInterface $scope * @return array */ @@ -99,6 +105,8 @@ protected function getContextVariables(ScopeInterface $scope) } /** + * Execute read handler + * * @param string $entityType * @param array $entityData * @param array $arguments @@ -129,33 +137,40 @@ public function execute($entityType, $entityData, $arguments = []) } } if (count($attributeTables)) { - $attributeTables = array_keys($attributeTables); - foreach ($attributeTables as $attributeTable) { + $identifiers = null; + foreach ($attributeTables as $attributeTable => $attributeIds) { $select = $connection->select() ->from( ['t' => $attributeTable], ['value' => 't.value', 'attribute_id' => 't.attribute_id'] ) - ->where($metadata->getLinkField() . ' = ?', $entityData[$metadata->getLinkField()]); + ->where($metadata->getLinkField() . ' = ?', $entityData[$metadata->getLinkField()]) + ->where('attribute_id IN (?)', $attributeIds); + $attributeIdentifiers = []; foreach ($context as $scope) { //TODO: if (in table exists context field) $select->where( - $metadata->getEntityConnection()->quoteIdentifier($scope->getIdentifier()) . ' IN (?)', + $connection->quoteIdentifier($scope->getIdentifier()) . ' IN (?)', $this->getContextVariables($scope) - )->order('t.' . $scope->getIdentifier() . ' DESC'); + ); + $attributeIdentifiers[] = $scope->getIdentifier(); } + $attributeIdentifiers = array_unique($attributeIdentifiers); + $identifiers = array_intersect($identifiers ?? $attributeIdentifiers, $attributeIdentifiers); $selects[] = $select; } - $unionSelect = new \Magento\Framework\DB\Sql\UnionExpression( - $selects, - \Magento\Framework\DB\Select::SQL_UNION_ALL - ); - foreach ($connection->fetchAll($unionSelect) as $attributeValue) { + $this->applyIdentifierForSelects($selects, $identifiers); + $unionSelect = new UnionExpression($selects, Select::SQL_UNION_ALL, '( %s )'); + $orderedUnionSelect = $connection->select(); + $orderedUnionSelect->from(['u' => $unionSelect]); + $this->applyIdentifierForUnion($orderedUnionSelect, $identifiers); + $attributes = $connection->fetchAll($orderedUnionSelect); + foreach ($attributes as $attributeValue) { if (isset($attributesMap[$attributeValue['attribute_id']])) { $entityData[$attributesMap[$attributeValue['attribute_id']]] = $attributeValue['value']; } else { $this->logger->warning( - "Attempt to load value of nonexistent EAV attribute '{$attributeValue['attribute_id']}' + "Attempt to load value of nonexistent EAV attribute '{$attributeValue['attribute_id']}' for entity type '$entityType'." ); } @@ -163,4 +178,32 @@ public function execute($entityType, $entityData, $arguments = []) } return $entityData; } + + /** + * Apply identifiers column on select array + * + * @param Select[] $selects + * @param array $identifiers + */ + private function applyIdentifierForSelects(array $selects, array $identifiers) + { + foreach ($selects as $select) { + foreach ($identifiers as $identifier) { + $select->columns($identifier, 't'); + } + } + } + + /** + * Apply identifiers order on union select + * + * @param Select $unionSelect + * @param array $identifiers + */ + private function applyIdentifierForUnion(Select $unionSelect, array $identifiers) + { + foreach ($identifiers as $identifier) { + $unionSelect->order($identifier); + } + } } diff --git a/app/code/Magento/Eav/Model/Validator/Attribute/Code.php b/app/code/Magento/Eav/Model/Validator/Attribute/Code.php new file mode 100644 index 000000000000..f3ee37721b8c --- /dev/null +++ b/app/code/Magento/Eav/Model/Validator/Attribute/Code.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Eav\Model\Validator\Attribute; + +use Magento\Eav\Model\Entity\Attribute; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Validator\AbstractValidator; +use Zend_Validate; + +/** + * Class Code + * + * Validation EAV attribute code + */ +class Code extends AbstractValidator +{ + /** + * Validation pattern for attribute code + */ + const VALIDATION_RULE_PATTERN = '/^[a-zA-Z]+[a-zA-Z0-9_]*$/u'; + + /** + * Validates the correctness of the attribute code + * + * @param string $attributeCode + * @return bool + * @throws \Zend_Validate_Exception + */ + public function isValid($attributeCode): bool + { + $errorMessages = []; + /** + * Check attribute_code for allowed characters + */ + if (trim($attributeCode) + && !preg_match(self::VALIDATION_RULE_PATTERN, trim($attributeCode)) + ) { + $errorMessages[] = __( + 'Attribute code "%1" is invalid. Please use only letters (a-z or A-Z), ' . + 'numbers (0-9) or underscore (_) in this field, and the first character should be a letter.', + $attributeCode + ); + } + + /** + * Check attribute_code for allowed length + */ + $minLength = Attribute::ATTRIBUTE_CODE_MIN_LENGTH; + $maxLength = Attribute::ATTRIBUTE_CODE_MAX_LENGTH; + $isAllowedLength = Zend_Validate::is( + trim($attributeCode), + 'StringLength', + ['min' => $minLength, 'max' => $maxLength] + ); + if (!$isAllowedLength) { + $errorMessages[] = __( + 'An attribute code must not be less than %1 and more than %2 characters.', + $minLength, + $maxLength + ); + } + + $this->_addMessages($errorMessages); + + return !$this->hasMessages(); + } +} diff --git a/app/code/Magento/Eav/Setup/EavSetup.php b/app/code/Magento/Eav/Setup/EavSetup.php index 6e81ddc36e9c..de285e81b1d0 100644 --- a/app/code/Magento/Eav/Setup/EavSetup.php +++ b/app/code/Magento/Eav/Setup/EavSetup.php @@ -9,13 +9,15 @@ use Magento\Eav\Model\Entity\Setup\Context; use Magento\Eav\Model\Entity\Setup\PropertyMapperInterface; use Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\CollectionFactory; +use Magento\Eav\Model\Validator\Attribute\Code; use Magento\Framework\App\CacheInterface; use Magento\Framework\App\ObjectManager; -use Magento\Framework\App\ResourceConnection; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Setup\ModuleDataSetupInterface; /** + * Base eav setup class. + * * @api * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -80,6 +82,11 @@ class EavSetup */ private $_defaultAttributeSetName = 'Default'; + /** + * @var Code + */ + private $attributeCodeValidator; + /** * Init * @@ -87,21 +94,27 @@ class EavSetup * @param Context $context * @param CacheInterface $cache * @param CollectionFactory $attrGroupCollectionFactory + * @param Code|null $attributeCodeValidator */ public function __construct( ModuleDataSetupInterface $setup, Context $context, CacheInterface $cache, - CollectionFactory $attrGroupCollectionFactory + CollectionFactory $attrGroupCollectionFactory, + Code $attributeCodeValidator = null ) { $this->cache = $cache; $this->attrGroupCollectionFactory = $attrGroupCollectionFactory; $this->attributeMapper = $context->getAttributeMapper(); $this->setup = $setup; + $this->attributeCodeValidator = $attributeCodeValidator ?: ObjectManager::getInstance()->get( + Code::class + ); } /** - * Gets setup model + * Gets setup model. + * * @deprecated * @return ModuleDataSetupInterface */ @@ -568,6 +581,8 @@ public function addAttributeGroup($entityTypeId, $setId, $name, $sortOrder = nul } /** + * Convert group name to attribute group code. + * * @param string $groupName * @return string * @since 100.1.0 @@ -774,38 +789,6 @@ private function _getValue($array, $key, $default = null) return isset($array[$key]) ? $array[$key] : $default; } - /** - * Validate attribute data before insert into table - * - * @param array $data - * @return true - * @throws LocalizedException - */ - private function _validateAttributeData($data) - { - $minLength = \Magento\Eav\Model\Entity\Attribute::ATTRIBUTE_CODE_MIN_LENGTH; - $maxLength = \Magento\Eav\Model\Entity\Attribute::ATTRIBUTE_CODE_MAX_LENGTH; - $attributeCode = isset($data['attribute_code']) ? $data['attribute_code'] : ''; - - $isAllowedLength = \Zend_Validate::is( - trim($attributeCode), - 'StringLength', - ['min' => $minLength, 'max' => $maxLength] - ); - - if (!$isAllowedLength) { - $errorMessage = __( - 'An attribute code must not be less than %1 and more than %2 characters.', - $minLength, - $maxLength - ); - - throw new LocalizedException($errorMessage); - } - - return true; - } - /** * Add attribute to an entity type * @@ -815,6 +798,8 @@ private function _validateAttributeData($data) * @param string $code * @param array $attr * @return $this + * @throws LocalizedException + * @throws \Zend_Validate_Exception */ public function addAttribute($entityTypeId, $code, array $attr) { @@ -825,7 +810,7 @@ public function addAttribute($entityTypeId, $code, array $attr) $this->attributeMapper->map($attr, $entityTypeId) ); - $this->_validateAttributeData($data); + $this->validateAttributeCode($data); $sortOrder = isset($attr['sort_order']) ? $attr['sort_order'] : null; $attributeId = $this->getAttribute($entityTypeId, $code, 'attribute_id'); @@ -1063,7 +1048,7 @@ private function _updateAttributeAdditionalData($entityTypeId, $id, $field, $val return $this; } } - + $attributeId = $this->getAttributeId($entityTypeId, $id); if (false === $attributeId) { throw new LocalizedException(__('Attribute with ID: "%1" does not exist', $id)); @@ -1546,4 +1531,21 @@ private function _insertAttributeAdditionalData($entityTypeId, array $data) return $this; } + + /** + * Validate attribute code. + * + * @param array $data + * @throws LocalizedException + * @throws \Zend_Validate_Exception + */ + private function validateAttributeCode(array $data): void + { + $attributeCode = $data['attribute_code'] ?? ''; + if (!$this->attributeCodeValidator->isValid($attributeCode)) { + $errorMessage = implode('\n', $this->attributeCodeValidator->getMessages()); + + throw new LocalizedException(__($errorMessage)); + } + } } diff --git a/app/code/Magento/Eav/Test/Unit/Model/Attribute/Data/ImageTest.php b/app/code/Magento/Eav/Test/Unit/Model/Attribute/Data/ImageTest.php index c89f581daac8..2df87c39868a 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Attribute/Data/ImageTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Attribute/Data/ImageTest.php @@ -143,7 +143,7 @@ public function validateValueDataProvider() 'originalValue' => 'value', 'isRequired' => true, 'isAjaxRequest' => false, - 'rules' => ['max_image_heght' => 2], + 'rules' => ['max_image_height' => 2], 'expectedResult' => ['"Label" height exceeds allowed value of 2 px.'] ], [ @@ -151,7 +151,7 @@ public function validateValueDataProvider() 'originalValue' => 'value', 'isRequired' => true, 'isAjaxRequest' => false, - 'rules' => ['max_image_heght' => 2000], + 'rules' => ['max_image_height' => 2000], 'expectedResult' => true ], [ @@ -159,7 +159,7 @@ public function validateValueDataProvider() 'originalValue' => 'value', 'isRequired' => true, 'isAjaxRequest' => false, - 'rules' => ['max_image_heght' => 2, 'max_image_width' => 2], + 'rules' => ['max_image_height' => 2, 'max_image_width' => 2], 'expectedResult' => [ '"Label" width exceeds allowed value of 2 px.', '"Label" height exceeds allowed value of 2 px.', diff --git a/app/code/Magento/Eav/Test/Unit/Model/Attribute/Data/TextTest.php b/app/code/Magento/Eav/Test/Unit/Model/Attribute/Data/TextTest.php index bbbe712b2bb4..331d1e6216ae 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Attribute/Data/TextTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Attribute/Data/TextTest.php @@ -6,12 +6,15 @@ namespace Magento\Eav\Test\Unit\Model\Attribute\Data; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Stdlib\StringUtils; + class TextTest extends \PHPUnit\Framework\TestCase { /** * @var \Magento\Eav\Model\Attribute\Data\Text */ - protected $_model; + private $model; /** * {@inheritDoc} @@ -21,10 +24,10 @@ protected function setUp() $locale = $this->createMock(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class); $localeResolver = $this->createMock(\Magento\Framework\Locale\ResolverInterface::class); $logger = $this->createMock(\Psr\Log\LoggerInterface::class); - $helper = $this->createMock(\Magento\Framework\Stdlib\StringUtils::class); + $helper = new StringUtils; - $this->_model = new \Magento\Eav\Model\Attribute\Data\Text($locale, $logger, $localeResolver, $helper); - $this->_model->setAttribute( + $this->model = new \Magento\Eav\Model\Attribute\Data\Text($locale, $logger, $localeResolver, $helper); + $this->model->setAttribute( $this->createAttribute( [ 'store_label' => 'Test', @@ -41,7 +44,7 @@ protected function setUp() */ protected function tearDown() { - $this->_model = null; + $this->model = null; } /** @@ -51,7 +54,7 @@ public function testValidateValueString(): void { $inputValue = '0'; $expectedResult = true; - $this->assertEquals($expectedResult, $this->_model->validateValue($inputValue)); + self::assertEquals($expectedResult, $this->model->validateValue($inputValue)); } /** @@ -61,8 +64,8 @@ public function testValidateValueInteger(): void { $inputValue = 0; $expectedResult = ['"Test" is a required value.']; - $result = $this->_model->validateValue($inputValue); - $this->assertEquals($expectedResult, [(string)$result[0]]); + $result = $this->model->validateValue($inputValue); + self::assertEquals($expectedResult, [(string)$result[0]]); } /** @@ -79,12 +82,106 @@ public function testWithoutLengthValidation(): void ]; $defaultAttributeData['validate_rules']['min_text_length'] = 2; - $this->_model->setAttribute($this->createAttribute($defaultAttributeData)); - $this->assertEquals($expectedResult, $this->_model->validateValue('t')); + $this->model->setAttribute($this->createAttribute($defaultAttributeData)); + self::assertEquals($expectedResult, $this->model->validateValue('t')); $defaultAttributeData['validate_rules']['max_text_length'] = 3; - $this->_model->setAttribute($this->createAttribute($defaultAttributeData)); - $this->assertEquals($expectedResult, $this->_model->validateValue('test')); + $this->model->setAttribute($this->createAttribute($defaultAttributeData)); + self::assertEquals($expectedResult, $this->model->validateValue('test')); + } + + /** + * Test of alphanumeric validation. + * + * @param {String} $value - provided value + * @param {Boolean|Array} $expectedResult - validation result + * @return void + * @throws LocalizedException + * @dataProvider alphanumDataProvider + */ + public function testAlphanumericValidation($value, $expectedResult): void + { + $defaultAttributeData = [ + 'store_label' => 'Test', + 'attribute_code' => 'test', + 'is_required' => 1, + 'validate_rules' => [ + 'min_text_length' => 0, + 'max_text_length' => 10, + 'input_validation' => 'alphanumeric' + ], + ]; + + $this->model->setAttribute($this->createAttribute($defaultAttributeData)); + self::assertEquals($expectedResult, $this->model->validateValue($value)); + } + + /** + * Provides possible input values. + * + * @return array + */ + public function alphanumDataProvider(): array + { + return [ + ['QazWsx', true], + ['QazWsx123', true], + ['QazWsx 123', + [\Zend_Validate_Alnum::NOT_ALNUM => '"Test" contains non-alphabetic or non-numeric characters.'] + ], + ['QazWsx_123', + [\Zend_Validate_Alnum::NOT_ALNUM => '"Test" contains non-alphabetic or non-numeric characters.'] + ], + ['QazWsx12345', [ + __('"%1" length must be equal or less than %2 characters.', 'Test', 10)] + ], + ]; + } + + /** + * Test of alphanumeric validation with spaces. + * + * @param {String} $value - provided value + * @param {Boolean|Array} $expectedResult - validation result + * @return void + * @throws LocalizedException + * @dataProvider alphanumWithSpacesDataProvider + */ + public function testAlphanumericValidationWithSpaces($value, $expectedResult): void + { + $defaultAttributeData = [ + 'store_label' => 'Test', + 'attribute_code' => 'test', + 'is_required' => 1, + 'validate_rules' => [ + 'min_text_length' => 0, + 'max_text_length' => 10, + 'input_validation' => 'alphanum-with-spaces' + ], + ]; + + $this->model->setAttribute($this->createAttribute($defaultAttributeData)); + self::assertEquals($expectedResult, $this->model->validateValue($value)); + } + + /** + * Provides possible input values. + * + * @return array + */ + public function alphanumWithSpacesDataProvider(): array + { + return [ + ['QazWsx', true], + ['QazWsx123', true], + ['QazWsx 123', true], + ['QazWsx_123', + [\Zend_Validate_Alnum::NOT_ALNUM => '"Test" contains non-alphabetic or non-numeric characters.'] + ], + ['QazWsx12345', [ + __('"%1" length must be equal or less than %2 characters.', 'Test', 10)] + ], + ]; } /** diff --git a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Frontend/DefaultFrontendTest.php b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Frontend/DefaultFrontendTest.php index a61c9ef44745..fd4f7472b2fa 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Frontend/DefaultFrontendTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Frontend/DefaultFrontendTest.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Eav\Test\Unit\Model\Entity\Attribute\Frontend; use Magento\Eav\Model\Entity\Attribute\Frontend\DefaultFrontend; @@ -13,43 +15,44 @@ use Magento\Framework\App\CacheInterface; use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource; +use PHPUnit\Framework\MockObject\MockObject; class DefaultFrontendTest extends \PHPUnit\Framework\TestCase { /** * @var DefaultFrontend */ - protected $model; + private $model; /** - * @var BooleanFactory|\PHPUnit_Framework_MockObject_MockObject + * @var BooleanFactory | MockObject */ - protected $booleanFactory; + private $booleanFactory; /** - * @var Serializer|\PHPUnit_Framework_MockObject_MockObject + * @var Serializer| MockObject */ - private $serializerMock; + private $serializer; /** - * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var StoreManagerInterface | MockObject */ - private $storeManagerMock; + private $storeManager; /** - * @var StoreInterface|\PHPUnit_Framework_MockObject_MockObject + * @var StoreInterface | MockObject */ - private $storeMock; + private $store; /** - * @var CacheInterface|\PHPUnit_Framework_MockObject_MockObject + * @var CacheInterface | MockObject */ - private $cacheMock; + private $cache; /** - * @var AbstractAttribute|\PHPUnit_Framework_MockObject_MockObject + * @var AbstractAttribute | MockObject */ - private $attributeMock; + private $attribute; /** * @var array @@ -57,10 +60,13 @@ class DefaultFrontendTest extends \PHPUnit\Framework\TestCase private $cacheTags; /** - * @var AbstractSource|\PHPUnit_Framework_MockObject_MockObject + * @var AbstractSource | MockObject */ - private $sourceMock; + private $source; + /** + * @inheritdoc + */ protected function setUp() { $this->cacheTags = ['tag1', 'tag2']; @@ -68,111 +74,108 @@ protected function setUp() $this->booleanFactory = $this->getMockBuilder(BooleanFactory::class) ->disableOriginalConstructor() ->getMock(); - $this->serializerMock = $this->getMockBuilder(Serializer::class) + $this->serializer = $this->getMockBuilder(Serializer::class) ->getMock(); - $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class) + $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class) ->getMockForAbstractClass(); - $this->storeMock = $this->getMockBuilder(StoreInterface::class) + $this->store = $this->getMockBuilder(StoreInterface::class) ->getMockForAbstractClass(); - $this->cacheMock = $this->getMockBuilder(CacheInterface::class) + $this->cache = $this->getMockBuilder(CacheInterface::class) ->getMockForAbstractClass(); - $this->attributeMock = $this->getMockBuilder(AbstractAttribute::class) - ->disableOriginalConstructor() - ->setMethods(['getAttributeCode', 'getSource']) - ->getMockForAbstractClass(); - $this->sourceMock = $this->getMockBuilder(AbstractSource::class) + $this->attribute = $this->createAttribute(); + $this->source = $this->getMockBuilder(AbstractSource::class) ->disableOriginalConstructor() ->setMethods(['getAllOptions']) ->getMockForAbstractClass(); - $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->model = $objectManager->getObject( - DefaultFrontend::class, - [ - '_attrBooleanFactory' => $this->booleanFactory, - 'cache' => $this->cacheMock, - 'storeManager' => $this->storeManagerMock, - 'serializer' => $this->serializerMock, - '_attribute' => $this->attributeMock, - 'cacheTags' => $this->cacheTags - ] + $this->model = new DefaultFrontend( + $this->booleanFactory, + $this->cache, + null, + $this->cacheTags, + $this->storeManager, + $this->serializer ); + + $this->model->setAttribute($this->attribute); } public function testGetClassEmpty() { - $attributeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class) - ->disableOriginalConstructor() - ->setMethods([ - 'getIsRequired', - 'getFrontendClass', - 'getValidateRules', - ]) - ->getMock(); - $attributeMock->expects($this->once()) - ->method('getIsRequired') + /** @var AbstractAttribute | MockObject $attribute */ + $attribute = $this->createAttribute(); + $attribute->method('getIsRequired') ->willReturn(false); - $attributeMock->expects($this->once()) - ->method('getFrontendClass') + $attribute->method('getFrontendClass') ->willReturn(''); - $attributeMock->expects($this->exactly(2)) + $attribute->expects($this->exactly(2)) ->method('getValidateRules') ->willReturn(''); - $this->model->setAttribute($attributeMock); - $this->assertEmpty($this->model->getClass()); + $this->model->setAttribute($attribute); + + self::assertEmpty($this->model->getClass()); } - public function testGetClass() + /** + * Validates generated html classes. + * + * @param String $validationRule + * @param String $expectedClass + * @return void + * @dataProvider validationRulesDataProvider + */ + public function testGetClass(String $validationRule, String $expectedClass): void { - $attributeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class) - ->disableOriginalConstructor() - ->setMethods([ - 'getIsRequired', - 'getFrontendClass', - 'getValidateRules', - ]) - ->getMock(); - $attributeMock->expects($this->once()) - ->method('getIsRequired') + /** @var AbstractAttribute | MockObject $attribute */ + $attribute = $this->createAttribute(); + $attribute->method('getIsRequired') ->willReturn(true); - $attributeMock->expects($this->once()) - ->method('getFrontendClass') + $attribute->method('getFrontendClass') ->willReturn(''); - $attributeMock->expects($this->exactly(3)) + $attribute->expects($this->exactly(3)) ->method('getValidateRules') ->willReturn([ - 'input_validation' => 'alphanumeric', + 'input_validation' => $validationRule, 'min_text_length' => 1, 'max_text_length' => 2, ]); - $this->model->setAttribute($attributeMock); + $this->model->setAttribute($attribute); $result = $this->model->getClass(); - $this->assertContains('validate-alphanum', $result); - $this->assertContains('minimum-length-1', $result); - $this->assertContains('maximum-length-2', $result); - $this->assertContains('validate-length', $result); + self::assertContains($expectedClass, $result); + self::assertContains('minimum-length-1', $result); + self::assertContains('maximum-length-2', $result); + self::assertContains('validate-length', $result); + } + + /** + * Provides possible validation types. + * + * @return array + */ + public function validationRulesDataProvider(): array + { + return [ + ['alphanumeric', 'validate-alphanum'], + ['alphanum-with-spaces', 'validate-alphanum-with-spaces'], + ['alpha', 'validate-alpha'], + ['numeric', 'validate-digits'], + ['url', 'validate-url'], + ['email', 'validate-email'], + ['length', 'validate-length'] + ]; } public function testGetClassLength() { - $attributeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class) - ->disableOriginalConstructor() - ->setMethods([ - 'getIsRequired', - 'getFrontendClass', - 'getValidateRules', - ]) - ->getMock(); - $attributeMock->expects($this->once()) - ->method('getIsRequired') + $attribute = $this->createAttribute(); + $attribute->method('getIsRequired') ->willReturn(true); - $attributeMock->expects($this->once()) - ->method('getFrontendClass') + $attribute->method('getFrontendClass') ->willReturn(''); - $attributeMock->expects($this->exactly(3)) + $attribute->expects($this->exactly(3)) ->method('getValidateRules') ->willReturn([ 'input_validation' => 'length', @@ -180,12 +183,31 @@ public function testGetClassLength() 'max_text_length' => 2, ]); - $this->model->setAttribute($attributeMock); + $this->model->setAttribute($attribute); $result = $this->model->getClass(); - $this->assertContains('minimum-length-1', $result); - $this->assertContains('maximum-length-2', $result); - $this->assertContains('validate-length', $result); + self::assertContains('minimum-length-1', $result); + self::assertContains('maximum-length-2', $result); + self::assertContains('validate-length', $result); + } + + /** + * Entity attribute factory. + * + * @return AbstractAttribute | MockObject + */ + private function createAttribute() + { + return $this->getMockBuilder(AbstractAttribute::class) + ->disableOriginalConstructor() + ->setMethods([ + 'getIsRequired', + 'getFrontendClass', + 'getValidateRules', + 'getAttributeCode', + 'getSource' + ]) + ->getMockForAbstractClass(); } public function testGetSelectOptions() @@ -196,33 +218,25 @@ public function testGetSelectOptions() $options = ['option1', 'option2']; $serializedOptions = "{['option1', 'option2']}"; - $this->storeManagerMock->expects($this->once()) - ->method('getStore') - ->willReturn($this->storeMock); - $this->storeMock->expects($this->once()) - ->method('getId') + $this->storeManager->method('getStore') + ->willReturn($this->store); + $this->store->method('getId') ->willReturn($storeId); - $this->attributeMock->expects($this->once()) - ->method('getAttributeCode') + $this->attribute->method('getAttributeCode') ->willReturn($attributeCode); - $this->cacheMock->expects($this->once()) - ->method('load') + $this->cache->method('load') ->with($cacheKey) ->willReturn(false); - $this->attributeMock->expects($this->once()) - ->method('getSource') - ->willReturn($this->sourceMock); - $this->sourceMock->expects($this->once()) - ->method('getAllOptions') + $this->attribute->method('getSource') + ->willReturn($this->source); + $this->source->method('getAllOptions') ->willReturn($options); - $this->serializerMock->expects($this->once()) - ->method('serialize') + $this->serializer->method('serialize') ->with($options) ->willReturn($serializedOptions); - $this->cacheMock->expects($this->once()) - ->method('save') + $this->cache->method('save') ->with($serializedOptions, $cacheKey, $this->cacheTags); - $this->assertSame($options, $this->model->getSelectOptions()); + self::assertSame($options, $this->model->getSelectOptions()); } } diff --git a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/GroupTest.php b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/GroupTest.php index d4c91e98d960..1584b922abaa 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/GroupTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/GroupTest.php @@ -40,6 +40,7 @@ protected function setUp() 'resource' => $this->resourceMock, 'translitFilter' => $translitFilter, 'context' => $contextMock, + 'reservedSystemNames' => ['configurable'], ]; $objectManager = new ObjectManager($this); $this->model = $objectManager->getObject( @@ -67,6 +68,8 @@ public function attributeGroupCodeDataProvider() { return [ ['General Group', 'general-group'], + ['configurable', md5('configurable')], + ['configurAble', md5('configurable')], ['///', md5('///')], ]; } diff --git a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/OptionManagementTest.php b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/OptionManagementTest.php index 2ae31a05d957..b63a4dd2c9ae 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/OptionManagementTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/OptionManagementTest.php @@ -59,13 +59,13 @@ public function testAdd() $labelMock = $this->createMock(\Magento\Eav\Api\Data\AttributeOptionLabelInterface::class); $option = ['value' => [ - 'new_option' => [ + 'id_new_option' => [ 0 => 'optionLabel', 42 => 'labelLabel', ], ], 'order' => [ - 'new_option' => 'optionSortOrder', + 'id_new_option' => 'optionSortOrder', ], ]; @@ -78,10 +78,10 @@ public function testAdd() $labelMock->expects($this->once())->method('getStoreId')->willReturn(42); $labelMock->expects($this->once())->method('getLabel')->willReturn('labelLabel'); $optionMock->expects($this->once())->method('getIsDefault')->willReturn(true); - $attributeMock->expects($this->once())->method('setDefault')->with(['new_option']); + $attributeMock->expects($this->once())->method('setDefault')->with(['id_new_option']); $attributeMock->expects($this->once())->method('setOption')->with($option); $this->resourceModelMock->expects($this->once())->method('save')->with($attributeMock); - $this->assertEquals('new_option', $this->model->add($entityType, $attributeCode, $optionMock)); + $this->assertEquals('id_new_option', $this->model->add($entityType, $attributeCode, $optionMock)); } /** @@ -167,13 +167,13 @@ public function testAddWithCannotSaveException() $labelMock = $this->createMock(\Magento\Eav\Api\Data\AttributeOptionLabelInterface::class); $option = ['value' => [ - 'new_option' => [ + 'id_new_option' => [ 0 => 'optionLabel', 42 => 'labelLabel', ], ], 'order' => [ - 'new_option' => 'optionSortOrder', + 'id_new_option' => 'optionSortOrder', ], ]; @@ -186,7 +186,7 @@ public function testAddWithCannotSaveException() $labelMock->expects($this->once())->method('getStoreId')->willReturn(42); $labelMock->expects($this->once())->method('getLabel')->willReturn('labelLabel'); $optionMock->expects($this->once())->method('getIsDefault')->willReturn(true); - $attributeMock->expects($this->once())->method('setDefault')->with(['new_option']); + $attributeMock->expects($this->once())->method('setDefault')->with(['id_new_option']); $attributeMock->expects($this->once())->method('setOption')->with($option); $this->resourceModelMock->expects($this->once())->method('save')->with($attributeMock) ->willThrowException(new \Exception()); diff --git a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Source/BooleanTest.php b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Source/BooleanTest.php index ee972c27aa8a..8cf5df877a6e 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Source/BooleanTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Source/BooleanTest.php @@ -101,13 +101,13 @@ public function addValueSortToCollectionDataProvider() 'expectedJoinCondition' => [ 0 => [ 'requisites' => ['code_t1' => "table"], - 'condition' => - "e.entity_id=code_t1.entity_id AND code_t1.attribute_id='123' AND code_t1.store_id='0'", + 'condition' => "e.entity_id=code_t1.entity_id AND code_t1.attribute_id='123'" + . " AND code_t1.store_id='0'", ], 1 => [ 'requisites' => ['code_t2' => "table"], - 'condition' => - "e.entity_id=code_t2.entity_id AND code_t2.attribute_id='123' AND code_t2.store_id='12'", + 'condition' => "e.entity_id=code_t2.entity_id AND code_t2.attribute_id='123'" + . " AND code_t2.store_id='12'", ], ], 'expectedOrder' => 'IF(code_t2.value_id > 0, code_t2.value, code_t1.value) ASC', @@ -118,13 +118,13 @@ public function addValueSortToCollectionDataProvider() 'expectedJoinCondition' => [ 0 => [ 'requisites' => ['code_t1' => "table"], - 'condition' => - "e.entity_id=code_t1.entity_id AND code_t1.attribute_id='123' AND code_t1.store_id='0'", + 'condition' => "e.entity_id=code_t1.entity_id AND code_t1.attribute_id='123'" + . " AND code_t1.store_id='0'", ], 1 => [ 'requisites' => ['code_t2' => "table"], - 'condition' => - "e.entity_id=code_t2.entity_id AND code_t2.attribute_id='123' AND code_t2.store_id='12'", + 'condition' => "e.entity_id=code_t2.entity_id AND code_t2.attribute_id='123'" + . " AND code_t2.store_id='12'", ], ], 'expectedOrder' => 'IF(code_t2.value_id > 0, code_t2.value, code_t1.value) DESC', @@ -135,8 +135,8 @@ public function addValueSortToCollectionDataProvider() 'expectedJoinCondition' => [ 0 => [ 'requisites' => ['code_t' => "table"], - 'condition' => - "e.entity_id=code_t.entity_id AND code_t.attribute_id='123' AND code_t.store_id='0'", + 'condition' => "e.entity_id=code_t.entity_id AND code_t.attribute_id='123'" + . " AND code_t.store_id='0'", ], ], 'expectedOrder' => 'code_t.value DESC', @@ -147,8 +147,8 @@ public function addValueSortToCollectionDataProvider() 'expectedJoinCondition' => [ 0 => [ 'requisites' => ['code_t' => "table"], - 'condition' => - "e.entity_id=code_t.entity_id AND code_t.attribute_id='123' AND code_t.store_id='0'", + 'condition' => "e.entity_id=code_t.entity_id AND code_t.attribute_id='123'" + . " AND code_t.store_id='0'", ], ], 'expectedOrder' => 'code_t.value ASC', diff --git a/app/code/Magento/Eav/Test/Unit/Model/Entity/AttributeTest.php b/app/code/Magento/Eav/Test/Unit/Model/Entity/AttributeTest.php index b15174960524..402497a1379c 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Entity/AttributeTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Entity/AttributeTest.php @@ -5,6 +5,9 @@ */ namespace Magento\Eav\Test\Unit\Model\Entity; +/** + * Class AttributeTest. + */ class AttributeTest extends \PHPUnit\Framework\TestCase { /** @@ -128,6 +131,7 @@ public function testGetFrontendLabels() $attributeId = 1; $storeLabels = ['test_attribute_store1']; $frontendLabelFactory = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\FrontendLabelFactory::class) + ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); $resource = $this->getMockBuilder(\Magento\Eav\Model\ResourceModel\Entity\Attribute::class) 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 bc4ed7d4bd9e..ab5b40c56685 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Entity/Collection/AbstractCollectionTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Entity/Collection/AbstractCollectionTest.php @@ -5,6 +5,9 @@ */ namespace Magento\Eav\Test\Unit\Model\Entity\Collection; +use Magento\Framework\Data\Collection\Db\FetchStrategyInterface; +use Magento\Framework\Model\ResourceModel\ResourceModelPoolInterface; + /** * AbstractCollection test * @@ -28,7 +31,7 @@ class AbstractCollectionTest extends \PHPUnit\Framework\TestCase protected $loggerMock; /** - * @var \Magento\Framework\Data\Collection\Db\FetchStrategyInterface|\PHPUnit_Framework_MockObject_MockObject + * @var FetchStrategyInterface|\PHPUnit_Framework_MockObject_MockObject */ protected $fetchStrategyMock; @@ -58,7 +61,7 @@ class AbstractCollectionTest extends \PHPUnit\Framework\TestCase protected $resourceHelperMock; /** - * @var \Magento\Framework\Validator\UniversalFactory|\PHPUnit_Framework_MockObject_MockObject + * @var ResourceModelPoolInterface|\PHPUnit_Framework_MockObject_MockObject */ protected $validatorFactoryMock; diff --git a/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/CodeTest.php b/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/CodeTest.php new file mode 100644 index 000000000000..9db290bcba3e --- /dev/null +++ b/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/CodeTest.php @@ -0,0 +1,67 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +/** + * Test for \Magento\Eav\Model\Validator\Attribute\Code + */ +namespace Magento\Eav\Test\Unit\Model\Validator\Attribute; + +use Magento\Eav\Model\Validator\Attribute\Code; +use PHPUnit\Framework\TestCase; + +/** + * Class CodeTest + */ +class CodeTest extends TestCase +{ + /** + * Testing \Magento\Eav\Model\Validator\Attribute\Code::isValid + * + * @dataProvider isValidDataProvider + * @param string $attributeCode + * @param bool $expected + * @throws \Zend_Validate_Exception + */ + public function testIsValid(string $attributeCode, bool $expected): void + { + $validator = new Code(); + $this->assertEquals($expected, $validator->isValid($attributeCode)); + } + + /** + * Data provider for testIsValid + * + * @return array + */ + public function isValidDataProvider(): array + { + return [ + [ + 'Attribute_code', + true + ], [ + 'attribute_1', + true + ],[ + 'Attribute_1', + true + ], [ + '_attribute_code', + false + ], [ + 'attribute.code', + false + ], [ + '1attribute_code', + false + ], [ + 'more_than_60_chars_more_than_60_chars_more_than_60_chars_more', + false + ] + ]; + } +} diff --git a/app/code/Magento/Eav/etc/db_schema.xml b/app/code/Magento/Eav/etc/db_schema.xml index f930321e5259..b6c42d725e5e 100644 --- a/app/code/Magento/Eav/etc/db_schema.xml +++ b/app/code/Magento/Eav/etc/db_schema.xml @@ -15,7 +15,7 @@ <column xsi:type="varchar" name="attribute_model" nullable="true" length="255" comment="Attribute Model"/> <column xsi:type="varchar" name="entity_table" nullable="true" length="255" comment="Entity Table"/> <column xsi:type="varchar" name="value_table_prefix" nullable="true" length="255" comment="Value Table Prefix"/> - <column xsi:type="varchar" name="entity_id_field" nullable="true" length="255" comment="Entity Id Field"/> + <column xsi:type="varchar" name="entity_id_field" nullable="true" length="255" comment="Entity ID Field"/> <column xsi:type="smallint" name="is_data_sharing" padding="5" unsigned="true" nullable="false" identity="false" default="1" comment="Defines Is Data Sharing"/> <column xsi:type="varchar" name="data_sharing_key" nullable="true" length="100" default="default" @@ -33,16 +33,16 @@ comment="Additional Attribute Table"/> <column xsi:type="varchar" name="entity_attribute_collection" nullable="true" length="255" comment="Entity Attribute Collection"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_type_id"/> </constraint> - <index name="EAV_ENTITY_TYPE_ENTITY_TYPE_CODE" indexType="btree"> + <index referenceId="EAV_ENTITY_TYPE_ENTITY_TYPE_CODE" indexType="btree"> <column name="entity_type_code"/> </index> </table> <table name="eav_entity" resource="default" engine="innodb" comment="Eav Entity"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="smallint" name="entity_type_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Entity Type Id"/> <column xsi:type="smallint" name="attribute_set_id" padding="5" unsigned="true" nullable="false" @@ -58,18 +58,18 @@ comment="Updated At"/> <column xsi:type="smallint" name="is_active" padding="5" unsigned="true" nullable="false" identity="false" default="1" comment="Defines Is Entity Active"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> - <constraint xsi:type="foreign" name="EAV_ENTITY_ENTITY_TYPE_ID_EAV_ENTITY_TYPE_ENTITY_TYPE_ID" + <constraint xsi:type="foreign" referenceId="EAV_ENTITY_ENTITY_TYPE_ID_EAV_ENTITY_TYPE_ENTITY_TYPE_ID" table="eav_entity" column="entity_type_id" referenceTable="eav_entity_type" referenceColumn="entity_type_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="EAV_ENTITY_STORE_ID_STORE_STORE_ID" table="eav_entity" column="store_id" + <constraint xsi:type="foreign" referenceId="EAV_ENTITY_STORE_ID_STORE_STORE_ID" table="eav_entity" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <index name="EAV_ENTITY_ENTITY_TYPE_ID" indexType="btree"> + <index referenceId="EAV_ENTITY_ENTITY_TYPE_ID" indexType="btree"> <column name="entity_type_id"/> </index> - <index name="EAV_ENTITY_STORE_ID" indexType="btree"> + <index referenceId="EAV_ENTITY_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -83,32 +83,32 @@ <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Store Id"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Id"/> + default="0" comment="Entity ID"/> <column xsi:type="datetime" name="value" on_update="false" nullable="true" comment="Attribute Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="foreign" name="EAV_ENTITY_DATETIME_ENTITY_ID_EAV_ENTITY_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="EAV_ENTITY_DATETIME_ENTITY_ID_EAV_ENTITY_ENTITY_ID" table="eav_entity_datetime" column="entity_id" referenceTable="eav_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="EAV_ENTT_DTIME_ENTT_TYPE_ID_EAV_ENTT_TYPE_ENTT_TYPE_ID" + <constraint xsi:type="foreign" referenceId="EAV_ENTT_DTIME_ENTT_TYPE_ID_EAV_ENTT_TYPE_ENTT_TYPE_ID" table="eav_entity_datetime" column="entity_type_id" referenceTable="eav_entity_type" referenceColumn="entity_type_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="EAV_ENTITY_DATETIME_STORE_ID_STORE_STORE_ID" table="eav_entity_datetime" + <constraint xsi:type="foreign" referenceId="EAV_ENTITY_DATETIME_STORE_ID_STORE_STORE_ID" table="eav_entity_datetime" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="EAV_ENTITY_DATETIME_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> + <constraint xsi:type="unique" referenceId="EAV_ENTITY_DATETIME_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> <column name="entity_id"/> <column name="attribute_id"/> <column name="store_id"/> </constraint> - <index name="EAV_ENTITY_DATETIME_STORE_ID" indexType="btree"> + <index referenceId="EAV_ENTITY_DATETIME_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="EAV_ENTITY_DATETIME_ATTRIBUTE_ID_VALUE" indexType="btree"> + <index referenceId="EAV_ENTITY_DATETIME_ATTRIBUTE_ID_VALUE" indexType="btree"> <column name="attribute_id"/> <column name="value"/> </index> - <index name="EAV_ENTITY_DATETIME_ENTITY_TYPE_ID_VALUE" indexType="btree"> + <index referenceId="EAV_ENTITY_DATETIME_ENTITY_TYPE_ID_VALUE" indexType="btree"> <column name="entity_type_id"/> <column name="value"/> </index> @@ -123,33 +123,33 @@ <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Store Id"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Id"/> + default="0" comment="Entity ID"/> <column xsi:type="decimal" name="value" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Attribute Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="foreign" name="EAV_ENTITY_DECIMAL_ENTITY_ID_EAV_ENTITY_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="EAV_ENTITY_DECIMAL_ENTITY_ID_EAV_ENTITY_ENTITY_ID" table="eav_entity_decimal" column="entity_id" referenceTable="eav_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="EAV_ENTITY_DECIMAL_ENTITY_TYPE_ID_EAV_ENTITY_TYPE_ENTITY_TYPE_ID" + <constraint xsi:type="foreign" referenceId="EAV_ENTITY_DECIMAL_ENTITY_TYPE_ID_EAV_ENTITY_TYPE_ENTITY_TYPE_ID" table="eav_entity_decimal" column="entity_type_id" referenceTable="eav_entity_type" referenceColumn="entity_type_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="EAV_ENTITY_DECIMAL_STORE_ID_STORE_STORE_ID" table="eav_entity_decimal" + <constraint xsi:type="foreign" referenceId="EAV_ENTITY_DECIMAL_STORE_ID_STORE_STORE_ID" table="eav_entity_decimal" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="EAV_ENTITY_DECIMAL_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> + <constraint xsi:type="unique" referenceId="EAV_ENTITY_DECIMAL_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> <column name="entity_id"/> <column name="attribute_id"/> <column name="store_id"/> </constraint> - <index name="EAV_ENTITY_DECIMAL_STORE_ID" indexType="btree"> + <index referenceId="EAV_ENTITY_DECIMAL_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="EAV_ENTITY_DECIMAL_ATTRIBUTE_ID_VALUE" indexType="btree"> + <index referenceId="EAV_ENTITY_DECIMAL_ATTRIBUTE_ID_VALUE" indexType="btree"> <column name="attribute_id"/> <column name="value"/> </index> - <index name="EAV_ENTITY_DECIMAL_ENTITY_TYPE_ID_VALUE" indexType="btree"> + <index referenceId="EAV_ENTITY_DECIMAL_ENTITY_TYPE_ID_VALUE" indexType="btree"> <column name="entity_type_id"/> <column name="value"/> </index> @@ -164,32 +164,32 @@ <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Store Id"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Id"/> + default="0" comment="Entity ID"/> <column xsi:type="int" name="value" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Attribute Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="foreign" name="EAV_ENTITY_INT_ENTITY_ID_EAV_ENTITY_ENTITY_ID" table="eav_entity_int" + <constraint xsi:type="foreign" referenceId="EAV_ENTITY_INT_ENTITY_ID_EAV_ENTITY_ENTITY_ID" table="eav_entity_int" column="entity_id" referenceTable="eav_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="EAV_ENTITY_INT_ENTITY_TYPE_ID_EAV_ENTITY_TYPE_ENTITY_TYPE_ID" + <constraint xsi:type="foreign" referenceId="EAV_ENTITY_INT_ENTITY_TYPE_ID_EAV_ENTITY_TYPE_ENTITY_TYPE_ID" table="eav_entity_int" column="entity_type_id" referenceTable="eav_entity_type" referenceColumn="entity_type_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="EAV_ENTITY_INT_STORE_ID_STORE_STORE_ID" table="eav_entity_int" + <constraint xsi:type="foreign" referenceId="EAV_ENTITY_INT_STORE_ID_STORE_STORE_ID" table="eav_entity_int" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="EAV_ENTITY_INT_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> + <constraint xsi:type="unique" referenceId="EAV_ENTITY_INT_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> <column name="entity_id"/> <column name="attribute_id"/> <column name="store_id"/> </constraint> - <index name="EAV_ENTITY_INT_STORE_ID" indexType="btree"> + <index referenceId="EAV_ENTITY_INT_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="EAV_ENTITY_INT_ATTRIBUTE_ID_VALUE" indexType="btree"> + <index referenceId="EAV_ENTITY_INT_ATTRIBUTE_ID_VALUE" indexType="btree"> <column name="attribute_id"/> <column name="value"/> </index> - <index name="EAV_ENTITY_INT_ENTITY_TYPE_ID_VALUE" indexType="btree"> + <index referenceId="EAV_ENTITY_INT_ENTITY_TYPE_ID_VALUE" indexType="btree"> <column name="entity_type_id"/> <column name="value"/> </index> @@ -204,30 +204,30 @@ <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Store Id"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Id"/> + default="0" comment="Entity ID"/> <column xsi:type="text" name="value" nullable="false" comment="Attribute Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="foreign" name="EAV_ENTITY_TEXT_ENTITY_ID_EAV_ENTITY_ENTITY_ID" table="eav_entity_text" + <constraint xsi:type="foreign" referenceId="EAV_ENTITY_TEXT_ENTITY_ID_EAV_ENTITY_ENTITY_ID" table="eav_entity_text" column="entity_id" referenceTable="eav_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="EAV_ENTITY_TEXT_ENTITY_TYPE_ID_EAV_ENTITY_TYPE_ENTITY_TYPE_ID" + <constraint xsi:type="foreign" referenceId="EAV_ENTITY_TEXT_ENTITY_TYPE_ID_EAV_ENTITY_TYPE_ENTITY_TYPE_ID" table="eav_entity_text" column="entity_type_id" referenceTable="eav_entity_type" referenceColumn="entity_type_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="EAV_ENTITY_TEXT_STORE_ID_STORE_STORE_ID" table="eav_entity_text" + <constraint xsi:type="foreign" referenceId="EAV_ENTITY_TEXT_STORE_ID_STORE_STORE_ID" table="eav_entity_text" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="EAV_ENTITY_TEXT_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> + <constraint xsi:type="unique" referenceId="EAV_ENTITY_TEXT_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> <column name="entity_id"/> <column name="attribute_id"/> <column name="store_id"/> </constraint> - <index name="EAV_ENTITY_TEXT_ENTITY_TYPE_ID" indexType="btree"> + <index referenceId="EAV_ENTITY_TEXT_ENTITY_TYPE_ID" indexType="btree"> <column name="entity_type_id"/> </index> - <index name="EAV_ENTITY_TEXT_ATTRIBUTE_ID" indexType="btree"> + <index referenceId="EAV_ENTITY_TEXT_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> - <index name="EAV_ENTITY_TEXT_STORE_ID" indexType="btree"> + <index referenceId="EAV_ENTITY_TEXT_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -241,32 +241,32 @@ <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Store Id"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Id"/> + default="0" comment="Entity ID"/> <column xsi:type="varchar" name="value" nullable="true" length="255" comment="Attribute Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="foreign" name="EAV_ENTITY_VARCHAR_ENTITY_ID_EAV_ENTITY_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="EAV_ENTITY_VARCHAR_ENTITY_ID_EAV_ENTITY_ENTITY_ID" table="eav_entity_varchar" column="entity_id" referenceTable="eav_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="EAV_ENTITY_VARCHAR_ENTITY_TYPE_ID_EAV_ENTITY_TYPE_ENTITY_TYPE_ID" + <constraint xsi:type="foreign" referenceId="EAV_ENTITY_VARCHAR_ENTITY_TYPE_ID_EAV_ENTITY_TYPE_ENTITY_TYPE_ID" table="eav_entity_varchar" column="entity_type_id" referenceTable="eav_entity_type" referenceColumn="entity_type_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="EAV_ENTITY_VARCHAR_STORE_ID_STORE_STORE_ID" table="eav_entity_varchar" + <constraint xsi:type="foreign" referenceId="EAV_ENTITY_VARCHAR_STORE_ID_STORE_STORE_ID" table="eav_entity_varchar" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="EAV_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> + <constraint xsi:type="unique" referenceId="EAV_ENTITY_VARCHAR_ENTITY_ID_ATTRIBUTE_ID_STORE_ID"> <column name="entity_id"/> <column name="attribute_id"/> <column name="store_id"/> </constraint> - <index name="EAV_ENTITY_VARCHAR_STORE_ID" indexType="btree"> + <index referenceId="EAV_ENTITY_VARCHAR_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="EAV_ENTITY_VARCHAR_ATTRIBUTE_ID_VALUE" indexType="btree"> + <index referenceId="EAV_ENTITY_VARCHAR_ATTRIBUTE_ID_VALUE" indexType="btree"> <column name="attribute_id"/> <column name="value"/> </index> - <index name="EAV_ENTITY_VARCHAR_ENTITY_TYPE_ID_VALUE" indexType="btree"> + <index referenceId="EAV_ENTITY_VARCHAR_ENTITY_TYPE_ID_VALUE" indexType="btree"> <column name="entity_type_id"/> <column name="value"/> </index> @@ -295,13 +295,13 @@ <column xsi:type="smallint" name="is_unique" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Defines Is Unique"/> <column xsi:type="varchar" name="note" nullable="true" length="255" comment="Note"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="attribute_id"/> </constraint> - <constraint xsi:type="foreign" name="EAV_ATTRIBUTE_ENTITY_TYPE_ID_EAV_ENTITY_TYPE_ENTITY_TYPE_ID" + <constraint xsi:type="foreign" referenceId="EAV_ATTRIBUTE_ENTITY_TYPE_ID_EAV_ENTITY_TYPE_ENTITY_TYPE_ID" table="eav_attribute" column="entity_type_id" referenceTable="eav_entity_type" referenceColumn="entity_type_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="EAV_ATTRIBUTE_ENTITY_TYPE_ID_ATTRIBUTE_CODE"> + <constraint xsi:type="unique" referenceId="EAV_ATTRIBUTE_ENTITY_TYPE_ID_ATTRIBUTE_CODE"> <column name="entity_type_id"/> <column name="attribute_code"/> </constraint> @@ -315,18 +315,18 @@ default="0" comment="Store Id"/> <column xsi:type="varchar" name="increment_prefix" nullable="true" length="20" comment="Increment Prefix"/> <column xsi:type="varchar" name="increment_last_id" nullable="true" length="50" comment="Last Incremented Id"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_store_id"/> </constraint> - <constraint xsi:type="foreign" name="EAV_ENTITY_STORE_ENTITY_TYPE_ID_EAV_ENTITY_TYPE_ENTITY_TYPE_ID" + <constraint xsi:type="foreign" referenceId="EAV_ENTITY_STORE_ENTITY_TYPE_ID_EAV_ENTITY_TYPE_ENTITY_TYPE_ID" table="eav_entity_store" column="entity_type_id" referenceTable="eav_entity_type" referenceColumn="entity_type_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="EAV_ENTITY_STORE_STORE_ID_STORE_STORE_ID" table="eav_entity_store" + <constraint xsi:type="foreign" referenceId="EAV_ENTITY_STORE_STORE_ID_STORE_STORE_ID" table="eav_entity_store" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <index name="EAV_ENTITY_STORE_ENTITY_TYPE_ID" indexType="btree"> + <index referenceId="EAV_ENTITY_STORE_ENTITY_TYPE_ID" indexType="btree"> <column name="entity_type_id"/> </index> - <index name="EAV_ENTITY_STORE_STORE_ID" indexType="btree"> + <index referenceId="EAV_ENTITY_STORE_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -338,17 +338,17 @@ <column xsi:type="varchar" name="attribute_set_name" nullable="true" length="255" comment="Attribute Set Name"/> <column xsi:type="smallint" name="sort_order" padding="6" unsigned="false" nullable="false" identity="false" default="0" comment="Sort Order"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="attribute_set_id"/> </constraint> - <constraint xsi:type="foreign" name="EAV_ATTRIBUTE_SET_ENTITY_TYPE_ID_EAV_ENTITY_TYPE_ENTITY_TYPE_ID" + <constraint xsi:type="foreign" referenceId="EAV_ATTRIBUTE_SET_ENTITY_TYPE_ID_EAV_ENTITY_TYPE_ENTITY_TYPE_ID" table="eav_attribute_set" column="entity_type_id" referenceTable="eav_entity_type" referenceColumn="entity_type_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="EAV_ATTRIBUTE_SET_ENTITY_TYPE_ID_ATTRIBUTE_SET_NAME"> + <constraint xsi:type="unique" referenceId="EAV_ATTRIBUTE_SET_ENTITY_TYPE_ID_ATTRIBUTE_SET_NAME"> <column name="entity_type_id"/> <column name="attribute_set_name"/> </constraint> - <index name="EAV_ATTRIBUTE_SET_ENTITY_TYPE_ID_SORT_ORDER" indexType="btree"> + <index referenceId="EAV_ATTRIBUTE_SET_ENTITY_TYPE_ID_SORT_ORDER" indexType="btree"> <column name="entity_type_id"/> <column name="sort_order"/> </index> @@ -367,21 +367,21 @@ <column xsi:type="varchar" name="attribute_group_code" nullable="false" length="255" comment="Attribute Group Code"/> <column xsi:type="varchar" name="tab_group_code" nullable="true" length="255" comment="Tab Group Code"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="attribute_group_id"/> </constraint> - <constraint xsi:type="foreign" name="EAV_ATTR_GROUP_ATTR_SET_ID_EAV_ATTR_SET_ATTR_SET_ID" + <constraint xsi:type="foreign" referenceId="EAV_ATTR_GROUP_ATTR_SET_ID_EAV_ATTR_SET_ATTR_SET_ID" table="eav_attribute_group" column="attribute_set_id" referenceTable="eav_attribute_set" referenceColumn="attribute_set_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="EAV_ATTRIBUTE_GROUP_ATTRIBUTE_SET_ID_ATTRIBUTE_GROUP_NAME"> + <constraint xsi:type="unique" referenceId="EAV_ATTRIBUTE_GROUP_ATTRIBUTE_SET_ID_ATTRIBUTE_GROUP_NAME"> <column name="attribute_set_id"/> <column name="attribute_group_name"/> </constraint> - <constraint xsi:type="unique" name="EAV_ATTRIBUTE_GROUP_ATTRIBUTE_SET_ID_ATTRIBUTE_GROUP_CODE"> + <constraint xsi:type="unique" referenceId="EAV_ATTRIBUTE_GROUP_ATTRIBUTE_SET_ID_ATTRIBUTE_GROUP_CODE"> <column name="attribute_set_id"/> <column name="attribute_group_code"/> </constraint> - <index name="EAV_ATTRIBUTE_GROUP_ATTRIBUTE_SET_ID_SORT_ORDER" indexType="btree"> + <index referenceId="EAV_ATTRIBUTE_GROUP_ATTRIBUTE_SET_ID_SORT_ORDER" indexType="btree"> <column name="attribute_set_id"/> <column name="sort_order"/> </index> @@ -399,28 +399,28 @@ default="0" comment="Attribute Id"/> <column xsi:type="smallint" name="sort_order" padding="6" unsigned="false" nullable="false" identity="false" default="0" comment="Sort Order"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_attribute_id"/> </constraint> - <constraint xsi:type="foreign" name="EAV_ENTITY_ATTRIBUTE_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" + <constraint xsi:type="foreign" referenceId="EAV_ENTITY_ATTRIBUTE_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" table="eav_entity_attribute" column="attribute_id" referenceTable="eav_attribute" referenceColumn="attribute_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="EAV_ENTT_ATTR_ATTR_GROUP_ID_EAV_ATTR_GROUP_ATTR_GROUP_ID" + <constraint xsi:type="foreign" referenceId="EAV_ENTT_ATTR_ATTR_GROUP_ID_EAV_ATTR_GROUP_ATTR_GROUP_ID" table="eav_entity_attribute" column="attribute_group_id" referenceTable="eav_attribute_group" referenceColumn="attribute_group_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="EAV_ENTITY_ATTRIBUTE_ATTRIBUTE_SET_ID_ATTRIBUTE_ID"> + <constraint xsi:type="unique" referenceId="EAV_ENTITY_ATTRIBUTE_ATTRIBUTE_SET_ID_ATTRIBUTE_ID"> <column name="attribute_set_id"/> <column name="attribute_id"/> </constraint> - <constraint xsi:type="unique" name="EAV_ENTITY_ATTRIBUTE_ATTRIBUTE_GROUP_ID_ATTRIBUTE_ID"> + <constraint xsi:type="unique" referenceId="EAV_ENTITY_ATTRIBUTE_ATTRIBUTE_GROUP_ID_ATTRIBUTE_ID"> <column name="attribute_group_id"/> <column name="attribute_id"/> </constraint> - <index name="EAV_ENTITY_ATTRIBUTE_ATTRIBUTE_SET_ID_SORT_ORDER" indexType="btree"> + <index referenceId="EAV_ENTITY_ATTRIBUTE_ATTRIBUTE_SET_ID_SORT_ORDER" indexType="btree"> <column name="attribute_set_id"/> <column name="sort_order"/> </index> - <index name="EAV_ENTITY_ATTRIBUTE_ATTRIBUTE_ID" indexType="btree"> + <index referenceId="EAV_ENTITY_ATTRIBUTE_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> </table> @@ -431,13 +431,13 @@ default="0" comment="Attribute Id"/> <column xsi:type="smallint" name="sort_order" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Sort Order"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="option_id"/> </constraint> - <constraint xsi:type="foreign" name="EAV_ATTRIBUTE_OPTION_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" + <constraint xsi:type="foreign" referenceId="EAV_ATTRIBUTE_OPTION_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" table="eav_attribute_option" column="attribute_id" referenceTable="eav_attribute" referenceColumn="attribute_id" onDelete="CASCADE"/> - <index name="EAV_ATTRIBUTE_OPTION_ATTRIBUTE_ID" indexType="btree"> + <index referenceId="EAV_ATTRIBUTE_OPTION_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> </table> @@ -449,19 +449,19 @@ <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Store Id"/> <column xsi:type="varchar" name="value" nullable="true" length="255" comment="Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="foreign" name="EAV_ATTR_OPT_VAL_OPT_ID_EAV_ATTR_OPT_OPT_ID" + <constraint xsi:type="foreign" referenceId="EAV_ATTR_OPT_VAL_OPT_ID_EAV_ATTR_OPT_OPT_ID" table="eav_attribute_option_value" column="option_id" referenceTable="eav_attribute_option" referenceColumn="option_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="EAV_ATTRIBUTE_OPTION_VALUE_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="EAV_ATTRIBUTE_OPTION_VALUE_STORE_ID_STORE_STORE_ID" table="eav_attribute_option_value" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <index name="EAV_ATTRIBUTE_OPTION_VALUE_OPTION_ID" indexType="btree"> + <index referenceId="EAV_ATTRIBUTE_OPTION_VALUE_OPTION_ID" indexType="btree"> <column name="option_id"/> </index> - <index name="EAV_ATTRIBUTE_OPTION_VALUE_STORE_ID" indexType="btree"> + <index referenceId="EAV_ATTRIBUTE_OPTION_VALUE_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -473,18 +473,18 @@ <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Store Id"/> <column xsi:type="varchar" name="value" nullable="true" length="255" comment="Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="attribute_label_id"/> </constraint> - <constraint xsi:type="foreign" name="EAV_ATTRIBUTE_LABEL_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" + <constraint xsi:type="foreign" referenceId="EAV_ATTRIBUTE_LABEL_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" table="eav_attribute_label" column="attribute_id" referenceTable="eav_attribute" referenceColumn="attribute_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="EAV_ATTRIBUTE_LABEL_STORE_ID_STORE_STORE_ID" table="eav_attribute_label" + <constraint xsi:type="foreign" referenceId="EAV_ATTRIBUTE_LABEL_STORE_ID_STORE_STORE_ID" table="eav_attribute_label" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <index name="EAV_ATTRIBUTE_LABEL_STORE_ID" indexType="btree"> + <index referenceId="EAV_ATTRIBUTE_LABEL_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="EAV_ATTRIBUTE_LABEL_ATTRIBUTE_ID_STORE_ID" indexType="btree"> + <index referenceId="EAV_ATTRIBUTE_LABEL_ATTRIBUTE_ID_STORE_ID" indexType="btree"> <column name="attribute_id"/> <column name="store_id"/> </index> @@ -499,17 +499,17 @@ <column xsi:type="varchar" name="theme" nullable="true" length="64" comment="Theme"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Store Id"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="type_id"/> </constraint> - <constraint xsi:type="foreign" name="EAV_FORM_TYPE_STORE_ID_STORE_STORE_ID" table="eav_form_type" + <constraint xsi:type="foreign" referenceId="EAV_FORM_TYPE_STORE_ID_STORE_STORE_ID" table="eav_form_type" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="EAV_FORM_TYPE_CODE_THEME_STORE_ID"> + <constraint xsi:type="unique" referenceId="EAV_FORM_TYPE_CODE_THEME_STORE_ID"> <column name="code"/> <column name="theme"/> <column name="store_id"/> </constraint> - <index name="EAV_FORM_TYPE_STORE_ID" indexType="btree"> + <index referenceId="EAV_FORM_TYPE_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -518,17 +518,17 @@ comment="Type Id"/> <column xsi:type="smallint" name="entity_type_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Entity Type Id"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="type_id"/> <column name="entity_type_id"/> </constraint> - <constraint xsi:type="foreign" name="EAV_FORM_TYPE_ENTT_ENTT_TYPE_ID_EAV_ENTT_TYPE_ENTT_TYPE_ID" + <constraint xsi:type="foreign" referenceId="EAV_FORM_TYPE_ENTT_ENTT_TYPE_ID_EAV_ENTT_TYPE_ENTT_TYPE_ID" table="eav_form_type_entity" column="entity_type_id" referenceTable="eav_entity_type" referenceColumn="entity_type_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="EAV_FORM_TYPE_ENTITY_TYPE_ID_EAV_FORM_TYPE_TYPE_ID" + <constraint xsi:type="foreign" referenceId="EAV_FORM_TYPE_ENTITY_TYPE_ID_EAV_FORM_TYPE_TYPE_ID" table="eav_form_type_entity" column="type_id" referenceTable="eav_form_type" referenceColumn="type_id" onDelete="CASCADE"/> - <index name="EAV_FORM_TYPE_ENTITY_ENTITY_TYPE_ID" indexType="btree"> + <index referenceId="EAV_FORM_TYPE_ENTITY_ENTITY_TYPE_ID" indexType="btree"> <column name="entity_type_id"/> </index> </table> @@ -540,12 +540,12 @@ <column xsi:type="varchar" name="code" nullable="false" length="64" comment="Code"/> <column xsi:type="int" name="sort_order" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Sort Order"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="fieldset_id"/> </constraint> - <constraint xsi:type="foreign" name="EAV_FORM_FIELDSET_TYPE_ID_EAV_FORM_TYPE_TYPE_ID" table="eav_form_fieldset" + <constraint xsi:type="foreign" referenceId="EAV_FORM_FIELDSET_TYPE_ID_EAV_FORM_TYPE_TYPE_ID" table="eav_form_fieldset" column="type_id" referenceTable="eav_form_type" referenceColumn="type_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="EAV_FORM_FIELDSET_TYPE_ID_CODE"> + <constraint xsi:type="unique" referenceId="EAV_FORM_FIELDSET_TYPE_ID_CODE"> <column name="type_id"/> <column name="code"/> </constraint> @@ -556,17 +556,17 @@ <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Store Id"/> <column xsi:type="varchar" name="label" nullable="false" length="255" comment="Label"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="fieldset_id"/> <column name="store_id"/> </constraint> - <constraint xsi:type="foreign" name="EAV_FORM_FSET_LBL_FSET_ID_EAV_FORM_FSET_FSET_ID" + <constraint xsi:type="foreign" referenceId="EAV_FORM_FSET_LBL_FSET_ID_EAV_FORM_FSET_FSET_ID" table="eav_form_fieldset_label" column="fieldset_id" referenceTable="eav_form_fieldset" referenceColumn="fieldset_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="EAV_FORM_FIELDSET_LABEL_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="EAV_FORM_FIELDSET_LABEL_STORE_ID_STORE_STORE_ID" table="eav_form_fieldset_label" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <index name="EAV_FORM_FIELDSET_LABEL_STORE_ID" indexType="btree"> + <index referenceId="EAV_FORM_FIELDSET_LABEL_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -581,25 +581,25 @@ comment="Attribute Id"/> <column xsi:type="int" name="sort_order" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Sort Order"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="element_id"/> </constraint> - <constraint xsi:type="foreign" name="EAV_FORM_ELEMENT_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" + <constraint xsi:type="foreign" referenceId="EAV_FORM_ELEMENT_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" table="eav_form_element" column="attribute_id" referenceTable="eav_attribute" referenceColumn="attribute_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="EAV_FORM_ELEMENT_FIELDSET_ID_EAV_FORM_FIELDSET_FIELDSET_ID" + <constraint xsi:type="foreign" referenceId="EAV_FORM_ELEMENT_FIELDSET_ID_EAV_FORM_FIELDSET_FIELDSET_ID" table="eav_form_element" column="fieldset_id" referenceTable="eav_form_fieldset" referenceColumn="fieldset_id" onDelete="SET NULL"/> - <constraint xsi:type="foreign" name="EAV_FORM_ELEMENT_TYPE_ID_EAV_FORM_TYPE_TYPE_ID" table="eav_form_element" + <constraint xsi:type="foreign" referenceId="EAV_FORM_ELEMENT_TYPE_ID_EAV_FORM_TYPE_TYPE_ID" table="eav_form_element" column="type_id" referenceTable="eav_form_type" referenceColumn="type_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="EAV_FORM_ELEMENT_TYPE_ID_ATTRIBUTE_ID"> + <constraint xsi:type="unique" referenceId="EAV_FORM_ELEMENT_TYPE_ID_ATTRIBUTE_ID"> <column name="type_id"/> <column name="attribute_id"/> </constraint> - <index name="EAV_FORM_ELEMENT_FIELDSET_ID" indexType="btree"> + <index referenceId="EAV_FORM_ELEMENT_FIELDSET_ID" indexType="btree"> <column name="fieldset_id"/> </index> - <index name="EAV_FORM_ELEMENT_ATTRIBUTE_ID" indexType="btree"> + <index referenceId="EAV_FORM_ELEMENT_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> </table> diff --git a/app/code/Magento/Eav/etc/di.xml b/app/code/Magento/Eav/etc/di.xml index 8e897b979d2f..db6f9b0a64f9 100644 --- a/app/code/Magento/Eav/etc/di.xml +++ b/app/code/Magento/Eav/etc/di.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <preference for="Magento\Eav\Model\Entity\Setup\PropertyMapperInterface" type="Magento\Eav\Model\Entity\Setup\PropertyMapper\Composite" /> <preference for="Magento\Eav\Model\Entity\AttributeLoaderInterface" type="Magento\Eav\Model\Entity\AttributeLoader" /> + <preference for="Magento\Eav\Model\Entity\Attribute\UniqueValidationInterface" type="Magento\Eav\Model\Entity\Attribute\UniqueValidator" /> <preference for="Magento\Eav\Api\Data\AttributeInterface" type="Magento\Eav\Model\Entity\Attribute" /> <preference for="Magento\Eav\Api\AttributeRepositoryInterface" type="Magento\Eav\Model\AttributeRepository" /> <preference for="Magento\Eav\Api\Data\AttributeGroupInterface" type="Magento\Eav\Model\Entity\Attribute\Group" /> @@ -209,4 +210,3 @@ </arguments> </type> </config> - diff --git a/app/code/Magento/Eav/etc/module.xml b/app/code/Magento/Eav/etc/module.xml index 7b2b651b2d2f..97655bd4e1e3 100644 --- a/app/code/Magento/Eav/etc/module.xml +++ b/app/code/Magento/Eav/etc/module.xml @@ -9,6 +9,7 @@ <module name="Magento_Eav" > <sequence> <module name="Magento_Store"/> + <module name="Magento_Theme"/> </sequence> </module> </config> diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/AttributeOptions.php b/app/code/Magento/EavGraphQl/Model/Resolver/AttributeOptions.php index 6ccd610bead0..e4c27adc6024 100644 --- a/app/code/Magento/EavGraphQl/Model/Resolver/AttributeOptions.php +++ b/app/code/Magento/EavGraphQl/Model/Resolver/AttributeOptions.php @@ -9,6 +9,7 @@ use Magento\EavGraphQl\Model\Resolver\DataProvider\AttributeOptions as AttributeOptionsDataProvider; use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\StateException; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Exception\GraphQlInputException; @@ -46,7 +47,7 @@ public function __construct( } /** - * @inheritDoc + * @inheritdoc */ public function resolve( Field $field, @@ -66,34 +67,40 @@ public function resolve( } /** + * Get entity type + * * @param array $value * @return int - * @throws GraphQlInputException + * @throws LocalizedException */ private function getEntityType(array $value): int { if (!isset($value['entity_type'])) { - throw new GraphQlInputException(__('"Entity type should be specified')); + throw new LocalizedException(__('"Entity type should be specified')); } return (int)$value['entity_type']; } /** + * Get attribute code + * * @param array $value * @return string - * @throws GraphQlInputException + * @throws LocalizedException */ private function getAttributeCode(array $value): string { if (!isset($value['attribute_code'])) { - throw new GraphQlInputException(__('"Attribute code should be specified')); + throw new LocalizedException(__('"Attribute code should be specified')); } return $value['attribute_code']; } /** + * Get attribute options data + * * @param int $entityType * @param string $attributeCode * @return array diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/CustomAttributeMetadata.php b/app/code/Magento/EavGraphQl/Model/Resolver/CustomAttributeMetadata.php index 89d2ab9f9d05..62e3f0183661 100644 --- a/app/code/Magento/EavGraphQl/Model/Resolver/CustomAttributeMetadata.php +++ b/app/code/Magento/EavGraphQl/Model/Resolver/CustomAttributeMetadata.php @@ -14,8 +14,6 @@ use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; -use Magento\Framework\GraphQl\Query\Resolver\Value; -use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; /** @@ -28,20 +26,16 @@ class CustomAttributeMetadata implements ResolverInterface */ private $type; - private $valueFactory; - /** * @param Type $type - * @param ValueFactory $valueFactory */ - public function __construct(Type $type, ValueFactory $valueFactory) + public function __construct(Type $type) { $this->type = $type; - $this->valueFactory = $valueFactory; } /** - * {@inheritDoc} + * @inheritdoc */ public function resolve( Field $field, @@ -49,7 +43,7 @@ public function resolve( ResolveInfo $info, array $value = null, array $args = null - ) : Value { + ) { $attributes['items'] = null; $attributeInputs = $args['attributes']; foreach ($attributeInputs as $attribute) { @@ -88,11 +82,7 @@ public function resolve( ]; } - $result = function () use ($attributes) { - return $attributes; - }; - - return $this->valueFactory->create($result); + return $attributes; } /** diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/Query/Type.php b/app/code/Magento/EavGraphQl/Model/Resolver/Query/Type.php index cbea3a86bbdd..ef21a26f1f62 100644 --- a/app/code/Magento/EavGraphQl/Model/Resolver/Query/Type.php +++ b/app/code/Magento/EavGraphQl/Model/Resolver/Query/Type.php @@ -9,7 +9,6 @@ use Magento\Framework\Webapi\CustomAttributeTypeLocatorInterface; use Magento\Framework\Reflection\TypeProcessor; -use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Exception\GraphQlInputException; /** @@ -37,7 +36,7 @@ class Type /** * @param CustomAttributeTypeLocatorInterface $typeLocator * @param TypeProcessor $typeProcessor - * @param $customTypes + * @param array $customTypes */ public function __construct( CustomAttributeTypeLocatorInterface $typeLocator, @@ -71,12 +70,7 @@ public function getType(string $attributeCode, string $entityType) : string try { $type = $this->typeProcessor->translateTypeName($type); } catch (\InvalidArgumentException $exception) { - throw new GraphQlInputException( - __('Type %1 has no internal representation declared.', [$type]), - null, - 0, - false - ); + throw new GraphQlInputException(__('Cannot resolve EAV type')); } } else { $type = $type === 'double' ? 'float' : $type; diff --git a/app/code/Magento/EavGraphQl/etc/schema.graphqls b/app/code/Magento/EavGraphQl/etc/schema.graphqls index adada3030f50..0299067bd052 100644 --- a/app/code/Magento/EavGraphQl/etc/schema.graphqls +++ b/app/code/Magento/EavGraphQl/etc/schema.graphqls @@ -2,7 +2,7 @@ # See COPYING.txt for license details. type Query { - customAttributeMetadata(attributes: [AttributeInput!]!): CustomAttributeMetadata @resolver(class: "Magento\\EavGraphQl\\Model\\Resolver\\CustomAttributeMetadata") @doc(description: "The customAttributeMetadata query returns the attribute type, given an attribute code and entity type") + customAttributeMetadata(attributes: [AttributeInput!]!): CustomAttributeMetadata @resolver(class: "Magento\\EavGraphQl\\Model\\Resolver\\CustomAttributeMetadata") @doc(description: "The customAttributeMetadata query returns the attribute type, given an attribute code and entity type") @cache(cacheable: false) } type CustomAttributeMetadata @doc(description: "CustomAttributeMetadata defines an array of attribute_codes and entity_types") { diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/BatchDataMapper/CategoryFieldsProvider.php b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/BatchDataMapper/CategoryFieldsProvider.php index 009c07602fc2..eb7874a93614 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/BatchDataMapper/CategoryFieldsProvider.php +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/BatchDataMapper/CategoryFieldsProvider.php @@ -3,10 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Elasticsearch\Elasticsearch5\Model\Adapter\BatchDataMapper; use Magento\Elasticsearch\Model\ResourceModel\Index; use Magento\AdvancedSearch\Model\Adapter\DataMapper\AdditionalFieldsProviderInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; +use Magento\Framework\App\ObjectManager; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface; /** * Provide data mapping for categories fields @@ -18,16 +22,35 @@ class CategoryFieldsProvider implements AdditionalFieldsProviderInterface */ private $resourceIndex; + /** + * @var AttributeProvider + */ + private $attributeAdapterProvider; + + /** + * @var ResolverInterface + */ + private $fieldNameResolver; + /** * @param Index $resourceIndex + * @param AttributeProvider|null $attributeAdapterProvider + * @param ResolverInterface|null $fieldNameResolver */ - public function __construct(Index $resourceIndex) - { + public function __construct( + Index $resourceIndex, + AttributeProvider $attributeAdapterProvider = null, + ResolverInterface $fieldNameResolver = null + ) { $this->resourceIndex = $resourceIndex; + $this->attributeAdapterProvider = $attributeAdapterProvider ?: ObjectManager::getInstance() + ->get(AttributeProvider::class); + $this->fieldNameResolver = $fieldNameResolver ?: ObjectManager::getInstance() + ->get(ResolverInterface::class); } /** - * {@inheritdoc} + * @inheritdoc */ public function getFields(array $productIds, $storeId) { @@ -58,9 +81,19 @@ private function getProductCategoryData($productId, array $categoryIndexData) if (count($categoryIds)) { $result = ['category_ids' => $categoryIds]; + $positionAttribute = $this->attributeAdapterProvider->getByAttributeCode('position'); + $categoryNameAttribute = $this->attributeAdapterProvider->getByAttributeCode('category_name'); foreach ($indexData as $data) { - $result['position_category_' . $data['id']] = $data['position']; - $result['name_category_' . $data['id']] = $data['name']; + $categoryPositionKey = $this->fieldNameResolver->getFieldName( + $positionAttribute, + ['categoryId' => $data['id']] + ); + $categoryNameKey = $this->fieldNameResolver->getFieldName( + $categoryNameAttribute, + ['categoryId' => $data['id']] + ); + $result[$categoryPositionKey] = $data['position']; + $result[$categoryNameKey] = $data['name']; } } } diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/DataMapper/ProductDataMapper.php b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/DataMapper/ProductDataMapper.php index 659df0f8447f..f0b738039723 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/DataMapper/ProductDataMapper.php +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/DataMapper/ProductDataMapper.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Elasticsearch\Elasticsearch5\Model\Adapter\DataMapper; use Magento\Catalog\Model\ResourceModel\Eav\Attribute; @@ -14,8 +15,13 @@ use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface; use Magento\Elasticsearch\Model\Adapter\DataMapperInterface; use Magento\Elasticsearch\Model\Adapter\FieldType\Date as DateFieldType; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; +use Magento\Framework\App\ObjectManager; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface; /** + * Don't use this product data mapper class. + * * @deprecated 100.2.0 * @see \Magento\Elasticsearch\Model\Adapter\BatchDataMapperInterface */ @@ -78,6 +84,16 @@ class ProductDataMapper implements DataMapperInterface */ protected $mediaGalleryRoles; + /** + * @var AttributeProvider + */ + private $attributeAdapterProvider; + + /** + * @var ResolverInterface + */ + private $fieldNameResolver; + /** * Construction for DocumentDataMapper * @@ -87,6 +103,8 @@ class ProductDataMapper implements DataMapperInterface * @param FieldMapperInterface $fieldMapper * @param StoreManagerInterface $storeManager * @param DateFieldType $dateFieldType + * @param AttributeProvider|null $attributeAdapterProvider + * @param ResolverInterface|null $fieldNameResolver */ public function __construct( Builder $builder, @@ -94,7 +112,9 @@ public function __construct( Index $resourceIndex, FieldMapperInterface $fieldMapper, StoreManagerInterface $storeManager, - DateFieldType $dateFieldType + DateFieldType $dateFieldType, + AttributeProvider $attributeAdapterProvider = null, + ResolverInterface $fieldNameResolver = null ) { $this->builder = $builder; $this->attributeContainer = $attributeContainer; @@ -102,6 +122,10 @@ public function __construct( $this->fieldMapper = $fieldMapper; $this->storeManager = $storeManager; $this->dateFieldType = $dateFieldType; + $this->attributeAdapterProvider = $attributeAdapterProvider ?: ObjectManager::getInstance() + ->get(AttributeProvider::class); + $this->fieldNameResolver = $fieldNameResolver ?: ObjectManager::getInstance() + ->get(ResolverInterface::class); $this->mediaGalleryRoles = [ self::MEDIA_ROLE_IMAGE, @@ -203,6 +227,8 @@ protected function processAdvancedAttributes($productId, array $productIndexData } /** + * Check value. + * * @param mixed $value * @param Attribute $attribute * @param string $storeId @@ -232,14 +258,14 @@ protected function getProductTierPriceData($data) if (!empty($data)) { $i = 0; foreach ($data as $tierPrice) { - $result['tier_price_id_'.$i] = $tierPrice['price_id']; - $result['tier_website_id_'.$i] = $tierPrice['website_id']; - $result['tier_all_groups_'.$i] = $tierPrice['all_groups']; - $result['tier_cust_group_'.$i] = $tierPrice['cust_group'] == GroupInterface::CUST_GROUP_ALL + $result['tier_price_id_' . $i] = $tierPrice['price_id']; + $result['tier_website_id_' . $i] = $tierPrice['website_id']; + $result['tier_all_groups_' . $i] = $tierPrice['all_groups']; + $result['tier_cust_group_' . $i] = $tierPrice['cust_group'] == GroupInterface::CUST_GROUP_ALL ? '' : $tierPrice['cust_group']; - $result['tier_price_qty_'.$i] = $tierPrice['price_qty']; - $result['tier_website_price_'.$i] = $tierPrice['website_price']; - $result['tier_price_'.$i] = $tierPrice['price']; + $result['tier_price_qty_' . $i] = $tierPrice['price_qty']; + $result['tier_website_price_' . $i] = $tierPrice['website_price']; + $result['tier_price_' . $i] = $tierPrice['price']; $i++; } } @@ -293,6 +319,8 @@ protected function getProductMediaGalleryData($media, $roles) } /** + * Get media role image. + * * @param string $file * @param array $roles * @return string @@ -303,6 +331,8 @@ protected function getMediaRoleImage($file, $roles) } /** + * Get media role small image. + * * @param string $file * @param array $roles * @return string @@ -313,6 +343,8 @@ protected function getMediaRoleSmallImage($file, $roles) } /** + * Get media role thumbnail. + * * @param string $file * @param array $roles * @return string @@ -323,6 +355,8 @@ protected function getMediaRoleThumbnail($file, $roles) } /** + * Get media role swatch image. + * * @param string $file * @param array $roles * @return string @@ -364,9 +398,11 @@ protected function getProductPriceData($productId, $storeId, array $priceIndexDa $result = []; if (array_key_exists($productId, $priceIndexData)) { $productPriceIndexData = $priceIndexData[$productId]; - $websiteId = $this->storeManager->getStore($storeId)->getWebsiteId(); foreach ($productPriceIndexData as $customerGroupId => $price) { - $fieldName = 'price_' . $customerGroupId . '_' . $websiteId; + $fieldName = $this->fieldMapper->getFieldName( + 'price', + ['customerGroupId' => $customerGroupId, 'websiteId' => $storeId] + ); $result[$fieldName] = sprintf('%F', $price); } } @@ -393,13 +429,23 @@ protected function getProductCategoryData($productId, array $categoryIndexData) if (array_key_exists($productId, $categoryIndexData)) { $indexData = $categoryIndexData[$productId]; foreach ($indexData as $categoryData) { - $categoryIds[] = (int) $categoryData['id']; + $categoryIds[] = (int)$categoryData['id']; } if (count($categoryIds)) { $result = ['category_ids' => implode(' ', $categoryIds)]; + $positionAttribute = $this->attributeAdapterProvider->getByAttributeCode('position'); + $categoryNameAttribute = $this->attributeAdapterProvider->getByAttributeCode('category_name'); foreach ($indexData as $data) { - $result['position_category_' . $data['id']] = $data['position']; - $result['name_category_' . $data['id']] = $data['name']; + $categoryPositionKey = $this->fieldNameResolver->getFieldName( + $positionAttribute, + ['categoryId' => $data['id']] + ); + $categoryNameKey = $this->fieldNameResolver->getFieldName( + $categoryNameAttribute, + ['categoryId' => $data['id']] + ); + $result[$categoryPositionKey] = $data['position']; + $result[$categoryNameKey] = $data['name']; } } } 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 new file mode 100644 index 000000000000..26173fcf29b0 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/Converter.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\ConverterInterface; + +/** + * Field type converter from internal index type to elastic service. + */ +class Converter implements ConverterInterface +{ + /** + * Text flags for Elasticsearch index value + */ + 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 = [ + ConverterInterface::INTERNAL_NO_INDEX_VALUE => self::ES_NO_INDEX, + ConverterInterface::INTERNAL_NO_ANALYZE_VALUE => self::ES_NO_ANALYZE, + ]; + + /** + * Get service field index type for elasticsearch 5. + * + * @param string $internalType + * @return string|boolean + * @throws \DomainException + */ + public function convert(string $internalType) + { + if (!isset($this->mapping[$internalType])) { + throw new \DomainException(sprintf('Unsupported internal field index type: %s', $internalType)); + } + return $this->mapping[$internalType]; + } +} diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/IndexResolver.php b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/IndexResolver.php new file mode 100644 index 000000000000..954deaec639e --- /dev/null +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/IndexResolver.php @@ -0,0 +1,87 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\ResolverInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\ConverterInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface + as FieldTypeConverterInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ResolverInterface + as FieldTypeResolver; + +/** + * Field index resolver that provides index type for the attribute in mapping. + * For example, we need to set ‘no’/false in the case when attribute must be present in index data, + * but stay as not indexable. + */ +class IndexResolver implements ResolverInterface +{ + /** + * @var ConverterInterface + */ + private $converter; + + /** + * @var FieldTypeConverterInterface + */ + private $fieldTypeConverter; + + /** + * @var FieldTypeResolver + */ + private $fieldTypeResolver; + + /** + * @param ConverterInterface $converter + * @param FieldTypeConverterInterface $fieldTypeConverter + * @param FieldTypeResolver $fieldTypeResolver + */ + public function __construct( + ConverterInterface $converter, + FieldTypeConverterInterface $fieldTypeConverter, + FieldTypeResolver $fieldTypeResolver + ) { + $this->converter = $converter; + $this->fieldTypeConverter = $fieldTypeConverter; + $this->fieldTypeResolver = $fieldTypeResolver; + } + + /** + * @inheritdoc + */ + public function getFieldIndex(AttributeAdapter $attribute) + { + $index = null; + if (!$attribute->isSearchable() + && !$attribute->isAlwaysIndexable() + && ($this->isStringServiceFieldType($attribute) || $attribute->isComplexType()) + && !(($attribute->isIntegerType() || $attribute->isBooleanType()) + && !$attribute->isUserDefined()) + && !$attribute->isFloatType() + ) { + $index = $this->converter->convert(ConverterInterface::INTERNAL_NO_INDEX_VALUE); + } + + return $index; + } + + /** + * Check if service field type for field set as 'string' + * + * @param AttributeAdapter $attribute + * @return bool + */ + private function isStringServiceFieldType(AttributeAdapter $attribute): bool + { + $serviceFieldType = $this->fieldTypeResolver->getFieldType($attribute); + $stringTypeKey = $this->fieldTypeConverter->convert(FieldTypeConverterInterface::INTERNAL_DATA_TYPE_STRING); + + return $serviceFieldType === $stringTypeKey; + } +} diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php new file mode 100644 index 000000000000..1f6e05c9e02f --- /dev/null +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface; + +/** + * Field type converter from internal data types to elastic service. + */ +class Converter implements ConverterInterface +{ + /**#@+ + * Text flags for Elasticsearch field types + */ + private const ES_DATA_TYPE_TEXT = 'text'; + private const ES_DATA_TYPE_KEYWORD = 'keyword'; + private const ES_DATA_TYPE_FLOAT = 'float'; + private const ES_DATA_TYPE_INT = 'integer'; + private const ES_DATA_TYPE_DATE = 'date'; + /**#@-*/ + + /** + * Mapping between internal data types and elastic service. + * + * @var array + */ + private $mapping = [ + self::INTERNAL_DATA_TYPE_STRING => self::ES_DATA_TYPE_TEXT, + self::INTERNAL_DATA_TYPE_KEYWORD => self::ES_DATA_TYPE_KEYWORD, + self::INTERNAL_DATA_TYPE_FLOAT => self::ES_DATA_TYPE_FLOAT, + self::INTERNAL_DATA_TYPE_INT => self::ES_DATA_TYPE_INT, + self::INTERNAL_DATA_TYPE_DATE => self::ES_DATA_TYPE_DATE, + ]; + + /** + * Get service field type for elasticsearch 5. + * + * @param string $internalType + * @return string + * @throws \DomainException + */ + public function convert(string $internalType): string + { + if (!isset($this->mapping[$internalType])) { + throw new \DomainException(sprintf('Unsupported internal field type: %s', $internalType)); + } + return $this->mapping[$internalType]; + } +} diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/CompositeResolver.php b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/CompositeResolver.php new file mode 100644 index 000000000000..fed36ff6b1c8 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/CompositeResolver.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ResolverInterface; + +/** + * Composite resolver for resolving field type. + */ +class CompositeResolver implements ResolverInterface +{ + /** + * @var ResolverInterface[] + */ + private $items; + + /** + * @param ResolverInterface[] $items + */ + public function __construct(array $items) + { + foreach ($items as $item) { + if (!$item instanceof ResolverInterface) { + throw new \InvalidArgumentException( + sprintf('Instance of the field type resolver is expected, got %s instead.', get_class($item)) + ); + } + } + $this->items = $items; + } + + /** + * Get field type. + * + * @param AttributeAdapter $attribute + * @return string + */ + public function getFieldType(AttributeAdapter $attribute): ?string + { + $result = null; + foreach ($this->items as $item) { + $result = $item->getFieldType($attribute); + if (null !== $result) { + break; + } + } + + return $result; + } +} diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerType.php b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerType.php new file mode 100644 index 000000000000..e5c8dd48c7af --- /dev/null +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerType.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ResolverInterface; + +/** + * Integer type resolver. + */ +class IntegerType implements ResolverInterface +{ + /** + * @var ConverterInterface + */ + private $fieldTypeConverter; + + /** + * @var array + */ + private $integerTypeAttributes; + + /** + * @param ConverterInterface $fieldTypeConverter + * @param array $integerTypeAttributes + */ + public function __construct(ConverterInterface $fieldTypeConverter, $integerTypeAttributes = ['category_ids']) + { + $this->fieldTypeConverter = $fieldTypeConverter; + $this->integerTypeAttributes = $integerTypeAttributes; + } + + /** + * Get integer field type. + * + * @param AttributeAdapter $attribute + * @return string + */ + public function getFieldType(AttributeAdapter $attribute): ?string + { + if (in_array($attribute->getAttributeCode(), $this->integerTypeAttributes, true) + || (($attribute->isIntegerType() || $attribute->isBooleanType()) + && !$attribute->isUserDefined()) + ) { + return $this->fieldTypeConverter->convert(ConverterInterface::INTERNAL_DATA_TYPE_INT); + } + + return null; + } +} diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/KeywordType.php b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/KeywordType.php new file mode 100644 index 000000000000..e522d4ae5e07 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/KeywordType.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ResolverInterface; + +/** + * Keyword type resolver. + */ +class KeywordType implements ResolverInterface +{ + /** + * @var ConverterInterface + */ + private $fieldTypeConverter; + + /** + * @param ConverterInterface $fieldTypeConverter + */ + public function __construct(ConverterInterface $fieldTypeConverter) + { + $this->fieldTypeConverter = $fieldTypeConverter; + } + + /** + * Get field type. + * + * @param AttributeAdapter $attribute + * @return string + */ + public function getFieldType(AttributeAdapter $attribute): ?string + { + if ($attribute->isComplexType() + || (!$attribute->isSearchable() && !$attribute->isAlwaysIndexable() && $attribute->isFilterable()) + ) { + return $this->fieldTypeConverter->convert(ConverterInterface::INTERNAL_DATA_TYPE_KEYWORD); + } + + return null; + } +} diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/ProductFieldMapper.php b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/ProductFieldMapper.php index 09357216d206..5aea87e5e6ae 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/ProductFieldMapper.php +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/ProductFieldMapper.php @@ -3,11 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper; -use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Eav\Model\Config; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProviderInterface; use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface; +use Magento\Framework\App\ObjectManager; use Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldType; use Magento\Framework\Registry; use Magento\Store\Model\StoreManagerInterface as StoreManager; @@ -19,222 +23,104 @@ class ProductFieldMapper implements FieldMapperInterface { /** + * @deprecated * @var Config */ protected $eavConfig; /** + * @deprecated * @var FieldType */ protected $fieldType; /** + * @deprecated * @var CustomerSession */ protected $customerSession; /** - * Store manager - * + * @deprecated * @var StoreManager */ protected $storeManager; /** - * Core registry - * + * @deprecated * @var Registry */ protected $coreRegistry; + /** + * @var AttributeProvider + */ + private $attributeAdapterProvider; + + /** + * @var ResolverInterface + */ + private $fieldNameResolver; + + /** + * @var FieldProviderInterface + */ + private $fieldProvider; + /** * @param Config $eavConfig * @param FieldType $fieldType * @param CustomerSession $customerSession * @param StoreManager $storeManager * @param Registry $coreRegistry + * @param ResolverInterface|null $fieldNameResolver + * @param AttributeProvider|null $attributeAdapterProvider + * @param FieldProviderInterface|null $fieldProvider */ public function __construct( Config $eavConfig, FieldType $fieldType, CustomerSession $customerSession, StoreManager $storeManager, - Registry $coreRegistry + Registry $coreRegistry, + ResolverInterface $fieldNameResolver = null, + AttributeProvider $attributeAdapterProvider = null, + FieldProviderInterface $fieldProvider = null ) { $this->eavConfig = $eavConfig; $this->fieldType = $fieldType; $this->customerSession = $customerSession; $this->storeManager = $storeManager; $this->coreRegistry = $coreRegistry; + $this->fieldNameResolver = $fieldNameResolver ?: ObjectManager::getInstance() + ->get(ResolverInterface::class); + $this->attributeAdapterProvider = $attributeAdapterProvider ?: ObjectManager::getInstance() + ->get(AttributeProvider::class); + $this->fieldProvider = $fieldProvider ?: ObjectManager::getInstance() + ->get(FieldProviderInterface::class); } /** + * Get field name. + * * @param string $attributeCode * @param array $context * @return string */ public function getFieldName($attributeCode, $context = []) { - $attribute = $this->eavConfig->getAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, $attributeCode); - if (!$attribute || in_array($attributeCode, ['id', 'sku', 'store_id', 'visibility'], true)) { - return $attributeCode; - } - if ($attributeCode === 'price') { - return $this->getPriceFieldName($context); - } - if ($attributeCode === 'position') { - return $this->getPositionFiledName($context); - } - $fieldType = $this->fieldType->getFieldType($attribute); - $frontendInput = $attribute->getFrontendInput(); - if (empty($context['type'])) { - $fieldName = $attributeCode; - } elseif ($context['type'] === FieldMapperInterface::TYPE_FILTER) { - if ($fieldType === FieldType::ES_DATA_TYPE_TEXT) { - return $this->getFieldName( - $attributeCode, - array_merge($context, ['type' => FieldMapperInterface::TYPE_QUERY]) - ); - } - $fieldName = $attributeCode; - } elseif ($context['type'] === FieldMapperInterface::TYPE_QUERY) { - $fieldName = $this->getQueryTypeFieldName($frontendInput, $fieldType, $attributeCode); - } else { - $fieldName = 'sort_' . $attributeCode; - } - - return $fieldName; + $attributeAdapter = $this->attributeAdapterProvider->getByAttributeCode($attributeCode); + return $this->fieldNameResolver->getFieldName($attributeAdapter, $context); } /** + * Get all attributes types. + * * @param array $context * @return array - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function getAllAttributesTypes($context = []) { - $attributeCodes = $this->eavConfig->getEntityAttributeCodes(ProductAttributeInterface::ENTITY_TYPE_CODE); - $allAttributes = []; - // List of attributes which are required to be indexable - $alwaysIndexableAttributes = [ - 'category_ids', - 'visibility', - ]; - - foreach ($attributeCodes as $attributeCode) { - $attribute = $this->eavConfig->getAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, $attributeCode); - - $allAttributes[$attributeCode] = [ - 'type' => $this->fieldType->getFieldType($attribute), - ]; - - if (!$attribute->getIsSearchable() && !$this->isAttributeUsedInAdvancedSearch($attribute) - && !in_array($attributeCode, $alwaysIndexableAttributes, true) - ) { - if ($attribute->getIsFilterable() || $attribute->getIsFilterableInSearch()) { - $allAttributes[$attributeCode]['type'] = FieldType::ES_DATA_TYPE_KEYWORD; - } else if ($allAttributes[$attributeCode]['type'] === FieldType::ES_DATA_TYPE_TEXT) { - $allAttributes[$attributeCode]['index'] = 'no'; - } - } else if ($attributeCode == "category_ids") { - $allAttributes[$attributeCode] = [ - 'type' => FieldType::ES_DATA_TYPE_INT, - ]; - } - - if ($attribute->usesSource() - || $attribute->getFrontendInput() === 'select' - || $attribute->getFrontendInput() === 'multiselect' - ) { - $allAttributes[$attributeCode]['type'] = FieldType::ES_DATA_TYPE_KEYWORD; - - $allAttributes[$attributeCode . '_value'] = [ - 'type' => FieldType::ES_DATA_TYPE_TEXT, - ]; - } - } - - return $allAttributes; - } - - /** - * @param Object $attribute - * @return bool - */ - protected function isAttributeUsedInAdvancedSearch($attribute) - { - return $attribute->getIsVisibleInAdvancedSearch() - || $attribute->getIsFilterable() - || $attribute->getIsFilterableInSearch(); - } - - /** - * @param string $frontendInput - * @param string $fieldType - * @param string $attributeCode - * @return string - */ - protected function getRefinedFieldName($frontendInput, $fieldType, $attributeCode) - { - switch ($frontendInput) { - case 'select': - return in_array($fieldType, ['text','integer'], true) ? $attributeCode . '_value' : $attributeCode; - case 'boolean': - return $fieldType === 'integer' ? $attributeCode . '_value' : $attributeCode; - default: - return $attributeCode; - } - } - - /** - * @param string $frontendInput - * @param string $fieldType - * @param string $attributeCode - * @return string - */ - protected function getQueryTypeFieldName($frontendInput, $fieldType, $attributeCode) - { - if ($attributeCode === '*') { - $fieldName = '_all'; - } else { - $fieldName = $this->getRefinedFieldName($frontendInput, $fieldType, $attributeCode); - } - return $fieldName; - } - - /** - * Get "position" field name - * - * @param array $context - * @return string - */ - protected function getPositionFiledName($context) - { - if (isset($context['categoryId'])) { - $category = $context['categoryId']; - } else { - $category = $this->coreRegistry->registry('current_category') - ? $this->coreRegistry->registry('current_category')->getId() - : $this->storeManager->getStore()->getRootCategoryId(); - } - return 'position_category_' . $category; - } - - /** - * Prepare price field name for search engine - * - * @param array $context - * @return string - */ - protected function getPriceFieldName($context) - { - $customerGroupId = !empty($context['customerGroupId']) - ? $context['customerGroupId'] - : $this->customerSession->getCustomerGroupId(); - $websiteId = !empty($context['websiteId']) - ? $context['websiteId'] - : $this->storeManager->getStore()->getWebsiteId(); - return 'price_' . $customerGroupId . '_' . $websiteId; + return $this->fieldProvider->getFields($context); } } diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldType.php b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldType.php index d11603295453..6891b8c69362 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldType.php +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldType.php @@ -6,15 +6,23 @@ namespace Magento\Elasticsearch\Elasticsearch5\Model\Adapter; use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ResolverInterface; /** * Class FieldType + * * @api * @since 100.1.0 + * + * @deprecated This class provide not full data about field type. Only basic rules apply on this class. + * @see ResolverInterface */ class FieldType { /**#@+ + * @deprecated + * @see \Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface + * * Text flags for Elasticsearch field types */ const ES_DATA_TYPE_TEXT = 'text'; @@ -28,16 +36,22 @@ class FieldType /**#@-*/ /** + * Get field type. + * + * @deprecated + * @see ResolverInterface::getFieldType + * * @param AbstractAttribute $attribute * @return string * @since 100.1.0 */ public function getFieldType($attribute) { + trigger_error('Class is deprecated', E_USER_DEPRECATED); $backendType = $attribute->getBackendType(); $frontendInput = $attribute->getFrontendInput(); - if (in_array($backendType, ['timestamp', 'datetime'], true)) { + if ($backendType === 'timestamp') { $fieldType = self::ES_DATA_TYPE_DATE; } elseif ((in_array($backendType, ['int', 'smallint'], true) || (in_array($frontendInput, ['select', 'boolean'], true) && $backendType !== 'varchar')) diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php index 8ffa5839a125..c05e8a441604 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php @@ -101,6 +101,8 @@ public function testConnection() } /** + * Build config. + * * @param array $options * @return array */ @@ -207,9 +209,10 @@ public function indexExists($index) } /** + * Exists alias. + * * @param string $alias * @param string $index - * * @return bool */ public function existsAlias($alias, $index = '') @@ -222,8 +225,9 @@ public function existsAlias($alias, $index = '') } /** - * @param string $alias + * Get alias. * + * @param string $alias * @return array */ public function getAlias($alias) @@ -268,7 +272,7 @@ public function addFieldsMapping(array $fields, $index, $entityType) 'match_mapping_type' => 'string', 'mapping' => $this->prepareFieldInfo([ 'type' => 'text', - 'index' => 'no', + 'index' => false, ]), ], ], @@ -293,8 +297,7 @@ public function addFieldsMapping(array $fields, $index, $entityType) } /** - * Fix backward compatibility of field definition. - * Allow to run both 2.x and 5.x servers. + * Fix backward compatibility of field definition. Allow to run both 2.x and 5.x servers. * * @param array $fieldInfo * @@ -346,8 +349,7 @@ public function query($query) } /** - * Fix backward compatibility of the search queries. - * Allow to run both 2.x and 5.x servers. + * Fix backward compatibility of the search queries. Allow to run both 2.x and 5.x servers. * * @param array $query * diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Adapter.php b/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Adapter.php index 13d4f8a7296c..0ae347d5791a 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Adapter.php +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Adapter.php @@ -12,6 +12,7 @@ use Magento\Elasticsearch\SearchAdapter\Aggregation\Builder as AggregationBuilder; use Magento\Elasticsearch\SearchAdapter\ConnectionManager; use \Magento\Elasticsearch\SearchAdapter\ResponseFactory; +use Psr\Log\LoggerInterface; /** * Elasticsearch Search Adapter @@ -47,28 +48,60 @@ class Adapter implements AdapterInterface */ private $queryContainerFactory; + /** + * Empty response from Elasticsearch. + * + * @var array + */ + private static $emptyRawResponse = [ + "hits" => + [ + "hits" => [] + ], + "aggregations" => + [ + "price_bucket" => [], + "category_bucket" => + [ + "buckets" => [] + + ] + ] + ]; + + /** + * @var LoggerInterface + */ + private $logger; + /** * @param ConnectionManager $connectionManager * @param Mapper $mapper * @param ResponseFactory $responseFactory * @param AggregationBuilder $aggregationBuilder * @param \Magento\Elasticsearch\SearchAdapter\QueryContainerFactory $queryContainerFactory + * @param LoggerInterface $logger */ public function __construct( ConnectionManager $connectionManager, Mapper $mapper, ResponseFactory $responseFactory, AggregationBuilder $aggregationBuilder, - \Magento\Elasticsearch\SearchAdapter\QueryContainerFactory $queryContainerFactory + \Magento\Elasticsearch\SearchAdapter\QueryContainerFactory $queryContainerFactory, + LoggerInterface $logger = null ) { $this->connectionManager = $connectionManager; $this->mapper = $mapper; $this->responseFactory = $responseFactory; $this->aggregationBuilder = $aggregationBuilder; $this->queryContainerFactory = $queryContainerFactory; + $this->logger = $logger ?: ObjectManager::getInstance() + ->get(LoggerInterface::class); } /** + * Search query + * * @param RequestInterface $request * @return QueryResponse */ @@ -76,17 +109,23 @@ public function query(RequestInterface $request) { $client = $this->connectionManager->getConnection(); $aggregationBuilder = $this->aggregationBuilder; - $query = $this->mapper->buildQuery($request); $aggregationBuilder->setQuery($this->queryContainerFactory->create(['query' => $query])); - $rawResponse = $client->query($query); - $rawDocuments = isset($rawResponse['hits']['hits']) ? $rawResponse['hits']['hits'] : []; + try { + $rawResponse = $client->query($query); + } catch (\Exception $e) { + $this->logger->critical($e); + // return empty search result in case an exception is thrown from Elasticsearch + $rawResponse = self::$emptyRawResponse; + } + $rawDocuments = isset($rawResponse['hits']['hits']) ? $rawResponse['hits']['hits'] : []; $queryResponse = $this->responseFactory->create( [ '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 db961d86962e..09968db00aa2 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Query/Builder.php +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Query/Builder.php @@ -3,8 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Query; +use Magento\Elasticsearch\SearchAdapter\Query\Builder\Sort; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Search\RequestInterface; use Magento\Elasticsearch\Model\Config; use Magento\Elasticsearch\SearchAdapter\SearchIndexNameResolver; @@ -12,6 +15,8 @@ use Magento\Framework\App\ScopeResolverInterface; /** + * Query builder for search adapter. + * * @api * @since 100.1.0 */ @@ -41,22 +46,30 @@ class Builder */ protected $scopeResolver; + /** + * @var Sort + */ + protected $sortBuilder; + /** * @param Config $clientConfig * @param SearchIndexNameResolver $searchIndexNameResolver * @param AggregationBuilder $aggregationBuilder * @param ScopeResolverInterface $scopeResolver + * @param Sort|null $sortBuilder */ public function __construct( Config $clientConfig, SearchIndexNameResolver $searchIndexNameResolver, AggregationBuilder $aggregationBuilder, - ScopeResolverInterface $scopeResolver + ScopeResolverInterface $scopeResolver, + Sort $sortBuilder = null ) { $this->clientConfig = $clientConfig; $this->searchIndexNameResolver = $searchIndexNameResolver; $this->aggregationBuilder = $aggregationBuilder; $this->scopeResolver = $scopeResolver; + $this->sortBuilder = $sortBuilder ?: ObjectManager::getInstance()->get(Sort::class); } /** @@ -70,6 +83,7 @@ public function initQuery(RequestInterface $request) { $dimension = current($request->getDimensions()); $storeId = $this->scopeResolver->getScope($dimension->getValue())->getId(); + $searchQuery = [ 'index' => $this->searchIndexNameResolver->getIndexName($storeId, $request->getIndex()), 'type' => $this->clientConfig->getEntityType(), @@ -77,6 +91,7 @@ public function initQuery(RequestInterface $request) 'from' => $request->getFrom(), 'size' => $request->getSize(), 'stored_fields' => ['_id', '_score'], + 'sort' => $this->sortBuilder->getSort($request), 'query' => [], ], ]; diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/CategoryFieldsProvider.php b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/CategoryFieldsProvider.php index e2de4aec717a..0e130c24e79d 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/CategoryFieldsProvider.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/CategoryFieldsProvider.php @@ -3,10 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Elasticsearch\Model\Adapter\BatchDataMapper; use Magento\Elasticsearch\Model\ResourceModel\Index; use Magento\AdvancedSearch\Model\Adapter\DataMapper\AdditionalFieldsProviderInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; +use Magento\Framework\App\ObjectManager; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface; /** * Provide data mapping for categories fields @@ -18,16 +22,35 @@ class CategoryFieldsProvider implements AdditionalFieldsProviderInterface */ private $resourceIndex; + /** + * @var AttributeProvider + */ + private $attributeAdapterProvider; + + /** + * @var ResolverInterface + */ + private $fieldNameResolver; + /** * @param Index $resourceIndex + * @param AttributeProvider|null $attributeAdapterProvider + * @param ResolverInterface|null $fieldNameResolver */ - public function __construct(Index $resourceIndex) - { + public function __construct( + Index $resourceIndex, + AttributeProvider $attributeAdapterProvider = null, + ResolverInterface $fieldNameResolver = null + ) { $this->resourceIndex = $resourceIndex; + $this->attributeAdapterProvider = $attributeAdapterProvider ?: ObjectManager::getInstance() + ->get(AttributeProvider::class); + $this->fieldNameResolver = $fieldNameResolver ?: ObjectManager::getInstance() + ->get(ResolverInterface::class); } /** - * {@inheritdoc} + * @inheritdoc */ public function getFields(array $productIds, $storeId) { @@ -58,9 +81,19 @@ private function getProductCategoryData($productId, array $categoryIndexData) if (count($categoryIds)) { $result = ['category_ids' => implode(' ', $categoryIds)]; + $positionAttribute = $this->attributeAdapterProvider->getByAttributeCode('position'); + $categoryNameAttribute = $this->attributeAdapterProvider->getByAttributeCode('category_name'); foreach ($indexData as $data) { - $result['position_category_' . $data['id']] = $data['position']; - $result['name_category_' . $data['id']] = $data['name']; + $categoryPositionKey = $this->fieldNameResolver->getFieldName( + $positionAttribute, + ['categoryId' => $data['id']] + ); + $categoryNameKey = $this->fieldNameResolver->getFieldName( + $categoryNameAttribute, + ['categoryId' => $data['id']] + ); + $result[$categoryPositionKey] = $data['position']; + $result[$categoryNameKey] = $data['name']; } } } diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/PriceFieldsProvider.php b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/PriceFieldsProvider.php index f5e8a23525a0..56c84593256b 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/PriceFieldsProvider.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/PriceFieldsProvider.php @@ -3,12 +3,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Elasticsearch\Model\Adapter\BatchDataMapper; use Magento\Elasticsearch\Model\ResourceModel\Index; use Magento\Store\Model\StoreManagerInterface; use Magento\AdvancedSearch\Model\Adapter\DataMapper\AdditionalFieldsProviderInterface; use Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; +use Magento\Framework\App\ObjectManager; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface; /** * Provide data mapping for price fields @@ -30,33 +34,53 @@ class PriceFieldsProvider implements AdditionalFieldsProviderInterface */ private $storeManager; + /** + * @var AttributeProvider + */ + private $attributeAdapterProvider; + + /** + * @var ResolverInterface + */ + private $fieldNameResolver; + /** * @param Index $resourceIndex * @param DataProvider $dataProvider * @param StoreManagerInterface $storeManager + * @param AttributeProvider|null $attributeAdapterProvider + * @param ResolverInterface|null $fieldNameResolver */ public function __construct( Index $resourceIndex, DataProvider $dataProvider, - StoreManagerInterface $storeManager + StoreManagerInterface $storeManager, + AttributeProvider $attributeAdapterProvider = null, + ResolverInterface $fieldNameResolver = null ) { $this->resourceIndex = $resourceIndex; $this->dataProvider = $dataProvider; $this->storeManager = $storeManager; + $this->attributeAdapterProvider = $attributeAdapterProvider ?: ObjectManager::getInstance() + ->get(AttributeProvider::class); + $this->fieldNameResolver = $fieldNameResolver ?: ObjectManager::getInstance() + ->get(ResolverInterface::class); } /** - * {@inheritdoc} + * @inheritdoc */ public function getFields(array $productIds, $storeId) { + $websiteId = $this->storeManager->getStore($storeId)->getWebsiteId(); + $priceData = $this->dataProvider->getSearchableAttribute('price') ? $this->resourceIndex->getPriceIndexData($productIds, $storeId) : []; $fields = []; foreach ($productIds as $productId) { - $fields[$productId] = $this->getProductPriceData($productId, $storeId, $priceData); + $fields[$productId] = $this->getProductPriceData($productId, $websiteId, $priceData); } return $fields; @@ -75,8 +99,12 @@ private function getProductPriceData($productId, $websiteId, array $priceIndexDa $result = []; if (array_key_exists($productId, $priceIndexData)) { $productPriceIndexData = $priceIndexData[$productId]; + $priceAttribute = $this->attributeAdapterProvider->getByAttributeCode('price'); foreach ($productPriceIndexData as $customerGroupId => $price) { - $fieldName = 'price_' . $customerGroupId . '_' . $websiteId; + $fieldName = $this->fieldNameResolver->getFieldName( + $priceAttribute, + ['customerGroupId' => $customerGroupId, 'websiteId' => $websiteId] + ); $result[$fieldName] = sprintf('%F', $price); } } diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php index 4d329f212dd8..270ca37e2d42 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php @@ -6,18 +6,24 @@ namespace Magento\Elasticsearch\Model\Adapter\BatchDataMapper; use Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider; -use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Eav\Model\Entity\Attribute; use Magento\Elasticsearch\Model\Adapter\Document\Builder; use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface; use Magento\Elasticsearch\Model\Adapter\BatchDataMapperInterface; use Magento\Elasticsearch\Model\Adapter\FieldType\Date as DateFieldType; use Magento\AdvancedSearch\Model\Adapter\DataMapper\AdditionalFieldsProviderInterface; +use Magento\Eav\Api\Data\AttributeOptionInterface; /** * Map product index data to search engine metadata */ class ProductDataMapper implements BatchDataMapperInterface { + /** + * @var AttributeOptionInterface[] + */ + private $attributeOptionsCache; + /** * @var Builder */ @@ -33,11 +39,6 @@ class ProductDataMapper implements BatchDataMapperInterface */ private $dateFieldType; - /** - * @var array - */ - private $attributeData = []; - /** * @var array */ @@ -100,15 +101,19 @@ public function __construct( $this->excludedAttributes = array_merge($this->defaultExcludedAttributes, $excludedAttributes); $this->additionalFieldsProvider = $additionalFieldsProvider; $this->dataProvider = $dataProvider; + $this->attributeOptionsCache = []; } /** - * {@inheritdoc} + * Map index data for using in search engine metadata + * + * @param array $documentData + * @param int $storeId + * @param array $context + * @return array */ public function map(array $documentData, $storeId, array $context = []) { - // reset attribute data for new store - $this->attributeData = []; $documents = []; foreach ($documentData as $productId => $indexData) { @@ -120,9 +125,7 @@ public function map(array $documentData, $storeId, array $context = []) $this->builder->addField($attributeCode, $value); continue; } - if (in_array($attributeCode, $this->excludedAttributes, true)) { - continue; - } + $this->builder->addField( $this->fieldMapper->getFieldName( $attributeCode, @@ -146,208 +149,179 @@ public function map(array $documentData, $storeId, array $context = []) } /** - * Convert raw data retrieved from source tables to human-readable format - * E.g. [42 => [1 => 2]] will be converted to ['color' => '2', 'color_value' => 'red'] + * Convert raw data retrieved from source tables to human-readable format. * * @param int $productId * @param array $indexData * @param int $storeId * @return array */ - private function convertToProductData($productId, array $indexData, $storeId) + private function convertToProductData(int $productId, array $indexData, int $storeId): array { $productAttributes = []; - foreach ($indexData as $attributeId => $attributeValue) { - $attributeData = $this->getAttributeData($attributeId); - if (!$attributeData) { + + if (isset($indexData['options'])) { + // cover case with "options" + // see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::prepareProductIndex + $productAttributes['options'] = $indexData['options']; + unset($indexData['options']); + } + + foreach ($indexData as $attributeId => $attributeValues) { + $attribute = $this->dataProvider->getSearchableAttribute($attributeId); + if (in_array($attribute->getAttributeCode(), $this->excludedAttributes, true)) { continue; } - $productAttributes = array_merge( - $productAttributes, - $this->convertAttribute( - $productId, - $attributeId, - $attributeValue, - $attributeData, - $storeId - ) - ); + + if (!\is_array($attributeValues)) { + $attributeValues = [$productId => $attributeValues]; + } + $attributeValues = $this->prepareAttributeValues($productId, $attribute, $attributeValues, $storeId); + $productAttributes += $this->convertAttribute($attribute, $attributeValues); } + return $productAttributes; } /** - * Convert data for attribute: 1) add new value {attribute_code}_value for select and multiselect searchable - * attributes, that will contain actual value 2) add child products data to composite products + * Convert data for attribute, add {attribute_code}_value for searchable attributes, that contain actual value. * - * @param int $productId - * @param int $attributeId - * @param mixed $attributeValue - * @param array $attributeData - * @param int $storeId + * @param Attribute $attribute + * @param array $attributeValues * @return array */ - private function convertAttribute($productId, $attributeId, $attributeValue, array $attributeData, $storeId) + private function convertAttribute(Attribute $attribute, array $attributeValues): array { $productAttributes = []; - $attributeCode = $attributeData[AttributeInterface::ATTRIBUTE_CODE]; - $attributeFrontendInput = $attributeData[AttributeInterface::FRONTEND_INPUT]; - if (is_array($attributeValue)) { - if (!$attributeData['is_searchable']) { - $value = $this->getValueForAttribute( - $productId, - $attributeCode, - $attributeValue, - $attributeData['is_searchable'] - ); - } else { - if (($attributeFrontendInput == 'select' || $attributeFrontendInput == 'multiselect') - && !in_array($attributeCode, $this->excludedAttributes) - ) { - $value = $this->getValueForAttribute( - $productId, - $attributeCode, - $attributeValue, - $attributeData['is_searchable'] - ); - $productAttributes[$attributeCode . '_value'] = $this->getValueForAttributeOptions( - $attributeData, - $attributeValue - ); - } else { - $value = implode(' ', $attributeValue); - } - } - } else { - $value = $attributeValue; - } - // cover case with "options" - // see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::prepareProductIndex - if ($value) { - if ($attributeId === 'options') { - $productAttributes[$attributeId] = $value; - } else { - if (isset($attributeData[AttributeInterface::OPTIONS][$value])) { - $productAttributes[$attributeCode . '_value'] = $attributeData[AttributeInterface::OPTIONS][$value]; + $retrievedValue = $this->retrieveFieldValue($attributeValues); + if ($retrievedValue) { + $productAttributes[$attribute->getAttributeCode()] = $retrievedValue; + + if ($attribute->getIsSearchable()) { + $attributeLabels = $this->getValuesLabels($attribute, $attributeValues); + $retrievedLabel = $this->retrieveFieldValue($attributeLabels); + if ($retrievedLabel) { + $productAttributes[$attribute->getAttributeCode() . '_value'] = $retrievedLabel; } - $productAttributes[$attributeCode] = $this->formatProductAttributeValue( - $value, - $attributeData, - $storeId - ); } } + return $productAttributes; } /** - * Get product attribute data by attribute id + * Prepare attribute values. * - * @param int $attributeId + * @param int $productId + * @param Attribute $attribute + * @param array $attributeValues + * @param int $storeId * @return array */ - private function getAttributeData($attributeId) - { - if (!array_key_exists($attributeId, $this->attributeData)) { - $attribute = $this->dataProvider->getSearchableAttribute($attributeId); - if ($attribute) { - $options = []; - if ($attribute->getFrontendInput() === 'select' || $attribute->getFrontendInput() === 'multiselect') { - foreach ($attribute->getOptions() as $option) { - $options[$option->getValue()] = $option->getLabel(); - } - } - $this->attributeData[$attributeId] = [ - AttributeInterface::ATTRIBUTE_CODE => $attribute->getAttributeCode(), - AttributeInterface::FRONTEND_INPUT => $attribute->getFrontendInput(), - AttributeInterface::BACKEND_TYPE => $attribute->getBackendType(), - AttributeInterface::OPTIONS => $options, - 'is_searchable' => $attribute->getIsSearchable(), - ]; - } else { - $this->attributeData[$attributeId] = null; + private function prepareAttributeValues( + int $productId, + Attribute $attribute, + array $attributeValues, + int $storeId + ): array { + if (in_array($attribute->getAttributeCode(), $this->attributesExcludedFromMerge, true)) { + $attributeValues = [ + $productId => $attributeValues[$productId] ?? '', + ]; + } + + if ($attribute->getFrontendInput() === 'multiselect') { + $attributeValues = $this->prepareMultiselectValues($attributeValues); + } + + if ($this->isAttributeDate($attribute)) { + foreach ($attributeValues as $key => $attributeValue) { + $attributeValues[$key] = $this->dateFieldType->formatDate($storeId, $attributeValue); } } - return $this->attributeData[$attributeId]; + return $attributeValues; } /** - * Format product attribute value for search engine + * Prepare multiselect values. * - * @param mixed $value - * @param array $attributeData - * @param string $storeId - * @return string + * @param array $values + * @return array */ - private function formatProductAttributeValue($value, $attributeData, $storeId) + private function prepareMultiselectValues(array $values): array { - if ($attributeData[AttributeInterface::FRONTEND_INPUT] === 'date' - || in_array($attributeData[AttributeInterface::BACKEND_TYPE], ['datetime', 'timestamp'])) { - return $this->dateFieldType->formatDate($storeId, $value); - } elseif ($attributeData[AttributeInterface::FRONTEND_INPUT] === 'multiselect') { - return str_replace(',', ' ', $value); - } else { - return $value; - } + return \array_merge(...\array_map(function (string $value) { + return \explode(',', $value); + }, $values)); } /** - * Return single value if value exists for the productId in array, otherwise return concatenated array values + * Is attribute date. * - * @param int $productId - * @param string $attributeCode - * @param array $attributeValue - * @param bool $isSearchable - * @return mixed + * @param Attribute $attribute + * @return bool */ - private function getValueForAttribute($productId, $attributeCode, array $attributeValue, $isSearchable) + private function isAttributeDate(Attribute $attribute): bool { - if ((!$isSearchable || in_array($attributeCode, $this->attributesExcludedFromMerge)) - && isset($attributeValue[$productId]) - ) { - $value = $attributeValue[$productId]; - } elseif (in_array($attributeCode, $this->attributesExcludedFromMerge) && !isset($attributeValue[$productId])) { - $value = ''; - } else { - $value = implode(' ', $attributeValue); - } - return $value; + return $attribute->getFrontendInput() === 'date' + || in_array($attribute->getBackendType(), ['datetime', 'timestamp'], true); } /** - * Concatenate select and multiselect attribute values + * Get values labels. * - * @param array $attributeData - * @param array $attributeValue - * @return string + * @param Attribute $attribute + * @param array $attributeValues + * @return array */ - private function getValueForAttributeOptions(array $attributeData, array $attributeValue) + private function getValuesLabels(Attribute $attribute, array $attributeValues): array { - $result = null; - $selectedValues = []; - if ($attributeData[AttributeInterface::FRONTEND_INPUT] == 'select') { - foreach ($attributeValue as $selectedValue) { - if (isset($attributeData[AttributeInterface::OPTIONS][$selectedValue])) { - $selectedValues[] = $attributeData[AttributeInterface::OPTIONS][$selectedValue]; - } - } + $attributeLabels = []; + + $options = $this->getAttributeOptions($attribute); + if (empty($options)) { + return $attributeLabels; } - if ($attributeData[AttributeInterface::FRONTEND_INPUT] == 'multiselect') { - foreach ($attributeValue as $selectedAttributeValues) { - $selectedAttributeValues = explode(',', $selectedAttributeValues); - foreach ($selectedAttributeValues as $selectedValue) { - if (isset($attributeData[AttributeInterface::OPTIONS][$selectedValue])) { - $selectedValues[] = $attributeData[AttributeInterface::OPTIONS][$selectedValue]; - } - } + + foreach ($options as $option) { + if (\in_array($option->getValue(), $attributeValues)) { + $attributeLabels[] = $option->getLabel(); } } - $selectedValues = array_unique($selectedValues); - if (!empty($selectedValues)) { - $result = implode(' ', $selectedValues); + + return $attributeLabels; + } + + /** + * Retrieve options for attribute + * + * @param Attribute $attribute + * @return array + */ + private function getAttributeOptions(Attribute $attribute): array + { + if (!isset($this->attributeOptionsCache[$attribute->getId()])) { + $options = $attribute->getOptions() ?? []; + $this->attributeOptionsCache[$attribute->getId()] = $options; } - return $result; + + return $this->attributeOptionsCache[$attribute->getId()]; + } + + /** + * Retrieve value for field. If field have only one value this method return it. + * Otherwise will be returned array of these values. + * Note: array of values must have index keys, not as associative array. + * + * @param array $values + * @return array|string + */ + private function retrieveFieldValue(array $values) + { + $values = \array_filter(\array_unique($values)); + + return count($values) === 1 ? \array_shift($values) : \array_values($values); } } diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/DataMapper/ProductDataMapper.php b/app/code/Magento/Elasticsearch/Model/Adapter/DataMapper/ProductDataMapper.php index da37c8d7647d..24b740b554fc 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/DataMapper/ProductDataMapper.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/DataMapper/ProductDataMapper.php @@ -5,48 +5,15 @@ */ namespace Magento\Elasticsearch\Model\Adapter\DataMapper; -use Magento\Catalog\Model\ResourceModel\Eav\Attribute; -use Magento\Store\Model\StoreManagerInterface; -use Magento\Customer\Api\Data\GroupInterface; use Magento\Elasticsearch\Model\Adapter\DataMapperInterface; use Magento\Elasticsearch\Elasticsearch5\Model\Adapter\DataMapper\ProductDataMapper as ElasticSearch5ProductDataMapper; /** + * Don't use this product data mapper class. + * * @deprecated 100.2.0 * @see \Magento\Elasticsearch\Model\Adapter\BatchDataMapperInterface */ class ProductDataMapper extends ElasticSearch5ProductDataMapper implements DataMapperInterface { - /** - * Prepare category index data for product - * - * @param int $productId - * @param array $categoryIndexData - * @return array - */ - protected function getProductCategoryData($productId, array $categoryIndexData) - { - $result = []; - $categoryIds = []; - - if (array_key_exists($productId, $categoryIndexData)) { - $indexData = $categoryIndexData[$productId]; - $result = $indexData; - } - - if (array_key_exists($productId, $categoryIndexData)) { - $indexData = $categoryIndexData[$productId]; - foreach ($indexData as $categoryData) { - $categoryIds[] = $categoryData['id']; - } - if (count($categoryIds)) { - $result = ['category_ids' => implode(' ', $categoryIds)]; - foreach ($indexData as $data) { - $result['position_category_' . $data['id']] = $data['position']; - $result['name_category_' . $data['id']] = $data['name']; - } - } - } - return $result; - } } diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/Elasticsearch.php b/app/code/Magento/Elasticsearch/Model/Adapter/Elasticsearch.php index 7fc45eed3565..97a76de4b995 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/Elasticsearch.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/Elasticsearch.php @@ -23,6 +23,11 @@ class Elasticsearch const BULK_ACTION_UPDATE = 'update'; /**#@-*/ + /** + * Buffer for total fields limit in mapping. + */ + private const MAPPING_TOTAL_FIELDS_BUFFER_LIMIT = 1000; + /**#@-*/ protected $connectionManager; @@ -197,7 +202,7 @@ public function cleanIndex($storeId, $mappedIndexerId) // prepare new index name and increase version $indexPattern = $this->indexNameResolver->getIndexPattern($storeId, $mappedIndexerId); - $version = intval(str_replace($indexPattern, '', $indexName)); + $version = (int)(str_replace($indexPattern, '', $indexName)); $newIndexName = $indexPattern . ++$version; // remove index if already exists @@ -279,8 +284,9 @@ protected function getDocsArrayInBulkIndexFormat( * Checks whether Elasticsearch index and alias exists. * * @param int $storeId - * @param bool $checkAlias * @param string $mappedIndexerId + * @param bool $checkAlias + * * @return $this */ public function checkIndex( @@ -347,13 +353,32 @@ public function updateAlias($storeId, $mappedIndexerId) protected function prepareIndex($storeId, $indexName, $mappedIndexerId) { $this->indexBuilder->setStoreId($storeId); - $this->client->createIndex($indexName, ['settings' => $this->indexBuilder->build()]); + $settings = $this->indexBuilder->build(); + $allAttributeTypes = $this->fieldMapper->getAllAttributesTypes([ + 'entityType' => $mappedIndexerId, + // Use store id instead of website id from context for save existing fields mapping. + // In future websiteId will be eliminated due to index stored per store + 'websiteId' => $storeId + ]); + $settings['index']['mapping']['total_fields']['limit'] = $this->getMappingTotalFieldsLimit($allAttributeTypes); + $this->client->createIndex($indexName, ['settings' => $settings]); $this->client->addFieldsMapping( - $this->fieldMapper->getAllAttributesTypes(['entityType' => $mappedIndexerId]), + $allAttributeTypes, $indexName, $this->clientConfig->getEntityType() ); $this->preparedIndex[$storeId] = $indexName; return $this; } + + /** + * Get total fields limit for mapping. + * + * @param array $allAttributeTypes + * @return int + */ + private function getMappingTotalFieldsLimit(array $allAttributeTypes): int + { + return count($allAttributeTypes) + self::MAPPING_TOTAL_FIELDS_BUFFER_LIMIT; + } } diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter.php new file mode 100644 index 000000000000..54586fa357ff --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter.php @@ -0,0 +1,188 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Model\Adapter\FieldMapper\Product; + +use Magento\Framework\Api\CustomAttributesDataInterface; + +/** + * Product attribute adapter for elasticsearch context. + */ +class AttributeAdapter +{ + /** + * @var CustomAttributesDataInterface + */ + private $attribute; + + /** + * @var string + */ + private $attributeCode; + + /** + * @param CustomAttributesDataInterface $attribute + * @param string $attributeCode + */ + public function __construct( + CustomAttributesDataInterface $attribute, + string $attributeCode + ) { + $this->attribute = $attribute; + $this->attributeCode = $attributeCode; + } + + /** + * Check if attribute is filterable. + * + * @return bool + */ + public function isFilterable(): bool + { + return $this->getAttribute()->getIsFilterable() || $this->getAttribute()->getIsFilterableInSearch(); + } + + /** + * Check if attribute is searchable. + * + * @return bool + */ + public function isSearchable(): bool + { + return $this->getAttribute()->getIsSearchable() + || ($this->getAttribute()->getIsVisibleInAdvancedSearch() + || $this->isFilterable()); + } + + /** + * Check if attribute is need to index always. + * + * @return bool + */ + public function isAlwaysIndexable(): bool + { + // List of attributes which are required to be indexable + $alwaysIndexableAttributes = [ + 'category_ids', + 'visibility', + ]; + + return in_array($this->getAttributeCode(), $alwaysIndexableAttributes, true); + } + + /** + * Check if attribute has date/time type. + * + * @return bool + */ + public function isDateTimeType(): bool + { + return in_array($this->getAttribute()->getBackendType(), ['timestamp', 'datetime'], true); + } + + /** + * Check if attribute has float type. + * + * @return bool + */ + public function isFloatType(): bool + { + return $this->getAttribute()->getBackendType() === 'decimal'; + } + + /** + * Check if attribute has integer type. + * + * @return bool + */ + public function isIntegerType(): bool + { + return in_array($this->getAttribute()->getBackendType(), ['int', 'smallint'], true); + } + + /** + * Check if attribute has boolean type. + * + * @return bool + */ + public function isBooleanType(): bool + { + return in_array($this->getAttribute()->getFrontendInput(), ['select', 'boolean'], true) + && $this->getAttribute()->getBackendType() !== 'varchar'; + } + + /** + * Check if attribute has boolean type. + * + * @return bool + */ + public function isComplexType(): bool + { + return in_array($this->getAttribute()->getFrontendInput(), ['select', 'multiselect'], true) + || $this->getAttribute()->usesSource(); + } + + /** + * Check if product attribute is EAV. + * + * @return bool + */ + public function isEavAttribute(): bool + { + return $this->getAttribute() instanceof \Magento\Eav\Api\Data\AttributeInterface; + } + + /** + * Get attribute code. + * + * @return string + */ + 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. + * + * @return string + */ + public function isUserDefined(): string + { + return $this->getAttribute()->getIsUserDefined(); + } + + /** + * Frontend HTML for input element. + * + * @return string + */ + public function getFrontendInput() + { + return $this->getAttribute()->getFrontendInput(); + } + + /** + * Get product attribute instance. + * + * @return CustomAttributesDataInterface|\Magento\Eav\Api\Data\AttributeInterface + */ + private function getAttribute(): CustomAttributesDataInterface + { + return $this->attribute; + } +} 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 new file mode 100644 index 000000000000..19b9f85c44b0 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter/DummyAttribute.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; + +use Magento\Framework\Api\CustomAttributesDataInterface; + +/** + * Dummy class for Not EAV attribute. + * @SuppressWarnings(PHPMD) + */ +class DummyAttribute implements CustomAttributesDataInterface +{ + /** + * Get an attribute value. + * + * @param string $attributeCode + * @return \Magento\Framework\Api\AttributeInterface|null + */ + public function getCustomAttribute($attributeCode) + { + return null; + } + + /** + * Set an attribute value for a given attribute code + * + * @param string $attributeCode + * @param mixed $attributeValue + * @return $this + */ + public function setCustomAttribute($attributeCode, $attributeValue) + { + return $this; + } + + /** + * Retrieve custom attributes values. + * + * @return \Magento\Framework\Api\AttributeInterface[]|null + */ + public function getCustomAttributes() + { + return null; + } + + /** + * Set array of custom attributes + * + * @param \Magento\Framework\Api\AttributeInterface[] $attributes + * @return $this + * @throws \LogicException + */ + 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/AttributeProvider.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeProvider.php new file mode 100644 index 000000000000..89c98d29ae03 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeProvider.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Model\Adapter\FieldMapper\Product; + +use Magento\Eav\Model\Config; +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter\DummyAttribute; +use Psr\Log\LoggerInterface; + +/** + * Provide attribute adapter. + */ +class AttributeProvider +{ + /** + * Object Manager instance + * + * @var \Magento\Framework\ObjectManagerInterface + */ + private $objectManager; + + /** + * Instance name to create + * + * @var string + */ + private $instanceName; + + /** + * @var Config + */ + private $eavConfig; + + /** + * @var array + */ + private $cachedPool = []; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * Factory constructor + * + * @param \Magento\Framework\ObjectManagerInterface $objectManager + * @param Config $eavConfig + * @param LoggerInterface $logger + * @param string $instanceName + */ + public function __construct( + \Magento\Framework\ObjectManagerInterface $objectManager, + Config $eavConfig, + LoggerInterface $logger, + $instanceName = AttributeAdapter::class + ) { + $this->objectManager = $objectManager; + $this->instanceName = $instanceName; + $this->eavConfig = $eavConfig; + $this->logger = $logger; + } + + /** + * @inheritdoc + */ + public function getByAttributeCode(string $attributeCode): AttributeAdapter + { + if (!isset($this->cachedPool[$attributeCode])) { + $attribute = $this->eavConfig->getAttribute( + ProductAttributeInterface::ENTITY_TYPE_CODE, + $attributeCode + ); + if (null === $attribute) { + $attribute = $this->objectManager->create(DummyAttribute::class); + } + $this->cachedPool[$attributeCode] = $this->objectManager->create( + $this->instanceName, + ['attribute' => $attribute, 'attributeCode' => $attributeCode] + ); + } + + return $this->cachedPool[$attributeCode]; + } +} diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/CompositeFieldProvider.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/CompositeFieldProvider.php new file mode 100644 index 000000000000..8038c8c05bc1 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/CompositeFieldProvider.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Model\Adapter\FieldMapper\Product; + +/** + * Provide fields for product. + */ +class CompositeFieldProvider implements FieldProviderInterface +{ + /** + * @var FieldProviderInterface[] + */ + private $providers; + + /** + * @param FieldProviderInterface[] $providers + */ + public function __construct(array $providers) + { + foreach ($providers as $provider) { + if (!$provider instanceof FieldProviderInterface) { + throw new \InvalidArgumentException( + sprintf('Instance of the field provider is expected, got %s instead.', get_class($provider)) + ); + } + } + $this->providers = $providers; + } + + /** + * Get fields. + * + * @param array $context + * @return array + */ + public function getFields(array $context = []): array + { + $allAttributes = []; + + foreach ($this->providers as $provider) { + $allAttributes = array_merge($allAttributes, $provider->getFields($context)); + } + + return $allAttributes; + } +} 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 new file mode 100644 index 000000000000..268fe00e4c41 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicField.php @@ -0,0 +1,155 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider; + +use Magento\Catalog\Api\CategoryListInterface; +use Magento\Customer\Api\GroupRepositoryInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProviderInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface + as FieldTypeConverterInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\ConverterInterface + as IndexTypeConverterInterface; +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. + */ +class DynamicField implements FieldProviderInterface +{ + /** + * Category list. + * + * @deprecated + * @var CategoryListInterface + */ + private $categoryList; + + /** + * Category collection. + * + * @var Collection + */ + private $categoryCollection; + + /** + * Customer group repository. + * + * @var GroupRepositoryInterface + */ + private $groupRepository; + + /** + * Search criteria builder. + * + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var FieldTypeConverterInterface + */ + private $fieldTypeConverter; + + /** + * @var IndexTypeConverterInterface + */ + private $indexTypeConverter; + + /** + * @var AttributeProvider + */ + private $attributeAdapterProvider; + + /** + * @var FieldNameResolver + */ + private $fieldNameResolver; + + /** + * @param FieldTypeConverterInterface $fieldTypeConverter + * @param IndexTypeConverterInterface $indexTypeConverter + * @param GroupRepositoryInterface $groupRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param CategoryListInterface $categoryList + * @param FieldNameResolver $fieldNameResolver + * @param AttributeProvider $attributeAdapterProvider + * @param Collection|null $categoryCollection + */ + public function __construct( + FieldTypeConverterInterface $fieldTypeConverter, + IndexTypeConverterInterface $indexTypeConverter, + GroupRepositoryInterface $groupRepository, + SearchCriteriaBuilder $searchCriteriaBuilder, + CategoryListInterface $categoryList, + FieldNameResolver $fieldNameResolver, + AttributeProvider $attributeAdapterProvider, + Collection $categoryCollection = null + ) { + $this->groupRepository = $groupRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->fieldTypeConverter = $fieldTypeConverter; + $this->indexTypeConverter = $indexTypeConverter; + $this->categoryList = $categoryList; + $this->fieldNameResolver = $fieldNameResolver; + $this->attributeAdapterProvider = $attributeAdapterProvider; + $this->categoryCollection = $categoryCollection ?: + ObjectManager::getInstance()->get(Collection::class); + } + + /** + * @inheritdoc + */ + public function getFields(array $context = []): array + { + $allAttributes = []; + $categoryIds = $this->categoryCollection->getAllIds(); + $positionAttribute = $this->attributeAdapterProvider->getByAttributeCode('position'); + $categoryNameAttribute = $this->attributeAdapterProvider->getByAttributeCode('category_name'); + foreach ($categoryIds as $categoryId) { + $categoryPositionKey = $this->fieldNameResolver->getFieldName( + $positionAttribute, + ['categoryId' => $categoryId] + ); + $categoryNameKey = $this->fieldNameResolver->getFieldName( + $categoryNameAttribute, + ['categoryId' => $categoryId] + ); + $allAttributes[$categoryPositionKey] = [ + 'type' => $this->fieldTypeConverter->convert(FieldTypeConverterInterface::INTERNAL_DATA_TYPE_STRING), + 'index' => $this->indexTypeConverter->convert(IndexTypeConverterInterface::INTERNAL_NO_INDEX_VALUE) + ]; + $allAttributes[$categoryNameKey] = [ + 'type' => $this->fieldTypeConverter->convert(FieldTypeConverterInterface::INTERNAL_DATA_TYPE_STRING), + 'index' => $this->indexTypeConverter->convert(IndexTypeConverterInterface::INTERNAL_NO_INDEX_VALUE) + ]; + } + + $searchCriteria = $this->searchCriteriaBuilder->create(); + $groups = $this->groupRepository->getList($searchCriteria)->getItems(); + $priceAttribute = $this->attributeAdapterProvider->getByAttributeCode('price'); + $ctx = isset($context['websiteId']) ? ['websiteId' => $context['websiteId']] : []; + foreach ($groups as $group) { + $ctx['customerGroupId'] = $group->getId(); + $groupPriceKey = $this->fieldNameResolver->getFieldName( + $priceAttribute, + $ctx + ); + $allAttributes[$groupPriceKey] = [ + 'type' => $this->fieldTypeConverter->convert(FieldTypeConverterInterface::INTERNAL_DATA_TYPE_FLOAT), + 'store' => true + ]; + } + + return $allAttributes; + } +} 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 new file mode 100644 index 000000000000..535ab62dd599 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/Converter.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex; + +/** + * Field type converter from internal index type to elastic service. + */ +class Converter implements ConverterInterface +{ + /** + * Text flags for Elasticsearch index value + */ + 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 = [ + ConverterInterface::INTERNAL_NO_INDEX_VALUE => self::ES_NO_INDEX, + ConverterInterface::INTERNAL_NO_ANALYZE_VALUE => self::ES_NO_ANALYZE, + ]; + + /** + * Get service field index type for elasticsearch 2. + * + * @param string $internalType + * @return string|boolean + * @throws \DomainException + */ + public function convert(string $internalType) + { + if (!isset($this->mapping[$internalType])) { + throw new \DomainException(sprintf('Unsupported internal field index type: %s', $internalType)); + } + return $this->mapping[$internalType]; + } +} 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 new file mode 100644 index 000000000000..5ecfd6243003 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/ConverterInterface.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex; + +/** + * Field type converter from internal index value to elastic service. + */ +interface ConverterInterface +{ + /**#@+ + * Text flags for internal no index value. + */ + 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. + * + * @param string $internalType + * @return string|boolean + */ + public function convert(string $internalType); +} diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/IndexResolver.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/IndexResolver.php new file mode 100644 index 000000000000..590430a7e243 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/IndexResolver.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; + +/** + * Field index resolver that provides index type for the attribute in mapping. + * For example, we need to set ‘no’/false in the case when attribute must be present in index data, + * but stay as not indexable. + */ +class IndexResolver implements ResolverInterface +{ + /** + * @var ConverterInterface + */ + private $converter; + + /** + * @param ConverterInterface $converter + */ + public function __construct(ConverterInterface $converter) + { + $this->converter = $converter; + } + + /** + * @inheritdoc + */ + public function getFieldIndex(AttributeAdapter $attribute) + { + $index = null; + if (!($attribute->isSearchable() || $attribute->isAlwaysIndexable())) { + $index = $this->converter->convert(ConverterInterface::INTERNAL_NO_INDEX_VALUE); + } + + return $index; + } +} diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/ResolverInterface.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/ResolverInterface.php new file mode 100644 index 000000000000..663b1d865fb5 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/ResolverInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; + +/** + * Field index type resolver interface. + */ +interface ResolverInterface +{ + /** + * Get field index. + * + * @param AttributeAdapter $attribute + * @return string|boolean + */ + public function getFieldIndex(AttributeAdapter $attribute); +} diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/CategoryName.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/CategoryName.php new file mode 100644 index 000000000000..5824aca6cdd5 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/CategoryName.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; +use Magento\Framework\Registry; +use Magento\Store\Model\StoreManagerInterface as StoreManager; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface; +use Magento\Framework\App\ObjectManager; + +/** + * Resolver field name for Category name attribute. + */ +class CategoryName implements ResolverInterface +{ + /** + * @var StoreManager + */ + private $storeManager; + + /** + * @var Registry + */ + private $coreRegistry; + + /** + * @param StoreManager $storeManager + * @param Registry $coreRegistry + */ + public function __construct( + StoreManager $storeManager = null, + Registry $coreRegistry = null + ) { + $this->storeManager = $storeManager ?: ObjectManager::getInstance() + ->get(StoreManager::class); + $this->coreRegistry = $coreRegistry ?: ObjectManager::getInstance() + ->get(Registry::class); + } + + /** + * Get field name. + * + * @param AttributeAdapter $attribute + * @param array $context + * @return string + */ + public function getFieldName(AttributeAdapter $attribute, $context = []): ?string + { + if ($attribute->getAttributeCode() === 'category_name') { + return 'name_category_' . $this->resolveCategoryId($context); + } + + return null; + } + + /** + * @inheritdoc + */ + private function resolveCategoryId($context): int + { + if (isset($context['categoryId'])) { + $id = $context['categoryId']; + } else { + $id = $this->coreRegistry->registry('current_category') + ? $this->coreRegistry->registry('current_category')->getId() + : $this->storeManager->getStore()->getRootCategoryId(); + } + + return (int)$id; + } +} diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/CompositeResolver.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/CompositeResolver.php new file mode 100644 index 000000000000..f1fed3f06fe3 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/CompositeResolver.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface; + +/** + * Composite class for resolving field name. + */ +class CompositeResolver implements ResolverInterface +{ + /** + * @var ResolverInterface[] + */ + private $items; + + /** + * @param ResolverInterface[] $items + */ + public function __construct(array $items) + { + foreach ($items as $item) { + if (!$item instanceof ResolverInterface) { + throw new \InvalidArgumentException( + sprintf('Instance of the field name resolver is expected, got %s instead.', get_class($item)) + ); + } + } + $this->items = $items; + } + + /** + * Get field name. + * + * @param AttributeAdapter $attribute + * @param array $context + * @return string + */ + public function getFieldName(AttributeAdapter $attribute, $context = []): ?string + { + $result = null; + foreach ($this->items as $item) { + $result = $item->getFieldName($attribute, $context); + if (null !== $result) { + break; + } + } + + return $result; + } +} 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 new file mode 100644 index 000000000000..5f319daeb3b3 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolver.php @@ -0,0 +1,131 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; +use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ResolverInterface + as FieldTypeResolver; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface + as FieldTypeConverterInterface; + +/** + * Default name resolver. + */ +class DefaultResolver implements ResolverInterface +{ + /** + * @var FieldTypeResolver + */ + private $fieldTypeResolver; + + /** + * @var FieldTypeConverterInterface + */ + private $fieldTypeConverter; + + /** + * @param FieldTypeResolver $fieldTypeResolver + * @param FieldTypeConverterInterface $fieldTypeConverter + */ + public function __construct( + FieldTypeResolver $fieldTypeResolver, + FieldTypeConverterInterface $fieldTypeConverter + ) { + $this->fieldTypeResolver = $fieldTypeResolver; + $this->fieldTypeConverter = $fieldTypeConverter; + } + + /** + * Get field name. + * + * @param AttributeAdapter $attribute + * @param array $context + * @return string + */ + public function getFieldName(AttributeAdapter $attribute, $context = []): ?string + { + $attributeCode = $attribute->getAttributeCode(); + if (empty($context['type'])) { + return $attributeCode; + } + + $fieldType = $this->fieldTypeResolver->getFieldType($attribute); + $frontendInput = $attribute->getFrontendInput(); + $fieldName = $attributeCode; + if ($context['type'] === FieldMapperInterface::TYPE_FILTER) { + if ($this->isStringServiceFieldType($fieldType)) { + return $this->getQueryTypeFieldName($frontendInput, $fieldType, $attributeCode); + } + $fieldName = $attributeCode; + } elseif ($context['type'] === FieldMapperInterface::TYPE_QUERY) { + $fieldName = $this->getQueryTypeFieldName($frontendInput, $fieldType, $attributeCode); + } elseif ($context['type'] == FieldMapperInterface::TYPE_SORT && $attribute->isSortable()) { + $fieldName = 'sort_' . $attributeCode; + } + + return $fieldName; + } + + /** + * Check if service field type for field set as 'string' + * + * @param string $serviceFieldType + * @return bool + */ + private function isStringServiceFieldType(string $serviceFieldType): bool + { + $stringTypeKey = $this->fieldTypeConverter->convert(FieldTypeConverterInterface::INTERNAL_DATA_TYPE_STRING); + + return $serviceFieldType === $stringTypeKey; + } + + /** + * Get field name for query type fields. + * + * @param string $frontendInput + * @param string $fieldType + * @param string $attributeCode + * @return string + */ + private function getQueryTypeFieldName($frontendInput, $fieldType, $attributeCode) + { + if ($attributeCode === '*') { + $fieldName = '_all'; + } else { + $fieldName = $this->getRefinedFieldName($frontendInput, $fieldType, $attributeCode); + } + return $fieldName; + } + + /** + * Prepare field name for complex fields. + * + * @param string $frontendInput + * @param string $fieldType + * @param string $attributeCode + * @return string + */ + 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, $keywordTypeKey, 'integer'], true) + ? $attributeCode . '_value' + : $attributeCode; + case 'boolean': + return $fieldType === 'integer' ? $attributeCode . '_value' : $attributeCode; + default: + return $attributeCode; + } + } +} diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/NotEavAttribute.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/NotEavAttribute.php new file mode 100644 index 000000000000..cbbd15083ee6 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/NotEavAttribute.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface; + +/** + * Resolver field name for not EAV attribute. + */ +class NotEavAttribute implements ResolverInterface +{ + /** + * Get field name for not EAV attributes. + * + * @param AttributeAdapter $attribute + * @param array $context + * @return string + */ + public function getFieldName(AttributeAdapter $attribute, $context = []): ?string + { + if (!$attribute->isEavAttribute()) { + return $attribute->getAttributeCode(); + } + + return null; + } +} diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/Position.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/Position.php new file mode 100644 index 000000000000..044d5d8da9a6 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/Position.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; +use Magento\Framework\Registry; +use Magento\Store\Model\StoreManagerInterface as StoreManager; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface; +use Magento\Framework\App\ObjectManager; + +/** + * Resolver field name for position attribute. + */ +class Position implements ResolverInterface +{ + /** + * @var StoreManager + */ + private $storeManager; + + /** + * @var Registry + */ + private $coreRegistry; + + /** + * @param StoreManager $storeManager + * @param Registry $coreRegistry + */ + public function __construct( + StoreManager $storeManager = null, + Registry $coreRegistry = null + ) { + $this->storeManager = $storeManager ?: ObjectManager::getInstance() + ->get(StoreManager::class); + $this->coreRegistry = $coreRegistry ?: ObjectManager::getInstance() + ->get(Registry::class); + } + + /** + * @inheritdoc + */ + public function getFieldName(AttributeAdapter $attribute, $context = []): ?string + { + if ($attribute->getAttributeCode() === 'position') { + return 'position_category_' . $this->resolveCategoryId($context); + } + + return null; + } + + /** + * @inheritdoc + */ + private function resolveCategoryId($context) + { + if (isset($context['categoryId'])) { + $id = $context['categoryId']; + } else { + $id = $this->coreRegistry->registry('current_category') + ? $this->coreRegistry->registry('current_category')->getId() + : $this->storeManager->getStore()->getRootCategoryId(); + } + + return $id; + } +} diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/Price.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/Price.php new file mode 100644 index 000000000000..12e53ca2bd71 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/Price.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver; + +use Magento\Framework\App\ObjectManager; +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Store\Model\StoreManagerInterface as StoreManager; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface; + +/** + * Resolver field name for price attribute. + */ +class Price implements ResolverInterface +{ + /** + * @var CustomerSession + */ + private $customerSession; + + /** + * @var StoreManager + */ + private $storeManager; + + /** + * @param CustomerSession $customerSession + * @param StoreManager $storeManager + */ + public function __construct( + CustomerSession $customerSession = null, + StoreManager $storeManager = null + ) { + $this->storeManager = $storeManager ?: ObjectManager::getInstance() + ->get(StoreManager::class); + $this->customerSession = $customerSession ?: ObjectManager::getInstance() + ->get(CustomerSession::class); + } + + /** + * @inheritdoc + */ + public function getFieldName(AttributeAdapter $attribute, $context = []): ?string + { + if ($attribute->getAttributeCode() === 'price') { + $customerGroupId = !empty($context['customerGroupId']) + ? $context['customerGroupId'] + : $this->customerSession->getCustomerGroupId(); + $websiteId = !empty($context['websiteId']) + ? $context['websiteId'] + : $this->storeManager->getStore()->getWebsiteId(); + + return 'price_' . $customerGroupId . '_' . $websiteId; + } + + return null; + } +} 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 new file mode 100644 index 000000000000..652fc853adbc --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/SpecialAttribute.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface; + +/** + * Resolver field name for not special attribute. + */ +class SpecialAttribute implements ResolverInterface +{ + /** + * Get field name for special list of attributes. + * + * @param AttributeAdapter $attribute + * @param array $context + * @return string + */ + public function getFieldName(AttributeAdapter $attribute, $context = []): ?string + { + if (in_array($attribute->getAttributeCode(), ['id', 'sku', 'store_id', 'visibility'], true) + && empty($context['type']) + ) { + return $attribute->getAttributeCode(); + } + + return null; + } +} diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/ResolverInterface.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/ResolverInterface.php new file mode 100644 index 000000000000..3d36e026f70d --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/ResolverInterface.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; + +/** + * Field name resolver for preparing field key for elasticsearch mapping by attribute. + */ +interface ResolverInterface +{ + /** + * Get field name. + * + * @param AttributeAdapter $attribute + * @param array $context + * @return string + */ + public function getFieldName(AttributeAdapter $attribute, $context = []): ?string; +} diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php new file mode 100644 index 000000000000..88dab8369879 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType; + +/** + * Field type converter from internal data types to elastic service. + */ +class Converter implements ConverterInterface +{ + /**#@+ + * Text flags for Elasticsearch field types + */ + private const ES_DATA_TYPE_STRING = 'string'; + private const ES_DATA_TYPE_FLOAT = 'float'; + private const ES_DATA_TYPE_INT = 'integer'; + private const ES_DATA_TYPE_DATE = 'date'; + /**#@-*/ + + /** + * Mapping between internal data types and elastic service. + * + * @var array + */ + private $mapping = [ + self::INTERNAL_DATA_TYPE_STRING => self::ES_DATA_TYPE_STRING, + self::INTERNAL_DATA_TYPE_KEYWORD => self::ES_DATA_TYPE_STRING, + self::INTERNAL_DATA_TYPE_FLOAT => self::ES_DATA_TYPE_FLOAT, + self::INTERNAL_DATA_TYPE_INT => self::ES_DATA_TYPE_INT, + self::INTERNAL_DATA_TYPE_DATE => self::ES_DATA_TYPE_DATE, + ]; + + /** + * Get service field type for elasticsearch 2. + * + * @param string $internalType + * @return string + * @throws \DomainException + */ + public function convert(string $internalType): string + { + if (!isset($this->mapping[$internalType])) { + throw new \DomainException(sprintf('Unsupported internal field type: %s', $internalType)); + } + return $this->mapping[$internalType]; + } +} diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterInterface.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterInterface.php new file mode 100644 index 000000000000..3e7c3e9b592b --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterInterface.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType; + +/** + * @api + * Field type converter from internal data types to elastic service. + */ +interface ConverterInterface +{ + /**#@+ + * Text flags for internal field types + */ + public const INTERNAL_DATA_TYPE_STRING = 'string'; + public const INTERNAL_DATA_TYPE_FLOAT = 'float'; + public const INTERNAL_DATA_TYPE_INT = 'integer'; + public const INTERNAL_DATA_TYPE_DATE = 'date'; + public const INTERNAL_DATA_TYPE_KEYWORD = 'keyword'; + /**#@-*/ + + /** + * Get service field type. + * + * @param string $internalType + * @return string + */ + public function convert(string $internalType): string; +} diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/CompositeResolver.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/CompositeResolver.php new file mode 100644 index 000000000000..e83502e4fe32 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/CompositeResolver.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ResolverInterface; + +/** + * Composite resolver for resolving field type. + */ +class CompositeResolver implements ResolverInterface +{ + /** + * @var ResolverInterface[] + */ + private $items; + + /** + * @param ResolverInterface[] $items + */ + public function __construct(array $items) + { + foreach ($items as $item) { + if (!$item instanceof ResolverInterface) { + throw new \InvalidArgumentException( + sprintf('Instance of the field type resolver is expected, got %s instead.', get_class($item)) + ); + } + } + $this->items = $items; + } + + /** + * Get field type. + * + * @param AttributeAdapter $attribute + * @return string + */ + public function getFieldType(AttributeAdapter $attribute): ?string + { + $result = null; + foreach ($this->items as $item) { + $result = $item->getFieldType($attribute); + if (null !== $result) { + break; + } + } + + return $result; + } +} diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/DateTimeType.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/DateTimeType.php new file mode 100644 index 000000000000..a4e3c634ce71 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/DateTimeType.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ResolverInterface; + +/** + * Date/Time type resolver. + */ +class DateTimeType implements ResolverInterface +{ + /** + * @var ConverterInterface + */ + private $fieldTypeConverter; + + /** + * @param ConverterInterface $fieldTypeConverter + */ + public function __construct(ConverterInterface $fieldTypeConverter) + { + $this->fieldTypeConverter = $fieldTypeConverter; + } + + /** + * Get datetime field type. + * + * @param AttributeAdapter $attribute + * @return string + */ + public function getFieldType(AttributeAdapter $attribute): ?string + { + if ($attribute->isDateTimeType()) { + return $this->fieldTypeConverter->convert(ConverterInterface::INTERNAL_DATA_TYPE_DATE); + } + + return null; + } +} diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/DefaultResolver.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/DefaultResolver.php new file mode 100644 index 000000000000..be4a8cca2881 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/DefaultResolver.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ResolverInterface; + +/** + * Default field type resolver. + */ +class DefaultResolver implements ResolverInterface +{ + /** + * @var ConverterInterface + */ + private $fieldTypeConverter; + + /** + * @param ConverterInterface $fieldTypeConverter + */ + public function __construct(ConverterInterface $fieldTypeConverter) + { + $this->fieldTypeConverter = $fieldTypeConverter; + } + + /** + * Get default field type. + * + * @param AttributeAdapter $attribute + * @return string + */ + public function getFieldType(AttributeAdapter $attribute): ?string + { + return $this->fieldTypeConverter->convert(ConverterInterface::INTERNAL_DATA_TYPE_STRING); + } +} diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/FloatType.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/FloatType.php new file mode 100644 index 000000000000..a6681a7f676b --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/FloatType.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ResolverInterface; + +/** + * Float type resolver. + */ +class FloatType implements ResolverInterface +{ + /** + * @var ConverterInterface + */ + private $fieldTypeConverter; + + /** + * @param ConverterInterface $fieldTypeConverter + */ + public function __construct(ConverterInterface $fieldTypeConverter) + { + $this->fieldTypeConverter = $fieldTypeConverter; + } + + /** + * Get float field type. + * + * @param AttributeAdapter $attribute + * @return string + */ + public function getFieldType(AttributeAdapter $attribute): ?string + { + if ($attribute->isFloatType()) { + return $this->fieldTypeConverter->convert(ConverterInterface::INTERNAL_DATA_TYPE_FLOAT); + } + + return null; + } +} diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerType.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerType.php new file mode 100644 index 000000000000..7793f60cd1ef --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerType.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ResolverInterface; + +/** + * Integer type resolver. + */ +class IntegerType implements ResolverInterface +{ + /** + * @var ConverterInterface + */ + private $fieldTypeConverter; + + /** + * @param ConverterInterface $fieldTypeConverter + */ + public function __construct(ConverterInterface $fieldTypeConverter) + { + $this->fieldTypeConverter = $fieldTypeConverter; + } + + /** + * Get integer field type. + * + * @param AttributeAdapter $attribute + * @return string + */ + public function getFieldType(AttributeAdapter $attribute): ?string + { + if (($attribute->isIntegerType() || $attribute->isBooleanType()) + && !$attribute->isUserDefined() + ) { + return $this->fieldTypeConverter->convert(ConverterInterface::INTERNAL_DATA_TYPE_INT); + } + + return null; + } +} diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ResolverInterface.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ResolverInterface.php new file mode 100644 index 000000000000..43e512ecb025 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ResolverInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; + +/** + * Field type resolver interface. + */ +interface ResolverInterface +{ + /** + * Get field type. + * + * @param AttributeAdapter $attribute + * @return string + */ + public function getFieldType(AttributeAdapter $attribute): ?string; +} 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 new file mode 100644 index 000000000000..6876b23bbb15 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php @@ -0,0 +1,151 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +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; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProviderInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface + as FieldTypeConverterInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\ConverterInterface + as IndexTypeConverterInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ResolverInterface + 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. + */ +class StaticField implements FieldProviderInterface +{ + /** + * @var Config + */ + private $eavConfig; + + /** + * @var FieldTypeConverterInterface + */ + private $fieldTypeConverter; + + /** + * @var IndexTypeConverterInterface + */ + private $indexTypeConverter; + + /** + * @var AttributeProvider + */ + private $attributeAdapterProvider; + + /** + * @var FieldTypeResolver + */ + private $fieldTypeResolver; + + /** + * @var FieldIndexResolver + */ + private $fieldIndexResolver; + + /** + * @var FieldName\ResolverInterface + */ + private $fieldNameResolver; + + /** + * @param Config $eavConfig + * @param FieldTypeConverterInterface $fieldTypeConverter + * @param IndexTypeConverterInterface $indexTypeConverter + * @param FieldTypeResolver $fieldTypeResolver + * @param FieldIndexResolver $fieldIndexResolver + * @param AttributeProvider $attributeAdapterProvider + * @param FieldName\ResolverInterface|null $fieldNameResolver + */ + public function __construct( + Config $eavConfig, + FieldTypeConverterInterface $fieldTypeConverter, + IndexTypeConverterInterface $indexTypeConverter, + FieldTypeResolver $fieldTypeResolver, + FieldIndexResolver $fieldIndexResolver, + AttributeProvider $attributeAdapterProvider, + FieldName\ResolverInterface $fieldNameResolver = null + ) { + $this->eavConfig = $eavConfig; + $this->fieldTypeConverter = $fieldTypeConverter; + $this->indexTypeConverter = $indexTypeConverter; + $this->fieldTypeResolver = $fieldTypeResolver; + $this->fieldIndexResolver = $fieldIndexResolver; + $this->attributeAdapterProvider = $attributeAdapterProvider; + $this->fieldNameResolver = $fieldNameResolver ?: ObjectManager::getInstance() + ->get(FieldName\ResolverInterface::class); + } + + /** + * Get static fields. + * + * @param array $context + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function getFields(array $context = []): array + { + $attributes = $this->eavConfig->getEntityAttributes(ProductAttributeInterface::ENTITY_TYPE_CODE); + $allAttributes = []; + + foreach ($attributes as $attribute) { + $attributeAdapter = $this->attributeAdapterProvider->getByAttributeCode($attribute->getAttributeCode()); + $fieldName = $this->fieldNameResolver->getFieldName($attributeAdapter); + + $allAttributes[$fieldName] = [ + 'type' => $this->fieldTypeResolver->getFieldType($attributeAdapter), + ]; + + $index = $this->fieldIndexResolver->getFieldIndex($attributeAdapter); + if (null !== $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()) { + $childFieldName = $this->fieldNameResolver->getFieldName( + $attributeAdapter, + ['type' => FieldMapperInterface::TYPE_QUERY] + ); + $allAttributes[$childFieldName] = [ + 'type' => $this->fieldTypeConverter->convert(FieldTypeConverterInterface::INTERNAL_DATA_TYPE_STRING) + ]; + } + } + + $allAttributes['store_id'] = [ + 'type' => $this->fieldTypeConverter->convert(FieldTypeConverterInterface::INTERNAL_DATA_TYPE_STRING), + 'index' => $this->indexTypeConverter->convert(IndexTypeConverterInterface::INTERNAL_NO_INDEX_VALUE), + ]; + + return $allAttributes; + } +} diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProviderInterface.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProviderInterface.php new file mode 100644 index 000000000000..aaafb699b33b --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProviderInterface.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Model\Adapter\FieldMapper\Product; + +/** + * Product fields provider. + * Provide fields mapping configuration for elasticsearch service of internal product attributes. + */ +interface FieldProviderInterface +{ + /** + * Get fields. + * + * @param array $context + * @return array + */ + public function getFields(array $context = []): array; +} diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/ProductFieldMapper.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/ProductFieldMapper.php index 2a8ccdb5c72a..657605bbd019 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/ProductFieldMapper.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/ProductFieldMapper.php @@ -5,135 +5,13 @@ */ namespace Magento\Elasticsearch\Model\Adapter\FieldMapper; -use Magento\Catalog\Api\Data\ProductAttributeInterface; -use Magento\Eav\Model\Config; use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface; -use Magento\Framework\Registry; -use Magento\Store\Model\StoreManagerInterface as StoreManager; -use \Magento\Customer\Model\Session as CustomerSession; use Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\ProductFieldMapper as Elasticsearch5ProductFieldMapper; -use Magento\Elasticsearch\Model\Adapter\FieldType; /** * Class ProductFieldMapper */ class ProductFieldMapper extends Elasticsearch5ProductFieldMapper implements FieldMapperInterface { - /** - * @param Config $eavConfig - * @param FieldType $fieldType - * @param CustomerSession $customerSession - * @param StoreManager $storeManager - * @param Registry $coreRegistry - */ - public function __construct( - Config $eavConfig, - FieldType $fieldType, - CustomerSession $customerSession, - StoreManager $storeManager, - Registry $coreRegistry - ) { - $this->eavConfig = $eavConfig; - $this->fieldType = $fieldType; - $this->customerSession = $customerSession; - $this->storeManager = $storeManager; - $this->coreRegistry = $coreRegistry; - } - - /** - * @param string $attributeCode - * @param array $context - * @return string - */ - public function getFieldName($attributeCode, $context = []) - { - $attribute = $this->eavConfig->getAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, $attributeCode); - if (!$attribute || in_array($attributeCode, ['id', 'sku', 'store_id', 'visibility'], true)) { - return $attributeCode; - } - if ($attributeCode === 'price') { - return $this->getPriceFieldName($context); - } - if ($attributeCode === 'position') { - return $this->getPositionFiledName($context); - } - $fieldType = $this->fieldType->getFieldType($attribute); - $frontendInput = $attribute->getFrontendInput(); - if (empty($context['type'])) { - $fieldName = $attributeCode; - } elseif ($context['type'] === FieldMapperInterface::TYPE_FILTER) { - if ($fieldType === 'string') { - return $this->getFieldName( - $attributeCode, - array_merge($context, ['type' => FieldMapperInterface::TYPE_QUERY]) - ); - } - $fieldName = $attributeCode; - } elseif ($context['type'] === FieldMapperInterface::TYPE_QUERY) { - $fieldName = $this->getQueryTypeFieldName($frontendInput, $fieldType, $attributeCode); - } else { - $fieldName = 'sort_' . $attributeCode; - } - - return $fieldName; - } - - /** - * @param array $context - * @return array - */ - public function getAllAttributesTypes($context = []) - { - $attributeCodes = $this->eavConfig->getEntityAttributeCodes(ProductAttributeInterface::ENTITY_TYPE_CODE); - $allAttributes = []; - // List of attributes which are required to be indexable - $alwaysIndexableAttributes = [ - 'category_ids', - 'visibility', - ]; - - foreach ($attributeCodes as $attributeCode) { - $attribute = $this->eavConfig->getAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, $attributeCode); - - $allAttributes[$attributeCode] = [ - 'type' => $this->fieldType->getFieldType($attribute) - ]; - - if (!$attribute->getIsSearchable() && !$this->isAttributeUsedInAdvancedSearch($attribute) - && !in_array($attributeCode, $alwaysIndexableAttributes, true) - ) { - $allAttributes[$attributeCode] = array_merge( - $allAttributes[$attributeCode], - ['index' => 'no'] - ); - } - - if ($attribute->getFrontendInput() === 'select' || $attribute->getFrontendInput() === 'multiselect') { - $allAttributes[$attributeCode . '_value'] = [ - 'type' => FieldType::ES_DATA_TYPE_STRING, - ]; - } - } - - return $allAttributes; - } - - /** - * @param string $frontendInput - * @param string $fieldType - * @param string $attributeCode - * @return string - */ - protected function getRefinedFieldName($frontendInput, $fieldType, $attributeCode) - { - switch ($frontendInput) { - case 'select': - return in_array($fieldType, ['string','integer'], true) ? $attributeCode . '_value' : $attributeCode; - case 'boolean': - return $fieldType === 'integer' ? $attributeCode . '_value' : $attributeCode; - default: - return $attributeCode; - } - } } diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldType.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldType.php index 4315597a3cf5..e7d8d0672aaf 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldType.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldType.php @@ -6,15 +6,23 @@ namespace Magento\Elasticsearch\Model\Adapter; use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ResolverInterface; /** * Class FieldType + * * @api * @since 100.1.0 + * + * @deprecated This class provide not full data about field type. Only basic rules apply in this class. + * @see ResolverInterface */ class FieldType { /**#@+ + * @deprecated + * @see \Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface + * * Text flags for Elasticsearch field types */ const ES_DATA_TYPE_STRING = 'string'; @@ -27,16 +35,21 @@ class FieldType /**#@-*/ /** + * Get field type. + * + * @deprecated + * @see ResolverInterface::getFieldType + * * @param AbstractAttribute $attribute * @return string - * @since 100.1.0 */ public function getFieldType($attribute) { + trigger_error('Class is deprecated', E_USER_DEPRECATED); $backendType = $attribute->getBackendType(); $frontendInput = $attribute->getFrontendInput(); - if (in_array($backendType, ['timestamp', 'datetime'], true)) { + if ($backendType === 'timestamp') { $fieldType = self::ES_DATA_TYPE_DATE; } elseif ((in_array($backendType, ['int', 'smallint'], true) || (in_array($frontendInput, ['select', 'boolean'], true) && $backendType !== 'varchar')) 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 000000000000..b3f8a56110f8 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Advanced/ProductCollectionPrepareStrategy.php @@ -0,0 +1,41 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Elasticsearch\Model\Advanced; + +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Catalog\Model\Config; +use Magento\CatalogSearch\Model\Advanced\ProductCollectionPrepareStrategyInterface; + +/** + * Strategy interface for preparing product collection. + */ +class ProductCollectionPrepareStrategy implements ProductCollectionPrepareStrategyInterface +{ + /** + * @var Config + */ + private $catalogConfig; + + /** + * @param Config $catalogConfig + */ + public function __construct( + Config $catalogConfig + ) { + $this->catalogConfig = $catalogConfig; + } + + /** + * @inheritdoc + */ + public function prepare(Collection $collection) + { + $collection + ->addAttributeToSelect($this->catalogConfig->getProductAttributes()) + ->addMinimalPrice() + ->addTaxPercents(); + } +} diff --git a/app/code/Magento/Elasticsearch/Model/Config.php b/app/code/Magento/Elasticsearch/Model/Config.php index a0f3b6433b43..387db07c62f9 100644 --- a/app/code/Magento/Elasticsearch/Model/Config.php +++ b/app/code/Magento/Elasticsearch/Model/Config.php @@ -62,27 +62,36 @@ class Config implements ClientOptionsInterface private $engineResolver; /** - * Constructor + * Available Elasticsearch engines. * + * @var array + */ + private $engineList; + + /** * @param ScopeConfigInterface $scopeConfig * @param ClientResolver|null $clientResolver * @param EngineResolverInterface|null $engineResolver * @param string|null $prefix + * @param array $engineList */ public function __construct( ScopeConfigInterface $scopeConfig, ClientResolver $clientResolver = null, EngineResolverInterface $engineResolver = null, - $prefix = null + $prefix = null, + $engineList = [] ) { $this->scopeConfig = $scopeConfig; $this->clientResolver = $clientResolver ?: ObjectManager::getInstance()->get(ClientResolver::class); $this->engineResolver = $engineResolver ?: ObjectManager::getInstance()->get(EngineResolverInterface::class); $this->prefix = $prefix ?: $this->clientResolver->getCurrentEngine(); + $this->engineList = $engineList; } /** - * {@inheritdoc} + * @inheritdoc + * * @since 100.1.0 */ public function prepareClientOptions($options = []) @@ -135,7 +144,7 @@ public function getSearchConfigData($field, $storeId = null) */ public function isElasticsearchEnabled() { - return $this->engineResolver->getCurrentSearchEngine() === self::ENGINE_NAME; + return in_array($this->engineResolver->getCurrentSearchEngine(), $this->engineList); } /** @@ -150,7 +159,7 @@ public function getIndexPrefix() } /** - * get Elasticsearch entity type + * Get Elasticsearch entity type * * @return string * @since 100.1.0 diff --git a/app/code/Magento/Elasticsearch/Model/DataProvider/Suggestions.php b/app/code/Magento/Elasticsearch/Model/DataProvider/Suggestions.php index 8ebd45596d39..c4fab39dfde6 100644 --- a/app/code/Magento/Elasticsearch/Model/DataProvider/Suggestions.php +++ b/app/code/Magento/Elasticsearch/Model/DataProvider/Suggestions.php @@ -15,6 +15,9 @@ use Magento\Elasticsearch\SearchAdapter\SearchIndexNameResolver; use Magento\Store\Model\StoreManagerInterface as StoreManager; +/** + * Class Suggestions + */ class Suggestions implements SuggestedQueriesInterface { /** @@ -66,6 +69,8 @@ class Suggestions implements SuggestedQueriesInterface private $storeManager; /** + * Suggestions constructor. + * * @param ScopeConfigInterface $scopeConfig * @param Config $config * @param QueryResultFactory $queryResultFactory @@ -90,11 +95,9 @@ public function __construct( } /** - * {@inheritdoc} - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @inheritdoc */ - public function getItems(QueryInterface $query, $limit = null, $additionalFilters = null) + public function getItems(QueryInterface $query) { $result = []; if ($this->isSuggestionsAllowed()) { @@ -118,19 +121,23 @@ public function getItems(QueryInterface $query, $limit = null, $additionalFilter } /** - * {@inheritdoc} + * @inheritdoc */ public function isResultsCountEnabled() { - return (bool)$this->scopeConfig->getValue( + return $this->scopeConfig->isSetFlag( self::CONFIG_SUGGESTION_COUNT_RESULTS_ENABLED, ScopeInterface::SCOPE_STORE ); } /** + * Get Suggestions + * * @param QueryInterface $query + * * @return array + * @throws \Magento\Framework\Exception\NoSuchEntityException */ private function getSuggestions(QueryInterface $query) { @@ -178,6 +185,8 @@ private function getSuggestions(QueryInterface $query) } /** + * Fetch Query + * * @param array $query * @return array */ @@ -200,16 +209,18 @@ private function getSearchSuggestionsCount() } /** + * Is Search Suggestions Allowed + * * @return bool */ private function isSuggestionsAllowed() { - $isSearchSuggestionsEnabled = (bool)$this->scopeConfig->getValue( + $isSuggestionsEnabled = $this->scopeConfig->isSetFlag( self::CONFIG_SUGGESTION_ENABLED, ScopeInterface::SCOPE_STORE ); $isEnabled = $this->config->isElasticsearchEnabled(); - $isSuggestionsAllowed = ($isEnabled && $isSearchSuggestionsEnabled); + $isSuggestionsAllowed = ($isEnabled && $isSuggestionsEnabled); return $isSuggestionsAllowed; } } diff --git a/app/code/Magento/Elasticsearch/Model/Indexer/IndexerHandler.php b/app/code/Magento/Elasticsearch/Model/Indexer/IndexerHandler.php index cfcdbecb1624..847710eaa445 100644 --- a/app/code/Magento/Elasticsearch/Model/Indexer/IndexerHandler.php +++ b/app/code/Magento/Elasticsearch/Model/Indexer/IndexerHandler.php @@ -12,6 +12,9 @@ use Magento\Elasticsearch\Model\Adapter\Index\IndexNameResolver; use Magento\Framework\App\ScopeResolverInterface; +/** + * Indexer Handler for Elasticsearch engine. + */ class IndexerHandler implements IndexerInterface { /** @@ -82,7 +85,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function saveIndex($dimensions, \Traversable $documents) { @@ -97,7 +100,7 @@ public function saveIndex($dimensions, \Traversable $documents) } /** - * {@inheritdoc} + * @inheritdoc */ public function deleteIndex($dimensions, \Traversable $documents) { @@ -112,7 +115,7 @@ public function deleteIndex($dimensions, \Traversable $documents) } /** - * {@inheritdoc} + * @inheritdoc */ public function cleanIndex($dimensions) { @@ -122,14 +125,16 @@ public function cleanIndex($dimensions) } /** - * {@inheritdoc} + * @inheritdoc */ - public function isAvailable() + public function isAvailable($dimensions = []) { return $this->adapter->ping(); } /** + * Returns indexer id. + * * @return string */ private function getIndexerId() 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 000000000000..ef2992e1fff9 --- /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 000000000000..7d2a30b493d3 --- /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 000000000000..32cb85ff750c --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolver.php @@ -0,0 +1,87 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Elasticsearch\Model\ResourceModel\Fulltext\Collection; + +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverInterface; +use Magento\Framework\Data\Collection; +use Magento\Framework\Api\Search\SearchCriteriaBuilder; +use Magento\Framework\Api\Search\SearchCriteria; + +/** + * Resolve specific attributes for search criteria. + */ +class SearchCriteriaResolver implements SearchCriteriaResolverInterface +{ + /** + * @var SearchCriteriaBuilder + */ + private $builder; + + /** + * @var Collection + */ + private $collection; + + /** + * @var string + */ + private $searchRequestName; + + /** + * @var int + */ + private $size; + + /** + * @var array + */ + private $orders; + + /** + * @var int + */ + private $currentPage; + + /** + * SearchCriteriaResolver constructor. + * @param SearchCriteriaBuilder $builder + * @param Collection $collection + * @param string $searchRequestName + * @param int $currentPage + * @param int $size + * @param array $orders + */ + public function __construct( + SearchCriteriaBuilder $builder, + Collection $collection, + string $searchRequestName, + int $currentPage, + int $size, + ?array $orders + ) { + $this->builder = $builder; + $this->collection = $collection; + $this->searchRequestName = $searchRequestName; + $this->currentPage = $currentPage; + $this->size = $size; + $this->orders = $orders; + } + + /** + * @inheritdoc + */ + public function resolve(): SearchCriteria + { + $this->builder->setPageSize($this->size); + $searchCriteria = $this->builder->create(); + $searchCriteria->setRequestName($this->searchRequestName); + $searchCriteria->setSortOrders(array_merge(['relevance' => 'DESC'], $this->orders)); + $searchCriteria->setCurrentPage($this->currentPage - 1); + + return $searchCriteria; + } +} diff --git a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php new file mode 100644 index 000000000000..3ae2d384782c --- /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 000000000000..109721fcc71a --- /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/Observer/CategoryProductIndexer.php b/app/code/Magento/Elasticsearch/Observer/CategoryProductIndexer.php new file mode 100644 index 000000000000..fd2734bb713b --- /dev/null +++ b/app/code/Magento/Elasticsearch/Observer/CategoryProductIndexer.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Observer; + +use Magento\CatalogSearch\Model\Indexer\Fulltext\Processor; +use Magento\Elasticsearch\Model\Config; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; + +/** + * Checks if a category has changed products and depends on indexer configuration. + */ +class CategoryProductIndexer implements ObserverInterface +{ + /** + * @var Config + */ + private $config; + + /** + * @var Processor + */ + private $processor; + + /** + * @param Config $config + * @param Processor $processor + */ + public function __construct(Config $config, Processor $processor) + { + $this->processor = $processor; + $this->config = $config; + } + + /** + * @inheritdoc + */ + public function execute(Observer $observer): void + { + if (!$this->config->isElasticsearchEnabled()) { + return; + } + + $productIds = $observer->getEvent()->getProductIds(); + if (!empty($productIds) && $this->processor->isIndexerScheduled()) { + $this->processor->markIndexerAsInvalid(); + } + } +} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Adapter.php b/app/code/Magento/Elasticsearch/SearchAdapter/Adapter.php index 43b2bfe553a9..6f9ef552351f 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 bcfb7f5565b8..eeb48f805bcc 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Term.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Term.php @@ -8,10 +8,13 @@ use Magento\Framework\Search\Request\BucketInterface as RequestBucketInterface; use Magento\Framework\Search\Dynamic\DataProviderInterface; +/** + * Builder for term buckets. + */ class Term implements BucketBuilderInterface { /** - * {@inheritdoc} + * @inheritdoc */ public function build( RequestBucketInterface $bucket, @@ -19,13 +22,15 @@ public function build( array $queryResult, DataProviderInterface $dataProvider ) { + $buckets = $queryResult['aggregations'][$bucket->getName()]['buckets'] ?? []; $values = []; - foreach ($queryResult['aggregations'][$bucket->getName()]['buckets'] as $resultBucket) { + foreach ($buckets as $resultBucket) { $values[$resultBucket['key']] = [ 'value' => $resultBucket['key'], 'count' => $resultBucket['doc_count'], ]; } + return $values; } } diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php b/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php index 0add517ba94a..496a77e4c5ac 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php @@ -9,6 +9,8 @@ use Magento\Elasticsearch\SearchAdapter\QueryContainer; /** + * Elastic search data provider + * * @api * @since 100.1.0 */ @@ -120,7 +122,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc * @since 100.1.0 */ public function getRange() @@ -129,7 +131,7 @@ public function getRange() } /** - * {@inheritdoc} + * @inheritdoc * @since 100.1.0 */ public function getAggregations(\Magento\Framework\Search\Dynamic\EntityStorage $entityStorage) @@ -168,7 +170,7 @@ public function getAggregations(\Magento\Framework\Search\Dynamic\EntityStorage } /** - * {@inheritdoc} + * @inheritdoc * @since 100.1.0 */ public function getInterval( @@ -191,7 +193,7 @@ public function getInterval( } /** - * {@inheritdoc} + * @inheritdoc * @since 100.1.0 */ public function getAggregation( @@ -209,7 +211,8 @@ public function getAggregation( 'prices' => [ 'histogram' => [ 'field' => $fieldName, - 'interval' => $range, + 'interval' => (float)$range, + 'min_doc_count' => 1, ], ], ]; @@ -217,7 +220,7 @@ public function getAggregation( $queryResult = $this->connectionManager->getConnection() ->query($query); foreach ($queryResult['aggregations']['prices']['buckets'] as $bucket) { - $key = intval($bucket['key'] / $range + 1); + $key = (int)($bucket['key'] / $range + 1); $result[$key] = $bucket['doc_count']; } @@ -225,7 +228,7 @@ public function getAggregation( } /** - * {@inheritdoc} + * @inheritdoc * @since 100.1.0 */ public function prepareData($range, array $dbRanges) diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder.php index cf75366b9b25..d0aaa4b3dd57 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 f1c3451482ba..afd383c13421 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Match.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Match.php @@ -5,11 +5,18 @@ */ namespace Magento\Elasticsearch\SearchAdapter\Query\Builder; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ResolverInterface as TypeResolver; +use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerPool; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Search\Request\Query\BoolExpression; use Magento\Framework\Search\Request\QueryInterface as RequestQueryInterface; use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface; use Magento\Framework\Search\Adapter\Preprocessor\PreprocessorInterface; +/** + * Builder for match query. + */ class Match implements QueryInterface { /** @@ -23,24 +30,53 @@ class Match implements QueryInterface private $fieldMapper; /** + * @deprecated + * @see \Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\TextTransformer * @var PreprocessorInterface[] */ protected $preprocessorContainer; + /** + * @var AttributeProvider + */ + private $attributeProvider; + + /** + * @var TypeResolver + */ + private $fieldTypeResolver; + + /** + * @var ValueTransformerPool + */ + private $valueTransformerPool; + /** * @param FieldMapperInterface $fieldMapper * @param PreprocessorInterface[] $preprocessorContainer + * @param AttributeProvider|null $attributeProvider + * @param TypeResolver|null $fieldTypeResolver + * @param ValueTransformerPool|null $valueTransformerPool */ public function __construct( FieldMapperInterface $fieldMapper, - array $preprocessorContainer + array $preprocessorContainer, + AttributeProvider $attributeProvider = null, + TypeResolver $fieldTypeResolver = null, + ValueTransformerPool $valueTransformerPool = null ) { $this->fieldMapper = $fieldMapper; $this->preprocessorContainer = $preprocessorContainer; + $this->attributeProvider = $attributeProvider ?? ObjectManager::getInstance() + ->get(AttributeProvider::class); + $this->fieldTypeResolver = $fieldTypeResolver ?? ObjectManager::getInstance() + ->get(TypeResolver::class); + $this->valueTransformerPool = $valueTransformerPool ?? ObjectManager::getInstance() + ->get(ValueTransformerPool::class); } /** - * {@inheritdoc} + * @inheritdoc */ public function build(array $selectQuery, RequestQueryInterface $requestQuery, $conditionType) { @@ -61,16 +97,14 @@ public function build(array $selectQuery, RequestQueryInterface $requestQuery, $ } /** + * Prepare query. + * * @param string $queryValue * @param string $conditionType * @return array */ protected function prepareQuery($queryValue, $conditionType) { - $queryValue = $this->escape($queryValue); - foreach ($this->preprocessorContainer as $preprocessor) { - $queryValue = $preprocessor->process($queryValue); - } $condition = $conditionType === BoolExpression::QUERY_CONDITION_NOT ? self::QUERY_CONDITION_MUST_NOT : $conditionType; return [ @@ -99,10 +133,24 @@ protected function buildQueries(array $matches, array $queryValue) // Checking for quoted phrase \"phrase test\", trim escaped surrounding quotes if found $count = 0; - $value = preg_replace('#^\\\\"(.*)\\\\"$#m', '$1', $queryValue['value'], -1, $count); + $value = preg_replace('#^"(.*)"$#m', '$1', $queryValue['value'], -1, $count); $condition = ($count) ? 'match_phrase' : 'match'; + $transformedTypes = []; foreach ($matches as $match) { + $attributeAdapter = $this->attributeProvider->getByAttributeCode($match['field']); + $fieldType = $this->fieldTypeResolver->getFieldType($attributeAdapter); + $valueTransformer = $this->valueTransformerPool->get($fieldType ?? 'text'); + $valueTransformerHash = \spl_object_hash($valueTransformer); + if (!isset($transformedTypes[$valueTransformerHash])) { + $transformedTypes[$valueTransformerHash] = $valueTransformer->transform($value); + } + $transformedValue = $transformedTypes[$valueTransformerHash]; + if (null === $transformedValue) { + //Value is incompatible with this field type. + continue; + } + $resolvedField = $this->fieldMapper->getFieldName( $match['field'], ['type' => FieldMapperInterface::TYPE_QUERY] @@ -112,8 +160,8 @@ protected function buildQueries(array $matches, array $queryValue) 'body' => [ $condition => [ $resolvedField => [ - 'query' => $value, - 'boost' => isset($match['boost']) ? $match['boost'] : 1, + 'query' => $transformedValue, + 'boost' => $match['boost'] ?? 1, ], ], ], @@ -124,18 +172,15 @@ protected function buildQueries(array $matches, array $queryValue) } /** - * Cut trailing plus or minus sign, and @ symbol, using of which causes InnoDB to report a syntax error. - * @link https://dev.mysql.com/doc/refman/5.7/en/fulltext-boolean.html Fulltext-boolean search docs. - * * Escape a value for special query characters such as ':', '(', ')', '*', '?', etc. * + * @deprecated + * @see \Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\TextTransformer * @param string $value * @return string */ protected function escape($value) { - $value = preg_replace('/@+|[@+-]+$/', '', $value); - $pattern = '/(\+|-|&&|\|\||!|\(|\)|\{|}|\[|]|\^|"|~|\*|\?|:|\\\)/'; $replace = '\\\$1'; diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort.php new file mode 100644 index 000000000000..5ccf202e3812 --- /dev/null +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort.php @@ -0,0 +1,106 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Elasticsearch\SearchAdapter\Query\Builder; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface + as FieldNameResolver; +use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface; +use Magento\Framework\Search\RequestInterface; + +/** + * Sort builder. + */ +class Sort +{ + /** + * List of fields that need to skipp by default. + */ + private const DEFAULT_SKIPPED_FIELDS = [ + 'entity_id', + ]; + + /** + * Default mapping for special fields. + */ + private const DEFAULT_MAP = [ + 'relevance' => '_score', + ]; + + /** + * @var AttributeProvider + */ + private $attributeAdapterProvider; + + /** + * @var FieldNameResolver + */ + private $fieldNameResolver; + + /** + * @var array + */ + private $skippedFields; + + /** + * @var array + */ + private $map; + + /** + * @param AttributeProvider $attributeAdapterProvider + * @param FieldNameResolver $fieldNameResolver + * @param array $skippedFields + * @param array $map + */ + public function __construct( + AttributeProvider $attributeAdapterProvider, + FieldNameResolver $fieldNameResolver, + array $skippedFields = [], + array $map = [] + ) { + $this->attributeAdapterProvider = $attributeAdapterProvider; + $this->fieldNameResolver = $fieldNameResolver; + $this->skippedFields = array_merge(self::DEFAULT_SKIPPED_FIELDS, $skippedFields); + $this->map = array_merge(self::DEFAULT_MAP, $map); + } + + /** + * Prepare sort. + * + * @param RequestInterface $request + * @return array + */ + public function getSort(RequestInterface $request) + { + $sorts = []; + foreach ($request->getSort() as $item) { + if (in_array($item['field'], $this->skippedFields)) { + continue; + } + $attribute = $this->attributeAdapterProvider->getByAttributeCode($item['field']); + $fieldName = $this->fieldNameResolver->getFieldName($attribute); + if (isset($this->map[$fieldName])) { + $fieldName = $this->map[$fieldName]; + } + if ($attribute->isSortable() && !($attribute->isFloatType() || $attribute->isIntegerType())) { + $suffix = $this->fieldNameResolver->getFieldName( + $attribute, + ['type' => FieldMapperInterface::TYPE_SORT] + ); + $fieldName .= '.' . $suffix; + } + $sorts[] = [ + $fieldName => [ + 'order' => strtolower($item['direction']) + ] + ]; + } + + return $sorts; + } +} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/DateTransformer.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/DateTransformer.php new file mode 100644 index 000000000000..49eca6e9d82a --- /dev/null +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/DateTransformer.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer; + +use Magento\Elasticsearch\Model\Adapter\FieldType\Date; +use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerInterface; + +/** + * Value transformer for date type fields. + */ +class DateTransformer implements ValueTransformerInterface +{ + /** + * @var Date + */ + private $dateFieldType; + + /** + * @param Date $dateFieldType + */ + public function __construct(Date $dateFieldType) + { + $this->dateFieldType = $dateFieldType; + } + + /** + * @inheritdoc + */ + public function transform(string $value): ?string + { + try { + $formattedDate = $this->dateFieldType->formatDate(null, $value); + } catch (\Exception $e) { + $formattedDate = null; + } + + return $formattedDate; + } +} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/FloatTransformer.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/FloatTransformer.php new file mode 100644 index 000000000000..5e330076d3df --- /dev/null +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/FloatTransformer.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer; + +use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerInterface; + +/** + * Value transformer for float type fields. + */ +class FloatTransformer implements ValueTransformerInterface +{ + /** + * @inheritdoc + */ + public function transform(string $value): ?float + { + return \is_numeric($value) ? (float) $value : null; + } +} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/IntegerTransformer.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/IntegerTransformer.php new file mode 100644 index 000000000000..0846ff3a9bd8 --- /dev/null +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/IntegerTransformer.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer; + +use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerInterface; + +/** + * Value transformer for integer type fields. + */ +class IntegerTransformer implements ValueTransformerInterface +{ + /** + * @inheritdoc + */ + public function transform(string $value): ?int + { + return \is_numeric($value) ? (int) $value : null; + } +} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/TextTransformer.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/TextTransformer.php new file mode 100644 index 000000000000..68bec2580f62 --- /dev/null +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/TextTransformer.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer; + +use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerInterface; +use Magento\Framework\Search\Adapter\Preprocessor\PreprocessorInterface; + +/** + * Value transformer for fields with text types. + */ +class TextTransformer implements ValueTransformerInterface +{ + /** + * @var PreprocessorInterface[] + */ + private $preprocessors; + + /** + * @param PreprocessorInterface[] $preprocessors + */ + public function __construct(array $preprocessors = []) + { + foreach ($preprocessors as $preprocessor) { + if (!$preprocessor instanceof PreprocessorInterface) { + throw new \InvalidArgumentException( + \sprintf('"%s" is not a instance of ValueTransformerInterface.', get_class($preprocessor)) + ); + } + } + + $this->preprocessors = $preprocessors; + } + + /** + * @inheritdoc + */ + public function transform(string $value): string + { + $value = $this->escape($value); + foreach ($this->preprocessors as $preprocessor) { + $value = $preprocessor->process($value); + } + + return $value; + } + + /** + * Escape a value for special query characters such as ':', '(', ')', '*', '?', etc. + * + * @param string $value + * @return string + */ + private function escape(string $value): string + { + $pattern = '/(\+|-|&&|\|\||!|\(|\)|\{|}|\[|]|\^|"|~|\*|\?|:|\\\)/'; + $replace = '\\\$1'; + + return preg_replace($pattern, $replace, $value); + } +} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformerInterface.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformerInterface.php new file mode 100644 index 000000000000..c84ddc69cc7a --- /dev/null +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformerInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\SearchAdapter\Query; + +/** + * Value transformer of search term for matching with ES field types. + */ +interface ValueTransformerInterface +{ + /** + * Transform value according to field type. + * + * @param string $value + * @return mixed + */ + public function transform(string $value); +} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformerPool.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformerPool.php new file mode 100644 index 000000000000..11a35d79ce1f --- /dev/null +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformerPool.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\SearchAdapter\Query; + +/** + * Pool of value transformers. + */ +class ValueTransformerPool +{ + /** + * @var ValueTransformerInterface[] + */ + private $transformers; + + /** + * @param ValueTransformerInterface[] $valueTransformers + */ + public function __construct(array $valueTransformers = []) + { + foreach ($valueTransformers as $valueTransformer) { + if (!$valueTransformer instanceof ValueTransformerInterface) { + throw new \InvalidArgumentException( + \sprintf('"%s" is not a instance of ValueTransformerInterface.', get_class($valueTransformer)) + ); + } + } + + $this->transformers = $valueTransformers; + } + + /** + * Get value transformer related to field type. + * + * @param string $fieldType + * @return ValueTransformerInterface + */ + public function get(string $fieldType): ValueTransformerInterface + { + return $this->transformers[$fieldType] ?? $this->transformers['default']; + } +} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/ResponseFactory.php b/app/code/Magento/Elasticsearch/SearchAdapter/ResponseFactory.php index 33fda48f4af5..0813975ac9a4 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/Mftf/Test/ProductQuickSearchUsingElasticSearchTest.xml b/app/code/Magento/Elasticsearch/Test/Mftf/Test/ProductQuickSearchUsingElasticSearchTest.xml new file mode 100644 index 000000000000..d8ce091fba76 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Test/Mftf/Test/ProductQuickSearchUsingElasticSearchTest.xml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="ProductQuickSearchUsingElasticSearchTest"> + <annotations> + <features value="Search"/> + <stories value="Quick Search of products on Storefront when ES 5.0+ is enabled"/> + <title value="Product quick search doesn't throw exception after ES is chosen as search engine"/> + <description value="Verify no elastic search exception is thrown when searching for product before catalogsearch reindexing"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-94995"/> + <group value="Catalog"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="categoryFirst"/> + <createData entity="SimpleProduct" stepKey="simpleProduct1"> + <requiredEntity createDataKey="categoryFirst"/> + </createData> + </before> + + <after> + <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="categoryFirst" stepKey="deleteCategory"/> + <actionGroup ref="ResetSearchEngineConfiguration" stepKey="resetCatalogSearchConfiguration"/> + <actionGroup ref="updateIndexerOnSave" stepKey="resetIndexerBackToOriginalState"> + <argument name="indexerName" value="catalogsearch_fulltext"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <comment userInput="Change Catalog search engine option to Elastic Search 5.0+" stepKey="chooseElasticSearch5"/> + <actionGroup ref="ChooseElasticSearchAsSearchEngine" stepKey="chooseES5"/> + <actionGroup ref="ClearPageCacheActionGroup" stepKey="clearing"/> + <actionGroup ref="updateIndexerBySchedule" stepKey="updateAnIndexerBySchedule"> + <argument name="indexerName" value="catalogsearch_fulltext"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + <!--Navigate to storefront and do a quick search for the product --> + <comment userInput="Navigate to Storefront to check if quick search works" stepKey="commentCheckQuickSearch" /> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToHomePage"/> + + <waitForPageLoad stepKey="waitForHomePageToLoad" time="30"/> + <fillField userInput="Simple" selector="{{StorefrontQuickSearchSection.searchPhrase}}" stepKey="fillSearchBar"/> + <waitForPageLoad stepKey="wait2" time="30"/> + <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> + <seeInCurrentUrl url="{{StorefrontCatalogSearchPage.url}}" stepKey="checkUrl"/> + <seeInTitle userInput="Search results for: 'Simple'" stepKey="assertQuickSearchTitle"/> + <see userInput="Search results for: 'Simple'" selector="{{StorefrontCatalogSearchMainSection.SearchTitle}}" stepKey="assertQuickSearchName"/> + <comment userInput="End of searching products" stepKey="endOfSearchingProducts"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin2"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/IndexResolverTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/IndexResolverTest.php new file mode 100644 index 000000000000..98284e49ad85 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/IndexResolverTest.php @@ -0,0 +1,165 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Test\Unit\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\ConverterInterface; +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\ConverterInterface + as FieldTypeConverterInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ResolverInterface + as FieldTypeResolver; +use Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\IndexResolver; + +/** + * @SuppressWarnings(PHPMD) + */ +class IndexResolverTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var IndexResolver + */ + private $resolver; + + /** + * @var ConverterInterface + */ + private $converter; + + /** + * @var FieldTypeConverterInterface + */ + private $fieldTypeConverter; + + /** + * @var FieldTypeResolver + */ + private $fieldTypeResolver; + + /** + * Set up test environment + * + * @return void + */ + protected function setUp() + { + $this->converter = $this->getMockBuilder(ConverterInterface::class) + ->disableOriginalConstructor() + ->setMethods(['convert']) + ->getMockForAbstractClass(); + $this->fieldTypeConverter = $this->getMockBuilder(FieldTypeConverterInterface::class) + ->disableOriginalConstructor() + ->setMethods(['convert']) + ->getMockForAbstractClass(); + $this->fieldTypeResolver = $this->getMockBuilder(FieldTypeResolver::class) + ->disableOriginalConstructor() + ->setMethods(['getFieldType']) + ->getMockForAbstractClass(); + $objectManager = new ObjectManagerHelper($this); + + $this->resolver = $objectManager->getObject( + IndexResolver::class, + [ + 'converter' => $this->converter, + 'fieldTypeConverter' => $this->fieldTypeConverter, + 'fieldTypeResolver' => $this->fieldTypeResolver, + ] + ); + } + + /** + * @dataProvider getFieldIndexProvider + * @param $isSearchable + * @param $isAlwaysIndexable + * @param $isComplexType + * @param $isIntegerType + * @param $isBooleanType + * @param $isUserDefined + * @param $isFloatType + * @param $serviceFieldType + * @param $expected + * @return void + */ + public function testGetFieldName( + $isSearchable, + $isAlwaysIndexable, + $isComplexType, + $isIntegerType, + $isBooleanType, + $isUserDefined, + $isFloatType, + $serviceFieldType, + $expected + ) { + $this->converter->expects($this->any()) + ->method('convert') + ->willReturn('something'); + $this->fieldTypeResolver->expects($this->any()) + ->method('getFieldType') + ->willReturn($serviceFieldType); + $this->fieldTypeConverter->expects($this->any()) + ->method('convert') + ->willReturn('string'); + $attributeMock = $this->getMockBuilder(AttributeAdapter::class) + ->disableOriginalConstructor() + ->setMethods([ + 'isSearchable', + 'isAlwaysIndexable', + 'isComplexType', + 'isIntegerType', + 'isBooleanType', + 'isUserDefined', + 'isFloatType', + ]) + ->getMock(); + $attributeMock->expects($this->any()) + ->method('isSearchable') + ->willReturn($isSearchable); + $attributeMock->expects($this->any()) + ->method('isAlwaysIndexable') + ->willReturn($isAlwaysIndexable); + $attributeMock->expects($this->any()) + ->method('isComplexType') + ->willReturn($isComplexType); + $attributeMock->expects($this->any()) + ->method('isIntegerType') + ->willReturn($isIntegerType); + $attributeMock->expects($this->any()) + ->method('isBooleanType') + ->willReturn($isBooleanType); + $attributeMock->expects($this->any()) + ->method('isUserDefined') + ->willReturn($isUserDefined); + $attributeMock->expects($this->any()) + ->method('isFloatType') + ->willReturn($isFloatType); + + $this->assertEquals( + $expected, + $this->resolver->getFieldIndex($attributeMock) + ); + } + + /** + * @return array + */ + public function getFieldIndexProvider() + { + return [ + [true, true, true, true, true, true, true, 'string', null], + [false, false, true, false, false, false, false, 'string', 'something'], + [false, false, true, false, false, false, false, 'string', 'something'], + [false, false, false, false, false, false, false, 'string', 'something'], + [false, false, false, false, false, false, false, 'int', null], + [false, false, true, true, false, true, false, 'string', 'something'], + [false, false, true, false, true, true, false, 'string', 'something'], + [false, false, true, false, true, false, false, 'string', null], + [false, false, true, false, true, true, true, 'string', null], + ]; + } +} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerTypeTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerTypeTest.php new file mode 100644 index 000000000000..39155e4f4fe0 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerTypeTest.php @@ -0,0 +1,105 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace +Magento\Elasticsearch\Test\Unit\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface + as FieldTypeConverterInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\IntegerType; + +/** + * @SuppressWarnings(PHPMD) + */ +class IntegerTypeTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var IntegerType + */ + private $resolver; + + /** + * @var FieldTypeConverterInterface + */ + private $fieldTypeConverter; + + /** + * Set up test environment + * + * @return void + */ + protected function setUp() + { + $this->fieldTypeConverter = $this->getMockBuilder(FieldTypeConverterInterface::class) + ->disableOriginalConstructor() + ->setMethods(['convert']) + ->getMockForAbstractClass(); + + $objectManager = new ObjectManagerHelper($this); + + $this->resolver = $objectManager->getObject( + IntegerType::class, + [ + 'fieldTypeConverter' => $this->fieldTypeConverter, + ] + ); + } + + /** + * @dataProvider getFieldTypeProvider + * @param $attributeCode + * @param $isIntegerType + * @param $isBooleanType + * @param $isUserDefined + * @param $expected + * @return void + */ + public function testGetFieldType($attributeCode, $isIntegerType, $isBooleanType, $isUserDefined, $expected) + { + $attributeMock = $this->getMockBuilder(AttributeAdapter::class) + ->disableOriginalConstructor() + ->setMethods(['getAttributeCode', 'isIntegerType', 'isBooleanType', 'isUserDefined']) + ->getMock(); + $attributeMock->expects($this->any()) + ->method('getAttributeCode') + ->willReturn($attributeCode); + $attributeMock->expects($this->any()) + ->method('isIntegerType') + ->willReturn($isIntegerType); + $attributeMock->expects($this->any()) + ->method('isBooleanType') + ->willReturn($isBooleanType); + $attributeMock->expects($this->any()) + ->method('isUserDefined') + ->willReturn($isUserDefined); + $this->fieldTypeConverter->expects($this->any()) + ->method('convert') + ->willReturn('something'); + + $this->assertEquals( + $expected, + $this->resolver->getFieldType($attributeMock) + ); + } + + /** + * @return array + */ + public function getFieldTypeProvider() + { + return [ + ['category_ids', true, true, true, 'something'], + ['category_ids', false, false, false, 'something'], + ['type', true, false, false, 'something'], + ['type', false, true, false, 'something'], + ['type', true, true, true, ''], + ['type', false, false, true, ''], + ]; + } +} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/KeywordTypeTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/KeywordTypeTest.php new file mode 100644 index 000000000000..0e33498a16fb --- /dev/null +++ b/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/KeywordTypeTest.php @@ -0,0 +1,106 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace +Magento\Elasticsearch\Test\Unit\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface + as FieldTypeConverterInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\KeywordType; + +/** + * @SuppressWarnings(PHPMD) + */ +class KeywordTypeTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var KeywordType + */ + private $resolver; + + /** + * @var FieldTypeConverterInterface + */ + private $fieldTypeConverter; + + /** + * Set up test environment + * + * @return void + */ + protected function setUp() + { + $this->fieldTypeConverter = $this->getMockBuilder(FieldTypeConverterInterface::class) + ->disableOriginalConstructor() + ->setMethods(['convert']) + ->getMockForAbstractClass(); + + $objectManager = new ObjectManagerHelper($this); + + $this->resolver = $objectManager->getObject( + KeywordType::class, + [ + 'fieldTypeConverter' => $this->fieldTypeConverter, + ] + ); + } + + /** + * @dataProvider getFieldTypeProvider + * @param $isComplexType + * @param $isSearchable + * @param $isAlwaysIndexable + * @param $isFilterable + * @param $expected + * @return void + */ + public function testGetFieldType($isComplexType, $isSearchable, $isAlwaysIndexable, $isFilterable, $expected) + { + $attributeMock = $this->getMockBuilder(AttributeAdapter::class) + ->disableOriginalConstructor() + ->setMethods(['isComplexType', 'isSearchable', 'isAlwaysIndexable', 'isFilterable']) + ->getMock(); + $attributeMock->expects($this->any()) + ->method('isComplexType') + ->willReturn($isComplexType); + $attributeMock->expects($this->any()) + ->method('isSearchable') + ->willReturn($isSearchable); + $attributeMock->expects($this->any()) + ->method('isAlwaysIndexable') + ->willReturn($isAlwaysIndexable); + $attributeMock->expects($this->any()) + ->method('isFilterable') + ->willReturn($isFilterable); + $this->fieldTypeConverter->expects($this->any()) + ->method('convert') + ->willReturn('something'); + + $this->assertEquals( + $expected, + $this->resolver->getFieldType($attributeMock) + ); + } + + /** + * @return array + */ + public function getFieldTypeProvider() + { + return [ + [true, true, true, true, 'something'], + [true, false, false, false, 'something'], + [true, false, false, true, 'something'], + [false, false, false, true, 'something'], + [false, false, false, false, ''], + [false, false, true, false, ''], + [false, true, false, false, ''], + ]; + } +} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Adapter/FieldMapper/ProductFieldMapperTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Adapter/FieldMapper/ProductFieldMapperTest.php deleted file mode 100644 index 6ee32a58698c..000000000000 --- a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Adapter/FieldMapper/ProductFieldMapperTest.php +++ /dev/null @@ -1,296 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Elasticsearch\Test\Unit\Elasticsearch5\Model\Adapter\FieldMapper; - -use Magento\Catalog\Api\Data\ProductAttributeInterface; -use Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldType; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; - -class ProductFieldMapperTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Elasticsearch\Model\Adapter\FieldMapper\ProductFieldMapper - */ - protected $mapper; - - /** - * @var \Magento\Eav\Model\Config|\PHPUnit_Framework_MockObject_MockObject - */ - protected $eavConfig; - - /** - * @var \Magento\Framework\Registry|\PHPUnit_Framework_MockObject_MockObject - */ - protected $coreRegistry; - - /** - * @var \Magento\Customer\Model\Session|\PHPUnit_Framework_MockObject_MockObject - */ - protected $customerSession; - - /** - * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $storeManager; - - /** - * @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute|\PHPUnit_Framework_MockObject_MockObject - */ - protected $eavAttributeResource; - - /** - * @var FieldType|\PHPUnit_Framework_MockObject_MockObject - */ - protected $fieldType; - - /** - * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $store; - - /** - * Set up test environment - * - * @return void - */ - protected function setUp() - { - $this->eavConfig = $this->getMockBuilder(\Magento\Eav\Model\Config::class) - ->disableOriginalConstructor() - ->setMethods(['getEntityType', 'getAttribute', 'getEntityAttributeCodes']) - ->getMock(); - - $this->fieldType = $this->getMockBuilder(FieldType::class) - ->disableOriginalConstructor() - ->setMethods(['getFieldType']) - ->getMock(); - - $this->customerSession = $this->getMockBuilder(\Magento\Customer\Model\Session::class) - ->disableOriginalConstructor() - ->setMethods(['getCustomerGroupId']) - ->getMock(); - - $this->storeManager = $this->storeManager = $this->getMockForAbstractClass( - \Magento\Store\Model\StoreManagerInterface::class, - [], - '', - false - ); - - $this->store = $this->getMockForAbstractClass( - \Magento\Store\Api\Data\StoreInterface::class, - [], - '', - false, - false, - true, - ['getWebsiteId', 'getRootCategoryId'] - ); - - $this->coreRegistry = $this->createMock(\Magento\Framework\Registry::class); - - $objectManager = new ObjectManagerHelper($this); - - $this->eavAttributeResource = $this->createPartialMock( - \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class, - [ - '__wakeup', - 'getBackendType', - 'getFrontendInput' - ] - ); - - $this->mapper = $objectManager->getObject( - \Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\ProductFieldMapper::class, - [ - 'eavConfig' => $this->eavConfig, - 'storeManager' => $this->storeManager, - 'fieldType' => $this->fieldType, - 'customerSession' => $this->customerSession, - 'coreRegistry' => $this->coreRegistry - ] - ); - } - - /** - * @dataProvider attributeCodeProvider - * @param string $attributeCode - * @param string $fieldName - * @param string $fieldType - * @param array $context - * - * @return void - */ - public function testGetFieldName($attributeCode, $fieldName, $fieldType, $context = []) - { - $attributeMock = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class) - ->setMethods(['getBackendType', 'getFrontendInput', 'getAttribute']) - ->disableOriginalConstructor() - ->getMock(); - - $this->customerSession->expects($this->any()) - ->method('getCustomerGroupId') - ->willReturn('0'); - - $this->storeManager->expects($this->any()) - ->method('getStore') - ->willReturn($this->store); - $this->store->expects($this->any()) - ->method('getWebsiteId') - ->willReturn('1'); - $this->store->expects($this->any()) - ->method('getRootCategoryId') - ->willReturn('1'); - - $this->eavConfig->expects($this->any())->method('getAttribute') - ->with(ProductAttributeInterface::ENTITY_TYPE_CODE, $attributeCode) - ->willReturn($attributeMock); - - $attributeMock->expects($this->any())->method('getFrontendInput') - ->will($this->returnValue('select')); - - $this->fieldType->expects($this->any())->method('getFieldType') - ->with($attributeMock) - ->willReturn($fieldType); - - $this->assertEquals( - $fieldName, - $this->mapper->getFieldName($attributeCode, $context) - ); - } - - /** - * @return void - */ - public function testGetFieldNameWithoutAttribute() - { - $this->eavConfig->expects($this->any())->method('getAttribute') - ->with(ProductAttributeInterface::ENTITY_TYPE_CODE, 'attr1') - ->willReturn(''); - - $this->assertEquals( - 'attr1', - $this->mapper->getFieldName('attr1', []) - ); - } - - /** - * @dataProvider attributeProvider - * @param string $attributeCode - * @param string $inputType - * @param array $searchAttributes - * @param array $expected - * @return void - */ - public function testGetAllAttributesTypes($attributeCode, $inputType, $searchAttributes, $expected) - { - $attributeMock = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->eavConfig->expects($this->any())->method('getEntityAttributeCodes') - ->with(ProductAttributeInterface::ENTITY_TYPE_CODE) - ->willReturn([$attributeCode]); - - $this->eavConfig->expects($this->any())->method('getAttribute') - ->with(ProductAttributeInterface::ENTITY_TYPE_CODE, $attributeCode) - ->willReturn($attributeMock); - - $this->fieldType->expects($this->once())->method('getFieldType')->willReturn(FieldType::ES_DATA_TYPE_INT); - - $attributeMock->expects($this->any()) - ->method('getIsSearchable') - ->willReturn($searchAttributes['searchable']); - $attributeMock->expects($this->any()) - ->method('getIsFilterable') - ->willReturn($searchAttributes['filterable']); - $attributeMock->expects($this->any()) - ->method('getIsFilterableInSearch') - ->willReturn($searchAttributes['filterableInSearch']); - $attributeMock->expects($this->any()) - ->method('getIsVisibleInAdvancedSearch') - ->willReturn($searchAttributes['advSearch']); - - $attributeMock->expects($this->any())->method('getFrontendInput') - ->will($this->returnValue($inputType)); - - $this->assertEquals( - $expected, - $this->mapper->getAllAttributesTypes() - ); - } - - /** - * @return array - */ - public function attributeCodeProvider() - { - return [ - ['id', 'id', 'text'], - ['status', 'status', 'text'], - ['status', 'status_value', 'text', ['type'=>'default']], - ['price', 'price_0_1', 'text', ['type'=>'default']], - ['position', 'position_category_1', 'text', ['type'=>'default']], - ['price', 'price_2_3', 'text', ['type'=>'default', 'customerGroupId'=>'2', 'websiteId'=>'3']], - ['position', 'position_category_3', 'text', ['type'=>'default', 'categoryId'=>'3']], - ['color', 'color_value', 'text', ['type'=>'text']], - ['description', 'sort_description', 'text', ['type'=>'some']], - ['*', '_all', 'text', ['type'=>'text']], - ['description', 'description_value', 'text', ['type'=>'text']], - ]; - } - - /** - * @return array - */ - public function attributeProvider() - { - return [ - [ - 'category_ids', - 'select', - ['searchable' => false, 'filterable' => false, 'filterableInSearch' => false, 'advSearch' => false], - ['category_ids' => ['type' => 'keyword'], 'category_ids_value' => ['type' => 'text']] - ], - [ - 'attr_code', - 'text', - ['searchable' => false, 'filterable' => false, 'filterableInSearch' => false, 'advSearch' => false], - ['attr_code' => ['type' => 'integer']] - ], - [ - 'attr_code', - 'text', - ['searchable' => '0', 'filterable' => '0', 'filterableInSearch' => '0', 'advSearch' => '0'], - ['attr_code' => ['type' => 'integer']] - ], - [ - 'attr_code', - 'text', - ['searchable' => true, 'filterable' => false, 'filterableInSearch' => false, 'advSearch' => false], - ['attr_code' => ['type' => 'integer']] - ], - [ - 'attr_code', - 'text', - ['searchable' => '1', 'filterable' => '0', 'filterableInSearch' => '0', 'advSearch' => '0'], - ['attr_code' => ['type' => 'integer']] - ], - [ - 'attr_code', - 'text', - ['searchable' => false, 'filterable' => false, 'filterableInSearch' => false, 'advSearch' => true], - ['attr_code' => ['type' => 'integer']] - ], - [ - 'attr_code', - 'text', - ['searchable' => '0', 'filterable' => '0', 'filterableInSearch' => '1', 'advSearch' => '0'], - ['attr_code' => ['type' => 'integer']] - ], - ]; - } -} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Adapter/FieldTypeTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Adapter/FieldTypeTest.php deleted file mode 100644 index 63c993e27c9a..000000000000 --- a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Adapter/FieldTypeTest.php +++ /dev/null @@ -1,107 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Elasticsearch\Test\Unit\Elasticsearch5\Model\Adapter; - -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -use PHPUnit_Framework_MockObject_MockObject as MockObject; - -class FieldTypeTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Elasticsearch\Model\Adapter\FieldType - */ - protected $type; - - /** - * @var \Magento\Eav\Model\Config|MockObject - */ - protected $eavConfig; - - /** - * @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute|\PHPUnit_Framework_MockObject_MockObject - */ - protected $eavAttributeResource; - - /** - * Set up test environment. - * - * @return void - */ - protected function setUp() - { - $this->eavConfig = $this->getMockBuilder(\Magento\Eav\Model\Config::class) - ->disableOriginalConstructor() - ->setMethods(['getEntityType', 'getAttribute', 'getEntityAttributeCodes']) - ->getMock(); - - $objectManager = new ObjectManagerHelper($this); - - $this->eavAttributeResource = $this->createPartialMock( - \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class, - [ - '__wakeup', - 'getBackendType', - 'getFrontendInput' - ] - ); - - $this->type = $objectManager->getObject( - \Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldType::class, - [ - 'eavConfig' => $this->eavConfig, - ] - ); - } - - /** - * Test getFieldType() method. - * - * @dataProvider attributeTypesProvider - * @param string $attributeCode - * @param string $backendType - * @param string $frontendType - * @param string $expectedFieldType - * @return void - */ - public function testGetFieldType($attributeCode, $backendType, $frontendType, $expectedFieldType) - { - $attributeMock = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class) - ->setMethods(['getBackendType', 'getFrontendInput', 'getAttributeCode']) - ->disableOriginalConstructor() - ->getMock(); - - $attributeMock->expects($this->any())->method('getBackendType') - ->will($this->returnValue($backendType)); - - $attributeMock->expects($this->any())->method('getFrontendInput') - ->will($this->returnValue($frontendType)); - - $attributeMock->expects($this->any())->method('getAttributeCode') - ->will($this->returnValue($attributeCode)); - - $this->assertEquals($expectedFieldType, $this->type->getFieldType($attributeMock)); - } - - /** - * @return array - */ - public static function attributeTypesProvider() - { - return [ - ['attr1', 'static', 'select', 'integer'], - ['attr1', 'static', 'text', 'text'], - ['attr1', 'timestamp', 'select', 'date'], - ['attr1', 'datetime', 'text', 'date'], - ['attr1', 'int', 'select', 'integer'], - ['attr1', 'decimal', 'text', 'float'], - ['attr1', 'varchar', 'select', 'text'], - ['attr1', 'array', 'multiselect', 'text'], - ['price', 'int', 'text', 'integer'], - ['tier_price', 'int', 'text', 'integer'], - ['tier_price', 'smallint', 'text', 'integer'], - ]; - } -} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php index 026d385da34d..8fbd183441b6 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php @@ -367,7 +367,7 @@ public function testAddFieldsMapping() 'match_mapping_type' => 'string', 'mapping' => [ 'type' => 'text', - 'index' => 'no', + 'index' => false, ], ], ], @@ -434,7 +434,7 @@ public function testAddFieldsMappingFailure() 'match_mapping_type' => 'string', 'mapping' => [ 'type' => 'text', - 'index' => 'no', + 'index' => false, ], ], ], diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php index ca478550fa68..cfa048fc26c9 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php @@ -8,7 +8,10 @@ use Magento\AdvancedSearch\Model\Adapter\DataMapper\AdditionalFieldsProviderInterface; use Magento\Catalog\Model\ResourceModel\Eav\Attribute; use Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider; +use Magento\Eav\Api\Data\AttributeOptionInterface; use Magento\Elasticsearch\Model\Adapter\BatchDataMapper\ProductDataMapper; +use Magento\Elasticsearch\Model\Adapter\Document\Builder; +use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface; use Magento\Elasticsearch\Model\Adapter\FieldType\Date; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; @@ -59,31 +62,13 @@ class ProductDataMapperTest extends \PHPUnit\Framework\TestCase */ protected function setUp() { - $this->builderMock = $this->getMockBuilder(\Magento\Elasticsearch\Model\Adapter\Document\Builder::class) - ->setMethods(['addField', 'addFields', 'build']) - ->disableOriginalConstructor() - ->getMock(); + $this->builderMock = $this->createTestProxy(Builder::class); + $this->fieldMapperMock = $this->createMock(FieldMapperInterface::class); + $this->dataProvider = $this->createMock(DataProvider::class); + $this->attribute = $this->createMock(Attribute::class); + $this->additionalFieldsProvider = $this->createMock(AdditionalFieldsProviderInterface::class); + $this->dateFieldTypeMock = $this->createMock(Date::class); - $this->fieldMapperMock = $this->getMockBuilder(\Magento\Elasticsearch\Model\Adapter\FieldMapperInterface::class) - ->setMethods(['getFieldName', 'getAllAttributesTypes']) - ->disableOriginalConstructor() - ->getMock(); - - $this->dataProvider = $this->getMockBuilder(DataProvider::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->attribute = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->additionalFieldsProvider = $this->getMockBuilder(AdditionalFieldsProviderInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->dateFieldTypeMock = $this->getMockBuilder(Date::class) - ->disableOriginalConstructor() - ->getMock(); $objectManager = new ObjectManagerHelper($this); $this->model = $objectManager->getObject( ProductDataMapper::class, @@ -97,258 +82,348 @@ protected function setUp() ); } + /** + * @return void + */ public function testGetMapAdditionalFieldsOnly() { - $productId = 42; $storeId = 1; + $productId = 42; $additionalFields = ['some data']; - $this->builderMock->expects($this->once())->method('addField')->with('store_id', $storeId); + $this->builderMock->expects($this->once()) + ->method('addField') + ->with('store_id', $storeId); - $this->builderMock->expects($this->any())->method('addFields') + $this->builderMock->expects($this->any()) + ->method('addFields') ->withConsecutive([$additionalFields]) - ->will( - $this->returnSelf() - ); - $this->builderMock->expects($this->any())->method('build')->will( - $this->returnValue([]) - ); - $this->additionalFieldsProvider->expects($this->once())->method('getFields') + ->will($this->returnSelf()); + $this->builderMock->expects($this->any()) + ->method('build') + ->will($this->returnValue([])); + $this->additionalFieldsProvider->expects($this->once()) + ->method('getFields') ->with([$productId], $storeId) ->willReturn([$productId => $additionalFields]); - $this->assertEquals( - [$productId], - array_keys($this->model->map([$productId => []], $storeId, [])) - ); + $documents = $this->model->map([$productId => []], $storeId, []); + $this->assertEquals([$productId], array_keys($documents)); } + /** + * @return void + */ public function testGetMapEmptyData() { $storeId = 1; + $this->builderMock->expects($this->never())->method('addField'); $this->builderMock->expects($this->never())->method('build'); $this->additionalFieldsProvider->expects($this->once()) ->method('getFields') - ->with([], $storeId)->willReturn([]); + ->with([], $storeId) + ->willReturn([]); - $this->assertEquals( - [], - $this->model->map([], $storeId, []) - ); - } - - public function testGetMapWithExcludedAttribute() - { - $productId = 42; - $storeId = 1; - $productAttributeData = ['price' => 42]; - $attributeCode = 'price'; - $returnAttributeData = ['store_id' => $storeId]; - - $this->dataProvider->expects($this->any())->method('getSearchableAttribute') - ->with($attributeCode) - ->willReturn($this->getAttribute($attributeCode, [ - 'value' => 42, - 'backendType' => 'int', - 'frontendInput' => 'int', - 'options' => [] - ])); - - $this->fieldMapperMock->expects($this->never())->method('getFieldName'); - $this->builderMock->expects($this->any()) - ->method('addField') - ->with('store_id', $storeId); - $this->builderMock->expects($this->once())->method('build')->willReturn($returnAttributeData); - - $this->additionalFieldsProvider->expects($this->once())->method('getFields')->willReturn([]); - $this->assertEquals( - [$productId => $returnAttributeData], - $this->model->map([$productId => $productAttributeData], $storeId, []) - ); + $documents = $this->model->map([], $storeId, []); + $this->assertEquals([], $documents); } /** * @param int $productId - * @param array $productData + * @param array $attributeData + * @param array|string $attributeValue * @param array $returnAttributeData * @dataProvider mapProvider */ - public function testGetMap($productId, $productData, $returnAttributeData) + public function testGetMap(int $productId, array $attributeData, $attributeValue, array $returnAttributeData) { $storeId = 1; - $attributeCode = $productData['attributeCode']; - $this->dataProvider->expects($this->any())->method('getSearchableAttribute') - ->with($attributeCode) - ->willReturn($this->getAttribute($attributeCode, $productData['attributeData'])); + $attributeId = 5; + $context = []; - $this->fieldMapperMock->expects($this->any())->method('getFieldName') - ->with($attributeCode, []) + $this->dataProvider->method('getSearchableAttribute') + ->with($attributeId) + ->willReturn($this->getAttribute($attributeData)); + $this->fieldMapperMock->method('getFieldName') ->willReturnArgument(0); - if ($productData['attributeData']['frontendInput'] === 'date') { - $this->dateFieldTypeMock->expects($this->once())->method('formatDate') - ->with($storeId, $productData['attributeValue']) - ->willReturnArgument(1); - } - - $this->builderMock->expects($this->exactly(2)) - ->method('addField') - ->withConsecutive( - ['store_id', $storeId], - [$attributeCode, $returnAttributeData[$attributeCode]] - ); + $this->dateFieldTypeMock->method('formatDate') + ->willReturnArgument(1); + $this->additionalFieldsProvider->expects($this->once()) + ->method('getFields') + ->willReturn([]); - $this->builderMock->expects($this->once())->method('build')->willReturn($returnAttributeData); - $this->additionalFieldsProvider->expects($this->once())->method('getFields')->willReturn([]); $documentData = [ - $productId => [$productData['attributeCode'] => $productData['attributeValue']] + $productId => [$attributeId => $attributeValue], ]; - $this->assertEquals( - [$productId => $returnAttributeData], - $this->model->map($documentData, $storeId, []) - ); + $documents = $this->model->map($documentData, $storeId, $context); + $returnAttributeData['store_id'] = $storeId; + $this->assertEquals($returnAttributeData, $documents[$productId]); } /** - * @param int $productId - * @param array $productData - * @param array $returnAttributeData - * @dataProvider mapProviderForAttributeWithOptions + * @return void */ - public function testGetMapForAttributeWithOptions($productId, $productData, $returnAttributeData) + public function testGetMapWithOptions() { $storeId = 1; - $attributeCode = $productData['attributeCode']; - $this->dataProvider->expects($this->any())->method('getSearchableAttribute') - ->with($attributeCode) - ->willReturn($this->getAttribute($attributeCode, $productData['attributeData'])); + $productId = 10; + $context = []; + $attributeValue = ['o1', 'o2']; + $returnAttributeData = [ + 'store_id' => $storeId, + 'options' => $attributeValue, + ]; - $this->fieldMapperMock->expects($this->any())->method('getFieldName') - ->with($attributeCode, []) + $this->dataProvider->expects($this->never()) + ->method('getSearchableAttribute'); + $this->fieldMapperMock->method('getFieldName') ->willReturnArgument(0); - if ($productData['attributeData']['frontendInput'] === 'date') { - $this->dateFieldTypeMock->expects($this->once())->method('formatDate') - ->with($storeId, $productData['attributeValue']) - ->willReturnArgument(1); - } - $this->builderMock->expects($this->exactly(3)) - ->method('addField') - ->withConsecutive( - ['store_id', $storeId], - [$attributeCode . '_value', $returnAttributeData[$attributeCode . '_value']], - [$attributeCode, $returnAttributeData[$attributeCode]] - ); - $this->builderMock->expects($this->once())->method('build')->willReturn($returnAttributeData); - $this->additionalFieldsProvider->expects($this->once())->method('getFields')->willReturn([]); + $this->additionalFieldsProvider->expects($this->once()) + ->method('getFields') + ->willReturn([]); + $documentData = [ - $productId => [$productData['attributeCode'] => $productData['attributeValue']] + $productId => ['options' => $attributeValue], ]; - $this->assertEquals( - [$productId => $returnAttributeData], - $this->model->map($documentData, $storeId, []) - ); + $documents = $this->model->map($documentData, $storeId, $context); + $this->assertEquals($returnAttributeData, $documents[$productId]); } /** * Return attribute mock * - * @param string $attributeCode * @param array attributeData * @return \PHPUnit_Framework_MockObject_MockObject */ - private function getAttribute($attributeCode, $attributeData) + private function getAttribute(array $attributeData): \PHPUnit_Framework_MockObject_MockObject { - $attribute = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class) - ->disableOriginalConstructor() - ->getMock(); - $attribute->expects($this->once())->method('getAttributeCode')->willReturn($attributeCode); - $attribute->expects($this->once())->method('getBackendType')->willReturn($attributeData['backendType']); - $attribute->expects($this->any())->method('getFrontendInput')->willReturn($attributeData['frontendInput']); - $attribute->expects($this->any())->method('getOptions')->willReturn($attributeData['options']); + $attributeMock = $this->createMock(Attribute::class); + $attributeMock->method('getAttributeCode')->willReturn($attributeData['code']); + $attributeMock->method('getBackendType')->willReturn($attributeData['backendType']); + $attributeMock->method('getFrontendInput')->willReturn($attributeData['frontendInput']); + $attributeMock->method('getIsSearchable')->willReturn($attributeData['is_searchable']); + $options = []; + foreach ($attributeData['options'] as $option) { + $optionMock = $this->createMock(AttributeOptionInterface::class); + $optionMock->method('getValue')->willReturn($option['value']); + $optionMock->method('getLabel')->willReturn($option['label']); + $options[] = $optionMock; + } + $attributeMock->method('getOptions')->willReturn($options); - return $attribute; + return $attributeMock; } /** * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public static function mapProvider() + public static function mapProvider(): array { return [ - 'text attribute' => [ - 11, + 'text' => [ + 10, [ - 'attributeCode' => 'description', - 'attributeValue' => 'some text', - 'attributeData' => [ - 'backendType' => 'text', - 'frontendInput' => 'text', - 'options' => [] - ] + 'code' => 'description', + 'backendType' => 'text', + 'frontendInput' => 'text', + 'is_searchable' => false, + 'options' => [], ], + 'some text', ['description' => 'some text'], ], - 'date time attribute' => [ - 12, + 'datetime' => [ + 10, [ - 'attributeCode' => 'created_at', - 'attributeValue' => '00-00-00 00:00:00', - 'attributeData' => [ - 'backendType' => 'datetime', - 'frontendInput' => 'date', - 'options' => [] - ] + 'code' => 'created_at', + 'backendType' => 'datetime', + 'frontendInput' => 'date', + 'is_searchable' => false, + 'options' => [], ], + '00-00-00 00:00:00', ['created_at' => '00-00-00 00:00:00'], ], - 'array value attribute' => [ - 12, + 'array single value' => [ + 10, [ - 'attributeCode' => 'attribute_array', - 'attributeValue' => ['one', 'two', 'three'], - 'attributeData' => [ - 'backendType' => 'text', - 'frontendInput' => 'text', - 'options' => [] - ] + 'code' => 'attribute_array', + 'backendType' => 'text', + 'frontendInput' => 'text', + 'is_searchable' => false, + 'options' => [], ], - ['attribute_array' => 'one two three'], + [10 => 'one'], + ['attribute_array' => 'one'], ], - 'multiselect value attribute' => [ - 12, + 'array multiple value' => [ + 10, [ - 'attributeCode' => 'multiselect', - 'attributeValue' => 'some,data with,comma', - 'attributeData' => [ - 'backendType' => 'text', - 'frontendInput' => 'multiselect', - 'options' => [] - ] + 'code' => 'attribute_array', + 'backendType' => 'text', + 'frontendInput' => 'text', + 'is_searchable' => false, + 'options' => [], ], - ['multiselect' => 'some data with comma'], + [10 => 'one', 11 => 'two', 12 => 'three'], + ['attribute_array' => ['one', 'two', 'three']], ], - ]; - } - - /** - * @return array - */ - public static function mapProviderForAttributeWithOptions() - { - return [ - 'select value attribute' => [ - 12, + 'array multiple decimal value' => [ + 10, + [ + 'code' => 'decimal_array', + 'backendType' => 'decimal', + 'frontendInput' => 'text', + 'is_searchable' => false, + 'options' => [], + ], + [10 => '0.1', 11 => '0.2', 12 => '0.3'], + ['decimal_array' => ['0.1', '0.2', '0.3']], + ], + 'array excluded from merge' => [ + 10, + [ + 'code' => 'status', + 'backendType' => 'int', + 'frontendInput' => 'select', + 'is_searchable' => false, + 'options' => [ + ['value' => '1', 'label' => 'Enabled'], + ['value' => '2', 'label' => 'Disabled'], + ], + ], + [10 => '1', 11 => '2'], + ['status' => '1'], + ], + 'select without options' => [ + 10, + [ + 'code' => 'color', + 'backendType' => 'text', + 'frontendInput' => 'select', + 'is_searchable' => false, + 'options' => [], + ], + '44', + ['color' => '44'], + ], + 'unsearchable select with options' => [ + 10, [ - 'attributeCode' => 'color', - 'attributeValue' => '44', - 'attributeData' => [ - 'backendType' => 'text', - 'frontendInput' => 'select', - 'options' => [new \Magento\Framework\DataObject(['value' => '44', 'label' => 'red'])] - ] + 'code' => 'color', + 'backendType' => 'text', + 'frontendInput' => 'select', + 'is_searchable' => false, + 'options' => [ + ['value' => '44', 'label' => 'red'], + ['value' => '45', 'label' => 'black'], + ], ], + '44', + ['color' => '44'], + ], + 'searchable select with options' => [ + 10, + [ + 'code' => 'color', + 'backendType' => 'text', + 'frontendInput' => 'select', + 'is_searchable' => true, + 'options' => [ + ['value' => '44', 'label' => 'red'], + ['value' => '45', 'label' => 'black'], + ], + ], + '44', ['color' => '44', 'color_value' => 'red'], ], + 'composite select with options' => [ + 10, + [ + 'code' => 'color', + 'backendType' => 'text', + 'frontendInput' => 'select', + 'is_searchable' => true, + 'options' => [ + ['value' => '44', 'label' => 'red'], + ['value' => '45', 'label' => 'black'], + ], + ], + [10 => '44', 11 => '45'], + ['color' => ['44', '45'], 'color_value' => ['red', 'black']], + ], + 'multiselect without options' => [ + 10, + [ + 'code' => 'multicolor', + 'backendType' => 'text', + 'frontendInput' => 'multiselect', + 'is_searchable' => false, + 'options' => [], + ], + '44,45', + ['multicolor' => [44, 45]], + ], + 'unsearchable multiselect with options' => [ + 10, + [ + 'code' => 'multicolor', + 'backendType' => 'text', + 'frontendInput' => 'multiselect', + 'is_searchable' => false, + 'options' => [ + ['value' => '44', 'label' => 'red'], + ['value' => '45', 'label' => 'black'], + ], + ], + '44,45', + ['multicolor' => [44, 45]], + ], + 'searchable multiselect with options' => [ + 10, + [ + 'code' => 'multicolor', + 'backendType' => 'text', + 'frontendInput' => 'multiselect', + 'is_searchable' => true, + 'options' => [ + ['value' => '44', 'label' => 'red'], + ['value' => '45', 'label' => 'black'], + ], + ], + '44,45', + ['multicolor' => [44, 45], 'multicolor_value' => ['red', 'black']], + ], + 'composite multiselect with options' => [ + 10, + [ + 'code' => 'multicolor', + 'backendType' => 'text', + 'frontendInput' => 'multiselect', + 'is_searchable' => true, + 'options' => [ + ['value' => '44', 'label' => 'red'], + ['value' => '45', 'label' => 'black'], + ['value' => '46', 'label' => 'green'], + ], + ], + [10 => '44,45', 11 => '45,46'], + ['multicolor' => [44, 45, 46], 'multicolor_value' => ['red', 'black', 'green']], + ], + 'excluded attribute' => [ + 10, + [ + 'code' => 'price', + 'backendType' => 'int', + 'frontendInput' => 'int', + 'is_searchable' => false, + 'options' => [] + ], + 15, + [] + ], ]; } } diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/AttributeAdapterTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/AttributeAdapterTest.php new file mode 100644 index 000000000000..9ca65d8e675b --- /dev/null +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/AttributeAdapterTest.php @@ -0,0 +1,387 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Test\Unit\Model\Adapter\FieldMapper\Product; + +use Magento\Framework\Api\CustomAttributesDataInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Framework\Model\AbstractExtensibleModel; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface + as FieldTypeConverterInterface; + +/** + * @SuppressWarnings(PHPMD) + */ +class AttributeAdapterTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter + */ + private $adapter; + + /** + * @var AbstractExtensibleModel + */ + private $attribute; + + /** + * Set up test environment + * + * @return void + */ + protected function setUp() + { + $this->attribute = $this->getMockBuilder(CustomAttributesDataInterface::class) + ->disableOriginalConstructor() + ->setMethods([ + 'getIsFilterable', + 'getIsFilterableInSearch', + 'getIsSearchable', + 'getIsVisibleInAdvancedSearch', + 'getBackendType', + 'getFrontendInput', + 'usesSource', + ]) + ->getMockForAbstractClass(); + + $objectManager = new ObjectManagerHelper($this); + + $this->adapter = $objectManager->getObject( + \Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter::class, + [ + 'attribute' => $this->attribute, + 'attributeCode' => 'code', + ] + ); + } + + /** + * @dataProvider isFilterableProvider + * @param $isFilterable + * @param $isFilterableInSearch + * @param $expected + * @return void + */ + public function testIsFilterable($isFilterable, $isFilterableInSearch, $expected) + { + $this->attribute + ->method('getIsFilterable') + ->willReturn($isFilterable); + $this->attribute + ->method('getIsFilterableInSearch') + ->willReturn($isFilterableInSearch); + $this->assertEquals( + $expected, + $this->adapter->isFilterable() + ); + } + + /** + * @dataProvider isSearchableProvider + * @param $isSearchable + * @param $isVisibleInAdvancedSearch + * @param $isFilterable + * @param $isFilterableInSearch + * @param $expected + * @return void + */ + public function testIsSearchable( + $isSearchable, + $isVisibleInAdvancedSearch, + $isFilterable, + $isFilterableInSearch, + $expected + ) { + $this->attribute + ->method('getIsSearchable') + ->willReturn($isSearchable); + $this->attribute + ->method('getIsVisibleInAdvancedSearch') + ->willReturn($isVisibleInAdvancedSearch); + $this->attribute + ->method('getIsFilterable') + ->willReturn($isFilterable); + $this->attribute + ->method('getIsFilterableInSearch') + ->willReturn($isFilterableInSearch); + $this->assertEquals( + $expected, + $this->adapter->isSearchable() + ); + } + + /** + * @dataProvider isAlwaysIndexableProvider + * @param $expected + * @return void + */ + public function testIsAlwaysIndexable($expected) + { + $this->assertEquals( + $expected, + $this->adapter->isAlwaysIndexable() + ); + } + + /** + * @dataProvider isDateTimeTypeProvider + * @param $backendType + * @param $expected + * @return void + */ + public function testIsDateTimeType($backendType, $expected) + { + $this->attribute + ->method('getBackendType') + ->willReturn($backendType); + $this->assertEquals( + $expected, + $this->adapter->isDateTimeType() + ); + } + + /** + * @dataProvider isFloatTypeProvider + * @param $backendType + * @param $expected + * @return void + */ + public function testIsFloatType($backendType, $expected) + { + $this->attribute + ->method('getBackendType') + ->willReturn($backendType); + $this->assertEquals( + $expected, + $this->adapter->isFloatType() + ); + } + + /** + * @dataProvider isIntegerTypeProvider + * @param $backendType + * @param $expected + * @return void + */ + public function testIsIntegerType($backendType, $expected) + { + $this->attribute + ->method('getBackendType') + ->willReturn($backendType); + $this->assertEquals( + $expected, + $this->adapter->isIntegerType() + ); + } + + /** + * @dataProvider isBooleanTypeProvider + * @param $frontendInput + * @param $backendType + * @param $expected + * @return void + */ + public function testIsBooleanType($frontendInput, $backendType, $expected) + { + $this->attribute + ->method('getBackendType') + ->willReturn($backendType); + $this->attribute + ->method('getFrontendInput') + ->willReturn($frontendInput); + $this->assertEquals( + $expected, + $this->adapter->isBooleanType() + ); + } + + /** + * @dataProvider isComplexTypeProvider + * @param $frontendInput + * @param $usesSource + * @param $expected + * @return void + */ + public function testIsComplexType($frontendInput, $usesSource, $expected) + { + $this->attribute + ->method('usesSource') + ->willReturn($usesSource); + $this->attribute + ->method('getFrontendInput') + ->willReturn($frontendInput); + $this->assertEquals( + $expected, + $this->adapter->isComplexType() + ); + } + + /** + * @dataProvider isEavAttributeProvider + * @param $expected + * @return void + */ + public function testIsEavAttribute($expected) + { + $this->assertEquals( + $expected, + $this->adapter->isEavAttribute() + ); + } + + /** + * @return array + */ + public function isEavAttributeProvider() + { + return [ + [false], + ]; + } + + /** + * @return array + */ + public function isComplexTypeProvider() + { + return [ + ['select', true, true], + ['multiselect', true, true], + ['multiselect', false, true], + ['int', false, false], + ['int', true, true], + ]; + } + + /** + * @return array + */ + public function isBooleanTypeProvider() + { + return [ + ['select', 'int', true], + ['boolean', 'int', true], + ['boolean', 'varchar', false], + ['select', 'varchar', false], + ['int', 'varchar', false], + ['int', 'int', false], + ]; + } + + /** + * @return array + */ + public function isIntegerTypeProvider() + { + return [ + ['smallint', true], + ['int', true], + ['string', false], + ]; + } + + /** + * @return array + */ + public function isFloatTypeProvider() + { + return [ + ['decimal', true], + ['int', false], + ]; + } + + /** + * @return array + */ + public function isDateTimeTypeProvider() + { + return [ + ['timestamp', true], + ['datetime', true], + ['int', false], + ]; + } + + /** + * @return array + */ + public function isAlwaysIndexableProvider() + { + return [ + [false] + ]; + } + + /** + * @return array + */ + public function isSearchableProvider() + { + return [ + [true, false, false, false, true], + [false, false, false, false, false], + [false, true, false, false, true], + [false, false, true, false, true], + [true, true, true, false, true], + [true, true, false, false, true], + ]; + } + + /** + * @return array + */ + public function isFilterableProvider() + { + return [ + [true, false, true,], + [true, false, true,], + [false, false, false,] + ]; + } + + /** + * @return array + */ + public function isStringServiceFieldTypeProvider() + { + return [ + ['string', 'text', false,], + ['text', 'text', true,] + ]; + } + + /** + * @return array + */ + public function getFieldNameProvider() + { + return [ + ['name', [], 'name'] + ]; + } + + /** + * @return array + */ + public function getFieldTypeProvider() + { + return [ + ['type', 'type'] + ]; + } + + /** + * @return array + */ + public function getFieldIndexProvider() + { + return [ + ['type', 'no', 'no'] + ]; + } +} 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 new file mode 100644 index 000000000000..7c2a33c05aa0 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php @@ -0,0 +1,328 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Test\Unit\Model\Adapter\FieldMapper\Product\FieldProvider; + +use Magento\Framework\Api\SearchCriteria; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Catalog\Api\CategoryListInterface; +use Magento\Customer\Api\GroupRepositoryInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface + as FieldTypeConverterInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\ConverterInterface + as IndexTypeConverterInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Catalog\Api\Data\CategorySearchResultsInterface; +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Customer\Api\Data\GroupSearchResultsInterface; +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) + */ +class DynamicFieldTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\DynamicField + */ + private $provider; + + /** + * @var GroupRepositoryInterface + */ + private $groupRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var FieldTypeConverterInterface + */ + private $fieldTypeConverter; + + /** + * @var IndexTypeConverterInterface + */ + private $indexTypeConverter; + + /** + * @var AttributeProvider + */ + private $attributeAdapterProvider; + + /** + * @var CategoryListInterface + */ + private $categoryList; + + /** + * @var Collection + */ + private $categoryCollection; + + /** + * @var FieldNameResolver + */ + private $fieldNameResolver; + + /** + * Set up test environment + * + * @return void + */ + protected function setUp() + { + $this->groupRepository = $this->getMockBuilder(GroupRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->searchCriteriaBuilder = $this->getMockBuilder(SearchCriteriaBuilder::class) + ->disableOriginalConstructor() + ->getMock(); + $this->fieldTypeConverter = $this->getMockBuilder(FieldTypeConverterInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->indexTypeConverter = $this->getMockBuilder(IndexTypeConverterInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->attributeAdapterProvider = $this->getMockBuilder(AttributeProvider::class) + ->disableOriginalConstructor() + ->setMethods(['getByAttributeCode', 'getByAttribute']) + ->getMock(); + $this->fieldNameResolver = $this->getMockBuilder(FieldNameResolver::class) + ->disableOriginalConstructor() + ->setMethods(['getFieldName']) + ->getMock(); + $this->categoryList = $this->getMockBuilder(CategoryListInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->categoryCollection = $this->getMockBuilder(Collection::class) + ->disableOriginalConstructor() + ->setMethods(['getAllIds']) + ->getMock(); + + $objectManager = new ObjectManagerHelper($this); + + $this->provider = $objectManager->getObject( + \Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\DynamicField::class, + [ + 'groupRepository' => $this->groupRepository, + 'searchCriteriaBuilder' => $this->searchCriteriaBuilder, + 'fieldTypeConverter' => $this->fieldTypeConverter, + 'indexTypeConverter' => $this->indexTypeConverter, + 'attributeAdapterProvider' => $this->attributeAdapterProvider, + 'categoryList' => $this->categoryList, + 'fieldNameResolver' => $this->fieldNameResolver, + 'categoryCollection' => $this->categoryCollection, + ] + ); + } + + /** + * @dataProvider attributeProvider + * @param $complexType + * @param $categoryId + * @param $groupId + * @param array $expected + * @return void + */ + public function testGetAllAttributesTypes( + $complexType, + $categoryId, + $groupId, + $expected + ) { + $searchCriteria = $this->getMockBuilder(SearchCriteria::class) + ->disableOriginalConstructor() + ->getMock(); + $this->searchCriteriaBuilder->expects($this->any()) + ->method('create') + ->willReturn($searchCriteria); + $groupSearchResults = $this->getMockBuilder(GroupSearchResultsInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getItems']) + ->getMockForAbstractClass(); + $group = $this->getMockBuilder(GroupInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getId']) + ->getMockForAbstractClass(); + $group->expects($this->any()) + ->method('getId') + ->willReturn($groupId); + $groupSearchResults->expects($this->any()) + ->method('getItems') + ->willReturn([$group]); + + $this->categoryCollection->expects($this->any()) + ->method('getAllIds') + ->willReturn([$categoryId]); + + $categoryAttributeMock = $this->getMockBuilder(AttributeAdapter::class) + ->disableOriginalConstructor() + ->setMethods(['getAttributeCode']) + ->getMock(); + $categoryAttributeMock->expects($this->any()) + ->method('getAttributeCode') + ->willReturn('category'); + $positionAttributeMock = $this->getMockBuilder(AttributeAdapter::class) + ->disableOriginalConstructor() + ->setMethods(['getAttributeCode']) + ->getMock(); + $positionAttributeMock->expects($this->any()) + ->method('getAttributeCode') + ->willReturn('position'); + + $this->fieldNameResolver->expects($this->any()) + ->method('getFieldName') + ->will($this->returnCallback( + function ($attribute) use ($categoryId) { + static $callCount = []; + $attributeCode = $attribute->getAttributeCode(); + $callCount[$attributeCode] = !isset($callCount[$attributeCode]) ? 1 : ++$callCount[$attributeCode]; + + if ($attributeCode === 'category') { + return 'category_name_' . $categoryId; + } elseif ($attributeCode === 'position') { + return 'position_' . $categoryId; + } elseif ($attributeCode === 'price') { + return 'price_' . $categoryId . '_1'; + } + } + )); + $priceAttributeMock = $this->getMockBuilder(AttributeAdapter::class) + ->disableOriginalConstructor() + ->setMethods(['getAttributeCode']) + ->getMock(); + $priceAttributeMock->expects($this->any()) + ->method('getAttributeCode') + ->willReturn('price'); + $this->indexTypeConverter->expects($this->any()) + ->method('convert') + ->willReturn('no_index'); + $this->groupRepository->expects($this->any()) + ->method('getList') + ->willReturn($groupSearchResults); + $this->attributeAdapterProvider->expects($this->any()) + ->method('getByAttributeCode') + ->with($this->anything()) + ->will($this->returnCallback( + function ($code) use ( + $categoryAttributeMock, + $positionAttributeMock, + $priceAttributeMock + ) { + static $callCount = []; + $callCount[$code] = !isset($callCount[$code]) ? 1 : ++$callCount[$code]; + + if ($code === 'position') { + return $positionAttributeMock; + } elseif ($code === 'category_name') { + return $categoryAttributeMock; + } elseif ($code === 'price') { + return $priceAttributeMock; + } + } + )); + $this->fieldTypeConverter->expects($this->any()) + ->method('convert') + ->with($this->anything()) + ->will($this->returnCallback( + 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'; + } else { + return $complexType; + } + } + )); + + $this->assertEquals( + $expected, + $this->provider->getFields(['websiteId' => 1]) + ); + } + + /** + * @return array + */ + public function attributeProvider() + { + return [ + [ + 'text', + 1, + 1, + [ + 'category_name_1' => [ + 'type' => 'string', + 'index' => 'no_index' + ], + 'position_1' => [ + 'type' => 'string', + 'index' => 'no_index' + ], + 'price_1_1' => [ + 'type' => 'float', + 'store' => true + ] + ] + ], + [ + null, + 1, + 1, + [ + 'category_name_1' => [ + 'type' => 'string', + 'index' => 'no_index' + ], + 'position_1' => [ + 'type' => 'string', + 'index' => 'no_index' + ], + 'price_1_1' => [ + 'type' => 'float', + 'store' => true + ] + ], + ], + [ + null, + 1, + 1, + [ + 'category_name_1' => [ + 'type' => 'string', + 'index' => 'no_index' + ], + 'position_1' => [ + 'type' => 'string', + 'index' => 'no_index' + ], + 'price_1_1' => [ + 'type' => 'float', + 'store' => true + ] + ] + ] + ]; + } +} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/IndexResolverTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/IndexResolverTest.php new file mode 100644 index 000000000000..86bcfeb06480 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/IndexResolverTest.php @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Test\Unit\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\ConverterInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +/** + * @SuppressWarnings(PHPMD) + */ +class IndexResolverTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\IndexResolver + */ + private $resolver; + + /** + * @var ConverterInterface + */ + private $converter; + + /** + * Set up test environment + * + * @return void + */ + protected function setUp() + { + $this->converter = $this->getMockBuilder(ConverterInterface::class) + ->disableOriginalConstructor() + ->setMethods(['convert']) + ->getMockForAbstractClass(); + $objectManager = new ObjectManagerHelper($this); + + $this->resolver = $objectManager->getObject( + \Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\IndexResolver::class, + [ + 'converter' => $this->converter + ] + ); + } + + /** + * @dataProvider getFieldIndexProvider + * @param $isSearchable + * @param $isAlwaysIndexable + * @param $serviceFieldType + * @param $expected + * @return void + */ + public function testGetFieldName( + $isSearchable, + $isAlwaysIndexable, + $serviceFieldType, + $expected + ) { + $this->converter->expects($this->any()) + ->method('convert') + ->willReturn('something'); + $attributeMock = $this->getMockBuilder(AttributeAdapter::class) + ->disableOriginalConstructor() + ->setMethods([ + 'isSearchable', + 'isAlwaysIndexable', + ]) + ->getMock(); + $attributeMock->expects($this->any()) + ->method('isSearchable') + ->willReturn($isSearchable); + $attributeMock->expects($this->any()) + ->method('isAlwaysIndexable') + ->willReturn($isAlwaysIndexable); + + $this->assertEquals( + $expected, + $this->resolver->getFieldIndex($attributeMock, $serviceFieldType) + ); + } + + /** + * @return array + */ + public function getFieldIndexProvider() + { + return [ + [true, true, 'string', null], + [false, false, 'string', 'something'], + [true, false, 'string', null], + [false, true, 'string', null], + ]; + } +} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/CategoryNameTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/CategoryNameTest.php new file mode 100644 index 000000000000..5ebeac228443 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/CategoryNameTest.php @@ -0,0 +1,124 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Test\Unit\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver; + +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Framework\Registry; +use Magento\Store\Model\StoreManagerInterface as StoreManager; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\CategoryName; + +/** + * @SuppressWarnings(PHPMD) + */ +class CategoryNameTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var CategoryName + */ + private $resolver; + + /** + * @var StoreManager + */ + private $storeManager; + + /** + * @var Registry + */ + private $coreRegistry; + + /** + * Set up test environment + * + * @return void + */ + protected function setUp() + { + $this->storeManager = $this->getMockBuilder(StoreManager::class) + ->disableOriginalConstructor() + ->setMethods(['getStore']) + ->getMockForAbstractClass(); + $this->coreRegistry = $this->getMockBuilder(Registry::class) + ->disableOriginalConstructor() + ->setMethods(['registry']) + ->getMock(); + + $objectManager = new ObjectManagerHelper($this); + + $this->resolver = $objectManager->getObject( + CategoryName::class, + [ + 'storeManager' => $this->storeManager, + 'coreRegistry' => $this->coreRegistry, + ] + ); + } + + /** + * @dataProvider getFieldNameProvider + * @param $attributeCode + * @param $context + * @param $fromRegistry + * @param $expected + * @return void + */ + public function testGetFieldName($attributeCode, $context, $fromRegistry, $expected) + { + $attributeMock = $this->getMockBuilder(AttributeAdapter::class) + ->disableOriginalConstructor() + ->setMethods(['getAttributeCode']) + ->getMock(); + $attributeMock->expects($this->any()) + ->method('getAttributeCode') + ->willReturn($attributeCode); + $store = $this->getMockBuilder(StoreInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getRootCategoryId']) + ->getMockForAbstractClass(); + $store->expects($this->any()) + ->method('getRootCategoryId') + ->willReturn(2); + $this->storeManager->expects($this->any()) + ->method('getStore') + ->willReturn($store); + $category = null; + if ($fromRegistry) { + $category = $this->getMockBuilder(CategoryInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getId']) + ->getMockForAbstractClass(); + $category->expects($this->any()) + ->method('getId') + ->willReturn(1); + } + $this->coreRegistry->expects($this->any()) + ->method('registry') + ->willReturn($category); + + $this->assertEquals( + $expected, + $this->resolver->getFieldName($attributeMock, $context) + ); + } + + /** + * @return array + */ + public function getFieldNameProvider() + { + return [ + ['category_name', [], true, 'name_category_1'], + ['category_name', [], false, 'name_category_2'], + ['category_name', ['categoryId' => 3], false, 'name_category_3'], + ['price', ['categoryId' => 3], false, ''], + ]; + } +} 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 new file mode 100644 index 000000000000..fd5c87bc9518 --- /dev/null +++ b/app/code/Magento/Elasticsearch/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\Elasticsearch\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\Elasticsearch\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'], '_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/FieldName/Resolver/NotEavAttributeTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/NotEavAttributeTest.php new file mode 100644 index 000000000000..cdd8dde585a5 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/NotEavAttributeTest.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\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\FieldName\Resolver\NotEavAttribute; + +/** + * @SuppressWarnings(PHPMD) + */ +class NotEavAttributeTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var NotEavAttribute + */ + private $resolver; + + /** + * Set up test environment + * + * @return void + */ + protected function setUp() + { + $objectManager = new ObjectManagerHelper($this); + + $this->resolver = $objectManager->getObject( + NotEavAttribute::class + ); + } + + /** + * @dataProvider getFieldNameProvider + * @param $attributeCode + * @param $isEavAttribute + * @param $context + * @param $expected + * @return void + */ + public function testGetFieldName($attributeCode, $isEavAttribute, $context, $expected) + { + $attributeMock = $this->getMockBuilder(AttributeAdapter::class) + ->disableOriginalConstructor() + ->setMethods(['isEavAttribute', 'getAttributeCode']) + ->getMock(); + $attributeMock->expects($this->any()) + ->method('isEavAttribute') + ->willReturn($isEavAttribute); + $attributeMock->expects($this->any()) + ->method('getAttributeCode') + ->willReturn($attributeCode); + + $this->assertEquals( + $expected, + $this->resolver->getFieldName($attributeMock, $context) + ); + } + + /** + * @return array + */ + public function getFieldNameProvider() + { + return [ + ['code', true, [], ''], + ['code', false, [], 'code'], + ]; + } +} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/PositionTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/PositionTest.php new file mode 100644 index 000000000000..dcfd4d02988d --- /dev/null +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/PositionTest.php @@ -0,0 +1,123 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Test\Unit\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver; + +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Framework\Registry; +use Magento\Store\Model\StoreManagerInterface as StoreManager; +use Magento\Store\Api\Data\StoreInterface; + +/** + * @SuppressWarnings(PHPMD) + */ +class PositionTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\Position + */ + private $resolver; + + /** + * @var StoreManager + */ + private $storeManager; + + /** + * @var Registry + */ + private $coreRegistry; + + /** + * Set up test environment + * + * @return void + */ + protected function setUp() + { + $this->storeManager = $this->getMockBuilder(StoreManager::class) + ->disableOriginalConstructor() + ->setMethods(['getStore']) + ->getMockForAbstractClass(); + $this->coreRegistry = $this->getMockBuilder(Registry::class) + ->disableOriginalConstructor() + ->setMethods(['registry']) + ->getMock(); + + $objectManager = new ObjectManagerHelper($this); + + $this->resolver = $objectManager->getObject( + \Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\Position::class, + [ + 'storeManager' => $this->storeManager, + 'coreRegistry' => $this->coreRegistry, + ] + ); + } + + /** + * @dataProvider getFieldNameProvider + * @param $attributeCode + * @param $context + * @param $fromRegistry + * @param $expected + * @return void + */ + public function testGetFieldName($attributeCode, $context, $fromRegistry, $expected) + { + $attributeMock = $this->getMockBuilder(AttributeAdapter::class) + ->disableOriginalConstructor() + ->setMethods(['getAttributeCode']) + ->getMock(); + $attributeMock->expects($this->any()) + ->method('getAttributeCode') + ->willReturn($attributeCode); + $store = $this->getMockBuilder(StoreInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getRootCategoryId']) + ->getMockForAbstractClass(); + $store->expects($this->any()) + ->method('getRootCategoryId') + ->willReturn(2); + $this->storeManager->expects($this->any()) + ->method('getStore') + ->willReturn($store); + $category = null; + if ($fromRegistry) { + $category = $this->getMockBuilder(CategoryInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getId']) + ->getMockForAbstractClass(); + $category->expects($this->any()) + ->method('getId') + ->willReturn(1); + } + $this->coreRegistry->expects($this->any()) + ->method('registry') + ->willReturn($category); + + $this->assertEquals( + $expected, + $this->resolver->getFieldName($attributeMock, $context) + ); + } + + /** + * @return array + */ + public function getFieldNameProvider() + { + return [ + ['position', [], true, 'position_category_1'], + ['position', [], false, 'position_category_2'], + ['position', ['categoryId' => 2], false, 'position_category_2'], + ['price', ['categoryId' => 2], false, ''], + ]; + } +} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/PriceTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/PriceTest.php new file mode 100644 index 000000000000..244da5a42e00 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/PriceTest.php @@ -0,0 +1,111 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\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\Customer\Model\Session as CustomerSession; +use Magento\Store\Model\StoreManagerInterface as StoreManager; +use Magento\Store\Api\Data\StoreInterface; + +/** + * @SuppressWarnings(PHPMD) + */ +class PriceTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\Price + */ + private $resolver; + + /** + * @var CustomerSession + */ + private $customerSession; + + /** + * @var StoreManager + */ + private $storeManager; + + /** + * Set up test environment + * + * @return void + */ + protected function setUp() + { + $this->customerSession = $this->getMockBuilder(CustomerSession::class) + ->disableOriginalConstructor() + ->setMethods(['getCustomerGroupId']) + ->getMock(); + $this->storeManager = $this->getMockBuilder(StoreManager::class) + ->disableOriginalConstructor() + ->setMethods(['getStore']) + ->getMockForAbstractClass(); + + $objectManager = new ObjectManagerHelper($this); + + $this->resolver = $objectManager->getObject( + \Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\Price::class, + [ + 'customerSession' => $this->customerSession, + 'storeManager' => $this->storeManager, + ] + ); + } + + /** + * @dataProvider getFieldNameProvider + * @param $attributeCode + * @param $context + * @param $expected + * @return void + */ + public function testGetFieldName($attributeCode, $context, $expected) + { + $attributeMock = $this->getMockBuilder(AttributeAdapter::class) + ->disableOriginalConstructor() + ->setMethods(['getAttributeCode']) + ->getMock(); + $attributeMock->expects($this->any()) + ->method('getAttributeCode') + ->willReturn($attributeCode); + $this->customerSession->expects($this->any()) + ->method('getCustomerGroupId') + ->willReturn(1); + $store = $this->getMockBuilder(StoreInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getWebsiteId']) + ->getMockForAbstractClass(); + $store->expects($this->any()) + ->method('getWebsiteId') + ->willReturn(2); + $this->storeManager->expects($this->any()) + ->method('getStore') + ->willReturn($store); + + $this->assertEquals( + $expected, + $this->resolver->getFieldName($attributeMock, $context) + ); + } + + /** + * @return array + */ + public function getFieldNameProvider() + { + return [ + ['price', [], 'price_1_2'], + ['price', ['customerGroupId' => 2, 'websiteId' => 3], 'price_2_3'], + ['price', ['customerGroupId' => 2], 'price_2_2'], + ['sku', ['customerGroupId' => 2], ''], + ]; + } +} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/SpecialAttributeTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/SpecialAttributeTest.php new file mode 100644 index 000000000000..dcbf64bbb200 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/SpecialAttributeTest.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\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\FieldName\Resolver\SpecialAttribute; + +/** + * @SuppressWarnings(PHPMD) + */ +class SpecialAttributeTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var SpecialAttribute + */ + private $resolver; + + /** + * Set up test environment + * + * @return void + */ + protected function setUp() + { + $objectManager = new ObjectManagerHelper($this); + + $this->resolver = $objectManager->getObject( + SpecialAttribute::class + ); + } + + /** + * @dataProvider getFieldNameProvider + * @param $attributeCode + * @param $expected + * @return void + */ + public function testGetFieldName($attributeCode, $expected) + { + $attributeMock = $this->getMockBuilder(AttributeAdapter::class) + ->disableOriginalConstructor() + ->setMethods(['getAttributeCode']) + ->getMock(); + $attributeMock->expects($this->any()) + ->method('getAttributeCode') + ->willReturn($attributeCode); + + $this->assertEquals( + $expected, + $this->resolver->getFieldName($attributeMock) + ); + } + + /** + * @return array + */ + public function getFieldNameProvider() + { + return [ + ['id', 'id'], + ['sku', 'sku'], + ['store_id', 'store_id'], + ['visibility', 'visibility'], + ['price', ''], + ]; + } +} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterTest.php new file mode 100644 index 000000000000..842007aee294 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterTest.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Test\Unit\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +/** + * @SuppressWarnings(PHPMD) + */ +class ConverterTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter + */ + private $converter; + + /** + * Set up test environment + * + * @return void + */ + protected function setUp() + { + $objectManager = new ObjectManagerHelper($this); + + $this->converter = $objectManager->getObject( + \Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter::class + ); + } + + /** + * @dataProvider convertProvider + * @param $internalType + * @param $expected + * @return void + */ + public function testConvert($internalType, $expected) + { + $this->assertEquals( + $expected, + $this->converter->convert($internalType) + ); + } + + /** + * @return array + */ + public function convertProvider() + { + return [ + ['string', 'string'], + ['float', 'float'], + ['integer', 'integer'], + ]; + } +} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/DateTimeTypeTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/DateTimeTypeTest.php new file mode 100644 index 000000000000..ca474c4bee17 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/DateTimeTypeTest.php @@ -0,0 +1,88 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Test\Unit\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface + as FieldTypeConverterInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\DateTimeType; + +/** + * @SuppressWarnings(PHPMD) + */ +class DateTimeTypeTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var DateTimeType + */ + private $resolver; + + /** + * @var FieldTypeConverterInterface + */ + private $fieldTypeConverter; + + /** + * Set up test environment + * + * @return void + */ + protected function setUp() + { + $this->fieldTypeConverter = $this->getMockBuilder(FieldTypeConverterInterface::class) + ->disableOriginalConstructor() + ->setMethods(['convert']) + ->getMockForAbstractClass(); + + $objectManager = new ObjectManagerHelper($this); + + $this->resolver = $objectManager->getObject( + DateTimeType::class, + [ + 'fieldTypeConverter' => $this->fieldTypeConverter, + ] + ); + } + + /** + * @dataProvider getFieldTypeProvider + * @param $isDateTimeType + * @param $expected + * @return void + */ + public function testGetFieldType($isDateTimeType, $expected) + { + $attributeMock = $this->getMockBuilder(AttributeAdapter::class) + ->disableOriginalConstructor() + ->setMethods(['isDateTimeType']) + ->getMock(); + $attributeMock->expects($this->any()) + ->method('isDateTimeType') + ->willReturn($isDateTimeType); + $this->fieldTypeConverter->expects($this->any()) + ->method('convert') + ->willReturn('something'); + + $this->assertEquals( + $expected, + $this->resolver->getFieldType($attributeMock) + ); + } + + /** + * @return array + */ + public function getFieldTypeProvider() + { + return [ + [true, 'something'], + [false, ''], + ]; + } +} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/DefaultResolverTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/DefaultResolverTest.php new file mode 100644 index 000000000000..72b5320a050b --- /dev/null +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/DefaultResolverTest.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Test\Unit\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface + as FieldTypeConverterInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\DefaultResolver; + +/** + * @SuppressWarnings(PHPMD) + */ +class DefaultResolverTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var DefaultResolver + */ + private $resolver; + + /** + * @var FieldTypeConverterInterface + */ + private $fieldTypeConverter; + + /** + * Set up test environment + * + * @return void + */ + protected function setUp() + { + $this->fieldTypeConverter = $this->getMockBuilder(FieldTypeConverterInterface::class) + ->disableOriginalConstructor() + ->setMethods(['convert']) + ->getMockForAbstractClass(); + + $objectManager = new ObjectManagerHelper($this); + + $this->resolver = $objectManager->getObject( + DefaultResolver::class, + [ + 'fieldTypeConverter' => $this->fieldTypeConverter, + ] + ); + } + + /** + * @dataProvider getFieldTypeProvider + * @param $expected + * @return void + */ + public function testGetFieldType($expected) + { + $attributeMock = $this->getMockBuilder(AttributeAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + $this->fieldTypeConverter->expects($this->any()) + ->method('convert') + ->willReturn('something'); + + $this->assertEquals( + $expected, + $this->resolver->getFieldType($attributeMock) + ); + } + + /** + * @return array + */ + public function getFieldTypeProvider() + { + return [ + ['something'], + ]; + } +} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/FloatTypeTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/FloatTypeTest.php new file mode 100644 index 000000000000..c69a6f35b1eb --- /dev/null +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/FloatTypeTest.php @@ -0,0 +1,87 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Test\Unit\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface + as FieldTypeConverterInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +/** + * @SuppressWarnings(PHPMD) + */ +class FloatTypeTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\FloatType + */ + private $resolver; + + /** + * @var FieldTypeConverterInterface + */ + private $fieldTypeConverter; + + /** + * Set up test environment + * + * @return void + */ + protected function setUp() + { + $this->fieldTypeConverter = $this->getMockBuilder(FieldTypeConverterInterface::class) + ->disableOriginalConstructor() + ->setMethods(['convert']) + ->getMockForAbstractClass(); + + $objectManager = new ObjectManagerHelper($this); + + $this->resolver = $objectManager->getObject( + \Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\FloatType::class, + [ + 'fieldTypeConverter' => $this->fieldTypeConverter, + ] + ); + } + + /** + * @dataProvider getFieldTypeProvider + * @param $isFloatType + * @param $expected + * @return void + */ + public function testGetFieldType($isFloatType, $expected) + { + $attributeMock = $this->getMockBuilder(AttributeAdapter::class) + ->disableOriginalConstructor() + ->setMethods(['isFloatType']) + ->getMock(); + $attributeMock->expects($this->any()) + ->method('isFloatType') + ->willReturn($isFloatType); + $this->fieldTypeConverter->expects($this->any()) + ->method('convert') + ->willReturn('something'); + + $this->assertEquals( + $expected, + $this->resolver->getFieldType($attributeMock) + ); + } + + /** + * @return array + */ + public function getFieldTypeProvider() + { + return [ + [true, 'something'], + [false, ''], + ]; + } +} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerTypeTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerTypeTest.php new file mode 100644 index 000000000000..7570c8971b27 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerTypeTest.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Test\Unit\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface + as FieldTypeConverterInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\IntegerType; + +/** + * @SuppressWarnings(PHPMD) + */ +class IntegerTypeTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var IntegerType + */ + private $resolver; + + /** + * @var FieldTypeConverterInterface + */ + private $fieldTypeConverter; + + /** + * Set up test environment + * + * @return void + */ + protected function setUp() + { + $this->fieldTypeConverter = $this->getMockBuilder(FieldTypeConverterInterface::class) + ->disableOriginalConstructor() + ->setMethods(['convert']) + ->getMockForAbstractClass(); + + $objectManager = new ObjectManagerHelper($this); + + $this->resolver = $objectManager->getObject( + IntegerType::class, + [ + 'fieldTypeConverter' => $this->fieldTypeConverter, + ] + ); + } + + /** + * @dataProvider getFieldTypeProvider + * @param $attributeCode + * @param $isIntegerType + * @param $isBooleanType + * @param $isUserDefined + * @param $expected + * @return void + */ + public function testGetFieldType($attributeCode, $isIntegerType, $isBooleanType, $isUserDefined, $expected) + { + $attributeMock = $this->getMockBuilder(AttributeAdapter::class) + ->disableOriginalConstructor() + ->setMethods(['getAttributeCode', 'isIntegerType', 'isBooleanType', 'isUserDefined']) + ->getMock(); + $attributeMock->expects($this->any()) + ->method('getAttributeCode') + ->willReturn($attributeCode); + $attributeMock->expects($this->any()) + ->method('isIntegerType') + ->willReturn($isIntegerType); + $attributeMock->expects($this->any()) + ->method('isBooleanType') + ->willReturn($isBooleanType); + $attributeMock->expects($this->any()) + ->method('isUserDefined') + ->willReturn($isUserDefined); + $this->fieldTypeConverter->expects($this->any()) + ->method('convert') + ->willReturn('something'); + + $this->assertEquals( + $expected, + $this->resolver->getFieldType($attributeMock) + ); + } + + /** + * @return array + */ + public function getFieldTypeProvider() + { + return [ + ['category_ids', true, true, true, null], + ['category_ids', false, false, false, null], + ['type', true, false, false, 'something'], + ['type', false, true, false, 'something'], + ['type', true, true, true, ''], + ['type', false, false, true, ''], + ]; + } +} 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 new file mode 100644 index 000000000000..de85b8b6602b --- /dev/null +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/StaticFieldTest.php @@ -0,0 +1,332 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Test\Unit\Model\Adapter\FieldMapper\Product\FieldProvider; + +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Eav\Model\Config; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface + as FieldTypeConverterInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\ConverterInterface + as IndexTypeConverterInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ResolverInterface + 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) + */ +class StaticFieldTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\StaticField + */ + private $provider; + + /** + * @var \Magento\Eav\Model\Config|\PHPUnit_Framework_MockObject_MockObject + */ + private $eavConfig; + + /** + * @var FieldTypeConverterInterface + */ + private $fieldTypeConverter; + + /** + * @var IndexTypeConverterInterface + */ + private $indexTypeConverter; + + /** + * @var AttributeProvider + */ + private $attributeAdapterProvider; + + /** + * @var FieldIndexResolver + */ + private $fieldIndexResolver; + + /** + * @var FieldTypeResolver + */ + private $fieldTypeResolver; + + /** + * @var FieldNameResolver + */ + private $fieldNameResolver; + + /** + * Set up test environment + * + * @return void + */ + protected function setUp() + { + $this->eavConfig = $this->getMockBuilder(Config::class) + ->disableOriginalConstructor() + ->setMethods(['getEntityAttributes']) + ->getMock(); + $this->fieldTypeConverter = $this->getMockBuilder(FieldTypeConverterInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->indexTypeConverter = $this->getMockBuilder(IndexTypeConverterInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->attributeAdapterProvider = $this->getMockBuilder(AttributeProvider::class) + ->disableOriginalConstructor() + ->setMethods(['getByAttributeCode']) + ->getMock(); + $this->fieldTypeResolver = $this->getMockBuilder(FieldTypeResolver::class) + ->disableOriginalConstructor() + ->setMethods(['getFieldType']) + ->getMock(); + $this->fieldIndexResolver = $this->getMockBuilder(FieldIndexResolver::class) + ->disableOriginalConstructor() + ->setMethods(['getFieldIndex']) + ->getMock(); + $this->fieldNameResolver = $this->getMockBuilder(FieldNameResolver::class) + ->disableOriginalConstructor() + ->setMethods(['getFieldName']) + ->getMock(); + + $objectManager = new ObjectManagerHelper($this); + + $this->provider = $objectManager->getObject( + \Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\StaticField::class, + [ + 'eavConfig' => $this->eavConfig, + 'fieldTypeConverter' => $this->fieldTypeConverter, + 'indexTypeConverter' => $this->indexTypeConverter, + 'attributeAdapterProvider' => $this->attributeAdapterProvider, + 'fieldIndexResolver' => $this->fieldIndexResolver, + 'fieldTypeResolver' => $this->fieldTypeResolver, + 'fieldNameResolver' => $this->fieldNameResolver, + ] + ); + } + + /** + * @dataProvider attributeProvider + * @param string $attributeCode + * @param string $inputType + * @param $indexType + * @param $isComplexType + * @param $complexType + * @param $isSortable + * @param $fieldName + * @param $compositeFieldName + * @param $sortFieldName + * @param array $expected + * @return void + */ + public function testGetAllAttributesTypes( + $attributeCode, + $inputType, + $indexType, + $isComplexType, + $complexType, + $isSortable, + $fieldName, + $compositeFieldName, + $sortFieldName, + $expected + ) { + $this->fieldTypeResolver->expects($this->any()) + ->method('getFieldType') + ->willReturn($inputType); + $this->fieldIndexResolver->expects($this->any()) + ->method('getFieldIndex') + ->willReturn($indexType); + $this->indexTypeConverter->expects($this->any()) + ->method('convert') + ->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']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $productAttributeMock->expects($this->any()) + ->method('getAttributeCode') + ->willReturn($attributeCode); + $this->eavConfig->expects($this->any())->method('getEntityAttributes') + ->willReturn([$productAttributeMock]); + + $attributeMock = $this->getMockBuilder(AttributeAdapter::class) + ->disableOriginalConstructor() + ->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); + $this->attributeAdapterProvider->expects($this->any()) + ->method('getByAttributeCode') + ->with($this->anything()) + ->willReturn($attributeMock); + $this->fieldTypeConverter->expects($this->any()) + ->method('convert') + ->with($this->anything()) + ->will($this->returnCallback( + function ($type) use ($complexType) { + static $callCount = []; + $callCount[$type] = !isset($callCount[$type]) ? 1 : ++$callCount[$type]; + + if ($type === 'string') { + return 'string'; + } elseif ($type === 'float') { + return 'float'; + } elseif ($type === 'keyword') { + return 'string'; + } else { + return $complexType; + } + } + )); + + $this->assertEquals( + $expected, + $this->provider->getFields(['storeId' => 1]) + ); + } + + /** + * @return array + */ + public function attributeProvider() + { + return [ + [ + 'category_ids', + 'select', + true, + true, + 'text', + false, + 'category_ids', + 'category_ids_value', + '', + [ + 'category_ids' => [ + 'type' => 'select', + 'index' => true + ], + 'category_ids_value' => [ + 'type' => 'string' + ], + 'store_id' => [ + 'type' => 'string', + 'index' => 'no' + ] + ] + ], + [ + 'attr_code', + 'text', + 'no', + false, + null, + false, + 'attr_code', + '', + '', + [ + 'attr_code' => [ + 'type' => 'text', + 'index' => 'no' + ], + 'store_id' => [ + 'type' => 'string', + 'index' => 'no' + ] + ], + ], + [ + 'attr_code', + 'text', + null, + false, + null, + false, + 'attr_code', + '', + '', + [ + 'attr_code' => [ + 'type' => 'text' + ], + 'store_id' => [ + 'type' => 'string', + '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/Model/Adapter/FieldMapper/ProductFieldMapperTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/ProductFieldMapperTest.php deleted file mode 100644 index 8b7ac6abbb19..000000000000 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/ProductFieldMapperTest.php +++ /dev/null @@ -1,298 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Elasticsearch\Test\Unit\Model\Adapter\FieldMapper; - -use Magento\Catalog\Api\Data\ProductAttributeInterface; -use Magento\Elasticsearch\Model\Adapter\FieldType; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; - -class ProductFieldMapperTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Elasticsearch\Model\Adapter\FieldMapper\ProductFieldMapper - */ - protected $mapper; - - /** - * @var \Magento\Eav\Model\Config|\PHPUnit_Framework_MockObject_MockObject - */ - protected $eavConfig; - - /** - * @var \Magento\Framework\Registry|\PHPUnit_Framework_MockObject_MockObject - */ - protected $coreRegistry; - - /** - * @var \Magento\Customer\Model\Session|\PHPUnit_Framework_MockObject_MockObject - */ - protected $customerSession; - - /** - * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $storeManager; - - /** - * @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute|\PHPUnit_Framework_MockObject_MockObject - */ - protected $eavAttributeResource; - - /** - * @var FieldType|\PHPUnit_Framework_MockObject_MockObject - */ - protected $fieldType; - - /** - * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $store; - - /** - * Set up test environment - * - * @return void - */ - protected function setUp() - { - $this->eavConfig = $this->getMockBuilder(\Magento\Eav\Model\Config::class) - ->disableOriginalConstructor() - ->setMethods(['getEntityType', 'getAttribute', 'getEntityAttributeCodes']) - ->getMock(); - - $this->fieldType = $this->getMockBuilder(FieldType::class) - ->disableOriginalConstructor() - ->setMethods(['getFieldType']) - ->getMock(); - - $this->customerSession = $this->getMockBuilder(\Magento\Customer\Model\Session::class) - ->disableOriginalConstructor() - ->setMethods(['getCustomerGroupId']) - ->getMock(); - - $this->storeManager = $this->storeManager = $this->getMockForAbstractClass( - \Magento\Store\Model\StoreManagerInterface::class, - [], - '', - false - ); - - $this->store = $this->getMockForAbstractClass( - \Magento\Store\Api\Data\StoreInterface::class, - [], - '', - false, - false, - true, - ['getWebsiteId', 'getRootCategoryId'] - ); - - $this->coreRegistry = $this->createMock(\Magento\Framework\Registry::class); - - $objectManager = new ObjectManagerHelper($this); - - $this->eavAttributeResource = $this->createPartialMock( - \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class, - [ - '__wakeup', - 'getBackendType', - 'getFrontendInput' - ] - ); - - $this->mapper = $objectManager->getObject( - \Magento\Elasticsearch\Model\Adapter\FieldMapper\ProductFieldMapper::class, - [ - 'eavConfig' => $this->eavConfig, - 'storeManager' => $this->storeManager, - 'fieldType' => $this->fieldType, - 'customerSession' => $this->customerSession, - 'coreRegistry' => $this->coreRegistry - ] - ); - } - - /** - * @dataProvider attributeCodeProvider - * @param string $attributeCode - * @param string $fieldName - * @param string $fieldType - * @param array $context - * - * @return void - */ - public function testGetFieldName($attributeCode, $fieldName, $fieldType, $context = []) - { - $attributeMock = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class) - ->setMethods(['getBackendType', 'getFrontendInput', 'getAttribute']) - ->disableOriginalConstructor() - ->getMock(); - - $this->customerSession->expects($this->any()) - ->method('getCustomerGroupId') - ->willReturn('0'); - - $this->storeManager->expects($this->any()) - ->method('getStore') - ->willReturn($this->store); - $this->store->expects($this->any()) - ->method('getWebsiteId') - ->willReturn('1'); - $this->store->expects($this->any()) - ->method('getRootCategoryId') - ->willReturn('1'); - - $this->eavConfig->expects($this->any())->method('getAttribute') - ->with(ProductAttributeInterface::ENTITY_TYPE_CODE, $attributeCode) - ->willReturn($attributeMock); - - $attributeMock->expects($this->any())->method('getFrontendInput') - ->will($this->returnValue('select')); - - $this->fieldType->expects($this->any())->method('getFieldType') - ->with($attributeMock) - ->willReturn($fieldType); - - $this->assertEquals( - $fieldName, - $this->mapper->getFieldName($attributeCode, $context) - ); - } - - /** - * @return void - */ - public function testGetFieldNameWithoutAttribute() - { - $this->eavConfig->expects($this->any())->method('getAttribute') - ->with(ProductAttributeInterface::ENTITY_TYPE_CODE, 'attr1') - ->willReturn(''); - - $this->assertEquals( - 'attr1', - $this->mapper->getFieldName('attr1', []) - ); - } - - /** - * @dataProvider attributeProvider - * @param string $attributeCode - * @param string $inputType - * @param array $searchAttributes - * @param array $expected - * @return void - */ - public function testGetAllAttributesTypes($attributeCode, $inputType, $searchAttributes, $expected) - { - $attributeMock = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->eavConfig->expects($this->any())->method('getEntityAttributeCodes') - ->with(ProductAttributeInterface::ENTITY_TYPE_CODE) - ->willReturn([$attributeCode]); - - $this->eavConfig->expects($this->any())->method('getAttribute') - ->with(ProductAttributeInterface::ENTITY_TYPE_CODE, $attributeCode) - ->willReturn($attributeMock); - - $this->fieldType->expects($this->once())->method('getFieldType')->willReturn(FieldType::ES_DATA_TYPE_INT); - - $attributeMock->expects($this->any()) - ->method('getIsSearchable') - ->willReturn($searchAttributes['searchable']); - $attributeMock->expects($this->any()) - ->method('getIsFilterable') - ->willReturn($searchAttributes['filterable']); - $attributeMock->expects($this->any()) - ->method('getIsFilterableInSearch') - ->willReturn($searchAttributes['filterableInSearch']); - $attributeMock->expects($this->any()) - ->method('getIsVisibleInAdvancedSearch') - ->willReturn($searchAttributes['advSearch']); - - $attributeMock->expects($this->any())->method('getFrontendInput') - ->will($this->returnValue($inputType)); - - $this->assertEquals( - $expected, - $this->mapper->getAllAttributesTypes() - ); - } - - /** - * @return array - */ - public function attributeCodeProvider() - { - return [ - ['id', 'id', 'string'], - ['status', 'status', 'string'], - ['status', 'status_value', 'string', ['type'=>'default']], - ['price', 'price_0_1', 'string', ['type'=>'default']], - ['position', 'position_category_1', 'string', ['type'=>'default']], - ['price', 'price_2_3', 'string', ['type'=>'default', 'customerGroupId'=>'2', 'websiteId'=>'3']], - ['position', 'position_category_3', 'string', ['type'=>'default', 'categoryId'=>'3']], - ['color', 'color_value', 'string', ['type'=>'text']], - ['description', 'sort_description', 'string', ['type'=>'some']], - ['*', '_all', 'string', ['type'=>'text']], - ['description', 'description_value', 'string', ['type'=>'text']], - ]; - } - - /** - * @return array - */ - public function attributeProvider() - { - return [ - [ - 'category_ids', - 'text', - ['searchable' => false, 'filterable' => false, 'filterableInSearch' => false, 'advSearch' => false], - ['category_ids' => ['type' => 'integer']] - ], - [ - 'attr_code', - 'string', - ['searchable' => false, 'filterable' => false, 'filterableInSearch' => false, 'advSearch' => false], - ['attr_code' => ['type' => 'integer', 'index' => 'no']] - ], - [ - 'attr_code', - 'string', - ['searchable' => '0', 'filterable' => '0', 'filterableInSearch' => '0', 'advSearch' => '0'], - ['attr_code' => ['type' => 'integer', 'index' => 'no']] - ], - [ - 'attr_code', - 'string', - ['searchable' => true, 'filterable' => false, 'filterableInSearch' => false, 'advSearch' => false], - ['attr_code' => ['type' => 'integer']] - ], - [ - 'attr_code', - 'string', - ['searchable' => '1', 'filterable' => '0', 'filterableInSearch' => '0', 'advSearch' => '0'], - ['attr_code' => ['type' => 'integer']] - ], - [ - 'attr_code', - 'string', - ['searchable' => false, 'filterable' => false, 'filterableInSearch' => false, 'advSearch' => true], - ['attr_code' => ['type' => 'integer']] - ], - [ - 'attr_code', - 'string', - ['searchable' => '0', 'filterable' => '0', 'filterableInSearch' => '1', 'advSearch' => '0'], - ['attr_code' => ['type' => 'integer']] - ], - ]; - } -} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Observer/CategoryProductIndexerTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Observer/CategoryProductIndexerTest.php new file mode 100644 index 000000000000..adebee0d591a --- /dev/null +++ b/app/code/Magento/Elasticsearch/Test/Unit/Observer/CategoryProductIndexerTest.php @@ -0,0 +1,136 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Test\Unit\Observer; + +use Magento\CatalogSearch\Model\Indexer\Fulltext\Processor; +use Magento\Elasticsearch\Model\Config; +use Magento\Elasticsearch\Observer\CategoryProductIndexer; +use Magento\Framework\Event; +use Magento\Framework\Event\Observer; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; + +/** + * Class CategoryProductIndexerTest + */ +class CategoryProductIndexerTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var CategoryProductIndexer + */ + private $observer; + + /** + * @var Config|\PHPUnit_Framework_MockObject_MockObject + */ + private $configMock; + + /** + * @var Processor|\PHPUnit_Framework_MockObject_MockObject + */ + private $processorMock; + + /** + * @var Observer|\PHPUnit_Framework_MockObject_MockObject + */ + private $observerMock; + + /** + * Set Up method + * + * @return void + */ + protected function setUp(): void + { + $this->configMock = $this->createMock(Config::class); + $this->processorMock = $this->createMock(Processor::class); + $this->observerMock = $this->createMock(Observer::class); + + $objectManager = new ObjectManagerHelper($this); + $this->observer = $objectManager->getObject( + CategoryProductIndexer::class, + [ + 'config' => $this->configMock, + 'processor' => $this->processorMock, + ] + ); + } + + /** + * Test if a category has changed products + * + * @return void + */ + public function testExecuteIfCategoryHasChangedProducts() + { + $this->getProductIdsWithEnabledElasticSearch(); + $this->processorMock->expects($this->once())->method('isIndexerScheduled')->willReturn(true); + $this->processorMock->expects($this->once())->method('markIndexerAsInvalid'); + $this->observer->execute($this->observerMock); + } + + /** + * Test if a category has changed products and not scheduled indexer + * + * @return void + */ + public function testExecuteIfCategoryHasChangedProductsAndNotScheduledIndexer(): void + { + $this->getProductIdsWithEnabledElasticSearch(); + $this->processorMock->expects($this->once())->method('isIndexerScheduled')->willReturn(false); + $this->processorMock->expects($this->never())->method('markIndexerAsInvalid'); + $this->observer->execute($this->observerMock); + } + + /** + * Test if a category has none changed products + * + * @return void + */ + public function testExecuteIfCategoryHasNoneChangedProducts(): void + { + /** @var Event|\PHPUnit_Framework_MockObject_MockObject $eventMock */ + $eventMock = $this->createPartialMock(Event::class, ['getProductIds']); + $this->configMock->expects($this->once())->method('isElasticsearchEnabled')->willReturn(true); + + $eventMock->expects($this->once())->method('getProductIds')->willReturn([]); + $this->observerMock->expects($this->once())->method('getEvent')->willReturn($eventMock); + + $this->processorMock->expects($this->never())->method('isIndexerScheduled'); + $this->processorMock->expects($this->never())->method('markIndexerAsInvalid'); + + $this->observer->execute($this->observerMock); + } + + /** + * Test if ElasticSearch is disabled + * + * @return void + */ + public function testExecuteIfElasticSearchIsDisabled(): void + { + /** @var Event|\PHPUnit_Framework_MockObject_MockObject $eventMock */ + $eventMock = $this->createPartialMock(Event::class, ['getProductIds']); + $this->configMock->expects($this->once())->method('isElasticsearchEnabled')->willReturn(false); + $eventMock->expects($this->never())->method('getProductIds')->willReturn([]); + $this->observer->execute($this->observerMock); + } + + /** + * Get product ids with enabled ElasticSearch + * + * @return void + */ + private function getProductIdsWithEnabledElasticSearch(): void + { + /** @var Event|\PHPUnit_Framework_MockObject_MockObject $eventMock */ + $eventMock = $this->createPartialMock(Event::class, ['getProductIds']); + $this->configMock->expects($this->once())->method('isElasticsearchEnabled')->willReturn(true); + $eventMock->expects($this->once())->method('getProductIds')->willReturn([1]); + $this->observerMock->expects($this->once())->method('getEvent')->willReturn($eventMock); + } +} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Dynamic/DataProviderTest.php b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Dynamic/DataProviderTest.php index 41be237efe53..6258a4a20d69 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Dynamic/DataProviderTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Dynamic/DataProviderTest.php @@ -320,6 +320,17 @@ public function testGetAggregation() $this->clientMock->expects($this->once()) ->method('query') + ->with($this->callback(function ($query) { + $histogramParams = $query['body']['aggregations']['prices']['histogram']; + // Assert the interval is queried as a float. See MAGETWO-95471 + if ($histogramParams['interval'] !== 10.0) { + return false; + } + if (!isset($histogramParams['min_doc_count']) || $histogramParams['min_doc_count'] !== 1) { + return false; + } + return true; + })) ->willReturn([ 'aggregations' => [ 'prices' => [ 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 8114feb09d35..d0ffc6debcd8 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/MatchTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/MatchTest.php @@ -5,14 +5,29 @@ */ namespace Magento\Elasticsearch\Test\Unit\SearchAdapter\Query\Builder; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ResolverInterface as TypeResolver; use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface; use Magento\Elasticsearch\SearchAdapter\Query\Builder\Match as MatchQueryBuilder; +use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerInterface; +use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerPool; use Magento\Framework\Search\Request\Query\Match as MatchRequestQuery; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use PHPUnit_Framework_MockObject_MockObject as MockObject; +use PHPUnit\Framework\MockObject\MockObject as MockObject; class MatchTest extends \PHPUnit\Framework\TestCase { + /** + * @var AttributeProvider|MockObject + */ + private $attributeProvider; + + /** + * @var TypeResolver|MockObject + */ + private $fieldTypeResolver; + /** * @var MatchQueryBuilder */ @@ -23,46 +38,63 @@ class MatchTest extends \PHPUnit\Framework\TestCase */ protected function setUp() { + $this->attributeProvider = $this->createMock(AttributeProvider::class); + $this->fieldTypeResolver = $this->createMock(TypeResolver::class); + + $valueTransformerPoolMock = $this->createMock(ValueTransformerPool::class); + $valueTransformerMock = $this->createMock(ValueTransformerInterface::class); + $valueTransformerPoolMock->method('get') + ->willReturn($valueTransformerMock); + $valueTransformerMock->method('transform') + ->willReturnArgument(0); + $this->matchQueryBuilder = (new ObjectManager($this))->getObject( MatchQueryBuilder::class, [ 'fieldMapper' => $this->getFieldMapper(), 'preprocessorContainer' => [], + 'attributeProvider' => $this->attributeProvider, + 'fieldTypeResolver' => $this->fieldTypeResolver, + 'valueTransformerPool' => $valueTransformerPoolMock, ] ); } /** * Tests that method constructs a correct select query. - * @see MatchQueryBuilder::build - * - * @dataProvider queryValuesInvariantsProvider * - * @param string $rawQueryValue - * @param string $errorMessage + * @see MatchQueryBuilder::build */ - public function testBuild($rawQueryValue, $errorMessage) + public function testBuild() { - $this->assertSelectQuery( - $this->matchQueryBuilder->build([], $this->getMatchRequestQuery($rawQueryValue), 'not'), - $errorMessage - ); - } + $attributeAdapter = $this->createMock(AttributeAdapter::class); + $this->attributeProvider->expects($this->once()) + ->method('getByAttributeCode') + ->with('some_field') + ->willReturn($attributeAdapter); + $this->fieldTypeResolver->expects($this->once()) + ->method('getFieldType') + ->with($attributeAdapter) + ->willReturn('text'); + + $rawQueryValue = 'query_value'; + $selectQuery = $this->matchQueryBuilder->build([], $this->getMatchRequestQuery($rawQueryValue), 'not'); - /** - * @link https://dev.mysql.com/doc/refman/5.7/en/fulltext-boolean.html Fulltext-boolean search docs. - * - * @return array - */ - public function queryValuesInvariantsProvider() - { - return [ - ['query_value', 'Select query field must match simple raw query value.'], - ['query_value+', 'Specifying a trailing plus sign causes InnoDB to report a syntax error.'], - ['query_value-', 'Specifying a trailing minus sign causes InnoDB to report a syntax error.'], - ['query_@value', 'The @ symbol is reserved for use by the @distance proximity search operator.'], - ['query_value+@', 'The @ symbol is reserved for use by the @distance proximity search operator.'], + $expectedSelectQuery = [ + 'bool' => [ + 'must_not' => [ + [ + 'match' => [ + 'some_field' => [ + 'query' => $rawQueryValue, + 'boost' => 43, + ], + ], + ], + ], + ], ]; + $this->assertEquals($expectedSelectQuery, $selectQuery); } /** @@ -76,6 +108,16 @@ public function queryValuesInvariantsProvider() */ public function testBuildMatchQuery($rawQueryValue, $queryValue, $match) { + $attributeAdapter = $this->createMock(AttributeAdapter::class); + $this->attributeProvider->expects($this->once()) + ->method('getByAttributeCode') + ->with('some_field') + ->willReturn($attributeAdapter); + $this->fieldTypeResolver->expects($this->once()) + ->method('getFieldType') + ->with($attributeAdapter) + ->willReturn('text'); + $query = $this->matchQueryBuilder->build([], $this->getMatchRequestQuery($rawQueryValue), 'should'); $expectedSelectQuery = [ @@ -111,30 +153,6 @@ public function matchProvider() ]; } - /** - * @param array $selectQuery - * @param string $errorMessage - */ - private function assertSelectQuery($selectQuery, $errorMessage) - { - $expectedSelectQuery = [ - 'bool' => [ - 'must_not' => [ - [ - 'match' => [ - 'some_field' => [ - 'query' => 'query_value', - 'boost' => 43, - ], - ], - ], - ], - ], - ]; - - $this->assertEquals($expectedSelectQuery, $selectQuery, $errorMessage); - } - /** * Gets fieldMapper mock object. * diff --git a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/SortTest.php b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/SortTest.php new file mode 100644 index 000000000000..efd907369412 --- /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 9ea241b2fbf5..d89e42045720 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 a821506f5ef6..c6ac38c1e400 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/adminhtml/events.xml b/app/code/Magento/Elasticsearch/etc/adminhtml/events.xml new file mode 100644 index 000000000000..a9b60aee1e69 --- /dev/null +++ b/app/code/Magento/Elasticsearch/etc/adminhtml/events.xml @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd"> + <event name="catalog_category_change_products"> + <observer name="category_product_elasticsearch_indexer" instance="Magento\Elasticsearch\Observer\CategoryProductIndexer"/> + </event> +</config> diff --git a/app/code/Magento/Elasticsearch/etc/adminhtml/system.xml b/app/code/Magento/Elasticsearch/etc/adminhtml/system.xml index b9fddc46f78e..dd42b408ff75 100644 --- a/app/code/Magento/Elasticsearch/etc/adminhtml/system.xml +++ b/app/code/Magento/Elasticsearch/etc/adminhtml/system.xml @@ -92,15 +92,15 @@ <field id="elasticsearch5_username" translate="label" type="text" sortOrder="65" showInDefault="1" showInWebsite="0" showInStore="0"> <label>Elasticsearch HTTP Username</label> <depends> - <field id="elasticsearch_enable_auth">1</field> <field id="engine">elasticsearch5</field> + <field id="elasticsearch5_enable_auth">1</field> </depends> </field> <field id="elasticsearch5_password" translate="label" type="text" sortOrder="66" showInDefault="1" showInWebsite="0" showInStore="0"> <label>Elasticsearch HTTP Password</label> <depends> - <field id="elasticsearch_enable_auth">1</field> <field id="engine">elasticsearch5</field> + <field id="elasticsearch5_enable_auth">1</field> </depends> </field> <field id="elasticsearch5_server_timeout" translate="label" type="text" sortOrder="67" showInDefault="1" showInWebsite="0" showInStore="0"> diff --git a/app/code/Magento/Elasticsearch/etc/di.xml b/app/code/Magento/Elasticsearch/etc/di.xml index 0cfaba845fd6..9732ae822643 100644 --- a/app/code/Magento/Elasticsearch/etc/di.xml +++ b/app/code/Magento/Elasticsearch/etc/di.xml @@ -7,6 +7,130 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <preference for="Magento\Elasticsearch\Model\Adapter\FieldMapperInterface" type="Magento\Elasticsearch\Model\Adapter\FieldMapper\FieldMapperResolver" /> + <preference for="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface" type="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\CompositeResolver" /> + <preference for="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ResolverInterface" type="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\CompositeResolver" /> + <preference for="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\ResolverInterface" type="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\IndexResolver" /> + <preference for="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\ConverterInterface" type="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\Converter" /> + <preference for="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface" type="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter" /> + <preference for="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProviderInterface" type="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\CompositeFieldProvider" /> + <type name="Magento\Elasticsearch\Model\Config"> + <arguments> + <argument name="engineList" xsi:type="array"> + <item name="elasticsearch" xsi:type="string">elasticsearch</item> + <item name="elasticsearch5" xsi:type="string">elasticsearch5</item> + </argument> + </arguments> + </type> + + <virtualType name="Magento\Elasticsearch\Model\Layer\Search\Context" type="Magento\Catalog\Model\Layer\Search\Context"> + <arguments> + <argument name="collectionProvider" xsi:type="object">elasticsearchLayerSearchItemCollectionProvider</argument> + <argument name="stateKey" xsi:type="object">Magento\CatalogSearch\Model\Layer\Search\StateKey</argument> + </arguments> + </virtualType> + <type name="Magento\Catalog\Model\Layer\Search"> + <arguments> + <argument name="context" xsi:type="object">Magento\Elasticsearch\Model\Layer\Search\Context</argument> + </arguments> + </type> + <virtualType name="Magento\Elasticsearch\Model\Layer\Category\Context" type="Magento\Catalog\Model\Layer\Category\Context"> + <arguments> + <argument name="collectionProvider" xsi:type="object">elasticsearchLayerCategoryItemCollectionProvider</argument> + </arguments> + </virtualType> + <type name="Magento\Catalog\Model\Layer\Category"> + <arguments> + <argument name="context" xsi:type="object">Magento\Elasticsearch\Model\Layer\Category\Context</argument> + </arguments> + </type> + <virtualType name="elasticsearchFulltextSearchCollection" type="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection"> + <arguments> + <argument name="searchRequestName" xsi:type="string">quick_search_container</argument> + <argument name="searchCriteriaResolverFactory" xsi:type="object">elasticsearchSearchCriteriaResolverFactory</argument> + <argument name="searchResultApplierFactory" xsi:type="object">elasticsearchSearchResultApplier\Factory</argument> + <argument name="totalRecordsResolverFactory" xsi:type="object">elasticsearchTotalRecordsResolver\Factory</argument> + </arguments> + </virtualType> + <virtualType name="elasticsearchFulltextSearchCollectionFactory" type="Magento\CatalogSearch\Model\ResourceModel\Fulltext\SearchCollectionFactory"> + <arguments> + <argument name="instanceName" xsi:type="string">elasticsearchFulltextSearchCollection</argument> + </arguments> + </virtualType> + <virtualType name="elasticsearchLayerSearchItemCollectionProvider" type="Magento\Elasticsearch\Model\Layer\Search\ItemCollectionProvider"> + <arguments> + <argument name="factories" xsi:type="array"> + <item name="mysql" xsi:type="object">Magento\CatalogSearch\Model\ResourceModel\Fulltext\SearchCollectionFactory</item> + <item name="elasticsearch" xsi:type="object">elasticsearchFulltextSearchCollectionFactory</item> + <item name="elasticsearch5" xsi:type="object">elasticsearchFulltextSearchCollectionFactory</item> + </argument> + </arguments> + </virtualType> + <virtualType name="elasticsearchCategoryCollection" type="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection"> + <arguments> + <argument name="searchRequestName" xsi:type="string">catalog_view_container</argument> + <argument name="searchCriteriaResolverFactory" xsi:type="object">elasticsearchSearchCriteriaResolverFactory</argument> + <argument name="searchResultApplierFactory" xsi:type="object">elasticsearchSearchResultApplier\Factory</argument> + <argument name="totalRecordsResolverFactory" xsi:type="object">elasticsearchTotalRecordsResolver\Factory</argument> + </arguments> + </virtualType> + <virtualType name="elasticsearchCategoryCollectionFactory" type="Magento\CatalogSearch\Model\ResourceModel\Fulltext\SearchCollectionFactory"> + <arguments> + <argument name="instanceName" xsi:type="string">elasticsearchCategoryCollection</argument> + </arguments> + </virtualType> + <virtualType name="elasticsearchLayerCategoryItemCollectionProvider" type="Magento\Elasticsearch\Model\Layer\Category\ItemCollectionProvider"> + <arguments> + <argument name="factories" xsi:type="array"> + <item name="mysql" xsi:type="object">Magento\CatalogSearch\Model\ResourceModel\Fulltext\CollectionFactory</item> + <item name="elasticsearch" xsi:type="object">elasticsearchCategoryCollectionFactory</item> + <item name="elasticsearch5" xsi:type="object">elasticsearchCategoryCollectionFactory</item> + </argument> + </arguments> + </virtualType> + <virtualType name="elasticsearchAdvancedCollection" type="Magento\CatalogSearch\Model\ResourceModel\Advanced\Collection"> + <arguments> + <argument name="searchRequestName" xsi:type="string">advanced_search_container</argument> + <argument name="searchCriteriaResolverFactory" xsi:type="object">elasticsearchSearchCriteriaResolverFactory</argument> + <argument name="searchResultApplierFactory" xsi:type="object">elasticsearchSearchResultApplier\Factory</argument> + <argument name="totalRecordsResolverFactory" xsi:type="object">elasticsearchTotalRecordsResolver\Factory</argument> + </arguments> + </virtualType> + <virtualType name="elasticsearchAdvancedCollectionFactory" type="Magento\CatalogSearch\Model\ResourceModel\Advanced\CollectionFactory"> + <arguments> + <argument name="instanceName" xsi:type="string">elasticsearchAdvancedCollection</argument> + </arguments> + </virtualType> + <type name="Magento\CatalogSearch\Model\Search\ItemCollectionProvider"> + <arguments> + <argument name="factories" xsi:type="array"> + <item name="elasticsearch" xsi:type="object">elasticsearchAdvancedCollectionFactory</item> + <item name="elasticsearch5" xsi:type="object">elasticsearchAdvancedCollectionFactory</item> + </argument> + </arguments> + </type> + <type name="Magento\CatalogSearch\Model\Advanced\ProductCollectionPrepareStrategyProvider"> + <arguments> + <argument name="strategies" xsi:type="array"> + <item name="elasticsearch" xsi:type="object">Magento\Elasticsearch\Model\Advanced\ProductCollectionPrepareStrategy</item> + <item name="elasticsearch5" xsi:type="object">Magento\Elasticsearch\Model\Advanced\ProductCollectionPrepareStrategy</item> + </argument> + </arguments> + </type> + <virtualType name="elasticsearchSearchCriteriaResolverFactory" type="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverFactory"> + <arguments> + <argument name="instanceName" xsi:type="string">Magento\Elasticsearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolver</argument> + </arguments> + </virtualType> + <virtualType name="elasticsearchSearchResultApplier\Factory" type="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierFactory"> + <arguments> + <argument name="instanceName" xsi:type="string">Magento\Elasticsearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplier</argument> + </arguments> + </virtualType> + <virtualType name="elasticsearchTotalRecordsResolver\Factory" type="Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverFactory"> + <arguments> + <argument name="instanceName" xsi:type="string">Magento\Elasticsearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolver</argument> + </arguments> + </virtualType> <type name="Magento\Elasticsearch\Model\Adapter\FieldMapper\FieldMapperResolver"> <arguments> <argument name="fieldMappers" xsi:type="array"> @@ -15,7 +139,7 @@ </arguments> </type> <preference for="Magento\Elasticsearch\Model\Adapter\DataMapperInterface" type="Magento\Elasticsearch\Model\Adapter\DataMapper\DataMapperResolver" /> - <virtualType name="AdditionalFieldsForElasticsearchDataMapper" type="Magento\AdvancedSearch\Model\Adapter\DataMapper\AdditionalFieldsProvider"> + <virtualType name="additionalFieldsProviderForElasticsearch" type="Magento\AdvancedSearch\Model\Adapter\DataMapper\AdditionalFieldsProvider"> <arguments> <argument name="fieldsProviders" xsi:type="array"> <item name="categories" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\BatchDataMapper\CategoryFieldsProviderProxy</item> @@ -25,7 +149,7 @@ </virtualType> <type name="Magento\Elasticsearch\Model\Adapter\BatchDataMapper\ProductDataMapper"> <arguments> - <argument name="additionalFieldsProvider" xsi:type="object">AdditionalFieldsForElasticsearchDataMapper</argument> + <argument name="additionalFieldsProvider" xsi:type="object">additionalFieldsProviderForElasticsearch</argument> </arguments> </type> <preference for="Magento\Elasticsearch\Model\Adapter\BatchDataMapperInterface" type="Magento\Elasticsearch\Model\Adapter\BatchDataMapper\DataMapperResolver" /> @@ -62,7 +186,7 @@ </argument> </arguments> </type> - <type name="\Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\ProductFieldMapperProxy"> + <type name="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\ProductFieldMapperProxy"> <arguments> <argument name="productFieldMappers" xsi:type="array"> <item name="elasticsearch" xsi:type="object">Magento\Elasticsearch\Model\Adapter\FieldMapper\ProductFieldMapper</item> @@ -271,4 +395,167 @@ </argument> </arguments> </type> + <type name="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\BatchDataMapper\CategoryFieldsProvider"> + <arguments> + <argument name="fieldNameResolver" xsi:type="object">elasticsearch5FieldNameResolver</argument> + </arguments> + </type> + <type name="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\DataMapper\ProductDataMapper"> + <arguments> + <argument name="fieldNameResolver" xsi:type="object">elasticsearch5FieldNameResolver</argument> + </arguments> + </type> + <type name="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\CompositeResolver"> + <arguments> + <argument name="items" xsi:type="array"> + <item name="notEav" xsi:type="object">\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\NotEavAttribute</item> + <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\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\DefaultResolver</item> + </argument> + </arguments> + </type> + <virtualType name="elasticsearch5FieldNameResolver" 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">elasticsearch5FieldNameDefaultResolver</item> + </argument> + </arguments> + </virtualType> + <virtualType name="elasticsearch5FieldNameDefaultResolver" type="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\DefaultResolver"> + <arguments> + <argument name="fieldTypeResolver" xsi:type="object">\Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\CompositeResolver</argument> + <argument name="fieldTypeConverter" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter</argument> + </arguments> + </virtualType> + <type name="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\CompositeResolver"> + <arguments> + <argument name="items" xsi:type="array"> + <item name="integer" xsi:type="object">\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\IntegerType</item> + <item name="datetime" xsi:type="object">\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\DateTimeType</item> + <item name="float" xsi:type="object">\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\FloatType</item> + <item name="default" xsi:type="object">\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\DefaultResolver</item> + </argument> + </arguments> + </type> + <type name="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\CompositeResolver"> + <arguments> + <argument name="items" xsi:type="array"> + <item name="keyword" xsi:type="object">\Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\KeywordType</item> + <item name="integer" xsi:type="object">\Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\IntegerType</item> + <item name="datetime" xsi:type="object">elasticsearch5FieldTypeDateTimeResolver</item> + <item name="float" xsi:type="object">elasticsearch5FieldTypeFloatResolver</item> + <item name="default" xsi:type="object">elasticsearch5FieldTypeDefaultResolver</item> + </argument> + </arguments> + </type> + <type name="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\CompositeFieldProvider"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="static" xsi:type="object">\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\StaticField</item> + <item name="dynamic" xsi:type="object">\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\DynamicField</item> + </argument> + </arguments> + </type> + <virtualType name="elasticsearch5FieldProvider" type="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\CompositeFieldProvider"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="static" xsi:type="object">elasticsearch5StaticFieldProvider</item> + <item name="dynamic" xsi:type="object">elasticsearch5DynamicFieldProvider</item> + </argument> + </arguments> + </virtualType> + <virtualType name="elasticsearch5StaticFieldProvider" type="\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\StaticField"> + <arguments> + <argument name="fieldTypeConverter" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter</argument> + <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"> + <arguments> + <argument name="fieldTypeConverter" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter</argument> + <argument name="indexTypeConverter" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\Converter</argument> + </arguments> + </virtualType> + <type name="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\KeywordType"> + <arguments> + <argument name="fieldTypeConverter" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter</argument> + </arguments> + </type> + <type name="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\IntegerType"> + <arguments> + <argument name="fieldTypeConverter" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter</argument> + </arguments> + </type> + <virtualType name="elasticsearch5FieldTypeDateTimeResolver" type="\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\DateTimeType"> + <arguments> + <argument name="fieldTypeConverter" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter</argument> + </arguments> + </virtualType> + <virtualType name="elasticsearch5FieldTypeFloatResolver" type="\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\FloatType"> + <arguments> + <argument name="fieldTypeConverter" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter</argument> + </arguments> + </virtualType> + <virtualType name="elasticsearch5FieldTypeDefaultResolver" type="\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\DefaultResolver"> + <arguments> + <argument name="fieldTypeConverter" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter</argument> + </arguments> + </virtualType> + <type name="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\ProductFieldMapper"> + <arguments> + <argument name="fieldProvider" xsi:type="object">elasticsearch5FieldProvider</argument> + <argument name="fieldNameResolver" xsi:type="object">elasticsearch5FieldNameResolver</argument> + </arguments> + </type> + <type name="Magento\Elasticsearch\Model\Adapter\FieldMapper\ProductFieldMapper"> + <arguments> + <argument name="attributeAdapterProvider" xsi:type="object">Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider</argument> + <argument name="fieldProvider" xsi:type="object">Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProviderInterface</argument> + <argument name="fieldNameResolver" xsi:type="object">Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface</argument> + </arguments> + </type> + <type name="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\IndexResolver"> + <arguments> + <argument name="converter" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\Converter</argument> + <argument name="fieldTypeConverter" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter</argument> + <argument name="fieldTypeResolver" xsi:type="object">\Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\CompositeResolver</argument> + </arguments> + </type> + <type name="Magento\Search\Model\Search\PageSizeProvider"> + <arguments> + <argument name="pageSizeBySearchEngine" xsi:type="array"> + <item name="elasticsearch" xsi:type="number">10000</item> + <item name="elasticsearch5" xsi:type="number">10000</item> + </argument> + </arguments> + </type> + <type name="Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerPool"> + <arguments> + <argument name="valueTransformers" xsi:type="array"> + <item name="default" xsi:type="object">Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\TextTransformer</item> + <item name="date" xsi:type="object">Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\DateTransformer</item> + <item name="float" xsi:type="object">Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\FloatTransformer</item> + <item name="integer" xsi:type="object">Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\IntegerTransformer</item> + </argument> + </arguments> + </type> + <type name="Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\TextTransformer"> + <arguments> + <argument name="preprocessors" xsi:type="array"> + <item name="stopwordsPreprocessor" xsi:type="object">Magento\Elasticsearch\SearchAdapter\Query\Preprocessor\Stopwords</item> + <item name="synonymsPreprocessor" xsi:type="object">Magento\Search\Adapter\Query\Preprocessor\Synonyms</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Elasticsearch/etc/esconfig.xml b/app/code/Magento/Elasticsearch/etc/esconfig.xml index 49124c4b7039..e540251a3c72 100644 --- a/app/code/Magento/Elasticsearch/etc/esconfig.xml +++ b/app/code/Magento/Elasticsearch/etc/esconfig.xml @@ -25,6 +25,5 @@ <fr_FR>stopwords_fr_FR.csv</fr_FR> <nl_NL>stopwords_nl_NL.csv</nl_NL> <pt_BR>stopwords_pt_BR.csv</pt_BR> - <zh_Hans_CN>stopwords_zh_Hans_CN.csv</zh_Hans_CN> </stopwords_file> </config> diff --git a/app/code/Magento/Elasticsearch/etc/indexer.xml b/app/code/Magento/Elasticsearch/etc/indexer.xml index 8d75a59f8404..f22eb8f0bd39 100644 --- a/app/code/Magento/Elasticsearch/etc/indexer.xml +++ b/app/code/Magento/Elasticsearch/etc/indexer.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Indexer/etc/indexer.xsd"> <indexer id="catalogsearch_fulltext"> <dependencies> + <indexer id="catalog_category_product" /> <indexer id="cataloginventory_stock" /> <indexer id="catalog_product_price" /> </dependencies> diff --git a/app/code/Magento/Elasticsearch/etc/stopwords/stopwords_zh_Hans_CN.csv b/app/code/Magento/Elasticsearch/etc/stopwords/stopwords_zh_Hans_CN.csv deleted file mode 100644 index 0b3ef8f89fb3..000000000000 --- a/app/code/Magento/Elasticsearch/etc/stopwords/stopwords_zh_Hans_CN.csv +++ /dev/null @@ -1,125 +0,0 @@ -的 -一 -不 -在 -人 -有 -是 -为 -以 -于 -上 -他 -而 -后 -之 -来 -及 -了 -因 -下 -可 -到 -由 -这 -与 -也 -此 -但 -并 -个 -其 -已 -无 -小 -我 -们 -起 -最 -再 -今 -去 -好 -只 -又 -或 -很 -亦 -某 -把 -那 -你 -乃 -它 -吧 -被 -比 -别 -趁 -当 -从 -到 -得 -打 -凡 -儿 -尔 -该 -各 -给 -跟 -和 -何 -还 -即 -几 -既 -看 -据 -距 -靠 -啦 -了 -另 -么 -每 -们 -嘛 -拿 -哪 -那 -您 -凭 -且 -却 -让 -仍 -啥 -如 -若 -使 -谁 -虽 -随 -同 -所 -她 -哇 -嗡 -往 -哪 -些 -向 -沿 -哟 -用 -于 -咱 -则 -怎 -曾 -至 -致 -着 -诸 -自 \ No newline at end of file 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 000000000000..1b17db1a00f6 --- /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/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/LICENSE.txt b/app/code/Magento/Elasticsearch6/LICENSE.txt similarity index 100% rename from dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/LICENSE.txt rename to app/code/Magento/Elasticsearch6/LICENSE.txt diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/LICENSE_AFL.txt b/app/code/Magento/Elasticsearch6/LICENSE_AFL.txt similarity index 100% rename from dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/LICENSE_AFL.txt rename to app/code/Magento/Elasticsearch6/LICENSE_AFL.txt 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 000000000000..7532927f1dc8 --- /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 000000000000..af39b24acda5 --- /dev/null +++ b/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php @@ -0,0 +1,332 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Elasticsearch6\Model\Client; + +use Magento\Framework\Exception\LocalizedException; +use Magento\AdvancedSearch\Model\Client\ClientInterface; + +/** + * Elasticsearch client + */ +class Elasticsearch implements ClientInterface +{ + /** + * Elasticsearch Client instances + * + * @var \Elasticsearch\Client[] + */ + private $client; + + /** + * @var array + */ + private $clientOptions; + + /** + * @var bool + */ + private $pingResult; + + /** + * Initialize Elasticsearch Client + * + * @param array $options + * @param \Elasticsearch\Client|null $elasticsearchClient + * @throws LocalizedException + */ + public function __construct( + $options = [], + $elasticsearchClient = null + ) { + if (empty($options['hostname']) || ((!empty($options['enableAuth']) && + ($options['enableAuth'] == 1)) && (empty($options['username']) || empty($options['password'])))) { + throw new LocalizedException( + __('The search failed because of a search engine misconfiguration.') + ); + } + + if (!($elasticsearchClient instanceof \Elasticsearch\Client)) { + $config = $this->buildConfig($options); + $elasticsearchClient = \Elasticsearch\ClientBuilder::fromConfig($config, true); + } + $this->client[getmypid()] = $elasticsearchClient; + $this->clientOptions = $options; + } + + /** + * Get Elasticsearch Client + * + * @return \Elasticsearch\Client + */ + private function getClient() + { + $pid = getmypid(); + if (!isset($this->client[$pid])) { + $config = $this->buildConfig($this->clientOptions); + $this->client[$pid] = \Elasticsearch\ClientBuilder::fromConfig($config, true); + } + return $this->client[$pid]; + } + + /** + * Ping the Elasticsearch client + * + * @return bool + */ + public function ping() + { + if ($this->pingResult === null) { + $this->pingResult = $this->getClient()->ping(['client' => ['timeout' => $this->clientOptions['timeout']]]); + } + + return $this->pingResult; + } + + /** + * Validate connection params + * + * @return bool + */ + public function testConnection() + { + return $this->ping(); + } + + /** + * Build config. + * + * @param array $options + * @return array + */ + private function buildConfig($options = []) + { + $host = preg_replace('/http[s]?:\/\//i', '', $options['hostname']); + $protocol = parse_url($options['hostname'], PHP_URL_SCHEME); + if (!$protocol) { + $protocol = 'http'; + } + if (!empty($options['port'])) { + $host .= ':' . $options['port']; + } + if (!empty($options['enableAuth']) && ($options['enableAuth'] == 1)) { + $host = sprintf('%s://%s:%s@%s', $protocol, $options['username'], $options['password'], $host); + } + + $options['hosts'] = [$host]; + return $options; + } + + /** + * Performs bulk query over Elasticsearch index + * + * @param array $query + * @return void + */ + public function bulkQuery($query) + { + $this->getClient()->bulk($query); + } + + /** + * Creates an Elasticsearch index. + * + * @param string $index + * @param array $settings + * @return void + */ + public function createIndex($index, $settings) + { + $this->getClient()->indices()->create([ + 'index' => $index, + 'body' => $settings, + ]); + } + + /** + * Delete an Elasticsearch index. + * + * @param string $index + * @return void + */ + public function deleteIndex($index) + { + $this->getClient()->indices()->delete(['index' => $index]); + } + + /** + * Check if index is empty. + * + * @param string $index + * @return bool + */ + public function isEmptyIndex($index) + { + $stats = $this->getClient()->indices()->stats(['index' => $index, 'metric' => 'docs']); + if ($stats['indices'][$index]['primaries']['docs']['count'] == 0) { + return true; + } + return false; + } + + /** + * Updates alias. + * + * @param string $alias + * @param string $newIndex + * @param string $oldIndex + * @return void + */ + public function updateAlias($alias, $newIndex, $oldIndex = '') + { + $params['body'] = ['actions' => []]; + if ($oldIndex) { + $params['body']['actions'][] = ['remove' => ['alias' => $alias, 'index' => $oldIndex]]; + } + if ($newIndex) { + $params['body']['actions'][] = ['add' => ['alias' => $alias, 'index' => $newIndex]]; + } + + $this->getClient()->indices()->updateAliases($params); + } + + /** + * Checks whether Elasticsearch index exists + * + * @param string $index + * @return bool + */ + public function indexExists($index) + { + return $this->getClient()->indices()->exists(['index' => $index]); + } + + /** + * Exists alias. + * + * @param string $alias + * @param string $index + * @return bool + */ + public function existsAlias($alias, $index = '') + { + $params = ['name' => $alias]; + if ($index) { + $params['index'] = $index; + } + return $this->getClient()->indices()->existsAlias($params); + } + + /** + * Get alias. + * + * @param string $alias + * @return array + */ + public function getAlias($alias) + { + return $this->getClient()->indices()->getAlias(['name' => $alias]); + } + + /** + * Add mapping to Elasticsearch index + * + * @param array $fields + * @param string $index + * @param string $entityType + * @return void + */ + public function addFieldsMapping(array $fields, $index, $entityType) + { + $params = [ + 'index' => $index, + 'type' => $entityType, + 'body' => [ + $entityType => [ + 'properties' => [ + '_search' => [ + 'type' => 'text' + ], + ], + 'dynamic_templates' => [ + [ + 'price_mapping' => [ + 'match' => 'price_*', + 'match_mapping_type' => 'string', + 'mapping' => [ + 'type' => 'float', + 'store' => true, + ], + ], + ], + [ + 'string_mapping' => [ + 'match' => '*', + 'match_mapping_type' => 'string', + 'mapping' => [ + 'type' => 'text', + 'index' => false, + 'copy_to' => '_search' + ], + ], + ], + [ + 'position_mapping' => [ + 'match' => 'position_*', + 'match_mapping_type' => 'string', + 'mapping' => [ + 'type' => 'int', + ], + ], + ], + ], + ], + ], + ]; + + foreach ($fields as $field => $fieldInfo) { + $params['body'][$entityType]['properties'][$field] = $fieldInfo; + } + + $this->getClient()->indices()->putMapping($params); + } + + /** + * Delete mapping in Elasticsearch index + * + * @param string $index + * @param string $entityType + * @return void + */ + public function deleteMapping($index, $entityType) + { + $this->getClient()->indices()->deleteMapping([ + 'index' => $index, + 'type' => $entityType, + ]); + } + + /** + * Execute search by $query + * + * @param array $query + * @return array + */ + public function query($query) + { + return $this->getClient()->search($query); + } + + /** + * Execute suggest query + * + * @param array $query + * @return array + */ + public function suggest($query) + { + return $this->getClient()->suggest($query); + } +} diff --git a/app/code/Magento/Elasticsearch6/Model/DataProvider/Suggestions.php b/app/code/Magento/Elasticsearch6/Model/DataProvider/Suggestions.php new file mode 100644 index 000000000000..d05471734bb8 --- /dev/null +++ b/app/code/Magento/Elasticsearch6/Model/DataProvider/Suggestions.php @@ -0,0 +1,275 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Elasticsearch6\Model\DataProvider; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProviderInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Search\Model\QueryInterface; +use Magento\AdvancedSearch\Model\SuggestedQueriesInterface; +use Magento\Elasticsearch\Model\Config; +use Magento\Elasticsearch\SearchAdapter\ConnectionManager; +use Magento\Search\Model\QueryResultFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Elasticsearch\SearchAdapter\SearchIndexNameResolver; +use Magento\Store\Model\StoreManagerInterface as StoreManager; + +/** + * Class Suggestions + */ +class Suggestions implements SuggestedQueriesInterface +{ + /** + * @var Config + */ + private $config; + + /** + * @var QueryResultFactory + */ + private $queryResultFactory; + + /** + * @var ConnectionManager + */ + private $connectionManager; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var SearchIndexNameResolver + */ + private $searchIndexNameResolver; + + /** + * @var StoreManager + */ + private $storeManager; + + /** + * @var FieldProviderInterface + */ + private $fieldProvider; + + /** + * Suggestions constructor. + * + * @param ScopeConfigInterface $scopeConfig + * @param Config $config + * @param QueryResultFactory $queryResultFactory + * @param ConnectionManager $connectionManager + * @param SearchIndexNameResolver $searchIndexNameResolver + * @param StoreManager $storeManager + * @param FieldProviderInterface $fieldProvider + */ + public function __construct( + ScopeConfigInterface $scopeConfig, + Config $config, + QueryResultFactory $queryResultFactory, + ConnectionManager $connectionManager, + SearchIndexNameResolver $searchIndexNameResolver, + StoreManager $storeManager, + FieldProviderInterface $fieldProvider + ) { + $this->queryResultFactory = $queryResultFactory; + $this->connectionManager = $connectionManager; + $this->scopeConfig = $scopeConfig; + $this->config = $config; + $this->searchIndexNameResolver = $searchIndexNameResolver; + $this->storeManager = $storeManager; + $this->fieldProvider = $fieldProvider; + } + + /** + * @inheritdoc + */ + public function getItems(QueryInterface $query) + { + $result = []; + if ($this->isSuggestionsAllowed()) { + $isResultsCountEnabled = $this->isResultsCountEnabled(); + + foreach ($this->getSuggestions($query) as $suggestion) { + $count = null; + if ($isResultsCountEnabled) { + $count = isset($suggestion['freq']) ? $suggestion['freq'] : null; + } + $result[] = $this->queryResultFactory->create( + [ + 'queryText' => $suggestion['text'], + 'resultsCount' => $count, + ] + ); + } + } + + return $result; + } + + /** + * @inheritdoc + */ + public function isResultsCountEnabled() + { + return $this->scopeConfig->isSetFlag( + SuggestedQueriesInterface::SEARCH_SUGGESTION_COUNT_RESULTS_ENABLED, + ScopeInterface::SCOPE_STORE + ); + } + + /** + * Get Suggestions + * + * @param QueryInterface $query + * + * @return array + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function getSuggestions(QueryInterface $query) + { + $suggestions = []; + $searchSuggestionsCount = $this->getSearchSuggestionsCount(); + + $searchQuery = $this->initQuery($query); + $searchQuery = $this->addSuggestFields($searchQuery, $searchSuggestionsCount); + + $result = $this->fetchQuery($searchQuery); + + if (is_array($result)) { + foreach ($result['suggest'] ?? [] as $suggest) { + foreach ($suggest as $token) { + foreach ($token['options'] ?? [] as $key => $suggestion) { + $suggestions[$suggestion['score'] . '_' . $key] = $suggestion; + } + } + } + ksort($suggestions); + $texts = array_unique(array_column($suggestions, 'text')); + $suggestions = array_slice( + array_intersect_key(array_values($suggestions), $texts), + 0, + $searchSuggestionsCount + ); + } + + return $suggestions; + } + + /** + * Init Search Query + * + * @param string $query + * + * @return array + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function initQuery($query) + { + $searchQuery = [ + 'index' => $this->searchIndexNameResolver->getIndexName( + $this->storeManager->getStore()->getId(), + Config::ELASTICSEARCH_TYPE_DEFAULT + ), + 'type' => Config::ELASTICSEARCH_TYPE_DEFAULT, + 'body' => [ + 'suggest' => [ + 'text' => $query->getQueryText() + ] + ], + ]; + + return $searchQuery; + } + + /** + * Build Suggest on searchable fields. + * + * @param array $searchQuery + * @param int $searchSuggestionsCount + * + * @return array + */ + private function addSuggestFields($searchQuery, $searchSuggestionsCount) + { + $fields = $this->getSuggestFields(); + foreach ($fields as $field) { + $searchQuery['body']['suggest']['phrase_' . $field] = [ + 'phrase' => [ + 'field' => $field, + 'analyzer' => 'standard', + 'size' => $searchSuggestionsCount, + 'max_errors' => 1, + 'direct_generator' => [ + [ + 'field' => $field, + 'min_word_length' => 3, + 'min_doc_freq' => 1, + ] + ], + ], + ]; + } + + return $searchQuery; + } + + /** + * Get fields to build suggest query on. + * + * @return array + */ + private function getSuggestFields() + { + $fields = array_filter($this->fieldProvider->getFields(), function ($field) { + return (($field['type'] ?? null) === 'text') && (($field['index'] ?? null) !== false); + }); + + return array_keys($fields); + } + + /** + * Fetch Query + * + * @param array $query + * @return array + */ + private function fetchQuery(array $query) + { + return $this->connectionManager->getConnection()->query($query); + } + + /** + * Get search suggestions Max Count from config + * + * @return int + */ + private function getSearchSuggestionsCount() + { + return (int) $this->scopeConfig->getValue( + SuggestedQueriesInterface::SEARCH_SUGGESTION_COUNT, + ScopeInterface::SCOPE_STORE + ); + } + + /** + * Is Search Suggestions Allowed + * + * @return bool + */ + private function isSuggestionsAllowed() + { + $isSuggestionsEnabled = $this->scopeConfig->isSetFlag( + SuggestedQueriesInterface::SEARCH_SUGGESTION_ENABLED, + ScopeInterface::SCOPE_STORE + ); + $isEnabled = $this->config->isElasticsearchEnabled(); + $isSuggestionsAllowed = ($isEnabled && $isSuggestionsEnabled); + + return $isSuggestionsAllowed; + } +} diff --git a/app/code/Magento/Elasticsearch6/README.md b/app/code/Magento/Elasticsearch6/README.md new file mode 100644 index 000000000000..8bf95ad95d14 --- /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 000000000000..a3c6e7e148f3 --- /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 000000000000..8276d0dd8dbe --- /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 000000000000..b3c60b70ffa8 --- /dev/null +++ b/app/code/Magento/Elasticsearch6/Test/Unit/Model/DataProvider/SuggestionsTest.php @@ -0,0 +1,183 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Elasticsearch6\Test\Unit\Model\DataProvider; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Elasticsearch\Model\DataProvider\Suggestions; +use Magento\Elasticsearch\Model\Config; +use Magento\Elasticsearch\SearchAdapter\ConnectionManager; +use Magento\Search\Model\QueryResultFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Elasticsearch\SearchAdapter\SearchIndexNameResolver; +use Magento\Store\Model\StoreManagerInterface as StoreManager; +use Magento\Search\Model\QueryInterface; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class SuggestionsTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Suggestions + */ + private $model; + + /** + * @var Config|\PHPUnit_Framework_MockObject_MockObject + */ + private $config; + + /** + * @var QueryResultFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $queryResultFactory; + + /** + * @var ConnectionManager|\PHPUnit_Framework_MockObject_MockObject + */ + private $connectionManager; + + /** + * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $scopeConfig; + + /** + * @var SearchIndexNameResolver|\PHPUnit_Framework_MockObject_MockObject + */ + private $searchIndexNameResolver; + + /** + * @var StoreManager|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeManager; + + /** + * @var QueryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $query; + + /** + * Set up test environment + * + * @return void + */ + protected function setUp() + { + $this->config = $this->getMockBuilder(\Magento\Elasticsearch\Model\Config::class) + ->disableOriginalConstructor() + ->setMethods(['isElasticsearchEnabled']) + ->getMock(); + + $this->queryResultFactory = $this->getMockBuilder(\Magento\Search\Model\QueryResultFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $this->connectionManager = $this->getMockBuilder(\Magento\Elasticsearch\SearchAdapter\ConnectionManager::class) + ->disableOriginalConstructor() + ->setMethods(['getConnection']) + ->getMock(); + + $this->scopeConfig = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->searchIndexNameResolver = $this + ->getMockBuilder(\Magento\Elasticsearch\SearchAdapter\SearchIndexNameResolver::class) + ->disableOriginalConstructor() + ->setMethods(['getIndexName']) + ->getMock(); + + $this->storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->query = $this->getMockBuilder(\Magento\Search\Model\QueryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $objectManager = new ObjectManagerHelper($this); + + $this->model = $objectManager->getObject( + \Magento\Elasticsearch6\Model\DataProvider\Suggestions::class, + [ + 'queryResultFactory' => $this->queryResultFactory, + 'connectionManager' => $this->connectionManager, + 'scopeConfig' => $this->scopeConfig, + 'config' => $this->config, + 'searchIndexNameResolver' => $this->searchIndexNameResolver, + 'storeManager' => $this->storeManager + ] + ); + } + + /** + * Test getItems() method + */ + public function testGetItems() + { + $this->scopeConfig->expects($this->any()) + ->method('getValue') + ->willReturn(1); + + $this->config->expects($this->any()) + ->method('isElasticsearchEnabled') + ->willReturn(1); + + $store = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->storeManager->expects($this->any()) + ->method('getStore') + ->willReturn($store); + + $store->expects($this->any()) + ->method('getId') + ->willReturn(1); + + $this->searchIndexNameResolver->expects($this->any()) + ->method('getIndexName') + ->willReturn('magento2_product_1'); + + $this->query->expects($this->any()) + ->method('getQueryText') + ->willReturn('query'); + + $client = $this->getMockBuilder(\Magento\Elasticsearch6\Model\Client\Elasticsearch::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->connectionManager->expects($this->any()) + ->method('getConnection') + ->willReturn($client); + + $client->expects($this->any()) + ->method('query') + ->willReturn([ + 'suggest' => [ + 'phrase_field' => [ + 'options' => [ + 'text' => 'query', + 'score' => 1, + 'freq' => 1, + ] + ], + ], + ]); + + $query = $this->getMockBuilder(\Magento\Search\Model\QueryResult::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->queryResultFactory->expects($this->any()) + ->method('create') + ->willReturn($query); + + $this->assertInternalType('array', $this->model->getItems($this->query)); + } +} diff --git a/app/code/Magento/Elasticsearch6/composer.json b/app/code/Magento/Elasticsearch6/composer.json new file mode 100644 index 000000000000..26b6c8c678ad --- /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 000000000000..067a0acb8c90 --- /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 000000000000..047ae977fdef --- /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 000000000000..011dfa101973 --- /dev/null +++ b/app/code/Magento/Elasticsearch6/etc/di.xml @@ -0,0 +1,205 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Elasticsearch\Model\Config"> + <arguments> + <argument name="engineList" xsi:type="array"> + <item name="elasticsearch6" xsi:type="string">elasticsearch6</item> + </argument> + </arguments> + </type> + + <type name="Magento\Search\Model\Adminhtml\System\Config\Source\Engine"> + <arguments> + <argument name="engines" xsi:type="array"> + <item name="elasticsearch6" xsi:type="string">Elasticsearch 6.0+</item> + </argument> + </arguments> + </type> + + <type name="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\BatchDataMapper\CategoryFieldsProviderProxy"> + <arguments> + <argument name="categoryFieldsProviders" xsi:type="array"> + <item name="elasticsearch6" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\BatchDataMapper\CategoryFieldsProvider</item> + </argument> + </arguments> + </type> + + <type name="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\DataMapper\ProductDataMapperProxy"> + <arguments> + <argument name="dataMappers" xsi:type="array"> + <item name="elasticsearch6" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\DataMapper\ProductDataMapper</item> + </argument> + </arguments> + </type> + + <type name="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\ProductFieldMapperProxy"> + <arguments> + <argument name="productFieldMappers" xsi:type="array"> + <item name="elasticsearch6" xsi:type="object">Magento\Elasticsearch6\Model\Adapter\FieldMapper\ProductFieldMapper</item> + </argument> + </arguments> + </type> + + <type name="Magento\AdvancedSearch\Model\Client\ClientResolver"> + <arguments> + <argument name="clientFactories" xsi:type="array"> + <item name="elasticsearch6" xsi:type="string">\Magento\Elasticsearch6\Model\Client\ElasticsearchFactory</item> + </argument> + <argument name="clientOptions" xsi:type="array"> + <item name="elasticsearch6" xsi:type="string">\Magento\Elasticsearch\Model\Config</item> + </argument> + </arguments> + </type> + + <type name="Magento\CatalogSearch\Model\Indexer\IndexerHandlerFactory"> + <arguments> + <argument name="handlers" xsi:type="array"> + <item name="elasticsearch6" xsi:type="string">Magento\Elasticsearch\Model\Indexer\IndexerHandler</item> + </argument> + </arguments> + </type> + + <type name="Magento\CatalogSearch\Model\Indexer\IndexStructureFactory"> + <arguments> + <argument name="structures" xsi:type="array"> + <item name="elasticsearch6" xsi:type="string">Magento\Elasticsearch\Model\Indexer\IndexStructure</item> + </argument> + </arguments> + </type> + + <type name="Magento\CatalogSearch\Model\ResourceModel\EngineProvider"> + <arguments> + <argument name="engines" xsi:type="array"> + <item name="elasticsearch6" xsi:type="string">Magento\Elasticsearch\Model\ResourceModel\Engine</item> + </argument> + </arguments> + </type> + + <type name="Magento\Search\Model\AdapterFactory"> + <arguments> + <argument name="adapters" xsi:type="array"> + <item name="elasticsearch6" xsi:type="string">Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Adapter</item> + </argument> + </arguments> + </type> + + <type name="Magento\Search\Model\EngineResolver"> + <arguments> + <argument name="engines" xsi:type="array"> + <item name="elasticsearch6" xsi:type="string">elasticsearch6</item> + </argument> + </arguments> + </type> + + <virtualType name="Magento\Elasticsearch6\Model\Client\ElasticsearchFactory" type="Magento\AdvancedSearch\Model\Client\ClientFactory"> + <arguments> + <argument name="clientClass" xsi:type="string">Magento\Elasticsearch6\Model\Client\Elasticsearch</argument> + </arguments> + </virtualType> + + <type name="Magento\Elasticsearch\Elasticsearch5\Model\Client\ClientFactoryProxy"> + <arguments> + <argument name="clientFactories" xsi:type="array"> + <item name="elasticsearch6" xsi:type="object">Magento\Elasticsearch6\Model\Client\ElasticsearchFactory</item> + </argument> + </arguments> + </type> + + <type name="Magento\Framework\Search\Dynamic\IntervalFactory"> + <arguments> + <argument name="intervals" xsi:type="array"> + <item name="elasticsearch6" xsi:type="string">Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Aggregation\Interval</item> + </argument> + </arguments> + </type> + + <type name="Magento\Framework\Search\Dynamic\DataProviderFactory"> + <arguments> + <argument name="dataProviders" xsi:type="array"> + <item name="elasticsearch6" xsi:type="string">Magento\Elasticsearch\SearchAdapter\Dynamic\DataProvider</item> + </argument> + </arguments> + </type> + + + <type name="Magento\AdvancedSearch\Model\SuggestedQueries"> + <arguments> + <argument name="data" xsi:type="array"> + <item name="elasticsearch6" xsi:type="string">Magento\Elasticsearch6\Model\DataProvider\Suggestions</item> + </argument> + </arguments> + </type> + + <type name="Magento\Elasticsearch6\Model\DataProvider\Suggestions"> + <arguments> + <argument name="fieldProvider" xsi:type="object">elasticsearch5FieldProvider</argument> + </arguments> + </type> + + <virtualType name="elasticsearch6FieldNameResolver" type="\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\CompositeResolver"> + <arguments> + <argument name="items" xsi:type="array"> + <item name="notEav" xsi:type="object">\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\NotEavAttribute</item> + <item name="special" xsi:type="object">\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\SpecialAttribute</item> + <item name="price" xsi:type="object">\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\Price</item> + <item name="categoryName" xsi:type="object">\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\CategoryName</item> + <item name="position" xsi:type="object">\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\Position</item> + <item name="default" xsi:type="object">\Magento\Elasticsearch6\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\DefaultResolver</item> + </argument> + </arguments> + </virtualType> + + <virtualType name="Magento\Elasticsearch6\Model\Adapter\FieldMapper\ProductFieldMapper" + type="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\ProductFieldMapper"> + <arguments> + <argument name="fieldProvider" xsi:type="object">elasticsearch5FieldProvider</argument> + <argument name="fieldNameResolver" xsi:type="object">elasticsearch6FieldNameResolver</argument> + </arguments> + </virtualType> + + <type name="Magento\Search\Model\Search\PageSizeProvider"> + <arguments> + <argument name="pageSizeBySearchEngine" xsi:type="array"> + <item name="elasticsearch6" xsi:type="number">10000</item> + </argument> + </arguments> + </type> + + <virtualType name="elasticsearchLayerCategoryItemCollectionProvider" type="Magento\Elasticsearch\Model\Layer\Category\ItemCollectionProvider"> + <arguments> + <argument name="factories" xsi:type="array"> + <item name="elasticsearch6" xsi:type="object">elasticsearchCategoryCollectionFactory</item> + </argument> + </arguments> + </virtualType> + + <type name="Magento\CatalogSearch\Model\Search\ItemCollectionProvider"> + <arguments> + <argument name="factories" xsi:type="array"> + <item name="elasticsearch6" xsi:type="object">elasticsearchAdvancedCollectionFactory</item> + </argument> + </arguments> + </type> + + <type name="Magento\CatalogSearch\Model\Advanced\ProductCollectionPrepareStrategyProvider"> + <arguments> + <argument name="strategies" xsi:type="array"> + <item name="elasticsearch6" xsi:type="object">Magento\Elasticsearch\Model\Advanced\ProductCollectionPrepareStrategy</item> + </argument> + </arguments> + </type> + + <virtualType name="elasticsearchLayerSearchItemCollectionProvider" type="Magento\Elasticsearch\Model\Layer\Search\ItemCollectionProvider"> + <arguments> + <argument name="factories" xsi:type="array"> + <item name="elasticsearch6" xsi:type="object">elasticsearchFulltextSearchCollectionFactory</item> + </argument> + </arguments> + </virtualType> +</config> diff --git a/app/code/Magento/Elasticsearch6/etc/module.xml b/app/code/Magento/Elasticsearch6/etc/module.xml new file mode 100644 index 000000000000..4fde2394dfbd --- /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 000000000000..7ab10e996eb8 --- /dev/null +++ b/app/code/Magento/Elasticsearch6/registration.php @@ -0,0 +1,11 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +\Magento\Framework\Component\ComponentRegistrar::register( + \Magento\Framework\Component\ComponentRegistrar::MODULE, + 'Magento_Elasticsearch6', + __DIR__ +); diff --git a/app/code/Magento/Email/Controller/Adminhtml/Email/Template/Edit.php b/app/code/Magento/Email/Controller/Adminhtml/Email/Template/Edit.php index 240b688402b7..998cf62a83ab 100644 --- a/app/code/Magento/Email/Controller/Adminhtml/Email/Template/Edit.php +++ b/app/code/Magento/Email/Controller/Adminhtml/Email/Template/Edit.php @@ -6,7 +6,9 @@ */ namespace Magento\Email\Controller\Adminhtml\Email\Template; -class Edit extends \Magento\Email\Controller\Adminhtml\Email\Template +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Edit extends \Magento\Email\Controller\Adminhtml\Email\Template implements HttpGetActionInterface { /** * Edit transactional email action diff --git a/app/code/Magento/Email/Controller/Adminhtml/Email/Template/Index.php b/app/code/Magento/Email/Controller/Adminhtml/Email/Template/Index.php index 11b38ae8e503..013f97b9ad31 100644 --- a/app/code/Magento/Email/Controller/Adminhtml/Email/Template/Index.php +++ b/app/code/Magento/Email/Controller/Adminhtml/Email/Template/Index.php @@ -6,7 +6,9 @@ */ namespace Magento\Email\Controller\Adminhtml\Email\Template; -class Index extends \Magento\Email\Controller\Adminhtml\Email\Template +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\Email\Controller\Adminhtml\Email\Template implements HttpGetActionInterface { /** * Index action diff --git a/app/code/Magento/Email/Controller/Adminhtml/Email/Template/NewAction.php b/app/code/Magento/Email/Controller/Adminhtml/Email/Template/NewAction.php index f5024943e833..87f5470440db 100644 --- a/app/code/Magento/Email/Controller/Adminhtml/Email/Template/NewAction.php +++ b/app/code/Magento/Email/Controller/Adminhtml/Email/Template/NewAction.php @@ -6,7 +6,9 @@ */ namespace Magento\Email\Controller\Adminhtml\Email\Template; -class NewAction extends \Magento\Email\Controller\Adminhtml\Email\Template +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class NewAction extends \Magento\Email\Controller\Adminhtml\Email\Template implements HttpGetActionInterface { /** * New transactional email action diff --git a/app/code/Magento/Email/Model/Template.php b/app/code/Magento/Email/Model/Template.php index 8724b725ae48..ef2c4341dafa 100644 --- a/app/code/Magento/Email/Model/Template.php +++ b/app/code/Magento/Email/Model/Template.php @@ -326,7 +326,7 @@ public function getVariablesOptionArray($withGroup = false) $optionArray[] = ['value' => '{{' . $value . '}}', 'label' => __('%1', $label)]; } if ($withGroup) { - $optionArray = ['label' => __('Template Variables'), 'value' => $optionArray]; + $optionArray = [['label' => __('Template Variables'), 'value' => $optionArray]]; } } return $optionArray; @@ -418,6 +418,8 @@ public function setOptions(array $options) } /** + * Return filter factory. + * * @return \Magento\Email\Model\Template\FilterFactory */ protected function getFilterFactory() diff --git a/app/code/Magento/Email/Model/Transport.php b/app/code/Magento/Email/Model/Transport.php index 5a4d64e6d6c0..90a4e6571c9b 100644 --- a/app/code/Magento/Email/Model/Transport.php +++ b/app/code/Magento/Email/Model/Transport.php @@ -31,6 +31,23 @@ class Transport implements TransportInterface */ const XML_PATH_SENDING_RETURN_PATH_EMAIL = 'system/smtp/return_path_email'; + /** + * Whether return path should be set or no. + * + * Possible values are: + * 0 - no + * 1 - yes (set value as FROM address) + * 2 - use custom value + * + * @var int + */ + private $isSetReturnPath; + + /** + * @var string|null + */ + private $returnPathValue; + /** * @var Sendmail */ @@ -51,25 +68,15 @@ public function __construct( ScopeConfigInterface $scopeConfig, $parameters = null ) { - /* configuration of whether return path should be set or no. Possible values are: - * 0 - no - * 1 - yes (set value as FROM address) - * 2 - use custom value - * @see Magento\Config\Model\Config\Source\Yesnocustom - */ - $isSetReturnPath = $scopeConfig->getValue( + $this->isSetReturnPath = (int) $scopeConfig->getValue( self::XML_PATH_SENDING_SET_RETURN_PATH, ScopeInterface::SCOPE_STORE ); - $returnPathValue = $scopeConfig->getValue( + $this->returnPathValue = $scopeConfig->getValue( self::XML_PATH_SENDING_RETURN_PATH_EMAIL, ScopeInterface::SCOPE_STORE ); - if ($isSetReturnPath == '2' && $returnPathValue !== null) { - $parameters .= ' -f' . \escapeshellarg($returnPathValue); - } - $this->zendTransport = new Sendmail($parameters); $this->message = $message; } @@ -80,9 +87,16 @@ public function __construct( public function sendMessage() { try { - $this->zendTransport->send( - Message::fromString($this->message->getRawMessage()) - ); + $zendMessage = Message::fromString($this->message->getRawMessage())->setEncoding('utf-8'); + if (2 === $this->isSetReturnPath && $this->returnPathValue) { + $zendMessage->setSender($this->returnPathValue); + } elseif (1 === $this->isSetReturnPath && $zendMessage->getFrom()->count()) { + $fromAddressList = $zendMessage->getFrom(); + $fromAddressList->rewind(); + $zendMessage->setSender($fromAddressList->current()->getEmail()); + } + + $this->zendTransport->send($zendMessage); } catch (\Exception $e) { throw new MailException(new Phrase($e->getMessage()), $e); } diff --git a/app/code/Magento/Email/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Email/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 000000000000..5086d74efa60 --- /dev/null +++ b/app/code/Magento/Email/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminMenuMarketingCommunicationsEmailTemplates"> + <data key="pageTitle">Email Templates</data> + <data key="title">Email Templates</data> + <data key="dataUiId">magento-email-template</data> + </entity> +</entities> diff --git a/app/code/Magento/Email/Test/Mftf/Test/AdminMarketingEmailTemplatesNavigateMenuTest.xml b/app/code/Magento/Email/Test/Mftf/Test/AdminMarketingEmailTemplatesNavigateMenuTest.xml new file mode 100644 index 000000000000..d512fc263ef2 --- /dev/null +++ b/app/code/Magento/Email/Test/Mftf/Test/AdminMarketingEmailTemplatesNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMarketingEmailTemplatesNavigateMenuTest"> + <annotations> + <features value="Email"/> + <stories value="Menu Navigation"/> + <title value="Admin marketing email templates navigate menu test"/> + <description value="Admin should be able to navigate to Marketing > Email Templates"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14173"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToMarketingEmailTemplatesPage"> + <argument name="menuUiId" value="{{AdminMenuMarketing.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuMarketingCommunicationsEmailTemplates.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuMarketingCommunicationsEmailTemplates.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Email/Test/Mftf/Test/TransactionalEmailsLogoUploadTest.xml b/app/code/Magento/Email/Test/Mftf/Test/TransactionalEmailsLogoUploadTest.xml new file mode 100644 index 000000000000..c3870417fa5e --- /dev/null +++ b/app/code/Magento/Email/Test/Mftf/Test/TransactionalEmailsLogoUploadTest.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="TransactionalEmailsLogoUploadTest"> + <annotations> + <features value="Email"/> + <stories value="Email"/> + <title value="MC-13908: Uploading a Transactional Emails logo"/> + <description value="Transactional Emails Logo should be able to be uploaded in the admin and previewed"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-13908"/> + <group value="LogoUpload"/> + </annotations> + <!--Login to Admin Area--> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminArea"/> + </before> + <!--Logout from Admin Area--> + <after> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + <!--Navigate to content->Design->Config page--> + <amOnPage url="{{DesignConfigPage.url}}" stepKey="navigateToDesignConfigPage" /> + <waitForPageLoad stepKey="waitForPageloadToViewDesignConfigPage"/> + <click selector="{{AdminDesignConfigSection.scopeRow('3')}}" stepKey="editStoreView"/> + <waitForPageLoad stepKey="waitForPageLoadToOpenStoreViewEditPage"/> + <!--Click Upload logo in Transactional Emails and upload the image and preview it--> + <click selector="{{AdminDesignConfigSection.logoWrapperOpen}}" stepKey="openTab" /> + <attachFile selector="{{AdminDesignConfigSection.logoUpload}}" userInput="{{MagentoLogo.file}}" stepKey="attachLogo"/> + <wait time="5" stepKey="waitingForLogoToUpload" /> + <seeElement selector="{{AdminDesignConfigSection.logoPreview}}" stepKey="LogoPreviewIsVisible"/> + </test> +</tests> diff --git a/app/code/Magento/Email/Test/Unit/Model/Plugin/WindowsSmtpConfigTest.php b/app/code/Magento/Email/Test/Unit/Model/Plugin/WindowsSmtpConfigTest.php new file mode 100644 index 000000000000..5f7c44b988c6 --- /dev/null +++ b/app/code/Magento/Email/Test/Unit/Model/Plugin/WindowsSmtpConfigTest.php @@ -0,0 +1,100 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Email\Test\Unit\Model\Plugin; + +use Magento\Email\Model\Plugin\WindowsSmtpConfig; +use Magento\Framework\App\Config\ReinitableConfigInterface; +use Magento\Framework\Mail\TransportInterface; +use Magento\Framework\OsInfo; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + +/** + * WindowsSmtpConfigTest + */ +class WindowsSmtpConfigTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var WindowsSmtpConfig + */ + private $windowsSmtpConfig; + + /** + * @var OsInfo|\PHPUnit_Framework_MockObject_MockObject + */ + private $osInfoMock; + + /** + * @var ReinitableConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $configMock; + + /** + * @var TransportInterface + */ + private $transportMock; + + /** + * setUp + * + * @return void + */ + public function setUp(): void + { + $objectManager = new ObjectManager($this); + + $this->osInfoMock = $this->createMock(OsInfo::class); + $this->configMock = $this->createMock(ReinitableConfigInterface::class); + $this->transportMock = $this->createMock(TransportInterface::class); + + $this->windowsSmtpConfig = $objectManager->getObject( + WindowsSmtpConfig::class, + [ + 'config' => $this->configMock, + 'osInfo' => $this->osInfoMock + ] + ); + } + + /** + * Test if SMTP settings if windows server + * + * @return void + */ + public function testBeforeSendMessageOsWindows(): void + { + $this->osInfoMock->expects($this->once()) + ->method('isWindows') + ->willReturn(true); + + $this->configMock->expects($this->exactly(2)) + ->method('getValue') + ->willReturnMap([ + [WindowsSmtpConfig::XML_SMTP_HOST, '127.0.0.1'], + [WindowsSmtpConfig::XML_SMTP_PORT, '80'] + ]); + + $this->windowsSmtpConfig->beforeSendMessage($this->transportMock); + } + + /** + * Test if SMTP settings if not windows server + * + * @return void + */ + public function testBeforeSendMessageOsIsWindows(): void + { + $this->osInfoMock->expects($this->once()) + ->method('isWindows') + ->willReturn(false); + + $this->configMock->expects($this->never()) + ->method('getValue'); + + $this->windowsSmtpConfig->beforeSendMessage($this->transportMock); + } +} diff --git a/app/code/Magento/Email/Test/Unit/Model/Template/SenderResolverTest.php b/app/code/Magento/Email/Test/Unit/Model/Template/SenderResolverTest.php new file mode 100644 index 000000000000..fb25290f9d27 --- /dev/null +++ b/app/code/Magento/Email/Test/Unit/Model/Template/SenderResolverTest.php @@ -0,0 +1,112 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Email\Test\Unit\Model\Template; + +use Magento\Email\Model\Template\SenderResolver; +use Magento\Framework\Exception\MailException; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\App\Config\ScopeConfigInterface; + +/** + * SenderResolverTest + */ +class SenderResolverTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var SenderResolver + */ + private $senderResolver; + + /** + * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $scopeConfig; + + /** + * @return void + */ + public function setUp(): void + { + $objectManager = new ObjectManager($this); + + $this->scopeConfig = $this->createMock(ScopeConfigInterface::class); + + $this->senderResolver = $objectManager->getObject( + SenderResolver::class, + [ + 'scopeConfig' => $this->scopeConfig + ] + ); + } + + /** + * Test returned information for given sender's name and email + * + * @return void + */ + public function testResolve(): void + { + $sender = 'general'; + $scopeId = null; + + $this->scopeConfig->expects($this->exactly(2)) + ->method('getValue') + ->willReturnMap([ + [ + 'trans_email/ident_' . $sender . '/name', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $scopeId, + 'Test Name' + ], + [ + 'trans_email/ident_' . $sender . '/email', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $scopeId, + 'test@email.com' + ] + ]); + + $result = $this->senderResolver->resolve($sender); + + $this->assertTrue(isset($result['name'])); + $this->assertEquals('Test Name', $result['name']); + + $this->assertTrue(isset($result['email'])); + $this->assertEquals('test@email.com', $result['email']); + } + + /** + * Test if exception is thrown in case there is no name or email in result + * + * @dataProvider dataProvidedSenderArray + * @param array $sender + * + * @return void + */ + public function testResolveThrowException(array $sender): void + { + $this->expectExceptionMessage('Invalid sender data'); + $this->expectException(MailException::class); + $this->senderResolver->resolve($sender); + } + + /** + * @return array + */ + public function dataProvidedSenderArray() + { + return [ + [ + ['name' => 'Name'] + ], + [ + ['email' => 'test@email.com'] + ] + ]; + } +} diff --git a/app/code/Magento/Email/Test/Unit/Model/TemplateTest.php b/app/code/Magento/Email/Test/Unit/Model/TemplateTest.php index 5464ca51cbe3..9599cd1fef44 100644 --- a/app/code/Magento/Email/Test/Unit/Model/TemplateTest.php +++ b/app/code/Magento/Email/Test/Unit/Model/TemplateTest.php @@ -5,21 +5,37 @@ */ namespace Magento\Email\Test\Unit\Model; +use Magento\Email\Model\Template; +use Magento\Email\Model\Template\Config; +use Magento\Email\Model\Template\FilterFactory; +use Magento\Email\Model\TemplateFactory; use Magento\Framework\App\Area; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\TemplateTypesInterface; +use Magento\Framework\Filesystem; +use Magento\Framework\Filter\FilterManager; +use Magento\Framework\Model\Context; +use Magento\Framework\Registry; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\Url; +use Magento\Framework\View\Asset\Repository; use Magento\Setup\Module\I18n\Locale; +use Magento\Store\Model\App\Emulation; use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManager; +use Magento\Theme\Model\View\Design; +use PHPUnit\Framework\TestCase; /** * Covers \Magento\Email\Model\Template * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class TemplateTest extends \PHPUnit\Framework\TestCase +class TemplateTest extends TestCase { /** - * @var \Magento\Framework\Model\Context|\PHPUnit_Framework_MockObject_MockObject + * @var Context|\PHPUnit_Framework_MockObject_MockObject */ private $context; @@ -29,13 +45,13 @@ class TemplateTest extends \PHPUnit\Framework\TestCase private $design; /** - * @var \Magento\Framework\Registry|\PHPUnit_Framework_MockObject_MockObject + * @var Registry|\PHPUnit_Framework_MockObject_MockObject * @deprecated since 2.3.0 in favor of stateful global objects elimination. */ private $registry; /** - * @var \Magento\Store\Model\App\Emulation|\PHPUnit_Framework_MockObject_MockObject + * @var Emulation|\PHPUnit_Framework_MockObject_MockObject */ private $appEmulation; @@ -45,53 +61,53 @@ class TemplateTest extends \PHPUnit\Framework\TestCase private $storeManager; /** - * @var \Magento\Framework\Filesystem|\PHPUnit_Framework_MockObject_MockObject + * @var Filesystem|\PHPUnit_Framework_MockObject_MockObject */ private $filesystem; /** - * @var \Magento\Framework\View\Asset\Repository|\PHPUnit_Framework_MockObject_MockObject + * @var Repository|\PHPUnit_Framework_MockObject_MockObject */ private $assetRepo; /** - * @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject */ private $scopeConfig; /** - * @var \Magento\Email\Model\Template\FilterFactory|\PHPUnit_Framework_MockObject_MockObject + * @var FilterFactory|\PHPUnit_Framework_MockObject_MockObject */ private $filterFactory; /** - * @var \Magento\Framework\Filter\FilterManager|\PHPUnit_Framework_MockObject_MockObject + * @var FilterManager|\PHPUnit_Framework_MockObject_MockObject */ private $filterManager; /** - * @var \Magento\Framework\Url|\PHPUnit_Framework_MockObject_MockObject + * @var Url|\PHPUnit_Framework_MockObject_MockObject */ private $urlModel; /** - * @var \Magento\Email\Model\Template\Config|\PHPUnit_Framework_MockObject_MockObject + * @var Config|\PHPUnit_Framework_MockObject_MockObject */ private $emailConfig; /** - * @var \Magento\Email\Model\TemplateFactory|\PHPUnit_Framework_MockObject_MockObject + * @var TemplateFactory|\PHPUnit_Framework_MockObject_MockObject */ private $templateFactory; /** - * @var \Magento\Framework\Serialize\Serializer\Json|\PHPUnit_Framework_MockObject_MockObject + * @var Json|\PHPUnit_Framework_MockObject_MockObject */ private $serializerMock; protected function setUp() { - $this->context = $this->getMockBuilder(\Magento\Framework\Model\Context::class) + $this->context = $this->getMockBuilder(Context::class) ->disableOriginalConstructor() ->getMock(); @@ -99,11 +115,11 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $this->registry = $this->getMockBuilder(\Magento\Framework\Registry::class) + $this->registry = $this->getMockBuilder(Registry::class) ->disableOriginalConstructor() ->getMock(); - $this->appEmulation = $this->getMockBuilder(\Magento\Store\Model\App\Emulation::class) + $this->appEmulation = $this->getMockBuilder(Emulation::class) ->disableOriginalConstructor() ->getMock(); @@ -111,51 +127,51 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $this->assetRepo = $this->getMockBuilder(\Magento\Framework\View\Asset\Repository::class) + $this->assetRepo = $this->getMockBuilder(Repository::class) ->disableOriginalConstructor() ->getMock(); - $this->filesystem = $this->getMockBuilder(\Magento\Framework\Filesystem::class) + $this->filesystem = $this->getMockBuilder(Filesystem::class) ->disableOriginalConstructor() ->getMock(); - $this->scopeConfig = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class) + $this->scopeConfig = $this->getMockBuilder(ScopeConfigInterface::class) ->disableOriginalConstructor() ->getMock(); - $this->emailConfig = $this->getMockBuilder(\Magento\Email\Model\Template\Config::class) + $this->emailConfig = $this->getMockBuilder(Config::class) ->disableOriginalConstructor() ->getMock(); - $this->templateFactory = $this->getMockBuilder(\Magento\Email\Model\TemplateFactory::class) + $this->templateFactory = $this->getMockBuilder(TemplateFactory::class) ->disableOriginalConstructor() ->getMock(); - $this->filterManager = $this->getMockBuilder(\Magento\Framework\Filter\FilterManager::class) + $this->filterManager = $this->getMockBuilder(FilterManager::class) ->disableOriginalConstructor() ->getMock(); - $this->urlModel = $this->getMockBuilder(\Magento\Framework\Url::class) + $this->urlModel = $this->getMockBuilder(Url::class) ->disableOriginalConstructor() ->getMock(); - $this->filterFactory = $this->getMockBuilder(\Magento\Email\Model\Template\FilterFactory::class) + $this->filterFactory = $this->getMockBuilder(FilterFactory::class) ->setMethods(['create']) ->disableOriginalConstructor() ->getMock(); - $this->serializerMock = $this->getMockBuilder(\Magento\Framework\Serialize\Serializer\Json::class)->getMock(); + $this->serializerMock = $this->getMockBuilder(Json::class)->getMock(); } /** * Return the model under test with additional methods mocked. * * @param array $mockedMethods - * @return \Magento\Email\Model\Template|\PHPUnit_Framework_MockObject_MockObject + * @return Template|\PHPUnit_Framework_MockObject_MockObject */ protected function getModelMock(array $mockedMethods = []) { - return $this->getMockBuilder(\Magento\Email\Model\Template::class) + return $this->getMockBuilder(Template::class) ->setMethods(array_merge($mockedMethods, ['__wakeup', '__sleep', '_init'])) ->setConstructorArgs([ $this->context, @@ -210,7 +226,7 @@ public function testGetTemplateFilterWithEmptyValue() ->method('setStoreId') ->will($this->returnSelf()); $this->filterFactory->method('create') - ->will($this->returnValue($filterTemplate)); + ->willReturn($filterTemplate); $designConfig = $this->getMockBuilder(\Magento\Framework\DataObject::class) ->setMethods(['getStore']) ->disableOriginalConstructor() @@ -219,7 +235,7 @@ public function testGetTemplateFilterWithEmptyValue() $model = $this->getModelMock(['getUseAbsoluteLinks', 'getDesignConfig']); $model->expects($this->once()) ->method('getDesignConfig') - ->will($this->returnValue($designConfig)); + ->willReturn($designConfig); $this->assertSame($filterTemplate, $model->getTemplateFilter()); } @@ -253,7 +269,7 @@ public function testLoadDefault( $model->expects($this->once()) ->method('getDesignParams') - ->will($this->returnValue($designParams)); + ->willReturn($designParams); $templateId = 'templateId'; @@ -261,11 +277,11 @@ public function testLoadDefault( $this->emailConfig->expects($this->once()) ->method('getTemplateFilename') ->with($templateId) - ->will($this->returnValue($templateFile)); + ->willReturn($templateFile); $this->emailConfig->expects($this->once()) ->method('getTemplateType') ->with($templateId) - ->will($this->returnValue($templateType)); + ->willReturn($templateType); $modulesDir = $this->getMockBuilder(\Magento\Framework\Filesystem\Directory\ReadInterface::class) ->setMethods(['readFile', 'getRelativePath']) @@ -275,15 +291,15 @@ public function testLoadDefault( $modulesDir->expects($this->once()) ->method('getRelativePath') ->with($templateFile) - ->will($this->returnValue($relativePath)); + ->willReturn($relativePath); $modulesDir->expects($this->once()) ->method('readFile') - ->will($this->returnValue($templateText)); + ->willReturn($templateText); $this->filesystem->expects($this->once()) ->method('getDirectoryRead') ->with(DirectoryList::ROOT) - ->will($this->returnValue($modulesDir)); + ->willReturn($modulesDir); $model->loadDefault($templateId); @@ -382,10 +398,10 @@ public function testLoadByConfigPath($loadFromDatabase) $storeId = 'storeId'; $designConfig->expects($this->once()) ->method('getStore') - ->will($this->returnValue($storeId)); + ->willReturn($storeId); $model->expects($this->once()) ->method('getDesignConfig') - ->will($this->returnValue($designConfig)); + ->willReturn($designConfig); if ($loadFromDatabase) { $templateId = '1'; @@ -404,7 +420,7 @@ public function testLoadByConfigPath($loadFromDatabase) $this->scopeConfig->expects($this->once()) ->method('getValue') ->with($configPath, ScopeInterface::SCOPE_STORE, $storeId) - ->will($this->returnValue($templateId)); + ->willReturn($templateId); $model->loadByConfigPath($configPath); } @@ -447,13 +463,13 @@ public function testIsValidForSend($senderName, $senderEmail, $templateSubject, $model = $this->getModelMock(['getSenderName', 'getSenderEmail', 'getTemplateSubject']); $model->expects($this->any()) ->method('getSenderName') - ->will($this->returnValue($senderName)); + ->willReturn($senderName); $model->expects($this->any()) ->method('getSenderEmail') - ->will($this->returnValue($senderEmail)); + ->willReturn($senderEmail); $model->expects($this->any()) ->method('getTemplateSubject') - ->will($this->returnValue($templateSubject)); + ->willReturn($templateSubject); $this->assertEquals($expectedValue, $model->isValidForSend()); } @@ -503,7 +519,7 @@ public function testGetProcessedTemplateSubject() ->getMock(); $model->expects($this->once()) ->method('getTemplateFilter') - ->will($this->returnValue($filterTemplate)); + ->willReturn($filterTemplate); $model->expects($this->once()) ->method('applyDesignConfig'); @@ -515,10 +531,10 @@ public function testGetProcessedTemplateSubject() $storeId = 'storeId'; $designConfig->expects($this->once()) ->method('getStore') - ->will($this->returnValue($storeId)); + ->willReturn($storeId); $model->expects($this->once()) ->method('getDesignConfig') - ->will($this->returnValue($designConfig)); + ->willReturn($designConfig); $filterTemplate->expects($this->once()) ->method('setStoreId') @@ -528,7 +544,7 @@ public function testGetProcessedTemplateSubject() $filterTemplate->expects($this->once()) ->method('filter') ->with($templateSubject) - ->will($this->returnValue($expectedResult)); + ->willReturn($expectedResult); $variables = [ 'key' => 'value' ]; $filterTemplate->expects($this->once()) @@ -595,19 +611,21 @@ public function getVariablesOptionArrayDataProvider() 'templateVariables' => '{"store url=\"\"":"Store Url","var logo_url":"Email Logo Image Url",' . '"var customer.name":"Customer Name"}', 'expectedResult' => [ - 'label' => __('Template Variables'), - 'value' => [ - [ - 'value' => '{{store url=""}}', - 'label' => __('%1', 'Store Url'), - ], - [ - 'value' => '{{var logo_url}}', - 'label' => __('%1', 'Email Logo Image Url'), - ], - [ - 'value' => '{{var customer.name}}', - 'label' => __('%1', 'Customer Name'), + [ + 'label' => __('Template Variables'), + 'value' => [ + [ + 'value' => '{{store url=""}}', + 'label' => __('%1', 'Store Url'), + ], + [ + 'value' => '{{var logo_url}}', + 'label' => __('%1', 'Email Logo Image Url'), + ], + [ + 'value' => '{{var customer.name}}', + 'label' => __('%1', 'Customer Name'), + ], ], ], ], @@ -642,17 +660,17 @@ public function testProcessTemplate($templateId, $expectedResult) $model->expects($this->once()) ->method('applyDesignConfig') - ->will($this->returnValue(true)); + ->willReturn(true); $model->expects($this->once()) ->method('cancelDesignConfig') - ->will($this->returnValue(true)); + ->willReturn(true); $vars = [ 'key' => 'value' ]; $model->setVars($vars); $model->expects($this->once()) ->method('getProcessedTemplate') ->with($vars) - ->will($this->returnValue($expectedResult)); + ->willReturn($expectedResult); $this->assertEquals($expectedResult, $model->processTemplate()); $this->assertTrue($model->getUseAbsoluteLinks()); @@ -686,11 +704,11 @@ public function testProcessTemplateThrowsExceptionNonExistentTemplate() ]); $model->expects($this->once()) ->method('loadDefault') - ->will($this->returnValue(true)); + ->willReturn(true); $model->expects($this->once()) ->method('applyDesignConfig') - ->will($this->returnValue(true)); + ->willReturn(true); $model->processTemplate(); } @@ -704,7 +722,7 @@ public function testGetSubject() $model->expects($this->once()) ->method('getProcessedTemplateSubject') ->with($variables) - ->will($this->returnValue($expectedResult)); + ->willReturn($expectedResult); $this->assertEquals($expectedResult, $model->getSubject()); } @@ -725,30 +743,32 @@ public function testSetOptions() */ public function testGetType($templateType, $expectedResult) { - $emailConfig = $this->getMockBuilder(\Magento\Email\Model\Template\Config::class) + $emailConfig = $this->getMockBuilder(Config::class) ->setMethods(['getTemplateType']) ->disableOriginalConstructor() ->getMock(); $emailConfig->expects($this->once())->method('getTemplateType')->will($this->returnValue($templateType)); - /** @var \Magento\Email\Model\Template $model */ - $model = $this->getMockBuilder(\Magento\Email\Model\Template::class) + /** @var Template $model */ + $model = $this->getMockBuilder(Template::class) ->setMethods(['_init']) ->setConstructorArgs([ - $this->createMock(\Magento\Framework\Model\Context::class), - $this->createMock(\Magento\Theme\Model\View\Design::class), - $this->createMock(\Magento\Framework\Registry::class), - $this->createMock(\Magento\Store\Model\App\Emulation::class), - $this->createMock(\Magento\Store\Model\StoreManager::class), - $this->createMock(\Magento\Framework\View\Asset\Repository::class), - $this->createMock(\Magento\Framework\Filesystem::class), - $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class), + $this->createMock(Context::class), + $this->createMock(Design::class), + $this->createMock(Registry::class), + $this->createMock(Emulation::class), + $this->createMock(StoreManager::class), + $this->createMock(Repository::class), + $this->createMock(Filesystem::class), + $this->createMock(ScopeConfigInterface::class), $emailConfig, - $this->createMock(\Magento\Email\Model\TemplateFactory::class), - $this->createMock(\Magento\Framework\Filter\FilterManager::class), - $this->createMock(\Magento\Framework\Url::class), - $this->createMock(\Magento\Email\Model\Template\FilterFactory::class), + $this->createMock(TemplateFactory::class), + $this->createMock(FilterManager::class), + $this->createMock(Url::class), + $this->createMock(FilterFactory::class), + [], + $this->createMock(Json::class) ]) ->getMock(); diff --git a/app/code/Magento/Email/etc/db_schema.xml b/app/code/Magento/Email/etc/db_schema.xml index 53ef349a383a..dbb8855e9e57 100644 --- a/app/code/Magento/Email/etc/db_schema.xml +++ b/app/code/Magento/Email/etc/db_schema.xml @@ -27,16 +27,16 @@ <column xsi:type="varchar" name="orig_template_code" nullable="true" length="200" comment="Original Template Code"/> <column xsi:type="text" name="orig_template_variables" nullable="true" comment="Original Template Variables"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="template_id"/> </constraint> - <constraint xsi:type="unique" name="EMAIL_TEMPLATE_TEMPLATE_CODE"> + <constraint xsi:type="unique" referenceId="EMAIL_TEMPLATE_TEMPLATE_CODE"> <column name="template_code"/> </constraint> - <index name="EMAIL_TEMPLATE_ADDED_AT" indexType="btree"> + <index referenceId="EMAIL_TEMPLATE_ADDED_AT" indexType="btree"> <column name="added_at"/> </index> - <index name="EMAIL_TEMPLATE_MODIFIED_AT" indexType="btree"> + <index referenceId="EMAIL_TEMPLATE_MODIFIED_AT" indexType="btree"> <column name="modified_at"/> </index> </table> diff --git a/app/code/Magento/Email/view/adminhtml/layout/adminhtml_email_template_grid_block.xml b/app/code/Magento/Email/view/adminhtml/layout/adminhtml_email_template_grid_block.xml index fa15560817dd..87da146d8ce5 100644 --- a/app/code/Magento/Email/view/adminhtml/layout/adminhtml_email_template_grid_block.xml +++ b/app/code/Magento/Email/view/adminhtml/layout/adminhtml_email_template_grid_block.xml @@ -11,7 +11,7 @@ <block class="Magento\Backend\Block\Widget\Grid" name="adminhtml.system.email.template.grid" as="grid"> <arguments> <argument name="id" xsi:type="string">systemEmailTemplateGrid</argument> - <argument name="dataSource" xsi:type="object">Magento\Email\Model\ResourceModel\Template\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Email\Model\ResourceModel\Template\Collection</argument> <argument name="use_ajax" xsi:type="string">1</argument> <argument name="save_parameters_in_session" xsi:type="string">1</argument> <argument name="grid_url" xsi:type="url" path="*/*/grid"> diff --git a/app/code/Magento/Email/view/adminhtml/ui_component/design_config_form.xml b/app/code/Magento/Email/view/adminhtml/ui_component/design_config_form.xml index 91c38c92dc75..76a914d10b27 100644 --- a/app/code/Magento/Email/view/adminhtml/ui_component/design_config_form.xml +++ b/app/code/Magento/Email/view/adminhtml/ui_component/design_config_form.xml @@ -13,7 +13,7 @@ <collapsible>true</collapsible> <label translate="true">Transactional Emails</label> </settings> - <field name="email_logo" formElement="fileUploader"> + <field name="email_logo" formElement="imageUploader"> <settings> <notice translate="true">To optimize logo for high-resolution displays, upload an image that is 3x normal size and then specify 1x dimensions in the width/height fields below.</notice> <label translate="true">Logo Image</label> diff --git a/app/code/Magento/Email/view/frontend/email/header.html b/app/code/Magento/Email/view/frontend/email/header.html index a6895f629b64..c4f49698dc69 100644 --- a/app/code/Magento/Email/view/frontend/email/header.html +++ b/app/code/Magento/Email/view/frontend/email/header.html @@ -43,8 +43,6 @@ {{if logo_height}} height="{{var logo_height}}" - {{else}} - height="52" {{/if}} src="{{var logo_url}}" diff --git a/app/code/Magento/Email/view/frontend/web/logo_email.png b/app/code/Magento/Email/view/frontend/web/logo_email.png index d01b530456e8..215e9d06edcd 100644 Binary files a/app/code/Magento/Email/view/frontend/web/logo_email.png and b/app/code/Magento/Email/view/frontend/web/logo_email.png differ diff --git a/app/code/Magento/EncryptionKey/Controller/Adminhtml/Crypt/Key/Index.php b/app/code/Magento/EncryptionKey/Controller/Adminhtml/Crypt/Key/Index.php index 8e42e0c1313c..86fc0082f7a5 100644 --- a/app/code/Magento/EncryptionKey/Controller/Adminhtml/Crypt/Key/Index.php +++ b/app/code/Magento/EncryptionKey/Controller/Adminhtml/Crypt/Key/Index.php @@ -6,10 +6,12 @@ */ namespace Magento\EncryptionKey\Controller\Adminhtml\Crypt\Key; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + /** * Key Index action */ -class Index extends \Magento\EncryptionKey\Controller\Adminhtml\Crypt\Key +class Index extends \Magento\EncryptionKey\Controller\Adminhtml\Crypt\Key implements HttpGetActionInterface { /** * Render main page with form diff --git a/app/code/Magento/EncryptionKey/Setup/Patch/Data/SodiumChachaPatch.php b/app/code/Magento/EncryptionKey/Setup/Patch/Data/SodiumChachaPatch.php index aae30026b2b7..9331b68675b6 100644 --- a/app/code/Magento/EncryptionKey/Setup/Patch/Data/SodiumChachaPatch.php +++ b/app/code/Magento/EncryptionKey/Setup/Patch/Data/SodiumChachaPatch.php @@ -8,12 +8,18 @@ namespace Magento\EncryptionKey\Setup\Patch\Data; use Magento\Framework\Setup\Patch\DataPatchInterface; +use Magento\Framework\App\ObjectManager; /** * Migrate encrypted configuration values to the latest cipher */ class SodiumChachaPatch implements DataPatchInterface { + /** + * @var \Magento\Framework\Config\ScopeInterface + */ + private $scope; + /** * @var \Magento\Framework\Setup\ModuleDataSetupInterface */ @@ -35,25 +41,29 @@ class SodiumChachaPatch implements DataPatchInterface private $state; /** + * SodiumChachaPatch constructor. * @param \Magento\Framework\Setup\ModuleDataSetupInterface $moduleDataSetup * @param \Magento\Config\Model\Config\Structure\Proxy $structure * @param \Magento\Framework\Encryption\EncryptorInterface $encryptor * @param \Magento\Framework\App\State $state + * @param \Magento\Framework\Config\ScopeInterface|null $scope */ public function __construct( \Magento\Framework\Setup\ModuleDataSetupInterface $moduleDataSetup, \Magento\Config\Model\Config\Structure\Proxy $structure, \Magento\Framework\Encryption\EncryptorInterface $encryptor, - \Magento\Framework\App\State $state + \Magento\Framework\App\State $state, + \Magento\Framework\Config\ScopeInterface $scope = null ) { $this->moduleDataSetup = $moduleDataSetup; $this->structure = $structure; $this->encryptor = $encryptor; $this->state = $state; + $this->scope = $scope ?? ObjectManager::getInstance()->get(\Magento\Framework\Config\ScopeInterface::class); } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { @@ -65,7 +75,7 @@ public function apply() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -73,41 +83,67 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { return []; } + /** + * Re encrypt sensitive data in the system configuration + */ private function reEncryptSystemConfigurationValues() { - $structure = $this->structure; - $paths = $this->state->emulateAreaCode( - \Magento\Framework\App\Area::AREA_ADMINHTML, - function () use ($structure) { - return $structure->getFieldPathsByAttribute( - 'backend_model', - \Magento\Config\Model\Config\Backend\Encrypted::class - ); - } + $table = $this->moduleDataSetup->getTable('core_config_data'); + $hasEncryptedData = $this->moduleDataSetup->getConnection()->fetchOne( + $this->moduleDataSetup->getConnection() + ->select() + ->from($table, [new \Zend_Db_Expr('count(value)')]) + ->where('value LIKE ?', '0:2%') ); - // walk through found data and re-encrypt it - if ($paths) { - $table = $this->moduleDataSetup->getTable('core_config_data'); - $values = $this->moduleDataSetup->getConnection()->fetchPairs( - $this->moduleDataSetup->getConnection() - ->select() - ->from($table, ['config_id', 'value']) - ->where('path IN (?)', $paths) - ->where('value NOT LIKE ?', '') + if ($hasEncryptedData !== '0') { + $currentScope = $this->scope->getCurrentScope(); + $structure = $this->structure; + $paths = $this->state->emulateAreaCode( + \Magento\Framework\App\Area::AREA_ADMINHTML, + function () use ($structure) { + $this->scope->setCurrentScope(\Magento\Framework\App\Area::AREA_ADMINHTML); + /** Returns list of structure paths to be re encrypted */ + $paths = $structure->getFieldPathsByAttribute( + 'backend_model', + \Magento\Config\Model\Config\Backend\Encrypted::class + ); + /** Returns list of mapping between configPath => [structurePaths] */ + $mappedPaths = $structure->getFieldPaths(); + foreach ($mappedPaths as $mappedPath => $data) { + foreach ($data as $structurePath) { + if ($structurePath !== $mappedPath && $key = array_search($structurePath, $paths)) { + $paths[$key] = $mappedPath; + } + } + } + + return array_unique($paths); + } ); - foreach ($values as $configId => $value) { - $this->moduleDataSetup->getConnection()->update( - $table, - ['value' => $this->encryptor->encrypt($this->encryptor->decrypt($value))], - ['config_id = ?' => (int)$configId] + $this->scope->setCurrentScope($currentScope); + // walk through found data and re-encrypt it + if ($paths) { + $values = $this->moduleDataSetup->getConnection()->fetchPairs( + $this->moduleDataSetup->getConnection() + ->select() + ->from($table, ['config_id', 'value']) + ->where('path IN (?)', $paths) + ->where('value NOT LIKE ?', '') ); + foreach ($values as $configId => $value) { + $this->moduleDataSetup->getConnection()->update( + $table, + ['value' => $this->encryptor->encrypt($this->encryptor->decrypt($value))], + ['config_id = ?' => (int)$configId] + ); + } } } } diff --git a/app/code/Magento/Fedex/Model/Carrier.php b/app/code/Magento/Fedex/Model/Carrier.php index f6e63e04ac55..0cbd90150734 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()) { @@ -986,6 +985,7 @@ public function getCode($type, $code = '') * Return FeDex currency ISO code by Magento Base Currency Code * * @return string 3-digit currency code + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function getCurrencyCode() { @@ -1008,7 +1008,7 @@ public function getCurrencyCode() ]; $currencyCode = $this->_storeManager->getStore()->getBaseCurrencyCode(); - return isset($codes[$currencyCode]) ? $codes[$currencyCode] : $currencyCode; + return $codes[$currencyCode] ?? $currencyCode; } /** @@ -1438,6 +1438,8 @@ protected function _doShipmentRequest(\Magento\Framework\DataObject $request) } /** + * Return Tracking Number + * * @param array|object $trackingIds * @return string */ @@ -1452,10 +1454,10 @@ function ($val) { } /** - * For multi package shipments. Delete requested shipments if the current shipment - * request is failed + * For multi package shipments. Delete requested shipments if the current shipment request is failed * * @param array $data + * * @return bool */ public function rollBack($data) @@ -1475,6 +1477,7 @@ public function rollBack($data) * Return container types of carrier * * @param \Magento\Framework\DataObject|null $params + * * @return array|bool * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ @@ -1542,6 +1545,7 @@ public function getContainerTypesFilter() * Return delivery confirmation types of carrier * * @param \Magento\Framework\DataObject|null $params + * * @return array * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -1552,6 +1556,7 @@ public function getDeliveryConfirmationTypes(\Magento\Framework\DataObject $para /** * Recursive replace sensitive fields in debug data by the mask + * * @param string $data * @return string */ @@ -1569,6 +1574,7 @@ protected function filterDebugData($data) /** * Parse track details response from Fedex + * * @param \stdClass $trackInfo * @return array * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -1639,6 +1645,7 @@ private function processTrackingDetails(\stdClass $trackInfo) /** * Parse delivery datetime from tracking details + * * @param \stdClass $trackInfo * @return \Datetime|null */ @@ -1655,8 +1662,7 @@ private function getDeliveryDateTime(\stdClass $trackInfo) } /** - * Get delivery address details in string representation - * Return City, State, Country Code + * Get delivery address details in string representation Return City, State, Country Code * * @param \stdClass $address * @return \Magento\Framework\Phrase|string @@ -1718,6 +1724,7 @@ private function processTrackDetailsEvents(array $events) /** * Append error message to rate result instance + * * @param string $trackingValue * @param string $errorMessage */ @@ -1759,8 +1766,7 @@ private function parseDate($timestamp) } /** - * Defines payment type by request. - * Two values are available: RECIPIENT or SENDER. + * Defines payment type by request. Two values are available: RECIPIENT or SENDER. * * @param DataObject $request * @return string 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 000000000000..86a576f2db65 --- /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 000000000000..e1597707f9d0 --- /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 f17f8f2afe66..c542b1f04d1e 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/Fedex/etc/wsdl/RateService_v10.wsdl b/app/code/Magento/Fedex/etc/wsdl/RateService_v10.wsdl index 6ae04ccee077..62795f07239a 100644 --- a/app/code/Magento/Fedex/etc/wsdl/RateService_v10.wsdl +++ b/app/code/Magento/Fedex/etc/wsdl/RateService_v10.wsdl @@ -355,7 +355,7 @@ </xs:element> <xs:element name="AppliedOptions" type="ns:ServiceOptionType" minOccurs="0" maxOccurs="unbounded"> <xs:annotation> - <xs:documentation>Shows the specific combination of service options combined with the service type that produced this committment in the set returned to the caller.</xs:documentation> + <xs:documentation>Shows the specific combination of service options combined with the service type that produced this commitment in the set returned to the caller.</xs:documentation> </xs:annotation> </xs:element> <xs:element name="AppliedSubOptions" type="ns:ServiceSubOptionDetail" minOccurs="0"> @@ -473,7 +473,7 @@ <xs:annotation> <xs:documentation> For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction. - If this shipment cotains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. + If this shipment contains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. </xs:documentation> </xs:annotation> <xs:sequence> @@ -916,7 +916,7 @@ </xs:element> <xs:element name="SecondaryBarcode" type="ns:SecondaryBarcodeType" minOccurs="0"> <xs:annotation> - <xs:documentation>For customers producing their own Ground labels, this field specifies which secondary barcode will be printed on the label; so that the primary barcode produced by FedEx has the corect SCNC.</xs:documentation> + <xs:documentation>For customers producing their own Ground labels, this field specifies which secondary barcode will be printed on the label; so that the primary barcode produced by FedEx has the correct SCNC.</xs:documentation> </xs:annotation> </xs:element> <xs:element name="TermsAndConditionsLocalization" type="ns:Localization" minOccurs="0"> @@ -1006,7 +1006,7 @@ <xs:annotation> <xs:documentation> For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction. - If this shipment cotains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. + If this shipment contains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. </xs:documentation> </xs:annotation> </xs:element> @@ -1151,7 +1151,7 @@ </xs:simpleType> <xs:complexType name="DestinationControlDetail"> <xs:annotation> - <xs:documentation>Data required to complete the Destionation Control Statement for US exports.</xs:documentation> + <xs:documentation>Data required to complete the Destination Control Statement for US exports.</xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="StatementTypes" type="ns:DestinationControlStatementType" minOccurs="0" maxOccurs="unbounded"/> @@ -2278,7 +2278,7 @@ </xs:element> <xs:element name="LabelStockType" type="ns:LabelStockType" minOccurs="0"> <xs:annotation> - <xs:documentation>For thermal printer lables this indicates the size of the label and the location of the doc tab if present.</xs:documentation> + <xs:documentation>For thermal printer labels this indicates the size of the label and the location of the doc tab if present.</xs:documentation> </xs:annotation> </xs:element> <xs:element name="LabelPrintingOrientation" type="ns:LabelPrintingOrientationType" minOccurs="0"> @@ -2780,7 +2780,7 @@ </xs:simpleType> <xs:complexType name="Party"> <xs:annotation> - <xs:documentation>The descriptive data for a person or company entitiy doing business with FedEx.</xs:documentation> + <xs:documentation>The descriptive data for a person or company entity doing business with FedEx.</xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="AccountNumber" type="xs:string" minOccurs="0"> @@ -3069,7 +3069,7 @@ </xs:element> <xs:element name="AppliedOptions" type="ns:ServiceOptionType" minOccurs="0" maxOccurs="unbounded"> <xs:annotation> - <xs:documentation>Shows the specific combination of service options combined with the service type that produced this committment in the set returned to the caller.</xs:documentation> + <xs:documentation>Shows the specific combination of service options combined with the service type that produced this commitment in the set returned to the caller.</xs:documentation> </xs:annotation> </xs:element> <xs:element name="AppliedSubOptions" type="ns:ServiceSubOptionDetail" minOccurs="0"> @@ -3195,7 +3195,7 @@ </xs:simpleType> <xs:complexType name="RatedPackageDetail"> <xs:annotation> - <xs:documentation>If requesting rates using the PackageDetails element (one package at a time) in the request, the rates for each package will be returned in this element. Currently total piece total weight rates are also retuned in this element.</xs:documentation> + <xs:documentation>If requesting rates using the PackageDetails element (one package at a time) in the request, the rates for each package will be returned in this element. Currently total piece total weight rates are also returned in this element.</xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="TrackingIds" type="ns:TrackingId" minOccurs="0" maxOccurs="unbounded"> @@ -3416,7 +3416,7 @@ </xs:element> <xs:element name="TotalWeight" type="ns:Weight" minOccurs="0"> <xs:annotation> - <xs:documentation>Identifies the total weight of the shipment being conveyed to FedEx.This is only applicable to International shipments and should only be used on the first package of a mutiple piece shipment.This value contains 1 explicit decimal position</xs:documentation> + <xs:documentation>Identifies the total weight of the shipment being conveyed to FedEx.This is only applicable to International shipments and should only be used on the first package of a multiple piece shipment.This value contains 1 explicit decimal position</xs:documentation> </xs:annotation> </xs:element> <xs:element name="TotalInsuredValue" type="ns:Money" minOccurs="0"> @@ -3582,7 +3582,7 @@ <xs:sequence> <xs:element name="ReturnType" type="ns:ReturnType" minOccurs="1"> <xs:annotation> - <xs:documentation>The type of return shipment that is being requested. At present the only type of retrun shipment that is supported is PRINT_RETURN_LABEL. With this option you can print a return label to insert into the box of an outbound shipment. This option can not be used to print an outbound label.</xs:documentation> + <xs:documentation>The type of return shipment that is being requested. At present the only type of return shipment that is supported is PRINT_RETURN_LABEL. With this option you can print a return label to insert into the box of an outbound shipment. This option can not be used to print an outbound label.</xs:documentation> </xs:annotation> </xs:element> <xs:element name="Rma" type="ns:Rma" minOccurs="0"> @@ -3655,7 +3655,7 @@ </xs:simpleType> <xs:simpleType name="ServiceOptionType"> <xs:annotation> - <xs:documentation>These values control the optional features of service that may be combined in a commitment/rate comparision transaction.</xs:documentation> + <xs:documentation>These values control the optional features of service that may be combined in a commitment/rate comparison transaction.</xs:documentation> </xs:annotation> <xs:restriction base="xs:string"> <xs:enumeration value="FREIGHT_GUARANTEE"/> @@ -4731,7 +4731,7 @@ </xs:element> <xs:element name="TotalCustomerCharge" type="ns:Money" minOccurs="0"> <xs:annotation> - <xs:documentation>The calculated varibale handling charge plus the net charge.</xs:documentation> + <xs:documentation>The calculated variable handling charge plus the net charge.</xs:documentation> </xs:annotation> </xs:element> </xs:sequence> diff --git a/app/code/Magento/Fedex/etc/wsdl/RateService_v9.wsdl b/app/code/Magento/Fedex/etc/wsdl/RateService_v9.wsdl index efbf75076c2c..17a6f74cc09b 100644 --- a/app/code/Magento/Fedex/etc/wsdl/RateService_v9.wsdl +++ b/app/code/Magento/Fedex/etc/wsdl/RateService_v9.wsdl @@ -354,7 +354,7 @@ </xs:element> <xs:element name="AppliedOptions" type="ns:ServiceOptionType" minOccurs="0" maxOccurs="unbounded"> <xs:annotation> - <xs:documentation>Shows the specific combination of service options combined with the service type that produced this committment in the set returned to the caller.</xs:documentation> + <xs:documentation>Shows the specific combination of service options combined with the service type that produced this commitment in the set returned to the caller.</xs:documentation> </xs:annotation> </xs:element> <xs:element name="AppliedSubOptions" type="ns:ServiceSubOptionDetail" minOccurs="0"> @@ -472,7 +472,7 @@ <xs:annotation> <xs:documentation> For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction. - If this shipment cotains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. + If this shipment commitment more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. </xs:documentation> </xs:annotation> <xs:sequence> @@ -1006,7 +1006,7 @@ <xs:annotation> <xs:documentation> For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction. - If this shipment cotains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. + If this shipment contains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. </xs:documentation> </xs:annotation> </xs:element> @@ -1148,7 +1148,7 @@ </xs:simpleType> <xs:complexType name="DestinationControlDetail"> <xs:annotation> - <xs:documentation>Data required to complete the Destionation Control Statement for US exports.</xs:documentation> + <xs:documentation>Data required to complete the Destination Control Statement for US exports.</xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="StatementTypes" type="ns:DestinationControlStatementType" minOccurs="0" maxOccurs="unbounded"/> @@ -2313,7 +2313,7 @@ </xs:element> <xs:element name="LabelStockType" type="ns:LabelStockType" minOccurs="0"> <xs:annotation> - <xs:documentation>For thermal printer lables this indicates the size of the label and the location of the doc tab if present.</xs:documentation> + <xs:documentation>For thermal printer labels this indicates the size of the label and the location of the doc tab if present.</xs:documentation> </xs:annotation> </xs:element> <xs:element name="LabelPrintingOrientation" type="ns:LabelPrintingOrientationType" minOccurs="0"> @@ -3099,7 +3099,7 @@ </xs:element> <xs:element name="AppliedOptions" type="ns:ServiceOptionType" minOccurs="0" maxOccurs="unbounded"> <xs:annotation> - <xs:documentation>Shows the specific combination of service options combined with the service type that produced this committment in the set returned to the caller.</xs:documentation> + <xs:documentation>Shows the specific combination of service options combined with the service type that produced this commitment in the set returned to the caller.</xs:documentation> </xs:annotation> </xs:element> <xs:element name="AppliedSubOptions" type="ns:ServiceSubOptionDetail" minOccurs="0"> @@ -3216,7 +3216,7 @@ </xs:simpleType> <xs:complexType name="RatedPackageDetail"> <xs:annotation> - <xs:documentation>If requesting rates using the PackageDetails element (one package at a time) in the request, the rates for each package will be returned in this element. Currently total piece total weight rates are also retuned in this element.</xs:documentation> + <xs:documentation>If requesting rates using the PackageDetails element (one package at a time) in the request, the rates for each package will be returned in this element. Currently total piece total weight rates are also returned in this element.</xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="TrackingIds" type="ns:TrackingId" minOccurs="0" maxOccurs="unbounded"> @@ -3622,7 +3622,7 @@ <xs:sequence> <xs:element name="ReturnType" type="ns:ReturnType"> <xs:annotation> - <xs:documentation>The type of return shipment that is being requested. At present the only type of retrun shipment that is supported is PRINT_RETURN_LABEL. With this option you can print a return label to insert into the box of an outbound shipment. This option can not be used to print an outbound label.</xs:documentation> + <xs:documentation>The type of return shipment that is being requested. At present the only type of return shipment that is supported is PRINT_RETURN_LABEL. With this option you can print a return label to insert into the box of an outbound shipment. This option can not be used to print an outbound label.</xs:documentation> </xs:annotation> </xs:element> <xs:element name="Rma" type="ns:Rma" minOccurs="0"> @@ -3687,7 +3687,7 @@ </xs:complexType> <xs:simpleType name="ServiceOptionType"> <xs:annotation> - <xs:documentation>These values control the optional features of service that may be combined in a commitment/rate comparision transaction.</xs:documentation> + <xs:documentation>These values control the optional features of service that may be combined in a commitment/rate comparison transaction.</xs:documentation> </xs:annotation> <xs:restriction base="xs:string"> <xs:enumeration value="FREIGHT_GUARANTEE"/> @@ -4611,7 +4611,7 @@ </xs:element> <xs:element name="TotalCustomerCharge" type="ns:Money" minOccurs="0"> <xs:annotation> - <xs:documentation>The calculated varibale handling charge plus the net charge.</xs:documentation> + <xs:documentation>The calculated variable handling charge plus the net charge.</xs:documentation> </xs:annotation> </xs:element> </xs:sequence> diff --git a/app/code/Magento/Fedex/etc/wsdl/ShipService_v10.wsdl b/app/code/Magento/Fedex/etc/wsdl/ShipService_v10.wsdl index 6c43af98ef68..54bb57d490c7 100644 --- a/app/code/Magento/Fedex/etc/wsdl/ShipService_v10.wsdl +++ b/app/code/Magento/Fedex/etc/wsdl/ShipService_v10.wsdl @@ -498,7 +498,7 @@ <xs:annotation> <xs:documentation> For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction. - If this shipment cotains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. + If this shipment contains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. </xs:documentation> </xs:annotation> <xs:sequence> @@ -1515,12 +1515,12 @@ </xs:simpleType> <xs:complexType name="DestinationControlDetail"> <xs:annotation> - <xs:documentation>Data required to complete the Destionation Control Statement for US exports.</xs:documentation> + <xs:documentation>Data required to complete the Destination Control Statement for US exports.</xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="StatementTypes" type="ns:DestinationControlStatementType" minOccurs="1" maxOccurs="unbounded"> <xs:annotation> - <xs:documentation>List of applicable Statment types.</xs:documentation> + <xs:documentation>List of applicable Statement types.</xs:documentation> </xs:annotation> </xs:element> <xs:element name="DestinationCountries" type="xs:string" minOccurs="0"> @@ -2524,7 +2524,7 @@ </xs:element> <xs:element name="LabelStockType" type="ns:LabelStockType" minOccurs="0"> <xs:annotation> - <xs:documentation>For thermal printer lables this indicates the size of the label and the location of the doc tab if present.</xs:documentation> + <xs:documentation>For thermal printer labels this indicates the size of the label and the location of the doc tab if present.</xs:documentation> </xs:annotation> </xs:element> <xs:element name="LabelPrintingOrientation" type="ns:LabelPrintingOrientationType" minOccurs="0"> @@ -5225,7 +5225,7 @@ </xs:element> <xs:element name="TotalCustomerCharge" type="ns:Money" minOccurs="0"> <xs:annotation> - <xs:documentation>The calculated varibale handling charge plus the net charge.</xs:documentation> + <xs:documentation>The calculated variable handling charge plus the net charge.</xs:documentation> </xs:annotation> </xs:element> </xs:sequence> diff --git a/app/code/Magento/Fedex/etc/wsdl/ShipService_v9.wsdl b/app/code/Magento/Fedex/etc/wsdl/ShipService_v9.wsdl index 8c11a5f66035..d8dc0fdfed4a 100644 --- a/app/code/Magento/Fedex/etc/wsdl/ShipService_v9.wsdl +++ b/app/code/Magento/Fedex/etc/wsdl/ShipService_v9.wsdl @@ -498,7 +498,7 @@ <xs:annotation> <xs:documentation> For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction. - If this shipment cotains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. + If this shipment contains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. </xs:documentation> </xs:annotation> <xs:sequence> @@ -1515,12 +1515,12 @@ </xs:simpleType> <xs:complexType name="DestinationControlDetail"> <xs:annotation> - <xs:documentation>Data required to complete the Destionation Control Statement for US exports.</xs:documentation> + <xs:documentation>Data required to complete the Destination Control Statement for US exports.</xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="StatementTypes" type="ns:DestinationControlStatementType" minOccurs="1" maxOccurs="unbounded"> <xs:annotation> - <xs:documentation>List of applicable Statment types.</xs:documentation> + <xs:documentation>List of applicable Statement types.</xs:documentation> </xs:annotation> </xs:element> <xs:element name="DestinationCountries" type="xs:string" minOccurs="0"> @@ -2524,7 +2524,7 @@ </xs:element> <xs:element name="LabelStockType" type="ns:LabelStockType" minOccurs="0"> <xs:annotation> - <xs:documentation>For thermal printer lables this indicates the size of the label and the location of the doc tab if present.</xs:documentation> + <xs:documentation>For thermal printer labels this indicates the size of the label and the location of the doc tab if present.</xs:documentation> </xs:annotation> </xs:element> <xs:element name="LabelPrintingOrientation" type="ns:LabelPrintingOrientationType" minOccurs="0"> @@ -5225,7 +5225,7 @@ </xs:element> <xs:element name="TotalCustomerCharge" type="ns:Money" minOccurs="0"> <xs:annotation> - <xs:documentation>The calculated varibale handling charge plus the net charge.</xs:documentation> + <xs:documentation>The calculated variable handling charge plus the net charge.</xs:documentation> </xs:annotation> </xs:element> </xs:sequence> 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 c923ced8918d..c15b76583187 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 5da8b3b55700..28a6baa436ef 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/Block/Message/Inline.php b/app/code/Magento/GiftMessage/Block/Message/Inline.php index e5b80848661f..4a9311c1b4ba 100644 --- a/app/code/Magento/GiftMessage/Block/Message/Inline.php +++ b/app/code/Magento/GiftMessage/Block/Message/Inline.php @@ -139,7 +139,7 @@ public function getType() /** * Define checkout type * - * @param $type string + * @param string $type * @return $this * @codeCoverageIgnore */ @@ -238,7 +238,7 @@ public function getMessage($entity = null) */ public function getItems() { - if (!$this->getData('items')) { + if (!$this->hasData('items')) { $items = []; $entityItems = $this->getEntity()->getAllItems(); @@ -278,6 +278,8 @@ public function countItems() } /** + * Call method getItemsHasMessages + * * @deprecated Misspelled method * @see getItemsHasMessages */ @@ -325,6 +327,20 @@ public function getEscaped($value, $defaultValue = '') return $this->escapeHtml(trim($value) != '' ? $value : $defaultValue); } + /** + * Check availability of order level functionality + * + * @return bool + */ + public function isMessagesOrderAvailable() + { + $entity = $this->getEntity(); + if (!$entity->hasIsGiftOptionsAvailable()) { + $this->_eventManager->dispatch('gift_options_prepare', ['entity' => $entity]); + } + return $entity->getIsGiftOptionsAvailable(); + } + /** * Check availability of giftmessages on order level * @@ -355,7 +371,7 @@ public function isItemMessagesAvailable($item) protected function _toHtml() { // render HTML when messages are allowed for order or for items only - if ($this->isItemsAvailable() || $this->isMessagesAvailable()) { + if ($this->isItemsAvailable() || $this->isMessagesAvailable() || $this->isMessagesOrderAvailable()) { return parent::_toHtml(); } return ''; diff --git a/app/code/Magento/GiftMessage/Model/CompositeConfigProvider.php b/app/code/Magento/GiftMessage/Model/CompositeConfigProvider.php index 0fdce9e9090a..cb370c27863c 100644 --- a/app/code/Magento/GiftMessage/Model/CompositeConfigProvider.php +++ b/app/code/Magento/GiftMessage/Model/CompositeConfigProvider.php @@ -7,6 +7,9 @@ use Magento\Checkout\Model\ConfigProviderInterface; +/** + * Class CompositeConfigProvider + */ class CompositeConfigProvider implements ConfigProviderInterface { /** @@ -18,13 +21,13 @@ class CompositeConfigProvider implements ConfigProviderInterface * @param ConfigProviderInterface[] $configProviders */ public function __construct( - array $configProviders + array $configProviders = [] ) { $this->configProviders = $configProviders; } /** - * {@inheritdoc} + * @inheritdoc */ public function getConfig() { diff --git a/app/code/Magento/GiftMessage/Model/GiftMessageConfigProvider.php b/app/code/Magento/GiftMessage/Model/GiftMessageConfigProvider.php index c7f145eaddbb..b124897770b6 100644 --- a/app/code/Magento/GiftMessage/Model/GiftMessageConfigProvider.php +++ b/app/code/Magento/GiftMessage/Model/GiftMessageConfigProvider.php @@ -13,6 +13,7 @@ use Magento\Framework\Locale\FormatInterface as LocaleFormat; use Magento\Framework\Data\Form\FormKey; use Magento\Catalog\Model\Product\Attribute\Source\Boolean; +use Magento\Store\Model\ScopeInterface; /** * Configuration provider for GiftMessage rendering on "Checkout cart" page. @@ -41,6 +42,11 @@ class GiftMessageConfigProvider implements ConfigProviderInterface */ protected $checkoutSession; + /** + * @var HttpContext + */ + protected $httpContext; + /** * @var \Magento\Store\Model\StoreManagerInterface */ @@ -57,6 +63,8 @@ class GiftMessageConfigProvider implements ConfigProviderInterface protected $formKey; /** + * GiftMessageConfigProvider constructor. + * * @param \Magento\Framework\App\Helper\Context $context * @param \Magento\GiftMessage\Api\CartRepositoryInterface $cartRepository * @param \Magento\GiftMessage\Api\ItemRepositoryInterface $itemRepository @@ -87,28 +95,28 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getConfig() { $configuration = []; $configuration['giftMessage'] = []; - $orderLevelGiftMessageConfiguration = (bool)$this->scopeConfiguration->getValue( + $orderLevelGiftMsg = $this->scopeConfiguration->isSetFlag( GiftMessageHelper::XPATH_CONFIG_GIFT_MESSAGE_ALLOW_ORDER, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE ); - $itemLevelGiftMessageConfiguration = (bool)$this->scopeConfiguration->getValue( + $itemLevelGiftMessage = $this->scopeConfiguration->isSetFlag( GiftMessageHelper::XPATH_CONFIG_GIFT_MESSAGE_ALLOW_ITEMS, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE ); - if ($orderLevelGiftMessageConfiguration) { + if ($orderLevelGiftMsg) { $orderMessages = $this->getOrderLevelGiftMessages(); $configuration['isOrderLevelGiftOptionsEnabled'] = (bool)$this->isQuoteVirtual() ? false : true; $configuration['giftMessage']['orderLevel'] = $orderMessages === null ? true : $orderMessages->getData(); } $itemMessages = $this->getItemLevelGiftMessages(); - $configuration['isItemLevelGiftOptionsEnabled'] = $itemLevelGiftMessageConfiguration; + $configuration['isItemLevelGiftOptionsEnabled'] = $itemLevelGiftMessage; $configuration['giftMessage']['itemLevel'] = $itemMessages === null ? true : $itemMessages; $configuration['priceFormat'] = $this->localeFormat->getPriceFormat( @@ -168,6 +176,7 @@ protected function getOrderLevelGiftMessages() * Load already specified item level gift messages and related configuration. * * @return \Magento\GiftMessage\Api\Data\MessageInterface[]|null + * @throws \Magento\Framework\Exception\NoSuchEntityException */ protected function getItemLevelGiftMessages() { diff --git a/app/code/Magento/GiftMessage/Model/ItemRepository.php b/app/code/Magento/GiftMessage/Model/ItemRepository.php index 3c62a489af4a..aa65bf94f361 100644 --- a/app/code/Magento/GiftMessage/Model/ItemRepository.php +++ b/app/code/Magento/GiftMessage/Model/ItemRepository.php @@ -74,7 +74,7 @@ public function __construct( } /** - * {@inheritDoc} + * @inheritdoc */ public function get($cartId, $itemId) { @@ -88,7 +88,7 @@ public function get($cartId, $itemId) throw new NoSuchEntityException( __('No item with the provided ID was found in the Cart. Verify the ID and try again.') ); - }; + } $messageId = $item->getGiftMessageId(); if (!$messageId) { return null; @@ -103,7 +103,7 @@ public function get($cartId, $itemId) } /** - * {@inheritDoc} + * @inheritdoc */ public function save($cartId, \Magento\GiftMessage\Api\Data\MessageInterface $giftMessage, $itemId) { @@ -121,7 +121,7 @@ public function save($cartId, \Magento\GiftMessage\Api\Data\MessageInterface $gi $itemId ) ); - }; + } if ($item->getIsVirtual()) { throw new InvalidTransitionException(__('Gift messages can\'t be used for virtual products.')); diff --git a/app/code/Magento/GiftMessage/Model/OrderItemRepository.php b/app/code/Magento/GiftMessage/Model/OrderItemRepository.php index 943552e2b75b..445ba54ac4d9 100644 --- a/app/code/Magento/GiftMessage/Model/OrderItemRepository.php +++ b/app/code/Magento/GiftMessage/Model/OrderItemRepository.php @@ -80,7 +80,7 @@ public function __construct( } /** - * {@inheritDoc} + * @inheritdoc */ public function get($orderId, $orderItemId) { @@ -89,7 +89,7 @@ public function get($orderId, $orderItemId) throw new NoSuchEntityException( __('No item with the provided ID was found in the Order. Verify the ID and try again.') ); - }; + } if (!$this->helper->isMessagesAllowed('order_item', $orderItem, $this->storeManager->getStore())) { throw new NoSuchEntityException( @@ -111,7 +111,7 @@ public function get($orderId, $orderItemId) } /** - * {@inheritDoc} + * @inheritdoc */ public function save($orderId, $orderItemId, \Magento\GiftMessage\Api\Data\MessageInterface $giftMessage) { @@ -123,7 +123,7 @@ public function save($orderId, $orderItemId, \Magento\GiftMessage\Api\Data\Messa throw new NoSuchEntityException( __('No item with the provided ID was found in the Order. Verify the ID and try again.') ); - }; + } if ($order->getIsVirtual()) { throw new InvalidTransitionException(__("Gift messages can't be used for virtual products.")); diff --git a/app/code/Magento/GiftMessage/Model/OrderRepository.php b/app/code/Magento/GiftMessage/Model/OrderRepository.php index abf38f1287b7..e943fa2a3b08 100644 --- a/app/code/Magento/GiftMessage/Model/OrderRepository.php +++ b/app/code/Magento/GiftMessage/Model/OrderRepository.php @@ -74,7 +74,7 @@ public function __construct( } /** - * {@inheritDoc} + * @inheritdoc */ public function get($orderId) { @@ -98,7 +98,7 @@ public function get($orderId) } /** - * {@inheritDoc} + * @inheritdoc */ public function save($orderId, \Magento\GiftMessage\Api\Data\MessageInterface $giftMessage) { @@ -106,7 +106,7 @@ public function save($orderId, \Magento\GiftMessage\Api\Data\MessageInterface $g $order = $this->orderFactory->create()->load($orderId); if (!$order->getEntityId()) { throw new NoSuchEntityException(__('No order exists with this ID. Verify your information and try again.')); - }; + } if (0 == $order->getTotalItemCount()) { throw new InputException( diff --git a/app/code/Magento/GiftMessage/Model/Type/Plugin/Onepage.php b/app/code/Magento/GiftMessage/Model/Type/Plugin/Onepage.php index adb500a81851..e1c8c0b5bf5a 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/GiftMessage/Test/Mftf/ActionGroup/CheckingGiftOptionsActionGroup.xml b/app/code/Magento/GiftMessage/Test/Mftf/ActionGroup/CheckingGiftOptionsActionGroup.xml new file mode 100644 index 000000000000..f81877006c7a --- /dev/null +++ b/app/code/Magento/GiftMessage/Test/Mftf/ActionGroup/CheckingGiftOptionsActionGroup.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/testSchema.xsd"> + <actionGroup name="CheckingGiftOptionsActionGroup"> + <click stepKey="clickOnCheckoutWithMultipleAddresses" selector="{{MultishippingSection.checkoutWithMultipleAddresses}}"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <click stepKey="goToShippingInformation" selector="{{AdminShipmentAddressInformationSection.goToShippingInformation}}"/> + <waitForPageLoad stepKey="waitForGiftOption"/> + <click stepKey="thickAddGiftOptions" selector="{{GiftOptionsOnFrontSection.giftOptionCheckbox}}"/> + <waitForPageLoad stepKey="waitForOptions"/> + <see stepKey="seeAddGiftOptionsForIndividualItems" userInput="Add Gift Options for Individual Items"/> + <dontSee stepKey="dontSeeOtherElement1" userInput="Send Gift Receipt"/> + <dontSee stepKey="dontSeeOtherElement2" userInput="Add Printed Card"/> + </actionGroup> +</actionGroups> + diff --git a/app/code/Magento/GiftMessage/Test/Mftf/Data/GiftOptionsData.xml b/app/code/Magento/GiftMessage/Test/Mftf/Data/GiftOptionsData.xml new file mode 100644 index 000000000000..bb3f566f1cbc --- /dev/null +++ b/app/code/Magento/GiftMessage/Test/Mftf/Data/GiftOptionsData.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="EnableGiftMessageForOrder" type="gift_message_config_state"> + <requiredEntity type="allow_order">AllowGiftMessageForOrder</requiredEntity> + </entity> + <entity name="AllowGiftMessageForOrder" type="allow_order"> + <data key="value">1</data> + </entity> + <entity name="DefaultConfigGiftMessageOptions" type="gift_message_config_state"> + <requiredEntity type="allow_order">OrderDefault</requiredEntity> + <requiredEntity type="allow_items">ItemsDefault</requiredEntity> + </entity> + <entity name="OrderDefault" type="allow_order"> + <data key="value">0</data> + </entity> + <entity name="ItemsDefault" type="allow_items"> + <data key="value">0</data> + </entity> +</entities> diff --git a/app/code/Magento/GiftMessage/Test/Mftf/Metadata/gift_options-meta.xml b/app/code/Magento/GiftMessage/Test/Mftf/Metadata/gift_options-meta.xml new file mode 100644 index 000000000000..d4b7997e77d1 --- /dev/null +++ b/app/code/Magento/GiftMessage/Test/Mftf/Metadata/gift_options-meta.xml @@ -0,0 +1,24 @@ +<?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="SalesGiftMessageConfigState" dataType="gift_message_config_state" type="create" auth="adminFormKey" url="/admin/system_config/save/section/sales/" method="POST" successRegex="/messages-message-success/"> + <object key="groups" dataType="gift_message_config_state"> + <object key="gift_options" dataType="gift_message_config_state"> + <object key="fields" dataType="gift_message_config_state"> + <object key="allow_order" dataType="allow_order"> + <field key="value">string</field> + </object> + <object key="allow_items" dataType="allow_items"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/GiftMessage/Test/Mftf/Section/GiftOptionsOnFrontSection.xml b/app/code/Magento/GiftMessage/Test/Mftf/Section/GiftOptionsOnFrontSection.xml new file mode 100644 index 000000000000..63243030682b --- /dev/null +++ b/app/code/Magento/GiftMessage/Test/Mftf/Section/GiftOptionsOnFrontSection.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="GiftOptionsOnFrontSection"> + <element name="giftOptionCheckbox" type="text" selector="//span[text()='Add Gift Options']"/> + <element name="addGiftOptionsForIndividualItems" type="text" selector="//dt[@class='order-title individual']"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/GiftMessage/Test/Unit/Model/GiftMessageConfigProviderTest.php b/app/code/Magento/GiftMessage/Test/Unit/Model/GiftMessageConfigProviderTest.php index a5580606134b..08ebefb68d72 100644 --- a/app/code/Magento/GiftMessage/Test/Unit/Model/GiftMessageConfigProviderTest.php +++ b/app/code/Magento/GiftMessage/Test/Unit/Model/GiftMessageConfigProviderTest.php @@ -111,7 +111,7 @@ public function testGetConfig() ); $messageMock = $this->createMock(\Magento\GiftMessage\Model\Message::class); - $this->scopeConfigMock->expects($this->atLeastOnce())->method('getValue')->willReturnMap( + $this->scopeConfigMock->expects($this->atLeastOnce())->method('isSetFlag')->willReturnMap( [ [GiftMessageHelper::XPATH_CONFIG_GIFT_MESSAGE_ALLOW_ORDER, Scope::SCOPE_STORE, null, $orderLevel], [GiftMessageHelper::XPATH_CONFIG_GIFT_MESSAGE_ALLOW_ITEMS, Scope::SCOPE_STORE, null, $itemLevel] diff --git a/app/code/Magento/GiftMessage/Test/Unit/Model/Plugin/OrderSaveTest.php b/app/code/Magento/GiftMessage/Test/Unit/Model/Plugin/OrderSaveTest.php index 2170864407ea..f3e060ad5fc7 100644 --- a/app/code/Magento/GiftMessage/Test/Unit/Model/Plugin/OrderSaveTest.php +++ b/app/code/Magento/GiftMessage/Test/Unit/Model/Plugin/OrderSaveTest.php @@ -128,7 +128,7 @@ public function testAfterSaveGiftMessages() /** * @expectedException \Magento\Framework\Exception\CouldNotSaveException - * @expectedMessage The gift message couldn't be added to the "Test message" order. + * @expectedExceptionMessage The gift message couldn't be added to the "Test message" order. */ public function testAfterSaveIfGiftMessagesNotExist() { @@ -146,7 +146,7 @@ public function testAfterSaveIfGiftMessagesNotExist() $this->giftMessageOrderRepositoryMock ->expects($this->once()) ->method('save') - ->willThrowException(new \Exception('TestMessage')); + ->willThrowException(new \Exception('Test message')); // save Gift Messages on item level $this->orderMock->expects($this->never())->method('getItems'); @@ -155,7 +155,7 @@ public function testAfterSaveIfGiftMessagesNotExist() /** * @expectedException \Magento\Framework\Exception\CouldNotSaveException - * @expectedMessage The gift message couldn't be added to the "Test message" order. + * @expectedExceptionMessage The gift message couldn't be added to the "Test message" order item. */ public function testAfterSaveIfItemGiftMessagesNotExist() { @@ -185,7 +185,7 @@ public function testAfterSaveIfItemGiftMessagesNotExist() $this->giftMessageOrderItemRepositoryMock ->expects($this->once())->method('save') ->with($orderId, $orderItemId, $this->giftMessageMock) - ->willThrowException(new \Exception('TestMessage')); + ->willThrowException(new \Exception('Test message')); $this->plugin->afterSave($this->orderRepositoryMock, $this->orderMock); } } diff --git a/app/code/Magento/GiftMessage/Test/Unit/Observer/SalesEventQuoteMergeTest.php b/app/code/Magento/GiftMessage/Test/Unit/Observer/SalesEventQuoteMergeTest.php new file mode 100644 index 000000000000..7a3000f7c074 --- /dev/null +++ b/app/code/Magento/GiftMessage/Test/Unit/Observer/SalesEventQuoteMergeTest.php @@ -0,0 +1,81 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\GiftMessage\Test\Unit\Observer; + +use Magento\GiftMessage\Observer\SalesEventQuoteMerge; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\Event\Observer; +use Magento\Quote\Model\Quote; + +/** + * SalesEventQuoteMergeTest + */ +class SalesEventQuoteMergeTest extends \PHPUnit\Framework\TestCase +{ + + /** + * @var SalesEventQuoteMerge + */ + private $salesEventQuoteMerge; + + /** + * @return void + */ + public function setUp(): void + { + $objectManger = new ObjectManager($this); + $this->salesEventQuoteMerge = $objectManger->getObject(SalesEventQuoteMerge::class); + } + + /** + * @dataProvider dataProviderGiftMessageId + * + * @param null|int $giftMessageId + * + * @return void + */ + public function testExecute($giftMessageId): void + { + $sourceQuoteMock = $this->createPartialMock(Quote::class, ['getGiftMessageId']); + $sourceQuoteMock->expects($this->once()) + ->method('getGiftMessageId') + ->willReturn($giftMessageId); + + $targetQuoteMock = $this->createPartialMock(Quote::class, ['setGiftMessageId']); + + if ($giftMessageId) { + $targetQuoteMock->expects($this->once()) + ->method('setGiftMessageId'); + } else { + $targetQuoteMock->expects($this->never()) + ->method('setGiftMessageId'); + } + + $observer = $this->createMock(Observer::class); + $observer->expects($this->exactly(2)) + ->method('getData') + ->willReturnMap([ + ['quote', null, $targetQuoteMock], + ['source', null, $sourceQuoteMock] + ]); + + $this->salesEventQuoteMerge->execute($observer); + } + + /** + * @return array + */ + public function dataProviderGiftMessageId(): array + { + return [ + [null], + [1] + ]; + } +} diff --git a/app/code/Magento/GiftMessage/etc/db_schema.xml b/app/code/Magento/GiftMessage/etc/db_schema.xml index 518c6398e4c8..4ae98799df0c 100644 --- a/app/code/Magento/GiftMessage/etc/db_schema.xml +++ b/app/code/Magento/GiftMessage/etc/db_schema.xml @@ -15,7 +15,7 @@ <column xsi:type="varchar" name="sender" nullable="true" length="255" comment="Sender"/> <column xsi:type="varchar" name="recipient" nullable="true" length="255" comment="Registrant"/> <column xsi:type="text" name="message" nullable="true" comment="Message"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="gift_message_id"/> </constraint> </table> diff --git a/app/code/Magento/GiftMessage/view/frontend/requirejs-config.js b/app/code/Magento/GiftMessage/view/frontend/requirejs-config.js index db33efeba1a9..f8a463bec37f 100644 --- a/app/code/Magento/GiftMessage/view/frontend/requirejs-config.js +++ b/app/code/Magento/GiftMessage/view/frontend/requirejs-config.js @@ -6,8 +6,10 @@ var config = { map: { '*': { - giftOptions: 'Magento_GiftMessage/gift-options', - extraOptions: 'Magento_GiftMessage/extra-options' + giftOptions: 'Magento_GiftMessage/js/gift-options', + extraOptions: 'Magento_GiftMessage/js/extra-options', + 'Magento_GiftMessage/gift-options': 'Magento_GiftMessage/js/gift-options', + 'Magento_GiftMessage/extra-options': 'Magento_GiftMessage/js/extra-options' } } }; diff --git a/app/code/Magento/GiftMessage/view/frontend/templates/inline.phtml b/app/code/Magento/GiftMessage/view/frontend/templates/inline.phtml index dec54cfeb9df..640ef1ba1648 100644 --- a/app/code/Magento/GiftMessage/view/frontend/templates/inline.phtml +++ b/app/code/Magento/GiftMessage/view/frontend/templates/inline.phtml @@ -152,6 +152,7 @@ </div> <dl class="options-items" id="allow-gift-options-container-<?= /* @escapeNotVerified */ $block->getEntity()->getId() ?>"> + <?php if ($block->isMessagesOrderAvailable() || $block->isMessagesAvailable()): ?> <dt id="add-gift-options-for-order-<?= /* @escapeNotVerified */ $block->getEntity()->getId() ?>" class="order-title"> <div class="field choice"> <input type="checkbox" name="allow_gift_options_for_order_<?= /* @escapeNotVerified */ $block->getEntity()->getId() ?>" id="allow_gift_options_for_order_<?= /* @escapeNotVerified */ $block->getEntity()->getId() ?>" data-mage-init='{"giftOptions":{}}' value="1" data-selector='{"id":"#allow-gift-options-for-order-container-<?= /* @escapeNotVerified */ $block->getEntity()->getId() ?>"}'<?php if ($block->getEntityHasMessage()): ?> checked="checked"<?php endif; ?> class="checkbox" /> @@ -192,7 +193,7 @@ </div> <?php endif; ?> </dd> - + <?php endif; ?> <?php if ($block->isItemsAvailable()): ?> <dt id="add-gift-options-for-items-<?= /* @escapeNotVerified */ $block->getEntity()->getId() ?>" class="order-title individual"> <div class="field choice"> diff --git a/app/code/Magento/GiftMessage/view/frontend/web/extra-options.js b/app/code/Magento/GiftMessage/view/frontend/web/js/extra-options.js similarity index 100% rename from app/code/Magento/GiftMessage/view/frontend/web/extra-options.js rename to app/code/Magento/GiftMessage/view/frontend/web/js/extra-options.js diff --git a/app/code/Magento/GiftMessage/view/frontend/web/gift-options.js b/app/code/Magento/GiftMessage/view/frontend/web/js/gift-options.js similarity index 100% rename from app/code/Magento/GiftMessage/view/frontend/web/gift-options.js rename to app/code/Magento/GiftMessage/view/frontend/web/js/gift-options.js diff --git a/app/code/Magento/GiftMessage/view/frontend/web/js/view/gift-message.js b/app/code/Magento/GiftMessage/view/frontend/web/js/view/gift-message.js index d025f6974f35..4c455c83a77a 100644 --- a/app/code/Magento/GiftMessage/view/frontend/web/js/view/gift-message.js +++ b/app/code/Magento/GiftMessage/view/frontend/web/js/view/gift-message.js @@ -31,8 +31,9 @@ define([ this.itemId = this.itemId || 'orderLevel'; model = new GiftMessage(this.itemId); - giftOptions.addOption(model); this.model = model; + this.isResultBlockVisible(); + giftOptions.addOption(model); this.model.getObservable('isClear').subscribe(function (value) { if (value == true) { //eslint-disable-line eqeqeq @@ -40,8 +41,6 @@ define([ self.model.getObservable('alreadyAdded')(true); } }); - - this.isResultBlockVisible(); }, /** diff --git a/app/code/Magento/GoogleAnalytics/Block/Ga.php b/app/code/Magento/GoogleAnalytics/Block/Ga.php index 7d065ea50b36..b5917407b60a 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 2af03c71fb1b..90a207921d51 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/GoogleAnalytics/view/frontend/layout/default.xml b/app/code/Magento/GoogleAnalytics/view/frontend/layout/default.xml index 892a22129adf..c3f34c5abc1d 100644 --- a/app/code/Magento/GoogleAnalytics/view/frontend/layout/default.xml +++ b/app/code/Magento/GoogleAnalytics/view/frontend/layout/default.xml @@ -7,8 +7,8 @@ --> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> - <referenceContainer name="head.additional"> + <referenceBlock name="head.additional"> <block class="Magento\GoogleAnalytics\Block\Ga" name="google_analytics" as="google_analytics" template="Magento_GoogleAnalytics::ga.phtml"/> - </referenceContainer> + </referenceBlock> </body> </page> diff --git a/app/code/Magento/GoogleOptimizer/Helper/Data.php b/app/code/Magento/GoogleOptimizer/Helper/Data.php index 5b0291eb1435..7138a7729fe0 100644 --- a/app/code/Magento/GoogleOptimizer/Helper/Data.php +++ b/app/code/Magento/GoogleOptimizer/Helper/Data.php @@ -11,6 +11,8 @@ use \Magento\Store\Model\ScopeInterface; /** + * Class Data + * * @api * @since 100.0.2 */ @@ -32,6 +34,8 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper protected $_analyticsHelper; /** + * Data constructor. + * * @param \Magento\Framework\App\Helper\Context $context * @param \Magento\GoogleAnalytics\Helper\Data $analyticsHelper */ @@ -51,7 +55,7 @@ public function __construct( */ public function isGoogleExperimentEnabled($store = null) { - return (bool)$this->scopeConfig->isSetFlag(self::XML_PATH_ENABLED, ScopeInterface::SCOPE_STORE, $store); + return $this->scopeConfig->isSetFlag(self::XML_PATH_ENABLED, ScopeInterface::SCOPE_STORE, $store); } /** diff --git a/app/code/Magento/GoogleOptimizer/etc/db_schema.xml b/app/code/Magento/GoogleOptimizer/etc/db_schema.xml index ad3e03e490b0..76c377544cfb 100644 --- a/app/code/Magento/GoogleOptimizer/etc/db_schema.xml +++ b/app/code/Magento/GoogleOptimizer/etc/db_schema.xml @@ -16,12 +16,12 @@ <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Store id"/> <column xsi:type="text" name="experiment_script" nullable="true" comment="Google experiment script"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="code_id"/> </constraint> - <constraint xsi:type="foreign" name="GOOGLEOPTIMIZER_CODE_STORE_ID_STORE_STORE_ID" table="googleoptimizer_code" + <constraint xsi:type="foreign" referenceId="GOOGLEOPTIMIZER_CODE_STORE_ID_STORE_STORE_ID" table="googleoptimizer_code" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="GOOGLEOPTIMIZER_CODE_STORE_ID_ENTITY_ID_ENTITY_TYPE"> + <constraint xsi:type="unique" referenceId="GOOGLEOPTIMIZER_CODE_STORE_ID_ENTITY_ID_ENTITY_TYPE"> <column name="store_id"/> <column name="entity_id"/> <column name="entity_type"/> diff --git a/app/code/Magento/GraphQl/Controller/GraphQl.php b/app/code/Magento/GraphQl/Controller/GraphQl.php index 493cabfaee92..75b3ad277c60 100644 --- a/app/code/Magento/GraphQl/Controller/GraphQl.php +++ b/app/code/Magento/GraphQl/Controller/GraphQl.php @@ -17,16 +17,22 @@ use Magento\Framework\GraphQl\Schema\SchemaGeneratorInterface; use Magento\Framework\Serialize\SerializerInterface; use Magento\Framework\Webapi\Response; +use Magento\Framework\App\Response\Http as HttpResponse; +use Magento\Framework\GraphQl\Query\Fields as QueryFields; +use Magento\Framework\Controller\Result\JsonFactory; +use Magento\Framework\App\ObjectManager; /** * Front controller for web API GraphQL area. * * @api + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class GraphQl implements FrontControllerInterface { /** - * @var Response + * @var \Magento\Framework\Webapi\Response + * @deprecated */ private $response; @@ -46,12 +52,12 @@ class GraphQl implements FrontControllerInterface private $queryProcessor; /** - * @var \Magento\Framework\GraphQl\Exception\ExceptionFormatter + * @var ExceptionFormatter */ private $graphQlError; /** - * @var \Magento\Framework\GraphQl\Query\Resolver\ContextInterface + * @var ContextInterface */ private $resolverContext; @@ -60,14 +66,33 @@ class GraphQl implements FrontControllerInterface */ private $requestProcessor; + /** + * @var QueryFields + */ + private $queryFields; + + /** + * @var JsonFactory + */ + private $jsonFactory; + + /** + * @var HttpResponse + */ + private $httpResponse; + /** * @param Response $response * @param SchemaGeneratorInterface $schemaGenerator * @param SerializerInterface $jsonSerializer * @param QueryProcessor $queryProcessor - * @param \Magento\Framework\GraphQl\Exception\ExceptionFormatter $graphQlError - * @param \Magento\Framework\GraphQl\Query\Resolver\ContextInterface $resolverContext + * @param ExceptionFormatter $graphQlError + * @param ContextInterface $resolverContext * @param HttpRequestProcessor $requestProcessor + * @param QueryFields $queryFields + * @param JsonFactory|null $jsonFactory + * @param HttpResponse|null $httpResponse + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( Response $response, @@ -76,7 +101,10 @@ public function __construct( QueryProcessor $queryProcessor, ExceptionFormatter $graphQlError, ContextInterface $resolverContext, - HttpRequestProcessor $requestProcessor + HttpRequestProcessor $requestProcessor, + QueryFields $queryFields, + JsonFactory $jsonFactory = null, + HttpResponse $httpResponse = null ) { $this->response = $response; $this->schemaGenerator = $schemaGenerator; @@ -85,6 +113,9 @@ public function __construct( $this->graphQlError = $graphQlError; $this->resolverContext = $resolverContext; $this->requestProcessor = $requestProcessor; + $this->queryFields = $queryFields; + $this->jsonFactory = $jsonFactory ?: ObjectManager::getInstance()->get(JsonFactory::class); + $this->httpResponse = $httpResponse ?: ObjectManager::getInstance()->get(HttpResponse::class); } /** @@ -96,26 +127,59 @@ public function __construct( public function dispatch(RequestInterface $request) : ResponseInterface { $statusCode = 200; + $jsonResult = $this->jsonFactory->create(); try { /** @var Http $request */ - $this->requestProcessor->processHeaders($request); - $data = $this->jsonSerializer->unserialize($request->getContent()); + $this->requestProcessor->validateRequest($request); + + $data = $this->getDataFromRequest($request); + $query = $data['query'] ?? ''; + $variables = $data['variables'] ?? null; + + // We must extract queried field names to avoid instantiation of unnecessary fields in webonyx schema + // Temporal coupling is required for performance optimization + $this->queryFields->setQuery($query, $variables); $schema = $this->schemaGenerator->generate(); + $result = $this->queryProcessor->process( $schema, - isset($data['query']) ? $data['query'] : '', + $query, $this->resolverContext, - isset($data['variables']) ? $data['variables'] : [] + $data['variables'] ?? [] ); } catch (\Exception $error) { $result['errors'] = isset($result) && isset($result['errors']) ? $result['errors'] : []; $result['errors'][] = $this->graphQlError->create($error); $statusCode = ExceptionFormatter::HTTP_GRAPH_QL_SCHEMA_ERROR_STATUS; } - $this->response->setBody($this->jsonSerializer->serialize($result))->setHeader( - 'Content-Type', - 'application/json' - )->setHttpResponseCode($statusCode); - return $this->response; + + $jsonResult->setHttpResponseCode($statusCode); + $jsonResult->setData($result); + $jsonResult->renderResult($this->httpResponse); + return $this->httpResponse; + } + + /** + * Get data from request body or query string + * + * @param RequestInterface $request + * @return array + */ + private function getDataFromRequest(RequestInterface $request) : array + { + /** @var Http $request */ + if ($request->isPost()) { + $data = $this->jsonSerializer->unserialize($request->getContent()); + } elseif ($request->isGet()) { + $data = $request->getParams(); + $data['variables'] = isset($data['variables']) ? + $this->jsonSerializer->unserialize($data['variables']) : null; + $data['variables'] = is_array($data['variables']) ? + $data['variables'] : null; + } else { + return []; + } + + return $data; } } diff --git a/app/code/Magento/GraphQl/Controller/HttpHeaderProcessor/ContentTypeProcessor.php b/app/code/Magento/GraphQl/Controller/HttpHeaderProcessor/ContentTypeProcessor.php deleted file mode 100644 index 2270f2616e67..000000000000 --- a/app/code/Magento/GraphQl/Controller/HttpHeaderProcessor/ContentTypeProcessor.php +++ /dev/null @@ -1,32 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\GraphQl\Controller\HttpHeaderProcessor; - -use Magento\Framework\Exception\LocalizedException; -use Magento\GraphQl\Controller\HttpHeaderProcessorInterface; - -/** - * Processes the "Content-Type" header entry - */ -class ContentTypeProcessor implements HttpHeaderProcessorInterface -{ - /** - * Handle the mandatory application/json header - * - * {@inheritDoc} - * @throws LocalizedException - */ - public function processHeaderValue(string $headerValue) : void - { - if (!$headerValue || strpos($headerValue, 'application/json') === false) { - throw new LocalizedException( - new \Magento\Framework\Phrase('Request content type must be application/json') - ); - } - } -} diff --git a/app/code/Magento/GraphQl/Controller/HttpHeaderProcessor/StoreProcessor.php b/app/code/Magento/GraphQl/Controller/HttpHeaderProcessor/StoreProcessor.php deleted file mode 100644 index be359eafdf24..000000000000 --- a/app/code/Magento/GraphQl/Controller/HttpHeaderProcessor/StoreProcessor.php +++ /dev/null @@ -1,55 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\GraphQl\Controller\HttpHeaderProcessor; - -use Magento\Framework\Exception\NoSuchEntityException; -use Magento\Framework\GraphQl\Exception\GraphQlInputException; -use Magento\GraphQl\Controller\HttpHeaderProcessorInterface; -use Magento\Store\Model\StoreManagerInterface; - -/** - * Process the "Store" header entry - */ -class StoreProcessor implements HttpHeaderProcessorInterface -{ - /** - * @var StoreManagerInterface - */ - private $storeManager; - - /** - * StoreProcessor constructor. - * - * @param StoreManagerInterface $storeManager - */ - public function __construct(StoreManagerInterface $storeManager) - { - $this->storeManager = $storeManager; - } - - /** - * Handle the value of the store and set the scope - * - * {@inheritDoc} - * @throws NoSuchEntityException - */ - public function processHeaderValue(string $headerValue) : void - { - if ($headerValue) { - $storeCode = ltrim(rtrim($headerValue)); - $stores = $this->storeManager->getStores(false, true); - if (isset($stores[$storeCode])) { - $this->storeManager->setCurrentStore($storeCode); - } elseif (strtolower($storeCode) !== 'default') { - throw new GraphQlInputException( - new \Magento\Framework\Phrase('Store code %1 does not exist', [$storeCode]) - ); - } - } - } -} diff --git a/app/code/Magento/GraphQl/Controller/HttpRequestProcessor.php b/app/code/Magento/GraphQl/Controller/HttpRequestProcessor.php index 5e42b81a6198..bb29f1fa68af 100644 --- a/app/code/Magento/GraphQl/Controller/HttpRequestProcessor.php +++ b/app/code/Magento/GraphQl/Controller/HttpRequestProcessor.php @@ -19,12 +19,19 @@ class HttpRequestProcessor */ private $headerProcessors = []; + /** + * @var HttpRequestValidatorInterface[] array + */ + private $requestValidators = []; + /** * @param HttpHeaderProcessorInterface[] $graphQlHeaders + * @param HttpRequestValidatorInterface[] $requestValidators */ - public function __construct(array $graphQlHeaders = []) + public function __construct(array $graphQlHeaders = [], array $requestValidators = []) { $this->headerProcessors = $graphQlHeaders; + $this->requestValidators = $requestValidators; } /** @@ -39,4 +46,17 @@ public function processHeaders(Http $request) : void $headerClass->processHeaderValue((string)$request->getHeader($headerName)); } } + + /** + * Validate HTTP request + * + * @param Http $request + * @return void + */ + public function validateRequest(Http $request) : void + { + foreach ($this->requestValidators as $requestValidator) { + $requestValidator->validate($request); + } + } } diff --git a/app/code/Magento/GraphQl/Controller/HttpRequestValidator/ContentTypeValidator.php b/app/code/Magento/GraphQl/Controller/HttpRequestValidator/ContentTypeValidator.php new file mode 100644 index 000000000000..555048aac677 --- /dev/null +++ b/app/code/Magento/GraphQl/Controller/HttpRequestValidator/ContentTypeValidator.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Controller\HttpRequestValidator; + +use Magento\Framework\App\HttpRequestInterface; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\GraphQl\Controller\HttpRequestValidatorInterface; + +/** + * Processes the "Content-Type" header entry + */ +class ContentTypeValidator implements HttpRequestValidatorInterface +{ + /** + * Handle the mandatory application/json header + * + * @param HttpRequestInterface $request + * @return void + * @throws GraphQlInputException + */ + public function validate(HttpRequestInterface $request) : void + { + $headerName = 'Content-Type'; + $requiredHeaderValue = 'application/json'; + + $headerValue = (string)$request->getHeader($headerName); + if ($request->isPost() + && strpos($headerValue, $requiredHeaderValue) === false + ) { + throw new GraphQlInputException( + new \Magento\Framework\Phrase('Request content type must be application/json') + ); + } + } +} diff --git a/app/code/Magento/GraphQl/Controller/HttpRequestValidator/HttpVerbValidator.php b/app/code/Magento/GraphQl/Controller/HttpRequestValidator/HttpVerbValidator.php new file mode 100644 index 000000000000..300b3d4f44dc --- /dev/null +++ b/app/code/Magento/GraphQl/Controller/HttpRequestValidator/HttpVerbValidator.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Controller\HttpRequestValidator; + +use Magento\Framework\App\HttpRequestInterface; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\App\Request\Http; +use Magento\GraphQl\Controller\HttpRequestValidatorInterface; + +/** + * Validator to check HTTP verb for Graphql requests + */ +class HttpVerbValidator implements HttpRequestValidatorInterface +{ + /** + * Check if request is using correct verb for query or mutation + * + * @param HttpRequestInterface $request + * @return void + * @throws GraphQlInputException + */ + public function validate(HttpRequestInterface $request) : void + { + /** @var Http $request */ + if (false === $request->isPost()) { + $query = $request->getParam('query', ''); + // The easiest way to determine mutations without additional parsing + if (strpos(trim($query), 'mutation') === 0) { + throw new GraphQlInputException( + new \Magento\Framework\Phrase('Mutation requests allowed only for POST requests') + ); + } + } + } +} diff --git a/app/code/Magento/GraphQl/Controller/HttpRequestValidatorInterface.php b/app/code/Magento/GraphQl/Controller/HttpRequestValidatorInterface.php new file mode 100644 index 000000000000..2d9d50569e34 --- /dev/null +++ b/app/code/Magento/GraphQl/Controller/HttpRequestValidatorInterface.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Controller; + +use Magento\Framework\App\HttpRequestInterface; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; + +/** + * Use this interface to implement a validator for a Graphql HTTP requests + */ +interface HttpRequestValidatorInterface +{ + /** + * Perform validation of request + * + * @param HttpRequestInterface $request + * @return void + * @throws GraphQlInputException + */ + public function validate(HttpRequestInterface $request) : void; +} diff --git a/app/code/Magento/GraphQl/Model/EntityAttributeList.php b/app/code/Magento/GraphQl/Model/EntityAttributeList.php index 6b8a4f477069..3802b74f3ec1 100644 --- a/app/code/Magento/GraphQl/Model/EntityAttributeList.php +++ b/app/code/Magento/GraphQl/Model/EntityAttributeList.php @@ -14,7 +14,7 @@ use Magento\Framework\Api\MetadataServiceInterface; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\Exception\NoSuchEntityException; -use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; /** * Iterate through all attribute sets to retrieve attributes for any given entity type @@ -69,7 +69,7 @@ public function __construct( * @param string $entityCode * @param MetadataServiceInterface $metadataService * @return boolean[] - * @throws GraphQlInputException + * @throws GraphQlNoSuchEntityException */ public function getDefaultEntityAttributes( string $entityCode, @@ -93,7 +93,7 @@ public function getDefaultEntityAttributes( $this->attributeManagement->getAttributes($entityCode, $attributeSet->getAttributeSetId()) ); } catch (NoSuchEntityException $exception) { - throw new GraphQlInputException(__('Entity code %1 does not exist.', [$entityCode])); + throw new GraphQlNoSuchEntityException(__('Entity code %1 does not exist.', [$entityCode])); } } $attributeCodes = []; diff --git a/app/code/Magento/GraphQl/composer.json b/app/code/Magento/GraphQl/composer.json index 3e821b090944..3a1e8d1bfd9f 100644 --- a/app/code/Magento/GraphQl/composer.json +++ b/app/code/Magento/GraphQl/composer.json @@ -5,12 +5,12 @@ "require": { "php": "~7.1.3||~7.2.0", "magento/module-authorization": "*", - "magento/module-store": "*", "magento/module-eav": "*", "magento/framework": "*" }, "suggest": { - "magento/module-webapi": "*" + "magento/module-webapi": "*", + "magento/module-graph-ql-cache": "*" }, "license": [ "OSL-3.0", diff --git a/app/code/Magento/GraphQl/etc/di.xml b/app/code/Magento/GraphQl/etc/di.xml index 105fa8bf166b..6acb78f9c7f9 100644 --- a/app/code/Magento/GraphQl/etc/di.xml +++ b/app/code/Magento/GraphQl/etc/di.xml @@ -27,7 +27,7 @@ <argument name="factoryMapByConfigElementType" xsi:type="array"> <item name="graphql_interface" xsi:type="object">Magento\Framework\GraphQl\Config\Element\InterfaceFactory</item> <item name="graphql_type" xsi:type="object">Magento\Framework\GraphQl\Config\Element\TypeFactory</item> - <item name="graphql_input" xsi:type="object">Magento\Framework\GraphQl\Config\Element\TypeFactory</item> + <item name="graphql_input" xsi:type="object">Magento\Framework\GraphQl\Config\Element\InputFactory</item> <item name="graphql_enum" xsi:type="object">Magento\Framework\GraphQl\Config\Element\EnumFactory</item> </argument> </arguments> @@ -55,24 +55,16 @@ </argument> </arguments> </virtualType> - <type name="Magento\Framework\GraphQl\Schema\Type\Output\OutputFactory"> + <type name="Magento\Framework\GraphQl\Schema\Type\TypeRegistry"> <arguments> - <argument name="prototypes" xsi:type="array"> + <argument name="configToTypeMap" xsi:type="array"> <item name="Magento\Framework\GraphQl\Config\Element\Type" xsi:type="string">Magento\Framework\GraphQl\Schema\Type\Output\OutputTypeObject</item> + <item name="Magento\Framework\GraphQl\Config\Element\Input" xsi:type="string">Magento\Framework\GraphQl\Schema\Type\Input\InputObjectType</item> <item name="Magento\Framework\GraphQl\Config\Element\InterfaceType" xsi:type="string">Magento\Framework\GraphQl\Schema\Type\Output\OutputInterfaceObject</item> <item name="Magento\Framework\GraphQl\Config\Element\Enum" xsi:type="string">Magento\Framework\GraphQl\Schema\Type\Enum\Enum</item> </argument> </arguments> </type> - <type name="Magento\Framework\GraphQl\Schema\Type\Input\InputFactory"> - <arguments> - <argument name="prototypes" xsi:type="array"> - <item name="Magento\Framework\GraphQl\Config\Element\Type" xsi:type="string">Magento\Framework\GraphQl\Schema\Type\Input\InputObjectType</item> - <item name="Magento\Framework\GraphQl\Config\Element\InterfaceType" xsi:type="string">Magento\Framework\GraphQl\Schema\Type\Input\InputObjectType</item> - <item name="Magento\Framework\GraphQl\Config\Element\Enum" xsi:type="string">Magento\Framework\GraphQl\Schema\Type\Enum\Enum</item> - </argument> - </arguments> - </type> <type name="Magento\Framework\GraphQl\Schema\Type\Output\ElementMapper"> <arguments> <argument name="formatter" xsi:type="object">Magento\Framework\GraphQl\Schema\Type\Output\ElementMapper\FormatterComposite</argument> @@ -97,4 +89,10 @@ </argument> </arguments> </type> + <type name="Magento\Framework\GraphQl\Query\QueryComplexityLimiter"> + <arguments> + <argument name="queryDepth" xsi:type="number">20</argument> + <argument name="queryComplexity" xsi:type="number">300</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/GraphQl/etc/graphql/di.xml b/app/code/Magento/GraphQl/etc/graphql/di.xml index f4e6ca59364b..03bae5c80e12 100644 --- a/app/code/Magento/GraphQl/etc/graphql/di.xml +++ b/app/code/Magento/GraphQl/etc/graphql/di.xml @@ -27,9 +27,9 @@ </type> <type name="Magento\GraphQl\Controller\HttpRequestProcessor"> <arguments> - <argument name="graphQlHeaders" xsi:type="array"> - <item name="Content-Type" xsi:type="object">Magento\GraphQl\Controller\HttpHeaderProcessor\ContentTypeProcessor</item> - <item name="Store" xsi:type="object">Magento\GraphQl\Controller\HttpHeaderProcessor\StoreProcessor</item> + <argument name="requestValidators" xsi:type="array"> + <item name="ContentTypeValidator" xsi:type="object">Magento\GraphQl\Controller\HttpRequestValidator\ContentTypeValidator</item> + <item name="VerbValidator" xsi:type="object">Magento\GraphQl\Controller\HttpRequestValidator\HttpVerbValidator</item> </argument> </arguments> </type> diff --git a/app/code/Magento/GraphQl/etc/module.xml b/app/code/Magento/GraphQl/etc/module.xml index 4d8b2090a851..af0f5d06d3ba 100644 --- a/app/code/Magento/GraphQl/etc/module.xml +++ b/app/code/Magento/GraphQl/etc/module.xml @@ -9,7 +9,6 @@ <module name="Magento_GraphQl" > <sequence> <module name="Magento_Authorization"/> - <module name="Magento_Store"/> <module name="Magento_Eav"/> </sequence> </module> diff --git a/app/code/Magento/GraphQl/etc/schema.graphqls b/app/code/Magento/GraphQl/etc/schema.graphqls index c6651cdde0cb..ab8472aa56cf 100644 --- a/app/code/Magento/GraphQl/etc/schema.graphqls +++ b/app/code/Magento/GraphQl/etc/schema.graphqls @@ -5,7 +5,6 @@ type Query { } type Mutation { - placeholderMutation: String @doc(description: "Mutation type cannot be declared without fields. The placeholder will be removed when at least one mutation field is declared.") } input FilterTypeInput @doc(description: "FilterTypeInput specifies which action will be performed in a query ") { @@ -29,9 +28,192 @@ input FilterTypeInput @doc(description: "FilterTypeInput specifies which action type SearchResultPageInfo @doc(description: "SearchResultPageInfo provides navigation for the query response") { page_size: Int @doc(description: "Specifies the maximum number of items to return") current_page: Int @doc(description: "Specifies which page of results to return") + total_pages: Int @doc(description: "Total pages") } enum SortEnum @doc(description: "This enumeration indicates whether to return results in ascending or descending order") { ASC DESC } + +type ComplexTextValue { + html: String! @doc(description: "HTML format") +} + +type Money @doc(description: "A Money object defines a monetary value, including a numeric value and a currency code.") { + value: Float @doc(description: "A number expressing a monetary value") + currency: CurrencyEnum @doc(description: "A three-letter currency code, such as USD or EUR") +} + +enum CurrencyEnum @doc(description: "The list of available currency codes") { + AFN + ALL + AZN + DZD + AOA + ARS + AMD + AWG + AUD + BSD + BHD + BDT + BBD + BYR + BZD + BMD + BTN + BOB + BAM + BWP + BRL + GBP + BND + BGN + BUK + BIF + KHR + CAD + CVE + CZK + KYD + GQE + CLP + CNY + COP + KMF + CDF + CRC + HRK + CUP + DKK + DJF + DOP + XCD + EGP + SVC + ERN + EEK + ETB + EUR + FKP + FJD + GMD + GEK + GEL + GHS + GIP + GTQ + GNF + GYD + HTG + HNL + HKD + HUF + ISK + INR + IDR + IRR + IQD + ILS + JMD + JPY + JOD + KZT + KES + KWD + KGS + LAK + LVL + LBP + LSL + LRD + LYD + LTL + MOP + MKD + MGA + MWK + MYR + MVR + LSM + MRO + MUR + MXN + MDL + MNT + MAD + MZN + MMK + NAD + NPR + ANG + YTL + NZD + NIC + NGN + KPW + NOK + OMR + PKR + PAB + PGK + PYG + PEN + PHP + PLN + QAR + RHD + RON + RUB + RWF + SHP + STD + SAR + RSD + SCR + SLL + SGD + SKK + SBD + SOS + ZAR + KRW + LKR + SDG + SRD + SZL + SEK + CHF + SYP + TWD + TJS + TZS + THB + TOP + TTD + TND + TMM + USD + UGX + UAH + AED + UYU + UZS + VUV + VEB + VEF + VND + CHE + CHW + XOF + WST + YER + ZMK + ZWD + TRY + AZM + ROL + TRL + XPF +} diff --git a/app/code/Magento/GraphQlCache/Controller/Plugin/GraphQl.php b/app/code/Magento/GraphQlCache/Controller/Plugin/GraphQl.php new file mode 100644 index 000000000000..7c026e7d4136 --- /dev/null +++ b/app/code/Magento/GraphQlCache/Controller/Plugin/GraphQl.php @@ -0,0 +1,108 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlCache\Controller\Plugin; + +use Magento\Framework\App\FrontControllerInterface; +use Magento\Framework\App\RequestInterface; +use Magento\GraphQlCache\Model\CacheableQuery; +use Magento\Framework\App\Response\Http as HttpResponse; +use Magento\Framework\Controller\ResultInterface; +use Magento\PageCache\Model\Config; +use Magento\GraphQl\Controller\HttpRequestProcessor; +use Magento\Framework\App\Response\Http as ResponseHttp; + +/** + * Plugin for handling controller after controller tags and pre-controller validation. + */ +class GraphQl +{ + /** + * @var CacheableQuery + */ + private $cacheableQuery; + + /** + * @var Config + */ + private $config; + + /** + * @var HttpResponse + */ + private $response; + + /** + * @var HttpRequestProcessor + */ + private $requestProcessor; + + /** + * @param CacheableQuery $cacheableQuery + * @param Config $config + * @param HttpResponse $response + * @param HttpRequestProcessor $requestProcessor + */ + public function __construct( + CacheableQuery $cacheableQuery, + Config $config, + HttpResponse $response, + HttpRequestProcessor $requestProcessor + ) { + $this->cacheableQuery = $cacheableQuery; + $this->config = $config; + $this->response = $response; + $this->requestProcessor = $requestProcessor; + } + + /** + * Process graphql headers + * + * @param FrontControllerInterface $subject + * @param RequestInterface $request + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeDispatch( + FrontControllerInterface $subject, + RequestInterface $request + ) { + /** @var \Magento\Framework\App\Request\Http $request */ + $this->requestProcessor->processHeaders($request); + } + + /** + * Plugin for GraphQL after render from dispatch to set tag and cache headers + * + * @param ResultInterface $subject + * @param ResultInterface $result + * @param ResponseHttp $response + * @return ResultInterface + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterRenderResult(ResultInterface $subject, ResultInterface $result, ResponseHttp $response) + { + $sendNoCacheHeaders = false; + if ($this->config->isEnabled()) { + if ($this->cacheableQuery->shouldPopulateCacheHeadersWithTags()) { + $this->response->setPublicHeaders($this->config->getTtl()); + $this->response->setHeader('X-Magento-Tags', implode(',', $this->cacheableQuery->getCacheTags()), true); + } else { + $sendNoCacheHeaders = true; + } + } else { + $sendNoCacheHeaders = true; + } + + if ($sendNoCacheHeaders) { + $this->response->setNoCacheHeaders(); + } + + return $result; + } +} diff --git a/app/code/Magento/GraphQlCache/Model/CacheableQuery.php b/app/code/Magento/GraphQlCache/Model/CacheableQuery.php new file mode 100644 index 000000000000..451e1039eec5 --- /dev/null +++ b/app/code/Magento/GraphQlCache/Model/CacheableQuery.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlCache\Model; + +/** + * CacheableQuery should be used as a singleton for collecting cache related info and tags of all entities. + */ +class CacheableQuery +{ + /** + * @var string[] + */ + private $cacheTags = []; + + /** + * @var bool + */ + private $cacheable = true; + + /** + * Return cache tags + * + * @return array + */ + public function getCacheTags(): array + { + return $this->cacheTags; + } + + /** + * Add Cache Tags + * + * @param array $cacheTags + * @return void + */ + public function addCacheTags(array $cacheTags): void + { + $this->cacheTags = array_merge($this->cacheTags, $cacheTags); + } + + /** + * Return if its valid to cache the response + * + * @return bool + */ + public function isCacheable(): bool + { + return $this->cacheable; + } + + /** + * Set cache validity + * + * @param bool $cacheable + */ + public function setCacheValidity(bool $cacheable): void + { + $this->cacheable = $cacheable; + } + + /** + * Check if query is cacheable and we have a list of tags to populate + * + * @return bool + */ + public function shouldPopulateCacheHeadersWithTags() : bool + { + $cacheTags = $this->getCacheTags(); + $isQueryCaheable = $this->isCacheable(); + return !empty($cacheTags) && $isQueryCaheable; + } +} diff --git a/app/code/Magento/GraphQlCache/Model/CacheableQueryHandler.php b/app/code/Magento/GraphQlCache/Model/CacheableQueryHandler.php new file mode 100644 index 000000000000..7e624845f568 --- /dev/null +++ b/app/code/Magento/GraphQlCache/Model/CacheableQueryHandler.php @@ -0,0 +1,94 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlCache\Model; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\App\RequestInterface; +use Magento\GraphQlCache\Model\Resolver\IdentityPool; + +/** + * Handler of collecting tagging on cache. + * + * This class would be used to collect tags after each operation where we need to collect tags + * usually after data is fetched or resolved. + */ +class CacheableQueryHandler +{ + /** + * @var CacheableQuery + */ + private $cacheableQuery; + + /** + * @var RequestInterface + */ + private $request; + + /** + * @var IdentityPool + */ + private $identityPool; + + /** + * @param CacheableQuery $cacheableQuery + * @param RequestInterface $request + * @param IdentityPool $identityPool + */ + public function __construct( + CacheableQuery $cacheableQuery, + RequestInterface $request, + IdentityPool $identityPool + ) { + $this->cacheableQuery = $cacheableQuery; + $this->request = $request; + $this->identityPool = $identityPool; + } + + /** + * Set cache validity to the cacheableQuery after resolving any resolver or evaluating a promise in a query + * + * @param array $resolvedValue + * @param Field $field + * @return void + */ + public function handleCacheFromResolverResponse(array $resolvedValue, Field $field) : void + { + $cache = $field->getCache(); + $cacheIdentityClass = $cache['cacheIdentity'] ?? ''; + $cacheable = $cache['cacheable'] ?? true; + $cacheTag = $cache['cacheTag'] ?? null; + + $cacheTags = []; + if ($cacheTag && $this->request->isGet()) { + if (!empty($cacheIdentityClass)) { + $cacheIdentity = $this->identityPool->get($cacheIdentityClass); + $cacheTagIds = $cacheIdentity->getIdentities($resolvedValue); + if (!empty($cacheTagIds)) { + $cacheTags[] = $cacheTag; + foreach ($cacheTagIds as $cacheTagId) { + $cacheTags[] = $cacheTag . '_' . $cacheTagId; + } + } + } + $this->cacheableQuery->addCacheTags($cacheTags); + } + $this->setCacheValidity($cacheable); + } + + /** + * Set cache validity for the graphql request + * + * @param bool $isValid + * @return void + */ + private function setCacheValidity(bool $isValid): void + { + $cacheValidity = $this->cacheableQuery->isCacheable() && $isValid; + $this->cacheableQuery->setCacheValidity($cacheValidity); + } +} diff --git a/app/code/Magento/GraphQlCache/Model/Plugin/App/PageCache/Identifier.php b/app/code/Magento/GraphQlCache/Model/Plugin/App/PageCache/Identifier.php new file mode 100644 index 000000000000..e02a51d2c1ca --- /dev/null +++ b/app/code/Magento/GraphQlCache/Model/Plugin/App/PageCache/Identifier.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlCache\Model\Plugin\App\PageCache; + +/** + * Handles unique identifier for graphql query + */ +class Identifier +{ + /** + * @var \Magento\Framework\App\Request\Http + */ + private $request; + + /** + * @var \Magento\Framework\App\Http\Context + */ + private $context; + + /** + * @var \Magento\Framework\Serialize\Serializer\Json + */ + private $serializer; + + /** + * @var \Magento\PageCache\Model\Config + */ + private $config; + + /** + * @param \Magento\Framework\App\Request\Http $request + * @param \Magento\Framework\App\Http\Context $context + * @param \Magento\Framework\Serialize\Serializer\Json $serializer + * @param \Magento\PageCache\Model\Config $config + */ + public function __construct( + \Magento\Framework\App\Request\Http $request, + \Magento\Framework\App\Http\Context $context, + \Magento\Framework\Serialize\Serializer\Json $serializer, + \Magento\PageCache\Model\Config $config + ) { + $this->request = $request; + $this->context = $context; + $this->serializer = $serializer; + $this->config = $config; + } + + /** + * Add/Override a unique key identifier for graphql specific query and variables that skips X-Magento-Vary cookie + * + * @param \Magento\Framework\App\PageCache\Identifier $identifier + * @param string $result + * @return string + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGetValue(\Magento\Framework\App\PageCache\Identifier $identifier, string $result) : string + { + if ($this->config->isEnabled()) { + $data = [ + $this->request->isSecure(), + $this->request->getUriString(), + $this->context->getVaryString() + ]; + $result = sha1($this->serializer->serialize($data)); + } + return $result; + } +} diff --git a/app/code/Magento/GraphQlCache/Model/Plugin/Query/Resolver.php b/app/code/Magento/GraphQlCache/Model/Plugin/Query/Resolver.php new file mode 100644 index 000000000000..54cb5559923a --- /dev/null +++ b/app/code/Magento/GraphQlCache/Model/Plugin/Query/Resolver.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlCache\Model\Plugin\Query; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\GraphQl\Model\Query\Resolver\Context; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\GraphQlCache\Model\CacheableQueryHandler; + +/** + * Plugin to handle cache validation that can be done after each resolver + */ +class Resolver +{ + /** + * @var CacheableQueryHandler + */ + private $cacheableQueryHandler; + + /** + * @param CacheableQueryHandler $cacheableQueryHandler + */ + public function __construct( + CacheableQueryHandler $cacheableQueryHandler + ) { + $this->cacheableQueryHandler = $cacheableQueryHandler; + } + + /** + * Set cache validity to the cacheableQuery after resolving any resolver in a query + * + * @param ResolverInterface $subject + * @param mixed|Value $resolvedValue + * @param Field $field + * @param Context $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * @return mixed + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterResolve( + ResolverInterface $subject, + $resolvedValue, + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + /** Only if array @see \Magento\Framework\GraphQl\Query\Resolver\Value */ + if (is_array($resolvedValue) && !empty($field->getCache())) { + $this->cacheableQueryHandler->handleCacheFromResolverResponse($resolvedValue, $field); + } elseif ($resolvedValue instanceof \Magento\Framework\GraphQl\Query\Resolver\Value) { + $resolvedValue->then(function () use ($resolvedValue, $field) { + if (is_array($resolvedValue->promise->result) && $field) { + $this->cacheableQueryHandler->handleCacheFromResolverResponse( + $resolvedValue->promise->result, + $field + ); + } + }); + } + return $resolvedValue; + } +} diff --git a/app/code/Magento/GraphQlCache/Model/Resolver/IdentityPool.php b/app/code/Magento/GraphQlCache/Model/Resolver/IdentityPool.php new file mode 100644 index 000000000000..00ef8762c28e --- /dev/null +++ b/app/code/Magento/GraphQlCache/Model/Resolver/IdentityPool.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlCache\Model\Resolver; + +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface; + +/** + * Pool of IdentityInterface objects + */ +class IdentityPool +{ + /** + * @var IdentityInterface[] + */ + private $identityInstances = []; + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @param ObjectManagerInterface $objectManager + */ + public function __construct(ObjectManagerInterface $objectManager) + { + $this->objectManager = $objectManager; + } + + /** + * Get an identity resolver by class name + * + * @param string $identityClass + * @return IdentityInterface + */ + public function get(string $identityClass): IdentityInterface + { + if (!isset($this->identityInstances[$identityClass])) { + $this->identityInstances[$identityClass] = $this->objectManager->create($identityClass); + } + return $this->identityInstances[$identityClass]; + } +} diff --git a/app/code/Magento/GraphQlCache/README.md b/app/code/Magento/GraphQlCache/README.md new file mode 100644 index 000000000000..fd2f19f957c5 --- /dev/null +++ b/app/code/Magento/GraphQlCache/README.md @@ -0,0 +1,4 @@ +# GraphQl Cache + +**GraphQL Cache** provides the ability to cache GraphQL queries. +This module allows Magento's built-in cache or Varnish as the application for serving the Full Page Cache to the front end. diff --git a/app/code/Magento/GraphQlCache/Test/Unit/Model/CacheableQueryHandlerTest.php b/app/code/Magento/GraphQlCache/Test/Unit/Model/CacheableQueryHandlerTest.php new file mode 100644 index 000000000000..9c1be8992821 --- /dev/null +++ b/app/code/Magento/GraphQlCache/Test/Unit/Model/CacheableQueryHandlerTest.php @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlCache\Test\Unit\Model; + +use Magento\Framework\App\Request\Http; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface; +use Magento\GraphQlCache\Model\CacheableQueryHandler; +use Magento\GraphQlCache\Model\Resolver\IdentityPool; +use Magento\GraphQlCache\Model\CacheableQuery; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use PHPUnit\Framework\TestCase; + +/** + * Test CacheableQueryHandler + */ +class CacheableQueryHandlerTest extends TestCase +{ + + private $cacheableQueryHandler; + + private $cacheableQueryMock; + + private $requestMock; + + private $identityPoolMock; + + protected function setup(): void + { + $objectManager = new ObjectManager($this); + $this->cacheableQueryMock = $this->createMock(CacheableQuery::class); + $this->requestMock = $this->createMock(Http::class); + $this->identityPoolMock = $this->createMock(IdentityPool::class); + $this->cacheableQueryHandler = $objectManager->getObject( + CacheableQueryHandler::class, + [ + 'cacheableQuery' => $this->cacheableQueryMock, + 'request' => $this->requestMock, + 'identityPool' => $this->identityPoolMock + ] + ); + } + + /** + * @param array $resolvedData + * @param array $identities + * @dataProvider resolvedDataProvider + */ + public function testhandleCacheFromResolverResponse( + array $resolvedData, + array $identities, + array $expectedCacheTags + ): void { + $cacheData = [ + 'cacheIdentity' => IdentityInterface::class, + 'cacheTag' => 'cat_p' + ]; + $fieldMock = $this->createMock(Field::class); + $mockIdentity = $this->getMockBuilder($cacheData['cacheIdentity']) + ->setMethods(['getIdentities']) + ->getMockForAbstractClass(); + + $this->requestMock->expects($this->once())->method('isGet')->willReturn(true); + $this->identityPoolMock->expects($this->once())->method('get')->willReturn($mockIdentity); + $fieldMock->expects($this->once())->method('getCache')->willReturn($cacheData); + $mockIdentity->expects($this->once()) + ->method('getIdentities') + ->with($resolvedData) + ->willReturn($identities); + $this->cacheableQueryMock->expects($this->once())->method('addCacheTags')->with($expectedCacheTags); + $this->cacheableQueryMock->expects($this->once())->method('isCacheable')->willReturn(true); + $this->cacheableQueryMock->expects($this->once())->method('setCacheValidity')->with(true); + + $this->cacheableQueryHandler->handleCacheFromResolverResponse($resolvedData, $fieldMock); + } + + /** + * @return array + */ + public function resolvedDataProvider(): array + { + return [ + [ + "resolvedData" => [ + "id" => 10, + "name" => "TesName", + "sku" => "TestSku" + ], + "identities" => [10], + "expectedCacheTags" => ["cat_p", "cat_p_10"] + ] + ]; + } +} diff --git a/app/code/Magento/GraphQlCache/composer.json b/app/code/Magento/GraphQlCache/composer.json new file mode 100644 index 000000000000..436ae95da40f --- /dev/null +++ b/app/code/Magento/GraphQlCache/composer.json @@ -0,0 +1,23 @@ +{ + "name": "magento/module-graph-ql-cache", + "description": "N/A", + "type": "magento2-module", + "require": { + "php": "~7.1.3||~7.2.0", + "magento/framework": "*", + "magento/module-page-cache": "*", + "magento/module-graph-ql": "*" + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\GraphQlCache\\": "" + } + } +} diff --git a/app/code/Magento/GraphQlCache/etc/graphql/di.xml b/app/code/Magento/GraphQlCache/etc/graphql/di.xml new file mode 100644 index 000000000000..5dd8c816ce92 --- /dev/null +++ b/app/code/Magento/GraphQlCache/etc/graphql/di.xml @@ -0,0 +1,26 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Framework\App\FrontControllerInterface"> + <plugin name="graphql-dispatch-plugin" type="Magento\GraphQlCache\Controller\Plugin\GraphQl"/> + <plugin name="front-controller-builtin-cache" type="Magento\PageCache\Model\App\FrontController\BuiltinPlugin"/> + <plugin name="front-controller-varnish-cache" type="Magento\PageCache\Model\App\FrontController\VarnishPlugin"/> + </type> + <type name="Magento\Framework\GraphQl\Query\ResolverInterface"> + <plugin name="cache" type="Magento\GraphQlCache\Model\Plugin\Query\Resolver"/> + </type> + <type name="Magento\Framework\App\PageCache\Identifier"> + <plugin name="core-app-area-design-exception-plugin" + type="Magento\GraphQlCache\Model\Plugin\App\PageCache\Identifier" sortOrder="1"/> + </type> + <type name="Magento\Framework\Controller\ResultInterface"> + <plugin name="graphql-result-plugin" type="Magento\GraphQlCache\Controller\Plugin\GraphQl"/> + <plugin name="result-builtin-cache" type="Magento\PageCache\Model\Controller\Result\BuiltinPlugin"/> + <plugin name="result-varnish-cache" type="Magento\PageCache\Model\Controller\Result\VarnishPlugin"/> + </type> +</config> diff --git a/app/code/Magento/GraphQlCache/etc/module.xml b/app/code/Magento/GraphQlCache/etc/module.xml new file mode 100644 index 000000000000..3cbd4d8f0cb4 --- /dev/null +++ b/app/code/Magento/GraphQlCache/etc/module.xml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_GraphQlCache"> + <sequence> + <module name="Magento_PageCache"/> + <module name="Magento_GraphQl"/> + </sequence> + </module> +</config> diff --git a/app/code/Magento/GraphQlCache/registration.php b/app/code/Magento/GraphQlCache/registration.php new file mode 100644 index 000000000000..2dfe717003a0 --- /dev/null +++ b/app/code/Magento/GraphQlCache/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_GraphQlCache', __DIR__); diff --git a/app/code/Magento/GroupedCatalogInventory/LICENSE.txt b/app/code/Magento/GroupedCatalogInventory/LICENSE.txt new file mode 100644 index 000000000000..49525fd99da9 --- /dev/null +++ b/app/code/Magento/GroupedCatalogInventory/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/GroupedCatalogInventory/LICENSE_AFL.txt b/app/code/Magento/GroupedCatalogInventory/LICENSE_AFL.txt new file mode 100644 index 000000000000..f39d641b18a1 --- /dev/null +++ b/app/code/Magento/GroupedCatalogInventory/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/GroupedCatalogInventory/Plugin/OutOfStockFilter.php b/app/code/Magento/GroupedCatalogInventory/Plugin/OutOfStockFilter.php new file mode 100644 index 000000000000..599f40deddde --- /dev/null +++ b/app/code/Magento/GroupedCatalogInventory/Plugin/OutOfStockFilter.php @@ -0,0 +1,88 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\GroupedCatalogInventory\Plugin; + +use Magento\Catalog\Model\Product; +use Magento\CatalogInventory\Api\Data\StockStatusInterface; +use Magento\CatalogInventory\Api\StockStatusCriteriaInterfaceFactory; +use Magento\CatalogInventory\Api\StockStatusRepositoryInterface; +use Magento\GroupedProduct\Model\Product\Type\Grouped; + +/** + * Removes out of stock products from cart candidates when appropriate + */ +class OutOfStockFilter +{ + /** + * @var StockStatusRepositoryInterface + */ + private $stockStatusRepository; + + /** + * @var StockStatusCriteriaInterfaceFactory + */ + private $criteriaInterfaceFactory; + + /** + * @param StockStatusRepositoryInterface $stockStatusRepository + * @param StockStatusCriteriaInterfaceFactory $criteriaInterfaceFactory + */ + public function __construct( + StockStatusRepositoryInterface $stockStatusRepository, + StockStatusCriteriaInterfaceFactory $criteriaInterfaceFactory + ) { + $this->stockStatusRepository = $stockStatusRepository; + $this->criteriaInterfaceFactory = $criteriaInterfaceFactory; + } + + /** + * Removes out of stock products for requests that don't specify the super group + * + * @param Grouped $subject + * @param array|string $result + * @param \Magento\Framework\DataObject $buyRequest + * @return string|array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterPrepareForCartAdvanced( + Grouped $subject, + $result, + \Magento\Framework\DataObject $buyRequest + ) { + if (!is_array($result) && $result instanceof Product) { + $result = [$result]; + } + + // Only remove out-of-stock products if no quantities were specified + if (is_array($result) && !empty($result) && !$buyRequest->getData('super_group')) { + $productIds = []; + $productIdMap = []; + + foreach ($result as $index => $cartItem) { + $productIds[] = $cartItem->getId(); + $productIdMap[$cartItem->getId()] = $index; + } + + $criteria = $this->criteriaInterfaceFactory->create(); + $criteria->setProductsFilter($productIds); + + $stockStatusCollection = $this->stockStatusRepository->getList($criteria); + foreach ($stockStatusCollection->getItems() as $status) { + /** @var $status StockStatusInterface */ + if ($status->getStockStatus() == StockStatusInterface::STATUS_OUT_OF_STOCK) { + unset($result[$productIdMap[$status->getProductId()]]); + } + } + + unset($productIdMap); + } + + return $result; + } +} diff --git a/app/code/Magento/GroupedCatalogInventory/README.md b/app/code/Magento/GroupedCatalogInventory/README.md new file mode 100644 index 000000000000..c229c7e9919a --- /dev/null +++ b/app/code/Magento/GroupedCatalogInventory/README.md @@ -0,0 +1 @@ +Magento_GroupedCatalogInventory contains behavior related to the inventory status of items within grouped products. diff --git a/app/code/Magento/GroupedCatalogInventory/Test/Unit/Plugin/OutOfStockFilterTest.php b/app/code/Magento/GroupedCatalogInventory/Test/Unit/Plugin/OutOfStockFilterTest.php new file mode 100644 index 000000000000..07877938e36a --- /dev/null +++ b/app/code/Magento/GroupedCatalogInventory/Test/Unit/Plugin/OutOfStockFilterTest.php @@ -0,0 +1,221 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\GroupedCatalogInventory\Test\Unit\Plugin; + +use Magento\Catalog\Model\Product; +use Magento\CatalogInventory\Api\Data\StockStatusCollectionInterface; +use Magento\CatalogInventory\Api\Data\StockStatusInterface; +use Magento\CatalogInventory\Api\StockStatusCriteriaInterface; +use Magento\CatalogInventory\Api\StockStatusCriteriaInterfaceFactory; +use Magento\CatalogInventory\Api\StockStatusRepositoryInterface; +use Magento\Framework\DataObject; +use Magento\GroupedCatalogInventory\Plugin\OutOfStockFilter; +use Magento\GroupedProduct\Model\Product\Type\Grouped; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class OutOfStockFilterTest extends TestCase +{ + /** + * @var MockObject + */ + private $subjectMock; + + /** + * @var MockObject + */ + private $stockStatusRepositoryMock; + + /** + * @var MockObject + */ + private $searchCriteriaMock; + + /** + * @var MockObject + */ + private $searchCriteriaFactoryMock; + + /** + * @var MockObject + */ + private $stockStatusCollectionMock; + + /** + * @param $nonArrayResult + * @dataProvider nonArrayResults + */ + public function testFilterOnlyProcessesArray($nonArrayResult) + { + $this->searchCriteriaMock->expects($this->never())->method('setProductsFilter'); + $this->stockStatusRepositoryMock->expects($this->never())->method('getList'); + + $plugin = $this->getPluginInstance(); + + $result = $plugin->afterPrepareForCartAdvanced( + $this->subjectMock, + $nonArrayResult, + new DataObject() + ); + + $this->assertSame($nonArrayResult, $result); + } + + public function testFilterIgnoresResultIfSuperGroupIsPresent() + { + $this->searchCriteriaMock->expects($this->never())->method('setProductsFilter'); + $this->stockStatusRepositoryMock->expects($this->never())->method('getList'); + + $plugin = $this->getPluginInstance(); + + $product = $this->createProductMock(); + + $result = $plugin->afterPrepareForCartAdvanced( + $this->subjectMock, + [$product], + new DataObject(['super_group' => [123 => '1']]) + ); + + $this->assertSame([$product], $result); + } + + /** + * @param $originalResult + * @param $stockStatusCollection + * @param $expectedResult + * @dataProvider outOfStockProductData + */ + public function testFilterRemovesOutOfStockProductsWhenSuperGroupIsNotPresent( + $originalResult, + $stockStatusCollection, + $expectedResult + ) { + $this->stockStatusRepositoryMock + ->expects($this->once()) + ->method('getList') + ->with($this->searchCriteriaMock) + ->willReturn($stockStatusCollection); + + $plugin = $this->getPluginInstance(); + + $result = $plugin->afterPrepareForCartAdvanced( + $this->subjectMock, + $originalResult, + new DataObject() + ); + + $this->assertSame($expectedResult, $result); + } + + public function outOfStockProductData() + { + $product1 = $this->createProductMock(); + $product1->method('getId')->willReturn(123); + + $product2 = $this->createProductMock(); + $product2->method('getId')->willReturn(321); + + return [ + [[$product1, $product2], $this->createStatusResult([123 => false, 321 => true]), [1 => $product2]], + [[$product1], $this->createStatusResult([123 => true]), [0 => $product1]], + [$product1, $this->createStatusResult([123 => true]), [0 => $product1]] + ]; + } + + public function nonArrayResults() + { + return [ + [123], + ['abc'], + [new \stdClass()] + ]; + } + + protected function setUp() + { + $this->subjectMock = $this->getMockBuilder(Grouped::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->stockStatusRepositoryMock = $this->getMockBuilder(StockStatusRepositoryInterface::class) + ->getMock(); + + $this->searchCriteriaFactoryMock = $this->getMockBuilder(StockStatusCriteriaInterfaceFactory::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->searchCriteriaMock = $this->getMockBuilder(StockStatusCriteriaInterface::class) + ->getMock(); + + $this->stockStatusCollectionMock = $this->getMockBuilder(StockStatusCollectionInterface::class) + ->getMock(); + + $this->searchCriteriaFactoryMock + ->expects($this->any()) + ->method('create') + ->willReturn($this->searchCriteriaMock); + } + + private function createProductMock() + { + return $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->getMock(); + } + + /** + * @return OutOfStockFilter + */ + private function getPluginInstance() + { + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + /** @var OutOfStockFilter $filter */ + $filter = $objectManager->getObject(OutOfStockFilter::class, [ + 'stockStatusRepository' => $this->stockStatusRepositoryMock, + 'criteriaInterfaceFactory' => $this->searchCriteriaFactoryMock + ]); + + return $filter; + } + + private function createStatusResult(array $productStatuses) + { + $result = []; + + foreach ($productStatuses as $productId => $status) { + $mock = $this->getMockBuilder(StockStatusInterface::class) + ->getMock(); + + $mock->expects($this->any()) + ->method('getProductId') + ->willReturn($productId); + + $mock->expects($this->any()) + ->method('getStockStatus') + ->willReturn( + $status + ? StockStatusInterface::STATUS_IN_STOCK + : StockStatusInterface::STATUS_OUT_OF_STOCK + ); + + $result[] = $mock; + } + + $stockStatusCollection = $this->getMockBuilder(StockStatusCollectionInterface::class) + ->getMock(); + + $stockStatusCollection + ->expects($this->once()) + ->method('getItems') + ->willReturn($result); + + return $stockStatusCollection; + } +} diff --git a/app/code/Magento/GroupedCatalogInventory/composer.json b/app/code/Magento/GroupedCatalogInventory/composer.json new file mode 100644 index 000000000000..c9946bc3f36a --- /dev/null +++ b/app/code/Magento/GroupedCatalogInventory/composer.json @@ -0,0 +1,27 @@ +{ + "name": "magento/module-grouped-catalog-inventory", + "description": "N/A", + "config": { + "sort-packages": true + }, + "require": { + "php": "~7.1.3||~7.2.0", + "magento/framework": "*", + "magento/module-catalog": "*", + "magento/module-catalog-inventory": "*", + "magento/module-grouped-product": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\GroupedCatalogInventory\\": "" + } + } +} diff --git a/app/code/Magento/GroupedCatalogInventory/etc/di.xml b/app/code/Magento/GroupedCatalogInventory/etc/di.xml new file mode 100644 index 000000000000..f38a9e845a15 --- /dev/null +++ b/app/code/Magento/GroupedCatalogInventory/etc/di.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:ObjectManager/etc/config.xsd"> + <type name="Magento\GroupedProduct\Model\Product\Type\Grouped"> + <plugin name="outOfStockFilter" type="Magento\GroupedCatalogInventory\Plugin\OutOfStockFilter" /> + </type> +</config> diff --git a/app/code/Magento/GroupedCatalogInventory/etc/module.xml b/app/code/Magento/GroupedCatalogInventory/etc/module.xml new file mode 100644 index 000000000000..246f10ff6793 --- /dev/null +++ b/app/code/Magento/GroupedCatalogInventory/etc/module.xml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_GroupedCatalogInventory" > + <sequence> + <module name="Magento_Catalog"/> + <module name="Magento_GroupedProduct"/> + </sequence> + </module> +</config> diff --git a/app/code/Magento/GroupedCatalogInventory/registration.php b/app/code/Magento/GroupedCatalogInventory/registration.php new file mode 100644 index 000000000000..8899a48edf6d --- /dev/null +++ b/app/code/Magento/GroupedCatalogInventory/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_GroupedCatalogInventory', __DIR__); diff --git a/app/code/Magento/GroupedImportExport/etc/di.xml b/app/code/Magento/GroupedImportExport/etc/di.xml index 38030b3ec94e..25fd3b569751 100644 --- a/app/code/Magento/GroupedImportExport/etc/di.xml +++ b/app/code/Magento/GroupedImportExport/etc/di.xml @@ -9,7 +9,7 @@ <type name="Magento\CatalogImportExport\Model\Export\RowCustomizer\Composite"> <arguments> <argument name="customizers" xsi:type="array"> - <item name="gropedProduct" xsi:type="string">Magento\GroupedImportExport\Model\Export\RowCustomizer</item> + <item name="groupedProduct" xsi:type="string">Magento\GroupedImportExport\Model\Export\RowCustomizer</item> </argument> </arguments> </type> diff --git a/app/code/Magento/GroupedProduct/Model/Product/Initialization/Helper/ProductLinks/Plugin/Grouped.php b/app/code/Magento/GroupedProduct/Model/Product/Initialization/Helper/ProductLinks/Plugin/Grouped.php index 7b9523015c88..a1dc90336427 100644 --- a/app/code/Magento/GroupedProduct/Model/Product/Initialization/Helper/ProductLinks/Plugin/Grouped.php +++ b/app/code/Magento/GroupedProduct/Model/Product/Initialization/Helper/ProductLinks/Plugin/Grouped.php @@ -8,6 +8,7 @@ use Magento\Catalog\Api\Data\ProductLinkExtensionFactory; use Magento\Catalog\Api\Data\ProductLinkInterfaceFactory; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Api\SimpleDataObjectConverter; use Magento\Framework\Exception\NoSuchEntityException; use Magento\GroupedProduct\Model\Product\Type\Grouped as TypeGrouped; @@ -111,7 +112,7 @@ public function beforeInitializeLinks( foreach ($linkRaw['custom_attributes'] as $option) { $name = $option['attribute_code']; $value = $option['value']; - $setterName = 'set' . ucfirst($name); + $setterName = 'set' . SimpleDataObjectConverter::snakeCaseToUpperCamelCase($name); // Check if setter exists if (method_exists($productLinkExtension, $setterName)) { call_user_func([$productLinkExtension, $setterName], $value); diff --git a/app/code/Magento/GroupedProduct/Model/Product/Type/Grouped.php b/app/code/Magento/GroupedProduct/Model/Product/Type/Grouped.php index e1c97c8912cf..187fd27fa055 100644 --- a/app/code/Magento/GroupedProduct/Model/Product/Type/Grouped.php +++ b/app/code/Magento/GroupedProduct/Model/Product/Type/Grouped.php @@ -10,6 +10,8 @@ use Magento\Catalog\Api\ProductRepositoryInterface; /** + * Grouped product type model + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api * @since 100.0.2 @@ -208,7 +210,7 @@ public function getAssociatedProducts($product) $collection = $this->getAssociatedProductCollection( $product )->addAttributeToSelect( - ['name', 'price', 'special_price', 'special_from_date', 'special_to_date', 'tax_class_id'] + ['name', 'price', 'special_price', 'special_from_date', 'special_to_date', 'tax_class_id', 'image'] )->addFilterByRequiredOptions()->setPositionOrder()->addStoreFilter( $this->getStoreFilter($product) )->addAttributeToFilter( @@ -226,13 +228,15 @@ public function getAssociatedProducts($product) } /** + * Flush Associated Products Cache + * * @param \Magento\Catalog\Model\Product $product * @return \Magento\Catalog\Model\Product * @since 100.1.0 */ public function flushAssociatedProductsCache($product) { - return $product->unsData($this->_keyAssociatedProducts); + return $product->unsetData($this->_keyAssociatedProducts); } /** @@ -323,6 +327,8 @@ public function getAssociatedProductCollection($product) } /** + * Returns product info + * * @param \Magento\Framework\DataObject $buyRequest * @param \Magento\Catalog\Model\Product $product * @param bool $isStrictProcessMode @@ -341,7 +347,7 @@ protected function getProductInfo(\Magento\Framework\DataObject $buyRequest, $pr if ($isStrictProcessMode && !$subProduct->getQty()) { return __('Please specify the quantity of product(s).')->render(); } - $productsInfo[$subProduct->getId()] = intval($subProduct->getQty()); + $productsInfo[$subProduct->getId()] = $subProduct->isSalable() ? (float)$subProduct->getQty() : 0; } } @@ -350,6 +356,7 @@ protected function getProductInfo(\Magento\Framework\DataObject $buyRequest, $pr /** * Prepare product and its configuration to be added to some products list. + * * Perform standard preparation process and add logic specific to Grouped product type. * * @param \Magento\Framework\DataObject $buyRequest @@ -423,6 +430,7 @@ protected function _prepareProduct(\Magento\Framework\DataObject $buyRequest, $p /** * Retrieve products divided into groups required to purchase + * * At least one product in each group has to be purchased * * @param \Magento\Catalog\Model\Product $product @@ -436,8 +444,8 @@ public function getProductsToPurchaseByReqGroups($product) /** * Prepare selected qty for grouped product's options * - * @param \Magento\Catalog\Model\Product $product - * @param \Magento\Framework\DataObject $buyRequest + * @param \Magento\Catalog\Model\Product $product + * @param \Magento\Framework\DataObject $buyRequest * @return array * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -467,25 +475,30 @@ public function hasWeight() * @param \Magento\Catalog\Model\Product $product * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * phpcs:disable Magento2.CodeAnalysis.EmptyBlock */ public function deleteTypeSpecificData(\Magento\Catalog\Model\Product $product) { } + //phpcs:enable /** - * {@inheritdoc} + * @inheritdoc */ public function beforeSave($product) { //clear cached associated links $product->unsetData($this->_keyAssociatedProducts); if ($product->hasData('product_options') && !empty($product->getData('product_options'))) { + //phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception('Custom options for grouped product type are not supported'); } return parent::beforeSave($product); } /** + * Returns msrp for children products + * * @param \Magento\Catalog\Model\Product $product * @return int */ 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 8d1548036cd3..cbe1ef26c54b 100644 --- a/app/code/Magento/GroupedProduct/Model/ResourceModel/Product/Type/Grouped/AssociatedProductsCollection.php +++ b/app/code/Magento/GroupedProduct/Model/ResourceModel/Product/Type/Grouped/AssociatedProductsCollection.php @@ -8,7 +8,10 @@ namespace Magento\GroupedProduct\Model\ResourceModel\Product\Type\Grouped; /** + * Associated products collection. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class AssociatedProductsCollection extends \Magento\Catalog\Model\ResourceModel\Product\Link\Product\Collection { diff --git a/app/code/Magento/GroupedProduct/Pricing/Price/ConfiguredRegularPrice.php b/app/code/Magento/GroupedProduct/Pricing/Price/ConfiguredRegularPrice.php index 8b29e82d93a4..6a6b35557100 100644 --- a/app/code/Magento/GroupedProduct/Pricing/Price/ConfiguredRegularPrice.php +++ b/app/code/Magento/GroupedProduct/Pricing/Price/ConfiguredRegularPrice.php @@ -82,7 +82,7 @@ public function getValue() if ($this->value === null) { $price = $this->product->getPrice(); $priceInCurrentCurrency = $this->priceCurrency->convertAndRound($price); - $this->value = $priceInCurrentCurrency ? floatval($priceInCurrentCurrency) : false; + $this->value = $priceInCurrentCurrency ? (float)$priceInCurrentCurrency : false; } return $this->value; 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 000000000000..5937267b4a61 --- /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 4d979953a934..ba3703e7b0ed 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Data/GroupedProductData.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Data/GroupedProductData.xml @@ -18,7 +18,7 @@ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> </entity> <entity name="ApiGroupedProduct" type="product3"> - <data key="sku" unique="suffix">api-grouped-product</data> + <data key="sku" unique="suffix">apiGroupedProduct</data> <data key="type_id">grouped</data> <data key="attribute_set_id">4</data> <data key="name" unique="suffix">Api Grouped Product</data> @@ -28,4 +28,16 @@ <requiredEntity type="custom_attribute_array">ApiProductDescription</requiredEntity> <requiredEntity type="custom_attribute_array">ApiProductShortDescription</requiredEntity> </entity> + <entity name="ApiGroupedProduct2" type="product3"> + <data key="sku" unique="suffix">apiGroupedProduct</data> + <data key="type_id">grouped</data> + <data key="attribute_set_id">4</data> + <data key="name" unique="suffix">Api Grouped Product</data> + <data key="status">1</data> + <data key="urlKey" unique="suffix">api-grouped-product</data> + <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">ApiProductDescription</requiredEntity> + <requiredEntity type="custom_attribute_array">ApiProductShortDescription</requiredEntity> + </entity> </entities> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Section/AdminProductDropdownOrderSection.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Section/AdminProductDropdownOrderSection.xml new file mode 100644 index 000000000000..3efbabd34c1a --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Section/AdminProductDropdownOrderSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminProductDropdownOrderSection"> + <element name="groupedProduct" type="text" selector="//li[not(preceding-sibling::li[span[@title='Virtual Product']]) and not(preceding-sibling::li[span[@title='Bundle Product']]) and not(preceding-sibling::li[span[@title='Downloadable Product']]) and not(following-sibling::li[span[@title='Simple Product']]) and not(following-sibling::li[span[@title='Configurable Product']])]/span[@title='Grouped Product']"/> + </section> +</sections> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Section/AdminProductFormGroupedProductsSection.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Section/AdminProductFormGroupedProductsSection.xml index 64dcd9566d89..547c856d144c 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Section/AdminProductFormGroupedProductsSection.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Section/AdminProductFormGroupedProductsSection.xml @@ -11,5 +11,9 @@ <section name="AdminProductFormGroupedProductsSection"> <element name="toggleGroupedProduct" type="button" selector="div[data-index=grouped] .admin__collapsible-title"/> <element name="addProductsToGroup" type="button" selector="button[data-index='grouped_products_button']" timeout="30"/> + <element name="nextActionButton" type="button" selector="//*[@data-index='grouped']//*[@class='action-next']"/> + <element name="previousActionButton" type="button" selector="//*[@data-index='grouped']//*[@class='action-previous']"/> + <element name="positionProduct" type="input" selector="//tbody/tr[{{arg}}][contains(@class,'data-row')]/td[10]//input[@class='position-widget-input']" parameterized="true"/> + <element name="nameProductFromGrid" type="text" selector="//tbody/tr[{{arg}}][contains(@class,'data-row')]/td[4]//*[@class='admin__field-control']//span" parameterized="true"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAddDefaultVideoGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAddDefaultVideoGroupedProductTest.xml index 8634fc3f6f9d..c3a95bbef3aa 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAddDefaultVideoGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAddDefaultVideoGroupedProductTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminAddDefaultVideoGroupedProductTest" extends="AdminAddDefaultVideoSimpleProductTest"> <annotations> <features value="GroupedProduct"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminDeleteGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminDeleteGroupedProductTest.xml new file mode 100644 index 000000000000..966e24851395 --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminDeleteGroupedProductTest.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminDeleteGroupedProductTest"> + <annotations> + <features value="GroupedProduct"/> + <title value="Delete Grouped Product"/> + <description value="Admin should be able to delete a grouped product"/> + <testCaseId value="MC-11019"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="ApiProductWithDescription" stepKey="createSimpleProduct"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="ApiGroupedProduct2" stepKey="createGroupedProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="OneSimpleProductLink" stepKey="addProductOne"> + <requiredEntity createDataKey="createGroupedProduct"/> + <requiredEntity createDataKey="createSimpleProduct"/> + </createData> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteGroupedProductFilteredBySkuAndName"> + <argument name="product" value="$$createGroupedProduct$$"/> + </actionGroup> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="A total of 1 record(s) have been deleted." stepKey="deleteMessage"/> + <!--Verify product on Product Page --> + <amOnPage url="{{StorefrontProductPage.url($$createGroupedProduct.name$$)}}" stepKey="amOnGroupedProductPage"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> + <!--Search for the product by sku--> + <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createGroupedProduct.sku$$" stepKey="fillSearchBarByProductSku"/> + <waitForPageLoad stepKey="waitForSearchButton"/> + <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForSearchResults"/> + <!-- Should not see any search results --> + <dontSee userInput="$$createGroupedProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> + <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> + <!-- Go to the category page that we created in the before block --> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> + <!-- Should not see the product --> + <dontSee userInput="$$createGroupedProduct.name$$" selector="{{StorefrontCategoryMainSection.productsList}}" stepKey="dontSeeProductInCategory"/> + <see selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" userInput="We can't find products matching the selection." stepKey="seeEmptyProductMessage"/> + </test> +</tests> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminGroupedProductsListTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminGroupedProductsListTest.xml new file mode 100644 index 000000000000..b59cf1e2175d --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminGroupedProductsListTest.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="AdminGroupedProductsAreListedWhenOutOfStock"> + <annotations> + <features value="GroupedProduct"/> + <stories value="MAGETWO-93181: Grouped product doesn't take care about his Linked Products when SalableQuantity < ProductLink.ExtensionAttributes.Qty after Source Deduction"/> + <title value="Products in group should show in admin list even when they are out of stock"/> + <description value="Products in group should show in admin list even when they are out of stock"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-93181"/> + <group value="GroupedProduct"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category1"/> + <createData entity="SimpleProduct" stepKey="simpleProduct1"> + <requiredEntity createDataKey="category1"/> + </createData> + <!--Out of Stock--> + <createData entity="SimpleProduct4" stepKey="simpleProduct2"> + <requiredEntity createDataKey="category1"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> + <deleteData createDataKey="category1" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Create product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="adminProductIndexPageAdd"/> + <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> + <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProductPage"> + <argument name="product" value="GroupedProduct"/> + </actionGroup> + <actionGroup ref="fillGroupedProductForm" stepKey="fillMainProductForm"> + <argument name="product" value="GroupedProduct"/> + </actionGroup> + <!-- Add two simple products to grouped product --> + <scrollTo selector="{{AdminProductFormGroupedProductsSection.toggleGroupedProduct}}" x="0" y="-100" stepKey="scrollToSection"/> + <conditionalClick selector="{{AdminProductFormGroupedProductsSection.toggleGroupedProduct}}" dependentSelector="{{AdminProductFormGroupedProductsSection.addProductsToGroup}}" visible="false" stepKey="openGroupedProductSection"/> + <click selector="{{AdminProductFormGroupedProductsSection.addProductsToGroup}}" stepKey="clickAddProductsToGroup"/> + <waitForElementVisible selector="{{AdminAddProductsToGroupPanel.filters}}" stepKey="waitForFilter"/> + <actionGroup ref="filterProductGridBySku" stepKey="filterProductGridBySku1"> + <argument name="product" value="$$simpleProduct1$$"/> + </actionGroup> + <checkOption selector="{{AdminAddProductsToGroupPanel.firstCheckbox}}" stepKey="checkOption1"/> + <actionGroup ref="filterProductGridBySku" stepKey="filterProductGridBySku2"> + <argument name="product" value="$$simpleProduct2$$"/> + </actionGroup> + <checkOption selector="{{AdminAddProductsToGroupPanel.firstCheckbox}}" stepKey="checkOption2"/> + <click selector="{{AdminAddProductsToGroupPanel.addSelectedProducts}}" stepKey="addSelectedProducts"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + + <!-- Save product --> + <actionGroup ref="saveProductForm" stepKey="saveProductForm"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + + <scrollTo selector="{{AdminProductFormGroupedProductsSection.toggleGroupedProduct}}" x="0" y="-100" stepKey="scrollToProducts"/> + + <waitForText userInput="$$simpleProduct1.name$$" stepKey="assertProductIsInTheList"/> + <waitForText userInput="$$simpleProduct2.name$$" stepKey="assertProduct2IsInTheList"/> + </test> +</tests> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminRemoveDefaultVideoGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminRemoveDefaultVideoGroupedProductTest.xml index 25c45abdfe04..e322d4a1eb03 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminRemoveDefaultVideoGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminRemoveDefaultVideoGroupedProductTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminRemoveDefaultVideoGroupedProductTest" extends="AdminRemoveDefaultVideoSimpleProductTest"> <annotations> <features value="GroupedProduct"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminSortingAssociatedProductsTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminSortingAssociatedProductsTest.xml new file mode 100644 index 000000000000..ad5fbbb30ede --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminSortingAssociatedProductsTest.xml @@ -0,0 +1,207 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminSortingAssociatedProductsTest"> + <annotations> + <features value="GroupedProduct"/> + <stories value="MAGETWO-91633: Grouped Products: Associated Products Can't Be Sorted Between Pages"/> + <title value="Grouped Products: Sorting Associated Products Between Pages"/> + <description value="Make sure that products in grid were recalculated when sorting associated products between pages"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-95085"/> + <group value="GroupedProduct"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <createData entity="_defaultCategory" stepKey="category"/> + <!-- Create 23 products so that grid can have more than one page --> + <createData entity="ApiSimpleProduct" stepKey="product1"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="product2"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="product3"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="product4"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="product5"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="product6"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="product7"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="product8"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="product9"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="product10"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="product11"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="product12"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="product13"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="product14"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="product15"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="product16"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="product17"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="product18"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="product19"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="product20"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="product21"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="product22"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="product23"> + <requiredEntity createDataKey="category"/> + </createData> + </before> + <after> + <!--Delete created grouped product--> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> + <argument name="product" value="GroupedProduct"/> + </actionGroup> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" + dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <deleteData createDataKey="product1" stepKey="deleteProduct1"/> + <deleteData createDataKey="product2" stepKey="deleteProduct2"/> + <deleteData createDataKey="product3" stepKey="deleteProduct3"/> + <deleteData createDataKey="product4" stepKey="deleteProduct4"/> + <deleteData createDataKey="product5" stepKey="deleteProduct5"/> + <deleteData createDataKey="product6" stepKey="deleteProduct6"/> + <deleteData createDataKey="product7" stepKey="deleteProduct7"/> + <deleteData createDataKey="product8" stepKey="deleteProduct8"/> + <deleteData createDataKey="product9" stepKey="deleteProduct9"/> + <deleteData createDataKey="product10" stepKey="deleteProduct10"/> + <deleteData createDataKey="product11" stepKey="deleteProduct11"/> + <deleteData createDataKey="product12" stepKey="deleteProduct12"/> + <deleteData createDataKey="product13" stepKey="deleteProduct13"/> + <deleteData createDataKey="product14" stepKey="deleteProduct14"/> + <deleteData createDataKey="product15" stepKey="deleteProduct15"/> + <deleteData createDataKey="product16" stepKey="deleteProduct16"/> + <deleteData createDataKey="product17" stepKey="deleteProduct17"/> + <deleteData createDataKey="product18" stepKey="deleteProduct18"/> + <deleteData createDataKey="product19" stepKey="deleteProduct19"/> + <deleteData createDataKey="product20" stepKey="deleteProduct20"/> + <deleteData createDataKey="product21" stepKey="deleteProduct21"/> + <deleteData createDataKey="product22" stepKey="deleteProduct22"/> + <deleteData createDataKey="product23" stepKey="deleteProduct23"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create grouped Product--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="resetProductGridToDefaultView" stepKey="resetProductGridColumnsInitial"/> + <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProduct"> + <argument name="product" value="GroupedProduct"/> + </actionGroup> + <actionGroup ref="fillGroupedProductForm" stepKey="fillProductForm"> + <argument name="product" value="GroupedProduct"/> + </actionGroup> + + <scrollTo selector="{{AdminProductFormGroupedProductsSection.toggleGroupedProduct}}" x="0" y="-100" stepKey="scrollToGroupedSection"/> + <conditionalClick selector="{{AdminProductFormGroupedProductsSection.toggleGroupedProduct}}" dependentSelector="{{AdminProductFormGroupedProductsSection.addProductsToGroup}}" visible="false" stepKey="openGroupedProductsSection"/> + <click selector="body" stepKey="clickBodyToCorrectFocusGrouped"/> + <click selector="{{AdminProductFormGroupedProductsSection.addProductsToGroup}}" stepKey="clickAddProductsToGroup"/> + <waitForElementVisible selector="{{AdminAddProductsToGroupPanel.filters}}" stepKey="waitForGroupedProductModal"/> + <actionGroup ref="filterProductGridBySku2" stepKey="filterGroupedProducts"> + <argument name="sku" value="api-simple-product"/> + </actionGroup> + + <!-- Select all, then start the bulk update attributes flow --> + <click selector="{{AdminProductGridSection.multicheckDropdown}}" stepKey="openMulticheckDropdown"/> + <click selector="{{AdminProductGridSection.multicheckOption('Select All')}}" stepKey="selectAllProductInFilteredGrid"/> + + <click selector="{{AdminAddProductsToGroupPanel.addSelectedProducts}}" stepKey="clickAddSelectedGroupProducts"/> + <waitForPageLoad stepKey="waitForProductsAdded"/> + + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + + <!--Open created Product group--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> + <actionGroup ref="resetProductGridToDefaultView" stepKey="resetFiltersIfExist"/> + <actionGroup ref="searchProductGridByKeyword" stepKey="searchProductGridForm"> + <argument name="keyword" value="GroupedProduct.name"/> + </actionGroup> + <click selector="{{AdminProductGridSection.selectRowBasedOnName(GroupedProduct.name)}}" stepKey="openGroupedProduct"/> + <waitForPageLoad stepKey="waitForProductEditPageLoad"/> + + <scrollTo selector="{{AdminProductFormGroupedProductsSection.toggleGroupedProduct}}" x="0" y="-100" stepKey="scrollToGroupedSection2"/> + <conditionalClick selector="{{AdminProductFormGroupedProductsSection.toggleGroupedProduct}}" dependentSelector="{{AdminProductFormGroupedProductsSection.addProductsToGroup}}" visible="false" stepKey="openGroupedProductsSection2"/> + + <!--Change position value for the Product Position 0--> + <grabTextFrom selector="{{AdminProductFormGroupedProductsSection.nameProductFromGrid('1')}}" stepKey="grabNameProductPosition0"/> + <grabTextFrom selector="{{AdminProductFormGroupedProductsSection.nameProductFromGrid('2')}}" stepKey="grabNameProductPositionFirst"/> + <fillField selector="{{AdminProductFormGroupedProductsSection.positionProduct('1')}}" userInput="21" stepKey="fillFieldProductPosition0"/> + <doubleClick selector="{{AdminProductFormGroupedProductsSection.nextActionButton}}" stepKey="clickButton"/> + <waitForAjaxLoad stepKey="waitForAjax1"/> + + <!--Go to next page and verify that Products in grid were recalculated--> + <doubleClick selector="{{AdminProductFormGroupedProductsSection.nextActionButton}}" stepKey="clickNextActionButton"/> + <waitForAjaxLoad stepKey="waitForAjax2"/> + + <grabTextFrom selector="{{AdminProductFormGroupedProductsSection.nameProductFromGrid('2')}}" stepKey="grabNameProductPosition21"/> + <assertEquals stepKey="assertProductsRecalculated"> + <actualResult type="string">$grabNameProductPosition0</actualResult> + <expectedResult type="string">$grabNameProductPosition21</expectedResult> + </assertEquals> + + <!--Change position value for the product to 1--> + <fillField selector="{{AdminProductFormGroupedProductsSection.positionProduct('2')}}" userInput="1" stepKey="fillFieldProductPosition1"/> + <doubleClick selector="{{AdminProductFormGroupedProductsSection.previousActionButton}}" stepKey="clickButton2"/> + <waitForAjaxLoad stepKey="waitForAjax3"/> + + <!--Go to previous page and verify that Products in grid were recalculated--> + <click selector="{{AdminProductFormGroupedProductsSection.previousActionButton}}" stepKey="clickPreviousActionButton"/> + <waitForAjaxLoad stepKey="waitForAjax4"/> + <grabTextFrom selector="{{AdminProductFormGroupedProductsSection.nameProductFromGrid('2')}}" stepKey="grabNameProductPosition2"/> + <grabTextFrom selector="{{AdminProductFormGroupedProductsSection.nameProductFromGrid('1')}}" stepKey="grabNameProductPositionZero"/> + <assertEquals stepKey="assertProductsRecalculated2"> + <actualResult type="string">$grabNameProductPosition2</actualResult> + <expectedResult type="string">$grabNameProductPosition0</expectedResult> + </assertEquals> + <assertEquals stepKey="assertProductsRecalculated3"> + <actualResult type="string">$grabNameProductPositionFirst</actualResult> + <expectedResult type="string">$grabNameProductPositionZero</expectedResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest.xml index 0fd52ac4a65a..2a600d38250f 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdvanceCatalogSearchGroupedProductByNameTest" extends="AdvanceCatalogSearchSimpleProductByNameTest"> <annotations> <features value="GroupedProduct"/> diff --git a/app/code/Magento/GroupedProduct/Test/Unit/Model/Product/Type/Grouped/PriceTest.php b/app/code/Magento/GroupedProduct/Test/Unit/Model/Product/Type/Grouped/PriceTest.php index f02849c244cb..176c29add483 100644 --- a/app/code/Magento/GroupedProduct/Test/Unit/Model/Product/Type/Grouped/PriceTest.php +++ b/app/code/Magento/GroupedProduct/Test/Unit/Model/Product/Type/Grouped/PriceTest.php @@ -64,7 +64,7 @@ public function testGetFinalPrice( $expectedFinalPrice ) { $rawFinalPrice = 10; - $rawPriceCheckStep = 6; + $rawPriceCheckStep = 5; $this->productMock->expects( $this->any() @@ -155,7 +155,7 @@ public function getFinalPriceDataProvider() 'custom_option_null' => [ 'associatedProducts' => [], 'options' => [[], []], - 'expectedPriceCall' => 6, /* product call number to check final price formed correctly */ + 'expectedPriceCall' => 5, /* product call number to check final price formed correctly */ 'expectedFinalPrice' => 10, /* 10(product price) + 2(options count) * 5(qty) * 5(option price) */ ], 'custom_option_exist' => [ @@ -165,7 +165,7 @@ public function getFinalPriceDataProvider() ['associated_product_2', $optionMock], ['associated_product_3', $optionMock], ], - 'expectedPriceCall' => 16, /* product call number to check final price formed correctly */ + 'expectedPriceCall' => 15, /* product call number to check final price formed correctly */ 'expectedFinalPrice' => 35, /* 10(product price) + 2(options count) * 5(qty) * 5(option price) */ ] ]; diff --git a/app/code/Magento/GroupedProduct/Test/Unit/Model/Product/Type/GroupedTest.php b/app/code/Magento/GroupedProduct/Test/Unit/Model/Product/Type/GroupedTest.php index 06c07a8dc34a..e50d6491a6ac 100644 --- a/app/code/Magento/GroupedProduct/Test/Unit/Model/Product/Type/GroupedTest.php +++ b/app/code/Magento/GroupedProduct/Test/Unit/Model/Product/Type/GroupedTest.php @@ -611,9 +611,9 @@ public function testPrepareForCartAdvancedZeroQty() public function testFlushAssociatedProductsCache() { - $productMock = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['unsData']); + $productMock = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['unsetData']); $productMock->expects($this->once()) - ->method('unsData') + ->method('unsetData') ->with('_cache_instance_associated_products') ->willReturnSelf(); $this->assertEquals($productMock, $this->_model->flushAssociatedProductsCache($productMock)); diff --git a/app/code/Magento/GroupedProduct/Test/Unit/Ui/DataProvider/Product/Form/Modifier/GroupedTest.php b/app/code/Magento/GroupedProduct/Test/Unit/Ui/DataProvider/Product/Form/Modifier/GroupedTest.php index 6ef87117deae..ad4b86351a66 100644 --- a/app/code/Magento/GroupedProduct/Test/Unit/Ui/DataProvider/Product/Form/Modifier/GroupedTest.php +++ b/app/code/Magento/GroupedProduct/Test/Unit/Ui/DataProvider/Product/Form/Modifier/GroupedTest.php @@ -21,6 +21,9 @@ use Magento\GroupedProduct\Model\Product\Type\Grouped as GroupedProductType; use Magento\GroupedProduct\Ui\DataProvider\Product\Form\Modifier\Grouped; use Magento\Store\Api\Data\StoreInterface; +use Magento\Catalog\Model\Product; +use Magento\GroupedProduct\Model\Product\Link\CollectionProvider\Grouped as GroupedProducts; +use Magento\Catalog\Api\Data\ProductLinkInterfaceFactory; /** * Class GroupedTest @@ -34,6 +37,7 @@ class GroupedTest extends AbstractModifierTest const LINKED_PRODUCT_NAME = 'linked'; const LINKED_PRODUCT_QTY = '0'; const LINKED_PRODUCT_POSITION = 1; + const LINKED_PRODUCT_POSITION_CALCULATED = 1; const LINKED_PRODUCT_PRICE = '1'; /** @@ -81,23 +85,38 @@ class GroupedTest extends AbstractModifierTest */ protected $storeMock; + /** + * @var GroupedProducts|\PHPUnit_Framework_MockObject_MockObject + */ + private $groupedProductsMock; + + /** + * @var ProductLinkInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $productLinkFactoryMock; + + /** + * @inheritdoc + */ protected function setUp() { $this->objectManager = new ObjectManager($this); $this->locatorMock = $this->getMockBuilder(LocatorInterface::class) ->getMockForAbstractClass(); - $this->productMock = $this->getMockBuilder(ProductInterface::class) + $this->productMock = $this->getMockBuilder(Product::class) ->setMethods(['getId', 'getTypeId']) - ->getMockForAbstractClass(); + ->disableOriginalConstructor() + ->getMock(); $this->productMock->expects($this->any()) ->method('getId') ->willReturn(self::PRODUCT_ID); $this->productMock->expects($this->any()) ->method('getTypeId') ->willReturn(GroupedProductType::TYPE_CODE); - $this->linkedProductMock = $this->getMockBuilder(ProductInterface::class) + $this->linkedProductMock = $this->getMockBuilder(Product::class) ->setMethods(['getId', 'getName', 'getPrice']) - ->getMockForAbstractClass(); + ->disableOriginalConstructor() + ->getMock(); $this->linkedProductMock->expects($this->any()) ->method('getId') ->willReturn(self::LINKED_PRODUCT_ID); @@ -134,7 +153,7 @@ protected function setUp() $this->linkRepositoryMock->expects($this->any()) ->method('getList') ->with($this->productMock) - ->willReturn([$this->linkMock]); + ->willReturn([$this->linkedProductMock]); $this->productRepositoryMock = $this->getMockBuilder(ProductRepositoryInterface::class) ->setMethods(['get']) ->getMockForAbstractClass(); @@ -154,7 +173,7 @@ protected function setUp() } /** - * {@inheritdoc} + * @inheritdoc */ protected function createModel() { @@ -168,6 +187,16 @@ protected function createModel() ->setMethods(['init', 'getUrl']) ->disableOriginalConstructor() ->getMock(); + + $this->groupedProductsMock = $this->getMockBuilder(GroupedProducts::class) + ->setMethods(['getLinkedProducts']) + ->disableOriginalConstructor() + ->getMock(); + $this->productLinkFactoryMock = $this->getMockBuilder(ProductLinkInterfaceFactory::class) + ->setMethods(['create']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->imageHelperMock->expects($this->any()) ->method('init') ->willReturn($this->imageHelperMock); @@ -188,16 +217,23 @@ protected function createModel() 'localeCurrency' => $this->currencyMock, 'imageHelper' => $this->imageHelperMock, 'attributeSetRepository' => $this->attributeSetRepositoryMock, + 'groupedProducts' => $this->groupedProductsMock, + 'productLinkFactory' => $this->productLinkFactoryMock, ]); } + /** + * Assert array has key + * + * @return void + */ public function testModifyMeta() { $this->assertArrayHasKey(Grouped::GROUP_GROUPED, $this->getModel()->modifyMeta([])); } /** - * {@inheritdoc} + * @inheritdoc */ public function testModifyData() { @@ -212,6 +248,7 @@ public function testModifyData() 'price' => null, 'qty' => self::LINKED_PRODUCT_QTY, 'position' => self::LINKED_PRODUCT_POSITION, + 'positionCalculated' => self::LINKED_PRODUCT_POSITION_CALCULATED, 'thumbnail' => null, 'type_id' => null, 'status' => null, @@ -224,6 +261,42 @@ public function testModifyData() ], ], ]; - $this->assertSame($expectedData, $this->getModel()->modifyData([])); + $model = $this->getModel(); + $linkedProductMock = $this->getMockBuilder(Product::class) + ->setMethods(['getId', 'getName', 'getPrice', 'getSku', 'getImage', 'getPosition', 'getQty']) + ->disableOriginalConstructor() + ->getMock(); + $linkedProductMock->expects($this->once()) + ->method('getId') + ->willReturn(self::LINKED_PRODUCT_ID); + $linkedProductMock->expects($this->once()) + ->method('getName') + ->willReturn(self::LINKED_PRODUCT_NAME); + $linkedProductMock->expects($this->once()) + ->method('getPrice') + ->willReturn(self::LINKED_PRODUCT_PRICE); + $linkedProductMock->expects($this->once()) + ->method('getSku') + ->willReturn(self::LINKED_PRODUCT_SKU); + $linkedProductMock->expects($this->once()) + ->method('getImage') + ->willReturn(''); + $linkedProductMock->expects($this->exactly(2)) + ->method('getPosition') + ->willReturn(self::LINKED_PRODUCT_POSITION); + $linkedProductMock->expects($this->once()) + ->method('getQty') + ->willReturn(self::LINKED_PRODUCT_QTY); + $this->groupedProductsMock->expects($this->once()) + ->method('getLinkedProducts') + ->willReturn([$linkedProductMock]); + $linkMock = $this->getMockBuilder(ProductLinkInterface::class) + ->getMockForAbstractClass(); + + $this->productLinkFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($linkMock); + + $this->assertSame($expectedData, $model->modifyData([])); } } diff --git a/app/code/Magento/GroupedProduct/Ui/DataProvider/Product/Form/Modifier/Grouped.php b/app/code/Magento/GroupedProduct/Ui/DataProvider/Product/Form/Modifier/Grouped.php index 2d1a1d19757e..2ea622c1c2b8 100644 --- a/app/code/Magento/GroupedProduct/Ui/DataProvider/Product/Form/Modifier/Grouped.php +++ b/app/code/Magento/GroupedProduct/Ui/DataProvider/Product/Form/Modifier/Grouped.php @@ -21,6 +21,9 @@ use Magento\Eav\Api\AttributeSetRepositoryInterface; use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\Framework\Locale\CurrencyInterface; +use Magento\GroupedProduct\Model\Product\Link\CollectionProvider\Grouped as GroupedProducts; +use Magento\Framework\App\ObjectManager; +use Magento\Catalog\Api\Data\ProductLinkInterfaceFactory; /** * Data provider for Grouped products @@ -99,6 +102,16 @@ class Grouped extends AbstractModifier */ private static $codeQty = 'qty'; + /** + * @var GroupedProducts + */ + private $groupedProducts; + + /** + * @var ProductLinkInterfaceFactory + */ + private $productLinkFactory; + /** * @param LocatorInterface $locator * @param UrlInterface $urlBuilder @@ -109,6 +122,9 @@ class Grouped extends AbstractModifier * @param AttributeSetRepositoryInterface $attributeSetRepository * @param CurrencyInterface $localeCurrency * @param array $uiComponentsConfig + * @param GroupedProducts $groupedProducts + * @param \Magento\Catalog\Api\Data\ProductLinkInterfaceFactory|null $productLinkFactory + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( LocatorInterface $locator, @@ -119,7 +135,9 @@ public function __construct( Status $status, AttributeSetRepositoryInterface $attributeSetRepository, CurrencyInterface $localeCurrency, - array $uiComponentsConfig = [] + array $uiComponentsConfig = [], + GroupedProducts $groupedProducts = null, + \Magento\Catalog\Api\Data\ProductLinkInterfaceFactory $productLinkFactory = null ) { $this->locator = $locator; $this->urlBuilder = $urlBuilder; @@ -130,10 +148,15 @@ public function __construct( $this->status = $status; $this->localeCurrency = $localeCurrency; $this->uiComponentsConfig = array_replace_recursive($this->uiComponentsConfig, $uiComponentsConfig); + $this->groupedProducts = $groupedProducts ?: ObjectManager::getInstance()->get( + \Magento\GroupedProduct\Model\Product\Link\CollectionProvider\Grouped::class + ); + $this->productLinkFactory = $productLinkFactory ?: ObjectManager::getInstance() + ->get(\Magento\Catalog\Api\Data\ProductLinkInterfaceFactory::class); } /** - * {@inheritdoc} + * @inheritdoc */ public function modifyData(array $data) { @@ -143,13 +166,15 @@ public function modifyData(array $data) if ($modelId) { $storeId = $this->locator->getStore()->getId(); $data[$product->getId()]['links'][self::LINK_TYPE] = []; - foreach ($this->productLinkRepository->getList($product) as $linkItem) { - if ($linkItem->getLinkType() !== self::LINK_TYPE) { - continue; - } + $linkedItems = $this->groupedProducts->getLinkedProducts($product); + usort($linkedItems, function ($a, $b) { + return $a->getPosition() <=> $b->getPosition(); + }); + $productLink = $this->productLinkFactory->create(); + foreach ($linkedItems as $index => $linkItem) { /** @var \Magento\Catalog\Api\Data\ProductInterface $linkedProduct */ - $linkedProduct = $this->productRepository->get($linkItem->getLinkedProductSku(), false, $storeId); - $data[$modelId]['links'][self::LINK_TYPE][] = $this->fillData($linkedProduct, $linkItem); + $linkItem->setPosition($index); + $data[$modelId]['links'][self::LINK_TYPE][] = $this->fillData($linkItem, $productLink); } $data[$modelId][self::DATA_SOURCE_DEFAULT]['current_store_id'] = $storeId; } @@ -162,6 +187,7 @@ public function modifyData(array $data) * @param ProductInterface $linkedProduct * @param ProductLinkInterface $linkItem * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ protected function fillData(ProductInterface $linkedProduct, ProductLinkInterface $linkItem) { @@ -171,11 +197,15 @@ protected function fillData(ProductInterface $linkedProduct, ProductLinkInterfac return [ 'id' => $linkedProduct->getId(), 'name' => $linkedProduct->getName(), - 'sku' => $linkItem->getLinkedProductSku(), + 'sku' => $linkedProduct->getSku(), 'price' => $currency->toCurrency(sprintf("%f", $linkedProduct->getPrice())), - 'qty' => $linkItem->getExtensionAttributes()->getQty(), - 'position' => $linkItem->getPosition(), - 'thumbnail' => $this->imageHelper->init($linkedProduct, 'product_listing_thumbnail')->getUrl(), + 'qty' => $linkedProduct->getQty(), + 'position' => $linkedProduct->getPosition(), + 'positionCalculated' => $linkedProduct->getPosition(), + 'thumbnail' => $this->imageHelper + ->init($linkedProduct, 'product_listing_thumbnail') + ->setImageFile($linkedProduct->getImage()) + ->getUrl(), 'type_id' => $linkedProduct->getTypeId(), 'status' => $this->status->getOptionText($linkedProduct->getStatus()), 'attribute_set' => $this->attributeSetRepository @@ -185,7 +215,7 @@ protected function fillData(ProductInterface $linkedProduct, ProductLinkInterfac } /** - * {@inheritdoc} + * @inheritdoc */ public function modifyMeta(array $meta) { @@ -408,8 +438,8 @@ protected function getButtonSet() 'component' => 'Magento_Ui/js/form/components/button', 'actions' => [ [ - 'targetName' => - $this->uiComponentsConfig['form'] . '.' . $this->uiComponentsConfig['form'] + 'targetName' => $this->uiComponentsConfig['form'] . + '.' . $this->uiComponentsConfig['form'] . '.' . static::GROUP_GROUPED . '.' @@ -417,8 +447,8 @@ protected function getButtonSet() 'actionName' => 'openModal', ], [ - 'targetName' => - $this->uiComponentsConfig['form'] . '.' . $this->uiComponentsConfig['form'] + 'targetName' => $this->uiComponentsConfig['form'] . + '.' . $this->uiComponentsConfig['form'] . '.' . static::GROUP_GROUPED . '.' @@ -454,7 +484,7 @@ protected function getGrid() 'label' => null, 'renderDefaultRecord' => false, 'template' => 'ui/dynamic-rows/templates/grid', - 'component' => 'Magento_Ui/js/dynamic-rows/dynamic-rows-grid', + 'component' => 'Magento_GroupedProduct/js/grouped-product-grid', 'addButton' => false, 'itemTemplate' => 'record', 'dataScope' => 'data.links', @@ -555,6 +585,22 @@ protected function fillMeta() ], ], ], + 'positionCalculated' => [ + 'arguments' => [ + 'data' => [ + 'config' => [ + 'label' => __('Position'), + 'dataType' => Form\Element\DataType\Number::NAME, + 'formElement' => Form\Element\Input::NAME, + 'componentType' => Form\Field::NAME, + 'elementTmpl' => 'Magento_GroupedProduct/components/position', + 'sortOrder' => 90, + 'fit' => true, + 'dataScope' => 'positionCalculated' + ], + ], + ], + ], 'actionDelete' => [ 'arguments' => [ 'data' => [ @@ -563,7 +609,7 @@ protected function fillMeta() 'componentType' => 'actionDelete', 'dataType' => Form\Element\DataType\Text::NAME, 'label' => __('Actions'), - 'sortOrder' => 90, + 'sortOrder' => 100, 'fit' => true, ], ], @@ -577,7 +623,7 @@ protected function fillMeta() 'formElement' => Form\Element\Input::NAME, 'componentType' => Form\Field::NAME, 'dataScope' => 'position', - 'sortOrder' => 100, + 'sortOrder' => 110, 'visible' => false, ], ], diff --git a/app/code/Magento/GroupedProduct/view/adminhtml/layout/groupedproduct_popup_grid.xml b/app/code/Magento/GroupedProduct/view/adminhtml/layout/groupedproduct_popup_grid.xml index 503404c6cb3c..fab7851df5bc 100644 --- a/app/code/Magento/GroupedProduct/view/adminhtml/layout/groupedproduct_popup_grid.xml +++ b/app/code/Magento/GroupedProduct/view/adminhtml/layout/groupedproduct_popup_grid.xml @@ -11,7 +11,7 @@ <block class="Magento\Backend\Block\Widget\Grid" name="catalog.product.edit.tab.super.group.popup" as="grid"> <arguments> <argument name="id" xsi:type="string">grouped_grid_popup</argument> - <argument name="dataSource" xsi:type="object">Magento\GroupedProduct\Model\ResourceModel\Product\Type\Grouped\AssociatedProductsCollection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\GroupedProduct\Model\ResourceModel\Product\Type\Grouped\AssociatedProductsCollection</argument> <argument name="use_ajax" xsi:type="string">1</argument> <argument name="default_sort" xsi:type="string">id</argument> <argument name="default_dir" xsi:type="string">ASC</argument> diff --git a/app/code/Magento/GroupedProduct/view/adminhtml/web/css/grouped-product.css b/app/code/Magento/GroupedProduct/view/adminhtml/web/css/grouped-product.css index 3d723387d23b..9142eaf90899 100644 --- a/app/code/Magento/GroupedProduct/view/adminhtml/web/css/grouped-product.css +++ b/app/code/Magento/GroupedProduct/view/adminhtml/web/css/grouped-product.css @@ -66,3 +66,49 @@ overflow: hidden; text-overflow: ellipsis; } + +.position { + width: 100px; +} + +.icon-rearrange-position > span { + border: 0; + clip: rect(0, 0, 0, 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; +} + +.icon-forward, +.icon-backward { + -webkit-font-smoothing: antialiased; + font-family: 'Admin Icons'; + font-size: 17px; + speak: none; +} + +.position > * { + float: left; + margin: 3px; +} + +.position-widget-input { + text-align: center; + width: 40px; +} + +.icon-forward:before { + content: '\e618'; +} + +.icon-backward:before { + content: '\e619'; +} + +.icon-rearrange-position, .icon-rearrange-position:hover { + color: #d9d9d9; + text-decoration: none; +} diff --git a/app/code/Magento/GroupedProduct/view/adminhtml/web/js/grouped-product-grid.js b/app/code/Magento/GroupedProduct/view/adminhtml/web/js/grouped-product-grid.js new file mode 100644 index 000000000000..0ac3b58d6e3a --- /dev/null +++ b/app/code/Magento/GroupedProduct/view/adminhtml/web/js/grouped-product-grid.js @@ -0,0 +1,219 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'underscore', + 'uiRegistry', + 'Magento_Ui/js/dynamic-rows/dynamic-rows-grid' +], function (_, registry, dynamicRowsGrid) { + 'use strict'; + + return dynamicRowsGrid.extend({ + + /** + * Set max element position + * + * @param {Number} position - element position + * @param {Object} elem - instance + */ + setMaxPosition: function (position, elem) { + + if (position || position === 0) { + this.checkMaxPosition(position); + this.sort(position, elem); + + if (~~position === this.maxPosition && ~~position > this.getDefaultPageBoundary() + 1) { + this.shiftNextPagesPositions(position); + } + } else { + this.maxPosition += 1; + } + }, + + /** + * Shift positions for next page elements + * + * @param {Number} position + */ + shiftNextPagesPositions: function (position) { + + var recordData = this.recordData(), + startIndex = ~~this.currentPage() * this.pageSize, + offset = position - startIndex + 1, + index = startIndex; + + if (~~this.currentPage() === this.pages()) { + return false; + } + + for (index; index < recordData.length; index++) { + recordData[index].position = index + offset; + } + this.recordData(recordData); + }, + + /** + * Update position for element after position from another page is entered + * + * @param {Object} data + * @param {Object} event + */ + updateGridPosition: function (data, event) { + var inputValue = parseInt(event.target.value, 10), + recordData = this.recordData(), + record, + previousValue, + updatedRecord; + + record = this.elems().find(function (obj) { + return obj.dataScope === data.parentScope; + }); + + previousValue = this.getCalculatedPosition(record); + + if (isNaN(inputValue) || inputValue < 0 || inputValue === previousValue) { + return false; + } + + this.elems([]); + + updatedRecord = this.getUpdatedRecordIndex(recordData, record.data().id); + + if (inputValue >= this.recordData().size() - 1) { + recordData[updatedRecord].position = this.getGlobalMaxPosition() + 1; + } else { + recordData.forEach(function (value, index) { + if (~~value.id === ~~record.data().id) { + recordData[index].position = inputValue; + } else if (inputValue > previousValue && index <= inputValue) { + recordData[index].position = index - 1; + } else if (inputValue < previousValue && index >= inputValue) { + recordData[index].position = index + 1; + } + }); + } + + this.reloadGridData(recordData); + + }, + + /** + * Get updated record index + * + * @param {Array} recordData + * @param {Number} recordId + * @return {Number} + */ + getUpdatedRecordIndex: function (recordData, recordId) { + return recordData.map(function (o) { + return ~~o.id; + }).indexOf(~~recordId); + }, + + /** + * + * @param {Array} recordData - to reprocess + */ + reloadGridData: function (recordData) { + this.recordData(recordData.sort(function (a, b) { + return ~~a.position - ~~b.position; + })); + this._updateCollection(); + this.reload(); + }, + + /** + * Event handler for "Send to bottom" button + * + * @param {Object} positionObj + * @return {Boolean} + */ + sendToBottom: function (positionObj) { + + var objectToUpdate = this.getObjectToUpdate(positionObj), + recordData = this.recordData(), + updatedRecord; + + if (~~this.currentPage() === this.pages) { + objectToUpdate.position = this.maxPosition; + } else { + this.elems([]); + updatedRecord = this.getUpdatedRecordIndex(recordData, objectToUpdate.data().id); + recordData[updatedRecord].position = this.getGlobalMaxPosition() + 1; + this.reloadGridData(recordData); + } + + return false; + }, + + /** + * Event handler for "Send to top" button + * + * @param {Object} positionObj + * @return {Boolean} + */ + sendToTop: function (positionObj) { + var objectToUpdate = this.getObjectToUpdate(positionObj), + recordData = this.recordData(), + updatedRecord; + + //isFirst + if (~~this.currentPage() === 1) { + objectToUpdate.position = 0; + } else { + this.elems([]); + updatedRecord = this.getUpdatedRecordIndex(recordData, objectToUpdate.data().id); + recordData.forEach(function (value, index) { + recordData[index].position = index === updatedRecord ? 0 : value.position + 1; + }); + this.reloadGridData(recordData); + } + + return false; + }, + + /** + * Get element from grid for update + * + * @param {Object} object + * @return {*} + */ + getObjectToUpdate: function (object) { + return this.elems().filter(function (item) { + return item.name === object.parentName; + })[0]; + }, + + /** + * Value function for position input + * + * @param {Object} data + * @return {Number} + */ + getCalculatedPosition: function (data) { + return (~~this.currentPage() - 1) * this.pageSize + this.elems().pluck('name').indexOf(data.name); + }, + + /** + * Return Page Boundary + * + * @return {Number} + */ + getDefaultPageBoundary: function () { + return ~~this.currentPage() * this.pageSize - 1; + }, + + /** + * Returns position for last element to be moved after + * + * @return {Number} + */ + getGlobalMaxPosition: function () { + return _.max(this.recordData().map(function (r) { + return ~~r.position; + })); + } + }); +}); diff --git a/app/code/Magento/GroupedProduct/view/adminhtml/web/template/components/position.html b/app/code/Magento/GroupedProduct/view/adminhtml/web/template/components/position.html new file mode 100644 index 000000000000..050fb3c2898c --- /dev/null +++ b/app/code/Magento/GroupedProduct/view/adminhtml/web/template/components/position.html @@ -0,0 +1,19 @@ +<div class="position"> + <a href="#" class="move-top icon-backward icon-rearrange-position" + data-bind=" + click: $parent.sendToTop.bind($parent) + "> + <span>Top</span> + </a> + <input type="text" class="position-widget-input" + data-bind=" + value: $parent.getCalculatedPosition($record()), + event: {blur: $parent.updateGridPosition.bind($parent)} + "/> + <a href="#" class="move-bottom icon-forward icon-rearrange-position" + data-bind=" + click: $parent.sendToBottom.bind($parent) + "> + <span>Bottom</span> + </a> +</div> diff --git a/app/code/Magento/GroupedProduct/view/base/templates/product/price/final_price.phtml b/app/code/Magento/GroupedProduct/view/base/templates/product/price/final_price.phtml index 34de1a18cf28..d9238e9794d7 100644 --- a/app/code/Magento/GroupedProduct/view/base/templates/product/price/final_price.phtml +++ b/app/code/Magento/GroupedProduct/view/base/templates/product/price/final_price.phtml @@ -26,7 +26,7 @@ if ($minProduct) { ); } ?> -<div class="price-box" itemprop="offers" itemscope itemtype="http://schema.org/Offer"> +<div class="price-box"> <?php if ($minProduct && \Magento\Framework\Pricing\Render::ZONE_ITEM_VIEW != $block->getZone()): ?> <p class="minimal-price"> <span class="price-label"><?= /* @escapeNotVerified */ __('Starting at') ?></span><?= $amountRender->toHtml() ?> diff --git a/app/code/Magento/GroupedProduct/view/frontend/templates/product/view/type/grouped.phtml b/app/code/Magento/GroupedProduct/view/frontend/templates/product/view/type/grouped.phtml index 7fad5635f869..0be71f20a382 100644 --- a/app/code/Magento/GroupedProduct/view/frontend/templates/product/view/type/grouped.phtml +++ b/app/code/Magento/GroupedProduct/view/frontend/templates/product/view/type/grouped.phtml @@ -19,7 +19,9 @@ <?php $_hasAssociatedProducts = count($_associatedProducts) > 0; ?> <div class="table-wrapper grouped"> - <table class="table data grouped" id="super-product-table"> + <table class="table data grouped" + id="super-product-table" + data-mage-init='{ "Magento_GroupedProduct/js/product-ids-resolver": {} }'> <caption class="table-caption"><?= /* @escapeNotVerified */ __('Grouped product items') ?></caption> <thead> <tr> @@ -31,8 +33,8 @@ </thead> <?php if ($_hasAssociatedProducts): ?> - <?php foreach ($_associatedProducts as $_item): ?> <tbody> + <?php foreach ($_associatedProducts as $_item): ?> <tr> <td data-th="<?= $block->escapeHtml(__('Product Name')) ?>" class="col item"> <strong class="product-item-name"><?= $block->escapeHtml($_item->getName()) ?></strong> @@ -78,8 +80,8 @@ </td> </tr> <?php endif; ?> - </tbody> <?php endforeach; ?> + </tbody> <?php else: ?> <tbody> <tr> diff --git a/app/code/Magento/GroupedProduct/view/frontend/web/js/product-ids-resolver.js b/app/code/Magento/GroupedProduct/view/frontend/web/js/product-ids-resolver.js new file mode 100644 index 000000000000..e6294d8043a5 --- /dev/null +++ b/app/code/Magento/GroupedProduct/view/frontend/web/js/product-ids-resolver.js @@ -0,0 +1,25 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'Magento_Catalog/js/product/view/product-ids' +], function ($, productIds) { + 'use strict'; + + /** + * Returns id's of products in form. + * + * @param {Object} config + * @param {HTMLElement} element + * @return {Array} + */ + return function (config, element) { + $(element).find('div[data-product-id]').each(function () { + productIds.push($(this).data('productId').toString()); + }); + + return productIds(); + }; +}); diff --git a/app/code/Magento/GroupedProductGraphQl/Model/GroupedProductTypeResolver.php b/app/code/Magento/GroupedProductGraphQl/Model/GroupedProductTypeResolver.php index 087cf10c8d6b..8818766692fe 100644 --- a/app/code/Magento/GroupedProductGraphQl/Model/GroupedProductTypeResolver.php +++ b/app/code/Magento/GroupedProductGraphQl/Model/GroupedProductTypeResolver.php @@ -8,19 +8,21 @@ namespace Magento\GroupedProductGraphQl\Model; use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; +use Magento\GroupedProduct\Model\Product\Type\Grouped as Type; /** - * {@inheritdoc} + * @inheritdoc */ class GroupedProductTypeResolver implements TypeResolverInterface { + const GROUPED_PRODUCT = 'GroupedProduct'; /** - * {@inheritdoc} + * @inheritdoc */ public function resolveType(array $data) : string { - if (isset($data['type_id']) && $data['type_id'] == 'grouped') { - return 'GroupedProduct'; + if (isset($data['type_id']) && $data['type_id'] == Type::TYPE_CODE) { + return self::GROUPED_PRODUCT; } return ''; } diff --git a/app/code/Magento/GroupedProductGraphQl/Model/Resolver/GroupedItems.php b/app/code/Magento/GroupedProductGraphQl/Model/Resolver/GroupedItems.php index fafbbcf436e6..d51b22ffe21d 100644 --- a/app/code/Magento/GroupedProductGraphQl/Model/Resolver/GroupedItems.php +++ b/app/code/Magento/GroupedProductGraphQl/Model/Resolver/GroupedItems.php @@ -7,43 +7,34 @@ namespace Magento\GroupedProductGraphQl\Model\Resolver; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Deferred\Product; use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Query\Resolver\Value; -use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\GroupedProduct\Model\Product\Initialization\Helper\ProductLinks\Plugin\Grouped; /** - * {@inheritdoc} + * @inheritdoc */ class GroupedItems implements ResolverInterface { - /** - * @var ValueFactory - */ - private $valueFactory; - /** * @var Product */ private $productResolver; /** - * @param ValueFactory $valueFactory * @param Product $productResolver */ public function __construct( - ValueFactory $valueFactory, Product $productResolver ) { - $this->valueFactory = $valueFactory; $this->productResolver = $productResolver; } /** - * {@inheritDoc} + * @inheritdoc */ public function resolve( Field $field, @@ -51,14 +42,12 @@ public function resolve( ResolveInfo $info, array $value = null, array $args = null - ): Value { + ) { if (!isset($value['model'])) { - $result = function () { - return null; - }; - return $this->valueFactory->create($result); + throw new LocalizedException(__('"model" value should be specified')); } + $data = []; $productModel = $value['model']; $links = $productModel->getProductLinks(); foreach ($links as $link) { @@ -73,10 +62,6 @@ public function resolve( ]; } - $result = function () use ($data) { - return $data; - }; - - return $this->valueFactory->create($result); + return $data; } } 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 000000000000..01c41e35fc4e --- /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 000000000000..39bb89b43c83 --- /dev/null +++ b/app/code/Magento/ImportExport/Api/ExportManagementInterface.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Api; + +use Magento\ImportExport\Api\Data\ExportInfoInterface; + +/** + * Describes how to do export operation with data interface. + * @api + */ +interface ExportManagementInterface +{ + /** + * Return export data. + * + * @param ExportInfoInterface $exportInfo + * @return string + */ + public function export(ExportInfoInterface $exportInfo); +} diff --git a/app/code/Magento/ImportExport/Block/Adminhtml/Export/Filter.php b/app/code/Magento/ImportExport/Block/Adminhtml/Export/Filter.php index dc928b4c7942..d032f2f7621b 100644 --- a/app/code/Magento/ImportExport/Block/Adminhtml/Export/Filter.php +++ b/app/code/Magento/ImportExport/Block/Adminhtml/Export/Filter.php @@ -237,20 +237,20 @@ protected function _getSelectHtmlWithValue(Attribute $attribute, $value) if ($attribute->getFilterOptions()) { $options = []; - foreach ($attribute->getFilterOptions() as $value => $label) { - $options[] = ['value' => $value, 'label' => $label]; + foreach ($attribute->getFilterOptions() as $optionValue => $label) { + $options[] = ['value' => $optionValue, 'label' => $label]; } } else { $options = $attribute->getSource()->getAllOptions(false); } if ($size = count($options)) { - // add empty vaue option + // add empty value option $firstOption = reset($options); if ('' === $firstOption['value']) { $options[key($options)]['label'] = ''; } else { - array_unshift($options, ['value' => '', 'label' => '']); + array_unshift($options, ['value' => '', 'label' => __('-- Not Selected --')]); } $arguments = [ 'name' => $this->getFilterElementName($attribute->getAttributeCode()), 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 39d0d5c7feae..d6b96a28afcc 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', [ @@ -113,15 +114,16 @@ protected function _prepareForm() 'class' => $behaviorCode, 'onchange' => 'varienImport.handleImportBehaviorSelector();', 'note' => ' ', + '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, @@ -132,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, @@ -148,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, @@ -161,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, @@ -174,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, @@ -187,39 +189,43 @@ 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, - 'class' => 'input-file' + 'class' => 'input-file', + 'note' => __( + 'File must be saved in UTF-8 encoding for proper import' + ), ] ); - $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, @@ -230,6 +236,7 @@ protected function _prepareForm() ), ] ); + $fieldsets['upload'] = $fieldset; $form->setUseContainer(true); $this->setForm($form); @@ -249,4 +256,19 @@ protected function getDownloadSampleFileHtml() . '</a></span>'; return $html; } + + /** + * Get Import Behavior field tooltip + * + * @return string + */ + private function getImportBehaviorTooltip() + { + $html = '<div class="admin__field-tooltip tooltip"> + <a class="admin__field-tooltip-action action-help" target="_blank" title="What is this?" + href="https://docs.magento.com/m2/ce/user_guide/system/data-import.html"><span>' + . __('What is this?') + . '</span></a></div>'; + return $html; + } } diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/Export/Export.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/Export.php index 06d8610a247c..13c22a976e79 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/Export/Export.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/Export.php @@ -5,15 +5,19 @@ */ namespace Magento\ImportExport\Controller\Adminhtml\Export; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\Controller\ResultFactory; use Magento\ImportExport\Controller\Adminhtml\Export as ExportController; 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; -class Export extends ExportController +/** + * Controller for export operation. + */ +class Export extends ExportController implements HttpPostActionInterface { /** * @var \Magento\Framework\App\Response\Http\FileFactory @@ -26,18 +30,38 @@ class Export extends ExportController 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); } @@ -50,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 000000000000..6996ba90c3e1 --- /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 000000000000..32385e62a5dc --- /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/Export/GetFilter.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/GetFilter.php index 05475b516197..722d32c9eb21 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/Export/GetFilter.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/GetFilter.php @@ -5,10 +5,12 @@ */ namespace Magento\ImportExport\Controller\Adminhtml\Export; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\ImportExport\Controller\Adminhtml\Export as ExportController; use Magento\Framework\Controller\ResultFactory; -class GetFilter extends ExportController +class GetFilter extends ExportController implements HttpGetActionInterface, HttpPostActionInterface { /** * Get grid-filter of entity attributes action. diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/Export/Index.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/Index.php index d2ca99f1a797..3dd5bfd550a3 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/Export/Index.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/Index.php @@ -5,10 +5,11 @@ */ namespace Magento\ImportExport\Controller\Adminhtml\Export; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\ImportExport\Controller\Adminhtml\Export as ExportController; use Magento\Framework\Controller\ResultFactory; -class Index extends ExportController +class Index extends ExportController implements HttpGetActionInterface { /** * Index action. diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/History/Download.php b/app/code/Magento/ImportExport/Controller/Adminhtml/History/Download.php index e490ee401837..ba37cc8aacff 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/History/Download.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/History/Download.php @@ -1,20 +1,28 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\ImportExport\Controller\Adminhtml\History; +use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Framework\App\Filesystem\DirectoryList; -class Download extends \Magento\ImportExport\Controller\Adminhtml\History +/** + * Download history controller + */ +class Download extends \Magento\ImportExport\Controller\Adminhtml\History implements HttpGetActionInterface { /** * @var \Magento\Framework\Controller\Result\RawFactory */ protected $resultRawFactory; + /** + * @var \Magento\Framework\App\Response\Http\FileFactory + */ + private $fileFactory; + /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Framework\App\Response\Http\FileFactory $fileFactory diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Index.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Index.php index df32d61cceb7..3cca7ae7ccce 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Index.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Index.php @@ -5,10 +5,11 @@ */ namespace Magento\ImportExport\Controller\Adminhtml\Import; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\ImportExport\Controller\Adminhtml\Import as ImportController; use Magento\Framework\Controller\ResultFactory; -class Index extends ImportController +class Index extends ImportController implements HttpGetActionInterface { /** * Index action diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Start.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Start.php index 695a0e61709f..e850f6af86cf 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Start.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Start.php @@ -5,10 +5,15 @@ */ namespace Magento\ImportExport\Controller\Adminhtml\Import; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\ImportExport\Controller\Adminhtml\ImportResult as ImportResultController; use Magento\Framework\Controller\ResultFactory; +use Magento\ImportExport\Model\Import; -class Start extends ImportResultController +/** + * Controller responsible for initiating the import process + */ +class Start extends ImportResultController implements HttpPostActionInterface { /** * @var \Magento\ImportExport\Model\Import @@ -62,6 +67,11 @@ public function execute() $this->importModel->setData($data); $errorAggregator = $this->importModel->getErrorAggregator(); + $errorAggregator->initValidationStrategy( + $this->importModel->getData(Import::FIELD_NAME_VALIDATION_STRATEGY), + $this->importModel->getData(Import::FIELD_NAME_ALLOWED_ERROR_COUNT) + ); + try { $this->importModel->importSource(); } catch (\Exception $e) { @@ -83,6 +93,20 @@ public function execute() $this->addErrorMessages($resultBlock, $errorAggregator); } else { $this->importModel->invalidateIndex(); + + $noticeHtml = $this->historyModel->getSummary(); + + if ($this->historyModel->getErrorFile()) { + $noticeHtml .= '<div class="import-error-wrapper">' . __('Only the first 100 errors are shown. ') + . '<a href="' + . $this->createDownloadUrlImportHistoryFile($this->historyModel->getErrorFile()) + . '">' . __('Download full report') . '</a></div>'; + } + + $resultBlock->addNotice( + $noticeHtml + ); + $this->addErrorMessages($resultBlock, $errorAggregator); $resultBlock->addSuccess(__('Import successfully done')); } diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php index df9f63e79b75..c18e66626089 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php @@ -5,15 +5,19 @@ */ namespace Magento\ImportExport\Controller\Adminhtml\Import; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\ImportExport\Controller\Adminhtml\ImportResult as ImportResultController; use Magento\ImportExport\Model\Import; use Magento\ImportExport\Block\Adminhtml\Import\Frame\Result; use Magento\Framework\Controller\ResultFactory; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\ImportExport\Model\Import\Adapter as ImportAdapter; -use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; -class Validate extends ImportResultController +/** + * Import validate controller action. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class Validate extends ImportResultController implements HttpPostActionInterface { /** * @var Import @@ -24,6 +28,7 @@ class Validate extends ImportResultController * Validate uploaded files action * * @return \Magento\Framework\Controller\ResultInterface + * @SuppressWarnings(PHPMD.Superglobals) */ public function execute() { @@ -42,12 +47,7 @@ public function execute() /** @var $import \Magento\ImportExport\Model\Import */ $import = $this->getImport()->setData($data); try { - $source = ImportAdapter::findAdapterFor( - $import->uploadSource(), - $this->_objectManager->create(\Magento\Framework\Filesystem::class) - ->getDirectoryWrite(DirectoryList::ROOT), - $data[$import::FIELD_FIELD_SEPARATOR] - ); + $source = $import->uploadFileAndGetSource(); $this->processValidationResult($import->validateSource($source), $resultBlock); } catch (\Magento\Framework\Exception\LocalizedException $e) { $resultBlock->addError($e->getMessage()); @@ -72,6 +72,7 @@ public function execute() * @param bool $validationResult * @param Result $resultBlock * @return void + * @throws \Magento\Framework\Exception\LocalizedException */ private function processValidationResult($validationResult, $resultBlock) { @@ -85,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); } @@ -99,6 +100,8 @@ private function processValidationResult($validationResult, $resultBlock) $errorAggregator->getErrorsCount() ) ); + + $this->addErrorMessages($resultBlock, $errorAggregator); } else { if ($errorAggregator->getErrorsCount()) { $this->collectErrors($resultBlock); @@ -109,6 +112,8 @@ private function processValidationResult($validationResult, $resultBlock) } /** + * Provides import model. + * * @return Import * @deprecated 100.1.0 */ @@ -128,6 +133,7 @@ private function getImport() * * @param Result $resultBlock * @return void + * @throws \Magento\Framework\Exception\LocalizedException */ private function addMessageToSkipErrors(Result $resultBlock) { @@ -148,6 +154,7 @@ private function addMessageToSkipErrors(Result $resultBlock) * * @param Result $resultBlock * @return void + * @throws \Magento\Framework\Exception\LocalizedException */ private function addMessageForValidResult(Result $resultBlock) { @@ -161,11 +168,12 @@ private function addMessageForValidResult(Result $resultBlock) /** * Collect errors and add error messages to Result block * - * Get all errors from ProcessingErrorAggregatorInterface and add appropriated error messages + * Get all errors from Error Aggregator and add appropriated error messages * to Result block. * * @param Result $resultBlock * @return void + * @throws \Magento\Framework\Exception\LocalizedException */ private function collectErrors(Result $resultBlock) { diff --git a/app/code/Magento/ImportExport/Helper/Report.php b/app/code/Magento/ImportExport/Helper/Report.php index 0c54a95a988f..012aeefd8bf9 100644 --- a/app/code/Magento/ImportExport/Helper/Report.php +++ b/app/code/Magento/ImportExport/Helper/Report.php @@ -52,9 +52,9 @@ public function __construct( */ public function getExecutionTime($time) { - $reportTime = $this->timeZone->date($time, $this->timeZone->getConfigTimezone()); + $reportTime = $this->timeZone->date($time); $timeDiff = $reportTime->diff($this->timeZone->date()); - return $timeDiff->format('%H:%M:%S'); + return $timeDiff->format('%H:%I:%S'); } /** @@ -97,6 +97,8 @@ public function getReportOutput($filename) } /** + * Get report absolute path. + * * @param string $fileName * @return string */ diff --git a/app/code/Magento/ImportExport/Model/Export.php b/app/code/Magento/ImportExport/Model/Export.php index 695df18fd103..850ded7c8f25 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 000000000000..27019780269c --- /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 000000000000..6dffc1827cfd --- /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 000000000000..e3cbd162aa5a --- /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 000000000000..b4adcdd62b66 --- /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/History.php b/app/code/Magento/ImportExport/Model/History.php index 3138f150d5fc..b85bf7da81a3 100644 --- a/app/code/Magento/ImportExport/Model/History.php +++ b/app/code/Magento/ImportExport/Model/History.php @@ -42,6 +42,11 @@ class History extends \Magento\Framework\Model\AbstractModel */ protected $reportHelper; + /** + * @var \Magento\Backend\Model\Auth\Session + */ + protected $session; + /** * Class constructor * @@ -293,6 +298,8 @@ public function setSummary($summary) } /** + * Load the last inserted item + * * @return $this */ public function loadLastInsertItem() diff --git a/app/code/Magento/ImportExport/Model/Import.php b/app/code/Magento/ImportExport/Model/Import.php index b5e8220e0e9b..2a4d6904b11b 100644 --- a/app/code/Magento/ImportExport/Model/Import.php +++ b/app/code/Magento/ImportExport/Model/Import.php @@ -7,9 +7,12 @@ namespace Magento\ImportExport\Model; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ObjectManager; use Magento\Framework\HTTP\Adapter\FileTransferFactory; +use Magento\Framework\Stdlib\DateTime\DateTime; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; +use Magento\Framework\Message\ManagerInterface; /** * Import model @@ -19,6 +22,7 @@ * @method string getBehavior() getBehavior() * @method \Magento\ImportExport\Model\Import setEntity() setEntity(string $value) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @since 100.0.2 */ class Import extends \Magento\ImportExport\Model\AbstractModel @@ -122,6 +126,11 @@ class Import extends \Magento\ImportExport\Model\AbstractModel */ protected $_importExportData = null; + /** + * @var \Magento\Framework\App\Config\ScopeConfigInterface + */ + private $_coreConfig; + /** * @var \Magento\ImportExport\Model\Import\ConfigInterface */ @@ -167,6 +176,21 @@ class Import extends \Magento\ImportExport\Model\AbstractModel */ protected $_filesystem; + /** + * @var History + */ + private $importHistoryModel; + + /** + * @var DateTime + */ + private $localeDate; + + /** + * @var ManagerInterface + */ + private $messageManager; + /** * @param \Psr\Log\LoggerInterface $logger * @param \Magento\Framework\Filesystem $filesystem @@ -181,8 +205,9 @@ class Import extends \Magento\ImportExport\Model\AbstractModel * @param Source\Import\Behavior\Factory $behaviorFactory * @param \Magento\Framework\Indexer\IndexerRegistry $indexerRegistry * @param History $importHistoryModel - * @param \Magento\Framework\Stdlib\DateTime\DateTime + * @param DateTime $localeDate * @param array $data + * @param ManagerInterface|null $messageManager * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -199,8 +224,9 @@ public function __construct( \Magento\ImportExport\Model\Source\Import\Behavior\Factory $behaviorFactory, \Magento\Framework\Indexer\IndexerRegistry $indexerRegistry, \Magento\ImportExport\Model\History $importHistoryModel, - \Magento\Framework\Stdlib\DateTime\DateTime $localeDate, - array $data = [] + DateTime $localeDate, + array $data = [], + ManagerInterface $messageManager = null ) { $this->_importExportData = $importExportData; $this->_coreConfig = $coreConfig; @@ -215,6 +241,7 @@ public function __construct( $this->_filesystem = $filesystem; $this->importHistoryModel = $importHistoryModel; $this->localeDate = $localeDate; + $this->messageManager = $messageManager ?: ObjectManager::getInstance()->get(ManagerInterface::class); parent::__construct($logger, $filesystem, $data); } @@ -268,6 +295,7 @@ protected function _getEntityAdapter() * * @param string $sourceFile Full path to source file * @return \Magento\ImportExport\Model\Import\AbstractSource + * @throws \Magento\Framework\Exception\FileSystemException */ protected function _getSourceAdapter($sourceFile) { @@ -283,12 +311,13 @@ protected function _getSourceAdapter($sourceFile) * * @param ProcessingErrorAggregatorInterface $validationResult * @return string[] + * @throws \Magento\Framework\Exception\LocalizedException */ public function getOperationResultMessages(ProcessingErrorAggregatorInterface $validationResult) { $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 @@ -379,6 +408,7 @@ public function getEntity() * Returns number of checked entities. * * @return int + * @throws \Magento\Framework\Exception\LocalizedException */ public function getProcessedEntitiesCount() { @@ -389,6 +419,7 @@ public function getProcessedEntitiesCount() * Returns number of checked rows. * * @return int + * @throws \Magento\Framework\Exception\LocalizedException */ public function getProcessedRowsCount() { @@ -443,6 +474,8 @@ public function importSource() } /** + * Process import. + * * @return bool * @throws \Magento\Framework\Exception\LocalizedException */ @@ -455,6 +488,7 @@ protected function processImport() * Import possibility getter. * * @return bool + * @throws \Magento\Framework\Exception\LocalizedException */ public function isImportAllowed() { @@ -462,6 +496,8 @@ public function isImportAllowed() } /** + * Get error aggregator instance. + * * @return ProcessingErrorAggregatorInterface * @throws \Magento\Framework\Exception\LocalizedException */ @@ -471,7 +507,7 @@ public function getErrorAggregator() } /** - * Move uploaded file and create source adapter instance. + * Move uploaded file. * * @throws \Magento\Framework\Exception\LocalizedException * @return string Source file path @@ -523,14 +559,27 @@ public function uploadSource() } $this->_removeBom($sourceFile); $this->createHistoryReport($sourceFileRelative, $entity, $extension, $result); - // trying to create source adapter for file and catch possible exception to be convinced in its adequacy + return $sourceFile; + } + + /** + * Move uploaded file and provide source instance. + * + * @return Import\AbstractSource + * @throws \Magento\Framework\Exception\FileSystemException + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function uploadFileAndGetSource() + { + $sourceFile = $this->uploadSource(); try { - $this->_getSourceAdapter($sourceFile); + $source = $this->_getSourceAdapter($sourceFile); } catch (\Exception $e) { - $this->_varDirectory->delete($sourceFileRelative); + $this->_varDirectory->delete($this->_varDirectory->getRelativePath($sourceFile)); throw new \Magento\Framework\Exception\LocalizedException(__($e->getMessage())); } - return $sourceFile; + + return $source; } /** @@ -538,6 +587,7 @@ public function uploadSource() * * @param string $sourceFile * @return $this + * @throws \Magento\Framework\Exception\FileSystemException */ protected function _removeBom($sourceFile) { @@ -557,6 +607,7 @@ protected function _removeBom($sourceFile) * * @param \Magento\ImportExport\Model\Import\AbstractSource $source * @return bool + * @throws \Magento\Framework\Exception\LocalizedException */ public function validateSource(\Magento\ImportExport\Model\Import\AbstractSource $source) { @@ -584,7 +635,7 @@ public function validateSource(\Magento\ImportExport\Model\Import\AbstractSource $messages = $this->getOperationResultMessages($errorAggregator); $this->addLogComment($messages); - $result = !$errorAggregator->getErrorsCount(); + $result = !$errorAggregator->isErrorLimitExceeded(); if ($result) { $this->addLogComment(__('Import data validation is complete.')); } @@ -595,6 +646,7 @@ public function validateSource(\Magento\ImportExport\Model\Import\AbstractSource * Invalidate indexes by process codes. * * @return $this + * @throws \Magento\Framework\Exception\LocalizedException */ public function invalidateIndex() { @@ -661,6 +713,7 @@ public function getEntityBehaviors() * ) * * @return array + * @throws \Magento\Framework\Exception\LocalizedException */ public function getUniqueEntityBehaviors() { @@ -710,9 +763,9 @@ public function isReportEntityType($entity = null) /** * Create history report * + * @param string $sourceFileRelative * @param string $entity * @param string $extension - * @param string $sourceFileRelative * @param array $result * @return $this * @throws \Magento\Framework\Exception\LocalizedException @@ -751,6 +804,7 @@ protected function createHistoryReport($sourceFileRelative, $entity, $extension * Get count of created items * * @return int + * @throws \Magento\Framework\Exception\LocalizedException */ public function getCreatedItemsCount() { @@ -761,6 +815,7 @@ public function getCreatedItemsCount() * Get count of updated items * * @return int + * @throws \Magento\Framework\Exception\LocalizedException */ public function getUpdatedItemsCount() { @@ -771,6 +826,7 @@ public function getUpdatedItemsCount() * Get count of deleted items * * @return int + * @throws \Magento\Framework\Exception\LocalizedException */ public function getDeletedItemsCount() { diff --git a/app/code/Magento/ImportExport/Model/Import/Entity/AbstractEntity.php b/app/code/Magento/ImportExport/Model/Import/Entity/AbstractEntity.php index e965e8ad207f..10b29a50a406 100644 --- a/app/code/Magento/ImportExport/Model/Import/Entity/AbstractEntity.php +++ b/app/code/Magento/ImportExport/Model/Import/Entity/AbstractEntity.php @@ -408,7 +408,9 @@ protected function _saveValidatedBunches() if ($source->valid()) { try { $rowData = $source->current(); - $skuSet[$rowData['sku']] = true; + if (array_key_exists('sku', $rowData)) { + $skuSet[$rowData['sku']] = true; + } } catch (\InvalidArgumentException $e) { $this->addRowError($e->getMessage(), $this->_processedRowsCount); $this->_processedRowsCount++; @@ -436,7 +438,7 @@ protected function _saveValidatedBunches() $source->next(); } } - $this->_processedEntitiesCount = count($skuSet); + $this->_processedEntitiesCount = (count($skuSet)) ? : $this->_processedRowsCount; return $this; } @@ -554,7 +556,9 @@ public function getBehavior() $this->_parameters['behavior'] ) || $this->_parameters['behavior'] != ImportExport::BEHAVIOR_APPEND && + $this->_parameters['behavior'] != ImportExport::BEHAVIOR_ADD_UPDATE && $this->_parameters['behavior'] != ImportExport::BEHAVIOR_REPLACE && + $this->_parameters['behavior'] != ImportExport::BEHAVIOR_CUSTOM && $this->_parameters['behavior'] != ImportExport::BEHAVIOR_DELETE ) { return ImportExport::getDefaultBehavior(); @@ -828,6 +832,8 @@ public function validateData() } /** + * Get error aggregator object + * * @return ProcessingErrorAggregatorInterface */ public function getErrorAggregator() diff --git a/app/code/Magento/ImportExport/Model/Import/ErrorProcessing/ProcessingErrorAggregator.php b/app/code/Magento/ImportExport/Model/Import/ErrorProcessing/ProcessingErrorAggregator.php index 7f49e2022c41..028bf2c464d4 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/Model/Import/Source/Zip.php b/app/code/Magento/ImportExport/Model/Import/Source/Zip.php index b7fafc43ca4e..6fa87ab5d5c4 100644 --- a/app/code/Magento/ImportExport/Model/Import/Source/Zip.php +++ b/app/code/Magento/ImportExport/Model/Import/Source/Zip.php @@ -5,6 +5,8 @@ */ namespace Magento\ImportExport\Model\Import\Source; +use Magento\Framework\Exception\ValidatorException; + /** * Zip import adapter. */ @@ -14,6 +16,8 @@ class Zip extends Csv * @param string $file * @param \Magento\Framework\Filesystem\Directory\Write $directory * @param string $options + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\ValidatorException */ public function __construct( $file, @@ -21,10 +25,14 @@ public function __construct( $options ) { $zip = new \Magento\Framework\Archive\Zip(); - $file = $zip->unpack( - $directory->getRelativePath($file), - $directory->getRelativePath(preg_replace('/\.zip$/i', '.csv', $file)) + $csvFile = $zip->unpack( + $file, + preg_replace('/\.zip$/i', '.csv', $file) ); - parent::__construct($file, $directory, $options); + if (!$csvFile) { + throw new ValidatorException(__('Sorry, but the data is invalid or the file is not uploaded.')); + } + $directory->delete($directory->getRelativePath($file)); + parent::__construct($csvFile, $directory, $options); } } diff --git a/app/code/Magento/ImportExport/Model/Source/Import/Behavior/Basic.php b/app/code/Magento/ImportExport/Model/Source/Import/Behavior/Basic.php index f28aae9f8ae1..e0002474e191 100644 --- a/app/code/Magento/ImportExport/Model/Source/Import/Behavior/Basic.php +++ b/app/code/Magento/ImportExport/Model/Source/Import/Behavior/Basic.php @@ -5,6 +5,8 @@ */ namespace Magento\ImportExport\Model\Source\Import\Behavior; +use Magento\ImportExport\Model\Import; + /** * Import behavior source model used for defining the behaviour during the import. * @@ -14,19 +16,19 @@ class Basic extends \Magento\ImportExport\Model\Source\Import\AbstractBehavior { /** - * {@inheritdoc} + * @inheritdoc */ public function toArray() { return [ - \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND => __('Add/Update'), - \Magento\ImportExport\Model\Import::BEHAVIOR_REPLACE => __('Replace'), - \Magento\ImportExport\Model\Import::BEHAVIOR_DELETE => __('Delete') + Import::BEHAVIOR_APPEND => __('Add/Update'), + Import::BEHAVIOR_REPLACE => __('Replace'), + Import::BEHAVIOR_DELETE => __('Delete') ]; } /** - * {@inheritdoc} + * @inheritdoc */ public function getCode() { @@ -34,12 +36,23 @@ public function getCode() } /** - * {@inheritdoc} + * @inheritdoc */ public function getNotes($entityCode) { $messages = ['catalog_product' => [ - \Magento\ImportExport\Model\Import::BEHAVIOR_REPLACE => __("Note: Product IDs will be regenerated.") + Import::BEHAVIOR_APPEND => __( + "New product data is added to the existing product data for the existing entries in the database. " + . "All fields except sku can be updated." + ), + Import::BEHAVIOR_REPLACE => __( + "The existing product data is replaced with new data. <b>Exercise caution when replacing data " + . "because the existing product data will be completely cleared and all references " + . "in the system will be lost.</b>" + ), + Import::BEHAVIOR_DELETE => __( + "Any entities in the import data that already exist in the database are deleted from the database." + ), ]]; return isset($messages[$entityCode]) ? $messages[$entityCode] : []; } diff --git a/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminImportProductsActionGroup.xml b/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminImportProductsActionGroup.xml new file mode 100644 index 000000000000..a9100b4730b8 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminImportProductsActionGroup.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminImportProductsActionGroup"> + <arguments> + <argument name="behavior" type="string"/> + <argument name="importFile" type="string"/> + <argument name="importMessage" type="string"/> + </arguments> + <amOnPage url="{{AdminImportIndexPage.url}}" stepKey="goToImportIndexPage"/> + <waitForPageLoad stepKey="AdminImportMainSectionLoad"/> + <selectOption selector="{{AdminImportMainSection.entityType}}" userInput="Products" stepKey="selectProductsOption"/> + <waitForElementVisible selector="{{AdminImportMainSection.importBehavior}}" stepKey="waitForImportBehaviorElementVisible"/> + <selectOption selector="{{AdminImportMainSection.importBehavior}}" userInput="{{behavior}}" stepKey="selectImportOption"/> + <attachFile selector="{{AdminImportMainSection.selectFileToImport}}" userInput="{{importFile}}" stepKey="attachFileForImport"/> + <click selector="{{AdminImportHeaderSection.checkDataButton}}" stepKey="clickCheckDataButton"/> + <click selector="{{AdminImportMainSection.importButton}}" stepKey="clickImportButton"/> + <waitForPageLoad stepKey="AdminImportMainSectionLoad2"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="Import successfully done" stepKey="assertSuccessMessage"/> + <waitForPageLoad stepKey="AdminMessagesSection"/> + <see selector="{{AdminMessagesSection.notice}}" userInput="{{importMessage}}" stepKey="seeImportMessage"/> + </actionGroup> + </actionGroups> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/ImportExport/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 000000000000..c09cd192d05c --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminMenuSystemDataTransferExport"> + <data key="pageTitle">Export</data> + <data key="title">Export</data> + <data key="dataUiId">magento-importexport-system-convert-export</data> + </entity> + <entity name="AdminMenuSystemDataTransferImport"> + <data key="pageTitle">Import</data> + <data key="title">Import</data> + <data key="dataUiId">magento-importexport-system-convert-import</data> + </entity> +</entities> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Page/AdminExportIndexPage.xml b/app/code/Magento/ImportExport/Test/Mftf/Page/AdminExportIndexPage.xml new file mode 100644 index 000000000000..55ed3edd9bc7 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Page/AdminExportIndexPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminExportIndexPage" url="admin/export/" area="admin" module="Magento_ImportExport"> + <section name="AdminExportMainSection"/> + </page> +</pages> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Page/AdminImportIndexPage.xml b/app/code/Magento/ImportExport/Test/Mftf/Page/AdminImportIndexPage.xml new file mode 100644 index 000000000000..87807eb9b0e8 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Page/AdminImportIndexPage.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminImportIndexPage" url="admin/import/" area="admin" module="Magento_ImportExport"> + <section name="AdminImportHeaderSection"/> + <section name="AdminImportMainSection"/> + </page> +</pages> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Section/AdminExportAttributeSection.xml b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminExportAttributeSection.xml new file mode 100644 index 000000000000..528ad23aaf2b --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminExportAttributeSection.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminExportAttributeSection"> + <element name="filterByAttributeCode" type="input" selector="#export_filter_grid_filter_attribute_code"/> + <element name="resetFilter" type="button" selector="button[data-action='grid-filter-reset']" timeout="30"/> + <element name="search" type="button" selector="button[data-action=grid-filter-apply]" timeout="30"/> + <element name="chooseAttribute" type="checkbox" selector="//*[@name='export_filter[{{var}}]']/ancestor::tr//input[@type='checkbox']" parameterized="true"/> + <element name="fillFilter" type="input" selector="//*[@name='export_filter[{{var}}]']/ancestor::tr//input[@type='text']" parameterized="true"/> + <element name="continueBtn" type="button" selector="//*[@id='export_filter_container']/button" timeout="30"/> + <element name="selectByIndex" type="button" selector="//tr[@data-repeat-index='{{var}}']//button" parameterized="true" timeout="30"/> + <element name="download" type="button" selector="//tr[@data-repeat-index='{{var}}']//a[text()='Download']" parameterized="true" timeout="30"/> + <element name="delete" type="button" selector="//tr[@data-repeat-index='{{var}}']//a[text()='Delete']" parameterized="true" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Section/AdminExportMainSection.xml b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminExportMainSection.xml new file mode 100644 index 000000000000..da1d928607e7 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminExportMainSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminExportMainSection"> + <element name="entityType" type="select" selector="#entity"/> + <element name="entityAttributes" type="select" selector="#export_filter_form"/> + </section> +</sections> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportHeaderSection.xml b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportHeaderSection.xml new file mode 100644 index 000000000000..748580be0940 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportHeaderSection.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="AdminImportHeaderSection"> + <element name="checkDataButton" type="button" selector="#upload_button" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportMainSection.xml b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportMainSection.xml new file mode 100644 index 000000000000..2ce6b1e35777 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportMainSection.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="AdminImportMainSection"> + <element name="entityType" type="select" selector="#entity"/> + <element name="importBehavior" type="select" selector="#basic_behavior"/> + <element name="selectFileToImport" type="input" selector="#import_file"/> + <element name="importButton" type="button" selector="#import_validation_container button" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminExportPageNavigateMenuTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminExportPageNavigateMenuTest.xml new file mode 100644 index 000000000000..e8fb12ca521c --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminExportPageNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminExportPageNavigateMenuTest"> + <annotations> + <features value="ImportExport"/> + <stories value="Menu Navigation"/> + <title value="Admin export page navigate menu test"/> + <description value="Admin should be able to navigate to System > Export"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14157"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToExportPage"> + <argument name="menuUiId" value="{{AdminMenuSystem.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSystemDataTransferExport.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSystemDataTransferExport.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithAddUpdateBehaviorTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithAddUpdateBehaviorTest.xml new file mode 100644 index 000000000000..ceb4e93e4e9a --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithAddUpdateBehaviorTest.xml @@ -0,0 +1,123 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminImportProductsWithAddUpdateBehaviorTest"> + <annotations> + <description value="Verify Magento native import products with add/update behavior."/> + <stories value="Import Products"/> + <features value="Import/Export"/> + <title value="Verify Magento native import products with add/update behavior."/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14077"/> + <group value="importExport"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create Simple Product1 --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct1"> + <field key="name">SimpleProductForTest1</field> + <field key="sku">SimpleProductForTest1</field> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Login as Admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Create Website --> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="AdminCreateWebsite"> + <argument name="newWebsiteName" value="secondWebsite"/> + <argument name="websiteCode" value="second_website"/> + </actionGroup> + + <!-- Create store group --> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="AdminCreateStore"> + <argument name="website" value="secondWebsite"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> + </actionGroup> + + <!-- Create store view --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="AdminCreateStoreView"> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStore"/> + </actionGroup> + </before> + <after> + <!-- Delete all products that replaced products in the before block post import --> + <deleteData stepKey="deleteSimpleProduct1" url="/V1/products/SimpleProductForTest1"/> + <deleteData stepKey="deleteSimpleProduct2" url="/V1/products/SimpleProductForTest2"/> + <deleteData stepKey="deleteSimpleProduct3" url="/V1/products/SimpleProductForTest3"/> + + <!-- Delete category created in the before block --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!--Delete website created in the before block --> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="DeleteWebsite" before="logoutFromAdmin"> + <argument name="websiteName" value="secondWebsite"/> + </actionGroup> + + <!-- Logout --> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + + <!-- Import products with add/update behavior --> + <actionGroup ref="AdminImportProductsActionGroup" stepKey="adminImportProducts"> + <argument name="behavior" value="Add/Update"/> + <argument name="importFile" value="catalog_import_products.csv"/> + <argument name="importMessage" value="Created: 2, Updated: 1, Deleted: 0"/> + </actionGroup> + + <!-- Assert Simple Product1 on grid--> + <actionGroup ref="AssertProductOnAdminGridActionGroup" stepKey="assertSimpleProduct1OnAdminGrid"> + <argument name="product" value="SimpleProductAfterImport1"/> + </actionGroup> + + <!-- Assert Simple Product1 on edit page--> + <actionGroup ref="AssertProductInfoOnEditPageActionGroup" stepKey="assertSimpleProduct1OnEditPage"> + <argument name="product" value="SimpleProductAfterImport1"/> + </actionGroup> + + <!-- Assert Simple Product2 on grid--> + <actionGroup ref="AssertProductOnAdminGridActionGroup" stepKey="assertSimpleProduct2OnAdminGrid"> + <argument name="product" value="SimpleProductAfterImport2"/> + </actionGroup> + + <!-- Assert Simple Product2 on edit page--> + <actionGroup ref="AssertProductInfoOnEditPageActionGroup" stepKey="assertSimpleProduct2OnEditPage"> + <argument name="product" value="SimpleProductAfterImport2"/> + </actionGroup> + + <!-- Assert Simple Product3 on grid--> + <actionGroup ref="AssertProductOnAdminGridActionGroup" stepKey="assertSimpleProduct3OnAdminGrid"> + <argument name="product" value="SimpleProductAfterImport3"/> + </actionGroup> + + <!-- Assert Simple Product3 on edit page--> + <actionGroup ref="AssertProductInfoOnEditPageActionGroup" stepKey="assertSimpleProduct3OnEditPage"> + <argument name="product" value="SimpleProductAfterImport3"/> + </actionGroup> + + <!-- Assert SimpleProduct1 on store front--> + <actionGroup ref="StoreFrontProductValidationActionGroup" stepKey="storeFrontSimpleProduct1Validation"> + <argument name="product" value="SimpleProductAfterImport1"/> + </actionGroup> + + <!-- Assert SimpleProduct2 on store front--> + <actionGroup ref="StoreFrontProductValidationActionGroup" stepKey="storeFrontSimpleProduct2Validation"> + <argument name="product" value="SimpleProductAfterImport2"/> + </actionGroup> + + <!-- Assert SimpleProduct3 on store front--> + <actionGroup ref="StoreFrontProductValidationActionGroup" stepKey="storeFrontSimpleProduct3Validation"> + <argument name="product" value="SimpleProductAfterImport3"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithDeleteBehaviorTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithDeleteBehaviorTest.xml new file mode 100644 index 000000000000..4cbb0603d907 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithDeleteBehaviorTest.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="AdminImportProductsWithDeleteBehaviorTest"> + <annotations> + <description value="Verify Magento native import products with delete behavior."/> + <stories value="Verify Magento native import products with delete behavior."/> + <features value="Import/Export"/> + <title value="Verify Magento native import products with delete behavior."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-30587"/> + <group value="importExport"/> + </annotations> + <before> + <!--Create Simple product--> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"> + <field key="name">Simple Product for Test</field> + <field key="sku">Simple Product for Test</field> + </createData> + <!--Create Virtual product--> + <createData entity="VirtualProduct" stepKey="createVirtualProduct"> + <field key="name">Virtual Product for Test</field> + <field key="sku">Virtual Product for Test</field> + </createData> + <!-- Create Downloadable product --> + <createData entity="ApiDownloadableProduct" stepKey="createDownloadableProduct"> + <field key="name">Api Downloadable Product for Test</field> + <field key="sku">Api Downloadable Product for Test</field> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductFilters"/> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + <amOnPage url="{{AdminImportIndexPage.url}}" stepKey="goToImportIndexPage"/> + <selectOption selector="{{AdminImportMainSection.entityType}}" userInput="Products" stepKey="selectProductsOption"/> + <waitForElementVisible selector="{{AdminImportMainSection.importBehavior}}" stepKey="waitForImportBehaviorElementVisible"/> + <selectOption selector="{{AdminImportMainSection.importBehavior}}" userInput="Delete" stepKey="selectDeleteOption"/> + <attachFile selector="{{AdminImportMainSection.selectFileToImport}}" userInput="catalog_products.csv" stepKey="attachFileForImport"/> + <click selector="{{AdminImportHeaderSection.checkDataButton}}" stepKey="clickCheckDataButton"/> + <click selector="{{AdminImportMainSection.importButton}}" stepKey="clickImportButton"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="Import successfully done" stepKey="assertSuccessMessage"/> + <see selector="{{AdminMessagesSection.notice}}" userInput="Created: 0, Updated: 0, Deleted: 3" stepKey="assertNotice"/> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchSimpleProductOnBackend"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage"/> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchVirtualProductOnBackend"> + <argument name="product" value="$$createVirtualProduct$$"/> + </actionGroup> + <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage1"/> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchDownloadableProductOnBackend"> + <argument name="product" value="$$createDownloadableProduct$$"/> + </actionGroup> + <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage2"/> + </test> +</tests> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithReplaceBehaviorTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithReplaceBehaviorTest.xml new file mode 100644 index 000000000000..d63a5546716b --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithReplaceBehaviorTest.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminImportProductsWithReplaceBehaviorTest" extends="AdminImportProductsWithAddUpdateBehaviorTest"> + <annotations> + <description value="Verify Magento native import products with replace behavior."/> + <stories value="Import Products"/> + <features value="Import/Export"/> + <title value="Verify Magento native import products with replace behavior."/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14076"/> + <group value="importExport"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create Simple Product2 --> + <createData entity="SimpleProduct" stepKey="createSimpleProduct2"> + <field key="name">SimpleProductForTest2</field> + <field key="sku">SimpleProductForTest2</field> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Create Simple Product3 --> + <createData entity="SimpleProduct" stepKey="createSimpleProduct3"> + <field key="name">SimpleProductForTest3</field> + <field key="sku">SimpleProductForTest3</field> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + + <!-- Import products with replace behavior --> + <actionGroup ref="AdminImportProductsActionGroup" stepKey="adminImportProducts"> + <argument name="behavior" value="Replace"/> + <argument name="importFile" value="catalog_import_products.csv"/> + <argument name="importMessage" value="Created: 3, Updated: 0, Deleted: 3"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminSystemImportNavigateMenuTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminSystemImportNavigateMenuTest.xml new file mode 100644 index 000000000000..9913933d857a --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminSystemImportNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminSystemImportNavigateMenuTest"> + <annotations> + <features value="ImportExport"/> + <stories value="Menu Navigation"/> + <title value="Admin system import navigate menu test"/> + <description value="Admin should be able to navigate to System > Import"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14156"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToImportPage"> + <argument name="menuUiId" value="{{AdminMenuSystem.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSystemDataTransferImport.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSystemDataTransferImport.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/ImportExport/Test/Unit/Helper/ReportTest.php b/app/code/Magento/ImportExport/Test/Unit/Helper/ReportTest.php index 8e7f11984c52..679b726d8e52 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Helper/ReportTest.php +++ b/app/code/Magento/ImportExport/Test/Unit/Helper/ReportTest.php @@ -89,12 +89,17 @@ protected function setUp() */ public function testGetExecutionTime() { - $time = '01:02:03'; - $this->timezone->expects($this->any())->method('date')->willReturnSelf(); - $this->timezone->expects($this->any())->method('getConfigTimezone')->willReturn('America/Los_Angeles'); - $this->timezone->expects($this->any())->method('diff')->willReturnSelf(); - $this->timezone->expects($this->any())->method('format')->willReturn($time); - $this->assertEquals($time, $this->report->getExecutionTime($time)); + $startDate = '2000-01-01 01:01:01'; + $endDate = '2000-01-01 02:03:04'; + $executionTime = '01:02:03'; + + $startDateMock = $this->createTestProxy(\DateTime::class, ['time' => $startDate]); + $endDateMock = $this->createTestProxy(\DateTime::class, ['time' => $endDate]); + $this->timezone->method('date') + ->withConsecutive([$startDate], []) + ->willReturnOnConsecutiveCalls($startDateMock, $endDateMock); + + $this->assertEquals($executionTime, $this->report->getExecutionTime($startDate)); } /** @@ -159,6 +164,9 @@ public function testImportFileExistsException($fileName) $this->report->importFileExists($fileName); } + /** + * Test importFileExists() + */ public function testImportFileExists() { $this->assertEquals($this->report->importFileExists('..file..name'), true); 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 288a99770974..179f3f3cadab 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 357b35e8a313..409c1af9cb38 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 c913b53e8b53..c7b06a8731f0 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 a4c380d51ebe..722cca4af6d4 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 @@ -191,7 +191,7 @@ public function testInitValidationStrategyExceed() } /** - * Test for method initValidationStrategy. Expected exeption due null incoming parameter + * Test for method initValidationStrategy. Expected exception due null incoming parameter */ public function testInitValidationStrategyException() { @@ -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'); @@ -316,7 +317,7 @@ public function testAddTheSameErrorTwice() } /** - * Test for method getErrorsByCode. Expects receive errors with code, which present in incomming parameter. + * Test for method getErrorsByCode. Expects receive errors with code, which present in incoming parameter. */ public function testGetErrorsByCodeInArray() { @@ -328,7 +329,7 @@ public function testGetErrorsByCodeInArray() } /** - * Test for method getErrorsByCode. Unexpects receive errors with code, which present in incomming parameter. + * Test for method getErrorsByCode. Unexpects receive errors with code, which present in incoming parameter. */ public function testGetErrorsByCodeNotInArray() { diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Source/Import/Behavior/CustomTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/Source/Import/Behavior/CustomTest.php index d073f3866bfe..73b4f10b3b0f 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Model/Source/Import/Behavior/CustomTest.php +++ b/app/code/Magento/ImportExport/Test/Unit/Model/Source/Import/Behavior/CustomTest.php @@ -32,7 +32,7 @@ class CustomTest extends \Magento\ImportExport\Test\Unit\Model\Source\Import\Abs protected function setUp() { parent::setUp(); - $this->_model = new \Magento\ImportExport\Model\Source\Import\Behavior\Custom([]); + $this->_model = new \Magento\ImportExport\Model\Source\Import\Behavior\Custom(); } /** 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 000000000000..a7b9b072f00f --- /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 000000000000..e64a6df430ea --- /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 b0ba04f5aa0e..6363722eba7c 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 03c24c7b2bf6..04ee72634912 100644 --- a/app/code/Magento/ImportExport/etc/adminhtml/di.xml +++ b/app/code/Magento/ImportExport/etc/adminhtml/di.xml @@ -6,9 +6,24 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> - <type name="\Magento\ImportExport\Controller\Adminhtml\Import\Start"> + <type name="Magento\ImportExport\Controller\Adminhtml\Import\Start"> <arguments> <argument name="exceptionMessageFactory" xsi:type="object">Magento\Framework\Message\ExceptionMessageLookupFactory</argument> </arguments> </type> + <type name="Magento\ImportExport\Model\Export\Entity\ExportInfoFactory"> + <arguments> + <argument name="serializer" xsi:type="object">Magento\Framework\Serialize\Serializer\Json</argument> + </arguments> + </type> + <type name="Magento\ImportExport\Controller\Adminhtml\Export\File\Delete"> + <arguments> + <argument name="file" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <type name="Magento\ImportExport\Ui\DataProvider\ExportFileDataProvider"> + <arguments> + <argument name="file" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/ImportExport/etc/communication.xml b/app/code/Magento/ImportExport/etc/communication.xml new file mode 100644 index 000000000000..7794b3e5ab24 --- /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/db_schema.xml b/app/code/Magento/ImportExport/etc/db_schema.xml index 01f20daabf0f..df4513184851 100644 --- a/app/code/Magento/ImportExport/etc/db_schema.xml +++ b/app/code/Magento/ImportExport/etc/db_schema.xml @@ -12,7 +12,7 @@ <column xsi:type="varchar" name="entity" nullable="false" length="50" comment="Entity"/> <column xsi:type="varchar" name="behavior" nullable="false" length="10" default="append" comment="Behavior"/> <column xsi:type="longtext" name="data" nullable="true" comment="Data"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> </constraint> </table> @@ -27,7 +27,7 @@ <column xsi:type="varchar" name="execution_time" nullable="true" length="255" comment="Execution time"/> <column xsi:type="varchar" name="summary" nullable="true" length="255" comment="Summary"/> <column xsi:type="varchar" name="error_file" nullable="false" length="255" comment="Imported file with errors"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="history_id"/> </constraint> </table> diff --git a/app/code/Magento/ImportExport/etc/di.xml b/app/code/Magento/ImportExport/etc/di.xml index 36c76022a41c..909b526e4790 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 65728a9be5c6..f62dbc891ef0 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 aefa6541d7e1..e73038ebc071 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 000000000000..7eb96819faf1 --- /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 000000000000..2c6612ac0ef1 --- /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 000000000000..097b60bee153 --- /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 000000000000..f77c13e2ba05 --- /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 60c9ee4c5a96..d7680a71ac5f 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" @@ -119,3 +120,6 @@ User,User "Error File","Error File" "Execution Time","Execution Time" Summary,Summary +"New product data is added to existing product data entries in the database. All fields except SKU can be updated.","New product data is added to existing product data entries in the database. All fields except SKU can be updated." +"All existing product data is replaced with the imported new data. <b>Exercise caution when replacing data. All existing product data will be completely cleared and all references in the system will be lost.</b>","All existing product data is replaced with the imported new data. <b>Exercise caution when replacing data. All existing product data will be completely cleared and all references in the system will be lost.</b>" +"Any entities in the import data that match existing entities in the database are deleted from the database.","Any entities in the import data that match existing entities in the database are deleted from the database." 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 684865097930..b60fb40bfbd8 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/layout/adminhtml_history_grid_block.xml b/app/code/Magento/ImportExport/view/adminhtml/layout/adminhtml_history_grid_block.xml index 02fc198cb0ad..51e5827c8ab9 100644 --- a/app/code/Magento/ImportExport/view/adminhtml/layout/adminhtml_history_grid_block.xml +++ b/app/code/Magento/ImportExport/view/adminhtml/layout/adminhtml_history_grid_block.xml @@ -11,7 +11,7 @@ <block class="Magento\Backend\Block\Widget\Grid" name="adminhtml.import.history.grid" as="grid"> <arguments> <argument name="id" xsi:type="string">importHistoryGrid</argument> - <argument name="dataSource" xsi:type="object">Magento\ImportExport\Model\ResourceModel\History\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\ImportExport\Model\ResourceModel\History\Collection</argument> <argument name="default_sort" xsi:type="string">history_id</argument> <argument name="default_dir" xsi:type="string">desc</argument> </arguments> 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 000000000000..2b160bc9f6f4 --- /dev/null +++ b/app/code/Magento/ImportExport/view/adminhtml/ui_component/export_grid.xml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <argument name="data" xsi:type="array"> + <item name="js_config" xsi:type="array"> + <item name="provider" xsi:type="string">export_grid.export_grid_data_source</item> + </item> + </argument> + <settings> + <deps> + <dep>export_grid.export_grid_data_source</dep> + </deps> + <spinner>export_grid_columns</spinner> + </settings> + <dataSource name="export_grid_data_source" component="Magento_Ui/js/grid/provider"> + <settings> + <storageConfig> + <param name="indexField" xsi:type="string">file_name</param> + </storageConfig> + <updateUrl path="mui/index/render"/> + </settings> + <aclResource>Magento_ImportExport::export</aclResource> + <dataProvider class="Magento\ImportExport\Ui\DataProvider\ExportFileDataProvider" name="export_grid_data_source"> + <settings> + <requestFieldName>file_name</requestFieldName> + <primaryFieldName>file_name</primaryFieldName> + </settings> + </dataProvider> + </dataSource> + <columns name="export_grid_columns"> + <column name="file_name"> + <settings> + <sortable>false</sortable> + <label translate="true">File name</label> + </settings> + </column> + <actionsColumn name="actions" class="Magento\ImportExport\Ui\Component\Columns\ExportGridActions"> + <settings> + <indexField>file_name</indexField> + </settings> + </actionsColumn> + </columns> +</listing> \ No newline at end of file diff --git a/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php b/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php index 6eab92f65117..fffa4503e14a 100644 --- a/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php +++ b/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php @@ -58,7 +58,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ protected function configure() { @@ -70,7 +70,7 @@ protected function configure() } /** - * {@inheritdoc} + * @inheritdoc */ protected function execute(InputInterface $input, OutputInterface $output) { @@ -105,7 +105,9 @@ protected function execute(InputInterface $input, OutputInterface $output) } /** - * {@inheritdoc} Returns the ordered list of specified indexers and related indexers. + * @inheritdoc + * + * Returns the ordered list of specified indexers and related indexers. */ protected function getIndexers(InputInterface $input) { @@ -187,7 +189,7 @@ private function getDependentIndexerIds(string $indexerId) $this->getDependentIndexerIds($id) ); } - }; + } return array_unique($dependentIndexerIds); } @@ -272,6 +274,8 @@ private function getConfig() } /** + * Get indexer registry + * * @return IndexerRegistry * @deprecated 100.2.0 */ @@ -284,6 +288,8 @@ private function getIndexerRegistry() } /** + * Get dependency info provider + * * @return DependencyInfoProvider * @deprecated 100.2.0 */ diff --git a/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/ListAction.php b/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/ListAction.php index 02e600dbd939..35cefbcda43e 100644 --- a/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/ListAction.php +++ b/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/ListAction.php @@ -6,7 +6,9 @@ */ namespace Magento\Indexer\Controller\Adminhtml\Indexer; -class ListAction extends \Magento\Indexer\Controller\Adminhtml\Indexer +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class ListAction extends \Magento\Indexer\Controller\Adminhtml\Indexer implements HttpGetActionInterface { /** * Display processes grid action diff --git a/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassChangelog.php b/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassChangelog.php index b4a4d9f06ae4..3dffd514218d 100644 --- a/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassChangelog.php +++ b/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassChangelog.php @@ -6,7 +6,9 @@ */ namespace Magento\Indexer\Controller\Adminhtml\Indexer; -class MassChangelog extends \Magento\Indexer\Controller\Adminhtml\Indexer +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; + +class MassChangelog extends \Magento\Indexer\Controller\Adminhtml\Indexer implements HttpPostActionInterface { /** * Turn mview on for the given indexers diff --git a/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassOnTheFly.php b/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassOnTheFly.php index 7ace4a64d382..9f7faff8843a 100644 --- a/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassOnTheFly.php +++ b/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassOnTheFly.php @@ -6,7 +6,9 @@ */ namespace Magento\Indexer\Controller\Adminhtml\Indexer; -class MassOnTheFly extends \Magento\Indexer\Controller\Adminhtml\Indexer +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; + +class MassOnTheFly extends \Magento\Indexer\Controller\Adminhtml\Indexer implements HttpPostActionInterface { /** * Turn mview off for the given indexers diff --git a/app/code/Magento/Indexer/Model/DimensionModes.php b/app/code/Magento/Indexer/Model/DimensionModes.php index bbdee0ced866..a9507a6f4d35 100644 --- a/app/code/Magento/Indexer/Model/DimensionModes.php +++ b/app/code/Magento/Indexer/Model/DimensionModes.php @@ -26,7 +26,7 @@ public function __construct(array $dimensions) $result = []; foreach ($dimensions as $dimension) { $result[$dimension->getName()] = $dimension; - }; + } return $result; })(...$dimensions); } diff --git a/app/code/Magento/Indexer/Model/Indexer.php b/app/code/Magento/Indexer/Model/Indexer.php index 87a7cce58e1a..2821a46f2941 100644 --- a/app/code/Magento/Indexer/Model/Indexer.php +++ b/app/code/Magento/Indexer/Model/Indexer.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Indexer\Model; use Magento\Framework\Indexer\ActionFactory; @@ -14,6 +15,8 @@ use Magento\Framework\Indexer\StructureFactory; /** + * Indexer model. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Indexer extends \Magento\Framework\DataObject implements IndexerInterface @@ -361,7 +364,7 @@ public function getLatestUpdated() return $this->getView()->getUpdated(); } } - return $this->getState()->getUpdated(); + return $this->getState()->getUpdated() ?: ''; } /** diff --git a/app/code/Magento/Indexer/Model/Message/Invalid.php b/app/code/Magento/Indexer/Model/Message/Invalid.php index 5a3f879b0ad8..79f9fcef9641 100644 --- a/app/code/Magento/Indexer/Model/Message/Invalid.php +++ b/app/code/Magento/Indexer/Model/Message/Invalid.php @@ -6,6 +6,9 @@ namespace Magento\Indexer\Model\Message; +/** + * Message about invalid indexers. + */ class Invalid implements \Magento\Framework\Notification\MessageInterface { /** @@ -71,7 +74,7 @@ public function getText() return __( 'One or more <a href="%1">indexers are invalid</a>. Make sure your <a href="%2" target="_blank">Magento cron job</a> is running.', $url, - 'http://devdocs.magento.com/guides/v2.2/config-guide/cli/config-cli-subcommands-cron.html#create-or-remove-the-magento-crontab' + 'https://devdocs.magento.com/guides/v2.2/config-guide/cli/config-cli-subcommands-cron.html#create-or-remove-the-magento-crontab' ); //@codingStandardsIgnoreEnd } diff --git a/app/code/Magento/Indexer/Model/ProcessManager.php b/app/code/Magento/Indexer/Model/ProcessManager.php index 04cd713fffb1..2f2c500e028c 100644 --- a/app/code/Magento/Indexer/Model/ProcessManager.php +++ b/app/code/Magento/Indexer/Model/ProcessManager.php @@ -71,6 +71,7 @@ public function execute($userFunctions) private function simpleThreadExecute($userFunctions) { foreach ($userFunctions as $userFunction) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction call_user_func($userFunction); } } @@ -79,6 +80,7 @@ private function simpleThreadExecute($userFunctions) * Execute user functions in multiThreads mode * * @param \Traversable $userFunctions + * @throws \RuntimeException * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ private function multiThreadsExecute($userFunctions) @@ -86,6 +88,7 @@ private function multiThreadsExecute($userFunctions) $this->resource->closeConnection(null); $threadNumber = 0; foreach ($userFunctions as $userFunction) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $pid = pcntl_fork(); if ($pid == -1) { throw new \RuntimeException('Unable to fork a new process'); @@ -95,6 +98,7 @@ private function multiThreadsExecute($userFunctions) $this->startChildProcess($userFunction); } } + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock,Magento2.Functions.DiscouragedFunction while (pcntl_waitpid(0, $status) != -1) { //Waiting for the completion of child processes } @@ -128,12 +132,13 @@ private function isSetupMode(): bool * Start child process * * @param callable $userFunction - * @SuppressWarnings(PHPMD.ExitExpression) */ private function startChildProcess(callable $userFunction) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $status = call_user_func($userFunction); - $status = is_integer($status) ? $status : 0; + $status = is_int($status) ? $status : 0; + // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage exit($status); } @@ -146,8 +151,10 @@ private function executeParentProcess(int &$threadNumber) { $threadNumber++; if ($threadNumber >= $this->threadsCount) { + // phpcs:disable Magento2.Functions.DiscouragedFunction pcntl_wait($status); if (pcntl_wexitstatus($status) !== 0) { + // phpcs:enable $this->failInChildProcess = true; } $threadNumber--; diff --git a/app/code/Magento/Indexer/Test/Mftf/ActionGroup/IndexerActionGroup.xml b/app/code/Magento/Indexer/Test/Mftf/ActionGroup/IndexerActionGroup.xml new file mode 100644 index 000000000000..52444e0c26ab --- /dev/null +++ b/app/code/Magento/Indexer/Test/Mftf/ActionGroup/IndexerActionGroup.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See 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="updateIndexerBySchedule"> + <arguments> + <argument name="indexerName" type="string"/> + </arguments> + <amOnPage url="{{_ENV.MAGENTO_BACKEND_NAME}}/indexer/indexer/list/" stepKey="amOnIndexManagementPage"/> + <waitForPageLoad stepKey="waitForIndexManagementPageToLoad"/> + <click selector="{{AdminIndexManagementSection.indexerCheckbox(indexerName)}}" stepKey="selectIndexer1"/> + <selectOption selector="{{AdminIndexManagementSection.massActionSelect}}" userInput="change_mode_changelog" stepKey="selectUpdateBySchedule"/> + <click selector="{{AdminIndexManagementSection.massActionSubmit}}" stepKey="submitIndexerForm"/> + <!-- No re-indexing is done as part of this actionGroup since the test required no re-indexing --> + <waitForPageLoad stepKey="waitForSave"/> + </actionGroup> + <actionGroup name="updateIndexerOnSave"> + <arguments> + <argument name="indexerName" type="string"/> + </arguments> + <amOnPage url="{{_ENV.MAGENTO_BACKEND_NAME}}/indexer/indexer/list/" stepKey="amOnIndexManagementPage2"/> + <waitForPageLoad stepKey="waitForIndexManagementPageToLoad2"/> + <click selector="{{AdminIndexManagementSection.indexerCheckbox(indexerName)}}" stepKey="selectIndexer2"/> + <selectOption selector="{{AdminIndexManagementSection.massActionSelect}}" userInput="change_mode_onthefly" stepKey="selectUpdateOnSave"/> + <click selector="{{AdminIndexManagementSection.massActionSubmit}}" stepKey="submitIndexerForm2"/> + <!-- No re-indexing is done as part of this actionGroup since the test required no re-indexing --> + <waitForPageLoad stepKey="waitForSave2"/> + + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Indexer/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Indexer/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 000000000000..b74d521f2cb3 --- /dev/null +++ b/app/code/Magento/Indexer/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminMenuSystemToolsIndexManagement"> + <data key="pageTitle">Index Management</data> + <data key="title">Index Management</data> + <data key="dataUiId">magento-indexer-system-index</data> + </entity> +</entities> diff --git a/app/code/Magento/Indexer/Test/Mftf/Page/AdminIndexManagementPage.xml b/app/code/Magento/Indexer/Test/Mftf/Page/AdminIndexManagementPage.xml new file mode 100644 index 000000000000..ed9a3dbb8c74 --- /dev/null +++ b/app/code/Magento/Indexer/Test/Mftf/Page/AdminIndexManagementPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminIndexManagementPage" url="indexer/indexer/list/" area="admin" module="Indexer"> + <section name="AdminIndexManagementSection"/> + </page> +</pages> diff --git a/app/code/Magento/Indexer/Test/Mftf/Section/AdminIndexManagementSection.xml b/app/code/Magento/Indexer/Test/Mftf/Section/AdminIndexManagementSection.xml new file mode 100644 index 000000000000..860b600de2b5 --- /dev/null +++ b/app/code/Magento/Indexer/Test/Mftf/Section/AdminIndexManagementSection.xml @@ -0,0 +1,20 @@ +<?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="AdminIndexManagementSection"> + <element name="catalogSearchCheckbox" type="checkbox" selector="input[value='catalogsearch_fulltext']"/> + <element name="indexerCheckbox" type="checkbox" selector="input[value='{{var1}}']" parameterized="true"/> + <element name="massActionSelect" type="select" selector="#gridIndexer_massaction-select"/> + <element name="massActionSubmit" type="button" selector="#gridIndexer_massaction-form button"/> + <element name="indexerSelect" type="select" selector="//select[contains(@class,'action-select-multiselect')]"/> + <element name="indexerStatus" type="text" selector="//tr[descendant::td[contains(., '{{status}}')]]//*[contains(@class, 'col-indexer_status')]/span" parameterized="true"/> + <element name="successMessage" type="text" selector="//*[@data-ui-id='messages-message-success']"/> + </section> +</sections> diff --git a/app/code/Magento/Indexer/Test/Mftf/Test/AdminSystemIndexManagementNavigateMenuTest.xml b/app/code/Magento/Indexer/Test/Mftf/Test/AdminSystemIndexManagementNavigateMenuTest.xml new file mode 100644 index 000000000000..140c93f7f2b4 --- /dev/null +++ b/app/code/Magento/Indexer/Test/Mftf/Test/AdminSystemIndexManagementNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminSystemIndexManagementNavigateMenuTest"> + <annotations> + <features value="Indexer"/> + <stories value="Menu Navigation"/> + <title value="Admin system index management navigate menu test"/> + <description value="Admin should be able to navigate to System > Index Management"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14127"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToIndexManagementPage"> + <argument name="menuUiId" value="{{AdminMenuSystem.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSystemToolsIndexManagement.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSystemToolsIndexManagement.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php b/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php index 6b7cc1221899..ca2da9585f93 100644 --- a/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php @@ -164,7 +164,12 @@ public function testGetLatestUpdated($getViewIsEnabled, $getViewGetUpdated, $get } } } else { - $this->assertEquals($getStateGetUpdated, $this->model->getLatestUpdated()); + $getLatestUpdated = $this->model->getLatestUpdated(); + $this->assertEquals($getStateGetUpdated, $getLatestUpdated); + + if ($getStateGetUpdated === null) { + $this->assertNotNull($getLatestUpdated); + } } } @@ -182,7 +187,8 @@ public function getLatestUpdatedDataProvider() [true, '', '06-Jan-1944'], [true, '06-Jan-1944', ''], [true, '', ''], - [true, '06-Jan-1944', '05-Jan-1944'] + [true, '06-Jan-1944', '05-Jan-1944'], + [false, null, null], ]; } diff --git a/app/code/Magento/Indexer/etc/db_schema.xml b/app/code/Magento/Indexer/etc/db_schema.xml index 58c447f4d262..d7cb006a2cf4 100644 --- a/app/code/Magento/Indexer/etc/db_schema.xml +++ b/app/code/Magento/Indexer/etc/db_schema.xml @@ -15,10 +15,10 @@ comment="Indexer Status"/> <column xsi:type="datetime" name="updated" on_update="false" nullable="true" comment="Indexer Status"/> <column xsi:type="varchar" name="hash_config" nullable="false" length="32" comment="Hash of indexer config"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="state_id"/> </constraint> - <index name="INDEXER_STATE_INDEXER_ID" indexType="btree"> + <index referenceId="INDEXER_STATE_INDEXER_ID" indexType="btree"> <column name="indexer_id"/> </index> </table> @@ -31,13 +31,13 @@ <column xsi:type="datetime" name="updated" on_update="false" nullable="true" comment="View updated time"/> <column xsi:type="int" name="version_id" padding="10" unsigned="true" nullable="true" identity="false" comment="View Version Id"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="state_id"/> </constraint> - <index name="MVIEW_STATE_VIEW_ID" indexType="btree"> + <index referenceId="MVIEW_STATE_VIEW_ID" indexType="btree"> <column name="view_id"/> </index> - <index name="MVIEW_STATE_MODE" indexType="btree"> + <index referenceId="MVIEW_STATE_MODE" indexType="btree"> <column name="mode"/> </index> </table> diff --git a/app/code/Magento/Indexer/etc/di.xml b/app/code/Magento/Indexer/etc/di.xml index c7603191e860..76e7e7a46224 100644 --- a/app/code/Magento/Indexer/etc/di.xml +++ b/app/code/Magento/Indexer/etc/di.xml @@ -42,7 +42,7 @@ <plugin name="page-cache-indexer-reindex-clean-cache" type="Magento\Indexer\Model\Processor\CleanCache" sortOrder="10"/> </type> - <type name="\Magento\Indexer\Model\ProcessManager"> + <type name="Magento\Indexer\Model\ProcessManager"> <arguments> <argument name="threadsCount" xsi:type="init_parameter">Magento\Indexer\Model\ProcessManager::THREADS_COUNT</argument> </arguments> diff --git a/app/code/Magento/Indexer/view/adminhtml/layout/indexer_indexer_list_grid.xml b/app/code/Magento/Indexer/view/adminhtml/layout/indexer_indexer_list_grid.xml index bf6b2351f75f..f1099c4133bc 100644 --- a/app/code/Magento/Indexer/view/adminhtml/layout/indexer_indexer_list_grid.xml +++ b/app/code/Magento/Indexer/view/adminhtml/layout/indexer_indexer_list_grid.xml @@ -13,7 +13,7 @@ <argument name="use_ajax" xsi:type="string">0</argument> <argument name="pager_visibility" xsi:type="string">0</argument> <argument name="id" xsi:type="string">gridIndexer</argument> - <argument name="dataSource" xsi:type="object">Magento\Indexer\Ui\DataProvider\Indexer\DataCollection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Indexer\Ui\DataProvider\Indexer\DataCollection</argument> </arguments> <block class="Magento\Backend\Block\Widget\Grid\Massaction" name="adminhtml.indexer.grid.grid.massaction" as="grid.massaction"> <arguments> diff --git a/app/code/Magento/InstantPurchase/PaymentMethodIntegration/IntegrationFactory.php b/app/code/Magento/InstantPurchase/PaymentMethodIntegration/IntegrationFactory.php index cdca81ed98b7..0b698afea1de 100644 --- a/app/code/Magento/InstantPurchase/PaymentMethodIntegration/IntegrationFactory.php +++ b/app/code/Magento/InstantPurchase/PaymentMethodIntegration/IntegrationFactory.php @@ -65,13 +65,13 @@ public function create(VaultPaymentInterface $paymentMethod, int $storeId): Inte /** * Reads value from config. * - * @param $config + * @param array $config * @param string $field * @param string $default * @return string */ private function extractFromConfig($config, string $field, string $default): string { - return isset($config[$field]) ? $config[$field] : $default; + return $config[$field] ?? $default; } } diff --git a/app/code/Magento/InstantPurchase/README.md b/app/code/Magento/InstantPurchase/README.md index 534696bf353f..9b618eaca997 100644 --- a/app/code/Magento/InstantPurchase/README.md +++ b/app/code/Magento/InstantPurchase/README.md @@ -10,7 +10,7 @@ Prerequisites to display the Instant Purchase button: ## Structure -In addition to [a typical file structure for a Magento 2 module](http://devdocs.magento.com/guides/v2.2/extension-dev-guide/build/module-file-structure.html) `PaymentMethodsIntegration` directory contains interfaces and basic implementation of integration vault payment method to the instant purchase. +In addition to [a typical file structure for a Magento 2 module](https://devdocs.magento.com/guides/v2.2/extension-dev-guide/build/module-file-structure.html) `PaymentMethodsIntegration` directory contains interfaces and basic implementation of integration vault payment method to the instant purchase. ## Extensibility @@ -22,7 +22,7 @@ All payments created for instant purchase also have `'instant-purchase' => true` ### Payment method integration -Instant purchase support may be implemented for any payment method with [vault support](http://devdocs.magento.com/guides/v2.1/payments-integrations/vault/vault-intro.html). +Instant purchase support may be implemented for any payment method with [vault support](https://devdocs.magento.com/guides/v2.1/payments-integrations/vault/vault-intro.html). Basic implementation provided in `Magento\InstantPurchase\PaymentMethodIntegration` should be enough in most cases. It is not enabled by default to avoid issues on production sites and authors of vault payment method should verify correct work for instant purchase manually. To enable basic implementation just add single option to configuration of payemnt method in `config.xml`: @@ -52,7 +52,7 @@ Basic implementation is a good start point but it's recommended to provide own i The `Magento_InstantPurchase` module does not introduce backward incompatible changes. -You can track [backward incompatible changes in patch releases](http://devdocs.magento.com/guides/v2.2/release-notes/changes/ce_changes.html). +You can track [backward incompatible changes in patch releases](https://devdocs.magento.com/guides/v2.2/release-notes/changes/ce_changes.html). *** diff --git a/app/code/Magento/Integration/Controller/Adminhtml/Integration/Delete.php b/app/code/Magento/Integration/Controller/Adminhtml/Integration/Delete.php index 36073af56327..4ce462bb44c8 100644 --- a/app/code/Magento/Integration/Controller/Adminhtml/Integration/Delete.php +++ b/app/code/Magento/Integration/Controller/Adminhtml/Integration/Delete.php @@ -6,11 +6,12 @@ */ namespace Magento\Integration\Controller\Adminhtml\Integration; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Integration\Block\Adminhtml\Integration\Edit\Tab\Info; use Magento\Framework\Exception\IntegrationException; use Magento\Framework\Controller\ResultFactory; -class Delete extends \Magento\Integration\Controller\Adminhtml\Integration +class Delete extends \Magento\Integration\Controller\Adminhtml\Integration implements HttpPostActionInterface { /** * Delete the integration. diff --git a/app/code/Magento/Integration/Controller/Adminhtml/Integration/Edit.php b/app/code/Magento/Integration/Controller/Adminhtml/Integration/Edit.php index 97cd7b46e086..599b6017059e 100644 --- a/app/code/Magento/Integration/Controller/Adminhtml/Integration/Edit.php +++ b/app/code/Magento/Integration/Controller/Adminhtml/Integration/Edit.php @@ -6,11 +6,12 @@ */ namespace Magento\Integration\Controller\Adminhtml\Integration; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Backend\App\Action; use Magento\Integration\Block\Adminhtml\Integration\Edit\Tab\Info; use Magento\Framework\Exception\IntegrationException; -class Edit extends \Magento\Integration\Controller\Adminhtml\Integration +class Edit extends \Magento\Integration\Controller\Adminhtml\Integration implements HttpGetActionInterface { /** * Edit integration action. diff --git a/app/code/Magento/Integration/Controller/Adminhtml/Integration/Grid.php b/app/code/Magento/Integration/Controller/Adminhtml/Integration/Grid.php index bd10873a59f3..f3ad4306db5a 100644 --- a/app/code/Magento/Integration/Controller/Adminhtml/Integration/Grid.php +++ b/app/code/Magento/Integration/Controller/Adminhtml/Integration/Grid.php @@ -6,7 +6,11 @@ */ namespace Magento\Integration\Controller\Adminhtml\Integration; -class Grid extends \Magento\Integration\Controller\Adminhtml\Integration +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Integration\Controller\Adminhtml\Integration as IntegrationAction; + +class Grid extends IntegrationAction implements HttpPostActionInterface, HttpGetActionInterface { /** * AJAX integrations grid. diff --git a/app/code/Magento/Integration/Controller/Adminhtml/Integration/Index.php b/app/code/Magento/Integration/Controller/Adminhtml/Integration/Index.php index 2caf9054ce35..131e1e149473 100644 --- a/app/code/Magento/Integration/Controller/Adminhtml/Integration/Index.php +++ b/app/code/Magento/Integration/Controller/Adminhtml/Integration/Index.php @@ -6,7 +6,9 @@ */ namespace Magento\Integration\Controller\Adminhtml\Integration; -class Index extends \Magento\Integration\Controller\Adminhtml\Integration +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\Integration\Controller\Adminhtml\Integration implements HttpGetActionInterface { /** * Integrations grid. diff --git a/app/code/Magento/Integration/Controller/Adminhtml/Integration/NewAction.php b/app/code/Magento/Integration/Controller/Adminhtml/Integration/NewAction.php index e05d559aa1cd..7354b22ccf46 100644 --- a/app/code/Magento/Integration/Controller/Adminhtml/Integration/NewAction.php +++ b/app/code/Magento/Integration/Controller/Adminhtml/Integration/NewAction.php @@ -6,7 +6,9 @@ */ namespace Magento\Integration\Controller\Adminhtml\Integration; -class NewAction extends \Magento\Integration\Controller\Adminhtml\Integration +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class NewAction extends \Magento\Integration\Controller\Adminhtml\Integration implements HttpGetActionInterface { /** * New integration action. diff --git a/app/code/Magento/Integration/Controller/Adminhtml/Integration/PermissionsDialog.php b/app/code/Magento/Integration/Controller/Adminhtml/Integration/PermissionsDialog.php index 8c2e10ebf1f3..8b2a94da01d7 100644 --- a/app/code/Magento/Integration/Controller/Adminhtml/Integration/PermissionsDialog.php +++ b/app/code/Magento/Integration/Controller/Adminhtml/Integration/PermissionsDialog.php @@ -6,9 +6,10 @@ */ namespace Magento\Integration\Controller\Adminhtml\Integration; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Framework\Exception\IntegrationException; -class PermissionsDialog extends \Magento\Integration\Controller\Adminhtml\Integration +class PermissionsDialog extends \Magento\Integration\Controller\Adminhtml\Integration implements HttpGetActionInterface { /** * Show permissions popup. diff --git a/app/code/Magento/Integration/Controller/Adminhtml/Integration/Save.php b/app/code/Magento/Integration/Controller/Adminhtml/Integration/Save.php index cb2fcb2ba0e2..8bcbb4565349 100644 --- a/app/code/Magento/Integration/Controller/Adminhtml/Integration/Save.php +++ b/app/code/Magento/Integration/Controller/Adminhtml/Integration/Save.php @@ -5,6 +5,7 @@ */ namespace Magento\Integration\Controller\Adminhtml\Integration; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Integration\Block\Adminhtml\Integration\Edit\Tab\Info; use Magento\Framework\Exception\IntegrationException; use Magento\Framework\Exception\LocalizedException; @@ -17,7 +18,7 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Save extends \Magento\Integration\Controller\Adminhtml\Integration +class Save extends \Magento\Integration\Controller\Adminhtml\Integration implements HttpPostActionInterface { /** * @var SecurityCookie diff --git a/app/code/Magento/Integration/Controller/Adminhtml/Integration/TokensDialog.php b/app/code/Magento/Integration/Controller/Adminhtml/Integration/TokensDialog.php index dcea6da32128..4c99dafb1d99 100644 --- a/app/code/Magento/Integration/Controller/Adminhtml/Integration/TokensDialog.php +++ b/app/code/Magento/Integration/Controller/Adminhtml/Integration/TokensDialog.php @@ -6,9 +6,10 @@ */ namespace Magento\Integration\Controller\Adminhtml\Integration; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Integration\Model\Integration as IntegrationModel; -class TokensDialog extends \Magento\Integration\Controller\Adminhtml\Integration +class TokensDialog extends \Magento\Integration\Controller\Adminhtml\Integration implements HttpGetActionInterface { /** * Set success message based on Integration activation or re-authorization. diff --git a/app/code/Magento/Integration/Model/AdminTokenService.php b/app/code/Magento/Integration/Model/AdminTokenService.php index 5a030325e9fb..7726ff979c6d 100644 --- a/app/code/Magento/Integration/Model/AdminTokenService.php +++ b/app/code/Magento/Integration/Model/AdminTokenService.php @@ -72,7 +72,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function createAdminAccessToken($username, $password) { @@ -110,7 +110,7 @@ public function revokeAdminAccessToken($adminId) { $tokenCollection = $this->tokenModelCollectionFactory->create()->addFilterByAdminId($adminId); if ($tokenCollection->getSize() == 0) { - throw new LocalizedException(__('This user has no tokens.')); + return true; } try { foreach ($tokenCollection as $token) { diff --git a/app/code/Magento/Integration/Model/Config/Consolidated/Converter.php b/app/code/Magento/Integration/Model/Config/Consolidated/Converter.php index 674a97a68d06..c10759d60274 100644 --- a/app/code/Magento/Integration/Model/Config/Consolidated/Converter.php +++ b/app/code/Magento/Integration/Model/Config/Consolidated/Converter.php @@ -36,7 +36,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function convert($source) { @@ -80,9 +80,16 @@ public function convert($source) $result[$integrationName][self::API_RESOURCES][] = $name; } } + + // Add root resource if any child has been added + if (!empty($result[$integrationName][self::API_RESOURCES])) { + array_unshift($result[$integrationName][self::API_RESOURCES], $allResources[1]['id']); + } + // Remove any duplicates added parents - $result[$integrationName][self::API_RESOURCES] = - array_values(array_unique($result[$integrationName][self::API_RESOURCES])); + $result[$integrationName][self::API_RESOURCES] = array_values( + array_unique($result[$integrationName][self::API_RESOURCES]) + ); } return $result; } diff --git a/app/code/Magento/Integration/Model/CustomerTokenService.php b/app/code/Magento/Integration/Model/CustomerTokenService.php index 3c245804a9f6..7c2c444a734e 100644 --- a/app/code/Magento/Integration/Model/CustomerTokenService.php +++ b/app/code/Magento/Integration/Model/CustomerTokenService.php @@ -14,7 +14,11 @@ use Magento\Integration\Model\ResourceModel\Oauth\Token\CollectionFactory as TokenCollectionFactory; use Magento\Integration\Model\Oauth\Token\RequestThrottler; use Magento\Framework\Exception\AuthenticationException; +use Magento\Framework\Event\ManagerInterface; +/** + * @inheritdoc + */ class CustomerTokenService implements \Magento\Integration\Api\CustomerTokenServiceInterface { /** @@ -24,6 +28,11 @@ class CustomerTokenService implements \Magento\Integration\Api\CustomerTokenServ */ private $tokenModelFactory; + /** + * @var Magento\Framework\Event\ManagerInterface + */ + private $eventManager; + /** * Customer Account Service * @@ -55,21 +64,25 @@ class CustomerTokenService implements \Magento\Integration\Api\CustomerTokenServ * @param AccountManagementInterface $accountManagement * @param TokenCollectionFactory $tokenModelCollectionFactory * @param \Magento\Integration\Model\CredentialsValidator $validatorHelper + * @param \Magento\Framework\Event\ManagerInterface $eventManager */ public function __construct( TokenModelFactory $tokenModelFactory, AccountManagementInterface $accountManagement, TokenCollectionFactory $tokenModelCollectionFactory, - CredentialsValidator $validatorHelper + CredentialsValidator $validatorHelper, + ManagerInterface $eventManager = null ) { $this->tokenModelFactory = $tokenModelFactory; $this->accountManagement = $accountManagement; $this->tokenModelCollectionFactory = $tokenModelCollectionFactory; $this->validatorHelper = $validatorHelper; + $this->eventManager = $eventManager ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(ManagerInterface::class); } /** - * {@inheritdoc} + * @inheritdoc */ public function createCustomerAccessToken($username, $password) { @@ -86,6 +99,7 @@ public function createCustomerAccessToken($username, $password) ) ); } + $this->eventManager->dispatch('customer_login', ['customer' => $customerDataObject]); $this->getRequestThrottler()->resetAuthenticationFailuresCount($username, RequestThrottler::USER_TYPE_CUSTOMER); return $this->tokenModelFactory->create()->createCustomerToken($customerDataObject->getId())->getToken(); } diff --git a/app/code/Magento/Integration/Plugin/Model/AdminUser.php b/app/code/Magento/Integration/Plugin/Model/AdminUser.php index df3766250caa..7b2fa1981bce 100644 --- a/app/code/Magento/Integration/Plugin/Model/AdminUser.php +++ b/app/code/Magento/Integration/Plugin/Model/AdminUser.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Integration\Plugin\Model; use Magento\Integration\Model\AdminTokenService; @@ -31,14 +32,15 @@ public function __construct( * * @param \Magento\User\Model\User $subject * @param \Magento\Framework\DataObject $object - * @return $this + * @return \Magento\User\Model\User + * @throws \Magento\Framework\Exception\LocalizedException */ public function afterSave( \Magento\User\Model\User $subject, \Magento\Framework\DataObject $object - ) { + ): \Magento\User\Model\User { $isActive = $object->getIsActive(); - if (isset($isActive) && $isActive == 0) { + if ($isActive !== null && $isActive == 0) { $this->adminTokenService->revokeAdminAccessToken($object->getId()); } return $subject; diff --git a/app/code/Magento/Integration/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Integration/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 000000000000..be5b3f16ad3c --- /dev/null +++ b/app/code/Magento/Integration/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminMenuSystemExtensionsIntegrations"> + <data key="pageTitle">Integrations</data> + <data key="title">Integrations</data> + <data key="dataUiId">magento-integration-system-integrations</data> + </entity> +</entities> diff --git a/app/code/Magento/Integration/Test/Mftf/Test/AdminSystemIntegrationsNavigateMenuTest.xml b/app/code/Magento/Integration/Test/Mftf/Test/AdminSystemIntegrationsNavigateMenuTest.xml new file mode 100644 index 000000000000..3bd149d222c0 --- /dev/null +++ b/app/code/Magento/Integration/Test/Mftf/Test/AdminSystemIntegrationsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminSystemIntegrationsNavigateMenuTest"> + <annotations> + <features value="Integration"/> + <stories value="Menu Navigation"/> + <title value="Admin system integrations navigate menu test"/> + <description value="Admin should be able to navigate to System > Integrations"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14149"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToIntegrationsPage"> + <argument name="menuUiId" value="{{AdminMenuSystem.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSystemExtensionsIntegrations.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSystemExtensionsIntegrations.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Integration/Test/Unit/Model/AdminTokenServiceTest.php b/app/code/Magento/Integration/Test/Unit/Model/AdminTokenServiceTest.php index e0b1113e80d8..83efe4074e15 100644 --- a/app/code/Magento/Integration/Test/Unit/Model/AdminTokenServiceTest.php +++ b/app/code/Magento/Integration/Test/Unit/Model/AdminTokenServiceTest.php @@ -8,6 +8,9 @@ use Magento\Integration\Model\Oauth\Token; +/** + * Test for Magento\Integration\Model\AdminTokenService class. + */ class AdminTokenServiceTest extends \PHPUnit\Framework\TestCase { /** \Magento\Integration\Model\AdminTokenService */ @@ -99,10 +102,6 @@ public function testRevokeAdminAccessToken() $this->assertTrue($this->_tokenService->revokeAdminAccessToken($adminId)); } - /** - * @expectedException \Magento\Framework\Exception\LocalizedException - * @expectedExceptionMessage This user has no tokens. - */ public function testRevokeAdminAccessTokenWithoutAdminId() { $this->_tokenModelCollectionMock->expects($this->once()) diff --git a/app/code/Magento/Integration/Test/Unit/Model/Config/Consolidated/_files/acl.php b/app/code/Magento/Integration/Test/Unit/Model/Config/Consolidated/_files/acl.php index 5b7bbace5a65..42b62acaba5b 100644 --- a/app/code/Magento/Integration/Test/Unit/Model/Config/Consolidated/_files/acl.php +++ b/app/code/Magento/Integration/Test/Unit/Model/Config/Consolidated/_files/acl.php @@ -6,6 +6,8 @@ return [ [], [ + 'id' => 'Magento_Backend::admin', + 'title' => 'Magento Admin (Root)', 'children' => [ [ diff --git a/app/code/Magento/Integration/Test/Unit/Model/Config/Consolidated/_files/integration.php b/app/code/Magento/Integration/Test/Unit/Model/Config/Consolidated/_files/integration.php index 54e05d5ef901..0293492c7765 100644 --- a/app/code/Magento/Integration/Test/Unit/Model/Config/Consolidated/_files/integration.php +++ b/app/code/Magento/Integration/Test/Unit/Model/Config/Consolidated/_files/integration.php @@ -9,6 +9,7 @@ 'endpoint_url' => 'http://endpoint.com', 'identity_link_url' => 'http://www.example.com/identity', 'resource' => [ + 'Magento_Backend::admin', 'Magento_Customer::manageParent', 'Magento_Customer::manage', 'Magento_SalesRule::quoteParent', @@ -17,6 +18,9 @@ ], 'TestIntegration2' => [ 'email' => 'test-integration2@magento.com', - 'resource' => ['Magento_Sales::sales'] + 'resource' => [ + 'Magento_Backend::admin', + 'Magento_Sales::sales' + ] ] ]; diff --git a/app/code/Magento/Integration/Test/Unit/Model/Config/Consolidated/_files/integration.xml b/app/code/Magento/Integration/Test/Unit/Model/Config/Consolidated/_files/integration.xml index 585fabc24299..f8bcf3fc4a2c 100644 --- a/app/code/Magento/Integration/Test/Unit/Model/Config/Consolidated/_files/integration.xml +++ b/app/code/Magento/Integration/Test/Unit/Model/Config/Consolidated/_files/integration.xml @@ -18,6 +18,7 @@ <integration name="TestIntegration2"> <email>test-integration2@magento.com</email> <resources> + <resource name="Magento_Backend::admin" /> <resource name="Magento_Sales::sales" /> </resources> </integration> diff --git a/app/code/Magento/Integration/Test/Unit/Model/CustomerTokenServiceTest.php b/app/code/Magento/Integration/Test/Unit/Model/CustomerTokenServiceTest.php index 1a7c81934329..1bc7d4247080 100644 --- a/app/code/Magento/Integration/Test/Unit/Model/CustomerTokenServiceTest.php +++ b/app/code/Magento/Integration/Test/Unit/Model/CustomerTokenServiceTest.php @@ -32,6 +32,9 @@ class CustomerTokenServiceTest extends \PHPUnit\Framework\TestCase /** @var \Magento\Integration\Model\Oauth\Token|\PHPUnit_Framework_MockObject_MockObject */ private $_tokenMock; + /** @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ + protected $manager; + protected function setUp() { $this->_tokenFactoryMock = $this->getMockBuilder(\Magento\Integration\Model\Oauth\TokenFactory::class) @@ -67,11 +70,14 @@ protected function setUp() \Magento\Integration\Model\CredentialsValidator::class )->disableOriginalConstructor()->getMock(); + $this->manager = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); + $this->_tokenService = new \Magento\Integration\Model\CustomerTokenService( $this->_tokenFactoryMock, $this->_accountManagementMock, $this->_tokenModelCollectionFactoryMock, - $this->validatorHelperMock + $this->validatorHelperMock, + $this->manager ); } diff --git a/app/code/Magento/Integration/etc/adminhtml/system.xml b/app/code/Magento/Integration/etc/adminhtml/system.xml index 5abec8efbfdd..fe80fe105493 100644 --- a/app/code/Magento/Integration/etc/adminhtml/system.xml +++ b/app/code/Magento/Integration/etc/adminhtml/system.xml @@ -54,7 +54,7 @@ <label>Maximum Login Failures to Lock Out Account</label> <comment>Maximum Number of authentication failures to lock out account.</comment> </field> - <field id="timeout" translate="label" type="text comment" sortOrder="30" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> + <field id="timeout" translate="label comment" type="text" sortOrder="30" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> <label>Lockout Time (seconds)</label> <comment>Period of time in seconds after which account will be unlocked.</comment> </field> diff --git a/app/code/Magento/Integration/etc/db_schema.xml b/app/code/Magento/Integration/etc/db_schema.xml index 9e81b867d36d..cbf43d79b2cf 100644 --- a/app/code/Magento/Integration/etc/db_schema.xml +++ b/app/code/Magento/Integration/etc/db_schema.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="oauth_consumer" resource="default" engine="innodb" comment="OAuth Consumers"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="true" default="0" @@ -19,19 +19,19 @@ <column xsi:type="varchar" name="secret" nullable="false" length="32" comment="Secret code"/> <column xsi:type="text" name="callback_url" nullable="true" comment="Callback URL"/> <column xsi:type="text" name="rejected_callback_url" nullable="false" comment="Rejected callback URL"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> - <constraint xsi:type="unique" name="OAUTH_CONSUMER_KEY"> + <constraint xsi:type="unique" referenceId="OAUTH_CONSUMER_KEY"> <column name="key"/> </constraint> - <constraint xsi:type="unique" name="OAUTH_CONSUMER_SECRET"> + <constraint xsi:type="unique" referenceId="OAUTH_CONSUMER_SECRET"> <column name="secret"/> </constraint> - <index name="OAUTH_CONSUMER_CREATED_AT" indexType="btree"> + <index referenceId="OAUTH_CONSUMER_CREATED_AT" indexType="btree"> <column name="created_at"/> </index> - <index name="OAUTH_CONSUMER_UPDATED_AT" indexType="btree"> + <index referenceId="OAUTH_CONSUMER_UPDATED_AT" indexType="btree"> <column name="updated_at"/> </index> </table> @@ -57,21 +57,21 @@ comment="User type"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Token creation timestamp"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> - <constraint xsi:type="foreign" name="OAUTH_TOKEN_ADMIN_ID_ADMIN_USER_USER_ID" table="oauth_token" + <constraint xsi:type="foreign" referenceId="OAUTH_TOKEN_ADMIN_ID_ADMIN_USER_USER_ID" table="oauth_token" column="admin_id" referenceTable="admin_user" referenceColumn="user_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="OAUTH_TOKEN_CONSUMER_ID_OAUTH_CONSUMER_ENTITY_ID" table="oauth_token" + <constraint xsi:type="foreign" referenceId="OAUTH_TOKEN_CONSUMER_ID_OAUTH_CONSUMER_ENTITY_ID" table="oauth_token" column="consumer_id" referenceTable="oauth_consumer" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="OAUTH_TOKEN_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID" table="oauth_token" + <constraint xsi:type="foreign" referenceId="OAUTH_TOKEN_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID" table="oauth_token" column="customer_id" referenceTable="customer_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="OAUTH_TOKEN_TOKEN"> + <constraint xsi:type="unique" referenceId="OAUTH_TOKEN_TOKEN"> <column name="token"/> </constraint> - <index name="OAUTH_TOKEN_CONSUMER_ID" indexType="btree"> + <index referenceId="OAUTH_TOKEN_CONSUMER_ID" indexType="btree"> <column name="consumer_id"/> </index> </table> @@ -81,14 +81,14 @@ comment="Nonce Timestamp"/> <column xsi:type="int" name="consumer_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Consumer ID"/> - <constraint xsi:type="foreign" name="OAUTH_NONCE_CONSUMER_ID_OAUTH_CONSUMER_ENTITY_ID" table="oauth_nonce" + <constraint xsi:type="foreign" referenceId="OAUTH_NONCE_CONSUMER_ID_OAUTH_CONSUMER_ENTITY_ID" table="oauth_nonce" column="consumer_id" referenceTable="oauth_consumer" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="OAUTH_NONCE_NONCE_CONSUMER_ID"> + <constraint xsi:type="unique" referenceId="OAUTH_NONCE_NONCE_CONSUMER_ID"> <column name="nonce"/> <column name="consumer_id"/> </constraint> - <index name="OAUTH_NONCE_TIMESTAMP" indexType="btree"> + <index referenceId="OAUTH_NONCE_TIMESTAMP" indexType="btree"> <column name="timestamp"/> </index> </table> @@ -107,22 +107,22 @@ comment="Oauth consumer"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Creation Time"/> - <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="0" + <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" comment="Update Time"/> <column xsi:type="smallint" name="setup_type" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Integration type - manual or config file"/> <column xsi:type="varchar" name="identity_link_url" nullable="true" length="255" comment="Identity linking Url"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="integration_id"/> </constraint> - <constraint xsi:type="foreign" name="INTEGRATION_CONSUMER_ID_OAUTH_CONSUMER_ENTITY_ID" table="integration" + <constraint xsi:type="foreign" referenceId="INTEGRATION_CONSUMER_ID_OAUTH_CONSUMER_ENTITY_ID" table="integration" column="consumer_id" referenceTable="oauth_consumer" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="INTEGRATION_NAME"> + <constraint xsi:type="unique" referenceId="INTEGRATION_NAME"> <column name="name"/> </constraint> - <constraint xsi:type="unique" name="INTEGRATION_CONSUMER_ID"> + <constraint xsi:type="unique" referenceId="INTEGRATION_CONSUMER_ID"> <column name="consumer_id"/> </constraint> </table> @@ -136,12 +136,12 @@ comment="User type (admin or customer)"/> <column xsi:type="smallint" name="failures_count" padding="5" unsigned="true" nullable="true" identity="false" default="0" comment="Number of failed authentication attempts in a row"/> - <column xsi:type="timestamp" name="lock_expires_at" on_update="true" nullable="true" default="CURRENT_TIMESTAMP" + <column xsi:type="timestamp" name="lock_expires_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" comment="Lock expiration time"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="log_id"/> </constraint> - <constraint xsi:type="unique" name="OAUTH_TOKEN_REQUEST_LOG_USER_NAME_USER_TYPE"> + <constraint xsi:type="unique" referenceId="OAUTH_TOKEN_REQUEST_LOG_USER_NAME_USER_TYPE"> <column name="user_name"/> <column name="user_type"/> </constraint> diff --git a/app/code/Magento/Integration/etc/webapi_rest/events.xml b/app/code/Magento/Integration/etc/webapi_rest/events.xml new file mode 100644 index 000000000000..e97869873427 --- /dev/null +++ b/app/code/Magento/Integration/etc/webapi_rest/events.xml @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd"> + <event name="customer_login"> + <observer name="customer_log_login" instance="Magento\Customer\Observer\LogLastLoginAtObserver" /> + </event> +</config> diff --git a/app/code/Magento/Integration/etc/webapi_soap/events.xml b/app/code/Magento/Integration/etc/webapi_soap/events.xml new file mode 100644 index 000000000000..e97869873427 --- /dev/null +++ b/app/code/Magento/Integration/etc/webapi_soap/events.xml @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd"> + <event name="customer_login"> + <observer name="customer_log_login" instance="Magento\Customer\Observer\LogLastLoginAtObserver" /> + </event> +</config> diff --git a/app/code/Magento/Integration/view/adminhtml/layout/adminhtml_integration_grid_block.xml b/app/code/Magento/Integration/view/adminhtml/layout/adminhtml_integration_grid_block.xml index 506f836f9951..43b67d6904f1 100644 --- a/app/code/Magento/Integration/view/adminhtml/layout/adminhtml_integration_grid_block.xml +++ b/app/code/Magento/Integration/view/adminhtml/layout/adminhtml_integration_grid_block.xml @@ -13,7 +13,7 @@ <block class="Magento\Integration\Block\Adminhtml\Integration\Grid" name="integration.grid" as="grid"> <arguments> <argument name="id" xsi:type="string">integrationGrid</argument> - <argument name="dataSource" xsi:type="object">Magento\Integration\Model\ResourceModel\Integration\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Integration\Model\ResourceModel\Integration\Collection</argument> <argument name="use_ajax" xsi:type="string">1</argument> <argument name="default_sort" xsi:type="string">integration_id</argument> <argument name="default_dir" xsi:type="string">asc</argument> diff --git a/app/code/Magento/Integration/view/adminhtml/templates/integration/popup_container.phtml b/app/code/Magento/Integration/view/adminhtml/templates/integration/popup_container.phtml index 8b7e787337e1..ae16da1760f6 100644 --- a/app/code/Magento/Integration/view/adminhtml/templates/integration/popup_container.phtml +++ b/app/code/Magento/Integration/view/adminhtml/templates/integration/popup_container.phtml @@ -15,7 +15,8 @@ "jquery", 'Magento_Ui/js/modal/confirm', "jquery/ui", - "Magento_Integration/js/integration" + "Magento_Integration/js/integration", + 'mage/dataPost' ], function ($, Confirm) { window.integration = new Integration( @@ -37,7 +38,7 @@ content: "<?= /* @escapeNotVerified */ __("Are you sure you want to delete this integration? You can't undo this action.") ?>", actions: { confirm: function () { - window.location.href = $(e.target).data('url'); + $.mage.dataPost().postData({action: $(e.target).data('url'), data: {}}); } } }); diff --git a/app/code/Magento/Integration/view/adminhtml/web/js/integration.js b/app/code/Magento/Integration/view/adminhtml/web/js/integration.js index 6921f645a233..a2acc40c6b86 100644 --- a/app/code/Magento/Integration/view/adminhtml/web/js/integration.js +++ b/app/code/Magento/Integration/view/adminhtml/web/js/integration.js @@ -187,7 +187,7 @@ define([ }, /** - * Function to check the location of the child popoup window. + * Function to check the location of the child popup window. * Once detected if the callback is successful, parent window will be reloaded */ fnCheckLocation: function () { diff --git a/app/code/Magento/LayeredNavigation/Observer/Edit/Tab/Front/ProductAttributeFormBuildFrontTabObserver.php b/app/code/Magento/LayeredNavigation/Observer/Edit/Tab/Front/ProductAttributeFormBuildFrontTabObserver.php index 6b64610caf5e..ce618f97883b 100644 --- a/app/code/Magento/LayeredNavigation/Observer/Edit/Tab/Front/ProductAttributeFormBuildFrontTabObserver.php +++ b/app/code/Magento/LayeredNavigation/Observer/Edit/Tab/Front/ProductAttributeFormBuildFrontTabObserver.php @@ -11,6 +11,9 @@ use Magento\Framework\Module\Manager; use Magento\Framework\Event\ObserverInterface; +/** + * Observer for Product Attribute Form + */ class ProductAttributeFormBuildFrontTabObserver implements ObserverInterface { /** @@ -34,6 +37,8 @@ public function __construct(Manager $moduleManager, Source\Yesno $optionList) } /** + * Execute + * * @param \Magento\Framework\Event\Observer $observer * @return void */ @@ -54,8 +59,12 @@ public function execute(\Magento\Framework\Event\Observer $observer) [ 'name' => 'is_filterable', 'label' => __("Use in Layered Navigation"), - 'title' => __('Can be used only with catalog input type Dropdown, Multiple Select and Price'), - 'note' => __('Can be used only with catalog input type Dropdown, Multiple Select and Price.'), + 'title' => __('Can be used only with catalog input type Yes/No, Dropdown, Multiple Select and Price'), + 'note' => __( + 'Can be used only with catalog input type Yes/No, Dropdown, Multiple Select and Price. + <br>Price is not compatible with <b>\'Filterable (no results)\'</b> option - + it will make no affect on Price filter.' + ), 'values' => [ ['value' => '0', 'label' => __('No')], ['value' => '1', 'label' => __('Filterable (with results)')], @@ -70,8 +79,8 @@ public function execute(\Magento\Framework\Event\Observer $observer) [ 'name' => 'is_filterable_in_search', 'label' => __("Use in Search Results Layered Navigation"), - 'title' => __('Can be used only with catalog input type Dropdown, Multiple Select and Price'), - 'note' => __('Can be used only with catalog input type Dropdown, Multiple Select and Price.'), + 'title' => __('Can be used only with catalog input type Yes/No, Dropdown, Multiple Select and Price'), + 'note' => __('Can be used only with catalog input type Yes/No, Dropdown, Multiple Select and Price.'), 'values' => $this->optionList->toOptionArray(), ] ); diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/Section/LayeredNavigationSection.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/Section/LayeredNavigationSection.xml new file mode 100644 index 000000000000..b44ee9ddbd73 --- /dev/null +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/Section/LayeredNavigationSection.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="LayeredNavigationSection"> + <element name="layeredNavigation" type="select" selector="#catalog_layered_navigation-head"/> + <element name="CheckIfTabExpand" type="button" selector="#catalog_layered_navigation-head:not(.open)"/> + <element name="NavigationStepCalculation" type="button" selector="#catalog_layered_navigation_price_range_calculation"/> + <element name="NavigationStepCalculationSystemValue" type="button" selector="#catalog_layered_navigation_price_range_calculation_inherit"/> + <element name="PriceNavigationStep" type="button" selector="#catalog_layered_navigation_price_range_step"/> + <element name="PriceNavigationStepSystemValue" type="button" selector="#catalog_layered_navigation_price_range_step_inherit"/> + </section> + + <section name="StorefrontLayeredNavigationSection"> + <element name="shoppingOptionsByName" type="button" selector="//*[text()='Shopping Options']/..//*[contains(text(),'{{arg}}')]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/AdminSpecifyLayerNavigationConfigurationTest.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/AdminSpecifyLayerNavigationConfigurationTest.xml new file mode 100644 index 000000000000..0a09f17d66f6 --- /dev/null +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/AdminSpecifyLayerNavigationConfigurationTest.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminSpecifyLayerNavigationConfigurationTest"> + <annotations> + <features value="LayerNavigation"/> + <group value="LayerNavigation"/> + <stories value="Magento_LayeredNavigation"/> + <title value="Admin should be able to uncheck Default Value checkbox for dependent field"/> + <description value="Admin should be able to uncheck Default Value checkbox for dependent field"/> + <severity value="AVERAGE"/> + <testCaseId value="MAGETWO-94872"/> + </annotations> + <before> + <actionGroup ref="LoginActionGroup" stepKey="login"/> + </before> + <amOnPage url="{{CatalogConfigPage.url}}" stepKey="navigateToConfigurationPage" /> + <waitForPageLoad stepKey="wait1"/> + <conditionalClick stepKey="expandLayeredNavigationTab" selector="{{LayeredNavigationSection.layeredNavigation}}" dependentSelector="{{LayeredNavigationSection.CheckIfTabExpand}}" visible="true" /> + <waitForElementVisible selector="{{LayeredNavigationSection.NavigationStepCalculationSystemValue}}" stepKey="waitForUseSystemValueVisible"/> + <uncheckOption selector="{{LayeredNavigationSection.NavigationStepCalculationSystemValue}}" stepKey="uncheckUseSystemValue"/> + <selectOption selector="{{LayeredNavigationSection.NavigationStepCalculation}}" userInput="Manual" stepKey="selectOption1"/> + <uncheckOption selector="{{LayeredNavigationSection.PriceNavigationStepSystemValue}}" stepKey="uncheckUseSystemValue2"/> + <fillField selector="{{LayeredNavigationSection.PriceNavigationStep}}" userInput="102" stepKey="fillAdmin1"/> + <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig" /> + <waitForPageLoad stepKey="waitForSavingSystemConfiguration"/> + <waitForElementVisible selector="#catalog_layered_navigation" stepKey="waitForLayeredNav" /> + <scrollTo selector="#catalog_layered_navigation" x="0" y="-80" stepKey="scrollToLayeredNavigation" /> + <seeInField stepKey="seeThatValueWasSaved" selector="{{LayeredNavigationSection.PriceNavigationStep}}" userInput="102"/> + <checkOption selector="{{LayeredNavigationSection.NavigationStepCalculationSystemValue}}" stepKey="setToDefaultValue1"/> + <checkOption selector="{{LayeredNavigationSection.PriceNavigationStepSystemValue}}" stepKey="setToDefaultValue2"/> + <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig2" /> + <waitForPageLoad stepKey="waitForSavingSystemConfiguration2"/> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/ShopByButtonInMobile.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/ShopByButtonInMobile.xml new file mode 100644 index 000000000000..28b937f61ee9 --- /dev/null +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/ShopByButtonInMobile.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="ShopByButtonInMobile"> + <annotations> + <features value="Layered Navigation"/> + <stories value="Shop By button is not working in a mobile theme"/> + <title value="Layered Navigation"/> + <description value="Storefront Shop By collapsible button works in a mobile theme"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-94873"/> + <group value="LayeredNavigation"/> + </annotations> + <before> + <createData entity="productDropDownAttribute" stepKey="attribute"/> + <createData entity="productAttributeOption1" stepKey="option1"> + <requiredEntity createDataKey="attribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="attributeSet"> + <requiredEntity createDataKey="attribute"/> + </createData> + <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="attribute" stepKey="deleteAttribute"/> + <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + <resizeWindow width="1280" height="1024" stepKey="resizeWindowToDesktop"/> + </after> + <!-- Go to default attribute set edit page and add the product attribute to the set --> + <comment userInput="Go to default attribute set edit page and add the product attribute to the set" stepKey="commentAttributeSetEdit" /> + <amOnPage url="{{AdminProductAttributeSetEditPage.url}}/{{AddToDefaultSet.attributeSetId}}/" stepKey="onAttributeSetEdit"/> + <dragAndDrop selector1="{{AdminProductAttributeSetSection.attribute($$attribute.attribute[attribute_code]$$)}}" selector2="{{AdminProductAttributeSetSection.attribute('Product Details')}}" stepKey="assignTestAttributes"/> + <click selector="{{AdminProductAttributeSetSection.saveBtn}}" stepKey="clickAttributeSetSave"/> + <!-- Go to simple product edit page and set the product attribute to a value --> + <comment userInput="Go to simple product edit page and set the product attribute to a value" stepKey="commentProductAttributeEdit" /> + <amOnPage url="{{AdminProductEditPage.url($$simpleProduct1.id$$)}}" stepKey="goToEditPage"/> + <selectOption selector="{{AdminProductFormSection.customSelectField($$attribute.attribute[attribute_code]$$)}}" userInput="option1" stepKey="selectAttribute"/> + <actionGroup ref="saveProductForm" stepKey="saveSimpleProduct"/> + <!-- Check storefront mobile view for shop by button is functioning as expected --> + <comment userInput="Check storefront mobile view for shop by button is functioning as expected" stepKey="commentCheckShopByButton" /> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToHomePage"/> + <waitForPageLoad stepKey="waitForHomePageToLoad"/> + <submitForm selector="#search_mini_form" parameterArray="['q' => 'Test Simple Product']" stepKey="fillSearchBar" /> + <resizeWindow width="600" height="800" stepKey="resizeWindow"/> + <waitForPageLoad stepKey="waitForHomePageToLoad2"/> + <seeElement selector="{{StorefrontCategorySidebarMobileSection.shopByButton}}" stepKey="seeShopByButton"/> + <click selector="{{StorefrontCategorySidebarMobileSection.shopByButton}}" stepKey="clickShopByButton"/> + <seeElement selector="{{StorefrontCategorySidebarSection.filterOptionsTitle('$$attribute.attribute[default_label]$$')}}" stepKey="seeShopByOption"/> + </test> +</tests> diff --git a/app/code/Magento/LayeredNavigation/etc/adminhtml/system.xml b/app/code/Magento/LayeredNavigation/etc/adminhtml/system.xml index de4637847456..8d3f70c2806a 100644 --- a/app/code/Magento/LayeredNavigation/etc/adminhtml/system.xml +++ b/app/code/Magento/LayeredNavigation/etc/adminhtml/system.xml @@ -20,7 +20,7 @@ </field> <field id="price_range_step" translate="label" type="text" sortOrder="15" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Default Price Navigation Step</label> - <validate>validate-number validate-number-range number-range-0.01-1000000000</validate> + <validate>validate-number validate-number-range number-range-0.01-9999999999999999</validate> <depends> <field id="price_range_calculation">manual</field> </depends> diff --git a/app/code/Magento/LayeredNavigation/view/adminhtml/ui_component/product_attribute_add_form.xml b/app/code/Magento/LayeredNavigation/view/adminhtml/ui_component/product_attribute_add_form.xml index 000e10197af1..0d7476081d19 100644 --- a/app/code/Magento/LayeredNavigation/view/adminhtml/ui_component/product_attribute_add_form.xml +++ b/app/code/Magento/LayeredNavigation/view/adminhtml/ui_component/product_attribute_add_form.xml @@ -11,6 +11,7 @@ <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="valuesForEnable" xsi:type="array"> + <item name="boolean" xsi:type="string">boolean</item> <item name="select" xsi:type="string">select</item> <item name="multiselect" xsi:type="string">multiselect</item> <item name="price" xsi:type="string">price</item> @@ -19,7 +20,7 @@ </item> </argument> <settings> - <notice translate="true">Can be used only with catalog input type Dropdown, Multiple Select and Price.</notice> + <notice translate="true">Can be used only with catalog input type Yes/No (Boolean), Dropdown, Multiple Select and Price.</notice> <dataType>string</dataType> <label translate="true">Use in Layered Navigation</label> <dataScope>is_filterable</dataScope> @@ -36,6 +37,7 @@ <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="valuesForEnable" xsi:type="array"> + <item name="boolean" xsi:type="string">boolean</item> <item name="select" xsi:type="string">select</item> <item name="multiselect" xsi:type="string">multiselect</item> <item name="price" xsi:type="string">price</item> @@ -45,7 +47,7 @@ </item> </argument> <settings> - <notice translate="true">Can be used only with catalog input type Dropdown, Multiple Select and Price.</notice> + <notice translate="true">Can be used only with catalog input type Yes/No (Boolean), Dropdown, Multiple Select and Price.</notice> <label translate="true">Use in Search Results Layered Navigation</label> <dataScope>is_filterable_in_search</dataScope> <imports> diff --git a/app/code/Magento/Marketplace/Block/Partners.php b/app/code/Magento/Marketplace/Block/Partners.php index 4f8ca798f175..30d6a2910f4d 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 6e3929296e25..aae90512b3d9 100644 --- a/app/code/Magento/MediaStorage/Service/ImageResize.php +++ b/app/code/Magento/MediaStorage/Service/ImageResize.php @@ -24,6 +24,8 @@ use Magento\Framework\App\Filesystem\DirectoryList; /** + * Image resize service. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ImageResize @@ -123,7 +125,8 @@ public function __construct( } /** - * Create resized images of different sizes from an original image + * Create resized images of different sizes from an original image. + * * @param string $originalImageName * @throws NotFoundException */ @@ -141,7 +144,8 @@ public function resizeFromImageName(string $originalImageName) } /** - * Create resized images of different sizes from themes + * Create resized images of different sizes from themes. + * * @param array|null $themes * @return \Generator * @throws NotFoundException @@ -169,7 +173,8 @@ public function resizeFromThemes(array $themes = null): \Generator } /** - * Search the current theme + * Search the current theme. + * * @return array */ private function getThemesInUse(): array @@ -187,7 +192,8 @@ private function getThemesInUse(): array } /** - * Get view images data from themes + * Get view images data from themes. + * * @param array $themes * @return array */ @@ -211,7 +217,8 @@ private function getViewImages(array $themes): array } /** - * Get unique image index + * Get unique image index. + * * @param array $imageData * @return string */ @@ -223,7 +230,8 @@ private function getUniqueImageIndex(array $imageData): string } /** - * Make image + * Make image. + * * @param string $originalImagePath * @param array $imageParams * @return Image @@ -241,7 +249,8 @@ private function makeImage(string $originalImagePath, array $imageParams): Image } /** - * Resize image + * Resize image. + * * @param array $viewImage * @param string $originalImagePath * @param string $originalImageName @@ -257,9 +266,41 @@ private function resize(array $viewImage, string $originalImagePath, string $ori ] ); + if (isset($imageParams['watermark_file'])) { + if ($imageParams['watermark_height'] !== null) { + $image->setWatermarkHeight($imageParams['watermark_height']); + } + + if ($imageParams['watermark_width'] !== null) { + $image->setWatermarkWidth($imageParams['watermark_width']); + } + + if ($imageParams['watermark_position'] !== null) { + $image->setWatermarkPosition($imageParams['watermark_position']); + } + + if ($imageParams['watermark_image_opacity'] !== null) { + $image->setWatermarkImageOpacity($imageParams['watermark_image_opacity']); + } + + $image->watermark($this->getWatermarkFilePath($imageParams['watermark_file'])); + } + if ($imageParams['image_width'] !== null && $imageParams['image_height'] !== null) { $image->resize($imageParams['image_width'], $imageParams['image_height']); } $image->save($imageAsset->getPath()); } + + /** + * Returns watermark file absolute path + * + * @param string $file + * @return string + */ + private function getWatermarkFilePath($file) + { + $path = $this->imageConfig->getMediaPath('/watermark/' . $file); + return $this->mediaDirectory->getAbsolutePath($path); + } } diff --git a/app/code/Magento/MessageQueue/Model/Cron/ConsumersRunner.php b/app/code/Magento/MessageQueue/Model/Cron/ConsumersRunner.php index f301fb9289ef..f5011248d777 100644 --- a/app/code/Magento/MessageQueue/Model/Cron/ConsumersRunner.php +++ b/app/code/Magento/MessageQueue/Model/Cron/ConsumersRunner.php @@ -5,9 +5,13 @@ */ namespace Magento\MessageQueue\Model\Cron; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\MessageQueue\ConnectionTypeResolver; +use Magento\Framework\MessageQueue\Consumer\Config\ConsumerConfigItemInterface; use Magento\Framework\ShellInterface; use Magento\Framework\MessageQueue\Consumer\ConfigInterface as ConsumerConfigInterface; use Magento\Framework\App\DeploymentConfig; +use Psr\Log\LoggerInterface; use Symfony\Component\Process\PhpExecutableFinder; use Magento\MessageQueue\Model\Cron\ConsumersRunner\PidConsumerManager; @@ -56,6 +60,16 @@ class ConsumersRunner */ private $pidConsumerManager; + /** + * @var ConnectionTypeResolver + */ + private $mqConnectionTypeResolver; + + /** + * @var LoggerInterface + */ + private $logger; + /** * @param PhpExecutableFinder $phpExecutableFinder The executable finder specifically designed * for the PHP executable @@ -63,19 +77,27 @@ class ConsumersRunner * @param DeploymentConfig $deploymentConfig The application deployment configuration * @param ShellInterface $shellBackground The shell command line wrapper for executing command in background * @param PidConsumerManager $pidConsumerManager The class for checking status of process by PID + * @param ConnectionTypeResolver $mqConnectionTypeResolver Consumer connection resolver + * @param LoggerInterface $logger Logger */ public function __construct( PhpExecutableFinder $phpExecutableFinder, ConsumerConfigInterface $consumerConfig, DeploymentConfig $deploymentConfig, ShellInterface $shellBackground, - PidConsumerManager $pidConsumerManager + PidConsumerManager $pidConsumerManager, + ConnectionTypeResolver $mqConnectionTypeResolver = null, + LoggerInterface $logger = null ) { $this->phpExecutableFinder = $phpExecutableFinder; $this->consumerConfig = $consumerConfig; $this->deploymentConfig = $deploymentConfig; $this->shellBackground = $shellBackground; $this->pidConsumerManager = $pidConsumerManager; + $this->mqConnectionTypeResolver = $mqConnectionTypeResolver + ?: ObjectManager::getInstance()->get(ConnectionTypeResolver::class); + $this->logger = $logger + ?: ObjectManager::getInstance()->get(LoggerInterface::class); } /** @@ -94,12 +116,12 @@ public function run() $php = $this->phpExecutableFinder->find() ?: 'php'; foreach ($this->consumerConfig->getConsumers() as $consumer) { - $consumerName = $consumer->getName(); - - if (!$this->canBeRun($consumerName, $allowedConsumers)) { + if (!$this->canBeRun($consumer, $allowedConsumers)) { continue; } + $consumerName = $consumer->getName(); + $arguments = [ $consumerName, '--pid-file-path=' . $this->getPidFilePath($consumerName), @@ -119,16 +141,37 @@ public function run() /** * Checks that the consumer can be run * - * @param string $consumerName The consumer name + * @param ConsumerConfigItemInterface $consumerConfig The consumer config * @param array $allowedConsumers The list of allowed consumers * If $allowedConsumers is empty it means that all consumers are allowed * @return bool Returns true if the consumer can be run + * @throws \Magento\Framework\Exception\FileSystemException */ - private function canBeRun($consumerName, array $allowedConsumers = []) + private function canBeRun(ConsumerConfigItemInterface $consumerConfig, array $allowedConsumers = []): bool { - $allowed = empty($allowedConsumers) ?: in_array($consumerName, $allowedConsumers); + $consumerName = $consumerConfig->getName(); + if (!empty($allowedConsumers) && !in_array($consumerName, $allowedConsumers)) { + return false; + } + + if ($this->pidConsumerManager->isRun($this->getPidFilePath($consumerName))) { + return false; + } + + $connectionName = $consumerConfig->getConnection(); + try { + $this->mqConnectionTypeResolver->getConnectionType($connectionName); + } catch (\LogicException $e) { + $this->logger->info(sprintf( + 'Consumer "%s" skipped as required connection "%s" is not configured. %s', + $consumerName, + $connectionName, + $e->getMessage() + )); + return false; + } - return $allowed && !$this->pidConsumerManager->isRun($this->getPidFilePath($consumerName)); + return true; } /** diff --git a/app/code/Magento/MessageQueue/Model/PoisonPillCompare.php b/app/code/Magento/MessageQueue/Model/PoisonPillCompare.php new file mode 100644 index 000000000000..ffa478aecf36 --- /dev/null +++ b/app/code/Magento/MessageQueue/Model/PoisonPillCompare.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MessageQueue\Model; + +use Magento\Framework\MessageQueue\PoisonPill\PoisonPillCompareInterface; +use Magento\Framework\MessageQueue\PoisonPill\PoisonPillReadInterface; + +/** + * Poison pill compare + */ +class PoisonPillCompare implements PoisonPillCompareInterface +{ + /** + * @var PoisonPillReadInterface + */ + private $poisonPillRead; + + /** + * PoisonPillCompare constructor. + * @param PoisonPillReadInterface $poisonPillRead + */ + public function __construct( + PoisonPillReadInterface $poisonPillRead + ) { + $this->poisonPillRead = $poisonPillRead; + } + + /** + * @inheritdoc + */ + public function isLatestVersion(string $poisonPillVersion): bool + { + return $poisonPillVersion === $this->poisonPillRead->getLatestVersion(); + } +} diff --git a/app/code/Magento/MessageQueue/Model/ResourceModel/PoisonPill.php b/app/code/Magento/MessageQueue/Model/ResourceModel/PoisonPill.php new file mode 100644 index 000000000000..e59abec8724f --- /dev/null +++ b/app/code/Magento/MessageQueue/Model/ResourceModel/PoisonPill.php @@ -0,0 +1,74 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MessageQueue\Model\ResourceModel; + +use Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface; +use Magento\Framework\MessageQueue\PoisonPill\PoisonPillReadInterface; +use Magento\Framework\Model\ResourceModel\Db\AbstractDb; + +/** + * PoisonPill class that enclose read and put interface. + */ +class PoisonPill extends AbstractDb implements PoisonPillPutInterface, PoisonPillReadInterface +{ + /** + * Table name. + */ + const QUEUE_POISON_PILL_TABLE = 'queue_poison_pill'; + + /** + * @inheritdoc + */ + protected function _construct() + { + $this->_init(self::QUEUE_POISON_PILL_TABLE, 'version'); + } + + /** + * @inheritdoc + */ + public function put(): string + { + $connection = $this->getConnection(); + $table = $this->getMainTable(); + $uuid = uniqid('version-'); + $version = $this->getVersionFromDb(); + if ($version !== '') { + $connection->update($table, ['version' => $uuid]); + } else { + $connection->insert($table, ['version' => $uuid]); + } + + return $uuid; + } + + /** + * @inheritdoc + */ + public function getLatestVersion(): string + { + return $this->getVersionFromDb(); + } + + /** + * Returns version form DB or null. + * + * @return string + */ + private function getVersionFromDb(): string + { + $select = $this->getConnection()->select()->from( + $this->getTable(self::QUEUE_POISON_PILL_TABLE), + 'version' + ); + + $result = $this->getConnection()->fetchOne($select); + + return (string)$result; + } +} diff --git a/app/code/Magento/MessageQueue/Test/Unit/Model/Cron/ConsumersRunnerTest.php b/app/code/Magento/MessageQueue/Test/Unit/Model/Cron/ConsumersRunnerTest.php index bf8796a03f52..006354b997d3 100644 --- a/app/code/Magento/MessageQueue/Test/Unit/Model/Cron/ConsumersRunnerTest.php +++ b/app/code/Magento/MessageQueue/Test/Unit/Model/Cron/ConsumersRunnerTest.php @@ -5,6 +5,7 @@ */ namespace Magento\MessageQueue\Test\Unit\Model\Cron; +use Magento\Framework\MessageQueue\ConnectionTypeResolver; use \PHPUnit_Framework_MockObject_MockObject as MockObject; use Magento\Framework\ShellInterface; use Magento\Framework\MessageQueue\Consumer\ConfigInterface as ConsumerConfigInterface; @@ -41,6 +42,11 @@ class ConsumersRunnerTest extends \PHPUnit\Framework\TestCase */ private $phpExecutableFinderMock; + /** + * @var ConnectionTypeResolver + */ + private $connectionTypeResover; + /** * @var ConsumersRunner */ @@ -66,13 +72,18 @@ protected function setUp() $this->deploymentConfigMock = $this->getMockBuilder(DeploymentConfig::class) ->disableOriginalConstructor() ->getMock(); + $this->connectionTypeResover = $this->getMockBuilder(ConnectionTypeResolver::class) + ->disableOriginalConstructor() + ->getMock(); + $this->connectionTypeResover->method('getConnectionType')->willReturn('something'); $this->consumersRunner = new ConsumersRunner( $this->phpExecutableFinderMock, $this->consumerConfigMock, $this->deploymentConfigMock, $this->shellBackgroundMock, - $this->pidConsumerManagerMock + $this->pidConsumerManagerMock, + $this->connectionTypeResover ); } diff --git a/app/code/Magento/MessageQueue/etc/db_schema.xml b/app/code/Magento/MessageQueue/etc/db_schema.xml index 9cbad4d32a88..4403384e9311 100644 --- a/app/code/Magento/MessageQueue/etc/db_schema.xml +++ b/app/code/Magento/MessageQueue/etc/db_schema.xml @@ -14,11 +14,14 @@ <column xsi:type="varchar" name="message_code" nullable="false" length="255" default="" comment="Message Code"/> <column xsi:type="timestamp" name="created_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> </constraint> - <constraint xsi:type="unique" name="QUEUE_LOCK_MESSAGE_CODE"> + <constraint xsi:type="unique" referenceId="QUEUE_LOCK_MESSAGE_CODE"> <column name="message_code"/> </constraint> </table> + <table name="queue_poison_pill" resource="default" engine="innodb" comment="Sequence table for poison pill versions"> + <column xsi:type="varchar" name="version" length="255" nullable="false" comment="Poison Pill version."/> + </table> </schema> diff --git a/app/code/Magento/MessageQueue/etc/db_schema_whitelist.json b/app/code/Magento/MessageQueue/etc/db_schema_whitelist.json index f31981d2ec40..37a342b21e64 100644 --- a/app/code/Magento/MessageQueue/etc/db_schema_whitelist.json +++ b/app/code/Magento/MessageQueue/etc/db_schema_whitelist.json @@ -9,5 +9,10 @@ "PRIMARY": true, "QUEUE_LOCK_MESSAGE_CODE": true } + }, + "queue_poison_pill": { + "column": { + "version": true + } } -} \ No newline at end of file +} diff --git a/app/code/Magento/MessageQueue/etc/di.xml b/app/code/Magento/MessageQueue/etc/di.xml index c8f2edb86261..f60eb5fbc20d 100644 --- a/app/code/Magento/MessageQueue/etc/di.xml +++ b/app/code/Magento/MessageQueue/etc/di.xml @@ -13,6 +13,9 @@ <preference for="Magento\Framework\MessageQueue\EnvelopeInterface" type="Magento\Framework\MessageQueue\Envelope"/> <preference for="Magento\Framework\MessageQueue\ConsumerInterface" type="Magento\Framework\MessageQueue\Consumer"/> <preference for="Magento\Framework\MessageQueue\MergedMessageInterface" type="Magento\Framework\MessageQueue\MergedMessage"/> + <preference for="Magento\Framework\MessageQueue\PoisonPill\PoisonPillCompareInterface" type="Magento\MessageQueue\Model\PoisonPillCompare"/> + <preference for="Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface" type="Magento\MessageQueue\Model\ResourceModel\PoisonPill"/> + <preference for="Magento\Framework\MessageQueue\PoisonPill\PoisonPillReadInterface" type="Magento\MessageQueue\Model\ResourceModel\PoisonPill"/> <type name="Magento\Framework\Console\CommandListInterface"> <arguments> <argument name="commands" xsi:type="array"> diff --git a/app/code/Magento/Msrp/Helper/Data.php b/app/code/Magento/Msrp/Helper/Data.php index b4ec34ebee19..2f6dd2da9bbc 100644 --- a/app/code/Magento/Msrp/Helper/Data.php +++ b/app/code/Magento/Msrp/Helper/Data.php @@ -7,13 +7,17 @@ use Magento\Framework\App\Helper\AbstractHelper; use Magento\Framework\App\Helper\Context; +use Magento\Framework\App\ObjectManager; use Magento\Msrp\Model\Product\Attribute\Source\Type; +use Magento\Msrp\Pricing\MsrpPriceCalculatorInterface; use Magento\Store\Model\StoreManagerInterface; use Magento\Catalog\Model\Product; use Magento\Catalog\Api\ProductRepositoryInterface; /** * Msrp data helper + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Data extends AbstractHelper { @@ -42,6 +46,11 @@ class Data extends AbstractHelper */ protected $productRepository; + /** + * @var MsrpPriceCalculatorInterface + */ + private $msrpPriceCalculator; + /** * @param Context $context * @param StoreManagerInterface $storeManager @@ -50,6 +59,7 @@ class Data extends AbstractHelper * @param \Magento\Msrp\Model\Config $config * @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency * @param ProductRepositoryInterface $productRepository + * @param MsrpPriceCalculatorInterface|null $msrpPriceCalculator */ public function __construct( Context $context, @@ -58,7 +68,8 @@ public function __construct( \Magento\Msrp\Model\Msrp $msrp, \Magento\Msrp\Model\Config $config, \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency, - ProductRepositoryInterface $productRepository + ProductRepositoryInterface $productRepository, + MsrpPriceCalculatorInterface $msrpPriceCalculator = null ) { parent::__construct($context); $this->storeManager = $storeManager; @@ -67,17 +78,19 @@ public function __construct( $this->config = $config; $this->priceCurrency = $priceCurrency; $this->productRepository = $productRepository; + $this->msrpPriceCalculator = $msrpPriceCalculator + ?: ObjectManager::getInstance()->get(MsrpPriceCalculatorInterface::class); } /** - * Check if can apply Minimum Advertise price to product - * in specific visibility + * Check if can apply Minimum Advertise price to product in specific visibility * * @param int|Product $product * @param int|null $visibility Check displaying price in concrete place (by default generally) * @return bool * * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function canApplyMsrp($product, $visibility = null) { @@ -111,6 +124,7 @@ public function canApplyMsrp($product, $visibility = null) * * @param Product $product * @return string + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function getMsrpPriceMessage($product) { @@ -128,6 +142,7 @@ public function getMsrpPriceMessage($product) * * @param int|Product $product * @return bool + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function isShowPriceOnGesture($product) { @@ -135,8 +150,11 @@ public function isShowPriceOnGesture($product) } /** + * Check if we should show MAP proce before order confirmation + * * @param int|Product $product * @return bool + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function isShowBeforeOrderConfirm($product) { @@ -144,23 +162,19 @@ public function isShowBeforeOrderConfirm($product) } /** + * Check if any MAP price is larger than as low as value. + * * @param int|Product $product - * @return bool|float + * @return bool + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function isMinimalPriceLessMsrp($product) { if (is_numeric($product)) { $product = $this->productRepository->getById($product, false, $this->storeManager->getStore()->getId()); } - $msrp = $product->getMsrp(); + $msrp = $this->msrpPriceCalculator->getMsrpPriceValue($product); $price = $product->getPriceInfo()->getPrice(\Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE); - if ($msrp === null) { - if ($product->getTypeId() !== \Magento\GroupedProduct\Model\Product\Type\Grouped::TYPE_CODE) { - return false; - } else { - $msrp = $product->getTypeInstance()->getChildrenMsrp($product); - } - } if ($msrp) { $msrp = $this->priceCurrency->convertAndRound($msrp); } diff --git a/app/code/Magento/Msrp/Model/Config.php b/app/code/Magento/Msrp/Model/Config.php index 2ee9f41870a5..3662a2cef8d2 100644 --- a/app/code/Magento/Msrp/Model/Config.php +++ b/app/code/Magento/Msrp/Model/Config.php @@ -10,6 +10,9 @@ use Magento\Store\Model\StoreManagerInterface; use Magento\Framework\Escaper; +/** + * Class Config + */ class Config { /**#@+ @@ -40,6 +43,8 @@ class Config protected $storeId; /** + * Config constructor. + * * @param ScopeConfigInterface $scopeConfig * @param StoreManagerInterface $storeManager * @param Escaper $escaper @@ -74,7 +79,7 @@ public function setStoreId($store) */ public function isEnabled() { - return (bool)$this->scopeConfig->getValue( + return $this->scopeConfig->isSetFlag( self::XML_PATH_MSRP_ENABLED, ScopeInterface::SCOPE_STORE, $this->storeId diff --git a/app/code/Magento/Msrp/Pricing/MsrpPriceCalculator.php b/app/code/Magento/Msrp/Pricing/MsrpPriceCalculator.php new file mode 100644 index 000000000000..af5a29eb288e --- /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 000000000000..c50a381a2efa --- /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 000000000000..892c0bcb5124 --- /dev/null +++ b/app/code/Magento/Msrp/Pricing/Render/PriceBox.php @@ -0,0 +1,62 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Msrp\Pricing\Render; + +use Magento\Catalog\Model\Product; +use Magento\Framework\Json\Helper\Data; +use Magento\Framework\Math\Random; +use Magento\Framework\Pricing\Price\PriceInterface; +use Magento\Framework\Pricing\Render\RendererPool; +use Magento\Framework\View\Element\Template\Context; +use Magento\Msrp\Pricing\MsrpPriceCalculatorInterface; + +/** + * MSRP price box render. + */ +class PriceBox extends \Magento\Catalog\Pricing\Render\PriceBox +{ + /** + * @var MsrpPriceCalculatorInterface + */ + private $msrpPriceCalculator; + + /** + * Constructor + * + * @param Context $context + * @param Product $saleableItem + * @param PriceInterface $price + * @param RendererPool $rendererPool + * @param Data $jsonHelper + * @param Random $mathRandom + * @param MsrpPriceCalculatorInterface $msrpPriceCalculator + */ + public function __construct( + Context $context, + Product $saleableItem, + PriceInterface $price, + RendererPool $rendererPool, + Data $jsonHelper, + Random $mathRandom, + MsrpPriceCalculatorInterface $msrpPriceCalculator + ) { + $this->msrpPriceCalculator = $msrpPriceCalculator; + parent::__construct($context, $saleableItem, $price, $rendererPool, $jsonHelper, $mathRandom); + } + + /** + * Return MSRP price calculator. + * + * @return MsrpPriceCalculatorInterface + */ + public function getMsrpPriceCalculator(): MsrpPriceCalculatorInterface + { + return $this->msrpPriceCalculator; + } +} diff --git a/app/code/Magento/Msrp/Test/Mftf/Data/MsrpSettingsData.xml b/app/code/Magento/Msrp/Test/Mftf/Data/MsrpSettingsData.xml new file mode 100644 index 000000000000..3922bb486891 --- /dev/null +++ b/app/code/Magento/Msrp/Test/Mftf/Data/MsrpSettingsData.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="MsrpEnableMAP" type="msrp_settings_config"> + <requiredEntity type="enabled">EnableMAP</requiredEntity> + </entity> + <entity name="EnableMAP" type="msrp_settings_config"> + <data key="value">1</data> + </entity> + + <entity name="MsrpDisableMAP" type="msrp_settings_config"> + <requiredEntity type="enabled">DisableMAP</requiredEntity> + </entity> + <entity name="DisableMAP" type="msrp_settings_config"> + <data key="value">0</data> + </entity> +</entities> diff --git a/app/code/Magento/Msrp/Test/Mftf/Metadata/msrp_settings-meta.xml b/app/code/Magento/Msrp/Test/Mftf/Metadata/msrp_settings-meta.xml new file mode 100644 index 000000000000..be91a548ad90 --- /dev/null +++ b/app/code/Magento/Msrp/Test/Mftf/Metadata/msrp_settings-meta.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="MsrpSettingsConfig" dataType="msrp_settings_config" type="create" auth="adminFormKey" url="/admin/system_config/save/section/sales/" method="POST"> + <object key="groups" dataType="msrp_settings_config"> + <object key="msrp" dataType="msrp_settings_config"> + <object key="fields" dataType="msrp_settings_config"> + <object key="enabled" dataType="enabled"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> +</operations> \ No newline at end of file diff --git a/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontProductWithMapAssignedConfigProductIsCorrectTest.xml b/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontProductWithMapAssignedConfigProductIsCorrectTest.xml new file mode 100644 index 000000000000..a874de3b223a --- /dev/null +++ b/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontProductWithMapAssignedConfigProductIsCorrectTest.xml @@ -0,0 +1,157 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontProductWithMapAssignedConfigProductIsCorrectTest"> + <annotations> + <features value="Msrp"/> + <title value="Check that simple products with MAP assigned to configurable product displayed correctly"/> + <description value="Check that simple products with MAP assigned to configurable product displayed correctly"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-12292"/> + <useCaseId value="MC-10973"/> + <group value="Msrp"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!--Create category--> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + + <!-- Create the configurable product based on the data in the /data folder --> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Make the configurable product have two options, that are children of the default attribute set --> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption3" stepKey="createConfigProductAttributeOption3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="3" stepKey="getConfigAttributeOption3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create the 2 children that will be a part of the configurable product --> + <createData entity="ApiSimpleProductWithPrice50" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <createData entity="ApiSimpleProductWithPrice60" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <createData entity="ApiSimpleProductWithPrice70" stepKey="createConfigChildProduct3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption3"/> + </createData> + + <!-- Assign the two products to the configurable product --> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + <requiredEntity createDataKey="getConfigAttributeOption3"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild3"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct3"/> + </createData> + + <!--Enable Minimum advertised Price--> + <createData entity="MsrpEnableMAP" stepKey="enableMAP"/> + </before> + <after> + <!--Delete created data--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> + <deleteData createDataKey="createConfigChildProduct3" stepKey="deleteConfigChildProduct3"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + + <!--Disable Minimum advertised Price--> + <createData entity="MsrpDisableMAP" stepKey="disableMAP"/> + + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + + <!-- Set Manufacturer's Suggested Retail Price to products--> + <amOnPage url="{{AdminProductEditPage.url($$createConfigChildProduct1.id$$)}}" stepKey="goToFirstChildProductEditPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingButton"/> + <waitForElement selector="{{AdminProductFormAdvancedPricingSection.msrp}}" stepKey="waitForMsrp"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.msrp}}" userInput="55" stepKey="setMsrpForFirstChildProduct"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct1"/> + + <amOnPage url="{{AdminProductEditPage.url($$createConfigChildProduct2.id$$)}}" stepKey="goToSecondChildProductEditPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad1"/> + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingButton1"/> + <waitForElement selector="{{AdminProductFormAdvancedPricingSection.msrp}}" stepKey="waitForMsrp1"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.msrp}}" userInput="66" stepKey="setMsrpForSecondChildProduct"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton1"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + + <!--Clear cache--> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + + <!--Go to store front and check msrp for products--> + <amOnPage url="{{StorefrontProductPage.url($$createConfigProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToConfigProductPage"/> + <waitForPageLoad stepKey="waitForLoadConfigProductPage"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.mapPrice}}" stepKey="grabMapPrice"/> + <assertEquals expected='$66.00' expectedType="string" actual="($grabMapPrice)" stepKey="assertMapPrice"/> + <seeElement selector="{{StorefrontProductInfoMainSection.clickForPriceLink}}" stepKey="checkClickForPriceLink"/> + + <!--Check msrp for second child product--> + <selectOption selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" userInput="$$getConfigAttributeOption2.value$$" stepKey="selectSecondOption"/> + <waitForElement selector="{{StorefrontProductInfoMainSection.mapPrice}}" stepKey="waitForLoad"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.mapPrice}}" stepKey="grabSecondProductMapPrice"/> + <assertEquals expected='$66.00' expectedType="string" actual="($grabSecondProductMapPrice)" stepKey="assertSecondProductMapPrice"/> + <seeElement selector="{{StorefrontProductInfoMainSection.clickForPriceLink}}" stepKey="checkClickForPriceLinkForSecondProduct"/> + + <!--Check msrp for first child product--> + <selectOption selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" userInput="$$getConfigAttributeOption1.value$$" stepKey="selectFirstOption"/> + <waitForElement selector="{{StorefrontProductInfoMainSection.mapPrice}}" stepKey="waitForLoad1"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.mapPrice}}" stepKey="grabFirstProductMapPrice"/> + <assertEquals expected='$55.00' expectedType="string" actual="($grabFirstProductMapPrice)" stepKey="assertFirstProductMapPrice"/> + <seeElement selector="{{StorefrontProductInfoMainSection.clickForPriceLink}}" stepKey="checkClickForPriceLinkForFirstProduct"/> + + <!--Check price for third child product--> + <selectOption selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" userInput="$$getConfigAttributeOption3.value$$" stepKey="selectThirdOption"/> + <waitForElement selector="{{StorefrontProductInfoMainSection.mapPrice}}" stepKey="waitForLoad2"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="grabThirdProductMapPrice"/> + <assertEquals expected='$70.00' expectedType="string" actual="($grabThirdProductMapPrice)" stepKey="assertThirdProductMapPrice"/> + <dontSeeElement selector="{{StorefrontProductInfoMainSection.clickForPriceLink}}" stepKey="checkClickForPriceLinkForThirdProduct"/> + </test> +</tests> diff --git a/app/code/Magento/Msrp/Test/Unit/Helper/DataTest.php b/app/code/Magento/Msrp/Test/Unit/Helper/DataTest.php index 19f46b06ac5a..e4cd411a2e0b 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 6e7bf61063a2..a2e6da6de538 100644 --- a/app/code/Magento/Msrp/composer.json +++ b/app/code/Magento/Msrp/composer.json @@ -10,7 +10,6 @@ "magento/module-catalog": "*", "magento/module-downloadable": "*", "magento/module-eav": "*", - "magento/module-grouped-product": "*", "magento/module-store": "*", "magento/module-tax": "*" }, diff --git a/app/code/Magento/Msrp/etc/adminhtml/system.xml b/app/code/Magento/Msrp/etc/adminhtml/system.xml index 8ce0ea67343f..c20d753a2e79 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 6f197f769d41..b8392b0bb0fe 100644 --- a/app/code/Magento/Msrp/etc/di.xml +++ b/app/code/Magento/Msrp/etc/di.xml @@ -7,6 +7,7 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <preference for="\Magento\Msrp\Api\Data\ProductRender\MsrpPriceInfoInterface" type="\Magento\Msrp\Model\ProductRender\MsrpPriceInfo" /> + <preference for="\Magento\Msrp\Pricing\MsrpPriceCalculatorInterface" type="\Magento\Msrp\Pricing\MsrpPriceCalculator"/> <virtualType name="Magento\Catalog\Pricing\Price\Pool"> <arguments> <argument name="prices" xsi:type="array"> diff --git a/app/code/Magento/Msrp/i18n/de_DE.csv b/app/code/Magento/Msrp/i18n/de_DE.csv deleted file mode 100644 index be8e733a4bfd..000000000000 --- a/app/code/Magento/Msrp/i18n/de_DE.csv +++ /dev/null @@ -1,2 +0,0 @@ -"Enabling MAP by default will hide all product prices on Storefront.","Enabling MAP by default will hide all product prices on Storefront." -"Warning!","Warning!" diff --git a/app/code/Magento/Msrp/i18n/en_US.csv b/app/code/Magento/Msrp/i18n/en_US.csv index d647f8527ec1..d47d72b2bdc9 100644 --- a/app/code/Magento/Msrp/i18n/en_US.csv +++ b/app/code/Magento/Msrp/i18n/en_US.csv @@ -13,6 +13,7 @@ Price,Price "Add to Cart","Add to Cart" "Minimum Advertised Price","Minimum Advertised Price" "Enable MAP","Enable MAP" +"<strong style=""color:red"">Warning!</strong> Enabling MAP by default will hide all product prices on Storefront.","<strong style=""color:red"">Warning!</strong> Enabling MAP by default will hide all product prices on Storefront." "Display Actual Price","Display Actual Price" "Default Popup Text Message","Default Popup Text Message" "Default ""What's This"" Text Message","Default ""What's This"" Text Message" diff --git a/app/code/Magento/Msrp/i18n/es_ES.csv b/app/code/Magento/Msrp/i18n/es_ES.csv deleted file mode 100644 index be8e733a4bfd..000000000000 --- a/app/code/Magento/Msrp/i18n/es_ES.csv +++ /dev/null @@ -1,2 +0,0 @@ -"Enabling MAP by default will hide all product prices on Storefront.","Enabling MAP by default will hide all product prices on Storefront." -"Warning!","Warning!" diff --git a/app/code/Magento/Msrp/i18n/fr_FR.csv b/app/code/Magento/Msrp/i18n/fr_FR.csv deleted file mode 100644 index be8e733a4bfd..000000000000 --- a/app/code/Magento/Msrp/i18n/fr_FR.csv +++ /dev/null @@ -1,2 +0,0 @@ -"Enabling MAP by default will hide all product prices on Storefront.","Enabling MAP by default will hide all product prices on Storefront." -"Warning!","Warning!" diff --git a/app/code/Magento/Msrp/i18n/nl_NL.csv b/app/code/Magento/Msrp/i18n/nl_NL.csv deleted file mode 100644 index be8e733a4bfd..000000000000 --- a/app/code/Magento/Msrp/i18n/nl_NL.csv +++ /dev/null @@ -1,2 +0,0 @@ -"Enabling MAP by default will hide all product prices on Storefront.","Enabling MAP by default will hide all product prices on Storefront." -"Warning!","Warning!" diff --git a/app/code/Magento/Msrp/i18n/pt_BR.csv b/app/code/Magento/Msrp/i18n/pt_BR.csv deleted file mode 100644 index be8e733a4bfd..000000000000 --- a/app/code/Magento/Msrp/i18n/pt_BR.csv +++ /dev/null @@ -1,2 +0,0 @@ -"Enabling MAP by default will hide all product prices on Storefront.","Enabling MAP by default will hide all product prices on Storefront." -"Warning!","Warning!" diff --git a/app/code/Magento/Msrp/i18n/zh_Hans_CN.csv b/app/code/Magento/Msrp/i18n/zh_Hans_CN.csv deleted file mode 100644 index be8e733a4bfd..000000000000 --- a/app/code/Magento/Msrp/i18n/zh_Hans_CN.csv +++ /dev/null @@ -1,2 +0,0 @@ -"Enabling MAP by default will hide all product prices on Storefront.","Enabling MAP by default will hide all product prices on Storefront." -"Warning!","Warning!" diff --git a/app/code/Magento/Msrp/view/base/layout/catalog_product_prices.xml b/app/code/Magento/Msrp/view/base/layout/catalog_product_prices.xml index 9e2dd2063864..b8a3910bf21b 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/layout/cms_index_index.xml b/app/code/Magento/Msrp/view/base/layout/cms_index_index.xml new file mode 100644 index 000000000000..b60bc71e9e70 --- /dev/null +++ b/app/code/Magento/Msrp/view/base/layout/cms_index_index.xml @@ -0,0 +1,11 @@ +<?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" layout="2columns-left" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <update handle="msrp_popup"/> + <body/> +</page> \ No newline at end of file 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 869d81563645..a428df57ab11 100644 --- a/app/code/Magento/Msrp/view/base/templates/product/price/msrp.phtml +++ b/app/code/Magento/Msrp/view/base/templates/product/price/msrp.phtml @@ -9,7 +9,7 @@ /** * Template for displaying product price at product view page, gift registry and wish-list * - * @var $block \Magento\Catalog\Pricing\Render\PriceBox + * @var $block \Magento\Msrp\Pricing\Render\PriceBox */ ?> <?php @@ -20,8 +20,11 @@ $priceType = $block->getPrice(); /** @var $product \Magento\Catalog\Model\Product */ $product = $block->getSaleableItem(); $productId = $product->getId(); + +$amount = $block->getMsrpPriceCalculator()->getMsrpPriceValue($product); + $msrpPrice = $block->renderAmount( - $priceType->getCustomAmount($product->getMsrp() ?: $product->getTypeInstance()->getChildrenMsrp($product)), + $priceType->getCustomAmount($amount), [ 'price_id' => $block->getPriceId() ? $block->getPriceId() : 'old-price-' . $productId, 'include_container' => false, @@ -29,52 +32,56 @@ $msrpPrice = $block->renderAmount( ] ); $priceElementIdPrefix = $block->getPriceElementIdPrefix() ? $block->getPriceElementIdPrefix() : 'product-price-'; - -$addToCartUrl = ''; -if ($product->isSaleable()) { - /** @var Magento\Catalog\Block\Product\AbstractProduct $addToCartUrlGenerator */ - $addToCartUrlGenerator = $block->getLayout()->getBlockSingleton('Magento\Catalog\Block\Product\AbstractProduct'); - $addToCartUrl = $addToCartUrlGenerator->getAddToCartUrl( - $product, - ['_query' => [ - \Magento\Framework\App\ActionInterface::PARAM_NAME_URL_ENCODED => - $this->helper('Magento\Framework\Url\Helper\Data')->getEncodedUrl( - $addToCartUrlGenerator->getAddToCartUrl($product) - ), - ]] - ); -} ?> -<?php if ($product->getMsrp()): ?> + +<?php if ($amount): ?> <span class="old-price map-old-price"><?= /* @escapeNotVerified */ $msrpPrice ?></span> + <span class="map-fallback-price normal-price"><?= /* @escapeNotVerified */ $msrpPrice ?></span> <?php endif; ?> <?php if ($priceType->isShowPriceOnGesture()): ?> <?php - $priceElementId = $priceElementIdPrefix . $productId . $block->getIdSuffix(); - $popupId = 'msrp-popup-' . $productId . $block->getRandomString(20); - $data = ['addToCart' => [ - 'origin'=> 'msrp', - 'popupId' => '#' . $popupId, - 'productName' => $block->escapeJs($block->escapeHtml($product->getName())), - 'productId' => $productId, - 'productIdInput' => 'input[type="hidden"][name="product"]', - 'realPrice' => $block->getRealPriceHtml(), - 'isSaleable' => $product->isSaleable(), - 'msrpPrice' => $msrpPrice, - 'priceElementId' => $priceElementId, - 'closeButtonId' => '#map-popup-close', - 'addToCartUrl' => $addToCartUrl, - 'paymentButtons' => '[data-label=or]' - ]]; - if ($block->getRequest()->getFullActionName() === 'catalog_product_view') { - $data['addToCart']['addToCartButton'] = '#product_addtocart_form [type=submit]'; - } else { - $data['addToCart']['addToCartButton'] = sprintf( - 'form:has(input[type="hidden"][name="product"][value="%s"]) button[type="submit"]', - (int) $productId - ); - } + + $addToCartUrl = ''; + if ($product->isSaleable()) { + /** @var Magento\Catalog\Block\Product\AbstractProduct $addToCartUrlGenerator */ + $addToCartUrlGenerator = $block->getLayout()->getBlockSingleton('Magento\Catalog\Block\Product\AbstractProduct'); + $addToCartUrl = $addToCartUrlGenerator->getAddToCartUrl( + $product, + ['_query' => [ + \Magento\Framework\App\ActionInterface::PARAM_NAME_URL_ENCODED => + $this->helper('Magento\Framework\Url\Helper\Data')->getEncodedUrl( + $addToCartUrlGenerator->getAddToCartUrl($product) + ), + ]] + ); + } + + $priceElementId = $priceElementIdPrefix . $productId . $block->getIdSuffix(); + $popupId = 'msrp-popup-' . $productId . $block->getRandomString(20); + $data = ['addToCart' => [ + 'origin'=> 'msrp', + 'popupId' => '#' . $popupId, + 'productName' => $block->escapeJs($block->escapeHtml($product->getName())), + 'productId' => $productId, + 'productIdInput' => 'input[type="hidden"][name="product"]', + 'realPrice' => $block->getRealPriceHtml(), + 'isSaleable' => $product->isSaleable(), + 'msrpPrice' => $msrpPrice, + 'priceElementId' => $priceElementId, + 'closeButtonId' => '#map-popup-close', + 'addToCartUrl' => $addToCartUrl, + 'paymentButtons' => '[data-label=or]' + ]]; + if ($block->getRequest()->getFullActionName() === 'catalog_product_view') { + $data['addToCart']['addToCartButton'] = '#product_addtocart_form [type=submit]'; + } else { + $data['addToCart']['addToCartButton'] = sprintf( + 'form:has(input[type="hidden"][name="product"][value="%s"]) button[type="submit"]', + (int) $productId . ',' . + sprintf('.block.widget .price-box[data-product-id=%s]+.product-item-actions button.tocart', + (int) $productId)); + } ?> <span id="<?= /* @escapeNotVerified */ $block->getPriceId() ? $block->getPriceId() : $priceElementId ?>" style="display:none"></span> <a href="javascript:void(0);" @@ -98,4 +105,4 @@ if ($product->isSaleable()) { "productName": "<?= $block->escapeJs($block->escapeHtml($product->getName())) ?>", "closeButtonId": "#map-popup-close"}}'><span><?= /* @escapeNotVerified */ __("What's this?") ?></span> </a> -<?php endif; ?> +<?php endif; ?> \ No newline at end of file diff --git a/app/code/Magento/Msrp/view/base/web/js/msrp.js b/app/code/Magento/Msrp/view/base/web/js/msrp.js index deeadd9b55b8..a0bd3ec132de 100644 --- a/app/code/Magento/Msrp/view/base/web/js/msrp.js +++ b/app/code/Magento/Msrp/view/base/web/js/msrp.js @@ -4,11 +4,12 @@ */ define([ 'jquery', + 'Magento_Catalog/js/price-utils', 'underscore', 'jquery/ui', 'mage/dropdown', 'mage/template' -], function ($) { +], function ($, priceUtils, _) { 'use strict'; $.widget('mage.addToCart', { @@ -24,7 +25,14 @@ define([ // Selectors cartForm: '.form.map.checkout', msrpLabelId: '#map-popup-msrp', + msrpPriceElement: '#map-popup-msrp .price-wrapper', priceLabelId: '#map-popup-price', + priceElement: '#map-popup-price .price', + mapInfoLinks: '.map-show-info', + displayPriceElement: '.old-price.map-old-price .price-wrapper', + fallbackPriceElement: '.normal-price.map-fallback-price .price-wrapper', + displayPriceContainer: '.old-price.map-old-price', + fallbackPriceContainer: '.normal-price.map-fallback-price', popUpAttr: '[data-role=msrp-popup-template]', popupCartButtonId: '#map-popup-button', paypalCheckoutButons: '[data-action=checkout-form-submit]', @@ -59,9 +67,11 @@ define([ shadowHinter: 'popup popup-pointer' }, popupOpened: false, + wasOpened: false, /** * Creates widget instance + * * @private */ _create: function () { @@ -73,10 +83,13 @@ define([ this.initTierPopup(); } $(this.options.cartButtonId).on('click', this._addToCartSubmit.bind(this)); + $(document).on('updateMsrpPriceBlock', this.onUpdateMsrpPrice.bind(this)); + $(this.options.cartForm).on('submit', this._onSubmitForm.bind(this)); }, /** * Init msrp popup + * * @private */ initMsrpPopup: function () { @@ -89,7 +102,7 @@ define([ $msrpPopup.find('button') .on('click', - this.handleMsrpAddToCart.bind(this)) + this.handleMsrpAddToCart.bind(this)) .filter(this.options.popupCartButtonId) .text($(this.options.addToCartButton).text()); @@ -104,6 +117,7 @@ define([ /** * Init info popup + * * @private */ initInfoPopup: function () { @@ -212,8 +226,12 @@ define([ var options = this.tierOptions || this.options; this.popUpOptions.position.of = $(event.target); - this.$popup.find(this.options.msrpLabelId).html(options.msrpPrice); - this.$popup.find(this.options.priceLabelId).html(options.realPrice); + + if (!this.wasOpened) { + this.$popup.find(this.options.msrpLabelId).html(options.msrpPrice); + this.$popup.find(this.options.priceLabelId).html(options.realPrice); + this.wasOpened = true; + } this.$popup.dropdownDialog(this.popUpOptions).dropdownDialog('open'); this._toggle(this.$popup); @@ -223,6 +241,7 @@ define([ }, /** + * Toggle MAP popup visibility * * @param {HTMLElement} $elem * @private @@ -239,6 +258,7 @@ define([ }, /** + * Close MAP information popup * * @param {HTMLElement} $elem */ @@ -249,8 +269,10 @@ define([ /** * Handler for addToCart action + * + * @param {Object} e */ - _addToCartSubmit: function () { + _addToCartSubmit: function (e) { this.element.trigger('addToCart', this.element); if (this.element.data('stop-processing')) { @@ -266,9 +288,106 @@ define([ if (this.options.addToCartUrl) { $('.mage-dropdown-dialog > .ui-dialog-content').dropdownDialog('close'); } + + e.preventDefault(); $(this.options.cartForm).submit(); + }, + /** + * Call on event updatePrice. Proxy to updateMsrpPrice method. + * + * @param {Event} event + * @param {mixed} priceIndex + * @param {Object} prices + */ + onUpdateMsrpPrice: function onUpdateMsrpPrice(event, priceIndex, prices) { + + var defaultMsrp, + defaultPrice, + msrpPrice, + finalPrice; + + defaultMsrp = _.chain(prices).map(function (price) { + return price.msrpPrice.amount; + }).reject(function (p) { + return p === null; + }).max().value(); + + defaultPrice = _.chain(prices).map(function (p) { + return p.finalPrice.amount; + }).min().value(); + + if (typeof priceIndex !== 'undefined') { + msrpPrice = prices[priceIndex].msrpPrice.amount; + finalPrice = prices[priceIndex].finalPrice.amount; + + if (msrpPrice === null || msrpPrice <= finalPrice) { + this.updateNonMsrpPrice(priceUtils.formatPrice(finalPrice)); + } else { + this.updateMsrpPrice( + priceUtils.formatPrice(finalPrice), + priceUtils.formatPrice(msrpPrice), + false); + } + } else { + this.updateMsrpPrice( + priceUtils.formatPrice(defaultPrice), + priceUtils.formatPrice(defaultMsrp), + true); + } + }, + + /** + * Update prices for configurable product with MSRP enabled + * + * @param {String} finalPrice + * @param {String} msrpPrice + * @param {Boolean} useDefaultPrice + */ + updateMsrpPrice: function (finalPrice, msrpPrice, useDefaultPrice) { + var options = this.tierOptions || this.options; + + $(this.options.fallbackPriceContainer).hide(); + $(this.options.displayPriceContainer).show(); + $(this.options.mapInfoLinks).show(); + + if (useDefaultPrice || !this.wasOpened) { + this.$popup.find(this.options.msrpLabelId).html(options.msrpPrice); + this.$popup.find(this.options.priceLabelId).html(options.realPrice); + $(this.options.displayPriceElement).html(msrpPrice); + this.wasOpened = true; + } + + if (!useDefaultPrice) { + this.$popup.find(this.options.msrpPriceElement).html(msrpPrice); + this.$popup.find(this.options.priceElement).html(finalPrice); + $(this.options.displayPriceElement).html(msrpPrice); + } + }, + + /** + * Display non MAP price for irrelevant products + * + * @param {String} price + */ + updateNonMsrpPrice: function (price) { + $(this.options.fallbackPriceElement).html(price); + $(this.options.displayPriceContainer).hide(); + $(this.options.mapInfoLinks).hide(); + $(this.options.fallbackPriceContainer).show(); + }, + + /** + * Handler for submit form + * + * @private + */ + _onSubmitForm: function () { + if ($(this.options.cartForm).valid()) { + $(this.options.cartButtonId).prop('disabled', true); + } } + }); return $.mage.addToCart; diff --git a/app/code/Magento/Msrp/view/frontend/templates/popup.phtml b/app/code/Magento/Msrp/view/frontend/templates/popup.phtml index 7ccd606b8f11..e0b3dd77dedc 100644 --- a/app/code/Magento/Msrp/view/frontend/templates/popup.phtml +++ b/app/code/Magento/Msrp/view/frontend/templates/popup.phtml @@ -30,12 +30,11 @@ <span id="map-popup-price" class="actual-price"></span> </div> </div> - <form action="" method="POST" id="product_addtocart_form_from_popup" class="map-form-addtocart"> - <input type="hidden" name="product" class="product_id" value="" id="map-popup-product-id"/> + <form action="" method="POST" class="map-form-addtocart"> + <input type="hidden" name="product" class="product_id" value="" /> <button type="button" title="<?= $block->escapeHtml(__('Add to Cart')) ?>" - class="action tocart primary" - id="map-popup-button"> + class="action tocart primary"> <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> </button> <div class="additional-addtocart-box"> diff --git a/app/code/Magento/MsrpConfigurableProduct/Pricing/MsrpPriceCalculator.php b/app/code/Magento/MsrpConfigurableProduct/Pricing/MsrpPriceCalculator.php new file mode 100644 index 000000000000..b6f5194ab8cb --- /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 000000000000..8911b6e9e666 --- /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 000000000000..00c3cf6b0307 --- /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 000000000000..ea33c81ff7cf --- /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 000000000000..b00e363b0b26 --- /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 000000000000..d4d58ec3c013 --- /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 000000000000..b99f328a8b20 --- /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 000000000000..d597ba7fc18a --- /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 000000000000..a626f199ad6c --- /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 000000000000..29b25f15bc2c --- /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 000000000000..898dd904b5df --- /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 000000000000..c5a261e66c64 --- /dev/null +++ b/app/code/Magento/MsrpGroupedProduct/registration.php @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use \Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_MsrpGroupedProduct', __DIR__); diff --git a/app/code/Magento/Multishipping/Controller/Checkout/Addresses.php b/app/code/Magento/Multishipping/Controller/Checkout/Addresses.php index 78ea9fc1d2b8..f97589342bc3 100644 --- a/app/code/Magento/Multishipping/Controller/Checkout/Addresses.php +++ b/app/code/Magento/Multishipping/Controller/Checkout/Addresses.php @@ -6,9 +6,10 @@ */ namespace Magento\Multishipping\Controller\Checkout; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Multishipping\Model\Checkout\Type\Multishipping\State; -class Addresses extends \Magento\Multishipping\Controller\Checkout +class Addresses extends \Magento\Multishipping\Controller\Checkout implements HttpGetActionInterface { /** * Multishipping checkout select address page diff --git a/app/code/Magento/Multishipping/Controller/Checkout/Index.php b/app/code/Magento/Multishipping/Controller/Checkout/Index.php index 1fa96cfab8a1..a4314d6d5188 100644 --- a/app/code/Magento/Multishipping/Controller/Checkout/Index.php +++ b/app/code/Magento/Multishipping/Controller/Checkout/Index.php @@ -6,7 +6,9 @@ */ namespace Magento\Multishipping\Controller\Checkout; -class Index extends \Magento\Multishipping\Controller\Checkout +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\Multishipping\Controller\Checkout implements HttpGetActionInterface { /** * Index action of Multishipping checkout diff --git a/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php b/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php index 9412b1389559..42f5289d2109 100644 --- a/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php +++ b/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php @@ -176,8 +176,6 @@ class Multishipping extends \Magento\Framework\DataObject private $dataObjectHelper; /** - * Constructor - * * @param \Magento\Checkout\Model\Session $checkoutSession * @param \Magento\Customer\Model\Session $customerSession * @param \Magento\Sales\Model\OrderFactory $orderFactory @@ -202,8 +200,9 @@ class Multishipping extends \Magento\Framework\DataObject * @param array $data * @param \Magento\Quote\Api\Data\CartExtensionFactory|null $cartExtensionFactory * @param AllowedCountries|null $allowedCountryReader - * @param Multishipping\PlaceOrderFactory $placeOrderFactory - * @param LoggerInterface $logger + * @param Multishipping\PlaceOrderFactory|null $placeOrderFactory + * @param LoggerInterface|null $logger + * @param \Magento\Framework\Api\DataObjectHelper|null $dataObjectHelper * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -272,6 +271,7 @@ public function __construct( /** * Initialize multishipping checkout. + * * Split virtual/not virtual items between default billing/shipping addresses * * @return \Magento\Multishipping\Model\Checkout\Type\Multishipping @@ -337,6 +337,7 @@ protected function _init() /** * Get quote items assigned to different quote addresses populated per item qty. + * * Based on result array we can display each item separately * * @return array @@ -397,7 +398,7 @@ public function removeAddressItem($addressId, $itemId) /** * Assign quote items to addresses and specify items qty * - * array structure: + * Array structure: * array( * $quoteItemId => array( * 'qty' => $qty, @@ -693,6 +694,7 @@ protected function _prepareOrder(\Magento\Quote\Model\Quote\Address $address) $order->setIsVirtual(1); } else { $order->setShippingAddress($this->quoteAddressToOrderAddress->convert($address)); + $order->setShippingMethod($address->getShippingMethod()); } $order->setPayment($this->quotePaymentToOrderPayment->convert($quote->getPayment())); @@ -874,7 +876,7 @@ private function logExceptions(array $exceptionList) */ public function save() { - $this->getQuote()->collectTotals(); + $this->getQuote()->setTotalsCollectedFlag(false)->collectTotals(); $this->quoteRepository->save($this->getQuote()); return $this; } @@ -897,13 +899,23 @@ public function reset() */ public function validateMinimumAmount() { - return !($this->_scopeConfig->isSetFlag( + $minimumOrderActive = $this->_scopeConfig->isSetFlag( 'sales/minimum_order/active', \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ) && $this->_scopeConfig->isSetFlag( + ); + + $minimumOrderMultiFlag = $this->_scopeConfig->isSetFlag( 'sales/minimum_order/multi_address', \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ) && !$this->getQuote()->validateMinimumAmount()); + ); + + if ($minimumOrderMultiFlag) { + $result = !($minimumOrderActive && !$this->getQuote()->validateMinimumAmount()); + } else { + $result = !($minimumOrderActive && !$this->validateMinimumAmountForAddressItems()); + } + + return $result; } /** @@ -927,6 +939,8 @@ public function getMinimumAmountDescription() } /** + * Get minimum amount error. + * * @return string */ public function getMinimumAmountError() @@ -1071,7 +1085,7 @@ public function getCustomer() /** * Check if specified address ID belongs to customer. * - * @param $addressId + * @param mixed $addressId * @return bool */ protected function isAddressIdApplicable($addressId) @@ -1084,6 +1098,8 @@ protected function isAddressIdApplicable($addressId) } /** + * Prepare shipping assignment. + * * @param \Magento\Quote\Model\Quote $quote * @return \Magento\Quote\Model\Quote */ @@ -1104,6 +1120,8 @@ private function prepareShippingAssignment($quote) } /** + * Get shipping assignment processor. + * * @return \Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentProcessor */ private function getShippingAssignmentProcessor() @@ -1115,6 +1133,44 @@ private function getShippingAssignmentProcessor() return $this->shippingAssignmentProcessor; } + /** + * Validate minimum amount for "Checkout with Multiple Addresses" when + * "Validate Each Address Separately in Multi-address Checkout" is No. + * + * @return bool + */ + private function validateMinimumAmountForAddressItems() + { + $result = true; + $storeId = $this->getQuote()->getStoreId(); + + $minAmount = $this->_scopeConfig->getValue( + 'sales/minimum_order/amount', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $storeId + ); + $taxInclude = $this->_scopeConfig->getValue( + 'sales/minimum_order/tax_including', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $storeId + ); + + $this->getQuote()->collectTotals(); + $addresses = $this->getQuote()->getAllAddresses(); + + $baseTotal = 0; + foreach ($addresses as $address) { + $taxes = $taxInclude ? $address->getBaseTaxAmount() : 0; + $baseTotal += $address->getBaseSubtotalWithDiscount() + $taxes; + } + + if ($baseTotal < $minAmount) { + $result = false; + } + + return $result; + } + /** * Remove successfully placed items from quote. * @@ -1126,7 +1182,7 @@ private function removePlacedItemsFromQuote(array $shippingAddresses, array $pla { foreach ($shippingAddresses as $address) { foreach ($address->getAllItems() as $addressItem) { - if (in_array($addressItem->getId(), $placedAddressItems)) { + if (in_array($addressItem->getQuoteItemId(), $placedAddressItems)) { if ($addressItem->getProduct()->getIsVirtual()) { $addressItem->isDeleted(true); } else { @@ -1176,7 +1232,7 @@ private function searchQuoteAddressId(OrderInterface $order, array $addresses): $item = array_pop($items); foreach ($addresses as $address) { foreach ($address->getAllItems() as $addressItem) { - if ($addressItem->getId() == $item->getQuoteItemId()) { + if ($addressItem->getQuoteItemId() == $item->getQuoteItemId()) { return (int)$address->getId(); } } @@ -1186,10 +1242,11 @@ private function searchQuoteAddressId(OrderInterface $order, array $addresses): } /** + * Get quote address errors. + * * @param OrderInterface[] $orders * @param \Magento\Quote\Model\Quote\Address[] $addresses * @param \Exception[] $exceptionList - * * @return string[] * @throws NotFoundException */ diff --git a/app/code/Magento/Multishipping/Test/Mftf/Page/NewMultishippingAddressPage.xml b/app/code/Magento/Multishipping/Test/Mftf/Page/NewMultishippingAddressPage.xml new file mode 100644 index 000000000000..f25687a012ec --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/Page/NewMultishippingAddressPage.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="NewMultishippingAddressPage" url="/multishipping/checkout_address/newShipping" module="Multishipping" area="storefront"> + </page> +</pages> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Section/MultishippingSection.xml b/app/code/Magento/Multishipping/Test/Mftf/Section/MultishippingSection.xml new file mode 100644 index 000000000000..e7d57af1172c --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/Section/MultishippingSection.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:Test/etc/testSchema.xsd"> + <section name="MultishippingSection"> + <element name="checkoutWithMultipleAddresses" type="button" selector="//span[text()='Check Out with Multiple Addresses']"/> + </section> +</sections> diff --git a/app/code/Magento/Multishipping/Test/Unit/Model/Checkout/Type/MultishippingTest.php b/app/code/Magento/Multishipping/Test/Unit/Model/Checkout/Type/MultishippingTest.php index 94c11bef1d31..02bc96687377 100644 --- a/app/code/Magento/Multishipping/Test/Unit/Model/Checkout/Type/MultishippingTest.php +++ b/app/code/Magento/Multishipping/Test/Unit/Model/Checkout/Type/MultishippingTest.php @@ -167,13 +167,18 @@ class MultishippingTest extends \PHPUnit\Framework\TestCase */ private $sessionMock; + /** + * @var PHPUnit_Framework_MockObject_MockObject + */ + private $scopeConfigMock; + protected function setUp() { $this->checkoutSessionMock = $this->createSimpleMock(Session::class); $this->customerSessionMock = $this->createSimpleMock(CustomerSession::class); $this->orderFactoryMock = $this->createSimpleMock(OrderFactory::class); $eventManagerMock = $this->createSimpleMock(ManagerInterface::class); - $scopeConfigMock = $this->createSimpleMock(ScopeConfigInterface::class); + $this->scopeConfigMock = $this->createSimpleMock(ScopeConfigInterface::class); $this->sessionMock = $this->createSimpleMock(Generic::class); $addressFactoryMock = $this->createSimpleMock(AddressFactory::class); $this->toOrderMock = $this->createSimpleMock(ToOrder::class); @@ -224,7 +229,7 @@ protected function setUp() $this->orderFactoryMock, $this->addressRepositoryMock, $eventManagerMock, - $scopeConfigMock, + $this->scopeConfigMock, $this->sessionMock, $addressFactoryMock, $this->toOrderMock, @@ -277,6 +282,10 @@ public function testSetShippingItemsInformation() ->willReturn(null); $this->quoteMock->expects($this->atLeastOnce())->method('getAllItems')->willReturn([]); + $this->quoteMock->expects($this->once()) + ->method('__call') + ->with('setTotalsCollectedFlag', [false]) + ->willReturnSelf(); $this->filterBuilderMock->expects($this->atLeastOnce())->method('setField')->willReturnSelf(); $this->filterBuilderMock->expects($this->atLeastOnce())->method('setValue')->willReturnSelf(); @@ -416,6 +425,10 @@ public function testSetShippingMethods() $addressMock->expects($this->once())->method('getId')->willReturn($addressId); $this->quoteMock->expects($this->once())->method('getAllShippingAddresses')->willReturn([$addressMock]); $addressMock->expects($this->once())->method('setShippingMethod')->with($methodsArray[$addressId]); + $this->quoteMock->expects($this->once()) + ->method('__call') + ->with('setTotalsCollectedFlag', [false]) + ->willReturnSelf(); $this->mockShippingAssignment(); @@ -659,11 +672,13 @@ private function getOrderMock( 'getId', 'getCanSendNewEmailFlag', 'getItems', + 'setShippingMethod', ] )->getMock(); $orderMock->method('setQuote')->with($this->quoteMock); $orderMock->method('setBillingAddress')->with($orderAddressMock)->willReturnSelf(); $orderMock->method('setShippingAddress')->with($orderAddressMock)->willReturnSelf(); + $orderMock->expects($this->once())->method('setShippingMethod')->with('carrier')->willReturnSelf(); $orderMock->method('setPayment')->with($orderPaymentMock)->willReturnSelf(); $orderMock->method('addItem')->with($orderItemMock)->willReturnSelf(); $orderMock->method('getIncrementId')->willReturn('1'); @@ -807,4 +822,37 @@ private function createSimpleMock($className) ->disableOriginalConstructor() ->getMock(); } + + public function testValidateMinimumAmountMultiAddressTrue() + { + $this->scopeConfigMock->expects($this->exactly(2))->method('isSetFlag')->withConsecutive( + ['sales/minimum_order/active', \Magento\Store\Model\ScopeInterface::SCOPE_STORE], + ['sales/minimum_order/multi_address', \Magento\Store\Model\ScopeInterface::SCOPE_STORE] + )->willReturnOnConsecutiveCalls(true, true); + + $this->checkoutSessionMock->expects($this->atLeastOnce())->method('getQuote')->willReturn($this->quoteMock); + $this->quoteMock->expects($this->once())->method('validateMinimumAmount')->willReturn(false); + $this->assertFalse($this->model->validateMinimumAmount()); + } + + public function testValidateMinimumAmountMultiAddressFalse() + { + $addressMock = $this->createMock(\Magento\Quote\Model\Quote\Address::class); + $this->scopeConfigMock->expects($this->exactly(2))->method('isSetFlag')->withConsecutive( + ['sales/minimum_order/active', \Magento\Store\Model\ScopeInterface::SCOPE_STORE], + ['sales/minimum_order/multi_address', \Magento\Store\Model\ScopeInterface::SCOPE_STORE] + )->willReturnOnConsecutiveCalls(true, false); + + $this->scopeConfigMock->expects($this->exactly(2))->method('getValue')->withConsecutive( + ['sales/minimum_order/amount', \Magento\Store\Model\ScopeInterface::SCOPE_STORE], + ['sales/minimum_order/tax_including', \Magento\Store\Model\ScopeInterface::SCOPE_STORE] + )->willReturnOnConsecutiveCalls(100, false); + + $this->checkoutSessionMock->expects($this->atLeastOnce())->method('getQuote')->willReturn($this->quoteMock); + $this->quoteMock->expects($this->once())->method('getStoreId')->willReturn(1); + $this->quoteMock->expects($this->once())->method('getAllAddresses')->willReturn([$addressMock]); + $addressMock->expects($this->once())->method('getBaseSubtotalWithDiscount')->willReturn(101); + + $this->assertTrue($this->model->validateMinimumAmount()); + } } diff --git a/app/code/Magento/Multishipping/view/frontend/layout/multishipping_checkout_customer_address.xml b/app/code/Magento/Multishipping/view/frontend/layout/multishipping_checkout_customer_address.xml index c6bcdeb7b041..fee3cb790a52 100644 --- a/app/code/Magento/Multishipping/view/frontend/layout/multishipping_checkout_customer_address.xml +++ b/app/code/Magento/Multishipping/view/frontend/layout/multishipping_checkout_customer_address.xml @@ -12,6 +12,7 @@ <block class="Magento\Customer\Block\Address\Edit" name="customer_address_edit" template="Magento_Customer::address/edit.phtml" cacheable="false"> <arguments> <argument name="attribute_data" xsi:type="object">Magento\Customer\Block\DataProviders\AddressAttributeData</argument> + <argument name="post_code_config" xsi:type="object">Magento\Customer\Block\DataProviders\PostCodesPatternsAttributeData</argument> </arguments> </block> </referenceContainer> diff --git a/app/code/Magento/Multishipping/view/frontend/templates/checkout/address/select.phtml b/app/code/Magento/Multishipping/view/frontend/templates/checkout/address/select.phtml index 30e721828564..ab49788d8dc1 100644 --- a/app/code/Magento/Multishipping/view/frontend/templates/checkout/address/select.phtml +++ b/app/code/Magento/Multishipping/view/frontend/templates/checkout/address/select.phtml @@ -10,41 +10,49 @@ ?> <div class="multicheckout"> <div class="block block-billing"> - <?php foreach ($block->getAddress() as $_address): ?> + <?php foreach ($block->getAddress() as $address): ?> <div class="box box-billing-address"> <div class="box-content"> <address> - <?= $block->getAddressAsHtml($_address) ?> - <?php if ($block->isAddressDefaultBilling($_address)): ?> - <br /><strong><?= /* @escapeNotVerified */ __('Default Billing') ?></strong> + <?= $block->getAddressAsHtml($address) ?> + <?php if ($block->isAddressDefaultBilling($address)): ?> + <br /><strong><?= $block->escapeHtml(__('Default Billing')); ?></strong> <?php endif; ?> - <?php if ($block->isAddressDefaultShipping($_address)): ?> - <br /><strong><?= /* @escapeNotVerified */ __('Default Shipping') ?></strong> + <?php if ($block->isAddressDefaultShipping($address)): ?> + <br /><strong><?= $block->escapeHtml(__('Default Shipping')); ?></strong> <?php endif; ?> </address> </div> <div class="box-actions"> - <a href="<?= /* @escapeNotVerified */ $block->getEditAddressUrl($_address) ?>" class="action edit"><span><?= /* @escapeNotVerified */ __('Edit Address') ?></span></a> - <a href="<?= /* @escapeNotVerified */ $block->getSetAddressUrl($_address) ?>" class="action select"><span><?= /* @escapeNotVerified */ __('Select Address') ?></span></a> + <a href="<?= $block->escapeUrl($block->getEditAddressUrl($address)); ?>" class="action edit"> + <span><?= $block->escapeHtml(__('Edit Address')); ?></span> + </a> + <a href="<?= $block->escapeUrl($block->getSetAddressUrl($address)); ?>" class="action select"> + <span><?= $block->escapeHtml(__('Select Address')); ?></span> + </a> </div> </div> <?php endforeach; ?> </div> <div class="actions-toolbar"> <div class="primary"> - <button type="button" class="action add primary" role="add-address" title="<?= /* @escapeNotVerified */ __('Add New Address') ?>"><span><?= /* @escapeNotVerified */ __('Add New Address') ?></span></button> + <button type="button" class="action add primary" role="add-address" title="<?= $block->escapeHtml(__('Add New Address')); ?>"> + <span><?= $block->escapeHtml(__('Add New Address')); ?></span> + </button> </div> <div class="secondary"> - <a href="<?= /* @escapeNotVerified */ $block->getBackUrl() ?>" class="action back"><span><?= /* @escapeNotVerified */ __('Back to Billing Information') ?></span></a> + <a href="<?= $block->escapeUrl($block->getBackUrl()); ?>" class="action back"> + <span><?= $block->escapeHtml(__('Back to Billing Information')); ?></span> + </a> </div> </div> </div> <script type="text/x-magento-init"> { - ".actions": { + ".action": { "address": { "addAddress": "button[role='add-address']", - "addAddressLocation": "<?= /* @escapeNotVerified */ $block->getAddNewUrl() ?>" + "addAddressLocation": "<?= $block->escapeUrl($block->getAddNewUrl()); ?>" } } } diff --git a/app/code/Magento/Multishipping/view/frontend/templates/checkout/addresses.phtml b/app/code/Magento/Multishipping/view/frontend/templates/checkout/addresses.phtml index 0f96f7cdb708..a29013cc7172 100644 --- a/app/code/Magento/Multishipping/view/frontend/templates/checkout/addresses.phtml +++ b/app/code/Magento/Multishipping/view/frontend/templates/checkout/addresses.phtml @@ -14,17 +14,6 @@ * @var $block \Magento\Multishipping\Block\Checkout\Addresses */ ?> -<form id="checkout_multishipping_form" - data-mage-init='{ - "multiShipping":{}, - "validation":{}, - "cartUpdate": { - "validationURL": "/multishipping/checkout/checkItems", - "eventName": "updateMulticartItemQty" - }}' - action="<?= $block->escapeUrl($block->getPostActionUrl()) ?>" - method="post" - class="multicheckout address form"> <form id="checkout_multishipping_form" data-mage-init='{ "multiShipping":{}, diff --git a/app/code/Magento/Multishipping/view/frontend/templates/checkout/billing.phtml b/app/code/Magento/Multishipping/view/frontend/templates/checkout/billing.phtml index da553b7823da..4354cfb7c1c3 100644 --- a/app/code/Magento/Multishipping/view/frontend/templates/checkout/billing.phtml +++ b/app/code/Magento/Multishipping/view/frontend/templates/checkout/billing.phtml @@ -55,6 +55,10 @@ <div class="box-content"> <address> <?= /* @noEscape */ $block->getCheckoutData()->getAddressHtml($block->getAddress()); ?> + <input type="hidden" + id="multishipping_billing_country_id" + value="<?= /* @noEscape */ $block->getAddress()->getCountryId(); ?>" + name="multishipping_billing_country_id"/> </address> </div> </div> @@ -80,35 +84,44 @@ $block->setMethodFormTemplate($code, $methodsForms[$code]); } ?> - <dt class="item-title"> - <?php if ($methodsCount > 1) : ?> - <input type="radio" - id="p_method_<?= $block->escapeHtml($code); ?>" - value="<?= $block->escapeHtml($code); ?>" - name="payment[method]" - title="<?= $block->escapeHtml($_method->getTitle()) ?>" - <?php if ($checked) : ?> - checked="checked" + <div data-bind="scope: 'payment_method_<?= $block->escapeHtml($code);?>'"> + <dt class="item-title"> + <?php if ($methodsCount > 1) : ?> + <input type="radio" + id="p_method_<?= $block->escapeHtml($code); ?>" + value="<?= $block->escapeHtml($code); ?>" + name="payment[method]" + title="<?= $block->escapeHtml($_method->getTitle()) ?>" + data-bind=" + value: getCode(), + checked: isChecked, + click: selectPaymentMethod, + visible: isRadioButtonVisible()" + <?php if ($checked) : ?> + checked="checked" + <?php endif; ?> + class="radio"/> + <?php else : ?> + <input type="radio" + id="p_method_<?= $block->escapeHtml($code); ?>" + value="<?= $block->escapeHtml($code); ?>" + name="payment[method]" + data-bind=" + value: getCode(), + afterRender: selectPaymentMethod" + checked="checked" + class="radio solo method" /> <?php endif; ?> - class="radio"/> - <?php else : ?> - <input type="radio" - id="p_method_<?= $block->escapeHtml($code); ?>" - value="<?= $block->escapeHtml($code); ?>" - name="payment[method]" - checked="checked" - class="radio solo method" /> - <?php endif; ?> - <label for="p_method_<?= $block->escapeHtml($code); ?>"> - <?= $block->escapeHtml($_method->getTitle()) ?> - </label> - </dt> - <?php if ($html = $block->getChildHtml('payment.method.' . $code)) : ?> - <dd class="item-content <?= $checked ? '' : 'no-display'; ?>" - data-bind="scope: 'payment_method_<?= $block->escapeHtml($code);?>'"> - <?= /* @noEscape */ $html; ?> - </dd> - <?php endif; ?> + <label for="p_method_<?= $block->escapeHtml($code); ?>"> + <?= $block->escapeHtml($_method->getTitle()) ?> + </label> + </dt> + <?php if ($html = $block->getChildHtml('payment.method.' . $code)) : ?> + <dd class="item-content <?= $checked ? '' : 'no-display'; ?>"> + <?= /* @noEscape */ $html; ?> + </dd> + <?php endif; ?> + </div> <?php endforeach; ?> </dl> <?= $block->getChildHtml('payment_methods_after') ?> diff --git a/app/code/Magento/Multishipping/view/frontend/web/js/overview.js b/app/code/Magento/Multishipping/view/frontend/web/js/overview.js index 9b867cd7217b..3a6d73e30497 100644 --- a/app/code/Magento/Multishipping/view/frontend/web/js/overview.js +++ b/app/code/Magento/Multishipping/view/frontend/web/js/overview.js @@ -15,7 +15,7 @@ define([ opacity: 0.5, // CSS opacity for the 'Place Order' button when it's clicked and then disabled. pleaseWaitLoader: 'span.please-wait', // 'Submitting order information...' Ajax loader. placeOrderSubmit: 'button[type="submit"]', // The 'Place Order' button. - agreements: '#checkout-agreements' // Container for all of the checkout agreements and terms/conditions + agreements: '.checkout-agreements' // Container for all of the checkout agreements and terms/conditions }, /** diff --git a/app/code/Magento/MysqlMq/Model/Driver/Queue.php b/app/code/Magento/MysqlMq/Model/Driver/Queue.php index b8dab6fac7b2..cbc2e951782f 100644 --- a/app/code/Magento/MysqlMq/Model/Driver/Queue.php +++ b/app/code/Magento/MysqlMq/Model/Driver/Queue.php @@ -73,7 +73,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function dequeue() { @@ -92,7 +92,7 @@ public function dequeue() } /** - * {@inheritdoc} + * @inheritdoc */ public function acknowledge(EnvelopeInterface $envelope) { @@ -103,25 +103,26 @@ public function acknowledge(EnvelopeInterface $envelope) } /** - * {@inheritdoc} + * @inheritdoc */ public function subscribe($callback) { while (true) { while ($envelope = $this->dequeue()) { try { + // phpcs:ignore Magento2.Functions.DiscouragedFunction call_user_func($callback, $envelope); - $this->acknowledge($envelope); } catch (\Exception $e) { $this->reject($envelope); } } + // phpcs:ignore Magento2.Functions.DiscouragedFunction sleep($this->interval); } } /** - * {@inheritdoc} + * @inheritdoc */ public function reject(EnvelopeInterface $envelope, $requeue = true, $rejectionMessage = null) { @@ -139,7 +140,7 @@ public function reject(EnvelopeInterface $envelope, $requeue = true, $rejectionM } /** - * {@inheritDoc} + * @inheritDoc */ public function push(EnvelopeInterface $envelope) { diff --git a/app/code/Magento/MysqlMq/Model/QueueManagement.php b/app/code/Magento/MysqlMq/Model/QueueManagement.php index 0840add2a5c4..74d2154a36b0 100644 --- a/app/code/Magento/MysqlMq/Model/QueueManagement.php +++ b/app/code/Magento/MysqlMq/Model/QueueManagement.php @@ -107,9 +107,10 @@ public function addMessagesToQueues($topic, $messages, $queueNames) } /** - * Mark messages to be deleted if sufficient amount of time passed since last update * Delete marked messages * + * Mark messages to be deleted if sufficient amount of time passed since last update + * * @return void */ public function markMessagesForDelete() @@ -223,7 +224,7 @@ private function getErrorMessageLifetime() } /** - * In progress message delay befor retry + * In progress message delay before retry * * Indicates how long message will stay in IN PROGRESS status before attempted to retry * diff --git a/app/code/Magento/MysqlMq/etc/db_schema.xml b/app/code/Magento/MysqlMq/etc/db_schema.xml index 02757afa53fc..3850a6749a3d 100644 --- a/app/code/Magento/MysqlMq/etc/db_schema.xml +++ b/app/code/Magento/MysqlMq/etc/db_schema.xml @@ -11,10 +11,10 @@ <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Queue ID"/> <column xsi:type="varchar" name="name" nullable="true" length="255" comment="Queue name"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> </constraint> - <constraint xsi:type="unique" name="QUEUE_NAME"> + <constraint xsi:type="unique" referenceId="QUEUE_NAME"> <column name="name"/> </constraint> </table> @@ -23,7 +23,7 @@ comment="Message ID"/> <column xsi:type="varchar" name="topic_name" nullable="true" length="255" comment="Message topic"/> <column xsi:type="longtext" name="body" nullable="true" comment="Message body"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> </constraint> </table> @@ -41,19 +41,19 @@ comment="Message status in particular queue"/> <column xsi:type="smallint" name="number_of_trials" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Number of trials to processed failed message processing"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> </constraint> - <constraint xsi:type="foreign" name="QUEUE_MESSAGE_STATUS_MESSAGE_ID_QUEUE_MESSAGE_ID" + <constraint xsi:type="foreign" referenceId="QUEUE_MESSAGE_STATUS_MESSAGE_ID_QUEUE_MESSAGE_ID" table="queue_message_status" column="message_id" referenceTable="queue_message" referenceColumn="id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="QUEUE_MESSAGE_STATUS_QUEUE_ID_QUEUE_ID" table="queue_message_status" + <constraint xsi:type="foreign" referenceId="QUEUE_MESSAGE_STATUS_QUEUE_ID_QUEUE_ID" table="queue_message_status" column="queue_id" referenceTable="queue" referenceColumn="id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="QUEUE_MESSAGE_STATUS_QUEUE_ID_MESSAGE_ID"> + <constraint xsi:type="unique" referenceId="QUEUE_MESSAGE_STATUS_QUEUE_ID_MESSAGE_ID"> <column name="queue_id"/> <column name="message_id"/> </constraint> - <index name="QUEUE_MESSAGE_STATUS_STATUS_UPDATED_AT" indexType="btree"> + <index referenceId="QUEUE_MESSAGE_STATUS_STATUS_UPDATED_AT" indexType="btree"> <column name="status"/> <column name="updated_at"/> </index> diff --git a/app/code/Magento/NewRelicReporting/Model/Config.php b/app/code/Magento/NewRelicReporting/Model/Config.php index 32e1078c01c9..6f3ff1df5744 100644 --- a/app/code/Magento/NewRelicReporting/Model/Config.php +++ b/app/code/Magento/NewRelicReporting/Model/Config.php @@ -5,6 +5,9 @@ */ namespace Magento\NewRelicReporting\Model; +/** + * NewRelic configuration model + */ class Config { /**#@+ @@ -88,7 +91,7 @@ public function __construct( */ public function isNewRelicEnabled() { - return (bool)$this->scopeConfig->getValue('newrelicreporting/general/enable'); + return $this->scopeConfig->isSetFlag('newrelicreporting/general/enable'); } /** @@ -161,6 +164,16 @@ public function getNewRelicAppName() return (string)$this->scopeConfig->getValue('newrelicreporting/general/app_name'); } + /** + * Returns configured separate apps value + * + * @return bool + */ + public function isSeparateApps() + { + return (bool)$this->scopeConfig->getValue('newrelicreporting/general/separate_apps'); + } + /** * Returns config setting for overall cron to be enabled * @@ -168,7 +181,7 @@ public function getNewRelicAppName() */ public function isCronEnabled() { - return (bool)$this->scopeConfig->getValue('newrelicreporting/cron/enable_cron'); + return $this->scopeConfig->isSetFlag('newrelicreporting/cron/enable_cron'); } /** diff --git a/app/code/Magento/NewRelicReporting/Model/Cron/ReportModulesInfo.php b/app/code/Magento/NewRelicReporting/Model/Cron/ReportModulesInfo.php index 9cdc90bc46b2..78c485c5bb6f 100644 --- a/app/code/Magento/NewRelicReporting/Model/Cron/ReportModulesInfo.php +++ b/app/code/Magento/NewRelicReporting/Model/Cron/ReportModulesInfo.php @@ -64,6 +64,7 @@ public function report() $moduleData = $this->collect->getModuleData(); if (count($moduleData['changes']) > 0) { foreach ($moduleData['changes'] as $change) { + $modelData = []; switch ($change['type']) { case Config::ENABLED: $modelData = [ diff --git a/app/code/Magento/NewRelicReporting/Model/Module/Collect.php b/app/code/Magento/NewRelicReporting/Model/Module/Collect.php index 7e381762f5d2..fe5389e258aa 100644 --- a/app/code/Magento/NewRelicReporting/Model/Module/Collect.php +++ b/app/code/Magento/NewRelicReporting/Model/Module/Collect.php @@ -11,6 +11,9 @@ use Magento\NewRelicReporting\Model\Config; use Magento\NewRelicReporting\Model\Module; +/** + * Class for collecting data for the report + */ class Collect { /** @@ -92,7 +95,6 @@ protected function getAllModules() * @param string $active * @param string $setupVersion * @param string $state - * * @return array */ protected function getNewModuleChanges($moduleName, $active, $setupVersion, $state) @@ -277,9 +279,7 @@ public function getModuleData($refresh = true) $changes = array_diff($module, $changeTest); $changesCleanArray = $this->getCleanChangesArray($changes); - if (count($changesCleanArray) > 0 || - ($this->moduleManager->isOutputEnabled($changeTest['name']) && - $module['setup_version'] != null)) { + if (!empty($changesCleanArray)) { $data = [ 'entity_id' => $changeTest['entity_id'], 'name' => $changeTest['name'], diff --git a/app/code/Magento/NewRelicReporting/Model/NewRelicWrapper.php b/app/code/Magento/NewRelicReporting/Model/NewRelicWrapper.php index 845ed0429d2c..bce42b4e9007 100644 --- a/app/code/Magento/NewRelicReporting/Model/NewRelicWrapper.php +++ b/app/code/Magento/NewRelicReporting/Model/NewRelicWrapper.php @@ -21,7 +21,7 @@ class NewRelicWrapper */ public function addCustomParameter($param, $value) { - if (extension_loaded('newrelic')) { + if ($this->isExtensionInstalled()) { newrelic_add_custom_parameter($param, $value); return true; } @@ -31,16 +31,42 @@ public function addCustomParameter($param, $value) /** * Wrapper for 'newrelic_notice_error' function * - * @param Exception $exception + * @param \Exception $exception * @return void */ public function reportError($exception) { - if (extension_loaded('newrelic')) { + if ($this->isExtensionInstalled()) { newrelic_notice_error($exception->getMessage(), $exception); } } + /** + * Wrapper for 'newrelic_set_appname' + * + * @param string $appName + * @return void + */ + public function setAppName(string $appName) + { + if ($this->isExtensionInstalled()) { + newrelic_set_appname($appName); + } + } + + /** + * Wrapper for 'newrelic_name_transaction' + * + * @param string $transactionName + * @return void + */ + public function setTransactionName(string $transactionName): void + { + if ($this->isExtensionInstalled()) { + newrelic_name_transaction($transactionName); + } + } + /** * Checks whether newrelic-php5 agent is installed * diff --git a/app/code/Magento/NewRelicReporting/Plugin/CommandPlugin.php b/app/code/Magento/NewRelicReporting/Plugin/CommandPlugin.php new file mode 100644 index 000000000000..04ad3d0504d3 --- /dev/null +++ b/app/code/Magento/NewRelicReporting/Plugin/CommandPlugin.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\NewRelicReporting\Plugin; + +use Magento\NewRelicReporting\Model\Config; +use Magento\NewRelicReporting\Model\NewRelicWrapper; +use Symfony\Component\Console\Command\Command; + +/** + * Describe NewRelic commands plugin. + */ +class CommandPlugin +{ + /** + * @var Config + */ + private $config; + + /** + * @var NewRelicWrapper + */ + private $newRelicWrapper; + + /** + * @param Config $config + * @param NewRelicWrapper $newRelicWrapper + */ + public function __construct( + Config $config, + NewRelicWrapper $newRelicWrapper + ) { + $this->config = $config; + $this->newRelicWrapper = $newRelicWrapper; + } + + /** + * Set NewRelic Transaction name before running command. + * + * @param Command $command + * @param array $args + * @return array + */ + public function beforeRun(Command $command, ...$args) + { + $this->newRelicWrapper->setTransactionName( + sprintf('CLI %s', $command->getName()) + ); + + return $args; + } +} diff --git a/app/code/Magento/NewRelicReporting/Plugin/StatePlugin.php b/app/code/Magento/NewRelicReporting/Plugin/StatePlugin.php new file mode 100644 index 000000000000..8be29fa6db9d --- /dev/null +++ b/app/code/Magento/NewRelicReporting/Plugin/StatePlugin.php @@ -0,0 +1,102 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\NewRelicReporting\Plugin; + +use Magento\Framework\App\State; +use Magento\Framework\Exception\LocalizedException; +use Magento\NewRelicReporting\Model\Config; +use Magento\NewRelicReporting\Model\NewRelicWrapper; +use Psr\Log\LoggerInterface; + +/** + * Handles setting which, when enabled, reports frontend and adminhtml as separate apps to New Relic. + */ +class StatePlugin +{ + /** + * @var Config + */ + private $config; + + /** + * @var NewRelicWrapper + */ + private $newRelicWrapper; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param Config $config + * @param NewRelicWrapper $newRelicWrapper + * @param LoggerInterface $logger + */ + public function __construct( + Config $config, + NewRelicWrapper $newRelicWrapper, + LoggerInterface $logger + ) { + $this->config = $config; + $this->newRelicWrapper = $newRelicWrapper; + $this->logger = $logger; + } + + /** + * Set separate appname + * + * @param State $subject + * @param mixed $result + * @return mixed + */ + public function afterSetAreaCode(State $subject, $result) + { + if (!$this->shouldSetAppName()) { + return $result; + } + + try { + $this->newRelicWrapper->setAppName($this->appName($subject)); + } catch (LocalizedException $e) { + $this->logger->critical($e); + return $result; + } + + return $result; + } + + /** + * Format appName. + * + * @param State $state + * @return string + * @throws LocalizedException + */ + private function appName(State $state): string + { + $code = $state->getAreaCode(); + $current = $this->config->getNewRelicAppName(); + + return $current . ';' . $current . '_' . $code; + } + + /** + * Check if app name should be set. + * + * @return bool + */ + private function shouldSetAppName(): bool + { + return ( + $this->config->isSeparateApps() && + $this->config->getNewRelicAppName() && + $this->config->isNewRelicEnabled() + ); + } +} diff --git a/app/code/Magento/NewRelicReporting/Test/Unit/Model/Module/CollectTest.php b/app/code/Magento/NewRelicReporting/Test/Unit/Model/Module/CollectTest.php index 8d8e6255ab8d..4286406d6e9a 100644 --- a/app/code/Magento/NewRelicReporting/Test/Unit/Model/Module/CollectTest.php +++ b/app/code/Magento/NewRelicReporting/Test/Unit/Model/Module/CollectTest.php @@ -162,10 +162,6 @@ public function testGetModuleDataWithoutRefresh() ->method('getNames') ->willReturn($enabledModulesMockArray); - $this->moduleManagerMock->expects($this->any())->method('isOutputEnabled')->will( - $this->returnValue(false) - ); - $this->assertInternalType( 'array', $this->model->getModuleData() @@ -256,10 +252,6 @@ public function testGetModuleDataRefresh($data) ->method('getNames') ->willReturn($enabledModulesMockArray); - $this->moduleManagerMock->expects($this->any())->method('isOutputEnabled')->will( - $this->returnValue(true) - ); - $this->assertInternalType( 'array', $this->model->getModuleData() @@ -350,10 +342,6 @@ public function testGetModuleDataRefreshOrStatement($data) ->method('getNames') ->willReturn($enabledModulesMockArray); - $this->moduleManagerMock->expects($this->any())->method('isOutputEnabled')->will( - $this->returnValue(true) - ); - $this->assertInternalType( 'array', $this->model->getModuleData() diff --git a/app/code/Magento/NewRelicReporting/etc/adminhtml/system.xml b/app/code/Magento/NewRelicReporting/etc/adminhtml/system.xml index 582b7c752386..98f9c55adbdf 100644 --- a/app/code/Magento/NewRelicReporting/etc/adminhtml/system.xml +++ b/app/code/Magento/NewRelicReporting/etc/adminhtml/system.xml @@ -46,6 +46,11 @@ <label>New Relic Application Name</label> <comment>This is located by navigating to Settings from the New Relic APM website</comment> </field> + <field id="separate_apps" translate="label comment" type="select" sortOrder="9" showInDefault="1" showInWebsite="1" showInStore="1"> + <label>Send Adminhtml and Frontend as Separate Apps</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <comment>In addition to the main app (which includes all PHP execution), separate apps for adminhtml and frontend will be created. Requires New Relic Application Name to be set.</comment> + </field> </group> <group id="cron" translate="label" type="text" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Cron</label> diff --git a/app/code/Magento/NewRelicReporting/etc/db_schema.xml b/app/code/Magento/NewRelicReporting/etc/db_schema.xml index 6f9ce29436f6..c6e61b88f4b1 100644 --- a/app/code/Magento/NewRelicReporting/etc/db_schema.xml +++ b/app/code/Magento/NewRelicReporting/etc/db_schema.xml @@ -10,13 +10,13 @@ <table name="reporting_counts" resource="default" engine="innodb" comment="Reporting for all count related events generated via the cron job"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="varchar" name="type" nullable="true" length="255" comment="Item Reported"/> <column xsi:type="int" name="count" padding="10" unsigned="true" nullable="true" identity="false" comment="Count Value"/> <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" comment="Updated At"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> </table> @@ -29,44 +29,44 @@ <column xsi:type="varchar" name="state" nullable="true" length="255" comment="Module State"/> <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" comment="Updated At"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> </table> <table name="reporting_orders" resource="default" engine="innodb" comment="Reporting for all orders"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="true" identity="false" - comment="Customer Id"/> - <column xsi:type="decimal" name="total" scale="0" precision="10" unsigned="true" nullable="true"/> - <column xsi:type="decimal" name="total_base" scale="0" precision="10" unsigned="true" nullable="true"/> + comment="Customer ID"/> + <column xsi:type="decimal" name="total" scale="4" precision="20" unsigned="true" nullable="true"/> + <column xsi:type="decimal" name="total_base" scale="4" precision="20" unsigned="true" nullable="true"/> <column xsi:type="int" name="item_count" padding="10" unsigned="true" nullable="false" identity="false" comment="Line Item Count"/> <column xsi:type="timestamp" name="updated_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Updated At"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> </table> <table name="reporting_users" resource="default" engine="innodb" comment="Reporting for user actions"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="varchar" name="type" nullable="true" length="255" comment="User Type"/> <column xsi:type="varchar" name="action" nullable="true" length="255" comment="Action Performed"/> <column xsi:type="timestamp" name="updated_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Updated At"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> </table> <table name="reporting_system_updates" resource="default" engine="innodb" comment="Reporting for system updates"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="varchar" name="type" nullable="true" length="255" comment="Update Type"/> <column xsi:type="varchar" name="action" nullable="true" length="255" comment="Action Performed"/> <column xsi:type="timestamp" name="updated_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Updated At"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> </table> diff --git a/app/code/Magento/NewRelicReporting/etc/di.xml b/app/code/Magento/NewRelicReporting/etc/di.xml index 2dccc45c1129..15516f6df89b 100644 --- a/app/code/Magento/NewRelicReporting/etc/di.xml +++ b/app/code/Magento/NewRelicReporting/etc/di.xml @@ -30,6 +30,9 @@ <type name="Magento\Framework\App\Http"> <plugin name="framework-http-newrelic" type="Magento\NewRelicReporting\Plugin\HttpPlugin"/> </type> + <type name="Magento\Framework\App\State"> + <plugin name="framework-state-newrelic" type="Magento\NewRelicReporting\Plugin\StatePlugin"/> + </type> <type name="Magento\Framework\Console\CommandListInterface"> <arguments> <argument name="commands" xsi:type="array"> @@ -37,4 +40,7 @@ </argument> </arguments> </type> + <type name="Symfony\Component\Console\Command\Command"> + <plugin name="newrelic-describe-commands" type="Magento\NewRelicReporting\Plugin\CommandPlugin"/> + </type> </config> diff --git a/app/code/Magento/NewRelicReporting/i18n/en_US.csv b/app/code/Magento/NewRelicReporting/i18n/en_US.csv index 433b1b22fcdd..5ea64d3d4343 100644 --- a/app/code/Magento/NewRelicReporting/i18n/en_US.csv +++ b/app/code/Magento/NewRelicReporting/i18n/en_US.csv @@ -21,3 +21,5 @@ General,General "This is located by navigating to Settings from the New Relic APM website","This is located by navigating to Settings from the New Relic APM website" Cron,Cron "Enable Cron","Enable Cron" +"Send Adminhtml and Frontend as Separate Apps","Send Adminhtml and Frontend as Separate Apps" +"In addition to the main app (which includes all PHP execution), separate apps for adminhtml and frontend will be created. Requires New Relic Application Name to be set.","In addition to the main app (which includes all PHP execution), separate apps for adminhtml and frontend will be created. Requires New Relic Application Name to be set." diff --git a/app/code/Magento/Newsletter/Controller/Adminhtml/Problem/Index.php b/app/code/Magento/Newsletter/Controller/Adminhtml/Problem/Index.php index 7ea3b2cb9095..453a42def61f 100644 --- a/app/code/Magento/Newsletter/Controller/Adminhtml/Problem/Index.php +++ b/app/code/Magento/Newsletter/Controller/Adminhtml/Problem/Index.php @@ -6,7 +6,9 @@ */ namespace Magento\Newsletter\Controller\Adminhtml\Problem; -class Index extends \Magento\Newsletter\Controller\Adminhtml\Problem +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\Newsletter\Controller\Adminhtml\Problem implements HttpGetActionInterface { /** * Newsletter problems report page diff --git a/app/code/Magento/Newsletter/Controller/Adminhtml/Queue/Edit.php b/app/code/Magento/Newsletter/Controller/Adminhtml/Queue/Edit.php index b012979a11ac..f99c22a47439 100644 --- a/app/code/Magento/Newsletter/Controller/Adminhtml/Queue/Edit.php +++ b/app/code/Magento/Newsletter/Controller/Adminhtml/Queue/Edit.php @@ -6,7 +6,9 @@ */ namespace Magento\Newsletter\Controller\Adminhtml\Queue; -class Edit extends \Magento\Newsletter\Controller\Adminhtml\Queue +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Edit extends \Magento\Newsletter\Controller\Adminhtml\Queue implements HttpGetActionInterface { /** * Core registry diff --git a/app/code/Magento/Newsletter/Controller/Adminhtml/Queue/Grid.php b/app/code/Magento/Newsletter/Controller/Adminhtml/Queue/Grid.php index c992fe6c3770..4f734c09e475 100644 --- a/app/code/Magento/Newsletter/Controller/Adminhtml/Queue/Grid.php +++ b/app/code/Magento/Newsletter/Controller/Adminhtml/Queue/Grid.php @@ -6,7 +6,11 @@ */ namespace Magento\Newsletter\Controller\Adminhtml\Queue; -class Grid extends \Magento\Newsletter\Controller\Adminhtml\Queue +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; +use Magento\Newsletter\Controller\Adminhtml\Queue as QueueAction; + +class Grid extends QueueAction implements HttpPostActionInterface, HttpGetActionInterface { /** * Queue list Ajax action diff --git a/app/code/Magento/Newsletter/Controller/Adminhtml/Queue/Index.php b/app/code/Magento/Newsletter/Controller/Adminhtml/Queue/Index.php index 22c7adc5255c..2d07ffabe383 100644 --- a/app/code/Magento/Newsletter/Controller/Adminhtml/Queue/Index.php +++ b/app/code/Magento/Newsletter/Controller/Adminhtml/Queue/Index.php @@ -6,7 +6,14 @@ */ namespace Magento\Newsletter\Controller\Adminhtml\Queue; -class Index extends \Magento\Newsletter\Controller\Adminhtml\Queue +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Newsletter\Controller\Adminhtml\Queue as QueueAction; + +/** + * Show newsletter queue. Needs to be accessible by POST because of filtering. + */ +class Index extends QueueAction implements HttpGetActionInterface, HttpPostActionInterface { /** * Queue list action diff --git a/app/code/Magento/Newsletter/Controller/Adminhtml/Queue/Save.php b/app/code/Magento/Newsletter/Controller/Adminhtml/Queue/Save.php index d3b091976e92..2dbe10bf1bdc 100644 --- a/app/code/Magento/Newsletter/Controller/Adminhtml/Queue/Save.php +++ b/app/code/Magento/Newsletter/Controller/Adminhtml/Queue/Save.php @@ -7,7 +7,9 @@ namespace Magento\Newsletter\Controller\Adminhtml\Queue; -class Save extends \Magento\Newsletter\Controller\Adminhtml\Queue +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; + +class Save extends \Magento\Newsletter\Controller\Adminhtml\Queue implements HttpPostActionInterface { /** * Save Newsletter queue diff --git a/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/Index.php b/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/Index.php index a730b8a21a15..09cf1ae7959f 100644 --- a/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/Index.php +++ b/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/Index.php @@ -6,7 +6,11 @@ */ namespace Magento\Newsletter\Controller\Adminhtml\Subscriber; -class Index extends \Magento\Newsletter\Controller\Adminhtml\Subscriber +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Newsletter\Controller\Adminhtml\Subscriber as SubscriberAction; + +class Index extends SubscriberAction implements HttpGetActionInterface, HttpPostActionInterface { /** * Newsletter subscribers page diff --git a/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Drop.php b/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Drop.php index 52d46065ad05..2d167d4ccf5f 100644 --- a/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Drop.php +++ b/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Drop.php @@ -6,7 +6,9 @@ */ namespace Magento\Newsletter\Controller\Adminhtml\Template; -class Drop extends \Magento\Newsletter\Controller\Adminhtml\Template +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; + +class Drop extends \Magento\Newsletter\Controller\Adminhtml\Template implements HttpPostActionInterface { /** * Drop Newsletter Template diff --git a/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Edit.php b/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Edit.php index a1126be35af3..e60b865003f4 100644 --- a/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Edit.php +++ b/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Edit.php @@ -6,7 +6,9 @@ */ namespace Magento\Newsletter\Controller\Adminhtml\Template; -class Edit extends \Magento\Newsletter\Controller\Adminhtml\Template +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Edit extends \Magento\Newsletter\Controller\Adminhtml\Template implements HttpGetActionInterface { /** * Core registry diff --git a/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Index.php b/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Index.php index 5fcce53c6abb..12cdb7e262db 100644 --- a/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Index.php +++ b/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Index.php @@ -6,7 +6,9 @@ */ namespace Magento\Newsletter\Controller\Adminhtml\Template; -class Index extends \Magento\Newsletter\Controller\Adminhtml\Template +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\Newsletter\Controller\Adminhtml\Template implements HttpGetActionInterface { /** * View Templates list diff --git a/app/code/Magento/Newsletter/Controller/Adminhtml/Template/NewAction.php b/app/code/Magento/Newsletter/Controller/Adminhtml/Template/NewAction.php index 054e9025a4ff..c739d6ba26b1 100644 --- a/app/code/Magento/Newsletter/Controller/Adminhtml/Template/NewAction.php +++ b/app/code/Magento/Newsletter/Controller/Adminhtml/Template/NewAction.php @@ -6,7 +6,9 @@ */ namespace Magento\Newsletter\Controller\Adminhtml\Template; -class NewAction extends \Magento\Newsletter\Controller\Adminhtml\Template +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class NewAction extends \Magento\Newsletter\Controller\Adminhtml\Template implements HttpGetActionInterface { /** * Create new Newsletter Template diff --git a/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Preview.php b/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Preview.php index fbce319a75fc..9fd9f4335b5c 100644 --- a/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Preview.php +++ b/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Preview.php @@ -6,7 +6,12 @@ */ namespace Magento\Newsletter\Controller\Adminhtml\Template; -class Preview extends \Magento\Newsletter\Controller\Adminhtml\Template +use Magento\Framework\App\Action\HttpGetActionInterface; + +/** + * View a rendered template. + */ +class Preview extends \Magento\Newsletter\Controller\Adminhtml\Template implements HttpGetActionInterface { /** * Preview Newsletter template diff --git a/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Save.php b/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Save.php index c9db0d62e2e2..8fc729ea3407 100644 --- a/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Save.php +++ b/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Save.php @@ -6,10 +6,11 @@ */ namespace Magento\Newsletter\Controller\Adminhtml\Template; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\App\TemplateTypesInterface; use Magento\Framework\Exception\LocalizedException; -class Save extends \Magento\Newsletter\Controller\Adminhtml\Template +class Save extends \Magento\Newsletter\Controller\Adminhtml\Template implements HttpPostActionInterface { /** * Save Newsletter Template diff --git a/app/code/Magento/Newsletter/Controller/Manage/Save.php b/app/code/Magento/Newsletter/Controller/Manage/Save.php index 419cbac10ffd..698c2d19aae6 100644 --- a/app/code/Magento/Newsletter/Controller/Manage/Save.php +++ b/app/code/Magento/Newsletter/Controller/Manage/Save.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -8,9 +7,15 @@ namespace Magento\Newsletter\Controller\Manage; use Magento\Customer\Api\CustomerRepositoryInterface as CustomerRepository; +use Magento\Customer\Model\Customer; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Newsletter\Model\Subscriber; -class Save extends \Magento\Newsletter\Controller\Manage +/** + * Customers newsletter subscription save controller + */ +class Save extends \Magento\Newsletter\Controller\Manage implements HttpPostActionInterface, HttpGetActionInterface { /** * @var \Magento\Framework\Data\Form\FormKey\Validator @@ -81,6 +86,8 @@ public function execute() $isSubscribedParam = (boolean)$this->getRequest() ->getParam('is_subscribed', false); if ($isSubscribedParam !== $isSubscribedState) { + // No need to validate customer and customer address while saving subscription preferences + $this->setIgnoreValidationFlag($customer); $this->customerRepository->save($customer); if ($isSubscribedParam) { $subscribeModel = $this->subscriberFactory->create() @@ -105,4 +112,15 @@ public function execute() } $this->_redirect('customer/account/'); } + + /** + * Set ignore_validation_flag to skip unnecessary address and customer validation + * + * @param Customer $customer + * @return void + */ + private function setIgnoreValidationFlag($customer) + { + $customer->setData('ignore_validation_flag', true); + } } diff --git a/app/code/Magento/Newsletter/Controller/Subscriber/Confirm.php b/app/code/Magento/Newsletter/Controller/Subscriber/Confirm.php index 4e338c2d1df3..c27717f4c779 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 fd2a61702e90..7557f1610b4f 100644 --- a/app/code/Magento/Newsletter/Controller/Subscriber/NewAction.php +++ b/app/code/Magento/Newsletter/Controller/Subscriber/NewAction.php @@ -4,12 +4,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Newsletter\Controller\Subscriber; use Magento\Customer\Api\AccountManagementInterface as CustomerAccountManagement; use Magento\Customer\Model\Session; use Magento\Customer\Model\Url as CustomerUrl; use Magento\Framework\App\Action\Context; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; @@ -22,9 +24,11 @@ use Magento\Newsletter\Model\SubscriberFactory; /** + * New newsletter subscription action + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class NewAction extends SubscriberController +class NewAction extends SubscriberController implements HttpPostActionInterface { /** * @var CustomerAccountManagement @@ -77,8 +81,9 @@ public function __construct( protected function validateEmailAvailable($email) { $websiteId = $this->_storeManager->getStore()->getWebsiteId(); - if ($this->_customerSession->getCustomerDataObject()->getEmail() !== $email - && !$this->customerAccountManagement->isEmailAvailable($email, $websiteId) + if ($this->_customerSession->isLoggedIn() + && ($this->_customerSession->getCustomerDataObject()->getEmail() !== $email + && !$this->customerAccountManagement->isEmailAvailable($email, $websiteId)) ) { throw new LocalizedException( __('This email address is already assigned to another user.') @@ -127,7 +132,7 @@ protected function validateEmailFormat($email) /** * New subscription action * - * @return void + * @return \Magento\Framework\Controller\Result\Redirect */ public function execute() { @@ -151,18 +156,20 @@ public function execute() $status = (int) $this->_subscriberFactory->create()->subscribe($email); $this->messageManager->addSuccessMessage($this->getSuccessMessage($status)); } catch (LocalizedException $e) { - $this->messageManager->addExceptionMessage( - $e, - __('There was a problem with the subscription: %1', $e->getMessage()) - ); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { $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); } /** + * Get success message + * * @param int $status * @return Phrase */ diff --git a/app/code/Magento/Newsletter/Controller/Subscriber/Unsubscribe.php b/app/code/Magento/Newsletter/Controller/Subscriber/Unsubscribe.php index efc469e15dea..e37a3786e140 100644 --- a/app/code/Magento/Newsletter/Controller/Subscriber/Unsubscribe.php +++ b/app/code/Magento/Newsletter/Controller/Subscriber/Unsubscribe.php @@ -1,16 +1,22 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Newsletter\Controller\Subscriber; -class Unsubscribe extends \Magento\Newsletter\Controller\Subscriber +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +/** + * Controller for unsubscribing customers. + */ +class Unsubscribe extends \Magento\Newsletter\Controller\Subscriber implements HttpGetActionInterface { /** - * Unsubscribe newsletter - * @return void + * Unsubscribe newsletter. + * + * @return \Magento\Framework\Controller\Result\Redirect */ public function execute() { @@ -20,13 +26,16 @@ 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.')); } } - $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/Model/Plugin/CustomerPlugin.php b/app/code/Magento/Newsletter/Model/Plugin/CustomerPlugin.php index 22b31575debb..309bfadab41b 100644 --- a/app/code/Magento/Newsletter/Model/Plugin/CustomerPlugin.php +++ b/app/code/Magento/Newsletter/Model/Plugin/CustomerPlugin.php @@ -12,6 +12,9 @@ use Magento\Newsletter\Model\ResourceModel\Subscriber; use Magento\Customer\Api\Data\CustomerExtensionInterface; +/** + * Newsletter Plugin for customer + */ class CustomerPlugin { /** diff --git a/app/code/Magento/Newsletter/Model/Queue.php b/app/code/Magento/Newsletter/Model/Queue.php index efb68fd4243d..a3279f8c8369 100644 --- a/app/code/Magento/Newsletter/Model/Queue.php +++ b/app/code/Magento/Newsletter/Model/Queue.php @@ -7,6 +7,7 @@ use Magento\Framework\App\TemplateTypesInterface; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Framework\Stdlib\DateTime\Timezone\LocalizedDateToUtcConverterInterface; /** * Newsletter queue model. @@ -117,6 +118,11 @@ class Queue extends \Magento\Framework\Model\AbstractModel implements TemplateTy */ private $timezone; + /** + * @var LocalizedDateToUtcConverterInterface + */ + private $utcConverter; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -130,6 +136,7 @@ class Queue extends \Magento\Framework\Model\AbstractModel implements TemplateTy * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data * @param TimezoneInterface $timezone + * @param LocalizedDateToUtcConverterInterface $utcConverter * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -144,7 +151,8 @@ public function __construct( \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], - TimezoneInterface $timezone = null + TimezoneInterface $timezone = null, + LocalizedDateToUtcConverterInterface $utcConverter = null ) { parent::__construct( $context, @@ -159,9 +167,10 @@ public function __construct( $this->_problemFactory = $problemFactory; $this->_subscribersCollection = $subscriberCollectionFactory->create(); $this->_transportBuilder = $transportBuilder; - $this->timezone = $timezone ?: \Magento\Framework\App\ObjectManager::getInstance()->get( - TimezoneInterface::class - ); + + $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); + $this->timezone = $timezone ?: $objectManager->get(TimezoneInterface::class); + $this->utcConverter = $utcConverter ?? $objectManager->get(LocalizedDateToUtcConverterInterface::class); } /** @@ -196,7 +205,7 @@ public function setQueueStartAtByString($startAt) if ($startAt === null || $startAt == '') { $this->setQueueStartAt(null); } else { - $this->setQueueStartAt($this->timezone->convertConfigTimeToUtc($startAt)); + $this->setQueueStartAt($this->utcConverter->convertLocalizedDateToUtc($startAt)); } return $this; } diff --git a/app/code/Magento/Newsletter/Model/ResourceModel/Queue/Collection.php b/app/code/Magento/Newsletter/Model/ResourceModel/Queue/Collection.php index 189549d2a73d..52009dad6614 100644 --- a/app/code/Magento/Newsletter/Model/ResourceModel/Queue/Collection.php +++ b/app/code/Magento/Newsletter/Model/ResourceModel/Queue/Collection.php @@ -141,8 +141,7 @@ public function addSubscribersInfo() } /** - * Checks if field is 'subscribers_total', 'subscribers_sent' - * to add specific filter or adds regular filter + * Checks if field is 'subscribers_total', 'subscribers_sent' to add specific filter or adds regular filter * * @param string $field * @param null|string|array $condition @@ -210,7 +209,7 @@ public function addSubscriberFilter($subscriberId) } /** - * Add filter by only ready fot sending item + * Add filter by only ready for sending item * * @return $this */ diff --git a/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php b/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php index 4e059f9fe79a..f9e9d57bf4b4 100644 --- a/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php +++ b/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php @@ -5,6 +5,9 @@ */ namespace Magento\Newsletter\Model\ResourceModel; +use Magento\Framework\App\ObjectManager; +use Magento\Store\Model\StoreManagerInterface; + /** * Newsletter subscriber resource model * @@ -49,11 +52,11 @@ class Subscriber extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb protected $mathRandom; /** - * Guest customer id + * Store manager * - * @var int + * @var StoreManagerInterface */ - private $guestCustomerId = 0; + private $storeManager; /** * Construct @@ -62,21 +65,23 @@ class Subscriber extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb * @param \Magento\Framework\Stdlib\DateTime\DateTime $date * @param \Magento\Framework\Math\Random $mathRandom * @param string $connectionName + * @param StoreManagerInterface $storeManager */ public function __construct( \Magento\Framework\Model\ResourceModel\Db\Context $context, \Magento\Framework\Stdlib\DateTime\DateTime $date, \Magento\Framework\Math\Random $mathRandom, - $connectionName = null + $connectionName = null, + StoreManagerInterface $storeManager = null ) { $this->_date = $date; $this->mathRandom = $mathRandom; + $this->storeManager = $storeManager ?: ObjectManager::getInstance()->get(StoreManagerInterface::class); parent::__construct($context, $connectionName); } /** - * Initialize resource model - * Get tablename from config + * Initialize resource model. Get tablename from config * * @return void */ @@ -125,38 +130,32 @@ public function loadByEmail($subscriberEmail) */ public function loadByCustomerData(\Magento\Customer\Api\Data\CustomerInterface $customer) { - $select = $this->connection - ->select() - ->from($this->getMainTable()) - ->where('customer_id=:customer_id and store_id=:store_id'); - - $result = $this->connection - ->fetchRow( - $select, - [ - 'customer_id' => $customer->getId(), - 'store_id' => $customer->getStoreId() - ] - ); + $storeIds = $this->storeManager->getWebsite($customer->getWebsiteId())->getStoreIds(); + + if ($customer->getId()) { + $select = $this->connection + ->select() + ->from($this->getMainTable()) + ->where('customer_id = ?', $customer->getId()) + ->where('store_id IN (?)', $storeIds) + ->limit(1); - if ($result) { - return $result; + $result = $this->connection->fetchRow($select); + + if ($result) { + return $result; + } } - if ($customer->getId() === $this->guestCustomerId) { + if ($customer->getEmail()) { $select = $this->connection ->select() ->from($this->getMainTable()) - ->where('subscriber_email=:subscriber_email and store_id=:store_id'); - - $result = $this->connection - ->fetchRow( - $select, - [ - 'subscriber_email' => $customer->getEmail(), - 'store_id' => $customer->getStoreId() - ] - ); + ->where('subscriber_email = ?', $customer->getEmail()) + ->where('store_id IN (?)', $storeIds) + ->limit(1); + + $result = $this->connection->fetchRow($select); if ($result) { return $result; diff --git a/app/code/Magento/Newsletter/Model/Subscriber.php b/app/code/Magento/Newsletter/Model/Subscriber.php index 27ee8197778f..e7e5d5f20281 100644 --- a/app/code/Magento/Newsletter/Model/Subscriber.php +++ b/app/code/Magento/Newsletter/Model/Subscriber.php @@ -9,6 +9,9 @@ use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Framework\Exception\MailException; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Customer\Api\Data\CustomerInterfaceFactory; +use Magento\Framework\Api\DataObjectHelper; +use Magento\Framework\App\ObjectManager; /** * Subscriber model @@ -127,6 +130,16 @@ class Subscriber extends \Magento\Framework\Model\AbstractModel */ protected $inlineTranslation; + /** + * @var CustomerInterfaceFactory + */ + private $customerFactory; + + /** + * @var DataObjectHelper + */ + private $dataObjectHelper; + /** * Initialize dependencies. * @@ -144,6 +157,8 @@ class Subscriber extends \Magento\Framework\Model\AbstractModel * @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection * @param array $data * @param \Magento\Framework\Stdlib\DateTime\DateTime|null $dateTime + * @param CustomerInterfaceFactory|null $customerFactory + * @param DataObjectHelper|null $dataObjectHelper * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -160,7 +175,9 @@ public function __construct( \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], - \Magento\Framework\Stdlib\DateTime\DateTime $dateTime = null + \Magento\Framework\Stdlib\DateTime\DateTime $dateTime = null, + CustomerInterfaceFactory $customerFactory = null, + DataObjectHelper $dataObjectHelper = null ) { $this->_newsletterData = $newsletterData; $this->_scopeConfig = $scopeConfig; @@ -170,6 +187,10 @@ public function __construct( $this->dateTime = $dateTime ?: \Magento\Framework\App\ObjectManager::getInstance()->get( \Magento\Framework\Stdlib\DateTime\DateTime::class ); + $this->customerFactory = $customerFactory ?: ObjectManager::getInstance() + ->get(CustomerInterfaceFactory::class); + $this->dataObjectHelper = $dataObjectHelper ?: ObjectManager::getInstance() + ->get(DataObjectHelper::class); $this->customerRepository = $customerRepository; $this->customerAccountManagement = $customerAccountManagement; $this->inlineTranslation = $inlineTranslation; @@ -346,7 +367,17 @@ public function isSubscribed() */ public function loadByEmail($subscriberEmail) { - $this->addData($this->getResource()->loadByEmail($subscriberEmail)); + $storeId = $this->_storeManager->getStore()->getId(); + $customerData = ['store_id' => $storeId, 'email'=> $subscriberEmail]; + + /** @var \Magento\Customer\Api\Data\CustomerInterface $customer */ + $customer = $this->customerFactory->create(); + $this->dataObjectHelper->populateWithArray( + $customer, + $customerData, + \Magento\Customer\Api\Data\CustomerInterface::class + ); + $this->addData($this->getResource()->loadByCustomerData($customer)); return $this; } @@ -361,6 +392,9 @@ public function loadByCustomerId($customerId) try { $customerData = $this->customerRepository->getById($customerId); $customerData->setStoreId($this->_storeManager->getStore()->getId()); + if ($customerData->getWebsiteId() === null) { + $customerData->setWebsiteId($this->_storeManager->getStore()->getWebsiteId()); + } $data = $this->getResource()->loadByCustomerData($customerData); $this->addData($data); if (!empty($data) && $customerData->getId() && !$this->getCustomerId()) { @@ -497,7 +531,7 @@ public function subscribeCustomerById($customerId) } /** - * unsubscribe the customer with the id provided + * Unsubscribe the customer with the id provided * * @param int $customerId * @return $this @@ -557,7 +591,12 @@ protected function _updateCustomerSubscription($customerId, $subscribe) if (AccountManagementInterface::ACCOUNT_CONFIRMATION_REQUIRED == $this->customerAccountManagement->getConfirmationStatus($customerId) ) { - $status = self::STATUS_UNCONFIRMED; + if ($this->getId() && $this->getStatus() == self::STATUS_SUBSCRIBED) { + // if a customer was already subscribed then keep the subscribed + $status = self::STATUS_SUBSCRIBED; + } else { + $status = self::STATUS_UNCONFIRMED; + } } elseif ($isConfirmNeed) { if ($this->getStatus() != self::STATUS_SUBSCRIBED) { $status = self::STATUS_NOT_ACTIVE; @@ -566,6 +605,8 @@ protected function _updateCustomerSubscription($customerId, $subscribe) } elseif (($this->getStatus() == self::STATUS_UNCONFIRMED) && ($customerData->getConfirmation() === null)) { $status = self::STATUS_SUBSCRIBED; $sendInformationEmail = true; + } elseif (($this->getStatus() == self::STATUS_NOT_ACTIVE) && ($customerData->getConfirmation() === null)) { + $status = self::STATUS_NOT_ACTIVE; } else { $status = self::STATUS_UNSUBSCRIBED; } @@ -582,16 +623,17 @@ protected function _updateCustomerSubscription($customerId, $subscribe) $this->setStatus($status); + $storeId = $customerData->getStoreId(); + if ((int)$customerData->getStoreId() === 0) { + $storeId = $this->_storeManager->getWebsite($customerData->getWebsiteId())->getDefaultStore()->getId(); + } + if (!$this->getId()) { - $storeId = $customerData->getStoreId(); - if ($customerData->getStoreId() == 0) { - $storeId = $this->_storeManager->getWebsite($customerData->getWebsiteId())->getDefaultStore()->getId(); - } $this->setStoreId($storeId) ->setCustomerId($customerData->getId()) ->setEmail($customerData->getEmail()); } else { - $this->setStoreId($customerData->getStoreId()) + $this->setStoreId($storeId) ->setEmail($customerData->getEmail()); } diff --git a/app/code/Magento/Newsletter/Observer/PredispatchNewsletterObserver.php b/app/code/Magento/Newsletter/Observer/PredispatchNewsletterObserver.php new file mode 100644 index 000000000000..9860798b2b9f --- /dev/null +++ b/app/code/Magento/Newsletter/Observer/PredispatchNewsletterObserver.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); + +namespace Magento\Newsletter\Observer; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\UrlInterface; +use Magento\Store\Model\ScopeInterface; + +/** + * Class PredispatchNewsletterObserver + */ +class PredispatchNewsletterObserver implements ObserverInterface +{ + /** + * Configuration path to newsletter active setting + */ + const XML_PATH_NEWSLETTER_ACTIVE = 'newsletter/general/active'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var UrlInterface + */ + private $url; + + /** + * PredispatchNewsletterObserver constructor. + * + * @param ScopeConfigInterface $scopeConfig + * @param UrlInterface $url + */ + public function __construct(ScopeConfigInterface $scopeConfig, UrlInterface $url) + { + $this->scopeConfig = $scopeConfig; + $this->url = $url; + } + + /** + * Redirect newsletter routes to 404 when newsletter module is disabled. + * + * @param Observer $observer + */ + public function execute(Observer $observer) : void + { + if (!$this->scopeConfig->getValue( + self::XML_PATH_NEWSLETTER_ACTIVE, + ScopeInterface::SCOPE_STORE + ) + ) { + $defaultNoRouteUrl = $this->scopeConfig->getValue( + 'web/default/no_route', + ScopeInterface::SCOPE_STORE + ); + $redirectUrl = $this->url->getUrl($defaultNoRouteUrl); + $observer->getControllerAction() + ->getResponse() + ->setRedirect($redirectUrl); + } + } +} diff --git a/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/VerifySubscribedNewsletterDisplayedActionGroup.xml b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/VerifySubscribedNewsletterDisplayedActionGroup.xml index 92d17e9d71e2..059f157c407c 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/VerifySubscribedNewsletterDisplayedActionGroup.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/VerifySubscribedNewsletterDisplayedActionGroup.xml @@ -6,13 +6,10 @@ */ --> <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <!--Create an Account. Check Sign Up for Newsletter checkbox --> <actionGroup name="StorefrontCreateNewAccountNewsletterChecked" extends="SignUpNewUserFromStorefrontActionGroup"> - <arguments> - <argument name="Customer"/> - </arguments> <click selector="{{StorefrontCustomerCreateFormSection.signUpForNewsletter}}" stepKey="selectSignUpForNewsletterCheckbox" after="fillLastName"/> <see stepKey="seeDescriptionNewsletter" userInput='You are subscribed to "General Subscription".' selector="{{CustomerMyAccountPage.DescriptionNewsletter}}" /> </actionGroup> @@ -28,4 +25,9 @@ <see stepKey="seeThankYouMessage" userInput="Thank you for registering with NewStore."/> </actionGroup> + <!--Check Subscribed Newsletter via StoreFront--> + <actionGroup name="CheckSubscribedNewsletterActionGroup"> + <amOnPage url="{{StorefrontNewsletterManagePage.url}}" stepKey="goToNewsletterManage"/> + <seeCheckboxIsChecked selector="{{StorefrontNewsletterManageSection.subscriptionCheckbox}}" stepKey="checkSubscribedNewsletter"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Newsletter/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 000000000000..1df1cd5f8dae --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminMenuMarketingCommunicationsNewsletterQueue"> + <data key="pageTitle">Newsletter Queue</data> + <data key="title">Newsletter Queue</data> + <data key="dataUiId">magento-newsletter-newsletter-queue</data> + </entity> + <entity name="AdminMenuMarketingCommunicationsNewsletterSubscribers"> + <data key="pageTitle">Newsletter Subscribers</data> + <data key="title">Newsletter Subscribers</data> + <data key="dataUiId">magento-newsletter-newsletter-subscriber</data> + </entity> + <entity name="AdminMenuMarketingCommunicationsNewsletterTemplate"> + <data key="pageTitle">Newsletter Template</data> + <data key="title">Newsletter Template</data> + <data key="dataUiId">magento-newsletter-newsletter-template</data> + </entity> + <entity name="AdminMenuReportsMarketingNewsletterProblemReports"> + <data key="pageTitle">Newsletter Problems Report</data> + <data key="title">Newsletter Problem Reports</data> + <data key="dataUiId">magento-newsletter-newsletter-problem</data> + </entity> +</entities> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Page/StorefrontNewsletterManagePage.xml b/app/code/Magento/Newsletter/Test/Mftf/Page/StorefrontNewsletterManagePage.xml new file mode 100644 index 000000000000..81fd3eb7c391 --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/Page/StorefrontNewsletterManagePage.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="StorefrontNewsletterManagePage" url="newsletter/manage/" area="storefront" module="Magento_Newsletter"> + <section name="StorefrontNewsletterManageSection"/> + </page> +</pages> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Section/StorefrontNewsletterManageSection.xml b/app/code/Magento/Newsletter/Test/Mftf/Section/StorefrontNewsletterManageSection.xml new file mode 100644 index 000000000000..96a944a4952a --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/Section/StorefrontNewsletterManageSection.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="StorefrontNewsletterManageSection"> + <element name="subscriptionCheckbox" type="checkbox" selector="#subscription" /> + </section> +</sections> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Section/VerifySubscribedNewsLetterDisplayedSection.xml b/app/code/Magento/Newsletter/Test/Mftf/Section/VerifySubscribedNewsLetterDisplayedSection.xml index 06f762900436..bb651784d4dc 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Section/VerifySubscribedNewsLetterDisplayedSection.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Section/VerifySubscribedNewsLetterDisplayedSection.xml @@ -7,8 +7,7 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> - + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontCustomerCreateFormSection"> <element name="signUpForNewsletter" type="checkbox" selector="//span[contains(text(), 'Sign Up for Newsletter')]"/> </section> @@ -16,5 +15,4 @@ <section name="CustomerMyAccountPage"> <element name="DescriptionNewsletter" type="text" selector=".box-newsletter p"/> </section> - -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterQueueNavigateMenuTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterQueueNavigateMenuTest.xml new file mode 100644 index 000000000000..31da588250a0 --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterQueueNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMarketingNewsletterQueueNavigateMenuTest"> + <annotations> + <features value="Newsletter"/> + <stories value="Menu Navigation"/> + <title value="Admin marketing newsletter queue navigate menu test"/> + <description value="Admin should be able to navigate to Marketing > Newsletter Queue"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14189"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToNewsletterQueuePage"> + <argument name="menuUiId" value="{{AdminMenuMarketing.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuMarketingCommunicationsNewsletterQueue.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuMarketingCommunicationsNewsletterQueue.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterSubscribersNavigateMenuTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterSubscribersNavigateMenuTest.xml new file mode 100644 index 000000000000..8ced2690322f --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterSubscribersNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMarketingNewsletterSubscribersNavigateMenuTest"> + <annotations> + <features value="Newsletter"/> + <stories value="Menu Navigation"/> + <title value="Admin marketing newsletter subscribers navigate menu test"/> + <description value="Admin should be able to navigate to Marketing > Newsletter Subscribers"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14190"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToNewsletterSubscribersPage"> + <argument name="menuUiId" value="{{AdminMenuMarketing.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuMarketingCommunicationsNewsletterSubscribers.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuMarketingCommunicationsNewsletterSubscribers.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterTemplateNavigateMenuTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterTemplateNavigateMenuTest.xml new file mode 100644 index 000000000000..ca994aa1d626 --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterTemplateNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMarketingNewsletterTemplateNavigateMenuTest"> + <annotations> + <features value="Newsletter"/> + <stories value="Menu Navigation"/> + <title value="Admin marketing newsletter template navigate menu test"/> + <description value="Admin should be able to navigate to Marketing > Newsletter Template"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14188"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToNewsletterTemplatePage"> + <argument name="menuUiId" value="{{AdminMenuMarketing.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuMarketingCommunicationsNewsletterTemplate.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuMarketingCommunicationsNewsletterTemplate.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminReportsNewsletterProblemReportsNavigateMenuTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminReportsNewsletterProblemReportsNavigateMenuTest.xml new file mode 100644 index 000000000000..3891b90536a1 --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminReportsNewsletterProblemReportsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminReportsNewsletterProblemReportsNavigateMenuTest"> + <annotations> + <features value="Newsletter"/> + <stories value="Menu Navigation"/> + <title value="Admin reports newsletter problem reports navigate menu test"/> + <description value="Admin should be able to navigate to Reports > Newsletter Problem Reports"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14191"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToNewsletterProblemsReportPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsMarketingNewsletterProblemReports.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsMarketingNewsletterProblemReports.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml index faed8b1af952..22ca214c94ae 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml @@ -5,8 +5,9 @@ * See COPYING.txt for license details. */ --> + <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="VerifySubscribedNewsletterDisplayedTest"> <annotations> <features value="Newsletter"/> @@ -46,6 +47,8 @@ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> <argument name="websiteName" value="Second"/> </actionGroup> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> </after> @@ -63,4 +66,3 @@ </actionGroup> </test> </tests> - diff --git a/app/code/Magento/Newsletter/Test/Unit/Model/ProblemTest.php b/app/code/Magento/Newsletter/Test/Unit/Model/ProblemTest.php index 889fc11d71d7..8ee6de1e4447 100644 --- a/app/code/Magento/Newsletter/Test/Unit/Model/ProblemTest.php +++ b/app/code/Magento/Newsletter/Test/Unit/Model/ProblemTest.php @@ -17,6 +17,9 @@ use Magento\Newsletter\Model\Subscriber; use Magento\Newsletter\Model\SubscriberFactory; +/** + * Class ProblemTest + */ class ProblemTest extends \PHPUnit\Framework\TestCase { /** @@ -59,6 +62,9 @@ class ProblemTest extends \PHPUnit\Framework\TestCase */ private $problemModel; + /** + * @inheritdoc + */ protected function setUp() { $this->contextMock = $this->getMockBuilder(Context::class) @@ -68,6 +74,7 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); $this->subscriberFactoryMock = $this->getMockBuilder(SubscriberFactory::class) + ->disableOriginalConstructor() ->getMock(); $this->subscriberMock = $this->getMockBuilder(Subscriber::class) ->disableOriginalConstructor() @@ -98,6 +105,9 @@ protected function setUp() ); } + /** + * @return void + */ public function testAddSubscriberData() { $subscriberId = 1; @@ -111,6 +121,9 @@ public function testAddSubscriberData() self::assertEquals($subscriberId, $this->problemModel->getSubscriberId()); } + /** + * @return void + */ public function testAddQueueData() { $queueId = 1; @@ -127,6 +140,9 @@ public function testAddQueueData() self::assertEquals($queueId, $this->problemModel->getQueueId()); } + /** + * @return void + */ public function testAddErrorData() { $exceptionMessage = 'Some message'; @@ -140,17 +156,26 @@ public function testAddErrorData() self::assertEquals($exceptionCode, $this->problemModel->getProblemErrorCode()); } + /** + * @return void + */ public function testGetSubscriberWithNoSubscriberId() { self::assertNull($this->problemModel->getSubscriber()); } + /** + * @return void + */ public function testGetSubscriber() { $this->setSubscriber(); self::assertEquals($this->subscriberMock, $this->problemModel->getSubscriber()); } + /** + * @return void + */ public function testUnsubscribeWithNoSubscriber() { $this->subscriberMock->expects($this->never()) @@ -162,6 +187,9 @@ public function testUnsubscribeWithNoSubscriber() self::assertEquals($this->problemModel, $result); } + /** + * @return void + */ public function testUnsubscribe() { $this->setSubscriber(); diff --git a/app/code/Magento/Newsletter/Test/Unit/Model/SubscriberTest.php b/app/code/Magento/Newsletter/Test/Unit/Model/SubscriberTest.php index 1318cb1f98f5..6ccbba9f8828 100644 --- a/app/code/Magento/Newsletter/Test/Unit/Model/SubscriberTest.php +++ b/app/code/Magento/Newsletter/Test/Unit/Model/SubscriberTest.php @@ -62,6 +62,16 @@ class SubscriberTest extends \PHPUnit\Framework\TestCase */ protected $objectManager; + /** + * @var \Magento\Framework\Api\DataObjectHelper|\PHPUnit_Framework_MockObject_MockObject + */ + private $dataObjectHelper; + + /** + * @var \Magento\Customer\Api\Data\CustomerInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $customerFactory; + /** * @var \Magento\Newsletter\Model\Subscriber */ @@ -97,6 +107,14 @@ protected function setUp() ]); $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->customerFactory = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterfaceFactory::class) + ->setMethods(['create']) + ->disableOriginalConstructor() + ->getMock(); + $this->dataObjectHelper = $this->getMockBuilder(\Magento\Framework\Api\DataObjectHelper::class) + ->disableOriginalConstructor() + ->getMock(); + $this->subscriber = $this->objectManager->getObject( \Magento\Newsletter\Model\Subscriber::class, [ @@ -108,7 +126,9 @@ protected function setUp() 'customerRepository' => $this->customerRepository, 'customerAccountManagement' => $this->customerAccountManagement, 'inlineTranslation' => $this->inlineTranslation, - 'resource' => $this->resource + 'resource' => $this->resource, + 'customerFactory' => $this->customerFactory, + 'dataObjectHelper' => $this->dataObjectHelper ] ); } @@ -116,7 +136,21 @@ protected function setUp() public function testSubscribe() { $email = 'subscriber_email@magento.com'; - $this->resource->expects($this->any())->method('loadByEmail')->willReturn( + $storeId = 1; + $customerData = ['store_id' => $storeId, 'email' => $email]; + $storeModel = $this->getMockBuilder(\Magento\Store\Model\Store::class) + ->disableOriginalConstructor() + ->getMock(); + $this->storeManager->expects($this->any())->method('getStore')->willReturn($storeModel); + $storeModel->expects($this->any())->method('getId')->willReturn($storeId); + $customer = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); + $this->customerFactory->expects($this->once())->method('create')->willReturn($customer); + $this->dataObjectHelper->expects($this->once())->method('populateWithArray')->with( + $customer, + $customerData, + \Magento\Customer\Api\Data\CustomerInterface::class + ); + $this->resource->expects($this->any())->method('loadByCustomerData')->with($customer)->willReturn( [ 'subscriber_status' => Subscriber::STATUS_UNSUBSCRIBED, 'subscriber_email' => $email, @@ -130,7 +164,7 @@ public function testSubscribe() $this->customerSession->expects($this->any())->method('getCustomerId')->willReturn(1); $customerDataModel->expects($this->any())->method('getEmail')->willReturn($email); $this->customerRepository->expects($this->any())->method('getById')->willReturn($customerDataModel); - $customerDataModel->expects($this->any())->method('getStoreId')->willReturn(1); + $customerDataModel->expects($this->any())->method('getStoreId')->willReturn($storeId); $customerDataModel->expects($this->any())->method('getId')->willReturn(1); $this->sendEmailCheck(); $this->resource->expects($this->atLeastOnce())->method('save')->willReturnSelf(); @@ -141,7 +175,21 @@ public function testSubscribe() public function testSubscribeNotLoggedIn() { $email = 'subscriber_email@magento.com'; - $this->resource->expects($this->any())->method('loadByEmail')->willReturn( + $storeId = 1; + $customerData = ['store_id' => $storeId, 'email' => $email]; + $storeModel = $this->getMockBuilder(\Magento\Store\Model\Store::class) + ->disableOriginalConstructor() + ->getMock(); + $this->storeManager->expects($this->any())->method('getStore')->willReturn($storeModel); + $storeModel->expects($this->any())->method('getId')->willReturn($storeId); + $customer = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); + $this->customerFactory->expects($this->once())->method('create')->willReturn($customer); + $this->dataObjectHelper->expects($this->once())->method('populateWithArray')->with( + $customer, + $customerData, + \Magento\Customer\Api\Data\CustomerInterface::class + ); + $this->resource->expects($this->any())->method('loadByCustomerData')->with($customer)->willReturn( [ 'subscriber_status' => Subscriber::STATUS_UNSUBSCRIBED, 'subscriber_email' => $email, @@ -155,7 +203,7 @@ public function testSubscribeNotLoggedIn() $this->customerSession->expects($this->any())->method('getCustomerId')->willReturn(1); $customerDataModel->expects($this->any())->method('getEmail')->willReturn($email); $this->customerRepository->expects($this->any())->method('getById')->willReturn($customerDataModel); - $customerDataModel->expects($this->any())->method('getStoreId')->willReturn(1); + $customerDataModel->expects($this->any())->method('getStoreId')->willReturn($storeId); $customerDataModel->expects($this->any())->method('getId')->willReturn(1); $this->sendEmailCheck(); $this->resource->expects($this->atLeastOnce())->method('save')->willReturnSelf(); @@ -165,6 +213,7 @@ public function testSubscribeNotLoggedIn() public function testUpdateSubscription() { + $storeId = 2; $customerId = 1; $customerDataMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) ->getMock(); @@ -186,7 +235,7 @@ public function testUpdateSubscription() ->method('getConfirmationStatus') ->with($customerId) ->willReturn('account_confirmation_required'); - $customerDataMock->expects($this->once())->method('getStoreId')->willReturn('store_id'); + $customerDataMock->expects($this->exactly(2))->method('getStoreId')->willReturn($storeId); $customerDataMock->expects($this->once())->method('getEmail')->willReturn('email'); $storeModel = $this->getMockBuilder(\Magento\Store\Model\Store::class) @@ -200,6 +249,7 @@ public function testUpdateSubscription() public function testUnsubscribeCustomerById() { + $storeId = 2; $customerId = 1; $customerDataMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) ->getMock(); @@ -217,7 +267,7 @@ public function testUnsubscribeCustomerById() ); $customerDataMock->expects($this->atLeastOnce())->method('getId')->willReturn('id'); $this->resource->expects($this->atLeastOnce())->method('save')->willReturnSelf(); - $customerDataMock->expects($this->once())->method('getStoreId')->willReturn('store_id'); + $customerDataMock->expects($this->exactly(2))->method('getStoreId')->willReturn($storeId); $customerDataMock->expects($this->once())->method('getEmail')->willReturn('email'); $this->sendEmailCheck(); @@ -226,6 +276,7 @@ public function testUnsubscribeCustomerById() public function testSubscribeCustomerById() { + $storeId = 2; $customerId = 1; $customerDataMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) ->getMock(); @@ -243,7 +294,7 @@ public function testSubscribeCustomerById() ); $customerDataMock->expects($this->atLeastOnce())->method('getId')->willReturn('id'); $this->resource->expects($this->atLeastOnce())->method('save')->willReturnSelf(); - $customerDataMock->expects($this->once())->method('getStoreId')->willReturn('store_id'); + $customerDataMock->expects($this->exactly(2))->method('getStoreId')->willReturn($storeId); $customerDataMock->expects($this->once())->method('getEmail')->willReturn('email'); $this->sendEmailCheck(); @@ -252,6 +303,7 @@ public function testSubscribeCustomerById() public function testSubscribeCustomerById1() { + $storeId = 2; $customerId = 1; $customerDataMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) ->getMock(); @@ -269,7 +321,7 @@ public function testSubscribeCustomerById1() ); $customerDataMock->expects($this->atLeastOnce())->method('getId')->willReturn('id'); $this->resource->expects($this->atLeastOnce())->method('save')->willReturnSelf(); - $customerDataMock->expects($this->once())->method('getStoreId')->willReturn('store_id'); + $customerDataMock->expects($this->exactly(2))->method('getStoreId')->willReturn($storeId); $customerDataMock->expects($this->once())->method('getEmail')->willReturn('email'); $this->sendEmailCheck(); $this->customerAccountManagement->expects($this->once()) @@ -283,6 +335,7 @@ public function testSubscribeCustomerById1() public function testSubscribeCustomerByIdAfterConfirmation() { + $storeId = 2; $customerId = 1; $customerDataMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) ->getMock(); @@ -300,7 +353,7 @@ public function testSubscribeCustomerByIdAfterConfirmation() ); $customerDataMock->expects($this->atLeastOnce())->method('getId')->willReturn('id'); $this->resource->expects($this->atLeastOnce())->method('save')->willReturnSelf(); - $customerDataMock->expects($this->once())->method('getStoreId')->willReturn('store_id'); + $customerDataMock->expects($this->exactly(2))->method('getStoreId')->willReturn($storeId); $customerDataMock->expects($this->once())->method('getEmail')->willReturn('email'); $this->sendEmailCheck(); $this->customerAccountManagement->expects($this->never())->method('getConfirmationStatus'); diff --git a/app/code/Magento/Newsletter/Test/Unit/Observer/PredispatchNewsletterObserverTest.php b/app/code/Magento/Newsletter/Test/Unit/Observer/PredispatchNewsletterObserverTest.php new file mode 100644 index 000000000000..38d69e5128af --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Unit/Observer/PredispatchNewsletterObserverTest.php @@ -0,0 +1,147 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); + +namespace Magento\Newsletter\Test\Unit\Observer; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Response\RedirectInterface; +use Magento\Framework\App\ResponseInterface; +use Magento\Framework\Event\Observer; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\UrlInterface; +use Magento\Newsletter\Observer\PredispatchNewsletterObserver; +use Magento\Store\Model\ScopeInterface; +use PHPUnit\Framework\TestCase; + +/** + * Test class for \Magento\Newsletter\Observer\PredispatchNewsletterObserver + */ +class PredispatchNewsletterObserverTest extends TestCase +{ + /** + * @var Observer|\PHPUnit_Framework_MockObject_MockObject + */ + private $mockObject; + + /** + * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $configMock; + + /** + * @var UrlInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $urlMock; + + /** + * @var \Magento\Framework\App\Response\RedirectInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $redirectMock; + + /** + * @var ResponseInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $responseMock; + + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @inheritdoc + */ + protected function setUp() : void + { + $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->urlMock = $this->getMockBuilder(UrlInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->responseMock = $this->getMockBuilder(ResponseInterface::class) + ->disableOriginalConstructor() + ->setMethods(['setRedirect']) + ->getMockForAbstractClass(); + $this->redirectMock = $this->getMockBuilder(RedirectInterface::class) + ->getMock(); + $this->objectManager = new ObjectManager($this); + $this->mockObject = $this->objectManager->getObject( + PredispatchNewsletterObserver::class, + [ + 'scopeConfig' => $this->configMock, + 'url' => $this->urlMock + ] + ); + } + + /** + * Test with enabled newsletter active config. + */ + public function testNewsletterEnabled() : void + { + $observerMock = $this->getMockBuilder(Observer::class) + ->disableOriginalConstructor() + ->setMethods(['getResponse', 'getData', 'setRedirect']) + ->getMockForAbstractClass(); + + $this->configMock->method('getValue') + ->with(PredispatchNewsletterObserver::XML_PATH_NEWSLETTER_ACTIVE, ScopeInterface::SCOPE_STORE) + ->willReturn(true); + $observerMock->expects($this->never()) + ->method('getData') + ->with('controller_action') + ->willReturnSelf(); + + $observerMock->expects($this->never()) + ->method('getResponse') + ->willReturnSelf(); + + $this->assertNull($this->mockObject->execute($observerMock)); + } + + /** + * Test with disabled newsletter active config. + */ + public function testNewsletterDisabled() : void + { + $observerMock = $this->getMockBuilder(Observer::class) + ->disableOriginalConstructor() + ->setMethods(['getControllerAction', 'getResponse']) + ->getMockForAbstractClass(); + + $this->configMock->expects($this->at(0)) + ->method('getValue') + ->with(PredispatchNewsletterObserver::XML_PATH_NEWSLETTER_ACTIVE, ScopeInterface::SCOPE_STORE) + ->willReturn(false); + + $expectedRedirectUrl = 'https://test.com/index'; + + $this->configMock->expects($this->at(1)) + ->method('getValue') + ->with('web/default/no_route', ScopeInterface::SCOPE_STORE) + ->willReturn($expectedRedirectUrl); + + $this->urlMock->expects($this->once()) + ->method('getUrl') + ->willReturn($expectedRedirectUrl); + + $observerMock->expects($this->once()) + ->method('getControllerAction') + ->willReturnSelf(); + + $observerMock->expects($this->once()) + ->method('getResponse') + ->willReturn($this->responseMock); + + $this->responseMock->expects($this->once()) + ->method('setRedirect') + ->with($expectedRedirectUrl); + + $this->assertNull($this->mockObject->execute($observerMock)); + } +} diff --git a/app/code/Magento/Newsletter/etc/adminhtml/system.xml b/app/code/Magento/Newsletter/etc/adminhtml/system.xml index 1173f6431030..16af7b2158dd 100644 --- a/app/code/Magento/Newsletter/etc/adminhtml/system.xml +++ b/app/code/Magento/Newsletter/etc/adminhtml/system.xml @@ -11,39 +11,46 @@ <label>Newsletter</label> <tab>customer</tab> <resource>Magento_Newsletter::newsletter</resource> - <group id="subscription" translate="label" type="text" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1"> + <group id="general" translate="label" type="text" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1"> + <label>General Options</label> + <field id="active" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <label>Enabled</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + </field> + </group> + <group id="subscription" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Subscription Options</label> - <field id="allow_guest_subscribe" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="allow_guest_subscribe" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Allow Guest Subscription</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> - <field id="confirm" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="confirm" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Need to Confirm</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> - <field id="confirm_email_identity" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="confirm_email_identity" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Confirmation Email Sender</label> <source_model>Magento\Config\Model\Config\Source\Email\Identity</source_model> </field> - <field id="confirm_email_template" translate="label comment" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="confirm_email_template" translate="label comment" type="select" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Confirmation Email Template</label> <comment>Email template chosen based on theme fallback when "Default" option is selected.</comment> <source_model>Magento\Config\Model\Config\Source\Email\Template</source_model> </field> - <field id="success_email_identity" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="success_email_identity" translate="label" type="select" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Success Email Sender</label> <source_model>Magento\Config\Model\Config\Source\Email\Identity</source_model> </field> - <field id="success_email_template" translate="label comment" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="success_email_template" translate="label comment" type="select" sortOrder="60" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Success Email Template</label> <comment>Email template chosen based on theme fallback when "Default" option is selected.</comment> <source_model>Magento\Config\Model\Config\Source\Email\Template</source_model> </field> - <field id="un_email_identity" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="un_email_identity" translate="label" type="select" sortOrder="70" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Unsubscription Email Sender</label> <source_model>Magento\Config\Model\Config\Source\Email\Identity</source_model> </field> - <field id="un_email_template" translate="label comment" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="un_email_template" translate="label comment" type="select" sortOrder="80" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Unsubscription Email Template</label> <comment>Email template chosen based on theme fallback when "Default" option is selected.</comment> <source_model>Magento\Config\Model\Config\Source\Email\Template</source_model> diff --git a/app/code/Magento/Newsletter/etc/config.xml b/app/code/Magento/Newsletter/etc/config.xml index f976ece8d712..4c5e385105cf 100644 --- a/app/code/Magento/Newsletter/etc/config.xml +++ b/app/code/Magento/Newsletter/etc/config.xml @@ -8,6 +8,9 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd"> <default> <newsletter> + <general> + <active>1</active> + </general> <subscription> <allow_guest_subscribe>1</allow_guest_subscribe> <confirm>0</confirm> diff --git a/app/code/Magento/Newsletter/etc/db_schema.xml b/app/code/Magento/Newsletter/etc/db_schema.xml index 5084b8b6d01e..5cb572f41b6b 100644 --- a/app/code/Magento/Newsletter/etc/db_schema.xml +++ b/app/code/Magento/Newsletter/etc/db_schema.xml @@ -21,19 +21,19 @@ default="0" comment="Subscriber Status"/> <column xsi:type="varchar" name="subscriber_confirm_code" nullable="true" length="32" default="NULL" comment="Subscriber Confirm Code"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="subscriber_id"/> </constraint> - <constraint xsi:type="foreign" name="NEWSLETTER_SUBSCRIBER_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="NEWSLETTER_SUBSCRIBER_STORE_ID_STORE_STORE_ID" table="newsletter_subscriber" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="SET NULL"/> - <index name="NEWSLETTER_SUBSCRIBER_CUSTOMER_ID" indexType="btree"> + <index referenceId="NEWSLETTER_SUBSCRIBER_CUSTOMER_ID" indexType="btree"> <column name="customer_id"/> </index> - <index name="NEWSLETTER_SUBSCRIBER_STORE_ID" indexType="btree"> + <index referenceId="NEWSLETTER_SUBSCRIBER_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="NEWSLETTER_SUBSCRIBER_SUBSCRIBER_EMAIL" indexType="btree"> + <index referenceId="NEWSLETTER_SUBSCRIBER_SUBSCRIBER_EMAIL" indexType="btree"> <column name="subscriber_email"/> </index> </table> @@ -54,16 +54,16 @@ default="1" comment="Template Actual"/> <column xsi:type="timestamp" name="added_at" on_update="false" nullable="true" comment="Added At"/> <column xsi:type="timestamp" name="modified_at" on_update="false" nullable="true" comment="Modified At"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="template_id"/> </constraint> - <index name="NEWSLETTER_TEMPLATE_TEMPLATE_ACTUAL" indexType="btree"> + <index referenceId="NEWSLETTER_TEMPLATE_TEMPLATE_ACTUAL" indexType="btree"> <column name="template_actual"/> </index> - <index name="NEWSLETTER_TEMPLATE_ADDED_AT" indexType="btree"> + <index referenceId="NEWSLETTER_TEMPLATE_ADDED_AT" indexType="btree"> <column name="added_at"/> </index> - <index name="NEWSLETTER_TEMPLATE_MODIFIED_AT" indexType="btree"> + <index referenceId="NEWSLETTER_TEMPLATE_MODIFIED_AT" indexType="btree"> <column name="modified_at"/> </index> </table> @@ -86,13 +86,13 @@ <column xsi:type="timestamp" name="queue_start_at" on_update="false" nullable="true" comment="Queue Start At"/> <column xsi:type="timestamp" name="queue_finish_at" on_update="false" nullable="true" comment="Queue Finish At"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="queue_id"/> </constraint> - <constraint xsi:type="foreign" name="NEWSLETTER_QUEUE_TEMPLATE_ID_NEWSLETTER_TEMPLATE_TEMPLATE_ID" + <constraint xsi:type="foreign" referenceId="NEWSLETTER_QUEUE_TEMPLATE_ID_NEWSLETTER_TEMPLATE_TEMPLATE_ID" table="newsletter_queue" column="template_id" referenceTable="newsletter_template" referenceColumn="template_id" onDelete="CASCADE"/> - <index name="NEWSLETTER_QUEUE_TEMPLATE_ID" indexType="btree"> + <index referenceId="NEWSLETTER_QUEUE_TEMPLATE_ID" indexType="btree"> <column name="template_id"/> </index> </table> @@ -104,19 +104,19 @@ <column xsi:type="int" name="subscriber_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Subscriber Id"/> <column xsi:type="timestamp" name="letter_sent_at" on_update="false" nullable="true" comment="Letter Sent At"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="queue_link_id"/> </constraint> - <constraint xsi:type="foreign" name="NEWSLETTER_QUEUE_LINK_QUEUE_ID_NEWSLETTER_QUEUE_QUEUE_ID" + <constraint xsi:type="foreign" referenceId="NEWSLETTER_QUEUE_LINK_QUEUE_ID_NEWSLETTER_QUEUE_QUEUE_ID" table="newsletter_queue_link" column="queue_id" referenceTable="newsletter_queue" referenceColumn="queue_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="NLTTR_QUEUE_LNK_SUBSCRIBER_ID_NLTTR_SUBSCRIBER_SUBSCRIBER_ID" + <constraint xsi:type="foreign" referenceId="NLTTR_QUEUE_LNK_SUBSCRIBER_ID_NLTTR_SUBSCRIBER_SUBSCRIBER_ID" table="newsletter_queue_link" column="subscriber_id" referenceTable="newsletter_subscriber" referenceColumn="subscriber_id" onDelete="CASCADE"/> - <index name="NEWSLETTER_QUEUE_LINK_SUBSCRIBER_ID" indexType="btree"> + <index referenceId="NEWSLETTER_QUEUE_LINK_SUBSCRIBER_ID" indexType="btree"> <column name="subscriber_id"/> </index> - <index name="NEWSLETTER_QUEUE_LINK_QUEUE_ID_LETTER_SENT_AT" indexType="btree"> + <index referenceId="NEWSLETTER_QUEUE_LINK_QUEUE_ID_LETTER_SENT_AT" indexType="btree"> <column name="queue_id"/> <column name="letter_sent_at"/> </index> @@ -126,17 +126,17 @@ default="0" comment="Queue Id"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Store Id"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="queue_id"/> <column name="store_id"/> </constraint> - <constraint xsi:type="foreign" name="NEWSLETTER_QUEUE_STORE_LINK_QUEUE_ID_NEWSLETTER_QUEUE_QUEUE_ID" + <constraint xsi:type="foreign" referenceId="NEWSLETTER_QUEUE_STORE_LINK_QUEUE_ID_NEWSLETTER_QUEUE_QUEUE_ID" table="newsletter_queue_store_link" column="queue_id" referenceTable="newsletter_queue" referenceColumn="queue_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="NEWSLETTER_QUEUE_STORE_LINK_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="NEWSLETTER_QUEUE_STORE_LINK_STORE_ID_STORE_STORE_ID" table="newsletter_queue_store_link" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <index name="NEWSLETTER_QUEUE_STORE_LINK_STORE_ID" indexType="btree"> + <index referenceId="NEWSLETTER_QUEUE_STORE_LINK_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -150,19 +150,19 @@ <column xsi:type="int" name="problem_error_code" padding="10" unsigned="true" nullable="true" identity="false" default="0" comment="Problem Error Code"/> <column xsi:type="varchar" name="problem_error_text" nullable="true" length="200" comment="Problem Error Text"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="problem_id"/> </constraint> - <constraint xsi:type="foreign" name="NEWSLETTER_PROBLEM_QUEUE_ID_NEWSLETTER_QUEUE_QUEUE_ID" + <constraint xsi:type="foreign" referenceId="NEWSLETTER_PROBLEM_QUEUE_ID_NEWSLETTER_QUEUE_QUEUE_ID" table="newsletter_problem" column="queue_id" referenceTable="newsletter_queue" referenceColumn="queue_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="NLTTR_PROBLEM_SUBSCRIBER_ID_NLTTR_SUBSCRIBER_SUBSCRIBER_ID" + <constraint xsi:type="foreign" referenceId="NLTTR_PROBLEM_SUBSCRIBER_ID_NLTTR_SUBSCRIBER_SUBSCRIBER_ID" table="newsletter_problem" column="subscriber_id" referenceTable="newsletter_subscriber" referenceColumn="subscriber_id" onDelete="CASCADE"/> - <index name="NEWSLETTER_PROBLEM_SUBSCRIBER_ID" indexType="btree"> + <index referenceId="NEWSLETTER_PROBLEM_SUBSCRIBER_ID" indexType="btree"> <column name="subscriber_id"/> </index> - <index name="NEWSLETTER_PROBLEM_QUEUE_ID" indexType="btree"> + <index referenceId="NEWSLETTER_PROBLEM_QUEUE_ID" indexType="btree"> <column name="queue_id"/> </index> </table> diff --git a/app/code/Magento/Newsletter/etc/frontend/events.xml b/app/code/Magento/Newsletter/etc/frontend/events.xml new file mode 100644 index 000000000000..6c46d562f516 --- /dev/null +++ b/app/code/Magento/Newsletter/etc/frontend/events.xml @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd"> + <event name="controller_action_predispatch_newsletter"> + <observer name="newsletter_enabled" instance="Magento\Newsletter\Observer\PredispatchNewsletterObserver" /> + </event> +</config> diff --git a/app/code/Magento/Newsletter/i18n/en_US.csv b/app/code/Magento/Newsletter/i18n/en_US.csv index c49fdc80da81..388b583f990b 100644 --- a/app/code/Magento/Newsletter/i18n/en_US.csv +++ b/app/code/Magento/Newsletter/i18n/en_US.csv @@ -67,8 +67,8 @@ Subscribers,Subscribers "Something went wrong while saving this template.","Something went wrong while saving this template." "Newsletter Subscription","Newsletter Subscription" "Something went wrong while saving your subscription.","Something went wrong while saving your subscription." -"We saved the subscription.","We saved the subscription." -"We removed the subscription.","We removed the subscription." +"We have saved your subscription.","We have saved your subscription." +"We have removed your newsletter subscription.","We have removed your newsletter subscription." "Your subscription has been confirmed.","Your subscription has been confirmed." "This is an invalid subscription confirmation code.","This is an invalid subscription confirmation code." "This is an invalid subscription ID.","This is an invalid subscription ID." @@ -76,7 +76,7 @@ Subscribers,Subscribers "Sorry, but the administrator denied subscription for guests. Please <a href=""%1"">register</a>.","Sorry, but the administrator denied subscription for guests. Please <a href=""%1"">register</a>." "Please enter a valid email address.","Please enter a valid email address." "This email address is already subscribed.","This email address is already subscribed." -"The confirmation request has been sent.","The confirmation request has been sent." +"A confirmation request has been sent.","A confirmation request has been sent." "Thank you for your subscription.","Thank you for your subscription." "There was a problem with the subscription: %1","There was a problem with the subscription: %1" "Something went wrong with the subscription.","Something went wrong with the subscription." @@ -151,3 +151,4 @@ Unconfirmed,Unconfirmed Store,Store "Store View","Store View" "Newsletter Subscriptions","Newsletter Subscriptions" +"We have updated your subscription.","We have updated your subscription." diff --git a/app/code/Magento/Newsletter/view/adminhtml/layout/newsletter_problem_block.xml b/app/code/Magento/Newsletter/view/adminhtml/layout/newsletter_problem_block.xml index 3eb7de194d24..838a9dbb41b4 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/layout/newsletter_problem_block.xml +++ b/app/code/Magento/Newsletter/view/adminhtml/layout/newsletter_problem_block.xml @@ -11,10 +11,11 @@ <block class="Magento\Backend\Block\Widget\Grid" name="adminhtml.newslettrer.problem.grid" as="grid"> <arguments> <argument name="id" xsi:type="string">problemGrid</argument> - <argument name="dataSource" xsi:type="object">Magento\Newsletter\Model\ResourceModel\Grid\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Newsletter\Model\ResourceModel\Grid\Collection</argument> <argument name="message_block_visibility" xsi:type="string">true</argument> <argument name="use_ajax" xsi:type="string">true</argument> <argument name="save_parameters_in_session" xsi:type="string">1</argument> + <argument name="grid_url" xsi:type="url" path="*/*/grid"/> </arguments> <block class="Magento\Backend\Block\Widget\Grid\ColumnSet" name="adminhtml.newslettrer.problem.grid.columnSet" as="grid.columnSet"> <arguments> diff --git a/app/code/Magento/Newsletter/view/adminhtml/layout/newsletter_queue_grid_block.xml b/app/code/Magento/Newsletter/view/adminhtml/layout/newsletter_queue_grid_block.xml index 3bfb52157bb9..8a2c891c68f8 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/layout/newsletter_queue_grid_block.xml +++ b/app/code/Magento/Newsletter/view/adminhtml/layout/newsletter_queue_grid_block.xml @@ -11,7 +11,7 @@ <block class="Magento\Backend\Block\Widget\Grid" name="adminhtml.newsletter.queue.grid" as="grid"> <arguments> <argument name="id" xsi:type="string">queueGrid</argument> - <argument name="dataSource" xsi:type="object">Magento\Newsletter\Model\ResourceModel\Queue\Grid\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Newsletter\Model\ResourceModel\Queue\Grid\Collection</argument> <argument name="default_sort" xsi:type="string">start_at</argument> <argument name="default_dir" xsi:type="string">DESC</argument> <argument name="save_parameters_in_session" xsi:type="string">1</argument> diff --git a/app/code/Magento/Newsletter/view/adminhtml/layout/newsletter_subscriber_block.xml b/app/code/Magento/Newsletter/view/adminhtml/layout/newsletter_subscriber_block.xml index 9de1807af18e..e8600c2d49d7 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/layout/newsletter_subscriber_block.xml +++ b/app/code/Magento/Newsletter/view/adminhtml/layout/newsletter_subscriber_block.xml @@ -11,7 +11,7 @@ <block class="Magento\Newsletter\Block\Adminhtml\Subscriber\Grid" name="adminhtml.newslettrer.subscriber.grid" as="grid"> <arguments> <argument name="id" xsi:type="string">subscriberGrid</argument> - <argument name="dataSource" xsi:type="object">Magento\Newsletter\Model\ResourceModel\Subscriber\Grid\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Newsletter\Model\ResourceModel\Subscriber\Grid\Collection</argument> <argument name="default_sort" xsi:type="string">subscriber_id</argument> <argument name="default_dir" xsi:type="string">desc</argument> <argument name="use_ajax" xsi:type="string">1</argument> diff --git a/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml b/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml index a64185ce6795..532ecde45607 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml +++ b/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml @@ -16,7 +16,16 @@ </div> <?php endif;?> </div> - <iframe name="preview_iframe" id="preview_iframe" frameborder="0" title="<?= $block->escapeHtmlAttr(__('Preview')) ?>" width="100%"></iframe> + <iframe + name="preview_iframe" + id="preview_iframe" + frameborder="0" + title="<?= $block->escapeHtmlAttr(__('Preview')) ?>" + width="100%" + sandbox="allow-forms allow-pointer-lock allow-scripts" + > + + </iframe> <?= $block->getChildHtml('preview_form') ?> </div> diff --git a/app/code/Magento/Newsletter/view/adminhtml/templates/template/edit.phtml b/app/code/Magento/Newsletter/view/adminhtml/templates/template/edit.phtml index 7355c06bdd65..279afe2c28e7 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/templates/template/edit.phtml +++ b/app/code/Magento/Newsletter/view/adminhtml/templates/template/edit.phtml @@ -19,8 +19,7 @@ use Magento\Framework\App\TemplateTypesInterface; </div> <?= /* @noEscape */ $block->getForm() ?> </form> -<form action="<?= $block->escapeUrl($block->getPreviewUrl()) ?>" method="post" id="newsletter_template_preview_form" target="_blank"> - <?= $block->getBlockHtml('formkey') ?> +<form action="<?= $block->escapeUrl($block->getPreviewUrl()) ?>" method="get" id="newsletter_template_preview_form" target="_blank"> <div class="no-display"> <input type="hidden" id="preview_type" name="type" value="<?= /* @noEscape */ $block->isTextType() ? 1 : 2 ?>" /> <input type="hidden" id="preview_text" name="text" value="" /> diff --git a/app/code/Magento/Newsletter/view/frontend/layout/customer_account.xml b/app/code/Magento/Newsletter/view/frontend/layout/customer_account.xml index 99190bb35fcf..fd55fce8ee01 100644 --- a/app/code/Magento/Newsletter/view/frontend/layout/customer_account.xml +++ b/app/code/Magento/Newsletter/view/frontend/layout/customer_account.xml @@ -8,7 +8,7 @@ <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceBlock name="customer_account_navigation"> - <block class="Magento\Customer\Block\Account\SortLinkInterface" name="customer-account-navigation-newsletter-subscriptions-link"> + <block class="Magento\Customer\Block\Account\SortLinkInterface" ifconfig="newsletter/general/active" name="customer-account-navigation-newsletter-subscriptions-link"> <arguments> <argument name="path" xsi:type="string">newsletter/manage</argument> <argument name="label" xsi:type="string" translate="true">Newsletter Subscriptions</argument> diff --git a/app/code/Magento/Newsletter/view/frontend/layout/default.xml b/app/code/Magento/Newsletter/view/frontend/layout/default.xml index d84f4894a9d2..32a08359333c 100644 --- a/app/code/Magento/Newsletter/view/frontend/layout/default.xml +++ b/app/code/Magento/Newsletter/view/frontend/layout/default.xml @@ -8,10 +8,10 @@ <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceBlock name="head.components"> - <block class="Magento\Framework\View\Element\Js\Components" name="newsletter_head_components" template="Magento_Newsletter::js/components.phtml"/> + <block class="Magento\Framework\View\Element\Js\Components" name="newsletter_head_components" template="Magento_Newsletter::js/components.phtml" ifconfig="newsletter/general/active"/> </referenceBlock> <referenceContainer name="footer"> - <block class="Magento\Newsletter\Block\Subscribe" name="form.subscribe" as="subscribe" before="-" template="Magento_Newsletter::subscribe.phtml"/> + <block class="Magento\Newsletter\Block\Subscribe" name="form.subscribe" as="subscribe" before="-" template="Magento_Newsletter::subscribe.phtml" ifconfig="newsletter/general/active"/> </referenceContainer> </body> </page> diff --git a/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml b/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml index c1a526ee9514..554437095f36 100644 --- a/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml +++ b/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml @@ -19,16 +19,24 @@ data-mage-init='{"validation": {"errorClass": "mage-error"}}' id="newsletter-validate-detail"> <div class="field newsletter"> - <label class="label" for="newsletter"><span><?= $block->escapeHtml(__('Sign Up for Our Newsletter:')) ?></span></label> <div class="control"> - <input name="email" type="email" id="newsletter" - placeholder="<?= $block->escapeHtml(__('Enter your email address')) ?>" - data-mage-init='{"mage/trim-input":{}}' - data-validate="{required:true, 'validate-email':true}"/> + <label for="newsletter"> + <span class="label"> + <?= $block->escapeHtml(__('Sign Up for Our Newsletter:')) ?> + </span> + <input name="email" type="email" id="newsletter" + placeholder="<?= $block->escapeHtml(__('Enter your email address')) ?>" + data-mage-init='{"mage/trim-input":{}}' + data-validate="{required:true, 'validate-email':true}" + /> + </label> </div> </div> <div class="actions"> - <button class="action subscribe primary" title="<?= $block->escapeHtmlAttr(__('Subscribe')) ?>" type="submit"> + <button class="action subscribe primary sr-only" + title="<?= $block->escapeHtmlAttr(__('Subscribe')) ?>" + type="submit" + aria-label="Subscribe"> <span><?= $block->escapeHtml(__('Subscribe')) ?></span> </button> </div> diff --git a/app/code/Magento/OfflinePayments/Model/Purchaseorder.php b/app/code/Magento/OfflinePayments/Model/Purchaseorder.php index 2e0e58060c75..464142df5b99 100644 --- a/app/code/Magento/OfflinePayments/Model/Purchaseorder.php +++ b/app/code/Magento/OfflinePayments/Model/Purchaseorder.php @@ -5,6 +5,8 @@ */ namespace Magento\OfflinePayments\Model; +use Magento\Framework\Exception\LocalizedException; + /** * Class Purchaseorder * @@ -46,11 +48,29 @@ class Purchaseorder extends \Magento\Payment\Model\Method\AbstractMethod * * @param \Magento\Framework\DataObject|mixed $data * @return $this - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function assignData(\Magento\Framework\DataObject $data) { $this->getInfoInstance()->setPoNumber($data->getPoNumber()); return $this; } + + /** + * Validate payment method information object + * + * @return $this + * @throws LocalizedException + * @api + */ + public function validate() + { + parent::validate(); + + if (empty($this->getInfoInstance()->getPoNumber())) { + throw new LocalizedException(__('Purchase order number is a required field.')); + } + + return $this; + } } diff --git a/app/code/Magento/OfflinePayments/Test/Mftf/Data/ConfigData.xml b/app/code/Magento/OfflinePayments/Test/Mftf/Data/ConfigData.xml new file mode 100644 index 000000000000..ec8dd46a00d8 --- /dev/null +++ b/app/code/Magento/OfflinePayments/Test/Mftf/Data/ConfigData.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="DisableCheckMoneyOrderPaymentMethod"> + <data key="path">payment/checkmo/active</data> + <data key="label">No</data> + <data key="value">0</data> + </entity> + <entity name="EnableCheckMoneyOrderPaymentMethod"> + <!-- Magento default value --> + <data key="path">payment/checkmo/active</data> + <data key="label">Yes</data> + <data key="value">1</data> + </entity> + <entity name="DisableCashOnDeliveryPaymentMethod"> + <!-- Magento default value --> + <data key="path">payment/cashondelivery/active</data> + <data key="label">No</data> + <data key="value">0</data> + </entity> + <entity name="EnableCashOnDeliveryPaymentMethod"> + <data key="path">payment/cashondelivery/active</data> + <data key="label">Yes</data> + <data key="value">1</data> + </entity> +</entities> diff --git a/app/code/Magento/OfflinePayments/Test/Unit/Model/PurchaseorderTest.php b/app/code/Magento/OfflinePayments/Test/Unit/Model/PurchaseorderTest.php index 548e1d5fb187..2eb204651fcf 100644 --- a/app/code/Magento/OfflinePayments/Test/Unit/Model/PurchaseorderTest.php +++ b/app/code/Magento/OfflinePayments/Test/Unit/Model/PurchaseorderTest.php @@ -5,10 +5,21 @@ */ namespace Magento\OfflinePayments\Test\Unit\Model; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\DataObject; +use Magento\Framework\Event\ManagerInterface as EventManagerInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\OfflinePayments\Model\Purchaseorder; +use Magento\Payment\Helper\Data as PaymentHelper; +use Magento\Payment\Model\Info as PaymentInfo; +use Magento\Sales\Api\Data\OrderAddressInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Model\Order\Payment; + class PurchaseorderTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\OfflinePayments\Model\Purchaseorder + * @var Purchaseorder */ protected $_object; @@ -19,15 +30,15 @@ class PurchaseorderTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $eventManager = $this->createMock(\Magento\Framework\Event\ManagerInterface::class); - $paymentDataMock = $this->createMock(\Magento\Payment\Helper\Data::class); + $objectManagerHelper = new ObjectManager($this); + $eventManager = $this->createMock(EventManagerInterface::class); + $paymentDataMock = $this->createMock(PaymentHelper::class); $this->_scopeConfig = $this->createPartialMock( - \Magento\Framework\App\Config\ScopeConfigInterface::class, + ScopeConfigInterface::class, ['getValue', 'isSetFlag'] ); $this->_object = $objectManagerHelper->getObject( - \Magento\OfflinePayments\Model\Purchaseorder::class, + Purchaseorder::class, [ 'eventManager' => $eventManager, 'paymentData' => $paymentDataMock, @@ -38,13 +49,37 @@ protected function setUp() public function testAssignData() { - $data = new \Magento\Framework\DataObject([ + $data = new DataObject([ 'po_number' => '12345' ]); - $instance = $this->createMock(\Magento\Payment\Model\Info::class); + $instance = $this->createMock(PaymentInfo::class); $this->_object->setData('info_instance', $instance); $result = $this->_object->assignData($data); $this->assertEquals($result, $this->_object); } + + /** + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Purchase order number is a required field. + */ + public function testValidate() + { + $data = new DataObject([]); + + $addressMock = $this->createMock(OrderAddressInterface::class); + $addressMock->expects($this->once())->method('getCountryId')->willReturn('UY'); + + $orderMock = $this->createMock(OrderInterface::class); + $orderMock->expects($this->once())->method('getBillingAddress')->willReturn($addressMock); + + $instance = $this->createMock(Payment::class); + + $instance->expects($this->once())->method('getOrder')->willReturn($orderMock); + + $this->_object->setData('info_instance', $instance); + $this->_object->assignData($data); + + $this->_object->validate(); + } } diff --git a/app/code/Magento/OfflinePayments/i18n/en_US.csv b/app/code/Magento/OfflinePayments/i18n/en_US.csv index 43a743b29e3d..5a180f7af494 100644 --- a/app/code/Magento/OfflinePayments/i18n/en_US.csv +++ b/app/code/Magento/OfflinePayments/i18n/en_US.csv @@ -11,6 +11,7 @@ Enabled,Enabled "New Order Status","New Order Status" "Sort Order","Sort Order" Title,Title +"Purchase order number is a required field.","Purchase order number is a required field." "Payment from Applicable Countries","Payment from Applicable Countries" "Payment from Specific Countries","Payment from Specific Countries" "Make Check Payable to","Make Check Payable to" 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 000000000000..4d63577319d5 --- /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 000000000000..4a6ea1c00b21 --- /dev/null +++ b/app/code/Magento/OfflinePayments/view/base/templates/info/pdf/purchaseorder.phtml @@ -0,0 +1,11 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +/** + * @var $block \Magento\OfflinePayments\Block\Info\Purchaseorder + */ +?> +<?= $block->escapeHtml(__('Purchase Order Number: %1', $block->getInfo()->getPoNumber())) ?> + {{pdf_row_separator}} diff --git a/app/code/Magento/OfflinePayments/view/frontend/layout/multishipping_checkout_billing.xml b/app/code/Magento/OfflinePayments/view/frontend/layout/multishipping_checkout_billing.xml new file mode 100644 index 000000000000..32810ecef20d --- /dev/null +++ b/app/code/Magento/OfflinePayments/view/frontend/layout/multishipping_checkout_billing.xml @@ -0,0 +1,19 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceBlock name="checkout_billing"> + <arguments> + <argument name="form_templates" xsi:type="array"> + <item name="checkmo" xsi:type="string">Magento_OfflinePayments::multishipping/checkmo_form.phtml</item> + </argument> + </arguments> + </referenceBlock> + </body> +</page> diff --git a/app/code/Magento/OfflinePayments/view/frontend/templates/multishipping/checkmo_form.phtml b/app/code/Magento/OfflinePayments/view/frontend/templates/multishipping/checkmo_form.phtml new file mode 100644 index 000000000000..b96918243a7a --- /dev/null +++ b/app/code/Magento/OfflinePayments/view/frontend/templates/multishipping/checkmo_form.phtml @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +?> +<script> + require([ + 'uiLayout', + 'jquery' + ], function (layout, $) { + $(function () { + var paymentMethodData = { + method: 'checkmo' + }; + layout([ + { + component: 'Magento_Checkout/js/view/payment/default', + name: 'payment_method_checkmo', + method: paymentMethodData.method, + item: paymentMethodData + } + ]); + + $('body').trigger('contentUpdated'); + }) + }) +</script> diff --git a/app/code/Magento/OfflineShipping/Model/Carrier/Freeshipping.php b/app/code/Magento/OfflineShipping/Model/Carrier/Freeshipping.php index b546237b8256..c2e6d0e92231 100644 --- a/app/code/Magento/OfflineShipping/Model/Carrier/Freeshipping.php +++ b/app/code/Magento/OfflineShipping/Model/Carrier/Freeshipping.php @@ -80,7 +80,7 @@ public function collectRates(RateRequest $request) $this->_updateFreeMethodQuote($request); - if ($request->getFreeShipping() || $request->getBaseSubtotalInclTax() >= $this->getConfigData( + if ($request->getFreeShipping() || $request->getPackageValueWithDiscount() >= $this->getConfigData( 'free_shipping_subtotal' ) ) { @@ -97,8 +97,18 @@ public function collectRates(RateRequest $request) $method->setCost('0.00'); $result->append($method); + } elseif ($this->getConfigData('showmethod')) { + $error = $this->_rateErrorFactory->create(); + $error->setCarrier($this->_code); + $error->setCarrierTitle($this->getConfigData('title')); + $errorMsg = $this->getConfigData('specificerrmsg'); + $error->setErrorMessage( + $errorMsg ? $errorMsg : __( + 'Sorry, but we can\'t deliver to the destination country with this shipping module.' + ) + ); + return $error; } - return $result; } @@ -128,6 +138,8 @@ protected function _updateFreeMethodQuote($request) } /** + * Returns allowed shipping methods + * * @return array */ public function getAllowedMethods() diff --git a/app/code/Magento/OfflineShipping/Model/Carrier/Tablerate.php b/app/code/Magento/OfflineShipping/Model/Carrier/Tablerate.php index 2c4fc9a4ccfe..373d64afc8cc 100644 --- a/app/code/Magento/OfflineShipping/Model/Carrier/Tablerate.php +++ b/app/code/Magento/OfflineShipping/Model/Carrier/Tablerate.php @@ -61,6 +61,7 @@ class Tablerate extends \Magento\Shipping\Model\Carrier\AbstractCarrier implemen * @param \Magento\Quote\Model\Quote\Address\RateResult\MethodFactory $resultMethodFactory * @param \Magento\OfflineShipping\Model\ResourceModel\Carrier\TablerateFactory $tablerateFactory * @param array $data + * @throws LocalizedException * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ public function __construct( @@ -82,6 +83,8 @@ public function __construct( } /** + * Collect rates. + * * @param RateRequest $request * @return \Magento\Shipping\Model\Rate\Result * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -129,8 +132,10 @@ public function collectRates(RateRequest $request) $freeQty += $item->getQty() * ($child->getQty() - $freeShipping); } } - } elseif ($item->getFreeShipping()) { - $freeShipping = is_numeric($item->getFreeShipping()) ? $item->getFreeShipping() : 0; + } elseif ($item->getFreeShipping() || $item->getAddress()->getFreeShipping()) { + $freeShipping = $item->getFreeShipping() ? + $item->getFreeShipping() : $item->getAddress()->getFreeShipping(); + $freeShipping = is_numeric($freeShipping) ? $freeShipping : 0; $freeQty += $item->getQty() - $freeShipping; $freePackageValue += $item->getBaseRowTotal(); } @@ -199,6 +204,8 @@ public function collectRates(RateRequest $request) } /** + * Get rate. + * * @param \Magento\Quote\Model\Quote\Address\RateRequest $request * @return array|bool */ @@ -208,6 +215,8 @@ public function getRate(\Magento\Quote\Model\Quote\Address\RateRequest $request) } /** + * Get code. + * * @param string $type * @param string $code * @return array @@ -218,12 +227,12 @@ public function getCode($type, $code = '') $codes = [ 'condition_name' => [ 'package_weight' => __('Weight vs. Destination'), - 'package_value' => __('Price vs. Destination'), + 'package_value_with_discount' => __('Price vs. Destination'), 'package_qty' => __('# of Items vs. Destination'), ], 'condition_name_short' => [ 'package_weight' => __('Weight (and above)'), - 'package_value' => __('Order Subtotal (and above)'), + 'package_value_with_discount' => __('Order Subtotal (and above)'), 'package_qty' => __('# of Items (and above)'), ], ]; diff --git a/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/CSV/RowParser.php b/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/CSV/RowParser.php index 9431973fdfe9..f7b487d37bfc 100644 --- a/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/CSV/RowParser.php +++ b/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/CSV/RowParser.php @@ -9,6 +9,9 @@ use Magento\Framework\Phrase; use Magento\OfflineShipping\Model\ResourceModel\Carrier\Tablerate\LocationDirectory; +/** + * Row parser. + */ class RowParser { /** @@ -26,6 +29,8 @@ public function __construct(LocationDirectory $locationDirectory) } /** + * Retrieve columns. + * * @return array */ public function getColumns() @@ -42,6 +47,8 @@ public function getColumns() } /** + * Parse provided row data. + * * @param array $rowData * @param int $rowNumber * @param int $websiteId @@ -71,23 +78,30 @@ public function parse( } $countryId = $this->getCountryId($rowData, $rowNumber, $columnResolver); - $regionId = $this->getRegionId($rowData, $rowNumber, $columnResolver, $countryId); + $regionIds = $this->getRegionIds($rowData, $rowNumber, $columnResolver, $countryId); $zipCode = $this->getZipCode($rowData, $columnResolver); $conditionValue = $this->getConditionValue($rowData, $rowNumber, $conditionFullName, $columnResolver); $price = $this->getPrice($rowData, $rowNumber, $columnResolver); - return [ - 'website_id' => $websiteId, - 'dest_country_id' => $countryId, - 'dest_region_id' => $regionId, - 'dest_zip' => $zipCode, - 'condition_name' => $conditionShortName, - 'condition_value' => $conditionValue, - 'price' => $price, - ]; + $rates = []; + foreach ($regionIds as $regionId) { + $rates[] = [ + 'website_id' => $websiteId, + 'dest_country_id' => $countryId, + 'dest_region_id' => $regionId, + 'dest_zip' => $zipCode, + 'condition_name' => $conditionShortName, + 'condition_value' => $conditionValue, + 'price' => $price, + ]; + } + + return $rates; } /** + * Get country id from provided row data. + * * @param array $rowData * @param int $rowNumber * @param ColumnResolver $columnResolver @@ -116,21 +130,23 @@ private function getCountryId(array $rowData, $rowNumber, ColumnResolver $column } /** + * Retrieve region id from provided row data. + * * @param array $rowData * @param int $rowNumber * @param ColumnResolver $columnResolver * @param int $countryId - * @return int|string + * @return array * @throws ColumnNotFoundException * @throws RowException */ - private function getRegionId(array $rowData, $rowNumber, ColumnResolver $columnResolver, $countryId) + private function getRegionIds(array $rowData, $rowNumber, ColumnResolver $columnResolver, $countryId) { $regionCode = $columnResolver->getColumnValue(ColumnResolver::COLUMN_REGION, $rowData); if ($countryId !== '0' && $this->locationDirectory->hasRegionId($countryId, $regionCode)) { - $regionId = $this->locationDirectory->getRegionId($countryId, $regionCode); + $regionIds = $this->locationDirectory->getRegionIds($countryId, $regionCode); } elseif ($regionCode === '*' || $regionCode === '') { - $regionId = 0; + $regionIds = [0]; } else { throw new RowException( __( @@ -141,10 +157,12 @@ private function getRegionId(array $rowData, $rowNumber, ColumnResolver $columnR ) ); } - return $regionId; + return $regionIds; } /** + * Retrieve zip code from provided row data. + * * @param array $rowData * @param ColumnResolver $columnResolver * @return float|int|null|string @@ -160,6 +178,8 @@ private function getZipCode(array $rowData, ColumnResolver $columnResolver) } /** + * Get condition value form provided row data. + * * @param array $rowData * @param int $rowNumber * @param string $conditionFullName @@ -187,6 +207,8 @@ private function getConditionValue(array $rowData, $rowNumber, $conditionFullNam } /** + * Retrieve price from provided row data. + * * @param array $rowData * @param int $rowNumber * @param ColumnResolver $columnResolver @@ -212,6 +234,7 @@ private function getPrice(array $rowData, $rowNumber, ColumnResolver $columnReso /** * Parse and validate positive decimal value + * * Return false if value is not decimal or is not positive * * @param string $value diff --git a/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/Import.php b/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/Import.php index a5b0d7e87d2e..7735f8ce8999 100644 --- a/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/Import.php +++ b/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/Import.php @@ -17,6 +17,7 @@ use Magento\Store\Model\StoreManagerInterface; /** + * Import offline shipping. * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Import @@ -87,6 +88,8 @@ public function __construct( } /** + * Check if there are errors. + * * @return bool */ public function hasErrors() @@ -95,6 +98,8 @@ public function hasErrors() } /** + * Get errors. + * * @return array */ public function getErrors() @@ -103,6 +108,8 @@ public function getErrors() } /** + * Retrieve columns. + * * @return array */ public function getColumns() @@ -111,6 +118,8 @@ public function getColumns() } /** + * Get data from file. + * * @param ReadInterface $file * @param int $websiteId * @param string $conditionShortName @@ -135,7 +144,7 @@ public function getData(ReadInterface $file, $websiteId, $conditionShortName, $c if (empty($csvLine)) { continue; } - $rowData = $this->rowParser->parse( + $rowsData = $this->rowParser->parse( $csvLine, $rowNumber, $websiteId, @@ -144,20 +153,25 @@ public function getData(ReadInterface $file, $websiteId, $conditionShortName, $c $columnResolver ); - // protect from duplicate - $hash = $this->dataHashGenerator->getHash($rowData); - if (array_key_exists($hash, $this->uniqueHash)) { - throw new RowException( - __( - 'Duplicate Row #%1 (duplicates row #%2)', - $rowNumber, - $this->uniqueHash[$hash] - ) - ); + foreach ($rowsData as $rowData) { + // protect from duplicate + $hash = $this->dataHashGenerator->getHash($rowData); + if (array_key_exists($hash, $this->uniqueHash)) { + throw new RowException( + __( + 'Duplicate Row #%1 (duplicates row #%2)', + $rowNumber, + $this->uniqueHash[$hash] + ) + ); + } + $this->uniqueHash[$hash] = $rowNumber; + + $items[] = $rowData; + } + if (count($rowsData) > 1) { + $bunchSize += count($rowsData) - 1; } - $this->uniqueHash[$hash] = $rowNumber; - - $items[] = $rowData; if (count($items) === $bunchSize) { yield $items; $items = []; @@ -172,6 +186,8 @@ public function getData(ReadInterface $file, $websiteId, $conditionShortName, $c } /** + * Retrieve column headers. + * * @param ReadInterface $file * @return array|bool * @throws LocalizedException diff --git a/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/LocationDirectory.php b/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/LocationDirectory.php index 1a311f3658a0..e015f7b54637 100644 --- a/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/LocationDirectory.php +++ b/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/LocationDirectory.php @@ -6,6 +6,9 @@ namespace Magento\OfflineShipping\Model\ResourceModel\Carrier\Tablerate; +/** + * Location directory. + */ class LocationDirectory { /** @@ -13,6 +16,11 @@ class LocationDirectory */ protected $regions; + /** + * @var array + */ + private $regionsByCode; + /** * @var array */ @@ -47,6 +55,8 @@ public function __construct( } /** + * Retrieve country id. + * * @param string $countryCode * @return null|string */ @@ -88,6 +98,8 @@ protected function loadCountries() } /** + * Check if there is country id with provided country code. + * * @param string $countryCode * @return bool */ @@ -98,6 +110,8 @@ public function hasCountryId($countryCode) } /** + * Check if there is region id with provided region code and country id. + * * @param string $countryId * @param string $regionCode * @return bool @@ -115,29 +129,50 @@ public function hasRegionId($countryId, $regionCode) */ protected function loadRegions() { - if ($this->regions !== null) { + if ($this->regions !== null && $this->regionsByCode !== null) { return $this; } $this->regions = []; + $this->regionsByCode = []; /** @var $collection \Magento\Directory\Model\ResourceModel\Region\Collection */ $collection = $this->_regionCollectionFactory->create(); foreach ($collection->getData() as $row) { $this->regions[$row['country_id']][$row['code']] = (int)$row['region_id']; + if (empty($this->regionsByCode[$row['country_id']][$row['code']])) { + $this->regionsByCode[$row['country_id']][$row['code']] = []; + } + $this->regionsByCode[$row['country_id']][$row['code']][] = (int)$row['region_id']; } return $this; } /** + * Retrieve region id. + * * @param int $countryId * @param string $regionCode * @return string + * @deprecated */ public function getRegionId($countryId, $regionCode) { $this->loadRegions(); return $this->regions[$countryId][$regionCode]; } + + /** + * Return region ids for country and region + * + * @param int $countryId + * @param string $regionCode + * @return array + */ + public function getRegionIds($countryId, $regionCode) + { + $this->loadRegions(); + return $this->regionsByCode[$countryId][$regionCode]; + } } diff --git a/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/RateQuery.php b/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/RateQuery.php index 5b03ef0cb02b..f7105b8e5476 100644 --- a/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/RateQuery.php +++ b/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/RateQuery.php @@ -6,6 +6,9 @@ namespace Magento\OfflineShipping\Model\ResourceModel\Carrier\Tablerate; +/** + * Query builder for table rate + */ class RateQuery { /** @@ -24,6 +27,8 @@ public function __construct( } /** + * Prepare select + * * @param \Magento\Framework\DB\Select $select * @return \Magento\Framework\DB\Select */ @@ -42,6 +47,7 @@ public function prepareSelect(\Magento\Framework\DB\Select $select) ') OR (', [ "dest_country_id = :country_id AND dest_region_id = :region_id AND dest_zip = :postcode", + "dest_country_id = :country_id AND dest_region_id = :region_id AND dest_zip = :postcode_prefix", "dest_country_id = :country_id AND dest_region_id = :region_id AND dest_zip = ''", // Handle asterisk in dest_zip field @@ -51,7 +57,7 @@ public function prepareSelect(\Magento\Framework\DB\Select $select) "dest_country_id = '0' AND dest_region_id = 0 AND dest_zip = '*'", "dest_country_id = :country_id AND dest_region_id = 0 AND dest_zip = ''", "dest_country_id = :country_id AND dest_region_id = 0 AND dest_zip = :postcode", - "dest_country_id = :country_id AND dest_region_id = 0 AND dest_zip = '*'" + "dest_country_id = :country_id AND dest_region_id = 0 AND dest_zip = :postcode_prefix" ] ) . ')'; $select->where($orWhere); @@ -76,6 +82,8 @@ public function prepareSelect(\Magento\Framework\DB\Select $select) } /** + * Returns query bindings + * * @return array */ public function getBindings() @@ -85,6 +93,7 @@ public function getBindings() ':country_id' => $this->request->getDestCountryId(), ':region_id' => (int)$this->request->getDestRegionId(), ':postcode' => $this->request->getDestPostcode(), + ':postcode_prefix' => $this->getDestPostcodePrefix() ]; // Render condition by condition name @@ -106,10 +115,26 @@ public function getBindings() } /** + * Returns rate request + * * @return \Magento\Quote\Model\Quote\Address\RateRequest */ public function getRequest() { return $this->request; } + + /** + * Returns the entire postcode if it contains no dash or the part of it prior to the dash in the other case + * + * @return string + */ + private function getDestPostcodePrefix() + { + if (!preg_match("/^(.+)-(.+)$/", $this->request->getDestPostcode(), $zipParts)) { + return $this->request->getDestPostcode(); + } + + return $zipParts[1]; + } } diff --git a/app/code/Magento/OfflineShipping/Setup/Patch/Data/UpdateShippingTablerate.php b/app/code/Magento/OfflineShipping/Setup/Patch/Data/UpdateShippingTablerate.php new file mode 100644 index 000000000000..070105846fdd --- /dev/null +++ b/app/code/Magento/OfflineShipping/Setup/Patch/Data/UpdateShippingTablerate.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\OfflineShipping\Setup\Patch\Data; + +use Magento\Framework\Setup\Patch\DataPatchInterface; +use Magento\OfflineShipping\Model\Carrier\Tablerate; + +/** + * Update for shipping_tablerate table for using price with discount in condition. + */ +class UpdateShippingTablerate implements DataPatchInterface +{ + /** + * @var \Magento\Framework\Setup\ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * PatchInitial constructor. + * @param \Magento\Framework\Setup\ModuleDataSetupInterface $moduleDataSetup + */ + public function __construct( + \Magento\Framework\Setup\ModuleDataSetupInterface $moduleDataSetup + ) { + $this->moduleDataSetup = $moduleDataSetup; + } + + /** + * @inheritdoc + */ + public function apply() + { + $this->moduleDataSetup->getConnection()->startSetup(); + $connection = $this->moduleDataSetup->getConnection(); + $connection->update( + $this->moduleDataSetup->getTable('shipping_tablerate'), + ['condition_name' => 'package_value_with_discount'], + [new \Zend_Db_Expr('condition_name = \'package_value\'')] + ); + $connection->update( + $this->moduleDataSetup->getTable('core_config_data'), + ['value' => 'package_value_with_discount'], + [ + new \Zend_Db_Expr('value = \'package_value\''), + new \Zend_Db_Expr('path = \'carriers/tablerate/condition_name\'') + ] + ); + $this->moduleDataSetup->getConnection()->endSetup(); + + $connection->endSetup(); + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return []; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/OfflineShipping/Test/Unit/Block/Adminhtml/Form/Field/ImportTest.php b/app/code/Magento/OfflineShipping/Test/Unit/Block/Adminhtml/Form/Field/ImportTest.php index 8d75cc32914b..a1fb2e449d7b 100644 --- a/app/code/Magento/OfflineShipping/Test/Unit/Block/Adminhtml/Form/Field/ImportTest.php +++ b/app/code/Magento/OfflineShipping/Test/Unit/Block/Adminhtml/Form/Field/ImportTest.php @@ -33,7 +33,10 @@ protected function setUp() $testHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->_object = $testHelper->getObject( \Magento\OfflineShipping\Block\Adminhtml\Form\Field\Import::class, - ['data' => $testData] + [ + 'data' => $testData, + '_escaper' => $testHelper->getObject(\Magento\Framework\Escaper::class) + ] ); $this->_object->setForm($this->_formMock); } diff --git a/app/code/Magento/OfflineShipping/Test/Unit/Model/ResourceModel/Carrier/Tablerate/CSV/RowParserTest.php b/app/code/Magento/OfflineShipping/Test/Unit/Model/ResourceModel/Carrier/Tablerate/CSV/RowParserTest.php index 8c34e9a0d651..683790c53126 100644 --- a/app/code/Magento/OfflineShipping/Test/Unit/Model/ResourceModel/Carrier/Tablerate/CSV/RowParserTest.php +++ b/app/code/Magento/OfflineShipping/Test/Unit/Model/ResourceModel/Carrier/Tablerate/CSV/RowParserTest.php @@ -37,7 +37,7 @@ class RowParserTest extends \PHPUnit\Framework\TestCase protected function setUp() { $this->locationDirectoryMock = $this->getMockBuilder(LocationDirectory::class) - ->setMethods(['hasCountryId', 'getCountryId', 'hasRegionId', 'getRegionId']) + ->setMethods(['hasCountryId', 'getCountryId', 'hasRegionId', 'getRegionIds']) ->disableOriginalConstructor() ->getMock(); $this->columnResolverMock = $this->getMockBuilder(ColumnResolver::class) @@ -92,7 +92,7 @@ public function testParse() $conditionShortName, $columnValueMap ); - $this->assertEquals($expectedResult, $result); + $this->assertEquals([$expectedResult], $result); } /** diff --git a/app/code/Magento/OfflineShipping/Test/Unit/Model/ResourceModel/Carrier/Tablerate/ImportTest.php b/app/code/Magento/OfflineShipping/Test/Unit/Model/ResourceModel/Carrier/Tablerate/ImportTest.php index 4e433c380f75..722683decb4c 100644 --- a/app/code/Magento/OfflineShipping/Test/Unit/Model/ResourceModel/Carrier/Tablerate/ImportTest.php +++ b/app/code/Magento/OfflineShipping/Test/Unit/Model/ResourceModel/Carrier/Tablerate/ImportTest.php @@ -77,9 +77,6 @@ protected function setUp() ->getMock(); $this->dataHashGeneratorMock = $this->getMockBuilder(DataHashGenerator::class) ->getMock(); - $this->rowParserMock->expects($this->any()) - ->method('parse') - ->willReturnArgument(0); $this->dataHashGeneratorMock->expects($this->any()) ->method('getHash') ->willReturnCallback( @@ -124,6 +121,15 @@ public function testGetData() ['a4', 'b4', 'c4', 'd4', 'e4'], ['a5', 'b5', 'c5', 'd5', 'e5'], ]; + $this->rowParserMock->expects($this->any()) + ->method('parse') + ->willReturn( + [['a1', 'b1', 'c1', 'd1', 'e1']], + [['a2', 'b2', 'c2', 'd2', 'e2']], + [['a3', 'b3', 'c3', 'd3', 'e3']], + [['a4', 'b4', 'c4', 'd4', 'e4']], + [['a5', 'b5', 'c5', 'd5', 'e5']] + ); $file = $this->createFileMock($lines); $expectedResult = [ [ @@ -167,6 +173,13 @@ public function testGetDataWithDuplicatedLine() [], ['a2', 'b2', 'c2', 'd2', 'e2'], ]; + $this->rowParserMock->expects($this->any()) + ->method('parse') + ->willReturn( + [['a1', 'b1', 'c1', 'd1', 'e1']], + [['a1', 'b1', 'c1', 'd1', 'e1']], + [['a2', 'b2', 'c2', 'd2', 'e2']] + ); $file = $this->createFileMock($lines); $expectedResult = [ [ diff --git a/app/code/Magento/OfflineShipping/etc/db_schema.xml b/app/code/Magento/OfflineShipping/etc/db_schema.xml index 80f4b56a1723..5129e8a29b2a 100644 --- a/app/code/Magento/OfflineShipping/etc/db_schema.xml +++ b/app/code/Magento/OfflineShipping/etc/db_schema.xml @@ -18,17 +18,17 @@ default="0" comment="Destination Region Id"/> <column xsi:type="varchar" name="dest_zip" nullable="false" length="10" default="*" comment="Destination Post Code (Zip)"/> - <column xsi:type="varchar" name="condition_name" nullable="false" length="20" comment="Rate Condition name"/> + <column xsi:type="varchar" name="condition_name" nullable="false" length="30" comment="Rate Condition name"/> <column xsi:type="decimal" name="condition_value" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Rate condition value"/> <column xsi:type="decimal" name="price" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Price"/> <column xsi:type="decimal" name="cost" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Cost"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="pk"/> </constraint> - <constraint xsi:type="unique" name="UNQ_D60821CDB2AFACEE1566CFC02D0D4CAA"> + <constraint xsi:type="unique" referenceId="UNQ_D60821CDB2AFACEE1566CFC02D0D4CAA"> <column name="website_id"/> <column name="dest_country_id"/> <column name="dest_region_id"/> diff --git a/app/code/Magento/PageCache/Model/Cache/Server.php b/app/code/Magento/PageCache/Model/Cache/Server.php index 349e9faffa67..7f3a4af969d7 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 new file mode 100644 index 000000000000..7c9391ba2218 --- /dev/null +++ b/app/code/Magento/PageCache/Model/System/Config/Backend/AccessList.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\PageCache\Model\System\Config\Backend; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Phrase; + +/** + * Access List config field. + */ +class AccessList extends Varnish +{ + /** + * @inheritDoc + */ + public function beforeSave() + { + parent::beforeSave(); + + $value = $this->getValue(); + if (!is_string($value) || !preg_match('/^[\w\s\.\-\,\:]+$/', $value)) { + throw new LocalizedException( + new Phrase( + 'Access List value "%1" is not valid. ' + . 'Please use only IP addresses and host names.', + [$value] + ) + ); + } + } +} diff --git a/app/code/Magento/PageCache/Model/System/Config/Backend/Ttl.php b/app/code/Magento/PageCache/Model/System/Config/Backend/Ttl.php index dab8d7be16dd..d9d26d0848c2 100644 --- a/app/code/Magento/PageCache/Model/System/Config/Backend/Ttl.php +++ b/app/code/Magento/PageCache/Model/System/Config/Backend/Ttl.php @@ -6,6 +6,10 @@ namespace Magento\PageCache\Model\System\Config\Backend; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Escaper; +use Magento\Framework\App\Config\ScopeConfigInterface; + /** * Backend model for processing Public content cache lifetime settings * @@ -13,6 +17,36 @@ */ class Ttl extends \Magento\Framework\App\Config\Value { + /** + * @var Escaper + */ + private $escaper; + + /** + * Ttl constructor. + * @param \Magento\Framework\Model\Context $context + * @param \Magento\Framework\Registry $registry + * @param ScopeConfigInterface $config + * @param \Magento\Framework\App\Cache\TypeListInterface $cacheTypeList + * @param \Magento\Framework\Model\ResourceModel\AbstractResource|null $resource + * @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection + * @param array $data + * @param Escaper|null $escaper + */ + public function __construct( + \Magento\Framework\Model\Context $context, + \Magento\Framework\Registry $registry, + ScopeConfigInterface $config, + \Magento\Framework\App\Cache\TypeListInterface $cacheTypeList, + ?\Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, + ?\Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, + array $data = [], + ?Escaper $escaper = null + ) { + parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data); + $this->escaper = $escaper ?: ObjectManager::getInstance()->create(Escaper::class); + } + /** * Throw exception if Ttl data is invalid or empty * @@ -24,7 +58,10 @@ public function beforeSave() $value = $this->getValue(); if ($value < 0 || !preg_match('/^[0-9]+$/', $value)) { throw new \Magento\Framework\Exception\LocalizedException( - __('Ttl value "%1" is not valid. Please use only numbers equal or greater than zero.', $value) + __( + 'Ttl value "%1" is not valid. Please use only numbers equal or greater than zero.', + $this->escaper->escapeHtml($value) + ) ); } return $this; diff --git a/app/code/Magento/PageCache/Model/Varnish/VclGenerator.php b/app/code/Magento/PageCache/Model/Varnish/VclGenerator.php index cf5a703142c8..a50fa090de2d 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 000000000000..7017da27eee9 --- /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 000000000000..e4cadf728f2e --- /dev/null +++ b/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance/PageCacheState.php @@ -0,0 +1,74 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\PageCache\Observer\SwitchPageCacheOnMaintenance; + +use Magento\Framework\Filesystem; +use Magento\Framework\App\Filesystem\DirectoryList; + +/** + * Page Cache state. + */ +class PageCacheState +{ + /** + * Full Page Cache Off state file name. + */ + private const PAGE_CACHE_STATE_FILENAME = '.maintenance.fpc.state'; + + /** + * @var Filesystem\Directory\WriteInterface + */ + private $flagDir; + + /** + * @param Filesystem $fileSystem + */ + public function __construct(Filesystem $fileSystem) + { + $this->flagDir = $fileSystem->getDirectoryWrite(DirectoryList::VAR_DIR); + } + + /** + * Saves Full Page Cache state. + * + * Saves FPC state across requests. + * + * @param bool $state + * @return void + */ + public function save(bool $state): void + { + $this->flagDir->writeFile(self::PAGE_CACHE_STATE_FILENAME, (string)$state); + } + + /** + * Returns stored Full Page Cache state. + * + * @return bool + */ + public function isEnabled(): bool + { + if (!$this->flagDir->isExist(self::PAGE_CACHE_STATE_FILENAME)) { + return false; + } + + return (bool)$this->flagDir->readFile(self::PAGE_CACHE_STATE_FILENAME); + } + + /** + * Flushes Page Cache state storage. + * + * @return void + */ + public function flush(): void + { + $this->flagDir->delete(self::PAGE_CACHE_STATE_FILENAME); + } +} diff --git a/app/code/Magento/PageCache/Test/Mftf/Section/AdminCacheManagementSection.xml b/app/code/Magento/PageCache/Test/Mftf/Section/AdminCacheManagementSection.xml index 34a77095d524..ee0c32633569 100644 --- a/app/code/Magento/PageCache/Test/Mftf/Section/AdminCacheManagementSection.xml +++ b/app/code/Magento/PageCache/Test/Mftf/Section/AdminCacheManagementSection.xml @@ -30,5 +30,6 @@ <element name="pageCacheCheckbox" type="checkbox" selector="input[value='full_page']"/> <element name="webServicesConfigCheckbox" type="checkbox" selector="input[value='config_webservice']"/> <element name="translationsCheckbox" type="checkbox" selector="input[value='translate']"/> + <element name="additionalCacheButton" type="button" selector="//*[@id='container']//button[contains(., '{{cacheType}}')]" parameterized="true" /> </section> </sections> diff --git a/app/code/Magento/PageCache/Test/Mftf/Test/FlushStaticFilesCacheButtonVisibilityTest.xml b/app/code/Magento/PageCache/Test/Mftf/Test/FlushStaticFilesCacheButtonVisibilityTest.xml new file mode 100644 index 000000000000..bd6f7ba362bf --- /dev/null +++ b/app/code/Magento/PageCache/Test/Mftf/Test/FlushStaticFilesCacheButtonVisibilityTest.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="FlushStaticFilesCacheButtonVisibilityTest"> + <annotations> + <features value="PageCache"/> + <stories value="Page Cache"/> + <title value="Check visibility of flush static files cache button"/> + <description value="Flush Static Files Cache button visibility"/> + <severity value="MAJOR"/> + <testCaseId value="MC-15454"/> + <group value="production_mode_only"/> + <group value="pagecache"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Log in to Admin Panel --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <!-- Open Cache Management page --> + <amOnPage url="{{AdminCacheManagementPage.url}}" stepKey="amOnPageCacheManagement"/> + <waitForPageLoad stepKey="waitForPageCacheManagementLoad"/> + + <!-- Check 'Flush Static Files Cache' not visible in production mode. --> + <dontSee selector="{{AdminCacheManagementSection.additionalCacheButton('Flush Static Files Cache')}}" stepKey="dontSeeFlushStaticFilesButton" /> + </test> +</tests> diff --git a/app/code/Magento/PageCache/Test/Mftf/Test/NewProductsListWidgetTest.xml b/app/code/Magento/PageCache/Test/Mftf/Test/NewProductsListWidgetTest.xml index c5871ddc3a37..a3c9e7b39217 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/Model/System/Config/Backend/AccessListTest.php b/app/code/Magento/PageCache/Test/Unit/Model/System/Config/Backend/AccessListTest.php new file mode 100644 index 000000000000..e84b412beb8a --- /dev/null +++ b/app/code/Magento/PageCache/Test/Unit/Model/System/Config/Backend/AccessListTest.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\PageCache\Test\Unit\Model\System\Config\Backend; + +use Magento\PageCache\Model\System\Config\Backend\AccessList; +use PHPUnit\Framework\TestCase; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\App\Config\ScopeConfigInterface; + +class AccessListTest extends TestCase +{ + /** + * @var AccessList + */ + private $accessList; + + /** + * @inheritDoc + */ + protected function setUp() + { + $objectManager = new ObjectManager($this); + $configMock = $this->getMockForAbstractClass( + ScopeConfigInterface::class + ); + $configMock->expects($this->any()) + ->method('getValue') + ->with('system/full_page_cache/default') + ->willReturn(['access_list' => 'localhost']); + $this->accessList = $objectManager->getObject( + AccessList::class, + [ + 'config' => $configMock, + 'data' => ['field' => 'access_list'] + ] + ); + } + + /** + * @return array + */ + public function getValidValues(): array + { + return [ + ['localhost', 'localhost'], + [null, 'localhost'], + ['127.0.0.1', '127.0.0.1'], + ['127.0.0.1, localhost, ::2', '127.0.0.1, localhost, ::2'], + ]; + } + + /** + * @param mixed $value + * @param mixed $expectedValue + * @dataProvider getValidValues + */ + public function testBeforeSave($value, $expectedValue) + { + $this->accessList->setValue($value); + $this->accessList->beforeSave(); + $this->assertEquals($expectedValue, $this->accessList->getValue()); + } + + /** + * @return array + */ + public function getInvalidValues(): array + { + return [ + ['\\bull val\\'], + ['{*I am not an IP*}'], + ['{*I am not an IP*}, 127.0.0.1'], + ]; + } + + /** + * @param mixed $value + * @expectedException \Magento\Framework\Exception\LocalizedException + * @dataProvider getInvalidValues + */ + public function testBeforeSaveInvalid($value) + { + $this->accessList->setValue($value); + $this->accessList->beforeSave(); + } +} 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 000000000000..2dbb815c7092 --- /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/adminhtml/system.xml b/app/code/Magento/PageCache/etc/adminhtml/system.xml index 5055b1795681..2a4439ac6a9c 100644 --- a/app/code/Magento/PageCache/etc/adminhtml/system.xml +++ b/app/code/Magento/PageCache/etc/adminhtml/system.xml @@ -20,7 +20,7 @@ <label>Access list</label> <comment>IPs access list separated with ',' that can purge Varnish configuration for config file generation. If field is empty default value localhost will be saved.</comment> - <backend_model>Magento\PageCache\Model\System\Config\Backend\Varnish</backend_model> + <backend_model>Magento\PageCache\Model\System\Config\Backend\AccessList</backend_model> <depends> <field id="caching_application">1</field> </depends> diff --git a/app/code/Magento/PageCache/etc/events.xml b/app/code/Magento/PageCache/etc/events.xml index 7584f5f36d69..3f0a2532ae60 100644 --- a/app/code/Magento/PageCache/etc/events.xml +++ b/app/code/Magento/PageCache/etc/events.xml @@ -57,4 +57,7 @@ <event name="customer_logout"> <observer name="FlushFormKey" instance="Magento\PageCache\Observer\FlushFormKey"/> </event> + <event name="maintenance_mode_changed"> + <observer name="page_cache_switcher_for_maintenance" instance="Magento\PageCache\Observer\SwitchPageCacheOnMaintenance"/> + </event> </config> diff --git a/app/code/Magento/PageCache/etc/varnish4.vcl b/app/code/Magento/PageCache/etc/varnish4.vcl index f6b7859af7c9..6de6b4e91704 100644 --- a/app/code/Magento/PageCache/etc/varnish4.vcl +++ b/app/code/Magento/PageCache/etc/varnish4.vcl @@ -86,15 +86,16 @@ sub vcl_recv { } elsif (req.http.Accept-Encoding ~ "deflate" && req.http.user-agent !~ "MSIE") { set req.http.Accept-Encoding = "deflate"; } else { - # unkown algorithm + # unknown algorithm unset req.http.Accept-Encoding; } } - # Remove Google gclid parameters to minimize the cache objects - set req.url = regsuball(req.url,"\?gclid=[^&]+$",""); # strips when QS = "?gclid=AAA" - set req.url = regsuball(req.url,"\?gclid=[^&]+&","?"); # strips when QS = "?gclid=AAA&foo=bar" - set req.url = regsuball(req.url,"&gclid=[^&]+",""); # strips when QS = "?foo=bar&gclid=AAA" or QS = "?foo=bar&gclid=AAA&bar=baz" + # Remove all marketing get parameters to minimize the cache objects + if (req.url ~ "(\?|&)(gclid|cx|ie|cof|siteurl|zanpid|origin|fbclid|mc_[a-z]+|utm_[a-z]+|_bta_[a-z]+)=") { + set req.url = regsuball(req.url, "(gclid|cx|ie|cof|siteurl|zanpid|origin|fbclid|mc_[a-z]+|utm_[a-z]+|_bta_[a-z]+)=[-_A-z0-9+()%.]+&?", ""); + set req.url = regsub(req.url, "[?|&]+$", ""); + } # Static files caching if (req.url ~ "^/(pub/)?(media|static)/") { @@ -122,6 +123,10 @@ sub vcl_hash { hash_data(server.ip); } + if (req.url ~ "/graphql") { + call process_graphql_headers; + } + # To make sure http users don't see ssl warning if (req.http./* {{ ssl_offloaded_header }} */) { hash_data(req.http./* {{ ssl_offloaded_header }} */); @@ -129,6 +134,15 @@ sub vcl_hash { /* {{ design_exceptions_code }} */ } +sub process_graphql_headers { + if (req.http.Store) { + hash_data(req.http.Store); + } + if (req.http.Content-Currency) { + hash_data(req.http.Content-Currency); + } +} + sub vcl_backend_response { set beresp.grace = 3d; diff --git a/app/code/Magento/PageCache/etc/varnish5.vcl b/app/code/Magento/PageCache/etc/varnish5.vcl index 388157af184a..4505e7462971 100644 --- a/app/code/Magento/PageCache/etc/varnish5.vcl +++ b/app/code/Magento/PageCache/etc/varnish5.vcl @@ -87,15 +87,16 @@ sub vcl_recv { } elsif (req.http.Accept-Encoding ~ "deflate" && req.http.user-agent !~ "MSIE") { set req.http.Accept-Encoding = "deflate"; } else { - # unkown algorithm + # unknown algorithm unset req.http.Accept-Encoding; } } - # Remove Google gclid parameters to minimize the cache objects - set req.url = regsuball(req.url,"\?gclid=[^&]+$",""); # strips when QS = "?gclid=AAA" - set req.url = regsuball(req.url,"\?gclid=[^&]+&","?"); # strips when QS = "?gclid=AAA&foo=bar" - set req.url = regsuball(req.url,"&gclid=[^&]+",""); # strips when QS = "?foo=bar&gclid=AAA" or QS = "?foo=bar&gclid=AAA&bar=baz" + # Remove all marketing get parameters to minimize the cache objects + if (req.url ~ "(\?|&)(gclid|cx|ie|cof|siteurl|zanpid|origin|fbclid|mc_[a-z]+|utm_[a-z]+|_bta_[a-z]+)=") { + set req.url = regsuball(req.url, "(gclid|cx|ie|cof|siteurl|zanpid|origin|fbclid|mc_[a-z]+|utm_[a-z]+|_bta_[a-z]+)=[-_A-z0-9+()%.]+&?", ""); + set req.url = regsub(req.url, "[?|&]+$", ""); + } # Static files caching if (req.url ~ "^/(pub/)?(media|static)/") { @@ -128,6 +129,19 @@ sub vcl_hash { hash_data(req.http./* {{ ssl_offloaded_header }} */); } /* {{ design_exceptions_code }} */ + + if (req.url ~ "/graphql") { + call process_graphql_headers; + } +} + +sub process_graphql_headers { + if (req.http.Store) { + hash_data(req.http.Store); + } + if (req.http.Content-Currency) { + hash_data(req.http.Content-Currency); + } } sub vcl_backend_response { diff --git a/app/code/Magento/PageCache/view/frontend/web/js/page-cache.js b/app/code/Magento/PageCache/view/frontend/web/js/page-cache.js index fccc8510ffc7..2e8a4769be10 100644 --- a/app/code/Magento/PageCache/view/frontend/web/js/page-cache.js +++ b/app/code/Magento/PageCache/view/frontend/web/js/page-cache.js @@ -6,9 +6,10 @@ define([ 'jquery', 'domReady', + 'consoleLogger', 'jquery/ui', 'mage/cookies' -], function ($, domReady) { +], function ($, domReady, consoleLogger) { 'use strict'; /** @@ -35,7 +36,9 @@ define([ * @returns {Array} */ $.fn.comments = function () { - var elements = []; + var elements = [], + contents, + elementContents; /** * @param {jQuery} element - Comment holder @@ -46,14 +49,35 @@ define([ // prevent cross origin iframe content reading if ($(element).prop('tagName') === 'IFRAME') { iframeHostName = $('<a>').prop('href', $(element).prop('src')) - .prop('hostname'); + .prop('hostname'); if (window.location.hostname !== iframeHostName) { return []; } } - $(element).contents().each(function (index, el) { + /** + * Rewrite jQuery contents(). + * + * @param {jQuery} elem + */ + contents = function (elem) { + return $.map(elem, function (el) { + try { + return $.nodeName(el, 'iframe') ? + el.contentDocument || (el.contentWindow ? el.contentWindow.document : []) : + $.merge([], el.childNodes); + } catch (e) { + consoleLogger.error(e); + + return []; + } + }); + }; + + elementContents = contents($(element)); + + $.each(elementContents, function (index, el) { switch (el.nodeType) { case 1: // ELEMENT_NODE lookup(el); diff --git a/app/code/Magento/Payment/Api/Data/PaymentAdditionalInfoInterface.php b/app/code/Magento/Payment/Api/Data/PaymentAdditionalInfoInterface.php new file mode 100644 index 000000000000..8afa064efd3e --- /dev/null +++ b/app/code/Magento/Payment/Api/Data/PaymentAdditionalInfoInterface.php @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Payment\Api\Data; + +use Magento\Framework\DataObject\KeyValueObjectInterface; + +/** + * Payment additional info interface. + */ +interface PaymentAdditionalInfoInterface extends KeyValueObjectInterface +{ +} diff --git a/app/code/Magento/Payment/Gateway/Validator/AbstractValidator.php b/app/code/Magento/Payment/Gateway/Validator/AbstractValidator.php index f1a895051415..110fe10ee5c3 100644 --- a/app/code/Magento/Payment/Gateway/Validator/AbstractValidator.php +++ b/app/code/Magento/Payment/Gateway/Validator/AbstractValidator.php @@ -3,11 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Payment\Gateway\Validator; /** - * Class AbstractValidator - * @package Magento\Payment\Gateway\Validator + * Represents a basic validator shell that can create a result + * * @api * @since 100.0.2 */ @@ -33,7 +36,7 @@ public function __construct( * @param bool $isValid * @param array $fails * @param array $errorCodes - * @return void + * @return \Magento\Payment\Gateway\Validator\ResultInterface */ protected function createResult($isValid, array $fails = [], array $errorCodes = []) { diff --git a/app/code/Magento/Payment/Gateway/Validator/ValidatorComposite.php b/app/code/Magento/Payment/Gateway/Validator/ValidatorComposite.php index b7f1368ddabc..8ea97d31ed4d 100644 --- a/app/code/Magento/Payment/Gateway/Validator/ValidatorComposite.php +++ b/app/code/Magento/Payment/Gateway/Validator/ValidatorComposite.php @@ -10,10 +10,9 @@ use Magento\Payment\Gateway\Validator\ResultInterfaceFactory; /** - * Class ValidatorComposite - * @package Magento\Payment\Gateway\Validator + * Compiles a result using the results of multiple validators + * * @api - * @since 100.0.2 */ class ValidatorComposite extends AbstractValidator { @@ -22,15 +21,22 @@ class ValidatorComposite extends AbstractValidator */ private $validators; + /** + * @var array + */ + private $chainBreakingValidators; + /** * @param ResultInterfaceFactory $resultFactory * @param TMapFactory $tmapFactory * @param array $validators + * @param array $chainBreakingValidators */ public function __construct( ResultInterfaceFactory $resultFactory, TMapFactory $tmapFactory, - array $validators = [] + array $validators = [], + array $chainBreakingValidators = [] ) { $this->validators = $tmapFactory->create( [ @@ -38,6 +44,7 @@ public function __construct( 'type' => ValidatorInterface::class ] ); + $this->chainBreakingValidators = $chainBreakingValidators; parent::__construct($resultFactory); } @@ -51,7 +58,8 @@ public function validate(array $validationSubject) { $isValid = true; $failsDescriptionAggregate = []; - foreach ($this->validators as $validator) { + $errorCodesAggregate = []; + foreach ($this->validators as $key => $validator) { $result = $validator->validate($validationSubject); if (!$result->isValid()) { $isValid = false; @@ -59,9 +67,16 @@ public function validate(array $validationSubject) $failsDescriptionAggregate, $result->getFailsDescription() ); + $errorCodesAggregate = array_merge( + $errorCodesAggregate, + $result->getErrorCodes() + ); + if (!empty($this->chainBreakingValidators[$key])) { + break; + } } } - return $this->createResult($isValid, $failsDescriptionAggregate); + return $this->createResult($isValid, $failsDescriptionAggregate, $errorCodesAggregate); } } diff --git a/app/code/Magento/Payment/Helper/Data.php b/app/code/Magento/Payment/Helper/Data.php index 0a4990313fa8..9bea19700d45 100644 --- a/app/code/Magento/Payment/Helper/Data.php +++ b/app/code/Magento/Payment/Helper/Data.php @@ -84,6 +84,8 @@ public function __construct( } /** + * Get config name of method model + * * @param string $code * @return string */ @@ -259,10 +261,13 @@ public function getPaymentMethodList($sorted = true, $asLabelValue = false, $wit $groupRelations = []; foreach ($this->getPaymentMethods() as $code => $data) { - if (isset($data['title'])) { - $methods[$code] = $data['title']; - } else { - $methods[$code] = $this->getMethodInstance($code)->getConfigData('title', $store); + if (!empty($data['active'])) { + $storedTitle = $this->getMethodInstance($code)->getConfigData('title', $store); + if (isset($storedTitle)) { + $methods[$code] = $storedTitle; + } elseif (isset($data['title'])) { + $methods[$code] = $data['title']; + } } if ($asLabelValue && $withGroups && isset($data['group'])) { $groupRelations[$code] = $data['group']; diff --git a/app/code/Magento/Payment/Model/CcConfigProvider.php b/app/code/Magento/Payment/Model/CcConfigProvider.php index 15bdd0072a51..497ce93c30c7 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/AbstractMethod.php b/app/code/Magento/Payment/Model/Method/AbstractMethod.php index 33200014c7ec..c0ccf887b18d 100644 --- a/app/code/Magento/Payment/Model/Method/AbstractMethod.php +++ b/app/code/Magento/Payment/Model/Method/AbstractMethod.php @@ -24,7 +24,7 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @deprecated 100.0.6 * @see \Magento\Payment\Model\Method\Adapter - * @see http://devdocs.magento.com/guides/v2.1/payments-integrations/payment-gateway/payment-gateway-intro.html + * @see https://devdocs.magento.com/guides/v2.1/payments-integrations/payment-gateway/payment-gateway-intro.html * @since 100.0.2 */ abstract class AbstractMethod extends \Magento\Framework\Model\AbstractExtensibleModel implements @@ -258,7 +258,7 @@ protected function initializeData($data = []) } /** - * {inheritdoc} + * @inheritdoc * @deprecated 100.2.0 */ public function setStore($storeId) @@ -267,7 +267,7 @@ public function setStore($storeId) } /** - * {inheritdoc} + * @inheritdoc * @deprecated 100.2.0 */ public function getStore() @@ -360,7 +360,8 @@ public function canRefundPartialPerInvoice() } /** - * Check void availability + * Check void availability. + * * @return bool * @internal param \Magento\Framework\DataObject $payment * @api @@ -372,8 +373,9 @@ public function canVoid() } /** - * Using internal pages for input payment data - * Can be used in admin + * Using internal pages for input payment data. + * + * Can be used in admin. * * @return bool * @deprecated 100.2.0 @@ -715,7 +717,8 @@ public function void(\Magento\Payment\Model\InfoInterface $payment) } /** - * Whether this method can accept or deny payment + * Whether this method can accept or deny payment. + * * @return bool * @api * @deprecated 100.2.0 @@ -867,8 +870,7 @@ public function isActive($storeId = null) } /** - * Method that will be executed instead of authorize or capture - * if flag isInitializeNeeded set to true + * Method that will be executed instead of authorize or capture if flag isInitializeNeeded set to true. * * @param string $paymentAction * @param object $stateObject @@ -884,8 +886,9 @@ public function initialize($paymentAction, $stateObject) } /** - * Get config payment action url - * Used to universalize payment actions when processing payment place + * Get config payment action url. + * + * Used to universalize payment actions when processing payment place. * * @return string * @api diff --git a/app/code/Magento/Payment/Model/Method/Cc.php b/app/code/Magento/Payment/Model/Method/Cc.php index c23ad5b535dd..11629308cd46 100644 --- a/app/code/Magento/Payment/Model/Method/Cc.php +++ b/app/code/Magento/Payment/Model/Method/Cc.php @@ -10,6 +10,8 @@ use Magento\Quote\Model\Quote\Payment; /** + * Credit Card payment method legacy implementation. + * * @method \Magento\Quote\Api\Data\PaymentMethodExtensionInterface getExtensionAttributes() * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @deprecated 100.0.8 @@ -93,6 +95,7 @@ public function __construct( * @throws \Magento\Framework\Exception\LocalizedException * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function validate() { @@ -148,6 +151,22 @@ public function validate() 'JCB' => '/^35(2[8-9][0-9]{12,15}|[3-8][0-9]{13,16})/', 'MI' => '/^(5(0|[6-9])|63|67(?!59|6770|6774))\d*$/', 'MD' => '/^(6759(?!24|38|40|6[3-9]|70|76)|676770|676774)\d*$/', + + //Hipercard + 'HC' => '/^((606282)|(637095)|(637568)|(637599)|(637609)|(637612))\d*$/', + //Elo + 'ELO' => '/^((509091)|(636368)|(636297)|(504175)|(438935)|(40117[8-9])|(45763[1-2])|' . + '(457393)|(431274)|(50990[0-2])|(5099[7-9][0-9])|(50996[4-9])|(509[1-8][0-9][0-9])|' . + '(5090(0[0-2]|0[4-9]|1[2-9]|[24589][0-9]|3[1-9]|6[0-46-9]|7[0-24-9]))|' . + '(5067(0[0-24-8]|1[0-24-9]|2[014-9]|3[0-379]|4[0-9]|5[0-3]|6[0-5]|7[0-8]))|' . + '(6504(0[5-9]|1[0-9]|2[0-9]|3[0-9]))|' . + '(6504(8[5-9]|9[0-9])|6505(0[0-9]|1[0-9]|2[0-9]|3[0-8]))|' . + '(6505(4[1-9]|5[0-9]|6[0-9]|7[0-9]|8[0-9]|9[0-8]))|' . + '(6507(0[0-9]|1[0-8]))|(65072[0-7])|(6509(0[1-9]|1[0-9]|20))|' . + '(6516(5[2-9]|6[0-9]|7[0-9]))|(6550(0[0-9]|1[0-9]))|' . + '(6550(2[1-9]|3[0-9]|4[0-9]|5[0-8])))\d*$/', + //Aura + 'AU' => '/^5078\d*$/' ]; $ccNumAndTypeMatches = isset( @@ -189,6 +208,8 @@ public function validate() } /** + * Check if verification should be used. + * * @return bool * @api */ @@ -202,6 +223,8 @@ public function hasVerification() } /** + * Get list of credit cards verification reg exp. + * * @return array * @api */ @@ -226,6 +249,8 @@ public function getVerificationRegEx() } /** + * Validate expiration date + * * @param string $expYear * @param string $expMonth * @return bool @@ -276,6 +301,8 @@ public function assignData(\Magento\Framework\DataObject $data) } /** + * Get code for "other" credit cards. + * * @param string $type * @return bool * @api diff --git a/app/code/Magento/Payment/Model/PaymentAdditionalInfo.php b/app/code/Magento/Payment/Model/PaymentAdditionalInfo.php new file mode 100644 index 000000000000..c4f135d5e004 --- /dev/null +++ b/app/code/Magento/Payment/Model/PaymentAdditionalInfo.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Payment\Model; + +use Magento\Payment\Api\Data\PaymentAdditionalInfoInterface; + +/** + * Payment additional info class. + */ +class PaymentAdditionalInfo implements PaymentAdditionalInfoInterface +{ + /** + * @var string + */ + private $key; + + /** + * @var string + */ + private $value; + + /** + * @inheritdoc + */ + public function getKey() + { + return $this->key; + } + + /** + * @inheritdoc + */ + public function getValue() + { + return $this->value; + } + + /** + * @inheritdoc + */ + public function setKey($key) + { + $this->key = $key; + return $key; + } + + /** + * @inheritdoc + */ + public function setValue($value) + { + $this->value = $value; + return $value; + } +} diff --git a/app/code/Magento/Payment/Observer/SalesOrderBeforeSaveObserver.php b/app/code/Magento/Payment/Observer/SalesOrderBeforeSaveObserver.php index ed8185e6dede..62c1cc15bad8 100644 --- a/app/code/Magento/Payment/Observer/SalesOrderBeforeSaveObserver.php +++ b/app/code/Magento/Payment/Observer/SalesOrderBeforeSaveObserver.php @@ -8,6 +8,9 @@ use Magento\Framework\Event\ObserverInterface; +/** + * Verify order payment observer + */ class SalesOrderBeforeSaveObserver implements ObserverInterface { /** @@ -15,12 +18,19 @@ class SalesOrderBeforeSaveObserver implements ObserverInterface * * @param \Magento\Framework\Event\Observer $observer * @return $this + * @throws \Magento\Framework\Exception\LocalizedException in case order has no payment specified. */ public function execute(\Magento\Framework\Event\Observer $observer) { /** @var \Magento\Sales\Model\Order $order */ $order = $observer->getEvent()->getOrder(); + if (!$order->getPayment()) { + throw new \Magento\Framework\Exception\LocalizedException( + __('Please provide payment for the order.') + ); + } + if ($order->getPayment()->getMethodInstance()->getCode() != 'free') { return $this; } diff --git a/app/code/Magento/Payment/Test/Unit/Gateway/Validator/ValidatorCompositeTest.php b/app/code/Magento/Payment/Test/Unit/Gateway/Validator/ValidatorCompositeTest.php index 7352cb7a4ac6..5dec99e2a4b1 100644 --- a/app/code/Magento/Payment/Test/Unit/Gateway/Validator/ValidatorCompositeTest.php +++ b/app/code/Magento/Payment/Test/Unit/Gateway/Validator/ValidatorCompositeTest.php @@ -13,9 +13,9 @@ class ValidatorCompositeTest extends \PHPUnit\Framework\TestCase public function testValidate() { $validationSubject = []; - $validator1 = $this->getMockBuilder(\Magento\Payment\Gateway\Validator\ValidatorInterface::class) + $validator1 = $this->getMockBuilder(ValidatorInterface::class) ->getMockForAbstractClass(); - $validator2 = $this->getMockBuilder(\Magento\Payment\Gateway\Validator\ValidatorInterface::class) + $validator2 = $this->getMockBuilder(ValidatorInterface::class) ->getMockForAbstractClass(); $tMapFactory = $this->getMockBuilder(\Magento\Framework\ObjectManager\TMapFactory::class) ->disableOriginalConstructor() @@ -30,8 +30,8 @@ public function testValidate() ->with( [ 'array' => [ - 'validator1' => \Magento\Payment\Gateway\Validator\ValidatorInterface::class, - 'validator2' => \Magento\Payment\Gateway\Validator\ValidatorInterface::class + 'validator1' => ValidatorInterface::class, + 'validator2' => ValidatorInterface::class ], 'type' => ValidatorInterface::class ] @@ -54,6 +54,9 @@ public function testValidate() $resultFail->expects(static::once()) ->method('getFailsDescription') ->willReturn(['Fail']); + $resultFail->expects(static::once()) + ->method('getErrorCodes') + ->willReturn(['abc123']); $validator1->expects(static::once()) ->method('validate') @@ -76,7 +79,7 @@ public function testValidate() [ 'isValid' => false, 'failsDescription' => ['Fail'], - 'errorCodes' => [] + 'errorCodes' => ['abc123'] ] ) ->willReturn($compositeResult); @@ -85,10 +88,91 @@ public function testValidate() $resultFactory, $tMapFactory, [ - 'validator1' => \Magento\Payment\Gateway\Validator\ValidatorInterface::class, - 'validator2' => \Magento\Payment\Gateway\Validator\ValidatorInterface::class + 'validator1' => ValidatorInterface::class, + 'validator2' => ValidatorInterface::class ] ); static::assertSame($compositeResult, $validatorComposite->validate($validationSubject)); } + + public function testValidateChainBreaksCorrectly() + { + $validationSubject = []; + $validator1 = $this->getMockBuilder(ValidatorInterface::class) + ->getMockForAbstractClass(); + $validator2 = $this->getMockBuilder(ValidatorInterface::class) + ->getMockForAbstractClass(); + $tMapFactory = $this->getMockBuilder(\Magento\Framework\ObjectManager\TMapFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $tMap = $this->getMockBuilder(\Magento\Framework\ObjectManager\TMap::class) + ->disableOriginalConstructor() + ->getMock(); + + $tMapFactory->expects($this->once()) + ->method('create') + ->with( + [ + 'array' => [ + 'validator1' => ValidatorInterface::class, + 'validator2' => ValidatorInterface::class + ], + 'type' => ValidatorInterface::class + ] + ) + ->willReturn($tMap); + $tMap->expects($this->once()) + ->method('getIterator') + ->willReturn(new \ArrayIterator([$validator1, $validator2])); + + $resultFail = $this->getMockBuilder(\Magento\Payment\Gateway\Validator\ResultInterface::class) + ->getMockForAbstractClass(); + $resultFail->expects($this->once()) + ->method('isValid') + ->willReturn(false); + $resultFail->expects($this->once()) + ->method('getFailsDescription') + ->willReturn(['Fail']); + $resultFail->expects($this->once()) + ->method('getErrorCodes') + ->willReturn(['abc123']); + + $validator1->expects($this->once()) + ->method('validate') + ->with($validationSubject) + ->willReturn($resultFail); + + // Assert this is never called + $validator2->expects($this->never()) + ->method('validate'); + + $compositeResult = $this->getMockBuilder(\Magento\Payment\Gateway\Validator\ResultInterface::class) + ->getMockForAbstractClass(); + $resultFactory = $this->getMockBuilder(\Magento\Payment\Gateway\Validator\ResultInterfaceFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $resultFactory->expects($this->once()) + ->method('create') + ->with( + [ + 'isValid' => false, + 'failsDescription' => ['Fail'], + 'errorCodes' => ['abc123'] + ] + ) + ->willReturn($compositeResult); + + $validatorComposite = new ValidatorComposite( + $resultFactory, + $tMapFactory, + [ + 'validator1' => ValidatorInterface::class, + 'validator2' => ValidatorInterface::class + ], + ['validator1'] + ); + $this->assertSame($compositeResult, $validatorComposite->validate($validationSubject)); + } } diff --git a/app/code/Magento/Payment/Test/Unit/Model/CcConfigProviderTest.php b/app/code/Magento/Payment/Test/Unit/Model/CcConfigProviderTest.php index a8856166995f..ff6aea44645c 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/Test/Unit/Observer/SalesOrderBeforeSaveObserverTest.php b/app/code/Magento/Payment/Test/Unit/Observer/SalesOrderBeforeSaveObserverTest.php index b86fbc6b1826..75916dd2ea99 100644 --- a/app/code/Magento/Payment/Test/Unit/Observer/SalesOrderBeforeSaveObserverTest.php +++ b/app/code/Magento/Payment/Test/Unit/Observer/SalesOrderBeforeSaveObserverTest.php @@ -61,7 +61,7 @@ public function testSalesOrderBeforeSaveCantUnhold() $paymentMock = $this->getMockBuilder( \Magento\Sales\Model\Order\Payment::class )->disableOriginalConstructor()->setMethods([])->getMock(); - $order->expects($this->once())->method('getPayment')->will($this->returnValue($paymentMock)); + $order->method('getPayment')->will($this->returnValue($paymentMock)); $methodInstance = $this->getMockBuilder( \Magento\Payment\Model\MethodInterface::class )->getMockForAbstractClass(); @@ -86,7 +86,7 @@ public function testSalesOrderBeforeSaveIsCanceled() $paymentMock = $this->getMockBuilder( \Magento\Sales\Model\Order\Payment::class )->disableOriginalConstructor()->setMethods([])->getMock(); - $order->expects($this->once())->method('getPayment')->will($this->returnValue($paymentMock)); + $order->method('getPayment')->will($this->returnValue($paymentMock)); $methodInstance = $this->getMockBuilder( \Magento\Payment\Model\MethodInterface::class )->getMockForAbstractClass(); @@ -114,7 +114,7 @@ public function testSalesOrderBeforeSaveIsClosed() $paymentMock = $this->getMockBuilder( \Magento\Sales\Model\Order\Payment::class )->disableOriginalConstructor()->setMethods([])->getMock(); - $order->expects($this->once())->method('getPayment')->will($this->returnValue($paymentMock)); + $order->method('getPayment')->will($this->returnValue($paymentMock)); $methodInstance = $this->getMockBuilder( \Magento\Payment\Model\MethodInterface::class )->getMockForAbstractClass(); @@ -156,6 +156,29 @@ public function testSalesOrderBeforeSaveSetForced() $this->salesOrderBeforeSaveObserver->execute($this->observerMock); } + /** + * The method should check that the payment is available, as this is not always the case. + * + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Please provide payment for the order. + */ + public function testDoesNothingWhenNoPaymentIsAvailable() + { + $this->_prepareEventMockWithMethods(['getOrder']); + + $order = $this->getMockBuilder(\Magento\Sales\Model\Order::class)->disableOriginalConstructor()->setMethods( + array_merge(['__wakeup', 'getPayment']) + )->getMock(); + + $this->eventMock->expects($this->once())->method('getOrder')->will( + $this->returnValue($order) + ); + + $order->expects($this->exactly(1))->method('getPayment')->willReturn(null); + + $this->salesOrderBeforeSaveObserver->execute($this->observerMock); + } + /** * Prepares EventMock with set of methods * @@ -184,7 +207,7 @@ private function _getPreparedOrderMethod($methodCode, $orderMethods = []) $paymentMock = $this->getMockBuilder( \Magento\Sales\Model\Order\Payment::class )->disableOriginalConstructor()->setMethods([])->getMock(); - $order->expects($this->once())->method('getPayment')->will($this->returnValue($paymentMock)); + $order->method('getPayment')->will($this->returnValue($paymentMock)); $methodInstance = $this->getMockBuilder( \Magento\Payment\Model\MethodInterface::class )->getMockForAbstractClass(); diff --git a/app/code/Magento/Payment/Ui/Component/Listing/Column/Method/Options.php b/app/code/Magento/Payment/Ui/Component/Listing/Column/Method/Options.php index 5afaa9fcf97b..fbf80de519f9 100644 --- a/app/code/Magento/Payment/Ui/Component/Listing/Column/Method/Options.php +++ b/app/code/Magento/Payment/Ui/Component/Listing/Column/Method/Options.php @@ -25,8 +25,9 @@ class Options implements \Magento\Framework\Data\OptionSourceInterface * * @param \Magento\Payment\Helper\Data $paymentHelper */ - public function __construct(\Magento\Payment\Helper\Data $paymentHelper) - { + public function __construct( + \Magento\Payment\Helper\Data $paymentHelper + ) { $this->paymentHelper = $paymentHelper; } diff --git a/app/code/Magento/Payment/etc/config.xml b/app/code/Magento/Payment/etc/config.xml index 9fe859c96a94..663734fb066c 100644 --- a/app/code/Magento/Payment/etc/config.xml +++ b/app/code/Magento/Payment/etc/config.xml @@ -13,6 +13,7 @@ <model>Magento\Payment\Model\Method\Free</model> <order_status>pending</order_status> <title>No Payment Information Required + authorize 0 1 diff --git a/app/code/Magento/Payment/etc/di.xml b/app/code/Magento/Payment/etc/di.xml index 74f553cc6409..b7422bb00d54 100644 --- a/app/code/Magento/Payment/etc/di.xml +++ b/app/code/Magento/Payment/etc/di.xml @@ -7,6 +7,7 @@ --> + diff --git a/app/code/Magento/Payment/etc/payment.xml b/app/code/Magento/Payment/etc/payment.xml index 19b5eb709c64..4afb6b01b366 100644 --- a/app/code/Magento/Payment/etc/payment.xml +++ b/app/code/Magento/Payment/etc/payment.xml @@ -41,5 +41,14 @@ + + + + + + + + + diff --git a/app/code/Magento/Payment/view/adminhtml/web/transparent.js b/app/code/Magento/Payment/view/adminhtml/web/js/transparent.js similarity index 100% rename from app/code/Magento/Payment/view/adminhtml/web/transparent.js rename to app/code/Magento/Payment/view/adminhtml/web/js/transparent.js 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 000000000000..7acac62f65d3 --- /dev/null +++ b/app/code/Magento/Payment/view/base/templates/info/pdf/default.phtml @@ -0,0 +1,23 @@ + +escapeHtml($block->getMethod()->getTitle()) ?>{{pdf_row_separator}} + +getSpecificInformation()):?> + $value):?> + escapeHtml($label) ?>: + escapeHtml(implode(' ', $block->getValueAsArray($value))) ?> + {{pdf_row_separator}} + + + +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 000000000000..04cb2df8fa33 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 000000000000..eba0296a0910 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 000000000000..203e0b7e305c 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.js b/app/code/Magento/Payment/view/base/web/js/model/credit-card-validation/credit-card-number-validator.js index 8fb12093e36e..785b636d5832 100644 --- a/app/code/Magento/Payment/view/base/web/js/model/credit-card-validation/credit-card-number-validator.js +++ b/app/code/Magento/Payment/view/base/web/js/model/credit-card-validation/credit-card-number-validator.js @@ -36,7 +36,7 @@ define([ return resultWrapper(null, false, false); } - value = value.replace(/\-|\s/g, ''); + value = value.replace(/|\s/g, ''); if (!/^\d*$/.test(value)) { return resultWrapper(null, false, false); 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 3ac67f6f3100..1b387b384104 100644 --- a/app/code/Magento/Payment/view/base/web/js/model/credit-card-validation/credit-card-number-validator/credit-card-type.js +++ b/app/code/Magento/Payment/view/base/web/js/model/credit-card-validation/credit-card-number-validator/credit-card-type.js @@ -110,6 +110,48 @@ define([ name: 'CVC', size: 3 } + }, + { + title: 'Hipercard', + type: 'HC', + pattern: '^((606282)|(637095)|(637568)|(637599)|(637609)|(637612))\\d*$', + gaps: [4, 8, 12], + lengths: [13, 16], + code: { + name: 'CVC', + size: 3 + } + }, + { + title: 'Elo', + type: 'ELO', + pattern: '^((509091)|(636368)|(636297)|(504175)|(438935)|(40117[8-9])|(45763[1-2])|' + + '(457393)|(431274)|(50990[0-2])|(5099[7-9][0-9])|(50996[4-9])|(509[1-8][0-9][0-9])|' + + '(5090(0[0-2]|0[4-9]|1[2-9]|[24589][0-9]|3[1-9]|6[0-46-9]|7[0-24-9]))|' + + '(5067(0[0-24-8]|1[0-24-9]|2[014-9]|3[0-379]|4[0-9]|5[0-3]|6[0-5]|7[0-8]))|' + + '(6504(0[5-9]|1[0-9]|2[0-9]|3[0-9]))|' + + '(6504(8[5-9]|9[0-9])|6505(0[0-9]|1[0-9]|2[0-9]|3[0-8]))|' + + '(6505(4[1-9]|5[0-9]|6[0-9]|7[0-9]|8[0-9]|9[0-8]))|' + + '(6507(0[0-9]|1[0-8]))|(65072[0-7])|(6509(0[1-9]|1[0-9]|20))|' + + '(6516(5[2-9]|6[0-9]|7[0-9]))|(6550(0[0-9]|1[0-9]))|' + + '(6550(2[1-9]|3[0-9]|4[0-9]|5[0-8])))\\d*$', + gaps: [4, 8, 12], + lengths: [16], + code: { + name: 'CVC', + size: 3 + } + }, + { + title: 'Aura', + type: 'AU', + pattern: '^5078\\d*$', + gaps: [4, 8, 12], + lengths: [19], + code: { + name: 'CVC', + size: 3 + } } ]; diff --git a/app/code/Magento/Payment/view/base/web/js/model/credit-card-validation/cvv-validator.js b/app/code/Magento/Payment/view/base/web/js/model/credit-card-validation/cvv-validator.js index c22121622d26..7651fba1090c 100644 --- a/app/code/Magento/Payment/view/base/web/js/model/credit-card-validation/cvv-validator.js +++ b/app/code/Magento/Payment/view/base/web/js/model/credit-card-validation/cvv-validator.js @@ -21,7 +21,7 @@ define([], function () { /** * CVV number validation. - * Validate digit count fot CVV code. + * Validate digit count for CVV code. * * @param {*} value * @param {Number} maxLength diff --git a/app/code/Magento/Payment/view/base/web/js/model/credit-card-validation/validator.js b/app/code/Magento/Payment/view/base/web/js/model/credit-card-validation/validator.js index c6f1bad31fc0..c41be40cba14 100644 --- a/app/code/Magento/Payment/view/base/web/js/model/credit-card-validation/validator.js +++ b/app/code/Magento/Payment/view/base/web/js/model/credit-card-validation/validator.js @@ -23,6 +23,12 @@ }(function ($, cvvValidator, creditCardNumberValidator, yearValidator, monthValidator, creditCardData) { 'use strict'; + $('.payment-method-content input[type="number"]').on('keyup', function () { + if ($(this).val() < 0) { + $(this).val($(this).val().replace(/^-/, '')); + } + }); + $.each({ 'validate-card-type': [ function (number, item, allowedTypes) { diff --git a/app/code/Magento/Payment/view/frontend/requirejs-config.js b/app/code/Magento/Payment/view/frontend/requirejs-config.js index 70f93854ead4..949b1554a6f7 100644 --- a/app/code/Magento/Payment/view/frontend/requirejs-config.js +++ b/app/code/Magento/Payment/view/frontend/requirejs-config.js @@ -6,7 +6,8 @@ var config = { map: { '*': { - creditCardType: 'Magento_Payment/cc-type' + creditCardType: 'Magento_Payment/js/cc-type', + 'Magento_Payment/cc-type': 'Magento_Payment/js/cc-type' } } }; diff --git a/app/code/Magento/Payment/view/frontend/templates/transparent/iframe.phtml b/app/code/Magento/Payment/view/frontend/templates/transparent/iframe.phtml index c127371cb5f4..ea06c60c30e2 100644 --- a/app/code/Magento/Payment/view/frontend/templates/transparent/iframe.phtml +++ b/app/code/Magento/Payment/view/frontend/templates/transparent/iframe.phtml @@ -40,9 +40,7 @@ $params = $block->getParams(); $(parent).trigger('clearTimeout'); fullScreenLoader.stopLoader(); globalMessageList.addErrorMessage({ - message: $t( - 'A server error stopped your order from being placed. Please try to place your order again.' - ) + message: $t() }); } ); diff --git a/app/code/Magento/Payment/view/frontend/web/cc-type.js b/app/code/Magento/Payment/view/frontend/web/js/cc-type.js similarity index 100% rename from app/code/Magento/Payment/view/frontend/web/cc-type.js rename to app/code/Magento/Payment/view/frontend/web/js/cc-type.js diff --git a/app/code/Magento/Payment/view/frontend/web/transparent.js b/app/code/Magento/Payment/view/frontend/web/js/transparent.js similarity index 100% rename from app/code/Magento/Payment/view/frontend/web/transparent.js rename to app/code/Magento/Payment/view/frontend/web/js/transparent.js diff --git a/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Field/Depends/ButtonStylesLabel.php b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Field/Depends/ButtonStylesLabel.php new file mode 100644 index 000000000000..82e0e5566063 --- /dev/null +++ b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Field/Depends/ButtonStylesLabel.php @@ -0,0 +1,26 @@ +config = $config; + parent::__construct($context, $data); + } + + /** + * Render country field considering request parameter + * + * @param AbstractElement $element + * @return string + */ + public function render(AbstractElement $element) + { + if (!$this->isSelectedMerchantCountry('US')) { + $fundingOptions = $element->getValues(); + $element->setValues($this->filterValuesForPaypalCredit($fundingOptions)); + } + return parent::render($element); + } + + /** + * Getting the name of a UI attribute + * + * @return string + */ + protected function getDataAttributeName(): string + { + return 'disable-funding-options'; + } + + /** + * Filters array for CREDIT + * + * @param array $options + * @return array + */ + private function filterValuesForPaypalCredit($options): array + { + return array_filter($options, function ($opt) { + return ($opt['value'] !== 'CREDIT'); + }); + } + + /** + * Checks for chosen Merchant country from the config/url + * + * @param string $country + * @return bool + */ + private function isSelectedMerchantCountry(string $country): bool + { + $merchantCountry = $this->getRequest()->getParam(StructurePlugin::REQUEST_PARAM_COUNTRY) + ?: $this->config->getMerchantCountry(); + return $merchantCountry === $country; + } +} diff --git a/app/code/Magento/Paypal/Block/Bml/Shortcut.php b/app/code/Magento/Paypal/Block/Bml/Shortcut.php index 39e5dbd3cefc..d2f5ca009a19 100644 --- a/app/code/Magento/Paypal/Block/Bml/Shortcut.php +++ b/app/code/Magento/Paypal/Block/Bml/Shortcut.php @@ -8,7 +8,13 @@ use Magento\Catalog\Block as CatalogBlock; use Magento\Paypal\Helper\Shortcut\ValidatorInterface; +use Magento\Paypal\Model\ConfigFactory; +use Magento\Paypal\Model\Config; +use Magento\Framework\App\ObjectManager; +/** + * Class shortcut + */ class Shortcut extends \Magento\Framework\View\Element\Template implements CatalogBlock\ShortcutInterface { /** @@ -66,6 +72,11 @@ class Shortcut extends \Magento\Framework\View\Element\Template implements Catal */ private $_shortcutValidator; + /** + * @var Config + */ + private $config; + /** * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Payment\Helper\Data $paymentData @@ -77,7 +88,9 @@ class Shortcut extends \Magento\Framework\View\Element\Template implements Catal * @param string $bmlMethodCode * @param string $shortcutTemplate * @param array $data + * @param ConfigFactory|null $config * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * @codingStandardsIgnoreStart */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, @@ -89,28 +102,35 @@ public function __construct( $alias, $bmlMethodCode, $shortcutTemplate, - array $data = [] + array $data = [], + ConfigFactory $config = null ) { $this->_paymentData = $paymentData; $this->_mathRandom = $mathRandom; $this->_shortcutValidator = $shortcutValidator; - $this->_paymentMethodCode = $paymentMethodCode; $this->_startAction = $startAction; $this->_alias = $alias; $this->setTemplate($shortcutTemplate); $this->_bmlMethodCode = $bmlMethodCode; + $this->config = $config + ? $config->create() + : ObjectManager::getInstance()->get(ConfigFactory::class)->create(); + $this->config->setMethod($this->_paymentMethodCode); parent::__construct($context, $data); } + //@codingStandardsIgnoreEnd /** - * @return \Magento\Framework\View\Element\AbstractBlock + * @inheritdoc */ protected function _beforeToHtml() { $result = parent::_beforeToHtml(); $isInCatalog = $this->getIsInCatalogProduct(); - if (!$this->_shortcutValidator->validate($this->_paymentMethodCode, $isInCatalog)) { + if (!$this->_shortcutValidator->validate($this->_paymentMethodCode, $isInCatalog) + || (bool)(int)$this->config->getValue('in_context') + ) { $this->_shouldRender = false; return $result; } diff --git a/app/code/Magento/Paypal/Block/Express/InContext/Minicart/Button.php b/app/code/Magento/Paypal/Block/Express/InContext/Minicart/Button.php index 79142ecb1bfa..8d1e04c1397f 100644 --- a/app/code/Magento/Paypal/Block/Express/InContext/Minicart/Button.php +++ b/app/code/Magento/Paypal/Block/Express/InContext/Minicart/Button.php @@ -9,7 +9,6 @@ use Magento\Payment\Model\MethodInterface; use Magento\Paypal\Model\Config; use Magento\Paypal\Model\ConfigFactory; -use Magento\Paypal\Block\Express\InContext; use Magento\Framework\View\Element\Template; use Magento\Catalog\Block\ShortcutInterface; use Magento\Framework\Locale\ResolverInterface; @@ -17,6 +16,7 @@ /** * Class Button + * @deprecated @see \Magento\Paypal\Block\Express\InContext\Minicart\SmartButton */ class Button extends Template implements ShortcutInterface { @@ -59,8 +59,8 @@ class Button extends Template implements ShortcutInterface * @param Context $context * @param ResolverInterface $localeResolver * @param ConfigFactory $configFactory - * @param MethodInterface $payment * @param Session $session + * @param MethodInterface $payment * @param array $data */ public function __construct( @@ -101,8 +101,7 @@ private function isVisibleOnCart() } /** - * Check is Paypal In-Context Express Checkout button - * should render in cart/mini-cart + * Check is Paypal In-Context Express Checkout button should render in cart/mini-cart * * @return bool */ @@ -127,6 +126,8 @@ protected function _toHtml() } /** + * Returns container id + * * @return string */ public function getContainerId() @@ -135,6 +136,8 @@ public function getContainerId() } /** + * Returns link action + * * @return string */ public function getLinkAction() @@ -143,6 +146,8 @@ public function getLinkAction() } /** + * Returns add to cart selector + * * @return string */ public function getAddToCartSelector() @@ -151,6 +156,8 @@ public function getAddToCartSelector() } /** + * Returns image url + * * @return string */ public function getImageUrl() @@ -171,6 +178,8 @@ public function getAlias() } /** + * Set information if button renders in the mini cart + * * @param bool $isCatalog * @return $this */ diff --git a/app/code/Magento/Paypal/Block/Express/InContext/Minicart/SmartButton.php b/app/code/Magento/Paypal/Block/Express/InContext/Minicart/SmartButton.php new file mode 100644 index 000000000000..c6a17fa5efb9 --- /dev/null +++ b/app/code/Magento/Paypal/Block/Express/InContext/Minicart/SmartButton.php @@ -0,0 +1,223 @@ +config = $configFactory->create(); + $this->config->setMethod(Config::METHOD_EXPRESS); + $this->payment = $payment; + $this->session = $session; + $this->serializer = $serializer; + $this->smartButtonConfig = $smartButtonConfig; + $this->urlBuilder = $urlBuilder; + $this->quoteIdMask = $quoteIdToMaskedQuoteId; + } + + /** + * Check `in_context` config value + * + * @return bool + */ + private function isInContext(): bool + { + return (bool)(int) $this->config->getValue('in_context'); + } + + /** + * Check `visible_on_cart` config value + * + * @return bool + */ + private function isVisibleOnCart(): bool + { + return (bool)(int) $this->config->getValue('visible_on_cart'); + } + + /** + * Check is Paypal In-Context Express Checkout button should render in cart/mini-cart + * + * @return bool + */ + private function shouldRender(): bool + { + return $this->payment->isAvailable($this->session->getQuote()) + && $this->isInContext() + && $this->isVisibleOnCart() + && $this->getQuoteId() + && !$this->getIsInCatalogProduct(); + } + + /** + * @inheritdoc + */ + protected function _toHtml() + { + if (!$this->shouldRender()) { + return ''; + } + + return parent::_toHtml(); + } + + /** + * Get shortcut alias + * + * @return string + */ + public function getAlias() + { + return $this->getData(self::ALIAS_ELEMENT_INDEX); + } + + /** + * Returns string to initialize js component + * + * @return string + */ + public function getJsInitParams(): string + { + $config = []; + $quoteId = $this->getQuoteId(); + if (!empty($quoteId)) { + $clientConfig = [ + 'quoteId' => $quoteId, + 'customerId' => $this->session->getQuote()->getCustomerId(), + 'button' => 1, + 'getTokenUrl' => $this->urlBuilder->getUrl( + 'paypal/express/getTokenData', + ['_secure' => $this->getRequest()->isSecure()] + ), + 'onAuthorizeUrl' => $this->urlBuilder->getUrl( + 'paypal/express/onAuthorization', + ['_secure' => $this->getRequest()->isSecure()] + ), + 'onCancelUrl' => $this->urlBuilder->getUrl( + 'paypal/express/cancel', + ['_secure' => $this->getRequest()->isSecure()] + ) + ]; + $smartButtonsConfig = $this->getIsShoppingCart() + ? $this->smartButtonConfig->getConfig('cart') + : $this->smartButtonConfig->getConfig('mini_cart'); + $clientConfig = array_replace_recursive($clientConfig, $smartButtonsConfig); + $config = [ + 'Magento_Paypal/js/in-context/button' => [ + 'clientConfig' => $clientConfig + ] + ]; + } + $json = $this->serializer->serialize($config); + return $json; + } + + /** + * Returns container id + * + * @return string + */ + public function getContainerId(): string + { + return $this->getData('button_id'); + } + + /** + * Get quote id from session + * + * @return string + */ + private function getQuoteId(): string + { + $quoteId = (int)$this->session->getQuoteId(); + if (!$this->session->getQuote()->getCustomerId()) { + try { + $quoteId = $this->quoteIdMask->execute($quoteId); + } catch (NoSuchEntityException $e) { + $quoteId = ""; + } + } + return (string)$quoteId; + } +} diff --git a/app/code/Magento/Paypal/Block/Express/InContext/SmartButton.php b/app/code/Magento/Paypal/Block/Express/InContext/SmartButton.php new file mode 100644 index 000000000000..6d355038cff1 --- /dev/null +++ b/app/code/Magento/Paypal/Block/Express/InContext/SmartButton.php @@ -0,0 +1,138 @@ +config = $configFactory->create(); + $this->config->setMethod(Config::METHOD_EXPRESS); + $this->serializer = $serializer; + $this->smartButtonConfig = $smartButtonConfig; + $this->urlBuilder = $urlBuilder; + } + + /** + * Check is Paypal In-Context Express Checkout button should render in cart/mini-cart + * + * @return bool + */ + private function shouldRender(): bool + { + $isInCatalog = $this->getIsInCatalogProduct(); + $isInContext = (bool)(int) $this->config->getValue('in_context'); + + return ($isInContext && $isInCatalog); + } + + /** + * @inheritdoc + */ + protected function _toHtml() + { + if (!$this->shouldRender()) { + return ''; + } + + return parent::_toHtml(); + } + + /** + * Get shortcut alias + * + * @return string + */ + public function getAlias() + { + return $this->getData(self::ALIAS_ELEMENT_INDEX); + } + + /** + * Returns string to initialize js component + * + * @return string + */ + public function getJsInitParams(): string + { + $clientConfig = [ + 'button' => 1, + 'getTokenUrl' => $this->urlBuilder->getUrl( + 'paypal/express/getTokenData', + ['_secure' => $this->getRequest()->isSecure()] + ), + 'onAuthorizeUrl' => $this->urlBuilder->getUrl( + 'paypal/express/onAuthorization', + ['_secure' => $this->getRequest()->isSecure()] + ), + 'onCancelUrl' => $this->urlBuilder->getUrl( + 'paypal/express/cancel', + ['_secure' => $this->getRequest()->isSecure()] + ) + ]; + $smartButtonsConfig = $this->smartButtonConfig->getConfig('product'); + $clientConfig = array_replace_recursive($clientConfig, $smartButtonsConfig); + $config = [ + 'Magento_Paypal/js/in-context/product-express-checkout' => [ + 'clientConfig' => $clientConfig + ] + ]; + + return $this->serializer->serialize($config); + } +} diff --git a/app/code/Magento/Paypal/Block/Express/Shortcut.php b/app/code/Magento/Paypal/Block/Express/Shortcut.php index bdb9279356d8..16305238e17d 100644 --- a/app/code/Magento/Paypal/Block/Express/Shortcut.php +++ b/app/code/Magento/Paypal/Block/Express/Shortcut.php @@ -137,7 +137,7 @@ public function __construct( } /** - * @return \Magento\Framework\View\Element\AbstractBlock + * @inheritdoc */ protected function _beforeToHtml() { @@ -145,7 +145,9 @@ protected function _beforeToHtml() $isInCatalog = $this->getIsInCatalogProduct(); - if (!$this->_shortcutValidator->validate($this->_paymentMethodCode, $isInCatalog)) { + if (!$this->_shortcutValidator->validate($this->_paymentMethodCode, $isInCatalog) + || (bool)(int)$this->config->getValue('in_context') + ) { $this->_shouldRender = false; return $result; } @@ -186,6 +188,8 @@ protected function _toHtml() } /** + * Check if we should render component + * * @return bool */ protected function shouldRender() diff --git a/app/code/Magento/Paypal/Controller/Adminhtml/Billing/Agreement/Index.php b/app/code/Magento/Paypal/Controller/Adminhtml/Billing/Agreement/Index.php index b4c68df6e29e..ca92b6a044fa 100644 --- a/app/code/Magento/Paypal/Controller/Adminhtml/Billing/Agreement/Index.php +++ b/app/code/Magento/Paypal/Controller/Adminhtml/Billing/Agreement/Index.php @@ -6,7 +6,9 @@ */ namespace Magento\Paypal\Controller\Adminhtml\Billing\Agreement; -class Index extends \Magento\Paypal\Controller\Adminhtml\Billing\Agreement +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\Paypal\Controller\Adminhtml\Billing\Agreement implements HttpGetActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Paypal/Controller/Adminhtml/Paypal/Reports/Index.php b/app/code/Magento/Paypal/Controller/Adminhtml/Paypal/Reports/Index.php index 91f3e8d9065b..c2cee0ffbc67 100644 --- a/app/code/Magento/Paypal/Controller/Adminhtml/Paypal/Reports/Index.php +++ b/app/code/Magento/Paypal/Controller/Adminhtml/Paypal/Reports/Index.php @@ -6,7 +6,9 @@ */ namespace Magento\Paypal\Controller\Adminhtml\Paypal\Reports; -class Index extends \Magento\Paypal\Controller\Adminhtml\Paypal\Reports +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\Paypal\Controller\Adminhtml\Paypal\Reports implements HttpGetActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Paypal/Controller/Express/AbstractExpress.php b/app/code/Magento/Paypal/Controller/Express/AbstractExpress.php index 571d73d07b68..7ad8fe658ec1 100644 --- a/app/code/Magento/Paypal/Controller/Express/AbstractExpress.php +++ b/app/code/Magento/Paypal/Controller/Express/AbstractExpress.php @@ -7,12 +7,18 @@ use Magento\Checkout\Controller\Express\RedirectLoginInterface; use Magento\Framework\App\Action\Action as AppAction; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Quote\Api\Data\CartInterface; /** * Abstract Express Checkout Controller * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -abstract class AbstractExpress extends AppAction implements RedirectLoginInterface +abstract class AbstractExpress extends AppAction implements + RedirectLoginInterface, + HttpGetActionInterface, + HttpPostActionInterface { /** * @var \Magento\Paypal\Model\Express\Checkout @@ -127,16 +133,26 @@ public function __construct( /** * Instantiate quote and checkout * + * @param CartInterface|null $quoteObject + * * @return void * @throws \Magento\Framework\Exception\LocalizedException */ - protected function _initCheckout() + protected function _initCheckout(CartInterface $quoteObject = null) { - $quote = $this->_getQuote(); + $quote = $quoteObject ? $quoteObject : $this->_getQuote(); if (!$quote->hasItems() || $quote->getHasError()) { $this->getResponse()->setStatusHeader(403, '1.1', 'Forbidden'); throw new \Magento\Framework\Exception\LocalizedException(__('We can\'t initialize Express Checkout.')); } + if (!(float)$quote->getGrandTotal()) { + throw new \Magento\Framework\Exception\LocalizedException( + __( + 'PayPal can\'t process orders with a zero balance due. ' + . 'To finish your purchase, please go through the standard checkout process.' + ) + ); + } if (!isset($this->_checkoutTypes[$this->_checkoutType])) { $parameters = [ 'params' => [ @@ -151,6 +167,8 @@ protected function _initCheckout() } /** + * Get Proper Checkout Token + * * Search for proper checkout token in request or session or (un)set specified one * Combined getter/setter * @@ -221,8 +239,7 @@ protected function _getQuote() } /** - * Returns before_auth_url redirect parameter for customer session - * @return null + * @inheritdoc */ public function getCustomerBeforeAuthUrl() { @@ -230,8 +247,7 @@ public function getCustomerBeforeAuthUrl() } /** - * Returns a list of action flags [flag_key] => boolean - * @return array + * @inheritdoc */ public function getActionFlagList() { @@ -240,6 +256,7 @@ public function getActionFlagList() /** * Returns login url parameter for redirect + * * @return string */ public function getLoginUrl() @@ -249,6 +266,7 @@ public function getLoginUrl() /** * Returns action name which requires redirect + * * @return string */ public function getRedirectActionName() @@ -269,4 +287,9 @@ public function redirectLogin() $this->_urlHelper->addRequestParam($this->_customerUrl->getLoginUrl(), ['context' => 'checkout']) ); } + + /** + * @inheritdoc + */ + abstract public function execute(); } diff --git a/app/code/Magento/Paypal/Controller/Express/AbstractExpress/ShippingOptionsCallback.php b/app/code/Magento/Paypal/Controller/Express/AbstractExpress/ShippingOptionsCallback.php index cb1b3388dc06..fc3a45e1e139 100644 --- a/app/code/Magento/Paypal/Controller/Express/AbstractExpress/ShippingOptionsCallback.php +++ b/app/code/Magento/Paypal/Controller/Express/AbstractExpress/ShippingOptionsCallback.php @@ -6,7 +6,17 @@ */ namespace Magento\Paypal\Controller\Express\AbstractExpress; -class ShippingOptionsCallback extends \Magento\Paypal\Controller\Express\AbstractExpress +use Magento\Framework\App\CsrfAwareActionInterface; +use Magento\Paypal\Controller\Express\AbstractExpress; +use Magento\Framework\App\Request\InvalidRequestException; +use Magento\Framework\App\RequestInterface; + +/** + * Returns shipping rates by server-to-server request from PayPal. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ShippingOptionsCallback extends AbstractExpress implements CsrfAwareActionInterface { /** * @var \Magento\Quote\Api\CartRepositoryInterface @@ -65,4 +75,21 @@ public function execute() $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); } } + + /** + * @inheritDoc + */ + public function createCsrfValidationException( + RequestInterface $request + ): ?InvalidRequestException { + return null; + } + + /** + * @inheritDoc + */ + public function validateForCsrf(RequestInterface $request): ?bool + { + return true; + } } diff --git a/app/code/Magento/Paypal/Controller/Express/GetToken.php b/app/code/Magento/Paypal/Controller/Express/GetToken.php index 7c127803cb0b..93e9abb4c097 100644 --- a/app/code/Magento/Paypal/Controller/Express/GetToken.php +++ b/app/code/Magento/Paypal/Controller/Express/GetToken.php @@ -5,6 +5,7 @@ */ namespace Magento\Paypal\Controller\Express; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Checkout\Helper\Data; use Magento\Checkout\Helper\ExpressRedirect; use Magento\Checkout\Model\Type\Onepage; @@ -19,7 +20,7 @@ * Class GetToken * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class GetToken extends AbstractExpress +class GetToken extends AbstractExpress implements HttpGetActionInterface { /** * Config mode type diff --git a/app/code/Magento/Paypal/Controller/Express/GetTokenData.php b/app/code/Magento/Paypal/Controller/Express/GetTokenData.php new file mode 100644 index 000000000000..512dac4cdec0 --- /dev/null +++ b/app/code/Magento/Paypal/Controller/Express/GetTokenData.php @@ -0,0 +1,206 @@ +logger = $logger; + $this->customerRepository = $customerRepository; + $this->cartRepository = $cartRepository; + $this->guestCartRepository = $guestCartRepository; + } + + /** + * Get token data + * + * @return ResultInterface + */ + public function execute(): ResultInterface + { + $controllerResult = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $responseContent = [ + 'success' => true, + 'error_message' => '', + ]; + + try { + $token = $this->getToken(); + if ($token === null) { + $token = false; + } + $this->_initToken($token); + + $responseContent['token'] = $token; + } catch (LocalizedException $exception) { + $this->logger->critical($exception); + + $responseContent['success'] = false; + $responseContent['error_message'] = $exception->getMessage(); + } catch (\Exception $exception) { + $this->logger->critical($exception); + + $responseContent['success'] = false; + $responseContent['error_message'] = __('Sorry, but something went wrong'); + } + + return $controllerResult->setData($responseContent); + } + + /** + * Get paypal token + * + * @return string|null + * @throws LocalizedException + */ + private function getToken(): ?string + { + $quoteId = $this->getRequest()->getParam('quote_id'); + $customerId = $this->getRequest()->getParam('customer_id') ?: $this->_customerSession->getId(); + $hasButton = (bool)$this->getRequest()->getParam(Checkout::PAYMENT_INFO_BUTTON); + + if ($quoteId) { + $quote = $customerId ? $this->cartRepository->get($quoteId) : $this->guestCartRepository->get($quoteId); + } else { + $quote = $this->_getQuote(); + } + + $this->_initCheckout($quote); + + if ($quote->getIsMultiShipping()) { + $quote->setIsMultiShipping(0); + $quote->removeAllAddresses(); + } + + if ($customerId) { + $customerData = $this->customerRepository->getById((int)$customerId); + + $this->_checkout->setCustomerWithAddressChange( + $customerData, + $quote->getBillingAddress(), + $quote->getShippingAddress() + ); + } + + // giropay urls + $this->_checkout->prepareGiropayUrls( + $this->_url->getUrl('checkout/onepage/success'), + $this->_url->getUrl('paypal/express/cancel'), + $this->_url->getUrl('checkout/onepage/success') + ); + + return $this->_checkout->start( + $this->_url->getUrl('*/*/return'), + $this->_url->getUrl('*/*/cancel'), + $hasButton + ); + } +} diff --git a/app/code/Magento/Paypal/Controller/Express/OnAuthorization.php b/app/code/Magento/Paypal/Controller/Express/OnAuthorization.php new file mode 100644 index 000000000000..62f4c4c4c457 --- /dev/null +++ b/app/code/Magento/Paypal/Controller/Express/OnAuthorization.php @@ -0,0 +1,172 @@ +cartRepository = $cartRepository; + $this->urlBuilder = $urlBuilder; + $this->guestCartRepository = $guestCartRepository; + } + + /** + * Place order or redirect on Paypal review page + * + * @return ResultInterface + */ + public function execute(): ResultInterface + { + $controllerResult = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $quoteId = $this->getRequest()->getParam('quoteId'); + $payerId = $this->getRequest()->getParam('payerId'); + $tokenId = $this->getRequest()->getParam('paymentToken'); + $customerId = $this->getRequest()->getParam('customerId') ?: $this->_customerSession->getId(); + + try { + if ($quoteId) { + $quote = $customerId ? $this->cartRepository->get($quoteId) : $this->guestCartRepository->get($quoteId); + } else { + $quote = $this->_getQuote(); + } + + $responseContent = [ + 'success' => true, + 'error_message' => '', + ]; + + /** Populate checkout object with new data */ + $this->_initCheckout($quote); + /** Populate quote with information about billing and shipping addresses*/ + $this->_checkout->returnFromPaypal($tokenId, $payerId); + if ($this->_checkout->canSkipOrderReviewStep()) { + $this->_checkout->place($tokenId); + $order = $this->_checkout->getOrder(); + /** "last successful quote" */ + $this->_getCheckoutSession()->setLastQuoteId($quote->getId())->setLastSuccessQuoteId($quote->getId()); + + $this->_getCheckoutSession()->setLastOrderId($order->getId()) + ->setLastRealOrderId($order->getIncrementId()) + ->setLastOrderStatus($order->getStatus()); + + $this->_eventManager->dispatch( + 'paypal_express_place_order_success', + [ + 'order' => $order, + 'quote' => $quote + ] + ); + $responseContent['redirectUrl'] = $this->urlBuilder->getUrl('checkout/onepage/success/'); + } else { + $responseContent['redirectUrl'] = $this->urlBuilder->getUrl('paypal/express/review'); + $this->_checkoutSession->setQuoteId($quote->getId()); + } + } catch (ApiProcessableException $e) { + $responseContent['success'] = false; + $responseContent['error_message'] = $e->getUserMessage(); + } catch (\Magento\Framework\Exception\LocalizedException $e) { + $responseContent['success'] = false; + $responseContent['error_message'] = $e->getMessage(); + } catch (\Exception $e) { + $responseContent['success'] = false; + $responseContent['error_message'] = __('We can\'t process Express Checkout approval.'); + } + + return $controllerResult->setData($responseContent); + } +} diff --git a/app/code/Magento/Paypal/Controller/Express/Start.php b/app/code/Magento/Paypal/Controller/Express/Start.php index e0a3c5381be5..b381c413071a 100644 --- a/app/code/Magento/Paypal/Controller/Express/Start.php +++ b/app/code/Magento/Paypal/Controller/Express/Start.php @@ -6,7 +6,9 @@ */ namespace Magento\Paypal\Controller\Express; -class Start extends \Magento\Paypal\Controller\Express\AbstractExpress\Start +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Start extends \Magento\Paypal\Controller\Express\AbstractExpress\Start implements HttpGetActionInterface { /** * Config mode type diff --git a/app/code/Magento/Paypal/Controller/Ipn/Index.php b/app/code/Magento/Paypal/Controller/Ipn/Index.php index 8a7b75558150..a879266bc191 100644 --- a/app/code/Magento/Paypal/Controller/Ipn/Index.php +++ b/app/code/Magento/Paypal/Controller/Ipn/Index.php @@ -4,16 +4,21 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Paypal\Controller\Ipn; use Magento\Framework\App\CsrfAwareActionInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\Request\InvalidRequestException; use Magento\Framework\App\RequestInterface; use Magento\Framework\Exception\RemoteServiceUnavailableException; +use Magento\Sales\Model\OrderFactory; /** * Unified IPN controller for all supported PayPal methods + * + * @SuppressWarnings(PHPMD.AllPurposeAction) */ class Index extends \Magento\Framework\App\Action\Action implements CsrfAwareActionInterface { @@ -27,18 +32,26 @@ class Index extends \Magento\Framework\App\Action\Action implements CsrfAwareAct */ protected $_ipnFactory; + /** + * @var OrderFactory + */ + private $orderFactory; + /** * @param \Magento\Framework\App\Action\Context $context * @param \Magento\Paypal\Model\IpnFactory $ipnFactory * @param \Psr\Log\LoggerInterface $logger + * @param OrderFactory $orderFactory */ public function __construct( \Magento\Framework\App\Action\Context $context, \Magento\Paypal\Model\IpnFactory $ipnFactory, - \Psr\Log\LoggerInterface $logger + \Psr\Log\LoggerInterface $logger, + OrderFactory $orderFactory = null ) { $this->_logger = $logger; $this->_ipnFactory = $ipnFactory; + $this->orderFactory = $orderFactory ?: ObjectManager::getInstance()->get(OrderFactory::class); parent::__construct($context); } @@ -63,7 +76,6 @@ public function validateForCsrf(RequestInterface $request): ?bool * Instantiate IPN model and pass IPN request to it * * @return void - * @SuppressWarnings(PHPMD.ExitExpression) */ public function execute() { @@ -74,10 +86,18 @@ public function execute() try { $data = $this->getRequest()->getPostValue(); $this->_ipnFactory->create(['data' => $data])->processIpnRequest(); + $incrementId = $this->getRequest()->getPostValue()['invoice']; + $this->_eventManager->dispatch( + 'paypal_checkout_success', + [ + 'order' => $this->orderFactory->create()->loadByIncrementId($incrementId) + ] + ); } catch (RemoteServiceUnavailableException $e) { $this->_logger->critical($e); $this->getResponse()->setStatusHeader(503, '1.1', 'Service Unavailable')->sendResponse(); /** @todo eliminate usage of exit statement */ + // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage exit; } catch (\Exception $e) { $this->_logger->critical($e); diff --git a/app/code/Magento/Paypal/Controller/Payflow/ReturnUrl.php b/app/code/Magento/Paypal/Controller/Payflow/ReturnUrl.php index 56a5da40edb8..d2a14febe54d 100644 --- a/app/code/Magento/Paypal/Controller/Payflow/ReturnUrl.php +++ b/app/code/Magento/Paypal/Controller/Payflow/ReturnUrl.php @@ -1,11 +1,11 @@ getIncrementId()) { if ($this->checkOrderState($order)) { $redirectBlock->setData('goto_success_page', true); - $this->_eventManager->dispatch('paypal_checkout_success', ['order' => $order]); } else { if ($this->checkPaymentMethod($order)) { $gotoSection = $this->_cancelPayment((string)$this->getRequest()->getParam('RESPMSG')); diff --git a/app/code/Magento/Paypal/Controller/Payflowexpress/Start.php b/app/code/Magento/Paypal/Controller/Payflowexpress/Start.php index a6fbcfd0239b..4615320c9455 100644 --- a/app/code/Magento/Paypal/Controller/Payflowexpress/Start.php +++ b/app/code/Magento/Paypal/Controller/Payflowexpress/Start.php @@ -6,7 +6,9 @@ */ namespace Magento\Paypal\Controller\Payflowexpress; -class Start extends \Magento\Paypal\Controller\Express\AbstractExpress\Start +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Start extends \Magento\Paypal\Controller\Express\AbstractExpress\Start implements HttpGetActionInterface { /** * Config mode type diff --git a/app/code/Magento/Paypal/Controller/Transparent/RequestSecureToken.php b/app/code/Magento/Paypal/Controller/Transparent/RequestSecureToken.php index 2efae34a9645..e2701bab1f06 100644 --- a/app/code/Magento/Paypal/Controller/Transparent/RequestSecureToken.php +++ b/app/code/Magento/Paypal/Controller/Transparent/RequestSecureToken.php @@ -5,12 +5,14 @@ */ namespace Magento\Paypal\Controller\Transparent; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\App\Action\Context; use Magento\Framework\Controller\Result\Json; use Magento\Framework\Controller\Result\JsonFactory; 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; @@ -21,7 +23,7 @@ * @package Magento\Paypal\Controller\Transparent * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class RequestSecureToken extends \Magento\Framework\App\Action\Action +class RequestSecureToken extends \Magento\Framework\App\Action\Action implements HttpPostActionInterface { /** * @var JsonFactory @@ -39,7 +41,7 @@ class RequestSecureToken extends \Magento\Framework\App\Action\Action private $secureTokenService; /** - * @var SessionManager + * @var SessionManager|SessionManagerInterface */ private $sessionManager; @@ -55,6 +57,7 @@ class RequestSecureToken extends \Magento\Framework\App\Action\Action * @param SecureToken $secureTokenService * @param SessionManager $sessionManager * @param Transparent $transparent + * @param SessionManagerInterface|null $sessionInterface */ public function __construct( Context $context, @@ -62,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); } @@ -82,7 +86,11 @@ public function execute() /** @var Quote $quote */ $quote = $this->sessionManager->getQuote(); - if (!$quote or !$quote instanceof Quote) { + if (!$quote || !$quote instanceof Quote) { + return $this->getErrorResponse(); + } + + if (!$this->transparent->isActive($quote->getStoreId())) { return $this->getErrorResponse(); } @@ -106,6 +114,8 @@ public function execute() } /** + * Get error response. + * * @return Json */ private function getErrorResponse() diff --git a/app/code/Magento/Paypal/CustomerData/BillingAgreement.php b/app/code/Magento/Paypal/CustomerData/BillingAgreement.php index 2c4cdf55bff9..a6304f32197b 100644 --- a/app/code/Magento/Paypal/CustomerData/BillingAgreement.php +++ b/app/code/Magento/Paypal/CustomerData/BillingAgreement.php @@ -79,7 +79,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getSectionData() { @@ -93,7 +93,7 @@ public function getSectionData() [\Magento\Paypal\Model\Express\Checkout::PAYMENT_INFO_TRANSPORT_BILLING_AGREEMENT => 1] ) ), - 'confirmMessage' => $this->escaper->escapeJs( + 'confirmMessage' => $this->escaper->escapeHtml( __('Would you like to sign a billing agreement to streamline further purchases with PayPal?') ) ]; diff --git a/app/code/Magento/Paypal/Model/AbstractConfig.php b/app/code/Magento/Paypal/Model/AbstractConfig.php index 3b0f7b974829..41f122ed9b3c 100644 --- a/app/code/Magento/Paypal/Model/AbstractConfig.php +++ b/app/code/Magento/Paypal/Model/AbstractConfig.php @@ -9,7 +9,6 @@ use Magento\Payment\Model\Method\ConfigInterface; use Magento\Payment\Model\MethodInterface; use Magento\Store\Model\ScopeInterface; -use Magento\Paypal\Model\Config; use Magento\Framework\App\ObjectManager; /** @@ -134,7 +133,7 @@ public function setStoreId($storeId) * Returns payment configuration value * * @param string $key - * @param null $storeId + * @param null|int $storeId * @return null|string * * @SuppressWarnings(PHPMD.UnusedFormalParameter) @@ -224,15 +223,26 @@ protected function _prepareValue($key, $value) */ public function shouldUseUnilateralPayments() { - return $this->getValue('business_account') && !$this->isWppApiAvailabe(); + return $this->getValue('business_account') && !$this->isWppApiAvailable(); } /** * Check whether WPP API credentials are available for this method * + * @deprecated * @return bool */ public function isWppApiAvailabe() + { + return $this->isWppApiAvailable(); + } + + /** + * Check whether WPP API credentials are available for this method + * + * @return bool + */ + public function isWppApiAvailable() { return $this->getValue('api_username') && $this->getValue('api_password') @@ -243,7 +253,7 @@ public function isWppApiAvailabe() /** * Check whether method available for checkout or not * - * @param null $methodCode + * @param null|string $methodCode * * @return bool */ @@ -282,11 +292,15 @@ public function isMethodActive($method) break; case Config::METHOD_WPS_BML: case Config::METHOD_WPP_BML: - $isEnabled = $this->_scopeConfig->isSetFlag( - 'payment/' . Config::METHOD_WPS_BML .'/active', + $disabledFunding = $this->_scopeConfig->getValue( + 'payment/paypal_express/disable_funding_options', ScopeInterface::SCOPE_STORE, $this->_storeId - ) + ); + $isExpressCreditEnabled = $disabledFunding + ? strpos($disabledFunding, 'CREDIT') === false + : true; + $isEnabled = $isExpressCreditEnabled || $this->_scopeConfig->isSetFlag( 'payment/' . Config::METHOD_WPP_BML .'/active', ScopeInterface::SCOPE_STORE, diff --git a/app/code/Magento/Paypal/Model/Api/AbstractApi.php b/app/code/Magento/Paypal/Model/Api/AbstractApi.php index 6f578e44eae4..73a4f6914799 100644 --- a/app/code/Magento/Paypal/Model/Api/AbstractApi.php +++ b/app/code/Magento/Paypal/Model/Api/AbstractApi.php @@ -454,6 +454,7 @@ protected function _exportLineItems(array &$request, $i = 0) /** * Prepare shipping options request + * * Returns false if there are no shipping options * * @param array &$request @@ -520,7 +521,7 @@ protected function _getDataOrConfig($key, $default = null) } /** - * region_id workaround: PayPal requires state code, try to find one in the address + * Region_id workaround: PayPal requires state code, try to find one in the address * * @param \Magento\Framework\DataObject $address * @return string @@ -577,6 +578,7 @@ protected function _buildQuery($request) /** * Filter qty in API calls + * * Paypal note: The value for quantity must be a positive integer. Null, zero, or negative numbers are not allowed. * * @param float|string|int $value @@ -584,7 +586,7 @@ protected function _buildQuery($request) */ protected function _filterQty($value) { - return intval($value); + return (int)$value; } /** diff --git a/app/code/Magento/Paypal/Model/Api/PayflowNvp.php b/app/code/Magento/Paypal/Model/Api/PayflowNvp.php index 6373c6d99fb1..b769cbe43645 100644 --- a/app/code/Magento/Paypal/Model/Api/PayflowNvp.php +++ b/app/code/Magento/Paypal/Model/Api/PayflowNvp.php @@ -136,6 +136,9 @@ class PayflowNvp extends \Magento\Paypal\Model\Api\Nvp 'CVV2MATCH' => 'cvv2_check_result', 'USERSELECTEDFUNDINGSOURCE' => 'funding_source', + + 'NOSHIPPING' => 'suppress_shipping', + 'REQBILLINGADDRESS' => 'require_billing_address', ]; /** @@ -248,6 +251,8 @@ class PayflowNvp extends \Magento\Paypal\Model\Api\Nvp 'PAYFLOWCOLOR', 'LOCALECODE', 'USERSELECTEDFUNDINGSOURCE', + 'NOSHIPPING', + 'REQBILLINGADDRESS', ]; /** @@ -727,6 +732,7 @@ protected function _prepareExpressCheckoutCallRequest(&$requestFields) /** * Additional response processing. + * * Hack to cut off length from API type response params. * * @param array $response @@ -784,7 +790,8 @@ protected function _exportLineItems(array &$request, $i = 0) } /** - * Set specific data when negative line item case + * Set specific data when negative line item case. + * * @return void */ protected function _setSpecificForNegativeLineItems() diff --git a/app/code/Magento/Paypal/Model/Billing/AbstractAgreement.php b/app/code/Magento/Paypal/Model/Billing/AbstractAgreement.php index 2ebe088d31d8..8965684d1085 100644 --- a/app/code/Magento/Paypal/Model/Billing/AbstractAgreement.php +++ b/app/code/Magento/Paypal/Model/Billing/AbstractAgreement.php @@ -6,7 +6,7 @@ namespace Magento\Paypal\Model\Billing; /** - * Billing Agreement abstaract class + * Billing Agreement abstract class */ abstract class AbstractAgreement extends \Magento\Framework\Model\AbstractModel { diff --git a/app/code/Magento/Paypal/Model/Config.php b/app/code/Magento/Paypal/Model/Config.php index 34e40ac7509d..9891f68bf874 100644 --- a/app/code/Magento/Paypal/Model/Config.php +++ b/app/code/Magento/Paypal/Model/Config.php @@ -10,6 +10,7 @@ /** * Config model that is aware of all \Magento\Paypal payment methods + * * Works with PayPal-specific system configuration * @SuppressWarnings(PHPMD.ExcessivePublicCount) @@ -632,6 +633,7 @@ public function __construct( /** * Check whether method available for checkout or not + * * Logic based on merchant country, methods dependence * * @param string|null $methodCode @@ -677,7 +679,7 @@ public function isMethodAvailable($methodCode = null) } break; case self::METHOD_BILLING_AGREEMENT: - $result = $this->isWppApiAvailabe(); + $result = $this->isWppApiAvailable(); break; } return $result; @@ -723,6 +725,7 @@ public function getMerchantCountry() /** * Check whether method supported for specified country or not + * * Use $_methodCode and merchant country by default * * @param string|null $method @@ -896,6 +899,7 @@ public function getExpressCheckoutEditUrl($token) /** * Get url for additional actions that PayPal may require customer to do after placing the order. + * * For instance, redirecting customer to bank for payment confirmation. * * @param string $token @@ -957,6 +961,7 @@ public function areButtonsDynamic() /** * Express checkout shortcut pic URL getter + * * PayPal will ignore "pal", if there is no total amount specified * * @param string $localeCode @@ -996,6 +1001,7 @@ public function getExpressCheckoutInContextImageUrl($localeCode) /** * Get PayPal "mark" image URL + * * Supposed to be used on payment methods selection * $staticSize is applicable for static images only * @@ -1032,6 +1038,7 @@ public function getPaymentMarkImageUrl($localeCode, $orderTotal = null, $pal = n /** * Get "What Is PayPal" localized URL + * * Supposed to be used with "mark" as popup window * * @param \Magento\Framework\Locale\ResolverInterface $localeResolver @@ -1262,6 +1269,7 @@ public function getExpressCheckoutBASignupOptions() /** * Whether to ask customer to create billing agreements + * * Unilateral payments are incompatible with the billing agreements * * @return bool @@ -1376,6 +1384,7 @@ public function exportExpressCheckoutStyleSettings(\Magento\Framework\DataObject /** * Dynamic PayPal image URL getter + * * Also can render dynamic Acceptance Mark * * @param string $type @@ -1626,6 +1635,7 @@ protected function _mapWpukFieldset($fieldName) * * @param string $fieldName * @return string|null + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ protected function _mapGenericStyleFieldset($fieldName) { @@ -1636,9 +1646,10 @@ protected function _mapGenericStyleFieldset($fieldName) case 'paypal_hdrbackcolor': case 'paypal_hdrbordercolor': case 'paypal_payflowcolor': + case 'disable_funding_options': return "paypal/style/{$fieldName}"; default: - return null; + return $this->mapButtonStyles($fieldName); } } @@ -1688,6 +1699,36 @@ protected function _mapMethodFieldset($fieldName) } } + /** + * Map PayPal button style config fields + * + * @param string $fieldName + * @return null|string + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + private function mapButtonStyles(string $fieldName) + { + $page = substr($fieldName, 0, (int)strpos($fieldName, '_page_button_')); + + if (!$page) { + return null; + } + + switch ($fieldName) { + case "{$page}_page_button_customize": + case "{$page}_page_button_layout": + case "{$page}_page_button_size": + case "{$page}_page_button_color": + case "{$page}_page_button_shape": + case "{$page}_page_button_label": + case "{$page}_page_button_mx_installment_period": + case "{$page}_page_button_br_installment_period": + return "paypal/style/{$fieldName}"; + default: + return null; + } + } + /** * Payment API authentication methods source getter * @@ -1725,6 +1766,7 @@ public function getBmlPublisherId() /** * Get Display option from stored config + * * @param string $section * * @return mixed @@ -1752,6 +1794,7 @@ public function getBmlDisplay($section) /** * Get Position option from stored config + * * @param string $section * * @return mixed @@ -1767,6 +1810,7 @@ public function getBmlPosition($section) /** * Get Size option from stored config + * * @param string $section * * @return mixed diff --git a/app/code/Magento/Paypal/Model/Config/Rules/Converter.php b/app/code/Magento/Paypal/Model/Config/Rules/Converter.php index 2bae810a5fde..2baedaa38f5a 100644 --- a/app/code/Magento/Paypal/Model/Config/Rules/Converter.php +++ b/app/code/Magento/Paypal/Model/Config/Rules/Converter.php @@ -63,6 +63,7 @@ protected function createEvents(\DOMElement $node) if ($this->hasNodeElement($child)) { $result[$child->getAttribute('name')] = [ 'value' => $child->getAttribute('value'), + 'include' => $child->getAttribute('include'), 'predicate' => $this->createPredicate($child), ]; } diff --git a/app/code/Magento/Paypal/Model/Config/Structure/Element/FieldPlugin.php b/app/code/Magento/Paypal/Model/Config/Structure/Element/FieldPlugin.php index c2056aea08c0..5d5db0128b1e 100644 --- a/app/code/Magento/Paypal/Model/Config/Structure/Element/FieldPlugin.php +++ b/app/code/Magento/Paypal/Model/Config/Structure/Element/FieldPlugin.php @@ -5,7 +5,6 @@ */ namespace Magento\Paypal\Model\Config\Structure\Element; -use Magento\Framework\App\RequestInterface; use Magento\Config\Model\Config\Structure\Element\Field as FieldConfigStructure; use Magento\Paypal\Model\Config\StructurePlugin as ConfigStructurePlugin; @@ -14,19 +13,6 @@ */ class FieldPlugin { - /** - * @var RequestInterface - */ - private $request; - - /** - * @param RequestInterface $request - */ - public function __construct(RequestInterface $request) - { - $this->request = $request; - } - /** * Get original configPath (not changed by PayPal configuration inheritance) * @@ -36,7 +22,7 @@ public function __construct(RequestInterface $request) */ public function afterGetConfigPath(FieldConfigStructure $subject, $result) { - if (!$result && $this->request->getParam('section') == 'payment') { + if (!$result && strpos($subject->getPath(), 'payment_') === 0) { $result = preg_replace( '@^(' . implode('|', ConfigStructurePlugin::getPaypalConfigCountries(true)) . ')/@', 'payment/', diff --git a/app/code/Magento/Paypal/Model/Express.php b/app/code/Magento/Paypal/Model/Express.php index 4684abdc9be6..e52a85da3e82 100644 --- a/app/code/Magento/Paypal/Model/Express.php +++ b/app/code/Magento/Paypal/Model/Express.php @@ -44,7 +44,7 @@ class Express extends \Magento\Payment\Model\Method\AbstractMethod * * @var bool */ - protected $_isGateway = false; + protected $_isGateway = true; /** * Availability option @@ -275,6 +275,7 @@ protected function _setApiProcessableErrors() /** * Store setter + * * Also updates store ID in config object * * @param \Magento\Store\Model\Store|int $store @@ -334,6 +335,7 @@ public function getConfigPaymentAction() /** * Check whether payment method can be used + * * @param \Magento\Quote\Api\Data\CartInterface|Quote|null $quote * @return bool */ @@ -432,8 +434,8 @@ public function void(\Magento\Payment\Model\InfoInterface $payment) public function capture(\Magento\Payment\Model\InfoInterface $payment, $amount) { $authorizationTransaction = $payment->getAuthorizationTransaction(); - $authorizationPeriod = abs(intval($this->getConfigData('authorization_honor_period'))); - $maxAuthorizationNumber = abs(intval($this->getConfigData('child_authorization_number'))); + $authorizationPeriod = abs((int)$this->getConfigData('authorization_honor_period')); + $maxAuthorizationNumber = abs((int)$this->getConfigData('child_authorization_number')); $order = $payment->getOrder(); $isAuthorizationCreated = false; @@ -545,6 +547,7 @@ public function cancel(\Magento\Payment\Model\InfoInterface $payment) /** * Whether payment can be reviewed + * * @return bool */ public function canReviewPayment() @@ -601,6 +604,8 @@ public function fetchTransactionInfo(\Magento\Payment\Model\InfoInterface $payme } /** + * Returns api instance + * * @return Api\Nvp */ public function getApi() @@ -717,6 +722,7 @@ protected function _importToPayment($api, $payment) /** * Check void availability + * * @return bool * @throws \Magento\Framework\Exception\LocalizedException * @internal param \Magento\Framework\DataObject $payment @@ -750,7 +756,7 @@ public function canCapture() return false; } - $orderValidPeriod = abs(intval($this->getConfigData('order_valid_period'))); + $orderValidPeriod = abs((int)$this->getConfigData('order_valid_period')); $dateCompass = new \DateTime($orderTransaction->getCreatedAt()); $dateCompass->modify('+' . $orderValidPeriod . ' days'); @@ -805,7 +811,7 @@ protected function _callDoAuthorize($amount, $payment, $parentTransactionId) */ protected function _isTransactionExpired(Transaction $transaction, $period) { - $period = intval($period); + $period = (int)$period; if (0 == $period) { return true; } diff --git a/app/code/Magento/Paypal/Model/Express/Checkout.php b/app/code/Magento/Paypal/Model/Express/Checkout.php index 16bcac300de9..72f166e8d07c 100644 --- a/app/code/Magento/Paypal/Model/Express/Checkout.php +++ b/app/code/Magento/Paypal/Model/Express/Checkout.php @@ -17,15 +17,17 @@ /** * Wrapper that performs Paypal Express and Checkout communication - * Use current Paypal Express method instance + * * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class Checkout { /** * Cache ID prefix for "pal" lookup + * * @var string */ const PAL_CACHE_ID = 'paypal_express_checkout_pal'; @@ -367,6 +369,7 @@ public function __construct( /** * Checkout with PayPal image URL getter + * * Spares API calls of getting "pal" variable, by putting it into cache per store view * * @return string @@ -599,15 +602,17 @@ public function canSkipOrderReviewStep() /** * Update quote when returned from PayPal - * rewrite billing address by paypal - * save old billing address for new customer + * + * Rewrite billing address by paypal, save old billing address for new customer, and * export shipping address in case address absence * * @param string $token + * @param string|null $payerIdentifier * @return void * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) */ - public function returnFromPaypal($token) + public function returnFromPaypal($token, string $payerIdentifier = null) { $this->_getApi() ->setToken($token) @@ -616,14 +621,15 @@ public function returnFromPaypal($token) $this->ignoreAddressValidation(); + // check if we came from the Express Checkout button + $isButton = (bool)$quote->getPayment()->getAdditionalInformation(self::PAYMENT_INFO_BUTTON); + // import shipping address $exportedShippingAddress = $this->_getApi()->getExportedShippingAddress(); if (!$quote->getIsVirtual()) { $shippingAddress = $quote->getShippingAddress(); if ($shippingAddress) { - if ($exportedShippingAddress - && $quote->getPayment()->getAdditionalInformation(self::PAYMENT_INFO_BUTTON) == 1 - ) { + if ($exportedShippingAddress && $isButton) { $this->_setExportedAddressData($shippingAddress, $exportedShippingAddress); // PayPal doesn't provide detailed shipping info: prefix, middlename, lastname, suffix $shippingAddress->setPrefix(null); @@ -651,12 +657,11 @@ public function returnFromPaypal($token) } // import billing address - $portBillingFromShipping = $quote->getPayment()->getAdditionalInformation(self::PAYMENT_INFO_BUTTON) == 1 - && $this->_config->getValue( - 'requireBillingAddress' - ) != \Magento\Paypal\Model\Config::REQUIRE_BILLING_ADDRESS_ALL - && !$quote->isVirtual(); - if ($portBillingFromShipping) { + $requireBillingAddress = (int)$this->_config->getValue( + 'requireBillingAddress' + ) === \Magento\Paypal\Model\Config::REQUIRE_BILLING_ADDRESS_ALL; + + if ($isButton && !$requireBillingAddress && !$quote->isVirtual()) { $billingAddress = clone $shippingAddress; $billingAddress->unsAddressId()->unsAddressType()->setCustomerAddressId(null); $data = $billingAddress->getData(); @@ -664,11 +669,17 @@ public function returnFromPaypal($token) $quote->getBillingAddress()->addData($data); $quote->getShippingAddress()->setSameAsBilling(1); } else { - $billingAddress = $quote->getBillingAddress(); + $billingAddress = $quote->getBillingAddress()->setCustomerAddressId(null); } $exportedBillingAddress = $this->_getApi()->getExportedBillingAddress(); - $this->_setExportedAddressData($billingAddress, $exportedBillingAddress); + // Since country is required field for billing and shipping address, + // we consider the address information to be empty if country is empty. + $isEmptyAddress = ($billingAddress->getCountryId() === null); + + if ($requireBillingAddress || $isEmptyAddress) { + $this->_setExportedAddressData($billingAddress, $exportedBillingAddress); + } $billingAddress->setCustomerNote($exportedBillingAddress->getData('note')); $quote->setBillingAddress($billingAddress); $quote->setCheckoutMethod($this->getCheckoutMethod()); @@ -677,7 +688,8 @@ public function returnFromPaypal($token) $payment = $quote->getPayment(); $payment->setMethod($this->_methodType); $this->_paypalInfo->importToPayment($this->_getApi(), $payment); - $payment->setAdditionalInformation(self::PAYMENT_INFO_TRANSPORT_PAYER_ID, $this->_getApi()->getPayerId()) + $payerId = $payerIdentifier ? : $this->_getApi()->getPayerId(); + $payment->setAdditionalInformation(self::PAYMENT_INFO_TRANSPORT_PAYER_ID, $payerId) ->setAdditionalInformation(self::PAYMENT_INFO_TRANSPORT_TOKEN, $token); $quote->collectTotals(); $this->quoteRepository->save($quote); @@ -904,17 +916,6 @@ public function getCheckoutMethod() */ protected function _setExportedAddressData($address, $exportedAddress) { - // Exported data is more priority if we came from Express Checkout button - $isButton = (bool)$this->_quote->getPayment()->getAdditionalInformation(self::PAYMENT_INFO_BUTTON); - - // Since country is required field for billing and shipping address, - // we consider the address information to be empty if country is empty. - $isEmptyAddress = ($address->getCountryId() === null); - - if (!$isButton && !$isEmptyAddress) { - return; - } - foreach ($exportedAddress->getExportedKeys() as $key) { $data = $exportedAddress->getData($key); if (!empty($data)) { @@ -951,6 +952,8 @@ protected function _setBillingAgreementRequest() } /** + * Get api + * * @return \Magento\Paypal\Model\Api\Nvp */ protected function _getApi() @@ -963,8 +966,9 @@ protected function _getApi() /** * Attempt to collect address shipping rates and return them for further usage in instant update API - * Returns empty array if it was impossible to obtain any shipping rate - * If there are shipping rates obtained, the method must return one of them as default. + * + * Returns empty array if it was impossible to obtain any shipping rate and + * if there are shipping rates obtained, the method must return one of them as default. * * @param Address $address * @param bool $mayReturnEmpty @@ -1048,22 +1052,20 @@ protected function _prepareShippingOptions(Address $address, $mayReturnEmpty = f * Compare two shipping options based on their amounts * * This function is used as a callback comparison function in shipping options sorting process - * @see self::_prepareShippingOptions() * + * @see self::_prepareShippingOptions() * @param \Magento\Framework\DataObject $option1 * @param \Magento\Framework\DataObject $option2 * @return int */ protected static function cmpShippingOptions(DataObject $option1, DataObject $option2) { - if ($option1->getAmount() == $option2->getAmount()) { - return 0; - } - return ($option1->getAmount() < $option2->getAmount()) ? -1 : 1; + return $option1->getAmount() <=> $option2->getAmount(); } /** * Try to find whether the code provided by PayPal corresponds to any of possible shipping rates + * * This method was created only because PayPal has issues with returning the selected code. * If in future the issue is fixed, we don't need to attempt to match it. It would be enough to set the method code * before collecting shipping rates @@ -1074,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 @@ -1089,6 +1092,7 @@ protected function _matchShippingMethodCode(Address $address, $selectedCode) /** * Create payment redirect url + * * @param bool|null $button * @param string $token * @return void @@ -1112,6 +1116,7 @@ public function getCustomerSession() /** * Set shipping options to api + * * @param \Magento\Paypal\Model\Cart $cart * @param \Magento\Quote\Model\Quote\Address|null $address * @return void diff --git a/app/code/Magento/Paypal/Model/ExpressConfigProvider.php b/app/code/Magento/Paypal/Model/ExpressConfigProvider.php index 518e8b12bfcd..c8adb137299f 100644 --- a/app/code/Magento/Paypal/Model/ExpressConfigProvider.php +++ b/app/code/Magento/Paypal/Model/ExpressConfigProvider.php @@ -66,14 +66,20 @@ class ExpressConfigProvider implements ConfigProviderInterface protected $urlBuilder; /** - * Constructor - * + * @var SmartButtonConfig + */ + private $smartButtonConfig; + + /** + * ExpressConfigProvider constructor. * @param ConfigFactory $configFactory * @param ResolverInterface $localeResolver * @param CurrentCustomer $currentCustomer * @param PaypalHelper $paypalHelper * @param PaymentHelper $paymentHelper * @param UrlInterface $urlBuilder + * @param SmartButtonConfig|null $smartButtonConfig + * @throws \Magento\Framework\Exception\LocalizedException */ public function __construct( ConfigFactory $configFactory, @@ -81,7 +87,8 @@ public function __construct( CurrentCustomer $currentCustomer, PaypalHelper $paypalHelper, PaymentHelper $paymentHelper, - UrlInterface $urlBuilder + UrlInterface $urlBuilder, + SmartButtonConfig $smartButtonConfig ) { $this->localeResolver = $localeResolver; $this->config = $configFactory->create(); @@ -89,6 +96,7 @@ public function __construct( $this->paypalHelper = $paypalHelper; $this->paymentHelper = $paymentHelper; $this->urlBuilder = $urlBuilder; + $this->smartButtonConfig = $smartButtonConfig; foreach ($this->methodCodes as $code) { $this->methods[$code] = $this->paymentHelper->getMethodInstance($code); @@ -96,7 +104,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getConfig() { @@ -123,15 +131,17 @@ public function getConfig() $config['payment']['paypalExpress']['inContextConfig'] = [ 'inContextId' => self::IN_CONTEXT_BUTTON_ID, 'merchantId' => $this->config->getValue('merchant_id'), - 'path' => $this->urlBuilder->getUrl('paypal/express/gettoken', ['_secure' => true]), - 'clientConfig' => [ - 'environment' => ((int) $this->config->getValue('sandbox_flag') ? 'sandbox' : 'production'), - 'locale' => $locale, - 'button' => [ - self::IN_CONTEXT_BUTTON_ID - ] + ]; + $clientConfig = [ + 'button' => [ + self::IN_CONTEXT_BUTTON_ID ], + 'getTokenUrl' => $this->urlBuilder->getUrl('paypal/express/getTokenData'), + 'onAuthorizeUrl' => $this->urlBuilder->getUrl('paypal/express/onAuthorization'), + 'onCancelUrl' => $this->urlBuilder->getUrl('paypal/express/cancel') ]; + $clientConfig = array_replace_recursive($clientConfig, $this->smartButtonConfig->getConfig('checkout')); + $config['payment']['paypalExpress']['inContextConfig']['clientConfig'] = $clientConfig; } foreach ($this->methodCodes as $code) { @@ -146,6 +156,8 @@ public function getConfig() } /** + * Return setting value for in context checkout + * * @return bool */ protected function isInContextCheckout() diff --git a/app/code/Magento/Paypal/Model/Payflow/Service/Request/SecureToken.php b/app/code/Magento/Paypal/Model/Payflow/Service/Request/SecureToken.php index da5599984b70..8a2825a16d33 100644 --- a/app/code/Magento/Paypal/Model/Payflow/Service/Request/SecureToken.php +++ b/app/code/Magento/Paypal/Model/Payflow/Service/Request/SecureToken.php @@ -64,6 +64,7 @@ public function requestToken(Quote $quote) $request->setTrxtype(Payflowpro::TRXTYPE_AUTH_ONLY); $request->setVerbosity('HIGH'); $request->setAmt(0); + $request->setCurrency($quote->getBaseCurrencyCode()); $request->setCreatesecuretoken('Y'); $request->setSecuretokenid($this->mathRandom->getUniqueHash()); $request->setReturnurl($this->url->getUrl('paypal/transparent/response')); diff --git a/app/code/Magento/Paypal/Model/Payflow/Service/Response/Transaction.php b/app/code/Magento/Paypal/Model/Payflow/Service/Response/Transaction.php index 06a8a5b680bf..7143576b71a0 100644 --- a/app/code/Magento/Paypal/Model/Payflow/Service/Response/Transaction.php +++ b/app/code/Magento/Paypal/Model/Payflow/Service/Response/Transaction.php @@ -3,9 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Paypal\Model\Payflow\Service\Response; use Magento\Framework\DataObject; +use Magento\Framework\Intl\DateTimeFactory; use Magento\Payment\Model\Method\Logger; use Magento\Paypal\Model\Payflow\Service\Response\Handler\HandlerInterface; use Magento\Framework\Session\Generic; @@ -18,6 +21,7 @@ /** * Class Transaction + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Transaction { @@ -51,6 +55,11 @@ class Transaction */ private $logger; + /** + * @var DateTimeFactory + */ + private $dateTimeFactory; + /** * @param Generic $sessionTransparent * @param CartRepositoryInterface $quoteRepository @@ -58,6 +67,7 @@ class Transaction * @param PaymentMethodManagementInterface $paymentManagement * @param HandlerInterface $errorHandler * @param Logger $logger + * @param DateTimeFactory $dateTimeFactory */ public function __construct( Generic $sessionTransparent, @@ -65,7 +75,8 @@ public function __construct( Transparent $transparent, PaymentMethodManagementInterface $paymentManagement, HandlerInterface $errorHandler, - Logger $logger + Logger $logger, + DateTimeFactory $dateTimeFactory ) { $this->sessionTransparent = $sessionTransparent; $this->quoteRepository = $quoteRepository; @@ -73,6 +84,7 @@ public function __construct( $this->paymentManagement = $paymentManagement; $this->errorHandler = $errorHandler; $this->logger = $logger; + $this->dateTimeFactory = $dateTimeFactory; } /** @@ -114,8 +126,45 @@ public function savePaymentInQuote($response) $payment->setData(OrderPaymentInterface::CC_TYPE, $response->getData(OrderPaymentInterface::CC_TYPE)); $payment->setAdditionalInformation(Payflowpro::PNREF, $response->getData(Payflowpro::PNREF)); + $expDate = $response->getData('expdate'); + $expMonth = $this->getCcExpMonth($expDate); + $payment->setCcExpMonth($expMonth); + $expYear = $this->getCcExpYear($expDate); + $payment->setCcExpYear($expYear); + $this->errorHandler->handle($payment, $response); $this->paymentManagement->set($quote->getId(), $payment); } + + /** + * Extracts expiration month from PayPal response expiration date. + * + * @param string $expDate format {MMYY} + * @return int + */ + private function getCcExpMonth(string $expDate): int + { + return (int)substr($expDate, 0, 2); + } + + /** + * Extracts expiration year from PayPal response expiration date. + * + * @param string $expDate format {MMYY} + * @return int + */ + private function getCcExpYear(string $expDate): int + { + $last2YearDigits = (int)substr($expDate, 2, 2); + $currentDate = $this->dateTimeFactory->create('now', new \DateTimeZone('UTC')); + $first2YearDigits = (int)substr($currentDate->format('Y'), 0, 2); + + // case when credit card expires at next century + if ((int)$currentDate->format('y') > $last2YearDigits) { + $first2YearDigits++; + } + + return 100 * $first2YearDigits + $last2YearDigits; + } } diff --git a/app/code/Magento/Paypal/Model/Payflowpro.php b/app/code/Magento/Paypal/Model/Payflowpro.php index b5fdaf4ae9fd..2ba72c4b26bd 100644 --- a/app/code/Magento/Paypal/Model/Payflowpro.php +++ b/app/code/Magento/Paypal/Model/Payflowpro.php @@ -420,6 +420,7 @@ public function capture(\Magento\Payment\Model\InfoInterface $payment, $amount) $request->setTrxtype(self::TRXTYPE_SALE); $request->setOrigid($payment->getAdditionalInformation(self::PNREF)); $payment->unsAdditionalInformation(self::PNREF); + $request->setData('currency', $payment->getOrder()->getBaseCurrencyCode()); } elseif ($payment->getParentTransactionId()) { $request = $this->buildBasicRequest(); $request->setOrigid($payment->getParentTransactionId()); @@ -472,6 +473,7 @@ public function void(\Magento\Payment\Model\InfoInterface $payment) /** * Check void availability + * * @return bool * @throws \Magento\Framework\Exception\LocalizedException */ @@ -584,7 +586,7 @@ public function getConfig() } /** - * {inheritdoc} + * @inheritdoc */ public function postRequest(DataObject $request, ConfigInterface $config) { @@ -719,6 +721,8 @@ public function reviewPayment(InfoInterface $payment, $action) } /** + * Set billing address + * * @param DataObject $request * @param DataObject $billing * @@ -745,6 +749,8 @@ public function setBilling(DataObject $request, $billing) } /** + * Set shipping address + * * @param DataObject $request * @param DataObject $shipping * @@ -815,6 +821,8 @@ public function mapGatewayResponse(array $postData, DataObject $response) } /** + * Set transaction status + * * @param DataObject $payment * @param DataObject $response * @@ -848,6 +856,8 @@ public function setTransStatus($payment, $response) } /** + * Fill customer contacts + * * @param DataObject $order * @param DataObject $request * @return DataObject @@ -869,6 +879,7 @@ public function fillCustomerContacts(DataObject $order, DataObject $request) /** * Add order details to payment request + * * @param DataObject $request * @param Order $order * @return void @@ -883,7 +894,7 @@ public function addRequestOrderInfo(DataObject $request, Order $order) $orderIncrementId = $order->getIncrementId(); $request->setCustref($orderIncrementId) ->setInvnum($orderIncrementId) - ->setComment1($orderIncrementId); + ->setData('comment1', $orderIncrementId); } /** @@ -917,6 +928,8 @@ public function assignData(DataObject $data) } /** + * Make a transaction Inquiry Request + * * @param InfoInterface $payment * @param string $transactionId * @return DataObject diff --git a/app/code/Magento/Paypal/Model/Report/Settlement.php b/app/code/Magento/Paypal/Model/Report/Settlement.php index 5dc51518f0b1..462ca2f97942 100644 --- a/app/code/Magento/Paypal/Model/Report/Settlement.php +++ b/app/code/Magento/Paypal/Model/Report/Settlement.php @@ -237,6 +237,7 @@ public function beforeSave() /** * Goes to specified host/path and fetches reports from there. + * * Save reports to database. * * @param \Magento\Framework\Filesystem\Io\Sftp $connection @@ -446,11 +447,11 @@ private function formatDateTimeColumns($lineItem) */ private function formatAmountColumn($lineItem) { - return intval($lineItem) / 100; + return (int)$lineItem / 100; } /** - * Load report by unique key (accoutn + report date) + * Load report by unique key (account + report date) * * @return $this */ @@ -517,6 +518,7 @@ public function getFieldLabel($field) /** * Iterate through website configurations and collect all SFTP configurations + * * Filter config values if necessary * * @param bool $automaticMode Whether to skip settings with disabled Automatic Fetching or not diff --git a/app/code/Magento/Paypal/Model/SmartButtonConfig.php b/app/code/Magento/Paypal/Model/SmartButtonConfig.php new file mode 100644 index 000000000000..80a0d477216b --- /dev/null +++ b/app/code/Magento/Paypal/Model/SmartButtonConfig.php @@ -0,0 +1,154 @@ +localeResolver = $localeResolver; + $this->config = $configFactory->create(); + $this->config->setMethod(Config::METHOD_EXPRESS); + $this->defaultStyles = $defaultStyles; + $this->allowedFunding = $allowedFunding; + } + + /** + * Get smart button config + * + * @param string $page + * @return array + */ + public function getConfig(string $page): array + { + return [ + 'merchantId' => $this->config->getValue('merchant_id'), + 'environment' => ((int)$this->config->getValue('sandbox_flag') ? 'sandbox' : 'production'), + 'locale' => $this->localeResolver->getLocale(), + 'allowedFunding' => $this->getAllowedFunding($page), + 'disallowedFunding' => $this->getDisallowedFunding(), + 'styles' => $this->getButtonStyles($page) + ]; + } + + /** + * Returns disallowed funding from configuration + * + * @return array + */ + private function getDisallowedFunding(): array + { + $disallowedFunding = $this->config->getValue('disable_funding_options'); + return $disallowedFunding ? explode(',', $disallowedFunding) : []; + } + + /** + * Returns allowed funding + * + * @param string $page + * @return array + */ + private function getAllowedFunding(string $page): array + { + return array_values(array_diff($this->allowedFunding[$page], $this->getDisallowedFunding())); + } + + /** + * Returns button styles based on configuration + * + * @param string $page + * @return array + */ + private function getButtonStyles(string $page): array + { + $styles = $this->defaultStyles[$page]; + if ((boolean)$this->config->getValue("{$page}_page_button_customize")) { + $styles['layout'] = $this->config->getValue("{$page}_page_button_layout"); + $styles['size'] = $this->config->getValue("{$page}_page_button_size"); + $styles['color'] = $this->config->getValue("{$page}_page_button_color"); + $styles['shape'] = $this->config->getValue("{$page}_page_button_shape"); + $styles['label'] = $this->config->getValue("{$page}_page_button_label"); + + $styles = $this->updateStyles($styles, $page); + } + return $styles; + } + + /** + * Update styles based on locale and labels + * + * @param array $styles + * @param string $page + * @return array + */ + private function updateStyles(array $styles, string $page): array + { + $locale = $this->localeResolver->getLocale(); + + $installmentPeriodLocale = [ + 'en_MX' => 'mx', + 'es_MX' => 'mx', + 'en_BR' => 'br', + 'pt_BR' => 'br' + ]; + + // Credit label cannot be used with any custom color option or vertical layout. + if ($styles['label'] === 'credit') { + $styles['color'] = 'darkblue'; + $styles['layout'] = 'horizontal'; + } + + // Installment label is only available for specific locales + if ($styles['label'] === 'installment') { + if (array_key_exists($locale, $installmentPeriodLocale)) { + $styles['installmentperiod'] = (int)$this->config->getValue( + $page .'_page_button_' . $installmentPeriodLocale[$locale] . '_installment_period' + ); + } else { + $styles['label'] = 'paypal'; + } + } + + return $styles; + } +} diff --git a/app/code/Magento/Paypal/Model/System/Config/Source/ButtonStyles.php b/app/code/Magento/Paypal/Model/System/Config/Source/ButtonStyles.php new file mode 100644 index 000000000000..8ad55d045ff1 --- /dev/null +++ b/app/code/Magento/Paypal/Model/System/Config/Source/ButtonStyles.php @@ -0,0 +1,110 @@ + __('Gold'), + 'blue' => __('Blue'), + 'silver' => __('Silver'), + 'black' => __('Black') + ]; + } + + /** + * Button layout source getter + * + * @return array + */ + public function getLayout(): array + { + return [ + 'vertical' => __('Vertical'), + 'horizontal' => __('Horizontal') + ]; + } + + /** + * Button shape source getter + * + * @return array + */ + public function getShape(): array + { + return [ + 'pill' => __('Pill'), + 'rect' => __('Rectangle') + ]; + } + + /** + * Button size source getter + * + * @return array + */ + public function getSize(): array + { + return [ + 'medium' => __('Medium'), + 'large' => __('Large'), + 'responsive' => __('Responsive') + ]; + } + + /** + * Button label source getter + * + * @return array + */ + public function getLabel(): array + { + return [ + 'checkout' => __('Checkout'), + 'pay' => __('Pay'), + 'buynow' => __('Buy Now'), + 'paypal' => __('PayPal'), + 'installment' => __('Installment'), + 'credit' => __('Credit') + ]; + } + + /** + * Brazil button installment period source getter + * + * @return array + */ + public function getBrInstallmentPeriod(): array + { + $numbers = range(2, 12); + + return array_combine($numbers, $numbers); + } + + /** + * Mexico button installment period source getter + * + * @return array + */ + public function getMxInstallmentPeriod(): array + { + $numbers = range(3, 12, 3); + + return array_combine($numbers, $numbers); + } +} diff --git a/app/code/Magento/Paypal/Model/System/Config/Source/DisableFundingOptions.php b/app/code/Magento/Paypal/Model/System/Config/Source/DisableFundingOptions.php new file mode 100644 index 000000000000..1a9cfe0998fb --- /dev/null +++ b/app/code/Magento/Paypal/Model/System/Config/Source/DisableFundingOptions.php @@ -0,0 +1,35 @@ + 'CREDIT', + 'label' => __('PayPal Credit') + ], + [ + 'value' => 'CARD', + 'label' => __('PayPal Guest Checkout Credit Card Icons') + ], + [ + 'value' => 'ELV', + 'label' => __('Elektronisches Lastschriftverfahren - German ELV') + ] + ]; + } +} diff --git a/app/code/Magento/Paypal/Observer/AddPaypalShortcutsObserver.php b/app/code/Magento/Paypal/Observer/AddPaypalShortcutsObserver.php index 58edf68f3475..861ca7406068 100644 --- a/app/code/Magento/Paypal/Observer/AddPaypalShortcutsObserver.php +++ b/app/code/Magento/Paypal/Observer/AddPaypalShortcutsObserver.php @@ -9,6 +9,8 @@ use Magento\Framework\Event\ObserverInterface; use Magento\Paypal\Model\Config as PaypalConfig; use Magento\Framework\Event\Observer as EventObserver; +use Magento\Paypal\Block\Express\InContext\Minicart\SmartButton as MinicartSmartButton; +use Magento\Paypal\Block\Express\InContext\SmartButton as SmartButton; /** * PayPal module observer @@ -50,8 +52,9 @@ public function execute(EventObserver $observer) /** @var \Magento\Catalog\Block\ShortcutButtons $shortcutButtons */ $shortcutButtons = $observer->getEvent()->getContainer(); $blocks = [ - \Magento\Paypal\Block\Express\InContext\Minicart\Button::class => + MinicartSmartButton::class => PaypalConfig::METHOD_WPS_EXPRESS, + SmartButton::class => PaypalConfig::METHOD_WPS_EXPRESS, \Magento\Paypal\Block\Express\Shortcut::class => PaypalConfig::METHOD_WPP_EXPRESS, \Magento\Paypal\Block\Bml\Shortcut::class => PaypalConfig::METHOD_WPP_EXPRESS, \Magento\Paypal\Block\WpsExpress\Shortcut::class => PaypalConfig::METHOD_WPS_EXPRESS, @@ -77,11 +80,9 @@ public function execute(EventObserver $observer) '', $params ); - $shortcut->setIsInCatalogProduct( - $observer->getEvent()->getIsCatalogProduct() - )->setShowOrPosition( - $observer->getEvent()->getOrPosition() - ); + $shortcut->setIsInCatalogProduct($observer->getEvent()->getIsCatalogProduct()) + ->setShowOrPosition($observer->getEvent()->getOrPosition()) + ->setIsShoppingCart((bool) $observer->getEvent()->getIsShoppingCart()); $shortcutButtons->addShortcut($shortcut); } } diff --git a/app/code/Magento/Paypal/Plugin/OrderCanInvoice.php b/app/code/Magento/Paypal/Plugin/OrderCanInvoice.php index 87abdf826450..edb50acc5ee7 100644 --- a/app/code/Magento/Paypal/Plugin/OrderCanInvoice.php +++ b/app/code/Magento/Paypal/Plugin/OrderCanInvoice.php @@ -40,6 +40,10 @@ public function __construct(Express $express) */ public function afterCanInvoice(Order $order, bool $result): bool { + if (!$order->getPayment()) { + return false; + } + if ($this->express->isOrderAuthorizationAllowed($order->getPayment())) { return false; } diff --git a/app/code/Magento/Paypal/README.md b/app/code/Magento/Paypal/README.md index 8f4453ae0a05..0ed4f2e90291 100644 --- a/app/code/Magento/Paypal/README.md +++ b/app/code/Magento/Paypal/README.md @@ -1,6 +1,6 @@ Module Magento\PayPal implements integration with the PayPal payment system. Namely, it enables the following payment methods: -*PayPal Express Checkout -*PayPal Payments Standard -*PayPal Payments Pro -*PayPal Credit -*PayFlow Payment Gateway +* PayPal Express Checkout +* PayPal Payments Standard +* PayPal Payments Pro +* PayPal Credit +* PayFlow Payment Gateway diff --git a/app/code/Magento/Paypal/Setup/Patch/Data/UpdatePaypalCreditOption.php b/app/code/Magento/Paypal/Setup/Patch/Data/UpdatePaypalCreditOption.php new file mode 100644 index 000000000000..6c4362d83e29 --- /dev/null +++ b/app/code/Magento/Paypal/Setup/Patch/Data/UpdatePaypalCreditOption.php @@ -0,0 +1,84 @@ +moduleDataSetup = $moduleDataSetup; + } + + /** + * @inheritdoc + */ + public function apply() + { + $this->moduleDataSetup->getConnection()->startSetup(); + $connection = $this->moduleDataSetup->getConnection(); + $select = $connection->select() + ->from($this->moduleDataSetup->getTable('core_config_data'), ['scope', 'scope_id', 'value']) + ->where('path = ?', 'payment/paypal_express_bml/active'); + foreach ($connection->fetchAll($select) as $pair) { + if (!$pair['value']) { + $this->moduleDataSetup->getConnection() + ->insertOnDuplicate( + $this->moduleDataSetup->getTable('core_config_data'), + [ + 'scope' => $pair['scope'], + 'scope_id' => $pair['scope_id'], + 'path' => 'paypal/style/disable_funding_options', + 'value' => 'CREDIT' + ] + ); + } + } + $this->moduleDataSetup->getConnection()->endSetup(); + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return []; + } + + /** + * @inheritdoc + */ + public static function getVersion() + { + return '2.3.1'; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/OpenPayPalButtonCheckoutPageActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/OpenPayPalButtonCheckoutPageActionGroup.xml new file mode 100644 index 000000000000..32c2fab40e97 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/OpenPayPalButtonCheckoutPageActionGroup.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + \ 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 000000000000..08ca6c783438 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/OtherPayPalConfigurationActionGroup.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/PayPalExpressCheckoutConfigurationActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/PayPalExpressCheckoutConfigurationActionGroup.xml new file mode 100644 index 000000000000..bae517ffe2f3 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/PayPalExpressCheckoutConfigurationActionGroup.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/code/Magento/Paypal/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Paypal/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 000000000000..207bf62abf3c --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,26 @@ + + + + + + PayPal Settlement Reports + PayPal Settlement + magento-paypal-report-salesroot-paypal-settlement-reports + + + Sales + Sales + magento-sales-sales + + + Billing Agreements + Billing Agreements + magento-paypal-paypal-billing-agreement + + diff --git a/app/code/Magento/Paypal/Test/Mftf/Data/PaypalData.xml b/app/code/Magento/Paypal/Test/Mftf/Data/PaypalData.xml index 6d5f80e30dc7..ae34476e9ac0 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 @@ 0 + + someMerchantId + @@ -58,4 +61,42 @@ + + buyer.mpi@gmail.com + 12345678 + + + checkout + credit + pay + buy now + pay pal + installment + + + horizontal + vertical + + + medium + large + responsive + + + pill + rectangle + + + gold + blue + silver + black + + + myBusinessAccount@magento.com + myApiUsername.magento.com + somePassword + someApiSignature + someMerchantId + 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 000000000000..ca8438d5ee06 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Section/OtherPayPalPaymentsConfigSection.xml @@ -0,0 +1,31 @@ + + + + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
diff --git a/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection.xml b/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection.xml new file mode 100644 index 000000000000..85f94cd8691a --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection.xml @@ -0,0 +1,62 @@ + + + + +
+ + + + + + + + + +
+
+ + +
+
+ + + +
+
+ + + + + + + +
+
+ + + + + +
+
+ + +
+
+ + + + + + + + + +
+
\ No newline at end of file diff --git a/app/code/Magento/Paypal/Test/Mftf/Section/PaymentsConfigSection.xml b/app/code/Magento/Paypal/Test/Mftf/Section/PaymentsConfigSection.xml new file mode 100644 index 000000000000..35162cb7d619 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Section/PaymentsConfigSection.xml @@ -0,0 +1,14 @@ + + + + +
+ +
+
diff --git a/app/code/Magento/Paypal/Test/Mftf/Suite/suite.xml b/app/code/Magento/Paypal/Test/Mftf/Suite/suite.xml new file mode 100644 index 000000000000..621f2e6a6768 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Suite/suite.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPal.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPal.xml new file mode 100644 index 000000000000..b485fcb2a8f9 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPal.xml @@ -0,0 +1,276 @@ + + + + + + + + + + <description value="A popup should show when enabling different paypal solutions when one is already enabled for merchant country United Kingdom"/> + <severity value="Major"/> + <testCaseId value="MC-13146"/> + <group value="paypal"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="ConfigPayPalExpressCheckout" stepKey="ConfigPayPalExpress"> + <argument name="credentials" value="SamplePaypalExpressConfig"/> + </actionGroup> + </before> + <after> + <magentoCLI command="config:set paypal/general/merchant_country US" stepKey="setMerchantCountry"/> + <magentoCLI command="config:set payment/paypal_express/active 0" stepKey="disablePayPalExpress"/> + <magentoCLI command="config:set payment/wps_express/active 0" stepKey="disableWPSExpress"/> + <magentoCLI command="config:set payment/hosted_pro/active 0" stepKey="disableHostedProExpress"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Change Merchant Country --> + <comment userInput="Change Merchant Country" stepKey="changeMerchantCountryComment"/> + <waitForElementVisible selector="{{PaymentsConfigSection.merchantCountry}}" stepKey="waitForMerchantCountry"/> + <selectOption selector="{{PaymentsConfigSection.merchantCountry}}" userInput="United Kingdom" stepKey="setMerchantCountry"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <!-- Enable WPS Express --> + <comment userInput="Enable WPS Express" stepKey="enableWPSExpressComment"/> + <actionGroup ref="EnablePayPalConfiguration" stepKey="EnableWPSExpress"> + <argument name="payPalConfigType" value="WPSExpressConfigSection"/> + <argument name="countryCode" value="gb"/> + </actionGroup> + <!-- Check only the correct solution is enabled --> + <comment userInput="Check only the correct solution is enabled" stepKey="checkOnlyTheCorrectSolutionIsEnabledComment1"/> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkPayPalExpressIsDisabled"> + <argument name="payPalConfigType" value="PayPalExpressCheckoutConfigSection"/> + <argument name="enabledOption" value="No"/> + <argument name="countryCode" value="gb"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkWPSExpressIsEnabled"> + <argument name="payPalConfigType" value="WPSExpressConfigSection"/> + <argument name="enabledOption" value="Yes"/> + <argument name="countryCode" value="gb"/> + </actionGroup> + <!-- Enable Pro Hosted With Express Checkout --> + <comment userInput="Enable Pro Hosted With Express Checkout" stepKey="enableProHostedWithExpressCheckoutComment"/> + <actionGroup ref="EnablePayPalConfiguration" stepKey="EnableProHostedWithExpressCheckou"> + <argument name="payPalConfigType" value="PaymentsProHostedWithExpressCheckoutConfigSection"/> + <argument name="countryCode" value="gb"/> + </actionGroup> + <!-- Check only the correct solution is enabled --> + <comment userInput="Check only the correct solution is enabled" stepKey="checkOnlyTheCorrectSolutionIsEnabledComment2"/> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkWPSExpressIsDisabled"> + <argument name="payPalConfigType" value="WPSExpressConfigSection"/> + <argument name="enabledOption" value="No"/> + <argument name="countryCode" value="gb"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkProHostedIsEnabled"> + <argument name="payPalConfigType" value="PaymentsProHostedWithExpressCheckoutConfigSection"/> + <argument name="enabledOption" value="Yes"/> + <argument name="countryCode" value="gb"/> + </actionGroup> + </test> + <test name="AdminConfigPaymentsConflictResolutionForPayPalInJapan" extends="AdminConfigPaymentsConflictResolutionForPayPalInUnitedKingdom"> + <annotations> + <features value="PayPal"/> + <stories value="Payment methods"/> + <title value="Conflict resolution for PayPal in Japan"/> + <description value="A popup should show when enabling different paypal solutions when one is already enabled for merchant country Japan"/> + <severity value="Major"/> + <testCaseId value="MC-13146"/> + <group value="paypal"/> + </annotations> + <selectOption selector="{{PaymentsConfigSection.merchantCountry}}" userInput="Japan" stepKey="setMerchantCountry"/> + <actionGroup ref="EnablePayPalConfiguration" stepKey="EnableWPSExpress"> + <argument name="payPalConfigType" value="WPSOtherConfigSection"/> + <argument name="countryCode" value="jp"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkPayPalExpressIsDisabled"> + <argument name="payPalConfigType" value="PayPalExpressCheckoutOtherCountryConfigSection"/> + <argument name="enabledOption" value="No"/> + <argument name="countryCode" value="jp"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkWPSExpressIsEnabled"> + <argument name="payPalConfigType" value="WPSOtherConfigSection"/> + <argument name="enabledOption" value="Yes"/> + <argument name="countryCode" value="jp"/> + </actionGroup> + <actionGroup ref="EnablePayPalConfiguration" stepKey="EnableProHostedWithExpressCheckou"> + <argument name="payPalConfigType" value="WebsitePaymentsPlusConfigSection"/> + <argument name="countryCode" value="jp"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkWPSExpressIsDisabled"> + <argument name="payPalConfigType" value="WPSOtherConfigSection"/> + <argument name="enabledOption" value="No"/> + <argument name="countryCode" value="jp"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkProHostedIsEnabled"> + <argument name="payPalConfigType" value="WebsitePaymentsPlusConfigSection"/> + <argument name="enabledOption" value="Yes"/> + <argument name="countryCode" value="jp"/> + </actionGroup> + </test> + <test name="AdminConfigPaymentsConflictResolutionForPayPalInFrance" extends="AdminConfigPaymentsConflictResolutionForPayPalInUnitedKingdom"> + <annotations> + <features value="PayPal"/> + <stories value="Payment methods"/> + <title value="Conflict resolution for PayPal in France"/> + <description value="A popup should show when enabling different paypal solutions when one is already enabled for merchant country France"/> + <severity value="Major"/> + <testCaseId value="MC-13146"/> + <group value="paypal"/> + </annotations> + <selectOption selector="{{PaymentsConfigSection.merchantCountry}}" userInput="France" stepKey="setMerchantCountry"/> + <actionGroup ref="EnablePayPalConfiguration" stepKey="EnableWPSExpress"> + <argument name="payPalConfigType" value="WPSOtherConfigSection"/> + <argument name="countryCode" value="fr"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkPayPalExpressIsDisabled"> + <argument name="payPalConfigType" value="PayPalExpressCheckoutOtherCountryConfigSection"/> + <argument name="enabledOption" value="No"/> + <argument name="countryCode" value="fr"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkWPSExpressIsEnabled"> + <argument name="payPalConfigType" value="WPSOtherConfigSection"/> + <argument name="enabledOption" value="Yes"/> + <argument name="countryCode" value="fr"/> + </actionGroup> + <actionGroup ref="EnablePayPalConfiguration" stepKey="EnableProHostedWithExpressCheckou"> + <argument name="payPalConfigType" value="WebsitePaymentsPlusConfigSection"/> + <argument name="countryCode" value="fr"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkWPSExpressIsDisabled"> + <argument name="payPalConfigType" value="WPSOtherConfigSection"/> + <argument name="enabledOption" value="No"/> + <argument name="countryCode" value="fr"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkProHostedIsEnabled"> + <argument name="payPalConfigType" value="WebsitePaymentsPlusConfigSection"/> + <argument name="enabledOption" value="Yes"/> + <argument name="countryCode" value="fr"/> + </actionGroup> + </test> + <test name="AdminConfigPaymentsConflictResolutionForPayPalInHongKong" extends="AdminConfigPaymentsConflictResolutionForPayPalInUnitedKingdom"> + <annotations> + <features value="PayPal"/> + <stories value="Payment methods"/> + <title value="Conflict resolution for PayPal in Hong Kong"/> + <description value="A popup should show when enabling different paypal solutions when one is already enabled for merchant country Hong Kong"/> + <severity value="Major"/> + <testCaseId value="MC-13146"/> + <group value="paypal"/> + </annotations> + <selectOption selector="{{PaymentsConfigSection.merchantCountry}}" userInput="Hong Kong SAR China" stepKey="setMerchantCountry"/> + <actionGroup ref="EnablePayPalConfiguration" stepKey="EnableWPSExpress"> + <argument name="payPalConfigType" value="WPSOtherConfigSection"/> + <argument name="countryCode" value="hk"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkPayPalExpressIsDisabled"> + <argument name="payPalConfigType" value="PayPalExpressCheckoutOtherCountryConfigSection"/> + <argument name="enabledOption" value="No"/> + <argument name="countryCode" value="hk"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkWPSExpressIsEnabled"> + <argument name="payPalConfigType" value="WPSOtherConfigSection"/> + <argument name="enabledOption" value="Yes"/> + <argument name="countryCode" value="hk"/> + </actionGroup> + <actionGroup ref="EnablePayPalConfiguration" stepKey="EnableProHostedWithExpressCheckou"> + <argument name="payPalConfigType" value="WebsitePaymentsPlusConfigSection"/> + <argument name="countryCode" value="hk"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkWPSExpressIsDisabled"> + <argument name="payPalConfigType" value="WPSOtherConfigSection"/> + <argument name="enabledOption" value="No"/> + <argument name="countryCode" value="hk"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkProHostedIsEnabled"> + <argument name="payPalConfigType" value="WebsitePaymentsPlusConfigSection"/> + <argument name="enabledOption" value="Yes"/> + <argument name="countryCode" value="hk"/> + </actionGroup> + </test> + <test name="AdminConfigPaymentsConflictResolutionForPayPalInItaly" extends="AdminConfigPaymentsConflictResolutionForPayPalInUnitedKingdom"> + <annotations> + <features value="PayPal"/> + <stories value="Payment methods"/> + <title value="Conflict resolution for PayPal in Italy"/> + <description value="A popup should show when enabling different paypal solutions when one is already enabled for merchant country Italy"/> + <severity value="Major"/> + <testCaseId value="MC-13146"/> + <group value="paypal"/> + </annotations> + <selectOption selector="{{PaymentsConfigSection.merchantCountry}}" userInput="Italy" stepKey="setMerchantCountry"/> + <actionGroup ref="EnablePayPalConfiguration" stepKey="EnableWPSExpress"> + <argument name="payPalConfigType" value="WPSOtherConfigSection"/> + <argument name="countryCode" value="it"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkPayPalExpressIsDisabled"> + <argument name="payPalConfigType" value="PayPalExpressCheckoutOtherCountryConfigSection"/> + <argument name="enabledOption" value="No"/> + <argument name="countryCode" value="it"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkWPSExpressIsEnabled"> + <argument name="payPalConfigType" value="WPSOtherConfigSection"/> + <argument name="enabledOption" value="Yes"/> + <argument name="countryCode" value="it"/> + </actionGroup> + <actionGroup ref="EnablePayPalConfiguration" stepKey="EnableProHostedWithExpressCheckou"> + <argument name="payPalConfigType" value="WebsitePaymentsPlusConfigSection"/> + <argument name="countryCode" value="it"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkWPSExpressIsDisabled"> + <argument name="payPalConfigType" value="WPSOtherConfigSection"/> + <argument name="enabledOption" value="No"/> + <argument name="countryCode" value="it"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkProHostedIsEnabled"> + <argument name="payPalConfigType" value="WebsitePaymentsPlusConfigSection"/> + <argument name="enabledOption" value="Yes"/> + <argument name="countryCode" value="it"/> + </actionGroup> + </test> + <test name="AdminConfigPaymentsConflictResolutionForPayPalInSpain" extends="AdminConfigPaymentsConflictResolutionForPayPalInUnitedKingdom"> + <annotations> + <features value="PayPal"/> + <stories value="Payment methods"/> + <title value="Conflict resolution for PayPal in Spain"/> + <description value="A popup should show when enabling different paypal solutions when one is already enabled for merchant country Spain"/> + <severity value="Major"/> + <testCaseId value="MC-13146"/> + <group value="paypal"/> + </annotations> + <selectOption selector="{{PaymentsConfigSection.merchantCountry}}" userInput="Spain" stepKey="setMerchantCountry"/> + <actionGroup ref="EnablePayPalConfiguration" stepKey="EnableWPSExpress"> + <argument name="payPalConfigType" value="WPSOtherConfigSection"/> + <argument name="countryCode" value="es"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkPayPalExpressIsDisabled"> + <argument name="payPalConfigType" value="PayPalExpressCheckoutOtherCountryConfigSection"/> + <argument name="enabledOption" value="No"/> + <argument name="countryCode" value="es"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkWPSExpressIsEnabled"> + <argument name="payPalConfigType" value="WPSOtherConfigSection"/> + <argument name="enabledOption" value="Yes"/> + <argument name="countryCode" value="es"/> + </actionGroup> + <actionGroup ref="EnablePayPalConfiguration" stepKey="EnableProHostedWithExpressCheckou"> + <argument name="payPalConfigType" value="WebsitePaymentsPlusConfigSection"/> + <argument name="countryCode" value="es"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkWPSExpressIsDisabled"> + <argument name="payPalConfigType" value="WPSOtherConfigSection"/> + <argument name="enabledOption" value="No"/> + <argument name="countryCode" value="es"/> + </actionGroup> + <actionGroup ref="CheckEnableOptionPayPalConfiguration" stepKey="checkProHostedIsEnabled"> + <argument name="payPalConfigType" value="WebsitePaymentsPlusConfigSection"/> + <argument name="enabledOption" value="Yes"/> + <argument name="countryCode" value="es"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsSectionState.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsSectionState.xml index ac752e8412ff..934449dfd136 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsSectionState.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsSectionState.xml @@ -18,7 +18,7 @@ <testCaseId value="MAGETWO-92043"/> </annotations> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="amOnLogoutPage"/> </after> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminReportsPayPalSettlementNavigateMenuTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminReportsPayPalSettlementNavigateMenuTest.xml new file mode 100644 index 000000000000..03f0167230e9 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminReportsPayPalSettlementNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminReportsPayPalSettlementNavigateMenuTest"> + <annotations> + <features value="Paypal"/> + <stories value="Menu Navigation"/> + <title value="Admin reports paypal settlement navigate menu test"/> + <description value="Admin should be able to navigate to Reports > PayPal Settlement"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14193"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToPayPalSettlementPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsSalesPayPalSettlement.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsSalesPayPalSettlement.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminSalesBillingAgreementsNavigateMenuTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminSalesBillingAgreementsNavigateMenuTest.xml new file mode 100644 index 000000000000..8c3735fcbd25 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminSalesBillingAgreementsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminSalesBillingAgreementsNavigateMenuTest"> + <annotations> + <features value="Paypal"/> + <stories value="Menu Navigation"/> + <title value="Admin sales billing agreements navigate menu test"/> + <description value="Admin should be able to navigate to Sales > Billing Agreements"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14194"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToBillingAgreementsPage"> + <argument name="menuUiId" value="{{AdminMenuSales.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSalesBillingAgreements.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSalesBillingAgreements.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/PayPalSmartButtonInCheckoutPage.xml b/app/code/Magento/Paypal/Test/Mftf/Test/PayPalSmartButtonInCheckoutPage.xml new file mode 100644 index 000000000000..1858ee130a34 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/PayPalSmartButtonInCheckoutPage.xml @@ -0,0 +1,170 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="CheckDefaultValueOfPayPalCustomizeButtonTest"> + <annotations> + <features value="PayPal"/> + <stories value="Button Configuration"/> + <title value="Check Default Value Of PayPal Customize Button"/> + <description value="Default value of PayPal Customize Button should be NO"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-10904"/> + <skip> + <issueId value="DEVOPS-3311"/> + </skip> + </annotations> + <before> + <actionGroup ref="LoginActionGroup" stepKey="login"/> + <actionGroup ref="ConfigPayPalExpressCheckout" stepKey="ConfigPayPalExpressCheckout"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + <amOnPage url="{{AdminConfigPaymentMethodsPage.url}}" stepKey="navigateToPaymentConfigurationPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <actionGroup ref="OpenPayPalButtonCheckoutPage" stepKey="openPayPalButtonCheckoutPage"/> + <seeElement selector="{{ButtonCustomization.customizeDrpDown}}" stepKey="seeCustomizeDropDown"/> + <seeOptionIsSelected selector="{{ButtonCustomization.customizeDrpDown}}" userInput="No" stepKey="seeNoIsDefaultValue"/> + <selectOption selector="{{ButtonCustomization.customizeDrpDown}}" userInput="Yes" stepKey="enableButtonCustomization"/> + <!--Verify default value--> + <comment userInput="Verify default value" stepKey="commentVerifyDefaultValue1"/> + <seeElement selector="{{ButtonCustomization.label}}" stepKey="seeLabel"/> + <seeElement selector="{{ButtonCustomization.layout}}" stepKey="seeLayout"/> + <seeElement selector="{{ButtonCustomization.size}}" stepKey="seeSize1"/> + <seeElement selector="{{ButtonCustomization.shape}}" stepKey="seeShape1"/> + <seeElement selector="{{ButtonCustomization.color}}" stepKey="seeColor"/> + </test> + <test name="CheckCreditButtonConfiguration"> + <annotations> + <features value="PayPal"/> + <stories value="Button Configuration"/> + <title value="Check Credit Button Configuration"/> + <description value="Admin is able to customize Credit button"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-10900"/> + <skip> + <issueId value="DEVOPS-3311"/> + </skip> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + <createData entity="_defaultProduct" stepKey="createPreReqProduct"> + <requiredEntity createDataKey="createPreReqCategory"/> + </createData> + <!-- Create Customer --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <actionGroup ref="LoginActionGroup" stepKey="login"/> + <!--Config PayPal Express Checkout--> + <comment userInput="config PayPal Express Checkout" stepKey="commemtConfigPayPalExpressCheckout"/> + <actionGroup ref="ConfigPayPalExpressCheckout" stepKey="ConfigPayPalExpressCheckout"/> + </before> + <after> + <deleteData stepKey="deleteCategory" createDataKey="createPreReqCategory"/> + <deleteData stepKey="deleteProduct" createDataKey="createPreReqProduct"/> + <deleteData stepKey="deleteCustomer" createDataKey="createCustomer"/> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + <amOnPage url="{{AdminConfigPaymentMethodsPage.url}}" stepKey="navigateToPaymentConfigurationPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <!--Navigate to button configuration setting--> + <comment userInput="Navigate to button configuration setting in Admin site" stepKey="commentNavigateToButtonConfigurationInAdmin"/> + <actionGroup ref="OpenPayPalButtonCheckoutPage" stepKey="openPayPalButtonCheckoutPage"/> + <waitForElement selector="{{ButtonCustomization.customizeDrpDown}}" stepKey="seeCustomizeDropDown"/> + <selectOption selector="{{ButtonCustomization.customizeDrpDown}}" userInput="Yes" stepKey="enableButtonCustomization"/> + <!--Verify Credit Button value--> + <comment userInput="Verify Credit Button value" stepKey="commentVerifyDefaultValue2"/> + <selectOption selector="{{ButtonCustomization.label}}" userInput="{{PayPalLabel.credit}}" stepKey="selectCreditAsLabel"/> + <seeElement selector="{{ButtonCustomization.size}}" stepKey="seeSize2"/> + <seeElement selector="{{ButtonCustomization.shape}}" stepKey="seeShape2"/> + <dontSeeElement selector="{{ButtonCustomization.layout}}" stepKey="dontSeeLayout"/> + <dontSeeElement selector="{{ButtonCustomization.color}}" stepKey="dontSeeColor"/> + <!--Customize Credit Button--> + <selectOption selector="{{ButtonCustomization.size}}" userInput="{{PayPalSize.medium}}" stepKey="selectSize"/> + <selectOption selector="{{ButtonCustomization.shape}}" userInput="{{PayPalShape.pill}}" stepKey="selectShape"/> + <!--Save configuration--> + <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig"/> + <waitForPageLoad stepKey="waitForConfigSave"/> + <openNewTab stepKey="openNewTab"/> + <amOnPage url="/" stepKey="openStorefront"/> + <!--Login to storefront as previously created customer--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <actionGroup ref="addProductToCheckoutPage" stepKey="addProductToCheckoutPage"> + <argument name="Category" value="$$createPreReqCategory$$"/> + </actionGroup> + <!--set ID for iframe of PayPal group button--> + <executeJS function="jQuery('.zoid-component-frame.zoid-visible').attr('id', 'myIframe')" stepKey="clickOrderLink"/> + <!--switch to iframe of PayPal group button--> + <comment userInput="switch to iframe of PayPal group button" stepKey="commentSwitchToIframe"/> + <switchToIframe userInput="myIframe" stepKey="clickPrintOrderLink"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.PayPalBtn}}" stepKey="waitForPayPalBtn"/> + <seeElement selector="{{PayPalButtonOnStorefront.label(PayPalLabel.credit)}}{{PayPalButtonOnStorefront.size(PayPalSize.medium)}}" stepKey="seeButtonInMediumSize"/> + <seeElement selector="{{PayPalButtonOnStorefront.label(PayPalLabel.credit)}}{{PayPalButtonOnStorefront.shape(PayPalShape.pill)}}" stepKey="seeButtonInPillShape"/> + </test> + <test name="PayPalSmartButtonInCheckoutPage"> + <annotations> + <features value="PayPal"/> + <stories value="Generic checkout skeleton flow"/> + <title value="Mainflow of PayPal Smart Button"/> + <description value="Users are able to place order using PayPal Smart Button"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-13690"/> + <skip> + <issueId value="DEVOPS-3311"/> + </skip> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + <createData entity="_defaultProduct" stepKey="createPreReqProduct"> + <requiredEntity createDataKey="createPreReqCategory"/> + </createData> + <!-- Create Customer --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <actionGroup ref="LoginActionGroup" stepKey="login"/> + <!--Config PayPal Express Checkout--> + <comment userInput="config PayPal Express Checkout" stepKey="commemtConfigPayPalExpressCheckout"/> + <actionGroup ref="ConfigPayPalExpressCheckout" stepKey="ConfigPayPalExpressCheckout"/> + <magentoCLI command="config:set payment/paypal_express/in_context 1" stepKey="disableInContextPayPal"/> + </before> + <after> + <deleteData stepKey="deleteCategory" createDataKey="createPreReqCategory"/> + <deleteData stepKey="deleteProduct" createDataKey="createPreReqProduct"/> + <deleteData stepKey="deleteCustomer" createDataKey="createCustomer"/> + <magentoCLI command="config:set payment/paypal_express/in_context 0" stepKey="enableInContextPayPal"/> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + <magentoCLI command="config:set payment/paypal_express/payment_action Authorization" stepKey="inputPaymentAction"/> + <magentoCLI command="config:set payment/paypal_express/solution_type Sole" stepKey="enablePayPalGuestCheckout"/> + <magentoCLI command="config:set payment/paypal_express/line_items_enabled 1" stepKey="enableTransferCartLine"/> + <magentoCLI command="config:set payment/paypal_express/skip_order_review_step 1" stepKey="enableSkipOrderReview"/> + <!--Login to storefront as previously created customer--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <!--Place an order using PayPal method--> + <comment userInput="Place an order using PayPal method" stepKey="commentPayPalPlaceOrder"/> + <actionGroup ref="CreatePayPalOrderWithSelectedPaymentMethodActionGroup" stepKey="createPayPalOrder"> + <argument name="Category" value="$$createPreReqCategory$$"/> + </actionGroup> + <!--Open Cart on PayPal--> + <comment userInput="Open Cart on PayPal" stepKey="commentOpenCart"/> + <click selector="{{PayPalPaymentSection.cartIcon}}" stepKey="openCart"/> + <seeElement selector="{{PayPalPaymentSection.itemName($$createPreReqProduct.name$$)}}" stepKey="seeProductname"/> + <click selector="{{PayPalPaymentSection.PayPalSubmitBtn}}" stepKey="clickPayPalSubmitBtn"/> + <switchToPreviousTab stepKey="switchToPreviousTab"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <!--I see order successful Page instead of Order Review Page--> + <comment userInput="I see order successful Page instead of Order Review Page" stepKey="commentVerifyOrderReviewPage"/> + <waitForElement selector="{{CheckoutSuccessMainSection.successTitle}}" stepKey="waitForLoadSuccessPageTitle"/> + <waitForElement selector="{{CheckoutSuccessMainSection.success}}" time="30" stepKey="waitForLoadSuccessPage"/> + <seeElement selector="{{CheckoutSuccessMainSection.orderLink}}" stepKey="seeOrderLink"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Field/Enable/AbstractEnableTest.php b/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Field/Enable/AbstractEnableTest.php index b33d2f572396..b9ea53c15401 100644 --- a/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Field/Enable/AbstractEnableTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Field/Enable/AbstractEnableTest.php @@ -5,6 +5,8 @@ */ namespace Magento\Paypal\Test\Unit\Block\Adminhtml\System\Config\Field\Enable; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + /** * Class AbstractEnableTest * @@ -43,8 +45,18 @@ protected function setUp() )->disableOriginalConstructor() ->getMockForAbstractClass(); + $objectManager = new ObjectManager($this); + $escaper = $objectManager->getObject(\Magento\Framework\Escaper::class); + $reflection = new \ReflectionClass($this->elementMock); + $reflection_property = $reflection->getProperty('_escaper'); + $reflection_property->setAccessible(true); + $reflection_property->setValue($this->elementMock, $escaper); + $this->abstractEnable = $objectManager->getObject( - \Magento\Paypal\Test\Unit\Block\Adminhtml\System\Config\Field\Enable\AbstractEnable\Stub::class + \Magento\Paypal\Test\Unit\Block\Adminhtml\System\Config\Field\Enable\AbstractEnable\Stub::class, + [ + '_escaper' => $objectManager->getObject(\Magento\Framework\Escaper::class) + ] ); } diff --git a/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Fieldset/GroupTest.php b/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Fieldset/GroupTest.php deleted file mode 100644 index cfdfe17b1e00..000000000000 --- a/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Fieldset/GroupTest.php +++ /dev/null @@ -1,107 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Paypal\Test\Unit\Block\Adminhtml\System\Config\Fieldset; - -class GroupTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var Group - */ - protected $_model; - - /** - * @var \Magento\Framework\Data\Form\Element\AbstractElement - */ - protected $_element; - - /** - * @var \Magento\Backend\Model\Auth\Session|\PHPUnit_Framework_MockObject_MockObject - */ - protected $_authSession; - - /** - * @var \Magento\User\Model\User|\PHPUnit_Framework_MockObject_MockObject - */ - protected $_user; - - /** - * @var \Magento\Config\Model\Config\Structure\Element\Group|\PHPUnit_Framework_MockObject_MockObject - */ - protected $_group; - - protected function setUp() - { - $helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->_group = $this->createMock(\Magento\Config\Model\Config\Structure\Element\Group::class); - $this->_element = $this->getMockForAbstractClass( - \Magento\Framework\Data\Form\Element\AbstractElement::class, - [], - '', - false, - true, - true, - ['getHtmlId', 'getElementHtml', 'getName', 'getElements', 'getId'] - ); - $this->_element->expects($this->any()) - ->method('getHtmlId') - ->will($this->returnValue('html id')); - $this->_element->expects($this->any()) - ->method('getElementHtml') - ->will($this->returnValue('element html')); - $this->_element->expects($this->any()) - ->method('getName') - ->will($this->returnValue('name')); - $this->_element->expects($this->any()) - ->method('getElements') - ->will($this->returnValue([])); - $this->_element->expects($this->any()) - ->method('getId') - ->will($this->returnValue('id')); - $this->_user = $this->createMock(\Magento\User\Model\User::class); - $this->_authSession = $this->createMock(\Magento\Backend\Model\Auth\Session::class); - $this->_authSession->expects($this->any()) - ->method('__call') - ->with('getUser') - ->will($this->returnValue($this->_user)); - $this->_model = $helper->getObject( - \Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Group::class, - ['authSession' => $this->_authSession] - ); - $this->_model->setGroup($this->_group); - } - - /** - * @param mixed $expanded - * @param int $expected - * @dataProvider isCollapseStateDataProvider - */ - public function testIsCollapseState($expanded, $expected) - { - $this->_user->setExtra(['configState' => []]); - $this->_element->setGroup(isset($expanded) ? ['expanded' => $expanded] : []); - $html = $this->_model->render($this->_element); - $this->assertContains( - '<input id="' . $this->_element->getHtmlId() . '-state" name="config_state[' - . $this->_element->getId() . ']" type="hidden" value="' . $expected . '" />', - $html - ); - } - - /** - * @return array - */ - public function isCollapseStateDataProvider() - { - return [ - [null, 0], - [false, 0], - ['', 0], - [1, 1], - ['1', 1], - ]; - } -} diff --git a/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Multiselect/DisabledFundingOptionsTest.php b/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Multiselect/DisabledFundingOptionsTest.php new file mode 100644 index 000000000000..2c9a33ce4385 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Multiselect/DisabledFundingOptionsTest.php @@ -0,0 +1,142 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Paypal\Test\Unit\Block\Adminhtml\System\Config\Multiselect; + +use \Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use \Magento\Framework\Data\Form\Element\AbstractElement; +use \Magento\Framework\App\RequestInterface; +use \Magento\Framework\View\Helper\Js; +use \Magento\Paypal\Model\Config; +use \Magento\Paypal\Block\Adminhtml\System\Config\MultiSelect\DisabledFundingOptions; +use \Magento\Paypal\Model\Config\StructurePlugin; +use \PHPUnit\Framework\TestCase; + +/** + * Class DisabledFundingOptionsTest + */ +class DisabledFundingOptionsTest extends TestCase +{ + /** + * @var \Magento\Paypal\Block\Adminhtml\System\Config\Multiselect\DisabledFundingOptions + */ + private $model; + + /** + * @var \Magento\Framework\Data\Form\Element\AbstractElement + */ + private $element; + + /** + * @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $request; + + /** + * @var \Magento\Framework\View\Helper\Js|\PHPUnit_Framework_MockObject_MockObject + */ + private $jsHelper; + + /** + * @var \Magento\Paypal\Model\Config + */ + private $config; + + protected function setUp() + { + $helper = new ObjectManager($this); + $this->element = $this->getMockForAbstractClass( + AbstractElement::class, + [], + '', + false, + true, + true, + ['getHtmlId', 'getElementHtml', 'getName'] + ); + $this->request = $this->getMockForAbstractClass(RequestInterface::class); + $this->jsHelper = $this->createMock(Js::class); + $this->config = $this->createMock(Config::class); + $this->element->setValues($this->getDefaultFundingOptions()); + $this->model = $helper->getObject( + DisabledFundingOptions::class, + ['request' => $this->request, 'jsHelper' => $this->jsHelper, 'config' => $this->config] + ); + } + + /** + * @param null|string $requestCountry + * @param null|string $merchantCountry + * @param bool $shouldContainPaypalCredit + * @dataProvider isPaypalCreditAvailableDataProvider + */ + public function testIsPaypalCreditAvailable( + ?string $requestCountry, + ?string $merchantCountry, + bool $shouldContainPaypalCredit + ) { + $this->request->expects($this->any()) + ->method('getParam') + ->will($this->returnCallback(function ($param) use ($requestCountry) { + if ($param == StructurePlugin::REQUEST_PARAM_COUNTRY) { + return $requestCountry; + } + return $param; + })); + $this->config->expects($this->any()) + ->method('getMerchantCountry') + ->will($this->returnCallback(function () use ($merchantCountry) { + return $merchantCountry; + })); + $this->model->render($this->element); + $payPalCreditOption = [ + 'value' => 'CREDIT', + 'label' => __('PayPal Credit') + ]; + $elementValues = $this->element->getValues(); + if ($shouldContainPaypalCredit) { + $this->assertContains($payPalCreditOption, $elementValues); + } else { + $this->assertNotContains($payPalCreditOption, $elementValues); + } + } + + /** + * @return array + */ + public function isPaypalCreditAvailableDataProvider(): array + { + return [ + [null, 'US', true], + ['US', 'US', true], + ['US', 'GB', true], + ['GB', 'GB', false], + ['GB', 'US', false], + ['GB', null, false], + ]; + } + + /** + * @inheritdoc + */ + private function getDefaultFundingOptions(): array + { + return [ + [ + 'value' => 'CREDIT', + 'label' => __('PayPal Credit') + ], + [ + 'value' => 'CARD', + 'label' => __('PayPal Guest Checkout Credit Card Icons') + ], + [ + 'value' => 'ELV', + 'label' => __('Elektronisches Lastschriftverfahren - German ELV') + ] + ]; + } +} diff --git a/app/code/Magento/Paypal/Test/Unit/Block/Bml/ShortcutTest.php b/app/code/Magento/Paypal/Test/Unit/Block/Bml/ShortcutTest.php index b8558cdc0849..3fce5dab9dda 100644 --- a/app/code/Magento/Paypal/Test/Unit/Block/Bml/ShortcutTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Block/Bml/ShortcutTest.php @@ -8,6 +8,8 @@ use Magento\Catalog\Block as CatalogBlock; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Paypal\Model\ConfigFactory; +use Magento\Paypal\Model\Config; class ShortcutTest extends \PHPUnit\Framework\TestCase { @@ -33,12 +35,24 @@ protected function setUp() $this->paypalShortcutHelperMock = $this->createMock(\Magento\Paypal\Helper\Shortcut\ValidatorInterface::class); $this->objectManagerHelper = new ObjectManagerHelper($this); + $configFactoryMock = $this->getMockBuilder(ConfigFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $configMock = $this->getMockBuilder(Config::class) + ->disableOriginalConstructor() + ->setMethods(['setMethod']) + ->getMock(); + $configFactoryMock->expects($this->any())->method('create')->willReturn($configMock); + $this->shortcut = $this->objectManagerHelper->getObject( \Magento\Paypal\Block\Bml\Shortcut::class, [ 'paymentData' => $this->paymentHelperMock, 'mathRandom' => $this->randomMock, 'shortcutValidator' => $this->paypalShortcutHelperMock, + 'config' => $configFactoryMock ] ); } diff --git a/app/code/Magento/Paypal/Test/Unit/Controller/Ipn/IndexTest.php b/app/code/Magento/Paypal/Test/Unit/Controller/Ipn/IndexTest.php index 1c7d25e37a6d..890261c6522f 100644 --- a/app/code/Magento/Paypal/Test/Unit/Controller/Ipn/IndexTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Controller/Ipn/IndexTest.php @@ -6,44 +6,105 @@ namespace Magento\Paypal\Test\Unit\Controller\Ipn; +use Magento\Framework\Event\ManagerInterface; +use Magento\Paypal\Controller\Ipn\Index; +use Magento\Paypal\Model\IpnFactory; +use Magento\Paypal\Model\IpnInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\OrderFactory; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class IndexTest extends \PHPUnit\Framework\TestCase { /** @var Index */ - protected $model; + private $model; /** @var \Psr\Log\LoggerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $logger; + private $loggerMock; /** @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $request; + private $requestMock; /** @var \Magento\Framework\App\ResponseInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $response; + private $responseMock; + + /** + * @var IpnFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $ipnFactoryMock; + + /** + * @var OrderFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $orderFactoryMock; + + /** + * @var ManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $eventManagerMock; protected function setUp() { - $this->logger = $this->createMock(\Psr\Log\LoggerInterface::class); - $this->request = $this->createMock(\Magento\Framework\App\Request\Http::class); - $this->response = $this->createMock(\Magento\Framework\App\Response\Http::class); + $this->loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class); + $this->requestMock = $this->createMock(\Magento\Framework\App\Request\Http::class); + $this->responseMock = $this->createMock(\Magento\Framework\App\Response\Http::class); + $this->ipnFactoryMock = $this->createMock(IpnFactory::class); + $this->orderFactoryMock = $this->createMock(OrderFactory::class); + $this->eventManagerMock = $this->createMock(ManagerInterface::class); $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->model = $objectManagerHelper->getObject( - \Magento\Paypal\Controller\Ipn\Index::class, + Index::class, [ - 'logger' => $this->logger, - 'request' => $this->request, - 'response' => $this->response, + 'logger' => $this->loggerMock, + 'request' => $this->requestMock, + 'response' => $this->responseMock, + 'ipnFactory' => $this->ipnFactoryMock, + 'orderFactory' => $this->orderFactoryMock, + 'eventManager' => $this->eventManagerMock ] ); } public function testIndexActionException() { - $this->request->expects($this->once())->method('isPost')->will($this->returnValue(true)); + $this->requestMock->expects($this->once())->method('isPost')->will($this->returnValue(true)); $exception = new \Exception(); - $this->request->expects($this->once())->method('getPostValue')->will($this->throwException($exception)); - $this->logger->expects($this->once())->method('critical')->with($this->identicalTo($exception)); - $this->response->expects($this->once())->method('setHttpResponseCode')->with(500); + $this->requestMock->expects($this->once())->method('getPostValue')->will($this->throwException($exception)); + $this->loggerMock->expects($this->once())->method('critical')->with($this->identicalTo($exception)); + $this->responseMock->expects($this->once())->method('setHttpResponseCode')->with(500); + $this->model->execute(); + } + + public function testIndexAction() + { + $this->requestMock->expects($this->once())->method('isPost')->will($this->returnValue(true)); + $incrementId = 'incrementId'; + $data = [ + 'invoice' => $incrementId, + 'other' => 'other data' + ]; + $this->requestMock->expects($this->exactly(2))->method('getPostValue')->willReturn($data); + $ipnMock = $this->createMock(IpnInterface::class); + $this->ipnFactoryMock->expects($this->once()) + ->method('create') + ->with(['data' => $data]) + ->willReturn($ipnMock); + $ipnMock->expects($this->once()) + ->method('processIpnRequest'); + $orderMock = $this->createMock(Order::class); + $this->orderFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($orderMock); + $orderMock->expects($this->once()) + ->method('loadByIncrementId') + ->with($incrementId) + ->willReturn($orderMock); + $this->eventManagerMock->expects($this->once()) + ->method('dispatch') + ->with('paypal_checkout_success', ['order' => $orderMock]); $this->model->execute(); } } diff --git a/app/code/Magento/Paypal/Test/Unit/Controller/Payflow/ReturnUrlTest.php b/app/code/Magento/Paypal/Test/Unit/Controller/Payflow/ReturnUrlTest.php index 44775d7e381b..32d3f2c73b15 100644 --- a/app/code/Magento/Paypal/Test/Unit/Controller/Payflow/ReturnUrlTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Controller/Payflow/ReturnUrlTest.php @@ -5,7 +5,6 @@ */ namespace Magento\Paypal\Test\Unit\Controller\Payflow; -use Magento\Framework\Event\ManagerInterface; use Magento\Sales\Api\PaymentFailuresInterface; use Magento\Checkout\Block\Onepage\Success; use Magento\Checkout\Model\Session; @@ -97,11 +96,6 @@ class ReturnUrlTest extends \PHPUnit\Framework\TestCase */ private $paymentFailures; - /** - * @var ManagerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $eventManagerMock; - /** * @inheritdoc */ @@ -158,16 +152,10 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $this->eventManagerMock = $this->getMockBuilder(ManagerInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->context->method('getView') ->willReturn($this->view); $this->context->method('getRequest') ->willReturn($this->request); - $this->context->method('getEventManager') - ->willReturn($this->eventManagerMock); $this->returnUrl = $this->objectManager->getObject( ReturnUrl::class, @@ -199,10 +187,6 @@ public function testExecuteAllowedOrderState($state) ->with('goto_success_page', true) ->willReturnSelf(); - $this->eventManagerMock->expects($this->once()) - ->method('dispatch') - ->with('paypal_checkout_success', $this->arrayHasKey('order')); - $result = $this->returnUrl->execute(); $this->assertNull($result); } diff --git a/app/code/Magento/Paypal/Test/Unit/Controller/Transparent/RequestSecureTokenTest.php b/app/code/Magento/Paypal/Test/Unit/Controller/Transparent/RequestSecureTokenTest.php index 60451a982709..6752eab6a778 100644 --- a/app/code/Magento/Paypal/Test/Unit/Controller/Transparent/RequestSecureTokenTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Controller/Transparent/RequestSecureTokenTest.php @@ -3,16 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Paypal\Test\Unit\Controller\Transparent; use Magento\Framework\App\Action\Context; use Magento\Framework\Controller\Result\JsonFactory; use Magento\Framework\Session\Generic; use Magento\Framework\Session\SessionManager; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Paypal\Controller\Transparent\RequestSecureToken; use Magento\Paypal\Model\Payflow\Service\Request\SecureToken; use Magento\Paypal\Model\Payflow\Transparent; +use PHPUnit\Framework\MockObject\MockObject; /** * Class RequestSecureTokenTest @@ -22,39 +24,39 @@ class RequestSecureTokenTest extends \PHPUnit\Framework\TestCase { /** - * @var Transparent|\PHPUnit_Framework_MockObject_MockObject + * @var Transparent|MockObject */ - protected $transparentMock; + private $transparent; /** - * @var RequestSecureToken|\PHPUnit_Framework_MockObject_MockObject + * @var RequestSecureToken|MockObject */ - protected $controller; + private $controller; /** - * @var Context|\PHPUnit_Framework_MockObject_MockObject + * @var Context|MockObject */ - protected $contextMock; + private $context; /** - * @var JsonFactory|\PHPUnit_Framework_MockObject_MockObject + * @var JsonFactory|MockObject */ - protected $resultJsonFactoryMock; + private $resultJsonFactory; /** - * @var Generic|\PHPUnit_Framework_MockObject_MockObject + * @var Generic|MockObject */ - protected $sessionTransparentMock; + private $sessionTransparent; /** - * @var SecureToken|\PHPUnit_Framework_MockObject_MockObject + * @var SecureToken|MockObject */ - protected $secureTokenServiceMock; + private $secureTokenService; /** - * @var SessionManager|\PHPUnit_Framework_MockObject_MockObject + * @var SessionManager|MockObject */ - protected $sessionManagerMock; + private $sessionManager; /** * Set up @@ -64,45 +66,46 @@ class RequestSecureTokenTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->contextMock = $this->getMockBuilder(\Magento\Framework\App\Action\Context::class) + $this->context = $this->getMockBuilder(\Magento\Framework\App\Action\Context::class) ->disableOriginalConstructor() ->getMock(); - $this->resultJsonFactoryMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\JsonFactory::class) + $this->resultJsonFactory = $this->getMockBuilder(\Magento\Framework\Controller\Result\JsonFactory::class) ->setMethods(['create']) ->disableOriginalConstructor() ->getMock(); - $this->sessionTransparentMock = $this->getMockBuilder(\Magento\Framework\Session\Generic::class) + $this->sessionTransparent = $this->getMockBuilder(\Magento\Framework\Session\Generic::class) ->setMethods(['setQuoteId']) ->disableOriginalConstructor() ->getMock(); - $this->secureTokenServiceMock = $this->getMockBuilder( + $this->secureTokenService = $this->getMockBuilder( \Magento\Paypal\Model\Payflow\Service\Request\SecureToken::class ) ->setMethods(['requestToken']) ->disableOriginalConstructor() ->getMock(); - $this->sessionManagerMock = $this->getMockBuilder(\Magento\Framework\Session\SessionManager::class) + $this->sessionManager = $this->getMockBuilder(\Magento\Framework\Session\SessionManager::class) ->setMethods(['getQuote']) ->disableOriginalConstructor() ->getMock(); - $this->transparentMock = $this->getMockBuilder(\Magento\Paypal\Model\Payflow\Transparent::class) - ->setMethods(['getCode']) + $this->transparent = $this->getMockBuilder(\Magento\Paypal\Model\Payflow\Transparent::class) + ->setMethods(['getCode', 'isActive']) ->disableOriginalConstructor() ->getMock(); $this->controller = new \Magento\Paypal\Controller\Transparent\RequestSecureToken( - $this->contextMock, - $this->resultJsonFactoryMock, - $this->sessionTransparentMock, - $this->secureTokenServiceMock, - $this->sessionManagerMock, - $this->transparentMock + $this->context, + $this->resultJsonFactory, + $this->sessionTransparent, + $this->secureTokenService, + $this->sessionManager, + $this->transparent ); } public function testExecuteSuccess() { $quoteId = 99; + $storeId = 2; $tokenFields = ['fields-1', 'fields-2', 'fields-3']; $secureToken = 'token_hash'; $resultExpectation = [ @@ -116,6 +119,8 @@ public function testExecuteSuccess() $quoteMock = $this->getMockBuilder(\Magento\Quote\Model\Quote::class) ->disableOriginalConstructor() ->getMock(); + $quoteMock->method('getStoreId') + ->willReturn($storeId); $tokenMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) ->disableOriginalConstructor() ->getMock(); @@ -123,21 +128,23 @@ public function testExecuteSuccess() ->disableOriginalConstructor() ->getMock(); - $this->sessionManagerMock->expects($this->atLeastOnce()) + $this->sessionManager->expects($this->atLeastOnce()) ->method('getQuote') ->willReturn($quoteMock); + $this->transparent->method('isActive') + ->with($storeId) + ->willReturn(true); $quoteMock->expects($this->once()) ->method('getId') ->willReturn($quoteId); - $this->sessionTransparentMock->expects($this->once()) + $this->sessionTransparent->expects($this->once()) ->method('setQuoteId') ->with($quoteId); - $this->secureTokenServiceMock->expects($this->once()) + $this->secureTokenService->expects($this->once()) ->method('requestToken') ->with($quoteMock) ->willReturn($tokenMock); - $this->transparentMock->expects($this->once()) - ->method('getCode') + $this->transparent->method('getCode') ->willReturn('transparent'); $tokenMock->expects($this->atLeastOnce()) ->method('getData') @@ -147,7 +154,7 @@ public function testExecuteSuccess() ['securetoken', null, $secureToken] ] ); - $this->resultJsonFactoryMock->expects($this->once()) + $this->resultJsonFactory->expects($this->once()) ->method('create') ->willReturn($jsonMock); $jsonMock->expects($this->once()) @@ -161,6 +168,7 @@ public function testExecuteSuccess() public function testExecuteTokenRequestException() { $quoteId = 99; + $storeId = 2; $resultExpectation = [ 'success' => false, 'error' => true, @@ -170,24 +178,29 @@ public function testExecuteTokenRequestException() $quoteMock = $this->getMockBuilder(\Magento\Quote\Model\Quote::class) ->disableOriginalConstructor() ->getMock(); + $quoteMock->method('getStoreId') + ->willReturn($storeId); $jsonMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Json::class) ->disableOriginalConstructor() ->getMock(); - $this->sessionManagerMock->expects($this->atLeastOnce()) + $this->sessionManager->expects($this->atLeastOnce()) ->method('getQuote') ->willReturn($quoteMock); $quoteMock->expects($this->once()) ->method('getId') ->willReturn($quoteId); - $this->sessionTransparentMock->expects($this->once()) + $this->transparent->method('isActive') + ->with($storeId) + ->willReturn(true); + $this->sessionTransparent->expects($this->once()) ->method('setQuoteId') ->with($quoteId); - $this->secureTokenServiceMock->expects($this->once()) + $this->secureTokenService->expects($this->once()) ->method('requestToken') ->with($quoteMock) ->willThrowException(new \Exception()); - $this->resultJsonFactoryMock->expects($this->once()) + $this->resultJsonFactory->expects($this->once()) ->method('create') ->willReturn($jsonMock); $jsonMock->expects($this->once()) @@ -211,10 +224,10 @@ public function testExecuteEmptyQuoteError() ->disableOriginalConstructor() ->getMock(); - $this->sessionManagerMock->expects($this->atLeastOnce()) + $this->sessionManager->expects($this->atLeastOnce()) ->method('getQuote') ->willReturn($quoteMock); - $this->resultJsonFactoryMock->expects($this->once()) + $this->resultJsonFactory->expects($this->once()) ->method('create') ->willReturn($jsonMock); $jsonMock->expects($this->once()) diff --git a/app/code/Magento/Paypal/Test/Unit/CustomerData/BillingAgreementTest.php b/app/code/Magento/Paypal/Test/Unit/CustomerData/BillingAgreementTest.php index 010c3f8f71de..82e94462445a 100644 --- a/app/code/Magento/Paypal/Test/Unit/CustomerData/BillingAgreementTest.php +++ b/app/code/Magento/Paypal/Test/Unit/CustomerData/BillingAgreementTest.php @@ -11,6 +11,7 @@ use Magento\Paypal\Model\Config; use Magento\Paypal\Model\ConfigFactory; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\Escaper; class BillingAgreementTest extends \PHPUnit\Framework\TestCase { @@ -35,9 +36,16 @@ class BillingAgreementTest extends \PHPUnit\Framework\TestCase */ private $billingAgreement; + /** + * @var Escaper + */ + private $escaperMock; + protected function setUp() { + $helper = new ObjectManager($this); $this->paypalConfig = $this->createMock(Config::class); + $this->escaperMock = $helper->getObject(Escaper::class); $this->paypalConfig ->expects($this->once()) ->method('setMethod') @@ -59,14 +67,13 @@ protected function setUp() ->willReturn($customerId); $this->paypalData = $this->createMock(Data::class); - - $helper = new ObjectManager($this); $this->billingAgreement = $helper->getObject( BillingAgreement::class, [ 'paypalConfigFactory' => $paypalConfigFactory, 'paypalData' => $this->paypalData, - 'currentCustomer' => $this->currentCustomer + 'currentCustomer' => $this->currentCustomer, + 'escaper' => $this->escaperMock ] ); } @@ -83,6 +90,10 @@ public function testGetSectionData() $this->assertArrayHasKey('askToCreate', $result); $this->assertArrayHasKey('confirmUrl', $result); $this->assertArrayHasKey('confirmMessage', $result); + $this->assertEquals( + 'Would you like to sign a billing agreement to streamline further purchases with PayPal?', + $result['confirmMessage'] + ); $this->assertTrue($result['askToCreate']); } diff --git a/app/code/Magento/Paypal/Test/Unit/Model/AbstractConfigTest.php b/app/code/Magento/Paypal/Test/Unit/Model/AbstractConfigTest.php index 9ec931821261..6bb2173e06f8 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/AbstractConfigTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/AbstractConfigTest.php @@ -189,14 +189,14 @@ public function getValueDataProvider() * * @dataProvider isWppApiAvailabeDataProvider */ - public function testIsWppApiAvailabe($returnMap, $expectedValue) + public function testIsWppApiAvailable($returnMap, $expectedValue) { $this->config->setMethod('paypal_express'); $this->scopeConfigMock->expects($this->any()) ->method('getValue') ->willReturnMap($returnMap); - $this->assertEquals($expectedValue, $this->config->isWppApiAvailabe()); + $this->assertEquals($expectedValue, $this->config->isWppApiAvailable()); } /** @@ -293,6 +293,48 @@ public function testIsMethodActive() $this->config->isMethodActive('method'); } + /** + * Check bill me later active setting uses disable funding options + * + * @param string|null $disableFundingOptions + * @param int $expectedFlag + * @param bool $expectedValue + * + * @dataProvider isMethodActiveBmlDataProvider + */ + public function testIsMethodActiveBml($disableFundingOptions, $expectedFlag, $expectedValue) + { + $this->scopeConfigMock->method('getValue') + ->with( + self::equalTo('payment/paypal_express/disable_funding_options'), + self::equalTo('store') + ) + ->willReturn($disableFundingOptions); + + $this->scopeConfigMock->method('isSetFlag') + ->with('payment/paypal_express_bml/active') + ->willReturn($expectedFlag); + + self::assertEquals($expectedValue, $this->config->isMethodActive('paypal_express_bml')); + } + + /** + * @return array + */ + public function isMethodActiveBmlDataProvider() + { + return [ + ['CREDIT,CARD,ELV', 0, false], + ['CREDIT,CARD,ELV', 1, true], + ['CREDIT', 0, false], + ['CREDIT', 1, true], + ['CARD', 0, true], + ['CARD', 1, true], + [null, 0, true], + [null, 1, true] + ]; + } + /** * Checks a case, when notation code based on Magento edition. */ diff --git a/app/code/Magento/Paypal/Test/Unit/Model/Config/Rules/ConverterTest.php b/app/code/Magento/Paypal/Test/Unit/Model/Config/Rules/ConverterTest.php index c1a3a5d5bd99..e7e723f1f3a5 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/Config/Rules/ConverterTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/Config/Rules/ConverterTest.php @@ -60,6 +60,7 @@ public function dataProviderExpectedData() 'value' => '0', 'predicate' => [ ], + 'include' => '', ], 'event1' => [ 'value' => '1', @@ -72,6 +73,7 @@ public function dataProviderExpectedData() 'argument2' => 'argument2', ], ], + 'include' => '', ], ], ], @@ -109,6 +111,7 @@ public function dataProviderExpectedData() 'event0' => [ 'value' => '0', 'predicate' => [], + 'include' => '', ], 'event1' => [ 'value' => '1', @@ -121,6 +124,7 @@ public function dataProviderExpectedData() 'argument2' => 'argument2', ], ], + 'include' => '', ], ], ], diff --git a/app/code/Magento/Paypal/Test/Unit/Model/Config/Structure/Element/FieldPluginTest.php b/app/code/Magento/Paypal/Test/Unit/Model/Config/Structure/Element/FieldPluginTest.php index 8615b91383aa..f0dda20b71c7 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/Config/Structure/Element/FieldPluginTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/Config/Structure/Element/FieldPluginTest.php @@ -7,7 +7,6 @@ use Magento\Paypal\Model\Config\Structure\Element\FieldPlugin as FieldConfigStructurePlugin; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -use Magento\Framework\App\RequestInterface; use Magento\Config\Model\Config\Structure\Element\Field as FieldConfigStructureMock; class FieldPluginTest extends \PHPUnit\Framework\TestCase @@ -22,11 +21,6 @@ class FieldPluginTest extends \PHPUnit\Framework\TestCase */ private $objectManagerHelper; - /** - * @var RequestInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $requestMock; - /** * @var FieldConfigStructureMock|\PHPUnit_Framework_MockObject_MockObject */ @@ -34,16 +28,13 @@ class FieldPluginTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->requestMock = $this->getMockBuilder(RequestInterface::class) - ->getMockForAbstractClass(); $this->subjectMock = $this->getMockBuilder(FieldConfigStructureMock::class) ->disableOriginalConstructor() ->getMock(); $this->objectManagerHelper = new ObjectManagerHelper($this); $this->plugin = $this->objectManagerHelper->getObject( - FieldConfigStructurePlugin::class, - ['request' => $this->requestMock] + FieldConfigStructurePlugin::class ); } @@ -56,10 +47,9 @@ public function testAroundGetConfigPathHasResult() public function testAroundGetConfigPathNonPaymentSection() { - $this->requestMock->expects(static::once()) - ->method('getParam') - ->with('section') - ->willReturn('non-payment'); + $this->subjectMock->expects($this->once()) + ->method('getPath') + ->willReturn('non-payment/group/field'); $this->assertNull($this->plugin->afterGetConfigPath($this->subjectMock, null)); } @@ -72,11 +62,7 @@ public function testAroundGetConfigPathNonPaymentSection() */ public function testAroundGetConfigPath($subjectPath, $expectedConfigPath) { - $this->requestMock->expects(static::once()) - ->method('getParam') - ->with('section') - ->willReturn('payment'); - $this->subjectMock->expects(static::once()) + $this->subjectMock->expects($this->exactly(2)) ->method('getPath') ->willReturn($subjectPath); diff --git a/app/code/Magento/Paypal/Test/Unit/Model/ConfigTest.php b/app/code/Magento/Paypal/Test/Unit/Model/ConfigTest.php index 113aa5766ed3..dd3cf11b87eb 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/ConfigTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/ConfigTest.php @@ -7,6 +7,7 @@ use Magento\Paypal\Model\Config; use Magento\Store\Model\ScopeInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; class ConfigTest extends \PHPUnit\Framework\TestCase { @@ -16,7 +17,7 @@ class ConfigTest extends \PHPUnit\Framework\TestCase private $model; /** - * @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject */ private $scopeConfig; @@ -117,14 +118,29 @@ public function testIsMethodAvailableWPPPE() */ public function testIsMethodAvailableForIsMethodActive($methodName, $expected) { - $this->scopeConfig->expects($this->any()) - ->method('getValue') - ->with('paypal/general/merchant_country') - ->will($this->returnValue('US')); - $this->scopeConfig->expects($this->exactly(2)) - ->method('isSetFlag') - ->withAnyParameters() - ->will($this->returnValue(true)); + if ($methodName == Config::METHOD_WPP_BML) { + $valueMap = [ + ['paypal/general/merchant_country', ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null, 'US'], + ['paypal/general/merchant_country', ScopeInterface::SCOPE_STORE, null, 'US'], + ['payment/paypal_express/disable_funding_options', ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null, []], + ]; + $this->scopeConfig + ->method('getValue') + ->willReturnMap($valueMap); + $this->scopeConfig->expects($this->exactly(1)) + ->method('isSetFlag') + ->withAnyParameters() + ->willReturn(true); + } else { + $this->scopeConfig + ->method('getValue') + ->with('paypal/general/merchant_country') + ->willReturn('US'); + $this->scopeConfig->expects($this->exactly(2)) + ->method('isSetFlag') + ->withAnyParameters() + ->willReturn(true); + } $this->model->setMethod($methodName); $this->assertEquals($expected, $this->model->isMethodAvailable($methodName)); @@ -219,6 +235,34 @@ public function testGetSpecificConfigPathPayflowAdvancedLink() $this->assertEquals('Authorization', $this->model->getValue('payment_action')); } + /** + * @param string $name + * @param string $expectedValue + * @param string|null $expectedResult + * + * @dataProvider payPalStylesDataProvider + */ + public function testGetSpecificConfigPathPayPalStyles($name, $expectedValue, $expectedResult) + { + // _mapGenericStyleFieldset + $this->scopeConfig->method('getValue') + ->with('paypal/style/' . $name) + ->willReturn($expectedValue); + + $this->assertEquals($expectedResult, $this->model->getValue($name)); + } + + /** + * @return array + */ + public function payPalStylesDataProvider(): array + { + return [ + ['checkout_page_button_customize', 'value', 'value'], + ['test', 'value', null], + ]; + } + /** * @dataProvider skipOrderReviewStepDataProvider */ diff --git a/app/code/Magento/Paypal/Test/Unit/Model/ExpressConfigProviderTest.php b/app/code/Magento/Paypal/Test/Unit/Model/ExpressConfigProviderTest.php index 935b4484a8d2..b316f92c0ce8 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/ExpressConfigProviderTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/ExpressConfigProviderTest.php @@ -5,7 +5,10 @@ */ namespace Magento\Paypal\Test\Unit\Model; +use Magento\Framework\UrlInterface; use Magento\Paypal\Model\ExpressConfigProvider; +use Magento\Paypal\Model\SmartButtonConfig; +use PHPUnit\Framework\MockObject\MockObject; class ExpressConfigProviderTest extends \PHPUnit\Framework\TestCase { @@ -40,16 +43,19 @@ public function testGetConfig() $payment->expects($this->atLeastOnce())->method('getCheckoutRedirectUrl')->willReturn('http://redirect.url'); $paymentHelper->expects($this->atLeastOnce())->method('getMethodInstance')->willReturn($payment); - /** @var \Magento\Framework\UrlInterface|\PHPUnit_Framework_MockObject_MockObject $urlBuilderMock */ + /** @var UrlInterface|MockObject $urlBuilderMock */ $urlBuilderMock = $this->createMock(\Magento\Framework\UrlInterface::class); + $smartButtonConfigMock = $this->createMock(SmartButtonConfig::class); + $configProvider = new ExpressConfigProvider( $configFactory, $localeResolver, $currentCustomer, $paypalHelper, $paymentHelper, - $urlBuilderMock + $urlBuilderMock, + $smartButtonConfigMock ); $configProvider->getConfig(); } diff --git a/app/code/Magento/Paypal/Test/Unit/Model/PayflowlinkTest.php b/app/code/Magento/Paypal/Test/Unit/Model/PayflowlinkTest.php index 80c8194e0765..5ac436bcf0a3 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/PayflowlinkTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/PayflowlinkTest.php @@ -131,7 +131,7 @@ public function testInitialize() ->method('postRequest') ->willReturn($response); - $this->payflowRequest->expects($this->exactly(3)) + $this->payflowRequest->expects($this->exactly(4)) ->method('setData') ->willReturnMap( [ diff --git a/app/code/Magento/Paypal/Test/Unit/Model/PayflowproTest.php b/app/code/Magento/Paypal/Test/Unit/Model/PayflowproTest.php index 3e8af6b2ee76..7c352fc497a3 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/PayflowproTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/PayflowproTest.php @@ -580,6 +580,40 @@ public function testPostRequestException() $this->payflowpro->postRequest($request, $config); } + /** + * @covers \Magento\Paypal\Model\Payflowpro::addRequestOrderInfo + */ + public function testAddRequestOrderInfo() + { + $orderData = [ + 'id' => 1, + 'increment_id' => '0000001' + ]; + $data = [ + 'ponum' => $orderData['id'], + 'custref' => $orderData['increment_id'], + 'invnum' => $orderData['increment_id'], + 'comment1' => $orderData['increment_id'] + ]; + $expectedData = new DataObject($data); + $actualData = new DataObject(); + + $orderMock = $this->getMockBuilder(\Magento\Sales\Model\Order::class) + ->disableOriginalConstructor() + ->setMethods(['getIncrementId', 'getId']) + ->getMock(); + $orderMock->expects(static::once()) + ->method('getId') + ->willReturn($orderData['id']); + $orderMock->expects(static::atLeastOnce()) + ->method('getIncrementId') + ->willReturn($orderData['increment_id']); + + $this->payflowpro->addRequestOrderInfo($actualData, $orderMock); + + $this->assertEquals($expectedData, $actualData); + } + /** * @covers \Magento\Paypal\Model\Payflowpro::assignData */ diff --git a/app/code/Magento/Paypal/Test/Unit/Model/SmartButtonConfigTest.php b/app/code/Magento/Paypal/Test/Unit/Model/SmartButtonConfigTest.php new file mode 100644 index 000000000000..ed62efe36c47 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Unit/Model/SmartButtonConfigTest.php @@ -0,0 +1,117 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Paypal\Test\Unit\Model; + +use Magento\Paypal\Model\SmartButtonConfig; +use Magento\Framework\Locale\ResolverInterface; +use Magento\Paypal\Model\ConfigFactory; + +class SmartButtonConfigTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Paypal\Model\SmartButtonConfig + */ + private $model; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $localeResolverMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $configMock; + + protected function setUp() + { + $this->localeResolverMock = $this->getMockForAbstractClass(ResolverInterface::class); + $this->configMock = $this->getMockBuilder(\Magento\Paypal\Model\Config::class) + ->disableOriginalConstructor() + ->getMock(); + /** @var \PHPUnit_Framework_MockObject_MockObject $configFactoryMock */ + $configFactoryMock = $this->getMockBuilder(ConfigFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + $configFactoryMock->expects($this->once())->method('create')->willReturn($this->configMock); + $this->model = new SmartButtonConfig( + $this->localeResolverMock, + $configFactoryMock, + $this->getDefaultStyles(), + $this->getAllowedFundings() + ); + } + + /** + * @param string $page + * @param string $locale + * @param string $disallowedFundings + * @param string $layout + * @param string $size + * @param string $shape + * @param string $label + * @param string $color + * @param string $installmentPeriodLabel + * @param string $installmentPeriodLocale + * @param array $expected + * @dataProvider getConfigDataProvider + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function testGetConfig( + string $page, + string $locale, + bool $isCustomize, + ?string $disallowedFundings, + string $layout, + string $size, + string $shape, + string $label, + string $color, + string $installmentPeriodLabel, + string $installmentPeriodLocale, + array $expected = [] + ) { + $this->localeResolverMock->expects($this->any())->method('getLocale')->willReturn($locale); + $this->configMock->expects($this->any())->method('getValue')->will($this->returnValueMap([ + ['merchant_id', null, 'merchant'], + ['sandbox_flag', null, true], + ['disable_funding_options', null, $disallowedFundings], + ["{$page}_page_button_customize", null, $isCustomize], + ["{$page}_page_button_layout", null, $layout], + ["{$page}_page_button_size", null, $size], + ["{$page}_page_button_color", null, $color], + ["{$page}_page_button_shape", null, $shape], + ["{$page}_page_button_label", null, $label], + [$page . '_page_button_' . $installmentPeriodLocale . '_installment_period', null, $installmentPeriodLabel] + ])); + + self::assertEquals($expected, $this->model->getConfig($page)); + } + + public function getConfigDataProvider() + { + return include __DIR__ . '/_files/expected_config.php'; + } + + /** + * @return array + */ + private function getDefaultStyles() + { + return include __DIR__ . '/_files/default_styles.php'; + } + + /** + * @return array + */ + private function getAllowedFundings() + { + return include __DIR__ . '/_files/allowed_fundings.php'; + } +} diff --git a/app/code/Magento/Paypal/Test/Unit/Model/_files/allowed_fundings.php b/app/code/Magento/Paypal/Test/Unit/Model/_files/allowed_fundings.php new file mode 100644 index 000000000000..6b6f8ccb87e1 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Unit/Model/_files/allowed_fundings.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +return [ + 'checkout' => [ + 'CREDIT', + 'ELV' + ], + 'cart' => [ + 'CREDIT', + 'ELV' + ], + 'mini_cart' => [ + 'CREDIT', + 'ELV' + ], + 'product' => [ + 'CREDIT' + ] +]; diff --git a/app/code/Magento/Paypal/Test/Unit/Model/_files/default_styles.php b/app/code/Magento/Paypal/Test/Unit/Model/_files/default_styles.php new file mode 100644 index 000000000000..87da99ed2e17 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Unit/Model/_files/default_styles.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +return [ + 'checkout' => [ + 'layout' => 'vertical', + 'size' => 'responsive', + 'color' => 'gold', + 'shape' => 'rect', + 'label' =>'paypal' + ], + 'cart' => [ + 'layout' => 'vertical', + 'size' => 'responsive', + 'color' => 'gold', + 'shape' => 'rect', + 'label' =>'paypal' + ], + 'mini_cart' => [ + 'layout' => 'vertical', + 'size' => 'responsive', + 'color' => 'gold', + 'shape' => 'rect', + 'label' =>'paypal' + ], + 'product' => [ + 'layout' => 'horizontal', + 'size' => 'responsive', + 'color' => 'gold', + 'shape' => 'pill', + 'label' =>'buynow' + ] +]; diff --git a/app/code/Magento/Paypal/Test/Unit/Model/_files/expected_config.php b/app/code/Magento/Paypal/Test/Unit/Model/_files/expected_config.php new file mode 100644 index 000000000000..3a76d11e5137 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Unit/Model/_files/expected_config.php @@ -0,0 +1,146 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +return [ + 'cart' => [ + 'cart', + 'es_MX', + true, + 'CREDIT', + 'horizontal', + 'small', + 'pillow', + 'installment', + 'blue', + 'my_label', + 'mx', + [ + 'merchantId' => 'merchant', + 'environment' => 'sandbox', + 'locale' => 'es_MX', + 'allowedFunding' => ['ELV'], + 'disallowedFunding' => ['CREDIT'], + 'styles' => [ + 'layout' => 'horizontal', + 'size' => 'small', + 'color' => 'blue', + 'shape' => 'pillow', + 'label' => 'installment', + 'installmentperiod' => 0 + ] + ] + ], + 'checkout' => [ + 'cart', + 'en_BR', + true, + null, + 'horizontal', + 'small', + 'pillow', + 'installment', + 'blue', + 'my_label', + 'br', + [ + 'merchantId' => 'merchant', + 'environment' => 'sandbox', + 'locale' => 'en_BR', + 'allowedFunding' => ['CREDIT', 'ELV'], + 'disallowedFunding' => [], + 'styles' => [ + 'layout' => 'horizontal', + 'size' => 'small', + 'color' => 'blue', + 'shape' => 'pillow', + 'label' => 'installment', + 'installmentperiod' => 0 + ] + ] + ], + 'mini_cart' => [ + 'cart', + 'en', + false, + null, + 'horizontal', + 'small', + 'pillow', + 'installment', + 'blue', + 'my_label', + 'br', + [ + 'merchantId' => 'merchant', + 'environment' => 'sandbox', + 'locale' => 'en', + 'allowedFunding' => ['CREDIT', 'ELV'], + 'disallowedFunding' => [], + 'styles' => [ + 'layout' => 'vertical', + 'size' => 'responsive', + 'color' => 'gold', + 'shape' => 'rect', + 'label' => 'paypal' + ] + ] + ], + 'mini_cart' => [ + 'cart', + 'en', + false, + null, + 'horizontal', + 'small', + 'pillow', + 'installment', + 'blue', + 'my_label', + 'br', + [ + 'merchantId' => 'merchant', + 'environment' => 'sandbox', + 'locale' => 'en', + 'allowedFunding' => ['CREDIT', 'ELV'], + 'disallowedFunding' => [], + 'styles' => [ + 'layout' => 'vertical', + 'size' => 'responsive', + 'color' => 'gold', + 'shape' => 'rect', + 'label' => 'paypal' + ] + ] + ], + 'product' => [ + 'cart', + 'en', + false, + 'CREDIT', + 'horizontal', + 'small', + 'pillow', + 'installment', + 'blue', + 'my_label', + 'br', + [ + 'merchantId' => 'merchant', + 'environment' => 'sandbox', + 'locale' => 'en', + 'allowedFunding' => ['ELV'], + 'disallowedFunding' => ['CREDIT'], + 'styles' => [ + 'layout' => 'vertical', + 'size' => 'responsive', + 'color' => 'gold', + 'shape' => 'rect', + 'label' => 'paypal', + ] + ] + ] +]; diff --git a/app/code/Magento/Paypal/Test/Unit/Observer/AddPaypalShortcutsObserverTest.php b/app/code/Magento/Paypal/Test/Unit/Observer/AddPaypalShortcutsObserverTest.php index 7cb521073e30..542b327475de 100644 --- a/app/code/Magento/Paypal/Test/Unit/Observer/AddPaypalShortcutsObserverTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Observer/AddPaypalShortcutsObserverTest.php @@ -9,9 +9,9 @@ use Magento\Catalog\Block\ShortcutInterface; use Magento\Framework\DataObject; use Magento\Framework\Event\Observer; -use Magento\Framework\View\Element\Template; use Magento\Framework\View\Layout; -use Magento\Paypal\Block\Express\InContext\Minicart\Button; +use Magento\Paypal\Block\Express\InContext\Minicart\SmartButton as MinicartButton; +use Magento\Paypal\Block\Express\InContext\SmartButton as Button; use Magento\Paypal\Helper\Shortcut\Factory; use Magento\Paypal\Model\Config; use Magento\Paypal\Observer\AddPaypalShortcutsObserver; @@ -119,7 +119,7 @@ public function testAddShortcutsButtons(array $blocks) ++$callIndexSession; } - $blockMock = $this->getMockBuilder(Button::class) + $blockMock = $this->getMockBuilder(MinicartButton::class) ->setMethods(['setIsInCatalogProduct', 'setShowOrPosition']) ->disableOriginalConstructor() ->getMockForAbstractClass(); @@ -159,7 +159,12 @@ public function dataProviderShortcutsButtons() return [ [ 'blocks1' => [ - \Magento\Paypal\Block\Express\InContext\Minicart\Button::class => [ + MinicartButton::class => [ + self::PAYMENT_CODE => Config::METHOD_WPS_EXPRESS, + self::PAYMENT_AVAILABLE => true, + self::PAYMENT_IS_BML => false, + ], + Button::class => [ self::PAYMENT_CODE => Config::METHOD_WPS_EXPRESS, self::PAYMENT_AVAILABLE => true, self::PAYMENT_IS_BML => false, @@ -198,11 +203,16 @@ public function dataProviderShortcutsButtons() ], [ 'blocks2' => [ - \Magento\Paypal\Block\Express\InContext\Minicart\Button::class => [ + MinicartButton::class => [ self::PAYMENT_CODE => Config::METHOD_WPS_EXPRESS, self::PAYMENT_AVAILABLE => false, self::PAYMENT_IS_BML => false, ], + Button::class => [ + self::PAYMENT_CODE => Config::METHOD_WPS_EXPRESS, + self::PAYMENT_AVAILABLE => true, + self::PAYMENT_IS_BML => false, + ], \Magento\Paypal\Block\Express\Shortcut::class => [ self::PAYMENT_CODE => Config::METHOD_WPP_EXPRESS, self::PAYMENT_AVAILABLE => false, diff --git a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_au.xml b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_au.xml index 99a6668c0153..cbb95e376c9f 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_au.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_au.xml @@ -132,6 +132,7 @@ <rule type="conflict" event=":load"> <argument name="wps_other">wps_other</argument> </rule> + <rule type="removeCreditOptionConditional" event=":load"/> </relation> </payment> </rules> diff --git a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_ca.xml b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_ca.xml index a8aac92fccd6..51297a96438d 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_ca.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_ca.xml @@ -207,6 +207,7 @@ <rule type="paypalExpressLockConfigurationConditional" event=":load"> <argument name="payflow_link_ca">payflow_link_ca</argument> </rule> + <rule type="removeCreditOptionConditional" event=":load"/> </relation> </payment> </rules> diff --git a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_de.xml b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_de.xml index fd570c9822f2..46c61b52b75d 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_de.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_de.xml @@ -26,6 +26,7 @@ <rule type="inContextActivate" event="activate-in-context-api"/> <rule type="inContextDeactivate" event="deactivate-in-context-api"/> <rule type="inContextDisableConditional" event=":load"/> + <rule type="removeCreditOptionConditional" event=":load"/> </relation> </payment> </rules> diff --git a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_es.xml b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_es.xml index fd2bcb266763..28cc075e0c61 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_es.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_es.xml @@ -95,6 +95,7 @@ <rule type="conflict" event=":load"> <argument name="wps_other">wps_other</argument> </rule> + <rule type="removeCreditOptionConditional" event=":load"/> </relation> </payment> </rules> diff --git a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_fr.xml b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_fr.xml index da0c1bd63534..7f1fcc08334f 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_fr.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_fr.xml @@ -95,6 +95,7 @@ <rule type="conflict" event=":load"> <argument name="wps_other">wps_other</argument> </rule> + <rule type="removeCreditOptionConditional" event=":load"/> </relation> </payment> </rules> diff --git a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_gb.xml b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_gb.xml index a809817fe9b3..d8b765b9b4d2 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_gb.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_gb.xml @@ -15,7 +15,7 @@ <predicate name="confirm" message="There is already another PayPal solution enabled. Enable this solution instead?" event="deactivate-rule" - > + > <argument name="wps_express">wps_express</argument> </predicate> </event> @@ -39,16 +39,16 @@ <predicate name="confirm" message="There is already another PayPal solution enabled. Enable this solution instead?" event="deactivate-rule" - > + > <argument name="payments_pro_hosted_solution_with_express_checkout">payments_pro_hosted_solution_with_express_checkout</argument> - <argument name="express_checkout_us">express_checkout_us</argument> + <argument name="express_checkout_gb">express_checkout_gb</argument> </predicate> </event> </events> <relation target="payments_pro_hosted_solution_with_express_checkout"> <rule type="disable" event="activate-rule"/> </relation> - <relation target="express_checkout_us"> + <relation target="express_checkout_gb"> <rule type="disable" event="activate-rule"/> </relation> <relation target=":self"> @@ -56,19 +56,19 @@ <rule type="simpleDisable" event="deactivate-rule"/> <rule type="conflict" event=":load"> <argument name="payments_pro_hosted_solution_with_express_checkout">payments_pro_hosted_solution_with_express_checkout</argument> - <argument name="express_checkout_us">express_checkout_us</argument> + <argument name="express_checkout_gb">express_checkout_gb</argument> </rule> </relation> </payment> <!-- Express Checkout --> - <payment id="express_checkout_us"> + <payment id="express_checkout_gb"> <events selector="[data-enable='payment']"> <event value="0" name="deactivate-rule"/> <event value="1" name="activate-rule"> <predicate name="confirm" message="There is already another PayPal solution enabled. Enable this solution instead?" event="deactivate-rule" - > + > <argument name="wps_express">wps_express</argument> </predicate> </event> @@ -95,6 +95,7 @@ <rule type="conflict" event=":load"> <argument name="wps_express">wps_express</argument> </rule> + <rule type="removeCreditOptionConditional" event=":load"/> </relation> </payment> -</rules> +</rules> \ No newline at end of file diff --git a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_hk.xml b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_hk.xml index 1c5dbaf22f97..50ce14e66ee0 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_hk.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_hk.xml @@ -95,6 +95,7 @@ <rule type="conflict" event=":load"> <argument name="wps_other">wps_other</argument> </rule> + <rule type="removeCreditOptionConditional" event=":load"/> </relation> </payment> </rules> diff --git a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_it.xml b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_it.xml index 2fe4ad78d4bf..de059dcc59c3 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_it.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_it.xml @@ -95,6 +95,7 @@ <rule type="conflict" event=":load"> <argument name="wps_other">wps_other</argument> </rule> + <rule type="removeCreditOptionConditional" event=":load"/> </relation> </payment> </rules> diff --git a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_jp.xml b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_jp.xml index e6fe55aa9049..d9fc7ef3f201 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_jp.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_jp.xml @@ -95,6 +95,7 @@ <rule type="conflict" event=":load"> <argument name="wps_other">wps_other</argument> </rule> + <rule type="removeCreditOptionConditional" event=":load"/> </relation> </payment> </rules> diff --git a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_nz.xml b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_nz.xml index 79309bcee701..c5b8b09c3a2c 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_nz.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_nz.xml @@ -95,6 +95,7 @@ <rule type="conflict" event=":load"> <argument name="wps_other">wps_other</argument> </rule> + <rule type="removeCreditOptionConditional" event=":load"/> </relation> </payment> </rules> diff --git a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_other.xml b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_other.xml index a4118cc964fc..972cc45505ec 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_other.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_other.xml @@ -64,6 +64,7 @@ <rule type="conflict" event=":load"> <argument name="wps_other">wps_other</argument> </rule> + <rule type="removeCreditOptionConditional" event=":load"/> </relation> </payment> </rules> diff --git a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_us.xml b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_us.xml index 02cb608c07c8..b7924e770aa2 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/rules/payment_us.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/rules/payment_us.xml @@ -396,6 +396,10 @@ <event value="0" name="deactivate-in-context-api"/> <event value="1" name="activate-in-context-api"/> </events> + <events selector="[data-enable='disable-funding-options']"> + <event value="CREDIT" include="true" name="remove-option"/> + <event value="CREDIT" include="false" name="add-option"/> + </events> <relation target="wps_express"> <rule type="disable" event="activate-rule"/> </relation> @@ -433,6 +437,9 @@ <argument name="paypal_payflowpro_with_express_checkout">paypal_payflowpro_with_express_checkout</argument> <argument name="payflow_link_us">payflow_link_us</argument> </rule> + <rule type="removeCreditOption" event="remove-option"/> + <rule type="addCreditOption" event="add-option"/> + <rule type="removeCreditOptionConditional" event=":load"/> </relation> </payment> </rules> diff --git a/app/code/Magento/Paypal/etc/adminhtml/system.xml b/app/code/Magento/Paypal/etc/adminhtml/system.xml index 26ec9b3152e4..ea48aa65132e 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/system.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/system.xml @@ -106,12 +106,23 @@ <field id="enable_express_checkout"> <config_path>payment/wps_express/active</config_path> </field> - <field id="enable_express_checkout_bml"> + <field id="enable_express_checkout_bml" showInDefault="1" showInWebsite="1"> <config_path>payment/wps_express_bml/active</config_path> </field> + <field id="express_checkout_bml_sort_order" showInDefault="1" showInWebsite="1"/> </group> <group id="settings_ec" translate="label"> <label>Basic Settings - PayPal Website Payments Standard</label> + <group id="settings_ec_advanced"> + <group id="express_checkout_frontend"> + <field id="checkout_display" showInDefault="0" showInWebsite="0" showInStore="0"/> + <group id="checkout_page_button" showInDefault="0" showInWebsite="0" showInStore="0"/> + <group id="product_page_button" showInDefault="0" showInWebsite="0" showInStore="0"/> + <group id="cart_page_button" showInDefault="0" showInWebsite="0" showInStore="0"/> + <group id="mini_cart_page_button" showInDefault="0" showInWebsite="0" showInStore="0"/> + <group id="features" showInDefault="0" showInWebsite="0" showInStore="0"/> + </group> + </group> </group> </group> </group> diff --git a/app/code/Magento/Paypal/etc/adminhtml/system/express_checkout.xml b/app/code/Magento/Paypal/etc/adminhtml/system/express_checkout.xml index bff076aad9cb..7abefbe1a674 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/system/express_checkout.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/system/express_checkout.xml @@ -158,7 +158,7 @@ </depends> <validate>required-entry</validate> </field> - <field id="enable_express_checkout_bml" translate="label comment" type="select" sortOrder="23" showInDefault="1" showInWebsite="1"> + <field id="enable_express_checkout_bml" translate="label comment" type="select" sortOrder="23" showInDefault="0" showInWebsite="0"> <label>Enable PayPal Credit</label> <comment><![CDATA[PayPal Express Checkout lets you give customers access to financing through PayPal Credit® - at no additional cost to you. You get paid up front, even though customers have more time to pay. A pre-integrated payment button lets customers pay quickly with PayPal Credit®. @@ -171,7 +171,7 @@ <field id="enable_express_checkout"/> </requires> </field> - <field id="express_checkout_bml_sort_order" translate="label" type="text" sortOrder="25" showInDefault="1" showInWebsite="1" showInStore="1"> + <field id="express_checkout_bml_sort_order" translate="label" type="text" sortOrder="25" showInDefault="0" showInWebsite="0" showInStore="0"> <label>Sort Order PayPal Credit</label> <config_path>payment/paypal_express_bml/sort_order</config_path> <frontend_class>validate-number</frontend_class> @@ -645,6 +645,262 @@ </tooltip> <attribute type="shared">1</attribute> </field> + <field id="checkout_display" translate="label" sortOrder="80" showInDefault="1" showInWebsite="1" showInStore="1"> + <label>Customize Smart Buttons</label> + <frontend_model>Magento\Config\Block\System\Config\Form\Field\Heading</frontend_model> + <attribute type="shared">1</attribute> + </field> + <group id="checkout_page_button" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="90"> + <label>Checkout Page</label> + <field id="checkout_page_button_customize" translate="label" type="select" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="10"> + <label>Customize Button</label> + <config_path>paypal/style/checkout_page_button_customize</config_path> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <attribute type="shared">1</attribute> + </field> + <field id="checkout_page_button_label" translate="label comment" type="select" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="20"> + <label>Label</label> + <comment><![CDATA[The installment feature is available only in these locales: en_MX, es_MX, en_BR, pt_BR.]]></comment> + <config_path>paypal/style/checkout_page_button_label</config_path> + <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Depends\ButtonStylesLabel</frontend_model> + <source_model>Magento\Paypal\Model\System\Config\Source\ButtonStyles::getLabel</source_model> + <depends> + <field id="checkout_page_button_customize">1</field> + </depends> + <attribute type="shared">1</attribute> + </field> + <field id="checkout_page_button_mx_installment_period" translate="label" type="select" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="20"> + <label>Mexico Installment Period</label> + <config_path>paypal/style/checkout_page_button_mx_installment_period</config_path> + <source_model>Magento\Paypal\Model\System\Config\Source\ButtonStyles::getMxInstallmentPeriod</source_model> + <depends> + <field id="checkout_page_button_customize">1</field> + <field id="checkout_page_button_label">installment</field> + </depends> + <attribute type="shared">1</attribute> + </field> + <field id="checkout_page_button_br_installment_period" translate="label" type="select" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="20"> + <label>Brazil Installment Period</label> + <config_path>paypal/style/checkout_page_button_br_installment_period</config_path> + <source_model>Magento\Paypal\Model\System\Config\Source\ButtonStyles::getBrInstallmentPeriod</source_model> + <depends> + <field id="checkout_page_button_customize">1</field> + <field id="checkout_page_button_label">installment</field> + </depends> + <attribute type="shared">1</attribute> + </field> + <field id="checkout_page_button_layout" translate="label" type="select" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="30"> + <label>Layout</label> + <config_path>paypal/style/checkout_page_button_layout</config_path> + <source_model>Magento\Paypal\Model\System\Config\Source\ButtonStyles::getLayout</source_model> + <depends> + <field id="checkout_page_button_customize">1</field> + <field id="checkout_page_button_label" negative="1">credit</field> + </depends> + <attribute type="shared">1</attribute> + </field> + <field id="checkout_page_button_size" translate="label tooltip" type="select" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="40"> + <label>Size</label> + <config_path>paypal/style/checkout_page_button_size</config_path> + <source_model>Magento\Paypal\Model\System\Config\Source\ButtonStyles::getSize</source_model> + <tooltip>Select Responsive to ensure the PayPal button renders correctly on mobile devices.</tooltip> + <depends> + <field id="checkout_page_button_customize">1</field> + </depends> + <attribute type="shared">1</attribute> + </field> + <field id="checkout_page_button_shape" translate="label" type="select" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="50"> + <label>Shape</label> + <config_path>paypal/style/checkout_page_button_shape</config_path> + <source_model>Magento\Paypal\Model\System\Config\Source\ButtonStyles::getShape</source_model> + <depends> + <field id="checkout_page_button_customize">1</field> + </depends> + <attribute type="shared">1</attribute> + </field> + <field id="checkout_page_button_color" translate="label" type="select" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="60"> + <label>Color</label> + <config_path>paypal/style/checkout_page_button_color</config_path> + <source_model>Magento\Paypal\Model\System\Config\Source\ButtonStyles::getColor</source_model> + <depends> + <field id="checkout_page_button_customize">1</field> + <field id="checkout_page_button_label" negative="1">credit</field> + </depends> + <attribute type="shared">1</attribute> + </field> + </group> + <group id="product_page_button" translate="label comment" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="100"> + <label>Product Pages</label> + <field id="product_page_button_customize" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_customize"> + <config_path>paypal/style/product_page_button_customize</config_path> + </field> + <field id="product_page_button_label" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_label"> + <config_path>paypal/style/product_page_button_label</config_path> + <depends> + <field id="product_page_button_customize">1</field> + </depends> + </field> + <field id="product_page_button_mx_installment_period" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_mx_installment_period"> + <config_path>paypal/style/product_page_button_mx_installment_period</config_path> + <depends> + <field id="product_page_button_customize">1</field> + <field id="product_page_button_label">installment</field> + </depends> + </field> + <field id="product_page_button_br_installment_period" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_br_installment_period"> + <config_path>paypal/style/product_page_button_br_installment_period</config_path> + <depends> + <field id="product_page_button_customize">1</field> + <field id="product_page_button_label">installment</field> + </depends> + </field> + <field id="product_page_button_layout" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_layout"> + <config_path>paypal/style/product_page_button_layout</config_path> + <depends> + <field id="product_page_button_customize">1</field> + <field id="product_page_button_label" negative="1">credit</field> + </depends> + </field> + <field id="product_page_button_size" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_size"> + <config_path>paypal/style/product_page_button_size</config_path> + <depends> + <field id="product_page_button_customize">1</field> + </depends> + </field> + <field id="product_page_button_shape" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_shape"> + <config_path>paypal/style/product_page_button_shape</config_path> + <depends> + <field id="product_page_button_customize">1</field> + </depends> + </field> + <field id="product_page_button_color" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_color"> + <config_path>paypal/style/product_page_button_color</config_path> + <depends> + <field id="product_page_button_customize">1</field> + <field id="product_page_button_label" negative="1">credit</field> + </depends> + </field> + </group> + <group id="cart_page_button" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="110"> + <label>Cart Page</label> + <field id="cart_page_button_customize" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_customize"> + <config_path>paypal/style/cart_page_button_customize</config_path> + </field> + <field id="cart_page_button_label" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_label"> + <config_path>paypal/style/cart_page_button_label</config_path> + <depends> + <field id="cart_page_button_customize">1</field> + </depends> + </field> + <field id="cart_page_button_mx_installment_period" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_mx_installment_period"> + <config_path>paypal/style/cart_page_button_mx_installment_period</config_path> + <depends> + <field id="cart_page_button_customize">1</field> + <field id="cart_page_button_label">installment</field> + </depends> + </field> + <field id="cart_page_button_br_installment_period" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_br_installment_period"> + <config_path>paypal/style/cart_page_button_br_installment_period</config_path> + <depends> + <field id="cart_page_button_customize">1</field> + <field id="cart_page_button_label">installment</field> + </depends> + </field> + <field id="cart_page_button_layout" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_layout"> + <config_path>paypal/style/cart_page_button_layout</config_path> + <depends> + <field id="cart_page_button_customize">1</field> + <field id="cart_page_button_label" negative="1">credit</field> + </depends> + </field> + <field id="cart_page_button_size" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_size"> + <config_path>paypal/style/cart_page_button_size</config_path> + <depends> + <field id="cart_page_button_customize">1</field> + </depends> + </field> + <field id="cart_page_button_shape" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_shape"> + <config_path>paypal/style/cart_page_button_shape</config_path> + <depends> + <field id="cart_page_button_customize">1</field> + </depends> + </field> + <field id="cart_page_button_color" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_color"> + <config_path>paypal/style/cart_page_button_color</config_path> + <depends> + <field id="cart_page_button_customize">1</field> + <field id="cart_page_button_label" negative="1">credit</field> + </depends> + </field> + </group> + <group id="mini_cart_page_button" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="120"> + <label>Mini Cart</label> + <field id="mini_cart_page_button_customize" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_customize"> + <config_path>paypal/style/mini_cart_page_button_customize</config_path> + </field> + <field id="mini_cart_page_button_label" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_label"> + <config_path>paypal/style/mini_cart_page_button_label</config_path> + <depends> + <field id="mini_cart_page_button_customize">1</field> + </depends> + </field> + <field id="mini_cart_page_button_mx_installment_period" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_mx_installment_period"> + <config_path>paypal/style/mini_cart_page_button_mx_installment_period</config_path> + <depends> + <field id="mini_cart_page_button_customize">1</field> + <field id="mini_cart_page_button_label">installment</field> + </depends> + </field> + <field id="mini_cart_page_button_br_installment_period" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_br_installment_period"> + <config_path>paypal/style/mini_cart_page_button_br_installment_period</config_path> + <depends> + <field id="mini_cart_page_button_customize">1</field> + <field id="mini_cart_page_button_label">installment</field> + </depends> + </field> + <field id="mini_cart_page_button_layout" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_layout"> + <config_path>paypal/style/mini_cart_page_button_layout</config_path> + <depends> + <field id="mini_cart_page_button_customize">1</field> + <field id="mini_cart_page_button_label" negative="1">credit</field> + </depends> + </field> + <field id="mini_cart_page_button_size" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_size"> + <config_path>paypal/style/mini_cart_page_button_size</config_path> + <depends> + <field id="mini_cart_page_button_customize">1</field> + </depends> + </field> + <field id="mini_cart_page_button_shape" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_shape"> + <config_path>paypal/style/mini_cart_page_button_shape</config_path> + <depends> + <field id="mini_cart_page_button_customize">1</field> + </depends> + </field> + <field id="mini_cart_page_button_color" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/checkout_page_button/checkout_page_button_color"> + <config_path>paypal/style/mini_cart_page_button_color</config_path> + <depends> + <field id="mini_cart_page_button_customize">1</field> + <field id="mini_cart_page_button_label" negative="1">credit</field> + </depends> + </field> + </group> + <group id="features" translate="label comment" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="130"> + <label>Features</label> + <field id="disable_funding_options" translate="label comment" type="multiselect" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> + <label>Disable Funding Options</label> + <comment> + <![CDATA[PayPal will automatically display each enabled funding option to eligible buyers. + For example, PayPal Credit is only shown to buyers in countries where PayPal Credit is + offered and the currency offered by the merchant is USD.]]> + </comment> + <config_path>paypal/style/disable_funding_options</config_path> + <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\MultiSelect\DisabledFundingOptions</frontend_model> + <source_model>Magento\Paypal\Model\System\Config\Source\DisableFundingOptions</source_model> + <attribute type="shared">1</attribute> + <can_be_empty>1</can_be_empty> + </field> + </group> </group> </group> </group> diff --git a/app/code/Magento/Paypal/etc/adminhtml/system/payflow_advanced.xml b/app/code/Magento/Paypal/etc/adminhtml/system/payflow_advanced.xml index 5eb596c9c4f4..e7de9c0d641a 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/system/payflow_advanced.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/system/payflow_advanced.xml @@ -97,7 +97,7 @@ <field id="enable_payflow_advanced"/> </requires> </field> - <field id="enable_express_checkout_bml" sortOrder="42" extends="payment_all_paypal/express_checkout/express_checkout_required/enable_express_checkout_bml"> + <field id="enable_express_checkout_bml" sortOrder="42" extends="payment_all_paypal/express_checkout/express_checkout_required/enable_express_checkout_bml" showInDefault="1" showInWebsite="1"> <comment><![CDATA[PayPal Express Checkout Payflow Edition lets you give customers access to financing through PayPal Credit® - at no additional cost to you. You get paid up front, even though customers have more time to pay. A pre-integrated payment button lets customers pay quickly with PayPal Credit®. <a href="https://www.paypal.com/webapps/mpp/promotional-financing" target="_blank">Learn More</a>]]> @@ -108,7 +108,7 @@ <field id="enable_payflow_advanced"/> </requires> </field> - <field id="express_checkout_bml_sort_order" sortOrder="50" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_bml_sort_order"> + <field id="express_checkout_bml_sort_order" sortOrder="50" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_bml_sort_order" showInDefault="1" showInWebsite="1"> <config_path>payment/payflow_express_bml/sort_order</config_path> <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Depends\BmlSortOrder</frontend_model> <depends> diff --git a/app/code/Magento/Paypal/etc/adminhtml/system/payflow_link.xml b/app/code/Magento/Paypal/etc/adminhtml/system/payflow_link.xml index d27dde02c579..647bc7a60975 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/system/payflow_link.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/system/payflow_link.xml @@ -106,7 +106,7 @@ <field id="enable_payflow_link"/> </requires> </field> - <field id="enable_express_checkout_bml" extends="payment_all_paypal/express_checkout/express_checkout_required/enable_express_checkout_bml" sortOrder="41"> + <field id="enable_express_checkout_bml" extends="payment_all_paypal/express_checkout/express_checkout_required/enable_express_checkout_bml" sortOrder="41" showInDefault="1" showInWebsite="1"> <comment><![CDATA[Payflow Link lets you give customers access to financing through PayPal Credit® - at no additional cost to you. You get paid up front, even though customers have more time to pay. A pre-integrated payment button lets customers pay quickly with PayPal Credit®. <a href="https://www.paypal.com/webapps/mpp/promotional-financing" target="_blank">Learn More</a>]]> @@ -117,7 +117,7 @@ <field id="enable_express_checkout"/> </requires> </field> - <field id="express_checkout_bml_sort_order" sortOrder="50" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_bml_sort_order"> + <field id="express_checkout_bml_sort_order" sortOrder="50" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_bml_sort_order" showInDefault="1" showInWebsite="1"> <config_path>payment/payflow_express_bml/sort_order</config_path> <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Depends\BmlSortOrder</frontend_model> <depends> diff --git a/app/code/Magento/Paypal/etc/adminhtml/system/payments_pro_hosted_solution.xml b/app/code/Magento/Paypal/etc/adminhtml/system/payments_pro_hosted_solution.xml index 77acff48c247..35cd84420484 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/system/payments_pro_hosted_solution.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/system/payments_pro_hosted_solution.xml @@ -46,7 +46,7 @@ <frontend_class>paypal-enabler paypal-ec-separate</frontend_class> </field> - <field id="enable_express_checkout_bml" extends="payment_all_paypal/express_checkout/express_checkout_required/enable_express_checkout_bml" sortOrder="21"> + <field id="enable_express_checkout_bml" extends="payment_all_paypal/express_checkout/express_checkout_required/enable_express_checkout_bml" sortOrder="21" showInDefault="1" showInWebsite="1"> <comment><![CDATA[Payments Pro Hosted Solution lets you give customers access to financing through PayPal Credit® - at no additional cost to you. You get paid up front, even though customers have more time to pay. A pre-integrated payment button lets customers pay quickly with PayPal Credit®. <a href="https://www.paypal.com/webapps/mpp/promotional-financing" target="_blank">Learn More</a>]]> diff --git a/app/code/Magento/Paypal/etc/adminhtml/system/paypal_payflowpro_with_express_checkout.xml b/app/code/Magento/Paypal/etc/adminhtml/system/paypal_payflowpro_with_express_checkout.xml index 6090025024dd..694e517816b2 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/system/paypal_payflowpro_with_express_checkout.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/system/paypal_payflowpro_with_express_checkout.xml @@ -6,7 +6,7 @@ */ --> <include xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_include.xsd"> - <group id="paypal_payflowpro_with_express_checkout" translate="label comment" extends="payment_all_paypal/paypal_payflowpro"> + <group id="paypal_payflowpro_with_express_checkout" translate="label" extends="payment_all_paypal/paypal_payflowpro"> <label>Payflow Pro</label> <attribute type="paypal_ec_separate">0</attribute> <group id="paypal_payflow_required" translate="label" showInDefault="1" showInWebsite="1" sortOrder="10"> @@ -30,7 +30,7 @@ <field id="enable_paypal_payflow"/> </requires> </field> - <field id="enable_express_checkout_bml_payflow" translate="label" type="select" sortOrder="21" showInWebsite="1" showInDefault="1"> + <field id="enable_express_checkout_bml_payflow" translate="label comment" type="select" sortOrder="21" showInWebsite="1" showInDefault="1"> <label>Enable PayPal Credit</label> <comment><![CDATA[PayPal Express Checkout Payflow Edition lets you give customers access to financing through PayPal Credit® - at no additional cost to you. You get paid up front, even though customers have more time to pay. A pre-integrated payment button lets customers pay quickly with PayPal Credit®. @@ -43,7 +43,7 @@ <field id="enable_paypal_payflow"/> </requires> </field> - <field id="express_checkout_bml_sort_order" sortOrder="30" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_bml_sort_order"> + <field id="express_checkout_bml_sort_order" sortOrder="30" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_bml_sort_order" showInDefault="1" showInWebsite="1"> <config_path>payment/payflow_express_bml/sort_order</config_path> <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Depends\BmlSortOrder</frontend_model> <depends> diff --git a/app/code/Magento/Paypal/etc/config.xml b/app/code/Magento/Paypal/etc/config.xml index f0df648af907..1880417af1b4 100644 --- a/app/code/Magento/Paypal/etc/config.xml +++ b/app/code/Magento/Paypal/etc/config.xml @@ -10,6 +10,30 @@ <paypal> <style> <logo></logo> + <checkout_page_button_customize>0</checkout_page_button_customize> + <checkout_page_button_label>paypal</checkout_page_button_label> + <checkout_page_button_layout>vertical</checkout_page_button_layout> + <checkout_page_button_size>responsive</checkout_page_button_size> + <checkout_page_button_shape>rect</checkout_page_button_shape> + <checkout_page_button_color>gold</checkout_page_button_color> + <product_page_button_customize>0</product_page_button_customize> + <product_page_button_label>buynow</product_page_button_label> + <product_page_button_layout>horizontal</product_page_button_layout> + <product_page_button_size>responsive</product_page_button_size> + <product_page_button_shape>pill</product_page_button_shape> + <product_page_button_color>gold</product_page_button_color> + <cart_page_button_customize>0</cart_page_button_customize> + <cart_page_button_label>paypal</cart_page_button_label> + <cart_page_button_layout>vertical</cart_page_button_layout> + <cart_page_button_size>responsive</cart_page_button_size> + <cart_page_button_shape>rect</cart_page_button_shape> + <cart_page_button_color>gold</cart_page_button_color> + <mini_cart_page_button_customize>0</mini_cart_page_button_customize> + <mini_cart_page_button_label>paypal</mini_cart_page_button_label> + <mini_cart_page_button_layout>vertical</mini_cart_page_button_layout> + <mini_cart_page_button_size>responsive</mini_cart_page_button_size> + <mini_cart_page_button_shape>rect</mini_cart_page_button_shape> + <mini_cart_page_button_color>gold</mini_cart_page_button_color> </style> <wpp> <api_password backend_model="Magento\Config\Model\Config\Backend\Encrypted" /> @@ -100,6 +124,7 @@ <instant_purchase> <tokenFormat>\Magento\Paypal\Model\InstantPurchase\Payflow\Pro\TokenFormatter</tokenFormat> </instant_purchase> + <group>paypal</group> </payflowpro_cc_vault> <paypal_billing_agreement> <active>1</active> diff --git a/app/code/Magento/Paypal/etc/db_schema.xml b/app/code/Magento/Paypal/etc/db_schema.xml index 2703ee4f5be3..4dc283fcb48c 100644 --- a/app/code/Magento/Paypal/etc/db_schema.xml +++ b/app/code/Magento/Paypal/etc/db_schema.xml @@ -21,19 +21,19 @@ <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" comment="Store Id"/> <column xsi:type="varchar" name="agreement_label" nullable="true" length="255" comment="Agreement Label"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="agreement_id"/> </constraint> - <constraint xsi:type="foreign" name="PAYPAL_BILLING_AGREEMENT_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="PAYPAL_BILLING_AGREEMENT_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID" table="paypal_billing_agreement" column="customer_id" referenceTable="customer_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="PAYPAL_BILLING_AGREEMENT_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="PAYPAL_BILLING_AGREEMENT_STORE_ID_STORE_STORE_ID" table="paypal_billing_agreement" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="SET NULL"/> - <index name="PAYPAL_BILLING_AGREEMENT_CUSTOMER_ID" indexType="btree"> + <index referenceId="PAYPAL_BILLING_AGREEMENT_CUSTOMER_ID" indexType="btree"> <column name="customer_id"/> </index> - <index name="PAYPAL_BILLING_AGREEMENT_STORE_ID" indexType="btree"> + <index referenceId="PAYPAL_BILLING_AGREEMENT_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -43,17 +43,17 @@ comment="Agreement Id"/> <column xsi:type="int" name="order_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Order Id"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="agreement_id"/> <column name="order_id"/> </constraint> - <constraint xsi:type="foreign" name="PAYPAL_BILLING_AGRT_ORDER_AGRT_ID_PAYPAL_BILLING_AGRT_AGRT_ID" + <constraint xsi:type="foreign" referenceId="PAYPAL_BILLING_AGRT_ORDER_AGRT_ID_PAYPAL_BILLING_AGRT_AGRT_ID" table="paypal_billing_agreement_order" column="agreement_id" referenceTable="paypal_billing_agreement" referenceColumn="agreement_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="PAYPAL_BILLING_AGREEMENT_ORDER_ORDER_ID_SALES_ORDER_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="PAYPAL_BILLING_AGREEMENT_ORDER_ORDER_ID_SALES_ORDER_ENTITY_ID" table="paypal_billing_agreement_order" column="order_id" referenceTable="sales_order" referenceColumn="entity_id" onDelete="CASCADE"/> - <index name="PAYPAL_BILLING_AGREEMENT_ORDER_ORDER_ID" indexType="btree"> + <index referenceId="PAYPAL_BILLING_AGREEMENT_ORDER_ORDER_ID" indexType="btree"> <column name="order_id"/> </index> </table> @@ -64,10 +64,10 @@ <column xsi:type="varchar" name="account_id" nullable="true" length="64" comment="Account Id"/> <column xsi:type="varchar" name="filename" nullable="true" length="24" comment="Filename"/> <column xsi:type="timestamp" name="last_modified" on_update="false" nullable="true" comment="Last Modified"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="report_id"/> </constraint> - <constraint xsi:type="unique" name="PAYPAL_SETTLEMENT_REPORT_REPORT_DATE_ACCOUNT_ID"> + <constraint xsi:type="unique" referenceId="PAYPAL_SETTLEMENT_REPORT_REPORT_DATE_ACCOUNT_ID"> <column name="report_date"/> <column name="account_id"/> </constraint> @@ -105,13 +105,13 @@ <column xsi:type="varchar" name="payment_tracking_id" nullable="true" length="255" comment="Payment Tracking ID"/> <column xsi:type="varchar" name="store_id" nullable="true" length="50" comment="Store ID"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="row_id"/> </constraint> - <constraint xsi:type="foreign" name="FK_E183E488F593E0DE10C6EBFFEBAC9B55" table="paypal_settlement_report_row" + <constraint xsi:type="foreign" referenceId="FK_E183E488F593E0DE10C6EBFFEBAC9B55" table="paypal_settlement_report_row" column="report_id" referenceTable="paypal_settlement_report" referenceColumn="report_id" onDelete="CASCADE"/> - <index name="PAYPAL_SETTLEMENT_REPORT_ROW_REPORT_ID" indexType="btree"> + <index referenceId="PAYPAL_SETTLEMENT_REPORT_ROW_REPORT_ID" indexType="btree"> <column name="report_id"/> </index> </table> @@ -122,26 +122,26 @@ default="0" comment="Website Id"/> <column xsi:type="text" name="content" nullable="true" comment="Content"/> <column xsi:type="timestamp" name="updated_at" on_update="false" nullable="true" comment="Updated At"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="cert_id"/> </constraint> - <constraint xsi:type="foreign" name="PAYPAL_CERT_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID" table="paypal_cert" + <constraint xsi:type="foreign" referenceId="PAYPAL_CERT_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID" table="paypal_cert" column="website_id" referenceTable="store_website" referenceColumn="website_id" onDelete="CASCADE"/> - <index name="PAYPAL_CERT_WEBSITE_ID" indexType="btree"> + <index referenceId="PAYPAL_CERT_WEBSITE_ID" indexType="btree"> <column name="website_id"/> </index> </table> <table name="paypal_payment_transaction" resource="default" engine="innodb" comment="PayPal Payflow Link Payment Transaction"> <column xsi:type="int" name="transaction_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="varchar" name="txn_id" nullable="true" length="100" comment="Txn Id"/> <column xsi:type="blob" name="additional_information" nullable="true" comment="Additional Information"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="true" comment="Created At"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="transaction_id"/> </constraint> - <constraint xsi:type="unique" name="PAYPAL_PAYMENT_TRANSACTION_TXN_ID"> + <constraint xsi:type="unique" referenceId="PAYPAL_PAYMENT_TRANSACTION_TXN_ID"> <column name="txn_id"/> </constraint> </table> diff --git a/app/code/Magento/Paypal/etc/frontend/di.xml b/app/code/Magento/Paypal/etc/frontend/di.xml index 407c251fc42f..8c29ae1e2685 100644 --- a/app/code/Magento/Paypal/etc/frontend/di.xml +++ b/app/code/Magento/Paypal/etc/frontend/di.xml @@ -86,7 +86,7 @@ </argument> </arguments> </type> - <type name="Magento\Paypal\Block\Express\InContext\Minicart\Button"> + <type name="Magento\Paypal\Block\Express\InContext\Minicart\SmartButton"> <arguments> <argument name="data" xsi:type="array"> <item name="template" xsi:type="string">Magento_Paypal::express/in-context/shortcut/button.phtml</item> @@ -97,6 +97,14 @@ <argument name="payment" xsi:type="object">Magento\Paypal\Model\Express</argument> </arguments> </type> + <type name="Magento\Paypal\Block\Express\InContext\SmartButton"> + <arguments> + <argument name="data" xsi:type="array"> + <item name="alias" xsi:type="string">product.info.addtocart.paypalexpress</item> + <item name="template" xsi:type="string">express/shortcut_button.phtml</item> + </argument> + </arguments> + </type> <type name="Magento\Vault\Model\Ui\TokensConfigProvider"> @@ -116,4 +124,56 @@ <type name="Magento\Quote\Model\QuoteRepository\SaveHandler"> <plugin name="paypal-cartitem" type="Magento\Paypal\Model\Express\QuotePlugin"/> </type> + + <type name="Magento\Paypal\Model\SmartButtonConfig"> + <arguments> + <argument name="defaultStyles" xsi:type="array"> + <item name="checkout" xsi:type="array"> + <item name="layout" xsi:type="string">vertical</item> + <item name="size" xsi:type="string">responsive</item> + <item name="color" xsi:type="string">gold</item> + <item name="shape" xsi:type="string">rect</item> + <item name="label" xsi:type="string">paypal</item> + </item> + <item name="cart" xsi:type="array"> + <item name="layout" xsi:type="string">vertical</item> + <item name="size" xsi:type="string">responsive</item> + <item name="color" xsi:type="string">gold</item> + <item name="shape" xsi:type="string">rect</item> + <item name="label" xsi:type="string">paypal</item> + </item> + <item name="mini_cart" xsi:type="array"> + <item name="layout" xsi:type="string">vertical</item> + <item name="size" xsi:type="string">responsive</item> + <item name="color" xsi:type="string">gold</item> + <item name="shape" xsi:type="string">rect</item> + <item name="label" xsi:type="string">paypal</item> + </item> + <item name="product" xsi:type="array"> + <item name="layout" xsi:type="string">horizontal</item> + <item name="size" xsi:type="string">responsive</item> + <item name="color" xsi:type="string">gold</item> + <item name="shape" xsi:type="string">pill</item> + <item name="label" xsi:type="string">buynow</item> + </item> + </argument> + <argument name="allowedFunding" xsi:type="array"> + <item name="checkout" xsi:type="array"> + <item name="0" xsi:type="string">CREDIT</item> + <item name="1" xsi:type="string">ELV</item> + </item> + <item name="cart" xsi:type="array"> + <item name="0" xsi:type="string">CREDIT</item> + <item name="1" xsi:type="string">ELV</item> + </item> + <item name="mini_cart" xsi:type="array"> + <item name="0" xsi:type="string">CREDIT</item> + <item name="1" xsi:type="string">ELV</item> + </item> + <item name="product" xsi:type="array"> + <item name="0" xsi:type="string">CREDIT</item> + </item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Paypal/etc/frontend/sections.xml b/app/code/Magento/Paypal/etc/frontend/sections.xml index 466b930fb9be..ca32d57cd9d2 100644 --- a/app/code/Magento/Paypal/etc/frontend/sections.xml +++ b/app/code/Magento/Paypal/etc/frontend/sections.xml @@ -15,4 +15,8 @@ <section name="cart"/> <section name="checkout-data"/> </action> + <action name="paypal/express/onAuthorization"> + <section name="cart"/> + <section name="checkout-data"/> + </action> </config> diff --git a/app/code/Magento/Paypal/etc/rules.xsd b/app/code/Magento/Paypal/etc/rules.xsd index 9a274a2dbd1c..c4385396cc0c 100644 --- a/app/code/Magento/Paypal/etc/rules.xsd +++ b/app/code/Magento/Paypal/etc/rules.xsd @@ -31,6 +31,7 @@ </xs:sequence> <xs:attribute type="xs:string" name="value" use="required"/> <xs:attribute type="xs:string" name="name" use="required"/> + <xs:attribute type="xs:boolean" name="include" use="optional"/> </xs:complexType> <xs:complexType name="predicate"> <xs:sequence minOccurs="0" maxOccurs="unbounded"> diff --git a/app/code/Magento/Paypal/i18n/en_US.csv b/app/code/Magento/Paypal/i18n/en_US.csv index c3f3e38fa03b..e7264a6de807 100644 --- a/app/code/Magento/Paypal/i18n/en_US.csv +++ b/app/code/Magento/Paypal/i18n/en_US.csv @@ -697,3 +697,43 @@ User,User The PayPal Advertising Program has been shown to generate additional purchases as well as increase consumer's average purchase sizes by 15% or more. <a href=""https://financing.paypal.com/ppfinportal/content/forrester"" target=""_blank"">See Details</a>. " +"PayPal Express Checkout Payflow Edition lets you give customers access to financing through PayPal Credit® - at no additional cost to you. + You get paid up front, even though customers have more time to pay. A pre-integrated payment button lets customers pay quickly with PayPal Credit®. + <a href=""https://www.paypal.com/webapps/mpp/promotional-financing"" target=""_blank"">Learn More</a>","PayPal Express Checkout Payflow Edition lets you give customers access to financing through PayPal Credit® - at no additional cost to you. + You get paid up front, even though customers have more time to pay. A pre-integrated payment button lets customers pay quickly with PayPal Credit®. + <a href=""https://www.paypal.com/webapps/mpp/promotional-financing"" target=""_blank"">Learn More</a>" +"Customize Smart Buttons","Customize Smart Buttons" +"Checkout Page","Checkout Page" +"Label","Label" +"The installment feature is available only in these locales: en_MX, es_MX, en_BR, pt_BR.","The installment feature is available only in these locales: en_MX, es_MX, en_BR, pt_BR." +"Checkout","Checkout" +"Credit","Credit" +"Pay","Pay" +"Buy Now","Buy Now" +"PayPal","PayPal" +"Installment","Installment" +"Mexico Installment Period","Mexico Installment Period" +"Brazil Installment Period","Brazil Installment Period" +"Layout","Layout" +"Vertical","Vertical" +"Horizontal","Horizontal" +"Size","Size" +"Medium","Medium" +"Large","Large" +"Responsive","Responsive" +"Shape","Shape" +"Pill","Pill" +"Rectangle","Rectangle" +"Color","Color" +"Gold","Gold" +"Blue","Blue" +"Silver","Silver" +"Black","Black" +"Product Pages","Product Pages" +"Cart Page","Cart Page" +"Mini Cart","Mini Cart" +"Features","Features" +"PayPal will automatically display each enabled funding option to eligible buyers. For example, PayPal Credit is only shown to buyers in countries where PayPal Credit is offered and the currency offered by the merchant is USD.","PayPal will automatically display each enabled funding option to eligible buyers. For example, PayPal Credit is only shown to buyers in countries where PayPal Credit is offered and the currency offered by the merchant is USD." +"PayPal Credit","PayPal Credit" +"PayPal Guest Checkout Credit Card Icons","PayPal Guest Checkout Credit Card Icons" +"Elektronisches Lastschriftverfahren - German ELV","Elektronisches Lastschriftverfahren - German ELV" diff --git a/app/code/Magento/Paypal/view/adminhtml/layout/adminhtml_paypal_reports_block.xml b/app/code/Magento/Paypal/view/adminhtml/layout/adminhtml_paypal_reports_block.xml index 12dcc46e3ece..596381a4b114 100644 --- a/app/code/Magento/Paypal/view/adminhtml/layout/adminhtml_paypal_reports_block.xml +++ b/app/code/Magento/Paypal/view/adminhtml/layout/adminhtml_paypal_reports_block.xml @@ -11,7 +11,7 @@ <block class="Magento\Backend\Block\Widget\Grid" name="paypal.report.grid" as="grid"> <arguments> <argument name="id" xsi:type="string">settlementGrid</argument> - <argument name="dataSource" xsi:type="object">Magento\Paypal\Model\ResourceModel\Report\Settlement\Row\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Paypal\Model\ResourceModel\Report\Settlement\Row\Collection</argument> <argument name="default_sort" xsi:type="string">row_id</argument> <argument name="default_dir" xsi:type="string">DESC</argument> <argument name="save_parameters_in_session" xsi:type="string">1</argument> diff --git a/app/code/Magento/Paypal/view/adminhtml/layout/paypal_billing_agreement_ordersgrid.xml b/app/code/Magento/Paypal/view/adminhtml/layout/paypal_billing_agreement_ordersgrid.xml index 76a105f5abcd..0510011a6a55 100644 --- a/app/code/Magento/Paypal/view/adminhtml/layout/paypal_billing_agreement_ordersgrid.xml +++ b/app/code/Magento/Paypal/view/adminhtml/layout/paypal_billing_agreement_ordersgrid.xml @@ -9,7 +9,7 @@ <update handle="sales_order_grid_block"/> <referenceBlock name="sales.order.grid"> <arguments> - <argument name="dataSource" xsi:type="object"> + <argument name="dataSource" xsi:type="object" shared="false"> <updater>Magento\Paypal\Model\Billing\Agreement\OrdersUpdater</updater> </argument> <argument name="grid_url" xsi:type="url" path="*/*/ordersgrid"/> diff --git a/app/code/Magento/Paypal/view/adminhtml/layout/paypal_billing_agreement_view.xml b/app/code/Magento/Paypal/view/adminhtml/layout/paypal_billing_agreement_view.xml index c76539f5cb20..d9c376701db3 100644 --- a/app/code/Magento/Paypal/view/adminhtml/layout/paypal_billing_agreement_view.xml +++ b/app/code/Magento/Paypal/view/adminhtml/layout/paypal_billing_agreement_view.xml @@ -12,7 +12,7 @@ <referenceBlock name="sales.order.grid"> <arguments> - <argument name="dataSource" xsi:type="object"> + <argument name="dataSource" xsi:type="object" shared="false"> <updater>Magento\Paypal\Model\Billing\Agreement\OrdersUpdater</updater> </argument> <argument name="grid_url" xsi:type="url" path="*/*/ordersgrid"/> diff --git a/app/code/Magento/Paypal/view/adminhtml/web/js/rules.js b/app/code/Magento/Paypal/view/adminhtml/web/js/rules.js index 4a5248cb8758..555d2a80a861 100644 --- a/app/code/Magento/Paypal/view/adminhtml/web/js/rules.js +++ b/app/code/Magento/Paypal/view/adminhtml/web/js/rules.js @@ -585,6 +585,41 @@ define([ 'Please re-enable the previously enabled payment solutions.' }); } + }, + + /** + * @param {*} $target + * @param {*} $owner + * @param {Object} data + */ + removeCreditOption: function ($target, $owner, data) { + if ($target.find(data.dependsButtonLabel + ' option[value="credit"]').length > 0) { + $target.find(data.dependsButtonLabel + ' option[value="credit"]').remove(); + } + }, + + /** + * @param {*} $target + * @param {*} $owner + * @param {Object} data + */ + addCreditOption: function ($target, $owner, data) { + if ($target.find(data.dependsButtonLabel + ' option[value="credit"]').length === 0) { + $target.find(data.dependsButtonLabel).append('<option value="credit">Credit</option>'); + } + }, + + /** + * @param {*} $target + * @param {*} $owner + * @param {Object} data + */ + removeCreditOptionConditional: function ($target, $owner, data) { + if ($target.find(data.dependsDisableFundingOptions + ' option[value="CREDIT"]').length === 0 || + $target.find(data.dependsDisableFundingOptions + ' option[value="CREDIT"]:selected').length > 0 + ) { + this.removeCreditOption($target, $owner, data); + } } }); }); diff --git a/app/code/Magento/Paypal/view/adminhtml/web/js/solution.js b/app/code/Magento/Paypal/view/adminhtml/web/js/solution.js index 3d832db09aa8..3e4a1ab0ccc7 100644 --- a/app/code/Magento/Paypal/view/adminhtml/web/js/solution.js +++ b/app/code/Magento/Paypal/view/adminhtml/web/js/solution.js @@ -65,6 +65,18 @@ define([ */ dependsBmlApiSortOrder: '[data-enable="bml-api-sort-order"]', + /** + * An attribute of the element responsible for the visibility of the + * button Label credit option (data attribute) + */ + dependsButtonLabel: '[data-enable="button-label"]', + + /** + * An attribute of the element responsible for the visibility of the + * button Label credit option on load (data attribute) + */ + dependsDisableFundingOptions: '[data-enable="disable-funding-options"]', + /** * Templates element selectors */ @@ -119,7 +131,9 @@ define([ } }; - if (solution.getValue($(this)) === elementEvent.value) { + if (solution.getValue($(this)) === elementEvent.value || + $(this).prop('multiple') && solution.checkMultiselectValue($(this), elementEvent) + ) { if (predicate.name) { require([ 'Magento_Paypal/js/predicate/' + predicate.name @@ -147,6 +161,23 @@ define([ return $element.val(); }, + /** + * Check multiselect value based on include value + * + * @param {Object} $element + * @param {Object} elementEvent + * @returns {Boolean} + */ + checkMultiselectValue: function ($element, elementEvent) { + var isValueSelected = $.inArray(elementEvent.value, $element.val()) >= 0; + + if (elementEvent.include) { + isValueSelected = (isValueSelected ? 'true' : 'false') === elementEvent.include; + } + + return isValueSelected; + }, + /** * Adding event listeners * @@ -175,6 +206,8 @@ define([ dependsMerchantId: this.dependsMerchantId, dependsBmlSortOrder: this.dependsBmlSortOrder, dependsBmlApiSortOrder: this.dependsBmlApiSortOrder, + dependsButtonLabel: this.dependsButtonLabel, + dependsDisableFundingOptions: this.dependsDisableFundingOptions, solutionsElements: this.solutionsElements, argument: instance.argument } diff --git a/app/code/Magento/Paypal/view/base/requirejs-config.js b/app/code/Magento/Paypal/view/base/requirejs-config.js index 8edc38dce6f6..8c4c90bf111d 100644 --- a/app/code/Magento/Paypal/view/base/requirejs-config.js +++ b/app/code/Magento/Paypal/view/base/requirejs-config.js @@ -6,7 +6,8 @@ var config = { map: { '*': { - transparent: 'Magento_Payment/transparent' + transparent: 'Magento_Payment/js/transparent', + 'Magento_Payment/transparent': 'Magento_Payment/js/transparent' } } }; diff --git a/app/code/Magento/Paypal/view/frontend/layout/checkout_index_index.xml b/app/code/Magento/Paypal/view/frontend/layout/checkout_index_index.xml index 73c44faff5a5..ebf38dd2d994 100644 --- a/app/code/Magento/Paypal/view/frontend/layout/checkout_index_index.xml +++ b/app/code/Magento/Paypal/view/frontend/layout/checkout_index_index.xml @@ -47,9 +47,6 @@ <item name="paypal_express" xsi:type="array"> <item name="isBillingAddressRequired" xsi:type="boolean">false</item> </item> - <item name="paypal_express_bml" xsi:type="array"> - <item name="isBillingAddressRequired" xsi:type="boolean">false</item> - </item> <item name="payflow_express_bml" xsi:type="array"> <item name="isBillingAddressRequired" xsi:type="boolean">false</item> </item> diff --git a/app/code/Magento/Paypal/view/frontend/requirejs-config.js b/app/code/Magento/Paypal/view/frontend/requirejs-config.js index d2cf3f9a0ce7..223ade43d86e 100644 --- a/app/code/Magento/Paypal/view/frontend/requirejs-config.js +++ b/app/code/Magento/Paypal/view/frontend/requirejs-config.js @@ -6,7 +6,8 @@ var config = { map: { '*': { - orderReview: 'Magento_Paypal/order-review', + orderReview: 'Magento_Paypal/js/order-review', + 'Magento_Paypal/order-review': 'Magento_Paypal/js/order-review', paypalCheckout: 'Magento_Paypal/js/paypal-checkout' } }, diff --git a/app/code/Magento/Paypal/view/frontend/templates/express/in-context/shortcut/button.phtml b/app/code/Magento/Paypal/view/frontend/templates/express/in-context/shortcut/button.phtml index 3725f51f0b8b..66dddfb0bda9 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/express/in-context/shortcut/button.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/express/in-context/shortcut/button.phtml @@ -4,26 +4,10 @@ * See COPYING.txt for license details. */ -use Magento\Paypal\Block\Express\InContext\Minicart\Button; - /** - * @var \Magento\Paypal\Block\Express\InContext\Minicart\Button $block + * @var \Magento\Paypal\Block\Express\InContext\Minicart\SmartButton $block */ -$config = [ - 'Magento_Paypal/js/in-context/button' => [ - 'id' => $block->escapeHtml($block->getContainerId()), - 'linkDataAction' => $block->escapeHtml($block->getLinkAction()), - 'paypalButton' => $block->escapeHtml(Button::PAYPAL_BUTTON_ID), - 'addToCartSelector' => $block->escapeHtml($block->getAddToCartSelector()) - ] -]; - ?> -<div data-mage-init='<?= /* @noEscape */ json_encode($config) ?>' +<div data-mage-init='<?= /* @noEscape */ $block->getJsInitParams() ?>' class="paypal checkout paypal-logo <?= $block->escapeHtml($block->getContainerId()) ?>-container"> - <a data-action="<?= $block->escapeHtml($block->getLinkAction()) ?>" href="#"> - <img class="paypal-button-hidden" - src="<?= $block->escapeHtml($block->getImageUrl()) ?>" - alt="Check out with PayPal" /> - </a> -</div> +</div> \ No newline at end of file diff --git a/app/code/Magento/Paypal/view/frontend/templates/express/shortcut_button.phtml b/app/code/Magento/Paypal/view/frontend/templates/express/shortcut_button.phtml new file mode 100644 index 000000000000..ac0eda99ee93 --- /dev/null +++ b/app/code/Magento/Paypal/view/frontend/templates/express/shortcut_button.phtml @@ -0,0 +1,13 @@ + +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +// @codingStandardsIgnoreFile +/** + * @var \Magento\Paypal\Block\Express\Shortcut $block + */ +?> +<div data-mage-init='<?= /* @noEscape */ $block->getJsInitParams() ?>'></div> \ No newline at end of file diff --git a/app/code/Magento/Paypal/view/frontend/web/js/in-context/button.js b/app/code/Magento/Paypal/view/frontend/web/js/in-context/button.js index 8b4855cff685..012a1f18f9ae 100644 --- a/app/code/Magento/Paypal/view/frontend/web/js/in-context/button.js +++ b/app/code/Magento/Paypal/view/frontend/web/js/in-context/button.js @@ -2,50 +2,52 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -define( - [ - 'uiComponent', - 'jquery', - 'domReady!' - ], - function ( - Component, - $ - ) { - 'use strict'; - - return Component.extend({ - - defaults: {}, - - /** - * @returns {Object} - */ - initialize: function () { - this._super(); - - return this.initEvents(); - }, - - /** - * @returns {Object} - */ - initEvents: function () { - $('a[data-action="' + this.linkDataAction + '"]').off('click.' + this.id) - .on('click.' + this.id, this.click.bind(this)); - - return this; - }, - - /** - * @param {Object} event - * @returns void - */ - click: function (event) { - event.preventDefault(); - - $('#' + this.paypalButton).click(); +define([ + 'uiComponent', + 'jquery', + 'Magento_Paypal/js/in-context/express-checkout-wrapper', + 'Magento_Customer/js/customer-data' +], function (Component, $, Wrapper, customerData) { + 'use strict'; + + return Component.extend(Wrapper).extend({ + defaults: { + declinePayment: false + }, + + /** @inheritdoc */ + initialize: function (config, element) { + var cart = customerData.get('cart'), + customer = customerData.get('customer'); + + this._super(); + this.renderPayPalButtons(element); + this.declinePayment = !customer().firstname && !cart().isGuestCheckoutAllowed; + + return this; + }, + + /** @inheritdoc */ + beforePayment: function (resolve, reject) { + var promise = $.Deferred(); + + if (this.declinePayment) { + this.addError(this.signInMessage, 'warning'); + + reject(); + } else { + promise.resolve(); } - }); - } -); + + return promise; + }, + + /** @inheritdoc */ + prepareClientConfig: function () { + this._super(); + this.clientConfig.commit = false; + + return this.clientConfig; + } + }); +}); diff --git a/app/code/Magento/Paypal/view/frontend/web/js/in-context/express-checkout-smart-buttons.js b/app/code/Magento/Paypal/view/frontend/web/js/in-context/express-checkout-smart-buttons.js new file mode 100644 index 000000000000..ad7e86f2e99e --- /dev/null +++ b/app/code/Magento/Paypal/view/frontend/web/js/in-context/express-checkout-smart-buttons.js @@ -0,0 +1,123 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'underscore', + 'paypalInContextExpressCheckout' +], function (_, paypal) { + 'use strict'; + + /** + * Returns array of allowed funding + * + * @param {Object} config + * @return {Array} + */ + function getFunding(config) { + return _.map(config, function (name) { + return paypal.FUNDING[name]; + }); + } + + return function (clientConfig, element) { + paypal.Button.render({ + env: clientConfig.environment, + client: clientConfig.client, + locale: clientConfig.locale, + funding: { + allowed: getFunding(clientConfig.allowedFunding), + disallowed: getFunding(clientConfig.disallowedFunding) + }, + style: clientConfig.styles, + + // Enable Pay Now checkout flow (optional) + commit: clientConfig.commit, + + /** + * Validate payment method + * + * @param {Object} actions + */ + validate: function (actions) { + clientConfig.rendererComponent.validate(actions); + }, + + /** + * Execute logic on Paypal button click + */ + onClick: function () { + clientConfig.rendererComponent.onClick(); + }, + + /** + * Set up a payment + * + * @return {*} + */ + payment: function () { + var params = { + 'quote_id': clientConfig.quoteId, + 'customer_id': clientConfig.customerId || '', + 'form_key': clientConfig.formKey, + button: clientConfig.button + }; + + return new paypal.Promise(function (resolve, reject) { + clientConfig.rendererComponent.beforePayment(resolve, reject).then(function () { + paypal.request.post(clientConfig.getTokenUrl, params).then(function (res) { + return clientConfig.rendererComponent.afterPayment(res, resolve, reject); + }).catch(function (err) { + return clientConfig.rendererComponent.catchPayment(err, resolve, reject); + }); + }); + }); + }, + + /** + * Execute the payment + * + * @param {Object} data + * @param {Object} actions + * @return {*} + */ + onAuthorize: function (data, actions) { + var params = { + paymentToken: data.paymentToken, + payerId: data.payerID, + quoteId: clientConfig.quoteId || '', + customerId: clientConfig.customerId || '', + 'form_key': clientConfig.formKey + }; + + return new paypal.Promise(function (resolve, reject) { + clientConfig.rendererComponent.beforeOnAuthorize(resolve, reject, actions).then(function () { + paypal.request.post(clientConfig.onAuthorizeUrl, params).then(function (res) { + clientConfig.rendererComponent.afterOnAuthorize(res, resolve, reject, actions); + }).catch(function (err) { + return clientConfig.rendererComponent.catchOnAuthorize(err, resolve, reject); + }); + }); + }); + + }, + + /** + * Process cancel action + * + * @param {Object} data + * @param {Object} actions + */ + onCancel: function (data, actions) { + clientConfig.rendererComponent.onCancel(data, actions); + }, + + /** + * Process errors + */ + onError: function (err) { + clientConfig.rendererComponent.onError(err); + } + }, element); + }; +}); diff --git a/app/code/Magento/Paypal/view/frontend/web/js/in-context/express-checkout-wrapper.js b/app/code/Magento/Paypal/view/frontend/web/js/in-context/express-checkout-wrapper.js new file mode 100644 index 000000000000..905f860fe265 --- /dev/null +++ b/app/code/Magento/Paypal/view/frontend/web/js/in-context/express-checkout-wrapper.js @@ -0,0 +1,187 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'mage/translate', + 'Magento_Customer/js/customer-data', + 'Magento_Paypal/js/in-context/express-checkout-smart-buttons', + 'mage/cookies' +], function ($, $t, customerData, checkoutSmartButtons) { + 'use strict'; + + return { + defaults: { + paymentActionError: $t('Something went wrong with your request. Please try again later.'), + signInMessage: $t('To check out, please sign in with your email address.') + }, + + /** + * Render PayPal buttons using checkout.js + */ + renderPayPalButtons: function (element) { + checkoutSmartButtons(this.prepareClientConfig(), element); + }, + + /** + * Validate payment method + * + * @param {Object} actions + */ + validate: function (actions) { + this.actions = actions || this.actions; + }, + + /** + * Execute logic on Paypal button click + */ + onClick: function () {}, + + /** + * Before payment execute + * + * @param {Function} resolve + * @param {Function} reject + * @return {*} + */ + beforePayment: function (resolve, reject) { //eslint-disable-line no-unused-vars + return $.Deferred().resolve(); + }, + + /** + * After payment execute + * + * @param {Object} res + * @param {Function} resolve + * @param {Function} reject + * + * @return {*} + */ + afterPayment: function (res, resolve, reject) { + if (res.success) { + return resolve(res.token); + } + + this.addError(res['error_message']); + + return reject(new Error(res['error_message'])); + }, + + /** + * Catch payment + * + * @param {Error} err + * @param {Function} resolve + * @param {Function} reject + */ + catchPayment: function (err, resolve, reject) { + this.addError(this.paymentActionError); + reject(err); + }, + + /** + * Before onAuthorize execute + * + * @param {Function} resolve + * @param {Function} reject + * @param {Object} actions + * + * @return {jQuery.Deferred} + */ + beforeOnAuthorize: function (resolve, reject, actions) { //eslint-disable-line no-unused-vars + return $.Deferred().resolve(); + }, + + /** + * After onAuthorize execute + * + * @param {Object} res + * @param {Function} resolve + * @param {Function} reject + * @param {Object} actions + * + * @return {*} + */ + afterOnAuthorize: function (res, resolve, reject, actions) { + if (res.success) { + resolve(); + + return actions.redirect(window, res.redirectUrl); + } + + this.addError(res['error_message']); + + return reject(new Error(res['error_message'])); + }, + + /** + * Catch payment + * + * @param {Error} err + * @param {Function} resolve + * @param {Function} reject + */ + catchOnAuthorize: function (err, resolve, reject) { + this.addError(this.paymentActionError); + reject(err); + }, + + /** + * Process cancel action + * + * @param {Object} data + * @param {Object} actions + */ + onCancel: function (data, actions) { + actions.redirect(window, this.clientConfig.onCancelUrl); + }, + + /** + * Process errors + * + * @param {Error} err + */ + onError: function (err) { //eslint-disable-line no-unused-vars + // Uncaught error isn't displayed in the console + }, + + /** + * Adds error message + * + * @param {String} message + * @param {String} [type] + */ + addError: function (message, type) { + type = type || 'error'; + customerData.set('messages', { + messages: [{ + type: type, + text: message + }], + 'data_id': Math.floor(Date.now() / 1000) + }); + }, + + /** + * @returns {String} + */ + getButtonId: function () { + return this.inContextId; + }, + + /** + * Populate client config with all required data + * + * @return {Object} + */ + prepareClientConfig: function () { + this.clientConfig.client = {}; + this.clientConfig.client[this.clientConfig.environment] = this.clientConfig.merchantId; + this.clientConfig.rendererComponent = this; + this.clientConfig.formKey = $.mage.cookies.get('form_key'); + + return this.clientConfig; + } + }; +}); diff --git a/app/code/Magento/Paypal/view/frontend/web/js/in-context/product-express-checkout.js b/app/code/Magento/Paypal/view/frontend/web/js/in-context/product-express-checkout.js new file mode 100644 index 000000000000..413820cc731a --- /dev/null +++ b/app/code/Magento/Paypal/view/frontend/web/js/in-context/product-express-checkout.js @@ -0,0 +1,76 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'underscore', + 'jquery', + 'uiComponent', + 'Magento_Paypal/js/in-context/express-checkout-wrapper', + 'Magento_Customer/js/customer-data' +], function (_, $, Component, Wrapper, customerData) { + 'use strict'; + + return Component.extend(Wrapper).extend({ + defaults: { + productFormSelector: '#product_addtocart_form', + declinePayment: false, + formInvalid: false + }, + + /** @inheritdoc */ + initialize: function (config, element) { + var cart = customerData.get('cart'), + customer = customerData.get('customer'); + + this._super(); + this.renderPayPalButtons(element); + this.declinePayment = !customer().firstname && !cart().isGuestCheckoutAllowed; + + return this; + }, + + /** @inheritdoc */ + onClick: function () { + var $form = $(this.productFormSelector); + + if (!this.declinePayment) { + $form.submit(); + this.formInvalid = !$form.validation('isValid'); + } + }, + + /** @inheritdoc */ + beforePayment: function (resolve, reject) { + var promise = $.Deferred(); + + if (this.declinePayment) { + this.addError(this.signInMessage, 'warning'); + reject(); + } else if (this.formInvalid) { + reject(); + } else { + $(document).on('ajax:addToCart', function (e, data) { + if (_.isEmpty(data.response)) { + return promise.resolve(); + } + + return reject(); + }); + $(document).on('ajax:addToCart:error', reject); + } + + return promise; + }, + + /** @inheritdoc */ + prepareClientConfig: function () { + this._super(); + this.clientConfig.quoteId = ''; + this.clientConfig.customerId = ''; + this.clientConfig.commit = false; + + return this.clientConfig; + } + }); +}); diff --git a/app/code/Magento/Paypal/view/frontend/web/order-review.js b/app/code/Magento/Paypal/view/frontend/web/js/order-review.js similarity index 100% rename from app/code/Magento/Paypal/view/frontend/web/order-review.js rename to app/code/Magento/Paypal/view/frontend/web/js/order-review.js diff --git a/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/iframe-methods.js b/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/iframe-methods.js index 3315a7c402d6..7fb94a7e2348 100644 --- a/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/iframe-methods.js +++ b/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/iframe-methods.js @@ -4,10 +4,9 @@ */ define([ 'Magento_Checkout/js/view/payment/default', - 'ko', 'Magento_Paypal/js/model/iframe', 'Magento_Checkout/js/model/full-screen-loader' -], function (Component, ko, iframe, fullScreenLoader) { +], function (Component, iframe, fullScreenLoader) { 'use strict'; return Component.extend({ diff --git a/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/in-context/checkout-express.js b/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/in-context/checkout-express.js index c56f21bc718f..5c509238fe5c 100644 --- a/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/in-context/checkout-express.js +++ b/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/in-context/checkout-express.js @@ -2,134 +2,103 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -define( - [ - 'underscore', - 'jquery', - 'Magento_Paypal/js/view/payment/method-renderer/paypal-express-abstract', - 'Magento_Paypal/js/action/set-payment-method', - 'Magento_Checkout/js/model/payment/additional-validators', - 'Magento_Ui/js/lib/view/utils/dom-observer', - 'paypalInContextExpressCheckout', - 'Magento_Customer/js/customer-data', - 'Magento_Ui/js/model/messageList' - ], - function ( - _, - $, - Component, - setPaymentMethodAction, - additionalValidators, - domObserver, - paypalExpressCheckout, - customerData, - messageList - ) { - 'use strict'; - - // State of PayPal module initialization - var clientInit = false; - - return Component.extend({ - - defaults: { - template: 'Magento_Paypal/payment/paypal-express-in-context', - clientConfig: { - /** - * @param {Object} event - */ - click: function (event) { - event.preventDefault(); - - if (additionalValidators.validate()) { - paypalExpressCheckout.checkout.initXO(); - - this.selectPaymentMethod(); - setPaymentMethodAction(this.messageContainer).done(function () { - $('body').trigger('processStart'); - - $.getJSON(this.path, { - button: 0 - }).done(function (response) { - var message = response && response.message; - - if (message) { - if (message.type === 'error') { - messageList.addErrorMessage({ - message: message.text - }); - } else { - messageList.addSuccessMessage({ - message: message.text - }); - } - } - - if (response && response.url) { - paypalExpressCheckout.checkout.startFlow(response.url); - - return; - } - - paypalExpressCheckout.checkout.closeFlow(); - }).fail(function () { - paypalExpressCheckout.checkout.closeFlow(); - }).always(function () { - $('body').trigger('processStop'); - customerData.invalidate(['cart']); - }); - }.bind(this)).fail(function () { - paypalExpressCheckout.checkout.closeFlow(); - }); - } - } - } - }, - - /** - * @returns {Object} - */ - initialize: function () { - this._super(); - this.initClient(); - - return this; - }, +define([ + 'jquery', + 'Magento_Paypal/js/view/payment/method-renderer/paypal-express-abstract', + 'Magento_Paypal/js/in-context/express-checkout-wrapper', + 'Magento_Paypal/js/action/set-payment-method', + 'Magento_Checkout/js/model/payment/additional-validators', + 'Magento_Ui/js/model/messageList', + 'Magento_Ui/js/lib/view/utils/async' +], function ($, Component, Wrapper, setPaymentMethod, additionalValidators, messageList) { + 'use strict'; + + return Component.extend(Wrapper).extend({ + defaults: { + template: 'Magento_Paypal/payment/paypal-express-in-context', + validationElements: 'input' + }, + + /** + * Listens element on change and validate it. + * + * @param {HTMLElement} context + */ + initListeners: function (context) { + $.async(this.validationElements, context, function (element) { + $(element).on('change', function () { + this.validate(); + }.bind(this)); + }.bind(this)); + }, + + /** + * Validates Smart Buttons + */ + validate: function () { + this._super(); + + if (this.actions) { + additionalValidators.validate(true) ? this.actions.enable() : this.actions.disable(); + } + }, - /** - * @returns {Object} - */ - initClient: function () { - var selector = '#' + this.getButtonId(); + /** @inheritdoc */ + beforePayment: function (resolve, reject) { + var promise = $.Deferred(); - _.each(this.clientConfig, function (fn, name) { - if (typeof fn === 'function') { - this.clientConfig[name] = fn.bind(this); - } - }, this); + setPaymentMethod(this.messageContainer).done(function () { + return promise.resolve(); + }).fail(function (response) { + var error; - if (!clientInit) { - domObserver.get(selector, function () { - paypalExpressCheckout.checkout.setup(this.merchantId, this.clientConfig); - clientInit = true; - domObserver.off(selector); - }.bind(this)); - } else { - domObserver.get(selector, function () { - $(selector).on('click', this.clientConfig.click); - domObserver.off(selector); - }.bind(this)); + try { + error = JSON.parse(response.responseText); + } catch (exception) { + error = this.paymentActionError; } - return this; - }, - - /** - * @returns {String} - */ - getButtonId: function () { - return this.inContextId; - } - }); - } -); + this.addError(error); + + return reject(new Error(error)); + }.bind(this)); + + return promise; + }, + + /** + * Populate client config with all required data + * + * @return {Object} + */ + prepareClientConfig: function () { + this._super(); + this.clientConfig.quoteId = window.checkoutConfig.quoteData['entity_id']; + this.clientConfig.customerId = window.customerData.id; + this.clientConfig.merchantId = this.merchantId; + this.clientConfig.button = 0; + this.clientConfig.commit = true; + + return this.clientConfig; + }, + + /** + * Adding logic to be triggered onClick action for smart buttons component + */ + onClick: function () { + additionalValidators.validate(); + this.selectPaymentMethod(); + }, + + /** + * Adds error message + * + * @param {String} message + */ + addError: function (message) { + messageList.addErrorMessage({ + message: message + }); + } + }); +}); diff --git a/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/payflowpro/vault.js b/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/payflowpro/vault.js index d0d72bf7dcdf..a0f3d3867fe7 100644 --- a/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/payflowpro/vault.js +++ b/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/payflowpro/vault.js @@ -2,12 +2,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -/*browser:true*/ -/*global define*/ + define([ - 'jquery', 'Magento_Vault/js/view/payment/method-renderer/vault' -], function ($, VaultComponent) { +], function (VaultComponent) { 'use strict'; return VaultComponent.extend({ diff --git a/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/paypal-express-abstract.js b/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/paypal-express-abstract.js index d038f08c348e..b01d0454b55d 100644 --- a/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/paypal-express-abstract.js +++ b/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/paypal-express-abstract.js @@ -15,7 +15,7 @@ define([ return Component.extend({ defaults: { - template: 'Magento_Paypal/payment/paypal-express-bml', + template: 'Magento_Paypal/payment/payflow-express-bml', billingAgreement: '' }, diff --git a/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/paypal-express-bml.js b/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/paypal-express-bml.js deleted file mode 100644 index 561b3c0e9716..000000000000 --- a/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/paypal-express-bml.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -define([ - 'Magento_Paypal/js/view/payment/method-renderer/paypal-express-abstract' -], function (Component) { - 'use strict'; - - return Component.extend({ - defaults: { - template: 'Magento_Paypal/payment/paypal-express-bml' - } - }); -}); diff --git a/app/code/Magento/Paypal/view/frontend/web/js/view/payment/paypal-payments.js b/app/code/Magento/Paypal/view/frontend/web/js/view/payment/paypal-payments.js index 9eae0dfb45e4..1628bbed8f1c 100644 --- a/app/code/Magento/Paypal/view/frontend/web/js/view/payment/paypal-payments.js +++ b/app/code/Magento/Paypal/view/frontend/web/js/view/payment/paypal-payments.js @@ -19,10 +19,6 @@ define([ component: paypalExpress, config: window.checkoutConfig.payment.paypalExpress.inContextConfig }, - { - type: 'paypal_express_bml', - component: 'Magento_Paypal/js/view/payment/method-renderer/paypal-express-bml' - }, { type: 'payflow_express', component: 'Magento_Paypal/js/view/payment/method-renderer/payflow-express' diff --git a/app/code/Magento/Paypal/view/frontend/web/js/view/review/actions/iframe.js b/app/code/Magento/Paypal/view/frontend/web/js/view/review/actions/iframe.js index e181faf56e36..09dffc73baad 100644 --- a/app/code/Magento/Paypal/view/frontend/web/js/view/review/actions/iframe.js +++ b/app/code/Magento/Paypal/view/frontend/web/js/view/review/actions/iframe.js @@ -8,9 +8,8 @@ */ define([ 'uiComponent', - 'ko', 'Magento_Paypal/js/model/iframe' -], function (Component, ko, iframe) { +], function (Component, iframe) { 'use strict'; return Component.extend({ diff --git a/app/code/Magento/Paypal/view/frontend/web/template/payment/payflowpro-form.html b/app/code/Magento/Paypal/view/frontend/web/template/payment/payflowpro-form.html index f1b14831bab3..d6fb2f3e6fc7 100644 --- a/app/code/Magento/Paypal/view/frontend/web/template/payment/payflowpro-form.html +++ b/app/code/Magento/Paypal/view/frontend/web/template/payment/payflowpro-form.html @@ -58,6 +58,7 @@ </div> <!-- /ko --> </form> + <div class="checkout-agreements-block"> <!-- ko foreach: $parent.getRegion('before-place-order') --> <!-- ko template: getTemplate() --><!-- /ko --> diff --git a/app/code/Magento/Paypal/view/frontend/web/template/payment/paypal-express-bml.html b/app/code/Magento/Paypal/view/frontend/web/template/payment/paypal-express-bml.html deleted file mode 100644 index 0f042824fe89..000000000000 --- a/app/code/Magento/Paypal/view/frontend/web/template/payment/paypal-express-bml.html +++ /dev/null @@ -1,52 +0,0 @@ -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<div class="payment-method" data-bind="css: {'_active': (getCode() == isChecked())}"> - <div class="payment-method-title field choice"> - <input type="radio" - name="payment[method]" - class="radio" - data-bind="attr: {'id': getCode()}, value: getCode(), checked: isChecked, click: selectPaymentMethod, visible: isRadioButtonVisible()" /> - <label data-bind="attr: {'for': getCode()}" class="label"> - <!-- PayPal Logo --> - <img src="https://www.paypalobjects.com/webstatic/en_US/i/buttons/ppc-acceptance-medium.png" - data-bind="attr: {alt: $t('Acceptance Mark')}" - class="payment-icon"/> - <!-- PayPal Logo --> - <span data-bind="text: getTitle()"></span> - <a href="https://www.securecheckout.billmelater.com/paycapture-content/fetch?hash=AU826TU8&content=/bmlweb/ppwpsiw.html" - data-bind="click: showAcceptanceWindow" - class="action action-help"> - <!-- ko i18n: 'See terms' --><!-- /ko --> - </a> - </label> - </div> - <div class="payment-method-content"> - <!-- ko foreach: getRegion('messages') --> - <!-- ko template: getTemplate() --><!-- /ko --> - <!--/ko--> - <fieldset class="fieldset" data-bind='attr: {id: "payment_form_" + getCode()}'> - <div class="payment-method-note"> - <!-- ko i18n: 'You will be redirected to the PayPal website when you place an order.' --><!-- /ko --> - </div> - </fieldset> - <div class="checkout-agreements-block"> - <!-- ko foreach: $parent.getRegion('before-place-order') --> - <!-- ko template: getTemplate() --><!-- /ko --> - <!--/ko--> - </div> - <div class="actions-toolbar"> - <div class="primary"> - <button class="action primary checkout" - type="submit" - data-bind="click: continueToPayPal, enable: (getCode() == isChecked())" - disabled> - <span data-bind="i18n: 'Continue to PayPal'"></span> - </button> - </div> - </div> - </div> -</div> diff --git a/app/code/Magento/Paypal/view/frontend/web/template/payment/paypal-express-in-context.html b/app/code/Magento/Paypal/view/frontend/web/template/payment/paypal-express-in-context.html index 562243decaa6..5f3218325234 100644 --- a/app/code/Magento/Paypal/view/frontend/web/template/payment/paypal-express-in-context.html +++ b/app/code/Magento/Paypal/view/frontend/web/template/payment/paypal-express-in-context.html @@ -4,41 +4,32 @@ * See COPYING.txt for license details. */ --> -<div class="payment-method" data-bind="css: {'_active': (getCode() == isChecked())}"> +<div class="payment-method" css="_active: getCode() == isChecked()" afterRender="initListeners"> <div class="payment-method-title field choice"> <input type="radio" name="payment[method]" class="radio" - data-bind="attr: {'id': getCode()}, value: getCode(), checked: isChecked, click: selectPaymentMethod, visible: isRadioButtonVisible()" /> - <label data-bind="attr: {'for': getCode()}" class="label"> + attr="id: getCode()" + ko-value="getCode()" + ko-checked="isChecked" + click="selectPaymentMethod" + visible="isRadioButtonVisible()"/> + <label attr="for: getCode()" class="label"> <!-- PayPal Logo --> - <img data-bind="attr: {src: getPaymentAcceptanceMarkSrc(), alt: $t('Acceptance Mark')}" class="payment-icon"/> + <img attr="src: getPaymentAcceptanceMarkSrc(), alt: $t('Acceptance Mark')" class="payment-icon"/> <!-- PayPal Logo --> - <span data-bind="text: getTitle()"></span> - <a data-bind="attr: {href: getPaymentAcceptanceMarkHref()}, click: showAcceptanceWindow" - class="action action-help"> - <!-- ko i18n: 'What is PayPal?' --><!-- /ko --> - </a> + <span text="getTitle()"/> + <a class="action action-help" + attr="href: getPaymentAcceptanceMarkHref()" + click="showAcceptanceWindow" + translate="'What is PayPal?'"/> </label> </div> <div class="payment-method-content"> - <!-- ko foreach: getRegion('messages') --> - <!-- ko template: getTemplate() --><!-- /ko --> - <!--/ko--> + <each args="getRegion('messages')" render=""/> <div class="checkout-agreements-block"> - <!-- ko foreach: $parent.getRegion('before-place-order') --> - <!-- ko template: getTemplate() --><!-- /ko --> - <!--/ko--> - </div> - <div class="actions-toolbar"> - <div class="primary"> - <button class="action primary checkout" - type="submit" - data-bind="enable: (getCode() == isChecked()), attr: {id: getButtonId()}" - disabled> - <span data-bind="i18n: 'Continue to PayPal'"></span> - </button> - </div> + <each args="$parent.getRegion('before-place-order')" render=""/> </div> + <div class="actions-toolbar" attr="id: getButtonId()" afterRender="renderPayPalButtons"/> </div> </div> diff --git a/app/code/Magento/PaypalCaptcha/LICENSE.txt b/app/code/Magento/PaypalCaptcha/LICENSE.txt new file mode 100644 index 000000000000..49525fd99da9 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/PaypalCaptcha/LICENSE_AFL.txt b/app/code/Magento/PaypalCaptcha/LICENSE_AFL.txt new file mode 100644 index 000000000000..f39d641b18a1 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/PaypalCaptcha/Model/Checkout/ConfigProviderPayPal.php b/app/code/Magento/PaypalCaptcha/Model/Checkout/ConfigProviderPayPal.php new file mode 100644 index 000000000000..289a1631ed1f --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/Model/Checkout/ConfigProviderPayPal.php @@ -0,0 +1,135 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PaypalCaptcha\Model\Checkout; + +use Magento\Captcha\Helper\Data; +use Magento\Captcha\Model\CaptchaInterface; +use Magento\Checkout\Model\ConfigProviderInterface; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Configuration provider for Captcha rendering. + */ +class ConfigProviderPayPal implements ConfigProviderInterface +{ + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var Data + */ + private $captchaData; + + /** + * @var string + */ + private static $formId = 'co-payment-form'; + + /** + * @param StoreManagerInterface $storeManager + * @param Data $captchaData + */ + public function __construct( + StoreManagerInterface $storeManager, + Data $captchaData + ) { + $this->storeManager = $storeManager; + $this->captchaData = $captchaData; + } + + /** + * @inheritdoc + */ + public function getConfig(): array + { + $config['captchaPayments'][self::$formId] = [ + 'isCaseSensitive' => $this->isCaseSensitive(self::$formId), + 'imageHeight' => $this->getImageHeight(self::$formId), + 'imageSrc' => $this->getImageSrc(self::$formId), + 'refreshUrl' => $this->getRefreshUrl(), + 'isRequired' => $this->isRequired(self::$formId), + 'timestamp' => time() + ]; + + return $config; + } + + /** + * Returns is captcha case sensitive + * + * @param string $formId + * @return bool + */ + private function isCaseSensitive(string $formId): bool + { + return (bool)$this->getCaptchaModel($formId)->isCaseSensitive(); + } + + /** + * Returns captcha image height + * + * @param string $formId + * @return int + */ + private function getImageHeight(string $formId): int + { + return (int)$this->getCaptchaModel($formId)->getHeight(); + } + + /** + * Returns captcha image source path + * + * @param string $formId + * @return string + */ + private function getImageSrc(string $formId): string + { + if ($this->isRequired($formId)) { + $captcha = $this->getCaptchaModel($formId); + $captcha->generate(); + return $captcha->getImgSrc(); + } + + return ''; + } + + /** + * Returns URL to controller action which returns new captcha image + * + * @return string + */ + private function getRefreshUrl(): string + { + $store = $this->storeManager->getStore(); + return $store->getUrl('captcha/refresh', ['_secure' => $store->isCurrentlySecure()]); + } + + /** + * Whether captcha is required to be inserted to this form + * + * @param string $formId + * @return bool + */ + private function isRequired(string $formId): bool + { + return (bool)$this->getCaptchaModel($formId)->isRequired(); + } + + /** + * Return captcha model for specified form + * + * @param string $formId + * @return CaptchaInterface + */ + private function getCaptchaModel(string $formId): CaptchaInterface + { + return $this->captchaData->getCaptcha($formId); + } +} diff --git a/app/code/Magento/PaypalCaptcha/Observer/CaptchaRequestToken.php b/app/code/Magento/PaypalCaptcha/Observer/CaptchaRequestToken.php new file mode 100644 index 000000000000..e7cb282b1799 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/Observer/CaptchaRequestToken.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PaypalCaptcha\Observer; + +use Magento\Captcha\Helper\Data; +use Magento\Framework\App\Action\Action; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\App\ActionFlag ; + +/** + * Validates Captcha for Request Token controller + */ +class CaptchaRequestToken implements ObserverInterface +{ + /** + * @var Data + */ + private $helper; + + /** + * @var Json + */ + private $jsonSerializer; + + /** + * @var ActionFlag + */ + private $actionFlag; + + /** + * @param Data $helper + * @param Json $jsonSerializer + * @param ActionFlag $actionFlag + */ + public function __construct(Data $helper, Json $jsonSerializer, ActionFlag $actionFlag) + { + $this->helper = $helper; + $this->jsonSerializer = $jsonSerializer; + $this->actionFlag = $actionFlag; + } + + /** + * @inheritdoc + */ + public function execute(Observer $observer) + { + $formId = 'co-payment-form'; + $captcha = $this->helper->getCaptcha($formId); + + if (!$captcha->isRequired()) { + return; + } + + /** @var Action $controller */ + $controller = $observer->getControllerAction(); + $word = $controller->getRequest()->getPost('captcha_string'); + if ($captcha->isCorrect($word)) { + return; + } + + $data = $this->jsonSerializer->serialize([ + 'success' => false, + 'error' => true, + 'error_messages' => __('Incorrect CAPTCHA.') + ]); + $this->actionFlag->set('', Action::FLAG_NO_DISPATCH, true); + $controller->getResponse()->representJson($data); + } +} diff --git a/app/code/Magento/PaypalCaptcha/README.md b/app/code/Magento/PaypalCaptcha/README.md new file mode 100644 index 000000000000..71588599a5ec --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/README.md @@ -0,0 +1 @@ +The PayPal Captcha module provides a possibility to enable Captcha validation on Payflow Pro payment form. \ No newline at end of file diff --git a/app/code/Magento/PaypalCaptcha/composer.json b/app/code/Magento/PaypalCaptcha/composer.json new file mode 100644 index 000000000000..e71ef8c0ec7d --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/composer.json @@ -0,0 +1,30 @@ +{ + "name": "magento/module-paypal-captcha", + "description": "Provides CAPTCHA validation for PayPal Payflow Pro", + "config": { + "sort-packages": true + }, + "require": { + "php": "~7.1.3||~7.2.0", + "magento/framework": "*", + "magento/module-captcha": "*", + "magento/module-checkout": "*", + "magento/module-store": "*" + }, + "suggest": { + "magento/module-paypal": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\PaypalCaptcha\\": "" + } + } +} diff --git a/app/code/Magento/PaypalCaptcha/etc/adminhtml/system.xml b/app/code/Magento/PaypalCaptcha/etc/adminhtml/system.xml new file mode 100644 index 000000000000..12afd8ceda60 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/etc/adminhtml/system.xml @@ -0,0 +1,18 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="customer"> + <group id="captcha" translate="label" type="text" sortOrder="110" showInDefault="1" showInWebsite="1" showInStore="0"> + <field id="forms" translate="label comment" type="multiselect" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> + <comment>CAPTCHA for "Create user", "Forgot password", "Payflow Pro" forms is always enabled if chosen.</comment> + </field> + </group> + </section> + </system> +</config> diff --git a/app/code/Magento/PaypalCaptcha/etc/config.xml b/app/code/Magento/PaypalCaptcha/etc/config.xml new file mode 100644 index 000000000000..133a78a42f7b --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/etc/config.xml @@ -0,0 +1,30 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd"> + <default> + <customer> + <captcha> + <shown_to_logged_in_user> + <co-payment-form>1</co-payment-form> + </shown_to_logged_in_user> + <always_for> + <co-payment-form>1</co-payment-form> + </always_for> + </captcha> + </customer> + <captcha translate="label"> + <frontend> + <areas> + <co-payment-form> + <label>Payflow Pro</label> + </co-payment-form> + </areas> + </frontend> + </captcha> + </default> +</config> diff --git a/app/code/Magento/PaypalCaptcha/etc/frontend/di.xml b/app/code/Magento/PaypalCaptcha/etc/frontend/di.xml new file mode 100644 index 000000000000..c236d5ea04ca --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/etc/frontend/di.xml @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Checkout\Model\CompositeConfigProvider"> + <arguments> + <argument name="configProviders" xsi:type="array"> + <item name="paypal_captcha_config_provider" xsi:type="object">Magento\PaypalCaptcha\Model\Checkout\ConfigProviderPayPal</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/PaypalCaptcha/etc/frontend/events.xml b/app/code/Magento/PaypalCaptcha/etc/frontend/events.xml new file mode 100644 index 000000000000..ae706c4485d6 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/etc/frontend/events.xml @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd"> + <event name="controller_action_predispatch_paypal_transparent_requestsecuretoken"> + <observer name="captcha_request_token" instance="Magento\PaypalCaptcha\Observer\CaptchaRequestToken"/> + </event> +</config> diff --git a/app/code/Magento/PaypalCaptcha/etc/module.xml b/app/code/Magento/PaypalCaptcha/etc/module.xml new file mode 100644 index 000000000000..425c829a5d39 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/etc/module.xml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_PaypalCaptcha" > + <sequence> + <module name="Magento_Captcha"/> + <module name="Magento_Paypal"/> + </sequence> + </module> +</config> diff --git a/app/code/Magento/PaypalCaptcha/registration.php b/app/code/Magento/PaypalCaptcha/registration.php new file mode 100644 index 000000000000..4dac0582a6d1 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/registration.php @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use \Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_PaypalCaptcha', __DIR__); diff --git a/app/code/Magento/PaypalCaptcha/view/frontend/layout/checkout_index_index.xml b/app/code/Magento/PaypalCaptcha/view/frontend/layout/checkout_index_index.xml new file mode 100644 index 000000000000..9837068faab7 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/view/frontend/layout/checkout_index_index.xml @@ -0,0 +1,56 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceBlock name="checkout.root"> + <arguments> + <argument name="jsLayout" xsi:type="array"> + <item name="components" xsi:type="array"> + <item name="checkout" xsi:type="array"> + <item name="children" xsi:type="array"> + <item name="steps" xsi:type="array"> + <item name="children" xsi:type="array"> + <item name="billing-step" xsi:type="array"> + <item name="children" xsi:type="array"> + <item name="payment" xsi:type="array"> + <item name="children" xsi:type="array"> + <item name="payments-list" xsi:type="array"> + <item name="children" xsi:type="array"> + <item name="paypal-captcha" xsi:type="array"> + <item name="component" xsi:type="string">uiComponent</item> + <item name="displayArea" xsi:type="string">paypal-captcha</item> + <item name="dataScope" xsi:type="string">paypal-captcha</item> + <item name="provider" xsi:type="string">checkoutProvider</item> + <item name="config" xsi:type="array"> + <item name="template" xsi:type="string">Magento_Checkout/payment/before-place-order</item> + </item> + <item name="children" xsi:type="array"> + <item name="captcha" xsi:type="array"> + <item name="component" xsi:type="string">Magento_PaypalCaptcha/js/view/checkout/paymentCaptcha</item> + <item name="displayArea" xsi:type="string">paypal-captcha</item> + <item name="formId" xsi:type="string">co-payment-form</item> + <item name="configSource" xsi:type="string">checkoutConfig</item> + </item> + </item> + </item> + </item> + </item> + </item> + </item> + </item> + </item> + </item> + </item> + </item> + </item> + </item> + </argument> + </arguments> + </referenceBlock> + </body> +</page> diff --git a/app/code/Magento/PaypalCaptcha/view/frontend/requirejs-config.js b/app/code/Magento/PaypalCaptcha/view/frontend/requirejs-config.js new file mode 100644 index 000000000000..78e7add4ec69 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/view/frontend/requirejs-config.js @@ -0,0 +1,14 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +var config = { + config: { + mixins: { + 'Magento_Checkout/js/view/payment/list': { + 'Magento_PaypalCaptcha/js/view/payment/list-mixin': true + } + } + } +}; diff --git a/app/code/Magento/PaypalCaptcha/view/frontend/web/js/view/checkout/paymentCaptcha.js b/app/code/Magento/PaypalCaptcha/view/frontend/web/js/view/checkout/paymentCaptcha.js new file mode 100644 index 000000000000..f8f119e3b339 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/view/frontend/web/js/view/checkout/paymentCaptcha.js @@ -0,0 +1,44 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'Magento_Captcha/js/view/checkout/defaultCaptcha', + 'Magento_Captcha/js/model/captchaList', + 'Magento_Captcha/js/model/captcha' +], +function ($, defaultCaptcha, captchaList, Captcha) { + 'use strict'; + + return defaultCaptcha.extend({ + + /** @inheritdoc */ + initialize: function () { + var captchaConfigPayment, + currentCaptcha; + + this._super(); + + if (window[this.configSource] && window[this.configSource].captchaPayments) { + captchaConfigPayment = window[this.configSource].captchaPayments; + + $.each(captchaConfigPayment, function (formId, captchaData) { + var captcha; + + captchaData.formId = formId; + captcha = Captcha(captchaData); + captchaList.add(captcha); + }); + } + + currentCaptcha = captchaList.getCaptchaByFormId(this.formId); + + if (currentCaptcha != null) { + currentCaptcha.setIsVisible(true); + this.setCurrentCaptcha(currentCaptcha); + } + } + }); +}); diff --git a/app/code/Magento/PaypalCaptcha/view/frontend/web/js/view/payment/list-mixin.js b/app/code/Magento/PaypalCaptcha/view/frontend/web/js/view/payment/list-mixin.js new file mode 100644 index 000000000000..60172f696e9e --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/view/frontend/web/js/view/payment/list-mixin.js @@ -0,0 +1,54 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'Magento_Captcha/js/model/captchaList' +], function ($, captchaList) { + 'use strict'; + + var mixin = { + + formId: 'co-payment-form', + + /** + * Sets custom template for Payflow Pro + * + * @param {Object} payment + * @returns {Object} + */ + createComponent: function (payment) { + + var component = this._super(payment); + + if (component.component === 'Magento_Paypal/js/view/payment/method-renderer/payflowpro-method') { + component.template = 'Magento_PaypalCaptcha/payment/payflowpro-form'; + $(window).off('clearTimeout') + .on('clearTimeout', this.clearTimeout.bind(this)); + } + + return component; + }, + + /** + * Overrides default window.clearTimeout() to catch errors from iframe and reload Captcha. + */ + clearTimeout: function () { + var captcha = captchaList.getCaptchaByFormId(this.formId); + + if (captcha !== null) { + captcha.refresh(); + } + clearTimeout(); + } + }; + + /** + * Overrides `Magento_Checkout/js/view/payment/list::createComponent` + */ + return function (target) { + return target.extend(mixin); + }; +}); diff --git a/app/code/Magento/PaypalCaptcha/view/frontend/web/template/payment/payflowpro-form.html b/app/code/Magento/PaypalCaptcha/view/frontend/web/template/payment/payflowpro-form.html new file mode 100644 index 000000000000..fec5cf96b032 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/view/frontend/web/template/payment/payflowpro-form.html @@ -0,0 +1,90 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="payment-method" data-bind="css: {'_active': (getCode() == isChecked())}"> + <div class="payment-method-title field choice"> + <input type="radio" + name="payment[method]" + class="radio" + data-bind="attr: {'id': getCode()}, value: getCode(), checked: isChecked, click: selectPaymentMethod, visible: isRadioButtonVisible()"/> + <label class="label" data-bind="attr: {'for': getCode()}"> + <span data-bind="text: getTitle()"></span> + </label> + </div> + + <div class="payment-method-content"> + <!-- ko foreach: getRegion('messages') --> + <!-- ko template: getTemplate() --><!-- /ko --> + <!--/ko--> + <div class="payment-method-billing-address"> + <!-- ko foreach: $parent.getRegion(getBillingAddressFormName()) --> + <!-- ko template: getTemplate() --><!-- /ko --> + <!--/ko--> + </div> + <iframe width="0" + height="0" + data-bind="src: getSource(), attr: {id: getCode() + '-transparent-iframe', 'data-container': getCode() + '-transparent-iframe'}" + allowtransparency="true" + frameborder="0" + name="iframeTransparent" + class="payment-method-iframe"> + </iframe> + <form class="form" id="co-transparent-form" action="#" method="post" data-bind="mageInit: { + 'transparent':{ + 'context': context(), + 'controller': getControllerName(), + 'gateway': getCode(), + 'orderSaveUrl':getPlaceOrderUrl(), + 'cgiUrl': getCgiUrl(), + 'dateDelim': getDateDelim(), + 'cardFieldsMap': getCardFieldsMap(), + 'nativeAction': getSaveOrderUrl() + }, 'validation':[]}"> + + <!-- ko template: 'Magento_Payment/payment/cc-form' --><!-- /ko --> + + <!-- ko if: (isVaultEnabled())--> + <div class="field-tooltip-content"> + <input type="checkbox" + name="vault[is_enabled]" + class="checkbox-inline" + data-bind="attr: {'id': getCode() + '_enable_vault'}, checked: vaultEnabler.isActivePaymentTokenEnabler"/> + <label class="label" data-bind="attr: {'for': getCode() + '_enable_vault'}"> + <span><!-- ko i18n: 'Save credit card information for future use.'--><!-- /ko --></span> + </label> + </div> + <!-- /ko --> + </form> + <fieldset class="fieldset payment items ccard"> + <!-- ko foreach: $parent.getRegion('paypal-captcha') --> + <!-- ko template: getTemplate() --><!-- /ko --> + <!-- /ko --> + </fieldset> + + + <div class="checkout-agreements-block"> + <!-- ko foreach: $parent.getRegion('before-place-order') --> + <!-- ko template: getTemplate() --><!-- /ko --> + <!--/ko--> + </div> + <div class="actions-toolbar"> + <div class="primary"> + <button data-role="review-save" + type="submit" + data-bind=" + attr: {title: $t('Place Order')}, + enable: (getCode() == isChecked()), + click: placeOrder, + css: {disabled: !isPlaceOrderActionAllowed()} + " + class="action primary checkout" + disabled> + <span data-bind="i18n: 'Place Order'"></span> + </button> + </div> + </div> + </div> +</div> diff --git a/app/code/Magento/Persistent/Block/Header/Additional.php b/app/code/Magento/Persistent/Block/Header/Additional.php index c740f5a3469f..dfde2adf1e6a 100644 --- a/app/code/Magento/Persistent/Block/Header/Additional.php +++ b/app/code/Magento/Persistent/Block/Header/Additional.php @@ -5,6 +5,10 @@ */ namespace Magento\Persistent\Block\Header; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Persistent\Helper\Data; + /** * Remember Me block * @@ -30,20 +34,37 @@ class Additional extends \Magento\Framework\View\Element\Html\Link protected $customerRepository; /** - * Constructor - * + * @var string + */ + protected $_template = 'Magento_Persistent::additional.phtml'; + + /** + * @var Json + */ + private $jsonSerializer; + + /** + * @var Data + */ + private $persistentHelper; + + /** * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Customer\Helper\View $customerViewHelper * @param \Magento\Persistent\Helper\Session $persistentSessionHelper * @param \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository * @param array $data + * @param Json|null $jsonSerializer + * @param Data|null $persistentHelper */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, \Magento\Customer\Helper\View $customerViewHelper, \Magento\Persistent\Helper\Session $persistentSessionHelper, \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository, - array $data = [] + array $data = [], + Json $jsonSerializer = null, + Data $persistentHelper = null ) { $this->isScopePrivate = true; $this->_customerViewHelper = $customerViewHelper; @@ -51,6 +72,8 @@ public function __construct( $this->customerRepository = $customerRepository; parent::__construct($context, $data); $this->_isScopePrivate = true; + $this->jsonSerializer = $jsonSerializer ?: ObjectManager::getInstance()->get(Json::class); + $this->persistentHelper = $persistentHelper ?: ObjectManager::getInstance()->get(Data::class); } /** @@ -64,17 +87,26 @@ public function getHref() } /** - * Render additional header html + * Get customer id. * - * @return string + * @return int */ - protected function _toHtml() + public function getCustomerId(): int { - if ($this->_persistentSessionHelper->getSession()->getCustomerId()) { - return '<span><a ' . $this->getLinkAttributes() . ' >' . __('Not you?') - . '</a></span>'; - } + return $this->_persistentSessionHelper->getSession()->getCustomerId(); + } - return ''; + /** + * Get persistent config. + * + * @return string + */ + public function getConfig(): string + { + return $this->jsonSerializer->serialize( + [ + 'expirationLifetime' => $this->persistentHelper->getLifeTime(), + ] + ); } } diff --git a/app/code/Magento/Persistent/CustomerData/Persistent.php b/app/code/Magento/Persistent/CustomerData/Persistent.php new file mode 100644 index 000000000000..5800e4e7aeeb --- /dev/null +++ b/app/code/Magento/Persistent/CustomerData/Persistent.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Persistent\CustomerData; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\CustomerData\SectionSourceInterface; +use Magento\Customer\Helper\View; +use Magento\Persistent\Helper\Session; + +/** + * Customer persistent section + */ +class Persistent implements SectionSourceInterface +{ + /** + * @var Session + */ + private $persistentSession; + + /** + * @var View + */ + private $customerViewHelper; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @param Session $persistentSession + * @param View $customerViewHelper + * @param CustomerRepositoryInterface $customerRepository + */ + public function __construct( + Session $persistentSession, + View $customerViewHelper, + CustomerRepositoryInterface $customerRepository + ) { + $this->persistentSession = $persistentSession; + $this->customerViewHelper = $customerViewHelper; + $this->customerRepository = $customerRepository; + } + + /** + * Get data. + * + * @return array + */ + public function getSectionData(): array + { + if (!$this->persistentSession->isPersistent()) { + return []; + } + + $customerId = $this->persistentSession->getSession()->getCustomerId(); + if (!$customerId) { + return []; + } + + $customer = $this->customerRepository->getById($customerId); + + return [ + 'fullname' => $this->customerViewHelper->getCustomerName($customer), + ]; + } +} diff --git a/app/code/Magento/Persistent/Helper/Data.php b/app/code/Magento/Persistent/Helper/Data.php index 39a9ce7a8ef4..9a547c003249 100644 --- a/app/code/Magento/Persistent/Helper/Data.php +++ b/app/code/Magento/Persistent/Helper/Data.php @@ -13,6 +13,8 @@ use Magento\Store\Model\ScopeInterface; /** + * Helper for persistence + * * @api * @since 100.0.2 */ @@ -136,12 +138,10 @@ public function isShoppingCartPersist($store = null) */ public function getLifeTime($store = null) { - $lifeTime = intval( - $this->scopeConfig->getValue( - self::XML_PATH_LIFE_TIME, - ScopeInterface::SCOPE_STORE, - $store - ) + $lifeTime = (int)$this->scopeConfig->getValue( + self::XML_PATH_LIFE_TIME, + ScopeInterface::SCOPE_STORE, + $store ); return $lifeTime < 0 ? 0 : $lifeTime; } diff --git a/app/code/Magento/Persistent/Model/Checkout/GuestPaymentInformationManagementPlugin.php b/app/code/Magento/Persistent/Model/Checkout/GuestPaymentInformationManagementPlugin.php index 2c0259bd12b0..2641102ca4d7 100644 --- a/app/code/Magento/Persistent/Model/Checkout/GuestPaymentInformationManagementPlugin.php +++ b/app/code/Magento/Persistent/Model/Checkout/GuestPaymentInformationManagementPlugin.php @@ -108,8 +108,8 @@ public function beforeSavePaymentInformationAndPlaceOrder( $this->customerSession->setCustomerId(null); $this->customerSession->setCustomerGroupId(null); $this->quoteManager->convertCustomerCartToGuest(); - /** @var \Magento\Quote\Api\Data\CartInterface $quote */ - $quote = $this->cartRepository->get($this->checkoutSession->getQuote()->getId()); + $quoteId = $this->checkoutSession->getQuoteId(); + $quote = $this->cartRepository->get($quoteId); $quote->setCustomerEmail($email); $quote->getAddressesCollection()->walk('setEmail', ['email' => $email]); $this->cartRepository->save($quote); diff --git a/app/code/Magento/Persistent/Model/Observer.php b/app/code/Magento/Persistent/Model/Observer.php index 53fe5f95531e..81c2870071a2 100644 --- a/app/code/Magento/Persistent/Model/Observer.php +++ b/app/code/Magento/Persistent/Model/Observer.php @@ -86,13 +86,8 @@ public function __construct( */ public function emulateWelcomeBlock($block) { - $customerName = $this->_customerViewHelper->getCustomerName( - $this->customerRepository->getById($this->_persistentSession->getSession()->getCustomerId()) - ); + $block->setWelcome(' '); - $this->_applyAccountLinksPersistentData(); - $welcomeMessage = __('Welcome, %1!', $customerName); - $block->setWelcome($welcomeMessage); return $this; } diff --git a/app/code/Magento/Persistent/Model/Plugin/PersistentCustomerContext.php b/app/code/Magento/Persistent/Model/Plugin/PersistentCustomerContext.php new file mode 100644 index 000000000000..be8998bc9be1 --- /dev/null +++ b/app/code/Magento/Persistent/Model/Plugin/PersistentCustomerContext.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Persistent\Model\Plugin; + +/** + * Plugin for Magento\Framework\App\Http\Context to create new page cache variation for persistent session. + */ +class PersistentCustomerContext +{ + /** + * Persistent session. + * + * @var \Magento\Persistent\Helper\Session + */ + private $persistentSession; + + /** + * @param \Magento\Persistent\Helper\Session $persistentSession + */ + public function __construct( + \Magento\Persistent\Helper\Session $persistentSession + ) { + $this->persistentSession = $persistentSession; + } + + /** + * Sets appropriate header if customer session is persistent. + * + * @param \Magento\Framework\App\Http\Context $subject + * @return mixed + */ + public function beforeGetVaryString(\Magento\Framework\App\Http\Context $subject) + { + if ($this->persistentSession->isPersistent()) { + $subject->setValue('PERSISTENT', 1, 0); + } + } +} diff --git a/app/code/Magento/Persistent/Model/QuoteManager.php b/app/code/Magento/Persistent/Model/QuoteManager.php index 8937a4920cb2..8ae22e4c26c6 100644 --- a/app/code/Magento/Persistent/Model/QuoteManager.php +++ b/app/code/Magento/Persistent/Model/QuoteManager.php @@ -7,6 +7,8 @@ /** * Class QuoteManager + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class QuoteManager { @@ -87,6 +89,7 @@ public function setGuest($checkQuote = false) ->setCustomerLastname(null) ->setCustomerGroupId(\Magento\Customer\Api\Data\GroupInterface::NOT_LOGGED_IN_ID) ->setIsPersistent(false) + ->setCustomerIsGuest(true) ->removeAllAddresses(); //Create guest addresses $quote->getShippingAddress(); @@ -108,8 +111,9 @@ public function setGuest($checkQuote = false) */ public function convertCustomerCartToGuest() { + $quoteId = $this->checkoutSession->getQuoteId(); /** @var $quote \Magento\Quote\Model\Quote */ - $quote = $this->quoteRepository->get($this->checkoutSession->getQuote()->getId()); + $quote = $this->quoteRepository->get($quoteId); if ($quote && $quote->getId()) { $this->_setQuotePersistent = false; $quote->setIsActive(true) diff --git a/app/code/Magento/Persistent/Model/Session.php b/app/code/Magento/Persistent/Model/Session.php index abc344671ab7..701b4fc21d29 100644 --- a/app/code/Magento/Persistent/Model/Session.php +++ b/app/code/Magento/Persistent/Model/Session.php @@ -197,6 +197,7 @@ public function getExpiredBefore($store = null) /** * Serialize info for Resource Model to save + * * For new model check and set available cookie key * * @return $this @@ -354,7 +355,7 @@ public function deleteExpired($websiteId = null) $lifetime = $this->_coreConfig->getValue( \Magento\Persistent\Helper\Data::XML_PATH_LIFE_TIME, 'website', - intval($websiteId) + (int)$websiteId ); if ($lifetime) { diff --git a/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php b/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php index f3720960ca6e..79fdf44c3c55 100644 --- a/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php +++ b/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -8,6 +7,11 @@ use Magento\Framework\Event\ObserverInterface; +/** + * Observer of expired session + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ class CheckExpirePersistentQuoteObserver implements ObserverInterface { /** @@ -107,8 +111,12 @@ public function execute(\Magento\Framework\Event\Observer $observer) !$this->_persistentSession->isPersistent() && !$this->_customerSession->isLoggedIn() && $this->_checkoutSession->getQuoteId() && - !$this->isRequestFromCheckoutPage($this->request) + !$this->isRequestFromCheckoutPage($this->request) && // persistent session does not expire on onepage checkout page + ( + $this->_checkoutSession->getQuote()->getIsPersistent() || + $this->_checkoutSession->getQuote()->getCustomerIsGuest() + ) ) { $this->_eventManager->dispatch('persistent_session_expired'); $this->quoteManager->expire(); diff --git a/app/code/Magento/Persistent/Observer/SetCheckoutSessionPersistentDataObserver.php b/app/code/Magento/Persistent/Observer/SetCheckoutSessionPersistentDataObserver.php new file mode 100644 index 000000000000..e89a09c0d2a9 --- /dev/null +++ b/app/code/Magento/Persistent/Observer/SetCheckoutSessionPersistentDataObserver.php @@ -0,0 +1,90 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Persistent\Observer; + +use Magento\Framework\Event\ObserverInterface; + +/** + * Class SetCheckoutSessionPersistentDataObserver + */ +class SetCheckoutSessionPersistentDataObserver implements ObserverInterface +{ + /** + * Persistent session + * + * @var \Magento\Persistent\Helper\Session + */ + private $persistentSession = null; + + /** + * Customer session + * + * @var \Magento\Customer\Model\Session + */ + private $customerSession; + + /** + * Persistent data + * + * @var \Magento\Persistent\Helper\Data + */ + private $persistentData = null; + + /** + * Customer Repository + * + * @var \Magento\Customer\Api\CustomerRepositoryInterface + */ + private $customerRepository = null; + + /** + * @param \Magento\Persistent\Helper\Session $persistentSession + * @param \Magento\Customer\Model\Session $customerSession + * @param \Magento\Persistent\Helper\Data $persistentData + * @param \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository + */ + public function __construct( + \Magento\Persistent\Helper\Session $persistentSession, + \Magento\Customer\Model\Session $customerSession, + \Magento\Persistent\Helper\Data $persistentData, + \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository + ) { + $this->persistentSession = $persistentSession; + $this->customerSession = $customerSession; + $this->persistentData = $persistentData; + $this->customerRepository = $customerRepository; + } + + /** + * Pass customer data from persistent session to checkout session and set quote to be loaded even if not active + * + * @param \Magento\Framework\Event\Observer $observer + * @return void + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function execute(\Magento\Framework\Event\Observer $observer) + { + /** @var $checkoutSession \Magento\Checkout\Model\Session */ + $checkoutSession = $observer->getEvent()->getData('checkout_session'); + if ($this->persistentData->isShoppingCartPersist() && $this->persistentSession->isPersistent()) { + $checkoutSession->setCustomerData( + $this->customerRepository->getById($this->persistentSession->getSession()->getCustomerId()) + ); + } + if (!(($this->persistentSession->isPersistent() && !$this->customerSession->isLoggedIn()) + && !$this->persistentData->isShoppingCartPersist() + )) { + return; + } + if ($checkoutSession) { + $checkoutSession->setLoadInactive(); + } + } +} diff --git a/app/code/Magento/Persistent/Observer/SetLoadPersistentQuoteObserver.php b/app/code/Magento/Persistent/Observer/SetLoadPersistentQuoteObserver.php deleted file mode 100644 index 6eeab94a91cc..000000000000 --- a/app/code/Magento/Persistent/Observer/SetLoadPersistentQuoteObserver.php +++ /dev/null @@ -1,78 +0,0 @@ -<?php -/** - * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Persistent\Observer; - -use Magento\Framework\Event\ObserverInterface; - -class SetLoadPersistentQuoteObserver implements ObserverInterface -{ - /** - * Customer session - * - * @var \Magento\Customer\Model\Session - */ - protected $_customerSession; - - /** - * Checkout session - * - * @var \Magento\Checkout\Model\Session - */ - protected $_checkoutSession; - - /** - * Persistent session - * - * @var \Magento\Persistent\Helper\Session - */ - protected $_persistentSession = null; - - /** - * Persistent data - * - * @var \Magento\Persistent\Helper\Data - */ - protected $_persistentData = null; - - /** - * @param \Magento\Persistent\Helper\Session $persistentSession - * @param \Magento\Persistent\Helper\Data $persistentData - * @param \Magento\Customer\Model\Session $customerSession - * @param \Magento\Checkout\Model\Session $checkoutSession - */ - public function __construct( - \Magento\Persistent\Helper\Session $persistentSession, - \Magento\Persistent\Helper\Data $persistentData, - \Magento\Customer\Model\Session $customerSession, - \Magento\Checkout\Model\Session $checkoutSession - ) { - $this->_persistentSession = $persistentSession; - $this->_customerSession = $customerSession; - $this->_checkoutSession = $checkoutSession; - $this->_persistentData = $persistentData; - } - - /** - * Set quote to be loaded even if not active - * - * @param \Magento\Framework\Event\Observer $observer - * @return void - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function execute(\Magento\Framework\Event\Observer $observer) - { - if (!(($this->_persistentSession->isPersistent() && !$this->_customerSession->isLoggedIn()) - && !$this->_persistentData->isShoppingCartPersist() - )) { - return; - } - - if ($this->_checkoutSession) { - $this->_checkoutSession->setLoadInactive(); - } - } -} diff --git a/app/code/Magento/Persistent/Observer/SetQuotePersistentDataObserver.php b/app/code/Magento/Persistent/Observer/SetQuotePersistentDataObserver.php index db6b6d1ee370..2803bc998dcb 100644 --- a/app/code/Magento/Persistent/Observer/SetQuotePersistentDataObserver.php +++ b/app/code/Magento/Persistent/Observer/SetQuotePersistentDataObserver.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -8,6 +7,11 @@ use Magento\Framework\Event\ObserverInterface; +/** + * Observer for setting "is_persistent" value to quote + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ class SetQuotePersistentDataObserver implements ObserverInterface { /** @@ -73,8 +77,8 @@ public function execute(\Magento\Framework\Event\Observer $observer) } if (( - ($this->_persistentSession->isPersistent() && !$this->_customerSession->isLoggedIn()) - && !$this->_persistentData->isShoppingCartPersist() + ($this->_persistentSession->isPersistent()) + && $this->_persistentData->isShoppingCartPersist() ) && $this->quoteManager->isPersistent() ) { diff --git a/app/code/Magento/Persistent/Test/Mftf/ActionGroup/StorefrontCustomerActionGroup.xml b/app/code/Magento/Persistent/Test/Mftf/ActionGroup/StorefrontCustomerActionGroup.xml new file mode 100644 index 000000000000..293fa04d8046 --- /dev/null +++ b/app/code/Magento/Persistent/Test/Mftf/ActionGroup/StorefrontCustomerActionGroup.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="CustomerLoginOnStorefrontWithRememberMeChecked" extends="LoginToStorefrontActionGroup"> + <checkOption selector="{{StorefrontCustomerSignInFormSection.rememberMe}}" + before="clickSignInAccountButton" + stepKey="checkRememberMe"/> + </actionGroup> + + <actionGroup name="CustomerLoginOnStorefrontWithRememberMeUnChecked" extends="LoginToStorefrontActionGroup"> + <uncheckOption selector="{{StorefrontCustomerSignInFormSection.rememberMe}}" + before="clickSignInAccountButton" + stepKey="unCheckRememberMe"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Persistent/Test/Mftf/Data/PersistentData.xml b/app/code/Magento/Persistent/Test/Mftf/Data/PersistentData.xml index f4e2fa198e7f..39e55693811e 100644 --- a/app/code/Magento/Persistent/Test/Mftf/Data/PersistentData.xml +++ b/app/code/Magento/Persistent/Test/Mftf/Data/PersistentData.xml @@ -20,4 +20,18 @@ <entity name="persistentEnabledState" type="persistent_options_enabled"> <data key="value">1</data> </entity> + + <entity name="PersistentLogoutClearEnabled" type="persistent_config_state"> + <requiredEntity type="persistent_options_logout_clear">PersistentEnabledLogoutClear</requiredEntity> + </entity> + <entity name="PersistentEnabledLogoutClear" type="logout_clear"> + <data key="value">1</data> + </entity> + + <entity name="PersistentLogoutClearDisable" type="persistent_config_state"> + <requiredEntity type="persistent_options_logout_clear">PersistentDisableLogoutClear</requiredEntity> + </entity> + <entity name="PersistentDisableLogoutClear" type="logout_clear"> + <data key="value">0</data> + </entity> </entities> diff --git a/app/code/Magento/Persistent/Test/Mftf/Metadata/persistent_config-meta.xml b/app/code/Magento/Persistent/Test/Mftf/Metadata/persistent_config-meta.xml index d165ca5f929b..7f0e12f8bef9 100644 --- a/app/code/Magento/Persistent/Test/Mftf/Metadata/persistent_config-meta.xml +++ b/app/code/Magento/Persistent/Test/Mftf/Metadata/persistent_config-meta.xml @@ -14,6 +14,9 @@ <object key="enabled" dataType="persistent_options_enabled"> <field key="value">string</field> </object> + <object key="logout_clear" dataType="persistent_options_logout_clear"> + <field key="value">string</field> + </object> </object> </object> </object> diff --git a/app/code/Magento/Persistent/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml b/app/code/Magento/Persistent/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml new file mode 100644 index 000000000000..c2220c33a605 --- /dev/null +++ b/app/code/Magento/Persistent/Test/Mftf/Section/StorefrontCustomerSignInFormSection.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="StorefrontCustomerSignInFormSection"> + <element name="rememberMe" type="checkbox" selector="[name='persistent_remember_me']"/> + </section> +</sections> diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/CheckShoppingCartBehaviorAfterSessionExpiredTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/CheckShoppingCartBehaviorAfterSessionExpiredTest.xml new file mode 100644 index 000000000000..c66a2979aa7f --- /dev/null +++ b/app/code/Magento/Persistent/Test/Mftf/Test/CheckShoppingCartBehaviorAfterSessionExpiredTest.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="CheckShoppingCartBehaviorAfterSessionExpiredTest"> + <annotations> + <features value="Persistent"/> + <stories value="MAGETWO-91733 - Unusual behavior with the persistent shopping cart after the session is expired"/> + <title value="Checking behavior with the persistent shopping cart after the session is expired"/> + <description value="Checking behavior with the persistent shopping cart after the session is expired"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-95118"/> + <group value="persistent"/> + </annotations> + <before> + <!--Enable Persistence--> + <createData entity="PersistentConfigEnabled" stepKey="enablePersistent"/> + <!--Create product--> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Create new customer --> + <actionGroup ref="SignUpNewUserFromStorefrontActionGroup" stepKey="SignUpNewUser"> + <argument name="Customer" value="Simple_US_Customer_NY"/> + </actionGroup> + <!--Add shipping information--> + <actionGroup ref="EnterCustomerAddressInfo" stepKey="enterAddressInfo"> + <argument name="Address" value="US_Address_NY"/> + </actionGroup> + </before> + <after> + <!--Roll back configuration--> + <createData entity="PersistentConfigDefault" stepKey="setDefaultPersistentState"/> + <!--Delete product--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + </after> + <!-- Add simple product to cart --> + <actionGroup ref="AddSimpleProductToCart" stepKey="addProductToCart1"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="goToShoppingCartFromMinicart"/> + <!--Reset cookies and refresh the page--> + <resetCookie userInput="PHPSESSID" stepKey="resetCookieForCart"/> + <reloadPage stepKey="reloadPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <!--Check product exists in cart--> + <see userInput="$$createProduct.name$$" stepKey="ProductExistsInCart"/> + </test> +</tests> diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/ShippingQuotePersistedForGuestTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/ShippingQuotePersistedForGuestTest.xml new file mode 100644 index 000000000000..e5c77ee41436 --- /dev/null +++ b/app/code/Magento/Persistent/Test/Mftf/Test/ShippingQuotePersistedForGuestTest.xml @@ -0,0 +1,84 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="ShippingQuotePersistedForGuestTest"> + <annotations> + <features value="Persistent"/> + <stories value="Guest checkout"/> + <title value="Estimate Shipping and Tax block sections on shipping cart saving correctly for Guest."/> + <description value="Verify that 'Estimate Shipping and Tax' block sections on shipping cart saving correctly for Guest after switching to another page. And check that the shopping cart is cleared after reset persistent cookie."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-99025"/> + <useCaseId value="MAGETWO-98620"/> + <group value="persistent"/> + </annotations> + <before> + <!--Enabled The Persistent Shopping Cart feature --> + <createData entity="PersistentConfigEnabled" stepKey="enablePersistent"/> + <createData entity="PersistentLogoutClearDisable" stepKey="persistentLogoutClearDisable"/> + <!--Create simple product--> + <createData entity="SimpleProduct2" stepKey="createProduct"> + <field key="price">150</field> + </createData> + <!--Create customer--> + <createData entity="Simple_US_Customer" stepKey="createCustomer"> + <field key="firstname">John1</field> + <field key="lastname">Doe1</field> + </createData> + </before> + <after> + <!--Revert persistent configuration to default--> + <createData entity="PersistentConfigDefault" stepKey="setDefaultPersistentState"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Step 1: Login as a Customer with remember me checked--> + <actionGroup ref="CustomerLoginOnStorefrontWithRememberMeChecked" stepKey="loginToStorefrontAccountWithRememberMeChecked"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <!--Step 2: Open the Product Page and add the product to shopping cart--> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToProductPageAsLoggedUser"/> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProductToCartAsLoggedUser"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + <!--Step 3: Log out, reset persistent cookie and go to homepage--> + <amOnPage url="{{StorefrontCustomerSignOutPage.url}}" stepKey="signOut"/> + <waitForLoadingMaskToDisappear stepKey="waitSignOutPage"/> + <resetCookie userInput="persistent_shopping_cart" stepKey="resetPersistentCookie"/> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnHomePageAfterResetPersistentCookie"/> + <waitForPageLoad stepKey="waitHomePageLoadAfterResetCookie"/> + <!--Check that the minicart is empty--> + <actionGroup ref="assertMiniCartEmpty" after="waitHomePageLoadAfterResetCookie" stepKey="seeMinicartEmpty"/> + <!--Step 4: Add the product to shopping cart and open cart--> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToProductPageAsGuestUser"/> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addProductToCartAsGuestUser"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="goToShoppingCartBeforeChangeShippingAndTaxSection"/> + <!--Step 5: Open Estimate Shipping and Tax block and fill the sections--> + <conditionalClick selector="{{CheckoutCartSummarySection.estimateShippingAndTax}}" dependentSelector="{{CheckoutCartSummarySection.country}}" visible="false" stepKey="expandEstimateShippingAndTax" /> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <selectOption selector="{{CheckoutCartSummarySection.country}}" userInput="{{US_Address_CA.country}}" stepKey="selectUSCountry"/> + <selectOption selector="{{CheckoutCartSummarySection.stateProvince}}" userInput="{{US_Address_CA.state}}" stepKey="selectCaliforniaRegion"/> + <fillField selector="{{CheckoutCartSummarySection.postcode}}" userInput="{{US_Address_CA.postcode}}" stepKey="inputPostCode"/> + <!--Step 6: Go to Homepage--> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToHomePageAfterChangingShippingAndTaxSection"/> + <!--Step 7: Go to shopping cart and check "Estimate Shipping and Tax" fields values are saved--> + <actionGroup ref="clickViewAndEditCartFromMiniCart" after="goToHomePageAfterChangingShippingAndTaxSection" stepKey="goToShoppingCartAfterChangingShippingAndTaxSection"/> + <conditionalClick selector="{{CheckoutCartSummarySection.estimateShippingAndTax}}" dependentSelector="{{CheckoutCartSummarySection.country}}" visible="false" stepKey="expandEstimateShippingAndTaxAfterChanging" /> + <seeOptionIsSelected selector="{{CheckoutCartSummarySection.country}}" userInput="{{US_Address_CA.country}}" stepKey="checkCustomerCountry" /> + <seeOptionIsSelected selector="{{CheckoutCartSummarySection.stateProvince}}" userInput="{{US_Address_CA.state}}" stepKey="checkCustomerRegion" /> + <grabValueFrom selector="{{CheckoutCartSummarySection.postcode}}" stepKey="grabTextPostCode"/> + <assertEquals message="Customer postcode is invalid" stepKey="checkCustomerPostcode"> + <expectedResult type="string">{{US_Address_CA.postcode}}</expectedResult> + <actualResult type="variable">grabTextPostCode</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontCorrectWelcomeMessageAfterCustomerIsLoggedOutTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontCorrectWelcomeMessageAfterCustomerIsLoggedOutTest.xml new file mode 100644 index 000000000000..2b58e5c7bf62 --- /dev/null +++ b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontCorrectWelcomeMessageAfterCustomerIsLoggedOutTest.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="StorefrontCorrectWelcomeMessageAfterCustomerIsLoggedOutTest"> + <annotations> + <features value="Persistent"/> + <stories value="MAGETWO-97278 - Incorrect use of cookies for customer"/> + <title value="Checking welcome message for persistent customer after logout"/> + <description value="Checking welcome message for persistent customer after logout"/> + <severity value="MAJOR"/> + <testCaseId value="MC-10800"/> + <group value="persistent"/> + <group value="customer"/> + </annotations> + <before> + <!--Enable Persistence--> + <createData entity="PersistentConfigEnabled" stepKey="enablePersistent"/> + <createData entity="PersistentLogoutClearDisable" stepKey="persistentLogoutClearDisable"/> + + <!--Create customers--> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <createData entity="Simple_US_Customer" stepKey="createCustomerForPersistent"> + <field key="firstname">John1</field> + <field key="lastname">Doe1</field> + </createData> + </before> + <after> + <!--Roll back configuration--> + <createData entity="PersistentConfigDefault" stepKey="setDefaultPersistentState"/> + <createData entity="PersistentLogoutClearEnabled" stepKey="persistentLogoutClearEnabled"/> + + <!-- Logout customer on Storefront--> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogoutStorefront"/> + <!--Delete customers--> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCustomerForPersistent" stepKey="deleteCustomerForPersistent"/> + </after> + <!--Login as a Customer with remember me unchecked--> + <actionGroup ref="CustomerLoginOnStorefrontWithRememberMeUnChecked" stepKey="loginToStorefrontAccountWithRememberMeUnchecked"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!--Check customer name and last name in welcome message--> + <seeInCurrentUrl url="{{StorefrontCustomerDashboardPage.url}}" stepKey="seeCustomerAccountPageUrl"/> + <see userInput="Welcome, $$createCustomer.firstname$$ $$createCustomer.lastname$$!" + selector="{{StorefrontHeaderSection.welcomeMessage}}" + stepKey="seeLoggedInCustomerWelcomeMessage"/> + <!--Logout and check default welcome message--> + <actionGroup ref="CustomerLogoutStorefrontByMenuItemsActionGroup" stepKey="storefrontCustomerLogout"/> + <seeInCurrentUrl url="{{StorefrontCustomerLogoutSuccessPage.url}}" wait="5" stepKey="seeCustomerSignOutPageUrl"/> + <see userInput="Default welcome msg!" + selector="{{StorefrontHeaderSection.welcomeMessage}}" + stepKey="seeDefaultWelcomeMessage"/> + + <!--Login as a Customer with remember me checked--> + <actionGroup ref="CustomerLoginOnStorefrontWithRememberMeChecked" stepKey="loginToStorefrontAccountWithRememberMeChecked"> + <argument name="Customer" value="$$createCustomerForPersistent$$"/> + </actionGroup> + <!--Check customer name and last name in welcome message--> + <seeInCurrentUrl url="{{StorefrontCustomerDashboardPage.url}}" stepKey="seeCustomerAccountPageUrl1"/> + <see userInput="Welcome, $$createCustomerForPersistent.firstname$$ $$createCustomerForPersistent.lastname$$!" + selector="{{StorefrontHeaderSection.welcomeMessage}}" + stepKey="seeLoggedInCustomerWelcomeMessage1"/> + + <!--Logout and check persistent customer welcome message--> + <actionGroup ref="CustomerLogoutStorefrontByMenuItemsActionGroup" stepKey="storefrontCustomerLogout1"/> + <seeInCurrentUrl url="{{StorefrontCustomerLogoutSuccessPage.url}}" wait="5" stepKey="seeCustomerSignOutPageUrl1"/> + <see userInput="Welcome, $$createCustomerForPersistent.firstname$$ $$createCustomerForPersistent.lastname$$! Not you?" + selector="{{StorefrontHeaderSection.welcomeMessage}}" + stepKey="seePersistentWelcomeMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyThatInformationAboutViewingComparisonWishlistIsPersistedUnderLongTermCookieTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyThatInformationAboutViewingComparisonWishlistIsPersistedUnderLongTermCookieTest.xml new file mode 100644 index 000000000000..dc6f87bef0ba --- /dev/null +++ b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyThatInformationAboutViewingComparisonWishlistIsPersistedUnderLongTermCookieTest.xml @@ -0,0 +1,193 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontVerifyThatInformationAboutViewingComparisonWishlistIsPersistedUnderLongTermCookieTest"> + <annotations> + <features value="Persistent"/> + <stories value="Catalog widget"/> + <title value="Verify that information about viewing, comparison, wishlist and last ordered items is persisted under long-term cookie"/> + <description value="Verify that information about viewing, comparison, wishlist and last ordered items is persisted under long-term cookie"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-12180"/> + <group value="persistent"/> + <group value="widget"/> + <group value="catalog_widget"/> + <skip> + <issueId value="MC-15741"/> + </skip> + </annotations> + <before> + <createData entity="PersistentConfigEnabled" stepKey="enablePersistent"/> + <createData entity="PersistentLogoutClearDisable" stepKey="persistentLogoutClearDisable"/> + <createData entity="EnableSynchronizeWidgetProductsWithBackendStorage" stepKey="enableSynchronizeWidgetProductsWithBackendStorage"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSecondSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <actionGroup ref="AdminCreateRecentlyProductsWidgetActionGroup" stepKey="createRecentlyComparedProductsWidget"> + <argument name="widget" value="RecentlyComparedProductsWidget"/> + </actionGroup> + <actionGroup ref="AdminCreateRecentlyProductsWidgetActionGroup" stepKey="createRecentlyViewedProductsWidget"> + <argument name="widget" value="RecentlyViewedProductsWidget"/> + </actionGroup> + </before> + <after> + <createData entity="PersistentConfigDefault" stepKey="setDefaultPersistentState"/> + <createData entity="PersistentLogoutClearEnabled" stepKey="persistentLogoutClearEnabled"/> + <createData entity="DisableSynchronizeWidgetProductsWithBackendStorage" stepKey="disableSynchronizeWidgetProductsWithBackendStorage"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createSecondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutFromCustomer"/> + <actionGroup ref="AdminDeleteWidgetActionGroup" stepKey="deleteRecentlyComparedProductsWidget"> + <argument name="widget" value="RecentlyComparedProductsWidget"/> + </actionGroup> + <actionGroup ref="AdminDeleteWidgetActionGroup" stepKey="deleteRecentlyViewedProductsWidget"> + <argument name="widget" value="RecentlyViewedProductsWidget"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Login to storefront from customer--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <see userInput="Welcome, $$createCustomer.firstname$$ $$createCustomer.lastname$$!" selector="{{StorefrontPanelHeaderSection.WelcomeMessage}}" stepKey="checkWelcomeMessage"/> + + <!--Open the details page of Simple Product 1, Simple Product 2 and add to cart, get to the category--> + <actionGroup ref="AddSimpleProductToCart" stepKey="addSimpleProductProductToCart"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="AddSimpleProductToCart" stepKey="addSecondSimpleProductProductToCart"> + <argument name="product" value="$$createSecondSimpleProduct$$"/> + </actionGroup> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.custom_attributes[url_key]$$)}}" stepKey="openCategoryPageAfterAddedProductToCart"/> + <!--The Recently Viewed widget displays Simple Product 1 and Simple Product 2--> + <actionGroup ref="StorefrontAssertProductInRecentlyViewedWidgetActionGroup" stepKey="seeSimpleProductInRecentlyViewedWidget"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductInRecentlyViewedWidgetActionGroup" stepKey="seeSecondSimpleProductInRecentlyViewedWidget"> + <argument name="product" value="$$createSecondSimpleProduct$$"/> + </actionGroup> + + <!--Add Simple Product 1 and Simple Product 2 to Wishlist--> + <actionGroup ref="StorefrontCustomerAddCategoryProductToWishlistActionGroup" stepKey="addSimpleProductToWishlist"> + <argument name="productVar" value="$$createSimpleProduct$$"/> + </actionGroup> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.custom_attributes[url_key]$$)}}" stepKey="openCategoryPageAfterProductAddToWishlist"/> + <actionGroup ref="StorefrontCustomerAddCategoryProductToWishlistActionGroup" stepKey="addSecondSimpleProductToWishlist"> + <argument name="productVar" value="$$createSecondSimpleProduct$$"/> + </actionGroup> + <!--The My Wishlist widget displays Simple Product 1 and Simple Product 2--> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.custom_attributes[url_key]$$)}}" stepKey="openCategoryPageToCheckProductsInWishlistSidebar"/> + <actionGroup ref="StorefrontCustomerCheckProductInWishlistSidebar" stepKey="checkSimpleProductInWishlistSidebar"> + <argument name="productVar" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontCustomerCheckProductInWishlistSidebar" stepKey="checkSecondSimpleProductInWishlistSidebar"> + <argument name="productVar" value="$$createSecondSimpleProduct$$"/> + </actionGroup> + + <!--Add to compare Simple Product and Simple Product 2--> + <actionGroup ref="StorefrontAddCategoryProductToCompareActionGroup" stepKey="addSimpleProductToCompare" > + <argument name="productVar" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddCategoryProductToCompareActionGroup" stepKey="addSecondSimpleProductToCompare" > + <argument name="productVar" value="$$createSecondSimpleProduct$$"/> + </actionGroup> + <!--The Compare Products widget displays Simple Product 1 and Simple Product 2--> + <actionGroup ref="StorefrontCheckCompareSidebarProductActionGroup" stepKey="checkSimpleProductInCompareSidebar"> + <argument name="productVar" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckCompareSidebarProductActionGroup" stepKey="checkSecondSimpleProductInCompareSidebar"> + <argument name="productVar" value="$$createSecondSimpleProduct$$"/> + </actionGroup> + + <!--Click Clear all in the Compare Products widget--> + <actionGroup ref="StorefrontClearCompareActionGroup" stepKey="clearCompareList"/> + <!--The Recently Compared widget displays Simple Product 1 and Simple Product 2--> + <actionGroup ref="StorefrontAssertProductInRecentlyComparedWidgetActionGroup" stepKey="checkSimpleProductInRecentlyComparedWidget"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductInRecentlyComparedWidgetActionGroup" stepKey="checkSecondSimpleProductInRecentlyComparedWidget"> + <argument name="product" value="$$createSecondSimpleProduct$$"/> + </actionGroup> + + <!--Place the order--> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToShoppingCartPage"/> + <actionGroup ref="PlaceOrderWithLoggedUserActionGroup" stepKey="placeOrder"> + <argument name="shippingMethod" value="Flat Rate"/> + </actionGroup> + <!--The Recently Ordered widget displays Simple Product 1 and Simple Product 2--> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.custom_attributes[url_key]$$)}}" stepKey="openCategoryPageToCheckProductsInRecentlyOrderedWidget"/> + <actionGroup ref="StorefrontAssertProductInRecentlyOrderedWidgetActionGroup" stepKey="checkSimpleProductInRecentlyOrderedWidget"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductInRecentlyOrderedWidgetActionGroup" stepKey="checkSecondSimpleProductInRecentlyOrderedWidget"> + <argument name="product" value="$$createSecondSimpleProduct$$"/> + </actionGroup> + + <!--Sign out and check that widgets persist the information about the items--> + <actionGroup ref="StorefrontSignOutActionGroup" stepKey="logoutFromCustomerToCheckThatWidgetsPersist"/> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.custom_attributes[url_key]$$)}}" stepKey="openCategoryPageAfterLogoutFromCustomer"/> + <see userInput="Welcome, $$createCustomer.firstname$$ $$createCustomer.lastname$$!" selector="{{StorefrontPanelHeaderSection.WelcomeMessage}}" stepKey="checkWelcomeMessageAfterLogoutFromCustomer"/> + <seeElement selector="{{StorefrontPanelHeaderSection.notYouLink}}" stepKey="checkLinkNotYouAfterLogoutFromCustomer"/> + + <actionGroup ref="StorefrontAssertProductInRecentlyViewedWidgetActionGroup" stepKey="checkSimpleProductInRecentlyViewedWidgetAfterLogout"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontCustomerCheckProductInWishlistSidebar" stepKey="checkSimpleProductInWishlistSidebarAfterLogout"> + <argument name="productVar" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductInRecentlyComparedWidgetActionGroup" stepKey="checkSimpleProductInRecentlyComparedWidgetAfterLogout"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductInRecentlyOrderedWidgetActionGroup" stepKey="checkSimpleProductInRecentlyOrderedWidgetAfterLogout"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + + <!--Click the *Not you?* link and check the price for Simple Product--> + <click selector="{{StorefrontPanelHeaderSection.notYouLink}}" stepKey="clickLinkNotYou"/> + <waitForPageLoad stepKey="waitForPageLoadAfterClickLinkNotYou"/> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.custom_attributes[url_key]$$)}}" stepKey="openCategoryPageAfterClickNotYou"/> + <see userInput="Default welcome msg!" selector="{{StorefrontPanelHeaderSection.WelcomeMessage}}" stepKey="checkWelcomeMessageAfterClickLinkNotYou"/> + <dontSee selector="{{StorefrontWidgetsSection.widgetRecentlyViewedProductsGrid}}" userInput="$$createSimpleProduct.name$$" stepKey="dontSeeProductInRecentlyViewedWidget"/> + <dontSee selector="{{StorefrontCustomerWishlistSidebarSection.ProductTitleByName($$createSimpleProduct.name$$)}}" stepKey="dontSeeProductInWishlistWidget"/> + <dontSee selector="{{StorefrontWidgetsSection.widgetRecentlyComparedProductsGrid}}" userInput="$$createSimpleProduct.name$$" stepKey="dontSeeProductInRecentlyComparedWidget"/> + <dontSee selector="{{StorefrontWidgetsSection.widgetRecentlyOrderedProductsGrid}}" userInput="$$createSimpleProduct.name$$" stepKey="dontSeeProductInRecentlyOrderedWidget"/> + + <!--Login to storefront from customer again--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="logInFromCustomerAfterClearLongTermCookie"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.custom_attributes[url_key]$$)}}" stepKey="openCategoryPageToCheckWidgets"/> + <see userInput="Welcome, $$createCustomer.firstname$$ $$createCustomer.lastname$$!" selector="{{StorefrontPanelHeaderSection.WelcomeMessage}}" stepKey="checkWelcomeMessageAfterLogin"/> + + <actionGroup ref="StorefrontCustomerCheckProductInWishlistSidebar" stepKey="checkSimpleProductNameInWishlistSidebarAfterLogin"> + <argument name="productVar" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductInRecentlyViewedWidgetActionGroup" stepKey="checkSimpleProductInRecentlyViewedWidgetAfterLogin"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductInRecentlyComparedWidgetActionGroup" stepKey="checkSimpleProductInRecentlyComparedWidgetAfterLogin"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductInRecentlyOrderedWidgetActionGroup" stepKey="checkSimpleProductInRecentlyOrderedWidgetAfterLogin"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Persistent/Test/Unit/Block/Header/AdditionalTest.php b/app/code/Magento/Persistent/Test/Unit/Block/Header/AdditionalTest.php index b88b02ab4cfb..407dad05c3ba 100644 --- a/app/code/Magento/Persistent/Test/Unit/Block/Header/AdditionalTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Block/Header/AdditionalTest.php @@ -34,44 +34,14 @@ class AdditionalTest extends \PHPUnit\Framework\TestCase protected $contextMock; /** - * @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Serialize\Serializer\Json|\PHPUnit_Framework_MockObject_MockObject */ - protected $eventManagerMock; + private $jsonSerializerMock; /** - * @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Persistent\Helper\Data|\PHPUnit_Framework_MockObject_MockObject */ - protected $scopeConfigMock; - - /** - * @var \Magento\Framework\App\Cache\StateInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $cacheStateMock; - - /** - * @var \Magento\Framework\App\CacheInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $cacheMock; - - /** - * @var \Magento\Framework\Session\SidResolverInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $sidResolverMock; - - /** - * @var \Magento\Framework\Session\SessionManagerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $sessionMock; - - /** - * @var \Magento\Framework\Escaper|\PHPUnit_Framework_MockObject_MockObject - */ - protected $escaperMock; - - /** - * @var \Magento\Framework\UrlInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $urlBuilderMock; + private $persistentHelperMock; /** * @var \Magento\Persistent\Block\Header\Additional @@ -93,17 +63,7 @@ protected function setUp() { $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->contextMock = $this->createPartialMock(\Magento\Framework\View\Element\Template\Context::class, [ - 'getEventManager', - 'getScopeConfig', - 'getCacheState', - 'getCache', - 'getInlineTranslation', - 'getSidResolver', - 'getSession', - 'getEscaper', - 'getUrlBuilder' - ]); + $this->contextMock = $this->createPartialMock(\Magento\Framework\View\Element\Template\Context::class, []); $this->customerViewHelperMock = $this->createMock(\Magento\Customer\Helper\View::class); $this->persistentSessionHelperMock = $this->createPartialMock( \Magento\Persistent\Helper\Session::class, @@ -119,103 +79,14 @@ protected function setUp() ['getById'] ); - $this->eventManagerMock = $this->getMockForAbstractClass( - \Magento\Framework\Event\ManagerInterface::class, - [], - '', - false, - true, - true, - ['dispatch'] - ); - $this->scopeConfigMock = $this->getMockForAbstractClass( - \Magento\Framework\App\Config\ScopeConfigInterface::class, - [], - '', - false, - true, - true, - ['getValue'] - ); - $this->cacheStateMock = $this->getMockForAbstractClass( - \Magento\Framework\App\Cache\StateInterface::class, - [], - '', - false, - true, - true, - ['isEnabled'] + $this->jsonSerializerMock = $this->createPartialMock( + \Magento\Framework\Serialize\Serializer\Json::class, + ['serialize'] ); - $this->cacheMock = $this->getMockForAbstractClass( - \Magento\Framework\App\CacheInterface::class, - [], - '', - false, - true, - true, - ['load'] + $this->persistentHelperMock = $this->createPartialMock( + \Magento\Persistent\Helper\Data::class, + ['getLifeTime'] ); - $this->sidResolverMock = $this->getMockForAbstractClass( - \Magento\Framework\Session\SidResolverInterface::class, - [], - '', - false, - true, - true, - ['getSessionIdQueryParam'] - ); - $this->sessionMock = $this->getMockForAbstractClass( - \Magento\Framework\Session\SessionManagerInterface::class, - [], - '', - false, - true, - true, - ['getSessionId'] - ); - $this->escaperMock = $this->getMockForAbstractClass( - \Magento\Framework\Escaper::class, - [], - '', - false, - true, - true, - ['escapeHtml'] - ); - $this->urlBuilderMock = $this->getMockForAbstractClass( - \Magento\Framework\UrlInterface::class, - [], - '', - false, - true, - true, - ['getUrl'] - ); - - $this->contextMock->expects($this->once()) - ->method('getEventManager') - ->willReturn($this->eventManagerMock); - $this->contextMock->expects($this->once()) - ->method('getScopeConfig') - ->willReturn($this->scopeConfigMock); - $this->contextMock->expects($this->once()) - ->method('getCacheState') - ->willReturn($this->cacheStateMock); - $this->contextMock->expects($this->once()) - ->method('getCache') - ->willReturn($this->cacheMock); - $this->contextMock->expects($this->once()) - ->method('getSidResolver') - ->willReturn($this->sidResolverMock); - $this->contextMock->expects($this->once()) - ->method('getSession') - ->willReturn($this->sessionMock); - $this->contextMock->expects($this->once()) - ->method('getEscaper') - ->willReturn($this->escaperMock); - $this->contextMock->expects($this->once()) - ->method('getUrlBuilder') - ->willReturn($this->urlBuilderMock); $this->additional = $this->objectManager->getObject( \Magento\Persistent\Block\Header\Additional::class, @@ -224,91 +95,48 @@ protected function setUp() 'customerViewHelper' => $this->customerViewHelperMock, 'persistentSessionHelper' => $this->persistentSessionHelperMock, 'customerRepository' => $this->customerRepositoryMock, - 'data' => [] + 'data' => [], + 'jsonSerializer' => $this->jsonSerializerMock, + 'persistentHelper' => $this->persistentHelperMock, ] ); } /** - * Run test toHtml method - * - * @param bool $customerId * @return void - * - * @dataProvider dataProviderToHtml */ - public function testToHtml($customerId) + public function testGetCustomerId(): void { - $cacheData = false; - $idQueryParam = 'id-query-param'; - $sessionId = 'session-id'; - - $this->additional->setData('cache_lifetime', 789); - $this->additional->setData('cache_key', 'cache-key'); - - $this->eventManagerMock->expects($this->at(0)) - ->method('dispatch') - ->with('view_block_abstract_to_html_before', ['block' => $this->additional]); - $this->eventManagerMock->expects($this->at(1)) - ->method('dispatch') - ->with('view_block_abstract_to_html_after'); - $this->scopeConfigMock->expects($this->once()) - ->method('getValue') - ->with( - 'advanced/modules_disable_output/Magento_Persistent', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - )->willReturn(false); - - // get cache - $this->cacheStateMock->expects($this->at(0)) - ->method('isEnabled') - ->with(\Magento\Persistent\Block\Header\Additional::CACHE_GROUP) - ->willReturn(true); - // save cache - $this->cacheStateMock->expects($this->at(1)) - ->method('isEnabled') - ->with(\Magento\Persistent\Block\Header\Additional::CACHE_GROUP) - ->willReturn(false); - - $this->cacheMock->expects($this->once()) - ->method('load') - ->willReturn($cacheData); - $this->sidResolverMock->expects($this->never()) - ->method('getSessionIdQueryParam') - ->with($this->sessionMock) - ->willReturn($idQueryParam); - $this->sessionMock->expects($this->never()) - ->method('getSessionId') - ->willReturn($sessionId); - - // call protected _toHtml method + $customerId = 1; + /** @var \Magento\Persistent\Model\Session|\PHPUnit_Framework_MockObject_MockObject $sessionMock */ $sessionMock = $this->createPartialMock(\Magento\Persistent\Model\Session::class, ['getCustomerId']); - - $this->persistentSessionHelperMock->expects($this->atLeastOnce()) - ->method('getSession') - ->willReturn($sessionMock); - - $sessionMock->expects($this->atLeastOnce()) + $sessionMock->expects($this->once()) ->method('getCustomerId') ->willReturn($customerId); + $this->persistentSessionHelperMock->expects($this->once()) + ->method('getSession') + ->willReturn($sessionMock); - if ($customerId) { - $this->assertEquals('<span><a >Not you?</a></span>', $this->additional->toHtml()); - } else { - $this->assertEquals('', $this->additional->toHtml()); - } + $this->assertEquals($customerId, $this->additional->getCustomerId()); } /** - * Data provider for dataProviderToHtml method - * - * @return array + * @return void */ - public function dataProviderToHtml() + public function testGetConfig(): void { - return [ - ['customerId' => 2], - ['customerId' => null], - ]; + $lifeTime = 500; + $arrayToSerialize = ['expirationLifetime' => $lifeTime]; + $serializedArray = '{"expirationLifetime":' . $lifeTime . '}'; + + $this->persistentHelperMock->expects($this->once()) + ->method('getLifeTime') + ->willReturn($lifeTime); + $this->jsonSerializerMock->expects($this->once()) + ->method('serialize') + ->with($arrayToSerialize) + ->willReturn($serializedArray); + + $this->assertEquals($serializedArray, $this->additional->getConfig()); } } diff --git a/app/code/Magento/Persistent/Test/Unit/Model/Checkout/GuestPaymentInformationManagementPluginTest.php b/app/code/Magento/Persistent/Test/Unit/Model/Checkout/GuestPaymentInformationManagementPluginTest.php index b9285715146a..c7f84b476fa7 100644 --- a/app/code/Magento/Persistent/Test/Unit/Model/Checkout/GuestPaymentInformationManagementPluginTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Model/Checkout/GuestPaymentInformationManagementPluginTest.php @@ -102,8 +102,7 @@ public function testBeforeSavePaymentInformationAndPlaceOrderCartConvertsToGuest ['setCustomerEmail', 'getAddressesCollection'], false ); - $this->checkoutSessionMock->expects($this->once())->method('getQuote')->willReturn($quoteMock); - $quoteMock->expects($this->once())->method('getId')->willReturn($cartId); + $this->checkoutSessionMock->method('getQuoteId')->willReturn($cartId); $this->cartRepositoryMock->expects($this->once())->method('get')->with($cartId)->willReturn($quoteMock); $quoteMock->expects($this->once())->method('setCustomerEmail')->with($email); /** @var \Magento\Framework\Data\Collection|\PHPUnit_Framework_MockObject_MockObject $collectionMock */ diff --git a/app/code/Magento/Persistent/Test/Unit/Model/ObserverTest.php b/app/code/Magento/Persistent/Test/Unit/Model/ObserverTest.php index 7008a9eb25e5..6d4db70adc64 100644 --- a/app/code/Magento/Persistent/Test/Unit/Model/ObserverTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Model/ObserverTest.php @@ -80,31 +80,18 @@ protected function setUp() ); } - public function testEmulateWelcomeBlock() + /** + * @return void + */ + public function testEmulateWelcomeBlock(): void { - $customerId = 1; - $customerName = 'Test Customer Name'; - $welcomeMessage = __('Welcome, %1!', $customerName); - $customerMock = $this->getMockForAbstractClass(\Magento\Customer\Api\Data\CustomerInterface::class); + $welcomeMessage = __(' '); $block = $this->getMockBuilder(\Magento\Framework\View\Element\AbstractBlock::class) ->disableOriginalConstructor() ->setMethods(['setWelcome']) ->getMock(); - $headerAdditionalBlock = $this->getMockBuilder(\Magento\Framework\View\Element\AbstractBlock::class) - ->disableOriginalConstructor() - ->getMock(); - $this->persistentSessionMock->expects($this->once())->method('getSession')->willReturn($this->sessionMock); - $this->sessionMock->expects($this->once())->method('getCustomerId')->willReturn($customerId); - $this->customerRepositoryMock - ->expects($this->once()) - ->method('getById') - ->with($customerId)->willReturn($customerMock); - $this->customerViewHelperMock->expects($this->once())->method('getCustomerName')->willReturn($customerName); - $this->layoutMock->expects($this->once()) - ->method('getBlock') - ->with('header.additional') - ->willReturn($headerAdditionalBlock); $block->expects($this->once())->method('setWelcome')->with($welcomeMessage); + $this->observer->emulateWelcomeBlock($block); } } diff --git a/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php b/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php index 5b96acae1d74..e5de8e7d4aad 100644 --- a/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php @@ -245,8 +245,8 @@ public function testConvertCustomerCartToGuest() $emailArgs = ['email' => null]; $this->checkoutSessionMock->expects($this->once()) - ->method('getQuote')->willReturn($this->quoteMock); - $this->quoteMock->expects($this->exactly(2))->method('getId')->willReturn($quoteId); + ->method('getQuoteId')->willReturn($quoteId); + $this->quoteMock->expects($this->once())->method('getId')->willReturn($quoteId); $this->quoteRepositoryMock->expects($this->once())->method('get')->with($quoteId)->willReturn($this->quoteMock); $this->quoteMock->expects($this->once()) ->method('setIsActive')->with(true)->willReturn($this->quoteMock); @@ -286,21 +286,19 @@ public function testConvertCustomerCartToGuest() public function testConvertCustomerCartToGuestWithEmptyQuote() { $this->checkoutSessionMock->expects($this->once()) - ->method('getQuote')->willReturn($this->quoteMock); - $this->quoteMock->expects($this->once())->method('getId')->willReturn(null); + ->method('getQuoteId')->willReturn(null); $this->quoteRepositoryMock->expects($this->once())->method('get')->with(null)->willReturn(null); - $this->model->convertCustomerCartToGuest(); } public function testConvertCustomerCartToGuestWithEmptyQuoteId() { $this->checkoutSessionMock->expects($this->once()) - ->method('getQuote')->willReturn($this->quoteMock); - $this->quoteMock->expects($this->once())->method('getId')->willReturn(1); + ->method('getQuoteId')->willReturn(1); $quoteWithNoId = $this->quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); $quoteWithNoId->expects($this->once())->method('getId')->willReturn(null); $this->quoteRepositoryMock->expects($this->once())->method('get')->with(1)->willReturn($quoteWithNoId); + $this->quoteMock->expects($this->once())->method('getId')->willReturn(1); $this->model->convertCustomerCartToGuest(); } } diff --git a/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php b/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php index 46dda1be365d..b096dd2317a3 100644 --- a/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php @@ -1,12 +1,16 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Persistent\Test\Unit\Observer; +use Magento\Quote\Model\Quote; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class CheckExpirePersistentQuoteObserverTest extends \PHPUnit\Framework\TestCase { /** @@ -54,6 +58,11 @@ class CheckExpirePersistentQuoteObserverTest extends \PHPUnit\Framework\TestCase */ private $requestMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject|Quote + */ + private $quoteMock; + /** * @inheritdoc */ @@ -83,6 +92,10 @@ protected function setUp() $this->checkoutSessionMock, $this->requestMock ); + $this->quoteMock = $this->getMockBuilder(Quote::class) + ->setMethods(['getCustomerIsGuest', 'getIsPersistent']) + ->disableOriginalConstructor() + ->getMock(); } public function testExecuteWhenCanNotApplyPersistentData() @@ -133,6 +146,11 @@ public function testExecuteWhenPersistentIsEnabled( ->willReturn(true); $this->persistentHelperMock->expects($this->once())->method('isEnabled')->willReturn(true); $this->sessionMock->expects($this->once())->method('isPersistent')->willReturn(false); + $this->checkoutSessionMock + ->method('getQuote') + ->willReturn($this->quoteMock); + $this->quoteMock->method('getCustomerIsGuest')->willReturn(true); + $this->quoteMock->method('getIsPersistent')->willReturn(true); $this->customerSessionMock ->expects($this->atLeastOnce()) ->method('isLoggedIn') diff --git a/app/code/Magento/Persistent/Test/Unit/Observer/SetCheckoutSessionPersistentDataObserverTest.php b/app/code/Magento/Persistent/Test/Unit/Observer/SetCheckoutSessionPersistentDataObserverTest.php new file mode 100644 index 000000000000..01c217c1c5cd --- /dev/null +++ b/app/code/Magento/Persistent/Test/Unit/Observer/SetCheckoutSessionPersistentDataObserverTest.php @@ -0,0 +1,149 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Persistent\Test\Unit\Observer; + +/** + * Class SetCheckoutSessionPersistentDataObserverTest + */ +class SetCheckoutSessionPersistentDataObserverTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Persistent\Observer\SetCheckoutSessionPersistentDataObserver + */ + private $model; + + /** + * @var \Magento\Persistent\Helper\Data| \PHPUnit_Framework_MockObject_MockObject + */ + private $helperMock; + + /** + * @var \Magento\Persistent\Helper\Session| \PHPUnit_Framework_MockObject_MockObject + */ + private $sessionHelperMock; + + /** + * @var \Magento\Checkout\Model\Session| \PHPUnit_Framework_MockObject_MockObject + */ + private $checkoutSessionMock; + + /** + * @var \Magento\Customer\Model\Session| \PHPUnit_Framework_MockObject_MockObject + */ + private $customerSessionMock; + + /** + * @var \Magento\Persistent\Model\Session| \PHPUnit_Framework_MockObject_MockObject + */ + private $persistentSessionMock; + + /** + * @var \Magento\Customer\Api\CustomerRepositoryInterface| \PHPUnit_Framework_MockObject_MockObject + */ + private $customerRepositoryMock; + + /** + * @var \Magento\Framework\Event\Observer|\PHPUnit_Framework_MockObject_MockObject + */ + private $observerMock; + + /** + * @var \Magento\Framework\Event|\PHPUnit_Framework_MockObject_MockObject + */ + private $eventMock; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->helperMock = $this->createMock(\Magento\Persistent\Helper\Data::class); + $this->sessionHelperMock = $this->createMock(\Magento\Persistent\Helper\Session::class); + $this->checkoutSessionMock = $this->createMock(\Magento\Checkout\Model\Session::class); + $this->customerSessionMock = $this->createMock(\Magento\Customer\Model\Session::class); + $this->observerMock = $this->createMock(\Magento\Framework\Event\Observer::class); + $this->eventMock = $this->createPartialMock(\Magento\Framework\Event::class, ['getData']); + $this->persistentSessionMock = $this->createPartialMock( + \Magento\Persistent\Model\Session::class, + ['getCustomerId'] + ); + $this->customerRepositoryMock = $this->createMock( + \Magento\Customer\Api\CustomerRepositoryInterface::class + ); + $this->model = new \Magento\Persistent\Observer\SetCheckoutSessionPersistentDataObserver( + $this->sessionHelperMock, + $this->customerSessionMock, + $this->helperMock, + $this->customerRepositoryMock + ); + } + + /** + * Test execute method when session is not persistent + * + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function testExecuteWhenSessionIsNotPersistent() + { + $this->observerMock->expects($this->once()) + ->method('getEvent') + ->will($this->returnValue($this->eventMock)); + $this->eventMock->expects($this->once()) + ->method('getData') + ->will($this->returnValue($this->checkoutSessionMock)); + $this->sessionHelperMock->expects($this->once()) + ->method('isPersistent') + ->will($this->returnValue(false)); + $this->checkoutSessionMock->expects($this->never()) + ->method('setLoadInactive'); + $this->checkoutSessionMock->expects($this->never()) + ->method('setCustomerData'); + $this->model->execute($this->observerMock); + } + + /** + * Test execute method when session is persistent + * + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function testExecute() + { + $this->observerMock->expects($this->once()) + ->method('getEvent') + ->will($this->returnValue($this->eventMock)); + $this->eventMock->expects($this->once()) + ->method('getData') + ->will($this->returnValue($this->checkoutSessionMock)); + $this->sessionHelperMock->expects($this->exactly(2)) + ->method('isPersistent') + ->will($this->returnValue(true)); + $this->customerSessionMock->expects($this->once()) + ->method('isLoggedIn') + ->will($this->returnValue(false)); + $this->helperMock->expects($this->exactly(2)) + ->method('isShoppingCartPersist') + ->will($this->returnValue(true)); + $this->persistentSessionMock->expects($this->once()) + ->method('getCustomerId') + ->will($this->returnValue(123)); + $this->sessionHelperMock->expects($this->once()) + ->method('getSession') + ->will($this->returnValue($this->persistentSessionMock)); + $this->customerRepositoryMock->expects($this->once()) + ->method('getById') + ->will($this->returnValue(true)); //? + $this->checkoutSessionMock->expects($this->never()) + ->method('setLoadInactive'); + $this->checkoutSessionMock->expects($this->once()) + ->method('setCustomerData'); + $this->model->execute($this->observerMock); + } +} diff --git a/app/code/Magento/Persistent/Test/Unit/Observer/SetLoadPersistentQuoteObserverTest.php b/app/code/Magento/Persistent/Test/Unit/Observer/SetLoadPersistentQuoteObserverTest.php deleted file mode 100644 index fd78a6852ea5..000000000000 --- a/app/code/Magento/Persistent/Test/Unit/Observer/SetLoadPersistentQuoteObserverTest.php +++ /dev/null @@ -1,73 +0,0 @@ -<?php -/** - * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Persistent\Test\Unit\Observer; - -class SetLoadPersistentQuoteObserverTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Persistent\Observer\SetLoadPersistentQuoteObserver - */ - protected $model; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $helperMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $sessionHelperMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $checkoutSessionMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $customerSessionMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $observerMock; - - protected function setUp() - { - $this->helperMock = $this->createMock(\Magento\Persistent\Helper\Data::class); - $this->sessionHelperMock = $this->createMock(\Magento\Persistent\Helper\Session::class); - $this->checkoutSessionMock = $this->createMock(\Magento\Checkout\Model\Session::class); - $this->customerSessionMock = $this->createMock(\Magento\Customer\Model\Session::class); - $this->observerMock = $this->createMock(\Magento\Framework\Event\Observer::class); - - $this->model = new \Magento\Persistent\Observer\SetLoadPersistentQuoteObserver( - $this->sessionHelperMock, - $this->helperMock, - $this->customerSessionMock, - $this->checkoutSessionMock - ); - } - - public function testExecuteWhenSessionIsNotPersistent() - { - $this->sessionHelperMock->expects($this->once())->method('isPersistent')->will($this->returnValue(false)); - $this->checkoutSessionMock->expects($this->never())->method('setLoadInactive'); - $this->model->execute($this->observerMock); - } - - public function testExecute() - { - $this->sessionHelperMock->expects($this->once())->method('isPersistent')->will($this->returnValue(true)); - $this->customerSessionMock->expects($this->once())->method('isLoggedIn')->will($this->returnValue(false)); - $this->helperMock->expects($this->once())->method('isShoppingCartPersist')->will($this->returnValue(true)); - $this->checkoutSessionMock->expects($this->never())->method('setLoadInactive'); - $this->model->execute($this->observerMock); - } -} diff --git a/app/code/Magento/Persistent/Test/Unit/Observer/SetQuotePersistentDataObserverTest.php b/app/code/Magento/Persistent/Test/Unit/Observer/SetQuotePersistentDataObserverTest.php index 6724743789ce..ffa829e8456c 100644 --- a/app/code/Magento/Persistent/Test/Unit/Observer/SetQuotePersistentDataObserverTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Observer/SetQuotePersistentDataObserverTest.php @@ -7,6 +7,9 @@ namespace Magento\Persistent\Test\Unit\Observer; +/** + * Observer test for setting "is_persistent" value to quote + */ class SetQuotePersistentDataObserverTest extends \PHPUnit\Framework\TestCase { /** @@ -83,7 +86,6 @@ public function testExecuteWhenQuoteNotExist() ->method('getEvent') ->will($this->returnValue($this->eventManagerMock)); $this->eventManagerMock->expects($this->once())->method('getQuote'); - $this->customerSessionMock->expects($this->never())->method('isLoggedIn'); $this->model->execute($this->observerMock); } @@ -98,8 +100,7 @@ public function testExecuteWhenSessionIsPersistent() ->expects($this->once()) ->method('getQuote') ->will($this->returnValue($this->quoteMock)); - $this->customerSessionMock->expects($this->once())->method('isLoggedIn')->will($this->returnValue(false)); - $this->helperMock->expects($this->once())->method('isShoppingCartPersist')->will($this->returnValue(false)); + $this->helperMock->expects($this->once())->method('isShoppingCartPersist')->will($this->returnValue(true)); $this->quoteManagerMock->expects($this->once())->method('isPersistent')->will($this->returnValue(true)); $this->quoteMock->expects($this->once())->method('setIsPersistent')->with(true); $this->model->execute($this->observerMock); diff --git a/app/code/Magento/Persistent/etc/db_schema.xml b/app/code/Magento/Persistent/etc/db_schema.xml index 68678fc60f09..5021d240417b 100644 --- a/app/code/Magento/Persistent/etc/db_schema.xml +++ b/app/code/Magento/Persistent/etc/db_schema.xml @@ -18,22 +18,22 @@ <column xsi:type="text" name="info" nullable="true" comment="Session Data"/> <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" comment="Updated At"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="persistent_id"/> </constraint> - <constraint xsi:type="foreign" name="PERSISTENT_SESSION_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="PERSISTENT_SESSION_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID" table="persistent_session" column="customer_id" referenceTable="customer_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="PERSISTENT_SESSION_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID" + <constraint xsi:type="foreign" referenceId="PERSISTENT_SESSION_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID" table="persistent_session" column="website_id" referenceTable="store_website" referenceColumn="website_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="PERSISTENT_SESSION_KEY"> + <constraint xsi:type="unique" referenceId="PERSISTENT_SESSION_KEY"> <column name="key"/> </constraint> - <constraint xsi:type="unique" name="PERSISTENT_SESSION_CUSTOMER_ID"> + <constraint xsi:type="unique" referenceId="PERSISTENT_SESSION_CUSTOMER_ID"> <column name="customer_id"/> </constraint> - <index name="PERSISTENT_SESSION_UPDATED_AT" indexType="btree"> + <index referenceId="PERSISTENT_SESSION_UPDATED_AT" indexType="btree"> <column name="updated_at"/> </index> </table> diff --git a/app/code/Magento/Persistent/etc/di.xml b/app/code/Magento/Persistent/etc/di.xml index f49d4361acb5..c28426b4f25b 100644 --- a/app/code/Magento/Persistent/etc/di.xml +++ b/app/code/Magento/Persistent/etc/di.xml @@ -12,4 +12,7 @@ <type name="Magento\Customer\CustomerData\Customer"> <plugin name="section_data" type="Magento\Persistent\Model\Plugin\CustomerData" /> </type> + <type name="Magento\Framework\App\Http\Context"> + <plugin name="persistent_page_cache_variation" type="Magento\Persistent\Model\Plugin\PersistentCustomerContext" /> + </type> </config> diff --git a/app/code/Magento/Persistent/etc/frontend/di.xml b/app/code/Magento/Persistent/etc/frontend/di.xml index f976f4de79c2..3c33f8a51c41 100644 --- a/app/code/Magento/Persistent/etc/frontend/di.xml +++ b/app/code/Magento/Persistent/etc/frontend/di.xml @@ -35,4 +35,18 @@ <type name="Magento\Checkout\Model\GuestPaymentInformationManagement"> <plugin name="inject_guest_address_for_nologin" type="Magento\Persistent\Model\Checkout\GuestPaymentInformationManagementPlugin" /> </type> + <type name="Magento\Customer\CustomerData\SectionPoolInterface"> + <arguments> + <argument name="sectionSourceMap" xsi:type="array"> + <item name="persistent" xsi:type="string">Magento\Persistent\CustomerData\Persistent</item> + </argument> + </arguments> + </type> + <type name="Magento\Customer\Block\CustomerData"> + <arguments> + <argument name="expirableSectionNames" xsi:type="array"> + <item name="persistent" xsi:type="string">persistent</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Persistent/etc/frontend/events.xml b/app/code/Magento/Persistent/etc/frontend/events.xml index 193b9a10818e..79720695ea6f 100644 --- a/app/code/Magento/Persistent/etc/frontend/events.xml +++ b/app/code/Magento/Persistent/etc/frontend/events.xml @@ -49,7 +49,7 @@ <observer name="persistent" instance="Magento\Persistent\Observer\SetQuotePersistentDataObserver" /> </event> <event name="custom_quote_process"> - <observer name="persistent" instance="Magento\Persistent\Observer\SetLoadPersistentQuoteObserver" /> + <observer name="persistent" instance="Magento\Persistent\Observer\SetCheckoutSessionPersistentDataObserver" /> </event> <event name="customer_register_success"> <observer name="persistent" instance="Magento\Persistent\Observer\RemovePersistentCookieOnRegisterObserver" /> diff --git a/app/code/Magento/Persistent/etc/frontend/sections.xml b/app/code/Magento/Persistent/etc/frontend/sections.xml new file mode 100644 index 000000000000..16b44c502fc4 --- /dev/null +++ b/app/code/Magento/Persistent/etc/frontend/sections.xml @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Customer:etc/sections.xsd"> + <action name="persistent/index/unsetCookie"> + <section name="persistent"/> + </action> +</config> diff --git a/app/code/Magento/Persistent/etc/webapi_rest/events.xml b/app/code/Magento/Persistent/etc/webapi_rest/events.xml index 1eff845386bf..79dffa183456 100644 --- a/app/code/Magento/Persistent/etc/webapi_rest/events.xml +++ b/app/code/Magento/Persistent/etc/webapi_rest/events.xml @@ -22,7 +22,7 @@ <observer name="persistent" instance="Magento\Persistent\Observer\SetQuotePersistentDataObserver" /> </event> <event name="custom_quote_process"> - <observer name="persistent" instance="Magento\Persistent\Observer\SetLoadPersistentQuoteObserver" /> + <observer name="persistent" instance="Magento\Persistent\Observer\SetCheckoutSessionPersistentDataObserver" /> </event> <event name="customer_register_success"> <observer name="persistent" instance="Magento\Persistent\Observer\RemovePersistentCookieOnRegisterObserver" /> diff --git a/app/code/Magento/Persistent/etc/webapi_soap/events.xml b/app/code/Magento/Persistent/etc/webapi_soap/events.xml index 1eff845386bf..79dffa183456 100644 --- a/app/code/Magento/Persistent/etc/webapi_soap/events.xml +++ b/app/code/Magento/Persistent/etc/webapi_soap/events.xml @@ -22,7 +22,7 @@ <observer name="persistent" instance="Magento\Persistent\Observer\SetQuotePersistentDataObserver" /> </event> <event name="custom_quote_process"> - <observer name="persistent" instance="Magento\Persistent\Observer\SetLoadPersistentQuoteObserver" /> + <observer name="persistent" instance="Magento\Persistent\Observer\SetCheckoutSessionPersistentDataObserver" /> </event> <event name="customer_register_success"> <observer name="persistent" instance="Magento\Persistent\Observer\RemovePersistentCookieOnRegisterObserver" /> diff --git a/app/code/Magento/Persistent/view/frontend/requirejs-config.js b/app/code/Magento/Persistent/view/frontend/requirejs-config.js new file mode 100644 index 000000000000..e30e07c454be --- /dev/null +++ b/app/code/Magento/Persistent/view/frontend/requirejs-config.js @@ -0,0 +1,14 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +var config = { + config: { + mixins: { + 'Magento_Customer/js/customer-data': { + 'Magento_Persistent/js/view/customer-data-mixin': true + } + } + } +}; diff --git a/app/code/Magento/Persistent/view/frontend/templates/additional.phtml b/app/code/Magento/Persistent/view/frontend/templates/additional.phtml new file mode 100644 index 000000000000..28dce5dc23cc --- /dev/null +++ b/app/code/Magento/Persistent/view/frontend/templates/additional.phtml @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +?> +<?php if ($block->getCustomerId()) :?> + <span> + <a <?= /* @escapeNotVerified */ $block->getLinkAttributes()?>><?= $block->escapeHtml(__('Not you?'));?></a> + </span> +<?php endif;?> +<script type="application/javascript"> + window.persistent = <?= /* @noEscape */ $block->getConfig(); ?>; +</script> +<script type="text/x-magento-init"> + { + "li.greet.welcome > span.not-logged-in": { + "Magento_Persistent/js/view/additional-welcome": {} + } + } +</script> diff --git a/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js b/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js new file mode 100644 index 000000000000..7ace6e60d1c3 --- /dev/null +++ b/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js @@ -0,0 +1,55 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'mage/translate', + 'Magento_Customer/js/customer-data' +], function ($, $t, customerData) { + 'use strict'; + + return { + /** + * Init. + */ + init: function () { + var persistent = customerData.get('persistent'); + + if (persistent().fullname === undefined) { + customerData.get('persistent').subscribe(this.replacePersistentWelcome); + } else { + this.replacePersistentWelcome(); + } + }, + + /** + * Replace welcome message for customer with persistent cookie. + */ + replacePersistentWelcome: function () { + var persistent = customerData.get('persistent'), + welcomeElems; + + if (persistent().fullname !== undefined) { + welcomeElems = $('li.greet.welcome > span.not-logged-in'); + + if (welcomeElems.length) { + $(welcomeElems).each(function () { + var html = $t('Welcome, %1!').replace('%1', persistent().fullname); + + $(this).attr('data-bind', html); + $(this).html(html); + }); + } + } + }, + + /** + * @constructor + */ + 'Magento_Persistent/js/view/additional-welcome': function () { + this.init(); + } + }; +}); diff --git a/app/code/Magento/Persistent/view/frontend/web/js/view/customer-data-mixin.js b/app/code/Magento/Persistent/view/frontend/web/js/view/customer-data-mixin.js new file mode 100644 index 000000000000..855404c6f6f3 --- /dev/null +++ b/app/code/Magento/Persistent/view/frontend/web/js/view/customer-data-mixin.js @@ -0,0 +1,51 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'mage/utils/wrapper' +], function ($, wrapper) { + 'use strict'; + + var mixin = { + + /** + * Check if persistent section is expired due to lifetime. + * + * @param {Function} originFn - Original method. + * @return {Array} + */ + getExpiredSectionNames: function (originFn) { + var expiredSections = originFn(), + storage = $.initNamespaceStorage('mage-cache-storage').localStorage, + currentTimestamp = Math.floor(Date.now() / 1000), + persistentIndex = expiredSections.indexOf('persistent'), + persistentLifeTime = 0, + sectionData; + + if (window.persistent !== undefined && window.persistent.expirationLifetime !== undefined) { + persistentLifeTime = window.persistent.expirationLifetime; + } + + if (persistentIndex !== -1) { + sectionData = storage.get('persistent'); + + if (typeof sectionData === 'object' && + sectionData['data_id'] + persistentLifeTime >= currentTimestamp + ) { + expiredSections.splice(persistentIndex, 1); + } + } + + return expiredSections; + } + }; + + /** + * Override default customer-data.getExpiredSectionNames(). + */ + return function (target) { + return wrapper.extend(target, mixin); + }; +}); diff --git a/app/code/Magento/ProductAlert/Controller/Add/Price.php b/app/code/Magento/ProductAlert/Controller/Add/Price.php index 04e623105e64..fbaff109fd15 100644 --- a/app/code/Magento/ProductAlert/Controller/Add/Price.php +++ b/app/code/Magento/ProductAlert/Controller/Add/Price.php @@ -6,6 +6,7 @@ namespace Magento\ProductAlert\Controller\Add; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\ProductAlert\Controller\Add as AddController; use Magento\Framework\App\Action\Context; use Magento\Customer\Model\Session as CustomerSession; @@ -16,7 +17,10 @@ use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Exception\NoSuchEntityException; -class Price extends AddController +/** + * Controller for notifying about price. + */ +class Price extends AddController implements HttpPostActionInterface { /** * @var \Magento\Store\Model\StoreManagerInterface @@ -62,6 +66,8 @@ protected function isInternal($url) } /** + * Method for adding info about product alert price. + * * @return \Magento\Framework\Controller\Result\Redirect */ public function execute() @@ -75,6 +81,7 @@ public function execute() return $resultRedirect; } + $store = $this->storeManager->getStore(); try { /* @var $product \Magento\Catalog\Model\Product */ $product = $this->productRepository->getById($productId); @@ -83,11 +90,8 @@ public function execute() ->setCustomerId($this->customerSession->getCustomerId()) ->setProductId($product->getId()) ->setPrice($product->getFinalPrice()) - ->setWebsiteId( - $this->_objectManager->get(\Magento\Store\Model\StoreManagerInterface::class) - ->getStore() - ->getWebsiteId() - ); + ->setWebsiteId($store->getWebsiteId()) + ->setStoreId($store->getId()); $model->save(); $this->messageManager->addSuccess(__('You saved the alert subscription.')); } catch (NoSuchEntityException $noEntityException) { diff --git a/app/code/Magento/ProductAlert/Controller/Add/Stock.php b/app/code/Magento/ProductAlert/Controller/Add/Stock.php index 56d052f7e11e..24cab39d544f 100644 --- a/app/code/Magento/ProductAlert/Controller/Add/Stock.php +++ b/app/code/Magento/ProductAlert/Controller/Add/Stock.php @@ -6,6 +6,7 @@ namespace Magento\ProductAlert\Controller\Add; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\ProductAlert\Controller\Add as AddController; use Magento\Framework\App\Action\Context; use Magento\Customer\Model\Session as CustomerSession; @@ -13,29 +14,44 @@ use Magento\Framework\App\Action\Action; use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Store\Model\StoreManagerInterface; -class Stock extends AddController +/** + * Controller for notifying about stock. + */ +class Stock extends AddController implements HttpPostActionInterface { /** * @var \Magento\Catalog\Api\ProductRepositoryInterface */ protected $productRepository; + /** + * @var StoreManagerInterface + */ + private $storeManager; + /** * @param \Magento\Framework\App\Action\Context $context * @param \Magento\Customer\Model\Session $customerSession * @param \Magento\Catalog\Api\ProductRepositoryInterface $productRepository + * @param StoreManagerInterface|null $storeManager */ public function __construct( Context $context, CustomerSession $customerSession, - ProductRepositoryInterface $productRepository + ProductRepositoryInterface $productRepository, + StoreManagerInterface $storeManager = null ) { $this->productRepository = $productRepository; parent::__construct($context, $customerSession); + $this->storeManager = $storeManager ?: $this->_objectManager + ->get(\Magento\Store\Model\StoreManagerInterface::class); } /** + * Method for adding info about product alert stock. + * * @return \Magento\Framework\Controller\Result\Redirect */ public function execute() @@ -52,15 +68,13 @@ public function execute() try { /* @var $product \Magento\Catalog\Model\Product */ $product = $this->productRepository->getById($productId); + $store = $this->storeManager->getStore(); /** @var \Magento\ProductAlert\Model\Stock $model */ $model = $this->_objectManager->create(\Magento\ProductAlert\Model\Stock::class) ->setCustomerId($this->customerSession->getCustomerId()) ->setProductId($product->getId()) - ->setWebsiteId( - $this->_objectManager->get(\Magento\Store\Model\StoreManagerInterface::class) - ->getStore() - ->getWebsiteId() - ); + ->setWebsiteId($store->getWebsiteId()) + ->setStoreId($store->getId()); $model->save(); $this->messageManager->addSuccess(__('Alert subscription has been saved.')); } catch (NoSuchEntityException $noEntityException) { diff --git a/app/code/Magento/ProductAlert/Model/Email.php b/app/code/Magento/ProductAlert/Model/Email.php index 7bc4aba35154..3351166aa6a1 100644 --- a/app/code/Magento/ProductAlert/Model/Email.php +++ b/app/code/Magento/ProductAlert/Model/Email.php @@ -1,10 +1,36 @@ <?php + /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\ProductAlert\Model; +use Magento\Catalog\Model\Product; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Helper\View; +use Magento\Framework\App\Area; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Data\Collection\AbstractDb; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\MailException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Mail\Template\TransportBuilder; +use Magento\Framework\Model\AbstractModel; +use Magento\Framework\Model\Context; +use Magento\Framework\Model\ResourceModel\AbstractResource; +use Magento\Framework\Registry; +use Magento\ProductAlert\Block\Email\AbstractEmail; +use Magento\ProductAlert\Block\Email\Price; +use Magento\ProductAlert\Block\Email\Stock; +use Magento\ProductAlert\Helper\Data; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\App\Emulation; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Model\Website; + /** * ProductAlert Email processor * @@ -13,8 +39,10 @@ * * @api * @since 100.0.2 + * @method int getStoreId() + * @method $this setStoreId() */ -class Email extends \Magento\Framework\Model\AbstractModel +class Email extends AbstractModel { const XML_PATH_EMAIL_PRICE_TEMPLATE = 'catalog/productalert/email_price_template'; @@ -32,14 +60,14 @@ class Email extends \Magento\Framework\Model\AbstractModel /** * Website Model * - * @var \Magento\Store\Model\Website + * @var Website */ protected $_website; /** * Customer model * - * @var \Magento\Customer\Api\Data\CustomerInterface + * @var CustomerInterface */ protected $_customer; @@ -60,83 +88,83 @@ class Email extends \Magento\Framework\Model\AbstractModel /** * Price block * - * @var \Magento\ProductAlert\Block\Email\Price + * @var Price */ protected $_priceBlock; /** * Stock block * - * @var \Magento\ProductAlert\Block\Email\Stock + * @var Stock */ protected $_stockBlock; /** * Product alert data * - * @var \Magento\ProductAlert\Helper\Data + * @var Data */ protected $_productAlertData = null; /** * Core store config * - * @var \Magento\Framework\App\Config\ScopeConfigInterface + * @var ScopeConfigInterface */ protected $_scopeConfig; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $_storeManager; /** - * @var \Magento\Customer\Api\CustomerRepositoryInterface + * @var CustomerRepositoryInterface */ protected $customerRepository; /** - * @var \Magento\Store\Model\App\Emulation + * @var Emulation */ protected $_appEmulation; /** - * @var \Magento\Framework\Mail\Template\TransportBuilder + * @var TransportBuilder */ protected $_transportBuilder; /** - * @var \Magento\Customer\Helper\View + * @var View */ protected $_customerHelper; /** - * @param \Magento\Framework\Model\Context $context - * @param \Magento\Framework\Registry $registry - * @param \Magento\ProductAlert\Helper\Data $productAlertData - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository - * @param \Magento\Customer\Helper\View $customerHelper - * @param \Magento\Store\Model\App\Emulation $appEmulation - * @param \Magento\Framework\Mail\Template\TransportBuilder $transportBuilder - * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource - * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection + * @param Context $context + * @param Registry $registry + * @param Data $productAlertData + * @param ScopeConfigInterface $scopeConfig + * @param StoreManagerInterface $storeManager + * @param CustomerRepositoryInterface $customerRepository + * @param View $customerHelper + * @param Emulation $appEmulation + * @param TransportBuilder $transportBuilder + * @param AbstractResource $resource + * @param AbstractDb $resourceCollection * @param array $data * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Framework\Model\Context $context, - \Magento\Framework\Registry $registry, - \Magento\ProductAlert\Helper\Data $productAlertData, - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository, - \Magento\Customer\Helper\View $customerHelper, - \Magento\Store\Model\App\Emulation $appEmulation, - \Magento\Framework\Mail\Template\TransportBuilder $transportBuilder, - \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, - \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, + Context $context, + Registry $registry, + Data $productAlertData, + ScopeConfigInterface $scopeConfig, + StoreManagerInterface $storeManager, + CustomerRepositoryInterface $customerRepository, + View $customerHelper, + Emulation $appEmulation, + TransportBuilder $transportBuilder, + AbstractResource $resource = null, + AbstractDb $resourceCollection = null, array $data = [] ) { $this->_productAlertData = $productAlertData; @@ -153,6 +181,7 @@ public function __construct( * Set model type * * @param string $type + * * @return void */ public function setType($type) @@ -173,7 +202,8 @@ public function getType() /** * Set website model * - * @param \Magento\Store\Model\Website $website + * @param Website $website + * * @return $this */ public function setWebsite(\Magento\Store\Model\Website $website) @@ -186,7 +216,9 @@ public function setWebsite(\Magento\Store\Model\Website $website) * Set website id * * @param int $websiteId + * * @return $this + * @throws LocalizedException */ public function setWebsiteId($websiteId) { @@ -198,7 +230,10 @@ public function setWebsiteId($websiteId) * Set customer by id * * @param int $customerId + * * @return $this + * @throws LocalizedException + * @throws NoSuchEntityException */ public function setCustomerId($customerId) { @@ -209,7 +244,8 @@ public function setCustomerId($customerId) /** * Set customer model * - * @param \Magento\Customer\Api\Data\CustomerInterface $customer + * @param CustomerInterface $customer + * * @return $this */ public function setCustomerData($customer) @@ -235,7 +271,8 @@ public function clean() /** * Add product (price change) to collection * - * @param \Magento\Catalog\Model\Product $product + * @param Product $product + * * @return $this */ public function addPriceProduct(\Magento\Catalog\Model\Product $product) @@ -247,7 +284,8 @@ public function addPriceProduct(\Magento\Catalog\Model\Product $product) /** * Add product (back in stock) to collection * - * @param \Magento\Catalog\Model\Product $product + * @param Product $product + * * @return $this */ public function addStockProduct(\Magento\Catalog\Model\Product $product) @@ -259,12 +297,13 @@ public function addStockProduct(\Magento\Catalog\Model\Product $product) /** * Retrieve price block * - * @return \Magento\ProductAlert\Block\Email\Price + * @return Price + * @throws LocalizedException */ protected function _getPriceBlock() { if ($this->_priceBlock === null) { - $this->_priceBlock = $this->_productAlertData->createBlock(\Magento\ProductAlert\Block\Email\Price::class); + $this->_priceBlock = $this->_productAlertData->createBlock(Price::class); } return $this->_priceBlock; } @@ -272,12 +311,13 @@ protected function _getPriceBlock() /** * Retrieve stock block * - * @return \Magento\ProductAlert\Block\Email\Stock + * @return Stock + * @throws LocalizedException */ protected function _getStockBlock() { if ($this->_stockBlock === null) { - $this->_stockBlock = $this->_productAlertData->createBlock(\Magento\ProductAlert\Block\Email\Stock::class); + $this->_stockBlock = $this->_productAlertData->createBlock(Stock::class); } return $this->_stockBlock; } @@ -286,110 +326,131 @@ protected function _getStockBlock() * Send customer email * * @return bool - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @throws MailException + * @throws NoSuchEntityException + * @throws LocalizedException */ public function send() { - if ($this->_website === null || $this->_customer === null) { - return false; - } - if ($this->_type == 'price' && count( - $this->_priceProducts - ) == 0 || $this->_type == 'stock' && count( - $this->_stockProducts - ) == 0 - ) { - return false; - } - if (!$this->_website->getDefaultGroup() || !$this->_website->getDefaultGroup()->getDefaultStore()) { + if ($this->_website === null || $this->_customer === null || !$this->isExistDefaultStore()) { return false; } - if ($this->_customer->getStoreId() > 0) { - $store = $this->_storeManager->getStore($this->_customer->getStoreId()); - } else { - $store = $this->_website->getDefaultStore(); - } - $storeId = $store->getId(); - - if ($this->_type == 'price' && !$this->_scopeConfig->getValue( - self::XML_PATH_EMAIL_PRICE_TEMPLATE, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $storeId - ) - ) { - return false; - } elseif ($this->_type == 'stock' && !$this->_scopeConfig->getValue( - self::XML_PATH_EMAIL_STOCK_TEMPLATE, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $storeId - ) - ) { + $products = $this->getProducts(); + $templateConfigPath = $this->getTemplateConfigPath(); + if (!in_array($this->_type, ['price', 'stock']) || count($products) === 0 || !$templateConfigPath) { return false; } - if ($this->_type != 'price' && $this->_type != 'stock') { - return false; - } + $storeId = $this->getStoreId() ?: (int) $this->_customer->getStoreId(); + $store = $this->getStore($storeId); $this->_appEmulation->startEnvironmentEmulation($storeId); - if ($this->_type == 'price') { - $this->_getPriceBlock()->setStore($store)->reset(); - foreach ($this->_priceProducts as $product) { - $product->setCustomerGroupId($this->_customer->getGroupId()); - $this->_getPriceBlock()->addProduct($product); - } - $block = $this->_getPriceBlock(); - $templateId = $this->_scopeConfig->getValue( - self::XML_PATH_EMAIL_PRICE_TEMPLATE, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $storeId - ); - } else { - $this->_getStockBlock()->setStore($store)->reset(); - foreach ($this->_stockProducts as $product) { - $product->setCustomerGroupId($this->_customer->getGroupId()); - $this->_getStockBlock()->addProduct($product); - } - $block = $this->_getStockBlock(); - $templateId = $this->_scopeConfig->getValue( - self::XML_PATH_EMAIL_STOCK_TEMPLATE, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $storeId - ); + $block = $this->getBlock(); + $block->setStore($store)->reset(); + + // Add products to the block + foreach ($products as $product) { + $product->setCustomerGroupId($this->_customer->getGroupId()); + $block->addProduct($product); } + $templateId = $this->_scopeConfig->getValue( + $templateConfigPath, + ScopeInterface::SCOPE_STORE, + $storeId + ); + $alertGrid = $this->_appState->emulateAreaCode( - \Magento\Framework\App\Area::AREA_FRONTEND, + Area::AREA_FRONTEND, [$block, 'toHtml'] ); $this->_appEmulation->stopEnvironmentEmulation(); - $transport = $this->_transportBuilder->setTemplateIdentifier( + $customerName = $this->_customerHelper->getCustomerName($this->_customer); + $this->_transportBuilder->setTemplateIdentifier( $templateId )->setTemplateOptions( - ['area' => \Magento\Framework\App\Area::AREA_FRONTEND, 'store' => $storeId] + ['area' => Area::AREA_FRONTEND, 'store' => $storeId] )->setTemplateVars( [ - 'customerName' => $this->_customerHelper->getCustomerName($this->_customer), + 'customerName' => $customerName, 'alertGrid' => $alertGrid, ] )->setFrom( $this->_scopeConfig->getValue( self::XML_PATH_EMAIL_IDENTITY, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $storeId ) )->addTo( $this->_customer->getEmail(), - $this->_customerHelper->getCustomerName($this->_customer) - )->getTransport(); + $customerName + )->getTransport()->sendMessage(); + + return true; + } + + /** + * Retrieve the store for the email + * + * @param int $storeId + * @return StoreInterface + * @throws NoSuchEntityException + */ + private function getStore(int $storeId): StoreInterface + { + return $this->_storeManager->getStore($storeId); + } + + /** + * Retrieve the block for the email based on type + * + * @return Price|Stock + * @throws LocalizedException + */ + private function getBlock(): AbstractEmail + { + return $this->_type === 'price' + ? $this->_getPriceBlock() + : $this->_getStockBlock(); + } - $transport->sendMessage(); + /** + * Retrieve the products for the email based on type + * + * @return array + */ + private function getProducts(): array + { + return $this->_type === 'price' + ? $this->_priceProducts + : $this->_stockProducts; + } + /** + * Retrieve template config path based on type + * + * @return string + */ + private function getTemplateConfigPath(): string + { + return $this->_type === 'price' + ? self::XML_PATH_EMAIL_PRICE_TEMPLATE + : self::XML_PATH_EMAIL_STOCK_TEMPLATE; + } + + /** + * Check if exists default store. + * + * @return bool + */ + private function isExistDefaultStore(): bool + { + if (!$this->_website->getDefaultGroup() || !$this->_website->getDefaultGroup()->getDefaultStore()) { + return false; + } return true; } } diff --git a/app/code/Magento/ProductAlert/Model/Observer.php b/app/code/Magento/ProductAlert/Model/Observer.php index 1870989f11dc..addc61d2f49a 100644 --- a/app/code/Magento/ProductAlert/Model/Observer.php +++ b/app/code/Magento/ProductAlert/Model/Observer.php @@ -218,6 +218,7 @@ protected function _processPrice(\Magento\ProductAlert\Model\Email $email) $previousCustomer = null; $email->setWebsite($website); foreach ($collection as $alert) { + $this->setAlertStoreId($alert, $email); try { if (!$previousCustomer || $previousCustomer->getId() != $alert->getCustomerId()) { $customer = $this->customerRepository->getById($alert->getCustomerId()); @@ -311,6 +312,7 @@ protected function _processStock(\Magento\ProductAlert\Model\Email $email) $previousCustomer = null; $email->setWebsite($website); foreach ($collection as $alert) { + $this->setAlertStoreId($alert, $email); try { if (!$previousCustomer || $previousCustomer->getId() != $alert->getCustomerId()) { $customer = $this->customerRepository->getById($alert->getCustomerId()); @@ -427,4 +429,21 @@ public function process() return $this; } + + /** + * Set alert store id. + * + * @param \Magento\ProductAlert\Model\Price|\Magento\ProductAlert\Model\Stock $alert + * @param Email $email + * @return Observer + */ + private function setAlertStoreId($alert, \Magento\ProductAlert\Model\Email $email) : Observer + { + $alertStoreId = $alert->getStoreId(); + if ($alertStoreId) { + $email->setStoreId((int)$alertStoreId); + } + + return $this; + } } diff --git a/app/code/Magento/ProductAlert/Model/Price.php b/app/code/Magento/ProductAlert/Model/Price.php index 0c12b7cfb489..ecdf4578838a 100644 --- a/app/code/Magento/ProductAlert/Model/Price.php +++ b/app/code/Magento/ProductAlert/Model/Price.php @@ -26,6 +26,8 @@ * @method \Magento\ProductAlert\Model\Price setSendCount(int $value) * @method int getStatus() * @method \Magento\ProductAlert\Model\Price setStatus(int $value) + * @method int getStoreId() + * @method \Magento\ProductAlert\Model\Stock setStoreId(int $value) * * @author Magento Core Team <core@magentocommerce.com> * @@ -60,6 +62,8 @@ public function __construct( } /** + * Create customer collection. + * * @return void */ protected function _construct() @@ -68,6 +72,8 @@ protected function _construct() } /** + * Create customer collection. + * * @return Collection */ public function getCustomerCollection() @@ -76,6 +82,8 @@ public function getCustomerCollection() } /** + * Load by param. + * * @return $this */ public function loadByParam() @@ -87,6 +95,8 @@ public function loadByParam() } /** + * Method for deleting customer from website. + * * @param int $customerId * @param int $websiteId * @return $this diff --git a/app/code/Magento/ProductAlert/Model/ResourceModel/AbstractResource.php b/app/code/Magento/ProductAlert/Model/ResourceModel/AbstractResource.php index 710ede8ecefa..c7b3d59138ec 100644 --- a/app/code/Magento/ProductAlert/Model/ResourceModel/AbstractResource.php +++ b/app/code/Magento/ProductAlert/Model/ResourceModel/AbstractResource.php @@ -5,6 +5,8 @@ */ namespace Magento\ProductAlert\Model\ResourceModel; +use Magento\Framework\Model\AbstractModel; + /** * Product alert for back in abstract resource model * @@ -15,13 +17,13 @@ abstract class AbstractResource extends \Magento\Framework\Model\ResourceModel\D /** * Retrieve alert row by object parameters * - * @param \Magento\Framework\Model\AbstractModel $object + * @param AbstractModel $object * @return array|false */ - protected function _getAlertRow(\Magento\Framework\Model\AbstractModel $object) + protected function _getAlertRow(AbstractModel $object) { $connection = $this->getConnection(); - if ($object->getCustomerId() && $object->getProductId() && $object->getWebsiteId()) { + if ($this->isExistAllBindIds($object)) { $select = $connection->select()->from( $this->getMainTable() )->where( @@ -30,24 +32,41 @@ protected function _getAlertRow(\Magento\Framework\Model\AbstractModel $object) 'product_id = :product_id' )->where( 'website_id = :website_id' + )->where( + 'store_id = :store_id' ); $bind = [ ':customer_id' => $object->getCustomerId(), ':product_id' => $object->getProductId(), ':website_id' => $object->getWebsiteId(), + ':store_id' => $object->getStoreId() ]; return $connection->fetchRow($select, $bind); } return false; } + /** + * Is exists all bind ids. + * + * @param AbstractModel $object + * @return bool + */ + private function isExistAllBindIds(AbstractModel $object): bool + { + return ($object->getCustomerId() + && $object->getProductId() + && $object->getWebsiteId() + && $object->getStoreId()); + } + /** * Load object data by parameters * - * @param \Magento\Framework\Model\AbstractModel $object + * @param AbstractModel $object * @return $this */ - public function loadByParam(\Magento\Framework\Model\AbstractModel $object) + public function loadByParam(AbstractModel $object) { $row = $this->_getAlertRow($object); if ($row) { @@ -59,13 +78,13 @@ public function loadByParam(\Magento\Framework\Model\AbstractModel $object) /** * Delete all customer alerts on website * - * @param \Magento\Framework\Model\AbstractModel $object + * @param AbstractModel $object * @param int $customerId * @param int $websiteId * @return $this * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function deleteCustomer(\Magento\Framework\Model\AbstractModel $object, $customerId, $websiteId = null) + public function deleteCustomer(AbstractModel $object, $customerId, $websiteId = null) { $connection = $this->getConnection(); $where = []; diff --git a/app/code/Magento/ProductAlert/Model/Stock.php b/app/code/Magento/ProductAlert/Model/Stock.php index 4d4dea5c2fe7..99ba95e63390 100644 --- a/app/code/Magento/ProductAlert/Model/Stock.php +++ b/app/code/Magento/ProductAlert/Model/Stock.php @@ -24,6 +24,8 @@ * @method \Magento\ProductAlert\Model\Stock setSendCount(int $value) * @method int getStatus() * @method \Magento\ProductAlert\Model\Stock setStatus(int $value) + * @method int getStoreId() + * @method \Magento\ProductAlert\Model\Stock setStoreId(int $value) * * @author Magento Core Team <core@magentocommerce.com> * @@ -58,6 +60,8 @@ public function __construct( } /** + * Class constructor. + * * @return void */ protected function _construct() @@ -66,6 +70,8 @@ protected function _construct() } /** + * Create customer collection. + * * @return Collection */ public function getCustomerCollection() @@ -74,6 +80,8 @@ public function getCustomerCollection() } /** + * Load by param. + * * @return $this */ public function loadByParam() @@ -85,6 +93,8 @@ public function loadByParam() } /** + * Method for deleting customer from website. + * * @param int $customerId * @param int $websiteId * @return $this diff --git a/app/code/Magento/ProductAlert/Test/Unit/Model/ObserverTest.php b/app/code/Magento/ProductAlert/Test/Unit/Model/ObserverTest.php index 9cd8546a0180..e3a2056a89ec 100644 --- a/app/code/Magento/ProductAlert/Test/Unit/Model/ObserverTest.php +++ b/app/code/Magento/ProductAlert/Test/Unit/Model/ObserverTest.php @@ -5,6 +5,7 @@ */ namespace Magento\ProductAlert\Test\Unit\Model; +use Magento\Customer\Api\Data\CustomerInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\ProductAlert\Model\ProductSalability; @@ -115,6 +116,9 @@ class ObserverTest extends \PHPUnit\Framework\TestCase */ private $productSalabilityMock; + /** + * @return void + */ protected function setUp() { $this->objectManagerMock = $this->getMockBuilder(\Magento\Framework\ObjectManagerInterface::class) @@ -296,8 +300,8 @@ public function testProcessPriceEmailThrowsException() ->method('setCustomerOrder') ->willReturn(new \ArrayIterator($items)); - $customer = new \Magento\Framework\DataObject(['group_id' => $id]); - $this->customerRepositoryMock->expects($this->once())->method('getById')->willReturn($customer); + $customerMock = $this->getMockForAbstractClass(CustomerInterface::class); + $this->customerRepositoryMock->expects($this->once())->method('getById')->willReturn($customerMock); $this->productMock->expects($this->once())->method('setCustomerGroupId')->willReturnSelf(); $this->productMock->expects($this->once())->method('getFinalPrice')->willReturn('655.99'); @@ -368,7 +372,6 @@ public function testProcessStockCustomerRepositoryThrowsException() */ public function testProcessStockEmailThrowsException() { - $id = 1; $this->scopeConfigMock->expects($this->any())->method('isSetFlag')->willReturn(false); $this->emailFactoryMock->expects($this->once())->method('create')->willReturn($this->emailMock); @@ -395,8 +398,8 @@ public function testProcessStockEmailThrowsException() ->method('setCustomerOrder') ->willReturn(new \ArrayIterator($items)); - $customer = new \Magento\Framework\DataObject(['group_id' => $id]); - $this->customerRepositoryMock->expects($this->once())->method('getById')->willReturn($customer); + $customerMock = $this->getMockForAbstractClass(CustomerInterface::class); + $this->customerRepositoryMock->expects($this->once())->method('getById')->willReturn($customerMock); $this->productMock->expects($this->once())->method('setCustomerGroupId')->willReturnSelf(); $this->productSalabilityMock->expects($this->once())->method('isSalable')->willReturn(false); diff --git a/app/code/Magento/ProductAlert/etc/db_schema.xml b/app/code/Magento/ProductAlert/etc/db_schema.xml index ddf8be8a37e9..820a8029e2d9 100644 --- a/app/code/Magento/ProductAlert/etc/db_schema.xml +++ b/app/code/Magento/ProductAlert/etc/db_schema.xml @@ -18,6 +18,8 @@ comment="Price amount"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Website id"/> + <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" + default="0" comment="Store id"/> <column xsi:type="timestamp" name="add_date" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Product alert add date"/> <column xsi:type="timestamp" name="last_send_date" on_update="false" nullable="true" @@ -26,27 +28,33 @@ default="0" comment="Product alert send count"/> <column xsi:type="smallint" name="status" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Product alert status"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="alert_price_id"/> </constraint> - <constraint xsi:type="foreign" name="PRODUCT_ALERT_PRICE_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="PRODUCT_ALERT_PRICE_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID" table="product_alert_price" column="customer_id" referenceTable="customer_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="PRODUCT_ALERT_PRICE_PRODUCT_ID_CATALOG_PRODUCT_ENTITY_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="PRODUCT_ALERT_PRICE_PRODUCT_ID_CATALOG_PRODUCT_ENTITY_ENTITY_ID" table="product_alert_price" column="product_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="PRODUCT_ALERT_PRICE_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID" + <constraint xsi:type="foreign" referenceId="PRODUCT_ALERT_PRICE_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID" table="product_alert_price" column="website_id" referenceTable="store_website" referenceColumn="website_id" onDelete="CASCADE"/> - <index name="PRODUCT_ALERT_PRICE_CUSTOMER_ID" indexType="btree"> + <constraint xsi:type="foreign" referenceId="PRODUCT_ALERT_PRICE_STORE_ID_STORE_STORE_ID" + table="product_alert_stock" column="store_id" referenceTable="store" + referenceColumn="store_id" onDelete="CASCADE"/> + <index referenceId="PRODUCT_ALERT_PRICE_CUSTOMER_ID" indexType="btree"> <column name="customer_id"/> </index> - <index name="PRODUCT_ALERT_PRICE_PRODUCT_ID" indexType="btree"> + <index referenceId="PRODUCT_ALERT_PRICE_PRODUCT_ID" indexType="btree"> <column name="product_id"/> </index> - <index name="PRODUCT_ALERT_PRICE_WEBSITE_ID" indexType="btree"> + <index referenceId="PRODUCT_ALERT_PRICE_WEBSITE_ID" indexType="btree"> <column name="website_id"/> </index> + <index referenceId="PRODUCT_ALERT_PRICE_STORE_ID" indexType="btree"> + <column name="store_id"/> + </index> </table> <table name="product_alert_stock" resource="default" engine="innodb" comment="Product Alert Stock"> <column xsi:type="int" name="alert_stock_id" padding="10" unsigned="true" nullable="false" identity="true" @@ -57,6 +65,8 @@ default="0" comment="Product id"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Website id"/> + <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" + default="0" comment="Store id"/> <column xsi:type="timestamp" name="add_date" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Product alert add date"/> <column xsi:type="timestamp" name="send_date" on_update="false" nullable="true" @@ -65,26 +75,32 @@ default="0" comment="Send Count"/> <column xsi:type="smallint" name="status" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Product alert status"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="alert_stock_id"/> </constraint> - <constraint xsi:type="foreign" name="PRODUCT_ALERT_STOCK_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID" + <constraint xsi:type="foreign" referenceId="PRODUCT_ALERT_STOCK_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID" table="product_alert_stock" column="website_id" referenceTable="store_website" referenceColumn="website_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="PRODUCT_ALERT_STOCK_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="PRODUCT_ALERT_STOCK_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID" table="product_alert_stock" column="customer_id" referenceTable="customer_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="PRODUCT_ALERT_STOCK_PRODUCT_ID_CATALOG_PRODUCT_ENTITY_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="PRODUCT_ALERT_STOCK_PRODUCT_ID_CATALOG_PRODUCT_ENTITY_ENTITY_ID" table="product_alert_stock" column="product_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <index name="PRODUCT_ALERT_STOCK_CUSTOMER_ID" indexType="btree"> + <constraint xsi:type="foreign" referenceId="PRODUCT_ALERT_STOCK_STORE_ID_STORE_STORE_ID" + table="product_alert_stock" column="store_id" referenceTable="store" + referenceColumn="store_id" onDelete="CASCADE"/> + <index referenceId="PRODUCT_ALERT_STOCK_CUSTOMER_ID" indexType="btree"> <column name="customer_id"/> </index> - <index name="PRODUCT_ALERT_STOCK_PRODUCT_ID" indexType="btree"> + <index referenceId="PRODUCT_ALERT_STOCK_PRODUCT_ID" indexType="btree"> <column name="product_id"/> </index> - <index name="PRODUCT_ALERT_STOCK_WEBSITE_ID" indexType="btree"> + <index referenceId="PRODUCT_ALERT_STOCK_WEBSITE_ID" indexType="btree"> <column name="website_id"/> </index> + <index referenceId="PRODUCT_ALERT_STOCK_STORE_ID" indexType="btree"> + <column name="store_id"/> + </index> </table> </schema> diff --git a/app/code/Magento/ProductAlert/etc/db_schema_whitelist.json b/app/code/Magento/ProductAlert/etc/db_schema_whitelist.json index 266d00e04c5b..94c9d07c8501 100644 --- a/app/code/Magento/ProductAlert/etc/db_schema_whitelist.json +++ b/app/code/Magento/ProductAlert/etc/db_schema_whitelist.json @@ -1,6 +1,7 @@ { "product_alert_price": { "column": { + "store_id": true, "alert_price_id": true, "customer_id": true, "product_id": true, @@ -12,11 +13,13 @@ "status": true }, "index": { + "PRODUCT_ALERT_PRICE_STORE_ID": true, "PRODUCT_ALERT_PRICE_CUSTOMER_ID": true, "PRODUCT_ALERT_PRICE_PRODUCT_ID": true, "PRODUCT_ALERT_PRICE_WEBSITE_ID": true }, "constraint": { + "PRODUCT_ALERT_PRICE_STORE_ID_STORE_STORE_ID": true, "PRIMARY": true, "PRODUCT_ALERT_PRICE_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID": true, "PRODUCT_ALERT_PRICE_PRODUCT_ID_CATALOG_PRODUCT_ENTITY_ENTITY_ID": true, @@ -26,6 +29,7 @@ }, "product_alert_stock": { "column": { + "store_id": true, "alert_stock_id": true, "customer_id": true, "product_id": true, @@ -36,11 +40,13 @@ "status": true }, "index": { + "PRODUCT_ALERT_STOCK_STORE_ID": true, "PRODUCT_ALERT_STOCK_CUSTOMER_ID": true, "PRODUCT_ALERT_STOCK_PRODUCT_ID": true, "PRODUCT_ALERT_STOCK_WEBSITE_ID": true }, "constraint": { + "PRODUCT_ALERT_STOCK_STORE_ID_STORE_STORE_ID": true, "PRIMARY": true, "PRODUCT_ALERT_STOCK_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID": true, "PRODUCT_ALERT_STOCK_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID": true, @@ -48,4 +54,4 @@ "PRODUCT_ALERT_STOCK_PRODUCT_ID_SEQUENCE_PRODUCT_SEQUENCE_VALUE": true } } -} \ No newline at end of file +} diff --git a/app/code/Magento/ProductVideo/Controller/Adminhtml/Product/Gallery/RetrieveImage.php b/app/code/Magento/ProductVideo/Controller/Adminhtml/Product/Gallery/RetrieveImage.php index 23721cb4b165..914b5fa27171 100644 --- a/app/code/Magento/ProductVideo/Controller/Adminhtml/Product/Gallery/RetrieveImage.php +++ b/app/code/Magento/ProductVideo/Controller/Adminhtml/Product/Gallery/RetrieveImage.php @@ -6,6 +6,7 @@ namespace Magento\ProductVideo\Controller\Adminhtml\Product\Gallery; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\File\Uploader; @@ -13,7 +14,7 @@ /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class RetrieveImage extends \Magento\Backend\App\Action +class RetrieveImage extends \Magento\Backend\App\Action implements HttpPostActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/ProductVideo/Model/Plugin/Catalog/Product/Gallery/CreateHandler.php b/app/code/Magento/ProductVideo/Model/Plugin/Catalog/Product/Gallery/CreateHandler.php index ce1493b349a8..42407ca6be0b 100644 --- a/app/code/Magento/ProductVideo/Model/Plugin/Catalog/Product/Gallery/CreateHandler.php +++ b/app/code/Magento/ProductVideo/Model/Plugin/Catalog/Product/Gallery/CreateHandler.php @@ -19,6 +19,8 @@ class CreateHandler extends AbstractHandler const ADDITIONAL_STORE_DATA_KEY = 'additional_store_data'; /** + * Execute before Plugin + * * @param \Magento\Catalog\Model\Product\Gallery\CreateHandler $mediaGalleryCreateHandler * @param \Magento\Catalog\Model\Product $product * @param array $arguments @@ -44,6 +46,8 @@ public function beforeExecute( } /** + * Execute plugin + * * @param \Magento\Catalog\Model\Product\Gallery\CreateHandler $mediaGalleryCreateHandler * @param \Magento\Catalog\Model\Product $product * @return \Magento\Catalog\Model\Product @@ -58,6 +62,9 @@ public function afterExecute( ); if (!empty($mediaCollection)) { + if ($product->getIsDuplicate() === true) { + $mediaCollection = $this->makeAllNewVideos($product->getId(), $mediaCollection); + } $newVideoCollection = $this->collectNewVideos($mediaCollection); $this->saveVideoData($newVideoCollection, 0); @@ -70,6 +77,8 @@ public function afterExecute( } /** + * Saves video data + * * @param array $videoDataCollection * @param int $storeId * @return void @@ -83,6 +92,8 @@ protected function saveVideoData(array $videoDataCollection, $storeId) } /** + * Saves additioanal video data + * * @param array $videoDataCollection * @return void */ @@ -99,6 +110,8 @@ protected function saveAdditionalStoreData(array $videoDataCollection) } /** + * Saves video data + * * @param array $item * @return void */ @@ -111,6 +124,8 @@ protected function saveVideoValuesItem(array $item) } /** + * Excludes current store data + * * @param array $mediaCollection * @param int $currentStoreId * @return array @@ -126,6 +141,8 @@ function ($item) use ($currentStoreId) { } /** + * Prepare video data for saving + * * @param array $rowData * @return array */ @@ -143,6 +160,8 @@ protected function prepareVideoRowDataForSave(array $rowData) } /** + * Loads video data + * * @param array $mediaCollection * @param int $excludedStore * @return array @@ -165,6 +184,8 @@ protected function loadStoreViewVideoData(array $mediaCollection, $excludedStore } /** + * Collect video data + * * @param array $mediaCollection * @return array */ @@ -182,6 +203,8 @@ protected function collectVideoData(array $mediaCollection) } /** + * Extract video data + * * @param array $rowData * @return array */ @@ -194,6 +217,8 @@ protected function extractVideoDataFromRowData(array $rowData) } /** + * Collect items for additional data adding + * * @param array $mediaCollection * @return array */ @@ -209,6 +234,8 @@ protected function collectVideoEntriesIdsToAdditionalLoad(array $mediaCollection } /** + * Add additional data + * * @param array $mediaCollection * @param array $data * @return array @@ -229,6 +256,8 @@ protected function addAdditionalStoreData(array $mediaCollection, array $data): } /** + * Creates additional video data + * * @param array $storeData * @param int $valueId * @return array @@ -247,6 +276,8 @@ protected function createAdditionalStoreDataCollection(array $storeData, $valueI } /** + * Collect new videos + * * @param array $mediaCollection * @return array */ @@ -262,6 +293,8 @@ private function collectNewVideos(array $mediaCollection): array } /** + * Checks if gallery item is video + * * @param array $item * @return bool */ @@ -273,6 +306,8 @@ private function isVideoItem(array $item): bool } /** + * Checks if video is new + * * @param array $item * @return bool */ @@ -282,4 +317,23 @@ private function isNewVideo(array $item): bool || empty($item['video_url_default']) || empty($item['video_title_default']); } + + /** + * Mark all videos as new + * + * @param int $entityId + * @param array $mediaCollection + * @return array + */ + private function makeAllNewVideos($entityId, array $mediaCollection): array + { + foreach ($mediaCollection as $key => $video) { + if ($this->isVideoItem($video)) { + unset($video['video_url_default'], $video['video_title_default']); + $video['entity_id'] = $entityId; + $mediaCollection[$key] = $video; + } + } + return $mediaCollection; + } } diff --git a/app/code/Magento/ProductVideo/Model/Plugin/ExternalVideoResourceBackend.php b/app/code/Magento/ProductVideo/Model/Plugin/ExternalVideoResourceBackend.php index dc64f03a42d1..04a3d868d14a 100644 --- a/app/code/Magento/ProductVideo/Model/Plugin/ExternalVideoResourceBackend.php +++ b/app/code/Magento/ProductVideo/Model/Plugin/ExternalVideoResourceBackend.php @@ -27,6 +27,8 @@ public function __construct(\Magento\ProductVideo\Model\ResourceModel\Video $vid } /** + * Plugin for after duplicate action + * * @param Gallery $originalResourceModel * @param array $valueIdMap * @return array @@ -45,6 +47,8 @@ public function afterDuplicate(Gallery $originalResourceModel, array $valueIdMap } /** + * Plugin for after create batch base select action + * * @param Gallery $originalResourceModel * @param Select $select * @return Select @@ -60,13 +64,7 @@ public function afterCreateBatchBaseSelect(Gallery $originalResourceModel, Selec 'value.store_id = value_video.store_id', ] ), - [ - 'video_provider' => 'provider', - 'video_url' => 'url', - 'video_title' => 'title', - 'video_description' => 'description', - 'video_metadata' => 'metadata' - ] + [] )->joinLeft( [ 'default_value_video' => $originalResourceModel->getTable( @@ -80,14 +78,24 @@ public function afterCreateBatchBaseSelect(Gallery $originalResourceModel, Selec 'default_value.store_id = default_value_video.store_id', ] ), - [ - 'video_provider_default' => 'provider', - 'video_url_default' => 'url', - 'video_title_default' => 'title', - 'video_description_default' => 'description', - 'video_metadata_default' => 'metadata', - ] - ); + [] + )->columns([ + 'video_provider' => $originalResourceModel->getConnection() + ->getIfNullSql('`value_video`.`provider`', '`default_value_video`.`provider`'), + 'video_url' => $originalResourceModel->getConnection() + ->getIfNullSql('`value_video`.`url`', '`default_value_video`.`url`'), + 'video_title' => $originalResourceModel->getConnection() + ->getIfNullSql('`value_video`.`title`', '`default_value_video`.`title`'), + 'video_description' => $originalResourceModel->getConnection() + ->getIfNullSql('`value_video`.`description`', '`default_value_video`.`description`'), + 'video_metadata' => $originalResourceModel->getConnection() + ->getIfNullSql('`value_video`.`metadata`', '`default_value_video`.`metadata`'), + 'video_provider_default' => 'default_value_video.provider', + 'video_url_default' => 'default_value_video.url', + 'video_title_default' => 'default_value_video.title', + 'video_description_default' => 'default_value_video.description', + 'video_metadata_default' => 'default_value_video.metadata', + ]); return $select; } diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/ProductVideo/Test/Mftf/Section/StorefrontProductInfoMainSection.xml index 564122f71b9f..c8e1ebcf12d9 100644 --- a/app/code/Magento/ProductVideo/Test/Mftf/Section/StorefrontProductInfoMainSection.xml +++ b/app/code/Magento/ProductVideo/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -10,5 +10,9 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontProductInfoMainSection"> <element name="productVideo" type="text" selector="//*[@class='product-video' and @data-type='{{videoType}}']" parameterized="true"/> + <element name="clickInVideo" type="video" selector="//*[@class='fotorama__stage__shaft']"/> + <element name="videoPausedMode" type="video" selector="//*[contains(@class, 'paused-mode')]"/> + <element name="videoPlayedMode" type="video" selector="//*[contains(@class,'playing-mode')]"/> + <element name="frameVideo" type="video" selector="widget2"/> </section> </sections> diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminAddDefaultVideoSimpleProductTest.xml b/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminAddDefaultVideoSimpleProductTest.xml index bd7cc0cdf5b4..2b5f87f78d5e 100644 --- a/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminAddDefaultVideoSimpleProductTest.xml +++ b/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminAddDefaultVideoSimpleProductTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminAddDefaultVideoSimpleProductTest"> <annotations> <group value="ProductVideo"/> diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminRemoveDefaultVideoSimpleProductTest.xml b/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminRemoveDefaultVideoSimpleProductTest.xml index f5a7886fed45..d4da0ffa5445 100644 --- a/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminRemoveDefaultVideoSimpleProductTest.xml +++ b/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminRemoveDefaultVideoSimpleProductTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminRemoveDefaultVideoSimpleProductTest"> <annotations> <group value="ProductVideo"/> diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Test/YoutubeVideoWindowOnProductPageTest.xml b/app/code/Magento/ProductVideo/Test/Mftf/Test/YoutubeVideoWindowOnProductPageTest.xml new file mode 100644 index 000000000000..7249a4223503 --- /dev/null +++ b/app/code/Magento/ProductVideo/Test/Mftf/Test/YoutubeVideoWindowOnProductPageTest.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="YoutubeVideoWindowOnProductPageTest"> + <annotations> + <features value="ProductVideo"/> + <stories value="MAGETWO-91707: [Sigma Beauty]Cannot pause Youtube video in IE 11"/> + <testCaseId value="MAGETWO-95254"/> + <title value="Youtube video window on the product page"/> + <description value="Check Youtube video window on the product page"/> + <severity value="MAJOR"/> + <group value="ProductVideo"/> + </annotations> + + <before> + <!--Log In--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create category--> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <!--Create product--> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Set product video Youtube api key configuration --> + <createData entity="ProductVideoYoutubeApiKeyConfig" stepKey="setStoreConfig" after="loginAsAdmin"/> + </before> + + <!--Open simple product--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex"/> + <waitForPageLoad stepKey="wait1"/> + <actionGroup ref="resetProductGridToDefaultView" stepKey="resetProductGrid"/> + <actionGroup ref="filterProductGridBySku" stepKey="filterProductGridBySku"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <actionGroup ref="openProducForEditByClickingRowXColumnYInProductGrid" stepKey="openFirstProductForEdit"/> + + <!-- Add product video --> + <actionGroup ref="addProductVideo" stepKey="addProductVideo" after="openFirstProductForEdit"/> + <!-- Assert product video in admin product form --> + <actionGroup ref="assertProductVideoAdminProductPage" stepKey="assertProductVideoAdminProductPage" after="addProductVideo"/> + + <!-- Save the product --> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveFirstProduct"/> + <waitForPageLoad stepKey="waitForFirstProductSaved"/> + + <!-- Assert product video in storefront product page --> + <amOnPage url="$$createProduct.name$$.html" stepKey="goToStorefrontCategoryPage"/> + <waitForPageLoad stepKey="waitForStorefrontPageLoaded"/> + <actionGroup ref="assertProductVideoStorefrontProductPage" stepKey="assertProductVideoStorefrontProductPage" after="waitForStorefrontPageLoaded"/> + + <!--Click Play video button--> + <click stepKey="clickToPlayVideo" selector="{{StorefrontProductInfoMainSection.clickInVideo}}"/> + <wait stepKey="waitFiveSecondToPlayVideo" time="5"/> + <switchToIFrame selector="{{StorefrontProductInfoMainSection.frameVideo}}" stepKey="switchToFrame"/> + <waitForElementVisible selector="{{StorefrontProductInfoMainSection.videoPlayedMode}}" stepKey="waitForVideoPlayed"/> + <seeElement selector="{{StorefrontProductInfoMainSection.videoPlayedMode}}" stepKey="AssertVideoIsPlayed"/> + <switchToIFrame stepKey="switchBack1"/> + + <!--Click Pause button--> + <click stepKey="clickToStopVideo" selector="{{StorefrontProductInfoMainSection.clickInVideo}}"/> + <wait stepKey="waitFiveSecondToStopVideo" time="5"/> + <switchToIFrame selector="{{StorefrontProductInfoMainSection.frameVideo}}" stepKey="switchToFrame2"/> + <waitForElementVisible selector="{{StorefrontProductInfoMainSection.videoPausedMode}}" stepKey="waitForVideoPaused"/> + <seeElement selector="{{StorefrontProductInfoMainSection.videoPausedMode}}" stepKey="AssertVideoIsPaused"/> + <switchToIFrame stepKey="switchBack2"/> + + <!--Click Play video button again. Make sure that Video continued playing--> + <click stepKey="clickAgainToPlayVideo" selector="{{StorefrontProductInfoMainSection.clickInVideo}}"/> + <wait stepKey="waitAgainFiveSecondToPlayVideo" time="5"/> + <switchToIFrame selector="{{StorefrontProductInfoMainSection.frameVideo}}" stepKey="switchToFrame3"/> + <waitForElementVisible selector="{{StorefrontProductInfoMainSection.videoPlayedMode}}" stepKey="waitForVideoPlayedAgain"/> + <seeElement selector="{{StorefrontProductInfoMainSection.videoPlayedMode}}" stepKey="AssertVideoIsPlayedAgain"/> + <switchToIFrame stepKey="switchBack3"/> + + <after> + <deleteData createDataKey="createProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategoryFirst"/> + <!-- Set product video configuration to default --> + <createData entity="DefaultProductVideoConfig" stepKey="setStoreDefaultConfig" before="logout"/> + <!--Log Out--> + <actionGroup ref="logout" stepKey="logout"/> + </after> + </test> +</tests> + diff --git a/app/code/Magento/ProductVideo/etc/db_schema.xml b/app/code/Magento/ProductVideo/etc/db_schema.xml index ceaf4d7fbd85..bfe087d9a576 100644 --- a/app/code/Magento/ProductVideo/etc/db_schema.xml +++ b/app/code/Magento/ProductVideo/etc/db_schema.xml @@ -18,14 +18,14 @@ <column xsi:type="varchar" name="title" nullable="true" length="255" comment="Title"/> <column xsi:type="text" name="description" nullable="true" comment="Page Meta Description"/> <column xsi:type="text" name="metadata" nullable="true" comment="Video meta data"/> - <constraint xsi:type="foreign" name="FK_6FDF205946906B0E653E60AA769899F8" + <constraint xsi:type="foreign" referenceId="FK_6FDF205946906B0E653E60AA769899F8" table="catalog_product_entity_media_gallery_value_video" column="value_id" referenceTable="catalog_product_entity_media_gallery" referenceColumn="value_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="CAT_PRD_ENTT_MDA_GLR_VAL_VIDEO_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_MDA_GLR_VAL_VIDEO_STORE_ID_STORE_STORE_ID" table="catalog_product_entity_media_gallery_value_video" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="CAT_PRD_ENTT_MDA_GLR_VAL_VIDEO_VAL_ID_STORE_ID"> + <constraint xsi:type="unique" referenceId="CAT_PRD_ENTT_MDA_GLR_VAL_VIDEO_VAL_ID_STORE_ID"> <column name="value_id"/> <column name="store_id"/> </constraint> diff --git a/app/code/Magento/ProductVideo/i18n/de_DE.csv b/app/code/Magento/ProductVideo/i18n/de_DE.csv deleted file mode 100644 index 704731739699..000000000000 --- a/app/code/Magento/ProductVideo/i18n/de_DE.csv +++ /dev/null @@ -1,9 +0,0 @@ -"Add video","Add video" -"New Video","New Video" -"Product Video","Product Video" -"YouTube API key","YouTube API key" -"You have not entered youtube API key. No information about youtube video will be retrieved.","You have not entered youtube API key. No information about youtube video will be retrieved." -"Url","Url" -"Preview Image","Preview Image" -"Get Video Information","Get Video Information" -"Youtube or Vimeo supported","Youtube or Vimeo supported" diff --git a/app/code/Magento/ProductVideo/i18n/en_US.csv b/app/code/Magento/ProductVideo/i18n/en_US.csv index 2d226c6daefa..debcab151cc9 100644 --- a/app/code/Magento/ProductVideo/i18n/en_US.csv +++ b/app/code/Magento/ProductVideo/i18n/en_US.csv @@ -40,3 +40,4 @@ Delete,Delete "Autostart base video","Autostart base video" "Show related video","Show related video" "Auto restart video","Auto restart video" +"Delete image in all store views","Delete image in all store views" diff --git a/app/code/Magento/ProductVideo/i18n/es_ES.csv b/app/code/Magento/ProductVideo/i18n/es_ES.csv deleted file mode 100644 index 704731739699..000000000000 --- a/app/code/Magento/ProductVideo/i18n/es_ES.csv +++ /dev/null @@ -1,9 +0,0 @@ -"Add video","Add video" -"New Video","New Video" -"Product Video","Product Video" -"YouTube API key","YouTube API key" -"You have not entered youtube API key. No information about youtube video will be retrieved.","You have not entered youtube API key. No information about youtube video will be retrieved." -"Url","Url" -"Preview Image","Preview Image" -"Get Video Information","Get Video Information" -"Youtube or Vimeo supported","Youtube or Vimeo supported" diff --git a/app/code/Magento/ProductVideo/i18n/fr_FR.csv b/app/code/Magento/ProductVideo/i18n/fr_FR.csv deleted file mode 100644 index 704731739699..000000000000 --- a/app/code/Magento/ProductVideo/i18n/fr_FR.csv +++ /dev/null @@ -1,9 +0,0 @@ -"Add video","Add video" -"New Video","New Video" -"Product Video","Product Video" -"YouTube API key","YouTube API key" -"You have not entered youtube API key. No information about youtube video will be retrieved.","You have not entered youtube API key. No information about youtube video will be retrieved." -"Url","Url" -"Preview Image","Preview Image" -"Get Video Information","Get Video Information" -"Youtube or Vimeo supported","Youtube or Vimeo supported" diff --git a/app/code/Magento/ProductVideo/i18n/nl_NL.csv b/app/code/Magento/ProductVideo/i18n/nl_NL.csv deleted file mode 100644 index 704731739699..000000000000 --- a/app/code/Magento/ProductVideo/i18n/nl_NL.csv +++ /dev/null @@ -1,9 +0,0 @@ -"Add video","Add video" -"New Video","New Video" -"Product Video","Product Video" -"YouTube API key","YouTube API key" -"You have not entered youtube API key. No information about youtube video will be retrieved.","You have not entered youtube API key. No information about youtube video will be retrieved." -"Url","Url" -"Preview Image","Preview Image" -"Get Video Information","Get Video Information" -"Youtube or Vimeo supported","Youtube or Vimeo supported" diff --git a/app/code/Magento/ProductVideo/i18n/pt_BR.csv b/app/code/Magento/ProductVideo/i18n/pt_BR.csv deleted file mode 100644 index 704731739699..000000000000 --- a/app/code/Magento/ProductVideo/i18n/pt_BR.csv +++ /dev/null @@ -1,9 +0,0 @@ -"Add video","Add video" -"New Video","New Video" -"Product Video","Product Video" -"YouTube API key","YouTube API key" -"You have not entered youtube API key. No information about youtube video will be retrieved.","You have not entered youtube API key. No information about youtube video will be retrieved." -"Url","Url" -"Preview Image","Preview Image" -"Get Video Information","Get Video Information" -"Youtube or Vimeo supported","Youtube or Vimeo supported" diff --git a/app/code/Magento/ProductVideo/i18n/zh_Hans_CN.csv b/app/code/Magento/ProductVideo/i18n/zh_Hans_CN.csv deleted file mode 100644 index 704731739699..000000000000 --- a/app/code/Magento/ProductVideo/i18n/zh_Hans_CN.csv +++ /dev/null @@ -1,9 +0,0 @@ -"Add video","Add video" -"New Video","New Video" -"Product Video","Product Video" -"YouTube API key","YouTube API key" -"You have not entered youtube API key. No information about youtube video will be retrieved.","You have not entered youtube API key. No information about youtube video will be retrieved." -"Url","Url" -"Preview Image","Preview Image" -"Get Video Information","Get Video Information" -"Youtube or Vimeo supported","Youtube or Vimeo supported" diff --git a/app/code/Magento/ProductVideo/view/adminhtml/layout/catalog_product_new.xml b/app/code/Magento/ProductVideo/view/adminhtml/layout/catalog_product_new.xml index f5a22c50e6d0..63bd5321ad30 100644 --- a/app/code/Magento/ProductVideo/view/adminhtml/layout/catalog_product_new.xml +++ b/app/code/Magento/ProductVideo/view/adminhtml/layout/catalog_product_new.xml @@ -6,6 +6,9 @@ */ --> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <head> + <css src="Magento_ProductVideo::css/gallery-delete-tooltip.css"/> + </head> <body> <referenceContainer name="content"> diff --git a/app/code/Magento/ProductVideo/view/adminhtml/templates/helper/gallery.phtml b/app/code/Magento/ProductVideo/view/adminhtml/templates/helper/gallery.phtml index f0ae057bc724..6dff53211892 100755 --- a/app/code/Magento/ProductVideo/view/adminhtml/templates/helper/gallery.phtml +++ b/app/code/Magento/ProductVideo/view/adminhtml/templates/helper/gallery.phtml @@ -34,7 +34,7 @@ $elementToggleCode = $element->getToggleCode() ? $element->getToggleCode() : 'to class="gallery" data-mage-init='{"openVideoModal":{}}' data-parent-component="<?= $block->escapeHtml($block->getData('config/parentComponent')) ?>" - data-images="<?= $block->escapeHtml($block->getImagesJson()) ?>" + data-images="<?= $block->escapeHtmlAttr($block->getImagesJson()) ?>" data-types="<?= $block->escapeHtml( $this->helper('Magento\Framework\Json\Helper\Data')->jsonEncode($block->getImageTypes()) ) ?>" @@ -140,30 +140,37 @@ $elementToggleCode = $element->getToggleCode() ? $element->getToggleCode() : 'to alt="<%- data.label %>"/> <div class="actions"> - <button type="button" - class="action-remove" - data-role="delete-button" - title="<% if (data.media_type == 'external-video') {%> + <div class="tooltip"> + <span class="delete-tooltiptext"> + <?= $block->escapeHtml( + __('Delete image in all store views') + ); ?> + </span> + <button type="button" + class="action-remove" + data-role="delete-button" + title="<% if (data.media_type == 'external-video') {%> + <?= $block->escapeHtml( + __('Delete video') + ); ?> + <%} else {%> + <?= $block->escapeHtml( + __('Delete image') + ); ?> + <%}%>"> + <span> + <% if (data.media_type == 'external-video') { %> <?= $block->escapeHtml( __('Delete video') ); ?> - <%} else {%> + <% } else {%> <?= $block->escapeHtml( __('Delete image') ); ?> - <%}%>"> - <span> - <% if (data.media_type == 'external-video') { %> - <?= $block->escapeHtml( - __('Delete video') - ); ?> - <% } else {%> - <?= $block->escapeHtml( - __('Delete image') - ); ?> - <%} %> - </span> - </button> + <%} %> + </span> + </button> + </div> <div class="draggable-handle"></div> </div> <div class="image-fade"><span><?= $block->escapeHtml( @@ -329,4 +336,4 @@ $elementToggleCode = $element->getToggleCode() ? $element->getToggleCode() : 'to </div> <script> jQuery('body').trigger('contentUpdated'); -</script> +</script> \ No newline at end of file diff --git a/app/code/Magento/ProductVideo/view/adminhtml/web/css/gallery-delete-tooltip.css b/app/code/Magento/ProductVideo/view/adminhtml/web/css/gallery-delete-tooltip.css new file mode 100644 index 000000000000..ad779c3f29bf --- /dev/null +++ b/app/code/Magento/ProductVideo/view/adminhtml/web/css/gallery-delete-tooltip.css @@ -0,0 +1,20 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +.gallery .tooltip .delete-tooltiptext { + visibility: hidden; + width: 112px; + background-color: #373330; + color: #F7F3EB; + text-align: center; + padding: 5px 0; + position: absolute; + z-index: 1; + left: 30px; + top: 91px; +} + +.gallery .tooltip:hover .delete-tooltiptext { + visibility: visible; +} \ No newline at end of file diff --git a/app/code/Magento/ProductVideo/view/adminhtml/web/js/new-video-dialog.js b/app/code/Magento/ProductVideo/view/adminhtml/web/js/new-video-dialog.js index 287c88e8a796..e9b234c5f116 100644 --- a/app/code/Magento/ProductVideo/view/adminhtml/web/js/new-video-dialog.js +++ b/app/code/Magento/ProductVideo/view/adminhtml/web/js/new-video-dialog.js @@ -87,6 +87,9 @@ define([ * @private */ _doUpdate: function () { + var uploaderLinkUrl, + uploaderLink; + this.reset(); this.element.find(this.options.container).append( '<div class="' + @@ -105,20 +108,15 @@ define([ this.element.find(this.options.metaData.DOM.duration).text(this.options.metaData.data.duration); if (this.options.videoProvider === 'youtube') { - this.element.find(this.options.metaData.DOM.uploader).html( - '<a href="https://youtube.com/channel/' + - this.options.metaData.data.uploaderUrl + - '" target="_blank">' + - this.options.metaData.data.uploader + - '</a>' - ); + uploaderLinkUrl = 'https://youtube.com/channel/' + this.options.metaData.data.uploaderUrl; } else if (this.options.videoProvider === 'vimeo') { - this.element.find(this.options.metaData.DOM.uploader).html( - '<a href="' + - this.options.metaData.data.uploaderUrl + - '" target="_blank">' + this.options.metaData.data.uploader + - '</a>'); + uploaderLinkUrl = this.options.metaData.data.uploaderUrl; } + uploaderLink = document.createElement('a'); + uploaderLink.setAttribute('href', uploaderLinkUrl); + uploaderLink.setAttribute('target', '_blank'); + uploaderLink.innerText = this.options.metaData.data.uploader; + this.element.find(this.options.metaData.DOM.uploader)[0].appendChild(uploaderLink); this.element.find('.' + this.options.videoClass).productVideoLoader(); }, diff --git a/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js b/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js index 82a7f2d1a8e1..cd0f3b3d630a 100644 --- a/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js +++ b/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js @@ -177,12 +177,14 @@ define([ * @private */ clearEvents: function () { - this.fotoramaItem.off( - 'fotorama:show.' + this.PV + - ' fotorama:showend.' + this.PV + - ' fotorama:fullscreenenter.' + this.PV + - ' fotorama:fullscreenexit.' + this.PV - ); + if (this.fotoramaItem !== undefined) { + this.fotoramaItem.off( + 'fotorama:show.' + this.PV + + ' fotorama:showend.' + this.PV + + ' fotorama:fullscreenenter.' + this.PV + + ' fotorama:fullscreenexit.' + this.PV + ); + } }, /** @@ -448,7 +450,7 @@ define([ scriptTag = document.getElementsByTagName('script')[0]; element.async = true; - element.src = 'https://secure-a.vimeocdn.com/js/froogaloop2.min.js'; + element.src = 'https://f.vimeocdn.com/js/froogaloop2.min.js'; /** * Vimeo js framework on load callback. diff --git a/app/code/Magento/Quote/Api/CartRepositoryInterface.php b/app/code/Magento/Quote/Api/CartRepositoryInterface.php index f507c1e83f10..ee122d1b02ff 100644 --- a/app/code/Magento/Quote/Api/CartRepositoryInterface.php +++ b/app/code/Magento/Quote/Api/CartRepositoryInterface.php @@ -25,10 +25,9 @@ public function get($cartId); * Enables administrative users to list carts that match specified search criteria. * * This call returns an array of objects, but detailed information about each object’s attributes might not be - * included. See http://devdocs.magento.com/codelinks/attributes.html#CartRepositoryInterface to determine + * included. See https://devdocs.magento.com/codelinks/attributes.html#CartRepositoryInterface to determine * which call to use to get detailed information about all attributes for an object. * - * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria * @return \Magento\Quote\Api\Data\CartSearchResultsInterface */ diff --git a/app/code/Magento/Quote/Api/Data/CartInterface.php b/app/code/Magento/Quote/Api/Data/CartInterface.php index 551833e3effb..b87869de6b3d 100644 --- a/app/code/Magento/Quote/Api/Data/CartInterface.php +++ b/app/code/Magento/Quote/Api/Data/CartInterface.php @@ -223,14 +223,14 @@ public function setBillingAddress(\Magento\Quote\Api\Data\AddressInterface $bill /** * Returns the reserved order ID for the cart. * - * @return int|null Reserved order ID. Otherwise, null. + * @return string|null Reserved order ID. Otherwise, null. */ public function getReservedOrderId(); /** * Sets the reserved order ID for the cart. * - * @param int $reservedOrderId + * @param string $reservedOrderId * @return $this */ public function setReservedOrderId($reservedOrderId); diff --git a/app/code/Magento/Quote/Api/GuestPaymentMethodManagementInterface.php b/app/code/Magento/Quote/Api/GuestPaymentMethodManagementInterface.php index a9d1772684ba..f1ee8bd83fe9 100644 --- a/app/code/Magento/Quote/Api/GuestPaymentMethodManagementInterface.php +++ b/app/code/Magento/Quote/Api/GuestPaymentMethodManagementInterface.php @@ -37,7 +37,7 @@ public function get($cartId); * List available payment methods for a specified shopping cart. * * This call returns an array of objects, but detailed information about each object’s attributes might not be - * included. See http://devdocs.magento.com/codelinks/attributes.html#GuestPaymentMethodManagementInterface to + * included. See https://devdocs.magento.com/codelinks/attributes.html#GuestPaymentMethodManagementInterface to * determine which call to use to get detailed information about all attributes for an object. * * @param string $cartId The cart ID. diff --git a/app/code/Magento/Quote/Api/PaymentMethodManagementInterface.php b/app/code/Magento/Quote/Api/PaymentMethodManagementInterface.php index 50fac772ed3d..b00a6617beae 100644 --- a/app/code/Magento/Quote/Api/PaymentMethodManagementInterface.php +++ b/app/code/Magento/Quote/Api/PaymentMethodManagementInterface.php @@ -37,7 +37,7 @@ public function get($cartId); * Lists available payment methods for a specified shopping cart. * * This call returns an array of objects, but detailed information about each object’s attributes might not be - * included. See http://devdocs.magento.com/codelinks/attributes.html#PaymentMethodManagementInterface to + * included. See https://devdocs.magento.com/codelinks/attributes.html#PaymentMethodManagementInterface to * determine which call to use to get detailed information about all attributes for an object. * * @param int $cartId The cart ID. diff --git a/app/code/Magento/Quote/Model/BillingAddressManagement.php b/app/code/Magento/Quote/Model/BillingAddressManagement.php index 70a8c94fefad..bc055e71c662 100644 --- a/app/code/Magento/Quote/Model/BillingAddressManagement.php +++ b/app/code/Magento/Quote/Model/BillingAddressManagement.php @@ -14,7 +14,6 @@ /** * Quote billing address write service object. - * */ class BillingAddressManagement implements BillingAddressManagementInterface { @@ -70,13 +69,14 @@ public function __construct( } /** - * {@inheritDoc} + * @inheritdoc * @SuppressWarnings(PHPMD.NPathComplexity) */ public function assign($cartId, \Magento\Quote\Api\Data\AddressInterface $address, $useForShipping = false) { /** @var \Magento\Quote\Model\Quote $quote */ $quote = $this->quoteRepository->getActive($cartId); + $address->setCustomerId($quote->getCustomerId()); $quote->removeAddress($quote->getBillingAddress()->getId()); $quote->setBillingAddress($address); try { @@ -91,7 +91,7 @@ public function assign($cartId, \Magento\Quote\Api\Data\AddressInterface $addres } /** - * {@inheritDoc} + * @inheritdoc */ public function get($cartId) { @@ -100,6 +100,8 @@ public function get($cartId) } /** + * Get shipping address assignment + * * @return \Magento\Quote\Model\ShippingAddressAssignment * @deprecated 100.2.0 */ diff --git a/app/code/Magento/Quote/Model/Cart/CartTotalRepository.php b/app/code/Magento/Quote/Model/Cart/CartTotalRepository.php index e18ab8587fc7..60e5ad9f4caf 100644 --- a/app/code/Magento/Quote/Model/Cart/CartTotalRepository.php +++ b/app/code/Magento/Quote/Model/Cart/CartTotalRepository.php @@ -79,7 +79,7 @@ public function __construct( } /** - * {@inheritDoc} + * @inheritdoc * * @param int $cartId The cart ID. * @return Totals Quote totals data. diff --git a/app/code/Magento/Quote/Model/MaskedQuoteIdToQuoteId.php b/app/code/Magento/Quote/Model/MaskedQuoteIdToQuoteId.php new file mode 100644 index 000000000000..37a8fcd494fb --- /dev/null +++ b/app/code/Magento/Quote/Model/MaskedQuoteIdToQuoteId.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model; + +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\ResourceModel\Quote\QuoteIdMask as QuoteIdMaskResource; + +/** + * MaskedQuoteId to QuoteId resolver + */ +class MaskedQuoteIdToQuoteId implements MaskedQuoteIdToQuoteIdInterface +{ + /** + * @var CartRepositoryInterface + */ + private $cartRepository; + + /** + * @var QuoteIdMaskFactory + */ + private $quoteIdMaskFactory; + + /** + * @var QuoteIdMaskResource + */ + private $quoteIdMaskResource; + + /** + * @param QuoteIdMaskFactory $quoteIdMaskFactory + * @param CartRepositoryInterface $cartRepository + * @param QuoteIdMaskResource $quoteIdMaskResource + */ + public function __construct( + QuoteIdMaskFactory $quoteIdMaskFactory, + CartRepositoryInterface $cartRepository, + QuoteIdMaskResource $quoteIdMaskResource + ) { + $this->quoteIdMaskFactory = $quoteIdMaskFactory; + $this->cartRepository = $cartRepository; + $this->quoteIdMaskResource = $quoteIdMaskResource; + } + + /** + * @inheritDoc + */ + public function execute(string $maskedQuoteId): int + { + $quoteIdMask = $this->quoteIdMaskFactory->create(); + $this->quoteIdMaskResource->load($quoteIdMask, $maskedQuoteId, 'masked_id'); + + $cart = $this->cartRepository->get($quoteIdMask->getQuoteId()); + + return (int)$cart->getId(); + } +} diff --git a/app/code/Magento/Quote/Model/MaskedQuoteIdToQuoteIdInterface.php b/app/code/Magento/Quote/Model/MaskedQuoteIdToQuoteIdInterface.php new file mode 100644 index 000000000000..152d575e059c --- /dev/null +++ b/app/code/Magento/Quote/Model/MaskedQuoteIdToQuoteIdInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model; + +use Magento\Framework\Exception\NoSuchEntityException; + +/** + * Converts masked quote id to the quote id (entity id) + * @api + */ +interface MaskedQuoteIdToQuoteIdInterface +{ + /** + * @param string $maskedQuoteId + * @return int + * @throws NoSuchEntityException + */ + public function execute(string $maskedQuoteId): int; +} diff --git a/app/code/Magento/Quote/Model/Product/Plugin/MarkQuotesRecollectMassDisabled.php b/app/code/Magento/Quote/Model/Product/Plugin/MarkQuotesRecollectMassDisabled.php new file mode 100644 index 000000000000..f18bb46fa63f --- /dev/null +++ b/app/code/Magento/Quote/Model/Product/Plugin/MarkQuotesRecollectMassDisabled.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\Product\Plugin; + +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Action as ProductAction; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; + +/** + * Remove quote items after mass disabling products + */ +class MarkQuotesRecollectMassDisabled +{ + /** @var QuoteResource$quoteResource */ + private $quoteResource; + + /** + * @param QuoteResource $quoteResource + */ + public function __construct( + QuoteResource $quoteResource + ) { + $this->quoteResource = $quoteResource; + } + + /** + * Clean quote items after mass disabling product + * + * @param \Magento\Catalog\Model\Product\Action $subject + * @param \Magento\Catalog\Model\Product\Action $result + * @param int[] $productIds + * @param int[] $attrData + * @param int $storeId + * @return \Magento\Catalog\Model\Product\Action + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterUpdateAttributes( + ProductAction $subject, + ProductAction $result, + $productIds, + $attrData, + $storeId + ): ProductAction { + if (isset($attrData['status']) && $attrData['status'] === Status::STATUS_DISABLED) { + $this->quoteResource->markQuotesRecollect($productIds); + } + + return $result; + } +} diff --git a/app/code/Magento/Quote/Model/Quote.php b/app/code/Magento/Quote/Model/Quote.php index 052e1c2bc380..b1f68d0411cf 100644 --- a/app/code/Magento/Quote/Model/Quote.php +++ b/app/code/Magento/Quote/Model/Quote.php @@ -503,9 +503,10 @@ protected function _construct() } /** - * @codeCoverageIgnoreStart + * Returns information about quote currency, such as code, exchange rate, and so on. * - * {@inheritdoc} + * @return \Magento\Quote\Api\Data\CurrencyInterface|null Quote currency information. Otherwise, null. + * @codeCoverageIgnoreStart */ public function getCurrency() { @@ -1163,6 +1164,8 @@ public function getShippingAddress() } /** + * Get all shipping addresses. + * * @return array */ public function getAllShippingAddresses() @@ -1193,6 +1196,7 @@ public function getAllAddresses() } /** + * Get address by id. * * @param int $addressId * @return Address|false @@ -1208,6 +1212,8 @@ public function getAddressById($addressId) } /** + * Get address by customer address id. + * * @param int|string $addressId * @return Address|false */ @@ -1242,6 +1248,8 @@ public function getShippingAddressByCustomerAddressId($addressId) } /** + * Remove address. + * * @param int|string $addressId * @return $this */ @@ -1294,6 +1302,8 @@ public function removeAllAddresses() } /** + * Add address. + * * @param \Magento\Quote\Api\Data\AddressInterface $address * @return $this */ @@ -1307,18 +1317,21 @@ public function addAddress(\Magento\Quote\Api\Data\AddressInterface $address) } /** + * Set billing address. + * * @param \Magento\Quote\Api\Data\AddressInterface $address * @return $this */ public function setBillingAddress(\Magento\Quote\Api\Data\AddressInterface $address = null) { - $old = $this->getBillingAddress(); - + $old = $this->getAddressesCollection()->getItemById($address->getId()) + ?? $this->getBillingAddress(); if (!empty($old)) { $old->addData($address->getData()); } else { $this->addAddress($address->setAddressType(Address::TYPE_BILLING)); } + return $this; } @@ -1333,17 +1346,21 @@ public function setShippingAddress(\Magento\Quote\Api\Data\AddressInterface $add if ($this->getIsMultiShipping()) { $this->addAddress($address->setAddressType(Address::TYPE_SHIPPING)); } else { - $old = $this->getShippingAddress(); + $old = $this->getAddressesCollection()->getItemById($address->getId()) + ?? $this->getShippingAddress(); if (!empty($old)) { $old->addData($address->getData()); } else { $this->addAddress($address->setAddressType(Address::TYPE_SHIPPING)); } } + return $this; } /** + * Add shipping address. + * * @param \Magento\Quote\Api\Data\AddressInterface $address * @return $this */ @@ -1358,14 +1375,13 @@ public function addShippingAddress(\Magento\Quote\Api\Data\AddressInterface $add * * @param bool $useCache * @return \Magento\Eav\Model\Entity\Collection\AbstractCollection - * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function getItemsCollection($useCache = true) { - if ($this->hasItemsCollection()) { + if ($this->hasItemsCollection() && $useCache) { return $this->getData('items_collection'); } - if (null === $this->_items) { + if (null === $this->_items || !$useCache) { $this->_items = $this->_quoteItemCollectionFactory->create(); $this->extensionAttributesJoinProcessor->process($this->_items); $this->_items->setQuote($this); @@ -1382,7 +1398,7 @@ public function getAllItems() { $items = []; foreach ($this->getItemsCollection() as $item) { - /** @var \Magento\Quote\Model\ResourceModel\Quote\Item $item */ + /** @var \Magento\Quote\Model\Quote\Item $item */ if (!$item->isDeleted()) { $items[] = $item; } @@ -1568,8 +1584,7 @@ public function addItem(\Magento\Quote\Model\Quote\Item $item) } /** - * Advanced func to add product to quote - processing mode can be specified there. - * Returns error message if product type instance can't prepare product. + * Add product. Returns error message if product type instance can't prepare product. * * @param mixed $product * @param null|float|\Magento\Framework\DataObject $request @@ -1805,6 +1820,8 @@ public function getItemByProduct($product) } /** + * Get items summary qty. + * * @return int */ public function getItemsSummaryQty() @@ -1832,6 +1849,8 @@ public function getItemsSummaryQty() } /** + * Get item virtual qty. + * * @return int */ public function getItemVirtualQty() @@ -1865,6 +1884,8 @@ public function getItemVirtualQty() /*********************** PAYMENTS ***************************/ /** + * Get payments collection. + * * @return \Magento\Eav\Model\Entity\Collection\AbstractCollection */ public function getPaymentsCollection() @@ -1882,6 +1903,8 @@ public function getPaymentsCollection() } /** + * Get payment. + * * @return \Magento\Quote\Model\Quote\Payment */ public function getPayment() @@ -1936,6 +1959,8 @@ public function setPayment(PaymentInterface $payment) } /** + * Remove payment. + * * @return $this */ public function removePayment() @@ -1973,6 +1998,8 @@ public function getTotals() } /** + * Add message. + * * @param string $message * @param string $index * @return $this @@ -2061,8 +2088,7 @@ public function setHasError($flag) } /** - * Clears list of errors, associated with this quote. - * Also automatically removes error-flag from oneself. + * Clears list of errors, associated with this quote. Also automatically removes error-flag from oneself. * * @return $this */ @@ -2074,8 +2100,7 @@ protected function _clearErrorInfo() } /** - * Adds error information to the quote. - * Automatically sets error flag. + * Adds error information to the quote. Automatically sets error flag. * * @param string $type An internal error type ('error', 'qty', etc.), passed then to adding messages routine * @param string|null $origin Usually a name of module, that embeds error @@ -2202,6 +2227,8 @@ public function reserveOrderId() } /** + * Validate minimum amount. + * * @param bool $multishipping * @return bool * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -2218,6 +2245,11 @@ public function validateMinimumAmount($multishipping = false) if (!$minOrderActive) { return true; } + $includeDiscount = $this->_scopeConfig->getValue( + 'sales/minimum_order/include_discount_amount', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $storeId + ); $minOrderMulti = $this->_scopeConfig->isSetFlag( 'sales/minimum_order/multi_address', \Magento\Store\Model\ScopeInterface::SCOPE_STORE, @@ -2251,7 +2283,10 @@ public function validateMinimumAmount($multishipping = false) $taxes = ($taxInclude) ? $address->getBaseTaxAmount() : 0; foreach ($address->getQuote()->getItemsCollection() as $item) { /** @var \Magento\Quote\Model\Quote\Item $item */ - $amount = $item->getBaseRowTotal() - $item->getBaseDiscountAmount() + $taxes; + $amount = $includeDiscount ? + $item->getBaseRowTotal() - $item->getBaseDiscountAmount() + $taxes : + $item->getBaseRowTotal() + $taxes; + if ($amount < $minAmount) { return false; } @@ -2261,7 +2296,9 @@ public function validateMinimumAmount($multishipping = false) $baseTotal = 0; foreach ($addresses as $address) { $taxes = ($taxInclude) ? $address->getBaseTaxAmount() : 0; - $baseTotal += $address->getBaseSubtotalWithDiscount() + $taxes; + $baseTotal += $includeDiscount ? + $address->getBaseSubtotalWithDiscount() + $taxes : + $address->getBaseSubtotal() + $taxes; } if ($baseTotal < $minAmount) { return false; @@ -2301,7 +2338,7 @@ public function isVirtual() */ public function getIsVirtual() { - return intval($this->isVirtual()); + return (int)$this->isVirtual(); } /** @@ -2452,7 +2489,6 @@ public function getCheckoutMethod($originalMethod = false) /** * Get quote items assigned to different quote addresses populated per item qty. - * Based on result array we can display each item separately * * @return array */ @@ -2541,7 +2577,7 @@ public function isMultipleShippingAddresses() } /** - * {@inheritdoc} + * Retrieve existing extension attributes object or create a new one. * * @return \Magento\Quote\Api\Data\CartExtensionInterface|null */ @@ -2551,7 +2587,7 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * Set an extension attributes object. * * @param \Magento\Quote\Api\Data\CartExtensionInterface $extensionAttributes * @return $this diff --git a/app/code/Magento/Quote/Model/Quote/Address.php b/app/code/Magento/Quote/Model/Quote/Address.php index 3eb5d6888503..3ecbc69b8078 100644 --- a/app/code/Magento/Quote/Model/Quote/Address.php +++ b/app/code/Magento/Quote/Model/Quote/Address.php @@ -28,8 +28,8 @@ * @method Address setAddressType(string $value) * @method int getFreeShipping() * @method Address setFreeShipping(int $value) - * @method int getCollectShippingRates() - * @method Address setCollectShippingRates(int $value) + * @method bool getCollectShippingRates() + * @method Address setCollectShippingRates(bool $value) * @method Address setShippingMethod(string $value) * @method string getShippingDescription() * @method Address setShippingDescription(string $value) @@ -965,6 +965,7 @@ public function collectShippingRates() /** * Request shipping rates for entire address or specified address item + * * Returns true if current selected shipping method code corresponds to one of the found rates * * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item @@ -1002,8 +1003,14 @@ public function requestShippingRates(\Magento\Quote\Model\Quote\Item\AbstractIte /** * Store and website identifiers specified from StoreManager */ - $request->setStoreId($this->storeManager->getStore()->getId()); - $request->setWebsiteId($this->storeManager->getWebsite()->getId()); + if ($this->getQuote()->getStoreId()) { + $storeId = $this->getQuote()->getStoreId(); + $request->setStoreId($storeId); + $request->setWebsiteId($this->storeManager->getStore($storeId)->getWebsiteId()); + } else { + $request->setStoreId($this->storeManager->getStore()->getId()); + $request->setWebsiteId($this->storeManager->getWebsite()->getId()); + } $request->setFreeShipping($this->getFreeShipping()); /** * Currencies need to convert in free shipping @@ -1142,6 +1149,11 @@ public function validateMinimumAmount() return true; } + $includeDiscount = $this->_scopeConfig->getValue( + 'sales/minimum_order/include_discount_amount', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $storeId + ); $amount = $this->_scopeConfig->getValue( 'sales/minimum_order/amount', \Magento\Store\Model\ScopeInterface::SCOPE_STORE, @@ -1152,9 +1164,12 @@ public function validateMinimumAmount() \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $storeId ); + $taxes = $taxInclude ? $this->getBaseTaxAmount() : 0; - return ($this->getBaseSubtotalWithDiscount() + $taxes >= $amount); + return $includeDiscount ? + ($this->getBaseSubtotalWithDiscount() + $taxes >= $amount) : + ($this->getBaseSubtotal() + $taxes >= $amount); } /** @@ -1348,7 +1363,7 @@ public function getAllBaseTotalAmounts() /******************************* End Total Collector Interface *******************************************/ /** - * {@inheritdoc} + * @inheritdoc */ protected function _getValidationRulesBeforeSave() { @@ -1356,7 +1371,7 @@ protected function _getValidationRulesBeforeSave() } /** - * {@inheritdoc} + * @inheritdoc */ public function getCountryId() { @@ -1364,7 +1379,7 @@ public function getCountryId() } /** - * {@inheritdoc} + * @inheritdoc */ public function setCountryId($countryId) { @@ -1372,7 +1387,7 @@ public function setCountryId($countryId) } /** - * {@inheritdoc} + * @inheritdoc */ public function getStreet() { @@ -1381,7 +1396,7 @@ public function getStreet() } /** - * {@inheritdoc} + * @inheritdoc */ public function setStreet($street) { @@ -1389,7 +1404,7 @@ public function setStreet($street) } /** - * {@inheritdoc} + * @inheritdoc */ public function getCompany() { @@ -1397,7 +1412,7 @@ public function getCompany() } /** - * {@inheritdoc} + * @inheritdoc */ public function setCompany($company) { @@ -1405,7 +1420,7 @@ public function setCompany($company) } /** - * {@inheritdoc} + * @inheritdoc */ public function getTelephone() { @@ -1413,7 +1428,7 @@ public function getTelephone() } /** - * {@inheritdoc} + * @inheritdoc */ public function setTelephone($telephone) { @@ -1421,7 +1436,7 @@ public function setTelephone($telephone) } /** - * {@inheritdoc} + * @inheritdoc */ public function getFax() { @@ -1429,7 +1444,7 @@ public function getFax() } /** - * {@inheritdoc} + * @inheritdoc */ public function setFax($fax) { @@ -1437,7 +1452,7 @@ public function setFax($fax) } /** - * {@inheritdoc} + * @inheritdoc */ public function getPostcode() { @@ -1445,7 +1460,7 @@ public function getPostcode() } /** - * {@inheritdoc} + * @inheritdoc */ public function setPostcode($postcode) { @@ -1453,7 +1468,7 @@ public function setPostcode($postcode) } /** - * {@inheritdoc} + * @inheritdoc */ public function getCity() { @@ -1461,7 +1476,7 @@ public function getCity() } /** - * {@inheritdoc} + * @inheritdoc */ public function setCity($city) { @@ -1469,7 +1484,7 @@ public function setCity($city) } /** - * {@inheritdoc} + * @inheritdoc */ public function getFirstname() { @@ -1477,7 +1492,7 @@ public function getFirstname() } /** - * {@inheritdoc} + * @inheritdoc */ public function setFirstname($firstname) { @@ -1485,7 +1500,7 @@ public function setFirstname($firstname) } /** - * {@inheritdoc} + * @inheritdoc */ public function getLastname() { @@ -1493,7 +1508,7 @@ public function getLastname() } /** - * {@inheritdoc} + * @inheritdoc */ public function setLastname($lastname) { @@ -1501,7 +1516,7 @@ public function setLastname($lastname) } /** - * {@inheritdoc} + * @inheritdoc */ public function getMiddlename() { @@ -1509,7 +1524,7 @@ public function getMiddlename() } /** - * {@inheritdoc} + * @inheritdoc */ public function setMiddlename($middlename) { @@ -1517,7 +1532,7 @@ public function setMiddlename($middlename) } /** - * {@inheritdoc} + * @inheritdoc */ public function getPrefix() { @@ -1525,7 +1540,7 @@ public function getPrefix() } /** - * {@inheritdoc} + * @inheritdoc */ public function setPrefix($prefix) { @@ -1533,7 +1548,7 @@ public function setPrefix($prefix) } /** - * {@inheritdoc} + * @inheritdoc */ public function getSuffix() { @@ -1541,7 +1556,7 @@ public function getSuffix() } /** - * {@inheritdoc} + * @inheritdoc */ public function setSuffix($suffix) { @@ -1549,7 +1564,7 @@ public function setSuffix($suffix) } /** - * {@inheritdoc} + * @inheritdoc */ public function getVatId() { @@ -1557,7 +1572,7 @@ public function getVatId() } /** - * {@inheritdoc} + * @inheritdoc */ public function setVatId($vatId) { @@ -1565,7 +1580,7 @@ public function setVatId($vatId) } /** - * {@inheritdoc} + * @inheritdoc */ public function getCustomerId() { @@ -1573,7 +1588,7 @@ public function getCustomerId() } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomerId($customerId) { @@ -1581,7 +1596,7 @@ public function setCustomerId($customerId) } /** - * {@inheritdoc} + * @inheritdoc */ public function getEmail() { @@ -1594,7 +1609,7 @@ public function getEmail() } /** - * {@inheritdoc} + * @inheritdoc */ public function setEmail($email) { @@ -1602,7 +1617,7 @@ public function setEmail($email) } /** - * {@inheritdoc} + * @inheritdoc */ public function setRegion($region) { @@ -1610,7 +1625,7 @@ public function setRegion($region) } /** - * {@inheritdoc} + * @inheritdoc */ public function setRegionId($regionId) { @@ -1618,7 +1633,7 @@ public function setRegionId($regionId) } /** - * {@inheritdoc} + * @inheritdoc */ public function setRegionCode($regionCode) { @@ -1626,7 +1641,7 @@ public function setRegionCode($regionCode) } /** - * {@inheritdoc} + * @inheritdoc */ public function getSameAsBilling() { @@ -1634,7 +1649,7 @@ public function getSameAsBilling() } /** - * {@inheritdoc} + * @inheritdoc */ public function setSameAsBilling($sameAsBilling) { @@ -1642,7 +1657,7 @@ public function setSameAsBilling($sameAsBilling) } /** - * {@inheritdoc} + * @inheritdoc */ public function getCustomerAddressId() { @@ -1650,7 +1665,7 @@ public function getCustomerAddressId() } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomerAddressId($customerAddressId) { @@ -1681,7 +1696,7 @@ public function setSaveInAddressBook($saveInAddressBook) //@codeCoverageIgnoreEnd /** - * {@inheritdoc} + * @inheritdoc * * @return \Magento\Quote\Api\Data\AddressExtensionInterface|null */ @@ -1691,7 +1706,7 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * @inheritdoc * * @param \Magento\Quote\Api\Data\AddressExtensionInterface $extensionAttributes * @return $this @@ -1712,7 +1727,7 @@ public function getShippingMethod() } /** - * {@inheritdoc} + * @inheritdoc */ protected function getCustomAttributesCodes() { diff --git a/app/code/Magento/Quote/Model/Quote/Address/BillingAddressPersister.php b/app/code/Magento/Quote/Model/Quote/Address/BillingAddressPersister.php index c5b8dc1c4b12..6fdb70350ed7 100644 --- a/app/code/Magento/Quote/Model/Quote/Address/BillingAddressPersister.php +++ b/app/code/Magento/Quote/Model/Quote/Address/BillingAddressPersister.php @@ -12,6 +12,9 @@ use Magento\Quote\Model\QuoteAddressValidator; use Magento\Customer\Api\AddressRepositoryInterface; +/** + * Saves billing address for quotes. + */ class BillingAddressPersister { /** @@ -37,6 +40,8 @@ public function __construct( } /** + * Save address for billing. + * * @param CartInterface $quote * @param AddressInterface $address * @param bool $useForShipping @@ -47,7 +52,7 @@ public function __construct( public function save(CartInterface $quote, AddressInterface $address, $useForShipping = false) { /** @var \Magento\Quote\Model\Quote $quote */ - $this->addressValidator->validate($address); + $this->addressValidator->validateForCart($quote, $address); $customerAddressId = $address->getCustomerAddressId(); $shippingAddress = null; $addressData = []; diff --git a/app/code/Magento/Quote/Model/Quote/Address/CustomAttributeListInterface.php b/app/code/Magento/Quote/Model/Quote/Address/CustomAttributeListInterface.php index 4d642c31ae16..313ad23f2215 100644 --- a/app/code/Magento/Quote/Model/Quote/Address/CustomAttributeListInterface.php +++ b/app/code/Magento/Quote/Model/Quote/Address/CustomAttributeListInterface.php @@ -12,7 +12,7 @@ interface CustomAttributeListInterface { /** - * Retrieve list of quote addresss custom attributes + * Retrieve list of quote address custom attributes * * @return array */ diff --git a/app/code/Magento/Quote/Model/Quote/Address/Item.php b/app/code/Magento/Quote/Model/Quote/Address/Item.php index d7014403f788..ade4f9270b68 100644 --- a/app/code/Magento/Quote/Model/Quote/Address/Item.php +++ b/app/code/Magento/Quote/Model/Quote/Address/Item.php @@ -8,6 +8,8 @@ use Magento\Quote\Model\Quote; /** + * Quote item model. + * * @api * @method int getParentItemId() * @method \Magento\Quote\Model\Quote\Address\Item setParentItemId(int $value) @@ -46,6 +48,8 @@ * @method \Magento\Quote\Model\Quote\Address\Item setSuperProductId(int $value) * @method int getParentProductId() * @method \Magento\Quote\Model\Quote\Address\Item setParentProductId(int $value) + * @method int getStoreId() + * @method \Magento\Quote\Model\Quote\Address\Item setStoreId(int $value) * @method string getSku() * @method \Magento\Quote\Model\Quote\Address\Item setSku(string $value) * @method string getImage() @@ -101,7 +105,7 @@ class Item extends \Magento\Quote\Model\Quote\Item\AbstractItem protected $_quote; /** - * @return void + * @inheritdoc */ protected function _construct() { @@ -109,7 +113,7 @@ protected function _construct() } /** - * @return $this|\Magento\Quote\Model\Quote\Item\AbstractItem + * @inheritdoc */ public function beforeSave() { @@ -154,6 +158,8 @@ public function getQuote() } /** + * Import quote item. + * * @param \Magento\Quote\Model\Quote\Item $quoteItem * @return $this */ @@ -168,6 +174,8 @@ public function importQuoteItem(\Magento\Quote\Model\Quote\Item $quoteItem) $quoteItem->getProductId() )->setProduct( $quoteItem->getProduct() + )->setStoreId( + $quoteItem->getStoreId() )->setSku( $quoteItem->getSku() )->setName( @@ -190,10 +198,9 @@ public function importQuoteItem(\Magento\Quote\Model\Quote\Item $quoteItem) } /** - * @param string $code - * @return \Magento\Catalog\Model\Product\Configuration\Item\Option\OptionInterface|null + * @inheritdoc */ - public function getOptionBycode($code) + public function getOptionByCode($code) { if ($this->getQuoteItem()) { return $this->getQuoteItem()->getOptionBycode($code); diff --git a/app/code/Magento/Quote/Model/Quote/Address/Total.php b/app/code/Magento/Quote/Model/Quote/Address/Total.php index 42224c970ed2..00060c15c10d 100644 --- a/app/code/Magento/Quote/Model/Quote/Address/Total.php +++ b/app/code/Magento/Quote/Model/Quote/Address/Total.php @@ -6,6 +6,8 @@ namespace Magento\Quote\Model\Quote\Address; /** + * Class Total + * * @method string getCode() * * @api @@ -54,6 +56,8 @@ public function __construct( */ public function setTotalAmount($code, $amount) { + $amount = is_float($amount) ? round($amount, 4) : $amount; + $this->totalAmounts[$code] = $amount; if ($code != 'subtotal') { $code = $code . '_amount'; @@ -72,6 +76,8 @@ public function setTotalAmount($code, $amount) */ public function setBaseTotalAmount($code, $amount) { + $amount = is_float($amount) ? round($amount, 4) : $amount; + $this->baseTotalAmounts[$code] = $amount; if ($code != 'subtotal') { $code = $code . '_amount'; @@ -167,6 +173,7 @@ public function getAllBaseTotalAmounts() /** * Set the full info, which is used to capture tax related information. + * * If a string is used, it is assumed to be serialized. * * @param array|string $info diff --git a/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php b/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php index 84f1fc1c35ad..e9a63dad6e16 100644 --- a/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php +++ b/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php @@ -9,6 +9,9 @@ use Magento\Quote\Api\Data\AddressInterface; use Magento\Quote\Model\Quote\Address\FreeShippingInterface; +/** + * Collect totals for shipping. + */ class Shipping extends \Magento\Quote\Model\Quote\Address\Total\AbstractTotal { /** @@ -111,7 +114,7 @@ public function fetch(\Magento\Quote\Model\Quote $quote, \Magento\Quote\Model\Qu { $amount = $total->getShippingAmount(); $shippingDescription = $total->getShippingDescription(); - $title = ($amount != 0 && $shippingDescription) + $title = ($shippingDescription) ? __('Shipping & Handling (%1)', $shippingDescription) : __('Shipping & Handling'); @@ -227,7 +230,7 @@ private function getAssignmentWeightData(AddressInterface $address, array $items * @param bool $addressFreeShipping * @param float $itemWeight * @param float $itemQty - * @param $freeShipping + * @param bool $freeShipping * @return float */ private function getItemRowWeight( diff --git a/app/code/Magento/Quote/Model/Quote/Item/AbstractItem.php b/app/code/Magento/Quote/Model/Quote/Item/AbstractItem.php index 1aca6d6c88cd..a6fa6828a04e 100644 --- a/app/code/Magento/Quote/Model/Quote/Item/AbstractItem.php +++ b/app/code/Magento/Quote/Model/Quote/Item/AbstractItem.php @@ -418,6 +418,7 @@ public function calcRowTotal() /** * Get item price used for quote calculation process. + * * This method get custom price (if it is defined) or original product final price * * @return float @@ -438,6 +439,7 @@ public function getCalculationPrice() /** * Get item price used for quote calculation process. + * * This method get original custom price applied before tax calculation * * @return float @@ -502,6 +504,7 @@ public function getBaseCalculationPriceOriginal() /** * Get original price (retrieved from product) for item. + * * Original price value is in quote selected currency * * @return float @@ -519,8 +522,8 @@ public function getOriginalPrice() /** * Set original price to item (calculation price will be refreshed too) * - * @param float $price - * @return \Magento\Quote\Model\Quote\Item\AbstractItem + * @param float $price + * @return \Magento\Quote\Model\Quote\Item\AbstractItem */ public function setOriginalPrice($price) { @@ -538,10 +541,10 @@ public function getBaseOriginalPrice() } /** - * Specify custom item price (used in case whe we have apply not product price to item) + * Specify custom item price (used in case when we have apply not product price to item) * - * @param float $value - * @return \Magento\Quote\Model\Quote\Item\AbstractItem + * @param float $value + * @return \Magento\Quote\Model\Quote\Item\AbstractItem */ public function setCustomPrice($value) { @@ -563,8 +566,8 @@ public function getPrice() /** * Specify item price (base calculation price and converted price will be refreshed too) * - * @param float $value - * @return $this + * @param float $value + * @return $this */ public function setPrice($value) { @@ -575,6 +578,7 @@ public function setPrice($value) /** * Get item price converted to quote currency + * * @return float */ public function getConvertedPrice() @@ -589,6 +593,7 @@ public function getConvertedPrice() /** * Set new value for converted price + * * @param float $value * @return $this */ @@ -614,8 +619,7 @@ public function __clone() } /** - * Checking if there children calculated or parent item - * when we have parent quote item and its children + * Checking if there children calculated or parent item when we have parent quote item and its children * * @return bool */ @@ -636,6 +640,8 @@ public function isChildrenCalculated() } /** + * Checking can we ship product separately + * * Checking can we ship product separately (each child separately) * or each parent product item can be shipped only like one item * @@ -658,8 +664,9 @@ public function isShipSeparately() } /** - * Returns the total discount amounts of all the child items. If there are no children, returns the discount - * amount of this item. + * Returns the total discount amounts of all the child items. + * + * If there are no children, returns the discount amount of this item. * * @return float */ diff --git a/app/code/Magento/Quote/Model/Quote/Item/Compare.php b/app/code/Magento/Quote/Model/Quote/Item/Compare.php index ddaa636ef32b..76ba324518dc 100644 --- a/app/code/Magento/Quote/Model/Quote/Item/Compare.php +++ b/app/code/Magento/Quote/Model/Quote/Item/Compare.php @@ -50,7 +50,7 @@ protected function getOptionValues($value) if (is_string($value) && $this->jsonValidator->isValid($value)) { $value = $this->serializer->unserialize($value); if (is_array($value)) { - unset($value['qty'], $value['uenc']); + unset($value['qty'], $value['uenc'], $value['related_product'], $value['item']); $value = array_filter($value, function ($optionValue) { return !empty($optionValue); }); diff --git a/app/code/Magento/Quote/Model/Quote/Item/ToOrderItem.php b/app/code/Magento/Quote/Model/Quote/Item/ToOrderItem.php index 32687499274f..6192d3471ccb 100644 --- a/app/code/Magento/Quote/Model/Quote/Item/ToOrderItem.php +++ b/app/code/Magento/Quote/Model/Quote/Item/ToOrderItem.php @@ -48,6 +48,8 @@ public function __construct( } /** + * Convert quote item(quote address item) into order item. + * * @param Item|AddressItem $item * @param array $data * @return OrderItemInterface @@ -63,6 +65,16 @@ public function convert($item, $data = []) 'to_order_item', $item ); + if ($item instanceof \Magento\Quote\Model\Quote\Address\Item) { + $orderItemData = array_merge( + $orderItemData, + $this->objectCopyService->getDataFromFieldset( + 'quote_convert_address_item', + 'to_order_item', + $item + ) + ); + } if (!$item->getNoDiscount()) { $data = array_merge( $data, diff --git a/app/code/Magento/Quote/Model/Quote/Item/Updater.php b/app/code/Magento/Quote/Model/Quote/Item/Updater.php index 6a7a3c1c1839..9865ae82ac3d 100644 --- a/app/code/Magento/Quote/Model/Quote/Item/Updater.php +++ b/app/code/Magento/Quote/Model/Quote/Item/Updater.php @@ -60,6 +60,7 @@ public function __construct( /** * Update quote item qty. + * * Custom price is updated in case 'custom_price' value exists * * @param Item $item @@ -145,8 +146,8 @@ protected function unsetCustomPrice(Item $item) $item->addOption($infoBuyRequest); } - $item->unsetData('custom_price'); - $item->unsetData('original_custom_price'); + $item->setData('custom_price', null); + $item->setData('original_custom_price', null); } /** diff --git a/app/code/Magento/Quote/Model/Quote/TotalsCollector.php b/app/code/Magento/Quote/Model/Quote/TotalsCollector.php index 8fa03232a0e8..9442cf2f5f1c 100644 --- a/app/code/Magento/Quote/Model/Quote/TotalsCollector.php +++ b/app/code/Magento/Quote/Model/Quote/TotalsCollector.php @@ -103,6 +103,8 @@ public function __construct( } /** + * Collect quote totals. + * * @param \Magento\Quote\Model\Quote $quote * @return Address\Total */ @@ -115,6 +117,8 @@ public function collectQuoteTotals(\Magento\Quote\Model\Quote $quote) } /** + * Collect quote. + * * @param \Magento\Quote\Model\Quote $quote * @return \Magento\Quote\Model\Quote\Address\Total */ @@ -172,6 +176,8 @@ public function collect(\Magento\Quote\Model\Quote $quote) } /** + * Validate coupon code. + * * @param \Magento\Quote\Model\Quote $quote * @return $this */ @@ -203,11 +209,12 @@ protected function _validateCouponCode(\Magento\Quote\Model\Quote $quote) */ protected function _collectItemsQtys(\Magento\Quote\Model\Quote $quote) { + $quoteItems = $quote->getAllVisibleItems(); $quote->setItemsCount(0); $quote->setItemsQty(0); $quote->setVirtualItemsQty(0); - foreach ($quote->getAllVisibleItems() as $item) { + foreach ($quoteItems as $item) { if ($item->getParentItem()) { continue; } @@ -231,6 +238,8 @@ protected function _collectItemsQtys(\Magento\Quote\Model\Quote $quote) } /** + * Collect address total. + * * @param \Magento\Quote\Model\Quote $quote * @param Address $address * @return Address\Total diff --git a/app/code/Magento/Quote/Model/QuoteAddressValidator.php b/app/code/Magento/Quote/Model/QuoteAddressValidator.php index 9a86829bfc4c..e7750f5879de 100644 --- a/app/code/Magento/Quote/Model/QuoteAddressValidator.php +++ b/app/code/Magento/Quote/Model/QuoteAddressValidator.php @@ -6,10 +6,13 @@ namespace Magento\Quote\Model; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\CartInterface; /** * Quote shipping/billing address validator service. * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class QuoteAddressValidator { @@ -28,7 +31,7 @@ class QuoteAddressValidator protected $customerRepository; /** - * @var \Magento\Customer\Model\Session + * @deprecated This class is not a part of HTML presentation layer and should not use sessions. */ protected $customerSession; @@ -50,44 +53,80 @@ public function __construct( } /** - * Validates the fields in a specified address data object. + * Validate address. * - * @param \Magento\Quote\Api\Data\AddressInterface $addressData The address data object. - * @return bool + * @param AddressInterface $address + * @param int|null $customerId Cart belongs to + * @return void * @throws \Magento\Framework\Exception\InputException The specified address belongs to another customer. * @throws \Magento\Framework\Exception\NoSuchEntityException The specified customer ID or address ID is not valid. */ - public function validate(\Magento\Quote\Api\Data\AddressInterface $addressData) + private function doValidate(AddressInterface $address, ?int $customerId): void { //validate customer id - if ($addressData->getCustomerId()) { - $customer = $this->customerRepository->getById($addressData->getCustomerId()); + if ($customerId) { + $customer = $this->customerRepository->getById($customerId); if (!$customer->getId()) { throw new \Magento\Framework\Exception\NoSuchEntityException( - __('Invalid customer id %1', $addressData->getCustomerId()) + __('Invalid customer id %1', $customerId) ); } } - if ($addressData->getCustomerAddressId()) { + if ($address->getCustomerAddressId()) { + //Existing address cannot belong to a guest + if (!$customerId) { + throw new \Magento\Framework\Exception\NoSuchEntityException( + __('Invalid customer address id %1', $address->getCustomerAddressId()) + ); + } + //Validating address ID try { - $this->addressRepository->getById($addressData->getCustomerAddressId()); + $this->addressRepository->getById($address->getCustomerAddressId()); } catch (NoSuchEntityException $e) { throw new \Magento\Framework\Exception\NoSuchEntityException( - __('Invalid address id %1', $addressData->getId()) + __('Invalid address id %1', $address->getId()) ); } - + //Finding available customer's addresses $applicableAddressIds = array_map(function ($address) { /** @var \Magento\Customer\Api\Data\AddressInterface $address */ return $address->getId(); - }, $this->customerRepository->getById($addressData->getCustomerId())->getAddresses()); - if (!in_array($addressData->getCustomerAddressId(), $applicableAddressIds)) { + }, $this->customerRepository->getById($customerId)->getAddresses()); + if (!in_array($address->getCustomerAddressId(), $applicableAddressIds)) { throw new \Magento\Framework\Exception\NoSuchEntityException( - __('Invalid customer address id %1', $addressData->getCustomerAddressId()) + __('Invalid customer address id %1', $address->getCustomerAddressId()) ); } } + } + + /** + * Validates the fields in a specified address data object. + * + * @param \Magento\Quote\Api\Data\AddressInterface $addressData The address data object. + * @return bool + * @throws \Magento\Framework\Exception\InputException The specified address belongs to another customer. + * @throws \Magento\Framework\Exception\NoSuchEntityException The specified customer ID or address ID is not valid. + */ + public function validate(AddressInterface $addressData) + { + $this->doValidate($addressData, $addressData->getCustomerId()); + return true; } + + /** + * Validate address to be used for cart. + * + * @param CartInterface $cart + * @param AddressInterface $address + * @return void + * @throws \Magento\Framework\Exception\InputException The specified address belongs to another customer. + * @throws \Magento\Framework\Exception\NoSuchEntityException The specified customer ID or address ID is not valid. + */ + public function validateForCart(CartInterface $cart, AddressInterface $address): void + { + $this->doValidate($address, $cart->getCustomerIsGuest() ? null : $cart->getCustomer()->getId()); + } } diff --git a/app/code/Magento/Quote/Model/QuoteIdMask.php b/app/code/Magento/Quote/Model/QuoteIdMask.php index 7950ab47c366..47b02db7650d 100644 --- a/app/code/Magento/Quote/Model/QuoteIdMask.php +++ b/app/code/Magento/Quote/Model/QuoteIdMask.php @@ -53,11 +53,14 @@ protected function _construct() * Initialize quote identifier before save * * @return $this + * @throws \Magento\Framework\Exception\LocalizedException */ public function beforeSave() { parent::beforeSave(); - $this->setMaskedId($this->randomDataGenerator->getUniqueHash()); + if (empty($this->getMaskedId())) { + $this->setMaskedId($this->randomDataGenerator->getUniqueHash()); + } return $this; } } diff --git a/app/code/Magento/Quote/Model/QuoteIdToMaskedQuoteId.php b/app/code/Magento/Quote/Model/QuoteIdToMaskedQuoteId.php new file mode 100644 index 000000000000..2e802f47cfef --- /dev/null +++ b/app/code/Magento/Quote/Model/QuoteIdToMaskedQuoteId.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model; + +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\ResourceModel\Quote\QuoteIdMask as QuoteIdMaskResource; + +/** + * QuoteId to MaskedQuoteId resolver + */ +class QuoteIdToMaskedQuoteId implements QuoteIdToMaskedQuoteIdInterface +{ + /** + * @var QuoteIdMaskFactory + */ + private $quoteIdMaskFactory; + /** + * @var CartRepositoryInterface + */ + private $cartRepository; + + /** + * @var QuoteIdMaskResource + */ + private $quoteIdMaskResource; + + /** + * @param QuoteIdMaskFactory $quoteIdMaskFactory + * @param CartRepositoryInterface $cartRepository + * @param QuoteIdMaskResource $quoteIdMaskResource + */ + public function __construct( + QuoteIdMaskFactory $quoteIdMaskFactory, + CartRepositoryInterface $cartRepository, + QuoteIdMaskResource $quoteIdMaskResource + ) { + $this->quoteIdMaskFactory = $quoteIdMaskFactory; + $this->cartRepository = $cartRepository; + $this->quoteIdMaskResource = $quoteIdMaskResource; + } + + /** + * @inheritDoc + */ + public function execute(int $quoteId): string + { + /* Check the quote exists to avoid database constraint issues */ + $this->cartRepository->get($quoteId); + + $quoteIdMask = $this->quoteIdMaskFactory->create(); + $this->quoteIdMaskResource->load($quoteIdMask, $quoteId, 'quote_id'); + $maskedId = $quoteIdMask->getMaskedId() ?? ''; + + return $maskedId; + } +} diff --git a/app/code/Magento/Quote/Model/QuoteIdToMaskedQuoteIdInterface.php b/app/code/Magento/Quote/Model/QuoteIdToMaskedQuoteIdInterface.php new file mode 100644 index 000000000000..4d2a8ce877d8 --- /dev/null +++ b/app/code/Magento/Quote/Model/QuoteIdToMaskedQuoteIdInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model; + +use Magento\Framework\Exception\NoSuchEntityException; + +/** + * Converts quote id to the masked quote id + * @api + */ +interface QuoteIdToMaskedQuoteIdInterface +{ + /** + * @param int $quoteId + * @return string + * @throws NoSuchEntityException + */ + public function execute(int $quoteId): string; +} diff --git a/app/code/Magento/Quote/Model/QuoteManagement.php b/app/code/Magento/Quote/Model/QuoteManagement.php index a718d9df9d92..0ad99ffe759f 100644 --- a/app/code/Magento/Quote/Model/QuoteManagement.php +++ b/app/code/Magento/Quote/Model/QuoteManagement.php @@ -25,6 +25,7 @@ /** * Class QuoteManagement * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) */ @@ -145,6 +146,16 @@ class QuoteManagement implements \Magento\Quote\Api\CartManagementInterface */ private $addressesToSync = []; + /** + * @var \Magento\Framework\App\RequestInterface + */ + private $request; + + /** + * @var \Magento\Framework\HTTP\PhpEnvironment\RemoteAddress + */ + private $remoteAddress; + /** * @param EventManager $eventManager * @param QuoteValidator $quoteValidator @@ -159,7 +170,7 @@ class QuoteManagement implements \Magento\Quote\Api\CartManagementInterface * @param \Magento\Quote\Api\CartRepositoryInterface $quoteRepository * @param \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository * @param \Magento\Customer\Model\CustomerFactory $customerModelFactory - * @param \Magento\Quote\Model\Quote\AddressFactory $quoteAddressFactory, + * @param \Magento\Quote\Model\Quote\AddressFactory $quoteAddressFactory * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper * @param StoreManagerInterface $storeManager * @param \Magento\Checkout\Model\Session $checkoutSession @@ -168,6 +179,8 @@ class QuoteManagement implements \Magento\Quote\Api\CartManagementInterface * @param QuoteFactory $quoteFactory * @param \Magento\Quote\Model\QuoteIdMaskFactory|null $quoteIdMaskFactory * @param \Magento\Customer\Api\AddressRepositoryInterface|null $addressRepository + * @param \Magento\Framework\App\RequestInterface|null $request + * @param \Magento\Framework\HTTP\PhpEnvironment\RemoteAddress $remoteAddress * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -192,7 +205,9 @@ public function __construct( \Magento\Customer\Api\AccountManagementInterface $accountManagement, \Magento\Quote\Model\QuoteFactory $quoteFactory, \Magento\Quote\Model\QuoteIdMaskFactory $quoteIdMaskFactory = null, - \Magento\Customer\Api\AddressRepositoryInterface $addressRepository = null + \Magento\Customer\Api\AddressRepositoryInterface $addressRepository = null, + \Magento\Framework\App\RequestInterface $request = null, + \Magento\Framework\HTTP\PhpEnvironment\RemoteAddress $remoteAddress = null ) { $this->eventManager = $eventManager; $this->quoteValidator = $quoteValidator; @@ -218,10 +233,14 @@ public function __construct( ->get(\Magento\Quote\Model\QuoteIdMaskFactory::class); $this->addressRepository = $addressRepository ?: ObjectManager::getInstance() ->get(\Magento\Customer\Api\AddressRepositoryInterface::class); + $this->request = $request ?: ObjectManager::getInstance() + ->get(\Magento\Framework\App\RequestInterface::class); + $this->remoteAddress = $remoteAddress ?: ObjectManager::getInstance() + ->get(\Magento\Framework\HTTP\PhpEnvironment\RemoteAddress::class); } /** - * {@inheritdoc} + * @inheritdoc */ public function createEmptyCart() { @@ -241,7 +260,7 @@ public function createEmptyCart() } /** - * {@inheritdoc} + * @inheritdoc */ public function createEmptyCartForCustomer($customerId) { @@ -253,11 +272,11 @@ public function createEmptyCartForCustomer($customerId) } catch (\Exception $e) { throw new CouldNotSaveException(__("The quote can't be created.")); } - return $quote->getId(); + return (int)$quote->getId(); } /** - * {@inheritdoc} + * @inheritdoc */ public function assignCustomer($cartId, $customerId, $storeId) { @@ -280,6 +299,7 @@ public function assignCustomer($cartId, $customerId, $storeId) throw new StateException( __("The customer can't be assigned to the cart because the customer already has an active cart.") ); + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { } @@ -332,7 +352,7 @@ protected function createCustomerCart($customerId, $storeId) } /** - * {@inheritdoc} + * @inheritdoc */ public function placeOrder($cartId, PaymentInterface $paymentMethod = null) { @@ -349,15 +369,32 @@ public function placeOrder($cartId, PaymentInterface $paymentMethod = null) $data = $paymentMethod->getData(); $quote->getPayment()->importData($data); + } else { + $quote->collectTotals(); } if ($quote->getCheckoutMethod() === self::METHOD_GUEST) { $quote->setCustomerId(null); $quote->setCustomerEmail($quote->getBillingAddress()->getEmail()); + if ($quote->getCustomerFirstname() === null && $quote->getCustomerLastname() === null) { + $quote->setCustomerFirstname($quote->getBillingAddress()->getFirstname()); + $quote->setCustomerLastname($quote->getBillingAddress()->getLastname()); + if ($quote->getBillingAddress()->getMiddlename() === null) { + $quote->setCustomerMiddlename($quote->getBillingAddress()->getMiddlename()); + } + } $quote->setCustomerIsGuest(true); $quote->setCustomerGroupId(\Magento\Customer\Api\Data\GroupInterface::NOT_LOGGED_IN_ID); } + $remoteAddress = $this->remoteAddress->getRemoteAddress(); + if ($remoteAddress !== false) { + $quote->setRemoteIp($remoteAddress); + $quote->setXForwardedFor( + $this->request->getServer('HTTP_X_FORWARDED_FOR') + ); + } + $this->eventManager->dispatch('checkout_submit_before', ['quote' => $quote]); $order = $this->submit($quote); @@ -379,7 +416,7 @@ public function placeOrder($cartId, PaymentInterface $paymentMethod = null) } /** - * {@inheritdoc} + * @inheritdoc */ public function getCartForCustomer($customerId) { @@ -406,6 +443,8 @@ public function submit(QuoteEntity $quote, $orderData = []) } /** + * Convert quote items to order items for quote + * * @param Quote $quote * @return array */ @@ -520,19 +559,7 @@ protected function submitQuote(QuoteEntity $quote, $orderData = []) ); $this->quoteRepository->save($quote); } catch (\Exception $e) { - if (!empty($this->addressesToSync)) { - foreach ($this->addressesToSync as $addressId) { - $this->addressRepository->deleteById($addressId); - } - } - $this->eventManager->dispatch( - 'sales_model_service_quote_submit_failure', - [ - 'order' => $order, - 'quote' => $quote, - 'exception' => $e - ] - ); + $this->rollbackAddresses($quote, $order, $e); throw $e; } return $order; @@ -599,4 +626,43 @@ protected function _prepareCustomerQuote($quote) $shipping->setIsDefaultBilling(true); } } + + /** + * Remove related to order and quote addresses and submit exception to further processing. + * + * @param Quote $quote + * @param \Magento\Sales\Api\Data\OrderInterface $order + * @param \Exception $e + * @throws \Exception + */ + private function rollbackAddresses( + QuoteEntity $quote, + \Magento\Sales\Api\Data\OrderInterface $order, + \Exception $e + ): void { + try { + if (!empty($this->addressesToSync)) { + foreach ($this->addressesToSync as $addressId) { + $this->addressRepository->deleteById($addressId); + } + } + $this->eventManager->dispatch( + 'sales_model_service_quote_submit_failure', + [ + 'order' => $order, + 'quote' => $quote, + 'exception' => $e, + ] + ); + // phpcs:ignore Magento2.Exceptions.ThrowCatch + } catch (\Exception $consecutiveException) { + $message = sprintf( + "An exception occurred on 'sales_model_service_quote_submit_failure' event: %s", + $consecutiveException->getMessage() + ); + + // phpcs:ignore Magento2.Exceptions.DirectThrow + throw new \Exception($message, 0, $e); + } + } } diff --git a/app/code/Magento/Quote/Model/QuoteRepository.php b/app/code/Magento/Quote/Model/QuoteRepository.php index 01c21bbbe50a..30931821ddc7 100644 --- a/app/code/Magento/Quote/Model/QuoteRepository.php +++ b/app/code/Magento/Quote/Model/QuoteRepository.php @@ -3,27 +3,33 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Quote\Model; +use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; +use Magento\Framework\Api\Search\FilterGroup; +use Magento\Framework\Api\SearchCriteria\CollectionProcessor; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; +use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\App\ObjectManager; -use Magento\Framework\Api\SortOrder; +use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Quote\Api\CartRepositoryInterface; use Magento\Quote\Api\Data\CartInterface; -use Magento\Quote\Model\Quote; -use Magento\Store\Model\StoreManagerInterface; -use Magento\Framework\Api\Search\FilterGroup; -use Magento\Quote\Model\ResourceModel\Quote\Collection as QuoteCollection; -use Magento\Quote\Model\ResourceModel\Quote\CollectionFactory as QuoteCollectionFactory; -use Magento\Framework\Exception\InputException; -use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; +use Magento\Quote\Api\Data\CartInterfaceFactory; +use Magento\Quote\Api\Data\CartSearchResultsInterfaceFactory; use Magento\Quote\Model\QuoteRepository\SaveHandler; use Magento\Quote\Model\QuoteRepository\LoadHandler; +use Magento\Quote\Model\ResourceModel\Quote\Collection as QuoteCollection; +use Magento\Quote\Model\ResourceModel\Quote\CollectionFactory as QuoteCollectionFactory; +use Magento\Store\Model\StoreManagerInterface; /** + * Quote repository. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class QuoteRepository implements \Magento\Quote\Api\CartRepositoryInterface +class QuoteRepository implements CartRepositoryInterface { /** * @var Quote[] @@ -37,6 +43,7 @@ class QuoteRepository implements \Magento\Quote\Api\CartRepositoryInterface /** * @var QuoteFactory + * @deprecated */ protected $quoteFactory; @@ -46,13 +53,13 @@ class QuoteRepository implements \Magento\Quote\Api\CartRepositoryInterface protected $storeManager; /** - * @var \Magento\Quote\Model\ResourceModel\Quote\Collection + * @var QuoteCollection * @deprecated 100.2.0 */ protected $quoteCollection; /** - * @var \Magento\Quote\Api\Data\CartSearchResultsInterfaceFactory + * @var CartSearchResultsInterfaceFactory */ protected $searchResultsDataFactory; @@ -77,43 +84,51 @@ class QuoteRepository implements \Magento\Quote\Api\CartRepositoryInterface private $collectionProcessor; /** - * @var \Magento\Quote\Model\ResourceModel\Quote\CollectionFactory + * @var QuoteCollectionFactory */ private $quoteCollectionFactory; + /** + * @var CartInterfaceFactory + */ + private $cartFactory; + /** * Constructor * * @param QuoteFactory $quoteFactory * @param StoreManagerInterface $storeManager - * @param \Magento\Quote\Model\ResourceModel\Quote\Collection $quoteCollection - * @param \Magento\Quote\Api\Data\CartSearchResultsInterfaceFactory $searchResultsDataFactory + * @param QuoteCollection $quoteCollection + * @param CartSearchResultsInterfaceFactory $searchResultsDataFactory * @param JoinProcessorInterface $extensionAttributesJoinProcessor * @param CollectionProcessorInterface|null $collectionProcessor - * @param \Magento\Quote\Model\ResourceModel\Quote\CollectionFactory|null $quoteCollectionFactory + * @param QuoteCollectionFactory|null $quoteCollectionFactory + * @param CartInterfaceFactory|null $cartFactory * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( QuoteFactory $quoteFactory, StoreManagerInterface $storeManager, - \Magento\Quote\Model\ResourceModel\Quote\Collection $quoteCollection, - \Magento\Quote\Api\Data\CartSearchResultsInterfaceFactory $searchResultsDataFactory, + QuoteCollection $quoteCollection, + CartSearchResultsInterfaceFactory $searchResultsDataFactory, JoinProcessorInterface $extensionAttributesJoinProcessor, CollectionProcessorInterface $collectionProcessor = null, - \Magento\Quote\Model\ResourceModel\Quote\CollectionFactory $quoteCollectionFactory = null + QuoteCollectionFactory $quoteCollectionFactory = null, + CartInterfaceFactory $cartFactory = null ) { $this->quoteFactory = $quoteFactory; $this->storeManager = $storeManager; $this->searchResultsDataFactory = $searchResultsDataFactory; $this->extensionAttributesJoinProcessor = $extensionAttributesJoinProcessor; - $this->collectionProcessor = $collectionProcessor ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Framework\Api\SearchCriteria\CollectionProcessor::class); - $this->quoteCollectionFactory = $quoteCollectionFactory ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Quote\Model\ResourceModel\Quote\CollectionFactory::class); + $this->collectionProcessor = $collectionProcessor ?: ObjectManager::getInstance() + ->get(CollectionProcessor::class); + $this->quoteCollectionFactory = $quoteCollectionFactory ?: ObjectManager::getInstance() + ->get(QuoteCollectionFactory::class); + $this->cartFactory = $cartFactory ?: ObjectManager::getInstance()->get(CartInterfaceFactory::class); } /** - * {@inheritdoc} + * @inheritdoc */ public function get($cartId, array $sharedStoreIds = []) { @@ -126,7 +141,7 @@ public function get($cartId, array $sharedStoreIds = []) } /** - * {@inheritdoc} + * @inheritdoc */ public function getForCustomer($customerId, array $sharedStoreIds = []) { @@ -140,7 +155,7 @@ public function getForCustomer($customerId, array $sharedStoreIds = []) } /** - * {@inheritdoc} + * @inheritdoc */ public function getActive($cartId, array $sharedStoreIds = []) { @@ -152,7 +167,7 @@ public function getActive($cartId, array $sharedStoreIds = []) } /** - * {@inheritdoc} + * @inheritdoc */ public function getActiveForCustomer($customerId, array $sharedStoreIds = []) { @@ -164,9 +179,9 @@ public function getActiveForCustomer($customerId, array $sharedStoreIds = []) } /** - * {@inheritdoc} + * @inheritdoc */ - public function save(\Magento\Quote\Api\Data\CartInterface $quote) + public function save(CartInterface $quote) { if ($quote->getId()) { $currentQuote = $this->get($quote->getId(), [$quote->getStoreId()]); @@ -184,9 +199,9 @@ public function save(\Magento\Quote\Api\Data\CartInterface $quote) } /** - * {@inheritdoc} + * @inheritdoc */ - public function delete(\Magento\Quote\Api\Data\CartInterface $quote) + public function delete(CartInterface $quote) { $quoteId = $quote->getId(); $customerId = $quote->getCustomerId(); @@ -203,13 +218,13 @@ public function delete(\Magento\Quote\Api\Data\CartInterface $quote) * @param int $identifier * @param int[] $sharedStoreIds * @throws NoSuchEntityException - * @return Quote + * @return CartInterface */ protected function loadQuote($loadMethod, $loadField, $identifier, array $sharedStoreIds = []) { - /** @var Quote $quote */ - $quote = $this->quoteFactory->create(); - if ($sharedStoreIds) { + /** @var CartInterface $quote */ + $quote = $this->cartFactory->create(); + if ($sharedStoreIds && method_exists($quote, 'setSharedStoreIds')) { $quote->setSharedStoreIds($sharedStoreIds); } $quote->setStoreId($this->storeManager->getStore()->getId())->$loadMethod($identifier); @@ -220,9 +235,9 @@ protected function loadQuote($loadMethod, $loadField, $identifier, array $shared } /** - * {@inheritdoc} + * @inheritdoc */ - public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria) + public function getList(SearchCriteriaInterface $searchCriteria) { $this->quoteCollection = $this->quoteCollectionFactory->create(); /** @var \Magento\Quote\Api\Data\CartSearchResultsInterface $searchData */ @@ -265,6 +280,7 @@ protected function addFilterGroupToCollection(FilterGroup $filterGroup, QuoteCol /** * Get new SaveHandler dependency for application code. + * * @return SaveHandler * @deprecated 100.1.0 */ @@ -277,6 +293,8 @@ private function getSaveHandler() } /** + * Get load handler instance. + * * @return LoadHandler * @deprecated 100.1.0 */ diff --git a/app/code/Magento/Quote/Model/QuoteValidator.php b/app/code/Magento/Quote/Model/QuoteValidator.php index 04d6d4ecba16..e67a0f135626 100644 --- a/app/code/Magento/Quote/Model/QuoteValidator.php +++ b/app/code/Magento/Quote/Model/QuoteValidator.php @@ -6,14 +6,17 @@ namespace Magento\Quote\Model; -use Magento\Framework\Exception\LocalizedException; -use Magento\Quote\Model\Quote as QuoteEntity; use Magento\Directory\Model\AllowedCountries; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Message\Error; +use Magento\Quote\Model\Quote as QuoteEntity; use Magento\Quote\Model\Quote\Validator\MinimumOrderAmount\ValidationMessage as OrderAmountValidationMessage; use Magento\Quote\Model\ValidationRules\QuoteValidationRuleInterface; /** + * Class to validate the quote + * * @api * @since 100.0.2 */ @@ -22,7 +25,7 @@ class QuoteValidator /** * Maximum available number */ - const MAXIMUM_AVAILABLE_NUMBER = 99999999; + const MAXIMUM_AVAILABLE_NUMBER = 10000000000000000; /** * @var AllowedCountries @@ -72,18 +75,24 @@ public function validateQuoteAmount(QuoteEntity $quote, $amount) $quote->setHasError(true); $quote->addMessage(__('This item price or quantity is not valid for checkout.')); } + return $this; } /** - * Validate quote before submit + * Validates quote before submit. * * @param Quote $quote * @return $this - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function validateBeforeSubmit(QuoteEntity $quote) { + if ($quote->getHasError()) { + $errors = $this->getQuoteErrors($quote); + throw new LocalizedException(__($errors ?: 'Something went wrong. Please try to place the order again.')); + } + foreach ($this->quoteValidationRule->validate($quote) as $validationResult) { if ($validationResult->isValid()) { continue; @@ -101,4 +110,22 @@ public function validateBeforeSubmit(QuoteEntity $quote) return $this; } + + /** + * Parses quote error messages and concatenates them into single string. + * + * @param Quote $quote + * @return string + */ + private function getQuoteErrors(QuoteEntity $quote): string + { + $errors = array_map( + function (Error $error) { + return $error->getText(); + }, + $quote->getErrors() + ); + + return implode(PHP_EOL, $errors); + } } diff --git a/app/code/Magento/Quote/Model/ResourceModel/Quote.php b/app/code/Magento/Quote/Model/ResourceModel/Quote.php index 946c0e0c5f3b..ae26407c7452 100644 --- a/app/code/Magento/Quote/Model/ResourceModel/Quote.php +++ b/app/code/Magento/Quote/Model/ResourceModel/Quote.php @@ -23,8 +23,8 @@ class Quote extends AbstractDb /** * @param \Magento\Framework\Model\ResourceModel\Db\Context $context - * @param Snapshot $entitySnapshot, - * @param RelationComposite $entityRelationComposite, + * @param Snapshot $entitySnapshot + * @param RelationComposite $entityRelationComposite * @param \Magento\SalesSequence\Model\Manager $sequenceManager * @param string $connectionName */ @@ -296,7 +296,7 @@ public function markQuotesRecollect($productIds) } /** - * {@inheritdoc} + * @inheritdoc */ public function save(\Magento\Framework\Model\AbstractModel $object) { diff --git a/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php b/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php index c7525754dffd..392a815ed963 100644 --- a/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php +++ b/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php @@ -3,9 +3,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Quote\Model\ResourceModel\Quote\Item; -use \Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; +use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Item as QuoteItem; +use Magento\Quote\Model\ResourceModel\Quote\Item as ResourceQuoteItem; /** * Quote item resource collection @@ -50,6 +57,11 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\VersionContro */ private $storeManager; + /** + * @var bool $recollectQuote + */ + private $recollectQuote = false; + /** * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory * @param \Psr\Log\LoggerInterface $logger @@ -102,7 +114,7 @@ public function __construct( */ protected function _construct() { - $this->_init(\Magento\Quote\Model\Quote\Item::class, \Magento\Quote\Model\ResourceModel\Quote\Item::class); + $this->_init(QuoteItem::class, ResourceQuoteItem::class); } /** @@ -110,7 +122,7 @@ protected function _construct() * * @return int */ - public function getStoreId() + public function getStoreId(): int { // Fallback to current storeId if no quote is provided // (see https://github.com/magento/magento2/commit/9d3be732a88884a66d667b443b3dc1655ddd0721) @@ -119,12 +131,12 @@ public function getStoreId() } /** - * Set Quote object to Collection + * Set Quote object to Collection. * - * @param \Magento\Quote\Model\Quote $quote + * @param Quote $quote * @return $this */ - public function setQuote($quote) + public function setQuote($quote): self { $this->_quote = $quote; $quoteId = $quote->getId(); @@ -138,14 +150,15 @@ public function setQuote($quote) } /** - * Reset the collection and inner join it to quotes table + * Reset the collection and inner join it to quotes table. + * * Optionally can select items with specified product id only * * @param string $quotesTableName * @param int $productId * @return $this */ - public function resetJoinQuotes($quotesTableName, $productId = null) + public function resetJoinQuotes($quotesTableName, $productId = null): self { $this->getSelect()->reset()->from( ['qi' => $this->getResource()->getMainTable()], @@ -162,11 +175,11 @@ public function resetJoinQuotes($quotesTableName, $productId = null) } /** - * After load processing + * After load processing. * * @return $this */ - protected function _afterLoad() + protected function _afterLoad(): self { parent::_afterLoad(); @@ -195,11 +208,11 @@ protected function _afterLoad() } /** - * Add options to items + * Add options to items. * * @return $this */ - protected function _assignOptions() + protected function _assignOptions(): self { $itemIds = array_keys($this->_items); $optionCollection = $this->_itemOptionCollectionFactory->create()->addItemFilter($itemIds); @@ -213,12 +226,12 @@ protected function _assignOptions() } /** - * Add products to items and item options + * Add products to items and item options. * * @return $this * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - protected function _assignProducts() + protected function _assignProducts(): self { \Magento\Framework\Profiler::start('QUOTE:' . __METHOD__, ['group' => 'QUOTE', 'method' => __METHOD__]); $productCollection = $this->_productCollectionFactory->create()->setStoreId( @@ -230,7 +243,6 @@ protected function _assignProducts() ); $this->skipStockStatusFilter($productCollection); $productCollection->addOptionsToResult()->addStoreFilter()->addUrlRewrite(); - $this->addTierPriceData($productCollection); $this->_eventManager->dispatch( 'prepare_catalog_product_collection_prices', @@ -241,46 +253,29 @@ protected function _assignProducts() ['collection' => $productCollection] ); - $recollectQuote = false; foreach ($this as $item) { + /** @var ProductInterface $product */ $product = $productCollection->getItemById($item->getProductId()); - if ($product) { + $qtyOptions = []; + if ($product && $this->isValidProduct($product)) { $product->setCustomOptions([]); - $qtyOptions = []; - $optionProductIds = []; - foreach ($item->getOptions() as $option) { - /** - * Call type-specific logic for product associated with quote item - */ - $product->getTypeInstance()->assignProductToOption( - $productCollection->getItemById($option->getProductId()), - $option, - $product - ); - - if (is_object($option->getProduct()) && $option->getProduct()->getId() != $product->getId()) { - $optionProductIds[$option->getProduct()->getId()] = $option->getProduct()->getId(); + $optionProductIds = $this->getOptionProductIds($item, $product, $productCollection); + foreach ($optionProductIds as $optionProductId) { + $qtyOption = $item->getOptionByCode('product_qty_' . $optionProductId); + if ($qtyOption) { + $qtyOptions[$optionProductId] = $qtyOption; } } - - if ($optionProductIds) { - foreach ($optionProductIds as $optionProductId) { - $qtyOption = $item->getOptionByCode('product_qty_' . $optionProductId); - if ($qtyOption) { - $qtyOptions[$optionProductId] = $qtyOption; - } - } - } - - $item->setQtyOptions($qtyOptions)->setProduct($product); } else { $item->isDeleted(true); - $recollectQuote = true; + $this->recollectQuote = true; + } + if (!$item->isDeleted()) { + $item->setQtyOptions($qtyOptions)->setProduct($product); + $item->checkData(); } - $item->checkData(); } - - if ($recollectQuote && $this->_quote) { + if ($this->recollectQuote && $this->_quote) { $this->_quote->collectTotals(); } \Magento\Framework\Profiler::stop('QUOTE:' . __METHOD__); @@ -289,31 +284,67 @@ protected function _assignProducts() } /** - * Prevents adding stock status filter to the collection of products. + * Get product Ids from option. * + * @param QuoteItem $item + * @param ProductInterface $product * @param ProductCollection $productCollection - * @return void + * @return array + */ + private function getOptionProductIds( + QuoteItem $item, + ProductInterface $product, + ProductCollection $productCollection + ): array { + $optionProductIds = []; + foreach ($item->getOptions() as $option) { + /** + * Call type-specific logic for product associated with quote item + */ + $product->getTypeInstance()->assignProductToOption( + $productCollection->getItemById($option->getProductId()), + $option, + $product + ); + + if (is_object($option->getProduct()) && $option->getProduct()->getId() != $product->getId()) { + $isValidProduct = $this->isValidProduct($option->getProduct()); + if (!$isValidProduct && !$item->isDeleted()) { + $item->isDeleted(true); + $this->recollectQuote = true; + continue; + } + $optionProductIds[$option->getProduct()->getId()] = $option->getProduct()->getId(); + } + } + + return $optionProductIds; + } + + /** + * Check is valid product. * - * @see \Magento\CatalogInventory\Helper\Stock::addIsInStockFilterToCollection + * @param ProductInterface $product + * @return bool */ - private function skipStockStatusFilter(ProductCollection $productCollection) + private function isValidProduct(ProductInterface $product): bool { - $productCollection->setFlag('has_stock_status_filter', true); + $result = ($product && (int)$product->getStatus() !== ProductStatus::STATUS_DISABLED); + + return $result; } /** - * Add tier prices to product collection. + * Prevents adding stock status filter to the collection of products. * * @param ProductCollection $productCollection * @return void + * + * @see \Magento\CatalogInventory\Helper\Stock::addIsInStockFilterToCollection */ - private function addTierPriceData(ProductCollection $productCollection) + private function skipStockStatusFilter(ProductCollection $productCollection): void { - if (empty($this->_quote)) { - $productCollection->addTierPriceData(); - } else { - $productCollection->addTierPriceDataByGroupId($this->_quote->getCustomerGroupId()); - } + $productCollection->setFlag('has_stock_status_filter', true); } /** @@ -321,7 +352,7 @@ private function addTierPriceData(ProductCollection $productCollection) * * @return void */ - private function removeItemsWithAbsentProducts() + private function removeItemsWithAbsentProducts(): void { if (count($this->_productIds) === 0) { return; diff --git a/app/code/Magento/Quote/Model/ShippingAddressManagement.php b/app/code/Magento/Quote/Model/ShippingAddressManagement.php index d8e70c68ba33..b9edcc13d007 100644 --- a/app/code/Magento/Quote/Model/ShippingAddressManagement.php +++ b/app/code/Magento/Quote/Model/ShippingAddressManagement.php @@ -79,7 +79,7 @@ public function __construct( } /** - * {@inheritDoc} + * @inheritDoc * @SuppressWarnings(PHPMD.NPathComplexity) */ public function assign($cartId, \Magento\Quote\Api\Data\AddressInterface $address) @@ -95,7 +95,7 @@ public function assign($cartId, \Magento\Quote\Api\Data\AddressInterface $addres $saveInAddressBook = $address->getSaveInAddressBook() ? 1 : 0; $sameAsBilling = $address->getSameAsBilling() ? 1 : 0; $customerAddressId = $address->getCustomerAddressId(); - $this->addressValidator->validate($address); + $this->addressValidator->validateForCart($quote, $address); $quote->setShippingAddress($address); $address = $quote->getShippingAddress(); @@ -123,7 +123,7 @@ public function assign($cartId, \Magento\Quote\Api\Data\AddressInterface $addres } /** - * {@inheritDoc} + * @inheritDoc */ public function get($cartId) { diff --git a/app/code/Magento/Quote/Model/ValidationRules/AllowedCountryValidationRule.php b/app/code/Magento/Quote/Model/ValidationRules/AllowedCountryValidationRule.php index 840e94e3ece1..2498e9976f00 100644 --- a/app/code/Magento/Quote/Model/ValidationRules/AllowedCountryValidationRule.php +++ b/app/code/Magento/Quote/Model/ValidationRules/AllowedCountryValidationRule.php @@ -10,6 +10,7 @@ use Magento\Directory\Model\AllowedCountries; use Magento\Framework\Validation\ValidationResultFactory; use Magento\Quote\Model\Quote; +use Magento\Store\Model\ScopeInterface; /** * @inheritdoc @@ -54,10 +55,15 @@ public function validate(Quote $quote): array $validationErrors = []; if (!$quote->isVirtual()) { + $shippingAddress = $quote->getShippingAddress(); + $shippingAddress->setStoreId($quote->getStoreId()); $validationResult = in_array( - $quote->getShippingAddress()->getCountryId(), - $this->allowedCountryReader->getAllowedCountries() + $shippingAddress->getCountryId(), + $this->allowedCountryReader->getAllowedCountries( + ScopeInterface::SCOPE_STORE, + $quote->getStoreId() + ) ); if (!$validationResult) { $validationErrors = [__($this->generalMessage)]; diff --git a/app/code/Magento/Quote/Model/ValidationRules/BillingAddressValidationRule.php b/app/code/Magento/Quote/Model/ValidationRules/BillingAddressValidationRule.php index fbacbf1c8d30..465aebdc418e 100644 --- a/app/code/Magento/Quote/Model/ValidationRules/BillingAddressValidationRule.php +++ b/app/code/Magento/Quote/Model/ValidationRules/BillingAddressValidationRule.php @@ -43,7 +43,9 @@ public function __construct( public function validate(Quote $quote): array { $validationErrors = []; - $validationResult = $quote->getBillingAddress()->validate(); + $billingAddress = $quote->getBillingAddress(); + $billingAddress->setStoreId($quote->getStoreId()); + $validationResult = $billingAddress->validate(); if ($validationResult !== true) { $validationErrors = [__($this->generalMessage)]; } diff --git a/app/code/Magento/Quote/Model/ValidationRules/ShippingAddressValidationRule.php b/app/code/Magento/Quote/Model/ValidationRules/ShippingAddressValidationRule.php index f5eebe241acc..2f215c17e6d7 100644 --- a/app/code/Magento/Quote/Model/ValidationRules/ShippingAddressValidationRule.php +++ b/app/code/Magento/Quote/Model/ValidationRules/ShippingAddressValidationRule.php @@ -45,7 +45,9 @@ public function validate(Quote $quote): array $validationErrors = []; if (!$quote->isVirtual()) { - $validationResult = $quote->getShippingAddress()->validate(); + $shippingAddress = $quote->getShippingAddress(); + $shippingAddress->setStoreId($quote->getStoreId()); + $validationResult = $shippingAddress->validate(); if ($validationResult !== true) { $validationErrors = [__($this->generalMessage)]; } diff --git a/app/code/Magento/Quote/Model/ValidationRules/ShippingMethodValidationRule.php b/app/code/Magento/Quote/Model/ValidationRules/ShippingMethodValidationRule.php index 6df7f663b063..3ef079f5a019 100644 --- a/app/code/Magento/Quote/Model/ValidationRules/ShippingMethodValidationRule.php +++ b/app/code/Magento/Quote/Model/ValidationRules/ShippingMethodValidationRule.php @@ -45,8 +45,10 @@ public function validate(Quote $quote): array $validationErrors = []; if (!$quote->isVirtual()) { - $shippingMethod = $quote->getShippingAddress()->getShippingMethod(); - $shippingRate = $quote->getShippingAddress()->getShippingRateByCode($shippingMethod); + $shippingAddress = $quote->getShippingAddress(); + $shippingAddress->setStoreId($quote->getStoreId()); + $shippingMethod = $shippingAddress->getShippingMethod(); + $shippingRate = $shippingAddress->getShippingRateByCode($shippingMethod); $validationResult = $shippingMethod && $shippingRate; if (!$validationResult) { $validationErrors = [__($this->generalMessage)]; diff --git a/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php b/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php index 76c9a49b290c..77dfec9603a5 100644 --- a/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php +++ b/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php @@ -7,6 +7,11 @@ use Magento\Framework\Event\ObserverInterface; +/** + * Handle customer VAT number on collect_totals_before event of quote address. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ class CollectTotalsObserver implements ObserverInterface { /** @@ -124,7 +129,7 @@ public function execute(\Magento\Framework\Event\Observer $observer) ); } - if ($groupId) { + if ($groupId !== null) { $address->setPrevQuoteCustomerGroupId($quote->getCustomerGroupId()); $quote->setCustomerGroupId($groupId); $this->customerSession->setCustomerGroupId($groupId); diff --git a/app/code/Magento/Quote/Plugin/RecollectOnGroupChange.php b/app/code/Magento/Quote/Plugin/RecollectOnGroupChange.php new file mode 100644 index 000000000000..03b19c6e661a --- /dev/null +++ b/app/code/Magento/Quote/Plugin/RecollectOnGroupChange.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Plugin; + +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Customer\Model\ResourceModel\Customer as CustomerResource; +use Magento\Customer\Model\Customer; +use Magento\Framework\Model\AbstractModel; + +/** + * Recollect quote totals after change customer group + */ +class RecollectOnGroupChange +{ + /** + * @var CartRepositoryInterface + */ + private $cartRepository; + + /** + * @param CartRepositoryInterface $cartRepository + */ + public function __construct( + CartRepositoryInterface $cartRepository + ) { + $this->cartRepository = $cartRepository; + } + + /** + * Recollect totals if customer group change + * + * @param CustomerResource $subject + * @param CustomerResource $result + * @param AbstractModel $customer + * @return CustomerResource + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterSave(CustomerResource $subject, CustomerResource $result, AbstractModel $customer) + { + /** @var Customer $customer */ + if ($customer->getOrigData('group_id') !== null + && $customer->getOrigData('group_id') != $customer->getGroupId() + ) { + try { + /** @var \Magento\Quote\Model\Quote $quote */ + $quote = $this->cartRepository->getActiveForCustomer($customer->getId()); + $quote->setCustomerGroupId($customer->getGroupId()); + $quote->collectTotals(); + $this->cartRepository->save($quote); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + //no active cart for customer + } + } + + return $result; + } +} diff --git a/app/code/Magento/Quote/Plugin/UpdateQuoteItemStore.php b/app/code/Magento/Quote/Plugin/UpdateQuoteItemStore.php new file mode 100644 index 000000000000..19a7e03264d8 --- /dev/null +++ b/app/code/Magento/Quote/Plugin/UpdateQuoteItemStore.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Plugin; + +use Magento\Checkout\Model\Session; +use Magento\Quote\Model\QuoteRepository; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreSwitcherInterface; + +/** + * Updates quote items store id. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class UpdateQuoteItemStore +{ + /** + * @var QuoteRepository + */ + private $quoteRepository; + + /** + * @var Session + */ + private $checkoutSession; + + /** + * @param QuoteRepository $quoteRepository + * @param Session $checkoutSession + */ + public function __construct( + QuoteRepository $quoteRepository, + Session $checkoutSession + ) { + $this->quoteRepository = $quoteRepository; + $this->checkoutSession = $checkoutSession; + } + + /** + * Update store id in active quote after store view switching. + * + * @param StoreSwitcherInterface $subject + * @param string $result + * @param StoreInterface $fromStore store where we came from + * @param StoreInterface $targetStore store where to go to + * @param string $redirectUrl original url requested for redirect after switching + * @return string url to be redirected after switching + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterSwitch( + StoreSwitcherInterface $subject, + $result, + StoreInterface $fromStore, + StoreInterface $targetStore, + string $redirectUrl + ): string { + $quote = $this->checkoutSession->getQuote(); + if ($quote->getIsActive()) { + $quote->setStoreId( + $targetStore->getId() + ); + $quote->getItemsCollection(false); + $this->quoteRepository->save($quote); + } + return $result; + } +} diff --git a/app/code/Magento/Quote/Setup/Patch/Data/ConvertSerializedDataToJson.php b/app/code/Magento/Quote/Setup/Patch/Data/ConvertSerializedDataToJson.php index f53728027222..6c23379a37cf 100644 --- a/app/code/Magento/Quote/Setup/Patch/Data/ConvertSerializedDataToJson.php +++ b/app/code/Magento/Quote/Setup/Patch/Data/ConvertSerializedDataToJson.php @@ -3,18 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Quote\Setup\Patch\Data; -use Magento\Framework\App\ResourceConnection; use Magento\Quote\Setup\ConvertSerializedDataToJsonFactory; use Magento\Quote\Setup\QuoteSetupFactory; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; /** - * Class ConvertSerializedDataToJson - * @package Magento\Quote\Setup\Patch + * Convert quote serialized data to json. */ class ConvertSerializedDataToJson implements DataPatchInterface, PatchVersionInterface { @@ -36,6 +33,8 @@ class ConvertSerializedDataToJson implements DataPatchInterface, PatchVersionInt /** * PatchInitial constructor. * @param \Magento\Framework\Setup\ModuleDataSetupInterface $moduleDataSetup + * @param QuoteSetupFactory $quoteSetupFactory + * @param ConvertSerializedDataToJsonFactory $convertSerializedDataToJsonFactory */ public function __construct( \Magento\Framework\Setup\ModuleDataSetupInterface $moduleDataSetup, @@ -48,7 +47,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { @@ -57,7 +56,7 @@ public function apply() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -67,7 +66,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -75,7 +74,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml b/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml new file mode 100644 index 000000000000..4ec608a18a68 --- /dev/null +++ b/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml @@ -0,0 +1,151 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontGuestCheckoutDisabledProductTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout via the Storefront"/> + <title value="Remove item from cart if product disabled"/> + <description value="Remove item from cart if simple or configurable product is disabled"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-95857"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <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"/> + </createData> + + <!-- Make the configurable product have two options, that are children of the default attribute set --> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create the 2 children that will be a part of the configurable product --> + <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + + <!-- Assign the two products to the configurable product --> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + </before> + <after> + <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 --> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.name$$)}}" stepKey="amOnSimpleProductPage"/> + <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"/> + <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"/> + <!--Disabled via admin panel--> + <openNewTab stepKey="openNewTab"/> + <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"/> + <actionGroup ref="filterProductGridBySku" stepKey="findCreatedProduct"> + <argument name="product" value="$$createConfigChildProduct1$$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForFiltersToBeApplied"/> + <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <!-- Disabled child configurable product --> + <click selector="{{AdminProductFormSection.enableProductAttributeLabel}}" stepKey="clickDisableProduct"/> + <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 --> + <reloadPage stepKey="reloadPage"/> + <waitForPageLoad stepKey="waitForCheckoutPageReload"/> + <click selector="{{StorefrontMiniCartSection.show}}" stepKey="clickMiniCart"/> + <dontSeeElement selector="{{StorefrontMiniCartSection.quantity}}" stepKey="dontSeeCartItem"/> + <!-- Add simple product to shopping cart --> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct2.name$$)}}" stepKey="amOnSimpleProductPage2"/> + <actionGroup ref="AddSimpleProductToCart" stepKey="cartAddSimpleProductToCart2"> + <argument name="product" value="$$createSimpleProduct2$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCheckoutCartPage"/> + <!-- Disabled via admin panel --> + <openNewTab stepKey="openNewTab2"/> + <!-- Find the first simple product that we just created using the product grid and go to its page --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage2"/> + <actionGroup ref="filterProductGridBySku" stepKey="findCreatedProduct2"> + <argument name="product" value="$$createSimpleProduct2$$"/> + </actionGroup> + <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage2"/> + <waitForPageLoad stepKey="waitForProductPageLoad2"/> + <!-- Disabled simple product from grid --> + <actionGroup ref="ChangeStatusProductUsingProductGridActionGroup" stepKey="disabledProductFromGrid2"> + <argument name="product" value="$$createSimpleProduct2$$"/> + <argument name="status" value="Disable"/> + </actionGroup> + <closeTab stepKey="closeTab2"/> + <!--Check cart--> + <reloadPage stepKey="reloadPage2"/> + <waitForPageLoad stepKey="waitForCheckoutPageReload2"/> + <click selector="{{StorefrontMiniCartSection.show}}" stepKey="clickMiniCart2"/> + <dontSeeElement selector="{{StorefrontMiniCartSection.quantity}}" stepKey="dontSeeCartItem2"/> + </test> +</tests> diff --git a/app/code/Magento/Quote/Test/Unit/Model/Cart/CartTotalRepositoryTest.php b/app/code/Magento/Quote/Test/Unit/Model/Cart/CartTotalRepositoryTest.php index 1e999cb5e523..804f0863d2d2 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Cart/CartTotalRepositoryTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Cart/CartTotalRepositoryTest.php @@ -77,7 +77,8 @@ protected function setUp() 'getAllVisibleItems', 'getBaseCurrencyCode', 'getQuoteCurrencyCode', - 'getItemsQty' + 'getItemsQty', + 'collectTotals' ]); $this->quoteRepositoryMock = $this->createMock(\Magento\Quote\Api\CartRepositoryInterface::class); $this->addressMock = $this->createPartialMock( diff --git a/app/code/Magento/Quote/Test/Unit/Model/GuestCartManagement/Plugin/AuthorizationTest.php b/app/code/Magento/Quote/Test/Unit/Model/GuestCartManagement/Plugin/AuthorizationTest.php index 22962aacc8da..49ed8a10bee3 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/GuestCartManagement/Plugin/AuthorizationTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/GuestCartManagement/Plugin/AuthorizationTest.php @@ -36,7 +36,7 @@ protected function setUp() /** * @expectedException \Magento\Framework\Exception\StateException - * @expectedMessage You don't have the correct permissions to assign the customer to the cart. + * @expectedExceptionMessage You don't have the correct permissions to assign the customer to the cart. */ public function testBeforeAssignCustomer() { diff --git a/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php b/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php index c1c131260f17..242f81b22250 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php @@ -216,6 +216,7 @@ public function testValidateMinimumAmountVirtual() $scopeConfigValues = [ ['sales/minimum_order/active', ScopeInterface::SCOPE_STORE, $storeId, true], ['sales/minimum_order/amount', ScopeInterface::SCOPE_STORE, $storeId, 20], + ['sales/minimum_order/include_discount_amount', ScopeInterface::SCOPE_STORE, $storeId, true], ['sales/minimum_order/tax_including', ScopeInterface::SCOPE_STORE, $storeId, true], ]; @@ -240,6 +241,31 @@ public function testValidateMinimumAmount() $scopeConfigValues = [ ['sales/minimum_order/active', ScopeInterface::SCOPE_STORE, $storeId, true], ['sales/minimum_order/amount', ScopeInterface::SCOPE_STORE, $storeId, 20], + ['sales/minimum_order/include_discount_amount', ScopeInterface::SCOPE_STORE, $storeId, true], + ['sales/minimum_order/tax_including', ScopeInterface::SCOPE_STORE, $storeId, true], + ]; + + $this->quote->expects($this->once()) + ->method('getStoreId') + ->willReturn($storeId); + $this->quote->expects($this->once()) + ->method('getIsVirtual') + ->willReturn(false); + + $this->scopeConfig->expects($this->once()) + ->method('isSetFlag') + ->willReturnMap($scopeConfigValues); + + $this->assertTrue($this->address->validateMinimumAmount()); + } + + public function testValidateMiniumumAmountWithoutDiscount() + { + $storeId = 1; + $scopeConfigValues = [ + ['sales/minimum_order/active', ScopeInterface::SCOPE_STORE, $storeId, true], + ['sales/minimum_order/amount', ScopeInterface::SCOPE_STORE, $storeId, 20], + ['sales/minimum_order/include_discount_amount', ScopeInterface::SCOPE_STORE, $storeId, false], ['sales/minimum_order/tax_including', ScopeInterface::SCOPE_STORE, $storeId, true], ]; @@ -263,6 +289,7 @@ public function testValidateMinimumAmountNegative() $scopeConfigValues = [ ['sales/minimum_order/active', ScopeInterface::SCOPE_STORE, $storeId, true], ['sales/minimum_order/amount', ScopeInterface::SCOPE_STORE, $storeId, 20], + ['sales/minimum_order/include_discount_amount', ScopeInterface::SCOPE_STORE, $storeId, true], ['sales/minimum_order/tax_including', ScopeInterface::SCOPE_STORE, $storeId, true], ]; diff --git a/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/UpdaterTest.php b/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/UpdaterTest.php index af47f6276705..8e6a3723caa7 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/UpdaterTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/UpdaterTest.php @@ -67,7 +67,7 @@ protected function setUp() 'addOption', 'setCustomPrice', 'setOriginalCustomPrice', - 'unsetData', + 'setData', 'hasData', 'setIsQtyDecimal' ]); @@ -92,7 +92,7 @@ protected function setUp() /** * @expectedException \InvalidArgumentException - * @ExceptedExceptionMessage The qty value is required to update quote item. + * @expectedExceptionMessage The qty value is required to update quote item. */ public function testUpdateNoQty() { @@ -301,7 +301,7 @@ public function testUpdateUnsetCustomPrice() 'setProduct', 'getData', 'unsetData', - 'hasData' + 'hasData', ]); $buyRequestMock->expects($this->never())->method('setCustomPrice'); $buyRequestMock->expects($this->once())->method('getData')->will($this->returnValue([])); @@ -353,7 +353,11 @@ public function testUpdateUnsetCustomPrice() ->will($this->returnValue($buyRequestMock)); $this->itemMock->expects($this->exactly(2)) - ->method('unsetData'); + ->method('setData') + ->withConsecutive( + ['custom_price', null], + ['original_custom_price', null] + ); $this->itemMock->expects($this->once()) ->method('hasData') diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteAddressValidatorTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteAddressValidatorTest.php deleted file mode 100644 index 08f5f6a80856..000000000000 --- a/app/code/Magento/Quote/Test/Unit/Model/QuoteAddressValidatorTest.php +++ /dev/null @@ -1,128 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Quote\Test\Unit\Model; - -class QuoteAddressValidatorTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Quote\Model\QuoteAddressValidator - */ - protected $model; - - /** - * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager - */ - protected $objectManager; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $addressRepositoryMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $customerRepositoryMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $quoteAddressMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $customerSessionMock; - - protected function setUp() - { - $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - - $this->addressRepositoryMock = $this->createMock(\Magento\Customer\Api\AddressRepositoryInterface::class); - $this->quoteAddressMock = $this->createPartialMock( - \Magento\Quote\Model\Quote\Address::class, - ['getCustomerId', 'load', 'getId', '__wakeup'] - ); - $this->customerRepositoryMock = $this->createMock(\Magento\Customer\Api\CustomerRepositoryInterface::class); - $this->customerSessionMock = $this->createMock(\Magento\Customer\Model\Session::class); - $this->model = $this->objectManager->getObject( - \Magento\Quote\Model\QuoteAddressValidator::class, - [ - 'addressRepository' => $this->addressRepositoryMock, - 'customerRepository' => $this->customerRepositoryMock, - 'customerSession' => $this->customerSessionMock - ] - ); - } - - /** - * @expectedException \Magento\Framework\Exception\NoSuchEntityException - * @expectedExceptionMessage Invalid customer id 100 - */ - public function testValidateInvalidCustomer() - { - $customerId = 100; - $address = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); - $customerMock = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); - - $address->expects($this->atLeastOnce())->method('getCustomerId')->willReturn($customerId); - $this->customerRepositoryMock->expects($this->once())->method('getById')->with($customerId) - ->willReturn($customerMock); - $this->model->validate($address); - } - - /** - * @expectedException \Magento\Framework\Exception\NoSuchEntityException - * @expectedExceptionMessage Invalid address id 101 - */ - public function testValidateInvalidAddress() - { - $address = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); - $this->customerRepositoryMock->expects($this->never())->method('getById'); - $address->expects($this->atLeastOnce())->method('getCustomerAddressId')->willReturn(101); - $address->expects($this->once())->method('getId')->willReturn(101); - - $this->addressRepositoryMock->expects($this->once())->method('getById') - ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException()); - - $this->model->validate($address); - } - - /** - * Neither customer id used nor address id exists - */ - public function testValidateNewAddress() - { - $this->customerRepositoryMock->expects($this->never())->method('getById'); - $this->addressRepositoryMock->expects($this->never())->method('getById'); - $address = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); - $this->assertTrue($this->model->validate($address)); - } - - public function testValidateWithValidAddress() - { - $addressCustomer = 100; - $customerAddressId = 42; - - $address = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); - $address->expects($this->atLeastOnce())->method('getCustomerId')->willReturn($addressCustomer); - $address->expects($this->atLeastOnce())->method('getCustomerAddressId')->willReturn($customerAddressId); - $customerMock = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); - $customerAddress = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); - - $this->customerRepositoryMock->expects($this->exactly(2))->method('getById')->willReturn($customerMock); - $customerMock->expects($this->once())->method('getId')->willReturn($addressCustomer); - - $this->addressRepositoryMock->expects($this->once())->method('getById')->willReturn($this->quoteAddressMock); - $this->quoteAddressMock->expects($this->any())->method('getCustomerId')->willReturn($addressCustomer); - - $customerMock->expects($this->once())->method('getAddresses')->willReturn([$customerAddress]); - $customerAddress->expects($this->once())->method('getId')->willReturn(42); - - $this->assertTrue($this->model->validate($address)); - } -} diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php index 107445bb18d2..8d8200cd6ef6 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php @@ -6,9 +6,12 @@ namespace Magento\Quote\Test\Unit\Model; +use Magento\Framework\App\RequestInterface; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\HTTP\PhpEnvironment\RemoteAddress; use Magento\Quote\Model\CustomerManagement; +use Magento\Quote\Model\QuoteIdMaskFactory; use Magento\Sales\Api\Data\OrderAddressInterface; /** @@ -137,6 +140,21 @@ class QuoteManagementTest extends \PHPUnit\Framework\TestCase */ private $quoteFactoryMock; + /** + * @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $requestMock; + + /** + * @var \Magento\Framework\HTTP\PhpEnvironment\RemoteAddress|\PHPUnit_Framework_MockObject_MockObject + */ + private $remoteAddressMock; + + /** + * @var \Magento\Quote\Model\QuoteIdMaskFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $quoteIdMaskFactoryMock; + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -178,17 +196,20 @@ protected function setUp() ); $this->quoteMock = $this->createPartialMock(\Magento\Quote\Model\Quote::class, [ - 'getId', - 'getCheckoutMethod', - 'setCheckoutMethod', - 'setCustomerId', - 'setCustomerEmail', - 'getBillingAddress', - 'setCustomerIsGuest', - 'setCustomerGroupId', - 'assignCustomer', - 'getPayment', - ]); + 'assignCustomer', + 'collectTotals', + 'getBillingAddress', + 'getCheckoutMethod', + 'getPayment', + 'setCheckoutMethod', + 'setCustomerEmail', + 'setCustomerGroupId', + 'setCustomerId', + 'setCustomerIsGuest', + 'setRemoteIp', + 'setXForwardedFor', + 'getId', + ]); $this->quoteAddressFactory = $this->createPartialMock( \Magento\Quote\Model\Quote\AddressFactory::class, @@ -236,8 +257,11 @@ protected function setUp() // Set the new dependency $this->quoteIdMock = $this->createMock(\Magento\Quote\Model\QuoteIdMask::class); - $quoteIdFactoryMock = $this->createPartialMock(\Magento\Quote\Model\QuoteIdMaskFactory::class, ['create']); - $this->setPropertyValue($this->model, 'quoteIdMaskFactory', $quoteIdFactoryMock); + $this->quoteIdMaskFactoryMock = $this->createPartialMock(QuoteIdMaskFactory::class, ['create']); + $this->setPropertyValue($this->model, 'quoteIdMaskFactory', $this->quoteIdMaskFactoryMock); + + $this->requestMock = $this->createPartialMockForAbstractClass(RequestInterface::class, ['getServer']); + $this->remoteAddressMock = $this->createMock(RemoteAddress::class); } public function testCreateEmptyCartAnonymous() @@ -644,7 +668,7 @@ public function testPlaceOrderIfCustomerIsGuest() $addressMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Address::class, ['getEmail']); $addressMock->expects($this->once())->method('getEmail')->willReturn($email); - $this->quoteMock->expects($this->once())->method('getBillingAddress')->with()->willReturn($addressMock); + $this->quoteMock->expects($this->any())->method('getBillingAddress')->with()->willReturn($addressMock); $this->quoteMock->expects($this->once())->method('setCustomerIsGuest')->with(true)->willReturnSelf(); $this->quoteMock->expects($this->once()) @@ -675,7 +699,11 @@ public function testPlaceOrderIfCustomerIsGuest() 'checkoutSession' => $this->checkoutSessionMock, 'customerSession' => $this->customerSessionMock, 'accountManagement' => $this->accountManagementMock, - 'quoteFactory' => $this->quoteFactoryMock + 'quoteFactory' => $this->quoteFactoryMock, + 'quoteIdMaskFactory' => $this->quoteIdMaskFactoryMock, + 'addressRepository' => $this->addressRepositoryMock, + 'request' => $this->requestMock, + 'remoteAddress' => $this->remoteAddressMock, ] ) ->getMock(); @@ -687,6 +715,7 @@ public function testPlaceOrderIfCustomerIsGuest() $service->expects($this->once())->method('submit')->willReturn($orderMock); $this->quoteMock->expects($this->atLeastOnce())->method('getId')->willReturn($cartId); + $this->quoteMock->expects($this->once())->method('collectTotals')->willReturnSelf(); $orderMock->expects($this->atLeastOnce())->method('getId')->willReturn($orderId); $orderMock->expects($this->atLeastOnce())->method('getIncrementId')->willReturn($orderIncrementId); @@ -707,13 +736,15 @@ public function testPlaceOrder() $orderId = 332; $orderIncrementId = 100003332; $orderStatus = 'status1'; + $remoteAddress = '192.168.1.10'; + $forwardedForIp = '192.168.1.11'; /** @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Quote\Model\QuoteManagement $service */ $service = $this->getMockBuilder(\Magento\Quote\Model\QuoteManagement::class) ->setMethods(['submit']) ->setConstructorArgs( [ - 'eventManager' => $this->eventManager, + 'eventManager' => $this->eventManager, 'quoteValidator' => $this->quoteValidator, 'orderFactory' => $this->orderFactory, 'orderManagement' => $this->orderManagement, @@ -732,7 +763,11 @@ public function testPlaceOrder() 'checkoutSession' => $this->checkoutSessionMock, 'customerSession' => $this->customerSessionMock, 'accountManagement' => $this->accountManagementMock, - 'quoteFactory' => $this->quoteFactoryMock + 'quoteFactory' => $this->quoteFactoryMock, + 'quoteIdMaskFactory' => $this->quoteIdMaskFactoryMock, + 'addressRepository' => $this->addressRepositoryMock, + 'request' => $this->requestMock, + 'remoteAddress' => $this->remoteAddressMock, ] ) ->getMock(); @@ -760,6 +795,17 @@ public function testPlaceOrder() ->method('setCustomerIsGuest') ->with(true); + $this->remoteAddressMock + ->method('getRemoteAddress') + ->willReturn($remoteAddress); + + $this->requestMock + ->method('getServer') + ->willReturn($forwardedForIp); + + $this->quoteMock->expects($this->once())->method('setRemoteIp')->with($remoteAddress); + $this->quoteMock->expects($this->once())->method('setXForwardedFor')->with($forwardedForIp); + $service->expects($this->once())->method('submit')->willReturn($orderMock); $this->quoteMock->expects($this->atLeastOnce())->method('getId')->willReturn($cartId); diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteRepositoryTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteRepositoryTest.php index 3101c7d0677a..095e1760df86 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/QuoteRepositoryTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/QuoteRepositoryTest.php @@ -5,17 +5,31 @@ */ namespace Magento\Quote\Test\Unit\Model; +use Magento\Framework\Api\SearchCriteria; +use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; use Magento\Framework\Api\SortOrder; use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Api\Data\CartInterfaceFactory; +use Magento\Quote\Api\Data\CartSearchResultsInterface; +use Magento\Quote\Api\Data\CartSearchResultsInterfaceFactory; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\QuoteRepository; use Magento\Quote\Model\QuoteRepository\LoadHandler; use Magento\Quote\Model\QuoteRepository\SaveHandler; +use Magento\Quote\Model\ResourceModel\Quote\Collection; use Magento\Quote\Model\ResourceModel\Quote\CollectionFactory; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use PHPUnit\Framework\TestCase; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyMethods) */ -class QuoteRepositoryTest extends \PHPUnit\Framework\TestCase +class QuoteRepositoryTest extends TestCase { /** * @var \Magento\Quote\Api\CartRepositoryInterface @@ -23,32 +37,32 @@ class QuoteRepositoryTest extends \PHPUnit\Framework\TestCase private $model; /** - * @var \Magento\Quote\Model\QuoteFactory|\PHPUnit_Framework_MockObject_MockObject + * @var CartInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject */ - private $quoteFactoryMock; + private $cartFactoryMock; /** - * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ private $storeManagerMock; /** - * @var \Magento\Store\Model\Store|\PHPUnit_Framework_MockObject_MockObject + * @var Store|\PHPUnit_Framework_MockObject_MockObject */ private $storeMock; /** - * @var \Magento\Quote\Model\Quote|\PHPUnit_Framework_MockObject_MockObject + * @var Quote|\PHPUnit_Framework_MockObject_MockObject */ private $quoteMock; /** - * @var \Magento\Quote\Api\Data\CartSearchResultsInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + * @var CartSearchResultsInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject */ private $searchResultsDataFactory; /** - * @var \Magento\Quote\Model\ResourceModel\Quote\Collection|\PHPUnit_Framework_MockObject_MockObject + * @var Collection|\PHPUnit_Framework_MockObject_MockObject */ private $quoteCollectionMock; @@ -78,21 +92,21 @@ class QuoteRepositoryTest extends \PHPUnit\Framework\TestCase private $objectManagerMock; /** - * @var \Magento\Quote\Model\ResourceModel\Quote\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject + * @var CollectionFactory|\PHPUnit_Framework_MockObject_MockObject */ private $quoteCollectionFactoryMock; protected function setUp() { - $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $objectManager = new ObjectManager($this); - $this->objectManagerMock = $this->createMock(\Magento\Framework\ObjectManagerInterface::class); + $this->objectManagerMock = $this->createMock(ObjectManagerInterface::class); \Magento\Framework\App\ObjectManager::setInstance($this->objectManagerMock); - $this->quoteFactoryMock = $this->createPartialMock(\Magento\Quote\Model\QuoteFactory::class, ['create']); - $this->storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); + $this->cartFactoryMock = $this->createPartialMock(CartInterfaceFactory::class, ['create']); + $this->storeManagerMock = $this->createMock(StoreManagerInterface::class); $this->quoteMock = $this->createPartialMock( - \Magento\Quote\Model\Quote::class, + Quote::class, [ 'load', 'loadByIdWithoutStore', @@ -108,35 +122,35 @@ protected function setUp() 'getData' ] ); - $this->storeMock = $this->createMock(\Magento\Store\Model\Store::class); + $this->storeMock = $this->createMock(Store::class); $this->searchResultsDataFactory = $this->createPartialMock( - \Magento\Quote\Api\Data\CartSearchResultsInterfaceFactory::class, + CartSearchResultsInterfaceFactory::class, ['create'] ); $this->quoteCollectionMock = - $this->createMock(\Magento\Quote\Model\ResourceModel\Quote\Collection::class); + $this->createMock(Collection::class); $this->extensionAttributesJoinProcessorMock = $this->createMock( - \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface::class + JoinProcessorInterface::class ); $this->collectionProcessor = $this->createMock( - \Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface::class + CollectionProcessorInterface::class ); $this->quoteCollectionFactoryMock = $this->createPartialMock( - \Magento\Quote\Model\ResourceModel\Quote\CollectionFactory::class, + CollectionFactory::class, ['create'] ); $this->model = $objectManager->getObject( - \Magento\Quote\Model\QuoteRepository::class, + QuoteRepository::class, [ - 'quoteFactory' => $this->quoteFactoryMock, 'storeManager' => $this->storeManagerMock, 'searchResultsDataFactory' => $this->searchResultsDataFactory, 'quoteCollection' => $this->quoteCollectionMock, 'extensionAttributesJoinProcessor' => $this->extensionAttributesJoinProcessorMock, 'collectionProcessor' => $this->collectionProcessor, - 'quoteCollectionFactory' => $this->quoteCollectionFactoryMock + 'quoteCollectionFactory' => $this->quoteCollectionFactoryMock, + 'cartFactory' => $this->cartFactoryMock ] ); @@ -161,7 +175,7 @@ public function testGetWithExceptionById() { $cartId = 14; - $this->quoteFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock); + $this->cartFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock); $this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($this->storeMock); $this->storeMock->expects($this->once())->method('getId')->willReturn(1); $this->quoteMock->expects($this->never())->method('setSharedStoreIds'); @@ -178,7 +192,7 @@ public function testGet() { $cartId = 15; - $this->quoteFactoryMock->expects(static::once()) + $this->cartFactoryMock->expects(static::once()) ->method('create') ->willReturn($this->quoteMock); $this->storeManagerMock->expects(static::once()) @@ -211,7 +225,7 @@ public function testGetForCustomerAfterGet() $cartId = 15; $customerId = 23; - $this->quoteFactoryMock->expects(static::exactly(2)) + $this->cartFactoryMock->expects(static::exactly(2)) ->method('create') ->willReturn($this->quoteMock); $this->storeManagerMock->expects(static::exactly(2)) @@ -249,7 +263,7 @@ public function testGetWithSharedStoreIds() $cartId = 16; $sharedStoreIds = [1, 2]; - $this->quoteFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock); + $this->cartFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock); $this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($this->storeMock); $this->storeMock->expects($this->once())->method('getId')->willReturn(1); $this->quoteMock->expects($this->once()) @@ -275,7 +289,7 @@ public function testGetForCustomer() $cartId = 17; $customerId = 23; - $this->quoteFactoryMock->expects(static::once()) + $this->cartFactoryMock->expects(static::once()) ->method('create') ->willReturn($this->quoteMock); $this->storeManagerMock->expects(static::once()) @@ -310,7 +324,7 @@ public function testGetActiveWithExceptionById() { $cartId = 14; - $this->quoteFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock); + $this->cartFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock); $this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($this->storeMock); $this->storeMock->expects($this->once())->method('getId')->willReturn(1); $this->quoteMock->expects($this->never())->method('setSharedStoreIds'); @@ -332,7 +346,7 @@ public function testGetActiveWithExceptionByIsActive() { $cartId = 15; - $this->quoteFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock); + $this->cartFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock); $this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($this->storeMock); $this->storeMock->expects($this->once())->method('getId')->willReturn(1); $this->quoteMock->expects($this->never())->method('setSharedStoreIds'); @@ -355,7 +369,7 @@ public function testGetActive() { $cartId = 15; - $this->quoteFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock); + $this->cartFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock); $this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($this->storeMock); $this->storeMock->expects($this->once())->method('getId')->willReturn(1); $this->quoteMock->expects($this->never())->method('setSharedStoreIds'); @@ -379,7 +393,7 @@ public function testGetActiveWithSharedStoreIds() $cartId = 16; $sharedStoreIds = [1, 2]; - $this->quoteFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock); + $this->cartFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock); $this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($this->storeMock); $this->storeMock->expects($this->once())->method('getId')->willReturn(1); $this->quoteMock->expects($this->once()) @@ -406,7 +420,7 @@ public function testGetActiveForCustomer() $cartId = 17; $customerId = 23; - $this->quoteFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock); + $this->cartFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock); $this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($this->storeMock); $this->storeMock->expects($this->once())->method('getId')->willReturn(1); $this->quoteMock->expects($this->never())->method('setSharedStoreIds'); @@ -430,14 +444,14 @@ public function testSave() { $cartId = 100; $quoteMock = $this->createPartialMock( - \Magento\Quote\Model\Quote::class, + Quote::class, ['getId', 'getCustomerId', 'getStoreId', 'hasData', 'setData'] ); $quoteMock->expects($this->exactly(3))->method('getId')->willReturn($cartId); $quoteMock->expects($this->once())->method('getCustomerId')->willReturn(2); $quoteMock->expects($this->once())->method('getStoreId')->willReturn(5); - $this->quoteFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock); + $this->cartFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock); $this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($this->storeMock); $this->storeMock->expects($this->once())->method('getId')->willReturn(1); $this->quoteMock->expects($this->once())->method('getId')->willReturn($cartId); @@ -481,8 +495,8 @@ public function testGetList() ->method('load') ->with($cartMock); - $searchResult = $this->createMock(\Magento\Quote\Api\Data\CartSearchResultsInterface::class); - $searchCriteriaMock = $this->createMock(\Magento\Framework\Api\SearchCriteria::class); + $searchResult = $this->createMock(CartSearchResultsInterface::class); + $searchCriteriaMock = $this->createMock(SearchCriteria::class); $this->searchResultsDataFactory ->expects($this->once()) ->method('create') @@ -495,7 +509,7 @@ public function testGetList() $this->extensionAttributesJoinProcessorMock->expects($this->once()) ->method('process') ->with( - $this->isInstanceOf(\Magento\Quote\Model\ResourceModel\Quote\Collection::class) + $this->isInstanceOf(Collection::class) ); $this->quoteCollectionMock->expects($this->atLeastOnce())->method('getItems')->willReturn([$cartMock]); $searchResult->expects($this->once())->method('setTotalCount')->with($pageSize); diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteTest.php index 22785f051dcf..07e203f71714 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/QuoteTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/QuoteTest.php @@ -975,6 +975,7 @@ public function testValidateMinimumAmount() ['sales/minimum_order/active', ScopeInterface::SCOPE_STORE, $storeId, true], ['sales/minimum_order/multi_address', ScopeInterface::SCOPE_STORE, $storeId, true], ['sales/minimum_order/amount', ScopeInterface::SCOPE_STORE, $storeId, 20], + ['sales/minimum_order/include_discount_amount', ScopeInterface::SCOPE_STORE, $storeId, true], ['sales/minimum_order/tax_including', ScopeInterface::SCOPE_STORE, $storeId, true], ]; $this->scopeConfig->expects($this->any()) @@ -1001,6 +1002,7 @@ public function testValidateMinimumAmountNegative() ['sales/minimum_order/active', ScopeInterface::SCOPE_STORE, $storeId, true], ['sales/minimum_order/multi_address', ScopeInterface::SCOPE_STORE, $storeId, true], ['sales/minimum_order/amount', ScopeInterface::SCOPE_STORE, $storeId, 20], + ['sales/minimum_order/include_discount_amount', ScopeInterface::SCOPE_STORE, $storeId, true], ['sales/minimum_order/tax_including', ScopeInterface::SCOPE_STORE, $storeId, true], ]; $this->scopeConfig->expects($this->any()) diff --git a/app/code/Magento/Quote/Test/Unit/Model/ShippingAddressManagementTest.php b/app/code/Magento/Quote/Test/Unit/Model/ShippingAddressManagementTest.php deleted file mode 100644 index e3d5528d62c7..000000000000 --- a/app/code/Magento/Quote/Test/Unit/Model/ShippingAddressManagementTest.php +++ /dev/null @@ -1,282 +0,0 @@ -<?php -/** - * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Quote\Test\Unit\Model; - -use \Magento\Quote\Model\ShippingAddressManagement; - -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class ShippingAddressManagementTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var ShippingAddressManagement - */ - protected $service; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $quoteRepositoryMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $quoteAddressMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $validatorMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $scopeConfigMock; - - /** - * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager - */ - protected $objectManager; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $totalsCollectorMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $addressRepository; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $amountErrorMessageMock; - - protected function setUp() - { - $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->quoteRepositoryMock = $this->createMock(\Magento\Quote\Api\CartRepositoryInterface::class); - $this->scopeConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); - - $this->quoteAddressMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Address::class, [ - 'setSameAsBilling', - 'setCollectShippingRates', - '__wakeup', - 'collectTotals', - 'save', - 'getId', - 'getCustomerAddressId', - 'getSaveInAddressBook', - 'getSameAsBilling', - 'importCustomerAddressData', - 'setSaveInAddressBook', - ]); - $this->validatorMock = $this->createMock(\Magento\Quote\Model\QuoteAddressValidator::class); - $this->totalsCollectorMock = $this->createMock(\Magento\Quote\Model\Quote\TotalsCollector::class); - $this->addressRepository = $this->createMock(\Magento\Customer\Api\AddressRepositoryInterface::class); - - $this->amountErrorMessageMock = $this->createPartialMock( - \Magento\Quote\Model\Quote\Validator\MinimumOrderAmount\ValidationMessage::class, - ['getMessage'] - ); - - $this->service = $this->objectManager->getObject( - \Magento\Quote\Model\ShippingAddressManagement::class, - [ - 'quoteRepository' => $this->quoteRepositoryMock, - 'addressValidator' => $this->validatorMock, - 'logger' => $this->createMock(\Psr\Log\LoggerInterface::class), - 'scopeConfig' => $this->scopeConfigMock, - 'totalsCollector' => $this->totalsCollectorMock, - 'addressRepository' => $this->addressRepository - ] - ); - } - - /** - * @expectedException \Magento\Framework\Exception\NoSuchEntityException - * @expected ExceptionMessage error345 - */ - public function testSetAddressValidationFailed() - { - $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); - $this->quoteRepositoryMock->expects($this->once()) - ->method('getActive') - ->with('cart654') - ->will($this->returnValue($quoteMock)); - - $this->validatorMock->expects($this->once())->method('validate') - ->will($this->throwException(new \Magento\Framework\Exception\NoSuchEntityException(__('error345')))); - - $this->service->assign('cart654', $this->quoteAddressMock); - } - - public function testSetAddress() - { - $addressId = 1; - $customerAddressId = 150; - - $quoteMock = $this->createPartialMock( - \Magento\Quote\Model\Quote::class, - ['getIsMultiShipping', 'isVirtual', 'validateMinimumAmount', 'setShippingAddress', 'getShippingAddress'] - ); - $this->quoteRepositoryMock->expects($this->once()) - ->method('getActive') - ->with('cart867') - ->willReturn($quoteMock); - $quoteMock->expects($this->once())->method('isVirtual')->will($this->returnValue(false)); - $quoteMock->expects($this->once()) - ->method('setShippingAddress') - ->with($this->quoteAddressMock) - ->willReturnSelf(); - - $this->quoteAddressMock->expects($this->once())->method('getSaveInAddressBook')->willReturn(1); - $this->quoteAddressMock->expects($this->once())->method('getSameAsBilling')->willReturn(1); - $this->quoteAddressMock->expects($this->once())->method('getCustomerAddressId')->willReturn($customerAddressId); - - $customerAddressMock = $this->createMock(\Magento\Customer\Api\Data\AddressInterface::class); - - $this->addressRepository->expects($this->once()) - ->method('getById') - ->with($customerAddressId) - ->willReturn($customerAddressMock); - - $this->validatorMock->expects($this->once())->method('validate') - ->with($this->quoteAddressMock) - ->willReturn(true); - - $quoteMock->expects($this->exactly(3))->method('getShippingAddress')->willReturn($this->quoteAddressMock); - $this->quoteAddressMock->expects($this->once()) - ->method('importCustomerAddressData') - ->with($customerAddressMock) - ->willReturnSelf(); - - $this->quoteAddressMock->expects($this->once())->method('setSameAsBilling')->with(1)->willReturnSelf(); - $this->quoteAddressMock->expects($this->once())->method('setSaveInAddressBook')->with(1)->willReturnSelf(); - $this->quoteAddressMock->expects($this->once()) - ->method('setCollectShippingRates') - ->with(true) - ->willReturnSelf(); - - $this->quoteAddressMock->expects($this->once())->method('save')->willReturnSelf(); - $this->quoteAddressMock->expects($this->once())->method('getId')->will($this->returnValue($addressId)); - - $this->assertEquals($addressId, $this->service->assign('cart867', $this->quoteAddressMock)); - } - - /** - * @expectedException \Magento\Framework\Exception\NoSuchEntityException - * @expectedExceptionMessage The Cart includes virtual product(s) only, so a shipping address is not used. - */ - public function testSetAddressForVirtualProduct() - { - $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); - $this->quoteRepositoryMock->expects($this->once()) - ->method('getActive') - ->with('cart867') - ->will($this->returnValue($quoteMock)); - $quoteMock->expects($this->once())->method('isVirtual')->will($this->returnValue(true)); - $quoteMock->expects($this->never())->method('setShippingAddress'); - - $this->quoteAddressMock->expects($this->never())->method('getCustomerAddressId'); - $this->quoteAddressMock->expects($this->never())->method('setSaveInAddressBook'); - - $quoteMock->expects($this->never())->method('save'); - - $this->service->assign('cart867', $this->quoteAddressMock); - } - - /** - * @expectedException \Magento\Framework\Exception\InputException - * @expectedExceptionMessage The address failed to save. Verify the address and try again. - */ - public function testSetAddressWithInabilityToSaveQuote() - { - $this->quoteAddressMock->expects($this->once())->method('save')->willThrowException( - new \Exception('The address failed to save. Verify the address and try again.') - ); - - $customerAddressId = 150; - - $quoteMock = $this->createPartialMock( - \Magento\Quote\Model\Quote::class, - ['getIsMultiShipping', 'isVirtual', 'validateMinimumAmount', 'setShippingAddress', 'getShippingAddress'] - ); - $this->quoteRepositoryMock->expects($this->once()) - ->method('getActive') - ->with('cart867') - ->willReturn($quoteMock); - $quoteMock->expects($this->once())->method('isVirtual')->will($this->returnValue(false)); - $quoteMock->expects($this->once()) - ->method('setShippingAddress') - ->with($this->quoteAddressMock) - ->willReturnSelf(); - - $customerAddressMock = $this->createMock(\Magento\Customer\Api\Data\AddressInterface::class); - - $this->addressRepository->expects($this->once()) - ->method('getById') - ->with($customerAddressId) - ->willReturn($customerAddressMock); - - $this->validatorMock->expects($this->once())->method('validate') - ->with($this->quoteAddressMock) - ->willReturn(true); - - $this->quoteAddressMock->expects($this->once())->method('getSaveInAddressBook')->willReturn(1); - $this->quoteAddressMock->expects($this->once())->method('getSameAsBilling')->willReturn(1); - $this->quoteAddressMock->expects($this->once())->method('getCustomerAddressId')->willReturn($customerAddressId); - - $quoteMock->expects($this->exactly(2))->method('getShippingAddress')->willReturn($this->quoteAddressMock); - $this->quoteAddressMock->expects($this->once()) - ->method('importCustomerAddressData') - ->with($customerAddressMock) - ->willReturnSelf(); - - $this->quoteAddressMock->expects($this->once())->method('setSameAsBilling')->with(1)->willReturnSelf(); - $this->quoteAddressMock->expects($this->once())->method('setSaveInAddressBook')->with(1)->willReturnSelf(); - $this->quoteAddressMock->expects($this->once()) - ->method('setCollectShippingRates') - ->with(true) - ->willReturnSelf(); - - $this->service->assign('cart867', $this->quoteAddressMock); - } - - public function testGetAddress() - { - $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); - $this->quoteRepositoryMock->expects($this->once())->method('getActive')->with('cartId')->will( - $this->returnValue($quoteMock) - ); - - $addressMock = $this->createMock(\Magento\Quote\Model\Quote\Address::class); - $quoteMock->expects($this->any())->method('getShippingAddress')->will($this->returnValue($addressMock)); - $quoteMock->expects($this->any())->method('isVirtual')->will($this->returnValue(false)); - $this->assertEquals($addressMock, $this->service->get('cartId')); - } - - /** - * @expectedException \Exception - * @expectedExceptionMessage The Cart includes virtual product(s) only, so a shipping address is not used. - */ - public function testGetAddressOfQuoteWithVirtualProducts() - { - $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); - $this->quoteRepositoryMock->expects($this->once())->method('getActive')->with('cartId')->will( - $this->returnValue($quoteMock) - ); - - $quoteMock->expects($this->any())->method('isVirtual')->will($this->returnValue(true)); - $quoteMock->expects($this->never())->method('getShippingAddress'); - - $this->service->get('cartId'); - } -} diff --git a/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php b/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php index f3357f8aacd3..4ea067c9be8f 100644 --- a/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php +++ b/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php @@ -199,7 +199,7 @@ public function testDispatchWithCustomerCountryNotInEUAndNotLoggedCustomerInGrou ->method('getNotLoggedInGroup') ->will($this->returnValue($this->groupInterfaceMock)); $this->groupInterfaceMock->expects($this->once()) - ->method('getId')->will($this->returnValue(0)); + ->method('getId')->will($this->returnValue(null)); $this->vatValidatorMock->expects($this->once()) ->method('isEnabled') ->with($this->quoteAddressMock, $this->storeId) @@ -220,9 +220,6 @@ public function testDispatchWithCustomerCountryNotInEUAndNotLoggedCustomerInGrou $this->returnValue(false) ); - $groupMock = $this->getMockBuilder(\Magento\Customer\Api\Data\GroupInterface::class) - ->disableOriginalConstructor() - ->getMock(); $this->customerMock->expects($this->once())->method('getId')->will($this->returnValue(null)); /** Assertions */ diff --git a/app/code/Magento/Quote/etc/db_schema.xml b/app/code/Magento/Quote/etc/db_schema.xml index 3bd7122e65d7..b4c75fc1d21d 100644 --- a/app/code/Magento/Quote/etc/db_schema.xml +++ b/app/code/Magento/Quote/etc/db_schema.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="quote" resource="checkout" engine="innodb" comment="Sales Flat Quote"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Store Id"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" @@ -37,9 +37,9 @@ comment="Store Currency Code"/> <column xsi:type="varchar" name="quote_currency_code" nullable="true" length="255" comment="Quote Currency Code"/> - <column xsi:type="decimal" name="grand_total" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="grand_total" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Grand Total"/> - <column xsi:type="decimal" name="base_grand_total" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_grand_total" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Base Grand Total"/> <column xsi:type="varchar" name="checkout_method" nullable="true" length="255" comment="Checkout Method"/> <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="true" identity="false" @@ -68,36 +68,36 @@ <column xsi:type="varchar" name="coupon_code" nullable="true" length="255" comment="Coupon Code"/> <column xsi:type="varchar" name="global_currency_code" nullable="true" length="255" comment="Global Currency Code"/> - <column xsi:type="decimal" name="base_to_global_rate" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_to_global_rate" scale="4" precision="20" unsigned="false" nullable="true" comment="Base To Global Rate"/> - <column xsi:type="decimal" name="base_to_quote_rate" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_to_quote_rate" scale="4" precision="20" unsigned="false" nullable="true" comment="Base To Quote Rate"/> <column xsi:type="varchar" name="customer_taxvat" nullable="true" length="255" comment="Customer Taxvat"/> <column xsi:type="varchar" name="customer_gender" nullable="true" length="255" comment="Customer Gender"/> - <column xsi:type="decimal" name="subtotal" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="subtotal" scale="4" precision="20" unsigned="false" nullable="true" comment="Subtotal"/> - <column xsi:type="decimal" name="base_subtotal" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_subtotal" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Subtotal"/> - <column xsi:type="decimal" name="subtotal_with_discount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="subtotal_with_discount" scale="4" precision="20" unsigned="false" nullable="true" comment="Subtotal With Discount"/> - <column xsi:type="decimal" name="base_subtotal_with_discount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_subtotal_with_discount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Subtotal With Discount"/> <column xsi:type="int" name="is_changed" padding="10" unsigned="true" nullable="true" identity="false" comment="Is Changed"/> <column xsi:type="smallint" name="trigger_recollect" padding="6" unsigned="false" nullable="false" identity="false" default="0" comment="Trigger Recollect"/> <column xsi:type="text" name="ext_shipping_info" nullable="true" comment="Ext Shipping Info"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> - <constraint xsi:type="foreign" name="QUOTE_STORE_ID_STORE_STORE_ID" table="quote" column="store_id" + <constraint xsi:type="foreign" referenceId="QUOTE_STORE_ID_STORE_STORE_ID" table="quote" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <index name="QUOTE_CUSTOMER_ID_STORE_ID_IS_ACTIVE" indexType="btree"> + <index referenceId="QUOTE_CUSTOMER_ID_STORE_ID_IS_ACTIVE" indexType="btree"> <column name="customer_id"/> <column name="store_id"/> <column name="is_active"/> </index> - <index name="QUOTE_STORE_ID" indexType="btree"> + <index referenceId="QUOTE_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -108,7 +108,7 @@ default="0" comment="Quote Id"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> - <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="0" + <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" comment="Updated At"/> <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="true" identity="false" comment="Customer Id"/> @@ -143,57 +143,57 @@ comment="Shipping Description"/> <column xsi:type="decimal" name="weight" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Weight"/> - <column xsi:type="decimal" name="subtotal" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="subtotal" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Subtotal"/> - <column xsi:type="decimal" name="base_subtotal" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="base_subtotal" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Base Subtotal"/> - <column xsi:type="decimal" name="subtotal_with_discount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="subtotal_with_discount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Subtotal With Discount"/> - <column xsi:type="decimal" name="base_subtotal_with_discount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_subtotal_with_discount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Base Subtotal With Discount"/> - <column xsi:type="decimal" name="tax_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="tax_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Tax Amount"/> - <column xsi:type="decimal" name="base_tax_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="base_tax_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Base Tax Amount"/> - <column xsi:type="decimal" name="shipping_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="shipping_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Shipping Amount"/> - <column xsi:type="decimal" name="base_shipping_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Base Shipping Amount"/> - <column xsi:type="decimal" name="shipping_tax_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="shipping_tax_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Tax Amount"/> - <column xsi:type="decimal" name="base_shipping_tax_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_tax_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Tax Amount"/> - <column xsi:type="decimal" name="discount_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="discount_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Discount Amount"/> - <column xsi:type="decimal" name="base_discount_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_discount_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Base Discount Amount"/> - <column xsi:type="decimal" name="grand_total" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="grand_total" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Grand Total"/> - <column xsi:type="decimal" name="base_grand_total" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="base_grand_total" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Base Grand Total"/> <column xsi:type="text" name="customer_notes" nullable="true" comment="Customer Notes"/> <column xsi:type="text" name="applied_taxes" nullable="true" comment="Applied Taxes"/> <column xsi:type="varchar" name="discount_description" nullable="true" length="255" comment="Discount Description"/> - <column xsi:type="decimal" name="shipping_discount_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="shipping_discount_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Discount Amount"/> - <column xsi:type="decimal" name="base_shipping_discount_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_discount_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Discount Amount"/> - <column xsi:type="decimal" name="subtotal_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="subtotal_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Subtotal Incl Tax"/> - <column xsi:type="decimal" name="base_subtotal_total_incl_tax" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_subtotal_total_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Subtotal Total Incl Tax"/> - <column xsi:type="decimal" name="discount_tax_compensation_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="base_discount_tax_compensation_amount" scale="4" precision="12" + <column xsi:type="decimal" name="base_discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="shipping_discount_tax_compensation_amount" scale="4" precision="12" + <column xsi:type="decimal" name="shipping_discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="base_shipping_discount_tax_compensation_amnt" scale="4" precision="12" + <column xsi:type="decimal" name="base_shipping_discount_tax_compensation_amnt" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="shipping_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="shipping_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Incl Tax"/> - <column xsi:type="decimal" name="base_shipping_incl_tax" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Incl Tax"/> <column xsi:type="text" name="vat_id" nullable="true" comment="Vat Id"/> <column xsi:type="smallint" name="vat_is_valid" padding="6" unsigned="false" nullable="true" identity="false" @@ -202,12 +202,14 @@ <column xsi:type="text" name="vat_request_date" nullable="true" comment="Vat Request Date"/> <column xsi:type="smallint" name="vat_request_success" padding="6" unsigned="false" nullable="true" identity="false" comment="Vat Request Success"/> - <constraint xsi:type="primary" name="PRIMARY"> + <column xsi:type="text" name="validated_country_code" nullable="true" comment="Validated Country Code"/> + <column xsi:type="text" name="validated_vat_number" nullable="true" comment="Validated Vat Number"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="address_id"/> </constraint> - <constraint xsi:type="foreign" name="QUOTE_ADDRESS_QUOTE_ID_QUOTE_ENTITY_ID" table="quote_address" + <constraint xsi:type="foreign" referenceId="QUOTE_ADDRESS_QUOTE_ID_QUOTE_ENTITY_ID" table="quote_address" column="quote_id" referenceTable="quote" referenceColumn="entity_id" onDelete="CASCADE"/> - <index name="QUOTE_ADDRESS_QUOTE_ID" indexType="btree"> + <index referenceId="QUOTE_ADDRESS_QUOTE_ID" indexType="btree"> <column name="quote_id"/> </index> </table> @@ -218,7 +220,7 @@ default="0" comment="Quote Id"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> - <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="0" + <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" comment="Updated At"/> <column xsi:type="int" name="product_id" padding="10" unsigned="true" nullable="true" identity="false" comment="Product Id"/> @@ -249,65 +251,65 @@ comment="Custom Price"/> <column xsi:type="decimal" name="discount_percent" scale="4" precision="12" unsigned="false" nullable="true" default="0" comment="Discount Percent"/> - <column xsi:type="decimal" name="discount_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="discount_amount" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Discount Amount"/> - <column xsi:type="decimal" name="base_discount_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_discount_amount" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Base Discount Amount"/> <column xsi:type="decimal" name="tax_percent" scale="4" precision="12" unsigned="false" nullable="true" default="0" comment="Tax Percent"/> - <column xsi:type="decimal" name="tax_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="tax_amount" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Tax Amount"/> - <column xsi:type="decimal" name="base_tax_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_tax_amount" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Base Tax Amount"/> - <column xsi:type="decimal" name="row_total" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="row_total" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Row Total"/> - <column xsi:type="decimal" name="base_row_total" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="base_row_total" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Base Row Total"/> - <column xsi:type="decimal" name="row_total_with_discount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="row_total_with_discount" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Row Total With Discount"/> <column xsi:type="decimal" name="row_weight" scale="4" precision="12" unsigned="false" nullable="true" default="0" comment="Row Weight"/> <column xsi:type="varchar" name="product_type" nullable="true" length="255" comment="Product Type"/> - <column xsi:type="decimal" name="base_tax_before_discount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_tax_before_discount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Tax Before Discount"/> - <column xsi:type="decimal" name="tax_before_discount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="tax_before_discount" scale="4" precision="20" unsigned="false" nullable="true" comment="Tax Before Discount"/> <column xsi:type="decimal" name="original_custom_price" scale="4" precision="12" unsigned="false" nullable="true" comment="Original Custom Price"/> <column xsi:type="varchar" name="redirect_url" nullable="true" length="255" comment="Redirect Url"/> <column xsi:type="decimal" name="base_cost" scale="4" precision="12" unsigned="false" nullable="true" comment="Base Cost"/> - <column xsi:type="decimal" name="price_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="price_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Price Incl Tax"/> - <column xsi:type="decimal" name="base_price_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_price_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Price Incl Tax"/> - <column xsi:type="decimal" name="row_total_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="row_total_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Row Total Incl Tax"/> - <column xsi:type="decimal" name="base_row_total_incl_tax" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_row_total_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Row Total Incl Tax"/> - <column xsi:type="decimal" name="discount_tax_compensation_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="base_discount_tax_compensation_amount" scale="4" precision="12" + <column xsi:type="decimal" name="base_discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Tax Compensation Amount"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="item_id"/> </constraint> - <constraint xsi:type="foreign" name="QUOTE_ITEM_PARENT_ITEM_ID_QUOTE_ITEM_ITEM_ID" table="quote_item" + <constraint xsi:type="foreign" referenceId="QUOTE_ITEM_PARENT_ITEM_ID_QUOTE_ITEM_ITEM_ID" table="quote_item" column="parent_item_id" referenceTable="quote_item" referenceColumn="item_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="QUOTE_ITEM_QUOTE_ID_QUOTE_ENTITY_ID" table="quote_item" column="quote_id" + <constraint xsi:type="foreign" referenceId="QUOTE_ITEM_QUOTE_ID_QUOTE_ENTITY_ID" table="quote_item" column="quote_id" referenceTable="quote" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="QUOTE_ITEM_STORE_ID_STORE_STORE_ID" table="quote_item" column="store_id" + <constraint xsi:type="foreign" referenceId="QUOTE_ITEM_STORE_ID_STORE_STORE_ID" table="quote_item" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="SET NULL"/> - <index name="QUOTE_ITEM_PARENT_ITEM_ID" indexType="btree"> + <index referenceId="QUOTE_ITEM_PARENT_ITEM_ID" indexType="btree"> <column name="parent_item_id"/> </index> - <index name="QUOTE_ITEM_PRODUCT_ID" indexType="btree"> + <index referenceId="QUOTE_ITEM_PRODUCT_ID" indexType="btree"> <column name="product_id"/> </index> - <index name="QUOTE_ITEM_QUOTE_ID" indexType="btree"> + <index referenceId="QUOTE_ITEM_QUOTE_ID" indexType="btree"> <column name="quote_id"/> </index> - <index name="QUOTE_ITEM_STORE_ID" indexType="btree"> + <index referenceId="QUOTE_ITEM_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -322,7 +324,7 @@ default="0" comment="Quote Item Id"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> - <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="0" + <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" comment="Updated At"/> <column xsi:type="text" name="applied_rule_ids" nullable="true" comment="Applied Rule Ids"/> <column xsi:type="text" name="additional_data" nullable="true" comment="Additional Data"/> @@ -330,19 +332,19 @@ comment="Weight"/> <column xsi:type="decimal" name="qty" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Qty"/> - <column xsi:type="decimal" name="discount_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="discount_amount" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Discount Amount"/> - <column xsi:type="decimal" name="tax_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="tax_amount" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Tax Amount"/> - <column xsi:type="decimal" name="row_total" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="row_total" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Row Total"/> - <column xsi:type="decimal" name="base_row_total" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="base_row_total" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Base Row Total"/> - <column xsi:type="decimal" name="row_total_with_discount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="row_total_with_discount" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Row Total With Discount"/> - <column xsi:type="decimal" name="base_discount_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_discount_amount" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Base Discount Amount"/> - <column xsi:type="decimal" name="base_tax_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_tax_amount" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Base Tax Amount"/> <column xsi:type="decimal" name="row_weight" scale="4" precision="12" unsigned="false" nullable="true" default="0" comment="Row Weight"/> @@ -352,6 +354,8 @@ comment="Super Product Id"/> <column xsi:type="int" name="parent_product_id" padding="10" unsigned="true" nullable="true" identity="false" comment="Parent Product Id"/> + <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" + comment="Store Id"/> <column xsi:type="varchar" name="sku" nullable="true" length="255" comment="Sku"/> <column xsi:type="varchar" name="image" nullable="true" length="255" comment="Image"/> <column xsi:type="varchar" name="name" nullable="true" length="255" comment="Name"/> @@ -370,39 +374,42 @@ comment="Base Price"/> <column xsi:type="decimal" name="base_cost" scale="4" precision="12" unsigned="false" nullable="true" comment="Base Cost"/> - <column xsi:type="decimal" name="price_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="price_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Price Incl Tax"/> - <column xsi:type="decimal" name="base_price_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_price_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Price Incl Tax"/> - <column xsi:type="decimal" name="row_total_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="row_total_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Row Total Incl Tax"/> - <column xsi:type="decimal" name="base_row_total_incl_tax" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_row_total_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Row Total Incl Tax"/> - <column xsi:type="decimal" name="discount_tax_compensation_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="base_discount_tax_compensation_amount" scale="4" precision="12" + <column xsi:type="decimal" name="base_discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Tax Compensation Amount"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="address_item_id"/> </constraint> - <constraint xsi:type="foreign" name="QUOTE_ADDRESS_ITEM_QUOTE_ADDRESS_ID_QUOTE_ADDRESS_ADDRESS_ID" + <constraint xsi:type="foreign" referenceId="QUOTE_ADDRESS_ITEM_QUOTE_ADDRESS_ID_QUOTE_ADDRESS_ADDRESS_ID" table="quote_address_item" column="quote_address_id" referenceTable="quote_address" referenceColumn="address_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="QUOTE_ADDR_ITEM_PARENT_ITEM_ID_QUOTE_ADDR_ITEM_ADDR_ITEM_ID" + <constraint xsi:type="foreign" referenceId="QUOTE_ADDR_ITEM_PARENT_ITEM_ID_QUOTE_ADDR_ITEM_ADDR_ITEM_ID" table="quote_address_item" column="parent_item_id" referenceTable="quote_address_item" referenceColumn="address_item_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="QUOTE_ADDRESS_ITEM_QUOTE_ITEM_ID_QUOTE_ITEM_ITEM_ID" + <constraint xsi:type="foreign" referenceId="QUOTE_ADDRESS_ITEM_QUOTE_ITEM_ID_QUOTE_ITEM_ITEM_ID" table="quote_address_item" column="quote_item_id" referenceTable="quote_item" referenceColumn="item_id" onDelete="CASCADE"/> - <index name="QUOTE_ADDRESS_ITEM_QUOTE_ADDRESS_ID" indexType="btree"> + <index referenceId="QUOTE_ADDRESS_ITEM_QUOTE_ADDRESS_ID" indexType="btree"> <column name="quote_address_id"/> </index> - <index name="QUOTE_ADDRESS_ITEM_PARENT_ITEM_ID" indexType="btree"> + <index referenceId="QUOTE_ADDRESS_ITEM_PARENT_ITEM_ID" indexType="btree"> <column name="parent_item_id"/> </index> - <index name="QUOTE_ADDRESS_ITEM_QUOTE_ITEM_ID" indexType="btree"> + <index referenceId="QUOTE_ADDRESS_ITEM_QUOTE_ITEM_ID" indexType="btree"> <column name="quote_item_id"/> </index> + <index referenceId="QUOTE_ADDRESS_ITEM_STORE_ID" indexType="btree"> + <column name="store_id"/> + </index> </table> <table name="quote_item_option" resource="checkout" engine="innodb" comment="Sales Flat Quote Item Option"> <column xsi:type="int" name="option_id" padding="10" unsigned="true" nullable="false" identity="true" @@ -413,12 +420,12 @@ comment="Product Id"/> <column xsi:type="varchar" name="code" nullable="false" length="255" comment="Code"/> <column xsi:type="text" name="value" nullable="true" comment="Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="option_id"/> </constraint> - <constraint xsi:type="foreign" name="QUOTE_ITEM_OPTION_ITEM_ID_QUOTE_ITEM_ITEM_ID" table="quote_item_option" + <constraint xsi:type="foreign" referenceId="QUOTE_ITEM_OPTION_ITEM_ID_QUOTE_ITEM_ITEM_ID" table="quote_item_option" column="item_id" referenceTable="quote_item" referenceColumn="item_id" onDelete="CASCADE"/> - <index name="QUOTE_ITEM_OPTION_ITEM_ID" indexType="btree"> + <index referenceId="QUOTE_ITEM_OPTION_ITEM_ID" indexType="btree"> <column name="item_id"/> </index> </table> @@ -429,7 +436,7 @@ default="0" comment="Quote Id"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> - <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="0" + <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" comment="Updated At"/> <column xsi:type="varchar" name="method" nullable="true" length="255" comment="Method"/> <column xsi:type="varchar" name="cc_type" nullable="true" length="255" comment="Cc Type"/> @@ -449,12 +456,12 @@ <column xsi:type="text" name="additional_data" nullable="true" comment="Additional Data"/> <column xsi:type="varchar" name="cc_ss_issue" nullable="true" length="255" comment="Cc Ss Issue"/> <column xsi:type="text" name="additional_information" nullable="true" comment="Additional Information"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="payment_id"/> </constraint> - <constraint xsi:type="foreign" name="QUOTE_PAYMENT_QUOTE_ID_QUOTE_ENTITY_ID" table="quote_payment" + <constraint xsi:type="foreign" referenceId="QUOTE_PAYMENT_QUOTE_ID_QUOTE_ENTITY_ID" table="quote_payment" column="quote_id" referenceTable="quote" referenceColumn="entity_id" onDelete="CASCADE"/> - <index name="QUOTE_PAYMENT_QUOTE_ID" indexType="btree"> + <index referenceId="QUOTE_PAYMENT_QUOTE_ID" indexType="btree"> <column name="quote_id"/> </index> </table> @@ -465,43 +472,43 @@ default="0" comment="Address Id"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> - <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="0" + <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" comment="Updated At"/> <column xsi:type="varchar" name="carrier" nullable="true" length="255" comment="Carrier"/> <column xsi:type="varchar" name="carrier_title" nullable="true" length="255" comment="Carrier Title"/> <column xsi:type="varchar" name="code" nullable="true" length="255" comment="Code"/> <column xsi:type="varchar" name="method" nullable="true" length="255" comment="Method"/> <column xsi:type="text" name="method_description" nullable="true" comment="Method Description"/> - <column xsi:type="decimal" name="price" scale="4" precision="12" unsigned="false" nullable="false" default="0" + <column xsi:type="decimal" name="price" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Price"/> <column xsi:type="text" name="error_message" nullable="true" comment="Error Message"/> <column xsi:type="text" name="method_title" nullable="true" comment="Method Title"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rate_id"/> </constraint> - <constraint xsi:type="foreign" name="QUOTE_SHIPPING_RATE_ADDRESS_ID_QUOTE_ADDRESS_ADDRESS_ID" + <constraint xsi:type="foreign" referenceId="QUOTE_SHIPPING_RATE_ADDRESS_ID_QUOTE_ADDRESS_ADDRESS_ID" table="quote_shipping_rate" column="address_id" referenceTable="quote_address" referenceColumn="address_id" onDelete="CASCADE"/> - <index name="QUOTE_SHIPPING_RATE_ADDRESS_ID" indexType="btree"> + <index referenceId="QUOTE_SHIPPING_RATE_ADDRESS_ID" indexType="btree"> <column name="address_id"/> </index> </table> <table name="quote_id_mask" resource="checkout" engine="innodb" comment="Quote ID and masked ID mapping"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="int" name="quote_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Quote ID"/> <column xsi:type="varchar" name="masked_id" nullable="true" length="32" comment="Masked ID"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> <column name="quote_id"/> </constraint> - <constraint xsi:type="foreign" name="QUOTE_ID_MASK_QUOTE_ID_QUOTE_ENTITY_ID" table="quote_id_mask" + <constraint xsi:type="foreign" referenceId="QUOTE_ID_MASK_QUOTE_ID_QUOTE_ENTITY_ID" table="quote_id_mask" column="quote_id" referenceTable="quote" referenceColumn="entity_id" onDelete="CASCADE"/> - <index name="QUOTE_ID_MASK_QUOTE_ID" indexType="btree"> + <index referenceId="QUOTE_ID_MASK_QUOTE_ID" indexType="btree"> <column name="quote_id"/> </index> - <index name="QUOTE_ID_MASK_MASKED_ID" indexType="btree"> + <index referenceId="QUOTE_ID_MASK_MASKED_ID" indexType="btree"> <column name="masked_id"/> </index> </table> diff --git a/app/code/Magento/Quote/etc/db_schema_whitelist.json b/app/code/Magento/Quote/etc/db_schema_whitelist.json index c2cc34293dcb..5667a9a5b460 100644 --- a/app/code/Magento/Quote/etc/db_schema_whitelist.json +++ b/app/code/Magento/Quote/etc/db_schema_whitelist.json @@ -212,6 +212,7 @@ "product_id": true, "super_product_id": true, "parent_product_id": true, + "store_id": true, "sku": true, "image": true, "name": true, @@ -233,7 +234,8 @@ "index": { "QUOTE_ADDRESS_ITEM_QUOTE_ADDRESS_ID": true, "QUOTE_ADDRESS_ITEM_PARENT_ITEM_ID": true, - "QUOTE_ADDRESS_ITEM_QUOTE_ITEM_ID": true + "QUOTE_ADDRESS_ITEM_QUOTE_ITEM_ID": true, + "QUOTE_ADDRESS_ITEM_STORE_ID": true }, "constraint": { "PRIMARY": true, diff --git a/app/code/Magento/Quote/etc/di.xml b/app/code/Magento/Quote/etc/di.xml index ecc642685567..cd5e62307fdc 100644 --- a/app/code/Magento/Quote/etc/di.xml +++ b/app/code/Magento/Quote/etc/di.xml @@ -11,6 +11,8 @@ <preference for="Magento\Quote\Api\Data\ShippingMethodInterface" type="Magento\Quote\Model\Cart\ShippingMethod" /> <preference for="Magento\Quote\Api\BillingAddressManagementInterface" type="Magento\Quote\Model\BillingAddressManagement" /> <preference for="Magento\Quote\Model\ShippingAddressManagementInterface" type="Magento\Quote\Model\ShippingAddressManagement" /> + <preference for="Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface" type="Magento\Quote\Model\MaskedQuoteIdToQuoteId" /> + <preference for="Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface" type="Magento\Quote\Model\QuoteIdToMaskedQuoteId" /> <preference for="Magento\Quote\Api\Data\AddressInterface" type="Magento\Quote\Model\Quote\Address" /> <preference for="Magento\Quote\Api\Data\CartItemInterface" type="Magento\Quote\Model\Quote\Item" /> <preference for="Magento\Quote\Api\Data\CartInterface" type="Magento\Quote\Model\Quote" /> @@ -96,6 +98,9 @@ <plugin name="clean_quote_items_after_product_delete" type="Magento\Quote\Model\Product\Plugin\RemoveQuoteItems"/> <plugin name="update_quote_items_after_product_save" type="Magento\Quote\Model\Product\Plugin\UpdateQuoteItems"/> </type> + <type name="Magento\Catalog\Model\Product\Action"> + <plugin name="quoteProductMassChange" type="Magento\Quote\Model\Product\Plugin\MarkQuotesRecollectMassDisabled"/> + </type> <type name="Magento\Quote\Model\ValidationRules\QuoteValidationComposite"> <arguments> <argument name="validationRules" xsi:type="array"> diff --git a/app/code/Magento/Quote/etc/fieldset.xml b/app/code/Magento/Quote/etc/fieldset.xml index 55ec76a647fc..85ee20c7f852 100644 --- a/app/code/Magento/Quote/etc/fieldset.xml +++ b/app/code/Magento/Quote/etc/fieldset.xml @@ -186,6 +186,11 @@ <aspect name="to_order_address" /> </field> </fieldset> + <fieldset id="quote_convert_address_item"> + <field name="quote_item_id"> + <aspect name="to_order_item" /> + </field> + </fieldset> <fieldset id="quote_convert_item"> <field name="sku"> <aspect name="to_order_item" /> diff --git a/app/code/Magento/Quote/etc/frontend/di.xml b/app/code/Magento/Quote/etc/frontend/di.xml index 25acd6763ba5..ecad94fbbc24 100644 --- a/app/code/Magento/Quote/etc/frontend/di.xml +++ b/app/code/Magento/Quote/etc/frontend/di.xml @@ -12,7 +12,13 @@ <argument name="checkoutSession" xsi:type="object">Magento\Checkout\Model\Session\Proxy</argument> </arguments> </type> + <type name="Magento\Store\Model\StoreSwitcherInterface"> + <plugin name="update_quote_item_store_after_switch_store_view" type="Magento\Quote\Plugin\UpdateQuoteItemStore"/> + </type> <type name="Magento\Store\Api\StoreCookieManagerInterface"> <plugin name="update_quote_store_after_switch_store_view" type="Magento\Quote\Plugin\UpdateQuoteStore"/> </type> + <type name="Magento\Customer\Model\ResourceModel\Customer"> + <plugin name="cart_recollect_on_group_change" type="Magento\Quote\Plugin\RecollectOnGroupChange"/> + </type> </config> diff --git a/app/code/Magento/Quote/etc/sales.xml b/app/code/Magento/Quote/etc/sales.xml index 3d54a6375c8d..3db72a122623 100644 --- a/app/code/Magento/Quote/etc/sales.xml +++ b/app/code/Magento/Quote/etc/sales.xml @@ -9,7 +9,7 @@ <section name="quote"> <group name="totals"> <item name="subtotal" instance="Magento\Quote\Model\Quote\Address\Total\Subtotal" sort_order="100"/> - <item name="shipping" instance="Magento\Quote\Model\Quote\Address\Total\Shipping" sort_order="250"/> + <item name="shipping" instance="Magento\Quote\Model\Quote\Address\Total\Shipping" sort_order="350"/> <item name="grand_total" instance="Magento\Quote\Model\Quote\Address\Total\Grand" sort_order="550"/> </group> </section> diff --git a/app/code/Magento/Quote/i18n/en_US.csv b/app/code/Magento/Quote/i18n/en_US.csv index ae7453aa0d0c..b24179297493 100644 --- a/app/code/Magento/Quote/i18n/en_US.csv +++ b/app/code/Magento/Quote/i18n/en_US.csv @@ -65,3 +65,5 @@ error345,error345 Carts,Carts "Manage carts","Manage carts" "Invalid state change requested","Invalid state change requested" +"Validated Country Code","Validated Country Code" +"Validated Vat Number","Validated Vat Number" diff --git a/app/code/Magento/QuoteAnalytics/README.md b/app/code/Magento/QuoteAnalytics/README.md index d4adcc931322..c5a3857c7af3 100644 --- a/app/code/Magento/QuoteAnalytics/README.md +++ b/app/code/Magento/QuoteAnalytics/README.md @@ -1,3 +1,3 @@ # Magento_QuoteAnalytics -The Magento_QuoteAnalytics module configures data definitions for a data collection related to the Quote module entities to be used in [Advanced Reporting](http://devdocs.magento.com/guides/v2.2/advanced-reporting/modules.html). +The Magento_QuoteAnalytics module configures data definitions for a data collection related to the Quote module entities to be used in [Advanced Reporting](https://devdocs.magento.com/guides/v2.2/advanced-reporting/modules.html). diff --git a/app/code/Magento/QuoteAnalytics/composer.json b/app/code/Magento/QuoteAnalytics/composer.json index 90dae1ec2adc..706bed674b4a 100644 --- a/app/code/Magento/QuoteAnalytics/composer.json +++ b/app/code/Magento/QuoteAnalytics/composer.json @@ -4,7 +4,8 @@ "require": { "php": "~7.1.3||~7.2.0", "magento/framework": "*", - "magento/module-quote": "*" + "magento/module-quote": "*", + "magento/module-analytics": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/AddProductsToCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/AddProductsToCart.php new file mode 100644 index 000000000000..005cf3a10ca8 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/AddProductsToCart.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Cart; + +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\Message\AbstractMessage; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\Quote; + +/** + * Add products to cart + */ +class AddProductsToCart +{ + /** + * @var CartRepositoryInterface + */ + private $cartRepository; + + /** + * @var AddSimpleProductToCart + */ + private $addProductToCart; + + /** + * @param CartRepositoryInterface $cartRepository + * @param AddSimpleProductToCart $addProductToCart + */ + public function __construct( + CartRepositoryInterface $cartRepository, + AddSimpleProductToCart $addProductToCart + ) { + $this->cartRepository = $cartRepository; + $this->addProductToCart = $addProductToCart; + } + + /** + * Add products to cart + * + * @param Quote $cart + * @param array $cartItems + * @throws GraphQlInputException + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException + */ + public function execute(Quote $cart, array $cartItems): void + { + foreach ($cartItems as $cartItemData) { + $this->addProductToCart->execute($cart, $cartItemData); + } + + if ($cart->getData('has_error')) { + throw new GraphQlInputException( + __('Shopping cart error: %message', ['message' => $this->getCartErrors($cart)]) + ); + } + + $this->cartRepository->save($cart); + } + + /** + * Collecting cart errors + * + * @param Quote $cart + * @return string + */ + private function getCartErrors(Quote $cart): string + { + $errorMessages = []; + + /** @var AbstractMessage $error */ + foreach ($cart->getErrors() as $error) { + $errorMessages[] = $error->getText(); + } + + return implode(PHP_EOL, $errorMessages); + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php new file mode 100644 index 000000000000..ad730288e5cc --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php @@ -0,0 +1,164 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Cart; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\DataObject; +use Magento\Framework\DataObjectFactory; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Framework\Stdlib\ArrayManager; +use Magento\Quote\Model\Quote; + +/** + * Add simple product to cart + * + * TODO: should be replaced for different types resolver + */ +class AddSimpleProductToCart +{ + /** + * @var ArrayManager + */ + private $arrayManager; + + /** + * @var DataObjectFactory + */ + private $dataObjectFactory; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @param ArrayManager $arrayManager + * @param DataObjectFactory $dataObjectFactory + * @param ProductRepositoryInterface $productRepository + */ + public function __construct( + ArrayManager $arrayManager, + DataObjectFactory $dataObjectFactory, + ProductRepositoryInterface $productRepository + ) { + $this->arrayManager = $arrayManager; + $this->dataObjectFactory = $dataObjectFactory; + $this->productRepository = $productRepository; + } + + /** + * Add simple product to cart + * + * @param Quote $cart + * @param array $cartItemData + * @return void + * @throws GraphQlNoSuchEntityException + * @throws GraphQlInputException + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function execute(Quote $cart, array $cartItemData): void + { + $sku = $this->extractSku($cartItemData); + $quantity = $this->extractQuantity($cartItemData); + if ($quantity <= 0) { + throw new GraphQlInputException( + __('Please enter a number greater than 0 in this field.') + ); + } + $customizableOptions = $this->extractCustomizableOptions($cartItemData); + + try { + $product = $this->productRepository->get($sku); + } catch (NoSuchEntityException $e) { + throw new GraphQlNoSuchEntityException(__('Could not find a product with SKU "%sku"', ['sku' => $sku])); + } + + try { + $result = $cart->addProduct($product, $this->createBuyRequest($quantity, $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)); + } + } + + /** + * Extract SKU from cart item data + * + * @param array $cartItemData + * @return string + * @throws GraphQlInputException + */ + private function extractSku(array $cartItemData): string + { + $sku = $this->arrayManager->get('data/sku', $cartItemData); + if (!isset($sku)) { + throw new GraphQlInputException(__('Missing key "sku" in cart item data')); + } + return (string)$sku; + } + + /** + * Extract quantity from cart item data + * + * @param array $cartItemData + * @return float + * @throws GraphQlInputException + */ + private function extractQuantity(array $cartItemData): float + { + $quantity = $this->arrayManager->get('data/quantity', $cartItemData); + if (!isset($quantity)) { + throw new GraphQlInputException(__('Missing key "quantity" in cart item data')); + } + return (float)$quantity; + } + + /** + * Extract Customizable Options from cart item data + * + * @param array $cartItemData + * @return array + */ + private function extractCustomizableOptions(array $cartItemData): array + { + $customizableOptions = $this->arrayManager->get('customizable_options', $cartItemData, []); + + $customizableOptionsData = []; + foreach ($customizableOptions as $customizableOption) { + $customizableOptionsData[$customizableOption['id']] = $customizableOption['value']; + } + return $customizableOptionsData; + } + + /** + * Format GraphQl input data to a shape that buy request has + * + * @param float $quantity + * @param array $customOptions + * @return DataObject + */ + private function createBuyRequest(float $quantity, array $customOptions): DataObject + { + return $this->dataObjectFactory->create([ + 'data' => [ + 'qty' => $quantity, + 'options' => $customOptions, + ], + ]); + } +} 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 000000000000..dd6478b4873c --- /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 000000000000..527999b245a4 --- /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 000000000000..5b30c0774c22 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/AssignShippingMethodToCart.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Cart; + +use Magento\Checkout\Api\Data\ShippingInformationInterface; +use Magento\Checkout\Api\Data\ShippingInformationInterfaceFactory; +use Magento\Checkout\Api\ShippingInformationManagementInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\CartInterface; + +/** + * Assign shipping method to cart + */ +class AssignShippingMethodToCart +{ + /** + * @var ShippingInformationInterfaceFactory + */ + private $shippingInformationFactory; + + /** + * @var ShippingInformationManagementInterface + */ + private $shippingInformationManagement; + + /** + * @param ShippingInformationInterfaceFactory $shippingInformationFactory + * @param ShippingInformationManagementInterface $shippingInformationManagement + */ + public function __construct( + ShippingInformationInterfaceFactory $shippingInformationFactory, + ShippingInformationManagementInterface $shippingInformationManagement + ) { + $this->shippingInformationFactory = $shippingInformationFactory; + $this->shippingInformationManagement = $shippingInformationManagement; + } + + /** + * Assign shipping method to cart + * + * @param CartInterface $cart + * @param AddressInterface $quoteAddress + * @param string $carrierCode + * @param string $methodCode + * @throws GraphQlInputException + * @throws GraphQlNoSuchEntityException + */ + public function execute( + CartInterface $cart, + AddressInterface $quoteAddress, + string $carrierCode, + string $methodCode + ): void { + /** @var ShippingInformationInterface $shippingInformation */ + $shippingInformation = $this->shippingInformationFactory->create([ + 'data' => [ + /* If the address is not a shipping address (but billing) the system will find the proper shipping + address for the selected cart and set the information there (actual for single shipping address) */ + ShippingInformationInterface::SHIPPING_ADDRESS => $quoteAddress, + ShippingInformationInterface::SHIPPING_CARRIER_CODE => $carrierCode, + ShippingInformationInterface::SHIPPING_METHOD_CODE => $methodCode, + ], + ]); + + try { + $this->shippingInformationManagement->saveAddressInformation($cart->getId(), $shippingInformation); + } catch (NoSuchEntityException $e) { + throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage()), $e); + } + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/CreateEmptyCartForCustomer.php b/app/code/Magento/QuoteGraphQl/Model/Cart/CreateEmptyCartForCustomer.php new file mode 100644 index 000000000000..481bad090dac --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/CreateEmptyCartForCustomer.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Cart; + +use Magento\Quote\Api\CartManagementInterface; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\Quote\Model\ResourceModel\Quote\QuoteIdMask as QuoteIdMaskResourceModel; + +/** + * Create empty cart for customer + */ +class CreateEmptyCartForCustomer +{ + /** + * @var CartManagementInterface + */ + private $cartManagement; + + /** + * @var QuoteIdMaskFactory + */ + private $quoteIdMaskFactory; + + /** + * @var QuoteIdMaskResourceModel + */ + private $quoteIdMaskResourceModel; + + /** + * @param CartManagementInterface $cartManagement + * @param QuoteIdMaskFactory $quoteIdMaskFactory + * @param QuoteIdMaskResourceModel $quoteIdMaskResourceModel + */ + public function __construct( + CartManagementInterface $cartManagement, + QuoteIdMaskFactory $quoteIdMaskFactory, + QuoteIdMaskResourceModel $quoteIdMaskResourceModel + ) { + $this->cartManagement = $cartManagement; + $this->quoteIdMaskFactory = $quoteIdMaskFactory; + $this->quoteIdMaskResourceModel = $quoteIdMaskResourceModel; + } + + /** + * Create empty cart for customer + * + * @param int $customerId + * @param string|null $predefinedMaskedQuoteId + * @return string + */ + public function execute(int $customerId, string $predefinedMaskedQuoteId = null): string + { + $quoteId = $this->cartManagement->createEmptyCartForCustomer($customerId); + + $quoteIdMask = $this->quoteIdMaskFactory->create(); + $quoteIdMask->setQuoteId($quoteId); + + if (isset($predefinedMaskedQuoteId)) { + $quoteIdMask->setMaskedId($predefinedMaskedQuoteId); + } + + $this->quoteIdMaskResourceModel->save($quoteIdMask); + return $quoteIdMask->getMaskedId(); + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/CreateEmptyCartForGuest.php b/app/code/Magento/QuoteGraphQl/Model/Cart/CreateEmptyCartForGuest.php new file mode 100644 index 000000000000..a6396ed6352a --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/CreateEmptyCartForGuest.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Cart; + +use Magento\Quote\Api\GuestCartManagementInterface; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\Quote\Model\ResourceModel\Quote\QuoteIdMask as QuoteIdMaskResourceModel; + +/** + * Create empty cart for guest + */ +class CreateEmptyCartForGuest +{ + /** + * @var GuestCartManagementInterface + */ + private $guestCartManagement; + + /** + * @var QuoteIdMaskFactory + */ + private $quoteIdMaskFactory; + + /** + * @var QuoteIdMaskResourceModel + */ + private $quoteIdMaskResourceModel; + + /** + * @param GuestCartManagementInterface $guestCartManagement + * @param QuoteIdMaskFactory $quoteIdMaskFactory + * @param QuoteIdMaskResourceModel $quoteIdMaskResourceModel + */ + public function __construct( + GuestCartManagementInterface $guestCartManagement, + QuoteIdMaskFactory $quoteIdMaskFactory, + QuoteIdMaskResourceModel $quoteIdMaskResourceModel + ) { + $this->guestCartManagement = $guestCartManagement; + $this->quoteIdMaskFactory = $quoteIdMaskFactory; + $this->quoteIdMaskResourceModel = $quoteIdMaskResourceModel; + } + + /** + * Create empty cart for guest + * + * @param string|null $predefinedMaskedQuoteId + * @return string + */ + public function execute(string $predefinedMaskedQuoteId = null): string + { + $maskedQuoteId = $this->guestCartManagement->createEmptyCart(); + + if (isset($predefinedMaskedQuoteId)) { + $quoteIdMask = $this->quoteIdMaskFactory->create(); + $this->quoteIdMaskResourceModel->load($quoteIdMask, $maskedQuoteId, 'masked_id'); + + $quoteIdMask->setMaskedId($predefinedMaskedQuoteId); + $this->quoteIdMaskResourceModel->save($quoteIdMask); + } + return $predefinedMaskedQuoteId ?? $maskedQuoteId; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/ExtractQuoteAddressData.php b/app/code/Magento/QuoteGraphQl/Model/Cart/ExtractQuoteAddressData.php new file mode 100644 index 000000000000..c4d795293220 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/ExtractQuoteAddressData.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Cart; + +use Magento\Customer\Model\Address\AbstractAddress; +use Magento\Framework\Api\ExtensibleDataObjectConverter; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Model\Quote\Address as QuoteAddress; + +/** + * Extract address fields from an Quote Address model + */ +class ExtractQuoteAddressData +{ + /** + * @var ExtensibleDataObjectConverter + */ + private $dataObjectConverter; + + /** + * @param ExtensibleDataObjectConverter $dataObjectConverter + */ + public function __construct(ExtensibleDataObjectConverter $dataObjectConverter) + { + $this->dataObjectConverter = $dataObjectConverter; + } + + /** + * Converts Address model to flat array + * + * @param QuoteAddress $address + * @return array + */ + public function execute(QuoteAddress $address): array + { + $addressData = $this->dataObjectConverter->toFlatArray($address, [], AddressInterface::class); + $addressData['model'] = $address; + + $addressData = array_merge($addressData, [ + 'country' => [ + 'code' => $address->getCountryId(), + 'label' => $address->getCountry() + ], + 'region' => [ + 'code' => $address->getRegionCode(), + 'label' => $address->getRegion() + ], + 'street' => $address->getStreet(), + 'items_weight' => $address->getWeight(), + 'customer_notes' => $address->getCustomerNotes() + ]); + + if (!$address->hasItems()) { + return $addressData; + } + + $addressItemsData = []; + foreach ($address->getAllItems() as $addressItem) { + $addressItemsData[] = [ + 'cart_item_id' => $addressItem->getQuoteItemId(), + 'quantity' => $addressItem->getQty() + ]; + } + $addressData['cart_items'] = $addressItemsData; + + return $addressData; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/GetCartForUser.php b/app/code/Magento/QuoteGraphQl/Model/Cart/GetCartForUser.php new file mode 100644 index 000000000000..3506ffc8c879 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/GetCartForUser.php @@ -0,0 +1,113 @@ +<?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\NoSuchEntityException; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface; +use Magento\Quote\Model\Quote; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Get cart + */ +class GetCartForUser +{ + /** + * @var MaskedQuoteIdToQuoteIdInterface + */ + private $maskedQuoteIdToQuoteId; + + /** + * @var CartRepositoryInterface + */ + private $cartRepository; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @param MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId + * @param CartRepositoryInterface $cartRepository + * @param StoreManagerInterface $storeManager + */ + public function __construct( + MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId, + CartRepositoryInterface $cartRepository, + StoreManagerInterface $storeManager + ) { + $this->maskedQuoteIdToQuoteId = $maskedQuoteIdToQuoteId; + $this->cartRepository = $cartRepository; + $this->storeManager = $storeManager; + } + + /** + * Get cart for user + * + * @param string $cartHash + * @param int|null $customerId + * @return Quote + * @throws GraphQlAuthorizationException + * @throws GraphQlNoSuchEntityException + */ + public function execute(string $cartHash, ?int $customerId): Quote + { + try { + $cartId = $this->maskedQuoteIdToQuoteId->execute($cartHash); + } catch (NoSuchEntityException $exception) { + throw new GraphQlNoSuchEntityException( + __('Could not find a cart with ID "%masked_cart_id"', ['masked_cart_id' => $cartHash]) + ); + } + + try { + /** @var Quote $cart */ + $cart = $this->cartRepository->get($cartId); + } catch (NoSuchEntityException $e) { + throw new GraphQlNoSuchEntityException( + __('Could not find a cart with ID "%masked_cart_id"', ['masked_cart_id' => $cartHash]) + ); + } + + if (false === (bool)$cart->getIsActive()) { + throw new GraphQlNoSuchEntityException( + __('Current user does not have an active cart.') + ); + } + + if ((int)$cart->getStoreId() !== (int)$this->storeManager->getStore()->getId()) { + throw new GraphQlNoSuchEntityException( + __( + 'Wrong store code specified for cart "%masked_cart_id"', + ['masked_cart_id' => $cartHash] + ) + ); + } + + $cartCustomerId = (int)$cart->getCustomerId(); + + /* Guest cart, allow operations */ + if (!$cartCustomerId && null === $customerId) { + return $cart; + } + + if ($cartCustomerId !== $customerId) { + throw new GraphQlAuthorizationException( + __( + 'The current user cannot perform operations on cart "%masked_cart_id"', + ['masked_cart_id' => $cartHash] + ) + ); + } + return $cart; + } +} 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 000000000000..13d6a1d3dce7 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/QuoteAddressFactory.php @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Cart; + +use Magento\Customer\Helper\Address as AddressHelper; +use Magento\CustomerGraphQl\Model\Customer\Address\GetCustomerAddress; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Quote\Model\Quote\Address as QuoteAddress; +use Magento\Quote\Model\Quote\AddressFactory as BaseQuoteAddressFactory; + +/** + * Create QuoteAddress + */ +class QuoteAddressFactory +{ + /** + * @var BaseQuoteAddressFactory + */ + private $quoteAddressFactory; + + /** + * @var GetCustomerAddress + */ + private $getCustomerAddress; + + /** + * @var AddressHelper + */ + private $addressHelper; + + /** + * @param BaseQuoteAddressFactory $quoteAddressFactory + * @param GetCustomerAddress $getCustomerAddress + * @param AddressHelper $addressHelper + */ + public function __construct( + BaseQuoteAddressFactory $quoteAddressFactory, + GetCustomerAddress $getCustomerAddress, + AddressHelper $addressHelper + ) { + $this->quoteAddressFactory = $quoteAddressFactory; + $this->getCustomerAddress = $getCustomerAddress; + $this->addressHelper = $addressHelper; + } + + /** + * Create QuoteAddress based on input data + * + * @param array $addressInput + * @return QuoteAddress + * @throws GraphQlInputException + */ + public function createBasedOnInputData(array $addressInput): QuoteAddress + { + $addressInput['country_id'] = $addressInput['country_code'] ?? ''; + + $maxAllowedLineCount = $this->addressHelper->getStreetLines(); + if (is_array($addressInput['street']) && count($addressInput['street']) > $maxAllowedLineCount) { + throw new GraphQlInputException( + __('"Street Address" cannot contain more than %1 lines.', $maxAllowedLineCount) + ); + } + + $quoteAddress = $this->quoteAddressFactory->create(); + $quoteAddress->addData($addressInput); + return $quoteAddress; + } + + /** + * Create Quote Address based on Customer Address + * + * @param int $customerAddressId + * @param int $customerId + * @return QuoteAddress + * @throws GraphQlInputException + * @throws GraphQlAuthorizationException + * @throws GraphQlNoSuchEntityException + */ + public function createBasedOnCustomerAddress(int $customerAddressId, int $customerId): QuoteAddress + { + $customerAddress = $this->getCustomerAddress->execute((int)$customerAddressId, $customerId); + + $quoteAddress = $this->quoteAddressFactory->create(); + try { + $quoteAddress->importCustomerAddressData($customerAddress); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage()), $e); + } + return $quoteAddress; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetBillingAddressOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetBillingAddressOnCart.php new file mode 100644 index 000000000000..c2bac13c0706 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetBillingAddressOnCart.php @@ -0,0 +1,103 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Cart; + +use Magento\CustomerGraphQl\Model\Customer\GetCustomer; +use Magento\Framework\GraphQl\Exception\GraphQlAuthenticationException; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Quote\Api\Data\CartInterface; + +/** + * Set billing address for a specified shopping cart + */ +class SetBillingAddressOnCart +{ + /** + * @var QuoteAddressFactory + */ + private $quoteAddressFactory; + + /** + * @var GetCustomer + */ + private $getCustomer; + + /** + * @var AssignBillingAddressToCart + */ + private $assignBillingAddressToCart; + + /** + * @param QuoteAddressFactory $quoteAddressFactory + * @param GetCustomer $getCustomer + * @param AssignBillingAddressToCart $assignBillingAddressToCart + */ + public function __construct( + QuoteAddressFactory $quoteAddressFactory, + GetCustomer $getCustomer, + AssignBillingAddressToCart $assignBillingAddressToCart + ) { + $this->quoteAddressFactory = $quoteAddressFactory; + $this->getCustomer = $getCustomer; + $this->assignBillingAddressToCart = $assignBillingAddressToCart; + } + + /** + * Set billing address for a specified shopping cart + * + * @param ContextInterface $context + * @param CartInterface $cart + * @param array $billingAddressInput + * @return void + * @throws GraphQlInputException + * @throws GraphQlAuthenticationException + * @throws GraphQlAuthorizationException + * @throws GraphQlNoSuchEntityException + */ + public function execute(ContextInterface $context, CartInterface $cart, array $billingAddressInput): void + { + $customerAddressId = $billingAddressInput['customer_address_id'] ?? null; + $addressInput = $billingAddressInput['address'] ?? null; + $useForShipping = isset($billingAddressInput['use_for_shipping']) + ? (bool)$billingAddressInput['use_for_shipping'] : false; + + if (null === $customerAddressId && null === $addressInput) { + throw new GraphQlInputException( + __('The billing address must contain either "customer_address_id" or "address".') + ); + } + + if ($customerAddressId && $addressInput) { + throw new GraphQlInputException( + __('The billing address cannot contain "customer_address_id" and "address" at the same time.') + ); + } + + $addresses = $cart->getAllShippingAddresses(); + if ($useForShipping && count($addresses) > 1) { + throw new GraphQlInputException( + __('Using the "use_for_shipping" option with multishipping is not possible.') + ); + } + + if (null === $customerAddressId) { + $billingAddress = $this->quoteAddressFactory->createBasedOnInputData($addressInput); + } else { + $customer = $this->getCustomer->execute($context); + $billingAddress = $this->quoteAddressFactory->createBasedOnCustomerAddress( + (int)$customerAddressId, + (int)$customer->getId() + ); + } + + $this->assignBillingAddressToCart->execute($cart, $billingAddress, $useForShipping); + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php new file mode 100644 index 000000000000..6b0e2a311bf4 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php @@ -0,0 +1,88 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Cart; + +use Magento\CustomerGraphQl\Model\Customer\GetCustomer; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Quote\Api\Data\CartInterface; + +/** + * Set single shipping address for a specified shopping cart + */ +class SetShippingAddressesOnCart implements SetShippingAddressesOnCartInterface +{ + /** + * @var QuoteAddressFactory + */ + private $quoteAddressFactory; + + /** + * @var GetCustomer + */ + private $getCustomer; + + /** + * @var AssignShippingAddressToCart + */ + private $assignShippingAddressToCart; + + /** + * @param QuoteAddressFactory $quoteAddressFactory + * @param GetCustomer $getCustomer + * @param AssignShippingAddressToCart $assignShippingAddressToCart + */ + public function __construct( + QuoteAddressFactory $quoteAddressFactory, + GetCustomer $getCustomer, + AssignShippingAddressToCart $assignShippingAddressToCart + ) { + $this->quoteAddressFactory = $quoteAddressFactory; + $this->getCustomer = $getCustomer; + $this->assignShippingAddressToCart = $assignShippingAddressToCart; + } + + /** + * @inheritdoc + */ + public function execute(ContextInterface $context, CartInterface $cart, array $shippingAddressesInput): void + { + if (count($shippingAddressesInput) > 1) { + throw new GraphQlInputException( + __('You cannot specify multiple shipping addresses.') + ); + } + $shippingAddressInput = current($shippingAddressesInput); + $customerAddressId = $shippingAddressInput['customer_address_id'] ?? null; + $addressInput = $shippingAddressInput['address'] ?? null; + + if (null === $customerAddressId && null === $addressInput) { + throw new GraphQlInputException( + __('The shipping address must contain either "customer_address_id" or "address".') + ); + } + + if ($customerAddressId && $addressInput) { + throw new GraphQlInputException( + __('The shipping address cannot contain "customer_address_id" and "address" at the same time.') + ); + } + + if (null === $customerAddressId) { + $shippingAddress = $this->quoteAddressFactory->createBasedOnInputData($addressInput); + } else { + $customer = $this->getCustomer->execute($context); + $shippingAddress = $this->quoteAddressFactory->createBasedOnCustomerAddress( + (int)$customerAddressId, + (int)$customer->getId() + ); + } + + $this->assignShippingAddressToCart->execute($cart, $shippingAddress); + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCartInterface.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCartInterface.php new file mode 100644 index 000000000000..eb0f3522102c --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCartInterface.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 addresses for a specified shopping cart + * + * All objects that are responsible for setting shipping addresses on a cart via GraphQl + * should implement this interface. + */ +interface SetShippingAddressesOnCartInterface +{ + /** + * Set shipping addresses for a specified shopping cart + * + * @param ContextInterface $context + * @param CartInterface $cart + * @param array $shippingAddressesInput + * @return void + * @throws GraphQlInputException + * @throws GraphQlAuthorizationException + * @throws GraphQlAuthenticationException + * @throws GraphQlNoSuchEntityException + */ + public function execute(ContextInterface $context, CartInterface $cart, array $shippingAddressesInput): void; +} 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 000000000000..b2526bdc04e9 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCart.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Cart; + +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Quote\Api\Data\CartInterface; + +/** + * Set single shipping method for a specified shopping cart + */ +class SetShippingMethodsOnCart implements SetShippingMethodsOnCartInterface +{ + /** + * @var AssignShippingMethodToCart + */ + private $assignShippingMethodToCart; + + /** + * @param AssignShippingMethodToCart $assignShippingMethodToCart + */ + public function __construct( + AssignShippingMethodToCart $assignShippingMethodToCart + ) { + $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['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']; + + $shippingAddress = $cart->getShippingAddress(); + $this->assignShippingMethodToCart->execute($cart, $shippingAddress, $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 000000000000..fa6c6cf0923e --- /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/CustomizableOption.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOption.php new file mode 100644 index 000000000000..3199668060ea --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOption.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\CartItem\DataProvider; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Quote\Model\Quote\Item as QuoteItem; + +/** + * Custom Option Data provider + */ +class CustomizableOption +{ + /** + * @var CustomizableOptionValueInterface + */ + private $customizableOptionValue; + + /** + * @param CustomizableOptionValueInterface $customOptionValueDataProvider + */ + public function __construct( + CustomizableOptionValueInterface $customOptionValueDataProvider + ) { + $this->customizableOptionValue = $customOptionValueDataProvider; + } + + /** + * Retrieve custom option data + * + * @param QuoteItem $cartItem + * @param int $optionId + * @return array + * @throws LocalizedException + */ + public function getData(QuoteItem $cartItem, int $optionId): array + { + $product = $cartItem->getProduct(); + $option = $product->getOptionById($optionId); + + if (!$option) { + return []; + } + + $selectedOption = $cartItem->getOptionByCode('option_' . $option->getId()); + + $selectedOptionValueData = $this->customizableOptionValue->getData( + $cartItem, + $option, + $selectedOption + ); + + return [ + 'id' => $option->getId(), + 'label' => $option->getTitle(), + 'type' => $option->getType(), + 'values' => $selectedOptionValueData, + 'sort_order' => $option->getSortOrder(), + 'is_required' => $option->getIsRequire(), + ]; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Composite.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Composite.php new file mode 100644 index 000000000000..529784512530 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Composite.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\CartItem\DataProvider\CustomizableOptionValue; + +use Magento\Catalog\Model\Product\Option; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\ObjectManagerInterface; +use Magento\Quote\Model\Quote\Item as QuoteItem; +use Magento\Quote\Model\Quote\Item\Option as SelectedOption; +use Magento\QuoteGraphQl\Model\CartItem\DataProvider\CustomizableOptionValueInterface; + +/** + * @inheritdoc + */ +class Composite implements CustomizableOptionValueInterface +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var CustomizableOptionValueInterface[] + */ + private $customizableOptionValues; + + /** + * @param ObjectManagerInterface $objectManager + * @param CustomizableOptionValueInterface[] $customizableOptionValues + */ + public function __construct( + ObjectManagerInterface $objectManager, + array $customizableOptionValues = [] + ) { + $this->objectManager = $objectManager; + $this->customizableOptionValues = $customizableOptionValues; + } + + /** + * @inheritdoc + */ + public function getData( + QuoteItem $cartItem, + Option $option, + SelectedOption $selectedOption + ): array { + $optionType = $option->getType(); + + if (!array_key_exists($optionType, $this->customizableOptionValues)) { + throw new GraphQlInputException(__('Option type "%1" is not supported', $optionType)); + } + $customizableOptionValueClassName = $this->customizableOptionValues[$optionType]; + + $customizableOptionValue = $this->objectManager->get($customizableOptionValueClassName); + if (!$customizableOptionValue instanceof CustomizableOptionValueInterface) { + throw new LocalizedException( + __('%1 doesn\'t implement CustomizableOptionValueInterface', $customizableOptionValueClassName) + ); + } + return $customizableOptionValue->getData($cartItem, $option, $selectedOption); + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Dropdown.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Dropdown.php new file mode 100644 index 000000000000..74ed40346500 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Dropdown.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\CartItem\DataProvider\CustomizableOptionValue; + +use Magento\Catalog\Model\Product\Option; +use Magento\Catalog\Model\Product\Option\Type\Select as SelectOptionType; +use Magento\Quote\Model\Quote\Item as QuoteItem; +use Magento\Quote\Model\Quote\Item\Option as SelectedOption; +use Magento\QuoteGraphQl\Model\CartItem\DataProvider\CustomizableOptionValueInterface; + +/** + * @inheritdoc + */ +class Dropdown implements CustomizableOptionValueInterface +{ + /** + * @var PriceUnitLabel + */ + private $priceUnitLabel; + + /** + * @param PriceUnitLabel $priceUnitLabel + */ + public function __construct( + PriceUnitLabel $priceUnitLabel + ) { + $this->priceUnitLabel = $priceUnitLabel; + } + + /** + * @inheritdoc + */ + public function getData( + QuoteItem $cartItem, + Option $option, + SelectedOption $selectedOption + ): array { + /** @var SelectOptionType $optionTypeRenderer */ + $optionTypeRenderer = $option->groupFactory($option->getType()) + ->setOption($option) + ->setConfigurationItemOption($selectedOption); + + $selectedValue = $selectedOption->getValue(); + $optionValue = $option->getValueById($selectedValue); + $optionPriceType = (string)$optionValue->getPriceType(); + $priceValueUnits = $this->priceUnitLabel->getData($optionPriceType); + + $selectedOptionValueData = [ + 'id' => $selectedOption->getId(), + 'label' => $optionTypeRenderer->getFormattedOptionValue($selectedValue), + 'value' => $selectedValue, + 'price' => [ + 'type' => strtoupper($optionPriceType), + 'units' => $priceValueUnits, + 'value' => $optionValue->getPrice(), + ] + ]; + return [$selectedOptionValueData]; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Multiple.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Multiple.php new file mode 100644 index 000000000000..619e84568a54 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Multiple.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\CartItem\DataProvider\CustomizableOptionValue; + +use Magento\Catalog\Model\Product\Option; +use Magento\Catalog\Model\Product\Option\Type\DefaultType; +use Magento\Quote\Model\Quote\Item as QuoteItem; +use Magento\Quote\Model\Quote\Item\Option as SelectedOption; +use Magento\QuoteGraphQl\Model\CartItem\DataProvider\CustomizableOptionValueInterface; + +/** + * Multiple Option Value Data provider + */ +class Multiple implements CustomizableOptionValueInterface +{ + /** + * @var PriceUnitLabel + */ + private $priceUnitLabel; + + /** + * @param PriceUnitLabel $priceUnitLabel + */ + public function __construct( + PriceUnitLabel $priceUnitLabel + ) { + $this->priceUnitLabel = $priceUnitLabel; + } + + /** + * @inheritdoc + */ + public function getData( + QuoteItem $cartItem, + Option $option, + SelectedOption $selectedOption + ): array { + $selectedOptionValueData = []; + $optionIds = explode(',', $selectedOption->getValue()); + + if (0 === count($optionIds)) { + return $selectedOptionValueData; + } + + foreach ($optionIds as $optionId) { + $optionValue = $option->getValueById($optionId); + $priceValueUnits = $this->priceUnitLabel->getData($optionValue->getPriceType()); + + $selectedOptionValueData[] = [ + 'id' => $selectedOption->getId(), + 'label' => $optionValue->getTitle(), + 'price' => [ + 'type' => strtoupper($optionValue->getPriceType()), + 'units' => $priceValueUnits, + 'value' => $optionValue->getPrice(), + ], + ]; + } + + return $selectedOptionValueData; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/PriceUnitLabel.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/PriceUnitLabel.php new file mode 100644 index 000000000000..bee2e54ed5f4 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/PriceUnitLabel.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\CartItem\DataProvider\CustomizableOptionValue; + +use Magento\Catalog\Model\Config\Source\ProductPriceOptionsInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Custom Option Data provider + */ +class PriceUnitLabel +{ + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @param StoreManagerInterface $storeManager + */ + public function __construct( + StoreManagerInterface $storeManager + ) { + $this->storeManager = $storeManager; + } + + /** + * Retrieve price value unit + * + * @param string $priceType + * @return string + */ + public function getData(string $priceType): string + { + if (ProductPriceOptionsInterface::VALUE_PERCENT == $priceType) { + return '%'; + } + + return $this->getCurrencySymbol(); + } + + /** + * Get currency symbol + * + * @return string + * @throws NoSuchEntityException + */ + private function getCurrencySymbol(): string + { + /** @var Store|StoreInterface $store */ + $store = $this->storeManager->getStore(); + + return $store->getBaseCurrency()->getCurrencySymbol(); + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Text.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Text.php new file mode 100644 index 000000000000..96f11badac82 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Text.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\CartItem\DataProvider\CustomizableOptionValue; + +use Magento\Catalog\Model\Product\Option; +use Magento\Catalog\Model\Product\Option\Type\Text as TextOptionType; +use Magento\Quote\Model\Quote\Item as QuoteItem; +use Magento\Quote\Model\Quote\Item\Option as SelectedOption; +use Magento\QuoteGraphQl\Model\CartItem\DataProvider\CustomizableOptionValueInterface; + +/** + * @inheritdoc + */ +class Text implements CustomizableOptionValueInterface +{ + /** + * @var PriceUnitLabel + */ + private $priceUnitLabel; + + /** + * @param PriceUnitLabel $priceUnitLabel + */ + public function __construct( + PriceUnitLabel $priceUnitLabel + ) { + $this->priceUnitLabel = $priceUnitLabel; + } + + /** + * @inheritdoc + */ + public function getData( + QuoteItem $cartItem, + Option $option, + SelectedOption $selectedOption + ): array { + /** @var TextOptionType $optionTypeRenderer */ + $optionTypeRenderer = $option->groupFactory($option->getType()); + $optionTypeRenderer->setOption($option); + $priceValueUnits = $this->priceUnitLabel->getData($option->getPriceType()); + + $selectedOptionValueData = [ + 'id' => $selectedOption->getId(), + 'label' => '', + 'value' => $optionTypeRenderer->getFormattedOptionValue($selectedOption->getValue()), + 'price' => [ + 'type' => strtoupper($option->getPriceType()), + 'units' => $priceValueUnits, + 'value' => $option->getPrice(), + ], + ]; + return [$selectedOptionValueData]; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValueInterface.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValueInterface.php new file mode 100644 index 000000000000..fce8b80f6d47 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValueInterface.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\CartItem\DataProvider; + +use Magento\Catalog\Model\Product\Option; +use Magento\Quote\Model\Quote\Item as QuoteItem; +use Magento\Quote\Model\Quote\Item\Option as SelectedOption; + +/** + * Customizable Option Value Data provider + */ +interface CustomizableOptionValueInterface +{ + /** + * Customizable Option Value Data Provider + * + * @param QuoteItem $cartItem + * @param Option $option + * @param SelectedOption $selectedOption + * @return array + */ + public function getData( + QuoteItem $cartItem, + Option $option, + SelectedOption $selectedOption + ): array; +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/AddSimpleProductsToCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/AddSimpleProductsToCart.php new file mode 100644 index 000000000000..82ffd0970d67 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/AddSimpleProductsToCart.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\QuoteGraphQl\Model\Cart\AddProductsToCart; +use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; + +/** + * Add simple products to cart GraphQl resolver + * {@inheritdoc} + */ +class AddSimpleProductsToCart implements ResolverInterface +{ + /** + * @var GetCartForUser + */ + private $getCartForUser; + + /** + * @var AddProductsToCart + */ + private $addProductsToCart; + + /** + * @param GetCartForUser $getCartForUser + * @param AddProductsToCart $addProductsToCart + */ + public function __construct( + GetCartForUser $getCartForUser, + AddProductsToCart $addProductsToCart + ) { + $this->getCartForUser = $getCartForUser; + $this->addProductsToCart = $addProductsToCart; + } + + /** + * @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']['cartItems']) || empty($args['input']['cartItems']) + || !is_array($args['input']['cartItems']) + ) { + throw new GraphQlInputException(__('Required parameter "cartItems" is missing')); + } + $cartItems = $args['input']['cartItems']; + + $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId()); + $this->addProductsToCart->execute($cart, $cartItems); + + return [ + '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 000000000000..8251089abcd6 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/AppliedCoupon.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Quote\Api\CouponManagementInterface; + +/** + * @inheritdoc + */ +class AppliedCoupon implements ResolverInterface +{ + /** + * @var CouponManagementInterface + */ + private $couponManagement; + + /** + * @param CouponManagementInterface $couponManagement + */ + public function __construct( + CouponManagementInterface $couponManagement + ) { + $this->couponManagement = $couponManagement; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + $cart = $value['model']; + $cartId = $cart->getId(); + + $appliedCoupon = $this->couponManagement->get($cartId); + return $appliedCoupon ? ['code' => $appliedCoupon] : null; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/ApplyCouponToCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/ApplyCouponToCart.php new file mode 100644 index 000000000000..4de046468118 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/ApplyCouponToCart.php @@ -0,0 +1,93 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\Exception\CouldNotSaveException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\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\CouponManagementInterface; +use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; + +/** + * @inheritdoc + */ +class ApplyCouponToCart implements ResolverInterface +{ + /** + * @var GetCartForUser + */ + private $getCartForUser; + + /** + * @var CouponManagementInterface + */ + private $couponManagement; + + /** + * @param GetCartForUser $getCartForUser + * @param CouponManagementInterface $couponManagement + */ + public function __construct( + GetCartForUser $getCartForUser, + CouponManagementInterface $couponManagement + ) { + $this->getCartForUser = $getCartForUser; + $this->couponManagement = $couponManagement; + } + + /** + * @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']['coupon_code']) || empty($args['input']['coupon_code'])) { + throw new GraphQlInputException(__('Required parameter "coupon_code" is missing')); + } + $couponCode = $args['input']['coupon_code']; + + $currentUserId = $context->getUserId(); + $cart = $this->getCartForUser->execute($maskedCartId, $currentUserId); + $cartId = $cart->getId(); + + /* Check current cart does not have coupon code applied */ + $appliedCouponCode = $this->couponManagement->get($cartId); + if (!empty($appliedCouponCode)) { + throw new GraphQlInputException( + __('A coupon is already applied to the cart. Please remove it to apply another') + ); + } + + try { + $this->couponManagement->set($cartId, $couponCode); + } catch (NoSuchEntityException $e) { + $message = $e->getMessage(); + if (preg_match('/The "\d+" Cart doesn\'t contain products/', $message)) { + $message = 'Cart does not contain products.'; + } + throw new GraphQlNoSuchEntityException(__($message), $e); + } catch (CouldNotSaveException $e) { + throw new LocalizedException(__($e->getMessage()), $e); + } + + return [ + 'cart' => [ + 'model' => $cart, + ], + ]; + } +} 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 000000000000..907d77855059 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/AvailablePaymentMethods.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Checkout\Api\PaymentInformationManagementInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Quote\Api\Data\CartInterface; + +/** + * Get list of active payment methods resolver. + */ +class AvailablePaymentMethods implements ResolverInterface +{ + /** + * @var PaymentInformationManagementInterface + */ + private $informationManagement; + + /** + * @param PaymentInformationManagementInterface $informationManagement + */ + public function __construct(PaymentInformationManagementInterface $informationManagement) + { + $this->informationManagement = $informationManagement; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + + $cart = $value['model']; + return $this->getPaymentMethodsData($cart); + } + + /** + * Collect and return information about available payment methods + * + * @param CartInterface $cart + * @return array + */ + private function getPaymentMethodsData(CartInterface $cart): array + { + $paymentInformation = $this->informationManagement->getPaymentInformation($cart->getId()); + $paymentMethods = $paymentInformation->getPaymentMethods(); + + $paymentMethodsData = []; + foreach ($paymentMethods as $paymentMethod) { + $paymentMethodsData[] = [ + 'title' => $paymentMethod->getTitle(), + 'code' => $paymentMethod->getCode(), + ]; + } + return $paymentMethodsData; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/BillingAddress.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/BillingAddress.php new file mode 100644 index 000000000000..a6bb0b0d04df --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/BillingAddress.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Quote\Api\Data\CartInterface; +use Magento\QuoteGraphQl\Model\Cart\ExtractQuoteAddressData; + +/** + * @inheritdoc + */ +class BillingAddress implements ResolverInterface +{ + /** + * @var ExtractQuoteAddressData + */ + private $extractQuoteAddressData; + + /** + * @param ExtractQuoteAddressData $extractQuoteAddressData + */ + public function __construct(ExtractQuoteAddressData $extractQuoteAddressData) + { + $this->extractQuoteAddressData = $extractQuoteAddressData; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + /** @var CartInterface $cart */ + $cart = $value['model']; + + $billingAddress = $cart->getBillingAddress(); + if (null === $billingAddress) { + return null; + } + + $addressData = $this->extractQuoteAddressData->execute($billingAddress); + return $addressData; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/Cart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/Cart.php new file mode 100644 index 000000000000..db74b1fa4174 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/Cart.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; + +/** + * @inheritdoc + */ +class Cart implements ResolverInterface +{ + /** + * @var GetCartForUser + */ + private $getCartForUser; + + /** + * @param GetCartForUser $getCartForUser + */ + public function __construct( + GetCartForUser $getCartForUser + ) { + $this->getCartForUser = $getCartForUser; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($args['cart_id']) || empty($args['cart_id'])) { + throw new GraphQlInputException(__('Required parameter "cart_id" is missing')); + } + $maskedCartId = $args['cart_id']; + + $currentUserId = $context->getUserId(); + $cart = $this->getCartForUser->execute($maskedCartId, $currentUserId); + + return [ + 'model' => $cart, + ]; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartAddressTypeResolver.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartAddressTypeResolver.php new file mode 100644 index 000000000000..6f7c70b09cb0 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartAddressTypeResolver.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Customer\Model\Address\AbstractAddress; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; +use Magento\Quote\Model\Quote\Address; + +/** + * @inheritdoc + */ +class CartAddressTypeResolver implements TypeResolverInterface +{ + /** + * @inheritdoc + */ + public function resolveType(array $data): string + { + if (!isset($data['model'])) { + throw new LocalizedException(__('Missing key "model" in cart address data')); + } + /** @var Address $address */ + $address = $data['model']; + + if ($address->getAddressType() == AbstractAddress::TYPE_SHIPPING) { + $addressType = 'ShippingCartAddress'; + } elseif ($address->getAddressType() == AbstractAddress::TYPE_BILLING) { + $addressType = 'BillingCartAddress'; + } else { + throw new LocalizedException(__('Unsupported cart address type')); + } + return $addressType; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartEmail.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartEmail.php new file mode 100644 index 000000000000..8d0cb114d831 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartEmail.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Quote\Model\Quote; +use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; + +/** + * @inheritdoc + */ +class CartEmail implements ResolverInterface +{ + /** + * @var GetCartForUser + */ + private $getCartForUser; + + /** + * @param GetCartForUser $getCartForUser + */ + public function __construct( + GetCartForUser $getCartForUser + ) { + $this->getCartForUser = $getCartForUser; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + /** @var Quote $cart */ + $cart = $value['model']; + + return $cart->getCustomerEmail(); + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemTypeResolver.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemTypeResolver.php new file mode 100644 index 000000000000..962463860a59 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemTypeResolver.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; + +/** + * @inheritdoc + */ +class CartItemTypeResolver implements TypeResolverInterface +{ + /** + * @var array + */ + private $supportedTypes = []; + + /** + * @param array $supportedTypes + */ + public function __construct(array $supportedTypes = []) + { + $this->supportedTypes = $supportedTypes; + } + + /** + * @inheritdoc + */ + public function resolveType(array $data) : string + { + if (!isset($data['product'])) { + throw new LocalizedException(__('Missing key "product" in cart data')); + } + $productData = $data['product']; + + if (!isset($productData['type_id'])) { + throw new LocalizedException(__('Missing key "type_id" in product data')); + } + $productTypeId = $productData['type_id']; + + if (!isset($this->supportedTypes[$productTypeId])) { + throw new LocalizedException( + __('Product "%product_type" type is not supported', ['product_type' => $productTypeId]) + ); + } + return $this->supportedTypes[$productTypeId]; + } +} 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 000000000000..f259dcd4a78f --- /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(), + 'quantity' => $cartItem->getQty(), + 'product' => $productData, + 'model' => $cartItem, + ]; + } + return $itemsData; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartPrices.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartPrices.php new file mode 100644 index 000000000000..7a9bdd926764 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartPrices.php @@ -0,0 +1,87 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Address\Total; +use Magento\Quote\Model\Quote\TotalsCollector; + +/** + * @inheritdoc + */ +class CartPrices implements ResolverInterface +{ + /** + * @var TotalsCollector + */ + private $totalsCollector; + + /** + * @param TotalsCollector $totalsCollector + */ + public function __construct( + TotalsCollector $totalsCollector + ) { + $this->totalsCollector = $totalsCollector; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + + /** @var Quote $quote */ + $quote = $value['model']; + $cartTotals = $this->totalsCollector->collectQuoteTotals($quote); + $currency = $quote->getQuoteCurrencyCode(); + + return [ + 'grand_total' => ['value' => $cartTotals->getGrandTotal(), 'currency' => $currency], + 'subtotal_including_tax' => ['value' => $cartTotals->getSubtotalInclTax(), 'currency' => $currency], + 'subtotal_excluding_tax' => ['value' => $cartTotals->getSubtotal(), 'currency' => $currency], + 'subtotal_with_discount_excluding_tax' => [ + 'value' => $cartTotals->getSubtotalWithDiscount(), 'currency' => $currency + ], + 'applied_taxes' => $this->getAppliedTaxes($cartTotals, $currency), + 'model' => $quote + ]; + } + + /** + * Returns taxes applied to the current quote + * + * @param Total $total + * @param string $currency + * @return array + */ + private function getAppliedTaxes(Total $total, string $currency): array + { + $appliedTaxesData = []; + $appliedTaxes = $total->getAppliedTaxes(); + + if (count($appliedTaxes) === 0) { + return $appliedTaxesData; + } + + foreach ($appliedTaxes as $appliedTax) { + $appliedTaxesData[] = [ + 'label' => $appliedTax['id'], + 'amount' => ['value' => $appliedTax['amount'], 'currency' => $currency] + ]; + } + return $appliedTaxesData; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CreateEmptyCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CreateEmptyCart.php new file mode 100644 index 000000000000..f020527d958e --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CreateEmptyCart.php @@ -0,0 +1,107 @@ +<?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\NoSuchEntityException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlAlreadyExistsException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface; +use Magento\QuoteGraphQl\Model\Cart\CreateEmptyCartForCustomer; +use Magento\QuoteGraphQl\Model\Cart\CreateEmptyCartForGuest; + +/** + * @inheritdoc + */ +class CreateEmptyCart implements ResolverInterface +{ + /** + * @var CreateEmptyCartForCustomer + */ + private $createEmptyCartForCustomer; + + /** + * @var CreateEmptyCartForGuest + */ + private $createEmptyCartForGuest; + + /** + * @var MaskedQuoteIdToQuoteIdInterface + */ + private $maskedQuoteIdToQuoteId; + + /** + * @param CreateEmptyCartForCustomer $createEmptyCartForCustomer + * @param CreateEmptyCartForGuest $createEmptyCartForGuest + * @param MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId + */ + public function __construct( + CreateEmptyCartForCustomer $createEmptyCartForCustomer, + CreateEmptyCartForGuest $createEmptyCartForGuest, + MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId + ) { + $this->createEmptyCartForCustomer = $createEmptyCartForCustomer; + $this->createEmptyCartForGuest = $createEmptyCartForGuest; + $this->maskedQuoteIdToQuoteId = $maskedQuoteIdToQuoteId; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + $customerId = $context->getUserId(); + + $predefinedMaskedQuoteId = null; + if (isset($args['input']['cart_id'])) { + $predefinedMaskedQuoteId = $args['input']['cart_id']; + $this->validateMaskedId($predefinedMaskedQuoteId); + } + + $maskedQuoteId = (0 === $customerId || null === $customerId) + ? $this->createEmptyCartForGuest->execute($predefinedMaskedQuoteId) + : $this->createEmptyCartForCustomer->execute($customerId, $predefinedMaskedQuoteId); + return $maskedQuoteId; + } + + /** + * Validate masked id + * + * @param string $maskedId + * @throws GraphQlAlreadyExistsException + * @throws GraphQlInputException + */ + private function validateMaskedId(string $maskedId): void + { + if (mb_strlen($maskedId) != 32) { + throw new GraphQlInputException(__('Cart ID length should to be 32 symbols.')); + } + + if ($this->isQuoteWithSuchMaskedIdAlreadyExists($maskedId)) { + throw new GraphQlAlreadyExistsException(__('Cart with ID "%1" already exists.', $maskedId)); + } + } + + /** + * Check is quote with such maskedId already exists + * + * @param string $maskedId + * @return bool + */ + private function isQuoteWithSuchMaskedIdAlreadyExists(string $maskedId): bool + { + try { + $this->maskedQuoteIdToQuoteId->execute($maskedId); + return true; + } catch (NoSuchEntityException $e) { + return false; + } + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CustomizableOptions.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CustomizableOptions.php new file mode 100644 index 000000000000..a681b2ca0d00 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CustomizableOptions.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\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; +use Magento\QuoteGraphQl\Model\CartItem\DataProvider\CustomizableOption; + +/** + * @inheritdoc + */ +class CustomizableOptions implements ResolverInterface +{ + /** + * @var CustomizableOption + */ + private $customizableOption; + + /** + * @param CustomizableOption $customizableOption + */ + public function __construct( + CustomizableOption $customizableOption + ) { + $this->customizableOption = $customizableOption; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + + /** @var QuoteItem $cartItem */ + $cartItem = $value['model']; + $quoteItemOption = $cartItem->getOptionByCode('option_ids'); + + if (null === $quoteItemOption) { + return []; + } + + $customizableOptionsData = []; + $customizableOptionIds = explode(',', $quoteItemOption->getValue()); + + foreach ($customizableOptionIds as $customizableOptionId) { + $customizableOption = $this->customizableOption->getData( + $cartItem, + (int)$customizableOptionId + ); + $customizableOptionsData[] = $customizableOption; + } + return $customizableOptionsData; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/PlaceOrder.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/PlaceOrder.php new file mode 100644 index 000000000000..1672474bb3dd --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/PlaceOrder.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Quote\Api\CartManagementInterface; +use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; +use Magento\Sales\Api\OrderRepositoryInterface; + +/** + * @inheritdoc + */ +class PlaceOrder implements ResolverInterface +{ + /** + * @var CartManagementInterface + */ + private $cartManagement; + + /** + * @var GetCartForUser + */ + private $getCartForUser; + + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @param GetCartForUser $getCartForUser + * @param CartManagementInterface $cartManagement + * @param OrderRepositoryInterface $orderRepository + */ + public function __construct( + GetCartForUser $getCartForUser, + CartManagementInterface $cartManagement, + OrderRepositoryInterface $orderRepository + ) { + $this->getCartForUser = $getCartForUser; + $this->cartManagement = $cartManagement; + $this->orderRepository = $orderRepository; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($args['input']['cart_id']) || empty($args['input']['cart_id'])) { + throw new GraphQlInputException(__('Required parameter "cart_id" is missing')); + } + $maskedCartId = $args['input']['cart_id']; + + $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId()); + + if ($context->getUserId() === 0) { + if (!$cart->getCustomerEmail()) { + throw new GraphQlInputException(__("Guest email for cart is missing. Please enter")); + } + $cart->setCheckoutMethod(CartManagementInterface::METHOD_GUEST); + } + + try { + $orderId = $this->cartManagement->placeOrder($cart->getId()); + $order = $this->orderRepository->get($orderId); + + return [ + 'order' => [ + 'order_id' => $order->getIncrementId(), + ], + ]; + } catch (NoSuchEntityException $e) { + throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__('Unable to place order: %message', ['message' => $e->getMessage()]), $e); + } + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveCouponFromCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveCouponFromCart.php new file mode 100644 index 000000000000..f81ea3020d3d --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveCouponFromCart.php @@ -0,0 +1,80 @@ +<?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\CouldNotDeleteException; +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\CouponManagementInterface; +use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; + +/** + * @inheritdoc + */ +class RemoveCouponFromCart implements ResolverInterface +{ + /** + * @var GetCartForUser + */ + private $getCartForUser; + + /** + * @var CouponManagementInterface + */ + private $couponManagement; + + /** + * @param GetCartForUser $getCartForUser + * @param CouponManagementInterface $couponManagement + */ + public function __construct( + GetCartForUser $getCartForUser, + CouponManagementInterface $couponManagement + ) { + $this->getCartForUser = $getCartForUser; + $this->couponManagement = $couponManagement; + } + + /** + * @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']; + + $currentUserId = $context->getUserId(); + $cart = $this->getCartForUser->execute($maskedCartId, $currentUserId); + $cartId = $cart->getId(); + + try { + $this->couponManagement->remove($cartId); + } catch (NoSuchEntityException $e) { + $message = $e->getMessage(); + if (preg_match('/The "\d+" Cart doesn\'t contain products/', $message)) { + $message = 'Cart does not contain products'; + } + throw new GraphQlNoSuchEntityException(__($message), $e); + } catch (CouldNotDeleteException $e) { + throw new LocalizedException(__($e->getMessage()), $e); + } + + return [ + 'cart' => [ + 'model' => $cart, + ], + ]; + } +} 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 000000000000..63e66f121f55 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveItemFromCart.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Quote\Api\CartItemRepositoryInterface; +use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; + +/** + * @inheritdoc + */ +class RemoveItemFromCart implements ResolverInterface +{ + /** + * @var GetCartForUser + */ + private $getCartForUser; + + /** + * @var CartItemRepositoryInterface + */ + private $cartItemRepository; + + /** + * @param GetCartForUser $getCartForUser + * @param CartItemRepositoryInterface $cartItemRepository + */ + public function __construct( + GetCartForUser $getCartForUser, + CartItemRepositoryInterface $cartItemRepository + ) { + $this->getCartForUser = $getCartForUser; + $this->cartItemRepository = $cartItemRepository; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($args['input']['cart_id']) || empty($args['input']['cart_id'])) { + throw new GraphQlInputException(__('Required parameter "cart_id" is missing.')); + } + $maskedCartId = $args['input']['cart_id']; + + if (!isset($args['input']['cart_item_id']) || empty($args['input']['cart_item_id'])) { + throw new GraphQlInputException(__('Required parameter "cart_item_id" is missing.')); + } + $itemId = $args['input']['cart_item_id']; + + $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId()); + + try { + $this->cartItemRepository->deleteById((int)$cart->getId(), $itemId); + } catch (NoSuchEntityException $e) { + throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage()), $e); + } + + return [ + 'cart' => [ + 'model' => $cart, + ], + ]; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/SelectedPaymentMethod.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/SelectedPaymentMethod.php new file mode 100644 index 000000000000..8cda06eba3c9 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/SelectedPaymentMethod.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; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * @inheritdoc + */ +class SelectedPaymentMethod implements ResolverInterface +{ + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + + /** @var \Magento\Quote\Model\Quote $cart */ + $cart = $value['model']; + + $payment = $cart->getPayment(); + if (!$payment) { + return []; + } + + return [ + 'code' => $payment->getMethod(), + 'title' => $payment->getMethodInstance()->getTitle(), + 'purchase_order_number' => $payment->getPoNumber(), + ]; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetBillingAddressOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetBillingAddressOnCart.php new file mode 100644 index 000000000000..f7c9a4b0697b --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetBillingAddressOnCart.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; +use Magento\QuoteGraphQl\Model\Cart\SetBillingAddressOnCart as SetBillingAddressOnCartModel; + +/** + * Mutation resolver for setting billing address for shopping cart + */ +class SetBillingAddressOnCart implements ResolverInterface +{ + /** + * @var GetCartForUser + */ + private $getCartForUser; + + /** + * @var SetBillingAddressOnCartModel + */ + private $setBillingAddressOnCart; + + /** + * @param GetCartForUser $getCartForUser + * @param SetBillingAddressOnCartModel $setBillingAddressOnCart + */ + public function __construct( + GetCartForUser $getCartForUser, + SetBillingAddressOnCartModel $setBillingAddressOnCart + ) { + $this->getCartForUser = $getCartForUser; + $this->setBillingAddressOnCart = $setBillingAddressOnCart; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($args['input']['cart_id']) || empty($args['input']['cart_id'])) { + throw new GraphQlInputException(__('Required parameter "cart_id" is missing')); + } + $maskedCartId = $args['input']['cart_id']; + + if (!isset($args['input']['billing_address']) || empty($args['input']['billing_address'])) { + throw new GraphQlInputException(__('Required parameter "billing_address" is missing')); + } + $billingAddress = $args['input']['billing_address']; + + $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId()); + $this->setBillingAddressOnCart->execute($context, $cart, $billingAddress); + + return [ + 'cart' => [ + 'model' => $cart, + ], + ]; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetGuestEmailOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetGuestEmailOnCart.php new file mode 100644 index 000000000000..d621057348b5 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetGuestEmailOnCart.php @@ -0,0 +1,95 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\Exception\CouldNotSaveException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\Validator\EmailAddress as EmailAddressValidator; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; + +/** + * @inheritdoc + */ +class SetGuestEmailOnCart implements ResolverInterface +{ + /** + * @var CartRepositoryInterface + */ + private $cartRepository; + + /** + * @var GetCartForUser + */ + private $getCartForUser; + + /** + * @var EmailAddressValidator + */ + private $emailValidator; + + /** + * @param GetCartForUser $getCartForUser + * @param CartRepositoryInterface $cartRepository + * @param EmailAddressValidator $emailValidator + */ + public function __construct( + GetCartForUser $getCartForUser, + CartRepositoryInterface $cartRepository, + EmailAddressValidator $emailValidator + ) { + $this->getCartForUser = $getCartForUser; + $this->cartRepository = $cartRepository; + $this->emailValidator = $emailValidator; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($args['input']['cart_id']) || empty($args['input']['cart_id'])) { + throw new GraphQlInputException(__('Required parameter "cart_id" is missing')); + } + $maskedCartId = $args['input']['cart_id']; + + if (!isset($args['input']['email']) || empty($args['input']['email'])) { + throw new GraphQlInputException(__('Required parameter "email" is missing')); + } + + if (false === $this->emailValidator->isValid($args['input']['email'])) { + throw new GraphQlInputException(__('Invalid email format')); + } + $email = $args['input']['email']; + + $currentUserId = $context->getUserId(); + + if ($currentUserId !== 0) { + throw new GraphQlInputException(__('The request is not allowed for logged in customers')); + } + + $cart = $this->getCartForUser->execute($maskedCartId, $currentUserId); + $cart->setCustomerEmail($email); + + try { + $this->cartRepository->save($cart); + } catch (CouldNotSaveException $e) { + throw new LocalizedException(__($e->getMessage()), $e); + } + + return [ + 'cart' => [ + 'model' => $cart, + ], + ]; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentMethodOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentMethodOnCart.php new file mode 100644 index 000000000000..7b81964f111c --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentMethodOnCart.php @@ -0,0 +1,98 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Quote\Api\Data\PaymentInterface; +use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; +use Magento\Quote\Api\Data\PaymentInterfaceFactory; +use Magento\Quote\Api\PaymentMethodManagementInterface; + +/** + * Mutation resolver for setting payment method for shopping cart + */ +class SetPaymentMethodOnCart implements ResolverInterface +{ + /** + * @var GetCartForUser + */ + private $getCartForUser; + + /** + * @var PaymentMethodManagementInterface + */ + private $paymentMethodManagement; + + /** + * @var PaymentInterfaceFactory + */ + private $paymentFactory; + + /** + * @param GetCartForUser $getCartForUser + * @param PaymentMethodManagementInterface $paymentMethodManagement + * @param PaymentInterfaceFactory $paymentFactory + */ + public function __construct( + GetCartForUser $getCartForUser, + PaymentMethodManagementInterface $paymentMethodManagement, + PaymentInterfaceFactory $paymentFactory + ) { + $this->getCartForUser = $getCartForUser; + $this->paymentMethodManagement = $paymentMethodManagement; + $this->paymentFactory = $paymentFactory; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($args['input']['cart_id']) || empty($args['input']['cart_id'])) { + throw new GraphQlInputException(__('Required parameter "cart_id" is missing.')); + } + $maskedCartId = $args['input']['cart_id']; + + if (!isset($args['input']['payment_method']['code']) || empty($args['input']['payment_method']['code'])) { + throw new GraphQlInputException(__('Required parameter "code" for "payment_method" is missing.')); + } + $paymentMethodCode = $args['input']['payment_method']['code']; + + $poNumber = $args['input']['payment_method']['purchase_order_number'] ?? null; + $additionalData = $args['input']['payment_method']['additional_data'] ?? []; + + $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 => $additionalData, + ] + ]); + + 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 new file mode 100644 index 000000000000..c3e1d371fe6a --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetShippingAddressesOnCart.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; +use Magento\QuoteGraphQl\Model\Cart\SetShippingAddressesOnCartInterface; + +/** + * Mutation resolver for setting shipping addresses for shopping cart + */ +class SetShippingAddressesOnCart implements ResolverInterface +{ + /** + * @var GetCartForUser + */ + private $getCartForUser; + + /** + * @var SetShippingAddressesOnCartInterface + */ + private $setShippingAddressesOnCart; + + /** + * @param GetCartForUser $getCartForUser + * @param SetShippingAddressesOnCartInterface $setShippingAddressesOnCart + */ + public function __construct( + GetCartForUser $getCartForUser, + SetShippingAddressesOnCartInterface $setShippingAddressesOnCart + ) { + $this->getCartForUser = $getCartForUser; + $this->setShippingAddressesOnCart = $setShippingAddressesOnCart; + } + + /** + * @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']['shipping_addresses']) || empty($args['input']['shipping_addresses'])) { + throw new GraphQlInputException(__('Required parameter "shipping_addresses" is missing')); + } + $shippingAddresses = $args['input']['shipping_addresses']; + + $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId()); + $this->setShippingAddressesOnCart->execute($context, $cart, $shippingAddresses); + + return [ + 'cart' => [ + 'model' => $cart, + ], + ]; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetShippingMethodsOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetShippingMethodsOnCart.php new file mode 100644 index 000000000000..e69ba47e7adf --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetShippingMethodsOnCart.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; +use Magento\QuoteGraphQl\Model\Cart\SetShippingMethodsOnCartInterface; + +/** + * Mutation resolver for setting shipping methods for shopping cart + */ +class SetShippingMethodsOnCart implements ResolverInterface +{ + /** + * @var GetCartForUser + */ + private $getCartForUser; + + /** + * @var SetShippingMethodsOnCartInterface + */ + private $setShippingMethodsOnCart; + + /** + * @param GetCartForUser $getCartForUser + * @param SetShippingMethodsOnCartInterface $setShippingMethodsOnCart + */ + public function __construct( + GetCartForUser $getCartForUser, + SetShippingMethodsOnCartInterface $setShippingMethodsOnCart + ) { + $this->getCartForUser = $getCartForUser; + $this->setShippingMethodsOnCart = $setShippingMethodsOnCart; + } + + /** + * @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']['shipping_methods']) || empty($args['input']['shipping_methods'])) { + throw new GraphQlInputException(__('Required parameter "shipping_methods" is missing')); + } + $shippingMethods = $args['input']['shipping_methods']; + + $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId()); + $this->setShippingMethodsOnCart->execute($context, $cart, $shippingMethods); + + return [ + '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 000000000000..a9e0ba59d15d --- /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 000000000000..cd8f20c3f164 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/SelectedShippingMethod.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\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; +use Magento\Quote\Model\Quote\Address; +use Magento\Quote\Model\Quote\Address\Rate; + +/** + * @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')); + } + /** @var Address $address */ + $address = $value['model']; + $rates = $address->getAllShippingRates(); + + if (count($rates) > 0) { + list($carrierCode, $methodCode) = explode('_', $address->getShippingMethod(), 2); + /** @var Rate $rate */ + $rate = current($rates); + + $data = [ + 'carrier_code' => $carrierCode, + 'method_code' => $methodCode, + 'carrier_title' => $rate->getCarrierTitle(), + 'method_title' => $rate->getMethodTitle(), + 'amount' => [ + 'value' => $address->getShippingAmount(), + 'currency' => $address->getQuote()->getQuoteCurrencyCode(), + ], + 'base_amount' => [ + 'value' => $address->getBaseShippingAmount(), + 'currency' => $address->getQuote()->getBaseCurrencyCode(), + ], + ]; + } else { + $data = [ + 'carrier_code' => null, + 'method_code' => null, + 'carrier_title' => null, + 'method_title' => null, + 'amount' => null, + 'base_amount' => null, + ]; + } + return $data; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddresses.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddresses.php new file mode 100644 index 000000000000..eb3b0966740e --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddresses.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Quote\Model\Quote; +use Magento\QuoteGraphQl\Model\Cart\ExtractQuoteAddressData; + +/** + * @inheritdoc + */ +class ShippingAddresses implements ResolverInterface +{ + /** + * @var ExtractQuoteAddressData + */ + private $extractQuoteAddressData; + + /** + * @param ExtractQuoteAddressData $extractQuoteAddressData + */ + public function __construct(ExtractQuoteAddressData $extractQuoteAddressData) + { + $this->extractQuoteAddressData = $extractQuoteAddressData; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + /** @var Quote $cart */ + $cart = $value['model']; + + $addressesData = []; + $shippingAddresses = $cart->getAllShippingAddresses(); + + if (count($shippingAddresses)) { + foreach ($shippingAddresses as $shippingAddress) { + $addressesData[] = $this->extractQuoteAddressData->execute($shippingAddress); + } + } + return $addressesData; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php new file mode 100644 index 000000000000..b76c9159d859 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php @@ -0,0 +1,146 @@ +<?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\Quote\Model\Quote\Item; +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.')); + } + $quantity = (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 ($quantity <= 0.0) { + $this->cartItemRepository->deleteById((int)$cart->getId(), $itemId); + } else { + $cartItem->setQty($quantity); + $this->validateCartItem($cartItem); + $this->cartItemRepository->save($cartItem); + } + } + } + + /** + * Validate cart item + * + * @param Item $cartItem + * @return void + * @throws GraphQlInputException + */ + private function validateCartItem(Item $cartItem): void + { + if ($cartItem->getHasError()) { + $errors = []; + foreach ($cartItem->getMessage(false) as $message) { + $errors[] = $message; + } + + if (!empty($errors)) { + throw new GraphQlInputException( + __( + 'Could not update the product with SKU %sku: %message', + ['sku' => $cartItem->getSku(), 'message' => __(implode("\n", $errors))] + ) + ); + } + } + } +} diff --git a/app/code/Magento/QuoteGraphQl/README.md b/app/code/Magento/QuoteGraphQl/README.md new file mode 100644 index 000000000000..05d6746b873e --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/README.md @@ -0,0 +1,4 @@ +# QuoteGraphQl + +**QuoteGraphQl** provides type and resolver information for the GraphQl module +to generate quote (cart) information endpoints. Also provides endpoints for modifying a quote. diff --git a/app/code/Magento/QuoteGraphQl/composer.json b/app/code/Magento/QuoteGraphQl/composer.json new file mode 100644 index 000000000000..a3c07f7df2ce --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/composer.json @@ -0,0 +1,32 @@ +{ + "name": "magento/module-quote-graph-ql", + "description": "N/A", + "type": "magento2-module", + "require": { + "php": "~7.1.3||~7.2.0", + "magento/framework": "*", + "magento/module-quote": "*", + "magento/module-checkout": "*", + "magento/module-catalog": "*", + "magento/module-store": "*", + "magento/module-customer": "*", + "magento/module-customer-graph-ql": "*", + "magento/module-sales": "*" + }, + "suggest": { + "magento/module-graph-ql": "*", + "magento/module-graph-ql-cache": "*" + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\QuoteGraphQl\\": "" + } + } +} diff --git a/app/code/Magento/QuoteGraphQl/etc/di.xml b/app/code/Magento/QuoteGraphQl/etc/di.xml new file mode 100644 index 000000000000..0697761a2a2a --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/etc/di.xml @@ -0,0 +1,34 @@ +<?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"> + <preference for="Magento\QuoteGraphQl\Model\CartItem\DataProvider\CustomizableOptionValueInterface" type="Magento\QuoteGraphQl\Model\CartItem\DataProvider\CustomizableOptionValue\Composite" /> + <type name="Magento\QuoteGraphQl\Model\Resolver\CartItemTypeResolver"> + <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> + <type name="Magento\QuoteGraphQl\Model\CartItem\DataProvider\CustomizableOptionValue\Composite"> + <arguments> + <argument name="customizableOptionValues" xsi:type="array"> + <item name="field" xsi:type="string">Magento\QuoteGraphQl\Model\CartItem\DataProvider\CustomizableOptionValue\Text</item> + <item name="date" xsi:type="string">Magento\QuoteGraphQl\Model\CartItem\DataProvider\CustomizableOptionValue\Text</item> + <item name="time" xsi:type="string">Magento\QuoteGraphQl\Model\CartItem\DataProvider\CustomizableOptionValue\Text</item> + <item name="date_time" xsi:type="string">Magento\QuoteGraphQl\Model\CartItem\DataProvider\CustomizableOptionValue\Text</item> + <item name="area" xsi:type="string">Magento\QuoteGraphQl\Model\CartItem\DataProvider\CustomizableOptionValue\Text</item> + <item name="drop_down" xsi:type="string">Magento\QuoteGraphQl\Model\CartItem\DataProvider\CustomizableOptionValue\Dropdown</item> + <item name="radio" xsi:type="string">Magento\QuoteGraphQl\Model\CartItem\DataProvider\CustomizableOptionValue\Dropdown</item> + <item name="checkbox" xsi:type="string">Magento\QuoteGraphQl\Model\CartItem\DataProvider\CustomizableOptionValue\Dropdown</item> + <item name="multiple" xsi:type="string">Magento\QuoteGraphQl\Model\CartItem\DataProvider\CustomizableOptionValue\Multiple</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/QuoteGraphQl/etc/graphql/di.xml b/app/code/Magento/QuoteGraphQl/etc/graphql/di.xml new file mode 100644 index 000000000000..c7389cf66784 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/etc/graphql/di.xml @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\QuoteGraphQl\Model\Cart\SetShippingAddressesOnCartInterface" + 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/module.xml b/app/code/Magento/QuoteGraphQl/etc/module.xml new file mode 100644 index 000000000000..9f29b124abbb --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/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_QuoteGraphQl"/> +</config> diff --git a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls new file mode 100644 index 000000000000..e2d29e93db57 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls @@ -0,0 +1,339 @@ +# Copyright © Magento, Inc. All rights reserved. +# 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") @cache(cacheable: false) +} + +type Mutation { + createEmptyCart(input: createEmptyCartInput): String @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CreateEmptyCart") @doc(description:"Creates an empty shopping cart for a guest or logged in user") + addSimpleProductsToCart(input: AddSimpleProductsToCartInput): AddSimpleProductsToCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\AddSimpleProductsToCart") + addVirtualProductsToCart(input: AddVirtualProductsToCartInput): AddVirtualProductsToCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\AddSimpleProductsToCart") + applyCouponToCart(input: ApplyCouponToCartInput): ApplyCouponToCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\ApplyCouponToCart") + removeCouponFromCart(input: RemoveCouponFromCartInput): RemoveCouponFromCartOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\RemoveCouponFromCart") + 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") + setPaymentMethodOnCart(input: SetPaymentMethodOnCartInput): SetPaymentMethodOnCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\SetPaymentMethodOnCart") + setGuestEmailOnCart(input: SetGuestEmailOnCartInput): SetGuestEmailOnCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\SetGuestEmailOnCart") + placeOrder(input: PlaceOrderInput): PlaceOrderOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\PlaceOrder") +} + +input createEmptyCartInput { + cart_id: String +} + +input AddSimpleProductsToCartInput { + cart_id: String! + cartItems: [SimpleProductCartItemInput!]! +} + +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! + quantity: Float! +} + +input CustomizableOptionInput { + id: Int! + value: String! +} + +input ApplyCouponToCartInput { + cart_id: String! + coupon_code: String! +} + +input UpdateCartItemsInput { + cart_id: String! + cart_items: [CartItemUpdateInput!]! +} + +input CartItemUpdateInput { + cart_item_id: Int! + quantity: Float! +} + +input RemoveItemFromCartInput { + cart_id: String! + cart_item_id: Int! +} + +input SetShippingAddressesOnCartInput { + cart_id: String! + shipping_addresses: [ShippingAddressInput!]! +} + +input ShippingAddressInput { + customer_address_id: Int # If provided then will be used address from address book + address: CartAddressInput +} + +input SetBillingAddressOnCartInput { + cart_id: String! + billing_address: BillingAddressInput! +} + +input BillingAddressInput { + customer_address_id: Int + address: CartAddressInput + use_for_shipping: Boolean +} + +input CartAddressInput { + firstname: String! + lastname: String! + company: String + street: [String!]! + city: String! + region: String + postcode: String + country_code: String! + telephone: String! + save_in_address_book: Boolean! +} + +input SetShippingMethodsOnCartInput { + cart_id: String! + shipping_methods: [ShippingMethodInput!]! +} + +input ShippingMethodInput { + carrier_code: String! + method_code: String! +} + +input PlaceOrderInput { + cart_id: String! +} + +input SetPaymentMethodOnCartInput { + cart_id: String! + payment_method: PaymentMethodInput! +} + +input PaymentMethodInput { + code: String! @doc(description:"Payment method code") + additional_data: PaymentMethodAdditionalDataInput @doc(description:"Additional payment data") + purchase_order_number: String @doc(description:"Purchase order number") +} + +input PaymentMethodAdditionalDataInput { +} + +input SetGuestEmailOnCartInput { + cart_id: String! + email: String! +} + +type CartPrices { + grand_total: Money + subtotal_including_tax: Money + subtotal_excluding_tax: Money + subtotal_with_discount_excluding_tax: Money + applied_taxes: [CartTaxItem] +} + +type CartTaxItem { + amount: Money! + label: String! +} + +type SetPaymentMethodOnCartOutput { + cart: Cart! +} + +type SetBillingAddressOnCartOutput { + cart: Cart! +} + +type SetShippingAddressesOnCartOutput { + cart: Cart! +} + +type SetShippingMethodsOnCartOutput { + cart: Cart! +} + +type ApplyCouponToCartOutput { + cart: Cart! +} + +type PlaceOrderOutput { + order: Order! +} + +type Cart { + items: [CartItemInterface] @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartItems") + applied_coupon: AppliedCoupon @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\AppliedCoupon") + email: String @resolver (class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartEmail") + shipping_addresses: [ShippingCartAddress]! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\ShippingAddresses") + billing_address: BillingCartAddress! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\BillingAddress") + available_payment_methods: [AvailablePaymentMethod] @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\AvailablePaymentMethods") @doc(description: "Available payment methods") + selected_payment_method: SelectedPaymentMethod @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\SelectedPaymentMethod") + prices: CartPrices @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartPrices") +} + +interface CartAddressInterface @typeResolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartAddressTypeResolver") { + firstname: String + lastname: String + company: String + street: [String] + city: String + region: CartAddressRegion + postcode: String + country: CartAddressCountry + telephone: String + customer_notes: String +} + +type ShippingCartAddress implements CartAddressInterface { + 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 + cart_items: [CartItemQuantity] +} + +type BillingCartAddress implements CartAddressInterface { +} + +type CartItemQuantity { + cart_item_id: Int! + quantity: Float! +} + +type CartAddressRegion { + code: String + label: String +} + +type CartAddressCountry { + code: String + label: String +} + +type SelectedShippingMethod { + carrier_code: String + method_code: String + carrier_title: String + method_title: String + amount: Money + base_amount: Money +} + +type AvailableShippingMethod { + carrier_code: String! + carrier_title: String! + method_code: String @doc(description: "Could be null if method is not available") + method_title: String @doc(description: "Could be null if method is not available") + error_message: String + amount: Float! + base_amount: Float @doc(description: "Could be null if method is not available") + price_excl_tax: Float! + price_incl_tax: Float! + available: Boolean! +} + +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") + title: String! @doc(description: "The payment method title.") + additional_data: SelectedPaymentMethodAdditionalData @doc(description: "Additional payment data") + purchase_order_number: String @doc(description: "The purchase order number.") +} + +type SelectedPaymentMethodAdditionalData { +} + +type AppliedCoupon { + code: String! +} + +input RemoveCouponFromCartInput { + cart_id: String! +} + +type RemoveCouponFromCartOutput { + cart: Cart +} + +type AddSimpleProductsToCartOutput { + cart: Cart! +} + +type AddVirtualProductsToCartOutput { + cart: Cart! +} + +type UpdateCartItemsOutput { + cart: Cart! +} + +type RemoveItemFromCartOutput { + cart: Cart! +} + +type SetGuestEmailOnCartOutput { + cart: Cart! +} + +type SimpleCartItem implements CartItemInterface @doc(description: "Simple Cart Item") { + customizable_options: [SelectedCustomizableOption] @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\CustomizableOptions") +} + +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") { + id: String! + quantity: Float! + product: ProductInterface! +} + +type SelectedCustomizableOption { + id: Int! + label: String! + type: String! + is_required: Int! + values: [SelectedCustomizableOptionValue!]! + sort_order: Int! +} + +type SelectedCustomizableOptionValue { + id: Int! + label: String! + value: String! + price: CartItemSelectedOptionValuePrice! + sort_order: Int! +} + +type CartItemSelectedOptionValuePrice { + value: Float! + units: String! + type: PriceTypeEnum! +} + +type Order { + order_id: String +} diff --git a/app/code/Magento/QuoteGraphQl/registration.php b/app/code/Magento/QuoteGraphQl/registration.php new file mode 100644 index 000000000000..8c945d902ba3 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/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_QuoteGraphQl', __DIR__); diff --git a/app/code/Magento/ReleaseNotification/README.md b/app/code/Magento/ReleaseNotification/README.md index df3206a176f0..1f6cac764b56 100644 --- a/app/code/Magento/ReleaseNotification/README.md +++ b/app/code/Magento/ReleaseNotification/README.md @@ -50,4 +50,4 @@ A clickable link to internal or external content in any text field will be creat ### Link Format Example: -The text: `http://devdocs.magento.com/ [Magento DevDocs].` will appear as [Magento DevDocs](http://devdocs.magento.com/). +The text: `https://devdocs.magento.com/ [Magento DevDocs].` will appear as [Magento DevDocs](https://devdocs.magento.com/). diff --git a/app/code/Magento/ReleaseNotification/Test/Unit/Model/Condition/CanViewNotificationTest.php b/app/code/Magento/ReleaseNotification/Test/Unit/Model/Condition/CanViewNotificationTest.php index 55f448730a50..b86f8dff2b3b 100644 --- a/app/code/Magento/ReleaseNotification/Test/Unit/Model/Condition/CanViewNotificationTest.php +++ b/app/code/Magento/ReleaseNotification/Test/Unit/Model/Condition/CanViewNotificationTest.php @@ -12,6 +12,7 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Backend\Model\Auth\Session; use Magento\Framework\App\CacheInterface; +use Magento\User\Model\User; class CanViewNotificationTest extends \PHPUnit\Framework\TestCase { @@ -33,6 +34,11 @@ class CanViewNotificationTest extends \PHPUnit\Framework\TestCase /** @var $cacheStorageMock \PHPUnit_Framework_MockObject_MockObject|CacheInterface */ private $cacheStorageMock; + /** + * @var User|\PHPUnit_Framework_MockObject_MockObject + */ + private $userMock; + public function setUp() { $this->cacheStorageMock = $this->getMockBuilder(CacheInterface::class) @@ -41,7 +47,6 @@ public function setUp() ->getMock(); $this->sessionMock = $this->getMockBuilder(Session::class) ->disableOriginalConstructor() - ->setMethods(['getUser', 'getId']) ->getMock(); $this->viewerLoggerMock = $this->getMockBuilder(Logger::class) ->disableOriginalConstructor() @@ -49,6 +54,7 @@ public function setUp() $this->productMetadataMock = $this->getMockBuilder(ProductMetadataInterface::class) ->disableOriginalConstructor() ->getMock(); + $this->userMock = $this->createMock(User::class); $objectManager = new ObjectManager($this); $this->canViewNotification = $objectManager->getObject( CanViewNotification::class, @@ -65,8 +71,8 @@ public function testIsVisibleLoadDataFromCache() { $this->sessionMock->expects($this->once()) ->method('getUser') - ->willReturn($this->sessionMock); - $this->sessionMock->expects($this->once()) + ->willReturn($this->userMock); + $this->userMock->expects($this->once()) ->method('getId') ->willReturn(1); $this->cacheStorageMock->expects($this->once()) @@ -90,8 +96,8 @@ public function testIsVisible($expected, $version, $lastViewVersion) ->willReturn(false); $this->sessionMock->expects($this->once()) ->method('getUser') - ->willReturn($this->sessionMock); - $this->sessionMock->expects($this->once()) + ->willReturn($this->userMock); + $this->userMock->expects($this->once()) ->method('getId') ->willReturn(1); $this->productMetadataMock->expects($this->once()) diff --git a/app/code/Magento/ReleaseNotification/Ui/DataProvider/NotificationDataProvider.php b/app/code/Magento/ReleaseNotification/Ui/DataProvider/NotificationDataProvider.php index cdf7e0c6ac7c..a1e558a5a2f8 100644 --- a/app/code/Magento/ReleaseNotification/Ui/DataProvider/NotificationDataProvider.php +++ b/app/code/Magento/ReleaseNotification/Ui/DataProvider/NotificationDataProvider.php @@ -82,7 +82,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getData() { @@ -95,7 +95,7 @@ public function getData() } /** - * {@inheritdoc} + * @inheritdoc */ public function getMeta() { @@ -107,7 +107,7 @@ public function getMeta() } /** - * {@inheritdoc} + * @inheritdoc */ public function getName() { @@ -115,15 +115,15 @@ public function getName() } /** - * {@inheritdoc} + * @inheritdoc */ public function getConfigData() { - return isset($this->data['config']) ? $this->data['config'] : []; + return $this->data['config'] ?? []; } /** - * {@inheritdoc} + * @inheritdoc */ public function setConfigData($config) { @@ -133,7 +133,7 @@ public function setConfigData($config) } /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function getFieldMetaInfo($fieldSetName, $fieldName) @@ -142,7 +142,7 @@ public function getFieldMetaInfo($fieldSetName, $fieldName) } /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function getFieldSetMetaInfo($fieldSetName) @@ -151,7 +151,7 @@ public function getFieldSetMetaInfo($fieldSetName) } /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function getFieldsMetaInfo($fieldSetName) @@ -160,7 +160,7 @@ public function getFieldsMetaInfo($fieldSetName) } /** - * {@inheritdoc} + * @inheritdoc */ public function getPrimaryFieldName() { @@ -168,7 +168,7 @@ public function getPrimaryFieldName() } /** - * {@inheritdoc} + * @inheritdoc */ public function getRequestFieldName() { @@ -176,31 +176,28 @@ public function getRequestFieldName() } /** - * {@inheritdoc} - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @inheritdoc */ public function addFilter(\Magento\Framework\Api\Filter $filter) { } /** - * {@inheritdoc} - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @inheritdoc */ public function addOrder($field, $direction) { } /** - * {@inheritdoc} - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @inheritdoc */ public function setLimit($offset, $size) { } /** - * {@inheritdoc} + * @inheritdoc */ public function getSearchCriteria() { @@ -208,7 +205,7 @@ public function getSearchCriteria() } /** - * {@inheritdoc} + * @inheritdoc */ public function getSearchResult() { diff --git a/app/code/Magento/ReleaseNotification/etc/db_schema.xml b/app/code/Magento/ReleaseNotification/etc/db_schema.xml index 367957fc1773..6f3aa481f73d 100644 --- a/app/code/Magento/ReleaseNotification/etc/db_schema.xml +++ b/app/code/Magento/ReleaseNotification/etc/db_schema.xml @@ -15,13 +15,13 @@ comment="Viewer admin user ID"/> <column xsi:type="varchar" name="last_view_version" nullable="false" length="16" comment="Viewer last view on product version"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> </constraint> - <constraint xsi:type="foreign" name="RELEASE_NOTIFICATION_VIEWER_LOG_VIEWER_ID_ADMIN_USER_USER_ID" + <constraint xsi:type="foreign" referenceId="RELEASE_NOTIFICATION_VIEWER_LOG_VIEWER_ID_ADMIN_USER_USER_ID" table="release_notification_viewer_log" column="viewer_id" referenceTable="admin_user" referenceColumn="user_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="RELEASE_NOTIFICATION_VIEWER_LOG_VIEWER_ID"> + <constraint xsi:type="unique" referenceId="RELEASE_NOTIFICATION_VIEWER_LOG_VIEWER_ID"> <column name="viewer_id"/> </constraint> </table> diff --git a/app/code/Magento/ReleaseNotification/etc/di.xml b/app/code/Magento/ReleaseNotification/etc/di.xml index 1404a6adb0a1..a4c434ff7f62 100644 --- a/app/code/Magento/ReleaseNotification/etc/di.xml +++ b/app/code/Magento/ReleaseNotification/etc/di.xml @@ -6,7 +6,6 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> - <preference for="Magento\Framework\HTTP\ClientInterface" type="Magento\Framework\HTTP\Client\Curl" /> <preference for="Magento\ReleaseNotification\Model\ContentProviderInterface" type="Magento\ReleaseNotification\Model\ContentProvider\Http\HttpContentProvider" /> <type name="Magento\Config\Model\Config\TypePool"> <arguments> diff --git a/app/code/Magento/ReleaseNotification/i18n/en_US.csv b/app/code/Magento/ReleaseNotification/i18n/en_US.csv index 426cdc0e863d..178482dc7a98 100644 --- a/app/code/Magento/ReleaseNotification/i18n/en_US.csv +++ b/app/code/Magento/ReleaseNotification/i18n/en_US.csv @@ -5,11 +5,11 @@ "<![CDATA[ <p>We’ll try to show it again the next time you sign in to Magento Admin.</p> <p>To learn more about new features, see our latest Release Notes in - <a href=""http://devdocs.magento.com/magento-release-information.html"" + <a href=""https://devdocs.magento.com/magento-release-information.html"" target=""_blank"">DevDocs' Release Information</a>. </p>]]>","<![CDATA[ <p>We’ll try to show it again the next time you sign in to Magento Admin.</p> <p>To learn more about new features, see our latest Release Notes in - <a href=""http://devdocs.magento.com/magento-release-information.html"" + <a href=""https://devdocs.magento.com/magento-release-information.html"" target=""_blank"">DevDocs' Release Information</a>. </p>]]>" diff --git a/app/code/Magento/ReleaseNotification/view/adminhtml/ui_component/release_notification.xml b/app/code/Magento/ReleaseNotification/view/adminhtml/ui_component/release_notification.xml index 3134b2a8af21..9c6d152bed27 100644 --- a/app/code/Magento/ReleaseNotification/view/adminhtml/ui_component/release_notification.xml +++ b/app/code/Magento/ReleaseNotification/view/adminhtml/ui_component/release_notification.xml @@ -67,7 +67,7 @@ <item name="text" xsi:type="string" translate="true"><![CDATA[ <p>We’ll try to show it again the next time you sign in to Magento Admin.</p> <p>To learn more about new features, see our latest Release Notes in - <a href="http://devdocs.magento.com/magento-release-information.html" + <a href="https://devdocs.magento.com/magento-release-information.html" target="_blank">DevDocs' Release Information</a>. </p>]]></item> </item> @@ -127,7 +127,7 @@ <item name="text" xsi:type="string" translate="true"><![CDATA[ <p>We’ll try to show it again the next time you sign in to Magento Admin.</p> <p>To learn more about new features, see our latest Release Notes in - <a href="http://devdocs.magento.com/magento-release-information.html" + <a href="https://devdocs.magento.com/magento-release-information.html" target="_blank">DevDocs' Release Information</a>. </p>]]></item> </item> @@ -208,7 +208,7 @@ <item name="text" xsi:type="string" translate="true"><![CDATA[ <p>We’ll try to show it again the next time you sign in to Magento Admin.</p> <p>To learn more about new features, see our latest Release Notes in - <a href="http://devdocs.magento.com/magento-release-information.html" + <a href="https://devdocs.magento.com/magento-release-information.html" target="_blank">DevDocs' Release Information</a>. </p>]]></item> </item> @@ -289,7 +289,7 @@ <item name="text" xsi:type="string" translate="true"><![CDATA[ <p>We’ll try to show it again the next time you sign in to Magento Admin.</p> <p>To learn more about new features, see our latest Release Notes in - <a href="http://devdocs.magento.com/magento-release-information.html" + <a href="https://devdocs.magento.com/magento-release-information.html" target="_blank">DevDocs' Release Information</a>. </p>]]></item> </item> diff --git a/app/code/Magento/Reports/Block/Adminhtml/Grid.php b/app/code/Magento/Reports/Block/Adminhtml/Grid.php index 2bb4dcd1efbf..7bbbac644bfe 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Grid.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Grid.php @@ -127,8 +127,8 @@ protected function _prepareCollection() * Validate from and to date */ try { - $from = $this->_localeDate->scopeDate(null, $this->getFilter('report_from'), false); - $to = $this->_localeDate->scopeDate(null, $this->getFilter('report_to'), false); + $from = $this->_localeDate->date($this->getFilter('report_from'), null, false, false); + $to = $this->_localeDate->date($this->getFilter('report_to'), null, false, false); $collection->setInterval($from, $to); } catch (\Exception $e) { @@ -215,8 +215,8 @@ public function setStoreSwitcherVisibility($visible = true) /** * Return visibility of store switcher - * @codeCoverageIgnore * + * @codeCoverageIgnore * @return bool * @SuppressWarnings(PHPMD.BooleanGetMethodName) */ @@ -227,8 +227,8 @@ public function getStoreSwitcherVisibility() /** * Return store switcher html - * @codeCoverageIgnore * + * @codeCoverageIgnore * @return string */ public function getStoreSwitcherHtml() @@ -250,8 +250,8 @@ public function setDateFilterVisibility($visible = true) /** * Return visibility of date filter - * @codeCoverageIgnore * + * @codeCoverageIgnore * @return bool * @SuppressWarnings(PHPMD.BooleanGetMethodName) */ @@ -262,8 +262,8 @@ public function getDateFilterVisibility() /** * Return date filter html - * @codeCoverageIgnore * + * @codeCoverageIgnore * @return string */ public function getDateFilterHtml() @@ -293,8 +293,8 @@ public function getDateFormat() /** * Return refresh button html - * @codeCoverageIgnore * + * @codeCoverageIgnore * @return string */ public function getRefreshButtonHtml() @@ -346,8 +346,8 @@ public function setSubReportSize($size) /** * Return sub-report rows count - * @codeCoverageIgnore * + * @codeCoverageIgnore * @return int */ public function getSubReportSize() diff --git a/app/code/Magento/Reports/Block/Adminhtml/Grid/AbstractGrid.php b/app/code/Magento/Reports/Block/Adminhtml/Grid/AbstractGrid.php index be7dfa70efbb..2ff87237222f 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Grid/AbstractGrid.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Grid/AbstractGrid.php @@ -3,8 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Reports\Block\Adminhtml\Grid; +/** + * Backend reports grid + */ class AbstractGrid extends \Magento\Backend\Block\Widget\Grid\Extended { /** @@ -90,9 +94,8 @@ protected function _construct() /** * Get resource collection name * - * @codeCoverageIgnore - * * @return string + * @codeCoverageIgnore */ public function getResourceCollectionName() { @@ -100,6 +103,8 @@ public function getResourceCollectionName() } /** + * Return reports collection + * * @return \Magento\Framework\Data\Collection */ public function getCollection() @@ -111,6 +116,8 @@ public function getCollection() } /** + * Retrieve array of columns that should be aggregated + * * @return array */ protected function _getAggregatedColumns() @@ -170,12 +177,7 @@ public function addColumn($columnId, $column) */ protected function _getStoreIds() { - $filterData = $this->getFilterData(); - if ($filterData) { - $storeIds = explode(',', $filterData->getData('store_ids')); - } else { - $storeIds = []; - } + $storeIds = $this->getFilteredStores(); // By default storeIds array contains only allowed stores $allowedStoreIds = array_keys($this->_storeManager->getStores()); // And then array_intersect with post data for prevent unauthorized stores reports @@ -191,6 +193,8 @@ protected function _getStoreIds() } /** + * Apply sorting and filtering to collection + * * @return $this|\Magento\Backend\Block\Widget\Grid * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) @@ -280,6 +284,8 @@ protected function _prepareCollection() } /** + * Return count totals + * * @return array */ public function getCountTotals() @@ -319,6 +325,8 @@ public function getCountTotals() } /** + * Retrieve subtotal items + * * @return array */ public function getSubTotals() @@ -360,6 +368,8 @@ public function setStoreIds($storeIds) } /** + * Return current currency code + * * @return string|\Magento\Directory\Model\Currency $currencyCode */ public function getCurrentCurrencyCode() @@ -399,6 +409,7 @@ protected function _addOrderStatusFilter($collection, $filterData) /** * Adds custom filter to resource collection + * * Can be overridden in child classes if custom filter needed * * @param \Magento\Reports\Model\ResourceModel\Report\Collection\AbstractCollection $collection @@ -411,4 +422,35 @@ protected function _addCustomFilter($collection, $filterData) { return $this; } + + /** + * Return stores by website, group and store id + * + * @return array + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function getFilteredStores(): array + { + $storeIds = []; + + $filterData = $this->getFilterData(); + if ($filterData) { + if ($filterData->getWebsite()) { + $storeIds = array_keys( + $this->_storeManager->getWebsite($filterData->getWebsite())->getStores() + ); + } + + if ($filterData->getGroup()) { + $storeIds = array_keys( + $this->_storeManager->getGroup($filterData->getGroup())->getStores() + ); + } + + if ($filterData->getData('store_ids')) { + $storeIds = explode(',', $filterData->getData('store_ids')); + } + } + return is_array($storeIds) ? $storeIds : []; + } } diff --git a/app/code/Magento/Reports/Block/Adminhtml/Grid/Column/Renderer/Currency.php b/app/code/Magento/Reports/Block/Adminhtml/Grid/Column/Renderer/Currency.php index 260d7bb50679..f22b3e7bb963 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Grid/Column/Renderer/Currency.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Grid/Column/Renderer/Currency.php @@ -30,7 +30,7 @@ public function render(\Magento\Framework\DataObject $row) return $data; } - $data = floatval($data) * $this->_getRate($row); + $data = (float)$data * $this->_getRate($row); $data = sprintf("%f", $data); $data = $this->_localeCurrency->getCurrency($currencyCode)->toCurrency($data); return $data; diff --git a/app/code/Magento/Reports/Block/Adminhtml/Sales/Sales/Grid.php b/app/code/Magento/Reports/Block/Adminhtml/Sales/Sales/Grid.php index 21836f1a8c27..25a4aa1b88ca 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Sales/Sales/Grid.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Sales/Sales/Grid.php @@ -6,23 +6,61 @@ namespace Magento\Reports\Block\Adminhtml\Sales\Sales; +use Magento\Framework\DataObject; +use Magento\Reports\Block\Adminhtml\Grid\Column\Renderer\Currency; +use Magento\Framework\App\ObjectManager; +use Magento\Sales\Model\Order\ConfigFactory; +use Magento\Sales\Model\Order; + /** * Adminhtml sales report grid block * - * @author Magento Core Team <core@magentocommerce.com> * @SuppressWarnings(PHPMD.DepthOfInheritance) */ class Grid extends \Magento\Reports\Block\Adminhtml\Grid\AbstractGrid { /** - * GROUP BY criteria - * * @var string */ protected $_columnGroupBy = 'period'; /** - * {@inheritdoc} + * @var ConfigFactory + */ + private $configFactory; + + /** + * @param \Magento\Backend\Block\Template\Context $context + * @param \Magento\Backend\Helper\Data $backendHelper + * @param \Magento\Reports\Model\ResourceModel\Report\Collection\Factory $resourceFactory + * @param \Magento\Reports\Model\Grouped\CollectionFactory $collectionFactory + * @param \Magento\Reports\Helper\Data $reportsData + * @param array $data + * @param ConfigFactory|null $configFactory + */ + public function __construct( + \Magento\Backend\Block\Template\Context $context, + \Magento\Backend\Helper\Data $backendHelper, + \Magento\Reports\Model\ResourceModel\Report\Collection\Factory $resourceFactory, + \Magento\Reports\Model\Grouped\CollectionFactory $collectionFactory, + \Magento\Reports\Helper\Data $reportsData, + array $data = [], + ConfigFactory $configFactory = null + ) { + parent::__construct( + $context, + $backendHelper, + $resourceFactory, + $collectionFactory, + $reportsData, + $data + ); + $this->configFactory = $configFactory ?: ObjectManager::getInstance()->get(ConfigFactory::class); + } + + /** + * Reports grid constructor + * * @codeCoverageIgnore */ protected function _construct() @@ -32,17 +70,19 @@ protected function _construct() } /** - * {@inheritdoc} + * Return collection name based on report_type + * + * @return string */ public function getResourceCollectionName() { - return $this->getFilterData()->getData('report_type') == 'updated_at_order' + return $this->getFilterData()->getData('report_type') === 'updated_at_order' ? \Magento\Sales\Model\ResourceModel\Report\Order\Updatedat\Collection::class : \Magento\Sales\Model\ResourceModel\Report\Order\Collection::class; } /** - * {@inheritdoc} + * Initialize reports grid columns * * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -103,9 +143,7 @@ protected function _prepareColumns() ] ); - if ($this->getFilterData()->getStoreIds()) { - $this->setStoreIds(explode(',', $this->getFilterData()->getStoreIds())); - } + $this->setStoreIds($this->_getStoreIds()); $currencyCode = $this->getCurrentCurrencyCode(); $rate = $this->getRate($currencyCode); @@ -118,6 +156,7 @@ protected function _prepareColumns() 'index' => 'total_income_amount', 'total' => 'sum', 'sortable' => false, + 'renderer' => Currency::class, 'rate' => $rate, 'header_css_class' => 'col-sales-total', 'column_css_class' => 'col-sales-total' @@ -133,6 +172,7 @@ protected function _prepareColumns() 'index' => 'total_revenue_amount', 'total' => 'sum', 'sortable' => false, + 'renderer' => Currency::class, 'visibility_filter' => ['show_actual_columns'], 'rate' => $rate, 'header_css_class' => 'col-revenue', @@ -149,6 +189,7 @@ protected function _prepareColumns() 'index' => 'total_profit_amount', 'total' => 'sum', 'sortable' => false, + 'renderer' => Currency::class, 'visibility_filter' => ['show_actual_columns'], 'rate' => $rate, 'header_css_class' => 'col-profit', @@ -165,6 +206,7 @@ protected function _prepareColumns() 'index' => 'total_invoiced_amount', 'total' => 'sum', 'sortable' => false, + 'renderer' => Currency::class, 'rate' => $rate, 'header_css_class' => 'col-invoiced', 'column_css_class' => 'col-invoiced' @@ -180,6 +222,7 @@ protected function _prepareColumns() 'index' => 'total_paid_amount', 'total' => 'sum', 'sortable' => false, + 'renderer' => Currency::class, 'visibility_filter' => ['show_actual_columns'], 'rate' => $rate, 'header_css_class' => 'col-paid', @@ -196,6 +239,7 @@ protected function _prepareColumns() 'index' => 'total_refunded_amount', 'total' => 'sum', 'sortable' => false, + 'renderer' => Currency::class, 'rate' => $rate, 'header_css_class' => 'col-refunded', 'column_css_class' => 'col-refunded' @@ -211,6 +255,7 @@ protected function _prepareColumns() 'index' => 'total_tax_amount', 'total' => 'sum', 'sortable' => false, + 'renderer' => Currency::class, 'rate' => $rate, 'header_css_class' => 'col-sales-tax', 'column_css_class' => 'col-sales-tax' @@ -226,6 +271,7 @@ protected function _prepareColumns() 'index' => 'total_tax_amount_actual', 'total' => 'sum', 'sortable' => false, + 'renderer' => Currency::class, 'visibility_filter' => ['show_actual_columns'], 'rate' => $rate, 'header_css_class' => 'col-tax', @@ -242,6 +288,7 @@ protected function _prepareColumns() 'index' => 'total_shipping_amount', 'total' => 'sum', 'sortable' => false, + 'renderer' => Currency::class, 'rate' => $rate, 'header_css_class' => 'col-sales-shipping', 'column_css_class' => 'col-sales-shipping' @@ -257,6 +304,7 @@ protected function _prepareColumns() 'index' => 'total_shipping_amount_actual', 'total' => 'sum', 'sortable' => false, + 'renderer' => Currency::class, 'visibility_filter' => ['show_actual_columns'], 'rate' => $rate, 'header_css_class' => 'col-shipping', @@ -273,6 +321,7 @@ protected function _prepareColumns() 'index' => 'total_discount_amount', 'total' => 'sum', 'sortable' => false, + 'renderer' => Currency::class, 'rate' => $rate, 'header_css_class' => 'col-sales-discount', 'column_css_class' => 'col-sales-discount' @@ -288,6 +337,7 @@ protected function _prepareColumns() 'index' => 'total_discount_amount_actual', 'total' => 'sum', 'sortable' => false, + 'renderer' => Currency::class, 'visibility_filter' => ['show_actual_columns'], 'rate' => $rate, 'header_css_class' => 'col-discount', @@ -304,6 +354,7 @@ protected function _prepareColumns() 'index' => 'total_canceled_amount', 'total' => 'sum', 'sortable' => false, + 'renderer' => Currency::class, 'rate' => $rate, 'header_css_class' => 'col-canceled', 'column_css_class' => 'col-canceled' @@ -315,4 +366,30 @@ protected function _prepareColumns() return parent::_prepareColumns(); } + + /** + * @inheritdoc + * + * Filter canceled statuses for orders. + * + * @return Grid + */ + protected function _prepareCollection() + { + /** @var DataObject $filterData */ + $filterData = $this->getData('filter_data'); + if (!$filterData->hasData('order_statuses')) { + $orderConfig = $this->configFactory->create(); + $statusValues = []; + $canceledStatuses = $orderConfig->getStateStatuses(Order::STATE_CANCELED); + $statusCodes = array_keys($orderConfig->getStatuses()); + foreach ($statusCodes as $code) { + if (!isset($canceledStatuses[$code])) { + $statusValues[] = $code; + } + } + $filterData->setData('order_statuses', $statusValues); + } + return parent::_prepareCollection(); + } } diff --git a/app/code/Magento/Reports/Block/Adminhtml/Sales/Tax/Grid.php b/app/code/Magento/Reports/Block/Adminhtml/Sales/Tax/Grid.php index 79deb27423be..f65a2b964fb3 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Sales/Tax/Grid.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Sales/Tax/Grid.php @@ -53,7 +53,8 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc + * * @codeCoverageIgnore */ protected function _construct() @@ -64,7 +65,7 @@ protected function _construct() } /** - * {@inheritdoc} + * @inheritdoc */ public function getResourceCollectionName() { @@ -74,7 +75,7 @@ public function getResourceCollectionName() } /** - * {@inheritdoc} + * @inheritdoc */ protected function _prepareColumns() { @@ -123,7 +124,6 @@ protected function _prepareColumns() [ 'header' => __('Orders'), 'index' => 'orders_count', - 'total' => 'sum', 'type' => 'number', 'sortable' => false, 'header_css_class' => 'col-qty', diff --git a/app/code/Magento/Reports/Block/Adminhtml/Shopcart/Abandoned/Grid.php b/app/code/Magento/Reports/Block/Adminhtml/Shopcart/Abandoned/Grid.php index f81176b7a112..5a92b6ab4e79 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Shopcart/Abandoned/Grid.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Shopcart/Abandoned/Grid.php @@ -37,6 +37,8 @@ public function __construct( } /** + * Grid constructor + * * @return void */ protected function _construct() @@ -46,6 +48,8 @@ protected function _construct() } /** + * Prepare collection + * * @return \Magento\Backend\Block\Widget\Grid */ protected function _prepareCollection() @@ -67,11 +71,16 @@ protected function _prepareCollection() $this->setCollection($collection); parent::_prepareCollection(); + if ($this->_isExport) { + $collection->setPageSize(null); + } $this->getCollection()->resolveCustomerNames(); return $this; } /** + * Add column filter to collection + * * @param array $column * * @return $this @@ -90,6 +99,8 @@ protected function _addColumnFilterToCollection($column) } /** + * Prepare columns + * * @return \Magento\Backend\Block\Widget\Grid\Extended * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -223,6 +234,8 @@ protected function _prepareColumns() } /** + * Get rows url + * * @param \Magento\Framework\DataObject $row * * @return string diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/AbstractReport.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/AbstractReport.php index 3dbced45e0a6..2fbff13a5b64 100644 --- a/app/code/Magento/Reports/Controller/Adminhtml/Report/AbstractReport.php +++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/AbstractReport.php @@ -9,13 +9,18 @@ * * @author Magento Core Team <core@magentocommerce.com> */ + namespace Magento\Reports\Controller\Adminhtml\Report; +use Magento\Backend\Helper\Data as BackendHelper; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; /** + * Reports api controller + * * @api * @since 100.0.2 + * @SuppressWarnings(PHPMD.AllPurposeAction) */ abstract class AbstractReport extends \Magento\Backend\App\Action { @@ -41,22 +46,30 @@ abstract class AbstractReport extends \Magento\Backend\App\Action */ protected $timezone; + /** + * @var BackendHelper + */ + private $backendHelper; + /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Framework\App\Response\Http\FileFactory $fileFactory * @param \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter * @param TimezoneInterface $timezone + * @param BackendHelper|null $backendHelperData */ public function __construct( \Magento\Backend\App\Action\Context $context, \Magento\Framework\App\Response\Http\FileFactory $fileFactory, \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter, - TimezoneInterface $timezone + TimezoneInterface $timezone, + BackendHelper $backendHelperData = null ) { parent::__construct($context); $this->_fileFactory = $fileFactory; $this->_dateFilter = $dateFilter; $this->timezone = $timezone; + $this->backendHelper = $backendHelperData ?: $this->_objectManager->get(BackendHelper::class); } /** @@ -103,25 +116,7 @@ public function _initReportAction($blocks) $blocks = [$blocks]; } - $requestData = $this->_objectManager->get( - \Magento\Backend\Helper\Data::class - )->prepareFilterString( - $this->getRequest()->getParam('filter') - ); - $inputFilter = new \Zend_Filter_Input( - ['from' => $this->_dateFilter, 'to' => $this->_dateFilter], - [], - $requestData - ); - $requestData = $inputFilter->getUnescaped(); - $requestData['store_ids'] = $this->getRequest()->getParam('store_ids'); - $params = new \Magento\Framework\DataObject(); - - foreach ($requestData as $key => $value) { - if (!empty($value)) { - $params->setData($key, $value); - } - } + $params = $this->initFilterData(); foreach ($blocks as $block) { if ($block) { @@ -147,7 +142,7 @@ protected function _showLastExecutionTime($flagCode, $refreshCode) ->loadSelf(); $updatedAt = 'undefined'; if ($flag->hasData()) { - $updatedAt = $this->timezone->formatDate( + $updatedAt = $this->timezone->formatDate( $flag->getLastUpdate(), \IntlDateFormatter::MEDIUM, true @@ -155,17 +150,51 @@ protected function _showLastExecutionTime($flagCode, $refreshCode) } $refreshStatsLink = $this->getUrl('reports/report_statistics'); - $directRefreshLink = $this->getUrl('reports/report_statistics/refreshRecent', ['code' => $refreshCode]); + $directRefreshLink = $this->getUrl('reports/report_statistics/refreshRecent'); $this->messageManager->addNotice( __( 'Last updated: %1. To refresh last day\'s <a href="%2">statistics</a>, ' . - 'click <a href="%3">here</a>.', + 'click <a href="#2" data-post="%3">here</a>.', $updatedAt, $refreshStatsLink, - $directRefreshLink + str_replace( + '"', + '"', + json_encode(['action' => $directRefreshLink, 'data' => ['code' => $refreshCode]]) + ) ) ); return $this; } + + /** + * Init filter data + * + * @return \Magento\Framework\DataObject + */ + private function initFilterData(): \Magento\Framework\DataObject + { + $requestData = $this->backendHelper + ->prepareFilterString( + $this->getRequest()->getParam('filter') + ); + + $filterRules = ['from' => $this->_dateFilter, 'to' => $this->_dateFilter]; + $inputFilter = new \Zend_Filter_Input($filterRules, [], $requestData); + + $requestData = $inputFilter->getUnescaped(); + $requestData['store_ids'] = $this->getRequest()->getParam('store_ids'); + $requestData['group'] = $this->getRequest()->getParam('group'); + $requestData['website'] = $this->getRequest()->getParam('website'); + + $params = new \Magento\Framework\DataObject(); + + foreach ($requestData as $key => $value) { + if (!empty($value)) { + $params->setData($key, $value); + } + } + return $params; + } } diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/Customer/Accounts.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/Customer/Accounts.php index ae1c0401add5..f8d0cbe9e690 100644 --- a/app/code/Magento/Reports/Controller/Adminhtml/Report/Customer/Accounts.php +++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/Customer/Accounts.php @@ -6,7 +6,9 @@ */ namespace Magento\Reports\Controller\Adminhtml\Report\Customer; -class Accounts extends \Magento\Reports\Controller\Adminhtml\Report\Customer +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Accounts extends \Magento\Reports\Controller\Adminhtml\Report\Customer implements HttpGetActionInterface { /** * New accounts action diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/Customer/Orders.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/Customer/Orders.php index 56a594ac24ea..be46fb6a94c7 100644 --- a/app/code/Magento/Reports/Controller/Adminhtml/Report/Customer/Orders.php +++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/Customer/Orders.php @@ -6,7 +6,9 @@ */ namespace Magento\Reports\Controller\Adminhtml\Report\Customer; -class Orders extends \Magento\Reports\Controller\Adminhtml\Report\Customer +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Orders extends \Magento\Reports\Controller\Adminhtml\Report\Customer implements HttpGetActionInterface { /** * Customers by number of orders action diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/Customer/Totals.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/Customer/Totals.php index 17928872eba9..02f40e5be980 100644 --- a/app/code/Magento/Reports/Controller/Adminhtml/Report/Customer/Totals.php +++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/Customer/Totals.php @@ -6,7 +6,9 @@ */ namespace Magento\Reports\Controller\Adminhtml\Report\Customer; -class Totals extends \Magento\Reports\Controller\Adminhtml\Report\Customer +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Totals extends \Magento\Reports\Controller\Adminhtml\Report\Customer implements HttpGetActionInterface { /** * Customers by orders total action diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/Product/Downloads.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/Product/Downloads.php index e8df3118caa2..f2c03d0dc22a 100644 --- a/app/code/Magento/Reports/Controller/Adminhtml/Report/Product/Downloads.php +++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/Product/Downloads.php @@ -6,7 +6,9 @@ */ namespace Magento\Reports\Controller\Adminhtml\Report\Product; -class Downloads extends \Magento\Reports\Controller\Adminhtml\Report\Product +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Downloads extends \Magento\Reports\Controller\Adminhtml\Report\Product implements HttpGetActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/Product/Lowstock.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/Product/Lowstock.php index b5e3602383f7..266d6a853414 100644 --- a/app/code/Magento/Reports/Controller/Adminhtml/Report/Product/Lowstock.php +++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/Product/Lowstock.php @@ -6,7 +6,9 @@ */ namespace Magento\Reports\Controller\Adminhtml\Report\Product; -class Lowstock extends \Magento\Reports\Controller\Adminhtml\Report\Product +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Lowstock extends \Magento\Reports\Controller\Adminhtml\Report\Product implements HttpGetActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/Product/Sold.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/Product/Sold.php index 19b8258beaa5..f01c46f4c142 100644 --- a/app/code/Magento/Reports/Controller/Adminhtml/Report/Product/Sold.php +++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/Product/Sold.php @@ -6,7 +6,9 @@ */ namespace Magento\Reports\Controller\Adminhtml\Report\Product; -class Sold extends \Magento\Reports\Controller\Adminhtml\Report\Product +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Sold extends \Magento\Reports\Controller\Adminhtml\Report\Product implements HttpGetActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/Product/Viewed.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/Product/Viewed.php index 07beec5a7273..980540fb1fa0 100644 --- a/app/code/Magento/Reports/Controller/Adminhtml/Report/Product/Viewed.php +++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/Product/Viewed.php @@ -6,9 +6,10 @@ */ namespace Magento\Reports\Controller\Adminhtml\Report\Product; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Reports\Model\Flag; -class Viewed extends \Magento\Reports\Controller\Adminhtml\Report\Product +class Viewed extends \Magento\Reports\Controller\Adminhtml\Report\Product implements HttpGetActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/Review/Customer.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/Review/Customer.php index 50b1f79abd4f..481522b86f0d 100644 --- a/app/code/Magento/Reports/Controller/Adminhtml/Report/Review/Customer.php +++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/Review/Customer.php @@ -6,7 +6,9 @@ */ namespace Magento\Reports\Controller\Adminhtml\Report\Review; -class Customer extends \Magento\Reports\Controller\Adminhtml\Report\Review +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Customer extends \Magento\Reports\Controller\Adminhtml\Report\Review implements HttpGetActionInterface { /** * Customer Reviews Report action diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/Review/Product.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/Review/Product.php index 3d0ebc5f5682..911a313377ca 100644 --- a/app/code/Magento/Reports/Controller/Adminhtml/Report/Review/Product.php +++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/Review/Product.php @@ -6,7 +6,9 @@ */ namespace Magento\Reports\Controller\Adminhtml\Report\Review; -class Product extends \Magento\Reports\Controller\Adminhtml\Report\Review +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Product extends \Magento\Reports\Controller\Adminhtml\Report\Review implements HttpGetActionInterface { /** * Product reviews report action diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/Sales/Bestsellers.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/Sales/Bestsellers.php index 60f8cc87f2ab..eff3796b6d47 100644 --- a/app/code/Magento/Reports/Controller/Adminhtml/Report/Sales/Bestsellers.php +++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/Sales/Bestsellers.php @@ -6,9 +6,10 @@ */ namespace Magento\Reports\Controller\Adminhtml\Report\Sales; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Reports\Model\Flag; -class Bestsellers extends \Magento\Reports\Controller\Adminhtml\Report\Sales +class Bestsellers extends \Magento\Reports\Controller\Adminhtml\Report\Sales implements HttpGetActionInterface { /** * Bestsellers report action diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/Sales/Coupons.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/Sales/Coupons.php index 2e324bceee3c..d9e83cd77b99 100644 --- a/app/code/Magento/Reports/Controller/Adminhtml/Report/Sales/Coupons.php +++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/Sales/Coupons.php @@ -6,9 +6,10 @@ */ namespace Magento\Reports\Controller\Adminhtml\Report\Sales; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Reports\Model\Flag; -class Coupons extends \Magento\Reports\Controller\Adminhtml\Report\Sales +class Coupons extends \Magento\Reports\Controller\Adminhtml\Report\Sales implements HttpGetActionInterface { /** * Coupons report action diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/Sales/Invoiced.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/Sales/Invoiced.php index f533d597990f..c26b0f931e6b 100644 --- a/app/code/Magento/Reports/Controller/Adminhtml/Report/Sales/Invoiced.php +++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/Sales/Invoiced.php @@ -6,9 +6,10 @@ */ namespace Magento\Reports\Controller\Adminhtml\Report\Sales; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Reports\Model\Flag; -class Invoiced extends \Magento\Reports\Controller\Adminhtml\Report\Sales +class Invoiced extends \Magento\Reports\Controller\Adminhtml\Report\Sales implements HttpGetActionInterface { /** * Invoice report action diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/Sales/Refunded.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/Sales/Refunded.php index 85c5fdaed427..f88e9f64b971 100644 --- a/app/code/Magento/Reports/Controller/Adminhtml/Report/Sales/Refunded.php +++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/Sales/Refunded.php @@ -6,9 +6,10 @@ */ namespace Magento\Reports\Controller\Adminhtml\Report\Sales; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Reports\Model\Flag; -class Refunded extends \Magento\Reports\Controller\Adminhtml\Report\Sales +class Refunded extends \Magento\Reports\Controller\Adminhtml\Report\Sales implements HttpGetActionInterface { /** * Refunds report action diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/Sales/Sales.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/Sales/Sales.php index d6460864c5c1..e3f383ab5a74 100644 --- a/app/code/Magento/Reports/Controller/Adminhtml/Report/Sales/Sales.php +++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/Sales/Sales.php @@ -6,9 +6,10 @@ */ namespace Magento\Reports\Controller\Adminhtml\Report\Sales; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Reports\Model\Flag; -class Sales extends \Magento\Reports\Controller\Adminhtml\Report\Sales +class Sales extends \Magento\Reports\Controller\Adminhtml\Report\Sales implements HttpGetActionInterface { /** * Sales report action diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/Sales/Tax.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/Sales/Tax.php index e8ae11088f5a..2e6ba487f398 100644 --- a/app/code/Magento/Reports/Controller/Adminhtml/Report/Sales/Tax.php +++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/Sales/Tax.php @@ -6,9 +6,10 @@ */ namespace Magento\Reports\Controller\Adminhtml\Report\Sales; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Reports\Model\Flag; -class Tax extends \Magento\Reports\Controller\Adminhtml\Report\Sales +class Tax extends \Magento\Reports\Controller\Adminhtml\Report\Sales implements HttpGetActionInterface { /** * Tax report action diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/Shopcart/Abandoned.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/Shopcart/Abandoned.php index e03f2f655601..3f0567c05a93 100644 --- a/app/code/Magento/Reports/Controller/Adminhtml/Report/Shopcart/Abandoned.php +++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/Shopcart/Abandoned.php @@ -6,7 +6,9 @@ */ namespace Magento\Reports\Controller\Adminhtml\Report\Shopcart; -class Abandoned extends \Magento\Reports\Controller\Adminhtml\Report\Shopcart +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Abandoned extends \Magento\Reports\Controller\Adminhtml\Report\Shopcart implements HttpGetActionInterface { /** * Abandoned carts action diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/Shopcart/Product.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/Shopcart/Product.php index 65db66f2d2e0..b41c901fe857 100644 --- a/app/code/Magento/Reports/Controller/Adminhtml/Report/Shopcart/Product.php +++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/Shopcart/Product.php @@ -6,7 +6,9 @@ */ namespace Magento\Reports\Controller\Adminhtml\Report\Shopcart; -class Product extends \Magento\Reports\Controller\Adminhtml\Report\Shopcart +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Product extends \Magento\Reports\Controller\Adminhtml\Report\Shopcart implements HttpGetActionInterface { /** * Products in carts action diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics.php index 6ba5b71b6c08..f4d2b962b9c9 100644 --- a/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics.php +++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics.php @@ -4,21 +4,19 @@ * See COPYING.txt for license details. */ -/** - * Report statistics admin controller - * - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Reports\Controller\Adminhtml\Report; use Magento\Backend\Model\Auth\Session as AuthSession; use Magento\Backend\Model\Session; +use Magento\Framework\App\Action\HttpGetActionInterface; /** + * Report statistics admin controller. + * * @api * @since 100.0.2 */ -abstract class Statistics extends \Magento\Backend\App\Action +abstract class Statistics extends \Magento\Backend\App\Action implements HttpGetActionInterface { /** * Authorization level of a basic admin session @@ -49,7 +47,7 @@ abstract class Statistics extends \Magento\Backend\App\Action /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter - * @param [] $reportTypes + * @param array $reportTypes */ public function __construct( \Magento\Backend\App\Action\Context $context, diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics/Index.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics/Index.php index d9f9de39657d..61ec31337db5 100644 --- a/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics/Index.php +++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics/Index.php @@ -6,7 +6,9 @@ */ namespace Magento\Reports\Controller\Adminhtml\Report\Statistics; -class Index extends \Magento\Reports\Controller\Adminhtml\Report\Statistics +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\Reports\Controller\Adminhtml\Report\Statistics implements HttpGetActionInterface { /** * Refresh statistics action diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics/RefreshLifetime.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics/RefreshLifetime.php index 1b7ae6398d30..b86839459355 100644 --- a/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics/RefreshLifetime.php +++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics/RefreshLifetime.php @@ -1,12 +1,17 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Reports\Controller\Adminhtml\Report\Statistics; -class RefreshLifetime extends \Magento\Reports\Controller\Adminhtml\Report\Statistics +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; +use Magento\Reports\Controller\Adminhtml\Report\Statistics; + +/** + * Refresh statistics action. + */ +class RefreshLifetime extends Statistics implements HttpPostActionInterface { /** * Refresh statistics for all period diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics/RefreshRecent.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics/RefreshRecent.php index 1f0f6e8e4053..66123938243d 100644 --- a/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics/RefreshRecent.php +++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics/RefreshRecent.php @@ -6,7 +6,12 @@ */ namespace Magento\Reports\Controller\Adminhtml\Report\Statistics; -class RefreshRecent extends \Magento\Reports\Controller\Adminhtml\Report\Statistics +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; + +/** + * Refresh recent stats. + */ +class RefreshRecent extends \Magento\Reports\Controller\Adminhtml\Report\Statistics implements HttpPostActionInterface { /** * Refresh statistics for last 25 hours diff --git a/app/code/Magento/Reports/Model/Product/Index/AbstractIndex.php b/app/code/Magento/Reports/Model/Product/Index/AbstractIndex.php index 5682892a77c6..7337286149cc 100644 --- a/app/code/Magento/Reports/Model/Product/Index/AbstractIndex.php +++ b/app/code/Magento/Reports/Model/Product/Index/AbstractIndex.php @@ -113,7 +113,7 @@ public function beforeSave() /** * Retrieve visitor id * - * if don't exists return current visitor id + * If don't exists return current visitor id * * @return int */ @@ -128,7 +128,7 @@ public function getVisitorId() /** * Retrieve customer id * - * if customer don't logged in return null + * If customer don't logged in return null * * @return int */ @@ -143,7 +143,7 @@ public function getCustomerId() /** * Retrieve store id * - * default return current store id + * Default return current store id * * @return int */ @@ -246,13 +246,14 @@ public function clean() /** * Add product ids to current visitor/customer log + * * @param string[] $productIds * @return $this */ public function registerIds($productIds) { $this->_getResource()->registerIds($this, $productIds); - $this->_getSession()->unsData($this->_countCacheKey); + $this->_getSession()->unsetData($this->_countCacheKey); return $this; } } diff --git a/app/code/Magento/Reports/Model/ReportStatus.php b/app/code/Magento/Reports/Model/ReportStatus.php index ec0c32d9af1e..d5d461168209 100644 --- a/app/code/Magento/Reports/Model/ReportStatus.php +++ b/app/code/Magento/Reports/Model/ReportStatus.php @@ -21,6 +21,8 @@ class ReportStatus private $scopeConfig; /** + * ReportStatus constructor. + * * @param ScopeConfigInterface $scopeConfig */ public function __construct(ScopeConfigInterface $scopeConfig) @@ -37,11 +39,13 @@ public function __construct(ScopeConfigInterface $scopeConfig) */ public function isReportEnabled(string $reportEventType): bool { - return (bool)$this->scopeConfig->getValue('reports/options/enabled') - && (bool)$this->scopeConfig->getValue($this->getConfigPathByEventType($reportEventType)); + return $this->scopeConfig->isSetFlag('reports/options/enabled') + && $this->scopeConfig->isSetFlag($this->getConfigPathByEventType($reportEventType)); } /** + * Get Config Path By Event Type + * * @param string $reportEventType * @return string * @throws InputException diff --git a/app/code/Magento/Reports/Model/ResourceModel/Customer/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Customer/Collection.php index bc5ceda53481..b6e55af96f4c 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Customer/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Customer/Collection.php @@ -4,12 +4,11 @@ * See COPYING.txt for license details. */ -/** - * Customers Report collection - */ namespace Magento\Reports\Model\ResourceModel\Customer; /** + * Customers Report collection. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api * @since 100.0.2 @@ -74,6 +73,7 @@ class Collection extends \Magento\Customer\Model\ResourceModel\Customer\Collecti protected $orderResource; /** + * Collection constructor. * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory * @param \Psr\Log\LoggerInterface $logger * @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy @@ -88,7 +88,7 @@ class Collection extends \Magento\Customer\Model\ResourceModel\Customer\Collecti * @param \Magento\Quote\Api\CartRepositoryInterface $quoteRepository * @param \Magento\Quote\Model\ResourceModel\Quote\Item\CollectionFactory $quoteItemFactory * @param \Magento\Sales\Model\ResourceModel\Order\Collection $orderResource - * @param mixed $connection + * @param \Magento\Framework\DB\Adapter\AdapterInterface|null $connection * @param string $modelName * * @SuppressWarnings(PHPMD.ExcessiveParameterList) diff --git a/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php index 82ebc74a0468..d89a118bff94 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php @@ -3,7 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Reports\Model\ResourceModel\Order; use Magento\Framework\DB\Select; @@ -81,7 +80,7 @@ class Collection extends \Magento\Sales\Model\ResourceModel\Order\Collection * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate * @param \Magento\Sales\Model\Order\Config $orderConfig * @param \Magento\Sales\Model\ResourceModel\Report\OrderFactory $reportOrderFactory - * @param null $connection + * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection * @param \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource * * @SuppressWarnings(PHPMD.ExcessiveParameterList) @@ -446,7 +445,7 @@ public function getDateRange($range, $customStart, $customEnd, $returnObjects = break; case 'custom': - $dateStart = $customStart ? $customStart : $dateEnd; + $dateStart = $customStart ? $customStart : $dateStart; $dateEnd = $customEnd ? $customEnd : $dateEnd; break; @@ -770,11 +769,12 @@ public function addOrdersCount() */ public function addRevenueToSelect($convertCurrency = false) { - $expr = $this->getTotalsExpression( + $expr = $this->getTotalsExpressionWithDiscountRefunded( !$convertCurrency, $this->getConnection()->getIfNullSql('main_table.base_subtotal_refunded', 0), $this->getConnection()->getIfNullSql('main_table.base_subtotal_canceled', 0), - $this->getConnection()->getIfNullSql('main_table.base_discount_canceled', 0) + $this->getConnection()->getIfNullSql('ABS(main_table.base_discount_refunded)', 0), + $this->getConnection()->getIfNullSql('ABS(main_table.base_discount_canceled)', 0) ); $this->getSelect()->columns(['revenue' => $expr]); @@ -792,11 +792,12 @@ public function addSumAvgTotals($storeId = 0) /** * calculate average and total amount */ - $expr = $this->getTotalsExpression( + $expr = $this->getTotalsExpressionWithDiscountRefunded( $storeId, $this->getConnection()->getIfNullSql('main_table.base_subtotal_refunded', 0), $this->getConnection()->getIfNullSql('main_table.base_subtotal_canceled', 0), - $this->getConnection()->getIfNullSql('main_table.base_discount_canceled', 0) + $this->getConnection()->getIfNullSql('ABS(main_table.base_discount_refunded)', 0), + $this->getConnection()->getIfNullSql('ABS(main_table.base_discount_canceled)', 0) ); $this->getSelect()->columns( @@ -809,13 +810,15 @@ public function addSumAvgTotals($storeId = 0) } /** - * Get SQL expression for totals + * Get SQL expression for totals. * * @param int $storeId * @param string $baseSubtotalRefunded * @param string $baseSubtotalCanceled * @param string $baseDiscountCanceled * @return string + * @deprecated + * @see getTotalsExpressionWithDiscountRefunded */ protected function getTotalsExpression( $storeId, @@ -825,11 +828,41 @@ protected function getTotalsExpression( ) { $template = ($storeId != 0) ? '(main_table.base_subtotal - %2$s - %1$s - ABS(main_table.base_discount_amount) - %3$s)' - : '((main_table.base_subtotal - %1$s - %2$s - ABS(main_table.base_discount_amount) - %3$s) ' - . ' * main_table.base_to_global_rate)'; + : '((main_table.base_subtotal - %1$s - %2$s - ABS(main_table.base_discount_amount) + %3$s) ' + . ' * main_table.base_to_global_rate)'; return sprintf($template, $baseSubtotalRefunded, $baseSubtotalCanceled, $baseDiscountCanceled); } + /** + * Get SQL expression for totals with discount refunded. + * + * @param int $storeId + * @param string $baseSubtotalRefunded + * @param string $baseSubtotalCanceled + * @param string $baseDiscountRefunded + * @param string $baseDiscountCanceled + * @return string + */ + private function getTotalsExpressionWithDiscountRefunded( + $storeId, + $baseSubtotalRefunded, + $baseSubtotalCanceled, + $baseDiscountRefunded, + $baseDiscountCanceled + ) { + $template = ($storeId != 0) + ? '(main_table.base_subtotal - %2$s - %1$s - (ABS(main_table.base_discount_amount) - %3$s - %4$s))' + : '((main_table.base_subtotal - %1$s - %2$s - (ABS(main_table.base_discount_amount) - %3$s - %4$s)) ' + . ' * main_table.base_to_global_rate)'; + return sprintf( + $template, + $baseSubtotalRefunded, + $baseSubtotalCanceled, + $baseDiscountRefunded, + $baseDiscountCanceled + ); + } + /** * Sort order by total amount * diff --git a/app/code/Magento/Reports/Model/ResourceModel/Product/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Product/Collection.php index 337c87f6da03..966ee14c2cb6 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Product/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Product/Collection.php @@ -5,14 +5,15 @@ */ /** - * Products Report collection - * * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Reports\Model\ResourceModel\Product; /** + * Products Report collection. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @api * @since 100.0.2 */ @@ -88,7 +89,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection * @param \Magento\Reports\Model\Event\TypeFactory $eventTypeFactory * @param \Magento\Catalog\Model\Product\Type $productType * @param \Magento\Quote\Model\ResourceModel\Quote\Collection $quoteResource - * @param mixed $connection + * @param \Magento\Framework\DB\Adapter\AdapterInterface|null $connection * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -149,7 +150,8 @@ public function __construct( } /** - * Set Type for COUNT SQL Select + * Set Type for COUNT SQL Select. + * * @codeCoverageIgnore * * @param int $type @@ -162,7 +164,8 @@ public function setSelectCountSqlType($type) } /** - * Set product entity id + * Set product entity id. + * * @codeCoverageIgnore * * @param string $entityId @@ -175,7 +178,8 @@ public function setProductEntityId($entityId) } /** - * Get product entity id + * Get product entity id. + * * @codeCoverageIgnore * * @return int @@ -186,7 +190,8 @@ public function getProductEntityId() } /** - * Set product entity table name + * Set product entity table name. + * * @codeCoverageIgnore * * @param string $value @@ -199,7 +204,8 @@ public function setProductEntityTableName($value) } /** - * Get product entity table name + * Get product entity table name. + * * @codeCoverageIgnore * * @return string @@ -210,7 +216,8 @@ public function getProductEntityTableName() } /** - * Get product attribute set id + * Get product attribute set id. + * * @codeCoverageIgnore * * @return int @@ -221,7 +228,8 @@ public function getProductAttributeSetId() } /** - * Set product attribute set id + * Set product attribute set id. + * * @codeCoverageIgnore * * @param int $value diff --git a/app/code/Magento/Reports/Model/ResourceModel/Product/Downloads/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Product/Downloads/Collection.php index 1985db0b90e2..2009cd3ff9d9 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Product/Downloads/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Product/Downloads/Collection.php @@ -4,16 +4,16 @@ * See COPYING.txt for license details. */ +namespace Magento\Reports\Model\ResourceModel\Product\Downloads; + /** * Product Downloads Report collection * * @author Magento Core Team <core@magentocommerce.com> - */ -namespace Magento\Reports\Model\ResourceModel\Product\Downloads; - -/** + * * @api * @since 100.0.2 + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection { @@ -97,4 +97,14 @@ public function addFieldToFilter($field, $condition = null) } return $this; } + + /** + * @inheritDoc + */ + public function getSelectCountSql() + { + $countSelect = parent::getSelectCountSql(); + $countSelect->reset(\Zend\Db\Sql\Select::GROUP); + return $countSelect; + } } diff --git a/app/code/Magento/Reports/Model/ResourceModel/Product/Index/Collection/AbstractCollection.php b/app/code/Magento/Reports/Model/ResourceModel/Product/Index/Collection/AbstractCollection.php index 7371bc4359f4..5b4cf39d65de 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Product/Index/Collection/AbstractCollection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Product/Index/Collection/AbstractCollection.php @@ -5,14 +5,16 @@ */ /** - * Reports Product Index Abstract Product Resource Collection - * * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Reports\Model\ResourceModel\Product\Index\Collection; /** + * Reports Product Index Abstract Product Resource Collection. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + * phpcs:disable Magento2.Classes.AbstractApi * @api * @since 100.0.2 */ @@ -52,7 +54,7 @@ abstract class AbstractCollection extends \Magento\Catalog\Model\ResourceModel\P * @param \Magento\Framework\Stdlib\DateTime $dateTime * @param \Magento\Customer\Api\GroupManagementInterface $groupManagement * @param \Magento\Customer\Model\Visitor $customerVisitor - * @param mixed $connection + * @param \Magento\Framework\DB\Adapter\AdapterInterface|null $connection * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -181,7 +183,8 @@ protected function _getWhereCondition() } /** - * Set customer id, that will be used in 'whereCondition' + * Set customer id, that will be used in 'whereCondition'. + * * @codeCoverageIgnore * * @param int $id diff --git a/app/code/Magento/Reports/Model/ResourceModel/Product/Lowstock/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Product/Lowstock/Collection.php index 732d819e3b2c..39d673911111 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Product/Lowstock/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Product/Lowstock/Collection.php @@ -14,7 +14,10 @@ use Magento\Framework\Exception\LocalizedException; /** + * Product Low Stock Report Collection. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @api * @since 100.0.2 */ @@ -50,7 +53,6 @@ class Collection extends \Magento\Reports\Model\ResourceModel\Product\Collection protected $_itemResource; /** - * Collection constructor. * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory * @param \Psr\Log\LoggerInterface $logger * @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy diff --git a/app/code/Magento/Reports/Model/ResourceModel/Quote/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Quote/Collection.php index 671acc970101..c1c6fb2eaed8 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Quote/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Quote/Collection.php @@ -5,7 +5,11 @@ */ namespace Magento\Reports\Model\ResourceModel\Quote; +use Magento\Store\Model\Store; + /** + * Collection of abandoned quotes with reports join. + * * @api * @since 100.0.2 */ @@ -48,6 +52,24 @@ public function __construct( $this->customerResource = $customerResource; } + /** + * Filter collections by stores. + * + * @param array $storeIds + * @param bool $withAdmin + * @return $this + */ + public function addStoreFilter(array $storeIds, $withAdmin = true) + { + if ($withAdmin) { + $storeIds[] = Store::DEFAULT_STORE_ID; + } + + $this->addFieldToFilter('store_id', ['in' => $storeIds]); + + return $this; + } + /** * Prepare for abandoned report * diff --git a/app/code/Magento/Reports/Observer/CatalogProductCompareClearObserver.php b/app/code/Magento/Reports/Observer/CatalogProductCompareClearObserver.php index bbe431aeeef9..26559dc27cc5 100644 --- a/app/code/Magento/Reports/Observer/CatalogProductCompareClearObserver.php +++ b/app/code/Magento/Reports/Observer/CatalogProductCompareClearObserver.php @@ -24,6 +24,7 @@ class CatalogProductCompareClearObserver implements ObserverInterface /** * @param \Magento\Reports\Model\Product\Index\ComparedFactory $productCompFactory + * @param \Magento\Reports\Model\ReportStatus $reportStatus */ public function __construct( \Magento\Reports\Model\Product\Index\ComparedFactory $productCompFactory, @@ -39,7 +40,7 @@ public function __construct( * Reset count of compared products cache * * @param \Magento\Framework\Event\Observer $observer - * @return $this + * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function execute(\Magento\Framework\Event\Observer $observer) diff --git a/app/code/Magento/Reports/Observer/CatalogProductViewObserver.php b/app/code/Magento/Reports/Observer/CatalogProductViewObserver.php index 7797dda8eabf..b3ec141ef01a 100644 --- a/app/code/Magento/Reports/Observer/CatalogProductViewObserver.php +++ b/app/code/Magento/Reports/Observer/CatalogProductViewObserver.php @@ -10,6 +10,7 @@ /** * Reports Event observer model + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class CatalogProductViewObserver implements ObserverInterface { @@ -49,6 +50,7 @@ class CatalogProductViewObserver implements ObserverInterface * @param \Magento\Customer\Model\Session $customerSession * @param \Magento\Customer\Model\Visitor $customerVisitor * @param EventSaver $eventSaver + * @param \Magento\Reports\Model\ReportStatus $reportStatus */ public function __construct( \Magento\Store\Model\StoreManagerInterface $storeManager, diff --git a/app/code/Magento/Reports/Observer/CheckoutCartAddProductObserver.php b/app/code/Magento/Reports/Observer/CheckoutCartAddProductObserver.php index 718cc02349ce..6a3b7832bd48 100644 --- a/app/code/Magento/Reports/Observer/CheckoutCartAddProductObserver.php +++ b/app/code/Magento/Reports/Observer/CheckoutCartAddProductObserver.php @@ -25,6 +25,7 @@ class CheckoutCartAddProductObserver implements ObserverInterface /** * @param EventSaver $eventSaver + * @param \Magento\Reports\Model\ReportStatus $reportStatus */ public function __construct( EventSaver $eventSaver, diff --git a/app/code/Magento/Reports/Observer/CustomerLogoutObserver.php b/app/code/Magento/Reports/Observer/CustomerLogoutObserver.php index 833834d06bc7..95d17ddacefb 100644 --- a/app/code/Magento/Reports/Observer/CustomerLogoutObserver.php +++ b/app/code/Magento/Reports/Observer/CustomerLogoutObserver.php @@ -38,7 +38,7 @@ public function __construct( * Customer logout processing * * @param \Magento\Framework\Event\Observer $observer - * @return $this + * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function execute(\Magento\Framework\Event\Observer $observer) diff --git a/app/code/Magento/Reports/Observer/SendfriendProductObserver.php b/app/code/Magento/Reports/Observer/SendfriendProductObserver.php index 0583b45d2d05..ca70b23d55ee 100644 --- a/app/code/Magento/Reports/Observer/SendfriendProductObserver.php +++ b/app/code/Magento/Reports/Observer/SendfriendProductObserver.php @@ -25,6 +25,7 @@ class SendfriendProductObserver implements ObserverInterface /** * @param EventSaver $eventSaver + * @param \Magento\Reports\Model\ReportStatus $reportStatus */ public function __construct( EventSaver $eventSaver, diff --git a/app/code/Magento/Reports/Observer/WishlistAddProductObserver.php b/app/code/Magento/Reports/Observer/WishlistAddProductObserver.php index 3fd868abbd96..e4c57cf3ef25 100644 --- a/app/code/Magento/Reports/Observer/WishlistAddProductObserver.php +++ b/app/code/Magento/Reports/Observer/WishlistAddProductObserver.php @@ -25,6 +25,7 @@ class WishlistAddProductObserver implements ObserverInterface /** * @param EventSaver $eventSaver + * @param \Magento\Reports\Model\ReportStatus $reportStatus */ public function __construct( EventSaver $eventSaver, diff --git a/app/code/Magento/Reports/Observer/WishlistShareObserver.php b/app/code/Magento/Reports/Observer/WishlistShareObserver.php index 2c4926ac12a1..de6e55ceb3f6 100644 --- a/app/code/Magento/Reports/Observer/WishlistShareObserver.php +++ b/app/code/Magento/Reports/Observer/WishlistShareObserver.php @@ -25,6 +25,7 @@ class WishlistShareObserver implements ObserverInterface /** * @param EventSaver $eventSaver + * @param \Magento\Reports\Model\ReportStatus $reportStatus */ public function __construct( EventSaver $eventSaver, diff --git a/app/code/Magento/Reports/Test/Mftf/ActionGroup/AdminReviewOrderActionGroup.xml b/app/code/Magento/Reports/Test/Mftf/ActionGroup/AdminReviewOrderActionGroup.xml new file mode 100644 index 000000000000..003a5e6655f3 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/ActionGroup/AdminReviewOrderActionGroup.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="AdminReviewOrderActionGroup"> + <arguments> + <argument name="productName" type="string"/> + </arguments> + <click stepKey="openReports" selector="{{OrderedProductsSection.reports}}"/> + <waitForPageLoad stepKey="waitForReports" time="5"/> + <click stepKey="openOrdered" selector="{{OrderedProductsSection.ordered}}"/> + <waitForPageLoad stepKey="waitForOrdersPage" time="5"/> + <click stepKey="refresh" selector="{{OrderedProductsSection.refresh}}"/> + <waitForPageLoad stepKey="waitForOrderList" time="5"/> + <scrollTo stepKey="scrollTo" selector="{{OrderedProductsSection.total}}"/> + <see stepKey="seeOrder" userInput="{{productName}}"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Reports/Test/Mftf/ActionGroup/GenerateOrderReportActionGroup.xml b/app/code/Magento/Reports/Test/Mftf/ActionGroup/GenerateOrderReportActionGroup.xml new file mode 100644 index 000000000000..d367b2deb592 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/ActionGroup/GenerateOrderReportActionGroup.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="GenerateOrderReportActionGroup"> + <arguments> + <argument name="orderFromDate" type="string"/> + <argument name="orderToDate" type="string"/> + </arguments> + <click selector="{{OrderReportMainSection.here}}" stepKey="clickOnHere" /> + <fillField selector="{{OrderReportFilterSection.dateFrom}}" userInput="{{orderFromDate}}" stepKey="fillFromDate"/> + <fillField selector="{{OrderReportFilterSection.dateTo}}" userInput="{{orderToDate}}" stepKey="fillToDate"/> + <selectOption selector="{{OrderReportFilterSection.orderStatus}}" userInput="Any" stepKey="selectAnyOption" /> + <click selector="{{OrderReportMainSection.showReport}}" stepKey="showReport" /> + </actionGroup> + <actionGroup name="GenerateOrderReportForNotCancelActionGroup"> + <arguments> + <argument name="orderFromDate" type="string"/> + <argument name="orderToDate" type="string"/> + <argument name="statuses" type="string"/> + </arguments> + <click selector="{{OrderReportMainSection.here}}" stepKey="clickOnHere" /> + <fillField selector="{{OrderReportFilterSection.dateFrom}}" userInput="{{orderFromDate}}" stepKey="fillFromDate"/> + <fillField selector="{{OrderReportFilterSection.dateTo}}" userInput="{{orderToDate}}" stepKey="fillToDate"/> + <selectOption selector="{{OrderReportFilterSection.orderStatus}}" userInput="Specified" stepKey="selectSpecifiedOption" /> + <selectOption selector="{{OrderReportFilterSection.orderStatusSpecified}}" parameterArray="{{statuses}}" stepKey="selectSpecifiedOptionStatus" /> + <click selector="{{OrderReportMainSection.showReport}}" stepKey="showReport" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Reports/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Reports/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 000000000000..e77e3ee8abd8 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,86 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminMenuReportsMarketingAbandonedCarts"> + <data key="pageTitle">Abandoned Carts</data> + <data key="title">Abandoned Carts</data> + <data key="dataUiId">magento-reports-report-shopcart-abandoned</data> + </entity> + <entity name="AdminMenuReportsProductsBestsellers"> + <data key="pageTitle">Bestsellers Report</data> + <data key="title">Bestsellers</data> + <data key="dataUiId">magento-reports-report-products-bestsellers</data> + </entity> + <entity name="AdminMenuReportsSalesCoupons"> + <data key="pageTitle">Coupons Report</data> + <data key="title">Coupons</data> + <data key="dataUiId">magento-reports-report-salesroot-coupons</data> + </entity> + <entity name="AdminMenuReportsProductsDownloads"> + <data key="pageTitle">Downloads Report</data> + <data key="title">Downloads</data> + <data key="dataUiId">magento-downloadable-report-products-downloads</data> + </entity> + <entity name="AdminMenuReportsSalesInvoiced"> + <data key="pageTitle">Invoice Report</data> + <data key="title">Invoiced</data> + <data key="dataUiId">magento-reports-report-salesroot-invoiced</data> + </entity> + <entity name="AdminMenuReportsProductsLowStock"> + <data key="pageTitle">Low Stock Report</data> + <data key="title">Low Stock</data> + <data key="dataUiId">magento-reports-report-products-lowstock</data> + </entity> + <entity name="AdminMenuReportsCustomersNew"> + <data key="pageTitle">New Accounts Report</data> + <data key="title">New</data> + <data key="dataUiId">magento-reports-report-customers-accounts</data> + </entity> + <entity name="AdminMenuReportsCustomersOrderCount"> + <data key="pageTitle">Order Count Report</data> + <data key="title">Order Count</data> + <data key="dataUiId">magento-reports-report-customers-orders</data> + </entity> + <entity name="AdminMenuReportsProductsOrdered"> + <data key="pageTitle">Ordered Products Report</data> + <data key="title">Ordered</data> + <data key="dataUiId">magento-reports-report-products-sold</data> + </entity> + <entity name="AdminMenuReportsSalesOrders"> + <data key="pageTitle">Orders Report</data> + <data key="title">Orders</data> + <data key="dataUiId">magento-reports-report-salesroot-sales</data> + </entity> + <entity name="AdminMenuReportsCustomersOrderTotal"> + <data key="pageTitle">Order Total Report</data> + <data key="title">Order Total</data> + <data key="dataUiId">magento-reports-report-customers-totals</data> + </entity> + <entity name="AdminMenuReportsMarketingProductsInCarts"> + <data key="pageTitle">Products in Carts</data> + <data key="title">Products in Cart</data> + <data key="dataUiId">magento-reports-report-shopcart-product</data> + </entity> + <entity name="AdminMenuReportsStatisticsRefreshStatistics"> + <data key="pageTitle">Refresh Statistics</data> + <data key="title">Refresh Statistics</data> + <data key="dataUiId">magento-reports-report-statistics-refresh</data> + </entity> + <entity name="AdminMenuReportsSalesTax"> + <data key="pageTitle">Tax Report</data> + <data key="title">Tax</data> + <data key="dataUiId">magento-reports-report-salesroot-tax</data> + </entity> + <entity name="AdminMenuReportsProductsViews"> + <data key="pageTitle">Product Views Report</data> + <data key="title">Views</data> + <data key="dataUiId">magento-reports-report-products-viewed</data> + </entity> +</entities> diff --git a/app/code/Magento/Reports/Test/Mftf/Page/OrdersReportPage.xml b/app/code/Magento/Reports/Test/Mftf/Page/OrdersReportPage.xml new file mode 100644 index 000000000000..46509089b97b --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Page/OrdersReportPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="OrdersReportPage" url="reports/report_sales/sales/" area="admin" module="Reports"> + <section name="OrderReportFilterSection"/> + <section name="OrderReportMainSection"/> + <section name="GeneratedReportSection" /> + </page> +</pages> diff --git a/app/code/Magento/Reports/Test/Mftf/Section/OrderReportMainSection.xml b/app/code/Magento/Reports/Test/Mftf/Section/OrderReportMainSection.xml new file mode 100644 index 000000000000..7ad9bdfa8c12 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Section/OrderReportMainSection.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="OrderReportMainSection"> + <element name="showReport" type="button" selector="#filter_form_submit"/> + <element name="here" type="text" selector="//a[contains(text(), 'here')]"/> + </section> + + <section name="OrderReportFilterSection"> + <element name="dateFrom" type="input" selector="#sales_report_from"/> + <element name="dateTo" type="input" selector="#sales_report_to"/> + <element name="orderStatus" type="select" selector="#sales_report_show_order_statuses"/> + <element name="optionAny" type="option" selector="//select[@id='sales_report_show_order_statuses']/option[contains(text(), 'Any')]"/> + <element name="optionSpecified" type="option" selector="//select[@id='sales_report_show_order_statuses']/option[contains(text(), 'Specified')]"/> + <element name="orderStatusSpecified" type="select" selector="#sales_report_order_statuses"/> + </section> + + <section name="GeneratedReportSection"> + <element name="ordersCount" type="text" selector="//tr[@class='totals']/th[@class=' col-orders col-orders_count col-number']"/> + <element name="canceledOrders" type="text" selector="//tr[@class='totals']/th[@class=' col-canceled col-total_canceled_amount a-right']"/> + </section> +</sections> diff --git a/app/code/Magento/Reports/Test/Mftf/Section/OrderedProductsSection.xml b/app/code/Magento/Reports/Test/Mftf/Section/OrderedProductsSection.xml new file mode 100644 index 000000000000..89e8497dddce --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Section/OrderedProductsSection.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="OrderedProductsSection"> + <element name="reports" type="button" selector="//li[@data-ui-id='menu-magento-reports-report']"/> + <element name="ordered" type="button" selector="//li[@data-ui-id='menu-magento-reports-report-products-sold']"/> + <element name="refresh" type="button" selector="//button[@title='Refresh']" timeout="30"/> + <element name="total" type="text" selector="//tfoot//th[contains(text(), 'Total')]"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsAbandonedCartsNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsAbandonedCartsNavigateMenuTest.xml new file mode 100644 index 000000000000..342955e0684b --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsAbandonedCartsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminReportsAbandonedCartsNavigateMenuTest"> + <annotations> + <features value="Reports"/> + <stories value="Menu Navigation"/> + <title value="Admin reports abandoned carts navigate menu test"/> + <description value="Admin should be able to navigate to Reports > Abandoned Carts"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14159"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToAbandonedCartsPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsMarketingAbandonedCarts.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsMarketingAbandonedCarts.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsBestsellersNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsBestsellersNavigateMenuTest.xml new file mode 100644 index 000000000000..259f2cde2786 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsBestsellersNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminReportsBestsellersNavigateMenuTest"> + <annotations> + <features value="Reports"/> + <stories value="Menu Navigation"/> + <title value="Admin reports bestsellers navigate menu test"/> + <description value="Admin should be able to navigate to Reports > Bestsellers"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14168"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsBestsellersPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsProductsBestsellers.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsProductsBestsellers.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsCouponsNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsCouponsNavigateMenuTest.xml new file mode 100644 index 000000000000..321f3078bc63 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsCouponsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminReportsCouponsNavigateMenuTest"> + <annotations> + <features value="Reports"/> + <stories value="Menu Navigation"/> + <title value="Admin reports coupons navigate menu test"/> + <description value="Admin should be able to navigate to Reports > Coupons"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14163"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsCouponsPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsSalesCoupons.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsSalesCoupons.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsDownloadsNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsDownloadsNavigateMenuTest.xml new file mode 100644 index 000000000000..584c1af6683a --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsDownloadsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminReportsDownloadsNavigateMenuTest"> + <annotations> + <features value="Reports"/> + <stories value="Menu Navigation"/> + <title value="Admin reports downloads navigate menu test"/> + <description value="Admin should be able to navigate to Reports > Downloads"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14171"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsDownloadsPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsProductsDownloads.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsProductsDownloads.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsInvoicedNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsInvoicedNavigateMenuTest.xml new file mode 100644 index 000000000000..34aec0620cad --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsInvoicedNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminReportsInvoicedNavigateMenuTest"> + <annotations> + <features value="Reports"/> + <stories value="Menu Navigation"/> + <title value="Admin reports invoiced navigate menu test"/> + <description value="Admin should be able to navigate to Reports > Invoiced"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14162"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsInvoicedPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsSalesInvoiced.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsSalesInvoiced.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsLowStockNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsLowStockNavigateMenuTest.xml new file mode 100644 index 000000000000..5d91d65a3a45 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsLowStockNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminReportsLowStockNavigateMenuTest"> + <annotations> + <features value="Reports"/> + <stories value="Menu Navigation"/> + <title value="Admin reports low stock navigate menu test"/> + <description value="Admin should be able to navigate to Reports > Low Stock"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14169"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsLowStockPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsProductsLowStock.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsProductsLowStock.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsNewNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsNewNavigateMenuTest.xml new file mode 100644 index 000000000000..aeb35ba65a38 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsNewNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminReportsNewNavigateMenuTest"> + <annotations> + <features value="Reports"/> + <stories value="Menu Navigation"/> + <title value="Admin reports new navigate menu test"/> + <description value="Admin should be able to navigate to Reports > New"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14166"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsNewPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsCustomersNew.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsCustomersNew.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderCountNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderCountNavigateMenuTest.xml new file mode 100644 index 000000000000..1bfbc654746e --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderCountNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminReportsOrderCountNavigateMenuTest"> + <annotations> + <features value="Reports"/> + <stories value="Menu Navigation"/> + <title value="Admin reports order count navigate menu test"/> + <description value="Admin should be able to navigate to Reports > Order Count"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14165"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsOrderCountPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsCustomersOrderCount.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsCustomersOrderCount.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderTotalNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderTotalNavigateMenuTest.xml new file mode 100644 index 000000000000..88c94b53f523 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderTotalNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminReportsOrderTotalNavigateMenuTest"> + <annotations> + <features value="Reports"/> + <stories value="Menu Navigation"/> + <title value="Admin reports order total navigate menu test"/> + <description value="Admin should be able to navigate to Reports > Order Total"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14164"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsOrderTotalPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsCustomersOrderTotal.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsCustomersOrderTotal.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedNavigateMenuTest.xml new file mode 100644 index 000000000000..e81239539a5b --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminReportsOrderedNavigateMenuTest"> + <annotations> + <features value="Reports"/> + <stories value="Menu Navigation"/> + <title value="Admin reports ordered navigate menu test"/> + <description value="Admin should be able to navigate to Reports > Ordered"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14170"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsOrderedPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsProductsOrdered.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsProductsOrdered.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrdersNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrdersNavigateMenuTest.xml new file mode 100644 index 000000000000..13fc8e735313 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrdersNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminReportsOrdersNavigateMenuTest"> + <annotations> + <features value="Reports"/> + <stories value="Menu Navigation"/> + <title value="Admin reports orders navigate menu test"/> + <description value="Admin should be able to navigate to Reports > Orders"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14160"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsOrdersPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsSalesOrders.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsSalesOrders.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsProductsInCartNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsProductsInCartNavigateMenuTest.xml new file mode 100644 index 000000000000..03877f8e58ec --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsProductsInCartNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminReportsProductsInCartNavigateMenuTest"> + <annotations> + <features value="Reports"/> + <stories value="Menu Navigation"/> + <title value="Admin reports products in cart navigate menu test"/> + <description value="Admin should be able to navigate to Reports > Products in Cart"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14158"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsProductsInCartPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsMarketingProductsInCarts.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsMarketingProductsInCarts.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsRefreshStatisticsNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsRefreshStatisticsNavigateMenuTest.xml new file mode 100644 index 000000000000..d05fc091357d --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsRefreshStatisticsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminReportsRefreshStatisticsNavigateMenuTest"> + <annotations> + <features value="Reports"/> + <stories value="Menu Navigation"/> + <title value="Admin reports refresh statistics navigate menu test"/> + <description value="Admin should be able to navigate to Reports > Refresh Statistics"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14172"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsRefreshStatisticsPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsStatisticsRefreshStatistics.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsStatisticsRefreshStatistics.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsTaxNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsTaxNavigateMenuTest.xml new file mode 100644 index 000000000000..11a065c933a3 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsTaxNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminReportsTaxNavigateMenuTest"> + <annotations> + <features value="Reports"/> + <stories value="Menu Navigation"/> + <title value="Admin reports tax navigate menu test"/> + <description value="Admin should be able to navigate to Reports > Tax"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14161"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsTaxPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsSalesTax.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsSalesTax.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsViewsNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsViewsNavigateMenuTest.xml new file mode 100644 index 000000000000..9154b96c71e3 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsViewsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminReportsViewsNavigateMenuTest"> + <annotations> + <features value="Reports"/> + <stories value="Menu Navigation"/> + <title value="Admin reports views navigate menu test"/> + <description value="Admin should be able to navigate to Reports > Views"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14167"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsViewsPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsProductsViews.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsProductsViews.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/CancelOrdersInOrderSalesReportTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/CancelOrdersInOrderSalesReportTest.xml new file mode 100644 index 000000000000..7cb23e54aa1b --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/CancelOrdersInOrderSalesReportTest.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="CancelOrdersInOrderSalesReportTest"> + <annotations> + <features value="Reports"/> + <stories value="Order Sales Report includes canceled orders"/> + <group value="reports"/> + <title value="Canceled orders in order sales report"/> + <description value="Verify canceling of orders in order sales report"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-95960"/> + <useCaseId value="MAGETWO-95823"/> + </annotations> + + <before> + <!-- log in as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- create new product --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- create new customer--> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + + <!-- Create completed order --> + <actionGroup ref="CreateOrderActionGroup" stepKey="createOrderd"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + + <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceAction"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Invoice" stepKey="seePageNameNewInvoicePage"/> + <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + + <click selector="{{AdminOrderDetailsMainActionsSection.ship}}" stepKey="clickShipAction"/> + <seeInCurrentUrl url="{{AdminShipmentNewPage.url}}" stepKey="seeOrderShipmentUrl"/> + <click selector="{{AdminShipmentMainActionsSection.submitShipment}}" stepKey="clickSubmitShipment"/> + + <!-- Create Order --> + <actionGroup ref="CreateOrderActionGroup" stepKey="createOrder"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + + <!-- Cancel order --> + <actionGroup ref="cancelPendingOrder" stepKey="cancelOrder"/> + + <!-- Generate Order report for statuses --> + <amOnPage url="{{OrdersReportPage.url}}" stepKey="goToOrdersReportPage1"/> + <!-- Get date --> + <generateDate stepKey="generateEndDate" date="+0 day" format="m/d/Y"/> + <generateDate stepKey="generateStartDate" date="-1 day" format="m/d/Y"/> + <actionGroup ref="GenerateOrderReportForNotCancelActionGroup" stepKey="generateReportAfterCancelOrderBefore"> + <argument name="orderFromDate" value="$generateStartDate"/> + <argument name="orderToDate" value="$generateEndDate"/> + <argument name="statuses" value="['closed', 'complete', 'fraud', 'holded', 'payment_review', 'paypal_canceled_reversal', 'paypal_reversed', 'processing']"/> + </actionGroup> + <waitForElement selector="{{GeneratedReportSection.ordersCount}}" stepKey="waitForOrdersCountBefore"/> + <grabTextFrom selector="{{GeneratedReportSection.ordersCount}}" stepKey="grabCanceledOrdersSpecified"/> + <!-- Generate Order report --> + <amOnPage url="{{OrdersReportPage.url}}" stepKey="goToOrdersReportPage2"/> + <!-- Get date --> + <actionGroup ref="GenerateOrderReportActionGroup" stepKey="generateReportAfterCancelOrder"> + <argument name="orderFromDate" value="$generateStartDate"/> + <argument name="orderToDate" value="$generateEndDate"/> + </actionGroup> + <waitForElement selector="{{GeneratedReportSection.ordersCount}}" stepKey="waitForOrdersCount"/> + <grabTextFrom selector="{{GeneratedReportSection.ordersCount}}" stepKey="grabCanceledOrdersAny"/> + + <!-- Compare canceled orders price --> + <assertEquals expected="{$grabCanceledOrdersSpecified}" expectedType="string" actual="{$grabCanceledOrdersAny}" actualType="string" stepKey="assertEquals"/> + </test> +</tests> diff --git a/app/code/Magento/Reports/etc/db_schema.xml b/app/code/Magento/Reports/etc/db_schema.xml index f6c8074411bf..1321ebba4d3d 100644 --- a/app/code/Magento/Reports/etc/db_schema.xml +++ b/app/code/Magento/Reports/etc/db_schema.xml @@ -21,33 +21,33 @@ comment="Store Id"/> <column xsi:type="timestamp" name="added_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Added At"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="index_id"/> </constraint> - <constraint xsi:type="foreign" name="REPORT_CMPD_PRD_IDX_CSTR_ID_CSTR_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="REPORT_CMPD_PRD_IDX_CSTR_ID_CSTR_ENTT_ENTT_ID" table="report_compared_product_index" column="customer_id" referenceTable="customer_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="REPORT_CMPD_PRD_IDX_PRD_ID_CAT_PRD_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="REPORT_CMPD_PRD_IDX_PRD_ID_CAT_PRD_ENTT_ENTT_ID" table="report_compared_product_index" column="product_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="REPORT_COMPARED_PRODUCT_INDEX_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="REPORT_COMPARED_PRODUCT_INDEX_STORE_ID_STORE_STORE_ID" table="report_compared_product_index" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="SET NULL"/> - <constraint xsi:type="unique" name="REPORT_COMPARED_PRODUCT_INDEX_VISITOR_ID_PRODUCT_ID"> + <constraint xsi:type="unique" referenceId="REPORT_COMPARED_PRODUCT_INDEX_VISITOR_ID_PRODUCT_ID"> <column name="visitor_id"/> <column name="product_id"/> </constraint> - <constraint xsi:type="unique" name="REPORT_COMPARED_PRODUCT_INDEX_CUSTOMER_ID_PRODUCT_ID"> + <constraint xsi:type="unique" referenceId="REPORT_COMPARED_PRODUCT_INDEX_CUSTOMER_ID_PRODUCT_ID"> <column name="customer_id"/> <column name="product_id"/> </constraint> - <index name="REPORT_COMPARED_PRODUCT_INDEX_STORE_ID" indexType="btree"> + <index referenceId="REPORT_COMPARED_PRODUCT_INDEX_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="REPORT_COMPARED_PRODUCT_INDEX_ADDED_AT" indexType="btree"> + <index referenceId="REPORT_COMPARED_PRODUCT_INDEX_ADDED_AT" indexType="btree"> <column name="added_at"/> </index> - <index name="REPORT_COMPARED_PRODUCT_INDEX_PRODUCT_ID" indexType="btree"> + <index referenceId="REPORT_COMPARED_PRODUCT_INDEX_PRODUCT_ID" indexType="btree"> <column name="product_id"/> </index> </table> @@ -65,33 +65,33 @@ comment="Store Id"/> <column xsi:type="timestamp" name="added_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Added At"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="index_id"/> </constraint> - <constraint xsi:type="foreign" name="REPORT_VIEWED_PRD_IDX_CSTR_ID_CSTR_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="REPORT_VIEWED_PRD_IDX_CSTR_ID_CSTR_ENTT_ENTT_ID" table="report_viewed_product_index" column="customer_id" referenceTable="customer_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="REPORT_VIEWED_PRD_IDX_PRD_ID_CAT_PRD_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="REPORT_VIEWED_PRD_IDX_PRD_ID_CAT_PRD_ENTT_ENTT_ID" table="report_viewed_product_index" column="product_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="REPORT_VIEWED_PRODUCT_INDEX_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="REPORT_VIEWED_PRODUCT_INDEX_STORE_ID_STORE_STORE_ID" table="report_viewed_product_index" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="SET NULL"/> - <constraint xsi:type="unique" name="REPORT_VIEWED_PRODUCT_INDEX_VISITOR_ID_PRODUCT_ID"> + <constraint xsi:type="unique" referenceId="REPORT_VIEWED_PRODUCT_INDEX_VISITOR_ID_PRODUCT_ID"> <column name="visitor_id"/> <column name="product_id"/> </constraint> - <constraint xsi:type="unique" name="REPORT_VIEWED_PRODUCT_INDEX_CUSTOMER_ID_PRODUCT_ID"> + <constraint xsi:type="unique" referenceId="REPORT_VIEWED_PRODUCT_INDEX_CUSTOMER_ID_PRODUCT_ID"> <column name="customer_id"/> <column name="product_id"/> </constraint> - <index name="REPORT_VIEWED_PRODUCT_INDEX_STORE_ID" indexType="btree"> + <index referenceId="REPORT_VIEWED_PRODUCT_INDEX_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="REPORT_VIEWED_PRODUCT_INDEX_ADDED_AT" indexType="btree"> + <index referenceId="REPORT_VIEWED_PRODUCT_INDEX_ADDED_AT" indexType="btree"> <column name="added_at"/> </index> - <index name="REPORT_VIEWED_PRODUCT_INDEX_PRODUCT_ID" indexType="btree"> + <index referenceId="REPORT_VIEWED_PRODUCT_INDEX_PRODUCT_ID" indexType="btree"> <column name="product_id"/> </index> </table> @@ -101,7 +101,7 @@ <column xsi:type="varchar" name="event_name" nullable="false" length="64" comment="Event Name"/> <column xsi:type="smallint" name="customer_login" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Customer Login"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="event_type_id"/> </constraint> </table> @@ -120,27 +120,27 @@ default="0" comment="Subtype"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Store Id"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="event_id"/> </constraint> - <constraint xsi:type="foreign" name="REPORT_EVENT_STORE_ID_STORE_STORE_ID" table="report_event" + <constraint xsi:type="foreign" referenceId="REPORT_EVENT_STORE_ID_STORE_STORE_ID" table="report_event" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="REPORT_EVENT_EVENT_TYPE_ID_REPORT_EVENT_TYPES_EVENT_TYPE_ID" + <constraint xsi:type="foreign" referenceId="REPORT_EVENT_EVENT_TYPE_ID_REPORT_EVENT_TYPES_EVENT_TYPE_ID" table="report_event" column="event_type_id" referenceTable="report_event_types" referenceColumn="event_type_id" onDelete="CASCADE"/> - <index name="REPORT_EVENT_EVENT_TYPE_ID" indexType="btree"> + <index referenceId="REPORT_EVENT_EVENT_TYPE_ID" indexType="btree"> <column name="event_type_id"/> </index> - <index name="REPORT_EVENT_SUBJECT_ID" indexType="btree"> + <index referenceId="REPORT_EVENT_SUBJECT_ID" indexType="btree"> <column name="subject_id"/> </index> - <index name="REPORT_EVENT_OBJECT_ID" indexType="btree"> + <index referenceId="REPORT_EVENT_OBJECT_ID" indexType="btree"> <column name="object_id"/> </index> - <index name="REPORT_EVENT_SUBTYPE" indexType="btree"> + <index referenceId="REPORT_EVENT_SUBTYPE" indexType="btree"> <column name="subtype"/> </index> - <index name="REPORT_EVENT_STORE_ID" indexType="btree"> + <index referenceId="REPORT_EVENT_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -159,24 +159,24 @@ default="0" comment="Number of Views"/> <column xsi:type="smallint" name="rating_pos" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Rating Pos"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> </constraint> - <constraint xsi:type="foreign" name="REPORT_VIEWED_PRODUCT_AGGREGATED_DAILY_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="REPORT_VIEWED_PRODUCT_AGGREGATED_DAILY_STORE_ID_STORE_STORE_ID" table="report_viewed_product_aggregated_daily" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="REPORT_VIEWED_PRD_AGGRED_DAILY_PRD_ID_CAT_PRD_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="REPORT_VIEWED_PRD_AGGRED_DAILY_PRD_ID_CAT_PRD_ENTT_ENTT_ID" table="report_viewed_product_aggregated_daily" column="product_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="REPORT_VIEWED_PRD_AGGRED_DAILY_PERIOD_STORE_ID_PRD_ID"> + <constraint xsi:type="unique" referenceId="REPORT_VIEWED_PRD_AGGRED_DAILY_PERIOD_STORE_ID_PRD_ID"> <column name="period"/> <column name="store_id"/> <column name="product_id"/> </constraint> - <index name="REPORT_VIEWED_PRODUCT_AGGREGATED_DAILY_STORE_ID" indexType="btree"> + <index referenceId="REPORT_VIEWED_PRODUCT_AGGREGATED_DAILY_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="REPORT_VIEWED_PRODUCT_AGGREGATED_DAILY_PRODUCT_ID" indexType="btree"> + <index referenceId="REPORT_VIEWED_PRODUCT_AGGREGATED_DAILY_PRODUCT_ID" indexType="btree"> <column name="product_id"/> </index> </table> @@ -195,24 +195,24 @@ default="0" comment="Number of Views"/> <column xsi:type="smallint" name="rating_pos" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Rating Pos"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> </constraint> - <constraint xsi:type="foreign" name="REPORT_VIEWED_PRODUCT_AGGREGATED_MONTHLY_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="REPORT_VIEWED_PRODUCT_AGGREGATED_MONTHLY_STORE_ID_STORE_STORE_ID" table="report_viewed_product_aggregated_monthly" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="REPORT_VIEWED_PRD_AGGRED_MONTHLY_PRD_ID_CAT_PRD_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="REPORT_VIEWED_PRD_AGGRED_MONTHLY_PRD_ID_CAT_PRD_ENTT_ENTT_ID" table="report_viewed_product_aggregated_monthly" column="product_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="REPORT_VIEWED_PRD_AGGRED_MONTHLY_PERIOD_STORE_ID_PRD_ID"> + <constraint xsi:type="unique" referenceId="REPORT_VIEWED_PRD_AGGRED_MONTHLY_PERIOD_STORE_ID_PRD_ID"> <column name="period"/> <column name="store_id"/> <column name="product_id"/> </constraint> - <index name="REPORT_VIEWED_PRODUCT_AGGREGATED_MONTHLY_STORE_ID" indexType="btree"> + <index referenceId="REPORT_VIEWED_PRODUCT_AGGREGATED_MONTHLY_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="REPORT_VIEWED_PRODUCT_AGGREGATED_MONTHLY_PRODUCT_ID" indexType="btree"> + <index referenceId="REPORT_VIEWED_PRODUCT_AGGREGATED_MONTHLY_PRODUCT_ID" indexType="btree"> <column name="product_id"/> </index> </table> @@ -231,24 +231,24 @@ default="0" comment="Number of Views"/> <column xsi:type="smallint" name="rating_pos" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Rating Pos"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> </constraint> - <constraint xsi:type="foreign" name="REPORT_VIEWED_PRODUCT_AGGREGATED_YEARLY_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="REPORT_VIEWED_PRODUCT_AGGREGATED_YEARLY_STORE_ID_STORE_STORE_ID" table="report_viewed_product_aggregated_yearly" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="REPORT_VIEWED_PRD_AGGRED_YEARLY_PRD_ID_CAT_PRD_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="REPORT_VIEWED_PRD_AGGRED_YEARLY_PRD_ID_CAT_PRD_ENTT_ENTT_ID" table="report_viewed_product_aggregated_yearly" column="product_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="REPORT_VIEWED_PRD_AGGRED_YEARLY_PERIOD_STORE_ID_PRD_ID"> + <constraint xsi:type="unique" referenceId="REPORT_VIEWED_PRD_AGGRED_YEARLY_PERIOD_STORE_ID_PRD_ID"> <column name="period"/> <column name="store_id"/> <column name="product_id"/> </constraint> - <index name="REPORT_VIEWED_PRODUCT_AGGREGATED_YEARLY_STORE_ID" indexType="btree"> + <index referenceId="REPORT_VIEWED_PRODUCT_AGGREGATED_YEARLY_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="REPORT_VIEWED_PRODUCT_AGGREGATED_YEARLY_PRODUCT_ID" indexType="btree"> + <index referenceId="REPORT_VIEWED_PRODUCT_AGGREGATED_YEARLY_PRODUCT_ID" indexType="btree"> <column name="product_id"/> </index> </table> diff --git a/app/code/Magento/Reports/view/adminhtml/layout/reports_report_customer_accounts_grid.xml b/app/code/Magento/Reports/view/adminhtml/layout/reports_report_customer_accounts_grid.xml index 900dc08d571d..55ca286ad3d4 100644 --- a/app/code/Magento/Reports/view/adminhtml/layout/reports_report_customer_accounts_grid.xml +++ b/app/code/Magento/Reports/view/adminhtml/layout/reports_report_customer_accounts_grid.xml @@ -11,7 +11,7 @@ <referenceBlock name="adminhtml.report.grid"> <arguments> <argument name="id" xsi:type="string">gridAccounts</argument> - <argument name="dataSource" xsi:type="object">Magento\Reports\Model\ResourceModel\Accounts\Collection\Initial</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Reports\Model\ResourceModel\Accounts\Collection\Initial</argument> </arguments> </referenceBlock> <referenceBlock name="adminhtml.report.grid.export"> diff --git a/app/code/Magento/Reports/view/adminhtml/layout/reports_report_customer_orders_grid.xml b/app/code/Magento/Reports/view/adminhtml/layout/reports_report_customer_orders_grid.xml index d886e5724cb0..f97bec3c1525 100644 --- a/app/code/Magento/Reports/view/adminhtml/layout/reports_report_customer_orders_grid.xml +++ b/app/code/Magento/Reports/view/adminhtml/layout/reports_report_customer_orders_grid.xml @@ -11,7 +11,7 @@ <referenceBlock name="adminhtml.report.grid"> <arguments> <argument name="id" xsi:type="string">gridOrdersCustomer</argument> - <argument name="dataSource" xsi:type="object">Magento\Reports\Model\ResourceModel\Customer\Orders\Collection\Initial</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Reports\Model\ResourceModel\Customer\Orders\Collection\Initial</argument> </arguments> </referenceBlock> <referenceBlock name="adminhtml.report.grid.export"> diff --git a/app/code/Magento/Reports/view/adminhtml/layout/reports_report_customer_totals_grid.xml b/app/code/Magento/Reports/view/adminhtml/layout/reports_report_customer_totals_grid.xml index 4914829cf6eb..e1df04237c2a 100644 --- a/app/code/Magento/Reports/view/adminhtml/layout/reports_report_customer_totals_grid.xml +++ b/app/code/Magento/Reports/view/adminhtml/layout/reports_report_customer_totals_grid.xml @@ -11,7 +11,7 @@ <referenceBlock name="adminhtml.report.grid"> <arguments> <argument name="id" xsi:type="string">gridTotalsCustomer</argument> - <argument name="dataSource" xsi:type="object">Magento\Reports\Model\ResourceModel\Customer\Totals\Collection\Initial</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Reports\Model\ResourceModel\Customer\Totals\Collection\Initial</argument> </arguments> </referenceBlock> <referenceBlock name="adminhtml.report.grid.export"> diff --git a/app/code/Magento/Reports/view/adminhtml/layout/reports_report_grid.xml b/app/code/Magento/Reports/view/adminhtml/layout/reports_report_grid.xml index 82aa475807a2..0f6fbabb6a55 100644 --- a/app/code/Magento/Reports/view/adminhtml/layout/reports_report_grid.xml +++ b/app/code/Magento/Reports/view/adminhtml/layout/reports_report_grid.xml @@ -24,7 +24,7 @@ <argument name="use_ajax" xsi:type="string">0</argument> <argument name="pager_visibility" xsi:type="string">0</argument> <argument name="id" xsi:type="string">gridReport</argument> - <argument name="dataSource" xsi:type="object">Magento\Reports\Model\ResourceModel\Report\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Reports\Model\ResourceModel\Report\Collection</argument> </arguments> <block class="Magento\Backend\Block\Widget\Grid\Export" name="adminhtml.report.grid.export" as="grid.export"> <arguments> diff --git a/app/code/Magento/Reports/view/adminhtml/layout/reports_report_product_lowstock_grid.xml b/app/code/Magento/Reports/view/adminhtml/layout/reports_report_product_lowstock_grid.xml index 070c39259aab..62916fe1c1d7 100644 --- a/app/code/Magento/Reports/view/adminhtml/layout/reports_report_product_lowstock_grid.xml +++ b/app/code/Magento/Reports/view/adminhtml/layout/reports_report_product_lowstock_grid.xml @@ -12,7 +12,7 @@ <arguments> <argument name="id" xsi:type="string">gridLowstock</argument> <argument name="use_ajax" xsi:type="string">0</argument> - <argument name="dataSource" xsi:type="object">Magento\Reports\Model\ResourceModel\Product\Lowstock\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Reports\Model\ResourceModel\Product\Lowstock\Collection</argument> </arguments> <block class="Magento\Backend\Block\Widget\Grid\Export" name="adminhtml.block.report.product.lowstock.export" as="grid.export"> <arguments> diff --git a/app/code/Magento/Reports/view/adminhtml/layout/reports_report_product_sold_grid.xml b/app/code/Magento/Reports/view/adminhtml/layout/reports_report_product_sold_grid.xml index a1b01aeeb526..22c66352b32e 100644 --- a/app/code/Magento/Reports/view/adminhtml/layout/reports_report_product_sold_grid.xml +++ b/app/code/Magento/Reports/view/adminhtml/layout/reports_report_product_sold_grid.xml @@ -11,7 +11,7 @@ <referenceBlock name="adminhtml.report.grid"> <arguments> <argument name="id" xsi:type="string">gridProductsSold</argument> - <argument name="dataSource" xsi:type="object">Magento\Reports\Model\ResourceModel\Product\Sold\Collection\Initial</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Reports\Model\ResourceModel\Product\Sold\Collection\Initial</argument> </arguments> </referenceBlock> <referenceBlock name="adminhtml.report.grid.export"> diff --git a/app/code/Magento/Reports/view/adminhtml/layout/reports_report_review_customer_grid.xml b/app/code/Magento/Reports/view/adminhtml/layout/reports_report_review_customer_grid.xml index f941ca52eef5..a728f471e4de 100644 --- a/app/code/Magento/Reports/view/adminhtml/layout/reports_report_review_customer_grid.xml +++ b/app/code/Magento/Reports/view/adminhtml/layout/reports_report_review_customer_grid.xml @@ -13,7 +13,7 @@ <argument name="id" xsi:type="string">customers_grid</argument> <argument name="default_sort" xsi:type="string">review_cnt</argument> <argument name="default_dir" xsi:type="string">desc</argument> - <argument name="dataSource" xsi:type="object">Magento\Reports\Model\ResourceModel\Review\Customer\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Reports\Model\ResourceModel\Review\Customer\Collection</argument> </arguments> <block class="Magento\Backend\Block\Widget\Grid\Export" name="adminhtml.block.report.review.customer.export" as="grid.export"> <arguments> diff --git a/app/code/Magento/Reports/view/adminhtml/layout/reports_report_review_product_grid.xml b/app/code/Magento/Reports/view/adminhtml/layout/reports_report_review_product_grid.xml index 1275e761ade3..26d0e8b13659 100644 --- a/app/code/Magento/Reports/view/adminhtml/layout/reports_report_review_product_grid.xml +++ b/app/code/Magento/Reports/view/adminhtml/layout/reports_report_review_product_grid.xml @@ -13,7 +13,7 @@ <argument name="id" xsi:type="string">gridProducts</argument> <argument name="default_sort" xsi:type="string">review_cnt</argument> <argument name="default_dir" xsi:type="string">desc</argument> - <argument name="dataSource" xsi:type="object">Magento\Reports\Model\ResourceModel\Review\Product\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Reports\Model\ResourceModel\Review\Product\Collection</argument> </arguments> <block class="Magento\Backend\Block\Widget\Grid\Export" name="adminhtml.block.report.review.product.export" as="grid.export"> <arguments> diff --git a/app/code/Magento/Reports/view/adminhtml/layout/reports_report_statistics_index.xml b/app/code/Magento/Reports/view/adminhtml/layout/reports_report_statistics_index.xml index 4ec984ef9fc1..649dc7ceeb06 100644 --- a/app/code/Magento/Reports/view/adminhtml/layout/reports_report_statistics_index.xml +++ b/app/code/Magento/Reports/view/adminhtml/layout/reports_report_statistics_index.xml @@ -15,7 +15,7 @@ <argument name="use_ajax" xsi:type="string">0</argument> <argument name="sortable" xsi:type="string">0</argument> <argument name="pager_visibility" xsi:type="string">0</argument> - <argument name="dataSource" xsi:type="object">Magento\Reports\Model\ResourceModel\Refresh\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Reports\Model\ResourceModel\Refresh\Collection</argument> </arguments> <block class="Magento\Backend\Block\Widget\Grid\Massaction" name="adminhtml.block.refresh.statistics.massactions" as="grid.massaction"> <arguments> diff --git a/app/code/Magento/Review/Block/Adminhtml/Add.php b/app/code/Magento/Review/Block/Adminhtml/Add.php index 96dd02e65f18..c5600fe06100 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Add.php +++ b/app/code/Magento/Review/Block/Adminhtml/Add.php @@ -56,7 +56,7 @@ protected function _construct() }, loadProductData : function() { jQuery.ajax({ - type: "POST", + type: "GET", url: review.productInfoUrl, data: { form_key: FORM_KEY diff --git a/app/code/Magento/Review/Block/Adminhtml/Edit.php b/app/code/Magento/Review/Block/Adminhtml/Edit.php index d6868eae6fcb..f6f0ccef9b4e 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Edit.php +++ b/app/code/Magento/Review/Block/Adminhtml/Edit.php @@ -159,13 +159,13 @@ protected function _construct() } if ($this->getRequest()->getParam('ret', false) == 'pending') { - $this->buttonList->update('back', 'onclick', 'setLocation(\'' . $this->getUrl('catalog/*/pending') . '\')'); + $this->buttonList->update('back', 'onclick', 'setLocation(\'' . $this->getUrl('review/*/pending') . '\')'); $this->buttonList->update( 'delete', 'onclick', 'deleteConfirm(' . '\'' . __( 'Are you sure you want to do this?' - ) . '\' ' . '\'' . $this->getUrl( + ) . '\', ' . '\'' . $this->getUrl( '*/*/delete', [$this->_objectId => $this->getRequest()->getParam($this->_objectId), 'ret' => 'pending'] ) . '\'' . ')' diff --git a/app/code/Magento/Review/Block/Adminhtml/Edit/Form.php b/app/code/Magento/Review/Block/Adminhtml/Edit/Form.php index 8a8395de72b6..4f7237a0b44b 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Edit/Form.php +++ b/app/code/Magento/Review/Block/Adminhtml/Edit/Form.php @@ -4,11 +4,11 @@ * See COPYING.txt for license details. */ +namespace Magento\Review\Block\Adminhtml\Edit; + /** * Adminhtml Review Edit Form */ -namespace Magento\Review\Block\Adminhtml\Edit; - class Form extends \Magento\Backend\Block\Widget\Form\Generic { /** @@ -84,7 +84,8 @@ protected function _prepareForm() 'review/*/save', [ 'id' => $this->getRequest()->getParam('id'), - 'ret' => $this->_coreRegistry->registry('ret') + 'ret' => $this->_coreRegistry->registry('ret'), + 'productId' => $this->getRequest()->getParam('productId') ] ), 'method' => 'post', diff --git a/app/code/Magento/Review/Block/Adminhtml/Rating/Detailed.php b/app/code/Magento/Review/Block/Adminhtml/Rating/Detailed.php index adad931da5a6..a02c998f856b 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Rating/Detailed.php +++ b/app/code/Magento/Review/Block/Adminhtml/Rating/Detailed.php @@ -121,9 +121,9 @@ public function getRating() )->setStoreFilter( $stores )->setPositionOrder()->load()->addOptionToItems(); - if (intval($this->getRequest()->getParam('id'))) { + if ((int)$this->getRequest()->getParam('id')) { $this->_voteCollection = $this->_votesFactory->create()->setReviewFilter( - intval($this->getRequest()->getParam('id')) + (int)$this->getRequest()->getParam('id') )->addOptionInfo()->load()->addRatingOptions(); } } diff --git a/app/code/Magento/Review/Block/Product/ReviewRenderer.php b/app/code/Magento/Review/Block/Product/ReviewRenderer.php index 3cd15aba3042..3183196ebf30 100644 --- a/app/code/Magento/Review/Block/Product/ReviewRenderer.php +++ b/app/code/Magento/Review/Block/Product/ReviewRenderer.php @@ -9,7 +9,11 @@ use Magento\Catalog\Block\Product\ReviewRendererInterface; use Magento\Catalog\Model\Product; +use Magento\Review\Observer\PredispatchReviewObserver; +/** + * Class ReviewRenderer + */ class ReviewRenderer extends \Magento\Framework\View\Element\Template implements ReviewRendererInterface { /** @@ -43,6 +47,19 @@ public function __construct( parent::__construct($context, $data); } + /** + * Review module availability + * + * @return string + */ + public function isReviewEnabled() : string + { + return $this->_scopeConfig->getValue( + PredispatchReviewObserver::XML_PATH_REVIEW_ACTIVE, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + } + /** * Get review summary html * diff --git a/app/code/Magento/Review/Block/Rating/Entity/Detailed.php b/app/code/Magento/Review/Block/Rating/Entity/Detailed.php index eee00483ace6..d496b3955de7 100644 --- a/app/code/Magento/Review/Block/Rating/Entity/Detailed.php +++ b/app/code/Magento/Review/Block/Rating/Entity/Detailed.php @@ -37,12 +37,14 @@ public function __construct( } /** + * Returns block html + * * @return string */ protected function _toHtml() { $entityId = $this->_request->getParam('id'); - if (intval($entityId) <= 0) { + if ((int)$entityId <= 0) { return ''; } diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/Edit.php b/app/code/Magento/Review/Controller/Adminhtml/Product/Edit.php index a39b22ec080c..7d922f30cd98 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Product/Edit.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Product/Edit.php @@ -5,10 +5,11 @@ */ namespace Magento\Review\Controller\Adminhtml\Product; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Review\Controller\Adminhtml\Product as ProductController; use Magento\Framework\Controller\ResultFactory; -class Edit extends ProductController +class Edit extends ProductController implements HttpGetActionInterface { /** * @return \Magento\Backend\Model\View\Result\Page diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/Index.php b/app/code/Magento/Review/Controller/Adminhtml/Product/Index.php index 0dce5b238838..94c80f89f8d8 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Product/Index.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Product/Index.php @@ -5,10 +5,11 @@ */ namespace Magento\Review\Controller\Adminhtml\Product; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Review\Controller\Adminhtml\Product as ProductController; use Magento\Framework\Controller\ResultFactory; -class Index extends ProductController +class Index extends ProductController implements HttpGetActionInterface { /** * @return \Magento\Framework\Controller\ResultInterface diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/JsonProductInfo.php b/app/code/Magento/Review/Controller/Adminhtml/Product/JsonProductInfo.php index c6e9cc81d581..db336d0c2f56 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Product/JsonProductInfo.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Product/JsonProductInfo.php @@ -5,6 +5,7 @@ */ namespace Magento\Review\Controller\Adminhtml\Product; +use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Review\Controller\Adminhtml\Product as ProductController; use Magento\Backend\App\Action\Context; use Magento\Framework\Registry; @@ -14,7 +15,10 @@ use Magento\Framework\DataObject; use Magento\Framework\Controller\ResultFactory; -class JsonProductInfo extends ProductController +/** + * Represents product info in json + */ +class JsonProductInfo extends ProductController implements HttpGetActionInterface { /** * @var \Magento\Catalog\Api\ProductRepositoryInterface @@ -40,13 +44,15 @@ public function __construct( } /** + * Execute controller + * * @return \Magento\Framework\Controller\Result\Json */ public function execute() { $response = new DataObject(); $id = $this->getRequest()->getParam('id'); - if (intval($id) > 0) { + if ((int)$id > 0) { $product = $this->productRepository->getById($id); $response->setId($id); $response->addData($product->getData()); diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/NewAction.php b/app/code/Magento/Review/Controller/Adminhtml/Product/NewAction.php index 5ac3a6a05724..2709b5ce64b3 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Product/NewAction.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Product/NewAction.php @@ -5,10 +5,11 @@ */ namespace Magento\Review\Controller\Adminhtml\Product; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Review\Controller\Adminhtml\Product as ProductController; use Magento\Framework\Controller\ResultFactory; -class NewAction extends ProductController +class NewAction extends ProductController implements HttpGetActionInterface { /** * @return \Magento\Backend\Model\View\Result\Page diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/Post.php b/app/code/Magento/Review/Controller/Adminhtml/Product/Post.php index 1f21a52077bb..b62fcc7326ee 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Product/Post.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Product/Post.php @@ -5,12 +5,13 @@ */ namespace Magento\Review\Controller\Adminhtml\Product; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Review\Controller\Adminhtml\Product as ProductController; use Magento\Framework\Controller\ResultFactory; use Magento\Store\Model\Store; use Magento\Framework\Exception\LocalizedException; -class Post extends ProductController +class Post extends ProductController implements HttpPostActionInterface { /** * @return \Magento\Backend\Model\View\Result\Redirect diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/ProductGrid.php b/app/code/Magento/Review/Controller/Adminhtml/Product/ProductGrid.php index f1b25c3613d4..e4057be14af2 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Product/ProductGrid.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Product/ProductGrid.php @@ -5,6 +5,8 @@ */ namespace Magento\Review\Controller\Adminhtml\Product; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Review\Controller\Adminhtml\Product as ProductController; use Magento\Backend\App\Action\Context; use Magento\Framework\Registry; @@ -13,7 +15,7 @@ use Magento\Framework\View\LayoutFactory; use Magento\Framework\Controller\ResultFactory; -class ProductGrid extends ProductController +class ProductGrid extends ProductController implements HttpPostActionInterface, HttpGetActionInterface { /** * @var \Magento\Framework\View\LayoutFactory diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/RatingItems.php b/app/code/Magento/Review/Controller/Adminhtml/Product/RatingItems.php index 5b0ab217e719..1da8e4abbd6b 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Product/RatingItems.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Product/RatingItems.php @@ -5,6 +5,8 @@ */ namespace Magento\Review\Controller\Adminhtml\Product; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Review\Controller\Adminhtml\Product as ProductController; use Magento\Backend\App\Action\Context; use Magento\Framework\Registry; @@ -13,7 +15,7 @@ use Magento\Framework\View\LayoutFactory; use Magento\Framework\Controller\ResultFactory; -class RatingItems extends ProductController +class RatingItems extends ProductController implements HttpPostActionInterface, HttpGetActionInterface { /** * @var \Magento\Framework\View\LayoutFactory diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/Save.php b/app/code/Magento/Review/Controller/Adminhtml/Product/Save.php index 7159b1825dc4..57b1e538ddb6 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Product/Save.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Product/Save.php @@ -5,13 +5,19 @@ */ namespace Magento\Review\Controller\Adminhtml\Product; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Review\Controller\Adminhtml\Product as ProductController; use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Exception\LocalizedException; -class Save extends ProductController +/** + * Save Review action. + */ +class Save extends ProductController implements HttpPostActionInterface { /** + * Save Review action. + * * @return \Magento\Backend\Model\View\Result\Redirect * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ @@ -63,10 +69,14 @@ public function execute() if ($nextId) { $resultRedirect->setPath('review/*/edit', ['id' => $nextId]); } elseif ($this->getRequest()->getParam('ret') == 'pending') { - $resultRedirect->setPath('*/*/pending'); + $resultRedirect->setPath('review/*/pending'); } else { $resultRedirect->setPath('*/*/'); } + $productId = (int)$this->getRequest()->getParam('productId'); + if ($productId) { + $resultRedirect->setPath("catalog/product/edit/id/$productId"); + } return $resultRedirect; } $resultRedirect->setPath('review/*/'); diff --git a/app/code/Magento/Review/Controller/Adminhtml/Rating/Delete.php b/app/code/Magento/Review/Controller/Adminhtml/Rating/Delete.php index 5535c3de26e4..b25db6e498fe 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Rating/Delete.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Rating/Delete.php @@ -5,10 +5,11 @@ */ namespace Magento\Review\Controller\Adminhtml\Rating; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Review\Controller\Adminhtml\Rating as RatingController; use Magento\Framework\Controller\ResultFactory; -class Delete extends RatingController +class Delete extends RatingController implements HttpPostActionInterface { /** * @return \Magento\Backend\Model\View\Result\Redirect diff --git a/app/code/Magento/Review/Controller/Adminhtml/Rating/Edit.php b/app/code/Magento/Review/Controller/Adminhtml/Rating/Edit.php index 1b65966b7705..90dac026cfd6 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Rating/Edit.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Rating/Edit.php @@ -5,10 +5,11 @@ */ namespace Magento\Review\Controller\Adminhtml\Rating; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Review\Controller\Adminhtml\Rating as RatingController; use Magento\Framework\Controller\ResultFactory; -class Edit extends RatingController +class Edit extends RatingController implements HttpGetActionInterface { /** * @return \Magento\Backend\Model\View\Result\Page diff --git a/app/code/Magento/Review/Controller/Adminhtml/Rating/Index.php b/app/code/Magento/Review/Controller/Adminhtml/Rating/Index.php index b719a2995057..ff9ab4d50eac 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Rating/Index.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Rating/Index.php @@ -5,10 +5,11 @@ */ namespace Magento\Review\Controller\Adminhtml\Rating; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Review\Controller\Adminhtml\Rating as RatingController; use Magento\Framework\Controller\ResultFactory; -class Index extends RatingController +class Index extends RatingController implements HttpGetActionInterface { /** * @return \Magento\Backend\Model\View\Result\Page diff --git a/app/code/Magento/Review/Controller/Adminhtml/Rating/NewAction.php b/app/code/Magento/Review/Controller/Adminhtml/Rating/NewAction.php index 18ff73ad31c5..92d20aeec3eb 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Rating/NewAction.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Rating/NewAction.php @@ -5,10 +5,11 @@ */ namespace Magento\Review\Controller\Adminhtml\Rating; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Review\Controller\Adminhtml\Rating as RatingController; use Magento\Framework\Controller\ResultFactory; -class NewAction extends RatingController +class NewAction extends RatingController implements HttpGetActionInterface { /** * @return \Magento\Backend\Model\View\Result\Forward diff --git a/app/code/Magento/Review/Controller/Adminhtml/Rating/Save.php b/app/code/Magento/Review/Controller/Adminhtml/Rating/Save.php index 62cca9c824e5..5dd464f7eb61 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Rating/Save.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Rating/Save.php @@ -5,10 +5,11 @@ */ namespace Magento\Review\Controller\Adminhtml\Rating; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Review\Controller\Adminhtml\Rating as RatingController; use Magento\Framework\Controller\ResultFactory; -class Save extends RatingController +class Save extends RatingController implements HttpPostActionInterface { /** * Save rating diff --git a/app/code/Magento/Review/Controller/Product/ListAjax.php b/app/code/Magento/Review/Controller/Product/ListAjax.php index a309b5f0626a..d923814c7dce 100644 --- a/app/code/Magento/Review/Controller/Product/ListAjax.php +++ b/app/code/Magento/Review/Controller/Product/ListAjax.php @@ -11,8 +11,9 @@ use Magento\Framework\View\Result\Layout; use Magento\Review\Controller\Product as ProductController; use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -class ListAjax extends ProductController +class ListAjax extends ProductController implements HttpGetActionInterface { /** * Show list of product's reviews diff --git a/app/code/Magento/Review/Controller/Product/Post.php b/app/code/Magento/Review/Controller/Product/Post.php index be18f8fe25bb..32838eb6acbb 100644 --- a/app/code/Magento/Review/Controller/Product/Post.php +++ b/app/code/Magento/Review/Controller/Product/Post.php @@ -5,11 +5,12 @@ */ namespace Magento\Review\Controller\Product; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Review\Controller\Product as ProductController; use Magento\Framework\Controller\ResultFactory; use Magento\Review\Model\Review; -class Post extends ProductController +class Post extends ProductController implements HttpPostActionInterface { /** * Submit new review action diff --git a/app/code/Magento/Review/Model/ResourceModel/Rating.php b/app/code/Magento/Review/Model/ResourceModel/Rating.php index 3f54c17f6ff7..37a93d40b110 100644 --- a/app/code/Magento/Review/Model/ResourceModel/Rating.php +++ b/app/code/Magento/Review/Model/ResourceModel/Rating.php @@ -5,6 +5,9 @@ */ namespace Magento\Review\Model\ResourceModel; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; + /** * Rating resource model * @@ -12,6 +15,7 @@ * * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Rating extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb { @@ -34,13 +38,19 @@ class Rating extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb */ protected $_logger; + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + /** * @param \Magento\Framework\Model\ResourceModel\Db\Context $context * @param \Psr\Log\LoggerInterface $logger * @param \Magento\Framework\Module\Manager $moduleManager * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Review\Model\ResourceModel\Review\Summary $reviewSummary + * @param Review\Summary $reviewSummary * @param string $connectionName + * @param ScopeConfigInterface|null $scopeConfig */ public function __construct( \Magento\Framework\Model\ResourceModel\Db\Context $context, @@ -48,12 +58,14 @@ public function __construct( \Magento\Framework\Module\Manager $moduleManager, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Review\Model\ResourceModel\Review\Summary $reviewSummary, - $connectionName = null + $connectionName = null, + ScopeConfigInterface $scopeConfig = null ) { $this->moduleManager = $moduleManager; $this->_storeManager = $storeManager; $this->_logger = $logger; $this->_reviewSummary = $reviewSummary; + $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); parent::__construct($context, $connectionName); } @@ -178,6 +190,8 @@ protected function _afterSave(\Magento\Framework\Model\AbstractModel $object) } /** + * Process rating codes + * * @param \Magento\Framework\Model\AbstractModel $object * @return $this */ @@ -201,6 +215,8 @@ protected function processRatingCodes(\Magento\Framework\Model\AbstractModel $ob } /** + * Process rating stores + * * @param \Magento\Framework\Model\AbstractModel $object * @return $this */ @@ -224,6 +240,8 @@ protected function processRatingStores(\Magento\Framework\Model\AbstractModel $o } /** + * Delete rating data + * * @param int $ratingId * @param string $table * @param array $storeIds @@ -247,6 +265,8 @@ protected function deleteRatingData($ratingId, $table, array $storeIds) } /** + * Insert rating data + * * @param string $table * @param array $data * @return void @@ -269,6 +289,7 @@ protected function insertRatingData($table, array $data) /** * Perform actions after object delete + * * Prepare rating data for reaggregate all data for reviews * * @param \Magento\Framework\Model\AbstractModel $object @@ -277,7 +298,12 @@ protected function insertRatingData($table, array $data) protected function _afterDelete(\Magento\Framework\Model\AbstractModel $object) { parent::_afterDelete($object); - if (!$this->moduleManager->isEnabled('Magento_Review')) { + if (!$this->moduleManager->isEnabled('Magento_Review') && + !$this->scopeConfig->getValue( + \Magento\Review\Observer\PredispatchReviewObserver::XML_PATH_REVIEW_ACTIVE, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ) + ) { return $this; } $data = $this->_getEntitySummaryData($object); @@ -425,9 +451,11 @@ public function getReviewSummary($object, $onlyForCurrentStore = true) $data = $connection->fetchAll($select, [':review_id' => $object->getReviewId()]); + $currentStore = $this->_storeManager->isSingleStoreMode() ? $this->_storeManager->getStore()->getId() : null; + if ($onlyForCurrentStore) { foreach ($data as $row) { - if ($row['store_id'] == $this->_storeManager->getStore()->getId()) { + if ($row['store_id'] !== $currentStore) { $object->addData($row); } } 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 830354796907..ab264ef1b617 100644 --- a/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php +++ b/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php @@ -5,16 +5,17 @@ */ namespace Magento\Review\Model\ResourceModel\Review\Product; +use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; use Magento\Framework\DB\Select; use Magento\Framework\EntityManager\MetadataPool; -use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; /** * Review Product Collection * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @since 100.0.2 */ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection @@ -403,12 +404,20 @@ public function getAllIds($limit = null, $offset = null) public function getResultingIds() { $idsSelect = clone $this->getSelect(); - $idsSelect->reset(Select::LIMIT_COUNT); - $idsSelect->reset(Select::LIMIT_OFFSET); - $idsSelect->reset(Select::COLUMNS); - $idsSelect->reset(Select::ORDER); - $idsSelect->columns('rt.review_id'); - return $this->getConnection()->fetchCol($idsSelect); + $data = $this->getConnection() + ->fetchAll( + $idsSelect + ->reset(Select::LIMIT_COUNT) + ->reset(Select::LIMIT_OFFSET) + ->columns('rt.review_id') + ); + + return array_map( + function ($value) { + return $value['review_id']; + }, + $data + ); } /** @@ -540,6 +549,16 @@ protected function _afterLoad() return $this; } + /** + * Not add store ids to items + * + * @return $this + */ + protected function prepareStoreId() + { + return $this; + } + /** * Add store data * diff --git a/app/code/Magento/Review/Model/Review.php b/app/code/Magento/Review/Model/Review.php index c00af3fc6140..e689d4ed460a 100644 --- a/app/code/Magento/Review/Model/Review.php +++ b/app/code/Magento/Review/Model/Review.php @@ -5,6 +5,7 @@ */ namespace Magento\Review\Model; +use Magento\Framework\DataObject; use Magento\Catalog\Model\Product; use Magento\Framework\DataObject\IdentityInterface; use Magento\Review\Model\ResourceModel\Review\Product\Collection as ProductCollection; @@ -327,6 +328,9 @@ public function appendSummary($collection) $item->setRatingSummary($summary); } } + if (!$item->getRatingSummary()) { + $item->setRatingSummary(new DataObject()); + } } return $this; diff --git a/app/code/Magento/Review/Observer/CatalogBlockProductCollectionBeforeToHtmlObserver.php b/app/code/Magento/Review/Observer/CatalogBlockProductCollectionBeforeToHtmlObserver.php index 6256194cef53..f35d6eac27ea 100644 --- a/app/code/Magento/Review/Observer/CatalogBlockProductCollectionBeforeToHtmlObserver.php +++ b/app/code/Magento/Review/Observer/CatalogBlockProductCollectionBeforeToHtmlObserver.php @@ -7,6 +7,9 @@ use Magento\Framework\Event\ObserverInterface; +/** + * Review block observer. + */ class CatalogBlockProductCollectionBeforeToHtmlObserver implements ObserverInterface { /** @@ -35,7 +38,9 @@ public function execute(\Magento\Framework\Event\Observer $observer) { $productCollection = $observer->getEvent()->getCollection(); if ($productCollection instanceof \Magento\Framework\Data\Collection) { - $productCollection->load(); + if (!$productCollection->isLoaded()) { + $productCollection->load(); + } $this->_reviewFactory->create()->appendSummary($productCollection); } diff --git a/app/code/Magento/Review/Observer/PredispatchReviewObserver.php b/app/code/Magento/Review/Observer/PredispatchReviewObserver.php new file mode 100644 index 000000000000..bdca0f5ecb1e --- /dev/null +++ b/app/code/Magento/Review/Observer/PredispatchReviewObserver.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); + +namespace Magento\Review\Observer; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\UrlInterface; +use Magento\Review\Block\Product\ReviewRenderer; +use Magento\Store\Model\ScopeInterface; + +/** + * Class PredispatchReviewObserver + */ +class PredispatchReviewObserver implements ObserverInterface +{ + /** + * Configuration path to review active setting + */ + const XML_PATH_REVIEW_ACTIVE = 'catalog/review/active'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var UrlInterface + */ + private $url; + + /** + * PredispatchReviewObserver constructor. + * + * @param ScopeConfigInterface $scopeConfig + * @param UrlInterface $url + */ + public function __construct( + ScopeConfigInterface $scopeConfig, + UrlInterface $url + ) { + $this->scopeConfig = $scopeConfig; + $this->url = $url; + } + /** + * Redirect review routes to 404 when review module is disabled. + * + * @param Observer $observer + */ + public function execute(Observer $observer) + { + if (!$this->scopeConfig->getValue( + self::XML_PATH_REVIEW_ACTIVE, + ScopeInterface::SCOPE_STORE + ) + ) { + $defaultNoRouteUrl = $this->scopeConfig->getValue( + 'web/default/no_route', + ScopeInterface::SCOPE_STORE + ); + $redirectUrl = $this->url->getUrl($defaultNoRouteUrl); + $observer->getControllerAction() + ->getResponse() + ->setRedirect($redirectUrl); + } + } +} diff --git a/app/code/Magento/Review/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Review/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 000000000000..89882707f5eb --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminMenuUserContentReviews"> + <data key="pageTitle">Reviews</data> + <data key="title">Reviews</data> + <data key="dataUiId">magento-review-catalog-reviews-ratings-reviews-all</data> + </entity> + <entity name="AdminMenuReportsReviewsByCustomers"> + <data key="pageTitle">Customer Reviews Report</data> + <data key="title">By Customers</data> + <data key="dataUiId">magento-review-report-review-customer</data> + </entity> + <entity name="AdminMenuReportsReviewsByProducts"> + <data key="pageTitle">Product Reviews Report</data> + <data key="title">By Products</data> + <data key="dataUiId">magento-review-report-review-product</data> + </entity> + <entity name="AdminMenuAttributesRating"> + <data key="pageTitle">Ratings</data> + <data key="title">Rating</data> + <data key="dataUiId">magento-review-catalog-reviews-ratings-ratings</data> + </entity> +</entities> diff --git a/app/code/Magento/Review/Test/Mftf/Test/AdminMarketingReviewsNavigateMenuTest.xml b/app/code/Magento/Review/Test/Mftf/Test/AdminMarketingReviewsNavigateMenuTest.xml new file mode 100644 index 000000000000..fade220d2210 --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/Test/AdminMarketingReviewsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMarketingReviewsNavigateMenuTest"> + <annotations> + <features value="Review"/> + <stories value="Menu Navigation"/> + <title value="Admin marketing reviews navigate menu test"/> + <description value="Admin should be able to navigate to Marketing > Reviews"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14196"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsViewsPage"> + <argument name="menuUiId" value="{{AdminMenuMarketing.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuUserContentReviews.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuUserContentReviews.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByCustomersNavigateMenuTest.xml b/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByCustomersNavigateMenuTest.xml new file mode 100644 index 000000000000..58492424e76f --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByCustomersNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminReportsByCustomersNavigateMenuTest"> + <annotations> + <features value="Review"/> + <stories value="Menu Navigation"/> + <title value="Admin reports by customers navigate menu test"/> + <description value="Admin should be able to navigate to Reports > By Customers"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14197"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsByCustomersPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsReviewsByCustomers.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsReviewsByCustomers.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByProductsNavigateMenuTest.xml b/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByProductsNavigateMenuTest.xml new file mode 100644 index 000000000000..e848aa4f2202 --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByProductsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminReportsByProductsNavigateMenuTest"> + <annotations> + <features value="Review"/> + <stories value="Menu Navigation"/> + <title value="Admin reports by products navigate menu test"/> + <description value="Admin should be able to navigate to Reports > By Products"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14198"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsByProductsPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsReviewsByProducts.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsReviewsByProducts.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Review/Test/Mftf/Test/AdminStoresRatingNavigateMenuTest.xml b/app/code/Magento/Review/Test/Mftf/Test/AdminStoresRatingNavigateMenuTest.xml new file mode 100644 index 000000000000..511ed5439dc3 --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/Test/AdminStoresRatingNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminStoresRatingNavigateMenuTest"> + <annotations> + <features value="Review"/> + <stories value="Menu Navigation"/> + <title value="Admin stores rating navigate menu test"/> + <description value="Admin should be able to navigate to Stores > Rating"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14199"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToStoresRatingPage"> + <argument name="menuUiId" value="{{AdminMenuStores.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuAttributesRating.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuAttributesRating.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Review/Test/Unit/Observer/PredispatchReviewObserverTest.php b/app/code/Magento/Review/Test/Unit/Observer/PredispatchReviewObserverTest.php new file mode 100644 index 000000000000..cb01e55e4f49 --- /dev/null +++ b/app/code/Magento/Review/Test/Unit/Observer/PredispatchReviewObserverTest.php @@ -0,0 +1,147 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); + +namespace Magento\Review\Test\Unit\Observer; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Response\RedirectInterface; +use Magento\Framework\App\ResponseInterface; +use Magento\Framework\Event\Observer; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\UrlInterface; +use Magento\Review\Observer\PredispatchReviewObserver; +use Magento\Store\Model\ScopeInterface; +use PHPUnit\Framework\TestCase; + +/** + * Test class for \Magento\Review\Observer\PredispatchReviewObserver + */ +class PredispatchReviewObserverTest extends TestCase +{ + /** + * @var Observer|\PHPUnit_Framework_MockObject_MockObject + */ + private $mockObject; + + /** + * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $configMock; + + /** + * @var UrlInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $urlMock; + + /** + * @var \Magento\Framework\App\Response\RedirectInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $redirectMock; + + /** + * @var ResponseInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $responseMock; + + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @inheritdoc + */ + protected function setUp() : void + { + $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->urlMock = $this->getMockBuilder(UrlInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->responseMock = $this->getMockBuilder(ResponseInterface::class) + ->disableOriginalConstructor() + ->setMethods(['setRedirect']) + ->getMockForAbstractClass(); + $this->redirectMock = $this->getMockBuilder(RedirectInterface::class) + ->getMock(); + $this->objectManager = new ObjectManager($this); + $this->mockObject = $this->objectManager->getObject( + PredispatchReviewObserver::class, + [ + 'scopeConfig' => $this->configMock, + 'url' => $this->urlMock + ] + ); + } + + /** + * Test with enabled review active config. + */ + public function testReviewEnabled() : void + { + $observerMock = $this->getMockBuilder(Observer::class) + ->disableOriginalConstructor() + ->setMethods(['getResponse', 'getData', 'setRedirect']) + ->getMockForAbstractClass(); + + $this->configMock->method('getValue') + ->with(PredispatchReviewObserver::XML_PATH_REVIEW_ACTIVE, ScopeInterface::SCOPE_STORE) + ->willReturn(true); + $observerMock->expects($this->never()) + ->method('getData') + ->with('controller_action') + ->willReturnSelf(); + + $observerMock->expects($this->never()) + ->method('getResponse') + ->willReturnSelf(); + + $this->assertNull($this->mockObject->execute($observerMock)); + } + + /** + * Test with disabled review active config. + */ + public function testReviewDisabled() : void + { + $observerMock = $this->getMockBuilder(Observer::class) + ->disableOriginalConstructor() + ->setMethods(['getControllerAction', 'getResponse']) + ->getMockForAbstractClass(); + + $this->configMock->expects($this->at(0)) + ->method('getValue') + ->with(PredispatchReviewObserver::XML_PATH_REVIEW_ACTIVE, ScopeInterface::SCOPE_STORE) + ->willReturn(false); + + $expectedRedirectUrl = 'https://test.com/index'; + + $this->configMock->expects($this->at(1)) + ->method('getValue') + ->with('web/default/no_route', ScopeInterface::SCOPE_STORE) + ->willReturn($expectedRedirectUrl); + + $this->urlMock->expects($this->once()) + ->method('getUrl') + ->willReturn($expectedRedirectUrl); + + $observerMock->expects($this->once()) + ->method('getControllerAction') + ->willReturnSelf(); + + $observerMock->expects($this->once()) + ->method('getResponse') + ->willReturn($this->responseMock); + + $this->responseMock->expects($this->once()) + ->method('setRedirect') + ->with($expectedRedirectUrl); + + $this->assertNull($this->mockObject->execute($observerMock)); + } +} diff --git a/app/code/Magento/Review/etc/acl.xml b/app/code/Magento/Review/etc/acl.xml index 397cc1cce61d..46fdb20dee4a 100644 --- a/app/code/Magento/Review/etc/acl.xml +++ b/app/code/Magento/Review/etc/acl.xml @@ -16,8 +16,8 @@ </resource> <resource id="Magento_Backend::marketing"> <resource id="Magento_Backend::marketing_user_content"> - <resource id="Magento_Review::reviews_all" title="Reviews" translate="title" sortOrder="10"/> - <resource id="Magento_Review::pending" title="Reviews" translate="title" sortOrder="20"/> + <resource id="Magento_Review::pending" title="Pending Reviews" translate="title" sortOrder="20"/> + <resource id="Magento_Review::reviews_all" title="All Reviews" translate="title" sortOrder="10"/> </resource> </resource> </resource> diff --git a/app/code/Magento/Review/etc/adminhtml/menu.xml b/app/code/Magento/Review/etc/adminhtml/menu.xml index e3532483f88a..737632947192 100644 --- a/app/code/Magento/Review/etc/adminhtml/menu.xml +++ b/app/code/Magento/Review/etc/adminhtml/menu.xml @@ -8,7 +8,8 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc/menu.xsd"> <menu> <add id="Magento_Review::catalog_reviews_ratings_ratings" title="Rating" translate="title" module="Magento_Review" sortOrder="60" parent="Magento_Backend::stores_attributes" action="review/rating/" resource="Magento_Review::ratings"/> - <add id="Magento_Review::catalog_reviews_ratings_reviews_all" title="Reviews" translate="title" module="Magento_Review" parent="Magento_Backend::marketing_user_content" sortOrder="10" action="review/product/index" resource="Magento_Review::reviews_all"/> + <add id="Magento_Review::catalog_reviews_ratings_pending" title="Pending Reviews" translate="title" module="Magento_Review" parent="Magento_Backend::marketing_user_content" sortOrder="20" action="review/product/pending" resource="Magento_Review::pending"/> + <add id="Magento_Review::catalog_reviews_ratings_reviews_all" title="All Reviews" translate="title" module="Magento_Review" parent="Magento_Backend::marketing_user_content" sortOrder="10" action="review/product/index" resource="Magento_Review::reviews_all"/> <add id="Magento_Review::report_review" title="Reviews" translate="title" module="Magento_Reports" sortOrder="20" parent="Magento_Reports::report" resource="Magento_Reports::review"/> <add id="Magento_Review::report_review_customer" title="By Customers" translate="title" sortOrder="10" module="Magento_Review" parent="Magento_Review::report_review" action="reports/report_review/customer" resource="Magento_Reports::review_customer"/> <add id="Magento_Review::report_review_product" title="By Products" translate="title" sortOrder="20" module="Magento_Review" parent="Magento_Review::report_review" action="reports/report_review/product" resource="Magento_Reports::review_product"/> diff --git a/app/code/Magento/Review/etc/adminhtml/system.xml b/app/code/Magento/Review/etc/adminhtml/system.xml index c0574e949178..a24ed29dc2c2 100644 --- a/app/code/Magento/Review/etc/adminhtml/system.xml +++ b/app/code/Magento/Review/etc/adminhtml/system.xml @@ -10,7 +10,11 @@ <section id="catalog"> <group id="review" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="0"> <label>Product Reviews</label> - <field id="allow_guest" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> + <field id="active" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <label>Enabled</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + </field> + <field id="allow_guest" translate="label" type="select" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Allow Guests to Write Reviews</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> diff --git a/app/code/Magento/Review/etc/config.xml b/app/code/Magento/Review/etc/config.xml index 78dc87960f09..9fd9443be67e 100644 --- a/app/code/Magento/Review/etc/config.xml +++ b/app/code/Magento/Review/etc/config.xml @@ -9,6 +9,7 @@ <default> <catalog> <review> + <active>1</active> <allow_guest>1</allow_guest> </review> </catalog> diff --git a/app/code/Magento/Review/etc/db_schema.xml b/app/code/Magento/Review/etc/db_schema.xml index 1a2ff588180c..d1090d413384 100644 --- a/app/code/Magento/Review/etc/db_schema.xml +++ b/app/code/Magento/Review/etc/db_schema.xml @@ -11,7 +11,7 @@ <column xsi:type="smallint" name="entity_id" padding="5" unsigned="true" nullable="false" identity="true" comment="Review entity id"/> <column xsi:type="varchar" name="entity_code" nullable="false" length="32" comment="Review entity code"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> </table> @@ -19,7 +19,7 @@ <column xsi:type="smallint" name="status_id" padding="5" unsigned="true" nullable="false" identity="true" comment="Status id"/> <column xsi:type="varchar" name="status_code" nullable="false" length="32" comment="Status code"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="status_id"/> </constraint> </table> @@ -34,20 +34,20 @@ default="0" comment="Product id"/> <column xsi:type="smallint" name="status_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Status code"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="review_id"/> </constraint> - <constraint xsi:type="foreign" name="REVIEW_ENTITY_ID_REVIEW_ENTITY_ENTITY_ID" table="review" column="entity_id" + <constraint xsi:type="foreign" referenceId="REVIEW_ENTITY_ID_REVIEW_ENTITY_ENTITY_ID" table="review" column="entity_id" referenceTable="review_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="REVIEW_STATUS_ID_REVIEW_STATUS_STATUS_ID" table="review" column="status_id" + <constraint xsi:type="foreign" referenceId="REVIEW_STATUS_ID_REVIEW_STATUS_STATUS_ID" table="review" column="status_id" referenceTable="review_status" referenceColumn="status_id" onDelete="NO ACTION"/> - <index name="REVIEW_ENTITY_ID" indexType="btree"> + <index referenceId="REVIEW_ENTITY_ID" indexType="btree"> <column name="entity_id"/> </index> - <index name="REVIEW_STATUS_ID" indexType="btree"> + <index referenceId="REVIEW_STATUS_ID" indexType="btree"> <column name="status_id"/> </index> - <index name="REVIEW_ENTITY_PK_VALUE" indexType="btree"> + <index referenceId="REVIEW_ENTITY_PK_VALUE" indexType="btree"> <column name="entity_pk_value"/> </index> </table> @@ -63,23 +63,23 @@ <column xsi:type="varchar" name="nickname" nullable="false" length="128" comment="User nickname"/> <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="true" identity="false" comment="Customer Id"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="detail_id"/> </constraint> - <constraint xsi:type="foreign" name="REVIEW_DETAIL_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID" table="review_detail" + <constraint xsi:type="foreign" referenceId="REVIEW_DETAIL_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID" table="review_detail" column="customer_id" referenceTable="customer_entity" referenceColumn="entity_id" onDelete="SET NULL"/> - <constraint xsi:type="foreign" name="REVIEW_DETAIL_REVIEW_ID_REVIEW_REVIEW_ID" table="review_detail" + <constraint xsi:type="foreign" referenceId="REVIEW_DETAIL_REVIEW_ID_REVIEW_REVIEW_ID" table="review_detail" column="review_id" referenceTable="review" referenceColumn="review_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="REVIEW_DETAIL_STORE_ID_STORE_STORE_ID" table="review_detail" + <constraint xsi:type="foreign" referenceId="REVIEW_DETAIL_STORE_ID_STORE_STORE_ID" table="review_detail" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="SET NULL"/> - <index name="REVIEW_DETAIL_REVIEW_ID" indexType="btree"> + <index referenceId="REVIEW_DETAIL_REVIEW_ID" indexType="btree"> <column name="review_id"/> </index> - <index name="REVIEW_DETAIL_STORE_ID" indexType="btree"> + <index referenceId="REVIEW_DETAIL_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="REVIEW_DETAIL_CUSTOMER_ID" indexType="btree"> + <index referenceId="REVIEW_DETAIL_CUSTOMER_ID" indexType="btree"> <column name="customer_id"/> </index> </table> @@ -95,64 +95,64 @@ <column xsi:type="smallint" name="rating_summary" padding="6" unsigned="false" nullable="false" identity="false" default="0" comment="Summarized rating"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Store id"/> - <constraint xsi:type="primary" name="PRIMARY"> + default="0" comment="Store ID"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="primary_id"/> </constraint> - <constraint xsi:type="foreign" name="REVIEW_ENTITY_SUMMARY_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="REVIEW_ENTITY_SUMMARY_STORE_ID_STORE_STORE_ID" table="review_entity_summary" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <index name="REVIEW_ENTITY_SUMMARY_STORE_ID" indexType="btree"> + <index referenceId="REVIEW_ENTITY_SUMMARY_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> <table name="review_store" resource="default" engine="innodb" comment="Review Store"> <column xsi:type="bigint" name="review_id" padding="20" unsigned="true" nullable="false" identity="false" - comment="Review Id"/> + comment="Review ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" - comment="Store Id"/> - <constraint xsi:type="primary" name="PRIMARY"> + comment="Store ID"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="review_id"/> <column name="store_id"/> </constraint> - <constraint xsi:type="foreign" name="REVIEW_STORE_REVIEW_ID_REVIEW_REVIEW_ID" table="review_store" + <constraint xsi:type="foreign" referenceId="REVIEW_STORE_REVIEW_ID_REVIEW_REVIEW_ID" table="review_store" column="review_id" referenceTable="review" referenceColumn="review_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="REVIEW_STORE_STORE_ID_STORE_STORE_ID" table="review_store" + <constraint xsi:type="foreign" referenceId="REVIEW_STORE_STORE_ID_STORE_STORE_ID" table="review_store" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <index name="REVIEW_STORE_STORE_ID" indexType="btree"> + <index referenceId="REVIEW_STORE_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> <table name="rating_entity" resource="default" engine="innodb" comment="Rating entities"> <column xsi:type="smallint" name="entity_id" padding="5" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="varchar" name="entity_code" nullable="false" length="64" comment="Entity Code"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> - <constraint xsi:type="unique" name="RATING_ENTITY_ENTITY_CODE"> + <constraint xsi:type="unique" referenceId="RATING_ENTITY_ENTITY_CODE"> <column name="entity_code"/> </constraint> </table> <table name="rating" resource="default" engine="innodb" comment="Ratings"> <column xsi:type="smallint" name="rating_id" padding="5" unsigned="true" nullable="false" identity="true" - comment="Rating Id"/> + comment="Rating ID"/> <column xsi:type="smallint" name="entity_id" padding="5" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Id"/> + default="0" comment="Entity ID"/> <column xsi:type="varchar" name="rating_code" nullable="false" length="64" comment="Rating Code"/> <column xsi:type="smallint" name="position" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Rating Position On Storefront"/> <column xsi:type="smallint" name="is_active" padding="6" unsigned="false" nullable="false" identity="false" default="1" comment="Rating is active."/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rating_id"/> </constraint> - <constraint xsi:type="foreign" name="RATING_ENTITY_ID_RATING_ENTITY_ENTITY_ID" table="rating" column="entity_id" + <constraint xsi:type="foreign" referenceId="RATING_ENTITY_ID_RATING_ENTITY_ENTITY_ID" table="rating" column="entity_id" referenceTable="rating_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="RATING_RATING_CODE"> + <constraint xsi:type="unique" referenceId="RATING_RATING_CODE"> <column name="rating_code"/> </constraint> - <index name="RATING_ENTITY_ID" indexType="btree"> + <index referenceId="RATING_ENTITY_ID" indexType="btree"> <column name="entity_id"/> </index> </table> @@ -166,12 +166,12 @@ default="0" comment="Rating Option Value"/> <column xsi:type="smallint" name="position" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Ration option position on Storefront"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="option_id"/> </constraint> - <constraint xsi:type="foreign" name="RATING_OPTION_RATING_ID_RATING_RATING_ID" table="rating_option" + <constraint xsi:type="foreign" referenceId="RATING_OPTION_RATING_ID_RATING_RATING_ID" table="rating_option" column="rating_id" referenceTable="rating" referenceColumn="rating_id" onDelete="CASCADE"/> - <index name="RATING_OPTION_RATING_ID" indexType="btree"> + <index referenceId="RATING_OPTION_RATING_ID" indexType="btree"> <column name="rating_id"/> </index> </table> @@ -195,15 +195,15 @@ default="0" comment="Percent amount"/> <column xsi:type="smallint" name="value" padding="6" unsigned="false" nullable="false" identity="false" default="0" comment="Vote option value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="vote_id"/> </constraint> - <constraint xsi:type="foreign" name="RATING_OPTION_VOTE_OPTION_ID_RATING_OPTION_OPTION_ID" + <constraint xsi:type="foreign" referenceId="RATING_OPTION_VOTE_OPTION_ID_RATING_OPTION_OPTION_ID" table="rating_option_vote" column="option_id" referenceTable="rating_option" referenceColumn="option_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="RATING_OPTION_VOTE_REVIEW_ID_REVIEW_REVIEW_ID" table="rating_option_vote" + <constraint xsi:type="foreign" referenceId="RATING_OPTION_VOTE_REVIEW_ID_REVIEW_REVIEW_ID" table="rating_option_vote" column="review_id" referenceTable="review" referenceColumn="review_id" onDelete="CASCADE"/> - <index name="RATING_OPTION_VOTE_OPTION_ID" indexType="btree"> + <index referenceId="RATING_OPTION_VOTE_OPTION_ID" indexType="btree"> <column name="option_id"/> </index> </table> @@ -224,19 +224,19 @@ identity="false" default="0" comment="Vote percent approved by admin"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Store Id"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="primary_id"/> </constraint> - <constraint xsi:type="foreign" name="RATING_OPTION_VOTE_AGGREGATED_RATING_ID_RATING_RATING_ID" + <constraint xsi:type="foreign" referenceId="RATING_OPTION_VOTE_AGGREGATED_RATING_ID_RATING_RATING_ID" table="rating_option_vote_aggregated" column="rating_id" referenceTable="rating" referenceColumn="rating_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="RATING_OPTION_VOTE_AGGREGATED_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="RATING_OPTION_VOTE_AGGREGATED_STORE_ID_STORE_STORE_ID" table="rating_option_vote_aggregated" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <index name="RATING_OPTION_VOTE_AGGREGATED_RATING_ID" indexType="btree"> + <index referenceId="RATING_OPTION_VOTE_AGGREGATED_RATING_ID" indexType="btree"> <column name="rating_id"/> </index> - <index name="RATING_OPTION_VOTE_AGGREGATED_STORE_ID" indexType="btree"> + <index referenceId="RATING_OPTION_VOTE_AGGREGATED_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -245,15 +245,15 @@ default="0" comment="Rating id"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Store id"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rating_id"/> <column name="store_id"/> </constraint> - <constraint xsi:type="foreign" name="RATING_STORE_STORE_ID_STORE_STORE_ID" table="rating_store" + <constraint xsi:type="foreign" referenceId="RATING_STORE_STORE_ID_STORE_STORE_ID" table="rating_store" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="RATING_STORE_RATING_ID_RATING_RATING_ID" table="rating_store" + <constraint xsi:type="foreign" referenceId="RATING_STORE_RATING_ID_RATING_RATING_ID" table="rating_store" column="rating_id" referenceTable="rating" referenceColumn="rating_id" onDelete="CASCADE"/> - <index name="RATING_STORE_STORE_ID" indexType="btree"> + <index referenceId="RATING_STORE_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -263,15 +263,15 @@ <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Store Id"/> <column xsi:type="varchar" name="value" nullable="false" length="255" comment="Rating Label"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rating_id"/> <column name="store_id"/> </constraint> - <constraint xsi:type="foreign" name="RATING_TITLE_RATING_ID_RATING_RATING_ID" table="rating_title" + <constraint xsi:type="foreign" referenceId="RATING_TITLE_RATING_ID_RATING_RATING_ID" table="rating_title" column="rating_id" referenceTable="rating" referenceColumn="rating_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="RATING_TITLE_STORE_ID_STORE_STORE_ID" table="rating_title" + <constraint xsi:type="foreign" referenceId="RATING_TITLE_STORE_ID_STORE_STORE_ID" table="rating_title" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <index name="RATING_TITLE_STORE_ID" indexType="btree"> + <index referenceId="RATING_TITLE_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> diff --git a/app/code/Magento/Review/etc/frontend/events.xml b/app/code/Magento/Review/etc/frontend/events.xml index bc94277d6970..8e883ce328a2 100644 --- a/app/code/Magento/Review/etc/frontend/events.xml +++ b/app/code/Magento/Review/etc/frontend/events.xml @@ -12,4 +12,7 @@ <event name="catalog_block_product_list_collection"> <observer name="review" instance="Magento\Review\Observer\CatalogBlockProductCollectionBeforeToHtmlObserver" shared="false" /> </event> + <event name="controller_action_predispatch_review"> + <observer name="catalog_review_enabled" instance="Magento\Review\Observer\PredispatchReviewObserver" /> + </event> </config> diff --git a/app/code/Magento/Review/view/adminhtml/layout/rating_block.xml b/app/code/Magento/Review/view/adminhtml/layout/rating_block.xml index b439bcb1c271..414cec14c3be 100644 --- a/app/code/Magento/Review/view/adminhtml/layout/rating_block.xml +++ b/app/code/Magento/Review/view/adminhtml/layout/rating_block.xml @@ -11,7 +11,7 @@ <block class="Magento\Backend\Block\Widget\Grid" name="adminhtml.rating.grid" as="grid"> <arguments> <argument name="id" xsi:type="string">ratingsGrid</argument> - <argument name="dataSource" xsi:type="object">Magento\Review\Model\ResourceModel\Rating\Grid\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Review\Model\ResourceModel\Rating\Grid\Collection</argument> <argument name="default_sort" xsi:type="string">rating_code</argument> <argument name="default_dir" xsi:type="string">ASC</argument> <argument name="save_parameters_in_session" xsi:type="string">1</argument> diff --git a/app/code/Magento/Review/view/frontend/layout/catalog_product_view.xml b/app/code/Magento/Review/view/frontend/layout/catalog_product_view.xml index d7c5c19d4d81..a6b46f8f25a7 100644 --- a/app/code/Magento/Review/view/frontend/layout/catalog_product_view.xml +++ b/app/code/Magento/Review/view/frontend/layout/catalog_product_view.xml @@ -9,7 +9,7 @@ <update handle="review_product_form_component"/> <body> <referenceContainer name="content"> - <block class="Magento\Cookie\Block\RequireCookie" name="require-cookie" template="Magento_Cookie::require_cookie.phtml"> + <block class="Magento\Cookie\Block\RequireCookie" name="require-cookie" template="Magento_Cookie::require_cookie.phtml" ifconfig="catalog/review/active"> <arguments> <argument name="triggers" xsi:type="array"> <item name="submitReviewButton" xsi:type="string">.review .action.submit</item> @@ -18,8 +18,11 @@ </block> </referenceContainer> <referenceBlock name="product.info.details"> - <block class="Magento\Review\Block\Product\Review" name="reviews.tab" as="reviews" template="Magento_Review::review.phtml" group="detailed_info"> - <block class="Magento\Review\Block\Form" name="product.review.form" as="review_form"> + <block class="Magento\Review\Block\Product\Review" name="reviews.tab" as="reviews" template="Magento_Review::review.phtml" group="detailed_info" ifconfig="catalog/review/active"> + <arguments> + <argument name="sort_order" xsi:type="string">30</argument> + </arguments> + <block class="Magento\Review\Block\Form" name="product.review.form" as="review_form" ifconfig="catalog/review/active"> <container name="product.review.form.fields.before" as="form_fields_before" label="Review Form Fields Before"/> </block> </block> diff --git a/app/code/Magento/Review/view/frontend/layout/checkout_cart_configure.xml b/app/code/Magento/Review/view/frontend/layout/checkout_cart_configure.xml index 0a7ddd8b8903..8a853cdd2e40 100644 --- a/app/code/Magento/Review/view/frontend/layout/checkout_cart_configure.xml +++ b/app/code/Magento/Review/view/frontend/layout/checkout_cart_configure.xml @@ -9,7 +9,7 @@ <update handle="catalog_product_view"/> <body> <referenceBlock name="reviews.tab"> - <block class="Magento\Review\Block\Form\Configure" name="product.review.form" as="review_form"> + <block class="Magento\Review\Block\Form\Configure" name="product.review.form" as="review_form" ifconfig="catalog/review/active"> <arguments> <argument name="jsLayout" xsi:type="array"> <item name="components" xsi:type="array"> diff --git a/app/code/Magento/Review/view/frontend/layout/customer_account.xml b/app/code/Magento/Review/view/frontend/layout/customer_account.xml index 54d171cbf132..9f759dba4178 100644 --- a/app/code/Magento/Review/view/frontend/layout/customer_account.xml +++ b/app/code/Magento/Review/view/frontend/layout/customer_account.xml @@ -8,7 +8,7 @@ <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceBlock name="customer_account_navigation"> - <block class="Magento\Customer\Block\Account\SortLinkInterface" name="customer-account-navigation-product-reviews-link"> + <block class="Magento\Customer\Block\Account\SortLinkInterface" name="customer-account-navigation-product-reviews-link" ifconfig="catalog/review/active"> <arguments> <argument name="path" xsi:type="string">review/customer</argument> <argument name="label" xsi:type="string" translate="true">My Product Reviews</argument> diff --git a/app/code/Magento/Review/view/frontend/layout/customer_account_index.xml b/app/code/Magento/Review/view/frontend/layout/customer_account_index.xml index 73174f0570e2..2e898a539a95 100644 --- a/app/code/Magento/Review/view/frontend/layout/customer_account_index.xml +++ b/app/code/Magento/Review/view/frontend/layout/customer_account_index.xml @@ -8,7 +8,7 @@ <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceContainer name="content"> - <block class="Magento\Review\Block\Customer\Recent" name="customer_account_dashboard_info1" template="Magento_Review::customer/recent.phtml" after="customer_account_dashboard_address" cacheable="false"/> + <block class="Magento\Review\Block\Customer\Recent" name="customer_account_dashboard_info1" template="Magento_Review::customer/recent.phtml" after="customer_account_dashboard_address" cacheable="false" ifconfig="catalog/review/active"/> </referenceContainer> </body> </page> diff --git a/app/code/Magento/Review/view/frontend/layout/review_customer_index.xml b/app/code/Magento/Review/view/frontend/layout/review_customer_index.xml index 2857e859aa06..b5f756296331 100644 --- a/app/code/Magento/Review/view/frontend/layout/review_customer_index.xml +++ b/app/code/Magento/Review/view/frontend/layout/review_customer_index.xml @@ -9,7 +9,7 @@ <update handle="customer_account"/> <body> <referenceContainer name="content"> - <block class="Magento\Review\Block\Customer\ListCustomer" name="review_customer_list" template="Magento_Review::customer/list.phtml" cacheable="false"/> + <block class="Magento\Review\Block\Customer\ListCustomer" name="review_customer_list" template="Magento_Review::customer/list.phtml" cacheable="false" ifconfig="catalog/review/active"/> </referenceContainer> </body> </page> diff --git a/app/code/Magento/Review/view/frontend/layout/review_customer_view.xml b/app/code/Magento/Review/view/frontend/layout/review_customer_view.xml index d51c89a1abe1..d3adbd7950cf 100644 --- a/app/code/Magento/Review/view/frontend/layout/review_customer_view.xml +++ b/app/code/Magento/Review/view/frontend/layout/review_customer_view.xml @@ -9,7 +9,7 @@ <update handle="customer_account"/> <body> <referenceContainer name="content"> - <block class="Magento\Review\Block\Customer\View" name="customers_review" cacheable="false"/> + <block class="Magento\Review\Block\Customer\View" name="customers_review" cacheable="false" ifconfig="catalog/review/active"/> </referenceContainer> </body> </page> diff --git a/app/code/Magento/Review/view/frontend/layout/review_product_list.xml b/app/code/Magento/Review/view/frontend/layout/review_product_list.xml index c83cfe95d796..8c5c1297cdda 100644 --- a/app/code/Magento/Review/view/frontend/layout/review_product_list.xml +++ b/app/code/Magento/Review/view/frontend/layout/review_product_list.xml @@ -9,15 +9,15 @@ <update handle="catalog_product_view"/> <body> <referenceContainer name="product.info.main"> - <block class="Magento\Review\Block\Product\View\Other" name="product.info.other" as="other" template="Magento_Review::product/view/other.phtml" before="product.info.addto"/> + <block class="Magento\Review\Block\Product\View\Other" name="product.info.other" as="other" template="Magento_Review::product/view/other.phtml" before="product.info.addto" ifconfig="catalog/review/active"/> </referenceContainer> <referenceContainer name="content"> <container name="product.info.details" htmlTag="div" htmlClass="product info detailed" after="product.info.media"> - <block class="Magento\Review\Block\Form" name="product.review.form" as="review_form"> + <block class="Magento\Review\Block\Form" name="product.review.form" as="review_form" ifconfig="catalog/review/active"> <container name="product.review.form.fields.before" as="form_fields_before" label="Review Form Fields Before" htmlTag="div" htmlClass="rewards"/> </block> - <block class="Magento\Review\Block\Product\View\ListView" name="product.info.product_additional_data" as="product_additional_data" template="Magento_Review::product/view/list.phtml"/> - <block class="Magento\Theme\Block\Html\Pager" name="product_review_list.toolbar"/> + <block class="Magento\Review\Block\Product\View\ListView" name="product.info.product_additional_data" as="product_additional_data" template="Magento_Review::product/view/list.phtml" ifconfig="catalog/review/active"/> + <block class="Magento\Theme\Block\Html\Pager" name="product_review_list.toolbar" ifconfig="catalog/review/active"/> </container> </referenceContainer> </body> diff --git a/app/code/Magento/Review/view/frontend/layout/review_product_listajax.xml b/app/code/Magento/Review/view/frontend/layout/review_product_listajax.xml index af8d2dc2f506..36fa71ea5125 100644 --- a/app/code/Magento/Review/view/frontend/layout/review_product_listajax.xml +++ b/app/code/Magento/Review/view/frontend/layout/review_product_listajax.xml @@ -7,8 +7,8 @@ --> <layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd"> <container name="root"> - <block class="Magento\Review\Block\Product\View\ListView" name="product.info.product_additional_data" as="product_additional_data" template="Magento_Review::product/view/list.phtml" /> - <block class="Magento\Theme\Block\Html\Pager" name="product_review_list.toolbar"> + <block class="Magento\Review\Block\Product\View\ListView" name="product.info.product_additional_data" as="product_additional_data" template="Magento_Review::product/view/list.phtml" ifconfig="catalog/review/active"/> + <block class="Magento\Theme\Block\Html\Pager" name="product_review_list.toolbar" ifconfig="catalog/review/active"> <arguments> <argument name="show_per_page" xsi:type="boolean">false</argument> <argument name="show_amounts" xsi:type="boolean">false</argument> diff --git a/app/code/Magento/Review/view/frontend/layout/review_product_view.xml b/app/code/Magento/Review/view/frontend/layout/review_product_view.xml index b70aec3f00b6..3bfc98cad973 100644 --- a/app/code/Magento/Review/view/frontend/layout/review_product_view.xml +++ b/app/code/Magento/Review/view/frontend/layout/review_product_view.xml @@ -8,7 +8,7 @@ <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="1column" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceContainer name="content"> - <block class="Magento\Review\Block\View" name="review_view"/> + <block class="Magento\Review\Block\View" name="review_view" ifconfig="catalog/review/active"/> </referenceContainer> </body> </page> diff --git a/app/code/Magento/Review/view/frontend/layout/wishlist_index_configure.xml b/app/code/Magento/Review/view/frontend/layout/wishlist_index_configure.xml index 0a7ddd8b8903..8a853cdd2e40 100644 --- a/app/code/Magento/Review/view/frontend/layout/wishlist_index_configure.xml +++ b/app/code/Magento/Review/view/frontend/layout/wishlist_index_configure.xml @@ -9,7 +9,7 @@ <update handle="catalog_product_view"/> <body> <referenceBlock name="reviews.tab"> - <block class="Magento\Review\Block\Form\Configure" name="product.review.form" as="review_form"> + <block class="Magento\Review\Block\Form\Configure" name="product.review.form" as="review_form" ifconfig="catalog/review/active"> <arguments> <argument name="jsLayout" xsi:type="array"> <item name="components" xsi:type="array"> diff --git a/app/code/Magento/Review/view/frontend/templates/helper/summary.phtml b/app/code/Magento/Review/view/frontend/templates/helper/summary.phtml index da689960dfe5..23cb6699aeb2 100644 --- a/app/code/Magento/Review/view/frontend/templates/helper/summary.phtml +++ b/app/code/Magento/Review/view/frontend/templates/helper/summary.phtml @@ -11,7 +11,7 @@ $url = $block->getReviewsUrl() . '#reviews'; $urlForm = $block->getReviewsUrl() . '#review-form'; ?> -<?php if ($block->getReviewsCount()): ?> +<?php if ($block->isReviewEnabled() && $block->getReviewsCount()): ?> <?php $rating = $block->getRatingSummary(); ?> <div class="product-reviews-summary<?= !$rating ? ' no-rating' : '' ?>" itemprop="aggregateRating" itemscope itemtype="http://schema.org/AggregateRating"> <?php if ($rating):?> @@ -35,7 +35,7 @@ $urlForm = $block->getReviewsUrl() . '#review-form'; <a class="action add" href="<?= $block->escapeUrl($urlForm) ?>"><?= $block->escapeHtml(__('Add Your Review')) ?></a> </div> </div> -<?php elseif ($block->getDisplayIfEmpty()): ?> +<?php elseif ($block->isReviewEnabled() && $block->getDisplayIfEmpty()): ?> <div class="product-reviews-summary empty"> <div class="reviews-actions"> <a class="action add" href="<?= $block->escapeUrl($urlForm) ?>"> diff --git a/app/code/Magento/Review/view/frontend/templates/helper/summary_short.phtml b/app/code/Magento/Review/view/frontend/templates/helper/summary_short.phtml index c3eb11f03fd7..a3ff56505f06 100644 --- a/app/code/Magento/Review/view/frontend/templates/helper/summary_short.phtml +++ b/app/code/Magento/Review/view/frontend/templates/helper/summary_short.phtml @@ -11,7 +11,7 @@ $url = $block->getReviewsUrl() . '#reviews'; $urlForm = $block->getReviewsUrl() . '#review-form'; ?> -<?php if ($block->getReviewsCount()): ?> +<?php if ($block->isReviewEnabled() && $block->getReviewsCount()): ?> <?php $rating = $block->getRatingSummary(); ?> <div class="product-reviews-summary short<?= !$rating ? ' no-rating' : '' ?>"> <?php if ($rating):?> @@ -26,7 +26,7 @@ $urlForm = $block->getReviewsUrl() . '#review-form'; <a class="action view" href="<?= $block->escapeUrl($url) ?>"><?= $block->escapeHtml($block->getReviewsCount()) ?> <span><?= ($block->getReviewsCount() == 1) ? $block->escapeHtml(__('Review')) : $block->escapeHtml(__('Reviews')) ?></span></a> </div> </div> -<?php elseif ($block->getDisplayIfEmpty()): ?> +<?php elseif ($block->isReviewEnabled() && $block->getDisplayIfEmpty()): ?> <div class="product-reviews-summary short empty"> <div class="reviews-actions"> <a class="action add" href="<?= $block->escapeUrl($urlForm) ?>"> diff --git a/app/code/Magento/Review/view/frontend/web/js/process-reviews.js b/app/code/Magento/Review/view/frontend/web/js/process-reviews.js index d1c40959e3ec..88c61fa38af3 100644 --- a/app/code/Magento/Review/view/frontend/web/js/process-reviews.js +++ b/app/code/Magento/Review/view/frontend/web/js/process-reviews.js @@ -20,7 +20,7 @@ define([ showLoader: false, loaderContext: $('.product.data.items') }).done(function (data) { - $('#product-review-container').html(data); + $('#product-review-container').html(data).trigger('contentUpdated'); $('[data-role="product-review"] .pages a').each(function (index, element) { $(element).click(function (event) { //eslint-disable-line max-nested-callbacks processReviews($(element).attr('href'), true); diff --git a/app/code/Magento/ReviewAnalytics/README.md b/app/code/Magento/ReviewAnalytics/README.md index b078083dfb7d..a8894f99ed07 100644 --- a/app/code/Magento/ReviewAnalytics/README.md +++ b/app/code/Magento/ReviewAnalytics/README.md @@ -1,3 +1,3 @@ # Magento_ReviewAnalytics module -The Magento_ReviewAnalytics module configures data definitions for a data collection related to the Review module entities to be used in [Advanced Reporting](http://devdocs.magento.com/guides/v2.2/advanced-reporting/modules.html). +The Magento_ReviewAnalytics module configures data definitions for a data collection related to the Review module entities to be used in [Advanced Reporting](https://devdocs.magento.com/guides/v2.2/advanced-reporting/modules.html). diff --git a/app/code/Magento/ReviewAnalytics/composer.json b/app/code/Magento/ReviewAnalytics/composer.json index 73f534451580..a82d4328ca15 100644 --- a/app/code/Magento/ReviewAnalytics/composer.json +++ b/app/code/Magento/ReviewAnalytics/composer.json @@ -4,7 +4,8 @@ "require": { "php": "~7.1.3||~7.2.0", "magento/framework": "*", - "magento/module-review": "*" + "magento/module-review": "*", + "magento/module-analytics": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/Robots/Block/Data.php b/app/code/Magento/Robots/Block/Data.php index 0e492eb73732..460225d3ed71 100644 --- a/app/code/Magento/Robots/Block/Data.php +++ b/app/code/Magento/Robots/Block/Data.php @@ -11,9 +11,11 @@ use Magento\Robots\Model\Config\Value; use Magento\Robots\Model\Robots; use Magento\Store\Model\StoreResolver; +use Magento\Store\Model\StoreManagerInterface; /** * Robots Block Class. + * * Prepares base content for robots.txt and implements Page Cache functionality. * * @api @@ -27,24 +29,29 @@ class Data extends AbstractBlock implements IdentityInterface private $robots; /** - * @var StoreResolver + * @var StoreManagerInterface */ - private $storeResolver; + private $storeManager; /** * @param Context $context * @param Robots $robots * @param StoreResolver $storeResolver + * @param StoreManagerInterface|null $storeManager * @param array $data + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( Context $context, Robots $robots, StoreResolver $storeResolver, + StoreManagerInterface $storeManager = null, array $data = [] ) { $this->robots = $robots; - $this->storeResolver = $storeResolver; + $this->storeManager = $storeManager ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(StoreManagerInterface::class); parent::__construct($context, $data); } @@ -69,7 +76,7 @@ protected function _toHtml() public function getIdentities() { return [ - Value::CACHE_TAG . '_' . $this->storeResolver->getCurrentStoreId(), + Value::CACHE_TAG . '_' . $this->storeManager->getStore()->getId(), ]; } } diff --git a/app/code/Magento/Robots/Model/Config/Value.php b/app/code/Magento/Robots/Model/Config/Value.php index 83c21d6602fc..16a5a486e107 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; @@ -14,9 +15,11 @@ use Magento\Framework\Model\ResourceModel\AbstractResource; use Magento\Framework\Registry; use Magento\Store\Model\StoreResolver; +use Magento\Store\Model\StoreManagerInterface; /** * Backend model for design/search_engine_robots/custom_instructions configuration value. + * * Required to implement Page Cache functionality. * * @api @@ -30,17 +33,16 @@ 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 StoreResolver + * @var StoreManagerInterface */ - private $storeResolver; + private $storeManager; /** * @param Context $context @@ -48,9 +50,12 @@ class Value extends ConfigValue implements IdentityInterface * @param ScopeConfigInterface $config * @param TypeListInterface $cacheTypeList * @param StoreResolver $storeResolver + * @param StoreManagerInterface|null $storeManager * @param AbstractResource|null $resource * @param AbstractDb|null $resourceCollection * @param array $data + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( Context $context, @@ -58,11 +63,13 @@ public function __construct( ScopeConfigInterface $config, TypeListInterface $cacheTypeList, StoreResolver $storeResolver, + StoreManagerInterface $storeManager = null, AbstractResource $resource = null, AbstractDb $resourceCollection = null, array $data = [] ) { - $this->storeResolver = $storeResolver; + $this->storeManager = $storeManager ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(StoreManagerInterface::class); parent::__construct( $context, @@ -84,7 +91,7 @@ public function __construct( public function getIdentities() { return [ - self::CACHE_TAG . '_' . $this->storeResolver->getCurrentStoreId(), + self::CACHE_TAG . '_' . $this->storeManager->getStore()->getId(), ]; } } diff --git a/app/code/Magento/Robots/Test/Unit/Block/DataTest.php b/app/code/Magento/Robots/Test/Unit/Block/DataTest.php index 10d2595832a4..95aa97fc8f67 100644 --- a/app/code/Magento/Robots/Test/Unit/Block/DataTest.php +++ b/app/code/Magento/Robots/Test/Unit/Block/DataTest.php @@ -27,6 +27,11 @@ class DataTest extends \PHPUnit\Framework\TestCase */ private $storeResolver; + /** + * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeManager; + /** * @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ @@ -65,10 +70,14 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); + $this->storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) + ->getMockForAbstractClass(); + $this->block = new \Magento\Robots\Block\Data( $this->context, $this->robots, - $this->storeResolver + $this->storeResolver, + $this->storeManager ); } @@ -97,8 +106,14 @@ public function testGetIdentities() { $storeId = 1; - $this->storeResolver->expects($this->once()) - ->method('getCurrentStoreId') + $storeMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class)->getMock(); + + $this->storeManager->expects($this->once()) + ->method('getStore') + ->willReturn($storeMock); + + $storeMock->expects($this->once()) + ->method('getId') ->willReturn($storeId); $expected = [ diff --git a/app/code/Magento/Robots/Test/Unit/Model/Config/ValueTest.php b/app/code/Magento/Robots/Test/Unit/Model/Config/ValueTest.php index fc0c55ccef14..44e843e7de93 100644 --- a/app/code/Magento/Robots/Test/Unit/Model/Config/ValueTest.php +++ b/app/code/Magento/Robots/Test/Unit/Model/Config/ValueTest.php @@ -37,6 +37,11 @@ class ValueTest extends \PHPUnit\Framework\TestCase */ private $storeResolver; + /** + * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeManager; + protected function setUp() { $this->context = $this->getMockBuilder(\Magento\Framework\Model\Context::class) @@ -57,12 +62,16 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); + $this->storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) + ->getMockForAbstractClass(); + $this->model = new \Magento\Robots\Model\Config\Value( $this->context, $this->registry, $this->scopeConfig, $this->typeList, - $this->storeResolver + $this->storeResolver, + $this->storeManager ); } @@ -73,8 +82,14 @@ public function testGetIdentities() { $storeId = 1; - $this->storeResolver->expects($this->once()) - ->method('getCurrentStoreId') + $storeMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class)->getMockForAbstractClass(); + + $this->storeManager->expects($this->once()) + ->method('getStore') + ->willReturn($storeMock); + + $storeMock->expects($this->once()) + ->method('getId') ->willReturn($storeId); $expected = [ diff --git a/app/code/Magento/Rule/Block/Editable.php b/app/code/Magento/Rule/Block/Editable.php index 67e4671236ea..d53213a7df87 100644 --- a/app/code/Magento/Rule/Block/Editable.php +++ b/app/code/Magento/Rule/Block/Editable.php @@ -9,6 +9,8 @@ use Magento\Framework\View\Element\AbstractBlock; /** + * Renderer for Editable sales rules + * * @api * @since 100.0.2 */ @@ -52,9 +54,9 @@ public function render(\Magento\Framework\Data\Form\Element\AbstractElement $ele if ($element->getShowAsText()) { $html = ' <input type="hidden" class="hidden" id="' . - $element->getHtmlId() . + $this->escapeHtmlAttr($element->getHtmlId()) . '" name="' . - $element->getName() . + $this->escapeHtmlAttr($element->getName()) . '" value="' . $element->getValue() . '" data-form-part="' . diff --git a/app/code/Magento/Rule/Model/Condition/AbstractCondition.php b/app/code/Magento/Rule/Model/Condition/AbstractCondition.php index a1987f67e47f..6729fe722de5 100644 --- a/app/code/Magento/Rule/Model/Condition/AbstractCondition.php +++ b/app/code/Magento/Rule/Model/Condition/AbstractCondition.php @@ -62,7 +62,8 @@ abstract class AbstractCondition extends \Magento\Framework\DataObject implement protected $_layout; /** - * Base name for hidden elements + * Base name for hidden elements. + * * @var string */ protected $elementName = 'rule'; @@ -105,8 +106,8 @@ public function getDefaultOperatorInputByType() 'string' => ['==', '!=', '>=', '>', '<=', '<', '{}', '!{}', '()', '!()'], 'numeric' => ['==', '!=', '>=', '>', '<=', '<', '()', '!()'], 'date' => ['==', '>=', '<='], - 'select' => ['==', '!='], - 'boolean' => ['==', '!='], + 'select' => ['==', '!=', '<=>'], + 'boolean' => ['==', '!=', '<=>'], 'multiselect' => ['{}', '!{}', '()', '!()'], 'grid' => ['()', '!()'], ]; @@ -116,8 +117,9 @@ public function getDefaultOperatorInputByType() } /** - * Default operator options getter - * Provides all possible operator options + * Default operator options getter. + * + * Provides all possible operator options. * * @return array */ @@ -135,12 +137,15 @@ public function getDefaultOperatorOptions() '!{}' => __('does not contain'), '()' => __('is one of'), '!()' => __('is not one of'), + '<=>' => __('is undefined'), ]; } return $this->_defaultOperatorOptions; } /** + * Get rule form. + * * @return Form */ public function getForm() @@ -149,6 +154,8 @@ public function getForm() } /** + * Get condition as array. + * * @param array $arrAttributes * @return array * @SuppressWarnings(PHPMD.UnusedFormalParameter) @@ -195,6 +202,8 @@ public function getMappedSqlField() } /** + * Get condition as xml. + * * @return string */ public function asXml() @@ -214,6 +223,8 @@ public function asXml() } /** + * Load condition from array. + * * @param array $arr * @return $this * @SuppressWarnings(PHPMD.NPathComplexity) @@ -229,6 +240,8 @@ public function loadArray($arr) } /** + * Load condition from xml. + * * @param string|array $xml * @return $this */ @@ -242,6 +255,8 @@ public function loadXml($xml) } /** + * Load attribute options. + * * @return $this */ public function loadAttributeOptions() @@ -250,6 +265,8 @@ public function loadAttributeOptions() } /** + * Get attribute options. + * * @return array */ public function getAttributeOptions() @@ -258,6 +275,8 @@ public function getAttributeOptions() } /** + * Get attribute select options. + * * @return array */ public function getAttributeSelectOptions() @@ -270,6 +289,8 @@ public function getAttributeSelectOptions() } /** + * Get attribute name. + * * @return string */ public function getAttributeName() @@ -278,6 +299,8 @@ public function getAttributeName() } /** + * Load operator options. + * * @return $this */ public function loadOperatorOptions() @@ -300,6 +323,8 @@ public function getInputType() } /** + * Get operator select options. + * * @return array */ public function getOperatorSelectOptions() @@ -316,6 +341,8 @@ public function getOperatorSelectOptions() } /** + * Get operator name. + * * @return array */ public function getOperatorName() @@ -324,6 +351,8 @@ public function getOperatorName() } /** + * Load value options. + * * @return $this */ public function loadValueOptions() @@ -333,6 +362,8 @@ public function loadValueOptions() } /** + * Get value select options. + * * @return array */ public function getValueSelectOptions() @@ -380,6 +411,8 @@ public function isArrayOperatorType() } /** + * Get value. + * * @return mixed */ public function getValue() @@ -395,6 +428,8 @@ public function getValue() } /** + * Get value name. + * * @return array|string * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ @@ -446,6 +481,8 @@ public function getNewChildSelectOptions() } /** + * Get new child name. + * * @return string */ public function getNewChildName() @@ -454,6 +491,8 @@ public function getNewChildName() } /** + * Get this condition as html. + * * @return string */ public function asHtml() @@ -467,6 +506,8 @@ public function asHtml() } /** + * Get this condition with subconditions as html. + * * @return string */ public function asHtmlRecursive() @@ -475,6 +516,8 @@ public function asHtmlRecursive() } /** + * Get type element. + * * @return AbstractElement */ public function getTypeElement() @@ -493,6 +536,8 @@ public function getTypeElement() } /** + * Get type element html. + * * @return string */ public function getTypeElementHtml() @@ -501,6 +546,8 @@ public function getTypeElementHtml() } /** + * Get attribute element. + * * @return $this */ public function getAttributeElement() @@ -528,6 +575,8 @@ public function getAttributeElement() } /** + * Get attribute element html. + * * @return string */ public function getAttributeElementHtml() @@ -536,8 +585,9 @@ public function getAttributeElementHtml() } /** - * Retrieve Condition Operator element Instance - * If the operator value is empty - define first available operator value as default + * Retrieve Condition Operator element Instance. + * + * If the operator value is empty - define first available operator value as default. * * @return \Magento\Framework\Data\Form\Element\Select */ @@ -568,6 +618,8 @@ public function getOperatorElement() } /** + * Get operator element html. + * * @return string */ public function getOperatorElementHtml() @@ -587,6 +639,8 @@ public function getValueElementType() } /** + * Get value element renderer. + * * @return \Magento\Rule\Block\Editable */ public function getValueElementRenderer() @@ -598,6 +652,8 @@ public function getValueElementRenderer() } /** + * Get value element. + * * @return $this */ public function getValueElement() @@ -615,6 +671,9 @@ public function getValueElement() // date format intentionally hard-coded $elementParams['input_format'] = \Magento\Framework\Stdlib\DateTime::DATE_INTERNAL_FORMAT; $elementParams['date_format'] = \Magento\Framework\Stdlib\DateTime::DATE_INTERNAL_FORMAT; + $elementParams['placeholder'] = \Magento\Framework\Stdlib\DateTime::DATE_INTERNAL_FORMAT; + $elementParams['autocomplete'] = 'off'; + $elementParams['readonly'] = 'true'; } return $this->getForm()->addField( $this->getPrefix() . '__' . $this->getId() . '__value', @@ -626,6 +685,8 @@ public function getValueElement() } /** + * Get value element html. + * * @return string */ public function getValueElementHtml() @@ -634,6 +695,8 @@ public function getValueElementHtml() } /** + * Get add link html. + * * @return string */ public function getAddLinkHtml() @@ -643,6 +706,8 @@ public function getAddLinkHtml() } /** + * Get remove link html. + * * @return string */ public function getRemoveLinkHtml() @@ -655,6 +720,8 @@ public function getRemoveLinkHtml() } /** + * Get chooser container html. + * * @return string */ public function getChooserContainerHtml() @@ -664,6 +731,8 @@ public function getChooserContainerHtml() } /** + * Get this condition as string. + * * @param string $format * @return string * @SuppressWarnings(PHPMD.UnusedFormalParameter) @@ -674,6 +743,8 @@ public function asString($format = '') } /** + * Get this condition with subconditions as string. + * * @param int $level * @return string */ @@ -816,6 +887,8 @@ protected function _compareValues($validatedValue, $value, $strict = true) } /** + * Validate model. + * * @param \Magento\Framework\Model\AbstractModel $model * @return bool */ diff --git a/app/code/Magento/Rule/Model/Condition/Product/AbstractProduct.php b/app/code/Magento/Rule/Model/Condition/Product/AbstractProduct.php index 5ab1379b96cf..e216e2ae658b 100644 --- a/app/code/Magento/Rule/Model/Condition/Product/AbstractProduct.php +++ b/app/code/Magento/Rule/Model/Condition/Product/AbstractProduct.php @@ -95,8 +95,8 @@ abstract class AbstractProduct extends \Magento\Rule\Model\Condition\AbstractCon * @param \Magento\Catalog\Model\ResourceModel\Product $productResource * @param \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\Collection $attrSetCollection * @param \Magento\Framework\Locale\FormatInterface $localeFormat - * @param ProductCategoryList|null $categoryList * @param array $data + * @param ProductCategoryList|null $categoryList * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -514,6 +514,10 @@ public function loadArray($arr) ) ? $this->_localeFormat->getNumber( $arr['is_value_parsed'] ) : false; + } elseif (!empty($arr['operator']) && $arr['operator'] == '()') { + if (isset($arr['value'])) { + $arr['value'] = preg_replace('/\s*,\s*/', ',', $arr['value']); + } } return parent::loadArray($arr); @@ -695,6 +699,7 @@ protected function _getAttributeSetId($productId) /** * Correct '==' and '!=' operators + * * Categories can't be equal because product is included categories selected by administrator and in their parents * * @return string diff --git a/app/code/Magento/Rule/Model/Condition/Sql/Builder.php b/app/code/Magento/Rule/Model/Condition/Sql/Builder.php index 2894de0f19b8..33e1bf97c347 100644 --- a/app/code/Magento/Rule/Model/Condition/Sql/Builder.php +++ b/app/code/Magento/Rule/Model/Condition/Sql/Builder.php @@ -32,15 +32,27 @@ class Builder '==' => ':field = ?', '!=' => ':field <> ?', '>=' => ':field >= ?', + '>=' => ':field >= ?', '>' => ':field > ?', + '>' => ':field > ?', '<=' => ':field <= ?', + '<=' => ':field <= ?', '<' => ':field < ?', + '<' => ':field < ?', '{}' => ':field IN (?)', '!{}' => ':field NOT IN (?)', '()' => ':field IN (?)', '!()' => ':field NOT IN (?)', ]; + /** + * @var array + */ + private $stringConditionOperatorMap = [ + '{}' => ':field LIKE ?', + '!{}' => ':field NOT LIKE ?', + ]; + /** * @var \Magento\Rule\Model\Condition\Sql\ExpressionFactory */ @@ -128,9 +140,10 @@ protected function _joinTablesToCollection( * * @param AbstractCondition $condition * @param string $value - * @param bool $isDefaultStoreUsed + * @param bool $isDefaultStoreUsed no longer used because caused an issue about not existing table alias * @return string * @throws \Magento\Framework\Exception\LocalizedException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ protected function _getMappedSqlCondition( AbstractCondition $condition, @@ -151,21 +164,27 @@ protected function _getMappedSqlCondition( } $defaultValue = 0; - // Check if attribute has a table with default value and add it to the query - if ($this->canAttributeHaveDefaultValue($condition->getAttribute(), $isDefaultStoreUsed)) { - $defaultField = 'at_' . $condition->getAttribute() . '_default.value'; - $defaultValue = $this->_connection->quoteIdentifier($defaultField); + //operator 'contains {}' is mapped to 'IN()' query that cannot work with substrings + // adding mapping to 'LIKE %%' + if ($condition->getInputType() === 'string' + && in_array($conditionOperator, array_keys($this->stringConditionOperatorMap), true) + ) { + $sql = str_replace( + ':field', + $this->_connection->getIfNullSql($this->_connection->quoteIdentifier($argument), $defaultValue), + $this->stringConditionOperatorMap[$conditionOperator] + ); + $bindValue = $condition->getBindArgumentValue(); + $expression = $value . $this->_connection->quoteInto($sql, "%$bindValue%"); + } else { + $sql = str_replace( + ':field', + $this->_connection->getIfNullSql($this->_connection->quoteIdentifier($argument), $defaultValue), + $this->_conditionOperatorMap[$conditionOperator] + ); + $bindValue = $condition->getBindArgumentValue(); + $expression = $value . $this->_connection->quoteInto($sql, $bindValue); } - - $sql = str_replace( - ':field', - $this->_connection->getIfNullSql($this->_connection->quoteIdentifier($argument), $defaultValue), - $this->_conditionOperatorMap[$conditionOperator] - ); - - $bindValue = $condition->getBindArgumentValue(); - $expression = $value . $this->_connection->quoteInto($sql, $bindValue); - // values for multiselect attributes can be saved in comma-separated format // below is a solution for matching such conditions with selected values if (is_array($bindValue) && \in_array($conditionOperator, ['()', '{}'], true)) { @@ -183,6 +202,8 @@ protected function _getMappedSqlCondition( } /** + * Get mapped sql combination. + * * @param Combine $combine * @param string $value * @param bool $isDefaultStoreUsed @@ -227,45 +248,32 @@ public function attachConditionToCollection( ): void { $this->_connection = $collection->getResource()->getConnection(); $this->_joinTablesToCollection($collection, $combine); - $isDefaultStoreUsed = $this->checkIsDefaultStoreUsed($collection); - $whereExpression = (string)$this->_getMappedSqlCombination($combine, '', $isDefaultStoreUsed); + $whereExpression = (string)$this->_getMappedSqlCombination($combine); if (!empty($whereExpression)) { - // Select ::where method adds braces even on empty expression - $collection->getSelect()->where($whereExpression); - } - } - - /** - * Check is default store used. - * - * @param AbstractCollection $collection - * @return bool - */ - private function checkIsDefaultStoreUsed(AbstractCollection $collection): bool - { - return (int)$collection->getStoreId() === (int)$collection->getDefaultStoreId(); - } + if (!empty($combine->getConditions())) { + $conditions = ''; + $attributeField = ''; + foreach ($combine->getConditions() as $condition) { + if ($condition->getData('attribute') === \Magento\Catalog\Api\Data\ProductInterface::SKU) { + $conditions = $condition->getData('value'); + $attributeField = $condition->getMappedSqlField(); + } + } - /** - * Check if attribute can have default value. - * - * @param string $attributeCode - * @param bool $isDefaultStoreUsed - * @return bool - */ - private function canAttributeHaveDefaultValue(string $attributeCode, bool $isDefaultStoreUsed): bool - { - if ($isDefaultStoreUsed) { - return false; - } + $collection->getSelect()->where($whereExpression); - try { - $attribute = $this->attributeRepository->get(Product::ENTITY, $attributeCode); - } catch (NoSuchEntityException $e) { - // It's not exceptional case as we want to check if we have such attribute or not - return false; + if (!empty($conditions) && !empty($attributeField)) { + $conditions = explode(',', $conditions); + foreach ($conditions as &$condition) { + $condition = "'" . trim($condition) . "'"; + } + $conditions = implode(', ', $conditions); + $collection->getSelect()->order("FIELD($attributeField, $conditions)"); + } + } else { + // Select ::where method adds braces even on empty expression + $collection->getSelect()->where($whereExpression); + } } - - return !$attribute->isScopeGlobal(); } } diff --git a/app/code/Magento/Rule/Test/Unit/Model/Condition/Sql/BuilderTest.php b/app/code/Magento/Rule/Test/Unit/Model/Condition/Sql/BuilderTest.php index daf7b1462c72..9dcbbd18c4c2 100644 --- a/app/code/Magento/Rule/Test/Unit/Model/Condition/Sql/BuilderTest.php +++ b/app/code/Magento/Rule/Test/Unit/Model/Condition/Sql/BuilderTest.php @@ -61,12 +61,6 @@ public function testAttachConditionToCollection() $collection->expects($this->any()) ->method('getSelect') ->will($this->returnValue($select)); - $collection->expects($this->once()) - ->method('getStoreId') - ->willReturn(1); - $collection->expects($this->once()) - ->method('getDefaultStoreId') - ->willReturn(1); $resource->expects($this->once()) ->method('getConnection') @@ -78,4 +72,69 @@ public function testAttachConditionToCollection() $this->_builder->attachConditionToCollection($collection, $combine); } + + /** + * Test for attach condition to collection with operator in html format + * + * @covers \Magento\Rule\Model\Condition\Sql\Builder::attachConditionToCollection() + * @return void; + */ + public function testAttachConditionAsHtmlToCollection() + { + $abstractCondition = $this->getMockForAbstractClass( + \Magento\Rule\Model\Condition\AbstractCondition::class, + [], + '', + false, + false, + true, + ['getOperatorForValidate', 'getMappedSqlField', 'getAttribute', 'getBindArgumentValue'] + ); + + $abstractCondition->expects($this->once())->method('getMappedSqlField')->will($this->returnValue('argument')); + $abstractCondition->expects($this->once())->method('getOperatorForValidate')->will($this->returnValue('>')); + $abstractCondition->expects($this->at(1))->method('getAttribute')->will($this->returnValue('attribute')); + $abstractCondition->expects($this->at(2))->method('getAttribute')->will($this->returnValue('attribute')); + $abstractCondition->expects($this->once())->method('getBindArgumentValue')->will($this->returnValue(10)); + + $conditions = [$abstractCondition]; + $collection = $this->createPartialMock( + \Magento\Eav\Model\Entity\Collection\AbstractCollection::class, + [ + 'getResource', + 'getSelect' + ] + ); + $combine = $this->createPartialMock( + \Magento\Rule\Model\Condition\Combine::class, + [ + 'getConditions', + 'getValue', + 'getAggregator' + ] + ); + + $resource = $this->createPartialMock(\Magento\Framework\DB\Adapter\Pdo\Mysql::class, ['getConnection']); + $select = $this->createPartialMock(\Magento\Framework\DB\Select::class, ['where']); + $select->expects($this->never())->method('where'); + + $connection = $this->getMockForAbstractClass( + \Magento\Framework\DB\Adapter\AdapterInterface::class, + ['quoteInto'], + '', + false + ); + + $connection->expects($this->once())->method('quoteInto')->with(' > ?', 10)->will($this->returnValue(' > 10')); + $collection->expects($this->once())->method('getResource')->will($this->returnValue($resource)); + $resource->expects($this->once())->method('getConnection')->will($this->returnValue($connection)); + $combine->expects($this->once())->method('getValue')->willReturn('attribute'); + $combine->expects($this->once())->method('getAggregator')->willReturn(' AND '); + $combine->expects($this->at(0))->method('getConditions')->will($this->returnValue($conditions)); + $combine->expects($this->at(1))->method('getConditions')->will($this->returnValue($conditions)); + $combine->expects($this->at(2))->method('getConditions')->will($this->returnValue($conditions)); + $combine->expects($this->at(3))->method('getConditions')->will($this->returnValue($conditions)); + + $this->_builder->attachConditionToCollection($collection, $combine); + } } diff --git a/app/code/Magento/Rule/view/adminhtml/web/rules.js b/app/code/Magento/Rule/view/adminhtml/web/rules.js index b094b9818364..95175d272f9a 100644 --- a/app/code/Magento/Rule/view/adminhtml/web/rules.js +++ b/app/code/Magento/Rule/view/adminhtml/web/rules.js @@ -13,6 +13,7 @@ define([ 'mage/translate', 'prototype' ], function (jQuery) { + 'use strict'; var VarienRulesForm = new Class.create(); @@ -101,6 +102,9 @@ define([ if (!elem.multiple) { Event.observe(elem, 'change', this.hideParamInputField.bind(this, container)); + + this.changeVisibilityForValueRuleParam(elem); + } Event.observe(elem, 'blur', this.hideParamInputField.bind(this, container)); } @@ -122,7 +126,7 @@ define([ var values = this.updateElement.value.split(','), s = ''; - for (i = 0; i < values.length; i++) { + for (var i = 0; i < values.length; i++) { s = values[i].strip(); if (s != '') { @@ -220,6 +224,8 @@ define([ var elem = Element.down(elemContainer, 'input.input-text'); + jQuery(elem).trigger('contentUpdated'); + if (elem) { elem.focus(); @@ -249,7 +255,7 @@ define([ if (elem && elem.options) { var selectedOptions = []; - for (i = 0; i < elem.options.length; i++) { + for (var i = 0; i < elem.options.length; i++) { if (elem.options[i].selected) { selectedOptions.push(elem.options[i].text); } @@ -260,6 +266,8 @@ define([ label.innerHTML = str != '' ? str : '...'; } + this.changeVisibilityForValueRuleParam(elem); + elem = Element.down(container, 'input.input-text'); if (elem) { @@ -291,6 +299,23 @@ define([ this.shownElement = null; }, + changeVisibilityForValueRuleParam: function(elem) { + var parsedElementId = elem.id.split('__'); + if (parsedElementId[2] !== 'operator') { + return false; + } + + var valueElement = jQuery('#' + parsedElementId[0] + '__' + parsedElementId[1] + '__value'); + + if(elem.value === '<=>') { + valueElement.closest('.rule-param').hide(); + } else { + valueElement.closest('.rule-param').show(); + } + + return true; + }, + addRuleNewChild: function (elem) { var parent_id = elem.id.replace(/^.*__(.*)__.*$/, '$1'); var children_ul_id = elem.id.replace(/__/g, ':').replace(/[^:]*$/, 'children').replace(/:/g, '__'); diff --git a/app/code/Magento/Sales/Api/CreditmemoRepositoryInterface.php b/app/code/Magento/Sales/Api/CreditmemoRepositoryInterface.php index 354b55eb2595..3c61384d8b84 100644 --- a/app/code/Magento/Sales/Api/CreditmemoRepositoryInterface.php +++ b/app/code/Magento/Sales/Api/CreditmemoRepositoryInterface.php @@ -20,7 +20,7 @@ interface CreditmemoRepositoryInterface * Lists credit memos that match specified search criteria. * * This call returns an array of objects, but detailed information about each object’s attributes might not be - * included. See http://devdocs.magento.com/codelinks/attributes.html#CreditmemoRepositoryInterface to + * included. See https://devdocs.magento.com/codelinks/attributes.html#CreditmemoRepositoryInterface to * determine which call to use to get detailed information about all attributes for an object. * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria The search criteria. diff --git a/app/code/Magento/Sales/Api/Data/OrderInterface.php b/app/code/Magento/Sales/Api/Data/OrderInterface.php index 3ca936afd409..b45fddc7d735 100644 --- a/app/code/Magento/Sales/Api/Data/OrderInterface.php +++ b/app/code/Magento/Sales/Api/Data/OrderInterface.php @@ -581,6 +581,8 @@ public function getAdjustmentPositive(); /** * Gets the applied rule IDs for the order. * + * Rules are comma separated if there are more than one. + * * @return string|null Applied rule IDs. */ public function getAppliedRuleIds(); diff --git a/app/code/Magento/Sales/Api/Data/OrderItemInterface.php b/app/code/Magento/Sales/Api/Data/OrderItemInterface.php index ae4064c0ab3d..2aee648ef7c9 100644 --- a/app/code/Magento/Sales/Api/Data/OrderItemInterface.php +++ b/app/code/Magento/Sales/Api/Data/OrderItemInterface.php @@ -415,6 +415,8 @@ public function getAmountRefunded(); /** * Gets the applied rule IDs for the order item. * + * Rules are comma separated if there are more than one. + * * @return string|null Applied rule IDs. */ public function getAppliedRuleIds(); diff --git a/app/code/Magento/Sales/Api/InvoiceRepositoryInterface.php b/app/code/Magento/Sales/Api/InvoiceRepositoryInterface.php index 9d4f11ed0f03..161b8405f11e 100644 --- a/app/code/Magento/Sales/Api/InvoiceRepositoryInterface.php +++ b/app/code/Magento/Sales/Api/InvoiceRepositoryInterface.php @@ -18,7 +18,7 @@ interface InvoiceRepositoryInterface * Lists invoices that match specified search criteria. * * This call returns an array of objects, but detailed information about each object’s attributes might not be - * included. See http://devdocs.magento.com/codelinks/attributes.html#InvoiceRepositoryInterface to + * included. See https://devdocs.magento.com/codelinks/attributes.html#InvoiceRepositoryInterface to * determine which call to use to get detailed information about all attributes for an object. * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria The search criteria. diff --git a/app/code/Magento/Sales/Api/OrderItemRepositoryInterface.php b/app/code/Magento/Sales/Api/OrderItemRepositoryInterface.php index e731f6f0e980..3449d0054b7e 100644 --- a/app/code/Magento/Sales/Api/OrderItemRepositoryInterface.php +++ b/app/code/Magento/Sales/Api/OrderItemRepositoryInterface.php @@ -20,7 +20,7 @@ interface OrderItemRepositoryInterface * Lists order items that match specified search criteria. * * This call returns an array of objects, but detailed information about each object’s attributes might not be - * included. See http://devdocs.magento.com/codelinks/attributes.html#OrderItemRepositoryInterface to + * included. See https://devdocs.magento.com/codelinks/attributes.html#OrderItemRepositoryInterface to * determine which call to use to get detailed information about all attributes for an object. * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria The search criteria. diff --git a/app/code/Magento/Sales/Api/OrderRepositoryInterface.php b/app/code/Magento/Sales/Api/OrderRepositoryInterface.php index a25f463ac861..0c3b6ab5cb02 100644 --- a/app/code/Magento/Sales/Api/OrderRepositoryInterface.php +++ b/app/code/Magento/Sales/Api/OrderRepositoryInterface.php @@ -20,7 +20,7 @@ interface OrderRepositoryInterface * Lists orders that match specified search criteria. * * This call returns an array of objects, but detailed information about each object’s attributes might not be - * included. See http://devdocs.magento.com/codelinks/attributes.html#OrderRepositoryInterface to + * included. See https://devdocs.magento.com/codelinks/attributes.html#OrderRepositoryInterface to * determine which call to use to get detailed information about all attributes for an object. * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria The search criteria. diff --git a/app/code/Magento/Sales/Api/ShipmentRepositoryInterface.php b/app/code/Magento/Sales/Api/ShipmentRepositoryInterface.php index bfdf17440ebd..3b3c8221596a 100644 --- a/app/code/Magento/Sales/Api/ShipmentRepositoryInterface.php +++ b/app/code/Magento/Sales/Api/ShipmentRepositoryInterface.php @@ -19,7 +19,7 @@ interface ShipmentRepositoryInterface * Lists shipments that match specified search criteria. * * This call returns an array of objects, but detailed information about each object’s attributes might not be - * included. See http://devdocs.magento.com/codelinks/attributes.html#ShipmentRepositoryInterface to + * included. See https://devdocs.magento.com/codelinks/attributes.html#ShipmentRepositoryInterface to * determine which call to use to get detailed information about all attributes for an object. * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria The search criteria. diff --git a/app/code/Magento/Sales/Api/TransactionRepositoryInterface.php b/app/code/Magento/Sales/Api/TransactionRepositoryInterface.php index de459662a832..e55b5d60d1f6 100644 --- a/app/code/Magento/Sales/Api/TransactionRepositoryInterface.php +++ b/app/code/Magento/Sales/Api/TransactionRepositoryInterface.php @@ -18,7 +18,7 @@ interface TransactionRepositoryInterface * Lists transactions that match specified search criteria. * * This call returns an array of objects, but detailed information about each object’s attributes might not be - * included. See http://devdocs.magento.com/codelinks/attributes.html#TransactionRepositoryInterface to + * included. See https://devdocs.magento.com/codelinks/attributes.html#TransactionRepositoryInterface to * determine which call to use to get detailed information about all attributes for an object. * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria The search criteria. diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Address/Form.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Address/Form.php index 6cab109b44db..12e59e63f6f7 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Address/Form.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Address/Form.php @@ -113,6 +113,7 @@ protected function _prepareForm() $this->getUrl('sales/*/addressSave', ['address_id' => $this->_getAddress()->getId()]) ); $this->_form->setUseContainer(true); + return $this; } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form.php index 12a8270acfff..2b214e42d8b9 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form.php @@ -164,6 +164,7 @@ public function getDataSelectorDisplay() public function getOrderDataJson() { $data = []; + $this->_storeManager->setCurrentStore($this->getStoreId()); if ($this->getCustomerId()) { $data['customer_id'] = $this->getCustomerId(); $data['addresses'] = []; diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/AbstractForm.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/AbstractForm.php index d15c218a60b4..6b87c1fe39d8 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/AbstractForm.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/AbstractForm.php @@ -6,6 +6,7 @@ namespace Magento\Sales\Block\Adminhtml\Order\Create\Form; use Magento\Framework\Pricing\PriceCurrencyInterface; +use Magento\Customer\Api\Data\AttributeMetadataInterface; /** * Sales Order Create Form Abstract Block @@ -57,8 +58,7 @@ public function __construct( } /** - * Prepare global layout - * Add renderers to \Magento\Framework\Data\Form + * Prepare global layout. Add renderers to \Magento\Framework\Data\Form * * @return $this */ @@ -152,7 +152,7 @@ protected function _addAdditionalFormElementData(\Magento\Framework\Data\Form\El /** * Add rendering EAV attributes to Form element * - * @param \Magento\Customer\Api\Data\AttributeMetadataInterface[] $attributes + * @param AttributeMetadataInterface[] $attributes * @param \Magento\Framework\Data\Form\AbstractForm $form * @return $this * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -176,8 +176,8 @@ protected function _addAttributesToForm($attributes, \Magento\Framework\Data\For [ 'name' => $attribute->getAttributeCode(), 'label' => __($attribute->getStoreLabel()), - 'class' => $attribute->getFrontendClass(), - 'required' => $attribute->isRequired() + 'class' => $this->getValidationClasses($attribute), + 'required' => $attribute->isRequired(), ] ); if ($inputType == 'multiline') { @@ -227,4 +227,58 @@ public function getFormValues() { return []; } + + /** + * Retrieve frontend classes according validation rules + * + * @param AttributeMetadataInterface $attribute + * + * @return string + */ + private function getValidationClasses(AttributeMetadataInterface $attribute) : string + { + $out = []; + $out[] = $attribute->getFrontendClass(); + + $textClasses = $this->getTextLengthValidateClasses($attribute); + if (!empty($textClasses)) { + $out = array_merge($out, $textClasses); + } + + $out = !empty($out) ? implode(' ', array_unique(array_filter($out))) : ''; + return $out; + } + + /** + * Retrieve validation classes by min_text_length and max_text_length rules + * + * @param AttributeMetadataInterface $attribute + * + * @return array + */ + private function getTextLengthValidateClasses(AttributeMetadataInterface $attribute) : array + { + $classes = []; + + $validateRules = $attribute->getValidationRules(); + if (!empty($validateRules)) { + foreach ($validateRules as $rule) { + switch ($rule->getName()) { + case 'min_text_length': + $classes[] = 'minimum-length-' . $rule->getValue(); + break; + + case 'max_text_length': + $classes[] = 'maximum-length-' . $rule->getValue(); + break; + } + } + + if (!empty($classes)) { + $classes[] = 'validate-length'; + } + } + + return $classes; + } } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Account.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Account.php index 26379a05fe69..03915c049936 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Account.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Account.php @@ -132,15 +132,7 @@ protected function _prepareForm() $this->_addAttributesToForm($attributes, $fieldset); $this->_form->addFieldNameSuffix('order[account]'); - - $formValues = $this->getFormValues(); - foreach ($attributes as $code => $attribute) { - $defaultValue = $attribute->getDefaultValue(); - if (isset($defaultValue) && !isset($formValues[$code])) { - $formValues[$code] = $defaultValue; - } - } - $this->_form->setValues($formValues); + $this->_form->setValues($this->extractValuesFromAttributes($attributes)); return $this; } @@ -193,4 +185,23 @@ public function getFormValues() return $data; } + + /** + * Extract the form values from attributes. + * + * @param array $attributes + * @return array + */ + private function extractValuesFromAttributes(array $attributes): array + { + $formValues = $this->getFormValues(); + foreach ($attributes as $code => $attribute) { + $defaultValue = $attribute->getDefaultValue(); + if (isset($defaultValue) && !isset($formValues[$code])) { + $formValues[$code] = $defaultValue; + } + } + + return $formValues; + } } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Address.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Address.php index 5738e8ee3339..0e3d308df912 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Address.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Address.php @@ -136,6 +136,7 @@ public function __construct( $this->searchCriteriaBuilder = $criteriaBuilder; $this->filterBuilder = $filterBuilder; $this->addressMapper = $addressMapper; + $this->backendQuoteSession = $sessionQuote; parent::__construct( $context, $sessionQuote, @@ -217,6 +218,11 @@ public function getAddressCollectionJson() */ protected function _prepareForm() { + $storeId = $this->getCreateOrderModel() + ->getSession() + ->getStoreId(); + $this->_storeManager->setCurrentStore($storeId); + $fieldset = $this->_form->addFieldset('main', ['no_container' => true]); $addressForm = $this->_customerFormFactory->create('customer_address', 'adminhtml_customer_address'); @@ -292,6 +298,8 @@ protected function _prepareForm() } /** + * Process country options. + * * @param \Magento\Framework\Data\Form\Element\AbstractElement $countryElement * @return void */ @@ -306,7 +314,8 @@ private function processCountryOptions(\Magento\Framework\Data\Form\Element\Abst } /** - * Retrieve Directiry Countries collection + * Retrieve Directory Countries collection + * * @deprecated 100.1.3 * @return \Magento\Directory\Model\ResourceModel\Country\Collection */ @@ -322,6 +331,7 @@ private function getCountriesCollection() /** * Retrieve Backend Quote Session + * * @deprecated 100.1.3 * @return Quote */ diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Load.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Load.php index f21cc96be92b..7cb46fcde2c4 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Load.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Load.php @@ -29,7 +29,7 @@ class Load extends \Magento\Framework\View\Element\Template /** * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Framework\Json\EncoderInterface $jsonEncoder - * @param \Magento\Framework\View\Helper\Js $adminhtmlJs + * @param \Magento\Framework\View\Helper\Js $jsHelper * @param array $data */ public function __construct( diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid.php index 4bd2227d4bb1..9a271f741edd 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid.php @@ -5,12 +5,17 @@ */ namespace Magento\Sales\Block\Adminhtml\Order\Create\Search; +use Magento\Sales\Block\Adminhtml\Order\Create\Search\Grid\DataProvider\ProductCollection + as ProductCollectionDataProvider; +use Magento\Framework\App\ObjectManager; + /** * Adminhtml sales order create search products block * * @api * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended { @@ -42,6 +47,11 @@ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended */ protected $_productFactory; + /** + * @var ProductCollectionDataProvider $productCollectionProvider + */ + private $productCollectionProvider; + /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Backend\Helper\Data $backendHelper @@ -50,6 +60,7 @@ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended * @param \Magento\Backend\Model\Session\Quote $sessionQuote * @param \Magento\Sales\Model\Config $salesConfig * @param array $data + * @param ProductCollectionDataProvider|null $productCollectionProvider */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -58,12 +69,15 @@ public function __construct( \Magento\Catalog\Model\Config $catalogConfig, \Magento\Backend\Model\Session\Quote $sessionQuote, \Magento\Sales\Model\Config $salesConfig, - array $data = [] + array $data = [], + ProductCollectionDataProvider $productCollectionProvider = null ) { $this->_productFactory = $productFactory; $this->_catalogConfig = $catalogConfig; $this->_sessionQuote = $sessionQuote; $this->_salesConfig = $salesConfig; + $this->productCollectionProvider = $productCollectionProvider + ?: ObjectManager::getInstance()->get(ProductCollectionDataProvider::class); parent::__construct($context, $backendHelper, $data); } @@ -140,20 +154,18 @@ protected function _addColumnFilterToCollection($column) */ protected function _prepareCollection() { + $attributes = $this->_catalogConfig->getProductAttributes(); + $store = $this->getStore(); + /* @var $collection \Magento\Catalog\Model\ResourceModel\Product\Collection */ - $collection = $this->_productFactory->create()->getCollection(); - $collection->setStore( - $this->getStore() - )->addAttributeToSelect( + $collection = $this->productCollectionProvider->getCollectionForStore($store); + $collection->addAttributeToSelect( $attributes - )->addAttributeToSelect( - 'sku' - )->addStoreFilter()->addAttributeToFilter( + ); + $collection->addAttributeToFilter( 'type_id', $this->_salesConfig->getAvailableProductTypes() - )->addAttributeToSelect( - 'gift_message_available' ); $this->setCollection($collection); diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid/DataProvider/ProductCollection.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid/DataProvider/ProductCollection.php new file mode 100644 index 000000000000..733791a2f954 --- /dev/null +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid/DataProvider/ProductCollection.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Block\Adminhtml\Order\Create\Search\Grid\DataProvider; + +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory; +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Store\Model\Store; + +/** + * Prepares product collection for the grid + */ +class ProductCollection +{ + /** + * @var ProductCollectionFactory + */ + private $collectionFactory; + + /** + * @param ProductCollectionFactory $collectionFactory + */ + public function __construct( + ProductCollectionFactory $collectionFactory + ) { + $this->collectionFactory = $collectionFactory; + } + + /** + * Provide products collection filtered with store + * + * @param Store $store + * @return Collection + */ + public function getCollectionForStore(Store $store):Collection + { + /** @var Collection $collection */ + $collection = $this->collectionFactory->create(); + + $collection->setStore($store); + $collection->addAttributeToSelect( + 'gift_message_available' + ); + $collection->addAttributeToSelect( + 'sku' + ); + $collection->addStoreFilter(); + + return $collection; + } +} diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Cart.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Cart.php index 34d7a3f8ee25..f2200e1c1a10 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Cart.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Cart.php @@ -3,8 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Block\Adminhtml\Order\Create\Sidebar; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Pricing\Price\FinalPrice; + /** * Adminhtml sales order create sidebar cart block * @@ -58,6 +63,17 @@ public function getItemCollection() return $collection; } + /** + * @inheritdoc + */ + public function getItemPrice(Product $product) + { + $customPrice = $this->getCartItemCustomPrice($product); + $price = $customPrice ?? $product->getPriceInfo()->getPrice(FinalPrice::PRICE_CODE)->getValue(); + + return $this->convertPrice($price); + } + /** * Retrieve display item qty availability * @@ -111,4 +127,23 @@ protected function _prepareLayout() return parent::_prepareLayout(); } + + /** + * Returns cart item custom price. + * + * @param Product $product + * @return float|null + */ + private function getCartItemCustomPrice(Product $product): ?float + { + $items = $this->getItemCollection(); + foreach ($items as $item) { + $productItemId = $this->getProduct($item)->getId(); + if ($productItemId === $product->getId() && $item->getCustomPrice()) { + return (float)$item->getCustomPrice(); + } + } + + return null; + } } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Adjustments.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Adjustments.php index d73371d46dae..9e13e9424d1f 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Adjustments.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Adjustments.php @@ -8,6 +8,8 @@ use Magento\Framework\Pricing\PriceCurrencyInterface; /** + * Credit memo adjustmets block + * * @api * @since 100.0.2 */ @@ -50,7 +52,7 @@ public function __construct( } /** - * Initialize creditmemo agjustment totals + * Initialize creditmemo adjustment totals * * @return $this */ diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Form.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Form.php index d2e42fe388da..ec959fc28633 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Form.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Form.php @@ -26,7 +26,7 @@ public function getOrder() /** * Retrieve source * - * @return \Magento\Sales\Model\Order\Invoice + * @return \Magento\Sales\Model\Order\Creditmemo */ public function getSource() { diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Invoice/View.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Invoice/View.php index fd6e5f403f2d..074aa99a5e79 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Invoice/View.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Invoice/View.php @@ -113,8 +113,8 @@ protected function _construct() $orderPayment->canRefund() && !$this->getInvoice()->getIsUsedForRefund() ) { $this->buttonList->add( - 'capture', - [ // capture? + 'credit-memo', + [ 'label' => __('Credit Memo'), 'class' => 'credit-memo', 'onclick' => 'setLocation(\'' . $this->getCreditMemoUrl() . '\')' diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/View/History.php b/app/code/Magento/Sales/Block/Adminhtml/Order/View/History.php index 1912655a9292..10b80b6f4e52 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/View/History.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/View/History.php @@ -88,7 +88,8 @@ public function getStatuses() */ public function canSendCommentEmail() { - return $this->_salesData->canSendOrderCommentEmail($this->getOrder()->getStore()->getId()); + return $this->_salesData->canSendOrderCommentEmail($this->getOrder()->getStore()->getId()) + && $this->_authorization->isAllowed('Magento_Sales::email'); } /** diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/View/Items/Renderer/DefaultRenderer.php b/app/code/Magento/Sales/Block/Adminhtml/Order/View/Items/Renderer/DefaultRenderer.php index 04a9f9437ae5..d19fc4992f04 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/View/Items/Renderer/DefaultRenderer.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/View/Items/Renderer/DefaultRenderer.php @@ -94,6 +94,7 @@ public function getFieldIdPrefix() * Indicate that block can display container * * @return bool + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function canDisplayContainer() { @@ -196,7 +197,7 @@ public function getMessage() /** * Retrieve save url * - * @return array + * @return string */ public function getSaveUrl() { @@ -259,9 +260,11 @@ public function displayPriceInclTax(\Magento\Framework\DataObject $item) } /** + * Retrieve rendered column html content + * * @param \Magento\Framework\DataObject|Item $item * @param string $column - * @param null $field + * @param string $field * @return string * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @since 100.1.0 @@ -301,6 +304,8 @@ public function getColumnHtml(\Magento\Framework\DataObject $item, $column, $fie } /** + * Get columns data. + * * @return array * @since 100.1.0 */ diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/View/Tab/History.php b/app/code/Magento/Sales/Block/Adminhtml/Order/View/Tab/History.php index 64b53d10d4af..0972d7431424 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/View/Tab/History.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/View/Tab/History.php @@ -61,6 +61,7 @@ public function getOrder() /** * Compose and get order full history. + * * Consists of the status history comments as well as of invoices, shipments and creditmemos creations * * @TODO This method requires refactoring. Need to create separate model for comment history handling @@ -218,7 +219,7 @@ protected function _prepareHistoryItem($label, $notified, $created, $comment = ' } /** - * {@inheritdoc} + * @inheritdoc */ public function getTabLabel() { @@ -226,7 +227,7 @@ public function getTabLabel() } /** - * {@inheritdoc} + * @inheritdoc */ public function getTabTitle() { @@ -264,7 +265,7 @@ public function getTabUrl() } /** - * {@inheritdoc} + * @inheritdoc */ public function canShowTab() { @@ -272,7 +273,7 @@ public function canShowTab() } /** - * {@inheritdoc} + * @inheritdoc */ public function isHidden() { @@ -303,11 +304,7 @@ public static function sortHistoryByTimestamp($a, $b) $createdAtA = $a['created_at']; $createdAtB = $b['created_at']; - /** @var $createdAtA \DateTime */ - if ($createdAtA->getTimestamp() == $createdAtB->getTimestamp()) { - return 0; - } - return $createdAtA->getTimestamp() < $createdAtB->getTimestamp() ? -1 : 1; + return $createdAtA->getTimestamp() <=> $createdAtB->getTimestamp(); } /** diff --git a/app/code/Magento/Sales/Block/Adminhtml/Rss/Order/Grid/Link.php b/app/code/Magento/Sales/Block/Adminhtml/Rss/Order/Grid/Link.php index 512539824da2..802ed1dc60f3 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Rss/Order/Grid/Link.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Rss/Order/Grid/Link.php @@ -7,6 +7,7 @@ /** * Class Link + * * @package Magento\Sales\Block\Adminhtml\Rss\Order\Grid */ class Link extends \Magento\Framework\View\Element\Template @@ -36,6 +37,8 @@ public function __construct( } /** + * Get url for link. + * * @return string */ public function getLink() @@ -44,6 +47,8 @@ public function getLink() } /** + * Get translatable label for link. + * * @return \Magento\Framework\Phrase */ public function getLabel() @@ -62,7 +67,9 @@ public function isRssAllowed() } /** - * @return string + * Get link type param. + * + * @return array */ protected function getLinkParams() { diff --git a/app/code/Magento/Sales/Block/Adminhtml/Totals.php b/app/code/Magento/Sales/Block/Adminhtml/Totals.php index 83b155293c2b..8172a3c0db4a 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Totals.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Totals.php @@ -5,6 +5,11 @@ */ namespace Magento\Sales\Block\Adminhtml; +use Magento\Sales\Model\Order; + +/** + * Adminhtml sales totals block + */ class Totals extends \Magento\Sales\Block\Order\Totals { /** @@ -67,12 +72,16 @@ protected function _initTotals() if (!$this->getSource()->getIsVirtual() && ((double)$this->getSource()->getShippingAmount() || $this->getSource()->getShippingDescription()) ) { + $shippingLabel = __('Shipping & Handling'); + if ($this->isFreeShipping($this->getOrder()) && $this->getSource()->getDiscountDescription()) { + $shippingLabel .= sprintf(' (%s)', $this->getSource()->getDiscountDescription()); + } $this->_totals['shipping'] = new \Magento\Framework\DataObject( [ 'code' => 'shipping', 'value' => $this->getSource()->getShippingAmount(), 'base_value' => $this->getSource()->getBaseShippingAmount(), - 'label' => __('Shipping & Handling'), + 'label' => $shippingLabel, ] ); } @@ -109,4 +118,23 @@ protected function _initTotals() return $this; } + + /** + * Availability of free shipping in at least one order item + * + * @param Order $order + * @return bool + */ + private function isFreeShipping(Order $order): bool + { + $isFreeShipping = false; + foreach ($order->getItems() as $orderItem) { + if ($orderItem->getFreeShipping() == '1') { + $isFreeShipping = true; + break; + } + } + + return $isFreeShipping; + } } diff --git a/app/code/Magento/Sales/Block/Order/Email/Invoice/Items.php b/app/code/Magento/Sales/Block/Order/Email/Invoice/Items.php index a5785a19cc66..bc7756816d32 100644 --- a/app/code/Magento/Sales/Block/Order/Email/Invoice/Items.php +++ b/app/code/Magento/Sales/Block/Order/Email/Invoice/Items.php @@ -4,14 +4,11 @@ * See COPYING.txt for license details. */ -/** - * Sales Order Email Invoice items - * - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Sales\Block\Order\Email\Invoice; /** + * Sales Order Email Invoice items + * * @api * @since 100.0.2 */ @@ -21,7 +18,7 @@ class Items extends \Magento\Sales\Block\Items\AbstractItems * Prepare item before output * * @param \Magento\Framework\View\Element\AbstractBlock $renderer - * @return \Magento\Sales\Block\Items\AbstractItems + * @return void */ protected function _prepareItem(\Magento\Framework\View\Element\AbstractBlock $renderer) { diff --git a/app/code/Magento/Sales/Block/Order/Email/Shipment/Items.php b/app/code/Magento/Sales/Block/Order/Email/Shipment/Items.php index 21c83e55a489..a4c9a7b80a00 100644 --- a/app/code/Magento/Sales/Block/Order/Email/Shipment/Items.php +++ b/app/code/Magento/Sales/Block/Order/Email/Shipment/Items.php @@ -4,14 +4,11 @@ * See COPYING.txt for license details. */ -/** - * Sales Order Email Shipment items - * - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Sales\Block\Order\Email\Shipment; /** + * Sales Order Email Shipment items + * * @api * @since 100.0.2 */ @@ -21,7 +18,7 @@ class Items extends \Magento\Sales\Block\Items\AbstractItems * Prepare item before output * * @param \Magento\Framework\View\Element\AbstractBlock $renderer - * @return \Magento\Sales\Block\Items\AbstractItems + * @return void */ protected function _prepareItem(\Magento\Framework\View\Element\AbstractBlock $renderer) { diff --git a/app/code/Magento/Sales/Block/Order/Info/Buttons/Rss.php b/app/code/Magento/Sales/Block/Order/Info/Buttons/Rss.php index 2b84b8f1444b..626dcf2a5a47 100644 --- a/app/code/Magento/Sales/Block/Order/Info/Buttons/Rss.php +++ b/app/code/Magento/Sales/Block/Order/Info/Buttons/Rss.php @@ -46,6 +46,8 @@ public function __construct( } /** + * Get link url. + * * @return string */ public function getLink() @@ -54,6 +56,8 @@ public function getLink() } /** + * Get translatable label for url. + * * @return \Magento\Framework\Phrase */ public function getLabel() @@ -91,7 +95,10 @@ protected function getUrlKey($order) } /** - * @return string + * Get type, secure and query params for link. + * + * @return array + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ protected function getLinkParams() { diff --git a/app/code/Magento/Sales/Block/Order/Item/Renderer/DefaultRenderer.php b/app/code/Magento/Sales/Block/Order/Item/Renderer/DefaultRenderer.php index 094649271174..83e66bbbce7c 100644 --- a/app/code/Magento/Sales/Block/Order/Item/Renderer/DefaultRenderer.php +++ b/app/code/Magento/Sales/Block/Order/Item/Renderer/DefaultRenderer.php @@ -48,6 +48,8 @@ public function __construct( } /** + * Set item. + * * @param \Magento\Framework\DataObject $item * @return $this */ @@ -58,6 +60,8 @@ public function setItem(\Magento\Framework\DataObject $item) } /** + * Get item. + * * @return array|null */ public function getItem() @@ -76,6 +80,8 @@ public function getOrder() } /** + * Get order item. + * * @return array|null */ public function getOrderItem() @@ -88,6 +94,8 @@ public function getOrderItem() } /** + * Get item options. + * * @return array */ public function getItemOptions() diff --git a/app/code/Magento/Sales/Block/Order/Items.php b/app/code/Magento/Sales/Block/Order/Items.php index 028544cd5621..d7255a24aead 100644 --- a/app/code/Magento/Sales/Block/Order/Items.php +++ b/app/code/Magento/Sales/Block/Order/Items.php @@ -5,13 +5,13 @@ */ /** - * Sales order view items block - * * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Sales\Block\Order; /** + * Sales order view items block. + * * @api * @since 100.0.2 */ @@ -71,7 +71,6 @@ protected function _prepareLayout() $this->itemCollection = $this->itemCollectionFactory->create(); $this->itemCollection->setOrderFilter($this->getOrder()); - $this->itemCollection->filterByParent(null); /** @var \Magento\Theme\Block\Html\Pager $pagerBlock */ $pagerBlock = $this->getChildBlock('sales_order_item_pager'); @@ -87,8 +86,9 @@ protected function _prepareLayout() } /** - * Determine if the pager should be displayed for order items list - * To be called from templates(after _prepareLayout()) + * Determine if the pager should be displayed for order items list. + * + * To be called from templates(after _prepareLayout()). * * @return bool * @since 100.1.7 @@ -101,7 +101,8 @@ public function isPagerDisplayed() /** * Get visible items for current page. - * To be called from templates(after _prepareLayout()) + * + * To be called from templates(after _prepareLayout()). * * @return \Magento\Framework\DataObject[] * @since 100.1.7 @@ -112,8 +113,9 @@ public function getItems() } /** - * Get pager HTML according to our requirements - * To be called from templates(after _prepareLayout()) + * Get pager HTML according to our requirements. + * + * To be called from templates(after _prepareLayout()). * * @return string HTML output * @since 100.1.7 diff --git a/app/code/Magento/Sales/Block/Order/PrintShipment.php b/app/code/Magento/Sales/Block/Order/PrintShipment.php index 335b6095d0ca..0006a38f0f1c 100644 --- a/app/code/Magento/Sales/Block/Order/PrintShipment.php +++ b/app/code/Magento/Sales/Block/Order/PrintShipment.php @@ -53,6 +53,8 @@ public function __construct( } /** + * Preparing global layout. + * * @return void */ protected function _prepareLayout() @@ -63,6 +65,8 @@ protected function _prepareLayout() } /** + * Get payment info child block html. + * * @return string */ public function getPaymentInfoHtml() @@ -71,6 +75,8 @@ public function getPaymentInfoHtml() } /** + * Retrieve current order from registry. + * * @return \Magento\Sales\Model\Order|null */ public function getOrder() @@ -104,6 +110,8 @@ public function getItems() } /** + * Prepare item before output. + * * @param AbstractBlock $renderer * @return $this */ @@ -116,7 +124,7 @@ protected function _prepareItem(AbstractBlock $renderer) /** * Returns string with formatted address * - * @param Address $address + * @param \Magento\Sales\Model\Order\Address $address * @return null|string */ public function getFormattedAddress(\Magento\Sales\Model\Order\Address $address) diff --git a/app/code/Magento/Sales/Block/Status/Grid/Column/Unassign.php b/app/code/Magento/Sales/Block/Status/Grid/Column/Unassign.php index b413951d9d4f..f989deb0ae7c 100644 --- a/app/code/Magento/Sales/Block/Status/Grid/Column/Unassign.php +++ b/app/code/Magento/Sales/Block/Status/Grid/Column/Unassign.php @@ -5,12 +5,35 @@ */ namespace Magento\Sales\Block\Status\Grid\Column; +use Magento\Framework\App\ObjectManager; +use \Magento\Backend\Block\Template\Context; +use Magento\Framework\Serialize\Serializer\Json; + /** * @api * @since 100.0.2 */ class Unassign extends \Magento\Backend\Block\Widget\Grid\Column { + /** + * @var Json + */ + private $json; + + /** + * @inheritDoc + * + * @param Json|null $json + */ + public function __construct( + Context $context, + array $data = [], + ?Json $json = null + ) { + parent::__construct($context, $data); + $this->json = $json ?? ObjectManager::getInstance()->get(Json::class); + } + /** * Add decorated action to column * @@ -36,9 +59,16 @@ public function decorateAction($value, $row, $column, $isExport) $cell = ''; $state = $row->getState(); if (!empty($state)) { - $url = $this->getUrl('*/*/unassign', ['status' => $row->getStatus(), 'state' => $row->getState()]); + $url = $this->getUrl('*/*/unassign'); $label = __('Unassign'); - $cell = '<a href="' . $url . '">' . $label . '</a>'; + $cell = '<a href="#" data-post="' + .$this->escapeHtmlAttr( + $this->json->serialize([ + 'action' => $url, + 'data' => ['status' => $row->getStatus(), 'state' => $row->getState()] + ]) + ) + .'">' . $label . '</a>'; } return $cell; } diff --git a/app/code/Magento/Sales/Controller/AbstractController/Reorder.php b/app/code/Magento/Sales/Controller/AbstractController/Reorder.php index dd4d73930cc8..65cb537e89fe 100644 --- a/app/code/Magento/Sales/Controller/AbstractController/Reorder.php +++ b/app/code/Magento/Sales/Controller/AbstractController/Reorder.php @@ -1,15 +1,21 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Sales\Controller\AbstractController; use Magento\Framework\App\Action; use Magento\Framework\Registry; +use Magento\Framework\App\Action\HttpPostActionInterface; -abstract class Reorder extends Action\Action +/** + * Abstract class for controllers Reorder(Customer) and Reorder(Guest) + * + * @package Magento\Sales\Controller\AbstractController + */ +abstract class Reorder extends Action\Action implements HttpPostActionInterface { /** * @var \Magento\Sales\Controller\AbstractController\OrderLoaderInterface diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Creditmemo/Index.php b/app/code/Magento/Sales/Controller/Adminhtml/Creditmemo/Index.php index b29c1ea3700e..165c9e894de7 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Creditmemo/Index.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Creditmemo/Index.php @@ -5,7 +5,10 @@ */ namespace Magento\Sales\Controller\Adminhtml\Creditmemo; -class Index extends \Magento\Sales\Controller\Adminhtml\Creditmemo\AbstractCreditmemo\Index +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; +use Magento\Sales\Controller\Adminhtml\Creditmemo\AbstractCreditmemo\Index as AbstractIndex; + +class Index extends AbstractIndex implements HttpGetActionInterface { /** * Index page diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Invoice/Index.php b/app/code/Magento/Sales/Controller/Adminhtml/Invoice/Index.php index 429a388d9392..bc53f752801b 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Invoice/Index.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Invoice/Index.php @@ -6,6 +6,8 @@ */ namespace Magento\Sales\Controller\Adminhtml\Invoice; -class Index extends \Magento\Sales\Controller\Adminhtml\Invoice\AbstractInvoice\Index +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\Sales\Controller\Adminhtml\Invoice\AbstractInvoice\Index implements HttpGetActionInterface { } diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Invoice/View.php b/app/code/Magento/Sales/Controller/Adminhtml/Invoice/View.php index 62add99ab597..3b5219994323 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Invoice/View.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Invoice/View.php @@ -6,6 +6,8 @@ */ namespace Magento\Sales\Controller\Adminhtml\Invoice; -class View extends \Magento\Sales\Controller\Adminhtml\Invoice\AbstractInvoice\View +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class View extends \Magento\Sales\Controller\Adminhtml\Invoice\AbstractInvoice\View implements HttpGetActionInterface { } diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/AddComment.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/AddComment.php index 07716314ec6e..e85083a50d72 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/AddComment.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/AddComment.php @@ -1,16 +1,17 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Sales\Controller\Adminhtml\Order; -use Magento\Backend\App\Action; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Sales\Model\Order\Email\Sender\OrderCommentSender; -class AddComment extends \Magento\Sales\Controller\Adminhtml\Order +/** + * Class AddComment + */ +class AddComment extends \Magento\Sales\Controller\Adminhtml\Order implements HttpPostActionInterface { /** * Authorization level of a basic admin session @@ -19,6 +20,11 @@ class AddComment extends \Magento\Sales\Controller\Adminhtml\Order */ const ADMIN_RESOURCE = 'Magento_Sales::comment'; + /** + * ACL resource needed to send comment email notification + */ + const ADMIN_SALES_EMAIL_RESOURCE = 'Magento_Sales::emails'; + /** * Add order comment action * @@ -36,8 +42,12 @@ public function execute() ); } - $notify = isset($data['is_customer_notified']) ? $data['is_customer_notified'] : false; - $visible = isset($data['is_visible_on_front']) ? $data['is_visible_on_front'] : false; + $notify = $data['is_customer_notified'] ?? false; + $visible = $data['is_visible_on_front'] ?? false; + + if ($notify && !$this->_authorization->isAllowed(self::ADMIN_SALES_EMAIL_RESOURCE)) { + $notify = false; + } $history = $order->addStatusHistoryComment($data['comment'], $data['status']); $history->setIsVisibleOnFront($visible); diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Cancel.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Cancel.php index 7e41c7417b38..f96d221a7d3b 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Cancel.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Cancel.php @@ -6,7 +6,9 @@ */ namespace Magento\Sales\Controller\Adminhtml\Order; -class Cancel extends \Magento\Sales\Controller\Adminhtml\Order +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; + +class Cancel extends \Magento\Sales\Controller\Adminhtml\Order implements HttpPostActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/CommentsHistory.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/CommentsHistory.php index 8496c4f28d8b..53ebc26d132a 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/CommentsHistory.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/CommentsHistory.php @@ -6,16 +6,20 @@ */ namespace Magento\Sales\Controller\Adminhtml\Order; +use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Backend\App\Action; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Sales\Api\OrderManagementInterface; use Magento\Sales\Api\OrderRepositoryInterface; use Psr\Log\LoggerInterface; +use Magento\Sales\Controller\Adminhtml\Order as OrderAction; /** - * Class CommentsHistory + * Comments History tab, needs to be accessible by POST because of tabs mechanism. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class CommentsHistory extends \Magento\Sales\Controller\Adminhtml\Order +class CommentsHistory extends OrderAction implements HttpGetActionInterface, HttpPostActionInterface { /** * @var \Magento\Framework\View\LayoutFactory diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/Index.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/Index.php index ce3a36729de9..603aa2586b05 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/Index.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/Index.php @@ -5,12 +5,17 @@ */ namespace Magento\Sales\Controller\Adminhtml\Order\Create; -class Index extends \Magento\Sales\Controller\Adminhtml\Order\Create +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +/** + * Order create index page controller. + */ +class Index extends \Magento\Sales\Controller\Adminhtml\Order\Create implements HttpGetActionInterface { /** * Index page * - * @return void + * @return \Magento\Backend\Model\View\Result\Page */ public function execute() { diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/LoadBlock.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/LoadBlock.php index bc4eb3cfba42..1e13e282cae3 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/LoadBlock.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/LoadBlock.php @@ -5,12 +5,15 @@ */ namespace Magento\Sales\Controller\Adminhtml\Order\Create; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Backend\App\Action; use Magento\Backend\Model\View\Result\ForwardFactory; use Magento\Framework\View\Result\PageFactory; use Magento\Framework\Controller\Result\RawFactory; +use Magento\Sales\Controller\Adminhtml\Order\Create as CreateAction; -class LoadBlock extends \Magento\Sales\Controller\Adminhtml\Order\Create +class LoadBlock extends CreateAction implements HttpPostActionInterface, HttpGetActionInterface { /** * @var RawFactory diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/Save.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/Save.php index 929e23075e07..4348984d0403 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/Save.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/Save.php @@ -6,9 +6,10 @@ namespace Magento\Sales\Controller\Adminhtml\Order\Create; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\Exception\PaymentException; -class Save extends \Magento\Sales\Controller\Adminhtml\Order\Create +class Save extends \Magento\Sales\Controller\Adminhtml\Order\Create implements HttpPostActionInterface { /** * Saving quote and create order diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/ShowUpdateResult.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/ShowUpdateResult.php index 01119c3aa88a..24aedf56ec5a 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/ShowUpdateResult.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/ShowUpdateResult.php @@ -5,12 +5,13 @@ */ namespace Magento\Sales\Controller\Adminhtml\Order\Create; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Backend\App\Action; use Magento\Backend\Model\View\Result\ForwardFactory; use Magento\Framework\View\Result\PageFactory; use Magento\Framework\Controller\Result\RawFactory; -class ShowUpdateResult extends \Magento\Sales\Controller\Adminhtml\Order\Create +class ShowUpdateResult extends \Magento\Sales\Controller\Adminhtml\Order\Create implements HttpGetActionInterface { /** * @var RawFactory diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/Start.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/Start.php index ee88b3361cc8..bc75bc4a5787 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/Start.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/Start.php @@ -5,9 +5,10 @@ */ namespace Magento\Sales\Controller\Adminhtml\Order\Create; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Backend\App\Action; -class Start extends \Magento\Sales\Controller\Adminhtml\Order\Create +class Start extends \Magento\Sales\Controller\Adminhtml\Order\Create implements HttpGetActionInterface { /** * Start order create action diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/NewAction.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/NewAction.php index 03910f2ecccf..3ac3abda82cd 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/NewAction.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/NewAction.php @@ -5,9 +5,10 @@ */ namespace Magento\Sales\Controller\Adminhtml\Order\Creditmemo; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Backend\App\Action; -class NewAction extends \Magento\Backend\App\Action +class NewAction extends \Magento\Backend\App\Action implements HttpGetActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/Save.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/Save.php index 2a2445d2b2ae..91ae3b7d4e05 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/Save.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/Save.php @@ -5,11 +5,12 @@ */ namespace Magento\Sales\Controller\Adminhtml\Order\Creditmemo; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Backend\App\Action; use Magento\Sales\Model\Order; use Magento\Sales\Model\Order\Email\Sender\CreditmemoSender; -class Save extends \Magento\Backend\App\Action +class Save extends \Magento\Backend\App\Action implements HttpPostActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/Start.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/Start.php index 10ba7e425b65..3dc4aa6dcd50 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/Start.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/Start.php @@ -5,7 +5,9 @@ */ namespace Magento\Sales\Controller\Adminhtml\Order\Creditmemo; -class Start extends \Magento\Backend\App\Action +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Start extends \Magento\Backend\App\Action implements HttpGetActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/UpdateQty.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/UpdateQty.php index bfd95666e7c4..d49fa8b8dc60 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/UpdateQty.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/UpdateQty.php @@ -5,9 +5,10 @@ */ namespace Magento\Sales\Controller\Adminhtml\Order\Creditmemo; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Backend\App\Action; -class UpdateQty extends \Magento\Backend\App\Action +class UpdateQty extends \Magento\Backend\App\Action implements HttpPostActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Index.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Index.php index 13a86befec0a..d5dc11719237 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Index.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Index.php @@ -5,7 +5,9 @@ */ namespace Magento\Sales\Controller\Adminhtml\Order; -class Index extends \Magento\Sales\Controller\Adminhtml\Order +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\Sales\Controller\Adminhtml\Order implements HttpGetActionInterface { /** * Orders grid diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/NewAction.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/NewAction.php index 2d7826807a6c..ceb231248ef5 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/NewAction.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/NewAction.php @@ -1,18 +1,22 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Sales\Controller\Adminhtml\Order\Invoice; +use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Backend\App\Action; use Magento\Framework\Registry; use Magento\Framework\View\Result\PageFactory; +use Magento\Sales\Api\OrderRepositoryInterface; use Magento\Sales\Model\Service\InvoiceService; -class NewAction extends \Magento\Backend\App\Action +/** + * Create new invoice action. + */ +class NewAction extends \Magento\Backend\App\Action implements HttpGetActionInterface { /** * Authorization level of a basic admin session @@ -36,22 +40,32 @@ class NewAction extends \Magento\Backend\App\Action */ private $invoiceService; + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + /** * @param Action\Context $context * @param Registry $registry * @param PageFactory $resultPageFactory * @param InvoiceService $invoiceService + * @param OrderRepositoryInterface|null $orderRepository */ public function __construct( Action\Context $context, Registry $registry, PageFactory $resultPageFactory, - InvoiceService $invoiceService + InvoiceService $invoiceService, + OrderRepositoryInterface $orderRepository = null ) { + parent::__construct($context); + $this->registry = $registry; $this->resultPageFactory = $resultPageFactory; - parent::__construct($context); $this->invoiceService = $invoiceService; + $this->orderRepository = $orderRepository ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(OrderRepositoryInterface::class); } /** @@ -77,14 +91,11 @@ public function execute() { $orderId = $this->getRequest()->getParam('order_id'); $invoiceData = $this->getRequest()->getParam('invoice', []); - $invoiceItems = isset($invoiceData['items']) ? $invoiceData['items'] : []; + $invoiceItems = $invoiceData['items'] ?? []; try { /** @var \Magento\Sales\Model\Order $order */ - $order = $this->_objectManager->create(\Magento\Sales\Model\Order::class)->load($orderId); - if (!$order->getId()) { - throw new \Magento\Framework\Exception\LocalizedException(__('The order no longer exists.')); - } + $order = $this->orderRepository->get($orderId); if (!$order->canInvoice()) { throw new \Magento\Framework\Exception\LocalizedException( diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/Save.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/Save.php index 71338c818c41..67a0dc469163 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/Save.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/Save.php @@ -7,6 +7,7 @@ namespace Magento\Sales\Controller\Adminhtml\Order\Invoice; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Backend\App\Action; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Registry; @@ -17,9 +18,11 @@ use Magento\Sales\Model\Service\InvoiceService; /** + * Save invoice controller. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Save extends \Magento\Backend\App\Action +class Save extends \Magento\Backend\App\Action implements HttpPostActionInterface { /** * Authorization level of a basic admin session @@ -102,6 +105,7 @@ protected function _prepareShipment($invoice) /** * Save invoice + * * We can save only new invoice. Existing invoices are not editable * * @return \Magento\Framework\Controller\ResultInterface @@ -193,12 +197,6 @@ public function execute() } $transactionSave->save(); - if (!empty($data['do_shipment'])) { - $this->messageManager->addSuccessMessage(__('You created the invoice and shipment.')); - } else { - $this->messageManager->addSuccessMessage(__('The invoice has been created.')); - } - // send invoice/shipment emails try { if (!empty($data['send_email'])) { @@ -218,6 +216,11 @@ public function execute() $this->messageManager->addErrorMessage(__('We can\'t send the shipment right now.')); } } + if (!empty($data['do_shipment'])) { + $this->messageManager->addSuccessMessage(__('You created the invoice and shipment.')); + } else { + $this->messageManager->addSuccessMessage(__('The invoice has been created.')); + } $this->_objectManager->get(\Magento\Backend\Model\Session::class)->getCommentText(true); return $resultRedirect->setPath('sales/order/view', ['order_id' => $orderId]); } catch (LocalizedException $e) { diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/Start.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/Start.php index ca97ee3b3349..4648656a3253 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/Start.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/Start.php @@ -6,7 +6,9 @@ */ namespace Magento\Sales\Controller\Adminhtml\Order\Invoice; -class Start extends \Magento\Sales\Controller\Adminhtml\Invoice\AbstractInvoice\View +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Start extends \Magento\Sales\Controller\Adminhtml\Invoice\AbstractInvoice\View implements HttpGetActionInterface { /** * Start create invoice action diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/UpdateQty.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/UpdateQty.php index 78fb4aff3f27..c94afa3cc805 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/UpdateQty.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/UpdateQty.php @@ -7,20 +7,21 @@ namespace Magento\Sales\Controller\Adminhtml\Order\Invoice; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\Exception\LocalizedException; -use Magento\Backend\App\Action; use Magento\Framework\Controller\Result\JsonFactory; use Magento\Framework\View\Result\PageFactory; use Magento\Framework\Controller\Result\RawFactory; use Magento\Backend\App\Action\Context; use Magento\Framework\Registry; use Magento\Sales\Model\Service\InvoiceService; +use Magento\Sales\Controller\Adminhtml\Invoice\AbstractInvoice\View as AbstractView; /** * Class UpdateQty * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class UpdateQty extends \Magento\Sales\Controller\Adminhtml\Invoice\AbstractInvoice\View +class UpdateQty extends AbstractView implements HttpPostActionInterface { /** * @var JsonFactory diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/View.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/View.php index 78a413d5636e..da700aae2f78 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/View.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/View.php @@ -6,11 +6,12 @@ */ namespace Magento\Sales\Controller\Adminhtml\Order\Invoice; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Framework\View\Result\PageFactory; use Magento\Backend\App\Action\Context; use Magento\Framework\Registry; -class View extends \Magento\Sales\Controller\Adminhtml\Invoice\AbstractInvoice\View +class View extends \Magento\Sales\Controller\Adminhtml\Invoice\AbstractInvoice\View implements HttpGetActionInterface { /** diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/MassCancel.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/MassCancel.php index 32cf6dbfc7c3..deb20a989a26 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/MassCancel.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/MassCancel.php @@ -5,13 +5,14 @@ */ namespace Magento\Sales\Controller\Adminhtml\Order; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection; use Magento\Backend\App\Action\Context; use Magento\Ui\Component\MassAction\Filter; use Magento\Sales\Model\ResourceModel\Order\CollectionFactory; use Magento\Sales\Api\OrderManagementInterface; -class MassCancel extends \Magento\Sales\Controller\Adminhtml\Order\AbstractMassAction +class MassCancel extends \Magento\Sales\Controller\Adminhtml\Order\AbstractMassAction implements HttpPostActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/MassHold.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/MassHold.php index 241255f68e39..5c96fbbe9e24 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/MassHold.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/MassHold.php @@ -5,6 +5,7 @@ */ namespace Magento\Sales\Controller\Adminhtml\Order; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection; use Magento\Backend\App\Action\Context; use Magento\Ui\Component\MassAction\Filter; @@ -14,7 +15,7 @@ /** * Class MassHold */ -class MassHold extends \Magento\Sales\Controller\Adminhtml\Order\AbstractMassAction +class MassHold extends \Magento\Sales\Controller\Adminhtml\Order\AbstractMassAction implements HttpPostActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/MassUnhold.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/MassUnhold.php index 5e8ea9c4ad07..e862710379a0 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/MassUnhold.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/MassUnhold.php @@ -5,13 +5,19 @@ */ namespace Magento\Sales\Controller\Adminhtml\Order; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection; use Magento\Backend\App\Action\Context; use Magento\Ui\Component\MassAction\Filter; use Magento\Sales\Model\ResourceModel\Order\CollectionFactory; use Magento\Sales\Api\OrderManagementInterface; -class MassUnhold extends AbstractMassAction +/** + * Class MassUnhold, change status for select orders + * + * @package Magento\Sales\Controller\Adminhtml\Order + */ +class MassUnhold extends AbstractMassAction implements HttpPostActionInterface { /** * Authorization level of a basic admin session @@ -24,6 +30,8 @@ class MassUnhold extends AbstractMassAction private $orderManagement; /** + * Class constructor + * * @param Context $context * @param Filter $filter * @param CollectionFactory $collectionFactory diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/Assign.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/Assign.php index 982ee53cb0ff..211ded865b4e 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/Assign.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/Assign.php @@ -6,11 +6,12 @@ */ namespace Magento\Sales\Controller\Adminhtml\Order\Status; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Framework\Registry; use Magento\Backend\App\Action\Context; use Magento\Framework\View\Result\PageFactory; -class Assign extends \Magento\Sales\Controller\Adminhtml\Order\Status +class Assign extends \Magento\Sales\Controller\Adminhtml\Order\Status implements HttpGetActionInterface { /** * @var PageFactory diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/AssignPost.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/AssignPost.php index 3b98d206d5f6..28a8e1cb02a1 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/AssignPost.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/AssignPost.php @@ -6,7 +6,9 @@ */ namespace Magento\Sales\Controller\Adminhtml\Order\Status; -class AssignPost extends \Magento\Sales\Controller\Adminhtml\Order\Status +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; + +class AssignPost extends \Magento\Sales\Controller\Adminhtml\Order\Status implements HttpPostActionInterface { /** * Save status assignment to state diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/Index.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/Index.php index 110402ee2707..3755dca9da95 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/Index.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/Index.php @@ -6,11 +6,12 @@ */ namespace Magento\Sales\Controller\Adminhtml\Order\Status; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Framework\Registry; use Magento\Backend\App\Action\Context; use Magento\Framework\View\Result\PageFactory; -class Index extends \Magento\Sales\Controller\Adminhtml\Order\Status +class Index extends \Magento\Sales\Controller\Adminhtml\Order\Status implements HttpGetActionInterface { /** * @var PageFactory diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/Save.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/Save.php index bd0ad771815b..4645588a7522 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/Save.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/Save.php @@ -6,25 +6,32 @@ */ namespace Magento\Sales\Controller\Adminhtml\Order\Status; -class Save extends \Magento\Sales\Controller\Adminhtml\Order\Status +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; +use Magento\Framework\Filter\FilterManager; +use Magento\Sales\Model\Order\Status; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Controller\Result\Redirect; +use Magento\Sales\Controller\Adminhtml\Order\Status as StatusAction; + +class Save extends StatusAction implements HttpPostActionInterface { /** * Save status form processing * - * @return \Magento\Backend\Model\View\Result\Redirect + * @return Redirect */ public function execute() { $data = $this->getRequest()->getPostValue(); $isNew = $this->getRequest()->getParam('is_new'); - /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ + /** @var Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); if ($data) { $statusCode = $this->getRequest()->getParam('status'); //filter tags in labels/status - /** @var $filterManager \Magento\Framework\Filter\FilterManager */ - $filterManager = $this->_objectManager->get(\Magento\Framework\Filter\FilterManager::class); + /** @var $filterManager FilterManager */ + $filterManager = $this->_objectManager->get(FilterManager::class); if ($isNew) { $statusCode = $data['status'] = $filterManager->stripTags($data['status']); } @@ -37,7 +44,7 @@ public function execute() $label = $filterManager->stripTags($label); } - $status = $this->_objectManager->create(\Magento\Sales\Model\Order\Status::class)->load($statusCode); + $status = $this->_objectManager->create(Status::class)->load($statusCode); // check if status exist if ($isNew && $status->getStatus()) { $this->messageManager @@ -52,7 +59,7 @@ public function execute() $status->save(); $this->messageManager->addSuccessMessage(__('You saved the order status.')); return $resultRedirect->setPath('sales/*/'); - } catch (\Magento\Framework\Exception\LocalizedException $e) { + } catch (LocalizedException $e) { $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { $this->messageManager->addExceptionMessage( @@ -67,11 +74,11 @@ public function execute() } /** - * @param \Magento\Backend\Model\View\Result\Redirect $resultRedirect + * @param Redirect $resultRedirect * @param bool $isNew - * @return \Magento\Backend\Model\View\Result\Redirect + * @return Redirect */ - private function getRedirect(\Magento\Backend\Model\View\Result\Redirect $resultRedirect, $isNew) + private function getRedirect(Redirect $resultRedirect, $isNew) { if ($isNew) { return $resultRedirect->setPath('sales/*/new'); diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/Unassign.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/Unassign.php index 2723d483dd38..16bb275a88ec 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/Unassign.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Status/Unassign.php @@ -6,7 +6,9 @@ */ namespace Magento\Sales\Controller\Adminhtml\Order\Status; -class Unassign extends \Magento\Sales\Controller\Adminhtml\Order\Status +use Magento\Framework\App\Action\HttpPostActionInterface; + +class Unassign extends \Magento\Sales\Controller\Adminhtml\Order\Status implements HttpPostActionInterface { /** * @return \Magento\Backend\Model\View\Result\Redirect diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Shipment/Index.php b/app/code/Magento/Sales/Controller/Adminhtml/Shipment/Index.php index c7f560c52fb6..59d8a68a7446 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Shipment/Index.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Shipment/Index.php @@ -6,6 +6,9 @@ */ namespace Magento\Sales\Controller\Adminhtml\Shipment; -class Index extends \Magento\Sales\Controller\Adminhtml\Shipment\AbstractShipment\Index +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; +use Magento\Sales\Controller\Adminhtml\Shipment\AbstractShipment\Index as AbstractIndex; + +class Index extends AbstractIndex implements HttpGetActionInterface { } diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Transactions/Index.php b/app/code/Magento/Sales/Controller/Adminhtml/Transactions/Index.php index 750781e99197..a7327050064a 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Transactions/Index.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Transactions/Index.php @@ -6,9 +6,10 @@ */ namespace Magento\Sales\Controller\Adminhtml\Transactions; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Backend\Model\View\Result\Page; -class Index extends \Magento\Sales\Controller\Adminhtml\Transactions +class Index extends \Magento\Sales\Controller\Adminhtml\Transactions implements HttpGetActionInterface { /** * @return Page diff --git a/app/code/Magento/Sales/Controller/Download/DownloadCustomOption.php b/app/code/Magento/Sales/Controller/Download/DownloadCustomOption.php index 0deddd9fb6ec..fc4e238d47c9 100644 --- a/app/code/Magento/Sales/Controller/Download/DownloadCustomOption.php +++ b/app/code/Magento/Sales/Controller/Download/DownloadCustomOption.php @@ -4,9 +4,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Sales\Controller\Download; +use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\App\Action\Context; use Magento\Catalog\Model\Product\Type\AbstractType; @@ -14,9 +16,10 @@ /** * Class DownloadCustomOption + * * @package Magento\Sales\Controller\Download */ -class DownloadCustomOption extends \Magento\Framework\App\Action\Action +class DownloadCustomOption extends \Magento\Framework\App\Action\Action implements HttpGetActionInterface { /** * @var ForwardFactory @@ -95,10 +98,11 @@ public function execute() /** @var $productOption \Magento\Catalog\Model\Product\Option */ $productOption = $this->_objectManager->create( \Magento\Catalog\Model\Product\Option::class - )->load($optionId); + ); + $productOption->load($optionId); } - if (!$productOption || !$productOption->getId() || $productOption->getType() != 'file') { + if ($productOption->getId() && $productOption->getType() != 'file') { return $resultForward->forward('noroute'); } @@ -118,10 +122,10 @@ public function execute() * Ends execution process * * @return void - * @SuppressWarnings(PHPMD.ExitExpression) */ protected function endExecute() { + // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage exit(0); } } diff --git a/app/code/Magento/Sales/Controller/Guest/Form.php b/app/code/Magento/Sales/Controller/Guest/Form.php index 8b4467cb538f..04bb66f3d5b6 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/Controller/Order/Creditmemo.php b/app/code/Magento/Sales/Controller/Order/Creditmemo.php index 2a7fd3e9849e..74aae3e1f841 100644 --- a/app/code/Magento/Sales/Controller/Order/Creditmemo.php +++ b/app/code/Magento/Sales/Controller/Order/Creditmemo.php @@ -6,8 +6,10 @@ */ namespace Magento\Sales\Controller\Order; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Sales\Controller\OrderInterface; +use Magento\Sales\Controller\AbstractController\Creditmemo as AbstractCreditmemo; -class Creditmemo extends \Magento\Sales\Controller\AbstractController\Creditmemo implements OrderInterface +class Creditmemo extends AbstractCreditmemo implements OrderInterface, HttpGetActionInterface { } diff --git a/app/code/Magento/Sales/Controller/Order/History.php b/app/code/Magento/Sales/Controller/Order/History.php index 23e8208d8b19..37de15984585 100644 --- a/app/code/Magento/Sales/Controller/Order/History.php +++ b/app/code/Magento/Sales/Controller/Order/History.php @@ -6,11 +6,12 @@ */ namespace Magento\Sales\Controller\Order; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Sales\Controller\OrderInterface; use Magento\Framework\App\Action\Context; use Magento\Framework\View\Result\PageFactory; -class History extends \Magento\Framework\App\Action\Action implements OrderInterface +class History extends \Magento\Framework\App\Action\Action implements OrderInterface, HttpGetActionInterface { /** * @var PageFactory diff --git a/app/code/Magento/Sales/Controller/Order/View.php b/app/code/Magento/Sales/Controller/Order/View.php index d68b9481c861..21a5706a4448 100644 --- a/app/code/Magento/Sales/Controller/Order/View.php +++ b/app/code/Magento/Sales/Controller/Order/View.php @@ -6,8 +6,9 @@ */ namespace Magento\Sales\Controller\Order; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Sales\Controller\OrderInterface; -class View extends \Magento\Sales\Controller\AbstractController\View implements OrderInterface +class View extends \Magento\Sales\Controller\AbstractController\View implements OrderInterface, HttpGetActionInterface { } diff --git a/app/code/Magento/Sales/Helper/Admin.php b/app/code/Magento/Sales/Helper/Admin.php index 45a6dd1252ba..7053a696dcab 100644 --- a/app/code/Magento/Sales/Helper/Admin.php +++ b/app/code/Magento/Sales/Helper/Admin.php @@ -3,8 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Helper; +/** + * Sales admin helper. + */ class Admin extends \Magento\Framework\App\Helper\AbstractHelper { /** @@ -148,22 +153,15 @@ public function escapeHtmlWithLinks($data, $allowedTags = null) $links = []; $i = 1; $data = str_replace('%', '%%', $data); - $regexp = "/<a\s[^>]*href\s*?=\s*?([\"\']??)([^\" >]*?)\\1[^>]*>(.*)<\/a>/siU"; + $regexp = "#(?J)<a" + ."(?:(?:\s+(?:(?:href\s*=\s*(['\"])(?<link>.*?)\\1\s*)|(?:\S+\s*=\s*(['\"])(.*?)\\3)\s*)*)|>)" + .">?(?:(?:(?<text>.*?)(?:<\/a\s*>?|(?=<\w))|(?<text>.*)))#si"; while (preg_match($regexp, $data, $matches)) { - //Revert the sprintf escaping - $url = str_replace('%%', '%', $matches[2]); - $text = str_replace('%%', '%', $matches[3]); - //Check for an valid url - if ($url) { - $urlScheme = strtolower(parse_url($url, PHP_URL_SCHEME)); - if ($urlScheme !== 'http' && $urlScheme !== 'https') { - $url = null; - } - } - //Use hash tag as fallback - if (!$url) { - $url = '#'; + $text = ''; + if (!empty($matches['text'])) { + $text = str_replace('%%', '%', $matches['text']); } + $url = $this->filterUrl($matches['link'] ?? ''); //Recreate a minimalistic secure a tag $links[] = sprintf( '<a href="%s">%s</a>', @@ -178,4 +176,29 @@ public function escapeHtmlWithLinks($data, $allowedTags = null) } return $this->escaper->escapeHtml($data, $allowedTags); } + + /** + * Filter the URL for allowed protocols. + * + * @param string $url + * @return string + */ + private function filterUrl(string $url): string + { + if ($url) { + //Revert the sprintf escaping + $url = str_replace('%%', '%', $url); + $urlScheme = parse_url($url, PHP_URL_SCHEME); + $urlScheme = $urlScheme ? strtolower($urlScheme) : ''; + if ($urlScheme !== 'http' && $urlScheme !== 'https') { + $url = null; + } + } + + if (!$url) { + $url = '#'; + } + + return $url; + } } diff --git a/app/code/Magento/Sales/Model/AdminOrder/Create.php b/app/code/Magento/Sales/Model/AdminOrder/Create.php index b690395ebab7..063433140566 100644 --- a/app/code/Magento/Sales/Model/AdminOrder/Create.php +++ b/app/code/Magento/Sales/Model/AdminOrder/Create.php @@ -14,6 +14,7 @@ use Magento\Quote\Model\Quote\Item; use Magento\Sales\Api\Data\OrderAddressInterface; use Magento\Sales\Model\Order; +use Magento\Store\Model\StoreManagerInterface; use Psr\Log\LoggerInterface; /** @@ -22,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 @@ -242,6 +244,11 @@ class Create extends \Magento\Framework\DataObject implements \Magento\Checkout\ */ private $dataObjectConverter; + /** + * @var StoreManagerInterface + */ + private $storeManager; + /** * @param \Magento\Framework\ObjectManagerInterface $objectManager * @param \Magento\Framework\Event\ManagerInterface $eventManager @@ -273,6 +280,7 @@ class Create extends \Magento\Framework\DataObject implements \Magento\Checkout\ * @param array $data * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer * @param ExtensibleDataObjectConverter|null $dataObjectConverter + * @param StoreManagerInterface $storeManager * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -305,7 +313,8 @@ public function __construct( \Magento\Quote\Model\QuoteFactory $quoteFactory, array $data = [], \Magento\Framework\Serialize\Serializer\Json $serializer = null, - ExtensibleDataObjectConverter $dataObjectConverter = null + ExtensibleDataObjectConverter $dataObjectConverter = null, + StoreManagerInterface $storeManager = null ) { $this->_objectManager = $objectManager; $this->_eventManager = $eventManager; @@ -339,6 +348,7 @@ public function __construct( parent::__construct($data); $this->dataObjectConverter = $dataObjectConverter ?: ObjectManager::getInstance() ->get(ExtensibleDataObjectConverter::class); + $this->storeManager = $storeManager ?: ObjectManager::getInstance()->get(StoreManagerInterface::class); } /** @@ -416,7 +426,8 @@ public function setRecollect($flag) /** * Recollect totals for customer cart. - * Set recollect totals flag for quote + * + * Set recollect totals flag for quote. * * @return $this */ @@ -572,6 +583,7 @@ public function initFromOrder(\Magento\Sales\Model\Order $order) } $quote->getShippingAddress()->unsCachedItemsAll(); + $quote->getBillingAddress()->unsCachedItemsAll(); $quote->setTotalsCollectedFlag(false); $this->quoteRepository->save($quote); @@ -1333,6 +1345,7 @@ protected function _createCustomerForm(\Magento\Customer\Api\Data\CustomerInterf /** * Set and validate Quote address + * * All errors added to _errors * * @param \Magento\Quote\Model\Quote\Address $address @@ -1536,6 +1549,8 @@ public function resetShippingMethod() */ public function collectShippingRates() { + $store = $this->getQuote()->getStore(); + $this->storeManager->setCurrentStore($store); $this->getQuote()->getShippingAddress()->setCollectShippingRates(true); $this->collectRates(); diff --git a/app/code/Magento/Sales/Model/CronJob/CleanExpiredOrders.php b/app/code/Magento/Sales/Model/CronJob/CleanExpiredOrders.php index 8a7bd0260df0..999bb1786cf8 100644 --- a/app/code/Magento/Sales/Model/CronJob/CleanExpiredOrders.php +++ b/app/code/Magento/Sales/Model/CronJob/CleanExpiredOrders.php @@ -3,11 +3,19 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Model\CronJob; +use Magento\Framework\App\ObjectManager; +use Magento\Sales\Api\OrderManagementInterface; +use Magento\Sales\Model\ResourceModel\Order\CollectionFactory; use Magento\Store\Model\StoresConfig; use Magento\Sales\Model\Order; +/** + * Class that provides functionality of cleaning expired quotes by cron + */ class CleanExpiredOrders { /** @@ -16,20 +24,28 @@ class CleanExpiredOrders protected $storesConfig; /** - * @var \Magento\Sales\Model\ResourceModel\Order\CollectionFactory + * @var CollectionFactory */ protected $orderCollectionFactory; + /** + * @var OrderManagementInterface + */ + private $orderManagement; + /** * @param StoresConfig $storesConfig - * @param \Magento\Sales\Model\ResourceModel\Order\CollectionFactory $collectionFactory + * @param CollectionFactory $collectionFactory + * @param OrderManagementInterface|null $orderManagement */ public function __construct( StoresConfig $storesConfig, - \Magento\Sales\Model\ResourceModel\Order\CollectionFactory $collectionFactory + CollectionFactory $collectionFactory, + OrderManagementInterface $orderManagement = null ) { $this->storesConfig = $storesConfig; $this->orderCollectionFactory = $collectionFactory; + $this->orderManagement = $orderManagement ?: ObjectManager::getInstance()->get(OrderManagementInterface::class); } /** @@ -48,8 +64,10 @@ public function execute() $orders->getSelect()->where( new \Zend_Db_Expr('TIME_TO_SEC(TIMEDIFF(CURRENT_TIMESTAMP, `updated_at`)) >= ' . $lifetime * 60) ); - $orders->walk('cancel'); - $orders->walk('save'); + + foreach ($orders->getAllIds() as $entityId) { + $this->orderManagement->cancel((int) $entityId); + } } } } diff --git a/app/code/Magento/Sales/Model/Download.php b/app/code/Magento/Sales/Model/Download.php index 6d3ad8491253..a76b72bfa41d 100644 --- a/app/code/Magento/Sales/Model/Download.php +++ b/app/code/Magento/Sales/Model/Download.php @@ -8,6 +8,9 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Exception\LocalizedException; +/** + * Class Download. Represents download logic for files + */ class Download { /** @@ -36,6 +39,8 @@ class Download protected $rootDirBasePath; /** + * Constructor method + * * @param \Magento\Framework\Filesystem $filesystem * @param \Magento\MediaStorage\Helper\File\Storage\Database $fileStorageDatabase * @param \Magento\MediaStorage\Model\File\Storage\DatabaseFactory $storageDatabaseFactory @@ -78,11 +83,14 @@ public function downloadFile($info) $this->_fileFactory->create( $info['title'], ['value' => $this->_rootDir->getRelativePath($relativePath), 'type' => 'filename'], - $this->rootDirBasePath + $this->rootDirBasePath, + $info['type'] ); } /** + * Method checks, if file can be returned depends on the given filepath + * * @param string $relativePath * @return bool */ diff --git a/app/code/Magento/Sales/Model/Order.php b/app/code/Magento/Sales/Model/Order.php index 7372d9715c72..48deddb2fe5a 100644 --- a/app/code/Magento/Sales/Model/Order.php +++ b/app/code/Magento/Sales/Model/Order.php @@ -8,19 +8,24 @@ use Magento\Directory\Model\Currency; use Magento\Framework\Api\AttributeValueFactory; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Locale\ResolverInterface; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\Data\OrderItemInterface; use Magento\Sales\Api\Data\OrderStatusHistoryInterface; use Magento\Sales\Model\Order\Payment; +use Magento\Sales\Model\Order\ProductOption; use Magento\Sales\Model\ResourceModel\Order\Address\Collection; use Magento\Sales\Model\ResourceModel\Order\Creditmemo\Collection as CreditmemoCollection; use Magento\Sales\Model\ResourceModel\Order\Invoice\Collection as InvoiceCollection; -use Magento\Sales\Model\ResourceModel\Order\Item\Collection as ImportCollection; +use Magento\Sales\Model\ResourceModel\Order\Item\Collection as ItemCollection; use Magento\Sales\Model\ResourceModel\Order\Payment\Collection as PaymentCollection; use Magento\Sales\Model\ResourceModel\Order\Shipment\Collection as ShipmentCollection; use Magento\Sales\Model\ResourceModel\Order\Shipment\Track\Collection as TrackCollection; use Magento\Sales\Model\ResourceModel\Order\Status\History\Collection as HistoryCollection; +use Magento\Sales\Api\OrderItemRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; /** * Order model @@ -279,6 +284,21 @@ class Order extends AbstractModel implements EntityInterface, OrderInterface */ private $localeResolver; + /** + * @var ProductOption + */ + private $productOption; + + /** + * @var OrderItemRepositoryInterface + */ + private $itemRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -308,6 +328,9 @@ class Order extends AbstractModel implements EntityInterface, OrderInterface * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data * @param ResolverInterface $localeResolver + * @param ProductOption|null $productOption + * @param OrderItemRepositoryInterface $itemRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -338,7 +361,10 @@ public function __construct( \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], - ResolverInterface $localeResolver = null + ResolverInterface $localeResolver = null, + ProductOption $productOption = null, + OrderItemRepositoryInterface $itemRepository = null, + SearchCriteriaBuilder $searchCriteriaBuilder = null ) { $this->_storeManager = $storeManager; $this->_orderConfig = $orderConfig; @@ -361,6 +387,11 @@ public function __construct( $this->salesOrderCollectionFactory = $salesOrderCollectionFactory; $this->priceCurrency = $priceCurrency; $this->localeResolver = $localeResolver ?: ObjectManager::getInstance()->get(ResolverInterface::class); + $this->productOption = $productOption ?: ObjectManager::getInstance()->get(ProductOption::class); + $this->itemRepository = $itemRepository ?: ObjectManager::getInstance() + ->get(OrderItemRepositoryInterface::class); + $this->searchCriteriaBuilder = $searchCriteriaBuilder ?: ObjectManager::getInstance() + ->get(SearchCriteriaBuilder::class); parent::__construct( $context, @@ -546,6 +577,7 @@ public function canCancel() break; } } + if ($allInvoiced) { return false; } @@ -564,6 +596,7 @@ public function canCancel() /** * Getter whether the payment can be voided + * * @return bool */ public function canVoidPayment() @@ -620,11 +653,8 @@ public function canCreditmemo() return $this->getForcedCanCreditmemo(); } - if ($this->canUnhold() || $this->isPaymentReview()) { - return false; - } - - if ($this->isCanceled() || $this->getState() === self::STATE_CLOSED) { + if ($this->canUnhold() || $this->isPaymentReview() || + $this->isCanceled() || $this->getState() === self::STATE_CLOSED) { return false; } @@ -634,15 +664,52 @@ public function canCreditmemo() * TotalPaid - contains amount, that were not rounded. */ $totalRefunded = $this->priceCurrency->round($this->getTotalPaid()) - $this->getTotalRefunded(); - if (abs($totalRefunded) < .0001) { - return false; + if (abs($this->getGrandTotal()) < .0001) { + return $this->canCreditmemoForZeroTotal($totalRefunded); } + + return $this->canCreditmemoForZeroTotalRefunded($totalRefunded); + } + + /** + * Retrieve credit memo for zero total refunded availability. + * + * @param float $totalRefunded + * @return bool + */ + private function canCreditmemoForZeroTotalRefunded($totalRefunded) + { + $isRefundZero = abs($totalRefunded) < .0001; // Case when Adjustment Fee (adjustment_negative) has been used for first creditmemo - if (abs($totalRefunded - $this->getAdjustmentNegative()) < .0001) { + $hasAdjustmentFee = abs($totalRefunded - $this->getAdjustmentNegative()) < .0001; + $hasActionFlag = $this->getActionFlag(self::ACTION_FLAG_EDIT) === false; + if ($isRefundZero || $hasAdjustmentFee || $hasActionFlag) { return false; } - if ($this->getActionFlag(self::ACTION_FLAG_EDIT) === false) { + return true; + } + + /** + * Retrieve credit memo for zero total availability. + * + * @param float $totalRefunded + * @return bool + */ + private function canCreditmemoForZeroTotal($totalRefunded) + { + $totalPaid = $this->getTotalPaid(); + //check if total paid is less than grandtotal + $checkAmtTotalPaid = $totalPaid <= $this->getGrandTotal(); + //case when amount is due for invoice + $hasDueAmount = $this->canInvoice() && ($checkAmtTotalPaid); + //case when paid amount is refunded and order has creditmemo created + $creditmemos = ($this->getCreditmemosCollection() === false) ? + true : (count($this->getCreditmemosCollection()) > 0); + $paidAmtIsRefunded = $this->getTotalRefunded() == $totalPaid && $creditmemos; + if (($hasDueAmount || $paidAmtIsRefunded) || + (!$checkAmtTotalPaid && + abs($totalRefunded - $this->getAdjustmentNegative()) < .0001)) { return false; } return true; @@ -719,13 +786,25 @@ public function canShip() } foreach ($this->getAllItems() as $item) { - if ($item->getQtyToShip() > 0 && !$item->getIsVirtual() && !$item->getLockedDoShip()) { + if ($item->getQtyToShip() > 0 && !$item->getIsVirtual() && + !$item->getLockedDoShip() && !$this->isRefunded($item)) { return true; } } return false; } + /** + * Check if item is refunded. + * + * @param OrderItemInterface $item + * @return bool + */ + private function isRefunded(OrderItemInterface $item) + { + return $item->getQtyRefunded() == $item->getQtyOrdered(); + } + /** * Retrieve order edit availability * @@ -880,7 +959,7 @@ protected function _placePayment() } /** - * {@inheritdoc} + * @inheritdoc */ public function getPayment() { @@ -972,7 +1051,7 @@ public function getShippingAddress() } /** - * Set order state + * @inheritdoc * * @param string $state * @return $this @@ -982,10 +1061,21 @@ public function setState($state) return $this->setData(self::STATE, $state); } + /** + * Retrieve frontend label of order status + * + * @return string + */ + public function getFrontendStatusLabel() + { + return $this->getConfig()->getStatusFrontendLabel($this->getStatus()); + } + /** * Retrieve label of order status * * @return string + * @throws LocalizedException */ public function getStatusLabel() { @@ -1007,8 +1097,9 @@ public function addStatusToHistory($status, $comment = '', $isCustomerNotified = } /** - * Add a comment to order - * Different or default status may be specified + * Add a comment to order. + * + * Different or default status may be specified. * * @param string $comment * @param bool|string $status @@ -1022,8 +1113,9 @@ public function addStatusHistoryComment($comment, $status = false) } /** - * Add a comment to order status history - * Different or default status may be specified + * Add a comment to order status history. + * + * Different or default status may be specified. * * @param string $comment * @param bool|string $status @@ -1088,13 +1180,15 @@ public function place() } /** + * Hold order + * * @return $this - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function hold() { if (!$this->canHold()) { - throw new \Magento\Framework\Exception\LocalizedException(__('A hold action is not available.')); + throw new LocalizedException(__('A hold action is not available.')); } $this->setHoldBeforeState($this->getState()); $this->setHoldBeforeStatus($this->getStatus()); @@ -1107,12 +1201,12 @@ public function hold() * Attempt to unhold the order * * @return $this - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function unhold() { if (!$this->canUnhold()) { - throw new \Magento\Framework\Exception\LocalizedException(__('You cannot remove the hold.')); + throw new LocalizedException(__('You cannot remove the hold.')); } $this->setState($this->getHoldBeforeState()) @@ -1156,7 +1250,7 @@ public function isFraudDetected() * @param string $comment * @param bool $graceful * @return $this - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function registerCancellation($comment = '', $graceful = true) @@ -1195,7 +1289,7 @@ public function registerCancellation($comment = '', $graceful = true) $this->addStatusHistoryComment($comment, false); } } elseif (!$graceful) { - throw new \Magento\Framework\Exception\LocalizedException(__('We cannot cancel this order.')); + throw new LocalizedException(__('We cannot cancel this order.')); } return $this; } @@ -1217,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); @@ -1233,6 +1327,8 @@ public function getShippingMethod($asObject = false) /*********************** ADDRESSES ***************************/ /** + * Get addresses collection + * * @return Collection */ public function getAddressesCollection() @@ -1247,6 +1343,8 @@ public function getAddressesCollection() } /** + * Get address by id + * * @param mixed $addressId * @return false */ @@ -1261,6 +1359,8 @@ public function getAddressById($addressId) } /** + * Add address + * * @param \Magento\Sales\Model\Order\Address $address * @return $this */ @@ -1275,9 +1375,11 @@ public function addAddress(\Magento\Sales\Model\Order\Address $address) } /** + * Get items collection + * * @param array $filterByTypes * @param bool $nonChildrenOnly - * @return ImportCollection + * @return ItemCollection */ public function getItemsCollection($filterByTypes = [], $nonChildrenOnly = false) { @@ -1293,6 +1395,7 @@ public function getItemsCollection($filterByTypes = [], $nonChildrenOnly = false if ($this->getId()) { foreach ($collection as $item) { $item->setOrder($this); + $this->productOption->add($item); } } return $collection; @@ -1302,7 +1405,7 @@ public function getItemsCollection($filterByTypes = [], $nonChildrenOnly = false * Get random items collection without related children * * @param int $limit - * @return ImportCollection + * @return ItemCollection */ public function getParentItemsRandomCollection($limit = 1) { @@ -1314,11 +1417,14 @@ public function getParentItemsRandomCollection($limit = 1) * * @param int $limit * @param bool $nonChildrenOnly - * @return ImportCollection + * @return ItemCollection */ protected function _getItemsRandomCollection($limit, $nonChildrenOnly = false) { - $collection = $this->_orderItemCollectionFactory->create()->setOrderFilter($this)->setRandomOrder(); + $collection = $this->_orderItemCollectionFactory->create() + ->setOrderFilter($this) + ->setRandomOrder() + ->setPageSize($limit); if ($nonChildrenOnly) { $collection->filterByParent(); @@ -1332,9 +1438,7 @@ protected function _getItemsRandomCollection($limit, $nonChildrenOnly = false) $products )->setVisibility( $this->_productVisibility->getVisibleInSiteIds() - )->addPriceData()->setPageSize( - $limit - )->load(); + )->addPriceData()->load(); foreach ($collection as $item) { $product = $productsCollection->getItemById($item->getProductId()); @@ -1347,6 +1451,8 @@ protected function _getItemsRandomCollection($limit, $nonChildrenOnly = false) } /** + * Get all items + * * @return \Magento\Sales\Model\Order\Item[] */ public function getAllItems() @@ -1361,6 +1467,8 @@ public function getAllItems() } /** + * Get all visible items + * * @return array */ public function getAllVisibleItems() @@ -1392,6 +1500,8 @@ public function getItemById($itemId) } /** + * Get item by quote item id + * * @param mixed $quoteItemId * @return \Magento\Framework\DataObject|null */ @@ -1406,6 +1516,8 @@ public function getItemByQuoteItemId($quoteItemId) } /** + * Add item + * * @param \Magento\Sales\Model\Order\Item $item * @return $this */ @@ -1421,6 +1533,8 @@ public function addItem(\Magento\Sales\Model\Order\Item $item) /*********************** PAYMENTS ***************************/ /** + * Get payments collection + * * @return PaymentCollection */ public function getPaymentsCollection() @@ -1435,6 +1549,8 @@ public function getPaymentsCollection() } /** + * Get all payments + * * @return array */ public function getAllPayments() @@ -1449,6 +1565,8 @@ public function getAllPayments() } /** + * Get payment by id + * * @param mixed $paymentId * @return Payment|false */ @@ -1463,7 +1581,7 @@ public function getPaymentById($paymentId) } /** - * {@inheritdoc} + * @inheritdoc */ public function setPayment(\Magento\Sales\Api\Data\OrderPaymentInterface $payment = null) { @@ -1530,6 +1648,8 @@ public function getVisibleStatusHistory() } /** + * Get status history by id + * * @param mixed $statusId * @return string|false */ @@ -1544,7 +1664,8 @@ public function getStatusHistoryById($statusId) } /** - * Set the order status history object and the order object to each other + * @inheritdoc + * * Adds the object to the status history collection, which is automatically saved when the order is saved. * See the entity_id attribute backend model. * Or the history record can be saved standalone after this. @@ -1564,6 +1685,8 @@ public function addStatusHistory(\Magento\Sales\Model\Order\Status\History $hist } /** + * Get real order id + * * @return string */ public function getRealOrderId() @@ -1592,9 +1715,9 @@ public function getOrderCurrency() /** * Get formatted price value including order currency rate to order website currency * - * @param float $price - * @param bool $addBrackets - * @return string + * @param float $price + * @param bool $addBrackets + * @return string */ public function formatPrice($price, $addBrackets = false) { @@ -1602,6 +1725,8 @@ public function formatPrice($price, $addBrackets = false) } /** + * Format price precision + * * @param float $price * @param int $precision * @param bool $addBrackets @@ -1615,8 +1740,8 @@ public function formatPricePrecision($price, $precision, $addBrackets = false) /** * Retrieve text formatted price value including order rate * - * @param float $price - * @return string + * @param float $price + * @return string */ public function formatPriceTxt($price) { @@ -1637,6 +1762,8 @@ public function getBaseCurrency() } /** + * Format base price + * * @param float $price * @return string */ @@ -1646,6 +1773,8 @@ public function formatBasePrice($price) } /** + * Format Base Price Precision + * * @param float $price * @param int $precision * @return string @@ -1656,6 +1785,8 @@ public function formatBasePricePrecision($price, $precision) } /** + * Is currency different + * * @return bool */ public function isCurrencyDifferent() @@ -1688,6 +1819,8 @@ public function getBaseTotalDue() } /** + * Get data + * * @param string $key * @param null|string|int $index * @return mixed @@ -1723,7 +1856,7 @@ public function getInvoiceCollection() } /** - * Set order invoices collection + * @inheritdoc * * @param InvoiceCollection $invoices * @return $this @@ -1828,6 +1961,8 @@ public function getRelatedObjects() } /** + * Get customer name + * * @return string */ public function getCustomerName() @@ -1855,8 +1990,9 @@ public function addRelatedObject(\Magento\Framework\Model\AbstractModel $object) /** * Get formatted order created date in store timezone * - * @param string $format date format type (short|medium|long|full) - * @return string + * @param int $format date format type (\IntlDateFormatter::SHORT|\IntlDateFormatter::MEDIUM + * |\IntlDateFormatter::LONG|\IntlDateFormatter::FULL) + * @return string */ public function getCreatedAtFormatted($format) { @@ -1870,6 +2006,8 @@ public function getCreatedAtFormatted($format) } /** + * Get email customer note + * * @return string */ public function getEmailCustomerNote() @@ -1881,6 +2019,8 @@ public function getEmailCustomerNote() } /** + * Get store group name + * * @return string */ public function getStoreGroupName() @@ -1893,8 +2033,7 @@ public function getStoreGroupName() } /** - * Resets all data in object - * so after another load it will be complete new object + * Reset all data in object so after another load it will be complete new object. * * @return $this */ @@ -1918,6 +2057,8 @@ public function reset() } /** + * Get order is not virtual + * * @return bool * @SuppressWarnings(PHPMD.BooleanGetMethodName) */ @@ -1948,7 +2089,7 @@ public function isCanceled() } /** - * Returns increment id + * Return increment id * * @codeCoverageIgnore * @@ -1960,21 +2101,26 @@ public function getIncrementId() } /** + * Get Items + * * @return \Magento\Sales\Api\Data\OrderItemInterface[] */ public function getItems() { if ($this->getData(OrderInterface::ITEMS) == null) { + $this->searchCriteriaBuilder->addFilter(OrderItemInterface::ORDER_ID, $this->getId()); + + $searchCriteria = $this->searchCriteriaBuilder->create(); $this->setData( OrderInterface::ITEMS, - $this->getItemsCollection()->getItems() + $this->itemRepository->getList($searchCriteria)->getItems() ); } return $this->getData(OrderInterface::ITEMS); } /** - * {@inheritdoc} + * @inheritdoc * @codeCoverageIgnore */ public function setItems($items) @@ -1983,6 +2129,8 @@ public function setItems($items) } /** + * Get addresses + * * @return \Magento\Sales\Api\Data\OrderAddressInterface[] */ public function getAddresses() @@ -1997,6 +2145,8 @@ public function getAddresses() } /** + * Get status History + * * @return \Magento\Sales\Api\Data\OrderStatusHistoryInterface[]|null */ public function getStatusHistories() @@ -2011,7 +2161,7 @@ public function getStatusHistories() } /** - * {@inheritdoc} + * @inheritdoc * * @return \Magento\Sales\Api\Data\OrderExtensionInterface|null */ @@ -2021,7 +2171,7 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * @inheritdoc * * @param \Magento\Sales\Api\Data\OrderExtensionInterface $extensionAttributes * @return $this @@ -2034,7 +2184,7 @@ public function setExtensionAttributes(\Magento\Sales\Api\Data\OrderExtensionInt //@codeCoverageIgnoreStart /** - * Returns adjustment_negative + * Return adjustment_negative * * @return float|null */ @@ -2044,7 +2194,7 @@ public function getAdjustmentNegative() } /** - * Returns adjustment_positive + * Return adjustment_positive * * @return float|null */ @@ -2054,7 +2204,9 @@ public function getAdjustmentPositive() } /** - * Returns applied_rule_ids + * Return applied_rule_ids + * + * Rules are comma separated if there are more than one. * * @return string|null */ @@ -2064,7 +2216,7 @@ public function getAppliedRuleIds() } /** - * Returns base_adjustment_negative + * Return base_adjustment_negative * * @return float|null */ @@ -2074,7 +2226,7 @@ public function getBaseAdjustmentNegative() } /** - * Returns base_adjustment_positive + * Return base_adjustment_positive * * @return float|null */ @@ -2084,7 +2236,7 @@ public function getBaseAdjustmentPositive() } /** - * Returns base_currency_code + * Return base_currency_code * * @return string|null */ @@ -2094,7 +2246,7 @@ public function getBaseCurrencyCode() } /** - * Returns base_discount_amount + * Return base_discount_amount * * @return float|null */ @@ -2104,7 +2256,7 @@ public function getBaseDiscountAmount() } /** - * Returns base_discount_canceled + * Return base_discount_canceled * * @return float|null */ @@ -2114,7 +2266,7 @@ public function getBaseDiscountCanceled() } /** - * Returns base_discount_invoiced + * Return base_discount_invoiced * * @return float|null */ @@ -2124,7 +2276,7 @@ public function getBaseDiscountInvoiced() } /** - * Returns base_discount_refunded + * Return base_discount_refunded * * @return float|null */ @@ -2134,7 +2286,7 @@ public function getBaseDiscountRefunded() } /** - * Returns base_grand_total + * Return base_grand_total * * @return float */ @@ -2144,7 +2296,7 @@ public function getBaseGrandTotal() } /** - * Returns base_discount_tax_compensation_amount + * Return base_discount_tax_compensation_amount * * @return float|null */ @@ -2154,7 +2306,7 @@ public function getBaseDiscountTaxCompensationAmount() } /** - * Returns base_discount_tax_compensation_invoiced + * Return base_discount_tax_compensation_invoiced * * @return float|null */ @@ -2164,7 +2316,7 @@ public function getBaseDiscountTaxCompensationInvoiced() } /** - * Returns base_discount_tax_compensation_refunded + * Return base_discount_tax_compensation_refunded * * @return float|null */ @@ -2174,7 +2326,7 @@ public function getBaseDiscountTaxCompensationRefunded() } /** - * Returns base_shipping_amount + * Return base_shipping_amount * * @return float|null */ @@ -2184,7 +2336,7 @@ public function getBaseShippingAmount() } /** - * Returns base_shipping_canceled + * Return base_shipping_canceled * * @return float|null */ @@ -2194,7 +2346,7 @@ public function getBaseShippingCanceled() } /** - * Returns base_shipping_discount_amount + * Return base_shipping_discount_amount * * @return float|null */ @@ -2204,7 +2356,7 @@ public function getBaseShippingDiscountAmount() } /** - * Returns base_shipping_discount_tax_compensation_amnt + * Return base_shipping_discount_tax_compensation_amnt * * @return float|null */ @@ -2214,7 +2366,7 @@ public function getBaseShippingDiscountTaxCompensationAmnt() } /** - * Returns base_shipping_incl_tax + * Return base_shipping_incl_tax * * @return float|null */ @@ -2224,7 +2376,7 @@ public function getBaseShippingInclTax() } /** - * Returns base_shipping_invoiced + * Return base_shipping_invoiced * * @return float|null */ @@ -2234,7 +2386,7 @@ public function getBaseShippingInvoiced() } /** - * Returns base_shipping_refunded + * Return base_shipping_refunded * * @return float|null */ @@ -2244,7 +2396,7 @@ public function getBaseShippingRefunded() } /** - * Returns base_shipping_tax_amount + * Return base_shipping_tax_amount * * @return float|null */ @@ -2254,7 +2406,7 @@ public function getBaseShippingTaxAmount() } /** - * Returns base_shipping_tax_refunded + * Return base_shipping_tax_refunded * * @return float|null */ @@ -2264,7 +2416,7 @@ public function getBaseShippingTaxRefunded() } /** - * Returns base_subtotal + * Return base_subtotal * * @return float|null */ @@ -2274,7 +2426,7 @@ public function getBaseSubtotal() } /** - * Returns base_subtotal_canceled + * Return base_subtotal_canceled * * @return float|null */ @@ -2284,7 +2436,7 @@ public function getBaseSubtotalCanceled() } /** - * Returns base_subtotal_incl_tax + * Return base_subtotal_incl_tax * * @return float|null */ @@ -2294,7 +2446,7 @@ public function getBaseSubtotalInclTax() } /** - * Returns base_subtotal_invoiced + * Return base_subtotal_invoiced * * @return float|null */ @@ -2304,7 +2456,7 @@ public function getBaseSubtotalInvoiced() } /** - * Returns base_subtotal_refunded + * Return base_subtotal_refunded * * @return float|null */ @@ -2314,7 +2466,7 @@ public function getBaseSubtotalRefunded() } /** - * Returns base_tax_amount + * Return base_tax_amount * * @return float|null */ @@ -2324,7 +2476,7 @@ public function getBaseTaxAmount() } /** - * Returns base_tax_canceled + * Return base_tax_canceled * * @return float|null */ @@ -2334,7 +2486,7 @@ public function getBaseTaxCanceled() } /** - * Returns base_tax_invoiced + * Return base_tax_invoiced * * @return float|null */ @@ -2344,7 +2496,7 @@ public function getBaseTaxInvoiced() } /** - * Returns base_tax_refunded + * Return base_tax_refunded * * @return float|null */ @@ -2354,7 +2506,7 @@ public function getBaseTaxRefunded() } /** - * Returns base_total_canceled + * Return base_total_canceled * * @return float|null */ @@ -2364,7 +2516,7 @@ public function getBaseTotalCanceled() } /** - * Returns base_total_invoiced + * Return base_total_invoiced * * @return float|null */ @@ -2374,7 +2526,7 @@ public function getBaseTotalInvoiced() } /** - * Returns base_total_invoiced_cost + * Return base_total_invoiced_cost * * @return float|null */ @@ -2384,7 +2536,7 @@ public function getBaseTotalInvoicedCost() } /** - * Returns base_total_offline_refunded + * Return base_total_offline_refunded * * @return float|null */ @@ -2394,7 +2546,7 @@ public function getBaseTotalOfflineRefunded() } /** - * Returns base_total_online_refunded + * Return base_total_online_refunded * * @return float|null */ @@ -2404,7 +2556,7 @@ public function getBaseTotalOnlineRefunded() } /** - * Returns base_total_paid + * Return base_total_paid * * @return float|null */ @@ -2414,7 +2566,7 @@ public function getBaseTotalPaid() } /** - * Returns base_total_qty_ordered + * Return base_total_qty_ordered * * @return float|null */ @@ -2424,7 +2576,7 @@ public function getBaseTotalQtyOrdered() } /** - * Returns base_total_refunded + * Return base_total_refunded * * @return float|null */ @@ -2434,7 +2586,7 @@ public function getBaseTotalRefunded() } /** - * Returns base_to_global_rate + * Return base_to_global_rate * * @return float|null */ @@ -2444,7 +2596,7 @@ public function getBaseToGlobalRate() } /** - * Returns base_to_order_rate + * Return base_to_order_rate * * @return float|null */ @@ -2454,7 +2606,7 @@ public function getBaseToOrderRate() } /** - * Returns billing_address_id + * Return billing_address_id * * @return int|null */ @@ -2464,7 +2616,7 @@ public function getBillingAddressId() } /** - * Returns can_ship_partially + * Return can_ship_partially * * @return int|null */ @@ -2474,7 +2626,7 @@ public function getCanShipPartially() } /** - * Returns can_ship_partially_item + * Return can_ship_partially_item * * @return int|null */ @@ -2484,7 +2636,7 @@ public function getCanShipPartiallyItem() } /** - * Returns coupon_code + * Return coupon_code * * @return string|null */ @@ -2494,7 +2646,7 @@ public function getCouponCode() } /** - * Returns created_at + * Return created_at * * @return string|null */ @@ -2504,7 +2656,7 @@ public function getCreatedAt() } /** - * {@inheritdoc} + * @inheritdoc */ public function setCreatedAt($createdAt) { @@ -2512,7 +2664,7 @@ public function setCreatedAt($createdAt) } /** - * Returns customer_dob + * Return customer_dob * * @return string|null */ @@ -2522,7 +2674,7 @@ public function getCustomerDob() } /** - * Returns customer_email + * Return customer_email * * @return string */ @@ -2532,7 +2684,7 @@ public function getCustomerEmail() } /** - * Returns customer_firstname + * Return customer_firstname * * @return string|null */ @@ -2542,7 +2694,7 @@ public function getCustomerFirstname() } /** - * Returns customer_gender + * Return customer_gender * * @return int|null */ @@ -2552,7 +2704,7 @@ public function getCustomerGender() } /** - * Returns customer_group_id + * Return customer_group_id * * @return int|null */ @@ -2562,7 +2714,7 @@ public function getCustomerGroupId() } /** - * Returns customer_id + * Return customer_id * * @return int|null */ @@ -2572,7 +2724,7 @@ public function getCustomerId() } /** - * Returns customer_is_guest + * Return customer_is_guest * * @return int|null */ @@ -2582,7 +2734,7 @@ public function getCustomerIsGuest() } /** - * Returns customer_lastname + * Return customer_lastname * * @return string|null */ @@ -2592,7 +2744,7 @@ public function getCustomerLastname() } /** - * Returns customer_middlename + * Return customer_middlename * * @return string|null */ @@ -2602,7 +2754,7 @@ public function getCustomerMiddlename() } /** - * Returns customer_note + * Return customer_note * * @return string|null */ @@ -2612,7 +2764,7 @@ public function getCustomerNote() } /** - * Returns customer_note_notify + * Return customer_note_notify * * @return int|null */ @@ -2622,7 +2774,7 @@ public function getCustomerNoteNotify() } /** - * Returns customer_prefix + * Return customer_prefix * * @return string|null */ @@ -2632,7 +2784,7 @@ public function getCustomerPrefix() } /** - * Returns customer_suffix + * Return customer_suffix * * @return string|null */ @@ -2642,7 +2794,7 @@ public function getCustomerSuffix() } /** - * Returns customer_taxvat + * Return customer_taxvat * * @return string|null */ @@ -2652,7 +2804,7 @@ public function getCustomerTaxvat() } /** - * Returns discount_amount + * Return discount_amount * * @return float|null */ @@ -2662,7 +2814,7 @@ public function getDiscountAmount() } /** - * Returns discount_canceled + * Return discount_canceled * * @return float|null */ @@ -2672,7 +2824,7 @@ public function getDiscountCanceled() } /** - * Returns discount_description + * Return discount_description * * @return string|null */ @@ -2682,7 +2834,7 @@ public function getDiscountDescription() } /** - * Returns discount_invoiced + * Return discount_invoiced * * @return float|null */ @@ -2692,7 +2844,7 @@ public function getDiscountInvoiced() } /** - * Returns discount_refunded + * Return discount_refunded * * @return float|null */ @@ -2702,7 +2854,7 @@ public function getDiscountRefunded() } /** - * Returns edit_increment + * Return edit_increment * * @return int|null */ @@ -2712,7 +2864,7 @@ public function getEditIncrement() } /** - * Returns email_sent + * Return email_sent * * @return int|null */ @@ -2722,7 +2874,7 @@ public function getEmailSent() } /** - * Returns ext_customer_id + * Return ext_customer_id * * @return string|null */ @@ -2732,7 +2884,7 @@ public function getExtCustomerId() } /** - * Returns ext_order_id + * Return ext_order_id * * @return string|null */ @@ -2742,7 +2894,7 @@ public function getExtOrderId() } /** - * Returns forced_shipment_with_invoice + * Return forced_shipment_with_invoice * * @return int|null */ @@ -2752,7 +2904,7 @@ public function getForcedShipmentWithInvoice() } /** - * Returns global_currency_code + * Return global_currency_code * * @return string|null */ @@ -2762,7 +2914,7 @@ public function getGlobalCurrencyCode() } /** - * Returns grand_total + * Return grand_total * * @return float */ @@ -2772,7 +2924,7 @@ public function getGrandTotal() } /** - * Returns discount_tax_compensation_amount + * Return discount_tax_compensation_amount * * @return float|null */ @@ -2782,7 +2934,7 @@ public function getDiscountTaxCompensationAmount() } /** - * Returns discount_tax_compensation_invoiced + * Return discount_tax_compensation_invoiced * * @return float|null */ @@ -2792,7 +2944,7 @@ public function getDiscountTaxCompensationInvoiced() } /** - * Returns discount_tax_compensation_refunded + * Return discount_tax_compensation_refunded * * @return float|null */ @@ -2812,7 +2964,7 @@ public function getHoldBeforeState() } /** - * Returns hold_before_status + * Return hold_before_status * * @return string|null */ @@ -2822,7 +2974,7 @@ public function getHoldBeforeStatus() } /** - * Returns is_virtual + * Return is_virtual * * @return int|null */ @@ -2832,7 +2984,7 @@ public function getIsVirtual() } /** - * Returns order_currency_code + * Return order_currency_code * * @return string|null */ @@ -2842,7 +2994,7 @@ public function getOrderCurrencyCode() } /** - * Returns original_increment_id + * Return original_increment_id * * @return string|null */ @@ -2852,7 +3004,7 @@ public function getOriginalIncrementId() } /** - * Returns payment_authorization_amount + * Return payment_authorization_amount * * @return float|null */ @@ -2862,7 +3014,7 @@ public function getPaymentAuthorizationAmount() } /** - * Returns payment_auth_expiration + * Return payment_auth_expiration * * @return int|null */ @@ -2872,7 +3024,7 @@ public function getPaymentAuthExpiration() } /** - * Returns protect_code + * Return protect_code * * @return string|null */ @@ -2882,7 +3034,7 @@ public function getProtectCode() } /** - * Returns quote_address_id + * Return quote_address_id * * @return int|null */ @@ -2892,7 +3044,7 @@ public function getQuoteAddressId() } /** - * Returns quote_id + * Return quote_id * * @return int|null */ @@ -2902,7 +3054,7 @@ public function getQuoteId() } /** - * Returns relation_child_id + * Return relation_child_id * * @return string|null */ @@ -2912,7 +3064,7 @@ public function getRelationChildId() } /** - * Returns relation_child_real_id + * Return relation_child_real_id * * @return string|null */ @@ -2922,7 +3074,7 @@ public function getRelationChildRealId() } /** - * Returns relation_parent_id + * Return relation_parent_id * * @return string|null */ @@ -2932,7 +3084,7 @@ public function getRelationParentId() } /** - * Returns relation_parent_real_id + * Return relation_parent_real_id * * @return string|null */ @@ -2942,7 +3094,7 @@ public function getRelationParentRealId() } /** - * Returns remote_ip + * Return remote_ip * * @return string|null */ @@ -2952,7 +3104,7 @@ public function getRemoteIp() } /** - * Returns shipping_amount + * Return shipping_amount * * @return float|null */ @@ -2962,7 +3114,7 @@ public function getShippingAmount() } /** - * Returns shipping_canceled + * Return shipping_canceled * * @return float|null */ @@ -2972,7 +3124,7 @@ public function getShippingCanceled() } /** - * Returns shipping_description + * Return shipping_description * * @return string|null */ @@ -2982,7 +3134,7 @@ public function getShippingDescription() } /** - * Returns shipping_discount_amount + * Return shipping_discount_amount * * @return float|null */ @@ -2992,7 +3144,7 @@ public function getShippingDiscountAmount() } /** - * Returns shipping_discount_tax_compensation_amount + * Return shipping_discount_tax_compensation_amount * * @return float|null */ @@ -3002,7 +3154,7 @@ public function getShippingDiscountTaxCompensationAmount() } /** - * Returns shipping_incl_tax + * Return shipping_incl_tax * * @return float|null */ @@ -3012,7 +3164,7 @@ public function getShippingInclTax() } /** - * Returns shipping_invoiced + * Return shipping_invoiced * * @return float|null */ @@ -3022,7 +3174,7 @@ public function getShippingInvoiced() } /** - * Returns shipping_refunded + * Return shipping_refunded * * @return float|null */ @@ -3032,7 +3184,7 @@ public function getShippingRefunded() } /** - * Returns shipping_tax_amount + * Return shipping_tax_amount * * @return float|null */ @@ -3042,7 +3194,7 @@ public function getShippingTaxAmount() } /** - * Returns shipping_tax_refunded + * Return shipping_tax_refunded * * @return float|null */ @@ -3052,7 +3204,7 @@ public function getShippingTaxRefunded() } /** - * Returns state + * Return state * * @return string|null */ @@ -3062,7 +3214,7 @@ public function getState() } /** - * Returns status + * Return status * * @return string|null */ @@ -3072,7 +3224,7 @@ public function getStatus() } /** - * Returns store_currency_code + * Return store_currency_code * * @return string|null */ @@ -3082,7 +3234,7 @@ public function getStoreCurrencyCode() } /** - * Returns store_id + * Return store_id * * @return int|null */ @@ -3092,7 +3244,7 @@ public function getStoreId() } /** - * Returns store_name + * Return store_name * * @return string|null */ @@ -3102,7 +3254,7 @@ public function getStoreName() } /** - * Returns store_to_base_rate + * Return store_to_base_rate * * @return float|null */ @@ -3112,7 +3264,7 @@ public function getStoreToBaseRate() } /** - * Returns store_to_order_rate + * Return store_to_order_rate * * @return float|null */ @@ -3122,7 +3274,7 @@ public function getStoreToOrderRate() } /** - * Returns subtotal + * Return subtotal * * @return float|null */ @@ -3132,7 +3284,7 @@ public function getSubtotal() } /** - * Returns subtotal_canceled + * Return subtotal_canceled * * @return float|null */ @@ -3142,7 +3294,7 @@ public function getSubtotalCanceled() } /** - * Returns subtotal_incl_tax + * Return subtotal_incl_tax * * @return float|null */ @@ -3152,7 +3304,7 @@ public function getSubtotalInclTax() } /** - * Returns subtotal_invoiced + * Return subtotal_invoiced * * @return float|null */ @@ -3162,7 +3314,7 @@ public function getSubtotalInvoiced() } /** - * Returns subtotal_refunded + * Return subtotal_refunded * * @return float|null */ @@ -3172,7 +3324,7 @@ public function getSubtotalRefunded() } /** - * Returns tax_amount + * Return tax_amount * * @return float|null */ @@ -3182,7 +3334,7 @@ public function getTaxAmount() } /** - * Returns tax_canceled + * Return tax_canceled * * @return float|null */ @@ -3192,7 +3344,7 @@ public function getTaxCanceled() } /** - * Returns tax_invoiced + * Return tax_invoiced * * @return float|null */ @@ -3202,7 +3354,7 @@ public function getTaxInvoiced() } /** - * Returns tax_refunded + * Return tax_refunded * * @return float|null */ @@ -3212,7 +3364,7 @@ public function getTaxRefunded() } /** - * Returns total_canceled + * Return total_canceled * * @return float|null */ @@ -3222,7 +3374,7 @@ public function getTotalCanceled() } /** - * Returns total_invoiced + * Return total_invoiced * * @return float|null */ @@ -3232,7 +3384,7 @@ public function getTotalInvoiced() } /** - * Returns total_item_count + * Return total_item_count * * @return int|null */ @@ -3242,7 +3394,7 @@ public function getTotalItemCount() } /** - * Returns total_offline_refunded + * Return total_offline_refunded * * @return float|null */ @@ -3252,7 +3404,7 @@ public function getTotalOfflineRefunded() } /** - * Returns total_online_refunded + * Return total_online_refunded * * @return float|null */ @@ -3262,7 +3414,7 @@ public function getTotalOnlineRefunded() } /** - * Returns total_paid + * Return total_paid * * @return float|null */ @@ -3272,7 +3424,7 @@ public function getTotalPaid() } /** - * Returns total_qty_ordered + * Return total_qty_ordered * * @return float|null */ @@ -3282,7 +3434,7 @@ public function getTotalQtyOrdered() } /** - * Returns total_refunded + * Return total_refunded * * @return float|null */ @@ -3292,7 +3444,7 @@ public function getTotalRefunded() } /** - * Returns updated_at + * Return updated_at * * @return string|null */ @@ -3302,7 +3454,7 @@ public function getUpdatedAt() } /** - * Returns weight + * Return weight * * @return float|null */ @@ -3312,7 +3464,7 @@ public function getWeight() } /** - * Returns x_forwarded_for + * Return x_forwarded_for * * @return string|null */ @@ -3322,7 +3474,7 @@ public function getXForwardedFor() } /** - * {@inheritdoc} + * @inheritdoc */ public function setStatusHistories(array $statusHistories = null) { @@ -3330,7 +3482,7 @@ public function setStatusHistories(array $statusHistories = null) } /** - * {@inheritdoc} + * @inheritdoc */ public function setStatus($status) { @@ -3338,7 +3490,7 @@ public function setStatus($status) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCouponCode($code) { @@ -3346,7 +3498,7 @@ public function setCouponCode($code) } /** - * {@inheritdoc} + * @inheritdoc */ public function setProtectCode($code) { @@ -3354,7 +3506,7 @@ public function setProtectCode($code) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingDescription($description) { @@ -3362,7 +3514,7 @@ public function setShippingDescription($description) } /** - * {@inheritdoc} + * @inheritdoc */ public function setIsVirtual($isVirtual) { @@ -3370,7 +3522,7 @@ public function setIsVirtual($isVirtual) } /** - * {@inheritdoc} + * @inheritdoc */ public function setStoreId($id) { @@ -3378,7 +3530,7 @@ public function setStoreId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomerId($id) { @@ -3386,7 +3538,7 @@ public function setCustomerId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseDiscountAmount($amount) { @@ -3394,7 +3546,7 @@ public function setBaseDiscountAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseDiscountCanceled($baseDiscountCanceled) { @@ -3402,7 +3554,7 @@ public function setBaseDiscountCanceled($baseDiscountCanceled) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseDiscountInvoiced($baseDiscountInvoiced) { @@ -3410,7 +3562,7 @@ public function setBaseDiscountInvoiced($baseDiscountInvoiced) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseDiscountRefunded($baseDiscountRefunded) { @@ -3418,7 +3570,7 @@ public function setBaseDiscountRefunded($baseDiscountRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseGrandTotal($amount) { @@ -3426,7 +3578,7 @@ public function setBaseGrandTotal($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingAmount($amount) { @@ -3434,7 +3586,7 @@ public function setBaseShippingAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingCanceled($baseShippingCanceled) { @@ -3442,7 +3594,7 @@ public function setBaseShippingCanceled($baseShippingCanceled) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingInvoiced($baseShippingInvoiced) { @@ -3450,7 +3602,7 @@ public function setBaseShippingInvoiced($baseShippingInvoiced) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingRefunded($baseShippingRefunded) { @@ -3458,7 +3610,7 @@ public function setBaseShippingRefunded($baseShippingRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingTaxAmount($amount) { @@ -3466,7 +3618,7 @@ public function setBaseShippingTaxAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingTaxRefunded($baseShippingTaxRefunded) { @@ -3474,7 +3626,7 @@ public function setBaseShippingTaxRefunded($baseShippingTaxRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseSubtotal($amount) { @@ -3482,7 +3634,7 @@ public function setBaseSubtotal($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseSubtotalCanceled($baseSubtotalCanceled) { @@ -3490,7 +3642,7 @@ public function setBaseSubtotalCanceled($baseSubtotalCanceled) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseSubtotalInvoiced($baseSubtotalInvoiced) { @@ -3498,7 +3650,7 @@ public function setBaseSubtotalInvoiced($baseSubtotalInvoiced) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseSubtotalRefunded($baseSubtotalRefunded) { @@ -3506,7 +3658,7 @@ public function setBaseSubtotalRefunded($baseSubtotalRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTaxAmount($amount) { @@ -3514,7 +3666,7 @@ public function setBaseTaxAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTaxCanceled($baseTaxCanceled) { @@ -3522,7 +3674,7 @@ public function setBaseTaxCanceled($baseTaxCanceled) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTaxInvoiced($baseTaxInvoiced) { @@ -3530,7 +3682,7 @@ public function setBaseTaxInvoiced($baseTaxInvoiced) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTaxRefunded($baseTaxRefunded) { @@ -3538,7 +3690,7 @@ public function setBaseTaxRefunded($baseTaxRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseToGlobalRate($rate) { @@ -3546,7 +3698,7 @@ public function setBaseToGlobalRate($rate) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseToOrderRate($rate) { @@ -3554,7 +3706,7 @@ public function setBaseToOrderRate($rate) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTotalCanceled($baseTotalCanceled) { @@ -3562,7 +3714,7 @@ public function setBaseTotalCanceled($baseTotalCanceled) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTotalInvoiced($baseTotalInvoiced) { @@ -3570,7 +3722,7 @@ public function setBaseTotalInvoiced($baseTotalInvoiced) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTotalInvoicedCost($baseTotalInvoicedCost) { @@ -3578,7 +3730,7 @@ public function setBaseTotalInvoicedCost($baseTotalInvoicedCost) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTotalOfflineRefunded($baseTotalOfflineRefunded) { @@ -3586,7 +3738,7 @@ public function setBaseTotalOfflineRefunded($baseTotalOfflineRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTotalOnlineRefunded($baseTotalOnlineRefunded) { @@ -3594,7 +3746,7 @@ public function setBaseTotalOnlineRefunded($baseTotalOnlineRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTotalPaid($baseTotalPaid) { @@ -3602,7 +3754,7 @@ public function setBaseTotalPaid($baseTotalPaid) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTotalQtyOrdered($baseTotalQtyOrdered) { @@ -3610,7 +3762,7 @@ public function setBaseTotalQtyOrdered($baseTotalQtyOrdered) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTotalRefunded($baseTotalRefunded) { @@ -3618,7 +3770,7 @@ public function setBaseTotalRefunded($baseTotalRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountAmount($amount) { @@ -3626,7 +3778,7 @@ public function setDiscountAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountCanceled($discountCanceled) { @@ -3634,7 +3786,7 @@ public function setDiscountCanceled($discountCanceled) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountInvoiced($discountInvoiced) { @@ -3642,7 +3794,7 @@ public function setDiscountInvoiced($discountInvoiced) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountRefunded($discountRefunded) { @@ -3650,7 +3802,7 @@ public function setDiscountRefunded($discountRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setGrandTotal($amount) { @@ -3658,7 +3810,7 @@ public function setGrandTotal($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingAmount($amount) { @@ -3666,7 +3818,7 @@ public function setShippingAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingCanceled($shippingCanceled) { @@ -3674,7 +3826,7 @@ public function setShippingCanceled($shippingCanceled) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingInvoiced($shippingInvoiced) { @@ -3682,7 +3834,7 @@ public function setShippingInvoiced($shippingInvoiced) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingRefunded($shippingRefunded) { @@ -3690,7 +3842,7 @@ public function setShippingRefunded($shippingRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingTaxAmount($amount) { @@ -3698,7 +3850,7 @@ public function setShippingTaxAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingTaxRefunded($shippingTaxRefunded) { @@ -3706,7 +3858,7 @@ public function setShippingTaxRefunded($shippingTaxRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setStoreToBaseRate($rate) { @@ -3714,7 +3866,7 @@ public function setStoreToBaseRate($rate) } /** - * {@inheritdoc} + * @inheritdoc */ public function setStoreToOrderRate($rate) { @@ -3722,7 +3874,7 @@ public function setStoreToOrderRate($rate) } /** - * {@inheritdoc} + * @inheritdoc */ public function setSubtotal($amount) { @@ -3730,7 +3882,7 @@ public function setSubtotal($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setSubtotalCanceled($subtotalCanceled) { @@ -3738,7 +3890,7 @@ public function setSubtotalCanceled($subtotalCanceled) } /** - * {@inheritdoc} + * @inheritdoc */ public function setSubtotalInvoiced($subtotalInvoiced) { @@ -3746,7 +3898,7 @@ public function setSubtotalInvoiced($subtotalInvoiced) } /** - * {@inheritdoc} + * @inheritdoc */ public function setSubtotalRefunded($subtotalRefunded) { @@ -3754,7 +3906,7 @@ public function setSubtotalRefunded($subtotalRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTaxAmount($amount) { @@ -3762,7 +3914,7 @@ public function setTaxAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTaxCanceled($taxCanceled) { @@ -3770,7 +3922,7 @@ public function setTaxCanceled($taxCanceled) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTaxInvoiced($taxInvoiced) { @@ -3778,7 +3930,7 @@ public function setTaxInvoiced($taxInvoiced) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTaxRefunded($taxRefunded) { @@ -3786,7 +3938,7 @@ public function setTaxRefunded($taxRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTotalCanceled($totalCanceled) { @@ -3794,7 +3946,7 @@ public function setTotalCanceled($totalCanceled) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTotalInvoiced($totalInvoiced) { @@ -3802,7 +3954,7 @@ public function setTotalInvoiced($totalInvoiced) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTotalOfflineRefunded($totalOfflineRefunded) { @@ -3810,7 +3962,7 @@ public function setTotalOfflineRefunded($totalOfflineRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTotalOnlineRefunded($totalOnlineRefunded) { @@ -3818,7 +3970,7 @@ public function setTotalOnlineRefunded($totalOnlineRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTotalPaid($totalPaid) { @@ -3826,7 +3978,7 @@ public function setTotalPaid($totalPaid) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTotalQtyOrdered($totalQtyOrdered) { @@ -3834,7 +3986,7 @@ public function setTotalQtyOrdered($totalQtyOrdered) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTotalRefunded($totalRefunded) { @@ -3842,7 +3994,7 @@ public function setTotalRefunded($totalRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCanShipPartially($flag) { @@ -3850,7 +4002,7 @@ public function setCanShipPartially($flag) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCanShipPartiallyItem($flag) { @@ -3858,7 +4010,7 @@ public function setCanShipPartiallyItem($flag) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomerIsGuest($customerIsGuest) { @@ -3866,7 +4018,7 @@ public function setCustomerIsGuest($customerIsGuest) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomerNoteNotify($customerNoteNotify) { @@ -3874,7 +4026,7 @@ public function setCustomerNoteNotify($customerNoteNotify) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBillingAddressId($id) { @@ -3882,7 +4034,7 @@ public function setBillingAddressId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomerGroupId($id) { @@ -3890,7 +4042,7 @@ public function setCustomerGroupId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setEditIncrement($editIncrement) { @@ -3898,7 +4050,7 @@ public function setEditIncrement($editIncrement) } /** - * {@inheritdoc} + * @inheritdoc */ public function setEmailSent($emailSent) { @@ -3906,7 +4058,7 @@ public function setEmailSent($emailSent) } /** - * {@inheritdoc} + * @inheritdoc */ public function setForcedShipmentWithInvoice($forcedShipmentWithInvoice) { @@ -3914,7 +4066,7 @@ public function setForcedShipmentWithInvoice($forcedShipmentWithInvoice) } /** - * {@inheritdoc} + * @inheritdoc */ public function setPaymentAuthExpiration($paymentAuthExpiration) { @@ -3922,7 +4074,7 @@ public function setPaymentAuthExpiration($paymentAuthExpiration) } /** - * {@inheritdoc} + * @inheritdoc */ public function setQuoteAddressId($id) { @@ -3930,7 +4082,7 @@ public function setQuoteAddressId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setQuoteId($id) { @@ -3938,7 +4090,7 @@ public function setQuoteId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAdjustmentNegative($adjustmentNegative) { @@ -3946,7 +4098,7 @@ public function setAdjustmentNegative($adjustmentNegative) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAdjustmentPositive($adjustmentPositive) { @@ -3954,7 +4106,7 @@ public function setAdjustmentPositive($adjustmentPositive) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseAdjustmentNegative($baseAdjustmentNegative) { @@ -3962,7 +4114,7 @@ public function setBaseAdjustmentNegative($baseAdjustmentNegative) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseAdjustmentPositive($baseAdjustmentPositive) { @@ -3970,7 +4122,7 @@ public function setBaseAdjustmentPositive($baseAdjustmentPositive) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingDiscountAmount($amount) { @@ -3978,7 +4130,7 @@ public function setBaseShippingDiscountAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseSubtotalInclTax($amount) { @@ -3986,7 +4138,7 @@ public function setBaseSubtotalInclTax($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTotalDue($baseTotalDue) { @@ -3994,7 +4146,7 @@ public function setBaseTotalDue($baseTotalDue) } /** - * {@inheritdoc} + * @inheritdoc */ public function setPaymentAuthorizationAmount($amount) { @@ -4002,7 +4154,7 @@ public function setPaymentAuthorizationAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingDiscountAmount($amount) { @@ -4010,7 +4162,7 @@ public function setShippingDiscountAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setSubtotalInclTax($amount) { @@ -4018,7 +4170,7 @@ public function setSubtotalInclTax($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTotalDue($totalDue) { @@ -4026,7 +4178,7 @@ public function setTotalDue($totalDue) } /** - * {@inheritdoc} + * @inheritdoc */ public function setWeight($weight) { @@ -4034,7 +4186,7 @@ public function setWeight($weight) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomerDob($customerDob) { @@ -4042,7 +4194,7 @@ public function setCustomerDob($customerDob) } /** - * {@inheritdoc} + * @inheritdoc */ public function setIncrementId($id) { @@ -4050,7 +4202,7 @@ public function setIncrementId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAppliedRuleIds($appliedRuleIds) { @@ -4058,7 +4210,7 @@ public function setAppliedRuleIds($appliedRuleIds) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseCurrencyCode($code) { @@ -4066,7 +4218,7 @@ public function setBaseCurrencyCode($code) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomerEmail($customerEmail) { @@ -4074,7 +4226,7 @@ public function setCustomerEmail($customerEmail) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomerFirstname($customerFirstname) { @@ -4082,7 +4234,7 @@ public function setCustomerFirstname($customerFirstname) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomerLastname($customerLastname) { @@ -4090,7 +4242,7 @@ public function setCustomerLastname($customerLastname) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomerMiddlename($customerMiddlename) { @@ -4098,7 +4250,7 @@ public function setCustomerMiddlename($customerMiddlename) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomerPrefix($customerPrefix) { @@ -4106,7 +4258,7 @@ public function setCustomerPrefix($customerPrefix) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomerSuffix($customerSuffix) { @@ -4114,7 +4266,7 @@ public function setCustomerSuffix($customerSuffix) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomerTaxvat($customerTaxvat) { @@ -4122,7 +4274,7 @@ public function setCustomerTaxvat($customerTaxvat) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountDescription($description) { @@ -4130,7 +4282,7 @@ public function setDiscountDescription($description) } /** - * {@inheritdoc} + * @inheritdoc */ public function setExtCustomerId($id) { @@ -4138,7 +4290,7 @@ public function setExtCustomerId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setExtOrderId($id) { @@ -4146,7 +4298,7 @@ public function setExtOrderId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setGlobalCurrencyCode($code) { @@ -4154,7 +4306,7 @@ public function setGlobalCurrencyCode($code) } /** - * {@inheritdoc} + * @inheritdoc */ public function setHoldBeforeState($holdBeforeState) { @@ -4162,7 +4314,7 @@ public function setHoldBeforeState($holdBeforeState) } /** - * {@inheritdoc} + * @inheritdoc */ public function setHoldBeforeStatus($holdBeforeStatus) { @@ -4170,7 +4322,7 @@ public function setHoldBeforeStatus($holdBeforeStatus) } /** - * {@inheritdoc} + * @inheritdoc */ public function setOrderCurrencyCode($code) { @@ -4178,7 +4330,7 @@ public function setOrderCurrencyCode($code) } /** - * {@inheritdoc} + * @inheritdoc */ public function setOriginalIncrementId($id) { @@ -4186,7 +4338,7 @@ public function setOriginalIncrementId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setRelationChildId($id) { @@ -4194,7 +4346,7 @@ public function setRelationChildId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setRelationChildRealId($realId) { @@ -4202,7 +4354,7 @@ public function setRelationChildRealId($realId) } /** - * {@inheritdoc} + * @inheritdoc */ public function setRelationParentId($id) { @@ -4210,7 +4362,7 @@ public function setRelationParentId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setRelationParentRealId($realId) { @@ -4218,7 +4370,7 @@ public function setRelationParentRealId($realId) } /** - * {@inheritdoc} + * @inheritdoc */ public function setRemoteIp($remoteIp) { @@ -4226,7 +4378,7 @@ public function setRemoteIp($remoteIp) } /** - * {@inheritdoc} + * @inheritdoc */ public function setStoreCurrencyCode($code) { @@ -4234,7 +4386,7 @@ public function setStoreCurrencyCode($code) } /** - * {@inheritdoc} + * @inheritdoc */ public function setStoreName($storeName) { @@ -4242,7 +4394,7 @@ public function setStoreName($storeName) } /** - * {@inheritdoc} + * @inheritdoc */ public function setXForwardedFor($xForwardedFor) { @@ -4250,7 +4402,7 @@ public function setXForwardedFor($xForwardedFor) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomerNote($customerNote) { @@ -4258,7 +4410,7 @@ public function setCustomerNote($customerNote) } /** - * {@inheritdoc} + * @inheritdoc */ public function setUpdatedAt($timestamp) { @@ -4266,7 +4418,7 @@ public function setUpdatedAt($timestamp) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTotalItemCount($totalItemCount) { @@ -4274,7 +4426,7 @@ public function setTotalItemCount($totalItemCount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomerGender($customerGender) { @@ -4282,7 +4434,7 @@ public function setCustomerGender($customerGender) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountTaxCompensationAmount($amount) { @@ -4290,7 +4442,7 @@ public function setDiscountTaxCompensationAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseDiscountTaxCompensationAmount($amount) { @@ -4298,7 +4450,7 @@ public function setBaseDiscountTaxCompensationAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingDiscountTaxCompensationAmount($amount) { @@ -4306,7 +4458,7 @@ public function setShippingDiscountTaxCompensationAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingDiscountTaxCompensationAmnt($amnt) { @@ -4314,7 +4466,7 @@ public function setBaseShippingDiscountTaxCompensationAmnt($amnt) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountTaxCompensationInvoiced($discountTaxCompensationInvoiced) { @@ -4322,7 +4474,7 @@ public function setDiscountTaxCompensationInvoiced($discountTaxCompensationInvoi } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseDiscountTaxCompensationInvoiced($baseDiscountTaxCompensationInvoiced) { @@ -4333,7 +4485,7 @@ public function setBaseDiscountTaxCompensationInvoiced($baseDiscountTaxCompensat } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountTaxCompensationRefunded($discountTaxCompensationRefunded) { @@ -4344,7 +4496,7 @@ public function setDiscountTaxCompensationRefunded($discountTaxCompensationRefun } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseDiscountTaxCompensationRefunded($baseDiscountTaxCompensationRefunded) { @@ -4355,7 +4507,7 @@ public function setBaseDiscountTaxCompensationRefunded($baseDiscountTaxCompensat } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingInclTax($amount) { @@ -4363,7 +4515,7 @@ public function setShippingInclTax($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingInclTax($amount) { diff --git a/app/code/Magento/Sales/Model/Order/Address.php b/app/code/Magento/Sales/Model/Order/Address.php index 77d8330a7255..11d8a937387d 100644 --- a/app/code/Magento/Sales/Model/Order/Address.php +++ b/app/code/Magento/Sales/Model/Order/Address.php @@ -173,7 +173,8 @@ protected function implodeStreetValue($value) * Enforce format of the street field * * @param array|string $key - * @param null $value + * @param array|string $value + * * @return \Magento\Framework\DataObject */ public function setData($key, $value = null) @@ -251,7 +252,7 @@ public function getStreet() public function getStreetLine($number) { $lines = $this->getStreet(); - return isset($lines[$number - 1]) ? $lines[$number - 1] : ''; + return $lines[$number - 1] ?? ''; } //@codeCoverageIgnoreStart @@ -508,7 +509,7 @@ public function getVatRequestSuccess() } /** - * {@inheritdoc} + * @inheritdoc */ public function setParentId($id) { @@ -516,7 +517,7 @@ public function setParentId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomerAddressId($id) { @@ -524,7 +525,7 @@ public function setCustomerAddressId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setRegionId($id) { @@ -532,7 +533,7 @@ public function setRegionId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setStreet($street) { @@ -540,7 +541,7 @@ public function setStreet($street) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomerId($id) { @@ -548,7 +549,7 @@ public function setCustomerId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setFax($fax) { @@ -556,7 +557,7 @@ public function setFax($fax) } /** - * {@inheritdoc} + * @inheritdoc */ public function setRegion($region) { @@ -564,7 +565,7 @@ public function setRegion($region) } /** - * {@inheritdoc} + * @inheritdoc */ public function setPostcode($postcode) { @@ -572,7 +573,7 @@ public function setPostcode($postcode) } /** - * {@inheritdoc} + * @inheritdoc */ public function setLastname($lastname) { @@ -580,7 +581,7 @@ public function setLastname($lastname) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCity($city) { @@ -588,7 +589,7 @@ public function setCity($city) } /** - * {@inheritdoc} + * @inheritdoc */ public function setEmail($email) { @@ -596,7 +597,7 @@ public function setEmail($email) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTelephone($telephone) { @@ -604,7 +605,7 @@ public function setTelephone($telephone) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCountryId($id) { @@ -612,7 +613,7 @@ public function setCountryId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setFirstname($firstname) { @@ -620,7 +621,7 @@ public function setFirstname($firstname) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAddressType($addressType) { @@ -628,7 +629,7 @@ public function setAddressType($addressType) } /** - * {@inheritdoc} + * @inheritdoc */ public function setPrefix($prefix) { @@ -636,7 +637,7 @@ public function setPrefix($prefix) } /** - * {@inheritdoc} + * @inheritdoc */ public function setMiddlename($middlename) { @@ -644,7 +645,7 @@ public function setMiddlename($middlename) } /** - * {@inheritdoc} + * @inheritdoc */ public function setSuffix($suffix) { @@ -652,7 +653,7 @@ public function setSuffix($suffix) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCompany($company) { @@ -660,7 +661,7 @@ public function setCompany($company) } /** - * {@inheritdoc} + * @inheritdoc */ public function setVatId($id) { @@ -668,7 +669,7 @@ public function setVatId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setVatIsValid($vatIsValid) { @@ -676,7 +677,7 @@ public function setVatIsValid($vatIsValid) } /** - * {@inheritdoc} + * @inheritdoc */ public function setVatRequestId($id) { @@ -684,7 +685,7 @@ public function setVatRequestId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setRegionCode($regionCode) { @@ -692,7 +693,7 @@ public function setRegionCode($regionCode) } /** - * {@inheritdoc} + * @inheritdoc */ public function setVatRequestDate($vatRequestDate) { @@ -700,7 +701,7 @@ public function setVatRequestDate($vatRequestDate) } /** - * {@inheritdoc} + * @inheritdoc */ public function setVatRequestSuccess($vatRequestSuccess) { @@ -708,7 +709,7 @@ public function setVatRequestSuccess($vatRequestSuccess) } /** - * {@inheritdoc} + * @inheritdoc * * @return \Magento\Sales\Api\Data\OrderAddressExtensionInterface|null */ @@ -718,9 +719,10 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * @inheritdoc * * @param \Magento\Sales\Api\Data\OrderAddressExtensionInterface $extensionAttributes + * * @return $this */ public function setExtensionAttributes(\Magento\Sales\Api\Data\OrderAddressExtensionInterface $extensionAttributes) diff --git a/app/code/Magento/Sales/Model/Order/Address/Validator.php b/app/code/Magento/Sales/Model/Order/Address/Validator.php index 31cb5bb1f60c..5d3186781e7d 100644 --- a/app/code/Magento/Sales/Model/Order/Address/Validator.php +++ b/app/code/Magento/Sales/Model/Order/Address/Validator.php @@ -49,8 +49,8 @@ class Validator /** * @param DirectoryHelper $directoryHelper - * @param CountryFactory $countryFactory - * @param EavConfig $eavConfig + * @param CountryFactory $countryFactory + * @param EavConfig $eavConfig */ public function __construct( DirectoryHelper $directoryHelper, @@ -61,6 +61,17 @@ public function __construct( $this->countryFactory = $countryFactory; $this->eavConfig = $eavConfig ?: ObjectManager::getInstance() ->get(EavConfig::class); + } + + /** + * Validate address. + * + * @param \Magento\Sales\Model\Order\Address $address + * @return array + */ + public function validate(Address $address) + { + $warnings = []; if ($this->isTelephoneRequired()) { $this->required['telephone'] = 'Phone Number'; @@ -73,16 +84,7 @@ public function __construct( if ($this->isFaxRequired()) { $this->required['fax'] = 'Fax'; } - } - /** - * - * @param \Magento\Sales\Model\Order\Address $address - * @return array - */ - public function validate(Address $address) - { - $warnings = []; foreach ($this->required as $code => $label) { if (!$address->hasData($code)) { $warnings[] = sprintf('"%s" is required. Enter and try again.', $label); @@ -195,7 +197,10 @@ protected function isStateRequired($countryId) } /** + * Check whether telephone is required for address. + * * @return bool + * @throws \Magento\Framework\Exception\LocalizedException */ protected function isTelephoneRequired() { @@ -203,7 +208,10 @@ protected function isTelephoneRequired() } /** + * Check whether company is required for address. + * * @return bool + * @throws \Magento\Framework\Exception\LocalizedException */ protected function isCompanyRequired() { @@ -211,7 +219,10 @@ protected function isCompanyRequired() } /** + * Check whether telephone is required for address. + * * @return bool + * @throws \Magento\Framework\Exception\LocalizedException */ protected function isFaxRequired() { diff --git a/app/code/Magento/Sales/Model/Order/AddressRepository.php b/app/code/Magento/Sales/Model/Order/AddressRepository.php index 2aed6ef16817..af83dde99c6f 100644 --- a/app/code/Magento/Sales/Model/Order/AddressRepository.php +++ b/app/code/Magento/Sales/Model/Order/AddressRepository.php @@ -90,7 +90,7 @@ public function get($id) * Find order addresses by criteria. * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria - * @return \Magento\Sales\Api\Data\OrderAddressInterface[] + * @return \Magento\Sales\Model\ResourceModel\Order\Address\Collection */ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria) { diff --git a/app/code/Magento/Sales/Model/Order/Config.php b/app/code/Magento/Sales/Model/Order/Config.php index e00eda647dc8..1b31caa573f9 100644 --- a/app/code/Magento/Sales/Model/Order/Config.php +++ b/app/code/Magento/Sales/Model/Order/Config.php @@ -5,6 +5,8 @@ */ namespace Magento\Sales\Model\Order; +use Magento\Framework\Exception\LocalizedException; + /** * Order configuration model * @@ -73,6 +75,8 @@ public function __construct( } /** + * Get collection. + * * @return \Magento\Sales\Model\ResourceModel\Order\Status\Collection */ protected function _getCollection() @@ -84,8 +88,10 @@ protected function _getCollection() } /** + * Get state. + * * @param string $state - * @return Status|null + * @return Status */ protected function _getState($state) { @@ -101,9 +107,9 @@ protected function _getState($state) * Retrieve default status for state * * @param string $state - * @return string + * @return string|null */ - public function getStateDefaultStatus($state) + public function getStateDefaultStatus($state): ?string { $status = false; $stateNode = $this->_getState($state); @@ -115,24 +121,48 @@ public function getStateDefaultStatus($state) } /** - * Retrieve status label + * Get status label for a specified area * - * @param string $code - * @return string + * @param string|null $code + * @param string $area + * @return string|null */ - public function getStatusLabel($code) + private function getStatusLabelForArea(?string $code, string $area): ?string { - $area = $this->state->getAreaCode(); $code = $this->maskStatusForArea($area, $code); $status = $this->orderStatusFactory->create()->load($code); - if ($area == 'adminhtml') { + if ($area === 'adminhtml') { return $status->getLabel(); } return $status->getStoreLabel(); } + /** + * Retrieve status label for detected area + * + * @param string|null $code + * @return string|null + * @throws LocalizedException + */ + public function getStatusLabel($code) + { + $area = $this->state->getAreaCode() ?: \Magento\Framework\App\Area::AREA_FRONTEND; + return $this->getStatusLabelForArea($code, $area); + } + + /** + * Retrieve status label for area + * + * @param string|null $code + * @return string|null + */ + public function getStatusFrontendLabel(?string $code): ?string + { + return $this->getStatusLabelForArea($code, \Magento\Framework\App\Area::AREA_FRONTEND); + } + /** * Mask status for order for specified area * @@ -249,8 +279,9 @@ public function getInvisibleOnFrontStatuses() } /** - * Get existing order statuses - * Visible or invisible on frontend according to passed param + * Get existing order statuses. + * + * Visible or invisible on frontend according to passed param. * * @param bool $visibility * @return array diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo.php b/app/code/Magento/Sales/Model/Order/Creditmemo.php index 7ea7e4af3c36..708aee5e5926 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo.php @@ -13,19 +13,21 @@ use Magento\Sales\Model\AbstractModel; use Magento\Sales\Model\EntityInterface; use Magento\Sales\Model\Order\InvoiceFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; /** * Order creditmemo model * * @api - * @method \Magento\Sales\Model\Order\Invoice setSendEmail(bool $value) - * @method \Magento\Sales\Model\Order\Invoice setCustomerNote(string $value) + * @method \Magento\Sales\Model\Order\Creditmemo setSendEmail(bool $value) + * @method \Magento\Sales\Model\Order\Creditmemo setCustomerNote(string $value) * @method string getCustomerNote() - * @method \Magento\Sales\Model\Order\Invoice setCustomerNoteNotify(bool $value) + * @method \Magento\Sales\Model\Order\Creditmemo setCustomerNoteNotify(bool $value) * @method bool getCustomerNoteNotify() * @SuppressWarnings(PHPMD.ExcessivePublicCount) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyFields) * @since 100.0.2 */ class Creditmemo extends AbstractModel implements EntityInterface, CreditmemoInterface @@ -119,6 +121,11 @@ class Creditmemo extends AbstractModel implements EntityInterface, CreditmemoInt */ private $invoiceFactory; + /** + * @var ScopeConfigInterface; + */ + private $scopeConfig; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -136,6 +143,7 @@ class Creditmemo extends AbstractModel implements EntityInterface, CreditmemoInt * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data * @param InvoiceFactory $invoiceFactory + * @param ScopeConfigInterface $scopeConfig * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -154,7 +162,8 @@ public function __construct( \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], - InvoiceFactory $invoiceFactory = null + InvoiceFactory $invoiceFactory = null, + ScopeConfigInterface $scopeConfig = null ) { $this->_creditmemoConfig = $creditmemoConfig; $this->_orderFactory = $orderFactory; @@ -165,6 +174,7 @@ public function __construct( $this->_commentCollectionFactory = $commentCollectionFactory; $this->priceCurrency = $priceCurrency; $this->invoiceFactory = $invoiceFactory ?: ObjectManager::getInstance()->get(InvoiceFactory::class); + $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); parent::__construct( $context, $registry, @@ -263,6 +273,8 @@ public function getShippingAddress() } /** + * Retrieve collection if items. + * * @return mixed */ public function getItemsCollection() @@ -278,6 +290,8 @@ public function getItemsCollection() } /** + * Retrieve all items. + * * @return \Magento\Sales\Model\Order\Creditmemo\Item[] */ public function getAllItems() @@ -292,6 +306,8 @@ public function getAllItems() } /** + * Retrieve item by id. + * * @param mixed $itemId * @return mixed */ @@ -322,6 +338,8 @@ public function getItemByOrderId($orderId) } /** + * Add an item to credit memo. + * * @param \Magento\Sales\Model\Order\Creditmemo\Item $item * @return $this */ @@ -367,6 +385,8 @@ public function roundPrice($price, $type = 'regular', $negative = false) } /** + * Check if credit memo can be refunded. + * * @return bool */ public function canRefund() @@ -464,6 +484,8 @@ public function getStateName($stateId = null) } /** + * Set shipping amount. + * * @param float $amount * @return $this */ @@ -473,6 +495,8 @@ public function setShippingAmount($amount) } /** + * Set adjustment positive amount. + * * @param string $amount * @return $this */ @@ -493,6 +517,8 @@ public function setAdjustmentPositive($amount) } /** + * Set adjustment negative amount. + * * @param string $amount * @return $this */ @@ -541,6 +567,8 @@ public function isLast() } /** + * Add comment to credit memo. + * * Adds comment to credit memo with additional possibility to send it to customer via email * and show it in customer account * @@ -567,6 +595,8 @@ public function addComment($comment, $notify = false, $visibleOnFront = false) } /** + * Retrieve collection of comments. + * * @param bool $reload * @return \Magento\Sales\Model\ResourceModel\Order\Creditmemo\Comment\Collection * @SuppressWarnings(PHPMD.UnusedFormalParameter) @@ -606,11 +636,27 @@ public function getIncrementId() } /** + * Check if grand total is valid. + * * @return bool */ public function isValidGrandTotal() { - return !($this->getGrandTotal() <= 0 && !$this->getAllowZeroGrandTotal()); + return !($this->getGrandTotal() <= 0 && !$this->isAllowZeroGrandTotal()); + } + + /** + * Return Zero GrandTotal availability. + * + * @return bool + */ + private function isAllowZeroGrandTotal() + { + $isAllowed = $this->scopeConfig->getValue( + 'sales/zerograndtotal_creditmemo/allow_zero_grandtotal', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + return $isAllowed; } /** @@ -658,7 +704,7 @@ public function getDiscountDescription() } /** - * {@inheritdoc} + * @inheritdoc */ public function setItems($items) { @@ -898,7 +944,7 @@ public function getCreatedAt() } /** - * {@inheritdoc} + * @inheritdoc */ public function setCreatedAt($createdAt) { @@ -1157,7 +1203,7 @@ public function getUpdatedAt() } /** - * {@inheritdoc} + * @inheritdoc */ public function setComments($comments) { @@ -1165,7 +1211,7 @@ public function setComments($comments) } /** - * {@inheritdoc} + * @inheritdoc */ public function setStoreId($id) { @@ -1173,7 +1219,7 @@ public function setStoreId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingTaxAmount($amount) { @@ -1181,7 +1227,7 @@ public function setBaseShippingTaxAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setStoreToOrderRate($rate) { @@ -1189,7 +1235,7 @@ public function setStoreToOrderRate($rate) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseDiscountAmount($amount) { @@ -1197,7 +1243,7 @@ public function setBaseDiscountAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseToOrderRate($rate) { @@ -1205,7 +1251,7 @@ public function setBaseToOrderRate($rate) } /** - * {@inheritdoc} + * @inheritdoc */ public function setGrandTotal($amount) { @@ -1213,7 +1259,7 @@ public function setGrandTotal($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseSubtotalInclTax($amount) { @@ -1221,7 +1267,7 @@ public function setBaseSubtotalInclTax($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setSubtotalInclTax($amount) { @@ -1229,7 +1275,7 @@ public function setSubtotalInclTax($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingAmount($amount) { @@ -1237,7 +1283,7 @@ public function setBaseShippingAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setStoreToBaseRate($rate) { @@ -1245,7 +1291,7 @@ public function setStoreToBaseRate($rate) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseToGlobalRate($rate) { @@ -1253,7 +1299,7 @@ public function setBaseToGlobalRate($rate) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseAdjustment($baseAdjustment) { @@ -1261,7 +1307,7 @@ public function setBaseAdjustment($baseAdjustment) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseSubtotal($amount) { @@ -1269,7 +1315,7 @@ public function setBaseSubtotal($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountAmount($amount) { @@ -1277,7 +1323,7 @@ public function setDiscountAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setSubtotal($amount) { @@ -1285,7 +1331,7 @@ public function setSubtotal($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAdjustment($adjustment) { @@ -1293,7 +1339,7 @@ public function setAdjustment($adjustment) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseGrandTotal($amount) { @@ -1301,7 +1347,7 @@ public function setBaseGrandTotal($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTaxAmount($amount) { @@ -1309,7 +1355,7 @@ public function setBaseTaxAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingTaxAmount($amount) { @@ -1317,7 +1363,7 @@ public function setShippingTaxAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTaxAmount($amount) { @@ -1325,7 +1371,7 @@ public function setTaxAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setOrderId($id) { @@ -1333,7 +1379,7 @@ public function setOrderId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setEmailSent($emailSent) { @@ -1341,7 +1387,7 @@ public function setEmailSent($emailSent) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCreditmemoStatus($creditmemoStatus) { @@ -1349,7 +1395,7 @@ public function setCreditmemoStatus($creditmemoStatus) } /** - * {@inheritdoc} + * @inheritdoc */ public function setState($state) { @@ -1357,7 +1403,7 @@ public function setState($state) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingAddressId($id) { @@ -1365,7 +1411,7 @@ public function setShippingAddressId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBillingAddressId($id) { @@ -1373,7 +1419,7 @@ public function setBillingAddressId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setInvoiceId($id) { @@ -1381,7 +1427,7 @@ public function setInvoiceId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setStoreCurrencyCode($code) { @@ -1389,7 +1435,7 @@ public function setStoreCurrencyCode($code) } /** - * {@inheritdoc} + * @inheritdoc */ public function setOrderCurrencyCode($code) { @@ -1397,7 +1443,7 @@ public function setOrderCurrencyCode($code) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseCurrencyCode($code) { @@ -1405,7 +1451,7 @@ public function setBaseCurrencyCode($code) } /** - * {@inheritdoc} + * @inheritdoc */ public function setGlobalCurrencyCode($code) { @@ -1413,7 +1459,7 @@ public function setGlobalCurrencyCode($code) } /** - * {@inheritdoc} + * @inheritdoc */ public function setIncrementId($id) { @@ -1421,7 +1467,7 @@ public function setIncrementId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setUpdatedAt($timestamp) { @@ -1429,7 +1475,7 @@ public function setUpdatedAt($timestamp) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountTaxCompensationAmount($amount) { @@ -1437,7 +1483,7 @@ public function setDiscountTaxCompensationAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseDiscountTaxCompensationAmount($amount) { @@ -1445,7 +1491,7 @@ public function setBaseDiscountTaxCompensationAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingDiscountTaxCompensationAmount($amount) { @@ -1453,7 +1499,7 @@ public function setShippingDiscountTaxCompensationAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingDiscountTaxCompensationAmnt($amnt) { @@ -1461,7 +1507,7 @@ public function setBaseShippingDiscountTaxCompensationAmnt($amnt) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingInclTax($amount) { @@ -1469,7 +1515,7 @@ public function setShippingInclTax($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingInclTax($amount) { @@ -1477,7 +1523,7 @@ public function setBaseShippingInclTax($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountDescription($description) { @@ -1485,9 +1531,7 @@ public function setDiscountDescription($description) } /** - * {@inheritdoc} - * - * @return \Magento\Sales\Api\Data\CreditmemoExtensionInterface|null + * @inheritdoc */ public function getExtensionAttributes() { @@ -1495,10 +1539,7 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} - * - * @param \Magento\Sales\Api\Data\CreditmemoExtensionInterface $extensionAttributes - * @return $this + * @inheritdoc */ public function setExtensionAttributes(\Magento\Sales\Api\Data\CreditmemoExtensionInterface $extensionAttributes) { diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/Item.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Item.php index 5c3f563a4f07..35244b266138 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo/Item.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Item.php @@ -10,6 +10,8 @@ use Magento\Sales\Model\AbstractModel; /** + * Creditmemo item model. + * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.ExcessivePublicCount) @@ -189,6 +191,8 @@ public function register() } /** + * Calculate qty for creditmemo item. + * * @return int|float * @throws \Magento\Framework\Exception\LocalizedException */ @@ -212,6 +216,8 @@ private function processQty() } /** + * Cancel creaditmemeo item. + * * @return $this */ public function cancel() @@ -236,7 +242,7 @@ public function cancel() /** * Invoice item row total calculation * - * @return \Magento\Sales\Model\Order\Invoice\Item + * @return $this */ public function calcRowTotal() { @@ -608,7 +614,7 @@ public function getWeeeTaxRowDisposition() //@codeCoverageIgnoreStart /** - * {@inheritdoc} + * @inheritdoc */ public function setParentId($id) { @@ -616,7 +622,7 @@ public function setParentId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBasePrice($price) { @@ -624,7 +630,7 @@ public function setBasePrice($price) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTaxAmount($amount) { @@ -632,7 +638,7 @@ public function setTaxAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseRowTotal($amount) { @@ -640,7 +646,7 @@ public function setBaseRowTotal($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountAmount($amount) { @@ -648,7 +654,7 @@ public function setDiscountAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setRowTotal($amount) { @@ -656,7 +662,7 @@ public function setRowTotal($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseDiscountAmount($amount) { @@ -664,7 +670,7 @@ public function setBaseDiscountAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setPriceInclTax($amount) { @@ -672,7 +678,7 @@ public function setPriceInclTax($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTaxAmount($amount) { @@ -680,7 +686,7 @@ public function setBaseTaxAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBasePriceInclTax($amount) { @@ -688,7 +694,7 @@ public function setBasePriceInclTax($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseCost($baseCost) { @@ -696,7 +702,7 @@ public function setBaseCost($baseCost) } /** - * {@inheritdoc} + * @inheritdoc */ public function setPrice($price) { @@ -704,7 +710,7 @@ public function setPrice($price) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseRowTotalInclTax($amount) { @@ -712,7 +718,7 @@ public function setBaseRowTotalInclTax($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setRowTotalInclTax($amount) { @@ -720,7 +726,7 @@ public function setRowTotalInclTax($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setProductId($id) { @@ -728,7 +734,7 @@ public function setProductId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setOrderItemId($id) { @@ -736,7 +742,7 @@ public function setOrderItemId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAdditionalData($additionalData) { @@ -744,7 +750,7 @@ public function setAdditionalData($additionalData) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDescription($description) { @@ -752,7 +758,7 @@ public function setDescription($description) } /** - * {@inheritdoc} + * @inheritdoc */ public function setSku($sku) { @@ -760,7 +766,7 @@ public function setSku($sku) } /** - * {@inheritdoc} + * @inheritdoc */ public function setName($name) { @@ -768,7 +774,7 @@ public function setName($name) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountTaxCompensationAmount($amount) { @@ -776,7 +782,7 @@ public function setDiscountTaxCompensationAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseDiscountTaxCompensationAmount($amount) { @@ -784,7 +790,7 @@ public function setBaseDiscountTaxCompensationAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setWeeeTaxDisposition($weeeTaxDisposition) { @@ -792,7 +798,7 @@ public function setWeeeTaxDisposition($weeeTaxDisposition) } /** - * {@inheritdoc} + * @inheritdoc */ public function setWeeeTaxRowDisposition($weeeTaxRowDisposition) { @@ -800,7 +806,7 @@ public function setWeeeTaxRowDisposition($weeeTaxRowDisposition) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseWeeeTaxDisposition($baseWeeeTaxDisposition) { @@ -808,7 +814,7 @@ public function setBaseWeeeTaxDisposition($baseWeeeTaxDisposition) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseWeeeTaxRowDisposition($baseWeeeTaxRowDisposition) { @@ -816,7 +822,7 @@ public function setBaseWeeeTaxRowDisposition($baseWeeeTaxRowDisposition) } /** - * {@inheritdoc} + * @inheritdoc */ public function setWeeeTaxApplied($weeeTaxApplied) { @@ -824,7 +830,7 @@ public function setWeeeTaxApplied($weeeTaxApplied) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseWeeeTaxAppliedAmount($amount) { @@ -832,7 +838,7 @@ public function setBaseWeeeTaxAppliedAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseWeeeTaxAppliedRowAmnt($amnt) { @@ -840,7 +846,7 @@ public function setBaseWeeeTaxAppliedRowAmnt($amnt) } /** - * {@inheritdoc} + * @inheritdoc */ public function setWeeeTaxAppliedAmount($amount) { @@ -848,7 +854,7 @@ public function setWeeeTaxAppliedAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setWeeeTaxAppliedRowAmount($amount) { @@ -856,7 +862,7 @@ public function setWeeeTaxAppliedRowAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc * * @return \Magento\Sales\Api\Data\CreditmemoItemExtensionInterface|null */ @@ -866,7 +872,7 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * @inheritdoc * * @param \Magento\Sales\Api\Data\CreditmemoItemExtensionInterface $extensionAttributes * @return $this diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/Item/Validation/CreationQuantityValidator.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Item/Validation/CreationQuantityValidator.php index 0c2adfff80a2..93a4e701e032 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo/Item/Validation/CreationQuantityValidator.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Item/Validation/CreationQuantityValidator.php @@ -30,6 +30,7 @@ class CreationQuantityValidator implements ValidatorInterface /** * ItemCreationQuantityValidator constructor. + * * @param OrderItemRepositoryInterface $orderItemRepository * @param mixed $context */ @@ -53,6 +54,10 @@ public function validate($entity) return [__('The creditmemo contains product item that is not part of the original order.')]; } + if ($orderItem->isDummy()) { + return [__('The creditmemo contains incorrect product items.')]; + } + if (!$this->isQtyAvailable($orderItem, $entity->getQty())) { return [__('The quantity to refund must not be greater than the unrefunded quantity.')]; } @@ -61,6 +66,8 @@ public function validate($entity) } /** + * Check the quantity to refund is greater than the unrefunded quantity + * * @param Item $orderItem * @param int $qty * @return bool @@ -71,6 +78,8 @@ private function isQtyAvailable(Item $orderItem, $qty) } /** + * Check to see if Item is part of the order + * * @param OrderItemInterface $orderItem * @return bool */ diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/ItemCreation.php b/app/code/Magento/Sales/Model/Order/Creditmemo/ItemCreation.php index d6da44c5cb5b..3fd3eaaa11a7 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo/ItemCreation.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/ItemCreation.php @@ -28,7 +28,7 @@ class ItemCreation implements CreditmemoItemCreationInterface private $extensionAttributes; /** - * {@inheritdoc} + * @inheritdoc */ public function getOrderItemId() { @@ -36,7 +36,7 @@ public function getOrderItemId() } /** - * {@inheritdoc} + * @inheritdoc */ public function setOrderItemId($orderItemId) { @@ -45,7 +45,7 @@ public function setOrderItemId($orderItemId) } /** - * {@inheritdoc} + * @inheritdoc */ public function getQty() { @@ -53,7 +53,7 @@ public function getQty() } /** - * {@inheritdoc} + * @inheritdoc */ public function setQty($qty) { diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/Sender/EmailSender.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Sender/EmailSender.php index ecd5670a319e..3d2c13cbaaaa 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo/Sender/EmailSender.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Sender/EmailSender.php @@ -89,6 +89,7 @@ public function __construct( * @param bool $forceSyncMode * * @return bool + * @throws \Exception */ public function send( \Magento\Sales\Api\Data\OrderInterface $order, @@ -96,7 +97,7 @@ public function send( \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface $comment = null, $forceSyncMode = false ) { - $creditmemo->setSendEmail(true); + $creditmemo->setSendEmail($this->identityContainer->isEnabled()); if (!$this->globalConfig->getValue('sales_email/general/async_sending') || $forceSyncMode) { $transport = [ @@ -145,6 +146,7 @@ public function send( * @param \Magento\Sales\Api\Data\OrderInterface $order * * @return string + * @throws \Exception */ private function getPaymentHtml(\Magento\Sales\Api\Data\OrderInterface $order) { diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Discount.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Discount.php index 0cc4143e569d..06bfbcf24daa 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Discount.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Discount.php @@ -5,11 +5,17 @@ */ namespace Magento\Sales\Model\Order\Creditmemo\Total; +/** + * Discount total calculator + */ class Discount extends AbstractTotal { /** + * Collect discount + * * @param \Magento\Sales\Model\Order\Creditmemo $creditmemo * @return $this + * @throws \Magento\Framework\Exception\LocalizedException */ public function collect(\Magento\Sales\Model\Order\Creditmemo $creditmemo) { @@ -26,6 +32,16 @@ public function collect(\Magento\Sales\Model\Order\Creditmemo $creditmemo) * basing on how much shipping should be refunded. */ $baseShippingAmount = $this->getBaseShippingAmount($creditmemo); + + /** + * If credit memo's shipping amount is set and Order's shipping amount is 0, + * throw exception with different message + */ + if ($baseShippingAmount && $order->getBaseShippingAmount() <= 0) { + throw new \Magento\Framework\Exception\LocalizedException( + __("You can not refund shipping if there is no shipping amount.") + ); + } if ($baseShippingAmount) { $baseShippingDiscount = $baseShippingAmount * $order->getBaseShippingDiscountAmount() / diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Shipping.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Shipping.php index f00334f496b2..f644d0c3a5a6 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Shipping.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Shipping.php @@ -37,6 +37,8 @@ public function __construct( } /** + * Collects credit memo shipping totals. + * * @param \Magento\Sales\Model\Order\Creditmemo $creditmemo * @return $this * @throws \Magento\Framework\Exception\LocalizedException @@ -55,12 +57,10 @@ public function collect(\Magento\Sales\Model\Order\Creditmemo $creditmemo) $orderShippingInclTax = $order->getShippingInclTax(); $orderBaseShippingInclTax = $order->getBaseShippingInclTax(); $allowedTaxAmount = $order->getShippingTaxAmount() - $order->getShippingTaxRefunded(); - $baseAllowedTaxAmount = $order->getBaseShippingTaxAmount() - $order->getBaseShippingTaxRefunded(); $allowedAmountInclTax = $allowedAmount + $allowedTaxAmount; - $baseAllowedAmountInclTax = $baseAllowedAmount + $baseAllowedTaxAmount; - - // for the credit memo - $shippingAmount = $baseShippingAmount = $shippingInclTax = $baseShippingInclTax = 0; + $baseAllowedAmountInclTax = $orderBaseShippingInclTax + - $order->getBaseShippingRefunded() + - $order->getBaseShippingTaxRefunded(); // Check if the desired shipping amount to refund was specified (from invoice or another source). if ($creditmemo->hasBaseShippingAmount()) { @@ -128,7 +128,6 @@ private function isSuppliedShippingAmountInclTax($order) /** * Get the Tax Config. - * In a future release, will become a constructor parameter. * * @return \Magento\Tax\Model\Config * diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Tax.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Tax.php index a842c0470ad8..d4c2e7b2d685 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Tax.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Tax.php @@ -5,9 +5,14 @@ */ namespace Magento\Sales\Model\Order\Creditmemo\Total; +/** + * Collects credit memo taxes. + */ class Tax extends AbstractTotal { /** + * Collects credit memo taxes. + * * @param \Magento\Sales\Model\Order\Creditmemo $creditmemo * @return $this * @@ -70,27 +75,20 @@ public function collect(\Magento\Sales\Model\Order\Creditmemo $creditmemo) } $isPartialShippingRefunded = false; + $baseOrderShippingAmount = (float)$order->getBaseShippingAmount(); if ($invoice = $creditmemo->getInvoice()) { //recalculate tax amounts in case if refund shipping value was changed - if ($order->getBaseShippingAmount() && $creditmemo->getBaseShippingAmount()) { - $taxFactor = $creditmemo->getBaseShippingAmount() / $order->getBaseShippingAmount(); + if ($baseOrderShippingAmount && $creditmemo->getBaseShippingAmount() !== null) { + $taxFactor = $creditmemo->getBaseShippingAmount() / $baseOrderShippingAmount; $shippingTaxAmount = $invoice->getShippingTaxAmount() * $taxFactor; $baseShippingTaxAmount = $invoice->getBaseShippingTaxAmount() * $taxFactor; $totalDiscountTaxCompensation += $invoice->getShippingDiscountTaxCompensationAmount() * $taxFactor; $baseTotalDiscountTaxCompensation += $invoice->getBaseShippingDiscountTaxCompensationAmnt() * $taxFactor; - $shippingDiscountTaxCompensationAmount = - $invoice->getShippingDiscountTaxCompensationAmount() * $taxFactor; - $baseShippingDiscountTaxCompensationAmount = - $invoice->getBaseShippingDiscountTaxCompensationAmnt() * $taxFactor; $shippingTaxAmount = $creditmemo->roundPrice($shippingTaxAmount); $baseShippingTaxAmount = $creditmemo->roundPrice($baseShippingTaxAmount, 'base'); $totalDiscountTaxCompensation = $creditmemo->roundPrice($totalDiscountTaxCompensation); $baseTotalDiscountTaxCompensation = $creditmemo->roundPrice($baseTotalDiscountTaxCompensation, 'base'); - $shippingDiscountTaxCompensationAmount = - $creditmemo->roundPrice($shippingDiscountTaxCompensationAmount); - $baseShippingDiscountTaxCompensationAmount = - $creditmemo->roundPrice($baseShippingDiscountTaxCompensationAmount, 'base'); if ($taxFactor < 1 && $invoice->getShippingTaxAmount() > 0) { $isPartialShippingRefunded = true; } @@ -99,7 +97,6 @@ public function collect(\Magento\Sales\Model\Order\Creditmemo $creditmemo) } } else { $orderShippingAmount = $order->getShippingAmount(); - $baseOrderShippingAmount = $order->getBaseShippingAmount(); $baseOrderShippingRefundedAmount = $order->getBaseShippingRefunded(); diff --git a/app/code/Magento/Sales/Model/Order/CreditmemoDocumentFactory.php b/app/code/Magento/Sales/Model/Order/CreditmemoDocumentFactory.php index 65c1788c68d0..b86acbab2080 100644 --- a/app/code/Magento/Sales/Model/Order/CreditmemoDocumentFactory.php +++ b/app/code/Magento/Sales/Model/Order/CreditmemoDocumentFactory.php @@ -99,11 +99,14 @@ private function attachComment( ->setCreditmemo($creditmemo) ->setIsCustomerNotified($appendComment); $creditmemo->setComments([$comment]); + $creditmemo->setCustomerNote($comment->getComment()); + $creditmemo->setCustomerNoteNotify($appendComment); return $creditmemo; } /** * Create new Creditmemo + * * @param \Magento\Sales\Api\Data\OrderInterface $order * @param \Magento\Sales\Api\Data\CreditmemoItemCreationInterface[] $items * @param \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface|null $comment @@ -128,6 +131,8 @@ public function createFromOrder( } /** + * Create credit memo from invoice + * * @param \Magento\Sales\Api\Data\InvoiceInterface $invoice * @param \Magento\Sales\Api\Data\CreditmemoItemCreationInterface[] $items * @param \Magento\Sales\Api\Data\CreditmemoCommentCreationInterface|null $comment diff --git a/app/code/Magento/Sales/Model/Order/CreditmemoFactory.php b/app/code/Magento/Sales/Model/Order/CreditmemoFactory.php index e61c2597975b..73afd0a06f71 100644 --- a/app/code/Magento/Sales/Model/Order/CreditmemoFactory.php +++ b/app/code/Magento/Sales/Model/Order/CreditmemoFactory.php @@ -200,6 +200,7 @@ protected function initData($creditmemo, $data) { if (isset($data['shipping_amount'])) { $creditmemo->setBaseShippingAmount((double)$data['shipping_amount']); + $creditmemo->setBaseShippingInclTax((double)$data['shipping_amount']); } if (isset($data['adjustment_positive'])) { $creditmemo->setAdjustmentPositive($data['adjustment_positive']); @@ -210,6 +211,8 @@ protected function initData($creditmemo, $data) } /** + * Calculate product options. + * * @param Item $orderItem * @param int $parentQty * @return int diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender.php b/app/code/Magento/Sales/Model/Order/Email/Sender.php index 6d4480c4c45e..564fd1e2a4b9 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender.php @@ -65,6 +65,8 @@ public function __construct( } /** + * Send order email if it is enabled in configuration. + * * @param Order $order * @return bool */ @@ -81,17 +83,21 @@ protected function checkAndSend(Order $order) try { $sender->send(); - $sender->sendCopyTo(); } catch (\Exception $e) { $this->logger->error($e->getMessage()); - return false; } - + try { + $sender->sendCopyTo(); + } catch (\Exception $e) { + $this->logger->error($e->getMessage()); + } return true; } /** + * Populate order email template with customer information. + * * @param Order $order * @return void */ @@ -113,6 +119,8 @@ protected function prepareTemplate(Order $order) } /** + * Create Sender object using appropriate template and identity. + * * @return Sender */ protected function getSender() @@ -126,6 +134,8 @@ protected function getSender() } /** + * Get template options. + * * @return array */ protected function getTemplateOptions() @@ -137,6 +147,8 @@ protected function getTemplateOptions() } /** + * Render shipping address into html. + * * @param Order $order * @return string|null */ @@ -148,6 +160,8 @@ protected function getFormattedShippingAddress($order) } /** + * Render billing address into html. + * * @param Order $order * @return string|null */ diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/CreditmemoSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/CreditmemoSender.php index 800448358311..126fe4f93f1e 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/CreditmemoSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/CreditmemoSender.php @@ -57,10 +57,10 @@ class CreditmemoSender extends Sender * @param CreditmemoIdentity $identityContainer * @param Order\Email\SenderBuilderFactory $senderBuilderFactory * @param \Psr\Log\LoggerInterface $logger + * @param Renderer $addressRenderer * @param PaymentHelper $paymentHelper * @param CreditmemoResource $creditmemoResource * @param \Magento\Framework\App\Config\ScopeConfigInterface $globalConfig - * @param Renderer $addressRenderer * @param ManagerInterface $eventManager */ public function __construct( @@ -96,10 +96,11 @@ public function __construct( * @param Creditmemo $creditmemo * @param bool $forceSyncMode * @return bool + * @throws \Exception */ public function send(Creditmemo $creditmemo, $forceSyncMode = false) { - $creditmemo->setSendEmail(true); + $creditmemo->setSendEmail($this->identityContainer->isEnabled()); if (!$this->globalConfig->getValue('sales_email/general/async_sending') || $forceSyncMode) { $order = $creditmemo->getOrder(); @@ -146,6 +147,7 @@ public function send(Creditmemo $creditmemo, $forceSyncMode = false) * * @param Order $order * @return string + * @throws \Exception */ protected function getPaymentHtml(Order $order) { diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceSender.php index 994fd79945cf..ba3895cfa152 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceSender.php @@ -57,10 +57,10 @@ class InvoiceSender extends Sender * @param InvoiceIdentity $identityContainer * @param Order\Email\SenderBuilderFactory $senderBuilderFactory * @param \Psr\Log\LoggerInterface $logger + * @param Renderer $addressRenderer * @param PaymentHelper $paymentHelper * @param InvoiceResource $invoiceResource * @param \Magento\Framework\App\Config\ScopeConfigInterface $globalConfig - * @param Renderer $addressRenderer * @param ManagerInterface $eventManager */ public function __construct( @@ -96,10 +96,11 @@ public function __construct( * @param Invoice $invoice * @param bool $forceSyncMode * @return bool + * @throws \Exception */ public function send(Invoice $invoice, $forceSyncMode = false) { - $invoice->setSendEmail(true); + $invoice->setSendEmail($this->identityContainer->isEnabled()); if (!$this->globalConfig->getValue('sales_email/general/async_sending') || $forceSyncMode) { $order = $invoice->getOrder(); @@ -146,6 +147,7 @@ public function send(Invoice $invoice, $forceSyncMode = false) * * @param Order $order * @return string + * @throws \Exception */ protected function getPaymentHtml(Order $order) { diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/OrderSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/OrderSender.php index f06da0de0fd0..bfbe1fb4fd7f 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/OrderSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/OrderSender.php @@ -55,10 +55,10 @@ class OrderSender extends Sender * @param OrderIdentity $identityContainer * @param Order\Email\SenderBuilderFactory $senderBuilderFactory * @param \Psr\Log\LoggerInterface $logger + * @param Renderer $addressRenderer * @param PaymentHelper $paymentHelper * @param OrderResource $orderResource * @param \Magento\Framework\App\Config\ScopeConfigInterface $globalConfig - * @param Renderer $addressRenderer * @param ManagerInterface $eventManager */ public function __construct( @@ -97,7 +97,7 @@ public function __construct( */ public function send(Order $order, $forceSyncMode = false) { - $order->setSendEmail(true); + $order->setSendEmail($this->identityContainer->isEnabled()); if (!$this->globalConfig->getValue('sales_email/general/async_sending') || $forceSyncMode) { if ($this->checkAndSend($order)) { diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentSender.php index 6729c746f556..10e5e37a4939 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentSender.php @@ -57,10 +57,10 @@ class ShipmentSender extends Sender * @param ShipmentIdentity $identityContainer * @param Order\Email\SenderBuilderFactory $senderBuilderFactory * @param \Psr\Log\LoggerInterface $logger + * @param Renderer $addressRenderer * @param PaymentHelper $paymentHelper * @param ShipmentResource $shipmentResource * @param \Magento\Framework\App\Config\ScopeConfigInterface $globalConfig - * @param Renderer $addressRenderer * @param ManagerInterface $eventManager */ public function __construct( @@ -96,10 +96,11 @@ public function __construct( * @param Shipment $shipment * @param bool $forceSyncMode * @return bool + * @throws \Exception */ public function send(Shipment $shipment, $forceSyncMode = false) { - $shipment->setSendEmail(true); + $shipment->setSendEmail($this->identityContainer->isEnabled()); if (!$this->globalConfig->getValue('sales_email/general/async_sending') || $forceSyncMode) { $order = $shipment->getOrder(); @@ -146,6 +147,7 @@ public function send(Shipment $shipment, $forceSyncMode = false) * * @param Order $order * @return string + * @throws \Exception */ protected function getPaymentHtml(Order $order) { diff --git a/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php b/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php index 7ec089b88297..ed9e38822245 100644 --- a/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php +++ b/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php @@ -5,12 +5,14 @@ */ namespace Magento\Sales\Model\Order\Email; -use Magento\Framework\App\ObjectManager; use Magento\Framework\Mail\Template\TransportBuilder; use Magento\Framework\Mail\Template\TransportBuilderByStore; use Magento\Sales\Model\Order\Email\Container\IdentityInterface; use Magento\Sales\Model\Order\Email\Container\Template; +/** + * Sender Builder + */ class SenderBuilder { /** @@ -29,11 +31,8 @@ class SenderBuilder protected $transportBuilder; /** - * @var TransportBuilderByStore - */ - private $transportBuilderByStore; - - /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * * @param Template $templateContainer * @param IdentityInterface $identityContainer * @param TransportBuilder $transportBuilder @@ -48,9 +47,6 @@ public function __construct( $this->templateContainer = $templateContainer; $this->identityContainer = $identityContainer; $this->transportBuilder = $transportBuilder; - $this->transportBuilderByStore = $transportBuilderByStore ?: ObjectManager::getInstance()->get( - TransportBuilderByStore::class - ); } /** @@ -110,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->transportBuilderByStore->setFromByStore( + $this->transportBuilder->setFromByScope( $this->identityContainer->getEmailIdentity(), $this->identityContainer->getStore()->getId() ); diff --git a/app/code/Magento/Sales/Model/Order/Invoice.php b/app/code/Magento/Sales/Model/Order/Invoice.php index 3f2fa1f72f6e..14dd0b14ac1f 100644 --- a/app/code/Magento/Sales/Model/Order/Invoice.php +++ b/app/code/Magento/Sales/Model/Order/Invoice.php @@ -11,6 +11,8 @@ use Magento\Sales\Model\EntityInterface; /** + * Invoice model. + * * @api * @method \Magento\Sales\Model\Order\Invoice setSendEmail(bool $value) * @method \Magento\Sales\Model\Order\Invoice setCustomerNote(string $value) @@ -445,10 +447,6 @@ public function cancel() $order->setBaseDiscountInvoiced($order->getBaseDiscountInvoiced() - $this->getBaseDiscountAmount()); $order->setBaseTotalInvoicedCost($order->getBaseTotalInvoicedCost() - $this->getBaseCost()); - if ($this->getState() == self::STATE_PAID) { - $order->setTotalPaid($order->getTotalPaid() - $this->getGrandTotal()); - $order->setBaseTotalPaid($order->getBaseTotalPaid() - $this->getBaseGrandTotal()); - } $this->setState(self::STATE_CANCELED); $order->setState(\Magento\Sales\Model\Order::STATE_PROCESSING) ->setStatus($order->getConfig()->getStateDefaultStatus(\Magento\Sales\Model\Order::STATE_PROCESSING)); @@ -508,6 +506,8 @@ public function getItemsCollection() } /** + * Get all Invoice Items. + * * @return array */ public function getAllItems() @@ -522,6 +522,8 @@ public function getAllItems() } /** + * Get Invoice Item by id. + * * @param int|string $itemId * @return bool|\Magento\Sales\Model\Order\Invoice\Item */ @@ -536,8 +538,11 @@ public function getItemById($itemId) } /** + * Add Item to Invoice. + * * @param \Magento\Sales\Model\Order\Invoice\Item $item * @return $this + * @throws \Exception */ public function addItem(\Magento\Sales\Model\Order\Invoice\Item $item) { @@ -682,13 +687,13 @@ public function isLast() } /** - * Adds comment to invoice with additional possibility to send it to customer via email - * and show it in customer account + * Add comment with possibility to send it to customer via email and show it in customer account. * * @param string $comment * @param bool $notify * @param bool $visibleOnFront * @return $this + * @throws \Exception */ public function addComment($comment, $notify = false, $visibleOnFront = false) { @@ -710,6 +715,8 @@ public function addComment($comment, $notify = false, $visibleOnFront = false) } /** + * Get Invoice comments. + * * @param bool $reload * @return \Magento\Sales\Model\ResourceModel\Order\Invoice\Comment\Collection */ @@ -839,7 +846,7 @@ public function getDiscountDescription() } /** - * {@inheritdoc} + * @inheritdoc */ public function setItems($items) { @@ -1007,7 +1014,7 @@ public function getCreatedAt() } /** - * {@inheritdoc} + * @inheritdoc */ public function setCreatedAt($createdAt) { @@ -1266,7 +1273,7 @@ public function getUpdatedAt() } /** - * {@inheritdoc} + * @inheritdoc */ public function setComments($comments) { @@ -1274,7 +1281,7 @@ public function setComments($comments) } /** - * {@inheritdoc} + * @inheritdoc */ public function setUpdatedAt($timestamp) { @@ -1282,7 +1289,7 @@ public function setUpdatedAt($timestamp) } /** - * {@inheritdoc} + * @inheritdoc */ public function setStoreId($id) { @@ -1290,7 +1297,7 @@ public function setStoreId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseGrandTotal($amount) { @@ -1298,7 +1305,7 @@ public function setBaseGrandTotal($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingTaxAmount($amount) { @@ -1306,7 +1313,7 @@ public function setShippingTaxAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTaxAmount($amount) { @@ -1314,7 +1321,7 @@ public function setTaxAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTaxAmount($amount) { @@ -1322,7 +1329,7 @@ public function setBaseTaxAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setStoreToOrderRate($rate) { @@ -1330,7 +1337,7 @@ public function setStoreToOrderRate($rate) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingTaxAmount($amount) { @@ -1338,7 +1345,7 @@ public function setBaseShippingTaxAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseDiscountAmount($amount) { @@ -1346,7 +1353,7 @@ public function setBaseDiscountAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseToOrderRate($rate) { @@ -1354,7 +1361,7 @@ public function setBaseToOrderRate($rate) } /** - * {@inheritdoc} + * @inheritdoc */ public function setGrandTotal($amount) { @@ -1362,7 +1369,7 @@ public function setGrandTotal($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingAmount($amount) { @@ -1370,7 +1377,7 @@ public function setShippingAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setSubtotalInclTax($amount) { @@ -1378,7 +1385,7 @@ public function setSubtotalInclTax($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseSubtotalInclTax($amount) { @@ -1386,7 +1393,7 @@ public function setBaseSubtotalInclTax($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setStoreToBaseRate($rate) { @@ -1394,7 +1401,7 @@ public function setStoreToBaseRate($rate) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingAmount($amount) { @@ -1402,7 +1409,7 @@ public function setBaseShippingAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTotalQty($qty) { @@ -1410,7 +1417,7 @@ public function setTotalQty($qty) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseToGlobalRate($rate) { @@ -1418,7 +1425,7 @@ public function setBaseToGlobalRate($rate) } /** - * {@inheritdoc} + * @inheritdoc */ public function setSubtotal($amount) { @@ -1426,7 +1433,7 @@ public function setSubtotal($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseSubtotal($amount) { @@ -1434,7 +1441,7 @@ public function setBaseSubtotal($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountAmount($amount) { @@ -1442,7 +1449,7 @@ public function setDiscountAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBillingAddressId($id) { @@ -1450,7 +1457,7 @@ public function setBillingAddressId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setIsUsedForRefund($isUsedForRefund) { @@ -1458,7 +1465,7 @@ public function setIsUsedForRefund($isUsedForRefund) } /** - * {@inheritdoc} + * @inheritdoc */ public function setOrderId($id) { @@ -1466,7 +1473,7 @@ public function setOrderId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setEmailSent($emailSent) { @@ -1474,7 +1481,7 @@ public function setEmailSent($emailSent) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCanVoidFlag($canVoidFlag) { @@ -1482,7 +1489,7 @@ public function setCanVoidFlag($canVoidFlag) } /** - * {@inheritdoc} + * @inheritdoc */ public function setState($state) { @@ -1490,7 +1497,7 @@ public function setState($state) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingAddressId($id) { @@ -1498,7 +1505,7 @@ public function setShippingAddressId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setStoreCurrencyCode($code) { @@ -1506,7 +1513,7 @@ public function setStoreCurrencyCode($code) } /** - * {@inheritdoc} + * @inheritdoc */ public function setOrderCurrencyCode($code) { @@ -1514,7 +1521,7 @@ public function setOrderCurrencyCode($code) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseCurrencyCode($code) { @@ -1522,7 +1529,7 @@ public function setBaseCurrencyCode($code) } /** - * {@inheritdoc} + * @inheritdoc */ public function setGlobalCurrencyCode($code) { @@ -1530,7 +1537,7 @@ public function setGlobalCurrencyCode($code) } /** - * {@inheritdoc} + * @inheritdoc */ public function setIncrementId($id) { @@ -1538,7 +1545,7 @@ public function setIncrementId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountTaxCompensationAmount($amount) { @@ -1546,7 +1553,7 @@ public function setDiscountTaxCompensationAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseDiscountTaxCompensationAmount($amount) { @@ -1554,7 +1561,7 @@ public function setBaseDiscountTaxCompensationAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingDiscountTaxCompensationAmount($amount) { @@ -1562,7 +1569,7 @@ public function setShippingDiscountTaxCompensationAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingDiscountTaxCompensationAmnt($amnt) { @@ -1570,7 +1577,7 @@ public function setBaseShippingDiscountTaxCompensationAmnt($amnt) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingInclTax($amount) { @@ -1578,7 +1585,7 @@ public function setShippingInclTax($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingInclTax($amount) { @@ -1586,7 +1593,7 @@ public function setBaseShippingInclTax($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTotalRefunded($amount) { @@ -1594,7 +1601,7 @@ public function setBaseTotalRefunded($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountDescription($description) { @@ -1602,7 +1609,7 @@ public function setDiscountDescription($description) } /** - * {@inheritdoc} + * @inheritdoc * * @return \Magento\Sales\Api\Data\InvoiceExtensionInterface|null */ @@ -1612,7 +1619,7 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * @inheritdoc * * @param \Magento\Sales\Api\Data\InvoiceExtensionInterface $extensionAttributes * @return $this diff --git a/app/code/Magento/Sales/Model/Order/Invoice/Sender/EmailSender.php b/app/code/Magento/Sales/Model/Order/Invoice/Sender/EmailSender.php index aa0687bee504..5ae3306ddf75 100644 --- a/app/code/Magento/Sales/Model/Order/Invoice/Sender/EmailSender.php +++ b/app/code/Magento/Sales/Model/Order/Invoice/Sender/EmailSender.php @@ -89,6 +89,7 @@ public function __construct( * @param bool $forceSyncMode * * @return bool + * @throws \Exception */ public function send( \Magento\Sales\Api\Data\OrderInterface $order, @@ -96,7 +97,7 @@ public function send( \Magento\Sales\Api\Data\InvoiceCommentCreationInterface $comment = null, $forceSyncMode = false ) { - $invoice->setSendEmail(true); + $invoice->setSendEmail($this->identityContainer->isEnabled()); if (!$this->globalConfig->getValue('sales_email/general/async_sending') || $forceSyncMode) { $transport = [ @@ -145,6 +146,7 @@ public function send( * @param \Magento\Sales\Api\Data\OrderInterface $order * * @return string + * @throws \Exception */ private function getPaymentHtml(\Magento\Sales\Api\Data\OrderInterface $order) { diff --git a/app/code/Magento/Sales/Model/Order/Invoice/Total/Tax.php b/app/code/Magento/Sales/Model/Order/Invoice/Total/Tax.php index fd5c015d9db4..6e12f10f0c67 100644 --- a/app/code/Magento/Sales/Model/Order/Invoice/Total/Tax.php +++ b/app/code/Magento/Sales/Model/Order/Invoice/Total/Tax.php @@ -5,6 +5,9 @@ */ namespace Magento\Sales\Model\Order\Invoice\Total; +/** + * Collects invoice taxes. + */ class Tax extends AbstractTotal { /** @@ -69,11 +72,24 @@ public function collect(\Magento\Sales\Model\Order\Invoice $invoice) } } + $taxDiscountCompensationAmt = $totalDiscountTaxCompensation; + $baseTaxDiscountCompensationAmt = $baseTotalDiscountTaxCompensation; + $allowedDiscountTaxCompensation = $order->getDiscountTaxCompensationAmount() - + $order->getDiscountTaxCompensationInvoiced(); + $allowedBaseDiscountTaxCompensation = $order->getBaseDiscountTaxCompensationAmount() - + $order->getBaseDiscountTaxCompensationInvoiced(); + if ($this->_canIncludeShipping($invoice)) { $totalTax += $order->getShippingTaxAmount(); $baseTotalTax += $order->getBaseShippingTaxAmount(); $totalDiscountTaxCompensation += $order->getShippingDiscountTaxCompensationAmount(); $baseTotalDiscountTaxCompensation += $order->getBaseShippingDiscountTaxCompensationAmnt(); + + $allowedDiscountTaxCompensation += $order->getShippingDiscountTaxCompensationAmount() - + $order->getShippingDiscountTaxCompensationInvoiced(); + $allowedBaseDiscountTaxCompensation += $order->getBaseShippingDiscountTaxCompensationAmnt() - + $order->getBaseShippingDiscountTaxCompensationInvoiced(); + $invoice->setShippingTaxAmount($order->getShippingTaxAmount()); $invoice->setBaseShippingTaxAmount($order->getBaseShippingTaxAmount()); $invoice->setShippingDiscountTaxCompensationAmount($order->getShippingDiscountTaxCompensationAmount()); @@ -81,14 +97,6 @@ public function collect(\Magento\Sales\Model\Order\Invoice $invoice) } $allowedTax = $order->getTaxAmount() - $order->getTaxInvoiced(); $allowedBaseTax = $order->getBaseTaxAmount() - $order->getBaseTaxInvoiced(); - $allowedDiscountTaxCompensation = $order->getDiscountTaxCompensationAmount() + - $order->getShippingDiscountTaxCompensationAmount() - - $order->getDiscountTaxCompensationInvoiced() - - $order->getShippingDiscountTaxCompensationInvoiced(); - $allowedBaseDiscountTaxCompensation = $order->getBaseDiscountTaxCompensationAmount() + - $order->getBaseShippingDiscountTaxCompensationAmnt() - - $order->getBaseDiscountTaxCompensationInvoiced() - - $order->getBaseShippingDiscountTaxCompensationInvoiced(); if ($invoice->isLast()) { $totalTax = $allowedTax; @@ -107,8 +115,8 @@ public function collect(\Magento\Sales\Model\Order\Invoice $invoice) $invoice->setTaxAmount($totalTax); $invoice->setBaseTaxAmount($baseTotalTax); - $invoice->setDiscountTaxCompensationAmount($totalDiscountTaxCompensation); - $invoice->setBaseDiscountTaxCompensationAmount($baseTotalDiscountTaxCompensation); + $invoice->setDiscountTaxCompensationAmount($taxDiscountCompensationAmt); + $invoice->setBaseDiscountTaxCompensationAmount($baseTaxDiscountCompensationAmt); $invoice->setGrandTotal($invoice->getGrandTotal() + $totalTax + $totalDiscountTaxCompensation); $invoice->setBaseGrandTotal($invoice->getBaseGrandTotal() + $baseTotalTax + $baseTotalDiscountTaxCompensation); diff --git a/app/code/Magento/Sales/Model/Order/Item.php b/app/code/Magento/Sales/Model/Order/Item.php index d2f5f5ce5669..a0eff45179ac 100644 --- a/app/code/Magento/Sales/Model/Order/Item.php +++ b/app/code/Magento/Sales/Model/Order/Item.php @@ -483,6 +483,7 @@ public function getProductOptions() /** * Get product options array by code. + * * If code is null return all options * * @param string $code @@ -540,8 +541,7 @@ public function getChildrenItems() } /** - * Return checking of what calculation - * type was for this product + * Return checking of what calculation type was for this product * * @return bool */ @@ -581,8 +581,7 @@ public function getForceApplyDiscountToParentItem() } /** - * Return checking of what shipment - * type was for this product + * Return checking of what shipment type was for this product * * @return bool */ @@ -605,9 +604,9 @@ public function isShipSeparately() } /** - * This is Dummy item or not - * if $shipment is true then we checking this for shipping situation if not - * then we checking this for calculation + * This is Dummy item or not. + * + * If $shipment is true then we checking this for shipping situation if not, we checking this for calculation. * * @param bool $shipment * @return bool @@ -652,8 +651,9 @@ public function isDummy($shipment = false) } /** - * Returns formatted buy request - object, holding request received from - * product view page with keys and options for configured product + * Return formatted buy request. + * + * This object is holding request received from product view page with keys and options for configured product. * * @return \Magento\Framework\DataObject */ @@ -703,7 +703,7 @@ public function getStore() //@codeCoverageIgnoreStart /** - * Returns additional_data + * Return additional_data * * @return string|null */ @@ -713,7 +713,7 @@ public function getAdditionalData() } /** - * Returns amount_refunded + * Return amount_refunded * * @return float|null */ @@ -723,7 +723,9 @@ public function getAmountRefunded() } /** - * Returns applied_rule_ids + * Return applied_rule_ids + * + * Rules are comma separated if there are more than one. * * @return string|null */ @@ -733,7 +735,7 @@ public function getAppliedRuleIds() } /** - * Returns base_amount_refunded + * Return base_amount_refunded * * @return float|null */ @@ -743,7 +745,7 @@ public function getBaseAmountRefunded() } /** - * Returns base_cost + * Return base_cost * * @return float|null */ @@ -753,7 +755,7 @@ public function getBaseCost() } /** - * Returns base_discount_amount + * Return base_discount_amount * * @return float|null */ @@ -763,7 +765,7 @@ public function getBaseDiscountAmount() } /** - * Returns base_discount_invoiced + * Return base_discount_invoiced * * @return float|null */ @@ -773,7 +775,7 @@ public function getBaseDiscountInvoiced() } /** - * Returns base_discount_refunded + * Return base_discount_refunded * * @return float|null */ @@ -783,7 +785,7 @@ public function getBaseDiscountRefunded() } /** - * Returns base_discount_tax_compensation_amount + * Return base_discount_tax_compensation_amount * * @return float|null */ @@ -793,7 +795,7 @@ public function getBaseDiscountTaxCompensationAmount() } /** - * Returns base_discount_tax_compensation_invoiced + * Return base_discount_tax_compensation_invoiced * * @return float|null */ @@ -803,7 +805,7 @@ public function getBaseDiscountTaxCompensationInvoiced() } /** - * Returns base_discount_tax_compensation_refunded + * Return base_discount_tax_compensation_refunded * * @return float|null */ @@ -813,7 +815,7 @@ public function getBaseDiscountTaxCompensationRefunded() } /** - * Returns base_original_price + * Return base_original_price * * @return float|null */ @@ -823,7 +825,7 @@ public function getBaseOriginalPrice() } /** - * Returns base_price + * Return base_price * * @return float|null */ @@ -833,7 +835,7 @@ public function getBasePrice() } /** - * Returns base_price_incl_tax + * Return base_price_incl_tax * * @return float|null */ @@ -843,7 +845,7 @@ public function getBasePriceInclTax() } /** - * Returns base_row_invoiced + * Return base_row_invoiced * * @return float|null */ @@ -853,7 +855,7 @@ public function getBaseRowInvoiced() } /** - * Returns base_row_total + * Return base_row_total * * @return float|null */ @@ -863,7 +865,7 @@ public function getBaseRowTotal() } /** - * Returns base_row_total_incl_tax + * Return base_row_total_incl_tax * * @return float|null */ @@ -873,7 +875,7 @@ public function getBaseRowTotalInclTax() } /** - * Returns base_tax_amount + * Return base_tax_amount * * @return float|null */ @@ -883,7 +885,7 @@ public function getBaseTaxAmount() } /** - * Returns base_tax_before_discount + * Return base_tax_before_discount * * @return float|null */ @@ -893,7 +895,7 @@ public function getBaseTaxBeforeDiscount() } /** - * Returns base_tax_invoiced + * Return base_tax_invoiced * * @return float|null */ @@ -903,7 +905,7 @@ public function getBaseTaxInvoiced() } /** - * Returns base_tax_refunded + * Return base_tax_refunded * * @return float|null */ @@ -913,7 +915,7 @@ public function getBaseTaxRefunded() } /** - * Returns base_weee_tax_applied_amount + * Return base_weee_tax_applied_amount * * @return float|null */ @@ -923,7 +925,7 @@ public function getBaseWeeeTaxAppliedAmount() } /** - * Returns base_weee_tax_applied_row_amnt + * Return base_weee_tax_applied_row_amnt * * @return float|null */ @@ -933,7 +935,7 @@ public function getBaseWeeeTaxAppliedRowAmnt() } /** - * Returns base_weee_tax_disposition + * Return base_weee_tax_disposition * * @return float|null */ @@ -943,7 +945,7 @@ public function getBaseWeeeTaxDisposition() } /** - * Returns base_weee_tax_row_disposition + * Return base_weee_tax_row_disposition * * @return float|null */ @@ -953,7 +955,7 @@ public function getBaseWeeeTaxRowDisposition() } /** - * Returns created_at + * Return created_at * * @return string|null */ @@ -963,7 +965,7 @@ public function getCreatedAt() } /** - * {@inheritdoc} + * @inheritdoc */ public function setCreatedAt($createdAt) { @@ -971,7 +973,7 @@ public function setCreatedAt($createdAt) } /** - * Returns description + * Return description * * @return string|null */ @@ -981,7 +983,7 @@ public function getDescription() } /** - * Returns discount_amount + * Return discount_amount * * @return float|null */ @@ -991,7 +993,7 @@ public function getDiscountAmount() } /** - * Returns discount_invoiced + * Return discount_invoiced * * @return float|null */ @@ -1001,7 +1003,7 @@ public function getDiscountInvoiced() } /** - * Returns discount_percent + * Return discount_percent * * @return float|null */ @@ -1011,7 +1013,7 @@ public function getDiscountPercent() } /** - * Returns discount_refunded + * Return discount_refunded * * @return float|null */ @@ -1021,7 +1023,7 @@ public function getDiscountRefunded() } /** - * Returns event_id + * Return event_id * * @return int|null */ @@ -1031,7 +1033,7 @@ public function getEventId() } /** - * Returns ext_order_item_id + * Return ext_order_item_id * * @return string|null */ @@ -1041,7 +1043,7 @@ public function getExtOrderItemId() } /** - * Returns free_shipping + * Return free_shipping * * @return int|null */ @@ -1051,7 +1053,7 @@ public function getFreeShipping() } /** - * Returns gw_base_price + * Return gw_base_price * * @return float|null */ @@ -1061,7 +1063,7 @@ public function getGwBasePrice() } /** - * Returns gw_base_price_invoiced + * Return gw_base_price_invoiced * * @return float|null */ @@ -1071,7 +1073,7 @@ public function getGwBasePriceInvoiced() } /** - * Returns gw_base_price_refunded + * Return gw_base_price_refunded * * @return float|null */ @@ -1081,7 +1083,7 @@ public function getGwBasePriceRefunded() } /** - * Returns gw_base_tax_amount + * Return gw_base_tax_amount * * @return float|null */ @@ -1091,7 +1093,7 @@ public function getGwBaseTaxAmount() } /** - * Returns gw_base_tax_amount_invoiced + * Return gw_base_tax_amount_invoiced * * @return float|null */ @@ -1101,7 +1103,7 @@ public function getGwBaseTaxAmountInvoiced() } /** - * Returns gw_base_tax_amount_refunded + * Return gw_base_tax_amount_refunded * * @return float|null */ @@ -1111,7 +1113,7 @@ public function getGwBaseTaxAmountRefunded() } /** - * Returns gw_id + * Return gw_id * * @return int|null */ @@ -1121,7 +1123,7 @@ public function getGwId() } /** - * Returns gw_price + * Return gw_price * * @return float|null */ @@ -1131,7 +1133,7 @@ public function getGwPrice() } /** - * Returns gw_price_invoiced + * Return gw_price_invoiced * * @return float|null */ @@ -1141,7 +1143,7 @@ public function getGwPriceInvoiced() } /** - * Returns gw_price_refunded + * Return gw_price_refunded * * @return float|null */ @@ -1151,7 +1153,7 @@ public function getGwPriceRefunded() } /** - * Returns gw_tax_amount + * Return gw_tax_amount * * @return float|null */ @@ -1161,7 +1163,7 @@ public function getGwTaxAmount() } /** - * Returns gw_tax_amount_invoiced + * Return gw_tax_amount_invoiced * * @return float|null */ @@ -1171,7 +1173,7 @@ public function getGwTaxAmountInvoiced() } /** - * Returns gw_tax_amount_refunded + * Return gw_tax_amount_refunded * * @return float|null */ @@ -1181,7 +1183,7 @@ public function getGwTaxAmountRefunded() } /** - * Returns discount_tax_compensation_amount + * Return discount_tax_compensation_amount * * @return float|null */ @@ -1191,7 +1193,7 @@ public function getDiscountTaxCompensationAmount() } /** - * Returns discount_tax_compensation_canceled + * Return discount_tax_compensation_canceled * * @return float|null */ @@ -1201,7 +1203,7 @@ public function getDiscountTaxCompensationCanceled() } /** - * Returns discount_tax_compensation_invoiced + * Return discount_tax_compensation_invoiced * * @return float|null */ @@ -1211,7 +1213,7 @@ public function getDiscountTaxCompensationInvoiced() } /** - * Returns discount_tax_compensation_refunded + * Return discount_tax_compensation_refunded * * @return float|null */ @@ -1221,7 +1223,7 @@ public function getDiscountTaxCompensationRefunded() } /** - * Returns is_qty_decimal + * Return is_qty_decimal * * @return int|null */ @@ -1231,7 +1233,7 @@ public function getIsQtyDecimal() } /** - * Returns is_virtual + * Return is_virtual * * @return int|null */ @@ -1241,7 +1243,7 @@ public function getIsVirtual() } /** - * Returns item_id + * Return item_id * * @return int|null */ @@ -1251,7 +1253,7 @@ public function getItemId() } /** - * Returns locked_do_invoice + * Return locked_do_invoice * * @return int|null */ @@ -1261,7 +1263,7 @@ public function getLockedDoInvoice() } /** - * Returns locked_do_ship + * Return locked_do_ship * * @return int|null */ @@ -1271,7 +1273,7 @@ public function getLockedDoShip() } /** - * Returns name + * Return name * * @return string|null */ @@ -1281,7 +1283,7 @@ public function getName() } /** - * Returns no_discount + * Return no_discount * * @return int|null */ @@ -1291,7 +1293,7 @@ public function getNoDiscount() } /** - * Returns order_id + * Return order_id * * @return int|null */ @@ -1301,7 +1303,7 @@ public function getOrderId() } /** - * Returns parent_item_id + * Return parent_item_id * * @return int|null */ @@ -1311,7 +1313,7 @@ public function getParentItemId() } /** - * Returns price + * Return price * * @return float|null */ @@ -1321,7 +1323,7 @@ public function getPrice() } /** - * Returns price_incl_tax + * Return price_incl_tax * * @return float|null */ @@ -1331,7 +1333,7 @@ public function getPriceInclTax() } /** - * Returns product_id + * Return product_id * * @return int|null */ @@ -1341,7 +1343,7 @@ public function getProductId() } /** - * Returns product_type + * Return product_type * * @return string|null */ @@ -1351,7 +1353,7 @@ public function getProductType() } /** - * Returns qty_backordered + * Return qty_backordered * * @return float|null */ @@ -1361,7 +1363,7 @@ public function getQtyBackordered() } /** - * Returns qty_canceled + * Return qty_canceled * * @return float|null */ @@ -1371,7 +1373,7 @@ public function getQtyCanceled() } /** - * Returns qty_invoiced + * Return qty_invoiced * * @return float|null */ @@ -1381,7 +1383,7 @@ public function getQtyInvoiced() } /** - * Returns qty_ordered + * Return qty_ordered * * @return float|null */ @@ -1391,7 +1393,7 @@ public function getQtyOrdered() } /** - * Returns qty_refunded + * Return qty_refunded * * @return float|null */ @@ -1401,7 +1403,7 @@ public function getQtyRefunded() } /** - * Returns qty_returned + * Return qty_returned * * @return float|null */ @@ -1411,7 +1413,7 @@ public function getQtyReturned() } /** - * Returns qty_shipped + * Return qty_shipped * * @return float|null */ @@ -1421,7 +1423,7 @@ public function getQtyShipped() } /** - * Returns quote_item_id + * Return quote_item_id * * @return int|null */ @@ -1431,7 +1433,7 @@ public function getQuoteItemId() } /** - * Returns row_invoiced + * Return row_invoiced * * @return float|null */ @@ -1441,7 +1443,7 @@ public function getRowInvoiced() } /** - * Returns row_total + * Return row_total * * @return float|null */ @@ -1451,7 +1453,7 @@ public function getRowTotal() } /** - * Returns row_total_incl_tax + * Return row_total_incl_tax * * @return float|null */ @@ -1461,7 +1463,7 @@ public function getRowTotalInclTax() } /** - * Returns row_weight + * Return row_weight * * @return float|null */ @@ -1471,7 +1473,7 @@ public function getRowWeight() } /** - * Returns sku + * Return sku * * @return string */ @@ -1481,7 +1483,7 @@ public function getSku() } /** - * Returns store_id + * Return store_id * * @return int|null */ @@ -1491,7 +1493,7 @@ public function getStoreId() } /** - * Returns tax_amount + * Return tax_amount * * @return float|null */ @@ -1501,7 +1503,7 @@ public function getTaxAmount() } /** - * Returns tax_before_discount + * Return tax_before_discount * * @return float|null */ @@ -1511,7 +1513,7 @@ public function getTaxBeforeDiscount() } /** - * Returns tax_canceled + * Return tax_canceled * * @return float|null */ @@ -1521,7 +1523,7 @@ public function getTaxCanceled() } /** - * Returns tax_invoiced + * Return tax_invoiced * * @return float|null */ @@ -1531,7 +1533,7 @@ public function getTaxInvoiced() } /** - * Returns tax_percent + * Return tax_percent * * @return float|null */ @@ -1541,7 +1543,7 @@ public function getTaxPercent() } /** - * Returns tax_refunded + * Return tax_refunded * * @return float|null */ @@ -1551,7 +1553,7 @@ public function getTaxRefunded() } /** - * Returns updated_at + * Return updated_at * * @return string|null */ @@ -1561,7 +1563,7 @@ public function getUpdatedAt() } /** - * Returns weee_tax_applied + * Return weee_tax_applied * * @return string|null */ @@ -1571,7 +1573,7 @@ public function getWeeeTaxApplied() } /** - * Returns weee_tax_applied_amount + * Return weee_tax_applied_amount * * @return float|null */ @@ -1581,7 +1583,7 @@ public function getWeeeTaxAppliedAmount() } /** - * Returns weee_tax_applied_row_amount + * Return weee_tax_applied_row_amount * * @return float|null */ @@ -1591,7 +1593,7 @@ public function getWeeeTaxAppliedRowAmount() } /** - * Returns weee_tax_disposition + * Return weee_tax_disposition * * @return float|null */ @@ -1601,7 +1603,7 @@ public function getWeeeTaxDisposition() } /** - * Returns weee_tax_row_disposition + * Return weee_tax_row_disposition * * @return float|null */ @@ -1611,7 +1613,7 @@ public function getWeeeTaxRowDisposition() } /** - * Returns weight + * Return weight * * @return float|null */ @@ -1621,7 +1623,7 @@ public function getWeight() } /** - * {@inheritdoc} + * @inheritdoc */ public function setUpdatedAt($timestamp) { @@ -1629,7 +1631,7 @@ public function setUpdatedAt($timestamp) } /** - * {@inheritdoc} + * @inheritdoc */ public function setItemId($id) { @@ -1637,7 +1639,7 @@ public function setItemId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setOrderId($id) { @@ -1645,7 +1647,7 @@ public function setOrderId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setParentItemId($id) { @@ -1653,7 +1655,7 @@ public function setParentItemId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setQuoteItemId($id) { @@ -1661,7 +1663,7 @@ public function setQuoteItemId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setStoreId($id) { @@ -1669,7 +1671,7 @@ public function setStoreId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setProductId($id) { @@ -1677,7 +1679,7 @@ public function setProductId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setProductType($productType) { @@ -1685,7 +1687,7 @@ public function setProductType($productType) } /** - * {@inheritdoc} + * @inheritdoc */ public function setWeight($weight) { @@ -1693,7 +1695,7 @@ public function setWeight($weight) } /** - * {@inheritdoc} + * @inheritdoc */ public function setIsVirtual($isVirtual) { @@ -1701,7 +1703,7 @@ public function setIsVirtual($isVirtual) } /** - * {@inheritdoc} + * @inheritdoc */ public function setSku($sku) { @@ -1709,7 +1711,7 @@ public function setSku($sku) } /** - * {@inheritdoc} + * @inheritdoc */ public function setName($name) { @@ -1717,7 +1719,7 @@ public function setName($name) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDescription($description) { @@ -1725,7 +1727,7 @@ public function setDescription($description) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAppliedRuleIds($appliedRuleIds) { @@ -1733,7 +1735,7 @@ public function setAppliedRuleIds($appliedRuleIds) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAdditionalData($additionalData) { @@ -1741,7 +1743,7 @@ public function setAdditionalData($additionalData) } /** - * {@inheritdoc} + * @inheritdoc */ public function setIsQtyDecimal($isQtyDecimal) { @@ -1749,7 +1751,7 @@ public function setIsQtyDecimal($isQtyDecimal) } /** - * {@inheritdoc} + * @inheritdoc */ public function setNoDiscount($noDiscount) { @@ -1757,7 +1759,7 @@ public function setNoDiscount($noDiscount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setQtyBackordered($qtyBackordered) { @@ -1765,7 +1767,7 @@ public function setQtyBackordered($qtyBackordered) } /** - * {@inheritdoc} + * @inheritdoc */ public function setQtyCanceled($qtyCanceled) { @@ -1773,7 +1775,7 @@ public function setQtyCanceled($qtyCanceled) } /** - * {@inheritdoc} + * @inheritdoc */ public function setQtyInvoiced($qtyInvoiced) { @@ -1781,7 +1783,7 @@ public function setQtyInvoiced($qtyInvoiced) } /** - * {@inheritdoc} + * @inheritdoc */ public function setQtyOrdered($qtyOrdered) { @@ -1789,7 +1791,7 @@ public function setQtyOrdered($qtyOrdered) } /** - * {@inheritdoc} + * @inheritdoc */ public function setQtyRefunded($qtyRefunded) { @@ -1797,7 +1799,7 @@ public function setQtyRefunded($qtyRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setQtyShipped($qtyShipped) { @@ -1805,7 +1807,7 @@ public function setQtyShipped($qtyShipped) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseCost($baseCost) { @@ -1813,7 +1815,7 @@ public function setBaseCost($baseCost) } /** - * {@inheritdoc} + * @inheritdoc */ public function setPrice($price) { @@ -1821,7 +1823,7 @@ public function setPrice($price) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBasePrice($price) { @@ -1829,7 +1831,7 @@ public function setBasePrice($price) } /** - * {@inheritdoc} + * @inheritdoc */ public function setOriginalPrice($price) { @@ -1837,7 +1839,7 @@ public function setOriginalPrice($price) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseOriginalPrice($price) { @@ -1845,7 +1847,7 @@ public function setBaseOriginalPrice($price) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTaxPercent($taxPercent) { @@ -1853,7 +1855,7 @@ public function setTaxPercent($taxPercent) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTaxAmount($amount) { @@ -1861,7 +1863,7 @@ public function setTaxAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTaxAmount($amount) { @@ -1869,7 +1871,7 @@ public function setBaseTaxAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTaxInvoiced($taxInvoiced) { @@ -1877,7 +1879,7 @@ public function setTaxInvoiced($taxInvoiced) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTaxInvoiced($baseTaxInvoiced) { @@ -1885,7 +1887,7 @@ public function setBaseTaxInvoiced($baseTaxInvoiced) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountPercent($discountPercent) { @@ -1893,7 +1895,7 @@ public function setDiscountPercent($discountPercent) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountAmount($amount) { @@ -1901,7 +1903,7 @@ public function setDiscountAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseDiscountAmount($amount) { @@ -1909,7 +1911,7 @@ public function setBaseDiscountAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountInvoiced($discountInvoiced) { @@ -1917,7 +1919,7 @@ public function setDiscountInvoiced($discountInvoiced) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseDiscountInvoiced($baseDiscountInvoiced) { @@ -1925,7 +1927,7 @@ public function setBaseDiscountInvoiced($baseDiscountInvoiced) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAmountRefunded($amountRefunded) { @@ -1933,7 +1935,7 @@ public function setAmountRefunded($amountRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseAmountRefunded($baseAmountRefunded) { @@ -1941,7 +1943,7 @@ public function setBaseAmountRefunded($baseAmountRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setRowTotal($amount) { @@ -1949,7 +1951,7 @@ public function setRowTotal($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseRowTotal($amount) { @@ -1957,7 +1959,7 @@ public function setBaseRowTotal($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setRowInvoiced($rowInvoiced) { @@ -1965,7 +1967,7 @@ public function setRowInvoiced($rowInvoiced) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseRowInvoiced($baseRowInvoiced) { @@ -1973,7 +1975,7 @@ public function setBaseRowInvoiced($baseRowInvoiced) } /** - * {@inheritdoc} + * @inheritdoc */ public function setRowWeight($rowWeight) { @@ -1981,7 +1983,7 @@ public function setRowWeight($rowWeight) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTaxBeforeDiscount($baseTaxBeforeDiscount) { @@ -1989,7 +1991,7 @@ public function setBaseTaxBeforeDiscount($baseTaxBeforeDiscount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTaxBeforeDiscount($taxBeforeDiscount) { @@ -1997,7 +1999,7 @@ public function setTaxBeforeDiscount($taxBeforeDiscount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setExtOrderItemId($id) { @@ -2005,7 +2007,7 @@ public function setExtOrderItemId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setLockedDoInvoice($flag) { @@ -2013,7 +2015,7 @@ public function setLockedDoInvoice($flag) } /** - * {@inheritdoc} + * @inheritdoc */ public function setLockedDoShip($flag) { @@ -2021,7 +2023,7 @@ public function setLockedDoShip($flag) } /** - * {@inheritdoc} + * @inheritdoc */ public function setPriceInclTax($amount) { @@ -2029,7 +2031,7 @@ public function setPriceInclTax($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBasePriceInclTax($amount) { @@ -2037,7 +2039,7 @@ public function setBasePriceInclTax($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setRowTotalInclTax($amount) { @@ -2045,7 +2047,7 @@ public function setRowTotalInclTax($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseRowTotalInclTax($amount) { @@ -2053,7 +2055,7 @@ public function setBaseRowTotalInclTax($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountTaxCompensationAmount($amount) { @@ -2061,7 +2063,7 @@ public function setDiscountTaxCompensationAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseDiscountTaxCompensationAmount($amount) { @@ -2069,7 +2071,7 @@ public function setBaseDiscountTaxCompensationAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountTaxCompensationInvoiced($discountTaxCompensationInvoiced) { @@ -2077,7 +2079,7 @@ public function setDiscountTaxCompensationInvoiced($discountTaxCompensationInvoi } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseDiscountTaxCompensationInvoiced($baseDiscountTaxCompensationInvoiced) { @@ -2088,7 +2090,7 @@ public function setBaseDiscountTaxCompensationInvoiced($baseDiscountTaxCompensat } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountTaxCompensationRefunded($discountTaxCompensationRefunded) { @@ -2096,7 +2098,7 @@ public function setDiscountTaxCompensationRefunded($discountTaxCompensationRefun } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseDiscountTaxCompensationRefunded($baseDiscountTaxCompensationRefunded) { @@ -2107,7 +2109,7 @@ public function setBaseDiscountTaxCompensationRefunded($baseDiscountTaxCompensat } /** - * {@inheritdoc} + * @inheritdoc */ public function setTaxCanceled($taxCanceled) { @@ -2115,7 +2117,7 @@ public function setTaxCanceled($taxCanceled) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountTaxCompensationCanceled($discountTaxCompensationCanceled) { @@ -2123,7 +2125,7 @@ public function setDiscountTaxCompensationCanceled($discountTaxCompensationCance } /** - * {@inheritdoc} + * @inheritdoc */ public function setTaxRefunded($taxRefunded) { @@ -2131,7 +2133,7 @@ public function setTaxRefunded($taxRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseTaxRefunded($baseTaxRefunded) { @@ -2139,7 +2141,7 @@ public function setBaseTaxRefunded($baseTaxRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDiscountRefunded($discountRefunded) { @@ -2147,7 +2149,7 @@ public function setDiscountRefunded($discountRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseDiscountRefunded($baseDiscountRefunded) { @@ -2155,7 +2157,7 @@ public function setBaseDiscountRefunded($baseDiscountRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setGwId($id) { @@ -2163,7 +2165,7 @@ public function setGwId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setGwBasePrice($price) { @@ -2171,7 +2173,7 @@ public function setGwBasePrice($price) } /** - * {@inheritdoc} + * @inheritdoc */ public function setGwPrice($price) { @@ -2179,7 +2181,7 @@ public function setGwPrice($price) } /** - * {@inheritdoc} + * @inheritdoc */ public function setGwBaseTaxAmount($amount) { @@ -2187,7 +2189,7 @@ public function setGwBaseTaxAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setGwTaxAmount($amount) { @@ -2195,7 +2197,7 @@ public function setGwTaxAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setGwBasePriceInvoiced($gwBasePriceInvoiced) { @@ -2203,7 +2205,7 @@ public function setGwBasePriceInvoiced($gwBasePriceInvoiced) } /** - * {@inheritdoc} + * @inheritdoc */ public function setGwPriceInvoiced($gwPriceInvoiced) { @@ -2211,7 +2213,7 @@ public function setGwPriceInvoiced($gwPriceInvoiced) } /** - * {@inheritdoc} + * @inheritdoc */ public function setGwBaseTaxAmountInvoiced($gwBaseTaxAmountInvoiced) { @@ -2219,7 +2221,7 @@ public function setGwBaseTaxAmountInvoiced($gwBaseTaxAmountInvoiced) } /** - * {@inheritdoc} + * @inheritdoc */ public function setGwTaxAmountInvoiced($gwTaxAmountInvoiced) { @@ -2227,7 +2229,7 @@ public function setGwTaxAmountInvoiced($gwTaxAmountInvoiced) } /** - * {@inheritdoc} + * @inheritdoc */ public function setGwBasePriceRefunded($gwBasePriceRefunded) { @@ -2235,7 +2237,7 @@ public function setGwBasePriceRefunded($gwBasePriceRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setGwPriceRefunded($gwPriceRefunded) { @@ -2243,7 +2245,7 @@ public function setGwPriceRefunded($gwPriceRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setGwBaseTaxAmountRefunded($gwBaseTaxAmountRefunded) { @@ -2251,7 +2253,7 @@ public function setGwBaseTaxAmountRefunded($gwBaseTaxAmountRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setGwTaxAmountRefunded($gwTaxAmountRefunded) { @@ -2259,7 +2261,7 @@ public function setGwTaxAmountRefunded($gwTaxAmountRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setFreeShipping($freeShipping) { @@ -2267,7 +2269,7 @@ public function setFreeShipping($freeShipping) } /** - * {@inheritdoc} + * @inheritdoc */ public function setQtyReturned($qtyReturned) { @@ -2275,7 +2277,7 @@ public function setQtyReturned($qtyReturned) } /** - * {@inheritdoc} + * @inheritdoc */ public function setEventId($id) { @@ -2283,7 +2285,7 @@ public function setEventId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseWeeeTaxAppliedAmount($amount) { @@ -2291,7 +2293,7 @@ public function setBaseWeeeTaxAppliedAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseWeeeTaxAppliedRowAmnt($amnt) { @@ -2299,7 +2301,7 @@ public function setBaseWeeeTaxAppliedRowAmnt($amnt) } /** - * {@inheritdoc} + * @inheritdoc */ public function setWeeeTaxAppliedAmount($amount) { @@ -2307,7 +2309,7 @@ public function setWeeeTaxAppliedAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setWeeeTaxAppliedRowAmount($amount) { @@ -2315,7 +2317,7 @@ public function setWeeeTaxAppliedRowAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setWeeeTaxApplied($weeeTaxApplied) { @@ -2323,7 +2325,7 @@ public function setWeeeTaxApplied($weeeTaxApplied) } /** - * {@inheritdoc} + * @inheritdoc */ public function setWeeeTaxDisposition($weeeTaxDisposition) { @@ -2331,7 +2333,7 @@ public function setWeeeTaxDisposition($weeeTaxDisposition) } /** - * {@inheritdoc} + * @inheritdoc */ public function setWeeeTaxRowDisposition($weeeTaxRowDisposition) { @@ -2339,7 +2341,7 @@ public function setWeeeTaxRowDisposition($weeeTaxRowDisposition) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseWeeeTaxDisposition($baseWeeeTaxDisposition) { @@ -2347,7 +2349,7 @@ public function setBaseWeeeTaxDisposition($baseWeeeTaxDisposition) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseWeeeTaxRowDisposition($baseWeeeTaxRowDisposition) { @@ -2355,7 +2357,7 @@ public function setBaseWeeeTaxRowDisposition($baseWeeeTaxRowDisposition) } /** - * {@inheritdoc} + * @inheritdoc */ public function getProductOption() { @@ -2363,7 +2365,7 @@ public function getProductOption() } /** - * {@inheritdoc} + * @inheritdoc */ public function setProductOption(\Magento\Catalog\Api\Data\ProductOptionInterface $productOption) { @@ -2371,7 +2373,7 @@ public function setProductOption(\Magento\Catalog\Api\Data\ProductOptionInterfac } /** - * {@inheritdoc} + * @inheritdoc * * @return \Magento\Sales\Api\Data\OrderItemExtensionInterface|null */ @@ -2381,7 +2383,7 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * @inheritdoc * * @param \Magento\Sales\Api\Data\OrderItemExtensionInterface $extensionAttributes * @return $this diff --git a/app/code/Magento/Sales/Model/Order/ItemRepository.php b/app/code/Magento/Sales/Model/Order/ItemRepository.php index 7916eb9db2b8..6e029ac46837 100644 --- a/app/code/Magento/Sales/Model/Order/ItemRepository.php +++ b/app/code/Magento/Sales/Model/Order/ItemRepository.php @@ -6,10 +6,8 @@ namespace Magento\Sales\Model\Order; -use Magento\Catalog\Api\Data\ProductOptionExtensionFactory; -use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; -use Magento\Catalog\Model\ProductOptionFactory; use Magento\Catalog\Model\ProductOptionProcessorInterface; +use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\DataObject; use Magento\Framework\DataObject\Factory as DataObjectFactory; @@ -18,6 +16,7 @@ use Magento\Sales\Api\Data\OrderItemInterface; use Magento\Sales\Api\Data\OrderItemSearchResultInterfaceFactory; use Magento\Sales\Api\OrderItemRepositoryInterface; +use Magento\Sales\Model\Order\ProductOption; use Magento\Sales\Model\ResourceModel\Metadata; /** @@ -41,16 +40,6 @@ class ItemRepository implements OrderItemRepositoryInterface */ protected $searchResultFactory; - /** - * @var ProductOptionFactory - */ - protected $productOptionFactory; - - /** - * @var ProductOptionExtensionFactory - */ - protected $extensionFactory; - /** * @var ProductOptionProcessorInterface[] */ @@ -62,40 +51,41 @@ class ItemRepository implements OrderItemRepositoryInterface protected $registry = []; /** - * @var \Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface + * @var CollectionProcessorInterface */ private $collectionProcessor; /** - * ItemRepository constructor. + * @var ProductOption + */ + private $productOption; + + /** * @param DataObjectFactory $objectFactory * @param Metadata $metadata * @param OrderItemSearchResultInterfaceFactory $searchResultFactory - * @param ProductOptionFactory $productOptionFactory - * @param ProductOptionExtensionFactory $extensionFactory + * @param CollectionProcessorInterface $collectionProcessor + * @param ProductOption $productOption * @param array $processorPool - * @param CollectionProcessorInterface|null $collectionProcessor */ public function __construct( DataObjectFactory $objectFactory, Metadata $metadata, OrderItemSearchResultInterfaceFactory $searchResultFactory, - ProductOptionFactory $productOptionFactory, - ProductOptionExtensionFactory $extensionFactory, - array $processorPool = [], - CollectionProcessorInterface $collectionProcessor = null + CollectionProcessorInterface $collectionProcessor, + ProductOption $productOption, + array $processorPool = [] ) { $this->objectFactory = $objectFactory; $this->metadata = $metadata; $this->searchResultFactory = $searchResultFactory; - $this->productOptionFactory = $productOptionFactory; - $this->extensionFactory = $extensionFactory; + $this->collectionProcessor = $collectionProcessor; + $this->productOption = $productOption; $this->processorPool = $processorPool; - $this->collectionProcessor = $collectionProcessor ?: $this->getCollectionProcessor(); } /** - * load entity + * Loads entity. * * @param int $id * @return OrderItemInterface @@ -116,7 +106,7 @@ public function get($id) ); } - $this->addProductOption($orderItem); + $this->productOption->add($orderItem); $this->addParentItem($orderItem); $this->registry[$id] = $orderItem; } @@ -137,7 +127,7 @@ public function getList(SearchCriteriaInterface $searchCriteria) $this->collectionProcessor->process($searchCriteria, $searchResult); /** @var OrderItemInterface $orderItem */ foreach ($searchResult->getItems() as $orderItem) { - $this->addProductOption($orderItem); + $this->productOption->add($orderItem); } return $searchResult; @@ -178,7 +168,9 @@ public function save(OrderItemInterface $entity) { if ($entity->getProductOption()) { $request = $this->getBuyRequest($entity); - $entity->setProductOptions(['info_buyRequest' => $request->toArray()]); + $productOptions = $entity->getProductOptions(); + $productOptions['info_buyRequest'] = $request->toArray(); + $entity->setProductOptions($productOptions); } $this->metadata->getMapper()->save($entity); @@ -186,37 +178,6 @@ public function save(OrderItemInterface $entity) return $this->registry[$entity->getEntityId()]; } - /** - * Add product option data - * - * @param OrderItemInterface $orderItem - * @return $this - */ - protected function addProductOption(OrderItemInterface $orderItem) - { - /** @var DataObject $request */ - $request = $orderItem->getBuyRequest(); - - $productType = $orderItem->getProductType(); - if (isset($this->processorPool[$productType]) - && !$orderItem->getParentItemId()) { - $data = $this->processorPool[$productType]->convertToProductOption($request); - if ($data) { - $this->setProductOption($orderItem, $data); - } - } - - if (isset($this->processorPool['custom_options']) - && !$orderItem->getParentItemId()) { - $data = $this->processorPool['custom_options']->convertToProductOption($request); - if ($data) { - $this->setProductOption($orderItem, $data); - } - } - - return $this; - } - /** * Set parent item. * @@ -228,33 +189,15 @@ private function addParentItem(OrderItemInterface $orderItem) { if ($parentId = $orderItem->getParentItemId()) { $orderItem->setParentItem($this->get($parentId)); - } - } + } else { + $orderCollection = $orderItem->getOrder()->getItemsCollection()->filterByParent($orderItem->getItemId()); - /** - * Set product options data - * - * @param OrderItemInterface $orderItem - * @param array $data - * @return $this - */ - protected function setProductOption(OrderItemInterface $orderItem, array $data) - { - $productOption = $orderItem->getProductOption(); - if (!$productOption) { - $productOption = $this->productOptionFactory->create(); - $orderItem->setProductOption($productOption); - } - - $extensionAttributes = $productOption->getExtensionAttributes(); - if (!$extensionAttributes) { - $extensionAttributes = $this->extensionFactory->create(); - $productOption->setExtensionAttributes($extensionAttributes); + foreach ($orderCollection->getItems() as $item) { + if ($item->getParentItemId() === $orderItem->getItemId()) { + $item->setParentItem($orderItem); + } + } } - - $extensionAttributes->setData(key($data), current($data)); - - return $this; } /** @@ -288,20 +231,4 @@ protected function getBuyRequest(OrderItemInterface $entity) return $request; } - - /** - * Retrieve collection processor - * - * @deprecated 100.2.0 - * @return CollectionProcessorInterface - */ - private function getCollectionProcessor() - { - if (!$this->collectionProcessor) { - $this->collectionProcessor = \Magento\Framework\App\ObjectManager::getInstance()->get( - \Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface::class - ); - } - return $this->collectionProcessor; - } } diff --git a/app/code/Magento/Sales/Model/Order/Payment.php b/app/code/Magento/Sales/Model/Order/Payment.php index 97040c0a578c..5d1d3f0d040a 100644 --- a/app/code/Magento/Sales/Model/Order/Payment.php +++ b/app/code/Magento/Sales/Model/Order/Payment.php @@ -264,6 +264,7 @@ public function getParentTransactionId() /** * Returns transaction parent * + * @param string $txnId * @return string * @since 100.1.0 */ @@ -299,6 +300,8 @@ public function canCapture() } /** + * Check refund availability. + * * @return bool */ public function canRefund() @@ -307,6 +310,8 @@ public function canRefund() } /** + * Check partial refund availability for invoice. + * * @return bool */ public function canRefundPartialPerInvoice() @@ -315,6 +320,8 @@ public function canRefundPartialPerInvoice() } /** + * Check partial capture availability. + * * @return bool */ public function canCapturePartial() @@ -324,6 +331,7 @@ public function canCapturePartial() /** * Authorize or authorize and capture payment on gateway, if applicable + * * This method is supposed to be called only when order is placed * * @return $this @@ -538,8 +546,7 @@ public function cancelInvoice($invoice) } /** - * Create new invoice with maximum qty for invoice for each item - * register this invoice and capture + * Create new invoice with maximum qty for invoice for each item register this invoice and capture * * @return Invoice */ @@ -677,6 +684,7 @@ public function refund($creditmemo) $gateway->refund($this, $baseAmountToRefund); $creditmemo->setTransactionId($this->getLastTransId()); + // phpcs:ignore Magento2.Exceptions.ThrowCatch } catch (\Magento\Framework\Exception\LocalizedException $e) { if (!$captureTxn) { throw new \Magento\Framework\Exception\LocalizedException( @@ -723,10 +731,14 @@ public function refund($creditmemo) $message = $message = $this->prependMessage($message); $message = $this->_appendTransactionToMessage($transaction, $message); $orderState = $this->getOrderStateResolver()->getStateForOrder($this->getOrder()); + $statuses = $this->getOrder()->getConfig()->getStateStatuses($orderState, false); + $status = in_array($this->getOrder()->getStatus(), $statuses, true) + ? $this->getOrder()->getStatus() + : $this->getOrder()->getConfig()->getStateDefaultStatus($orderState); $this->getOrder() ->addStatusHistoryComment( $message, - $this->getOrder()->getConfig()->getStateDefaultStatus($orderState) + $status )->setIsCustomerNotified($creditmemo->getOrder()->getCustomerNoteNotify()); $this->_eventManager->dispatch( 'sales_order_payment_refund', @@ -849,6 +861,7 @@ public function cancelCreditmemo($creditmemo) /** * Order cancellation hook for payment method instance + * * Adds void transaction if needed * * @return $this @@ -884,6 +897,8 @@ public function canReviewPayment() } /** + * Check fetch transaction info availability + * * @return bool */ public function canFetchTransactionInfo() @@ -1191,6 +1206,11 @@ public function addTransaction($type, $salesDocument = null, $failSafe = false) } /** + * Add transaction comments to order. + * + * @param Transaction|null $transaction + * @param string $message + * @return void */ public function addTransactionCommentsToOrder($transaction, $message) { @@ -1227,6 +1247,7 @@ public function importTransactionInfo(Transaction $transactionTo) /** * Totals updater utility method + * * Updates self totals by keys in data array('key' => $delta) * * @param array $data @@ -1261,6 +1282,7 @@ protected function _appendTransactionToMessage($transaction, $message) /** * Prepend a "prepared_message" that may be set to the payment instance before, to the specified message + * * Prepends value to the specified string or to the comment of specified order status history item instance * * @param string|\Magento\Sales\Model\Order\Status\History $messagePrependTo @@ -1303,6 +1325,7 @@ public function formatAmount($amount, $asFloat = false) /** * Format price with currency sign + * * @param float $amount * @return string */ @@ -1313,6 +1336,7 @@ public function formatPrice($amount) /** * Lookup an authorization transaction using parent transaction id, if set + * * @return Transaction|false */ public function getAuthorizationTransaction() @@ -1384,8 +1408,8 @@ public function resetTransactionAdditionalInfo() /** * Prepare credit memo * - * @param $amount - * @param $baseGrandTotal + * @param float $amount + * @param float $baseGrandTotal * @param false|Invoice $invoice * @return mixed */ @@ -1454,6 +1478,8 @@ protected function _getInvoiceForTransactionId($transactionId) } /** + * Get order state resolver instance. + * * @deprecated 100.2.0 * @return OrderStateResolverInterface */ @@ -1992,7 +2018,7 @@ public function getShippingRefunded() } /** - * {@inheritdoc} + * @inheritdoc */ public function setParentId($id) { @@ -2000,7 +2026,7 @@ public function setParentId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingCaptured($baseShippingCaptured) { @@ -2008,7 +2034,7 @@ public function setBaseShippingCaptured($baseShippingCaptured) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingCaptured($shippingCaptured) { @@ -2016,7 +2042,7 @@ public function setShippingCaptured($shippingCaptured) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAmountRefunded($amountRefunded) { @@ -2024,7 +2050,7 @@ public function setAmountRefunded($amountRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseAmountPaid($baseAmountPaid) { @@ -2032,7 +2058,7 @@ public function setBaseAmountPaid($baseAmountPaid) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAmountCanceled($amountCanceled) { @@ -2040,7 +2066,7 @@ public function setAmountCanceled($amountCanceled) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseAmountAuthorized($baseAmountAuthorized) { @@ -2048,7 +2074,7 @@ public function setBaseAmountAuthorized($baseAmountAuthorized) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseAmountPaidOnline($baseAmountPaidOnline) { @@ -2056,7 +2082,7 @@ public function setBaseAmountPaidOnline($baseAmountPaidOnline) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseAmountRefundedOnline($baseAmountRefundedOnline) { @@ -2064,7 +2090,7 @@ public function setBaseAmountRefundedOnline($baseAmountRefundedOnline) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingAmount($amount) { @@ -2072,7 +2098,7 @@ public function setBaseShippingAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingAmount($amount) { @@ -2080,7 +2106,7 @@ public function setShippingAmount($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAmountPaid($amountPaid) { @@ -2088,7 +2114,7 @@ public function setAmountPaid($amountPaid) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAmountAuthorized($amountAuthorized) { @@ -2096,7 +2122,7 @@ public function setAmountAuthorized($amountAuthorized) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseAmountOrdered($baseAmountOrdered) { @@ -2104,7 +2130,7 @@ public function setBaseAmountOrdered($baseAmountOrdered) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseShippingRefunded($baseShippingRefunded) { @@ -2112,7 +2138,7 @@ public function setBaseShippingRefunded($baseShippingRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setShippingRefunded($shippingRefunded) { @@ -2120,7 +2146,7 @@ public function setShippingRefunded($shippingRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseAmountRefunded($baseAmountRefunded) { @@ -2128,7 +2154,7 @@ public function setBaseAmountRefunded($baseAmountRefunded) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAmountOrdered($amountOrdered) { @@ -2136,7 +2162,7 @@ public function setAmountOrdered($amountOrdered) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBaseAmountCanceled($baseAmountCanceled) { @@ -2144,7 +2170,7 @@ public function setBaseAmountCanceled($baseAmountCanceled) } /** - * {@inheritdoc} + * @inheritdoc */ public function setQuotePaymentId($id) { @@ -2152,7 +2178,7 @@ public function setQuotePaymentId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAdditionalData($additionalData) { @@ -2160,7 +2186,7 @@ public function setAdditionalData($additionalData) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCcExpMonth($ccExpMonth) { @@ -2168,7 +2194,7 @@ public function setCcExpMonth($ccExpMonth) } /** - * {@inheritdoc} + * @inheritdoc * @deprecated 100.1.0 unused */ public function setCcSsStartYear($ccSsStartYear) @@ -2177,7 +2203,7 @@ public function setCcSsStartYear($ccSsStartYear) } /** - * {@inheritdoc} + * @inheritdoc */ public function setEcheckBankName($echeckBankName) { @@ -2185,7 +2211,7 @@ public function setEcheckBankName($echeckBankName) } /** - * {@inheritdoc} + * @inheritdoc */ public function setMethod($method) { @@ -2193,7 +2219,7 @@ public function setMethod($method) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCcDebugRequestBody($ccDebugRequestBody) { @@ -2201,7 +2227,7 @@ public function setCcDebugRequestBody($ccDebugRequestBody) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCcSecureVerify($ccSecureVerify) { @@ -2209,7 +2235,7 @@ public function setCcSecureVerify($ccSecureVerify) } /** - * {@inheritdoc} + * @inheritdoc */ public function setProtectionEligibility($protectionEligibility) { @@ -2217,7 +2243,7 @@ public function setProtectionEligibility($protectionEligibility) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCcApproval($ccApproval) { @@ -2225,7 +2251,7 @@ public function setCcApproval($ccApproval) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCcLast4($ccLast4) { @@ -2233,7 +2259,7 @@ public function setCcLast4($ccLast4) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCcStatusDescription($description) { @@ -2241,7 +2267,7 @@ public function setCcStatusDescription($description) } /** - * {@inheritdoc} + * @inheritdoc */ public function setEcheckType($echeckType) { @@ -2249,7 +2275,7 @@ public function setEcheckType($echeckType) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCcDebugResponseSerialized($ccDebugResponseSerialized) { @@ -2257,7 +2283,7 @@ public function setCcDebugResponseSerialized($ccDebugResponseSerialized) } /** - * {@inheritdoc} + * @inheritdoc * @deprecated 100.1.0 unused */ public function setCcSsStartMonth($ccSsStartMonth) @@ -2266,7 +2292,7 @@ public function setCcSsStartMonth($ccSsStartMonth) } /** - * {@inheritdoc} + * @inheritdoc */ public function setEcheckAccountType($echeckAccountType) { @@ -2274,7 +2300,7 @@ public function setEcheckAccountType($echeckAccountType) } /** - * {@inheritdoc} + * @inheritdoc */ public function setLastTransId($id) { @@ -2282,7 +2308,7 @@ public function setLastTransId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCcCidStatus($ccCidStatus) { @@ -2290,7 +2316,7 @@ public function setCcCidStatus($ccCidStatus) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCcOwner($ccOwner) { @@ -2298,7 +2324,7 @@ public function setCcOwner($ccOwner) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCcType($ccType) { @@ -2306,7 +2332,7 @@ public function setCcType($ccType) } /** - * {@inheritdoc} + * @inheritdoc */ public function setPoNumber($poNumber) { @@ -2314,7 +2340,7 @@ public function setPoNumber($poNumber) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCcExpYear($ccExpYear) { @@ -2322,7 +2348,7 @@ public function setCcExpYear($ccExpYear) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCcStatus($ccStatus) { @@ -2330,7 +2356,7 @@ public function setCcStatus($ccStatus) } /** - * {@inheritdoc} + * @inheritdoc */ public function setEcheckRoutingNumber($echeckRoutingNumber) { @@ -2338,7 +2364,7 @@ public function setEcheckRoutingNumber($echeckRoutingNumber) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAccountStatus($accountStatus) { @@ -2346,7 +2372,7 @@ public function setAccountStatus($accountStatus) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAnetTransMethod($anetTransMethod) { @@ -2354,7 +2380,7 @@ public function setAnetTransMethod($anetTransMethod) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCcDebugResponseBody($ccDebugResponseBody) { @@ -2362,7 +2388,7 @@ public function setCcDebugResponseBody($ccDebugResponseBody) } /** - * {@inheritdoc} + * @inheritdoc * @deprecated 100.1.0 unused */ public function setCcSsIssue($ccSsIssue) @@ -2371,7 +2397,7 @@ public function setCcSsIssue($ccSsIssue) } /** - * {@inheritdoc} + * @inheritdoc */ public function setEcheckAccountName($echeckAccountName) { @@ -2379,7 +2405,7 @@ public function setEcheckAccountName($echeckAccountName) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCcAvsStatus($ccAvsStatus) { @@ -2387,7 +2413,7 @@ public function setCcAvsStatus($ccAvsStatus) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCcNumberEnc($ccNumberEnc) { @@ -2395,7 +2421,7 @@ public function setCcNumberEnc($ccNumberEnc) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCcTransId($id) { @@ -2403,7 +2429,7 @@ public function setCcTransId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAddressStatus($addressStatus) { @@ -2411,7 +2437,7 @@ public function setAddressStatus($addressStatus) } /** - * {@inheritdoc} + * @inheritdoc * * @return \Magento\Sales\Api\Data\OrderPaymentExtensionInterface|null */ @@ -2421,7 +2447,7 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * @inheritdoc * * @param \Magento\Sales\Api\Data\OrderPaymentExtensionInterface $extensionAttributes * @return $this @@ -2505,6 +2531,7 @@ public function getShouldCloseParentTransaction() /** * Set payment parent transaction id and current transaction id if it not set + * * @param Transaction $transaction * @return void */ @@ -2526,6 +2553,7 @@ private function setTransactionIdsForRefund(Transaction $transaction) /** * Collects order invoices totals by provided keys. + * * Returns result as {key: amount}. * * @param Order $order diff --git a/app/code/Magento/Sales/Model/Order/Payment/Info.php b/app/code/Magento/Sales/Model/Order/Payment/Info.php index 063b3eaa71f1..fee846fe6a62 100644 --- a/app/code/Magento/Sales/Model/Order/Payment/Info.php +++ b/app/code/Magento/Sales/Model/Order/Payment/Info.php @@ -11,8 +11,8 @@ use Magento\Payment\Model\InfoInterface; /** - * * Payment information model + * * @api * @since 100.0.2 */ @@ -41,7 +41,7 @@ class Info extends AbstractModel implements InfoInterface * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry * @param \Magento\Framework\Api\ExtensionAttributesFactory $extensionFactory - * @param \Magento\Framework\Api\AttributeValueFactory $customAttributeFactory, + * @param \Magento\Framework\Api\AttributeValueFactory $customAttributeFactory * @param \Magento\Payment\Helper\Data $paymentData * @param \Magento\Framework\Encryption\EncryptorInterface $encryptor * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource @@ -181,7 +181,7 @@ public function getAdditionalInformation($key = null) if (null === $key) { return $this->additionalInformation; } - return isset($this->additionalInformation[$key]) ? $this->additionalInformation[$key] : null; + return $this->additionalInformation[$key] ?? null; } /** @@ -219,8 +219,7 @@ public function hasAdditionalInformation($key = null) } /** - * Initialize additional information container with data from model - * if property empty + * Initialize additional information container with data from model if property empty * * @return void */ diff --git a/app/code/Magento/Sales/Model/Order/Payment/Transaction.php b/app/code/Magento/Sales/Model/Order/Payment/Transaction.php index 8d8de47ba99c..57d6c204dcaf 100644 --- a/app/code/Magento/Sales/Model/Order/Payment/Transaction.php +++ b/app/code/Magento/Sales/Model/Order/Payment/Transaction.php @@ -132,10 +132,12 @@ class Transaction extends AbstractModel implements TransactionInterface * @param \Magento\Framework\Api\ExtensionAttributesFactory $extensionFactory * @param AttributeValueFactory $customAttributeFactory * @param \Magento\Sales\Model\OrderFactory $orderFactory + * @param \Magento\Sales\Api\OrderPaymentRepositoryInterface $orderPaymentRepository + * @param \Magento\Sales\Api\OrderRepositoryInterface $orderRepository * @param \Magento\Framework\Stdlib\DateTime\DateTimeFactory $dateFactory * @param TransactionFactory $transactionFactory - * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource - * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection + * @param \Magento\Framework\Model\ResourceModel\AbstractResource|null $resource + * @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection * @param array $data * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -193,8 +195,7 @@ public function setTxnId($txnId) } /** - * Parent transaction ID setter - * Can set the transaction id as well + * Parent transaction ID setter Can set the transaction id as well * * @param string $parentTxnId * @param string $txnId @@ -229,8 +230,7 @@ public function setTxnType($txnType) } /** - * Parent transaction getter - * May attempt to load it. + * Parent transaction getter. May attempt to load it. * * @param bool $shouldLoad * @return bool|\Magento\Sales\Model\Order\Payment\Transaction @@ -366,8 +366,7 @@ public function closeAuthorization($shouldSave = true, $dryRun = false) } /** - * Close a capture transaction - * Logic is similar to closeAuthorization(), but for a capture transaction + * Close a capture transaction. Logic is similar to closeAuthorization(), but for a capture transaction * * @param bool $shouldSave * @return bool|\Magento\Sales\Model\Order\Payment\Transaction @@ -395,6 +394,7 @@ public function closeCapture($shouldSave = true) /** * Check whether authorization in current hierarchy can be voided completely + * * Basically checks whether the authorization exists and it is not affected by a capture or void * * @return bool @@ -472,7 +472,7 @@ public function getAdditionalInformation($key = null) $info = []; } if ($key) { - return isset($info[$key]) ? $info[$key] : null; + return $info[$key] ?? null; } return $info; } @@ -536,6 +536,7 @@ public function close($shouldSave = true) /** * Order ID getter + * * Attempts to get ID from set order payment object, if any, or from data by key 'order_id' * * @return int|null @@ -559,7 +560,7 @@ public function getOrderId() /** * Retrieve order instance * - * @return \Magento\Sales\Model\Order + * @return \Magento\Sales\Model\Order\Payment */ public function getOrder() { @@ -572,6 +573,7 @@ public function getOrder() /** * Set order instance for transaction depends on transaction behavior + * * If $order equals to true, method isn't loading new order instance. * * @param \Magento\Sales\Model\Order|null|boolean $order @@ -695,7 +697,6 @@ protected function _loadChildren() /** * Check whether this transaction is voided * - * TODO: implement that there should be only one void per authorization * @return bool */ protected function _isVoided() @@ -805,7 +806,6 @@ protected function _verifyTxnId($txnId) /** * Make sure this object is a valid transaction - * TODO for more restriction we can check for data consistency * * @return void * @throws \Magento\Framework\Exception\LocalizedException @@ -833,7 +833,7 @@ public function getTransactionId() } /** - * {@inheritdoc} + * @inheritdoc */ public function setTransactionId($id) { @@ -898,7 +898,7 @@ public function getTxnId() public function getHtmlTxnId() { $this->_eventManager->dispatch($this->_eventPrefix . '_html_txn_id', $this->_getEventData()); - return isset($this->_data['html_txn_id']) ? $this->_data['html_txn_id'] : $this->getTxnId(); + return $this->_data['html_txn_id'] ?? $this->getTxnId(); } /** @@ -942,7 +942,7 @@ public function getCreatedAt() } /** - * {@inheritdoc} + * @inheritdoc */ public function setCreatedAt($createdAt) { @@ -950,7 +950,7 @@ public function setCreatedAt($createdAt) } /** - * {@inheritdoc} + * @inheritdoc */ public function setParentId($id) { @@ -958,7 +958,7 @@ public function setParentId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setOrderId($id) { @@ -966,7 +966,7 @@ public function setOrderId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setPaymentId($id) { @@ -974,7 +974,7 @@ public function setPaymentId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setIsClosed($isClosed) { @@ -982,7 +982,7 @@ public function setIsClosed($isClosed) } /** - * {@inheritdoc} + * @inheritdoc * * @return \Magento\Sales\Api\Data\TransactionExtensionInterface|null */ @@ -992,10 +992,7 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} - * - * @param \Magento\Sales\Api\Data\TransactionExtensionInterface $extensionAttributes - * @return $this + * @inheritdoc */ public function setExtensionAttributes(\Magento\Sales\Api\Data\TransactionExtensionInterface $extensionAttributes) { diff --git a/app/code/Magento/Sales/Model/Order/Pdf/AbstractPdf.php b/app/code/Magento/Sales/Model/Order/Pdf/AbstractPdf.php index 401fdcd2b04a..85e34f560bb7 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/AbstractPdf.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/AbstractPdf.php @@ -158,8 +158,7 @@ public function __construct( } /** - * Returns the total width in points of the string using the specified font and - * size. + * Returns the total width in points of the string using the specified font and size. * * This is not the most efficient way to perform this calculation. I'm * concentrating optimization efforts on the upcoming layout manager class. @@ -230,7 +229,7 @@ public function getAlignCenter($string, $x, $columnWidth, \Zend_Pdf_Resource_Fon * Insert logo to pdf page * * @param \Zend_Pdf_Page &$page - * @param null $store + * @param string|null $store * @return void * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ @@ -285,7 +284,7 @@ protected function insertLogo(&$page, $store = null) * Insert address to pdf page * * @param \Zend_Pdf_Page &$page - * @param null $store + * @param string|null $store * @return void */ protected function insertAddress(&$page, $store = null) @@ -641,11 +640,7 @@ protected function _sortTotalsList($a, $b) return 0; } - if ($a['sort_order'] == $b['sort_order']) { - return 0; - } - - return $a['sort_order'] > $b['sort_order'] ? 1 : -1; + return $a['sort_order'] <=> $b['sort_order']; } /** diff --git a/app/code/Magento/Sales/Model/Order/Pdf/Items/Creditmemo/DefaultCreditmemo.php b/app/code/Magento/Sales/Model/Order/Pdf/Items/Creditmemo/DefaultCreditmemo.php index 11908864236f..48934e24a379 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/Items/Creditmemo/DefaultCreditmemo.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/Items/Creditmemo/DefaultCreditmemo.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Model\Order\Pdf\Items\Creditmemo; /** @@ -66,11 +68,18 @@ public function draw() $lines = []; // draw Product name - $lines[0] = [['text' => $this->string->split($item->getName(), 35, true, true), 'feed' => 35]]; + $lines[0] = [ + [ + // phpcs:ignore Magento2.Functions.DiscouragedFunction + 'text' => $this->string->split(html_entity_decode($item->getName()), 35, true, true), + 'feed' => 35 + ] + ]; // draw SKU $lines[0][] = [ - 'text' => $this->string->split($this->getSku($item), 17), + // phpcs:ignore Magento2.Functions.DiscouragedFunction + 'text' => $this->string->split(html_entity_decode($this->getSku($item)), 17), 'feed' => 255, 'align' => 'right', ]; diff --git a/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php b/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php index 856232802554..23c2c00daadc 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Model\Order\Pdf\Items\Invoice; /** @@ -66,11 +68,18 @@ public function draw() $lines = []; // draw Product name - $lines[0] = [['text' => $this->string->split($item->getName(), 35, true, true), 'feed' => 35]]; + $lines[0] = [ + [ + // phpcs:ignore Magento2.Functions.DiscouragedFunction + 'text' => $this->string->split(html_entity_decode($item->getName()), 35, true, true), + 'feed' => 35 + ] + ]; // draw SKU $lines[0][] = [ - 'text' => $this->string->split($this->getSku($item), 17), + // phpcs:ignore Magento2.Functions.DiscouragedFunction + 'text' => $this->string->split(html_entity_decode($this->getSku($item)), 17), 'feed' => 290, 'align' => 'right', ]; diff --git a/app/code/Magento/Sales/Model/Order/Pdf/Items/Shipment/DefaultShipment.php b/app/code/Magento/Sales/Model/Order/Pdf/Items/Shipment/DefaultShipment.php index 6007e1dcf2b4..a88b508ba078 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/Items/Shipment/DefaultShipment.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/Items/Shipment/DefaultShipment.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Model\Order\Pdf\Items\Shipment; /** @@ -65,14 +67,21 @@ public function draw() $lines = []; // draw Product name - $lines[0] = [['text' => $this->string->split($item->getName(), 60, true, true), 'feed' => 100]]; + $lines[0] = [ + [ + // phpcs:ignore Magento2.Functions.DiscouragedFunction + 'text' => $this->string->split(html_entity_decode($item->getName()), 60, true, true), + 'feed' => 100 + ] + ]; // draw QTY $lines[0][] = ['text' => $item->getQty() * 1, 'feed' => 35]; // draw SKU $lines[0][] = [ - 'text' => $this->string->split($this->getSku($item), 25), + // phpcs:ignore Magento2.Functions.DiscouragedFunction + 'text' => $this->string->split(html_entity_decode($this->getSku($item)), 25), 'feed' => 565, 'align' => 'right', ]; diff --git a/app/code/Magento/Sales/Model/Order/Pdf/Shipment.php b/app/code/Magento/Sales/Model/Order/Pdf/Shipment.php index b171fccdeb05..32a289c0f5fa 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/Shipment.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/Shipment.php @@ -150,11 +150,11 @@ public function getPdf($shipments = []) $this->_drawItem($item, $page, $order); $page = end($pdf->pages); } + if ($shipment->getStoreId()) { + $this->_localeResolver->revert(); + } } $this->_afterGetPdf(); - if ($shipment->getStoreId()) { - $this->_localeResolver->revert(); - } return $pdf; } diff --git a/app/code/Magento/Sales/Model/Order/ProductOption.php b/app/code/Magento/Sales/Model/Order/ProductOption.php new file mode 100644 index 000000000000..dc9ec42e27e6 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/ProductOption.php @@ -0,0 +1,103 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Model\Order; + +use Magento\Sales\Api\Data\OrderItemInterface; +use Magento\Framework\DataObject; +use Magento\Catalog\Model\ProductOptionFactory; +use Magento\Catalog\Model\ProductOptionProcessorInterface; +use Magento\Catalog\Api\Data\ProductOptionExtensionFactory; + +/** + * Adds product option to the order item according to product options processors pool. + * + * @api + */ +class ProductOption +{ + /** + * @var ProductOptionFactory + */ + private $productOptionFactory; + + /** + * @var ProductOptionExtensionFactory + */ + private $extensionFactory; + + /** + * @var ProductOptionProcessorInterface[] + */ + private $processorPool; + + /** + * @param ProductOptionFactory $productOptionFactory + * @param ProductOptionExtensionFactory $extensionFactory + * @param array $processorPool + */ + public function __construct( + ProductOptionFactory $productOptionFactory, + ProductOptionExtensionFactory $extensionFactory, + array $processorPool = [] + ) { + $this->productOptionFactory = $productOptionFactory; + $this->extensionFactory = $extensionFactory; + $this->processorPool = $processorPool; + } + + /** + * Adds product option to the order item. + * + * @param OrderItemInterface $orderItem + */ + public function add(OrderItemInterface $orderItem): void + { + /** @var DataObject $request */ + $request = $orderItem->getBuyRequest(); + + $productType = $orderItem->getProductType(); + if (isset($this->processorPool[$productType]) + && !$orderItem->getParentItemId()) { + $data = $this->processorPool[$productType]->convertToProductOption($request); + if ($data) { + $this->setProductOption($orderItem, $data); + } + } + + if (isset($this->processorPool['custom_options']) + && !$orderItem->getParentItemId()) { + $data = $this->processorPool['custom_options']->convertToProductOption($request); + if ($data) { + $this->setProductOption($orderItem, $data); + } + } + } + + /** + * Sets product options data. + * + * @param OrderItemInterface $orderItem + * @param array $data + */ + private function setProductOption(OrderItemInterface $orderItem, array $data): void + { + $productOption = $orderItem->getProductOption(); + if (!$productOption) { + $productOption = $this->productOptionFactory->create(); + $orderItem->setProductOption($productOption); + } + + $extensionAttributes = $productOption->getExtensionAttributes(); + if (!$extensionAttributes) { + $extensionAttributes = $this->extensionFactory->create(); + $productOption->setExtensionAttributes($extensionAttributes); + } + + $extensionAttributes->setData(key($data), current($data)); + } +} diff --git a/app/code/Magento/Sales/Model/Order/Shipment.php b/app/code/Magento/Sales/Model/Order/Shipment.php index 8be12ec6ad57..ef9c6fc628dd 100644 --- a/app/code/Magento/Sales/Model/Order/Shipment.php +++ b/app/code/Magento/Sales/Model/Order/Shipment.php @@ -277,6 +277,8 @@ public function register() } /** + * Retrieves the collection used to track the shipment's items + * * @return mixed */ public function getItemsCollection() @@ -295,6 +297,8 @@ public function getItemsCollection() } /** + * Retrieves all non-deleted items from the shipment + * * @return array */ public function getAllItems() @@ -309,6 +313,8 @@ public function getAllItems() } /** + * Retrieves an item from the shipment using its ID + * * @param string|int $itemId * @return bool|\Magento\Sales\Model\Order\Shipment\Item */ @@ -323,6 +329,8 @@ public function getItemById($itemId) } /** + * Adds an item to the shipment + * * @param \Magento\Sales\Model\Order\Shipment\Item $item * @return $this */ @@ -346,13 +354,22 @@ public function addItem(\Magento\Sales\Model\Order\Shipment\Item $item) public function getTracksCollection() { if ($this->tracksCollection === null) { - $this->tracksCollection = $this->_trackCollectionFactory->create()->setShipmentFilter($this->getId()); + $this->tracksCollection = $this->_trackCollectionFactory->create(); + + $id = $this->getId() ?: 0; + $this->tracksCollection->setShipmentFilter($id); + + foreach ($this->tracksCollection as $item) { + $item->setShipment($this); + } } return $this->tracksCollection; } /** + * Retrieves all available tracks in the collection that aren't deleted + * * @return array */ public function getAllTracks() @@ -367,6 +384,8 @@ public function getAllTracks() } /** + * Retrieves a track using its ID + * * @param string|int $trackId * @return bool|\Magento\Sales\Model\Order\Shipment\Track */ @@ -381,24 +400,27 @@ public function getTrackById($trackId) } /** + * Addes a track to the collection and associates the shipment to the track + * * @param \Magento\Sales\Model\Order\Shipment\Track $track * @return $this */ public function addTrack(\Magento\Sales\Model\Order\Shipment\Track $track) { - $track->setShipment( - $this - )->setParentId( - $this->getId() - )->setOrderId( - $this->getOrderId() - )->setStoreId( - $this->getStoreId() - ); + $track->setShipment($this) + ->setParentId($this->getId()) + ->setOrderId($this->getOrderId()) + ->setStoreId($this->getStoreId()); + if (!$track->getId()) { $this->getTracksCollection()->addItem($track); } + $tracks = $this->getTracks(); + // as it's a new track entity, the collection doesn't contain it + $tracks[] = $track; + $this->setTracks($tracks); + /** * Track saving is implemented in _afterSave() * This enforces \Magento\Framework\Model\AbstractModel::save() not to skip _afterSave() @@ -409,8 +431,7 @@ public function addTrack(\Magento\Sales\Model\Order\Shipment\Track $track) } /** - * Adds comment to shipment with additional possibility to send it to customer via email - * and show it in customer account + * Adds comment to shipment with option to send it to customer via email and show it in customer account * * @param \Magento\Sales\Model\Order\Shipment\Comment|string $comment * @param bool $notify @@ -569,18 +590,16 @@ public function setItems($items) /** * Returns tracks * - * @return \Magento\Sales\Api\Data\ShipmentTrackInterface[] + * @return \Magento\Sales\Api\Data\ShipmentTrackInterface[]|null */ public function getTracks() { + if (!$this->getId()) { + return $this->getData(ShipmentInterface::TRACKS); + } + if ($this->getData(ShipmentInterface::TRACKS) === null) { - $collection = $this->_trackCollectionFactory->create()->setShipmentFilter($this->getId()); - if ($this->getId()) { - foreach ($collection as $item) { - $item->setShipment($this); - } - $this->setData(ShipmentInterface::TRACKS, $collection->getItems()); - } + $this->setData(ShipmentInterface::TRACKS, $this->getTracksCollection()->getItems()); } return $this->getData(ShipmentInterface::TRACKS); } diff --git a/app/code/Magento/Sales/Model/Order/Shipment/Sender/EmailSender.php b/app/code/Magento/Sales/Model/Order/Shipment/Sender/EmailSender.php index 0a393548069f..3657f84d4445 100644 --- a/app/code/Magento/Sales/Model/Order/Shipment/Sender/EmailSender.php +++ b/app/code/Magento/Sales/Model/Order/Shipment/Sender/EmailSender.php @@ -89,6 +89,7 @@ public function __construct( * @param bool $forceSyncMode * * @return bool + * @throws \Exception */ public function send( \Magento\Sales\Api\Data\OrderInterface $order, @@ -96,7 +97,7 @@ public function send( \Magento\Sales\Api\Data\ShipmentCommentCreationInterface $comment = null, $forceSyncMode = false ) { - $shipment->setSendEmail(true); + $shipment->setSendEmail($this->identityContainer->isEnabled()); if (!$this->globalConfig->getValue('sales_email/general/async_sending') || $forceSyncMode) { $transport = [ @@ -145,6 +146,7 @@ public function send( * @param \Magento\Sales\Api\Data\OrderInterface $order * * @return string + * @throws \Exception */ private function getPaymentHtml(\Magento\Sales\Api\Data\OrderInterface $order) { diff --git a/app/code/Magento/Sales/Model/Order/StateResolver.php b/app/code/Magento/Sales/Model/Order/StateResolver.php index 6f84c9b48b6d..f5575e0388af 100644 --- a/app/code/Magento/Sales/Model/Order/StateResolver.php +++ b/app/code/Magento/Sales/Model/Order/StateResolver.php @@ -39,7 +39,7 @@ private function isOrderClosed(OrderInterface $order, $arguments) { /** @var $order Order|OrderInterface */ $forceCreditmemo = in_array(self::FORCED_CREDITMEMO, $arguments); - if (floatval($order->getTotalRefunded()) || !$order->getTotalRefunded() && $forceCreditmemo) { + if ((float)$order->getTotalRefunded() || !$order->getTotalRefunded() && $forceCreditmemo) { return true; } return false; diff --git a/app/code/Magento/Sales/Model/Order/Status.php b/app/code/Magento/Sales/Model/Order/Status.php index 288955705425..4b33f4b4d925 100644 --- a/app/code/Magento/Sales/Model/Order/Status.php +++ b/app/code/Magento/Sales/Model/Order/Status.php @@ -12,6 +12,7 @@ * Class Status * * @method string getStatus() + * @method $this setStatus(string $status) * @method string getLabel() */ class Status extends \Magento\Sales\Model\AbstractModel diff --git a/app/code/Magento/Sales/Model/Order/Validation/CanInvoice.php b/app/code/Magento/Sales/Model/Order/Validation/CanInvoice.php index b109b87e61bb..7b346a232ab9 100644 --- a/app/code/Magento/Sales/Model/Order/Validation/CanInvoice.php +++ b/app/code/Magento/Sales/Model/Order/Validation/CanInvoice.php @@ -15,6 +15,8 @@ class CanInvoice implements ValidatorInterface { /** + * Validate + * * @param OrderInterface $entity * @return array */ @@ -32,6 +34,8 @@ public function validate($entity) } /** + * Is state ready for invoice + * * @param OrderInterface $order * @return bool */ @@ -44,12 +48,14 @@ private function isStateReadyForInvoice(OrderInterface $order) $order->getState() === Order::STATE_CLOSED ) { return false; - }; + } return true; } /** + * Can invoice + * * @param OrderInterface $order * @return bool */ diff --git a/app/code/Magento/Sales/Model/Order/Webapi/ChangeOutputArray.php b/app/code/Magento/Sales/Model/Order/Webapi/ChangeOutputArray.php new file mode 100644 index 000000000000..38728d88ff4f --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/Webapi/ChangeOutputArray.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Model\Order\Webapi; + +use Magento\Sales\Api\Data\OrderItemInterface; +use Magento\Sales\Block\Adminhtml\Items\Column\DefaultColumn; +use Magento\Sales\Block\Order\Item\Renderer\DefaultRenderer; + +/** + * Class for changing row total in response. + */ +class ChangeOutputArray +{ + /** + * @var DefaultColumn + */ + private $priceRenderer; + + /** + * @var DefaultRenderer + */ + private $defaultRenderer; + + /** + * @param DefaultColumn $priceRenderer + * @param DefaultRenderer $defaultRenderer + */ + public function __construct( + DefaultColumn $priceRenderer, + DefaultRenderer $defaultRenderer + ) { + $this->priceRenderer = $priceRenderer; + $this->defaultRenderer = $defaultRenderer; + } + + /** + * Changing row total for webapi order item response. + * + * @param OrderItemInterface $dataObject + * @param array $result + * @return array + */ + public function execute( + OrderItemInterface $dataObject, + array $result + ): array { + $result[OrderItemInterface::ROW_TOTAL] = $this->priceRenderer->getTotalAmount($dataObject); + $result[OrderItemInterface::BASE_ROW_TOTAL] = $this->priceRenderer->getBaseTotalAmount($dataObject); + $result[OrderItemInterface::ROW_TOTAL_INCL_TAX] = $this->defaultRenderer->getTotalAmount($dataObject); + $result[OrderItemInterface::BASE_ROW_TOTAL_INCL_TAX] = $dataObject->getBaseRowTotal() + + $dataObject->getBaseTaxAmount() + + $dataObject->getBaseDiscountTaxCompensationAmount() + + $dataObject->getBaseWeeeTaxAppliedAmount() + - $dataObject->getBaseDiscountAmount(); + + return $result; + } +} diff --git a/app/code/Magento/Sales/Model/OrderRepository.php b/app/code/Magento/Sales/Model/OrderRepository.php index f5ab45c5eb1b..79548cb19075 100644 --- a/app/code/Magento/Sales/Model/OrderRepository.php +++ b/app/code/Magento/Sales/Model/OrderRepository.php @@ -6,7 +6,9 @@ namespace Magento\Sales\Model; +use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Sales\Api\Data\OrderExtensionFactory; @@ -16,7 +18,10 @@ use Magento\Sales\Api\Data\ShippingAssignmentInterface; use Magento\Sales\Model\Order\ShippingAssignmentBuilder; use Magento\Sales\Model\ResourceModel\Metadata; -use Magento\Framework\App\ObjectManager; +use Magento\Tax\Api\OrderTaxManagementInterface; +use Magento\Payment\Api\Data\PaymentAdditionalInfoInterface; +use Magento\Payment\Api\Data\PaymentAdditionalInfoInterfaceFactory; +use Magento\Framework\Serialize\Serializer\Json as JsonSerializer; /** * Repository class @@ -55,6 +60,26 @@ class OrderRepository implements \Magento\Sales\Api\OrderRepositoryInterface */ protected $registry = []; + /** + * @var OrderTaxManagementInterface + */ + private $orderTaxManagement; + + /** + * @var PaymentAdditionalInfoFactory + */ + private $paymentAdditionalInfoFactory; + + /** + * @var JsonSerializer + */ + private $serializer; + + /** + * @var JoinProcessorInterface + */ + private $extensionAttributesJoinProcessor; + /** * Constructor * @@ -62,12 +87,20 @@ class OrderRepository implements \Magento\Sales\Api\OrderRepositoryInterface * @param SearchResultFactory $searchResultFactory * @param CollectionProcessorInterface|null $collectionProcessor * @param \Magento\Sales\Api\Data\OrderExtensionFactory|null $orderExtensionFactory + * @param OrderTaxManagementInterface|null $orderTaxManagement + * @param PaymentAdditionalInfoInterfaceFactory|null $paymentAdditionalInfoFactory + * @param JsonSerializer|null $serializer + * @param JoinProcessorInterface $extensionAttributesJoinProcessor */ public function __construct( Metadata $metadata, SearchResultFactory $searchResultFactory, CollectionProcessorInterface $collectionProcessor = null, - \Magento\Sales\Api\Data\OrderExtensionFactory $orderExtensionFactory = null + \Magento\Sales\Api\Data\OrderExtensionFactory $orderExtensionFactory = null, + OrderTaxManagementInterface $orderTaxManagement = null, + PaymentAdditionalInfoInterfaceFactory $paymentAdditionalInfoFactory = null, + JsonSerializer $serializer = null, + JoinProcessorInterface $extensionAttributesJoinProcessor = null ) { $this->metadata = $metadata; $this->searchResultFactory = $searchResultFactory; @@ -75,10 +108,18 @@ public function __construct( ->get(\Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface::class); $this->orderExtensionFactory = $orderExtensionFactory ?: ObjectManager::getInstance() ->get(\Magento\Sales\Api\Data\OrderExtensionFactory::class); + $this->orderTaxManagement = $orderTaxManagement ?: ObjectManager::getInstance() + ->get(OrderTaxManagementInterface::class); + $this->paymentAdditionalInfoFactory = $paymentAdditionalInfoFactory ?: ObjectManager::getInstance() + ->get(PaymentAdditionalInfoInterfaceFactory::class); + $this->serializer = $serializer ?: ObjectManager::getInstance() + ->get(JsonSerializer::class); + $this->extensionAttributesJoinProcessor = $extensionAttributesJoinProcessor + ?: ObjectManager::getInstance()->get(JoinProcessorInterface::class); } /** - * load entity + * Load entity * * @param int $id * @return \Magento\Sales\Api\Data\OrderInterface @@ -98,12 +139,65 @@ public function get($id) __("The entity that was requested doesn't exist. Verify the entity and try again.") ); } + $this->setOrderTaxDetails($entity); $this->setShippingAssignments($entity); + $this->setPaymentAdditionalInfo($entity); $this->registry[$id] = $entity; } return $this->registry[$id]; } + /** + * Set order tax details to extension attributes. + * + * @param OrderInterface $order + * @return void + */ + private function setOrderTaxDetails(OrderInterface $order) + { + $extensionAttributes = $order->getExtensionAttributes(); + $orderTaxDetails = $this->orderTaxManagement->getOrderTaxDetails($order->getEntityId()); + $appliedTaxes = $orderTaxDetails->getAppliedTaxes(); + + $extensionAttributes->setAppliedTaxes($appliedTaxes); + if (!empty($appliedTaxes)) { + $extensionAttributes->setConvertingFromQuote(true); + } + + $items = $orderTaxDetails->getItems(); + $extensionAttributes->setItemAppliedTaxes($items); + + $order->setExtensionAttributes($extensionAttributes); + } + + /** + * Set additional info to the order. + * + * @param OrderInterface $order + * @return void + */ + private function setPaymentAdditionalInfo(OrderInterface $order): void + { + $extensionAttributes = $order->getExtensionAttributes(); + $paymentAdditionalInformation = $order->getPayment()->getAdditionalInformation(); + + $objects = []; + foreach ($paymentAdditionalInformation as $key => $value) { + /** @var PaymentAdditionalInfoInterface $additionalInformationObject */ + $additionalInformationObject = $this->paymentAdditionalInfoFactory->create(); + $additionalInformationObject->setKey($key); + + if (!is_string($value)) { + $value = $this->serializer->serialize($value); + } + $additionalInformationObject->setValue($value); + + $objects[] = $additionalInformationObject; + } + $extensionAttributes->setPaymentAdditionalInfo($objects); + $order->setExtensionAttributes($extensionAttributes); + } + /** * Find entities by criteria * @@ -114,10 +208,13 @@ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCr { /** @var \Magento\Sales\Api\Data\OrderSearchResultInterface $searchResult */ $searchResult = $this->searchResultFactory->create(); + $this->extensionAttributesJoinProcessor->process($searchResult); $this->collectionProcessor->process($searchCriteria, $searchResult); $searchResult->setSearchCriteria($searchCriteria); foreach ($searchResult->getItems() as $order) { $this->setShippingAssignments($order); + $this->setOrderTaxDetails($order); + $this->setPaymentAdditionalInfo($order); } return $searchResult; } @@ -171,6 +268,8 @@ public function save(\Magento\Sales\Api\Data\OrderInterface $entity) } /** + * Set shipping assignments to extension attributes. + * * @param OrderInterface $order * @return void */ diff --git a/app/code/Magento/Sales/Model/RefundOrder.php b/app/code/Magento/Sales/Model/RefundOrder.php index d79f5ecf857c..07555cba1b7f 100644 --- a/app/code/Magento/Sales/Model/RefundOrder.php +++ b/app/code/Magento/Sales/Model/RefundOrder.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Sales\Model; use Magento\Framework\App\ResourceConnection; @@ -151,10 +152,13 @@ public function execute( $creditmemo->setState(\Magento\Sales\Model\Order\Creditmemo::STATE_REFUNDED); $order->setCustomerNoteNotify($notify); $order = $this->refundAdapter->refund($creditmemo, $order); - $order->setState( - $this->orderStateResolver->getStateForOrder($order, []) - ); - $order->setStatus($this->config->getStateDefaultStatus($order->getState())); + $orderState = $this->orderStateResolver->getStateForOrder($order, []); + $order->setState($orderState); + $statuses = $this->config->getStateStatuses($orderState, false); + $status = in_array($order->getStatus(), $statuses, true) + ? $order->getStatus() + : $this->config->getStateDefaultStatus($orderState); + $order->setStatus($status); $order = $this->orderRepository->save($order); $creditmemo = $this->creditmemoRepository->save($creditmemo); diff --git a/app/code/Magento/Sales/Model/ResourceModel/AbstractGrid.php b/app/code/Magento/Sales/Model/ResourceModel/AbstractGrid.php index 87c5b917f696..25c15449a9fb 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/AbstractGrid.php +++ b/app/code/Magento/Sales/Model/ResourceModel/AbstractGrid.php @@ -103,6 +103,6 @@ protected function getLastUpdatedAtValue($default = '0000-00-00 00:00:00') $row = $this->getConnection()->fetchRow($select); - return isset($row['updated_at']) ? $row['updated_at'] : $default; + return $row['updated_at'] ?? $default; } } diff --git a/app/code/Magento/Sales/Model/ResourceModel/Grid.php b/app/code/Magento/Sales/Model/ResourceModel/Grid.php index 42e5d92fb00c..432918450a69 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Grid.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Grid.php @@ -93,15 +93,20 @@ public function refresh($value, $field = null) { $select = $this->getGridOriginSelect() ->where(($field ?: $this->mainTableName . '.entity_id') . ' = ?', $value); - return $this->getConnection()->query( - $this->getConnection() - ->insertFromSelect( - $select, - $this->getTable($this->gridTableName), - array_keys($this->columns), - AdapterInterface::INSERT_ON_DUPLICATE - ) - ); + $sql = $this->getConnection() + ->insertFromSelect( + $select, + $this->getTable($this->gridTableName), + array_keys($this->columns), + AdapterInterface::INSERT_ON_DUPLICATE + ); + + $this->addCommitCallback(function () use ($sql) { + $this->getConnection()->query($sql); + }); + + // need for backward compatibility + return $this->getConnection()->query($sql); } /** @@ -126,6 +131,8 @@ public function refreshBySchedule() } /** + * Get order id field. + * * @return string */ public function getOrderIdField() diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Collection.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Collection.php index 5dca23836427..6ad8ebc3bb89 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Collection.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Collection.php @@ -48,7 +48,7 @@ class Collection extends AbstractCollection implements OrderSearchResultInterfac * @param \Magento\Framework\Event\ManagerInterface $eventManager * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot $entitySnapshot * @param \Magento\Framework\DB\Helper $coreResourceHelper - * @param string|null $connection + * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection * @param \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource */ public function __construct( @@ -138,6 +138,7 @@ protected function _getAllIdsSelect($limit = null, $offset = null) /** * Join table sales_order_address to select for billing and shipping order addresses. + * * Create correlation map * * @return $this diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Grid/Collection.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Grid/Collection.php index f6dd8f8527a5..82c612c1a781 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Grid/Collection.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Grid/Collection.php @@ -35,4 +35,19 @@ public function __construct( ) { parent::__construct($entityFactory, $logger, $fetchStrategy, $eventManager, $mainTable, $resourceModel); } + + /** + * @inheritdoc + */ + protected function _initSelect() + { + parent::_initSelect(); + + $tableDescription = $this->getConnection()->describeTable($this->getMainTable()); + foreach ($tableDescription as $columnInfo) { + $this->addFilterToMap($columnInfo['COLUMN_NAME'], 'main_table.' . $columnInfo['COLUMN_NAME']); + } + + return $this; + } } diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/State.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/State.php index c7bac874fa33..de15a627583f 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/State.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/State.php @@ -14,7 +14,7 @@ class State { /** - * Check order status before save + * Check order status and adjust the status before save * * @param Order $order * @return $this @@ -23,24 +23,24 @@ class State */ public function check(Order $order) { - if (!$order->isCanceled() && !$order->canUnhold() && !$order->canInvoice() && !$order->canShip()) { - if (0 == $order->getBaseGrandTotal() || $order->canCreditmemo()) { - if ($order->getState() !== Order::STATE_COMPLETE) { - $order->setState(Order::STATE_COMPLETE) - ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_COMPLETE)); - } - } elseif (floatval($order->getTotalRefunded()) - || !$order->getTotalRefunded() && $order->hasForcedCanCreditmemo() - ) { - if ($order->getState() !== Order::STATE_CLOSED) { - $order->setState(Order::STATE_CLOSED) - ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_CLOSED)); - } - } - } - if ($order->getState() == Order::STATE_NEW && $order->getIsInProcess()) { + $currentState = $order->getState(); + if ($currentState == Order::STATE_NEW && $order->getIsInProcess()) { $order->setState(Order::STATE_PROCESSING) ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_PROCESSING)); + $currentState = Order::STATE_PROCESSING; + } + + if (!$order->isCanceled() && !$order->canUnhold() && !$order->canInvoice()) { + if (in_array($currentState, [Order::STATE_PROCESSING, Order::STATE_COMPLETE]) + && !$order->canCreditmemo() + && !$order->canShip() + ) { + $order->setState(Order::STATE_CLOSED) + ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_CLOSED)); + } elseif ($currentState === Order::STATE_PROCESSING && !$order->canShip()) { + $order->setState(Order::STATE_COMPLETE) + ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_COMPLETE)); + } } return $this; } diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Payment/Collection.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Payment/Collection.php index 521db7f1f3a4..fead4f39f4c2 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Payment/Collection.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Payment/Collection.php @@ -33,7 +33,7 @@ class Collection extends AbstractCollection implements OrderPaymentSearchResultI * @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy * @param \Magento\Framework\Event\ManagerInterface $eventManager * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot $entitySnapshot - * @param null $connection + * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection * @param \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource */ public function __construct( diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Shipment/Collection.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Shipment/Collection.php index f004a1ee37e6..8758fc1da92d 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Shipment/Collection.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Shipment/Collection.php @@ -57,14 +57,16 @@ protected function _construct() } /** - * Used to emulate after load functionality for each item without loading them + * Unserialize packages in each item * * @return $this */ protected function _afterLoad() { - $this->walk('afterLoad'); + foreach ($this->_items as $item) { + $this->getResource()->unserializeFields($item); + } - return $this; + return parent::_afterLoad(); } } diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Shipment/Relation.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Shipment/Relation.php index 9c8671d02c57..5851b2d93613 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Shipment/Relation.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Shipment/Relation.php @@ -62,8 +62,8 @@ public function processRelation(\Magento\Framework\Model\AbstractModel $object) $this->shipmentItemResource->save($item); } } - if (null !== $object->getTracksCollection()) { - foreach ($object->getTracksCollection() as $track) { + if (null !== $object->getTracks()) { + foreach ($object->getTracks() as $track) { $track->setParentId($object->getId()); $this->shipmentTrackResource->save($track); } diff --git a/app/code/Magento/Sales/Model/ResourceModel/Report/Collection/AbstractCollection.php b/app/code/Magento/Sales/Model/ResourceModel/Report/Collection/AbstractCollection.php index 86d86255a0c2..9c4c87c2e2e2 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Report/Collection/AbstractCollection.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Report/Collection/AbstractCollection.php @@ -25,7 +25,7 @@ class AbstractCollection extends \Magento\Reports\Model\ResourceModel\Report\Col * @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy * @param \Magento\Framework\Event\ManagerInterface $eventManager * @param \Magento\Sales\Model\ResourceModel\Report $resource - * @param null $connection + * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection */ public function __construct( \Magento\Framework\Data\Collection\EntityFactory $entityFactory, diff --git a/app/code/Magento/Sales/Model/Service/CreditmemoService.php b/app/code/Magento/Sales/Model/Service/CreditmemoService.php index e8f2e6e5305f..e4435d3481a3 100644 --- a/app/code/Magento/Sales/Model/Service/CreditmemoService.php +++ b/app/code/Magento/Sales/Model/Service/CreditmemoService.php @@ -98,7 +98,7 @@ public function __construct( * Cancel an existing creditmemo * * @param int $id Credit Memo Id - * @return bool + * @return void * @throws \Magento\Framework\Exception\LocalizedException * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -166,8 +166,8 @@ public function refund( $creditmemo->getOrder(), !$offlineRequested ); - $this->getOrderRepository()->save($order); $this->creditmemoRepository->save($creditmemo); + $this->getOrderRepository()->save($order); $connection->commit(); } catch (\Exception $e) { $connection->rollBack(); @@ -178,6 +178,8 @@ public function refund( } /** + * Validates if credit memo is available for refund. + * * @param \Magento\Sales\Api\Data\CreditmemoInterface $creditmemo * @return bool * @throws \Magento\Framework\Exception\LocalizedException @@ -200,7 +202,7 @@ protected function validateForRefund(\Magento\Sales\Api\Data\CreditmemoInterface throw new \Magento\Framework\Exception\LocalizedException( __( 'The most money available to refund is %1.', - $creditmemo->getOrder()->formatBasePrice($baseAvailableRefund) + $creditmemo->getOrder()->formatPriceTxt($baseAvailableRefund) ) ); } @@ -208,8 +210,9 @@ protected function validateForRefund(\Magento\Sales\Api\Data\CreditmemoInterface } /** - * @return \Magento\Sales\Model\Order\RefundAdapterInterface + * Initializes RefundAdapterInterface dependency. * + * @return \Magento\Sales\Model\Order\RefundAdapterInterface * @deprecated 100.1.3 */ private function getRefundAdapter() @@ -222,8 +225,9 @@ private function getRefundAdapter() } /** - * @return \Magento\Framework\App\ResourceConnection|mixed + * Initializes ResourceConnection dependency. * + * @return \Magento\Framework\App\ResourceConnection|mixed * @deprecated 100.1.3 */ private function getResource() @@ -236,8 +240,9 @@ private function getResource() } /** - * @return \Magento\Sales\Api\OrderRepositoryInterface + * Initializes OrderRepositoryInterface dependency. * + * @return \Magento\Sales\Api\OrderRepositoryInterface * @deprecated 100.1.3 */ private function getOrderRepository() @@ -250,8 +255,9 @@ private function getOrderRepository() } /** - * @return \Magento\Sales\Api\InvoiceRepositoryInterface + * Initializes InvoiceRepositoryInterface dependency. * + * @return \Magento\Sales\Api\InvoiceRepositoryInterface * @deprecated 100.1.3 */ private function getInvoiceRepository() diff --git a/app/code/Magento/Sales/Model/Service/InvoiceService.php b/app/code/Magento/Sales/Model/Service/InvoiceService.php index 718f55c3e551..18efeba726c1 100644 --- a/app/code/Magento/Sales/Model/Service/InvoiceService.php +++ b/app/code/Magento/Sales/Model/Service/InvoiceService.php @@ -7,9 +7,14 @@ use Magento\Sales\Api\InvoiceManagementInterface; use Magento\Sales\Model\Order; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Catalog\Model\Product\Type; /** * Class InvoiceService + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class InvoiceService implements InvoiceManagementInterface { @@ -58,6 +63,13 @@ class InvoiceService implements InvoiceManagementInterface */ protected $orderConverter; + /** + * Serializer interface instance. + * + * @var Json + */ + private $serializer; + /** * Constructor * @@ -68,6 +80,7 @@ class InvoiceService implements InvoiceManagementInterface * @param \Magento\Sales\Model\Order\InvoiceNotifier $notifier * @param \Magento\Sales\Api\OrderRepositoryInterface $orderRepository * @param \Magento\Sales\Model\Convert\Order $orderConverter + * @param Json|null $serializer */ public function __construct( \Magento\Sales\Api\InvoiceRepositoryInterface $repository, @@ -76,7 +89,8 @@ public function __construct( \Magento\Framework\Api\FilterBuilder $filterBuilder, \Magento\Sales\Model\Order\InvoiceNotifier $notifier, \Magento\Sales\Api\OrderRepositoryInterface $orderRepository, - \Magento\Sales\Model\Convert\Order $orderConverter + \Magento\Sales\Model\Convert\Order $orderConverter, + Json $serializer = null ) { $this->repository = $repository; $this->commentRepository = $commentRepository; @@ -85,6 +99,7 @@ public function __construct( $this->invoiceNotifier = $notifier; $this->orderRepository = $orderRepository; $this->orderConverter = $orderConverter; + $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class); } /** @@ -125,6 +140,8 @@ public function setVoid($id) } /** + * Creates an invoice based on the order and quantities provided + * * @param Order $order * @param array $qtys * @return \Magento\Sales\Model\Order\Invoice @@ -132,19 +149,20 @@ public function setVoid($id) */ public function prepareInvoice(Order $order, array $qtys = []) { + $isQtysEmpty = empty($qtys); $invoice = $this->orderConverter->toInvoice($order); $totalQty = 0; $qtys = $this->prepareItemsQty($order, $qtys); foreach ($order->getAllItems() as $orderItem) { - if (!$this->_canInvoiceItem($orderItem)) { + if (!$this->_canInvoiceItem($orderItem, $qtys)) { continue; } $item = $this->orderConverter->itemToInvoiceItem($orderItem); - if ($orderItem->isDummy()) { - $qty = $orderItem->getQtyOrdered() ? $orderItem->getQtyOrdered() : 1; - } elseif (isset($qtys[$orderItem->getId()])) { + if (isset($qtys[$orderItem->getId()])) { $qty = (double) $qtys[$orderItem->getId()]; - } elseif (empty($qtys)) { + } elseif ($orderItem->isDummy()) { + $qty = $orderItem->getQtyOrdered() ? $orderItem->getQtyOrdered() : 1; + } elseif ($isQtysEmpty) { $qty = $orderItem->getQtyToInvoice(); } else { $qty = 0; @@ -170,38 +188,86 @@ private function prepareItemsQty(Order $order, array $qtys = []) { foreach ($order->getAllItems() as $orderItem) { if (empty($qtys[$orderItem->getId()])) { - continue; + if ($orderItem->getProductType() == Type::TYPE_BUNDLE && !$orderItem->isShipSeparately()) { + $qtys[$orderItem->getId()] = $orderItem->getQtyOrdered() - $orderItem->getQtyInvoiced(); + } else { + $parentItem = $orderItem->getParentItem(); + $parentItemId = $parentItem ? $parentItem->getId() : null; + if ($parentItemId && isset($qtys[$parentItemId])) { + $qtys[$orderItem->getId()] = $qtys[$parentItemId]; + } + continue; + } } - if ($orderItem->isDummy()) { - if ($orderItem->getHasChildren()) { - foreach ($orderItem->getChildrenItems() as $child) { - if (!isset($qtys[$child->getId()])) { - $qtys[$child->getId()] = $child->getQtyToInvoice(); - } + + $this->prepareItemQty($orderItem, $qtys); + } + + return $qtys; + } + + /** + * Prepare qty_invoiced for order item + * + * @param \Magento\Sales\Api\Data\OrderItemInterface $orderItem + * @param array $qtys + */ + private function prepareItemQty(\Magento\Sales\Api\Data\OrderItemInterface $orderItem, &$qtys) + { + $this->prepareBundleQty($orderItem, $qtys); + + if ($orderItem->isDummy()) { + if ($orderItem->getHasChildren()) { + foreach ($orderItem->getChildrenItems() as $child) { + if (!isset($qtys[$child->getId()])) { + $qtys[$child->getId()] = $child->getQtyToInvoice(); } - } elseif ($orderItem->getParentItem()) { - $parent = $orderItem->getParentItem(); - if (!isset($qtys[$parent->getId()])) { - $qtys[$parent->getId()] = $parent->getQtyToInvoice(); + $parentId = $orderItem->getParentItemId(); + if ($parentId && array_key_exists($parentId, $qtys)) { + $qtys[$orderItem->getId()] = $qtys[$parentId]; + } else { + continue; } } + } elseif ($orderItem->getParentItem()) { + $parent = $orderItem->getParentItem(); + if (!isset($qtys[$parent->getId()])) { + $qtys[$parent->getId()] = $parent->getQtyToInvoice(); + } } } + } - return $qtys; + /** + * Prepare qty to invoice for bundle products + * + * @param \Magento\Sales\Api\Data\OrderItemInterface $orderItem + * @param array $qtys + */ + private function prepareBundleQty(\Magento\Sales\Api\Data\OrderItemInterface $orderItem, &$qtys) + { + if ($orderItem->getProductType() == Type::TYPE_BUNDLE && !$orderItem->isShipSeparately()) { + foreach ($orderItem->getChildrenItems() as $childItem) { + $bundleSelectionAttributes = $childItem->getProductOptionByCode('bundle_selection_attributes'); + if (is_string($bundleSelectionAttributes)) { + $bundleSelectionAttributes = $this->serializer->unserialize($bundleSelectionAttributes); + } + + $qtys[$childItem->getId()] = $qtys[$orderItem->getId()] * $bundleSelectionAttributes['qty']; + } + } } /** - * Check if order item can be invoiced. Dummy item can be invoiced or with his children or - * with parent item which is included to invoice + * Check if order item can be invoiced. * * @param \Magento\Sales\Api\Data\OrderItemInterface $item + * @param array $qtys * @return bool * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - protected function _canInvoiceItem(\Magento\Sales\Api\Data\OrderItemInterface $item) + protected function _canInvoiceItem(\Magento\Sales\Api\Data\OrderItemInterface $item, array $qtys = []) { - $qtys = []; if ($item->getLockedDoInvoice()) { return false; } diff --git a/app/code/Magento/Sales/Model/Service/PaymentFailuresService.php b/app/code/Magento/Sales/Model/Service/PaymentFailuresService.php index 3a49bbce256e..a698276332af 100644 --- a/app/code/Magento/Sales/Model/Service/PaymentFailuresService.php +++ b/app/code/Magento/Sales/Model/Service/PaymentFailuresService.php @@ -9,6 +9,7 @@ use Magento\Backend\App\Area\FrontNameResolver; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Mail\Template\TransportBuilder; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\Framework\Translate\Inline\StateInterface; @@ -17,11 +18,14 @@ use Magento\Sales\Api\PaymentFailuresInterface; use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\Store; +use Psr\Log\LoggerInterface; /** * Service is responsible for handling failed payment transactions. * * It depends on Stores > Configuration > Sales > Checkout > Payment Failed Emails configuration. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class PaymentFailuresService implements PaymentFailuresInterface { @@ -52,25 +56,33 @@ class PaymentFailuresService implements PaymentFailuresInterface */ private $cartRepository; + /** + * @var LoggerInterface + */ + private $logger; + /** * @param ScopeConfigInterface $scopeConfig * @param StateInterface $inlineTranslation * @param TransportBuilder $transportBuilder * @param TimezoneInterface $localeDate * @param CartRepositoryInterface $cartRepository + * @param LoggerInterface|null $logger */ public function __construct( ScopeConfigInterface $scopeConfig, StateInterface $inlineTranslation, TransportBuilder $transportBuilder, TimezoneInterface $localeDate, - CartRepositoryInterface $cartRepository + CartRepositoryInterface $cartRepository, + LoggerInterface $logger = null ) { $this->scopeConfig = $scopeConfig; $this->inlineTranslation = $inlineTranslation; $this->transportBuilder = $transportBuilder; $this->localeDate = $localeDate; $this->cartRepository = $cartRepository; + $this->logger = $logger ?: ObjectManager::getInstance()->get(LoggerInterface::class); } /** @@ -128,7 +140,11 @@ public function handle( ->addBcc($bcc) ->getTransport(); - $transport->sendMessage(); + try { + $transport->sendMessage(); + } catch (\Exception $e) { + $this->logger->critical($e->getMessage()); + } } $this->inlineTranslation->resume(); diff --git a/app/code/Magento/Sales/Observer/AssignOrderToCustomerObserver.php b/app/code/Magento/Sales/Observer/AssignOrderToCustomerObserver.php index cade86d18e93..5883bde17510 100644 --- a/app/code/Magento/Sales/Observer/AssignOrderToCustomerObserver.php +++ b/app/code/Magento/Sales/Observer/AssignOrderToCustomerObserver.php @@ -31,7 +31,7 @@ public function __construct(OrderRepositoryInterface $orderRepository) } /** - * {@inheritdoc} + * @inheritdoc */ public function execute(Observer $observer) { @@ -44,9 +44,16 @@ public function execute(Observer $observer) $orderId = $delegateData['__sales_assign_order_id']; $order = $this->orderRepository->get($orderId); if (!$order->getCustomerId()) { - //if customer ID wasn't already assigned then assigning. - $order->setCustomerId($customer->getId()); - $order->setCustomerIsGuest(0); + //assign customer info to order after customer creation. + $order->setCustomerId($customer->getId()) + ->setCustomerIsGuest(0) + ->setCustomerEmail($customer->getEmail()) + ->setCustomerFirstname($customer->getFirstname()) + ->setCustomerLastname($customer->getLastname()) + ->setCustomerMiddlename($customer->getMiddlename()) + ->setCustomerPrefix($customer->getPrefix()) + ->setCustomerSuffix($customer->getSuffix()) + ->setCustomerGroupId($customer->getGroupId()); $this->orderRepository->save($order); } } diff --git a/app/code/Magento/Sales/Setup/Patch/Data/FillQuoteAddressIdInSalesOrderAddress.php b/app/code/Magento/Sales/Setup/Patch/Data/FillQuoteAddressIdInSalesOrderAddress.php index 2716e860243b..a75690536e76 100644 --- a/app/code/Magento/Sales/Setup/Patch/Data/FillQuoteAddressIdInSalesOrderAddress.php +++ b/app/code/Magento/Sales/Setup/Patch/Data/FillQuoteAddressIdInSalesOrderAddress.php @@ -8,15 +8,15 @@ use Magento\Eav\Model\Config; use Magento\Framework\App\State; -use Magento\Quote\Model\QuoteFactory; -use Magento\Sales\Model\OrderFactory; -use Magento\Sales\Model\ResourceModel\Order\Address\CollectionFactory as AddressCollectionFactory; -use Magento\Framework\App\ResourceConnection; -use Magento\Sales\Setup\SalesSetupFactory; +use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; -use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Sales\Model\Order\Address; +use Magento\Sales\Setup\SalesSetupFactory; +/** + * Fills quote_address_id in table sales_order_address if it is empty. + */ class FillQuoteAddressIdInSalesOrderAddress implements DataPatchInterface, PatchVersionInterface { /** @@ -24,11 +24,6 @@ class FillQuoteAddressIdInSalesOrderAddress implements DataPatchInterface, Patch */ private $moduleDataSetup; - /** - * @var SalesSetupFactory - */ - private $salesSetupFactory; - /** * @var State */ @@ -40,44 +35,22 @@ class FillQuoteAddressIdInSalesOrderAddress implements DataPatchInterface, Patch private $eavConfig; /** - * @var AddressCollectionFactory - */ - private $addressCollectionFactory; - - /** - * @var OrderFactory - */ - private $orderFactory; - - /** - * @var QuoteFactory - */ - private $quoteFactory; - - /** - * PatchInitial constructor. * @param ModuleDataSetupInterface $moduleDataSetup + * @param State $state + * @param Config $eavConfig */ public function __construct( ModuleDataSetupInterface $moduleDataSetup, - SalesSetupFactory $salesSetupFactory, State $state, - Config $eavConfig, - AddressCollectionFactory $addressCollectionFactory, - OrderFactory $orderFactory, - QuoteFactory $quoteFactory + Config $eavConfig ) { $this->moduleDataSetup = $moduleDataSetup; - $this->salesSetupFactory = $salesSetupFactory; $this->state = $state; $this->eavConfig = $eavConfig; - $this->addressCollectionFactory = $addressCollectionFactory; - $this->orderFactory = $orderFactory; - $this->quoteFactory = $quoteFactory; } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { @@ -96,32 +69,12 @@ public function apply() */ public function fillQuoteAddressIdInSalesOrderAddress(ModuleDataSetupInterface $setup) { - $addressTable = $setup->getTable('sales_order_address'); - $updateOrderAddress = $setup->getConnection() - ->select() - ->joinInner( - ['sales_order' => $setup->getTable('sales_order')], - $addressTable . '.parent_id = sales_order.entity_id', - ['quote_address_id' => 'quote_address.address_id'] - ) - ->joinInner( - ['quote_address' => $setup->getTable('quote_address')], - 'sales_order.quote_id = quote_address.quote_id - AND ' . $addressTable . '.address_type = quote_address.address_type', - [] - ) - ->where( - $addressTable . '.quote_address_id IS NULL' - ); - $updateOrderAddress = $setup->getConnection()->updateFromSelect( - $updateOrderAddress, - $addressTable - ); - $setup->getConnection()->query($updateOrderAddress); + $this->fillQuoteAddressIdInSalesOrderAddressByType($setup, Address::TYPE_SHIPPING); + $this->fillQuoteAddressIdInSalesOrderAddressByType($setup, Address::TYPE_BILLING); } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -131,7 +84,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -139,10 +92,99 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { return []; } + + /** + * Fill quote_address_id in sales_order_address by type. + * + * @param ModuleDataSetupInterface $setup + * @param string $addressType + * @throws \Zend_Db_Statement_Exception + */ + private function fillQuoteAddressIdInSalesOrderAddressByType(ModuleDataSetupInterface $setup, $addressType) + { + $salesConnection = $setup->getConnection('sales'); + + $orderTable = $setup->getTable('sales_order', 'sales'); + $orderAddressTable = $setup->getTable('sales_order_address', 'sales'); + + $query = $salesConnection + ->select() + ->from( + ['sales_order_address' => $orderAddressTable], + ['entity_id', 'address_type'] + ) + ->joinInner( + ['sales_order' => $orderTable], + 'sales_order_address.parent_id = sales_order.entity_id', + ['quote_id' => 'sales_order.quote_id'] + ) + ->where('sales_order_address.quote_address_id IS NULL') + ->where('sales_order_address.address_type = ?', $addressType) + ->order('sales_order_address.entity_id'); + + $batchSize = 5000; + $result = $salesConnection->query($query); + $count = $result->rowCount(); + $batches = ceil($count / $batchSize); + + for ($batch = $batches; $batch > 0; $batch--) { + $query->limitPage($batch, $batchSize); + $result = $salesConnection->fetchAssoc($query); + + $this->fillQuoteAddressIdInSalesOrderAddressProcessBatch($setup, $result, $addressType); + } + } + + /** + * Process filling quote_address_id in sales_order_address in batch. + * + * @param ModuleDataSetupInterface $setup + * @param array $orderAddresses + * @param string $addressType + */ + private function fillQuoteAddressIdInSalesOrderAddressProcessBatch( + ModuleDataSetupInterface $setup, + array $orderAddresses, + $addressType + ) { + $salesConnection = $setup->getConnection('sales'); + $quoteConnection = $setup->getConnection('checkout'); + + $quoteAddressTable = $setup->getTable('quote_address', 'checkout'); + $quoteTable = $setup->getTable('quote', 'checkout'); + $salesOrderAddressTable = $setup->getTable('sales_order_address', 'sales'); + + $query = $quoteConnection + ->select() + ->from( + ['quote_address' => $quoteAddressTable], + ['quote_id', 'address_id'] + ) + ->joinInner( + ['quote' => $quoteTable], + 'quote_address.quote_id = quote.entity_id', + [] + ) + ->where('quote.entity_id in (?)', array_column($orderAddresses, 'quote_id')) + ->where('address_type = ?', $addressType); + + $quoteAddresses = $quoteConnection->fetchAssoc($query); + + foreach ($orderAddresses as $orderAddress) { + $bind = [ + 'quote_address_id' => $quoteAddresses[$orderAddress['quote_id']]['address_id'] ?? null, + ]; + $where = [ + 'entity_id = ?' => $orderAddress['entity_id'] + ]; + + $salesConnection->update($salesOrderAddressTable, $bind, $where); + } + } } diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminInvoiceActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminInvoiceActionGroup.xml index 15aff7c751a1..b90bac7e0881 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminInvoiceActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminInvoiceActionGroup.xml @@ -19,18 +19,15 @@ <see selector="{{AdminInvoiceOrderInformationSection.customerName}}" userInput="{{customer.firstname}}" stepKey="seeCustomerName"/> <see selector="{{AdminInvoiceOrderInformationSection.customerEmail}}" userInput="{{customer.email}}" stepKey="seeCustomerEmail"/> <see selector="{{AdminInvoiceOrderInformationSection.customerGroup}}" userInput="{{customerGroup.code}}" stepKey="seeCustomerGroup"/> - <see selector="{{AdminInvoiceAddressInformationSection.billingAddress}}" userInput="{{billingAddress.street[0]}}" stepKey="seeBillingAddressStreet"/> <see selector="{{AdminInvoiceAddressInformationSection.billingAddress}}" userInput="{{billingAddress.city}}" stepKey="seeBillingAddressCity"/> <see selector="{{AdminInvoiceAddressInformationSection.billingAddress}}" userInput="{{billingAddress.country_id}}" stepKey="seeBillingAddressCountry"/> <see selector="{{AdminInvoiceAddressInformationSection.billingAddress}}" userInput="{{billingAddress.postcode}}" stepKey="seeBillingAddressPostcode"/> - <see selector="{{AdminInvoiceAddressInformationSection.shippingAddress}}" userInput="{{shippingAddress.street[0]}}" stepKey="seeShippingAddressStreet"/> <see selector="{{AdminInvoiceAddressInformationSection.shippingAddress}}" userInput="{{shippingAddress.city}}" stepKey="seeShippingAddressCity"/> <see selector="{{AdminInvoiceAddressInformationSection.shippingAddress}}" userInput="{{shippingAddress.country_id}}" stepKey="seeShippingAddressCountry"/> <see selector="{{AdminInvoiceAddressInformationSection.shippingAddress}}" userInput="{{shippingAddress.postcode}}" stepKey="seeShippingAddressPostcode"/> </actionGroup> - <!--Check that product is in invoice items--> <actionGroup name="seeProductInInvoiceItems"> <arguments> @@ -38,4 +35,49 @@ </arguments> <see selector="{{AdminInvoiceItemsSection.skuColumn}}" userInput="{{product.sku}}" stepKey="seeProductSkuInGrid"/> </actionGroup> + <!--Admin Fast Create Invoice--> + <actionGroup name="adminFastCreateInvoice"> + <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceButton"/> + <waitForPageLoad stepKey="waitForNewInvoicePageLoad"/> + <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeSuccessMessage"/> + <click selector="{{AdminOrderDetailsOrderViewSection.invoices}}" stepKey="clickInvoices"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask5" /> + <click selector="{{AdminOrderInvoicesTabSection.viewInvoice}}" stepKey="openInvoicePage"/> + <waitForPageLoad stepKey="waitForInvoicePageLoad"/> + </actionGroup> + <actionGroup name="clearInvoicesGridFilters"> + <amOnPage url="{{AdminInvoicesPage.url}}" stepKey="goToInvoices"/> + <waitForPageLoad stepKey="waitInvoicesGridToLoad"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearFilters" /> + <waitForPageLoad stepKey="waitInvoicesGrid"/> + </actionGroup> + <actionGroup name="goToInvoiceIntoOrder"> + <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceAction"/> + <seeInCurrentUrl url="{{AdminInvoiceNewPage.url}}" stepKey="seeOrderInvoiceUrl"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Invoice" stepKey="seePageNameNewInvoicePage"/> + </actionGroup> + <actionGroup name="StartCreateInvoiceFromOrderPage"> + <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceAction"/> + <seeInCurrentUrl url="{{AdminInvoiceNewPage.url}}" stepKey="seeNewInvoiceUrl"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Invoice" stepKey="seeNewInvoicePageTitle"/> + </actionGroup> + <actionGroup name="SubmitInvoice"> + <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + <waitForElementVisible selector="{{AdminMessagesSection.successMessage}}" stepKey="waitForMessageAppears"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeInvoiceCreateSuccess"/> + <grabFromCurrentUrl regex="~/order_id/(\d+)/~" stepKey="grabOrderId"/> + <seeInCurrentUrl url="{{AdminOrderDetailsPage.url('$grabOrderId')}}" stepKey="seeViewOrderPageInvoice"/> + </actionGroup> + <!--Filter invoices by order id --> + <actionGroup name="filterInvoiceGridByOrderId"> + <arguments> + <argument name="orderId" type="string"/> + </arguments> + <amOnPage url="{{AdminInvoicesPage.url}}" stepKey="goToInvoices"/> + <click selector="{{AdminInvoicesGridSection.filter}}" stepKey="clickFilter"/> + <fillField selector="{{AdminInvoicesFiltersSection.orderNum}}" userInput="{{orderId}}" stepKey="fillOrderIdForFilter"/> + <click selector="{{AdminInvoicesFiltersSection.applyFilters}}" stepKey="clickApplyFilters"/> + <waitForPageLoad stepKey="waitForFiltersApply"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml index c82623632d78..0e09f3933c1a 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml @@ -18,7 +18,8 @@ <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Orders" stepKey="seeIndexPageTitle"/> <click selector="{{AdminOrdersGridSection.createNewOrder}}" stepKey="clickCreateNewOrder"/> <click selector="{{AdminOrderFormActionSection.CreateNewCustomer}}" stepKey="clickCreateCustomer"/> - <click selector="{{AdminOrderStoreScopeTreeSection.storeOption(storeView.name)}}" stepKey="selectDefaultStoreView"/> + <conditionalClick selector="{{AdminOrderStoreScopeTreeSection.storeOption(storeView.name)}}" dependentSelector="{{AdminOrderStoreScopeTreeSection.storeOption(storeView.name)}}" visible="true" stepKey="selectStoreViewIfAppears"/> + <waitForPageLoad stepKey="waitForCreateOrderPageLoadAfterStoreSelect" /> <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Create New Order" stepKey="seeNewOrderPageTitle"/> </actionGroup> @@ -39,20 +40,35 @@ <actionGroup name="navigateToNewOrderPageExistingCustomer"> <arguments> <argument name="customer"/> + <argument name="storeView" defaultValue="_defaultStore"/> </arguments> <amOnPage url="{{AdminOrdersPage.url}}" stepKey="navigateToOrderIndexPage"/> <waitForPageLoad stepKey="waitForIndexPageLoad"/> <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Orders" stepKey="seeIndexPageTitle"/> <click selector="{{AdminOrdersGridSection.createNewOrder}}" stepKey="clickCreateNewOrder"/> <waitForPageLoad stepKey="waitForCustomerGridLoad"/> + <!--Clear grid filters--> + <conditionalClick selector="{{AdminOrderCustomersGridSection.resetButton}}" dependentSelector="{{AdminOrderCustomersGridSection.resetButton}}" visible="true" stepKey="clearExistingCustomerFilters"/> <fillField userInput="{{customer.email}}" selector="{{AdminOrderCustomersGridSection.emailInput}}" stepKey="filterEmail"/> <click selector="{{AdminOrderCustomersGridSection.apply}}" stepKey="applyFilter"/> <waitForPageLoad stepKey="waitForFilteredCustomerGridLoad"/> <click selector="{{AdminOrderCustomersGridSection.firstRow}}" stepKey="clickOnCustomer"/> <waitForPageLoad stepKey="waitForCreateOrderPageLoad" /> + <!-- Select store view if appears --> + <conditionalClick selector="{{AdminOrderStoreScopeTreeSection.storeOption(storeView.name)}}" dependentSelector="{{AdminOrderStoreScopeTreeSection.storeOption(storeView.name)}}" visible="true" stepKey="selectStoreViewIfAppears"/> + <waitForPageLoad stepKey="waitForCreateOrderPageLoadAfterStoreSelect" /> <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Create New Order" stepKey="seeNewOrderPageTitle"/> </actionGroup> + <!--Navigate to New Order Page for existing Customer And Store--> + <actionGroup name="NavigateToNewOrderPageExistingCustomerAndStoreActionGroup" extends="navigateToNewOrderPageExistingCustomer" > + <arguments> + <argument name="storeView" defaultValue="_defaultStore"/> + </arguments> + <click selector="{{AdminOrderStoreScopeTreeSection.storeOption(storeView.name)}}" stepKey="selectStoreView" after="waitForCreateOrderPageLoad"/> + <waitForPageLoad stepKey="waitForLoad" after="selectStoreView"/> + </actionGroup> + <!--Check the required fields are actually required--> <actionGroup name="checkRequiredFieldsNewOrderForm"> <seeElement selector="{{AdminOrderFormAccountSection.requiredGroup}}" stepKey="seeCustomerGroupRequired"/> @@ -92,6 +108,7 @@ <fillField selector="{{AdminOrderFormItemsSection.rowQty('1')}}" userInput="1" stepKey="fillProductQty"/> <scrollTo selector="{{AdminOrderFormItemsSection.addSelected}}" x="0" y="-100" stepKey="scrollToAddSelectedButton"/> <click selector="{{AdminOrderFormItemsSection.addSelected}}" stepKey="clickAddSelectedProducts"/> + <wait time="5" stepKey="waitForOptionsToLoad"/> </actionGroup> <!--Add configurable product to order --> @@ -156,6 +173,18 @@ <click selector="{{AdminOrderFormItemsSection.addSelected}}" stepKey="clickAddSelectedProducts"/> </actionGroup> + <!--Add bundle product to order and check product price in the grid--> + <actionGroup name="addBundleProductToOrderAndCheckPriceInGrid" extends="addBundleProductToOrder"> + <arguments> + <argument name="price" type="string"/> + </arguments> + <grabTextFrom selector="{{AdminOrderFormItemsSection.rowPrice('1')}}" stepKey="grabProductPriceFromGrid" after="clickOk"/> + <assertEquals stepKey="assertProductPriceInGrid" message="Bundle product price in grid should be equal {{price}}" after="grabProductPriceFromGrid"> + <expectedResult type="string">{{price}}</expectedResult> + <actualResult type="variable">grabProductPriceFromGrid</actualResult> + </assertEquals> + </actionGroup> + <!--Add downloadable product to order --> <actionGroup name="addDownloadableProductToOrder"> <arguments> @@ -222,6 +251,16 @@ <waitForElementVisible selector="{{AdminOrderFormPaymentSection.flatRateOption}}" stepKey="waitForShippingOptions"/> <selectOption selector="{{AdminOrderFormPaymentSection.flatRateOption}}" userInput="flatrate_flatrate" stepKey="checkFlatRate"/> </actionGroup> + + <!--Select free shipping method--> + <actionGroup name="orderSelectFreeShipping"> + <click selector="{{AdminOrderFormPaymentSection.header}}" stepKey="unfocus"/> + <waitForPageLoad stepKey="waitForJavascriptToFinish"/> + <click selector="{{AdminOrderFormPaymentSection.getShippingMethods}}" stepKey="clickShippingMethods"/> + <waitForElementVisible selector="{{AdminOrderFormPaymentSection.freeShippingOption}}" stepKey="waitForShippingOptions"/> + <selectOption selector="{{AdminOrderFormPaymentSection.freeShippingOption}}" userInput="freeshipping_freeshipping" stepKey="checkFreeShipping"/> + </actionGroup> + <!--Check that customer information is correct in order--> <actionGroup name="verifyBasicOrderInformation"> <arguments> @@ -259,13 +298,95 @@ <see selector="{{AdminOrderItemsOrderedSection.productSkuColumn}}" userInput="{{product.sku}}" stepKey="seeSkuInItemsOrdered"/> </actionGroup> + <actionGroup name="CreateOrderInStoreActionGroup"> + <arguments> + <argument name="product"/> + <argument name="customer"/> + <argument name="storeView"/> + </arguments> + <amOnPage stepKey="navigateToNewOrderPage" url="{{AdminOrderCreatePage.url}}"/> + <click stepKey="chooseCustomer" selector="{{AdminOrdersGridSection.customerInOrdersSection(customer.firstname)}}"/> + <waitForPageLoad stepKey="waitForStoresPageOpened"/> + <click stepKey="chooseStore" selector="{{AdminOrderStoreScopeTreeSection.storeForOrder(storeView.name)}}"/> + <scrollToTopOfPage stepKey="scrollToTop"/> + <waitForPageLoad stepKey="waitForStoreToAppear"/> + <click selector="{{OrdersGridSection.addProducts}}" stepKey="clickOnAddProducts"/> + <waitForPageLoad stepKey="waitForProductsListForOrder"/> + <click selector="{{AdminOrdersGridSection.productForOrder(product.sku)}}" stepKey="chooseTheProduct"/> + <click selector="{{AdminOrderFormItemsSection.addSelected}}" stepKey="addSelectedProductToOrder"/> + <waitForPageLoad stepKey="waitForProductAddedInOrder"/> + <click selector="{{AdminInvoicePaymentShippingSection.getShippingMethodAndRates}}" stepKey="openShippingMethod"/> + <waitForPageLoad stepKey="waitForShippingMethods"/> + <click selector="{{AdminInvoicePaymentShippingSection.shippingMethod}}" stepKey="chooseShippingMethod"/> + <waitForPageLoad stepKey="waitForShippingMethodsThickened"/> + <click selector="{{OrdersGridSection.submitOrder}}" stepKey="submitOrder"/> + <see stepKey="seeSuccessMessageForOrder" userInput="You created the order."/> + </actionGroup> + + <actionGroup name="CreateOrderInStoreChoosingPaymentMethodActionGroup"> + <arguments> + <argument name="product"/> + <argument name="customer"/> + <argument name="storeView"/> + </arguments> + <amOnPage stepKey="navigateToNewOrderPage" url="{{AdminOrderCreatePage.url}}"/> + <click stepKey="chooseCustomer" selector="{{AdminOrdersGridSection.customerInOrdersSection(customer.firstname)}}"/> + <waitForPageLoad stepKey="waitForStoresPageOpened"/> + <click stepKey="chooseStore" selector="{{AdminOrderStoreScopeTreeSection.storeForOrder(storeView.name)}}"/> + <scrollToTopOfPage stepKey="scrollToTop"/> + <click selector="{{OrdersGridSection.addProducts}}" stepKey="clickOnAddProducts"/> + <waitForPageLoad stepKey="waitForProductsListForOrder"/> + <click selector="{{AdminOrdersGridSection.productForOrder(product.sku)}}" stepKey="chooseTheProduct"/> + <click selector="{{AdminOrderFormItemsSection.addSelected}}" stepKey="addSelectedProductToOrder"/> + <waitForPageLoad stepKey="waitForProductAddedInOrder"/> + <click selector="{{AdminInvoicePaymentShippingSection.getShippingMethodAndRates}}" stepKey="openShippingMethod"/> + <waitForPageLoad stepKey="waitForShippingMethods"/> + <click selector="{{AdminInvoicePaymentShippingSection.shippingMethod}}" stepKey="chooseShippingMethod"/> + <waitForPageLoad stepKey="waitForShippingMethodsThickened"/> + <waitForElementVisible selector="{{AdminOrderFormPaymentSection.paymentBlock}}" stepKey="waitForPaymentOptions"/> + <conditionalClick selector="{{AdminOrderFormPaymentSection.checkMoneyOption}}" dependentSelector="{{AdminOrderFormPaymentSection.checkMoneyOption}}" visible="true" stepKey="checkCheckMoneyOption"/> + <click selector="{{OrdersGridSection.submitOrder}}" stepKey="submitOrder"/> + <see stepKey="seeSuccessMessageForOrder" userInput="You created the order."/> + </actionGroup> + <!--Cancel order that is in pending status--> <actionGroup name="cancelPendingOrder"> + <arguments> + <argument name="orderStatus" type="string" defaultValue="Canceled"/> + </arguments> <click selector="{{AdminOrderDetailsMainActionsSection.cancel}}" stepKey="clickCancelOrder"/> <waitForElement selector="{{AdminConfirmationModalSection.message}}" stepKey="waitForCancelConfirmation"/> <see selector="{{AdminConfirmationModalSection.message}}" userInput="Are you sure you want to cancel this order?" stepKey="seeConfirmationMessage"/> <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmOrderCancel"/> <see selector="{{AdminMessagesSection.success}}" userInput="You canceled the order." stepKey="seeCancelSuccessMessage"/> - <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="Canceled" stepKey="seeOrderStatusCanceled"/> + <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="{{orderStatus}}" stepKey="seeOrderStatusCanceled"/> + </actionGroup> + + <!--Select Check Money payment method--> + <actionGroup name="SelectCheckMoneyPaymentMethod"> + <waitForElementVisible selector="{{AdminOrderFormPaymentSection.paymentBlock}}" stepKey="waitForPaymentOptions"/> + <conditionalClick selector="{{AdminOrderFormPaymentSection.checkMoneyOption}}" dependentSelector="{{AdminOrderFormPaymentSection.checkMoneyOption}}" visible="true" stepKey="checkCheckMoneyOption"/> + </actionGroup> + + <!-- Create Order --> + <actionGroup name="CreateOrderActionGroup"> + <arguments> + <argument name="product"/> + <argument name="customer"/> + </arguments> + <amOnPage stepKey="navigateToNewOrderPage" url="{{AdminOrderCreatePage.url}}"/> + <click stepKey="chooseCustomer" selector="{{AdminOrdersGridSection.customerInOrdersSection(customer.firstname)}}"/> + <waitForPageLoad stepKey="waitForStoresPageOpened"/> + <click selector="{{OrdersGridSection.addProducts}}" stepKey="clickOnAddProducts"/> + <waitForPageLoad stepKey="waitForProductsListForOrder"/> + <click selector="{{AdminOrdersGridSection.productForOrder(product.sku)}}" stepKey="chooseTheProduct"/> + <click selector="{{AdminOrderFormItemsSection.addSelected}}" stepKey="addSelectedProductToOrder"/> + <waitForPageLoad stepKey="waitForProductAddedInOrder"/> + <click selector="{{AdminInvoicePaymentShippingSection.getShippingMethodAndRates}}" stepKey="openShippingMethod"/> + <waitForPageLoad stepKey="waitForShippingMethods"/> + <click selector="{{AdminInvoicePaymentShippingSection.shippingMethod}}" stepKey="chooseShippingMethod"/> + <waitForPageLoad stepKey="waitForShippingMethodsThickened"/> + <click selector="{{OrdersGridSection.submitOrder}}" stepKey="submitOrder"/> + <see stepKey="seeSuccessMessageForOrder" userInput="You created the order."/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderGridActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderGridActionGroup.xml index eed9f80c251c..a116a23dc02c 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderGridActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderGridActionGroup.xml @@ -14,7 +14,6 @@ <argument name="orderId" type="string"/> </arguments> <amOnPage url="{{AdminOrdersPage.url}}" stepKey="navigateToOrderGridPage"/> - <waitForPageLoad stepKey="waitForOrderGridLoad"/> <conditionalClick selector="{{AdminOrdersGridSection.clearFilters}}" dependentSelector="{{AdminOrdersGridSection.clearFilters}}" visible="true" stepKey="clearExistingOrderFilters"/> <click selector="{{AdminOrdersGridSection.filters}}" stepKey="openOrderGridFilters"/> <fillField selector="{{AdminOrdersGridSection.idFilter}}" userInput="{{orderId}}" stepKey="fillOrderIdFilter"/> @@ -74,4 +73,9 @@ <waitForPageLoad stepKey="waitForPageToLoad"/> <conditionalClick selector="{{AdminOrdersGridSection.clearFilters}}" dependentSelector="{{AdminOrdersGridSection.enabledFilters}}" visible="true" stepKey="clickOnButtonToRemoveFiltersIfPresent"/> </actionGroup> + + <actionGroup name="OpenOrderById" extends="filterOrderGridById"> + <click selector="{{AdminDataGridTableSection.firstRow}}" after="clickOrderApplyFilters" stepKey="openOrderViewPage"/> + <waitForPageLoad after="openOrderViewPage" stepKey="waitForOrderViewPageOpened"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderStatusFormFillAndSaveActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderStatusFormFillAndSaveActionGroup.xml new file mode 100644 index 000000000000..810857714542 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderStatusFormFillAndSaveActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Fill Order status form and click save --> + <actionGroup name="AdminOrderStatusFormFillAndSave"> + <arguments> + <argument name="status" type="string" /> + <argument name="label" type="string" /> + </arguments> + + <fillField stepKey="fillStatusCode" selector="{{AdminOrderStatusFormSection.statusCodeField}}" userInput="{{status}}"/> + <fillField stepKey="fillStatusLabel" selector="{{AdminOrderStatusFormSection.statusLabelField}}" userInput="{{label}}"/> + <click stepKey="clickSaveStatus" selector="{{AdminMainActionsSection.save}}"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertOrderStatusExistsInGridActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertOrderStatusExistsInGridActionGroup.xml new file mode 100644 index 000000000000..5f69f5298768 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertOrderStatusExistsInGridActionGroup.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Search order status grid for item with a specific code and validate data --> + <actionGroup name="AssertOrderStatusExistsInGrid"> + <arguments> + <argument name="status" type="string" /> + <argument name="label" type="string" /> + </arguments> + + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters"/> + <fillField selector="{{AdminOrderStatusGridSection.statusCodeFilterField}}" userInput="{{status}}" stepKey="fillStatusFilter"/> + <click selector="{{AdminSecondaryGridSection.searchButton}}" stepKey="clickSearch"/> + <see selector="{{AdminOrderStatusGridSection.statusCodeDataColumn}}" userInput="{{status}}" stepKey="seeStatusCodeInGrid"/> + <see selector="{{AdminOrderStatusGridSection.statusLabelDataColumn}}" userInput="{{label}}" stepKey="seeStatusLabelInGrid"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertOrderStatusFormSaveDuplicateErrorActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertOrderStatusFormSaveDuplicateErrorActionGroup.xml new file mode 100644 index 000000000000..5b4c3115744c --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertOrderStatusFormSaveDuplicateErrorActionGroup.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Assert that order status is not saved with duplication error message --> + <actionGroup name="AssertOrderStatusFormSaveDuplicateError"> + <see selector="{{AdminMessagesSection.error}}" userInput="We found another order status with the same order status code." stepKey="seeError"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertOrderStatusFormSaveSuccessActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertOrderStatusFormSaveSuccessActionGroup.xml new file mode 100644 index 000000000000..d82f4b9dd25e --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertOrderStatusFormSaveSuccessActionGroup.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Assert that order status saved with success message --> + <actionGroup name="AssertOrderStatusFormSaveSuccess"> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the order status." stepKey="seeSuccess"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateNewOrderActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateNewOrderActionGroup.xml new file mode 100644 index 000000000000..abc5698cc71e --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateNewOrderActionGroup.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="useBraintreeForMasterCard"> + <click stepKey="chooseBraintree" selector="{{NewOrderSection.creditCardBraintree}}"/> + <waitForPageLoad stepKey="waitForBraintreeConfigs"/> + <click stepKey="openCardTypes" selector="{{NewOrderSection.openCardTypes}}"/> + <waitForPageLoad stepKey="waitForCardTypes"/> + <click stepKey="chooseCardType" selector="{{NewOrderSection.masterCard}}"/> + <waitForPageLoad stepKey="waitForCardSelected"/> + + <switchToIFrame stepKey="switchToCardNumber" selector="{{NewOrderSection.cardFrame}}"/> + <waitForElementVisible selector="{{NewOrderSection.creditCardNumber}}" stepKey="waitForFillCardNumber"/> + <fillField stepKey="fillCardNumber" selector="{{NewOrderSection.creditCardNumber}}" userInput="{{PaymentAndShippingInfo.cardNumber}}"/> + <switchToIFrame stepKey="switchBackFromCard"/> + + <switchToIFrame stepKey="switchToExpirationMonth" selector="{{NewOrderSection.monthFrame}}"/> + <waitForElementVisible selector="{{NewOrderSection.expirationMonth}}" stepKey="waitForFillMonth"/> + <fillField stepKey="fillMonth" selector="{{NewOrderSection.expirationMonth}}" userInput="{{PaymentAndShippingInfo.month}}"/> + <switchToIFrame stepKey="switchBackFromMonth"/> + + <switchToIFrame stepKey="switchToExpirationYear" selector="{{NewOrderSection.yearFrame}}"/> + <waitForElementVisible selector="{{NewOrderSection.expirationYear}}" stepKey="waitForFillYear"/> + <fillField stepKey="fillYear" selector="{{NewOrderSection.expirationYear}}" userInput="{{PaymentAndShippingInfo.year}}"/> + <switchToIFrame stepKey="switchBackFromYear"/> + + <switchToIFrame stepKey="switchToCVV" selector="{{NewOrderSection.cvvFrame}}"/> + <waitForElementVisible selector="{{NewOrderSection.cvv}}" stepKey="waitForFillCVV"/> + <fillField stepKey="fillCVV" selector="{{NewOrderSection.cvv}}" userInput="{{PaymentAndShippingInfo.cvv}}"/> + <switchToIFrame stepKey="switchBackFromCVV"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateOrderToPrintPageActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateOrderToPrintPageActionGroup.xml new file mode 100644 index 000000000000..e48e7bb3f0b1 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateOrderToPrintPageActionGroup.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CreateOrderToPrintPageActionGroup"> + <arguments> + <argument name="Category"/> + </arguments> + <amOnPage url="{{StorefrontCategoryPage.url(Category.name)}}" stepKey="onCategoryPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <moveMouseOver selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" stepKey="hoverProduct"/> + <click selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" stepKey="addToCart"/> + <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" time="30" stepKey="waitForProductAdded"/> + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickCart"/> + <click selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="goToCheckout"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <click selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask2"/> + <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> + <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> + <click selector="{{CheckoutSuccessMainSection.orderLink}}" stepKey="clickOrderLink"/> + <click selector="{{StorefrontCustomerOrderViewSection.printOrderLink}}" stepKey="clickPrintOrderLink"/> + </actionGroup> + <actionGroup name="CreateOrderToPrintPageWithSelectedPaymentMethodActionGroup" extends="CreateOrderToPrintPageActionGroup"> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" after="clickNext" stepKey="waitForPaymentSectionLoaded"/> + <conditionalClick selector="{{CheckoutPaymentSection.checkMoneyOrderPayment}}" dependentSelector="{{CheckoutPaymentSection.billingAddress}}" visible="false" before="waitForPlaceOrderButton" stepKey="clickCheckMoneyOrderPayment"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontOrderActionGroupActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontOrderActionGroupActionGroup.xml new file mode 100644 index 000000000000..fcea25f99759 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontOrderActionGroupActionGroup.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!--Fill order information fields and click continue--> + <actionGroup name="StorefrontSearchGuestOrderActionGroup"> + <arguments> + <argument name="orderId" type="string"/> + <argument name="orderLastName" type="string"/> + <argument name="orderEmail" type="string"/> + </arguments> + <amOnPage url="{{StorefrontOrdersAndReturnsPage.url}}" stepKey="navigateToOrderAndReturnPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <fillField selector="{{StorefrontOrderAndReturnInformationSection.orderId}}" userInput="{{orderId}}" stepKey="fillOrderId"/> + <fillField selector="{{StorefrontOrderAndReturnInformationSection.bilingLastName}}" userInput="{{orderLastName}}" stepKey="fillBillingLastName"/> + <fillField selector="{{StorefrontOrderAndReturnInformationSection.email}}" userInput="{{orderEmail}}" stepKey="fillEmail"/> + <click selector="{{StorefrontOrderAndReturnInformationSection.continueButton}}" stepKey="clickContinue"/> + <waitForPageLoad stepKey="waitForOrderInformationPageLoad"/> + <seeInCurrentUrl url="{{StorefrontOrderInformationPage.url}}" stepKey="seeOrderInformationUrl"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Sales/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 000000000000..4f6faccbb26d --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminMenuSalesCreditMemos"> + <data key="pageTitle">Credit Memos</data> + <data key="title">Credit Memos</data> + <data key="dataUiId">magento-sales-sales-creditmemo</data> + </entity> + <entity name="AdminMenuSalesInvoices"> + <data key="pageTitle">Invoices</data> + <data key="title">Invoices</data> + <data key="dataUiId">magento-sales-sales-invoice</data> + </entity> + <entity name="AdminMenuSalesOrders"> + <data key="pageTitle">Orders</data> + <data key="title">Orders</data> + <data key="dataUiId">magento-sales-sales-order</data> + </entity> + <entity name="AdminMenuSalesShipments"> + <data key="pageTitle">Shipments</data> + <data key="title">Shipments</data> + <data key="dataUiId">magento-sales-sales-shipment</data> + </entity> + <entity name="AdminMenuSalesTransactions"> + <data key="pageTitle">Transactions</data> + <data key="title">Transactions</data> + <data key="dataUiId">magento-sales-sales-transactions</data> + </entity> + <entity name="AdminMenuStoresSettingsOrderStatus"> + <data key="pageTitle">Order Status</data> + <data key="title">Order Status</data> + <data key="dataUiId">magento-sales-system-order-statuses</data> + </entity> +</entities> diff --git a/app/code/Magento/Sales/Test/Mftf/Data/OrderStatusData.xml b/app/code/Magento/Sales/Test/Mftf/Data/OrderStatusData.xml new file mode 100644 index 000000000000..aecd7fcf1b70 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Data/OrderStatusData.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="defaultOrderStatus"> + <data key="status" unique="suffix">order_status</data> + <data key="label" unique="suffix">orderLabel</data> + </entity> + <entity name="duplicatingCodeOrderStatus"> + <data key="status">pending</data> + <data key="label" unique="suffix">orderLabel</data> + </entity> + <entity name="duplicatingLabelOrderStatus"> + <data key="status" unique="suffix">order_status</data> + <data key="label">Suspected Fraud</data> + </entity> +</entities> diff --git a/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderCreatePage.xml b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderCreatePage.xml index 333be23dbf34..c8ec12203c67 100644 --- a/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderCreatePage.xml +++ b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderCreatePage.xml @@ -17,5 +17,6 @@ <section name="AdminOrderFormShippingAddressSection"/> <section name="AdminOrderFormPaymentSection"/> <section name="AdminOrderFormTotalSection"/> + <section name="AdminOrderFormStoreSelectorSection"/> </page> -</pages> \ No newline at end of file +</pages> diff --git a/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderPage.xml new file mode 100644 index 000000000000..6abe265a37b7 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderPage.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminOrderPage" url="sales/order/view/order_id/{{var1}}" area="admin" module="Magento_Sales" parameterized="true"> + </page> +</pages> diff --git a/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderProcessDataPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderProcessDataPage.xml new file mode 100644 index 000000000000..2041bf8f3c9a --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderProcessDataPage.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminOrderProcessDataPage" url="sales/order_create/processData" area="admin" module="Magento_Sales"> + <section name="AdminOrderFormItemsOrderedSection"/> + </page> +</pages> diff --git a/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderStatusPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderStatusPage.xml new file mode 100644 index 000000000000..b158e4923074 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderStatusPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminOrderStatusPage" url="sales/order_status" area="admin" module="Magento_Sales"> + <section name="AdminOrderStatusFormSection"/> + </page> +</pages> diff --git a/app/code/Magento/Sales/Test/Mftf/Page/StorefrontCustomerSignOutPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/StorefrontCustomerSignOutPage.xml new file mode 100644 index 000000000000..4e89e5476c3b --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Page/StorefrontCustomerSignOutPage.xml @@ -0,0 +1,12 @@ +<?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="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="StorefrontCustomerSignOutPage" url="/customer/account/logout/" area="storefront" module="Magento_Customer"/> +</pages> diff --git a/app/code/Magento/Sales/Test/Mftf/Page/StorefrontGuestOrderSearchPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/StorefrontGuestOrderSearchPage.xml new file mode 100644 index 000000000000..d2a4f4f0459c --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Page/StorefrontGuestOrderSearchPage.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="StorefrontGuestOrderSearchPage" url="sales/guest/form/" module="Magento_Sales" area="storefront"> + <section name="StorefrontGuestOrderSearchSection"/> + </page> +</pages> diff --git a/app/code/Magento/Sales/Test/Mftf/Page/StorefrontGuestOrderViewPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/StorefrontGuestOrderViewPage.xml new file mode 100644 index 000000000000..69c7fa76129e --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Page/StorefrontGuestOrderViewPage.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="StorefrontGuestOrderViewPage" url="sales/guest/view/" module="Magento_Sales" area="storefront"> + <section name="StorefrontGuestOrderViewSection"/> + </page> +</pages> diff --git a/app/code/Magento/Sales/Test/Mftf/Page/StorefrontOrderInformationPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/StorefrontOrderInformationPage.xml new file mode 100644 index 000000000000..4159f9435c86 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Page/StorefrontOrderInformationPage.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="StorefrontOrderInformationPage" url="sales/guest/view" area="guest" module="Magento_Sales"> + <section name="StorefrontOrderInformationMainSection"/> + </page> +</pages> diff --git a/app/code/Magento/Sales/Test/Mftf/Page/StorefrontOrdersAndReturnsPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/StorefrontOrdersAndReturnsPage.xml new file mode 100644 index 000000000000..ee546174d968 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Page/StorefrontOrdersAndReturnsPage.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="StorefrontOrdersAndReturnsPage" url="sales/guest/form" area="guest" module="Magento_Sales"> + <section name="StorefrontOrderAndReturnInformationSection"/> + </page> +</pages> diff --git a/app/code/Magento/Sales/Test/Mftf/Page/StorefrontSalesOrderPrintPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/StorefrontSalesOrderPrintPage.xml new file mode 100644 index 000000000000..874e6889ec58 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Page/StorefrontSalesOrderPrintPage.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="StorefrontSalesOrderPrintPage" url="/sales/order/print/order_id/{{var1}}/" parameterized="true" + area="storefront" module="Magento_Sales"> + <section name="SalesOrderPrintSection"/> + </page> +</pages> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoTotalSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoTotalSection.xml index 00eb93452edd..731c529f2aec 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoTotalSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoTotalSection.xml @@ -19,5 +19,8 @@ <element name="emailCopy" type="checkbox" selector=".order-totals-actions #send_email"/> <element name="refundStoreCredit" type="checkbox" selector=".order-totals-actions .field-refund-store-credit input[type='checkbox']"/> <element name="submitRefundOffline" type="button" selector=".order-totals-actions button[data-ui-id='order-items-submit-button']" timeout="30"/> + <element name="creditMemoItem" type="text" selector="#sales_order_view_tabs_order_creditmemos"/> + <element name="viewMemo" type="text" selector="div#sales_order_view_tabs_order_creditmemos_content a.action-menu-item"/> + <element name="refundOffline" type="button" selector=".order-totals-actions button[data-ui-id='order-items-submit-offline']"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceItemsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceItemsSection.xml index bc0d1cffd5d3..92c01cf38074 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceItemsSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceItemsSection.xml @@ -29,4 +29,4 @@ <element name="totalColumn" type="text" selector=".order-invoice-tables .col-total .price"/> <element name="updateQty" type="button" selector=".order-invoice-tables tfoot button[data-ui-id='order-items-update-button']"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceMainActionsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceMainActionsSection.xml index 2a241708517b..011500fac3f6 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceMainActionsSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceMainActionsSection.xml @@ -9,6 +9,8 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminInvoiceMainActionsSection"> - <element name="submitInvoice" type="button" selector=".action-default.scalable.save.submit-button.primary"/> + <element name="submitInvoice" type="button" selector=".action-default.scalable.save.submit-button.primary" timeout="60"/> + <element name="openNewCreditMemoFromInvoice" type="button" selector=".action-default.scalable.credit-memo"/> + <element name="submitNewRefundFromInvoice" type="button" selector=".action-default.scalable.save.submit-button refund primary"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoicePaymentShippingSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoicePaymentShippingSection.xml index 918a8e814b55..8d7c64733972 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoicePaymentShippingSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoicePaymentShippingSection.xml @@ -15,5 +15,7 @@ <element name="ShippingMethod" type="text" selector=".order-shipping-address .shipping-description-title"/> <element name="ShippingPrice" type="text" selector=".order-shipping-address .shipping-description-content .price"/> <element name="CreateShipment" type="checkbox" selector=".order-shipping-address input[name='invoice[do_shipment]']"/> + <element name="getShippingMethodAndRates" type="button" selector="//span[text()='Get shipping methods and rates']"/> + <element name="shippingMethod" type="button" selector="//label[contains(text(), 'Fixed')]"/> </section> </sections> \ No newline at end of file diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoicesFiltersSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoicesFiltersSection.xml index 8fb45295bd26..78b9c853ff6a 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoicesFiltersSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoicesFiltersSection.xml @@ -10,5 +10,6 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminInvoicesFiltersSection"> <element name="orderNum" type="input" selector="input[name='order_increment_id']"/> + <element name="applyFilters" type="button" selector="button[data-action='grid-filter-apply']" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderCustomersGridSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderCustomersGridSection.xml index c02a36432851..39318a093dde 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderCustomersGridSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderCustomersGridSection.xml @@ -14,5 +14,6 @@ <element name="resetFilter" type="button" selector=".action-tertiary[title='Reset Filter']"/> <element name="emailInput" type="input" selector="#sales_order_create_customer_grid_filter_email"/> <element name="firstRow" type="button" selector="tr:nth-of-type(1)[data-role='row']"/> + <element name="resetButton" type="button" selector="#sales_order_create_customer_grid [data-action='grid-filter-reset']" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsMainActionsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsMainActionsSection.xml index 578022217f35..6fa5d9a9a378 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsMainActionsSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsMainActionsSection.xml @@ -18,5 +18,6 @@ <element name="ship" type="button" selector="#order_ship" timeout="30"/> <element name="reorder" type="button" selector="#order_reorder" timeout="30"/> <element name="edit" type="button" selector="#order_edit" timeout="30"/> + <element name="modalOk" type="button" selector=".action-accept"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormAccountSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormAccountSection.xml index 4ab1e3327960..11d973d1e19d 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormAccountSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormAccountSection.xml @@ -13,5 +13,6 @@ <element name="email" type="input" selector="#email"/> <element name="requiredGroup" type="text" selector=".admin__field.required[data-ui-id='billing-address-fieldset-element-form-field-group-id']"/> <element name="requiredEmail" type="text" selector=".admin__field.required[data-ui-id='billing-address-fieldset-element-form-field-email']"/> + <element name="defaultGeneral" type="text" selector="//*[contains(text(),'General')]" time="15"/> </section> </sections> \ No newline at end of file diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormActionSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormActionSection.xml index 2f6149dfa1cb..027962282b2c 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormActionSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormActionSection.xml @@ -12,5 +12,8 @@ <element name="SubmitOrder" type="button" selector="#submit_order_top_button" timeout="30"/> <element name="Cancel" type="button" selector="#reset_order_top_button" timeout="30"/> <element name="CreateNewCustomer" type="button" selector="#order-customer-selector .actions button.primary" timeout="30"/> + <element name="submitOrder" type="button" selector="#submit_order_top_button" timeout="30"/> + <element name="cancel" type="button" selector="#reset_order_top_button" timeout="30"/> + <element name="createNewCustomer" type="button" selector="#order-customer-selector .actions button.primary" timeout="30"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormItemsOrderedSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormItemsOrderedSection.xml new file mode 100644 index 000000000000..beb566b20806 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormItemsOrderedSection.xml @@ -0,0 +1,20 @@ +<?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="AdminOrderFormItemsOrderedSection"> + <element name="addProductsBySku" type="button" selector="//section[@id='order-items']//span[contains(text(),'Add Products By SKU')]"/> + <element name="configureButtonBySku" type="button" selector="//div[@class='sku-configure-button']//span[contains(text(),'Configure')]"/> + <element name="configureProductOk" type="button" selector="//div[@class='page-main-actions']//span[contains(text(),'OK')]"/> + <element name="configureProductQtyField" type="input" selector="//*[@id='super-product-table']/tbody/tr[{{arg}}]/td[5]/input[1]" parameterized="true"/> + <element name="addProductToOrder" type="input" selector="//*[@title='Add Products to Order']"/> + <element name="itemsOrderedSummaryText" type="textarea" selector="//table[@class='data-table admin__table-primary order-tables']/tfoot/tr"/> + <element name="configureSelectAttribute" type="select" selector="select[id*=attribute]"/> + </section> +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormItemsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormItemsSection.xml index 7a920166a8ca..d7af13c394fb 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormItemsSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormItemsSection.xml @@ -26,7 +26,12 @@ <element name="row" type="text" selector="#sales_order_create_search_grid_table > tbody tr:nth-of-type({{row}})" parameterized="true"/> <element name="rowCheck" type="checkbox" selector="#sales_order_create_search_grid_table > tbody tr:nth-of-type({{row}}) td.col-select [type=checkbox]" parameterized="true"/> <element name="rowQty" type="input" selector="#sales_order_create_search_grid_table > tbody tr:nth-of-type({{row}}) td.col-qty [name='qty']" parameterized="true"/> + <element name="rowPrice" type="text" selector="#sales_order_create_search_grid_table > tbody tr:nth-of-type({{row}}) td.price" parameterized="true"/> <element name="addSelected" type="button" selector="#order-search .admin__page-section-title .actions button.action-add" timeout="30"/> + <element name="customPriceCheckbox" type="checkbox" selector="//*[@class='custom-price-block']/input"/> + <element name="customPriceField" type="input" selector="//*[@class='custom-price-block']/following-sibling::input"/> + <element name="updateItemsAndQuantities" type="button" selector="//span[contains(text(),'Update Items and Quantities')]"/> + <element name="creditMemo" type="input" selector="#order_creditmemo"/> <element name="configure" type="button" selector=".product-configure-block button.action-default.scalable" timeout="30"/> </section> </sections> \ No newline at end of file diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormPaymentSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormPaymentSection.xml index e4d329bc8505..1c3cf9cc2b35 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormPaymentSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormPaymentSection.xml @@ -13,5 +13,9 @@ <element name="getShippingMethods" type="text" selector="#order-shipping_method a.action-default" timeout="30"/> <element name="flatRateOption" type="radio" selector="#s_method_flatrate_flatrate" timeout="30"/> <element name="shippingError" type="text" selector="#order[has_shipping]-error"/> + <element name="freeShippingOption" type="radio" selector="#s_method_freeshipping_freeshipping" timeout="30"/> + <element name="checkMoneyOption" type="radio" selector="#p_method_checkmo" timeout="30"/> + <element name="paymentBlock" type="text" selector="#order-billing_method" /> + <element name="paymentError" type="text" selector="#payment[method]-error"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormShippingAddressSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormShippingAddressSection.xml index b79d93326876..0f1461b121e1 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormShippingAddressSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormShippingAddressSection.xml @@ -10,6 +10,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminOrderFormShippingAddressSection"> <element name="SameAsBilling" type="checkbox" selector="#order-shipping_same_as_billing"/> + <element name="SelectFromExistingCustomerAddress" type="select" selector="#order-shipping_address_customer_address_id"/> <element name="NamePrefix" type="input" selector="#order-shipping_address_prefix"/> <element name="FirstName" type="input" selector="#order-shipping_address_firstname"/> <element name="MiddleName" type="input" selector="#order-shipping_address_middlename"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormStoreSelectorSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormStoreSelectorSection.xml new file mode 100644 index 000000000000..8602c0d3c608 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormStoreSelectorSection.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="AdminOrderFormStoreSelectorSection"> + <element name="storeSelectorContainer" type="input" selector="#order-store-selector"/> + <element name="defaultStoreViewButton" type="radio" selector="//*[contains(@class,'tree-store-scope')]//label[contains(text(),'Default Store View')]"/> + </section> +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderInvoicesTabSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderInvoicesTabSection.xml index b33276bed527..4ebce4de6b38 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderInvoicesTabSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderInvoicesTabSection.xml @@ -12,5 +12,6 @@ <element name="spinner" type="text" selector="[data-role='spinner'][data-component*='sales_order_view_invoice']"/> <element name="gridRow" type="text" selector="#sales_order_view_tabs_order_invoices_content .data-grid tbody > tr:nth-of-type({{row}})" parameterized="true"/> <element name="viewGridRow" type="button" selector="#sales_order_view_tabs_order_invoices_content .data-grid tbody > tr:nth-of-type({{row}}) a[href*='order_invoice/view']" parameterized="true"/> + <element name="viewInvoice" type="button" selector="//div[@class='admin__data-grid-wrap']//a[@class='action-menu-item']"/> </section> </sections> \ No newline at end of file diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderItemsOrderedSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderItemsOrderedSection.xml index 53aeeb62c6b7..5c2ff296ebee 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderItemsOrderedSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderItemsOrderedSection.xml @@ -23,8 +23,10 @@ <element name="productNameColumn" type="text" selector=".edit-order-table .col-product .product-title"/> <element name="productNameOptions" type="text" selector=".edit-order-table .col-product .item-options"/> + <element name="productName" type="text" selector="#order-items_grid span[id*=order_item]"/> <element name="productNameOptionsLink" type="text" selector="//table[contains(@class, 'edit-order-table')]//td[contains(@class, 'col-product')]//a[text() = '{{var1}}']" parameterized="true"/> <element name="productSkuColumn" type="text" selector=".edit-order-table .col-product .product-sku-block"/> + <element name="productTotal" type="text" selector="#order-items_grid .col-total"/> <element name="statusColumn" type="text" selector=".edit-order-table .col-status"/> <element name="originalPriceColumn" type="text" selector=".edit-order-table .col-original-price .price"/> <element name="priceColumn" type="text" selector=".edit-order-table .col-price .price"/> @@ -35,4 +37,4 @@ <element name="discountAmountColumn" type="text" selector=".edit-order-table .col-discont .price"/> <element name="totalColumn" type="text" selector=".edit-order-table .col-total .price"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderStatusFormSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderStatusFormSection.xml new file mode 100644 index 000000000000..1058b2d6f217 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderStatusFormSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminOrderStatusFormSection"> + <element name="statusCodeField" type="text" selector="#edit_form [name=status]"/> + <element name="statusLabelField" type="text" selector="#edit_form [name=label]"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderStatusGridSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderStatusGridSection.xml new file mode 100644 index 000000000000..b62463928118 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderStatusGridSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminOrderStatusGridSection"> + <element name="statusCodeFilterField" type="input" selector="[data-role=filter-form] [name=status]"/> + <element name="statusCodeDataColumn" type="input" selector="[data-role=row] [data-column=status]"/> + <element name="statusLabelDataColumn" type="input" selector="[data-role=row] [data-column=label]"/> + </section> +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderStoreScopeTreeSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderStoreScopeTreeSection.xml index 050e1ba8b2df..cbe17499319f 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderStoreScopeTreeSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderStoreScopeTreeSection.xml @@ -11,5 +11,6 @@ <section name="AdminOrderStoreScopeTreeSection"> <element name="storeTree" type="text" selector="div.tree-store-scope"/> <element name="storeOption" type="radio" selector="//div[contains(@class, 'tree-store-scope')]//label[contains(text(), '{{name}}')]/preceding-sibling::input" parameterized="true" timeout="30"/> + <element name="storeForOrder" type="radio" selector="//div[contains(@class, 'tree-store-scope')]//label[contains(text(), '{{arg}}')]" parameterized="true" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml index 7ece18fb863b..53a6cbffcdac 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml @@ -29,5 +29,7 @@ <element name="viewBookmark" type="button" selector="//div[contains(@class, 'admin__data-grid-action-bookmarks')]/ul/li/div/a[text() = '{{label}}']" parameterized="true" timeout="30"/> <element name="columnsDropdown" type="button" selector="div.admin__data-grid-action-columns button" timeout="30"/> <element name="viewColumnCheckbox" type="checkbox" selector="//div[contains(@class,'admin__data-grid-action-columns')]//div[contains(@class, 'admin__field-option')]//label[text() = '{{column}}']/preceding-sibling::input" parameterized="true"/> + <element name="customerInOrdersSection" type="button" selector="(//td[contains(text(),'{{customer}}')])[1]" parameterized="true"/> + <element name="productForOrder" type="button" selector="//td[contains(text(),'{{var}}')]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/ConfigurationListSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/ConfigurationListSection.xml new file mode 100644 index 000000000000..bce5f95cf78a --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/ConfigurationListSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="ConfigurationListSection"> + <element name="sales" type="button" selector="//div[contains(@class, 'admin__page-nav-title title _collapsible')]/strong[text()='Sales']"/> + <element name="salesPaymentMethods" type="button" selector="//span[text()='Payment Methods']"/> + </section> +</sections> diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/NewOrderSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/NewOrderSection.xml similarity index 89% rename from app/code/Magento/Braintree/Test/Mftf/Section/NewOrderSection.xml rename to app/code/Magento/Sales/Test/Mftf/Section/NewOrderSection.xml index 13f59ad2cf18..26e00bf4c0aa 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Section/NewOrderSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/NewOrderSection.xml @@ -5,7 +5,9 @@ * See COPYING.txt for license details. */ --> -<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="NewOrderSection"> <element name="createNewOrder" type="button" selector="#add"/> <element name="customer" type="button" selector="//td[contains(text(), 'Abgar')]"/> @@ -30,6 +32,5 @@ <element name="cvv" type="input" selector="#cvv"/> <element name="submitOrder" type="input" selector="#submit_order_top_button"/> <element name="successMessage" type="input" selector="#messages"/> - </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/OrdersGridSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/OrdersGridSection.xml index 55daae769204..b716047a3900 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/OrdersGridSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/OrdersGridSection.xml @@ -16,5 +16,19 @@ <element name="submitSearch22" type="button" selector=".//*[@class="admin__data-grid-filters-wrap"]/parent::*/div[@class="data-grid-search-control-wrap"]/button"/> <element name="firstRow" type="button" selector="//*[@id='container']//tr[@class='data-row']//a[@class='action-menu-item']"/> <element name="createNewOrder" type="button" selector="button[title='Create New Order'"/> + + <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')]/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"/> + <element name="customQuantity" type="input" selector="//span[text()='{{arg}}']/parent::td/following-sibling::td[@class='col-qty']/input" parameterized="true"/> + <element name="update" type="button" selector="//span[text()='Update Items and Quantities']"/> + <element name="discount" type="text" selector="//span[text()='{{arg}}']/parent::td/following-sibling::td[@class='col-discount col-price']/span" parameterized="true"/> + <element name="productPrice" type="text" selector="//span[text()='{{arg}}']/parent::td/following-sibling::td[@class='col-price col-row-subtotal']/span" parameterized="true"/> + <element name="removeItems" type="select" selector="//span[text()='{{arg}}']/parent::td/following-sibling::td/select[@class='admin__control-select']" parameterized="true"/> + <element name="applyCoupon" type="input" selector="#coupons:code"/> + <element name="submitOrder" type="button" selector="#submit_order_top_button"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/SalesOrderPrintSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/SalesOrderPrintSection.xml new file mode 100644 index 000000000000..b08a66140fab --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/SalesOrderPrintSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="SalesOrderPrintSection"> + <element name="isOrderPrintPage" type="block" selector=".preview-area"/> + </section> +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderSearchSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderSearchSection.xml new file mode 100644 index 000000000000..5e420ee03bf7 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderSearchSection.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="StorefrontGuestOrderSearchSection"> + <element name="orderId" type="input" selector="#oar-order-id"/> + <element name="billingLastName" type="input" selector="#oar-billing-lastname"/> + <element name="findOrderBy" type="select" selector="#quick-search-type-id"/> + <element name="email" type="input" selector="#oar_email"/> + <element name="continue" type="button" selector="//*/span[contains(text(), 'Continue')]"/> + </section> +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderViewSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderViewSection.xml new file mode 100644 index 000000000000..11080761b192 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderViewSection.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="StorefrontGuestOrderViewSection"> + <element name="orderInformationTab" type="text" selector="//*[@class='nav item current']/strong[contains(text(), 'Order Information')]"/> + </section> +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/StorefrontOrderAndReturnInformationSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontOrderAndReturnInformationSection.xml new file mode 100644 index 000000000000..aa57dd9bc17b --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontOrderAndReturnInformationSection.xml @@ -0,0 +1,20 @@ +<?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="StorefrontOrderAndReturnInformationSection"> + <element name="orderId" type="input" selector="#oar-order-id"/> + <element name="bilingLastName" type="input" selector="#oar-billing-lastname"/> + <element name="findOrderBy" type="select" selector="#quick-search-type-id"/> + <element name="email" type="input" selector="#oar_email"/> + <element name="bilingZipCode" type="input" selector="//input[@id='oar_zip']"/> + <element name="continueButton" type="submit" selector="//button[@title='Continue']"/> + <element name="ordersAndReturnsTitle" type="span" selector="//span[@id='page-title-wrapper']"/> + </section> +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/StorefrontOrderInformationMainSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontOrderInformationMainSection.xml new file mode 100644 index 000000000000..e42c30120615 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontOrderInformationMainSection.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="StorefrontOrderInformationMainSection"> + <element name="orderTitle" type="span" selector="#page-title-wrapper"/> + <element name="return" type="span" selector="//span[contains(text(), 'Return')]"/> + </section> +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml new file mode 100644 index 000000000000..e405173429b2 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.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="AdminAvailabilityCreditMemoWithNoPaymentTest"> + <annotations> + <features value="Sales"/> + <stories value="MAGETWO-91547: Unable to create Credit memo for order with no payment required"/> + <title value="Checking availability of 'Credit memo' button for order with no payment required"/> + <description value="*Credit Memo* button should be displayed"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-94470"/> + <group value="sales"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Enable *Free Shipping* --> + <createData entity="FreeShippingMethodsSettingConfig" stepKey="freeShippingMethodsSettingConfig"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!-- Disable *Free Shipping* --> + <createData entity="DefaultShippingMethodsConfig" stepKey="defaultShippingMethodsConfig"/> + <createData entity="DisableFreeShippingConfig" stepKey="disableFreeShippingConfig"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> + <argument name="customerEmail" value="Simple_US_Customer.email"/> + </actionGroup> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="logout" stepKey="logOut"/> + </after> + + <!--Proceed to Admin panel > SALES > Orders. Created order should be in Processing status--> + <actionGroup ref="navigateToNewOrderPageNewCustomer" stepKey="navigateToNewOrderPage"/> + + <!--Check if order can be submitted without the required fields including email address--> + <scrollToTopOfPage stepKey="scrollToTopOfOrderFormPage"/> + <actionGroup ref="addSimpleProductToOrder" stepKey="addFirstProductToOrder"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + + <!--Click *Custom Price* link, enter 0 and click *Update Items and Quantities* button--> + <click selector="{{AdminOrderFormItemsSection.customPriceCheckbox}}" stepKey="clickCustomPriceCheckbox"/> + <waitForElementVisible selector="{{AdminOrderFormItemsSection.customPriceField}}" stepKey="waitForPriceFieldAppears"/> + <fillField selector="{{AdminOrderFormItemsSection.customPriceField}}" userInput="0" stepKey="fillCustomPriceField"/> + <click selector="{{AdminOrderFormItemsSection.updateItemsAndQuantities}}" stepKey="clickUpdateItemsAndQuantitiesButton"/> + + <!--Fill customer group and customer email--> + <selectOption selector="{{AdminOrderFormAccountSection.group}}" userInput="{{GeneralCustomerGroup.code}}" stepKey="selectCustomerGroup"/> + <fillField selector="{{AdminOrderFormAccountSection.email}}" userInput="{{Simple_US_Customer.email}}" stepKey="fillCustomerEmail"/> + + <!--Fill customer address information--> + <actionGroup ref="fillOrderCustomerInformation" stepKey="fillCustomerAddress"> + <argument name="customer" value="Simple_US_Customer"/> + <argument name="address" value="US_Address_TX"/> + </actionGroup> + + <!-- Select Free shipping --> + <actionGroup ref="orderSelectFreeShipping" stepKey="selectFreeShippingOption"/> + + <!--Click *Submit Order* button--> + <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="clickSubmitOrder"/> + + <!--Click *Invoice* button--> + <actionGroup ref="StartCreateInvoiceFromOrderPage" stepKey="startCreateInvoice"/> + <actionGroup ref="SubmitInvoice" stepKey="submitInvoice"/> + + <!--Verify that *Credit Memo* button is displayed--> + <seeElement selector="{{AdminOrderFormItemsSection.creditMemo}}" stepKey="seeCreditMemo"/> + <click selector="{{AdminOrderFormItemsSection.creditMemo}}" stepKey="clickCreditMemoItem"/> + <waitForPageLoad stepKey="waitForCreditMemoPageLoaded"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Memo" stepKey="seeNewMemoPageTitle"/> + <seeInCurrentUrl url="{{AdminCreditMemoNewPage.url}}" stepKey="seeNewMemoUrlOnPage"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminChangeCustomerGroupInNewOrder.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminChangeCustomerGroupInNewOrder.xml new file mode 100644 index 000000000000..85ef563e10db --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminChangeCustomerGroupInNewOrder.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminChangeCustomerGroupInNewOrder"> + <annotations> + <title value="Customer account group cannot be selected while creating a new customer in order"/> + <stories value="MC-15290: Customer account group cannot be selected while creating a new customer in order"/> + <description value="Customer account group cannot be selected while creating a new customer in order"/> + <severity value="MAJOR"/> + <testCaseId value="MC-15290"/> + <useCaseId value="MC-15289"/> + <group value="sales"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <actionGroup ref="navigateToNewOrderPageNewCustomerSingleStore" stepKey="openNewOrder"/> + <selectOption selector="{{AdminOrderFormAccountSection.group}}" userInput="Retailer" stepKey="selectCustomerGroup"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <grabValueFrom selector="{{AdminOrderFormAccountSection.group}}" stepKey="grabGroupValue"/> + <assertEquals stepKey="assertValueIsStillSelected"> + <actualResult type="variable">$grabGroupValue</actualResult> + <expectedResult type="string">3</expectedResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCorrectnessInvoicedItemInBundleProductTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCorrectnessInvoicedItemInBundleProductTest.xml new file mode 100644 index 000000000000..f869841153ae --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCorrectnessInvoicedItemInBundleProductTest.xml @@ -0,0 +1,91 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCorrectnessInvoicedItemInBundleProductTest"> + <annotations> + <features value="Sales"/> + <title value="Check correctness of invoiced items in a Bundle Product"/> + <description value="Check correctness of invoiced items in a Bundle Product"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-11059"/> + <useCaseId value="MC-10969"/> + <group value="sales"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!--Create category and simple product--> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!--Create bundle product--> + <createData entity="ApiBundleProductPriceViewRange" stepKey="createBundleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="DropDownBundleOption" stepKey="bundleOption"> + <requiredEntity createDataKey="createBundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink1"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="bundleOption"/> + <requiredEntity createDataKey="createSimpleProduct"/> + <field key="qty">10</field> + </createData> + </before> + <after> + <!--Delete created data--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createBundleProduct" stepKey="deleteBundleProduct"/> + + <actionGroup ref="logout" stepKey="logOut"/> + </after> + + <!--Complete Bundle product creation--> + <amOnPage url="{{AdminProductEditPage.url($$createBundleProduct.id$$)}}" stepKey="goToProductEditPage"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + + <!--Go to bundle product page--> + <amOnPage url="{{StorefrontProductPage.url($$createCategory.name$$)}}" stepKey="navigateToBundleProductPage"/> + + <!--Place order bundle product with 10 options--> + <actionGroup ref="StorefrontAddCategoryBundleProductToCartActionGroup" stepKey="addBundleProductToCart"> + <argument name="product" value="$$createBundleProduct$$"/> + <argument name="quantity" value="10"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart" /> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShipping"/> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="placeOrder"> + <argument name="orderNumberMessage" value="CONST.successGuestCheckoutOrderNumberMessage" /> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage"/> + </actionGroup> + + <!--Go to order page submit invoice--> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> + <actionGroup ref="filterOrderGridById" stepKey="filterOrderGridById"> + <argument name="orderId" value="$grabOrderNumber"/> + </actionGroup> + <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> + <waitForPageLoad stepKey="waitForCreatedOrderPageOpened"/> + <actionGroup ref="goToInvoiceIntoOrder" stepKey="goToInvoiceIntoOrderPage"/> + <fillField selector="{{AdminInvoiceItemsSection.qtyToInvoiceColumn}}" userInput="5" stepKey="ChangeQtyToInvoice"/> + <click selector="{{AdminInvoiceItemsSection.updateQty}}" stepKey="updateQunatity"/> + <waitForPageLoad stepKey="waitPageToBeLoaded"/> + <actionGroup ref="SubmitInvoice" stepKey="submitInvoice"/> + + <!--Verify invoiced items qty in ship tab--> + <actionGroup ref="goToShipmentIntoOrder" stepKey="goToShipment"/> + <grabTextFrom selector="{{AdminShipmentItemsSection.itemQtyInvoiced('1')}}" stepKey="grabInvoicedItemQty"/> + <assertEquals expected="5" expectedType="string" actual="$grabInvoicedItemQty" stepKey="assertInvoicedItemsQty"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateInvoiceTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateInvoiceTest.xml index 05f20371851b..ce66409ed9b3 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateInvoiceTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateInvoiceTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminCreateInvoiceTest"> <annotations> <features value="Sales"/> @@ -52,6 +52,8 @@ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask2"/> <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton"/> <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> + <!-- Checkout select Check/Money Order payment --> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> @@ -61,6 +63,7 @@ <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask3"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearGridFilter"/> <fillField selector="{{AdminOrdersGridSection.search}}" userInput="{$grabOrderNumber}" stepKey="searchOrderNum"/> <click selector="{{AdminOrdersGridSection.submitSearch}}" stepKey="submitSearch"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask4"/> @@ -78,8 +81,10 @@ <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="Processing" stepKey="seeOrderStatus"/> <amOnPage url="{{AdminInvoicesPage.url}}" stepKey="goToInvoices"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask6" /> + <actionGroup ref="resetProductGridToDefaultView" stepKey="resetGridInitial"/> <click selector="{{AdminInvoicesGridSection.filter}}" stepKey="clickFilters"/> <fillField selector="{{AdminInvoicesFiltersSection.orderNum}}" userInput="{$grabOrderNumber}" stepKey="searchOrderNum2"/> + <click selector="{{AdminInvoicesFiltersSection.applyFilters}}" stepKey="clickApplyFilters"/> <click selector="{{AdminInvoicesGridSection.firstRow}}" stepKey="clickInvoice2"/> <see selector="{{AdminInvoiceDetailsInformationSection.orderStatus}}" userInput="Processing" stepKey="seeOrderStatus2"/> </test> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderStatusDuplicatingCodeTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderStatusDuplicatingCodeTest.xml new file mode 100644 index 000000000000..40a731410a89 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderStatusDuplicatingCodeTest.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateOrderStatusDuplicatingCodeTest"> + <annotations> + <stories value="Create order status"/> + <title value="Create order status with duplicating code"/> + <description value="Receive error when creating order status with the code which is already exist"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-15432" /> + <group value="sales"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to new order status page --> + <amOnPage url="{{AdminOrderStatusPage.url}}" stepKey="goToOrderStatusPage"/> + <click selector="{{AdminMainActionsSection.add}}" stepKey="clickCreateNewStatus"/> + + <!-- Fill the form and validate message --> + <actionGroup ref="AdminOrderStatusFormFillAndSave" stepKey="fillFormAndClickSave"> + <argument name="status" value="{{duplicatingCodeOrderStatus.status}}"/> + <argument name="label" value="{{duplicatingCodeOrderStatus.label}}"/> + </actionGroup> + <actionGroup ref="AssertOrderStatusFormSaveDuplicateError" stepKey="seeFormSaveDuplicateError"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderStatusDuplicatingLabelTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderStatusDuplicatingLabelTest.xml new file mode 100644 index 000000000000..d1381bbb1efb --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderStatusDuplicatingLabelTest.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateOrderStatusDuplicatingLabelTest"> + <annotations> + <stories value="Create order status"/> + <title value="Create order status with duplicating label"/> + <description value="Create an order status and get success message"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-15433" /> + <group value="sales"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to new order status page --> + <amOnPage url="{{AdminOrderStatusPage.url}}" stepKey="goToOrderStatusPage"/> + <click selector="{{AdminMainActionsSection.add}}" stepKey="clickCreateNewStatus"/> + + <!-- Fill the form and validate message --> + <actionGroup ref="AdminOrderStatusFormFillAndSave" stepKey="fillFormAndClickSave"> + <argument name="status" value="{{duplicatingLabelOrderStatus.status}}"/> + <argument name="label" value="{{duplicatingLabelOrderStatus.label}}"/> + </actionGroup> + <actionGroup ref="AssertOrderStatusFormSaveSuccess" stepKey="seeFormSaveSuccess"/> + + <!-- Verify the order status grid page shows the order status we just created --> + <actionGroup ref="AssertOrderStatusExistsInGrid" stepKey="searchCreatedOrderStatus"> + <argument name="status" value="{{duplicatingLabelOrderStatus.status}}"/> + <argument name="label" value="{{duplicatingLabelOrderStatus.label}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderStatusTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderStatusTest.xml new file mode 100644 index 000000000000..c2daaac84dd4 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderStatusTest.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateOrderStatusTest"> + <annotations> + <stories value="Create custom order status"/> + <title value="Create custom order status"/> + <description value="Tests opening admin order status page, create a new order status with success message"/> + <testCaseId value="MC-15431" /> + <severity value="AVERAGE"/> + <group value="sales"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to new order status page --> + <amOnPage url="{{AdminOrderStatusPage.url}}" stepKey="goToOrderStatusPage"/> + <click selector="{{AdminMainActionsSection.add}}" stepKey="clickCreateNewStatus"/> + + <!-- Fill the form and validate message --> + <actionGroup ref="AdminOrderStatusFormFillAndSave" stepKey="fillFormAndClickSave"> + <argument name="status" value="{{defaultOrderStatus.status}}"/> + <argument name="label" value="{{defaultOrderStatus.label}}"/> + </actionGroup> + <actionGroup ref="AssertOrderStatusFormSaveSuccess" stepKey="seeFormSaveSuccess"/> + + <!-- Verify the order status grid page shows the order status we just created --> + <actionGroup ref="AssertOrderStatusExistsInGrid" stepKey="searchCreatedOrderStatus"> + <argument name="status" value="{{defaultOrderStatus.status}}"/> + <argument name="label" value="{{defaultOrderStatus.label}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithBundleProductTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithBundleProductTest.xml new file mode 100644 index 000000000000..d087b291de87 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithBundleProductTest.xml @@ -0,0 +1,112 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateOrderWithBundleProductTest"> + <annotations> + <title value="Create Order in Admin and update bundle product configuration"/> + <stories value="MAGETWO-96488: Wrong price calculation for bundle product on creating order from the admin panel"/> + <description value="Add bundle product with bundle option items with default quantity 2 to order in Admin and check price in product grid"/> + <features value="Sales"/> + <severity value="AVERAGE"/> + <group value="Sales"/> + </annotations> + + <before> + <!--Set default flat rate shipping method settings--> + <createData entity="FlatRateShippingMethodDefault" stepKey="setDefaultFlatRateShippingMethod"/> + + <!--Create simple customer--> + <createData entity="Simple_US_Customer_CA" stepKey="simpleCustomer"/> + + <!--Create simple product 1--> + <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> + + <!--Create simple product 2--> + <createData entity="ApiProductWithDescription" stepKey="simple2" before="product"/> + + <!--Create bundle product with checkbox bundle option--> + <createData entity="ApiBundleProduct" stepKey="product"/> + <createData entity="CheckboxOption" stepKey="checkboxBundleOption"> + <requiredEntity createDataKey="product"/> + </createData> + + <!--Link simple product 1 to bundle option with default quantity 2--> + <createData entity="ApiBundleLink" stepKey="createBundleLink1"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="checkboxBundleOption"/> + <requiredEntity createDataKey="simple1"/> + <field key="qty">2</field> + <field key="is_default">1</field> + </createData> + + <!--Link simple product 2 to bundle option with default quantity 2--> + <createData entity="ApiBundleLink" stepKey="createBundleLink2"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="checkboxBundleOption"/> + <requiredEntity createDataKey="simple2"/> + <field key="qty">2</field> + <field key="is_default">1</field> + </createData> + + <!--Add drop-down bundle option--> + <createData entity="DropDownBundleOption" stepKey="dropDownBundleOption"> + <requiredEntity createDataKey="product"/> + </createData> + + <!--Link simple product 1 to drop-down bundle option with default quantity 2--> + <createData entity="ApiBundleLink" stepKey="createBundleLink3"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="dropDownBundleOption"/> + <requiredEntity createDataKey="simple1"/> + <field key="qty">2</field> + <field key="is_default">1</field> + </createData> + + <!--Link simple product 2 to drop-down bundle option with default quantity 2--> + <createData entity="ApiBundleLink" stepKey="createBundleLink4"> + <requiredEntity createDataKey="product"/> + <requiredEntity createDataKey="dropDownBundleOption"/> + <requiredEntity createDataKey="simple2"/> + <field key="qty">2</field> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <!--Create new customer order--> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="navigateToNewOrderWithExistingCustomer"> + <argument name="customer" value="$$simpleCustomer$$"/> + </actionGroup> + + <!--Add bundle product to order and check product price in grid--> + <actionGroup ref="addBundleProductToOrderAndCheckPriceInGrid" stepKey="addBundleProductToOrder"> + <argument name="product" value="$$product$$"/> + <argument name="quantity" value="1"/> + <argument name="price" value="$738.00"/> + </actionGroup> + + <!--Select FlatRate shipping method--> + <actionGroup ref="orderSelectFlatRateShipping" stepKey="orderSelectFlatRateShippingMethod"/> + + <!--Submit order--> + <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="submitOrder"/> + + <!--Verify order information--> + <actionGroup ref="verifyCreatedOrderInformation" stepKey="verifyCreatedOrderInformation"/> + + <after> + <actionGroup ref="logout" stepKey="logout"/> + + <deleteData createDataKey="product" stepKey="delete"/> + <deleteData createDataKey="simpleCustomer" stepKey="deleteSimpleCustomer"/> + <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> + <deleteData createDataKey="simple2" stepKey="deleteSimple2" before="delete"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithMinimumAmountEnabledTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithMinimumAmountEnabledTest.xml index 0b737ed459f3..40b26a6b4604 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithMinimumAmountEnabledTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithMinimumAmountEnabledTest.xml @@ -38,7 +38,8 @@ <!--Admin creates order--> <comment userInput="Admin creates order" stepKey="adminCreateOrder"/> <actionGroup ref="navigateToNewOrderPageNewCustomerSingleStore" stepKey="navigateToNewOrderPage"/> - + <conditionalClick selector="{{AdminOrderFormStoreSelectorSection.defaultStoreViewButton}}" dependentSelector="{{AdminOrderFormStoreSelectorSection.storeSelectorContainer}}" visible="true" stepKey="selectFirstStoreViewIfAppears"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappearedAfterStoreSelected"/> <actionGroup ref="addSimpleProductToOrder" stepKey="addSimpleProductToOrder"> <argument name="product" value="SimpleProduct"/> </actionGroup> @@ -55,6 +56,9 @@ <actionGroup ref="orderSelectFlatRateShipping" stepKey="selectFlatRateShipping"/> + <!-- Checkout select Check/Money Order payment --> + <actionGroup ref="SelectCheckMoneyPaymentMethod" stepKey="selectCheckMoneyPayment"/> + <!--Verify totals on Order page--> <see selector="{{AdminOrderFormTotalSection.total('Subtotal')}}" userInput="${{AdminOrderSimpleProduct.subtotal}}" stepKey="seeOrderSubTotal"/> <see selector="{{AdminOrderFormTotalSection.total('Shipping')}}" userInput="${{AdminOrderSimpleProduct.shipping}}" stepKey="seeOrderShipping"/> @@ -77,4 +81,4 @@ <argument name="product" value="SimpleProduct"/> </actionGroup> </test> -</tests> \ No newline at end of file +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminFreeShippingNotAvailableIfMinimumOrderAmountNotMatchOrderTotalTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminFreeShippingNotAvailableIfMinimumOrderAmountNotMatchOrderTotalTest.xml new file mode 100644 index 000000000000..5f6ea0937b52 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminFreeShippingNotAvailableIfMinimumOrderAmountNotMatchOrderTotalTest.xml @@ -0,0 +1,63 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminFreeShippingNotAvailableIfMinimumOrderAmountNotMatchOrderTotalTest"> + <annotations> + <features value="Sales"/> + <stories value="Admin create order"/> + <title value="Free Shipping is not available in Admin if Minimum Order Amount does not match Order total"/> + <description value="Admin should not be able place order with Free Shipping method if Minimum Order Amount does not match Order total"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-61001"/> + <group value="sales"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">100</field> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <createData entity="DisableFlatRateShippingMethodConfig" stepKey="disableFlatRate"/> + <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShippingMethod"/> + <createData entity="setFreeShippingSubtotal" stepKey="setFreeShippingSubtotal"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <createData entity="FlatRateShippingMethodConfig" stepKey="enableFlatRate"/> + <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShippingMethod"/> + <createData entity="setFreeShippingSubtotalToDefault" stepKey="setFreeShippingSubtotalToDefault"/> + <actionGroup ref="logout" stepKey="logout"/> + <magentoCLI command="cache:flush" stepKey="flushCache2"/> + </after> + <!--Create new order with existing customer--> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="goToCreateOrderPage"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + <!--Add product to order--> + <actionGroup ref="addSimpleProductToOrder" stepKey="addProductToOrder"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + + <click selector="{{AdminOrderFormPaymentSection.header}}" stepKey="unfocus"/> + <waitForPageLoad stepKey="waitForJavascriptToFinish"/> + <!--Click *Get shipping methods and rates* and see that Free Shipping is absent--> + <click selector="{{AdminOrderFormPaymentSection.getShippingMethods}}" stepKey="clickGetShippingMehods"/> + <dontSeeElement selector="{{AdminOrderFormPaymentSection.freeShippingOption}}" stepKey="seeAbsentFreeShipping"/> + <!--Submit Order and verify that Order isn't placed--> + <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="clickSubmitOrder"/> + + <dontSeeElement selector="{{AdminMessagesSection.successMessage}}" stepKey="seeSuccessMessage"/> + <seeElement selector="{{AdminMessagesSection.errorMessage}}" stepKey="seeErrorMessage"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesCreditMemosNavigateMenuTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesCreditMemosNavigateMenuTest.xml new file mode 100644 index 000000000000..af7cc1822d21 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesCreditMemosNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminSalesCreditMemosNavigateMenuTest"> + <annotations> + <features value="Sales"/> + <stories value="Menu Navigation"/> + <title value="Admin sales credit memos navigate menu test"/> + <description value="Admin should be able to navigate to Sales > Credit Memos"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14140"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToSalesCreditMemosPage"> + <argument name="menuUiId" value="{{AdminMenuSales.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSalesCreditMemos.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSalesCreditMemos.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesInvoicesNavigateMenuTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesInvoicesNavigateMenuTest.xml new file mode 100644 index 000000000000..5a38a66d1f4b --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesInvoicesNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminSalesInvoicesNavigateMenuTest"> + <annotations> + <features value="Sales"/> + <stories value="Menu Navigation"/> + <title value="Admin sales invoices navigate menu test"/> + <description value="Admin should be able to navigate to Sales > Invoices"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14138"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToSalesInvoicesPage"> + <argument name="menuUiId" value="{{AdminMenuSales.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSalesInvoices.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSalesInvoices.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesOrdersNavigateMenuTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesOrdersNavigateMenuTest.xml new file mode 100644 index 000000000000..8099254923a2 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesOrdersNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminSalesOrdersNavigateMenuTest"> + <annotations> + <features value="Sales"/> + <stories value="Menu Navigation"/> + <title value="Admin sales orders navigate menu test"/> + <description value="Admin should be able to navigate to Sales > Orders"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14137"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToSalesOrderPage"> + <argument name="menuUiId" value="{{AdminMenuSales.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSalesOrders.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSalesOrders.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesShipmentsNavigateMenuTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesShipmentsNavigateMenuTest.xml new file mode 100644 index 000000000000..5717c6c90fc1 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesShipmentsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminSalesShipmentsNavigateMenuTest"> + <annotations> + <features value="Sales"/> + <stories value="Menu Navigation"/> + <title value="Admin sales shipments navigate menu test"/> + <description value="Admin should be able to navigate to Sales > Shipments"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14139"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToSalesShipmentsPage"> + <argument name="menuUiId" value="{{AdminMenuSales.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSalesShipments.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSalesShipments.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesTransactionsNavigateMenuTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesTransactionsNavigateMenuTest.xml new file mode 100644 index 000000000000..68933be92efe --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesTransactionsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminSalesTransactionsNavigateMenuTest"> + <annotations> + <features value="Sales"/> + <stories value="Menu Navigation"/> + <title value="Admin sales transactions navigate menu test"/> + <description value="Admin should be able to navigate to Sales > Transactions"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14141"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToSalesTransactionsPage"> + <argument name="menuUiId" value="{{AdminMenuSales.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSalesTransactions.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSalesTransactions.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminStoresOrderStatusNavigateMenuTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminStoresOrderStatusNavigateMenuTest.xml new file mode 100644 index 000000000000..d55cde144903 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminStoresOrderStatusNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminStoresOrderStatusNavigateMenuTest"> + <annotations> + <features value="Sales"/> + <stories value="Menu Navigation"/> + <title value="Admin stores order status navigate menu test"/> + <description value="Admin should be able to navigate to Stores > Order Status"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14142"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToStoresOrderStatusPage"> + <argument name="menuUiId" value="{{AdminMenuStores.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuStoresSettingsOrderStatus.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuStoresSettingsOrderStatus.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitConfigurableProductOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitConfigurableProductOrderTest.xml index e32e6b9e6ec5..63607e59c41b 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitConfigurableProductOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitConfigurableProductOrderTest.xml @@ -1,129 +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="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> - <test name="AdminSubmitConfigurableProductOrderTest"> - <annotations> - <title value="Create Order in Admin and update product configuration"/> - <stories value="MAGETWO-59632: Create Sales > Order from admin add configurable product and change options click OK does not update Items Ordered List"/> - <description value="Create Order in Admin and update product configuration"/> - <features value="Sales"/> - <severity value="AVERAGE"/> - <testCaseId value="MAGETWO-59633"/> - <group value="Sales"/> - </annotations> - - <before> - <!--Set default flat rate shipping method settings--> - <createData entity="FlatRateShippingMethodDefault" stepKey="setDefaultFlatRateShippingMethod"/> - - <!--Create simple customer--> - <createData entity="Simple_US_Customer_CA" stepKey="simpleCustomer"/> - - <!-- Create the category --> - <createData entity="ApiCategory" stepKey="createCategory"/> - - <!-- Create the configurable product and add it to the category --> - <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> - <requiredEntity createDataKey="createCategory"/> - </createData> - - <!-- Create an attribute with two options to be used in the first child product --> - <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> - <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> - <requiredEntity createDataKey="createConfigProductAttribute"/> - </createData> - <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> - <requiredEntity createDataKey="createConfigProductAttribute"/> - </createData> - - <!-- Add the attribute we just created to default attribute set --> - <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> - <requiredEntity createDataKey="createConfigProductAttribute"/> - </createData> - - <!-- Get the option of the attribute we created --> - <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> - <requiredEntity createDataKey="createConfigProductAttribute"/> - </getData> - <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> - <requiredEntity createDataKey="createConfigProductAttribute"/> - </getData> - - <!-- Create a simple product and give it the attribute with option --> - <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> - <requiredEntity createDataKey="createConfigProductAttribute"/> - <requiredEntity createDataKey="getConfigAttributeOption1"/> - </createData> - <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 simple product to the configurable product --> - <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> - <requiredEntity createDataKey="createConfigProduct"/> - <requiredEntity createDataKey="createConfigChildProduct1"/> - </createData> - <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> - <requiredEntity createDataKey="createConfigProduct"/> - <requiredEntity createDataKey="createConfigChildProduct2"/> - </createData> - - <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> - </before> - - <!--Create new customer order--> - <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="navigateToNewOrderWithExistingCustomer"> - <argument name="customer" value="$$simpleCustomer$$"/> - </actionGroup> - - <!--Add configurable product to order--> - <actionGroup ref="addConfigurableProductToOrderFromAdmin" stepKey="addConfigurableProductToOrder"> - <argument name="product" value="$$createConfigProduct$$"/> - <argument name="attribute" value="$$createConfigProductAttribute$$"/> - <argument name="option" value="$$getConfigAttributeOption1$$"/> - </actionGroup> - - <!--Configure ordered configurable product--> - <actionGroup ref="configureOrderedConfigurableProduct" stepKey="configureOrderedConfigurableProduct"> - <argument name="attribute" value="$$createConfigProductAttribute$$"/> - <argument name="option" value="$$getConfigAttributeOption2$$"/> - <argument name="quantity" value="2"/> - </actionGroup> - - <!--Select FlatRate shipping method--> - <actionGroup ref="orderSelectFlatRateShipping" stepKey="orderSelectFlatRateShippingMethod"/> - - <!--Submit order--> - <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="submitOrder"/> - - <!--Verify order information--> - <actionGroup ref="verifyCreatedOrderInformation" stepKey="verifyCreatedOrderInformation"/> - - <after> - <actionGroup ref="logout" stepKey="logout"/> - - <deleteData createDataKey="simpleCustomer" stepKey="deleteSimpleCustomer"/> - - <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> - <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> - <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> - <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> - <deleteData createDataKey="createCategory" stepKey="deleteApiCategory"/> - </after> - </test> -</tests> \ No newline at end of file +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminSubmitConfigurableProductOrderTest"> + <annotations> + <title value="Create Order in Admin and update product configuration"/> + <stories value="MAGETWO-59632: Create Sales > Order from admin add configurable product and change options click OK does not update Items Ordered List"/> + <description value="Create Order in Admin and update product configuration"/> + <features value="Sales"/> + <severity value="AVERAGE"/> + <testCaseId value="MAGETWO-59633"/> + <group value="Sales"/> + <skip> + <issueId value="MAGETWO-96196"/> + </skip> + </annotations> + + <before> + <!--Set default flat rate shipping method settings--> + <createData entity="FlatRateShippingMethodDefault" stepKey="setDefaultFlatRateShippingMethod"/> + + <!--Create simple customer--> + <createData entity="Simple_US_Customer_CA" stepKey="simpleCustomer"/> + + <!-- Create the category --> + <createData entity="ApiCategory" stepKey="createCategory"/> + + <!-- Create the configurable product and add it to the category --> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Create an attribute with two options to be used in the first child product --> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + + <!-- Add the attribute we just created to default attribute set --> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + + <!-- Get the option of the attribute we created --> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create a simple product and give it the attribute with option --> + <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <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 simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <!--Create new customer order--> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="navigateToNewOrderWithExistingCustomer"> + <argument name="customer" value="$$simpleCustomer$$"/> + </actionGroup> + + <!--Add configurable product to order--> + <actionGroup ref="addConfigurableProductToOrderFromAdmin" stepKey="addConfigurableProductToOrder"> + <argument name="product" value="$$createConfigProduct$$"/> + <argument name="attribute" value="$$createConfigProductAttribute$$"/> + <argument name="option" value="$$getConfigAttributeOption1$$"/> + </actionGroup> + + <!--Configure ordered configurable product--> + <actionGroup ref="configureOrderedConfigurableProduct" stepKey="configureOrderedConfigurableProduct"> + <argument name="attribute" value="$$createConfigProductAttribute$$"/> + <argument name="option" value="$$getConfigAttributeOption2$$"/> + <argument name="quantity" value="2"/> + </actionGroup> + + <!--Select FlatRate shipping method--> + <actionGroup ref="orderSelectFlatRateShipping" stepKey="orderSelectFlatRateShippingMethod"/> + + <!-- Checkout select Check/Money Order payment --> + <actionGroup ref="SelectCheckMoneyPaymentMethod" stepKey="selectCheckMoneyPayment"/> + + <!--Submit order--> + <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="submitOrder"/> + + <!--Verify order information--> + <actionGroup ref="verifyCreatedOrderInformation" stepKey="verifyCreatedOrderInformation"/> + + <after> + <actionGroup ref="logout" stepKey="logout"/> + + <deleteData createDataKey="simpleCustomer" stepKey="deleteSimpleCustomer"/> + + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + <deleteData createDataKey="createCategory" stepKey="deleteApiCategory"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderPaymentMethodValidationTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderPaymentMethodValidationTest.xml new file mode 100644 index 000000000000..e487c62b9672 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderPaymentMethodValidationTest.xml @@ -0,0 +1,77 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminSubmitsOrderPaymentMethodValidationTest"> + <annotations> + <features value="Sales"/> + <stories value="MC-5537: No UI validation for Payment methods when creating an order from admin"/> + <title value="UI validation for Payment methods when creating an order from admin"/> + <description value="Admin should not be able to submit orders without selecting a payment method when there is more than one"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-6029"/> + <group value="sales"/> + </annotations> + <before> + <magentoCLI stepKey="allowSpecificValue" command="config:set payment/cashondelivery/active 1" /> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <magentoCLI stepKey="allowSpecificValue" command="config:set payment/cashondelivery/active 0" /> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + </after> + <!--Create order via Admin--> + <comment userInput="Admin creates order" stepKey="adminCreateOrderComment"/> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="navigateToOrderIndexPage"/> + <waitForPageLoad stepKey="waitForIndexPageLoad"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Orders" stepKey="seeIndexPageTitle"/> + <click selector="{{AdminOrdersGridSection.createNewOrder}}" stepKey="clickCreateNewOrder"/> + <click selector="{{AdminOrderFormActionSection.CreateNewCustomer}}" stepKey="clickCreateCustomer"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Create New Order" stepKey="seeNewOrderPageTitle"/> + + <!--Check if order can be submitted without the required fields--> + <actionGroup ref="addSimpleProductToOrder" stepKey="addSimpleProductToOrder" after="seeNewOrderPageTitle"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <actionGroup ref="checkRequiredFieldsNewOrderForm" stepKey="checkRequiredFieldsNewOrder" after="addSimpleProductToOrder"/> + <see selector="{{AdminOrderFormPaymentSection.paymentError}}" userInput="Please select one of the options." stepKey="seePaymentMethodRequired" after="checkRequiredFieldsNewOrder"/> + <scrollToTopOfPage stepKey="scrollToTopOfOrderFormPage" after="seePaymentMethodRequired"/> + + <!--Fill customer group and customer email--> + <selectOption selector="{{AdminOrderFormAccountSection.group}}" userInput="{{GeneralCustomerGroup.code}}" stepKey="selectCustomerGroup" after="scrollToTopOfOrderFormPage"/> + <fillField selector="{{AdminOrderFormAccountSection.email}}" userInput="{{Simple_US_Customer.email}}" stepKey="fillCustomerEmail" after="selectCustomerGroup"/> + + <!--Fill customer address information--> + <actionGroup ref="fillOrderCustomerInformation" stepKey="fillCustomerAddress" after="fillCustomerEmail"> + <argument name="customer" value="Simple_US_Customer"/> + <argument name="address" value="US_Address_TX"/> + </actionGroup> + + <!-- Select payment and shipping --> + <waitForElementVisible selector="{{AdminOrderFormPaymentSection.checkMoneyOption}}" stepKey="waitForPaymentOptions"/> + <selectOption selector="{{AdminOrderFormPaymentSection.checkMoneyOption}}" userInput="checkmo" stepKey="checkPaymentOption"/> + <actionGroup ref="orderSelectFlatRateShipping" stepKey="selectFlatRateShipping" after="fillCustomerAddress"/> + + <!--Verify totals on Order page--> + <see selector="{{AdminOrderFormTotalSection.total('Subtotal')}}" userInput="${{AdminOrderSimpleProduct.subtotal}}" stepKey="seeOrderSubTotal" after="selectFlatRateShipping"/> + <see selector="{{AdminOrderFormTotalSection.total('Shipping')}}" userInput="${{AdminOrderSimpleProduct.shipping}}" stepKey="seeOrderShipping" after="seeOrderSubTotal"/> + <scrollTo selector="{{AdminOrderFormTotalSection.grandTotal}}" stepKey="scrollToOrderGrandTotal"/> + <see selector="{{AdminOrderFormTotalSection.grandTotal}}" userInput="${{AdminOrderSimpleProduct.grandTotal}}" stepKey="seeCorrectGrandTotal" after="scrollToOrderGrandTotal"/> + + <!--Submit Order and verify information--> + <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="clickSubmitOrder" after="seeCorrectGrandTotal"/> + <seeInCurrentUrl url="{{AdminOrderDetailsPage.url}}" stepKey="seeViewOrderPage" after="clickSubmitOrder"/> + <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="You created the order." stepKey="seeSuccessMessage" after="seeViewOrderPage"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml index cc69b6dfb7d4..ed536bd3351f 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml @@ -58,6 +58,9 @@ <!-- Select shipping --> <actionGroup ref="orderSelectFlatRateShipping" stepKey="selectFlatRateShipping" after="fillCustomerAddress"/> + <!-- Checkout select Check/Money Order payment --> + <actionGroup ref="SelectCheckMoneyPaymentMethod" stepKey="selectCheckMoneyPayment"/> + <!--Verify totals on Order page--> <see selector="{{AdminOrderFormTotalSection.total('Subtotal')}}" userInput="${{AdminOrderSimpleProduct.subtotal}}" stepKey="seeOrderSubTotal" after="selectFlatRateShipping"/> <see selector="{{AdminOrderFormTotalSection.total('Shipping')}}" userInput="${{AdminOrderSimpleProduct.shipping}}" stepKey="seeOrderShipping" after="seeOrderSubTotal"/> @@ -69,4 +72,4 @@ <seeInCurrentUrl url="{{AdminOrderDetailsPage.url}}" stepKey="seeViewOrderPage" after="clickSubmitOrder"/> <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="You created the order." stepKey="seeSuccessMessage" after="seeViewOrderPage"/> </test> - </tests> \ No newline at end of file + </tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml new file mode 100644 index 000000000000..d418751c736e --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml @@ -0,0 +1,88 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminSubmitsOrderWithAndWithoutFieldsValidationTest"> + <annotations> + <features value="Sales"/> + <stories value="Create orders"/> + <title value="Fields validation is required to create an order from Admin Panel"/> + <description value="Admin should not be able to submit orders without invalid address fields"/> + <group value="sales"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + </after> + <!--Create order via Admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <comment userInput="Admin creates order" stepKey="adminCreateOrderComment"/> + <!--<actionGroup ref="navigateToNewOrderPageNewCustomer" stepKey="navigateToNewOrderPage"/>--> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="navigateToOrderIndexPage"/> + <waitForPageLoad stepKey="waitForIndexPageLoad"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Orders" stepKey="seeIndexPageTitle"/> + <click selector="{{AdminOrdersGridSection.createNewOrder}}" stepKey="clickCreateNewOrder"/> + <click selector="{{AdminOrderFormActionSection.CreateNewCustomer}}" stepKey="clickCreateCustomer"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Create New Order" stepKey="seeNewOrderPageTitle"/> + + <!--Check if order can be submitted without the required fields including email address--> + <actionGroup ref="checkRequiredFieldsNewOrderForm" stepKey="checkRequiredFieldsNewOrder" after="seeNewOrderPageTitle"/> + <scrollToTopOfPage stepKey="scrollToTopOfOrderFormPage" after="checkRequiredFieldsNewOrder"/> + <actionGroup ref="addSimpleProductToOrder" stepKey="addSimpleProductToOrder" after="scrollToTopOfOrderFormPage"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + + <!--Fill customer group and customer email--> + <selectOption selector="{{AdminOrderFormAccountSection.group}}" userInput="{{GeneralCustomerGroup.code}}" stepKey="selectCustomerGroup" after="addSimpleProductToOrder"/> + <fillField selector="{{AdminOrderFormAccountSection.email}}" userInput="{{Simple_US_Customer.email}}" stepKey="fillCustomerEmail" after="selectCustomerGroup"/> + + <!--Fill wrong customer address information--> + <actionGroup ref="fillOrderCustomerInformation" stepKey="fillWrongCustomerAddress" after="fillCustomerEmail"> + <argument name="customer" value="Simple_US_Customer_Incorrect_Name"/> + <argument name="address" value="US_Address_TX"/> + </actionGroup> + <!-- Select shipping --> + <actionGroup ref="orderSelectFlatRateShipping" stepKey="selectFlatRateShipping" + after="fillWrongCustomerAddress"/> + + <!--Verify totals on Order page--> + <see selector="{{AdminOrderFormTotalSection.total('Subtotal')}}" userInput="${{AdminOrderSimpleProduct.subtotal}}" stepKey="seeOrderSubTotal" after="selectFlatRateShipping"/> + <see selector="{{AdminOrderFormTotalSection.total('Shipping')}}" userInput="${{AdminOrderSimpleProduct.shipping}}" stepKey="seeOrderShipping" after="seeOrderSubTotal"/> + <scrollTo selector="{{AdminOrderFormTotalSection.grandTotal}}" stepKey="scrollToOrderGrandTotal"/> + <see selector="{{AdminOrderFormTotalSection.grandTotal}}" userInput="${{AdminOrderSimpleProduct.grandTotal}}" stepKey="seeCorrectGrandTotal" after="scrollToOrderGrandTotal"/> + + <!--Submit Order and verify information--> + <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="clickSubmitOrderWrong" + after="seeCorrectGrandTotal"/> + <see selector="{{AdminOrderFormBillingAddressSection.firstNameError}}" + userInput="Please enter less or equal than 255 symbols." stepKey="firstNameError" + after="clickSubmitOrderWrong"/> + + <!--Fill correct customer address information--> + <actionGroup ref="fillOrderCustomerInformation" stepKey="fillCustomerAddress" after="firstNameError"> + <argument name="customer" value="Simple_US_Customer"/> + <argument name="address" value="US_Address_TX"/> + </actionGroup> + + <!-- Select shipping --> + <click selector="{{AdminOrderFormPaymentSection.getShippingMethods}}" stepKey="clickShipping" after="fillCustomerAddress"/> + <click selector="{{AdminOrderFormPaymentSection.getShippingMethods}}" stepKey="selectShipping" after="clickShipping"/> + + <!--Submit Order and verify information--> + <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="clickSubmitOrder" after="selectShipping"/> + <seeInCurrentUrl url="{{AdminOrderDetailsPage.url}}" stepKey="seeViewOrderPage" after="clickSubmitOrder"/> + <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="You created the order." stepKey="seeSuccessMessage" after="seeViewOrderPage"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml index 60df3f27fd65..dfbdc5367799 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml @@ -45,6 +45,8 @@ <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{ApiSalesRule.name}}" stepKey="fillRuleName"/> <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsite"/> <actionGroup ref="selectNotLoggedInCustomerGroup" stepKey="chooseNotLoggedInCustomerGroup"/> + <generateDate date="-1 day" format="m/d/Y" stepKey="yesterdayDate"/> + <fillField selector="{{AdminCartPriceRulesFormSection.fromDate}}" userInput="{$yesterdayDate}" stepKey="fillFromDate"/> <!-- Open the Actions Tab in the Rules Edit page --> <click selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="clickToExpandActions"/> @@ -83,6 +85,10 @@ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask2"/> <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton"/> <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> + + <!-- Checkout select Check/Money Order payment --> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment3"/> + <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> @@ -118,6 +124,7 @@ <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeSuccessMessage1"/> + <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="Processing" stepKey="seeOrderProcessing"/> <!--Create Credit Memo--> <comment userInput="Admin creates credit memo" stepKey="createCreditMemoComment"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontRedirectToOrderHistory.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontRedirectToOrderHistory.xml new file mode 100644 index 000000000000..ad3a411d9241 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontRedirectToOrderHistory.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="StorefrontRedirectToOrderHistory"> + <annotations> + <features value="Redirection Rules"/> + <stories value="Create Invoice"/> + <title value="Create Invoice"/> + <description + value="Check while order printing URL with an id of not relevant order redirects to order history"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-92854"/> + <group value="sales"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <createData entity="Simple_US_Customer" stepKey="createCustomer2"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCustomer2" stepKey="deleteCustomer2"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + </after> + + <!--Log in to Storefront as Customer 1 --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="signUp"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!--Create an order at Storefront as Customer 1 --> + <actionGroup ref="CreateOrderToPrintPageWithSelectedPaymentMethodActionGroup" stepKey="createOrderToPrint"> + <argument name="Category" value="$$createCategory$$"/> + </actionGroup> + + <!--Go to 'print order' page by grabbed order id--> + <grabFromCurrentUrl regex="~/order_id/(\d+)/~" stepKey="grabOrderIdFromURL"/> + <switchToNextTab stepKey="switchToPrintPage"/> + <waitForElement selector="{{SalesOrderPrintSection.isOrderPrintPage}}" stepKey="checkPrintPage"/> + <openNewTab stepKey="openNewTab"/> + <switchToNextTab stepKey="switchForward"/> + <amOnPage url="{{StorefrontSalesOrderPrintPage.url({$grabOrderIdFromURL})}}" stepKey="duplicatePrintPage"/> + + <!--Log out as customer 1--> + <switchToNextTab stepKey="switchForward2"/> + <openNewTab stepKey="openNewTab2"/> + <amOnPage url="{{StorefrontCustomerSignOutPage.url}}" stepKey="signOut"/> + <waitForLoadingMaskToDisappear stepKey="waitSignOutPage"/> + + <!--Log in to Storefront as Customer 2 --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="signUp2"> + <argument name="Customer" value="$$createCustomer2$$"/> + </actionGroup> + + <!--Create an order at Storefront as Customer 2 --> + <actionGroup ref="CreateOrderToPrintPageWithSelectedPaymentMethodActionGroup" stepKey="createOrderToPrint2"> + <argument name="Category" value="$$createCategory$$"/> + </actionGroup> + + <!--Try to load 'print order' page with not relevant order id to be redirected to 'order history' page--> + <switchToNextTab stepKey="switchToPrintPage2"/> + <waitForElement selector="{{SalesOrderPrintSection.isOrderPrintPage}}" stepKey="checkPrintPage2"/> + <openNewTab stepKey="openNewTab3"/> + <switchToNextTab stepKey="switchForward4"/> + <amOnPage url="{{StorefrontSalesOrderPrintPage.url({$grabOrderIdFromURL})}}" stepKey="duplicatePrintPage2"/> + <seeElement selector="{{StorefrontCustomerOrderSection.isMyOrdersSection}}" stepKey="waitOrderHistoryPage"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Address/FormTest.php b/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Address/FormTest.php new file mode 100644 index 000000000000..94148cc51538 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Address/FormTest.php @@ -0,0 +1,144 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Test\Unit\Block\Adminhtml\Order\Address; + +use Magento\Customer\Model\Metadata\Form as CustomerForm; +use Magento\Customer\Model\Metadata\FormFactory as CustomerFormFactory; +use Magento\Directory\Model\ResourceModel\Country\Collection; +use Magento\Framework\Data\Form as DataForm; +use Magento\Framework\Data\Form\Element\Fieldset; +use Magento\Framework\Data\Form\Element\Select; +use Magento\Framework\Data\FormFactory; +use Magento\Framework\Registry; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Sales\Block\Adminhtml\Order\Address\Form; +use Magento\Sales\Model\AdminOrder\Create; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Address; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +use Magento\Backend\Model\Session\Quote as QuoteSession; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class FormTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Form + */ + private $addressBlock; + + /** + * @var MockObject + */ + private $formFactory; + + /** + * @var MockObject + */ + private $customerFormFactory; + + /** + * @var MockObject + */ + private $coreRegistry; + + /** + * @var MockObject + */ + private $countriesCollection; + + /** + * @var QuoteSession|MockObject + */ + private $sessionQuote; + + /** + * @var Create|MockObject + */ + private $orderCreate; + + protected function setUp() + { + $objectManager = new ObjectManager($this); + + $this->formFactory = $this->createMock(FormFactory::class); + $this->customerFormFactory = $this->createMock(CustomerFormFactory::class); + $this->coreRegistry = $this->createMock(Registry::class); + $this->countriesCollection = $this->createMock( + Collection::class + ); + $this->sessionQuote = $this->getMockBuilder(QuoteSession::class) + ->disableOriginalConstructor() + ->setMethods(['getStoreId', 'getStore']) + ->getMock(); + + $this->orderCreate = $this->getMockBuilder(Create::class) + ->disableOriginalConstructor() + ->getMock(); + $this->orderCreate->method('getSession') + ->willReturn($this->sessionQuote); + + $this->addressBlock = $objectManager->getObject( + Form::class, + [ + '_formFactory' => $this->formFactory, + '_customerFormFactory' => $this->customerFormFactory, + '_coreRegistry' => $this->coreRegistry, + 'countriesCollection' => $this->countriesCollection, + 'sessionQuote' => $this->sessionQuote, + '_orderCreate' => $this->orderCreate + ] + ); + } + + public function testGetForm() + { + $storeId = 5; + $form = $this->createMock(DataForm::class); + $fieldset = $this->createMock(Fieldset::class); + $addressForm = $this->createMock(CustomerForm::class); + $address = $this->createMock(Address::class); + $select = $this->createMock(Select::class); + $order = $this->createMock(Order::class); + + $this->formFactory->method('create') + ->willReturn($form); + $form->method('addFieldset') + ->willReturn($fieldset); + $this->customerFormFactory->method('create') + ->willReturn($addressForm); + $addressForm->method('getAttributes') + ->willReturn([]); + $this->coreRegistry->method('registry') + ->willReturn($address); + $form->method('getElement') + ->willReturnOnConsecutiveCalls( + $select, + $select, + $select, + $select, + $select, + $select, + $select, + null + ); + + $address->method('getOrder') + ->willReturn($order); + $order->method('getStoreId') + ->willReturn($storeId); + $this->sessionQuote->method('getStoreId') + ->willReturn($storeId); + $this->countriesCollection->method('loadByStore') + ->with($storeId) + ->willReturn($this->countriesCollection); + + $this->addressBlock->getForm(); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Create/FormTest.php b/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Create/FormTest.php index 4cbd30b1392f..037e8fd1eaaa 100644 --- a/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Create/FormTest.php +++ b/app/code/Magento/Sales/Test/Unit/Block/Adminhtml/Order/Create/FormTest.php @@ -23,6 +23,7 @@ use Magento\Sales\Block\Adminhtml\Order\Create\Form; use Magento\Sales\Model\AdminOrder\Create; use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; use PHPUnit\Framework\TestCase; use PHPUnit_Framework_MockObject_MockObject as MockObject; @@ -51,6 +52,11 @@ class FormTest extends TestCase */ private $block; + /** + * @var StoreManagerInterface|MockObject + */ + private $storeManager; + /** * @inheritdoc */ @@ -60,6 +66,10 @@ protected function setUp() $context = $this->getMockBuilder(Context::class) ->disableOriginalConstructor() ->getMock(); + $this->storeManager = $this->getMockForAbstractClass(StoreManagerInterface::class); + $context->method('getStoreManager') + ->willReturn($this->storeManager); + $this->quoteSession = $this->getMockBuilder(QuoteSession::class) ->disableOriginalConstructor() ->setMethods(['getCustomerId', 'getQuoteId', 'getStoreId', 'getStore', 'getQuote']) @@ -119,6 +129,8 @@ public function testGetOrderDataJson() 'quote_id' => $quoteId ]; + $this->storeManager->method('setCurrentStore') + ->with($storeId); $this->quoteSession->method('getCustomerId') ->willReturn($customerId); $this->quoteSession->method('getStoreId') diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/AddCommentTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/AddCommentTest.php new file mode 100644 index 000000000000..3a4157df5c76 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/AddCommentTest.php @@ -0,0 +1,176 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Test\Unit\Controller\Adminhtml\Order; + +class AddCommentTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Sales\Controller\Adminhtml\Order\AddComment + */ + private $addCommentController; + + /** + * @var \Magento\Backend\App\Action\Context|\PHPUnit_Framework_MockObject_MockObject + */ + private $contextMock; + + /** + * @var \Magento\Sales\Model\Order|\PHPUnit_Framework_MockObject_MockObject + */ + private $orderMock; + + /** + * @var \Magento\Backend\Model\View\Result\RedirectFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $resultRedirectFactoryMock; + + /** + * @var \Magento\Backend\Model\View\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject + */ + private $resultRedirectMock; + + /** + * @var \Magento\Framework\App\Request\Http|\PHPUnit_Framework_MockObject_MockObject + */ + private $requestMock; + + /** + * @var \Magento\Sales\Api\OrderRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $orderRepositoryMock; + + /** + * @var \Magento\Framework\AuthorizationInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $authorizationMock; + + /** + * @var \Magento\Sales\Model\Order\Status\History|\PHPUnit_Framework_MockObject_MockObject + */ + private $statusHistoryCommentMock; + + /** + * @var \Magento\Framework\ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $objectManagerMock; + + /** + * Test setup + */ + protected function setUp() + { + $this->contextMock = $this->createMock(\Magento\Backend\App\Action\Context::class); + $this->requestMock = $this->createMock(\Magento\Framework\App\Request\Http::class); + $this->orderRepositoryMock = $this->createMock(\Magento\Sales\Api\OrderRepositoryInterface::class); + $this->orderMock = $this->createMock(\Magento\Sales\Model\Order::class); + $this->resultRedirectFactoryMock = $this->createMock(\Magento\Backend\Model\View\Result\RedirectFactory::class); + $this->resultRedirectMock = $this->createMock(\Magento\Backend\Model\View\Result\Redirect::class); + $this->authorizationMock = $this->createMock(\Magento\Framework\AuthorizationInterface::class); + $this->statusHistoryCommentMock = $this->createMock(\Magento\Sales\Model\Order\Status\History::class); + $this->objectManagerMock = $this->createMock(\Magento\Framework\ObjectManagerInterface::class); + + $this->contextMock->expects($this->once())->method('getRequest')->willReturn($this->requestMock); + + $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->addCommentController = $objectManagerHelper->getObject( + \Magento\Sales\Controller\Adminhtml\Order\AddComment::class, + [ + 'context' => $this->contextMock, + 'orderRepository' => $this->orderRepositoryMock, + '_authorization' => $this->authorizationMock, + '_objectManager' => $this->objectManagerMock + ] + ); + } + + /** + * @param array $historyData + * @param bool $userHasResource + * @param bool $expectedNotify + * + * @dataProvider executeWillNotifyCustomerDataProvider + */ + public function testExecuteWillNotifyCustomer(array $historyData, bool $userHasResource, bool $expectedNotify) + { + $orderId = 30; + $this->requestMock->expects($this->once())->method('getParam')->with('order_id')->willReturn($orderId); + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->willReturn($this->orderMock); + $this->requestMock->expects($this->once())->method('getPost')->with('history')->willReturn($historyData); + $this->authorizationMock->expects($this->any())->method('isAllowed')->willReturn($userHasResource); + $this->orderMock->expects($this->once()) + ->method('addStatusHistoryComment') + ->willReturn($this->statusHistoryCommentMock); + $this->statusHistoryCommentMock->expects($this->once())->method('setIsCustomerNotified')->with($expectedNotify); + $this->objectManagerMock->expects($this->once())->method('create')->willReturn( + $this->createMock(\Magento\Sales\Model\Order\Email\Sender\OrderCommentSender::class) + ); + + $this->addCommentController->execute(); + } + + /** + * @return array + */ + public function executeWillNotifyCustomerDataProvider() + { + return [ + 'User Has Access - Notify True' => [ + 'postData' => [ + 'comment' => 'Great Product!', + 'is_customer_notified' => true, + 'status' => 'Processing' + ], + 'userHasResource' => true, + 'expectedNotify' => true + ], + 'User Has Access - Notify False' => [ + 'postData' => [ + 'comment' => 'Great Product!', + 'is_customer_notified' => false, + 'status' => 'Processing' + ], + 'userHasResource' => true, + 'expectedNotify' => false + ], + 'User Has Access - Notify Unset' => [ + 'postData' => [ + 'comment' => 'Great Product!', + 'status' => 'Processing' + ], + 'userHasResource' => true, + 'expectedNotify' => false + ], + 'User No Access - Notify True' => [ + 'postData' => [ + 'comment' => 'Great Product!', + 'is_customer_notified' => true, + 'status' => 'Processing' + ], + 'userHasResource' => false, + 'expectedNotify' => false + ], + 'User No Access - Notify False' => [ + 'postData' => [ + 'comment' => 'Great Product!', + 'is_customer_notified' => false, + 'status' => 'Processing' + ], + 'userHasResource' => false, + 'expectedNotify' => false + ], + 'User No Access - Notify Unset' => [ + 'postData' => [ + 'comment' => 'Great Product!', + 'status' => 'Processing' + ], + 'userHasResource' => false, + 'expectedNotify' => false + ], + ]; + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Creditmemo/SaveTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Creditmemo/SaveTest.php index 0112d09eb735..c3ff8a2acaf4 100644 --- a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Creditmemo/SaveTest.php +++ b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Creditmemo/SaveTest.php @@ -203,10 +203,9 @@ public function testSaveActionWithNegativeCreditmemo() $creditmemoMock = $this->createPartialMock( \Magento\Sales\Model\Order\Creditmemo::class, - ['load', 'getGrandTotal', 'getAllowZeroGrandTotal', '__wakeup'] + ['load', 'isValidGrandTotal', '__wakeup'] ); - $creditmemoMock->expects($this->once())->method('getGrandTotal')->will($this->returnValue('0')); - $creditmemoMock->expects($this->once())->method('getAllowZeroGrandTotal')->will($this->returnValue(false)); + $creditmemoMock->expects($this->once())->method('isValidGrandTotal')->will($this->returnValue(false)); $this->memoLoaderMock->expects( $this->once() )->method( diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/NewActionTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/NewActionTest.php index 05c99c9f9ef9..87e27fdb2206 100644 --- a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/NewActionTest.php +++ b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/NewActionTest.php @@ -6,12 +6,14 @@ namespace Magento\Sales\Test\Unit\Controller\Adminhtml\Order\Invoice; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Sales\Api\OrderRepositoryInterface; /** * Class NewActionTest * @package Magento\Sales\Controller\Adminhtml\Order\Invoice * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyFields) */ class NewActionTest extends \PHPUnit\Framework\TestCase { @@ -90,6 +92,11 @@ class NewActionTest extends \PHPUnit\Framework\TestCase */ protected $invoiceServiceMock; + /** + * @var OrderRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $orderRepositoryMock; + protected function setUp() { $objectManager = new ObjectManager($this); @@ -215,12 +222,15 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); + $this->orderRepositoryMock = $this->createMock(OrderRepositoryInterface::class); + $this->controller = $objectManager->getObject( \Magento\Sales\Controller\Adminhtml\Order\Invoice\NewAction::class, [ 'context' => $contextMock, 'resultPageFactory' => $this->resultPageFactoryMock, - 'invoiceService' => $this->invoiceServiceMock + 'invoiceService' => $this->invoiceServiceMock, + 'orderRepository' => $this->orderRepositoryMock ] ); } @@ -250,19 +260,17 @@ public function testExecute() $orderMock = $this->getMockBuilder(\Magento\Sales\Model\Order::class) ->disableOriginalConstructor() - ->setMethods(['load', 'getId', 'canInvoice']) + ->setMethods(['load', 'canInvoice']) ->getMock(); - $orderMock->expects($this->once()) - ->method('load') - ->with($orderId) - ->willReturnSelf(); - $orderMock->expects($this->once()) - ->method('getId') - ->willReturn($orderId); $orderMock->expects($this->once()) ->method('canInvoice') ->willReturn(true); + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->with($orderId) + ->willReturn($orderMock); + $this->invoiceServiceMock->expects($this->once()) ->method('prepareInvoice') ->with($orderMock, []) @@ -285,11 +293,7 @@ public function testExecute() ->with(true) ->will($this->returnValue($commentText)); - $this->objectManagerMock->expects($this->at(0)) - ->method('create') - ->with(\Magento\Sales\Model\Order::class) - ->willReturn($orderMock); - $this->objectManagerMock->expects($this->at(1)) + $this->objectManagerMock->expects($this->once()) ->method('get') ->with(\Magento\Backend\Model\Session::class) ->will($this->returnValue($this->sessionMock)); @@ -318,19 +322,12 @@ public function testExecuteNoOrder() $orderMock = $this->getMockBuilder(\Magento\Sales\Model\Order::class) ->disableOriginalConstructor() - ->setMethods(['load', 'getId', 'canInvoice']) + ->setMethods(['canInvoice']) ->getMock(); - $orderMock->expects($this->once()) - ->method('load') - ->with($orderId) - ->willReturnSelf(); - $orderMock->expects($this->once()) - ->method('getId') - ->willReturn(null); - $this->objectManagerMock->expects($this->at(0)) - ->method('create') - ->with(\Magento\Sales\Model\Order::class) + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->with($orderId) ->willReturn($orderMock); $resultRedirect = $this->getMockBuilder(\Magento\Backend\Model\View\Result\Redirect::class) diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/MassCancelTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/MassCancelTest.php deleted file mode 100644 index 38449c568663..000000000000 --- a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/MassCancelTest.php +++ /dev/null @@ -1,294 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Sales\Test\Unit\Controller\Adminhtml\Order; - -use Magento\Framework\App\Action\Context; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; - -/** - * Class MassCancelTest - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class MassCancelTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Sales\Controller\Adminhtml\Order\MassCancel - */ - protected $massAction; - - /** - * @var Context|\PHPUnit_Framework_MockObject_MockObject - */ - protected $contextMock; - - /** - * @var \Magento\Backend\Model\View\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject - */ - protected $resultRedirectMock; - - /** - * @var \Magento\Framework\App\Request\Http|\PHPUnit_Framework_MockObject_MockObject - */ - protected $requestMock; - - /** - * @var \Magento\Framework\App\ResponseInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $responseMock; - - /** - * @var \Magento\Framework\Message\Manager|\PHPUnit_Framework_MockObject_MockObject - */ - protected $messageManagerMock; - - /** - * @var \Magento\Framework\ObjectManager\ObjectManager|\PHPUnit_Framework_MockObject_MockObject - */ - protected $objectManagerMock; - - /** - * @var \Magento\Backend\Model\Session|\PHPUnit_Framework_MockObject_MockObject - */ - protected $sessionMock; - - /** - * @var \Magento\Framework\App\ActionFlag|\PHPUnit_Framework_MockObject_MockObject - */ - protected $actionFlagMock; - - /** - * @var \Magento\Backend\Helper\Data|\PHPUnit_Framework_MockObject_MockObject - */ - protected $helperMock; - - /** - * @var \Magento\Sales\Model\Order|\PHPUnit_Framework_MockObject_MockObject - */ - protected $orderMock; - - /** - * @var \Magento\Sales\Model\ResourceModel\Order\Collection|\PHPUnit_Framework_MockObject_MockObject - */ - protected $orderCollectionMock; - - /** - * @var \Magento\Sales\Model\ResourceModel\Order\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $orderCollectionFactoryMock; - - /** - * @var \Magento\Ui\Component\MassAction\Filter|\PHPUnit_Framework_MockObject_MockObject - */ - protected $filterMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $orderManagementMock; - - /** - * Test setup - */ - protected function setUp() - { - $objectManagerHelper = new ObjectManagerHelper($this); - $this->contextMock = $this->createMock(\Magento\Backend\App\Action\Context::class); - $this->messageManagerMock = $this->createMock(\Magento\Framework\Message\Manager::class); - $this->responseMock = $this->createMock(\Magento\Framework\App\ResponseInterface::class); - $this->requestMock = $this->createMock(\Magento\Framework\App\Request\Http::class); - $this->objectManagerMock = $this->createMock(\Magento\Framework\ObjectManager\ObjectManager::class); - - $resultRedirectFactory = $this->createMock(\Magento\Backend\Model\View\Result\RedirectFactory::class); - - $this->orderCollectionMock = $this->getMockBuilder(\Magento\Sales\Model\ResourceModel\Order\Collection::class) - ->disableOriginalConstructor() - ->getMock(); - - $resourceCollection = \Magento\Sales\Model\ResourceModel\Order\CollectionFactory::class; - $this->orderCollectionFactoryMock = $this->getMockBuilder($resourceCollection) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - - $this->sessionMock = $this->createPartialMock(\Magento\Backend\Model\Session::class, ['setIsUrlNotice']); - $this->actionFlagMock = $this->createPartialMock(\Magento\Framework\App\ActionFlag::class, ['get', 'set']); - $this->helperMock = $this->createPartialMock(\Magento\Backend\Helper\Data::class, ['getUrl']); - $this->resultRedirectMock = $this->createMock(\Magento\Backend\Model\View\Result\Redirect::class); - $resultRedirectFactory->expects($this->any())->method('create')->willReturn($this->resultRedirectMock); - - $redirectMock = $this->getMockBuilder(\Magento\Backend\Model\View\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $resultFactoryMock = $this->getMockBuilder(\Magento\Framework\Controller\ResultFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $resultFactoryMock->expects($this->any()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT) - ->willReturn($redirectMock); - - $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('getResponse')->willReturn($this->responseMock); - $this->contextMock->expects($this->once())->method('getObjectManager')->willReturn($this->objectManagerMock); - $this->contextMock->expects($this->once())->method('getSession')->willReturn($this->sessionMock); - $this->contextMock->expects($this->once())->method('getActionFlag')->willReturn($this->actionFlagMock); - $this->contextMock->expects($this->once())->method('getHelper')->willReturn($this->helperMock); - $this->contextMock->expects($this->once()) - ->method('getResultRedirectFactory') - ->willReturn($resultRedirectFactory); - $this->contextMock->expects($this->any()) - ->method('getResultFactory') - ->willReturn($resultFactoryMock); - $this->filterMock = $this->createMock(\Magento\Ui\Component\MassAction\Filter::class); - $this->filterMock->expects($this->once()) - ->method('getCollection') - ->with($this->orderCollectionMock) - ->willReturn($this->orderCollectionMock); - $this->orderCollectionFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($this->orderCollectionMock); - $this->orderManagementMock = $this->createMock(\Magento\Sales\Api\OrderManagementInterface::class); - - $this->massAction = $objectManagerHelper->getObject( - \Magento\Sales\Controller\Adminhtml\Order\MassCancel::class, - [ - 'context' => $this->contextMock, - 'filter' => $this->filterMock, - 'collectionFactory' => $this->orderCollectionFactoryMock, - 'orderManagement' => $this->orderManagementMock - ] - ); - } - - /** - * Test for selected orders - * Two orders, only $order1 can be canceled - */ - public function testExecuteCanCancelOneOrder() - { - $order1id = 100; - $order2id = 200; - - $order1 = $this->getMockBuilder(\Magento\Sales\Model\Order::class) - ->disableOriginalConstructor() - ->getMock(); - $order2 = $this->getMockBuilder(\Magento\Sales\Model\Order::class) - ->disableOriginalConstructor() - ->getMock(); - $orders = [$order1, $order2]; - $countOrders = count($orders); - - $this->orderCollectionMock->expects($this->any()) - ->method('getItems') - ->willReturn($orders); - - $order1->expects($this->once()) - ->method('getEntityId') - ->willReturn($order1id); - - $order2->expects($this->once()) - ->method('getEntityId') - ->willReturn($order2id); - - $this->orderCollectionMock->expects($this->once()) - ->method('count') - ->willReturn($countOrders); - - $this->orderManagementMock->expects($this->at(0))->method('cancel')->with($order1id)->willReturn(true); - $this->orderManagementMock->expects($this->at(1))->method('cancel')->with($order2id)->willReturn(false); - - $this->messageManagerMock->expects($this->once()) - ->method('addErrorMessage') - ->with('1 order(s) cannot be canceled.'); - - $this->messageManagerMock->expects($this->once()) - ->method('addSuccessMessage') - ->with('We canceled 1 order(s).'); - - $this->resultRedirectMock->expects($this->once()) - ->method('setPath') - ->with('sales/*/') - ->willReturnSelf(); - - $this->massAction->execute(); - } - - /** - * Test for excluded orders - * Two orders could't be canceled - */ - public function testExcludedCannotCancelOrders() - { - $order1 = $this->getMockBuilder(\Magento\Sales\Model\Order::class) - ->disableOriginalConstructor() - ->getMock(); - $order2 = $this->getMockBuilder(\Magento\Sales\Model\Order::class) - ->disableOriginalConstructor() - ->getMock(); - - $orders = [$order1, $order2]; - $countOrders = count($orders); - - $order1->expects($this->once()) - ->method('getEntityId') - ->willReturn(100); - - $order2->expects($this->once()) - ->method('getEntityId') - ->willReturn(200); - - $this->orderCollectionMock->expects($this->any()) - ->method('getItems') - ->willReturn([$order1, $order2]); - - $this->orderCollectionMock->expects($this->once()) - ->method('count') - ->willReturn($countOrders); - - $this->orderManagementMock->expects($this->atLeastOnce())->method('cancel')->willReturn(false); - - $this->messageManagerMock->expects($this->once()) - ->method('addErrorMessage') - ->with('You cannot cancel the order(s).'); - - $this->resultRedirectMock->expects($this->once()) - ->method('setPath') - ->with('sales/*/') - ->willReturnSelf(); - - $this->massAction->execute(); - } - - /** - * Order throws exception while canceling - */ - public function testException() - { - $exception = new \Exception('Can not cancel'); - - $order1 = $this->getMockBuilder(\Magento\Sales\Model\Order::class) - ->disableOriginalConstructor() - ->getMock(); - $this->orderCollectionMock->expects($this->any()) - ->method('getItems') - ->willReturn([$order1]); - - $order1->expects($this->once()) - ->method('getEntityId') - ->willReturn(100); - - $this->orderManagementMock->expects($this->atLeastOnce())->method('cancel')->willThrowException($exception); - - $this->messageManagerMock->expects($this->once()) - ->method('addErrorMessage') - ->with('Can not cancel'); - - $this->massAction->execute(); - } -} diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/MassHoldTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/MassHoldTest.php deleted file mode 100644 index bfd1d1e825f8..000000000000 --- a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/MassHoldTest.php +++ /dev/null @@ -1,262 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Sales\Test\Unit\Controller\Adminhtml\Order; - -use Magento\Framework\App\Action\Context; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; - -/** - * Class MassHoldTest - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class MassHoldTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Sales\Controller\Adminhtml\Order\MassHold - */ - protected $massAction; - - /** - * @var Context|\PHPUnit_Framework_MockObject_MockObject - */ - protected $contextMock; - - /** - * @var \Magento\Backend\Model\View\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject - */ - protected $resultRedirectMock; - - /** - * @var \Magento\Framework\App\Request\Http|\PHPUnit_Framework_MockObject_MockObject - */ - protected $requestMock; - - /** - * @var \Magento\Framework\App\ResponseInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $responseMock; - - /** - * @var \Magento\Framework\Message\Manager|\PHPUnit_Framework_MockObject_MockObject - */ - protected $messageManagerMock; - - /** - * @var \Magento\Framework\ObjectManager\ObjectManager|\PHPUnit_Framework_MockObject_MockObject - */ - protected $objectManagerMock; - - /** - * @var \Magento\Backend\Model\Session|\PHPUnit_Framework_MockObject_MockObject - */ - protected $sessionMock; - - /** - * @var \Magento\Framework\App\ActionFlag|\PHPUnit_Framework_MockObject_MockObject - */ - protected $actionFlagMock; - - /** - * @var \Magento\Backend\Helper\Data|\PHPUnit_Framework_MockObject_MockObject - */ - protected $helperMock; - - /** - * @var \Magento\Sales\Model\Order|\PHPUnit_Framework_MockObject_MockObject - */ - protected $orderMock; - - /** - * @var \Magento\Sales\Model\ResourceModel\Order\Collection|\PHPUnit_Framework_MockObject_MockObject - */ - protected $orderCollectionMock; - - /** - * @var \Magento\Sales\Model\ResourceModel\Order\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $orderCollectionFactoryMock; - - /** - * @var \Magento\Ui\Component\MassAction\Filter|\PHPUnit_Framework_MockObject_MockObject - */ - protected $filterMock; - - /** - * @var \Magento\Sales\Api\OrderManagementInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $orderManagementMock; - - /** - * Test setup - */ - protected function setUp() - { - $objectManagerHelper = new ObjectManagerHelper($this); - $this->orderManagementMock = $this->getMockBuilder(\Magento\Sales\Api\OrderManagementInterface::class) - ->getMockForAbstractClass(); - $this->contextMock = $this->createMock(\Magento\Backend\App\Action\Context::class); - $resultRedirectFactory = $this->createMock(\Magento\Backend\Model\View\Result\RedirectFactory::class); - $this->responseMock = $this->createMock(\Magento\Framework\App\ResponseInterface::class); - $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) - ->disableOriginalConstructor()->getMock(); - $this->objectManagerMock = $this->createPartialMock( - \Magento\Framework\ObjectManager\ObjectManager::class, - ['create'] - ); - $this->messageManagerMock = $this->createMock(\Magento\Framework\Message\Manager::class); - $this->orderCollectionMock = $this->getMockBuilder(\Magento\Sales\Model\ResourceModel\Order\Collection::class) - ->disableOriginalConstructor() - ->getMock(); - $orderCollection = \Magento\Sales\Model\ResourceModel\Order\CollectionFactory::class; - $this->orderCollectionFactoryMock = $this->getMockBuilder($orderCollection) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - $redirectMock = $this->getMockBuilder(\Magento\Backend\Model\View\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $resultFactoryMock = $this->getMockBuilder(\Magento\Framework\Controller\ResultFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $resultFactoryMock->expects($this->any()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT) - ->willReturn($redirectMock); - - $this->sessionMock = $this->createPartialMock(\Magento\Backend\Model\Session::class, ['setIsUrlNotice']); - $this->actionFlagMock = $this->createPartialMock(\Magento\Framework\App\ActionFlag::class, ['get', 'set']); - $this->helperMock = $this->createPartialMock(\Magento\Backend\Helper\Data::class, ['getUrl']); - $this->resultRedirectMock = $this->createMock(\Magento\Backend\Model\View\Result\Redirect::class); - $resultRedirectFactory->expects($this->any())->method('create')->willReturn($this->resultRedirectMock); - - $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('getResponse')->willReturn($this->responseMock); - $this->contextMock->expects($this->once())->method('getObjectManager')->willReturn($this->objectManagerMock); - $this->contextMock->expects($this->once())->method('getSession')->willReturn($this->sessionMock); - $this->contextMock->expects($this->once())->method('getActionFlag')->willReturn($this->actionFlagMock); - $this->contextMock->expects($this->once())->method('getHelper')->willReturn($this->helperMock); - $this->contextMock - ->expects($this->once()) - ->method('getResultRedirectFactory') - ->willReturn($resultRedirectFactory); - $this->contextMock->expects($this->any()) - ->method('getResultFactory') - ->willReturn($resultFactoryMock); - - $this->filterMock = $this->createMock(\Magento\Ui\Component\MassAction\Filter::class); - $this->filterMock->expects($this->once()) - ->method('getCollection') - ->with($this->orderCollectionMock) - ->willReturn($this->orderCollectionMock); - $this->orderCollectionFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($this->orderCollectionMock); - - $this->massAction = $objectManagerHelper->getObject( - \Magento\Sales\Controller\Adminhtml\Order\MassHold::class, - [ - 'context' => $this->contextMock, - 'filter' => $this->filterMock, - 'collectionFactory' => $this->orderCollectionFactoryMock, - 'orderManagement' => $this->orderManagementMock - ] - ); - } - - /** - * @throws \Magento\Framework\Exception\LocalizedException - */ - public function testExecuteOneOrderPutOnHold() - { - $order1 = $this->getMockBuilder(\Magento\Sales\Model\Order::class) - ->disableOriginalConstructor() - ->getMock(); - $order2 = $this->getMockBuilder(\Magento\Sales\Model\Order::class) - ->disableOriginalConstructor() - ->getMock(); - - $orders = [$order1, $order2]; - $countOrders = count($orders); - - $this->orderCollectionMock->expects($this->any()) - ->method('getItems') - ->willReturn($orders); - - $order1->expects($this->once()) - ->method('canHold') - ->willReturn(true); - $this->orderManagementMock->expects($this->once()) - ->method('hold'); - $this->orderCollectionMock->expects($this->once()) - ->method('count') - ->willReturn($countOrders); - - $order2->expects($this->once()) - ->method('canHold') - ->willReturn(false); - - $this->messageManagerMock->expects($this->once()) - ->method('addErrorMessage') - ->with('1 order(s) were not put on hold.'); - - $this->messageManagerMock->expects($this->once()) - ->method('addSuccessMessage') - ->with('You have put 1 order(s) on hold.'); - - $this->resultRedirectMock->expects($this->once()) - ->method('setPath') - ->with('sales/*/') - ->willReturnSelf(); - - $this->massAction->execute(); - } - - /** - * @throws \Magento\Framework\Exception\LocalizedException - */ - public function testExecuteNoOrdersPutOnHold() - { - $order1 = $this->getMockBuilder(\Magento\Sales\Model\Order::class) - ->disableOriginalConstructor() - ->getMock(); - $order2 = $this->getMockBuilder(\Magento\Sales\Model\Order::class) - ->disableOriginalConstructor() - ->getMock(); - - $orders = [$order1, $order2]; - $countOrders = count($orders); - - $this->orderCollectionMock->expects($this->any()) - ->method('getItems') - ->willReturn($orders); - - $order1->expects($this->once()) - ->method('canHold') - ->willReturn(false); - - $this->orderCollectionMock->expects($this->once()) - ->method('count') - ->willReturn($countOrders); - - $order2->expects($this->once()) - ->method('canHold') - ->willReturn(false); - - $this->messageManagerMock->expects($this->once()) - ->method('addErrorMessage') - ->with('No order(s) were put on hold.'); - - $this->resultRedirectMock->expects($this->once()) - ->method('setPath') - ->with('sales/*/') - ->willReturnSelf(); - - $this->massAction->execute(); - } -} diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/MassUnholdTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/MassUnholdTest.php deleted file mode 100644 index 56473ec948f2..000000000000 --- a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/MassUnholdTest.php +++ /dev/null @@ -1,261 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Sales\Test\Unit\Controller\Adminhtml\Order; - -use Magento\Framework\App\Action\Context; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; - -/** - * Class MassHoldTest - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class MassUnholdTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Sales\Controller\Adminhtml\Order\MassUnhold - */ - protected $massAction; - - /** - * @var Context|\PHPUnit_Framework_MockObject_MockObject - */ - protected $contextMock; - - /** - * @var \Magento\Backend\Model\View\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject - */ - protected $resultRedirectMock; - - /** - * @var \Magento\Framework\App\Request\Http|\PHPUnit_Framework_MockObject_MockObject - */ - protected $requestMock; - - /** - * @var \Magento\Framework\App\ResponseInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $responseMock; - - /** - * @var \Magento\Framework\Message\Manager|\PHPUnit_Framework_MockObject_MockObject - */ - protected $messageManagerMock; - - /** - * @var \Magento\Framework\ObjectManager\ObjectManager|\PHPUnit_Framework_MockObject_MockObject - */ - protected $objectManagerMock; - - /** - * @var \Magento\Backend\Model\Session|\PHPUnit_Framework_MockObject_MockObject - */ - protected $sessionMock; - - /** - * @var \Magento\Framework\App\ActionFlag|\PHPUnit_Framework_MockObject_MockObject - */ - protected $actionFlagMock; - - /** - * @var \Magento\Backend\Helper\Data|\PHPUnit_Framework_MockObject_MockObject - */ - protected $helperMock; - - /** - * @var \Magento\Sales\Model\Order|\PHPUnit_Framework_MockObject_MockObject - */ - protected $orderMock; - - /** - * @var \Magento\Sales\Model\ResourceModel\Order\Collection|\PHPUnit_Framework_MockObject_MockObject - */ - protected $orderCollectionMock; - - /** - * @var \Magento\Sales\Model\ResourceModel\Order\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $orderCollectionFactoryMock; - - /** - * @var \Magento\Ui\Component\MassAction\Filter|\PHPUnit_Framework_MockObject_MockObject - */ - protected $filterMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $orderManagementMock; - - /** - * Test setup - */ - protected function setUp() - { - $objectManagerHelper = new ObjectManagerHelper($this); - $this->contextMock = $this->createMock(\Magento\Backend\App\Action\Context::class); - $resultRedirectFactory = $this->createMock(\Magento\Backend\Model\View\Result\RedirectFactory::class); - $this->responseMock = $this->createMock(\Magento\Framework\App\ResponseInterface::class); - $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) - ->disableOriginalConstructor()->getMock(); - $this->objectManagerMock = $this->createMock(\Magento\Framework\ObjectManager\ObjectManager::class); - $this->messageManagerMock = $this->createMock(\Magento\Framework\Message\Manager::class); - - $this->orderCollectionMock = $this->getMockBuilder(\Magento\Sales\Model\ResourceModel\Order\Collection::class) - ->disableOriginalConstructor() - ->getMock(); - $orderCollection = \Magento\Sales\Model\ResourceModel\Order\CollectionFactory::class; - $this->orderCollectionFactoryMock = $this->getMockBuilder($orderCollection) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - $this->sessionMock = $this->createPartialMock(\Magento\Backend\Model\Session::class, ['setIsUrlNotice']); - $this->actionFlagMock = $this->createPartialMock(\Magento\Framework\App\ActionFlag::class, ['get', 'set']); - $this->helperMock = $this->createPartialMock(\Magento\Backend\Helper\Data::class, ['getUrl']); - $this->resultRedirectMock = $this->createMock(\Magento\Backend\Model\View\Result\Redirect::class); - $resultRedirectFactory->expects($this->any())->method('create')->willReturn($this->resultRedirectMock); - - $redirectMock = $this->getMockBuilder(\Magento\Backend\Model\View\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $resultFactoryMock = $this->getMockBuilder(\Magento\Framework\Controller\ResultFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $resultFactoryMock->expects($this->any()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT) - ->willReturn($redirectMock); - - $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('getResponse')->willReturn($this->responseMock); - $this->contextMock->expects($this->once())->method('getObjectManager')->willReturn($this->objectManagerMock); - $this->contextMock->expects($this->once())->method('getSession')->willReturn($this->sessionMock); - $this->contextMock->expects($this->once())->method('getActionFlag')->willReturn($this->actionFlagMock); - $this->contextMock->expects($this->once())->method('getHelper')->willReturn($this->helperMock); - $this->contextMock - ->expects($this->once()) - ->method('getResultRedirectFactory') - ->willReturn($resultRedirectFactory); - $this->contextMock->expects($this->any()) - ->method('getResultFactory') - ->willReturn($resultFactoryMock); - - $this->filterMock = $this->createMock(\Magento\Ui\Component\MassAction\Filter::class); - $this->filterMock->expects($this->once()) - ->method('getCollection') - ->with($this->orderCollectionMock) - ->willReturn($this->orderCollectionMock); - $this->orderCollectionFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($this->orderCollectionMock); - - $this->orderManagementMock = $this->createMock(\Magento\Sales\Api\OrderManagementInterface::class); - - $this->massAction = $objectManagerHelper->getObject( - \Magento\Sales\Controller\Adminhtml\Order\MassUnhold::class, - [ - 'context' => $this->contextMock, - 'filter' => $this->filterMock, - 'collectionFactory' => $this->orderCollectionFactoryMock, - 'orderManagement' => $this->orderManagementMock - ] - ); - } - - /** - * @throws \Magento\Framework\Exception\LocalizedException - */ - public function testExecuteOneOrdersReleasedFromHold() - { - $order1 = $this->getMockBuilder(\Magento\Sales\Model\Order::class) - ->disableOriginalConstructor() - ->getMock(); - $order2 = $this->getMockBuilder(\Magento\Sales\Model\Order::class) - ->disableOriginalConstructor() - ->getMock(); - - $orders = [$order1, $order2]; - - $this->orderCollectionMock->expects($this->any()) - ->method('getItems') - ->willReturn($orders); - - $order1->expects($this->once()) - ->method('canUnhold') - ->willReturn(true); - $order1->expects($this->once()) - ->method('getEntityId'); - - $this->orderCollectionMock->expects($this->once()) - ->method('count') - ->willReturn(count($orders)); - - $order2->expects($this->once()) - ->method('canUnhold') - ->willReturn(false); - - $this->orderManagementMock->expects($this->atLeastOnce())->method('unHold')->willReturn(true); - - $this->messageManagerMock->expects($this->once()) - ->method('addErrorMessage') - ->with('1 order(s) were not released from on hold status.'); - - $this->messageManagerMock->expects($this->once()) - ->method('addSuccessMessage') - ->with('1 order(s) have been released from on hold status.'); - - $this->resultRedirectMock->expects($this->once()) - ->method('setPath') - ->with('sales/*/') - ->willReturnSelf(); - - $this->massAction->execute(); - } - - /** - * @throws \Magento\Framework\Exception\LocalizedException - */ - public function testExecuteNoReleasedOrderFromHold() - { - $order1 = $this->getMockBuilder(\Magento\Sales\Model\Order::class) - ->disableOriginalConstructor() - ->getMock(); - $order2 = $this->getMockBuilder(\Magento\Sales\Model\Order::class) - ->disableOriginalConstructor() - ->getMock(); - - $orders = [$order1, $order2]; - - $this->orderCollectionMock->expects($this->any()) - ->method('getItems') - ->willReturn($orders); - - $order1->expects($this->once()) - ->method('canUnhold') - ->willReturn(false); - - $this->orderCollectionMock->expects($this->once()) - ->method('count') - ->willReturn(count($orders)); - - $order2->expects($this->once()) - ->method('canUnhold') - ->willReturn(false); - - $this->messageManagerMock->expects($this->once()) - ->method('addErrorMessage') - ->with('No order(s) were released from on hold status.'); - - $this->resultRedirectMock->expects($this->once()) - ->method('setPath') - ->with('sales/*/') - ->willReturnSelf(); - - $this->massAction->execute(); - } -} diff --git a/app/code/Magento/Sales/Test/Unit/Model/CronJob/CleanExpiredOrdersTest.php b/app/code/Magento/Sales/Test/Unit/Model/CronJob/CleanExpiredOrdersTest.php index 269ce829e64d..6844b908ea98 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/CronJob/CleanExpiredOrdersTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/CronJob/CleanExpiredOrdersTest.php @@ -26,6 +26,11 @@ class CleanExpiredOrdersTest extends \PHPUnit\Framework\TestCase */ protected $orderCollectionMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $orderManagementMock; + /** * @var ObjectManager */ @@ -44,10 +49,12 @@ protected function setUp() ['create'] ); $this->orderCollectionMock = $this->createMock(\Magento\Sales\Model\ResourceModel\Order\Collection::class); + $this->orderManagementMock = $this->createMock(\Magento\Sales\Api\OrderManagementInterface::class); $this->model = new CleanExpiredOrders( $this->storesConfigMock, - $this->collectionFactoryMock + $this->collectionFactoryMock, + $this->orderManagementMock ); } @@ -64,8 +71,11 @@ public function testExecute() $this->collectionFactoryMock->expects($this->exactly(2)) ->method('create') ->willReturn($this->orderCollectionMock); + $this->orderCollectionMock->expects($this->exactly(2)) + ->method('getAllIds') + ->willReturn([1, 2]); $this->orderCollectionMock->expects($this->exactly(4))->method('addFieldToFilter'); - $this->orderCollectionMock->expects($this->exactly(4))->method('walk'); + $this->orderManagementMock->expects($this->exactly(4))->method('cancel'); $selectMock = $this->createMock(\Magento\Framework\DB\Select::class); $selectMock->expects($this->exactly(2))->method('where')->willReturnSelf(); @@ -92,14 +102,18 @@ public function testExecuteWithException() $this->collectionFactoryMock->expects($this->once()) ->method('create') ->willReturn($this->orderCollectionMock); + $this->orderCollectionMock->expects($this->once()) + ->method('getAllIds') + ->willReturn([1]); $this->orderCollectionMock->expects($this->exactly(2))->method('addFieldToFilter'); + $this->orderManagementMock->expects($this->once())->method('cancel'); $selectMock = $this->createMock(\Magento\Framework\DB\Select::class); $selectMock->expects($this->once())->method('where')->willReturnSelf(); $this->orderCollectionMock->expects($this->once())->method('getSelect')->willReturn($selectMock); - $this->orderCollectionMock->expects($this->once()) - ->method('walk') + $this->orderManagementMock->expects($this->once()) + ->method('cancel') ->willThrowException(new \Exception($exceptionMessage)); $this->model->execute(); diff --git a/app/code/Magento/Sales/Test/Unit/Model/DownloadTest.php b/app/code/Magento/Sales/Test/Unit/Model/DownloadTest.php deleted file mode 100644 index dd430f8a0330..000000000000 --- a/app/code/Magento/Sales/Test/Unit/Model/DownloadTest.php +++ /dev/null @@ -1,262 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Sales\Test\Unit\Model; - -use Magento\Framework\App\Filesystem\DirectoryList; - -class DownloadTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Sales\Model\Download - */ - protected $model; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $filesystemMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $storageMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $storageFactoryMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $httpFileFactoryMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $writeDirectoryMock; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $driverMock; - - protected function setUp() - { - $this->writeDirectoryMock = $this->getMockBuilder(\Magento\Framework\Filesystem\Directory\Write::class) - ->disableOriginalConstructor() - ->getMock(); - $this->filesystemMock = $this->getMockBuilder(\Magento\Framework\Filesystem::class) - ->disableOriginalConstructor() - ->getMock(); - $this->filesystemMock->expects($this->any()) - ->method('getDirectoryWrite') - ->with(DirectoryList::MEDIA) - ->will($this->returnValue($this->writeDirectoryMock)); - - $this->driverMock = $this->getMockForAbstractClass(\Magento\Framework\Filesystem\DriverInterface::class); - $this->storageMock = $this->getMockBuilder(\Magento\MediaStorage\Helper\File\Storage\Database::class) - ->disableOriginalConstructor() - ->getMock(); - $this->storageFactoryMock = $this->getMockBuilder( - \Magento\MediaStorage\Model\File\Storage\DatabaseFactory::class - )->disableOriginalConstructor() - ->setMethods(['create', 'checkDbUsage']) - ->getMock(); - $this->httpFileFactoryMock = $this->getMockBuilder(\Magento\Framework\App\Response\Http\FileFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - $this->model = new \Magento\Sales\Model\Download( - $this->filesystemMock, - $this->storageMock, - $this->storageFactoryMock, - $this->httpFileFactoryMock - ); - } - - public function testInstanceOf() - { - $model = new \Magento\Sales\Model\Download( - $this->filesystemMock, - $this->storageMock, - $this->storageFactoryMock, - $this->httpFileFactoryMock - ); - $this->assertInstanceOf(\Magento\Sales\Model\Download::class, $model); - } - - /** - * @param $realPatchCheck - * @param $isFile - * @param $isReadable - * @expectedException \Magento\Framework\Exception\LocalizedException - * @dataProvider dataProviderForTestDownloadFileException - */ - public function testDownloadFileException($realPatchCheck, $isFile, $isReadable) - { - $info = ['order_path' => 'test/path', 'quote_path' => 'test/path2', 'title' => 'test title']; - - $this->writeDirectoryMock->expects($this->any()) - ->method('getAbsolutePath') - ->will($this->returnArgument(0)); - $this->writeDirectoryMock->expects($this->any()) - ->method('getDriver') - ->willReturn($this->driverMock); - $this->driverMock->expects($this->any())->method('getRealPath')->willReturn($realPatchCheck); - $this->writeDirectoryMock->expects($this->any()) - ->method('isFile') - ->will($this->returnValue($isFile)); - $this->writeDirectoryMock->expects($this->any()) - ->method('isReadable') - ->will($this->returnValue($isReadable)); - - $this->storageFactoryMock->expects($this->any()) - ->method('checkDbUsage') - ->will($this->returnValue(false)); - $this->httpFileFactoryMock->expects($this->never())->method('create'); - - $this->model->downloadFile($info); - } - - /** - * @return array - */ - public function dataProviderForTestDownloadFileException() - { - return [ - [1, true, false], - [1, false, true], - [false, true, true], - ]; - } - - /** - * @expectedException \Magento\Framework\Exception\LocalizedException - */ - public function testDownloadFileNoStorage() - { - $info = ['order_path' => 'test/path', 'quote_path' => 'test/path2', 'title' => 'test title']; - $isFile = true; - $isReadable = false; - - $this->writeDirectoryMock->expects($this->any()) - ->method('getAbsolutePath') - ->will($this->returnArgument(0)); - $this->writeDirectoryMock->expects($this->any()) - ->method('getDriver') - ->willReturn($this->driverMock); - $this->driverMock->expects($this->any())->method('getRealPath')->willReturn(true); - - $this->writeDirectoryMock->expects($this->any()) - ->method('isFile') - ->will($this->returnValue($isFile)); - $this->writeDirectoryMock->expects($this->any()) - ->method('isReadable') - ->will($this->returnValue($isReadable)); - - $this->storageMock->expects($this->any()) - ->method('checkDbUsage') - ->will($this->returnValue(true)); - - $storageDatabaseMock = $this->getMockBuilder(\Magento\MediaStorage\Model\File\Storage\Database::class) - ->disableOriginalConstructor() - ->getMock(); - $storageDatabaseMock->expects($this->at(0)) - ->method('loadByFilename') - ->with($this->equalTo($info['order_path'])) - ->will($this->returnSelf()); - $storageDatabaseMock->expects($this->at(2)) - ->method('loadByFilename') - ->with($this->equalTo($info['quote_path'])) - ->will($this->returnSelf()); - - $storageDatabaseMock->expects($this->any()) - ->method('getId') - ->will($this->returnValue(false)); - - $this->storageFactoryMock->expects($this->any()) - ->method('create') - ->will($this->returnValue($storageDatabaseMock)); - $this->httpFileFactoryMock->expects($this->never())->method('create'); - - $this->model->downloadFile($info); - } - - public function testDownloadFile() - { - $info = ['order_path' => 'test/path', 'quote_path' => 'test/path2', 'title' => 'test title']; - $isFile = true; - $isReadable = false; - - $writeMock = $this->getMockBuilder(\Magento\Framework\Filesystem\File\Write::class) - ->disableOriginalConstructor() - ->getMock(); - $writeMock->expects($this->any()) - ->method('lock'); - $writeMock->expects($this->any()) - ->method('write'); - $writeMock->expects($this->any()) - ->method('unlock'); - $writeMock->expects($this->any()) - ->method('close'); - - $this->writeDirectoryMock->expects($this->any()) - ->method('getAbsolutePath') - ->will($this->returnArgument(0)); - $this->writeDirectoryMock->expects($this->any()) - ->method('getDriver') - ->willReturn($this->driverMock); - $this->driverMock->expects($this->any())->method('getRealPath')->willReturn(true); - - $this->writeDirectoryMock->expects($this->any()) - ->method('isFile') - ->will($this->returnValue($isFile)); - $this->writeDirectoryMock->expects($this->any()) - ->method('isReadable') - ->will($this->returnValue($isReadable)); - $this->writeDirectoryMock->expects($this->any()) - ->method('openFile') - ->will($this->returnValue($writeMock)); - $this->writeDirectoryMock->expects($this->once()) - ->method('getRelativePath') - ->with($info['order_path']) - ->will($this->returnArgument(0)); - - $this->storageMock->expects($this->any()) - ->method('checkDbUsage') - ->will($this->returnValue(true)); - - $storageDatabaseMock = $this->getMockBuilder(\Magento\MediaStorage\Model\File\Storage\Database::class) - ->disableOriginalConstructor() - ->setMethods(['loadByFilename', 'getId', '__wakeup']) - ->getMock(); - $storageDatabaseMock->expects($this->any()) - ->method('loadByFilename') - ->will($this->returnSelf()); - - $storageDatabaseMock->expects($this->any()) - ->method('getId') - ->will($this->returnValue(true)); - - $this->storageFactoryMock->expects($this->any()) - ->method('create') - ->will($this->returnValue($storageDatabaseMock)); - - $this->httpFileFactoryMock->expects($this->once()) - ->method('create') - ->with( - $info['title'], - ['value' => $info['order_path'], 'type' => 'filename'], - DirectoryList::MEDIA, - 'application/octet-stream', - null - ); - - $result = $this->model->downloadFile($info); - $this->assertNull($result); - } -} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php index 9fd2a8b0d929..467476c9bb40 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php @@ -1,5 +1,4 @@ <?php - /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. @@ -249,7 +248,7 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending $this->creditmemoMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with($emailSendingResult); if (!$configValue || $forceSyncMode) { $transport = [ @@ -279,7 +278,7 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending ->method('setTemplateVars') ->with($transport->getData()); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn($emailSendingResult); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/DiscountTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/DiscountTest.php index 18efef38b204..8a45aa8c7958 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/DiscountTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/DiscountTest.php @@ -74,7 +74,7 @@ public function testCollect() $this->orderMock->expects($this->once()) ->method('getBaseShippingDiscountAmount') ->willReturn(1); - $this->orderMock->expects($this->exactly(2)) + $this->orderMock->expects($this->exactly(3)) ->method('getBaseShippingAmount') ->willReturn(1); $this->orderMock->expects($this->once()) @@ -150,7 +150,7 @@ public function testCollectNoBaseShippingAmount() $this->orderMock->expects($this->once()) ->method('getBaseShippingDiscountAmount') ->willReturn(1); - $this->orderMock->expects($this->exactly(2)) + $this->orderMock->expects($this->exactly(3)) ->method('getBaseShippingAmount') ->willReturn(1); $this->orderMock->expects($this->once()) @@ -269,4 +269,30 @@ public function testCollectZeroShipping() ); $this->assertEquals($this->total, $this->total->collect($this->creditmemoMock)); } + + /** + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage You can not refund shipping if there is no shipping amount. + */ + public function testCollectNonZeroShipping() + { + $this->creditmemoMock->expects($this->once()) + ->method('setDiscountAmount') + ->willReturnSelf(); + $this->creditmemoMock->expects($this->once()) + ->method('setBaseDiscountAmount') + ->willReturnSelf(); + $this->creditmemoMock->expects($this->once()) + ->method('getOrder') + ->willReturn($this->orderMock); + $this->creditmemoMock->expects($this->once()) + ->method('getBaseShippingAmount') + ->willReturn('10.0000'); + $this->orderMock->expects($this->never()) + ->method('getBaseShippingDiscountAmount'); + $this->orderMock->expects($this->once()) + ->method('getBaseShippingAmount') + ->willReturn('0.0000'); + $this->assertEquals($this->total, $this->total->collect($this->creditmemoMock)); + } } diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/TaxTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/TaxTest.php index 4bca669f089b..565d51ff515a 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/TaxTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/TaxTest.php @@ -447,10 +447,10 @@ public function collectDataProvider() ], ], 'creditmemo_data' => [ - 'grand_total' => 64.95, - 'base_grand_total' => 64.95, - 'tax_amount' => 4.95, - 'base_tax_amount' => 4.95, + 'grand_total' => 64.94, + 'base_grand_total' => 64.94, + 'tax_amount' => 4.94, + 'base_tax_amount' => 4.94, ], ], ]; diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoDocumentFactoryTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoDocumentFactoryTest.php index cb65b70b8271..dc1830694277 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoDocumentFactoryTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoDocumentFactoryTest.php @@ -12,7 +12,6 @@ use Magento\Sales\Api\Data\CreditmemoCommentInterface; use Magento\Sales\Model\Order; use Magento\Sales\Model\Order\Invoice; -use Magento\Sales\Api\Data\CreditmemoInterface; use Magento\Sales\Api\Data\CreditmemoItemCreationInterface; use Magento\Sales\Api\Data\CreditmemoCommentCreationInterface; use Magento\Framework\EntityManager\HydratorPool; @@ -82,7 +81,7 @@ class CreditmemoDocumentFactoryTest extends \PHPUnit\Framework\TestCase private $commentCreationArgumentsMock; /** - * @var CreditmemoInterface|\PHPUnit_Framework_MockObject_MockObject + * @var Order\Creditmemo|\PHPUnit_Framework_MockObject_MockObject */ private $creditmemoMock; @@ -121,7 +120,7 @@ public function setUp() $this->creditmemoItemCreationMock = $this->getMockBuilder(CreditmemoItemCreationInterface::class) ->disableOriginalConstructor() ->getMock(); - $this->creditmemoMock = $this->getMockBuilder(CreditmemoInterface::class) + $this->creditmemoMock = $this->getMockBuilder(Order\Creditmemo::class) ->disableOriginalConstructor() ->getMock(); $this->hydratorMock = $this->getMockBuilder(HydratorInterface::class) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoTest.php index b872a4ef9fba..4d8dd00ac65b 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoTest.php @@ -10,6 +10,7 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\Sales\Model\ResourceModel\Order\Creditmemo\Item\CollectionFactory; use Magento\Sales\Model\ResourceModel\Order\Creditmemo\Item\Collection as ItemCollection; +use Magento\Framework\App\Config\ScopeConfigInterface; /** * Class CreditmemoTest @@ -28,6 +29,11 @@ class CreditmemoTest extends \PHPUnit\Framework\TestCase */ protected $creditmemo; + /** + * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $scopeConfigMock; + /** * @var CollectionFactory|\PHPUnit_Framework_MockObject_MockObject */ @@ -36,6 +42,7 @@ class CreditmemoTest extends \PHPUnit\Framework\TestCase protected function setUp() { $this->orderFactory = $this->createPartialMock(\Magento\Sales\Model\OrderFactory::class, ['create']); + $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); $objectManagerHelper = new ObjectManagerHelper($this); $this->cmItemCollectionFactoryMock = $this->getMockBuilder( @@ -62,6 +69,7 @@ protected function setUp() 'commentCollectionFactory' => $this->createMock( \Magento\Sales\Model\ResourceModel\Order\Creditmemo\Comment\CollectionFactory::class ), + 'scopeConfig' => $this->scopeConfigMock ]; $this->creditmemo = $objectManagerHelper->getObject( \Magento\Sales\Model\Order\Creditmemo::class, @@ -109,7 +117,6 @@ public function testIsValidGrandTotalGrandTotalEmpty() public function testIsValidGrandTotalGrandTotal() { $this->creditmemo->setGrandTotal(0); - $this->creditmemo->getAllowZeroGrandTotal(true); $this->assertFalse($this->creditmemo->isValidGrandTotal()); } diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoSenderTest.php index 31bf84668923..1f074d7262f4 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoSenderTest.php @@ -7,6 +7,9 @@ use Magento\Sales\Model\Order\Email\Sender\CreditmemoSender; +/** + * Test for Magento\Sales\Model\Order\Email\Sender\CreditmemoSender class. + */ class CreditmemoSenderTest extends AbstractSenderTest { /** @@ -90,7 +93,7 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema $this->creditmemoMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with($emailSendingResult); $this->globalConfig->expects($this->once()) ->method('getValue') @@ -130,7 +133,7 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema ] ); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn($emailSendingResult); @@ -197,6 +200,8 @@ public function sendDataProvider() * @param bool $isVirtualOrder * @param int $formatCallCount * @param string|null $expectedShippingAddress + * + * @return void * @dataProvider sendVirtualOrderDataProvider */ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expectedShippingAddress) @@ -207,7 +212,7 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte $this->creditmemoMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with(false); $this->globalConfig->expects($this->once()) ->method('getValue') @@ -242,7 +247,7 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte ] ); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn(false); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceSenderTest.php index 9c54c716e420..d1aa5af53da4 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceSenderTest.php @@ -7,6 +7,9 @@ use Magento\Sales\Model\Order\Email\Sender\InvoiceSender; +/** + * Test for Magento\Sales\Model\Order\Email\Sender\InvoiceSender class. + */ class InvoiceSenderTest extends AbstractSenderTest { /** @@ -90,7 +93,7 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema $this->invoiceMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with($emailSendingResult); $this->globalConfig->expects($this->once()) ->method('getValue') @@ -136,7 +139,7 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema ] ); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn($emailSendingResult); @@ -212,7 +215,7 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte $this->invoiceMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with(false); $this->globalConfig->expects($this->once()) ->method('getValue') @@ -247,7 +250,7 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte ] ); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn(false); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/OrderSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/OrderSenderTest.php index 46c44c03b151..88053ea684ce 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/OrderSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/OrderSenderTest.php @@ -64,7 +64,7 @@ public function testSend($configValue, $forceSyncMode, $emailSendingResult, $sen $this->orderMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with($emailSendingResult); $this->globalConfig->expects($this->once()) ->method('getValue') @@ -72,7 +72,7 @@ public function testSend($configValue, $forceSyncMode, $emailSendingResult, $sen ->willReturn($configValue); if (!$configValue || $forceSyncMode) { - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn($emailSendingResult); @@ -118,7 +118,7 @@ public function testSend($configValue, $forceSyncMode, $emailSendingResult, $sen $this->orderMock->expects($this->once()) ->method('setEmailSent') - ->with(true); + ->with($emailSendingResult); $this->orderResourceMock->expects($this->once()) ->method('saveAttribute') @@ -210,7 +210,7 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte ->with('sales_email/general/async_sending') ->willReturn(false); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn(true); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentSenderTest.php index b1b18af63b59..2d7b42bccae5 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentSenderTest.php @@ -7,6 +7,9 @@ use Magento\Sales\Model\Order\Email\Sender\ShipmentSender; +/** + * Test for Magento\Sales\Model\Order\Email\Sender\ShipmentSender class. + */ class ShipmentSenderTest extends AbstractSenderTest { /** @@ -90,7 +93,7 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema $this->shipmentMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with($emailSendingResult); $this->globalConfig->expects($this->once()) ->method('getValue') @@ -136,7 +139,7 @@ public function testSend($configValue, $forceSyncMode, $customerNoteNotify, $ema ] ); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn($emailSendingResult); @@ -212,7 +215,7 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte $this->shipmentMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with(false); $this->globalConfig->expects($this->once()) ->method('getValue') @@ -247,7 +250,7 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte ] ); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn(false); $this->shipmentResourceMock->expects($this->once()) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/SenderBuilderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/SenderBuilderTest.php index 38209bb22aef..24cd54e3a46b 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 @@ -6,7 +6,6 @@ namespace Magento\Sales\Test\Unit\Model\Order\Email; -use Magento\Framework\Mail\Template\TransportBuilderByStore; use Magento\Sales\Model\Order\Email\SenderBuilder; class SenderBuilderTest extends \PHPUnit\Framework\TestCase @@ -36,11 +35,6 @@ class SenderBuilderTest extends \PHPUnit\Framework\TestCase */ private $storeMock; - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $transportBuilderByStore; - protected function setUp() { $templateId = 'test_template_id'; @@ -82,11 +76,10 @@ protected function setUp() 'setTemplateIdentifier', 'setTemplateOptions', 'setTemplateVars', + 'setFromByScope', ] ); - $this->transportBuilderByStore = $this->createMock(TransportBuilderByStore::class); - $this->templateContainerMock->expects($this->once()) ->method('getTemplateId') ->will($this->returnValue($templateId)); @@ -109,9 +102,9 @@ protected function setUp() $this->identityContainerMock->expects($this->once()) ->method('getEmailIdentity') ->will($this->returnValue($emailIdentity)); - $this->transportBuilderByStore->expects($this->once()) - ->method('setFromByStore') - ->with($this->equalTo($emailIdentity)); + $this->transportBuilder->expects($this->once()) + ->method('setFromByScope') + ->with($this->equalTo($emailIdentity), 1); $this->identityContainerMock->expects($this->once()) ->method('getEmailCopyTo') @@ -120,8 +113,7 @@ protected function setUp() $this->senderBuilder = new SenderBuilder( $this->templateContainerMock, $this->identityContainerMock, - $this->transportBuilder, - $this->transportBuilderByStore + $this->transportBuilder ); } @@ -129,6 +121,8 @@ public function testSend() { $customerName = 'test_name'; $customerEmail = 'test_email'; + $identity = 'email_identity_test'; + $transportMock = $this->createMock( \Magento\Sales\Test\Unit\Model\Order\Email\Stub\TransportInterfaceMock::class ); @@ -151,6 +145,9 @@ public function testSend() $this->storeMock->expects($this->once()) ->method('getId') ->willReturn(1); + $this->transportBuilder->expects($this->once()) + ->method('setFromByScope') + ->with($identity, 1); $this->transportBuilder->expects($this->once()) ->method('addTo') ->with($this->equalTo($customerEmail), $this->equalTo($customerName)); @@ -164,6 +161,7 @@ public function testSend() public function testSendCopyTo() { + $identity = 'email_identity_test'; $transportMock = $this->createMock( \Magento\Sales\Test\Unit\Model\Order\Email\Stub\TransportInterfaceMock::class ); @@ -177,6 +175,9 @@ public function testSendCopyTo() $this->transportBuilder->expects($this->once()) ->method('addTo') ->with($this->equalTo('example@mail.com')); + $this->transportBuilder->expects($this->once()) + ->method('setFromByScope') + ->with($identity, 1); $this->identityContainerMock->expects($this->once()) ->method('getStore') ->willReturn($this->storeMock); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Sender/EmailSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Sender/EmailSenderTest.php index 8a4e2920ba20..dcf689cf7d53 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Sender/EmailSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Sender/EmailSenderTest.php @@ -247,7 +247,7 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending $this->invoiceMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with($emailSendingResult); if (!$configValue || $forceSyncMode) { $transport = [ @@ -277,7 +277,7 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending ->method('setTemplateVars') ->with($transport->getData()); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn($emailSendingResult); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/ItemRepositoryTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/ItemRepositoryTest.php deleted file mode 100644 index 8be2c3c8612d..000000000000 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/ItemRepositoryTest.php +++ /dev/null @@ -1,366 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Sales\Test\Unit\Model\Order; - -use Magento\Sales\Model\Order\ItemRepository; - -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class ItemRepositoryTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Framework\DataObject\Factory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $objectFactory; - - /** - * @var \Magento\Sales\Model\ResourceModel\Metadata|\PHPUnit_Framework_MockObject_MockObject - */ - protected $metadata; - - /** - * @var \Magento\Sales\Api\Data\OrderItemSearchResultInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $searchResultFactory; - - /** - * @var \Magento\Catalog\Model\ProductOptionProcessorInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $productOptionProcessorMock; - - /** - * @var \Magento\Catalog\Model\ProductOptionFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $productOptionFactory; - - /** - * @var \Magento\Catalog\Api\Data\ProductOptionExtensionFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $extensionFactory; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $collectionProcessor; - - /** - * @var array - */ - protected $productOptionData = []; - - protected function setUp() - { - $this->objectFactory = $this->getMockBuilder(\Magento\Framework\DataObject\Factory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - - $this->metadata = $this->getMockBuilder(\Magento\Sales\Model\ResourceModel\Metadata::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->searchResultFactory = $this->getMockBuilder( - \Magento\Sales\Api\Data\OrderItemSearchResultInterfaceFactory::class - ) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - - $this->productOptionFactory = $this->getMockBuilder(\Magento\Catalog\Model\ProductOptionFactory::class) - ->setMethods([ - 'create', - ]) - ->disableOriginalConstructor() - ->getMock(); - - $this->collectionProcessor = $this->createMock( - \Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface::class - ); - - $this->extensionFactory = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductOptionExtensionFactory::class) - ->setMethods([ - 'create', - ]) - ->disableOriginalConstructor() - ->getMock(); - } - - /** - * @expectedException \Magento\Framework\Exception\InputException - * @expectedExceptionMessage An ID is needed. Set the ID and try again. - */ - public function testGetWithNoId() - { - $model = new ItemRepository( - $this->objectFactory, - $this->metadata, - $this->searchResultFactory, - $this->productOptionFactory, - $this->extensionFactory, - [], - $this->collectionProcessor - ); - - $model->get(null); - } - - /** - * @expectedException \Magento\Framework\Exception\NoSuchEntityException - * @expectedExceptionMessage The entity that was requested doesn't exist. Verify the entity and try again. - */ - public function testGetEmptyEntity() - { - $orderItemId = 1; - - $orderItemMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) - ->disableOriginalConstructor() - ->getMock(); - $orderItemMock->expects($this->once()) - ->method('load') - ->with($orderItemId) - ->willReturn($orderItemMock); - $orderItemMock->expects($this->once()) - ->method('getItemId') - ->willReturn(null); - - $this->metadata->expects($this->once()) - ->method('getNewInstance') - ->willReturn($orderItemMock); - - $model = new ItemRepository( - $this->objectFactory, - $this->metadata, - $this->searchResultFactory, - $this->productOptionFactory, - $this->extensionFactory, - [], - $this->collectionProcessor - ); - - $model->get($orderItemId); - } - - public function testGet() - { - $orderItemId = 1; - $productType = 'configurable'; - - $this->productOptionData = ['option1' => 'value1']; - - $this->getProductOptionExtensionMock(); - $productOption = $this->getProductOptionMock(); - $orderItemMock = $this->getOrderMock($productType, $productOption); - - $orderItemMock->expects($this->once()) - ->method('load') - ->with($orderItemId) - ->willReturn($orderItemMock); - $orderItemMock->expects($this->once()) - ->method('getItemId') - ->willReturn($orderItemId); - - $this->metadata->expects($this->once()) - ->method('getNewInstance') - ->willReturn($orderItemMock); - - $model = $this->getModel($orderItemMock, $productType); - $this->assertSame($orderItemMock, $model->get($orderItemId)); - - // Assert already registered - $this->assertSame($orderItemMock, $model->get($orderItemId)); - } - - public function testGetList() - { - $productType = 'configurable'; - $this->productOptionData = ['option1' => 'value1']; - $searchCriteriaMock = $this->getMockBuilder(\Magento\Framework\Api\SearchCriteria::class) - ->disableOriginalConstructor() - ->getMock(); - $this->getProductOptionExtensionMock(); - $productOption = $this->getProductOptionMock(); - $orderItemMock = $this->getOrderMock($productType, $productOption); - - $searchResultMock = $this->getMockBuilder(\Magento\Sales\Model\ResourceModel\Order\Item\Collection::class) - ->disableOriginalConstructor() - ->getMock(); - $searchResultMock->expects($this->once()) - ->method('getItems') - ->willReturn([$orderItemMock]); - - $this->searchResultFactory->expects($this->once()) - ->method('create') - ->willReturn($searchResultMock); - - $model = $this->getModel($orderItemMock, $productType); - $this->assertSame($searchResultMock, $model->getList($searchCriteriaMock)); - } - - public function testDeleteById() - { - $orderItemId = 1; - $productType = 'configurable'; - - $requestMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) - ->disableOriginalConstructor() - ->getMock(); - - $orderItemMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) - ->disableOriginalConstructor() - ->getMock(); - $orderItemMock->expects($this->once()) - ->method('load') - ->with($orderItemId) - ->willReturn($orderItemMock); - $orderItemMock->expects($this->once()) - ->method('getItemId') - ->willReturn($orderItemId); - $orderItemMock->expects($this->once()) - ->method('getProductType') - ->willReturn($productType); - $orderItemMock->expects($this->once()) - ->method('getBuyRequest') - ->willReturn($requestMock); - - $orderItemResourceMock = $this->getMockBuilder(\Magento\Framework\Model\ResourceModel\Db\AbstractDb::class) - ->disableOriginalConstructor() - ->getMock(); - $orderItemResourceMock->expects($this->once()) - ->method('delete') - ->with($orderItemMock) - ->willReturnSelf(); - - $this->metadata->expects($this->once()) - ->method('getNewInstance') - ->willReturn($orderItemMock); - $this->metadata->expects($this->exactly(1)) - ->method('getMapper') - ->willReturn($orderItemResourceMock); - - $model = $this->getModel($orderItemMock, $productType); - $this->assertTrue($model->deleteById($orderItemId)); - } - - /** - * @param \PHPUnit_Framework_MockObject_MockObject $orderItemMock - * @param string $productType - * @param array $data - * @return ItemRepository - */ - protected function getModel( - \PHPUnit_Framework_MockObject_MockObject $orderItemMock, - $productType, - array $data = [] - ) { - $requestMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) - ->disableOriginalConstructor() - ->getMock(); - - $requestUpdateMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) - ->disableOriginalConstructor() - ->getMock(); - $requestUpdateMock->expects($this->any()) - ->method('getData') - ->willReturn($data); - - $this->productOptionProcessorMock = $this->getMockBuilder( - \Magento\Catalog\Model\ProductOptionProcessorInterface::class - ) - ->getMockForAbstractClass(); - $this->productOptionProcessorMock->expects($this->any()) - ->method('convertToProductOption') - ->with($requestMock) - ->willReturn($this->productOptionData); - $this->productOptionProcessorMock->expects($this->any()) - ->method('convertToBuyRequest') - ->with($orderItemMock) - ->willReturn($requestUpdateMock); - - $model = new ItemRepository( - $this->objectFactory, - $this->metadata, - $this->searchResultFactory, - $this->productOptionFactory, - $this->extensionFactory, - [ - $productType => $this->productOptionProcessorMock, - 'custom_options' => $this->productOptionProcessorMock - ], - $this->collectionProcessor - ); - return $model; - } - - /** - * @param string $productType - * @param \PHPUnit_Framework_MockObject_MockObject $productOption - * @return \PHPUnit_Framework_MockObject_MockObject - */ - protected function getOrderMock($productType, $productOption) - { - $requestMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) - ->disableOriginalConstructor() - ->getMock(); - - $orderItemMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) - ->disableOriginalConstructor() - ->getMock(); - $orderItemMock->expects($this->once()) - ->method('getProductType') - ->willReturn($productType); - $orderItemMock->expects($this->once()) - ->method('getBuyRequest') - ->willReturn($requestMock); - $orderItemMock->expects($this->any()) - ->method('getProductOption') - ->willReturn(null); - $orderItemMock->expects($this->any()) - ->method('setProductOption') - ->with($productOption) - ->willReturnSelf(); - - return $orderItemMock; - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject - */ - protected function getProductOptionMock() - { - $productOption = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductOptionInterface::class) - ->getMockForAbstractClass(); - $productOption->expects($this->any()) - ->method('getExtensionAttributes') - ->willReturn(null); - - $this->productOptionFactory->expects($this->any()) - ->method('create') - ->willReturn($productOption); - - return $productOption; - } - - protected function getProductOptionExtensionMock() - { - $productOptionExtension = $this->getMockBuilder( - \Magento\Catalog\Api\Data\ProductOptionExtensionInterface::class - ) - ->setMethods([ - 'setData', - ]) - ->getMockForAbstractClass(); - $productOptionExtension->expects($this->any()) - ->method('setData') - ->with(key($this->productOptionData), current($this->productOptionData)) - ->willReturnSelf(); - - $this->extensionFactory->expects($this->any()) - ->method('create') - ->willReturn($productOptionExtension); - } -} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/PaymentTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/PaymentTest.php index 30b584b8c4eb..9d0f10a30e6e 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/PaymentTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/PaymentTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Sales\Test\Unit\Model\Order; use Magento\Framework\Model\Context; @@ -1526,7 +1527,7 @@ public function testRefund() $this->orderStateResolver->expects($this->once())->method('getStateForOrder') ->with($this->order) ->willReturn(Order::STATE_CLOSED); - $this->mockGetDefaultStatus(Order::STATE_CLOSED, $status); + $this->mockGetDefaultStatus(Order::STATE_CLOSED, $status, ['first, second']); $this->assertOrderUpdated(Order::STATE_PROCESSING, $status, $message); static::assertSame($this->payment, $this->payment->refund($this->creditMemoMock)); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php index 94347e8b32d5..391e99ba6f83 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php @@ -249,7 +249,7 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending $this->shipmentMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with($emailSendingResult); if (!$configValue || $forceSyncMode) { $transport = [ @@ -279,7 +279,7 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending ->method('setTemplateVars') ->with($transport->getData()); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn($emailSendingResult); diff --git a/app/code/Magento/Sales/Test/Unit/Model/OrderRepositoryTest.php b/app/code/Magento/Sales/Test/Unit/Model/OrderRepositoryTest.php index 3df667094f2a..7f0c0639d21f 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/OrderRepositoryTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/OrderRepositoryTest.php @@ -9,6 +9,8 @@ use Magento\Sales\Api\Data\OrderInterface; use Magento\Sales\Api\Data\OrderSearchResultInterfaceFactory as SearchResultFactory; use Magento\Sales\Model\ResourceModel\Metadata; +use Magento\Tax\Api\OrderTaxManagementInterface; +use Magento\Payment\Api\Data\PaymentAdditionalInfoInterfaceFactory; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -40,8 +42,20 @@ class OrderRepositoryTest extends \PHPUnit\Framework\TestCase */ private $collectionProcessor; + /** + * @var OrderTaxManagementInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $orderTaxManagementMock; + + /** + * @var PaymentAdditionalInfoInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $paymentAdditionalInfoFactory; + /** * Setup the test + * + * @return void */ protected function setUp() { @@ -58,34 +72,67 @@ protected function setUp() $orderExtensionFactoryMock = $this->getMockBuilder(\Magento\Sales\Api\Data\OrderExtensionFactory::class) ->disableOriginalConstructor() ->getMock(); + $this->orderTaxManagementMock = $this->getMockBuilder(OrderTaxManagementInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->paymentAdditionalInfoFactory = $this->getMockBuilder(PaymentAdditionalInfoInterfaceFactory::class) + ->disableOriginalConstructor()->setMethods(['create'])->getMockForAbstractClass(); $this->orderRepository = $this->objectManager->getObject( \Magento\Sales\Model\OrderRepository::class, [ 'metadata' => $this->metadata, 'searchResultFactory' => $this->searchResultFactory, 'collectionProcessor' => $this->collectionProcessor, - 'orderExtensionFactory' => $orderExtensionFactoryMock + 'orderExtensionFactory' => $orderExtensionFactoryMock, + 'orderTaxManagement' => $this->orderTaxManagementMock, + 'paymentAdditionalInfoFactory' => $this->paymentAdditionalInfoFactory ] ); } + /** + * Test for method getList. + * + * @return void + */ public function testGetList() { $searchCriteriaMock = $this->createMock(\Magento\Framework\Api\SearchCriteria::class); $collectionMock = $this->createMock(\Magento\Sales\Model\ResourceModel\Order\Collection::class); - $itemsMock = $this->getMockBuilder(OrderInterface::class)->disableOriginalConstructor()->getMock(); + $itemsMock = $this->getMockBuilder(OrderInterface::class)->disableOriginalConstructor() + ->getMockForAbstractClass(); + $orderTaxDetailsMock = $this->getMockBuilder(\Magento\Tax\Api\Data\OrderTaxDetailsInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getAppliedTaxes', 'getItems'])->getMockForAbstractClass(); + $paymentMock = $this->getMockBuilder(\Magento\Sales\Api\Data\OrderPaymentInterface::class) + ->disableOriginalConstructor()->getMockForAbstractClass(); + $paymentAdditionalInfo = $this->getMockBuilder(\Magento\Payment\Api\Data\PaymentAdditionalInfoInterface::class) + ->disableOriginalConstructor()->setMethods(['setKey', 'setValue'])->getMockForAbstractClass(); $extensionAttributes = $this->createPartialMock( \Magento\Sales\Api\Data\OrderExtension::class, - ['getShippingAssignments'] + [ + 'getShippingAssignments', 'setShippingAssignments', 'setConvertingFromQuote', + 'setAppliedTaxes', 'setItemAppliedTaxes', 'setPaymentAdditionalInfo' + ] ); $shippingAssignmentBuilder = $this->createMock( \Magento\Sales\Model\Order\ShippingAssignmentBuilder::class ); + $itemsMock->expects($this->atLeastOnce())->method('getEntityId')->willReturn(1); $this->collectionProcessor->expects($this->once()) ->method('process') ->with($searchCriteriaMock, $collectionMock); - $itemsMock->expects($this->once())->method('getExtensionAttributes')->willReturn($extensionAttributes); + $itemsMock->expects($this->atLeastOnce())->method('getExtensionAttributes')->willReturn($extensionAttributes); + $itemsMock->expects($this->atleastOnce())->method('getPayment')->willReturn($paymentMock); + $paymentMock->expects($this->atLeastOnce())->method('getAdditionalInformation') + ->willReturn(['method' => 'checkmo']); + $this->paymentAdditionalInfoFactory->expects($this->atLeastOnce())->method('create') + ->willReturn($paymentAdditionalInfo); + $paymentAdditionalInfo->expects($this->atLeastOnce())->method('setKey')->willReturnSelf(); + $paymentAdditionalInfo->expects($this->atLeastOnce())->method('setValue')->willReturnSelf(); + $this->orderTaxManagementMock->expects($this->atLeastOnce())->method('getOrderTaxDetails') + ->willReturn($orderTaxDetailsMock); $extensionAttributes->expects($this->any()) ->method('getShippingAssignments') ->willReturn($shippingAssignmentBuilder); @@ -96,6 +143,11 @@ public function testGetList() $this->assertEquals($collectionMock, $this->orderRepository->getList($searchCriteriaMock)); } + /** + * Test for method save. + * + * @return void + */ public function testSave() { $mapperMock = $this->getMockBuilder(\Magento\Sales\Model\ResourceModel\Order::class) diff --git a/app/code/Magento/Sales/Test/Unit/Model/OrderTest.php b/app/code/Magento/Sales/Test/Unit/Model/OrderTest.php index 7f6363346872..705d2face230 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/OrderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/OrderTest.php @@ -11,12 +11,19 @@ use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\Sales\Api\Data\OrderInterface; use Magento\Sales\Model\Order; +use Magento\Sales\Model\ResourceModel\Order\Item\Collection; use Magento\Sales\Model\ResourceModel\Order\Status\History\CollectionFactory as HistoryCollectionFactory; +use Magento\Sales\Api\OrderItemRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Api\SearchCriteria; +use Magento\Sales\Api\Data\OrderItemSearchResultInterface; /** * Test class for \Magento\Sales\Model\Order * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyPublicMethods) + * @SuppressWarnings(PHPMD.ExcessivePublicCount) */ class OrderTest extends \PHPUnit\Framework\TestCase { @@ -85,6 +92,16 @@ class OrderTest extends \PHPUnit\Framework\TestCase */ private $timezone; + /** + * @var OrderItemRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $itemRepository; + + /** + * @var SearchCriteriaBuilder|\PHPUnit_Framework_MockObject_MockObject + */ + private $searchCriteriaBuilder; + protected function setUp() { $helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -114,7 +131,7 @@ protected function setUp() 'getParentItemId', 'getQuoteItemId', 'getLockedDoInvoice', - 'getProductId' + 'getProductId', ]); $this->salesOrderCollectionMock = $this->getMockBuilder( \Magento\Sales\Model\ResourceModel\Order\Collection::class @@ -142,6 +159,15 @@ protected function setUp() $this->eventManager = $this->createMock(\Magento\Framework\Event\Manager::class); $context = $this->createPartialMock(\Magento\Framework\Model\Context::class, ['getEventDispatcher']); $context->expects($this->any())->method('getEventDispatcher')->willReturn($this->eventManager); + + $this->itemRepository = $this->getMockBuilder(OrderItemRepositoryInterface::class) + ->setMethods(['getList']) + ->disableOriginalConstructor()->getMockForAbstractClass(); + + $this->searchCriteriaBuilder = $this->getMockBuilder(SearchCriteriaBuilder::class) + ->setMethods(['addFilter', 'create']) + ->disableOriginalConstructor()->getMockForAbstractClass(); + $this->order = $helper->getObject( \Magento\Sales\Model\Order::class, [ @@ -155,37 +181,80 @@ protected function setUp() 'productListFactory' => $this->productCollectionFactoryMock, 'localeResolver' => $this->localeResolver, 'timezone' => $this->timezone, + 'itemRepository' => $this->itemRepository, + 'searchCriteriaBuilder' => $this->searchCriteriaBuilder ] ); } - public function testGetItemById() + /** + * Test testGetItems method. + */ + public function testGetItems() { - $realOrderItemId = 1; - $fakeOrderItemId = 2; + $orderItems = [$this->item]; - $orderItem = $this->createMock(\Magento\Sales\Model\Order\Item::class); + $this->searchCriteriaBuilder->expects($this->once())->method('addFilter')->willReturnSelf(); + + $searchCriteria = $this->getMockBuilder(SearchCriteria::class) + ->disableOriginalConstructor()->getMockForAbstractClass(); + $this->searchCriteriaBuilder->expects($this->once())->method('create')->willReturn($searchCriteria); + + $itemsCollection = $this->getMockBuilder(OrderItemSearchResultInterface::class) + ->setMethods(['getItems']) + ->disableOriginalConstructor()->getMockForAbstractClass(); + $itemsCollection->expects($this->once())->method('getItems')->willReturn($orderItems); + $this->itemRepository->expects($this->once())->method('getList')->willReturn($itemsCollection); + + $this->assertEquals($orderItems, $this->order->getItems()); + } + /** + * Prepare order item mock. + * + * @param int $orderId + * @return void + */ + private function prepareOrderItem(int $orderId = 0) + { $this->order->setData( \Magento\Sales\Api\Data\OrderInterface::ITEMS, [ - $realOrderItemId => $orderItem + $orderId => $this->item ] ); + } - $this->assertEquals($orderItem, $this->order->getItemById($realOrderItemId)); + /** + * Test GetItemById method. + * + * @return void + */ + public function testGetItemById() + { + $realOrderItemId = 1; + $fakeOrderItemId = 2; + + $this->prepareOrderItem($realOrderItemId); + + $this->assertEquals($this->item, $this->order->getItemById($realOrderItemId)); $this->assertEquals(null, $this->order->getItemById($fakeOrderItemId)); } /** + * Test GetItemByQuoteItemId method. + * * @param int|null $gettingQuoteItemId * @param int|null $quoteItemId * @param string|null $result * * @dataProvider dataProviderGetItemByQuoteItemId + * @return void */ public function testGetItemByQuoteItemId($gettingQuoteItemId, $quoteItemId, $result) { + $this->prepareOrderItem(); + $this->item->expects($this->any()) ->method('getQuoteItemId') ->willReturn($gettingQuoteItemId); @@ -210,14 +279,19 @@ public function dataProviderGetItemByQuoteItemId() } /** + * Test getAllVisibleItems method. + * * @param bool $isDeleted * @param int|null $parentItemId * @param array $result * * @dataProvider dataProviderGetAllVisibleItems + * @return void */ public function testGetAllVisibleItems($isDeleted, $parentItemId, array $result) { + $this->prepareOrderItem(); + $this->item->expects($this->once()) ->method('isDeleted') ->willReturn($isDeleted); @@ -261,8 +335,15 @@ public function testCanCancelIsPaymentReview() $this->assertFalse($this->order->canCancel()); } + /** + * Test CanInvoice method. + * + * @return void + */ public function testCanInvoice() { + $this->prepareOrderItem(); + $this->item->expects($this->any()) ->method('getQtyToInvoice') ->willReturn(42); @@ -302,8 +383,15 @@ public function testCanNotInvoiceWhenActionInvoiceFlagIsFalse() $this->assertFalse($this->order->canInvoice()); } + /** + * Test CanNotInvoice method when invoice is locked. + * + * @return void + */ public function testCanNotInvoiceWhenLockedInvoice() { + $this->prepareOrderItem(); + $this->item->expects($this->any()) ->method('getQtyToInvoice') ->willReturn(42); @@ -313,8 +401,15 @@ public function testCanNotInvoiceWhenLockedInvoice() $this->assertFalse($this->order->canInvoice()); } + /** + * Test CanNotInvoice method when didn't have qty to invoice. + * + * @return void + */ public function testCanNotInvoiceWhenDidNotHaveQtyToInvoice() { + $this->prepareOrderItem(); + $this->item->expects($this->any()) ->method('getQtyToInvoice') ->willReturn(0); @@ -327,6 +422,7 @@ public function testCanNotInvoiceWhenDidNotHaveQtyToInvoice() public function testCanCreditMemo() { $totalPaid = 10; + $this->prepareOrderItem(); $this->order->setTotalPaid($totalPaid); $this->priceCurrency->expects($this->once())->method('round')->with($totalPaid)->willReturnArgument(0); $this->assertTrue($this->order->canCreditmemo()); @@ -335,6 +431,7 @@ public function testCanCreditMemo() public function testCanNotCreditMemoWithTotalNull() { $totalPaid = 0; + $this->prepareOrderItem(); $this->order->setTotalPaid($totalPaid); $this->priceCurrency->expects($this->once())->method('round')->with($totalPaid)->willReturnArgument(0); $this->assertFalse($this->order->canCreditmemo()); @@ -346,6 +443,7 @@ public function testCanNotCreditMemoWithAdjustmentNegative() $adjustmentNegative = 10; $totalRefunded = 90; + $this->prepareOrderItem(); $this->order->setTotalPaid($totalPaid); $this->order->setTotalRefunded($totalRefunded); $this->order->setAdjustmentNegative($adjustmentNegative); @@ -360,6 +458,7 @@ public function testCanCreditMemoWithAdjustmentNegativeLowerThanTotalPaid() $adjustmentNegative = 9; $totalRefunded = 90; + $this->prepareOrderItem(); $this->order->setTotalPaid($totalPaid); $this->order->setTotalRefunded($totalRefunded); $this->order->setAdjustmentNegative($adjustmentNegative); @@ -584,8 +683,15 @@ public function testCanCancelCanReviewPayment() $this->assertFalse($this->order->canCancel()); } + /** + * Test CanCancelAllInvoiced method. + * + * @return void + */ public function testCanCancelAllInvoiced() { + $this->prepareOrderItem(); + $paymentMock = $this->getMockBuilder(\Magento\Sales\Model\ResourceModel\Order\Payment::class) ->disableOriginalConstructor() ->setMethods(['isDeleted', 'canReviewPayment', 'canFetchTransactionInfo', '__wakeUp']) @@ -645,11 +751,16 @@ public function testCanCancelState() } /** + * Test CanCancelActionFlag method. + * * @param bool $cancelActionFlag * @dataProvider dataProviderActionFlag + * @return void */ public function testCanCancelActionFlag($cancelActionFlag) { + $this->prepareOrderItem(); + $paymentMock = $this->getMockBuilder(\Magento\Sales\Model\ResourceModel\Order\Payment::class) ->disableOriginalConstructor() ->setMethods(['isDeleted', 'canReviewPayment', 'canFetchTransactionInfo', '__wakeUp']) @@ -1082,9 +1193,6 @@ public function testGetCreatedAtFormattedUsesCorrectLocale() $this->order->getCreatedAtFormatted(\IntlDateFormatter::SHORT); } - /** - * @return array - */ public function notInvoicingStatesProvider() { return [ @@ -1094,9 +1202,6 @@ public function notInvoicingStatesProvider() ]; } - /** - * @return array - */ public function canNotCreditMemoStatesProvider() { return [ diff --git a/app/code/Magento/Sales/Test/Unit/Model/RefundOrderTest.php b/app/code/Magento/Sales/Test/Unit/Model/RefundOrderTest.php index c95b56d81d6f..1ffeaa053cc2 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/RefundOrderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/RefundOrderTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Sales\Test\Unit\Model; use Magento\Framework\App\ResourceConnection; @@ -245,9 +246,9 @@ public function testOrderCreditmemo($orderId, $notify, $appendComment) ->method('setState') ->with(Order::STATE_CLOSED) ->willReturnSelf(); - $this->orderMock->expects($this->once()) - ->method('getState') - ->willReturn(Order::STATE_CLOSED); + $this->configMock->expects($this->once()) + ->method('getStateStatuses') + ->willReturn(['first, second']); $this->configMock->expects($this->once()) ->method('getStateDefaultStatus') ->with(Order::STATE_CLOSED) diff --git a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/StateTest.php b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/StateTest.php index e120d613e323..99a411c43c24 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/StateTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/StateTest.php @@ -24,7 +24,9 @@ class StateTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->orderMock = $this->createPartialMock(\Magento\Sales\Model\Order::class, [ + $this->orderMock = $this->createPartialMock( + \Magento\Sales\Model\Order::class, + [ '__wakeup', 'getId', 'hasCustomerNoteNotify', @@ -35,13 +37,12 @@ protected function setUp() 'canShip', 'getBaseGrandTotal', 'canCreditmemo', - 'getState', - 'setState', 'getTotalRefunded', 'hasForcedCanCreditmemo', 'getIsInProcess', 'getConfig', - ]); + ] + ); $this->orderMock->expects($this->any()) ->method('getConfig') ->willReturnSelf(); @@ -53,127 +54,88 @@ protected function setUp() } /** - * test check order - order without id - */ - public function testCheckOrderEmpty() - { - $this->orderMock->expects($this->once()) - ->method('getBaseGrandTotal') - ->willReturn(100); - $this->orderMock->expects($this->never()) - ->method('setState'); - - $this->state->check($this->orderMock); - } - - /** - * test check order - set state complete + * @param bool $isCanceled + * @param bool $canUnhold + * @param bool $canInvoice + * @param bool $canShip + * @param int $callCanSkipNum + * @param bool $canCreditmemo + * @param int $callCanCreditmemoNum + * @param string $currentState + * @param string $expectedState + * @param int $callSetStateNum + * @dataProvider stateCheckDataProvider + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ - public function testCheckSetStateComplete() - { + public function testCheck( + bool $canCreditmemo, + int $callCanCreditmemoNum, + bool $canShip, + int $callCanSkipNum, + string $currentState, + string $expectedState = '', + bool $isInProcess = false, + int $callGetIsInProcessNum = 0, + bool $isCanceled = false, + bool $canUnhold = false, + bool $canInvoice = false + ) { + $this->orderMock->setState($currentState); $this->orderMock->expects($this->any()) - ->method('getId') - ->will($this->returnValue(1)); - $this->orderMock->expects($this->once()) ->method('isCanceled') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->once()) - ->method('canUnhold') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->once()) - ->method('canInvoice') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->once()) - ->method('canShip') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->once()) - ->method('getBaseGrandTotal') - ->will($this->returnValue(100)); - $this->orderMock->expects($this->once()) - ->method('canCreditmemo') - ->will($this->returnValue(true)); - $this->orderMock->expects($this->exactly(2)) - ->method('getState') - ->will($this->returnValue(Order::STATE_PROCESSING)); - $this->orderMock->expects($this->once()) - ->method('setState') - ->with(Order::STATE_COMPLETE) - ->will($this->returnSelf()); - $this->assertEquals($this->state, $this->state->check($this->orderMock)); - } - - /** - * test check order - set state closed - */ - public function testCheckSetStateClosed() - { + ->willReturn($isCanceled); $this->orderMock->expects($this->any()) - ->method('getId') - ->will($this->returnValue(1)); - $this->orderMock->expects($this->once()) - ->method('isCanceled') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->once()) ->method('canUnhold') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->once()) + ->willReturn($canUnhold); + $this->orderMock->expects($this->any()) ->method('canInvoice') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->once()) + ->willReturn($canInvoice); + $this->orderMock->expects($this->exactly($callCanSkipNum)) ->method('canShip') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->once()) - ->method('getBaseGrandTotal') - ->will($this->returnValue(100)); - $this->orderMock->expects($this->once()) + ->willReturn($canShip); + $this->orderMock->expects($this->exactly($callCanCreditmemoNum)) ->method('canCreditmemo') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->exactly(2)) - ->method('getTotalRefunded') - ->will($this->returnValue(null)); - $this->orderMock->expects($this->once()) - ->method('hasForcedCanCreditmemo') - ->will($this->returnValue(true)); - $this->orderMock->expects($this->exactly(2)) - ->method('getState') - ->will($this->returnValue(Order::STATE_PROCESSING)); - $this->orderMock->expects($this->once()) - ->method('setState') - ->with(Order::STATE_CLOSED) - ->will($this->returnSelf()); - $this->assertEquals($this->state, $this->state->check($this->orderMock)); + ->willReturn($canCreditmemo); + $this->orderMock->expects($this->exactly($callGetIsInProcessNum)) + ->method('getIsInProcess') + ->willReturn($isInProcess); + $this->state->check($this->orderMock); + $this->assertEquals($expectedState, $this->orderMock->getState()); } - /** - * test check order - set state processing - */ - public function testCheckSetStateProcessing() + public function stateCheckDataProvider() { - $this->orderMock->expects($this->any()) - ->method('getId') - ->will($this->returnValue(1)); - $this->orderMock->expects($this->once()) - ->method('isCanceled') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->once()) - ->method('canUnhold') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->once()) - ->method('canInvoice') - ->will($this->returnValue(false)); - $this->orderMock->expects($this->once()) - ->method('canShip') - ->will($this->returnValue(true)); - $this->orderMock->expects($this->once()) - ->method('getState') - ->will($this->returnValue(Order::STATE_NEW)); - $this->orderMock->expects($this->once()) - ->method('getIsInProcess') - ->will($this->returnValue(true)); - $this->orderMock->expects($this->once()) - ->method('setState') - ->with(Order::STATE_PROCESSING) - ->will($this->returnSelf()); - $this->assertEquals($this->state, $this->state->check($this->orderMock)); + return [ + 'processing - !canCreditmemo!canShip -> closed' => + [false, 1, false, 1, Order::STATE_PROCESSING, Order::STATE_CLOSED], + 'complete - !canCreditmemo,!canShip -> closed' => + [false, 1, false, 1, Order::STATE_COMPLETE, Order::STATE_CLOSED], + 'processing - !canCreditmemo,canShip -> processing' => + [false, 1, true, 2, Order::STATE_PROCESSING, Order::STATE_PROCESSING], + 'complete - !canCreditmemo,canShip -> complete' => + [false, 1, true, 1, Order::STATE_COMPLETE, Order::STATE_COMPLETE], + 'processing - canCreditmemo,!canShip -> complete' => + [true, 1, false, 1, Order::STATE_PROCESSING, Order::STATE_COMPLETE], + 'complete - canCreditmemo,!canShip -> complete' => + [true, 1, false, 0, Order::STATE_COMPLETE, Order::STATE_COMPLETE], + 'processing - canCreditmemo, canShip -> processing' => + [true, 1, true, 1, Order::STATE_PROCESSING, Order::STATE_PROCESSING], + 'complete - canCreditmemo, canShip -> complete' => + [true, 1, true, 0, Order::STATE_COMPLETE, Order::STATE_COMPLETE], + 'new - canCreditmemo, canShip, IsInProcess -> processing' => + [true, 1, true, 1, Order::STATE_NEW, Order::STATE_PROCESSING, true, 1], + 'new - canCreditmemo, !canShip, IsInProcess -> processing' => + [true, 1, false, 1, Order::STATE_NEW, Order::STATE_COMPLETE, true, 1], + 'new - canCreditmemo, canShip, !IsInProcess -> new' => + [true, 0, true, 0, Order::STATE_NEW, Order::STATE_NEW, false, 1], + 'hold - canUnhold -> hold' => + [true, 0, true, 0, Order::STATE_HOLDED, Order::STATE_HOLDED, false, 0, false, true], + 'payment_review - canUnhold -> payment_review' => + [true, 0, true, 0, Order::STATE_PAYMENT_REVIEW, Order::STATE_PAYMENT_REVIEW, false, 0, false, true], + 'pending_payment - canUnhold -> pending_payment' => + [true, 0, true, 0, Order::STATE_PENDING_PAYMENT, Order::STATE_PENDING_PAYMENT, false, 0, false, true], + 'cancelled - isCanceled -> cancelled' => + [true, 0, true, 0, Order::STATE_HOLDED, Order::STATE_HOLDED, false, 0, true], + ]; } } diff --git a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Shipment/RelationTest.php b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Shipment/RelationTest.php index a7a615fb0f50..530306d77d3e 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Shipment/RelationTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Shipment/RelationTest.php @@ -3,145 +3,125 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Sales\Test\Unit\Model\ResourceModel\Order\Shipment; +use Magento\Sales\Model\Order\Shipment; +use Magento\Sales\Model\Order\Shipment\Comment as CommentEntity; +use Magento\Sales\Model\Order\Shipment\Item as ItemEntity; +use Magento\Sales\Model\Order\Shipment\Track as TrackEntity; +use Magento\Sales\Model\ResourceModel\Order\Shipment\Comment; +use Magento\Sales\Model\ResourceModel\Order\Shipment\Item; +use Magento\Sales\Model\ResourceModel\Order\Shipment\Relation; +use Magento\Sales\Model\ResourceModel\Order\Shipment\Track; +use PHPUnit\Framework\MockObject\MockObject; + /** * Class RelationTest */ class RelationTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Sales\Model\ResourceModel\Order\Shipment\Relation + * @var Relation */ - protected $relationProcessor; + private $relationProcessor; /** - * @var \Magento\Sales\Model\ResourceModel\Order\Shipment\Item|\PHPUnit_Framework_MockObject_MockObject + * @var Item|MockObject */ - protected $itemResourceMock; + private $itemResource; /** - * @var \Magento\Sales\Model\ResourceModel\Order\Shipment\Track|\PHPUnit_Framework_MockObject_MockObject + * @var Track|MockObject */ - protected $trackResourceMock; + private $trackResource; /** - * @var \Magento\Sales\Model\ResourceModel\Order\Shipment\Comment|\PHPUnit_Framework_MockObject_MockObject + * @var Comment|MockObject */ - protected $commentResourceMock; + private $commentResource; /** - * @var \Magento\Sales\Model\Order\Shipment\Comment|\PHPUnit_Framework_MockObject_MockObject + * @var CommentEntity|MockObject */ - protected $commentMock; + private $comment; /** - * @var \Magento\Sales\Model\Order\Shipment\Track|\PHPUnit_Framework_MockObject_MockObject + * @var TrackEntity|MockObject */ - protected $trackMock; + private $track; /** - * @var \Magento\Sales\Model\Order\Shipment|\PHPUnit_Framework_MockObject_MockObject + * @var Shipment|MockObject */ - protected $shipmentMock; + private $shipment; /** - * @var \Magento\Sales\Model\Order\Shipment\Item|\PHPUnit_Framework_MockObject_MockObject + * @var ItemEntity|MockObject */ - protected $itemMock; + private $item; - protected function setUp() + /** + * @inheritdoc + */ + protected function setUp(): void { - $this->itemResourceMock = $this->getMockBuilder(\Magento\Sales\Model\ResourceModel\Order\Shipment\Item::class) + $this->itemResource = $this->getMockBuilder(Item::class) ->disableOriginalConstructor() - ->setMethods( - [ - 'save' - ] - ) ->getMock(); - $this->commentResourceMock = $this->getMockBuilder( - \Magento\Sales\Model\ResourceModel\Order\Shipment\Comment::class - ) + $this->commentResource = $this->getMockBuilder(Comment::class) ->disableOriginalConstructor() - ->setMethods( - [ - 'save' - ] - ) ->getMock(); - $this->trackResourceMock = $this->getMockBuilder(\Magento\Sales\Model\ResourceModel\Order\Shipment\Track::class) + $this->trackResource = $this->getMockBuilder(Track::class) ->disableOriginalConstructor() - ->setMethods( - [ - 'save' - ] - ) ->getMock(); - $this->shipmentMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Shipment::class) + $this->shipment = $this->getMockBuilder(Shipment::class) ->disableOriginalConstructor() - ->setMethods( - [ - 'getId', - 'getItems', - 'getTracks', - 'getComments', - 'getTracksCollection', - ] - ) ->getMock(); - $this->itemMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) + $this->item = $this->getMockBuilder(ItemEntity::class) ->disableOriginalConstructor() - ->setMethods( - [ - 'setParentId' - ] - ) ->getMock(); - $this->trackMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Shipment\Track::class) + $this->track = $this->getMockBuilder(TrackEntity::class) ->disableOriginalConstructor() ->getMock(); - $this->commentMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Shipment::class) + $this->comment = $this->getMockBuilder(Shipment::class) ->disableOriginalConstructor() ->getMock(); - $this->relationProcessor = new \Magento\Sales\Model\ResourceModel\Order\Shipment\Relation( - $this->itemResourceMock, - $this->trackResourceMock, - $this->commentResourceMock + $this->relationProcessor = new Relation( + $this->itemResource, + $this->trackResource, + $this->commentResource ); } - public function testProcessRelations() + /** + * Checks saving shipment relations. + * + * @throws \Exception + */ + public function testProcessRelations(): void { - $this->shipmentMock->expects($this->exactly(3)) - ->method('getId') + $this->shipment->method('getId') ->willReturn('shipment-id-value'); - $this->shipmentMock->expects($this->exactly(2)) - ->method('getItems') - ->willReturn([$this->itemMock]); - $this->shipmentMock->expects($this->exactly(2)) - ->method('getComments') - ->willReturn([$this->commentMock]); - $this->shipmentMock->expects($this->exactly(2)) - ->method('getTracksCollection') - ->willReturn([$this->trackMock]); - $this->itemMock->expects($this->once()) - ->method('setParentId') + $this->shipment->method('getItems') + ->willReturn([$this->item]); + $this->shipment->method('getComments') + ->willReturn([$this->comment]); + $this->shipment->method('getTracks') + ->willReturn([$this->track]); + $this->item->method('setParentId') ->with('shipment-id-value') ->willReturnSelf(); - $this->itemResourceMock->expects($this->once()) - ->method('save') - ->with($this->itemMock) + $this->itemResource->method('save') + ->with($this->item) ->willReturnSelf(); - $this->commentResourceMock->expects($this->once()) - ->method('save') - ->with($this->commentMock) + $this->commentResource->method('save') + ->with($this->comment) ->willReturnSelf(); - $this->trackResourceMock->expects($this->once()) - ->method('save') - ->with($this->trackMock) + $this->trackResource->method('save') + ->with($this->track) ->willReturnSelf(); - $this->relationProcessor->processRelation($this->shipmentMock); + $this->relationProcessor->processRelation($this->shipment); } } diff --git a/app/code/Magento/Sales/Test/Unit/Model/Service/CreditmemoServiceTest.php b/app/code/Magento/Sales/Test/Unit/Model/Service/CreditmemoServiceTest.php index 2e668f0b0d6f..68681c6c5a66 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Service/CreditmemoServiceTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Service/CreditmemoServiceTest.php @@ -3,10 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Test\Unit\Model\Service; use Magento\Sales\Model\Order; +use Magento\Sales\Api\Data\CreditmemoInterface; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + /** * Class CreditmemoServiceTest * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -14,34 +19,34 @@ class CreditmemoServiceTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Sales\Api\CreditmemoRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Sales\Api\CreditmemoRepositoryInterface|MockObject */ protected $creditmemoRepositoryMock; /** - * @var \Magento\Sales\Api\CreditmemoCommentRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Sales\Api\CreditmemoCommentRepositoryInterface|MockObject */ protected $creditmemoCommentRepositoryMock; /** - * @var \Magento\Framework\Api\SearchCriteriaBuilder|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Api\SearchCriteriaBuilder|MockObject */ protected $searchCriteriaBuilderMock; /** - * @var \Magento\Framework\Api\FilterBuilder|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Api\FilterBuilder|MockObject */ protected $filterBuilderMock; /** - * @var \Magento\Sales\Model\Order\CreditmemoNotifier|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Sales\Model\Order\CreditmemoNotifier|MockObject */ protected $creditmemoNotifierMock; /** - * @var \Magento\Framework\Pricing\PriceCurrencyInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Pricing\PriceCurrencyInterface|MockObject */ - private $priceCurrencyMock; + private $priceCurrency; /** * @var \Magento\Sales\Model\Service\CreditmemoService @@ -79,7 +84,7 @@ protected function setUp() ['setField', 'setValue', 'setConditionType', 'create'] ); $this->creditmemoNotifierMock = $this->createMock(\Magento\Sales\Model\Order\CreditmemoNotifier::class); - $this->priceCurrencyMock = $this->getMockBuilder(\Magento\Framework\Pricing\PriceCurrencyInterface::class) + $this->priceCurrency = $this->getMockBuilder(\Magento\Framework\Pricing\PriceCurrencyInterface::class) ->getMockForAbstractClass(); $this->objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -91,7 +96,7 @@ protected function setUp() 'searchCriteriaBuilder' => $this->searchCriteriaBuilderMock, 'filterBuilder' => $this->filterBuilderMock, 'creditmemoNotifier' => $this->creditmemoNotifierMock, - 'priceCurrency' => $this->priceCurrencyMock, + 'priceCurrency' => $this->priceCurrency, ] ); } @@ -187,7 +192,7 @@ public function testRefund() $orderMock->expects($this->once())->method('getBaseTotalPaid')->willReturn(10); $creditMemoMock->expects($this->once())->method('getBaseGrandTotal')->willReturn(10); - $this->priceCurrencyMock->expects($this->any()) + $this->priceCurrency->expects($this->any()) ->method('round') ->willReturnArgument(0); @@ -259,7 +264,7 @@ public function testRefundPendingCreditMemo() $orderMock->expects($this->once())->method('getBaseTotalPaid')->willReturn(10); $creditMemoMock->expects($this->once())->method('getBaseGrandTotal')->willReturn(10); - $this->priceCurrencyMock->expects($this->any()) + $this->priceCurrency->expects($this->any()) ->method('round') ->willReturnArgument(0); @@ -324,27 +329,32 @@ public function testRefundExpectsMoneyAvailableToReturn() $baseGrandTotal = 10; $baseTotalRefunded = 9; $baseTotalPaid = 10; - $creditMemoMock = $this->getMockBuilder(\Magento\Sales\Api\Data\CreditmemoInterface::class) - ->setMethods(['getId', 'getOrder', 'formatBasePrice']) + /** @var CreditmemoInterface|MockObject $creditMemo */ + $creditMemo = $this->getMockBuilder(CreditmemoInterface::class) + ->setMethods(['getId', 'getOrder']) ->getMockForAbstractClass(); - $creditMemoMock->expects($this->once())->method('getId')->willReturn(null); - $orderMock = $this->getMockBuilder(Order::class)->disableOriginalConstructor()->getMock(); - $creditMemoMock->expects($this->atLeastOnce())->method('getOrder')->willReturn($orderMock); - $creditMemoMock->expects($this->once())->method('getBaseGrandTotal')->willReturn($baseGrandTotal); - $orderMock->expects($this->atLeastOnce())->method('getBaseTotalRefunded')->willReturn($baseTotalRefunded); - $this->priceCurrencyMock->expects($this->exactly(2))->method('round')->withConsecutive( - [$baseTotalRefunded + $baseGrandTotal], - [$baseTotalPaid] - )->willReturnOnConsecutiveCalls( - $baseTotalRefunded + $baseGrandTotal, - $baseTotalPaid - ); - $orderMock->expects($this->atLeastOnce())->method('getBaseTotalPaid')->willReturn($baseTotalPaid); + $creditMemo->method('getId') + ->willReturn(null); + /** @var Order|MockObject $order */ + $order = $this->getMockBuilder(Order::class) + ->disableOriginalConstructor() + ->getMock(); + $creditMemo->method('getOrder') + ->willReturn($order); + $creditMemo->method('getBaseGrandTotal') + ->willReturn($baseGrandTotal); + $order->method('getBaseTotalRefunded') + ->willReturn($baseTotalRefunded); + $this->priceCurrency->method('round') + ->withConsecutive([$baseTotalRefunded + $baseGrandTotal], [$baseTotalPaid]) + ->willReturnOnConsecutiveCalls($baseTotalRefunded + $baseGrandTotal, $baseTotalPaid); + $order->method('getBaseTotalPaid') + ->willReturn($baseTotalPaid); $baseAvailableRefund = $baseTotalPaid - $baseTotalRefunded; - $orderMock->expects($this->once())->method('formatBasePrice')->with( - $baseAvailableRefund - )->willReturn($baseAvailableRefund); - $this->creditmemoService->refund($creditMemoMock, true); + $order->method('formatPriceTxt') + ->with($baseAvailableRefund) + ->willReturn($baseAvailableRefund); + $this->creditmemoService->refund($creditMemo, true); } /** diff --git a/app/code/Magento/Sales/Test/Unit/Model/ValidatorResultMergerTest.php b/app/code/Magento/Sales/Test/Unit/Model/ValidatorResultMergerTest.php new file mode 100644 index 000000000000..4236890a2a37 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/ValidatorResultMergerTest.php @@ -0,0 +1,87 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Test\Unit\Model; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Sales\Model\ValidatorResultInterface; +use Magento\Sales\Model\ValidatorResultInterfaceFactory; +use Magento\Sales\Model\ValidatorResultMerger; + +/** + * @covers \Magento\Sales\Model\ValidatorResultMerger + */ +class ValidatorResultMergerTest extends \PHPUnit\Framework\TestCase +{ + /** + * Testable Object + * + * @var ValidatorResultMerger + */ + private $validatorResultMerger; + + /** + * Object Manager + * + * @var ObjectManager + */ + private $objectManager; + + /** + * @var ValidatorResultInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $validatorResultFactoryMock; + + /** + * Set Up + * + * @return void + */ + protected function setUp() + { + $this->validatorResultFactoryMock = $this->getMockBuilder(ValidatorResultInterfaceFactory::class) + ->setMethods(['create'])->disableOriginalConstructor()->getMock(); + $this->objectManager = new ObjectManager($this); + $this->validatorResultMerger = $this->objectManager->getObject( + ValidatorResultMerger::class, + [ + 'validatorResultInterfaceFactory' => $this->validatorResultFactoryMock, + ] + ); + } + + /** + * Test merge method + * + * @return void + */ + public function testMerge() + { + $validatorResultMock = $this->createMock(ValidatorResultInterface::class); + $orderValidationResultMock = $this->createMock(ValidatorResultInterface::class); + $creditmemoValidationResultMock = $this->createMock(ValidatorResultInterface::class); + $itemsValidationMessages = [['test04', 'test05'], ['test06']]; + $this->validatorResultFactoryMock->expects($this->once())->method('create') + ->willReturn($validatorResultMock); + $orderValidationResultMock->expects($this->once())->method('getMessages')->willReturn(['test01', 'test02']); + $creditmemoValidationResultMock->expects($this->once())->method('getMessages')->willReturn(['test03']); + + $validatorResultMock->expects($this->at(0))->method('addMessage')->with('test01'); + $validatorResultMock->expects($this->at(1))->method('addMessage')->with('test02'); + $validatorResultMock->expects($this->at(2))->method('addMessage')->with('test03'); + $validatorResultMock->expects($this->at(3))->method('addMessage')->with('test04'); + $validatorResultMock->expects($this->at(4))->method('addMessage')->with('test05'); + $validatorResultMock->expects($this->at(5))->method('addMessage')->with('test06'); + $expected = $validatorResultMock; + $actual = $this->validatorResultMerger->merge( + $orderValidationResultMock, + $creditmemoValidationResultMock, + ...$itemsValidationMessages + ); + $this->assertEquals($expected, $actual); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/ValidatorResultTest.php b/app/code/Magento/Sales/Test/Unit/Model/ValidatorResultTest.php new file mode 100644 index 000000000000..f4ab2d4f48e6 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/ValidatorResultTest.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Test\Unit\Model; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Sales\Model\ValidatorResult; + +/** + * @covers \Magento\Sales\Model\ValidatorResult + */ +class ValidatorResultTest extends \PHPUnit\Framework\TestCase +{ + /** + * Testable Object + * + * @var ValidatorResult + */ + private $validatorResult; + + /** + * Object Manager + * + * @var ObjectManager + */ + private $objectManager; + + /** + * Set Up + * + * @return void + */ + protected function setUp() + { + $this->objectManager = new ObjectManager($this); + $this->validatorResult = $this->objectManager->getObject(ValidatorResult::class); + } + + /** + * Test addMessage method + * + * @return void + */ + public function testAddMessages() + { + $messageFirst = 'Sample message 01.'; + $messageSecond = 'Sample messages 02.'; + $messageThird = 'Sample messages 03.'; + $expected = [$messageFirst, $messageSecond, $messageThird]; + $this->validatorResult->addMessage($messageFirst); + $this->validatorResult->addMessage($messageSecond); + $this->validatorResult->addMessage($messageThird); + $actual = $this->validatorResult->getMessages(); + $this->assertEquals($expected, $actual); + } + + /** + * Test hasMessages method + * + * @return void + */ + public function testHasMessages() + { + $this->assertFalse($this->validatorResult->hasMessages()); + $messageFirst = 'Sample message 01.'; + $messageSecond = 'Sample messages 02.'; + $this->validatorResult->addMessage($messageFirst); + $this->validatorResult->addMessage($messageSecond); + $this->assertTrue($this->validatorResult->hasMessages()); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/ValidatorTest.php b/app/code/Magento/Sales/Test/Unit/Model/ValidatorTest.php new file mode 100644 index 000000000000..a334cf0e6096 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/ValidatorTest.php @@ -0,0 +1,132 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Test\Unit\Model; + +use Magento\Framework\DataObject; +use Magento\Framework\Exception\ConfigurationMismatchException; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Model\Validator; +use Magento\Sales\Model\ValidatorInterface; +use Magento\Sales\Model\ValidatorResultInterface; +use Magento\Sales\Model\ValidatorResultInterfaceFactory; + +/** + * @covers \Magento\Sales\Model\Validator + */ +class ValidatorTest extends \PHPUnit\Framework\TestCase +{ + /** + * Testable Object + * + * @var Validator + */ + private $validator; + + /** + * Object Manager + * + * @var ObjectManager + */ + private $objectManager; + + /** + * @var ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $objectManagerMock; + + /** + * @var ValidatorResultInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $validatorResultFactoryMock; + + /** + * @var ValidatorResultInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $validatorResultMock; + + /** + * @var ValidatorInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $validatorMock; + + /** + * @var OrderInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $entityMock; + + /** + * Set Up + * + * @return void + */ + protected function setUp() + { + $this->objectManagerMock = $this->createMock(ObjectManagerInterface::class); + $this->entityMock = $this->createMock(OrderInterface::class); + $this->validatorMock = $this->createMock(ValidatorInterface::class); + $this->validatorResultFactoryMock = $this->getMockBuilder(ValidatorResultInterfaceFactory::class) + ->setMethods(['create'])->disableOriginalConstructor()->getMock(); + $this->validatorResultMock = $this->createMock(ValidatorResultInterface::class); + $this->validatorResultFactoryMock->expects($this->any())->method('create') + ->willReturn($this->validatorResultMock); + $this->objectManager = new ObjectManager($this); + $this->validator = $this->objectManager->getObject( + Validator::class, + [ + 'objectManager' => $this->objectManagerMock, + 'validatorResult' => $this->validatorResultFactoryMock, + ] + ); + } + + /** + * Test validate method + * + * @return void + * + * @throws ConfigurationMismatchException + */ + public function testValidate() + { + $validatorName = 'test'; + $validators = [$validatorName]; + $context = new DataObject(); + $validatorArguments = ['context' => $context]; + $message = __('Sample message.'); + $messages = [$message]; + + $this->objectManagerMock->expects($this->once())->method('create') + ->with($validatorName, $validatorArguments)->willReturn($this->validatorMock); + $this->validatorMock->expects($this->once())->method('validate')->with($this->entityMock) + ->willReturn($messages); + $this->validatorResultMock->expects($this->once())->method('addMessage')->with($message); + + $expected = $this->validatorResultMock; + $actual = $this->validator->validate($this->entityMock, $validators, $context); + $this->assertEquals($expected, $actual); + } + + /** + * Test validate method + * + * @return void + * + * @throws ConfigurationMismatchException + */ + public function testValidateWithException() + { + $validatorName = 'test'; + $validators = [$validatorName]; + $this->objectManagerMock->expects($this->once())->method('create')->willReturn(null); + $this->validatorResultMock->expects($this->never())->method('addMessage'); + $this->expectException(ConfigurationMismatchException::class); + $this->validator->validate($this->entityMock, $validators); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Observer/AssignOrderToCustomerObserverTest.php b/app/code/Magento/Sales/Test/Unit/Observer/AssignOrderToCustomerObserverTest.php new file mode 100644 index 000000000000..e919b45667f2 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Observer/AssignOrderToCustomerObserverTest.php @@ -0,0 +1,102 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Test\Unit\Observer; + +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Framework\Event; +use Magento\Framework\Event\Observer; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Observer\AssignOrderToCustomerObserver; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject; + +/** + * Class AssignOrderToCustomerObserverTest + */ +class AssignOrderToCustomerObserverTest extends TestCase +{ + /** @var AssignOrderToCustomerObserver */ + protected $sut; + + /** @var OrderRepositoryInterface|PHPUnit_Framework_MockObject_MockObject */ + protected $orderRepositoryMock; + + /** + * Set Up + */ + protected function setUp() + { + $this->orderRepositoryMock = $this->getMockBuilder(OrderRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->sut = new AssignOrderToCustomerObserver($this->orderRepositoryMock); + } + + /** + * Test assigning order to customer after issuing guest order + * + * @dataProvider getCustomerIds + * @param null|int $customerId + * @return void + */ + public function testAssignOrderToCustomerAfterGuestOrder($customerId) + { + $orderId = 1; + /** @var Observer|PHPUnit_Framework_MockObject_MockObject $observerMock */ + $observerMock = $this->createMock(Observer::class); + /** @var Event|PHPUnit_Framework_MockObject_MockObject $eventMock */ + $eventMock = $this->getMockBuilder(Event::class)->disableOriginalConstructor() + ->setMethods(['getData']) + ->getMock(); + /** @var CustomerInterface|PHPUnit_Framework_MockObject_MockObject $customerMock */ + $customerMock = $this->createMock(CustomerInterface::class); + /** @var OrderInterface|PHPUnit_Framework_MockObject_MockObject $orderMock */ + $orderMock = $this->getMockBuilder(OrderInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $observerMock->expects($this->once())->method('getEvent')->willReturn($eventMock); + $eventMock->expects($this->any())->method('getData') + ->willReturnMap([ + ['delegate_data', null, ['__sales_assign_order_id' => $orderId]], + ['customer_data_object', null, $customerMock] + ]); + $orderMock->expects($this->once())->method('getCustomerId')->willReturn($customerId); + $this->orderRepositoryMock->expects($this->once())->method('get')->with($orderId) + ->willReturn($orderMock); + + $orderMock->expects($this->once())->method('setCustomerId')->willReturn($orderMock); + $orderMock->expects($this->once())->method('setCustomerIsGuest')->willReturn($orderMock); + $orderMock->expects($this->once())->method('setCustomerEmail')->willReturn($orderMock); + $orderMock->expects($this->once())->method('setCustomerFirstname')->willReturn($orderMock); + $orderMock->expects($this->once())->method('setCustomerLastname')->willReturn($orderMock); + $orderMock->expects($this->once())->method('setCustomerMiddlename')->willReturn($orderMock); + $orderMock->expects($this->once())->method('setCustomerPrefix')->willReturn($orderMock); + $orderMock->expects($this->once())->method('setCustomerSuffix')->willReturn($orderMock); + $orderMock->expects($this->once())->method('setCustomerGroupId')->willReturn($orderMock); + + if (!$customerId) { + $this->orderRepositoryMock->expects($this->once())->method('save')->with($orderMock); + $this->sut->execute($observerMock); + return ; + } + + $this->orderRepositoryMock->expects($this->never())->method('save')->with($orderMock); + $this->sut->execute($observerMock); + } + + /** + * Customer id assigned to order + * + * @return array + */ + public function getCustomerIds() + { + return [[null, 1]]; + } +} diff --git a/app/code/Magento/Sales/Ui/Component/Listing/Column/Status.php b/app/code/Magento/Sales/Ui/Component/Listing/Column/Status.php index 2fd792fb4ae2..48b8740d86fc 100644 --- a/app/code/Magento/Sales/Ui/Component/Listing/Column/Status.php +++ b/app/code/Magento/Sales/Ui/Component/Listing/Column/Status.php @@ -44,7 +44,7 @@ public function __construct( * Prepare Data Source * * @param array $dataSource - * @return void + * @return array */ public function prepareDataSource(array $dataSource) { diff --git a/app/code/Magento/Sales/ViewModel/Customer/AddressFormatter.php b/app/code/Magento/Sales/ViewModel/Customer/AddressFormatter.php new file mode 100644 index 000000000000..333ab4066ac8 --- /dev/null +++ b/app/code/Magento/Sales/ViewModel/Customer/AddressFormatter.php @@ -0,0 +1,132 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\ViewModel\Customer; + +use Magento\Framework\View\Element\Block\ArgumentInterface; + +/** + * Customer address formatter + */ +class AddressFormatter implements ArgumentInterface +{ + /** + * Customer form factory + * + * @var \Magento\Customer\Model\Metadata\FormFactory + */ + private $customerFormFactory; + + /** + * Address format helper + * + * @var \Magento\Customer\Helper\Address + */ + private $addressFormatHelper; + + /** + * Directory helper + * + * @var \Magento\Directory\Helper\Data + */ + private $directoryHelper; + + /** + * Session quote + * + * @var \Magento\Backend\Model\Session\Quote + */ + private $session; + + /** + * Json encoder + * + * @var \Magento\Framework\Serialize\Serializer\Json + */ + private $jsonEncoder; + + /** + * Customer address + * + * @param \Magento\Customer\Model\Metadata\FormFactory $customerFormFactory + * @param \Magento\Customer\Helper\Address $addressFormatHelper + * @param \Magento\Directory\Helper\Data $directoryHelper + * @param \Magento\Backend\Model\Session\Quote $session + * @param \Magento\Framework\Serialize\Serializer\Json $jsonEncoder + */ + public function __construct( + \Magento\Customer\Model\Metadata\FormFactory $customerFormFactory, + \Magento\Customer\Helper\Address $addressFormatHelper, + \Magento\Directory\Helper\Data $directoryHelper, + \Magento\Backend\Model\Session\Quote $session, + \Magento\Framework\Serialize\Serializer\Json $jsonEncoder + ) { + $this->customerFormFactory = $customerFormFactory; + $this->addressFormatHelper = $addressFormatHelper; + $this->directoryHelper = $directoryHelper; + $this->session = $session; + $this->jsonEncoder = $jsonEncoder; + } + + /** + * Return customer address array as JSON + * + * @param array $addressArray + * + * @return string + */ + public function getAddressesJson(array $addressArray): string + { + $data = $this->getEmptyAddressForm(); + foreach ($addressArray as $addressId => $address) { + $addressForm = $this->customerFormFactory->create( + 'customer_address', + 'adminhtml_customer_address', + $address + ); + $data[$addressId] = $addressForm->outputData( + \Magento\Eav\Model\AttributeDataFactory::OUTPUT_FORMAT_JSON + ); + } + + return $this->jsonEncoder->serialize($data); + } + + /** + * Represent customer address in 'online' format. + * + * @param array $address + * @return string + */ + public function getAddressAsString(array $address): string + { + $formatTypeRenderer = $this->addressFormatHelper->getFormatTypeRenderer('oneline'); + $result = ''; + if ($formatTypeRenderer) { + $result = $formatTypeRenderer->renderArray($address); + } + + return $result; + } + + /** + * Return empty address address form + * + * @return array + */ + private function getEmptyAddressForm(): array + { + $defaultCountryId = $this->directoryHelper->getDefaultCountry($this->session->getStore()); + $emptyAddressForm = $this->customerFormFactory->create( + 'customer_address', + 'adminhtml_customer_address', + [\Magento\Customer\Api\Data\AddressInterface::COUNTRY_ID => $defaultCountryId] + ); + + return [0 => $emptyAddressForm->outputData(\Magento\Eav\Model\AttributeDataFactory::OUTPUT_FORMAT_JSON)]; + } +} diff --git a/app/code/Magento/Sales/etc/adminhtml/system.xml b/app/code/Magento/Sales/etc/adminhtml/system.xml index 157420b3d0c7..e437918b683b 100644 --- a/app/code/Magento/Sales/etc/adminhtml/system.xml +++ b/app/code/Magento/Sales/etc/adminhtml/system.xml @@ -27,18 +27,23 @@ <label>Checkout Totals Sort Order</label> <field id="discount" translate="label" type="text" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Discount</label> + <validate>required-number validate-number</validate> </field> <field id="grand_total" translate="label" type="text" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Grand Total</label> + <validate>required-number validate-number</validate> </field> <field id="shipping" translate="label" type="text" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Shipping</label> + <validate>required-number validate-number</validate> </field> <field id="subtotal" translate="label" type="text" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Subtotal</label> + <validate>required-number validate-number</validate> </field> <field id="tax" translate="label" type="text" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Tax</label> + <validate>required-number validate-number</validate> </field> </group> <group id="reorder" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> @@ -48,6 +53,13 @@ <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> </group> + <group id="zerograndtotal_creditmemo" translate="label" type="text" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1"> + <label>Allow Zero GrandTotal</label> + <field id="allow_zero_grandtotal" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <label>Allow Zero GrandTotal for Creditmemo</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + </field> + </group> <group id="identity" translate="label" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Invoice and Packing Slip Design</label> <field id="logo" translate="label comment" type="image" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="1"> @@ -82,6 +94,11 @@ <label>Minimum Amount</label> <comment>Subtotal after discount</comment> </field> + <field id="include_discount_amount" translate="label" sortOrder="12" type="select" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> + <label>Include Discount Amount</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <comment>Choosing yes will be used subtotal after discount, otherwise only subtotal will be used</comment> + </field> <field id="tax_including" translate="label" sortOrder="15" type="select" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Include Tax to Amount</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> diff --git a/app/code/Magento/Sales/etc/config.xml b/app/code/Magento/Sales/etc/config.xml index d4d10bfa6dcc..2480da4ad214 100644 --- a/app/code/Magento/Sales/etc/config.xml +++ b/app/code/Magento/Sales/etc/config.xml @@ -18,7 +18,11 @@ <reorder> <allow>1</allow> </reorder> + <zerograndtotal_creditmemo> + <allow_zero_grandtotal>1</allow_zero_grandtotal> + </zerograndtotal_creditmemo> <minimum_order> + <include_discount_amount>1</include_discount_amount> <tax_including>1</tax_including> </minimum_order> <orders> diff --git a/app/code/Magento/Sales/etc/db_schema.xml b/app/code/Magento/Sales/etc/db_schema.xml index 4b716e761094..d6ea9b7d5486 100644 --- a/app/code/Magento/Sales/etc/db_schema.xml +++ b/app/code/Magento/Sales/etc/db_schema.xml @@ -22,119 +22,119 @@ comment="Store Id"/> <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="true" identity="false" comment="Customer Id"/> - <column xsi:type="decimal" name="base_discount_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_discount_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Amount"/> - <column xsi:type="decimal" name="base_discount_canceled" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_discount_canceled" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Canceled"/> - <column xsi:type="decimal" name="base_discount_invoiced" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_discount_invoiced" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Invoiced"/> - <column xsi:type="decimal" name="base_discount_refunded" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_discount_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Refunded"/> - <column xsi:type="decimal" name="base_grand_total" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_grand_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Grand Total"/> - <column xsi:type="decimal" name="base_shipping_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Amount"/> - <column xsi:type="decimal" name="base_shipping_canceled" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_canceled" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Canceled"/> - <column xsi:type="decimal" name="base_shipping_invoiced" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_invoiced" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Invoiced"/> - <column xsi:type="decimal" name="base_shipping_refunded" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Refunded"/> - <column xsi:type="decimal" name="base_shipping_tax_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_tax_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Tax Amount"/> - <column xsi:type="decimal" name="base_shipping_tax_refunded" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_tax_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Tax Refunded"/> - <column xsi:type="decimal" name="base_subtotal" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_subtotal" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Subtotal"/> - <column xsi:type="decimal" name="base_subtotal_canceled" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_subtotal_canceled" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Subtotal Canceled"/> - <column xsi:type="decimal" name="base_subtotal_invoiced" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_subtotal_invoiced" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Subtotal Invoiced"/> - <column xsi:type="decimal" name="base_subtotal_refunded" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_subtotal_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Subtotal Refunded"/> - <column xsi:type="decimal" name="base_tax_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_tax_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Tax Amount"/> - <column xsi:type="decimal" name="base_tax_canceled" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_tax_canceled" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Tax Canceled"/> - <column xsi:type="decimal" name="base_tax_invoiced" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_tax_invoiced" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Tax Invoiced"/> - <column xsi:type="decimal" name="base_tax_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_tax_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Tax Refunded"/> - <column xsi:type="decimal" name="base_to_global_rate" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_to_global_rate" scale="4" precision="20" unsigned="false" nullable="true" comment="Base To Global Rate"/> - <column xsi:type="decimal" name="base_to_order_rate" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_to_order_rate" scale="4" precision="20" unsigned="false" nullable="true" comment="Base To Order Rate"/> - <column xsi:type="decimal" name="base_total_canceled" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_total_canceled" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Total Canceled"/> - <column xsi:type="decimal" name="base_total_invoiced" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_total_invoiced" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Total Invoiced"/> - <column xsi:type="decimal" name="base_total_invoiced_cost" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_total_invoiced_cost" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Total Invoiced Cost"/> - <column xsi:type="decimal" name="base_total_offline_refunded" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_total_offline_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Total Offline Refunded"/> - <column xsi:type="decimal" name="base_total_online_refunded" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_total_online_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Total Online Refunded"/> - <column xsi:type="decimal" name="base_total_paid" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_total_paid" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Total Paid"/> <column xsi:type="decimal" name="base_total_qty_ordered" scale="4" precision="12" unsigned="false" nullable="true" comment="Base Total Qty Ordered"/> - <column xsi:type="decimal" name="base_total_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_total_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Total Refunded"/> - <column xsi:type="decimal" name="discount_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="discount_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Amount"/> - <column xsi:type="decimal" name="discount_canceled" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="discount_canceled" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Canceled"/> - <column xsi:type="decimal" name="discount_invoiced" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="discount_invoiced" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Invoiced"/> - <column xsi:type="decimal" name="discount_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="discount_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Refunded"/> - <column xsi:type="decimal" name="grand_total" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="grand_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Grand Total"/> - <column xsi:type="decimal" name="shipping_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="shipping_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Amount"/> - <column xsi:type="decimal" name="shipping_canceled" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="shipping_canceled" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Canceled"/> - <column xsi:type="decimal" name="shipping_invoiced" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="shipping_invoiced" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Invoiced"/> - <column xsi:type="decimal" name="shipping_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="shipping_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Refunded"/> - <column xsi:type="decimal" name="shipping_tax_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="shipping_tax_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Tax Amount"/> - <column xsi:type="decimal" name="shipping_tax_refunded" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="shipping_tax_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Tax Refunded"/> <column xsi:type="decimal" name="store_to_base_rate" scale="4" precision="12" unsigned="false" nullable="true" comment="Store To Base Rate"/> <column xsi:type="decimal" name="store_to_order_rate" scale="4" precision="12" unsigned="false" nullable="true" comment="Store To Order Rate"/> - <column xsi:type="decimal" name="subtotal" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="subtotal" scale="4" precision="20" unsigned="false" nullable="true" comment="Subtotal"/> - <column xsi:type="decimal" name="subtotal_canceled" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="subtotal_canceled" scale="4" precision="20" unsigned="false" nullable="true" comment="Subtotal Canceled"/> - <column xsi:type="decimal" name="subtotal_invoiced" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="subtotal_invoiced" scale="4" precision="20" unsigned="false" nullable="true" comment="Subtotal Invoiced"/> - <column xsi:type="decimal" name="subtotal_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="subtotal_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Subtotal Refunded"/> - <column xsi:type="decimal" name="tax_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="tax_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Tax Amount"/> - <column xsi:type="decimal" name="tax_canceled" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="tax_canceled" scale="4" precision="20" unsigned="false" nullable="true" comment="Tax Canceled"/> - <column xsi:type="decimal" name="tax_invoiced" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="tax_invoiced" scale="4" precision="20" unsigned="false" nullable="true" comment="Tax Invoiced"/> - <column xsi:type="decimal" name="tax_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="tax_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Tax Refunded"/> - <column xsi:type="decimal" name="total_canceled" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="total_canceled" scale="4" precision="20" unsigned="false" nullable="true" comment="Total Canceled"/> - <column xsi:type="decimal" name="total_invoiced" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="total_invoiced" scale="4" precision="20" unsigned="false" nullable="true" comment="Total Invoiced"/> - <column xsi:type="decimal" name="total_offline_refunded" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_offline_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Total Offline Refunded"/> - <column xsi:type="decimal" name="total_online_refunded" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_online_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Total Online Refunded"/> - <column xsi:type="decimal" name="total_paid" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="total_paid" scale="4" precision="20" unsigned="false" nullable="true" comment="Total Paid"/> <column xsi:type="decimal" name="total_qty_ordered" scale="4" precision="12" unsigned="false" nullable="true" comment="Total Qty Ordered"/> - <column xsi:type="decimal" name="total_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="total_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Total Refunded"/> <column xsi:type="smallint" name="can_ship_partially" padding="5" unsigned="true" nullable="true" identity="false" comment="Can Ship Partially"/> @@ -163,27 +163,27 @@ comment="Quote Id"/> <column xsi:type="int" name="shipping_address_id" padding="11" unsigned="false" nullable="true" identity="false" comment="Shipping Address Id"/> - <column xsi:type="decimal" name="adjustment_negative" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="adjustment_negative" scale="4" precision="20" unsigned="false" nullable="true" comment="Adjustment Negative"/> - <column xsi:type="decimal" name="adjustment_positive" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="adjustment_positive" scale="4" precision="20" unsigned="false" nullable="true" comment="Adjustment Positive"/> - <column xsi:type="decimal" name="base_adjustment_negative" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_adjustment_negative" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Adjustment Negative"/> - <column xsi:type="decimal" name="base_adjustment_positive" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_adjustment_positive" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Adjustment Positive"/> - <column xsi:type="decimal" name="base_shipping_discount_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_discount_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Discount Amount"/> - <column xsi:type="decimal" name="base_subtotal_incl_tax" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_subtotal_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Subtotal Incl Tax"/> - <column xsi:type="decimal" name="base_total_due" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_total_due" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Total Due"/> - <column xsi:type="decimal" name="payment_authorization_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="payment_authorization_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Payment Authorization Amount"/> - <column xsi:type="decimal" name="shipping_discount_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="shipping_discount_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Discount Amount"/> - <column xsi:type="decimal" name="subtotal_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="subtotal_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Subtotal Incl Tax"/> - <column xsi:type="decimal" name="total_due" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="total_due" scale="4" precision="20" unsigned="false" nullable="true" comment="Total Due"/> <column xsi:type="decimal" name="weight" scale="4" precision="12" unsigned="false" nullable="true" comment="Weight"/> @@ -230,68 +230,68 @@ identity="false" default="0" comment="Total Item Count"/> <column xsi:type="int" name="customer_gender" padding="11" unsigned="false" nullable="true" identity="false" comment="Customer Gender"/> - <column xsi:type="decimal" name="discount_tax_compensation_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="base_discount_tax_compensation_amount" scale="4" precision="12" + <column xsi:type="decimal" name="base_discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="shipping_discount_tax_compensation_amount" scale="4" precision="12" + <column xsi:type="decimal" name="shipping_discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="base_shipping_discount_tax_compensation_amnt" scale="4" precision="12" + <column xsi:type="decimal" name="base_shipping_discount_tax_compensation_amnt" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="discount_tax_compensation_invoiced" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="discount_tax_compensation_invoiced" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Tax Compensation Invoiced"/> - <column xsi:type="decimal" name="base_discount_tax_compensation_invoiced" scale="4" precision="12" + <column xsi:type="decimal" name="base_discount_tax_compensation_invoiced" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Tax Compensation Invoiced"/> - <column xsi:type="decimal" name="discount_tax_compensation_refunded" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="discount_tax_compensation_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Tax Compensation Refunded"/> - <column xsi:type="decimal" name="base_discount_tax_compensation_refunded" scale="4" precision="12" + <column xsi:type="decimal" name="base_discount_tax_compensation_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Tax Compensation Refunded"/> - <column xsi:type="decimal" name="shipping_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="shipping_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Incl Tax"/> - <column xsi:type="decimal" name="base_shipping_incl_tax" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Incl Tax"/> <column xsi:type="varchar" name="coupon_rule_name" nullable="true" length="255" comment="Coupon Sales Rule Name"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> - <constraint xsi:type="foreign" name="SALES_ORDER_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID" table="sales_order" + <constraint xsi:type="foreign" referenceId="SALES_ORDER_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID" table="sales_order" column="customer_id" referenceTable="customer_entity" referenceColumn="entity_id" onDelete="SET NULL"/> - <constraint xsi:type="foreign" name="SALES_ORDER_STORE_ID_STORE_STORE_ID" table="sales_order" column="store_id" + <constraint xsi:type="foreign" referenceId="SALES_ORDER_STORE_ID_STORE_STORE_ID" table="sales_order" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="SET NULL"/> - <constraint xsi:type="unique" name="SALES_ORDER_INCREMENT_ID_STORE_ID"> + <constraint xsi:type="unique" referenceId="SALES_ORDER_INCREMENT_ID_STORE_ID"> <column name="increment_id"/> <column name="store_id"/> </constraint> - <index name="SALES_ORDER_STATUS" indexType="btree"> + <index referenceId="SALES_ORDER_STATUS" indexType="btree"> <column name="status"/> </index> - <index name="SALES_ORDER_STATE" indexType="btree"> + <index referenceId="SALES_ORDER_STATE" indexType="btree"> <column name="state"/> </index> - <index name="SALES_ORDER_STORE_ID" indexType="btree"> + <index referenceId="SALES_ORDER_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="SALES_ORDER_CREATED_AT" indexType="btree"> + <index referenceId="SALES_ORDER_CREATED_AT" indexType="btree"> <column name="created_at"/> </index> - <index name="SALES_ORDER_CUSTOMER_ID" indexType="btree"> + <index referenceId="SALES_ORDER_CUSTOMER_ID" indexType="btree"> <column name="customer_id"/> </index> - <index name="SALES_ORDER_EXT_ORDER_ID" indexType="btree"> + <index referenceId="SALES_ORDER_EXT_ORDER_ID" indexType="btree"> <column name="ext_order_id"/> </index> - <index name="SALES_ORDER_QUOTE_ID" indexType="btree"> + <index referenceId="SALES_ORDER_QUOTE_ID" indexType="btree"> <column name="quote_id"/> </index> - <index name="SALES_ORDER_UPDATED_AT" indexType="btree"> + <index referenceId="SALES_ORDER_UPDATED_AT" indexType="btree"> <column name="updated_at"/> </index> - <index name="SALES_ORDER_SEND_EMAIL" indexType="btree"> + <index referenceId="SALES_ORDER_SEND_EMAIL" indexType="btree"> <column name="send_email"/> </index> - <index name="SALES_ORDER_EMAIL_SENT" indexType="btree"> + <index referenceId="SALES_ORDER_EMAIL_SENT" indexType="btree"> <column name="email_sent"/> </index> </table> @@ -304,13 +304,13 @@ <column xsi:type="varchar" name="store_name" nullable="true" length="255" comment="Store Name"/> <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="true" identity="false" comment="Customer Id"/> - <column xsi:type="decimal" name="base_grand_total" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_grand_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Grand Total"/> - <column xsi:type="decimal" name="base_total_paid" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_total_paid" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Total Paid"/> - <column xsi:type="decimal" name="grand_total" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="grand_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Grand Total"/> - <column xsi:type="decimal" name="total_paid" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="total_paid" scale="4" precision="20" unsigned="false" nullable="true" comment="Total Paid"/> <column xsi:type="varchar" name="increment_id" nullable="true" length="50" comment="Increment Id"/> <column xsi:type="varchar" name="base_currency_code" nullable="true" length="3" comment="Base Currency Code"/> @@ -326,55 +326,55 @@ comment="Shipping Method Name"/> <column xsi:type="varchar" name="customer_email" nullable="true" length="255" comment="Customer Email"/> <column xsi:type="varchar" name="customer_group" nullable="true" length="255" comment="Customer Group"/> - <column xsi:type="decimal" name="subtotal" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="subtotal" scale="4" precision="20" unsigned="false" nullable="true" comment="Subtotal"/> - <column xsi:type="decimal" name="shipping_and_handling" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="shipping_and_handling" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping and handling amount"/> <column xsi:type="varchar" name="customer_name" nullable="true" length="255" comment="Customer Name"/> <column xsi:type="varchar" name="payment_method" nullable="true" length="255" comment="Payment Method"/> - <column xsi:type="decimal" name="total_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="total_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Total Refunded"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> - <constraint xsi:type="unique" name="SALES_ORDER_GRID_INCREMENT_ID_STORE_ID"> + <constraint xsi:type="unique" referenceId="SALES_ORDER_GRID_INCREMENT_ID_STORE_ID"> <column name="increment_id"/> <column name="store_id"/> </constraint> - <index name="SALES_ORDER_GRID_STATUS" indexType="btree"> + <index referenceId="SALES_ORDER_GRID_STATUS" indexType="btree"> <column name="status"/> </index> - <index name="SALES_ORDER_GRID_STORE_ID" indexType="btree"> + <index referenceId="SALES_ORDER_GRID_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="SALES_ORDER_GRID_BASE_GRAND_TOTAL" indexType="btree"> + <index referenceId="SALES_ORDER_GRID_BASE_GRAND_TOTAL" indexType="btree"> <column name="base_grand_total"/> </index> - <index name="SALES_ORDER_GRID_BASE_TOTAL_PAID" indexType="btree"> + <index referenceId="SALES_ORDER_GRID_BASE_TOTAL_PAID" indexType="btree"> <column name="base_total_paid"/> </index> - <index name="SALES_ORDER_GRID_GRAND_TOTAL" indexType="btree"> + <index referenceId="SALES_ORDER_GRID_GRAND_TOTAL" indexType="btree"> <column name="grand_total"/> </index> - <index name="SALES_ORDER_GRID_TOTAL_PAID" indexType="btree"> + <index referenceId="SALES_ORDER_GRID_TOTAL_PAID" indexType="btree"> <column name="total_paid"/> </index> - <index name="SALES_ORDER_GRID_SHIPPING_NAME" indexType="btree"> + <index referenceId="SALES_ORDER_GRID_SHIPPING_NAME" indexType="btree"> <column name="shipping_name"/> </index> - <index name="SALES_ORDER_GRID_BILLING_NAME" indexType="btree"> + <index referenceId="SALES_ORDER_GRID_BILLING_NAME" indexType="btree"> <column name="billing_name"/> </index> - <index name="SALES_ORDER_GRID_CREATED_AT" indexType="btree"> + <index referenceId="SALES_ORDER_GRID_CREATED_AT" indexType="btree"> <column name="created_at"/> </index> - <index name="SALES_ORDER_GRID_CUSTOMER_ID" indexType="btree"> + <index referenceId="SALES_ORDER_GRID_CUSTOMER_ID" indexType="btree"> <column name="customer_id"/> </index> - <index name="SALES_ORDER_GRID_UPDATED_AT" indexType="btree"> + <index referenceId="SALES_ORDER_GRID_UPDATED_AT" indexType="btree"> <column name="updated_at"/> </index> - <index name="FTI_65B9E9925EC58F0C7C2E2F6379C233E7" indexType="fulltext"> + <index referenceId="FTI_65B9E9925EC58F0C7C2E2F6379C233E7" indexType="fulltext"> <column name="increment_id"/> <column name="billing_name"/> <column name="shipping_name"/> @@ -419,13 +419,13 @@ <column xsi:type="text" name="vat_request_date" nullable="true" comment="Vat Request Date"/> <column xsi:type="smallint" name="vat_request_success" padding="6" unsigned="false" nullable="true" identity="false" comment="Vat Request Success"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> - <constraint xsi:type="foreign" name="SALES_ORDER_ADDRESS_PARENT_ID_SALES_ORDER_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="SALES_ORDER_ADDRESS_PARENT_ID_SALES_ORDER_ENTITY_ID" table="sales_order_address" column="parent_id" referenceTable="sales_order" referenceColumn="entity_id" onDelete="CASCADE"/> - <index name="SALES_ORDER_ADDRESS_PARENT_ID" indexType="btree"> + <index referenceId="SALES_ORDER_ADDRESS_PARENT_ID" indexType="btree"> <column name="parent_id"/> </index> </table> @@ -444,16 +444,16 @@ comment="Created At"/> <column xsi:type="varchar" name="entity_name" nullable="true" length="32" comment="Shows what entity history is bind to."/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> - <constraint xsi:type="foreign" name="SALES_ORDER_STATUS_HISTORY_PARENT_ID_SALES_ORDER_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="SALES_ORDER_STATUS_HISTORY_PARENT_ID_SALES_ORDER_ENTITY_ID" table="sales_order_status_history" column="parent_id" referenceTable="sales_order" referenceColumn="entity_id" onDelete="CASCADE"/> - <index name="SALES_ORDER_STATUS_HISTORY_PARENT_ID" indexType="btree"> + <index referenceId="SALES_ORDER_STATUS_HISTORY_PARENT_ID" indexType="btree"> <column name="parent_id"/> </index> - <index name="SALES_ORDER_STATUS_HISTORY_CREATED_AT" indexType="btree"> + <index referenceId="SALES_ORDER_STATUS_HISTORY_CREATED_AT" indexType="btree"> <column name="created_at"/> </index> </table> @@ -513,90 +513,90 @@ comment="Base Original Price"/> <column xsi:type="decimal" name="tax_percent" scale="4" precision="12" unsigned="false" nullable="true" default="0" comment="Tax Percent"/> - <column xsi:type="decimal" name="tax_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="tax_amount" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Tax Amount"/> - <column xsi:type="decimal" name="base_tax_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_tax_amount" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Base Tax Amount"/> - <column xsi:type="decimal" name="tax_invoiced" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="tax_invoiced" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Tax Invoiced"/> - <column xsi:type="decimal" name="base_tax_invoiced" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_tax_invoiced" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Base Tax Invoiced"/> <column xsi:type="decimal" name="discount_percent" scale="4" precision="12" unsigned="false" nullable="true" default="0" comment="Discount Percent"/> - <column xsi:type="decimal" name="discount_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="discount_amount" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Discount Amount"/> - <column xsi:type="decimal" name="base_discount_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_discount_amount" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Base Discount Amount"/> - <column xsi:type="decimal" name="discount_invoiced" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="discount_invoiced" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Discount Invoiced"/> - <column xsi:type="decimal" name="base_discount_invoiced" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_discount_invoiced" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Base Discount Invoiced"/> - <column xsi:type="decimal" name="amount_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="amount_refunded" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Amount Refunded"/> - <column xsi:type="decimal" name="base_amount_refunded" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_amount_refunded" scale="4" precision="20" unsigned="false" nullable="true" default="0" comment="Base Amount Refunded"/> - <column xsi:type="decimal" name="row_total" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="row_total" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Row Total"/> - <column xsi:type="decimal" name="base_row_total" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="base_row_total" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Base Row Total"/> - <column xsi:type="decimal" name="row_invoiced" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="row_invoiced" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Row Invoiced"/> - <column xsi:type="decimal" name="base_row_invoiced" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="base_row_invoiced" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Base Row Invoiced"/> <column xsi:type="decimal" name="row_weight" scale="4" precision="12" unsigned="false" nullable="true" default="0" comment="Row Weight"/> - <column xsi:type="decimal" name="base_tax_before_discount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_tax_before_discount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Tax Before Discount"/> - <column xsi:type="decimal" name="tax_before_discount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="tax_before_discount" scale="4" precision="20" unsigned="false" nullable="true" comment="Tax Before Discount"/> <column xsi:type="varchar" name="ext_order_item_id" nullable="true" length="255" comment="Ext Order Item Id"/> <column xsi:type="smallint" name="locked_do_invoice" padding="5" unsigned="true" nullable="true" identity="false" comment="Locked Do Invoice"/> <column xsi:type="smallint" name="locked_do_ship" padding="5" unsigned="true" nullable="true" identity="false" comment="Locked Do Ship"/> - <column xsi:type="decimal" name="price_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="price_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Price Incl Tax"/> - <column xsi:type="decimal" name="base_price_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_price_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Price Incl Tax"/> - <column xsi:type="decimal" name="row_total_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="row_total_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Row Total Incl Tax"/> - <column xsi:type="decimal" name="base_row_total_incl_tax" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_row_total_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Row Total Incl Tax"/> - <column xsi:type="decimal" name="discount_tax_compensation_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="base_discount_tax_compensation_amount" scale="4" precision="12" + <column xsi:type="decimal" name="base_discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="discount_tax_compensation_invoiced" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="discount_tax_compensation_invoiced" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Tax Compensation Invoiced"/> - <column xsi:type="decimal" name="base_discount_tax_compensation_invoiced" scale="4" precision="12" + <column xsi:type="decimal" name="base_discount_tax_compensation_invoiced" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Tax Compensation Invoiced"/> - <column xsi:type="decimal" name="discount_tax_compensation_refunded" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="discount_tax_compensation_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Tax Compensation Refunded"/> - <column xsi:type="decimal" name="base_discount_tax_compensation_refunded" scale="4" precision="12" + <column xsi:type="decimal" name="base_discount_tax_compensation_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Tax Compensation Refunded"/> <column xsi:type="decimal" name="tax_canceled" scale="4" precision="12" unsigned="false" nullable="true" comment="Tax Canceled"/> - <column xsi:type="decimal" name="discount_tax_compensation_canceled" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="discount_tax_compensation_canceled" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Tax Compensation Canceled"/> - <column xsi:type="decimal" name="tax_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="tax_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Tax Refunded"/> - <column xsi:type="decimal" name="base_tax_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_tax_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Tax Refunded"/> - <column xsi:type="decimal" name="discount_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="discount_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Refunded"/> - <column xsi:type="decimal" name="base_discount_refunded" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_discount_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Refunded"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="item_id"/> </constraint> - <constraint xsi:type="foreign" name="SALES_ORDER_ITEM_ORDER_ID_SALES_ORDER_ENTITY_ID" table="sales_order_item" + <constraint xsi:type="foreign" referenceId="SALES_ORDER_ITEM_ORDER_ID_SALES_ORDER_ENTITY_ID" table="sales_order_item" column="order_id" referenceTable="sales_order" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="SALES_ORDER_ITEM_STORE_ID_STORE_STORE_ID" table="sales_order_item" + <constraint xsi:type="foreign" referenceId="SALES_ORDER_ITEM_STORE_ID_STORE_STORE_ID" table="sales_order_item" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="SET NULL"/> - <index name="SALES_ORDER_ITEM_ORDER_ID" indexType="btree"> + <index referenceId="SALES_ORDER_ITEM_ORDER_ID" indexType="btree"> <column name="order_id"/> </index> - <index name="SALES_ORDER_ITEM_STORE_ID" indexType="btree"> + <index referenceId="SALES_ORDER_ITEM_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -605,41 +605,41 @@ comment="Entity Id"/> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Parent Id"/> - <column xsi:type="decimal" name="base_shipping_captured" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_captured" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Captured"/> - <column xsi:type="decimal" name="shipping_captured" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="shipping_captured" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Captured"/> - <column xsi:type="decimal" name="amount_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="amount_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Amount Refunded"/> - <column xsi:type="decimal" name="base_amount_paid" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_amount_paid" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Amount Paid"/> - <column xsi:type="decimal" name="amount_canceled" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="amount_canceled" scale="4" precision="20" unsigned="false" nullable="true" comment="Amount Canceled"/> - <column xsi:type="decimal" name="base_amount_authorized" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_amount_authorized" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Amount Authorized"/> - <column xsi:type="decimal" name="base_amount_paid_online" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_amount_paid_online" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Amount Paid Online"/> - <column xsi:type="decimal" name="base_amount_refunded_online" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_amount_refunded_online" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Amount Refunded Online"/> - <column xsi:type="decimal" name="base_shipping_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Amount"/> - <column xsi:type="decimal" name="shipping_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="shipping_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Amount"/> - <column xsi:type="decimal" name="amount_paid" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="amount_paid" scale="4" precision="20" unsigned="false" nullable="true" comment="Amount Paid"/> - <column xsi:type="decimal" name="amount_authorized" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="amount_authorized" scale="4" precision="20" unsigned="false" nullable="true" comment="Amount Authorized"/> - <column xsi:type="decimal" name="base_amount_ordered" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_amount_ordered" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Amount Ordered"/> - <column xsi:type="decimal" name="base_shipping_refunded" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Refunded"/> - <column xsi:type="decimal" name="shipping_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="shipping_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Refunded"/> - <column xsi:type="decimal" name="base_amount_refunded" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_amount_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Amount Refunded"/> - <column xsi:type="decimal" name="amount_ordered" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="amount_ordered" scale="4" precision="20" unsigned="false" nullable="true" comment="Amount Ordered"/> - <column xsi:type="decimal" name="base_amount_canceled" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_amount_canceled" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Amount Canceled"/> <column xsi:type="int" name="quote_payment_id" padding="11" unsigned="false" nullable="true" identity="false" comment="Quote Payment Id"/> @@ -663,7 +663,7 @@ <column xsi:type="varchar" name="cc_ss_start_month" nullable="true" length="128" comment="Cc Ss Start Month"/> <column xsi:type="varchar" name="echeck_account_type" nullable="true" length="255" comment="Echeck Account Type"/> - <column xsi:type="varchar" name="last_trans_id" nullable="true" length="32" comment="Last Trans Id"/> + <column xsi:type="varchar" name="last_trans_id" nullable="true" length="255" comment="Last Trans Id"/> <column xsi:type="varchar" name="cc_cid_status" nullable="true" length="32" comment="Cc Cid Status"/> <column xsi:type="varchar" name="cc_owner" nullable="true" length="128" comment="Cc Owner"/> <column xsi:type="varchar" name="cc_type" nullable="true" length="32" comment="Cc Type"/> @@ -684,13 +684,13 @@ <column xsi:type="varchar" name="cc_trans_id" nullable="true" length="32" comment="Cc Trans Id"/> <column xsi:type="varchar" name="address_status" nullable="true" length="32" comment="Address Status"/> <column xsi:type="text" name="additional_information" nullable="true" comment="Additional Information"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> - <constraint xsi:type="foreign" name="SALES_ORDER_PAYMENT_PARENT_ID_SALES_ORDER_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="SALES_ORDER_PAYMENT_PARENT_ID_SALES_ORDER_ENTITY_ID" table="sales_order_payment" column="parent_id" referenceTable="sales_order" referenceColumn="entity_id" onDelete="CASCADE"/> - <index name="SALES_ORDER_PAYMENT_PARENT_ID" indexType="btree"> + <index referenceId="SALES_ORDER_PAYMENT_PARENT_ID" indexType="btree"> <column name="parent_id"/> </index> </table> @@ -727,36 +727,36 @@ <column xsi:type="text" name="customer_note" nullable="true" comment="Customer Note"/> <column xsi:type="smallint" name="customer_note_notify" padding="5" unsigned="true" nullable="true" identity="false" comment="Customer Note Notify"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> - <constraint xsi:type="foreign" name="SALES_SHIPMENT_ORDER_ID_SALES_ORDER_ENTITY_ID" table="sales_shipment" + <constraint xsi:type="foreign" referenceId="SALES_SHIPMENT_ORDER_ID_SALES_ORDER_ENTITY_ID" table="sales_shipment" column="order_id" referenceTable="sales_order" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="SALES_SHIPMENT_STORE_ID_STORE_STORE_ID" table="sales_shipment" + <constraint xsi:type="foreign" referenceId="SALES_SHIPMENT_STORE_ID_STORE_STORE_ID" table="sales_shipment" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="SET NULL"/> - <constraint xsi:type="unique" name="SALES_SHIPMENT_INCREMENT_ID_STORE_ID"> + <constraint xsi:type="unique" referenceId="SALES_SHIPMENT_INCREMENT_ID_STORE_ID"> <column name="increment_id"/> <column name="store_id"/> </constraint> - <index name="SALES_SHIPMENT_STORE_ID" indexType="btree"> + <index referenceId="SALES_SHIPMENT_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="SALES_SHIPMENT_TOTAL_QTY" indexType="btree"> + <index referenceId="SALES_SHIPMENT_TOTAL_QTY" indexType="btree"> <column name="total_qty"/> </index> - <index name="SALES_SHIPMENT_ORDER_ID" indexType="btree"> + <index referenceId="SALES_SHIPMENT_ORDER_ID" indexType="btree"> <column name="order_id"/> </index> - <index name="SALES_SHIPMENT_CREATED_AT" indexType="btree"> + <index referenceId="SALES_SHIPMENT_CREATED_AT" indexType="btree"> <column name="created_at"/> </index> - <index name="SALES_SHIPMENT_UPDATED_AT" indexType="btree"> + <index referenceId="SALES_SHIPMENT_UPDATED_AT" indexType="btree"> <column name="updated_at"/> </index> - <index name="SALES_SHIPMENT_SEND_EMAIL" indexType="btree"> + <index referenceId="SALES_SHIPMENT_SEND_EMAIL" indexType="btree"> <column name="send_email"/> </index> - <index name="SALES_SHIPMENT_EMAIL_SENT" indexType="btree"> + <index referenceId="SALES_SHIPMENT_EMAIL_SENT" indexType="btree"> <column name="email_sent"/> </index> </table> @@ -769,7 +769,7 @@ <column xsi:type="varchar" name="order_increment_id" nullable="false" length="32" comment="Order Increment Id"/> <column xsi:type="int" name="order_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Order Id"/> - <column xsi:type="timestamp" name="order_created_at" on_update="true" nullable="true" + <column xsi:type="timestamp" name="order_created_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" comment="Order Increment Id"/> <column xsi:type="varchar" name="customer_name" nullable="false" length="128" comment="Customer Name"/> <column xsi:type="decimal" name="total_qty" scale="4" precision="12" unsigned="false" nullable="true" @@ -788,44 +788,44 @@ comment="Shipping Method Name"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="true" comment="Created At"/> <column xsi:type="timestamp" name="updated_at" on_update="false" nullable="true" comment="Updated At"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> - <constraint xsi:type="unique" name="SALES_SHIPMENT_GRID_INCREMENT_ID_STORE_ID"> + <constraint xsi:type="unique" referenceId="SALES_SHIPMENT_GRID_INCREMENT_ID_STORE_ID"> <column name="increment_id"/> <column name="store_id"/> </constraint> - <index name="SALES_SHIPMENT_GRID_STORE_ID" indexType="btree"> + <index referenceId="SALES_SHIPMENT_GRID_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="SALES_SHIPMENT_GRID_TOTAL_QTY" indexType="btree"> + <index referenceId="SALES_SHIPMENT_GRID_TOTAL_QTY" indexType="btree"> <column name="total_qty"/> </index> - <index name="SALES_SHIPMENT_GRID_ORDER_INCREMENT_ID" indexType="btree"> + <index referenceId="SALES_SHIPMENT_GRID_ORDER_INCREMENT_ID" indexType="btree"> <column name="order_increment_id"/> </index> - <index name="SALES_SHIPMENT_GRID_SHIPMENT_STATUS" indexType="btree"> + <index referenceId="SALES_SHIPMENT_GRID_SHIPMENT_STATUS" indexType="btree"> <column name="shipment_status"/> </index> - <index name="SALES_SHIPMENT_GRID_ORDER_STATUS" indexType="btree"> + <index referenceId="SALES_SHIPMENT_GRID_ORDER_STATUS" indexType="btree"> <column name="order_status"/> </index> - <index name="SALES_SHIPMENT_GRID_CREATED_AT" indexType="btree"> + <index referenceId="SALES_SHIPMENT_GRID_CREATED_AT" indexType="btree"> <column name="created_at"/> </index> - <index name="SALES_SHIPMENT_GRID_UPDATED_AT" indexType="btree"> + <index referenceId="SALES_SHIPMENT_GRID_UPDATED_AT" indexType="btree"> <column name="updated_at"/> </index> - <index name="SALES_SHIPMENT_GRID_ORDER_CREATED_AT" indexType="btree"> + <index referenceId="SALES_SHIPMENT_GRID_ORDER_CREATED_AT" indexType="btree"> <column name="order_created_at"/> </index> - <index name="SALES_SHIPMENT_GRID_SHIPPING_NAME" indexType="btree"> + <index referenceId="SALES_SHIPMENT_GRID_SHIPPING_NAME" indexType="btree"> <column name="shipping_name"/> </index> - <index name="SALES_SHIPMENT_GRID_BILLING_NAME" indexType="btree"> + <index referenceId="SALES_SHIPMENT_GRID_BILLING_NAME" indexType="btree"> <column name="billing_name"/> </index> - <index name="FTI_086B40C8955F167B8EA76653437879B4" indexType="fulltext"> + <index referenceId="FTI_086B40C8955F167B8EA76653437879B4" indexType="fulltext"> <column name="increment_id"/> <column name="order_increment_id"/> <column name="shipping_name"/> @@ -840,9 +840,9 @@ comment="Entity Id"/> <column xsi:type="int" name="parent_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Parent Id"/> - <column xsi:type="decimal" name="row_total" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="row_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Row Total"/> - <column xsi:type="decimal" name="price" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="price" scale="4" precision="20" unsigned="false" nullable="true" comment="Price"/> <column xsi:type="decimal" name="weight" scale="4" precision="12" unsigned="false" nullable="true" comment="Weight"/> @@ -855,13 +855,13 @@ <column xsi:type="text" name="description" nullable="true" comment="Description"/> <column xsi:type="varchar" name="name" nullable="true" length="255" comment="Name"/> <column xsi:type="varchar" name="sku" nullable="true" length="255" comment="Sku"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> - <constraint xsi:type="foreign" name="SALES_SHIPMENT_ITEM_PARENT_ID_SALES_SHIPMENT_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="SALES_SHIPMENT_ITEM_PARENT_ID_SALES_SHIPMENT_ENTITY_ID" table="sales_shipment_item" column="parent_id" referenceTable="sales_shipment" referenceColumn="entity_id" onDelete="CASCADE"/> - <index name="SALES_SHIPMENT_ITEM_PARENT_ID" indexType="btree"> + <index referenceId="SALES_SHIPMENT_ITEM_PARENT_ID" indexType="btree"> <column name="parent_id"/> </index> </table> @@ -883,19 +883,19 @@ comment="Created At"/> <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" comment="Updated At"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> - <constraint xsi:type="foreign" name="SALES_SHIPMENT_TRACK_PARENT_ID_SALES_SHIPMENT_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="SALES_SHIPMENT_TRACK_PARENT_ID_SALES_SHIPMENT_ENTITY_ID" table="sales_shipment_track" column="parent_id" referenceTable="sales_shipment" referenceColumn="entity_id" onDelete="CASCADE"/> - <index name="SALES_SHIPMENT_TRACK_PARENT_ID" indexType="btree"> + <index referenceId="SALES_SHIPMENT_TRACK_PARENT_ID" indexType="btree"> <column name="parent_id"/> </index> - <index name="SALES_SHIPMENT_TRACK_ORDER_ID" indexType="btree"> + <index referenceId="SALES_SHIPMENT_TRACK_ORDER_ID" indexType="btree"> <column name="order_id"/> </index> - <index name="SALES_SHIPMENT_TRACK_CREATED_AT" indexType="btree"> + <index referenceId="SALES_SHIPMENT_TRACK_CREATED_AT" indexType="btree"> <column name="created_at"/> </index> </table> @@ -911,16 +911,16 @@ <column xsi:type="text" name="comment" nullable="true" comment="Comment"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> - <constraint xsi:type="foreign" name="SALES_SHIPMENT_COMMENT_PARENT_ID_SALES_SHIPMENT_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="SALES_SHIPMENT_COMMENT_PARENT_ID_SALES_SHIPMENT_ENTITY_ID" table="sales_shipment_comment" column="parent_id" referenceTable="sales_shipment" referenceColumn="entity_id" onDelete="CASCADE"/> - <index name="SALES_SHIPMENT_COMMENT_CREATED_AT" indexType="btree"> + <index referenceId="SALES_SHIPMENT_COMMENT_CREATED_AT" indexType="btree"> <column name="created_at"/> </index> - <index name="SALES_SHIPMENT_COMMENT_PARENT_ID" indexType="btree"> + <index referenceId="SALES_SHIPMENT_COMMENT_PARENT_ID" indexType="btree"> <column name="parent_id"/> </index> </table> @@ -929,43 +929,43 @@ comment="Entity Id"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" comment="Store Id"/> - <column xsi:type="decimal" name="base_grand_total" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_grand_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Grand Total"/> - <column xsi:type="decimal" name="shipping_tax_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="shipping_tax_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Tax Amount"/> - <column xsi:type="decimal" name="tax_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="tax_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Tax Amount"/> - <column xsi:type="decimal" name="base_tax_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_tax_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Tax Amount"/> - <column xsi:type="decimal" name="store_to_order_rate" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="store_to_order_rate" scale="4" precision="20" unsigned="false" nullable="true" comment="Store To Order Rate"/> - <column xsi:type="decimal" name="base_shipping_tax_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_tax_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Tax Amount"/> - <column xsi:type="decimal" name="base_discount_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_discount_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Amount"/> - <column xsi:type="decimal" name="base_to_order_rate" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_to_order_rate" scale="4" precision="20" unsigned="false" nullable="true" comment="Base To Order Rate"/> - <column xsi:type="decimal" name="grand_total" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="grand_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Grand Total"/> - <column xsi:type="decimal" name="shipping_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="shipping_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Amount"/> - <column xsi:type="decimal" name="subtotal_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="subtotal_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Subtotal Incl Tax"/> - <column xsi:type="decimal" name="base_subtotal_incl_tax" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_subtotal_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Subtotal Incl Tax"/> - <column xsi:type="decimal" name="store_to_base_rate" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="store_to_base_rate" scale="4" precision="20" unsigned="false" nullable="true" comment="Store To Base Rate"/> - <column xsi:type="decimal" name="base_shipping_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Amount"/> <column xsi:type="decimal" name="total_qty" scale="4" precision="12" unsigned="false" nullable="true" comment="Total Qty"/> - <column xsi:type="decimal" name="base_to_global_rate" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_to_global_rate" scale="4" precision="20" unsigned="false" nullable="true" comment="Base To Global Rate"/> - <column xsi:type="decimal" name="subtotal" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="subtotal" scale="4" precision="20" unsigned="false" nullable="true" comment="Subtotal"/> - <column xsi:type="decimal" name="base_subtotal" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_subtotal" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Subtotal"/> - <column xsi:type="decimal" name="discount_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="discount_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Amount"/> <column xsi:type="int" name="billing_address_id" padding="11" unsigned="false" nullable="true" identity="false" comment="Billing Address Id"/> @@ -994,58 +994,58 @@ comment="Created At"/> <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" comment="Updated At"/> - <column xsi:type="decimal" name="discount_tax_compensation_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="base_discount_tax_compensation_amount" scale="4" precision="12" + <column xsi:type="decimal" name="base_discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="shipping_discount_tax_compensation_amount" scale="4" precision="12" + <column xsi:type="decimal" name="shipping_discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="base_shipping_discount_tax_compensation_amnt" scale="4" precision="12" + <column xsi:type="decimal" name="base_shipping_discount_tax_compensation_amnt" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="shipping_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="shipping_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Incl Tax"/> - <column xsi:type="decimal" name="base_shipping_incl_tax" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Incl Tax"/> - <column xsi:type="decimal" name="base_total_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_total_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Total Refunded"/> <column xsi:type="varchar" name="discount_description" nullable="true" length="255" comment="Discount Description"/> <column xsi:type="text" name="customer_note" nullable="true" comment="Customer Note"/> <column xsi:type="smallint" name="customer_note_notify" padding="5" unsigned="true" nullable="true" identity="false" comment="Customer Note Notify"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> - <constraint xsi:type="foreign" name="SALES_INVOICE_ORDER_ID_SALES_ORDER_ENTITY_ID" table="sales_invoice" + <constraint xsi:type="foreign" referenceId="SALES_INVOICE_ORDER_ID_SALES_ORDER_ENTITY_ID" table="sales_invoice" column="order_id" referenceTable="sales_order" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="SALES_INVOICE_STORE_ID_STORE_STORE_ID" table="sales_invoice" + <constraint xsi:type="foreign" referenceId="SALES_INVOICE_STORE_ID_STORE_STORE_ID" table="sales_invoice" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="SET NULL"/> - <constraint xsi:type="unique" name="SALES_INVOICE_INCREMENT_ID_STORE_ID"> + <constraint xsi:type="unique" referenceId="SALES_INVOICE_INCREMENT_ID_STORE_ID"> <column name="increment_id"/> <column name="store_id"/> </constraint> - <index name="SALES_INVOICE_STORE_ID" indexType="btree"> + <index referenceId="SALES_INVOICE_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="SALES_INVOICE_GRAND_TOTAL" indexType="btree"> + <index referenceId="SALES_INVOICE_GRAND_TOTAL" indexType="btree"> <column name="grand_total"/> </index> - <index name="SALES_INVOICE_ORDER_ID" indexType="btree"> + <index referenceId="SALES_INVOICE_ORDER_ID" indexType="btree"> <column name="order_id"/> </index> - <index name="SALES_INVOICE_STATE" indexType="btree"> + <index referenceId="SALES_INVOICE_STATE" indexType="btree"> <column name="state"/> </index> - <index name="SALES_INVOICE_CREATED_AT" indexType="btree"> + <index referenceId="SALES_INVOICE_CREATED_AT" indexType="btree"> <column name="created_at"/> </index> - <index name="SALES_INVOICE_UPDATED_AT" indexType="btree"> + <index referenceId="SALES_INVOICE_UPDATED_AT" indexType="btree"> <column name="updated_at"/> </index> - <index name="SALES_INVOICE_SEND_EMAIL" indexType="btree"> + <index referenceId="SALES_INVOICE_SEND_EMAIL" indexType="btree"> <column name="send_email"/> </index> - <index name="SALES_INVOICE_EMAIL_SENT" indexType="btree"> + <index referenceId="SALES_INVOICE_EMAIL_SENT" indexType="btree"> <column name="email_sent"/> </index> </table> @@ -1077,51 +1077,51 @@ <column xsi:type="varchar" name="shipping_address" nullable="true" length="255" comment="Shipping Address"/> <column xsi:type="varchar" name="shipping_information" nullable="true" length="255" comment="Shipping Method Name"/> - <column xsi:type="decimal" name="subtotal" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="subtotal" scale="4" precision="20" unsigned="false" nullable="true" comment="Subtotal"/> - <column xsi:type="decimal" name="shipping_and_handling" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="shipping_and_handling" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping and handling amount"/> - <column xsi:type="decimal" name="grand_total" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="grand_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Grand Total"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="true" comment="Created At"/> <column xsi:type="timestamp" name="updated_at" on_update="false" nullable="true" comment="Updated At"/> - <column xsi:type="decimal" name="base_grand_total" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_grand_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Grand Total"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> - <constraint xsi:type="unique" name="SALES_INVOICE_GRID_INCREMENT_ID_STORE_ID"> + <constraint xsi:type="unique" referenceId="SALES_INVOICE_GRID_INCREMENT_ID_STORE_ID"> <column name="increment_id"/> <column name="store_id"/> </constraint> - <index name="SALES_INVOICE_GRID_STORE_ID" indexType="btree"> + <index referenceId="SALES_INVOICE_GRID_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="SALES_INVOICE_GRID_GRAND_TOTAL" indexType="btree"> + <index referenceId="SALES_INVOICE_GRID_GRAND_TOTAL" indexType="btree"> <column name="grand_total"/> </index> - <index name="SALES_INVOICE_GRID_ORDER_ID" indexType="btree"> + <index referenceId="SALES_INVOICE_GRID_ORDER_ID" indexType="btree"> <column name="order_id"/> </index> - <index name="SALES_INVOICE_GRID_STATE" indexType="btree"> + <index referenceId="SALES_INVOICE_GRID_STATE" indexType="btree"> <column name="state"/> </index> - <index name="SALES_INVOICE_GRID_ORDER_INCREMENT_ID" indexType="btree"> + <index referenceId="SALES_INVOICE_GRID_ORDER_INCREMENT_ID" indexType="btree"> <column name="order_increment_id"/> </index> - <index name="SALES_INVOICE_GRID_CREATED_AT" indexType="btree"> + <index referenceId="SALES_INVOICE_GRID_CREATED_AT" indexType="btree"> <column name="created_at"/> </index> - <index name="SALES_INVOICE_GRID_UPDATED_AT" indexType="btree"> + <index referenceId="SALES_INVOICE_GRID_UPDATED_AT" indexType="btree"> <column name="updated_at"/> </index> - <index name="SALES_INVOICE_GRID_ORDER_CREATED_AT" indexType="btree"> + <index referenceId="SALES_INVOICE_GRID_ORDER_CREATED_AT" indexType="btree"> <column name="order_created_at"/> </index> - <index name="SALES_INVOICE_GRID_BILLING_NAME" indexType="btree"> + <index referenceId="SALES_INVOICE_GRID_BILLING_NAME" indexType="btree"> <column name="billing_name"/> </index> - <index name="FTI_95D9C924DD6A8734EB8B5D01D60F90DE" indexType="fulltext"> + <index referenceId="FTI_95D9C924DD6A8734EB8B5D01D60F90DE" indexType="fulltext"> <column name="increment_id"/> <column name="order_increment_id"/> <column name="billing_name"/> @@ -1130,7 +1130,7 @@ <column name="customer_name"/> <column name="customer_email"/> </index> - <index name="SALES_INVOICE_GRID_BASE_GRAND_TOTAL" indexType="btree"> + <index referenceId="SALES_INVOICE_GRID_BASE_GRAND_TOTAL" indexType="btree"> <column name="base_grand_total"/> </index> </table> @@ -1143,11 +1143,11 @@ comment="Base Price"/> <column xsi:type="decimal" name="tax_amount" scale="4" precision="12" unsigned="false" nullable="true" comment="Tax Amount"/> - <column xsi:type="decimal" name="base_row_total" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_row_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Row Total"/> <column xsi:type="decimal" name="discount_amount" scale="4" precision="12" unsigned="false" nullable="true" comment="Discount Amount"/> - <column xsi:type="decimal" name="row_total" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="row_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Row Total"/> <column xsi:type="decimal" name="base_discount_amount" scale="4" precision="12" unsigned="false" nullable="true" comment="Base Discount Amount"/> @@ -1180,13 +1180,13 @@ unsigned="false" nullable="true" comment="Base Discount Tax Compensation Amount"/> <column xsi:type="text" name="tax_ratio" nullable="true" comment="Ratio of tax invoiced over tax of the order item"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> - <constraint xsi:type="foreign" name="SALES_INVOICE_ITEM_PARENT_ID_SALES_INVOICE_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="SALES_INVOICE_ITEM_PARENT_ID_SALES_INVOICE_ENTITY_ID" table="sales_invoice_item" column="parent_id" referenceTable="sales_invoice" referenceColumn="entity_id" onDelete="CASCADE"/> - <index name="SALES_INVOICE_ITEM_PARENT_ID" indexType="btree"> + <index referenceId="SALES_INVOICE_ITEM_PARENT_ID" indexType="btree"> <column name="parent_id"/> </index> </table> @@ -1202,16 +1202,16 @@ <column xsi:type="text" name="comment" nullable="true" comment="Comment"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> - <constraint xsi:type="foreign" name="SALES_INVOICE_COMMENT_PARENT_ID_SALES_INVOICE_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="SALES_INVOICE_COMMENT_PARENT_ID_SALES_INVOICE_ENTITY_ID" table="sales_invoice_comment" column="parent_id" referenceTable="sales_invoice" referenceColumn="entity_id" onDelete="CASCADE"/> - <index name="SALES_INVOICE_COMMENT_CREATED_AT" indexType="btree"> + <index referenceId="SALES_INVOICE_COMMENT_CREATED_AT" indexType="btree"> <column name="created_at"/> </index> - <index name="SALES_INVOICE_COMMENT_PARENT_ID" indexType="btree"> + <index referenceId="SALES_INVOICE_COMMENT_PARENT_ID" indexType="btree"> <column name="parent_id"/> </index> </table> @@ -1220,53 +1220,53 @@ comment="Entity Id"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" comment="Store Id"/> - <column xsi:type="decimal" name="adjustment_positive" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="adjustment_positive" scale="4" precision="20" unsigned="false" nullable="true" comment="Adjustment Positive"/> - <column xsi:type="decimal" name="base_shipping_tax_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_tax_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Tax Amount"/> - <column xsi:type="decimal" name="store_to_order_rate" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="store_to_order_rate" scale="4" precision="20" unsigned="false" nullable="true" comment="Store To Order Rate"/> - <column xsi:type="decimal" name="base_discount_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_discount_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Amount"/> - <column xsi:type="decimal" name="base_to_order_rate" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_to_order_rate" scale="4" precision="20" unsigned="false" nullable="true" comment="Base To Order Rate"/> - <column xsi:type="decimal" name="grand_total" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="grand_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Grand Total"/> - <column xsi:type="decimal" name="base_adjustment_negative" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_adjustment_negative" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Adjustment Negative"/> - <column xsi:type="decimal" name="base_subtotal_incl_tax" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_subtotal_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Subtotal Incl Tax"/> - <column xsi:type="decimal" name="shipping_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="shipping_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Amount"/> - <column xsi:type="decimal" name="subtotal_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="subtotal_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Subtotal Incl Tax"/> - <column xsi:type="decimal" name="adjustment_negative" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="adjustment_negative" scale="4" precision="20" unsigned="false" nullable="true" comment="Adjustment Negative"/> - <column xsi:type="decimal" name="base_shipping_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Amount"/> - <column xsi:type="decimal" name="store_to_base_rate" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="store_to_base_rate" scale="4" precision="20" unsigned="false" nullable="true" comment="Store To Base Rate"/> - <column xsi:type="decimal" name="base_to_global_rate" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_to_global_rate" scale="4" precision="20" unsigned="false" nullable="true" comment="Base To Global Rate"/> - <column xsi:type="decimal" name="base_adjustment" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_adjustment" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Adjustment"/> - <column xsi:type="decimal" name="base_subtotal" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_subtotal" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Subtotal"/> - <column xsi:type="decimal" name="discount_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="discount_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Amount"/> - <column xsi:type="decimal" name="subtotal" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="subtotal" scale="4" precision="20" unsigned="false" nullable="true" comment="Subtotal"/> - <column xsi:type="decimal" name="adjustment" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="adjustment" scale="4" precision="20" unsigned="false" nullable="true" comment="Adjustment"/> - <column xsi:type="decimal" name="base_grand_total" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_grand_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Grand Total"/> - <column xsi:type="decimal" name="base_adjustment_positive" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_adjustment_positive" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Adjustment Positive"/> - <column xsi:type="decimal" name="base_tax_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_tax_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Tax Amount"/> - <column xsi:type="decimal" name="shipping_tax_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="shipping_tax_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Tax Amount"/> - <column xsi:type="decimal" name="tax_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="tax_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Tax Amount"/> <column xsi:type="int" name="order_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Order Id"/> @@ -1295,56 +1295,56 @@ comment="Created At"/> <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" comment="Updated At"/> - <column xsi:type="decimal" name="discount_tax_compensation_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="base_discount_tax_compensation_amount" scale="4" precision="12" + <column xsi:type="decimal" name="base_discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="shipping_discount_tax_compensation_amount" scale="4" precision="12" + <column xsi:type="decimal" name="shipping_discount_tax_compensation_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="base_shipping_discount_tax_compensation_amnt" scale="4" precision="12" + <column xsi:type="decimal" name="base_shipping_discount_tax_compensation_amnt" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Discount Tax Compensation Amount"/> - <column xsi:type="decimal" name="shipping_incl_tax" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="shipping_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping Incl Tax"/> - <column xsi:type="decimal" name="base_shipping_incl_tax" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="base_shipping_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Shipping Incl Tax"/> <column xsi:type="varchar" name="discount_description" nullable="true" length="255" comment="Discount Description"/> <column xsi:type="text" name="customer_note" nullable="true" comment="Customer Note"/> <column xsi:type="smallint" name="customer_note_notify" padding="5" unsigned="true" nullable="true" identity="false" comment="Customer Note Notify"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> - <constraint xsi:type="foreign" name="SALES_CREDITMEMO_ORDER_ID_SALES_ORDER_ENTITY_ID" table="sales_creditmemo" + <constraint xsi:type="foreign" referenceId="SALES_CREDITMEMO_ORDER_ID_SALES_ORDER_ENTITY_ID" table="sales_creditmemo" column="order_id" referenceTable="sales_order" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="SALES_CREDITMEMO_STORE_ID_STORE_STORE_ID" table="sales_creditmemo" + <constraint xsi:type="foreign" referenceId="SALES_CREDITMEMO_STORE_ID_STORE_STORE_ID" table="sales_creditmemo" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="SET NULL"/> - <constraint xsi:type="unique" name="SALES_CREDITMEMO_INCREMENT_ID_STORE_ID"> + <constraint xsi:type="unique" referenceId="SALES_CREDITMEMO_INCREMENT_ID_STORE_ID"> <column name="increment_id"/> <column name="store_id"/> </constraint> - <index name="SALES_CREDITMEMO_STORE_ID" indexType="btree"> + <index referenceId="SALES_CREDITMEMO_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="SALES_CREDITMEMO_ORDER_ID" indexType="btree"> + <index referenceId="SALES_CREDITMEMO_ORDER_ID" indexType="btree"> <column name="order_id"/> </index> - <index name="SALES_CREDITMEMO_CREDITMEMO_STATUS" indexType="btree"> + <index referenceId="SALES_CREDITMEMO_CREDITMEMO_STATUS" indexType="btree"> <column name="creditmemo_status"/> </index> - <index name="SALES_CREDITMEMO_STATE" indexType="btree"> + <index referenceId="SALES_CREDITMEMO_STATE" indexType="btree"> <column name="state"/> </index> - <index name="SALES_CREDITMEMO_CREATED_AT" indexType="btree"> + <index referenceId="SALES_CREDITMEMO_CREATED_AT" indexType="btree"> <column name="created_at"/> </index> - <index name="SALES_CREDITMEMO_UPDATED_AT" indexType="btree"> + <index referenceId="SALES_CREDITMEMO_UPDATED_AT" indexType="btree"> <column name="updated_at"/> </index> - <index name="SALES_CREDITMEMO_SEND_EMAIL" indexType="btree"> + <index referenceId="SALES_CREDITMEMO_SEND_EMAIL" indexType="btree"> <column name="send_email"/> </index> - <index name="SALES_CREDITMEMO_EMAIL_SENT" indexType="btree"> + <index referenceId="SALES_CREDITMEMO_EMAIL_SENT" indexType="btree"> <column name="email_sent"/> </index> </table> @@ -1362,7 +1362,7 @@ <column xsi:type="varchar" name="billing_name" nullable="true" length="255" comment="Billing Name"/> <column xsi:type="int" name="state" padding="11" unsigned="false" nullable="true" identity="false" comment="Status"/> - <column xsi:type="decimal" name="base_grand_total" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_grand_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Grand Total"/> <column xsi:type="varchar" name="order_status" nullable="true" length="32" comment="Order Status"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" @@ -1376,57 +1376,57 @@ <column xsi:type="varchar" name="payment_method" nullable="true" length="32" comment="Payment Method"/> <column xsi:type="varchar" name="shipping_information" nullable="true" length="255" comment="Shipping Method Name"/> - <column xsi:type="decimal" name="subtotal" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="subtotal" scale="4" precision="20" unsigned="false" nullable="true" comment="Subtotal"/> - <column xsi:type="decimal" name="shipping_and_handling" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="shipping_and_handling" scale="4" precision="20" unsigned="false" nullable="true" comment="Shipping and handling amount"/> - <column xsi:type="decimal" name="adjustment_positive" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="adjustment_positive" scale="4" precision="20" unsigned="false" nullable="true" comment="Adjustment Positive"/> - <column xsi:type="decimal" name="adjustment_negative" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="adjustment_negative" scale="4" precision="20" unsigned="false" nullable="true" comment="Adjustment Negative"/> - <column xsi:type="decimal" name="order_base_grand_total" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="order_base_grand_total" scale="4" precision="20" unsigned="false" nullable="true" comment="Order Grand Total"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> - <constraint xsi:type="unique" name="SALES_CREDITMEMO_GRID_INCREMENT_ID_STORE_ID"> + <constraint xsi:type="unique" referenceId="SALES_CREDITMEMO_GRID_INCREMENT_ID_STORE_ID"> <column name="increment_id"/> <column name="store_id"/> </constraint> - <index name="SALES_CREDITMEMO_GRID_ORDER_INCREMENT_ID" indexType="btree"> + <index referenceId="SALES_CREDITMEMO_GRID_ORDER_INCREMENT_ID" indexType="btree"> <column name="order_increment_id"/> </index> - <index name="SALES_CREDITMEMO_GRID_CREATED_AT" indexType="btree"> + <index referenceId="SALES_CREDITMEMO_GRID_CREATED_AT" indexType="btree"> <column name="created_at"/> </index> - <index name="SALES_CREDITMEMO_GRID_UPDATED_AT" indexType="btree"> + <index referenceId="SALES_CREDITMEMO_GRID_UPDATED_AT" indexType="btree"> <column name="updated_at"/> </index> - <index name="SALES_CREDITMEMO_GRID_ORDER_CREATED_AT" indexType="btree"> + <index referenceId="SALES_CREDITMEMO_GRID_ORDER_CREATED_AT" indexType="btree"> <column name="order_created_at"/> </index> - <index name="SALES_CREDITMEMO_GRID_STATE" indexType="btree"> + <index referenceId="SALES_CREDITMEMO_GRID_STATE" indexType="btree"> <column name="state"/> </index> - <index name="SALES_CREDITMEMO_GRID_BILLING_NAME" indexType="btree"> + <index referenceId="SALES_CREDITMEMO_GRID_BILLING_NAME" indexType="btree"> <column name="billing_name"/> </index> - <index name="SALES_CREDITMEMO_GRID_ORDER_STATUS" indexType="btree"> + <index referenceId="SALES_CREDITMEMO_GRID_ORDER_STATUS" indexType="btree"> <column name="order_status"/> </index> - <index name="SALES_CREDITMEMO_GRID_BASE_GRAND_TOTAL" indexType="btree"> + <index referenceId="SALES_CREDITMEMO_GRID_BASE_GRAND_TOTAL" indexType="btree"> <column name="base_grand_total"/> </index> - <index name="SALES_CREDITMEMO_GRID_STORE_ID" indexType="btree"> + <index referenceId="SALES_CREDITMEMO_GRID_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="SALES_CREDITMEMO_GRID_ORDER_BASE_GRAND_TOTAL" indexType="btree"> + <index referenceId="SALES_CREDITMEMO_GRID_ORDER_BASE_GRAND_TOTAL" indexType="btree"> <column name="order_base_grand_total"/> </index> - <index name="SALES_CREDITMEMO_GRID_ORDER_ID" indexType="btree"> + <index referenceId="SALES_CREDITMEMO_GRID_ORDER_ID" indexType="btree"> <column name="order_id"/> </index> - <index name="FTI_32B7BA885941A8254EE84AE650ABDC86" indexType="fulltext"> + <index referenceId="FTI_32B7BA885941A8254EE84AE650ABDC86" indexType="fulltext"> <column name="increment_id"/> <column name="order_increment_id"/> <column name="billing_name"/> @@ -1482,13 +1482,13 @@ unsigned="false" nullable="true" comment="Base Discount Tax Compensation Amount"/> <column xsi:type="text" name="tax_ratio" nullable="true" comment="Ratio of tax in the creditmemo item over tax of the order item"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> - <constraint xsi:type="foreign" name="SALES_CREDITMEMO_ITEM_PARENT_ID_SALES_CREDITMEMO_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="SALES_CREDITMEMO_ITEM_PARENT_ID_SALES_CREDITMEMO_ENTITY_ID" table="sales_creditmemo_item" column="parent_id" referenceTable="sales_creditmemo" referenceColumn="entity_id" onDelete="CASCADE"/> - <index name="SALES_CREDITMEMO_ITEM_PARENT_ID" indexType="btree"> + <index referenceId="SALES_CREDITMEMO_ITEM_PARENT_ID" indexType="btree"> <column name="parent_id"/> </index> </table> @@ -1504,16 +1504,16 @@ <column xsi:type="text" name="comment" nullable="true" comment="Comment"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> - <constraint xsi:type="foreign" name="SALES_CREDITMEMO_COMMENT_PARENT_ID_SALES_CREDITMEMO_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="SALES_CREDITMEMO_COMMENT_PARENT_ID_SALES_CREDITMEMO_ENTITY_ID" table="sales_creditmemo_comment" column="parent_id" referenceTable="sales_creditmemo" referenceColumn="entity_id" onDelete="CASCADE"/> - <index name="SALES_CREDITMEMO_COMMENT_CREATED_AT" indexType="btree"> + <index referenceId="SALES_CREDITMEMO_COMMENT_CREATED_AT" indexType="btree"> <column name="created_at"/> </index> - <index name="SALES_CREDITMEMO_COMMENT_PARENT_ID" indexType="btree"> + <index referenceId="SALES_CREDITMEMO_COMMENT_PARENT_ID" indexType="btree"> <column name="parent_id"/> </index> </table> @@ -1533,18 +1533,18 @@ comment="Invoiced Captured"/> <column xsi:type="decimal" name="invoiced_not_captured" scale="4" precision="12" unsigned="false" nullable="true" comment="Invoiced Not Captured"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> </constraint> - <constraint xsi:type="foreign" name="SALES_INVOICED_AGGREGATED_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="SALES_INVOICED_AGGREGATED_STORE_ID_STORE_STORE_ID" table="sales_invoiced_aggregated" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="SET NULL"/> - <constraint xsi:type="unique" name="SALES_INVOICED_AGGREGATED_PERIOD_STORE_ID_ORDER_STATUS"> + <constraint xsi:type="unique" referenceId="SALES_INVOICED_AGGREGATED_PERIOD_STORE_ID_ORDER_STATUS"> <column name="period"/> <column name="store_id"/> <column name="order_status"/> </constraint> - <index name="SALES_INVOICED_AGGREGATED_STORE_ID" indexType="btree"> + <index referenceId="SALES_INVOICED_AGGREGATED_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -1565,18 +1565,18 @@ comment="Invoiced Captured"/> <column xsi:type="decimal" name="invoiced_not_captured" scale="4" precision="12" unsigned="false" nullable="true" comment="Invoiced Not Captured"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> </constraint> - <constraint xsi:type="foreign" name="SALES_INVOICED_AGGREGATED_ORDER_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="SALES_INVOICED_AGGREGATED_ORDER_STORE_ID_STORE_STORE_ID" table="sales_invoiced_aggregated_order" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="SET NULL"/> - <constraint xsi:type="unique" name="SALES_INVOICED_AGGREGATED_ORDER_PERIOD_STORE_ID_ORDER_STATUS"> + <constraint xsi:type="unique" referenceId="SALES_INVOICED_AGGREGATED_ORDER_PERIOD_STORE_ID_ORDER_STATUS"> <column name="period"/> <column name="store_id"/> <column name="order_status"/> </constraint> - <index name="SALES_INVOICED_AGGREGATED_ORDER_STORE_ID" indexType="btree"> + <index referenceId="SALES_INVOICED_AGGREGATED_ORDER_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -1593,44 +1593,44 @@ default="0" comment="Total Qty Ordered"/> <column xsi:type="decimal" name="total_qty_invoiced" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Total Qty Invoiced"/> - <column xsi:type="decimal" name="total_income_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_income_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Income Amount"/> - <column xsi:type="decimal" name="total_revenue_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_revenue_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Revenue Amount"/> - <column xsi:type="decimal" name="total_profit_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_profit_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Profit Amount"/> - <column xsi:type="decimal" name="total_invoiced_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_invoiced_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Invoiced Amount"/> - <column xsi:type="decimal" name="total_canceled_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_canceled_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Canceled Amount"/> - <column xsi:type="decimal" name="total_paid_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="total_paid_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Paid Amount"/> - <column xsi:type="decimal" name="total_refunded_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_refunded_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Refunded Amount"/> - <column xsi:type="decimal" name="total_tax_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="total_tax_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Tax Amount"/> - <column xsi:type="decimal" name="total_tax_amount_actual" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_tax_amount_actual" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Tax Amount Actual"/> - <column xsi:type="decimal" name="total_shipping_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_shipping_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Shipping Amount"/> - <column xsi:type="decimal" name="total_shipping_amount_actual" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_shipping_amount_actual" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Shipping Amount Actual"/> - <column xsi:type="decimal" name="total_discount_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_discount_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Discount Amount"/> - <column xsi:type="decimal" name="total_discount_amount_actual" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_discount_amount_actual" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Discount Amount Actual"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> </constraint> - <constraint xsi:type="foreign" name="SALES_ORDER_AGGREGATED_CREATED_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="SALES_ORDER_AGGREGATED_CREATED_STORE_ID_STORE_STORE_ID" table="sales_order_aggregated_created" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="SET NULL"/> - <constraint xsi:type="unique" name="SALES_ORDER_AGGREGATED_CREATED_PERIOD_STORE_ID_ORDER_STATUS"> + <constraint xsi:type="unique" referenceId="SALES_ORDER_AGGREGATED_CREATED_PERIOD_STORE_ID_ORDER_STATUS"> <column name="period"/> <column name="store_id"/> <column name="order_status"/> </constraint> - <index name="SALES_ORDER_AGGREGATED_CREATED_STORE_ID" indexType="btree"> + <index referenceId="SALES_ORDER_AGGREGATED_CREATED_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -1647,44 +1647,44 @@ default="0" comment="Total Qty Ordered"/> <column xsi:type="decimal" name="total_qty_invoiced" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Total Qty Invoiced"/> - <column xsi:type="decimal" name="total_income_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_income_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Income Amount"/> - <column xsi:type="decimal" name="total_revenue_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_revenue_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Revenue Amount"/> - <column xsi:type="decimal" name="total_profit_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_profit_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Profit Amount"/> - <column xsi:type="decimal" name="total_invoiced_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_invoiced_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Invoiced Amount"/> - <column xsi:type="decimal" name="total_canceled_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_canceled_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Canceled Amount"/> - <column xsi:type="decimal" name="total_paid_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="total_paid_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Paid Amount"/> - <column xsi:type="decimal" name="total_refunded_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_refunded_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Refunded Amount"/> - <column xsi:type="decimal" name="total_tax_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="total_tax_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Tax Amount"/> - <column xsi:type="decimal" name="total_tax_amount_actual" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_tax_amount_actual" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Tax Amount Actual"/> - <column xsi:type="decimal" name="total_shipping_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_shipping_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Shipping Amount"/> - <column xsi:type="decimal" name="total_shipping_amount_actual" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_shipping_amount_actual" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Shipping Amount Actual"/> - <column xsi:type="decimal" name="total_discount_amount" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_discount_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Discount Amount"/> - <column xsi:type="decimal" name="total_discount_amount_actual" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_discount_amount_actual" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Discount Amount Actual"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> </constraint> - <constraint xsi:type="foreign" name="SALES_ORDER_AGGREGATED_UPDATED_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="SALES_ORDER_AGGREGATED_UPDATED_STORE_ID_STORE_STORE_ID" table="sales_order_aggregated_updated" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="SET NULL"/> - <constraint xsi:type="unique" name="SALES_ORDER_AGGREGATED_UPDATED_PERIOD_STORE_ID_ORDER_STATUS"> + <constraint xsi:type="unique" referenceId="SALES_ORDER_AGGREGATED_UPDATED_PERIOD_STORE_ID_ORDER_STATUS"> <column name="period"/> <column name="store_id"/> <column name="order_status"/> </constraint> - <index name="SALES_ORDER_AGGREGATED_UPDATED_STORE_ID" indexType="btree"> + <index referenceId="SALES_ORDER_AGGREGATED_UPDATED_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -1705,27 +1705,27 @@ <column xsi:type="blob" name="additional_information" nullable="true" comment="Additional Information"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="transaction_id"/> </constraint> - <constraint xsi:type="foreign" name="SALES_PAYMENT_TRANSACTION_ORDER_ID_SALES_ORDER_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="SALES_PAYMENT_TRANSACTION_ORDER_ID_SALES_ORDER_ENTITY_ID" table="sales_payment_transaction" column="order_id" referenceTable="sales_order" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="FK_B99FF1A06402D725EBDB0F3A7ECD47A2" table="sales_payment_transaction" + <constraint xsi:type="foreign" referenceId="FK_B99FF1A06402D725EBDB0F3A7ECD47A2" table="sales_payment_transaction" column="parent_id" referenceTable="sales_payment_transaction" referenceColumn="transaction_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="SALES_PAYMENT_TRANSACTION_PAYMENT_ID_SALES_ORDER_PAYMENT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="SALES_PAYMENT_TRANSACTION_PAYMENT_ID_SALES_ORDER_PAYMENT_ENTT_ID" table="sales_payment_transaction" column="payment_id" referenceTable="sales_order_payment" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="SALES_PAYMENT_TRANSACTION_ORDER_ID_PAYMENT_ID_TXN_ID"> + <constraint xsi:type="unique" referenceId="SALES_PAYMENT_TRANSACTION_ORDER_ID_PAYMENT_ID_TXN_ID"> <column name="order_id"/> <column name="payment_id"/> <column name="txn_id"/> </constraint> - <index name="SALES_PAYMENT_TRANSACTION_PARENT_ID" indexType="btree"> + <index referenceId="SALES_PAYMENT_TRANSACTION_PARENT_ID" indexType="btree"> <column name="parent_id"/> </index> - <index name="SALES_PAYMENT_TRANSACTION_PAYMENT_ID" indexType="btree"> + <index referenceId="SALES_PAYMENT_TRANSACTION_PAYMENT_ID" indexType="btree"> <column name="payment_id"/> </index> </table> @@ -1737,24 +1737,24 @@ <column xsi:type="varchar" name="order_status" nullable="false" length="50" comment="Order Status"/> <column xsi:type="int" name="orders_count" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Orders Count"/> - <column xsi:type="decimal" name="refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Refunded"/> - <column xsi:type="decimal" name="online_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="online_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Online Refunded"/> - <column xsi:type="decimal" name="offline_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="offline_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Offline Refunded"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> </constraint> - <constraint xsi:type="foreign" name="SALES_REFUNDED_AGGREGATED_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="SALES_REFUNDED_AGGREGATED_STORE_ID_STORE_STORE_ID" table="sales_refunded_aggregated" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="SET NULL"/> - <constraint xsi:type="unique" name="SALES_REFUNDED_AGGREGATED_PERIOD_STORE_ID_ORDER_STATUS"> + <constraint xsi:type="unique" referenceId="SALES_REFUNDED_AGGREGATED_PERIOD_STORE_ID_ORDER_STATUS"> <column name="period"/> <column name="store_id"/> <column name="order_status"/> </constraint> - <index name="SALES_REFUNDED_AGGREGATED_STORE_ID" indexType="btree"> + <index referenceId="SALES_REFUNDED_AGGREGATED_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -1767,24 +1767,24 @@ <column xsi:type="varchar" name="order_status" nullable="true" length="50" comment="Order Status"/> <column xsi:type="int" name="orders_count" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Orders Count"/> - <column xsi:type="decimal" name="refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Refunded"/> - <column xsi:type="decimal" name="online_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="online_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Online Refunded"/> - <column xsi:type="decimal" name="offline_refunded" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="offline_refunded" scale="4" precision="20" unsigned="false" nullable="true" comment="Offline Refunded"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> </constraint> - <constraint xsi:type="foreign" name="SALES_REFUNDED_AGGREGATED_ORDER_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="SALES_REFUNDED_AGGREGATED_ORDER_STORE_ID_STORE_STORE_ID" table="sales_refunded_aggregated_order" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="SET NULL"/> - <constraint xsi:type="unique" name="SALES_REFUNDED_AGGREGATED_ORDER_PERIOD_STORE_ID_ORDER_STATUS"> + <constraint xsi:type="unique" referenceId="SALES_REFUNDED_AGGREGATED_ORDER_PERIOD_STORE_ID_ORDER_STATUS"> <column name="period"/> <column name="store_id"/> <column name="order_status"/> </constraint> - <index name="SALES_REFUNDED_AGGREGATED_ORDER_STORE_ID" indexType="btree"> + <index referenceId="SALES_REFUNDED_AGGREGATED_ORDER_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -1798,23 +1798,23 @@ comment="Shipping Description"/> <column xsi:type="int" name="orders_count" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Orders Count"/> - <column xsi:type="decimal" name="total_shipping" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="total_shipping" scale="4" precision="20" unsigned="false" nullable="true" comment="Total Shipping"/> - <column xsi:type="decimal" name="total_shipping_actual" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_shipping_actual" scale="4" precision="20" unsigned="false" nullable="true" comment="Total Shipping Actual"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> </constraint> - <constraint xsi:type="foreign" name="SALES_SHIPPING_AGGREGATED_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="SALES_SHIPPING_AGGREGATED_STORE_ID_STORE_STORE_ID" table="sales_shipping_aggregated" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="SET NULL"/> - <constraint xsi:type="unique" name="SALES_SHPP_AGGRED_PERIOD_STORE_ID_ORDER_STS_SHPP_DESCRIPTION"> + <constraint xsi:type="unique" referenceId="SALES_SHPP_AGGRED_PERIOD_STORE_ID_ORDER_STS_SHPP_DESCRIPTION"> <column name="period"/> <column name="store_id"/> <column name="order_status"/> <column name="shipping_description"/> </constraint> - <index name="SALES_SHIPPING_AGGREGATED_STORE_ID" indexType="btree"> + <index referenceId="SALES_SHIPPING_AGGREGATED_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -1829,23 +1829,23 @@ comment="Shipping Description"/> <column xsi:type="int" name="orders_count" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Orders Count"/> - <column xsi:type="decimal" name="total_shipping" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="total_shipping" scale="4" precision="20" unsigned="false" nullable="true" comment="Total Shipping"/> - <column xsi:type="decimal" name="total_shipping_actual" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_shipping_actual" scale="4" precision="20" unsigned="false" nullable="true" comment="Total Shipping Actual"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> </constraint> - <constraint xsi:type="foreign" name="SALES_SHIPPING_AGGREGATED_ORDER_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="SALES_SHIPPING_AGGREGATED_ORDER_STORE_ID_STORE_STORE_ID" table="sales_shipping_aggregated_order" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="SET NULL"/> - <constraint xsi:type="unique" name="UNQ_C05FAE47282EEA68654D0924E946761F"> + <constraint xsi:type="unique" referenceId="UNQ_C05FAE47282EEA68654D0924E946761F"> <column name="period"/> <column name="store_id"/> <column name="order_status"/> <column name="shipping_description"/> </constraint> - <index name="SALES_SHIPPING_AGGREGATED_ORDER_STORE_ID" indexType="btree"> + <index referenceId="SALES_SHIPPING_AGGREGATED_ORDER_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -1864,21 +1864,21 @@ default="0" comment="Qty Ordered"/> <column xsi:type="smallint" name="rating_pos" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Rating Pos"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> </constraint> - <constraint xsi:type="foreign" name="SALES_BESTSELLERS_AGGREGATED_DAILY_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="SALES_BESTSELLERS_AGGREGATED_DAILY_STORE_ID_STORE_STORE_ID" table="sales_bestsellers_aggregated_daily" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="SALES_BESTSELLERS_AGGREGATED_DAILY_PERIOD_STORE_ID_PRODUCT_ID"> + <constraint xsi:type="unique" referenceId="SALES_BESTSELLERS_AGGREGATED_DAILY_PERIOD_STORE_ID_PRODUCT_ID"> <column name="period"/> <column name="store_id"/> <column name="product_id"/> </constraint> - <index name="SALES_BESTSELLERS_AGGREGATED_DAILY_STORE_ID" indexType="btree"> + <index referenceId="SALES_BESTSELLERS_AGGREGATED_DAILY_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="SALES_BESTSELLERS_AGGREGATED_DAILY_PRODUCT_ID" indexType="btree"> + <index referenceId="SALES_BESTSELLERS_AGGREGATED_DAILY_PRODUCT_ID" indexType="btree"> <column name="product_id"/> </index> </table> @@ -1897,21 +1897,21 @@ default="0" comment="Qty Ordered"/> <column xsi:type="smallint" name="rating_pos" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Rating Pos"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> </constraint> - <constraint xsi:type="foreign" name="SALES_BESTSELLERS_AGGREGATED_MONTHLY_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="SALES_BESTSELLERS_AGGREGATED_MONTHLY_STORE_ID_STORE_STORE_ID" table="sales_bestsellers_aggregated_monthly" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="SALES_BESTSELLERS_AGGREGATED_MONTHLY_PERIOD_STORE_ID_PRODUCT_ID"> + <constraint xsi:type="unique" referenceId="SALES_BESTSELLERS_AGGREGATED_MONTHLY_PERIOD_STORE_ID_PRODUCT_ID"> <column name="period"/> <column name="store_id"/> <column name="product_id"/> </constraint> - <index name="SALES_BESTSELLERS_AGGREGATED_MONTHLY_STORE_ID" indexType="btree"> + <index referenceId="SALES_BESTSELLERS_AGGREGATED_MONTHLY_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="SALES_BESTSELLERS_AGGREGATED_MONTHLY_PRODUCT_ID" indexType="btree"> + <index referenceId="SALES_BESTSELLERS_AGGREGATED_MONTHLY_PRODUCT_ID" indexType="btree"> <column name="product_id"/> </index> </table> @@ -1930,21 +1930,21 @@ default="0" comment="Qty Ordered"/> <column xsi:type="smallint" name="rating_pos" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Rating Pos"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> </constraint> - <constraint xsi:type="foreign" name="SALES_BESTSELLERS_AGGREGATED_YEARLY_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="SALES_BESTSELLERS_AGGREGATED_YEARLY_STORE_ID_STORE_STORE_ID" table="sales_bestsellers_aggregated_yearly" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="SALES_BESTSELLERS_AGGREGATED_YEARLY_PERIOD_STORE_ID_PRODUCT_ID"> + <constraint xsi:type="unique" referenceId="SALES_BESTSELLERS_AGGREGATED_YEARLY_PERIOD_STORE_ID_PRODUCT_ID"> <column name="period"/> <column name="store_id"/> <column name="product_id"/> </constraint> - <index name="SALES_BESTSELLERS_AGGREGATED_YEARLY_STORE_ID" indexType="btree"> + <index referenceId="SALES_BESTSELLERS_AGGREGATED_YEARLY_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="SALES_BESTSELLERS_AGGREGATED_YEARLY_PRODUCT_ID" indexType="btree"> + <index referenceId="SALES_BESTSELLERS_AGGREGATED_YEARLY_PRODUCT_ID" indexType="btree"> <column name="product_id"/> </index> </table> @@ -1957,22 +1957,22 @@ <column xsi:type="varchar" name="title" nullable="true" length="255" comment="Title"/> <column xsi:type="decimal" name="percent" scale="4" precision="12" unsigned="false" nullable="true" comment="Percent"/> - <column xsi:type="decimal" name="amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Amount"/> <column xsi:type="int" name="priority" padding="11" unsigned="false" nullable="false" identity="false" comment="Priority"/> <column xsi:type="int" name="position" padding="11" unsigned="false" nullable="false" identity="false" comment="Position"/> - <column xsi:type="decimal" name="base_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Amount"/> <column xsi:type="smallint" name="process" padding="6" unsigned="false" nullable="false" identity="false" comment="Process"/> - <column xsi:type="decimal" name="base_real_amount" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_real_amount" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Real Amount"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="tax_id"/> </constraint> - <index name="SALES_ORDER_TAX_ORDER_ID_PRIORITY_POSITION" indexType="btree"> + <index referenceId="SALES_ORDER_TAX_ORDER_ID_PRIORITY_POSITION" indexType="btree"> <column name="order_id"/> <column name="priority"/> <column name="position"/> @@ -1987,42 +1987,42 @@ comment="Item Id"/> <column xsi:type="decimal" name="tax_percent" scale="4" precision="12" unsigned="false" nullable="false" comment="Real Tax Percent For Item"/> - <column xsi:type="decimal" name="amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="amount" scale="4" precision="20" unsigned="false" nullable="false" comment="Tax amount for the item and tax rate"/> - <column xsi:type="decimal" name="base_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="base_amount" scale="4" precision="20" unsigned="false" nullable="false" comment="Base tax amount for the item and tax rate"/> - <column xsi:type="decimal" name="real_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="real_amount" scale="4" precision="20" unsigned="false" nullable="false" comment="Real tax amount for the item and tax rate"/> - <column xsi:type="decimal" name="real_base_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="real_base_amount" scale="4" precision="20" unsigned="false" nullable="false" comment="Real base tax amount for the item and tax rate"/> <column xsi:type="int" name="associated_item_id" padding="10" unsigned="true" nullable="true" identity="false" comment="Id of the associated item"/> <column xsi:type="varchar" name="taxable_item_type" nullable="false" length="32" comment="Type of the taxable item"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="tax_item_id"/> </constraint> - <constraint xsi:type="foreign" name="SALES_ORDER_TAX_ITEM_ASSOCIATED_ITEM_ID_SALES_ORDER_ITEM_ITEM_ID" + <constraint xsi:type="foreign" referenceId="SALES_ORDER_TAX_ITEM_ASSOCIATED_ITEM_ID_SALES_ORDER_ITEM_ITEM_ID" table="sales_order_tax_item" column="associated_item_id" referenceTable="sales_order_item" referenceColumn="item_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="SALES_ORDER_TAX_ITEM_TAX_ID_SALES_ORDER_TAX_TAX_ID" + <constraint xsi:type="foreign" referenceId="SALES_ORDER_TAX_ITEM_TAX_ID_SALES_ORDER_TAX_TAX_ID" table="sales_order_tax_item" column="tax_id" referenceTable="sales_order_tax" referenceColumn="tax_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="SALES_ORDER_TAX_ITEM_ITEM_ID_SALES_ORDER_ITEM_ITEM_ID" + <constraint xsi:type="foreign" referenceId="SALES_ORDER_TAX_ITEM_ITEM_ID_SALES_ORDER_ITEM_ITEM_ID" table="sales_order_tax_item" column="item_id" referenceTable="sales_order_item" referenceColumn="item_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="SALES_ORDER_TAX_ITEM_TAX_ID_ITEM_ID"> + <constraint xsi:type="unique" referenceId="SALES_ORDER_TAX_ITEM_TAX_ID_ITEM_ID"> <column name="tax_id"/> <column name="item_id"/> </constraint> - <index name="SALES_ORDER_TAX_ITEM_ITEM_ID" indexType="btree"> + <index referenceId="SALES_ORDER_TAX_ITEM_ITEM_ID" indexType="btree"> <column name="item_id"/> </index> </table> <table name="sales_order_status" resource="sales" engine="innodb" comment="Sales Order Status Table"> <column xsi:type="varchar" name="status" nullable="false" length="32" comment="Status"/> <column xsi:type="varchar" name="label" nullable="false" length="128" comment="Label"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="status"/> </constraint> </table> @@ -2033,11 +2033,11 @@ default="0" comment="Is Default"/> <column xsi:type="smallint" name="visible_on_front" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Visible on front"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="status"/> <column name="state"/> </constraint> - <constraint xsi:type="foreign" name="SALES_ORDER_STATUS_STATE_STATUS_SALES_ORDER_STATUS_STATUS" + <constraint xsi:type="foreign" referenceId="SALES_ORDER_STATUS_STATE_STATUS_SALES_ORDER_STATUS_STATUS" table="sales_order_status_state" column="status" referenceTable="sales_order_status" referenceColumn="status" onDelete="CASCADE"/> </table> @@ -2046,17 +2046,17 @@ <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Store Id"/> <column xsi:type="varchar" name="label" nullable="false" length="128" comment="Label"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="status"/> <column name="store_id"/> </constraint> - <constraint xsi:type="foreign" name="SALES_ORDER_STATUS_LABEL_STATUS_SALES_ORDER_STATUS_STATUS" + <constraint xsi:type="foreign" referenceId="SALES_ORDER_STATUS_LABEL_STATUS_SALES_ORDER_STATUS_STATUS" table="sales_order_status_label" column="status" referenceTable="sales_order_status" referenceColumn="status" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="SALES_ORDER_STATUS_LABEL_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="SALES_ORDER_STATUS_LABEL_STORE_ID_STORE_STORE_ID" table="sales_order_status_label" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <index name="SALES_ORDER_STATUS_LABEL_STORE_ID" indexType="btree"> + <index referenceId="SALES_ORDER_STATUS_LABEL_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> diff --git a/app/code/Magento/Sales/etc/di.xml b/app/code/Magento/Sales/etc/di.xml index be64ce5c84a3..68fcd17122bd 100644 --- a/app/code/Magento/Sales/etc/di.xml +++ b/app/code/Magento/Sales/etc/di.xml @@ -745,6 +745,10 @@ <virtualType name="ShippingAddressAggregator" type="Magento\Framework\DB\Sql\ConcatExpression"> <arguments> <argument name="columns" xsi:type="array"> + <item name="company" xsi:type="array"> + <item name="tableAlias" xsi:type="string">sales_shipping_address</item> + <item name="columnName" xsi:type="string">company</item> + </item> <item name="street" xsi:type="array"> <item name="tableAlias" xsi:type="string">sales_shipping_address</item> <item name="columnName" xsi:type="string">street</item> @@ -768,6 +772,10 @@ <virtualType name="BillingAddressAggregator" type="Magento\Framework\DB\Sql\ConcatExpression"> <arguments> <argument name="columns" xsi:type="array"> + <item name="company" xsi:type="array"> + <item name="tableAlias" xsi:type="string">sales_billing_address</item> + <item name="columnName" xsi:type="string">company</item> + </item> <item name="street" xsi:type="array"> <item name="tableAlias" xsi:type="string">sales_billing_address</item> <item name="columnName" xsi:type="string">street</item> @@ -1007,4 +1015,9 @@ <preference for="Magento\Sales\Api\OrderCustomerDelegateInterface" type="Magento\Sales\Model\Order\OrderCustomerDelegate" /> + <type name="Magento\Sales\Model\Order\Reorder\OrderedProductAvailabilityChecker"> + <arguments> + <argument name="productAvailabilityChecks" xsi:type="array" /> + </arguments> + </type> </config> diff --git a/app/code/Magento/Sales/etc/extension_attributes.xml b/app/code/Magento/Sales/etc/extension_attributes.xml index 7280a1a07154..222f61cdc732 100644 --- a/app/code/Magento/Sales/etc/extension_attributes.xml +++ b/app/code/Magento/Sales/etc/extension_attributes.xml @@ -10,4 +10,7 @@ <extension_attributes for="Magento\Sales\Api\Data\OrderInterface"> <attribute code="shipping_assignments" type="Magento\Sales\Api\Data\ShippingAssignmentInterface[]" /> </extension_attributes> + <extension_attributes for="Magento\Sales\Api\Data\OrderInterface"> + <attribute code="payment_additional_info" type="Magento\Payment\Api\Data\PaymentAdditionalInfoInterface[]" /> + </extension_attributes> </config> diff --git a/app/code/Magento/Sales/etc/webapi.xml b/app/code/Magento/Sales/etc/webapi.xml index cee245e34839..492dff805703 100644 --- a/app/code/Magento/Sales/etc/webapi.xml +++ b/app/code/Magento/Sales/etc/webapi.xml @@ -10,271 +10,271 @@ <route url="/V1/orders/:id" method="GET"> <service class="Magento\Sales\Api\OrderRepositoryInterface" method="get"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::actions_view" /> </resources> </route> <route url="/V1/orders" method="GET"> <service class="Magento\Sales\Api\OrderRepositoryInterface" method="getList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::actions_view" /> </resources> </route> <route url="/V1/orders/:id/statuses" method="GET"> <service class="Magento\Sales\Api\OrderManagementInterface" method="getStatus"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::actions_view" /> </resources> </route> <route url="/V1/orders/:id/cancel" method="POST"> <service class="Magento\Sales\Api\OrderManagementInterface" method="cancel"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::cancel" /> </resources> </route> <route url="/V1/orders/:id/emails" method="POST"> <service class="Magento\Sales\Api\OrderManagementInterface" method="notify"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::emails" /> </resources> </route> <route url="/V1/orders/:id/hold" method="POST"> <service class="Magento\Sales\Api\OrderManagementInterface" method="hold"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::hold" /> </resources> </route> <route url="/V1/orders/:id/unhold" method="POST"> <service class="Magento\Sales\Api\OrderManagementInterface" method="unHold"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::unhold" /> </resources> </route> <route url="/V1/orders/:id/comments" method="POST"> <service class="Magento\Sales\Api\OrderManagementInterface" method="addComment"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::comment" /> </resources> </route> <route url="/V1/orders/:id/comments" method="GET"> <service class="Magento\Sales\Api\OrderManagementInterface" method="getCommentsList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::actions_view" /> </resources> </route> <route url="/V1/orders/create" method="PUT"> <service class="Magento\Sales\Api\OrderRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::create" /> </resources> </route> <route url="/V1/orders/:parent_id" method="PUT"> <service class="Magento\Sales\Api\OrderAddressRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::create" /> </resources> </route> <route url="/V1/orders/items/:id" method="GET"> <service class="Magento\Sales\Api\OrderItemRepositoryInterface" method="get"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::actions_view" /> </resources> </route> <route url="/V1/orders/items" method="GET"> <service class="Magento\Sales\Api\OrderItemRepositoryInterface" method="getList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::actions_view" /> </resources> </route> <route url="/V1/invoices/:id" method="GET"> <service class="Magento\Sales\Api\InvoiceRepositoryInterface" method="get"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/invoices" method="GET"> <service class="Magento\Sales\Api\InvoiceRepositoryInterface" method="getList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/invoices/:id/comments" method="GET"> <service class="Magento\Sales\Api\InvoiceManagementInterface" method="getCommentsList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/invoices/:id/emails" method="POST"> <service class="Magento\Sales\Api\InvoiceManagementInterface" method="notify"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/invoices/:id/void" method="POST"> <service class="Magento\Sales\Api\InvoiceManagementInterface" method="setVoid"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/invoices/:id/capture" method="POST"> <service class="Magento\Sales\Api\InvoiceManagementInterface" method="setCapture"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/invoices/comments" method="POST"> <service class="Magento\Sales\Api\InvoiceCommentRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/invoices/" method="POST"> <service class="Magento\Sales\Api\InvoiceRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/invoice/:invoiceId/refund" method="POST"> <service class="Magento\Sales\Api\RefundInvoiceInterface" method="execute"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_invoice" /> </resources> </route> <route url="/V1/creditmemo/:id/comments" method="GET"> <service class="Magento\Sales\Api\CreditmemoManagementInterface" method="getCommentsList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_creditmemo" /> </resources> </route> <route url="/V1/creditmemos" method="GET"> <service class="Magento\Sales\Api\CreditmemoRepositoryInterface" method="getList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_creditmemo" /> </resources> </route> <route url="/V1/creditmemo/:id" method="GET"> <service class="Magento\Sales\Api\CreditmemoRepositoryInterface" method="get"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_creditmemo" /> </resources> </route> <route url="/V1/creditmemo/:id" method="PUT"> <service class="Magento\Sales\Api\CreditmemoManagementInterface" method="cancel"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_creditmemo" /> </resources> </route> <route url="/V1/creditmemo/:id/emails" method="POST"> <service class="Magento\Sales\Api\CreditmemoManagementInterface" method="notify"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_creditmemo" /> </resources> </route> <route url="/V1/creditmemo/refund" method="POST"> <service class="Magento\Sales\Api\CreditmemoManagementInterface" method="refund"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_creditmemo" /> </resources> </route> <route url="/V1/creditmemo/:id/comments" method="POST"> <service class="Magento\Sales\Api\CreditmemoCommentRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_creditmemo" /> </resources> </route> <route url="/V1/creditmemo" method="POST"> <service class="Magento\Sales\Api\CreditmemoRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::sales_creditmemo" /> </resources> </route> <route url="/V1/order/:orderId/refund" method="POST"> <service class="Magento\Sales\Api\RefundOrderInterface" method="execute"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::creditmemo" /> </resources> </route> <route url="/V1/shipment/:id" method="GET"> <service class="Magento\Sales\Api\ShipmentRepositoryInterface" method="get"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/shipments" method="GET"> <service class="Magento\Sales\Api\ShipmentRepositoryInterface" method="getList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/shipment/:id/comments" method="GET"> <service class="Magento\Sales\Api\ShipmentManagementInterface" method="getCommentsList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/shipment/:id/comments" method="POST"> <service class="Magento\Sales\Api\ShipmentCommentRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/shipment/:id/emails" method="POST"> <service class="Magento\Sales\Api\ShipmentManagementInterface" method="notify"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/shipment/track" method="POST"> <service class="Magento\Sales\Api\ShipmentTrackRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/shipment/track/:id" method="DELETE"> <service class="Magento\Sales\Api\ShipmentTrackRepositoryInterface" method="deleteById"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/shipment/" method="POST"> <service class="Magento\Sales\Api\ShipmentRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/shipment/:id/label" method="GET"> <service class="Magento\Sales\Api\ShipmentManagementInterface" method="getLabel"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::shipment" /> </resources> </route> <route url="/V1/order/:orderId/ship" method="POST"> <service class="Magento\Sales\Api\ShipOrderInterface" method="execute"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::ship" /> </resources> </route> <route url="/V1/orders/" method="POST"> <service class="Magento\Sales\Api\OrderRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::create" /> </resources> </route> <route url="/V1/transactions/:id" method="GET"> <service class="Magento\Sales\Api\TransactionRepositoryInterface" method="get"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::transactions_fetch" /> </resources> </route> <route url="/V1/transactions" method="GET"> <service class="Magento\Sales\Api\TransactionRepositoryInterface" method="getList"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::transactions_fetch" /> </resources> </route> <route url="/V1/order/:orderId/invoice" method="POST"> <service class="Magento\Sales\Api\InvoiceOrderInterface" method="execute"/> <resources> - <resource ref="Magento_Sales::sales" /> + <resource ref="Magento_Sales::invoice" /> </resources> </route> </routes> diff --git a/app/code/Magento/Sales/etc/webapi_rest/di.xml b/app/code/Magento/Sales/etc/webapi_rest/di.xml index 47fb3f188513..f2cbd14eb804 100644 --- a/app/code/Magento/Sales/etc/webapi_rest/di.xml +++ b/app/code/Magento/Sales/etc/webapi_rest/di.xml @@ -15,4 +15,11 @@ <type name="Magento\Sales\Api\ShipmentRepositoryInterface"> <plugin name="convert_blob_to_string" type="Magento\Sales\Plugin\ShippingLabelConverter" /> </type> + <type name="Magento\Framework\Reflection\DataObjectProcessor"> + <arguments> + <argument name="processors" xsi:type="array"> + <item name="Magento\Sales\Model\Order\Item" xsi:type="object">Magento\Sales\Model\Order\Webapi\ChangeOutputArray\Proxy</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Sales/etc/webapi_soap/di.xml b/app/code/Magento/Sales/etc/webapi_soap/di.xml index 47fb3f188513..f2cbd14eb804 100644 --- a/app/code/Magento/Sales/etc/webapi_soap/di.xml +++ b/app/code/Magento/Sales/etc/webapi_soap/di.xml @@ -15,4 +15,11 @@ <type name="Magento\Sales\Api\ShipmentRepositoryInterface"> <plugin name="convert_blob_to_string" type="Magento\Sales\Plugin\ShippingLabelConverter" /> </type> + <type name="Magento\Framework\Reflection\DataObjectProcessor"> + <arguments> + <argument name="processors" xsi:type="array"> + <item name="Magento\Sales\Model\Order\Item" xsi:type="object">Magento\Sales\Model\Order\Webapi\ChangeOutputArray\Proxy</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Sales/i18n/en_US.csv b/app/code/Magento/Sales/i18n/en_US.csv index a32f3cacce75..c5657f3a309f 100644 --- a/app/code/Magento/Sales/i18n/en_US.csv +++ b/app/code/Magento/Sales/i18n/en_US.csv @@ -686,17 +686,9 @@ General,General "Allow Reorder","Allow Reorder" "Invoice and Packing Slip Design","Invoice and Packing Slip Design" "Logo for PDF Print-outs (200x50)","Logo for PDF Print-outs (200x50)" -" - Your default logo will be used in PDF and HTML documents.<br />(jpeg, tiff, png) If your pdf image is distorted, try to use larger file-size image. - "," - Your default logo will be used in PDF and HTML documents.<br />(jpeg, tiff, png) If your pdf image is distorted, try to use larger file-size image. - " +"Your default logo will be used in PDF and HTML documents.<br />(jpeg, tiff, png) If your pdf image is distorted, try to use larger file-size image.","Your default logo will be used in PDF and HTML documents.<br />(jpeg, tiff, png) If your pdf image is distorted, try to use larger file-size image." "Logo for HTML Print View","Logo for HTML Print View" -" - Logo for HTML documents only. If empty, default will be used.<br />(jpeg, gif, png) - "," - Logo for HTML documents only. If empty, default will be used.<br />(jpeg, gif, png) - " +"Logo for HTML documents only. If empty, default will be used.<br />(jpeg, gif, png)","Logo for HTML documents only. If empty, default will be used.<br />(jpeg, gif, png)" Address,Address "Minimum Order Amount","Minimum Order Amount" Enable,Enable @@ -803,3 +795,5 @@ Created,Created "PDF Shipments","PDF Shipments" "PDF Creditmemos","PDF Creditmemos" Refunds,Refunds +"Allow Zero GrandTotal for Creditmemo","Allow Zero GrandTotal for Creditmemo" +"Allow Zero GrandTotal","Allow Zero GrandTotal" diff --git a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_customer_block.xml b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_customer_block.xml index c321bee460e4..fe2cb7e01b72 100644 --- a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_customer_block.xml +++ b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_customer_block.xml @@ -13,7 +13,7 @@ <argument name="id" xsi:type="string">sales_order_create_customer_grid</argument> <argument name="use_ajax" xsi:type="string">1</argument> <argument name="default_sort" xsi:type="string">entity_id</argument> - <argument name="dataSource" xsi:type="object">Magento\Sales\Model\ResourceModel\Order\Customer\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Sales\Model\ResourceModel\Order\Customer\Collection</argument> <argument name="pager_visibility" xsi:type="string">1</argument> <argument name="grid_url" xsi:type="url" path="*/*/loadBlock"> <param name="block">customer_grid</param> @@ -80,13 +80,13 @@ <argument name="align" xsi:type="string">center</argument> </arguments> </block> - </block> - <block class="Magento\Backend\Block\Widget\Grid\Column" name="adminhtml.customer.grid.columnSet.website_name" as="website_name"> - <arguments> - <argument name="header" xsi:type="string" translate="true">Website</argument> - <argument name="index" xsi:type="string">website_name</argument> - <argument name="align" xsi:type="string">center</argument> - </arguments> + <block class="Magento\Backend\Block\Widget\Grid\Column" name="adminhtml.customer.grid.columnSet.website_name" as="website_name"> + <arguments> + <argument name="header" xsi:type="string" translate="true">Website</argument> + <argument name="index" xsi:type="string">website_name</argument> + <argument name="align" xsi:type="string">center</argument> + </arguments> + </block> </block> </block> </referenceBlock> diff --git a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_index.xml b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_index.xml index eb0a7685e5e2..3832476ff697 100644 --- a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_index.xml +++ b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_index.xml @@ -45,8 +45,18 @@ <block class="Magento\Sales\Block\Adminhtml\Order\Create\Sidebar\Pviewed" template="Magento_Sales::order/create/sidebar/items.phtml" name="pviewed"/> </block> <block class="Magento\Sales\Block\Adminhtml\Order\Create\Form\Account" template="Magento_Sales::order/create/form/account.phtml" name="form_account"/> - <block class="Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Address" template="Magento_Sales::order/create/form/address.phtml" name="shipping_address"/> - <block class="Magento\Sales\Block\Adminhtml\Order\Create\Billing\Address" template="Magento_Sales::order/create/form/address.phtml" name="billing_address"/> + <block class="Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Address" template="Magento_Sales::order/create/form/address.phtml" name="shipping_address"> + <arguments> + <argument name="customerAddressFormatter" xsi:type="object">Magento\Sales\ViewModel\Customer\AddressFormatter</argument> + <argument name="customerAddressCollection" xsi:type="object">Magento\Customer\Model\ResourceModel\Address\Collection</argument> + </arguments> + </block> + <block class="Magento\Sales\Block\Adminhtml\Order\Create\Billing\Address" template="Magento_Sales::order/create/form/address.phtml" name="billing_address"> + <arguments> + <argument name="customerAddressFormatter" xsi:type="object">Magento\Sales\ViewModel\Customer\AddressFormatter</argument> + <argument name="customerAddressCollection" xsi:type="object">Magento\Customer\Model\ResourceModel\Address\Collection</argument> + </arguments> + </block> <block class="Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Method" template="Magento_Sales::order/create/abstract.phtml" name="shipping_method"> <block class="Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Method\Form" template="Magento_Sales::order/create/shipping/method/form.phtml" name="order_create_shipping_form" as="form"/> </block> diff --git a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_billing_address.xml b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_billing_address.xml index 6f0cbdb0cd43..c52f81d5cb56 100644 --- a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_billing_address.xml +++ b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_billing_address.xml @@ -8,7 +8,12 @@ <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceContainer name="content"> - <block class="Magento\Sales\Block\Adminhtml\Order\Create\Billing\Address" template="Magento_Sales::order/create/form/address.phtml" name="billing_address"/> + <block class="Magento\Sales\Block\Adminhtml\Order\Create\Billing\Address" template="Magento_Sales::order/create/form/address.phtml" name="billing_address"> + <arguments> + <argument name="customerAddressFormatter" xsi:type="object">Magento\Sales\ViewModel\Customer\AddressFormatter</argument> + <argument name="customerAddressCollection" xsi:type="object">Magento\Customer\Model\ResourceModel\Address\Collection</argument> + </arguments> + </block> </referenceContainer> </body> </page> diff --git a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_data.xml b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_data.xml index 70b5bfc29827..54348ce961c5 100644 --- a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_data.xml +++ b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_data.xml @@ -20,8 +20,18 @@ <block class="Magento\Sales\Block\Adminhtml\Order\Create\Sidebar\Pviewed" template="Magento_Sales::order/create/sidebar/items.phtml" name="pviewed"/> </block> <block class="Magento\Sales\Block\Adminhtml\Order\Create\Form\Account" template="Magento_Sales::order/create/form/account.phtml" name="form_account"/> - <block class="Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Address" template="Magento_Sales::order/create/form/address.phtml" name="shipping_address"/> - <block class="Magento\Sales\Block\Adminhtml\Order\Create\Billing\Address" template="Magento_Sales::order/create/form/address.phtml" name="billing_address"/> + <block class="Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Address" template="Magento_Sales::order/create/form/address.phtml" name="shipping_address"> + <arguments> + <argument name="customerAddressFormatter" xsi:type="object">Magento\Sales\ViewModel\Customer\AddressFormatter</argument> + <argument name="customerAddressCollection" xsi:type="object">Magento\Customer\Model\ResourceModel\Address\Collection</argument> + </arguments> + </block> + <block class="Magento\Sales\Block\Adminhtml\Order\Create\Billing\Address" template="Magento_Sales::order/create/form/address.phtml" name="billing_address"> + <arguments> + <argument name="customerAddressFormatter" xsi:type="object">Magento\Sales\ViewModel\Customer\AddressFormatter</argument> + <argument name="customerAddressCollection" xsi:type="object">Magento\Customer\Model\ResourceModel\Address\Collection</argument> + </arguments> + </block> <block class="Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Method" template="Magento_Sales::order/create/abstract.phtml" name="shipping_method"> <block class="Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Method\Form" template="Magento_Sales::order/create/shipping/method/form.phtml" name="order.create.shipping.method.form" as="form"/> </block> diff --git a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_shipping_address.xml b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_shipping_address.xml index 56f6786397df..559f56dcb845 100644 --- a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_shipping_address.xml +++ b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_shipping_address.xml @@ -8,7 +8,12 @@ <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceContainer name="content"> - <block class="Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Address" template="Magento_Sales::order/create/form/address.phtml" name="shipping_address"/> + <block class="Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Address" template="Magento_Sales::order/create/form/address.phtml" name="shipping_address"> + <arguments> + <argument name="customerAddressFormatter" xsi:type="object">Magento\Sales\ViewModel\Customer\AddressFormatter</argument> + <argument name="customerAddressCollection" xsi:type="object">Magento\Customer\Model\ResourceModel\Address\Collection</argument> + </arguments> + </block> </referenceContainer> </body> </page> diff --git a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_creditmemo_grid_block.xml b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_creditmemo_grid_block.xml index 7f14ff3728a4..318416a777f3 100644 --- a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_creditmemo_grid_block.xml +++ b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_creditmemo_grid_block.xml @@ -11,7 +11,7 @@ <block class="Magento\Backend\Block\Widget\Grid" name="sales.order_creditmemo.grid" as="grid"> <arguments> <argument name="id" xsi:type="string">order_creditmemos</argument> - <argument name="dataSource" xsi:type="object">Magento\Sales\Model\ResourceModel\Order\Creditmemo\Order\Grid\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Sales\Model\ResourceModel\Order\Creditmemo\Order\Grid\Collection</argument> <argument name="use_ajax" xsi:type="boolean">true</argument> <argument name="default_sort" xsi:type="string">created_at</argument> <argument name="default_dir" xsi:type="string">DESC</argument> diff --git a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_invoice_grid_block.xml b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_invoice_grid_block.xml index 941696f0ce89..d69ed4267710 100644 --- a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_invoice_grid_block.xml +++ b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_invoice_grid_block.xml @@ -11,7 +11,7 @@ <block class="Magento\Backend\Block\Widget\Grid" name="sales.order_invoice.grid" as="grid"> <arguments> <argument name="id" xsi:type="string">order_invoices</argument> - <argument name="dataSource" xsi:type="object">Magento\Sales\Model\ResourceModel\Order\Invoice\Orders\Grid\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Sales\Model\ResourceModel\Order\Invoice\Orders\Grid\Collection</argument> <argument name="use_ajax" xsi:type="boolean">true</argument> <argument name="default_sort" xsi:type="string">created_at</argument> <argument name="default_dir" xsi:type="string">DESC</argument> diff --git a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_shipment_grid_block.xml b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_shipment_grid_block.xml index 0180efd29d2f..21b5d3b06c16 100644 --- a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_shipment_grid_block.xml +++ b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_shipment_grid_block.xml @@ -11,7 +11,7 @@ <block class="Magento\Backend\Block\Widget\Grid" name="sales.order_shipment.grid" as="grid"> <arguments> <argument name="id" xsi:type="string">order_shipments</argument> - <argument name="dataSource" xsi:type="object">Magento\Sales\Model\ResourceModel\Order\Shipment\Order\Grid\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Sales\Model\ResourceModel\Order\Shipment\Order\Grid\Collection</argument> <argument name="use_ajax" xsi:type="boolean">true</argument> <argument name="default_sort" xsi:type="string">created_at</argument> <argument name="default_dir" xsi:type="string">DESC</argument> diff --git a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_status_index.xml b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_status_index.xml index 87d7644a4b00..6854d3bb7bc3 100644 --- a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_status_index.xml +++ b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_status_index.xml @@ -12,7 +12,7 @@ <block class="Magento\Backend\Block\Widget\Grid" name="sales_order_status.grid" as="grid"> <arguments> <argument name="id" xsi:type="string">sales_order_status_grid</argument> - <argument name="dataSource" xsi:type="object">Magento\Sales\Model\ResourceModel\Status\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Sales\Model\ResourceModel\Status\Collection</argument> <argument name="default_sort" xsi:type="string">state</argument> <argument name="default_dir" xsi:type="string">desc</argument> <argument name="pager_visibility" xsi:type="string">1</argument> diff --git a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_transactions_grid_block.xml b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_transactions_grid_block.xml index c2f553285720..adb722f6d0f5 100644 --- a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_transactions_grid_block.xml +++ b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_transactions_grid_block.xml @@ -13,7 +13,7 @@ <referenceBlock name="sales.transactions.grid"> <arguments> <argument name="id" xsi:type="string">order_transactions</argument> - <argument name="dataSource" xsi:type="object"> + <argument name="dataSource" xsi:type="object" shared="false"> <updater>Magento\Sales\Model\Grid\CollectionUpdater</updater> </argument> <argument name="use_ajax" xsi:type="boolean">true</argument> diff --git a/app/code/Magento/Sales/view/adminhtml/layout/sales_transaction_child_block.xml b/app/code/Magento/Sales/view/adminhtml/layout/sales_transaction_child_block.xml index aa8672a68bc6..5bdad85ebc16 100644 --- a/app/code/Magento/Sales/view/adminhtml/layout/sales_transaction_child_block.xml +++ b/app/code/Magento/Sales/view/adminhtml/layout/sales_transaction_child_block.xml @@ -11,7 +11,7 @@ <referenceBlock name="sales.transactions.grid"> <arguments> <argument name="id" xsi:type="string">transactionChildGrid</argument> - <argument name="dataSource" xsi:type="object"> + <argument name="dataSource" xsi:type="object" shared="false"> <updater>Magento\Sales\Model\Grid\Child\CollectionUpdater</updater> </argument> <argument name="use_ajax" xsi:type="string">false</argument> diff --git a/app/code/Magento/Sales/view/adminhtml/layout/sales_transactions_grid_block.xml b/app/code/Magento/Sales/view/adminhtml/layout/sales_transactions_grid_block.xml index 9f3b7f23ba20..37b319b94352 100644 --- a/app/code/Magento/Sales/view/adminhtml/layout/sales_transactions_grid_block.xml +++ b/app/code/Magento/Sales/view/adminhtml/layout/sales_transactions_grid_block.xml @@ -11,7 +11,7 @@ <block class="Magento\Backend\Block\Widget\Grid" name="sales.transactions.grid" as="grid"> <arguments> <argument name="id" xsi:type="string">sales_transactions_grid</argument> - <argument name="dataSource" xsi:type="object">Magento\Sales\Model\ResourceModel\Transaction\Grid\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Sales\Model\ResourceModel\Transaction\Grid\Collection</argument> <argument name="use_ajax" xsi:type="string">true</argument> <argument name="default_sort" xsi:type="string">created_at</argument> <argument name="default_dir" xsi:type="string">DESC</argument> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/billing/method/form.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/billing/method/form.phtml index c69d453fb81d..00fa55d38f5f 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/billing/method/form.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/billing/method/form.phtml @@ -7,7 +7,7 @@ ?> <?php if ($block->hasMethods()) : ?> <div id="order-billing_method_form"> - <dl class="admin__payment-methods"> + <dl class="admin__payment-methods control"> <?php $_methods = $block->getMethods(); $_methodsCount = count($_methods); @@ -28,8 +28,8 @@ <?php if ($currentSelectedMethod == $_code) : ?> checked="checked" <?php endif; ?> - <?php $className = ($_counter == $_methodsCount) ? ' validate-one-required-by-name' : ''; ?> - class="admin__control-radio<?= $block->escapeHtml($className); ?>"/> + data-validate="{'validate-one-required-by-name':true}" + class="admin__control-radio"/> <?php else :?> <span class="no-display"> <input id="p_method_<?= $block->escapeHtml($_code); ?>" diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml index 686e311292ac..d1a90783c68c 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml @@ -6,6 +6,21 @@ // @codingStandardsIgnoreFile +/** + * @var \Magento\Customer\Model\ResourceModel\Address\Collection $addressCollection + */ +$addressCollection = $block->getData('customerAddressCollection'); + +$addressArray = []; +if ($block->getCustomerId()) { + $addressArray = $addressCollection->setCustomerFilter([$block->getCustomerId()])->toArray(); +} + +/** + * @var \Magento\Sales\ViewModel\Customer\AddressFormatter $customerAddressFormatter + */ +$customerAddressFormatter = $block->getData('customerAddressFormatter'); + /** * @var \Magento\Sales\Block\Adminhtml\Order\Create\Billing\Address|\Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Address $block */ @@ -17,7 +32,7 @@ if ($block->getIsShipping()): require(["Magento_Sales/order/create/form"], function(){ order.shippingAddressContainer = '<?= /* @escapeNotVerified */ $_fieldsContainerId ?>'; - order.setAddresses(<?= /* @escapeNotVerified */ $block->getAddressCollectionJson() ?>); + order.setAddresses(<?= /* @escapeNotVerified */ $customerAddressFormatter->getAddressesJson($addressArray) ?>); }); </script> @@ -59,13 +74,11 @@ endif; ?> onchange="order.selectAddress(this, '<?= /* @escapeNotVerified */ $_fieldsContainerId ?>')" class="admin__control-select"> <option value=""><?= /* @escapeNotVerified */ __('Add New Address') ?></option> - <?php foreach ($block->getAddressCollection() as $_address): ?> - <?php //if($block->getAddressAsString($_address)!=$block->getAddressAsString($block->getAddress())): ?> + <?php foreach ($addressArray as $addressId => $address): ?> <option - value="<?= /* @escapeNotVerified */ $_address->getId() ?>"<?php if ($_address->getId() == $block->getAddressId()): ?> selected="selected"<?php endif; ?>> - <?= /* @escapeNotVerified */ $block->getAddressAsString($_address) ?> + value="<?= /* @escapeNotVerified */ $addressId ?>"<?php if ($addressId == $block->getAddressId()): ?> selected="selected"<?php endif; ?>> + <?= /* @escapeNotVerified */ $block->escapeHtml($customerAddressFormatter->getAddressAsString($address)) ?> </option> - <?php //endif; ?> <?php endforeach; ?> </select> </div> @@ -76,11 +89,8 @@ endif; ?> <?= $block->getForm()->toHtml() ?> <div class="admin__field admin__field-option order-save-in-address-book"> - <input name="<?= $block->getForm()->getHtmlNamePrefix() ?>[save_in_address_book]" type="checkbox" - id="<?= $block->getForm()->getHtmlIdPrefix() ?>save_in_address_book" - value="1" - <?php if (!$block->getDontSaveInAddressBook() && $block->getAddress()->getSaveInAddressBook()): ?> checked="checked"<?php endif; ?> - class="admin__control-checkbox"/> + <input name="<?= $block->getForm()->getHtmlNamePrefix() ?>[save_in_address_book]" type="checkbox" id="<?= $block->getForm()->getHtmlIdPrefix() ?>save_in_address_book" value="1" + <?php if (!$block->getDontSaveInAddressBook()): ?> checked="checked"<?php endif; ?> class="admin__control-checkbox"/> <label for="<?= $block->getForm()->getHtmlIdPrefix() ?>save_in_address_book" class="admin__field-label"><?= /* @escapeNotVerified */ __('Save in address book') ?></label> </div> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/shipping/method/form.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/shipping/method/form.phtml index 9e0394f6430b..65d3a612e513 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/shipping/method/form.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/shipping/method/form.phtml @@ -100,12 +100,8 @@ require(['prototype'], function(){ </div> <script> require(["Magento_Sales/order/create/form"], function(){ - order.overlay('shipping-method-overlay', <?php if ($block->getQuote()->isVirtual()): ?>false<?php else: ?>true<?php endif; ?>); order.overlay('address-shipping-overlay', <?php if ($block->getQuote()->isVirtual()): ?>false<?php else: ?>true<?php endif; ?>); - - <?php if ($block->getQuote()->isVirtual()): ?> - order.isOnlyVirtualProduct = true; - <?php endif; ?> + order.isOnlyVirtualProduct = <?= /* @noEscape */ $block->getQuote()->isVirtual() ? 'true' : 'false'; ?>; }); </script> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/tax.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/tax.phtml index 92139896273d..643146f7bb5c 100755 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/tax.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/tax.phtml @@ -33,7 +33,6 @@ $taxAmount = $block->getTotal()->getValue(); <?php $percent = $info['percent']; ?> <?php $amount = $info['amount']; ?> <?php $rates = $info['rates']; ?> - <?php $isFirst = 1; ?> <?php foreach ($rates as $rate): ?> <tr class="summary-details-<?= /* @escapeNotVerified */ $taxIter ?> summary-details<?php if ($isTop): echo ' summary-details-first'; endif; ?>" style="display:none;"> @@ -44,13 +43,10 @@ $taxAmount = $block->getTotal()->getValue(); <?php endif; ?> <br /> </td> - <?php if ($isFirst): ?> - <td style="<?= /* @escapeNotVerified */ $block->getTotal()->getStyle() ?>" class="admin__total-amount" rowspan="<?= count($rates) ?>"> - <?= /* @escapeNotVerified */ $block->formatPrice($amount) ?> - </td> - <?php endif; ?> + <td style="<?= /* @escapeNotVerified */ $block->getTotal()->getStyle() ?>" class="admin__total-amount"> + <?= /* @escapeNotVerified */ $block->formatPrice(($amount*(float)$rate['percent'])/$percent) ?> + </td> </tr> - <?php $isFirst = 0; ?> <?php $isTop = 0; ?> <?php endforeach; ?> <?php endforeach; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml index fcf4ccad7060..ba4af32ff69b 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml @@ -136,67 +136,76 @@ </section> <script> -require(['jquery', 'prototype'], function(jQuery){ +require(['jquery'], function(jQuery){ //<![CDATA[ -var submitButtons = $$('.submit-button'); -var updateButtons = $$('.update-button'); -var fields = $$('.qty-input'); +var submitButtons = jQuery('.submit-button'); +var updateButtons = jQuery('.update-button'); +var fields = jQuery('.qty-input'); -updateButtons.each(function (elem) {elem.disabled=true;elem.addClassName('disabled');}); +function enableButtons(buttons) { + buttons.removeClass('disabled').prop('disabled', false); +} -for(var i=0;i<fields.length;i++){ - fields[i].observe('change', checkButtonsRelation) - fields[i].baseValue = fields[i].value; +function disableButtons(buttons) { + buttons.addClass('disabled').prop('disabled', true); } +disableButtons(updateButtons); + +fields.on('change', checkButtonsRelation); +fields.each(function (i, elem) { + elem.baseValue = elem.value; +}); + function checkButtonsRelation() { var hasChanges = false; - fields.each(function (elem) { + fields.each(function (i, elem) { if (elem.baseValue != elem.value) { hasChanges = true; } }.bind(this)); if (hasChanges) { - submitButtons.each(function (elem) {elem.disabled=true;elem.addClassName('disabled');}); - updateButtons.each(function (elem) {elem.disabled=false;elem.removeClassName('disabled');}); + disableButtons(submitButtons); + enableButtons(updateButtons); } else { - submitButtons.each(function (elem) {elem.disabled=false;elem.removeClassName('disabled');}); - updateButtons.each(function (elem) {elem.disabled=true;elem.addClassName('disabled');}); + enableButtons(submitButtons); + disableButtons(updateButtons); } } submitCreditMemo = function() { - if ($('creditmemo_do_offline')) $('creditmemo_do_offline').value=0; + var creditMemoOffline = jQuery('#creditmemo_do_offline'); + if (creditMemoOffline.length) { + creditMemoOffline.prop('value', 0); + } // Temporary solution will be replaced after refactoring order functionality jQuery('#edit_form').triggerHandler('save'); -} +}; submitCreditMemoOffline = function() { - if ($('creditmemo_do_offline')) $('creditmemo_do_offline').value=1; + var creditMemoOffline = jQuery('#creditmemo_do_offline'); + if (creditMemoOffline.length) { + creditMemoOffline.prop('value', 1); + } // Temporary solution will be replaced after refactoring order functionality jQuery('#edit_form').triggerHandler('save'); -} - -var sendEmailCheckbox = $('send_email'); +}; -if (sendEmailCheckbox) { - var notifyCustomerCheckbox = $('notify_customer'); - var creditmemoCommentText = $('creditmemo_comment_text'); - Event.observe(sendEmailCheckbox, 'change', bindSendEmail); +var sendEmailCheckbox = jQuery('#send_email'); +if (sendEmailCheckbox.length) { + var notifyCustomerCheckbox = jQuery('#notify_customer'); + sendEmailCheckbox.on('change', bindSendEmail); bindSendEmail(); } -function bindSendEmail() -{ - if (sendEmailCheckbox.checked == true) { - notifyCustomerCheckbox.disabled = false; - //creditmemoCommentText.disabled = false; +function bindSendEmail() { + if (sendEmailCheckbox.prop('checked') == true) { + notifyCustomerCheckbox.prop('disabled', false); } else { - notifyCustomerCheckbox.disabled = true; - //creditmemoCommentText.disabled = true; + notifyCustomerCheckbox.prop('disabled', true); } } diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/items.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/items.phtml index fa5ea0568011..872be35cff20 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/items.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/items.phtml @@ -31,9 +31,9 @@ <?php if ($block->canEditQty()): ?> <tfoot> <tr> - <td colspan="2"> </td> - <td colspan="3"><?= $block->getUpdateButtonHtml() ?></td> <td colspan="3"> </td> + <td><?= $block->getUpdateButtonHtml() ?></td> + <td colspan="4"> </td> </tr> </tfoot> <?php endif; ?> @@ -134,56 +134,61 @@ </section> <script> -require(['jquery', 'prototype'], function(jQuery){ +require(['jquery'], function(jQuery){ //<![CDATA[ -var submitButtons = $$('.submit-button'); -var updateButtons = $$('.update-button'); +var submitButtons = jQuery('.submit-button'); +var updateButtons = jQuery('.update-button'); var enableSubmitButtons = <?= (int) !$block->getDisableSubmitButton() ?>; -var fields = $$('.qty-input'); +var fields = jQuery('.qty-input'); -updateButtons.each(function (elem) {elem.disabled=true;elem.addClassName('disabled');}); +function enableButtons(buttons) { + buttons.removeClass('disabled').prop('disabled', false); +} -for(var i=0;i<fields.length;i++){ - jQuery(fields[i]).on('keyup', checkButtonsRelation); - fields[i].baseValue = fields[i].value; +function disableButtons(buttons) { + buttons.addClass('disabled').prop('disabled', true); } +disableButtons(updateButtons); + +fields.on('keyup', checkButtonsRelation); +fields.each(function (i, elem) { + elem.baseValue = elem.value; +}); + function checkButtonsRelation() { var hasChanges = false; - fields.each(function (elem) { + fields.each(function (i, elem) { if (elem.baseValue != elem.value) { hasChanges = true; } }.bind(this)); if (hasChanges) { - submitButtons.each(function (elem) {elem.disabled=true;elem.addClassName('disabled');}); - updateButtons.each(function (elem) {elem.disabled=false;elem.removeClassName('disabled');}); + disableButtons(submitButtons); + enableButtons(updateButtons); } else { if (enableSubmitButtons) { - submitButtons.each(function (elem) {elem.disabled=false;elem.removeClassName('disabled');}); + enableButtons(submitButtons); } - updateButtons.each(function (elem) {elem.disabled=true;elem.addClassName('disabled');}); + disableButtons(updateButtons); } } -var sendEmailCheckbox = $('send_email'); -if (sendEmailCheckbox) { - var notifyCustomerCheckbox = $('notify_customer'); - var invoiceCommentText = $('invoice_comment_text'); - Event.observe(sendEmailCheckbox, 'change', bindSendEmail); +var sendEmailCheckbox = jQuery('#send_email'); +if (sendEmailCheckbox.length) { + var notifyCustomerCheckbox = jQuery('#notify_customer'); + sendEmailCheckbox.on('change', bindSendEmail); bindSendEmail(); } function bindSendEmail() { - if (sendEmailCheckbox.checked == true) { - notifyCustomerCheckbox.disabled = false; - //invoiceCommentText.disabled = false; + if (sendEmailCheckbox.prop('checked') == true) { + notifyCustomerCheckbox.prop('disabled', false); } else { - notifyCustomerCheckbox.disabled = true; - //invoiceCommentText.disabled = true; + notifyCustomerCheckbox.prop('disabled', true); } } diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/view/info.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/view/info.phtml index 5384a00dc894..bbd6394097f9 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/view/info.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/view/info.phtml @@ -104,7 +104,7 @@ $customerUrl = $block->getCustomerViewUrl(); <?php if ($order->getBaseCurrencyCode() != $order->getOrderCurrencyCode()): ?> <tr> <th><?= $block->escapeHtml(__('%1 / %2 rate:', $order->getOrderCurrencyCode(), $order->getBaseCurrencyCode())) ?></th> - <th><?= $block->escapeHtml($order->getBaseToOrderRate()) ?></th> + <td><?= $block->escapeHtml($order->getBaseToOrderRate()) ?></td> </tr> <?php endif; ?> </table> diff --git a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_creditmemo_grid.xml b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_creditmemo_grid.xml index f36a7d2821f7..e0b7dae8fdb1 100644 --- a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_creditmemo_grid.xml +++ b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_creditmemo_grid.xml @@ -92,7 +92,7 @@ <column name="order_increment_id"> <settings> <filter>text</filter> - <label translate="true">Order</label> + <label translate="true">Order #</label> </settings> </column> <column name="order_created_at" class="Magento\Ui\Component\Listing\Columns\Date" component="Magento_Ui/js/grid/columns/date"> diff --git a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_grid.xml b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_grid.xml index fe67f4d5e2de..e1f047b372c9 100644 --- a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_grid.xml +++ b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_grid.xml @@ -17,6 +17,7 @@ <url path="sales/order_create/start"/> <class>primary</class> <label translate="true">Create New Order</label> + <aclResource>Magento_Sales::create</aclResource> </button> </buttons> <spinner>sales_order_columns</spinner> diff --git a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_shipment_grid.xml b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_shipment_grid.xml index e0495e62d5ce..9e02c31a2063 100644 --- a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_shipment_grid.xml +++ b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_shipment_grid.xml @@ -100,7 +100,7 @@ <column name="order_increment_id"> <settings> <filter>text</filter> - <label translate="true">Order</label> + <label translate="true">Order #</label> </settings> </column> <column name="order_created_at" class="Magento\Ui\Component\Listing\Columns\Date" component="Magento_Ui/js/grid/columns/date"> diff --git a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_creditmemo_grid.xml b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_creditmemo_grid.xml index 10b7b1c028c6..cf536c27a0ac 100644 --- a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_creditmemo_grid.xml +++ b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_creditmemo_grid.xml @@ -105,7 +105,7 @@ <column name="order_increment_id"> <settings> <filter>text</filter> - <label translate="true">Order</label> + <label translate="true">Order #</label> </settings> </column> <column name="order_created_at" class="Magento\Ui\Component\Listing\Columns\Date" component="Magento_Ui/js/grid/columns/date"> diff --git a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_shipment_grid.xml b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_shipment_grid.xml index 6db77a79b8c1..5f8ebde29066 100644 --- a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_shipment_grid.xml +++ b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_shipment_grid.xml @@ -106,7 +106,7 @@ <column name="order_increment_id"> <settings> <filter>textRange</filter> - <label translate="true">Order</label> + <label translate="true">Order #</label> </settings> </column> <column name="order_created_at" class="Magento\Ui\Component\Listing\Columns\Date" component="Magento_Ui/js/grid/columns/date"> diff --git a/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js b/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js index b61f4617e1be..c508a5ecdfa5 100644 --- a/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js +++ b/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js @@ -4,14 +4,17 @@ */ define([ - "jquery", + 'jquery', 'Magento_Ui/js/modal/confirm', 'Magento_Ui/js/modal/alert', - "mage/translate", - "prototype", - "Magento_Catalog/catalog/product/composite/configure", + 'mage/template', + 'text!Magento_Sales/templates/order/create/shipping/reload.html', + 'text!Magento_Sales/templates/order/create/payment/reload.html', + 'mage/translate', + 'prototype', + 'Magento_Catalog/catalog/product/composite/configure', 'Magento_Ui/js/lib/view/utils/async' -], function(jQuery, confirm, alert){ +], function (jQuery, confirm, alert, template, shippingTemplate, paymentTemplate) { window.AdminOrder = new Class.create(); @@ -29,7 +32,7 @@ define([ this.gridProducts = $H({}); this.gridProductsGift = $H({}); this.billingAddressContainer = ''; - this.shippingAddressContainer= ''; + this.shippingAddressContainer = ''; this.isShippingMethodReseted = data.shipping_method_reseted ? data.shipping_method_reseted : false; this.overlayData = $H({}); this.giftMessageDataChanged = false; @@ -39,7 +42,19 @@ define([ this.isOnlyVirtualProduct = false; this.excludedPaymentMethods = []; this.summarizePrice = true; - this.timerId = null; + this.shippingTemplate = template(shippingTemplate, { + data: { + title: jQuery.mage.__('Shipping Method'), + linkText: jQuery.mage.__('Get shipping methods and rates') + } + }); + this.paymentTemplate = template(paymentTemplate, { + data: { + title: jQuery.mage.__('Payment Method'), + linkText: jQuery.mage.__('Get available payment methods') + } + }); + jQuery.async('#order-items', (function(){ this.dataArea = new OrderFormArea('data', $(this.getAreaId('data')), this); this.itemsArea = Object.extend(new OrderFormArea('items', $(this.getAreaId('items')), this), { @@ -48,7 +63,7 @@ define([ if (typeof controlButtonArea != 'undefined') { var buttons = controlButtonArea.childElements(); for (var i = 0; i < buttons.length; i++) { - if (buttons[i].innerHTML.include(button.label)) { + if (buttons[i].innerHTML.include(button.getLabel())) { return; } } @@ -168,43 +183,51 @@ define([ var data = this.serializeData(container); data[el.name] = id; - if(this.isShippingField(container) && !this.isShippingMethodReseted){ + + this.resetPaymentMethod(); + if (this.isShippingField(container) && !this.isShippingMethodReseted) { this.resetShippingMethod(data); - } - else{ + } else{ this.saveData(data); } }, - isShippingField : function(fieldId){ - if(this.shippingAsBilling){ + /** + * Checks if the field belongs to the shipping address. + * + * @param {String} fieldId + * @return {Boolean} + */ + isShippingField: function (fieldId) { + if (this.shippingAsBilling) { return fieldId.include('billing'); } + return fieldId.include('shipping'); }, - isBillingField : function(fieldId){ + /** + * Checks if the field belongs to the billing address. + * + * @param {String} fieldId + * @return {Boolean} + */ + isBillingField: function (fieldId) { return fieldId.include('billing'); }, - bindAddressFields : function(container) { - var fields = $(container).select('input', 'select', 'textarea'); - for(var i=0;i<fields.length;i++){ - Event.observe(fields[i], 'change', this.triggerChangeEvent.bind(this)); - } - }, - /** - * Calls changing address field handler after timeout to prevent multiple simultaneous calls. + * Binds events on container form fields. * - * @param {Event} event + * @param {String} container */ - triggerChangeEvent: function (event) { - if (this.timerId) { - window.clearTimeout(this.timerId); - } + bindAddressFields: function (container) { + var fields = $(container).select('input', 'select', 'textarea'), + i; - this.timerId = window.setTimeout(this.changeAddressField.bind(this), 500, event); + for (i = 0; i < fields.length; i++) { + jQuery(fields[i]).change(this.changeAddressField.bind(this)); + } }, /** @@ -218,7 +241,8 @@ define([ matchRes = field.name.match(re), type, name, - data; + data, + resetShipping = false; if (!matchRes) { return; @@ -234,20 +258,31 @@ define([ } data = data.toObject(); - if (type === 'billing' && this.shippingAsBilling || type === 'shipping' && !this.shippingAsBilling) { + if (type === 'billing' && this.shippingAsBilling) { + this.syncAddressField(this.shippingAddressContainer, field.name, field.value); + resetShipping = true; + } + + if (type === 'shipping' && !this.shippingAsBilling) { + resetShipping = true; + } + + if (resetShipping) { data['reset_shipping'] = true; } data['order[' + type + '_address][customer_address_id]'] = null; - data['shipping_as_billing'] = jQuery('[name="shipping_same_as_billing"]').is(':checked') ? 1 : 0; + data['shipping_as_billing'] = +this.shippingAsBilling; if (name === 'customer_address_id') { data['order[' + type + '_address][customer_address_id]'] = $('order-' + type + '_address_customer_address_id').value; } + this.resetPaymentMethod(); + if (data['reset_shipping']) { - this.resetShippingMethod(data); + this.resetShippingMethod(); } else { this.saveData(data); @@ -257,7 +292,28 @@ define([ } }, - fillAddressFields : function(container, data){ + /** + * Set address container form field value. + * + * @param {String} container - container ID + * @param {String} fieldName - form field name + * @param {*} fieldValue - form field value + */ + syncAddressField: function (container, fieldName, fieldValue) { + var syncName; + + if (this.isBillingField(fieldName)) { + syncName = fieldName.replace('billing', 'shipping'); + } + + $(container).select('[name="' + syncName + '"]').each(function (element) { + if (~['input', 'textarea', 'select'].indexOf(element.tagName.toLowerCase())) { + element.value = fieldValue; + } + }); + }, + + fillAddressFields: function(container, data){ var regionIdElem = false; var regionIdElemValue = false; @@ -298,10 +354,15 @@ define([ fields[i].setValue(data[name] ? data[name] : ''); } - if (fields[i].changeUpdater) fields[i].changeUpdater(); + if (fields[i].changeUpdater) { + fields[i].changeUpdater(); + } + if (name == 'region' && data['region_id'] && !data['region']){ fields[i].value = data['region_id']; } + + jQuery(fields[i]).trigger('change'); } }, @@ -332,46 +393,83 @@ define([ } }, - setShippingAsBilling : function(flag){ - var data; - var areasToLoad = ['billing_method', 'shipping_address', 'totals', 'giftmessage']; + /** + * Equals shipping and billing addresses. + * + * @param {Boolean} flag + */ + setShippingAsBilling: function (flag) { + var data, + areasToLoad = ['billing_method', 'shipping_address', 'shipping_method', 'totals', 'giftmessage']; + this.disableShippingAddress(flag); - if(flag){ - data = this.serializeData(this.billingAddressContainer); - } else { - data = this.serializeData(this.shippingAddressContainer); - } - areasToLoad.push('shipping_method'); + data = this.serializeData(flag ? this.billingAddressContainer : this.shippingAddressContainer); data = data.toObject(); data['shipping_as_billing'] = flag ? 1 : 0; data['reset_shipping'] = 1; this.loadArea(areasToLoad, true, data); }, - resetShippingMethod : function(data){ - var areasToLoad = ['billing_method', 'shipping_address', 'totals', 'giftmessage', 'items']; - if(!this.isOnlyVirtualProduct) { - areasToLoad.push('shipping_method'); - areasToLoad.push('shipping_address'); + /** + * Replace shipping method area. + */ + resetShippingMethod: function () { + if (!this.isOnlyVirtualProduct) { + $(this.getAreaId('shipping_method')).update(this.shippingTemplate); } + }, - data['reset_shipping'] = 1; - this.isShippingMethodReseted = true; - this.loadArea(areasToLoad, true, data); + /** + * Replace payment method area. + */ + resetPaymentMethod: function () { + $(this.getAreaId('billing_method')).update(this.paymentTemplate); }, - loadShippingRates : function(){ + /** + * Loads shipping options according to address data. + * + * @return {Boolean} + */ + loadShippingRates: function () { + var addressContainer = this.shippingAsBilling ? + 'billingAddressContainer' : + 'shippingAddressContainer', + data = this.serializeData(this[addressContainer]).toObject(); + + data['collect_shipping_rates'] = 1; this.isShippingMethodReseted = false; - this.loadArea(['shipping_method', 'totals'], true, {collect_shipping_rates: 1}); + this.loadArea(['shipping_method', 'totals'], true, data); + + return false; }, - setShippingMethod : function(method){ + setShippingMethod: function(method) { var data = {}; + data['order[shipping_method]'] = method; - this.loadArea(['shipping_method', 'totals', 'billing_method'], true, data); + this.loadArea([ + 'shipping_method', + 'totals', + 'billing_method' + ], true, data); + }, + + /** + * Updates available payment + * methods list according to order data. + * + * @return boolean + */ + loadPaymentMethods: function() { + var data = this.serializeData(this.billingAddressContainer).toObject(); + + this.loadArea(['billing_method','totals'], true, data); + + return false; }, - switchPaymentMethod : function(method){ + switchPaymentMethod: function(method){ jQuery('#edit_form') .off('submitOrder') .on('submitOrder', function(){ @@ -1149,9 +1247,15 @@ define([ || this.excludedPaymentMethods.indexOf(this.paymentMethod) == -1); }, - serializeData : function(container){ - var fields = $(container).select('input', 'select', 'textarea'); - var data = Form.serializeElements(fields, true); + /** + * Serializes container form elements data. + * + * @param {String} container + * @return {Object} + */ + serializeData: function (container) { + var fields = $(container).select('input', 'select', 'textarea'), + data = Form.serializeElements(fields, true); return $H(data); }, @@ -1171,8 +1275,12 @@ define([ submit : function() { - jQuery('#edit_form').trigger('processStart'); - jQuery('#edit_form').trigger('submitOrder'); + var $editForm = jQuery('#edit_form'); + + if ($editForm.valid()) { + $editForm.trigger('processStart'); + $editForm.trigger('submitOrder'); + } }, _realSubmit: function () { @@ -1432,8 +1540,10 @@ define([ node.update('<span>' + this._label + '</span>'); content[position] = node; Element.insert(element, content); + }, + + getLabel: function(){ + return this._label; } }; - }); - diff --git a/app/code/Magento/Sales/view/adminhtml/web/templates/order/create/payment/reload.html b/app/code/Magento/Sales/view/adminhtml/web/templates/order/create/payment/reload.html new file mode 100644 index 000000000000..c503f3c678ab --- /dev/null +++ b/app/code/Magento/Sales/view/adminhtml/web/templates/order/create/payment/reload.html @@ -0,0 +1,18 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<div class="admin__page-section-title"> + <span class="title"><%- data.title %></span> +</div> +<div id="order-billing_method_summary" + class="order-billing-method-summary"> + <a href="#" + onclick="return order.loadPaymentMethods();" + class="action-default"> + <span><%- data.linkText %></span> + </a> +</div> diff --git a/app/code/Magento/Sales/view/adminhtml/web/templates/order/create/shipping/reload.html b/app/code/Magento/Sales/view/adminhtml/web/templates/order/create/shipping/reload.html new file mode 100644 index 000000000000..6b191ee81a45 --- /dev/null +++ b/app/code/Magento/Sales/view/adminhtml/web/templates/order/create/shipping/reload.html @@ -0,0 +1,19 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<div class="admin__page-section-title"> + <span class="title"><%- data.title %></span> +</div> +<div id="order-shipping-method-summary" + class="order-shipping-method-summary"> + <a href="#" + onclick="return order.loadShippingRates();" + class="action-default"> + <span><%- data.linkText %></span> + </a> + <input type="hidden" name="order[has_shipping]" value="" class="required-entry" /> +</div> diff --git a/app/code/Magento/Sales/view/frontend/email/creditmemo_update.html b/app/code/Magento/Sales/view/frontend/email/creditmemo_update.html index 3a4aab19e9e7..a6a10fb49e3f 100644 --- a/app/code/Magento/Sales/view/frontend/email/creditmemo_update.html +++ b/app/code/Magento/Sales/view/frontend/email/creditmemo_update.html @@ -11,7 +11,7 @@ "var this.getUrl($store, 'customer/account/')":"Customer Account URL", "var order.getCustomerName()":"Customer Name", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status" +"var order.getFrontendStatusLabel()":"Order Status" } @--> {{template config_path="design/email/header_template"}} @@ -24,7 +24,7 @@ "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order.getFrontendStatusLabel() |raw}} </p> <p>{{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}}</p> diff --git a/app/code/Magento/Sales/view/frontend/email/creditmemo_update_guest.html b/app/code/Magento/Sales/view/frontend/email/creditmemo_update_guest.html index bc7c079d7f21..b7411d80d2ba 100644 --- a/app/code/Magento/Sales/view/frontend/email/creditmemo_update_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/creditmemo_update_guest.html @@ -10,7 +10,7 @@ "var creditmemo.increment_id":"Credit Memo Id", "var billing.getName()":"Guest Customer Name", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status" +"var order.getFrontendStatusLabel()":"Order Status" } @--> {{template config_path="design/email/header_template"}} @@ -23,7 +23,7 @@ "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order.getFrontendStatusLabel() |raw}} </p> <p> diff --git a/app/code/Magento/Sales/view/frontend/email/invoice_update.html b/app/code/Magento/Sales/view/frontend/email/invoice_update.html index cafdd65ff520..4043e59f9d7d 100644 --- a/app/code/Magento/Sales/view/frontend/email/invoice_update.html +++ b/app/code/Magento/Sales/view/frontend/email/invoice_update.html @@ -11,7 +11,7 @@ "var comment":"Invoice Comment", "var invoice.increment_id":"Invoice Id", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status" +"var order.getFrontendStatusLabel()":"Order Status" } @--> {{template config_path="design/email/header_template"}} @@ -24,7 +24,7 @@ "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order.getFrontendStatusLabel() |raw}} </p> <p>{{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}}</p> diff --git a/app/code/Magento/Sales/view/frontend/email/invoice_update_guest.html b/app/code/Magento/Sales/view/frontend/email/invoice_update_guest.html index fafb533301ef..40cdec7fb4ca 100644 --- a/app/code/Magento/Sales/view/frontend/email/invoice_update_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/invoice_update_guest.html @@ -10,7 +10,7 @@ "var comment":"Invoice Comment", "var invoice.increment_id":"Invoice Id", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status" +"var order.getFrontendStatusLabel()":"Order Status" } @--> {{template config_path="design/email/header_template"}} @@ -23,7 +23,7 @@ "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order.getFrontendStatusLabel() |raw}} </p> <p> diff --git a/app/code/Magento/Sales/view/frontend/email/order_update.html b/app/code/Magento/Sales/view/frontend/email/order_update.html index a709a9ed8a7f..a8f0068b70e8 100644 --- a/app/code/Magento/Sales/view/frontend/email/order_update.html +++ b/app/code/Magento/Sales/view/frontend/email/order_update.html @@ -10,7 +10,7 @@ "var order.getCustomerName()":"Customer Name", "var comment":"Order Comment", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status" +"var order.getFrontendStatusLabel()":"Order Status" } @--> {{template config_path="design/email/header_template"}} @@ -23,7 +23,7 @@ "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order.getFrontendStatusLabel() |raw}} </p> <p>{{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}}</p> diff --git a/app/code/Magento/Sales/view/frontend/email/order_update_guest.html b/app/code/Magento/Sales/view/frontend/email/order_update_guest.html index 5a39b01810c1..749fa3b60ad5 100644 --- a/app/code/Magento/Sales/view/frontend/email/order_update_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/order_update_guest.html @@ -9,7 +9,7 @@ "var billing.getName()":"Guest Customer Name", "var comment":"Order Comment", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status" +"var order.getFrontendStatusLabel()":"Order Status" } @--> {{template config_path="design/email/header_template"}} @@ -22,7 +22,7 @@ "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order.getFrontendStatusLabel() |raw}} </p> <p> diff --git a/app/code/Magento/Sales/view/frontend/email/shipment_new.html b/app/code/Magento/Sales/view/frontend/email/shipment_new.html index 8af49f322c68..84f5acb29ea3 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 df1677f56a50..bb181126724d 100644 --- a/app/code/Magento/Sales/view/frontend/email/shipment_new_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/shipment_new_guest.html @@ -51,7 +51,7 @@ <h1>{{trans "Your Shipment #%shipment_id for Order #%order_id" shipment_id=$ship </tr> </table> {{/depend}} - {{block class='Magento\\Framework\\View\\Element\\Template' area='frontend' template='Magento_Sales::email/shipment/track.phtml' shipment=$shipment order=$order}} + {{layout handle="sales_email_order_shipment_track" shipment=$shipment order=$order}} <table class="order-details"> <tr> <td class="address-details"> diff --git a/app/code/Magento/Sales/view/frontend/email/shipment_update.html b/app/code/Magento/Sales/view/frontend/email/shipment_update.html index 6d9efc37004b..9d1c93287549 100644 --- a/app/code/Magento/Sales/view/frontend/email/shipment_update.html +++ b/app/code/Magento/Sales/view/frontend/email/shipment_update.html @@ -10,7 +10,7 @@ "var order.getCustomerName()":"Customer Name", "var comment":"Order Comment", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status", +"var order.getFrontendStatusLabel()":"Order Status", "var shipment.increment_id":"Shipment Id" } @--> {{template config_path="design/email/header_template"}} @@ -24,7 +24,7 @@ "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order.getFrontendStatusLabel() |raw}} </p> <p>{{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}}</p> diff --git a/app/code/Magento/Sales/view/frontend/email/shipment_update_guest.html b/app/code/Magento/Sales/view/frontend/email/shipment_update_guest.html index 4896a00b7bc5..0d2dccd3377d 100644 --- a/app/code/Magento/Sales/view/frontend/email/shipment_update_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/shipment_update_guest.html @@ -9,7 +9,7 @@ "var billing.getName()":"Guest Customer Name", "var comment":"Order Comment", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status", +"var order.getFrontendStatusLabel()":"Order Status", "var shipment.increment_id":"Shipment Id" } @--> {{template config_path="design/email/header_template"}} @@ -23,7 +23,7 @@ "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order.getFrontendStatusLabel() |raw}} </p> <p> diff --git a/app/code/Magento/Sales/view/frontend/layout/sales_email_order_shipment_track.xml b/app/code/Magento/Sales/view/frontend/layout/sales_email_order_shipment_track.xml new file mode 100644 index 000000000000..91414663951d --- /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/layout/sales_guest_print.xml b/app/code/Magento/Sales/view/frontend/layout/sales_guest_print.xml index 0e2689e3e219..99af16ce629d 100644 --- a/app/code/Magento/Sales/view/frontend/layout/sales_guest_print.xml +++ b/app/code/Magento/Sales/view/frontend/layout/sales_guest_print.xml @@ -12,11 +12,20 @@ <body> <attribute name="class" value="sales-guest-view"/> <referenceContainer name="page.main.title"> - <block class="Magento\Sales\Block\Order\PrintShipment" name="order.status" template="Magento_Sales::order/order_status.phtml" /> - <block class="Magento\Sales\Block\Order\PrintShipment" name="order.date" template="Magento_Sales::order/order_date.phtml" /> + <block class="Magento\Sales\Block\Order\PrintShipment" + name="order.status" + template="Magento_Sales::order/order_status.phtml" + cacheable="false" /> + <block class="Magento\Sales\Block\Order\PrintShipment" + name="order.date" + template="Magento_Sales::order/order_date.phtml" + cacheable="false" /> </referenceContainer> <referenceContainer name="content"> - <block class="Magento\Sales\Block\Order\PrintShipment" name="sales.order.print" template="Magento_Sales::order/view.phtml"> + <block class="Magento\Sales\Block\Order\PrintShipment" + name="sales.order.print" + template="Magento_Sales::order/view.phtml" + cacheable="false"> <block class="Magento\Sales\Block\Order\PrintShipment" name="order_items" template="Magento_Sales::order/items.phtml"> <block class="Magento\Framework\View\Element\RendererList" name="sales.order.print.renderers" as="renderer.list" /> <block class="Magento\Sales\Block\Order\Totals" name="order_totals" template="Magento_Sales::order/totals.phtml"> diff --git a/app/code/Magento/Sales/view/frontend/layout/sales_order_print.xml b/app/code/Magento/Sales/view/frontend/layout/sales_order_print.xml index 50ffe979651e..4410a6fc4a9a 100644 --- a/app/code/Magento/Sales/view/frontend/layout/sales_order_print.xml +++ b/app/code/Magento/Sales/view/frontend/layout/sales_order_print.xml @@ -12,12 +12,21 @@ <body> <attribute name="class" value="account"/> <referenceContainer name="page.main.title"> - <block class="Magento\Sales\Block\Order\PrintShipment" name="order.status" template="Magento_Sales::order/order_status.phtml" /> - <block class="Magento\Sales\Block\Order\PrintShipment" name="order.date" template="Magento_Sales::order/order_date.phtml" /> + <block class="Magento\Sales\Block\Order\PrintShipment" + name="order.status" + template="Magento_Sales::order/order_status.phtml" + cacheable="false" /> + <block class="Magento\Sales\Block\Order\PrintShipment" + name="order.date" + template="Magento_Sales::order/order_date.phtml" + cacheable="false" /> </referenceContainer> <referenceContainer name="content"> - <block class="Magento\Sales\Block\Order\PrintShipment" name="sales.order.print" template="Magento_Sales::order/view.phtml"> - <block class="Magento\Sales\Block\Order\PrintShipment" name="order_items" template="Magento_Sales::order/items.phtml"> + <block class="Magento\Sales\Block\Order\PrintShipment" + name="sales.order.print" + template="Magento_Sales::order/view.phtml" + cacheable="false"> + <block class="Magento\Sales\Block\Order\Items" name="order_items" template="Magento_Sales::order/items.phtml"> <block class="Magento\Framework\View\Element\RendererList" name="sales.order.print.renderers" as="renderer.list" /> <block class="Magento\Sales\Block\Order\Totals" name="order_totals" template="Magento_Sales::order/totals.phtml"> <arguments> diff --git a/app/code/Magento/Sales/view/frontend/requirejs-config.js b/app/code/Magento/Sales/view/frontend/requirejs-config.js index 04778765f3c9..4d323684afff 100644 --- a/app/code/Magento/Sales/view/frontend/requirejs-config.js +++ b/app/code/Magento/Sales/view/frontend/requirejs-config.js @@ -6,8 +6,10 @@ var config = { map: { '*': { - giftMessage: 'Magento_Sales/gift-message', - ordersReturns: 'Magento_Sales/orders-returns' + giftMessage: 'Magento_Sales/js/gift-message', + ordersReturns: 'Magento_Sales/js/orders-returns', + 'Magento_Sales/gift-message': 'Magento_Sales/js/gift-message', + 'Magento_Sales/orders-returns': 'Magento_Sales/js/orders-returns' } } }; diff --git a/app/code/Magento/Sales/view/frontend/templates/email/items/creditmemo/default.phtml b/app/code/Magento/Sales/view/frontend/templates/email/items/creditmemo/default.phtml index 1fca65932b0b..20c2c1869fed 100644 --- a/app/code/Magento/Sales/view/frontend/templates/email/items/creditmemo/default.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/email/items/creditmemo/default.phtml @@ -31,6 +31,6 @@ </td> <td class="item-qty"><?= /* @escapeNotVerified */ $_item->getQty() * 1 ?></td> <td class="item-price"> - <?= /* @escapeNotVerified */ $block->getItemPrice($_item->getOrderItem()) ?> + <?= /* @escapeNotVerified */ $block->getItemPrice($_item) ?> </td> </tr> diff --git a/app/code/Magento/Sales/view/frontend/templates/email/shipment/track.phtml b/app/code/Magento/Sales/view/frontend/templates/email/shipment/track.phtml index 9f7146ab084d..f1cd5f2b9986 100644 --- a/app/code/Magento/Sales/view/frontend/templates/email/shipment/track.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/email/shipment/track.phtml @@ -9,22 +9,23 @@ ?> <?php $_shipment = $block->getShipment() ?> <?php $_order = $block->getOrder() ?> -<?php if ($_shipment && $_order && $_shipment->getAllTracks()): ?> -<br /> -<table class="shipment-track"> - <thead> +<?php $trackCollection = $_order->getTracksCollection($_shipment->getId()) ?> +<?php if ($_shipment && $_order && $trackCollection): ?> + <br /> + <table class="shipment-track"> + <thead> <tr> <th><?= /* @escapeNotVerified */ __('Shipped By') ?></th> <th><?= /* @escapeNotVerified */ __('Tracking Number') ?></th> </tr> - </thead> - <tbody> - <?php foreach ($_shipment->getAllTracks() as $_item): ?> - <tr> - <td><?= $block->escapeHtml($_item->getTitle()) ?>:</td> - <td><?= $block->escapeHtml($_item->getNumber()) ?></td> - </tr> - <?php endforeach ?> - </tbody> -</table> + </thead> + <tbody> + <?php foreach ($trackCollection as $_item): ?> + <tr> + <td><?= $block->escapeHtml($_item->getTitle()) ?>:</td> + <td><?= $block->escapeHtml($_item->getNumber()) ?></td> + </tr> + <?php endforeach ?> + </tbody> + </table> <?php endif; ?> diff --git a/app/code/Magento/Sales/view/frontend/templates/guest/form.phtml b/app/code/Magento/Sales/view/frontend/templates/guest/form.phtml index 3ebca4d08b34..89be19058867 100644 --- a/app/code/Magento/Sales/view/frontend/templates/guest/form.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/guest/form.phtml @@ -10,7 +10,7 @@ <form class="form form-orders-search" id="oar-widget-orders-and-returns-form" data-mage-init='{"ordersReturns":{}, "validation":{}}' action="<?= /* @escapeNotVerified */ $block->getActionUrl() ?>" method="post" name="guest_post"> <fieldset class="fieldset"> - <legend class="admin__legend"><span><?= /* @escapeNotVerified */ __('Order Information') ?></span></legend> + <legend class="legend"><span><?= /* @escapeNotVerified */ __('Order Information') ?></span></legend> <br> <div class="field id required"> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/history.phtml b/app/code/Magento/Sales/view/frontend/templates/order/history.phtml index 1c02a5c31ea6..b9a032212352 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/history.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/history.phtml @@ -6,6 +6,8 @@ // @codingStandardsIgnoreFile +/** @var \Magento\Sales\Block\Order\History $block */ + ?> <?php $_orders = $block->getOrders(); ?> <?= $block->getChildHtml('info') ?> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/items.phtml b/app/code/Magento/Sales/view/frontend/templates/order/items.phtml index e43d32760feb..dc179b6ee4ac 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/items.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/items.phtml @@ -29,9 +29,10 @@ </thead> <?php $items = $block->getItems(); ?> <?php $giftMessage = ''?> + <tbody> <?php foreach ($items as $item): ?> <?php if ($item->getParentItem()) continue; ?> - <tbody> + <?= $block->getItemHtml($item) ?> <?php if ($this->helper('Magento\GiftMessage\Helper\Message')->isMessagesAllowed('order_item', $item) && $item->getGiftMessageId()): ?> <?php $giftMessage = $this->helper('Magento\GiftMessage\Helper\Message')->getGiftMessageForEntity($item); ?> @@ -62,8 +63,8 @@ </td> </tr> <?php endif ?> - </tbody> <?php endforeach; ?> + </tbody> <tfoot> <?php if($block->isPagerDisplayed()): ?> <tr> 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 5ecf1ebe893b..a2ab3d02b13e 100644 --- a/app/code/Magento/Sales/view/frontend/templates/reorder/sidebar.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/reorder/sidebar.phtml @@ -26,14 +26,18 @@ <ol id="cart-sidebar-reorder" class="product-items product-items-names" data-bind="foreach: lastOrderedItems().items"> <li class="product-item"> - <div class="field item choice no-display" data-bind="css: {'no-display': !is_saleable}"> + <div class="field item choice"> <label class="label" data-bind="attr: {'for': 'reorder-item-' + id}"> <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> </label> <div class="control"> <input type="checkbox" name="order_items[]" - data-bind="attr: {id: 'reorder-item-' + id, value: id}" - title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>" + data-bind="attr: { + id: 'reorder-item-' + id, + value: id, + title: is_saleable ? '<?= /* @escapeNotVerified */ __('Add to Cart') ?>' : '<?= /* @escapeNotVerified */ __('Product is not salable.') ?>' + }, + disable: !is_saleable" class="checkbox" data-validate='{"validate-one-checkbox-required-by-name": true}'/> </div> </div> @@ -46,14 +50,14 @@ </ol> <div id="cart-sidebar-reorder-advice-container"></div> <div class="actions-toolbar"> - <div class="primary no-display" - data-bind="css: {'no-display': !lastOrderedItems().isShowAddToCart}"> + <div class="primary" + data-bind="visible: isShowAddToCart"> <button type="submit" title="<?= /* @escapeNotVerified */ __('Add to Cart') ?>" class="action tocart primary"> <span><?= /* @escapeNotVerified */ __('Add to Cart') ?></span> </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/Sales/view/frontend/web/gift-message.js b/app/code/Magento/Sales/view/frontend/web/js/gift-message.js similarity index 100% rename from app/code/Magento/Sales/view/frontend/web/gift-message.js rename to app/code/Magento/Sales/view/frontend/web/js/gift-message.js diff --git a/app/code/Magento/Sales/view/frontend/web/orders-returns.js b/app/code/Magento/Sales/view/frontend/web/js/orders-returns.js similarity index 100% rename from app/code/Magento/Sales/view/frontend/web/orders-returns.js rename to app/code/Magento/Sales/view/frontend/web/js/orders-returns.js diff --git a/app/code/Magento/Sales/view/frontend/web/js/view/last-ordered-items.js b/app/code/Magento/Sales/view/frontend/web/js/view/last-ordered-items.js index f393cc3fcd3b..17e61a77d98a 100644 --- a/app/code/Magento/Sales/view/frontend/web/js/view/last-ordered-items.js +++ b/app/code/Magento/Sales/view/frontend/web/js/view/last-ordered-items.js @@ -5,27 +5,43 @@ define([ 'uiComponent', - 'Magento_Customer/js/customer-data' -], function (Component, customerData) { + 'Magento_Customer/js/customer-data', + 'underscore' +], function (Component, customerData, _) { 'use strict'; return Component.extend({ + defaults: { + isShowAddToCart: false + }, + /** @inheritdoc */ initialize: function () { - var isShowAddToCart = false, - item; - this._super(); this.lastOrderedItems = customerData.get('last-ordered-items'); + this.lastOrderedItems.subscribe(this.checkSalableItems.bind(this)); + this.checkSalableItems(); + + return this; + }, + + /** @inheritdoc */ + initObservable: function () { + this._super() + .observe('isShowAddToCart'); + + return this; + }, - for (item in this.lastOrderedItems.items) { - if (item['is_saleable']) { - isShowAddToCart = true; - break; - } - } + /** + * Check if items is_saleable and change add to cart button visibility. + */ + checkSalableItems: function () { + var isShowAddToCart = _.some(this.lastOrderedItems().items, { + 'is_saleable': true + }); - this.lastOrderedItems.isShowAddToCart = isShowAddToCart; + this.isShowAddToCart(isShowAddToCart); } }); }); diff --git a/app/code/Magento/SalesAnalytics/README.md b/app/code/Magento/SalesAnalytics/README.md index 70f456c97d4b..0b1f804b278f 100644 --- a/app/code/Magento/SalesAnalytics/README.md +++ b/app/code/Magento/SalesAnalytics/README.md @@ -1,3 +1,3 @@ # Magento_SalesAnalytics module -The Magento_SalesAnalytics module configures data definitions for a data collection related to the Sales module entities to be used in [Advanced Reporting](http://devdocs.magento.com/guides/v2.2/advanced-reporting/modules.html). +The Magento_SalesAnalytics module configures data definitions for a data collection related to the Sales module entities to be used in [Advanced Reporting](https://devdocs.magento.com/guides/v2.2/advanced-reporting/modules.html). diff --git a/app/code/Magento/SalesAnalytics/composer.json b/app/code/Magento/SalesAnalytics/composer.json index 64424c8f5bc6..b77dcd7e71c6 100644 --- a/app/code/Magento/SalesAnalytics/composer.json +++ b/app/code/Magento/SalesAnalytics/composer.json @@ -4,7 +4,8 @@ "require": { "php": "~7.1.3||~7.2.0", "magento/framework": "*", - "magento/module-sales": "*" + "magento/module-sales": "*", + "magento/module-analytics": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/Orders.php b/app/code/Magento/SalesGraphQl/Model/Resolver/Orders.php new file mode 100644 index 000000000000..3e592cf061df --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/Orders.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Sales\Model\ResourceModel\Order\CollectionFactoryInterface; +use Magento\CustomerGraphQl\Model\Customer\GetCustomer; + +/** + * Orders data reslover + */ +class Orders implements ResolverInterface +{ + /** + * @var CollectionFactoryInterface + */ + private $collectionFactory; + + /** + * @var GetCustomer + */ + private $getCustomer; + + /** + * @param CollectionFactoryInterface $collectionFactory + * @param GetCustomer $getCustomer + */ + public function __construct( + CollectionFactoryInterface $collectionFactory, + GetCustomer $getCustomer + ) { + $this->collectionFactory = $collectionFactory; + $this->getCustomer = $getCustomer; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + $customer = $this->getCustomer->execute($context); + + $items = []; + $orders = $this->collectionFactory->create($customer->getId()); + + /** @var \Magento\Sales\Model\Order $order */ + foreach ($orders as $order) { + $items[] = [ + 'id' => $order->getId(), + 'increment_id' => $order->getIncrementId(), + 'created_at' => $order->getCreatedAt(), + 'grand_total' => $order->getGrandTotal(), + 'status' => $order->getStatus(), + ]; + } + return ['items' => $items]; + } +} diff --git a/app/code/Magento/SalesGraphQl/README.md b/app/code/Magento/SalesGraphQl/README.md new file mode 100644 index 000000000000..d5717821b164 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/README.md @@ -0,0 +1,4 @@ +# SalesGraphQl + +**SalesGraphQl** provides type and resolver information for the GraphQl module +to generate sales orders information. diff --git a/app/code/Magento/SalesGraphQl/composer.json b/app/code/Magento/SalesGraphQl/composer.json new file mode 100644 index 000000000000..0549d31d59a2 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/composer.json @@ -0,0 +1,26 @@ +{ + "name": "magento/module-sales-graph-ql", + "description": "N/A", + "type": "magento2-module", + "require": { + "php": "~7.1.3||~7.2.0", + "magento/framework": "*", + "magento/module-customer-graph-ql": "*", + "magento/module-sales": "*" + }, + "suggest": { + "magento/module-graph-ql": "*" + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\SalesGraphQl\\": "" + } + } +} diff --git a/app/code/Magento/SalesGraphQl/etc/module.xml b/app/code/Magento/SalesGraphQl/etc/module.xml new file mode 100644 index 000000000000..70a55db67a50 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/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_SalesGraphQl"/> +</config> diff --git a/app/code/Magento/SalesGraphQl/etc/schema.graphqls b/app/code/Magento/SalesGraphQl/etc/schema.graphqls new file mode 100644 index 000000000000..06146f805c64 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/etc/schema.graphqls @@ -0,0 +1,18 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +type Query { + customerOrders: CustomerOrders @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Orders") @doc(description: "List of customer orders") @cache(cacheable: false) +} + +type CustomerOrder @doc(description: "Order mapping fields") { + id: Int + increment_id: String + created_at: String + grand_total: Float + status: String +} + +type CustomerOrders { + items: [CustomerOrder] @doc(description: "Array of orders") +} diff --git a/app/code/Magento/SalesGraphQl/registration.php b/app/code/Magento/SalesGraphQl/registration.php new file mode 100644 index 000000000000..afb4091bfa32 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/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_SalesGraphQl', __DIR__); diff --git a/app/code/Magento/SalesRule/Api/CouponRepositoryInterface.php b/app/code/Magento/SalesRule/Api/CouponRepositoryInterface.php index d4cce37fd15f..1a631886f1a9 100644 --- a/app/code/Magento/SalesRule/Api/CouponRepositoryInterface.php +++ b/app/code/Magento/SalesRule/Api/CouponRepositoryInterface.php @@ -38,7 +38,7 @@ public function getById($couponId); * Retrieve a coupon using the specified search criteria. * * This call returns an array of objects, but detailed information about each object’s attributes might not be - * included. See http://devdocs.magento.com/codelinks/attributes.html#CouponRepositoryInterface to + * included. See https://devdocs.magento.com/codelinks/attributes.html#CouponRepositoryInterface to * determine which call to use to get detailed information about all attributes for an object. * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria diff --git a/app/code/Magento/SalesRule/Api/RuleRepositoryInterface.php b/app/code/Magento/SalesRule/Api/RuleRepositoryInterface.php index d0d275de63a0..963edf5483e4 100644 --- a/app/code/Magento/SalesRule/Api/RuleRepositoryInterface.php +++ b/app/code/Magento/SalesRule/Api/RuleRepositoryInterface.php @@ -38,7 +38,7 @@ public function getById($ruleId); * Retrieve sales rules that match te specified criteria. * * This call returns an array of objects, but detailed information about each object’s attributes might not be - * included. See http://devdocs.magento.com/codelinks/attributes.html#RuleRepositoryInterface to + * included. See https://devdocs.magento.com/codelinks/attributes.html#RuleRepositoryInterface to * determine which call to use to get detailed information about all attributes for an object. * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria diff --git a/app/code/Magento/SalesRule/Block/Adminhtml/Promo/Quote/Edit/DeleteButton.php b/app/code/Magento/SalesRule/Block/Adminhtml/Promo/Quote/Edit/DeleteButton.php index 0cb286056d82..bee7573c1fe2 100644 --- a/app/code/Magento/SalesRule/Block/Adminhtml/Promo/Quote/Edit/DeleteButton.php +++ b/app/code/Magento/SalesRule/Block/Adminhtml/Promo/Quote/Edit/DeleteButton.php @@ -26,7 +26,7 @@ public function getButtonData() 'class' => 'delete', 'on_click' => 'deleteConfirm(\'' . __( 'Are you sure you want to delete this?' - ) . '\', \'' . $this->urlBuilder->getUrl('*/*/delete', ['id' => $ruleId]) . '\')', + ) . '\', \'' . $this->urlBuilder->getUrl('*/*/delete', ['id' => $ruleId]) . '\', {data: {}})', 'sort_order' => 20, ]; } diff --git a/app/code/Magento/SalesRule/Block/Adminhtml/Promo/Widget/Chooser.php b/app/code/Magento/SalesRule/Block/Adminhtml/Promo/Widget/Chooser.php index b4be904223aa..901866d52f73 100644 --- a/app/code/Magento/SalesRule/Block/Adminhtml/Promo/Widget/Chooser.php +++ b/app/code/Magento/SalesRule/Block/Adminhtml/Promo/Widget/Chooser.php @@ -3,8 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\SalesRule\Block\Adminhtml\Promo\Widget; +/** + * Widget that allows to select a sales rule. + */ class Chooser extends \Magento\Backend\Block\Widget\Grid\Extended { /** @@ -87,7 +91,7 @@ public function prepareElementHtml(\Magento\Framework\Data\Form\Element\Abstract if ($element->getValue()) { $rule = $this->ruleFactory->create()->load((int)$element->getValue()); if ($rule->getId()) { - $chooser->setLabel($rule->getName()); + $chooser->setLabel($this->escapeHtml($rule->getName())); } } diff --git a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Delete.php b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Delete.php index 28993e159499..45b717fb9bd2 100644 --- a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Delete.php +++ b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Delete.php @@ -6,7 +6,9 @@ */ namespace Magento\SalesRule\Controller\Adminhtml\Promo\Quote; -class Delete extends \Magento\SalesRule\Controller\Adminhtml\Promo\Quote +use Magento\Framework\App\Action\HttpPostActionInterface; + +class Delete extends \Magento\SalesRule\Controller\Adminhtml\Promo\Quote implements HttpPostActionInterface { /** * Delete promo quote action diff --git a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Edit.php b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Edit.php index 717c71fde483..3255ee0f2b72 100644 --- a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Edit.php +++ b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Edit.php @@ -6,7 +6,9 @@ */ namespace Magento\SalesRule\Controller\Adminhtml\Promo\Quote; -class Edit extends \Magento\SalesRule\Controller\Adminhtml\Promo\Quote +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Edit extends \Magento\SalesRule\Controller\Adminhtml\Promo\Quote implements HttpGetActionInterface { /** * @var \Magento\Framework\View\Result\PageFactory 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 12d34b8320d0..cfafe110df22 100644 --- a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Generate.php +++ b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Generate.php @@ -1,21 +1,37 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\SalesRule\Controller\Adminhtml\Promo\Quote; -use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\SalesRule\Model\CouponGenerator; +use Magento\Framework\MessageQueue\PublisherInterface; +use Magento\SalesRule\Api\Data\CouponGenerationSpecInterfaceFactory; -class Generate extends \Magento\SalesRule\Controller\Adminhtml\Promo\Quote +/** + * Generate promo quote + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class Generate extends \Magento\SalesRule\Controller\Adminhtml\Promo\Quote implements HttpPostActionInterface { /** * @var CouponGenerator */ private $couponGenerator; + /** + * @var PublisherInterface + */ + private $messagePublisher; + + /** + * @var CouponGenerationSpecInterfaceFactory + */ + private $generationSpecFactory; + /** * Generate constructor. * @param \Magento\Backend\App\Action\Context $context @@ -23,17 +39,27 @@ class Generate extends \Magento\SalesRule\Controller\Adminhtml\Promo\Quote * @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 + ); } /** @@ -62,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/Index.php b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Index.php index 0c11ee3785a0..4f3c712049e4 100644 --- a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Index.php +++ b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Index.php @@ -6,7 +6,9 @@ */ namespace Magento\SalesRule\Controller\Adminhtml\Promo\Quote; -class Index extends \Magento\SalesRule\Controller\Adminhtml\Promo\Quote +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\SalesRule\Controller\Adminhtml\Promo\Quote implements HttpGetActionInterface { /** * Index action diff --git a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewAction.php b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewAction.php index ecf39605a870..5adef0552bc8 100644 --- a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewAction.php +++ b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewAction.php @@ -6,7 +6,9 @@ */ namespace Magento\SalesRule\Controller\Adminhtml\Promo\Quote; -class NewAction extends \Magento\SalesRule\Controller\Adminhtml\Promo\Quote +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class NewAction extends \Magento\SalesRule\Controller\Adminhtml\Promo\Quote implements HttpGetActionInterface { /** * New promo quote action diff --git a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewConditionHtml.php b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewConditionHtml.php index a0238890d98a..50545fd86486 100644 --- a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewConditionHtml.php +++ b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewConditionHtml.php @@ -1,12 +1,16 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\SalesRule\Controller\Adminhtml\Promo\Quote; -class NewConditionHtml extends \Magento\SalesRule\Controller\Adminhtml\Promo\Quote +use Magento\Framework\App\Action\HttpPostActionInterface; + +/** + * Controller class NewConditionHtml. Returns condition html + */ +class NewConditionHtml extends \Magento\SalesRule\Controller\Adminhtml\Promo\Quote implements HttpPostActionInterface { /** * New condition html action diff --git a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Save.php b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Save.php index a5b71130e70e..7d55d18b770e 100644 --- a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Save.php +++ b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Save.php @@ -6,8 +6,52 @@ */ namespace Magento\SalesRule\Controller\Adminhtml\Promo\Quote; -class Save extends \Magento\SalesRule\Controller\Adminhtml\Promo\Quote +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\App\Request\DataPersistorInterface; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; + +/** + * SalesRule save controller + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class Save extends \Magento\SalesRule\Controller\Adminhtml\Promo\Quote implements HttpPostActionInterface { + /** + * @var TimezoneInterface + */ + private $timezone; + + /** + * @var DataPersistorInterface + */ + private $dataPersistor; + + /** + * @param \Magento\Backend\App\Action\Context $context + * @param \Magento\Framework\Registry $coreRegistry + * @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, + 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 + ); + } + /** * Promo quote save action * @@ -26,6 +70,9 @@ public function execute() ['request' => $this->getRequest()] ); $data = $this->getRequest()->getPostValue(); + if (empty($data['from_date'])) { + $data['from_date'] = $this->timezone->formatDate(); + } $filterValues = ['from_date' => $this->_dateFilter]; if ($this->getRequest()->getParam('to_date')) { @@ -37,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); @@ -53,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; } @@ -111,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.php b/app/code/Magento/SalesRule/Model/Coupon.php index ee1eaff08303..3c96d08a38dd 100644 --- a/app/code/Magento/SalesRule/Model/Coupon.php +++ b/app/code/Magento/SalesRule/Model/Coupon.php @@ -39,10 +39,10 @@ protected function _construct() /** * Set rule instance * - * @param \Magento\SalesRule\Model\Rule $rule + * @param Rule $rule * @return $this */ - public function setRule(\Magento\SalesRule\Model\Rule $rule) + public function setRule(Rule $rule) { $this->setRuleId($rule->getId()); return $this; @@ -51,7 +51,7 @@ public function setRule(\Magento\SalesRule\Model\Rule $rule) /** * Load primary coupon for specified rule * - * @param \Magento\SalesRule\Model\Rule|int $rule + * @param Rule|int $rule * @return $this */ public function loadPrimaryByRule($rule) @@ -190,6 +190,8 @@ public function getTimesUsed() } /** + * Set Times Used + * * @param int $timesUsed * @return $this */ @@ -273,6 +275,8 @@ public function getType() } /** + * Set type + * * @param int $type * @return $this */ 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 000000000000..2354c72a3e29 --- /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/CouponRepository.php b/app/code/Magento/SalesRule/Model/CouponRepository.php index 99d6727a8be2..8a77c3bb0f93 100644 --- a/app/code/Magento/SalesRule/Model/CouponRepository.php +++ b/app/code/Magento/SalesRule/Model/CouponRepository.php @@ -8,7 +8,6 @@ use Magento\Framework\Api\Search\FilterGroup; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; -use Magento\Framework\Api\SortOrder; use Magento\SalesRule\Model\ResourceModel\Coupon\Collection; /** @@ -197,13 +196,13 @@ public function deleteById($couponId) /** * Helper function that adds a FilterGroup to the collection. * - * @param \Magento\Framework\Api\Search\FilterGroup $filterGroup + * @param FilterGroup $filterGroup * @param Collection $collection * @deprecated 100.2.0 * @return void */ protected function addFilterGroupToCollection( - \Magento\Framework\Api\Search\FilterGroup $filterGroup, + FilterGroup $filterGroup, Collection $collection ) { $fields = []; diff --git a/app/code/Magento/SalesRule/Model/Data/Rule.php b/app/code/Magento/SalesRule/Model/Data/Rule.php index 72465b285032..58520831c016 100644 --- a/app/code/Magento/SalesRule/Model/Data/Rule.php +++ b/app/code/Magento/SalesRule/Model/Data/Rule.php @@ -16,8 +16,7 @@ * @SuppressWarnings(PHPMD.ExcessivePublicCount) * @codeCoverageIgnore */ -class Rule extends \Magento\Framework\Api\AbstractExtensibleObject implements - \Magento\SalesRule\Api\Data\RuleInterface +class Rule extends \Magento\Framework\Api\AbstractExtensibleObject implements RuleInterface { const KEY_RULE_ID = 'rule_id'; const KEY_NAME = 'name'; @@ -271,6 +270,8 @@ public function getIsAdvanced() } /** + * Set Is Advanced + * * @param bool $isAdvanced * @return $this */ @@ -374,6 +375,8 @@ public function getSortOrder() } /** + * Set Sort Order + * * @param int $sortOrder * @return $this */ @@ -617,7 +620,7 @@ public function setSimpleFreeShipping($simpleFreeShipping) } /** - * {@inheritdoc} + * @inheritdoc * * @return \Magento\SalesRule\Api\Data\RuleExtensionInterface|null */ @@ -627,7 +630,7 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * @inheritdoc * * @param \Magento\SalesRule\Api\Data\RuleExtensionInterface $extensionAttributes * @return $this diff --git a/app/code/Magento/SalesRule/Model/Plugin/QuoteConfigProductAttributes.php b/app/code/Magento/SalesRule/Model/Plugin/QuoteConfigProductAttributes.php index 9705808c88a0..b9d461037a23 100644 --- a/app/code/Magento/SalesRule/Model/Plugin/QuoteConfigProductAttributes.php +++ b/app/code/Magento/SalesRule/Model/Plugin/QuoteConfigProductAttributes.php @@ -5,10 +5,11 @@ */ namespace Magento\SalesRule\Model\Plugin; -use Magento\Store\Model\StoreManagerInterface; -use Magento\Customer\Model\Session; use Magento\SalesRule\Model\ResourceModel\Rule as RuleResource; +/** + * Quote Config Product Attributes Class + */ class QuoteConfigProductAttributes { /** diff --git a/app/code/Magento/SalesRule/Model/Quote/Address/Total/ShippingDiscount.php b/app/code/Magento/SalesRule/Model/Quote/Address/Total/ShippingDiscount.php new file mode 100644 index 000000000000..c37ca276e0ee --- /dev/null +++ b/app/code/Magento/SalesRule/Model/Quote/Address/Total/ShippingDiscount.php @@ -0,0 +1,101 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Model\Quote\Address\Total; + +use Magento\Quote\Api\Data\ShippingAssignmentInterface as ShippingAssignment; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Address\Total; +use Magento\SalesRule\Model\Quote\Discount as DiscountCollector; +use Magento\SalesRule\Model\Validator; + +/** + * Total collector for shipping discounts. + */ +class ShippingDiscount extends \Magento\Quote\Model\Quote\Address\Total\AbstractTotal +{ + /** + * @var Validator + */ + private $calculator; + + /** + * @param Validator $calculator + */ + public function __construct(Validator $calculator) + { + $this->calculator = $calculator; + } + + /** + * @inheritdoc + * + * @param Quote $quote + * @param ShippingAssignment $shippingAssignment + * @param Total $total + * @return ShippingDiscount + */ + public function collect(Quote $quote, ShippingAssignment $shippingAssignment, Total $total): self + { + parent::collect($quote, $shippingAssignment, $total); + + $address = $shippingAssignment->getShipping()->getAddress(); + $this->calculator->reset($address); + + $items = $shippingAssignment->getItems(); + if (!count($items)) { + return $this; + } + + $address->setShippingDiscountAmount(0); + $address->setBaseShippingDiscountAmount(0); + if ($address->getShippingAmount()) { + $this->calculator->processShippingAmount($address); + $total->addTotalAmount(DiscountCollector::COLLECTOR_TYPE_CODE, -$address->getShippingDiscountAmount()); + $total->addBaseTotalAmount( + DiscountCollector::COLLECTOR_TYPE_CODE, + -$address->getBaseShippingDiscountAmount() + ); + $total->setShippingDiscountAmount($address->getShippingDiscountAmount()); + $total->setBaseShippingDiscountAmount($address->getBaseShippingDiscountAmount()); + + $this->calculator->prepareDescription($address); + $total->setDiscountDescription($address->getDiscountDescription()); + $total->setSubtotalWithDiscount($total->getSubtotal() + $total->getDiscountAmount()); + $total->setBaseSubtotalWithDiscount($total->getBaseSubtotal() + $total->getBaseDiscountAmount()); + + $address->setDiscountAmount($total->getDiscountAmount()); + $address->setBaseDiscountAmount($total->getBaseDiscountAmount()); + } + + return $this; + } + + /** + * @inheritdoc + * + * @param \Magento\Quote\Model\Quote $quote + * @param \Magento\Quote\Model\Quote\Address\Total $total + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function fetch(Quote $quote, Total $total): array + { + $result = []; + $amount = $total->getDiscountAmount(); + + if ($amount != 0) { + $description = $total->getDiscountDescription() ?: ''; + $result = [ + 'code' => DiscountCollector::COLLECTOR_TYPE_CODE, + 'title' => strlen($description) ? __('Discount (%1)', $description) : __('Discount'), + 'value' => $amount + ]; + } + return $result; + } +} diff --git a/app/code/Magento/SalesRule/Model/Quote/Discount.php b/app/code/Magento/SalesRule/Model/Quote/Discount.php index 693a61b272f6..315ce874513a 100644 --- a/app/code/Magento/SalesRule/Model/Quote/Discount.php +++ b/app/code/Magento/SalesRule/Model/Quote/Discount.php @@ -5,8 +5,13 @@ */ namespace Magento\SalesRule\Model\Quote; +/** + * Discount totals calculation model. + */ class Discount extends \Magento\Quote\Model\Quote\Address\Total\AbstractTotal { + const COLLECTOR_TYPE_CODE = 'discount'; + /** * Discount calculation object * @@ -43,7 +48,7 @@ public function __construct( \Magento\SalesRule\Model\Validator $validator, \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency ) { - $this->setCode('discount'); + $this->setCode(self::COLLECTOR_TYPE_CODE); $this->eventManager = $eventManager; $this->calculator = $validator; $this->storeManager = $storeManager; @@ -124,21 +129,14 @@ public function collect( } } - /** Process shipping amount discount */ - $address->setShippingDiscountAmount(0); - $address->setBaseShippingDiscountAmount(0); - if ($address->getShippingAmount()) { - $this->calculator->processShippingAmount($address); - $total->addTotalAmount($this->getCode(), -$address->getShippingDiscountAmount()); - $total->addBaseTotalAmount($this->getCode(), -$address->getBaseShippingDiscountAmount()); - $total->setShippingDiscountAmount($address->getShippingDiscountAmount()); - $total->setBaseShippingDiscountAmount($address->getBaseShippingDiscountAmount()); - } - $this->calculator->prepareDescription($address); $total->setDiscountDescription($address->getDiscountDescription()); $total->setSubtotalWithDiscount($total->getSubtotal() + $total->getDiscountAmount()); $total->setBaseSubtotalWithDiscount($total->getBaseSubtotal() + $total->getBaseDiscountAmount()); + + $address->setDiscountAmount($total->getDiscountAmount()); + $address->setBaseDiscountAmount($total->getBaseDiscountAmount()); + return $this; } diff --git a/app/code/Magento/SalesRule/Model/ResourceModel/Coupon/Usage.php b/app/code/Magento/SalesRule/Model/ResourceModel/Coupon/Usage.php index 6407f04611a3..db32bdbe1e90 100644 --- a/app/code/Magento/SalesRule/Model/ResourceModel/Coupon/Usage.php +++ b/app/code/Magento/SalesRule/Model/ResourceModel/Coupon/Usage.php @@ -73,11 +73,11 @@ public function loadByCustomerCoupon(\Magento\Framework\DataObject $object, $cus $select = $connection->select()->from( $this->getMainTable() )->where( - 'customer_id =:customet_id' + 'customer_id =:customer_id' )->where( 'coupon_id = :coupon_id' ); - $data = $connection->fetchRow($select, [':coupon_id' => $couponId, ':customet_id' => $customerId]); + $data = $connection->fetchRow($select, [':coupon_id' => $couponId, ':customer_id' => $customerId]); if ($data) { $object->setData($data); } diff --git a/app/code/Magento/SalesRule/Model/ResourceModel/ReadHandler.php b/app/code/Magento/SalesRule/Model/ResourceModel/ReadHandler.php index 193a94910810..22d8b8446538 100644 --- a/app/code/Magento/SalesRule/Model/ResourceModel/ReadHandler.php +++ b/app/code/Magento/SalesRule/Model/ResourceModel/ReadHandler.php @@ -5,7 +5,6 @@ */ namespace Magento\SalesRule\Model\ResourceModel; -use Magento\SalesRule\Model\ResourceModel\Rule; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\EntityManager\Operation\AttributeInterface; @@ -37,11 +36,14 @@ public function __construct( } /** + * Read handler + * * @param string $entityType * @param array $entityData * @param array $arguments * @return array * @throws \Exception + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function execute($entityType, $entityData, $arguments = []) { diff --git a/app/code/Magento/SalesRule/Model/ResourceModel/Rule/Collection.php b/app/code/Magento/SalesRule/Model/ResourceModel/Rule/Collection.php index 59f24fa8b6e0..3e7b6ea28150 100644 --- a/app/code/Magento/SalesRule/Model/ResourceModel/Rule/Collection.php +++ b/app/code/Magento/SalesRule/Model/ResourceModel/Rule/Collection.php @@ -9,6 +9,9 @@ use Magento\Framework\DB\Select; use Magento\Framework\Serialize\Serializer\Json; use Magento\Quote\Model\Quote\Address; +use Magento\SalesRule\Api\Data\CouponInterface; +use Magento\SalesRule\Model\Coupon; +use Magento\SalesRule\Model\Rule; /** * Sales Rules resource collection model. @@ -80,6 +83,8 @@ protected function _construct() } /** + * Map data for associated entities + * * @param string $entityType * @param string $objectField * @throws \Magento\Framework\Exception\LocalizedException @@ -105,15 +110,20 @@ protected function mapAssociatedEntities($entityType, $objectField) $associatedEntities = $this->getConnection()->fetchAll($select); - array_map(function ($associatedEntity) use ($entityInfo, $ruleIdField, $objectField) { - $item = $this->getItemByColumnValue($ruleIdField, $associatedEntity[$ruleIdField]); - $itemAssociatedValue = $item->getData($objectField) === null ? [] : $item->getData($objectField); - $itemAssociatedValue[] = $associatedEntity[$entityInfo['entity_id_field']]; - $item->setData($objectField, $itemAssociatedValue); - }, $associatedEntities); + array_map( + function ($associatedEntity) use ($entityInfo, $ruleIdField, $objectField) { + $item = $this->getItemByColumnValue($ruleIdField, $associatedEntity[$ruleIdField]); + $itemAssociatedValue = $item->getData($objectField) ?? []; + $itemAssociatedValue[] = $associatedEntity[$entityInfo['entity_id_field']]; + $item->setData($objectField, $itemAssociatedValue); + }, + $associatedEntities + ); } /** + * Add website ids and customer group ids to rules data + * * @return $this * @throws \Exception * @since 100.1.0 @@ -137,6 +147,7 @@ protected function _afterLoad() * @param string $couponCode * @param string|null $now * @param Address $address allow extensions to further filter out rules based on quote address + * @throws \Zend_Db_Select_Exception * @use $this->addWebsiteGroupDateFilter() * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @return $this @@ -149,77 +160,24 @@ public function setValidationFilter( Address $address = null ) { if (!$this->getFlag('validation_filter')) { - /* We need to overwrite joinLeft if coupon is applied */ - $this->getSelect()->reset(); - parent::_initSelect(); + $this->prepareSelect($websiteId, $customerGroupId, $now); - $this->addWebsiteGroupDateFilter($websiteId, $customerGroupId, $now); - $select = $this->getSelect(); + $noCouponRules = $this->getNoCouponCodeSelect(); - $connection = $this->getConnection(); - if (strlen($couponCode)) { - $select->joinLeft( - ['rule_coupons' => $this->getTable('salesrule_coupon')], - $connection->quoteInto( - 'main_table.rule_id = rule_coupons.rule_id AND main_table.coupon_type != ?', - \Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON - ), - ['code'] - ); - - $noCouponWhereCondition = $connection->quoteInto( - 'main_table.coupon_type = ? ', - \Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON - ); - - $autoGeneratedCouponCondition = [ - $connection->quoteInto( - "main_table.coupon_type = ?", - \Magento\SalesRule\Model\Rule::COUPON_TYPE_AUTO - ), - $connection->quoteInto( - "rule_coupons.type = ?", - \Magento\SalesRule\Api\Data\CouponInterface::TYPE_GENERATED - ), - ]; - - $orWhereConditions = [ - "(" . implode($autoGeneratedCouponCondition, " AND ") . ")", - $connection->quoteInto( - '(main_table.coupon_type = ? AND main_table.use_auto_generation = 1 AND rule_coupons.type = 1)', - \Magento\SalesRule\Model\Rule::COUPON_TYPE_SPECIFIC - ), - $connection->quoteInto( - '(main_table.coupon_type = ? AND main_table.use_auto_generation = 0 AND rule_coupons.type = 0)', - \Magento\SalesRule\Model\Rule::COUPON_TYPE_SPECIFIC - ), - ]; - - $andWhereConditions = [ - $connection->quoteInto( - 'rule_coupons.code = ?', - $couponCode - ), - $connection->quoteInto( - '(rule_coupons.expiration_date IS NULL OR rule_coupons.expiration_date >= ?)', - $this->_date->date()->format('Y-m-d') - ), - ]; - - $orWhereCondition = implode(' OR ', $orWhereConditions); - $andWhereCondition = implode(' AND ', $andWhereConditions); - - $select->where( - $noCouponWhereCondition . ' OR ((' . $orWhereCondition . ') AND ' . $andWhereCondition . ')', - null, - Select::TYPE_CONDITION - ); + if ($couponCode) { + $couponRules = $this->getCouponCodeSelect($couponCode); + + $allAllowedRules = $this->getConnection()->select(); + $allAllowedRules->union([$noCouponRules, $couponRules], Select::SQL_UNION_ALL); + + $wrapper = $this->getConnection()->select(); + $wrapper->from($allAllowedRules); + + $this->_select = $wrapper; } else { - $this->addFieldToFilter( - 'main_table.coupon_type', - \Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON - ); + $this->_select = $noCouponRules; } + $this->setOrder('sort_order', self::SORT_ORDER_ASC); $this->setFlag('validation_filter', true); } @@ -227,6 +185,101 @@ public function setValidationFilter( return $this; } + /** + * Recreate the default select object for specific needs of salesrule evaluation with coupon codes. + * + * @param int $websiteId + * @param int $customerGroupId + * @param string $now + */ + private function prepareSelect($websiteId, $customerGroupId, $now) + { + $this->getSelect()->reset(); + parent::_initSelect(); + + $this->addWebsiteGroupDateFilter($websiteId, $customerGroupId, $now); + } + + /** + * Return select object to determine all active rules not needing a coupon code. + * + * @return Select + */ + private function getNoCouponCodeSelect() + { + $noCouponSelect = clone $this->getSelect(); + + $noCouponSelect->where( + 'main_table.coupon_type = ?', + Rule::COUPON_TYPE_NO_COUPON + ); + + $noCouponSelect->columns([Coupon::KEY_CODE => new \Zend_Db_Expr('NULL')]); + + return $noCouponSelect; + } + + /** + * Determine all active rules that are valid for the given coupon code. + * + * @param string $couponCode + * @return Select + */ + private function getCouponCodeSelect($couponCode) + { + $couponSelect = clone $this->getSelect(); + + $this->joinCouponTable($couponCode, $couponSelect); + + $notExpired = $this->getConnection()->quoteInto( + '(rule_coupons.expiration_date IS NULL OR rule_coupons.expiration_date >= ?)', + $this->_date->date()->format('Y-m-d') + ); + + $isAutogenerated = + $this->getConnection()->quoteInto('main_table.coupon_type = ?', Rule::COUPON_TYPE_AUTO) + . ' AND ' . + $this->getConnection()->quoteInto('rule_coupons.type = ?', CouponInterface::TYPE_GENERATED); + + $isValidSpecific = + $this->getConnection()->quoteInto('(main_table.coupon_type = ?)', Rule::COUPON_TYPE_SPECIFIC) + . ' AND (' . + '(main_table.use_auto_generation = 1 AND rule_coupons.type = 1)' + . ' OR ' . + '(main_table.use_auto_generation = 0 AND rule_coupons.type = 0)' + . ')'; + + $couponSelect->where( + "$notExpired AND ($isAutogenerated OR $isValidSpecific)", + null, + Select::TYPE_CONDITION + ); + + return $couponSelect; + } + + /** + * Join coupong table to select. + * + * @param string $couponCode + * @param Select $couponSelect + */ + private function joinCouponTable($couponCode, Select $couponSelect) + { + $couponJoinCondition = + 'main_table.rule_id = rule_coupons.rule_id' + . ' AND ' . + $this->getConnection()->quoteInto('main_table.coupon_type <> ?', Rule::COUPON_TYPE_NO_COUPON) + . ' AND ' . + $this->getConnection()->quoteInto('rule_coupons.code = ?', $couponCode); + + $couponSelect->joinInner( + ['rule_coupons' => $this->getTable('salesrule_coupon')], + $couponJoinCondition, + [Coupon::KEY_CODE] + ); + } + /** * Filter collection by website(s), customer group(s) and date. * Filter collection to only active rules. @@ -366,6 +419,8 @@ public function addCustomerGroupFilter($customerGroupId) } /** + * Getter for _associatedEntitiesMap property + * * @return array * @deprecated 100.1.0 */ @@ -380,6 +435,8 @@ private function getAssociatedEntitiesMap() } /** + * Getter for dateApplier property + * * @return DateApplier * @deprecated 100.1.0 */ diff --git a/app/code/Magento/SalesRule/Model/ResourceModel/SaveHandler.php b/app/code/Magento/SalesRule/Model/ResourceModel/SaveHandler.php index 685de1b581fb..d4ef15f1801b 100644 --- a/app/code/Magento/SalesRule/Model/ResourceModel/SaveHandler.php +++ b/app/code/Magento/SalesRule/Model/ResourceModel/SaveHandler.php @@ -5,7 +5,6 @@ */ namespace Magento\SalesRule\Model\ResourceModel; -use Magento\SalesRule\Model\ResourceModel\Rule; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\EntityManager\Operation\AttributeInterface; @@ -37,11 +36,14 @@ public function __construct( } /** + * Save handler + * * @param string $entityType * @param array $entityData * @param array $arguments * @return array * @throws \Exception + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function execute($entityType, $entityData, $arguments = []) { diff --git a/app/code/Magento/SalesRule/Model/Rule.php b/app/code/Magento/SalesRule/Model/Rule.php index 59efdf5eb3f6..a9836d2632aa 100644 --- a/app/code/Magento/SalesRule/Model/Rule.php +++ b/app/code/Magento/SalesRule/Model/Rule.php @@ -308,8 +308,7 @@ public function afterSave() } /** - * Initialize rule model data from array. - * Set store labels if applicable. + * Initialize rule model data from array. Set store labels if applicable. * * @param array $data * @return $this @@ -501,6 +500,8 @@ public function acquireCoupon($saveNewlyCreated = true, $saveAttemptCount = 10) $this->getUsesPerCustomer() ? $this->getUsesPerCustomer() : null )->setExpirationDate( $this->getToDate() + )->setType( + \Magento\SalesRule\Api\Data\CouponInterface::TYPE_GENERATED ); $couponCode = self::getCouponCodeGenerator()->generateCode(); @@ -539,6 +540,8 @@ public function acquireCoupon($saveNewlyCreated = true, $saveAttemptCount = 10) } /** + * Get from date. + * * @return string * @since 100.1.0 */ @@ -548,6 +551,8 @@ public function getFromDate() } /** + * Get to date. + * * @return string * @since 100.1.0 */ @@ -610,6 +615,8 @@ private function _getAddressId($address) } /** + * Get conditions field set id. + * * @param string $formName * @return string * @since 100.1.0 @@ -620,6 +627,8 @@ public function getConditionsFieldSetId($formName = '') } /** + * Get actions field set id. + * * @param string $formName * @return string * @since 100.1.0 diff --git a/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php b/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php index 3cd776fe99f5..2ae1c1c7ac63 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php +++ b/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php @@ -5,7 +5,6 @@ */ namespace Magento\SalesRule\Model\Rule\Action\Discount; -use Magento\Framework\App\ObjectManager; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\SalesRule\Model\DeltaPriceRound; use Magento\SalesRule\Model\Validator; @@ -50,6 +49,8 @@ public function __construct( } /** + * Fixed discount for cart calculation + * * @param \Magento\SalesRule\Model\Rule $rule * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item * @param float $qty diff --git a/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php b/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php index fd5953697c7d..cf6301cb31a9 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php +++ b/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php @@ -5,6 +5,9 @@ */ namespace Magento\SalesRule\Model\Rule\Condition; +/** + * Address rule condition data model. + */ class Address extends \Magento\Rule\Model\Condition\AbstractCondition { /** @@ -58,9 +61,11 @@ public function __construct( public function loadAttributeOptions() { $attributes = [ + 'base_subtotal_with_discount' => __('Subtotal (Excl. Tax)'), 'base_subtotal' => __('Subtotal'), 'total_qty' => __('Total Items Quantity'), 'weight' => __('Total Weight'), + 'payment_method' => __('Payment Method'), 'shipping_method' => __('Shipping Method'), 'postcode' => __('Shipping Postcode'), 'region' => __('Shipping Region'), diff --git a/app/code/Magento/SalesRule/Model/Rule/Condition/Product.php b/app/code/Magento/SalesRule/Model/Rule/Condition/Product.php index d2e7cabe473f..ff83bb1ee912 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Condition/Product.php +++ b/app/code/Magento/SalesRule/Model/Rule/Condition/Product.php @@ -25,13 +25,145 @@ protected function _addSpecialAttributes(array &$attributes) $attributes['quote_item_qty'] = __('Quantity in cart'); $attributes['quote_item_price'] = __('Price in cart'); $attributes['quote_item_row_total'] = __('Row total in cart'); + + $attributes['parent::category_ids'] = __('Category (Parent only)'); + $attributes['children::category_ids'] = __('Category (Children Only)'); + } + + /** + * Retrieve attribute + * + * @return string + */ + public function getAttribute(): string + { + $attribute = $this->getData('attribute'); + if (strpos($attribute, '::') !== false) { + list(, $attribute) = explode('::', $attribute); + } + + return $attribute; + } + + /** + * @inheritdoc + */ + public function getAttributeName() + { + $attribute = $this->getAttribute(); + if ($this->getAttributeScope()) { + $attribute = $this->getAttributeScope() . '::' . $attribute; + } + + return $this->getAttributeOption($attribute); + } + + /** + * @inheritdoc + */ + public function loadAttributeOptions() + { + $productAttributes = $this->_productResource->loadAllAttributes()->getAttributesByCode(); + + $attributes = []; + foreach ($productAttributes as $attribute) { + /* @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ + if (!$attribute->isAllowedForRuleCondition() + || !$attribute->getDataUsingMethod($this->_isUsedForRuleProperty) + ) { + continue; + } + $frontLabel = $attribute->getFrontendLabel(); + $attributes[$attribute->getAttributeCode()] = $frontLabel; + $attributes['parent::' . $attribute->getAttributeCode()] = $frontLabel . __('(Parent Only)'); + $attributes['children::' . $attribute->getAttributeCode()] = $frontLabel . __('(Children Only)'); + } + + $this->_addSpecialAttributes($attributes); + + asort($attributes); + $this->setAttributeOption($attributes); + + return $this; + } + + /** + * @inheritdoc + */ + public function getAttributeElementHtml() + { + $html = parent::getAttributeElementHtml() . + $this->getAttributeScopeElement()->getHtml(); + + return $html; + } + + /** + * Retrieve form element for scope element + * + * @return \Magento\Framework\Data\Form\Element\AbstractElement + */ + private function getAttributeScopeElement(): \Magento\Framework\Data\Form\Element\AbstractElement + { + return $this->getForm()->addField( + $this->getPrefix() . '__' . $this->getId() . '__attribute_scope', + 'hidden', + [ + 'name' => $this->elementName . '[' . $this->getPrefix() . '][' . $this->getId() . '][attribute_scope]', + 'value' => $this->getAttributeScope(), + 'no_span' => true, + 'class' => 'hidden', + 'data-form-part' => $this->getFormName(), + ] + ); + } + + /** + * Set attribute value + * + * @param string $value + * @return void + */ + public function setAttribute(string $value) + { + if (strpos($value, '::') !== false) { + list($scope, $attribute) = explode('::', $value); + $this->setData('attribute_scope', $scope); + $this->setData('attribute', $attribute); + } else { + $this->setData('attribute', $value); + } + } + + /** + * @inheritdoc + */ + public function loadArray($arr) + { + parent::loadArray($arr); + $this->setAttributeScope($arr['attribute_scope'] ?? null); + + return $this; + } + + /** + * @inheritdoc + */ + public function asArray(array $arrAttributes = []) + { + $out = parent::asArray($arrAttributes); + $out['attribute_scope'] = $this->getAttributeScope(); + + return $out; } /** * Validate Product Rule Condition * * @param \Magento\Framework\Model\AbstractModel $model + * * @return bool + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function validate(\Magento\Framework\Model\AbstractModel $model) { @@ -89,6 +221,8 @@ public function getValueElementChooserUrl() } /** + * Get locale-based formatted price. + * * @param string $value * @return float|null */ diff --git a/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Combine.php b/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Combine.php index b5ac02e67b1e..1649dea80ef5 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Combine.php +++ b/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Combine.php @@ -8,6 +8,7 @@ use Magento\Catalog\Model\ResourceModel\Product\Collection; /** + * Combine conditions for product. * @api * @since 100.0.2 */ @@ -85,4 +86,73 @@ public function collectValidatedAttributes($productCollection) } return $this; } + + /** + * @inheritdoc + */ + protected function _isValid($entity) + { + if (!$this->getConditions()) { + return true; + } + + $all = $this->getAggregator() === 'all'; + $true = (bool)$this->getValue(); + + foreach ($this->getConditions() as $cond) { + if ($entity instanceof \Magento\Framework\Model\AbstractModel) { + $validated = $this->validateEntity($cond, $entity); + } else { + $validated = $cond->validateByEntityId($entity); + } + if ($all && $validated !== $true) { + return false; + } elseif (!$all && $validated === $true) { + return true; + } + } + return $all ? true : false; + } + + /** + * Validate entity. + * + * @param object $cond + * @param \Magento\Framework\Model\AbstractModel $entity + * @return bool + */ + private function validateEntity($cond, \Magento\Framework\Model\AbstractModel $entity) + { + $true = (bool)$this->getValue(); + $validated = !$true; + foreach ($this->retrieveValidateEntities($cond->getAttributeScope(), $entity) as $validateEntity) { + $validated = $cond->validate($validateEntity); + if ($validated === $true) { + break; + } + } + + return $validated; + } + + /** + * Retrieve entities for validation by attribute scope + * + * @param string $attributeScope + * @param \Magento\Framework\Model\AbstractModel $entity + * @return \Magento\Framework\Model\AbstractModel[] + */ + private function retrieveValidateEntities($attributeScope, \Magento\Framework\Model\AbstractModel $entity) + { + if ($attributeScope === 'parent') { + $validateEntities = [$entity]; + } elseif ($attributeScope === 'children') { + $validateEntities = $entity->getChildren() ?: [$entity]; + } else { + $validateEntities = $entity->getChildren() ?: []; + $validateEntities[] = $entity; + } + + return $validateEntities; + } } diff --git a/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Subselect.php b/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Subselect.php index 1e8fbf43ec3b..5b02d3c08093 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Subselect.php +++ b/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Subselect.php @@ -5,6 +5,9 @@ */ namespace Magento\SalesRule\Model\Rule\Condition\Product; +/** + * Subselect conditions for product. + */ class Subselect extends \Magento\SalesRule\Model\Rule\Condition\Product\Combine { /** @@ -161,7 +164,9 @@ public function validate(\Magento\Framework\Model\AbstractModel $model) } } if ($hasValidChild || parent::validate($item)) { - $total += (($hasValidChild && $useChildrenTotal) ? $childrenAttrTotal : $item->getData($attr)); + $total += ($hasValidChild && $useChildrenTotal) + ? $childrenAttrTotal * $item->getQty() + : $item->getData($attr); } } return $this->validateAttribute($total); diff --git a/app/code/Magento/SalesRule/Model/Rule/DataProvider.php b/app/code/Magento/SalesRule/Model/Rule/DataProvider.php index 916825776373..25f0ef91eae6 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/Rule/Metadata/ValueProvider.php b/app/code/Magento/SalesRule/Model/Rule/Metadata/ValueProvider.php index b57daac1f0d5..fdd6c2b169a7 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Metadata/ValueProvider.php +++ b/app/code/Magento/SalesRule/Model/Rule/Metadata/ValueProvider.php @@ -5,7 +5,6 @@ */ namespace Magento\SalesRule\Model\Rule\Metadata; -use Magento\SalesRule\Model\ResourceModel\Rule\Collection; use Magento\SalesRule\Model\Rule; use Magento\Store\Model\System\Store; use Magento\Customer\Api\GroupRepositoryInterface; diff --git a/app/code/Magento/SalesRule/Model/RuleRepository.php b/app/code/Magento/SalesRule/Model/RuleRepository.php index a2279db85aa1..2cff0d64dba0 100644 --- a/app/code/Magento/SalesRule/Model/RuleRepository.php +++ b/app/code/Magento/SalesRule/Model/RuleRepository.php @@ -8,7 +8,6 @@ use Magento\Framework\Api\Search\FilterGroup; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; use Magento\Framework\Api\SearchCriteriaInterface; -use Magento\Framework\Api\SortOrder; use Magento\SalesRule\Model\ResourceModel\Rule\Collection; /** @@ -107,7 +106,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function save(\Magento\SalesRule\Api\Data\RuleInterface $rule) { @@ -119,7 +118,7 @@ public function save(\Magento\SalesRule\Api\Data\RuleInterface $rule) } /** - * {@inheritdoc} + * @inheritdoc */ public function getById($id) { @@ -136,7 +135,7 @@ public function getById($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function getList(SearchCriteriaInterface $searchCriteria) { @@ -183,13 +182,13 @@ public function deleteById($id) /** * Helper function that adds a FilterGroup to the collection. * - * @param \Magento\Framework\Api\Search\FilterGroup $filterGroup + * @param FilterGroup $filterGroup * @param Collection $collection * @deprecated 100.2.0 * @return void */ protected function addFilterGroupToCollection( - \Magento\Framework\Api\Search\FilterGroup $filterGroup, + FilterGroup $filterGroup, Collection $collection ) { $fields = []; diff --git a/app/code/Magento/SalesRule/Model/Validator.php b/app/code/Magento/SalesRule/Model/Validator.php index 5c0f97ae0b08..ea0221d8f072 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/Setup/Patch/Data/ConvertSerializedDataToJson.php b/app/code/Magento/SalesRule/Setup/Patch/Data/ConvertSerializedDataToJson.php index 8a8c51e9d349..e863f2af3354 100644 --- a/app/code/Magento/SalesRule/Setup/Patch/Data/ConvertSerializedDataToJson.php +++ b/app/code/Magento/SalesRule/Setup/Patch/Data/ConvertSerializedDataToJson.php @@ -6,12 +6,12 @@ namespace Magento\SalesRule\Setup\Patch\Data; -use Magento\Framework\App\ResourceConnection; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; /** * Class ConvertSerializedDataToJson + * * @package Magento\SalesRule\Setup\Patch */ class ConvertSerializedDataToJson implements DataPatchInterface, PatchVersionInterface @@ -59,7 +59,7 @@ public function apply() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -69,7 +69,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -77,7 +77,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/SalesRule/Setup/Patch/Data/FillSalesRuleProductAttributeTable.php b/app/code/Magento/SalesRule/Setup/Patch/Data/FillSalesRuleProductAttributeTable.php index 625d5769fddf..22cf4fe6b107 100644 --- a/app/code/Magento/SalesRule/Setup/Patch/Data/FillSalesRuleProductAttributeTable.php +++ b/app/code/Magento/SalesRule/Setup/Patch/Data/FillSalesRuleProductAttributeTable.php @@ -6,13 +6,13 @@ namespace Magento\SalesRule\Setup\Patch\Data; -use Magento\Framework\App\ResourceConnection; use Magento\Framework\App\State; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; /** * Class FillSalesRuleProductAttributeTable + * * @package Magento\SalesRule\Setup\Patch */ class FillSalesRuleProductAttributeTable implements DataPatchInterface, PatchVersionInterface @@ -65,7 +65,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { @@ -102,7 +102,7 @@ public function fillSalesRuleProductAttributeTable() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -112,7 +112,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -120,7 +120,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/SalesRule/Setup/Patch/Data/PrepareRuleModelSerializedData.php b/app/code/Magento/SalesRule/Setup/Patch/Data/PrepareRuleModelSerializedData.php index 2387f5f1ed71..cad83b6f5d1d 100644 --- a/app/code/Magento/SalesRule/Setup/Patch/Data/PrepareRuleModelSerializedData.php +++ b/app/code/Magento/SalesRule/Setup/Patch/Data/PrepareRuleModelSerializedData.php @@ -7,33 +7,33 @@ namespace Magento\SalesRule\Setup\Patch\Data; use Magento\Framework\Setup\ModuleDataSetupInterface; -use Magento\Framework\App\ResourceConnection; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; /** * Class PrepareRuleModelSerializedData + * * @package Magento\SalesRule\Setup\Patch */ class PrepareRuleModelSerializedData implements DataPatchInterface, PatchVersionInterface { /** - * @var \Magento\Framework\Setup\ModuleDataSetupInterface + * @var ModuleDataSetupInterface */ private $moduleDataSetup; /** * PatchInitial constructor. - * @param \Magento\Framework\Setup\ModuleDataSetupInterface $moduleDataSetup + * @param ModuleDataSetupInterface $moduleDataSetup */ public function __construct( - \Magento\Framework\Setup\ModuleDataSetupInterface $moduleDataSetup + ModuleDataSetupInterface $moduleDataSetup ) { $this->moduleDataSetup = $moduleDataSetup; } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { @@ -59,7 +59,7 @@ public function apply() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -67,7 +67,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -75,7 +75,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleActionGroup.xml index bae706985993..210259f474ee 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleActionGroup.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleActionGroup.xml @@ -11,4 +11,54 @@ <!-- This actionGroup was created to be merged from B2B because B2B has a very different form control here --> <selectOption selector="{{AdminCartPriceRulesFormSection.customerGroups}}" userInput="NOT LOGGED IN" stepKey="selectCustomerGroup"/> </actionGroup> + + <actionGroup name="selectRetailerCustomerGroup"> + <!-- This actionGroup was created to be merged from B2B. Retailer Customer Group --> + <selectOption selector="{{AdminCartPriceRulesFormSection.customerGroups}}" userInput="Retailer" stepKey="selectRetailerCustomerGroup"/> + </actionGroup> + + <!--Set Subtotal condition for Customer Segment--> + <actionGroup name="SetCartAttributeConditionForCartPriceRuleActionGroup"> + <arguments> + <argument name="attributeName" type="string"/> + <argument name="operatorType" defaultValue="is" type="string"/> + <argument name="value" type="string"/> + </arguments> + <scrollTo selector="{{AdminCartPriceRulesFormSection.conditionsHeader}}" stepKey="scrollToActionTab"/> + <conditionalClick selector="{{AdminCartPriceRulesFormSection.conditionsHeader}}" dependentSelector="{{AdminCartPriceRulesFormSection.conditionsHeaderOpen}}" + visible="false" stepKey="openActionTab"/> + <click selector="{{AdminCartPriceRulesFormSection.conditions}}" stepKey="applyRuleForConditions"/> + <waitForPageLoad stepKey="waitForDropDownOpened"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.childAttribute}}" userInput="{{attributeName}}" stepKey="selectAttribute"/> + <waitForPageLoad stepKey="waitForOperatorOpened"/> + <click selector="{{AdminCartPriceRulesFormSection.condition('is')}}" stepKey="clickToChooseOption"/> + <selectOption userInput="{{operatorType}}" selector="{{AdminCartPriceRulesFormSection.conditionsOperator}}" stepKey="setOperatorType"/> + <click selector="{{AdminCartPriceRulesFormSection.condition('...')}}" stepKey="clickToChooseOption1"/> + <fillField userInput="{{value}}" selector="{{AdminCartPriceRulesFormSection.conditionsValue}}" stepKey="fillActionValue"/> + <click selector="{{AdminMainActionsSection.saveAndContinue}}" stepKey="clickSaveButton"/> + <see selector="{{AdminCartPriceRulesSection.messages}}" userInput="You saved the rule." stepKey="seeSuccessMessage"/> + </actionGroup> + + <actionGroup name="SetConditionForActionsInCartPriceRuleActionGroup"> + <arguments> + <argument name="actionsAggregator" type="string" defaultValue="ANY"/> + <argument name="actionsValue" type="string" defaultValue="FALSE"/> + <argument name="childAttribute" type="string" defaultValue="Category"/> + <argument name="actionValue" type="string"/> + </arguments> + <click selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="clickOnActionTab"/> + <click selector="{{AdminCartPriceRulesFormSection.condition('ALL')}}" stepKey="clickToChooseOption"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.actionsAggregator}}" userInput="{{actionsAggregator}}" stepKey="selectCondition"/> + <click selector="{{AdminCartPriceRulesFormSection.condition('TRUE')}}" stepKey="clickToChooseOption2"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.actionsValue}}" userInput="{{actionsValue}}" stepKey="selectCondition2"/> + <click selector="{{AdminCartPriceRulesFormSection.conditions}}" stepKey="selectActionConditions"/> + <waitForPageLoad stepKey="waitForDropDownOpened"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.childAttribute}}" userInput="{{childAttribute}}" stepKey="selectAttribute"/> + <waitForPageLoad stepKey="waitForOperatorOpened"/> + <click selector="{{AdminCartPriceRulesFormSection.condition('...')}}" stepKey="clickToChooseOption3"/> + <fillField selector="{{AdminCartPriceRulesFormSection.actionValue}}" userInput="{{actionValue}}" stepKey="fillActionValue"/> + <click selector="{{AdminCartPriceRulesFormSection.applyAction}}" stepKey="applyAction"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveButton"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the rule." stepKey="seeSuccessMessage"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCreateCartPriceRuleActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCreateCartPriceRuleActionGroup.xml new file mode 100644 index 000000000000..cc165e0b5dc9 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCreateCartPriceRuleActionGroup.xml @@ -0,0 +1,94 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See 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="AdminCreateCartPriceRuleActionGroup"> + <arguments> + <argument name="ruleName"/> + </arguments> + <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> + <waitForPageLoad stepKey="waitForPriceList"/> + <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> + <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{ruleName.name}}" stepKey="fillRuleName"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="{{ruleName.websites}}" stepKey="selectWebsites"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.customerGroups}}" parameterArray="[{{ruleName.customerGroups}}]" stepKey="selectCustomerGroup"/> + <click selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="clickToExpandActions"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.apply}}" userInput="{{ruleName.apply}}" stepKey="selectActionType"/> + <fillField selector="{{AdminCartPriceRulesFormSection.discountAmount}}" userInput="{{ruleName.discountAmount}}" stepKey="fillDiscountAmount"/> + <click selector="{{AdminCartPriceRulesFormSection.save}}" stepKey="clickSaveButton"/> + <see selector="{{AdminCartPriceRulesFormSection.successMessage}}" userInput="You saved the rule." stepKey="seeSuccessMessage"/> + </actionGroup> + + <actionGroup name="AdminCreateCartPriceRuleWithCouponCode" extends="AdminCreateCartPriceRuleActionGroup"> + <arguments> + <argument name="couponCode" defaultValue="_defaultCoupon.code"/> + </arguments> + <remove keyForRemoval="selectActionType"/> + <remove keyForRemoval="fillDiscountAmount"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.coupon}}" userInput="Specific Coupon" stepKey="selectCouponType" after="fillRuleName"/> + <waitForElementVisible selector="{{AdminCartPriceRulesFormSection.couponCode}}" stepKey="waitForElementVisible" after="selectCouponType"/> + <fillField selector="{{AdminCartPriceRulesFormSection.couponCode}}" userInput="{{couponCode}}" stepKey="fillCouponCode" after="waitForElementVisible"/> + <fillField selector="{{AdminCartPriceRulesFormSection.userPerCoupon}}" userInput="99" stepKey="fillUserPerCoupon" after="fillCouponCode"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.apply}}" userInput="Fixed amount discount for whole cart" stepKey="selectActionTypeToFixed" after="clickToExpandActions"/> + <fillField selector="{{AdminCartPriceRulesFormSection.discountAmount}}" userInput="1" stepKey="fillDiscountAmount" after="selectActionTypeToFixed"/> + </actionGroup> + + <!--Delete Cart price Rule for Retailer customer--> + <actionGroup name="AdminDeleteCartPriceRuleForRetailerActionGroup"> + <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="goToCartPriceRules"/> + <waitForPageLoad stepKey="waitForCartPriceRules"/> + <fillField selector="{{AdminCartPriceRulesSection.filterByNameInput}}" userInput="{{SimpleSalesRule.name}}" stepKey="filterByName"/> + <click selector="{{AdminCartPriceRulesSection.searchButton}}" stepKey="doFilter"/> + <click selector="{{AdminCartPriceRulesSection.rowByIndex('1')}}" stepKey="goToEditRulePage"/> + <click selector="{{AdminCartPriceRulesFormSection.delete}}" stepKey="clickDeleteButton"/> + <click selector="{{AdminCartPriceRulesFormSection.modalAcceptButton}}" stepKey="confirmDelete"/> + </actionGroup> + + <actionGroup name="AdminCreateCartPriceRuleWithConditions" extends="AdminCreateCartPriceRuleActionGroup"> + <arguments> + <argument name="condition1" type="string" defaultValue="Products subselection" /> + <argument name="condition2" type="string" defaultValue="Category" /> + <argument name="ruleToChange1" type="string" defaultValue="is" /> + <argument name="rule1" type="string" defaultValue="equals or greater than" /> + <argument name="ruleToChange2" type="string" defaultValue="..." /> + <argument name="rule2" type="string" defaultValue="2" /> + <argument name="categoryName" type="string" defaultValue="_defaultCategory.name" /> + </arguments> + <remove keyForRemoval="fillDiscountAmount" /> + <!--Go to Conditions section--> + <click selector="{{AdminCartPriceRulesFormSection.conditionsHeader}}" stepKey="openConditionsSection" after="selectActionType" /> + <click selector="{{AdminCartPriceRulesFormSection.addCondition('1')}}" stepKey="addFirstCondition" after="openConditionsSection" /> + <selectOption selector="{{AdminCartPriceRulesFormSection.ruleCondition('1')}}" userInput="{{condition1}}" stepKey="selectRule" after="addFirstCondition" /> + <waitForElementVisible selector="{{AdminCartPriceRulesFormSection.ruleParameter(ruleToChange1)}}" stepKey="waitForFirstRuleElement" after="selectRule" /> + <click selector="{{AdminCartPriceRulesFormSection.ruleParameter(ruleToChange1)}}" stepKey="clickToChangeRule" after="waitForFirstRuleElement" /> + <selectOption selector="{{AdminCartPriceRulesFormSection.ruleParameterSelect('1--1')}}" userInput="{{rule1}}" stepKey="selectRule1" after="clickToChangeRule" /> + <waitForElementVisible selector="{{AdminCartPriceRulesFormSection.ruleParameter(ruleToChange2)}}" stepKey="waitForSecondRuleElement" after="selectRule1" /> + <click selector="{{AdminCartPriceRulesFormSection.ruleParameter(ruleToChange2)}}" stepKey="clickToChangeRule1" after="waitForSecondRuleElement" /> + <fillField selector="{{AdminCartPriceRulesFormSection.ruleParameterInput('1--1')}}" userInput="{{rule2}}" stepKey="fillRule" after="clickToChangeRule1" /> + <click selector="{{AdminCartPriceRulesFormSection.addCondition('1--1')}}" stepKey="addSecondCondition" after="fillRule" /> + <selectOption selector="{{AdminCartPriceRulesFormSection.ruleCondition('1--1')}}" userInput="{{condition2}}" stepKey="selectSecondCondition" after="addSecondCondition" /> + <waitForElementVisible selector="{{AdminCartPriceRulesFormSection.ruleParameter(ruleToChange2)}}" stepKey="waitForThirdRuleElement" after="selectSecondCondition" /> + <click selector="{{AdminCartPriceRulesFormSection.ruleParameter(ruleToChange2)}}" stepKey="addThirdCondition" after="waitForThirdRuleElement" /> + <waitForElementVisible selector="{{AdminCartPriceRulesFormSection.openChooser('1--1--1')}}" stepKey="waitForForthRuleElement" after="addThirdCondition" /> + <click selector="{{AdminCartPriceRulesFormSection.openChooser('1--1--1')}}" stepKey="openChooser" after="waitForForthRuleElement" /> + <waitForElementVisible selector="{{AdminCartPriceRulesFormSection.categoryCheckbox(categoryName)}}" stepKey="waitForCategoryVisible" after="openChooser" /> + <checkOption selector="{{AdminCartPriceRulesFormSection.categoryCheckbox(categoryName)}}" stepKey="checkCategoryName" after="waitForCategoryVisible" /> + </actionGroup> + + <actionGroup name="CreateCartPriceRuleSecondWebsiteActionGroup"> + <arguments> + <argument name="ruleName"/> + </arguments> + <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> + <waitForPageLoad stepKey="waitForPriceList"/> + <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> + <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{ruleName.name}}" stepKey="fillRuleName"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Second Website" stepKey="selectWebsites"/> + + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminFilterCartPriceRuleActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminFilterCartPriceRuleActionGroup.xml new file mode 100644 index 000000000000..35e1bee0952c --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminFilterCartPriceRuleActionGroup.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"> + <!--Search grid with keyword search--> + <actionGroup name="AdminFilterCartPriceRuleActionGroup"> + <arguments> + <argument name="ruleName"/> + </arguments> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <fillField selector="{{AdminCartPriceRulesSection.filterByNameInput}}" userInput="{{ruleName}}" stepKey="filterByName"/> + <click selector="{{AdminCartPriceRulesSection.searchButton}}" stepKey="doFilter"/> + <click selector="{{AdminCartPriceRulesSection.rowByIndex('1')}}" stepKey="goToEditRulePage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminOpenNewCartPriceRuleFormPageActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminOpenNewCartPriceRuleFormPageActionGroup.xml new file mode 100644 index 000000000000..8cb958c9d930 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminOpenNewCartPriceRuleFormPageActionGroup.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenNewCartPriceRuleFormPageActionGroup"> + <amOnPage url="{{PriceRuleNewPage.url}}" stepKey="openNewCartPriceRulePage" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/ApplyCartRuleOnStorefrontActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/ApplyCartRuleOnStorefrontActionGroup.xml index 55b2e9c10fd6..37e171823b11 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/ApplyCartRuleOnStorefrontActionGroup.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/ApplyCartRuleOnStorefrontActionGroup.xml @@ -9,20 +9,16 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="ApplyCartRuleOnStorefrontActionGroup"> <arguments> - <argument name="Product" defaultValue="_defaultProduct"/> - <argument name="Coupon" defaultValue="SimpleSalesRuleCoupon"/> + <argument name="product"/> + <argument name="couponCode" type="string"/> </arguments> - <amOnPage url="{{Product.name}}.html" stepKey="navigateToProductPage"/> - <waitForPageLoad stepKey="waitForPageLoad1"/> <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addToCart" /> - <waitForText userInput="You added {{Product.name}} to your shopping cart." stepKey="waitForAddedBtn"/> - <amOnPage url="/checkout/cart/" stepKey="onPageShoppingCart"/> - <waitForPageLoad stepKey="waitForPageLoad2"/> - <click selector="{{DiscountSection.DiscountTab}}" stepKey="scrollToDiscountTab" /> - <fillField selector="{{DiscountSection.CouponInput}}" userInput="{{Coupon.code}}" stepKey="fillCouponCode" /> + <waitForText userInput="You added {{product.name}} to your shopping cart." stepKey="waitForText"/> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCheckoutPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <click selector="{{DiscountSection.DiscountTab}}" stepKey="clickToDiscountTab" /> + <fillField selector="{{DiscountSection.CouponInput}}" userInput="{{couponCode}}" stepKey="fillCouponCode"/> <click selector="{{DiscountSection.ApplyCodeBtn}}" stepKey="applyCode"/> - <waitForPageLoad stepKey="waitForPageLoad3"/> - <waitForText userInput="You used coupon code" stepKey="waitForText"/> - <see userInput="You used coupon code" stepKey="assertText"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AssertCustomerGroupNotOnCartPriceRuleFormActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AssertCustomerGroupNotOnCartPriceRuleFormActionGroup.xml new file mode 100644 index 000000000000..98a094aea77a --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AssertCustomerGroupNotOnCartPriceRuleFormActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertCustomerGroupNotOnCartPriceRuleFormActionGroup"> + <arguments> + <argument name="customerGroup" type="entity" /> + </arguments> + <grabMultiple selector="{{AdminCartPriceRulesFormSection.customerGroupsOptions}}" stepKey="customerGroups" /> + <assertNotContains stepKey="assertCustomerGroupNotInOptions"> + <actualResult type="variable">customerGroups</actualResult> + <expectedResult type="string">{{customerGroup.code}}</expectedResult> + </assertNotContains> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/StorefrontSalesRuleActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/StorefrontSalesRuleActionGroup.xml index 70d1fc56d2ce..3e55eb4f2660 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/StorefrontSalesRuleActionGroup.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/StorefrontSalesRuleActionGroup.xml @@ -18,6 +18,7 @@ <waitForElementVisible selector="{{StorefrontSalesRuleCartCouponSection.couponField}}" stepKey="waitForCouponField" /> <fillField userInput="{{coupon.code}}" selector="{{StorefrontSalesRuleCartCouponSection.couponField}}" stepKey="fillCouponField"/> <click selector="{{StorefrontSalesRuleCartCouponSection.applyButton}}" stepKey="clickApplyButton"/> + <waitForPageLoad stepKey="waitForPageLoad"/> </actionGroup> <!-- Cancel Sales Rule Coupon applied to the cart --> @@ -31,12 +32,12 @@ <!-- Check applied discount in cart summary --> <actionGroup name="StorefrontCheckCouponAppliedActionGroup"> <arguments> - <argument name="rule"/> - <argument name="discount"/> + <argument name="rule" /> + <argument name="discount" type="string" /> </arguments> - <waitForElementVisible selector="{{CheckoutCartSummarySection.discountTotal}}" stepKey="waitForDiscountTotal" /> - <see userInput="{{rule.store_labels[1][store_label]}}" selector="{{CheckoutCartSummarySection.discountLabel}}" stepKey="assertDiscountLabel" /> - <see userInput="-${{discount}}" selector="{{CheckoutCartSummarySection.discountTotal}}" stepKey="assertDiscountTotal" /> + <waitForElementVisible selector="{{CheckoutCartSummarySection.discountTotal}}" stepKey="waitForDiscountTotal"/> + <see userInput="{{rule.store_labels[1][store_label]}}" selector="{{CheckoutCartSummarySection.discountLabel}}" stepKey="assertDiscountLabel"/> + <see userInput="-${{discount}}" selector="{{CheckoutCartSummarySection.discountTotal}}" stepKey="assertDiscountTotal"/> </actionGroup> <actionGroup name="VerifyDiscountAmount"> @@ -55,4 +56,4 @@ <waitForElementVisible selector="{{CheckoutCartSummarySection.discountAmount}}" stepKey="waitForDiscountElement"/> <see selector="{{CheckoutCartSummarySection.discountAmount}}" userInput="{{expectedDiscount}}" stepKey="seeDiscountTotal"/> </actionGroup> -</actionGroups> \ No newline at end of file +</actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/SalesRule/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 000000000000..5a42980df1bb --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminMenuMarketingPromotionsCartPriceRules"> + <data key="pageTitle">Cart Price Rules</data> + <data key="title">Cart Price Rules</data> + <data key="dataUiId">magento-salesrule-promo-quote</data> + </entity> +</entities> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleAddressConditionsData.xml b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleAddressConditionsData.xml new file mode 100644 index 000000000000..cc695b347c4f --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleAddressConditionsData.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="SalesRuleAddressConditions" type="SalesRuleConditionAttribute"> + <data key="subtotal">Magento\SalesRule\Model\Rule\Condition\Address|base_subtotal</data> + <data key="totalItemsQty">Magento\SalesRule\Model\Rule\Condition\Address|total_qty</data> + <data key="totalWeight">Magento\SalesRule\Model\Rule\Condition\Address|weight</data> + <data key="shippingMethod">Magento\SalesRule\Model\Rule\Condition\Address|shipping_method</data> + <data key="shippingPostCode">Magento\SalesRule\Model\Rule\Condition\Address|postcode</data> + <data key="shippingRegion">Magento\SalesRule\Model\Rule\Condition\Address|region</data> + <data key="shippingState">Magento\SalesRule\Model\Rule\Condition\Address|region_id</data> + <data key="shippingCountry">Magento\SalesRule\Model\Rule\Condition\Address|country_id</data> + </entity> +</entities> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleCouponData.xml b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleCouponData.xml index 10b198b53f38..3dd87d94d014 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleCouponData.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleCouponData.xml @@ -11,5 +11,6 @@ <var key="rule_id" entityKey="rule_id" entityType="SalesRule"/> <data key="code" unique="suffix">Code</data> <data key="is_primary">1</data> + <data key="times_used">0</data> </entity> </entities> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml index 5b7585b8b2a3..8f6e63534b0c 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml @@ -39,6 +39,35 @@ <requiredEntity type="SalesRuleLabel">SalesRuleLabelStore1</requiredEntity> </entity> + <entity name="ApiCartRule" type="SalesRule"> + <data key="name" unique="suffix">salesRule</data> + <data key="description">Sales Rule Descritpion</data> + <array key="website_ids"> + <item>1</item> + </array> + <array key="customer_group_ids"> + <item>0</item> + <item>1</item> + <item>3</item> + </array> + <data key="uses_per_customer">0</data> + <data key="is_active">true</data> + <data key="stop_rules_processing">true</data> + <data key="is_advanced">true</data> + <data key="sort_order">0</data> + <data key="simple_action">by_percent</data> + <data key="discount_amount">50</data> + <data key="discount_qty">0</data> + <data key="discount_step">0</data> + <data key="apply_to_shipping">false</data> + <data key="times_used">0</data> + <data key="is_rss">true</data> + <data key="coupon_type">NO_COUPON</data> + <data key="use_auto_generation">false</data> + <data key="uses_per_coupon">0</data> + <data key="simple_free_shipping">0</data> + </entity> + <entity name="SimpleSalesRule" type="SalesRule"> <data key="name" unique="suffix">SimpleSalesRule</data> <data key="is_active">true</data> @@ -53,4 +82,115 @@ <item>1</item> </array> </entity> + <entity name="TestSalesRule" type="SalesRule"> + <data key="name" unique="suffix">TestSalesRule</data> + <data key="websites">Main Website</data> + <data key="customerGroups">'NOT LOGGED IN', 'General', 'Wholesale', 'Retailer'</data> + <data key="apply">Percent of product price discount</data> + <data key="discountAmount">50</data> + </entity> + <entity name="CatPriceRule" type="SalesRule"> + <data key="name" unique="suffix">CartPriceRule</data> + <data key="websites">Main Website</data> + <data key="customerGroups">'NOT LOGGED IN', 'General', 'Wholesale', 'Retailer'</data> + <data key="coupon_type">Specific Coupon</data> + <data key="coupon_code" unique="suffix">CouponCode</data> + <data key="apply">Percent of product price discount</data> + <data key="discountAmount">10</data> + </entity> + + <entity name="SalesRuleSpecificCoupon" type="SalesRule"> + <data key="name" unique="suffix">SimpleSalesRule</data> + <data key="description">Sales Rule Description</data> + <array key="website_ids"> + <item>1</item> + </array> + <array key="customer_group_ids"> + <item>0</item> + <item>1</item> + <item>3</item> + </array> + <data key="uses_per_customer">1</data> + <data key="is_active">true</data> + <data key="stop_rules_processing">false</data> + <data key="is_advanced">true</data> + <data key="sort_order">2</data> + <data key="simple_action">by_percent</data> + <data key="discount_amount">10</data> + <data key="discount_qty">1</data> + <data key="discount_step">0</data> + <data key="apply_to_shipping">false</data> + <data key="times_used">1</data> + <data key="is_rss">false</data> + <data key="coupon_type">SPECIFIC_COUPON</data> + <data key="use_auto_generation">true</data> + <data key="uses_per_coupon">2</data> + <data key="simple_free_shipping">1</data> + </entity> + + <entity name="SalesRuleSpecificCouponWithFixedDiscount" type="SalesRule"> + <data key="name" unique="suffix">SimpleSalesRule</data> + <data key="description">Sales Rule Description</data> + <array key="website_ids"> + <item>1</item> + </array> + <array key="customer_group_ids"> + <item>1</item> + </array> + <data key="uses_per_customer">10</data> + <data key="is_active">true</data> + <data key="stop_rules_processing">false</data> + <data key="is_advanced">true</data> + <data key="sort_order">1</data> + <data key="simple_action">cart_fixed</data> + <data key="discount_amount">10</data> + <data key="discount_qty">10</data> + <data key="discount_step">0</data> + <data key="apply_to_shipping">false</data> + <data key="times_used">0</data> + <data key="is_rss">false</data> + <data key="coupon_type">SPECIFIC_COUPON</data> + <data key="use_auto_generation">false</data> + <data key="uses_per_coupon">10</data> + <data key="simple_free_shipping">1</data> + </entity> + + <entity name="PriceRuleWithCondition" type="SalesRule"> + <data key="name" unique="suffix">SalesRule</data> + <data key="websites">Main Website</data> + <data key="customerGroups">'NOT LOGGED IN', 'General', 'Wholesale', 'Retailer'</data> + <data key="apply">Fixed amount discount for whole cart</data> + <data key="discountAmount">50</data> + </entity> + + <entity name="SalesRuleSpecificCouponWithPercentDiscount" type="SalesRule"> + <data key="name" unique="suffix">SimpleSalesRule</data> + <data key="description">Sales Rule Description</data> + <array key="website_ids"> + <item>1</item> + </array> + <array key="customer_group_ids"> + <item>1</item> + </array> + <data key="uses_per_customer">10</data> + <data key="is_active">true</data> + <data key="stop_rules_processing">false</data> + <data key="is_advanced">true</data> + <data key="sort_order">1</data> + <data key="simple_action">by_percent</data> + <data key="discount_amount">10</data> + <data key="discount_qty">10</data> + <data key="discount_step">0</data> + <data key="apply_to_shipping">false</data> + <data key="times_used">0</data> + <data key="is_rss">false</data> + <data key="coupon_type">SPECIFIC_COUPON</data> + <data key="use_auto_generation">false</data> + <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/Data/SalesRuleProductConditionsData.xml b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleProductConditionsData.xml new file mode 100644 index 000000000000..8af7ac0fdd99 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleProductConditionsData.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="SalesRuleProductConditions" type="SalesRuleConditionAttribute"> + <data key="priceInCart" >Magento\SalesRule\Model\Rule\Condition\Product|quote_item_price</data> + <data key="quantityInCart">Magento\SalesRule\Model\Rule\Condition\Product|quote_item_qty</data> + <data key="rowTotalInCart">Magento\SalesRule\Model\Rule\Condition\Product|quote_item_row_total</data> + </entity> +</entities> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Page/AdminCartPriceRuleEditPage.xml b/app/code/Magento/SalesRule/Test/Mftf/Page/AdminCartPriceRuleEditPage.xml new file mode 100644 index 000000000000..faed9d42bcde --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Page/AdminCartPriceRuleEditPage.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminCartPriceRuleEditPage" area="admin" url="sales_rule/promo_quote/edit/id/{{salesRuleId}}" module="Magento_SalesRule" parameterized="true"> + <section name="AdminCartPriceRulesFormSection"/> + </page> +</pages> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml index fcacb3a3a37b..8af28a7fcb33 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml @@ -18,17 +18,50 @@ <element name="ruleName" type="input" selector="input[name='name']"/> <element name="websites" type="multiselect" selector="select[name='website_ids']"/> <element name="customerGroups" type="multiselect" selector="select[name='customer_group_ids']"/> + <element name="customerGroupsOptions" type="multiselect" selector="select[name='customer_group_ids'] option"/> <element name="coupon" type="select" selector="select[name='coupon_type']"/> <element name="couponCode" type="input" selector="input[name='coupon_code']"/> <element name="useAutoGeneration" type="checkbox" selector="input[name='use_auto_generation']"/> + <element name="fromDate" type="input" selector="input[name='from_date']"/> + <element name="toDate" type="input" selector="input[name='to_date']"/> + <element name="userPerCoupon" type="input" selector="//input[@name='uses_per_coupon']"/> + <element name="userPerCustomer" type="input" selector="//input[@name='uses_per_customer']"/> + <element name="priority" type="input" selector="//*[@name='sort_order']"/> + + <!-- Conditions sub-form --> + <element name="conditionsHeader" type="button" selector="div[data-index='conditions']" timeout="30"/> + <element name="conditionsHeaderOpen" type="button" selector="div[data-index='conditions'] div[data-state-collapsible='open']" timeout="30"/> + <element name="conditionsValue" type="input" selector=".rule-param-edit input"/> + <element name="conditionsOperator" type="select" selector=".rule-param-edit select"/> + <element name="addCondition" type="button" selector="//*[@id='conditions__{{arg}}__children']//span" parameterized="true"/> + <element name="ruleCondition" type="select" selector="rule[conditions][{{arg}}][new_child]" parameterized="true"/> + <element name="ruleParameter" type="text" selector="//span[@class='rule-param']/a[contains(text(), '{{arg}}')]" parameterized="true"/> + <element name="ruleParameterSelect" type="select" selector="rule[conditions][{{arg}}][operator]" parameterized="true"/> + <element name="ruleParameterInput" type="input" selector="rule[conditions][{{arg}}][value]" parameterized="true"/> + <element name="openChooser" type="button" selector="//label[@for='conditions__{{arg}}__value']" parameterized="true"/> + <element name="categoryCheckbox" type="checkbox" selector="//span[contains(text(), '{{arg}}')]/parent::a/preceding-sibling::input[@type='checkbox']" parameterized="true"/> <!-- Actions sub-form --> + <element name="actionsTab" type="text" selector="//div[@data-index='actions']//span[contains(.,'Actions')][1]"/> <element name="actionsHeader" type="button" selector="div[data-index='actions']" timeout="30"/> + <element name="actionsHeaderOpen" type="button" selector="div[data-index='actions'] div[data-state-collapsible='open']" timeout="30"/> <element name="apply" type="select" selector="select[name='simple_action']"/> + <element name="conditions" type="button" selector=".rule-param.rule-param-new-child > a"/> + <element name="childAttribute" type="select" selector="//select[contains(@name, 'new_child')]"/> + <element name="condition" type="text" selector="//span[@class='rule-param']/a[text()='{{arg}}']" parameterized="true"/> + <element name="actionsAggregator" type="select" selector="#actions__1__aggregator"/> + <element name="actionsValue" type="select" selector="#actions__1__value"/> + <element name="operator" type="select" selector="//select[contains(@name, '[operator]')]"/> + <element name="option" type="select" selector="//ul[@class='rule-param-children']//select[contains(@name, '[value]')]"/> + <element name="actionValue" type="input" selector=".rule-param-edit input"/> + <element name="applyAction" type="text" selector=".rule-param-apply" timeout="30"/> + <element name="actionOperator" type="select" selector=".rule-param-edit select"/> <element name="applyDiscountToShipping" type="checkbox" selector="input[name='apply_to_shipping']"/> <element name="applyDiscountToShippingLabel" type="checkbox" selector="input[name='apply_to_shipping']+label"/> <element name="discountAmount" type="input" selector="input[name='discount_amount']"/> <element name="discountStep" type="input" selector="input[name='discount_step']"/> + <element name="addRewardPoints" type="input" selector="input[name='extension_attributes[reward_points_delta]']"/> + <element name="freeShipping" type="select" selector="//select[@name='simple_free_shipping']"/> <!-- Manage Coupon Codes sub-form --> <element name="manageCouponCodesHeader" type="button" selector="div[data-index='manage_coupon_codes']" timeout="30"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesSection.xml b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesSection.xml index a32c50d9d861..14d3a734408d 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesSection.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesSection.xml @@ -15,5 +15,8 @@ <element name="nameColumns" type="text" selector="td[data-column='name']"/> <element name="rowContainingText" type="text" selector="//*[@id='promo_quote_grid_table']/tbody/tr[td//text()[contains(., '{{var1}}')]]" parameterized="true" timeout="30"/> <element name="rowByIndex" type="text" selector="tr[data-role='row']:nth-of-type({{var1}})" parameterized="true" timeout="30"/> + <element name="rulesRow" type="text" selector="//tr[@data-role='row']"/> + <element name="pageCurrent" type="text" selector="//label[@for='promo_quote_grid_page-current']"/> + <element name="totalCount" type="text" selector="span[data-ui-id*='grid-total-count']"/> </section> </sections> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Section/CartPriceRulesSubmenuSection.xml b/app/code/Magento/SalesRule/Test/Mftf/Section/CartPriceRulesSubmenuSection.xml new file mode 100644 index 000000000000..eb4098d71dca --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Section/CartPriceRulesSubmenuSection.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="CartPriceRulesSubmenuSection"> + <element name="cartPriceRules" type="button" selector="//li[@data-ui-id='menu-magento-catalogrule-promo']//li[@data-ui-id='menu-magento-salesrule-promo-quote']"/> + </section> +</sections> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Section/DiscountSection.xml b/app/code/Magento/SalesRule/Test/Mftf/Section/DiscountSection.xml index cbab097c5291..7e2ef0b51202 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Section/DiscountSection.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Section/DiscountSection.xml @@ -8,8 +8,10 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="DiscountSection"> - <element name="DiscountTab" type="button" selector="//strong[text()='Apply Discount Code']"/> + <element name="DiscountTab" type="button" selector="//*[text()='Apply Discount Code']"/> <element name="CouponInput" type="input" selector="#coupon_code"/> + <element name="DiscountInput" type="input" selector="#discount-code"/> <element name="ApplyCodeBtn" type="button" selector="//span[text()='Apply Discount']"/> + <element name="CancelCoupon" type="button" selector="//button[@value='Cancel Coupon']"/> </section> </sections> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Section/StorefrontSalesRuleCartCouponSection.xml b/app/code/Magento/SalesRule/Test/Mftf/Section/StorefrontSalesRuleCartCouponSection.xml index be52aa05f5af..a51aa0b7a7e6 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Section/StorefrontSalesRuleCartCouponSection.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Section/StorefrontSalesRuleCartCouponSection.xml @@ -9,10 +9,10 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontSalesRuleCartCouponSection"> - <element name="couponHeader" type="button" selector="//*[@id='block-discount-heading']"/> - <element name="couponField" type="text" selector="//*[@id='coupon_code']"/> - <element name="discountBlockActive" type="text" selector="//*[@class='block discount active']" /> - <element name="applyButton" type="text" selector="//*[@id='discount-coupon-form']//button[contains(@class, 'apply')]"/> - <element name="cancelButton" type="text" selector="//*[@id='discount-coupon-form']//button[contains(@class, 'cancel')]"/> + <element name="couponHeader" type="button" selector="#block-discount-heading"/> + <element name="couponField" type="text" selector="#coupon_code"/> + <element name="discountBlockActive" type="text" selector=".block.discount.active" /> + <element name="applyButton" type="text" selector="#discount-coupon-form button[class*='apply']" timeout="30"/> + <element name="cancelButton" type="text" selector="#discount-coupon-form button[class*='cancel']" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCartRulesAppliedForProductInCartTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCartRulesAppliedForProductInCartTest.xml new file mode 100644 index 000000000000..ab085dc5ae13 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCartRulesAppliedForProductInCartTest.xml @@ -0,0 +1,126 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCartRulesAppliedForProductInCartTest"> + <annotations> + <features value="SalesRule"/> + <stories value="The cart rule cannot effect the cart"/> + <title value="Check that cart rules applied for product in cart"/> + <description value="Check that cart rules applied for product in cart"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-96722"/> + <useCaseId value="MAGETWO-96410"/> + <group value="SalesRule"/> + </annotations> + + <before> + <!--Create category and product--> + <createData entity="_defaultCategory" stepKey="defaultCategory"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct"> + <field key="price">200</field> + <field key="quantity">500</field> + </createData> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <after> + <!--Delete created data--> + <deleteData createDataKey="defaultCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="simpleProduct" stepKey="deleteSimpleProduct"/> + + <actionGroup ref="deleteProductBySku" stepKey="deleteBundleProduct"> + <argument name="sku" value="{{BundleProduct.sku}}"/> + </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearFilters"/> + + <actionGroup ref="DeleteCartPriceRuleByName" stepKey="deleteCartPriceRule"> + <argument name="ruleName" value="{{PriceRuleWithCondition.name}}"/> + </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearFilters1"/> + + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Start creating a bundle product--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductList"/> + <waitForPageLoad stepKey="waitForProductList"/> + <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProduct"> + <argument name="product" value="BundleProduct"/> + </actionGroup> + <actionGroup ref="fillProductNameAndSkuInProductForm" stepKey="fillNameAndSku"> + <argument name="product" value="BundleProduct"/> + </actionGroup> + <pressKey selector="{{AdminProductFormSection.productSku}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ENTER]" stepKey="enter"/> + + <!--Off dynamic price and set value--> + <scrollToTopOfPage stepKey="scrollToTopOfThePageToSeePriceTypeElement"/> + <click selector="{{AdminProductFormBundleSection.dynamicPrice}}" stepKey="offDynamicPrice"/> + <fillField selector="{{AdminProductFormBundleSection.priceField}}" userInput="0" stepKey="setProductPrice"/> + + <!-- Add category to product --> + <click selector="{{AdminProductFormBundleSection.categoriesDropDown}}" stepKey="dropDownCategories"/> + <fillField selector="{{AdminProductFormBundleSection.searchForCategory}}" userInput="$$defaultCategory.name$$" stepKey="searchForCategory"/> + <waitForElementVisible selector="{{AdminProductFormBundleSection.selectCategory}}" stepKey="waitForElementLoaded"/> + <click selector="{{AdminProductFormBundleSection.selectCategory}}" stepKey="selectCategory"/> + <click selector="{{AdminProductFormBundleSection.categoriesLabel}}" stepKey="clickOnCategoriesLabelToCloseOptions"/> + + <!-- Add option, a "Radio Buttons" type option, with one product and set fixed price 200--> + <actionGroup ref="addBundleOptionWithOneProduct" stepKey="addBundleOptionWithOneProduct"> + <argument name="x" value="0"/> + <argument name="n" value="1"/> + <argument name="prodOneSku" value="$$simpleProduct.sku$$"/> + <argument name="prodTwoSku" value=""/> + <argument name="optionTitle" value="Option One"/> + <argument name="inputType" value="radio"/> + </actionGroup> + <selectOption selector="{{AdminProductFormBundleSection.bundlePriceType}}" userInput="Fixed" stepKey="selectPriceType"/> + <fillField selector="{{AdminProductFormBundleSection.bundlePriceValue}}" userInput="200" stepKey="fillPriceValue"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + + <!--Create cart price rule--> + <actionGroup ref="AdminCreateCartPriceRuleWithConditions" stepKey="createRule"> + <argument name="ruleName" value="PriceRuleWithCondition"/> + <argument name="condition1" value="Products subselection"/> + <argument name="condition2" value="Category"/> + <argument name="ruleToChange1" value="is"/> + <argument name="rule1" value="equals or greater than"/> + <argument name="ruleToChange2" value="..."/> + <argument name="rule2" value="2"/> + <argument name="categoryName" value="{{_defaultCategory.name}}"/> + </actionGroup> + + <!--Go to Storefront and add product to cart and checkout from cart--> + <amOnPage url="{{StorefrontProductPage.url($$simpleProduct.custom_attributes[url_key]$$)}}" stepKey="goToProductPage"/> + <fillField selector="{{StorefrontProductActionSection.quantity}}" userInput="2" stepKey="setQuantity"/> + <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="AddProductToCard"> + <argument name="productName" value="$$simpleProduct.name$$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShipping"/> + + <!--Check totals--> + <grabTextFrom selector="{{CheckoutPaymentSection.orderSummarySubtotal}}" stepKey="grabSubtotal"/> + <grabTextFrom selector="{{CheckoutPaymentSection.orderSummaryShippingTotal}}" stepKey="grabShippingTotal"/> + <grabTextFrom selector="{{CheckoutPaymentSection.orderSummaryTotal}}" stepKey="grabTotal"/> + <assertEquals stepKey="assertSubtotal"> + <expectedResult type="string">$400.00</expectedResult> + <actualResult type="variable">$grabSubtotal</actualResult> + </assertEquals> + <assertEquals stepKey="assertShippingTotal"> + <expectedResult type="string">$10.00</expectedResult> + <actualResult type="variable">$grabShippingTotal</actualResult> + </assertEquals> + <assertEquals stepKey="assertTotal"> + <expectedResult type="string">$410.00</expectedResult> + <actualResult type="variable">$grabTotal</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateBuyXGetYFreeTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateBuyXGetYFreeTest.xml index c69fa97efc03..92d221de9e15 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateBuyXGetYFreeTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateBuyXGetYFreeTest.xml @@ -40,6 +40,8 @@ <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{_defaultCoupon.code}}" stepKey="fillRuleName"/> <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsites"/> <selectOption selector="{{AdminCartPriceRulesFormSection.customerGroups}}" userInput="NOT LOGGED IN" stepKey="selectCustomerGroup"/> + <generateDate date="-1 day" format="m/d/Y" stepKey="yesterdayDate"/> + <fillField selector="{{AdminCartPriceRulesFormSection.fromDate}}" userInput="{$yesterdayDate}" stepKey="fillFromDate"/> <click selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="clickToExpandActions"/> <selectOption selector="{{AdminCartPriceRulesFormSection.apply}}" userInput="Buy X get Y free (discount amount is Y)" stepKey="selectActionType"/> <fillField selector="{{AdminCartPriceRulesFormSection.discountAmount}}" userInput="1" stepKey="fillDiscountAmount"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleEmptyFromDateTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleEmptyFromDateTest.xml new file mode 100644 index 000000000000..e6676dab4eb5 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleEmptyFromDateTest.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="AdminCreateCartPriceRuleEmptyFromDateTest"> + <annotations> + <features value="SalesRule"/> + <stories value="Create cart price rule"/> + <title value="Admin should be able to create a cart price rule with no starting date"/> + <description value="Admin should be able to create a cart price rule without specifying the from_date and it should be set with the current date"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-5299"/> + <group value="SalesRule"/> + </annotations> + + <before> + <createData entity="_defaultCategory" stepKey="category"/> + <createData entity="SimpleProduct" stepKey="product"> + <requiredEntity createDataKey="category"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <after> + <!-- Delete the cart price rule we made during the test --> + <actionGroup ref="DeleteCartPriceRuleByName" stepKey="cleanUpRule"> + <argument name="ruleName" value="{{_defaultCoupon.code}}"/> + </actionGroup> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <deleteData createDataKey="product" stepKey="deleteProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Set timezone--> + <!--Set timezone so we need compare with the same timezone used in "generateDate" action--> + <amOnPage url="{{GeneralConfigurationPage.url}}" stepKey="goToGeneralConfig"/> + <waitForPageLoad stepKey="waitForConfigPage"/> + <wait stepKey="wait" time="10"/> + <conditionalClick selector="{{LocaleOptionsSection.sectionHeader}}" dependentSelector="{{LocaleOptionsSection.timezone}}" visible="false" stepKey="openLocaleSection"/> + <grabValueFrom selector="{{LocaleOptionsSection.timezone}}" stepKey="originalTimezone"/> + <selectOption selector="{{LocaleOptionsSection.timezone}}" userInput="America/Los_Angeles" stepKey="setTimezone"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="saveConfig"/> + + <!-- Create a cart price rule based on a coupon code --> + <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> + <waitForPageLoad stepKey="waitForPriceList"/> + <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> + <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{_defaultCoupon.code}}" stepKey="fillRuleName"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsites"/> + <actionGroup ref="selectNotLoggedInCustomerGroup" stepKey="selectCustomerGroup"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.coupon}}" userInput="Specific Coupon" stepKey="selectCouponType"/> + <fillField selector="{{AdminCartPriceRulesFormSection.couponCode}}" userInput="{{_defaultCoupon.code}}" stepKey="fillCouponCode"/> + <click selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="clickToExpandActions"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.apply}}" userInput="Fixed amount discount for whole cart" stepKey="selectActionType"/> + <fillField selector="{{AdminCartPriceRulesFormSection.discountAmount}}" userInput="5" stepKey="fillDiscountAmount"/> + <click selector="{{AdminCartPriceRulesFormSection.save}}" stepKey="clickSaveButton"/> + + <!-- Verify initial successful save --> + <see selector="{{AdminCartPriceRulesSection.messages}}" userInput="You saved the rule." stepKey="seeSuccessMessage"/> + <fillField selector="{{AdminCartPriceRulesSection.filterByNameInput}}" userInput="{{_defaultCoupon.code}}" stepKey="filterByName"/> + <click selector="{{AdminCartPriceRulesSection.searchButton}}" stepKey="doFilter"/> + <see selector="{{AdminCartPriceRulesSection.nameColumns}}" userInput="{{_defaultCoupon.code}}" stepKey="seeRuleInResults"/> + + <!-- Verify further on the Rule's edit page --> + <click selector="{{AdminCartPriceRulesSection.rowContainingText(_defaultCoupon.code)}}" stepKey="goToEditRule"/> + <seeInField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{_defaultCoupon.code}}" stepKey="seeRuleName"/> + <seeOptionIsSelected selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="seeWebsites"/> + <seeOptionIsSelected selector="{{AdminCartPriceRulesFormSection.coupon}}" userInput="Specific Coupon" stepKey="seeCouponType"/> + <seeInField selector="{{AdminCartPriceRulesFormSection.couponCode}}" userInput="{{_defaultCoupon.code}}" stepKey="seeCouponCode"/> + <generateDate date="now" format="m/j/Y" timezone="America/Los_Angeles" stepKey="today"/> + <seeInField selector="{{AdminCartPriceRulesFormSection.fromDate}}" userInput="$today" stepKey="seeCorrectFromDate"/> + <seeInField selector="{{AdminCartPriceRulesFormSection.toDate}}" userInput="" stepKey="seeEmptyToDate"/> + <click selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="clickToExpandActions2"/> + <seeOptionIsSelected selector="{{AdminCartPriceRulesFormSection.apply}}" userInput="Fixed amount discount for whole cart" stepKey="seeActionType"/> + <seeInField selector="{{AdminCartPriceRulesFormSection.discountAmount}}" userInput="5" stepKey="seeDiscountAmount"/> + + <!-- Spot check the storefront --> + <amOnPage url="$$product.custom_attributes[url_key]$$.html" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> + <waitForPageLoad stepKey="waitForAddToCart"/> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCartPage"/> + <waitForPageLoad stepKey="waitForCartPage"/> + <actionGroup ref="StorefrontApplyCouponActionGroup" stepKey="applyCoupon"> + <argument name="coupon" value="_defaultCoupon"/> + </actionGroup> + <waitForPageLoad stepKey="waitForProductPageLoad2"/> + <waitForElementVisible selector="{{CheckoutCartSummarySection.discountAmount}}" stepKey="waitForDiscountElement"/> + <see selector="{{CheckoutCartSummarySection.discountAmount}}" userInput="-$5.00" stepKey="seeDiscountTotal"/> + + <!--Reset timezone--> + <amOnPage url="{{GeneralConfigurationPage.url}}" stepKey="goToGeneralConfigReset"/> + <waitForPageLoad stepKey="waitForConfigPageReset"/> + <conditionalClick selector="{{LocaleOptionsSection.sectionHeader}}" dependentSelector="{{LocaleOptionsSection.timezone}}" visible="false" stepKey="openLocaleSectionReset"/> + <selectOption selector="{{LocaleOptionsSection.timezone}}" userInput="$originalTimezone" stepKey="resetTimezone"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="saveConfigReset"/> + </test> +</tests> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForCouponCodeTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForCouponCodeTest.xml index 88d8f1945ce6..3deb688de9c3 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForCouponCodeTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForCouponCodeTest.xml @@ -30,7 +30,7 @@ <argument name="ruleName" value="{{_defaultCoupon.code}}"/> </actionGroup> <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <actionGroup ref="logout" stepKey="logout"/> </after> <!-- Create a cart price rule based on a coupon code --> @@ -42,6 +42,8 @@ <selectOption selector="{{AdminCartPriceRulesFormSection.customerGroups}}" userInput="NOT LOGGED IN" stepKey="selectCustomerGroup"/> <selectOption selector="{{AdminCartPriceRulesFormSection.coupon}}" userInput="Specific Coupon" stepKey="selectCouponType"/> <fillField selector="{{AdminCartPriceRulesFormSection.couponCode}}" userInput="{{_defaultCoupon.code}}" stepKey="fillCouponCode"/> + <generateDate date="-1 day" format="m/d/Y" stepKey="yesterdayDate"/> + <fillField selector="{{AdminCartPriceRulesFormSection.fromDate}}" userInput="{$yesterdayDate}" stepKey="fillFromDate"/> <click selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="clickToExpandActions"/> <selectOption selector="{{AdminCartPriceRulesFormSection.apply}}" userInput="Fixed amount discount for whole cart" stepKey="selectActionType"/> <fillField selector="{{AdminCartPriceRulesFormSection.discountAmount}}" userInput="5" stepKey="fillDiscountAmount"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml index c1aeebfca520..7b350c0208cc 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml @@ -42,6 +42,8 @@ <selectOption selector="{{AdminCartPriceRulesFormSection.customerGroups}}" userInput="NOT LOGGED IN" stepKey="selectCustomerGroup"/> <selectOption selector="{{AdminCartPriceRulesFormSection.coupon}}" userInput="Specific Coupon" stepKey="selectCouponType"/> <checkOption selector="{{AdminCartPriceRulesFormSection.useAutoGeneration}}" stepKey="tickAutoGeneration"/> + <generateDate date="-1 day" format="m/d/Y" stepKey="yesterdayDate"/> + <fillField selector="{{AdminCartPriceRulesFormSection.fromDate}}" userInput="{$yesterdayDate}" stepKey="fillFromDate"/> <click selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="clickToExpandActions"/> <selectOption selector="{{AdminCartPriceRulesFormSection.apply}}" userInput="Fixed amount discount for whole cart" stepKey="selectActionType"/> <fillField selector="{{AdminCartPriceRulesFormSection.discountAmount}}" userInput="0.99" stepKey="fillDiscountAmount"/> @@ -51,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"/> @@ -73,7 +82,6 @@ <waitForElementVisible selector="{{StorefrontSalesRuleCartCouponSection.couponField}}" stepKey="waitForCouponField" /> <fillField selector="{{StorefrontSalesRuleCartCouponSection.couponField}}" userInput="{$grabCouponCode}" stepKey="fillCouponField"/> <click selector="{{StorefrontSalesRuleCartCouponSection.applyButton}}" stepKey="clickApplyButton"/> - <waitForPageLoad stepKey="waitForProductPageLoad2"/> <waitForElementVisible selector="{{CheckoutCartSummarySection.discountAmount}}" stepKey="waitForDiscountElement"/> <see selector="{{CheckoutCartSummarySection.discountAmount}}" userInput="-$0.99" stepKey="seeDiscountTotal"/> </test> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountDiscountTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountDiscountTest.xml index 30aa26b26ed3..08a08275ee07 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountDiscountTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountDiscountTest.xml @@ -40,6 +40,8 @@ <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{_defaultCoupon.code}}" stepKey="fillRuleName"/> <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsites"/> <selectOption selector="{{AdminCartPriceRulesFormSection.customerGroups}}" userInput="NOT LOGGED IN" stepKey="selectCustomerGroup"/> + <generateDate date="-1 day" format="m/d/Y" stepKey="yesterdayDate"/> + <fillField selector="{{AdminCartPriceRulesFormSection.fromDate}}" userInput="{$yesterdayDate}" stepKey="fillFromDate"/> <click selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="clickToExpandActions"/> <selectOption selector="{{AdminCartPriceRulesFormSection.apply}}" userInput="Fixed amount discount" stepKey="selectActionType"/> <fillField selector="{{AdminCartPriceRulesFormSection.discountAmount}}" userInput="10" stepKey="fillDiscountAmount"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountWholeCartDiscountTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountWholeCartDiscountTest.xml index 7ac69f82f79d..a39530f7607e 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountWholeCartDiscountTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountWholeCartDiscountTest.xml @@ -40,6 +40,8 @@ <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{SimpleSalesRule.name}}" stepKey="fillRuleName"/> <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsites"/> <selectOption selector="{{AdminCartPriceRulesFormSection.customerGroups}}" userInput="NOT LOGGED IN" stepKey="selectCustomerGroup"/> + <generateDate date="-1 day" format="m/d/Y" stepKey="yesterdayDate"/> + <fillField selector="{{AdminCartPriceRulesFormSection.fromDate}}" userInput="{$yesterdayDate}" stepKey="fillFromDate"/> <click selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="clickToExpandActions"/> <selectOption selector="{{AdminCartPriceRulesFormSection.apply}}" userInput="Fixed amount discount for whole cart" stepKey="selectActionType"/> <fillField selector="{{AdminCartPriceRulesFormSection.discountAmount}}" userInput="19.99" stepKey="fillDiscountAmount"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreatePercentOfProductPriceTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreatePercentOfProductPriceTest.xml index eb04ce04f071..1f7d849ac02b 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreatePercentOfProductPriceTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreatePercentOfProductPriceTest.xml @@ -40,6 +40,8 @@ <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{_defaultCoupon.code}}" stepKey="fillRuleName"/> <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsites"/> <selectOption selector="{{AdminCartPriceRulesFormSection.customerGroups}}" userInput="NOT LOGGED IN" stepKey="selectCustomerGroup"/> + <generateDate date="-1 day" format="m/d/Y" stepKey="yesterdayDate"/> + <fillField selector="{{AdminCartPriceRulesFormSection.fromDate}}" userInput="{$yesterdayDate}" stepKey="fillFromDate"/> <click selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="clickToExpandActions"/> <selectOption selector="{{AdminCartPriceRulesFormSection.apply}}" userInput="Percent of product price discount" stepKey="selectActionType"/> <fillField selector="{{AdminCartPriceRulesFormSection.discountAmount}}" userInput="50" stepKey="fillDiscountAmount"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminMarketingCartPriceRulesNavigateMenuTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminMarketingCartPriceRulesNavigateMenuTest.xml new file mode 100644 index 000000000000..f281b0abf87a --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminMarketingCartPriceRulesNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMarketingCartPriceRulesNavigateMenuTest"> + <annotations> + <features value="SalesRule"/> + <stories value="Menu Navigation"/> + <title value="Admin marketing cart price rules navigate menu test"/> + <description value="Admin should be able to navigate to Marketing > Cart Price Rules"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14143"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToMarketingCartPriceRulesPage"> + <argument name="menuUiId" value="{{AdminMenuMarketing.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuMarketingPromotionsCartPriceRules.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuMarketingPromotionsCartPriceRules.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForConfigurableProductTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForConfigurableProductTest.xml new file mode 100644 index 000000000000..e2687f5f16ba --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForConfigurableProductTest.xml @@ -0,0 +1,150 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="CartPriceRuleForConfigurableProductTest"> + <annotations> + <features value="SalesRule"/> + <stories value="MAGETWO-94407 - Cart Price Rule for configurable products"/> + <title value="Checking Cart Price Rule for configurable products"/> + <description value="Checking Cart Price Rule for configurable products"/> + <severity value="BLOCKER"/> + <testCaseId value="MAGETWO-94471"/> + <group value="SalesRule"/> + </annotations> + + <before> + <!-- Create the category --> + <createData entity="ApiCategory" stepKey="createCategory"/> + <!-- Create the configurable product and add it to the category --> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Create an attribute with two options to be used in the first child product --> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + + <!-- Add the attribute we just created to default attribute set --> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + + <!-- Get the option of the attribute we created --> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <!-- Create a simple product and give it the attribute with option --> + <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <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 simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <after> + <actionGroup ref="DeleteCartPriceRuleByName" stepKey="DeleteCartPriceRuleByName"> + <argument name="ruleName" value="{{SimpleSalesRule.name}}"/> + </actionGroup> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + <deleteData createDataKey="createCategory" stepKey="deleteApiCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Create the rule --> + <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> + <waitForPageLoad stepKey="waitForRulesPage"/> + <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> + <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{SimpleSalesRule.name}}" stepKey="fillRuleName"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsites"/> + <actionGroup ref="selectNotLoggedInCustomerGroup" stepKey="selectNotLoggedInCustomerGroup"/> + <generateDate date="-1 day" format="m/d/Y" stepKey="yesterdayDate"/> + <fillField selector="{{AdminCartPriceRulesFormSection.fromDate}}" userInput="{$yesterdayDate}" stepKey="fillFromDate"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.coupon}}" userInput="Specific Coupon" stepKey="selectCouponType"/> + <fillField selector="{{AdminCartPriceRulesFormSection.couponCode}}" userInput="ABCD" stepKey="fillCouponCOde"/> + <click selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="clickToExpandActions"/> + <fillField selector="{{AdminCartPriceRulesFormSection.discountAmount}}" userInput="50" stepKey="fillDiscountAmount"/> + <scrollTo selector="{{AdminCartPriceRulesFormSection.conditions}}" stepKey="ScrollToApplyRuleForConditions"/> + <click selector="{{AdminCartPriceRulesFormSection.conditions}}" stepKey="ApplyRuleForConditions"/> + <waitForPageLoad stepKey="waitForDropDownOpened"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.childAttribute}}" userInput="$$createConfigProductAttribute.attribute[frontend_labels][0][label]$$" stepKey="selectAttribute"/> + <waitForPageLoad stepKey="waitForOperatorOpened"/> + <click selector="{{AdminCartPriceRulesFormSection.condition('is')}}" stepKey="clickToChooseCondition"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.operator}}" userInput="is not" stepKey="selectOperator"/> + <waitForPageLoad stepKey="waitForOperatorOpened1"/> + <click selector="{{AdminCartPriceRulesFormSection.condition('...')}}" stepKey="clickToChooseOption"/> + <waitForPageLoad stepKey="waitForConditionOpened2"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.option}}" userInput="option1" stepKey="selectOption"/> + <waitForPageLoad stepKey="waitForPageLoaded"/> + <click selector="{{AdminCartPriceRulesFormSection.save}}" stepKey="clickSaveButton"/> + <see selector="{{AdminCartPriceRulesSection.messages}}" userInput="You saved the rule." stepKey="seeSuccessMessage"/> + + <!-- Add the first product to the cart --> + <amOnPage url="$$createConfigChildProduct1.sku$$.html" stepKey="goToProductPage1"/> + <waitForPageLoad stepKey="waitForProductPageLoad1"/> + <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart1"/> + <waitForPageLoad stepKey="waitForAddToCart1"/> + <!-- Add the second product to the cart --> + <amOnPage url="$$createConfigChildProduct2.sku$$.html" stepKey="goToProductPage2"/> + <waitForPageLoad stepKey="waitForProductPageLoad2"/> + <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart2"/> + <waitForPageLoad stepKey="waitForAddToCart2"/> + + <!--View and edit cart--> + <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="clickViewAndEditCartFromMiniCart"/> + <click selector="{{DiscountSection.DiscountTab}}" stepKey="scrollToDiscountTab" /> + <fillField selector="{{DiscountSection.CouponInput}}" userInput="ABCD" stepKey="fillCouponCode" /> + <click selector="{{DiscountSection.ApplyCodeBtn}}" stepKey="applyCode"/> + <waitForPageLoad stepKey="waitForPageLoad3"/> + <see userInput="You used coupon code" stepKey="assertText"/> + <!--Verify values--> + <grabTextFrom selector="{{StorefrontMinicartSection.itemDiscount}}" stepKey="getDiscount"/> + <grabTextFrom selector="{{StorefrontMinicartSection.subtotal}}" stepKey="getSubtotal"/> + <assertEquals stepKey="checkDescount"> + <expectedResult type="string">-$117.00</expectedResult> + <actualResult type="variable">$getDiscount</actualResult> + </assertEquals> + <assertEquals stepKey="checkSubtotal"> + <expectedResult type="string">$357.00</expectedResult> + <actualResult type="variable">$getSubtotal</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/DeleteCustomerGroupTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/DeleteCustomerGroupTest.xml new file mode 100644 index 000000000000..77ee270b3bca --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/DeleteCustomerGroupTest.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="DeleteCustomerGroupTest"> + <actionGroup ref="AdminOpenNewCartPriceRuleFormPageActionGroup" stepKey="openNewCartPriceRuleForm" /> + <actionGroup ref="AssertCustomerGroupNotOnCartPriceRuleFormActionGroup" stepKey="assertCustomerGroupNotOnCartPriceRuleForm"> + <argument name="customerGroup" value="$$customerGroup$$" /> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml index da9eb8e19790..0d365dc089e4 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml @@ -29,30 +29,21 @@ <actionGroup ref="StorefrontCheckCouponAppliedActionGroup" stepKey="couponCheckAppliedDiscount" after="couponApplyCoupon"> <argument name="rule" value="$$createSalesRule$$"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="discount" value="E2EB2CQuoteWith10PercentDiscount.discount"/> + <argument name="discount" value="48.00"/> </actionGroup> <actionGroup ref="StorefrontCheckCartActionGroup" stepKey="couponCheckCartWithDiscount" after="couponCheckAppliedDiscount"> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="subtotal" value="E2EB2CQuoteWith10PercentDiscount.subtotal"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="shipping" value="E2EB2CQuoteWith10PercentDiscount.shipping"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="shippingMethod" value="E2EB2CQuoteWith10PercentDiscount.shippingMethod"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="total" value="E2EB2CQuoteWith10PercentDiscount.total"/> + <argument name="subtotal" value="480.00"/> + <argument name="shipping" value="15.00"/> + <argument name="shippingMethod" value="Flat Rate - Fixed"/> + <argument name="total" value="447.00"/> </actionGroup> <actionGroup ref="StorefrontCancelCouponActionGroup" stepKey="couponCancelCoupon" after="couponCheckCartWithDiscount"/> <actionGroup ref="StorefrontCheckCartActionGroup" stepKey="cartAssertCartAfterCancelCoupon" after="couponCancelCoupon"> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="subtotal" value="E2EB2CQuote.subtotal"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="shipping" value="E2EB2CQuote.shipping"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="shippingMethod" value="E2EB2CQuote.shippingMethod"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="total" value="E2EB2CQuote.total"/> + <argument name="subtotal" value="480.00"/> + <argument name="shipping" value="15.00"/> + <argument name="shippingMethod" value="Flat Rate - Fixed"/> + <argument name="total" value="495.00"/> </actionGroup> <comment userInput="End of using coupon code" stepKey="endOfUsingCouponCode" after="cartAssertCartAfterCancelCoupon" /> </test> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml index d735d5a73f0f..7a995b1feeed 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml @@ -29,30 +29,21 @@ <actionGroup ref="StorefrontCheckCouponAppliedActionGroup" stepKey="couponCheckAppliedDiscount" after="couponApplyCoupon"> <argument name="rule" value="$$createSalesRule$$"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="discount" value="E2EB2CQuoteWith10PercentDiscount.discount"/> + <argument name="discount" value="48.00"/> </actionGroup> <actionGroup ref="StorefrontCheckCartActionGroup" stepKey="couponCheckCartWithDiscount" after="couponCheckAppliedDiscount"> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="subtotal" value="E2EB2CQuoteWith10PercentDiscount.subtotal"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="shipping" value="E2EB2CQuoteWith10PercentDiscount.shipping"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="shippingMethod" value="E2EB2CQuoteWith10PercentDiscount.shippingMethod"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="total" value="E2EB2CQuoteWith10PercentDiscount.total"/> + <argument name="subtotal" value="480.00"/> + <argument name="shipping" value="15.00"/> + <argument name="shippingMethod" value="Flat Rate - Fixed"/> + <argument name="total" value="447.00"/> </actionGroup> <actionGroup ref="StorefrontCancelCouponActionGroup" stepKey="couponCancelCoupon" after="couponCheckCartWithDiscount"/> <actionGroup ref="StorefrontCheckCartActionGroup" stepKey="cartAssertCartAfterCancelCoupon" after="couponCancelCoupon"> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="subtotal" value="E2EB2CQuote.subtotal"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="shipping" value="E2EB2CQuote.shipping"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="shippingMethod" value="E2EB2CQuote.shippingMethod"/> - <!-- @TODO: Change to scalar value after MQE-498 is implemented --> - <argument name="total" value="E2EB2CQuote.total"/> + <argument name="subtotal" value="480.00"/> + <argument name="shipping" value="15.00"/> + <argument name="shippingMethod" value="Flat Rate - Fixed"/> + <argument name="total" value="495.00"/> </actionGroup> <comment userInput="End of using coupon code" stepKey="endOfUsingCouponCode" after="cartAssertCartAfterCancelCoupon"/> </test> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml new file mode 100644 index 000000000000..84537fb69ed4 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml @@ -0,0 +1,174 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontAutoGeneratedCouponCodeTest"> + <annotations> + <features value="SalesRule"/> + <stories value="Create cart price rule"/> + <title value="[Cart Price Rule] Auto generated coupon code considers 'Uses per Coupon' and 'Uses per Customer' options"/> + <description + value="[Cart Price Rule] Auto generated coupon code considers 'Uses per Coupon' and 'Uses per Customer' options"/> + <severity value="AVERAGE"/> + <testCaseId value="MAGETWO-59323"/> + <group value="salesRule"/> + </annotations> + + <before> + <!-- Create customer --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <!-- Create simple product--> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> + <!-- Create a cart price rule --> + <createData entity="SalesRuleSpecificCoupon" stepKey="createSalesRule"/> + </before> + + <after> + <!-- Delete the cart price rule we made during the test --> + <deleteData createDataKey="createSalesRule" stepKey="deleteSalesRule"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Login as Admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Search Cart Price Rule and go to edit Cart Price Rule --> + <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <fillField selector="{{AdminCartPriceRulesSection.filterByNameInput}}" userInput="$$createSalesRule.name$$" + stepKey="fillFieldFilterByName"/> + <click selector="{{AdminCartPriceRulesSection.searchButton}}" stepKey="clickSearchButton"/> + <see selector="{{AdminCartPriceRulesSection.nameColumns}}" userInput="$$createSalesRule.name$$" + stepKey="seeRuleName"/> + <click selector="{{AdminCartPriceRulesSection.rowContainingText($$createSalesRule.name$$)}}" + stepKey="goToEditRule"/> + + <!-- Step 3-4. Navigate to Manage Coupon Codes section to generate 1 coupon code --> + <conditionalClick selector="{{AdminCartPriceRulesFormSection.manageCouponCodesHeader}}" + dependentSelector="{{AdminCartPriceRulesFormSection.manageCouponCodesHeader}}" visible="true" + stepKey="clickManageCouponCodes"/> + <fillField selector="{{AdminCartPriceRulesFormSection.couponQty}}" userInput="1" stepKey="fillFieldCouponQty"/> + <click selector="{{AdminCartPriceRulesFormSection.generateCouponsButton}}" stepKey="clickGenerateCoupon"/> + <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"/> + + <!-- Step: 5. Login to storefront as previously created customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!-- Step: 6-7. Open the Product Page, add the product to Cart, go to Shopping Cart and Apply the same coupon code --> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.name$$)}}" stepKey="openProductPage"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <actionGroup ref="ApplyCartRuleOnStorefrontActionGroup" stepKey="applyCartPriceRule"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="couponCode" value="{$couponCode}"/> + </actionGroup> + <waitForText userInput='You used coupon code "{$couponCode}"' stepKey="waitForText"/> + <see selector="{{StorefrontMessagesSection.success}}" userInput='You used coupon code "{$couponCode}"' + stepKey="seeSuccessMessage1"/> + <waitForElementVisible selector="{{CheckoutCartSummarySection.discountAmount}}" time="30" + stepKey="waitForElementDiscountVisible"/> + + <!-- Step 8. Go to Checkout and Click Place Order button --> + <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout"/> + <click selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('Flat Rate')}}" + stepKey="selectFlatShippingMethod"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> + <!-- Checkout select Check/Money Order payment --> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> + <waitForElement selector="{{CheckoutSuccessMainSection.success}}" time="30" stepKey="waitForLoadSuccessPage"/> + + <!-- Step: 9-10. Open the Product Page, add the product to Cart, go to Shopping Cart and Apply the same coupon code --> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.name$$)}}" stepKey="openProductPage1"/> + <waitForPageLoad stepKey="waitForPageLoad3"/> + <actionGroup ref="ApplyCartRuleOnStorefrontActionGroup" stepKey="applyCartPriceRule1"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="couponCode" value="{$couponCode}"/> + </actionGroup> + <waitForText userInput='The coupon code "{$couponCode}" is not valid.' stepKey="waitForText1"/> + <see selector="{{StorefrontMessagesSection.error}}" userInput='The coupon code "{$couponCode}" is not valid.' + stepKey="seeErrorMessages"/> + <waitForElementNotVisible selector="{{CheckoutCartSummarySection.discountAmount}}" time="30" + stepKey="waitForElementNotDiscountVisible"/> + + <!-- Step 11. Log out from storefront --> + <actionGroup ref="StorefrontSignOutActionGroup" stepKey="storefrontSignOut"/> + + <!-- Step: 12-13. Open the Product Page, add the product to Cart, go to Shopping Cart and Apply the same coupon code --> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.name$$)}}" stepKey="openProductPage2"/> + <waitForPageLoad stepKey="waitForPageLoad4"/> + <actionGroup ref="ApplyCartRuleOnStorefrontActionGroup" stepKey="applyCartPriceRule2"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="couponCode" value="{$couponCode}"/> + </actionGroup> + <waitForText userInput='You used coupon code "{$couponCode}"' stepKey="waitForText2"/> + <see selector="{{StorefrontMessagesSection.success}}" userInput='You used coupon code "{$couponCode}"' + stepKey="seeSuccessMessage2"/> + <waitForElementVisible selector="{{CheckoutCartSummarySection.discountAmount}}" time="30" + stepKey="waitForElementDiscountVisible1"/> + + <!-- Step 14. Go to Checkout and Click Place Order button --> + <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout1"/> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShippingSection"> + <argument name="customerVar" value="CustomerEntityOne"/> + <argument name="customerAddressVar" value="CustomerAddressSimple"/> + </actionGroup> + <!-- Checkout select Check/Money Order payment --> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment2"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder1"/> + <waitForElement selector="{{CheckoutSuccessMainSection.success}}" time="30" stepKey="waitForLoadSuccessPage1"/> + + <!-- Step; 15-16. Open the Product Page, add the product to Cart, go to Shopping Cart and Apply the same coupon code --> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.name$$)}}" stepKey="openProductPage3"/> + <waitForPageLoad stepKey="waitForPageLoad5"/> + <actionGroup ref="ApplyCartRuleOnStorefrontActionGroup" stepKey="applyCartPriceRule3"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="couponCode" value="{$couponCode}"/> + </actionGroup> + <waitForText userInput='The coupon code "{$couponCode}" is not valid.' stepKey="waitForText3"/> + <see selector="{{StorefrontMessagesSection.error}}" userInput='The coupon code "{$couponCode}" is not valid.' + stepKey="seeErrorMessages1"/> + <waitForElementNotVisible selector="{{CheckoutCartSummarySection.discountAmount}}" time="30" + stepKey="waitForElementNotDiscountVisible1"/> + + <!-- Step: 17. Reset Cookie --> + <resetCookie userInput="PHPSESSID" stepKey="resetCookie"/> + + <!-- Step: 18-19. Open the Product Page, add the product to Cart, go to Shopping Cart and Apply the same coupon code --> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.name$$)}}" stepKey="openProductPage4"/> + <waitForPageLoad stepKey="waitForPageLoad6"/> + <actionGroup ref="ApplyCartRuleOnStorefrontActionGroup" stepKey="applyCartPriceRule4"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="couponCode" value="{$couponCode}"/> + </actionGroup> + <waitForText userInput='The coupon code "{$couponCode}" is not valid.' stepKey="waitForText4"/> + <see selector="{{StorefrontMessagesSection.error}}" userInput='The coupon code "{$couponCode}" is not valid.' + stepKey="seeErrorMessages2"/> + <waitForElementNotVisible selector="{{CheckoutCartSummarySection.discountAmount}}" time="30" + stepKey="waitForElementNotDiscountVisible2"/> + </test> +</tests> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleCountry.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleCountry.xml index ca897bffe8b7..045fdbb33763 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleCountry.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleCountry.xml @@ -43,6 +43,8 @@ <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{SimpleSalesRule.name}}" stepKey="fillRuleName"/> <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsites"/> <actionGroup ref="selectNotLoggedInCustomerGroup" stepKey="selectNotLoggedInCustomerGroup"/> + <generateDate date="-1 day" format="m/d/Y" stepKey="yesterdayDate"/> + <fillField selector="{{AdminCartPriceRulesFormSection.fromDate}}" userInput="{$yesterdayDate}" stepKey="fillFromDate"/> <click selector="{{PriceRuleConditionsSection.conditionsTab}}" stepKey="expandConditions"/> <!-- Scroll down to fix some flaky behavior... --> <scrollTo selector="{{PriceRuleConditionsSection.conditionsTab}}" stepKey="scrollToConditionsTab"/> @@ -70,11 +72,12 @@ <!-- Should not see the discount yet because we have not set country --> <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCartPage"/> <waitForPageLoad stepKey="waitForCartPage"/> + <click selector="{{CheckoutCartSummarySection.shippingHeading}}" stepKey="openEstimateShippingSection"/> + <checkOption selector="{{CheckoutCartSummarySection.flatRateShippingMethod}}" stepKey="selectFlatRateShipping"/> <see selector="{{CheckoutCartSummarySection.subtotal}}" userInput="$123.00" stepKey="seeSubtotal"/> <dontSeeElement selector="{{CheckoutCartSummarySection.discountAmount}}" stepKey="dontSeeDiscount"/> <!-- See discount if we use valid country --> - <click selector="{{CheckoutCartSummarySection.shippingHeading}}" stepKey="expandShipping"/> <selectOption selector="{{CheckoutCartSummarySection.country}}" userInput="Brazil" stepKey="fillCountry"/> <waitForPageLoad stepKey="waitForCountry1"/> <waitForElementVisible selector="{{CheckoutCartSummarySection.discountAmount}}" stepKey="waitForDiscountElement"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRulePostcode.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRulePostcode.xml index 83854c4a767c..d8c3ef9c32b0 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRulePostcode.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRulePostcode.xml @@ -43,6 +43,8 @@ <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{SimpleSalesRule.name}}" stepKey="fillRuleName"/> <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsites"/> <actionGroup ref="selectNotLoggedInCustomerGroup" stepKey="selectNotLoggedInCustomerGroup"/> + <generateDate date="-1 day" format="m/d/Y" stepKey="yesterdayDate"/> + <fillField selector="{{AdminCartPriceRulesFormSection.fromDate}}" userInput="{$yesterdayDate}" stepKey="fillFromDate"/> <click selector="{{PriceRuleConditionsSection.conditionsTab}}" stepKey="expandConditions"/> <!-- Scroll down to fix some flaky behavior... --> <scrollTo selector="{{PriceRuleConditionsSection.conditionsTab}}" stepKey="scrollToConditionsTab"/> @@ -74,11 +76,12 @@ <!-- Should not see the discount yet because we have not filled in postcode --> <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCartPage"/> <waitForPageLoad stepKey="waitForCartPage"/> + <click selector="{{CheckoutCartSummarySection.shippingHeading}}" stepKey="openEstimateShippingSection"/> + <checkOption selector="{{CheckoutCartSummarySection.flatRateShippingMethod}}" stepKey="selectFlatRateShipping"/> <see selector="{{CheckoutCartSummarySection.subtotal}}" userInput="$123.00" stepKey="seeSubtotal"/> <dontSeeElement selector="{{CheckoutCartSummarySection.discountAmount}}" stepKey="dontSeeDiscount"/> <!-- See discount if we use valid postcode --> - <click selector="{{CheckoutCartSummarySection.shippingHeading}}" stepKey="expandShipping"/> <fillField selector="{{CheckoutCartSummarySection.postcode}}" userInput="78613" stepKey="fillPostcode"/> <waitForPageLoad stepKey="waitForPostcode1"/> <waitForElementVisible selector="{{CheckoutCartSummarySection.discountAmount}}" stepKey="waitForDiscountElement"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleQuantity.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleQuantity.xml index 60a19074317f..51d11b4e5cb1 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleQuantity.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleQuantity.xml @@ -43,6 +43,8 @@ <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{SimpleSalesRule.name}}" stepKey="fillRuleName"/> <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsites"/> <actionGroup ref="selectNotLoggedInCustomerGroup" stepKey="selectNotLoggedInCustomerGroup"/> + <generateDate date="-1 day" format="m/d/Y" stepKey="yesterdayDate"/> + <fillField selector="{{AdminCartPriceRulesFormSection.fromDate}}" userInput="{$yesterdayDate}" stepKey="fillFromDate"/> <click selector="{{PriceRuleConditionsSection.conditionsTab}}" stepKey="expandConditions"/> <!-- Scroll down to fix some flaky behavior... --> <scrollTo selector="{{PriceRuleConditionsSection.conditionsTab}}" stepKey="scrollToConditionsTab"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleState.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleState.xml index f98f7b408436..647f4d6e5c80 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleState.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleState.xml @@ -43,6 +43,8 @@ <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{SimpleSalesRule.name}}" stepKey="fillRuleName"/> <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsites"/> <actionGroup ref="selectNotLoggedInCustomerGroup" stepKey="selectNotLoggedInCustomerGroup"/> + <generateDate date="-1 day" format="m/d/Y" stepKey="yesterdayDate"/> + <fillField selector="{{AdminCartPriceRulesFormSection.fromDate}}" userInput="{$yesterdayDate}" stepKey="fillFromDate"/> <click selector="{{PriceRuleConditionsSection.conditionsTab}}" stepKey="expandConditions"/> <!-- Scroll down to fix some flaky behavior... --> <scrollTo selector="{{PriceRuleConditionsSection.conditionsTab}}" stepKey="scrollToConditionsTab"/> @@ -70,11 +72,12 @@ <!-- Should not see the discount yet because we have not filled in postcode --> <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCartPage"/> <waitForPageLoad stepKey="waitForCartPage"/> + <click selector="{{CheckoutCartSummarySection.shippingHeading}}" stepKey="expandShipping"/> + <checkOption selector="{{CheckoutCartSummarySection.flatRateShippingMethod}}" stepKey="selectFlatRateShipping"/> <see selector="{{CheckoutCartSummarySection.subtotal}}" userInput="$123.00" stepKey="seeSubtotal"/> <dontSeeElement selector="{{CheckoutCartSummarySection.discountAmount}}" stepKey="dontSeeDiscount"/> <!-- See discount if we use valid postcode --> - <click selector="{{CheckoutCartSummarySection.shippingHeading}}" stepKey="expandShipping"/> <selectOption selector="{{CheckoutCartSummarySection.stateProvince}}" userInput="Indiana" stepKey="fillState"/> <waitForPageLoad stepKey="waitForPostcode1"/> <waitForElementVisible selector="{{CheckoutCartSummarySection.discountAmount}}" stepKey="waitForDiscountElement"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleSubtotal.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleSubtotal.xml index 6567beba198e..7c9c52e1c02a 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleSubtotal.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleSubtotal.xml @@ -43,6 +43,8 @@ <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{SimpleSalesRule.name}}" stepKey="fillRuleName"/> <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsites"/> <actionGroup ref="selectNotLoggedInCustomerGroup" stepKey="selectNotLoggedInCustomerGroup"/> + <generateDate date="-1 day" format="m/d/Y" stepKey="yesterdayDate"/> + <fillField selector="{{AdminCartPriceRulesFormSection.fromDate}}" userInput="{$yesterdayDate}" stepKey="fillFromDate"/> <click selector="{{PriceRuleConditionsSection.conditionsTab}}" stepKey="expandConditions"/> <!-- Scroll down to fix some flaky behavior... --> <scrollTo selector="{{PriceRuleConditionsSection.conditionsTab}}" stepKey="scrollToConditionsTab"/> 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 000000000000..d8c5b42dbaaa --- /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/Block/Adminhtml/Promo/Quote/Edit/DeleteButtonTest.php b/app/code/Magento/SalesRule/Test/Unit/Block/Adminhtml/Promo/Quote/Edit/DeleteButtonTest.php index fb01476ed6b3..ae59a1f90b8e 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Block/Adminhtml/Promo/Quote/Edit/DeleteButtonTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Block/Adminhtml/Promo/Quote/Edit/DeleteButtonTest.php @@ -5,7 +5,6 @@ */ namespace Magento\SalesRule\Test\Unit\Block\Adminhtml\Promo\Quote\Edit; -use Magento\SalesRule\Model\RegistryConstants; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; class DeleteButtonTest extends \PHPUnit\Framework\TestCase @@ -25,13 +24,16 @@ class DeleteButtonTest extends \PHPUnit\Framework\TestCase */ protected $registryMock; + /** + * @inheritDoc + */ protected function setUp() { $this->urlBuilderMock = $this->createMock(\Magento\Framework\UrlInterface::class); $this->registryMock = $this->createMock(\Magento\Framework\Registry::class); $contextMock = $this->createMock(\Magento\Backend\Block\Widget\Context::class); - $contextMock->expects($this->once())->method('getUrlBuilder')->willReturn($this->urlBuilderMock); + $contextMock->expects($this->any())->method('getUrlBuilder')->willReturn($this->urlBuilderMock); $this->model = (new ObjectManager($this))->getObject( \Magento\SalesRule\Block\Adminhtml\Promo\Quote\Edit\DeleteButton::class, @@ -42,33 +44,9 @@ protected function setUp() ); } - public function testGetButtonData() - { - $ruleId = 42; - $deleteUrl = 'http://magento.com/rule/delete/' . $ruleId; - $ruleMock = new \Magento\Framework\DataObject(['id' => $ruleId]); - - $this->registryMock->expects($this->once()) - ->method('registry') - ->with(RegistryConstants::CURRENT_SALES_RULE) - ->willReturn($ruleMock); - $this->urlBuilderMock->expects($this->once()) - ->method('getUrl') - ->with('*/*/delete', ['id' => $ruleId]) - ->willReturn($deleteUrl); - - $data = [ - 'label' => __('Delete'), - 'class' => 'delete', - 'on_click' => 'deleteConfirm(\'' . __( - 'Are you sure you want to delete this?' - ) . '\', \'' . $deleteUrl . '\')', - 'sort_order' => 20, - ]; - - $this->assertEquals($data, $this->model->getButtonData()); - } - + /** + * Test empty response without a present rule. + */ public function testGetButtonDataWithoutRule() { $this->assertEquals([], $this->model->getButtonData()); 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 2ef77d72a8af..66970f28598b 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/ResourceModel/ReadHandlerTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/ResourceModel/ReadHandlerTest.php index 0d6c09b7f614..f7522746b210 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/ResourceModel/ReadHandlerTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/ResourceModel/ReadHandlerTest.php @@ -16,7 +16,7 @@ class ReadHandlerTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\SalesRule\Model\ResourceModel\ReadHandler + * @var ReadHandler */ protected $model; @@ -49,7 +49,7 @@ protected function setUp() $this->metadataPool = $this->createMock($className); $this->model = $this->objectManager->getObject( - \Magento\SalesRule\Model\ResourceModel\ReadHandler::class, + ReadHandler::class, [ 'ruleResource' => $this->ruleResource, 'metadataPool' => $this->metadataPool, diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/ResourceModel/Rule/DateApplierTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/ResourceModel/Rule/DateApplierTest.php index 528db1ea0b89..65f345cb78d6 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/ResourceModel/Rule/DateApplierTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/ResourceModel/Rule/DateApplierTest.php @@ -13,7 +13,7 @@ class DateApplierTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\SalesRule\Model\ResourceModel\Rule\DateApplier|\PHPUnit_Framework_MockObject_MockObject + * @var DateApplier|\PHPUnit_Framework_MockObject_MockObject */ protected $model; @@ -29,10 +29,7 @@ protected function setUp() { $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->model = $this->objectManager->getObject( - \Magento\SalesRule\Model\ResourceModel\Rule\DateApplier::class, - [] - ); + $this->model = $this->objectManager->getObject(DateApplier::class, []); } /** diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Condition/ProductTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Condition/ProductTest.php index 51c7b9ede5aa..8ca6b20db3b5 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Condition/ProductTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Condition/ProductTest.php @@ -298,8 +298,8 @@ public function testQuoteLocaleFormatPrice($isValid, $conditionValue, $operator ->method('getProduct') ->willReturn($product); - $this->model->setAttribute('quote_item_price') - ->setOperator($operator); + $this->model->setAttribute('quote_item_price'); + $this->model->setData('operator', $operator); $this->assertEquals($isValid, $this->model->setValue($conditionValue)->validate($item)); } diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php index 42448565791c..e86068946ca7 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 000000000000..4c905fa83e2f --- /dev/null +++ b/app/code/Magento/SalesRule/etc/communication.xml @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Communication/etc/communication.xsd"> + <topic name="sales_rule.codegenerator" request="Magento\SalesRule\Api\Data\CouponGenerationSpecInterface"> + <handler name="codegeneratorProcessor" type="Magento\SalesRule\Model\Coupon\Consumer" method="process" /> + </topic> +</config> diff --git a/app/code/Magento/SalesRule/etc/db_schema.xml b/app/code/Magento/SalesRule/etc/db_schema.xml index 3d882ee2eae6..c7427e49219b 100644 --- a/app/code/Magento/SalesRule/etc/db_schema.xml +++ b/app/code/Magento/SalesRule/etc/db_schema.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="salesrule" resource="default" engine="innodb" comment="Salesrule"> <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="varchar" name="name" nullable="true" length="255" comment="Name"/> <column xsi:type="text" name="description" nullable="true" comment="Description"/> <column xsi:type="date" name="from_date" comment="From"/> @@ -46,10 +46,10 @@ identity="false" default="0" comment="Use Auto Generation"/> <column xsi:type="int" name="uses_per_coupon" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="User Per Coupon"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rule_id"/> </constraint> - <index name="SALESRULE_IS_ACTIVE_SORT_ORDER_TO_DATE_FROM_DATE" indexType="btree"> + <index referenceId="SALESRULE_IS_ACTIVE_SORT_ORDER_TO_DATE_FROM_DATE" indexType="btree"> <column name="is_active"/> <column name="sort_order"/> <column name="to_date"/> @@ -60,7 +60,7 @@ <column xsi:type="int" name="coupon_id" padding="10" unsigned="true" nullable="false" identity="true" comment="Coupon Id"/> <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Rule Id"/> + comment="Rule ID"/> <column xsi:type="varchar" name="code" nullable="true" length="255" comment="Code"/> <column xsi:type="int" name="usage_limit" padding="10" unsigned="true" nullable="true" identity="false" comment="Usage Limit"/> @@ -76,19 +76,19 @@ comment="Coupon Code Creation Date"/> <column xsi:type="smallint" name="type" padding="6" unsigned="false" nullable="true" identity="false" default="0" comment="Coupon Code Type"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="coupon_id"/> </constraint> - <constraint xsi:type="foreign" name="SALESRULE_COUPON_RULE_ID_SALESRULE_RULE_ID" table="salesrule_coupon" + <constraint xsi:type="foreign" referenceId="SALESRULE_COUPON_RULE_ID_SALESRULE_RULE_ID" table="salesrule_coupon" column="rule_id" referenceTable="salesrule" referenceColumn="rule_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="SALESRULE_COUPON_CODE"> + <constraint xsi:type="unique" referenceId="SALESRULE_COUPON_CODE"> <column name="code"/> </constraint> - <constraint xsi:type="unique" name="SALESRULE_COUPON_RULE_ID_IS_PRIMARY"> + <constraint xsi:type="unique" referenceId="SALESRULE_COUPON_RULE_ID_IS_PRIMARY"> <column name="rule_id"/> <column name="is_primary"/> </constraint> - <index name="SALESRULE_COUPON_RULE_ID" indexType="btree"> + <index referenceId="SALESRULE_COUPON_RULE_ID" indexType="btree"> <column name="rule_id"/> </index> </table> @@ -99,17 +99,17 @@ comment="Customer Id"/> <column xsi:type="int" name="times_used" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Times Used"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="coupon_id"/> <column name="customer_id"/> </constraint> - <constraint xsi:type="foreign" name="SALESRULE_COUPON_USAGE_COUPON_ID_SALESRULE_COUPON_COUPON_ID" + <constraint xsi:type="foreign" referenceId="SALESRULE_COUPON_USAGE_COUPON_ID_SALESRULE_COUPON_COUPON_ID" table="salesrule_coupon_usage" column="coupon_id" referenceTable="salesrule_coupon" referenceColumn="coupon_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="SALESRULE_COUPON_USAGE_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="SALESRULE_COUPON_USAGE_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID" table="salesrule_coupon_usage" column="customer_id" referenceTable="customer_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <index name="SALESRULE_COUPON_USAGE_CUSTOMER_ID" indexType="btree"> + <index referenceId="SALESRULE_COUPON_USAGE_CUSTOMER_ID" indexType="btree"> <column name="customer_id"/> </index> </table> @@ -117,24 +117,24 @@ <column xsi:type="int" name="rule_customer_id" padding="10" unsigned="true" nullable="false" identity="true" comment="Rule Customer Id"/> <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" - comment="Rule Id"/> + comment="Rule ID"/> <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Customer Id"/> <column xsi:type="smallint" name="times_used" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Times Used"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rule_customer_id"/> </constraint> - <constraint xsi:type="foreign" name="SALESRULE_CUSTOMER_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="SALESRULE_CUSTOMER_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID" table="salesrule_customer" column="customer_id" referenceTable="customer_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="SALESRULE_CUSTOMER_RULE_ID_SALESRULE_RULE_ID" table="salesrule_customer" + <constraint xsi:type="foreign" referenceId="SALESRULE_CUSTOMER_RULE_ID_SALESRULE_RULE_ID" table="salesrule_customer" column="rule_id" referenceTable="salesrule" referenceColumn="rule_id" onDelete="CASCADE"/> - <index name="SALESRULE_CUSTOMER_RULE_ID_CUSTOMER_ID" indexType="btree"> + <index referenceId="SALESRULE_CUSTOMER_RULE_ID_CUSTOMER_ID" indexType="btree"> <column name="rule_id"/> <column name="customer_id"/> </index> - <index name="SALESRULE_CUSTOMER_CUSTOMER_ID_RULE_ID" indexType="btree"> + <index referenceId="SALESRULE_CUSTOMER_CUSTOMER_ID_RULE_ID" indexType="btree"> <column name="customer_id"/> <column name="rule_id"/> </index> @@ -143,62 +143,63 @@ <column xsi:type="int" name="label_id" padding="10" unsigned="true" nullable="false" identity="true" comment="Label Id"/> <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" - comment="Rule Id"/> + comment="Rule ID"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Store Id"/> <column xsi:type="varchar" name="label" nullable="true" length="255" comment="Label"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="label_id"/> </constraint> - <constraint xsi:type="foreign" name="SALESRULE_LABEL_RULE_ID_SALESRULE_RULE_ID" table="salesrule_label" + <constraint xsi:type="foreign" referenceId="SALESRULE_LABEL_RULE_ID_SALESRULE_RULE_ID" table="salesrule_label" column="rule_id" referenceTable="salesrule" referenceColumn="rule_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="SALESRULE_LABEL_STORE_ID_STORE_STORE_ID" table="salesrule_label" + <constraint xsi:type="foreign" referenceId="SALESRULE_LABEL_STORE_ID_STORE_STORE_ID" table="salesrule_label" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="SALESRULE_LABEL_RULE_ID_STORE_ID"> + <constraint xsi:type="unique" referenceId="SALESRULE_LABEL_RULE_ID_STORE_ID"> <column name="rule_id"/> <column name="store_id"/> </constraint> - <index name="SALESRULE_LABEL_STORE_ID" indexType="btree"> + <index referenceId="SALESRULE_LABEL_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> <table name="salesrule_product_attribute" resource="default" engine="innodb" comment="Salesrule Product Attribute"> - <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false"/> + <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" + comment="Rule ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Website Id"/> <column xsi:type="int" name="customer_group_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Customer Group Id"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Attribute Id"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rule_id"/> <column name="website_id"/> <column name="customer_group_id"/> <column name="attribute_id"/> </constraint> - <constraint xsi:type="foreign" name="SALESRULE_PRD_ATTR_ATTR_ID_EAV_ATTR_ATTR_ID" + <constraint xsi:type="foreign" referenceId="SALESRULE_PRD_ATTR_ATTR_ID_EAV_ATTR_ATTR_ID" table="salesrule_product_attribute" column="attribute_id" referenceTable="eav_attribute" referenceColumn="attribute_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="SALESRULE_PRD_ATTR_CSTR_GROUP_ID_CSTR_GROUP_CSTR_GROUP_ID" + <constraint xsi:type="foreign" referenceId="SALESRULE_PRD_ATTR_CSTR_GROUP_ID_CSTR_GROUP_CSTR_GROUP_ID" table="salesrule_product_attribute" column="customer_group_id" referenceTable="customer_group" referenceColumn="customer_group_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="SALESRULE_PRODUCT_ATTRIBUTE_RULE_ID_SALESRULE_RULE_ID" + <constraint xsi:type="foreign" referenceId="SALESRULE_PRODUCT_ATTRIBUTE_RULE_ID_SALESRULE_RULE_ID" table="salesrule_product_attribute" column="rule_id" referenceTable="salesrule" referenceColumn="rule_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="SALESRULE_PRODUCT_ATTRIBUTE_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID" + <constraint xsi:type="foreign" referenceId="SALESRULE_PRODUCT_ATTRIBUTE_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID" table="salesrule_product_attribute" column="website_id" referenceTable="store_website" referenceColumn="website_id" onDelete="CASCADE"/> - <index name="SALESRULE_PRODUCT_ATTRIBUTE_WEBSITE_ID" indexType="btree"> + <index referenceId="SALESRULE_PRODUCT_ATTRIBUTE_WEBSITE_ID" indexType="btree"> <column name="website_id"/> </index> - <index name="SALESRULE_PRODUCT_ATTRIBUTE_CUSTOMER_GROUP_ID" indexType="btree"> + <index referenceId="SALESRULE_PRODUCT_ATTRIBUTE_CUSTOMER_GROUP_ID" indexType="btree"> <column name="customer_group_id"/> </index> - <index name="SALESRULE_PRODUCT_ATTRIBUTE_ATTRIBUTE_ID" indexType="btree"> + <index referenceId="SALESRULE_PRODUCT_ATTRIBUTE_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> </table> - <table name="salesrule_coupon_aggregated" resource="default" engine="innodb" comment="Coupon Aggregated"> + <table name="salesrule_coupon_aggregated" resource="sales" engine="innodb" comment="Coupon Aggregated"> <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> <column xsi:type="date" name="period" nullable="false" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" @@ -207,39 +208,39 @@ <column xsi:type="varchar" name="coupon_code" nullable="true" length="50" comment="Coupon Code"/> <column xsi:type="int" name="coupon_uses" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Coupon Uses"/> - <column xsi:type="decimal" name="subtotal_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="subtotal_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Subtotal Amount"/> <column xsi:type="decimal" name="discount_amount" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Discount Amount"/> - <column xsi:type="decimal" name="total_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="total_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Amount"/> - <column xsi:type="decimal" name="subtotal_amount_actual" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="subtotal_amount_actual" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Subtotal Amount Actual"/> <column xsi:type="decimal" name="discount_amount_actual" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Discount Amount Actual"/> - <column xsi:type="decimal" name="total_amount_actual" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_amount_actual" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Amount Actual"/> <column xsi:type="varchar" name="rule_name" nullable="true" length="255" comment="Rule Name"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> </constraint> - <constraint xsi:type="foreign" name="SALESRULE_COUPON_AGGREGATED_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="SALESRULE_COUPON_AGGREGATED_STORE_ID_STORE_STORE_ID" table="salesrule_coupon_aggregated" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="SALESRULE_COUPON_AGGRED_PERIOD_STORE_ID_ORDER_STS_COUPON_CODE"> + <constraint xsi:type="unique" referenceId="SALESRULE_COUPON_AGGRED_PERIOD_STORE_ID_ORDER_STS_COUPON_CODE"> <column name="period"/> <column name="store_id"/> <column name="order_status"/> <column name="coupon_code"/> </constraint> - <index name="SALESRULE_COUPON_AGGREGATED_STORE_ID" indexType="btree"> + <index referenceId="SALESRULE_COUPON_AGGREGATED_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="SALESRULE_COUPON_AGGREGATED_RULE_NAME" indexType="btree"> + <index referenceId="SALESRULE_COUPON_AGGREGATED_RULE_NAME" indexType="btree"> <column name="rule_name"/> </index> </table> - <table name="salesrule_coupon_aggregated_updated" resource="default" engine="innodb" + <table name="salesrule_coupon_aggregated_updated" resource="sales" engine="innodb" comment="Salesrule Coupon Aggregated Updated"> <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> <column xsi:type="date" name="period" nullable="false" comment="Period"/> @@ -249,35 +250,35 @@ <column xsi:type="varchar" name="coupon_code" nullable="true" length="50" comment="Coupon Code"/> <column xsi:type="int" name="coupon_uses" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Coupon Uses"/> - <column xsi:type="decimal" name="subtotal_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="subtotal_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Subtotal Amount"/> <column xsi:type="decimal" name="discount_amount" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Discount Amount"/> - <column xsi:type="decimal" name="total_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="total_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Amount"/> - <column xsi:type="decimal" name="subtotal_amount_actual" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="subtotal_amount_actual" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Subtotal Amount Actual"/> <column xsi:type="decimal" name="discount_amount_actual" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Discount Amount Actual"/> - <column xsi:type="decimal" name="total_amount_actual" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="total_amount_actual" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Amount Actual"/> <column xsi:type="varchar" name="rule_name" nullable="true" length="255" comment="Rule Name"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> </constraint> - <constraint xsi:type="foreign" name="SALESRULE_COUPON_AGGREGATED_UPDATED_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="SALESRULE_COUPON_AGGREGATED_UPDATED_STORE_ID_STORE_STORE_ID" table="salesrule_coupon_aggregated_updated" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="UNQ_7196FA120A4F0F84E1B66605E87E213E"> + <constraint xsi:type="unique" referenceId="UNQ_7196FA120A4F0F84E1B66605E87E213E"> <column name="period"/> <column name="store_id"/> <column name="order_status"/> <column name="coupon_code"/> </constraint> - <index name="SALESRULE_COUPON_AGGREGATED_UPDATED_STORE_ID" indexType="btree"> + <index referenceId="SALESRULE_COUPON_AGGREGATED_UPDATED_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="SALESRULE_COUPON_AGGREGATED_UPDATED_RULE_NAME" indexType="btree"> + <index referenceId="SALESRULE_COUPON_AGGREGATED_UPDATED_RULE_NAME" indexType="btree"> <column name="rule_name"/> </index> </table> @@ -291,65 +292,67 @@ <column xsi:type="varchar" name="coupon_code" nullable="true" length="50" comment="Coupon Code"/> <column xsi:type="int" name="coupon_uses" padding="11" unsigned="false" nullable="false" identity="false" default="0" comment="Coupon Uses"/> - <column xsi:type="decimal" name="subtotal_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="subtotal_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Subtotal Amount"/> <column xsi:type="decimal" name="discount_amount" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Discount Amount"/> - <column xsi:type="decimal" name="total_amount" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="total_amount" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Total Amount"/> <column xsi:type="varchar" name="rule_name" nullable="true" length="255" comment="Rule Name"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> </constraint> - <constraint xsi:type="foreign" name="SALESRULE_COUPON_AGGREGATED_ORDER_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="SALESRULE_COUPON_AGGREGATED_ORDER_STORE_ID_STORE_STORE_ID" table="salesrule_coupon_aggregated_order" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="UNQ_1094D1FBBCBB11704A29DEF3ACC37D2B"> + <constraint xsi:type="unique" referenceId="UNQ_1094D1FBBCBB11704A29DEF3ACC37D2B"> <column name="period"/> <column name="store_id"/> <column name="order_status"/> <column name="coupon_code"/> </constraint> - <index name="SALESRULE_COUPON_AGGREGATED_ORDER_STORE_ID" indexType="btree"> + <index referenceId="SALESRULE_COUPON_AGGREGATED_ORDER_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="SALESRULE_COUPON_AGGREGATED_ORDER_RULE_NAME" indexType="btree"> + <index referenceId="SALESRULE_COUPON_AGGREGATED_ORDER_RULE_NAME" indexType="btree"> <column name="rule_name"/> </index> </table> <table name="salesrule_website" resource="default" engine="innodb" comment="Sales Rules To Websites Relations"> - <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false"/> + <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" + comment="Rule ID"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Website Id"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rule_id"/> <column name="website_id"/> </constraint> - <constraint xsi:type="foreign" name="SALESRULE_WEBSITE_RULE_ID_SALESRULE_RULE_ID" table="salesrule_website" + <constraint xsi:type="foreign" referenceId="SALESRULE_WEBSITE_RULE_ID_SALESRULE_RULE_ID" table="salesrule_website" column="rule_id" referenceTable="salesrule" referenceColumn="rule_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="SALESRULE_WEBSITE_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID" + <constraint xsi:type="foreign" referenceId="SALESRULE_WEBSITE_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID" table="salesrule_website" column="website_id" referenceTable="store_website" referenceColumn="website_id" onDelete="CASCADE"/> - <index name="SALESRULE_WEBSITE_WEBSITE_ID" indexType="btree"> + <index referenceId="SALESRULE_WEBSITE_WEBSITE_ID" indexType="btree"> <column name="website_id"/> </index> </table> <table name="salesrule_customer_group" resource="default" engine="innodb" comment="Sales Rules To Customer Groups Relations"> - <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false"/> + <column xsi:type="int" name="rule_id" padding="10" unsigned="true" nullable="false" identity="false" + comment="Rule ID"/> <column xsi:type="int" name="customer_group_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Customer Group Id"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="rule_id"/> <column name="customer_group_id"/> </constraint> - <constraint xsi:type="foreign" name="SALESRULE_CUSTOMER_GROUP_RULE_ID_SALESRULE_RULE_ID" + <constraint xsi:type="foreign" referenceId="SALESRULE_CUSTOMER_GROUP_RULE_ID_SALESRULE_RULE_ID" table="salesrule_customer_group" column="rule_id" referenceTable="salesrule" referenceColumn="rule_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="SALESRULE_CSTR_GROUP_CSTR_GROUP_ID_CSTR_GROUP_CSTR_GROUP_ID" + <constraint xsi:type="foreign" referenceId="SALESRULE_CSTR_GROUP_CSTR_GROUP_ID_CSTR_GROUP_CSTR_GROUP_ID" table="salesrule_customer_group" column="customer_group_id" referenceTable="customer_group" referenceColumn="customer_group_id" onDelete="CASCADE"/> - <index name="SALESRULE_CUSTOMER_GROUP_CUSTOMER_GROUP_ID" indexType="btree"> + <index referenceId="SALESRULE_CUSTOMER_GROUP_CUSTOMER_GROUP_ID" indexType="btree"> <column name="customer_group_id"/> </index> </table> diff --git a/app/code/Magento/SalesRule/etc/di.xml b/app/code/Magento/SalesRule/etc/di.xml index a8c350457a5a..27c9a41503b2 100644 --- a/app/code/Magento/SalesRule/etc/di.xml +++ b/app/code/Magento/SalesRule/etc/di.xml @@ -179,7 +179,7 @@ </arguments> </type> - <type name="\Magento\Quote\Model\Cart\CartTotalRepository"> + <type name="Magento\Quote\Model\Cart\CartTotalRepository"> <plugin name="coupon_label_plugin" type="Magento\SalesRule\Plugin\CartTotalRepository" /> </type> </config> diff --git a/app/code/Magento/SalesRule/etc/queue.xml b/app/code/Magento/SalesRule/etc/queue.xml new file mode 100644 index 000000000000..8217a0b9f6c1 --- /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 000000000000..9eb585f48e8e --- /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 000000000000..0863fba2307c --- /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 000000000000..fd6a9bf36721 --- /dev/null +++ b/app/code/Magento/SalesRule/etc/queue_topology.xml @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/topology.xsd"> + <exchange name="magento-db" type="topic" connection="db"> + <binding id="codegeneratorBinding" topic="sales_rule.codegenerator" destinationType="queue" destination="codegenerator"/> + </exchange> +</config> diff --git a/app/code/Magento/SalesRule/etc/sales.xml b/app/code/Magento/SalesRule/etc/sales.xml index 3ab197d40b0d..d2db66422487 100644 --- a/app/code/Magento/SalesRule/etc/sales.xml +++ b/app/code/Magento/SalesRule/etc/sales.xml @@ -8,7 +8,8 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Sales:etc/sales.xsd"> <section name="quote"> <group name="totals"> - <item name="discount" instance="Magento\SalesRule\Model\Quote\Discount" sort_order="400"/> + <item name="discount" instance="Magento\SalesRule\Model\Quote\Discount" sort_order="300"/> + <item name="shipping_discount" instance="Magento\SalesRule\Model\Quote\Address\Total\ShippingDiscount" sort_order="400"/> </group> </section> </config> diff --git a/app/code/Magento/SalesRule/view/adminhtml/layout/sales_rule_promo_quote_index.xml b/app/code/Magento/SalesRule/view/adminhtml/layout/sales_rule_promo_quote_index.xml index 21ce8bfc3eaa..7796f280efcb 100644 --- a/app/code/Magento/SalesRule/view/adminhtml/layout/sales_rule_promo_quote_index.xml +++ b/app/code/Magento/SalesRule/view/adminhtml/layout/sales_rule_promo_quote_index.xml @@ -12,7 +12,7 @@ <block class="Magento\Backend\Block\Widget\Grid" name="adminhtml.block.promo.quote.grid" as="grid"> <arguments> <argument name="id" xsi:type="string">promo_quote_grid</argument> - <argument name="dataSource" xsi:type="object">Magento\SalesRule\Model\ResourceModel\Rule\Quote\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\SalesRule\Model\ResourceModel\Rule\Quote\Collection</argument> <argument name="default_sort" xsi:type="string">sort_order</argument> <argument name="default_dir" xsi:type="string">ASC</argument> <argument name="save_parameters_in_session" xsi:type="string">1</argument> 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 9b579f47759a..570eb0bf151f 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 000000000000..dfb3f909345b --- /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/SalesRule/view/frontend/layout/checkout_cart_index.xml b/app/code/Magento/SalesRule/view/frontend/layout/checkout_cart_index.xml index a5d0be503baa..9e86671500c5 100644 --- a/app/code/Magento/SalesRule/view/frontend/layout/checkout_cart_index.xml +++ b/app/code/Magento/SalesRule/view/frontend/layout/checkout_cart_index.xml @@ -13,14 +13,10 @@ <item name="components" xsi:type="array"> <item name="block-totals" xsi:type="array"> <item name="children" xsi:type="array"> - <item name="before_grandtotal" xsi:type="array"> - <item name="children" xsi:type="array"> - <item name="discount" xsi:type="array"> - <item name="component" xsi:type="string">Magento_SalesRule/js/view/cart/totals/discount</item> - <item name="config" xsi:type="array"> - <item name="title" xsi:type="string" translate="true">Discount</item> - </item> - </item> + <item name="discount" xsi:type="array"> + <item name="component" xsi:type="string">Magento_SalesRule/js/view/cart/totals/discount</item> + <item name="config" xsi:type="array"> + <item name="title" xsi:type="string" translate="true">Discount</item> </item> </item> </item> diff --git a/app/code/Magento/SalesSequence/Model/Manager.php b/app/code/Magento/SalesSequence/Model/Manager.php index d18a872bf3b6..95b26ba425cf 100644 --- a/app/code/Magento/SalesSequence/Model/Manager.php +++ b/app/code/Magento/SalesSequence/Model/Manager.php @@ -41,7 +41,9 @@ public function __construct( * * @param string $entityType * @param int $storeId + * * @return \Magento\Framework\DB\Sequence\SequenceInterface + * @throws \Magento\Framework\Exception\LocalizedException */ public function getSequence($entityType, $storeId) { diff --git a/app/code/Magento/SalesSequence/Model/ResourceModel/Meta.php b/app/code/Magento/SalesSequence/Model/ResourceModel/Meta.php index be566b6119bd..7a4aabea8568 100644 --- a/app/code/Magento/SalesSequence/Model/ResourceModel/Meta.php +++ b/app/code/Magento/SalesSequence/Model/ResourceModel/Meta.php @@ -96,7 +96,9 @@ public function loadByEntityTypeAndStore($entityType, $storeId) * Using for load sequence profile and setting it into metadata * * @param \Magento\Framework\Model\AbstractModel $object - * @return $this + * + * @return $this|\Magento\Framework\Model\ResourceModel\Db\AbstractDb + * @throws \Magento\Framework\Exception\LocalizedException */ protected function _afterLoad(\Magento\Framework\Model\AbstractModel $object) { @@ -137,7 +139,12 @@ protected function _beforeSave(\Magento\Framework\Model\AbstractModel $object) } /** - * @inheritdoc + * Perform actions after object save + * + * @param \Magento\Framework\Model\AbstractModel $object + * + * @return $this|\Magento\Framework\Model\ResourceModel\Db\AbstractDb + * @throws \Magento\Framework\Exception\AlreadyExistsException */ protected function _afterSave(\Magento\Framework\Model\AbstractModel $object) { diff --git a/app/code/Magento/SalesSequence/Model/ResourceModel/Profile.php b/app/code/Magento/SalesSequence/Model/ResourceModel/Profile.php index 368bdf6c26f1..4f1788b0ca35 100644 --- a/app/code/Magento/SalesSequence/Model/ResourceModel/Profile.php +++ b/app/code/Magento/SalesSequence/Model/ResourceModel/Profile.php @@ -5,7 +5,6 @@ */ namespace Magento\SalesSequence\Model\ResourceModel; -use Magento\SalesSequence\Model\Meta as ModelMeta; use Magento\Framework\Model\ResourceModel\Db\Context as DatabaseContext; use Magento\SalesSequence\Model\ProfileFactory; diff --git a/app/code/Magento/SalesSequence/Observer/SequenceCreatorObserver.php b/app/code/Magento/SalesSequence/Observer/SequenceCreatorObserver.php index cc828e7d36a4..cf56c96fb171 100644 --- a/app/code/Magento/SalesSequence/Observer/SequenceCreatorObserver.php +++ b/app/code/Magento/SalesSequence/Observer/SequenceCreatorObserver.php @@ -49,8 +49,12 @@ public function __construct( } /** + * Observer triggered during adding new store + * * @param EventObserver $observer - * @return $this + * + * @return $this|void + * @throws \Magento\Framework\Exception\AlreadyExistsException */ public function execute(EventObserver $observer) { diff --git a/app/code/Magento/SalesSequence/Setup/SequenceCreator.php b/app/code/Magento/SalesSequence/Setup/SequenceCreator.php index b0cbf545957b..14df80224e2c 100644 --- a/app/code/Magento/SalesSequence/Setup/SequenceCreator.php +++ b/app/code/Magento/SalesSequence/Setup/SequenceCreator.php @@ -49,6 +49,8 @@ public function __construct( /** * Creates sales sequences. + * + * @throws \Magento\Framework\Exception\AlreadyExistsException */ public function create() { diff --git a/app/code/Magento/SalesSequence/etc/db_schema.xml b/app/code/Magento/SalesSequence/etc/db_schema.xml index 530a9a33b0cf..7ad48badf7b8 100644 --- a/app/code/Magento/SalesSequence/etc/db_schema.xml +++ b/app/code/Magento/SalesSequence/etc/db_schema.xml @@ -23,13 +23,13 @@ <column xsi:type="int" name="warning_value" padding="10" unsigned="true" nullable="false" identity="false" comment="WarningValue for sequence"/> <column xsi:type="boolean" name="is_active" nullable="false" default="false" comment="isActive flag"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="profile_id"/> </constraint> - <constraint xsi:type="foreign" name="SALES_SEQUENCE_PROFILE_META_ID_SALES_SEQUENCE_META_META_ID" + <constraint xsi:type="foreign" referenceId="SALES_SEQUENCE_PROFILE_META_ID_SALES_SEQUENCE_META_META_ID" table="sales_sequence_profile" column="meta_id" referenceTable="sales_sequence_meta" referenceColumn="meta_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="SALES_SEQUENCE_PROFILE_META_ID_PREFIX_SUFFIX"> + <constraint xsi:type="unique" referenceId="SALES_SEQUENCE_PROFILE_META_ID_PREFIX_SUFFIX"> <column name="meta_id"/> <column name="prefix"/> <column name="suffix"/> @@ -41,11 +41,11 @@ <column xsi:type="varchar" name="entity_type" nullable="false" length="32" comment="Prefix"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Store Id"/> - <column xsi:type="varchar" name="sequence_table" nullable="false" length="32" comment="table for sequence"/> - <constraint xsi:type="primary" name="PRIMARY"> + <column xsi:type="varchar" name="sequence_table" nullable="false" length="64" comment="table for sequence"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="meta_id"/> </constraint> - <constraint xsi:type="unique" name="SALES_SEQUENCE_META_ENTITY_TYPE_STORE_ID"> + <constraint xsi:type="unique" referenceId="SALES_SEQUENCE_META_ENTITY_TYPE_STORE_ID"> <column name="entity_type"/> <column name="store_id"/> </constraint> diff --git a/app/code/Magento/SampleData/README.md b/app/code/Magento/SampleData/README.md index 5abcbaab1481..1077038fc074 100644 --- a/app/code/Magento/SampleData/README.md +++ b/app/code/Magento/SampleData/README.md @@ -69,4 +69,4 @@ If you have deleted certain entities provided by sample data and want to restore The deleted sample data entities will be restored. Those entities, which were changed, will preserve these changes and will not be restored to the default view. ## Documentation -You can find the more detailed description of sample data manipulation procedures at [http://devdocs.magento.com/guides/v2.0/install-gde/install/cli/install-cli-sample-data.html](http://devdocs.magento.com/guides/v2.0/install-gde/install/cli/install-cli-sample-data.html) +You can find the more detailed description of sample data manipulation procedures at [https://devdocs.magento.com/guides/v2.0/install-gde/install/cli/install-cli-sample-data.html](https://devdocs.magento.com/guides/v2.0/install-gde/install/cli/install-cli-sample-data.html) diff --git a/app/code/Magento/Search/Controller/Adminhtml/Synonyms/MassDelete.php b/app/code/Magento/Search/Controller/Adminhtml/Synonyms/MassDelete.php index f2770f77cc53..c32ca04d39b6 100644 --- a/app/code/Magento/Search/Controller/Adminhtml/Synonyms/MassDelete.php +++ b/app/code/Magento/Search/Controller/Adminhtml/Synonyms/MassDelete.php @@ -6,15 +6,17 @@ namespace Magento\Search\Controller\Adminhtml\Synonyms; +use Magento\Framework\App\Action\HttpPostActionInterface; + /** - * Mass-Delete Controller + * Mass-Delete Controller. * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class MassDelete extends \Magento\Backend\App\Action +class MassDelete extends \Magento\Backend\App\Action implements HttpPostActionInterface { /** - * Authorization level of a basic admin session + * Authorization level of a basic admin session. * * @see _isAllowed() */ @@ -56,7 +58,7 @@ public function __construct( } /** - * Execute action + * Execute action. * * @return \Magento\Backend\Model\View\Result\Redirect * @throws \Magento\Framework\Exception\LocalizedException|\Exception diff --git a/app/code/Magento/Search/Controller/Adminhtml/Term/Delete.php b/app/code/Magento/Search/Controller/Adminhtml/Term/Delete.php index c7adf32da0fb..ce3dfcc601fa 100644 --- a/app/code/Magento/Search/Controller/Adminhtml/Term/Delete.php +++ b/app/code/Magento/Search/Controller/Adminhtml/Term/Delete.php @@ -5,10 +5,11 @@ */ namespace Magento\Search\Controller\Adminhtml\Term; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Search\Controller\Adminhtml\Term as TermController; use Magento\Framework\Controller\ResultFactory; -class Delete extends TermController +class Delete extends TermController implements HttpPostActionInterface { /** * @return \Magento\Backend\Model\View\Result\Redirect diff --git a/app/code/Magento/Search/Controller/Adminhtml/Term/Edit.php b/app/code/Magento/Search/Controller/Adminhtml/Term/Edit.php index 3ee0ea240377..cd1ef7553c7e 100644 --- a/app/code/Magento/Search/Controller/Adminhtml/Term/Edit.php +++ b/app/code/Magento/Search/Controller/Adminhtml/Term/Edit.php @@ -5,12 +5,13 @@ */ namespace Magento\Search\Controller\Adminhtml\Term; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Search\Controller\Adminhtml\Term as TermController; use Magento\Backend\App\Action\Context; use Magento\Framework\Registry; use Magento\Framework\Controller\ResultFactory; -class Edit extends TermController +class Edit extends TermController implements HttpGetActionInterface { /** * Core registry diff --git a/app/code/Magento/Search/Controller/Adminhtml/Term/Index.php b/app/code/Magento/Search/Controller/Adminhtml/Term/Index.php index f7d0b8659072..c097cc08489f 100644 --- a/app/code/Magento/Search/Controller/Adminhtml/Term/Index.php +++ b/app/code/Magento/Search/Controller/Adminhtml/Term/Index.php @@ -5,9 +5,10 @@ */ namespace Magento\Search\Controller\Adminhtml\Term; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Search\Controller\Adminhtml\Term as TermController; -class Index extends TermController +class Index extends TermController implements HttpGetActionInterface { /** * @return \Magento\Backend\Model\View\Result\Page diff --git a/app/code/Magento/Search/Controller/Adminhtml/Term/MassDelete.php b/app/code/Magento/Search/Controller/Adminhtml/Term/MassDelete.php index f6874078f2f6..84e475b7275a 100644 --- a/app/code/Magento/Search/Controller/Adminhtml/Term/MassDelete.php +++ b/app/code/Magento/Search/Controller/Adminhtml/Term/MassDelete.php @@ -5,10 +5,11 @@ */ namespace Magento\Search\Controller\Adminhtml\Term; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Search\Controller\Adminhtml\Term as TermController; use Magento\Framework\Controller\ResultFactory; -class MassDelete extends TermController +class MassDelete extends TermController implements HttpPostActionInterface { /** * @return \Magento\Backend\Model\View\Result\Redirect diff --git a/app/code/Magento/Search/Controller/Adminhtml/Term/NewAction.php b/app/code/Magento/Search/Controller/Adminhtml/Term/NewAction.php index 8565c03724c6..49805400713f 100644 --- a/app/code/Magento/Search/Controller/Adminhtml/Term/NewAction.php +++ b/app/code/Magento/Search/Controller/Adminhtml/Term/NewAction.php @@ -5,10 +5,11 @@ */ namespace Magento\Search\Controller\Adminhtml\Term; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Search\Controller\Adminhtml\Term as TermController; use Magento\Framework\Controller\ResultFactory; -class NewAction extends TermController +class NewAction extends TermController implements HttpGetActionInterface { /** * @return \Magento\Backend\Model\View\Result\Forward diff --git a/app/code/Magento/Search/Controller/Adminhtml/Term/Report.php b/app/code/Magento/Search/Controller/Adminhtml/Term/Report.php index fb4964717e56..b0ce066f1270 100644 --- a/app/code/Magento/Search/Controller/Adminhtml/Term/Report.php +++ b/app/code/Magento/Search/Controller/Adminhtml/Term/Report.php @@ -5,10 +5,11 @@ */ namespace Magento\Search\Controller\Adminhtml\Term; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Reports\Controller\Adminhtml\Index as ReportsIndexController; use Magento\Framework\Controller\ResultFactory; -class Report extends ReportsIndexController +class Report extends ReportsIndexController implements HttpGetActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Search/Controller/Adminhtml/Term/Save.php b/app/code/Magento/Search/Controller/Adminhtml/Term/Save.php index cd9b1347ed1e..39ae07f0d353 100644 --- a/app/code/Magento/Search/Controller/Adminhtml/Term/Save.php +++ b/app/code/Magento/Search/Controller/Adminhtml/Term/Save.php @@ -5,13 +5,14 @@ */ namespace Magento\Search\Controller\Adminhtml\Term; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Backend\App\Action\Context; use Magento\Framework\Controller\ResultFactory; use Magento\Search\Model\QueryFactory; use Magento\Search\Controller\Adminhtml\Term as TermController; use Magento\Framework\Exception\LocalizedException; -class Save extends TermController +class Save extends TermController implements HttpPostActionInterface { /** * @var QueryFactory diff --git a/app/code/Magento/Search/Controller/Ajax/Suggest.php b/app/code/Magento/Search/Controller/Ajax/Suggest.php index 307f4df16324..6eab0949145b 100644 --- a/app/code/Magento/Search/Controller/Ajax/Suggest.php +++ b/app/code/Magento/Search/Controller/Ajax/Suggest.php @@ -5,12 +5,13 @@ */ namespace Magento\Search\Controller\Ajax; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Framework\App\Action\Action; use Magento\Framework\App\Action\Context; use Magento\Search\Model\AutocompleteInterface; use Magento\Framework\Controller\ResultFactory; -class Suggest extends Action +class Suggest extends Action implements HttpGetActionInterface { /** * @var \Magento\Search\Model\AutocompleteInterface diff --git a/app/code/Magento/Search/Model/EngineResolver.php b/app/code/Magento/Search/Model/EngineResolver.php index 720df0e0fda9..9e4ebf543635 100644 --- a/app/code/Magento/Search/Model/EngineResolver.php +++ b/app/code/Magento/Search/Model/EngineResolver.php @@ -10,6 +10,8 @@ use Psr\Log\LoggerInterface; /** + * Search engine resolver model. + * * @api * @since 100.1.0 */ @@ -61,6 +63,7 @@ class EngineResolver implements EngineResolverInterface /** * @param ScopeConfigInterface $scopeConfig * @param array $engines + * @param LoggerInterface $logger * @param string $path * @param string $scopeType * @param string $scopeCode diff --git a/app/code/Magento/Search/Model/Query.php b/app/code/Magento/Search/Model/Query.php index 365e03b3648a..d8c92a4801e8 100644 --- a/app/code/Magento/Search/Model/Query.php +++ b/app/code/Magento/Search/Model/Query.php @@ -5,7 +5,6 @@ */ namespace Magento\Search\Model; -use Magento\Framework\App\ResourceConnection; use Magento\Search\Model\ResourceModel\Query\Collection as QueryCollection; use Magento\Search\Model\ResourceModel\Query\CollectionFactory as QueryCollectionFactory; use Magento\Search\Model\SearchCollectionInterface as Collection; @@ -296,8 +295,7 @@ public function getMaxQueryLength() } /** - * @return string - * @codeCoverageIgnore + * @inheritdoc */ public function getQueryText() { @@ -305,6 +303,8 @@ public function getQueryText() } /** + * Check if query maximum length exceeded. + * * @return bool * @codeCoverageIgnore */ @@ -314,6 +314,8 @@ public function isQueryTextExceeded() } /** + * Check if minimum query length reached. + * * @return bool * @codeCoverageIgnore * @since 100.1.0 diff --git a/app/code/Magento/Search/Model/ResourceModel/SynonymReader.php b/app/code/Magento/Search/Model/ResourceModel/SynonymReader.php index 46e794a1954c..45eee0a4001d 100644 --- a/app/code/Magento/Search/Model/ResourceModel/SynonymReader.php +++ b/app/code/Magento/Search/Model/ResourceModel/SynonymReader.php @@ -87,7 +87,7 @@ private function queryByPhrase($phrase) { $matchQuery = $this->fullTextSelect->getMatchQuery( ['synonyms' => 'synonyms'], - $phrase, + $this->escapePhrase($phrase), Fulltext::FULLTEXT_MODE_BOOLEAN ); $query = $this->getConnection()->select()->from( @@ -97,6 +97,18 @@ private function queryByPhrase($phrase) return $this->getConnection()->fetchAll($query); } + /** + * Cut trailing plus or minus sign, and @ symbol, using of which causes InnoDB to report a syntax error. + * + * @see https://dev.mysql.com/doc/refman/5.7/en/fulltext-boolean.html + * @param string $phrase + * @return string + */ + private function escapePhrase(string $phrase): string + { + return preg_replace('/@+|[@+-]+$/', '', $phrase); + } + /** * A private helper function to retrieve matching synonym groups per scope * diff --git a/app/code/Magento/Search/Model/Search/PageSizeProvider.php b/app/code/Magento/Search/Model/Search/PageSizeProvider.php new file mode 100644 index 000000000000..5572bac6addc --- /dev/null +++ b/app/code/Magento/Search/Model/Search/PageSizeProvider.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Search\Model\Search; + +/** + * Returns max page size by search engine name + * @api + */ +class PageSizeProvider +{ + /** + * @var \Magento\Search\Model\EngineResolver + */ + private $engineResolver; + + /** + * @var array + */ + private $pageSizeBySearchEngine; + + /** + * @param \Magento\Search\Model\EngineResolver $engineResolver + * @param array $pageSizeBySearchEngine + */ + public function __construct( + \Magento\Search\Model\EngineResolver $engineResolver, + array $pageSizeBySearchEngine = [] + ) { + $this->engineResolver = $engineResolver; + $this->pageSizeBySearchEngine = $pageSizeBySearchEngine; + } + + /** + * Returns max_page_size depends on engine + * + * @return integer + */ + public function getMaxPageSize() : int + { + $searchEngine = $this->engineResolver->getCurrentSearchEngine(); + + $pageSize = PHP_INT_MAX; + if (isset($this->pageSizeBySearchEngine[$searchEngine])) { + $pageSize = $this->pageSizeBySearchEngine[$searchEngine]; + } + + return (int)$pageSize; + } +} diff --git a/app/code/Magento/Search/Model/SearchEngine/MenuBuilder.php b/app/code/Magento/Search/Model/SearchEngine/MenuBuilder.php deleted file mode 100644 index 800fd0b02edc..000000000000 --- a/app/code/Magento/Search/Model/SearchEngine/MenuBuilder.php +++ /dev/null @@ -1,70 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Search\Model\SearchEngine; - -use Magento\Backend\Model\Menu; -use Magento\Backend\Model\Menu\Builder; -use Magento\Framework\Search\EngineResolverInterface; -use Magento\Framework\Search\SearchEngine\ConfigInterface; - -/** - * A plugin for Magento\Backend\Model\Menu\Builder class. Implements "after" for "getResult()". - * - * The purpose of this plugin is to go through the menu tree and remove "Search Terms" menu item if the - * selected search engine does not support "synonyms" feature. - */ -class MenuBuilder -{ - /** - * A constant to refer to "Search Synonyms" menu item id from etc/adminhtml/menu.xml - */ - const SEARCH_SYNONYMS_MENU_ITEM_ID = 'Magento_Search::search_synonyms'; - - /** - * @var ConfigInterface $searchFeatureConfig - */ - protected $searchFeatureConfig; - - /** - * @var EngineResolverInterface $engineResolver - */ - protected $engineResolver; - - /** - * MenuBuilder constructor. - * - * @param ConfigInterface $searchFeatureConfig - * @param EngineResolverInterface $engineResolver - */ - public function __construct( - ConfigInterface $searchFeatureConfig, - EngineResolverInterface $engineResolver - ) { - $this->searchFeatureConfig = $searchFeatureConfig; - $this->engineResolver = $engineResolver; - } - - /** - * Removes 'Search Synonyms' from the menu if 'synonyms' is not supported - * - * @param Builder $subject - * @param Menu $menu - * @return Menu - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function afterGetResult(Builder $subject, Menu $menu) - { - $searchEngine = $this->engineResolver->getCurrentSearchEngine(); - if (!$this->searchFeatureConfig - ->isFeatureSupported(ConfigInterface::SEARCH_ENGINE_FEATURE_SYNONYMS, $searchEngine) - ) { - // "Search Synonyms" feature is not supported by the current configured search engine. - // Menu will be updated to remove it from the list - $menu->remove(self::SEARCH_SYNONYMS_MENU_ITEM_ID); - } - return $menu; - } -} diff --git a/app/code/Magento/Search/Test/Mftf/ActionGroup/AdminSearchTermActionGroup.xml b/app/code/Magento/Search/Test/Mftf/ActionGroup/AdminSearchTermActionGroup.xml new file mode 100644 index 000000000000..e0b3d4b850bb --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/ActionGroup/AdminSearchTermActionGroup.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Filter by search query and select --> + <actionGroup name="searchTermFilterBySearchQuery"> + <arguments> + <argument name="searchQuery" type="string"/> + </arguments> + <click selector="{{AdminCatalogSearchTermIndexSection.resetFilterButton}}" stepKey="clickOnResetButton"/> + <waitForPageLoad stepKey="waitForResetFilter"/> + <fillField selector="{{AdminCatalogSearchTermIndexSection.searchQuery}}" userInput="{{searchQuery}}" stepKey="fillSearchQuery"/> + <click selector="{{AdminCatalogSearchTermIndexSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForSearchResultLoad"/> + <checkOption selector="{{AdminCatalogSearchTermIndexSection.searchTermRowCheckboxBySearchQuery(searchQuery)}}" stepKey="checkCheckBox"/> + </actionGroup> + + <!-- Delete search term --> + <actionGroup name="deleteSearchTerm"> + <selectOption selector="{{AdminCatalogSearchTermIndexSection.massActions}}" userInput="delete" stepKey="selectDeleteOption"/> + <click selector="{{AdminCatalogSearchTermIndexSection.submit}}" stepKey="clickSubmitButton"/> + <click selector="{{AdminCatalogSearchTermIndexSection.okButton}}" stepKey="clickOkButton"/> + <waitForElementVisible selector="{{AdminCatalogSearchTermMessagesSection.successMessage}}" stepKey="waitForSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Search/Test/Mftf/Data/SearchTermData.xml b/app/code/Magento/Search/Test/Mftf/Data/SearchTermData.xml new file mode 100644 index 000000000000..1518adad0134 --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Data/SearchTermData.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="SearchTerm" type="searchTerm"> + <data key="query_text" unique="suffix">Query text</data> + <data key="store_id">1</data> + <data key="redirect" unique="suffix">http://example.com/</data> + <data key="display_in_terms">0</data> + </entity> +</entities> diff --git a/app/code/Magento/Search/Test/Mftf/Metadata/search_term-meta.xml b/app/code/Magento/Search/Test/Mftf/Metadata/search_term-meta.xml new file mode 100644 index 000000000000..0bd2dc9be485 --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Metadata/search_term-meta.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateSearchTerm" dataType="searchTerm" type="create" auth="adminFormKey" url="/search/term/save/" method="POST"> + <field key="query_text">string</field> + <field key="store_id">integer</field> + <field key="redirect">string</field> + <field key="display_in_terms">integer</field> + </operation> +</operations> diff --git a/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchResultsSection.xml b/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchResultsSection.xml new file mode 100644 index 000000000000..0bb929d88935 --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchResultsSection.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontQuickSearchResultsSection"> + <element name="searchTextBox" type="text" selector="#search"/> + <element name="searchTextBoxButton" type="button" selector="button[class='action search']"/> + <element name="allResults" type="block" selector=".column.main"/> + <element name="productLink" type="select" selector="a[class='product-item-link']"/> + <element name="productByIndex" type="button" selector=".product-items li:nth-child({{var}}) .product-item-info" parameterized="true"/> + <element name="productByName" type="button" selector="//div[contains(@class, 'product-item-info') and .//*[contains(., '{{var}}')]]" parameterized="true"/> + <element name="addToCartBtn" type="button" selector="//button[contains(@class, 'tocart')]"/> + <element name="messageSection" type="text" selector="div .message"/> + <element name="productSpecialPrice" type="text" selector="//a[contains(text(), '{{productName}}')]/ancestor::div//span[contains(@data-price-type, 'finalPrice')]/span[contains(@class, 'price')]" parameterized="true"/> + <element name="asLowAsLabel" type="text" selector=".minimal-price-link > span"/> + <element name="textArea" type="text" selector="li[class='item']"/> + <element name="regularPrice" type="text" selector="//span[@class='price-wrapper ']/span[@class='price']"/> + </section> +</sections> diff --git a/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchSection.xml b/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchSection.xml index 2b08e9b4b85e..3c2909b59c0d 100644 --- a/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchSection.xml +++ b/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchSection.xml @@ -11,5 +11,6 @@ <section name="StorefrontQuickSearchSection"> <element name="searchPhrase" type="input" selector="#search"/> <element name="searchButton" type="button" selector="button.action.search" timeout="30"/> + <element name="searchMiniForm" type="input" selector="#search_mini_form"/> </section> </sections> diff --git a/app/code/Magento/Search/Test/Mftf/Test/AdminMassDeleteSearchTermEntityTest.xml b/app/code/Magento/Search/Test/Mftf/Test/AdminMassDeleteSearchTermEntityTest.xml new file mode 100644 index 000000000000..67ccb51bf401 --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Test/AdminMassDeleteSearchTermEntityTest.xml @@ -0,0 +1,83 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMassDeleteSearchTermEntityTest"> + <annotations> + <features value="CatalogSearch"/> + <stories value="Delete search term"/> + <title value="Admin mass delete search term entity test"/> + <description value="Admin should be able to Mass Delete Search Term Entity Test"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14767"/> + <group value="searchFrontend"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create three search term --> + <createData entity="SearchTerm" stepKey="createFirstSearchTerm"/> + <createData entity="SearchTerm" stepKey="createSecondSearchTerm"/> + <createData entity="SearchTerm" stepKey="createThirdSearchTerm"/> + + <!-- Login as admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!-- Log out --> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Go to the catalog search term page --> + <amOnPage url="{{AdminCatalogSearchTermIndexPage.url}}" stepKey="openAdminCatalogSearchTermIndexPage"/> + <waitForPageLoad stepKey="waitForAdminCatalogSearchTermIndexPageLoad"/> + + <!-- Select all created below search terms --> + <actionGroup ref="searchTermFilterBySearchQuery" stepKey="filterByFirstSearchQuery"> + <argument name="searchQuery" value="$$createFirstSearchTerm.query_text$$"/> + </actionGroup> + <actionGroup ref="searchTermFilterBySearchQuery" stepKey="filterBySecondSearchQuery"> + <argument name="searchQuery" value="$$createSecondSearchTerm.query_text$$"/> + </actionGroup> + <actionGroup ref="searchTermFilterBySearchQuery" stepKey="filterByThirdSearchQuery"> + <argument name="searchQuery" value="$$createThirdSearchTerm.query_text$$"/> + </actionGroup> + + <!-- Delete created below search terms --> + <actionGroup ref="deleteSearchTerm" stepKey="deleteSearchTerms"/> + + <!-- Assert search terms are absent on the search term page --> + <actionGroup ref="AssertSearchTermNotInGrid" stepKey="assertFirstSearchTermNotInGrid"> + <argument name="searchQuery" value="$$createFirstSearchTerm.query_text$$"/> + </actionGroup> + <actionGroup ref="AssertSearchTermNotInGrid" stepKey="assertSecondSearchTermNotInGrid"> + <argument name="searchQuery" value="$$createSecondSearchTerm.query_text$$"/> + </actionGroup> + <actionGroup ref="AssertSearchTermNotInGrid" stepKey="assertThirdSearchTermNotInGrid"> + <argument name="searchQuery" value="$$createThirdSearchTerm.query_text$$"/> + </actionGroup> + + <!-- Go to storefront page --> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefrontPage"/> + <waitForPageLoad stepKey="waitForStorefrontPageLoad"/> + + <!-- Verify search term deletion on storefront --> + <actionGroup ref="StorefrontCheckQuickSearchActionGroup" stepKey="quickSearchForFirstSearchTerm"> + <argument name="phrase" value="$$createFirstSearchTerm.query_text$$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckSearchIsEmpty" stepKey="checkEmptyForFirstSearchTerm"/> + <actionGroup ref="StorefrontCheckQuickSearchActionGroup" stepKey="quickSearchForSecondSearchTerm"> + <argument name="phrase" value="$$createSecondSearchTerm.query_text$$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckSearchIsEmpty" stepKey="checkEmptyForSecondSearchTerm"/> + <actionGroup ref="StorefrontCheckQuickSearchActionGroup" stepKey="quickSearchForThirdSearchTerm"> + <argument name="phrase" value="$$createThirdSearchTerm.query_text$$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckSearchIsEmpty" stepKey="checkEmptyForThirdSearchTerm"/> + </test> +</tests> diff --git a/app/code/Magento/Search/Test/Unit/Model/PopularSearchTermsTest.php b/app/code/Magento/Search/Test/Unit/Model/PopularSearchTermsTest.php new file mode 100644 index 000000000000..849a0c067d45 --- /dev/null +++ b/app/code/Magento/Search/Test/Unit/Model/PopularSearchTermsTest.php @@ -0,0 +1,95 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Search\Test\Unit\Model; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Search\Model\PopularSearchTerms; +use Magento\Search\Model\ResourceModel\Query\Collection; +use Magento\Store\Model\ScopeInterface; + +/** + * @covers \Magento\Search\Model\PopularSearchTerms + */ +class PopularSearchTermsTest extends \PHPUnit\Framework\TestCase +{ + /** + * Testable Object + * + * @var PopularSearchTerms + */ + private $popularSearchTerms; + + /** + * @var ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $scopeConfigMock; + + /** + * @var Collection|\PHPUnit_Framework_MockObject_MockObject + */ + private $queryCollectionMock; + + /** + * Set Up + * + * @return void + */ + protected function setUp() + { + $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); + $this->queryCollectionMock = $this->createMock(Collection::class); + $this->popularSearchTerms = new PopularSearchTerms($this->scopeConfigMock, $this->queryCollectionMock); + } + + /** + * Test isCacheableDataProvider method + * + * @dataProvider isCacheableDataProvider + * + * @param string $term + * @param array $terms + * @param $expected $terms + * + * @return void + */ + public function testIsCacheable($term, $terms, $expected) + { + $storeId = 7; + $pageSize = 25; + + $this->scopeConfigMock->expects($this->once())->method('getValue') + ->with( + PopularSearchTerms::XML_PATH_MAX_COUNT_CACHEABLE_SEARCH_TERMS, + ScopeInterface::SCOPE_STORE, + $storeId + )->willReturn($pageSize); + $this->queryCollectionMock->expects($this->once())->method('setPopularQueryFilter')->with($storeId) + ->willReturnSelf(); + $this->queryCollectionMock->expects($this->once())->method('setPageSize')->with($pageSize) + ->willReturnSelf(); + $this->queryCollectionMock->expects($this->once())->method('load')->willReturnSelf(); + $this->queryCollectionMock->expects($this->once())->method('getColumnValues')->with('query_text') + ->willReturn($terms); + + $actual = $this->popularSearchTerms->isCacheable($term, $storeId); + self::assertEquals($expected, $actual); + } + + /** + * @return array + */ + public function isCacheableDataProvider() + { + return [ + ['test01', [], false], + ['test02', ['test01', 'test02'], true], + ['test03', ['test01', 'test02'], false], + ['test04', ['test04'], true], + ]; + } +} diff --git a/app/code/Magento/Search/Test/Unit/Model/Search/PageSizeProviderTest.php b/app/code/Magento/Search/Test/Unit/Model/Search/PageSizeProviderTest.php new file mode 100644 index 000000000000..982d00bd9f64 --- /dev/null +++ b/app/code/Magento/Search/Test/Unit/Model/Search/PageSizeProviderTest.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Search\Test\Unit\Model\Search; + +use Magento\Search\Model\Search\PageSizeProvider; + +class PageSizeProviderTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var PageSizeProvider + */ + private $model; + + /** + * @var \Magento\Search\Model\EngineResolver|\PHPUnit_Framework_MockObject_MockObject + */ + private $pageSizeBySearchEngineMock; + + public function setUp() + { + $this->pageSizeBySearchEngineMock = $this->getMockBuilder(\Magento\Search\Model\EngineResolver::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->model = new PageSizeProvider( + + $this->pageSizeBySearchEngineMock, + ['search' => 10, + 'catalogSearch3' => 11 + ] + ); + } + + /** + * @param string $searchEngine + * @param int $size + * @dataProvider getPageSizeDataProvider + */ + public function testGetPageSize($searchEngine, $size) + { + $this->pageSizeBySearchEngineMock + ->expects($this->once()) + ->method('getCurrentSearchEngine') + ->willReturn($searchEngine); + $this->assertEquals($size, $this->model->getMaxPageSize()); + } + + public function getPageSizeDataProvider() + { + return [ + ['search', 10], + ['catalogSearch3', 11], + ['newSearch', PHP_INT_MAX] + ]; + } +} diff --git a/app/code/Magento/Search/Test/Unit/Model/SearchEngine/MenuBuilderTest.php b/app/code/Magento/Search/Test/Unit/Model/SearchEngine/MenuBuilderTest.php deleted file mode 100644 index c8c521c200e1..000000000000 --- a/app/code/Magento/Search/Test/Unit/Model/SearchEngine/MenuBuilderTest.php +++ /dev/null @@ -1,61 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Search\Test\Unit\Model\SearchEngine; - -use Magento\Framework\Search\EngineResolverInterface; -use Magento\Framework\Search\SearchEngine\ConfigInterface; - -/** - * Class MenuBuilderTest. A unit test class to test functionality of - * Magento\Search\Model\SearchEngine\MenuBuilder class - */ -class MenuBuilderTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var ConfigInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $searchFeatureConfig; - - /** - * @var EngineResolverInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $engineResolver; - - protected function setUp() - { - $this->searchFeatureConfig = $this->createMock(\Magento\Search\Model\SearchEngine\Config::class); - $this->engineResolver = $this->createMock(EngineResolverInterface::class); - } - - public function testAfterGetResult() - { - $this->engineResolver->expects($this->once())->method('getCurrentSearchEngine')->willReturn('mysql'); - $this->searchFeatureConfig - ->expects($this->once()) - ->method('isFeatureSupported') - ->with('synonyms', 'mysql') - ->willReturn(false); - /** @var \Magento\Backend\Model\Menu $menu */ - $menu = $this->createMock(\Magento\Backend\Model\Menu::class); - $menu->expects($this->once())->method('remove')->willReturn(true); - - /** @var \Magento\Backend\Model\Menu\Builder $menuBuilder */ - $menuBuilder = $this->createMock(\Magento\Backend\Model\Menu\Builder::class); - $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - /** @var \Magento\Search\Model\SearchEngine\MenuBuilder $searchMenuBuilder */ - $searchMenuBuilder = $objectManager->getObject( - \Magento\Search\Model\SearchEngine\MenuBuilder::class, - [ - 'searchFeatureConfig' => $this->searchFeatureConfig, - 'engineResolver' => $this->engineResolver - ] - ); - $this->assertInstanceOf( - \Magento\Backend\Model\Menu::class, - $searchMenuBuilder->afterGetResult($menuBuilder, $menu) - ); - } -} diff --git a/app/code/Magento/Search/Test/Unit/Model/SynonymAnalyzerTest.php b/app/code/Magento/Search/Test/Unit/Model/SynonymAnalyzerTest.php new file mode 100644 index 000000000000..c452ab506a13 --- /dev/null +++ b/app/code/Magento/Search/Test/Unit/Model/SynonymAnalyzerTest.php @@ -0,0 +1,88 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Search\Test\Unit\Model; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + +/** + * Class SynonymAnalyzerTest + */ +class SynonymAnalyzerTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Search\Model\SynonymAnalyzer + */ + private $synonymAnalyzer; + + /** + * @var \Magento\Search\Model\SynonymReader |\PHPUnit_Framework_MockObject_MockObject + */ + private $synReaderModel; + + /** + * Test set up + */ + protected function setUp() + { + $helper = new ObjectManager($this); + + $this->synReaderModel = $this->getMockBuilder(\Magento\Search\Model\SynonymReader::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->synonymAnalyzer = $helper->getObject( + \Magento\Search\Model\SynonymAnalyzer::class, + [ + 'synReader' => $this->synReaderModel, + ] + ); + } + + /** + * @test + */ + public function testGetSynonymsForPhrase() + { + $phrase = 'Elizabeth is the british queen'; + $expected = [ + 0 => [ 0 => "Elizabeth" ], + 1 => [ 0 => "is" ], + 2 => [ 0 => "the" ], + 3 => [ 0 => "british", 1 => "english" ], + 4 => [ 0 => "queen", 1 => "monarch" ], + ]; + $this->synReaderModel->expects($this->once()) + ->method('loadByPhrase') + ->with($phrase) + ->willReturnSelf() + ; + $this->synReaderModel->expects($this->once()) + ->method('getData') + ->willReturn([ + ['synonyms' => 'british,english'], + ['synonyms' => 'queen,monarch'], + ]) + ; + + $actual = $this->synonymAnalyzer->getSynonymsForPhrase($phrase); + $this->assertEquals($expected, $actual); + } + + /** + * @test + * + * Empty phrase scenario + */ + public function testGetSynonymsForPhraseEmptyPhrase() + { + $phrase = ''; + $expected = []; + $actual = $this->synonymAnalyzer->getSynonymsForPhrase($phrase); + $this->assertEquals($expected, $actual); + } +} diff --git a/app/code/Magento/Search/Test/Unit/Model/SynonymGroupRepositoryTest.php b/app/code/Magento/Search/Test/Unit/Model/SynonymGroupRepositoryTest.php index f62c07b149c0..4532479c482b 100644 --- a/app/code/Magento/Search/Test/Unit/Model/SynonymGroupRepositoryTest.php +++ b/app/code/Magento/Search/Test/Unit/Model/SynonymGroupRepositoryTest.php @@ -53,7 +53,7 @@ public function testSaveCreate() /** * @expectedException \Magento\Search\Model\Synonym\MergeConflictException - * @expecteExceptionMessage (c,d,e) + * @expectedExceptionMessage Merge conflict with existing synonym group(s): (a,b,c) */ public function testSaveCreateMergeConflict() { @@ -138,7 +138,7 @@ public function testSaveUpdate() /** * @expectedException \Magento\Search\Model\Synonym\MergeConflictException - * @expecteExceptionMessage (d,h,i) + * @expectedExceptionMessage (d,h,i) */ public function testSaveUpdateMergeConflict() { diff --git a/app/code/Magento/Search/etc/db_schema.xml b/app/code/Magento/Search/etc/db_schema.xml index 9b2dfb493dbb..754af7d246d6 100644 --- a/app/code/Magento/Search/etc/db_schema.xml +++ b/app/code/Magento/Search/etc/db_schema.xml @@ -26,24 +26,24 @@ default="0" comment="Processed status"/> <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" comment="Updated at"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="query_id"/> </constraint> - <constraint xsi:type="foreign" name="SEARCH_QUERY_STORE_ID_STORE_STORE_ID" table="search_query" + <constraint xsi:type="foreign" referenceId="SEARCH_QUERY_STORE_ID_STORE_STORE_ID" table="search_query" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="SEARCH_QUERY_QUERY_TEXT_STORE_ID"> + <constraint xsi:type="unique" referenceId="SEARCH_QUERY_QUERY_TEXT_STORE_ID"> <column name="query_text"/> <column name="store_id"/> </constraint> - <index name="SEARCH_QUERY_QUERY_TEXT_STORE_ID_POPULARITY" indexType="btree"> + <index referenceId="SEARCH_QUERY_QUERY_TEXT_STORE_ID_POPULARITY" indexType="btree"> <column name="query_text"/> <column name="store_id"/> <column name="popularity"/> </index> - <index name="SEARCH_QUERY_STORE_ID" indexType="btree"> + <index referenceId="SEARCH_QUERY_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="SEARCH_QUERY_IS_PROCESSED" indexType="btree"> + <index referenceId="SEARCH_QUERY_IS_PROCESSED" indexType="btree"> <column name="is_processed"/> </index> </table> @@ -55,21 +55,21 @@ default="0" comment="Store Id - identifies the store view these synonyms belong to"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Website Id - identifies the website id these synonyms belong to"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="group_id"/> </constraint> - <constraint xsi:type="foreign" name="SEARCH_SYNONYMS_STORE_ID_STORE_STORE_ID" table="search_synonyms" + <constraint xsi:type="foreign" referenceId="SEARCH_SYNONYMS_STORE_ID_STORE_STORE_ID" table="search_synonyms" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="SEARCH_SYNONYMS_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID" + <constraint xsi:type="foreign" referenceId="SEARCH_SYNONYMS_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID" table="search_synonyms" column="website_id" referenceTable="store_website" referenceColumn="website_id" onDelete="CASCADE"/> - <index name="SEARCH_SYNONYMS_SYNONYMS" indexType="fulltext"> + <index referenceId="SEARCH_SYNONYMS_SYNONYMS" indexType="fulltext"> <column name="synonyms"/> </index> - <index name="SEARCH_SYNONYMS_STORE_ID" indexType="btree"> + <index referenceId="SEARCH_SYNONYMS_STORE_ID" indexType="btree"> <column name="store_id"/> </index> - <index name="SEARCH_SYNONYMS_WEBSITE_ID" indexType="btree"> + <index referenceId="SEARCH_SYNONYMS_WEBSITE_ID" indexType="btree"> <column name="website_id"/> </index> </table> diff --git a/app/code/Magento/Search/etc/di.xml b/app/code/Magento/Search/etc/di.xml index 0897dcfb190b..4aa211d23f0a 100755 --- a/app/code/Magento/Search/etc/di.xml +++ b/app/code/Magento/Search/etc/di.xml @@ -86,7 +86,4 @@ <argument name="dataStorage" xsi:type="object">Magento\Search\Model\SearchEngine\Config\Data</argument> </arguments> </type> - <type name="Magento\Backend\Model\Menu\Builder"> - <plugin name="SearchTermMenuBuilder" type="Magento\Search\Model\SearchEngine\MenuBuilder" /> - </type> </config> diff --git a/app/code/Magento/Search/i18n/de_DE.csv b/app/code/Magento/Search/i18n/de_DE.csv deleted file mode 100644 index 8b4b04aa3b9e..000000000000 --- a/app/code/Magento/Search/i18n/de_DE.csv +++ /dev/null @@ -1 +0,0 @@ -"Search Engine","Search Engine" diff --git a/app/code/Magento/Search/i18n/es_ES.csv b/app/code/Magento/Search/i18n/es_ES.csv deleted file mode 100644 index 8b4b04aa3b9e..000000000000 --- a/app/code/Magento/Search/i18n/es_ES.csv +++ /dev/null @@ -1 +0,0 @@ -"Search Engine","Search Engine" diff --git a/app/code/Magento/Search/i18n/fr_FR.csv b/app/code/Magento/Search/i18n/fr_FR.csv deleted file mode 100644 index 8b4b04aa3b9e..000000000000 --- a/app/code/Magento/Search/i18n/fr_FR.csv +++ /dev/null @@ -1 +0,0 @@ -"Search Engine","Search Engine" diff --git a/app/code/Magento/Search/i18n/nl_NL.csv b/app/code/Magento/Search/i18n/nl_NL.csv deleted file mode 100644 index 8b4b04aa3b9e..000000000000 --- a/app/code/Magento/Search/i18n/nl_NL.csv +++ /dev/null @@ -1 +0,0 @@ -"Search Engine","Search Engine" diff --git a/app/code/Magento/Search/i18n/pt_BR.csv b/app/code/Magento/Search/i18n/pt_BR.csv deleted file mode 100644 index c10566a7c980..000000000000 --- a/app/code/Magento/Search/i18n/pt_BR.csv +++ /dev/null @@ -1 +0,0 @@ -"Search Engine","Mecanismo de Busca" diff --git a/app/code/Magento/Search/i18n/zh_Hans_CN.csv b/app/code/Magento/Search/i18n/zh_Hans_CN.csv deleted file mode 100644 index 8b4b04aa3b9e..000000000000 --- a/app/code/Magento/Search/i18n/zh_Hans_CN.csv +++ /dev/null @@ -1 +0,0 @@ -"Search Engine","Search Engine" diff --git a/app/code/Magento/Search/view/adminhtml/layout/search_term_block.xml b/app/code/Magento/Search/view/adminhtml/layout/search_term_block.xml index cc2e1509e441..9dc60bde1131 100644 --- a/app/code/Magento/Search/view/adminhtml/layout/search_term_block.xml +++ b/app/code/Magento/Search/view/adminhtml/layout/search_term_block.xml @@ -11,7 +11,7 @@ <block class="Magento\Backend\Block\Widget\Grid" name="adminhtml.report.search.grid" as="grid"> <arguments> <argument name="id" xsi:type="string">searchReportGrid</argument> - <argument name="dataSource" xsi:type="object">Magento\Search\Model\ResourceModel\Query\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Search\Model\ResourceModel\Query\Collection</argument> <argument name="default_sort" xsi:type="string">query_id</argument> <argument name="default_dir" xsi:type="string">DESC</argument> </arguments> diff --git a/app/code/Magento/Search/view/adminhtml/layout/search_term_grid_block.xml b/app/code/Magento/Search/view/adminhtml/layout/search_term_grid_block.xml index 7330ac1f9271..38c6fa52455b 100644 --- a/app/code/Magento/Search/view/adminhtml/layout/search_term_grid_block.xml +++ b/app/code/Magento/Search/view/adminhtml/layout/search_term_grid_block.xml @@ -11,7 +11,7 @@ <block class="Magento\Backend\Block\Widget\Grid" name="adminhtml.catalog.search.grid" as="grid"> <arguments> <argument name="id" xsi:type="string">search_term_grid</argument> - <argument name="dataSource" xsi:type="object">Magento\Search\Model\ResourceModel\Query\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Search\Model\ResourceModel\Query\Collection</argument> <argument name="default_sort" xsi:type="string">name</argument> <argument name="default_dir" xsi:type="string">ASC</argument> <argument name="save_parameters_in_session" xsi:type="string">1</argument> diff --git a/app/code/Magento/Search/view/adminhtml/layout/search_term_report_block.xml b/app/code/Magento/Search/view/adminhtml/layout/search_term_report_block.xml index b6b1455cf27c..f4c5d1ab85a0 100644 --- a/app/code/Magento/Search/view/adminhtml/layout/search_term_report_block.xml +++ b/app/code/Magento/Search/view/adminhtml/layout/search_term_report_block.xml @@ -12,7 +12,7 @@ <block class="Magento\Backend\Block\Widget\Grid" name="adminhtml.report.search.grid" as="grid"> <arguments> <argument name="id" xsi:type="string">searchReportGrid</argument> - <argument name="dataSource" xsi:type="object">Magento\Search\Model\ResourceModel\Query\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Search\Model\ResourceModel\Query\Collection</argument> <argument name="default_sort" xsi:type="string">query_id</argument> <argument name="default_dir" xsi:type="string">DESC</argument> </arguments> diff --git a/app/code/Magento/Search/view/frontend/requirejs-config.js b/app/code/Magento/Search/view/frontend/requirejs-config.js index c38cba4315ea..d945944daa1b 100644 --- a/app/code/Magento/Search/view/frontend/requirejs-config.js +++ b/app/code/Magento/Search/view/frontend/requirejs-config.js @@ -6,7 +6,8 @@ var config = { map: { '*': { - quickSearch: 'Magento_Search/form-mini' + quickSearch: 'Magento_Search/js/form-mini', + 'Magento_Search/form-mini': 'Magento_Search/js/form-mini' } } }; diff --git a/app/code/Magento/Search/view/frontend/templates/form.mini.phtml b/app/code/Magento/Search/view/frontend/templates/form.mini.phtml index 2ea87be13d5e..44c8db3f1a66 100644 --- a/app/code/Magento/Search/view/frontend/templates/form.mini.phtml +++ b/app/code/Magento/Search/view/frontend/templates/form.mini.phtml @@ -42,8 +42,10 @@ $helper = $this->helper(\Magento\Search\Helper\Data::class); </div> <div class="actions"> <button type="submit" - title="<?= $block->escapeHtml(__('Search')) ?>" - class="action search"> + title="<?= $block->escapeHtml(__('Search')) ?>" + class="action search" + aria-label="Search" + > <span><?= /* @escapeNotVerified */ __('Search') ?></span> </button> </div> diff --git a/app/code/Magento/Search/view/frontend/web/form-mini.js b/app/code/Magento/Search/view/frontend/web/js/form-mini.js similarity index 95% rename from app/code/Magento/Search/view/frontend/web/form-mini.js rename to app/code/Magento/Search/view/frontend/web/js/form-mini.js index 27a15017cb3f..5331ba3b447a 100644 --- a/app/code/Magento/Search/view/frontend/web/form-mini.js +++ b/app/code/Magento/Search/view/frontend/web/js/form-mini.js @@ -43,7 +43,8 @@ define([ '</li>', submitBtn: 'button[type="submit"]', searchLabel: '[data-role=minisearch-label]', - isExpandable: null + isExpandable: null, + suggestionDelay: 300 }, /** @inheritdoc */ @@ -71,7 +72,6 @@ define([ }.bind(this), exit: function () { this.isExpandable = false; - this.element.removeAttr('aria-expanded'); }.bind(this) }); @@ -104,7 +104,8 @@ define([ this.element.on('focus', this.setActiveState.bind(this, true)); this.element.on('keydown', this._onKeyDown); - this.element.on('input propertychange', this._onPropertyChange); + // Prevent spamming the server with requests by waiting till the user has stopped typing for period of time + this.element.on('input propertychange', _.debounce(this._onPropertyChange, this.options.suggestionDelay)); this.searchForm.on('submit', $.proxy(function (e) { this._onSubmit(e); @@ -304,12 +305,13 @@ define([ dropdown.append(html); }); + this._resetResponseList(true); + this.responseList.indexList = this.autoComplete.html(dropdown) .css(clonePosition) .show() .find(this.options.responseFieldElements + ':visible'); - this._resetResponseList(false); this.element.removeAttr('aria-activedescendant'); if (this.responseList.indexList.length) { @@ -336,6 +338,11 @@ define([ this._resetResponseList(false); } }.bind(this)); + } else { + this._resetResponseList(true); + this.autoComplete.hide(); + this._updateAriaHasPopup(false); + this.element.removeAttr('aria-activedescendant'); } }, this)); } else { diff --git a/app/code/Magento/Security/Model/SecurityChecker/Quantity.php b/app/code/Magento/Security/Model/SecurityChecker/Quantity.php index 9d86b55158be..5d72ba261f31 100644 --- a/app/code/Magento/Security/Model/SecurityChecker/Quantity.php +++ b/app/code/Magento/Security/Model/SecurityChecker/Quantity.php @@ -48,13 +48,13 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function check($securityEventType, $accountReference = null, $longIp = null) { $isEnabled = $this->securityConfig->getPasswordResetProtectionType() != ResetMethod::OPTION_NONE; $allowedAttemptsNumber = $this->securityConfig->getMaxNumberPasswordResetRequests(); - if ($isEnabled and $allowedAttemptsNumber) { + if ($isEnabled && $allowedAttemptsNumber) { $collection = $this->prepareCollection($securityEventType, $accountReference, $longIp); if ($collection->count() >= $allowedAttemptsNumber) { throw new SecurityViolationException( diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminUserLockWhenCreatingNewUserTest.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminUserLockWhenCreatingNewUserTest.xml new file mode 100644 index 000000000000..4ceffd676313 --- /dev/null +++ b/app/code/Magento/Security/Test/Mftf/Test/AdminUserLockWhenCreatingNewUserTest.xml @@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUserLockWhenCreatingNewUserTest"> + <annotations> + <features value="Security"/> + <stories value="Runs Lock admin user when creating new user test."/> + <title value="Lock admin user when creating new user"/> + <description value="Runs Lock admin user when creating new user test."/> + <testCaseId value="MC-14383" /> + <severity value="CRITICAL"/> + <group value="security"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Log in to Admin Panel --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!-- Unlock Admin user --> + <magentoCLI command="admin:user:unlock {{DefaultAdminUser.username}}" stepKey="unlockAdminUser"/> + </after> + + <!-- Open Admin New User Page --> + <actionGroup ref="AdminOpenNewUserPageActionGroup" stepKey="openNewUserPage" /> + + <!-- Perform add new admin user 6 specified number of times. + "The password entered for the current user is invalid. Verify the password and try again." appears after each attempt.--> + <actionGroup ref="AdminFillNewUserFormRequiredFieldsActionGroup" stepKey="failedSaveUserFirstAttempt"> + <argument name="user" value="NewAdminUserWrongCurrentPassword" /> + </actionGroup> + <actionGroup ref="AdminClickSaveButtonOnUserFormActionGroup" stepKey="clickSaveFirstAttempt" /> + <actionGroup ref="AssertAdminUserSaveMessageActionGroup" stepKey="seeInvalidPasswordError"> + <argument name="message" value="The password entered for the current user is invalid. Verify the password and try again." /> + <argument name="messageType" value="error" /> + </actionGroup> + + <actionGroup ref="AdminFillNewUserFormRequiredFieldsActionGroup" stepKey="failedSaveUserSecondAttempt"> + <argument name="user" value="NewAdminUserWrongCurrentPassword" /> + </actionGroup> + <actionGroup ref="AdminClickSaveButtonOnUserFormActionGroup" stepKey="clickSaveSecondAttempt" /> + + <actionGroup ref="AdminFillNewUserFormRequiredFieldsActionGroup" stepKey="failedSaveUserThirdAttempt"> + <argument name="user" value="NewAdminUserWrongCurrentPassword" /> + </actionGroup> + <actionGroup ref="AdminClickSaveButtonOnUserFormActionGroup" stepKey="clickSaveThirdAttempt" /> + + <actionGroup ref="AdminFillNewUserFormRequiredFieldsActionGroup" stepKey="failedSaveUserFourthAttempt"> + <argument name="user" value="NewAdminUserWrongCurrentPassword" /> + </actionGroup> + <actionGroup ref="AdminClickSaveButtonOnUserFormActionGroup" stepKey="clickSaveFourthAttempt" /> + + <actionGroup ref="AdminFillNewUserFormRequiredFieldsActionGroup" stepKey="failedSaveUserFifthAttempt"> + <argument name="user" value="NewAdminUserWrongCurrentPassword" /> + </actionGroup> + <actionGroup ref="AdminClickSaveButtonOnUserFormActionGroup" stepKey="clickSaveFifthAttempt" /> + + <actionGroup ref="AdminFillNewUserFormRequiredFieldsActionGroup" stepKey="failedSaveUserSixthAttempt"> + <argument name="user" value="NewAdminUserWrongCurrentPassword" /> + </actionGroup> + <actionGroup ref="AdminClickSaveButtonOnUserFormActionGroup" stepKey="clickSaveSixthAttempt" /> + + <!-- Check Error that account has been locked --> + <actionGroup ref="AssertMessageOnAdminLoginActionGroup" stepKey="seeLockUserErrorMessage"> + <argument name="message" value="Your account is temporarily disabled. Please try again later." /> + </actionGroup> + + <!-- Try to login as admin and check error --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsLockedAdmin"/> + <actionGroup ref="AssertMessageOnAdminLoginActionGroup" stepKey="seeLoginUserErrorMessage" /> + </test> +</tests> diff --git a/app/code/Magento/Security/Test/Unit/Model/SecurityCookieTest.php b/app/code/Magento/Security/Test/Unit/Model/SecurityCookieTest.php index b310bf63bc98..3a1855b3a220 100644 --- a/app/code/Magento/Security/Test/Unit/Model/SecurityCookieTest.php +++ b/app/code/Magento/Security/Test/Unit/Model/SecurityCookieTest.php @@ -87,7 +87,7 @@ public function testGetLogoutReasonCookie() ) ->willReturn($cookie); - $this->assertEquals(intval($cookie), $this->model->getLogoutReasonCookie()); + $this->assertEquals((int)$cookie, $this->model->getLogoutReasonCookie()); } /** @@ -114,7 +114,7 @@ public function testSetLogoutReasonCookie() ->method('setPublicCookie') ->with( SecurityCookie::LOGOUT_REASON_CODE_COOKIE_NAME, - intval($status), + (int)$status, $this->cookieMetadataMock ) ->willReturnSelf(); diff --git a/app/code/Magento/Security/etc/db_schema.xml b/app/code/Magento/Security/etc/db_schema.xml index f14e6de79e89..ce7143582ce6 100644 --- a/app/code/Magento/Security/etc/db_schema.xml +++ b/app/code/Magento/Security/etc/db_schema.xml @@ -21,15 +21,15 @@ comment="Update Time"/> <column xsi:type="varchar" name="ip" nullable="false" length="15" onCreate="migrateDataFrom(ip)" comment="Remote user IP"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> </constraint> - <constraint xsi:type="foreign" name="ADMIN_USER_SESSION_USER_ID_ADMIN_USER_USER_ID" table="admin_user_session" + <constraint xsi:type="foreign" referenceId="ADMIN_USER_SESSION_USER_ID_ADMIN_USER_USER_ID" table="admin_user_session" column="user_id" referenceTable="admin_user" referenceColumn="user_id" onDelete="CASCADE"/> - <index name="ADMIN_USER_SESSION_SESSION_ID" indexType="btree"> + <index referenceId="ADMIN_USER_SESSION_SESSION_ID" indexType="btree"> <column name="session_id"/> </index> - <index name="ADMIN_USER_SESSION_USER_ID" indexType="btree"> + <index referenceId="ADMIN_USER_SESSION_USER_ID" indexType="btree"> <column name="user_id"/> </index> </table> @@ -45,13 +45,13 @@ comment="Timestamp when the event occurs"/> <column xsi:type="varchar" name="ip" nullable="false" length="15" onCreate="migrateDataFrom(ip)" comment="Remote user IP"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> </constraint> - <index name="PASSWORD_RESET_REQUEST_EVENT_ACCOUNT_REFERENCE" indexType="btree"> + <index referenceId="PASSWORD_RESET_REQUEST_EVENT_ACCOUNT_REFERENCE" indexType="btree"> <column name="account_reference"/> </index> - <index name="PASSWORD_RESET_REQUEST_EVENT_CREATED_AT" indexType="btree"> + <index referenceId="PASSWORD_RESET_REQUEST_EVENT_CREATED_AT" indexType="btree"> <column name="created_at"/> </index> </table> diff --git a/app/code/Magento/SendFriend/Block/Send.php b/app/code/Magento/SendFriend/Block/Send.php index 43e95ebe43d4..1c4b55036135 100644 --- a/app/code/Magento/SendFriend/Block/Send.php +++ b/app/code/Magento/SendFriend/Block/Send.php @@ -5,6 +5,7 @@ */ namespace Magento\SendFriend\Block; +use Magento\Captcha\Block\Captcha; use Magento\Customer\Model\Context; /** @@ -170,6 +171,7 @@ public function setFormData($data) /** * Retrieve Current Product Id * + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) * @return int */ public function getProductId() @@ -180,6 +182,7 @@ public function getProductId() /** * Retrieve current category id for product * + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) * @return int */ public function getCategoryId() @@ -222,4 +225,24 @@ public function canSend() { return !$this->sendfriend->isExceedLimit(); } + + /** + * @inheritdoc + */ + protected function _prepareLayout() + { + if (!$this->getChildBlock('captcha')) { + $this->addChild( + 'captcha', + Captcha::class, + [ + 'cacheable' => false, + 'after' => '-', + 'form_id' => 'product_sendtofriend_form', + 'image_width' => 230, + 'image_height' => 230 + ] + ); + } + } } diff --git a/app/code/Magento/SendFriend/Controller/Product/Send.php b/app/code/Magento/SendFriend/Controller/Product/Send.php index 8b0ae8dcf438..d9fef595ce2a 100644 --- a/app/code/Magento/SendFriend/Controller/Product/Send.php +++ b/app/code/Magento/SendFriend/Controller/Product/Send.php @@ -5,9 +5,13 @@ */ namespace Magento\SendFriend\Controller\Product; +use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Framework\Controller\ResultFactory; -class Send extends \Magento\SendFriend\Controller\Product +/** + * Controller class. Represents rendering and request flow + */ +class Send extends \Magento\SendFriend\Controller\Product implements HttpGetActionInterface { /** * @var \Magento\Catalog\Model\Session diff --git a/app/code/Magento/SendFriend/Controller/Product/Sendmail.php b/app/code/Magento/SendFriend/Controller/Product/Sendmail.php index 4b1f724cb83a..696c23589937 100644 --- a/app/code/Magento/SendFriend/Controller/Product/Sendmail.php +++ b/app/code/Magento/SendFriend/Controller/Product/Sendmail.php @@ -6,10 +6,18 @@ namespace Magento\SendFriend\Controller\Product; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Controller\ResultFactory; +use Magento\SendFriend\Model\CaptchaValidator; -class Sendmail extends \Magento\SendFriend\Controller\Product +/** + * Class Sendmail. Represents request flow logic of 'sendmail' feature + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class Sendmail extends \Magento\SendFriend\Controller\Product implements HttpPostActionInterface { /** * @var \Magento\Catalog\Api\CategoryRepositoryInterface @@ -22,6 +30,13 @@ class Sendmail extends \Magento\SendFriend\Controller\Product protected $catalogSession; /** + * @var CaptchaValidator + */ + private $captchaValidator; + + /** + * Sendmail class construct + * * @param \Magento\Framework\App\Action\Context $context * @param \Magento\Framework\Registry $coreRegistry * @param \Magento\Framework\Data\Form\FormKey\Validator $formKeyValidator @@ -29,6 +44,7 @@ class Sendmail extends \Magento\SendFriend\Controller\Product * @param \Magento\Catalog\Api\ProductRepositoryInterface $productRepository * @param \Magento\Catalog\Api\CategoryRepositoryInterface $categoryRepository * @param \Magento\Catalog\Model\Session $catalogSession + * @param CaptchaValidator|null $captchaValidator */ public function __construct( \Magento\Framework\App\Action\Context $context, @@ -37,11 +53,13 @@ public function __construct( \Magento\SendFriend\Model\SendFriend $sendFriend, \Magento\Catalog\Api\ProductRepositoryInterface $productRepository, \Magento\Catalog\Api\CategoryRepositoryInterface $categoryRepository, - \Magento\Catalog\Model\Session $catalogSession + \Magento\Catalog\Model\Session $catalogSession, + CaptchaValidator $captchaValidator = null ) { parent::__construct($context, $coreRegistry, $formKeyValidator, $sendFriend, $productRepository); $this->categoryRepository = $categoryRepository; $this->catalogSession = $catalogSession; + $this->captchaValidator = $captchaValidator ?: ObjectManager::getInstance()->create(CaptchaValidator::class); } /** @@ -49,17 +67,13 @@ public function __construct( * * @return \Magento\Framework\Controller\ResultInterface * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) */ public function execute() { /** @var \Magento\Framework\Controller\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); - if (!$this->_formKeyValidator->validate($this->getRequest())) { - $resultRedirect->setPath('sendfriend/product/send', ['_current' => true]); - return $resultRedirect; - } - $product = $this->_initProduct(); $data = $this->getRequest()->getPostValue(); @@ -89,6 +103,9 @@ public function execute() try { $validate = $this->sendFriend->validate(); + + $this->captchaValidator->validateSending($this->getRequest()); + if ($validate === true) { $this->sendFriend->send(); $this->messageManager->addSuccess(__('The link to a friend was sent.')); diff --git a/app/code/Magento/SendFriend/Model/CaptchaValidator.php b/app/code/Magento/SendFriend/Model/CaptchaValidator.php new file mode 100644 index 000000000000..11fbbdf72f6d --- /dev/null +++ b/app/code/Magento/SendFriend/Model/CaptchaValidator.php @@ -0,0 +1,123 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\SendFriend\Model; + +use Magento\Framework\App\RequestInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Captcha\Helper\Data; +use Magento\Captcha\Model\DefaultModel; +use Magento\Captcha\Observer\CaptchaStringResolver; +use Magento\Authorization\Model\UserContextInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; + +/** + * Class CaptchaValidator. Performs captcha validation + */ +class CaptchaValidator +{ + /** + * @var Data + */ + private $captchaHelper; + + /** + * @var CaptchaStringResolver + */ + private $captchaStringResolver; + + /** + * @var UserContextInterface + */ + private $currentUser; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * CaptchaValidator constructor. + * + * @param Data $captchaHelper + * @param CaptchaStringResolver $captchaStringResolver + * @param UserContextInterface $currentUser + * @param CustomerRepositoryInterface $customerRepository + */ + public function __construct( + Data $captchaHelper, + CaptchaStringResolver $captchaStringResolver, + UserContextInterface $currentUser, + CustomerRepositoryInterface $customerRepository + ) { + $this->captchaHelper = $captchaHelper; + $this->captchaStringResolver = $captchaStringResolver; + $this->currentUser = $currentUser; + $this->customerRepository = $customerRepository; + } + + /** + * Entry point for captcha validation + * + * @param RequestInterface $request + * @throws LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function validateSending(RequestInterface $request): void + { + $this->validateCaptcha($request); + } + + /** + * Validates captcha and triggers log attempt + * + * @param RequestInterface $request + * @throws LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function validateCaptcha(RequestInterface $request): void + { + $captchaTargetFormName = 'product_sendtofriend_form'; + /** @var DefaultModel $captchaModel */ + $captchaModel = $this->captchaHelper->getCaptcha($captchaTargetFormName); + + if ($captchaModel->isRequired()) { + $word = $this->captchaStringResolver->resolve( + $request, + $captchaTargetFormName + ); + + $isCorrectCaptcha = $captchaModel->isCorrect($word); + + if (!$isCorrectCaptcha) { + $this->logCaptchaAttempt($captchaModel); + throw new LocalizedException(__('Incorrect CAPTCHA')); + } + } + + $this->logCaptchaAttempt($captchaModel); + } + + /** + * Log captcha attempts + * + * @param DefaultModel $captchaModel + * @throws LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function logCaptchaAttempt(DefaultModel $captchaModel): void + { + $email = ''; + + if ($this->currentUser->getUserType() == UserContextInterface::USER_TYPE_CUSTOMER) { + $email = $this->customerRepository->getById($this->currentUser->getUserId())->getEmail(); + } + + $captchaModel->logAttempt($email); + } +} diff --git a/app/code/Magento/SendFriend/Model/SendFriend.php b/app/code/Magento/SendFriend/Model/SendFriend.php index c69d6342b489..825cac2bae10 100644 --- a/app/code/Magento/SendFriend/Model/SendFriend.php +++ b/app/code/Magento/SendFriend/Model/SendFriend.php @@ -16,6 +16,7 @@ * @method \Magento\SendFriend\Model\SendFriend setTime(int $value) * * @author Magento Core Team <core@magentocommerce.com> + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * * @api @@ -162,6 +163,8 @@ protected function _construct() } /** + * Sends email to recipients + * * @return $this * @throws CoreException */ @@ -236,7 +239,7 @@ public function validate() } $email = $this->getSender()->getEmail(); - if (empty($email) or !\Zend_Validate::is($email, \Magento\Framework\Validator\EmailAddress::class)) { + if (empty($email) || !\Zend_Validate::is($email, \Magento\Framework\Validator\EmailAddress::class)) { $errors[] = __('Invalid Sender Email'); } @@ -281,13 +284,13 @@ public function setRecipients($recipients) // validate array if (!is_array( $recipients - ) or !isset( + ) || !isset( $recipients['email'] - ) or !isset( + ) || !isset( $recipients['name'] - ) or !is_array( + ) || !is_array( $recipients['email'] - ) or !is_array( + ) || !is_array( $recipients['name'] ) ) { @@ -487,7 +490,7 @@ protected function _sentCountByCookies($increment = false) $oldTimes = explode(',', $oldTimes); foreach ($oldTimes as $oldTime) { $periodTime = $time - $this->_sendfriendData->getPeriod(); - if (is_numeric($oldTime) and $oldTime >= $periodTime) { + if (is_numeric($oldTime) && $oldTime >= $periodTime) { $newTimes[] = $oldTime; } } diff --git a/app/code/Magento/SendFriend/Test/Unit/Controller/Product/SendTest.php b/app/code/Magento/SendFriend/Test/Unit/Controller/Product/SendTest.php deleted file mode 100644 index 9d48133c1d50..000000000000 --- a/app/code/Magento/SendFriend/Test/Unit/Controller/Product/SendTest.php +++ /dev/null @@ -1,423 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\SendFriend\Test\Unit\Controller\Product; - -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; - -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class SendTest extends \PHPUnit\Framework\TestCase -{ - /** @var \Magento\SendFriend\Controller\Product\Send */ - protected $model; - - /** @var ObjectManagerHelper */ - protected $objectManagerHelper; - - /** @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $requestMock; - - /** @var \Magento\Framework\Registry|\PHPUnit_Framework_MockObject_MockObject */ - protected $registryMock; - - /** @var \Magento\Framework\Data\Form\FormKey\Validator|\PHPUnit_Framework_MockObject_MockObject */ - protected $validatorMock; - - /** @var \Magento\SendFriend\Model\SendFriend|\PHPUnit_Framework_MockObject_MockObject */ - protected $sendFriendMock; - - /** @var \Magento\Catalog\Api\ProductRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $productRepositoryMock; - - /** @var \Magento\Catalog\Model\Session|\PHPUnit_Framework_MockObject_MockObject */ - protected $catalogSessionMock; - - /** @var \Magento\Framework\Message\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $messageManagerMock; - - /** @var \Magento\Framework\Controller\ResultFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $resultFactoryMock; - - /** @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $eventManagerMock; - - protected function setUp() - { - $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) - ->getMockForAbstractClass(); - $this->registryMock = $this->getMockBuilder(\Magento\Framework\Registry::class) - ->disableOriginalConstructor() - ->getMock(); - $this->validatorMock = $this->getMockBuilder(\Magento\Framework\Data\Form\FormKey\Validator::class) - ->disableOriginalConstructor() - ->getMock(); - $this->sendFriendMock = $this->getMockBuilder(\Magento\SendFriend\Model\SendFriend::class) - ->disableOriginalConstructor() - ->getMock(); - $this->productRepositoryMock = $this->getMockBuilder(\Magento\Catalog\Api\ProductRepositoryInterface::class) - ->getMockForAbstractClass(); - $this->catalogSessionMock = $this->getMockBuilder(\Magento\Catalog\Model\Session::class) - ->setMethods(['getSendfriendFormData', 'setSendfriendFormData']) - ->disableOriginalConstructor() - ->getMock(); - $this->messageManagerMock = $this->getMockBuilder(\Magento\Framework\Message\ManagerInterface::class) - ->getMock(); - $this->resultFactoryMock = $this->getMockBuilder(\Magento\Framework\Controller\ResultFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $this->eventManagerMock = $this->getMockBuilder(\Magento\Framework\Event\ManagerInterface::class) - ->getMock(); - - $this->objectManagerHelper = new ObjectManagerHelper($this); - $this->model = $this->objectManagerHelper->getObject( - \Magento\SendFriend\Controller\Product\Send::class, - [ - 'request' => $this->requestMock, - 'coreRegistry' => $this->registryMock, - 'formKeyValidator' => $this->validatorMock, - 'sendFriend' => $this->sendFriendMock, - 'productRepository' => $this->productRepositoryMock, - 'catalogSession' => $this->catalogSessionMock, - 'messageManager' => $this->messageManagerMock, - 'resultFactory' => $this->resultFactoryMock, - 'eventManager' => $this->eventManagerMock, - ] - ); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecute() - { - $productId = 11; - $formData = ['some' => 'data']; - - $this->requestMock->expects($this->once()) - ->method('getParam') - ->with('id', null) - ->willReturn($productId); - - /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ - $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog']) - ->getMockForAbstractClass(); - - $this->productRepositoryMock->expects($this->once()) - ->method('getById') - ->with($productId, false, null, false) - ->willReturn($productMock); - - $productMock->expects($this->once()) - ->method('isVisibleInCatalog') - ->willReturn(true); - - $this->registryMock->expects($this->once()) - ->method('register') - ->with('product', $productMock, false); - - $this->sendFriendMock->expects($this->once()) - ->method('getMaxSendsToFriend') - ->willReturn(11); - $this->sendFriendMock->expects($this->once()) - ->method('isExceedLimit') - ->willReturn(false); - - $this->messageManagerMock->expects($this->never()) - ->method('addNotice'); - - /** @var \Magento\Framework\View\Result\Page|\PHPUnit_Framework_MockObject_MockObject $pageMock */ - $pageMock = $this->getMockBuilder(\Magento\Framework\View\Result\Page::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_PAGE, []) - ->willReturn($pageMock); - - $this->eventManagerMock->expects($this->once()) - ->method('dispatch') - ->with('sendfriend_product', ['product' => $productMock]); - - $this->catalogSessionMock->expects($this->once()) - ->method('getSendfriendFormData') - ->willReturn($formData); - $this->catalogSessionMock->expects($this->once()) - ->method('setSendfriendFormData') - ->with(true); - - /** @var \Magento\Framework\View\Layout|\PHPUnit_Framework_MockObject_MockObject $layoutMock */ - $layoutMock = $this->getMockBuilder(\Magento\Framework\View\Layout::class) - ->disableOriginalConstructor() - ->getMock(); - - $pageMock->expects($this->once()) - ->method('getLayout') - ->willReturn($layoutMock); - - /** @var \Magento\SendFriend\Block\Send|\PHPUnit_Framework_MockObject_MockObject $blockMock */ - $blockMock = $this->getMockBuilder(\Magento\SendFriend\Block\Send::class) - ->disableOriginalConstructor() - ->getMock(); - - $layoutMock->expects($this->once()) - ->method('getBlock') - ->with('sendfriend.send') - ->willReturn($blockMock); - - $blockMock->expects($this->once()) - ->method('setFormData') - ->with($formData) - ->willReturnSelf(); - - $this->assertEquals($pageMock, $this->model->execute()); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecuteWithoutBlock() - { - $productId = 11; - $formData = ['some' => 'data']; - - $this->requestMock->expects($this->once()) - ->method('getParam') - ->with('id', null) - ->willReturn($productId); - - /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ - $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog']) - ->getMockForAbstractClass(); - - $this->productRepositoryMock->expects($this->once()) - ->method('getById') - ->with($productId, false, null, false) - ->willReturn($productMock); - - $productMock->expects($this->once()) - ->method('isVisibleInCatalog') - ->willReturn(true); - - $this->registryMock->expects($this->once()) - ->method('register') - ->with('product', $productMock, false); - - $this->sendFriendMock->expects($this->once()) - ->method('getMaxSendsToFriend') - ->willReturn(11); - $this->sendFriendMock->expects($this->once()) - ->method('isExceedLimit') - ->willReturn(false); - - $this->messageManagerMock->expects($this->never()) - ->method('addNotice'); - - /** @var \Magento\Framework\View\Result\Page|\PHPUnit_Framework_MockObject_MockObject $pageMock */ - $pageMock = $this->getMockBuilder(\Magento\Framework\View\Result\Page::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_PAGE, []) - ->willReturn($pageMock); - - $this->eventManagerMock->expects($this->once()) - ->method('dispatch') - ->with('sendfriend_product', ['product' => $productMock]); - - $this->catalogSessionMock->expects($this->once()) - ->method('getSendfriendFormData') - ->willReturn($formData); - $this->catalogSessionMock->expects($this->once()) - ->method('setSendfriendFormData') - ->with(true); - - /** @var \Magento\Framework\View\Layout|\PHPUnit_Framework_MockObject_MockObject $layoutMock */ - $layoutMock = $this->getMockBuilder(\Magento\Framework\View\Layout::class) - ->disableOriginalConstructor() - ->getMock(); - - $pageMock->expects($this->once()) - ->method('getLayout') - ->willReturn($layoutMock); - - $layoutMock->expects($this->once()) - ->method('getBlock') - ->with('sendfriend.send') - ->willReturn(false); - - $this->assertEquals($pageMock, $this->model->execute()); - } - - public function testExecuteWithNoticeAndNoData() - { - $productId = 11; - $formData = null; - - $this->requestMock->expects($this->once()) - ->method('getParam') - ->with('id', null) - ->willReturn($productId); - - /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ - $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog']) - ->getMockForAbstractClass(); - - $this->productRepositoryMock->expects($this->once()) - ->method('getById') - ->with($productId, false, null, false) - ->willReturn($productMock); - - $productMock->expects($this->once()) - ->method('isVisibleInCatalog') - ->willReturn(true); - - $this->registryMock->expects($this->once()) - ->method('register') - ->with('product', $productMock, false); - - $this->sendFriendMock->expects($this->exactly(2)) - ->method('getMaxSendsToFriend') - ->willReturn(11); - $this->sendFriendMock->expects($this->once()) - ->method('isExceedLimit') - ->willReturn(true); - - $this->messageManagerMock->expects($this->once()) - ->method('addNotice') - ->with(__('You can\'t send messages more than %1 times an hour.', 11)) - ->willReturnSelf(); - - /** @var \Magento\Framework\View\Result\Page|\PHPUnit_Framework_MockObject_MockObject $pageMock */ - $pageMock = $this->getMockBuilder(\Magento\Framework\View\Result\Page::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_PAGE, []) - ->willReturn($pageMock); - - $this->eventManagerMock->expects($this->once()) - ->method('dispatch') - ->with('sendfriend_product', ['product' => $productMock]); - - $this->catalogSessionMock->expects($this->once()) - ->method('getSendfriendFormData') - ->willReturn($formData); - $this->catalogSessionMock->expects($this->never()) - ->method('setSendfriendFormData'); - - $pageMock->expects($this->never()) - ->method('getLayout'); - - $this->assertEquals($pageMock, $this->model->execute()); - } - - public function testExecuteWithoutParam() - { - $this->requestMock->expects($this->once()) - ->method('getParam') - ->with('id', null) - ->willReturn(null); - - /** @var \Magento\Framework\Controller\Result\Forward|\PHPUnit_Framework_MockObject_MockObject $forwardMock */ - $forwardMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Forward::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_FORWARD, []) - ->willReturn($forwardMock); - - $forwardMock->expects($this->once()) - ->method('forward') - ->with('noroute') - ->willReturnSelf(); - - $this->assertEquals($forwardMock, $this->model->execute()); - } - - public function testExecuteWithoutProduct() - { - $productId = 11; - - $this->requestMock->expects($this->once()) - ->method('getParam') - ->with('id', null) - ->willReturn($productId); - - $this->productRepositoryMock->expects($this->once()) - ->method('getById') - ->with($productId, false, null, false) - ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException(__('No Product Exception.'))); - - /** @var \Magento\Framework\Controller\Result\Forward|\PHPUnit_Framework_MockObject_MockObject $forwardMock */ - $forwardMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Forward::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_FORWARD, []) - ->willReturn($forwardMock); - - $forwardMock->expects($this->once()) - ->method('forward') - ->with('noroute') - ->willReturnSelf(); - - $this->assertEquals($forwardMock, $this->model->execute()); - } - - public function testExecuteWithNonVisibleProduct() - { - $productId = 11; - - $this->requestMock->expects($this->once()) - ->method('getParam') - ->with('id', null) - ->willReturn($productId); - - /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ - $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog']) - ->getMockForAbstractClass(); - - $this->productRepositoryMock->expects($this->once()) - ->method('getById') - ->with($productId, false, null, false) - ->willReturn($productMock); - - $productMock->expects($this->once()) - ->method('isVisibleInCatalog') - ->willReturn(false); - - /** @var \Magento\Framework\Controller\Result\Forward|\PHPUnit_Framework_MockObject_MockObject $forwardMock */ - $forwardMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Forward::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_FORWARD, []) - ->willReturn($forwardMock); - - $forwardMock->expects($this->once()) - ->method('forward') - ->with('noroute') - ->willReturnSelf(); - - $this->assertEquals($forwardMock, $this->model->execute()); - } -} diff --git a/app/code/Magento/SendFriend/Test/Unit/Controller/Product/SendmailTest.php b/app/code/Magento/SendFriend/Test/Unit/Controller/Product/SendmailTest.php deleted file mode 100644 index c7881f366f52..000000000000 --- a/app/code/Magento/SendFriend/Test/Unit/Controller/Product/SendmailTest.php +++ /dev/null @@ -1,906 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\SendFriend\Test\Unit\Controller\Product; - -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; - -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class SendmailTest extends \PHPUnit\Framework\TestCase -{ - /** @var \Magento\SendFriend\Controller\Product\Sendmail */ - protected $model; - - /** @var ObjectManagerHelper */ - protected $objectManagerHelper; - - /** @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $requestMock; - - /** @var \Magento\Framework\Registry|\PHPUnit_Framework_MockObject_MockObject */ - protected $registryMock; - - /** @var \Magento\Framework\Data\Form\FormKey\Validator|\PHPUnit_Framework_MockObject_MockObject */ - protected $validatorMock; - - /** @var \Magento\SendFriend\Model\SendFriend|\PHPUnit_Framework_MockObject_MockObject */ - protected $sendFriendMock; - - /** @var \Magento\Catalog\Api\ProductRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $productRepositoryMock; - - /** @var \Magento\Catalog\Api\CategoryRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $categoryRepositoryMock; - - /** @var \Magento\Catalog\Model\Session|\PHPUnit_Framework_MockObject_MockObject */ - protected $catalogSessionMock; - - /** @var \Magento\Framework\Message\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $messageManagerMock; - - /** @var \Magento\Framework\Controller\ResultFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $resultFactoryMock; - - /** @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $eventManagerMock; - - /** @var \Magento\Framework\App\Response\RedirectInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $redirectMock; - - /** @var \Magento\Framework\UrlInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $urlBuilderMock; - - protected function setUp() - { - $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) - ->setMethods(['getPost', 'getPostValue', 'getParam']) - ->getMockForAbstractClass(); - $this->registryMock = $this->getMockBuilder(\Magento\Framework\Registry::class) - ->disableOriginalConstructor() - ->getMock(); - $this->validatorMock = $this->getMockBuilder(\Magento\Framework\Data\Form\FormKey\Validator::class) - ->disableOriginalConstructor() - ->getMock(); - $this->sendFriendMock = $this->getMockBuilder(\Magento\SendFriend\Model\SendFriend::class) - ->disableOriginalConstructor() - ->getMock(); - $this->productRepositoryMock = $this->getMockBuilder(\Magento\Catalog\Api\ProductRepositoryInterface::class) - ->getMockForAbstractClass(); - $this->categoryRepositoryMock = $this->getMockBuilder(\Magento\Catalog\Api\CategoryRepositoryInterface::class) - ->getMockForAbstractClass(); - $this->catalogSessionMock = $this->getMockBuilder(\Magento\Catalog\Model\Session::class) - ->setMethods(['getSendfriendFormData', 'setSendfriendFormData']) - ->disableOriginalConstructor() - ->getMock(); - $this->messageManagerMock = $this->getMockBuilder(\Magento\Framework\Message\ManagerInterface::class) - ->getMock(); - $this->resultFactoryMock = $this->getMockBuilder(\Magento\Framework\Controller\ResultFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $this->eventManagerMock = $this->getMockBuilder(\Magento\Framework\Event\ManagerInterface::class) - ->getMock(); - $this->redirectMock = $this->getMockBuilder(\Magento\Framework\App\Response\RedirectInterface::class) - ->getMock(); - $this->urlBuilderMock = $this->getMockBuilder(\Magento\Framework\UrlInterface::class) - ->getMock(); - - $this->objectManagerHelper = new ObjectManagerHelper($this); - $this->model = $this->objectManagerHelper->getObject( - \Magento\SendFriend\Controller\Product\Sendmail::class, - [ - 'request' => $this->requestMock, - 'coreRegistry' => $this->registryMock, - 'formKeyValidator' => $this->validatorMock, - 'sendFriend' => $this->sendFriendMock, - 'productRepository' => $this->productRepositoryMock, - 'categoryRepository' => $this->categoryRepositoryMock, - 'catalogSession' => $this->catalogSessionMock, - 'messageManager' => $this->messageManagerMock, - 'resultFactory' => $this->resultFactoryMock, - 'eventManager' => $this->eventManagerMock, - 'redirect' => $this->redirectMock, - 'url' => $this->urlBuilderMock, - ] - ); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecute() - { - $productId = 11; - $categoryId = 5; - $sender = 'sender'; - $recipients = 'recipients'; - $formData = [ - 'sender' => $sender, - 'recipients' => $recipients, - ]; - $productUrl = 'product_url'; - - /** @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT, []) - ->willReturn($redirectMock); - - $this->validatorMock->expects($this->once()) - ->method('validate') - ->with($this->requestMock) - ->willReturn(true); - - $this->requestMock->expects($this->exactly(2)) - ->method('getParam') - ->willReturnMap( - [ - ['id', null, $productId], - ['cat_id', null, $categoryId], - ] - ); - - /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ - $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog', 'setCategory', 'getProductUrl']) - ->getMockForAbstractClass(); - - $this->productRepositoryMock->expects($this->once()) - ->method('getById') - ->with($productId, false, null, false) - ->willReturn($productMock); - - $productMock->expects($this->once()) - ->method('isVisibleInCatalog') - ->willReturn(true); - - /** @var \Magento\Catalog\Api\Data\CategoryInterface|\PHPUnit_Framework_MockObject_MockObject $categoryMock */ - $categoryMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\CategoryInterface::class) - ->getMockForAbstractClass(); - - $this->categoryRepositoryMock->expects($this->once()) - ->method('get') - ->with($categoryId, null) - ->willReturn($categoryMock); - - $productMock->expects($this->once()) - ->method('setCategory') - ->with($categoryMock); - - $this->registryMock->expects($this->exactly(2)) - ->method('register') - ->willReturnMap( - [ - ['product', $productMock, false, null], - ['current_category', $categoryMock, false, null], - ] - ); - - $this->requestMock->expects($this->once()) - ->method('getPostValue') - ->willReturn($formData); - - $this->requestMock->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap( - [ - ['sender', $sender], - ['recipients', $recipients], - ] - ); - - $this->sendFriendMock->expects($this->once()) - ->method('setSender') - ->with($sender) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setRecipients') - ->with($recipients) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setProduct') - ->with($productMock) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('validate') - ->willReturn(true); - $this->sendFriendMock->expects($this->once()) - ->method('send') - ->willReturnSelf(); - - $this->messageManagerMock->expects($this->once()) - ->method('addSuccess') - ->with(__('The link to a friend was sent.')) - ->willReturnSelf(); - - $productMock->expects($this->once()) - ->method('getProductUrl') - ->willReturn($productUrl); - - $this->redirectMock->expects($this->once()) - ->method('success') - ->with($productUrl) - ->willReturnArgument(0); - - $redirectMock->expects($this->once()) - ->method('setUrl') - ->with($productUrl) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecuteWithoutValidationAndCategory() - { - $productId = 11; - $categoryId = 5; - $sender = 'sender'; - $recipients = 'recipients'; - $formData = [ - 'sender' => $sender, - 'recipients' => $recipients, - ]; - $redirectUrl = 'redirect_url'; - - /** @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT, []) - ->willReturn($redirectMock); - - $this->validatorMock->expects($this->once()) - ->method('validate') - ->with($this->requestMock) - ->willReturn(true); - - $this->requestMock->expects($this->exactly(2)) - ->method('getParam') - ->willReturnMap( - [ - ['id', null, $productId], - ['cat_id', null, $categoryId], - ] - ); - - /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ - $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog', 'setCategory', 'getProductUrl']) - ->getMockForAbstractClass(); - - $this->productRepositoryMock->expects($this->once()) - ->method('getById') - ->with($productId, false, null, false) - ->willReturn($productMock); - - $productMock->expects($this->once()) - ->method('isVisibleInCatalog') - ->willReturn(true); - - $this->categoryRepositoryMock->expects($this->once()) - ->method('get') - ->with($categoryId, null) - ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException(__('No Category Exception.'))); - - $productMock->expects($this->never()) - ->method('setCategory'); - - $this->registryMock->expects($this->once()) - ->method('register') - ->willReturnMap( - [ - ['product', $productMock, false, null], - ] - ); - - $this->requestMock->expects($this->once()) - ->method('getPostValue') - ->willReturn($formData); - - $this->requestMock->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap( - [ - ['sender', $sender], - ['recipients', $recipients], - ] - ); - - $this->sendFriendMock->expects($this->once()) - ->method('setSender') - ->with($sender) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setRecipients') - ->with($recipients) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setProduct') - ->with($productMock) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('validate') - ->willReturn(['Some error']); - $this->sendFriendMock->expects($this->never()) - ->method('send'); - - $this->messageManagerMock->expects($this->once()) - ->method('addError') - ->with(__('Some error')) - ->willReturnSelf(); - - $this->catalogSessionMock->expects($this->once()) - ->method('setSendfriendFormData') - ->with($formData); - - $this->urlBuilderMock->expects($this->once()) - ->method('getUrl') - ->with('sendfriend/product/send', ['_current' => true]) - ->willReturn($redirectUrl); - - $this->redirectMock->expects($this->once()) - ->method('error') - ->with($redirectUrl) - ->willReturnArgument(0); - - $redirectMock->expects($this->once()) - ->method('setUrl') - ->with($redirectUrl) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecuteWithoutValidationAndCategoryWithProblems() - { - $productId = 11; - $categoryId = 5; - $sender = 'sender'; - $recipients = 'recipients'; - $formData = [ - 'sender' => $sender, - 'recipients' => $recipients, - ]; - $redirectUrl = 'redirect_url'; - - /** @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT, []) - ->willReturn($redirectMock); - - $this->validatorMock->expects($this->once()) - ->method('validate') - ->with($this->requestMock) - ->willReturn(true); - - $this->requestMock->expects($this->exactly(2)) - ->method('getParam') - ->willReturnMap( - [ - ['id', null, $productId], - ['cat_id', null, $categoryId], - ] - ); - - /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ - $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog', 'setCategory', 'getProductUrl']) - ->getMockForAbstractClass(); - - $this->productRepositoryMock->expects($this->once()) - ->method('getById') - ->with($productId, false, null, false) - ->willReturn($productMock); - - $productMock->expects($this->once()) - ->method('isVisibleInCatalog') - ->willReturn(true); - - $this->categoryRepositoryMock->expects($this->once()) - ->method('get') - ->with($categoryId, null) - ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException(__('No Category Exception.'))); - - $productMock->expects($this->never()) - ->method('setCategory'); - - $this->registryMock->expects($this->once()) - ->method('register') - ->willReturnMap( - [ - ['product', $productMock, false, null], - ] - ); - - $this->requestMock->expects($this->once()) - ->method('getPostValue') - ->willReturn($formData); - - $this->requestMock->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap( - [ - ['sender', $sender], - ['recipients', $recipients], - ] - ); - - $this->sendFriendMock->expects($this->once()) - ->method('setSender') - ->with($sender) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setRecipients') - ->with($recipients) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setProduct') - ->with($productMock) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('validate') - ->willReturn('Some error'); - $this->sendFriendMock->expects($this->never()) - ->method('send'); - - $this->messageManagerMock->expects($this->once()) - ->method('addError') - ->with(__('We found some problems with the data.')) - ->willReturnSelf(); - - $this->catalogSessionMock->expects($this->once()) - ->method('setSendfriendFormData') - ->with($formData); - - $this->urlBuilderMock->expects($this->once()) - ->method('getUrl') - ->with('sendfriend/product/send', ['_current' => true]) - ->willReturn($redirectUrl); - - $this->redirectMock->expects($this->once()) - ->method('error') - ->with($redirectUrl) - ->willReturnArgument(0); - - $redirectMock->expects($this->once()) - ->method('setUrl') - ->with($redirectUrl) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecuteWithLocalizedException() - { - $productId = 11; - $categoryId = 5; - $sender = 'sender'; - $recipients = 'recipients'; - $formData = [ - 'sender' => $sender, - 'recipients' => $recipients, - ]; - $redirectUrl = 'redirect_url'; - - /** @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT, []) - ->willReturn($redirectMock); - - $this->validatorMock->expects($this->once()) - ->method('validate') - ->with($this->requestMock) - ->willReturn(true); - - $this->requestMock->expects($this->exactly(2)) - ->method('getParam') - ->willReturnMap( - [ - ['id', null, $productId], - ['cat_id', null, $categoryId], - ] - ); - - /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ - $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog', 'setCategory', 'getProductUrl']) - ->getMockForAbstractClass(); - - $this->productRepositoryMock->expects($this->once()) - ->method('getById') - ->with($productId, false, null, false) - ->willReturn($productMock); - - $productMock->expects($this->once()) - ->method('isVisibleInCatalog') - ->willReturn(true); - - $this->categoryRepositoryMock->expects($this->once()) - ->method('get') - ->with($categoryId, null) - ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException(__('No Category Exception.'))); - - $productMock->expects($this->never()) - ->method('setCategory'); - - $this->registryMock->expects($this->once()) - ->method('register') - ->willReturnMap( - [ - ['product', $productMock, false, null], - ] - ); - - $this->requestMock->expects($this->once()) - ->method('getPostValue') - ->willReturn($formData); - - $this->requestMock->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap( - [ - ['sender', $sender], - ['recipients', $recipients], - ] - ); - - $this->sendFriendMock->expects($this->once()) - ->method('setSender') - ->with($sender) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setRecipients') - ->with($recipients) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setProduct') - ->with($productMock) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('validate') - ->willThrowException(new \Magento\Framework\Exception\LocalizedException(__('Localized Exception.'))); - $this->sendFriendMock->expects($this->never()) - ->method('send'); - - $this->messageManagerMock->expects($this->once()) - ->method('addError') - ->with(__('Localized Exception.')) - ->willReturnSelf(); - - $this->catalogSessionMock->expects($this->once()) - ->method('setSendfriendFormData') - ->with($formData); - - $this->urlBuilderMock->expects($this->once()) - ->method('getUrl') - ->with('sendfriend/product/send', ['_current' => true]) - ->willReturn($redirectUrl); - - $this->redirectMock->expects($this->once()) - ->method('error') - ->with($redirectUrl) - ->willReturnArgument(0); - - $redirectMock->expects($this->once()) - ->method('setUrl') - ->with($redirectUrl) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecuteWithException() - { - $productId = 11; - $categoryId = 5; - $sender = 'sender'; - $recipients = 'recipients'; - $formData = [ - 'sender' => $sender, - 'recipients' => $recipients, - ]; - $redirectUrl = 'redirect_url'; - - /** @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->with(\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT, []) - ->willReturn($redirectMock); - - $this->validatorMock->expects($this->once()) - ->method('validate') - ->with($this->requestMock) - ->willReturn(true); - - $this->requestMock->expects($this->exactly(2)) - ->method('getParam') - ->willReturnMap( - [ - ['id', null, $productId], - ['cat_id', null, $categoryId], - ] - ); - - /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ - $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog', 'setCategory', 'getProductUrl']) - ->getMockForAbstractClass(); - - $this->productRepositoryMock->expects($this->once()) - ->method('getById') - ->with($productId, false, null, false) - ->willReturn($productMock); - - $productMock->expects($this->once()) - ->method('isVisibleInCatalog') - ->willReturn(true); - - $this->categoryRepositoryMock->expects($this->once()) - ->method('get') - ->with($categoryId, null) - ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException(__('No Category Exception.'))); - - $productMock->expects($this->never()) - ->method('setCategory'); - - $this->registryMock->expects($this->once()) - ->method('register') - ->willReturnMap( - [ - ['product', $productMock, false, null], - ] - ); - - $this->requestMock->expects($this->once()) - ->method('getPostValue') - ->willReturn($formData); - - $this->requestMock->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap( - [ - ['sender', $sender], - ['recipients', $recipients], - ] - ); - - $this->sendFriendMock->expects($this->once()) - ->method('setSender') - ->with($sender) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setRecipients') - ->with($recipients) - ->willReturnSelf(); - $this->sendFriendMock->expects($this->once()) - ->method('setProduct') - ->with($productMock) - ->willReturnSelf(); - $exception = new \Exception(__('Exception.')); - $this->sendFriendMock->expects($this->once()) - ->method('validate') - ->willThrowException($exception); - $this->sendFriendMock->expects($this->never()) - ->method('send'); - - $this->messageManagerMock->expects($this->once()) - ->method('addException') - ->with($exception, __('Some emails were not sent.')) - ->willReturnSelf(); - - $this->catalogSessionMock->expects($this->once()) - ->method('setSendfriendFormData') - ->with($formData); - - $this->urlBuilderMock->expects($this->once()) - ->method('getUrl') - ->with('sendfriend/product/send', ['_current' => true]) - ->willReturn($redirectUrl); - - $this->redirectMock->expects($this->once()) - ->method('error') - ->with($redirectUrl) - ->willReturnArgument(0); - - $redirectMock->expects($this->once()) - ->method('setUrl') - ->with($redirectUrl) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecuteWithoutProduct() - { - $sender = 'sender'; - $recipients = 'recipients'; - $formData = [ - 'sender' => $sender, - 'recipients' => $recipients, - ]; - - /** @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - /** @var \Magento\Framework\Controller\Result\Forward|\PHPUnit_Framework_MockObject_MockObject $forwardMock */ - $forwardMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Forward::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->exactly(2)) - ->method('create') - ->willReturnMap( - [ - [\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT, [], $redirectMock], - [\Magento\Framework\Controller\ResultFactory::TYPE_FORWARD, [], $forwardMock], - ] - ); - - $this->validatorMock->expects($this->once()) - ->method('validate') - ->with($this->requestMock) - ->willReturn(true); - - $this->requestMock->expects($this->once()) - ->method('getParam') - ->willReturnMap( - [ - ['id', null, null], - ] - ); - - $this->requestMock->expects($this->once()) - ->method('getPostValue') - ->willReturn($formData); - - $forwardMock->expects($this->once()) - ->method('forward') - ->with('noroute') - ->willReturnSelf(); - - $this->assertEquals($forwardMock, $this->model->execute()); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecuteWithoutData() - { - $productId = 11; - $formData = ''; - - /** @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - /** @var \Magento\Framework\Controller\Result\Forward|\PHPUnit_Framework_MockObject_MockObject $forwardMock */ - $forwardMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Forward::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->exactly(2)) - ->method('create') - ->willReturnMap( - [ - [\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT, [], $redirectMock], - [\Magento\Framework\Controller\ResultFactory::TYPE_FORWARD, [], $forwardMock], - ] - ); - - $this->validatorMock->expects($this->once()) - ->method('validate') - ->with($this->requestMock) - ->willReturn(true); - - $this->requestMock->expects($this->once()) - ->method('getParam') - ->willReturnMap( - [ - ['id', null, $productId], - ] - ); - - /** @var \Magento\Catalog\Api\Data\ProductInterface|\PHPUnit_Framework_MockObject_MockObject $productMock */ - $productMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) - ->setMethods(['isVisibleInCatalog', 'setCategory', 'getProductUrl']) - ->getMockForAbstractClass(); - - $this->productRepositoryMock->expects($this->once()) - ->method('getById') - ->with($productId, false, null, false) - ->willReturn($productMock); - - $productMock->expects($this->once()) - ->method('isVisibleInCatalog') - ->willReturn(true); - - $this->registryMock->expects($this->once()) - ->method('register') - ->willReturnMap( - [ - ['product', $productMock, false, null], - ] - ); - - $this->requestMock->expects($this->once()) - ->method('getPostValue') - ->willReturn($formData); - - $forwardMock->expects($this->once()) - ->method('forward') - ->with('noroute') - ->willReturnSelf(); - - $this->assertEquals($forwardMock, $this->model->execute()); - } - - public function testExecuteWithoutFormKey() - { - /** @var \Magento\Framework\Controller\Result\Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ - $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->once()) - ->method('create') - ->willReturnMap( - [ - [\Magento\Framework\Controller\ResultFactory::TYPE_REDIRECT, [], $redirectMock], - ] - ); - - $this->validatorMock->expects($this->once()) - ->method('validate') - ->with($this->requestMock) - ->willReturn(false); - - $redirectMock->expects($this->once()) - ->method('setPath') - ->with('sendfriend/product/send', ['_current' => true]) - ->willReturnSelf(); - - $this->assertEquals($redirectMock, $this->model->execute()); - } -} diff --git a/app/code/Magento/SendFriend/composer.json b/app/code/Magento/SendFriend/composer.json index 30aecf13c358..932093004cf7 100644 --- a/app/code/Magento/SendFriend/composer.json +++ b/app/code/Magento/SendFriend/composer.json @@ -9,7 +9,9 @@ "magento/framework": "*", "magento/module-catalog": "*", "magento/module-customer": "*", - "magento/module-store": "*" + "magento/module-store": "*", + "magento/module-captcha": "*", + "magento/module-authorization": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/SendFriend/etc/config.xml b/app/code/Magento/SendFriend/etc/config.xml index 9fa005dcd2fd..d65e5a4a073d 100644 --- a/app/code/Magento/SendFriend/etc/config.xml +++ b/app/code/Magento/SendFriend/etc/config.xml @@ -17,5 +17,21 @@ <check_by>0</check_by> </email> </sendfriend> + <captcha translate="label"> + <frontend> + <areas> + <product_sendtofriend_form> + <label>Send To Friend Form</label> + </product_sendtofriend_form> + </areas> + </frontend> + </captcha> + <customer> + <captcha> + <shown_to_logged_in_user> + <product_sendtofriend_form>1</product_sendtofriend_form> + </shown_to_logged_in_user> + </captcha> + </customer> </default> </config> diff --git a/app/code/Magento/SendFriend/etc/db_schema.xml b/app/code/Magento/SendFriend/etc/db_schema.xml index 7537f67cf552..b9551749dfc6 100644 --- a/app/code/Magento/SendFriend/etc/db_schema.xml +++ b/app/code/Magento/SendFriend/etc/db_schema.xml @@ -16,13 +16,13 @@ comment="Log time"/> <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Website ID"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="log_id"/> </constraint> - <index name="SENDFRIEND_LOG_IP" indexType="btree"> + <index referenceId="SENDFRIEND_LOG_IP" indexType="btree"> <column name="ip"/> </index> - <index name="SENDFRIEND_LOG_TIME" indexType="btree"> + <index referenceId="SENDFRIEND_LOG_TIME" indexType="btree"> <column name="time"/> </index> </table> diff --git a/app/code/Magento/SendFriend/etc/module.xml b/app/code/Magento/SendFriend/etc/module.xml index 01c267b3c4fc..7876ef88618c 100644 --- a/app/code/Magento/SendFriend/etc/module.xml +++ b/app/code/Magento/SendFriend/etc/module.xml @@ -10,6 +10,7 @@ <module name="Magento_SendFriend" > <sequence> <module name="Magento_Catalog"/> + <module name="Magento_Captcha"/> </sequence> </module> </config> diff --git a/app/code/Magento/SendFriend/view/frontend/layout/sendfriend_product_send.xml b/app/code/Magento/SendFriend/view/frontend/layout/sendfriend_product_send.xml index 8065b7e23613..4d6f3d8c628b 100644 --- a/app/code/Magento/SendFriend/view/frontend/layout/sendfriend_product_send.xml +++ b/app/code/Magento/SendFriend/view/frontend/layout/sendfriend_product_send.xml @@ -13,7 +13,7 @@ </action> </referenceBlock> <referenceContainer name="content"> - <block class="Magento\SendFriend\Block\Send" name="sendfriend.send" template="Magento_SendFriend::send.phtml"> + <block class="Magento\SendFriend\Block\Send" name="sendfriend.send" cacheable="false" template="Magento_SendFriend::send.phtml"> <container name="form.additional.info" as="form_additional_info"/> </block> </referenceContainer> diff --git a/app/code/Magento/SendFriend/view/frontend/templates/send.phtml b/app/code/Magento/SendFriend/view/frontend/templates/send.phtml index 3342530f01eb..3e00353a9157 100644 --- a/app/code/Magento/SendFriend/view/frontend/templates/send.phtml +++ b/app/code/Magento/SendFriend/view/frontend/templates/send.phtml @@ -17,13 +17,13 @@ <div class="secondary"> <button type="button" id="btn-remove<%- data._index_ %>" class="action remove" title="<?= $block->escapeHtmlAttr(__('Remove Recipent')) ?>"> - <span><?= $block->escapeJs($block->escapeHtml(__('Remove'))) ?></span> + <span><?= $block->escapeHtml(__('Remove')) ?></span> </button> </div> </div> <fieldset class="fieldset"> <div class="field name required"> - <label for="recipients-name<%- data._index_ %>" class="label"><span><?= $block->escapeJs($block->escapeHtml(__('Name'))) ?></span></label> + <label for="recipients-name<%- data._index_ %>" class="label"><span><?= $block->escapeHtml(__('Name')) ?></span></label> <div class="control"> <input name="recipients[name][<%- data._index_ %>]" type="text" title="<?= $block->escapeHtmlAttr(__('Name')) ?>" class="input-text" id="recipients-name<%- data._index_ %>" data-validate="{required:true}"/> @@ -31,7 +31,7 @@ </div> <div class="field email required"> - <label for="recipients-email<%- data._index_ %>" class="label"><span><?= $block->escapeJs($block->escapeHtml(__('Email'))) ?></span></label> + <label for="recipients-email<%- data._index_ %>" class="label"><span><?= $block->escapeHtml(__('Email')) ?></span></label> <div class="control"> <input name="recipients[email][<%- data._index_ %>]" title="<?= $block->escapeHtmlAttr(__('Email')) ?>" id="recipients-email<%- data._index_ %>" type="email" class="input-text" @@ -108,6 +108,7 @@ </div> <?= $block->getChildHtml('form_additional_info') ?> </fieldset> + <?= $block->getChildHtml('captcha'); ?> <div class="actions-toolbar"> <div class="primary"> <button type="submit" @@ -123,7 +124,7 @@ <script type="text/x-magento-init"> { "a[role='back']": { - "Magento_SendFriend/back-event": {} + "Magento_SendFriend/js/back-event": {} } } </script> diff --git a/app/code/Magento/SendFriend/view/frontend/web/back-event.js b/app/code/Magento/SendFriend/view/frontend/web/js/back-event.js similarity index 100% rename from app/code/Magento/SendFriend/view/frontend/web/back-event.js rename to app/code/Magento/SendFriend/view/frontend/web/js/back-event.js diff --git a/app/code/Magento/SendFriend/view/frontend/web/requirejs-config.js b/app/code/Magento/SendFriend/view/frontend/web/requirejs-config.js new file mode 100644 index 000000000000..8e85666c2362 --- /dev/null +++ b/app/code/Magento/SendFriend/view/frontend/web/requirejs-config.js @@ -0,0 +1,11 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +var config = { + map: { + '*': { + 'Magento_SendFriend/back-event': 'Magento_SendFriend/js/back-event' + } + } +}; diff --git a/app/code/Magento/SendFriendGraphQl/Model/Resolver/SendEmailToFriend.php b/app/code/Magento/SendFriendGraphQl/Model/Resolver/SendEmailToFriend.php new file mode 100644 index 000000000000..c0c01c71df76 --- /dev/null +++ b/app/code/Magento/SendFriendGraphQl/Model/Resolver/SendEmailToFriend.php @@ -0,0 +1,198 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SendFriendGraphQl\Model\Resolver; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\DataObjectFactory; +use Magento\Framework\Event\ManagerInterface; +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\SendFriend\Model\SendFriend; +use Magento\SendFriend\Model\SendFriendFactory; + +/** + * @inheritdoc + */ +class SendEmailToFriend implements ResolverInterface +{ + /** + * @var SendFriendFactory + */ + private $sendFriendFactory; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var DataObjectFactory + */ + private $dataObjectFactory; + + /** + * @var ManagerInterface + */ + private $eventManager; + + /** + * @param SendFriendFactory $sendFriendFactory + * @param ProductRepositoryInterface $productRepository + * @param DataObjectFactory $dataObjectFactory + * @param ManagerInterface $eventManager + */ + public function __construct( + SendFriendFactory $sendFriendFactory, + ProductRepositoryInterface $productRepository, + DataObjectFactory $dataObjectFactory, + ManagerInterface $eventManager + ) { + $this->sendFriendFactory = $sendFriendFactory; + $this->productRepository = $productRepository; + $this->dataObjectFactory = $dataObjectFactory; + $this->eventManager = $eventManager; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + /** @var SendFriend $sendFriend */ + $sendFriend = $this->sendFriendFactory->create(); + + if ($sendFriend->getMaxSendsToFriend() && $sendFriend->isExceedLimit()) { + throw new GraphQlInputException( + __('You can\'t send messages more than %1 times an hour.', $sendFriend->getMaxSendsToFriend()) + ); + } + + $product = $this->getProduct($args['input']['product_id']); + $this->eventManager->dispatch('sendfriend_product', ['product' => $product]); + $sendFriend->setProduct($product); + + $senderData = $this->extractSenderData($args); + $sendFriend->setSender($senderData); + + $recipientsData = $this->extractRecipientsData($args); + $sendFriend->setRecipients($recipientsData); + + $this->validateSendFriendModel($sendFriend, $senderData, $recipientsData); + $sendFriend->send(); + + return array_merge($senderData, $recipientsData); + } + + /** + * Validate send friend model + * + * @param SendFriend $sendFriend + * @param array $senderData + * @param array $recipientsData + * @return void + * @throws GraphQlInputException + */ + private function validateSendFriendModel(SendFriend $sendFriend, array $senderData, array $recipientsData): void + { + $sender = $this->dataObjectFactory->create()->setData($senderData['sender']); + $sendFriend->setData('_sender', $sender); + + $emails = array_column($recipientsData['recipients'], 'email'); + $recipients = $this->dataObjectFactory->create()->setData('emails', $emails); + $sendFriend->setData('_recipients', $recipients); + + $validationResult = $sendFriend->validate(); + if ($validationResult !== true) { + throw new GraphQlInputException(__(implode($validationResult))); + } + } + + /** + * Get product + * + * @param int $productId + * @return ProductInterface + * @throws GraphQlNoSuchEntityException + */ + private function getProduct(int $productId): ProductInterface + { + try { + $product = $this->productRepository->getById($productId); + if (!$product->isVisibleInCatalog()) { + throw new GraphQlNoSuchEntityException( + __("The product that was requested doesn't exist. Verify the product and try again.") + ); + } + } catch (NoSuchEntityException $e) { + throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); + } + return $product; + } + + /** + * Extract recipients data + * + * @param array $args + * @return array + * @throws GraphQlInputException + */ + private function extractRecipientsData(array $args): array + { + $recipients = []; + foreach ($args['input']['recipients'] as $recipient) { + if (empty($recipient['name'])) { + throw new GraphQlInputException(__('Please provide Name for all of recipients.')); + } + + if (empty($recipient['email'])) { + throw new GraphQlInputException(__('Please provide Email for all of recipients.')); + } + + $recipients[] = [ + 'name' => $recipient['name'], + 'email' => $recipient['email'], + ]; + } + return ['recipients' => $recipients]; + } + + /** + * Extract sender data + * + * @param array $args + * @return array + * @throws GraphQlInputException + */ + private function extractSenderData(array $args): array + { + if (empty($args['input']['sender']['name'])) { + throw new GraphQlInputException(__('Please provide Name of sender.')); + } + + if (empty($args['input']['sender']['email'])) { + throw new GraphQlInputException(__('Please provide Email of sender.')); + } + + if (empty($args['input']['sender']['message'])) { + throw new GraphQlInputException(__('Please provide Message.')); + } + + return [ + 'sender' => [ + 'name' => $args['input']['sender']['name'], + 'email' => $args['input']['sender']['email'], + 'message' => $args['input']['sender']['message'], + ], + ]; + } +} diff --git a/app/code/Magento/SendFriendGraphQl/README.md b/app/code/Magento/SendFriendGraphQl/README.md new file mode 100644 index 000000000000..d8051922ddad --- /dev/null +++ b/app/code/Magento/SendFriendGraphQl/README.md @@ -0,0 +1,3 @@ +# SendFriendGraphQl + +**SendFriendGraphQl** provides support of GraphQL for SendFriend functionality. diff --git a/app/code/Magento/SendFriendGraphQl/composer.json b/app/code/Magento/SendFriendGraphQl/composer.json new file mode 100644 index 000000000000..d401f57b2257 --- /dev/null +++ b/app/code/Magento/SendFriendGraphQl/composer.json @@ -0,0 +1,26 @@ +{ + "name": "magento/module-send-friend-graph-ql", + "description": "N/A", + "type": "magento2-module", + "require": { + "php": "~7.1.3||~7.2.0", + "magento/framework": "*", + "magento/module-catalog": "*", + "magento/module-send-friend": "*" + }, + "suggest": { + "magento/module-graph-ql": "*" + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\SendFriendGraphQl\\": "" + } + } +} diff --git a/app/code/Magento/SendFriendGraphQl/etc/module.xml b/app/code/Magento/SendFriendGraphQl/etc/module.xml new file mode 100644 index 000000000000..3df33266cac6 --- /dev/null +++ b/app/code/Magento/SendFriendGraphQl/etc/module.xml @@ -0,0 +1,11 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_SendFriendGraphQl"/> +</config> diff --git a/app/code/Magento/SendFriendGraphQl/etc/schema.graphqls b/app/code/Magento/SendFriendGraphQl/etc/schema.graphqls new file mode 100644 index 000000000000..1234b65a7b91 --- /dev/null +++ b/app/code/Magento/SendFriendGraphQl/etc/schema.graphqls @@ -0,0 +1,39 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +type Mutation { + sendEmailToFriend (input: SendEmailToFriendInput): SendEmailToFriendOutput @resolver(class: "\\Magento\\SendFriendGraphQl\\Model\\Resolver\\SendEmailToFriend") @doc(description:"Recommends Product by Sending Single/Multiple Email") +} + +input SendEmailToFriendInput { + product_id: Int! + sender: SendEmailToFriendSenderInput! + recipients: [SendEmailToFriendRecipientInput!]! +} + +input SendEmailToFriendSenderInput { + name: String! + email: String! + message: String! +} + +input SendEmailToFriendRecipientInput { + name: String! + email: String! +} + +type SendEmailToFriendOutput { + sender: SendEmailToFriendSender + recipients: [SendEmailToFriendRecipient] +} + +type SendEmailToFriendSender { + name: String! + email: String! + message: String! +} + +type SendEmailToFriendRecipient { + name: String! + email: String! +} diff --git a/app/code/Magento/SendFriendGraphQl/registration.php b/app/code/Magento/SendFriendGraphQl/registration.php new file mode 100644 index 000000000000..13ec47b16abd --- /dev/null +++ b/app/code/Magento/SendFriendGraphQl/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_SendFriendGraphQl', __DIR__); diff --git a/app/code/Magento/Shipping/Block/Adminhtml/Order/Packaging.php b/app/code/Magento/Shipping/Block/Adminhtml/Order/Packaging.php index b4ff445c63f4..e5e419328eea 100644 --- a/app/code/Magento/Shipping/Block/Adminhtml/Order/Packaging.php +++ b/app/code/Magento/Shipping/Block/Adminhtml/Order/Packaging.php @@ -74,6 +74,7 @@ public function getShipment() * Configuration for popup window for packaging * * @return string + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getConfigDataJson() { @@ -86,7 +87,7 @@ public function getConfigDataJson() $itemsName = []; $itemsWeight = []; $itemsProductId = []; - + $itemsOrderItemId = []; if ($shipmentId) { $urlParams['shipment_id'] = $shipmentId; $createLabelUrl = $this->getUrl('adminhtml/order_shipment/createLabel', $urlParams); 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 000000000000..ec1ee277a5a5 --- /dev/null +++ b/app/code/Magento/Shipping/Block/DataProviders/Tracking/DeliveryDateTitle.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Shipping\Block\DataProviders\Tracking; + +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\Shipping\Model\Tracking\Result\Status; + +/** + * Extension point to provide ability to change tracking details titles + */ +class DeliveryDateTitle implements ArgumentInterface +{ + /** + * Returns Title in case if carrier defined + * + * @param Status $trackingStatus + * @return \Magento\Framework\Phrase|string + */ + public function getTitle(Status $trackingStatus) + { + return $trackingStatus->getCarrier() ? __('Delivered on:') : ''; + } +} diff --git a/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AddTrack.php b/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AddTrack.php index fa83db490e38..c417a202321b 100644 --- a/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AddTrack.php +++ b/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AddTrack.php @@ -1,14 +1,27 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Shipping\Controller\Adminhtml\Order\Shipment; use Magento\Backend\App\Action; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Controller\ResultInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Sales\Api\Data\ShipmentTrackInterfaceFactory; +use Magento\Sales\Api\ShipmentRepositoryInterface; +use Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader; -class AddTrack extends \Magento\Backend\App\Action +/** + * Add new tracking number to shipment controller. + */ +class AddTrack extends Action implements HttpPostActionInterface { /** * Authorization level of a basic admin session @@ -18,27 +31,54 @@ class AddTrack extends \Magento\Backend\App\Action const ADMIN_RESOURCE = 'Magento_Sales::shipment'; /** - * @var \Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader + * @var ShipmentLoader */ protected $shipmentLoader; + /** + * @var ShipmentRepositoryInterface + */ + private $shipmentRepository; + + /** + * @var ShipmentTrackInterfaceFactory + */ + private $trackFactory; + + /** + * @var SerializerInterface + */ + private $serializer; + /** * @param Action\Context $context - * @param \Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader $shipmentLoader + * @param ShipmentLoader $shipmentLoader + * @param ShipmentRepositoryInterface|null $shipmentRepository + * @param ShipmentTrackInterfaceFactory|null $trackFactory + * @param SerializerInterface|null $serializer */ public function __construct( Action\Context $context, - \Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader $shipmentLoader + ShipmentLoader $shipmentLoader, + ShipmentRepositoryInterface $shipmentRepository = null, + ShipmentTrackInterfaceFactory $trackFactory = null, + SerializerInterface $serializer = null ) { - $this->shipmentLoader = $shipmentLoader; parent::__construct($context); + + $this->shipmentLoader = $shipmentLoader; + $this->shipmentRepository = $shipmentRepository ?: ObjectManager::getInstance() + ->get(ShipmentRepositoryInterface::class); + $this->trackFactory = $trackFactory ?: ObjectManager::getInstance() + ->get(ShipmentTrackInterfaceFactory::class); + $this->serializer = $serializer ?: ObjectManager::getInstance() + ->get(SerializerInterface::class); } /** - * Add new tracking number action + * Add new tracking number action. * - * @return void - * @throws \Magento\Framework\Exception\LocalizedException + * @return ResultInterface */ public function execute() { @@ -46,28 +86,29 @@ public function execute() $carrier = $this->getRequest()->getPost('carrier'); $number = $this->getRequest()->getPost('number'); $title = $this->getRequest()->getPost('title'); + if (empty($carrier)) { - throw new \Magento\Framework\Exception\LocalizedException(__('Please specify a carrier.')); + throw new LocalizedException(__('Please specify a carrier.')); } if (empty($number)) { - throw new \Magento\Framework\Exception\LocalizedException(__('Please enter a tracking number.')); + throw new LocalizedException(__('Please enter a tracking number.')); } + $this->shipmentLoader->setOrderId($this->getRequest()->getParam('order_id')); $this->shipmentLoader->setShipmentId($this->getRequest()->getParam('shipment_id')); $this->shipmentLoader->setShipment($this->getRequest()->getParam('shipment')); $this->shipmentLoader->setTracking($this->getRequest()->getParam('tracking')); $shipment = $this->shipmentLoader->load(); if ($shipment) { - $track = $this->_objectManager->create( - \Magento\Sales\Model\Order\Shipment\Track::class - )->setNumber( + $track = $this->trackFactory->create()->setNumber( $number )->setCarrierCode( $carrier )->setTitle( $title ); - $shipment->addTrack($track)->save(); + $shipment->addTrack($track); + $this->shipmentRepository->save($shipment); $this->_view->loadLayout(); $this->_view->getPage()->getConfig()->getTitle()->prepend(__('Shipments')); @@ -78,16 +119,18 @@ public function execute() 'message' => __('We can\'t initialize shipment for adding tracking number.'), ]; } - } catch (\Magento\Framework\Exception\LocalizedException $e) { + } catch (LocalizedException $e) { $response = ['error' => true, 'message' => $e->getMessage()]; } catch (\Exception $e) { $response = ['error' => true, 'message' => __('Cannot add tracking number.')]; } - if (is_array($response)) { - $response = $this->_objectManager->get(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($response); - $this->getResponse()->representJson($response); - } else { - $this->getResponse()->setBody($response); + + if (\is_array($response)) { + $response = $this->serializer->serialize($response); + + return $this->resultFactory->create(ResultFactory::TYPE_JSON)->setJsonData($response); } + + return $this->resultFactory->create(ResultFactory::TYPE_RAW)->setContents($response); } } diff --git a/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/NewAction.php b/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/NewAction.php index be0555fbcda4..7231bc8b9c6e 100644 --- a/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/NewAction.php +++ b/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/NewAction.php @@ -6,10 +6,11 @@ */ namespace Magento\Shipping\Controller\Adminhtml\Order\Shipment; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Backend\App\Action; use Magento\Framework\App\ObjectManager; -class NewAction extends \Magento\Backend\App\Action +class NewAction extends \Magento\Backend\App\Action implements HttpGetActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Save.php b/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Save.php index 4804efcc76ec..100ba029beab 100644 --- a/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Save.php +++ b/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Save.php @@ -1,19 +1,20 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Shipping\Controller\Adminhtml\Order\Shipment; -use Magento\Backend\App\Action; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\ResultFactory; use Magento\Sales\Model\Order\Shipment\Validation\QuantityValidator; /** * Class Save * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Save extends \Magento\Backend\App\Action +class Save extends \Magento\Backend\App\Action implements HttpPostActionInterface { /** * Authorization level of a basic admin session @@ -47,17 +48,22 @@ class Save extends \Magento\Backend\App\Action * @param \Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader $shipmentLoader * @param \Magento\Shipping\Model\Shipping\LabelGenerator $labelGenerator * @param \Magento\Sales\Model\Order\Email\Sender\ShipmentSender $shipmentSender + * @param \Magento\Sales\Model\Order\Shipment\ShipmentValidatorInterface|null $shipmentValidator */ public function __construct( \Magento\Backend\App\Action\Context $context, \Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader $shipmentLoader, \Magento\Shipping\Model\Shipping\LabelGenerator $labelGenerator, - \Magento\Sales\Model\Order\Email\Sender\ShipmentSender $shipmentSender + \Magento\Sales\Model\Order\Email\Sender\ShipmentSender $shipmentSender, + \Magento\Sales\Model\Order\Shipment\ShipmentValidatorInterface $shipmentValidator = null ) { + parent::__construct($context); + $this->shipmentLoader = $shipmentLoader; $this->labelGenerator = $labelGenerator; $this->shipmentSender = $shipmentSender; - parent::__construct($context); + $this->shipmentValidator = $shipmentValidator ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Sales\Model\Order\Shipment\ShipmentValidatorInterface::class); } /** @@ -83,9 +89,10 @@ protected function _saveShipment($shipment) /** * Save shipment + * * We can save only new shipment. Existing shipments are not editable * - * @return void + * @return \Magento\Framework\Controller\ResultInterface * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -97,7 +104,7 @@ public function execute() $formKeyIsValid = $this->_formKeyValidator->validate($this->getRequest()); $isPost = $this->getRequest()->isPost(); if (!$formKeyIsValid || !$isPost) { - $this->messageManager->addError(__('We can\'t save the shipment right now.')); + $this->messageManager->addErrorMessage(__('We can\'t save the shipment right now.')); return $resultRedirect->setPath('sales/order/index'); } @@ -117,8 +124,7 @@ public function execute() $this->shipmentLoader->setTracking($this->getRequest()->getParam('tracking')); $shipment = $this->shipmentLoader->load(); if (!$shipment) { - $this->_forward('noroute'); - return; + return $this->resultFactory->create(ResultFactory::TYPE_FORWARD)->forward('noroute'); } if (!empty($data['comment_text'])) { @@ -131,15 +137,13 @@ public function execute() $shipment->setCustomerNote($data['comment_text']); $shipment->setCustomerNoteNotify(isset($data['comment_customer_notify'])); } - $validationResult = $this->getShipmentValidator() - ->validate($shipment, [QuantityValidator::class]); + $validationResult = $this->shipmentValidator->validate($shipment, [QuantityValidator::class]); if ($validationResult->hasMessages()) { - $this->messageManager->addError( + $this->messageManager->addErrorMessage( __("Shipment Document Validation Error(s):\n" . implode("\n", $validationResult->getMessages())) ); - $this->_redirect('*/*/new', ['order_id' => $this->getRequest()->getParam('order_id')]); - return; + return $resultRedirect->setPath('*/*/new', ['order_id' => $this->getRequest()->getParam('order_id')]); } $shipment->register(); @@ -159,7 +163,7 @@ public function execute() $shipmentCreatedMessage = __('The shipment has been created.'); $labelCreatedMessage = __('You created the shipping label.'); - $this->messageManager->addSuccess( + $this->messageManager->addSuccessMessage( $isNeedCreateLabel ? $shipmentCreatedMessage . ' ' . $labelCreatedMessage : $shipmentCreatedMessage ); $this->_objectManager->get(\Magento\Backend\Model\Session::class)->getCommentText(true); @@ -168,8 +172,8 @@ public function execute() $responseAjax->setError(true); $responseAjax->setMessage($e->getMessage()); } else { - $this->messageManager->addError($e->getMessage()); - $this->_redirect('*/*/new', ['order_id' => $this->getRequest()->getParam('order_id')]); + $this->messageManager->addErrorMessage($e->getMessage()); + return $resultRedirect->setPath('*/*/new', ['order_id' => $this->getRequest()->getParam('order_id')]); } } catch (\Exception $e) { $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); @@ -177,29 +181,14 @@ public function execute() $responseAjax->setError(true); $responseAjax->setMessage(__('An error occurred while creating shipping label.')); } else { - $this->messageManager->addError(__('Cannot save shipment.')); - $this->_redirect('*/*/new', ['order_id' => $this->getRequest()->getParam('order_id')]); + $this->messageManager->addErrorMessage(__('Cannot save shipment.')); + return $resultRedirect->setPath('*/*/new', ['order_id' => $this->getRequest()->getParam('order_id')]); } } if ($isNeedCreateLabel) { - $this->getResponse()->representJson($responseAjax->toJson()); - } else { - $this->_redirect('sales/order/view', ['order_id' => $shipment->getOrderId()]); - } - } - - /** - * @return \Magento\Sales\Model\Order\Shipment\ShipmentValidatorInterface - * @deprecated 100.1.1 - */ - private function getShipmentValidator() - { - if ($this->shipmentValidator === null) { - $this->shipmentValidator = $this->_objectManager->get( - \Magento\Sales\Model\Order\Shipment\ShipmentValidatorInterface::class - ); + return $this->resultFactory->create(ResultFactory::TYPE_JSON)->setJsonData($responseAjax->toJson()); } - return $this->shipmentValidator; + return $resultRedirect->setPath('sales/order/view', ['order_id' => $shipment->getOrderId()]); } } diff --git a/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Start.php b/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Start.php index ac15c0accd1c..8a874ddc7952 100644 --- a/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Start.php +++ b/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Start.php @@ -6,7 +6,9 @@ */ namespace Magento\Shipping\Controller\Adminhtml\Order\Shipment; -class Start extends \Magento\Backend\App\Action +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Start extends \Magento\Backend\App\Action implements HttpGetActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php index f9030ee75630..76555ce8a6d8 100644 --- a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php +++ b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php @@ -160,7 +160,8 @@ public function getConfigFlag($field) } /** - * Do request to shipment + * Do request to shipment. + * * Implementation must be in overridden method * * @param Request $request @@ -173,7 +174,8 @@ public function requestToShipment($request) } /** - * Do return of shipment + * Do return of shipment. + * * Implementation must be in overridden method * * @param Request $request @@ -275,6 +277,8 @@ public function getDeliveryConfirmationTypes(\Magento\Framework\DataObject $para } /** + * Validate request for available ship countries. + * * @param \Magento\Framework\DataObject $request * @return $this|bool|false|\Magento\Framework\Model\AbstractModel * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -400,6 +404,8 @@ public function getSortOrder() } /** + * Allows free shipping when all product items have free shipping. + * * @param \Magento\Quote\Model\Quote\Address\RateRequest $request * @return void * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -531,10 +537,10 @@ protected function _getPerorderPrice($cost, $handlingType, $handlingFee) } /** - * Sets the number of boxes for shipping + * Gets the average weight of each box available for shipping * - * @param int $weight in some measure - * @return int + * @param float $weight in some measure + * @return float */ public function getTotalNumOfBoxes($weight) { @@ -545,7 +551,7 @@ public function getTotalNumOfBoxes($weight) $maxPackageWeight = $this->getConfigData('max_package_weight'); if ($weight > $maxPackageWeight && $maxPackageWeight != 0) { $this->_numBoxes = ceil($weight / $maxPackageWeight); - $weight = $weight / $this->_numBoxes; + $weight = (float)$weight / $this->_numBoxes; } return $weight; @@ -671,7 +677,8 @@ protected function filterDebugData($data) } /** - * Recursive replace sensitive xml nodes values by specified mask + * Recursive replace sensitive xml nodes values by specified mask. + * * @param \SimpleXMLElement $xml * @return void */ diff --git a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php index be2588dc4871..27047ae46bf1 100644 --- a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php +++ b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php @@ -184,11 +184,11 @@ public function setActiveFlag($code = 'active') /** * Return code of carrier * - * @return string + * @return string|null */ public function getCarrierCode() { - return isset($this->_code) ? $this->_code : null; + return $this->_code ?? null; } /** @@ -216,6 +216,7 @@ public function getTrackingInfo($tracking) /** * Check if carrier has shipping tracking option available + * * All \Magento\Usa carriers have shipping tracking option available * * @return boolean @@ -399,8 +400,8 @@ protected function _getQuotesCacheKey($requestParams) /** * Checks whether some request to rates have already been done, so we have cache for it - * Used to reduce number of same requests done to carrier service during one session * + * Used to reduce number of same requests done to carrier service during one session * Returns cached response or null * * @param string|array $requestParams @@ -410,7 +411,7 @@ protected function _getCachedQuotes($requestParams) { $key = $this->_getQuotesCacheKey($requestParams); - return isset(self::$_quotesCache[$key]) ? self::$_quotesCache[$key] : null; + return self::$_quotesCache[$key] ?? null; } /** @@ -443,8 +444,7 @@ protected function _prepareServiceName($name) } /** - * Prepare shipment request. - * Validate and correct request information + * Prepare shipment request. Validate and correct request information * * @param \Magento\Framework\DataObject $request * @return void @@ -558,8 +558,7 @@ public function returnOfShipment($request) } /** - * For multi package shipments. Delete requested shipments if the current shipment - * request is failed + * For multi package shipments. Delete requested shipments if the current shipment. Request is failed * * @param array $data * @return bool @@ -625,6 +624,8 @@ public function isGirthAllowed($countyDest = null, $carrierMethodCode = null) } /** + * Set Raw Request + * * @param \Magento\Framework\DataObject|null $request * @return $this * @api @@ -680,6 +681,7 @@ public function parseXml($xmlContent, $customSimplexml = 'SimpleXMLElement') /** * Checks if shipping method can collect rates + * * @return bool */ public function canCollectRates() @@ -689,6 +691,7 @@ public function canCollectRates() /** * Debug errors if showmethod is unset + * * @param Error $errors * * @return void diff --git a/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminShipmentActionGroup.xml b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminShipmentActionGroup.xml index 85430aeaa416..3d70a742b13e 100644 --- a/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminShipmentActionGroup.xml +++ b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminShipmentActionGroup.xml @@ -35,4 +35,16 @@ </arguments> <see selector="{{AdminShipmentItemsSection.skuColumn}}" userInput="{{product.sku}}" stepKey="seeProductSkuInGrid"/> </actionGroup> + + <actionGroup name="goToShipmentIntoOrder"> + <click selector="{{AdminOrderDetailsMainActionsSection.ship}}" stepKey="clickShipAction"/> + <seeInCurrentUrl url="{{AdminShipmentNewPage.url}}" stepKey="seeOrderShipmentUrl"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Shipment" stepKey="seePageNameNewInvoicePage"/> + </actionGroup> + + <actionGroup name="submitShipmentIntoOrder"> + <click selector="{{AdminShipmentMainActionsSection.submitShipment}}" stepKey="clickSubmitShipment"/> + <seeInCurrentUrl url="{{AdminOrderDetailsPage.url}}" stepKey="seeViewOrderPageShipping"/> + <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The shipment has been created." stepKey="seeShipmentCreateSuccess"/> + </actionGroup> </actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Shipping/Test/Mftf/Data/FlatRateShippingMethodData.xml b/app/code/Magento/Shipping/Test/Mftf/Data/FlatRateShippingMethodData.xml index 6c7e5cf1b18e..6d877dac5cbf 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Data/FlatRateShippingMethodData.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Data/FlatRateShippingMethodData.xml @@ -13,6 +13,13 @@ <entity name="flatRateActiveEnable" type="active"> <data key="value">1</data> </entity> + <!-- Disable Flat Rate Shipping method config --> + <entity name="DisableFlatRateShippingMethodConfig" type="flat_rate_shipping_method"> + <requiredEntity type="active">flatRateActiveDisable</requiredEntity> + </entity> + <entity name="flatRateActiveDisable" type="active"> + <data key="value">0</data> + </entity> <!-- Flat Rate Shipping method default setup --> <entity name="FlatRateShippingMethodDefault" type="flat_rate_shipping_method"> <requiredEntity type="active">flatRateActiveDefault</requiredEntity> diff --git a/app/code/Magento/Shipping/Test/Mftf/Data/FreeShippingMethodData.xml b/app/code/Magento/Shipping/Test/Mftf/Data/FreeShippingMethodData.xml index 110795533468..d700aa622c17 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Data/FreeShippingMethodData.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Data/FreeShippingMethodData.xml @@ -13,6 +13,13 @@ <entity name="freeActiveEnable" type="active"> <data key="value">1</data> </entity> + <!-- Disable Free Shipping method --> + <entity name="FreeShippingMethodDisableConfig" type="free_shipping_method"> + <requiredEntity type="active">freeActiveDisable</requiredEntity> + </entity> + <entity name="freeActiveDisable" type="active"> + <data key="value">0</data> + </entity> <!-- Free Shipping method default setup --> <entity name="FreeShippinMethodDefault" type="free_shipping_method"> <requiredEntity type="active">freeActiveDefault</requiredEntity> @@ -52,4 +59,18 @@ <entity name="freeSortOrderDefault" type="sort_order"> <data key="value" /> </entity> + <!--Set Free Shipping Subtotal to 101--> + <entity name="setFreeShippingSubtotal" type="free_shipping_method"> + <requiredEntity type="free_shipping_subtotal">freeShippingSubtotal</requiredEntity> + </entity> + <entity name="freeShippingSubtotal" type="free_shipping_subtotal"> + <data key="value">101</data> + </entity> + <!--Set to default Free Shipping Subtotal--> + <entity name="setFreeShippingSubtotalToDefault" type="free_shipping_method"> + <requiredEntity type="free_shipping_subtotal">freeShippingSubtotalDefault</requiredEntity> + </entity> + <entity name="freeShippingSubtotalDefault" type="free_shipping_subtotal"> + <data key="value">0</data> + </entity> </entities> diff --git a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentAddressInformationSection.xml b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentAddressInformationSection.xml index 14fefd981e4e..10878310c262 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentAddressInformationSection.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentAddressInformationSection.xml @@ -7,11 +7,12 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <section name="AdminShipmentAddressInformationSection"> <element name="billingAddress" type="text" selector=".order-billing-address address"/> <element name="billingAddressEdit" type="button" selector=".order-billing-address .actions a"/> <element name="shippingAddress" type="text" selector=".order-shipping-address address"/> <element name="shippingAddressEdit" type="button" selector=".order-shipping-address .actions a"/> + <element name="goToShippingInformation" type="button" selector="//button[@title='Go to Shipping Information']"/> </section> </sections> diff --git a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentItemsSection.xml b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentItemsSection.xml index a7bf82588f7c..0345c3f2949f 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentItemsSection.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentItemsSection.xml @@ -15,5 +15,6 @@ <element name="itemQtyToShip" type="input" selector=".order-shipment-table tbody:nth-of-type({{var1}}) .col-qty input.qty-item" parameterized="true"/> <element name="nameColumn" type="text" selector=".order-shipment-table .col-product .product-title"/> <element name="skuColumn" type="text" selector=".order-shipment-table .col-product .product-sku-block"/> + <element name="itemQtyInvoiced" type="text" selector="(//*[@class='col-ordered-qty']//th[contains(text(), 'Invoiced')]/following-sibling::td)[{{var}}]" parameterized="true"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/AddTrackTest.php b/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/AddTrackTest.php index cf3bf68e1041..1c9cdb1fa7d5 100644 --- a/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/AddTrackTest.php +++ b/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/AddTrackTest.php @@ -6,127 +6,162 @@ namespace Magento\Shipping\Test\Unit\Controller\Adminhtml\Order\Shipment; -use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Request\Http; +use Magento\Framework\App\ResponseInterface; +use Magento\Framework\App\ViewInterface; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Controller\ResultInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Framework\View\Element\BlockInterface; +use Magento\Framework\View\LayoutInterface; +use Magento\Framework\View\Page\Config; +use Magento\Framework\View\Page\Title; +use Magento\Framework\View\Result\Page; +use Magento\Sales\Api\Data\ShipmentTrackInterfaceFactory; +use Magento\Sales\Model\Order\Shipment; +use Magento\Sales\Model\Order\Shipment\Track; +use Magento\Shipping\Controller\Adminhtml\Order\Shipment\AddTrack; +use Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader; /** - * Class AddTrackTest + * Class AddTrackTest covers AddTrack controller. * - * @package Magento\Shipping\Controller\Adminhtml\Order\Shipment * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class AddTrackTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader|\PHPUnit_Framework_MockObject_MockObject + * @var ShipmentLoader|\PHPUnit_Framework_MockObject_MockObject */ - protected $shipmentLoader; + private $shipmentLoader; /** - * @var \Magento\Shipping\Controller\Adminhtml\Order\Shipment\AddTrack + * @var AddTrack */ - protected $controller; + private $controller; /** - * @var Action\Context|\PHPUnit_Framework_MockObject_MockObject + * @var Context|\PHPUnit_Framework_MockObject_MockObject */ - protected $context; + private $context; /** - * @var \Magento\Framework\App\Request\Http|\PHPUnit_Framework_MockObject_MockObject + * @var Http|\PHPUnit_Framework_MockObject_MockObject */ - protected $request; + private $request; /** - * @var \Magento\Framework\App\ResponseInterface|\PHPUnit_Framework_MockObject_MockObject + * @var ResponseInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $response; + private $response; /** - * @var \Magento\Framework\ObjectManager\ObjectManager|\PHPUnit_Framework_MockObject_MockObject + * @var ViewInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $objectManager; + private $view; /** - * @var \Magento\Framework\App\ViewInterface|\PHPUnit_Framework_MockObject_MockObject + * @var Page|\PHPUnit_Framework_MockObject_MockObject */ - protected $view; + private $resultPageMock; /** - * @var \Magento\Framework\View\Result\Page|\PHPUnit_Framework_MockObject_MockObject + * @var Config|\PHPUnit_Framework_MockObject_MockObject */ - protected $resultPageMock; + private $pageConfigMock; /** - * @var \Magento\Framework\View\Page\Config|\PHPUnit_Framework_MockObject_MockObject + * @var Title|\PHPUnit_Framework_MockObject_MockObject */ - protected $pageConfigMock; + private $pageTitleMock; /** - * @var \Magento\Framework\View\Page\Title|\PHPUnit_Framework_MockObject_MockObject + * @var ShipmentTrackInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $pageTitleMock; + private $trackFactory; + /** + * @var ResultInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $rawResult; + + /** + * @inheritdoc + */ protected function setUp() { $objectManagerHelper = new ObjectManagerHelper($this); $this->shipmentLoader = $this->getMockBuilder( - \Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader::class + ShipmentLoader::class ) ->disableOriginalConstructor() ->setMethods(['setShipmentId', 'setOrderId', 'setShipment', 'setTracking', 'load']) ->getMock(); $this->context = $this->createPartialMock( - \Magento\Backend\App\Action\Context::class, + Context::class, [ 'getRequest', 'getResponse', 'getRedirect', 'getObjectManager', 'getTitle', - 'getView' + 'getView', + 'getResultFactory' ] ); $this->response = $this->createPartialMock( - \Magento\Framework\App\ResponseInterface::class, + ResponseInterface::class, ['setRedirect', 'sendResponse', 'setBody'] ); - $this->request = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) + $this->request = $this->getMockBuilder(Http::class) ->disableOriginalConstructor()->getMock(); - $this->objectManager = $this->createPartialMock( - \Magento\Framework\ObjectManager\ObjectManager::class, - ['create', 'get'] - ); - $this->view = $this->createMock(\Magento\Framework\App\ViewInterface::class); - $this->resultPageMock = $this->getMockBuilder(\Magento\Framework\View\Result\Page::class) + $this->view = $this->createMock(ViewInterface::class); + $this->resultPageMock = $this->getMockBuilder(Page::class) ->disableOriginalConstructor() ->getMock(); - $this->pageConfigMock = $this->getMockBuilder(\Magento\Framework\View\Page\Config::class) + $this->pageConfigMock = $this->getMockBuilder(Config::class) ->disableOriginalConstructor() ->getMock(); - $this->pageTitleMock = $this->getMockBuilder(\Magento\Framework\View\Page\Title::class) + $this->pageTitleMock = $this->getMockBuilder(Title::class) ->disableOriginalConstructor() ->getMock(); + $this->trackFactory = $this->getMockBuilder(ShipmentTrackInterfaceFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMockForAbstractClass(); + $this->rawResult = $this->getMockBuilder(ResultInterface::class) + ->disableOriginalConstructor() + ->setMethods(['setContents']) + ->getMockForAbstractClass(); + $resultFactory = $this->getMockBuilder(ResultFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMockForAbstractClass(); $this->context->expects($this->once()) ->method('getRequest') ->will($this->returnValue($this->request)); $this->context->expects($this->once()) ->method('getResponse') ->will($this->returnValue($this->response)); - $this->context->expects($this->once()) - ->method('getObjectManager') - ->will($this->returnValue($this->objectManager)); $this->context->expects($this->once()) ->method('getView') ->will($this->returnValue($this->view)); + $resultFactory->expects($this->once()) + ->method('create') + ->willReturn($this->rawResult); + $this->context->expects($this->once()) + ->method('getResultFactory') + ->willReturn($resultFactory); $this->controller = $objectManagerHelper->getObject( - \Magento\Shipping\Controller\Adminhtml\Order\Shipment\AddTrack::class, + AddTrack::class, [ 'context' => $this->context, 'shipmentLoader' => $this->shipmentLoader, 'request' => $this->request, 'response' => $this->response, - 'view' => $this->view + 'view' => $this->view, + 'trackFactory' => $this->trackFactory, ] ); } @@ -144,7 +179,7 @@ public function testExecute() $tracking = []; $shipmentData = ['items' => [], 'send_email' => '']; $shipment = $this->createPartialMock( - \Magento\Sales\Model\Order\Shipment::class, + Shipment::class, ['addTrack', '__wakeup', 'save'] ); $this->request->expects($this->any()) @@ -152,8 +187,10 @@ public function testExecute() ->will( $this->returnValueMap( [ - ['order_id', null, $orderId], ['shipment_id', null, $shipmentId], - ['shipment', null, $shipmentData], ['tracking', null, $tracking], + ['order_id', null, $orderId], + ['shipment_id', null, $shipmentId], + ['shipment', null, $shipmentData], + ['tracking', null, $tracking], ] ) ); @@ -183,14 +220,13 @@ public function testExecute() $this->shipmentLoader->expects($this->once()) ->method('load') ->will($this->returnValue($shipment)); - $track = $this->getMockBuilder(\Magento\Sales\Model\Order\Shipment\Track::class) + $track = $this->getMockBuilder(Track::class) ->disableOriginalConstructor() ->setMethods(['__wakeup', 'setNumber', 'setCarrierCode', 'setTitle']) ->getMock(); - $this->objectManager->expects($this->atLeastOnce()) + $this->trackFactory->expects($this->once()) ->method('create') - ->with(\Magento\Sales\Model\Order\Shipment\Track::class) - ->will($this->returnValue($track)); + ->willReturn($track); $track->expects($this->once()) ->method('setNumber') ->with($number) @@ -206,8 +242,8 @@ public function testExecute() $this->view->expects($this->once()) ->method('loadLayout') ->will($this->returnSelf()); - $layout = $this->createMock(\Magento\Framework\View\LayoutInterface::class); - $menuBlock = $this->createPartialMock(\Magento\Framework\View\Element\BlockInterface::class, ['toHtml']); + $layout = $this->createMock(LayoutInterface::class); + $menuBlock = $this->createPartialMock(BlockInterface::class, ['toHtml']); $html = 'html string'; $this->view->expects($this->once()) ->method('getLayout') @@ -235,9 +271,10 @@ public function testExecute() $this->pageConfigMock->expects($this->any()) ->method('getTitle') ->willReturn($this->pageTitleMock); - $this->response->expects($this->once()) - ->method('setBody') - ->with($html); - $this->assertNull($this->controller->execute()); + $this->rawResult->expects($this->once()) + ->method('setContents') + ->with($html) + ->willReturnSelf(); + $this->assertInstanceOf(ResultInterface::class, $this->controller->execute()); } } diff --git a/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/SaveTest.php b/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/SaveTest.php index f841728416f8..c253900501d1 100644 --- a/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/SaveTest.php +++ b/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/SaveTest.php @@ -142,7 +142,7 @@ protected function setUp() ); $this->messageManager = $this->createPartialMock( \Magento\Framework\Message\Manager::class, - ['addSuccess', 'addError'] + ['addSuccessMessage', 'addErrorMessage'] ); $this->session = $this->createPartialMock( \Magento\Backend\Model\Session::class, @@ -236,7 +236,7 @@ public function testExecute($formKeyIsValid, $isPost) if (!$formKeyIsValid || !$isPost) { $this->messageManager->expects($this->once()) - ->method('addError'); + ->method('addErrorMessage'); $this->resultRedirect->expects($this->once()) ->method('setPath') @@ -325,12 +325,11 @@ public function testExecute($formKeyIsValid, $isPost) ->method('get') ->with(\Magento\Backend\Model\Session::class) ->will($this->returnValue($this->session)); - $path = 'sales/order/view'; $arguments = ['order_id' => $orderId]; $shipment->expects($this->once()) ->method('getOrderId') ->will($this->returnValue($orderId)); - $this->prepareRedirect($path, $arguments); + $this->prepareRedirect($arguments); $this->shipmentValidatorMock->expects($this->once()) ->method('validate') @@ -360,10 +359,9 @@ public function executeDataProvider() } /** - * @param string $path * @param array $arguments */ - protected function prepareRedirect($path, array $arguments = []) + protected function prepareRedirect(array $arguments = []) { $this->actionFlag->expects($this->any()) ->method('get') @@ -372,14 +370,8 @@ protected function prepareRedirect($path, array $arguments = []) $this->session->expects($this->any()) ->method('setIsUrlNotice') ->with(true); - - $url = $path . '/' . (!empty($arguments) ? $arguments['order_id'] : ''); - $this->helper->expects($this->atLeastOnce()) - ->method('getUrl') - ->with($path, $arguments) - ->will($this->returnValue($url)); - $this->response->expects($this->atLeastOnce()) - ->method('setRedirect') - ->with($url); + $this->resultRedirect->expects($this->once()) + ->method('setPath') + ->with('sales/order/view', $arguments); } } diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup_content.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup_content.phtml index c32b63bddab5..db0739d127b2 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup_content.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup_content.phtml @@ -61,7 +61,7 @@ <?php else: ?> class="admin__control-select" <?php endif; ?>> - <?php foreach ($block->getContainers() as $key => $value): ?> + <?php foreach ($containers as $key => $value): ?> <option value="<?= /* @escapeNotVerified */ $key ?>" > <?= /* @escapeNotVerified */ $value ?> </option> diff --git a/app/code/Magento/Shipping/view/adminhtml/web/js/packages.js b/app/code/Magento/Shipping/view/adminhtml/web/js/packages.js new file mode 100644 index 000000000000..f46ad4192d17 --- /dev/null +++ b/app/code/Magento/Shipping/view/adminhtml/web/js/packages.js @@ -0,0 +1,39 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'Magento_Ui/js/modal/modal', + 'mage/translate' +], function ($, modal, $t) { + 'use strict'; + + return function (config, element) { + config.buttons = [ + { + text: $t('Print'), + 'class': 'action action-primary', + + /** + * Click handler + */ + click: function () { + window.location.href = this.options.url; + } + }, { + text: $t('Cancel'), + 'class': 'action action-secondary', + + /** + * Click handler + */ + click: function () { + this.closeModal(); + } + } + ]; + modal(config, element); + }; +}); 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 1f5b0ae4630a..67d03da2599b 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 9253b47f82f5..e8584d8f6ad5 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/Signifyd/Model/CaseServices/UpdatingService.php b/app/code/Magento/Signifyd/Model/CaseServices/UpdatingService.php index 870705db941c..168ab67f8cf5 100644 --- a/app/code/Magento/Signifyd/Model/CaseServices/UpdatingService.php +++ b/app/code/Magento/Signifyd/Model/CaseServices/UpdatingService.php @@ -5,8 +5,8 @@ */ namespace Magento\Signifyd\Model\CaseServices; +use Magento\Framework\Api\SimpleDataObjectConverter; use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\Exception\NotFoundException; use Magento\Signifyd\Api\CaseRepositoryInterface; use Magento\Signifyd\Api\Data\CaseInterface; use Magento\Signifyd\Model\CommentsHistoryUpdater; @@ -73,7 +73,6 @@ public function __construct( * @param CaseInterface $case * @param array $data * @return void - * @throws NotFoundException * @throws LocalizedException */ public function update(CaseInterface $case, array $data) @@ -111,7 +110,7 @@ private function setCaseData(CaseInterface $case, array $data) 'orderId' ]; foreach ($data as $key => $value) { - $methodName = 'set' . ucfirst($key); + $methodName = 'set' . SimpleDataObjectConverter::snakeCaseToUpperCamelCase($key); if (!in_array($key, $notResolvedKeys) && method_exists($case, $methodName)) { call_user_func([$case, $methodName], $value); } diff --git a/app/code/Magento/Signifyd/Model/SignifydGateway/Request/AddressBuilder.php b/app/code/Magento/Signifyd/Model/SignifydGateway/Request/AddressBuilder.php index f95968d4a1bf..482f243f6f05 100644 --- a/app/code/Magento/Signifyd/Model/SignifydGateway/Request/AddressBuilder.php +++ b/app/code/Magento/Signifyd/Model/SignifydGateway/Request/AddressBuilder.php @@ -41,6 +41,6 @@ private function getStreetLine($number, $street) { $lines = is_array($street) ? $street : []; - return isset($lines[$number - 1]) ? $lines[$number - 1] : ''; + return $lines[$number - 1] ?? ''; } } diff --git a/app/code/Magento/Signifyd/README.md b/app/code/Magento/Signifyd/README.md index 9479972cb21b..753ace3128d2 100644 --- a/app/code/Magento/Signifyd/README.md +++ b/app/code/Magento/Signifyd/README.md @@ -47,7 +47,7 @@ The following interfaces (marked with the `@api` annotation) provide methods tha - might be used by `Magento\Signifyd\Api\CaseRepositoryInterface` to retrieve a list of case entities by specific conditions -For information about a public API in Magento 2, see [Public interfaces & APIs](http://devdocs.magento.com/guides/v2.1/extension-dev-guide/api-concepts.html). +For information about a public API in Magento 2, see [Public interfaces & APIs](https://devdocs.magento.com/guides/v2.1/extension-dev-guide/api-concepts.html). ## Additional information @@ -67,12 +67,12 @@ The Debug Mode may be enabled in the module configuration. This logs the communi The Magento_Signifyd module does not introduce backward incompatible changes. -You can track [backward incompatible changes in patch releases](http://devdocs.magento.com/guides/v2.0/release-notes/changes/ee_changes.html). +You can track [backward incompatible changes in patch releases](https://devdocs.magento.com/guides/v2.0/release-notes/changes/ee_changes.html). ### Processing supplementary payment information To improve the accuracy of Signifyd's transaction estimation, you may perform these operations (links lead to the Magento Developer Documentation Portal): -- [Provide custom AVS/CVV mapping](http://devdocs.magento.com/guides/v2.2/payments-integrations/signifyd/signifyd.html#provide-avscvv-response-codes) +- [Provide custom AVS/CVV mapping](https://devdocs.magento.com/guides/v2.2/payments-integrations/signifyd/signifyd.html#provide-avscvv-response-codes) -- [Retrieve payment method for a placed order](http://devdocs.magento.com/guides/v2.2/payments-integrations/signifyd/signifyd.html#retrieve-payment-method-for-a-placed-order) +- [Retrieve payment method for a placed order](https://devdocs.magento.com/guides/v2.2/payments-integrations/signifyd/signifyd.html#retrieve-payment-method-for-a-placed-order) diff --git a/app/code/Magento/Signifyd/etc/db_schema.xml b/app/code/Magento/Signifyd/etc/db_schema.xml index 6a31eacbb45f..8d47321c38b2 100644 --- a/app/code/Magento/Signifyd/etc/db_schema.xml +++ b/app/code/Magento/Signifyd/etc/db_schema.xml @@ -24,19 +24,20 @@ <column xsi:type="varchar" name="review_disposition" nullable="true" length="32" comment="Review_disposition"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="true" comment="Created_at"/> <column xsi:type="timestamp" name="updated_at" on_update="false" nullable="true" comment="Updated_at"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> - <constraint xsi:type="foreign" name="SIGNIFYD_CASE_ORDER_ID_SALES_ORDER_ENTITY_ID" table="signifyd_case" + <constraint xsi:type="foreign" referenceId="SIGNIFYD_CASE_ORDER_ID_SALES_ORDER_ENTITY_ID" table="signifyd_case" column="order_id" referenceTable="sales_order" referenceColumn="entity_id" onDelete="SET NULL"/> - <constraint xsi:type="unique" name="SIGNIFYD_CASE_ORDER_ID"> + <constraint xsi:type="unique" referenceId="SIGNIFYD_CASE_ORDER_ID"> <column name="order_id"/> </constraint> - <constraint xsi:type="unique" name="SIGNIFYD_CASE_CASE_ID"> + <constraint xsi:type="unique" referenceId="SIGNIFYD_CASE_CASE_ID"> <column name="case_id"/> </constraint> </table> <table name="sales_order_grid" resource="sales" comment="Sales Flat Order Grid"> - <column xsi:type="varchar" name="signifyd_guarantee_status" nullable="true" length="32"/> + <column xsi:type="varchar" name="signifyd_guarantee_status" nullable="true" length="32" + comment="Signifyd Guarantee Disposition Status"/> </table> </schema> diff --git a/app/code/Magento/Signifyd/etc/events.xml b/app/code/Magento/Signifyd/etc/events.xml index d5ba6e5a9922..d44665f9fb97 100644 --- a/app/code/Magento/Signifyd/etc/events.xml +++ b/app/code/Magento/Signifyd/etc/events.xml @@ -9,10 +9,10 @@ <event name="checkout_submit_all_after"> <observer name="signifyd_place_order_observer" instance="Magento\Signifyd\Observer\PlaceOrder" /> </event> - <event name="paypal_express_place_order_success"> - <observer name="signifyd_place_order_paypal_express_observer" instance="Magento\Signifyd\Observer\PlaceOrder"/> - </event> <event name="paypal_checkout_success"> <observer name="signifyd_place_order_checkout_success_observer" instance="Magento\Signifyd\Observer\PlaceOrder" /> </event> + <event name="checkout_onepage_controller_success_action"> + <observer name="signifyd_place_order_checkout_success_observer" instance="Magento\Signifyd\Observer\PlaceOrder" /> + </event> </config> diff --git a/app/code/Magento/Sitemap/Block/Adminhtml/Edit/Form.php b/app/code/Magento/Sitemap/Block/Adminhtml/Edit/Form.php index 5e90cf6e12f6..91b6a8446894 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/Block/Adminhtml/Grid/Renderer/Link.php b/app/code/Magento/Sitemap/Block/Adminhtml/Grid/Renderer/Link.php index ffc2bf5f6d1c..b4e54104bdfb 100644 --- a/app/code/Magento/Sitemap/Block/Adminhtml/Grid/Renderer/Link.php +++ b/app/code/Magento/Sitemap/Block/Adminhtml/Grid/Renderer/Link.php @@ -3,17 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -/** - * Sitemap grid link column renderer - * - */ namespace Magento\Sitemap\Block\Adminhtml\Grid\Renderer; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Config\Model\Config\Reader\Source\Deployed\DocumentRoot; use Magento\Framework\App\ObjectManager; +/** + * Sitemap grid link column renderer + */ class Link extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\AbstractRenderer { /** @@ -62,6 +60,7 @@ public function render(\Magento\Framework\DataObject $row) { /** @var $sitemap \Magento\Sitemap\Model\Sitemap */ $sitemap = $this->_sitemapFactory->create(); + $sitemap->setStoreId($row->getStoreId()); $url = $this->escapeHtml($sitemap->getSitemapUrl($row->getSitemapPath(), $row->getSitemapFilename())); $fileName = preg_replace('/^\//', '', $row->getSitemapPath() . $row->getSitemapFilename()); diff --git a/app/code/Magento/Sitemap/Block/Robots.php b/app/code/Magento/Sitemap/Block/Robots.php index 410bc02da363..ac99b2ab1cd4 100644 --- a/app/code/Magento/Sitemap/Block/Robots.php +++ b/app/code/Magento/Sitemap/Block/Robots.php @@ -22,11 +22,6 @@ */ class Robots extends AbstractBlock implements IdentityInterface { - /** - * @var StoreResolver - */ - private $storeResolver; - /** * @var CollectionFactory */ @@ -49,6 +44,8 @@ class Robots extends AbstractBlock implements IdentityInterface * @param SitemapHelper $sitemapHelper * @param StoreManagerInterface $storeManager * @param array $data + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( Context $context, @@ -58,7 +55,6 @@ public function __construct( StoreManagerInterface $storeManager, array $data = [] ) { - $this->storeResolver = $storeResolver; $this->sitemapCollectionFactory = $sitemapCollectionFactory; $this->sitemapHelper = $sitemapHelper; $this->storeManager = $storeManager; @@ -78,8 +74,7 @@ public function __construct( */ protected function _toHtml() { - $defaultStoreId = $this->storeResolver->getCurrentStoreId(); - $defaultStore = $this->storeManager->getStore($defaultStoreId); + $defaultStore = $this->storeManager->getDefaultStoreView(); /** @var \Magento\Store\Model\Website $website */ $website = $this->storeManager->getWebsite($defaultStore->getWebsiteId()); @@ -138,7 +133,7 @@ protected function getSitemapLinks(array $storeIds) public function getIdentities() { return [ - Value::CACHE_TAG . '_' . $this->storeResolver->getCurrentStoreId(), + Value::CACHE_TAG . '_' . $this->storeManager->getDefaultStoreView()->getId(), ]; } } diff --git a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Delete.php b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Delete.php index 29d50ea8408f..1c807cbfc194 100644 --- a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Delete.php +++ b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Delete.php @@ -5,10 +5,14 @@ */ namespace Magento\Sitemap\Controller\Adminhtml\Sitemap; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; -class Delete extends \Magento\Sitemap\Controller\Adminhtml\Sitemap +/** + * Controller class Delete. Represents adminhtml request flow for a sitemap deletion + */ +class Delete extends \Magento\Sitemap\Controller\Adminhtml\Sitemap implements HttpPostActionInterface { /** * @var \Magento\Framework\Filesystem diff --git a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Edit.php b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Edit.php index 111353550b9c..14771e7f03a3 100644 --- a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Edit.php +++ b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Edit.php @@ -1,12 +1,17 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Sitemap\Controller\Adminhtml\Sitemap; -class Edit extends \Magento\Sitemap\Controller\Adminhtml\Sitemap +use Magento\Framework\App\Action\HttpGetActionInterface; + +/** + * Controller class Edit. Responsible for rendering of a sitemap edit page + */ +class Edit extends \Magento\Sitemap\Controller\Adminhtml\Sitemap implements HttpGetActionInterface { /** * Core registry diff --git a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Generate.php b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Generate.php index 9592ab6f57c5..8eeeb5bf6bc1 100644 --- a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Generate.php +++ b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Generate.php @@ -1,16 +1,21 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Sitemap\Controller\Adminhtml\Sitemap; use Magento\Backend\App\Action; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Sitemap\Controller\Adminhtml\Sitemap; use Magento\Store\Model\App\Emulation; use Magento\Framework\App\ObjectManager; -class Generate extends \Magento\Sitemap\Controller\Adminhtml\Sitemap +/** + * Generate sitemap file + */ +class Generate extends Sitemap implements HttpGetActionInterface { /** @var \Magento\Store\Model\App\Emulation $appEmulation */ private $appEmulation; @@ -44,14 +49,7 @@ public function execute() // if sitemap record exists if ($sitemap->getId()) { try { - //We need to emulate to get the correct frontend URL for the product images - $this->appEmulation->startEnvironmentEmulation( - $sitemap->getStoreId(), - \Magento\Framework\App\Area::AREA_FRONTEND, - true - ); $sitemap->generateXml(); - $this->messageManager->addSuccessMessage( __('The sitemap "%1" has been generated.', $sitemap->getSitemapFilename()) ); @@ -59,8 +57,6 @@ public function execute() $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { $this->messageManager->addExceptionMessage($e, __('We can\'t generate the sitemap right now.')); - } finally { - $this->appEmulation->stopEnvironmentEmulation(); } } else { $this->messageManager->addErrorMessage(__('We can\'t find a sitemap to generate.')); diff --git a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Index.php b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Index.php index 5f2cc805de0f..e6823c6070a1 100644 --- a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Index.php +++ b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Index.php @@ -6,9 +6,10 @@ */ namespace Magento\Sitemap\Controller\Adminhtml\Sitemap; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Backend\App\Action; -class Index extends \Magento\Sitemap\Controller\Adminhtml\Sitemap +class Index extends \Magento\Sitemap\Controller\Adminhtml\Sitemap implements HttpGetActionInterface { /** * Index action diff --git a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Save.php b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Save.php index 1e0d1cb248f0..5230de042977 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/Config/Backend/Robots.php b/app/code/Magento/Sitemap/Model/Config/Backend/Robots.php index d69b8e6d4481..7a6d28259bfe 100644 --- a/app/code/Magento/Sitemap/Model/Config/Backend/Robots.php +++ b/app/code/Magento/Sitemap/Model/Config/Backend/Robots.php @@ -15,9 +15,11 @@ use Magento\Framework\Registry; use Magento\Robots\Model\Config\Value as RobotsValue; use Magento\Store\Model\StoreResolver; +use Magento\Store\Model\StoreManagerInterface; /** * Backend model for sitemap/search_engines/submission_robots configuration value. + * * Required to implement Page Cache functionality. */ class Robots extends Value implements IdentityInterface @@ -30,9 +32,9 @@ class Robots extends Value implements IdentityInterface protected $_cacheTag = true; /** - * @var StoreResolver + * @var StoreManagerInterface */ - private $storeResolver; + private $storeManager; /** * @param Context $context @@ -40,9 +42,12 @@ class Robots extends Value implements IdentityInterface * @param ScopeConfigInterface $config * @param TypeListInterface $cacheTypeList * @param StoreResolver $storeResolver + * @param StoreManagerInterface|null $storeManager * @param AbstractResource|null $resource * @param AbstractDb|null $resourceCollection * @param array $data + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( Context $context, @@ -50,11 +55,13 @@ public function __construct( ScopeConfigInterface $config, TypeListInterface $cacheTypeList, StoreResolver $storeResolver, + StoreManagerInterface $storeManager = null, AbstractResource $resource = null, AbstractDb $resourceCollection = null, array $data = [] ) { - $this->storeResolver = $storeResolver; + $this->storeManager = $storeManager ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(StoreManagerInterface::class); parent::__construct( $context, @@ -75,7 +82,7 @@ public function __construct( public function getIdentities() { return [ - RobotsValue::CACHE_TAG . '_' . $this->storeResolver->getCurrentStoreId(), + RobotsValue::CACHE_TAG . '_' . $this->storeManager->getStore()->getId(), ]; } } diff --git a/app/code/Magento/Sitemap/Model/EmailNotification.php b/app/code/Magento/Sitemap/Model/EmailNotification.php new file mode 100644 index 000000000000..27c042870a1d --- /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 a536ec998b82..ce74d738c4bc 100644 --- a/app/code/Magento/Sitemap/Model/Observer.php +++ b/app/code/Magento/Sitemap/Model/Observer.php @@ -5,6 +5,11 @@ */ namespace Magento\Sitemap\Model; +use Magento\Sitemap\Model\EmailNotification as SitemapEmail; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Sitemap\Model\ResourceModel\Sitemap\CollectionFactory; +use Magento\Store\Model\ScopeInterface; + /** * Sitemap module observer * @@ -44,47 +49,32 @@ class Observer * * @var \Magento\Framework\App\Config\ScopeConfigInterface */ - protected $_scopeConfig; + private $scopeConfig; /** * @var \Magento\Sitemap\Model\ResourceModel\Sitemap\CollectionFactory */ - protected $_collectionFactory; - - /** - * @var \Magento\Framework\Mail\Template\TransportBuilder - */ - protected $_transportBuilder; - - /** - * @var \Magento\Store\Model\StoreManagerInterface - */ - protected $_storeManager; + private $collectionFactory; /** - * @var \Magento\Framework\Translate\Inline\StateInterface + * @var $emailNotification */ - protected $inlineTranslation; + private $emailNotification; /** - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig - * @param \Magento\Sitemap\Model\ResourceModel\Sitemap\CollectionFactory $collectionFactory - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Mail\Template\TransportBuilder $transportBuilder - * @param \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation + * Observer constructor. + * @param ScopeConfigInterface $scopeConfig + * @param CollectionFactory $collectionFactory + * @param EmailNotification $emailNotification */ public function __construct( - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, - \Magento\Sitemap\Model\ResourceModel\Sitemap\CollectionFactory $collectionFactory, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\Mail\Template\TransportBuilder $transportBuilder, - \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation + ScopeConfigInterface $scopeConfig, + CollectionFactory $collectionFactory, + SitemapEmail $emailNotification ) { - $this->_scopeConfig = $scopeConfig; - $this->_collectionFactory = $collectionFactory; - $this->_storeManager = $storeManager; - $this->_transportBuilder = $transportBuilder; - $this->inlineTranslation = $inlineTranslation; + $this->scopeConfig = $scopeConfig; + $this->collectionFactory = $collectionFactory; + $this->emailNotification = $emailNotification; } /** @@ -97,17 +87,20 @@ public function __construct( public function scheduledGenerateSitemaps() { $errors = []; - + $recipient = $this->scopeConfig->getValue( + Observer::XML_PATH_ERROR_RECIPIENT, + ScopeInterface::SCOPE_STORE + ); // check if scheduled generation enabled - if (!$this->_scopeConfig->isSetFlag( + if (!$this->scopeConfig->isSetFlag( self::XML_PATH_GENERATION_ENABLED, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE ) ) { return; } - $collection = $this->_collectionFactory->create(); + $collection = $this->collectionFactory->create(); /* @var $collection \Magento\Sitemap\Model\ResourceModel\Sitemap\Collection */ foreach ($collection as $sitemap) { /* @var $sitemap \Magento\Sitemap\Model\Sitemap */ @@ -117,41 +110,8 @@ public function scheduledGenerateSitemaps() $errors[] = $e->getMessage(); } } - - if ($errors && $this->_scopeConfig->getValue( - self::XML_PATH_ERROR_RECIPIENT, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ) - ) { - $this->inlineTranslation->suspend(); - - $this->_transportBuilder->setTemplateIdentifier( - $this->_scopeConfig->getValue( - self::XML_PATH_ERROR_TEMPLATE, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ) - )->setTemplateOptions( - [ - 'area' => \Magento\Backend\App\Area\FrontNameResolver::AREA_CODE, - 'store' => \Magento\Store\Model\Store::DEFAULT_STORE_ID, - ] - )->setTemplateVars( - ['warnings' => join("\n", $errors)] - )->setFrom( - $this->_scopeConfig->getValue( - self::XML_PATH_ERROR_IDENTITY, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ) - )->addTo( - $this->_scopeConfig->getValue( - self::XML_PATH_ERROR_RECIPIENT, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ) - ); - $transport = $this->_transportBuilder->getTransport(); - $transport->sendMessage(); - - $this->inlineTranslation->resume(); + if ($errors && $recipient) { + $this->emailNotification->sendErrors($errors); } } } diff --git a/app/code/Magento/Sitemap/Model/ResourceModel/Catalog/Product.php b/app/code/Magento/Sitemap/Model/ResourceModel/Catalog/Product.php index 11a59cfa59f1..1419fa375a11 100644 --- a/app/code/Magento/Sitemap/Model/ResourceModel/Catalog/Product.php +++ b/app/code/Magento/Sitemap/Model/ResourceModel/Catalog/Product.php @@ -5,12 +5,12 @@ */ namespace Magento\Sitemap\Model\ResourceModel\Catalog; +use Magento\Catalog\Helper\Product as HelperProduct; use Magento\Catalog\Model\Product\Image\UrlBuilder; use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; -use Magento\Store\Model\Store; use Magento\Framework\App\ObjectManager; use Magento\Store\Model\ScopeInterface; -use Magento\Catalog\Helper\Product as HelperProduct; +use Magento\Store\Model\Store; /** * Sitemap resource product collection model @@ -105,6 +105,8 @@ class Product extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb private $scopeConfig; /** + * Product constructor. + * * @param \Magento\Framework\Model\ResourceModel\Db\Context $context * @param \Magento\Sitemap\Helper\Data $sitemapData * @param \Magento\Catalog\Model\ResourceModel\Product $productResource @@ -157,6 +159,8 @@ public function __construct( } /** + * Construct + * * @return void */ protected function _construct() @@ -171,7 +175,9 @@ protected function _construct() * @param string $attributeCode * @param mixed $value * @param string $type + * * @return \Magento\Framework\DB\Select|bool + * @throws \Magento\Framework\Exception\LocalizedException */ protected function _addFilter($storeId, $attributeCode, $value, $type = '=') { @@ -217,7 +223,9 @@ protected function _addFilter($storeId, $attributeCode, $value, $type = '=') * @param int $storeId * @param string $attributeCode * @param string $column Add attribute value to given column + * * @return void + * @throws \Magento\Framework\Exception\LocalizedException */ protected function _joinAttribute($storeId, $attributeCode, $column = null) { @@ -251,7 +259,7 @@ protected function _joinAttribute($storeId, $attributeCode, $column = null) // Add attribute value to result set if needed if (isset($column)) { $this->_select->columns([ - $column => $columnValue + $column => $columnValue ]); } } @@ -260,7 +268,9 @@ protected function _joinAttribute($storeId, $attributeCode, $column = null) * Get attribute data by attribute code * * @param string $attributeCode + * * @return array + * @throws \Magento\Framework\Exception\LocalizedException */ protected function _getAttribute($attributeCode) { @@ -272,7 +282,7 @@ protected function _getAttribute($attributeCode) 'attribute_id' => $attribute->getId(), 'table' => $attribute->getBackend()->getTable(), 'is_global' => $attribute->getIsGlobal() == - \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_GLOBAL, + \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_GLOBAL, 'backend_type' => $attribute->getBackendType(), ]; } @@ -283,7 +293,11 @@ protected function _getAttribute($attributeCode) * Get category collection array * * @param null|string|bool|int|Store $storeId + * * @return array|bool + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Zend_Db_Statement_Exception */ public function getCollection($storeId) { @@ -310,7 +324,8 @@ public function getCollection($storeId) [] )->joinLeft( ['url_rewrite' => $this->getTable('url_rewrite')], - 'e.entity_id = url_rewrite.entity_id AND url_rewrite.is_autogenerated = 1 AND url_rewrite.metadata IS ' + 'e.entity_id = url_rewrite.entity_id AND url_rewrite.is_autogenerated = 1 ' + . 'AND NULLIF(url_rewrite.metadata,"") IS ' . $urlsConfigCondition . 'NULL' . $connection->quoteInto(' AND url_rewrite.store_id = ?', $store->getId()) . $connection->quoteInto(' AND url_rewrite.entity_type = ?', ProductUrlRewriteGenerator::ENTITY_TYPE), @@ -348,7 +363,9 @@ public function getCollection($storeId) * * @param array $productRow * @param int $storeId + * * @return \Magento\Framework\DataObject + * @throws \Magento\Framework\Exception\LocalizedException */ protected function _prepareProduct(array $productRow, $storeId) { @@ -373,6 +390,7 @@ protected function _prepareProduct(array $productRow, $storeId) */ protected function _loadProductImages($product, $storeId) { + $this->_storeManager->setCurrentStore($storeId); /** @var $helper \Magento\Sitemap\Helper\Data */ $helper = $this->_sitemapData; $imageIncludePolicy = $helper->getProductImageIncludePolicy($storeId); @@ -476,13 +494,13 @@ private function getProductImageUrl($image) /** * Return Use Categories Path for Product URLs config value * - * @param $storeId + * @param null|string $storeId * * @return bool */ private function isCategoryProductURLsConfig($storeId) { - return (bool)$this->scopeConfig->getValue( + return $this->scopeConfig->isSetFlag( HelperProduct::XML_PATH_PRODUCT_URL_USE_CATEGORY, ScopeInterface::SCOPE_STORE, $storeId diff --git a/app/code/Magento/Sitemap/Model/Sitemap.php b/app/code/Magento/Sitemap/Model/Sitemap.php index d58ff732c81d..c35e20d997d8 100644 --- a/app/code/Magento/Sitemap/Model/Sitemap.php +++ b/app/code/Magento/Sitemap/Model/Sitemap.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Sitemap\Model; use Magento\Config\Model\Config\Reader\Source\Deployed\DocumentRoot; @@ -15,6 +16,8 @@ use Magento\Sitemap\Model\ResourceModel\Sitemap as SitemapResource; /** + * Sitemap model. + * * @method string getSitemapType() * @method \Magento\Sitemap\Model\Sitemap setSitemapType(string $value) * @method string getSitemapFilename() @@ -154,12 +157,11 @@ class Sitemap extends \Magento\Framework\Model\AbstractModel implements \Magento protected $dateTime; /** - * Model cache tag for clear cache in after save and after delete + * @inheritdoc * - * @var string * @since 100.2.0 */ - protected $_cacheTag = true; + protected $_cacheTag = [Value::CACHE_TAG]; /** * Item resolver diff --git a/app/code/Magento/Sitemap/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Sitemap/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 000000000000..c8fd6a751cb2 --- /dev/null +++ b/app/code/Magento/Sitemap/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminMenuSEOAndSearchSiteMap"> + <data key="pageTitle">Site Map</data> + <data key="title">Site Map</data> + <data key="dataUiId">magento-sitemap-catalog-sitemap</data> + </entity> +</entities> diff --git a/app/code/Magento/Sitemap/Test/Mftf/Test/AdminMarketingSiteMapNavigateMenuTest.xml b/app/code/Magento/Sitemap/Test/Mftf/Test/AdminMarketingSiteMapNavigateMenuTest.xml new file mode 100644 index 000000000000..54543fab8649 --- /dev/null +++ b/app/code/Magento/Sitemap/Test/Mftf/Test/AdminMarketingSiteMapNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMarketingSiteMapNavigateMenuTest"> + <annotations> + <features value="Sitemap"/> + <stories value="Menu Navigation"/> + <title value="Admin marketing site map navigate menu test"/> + <description value="Admin should be able to navigate to Marketing > Site Map"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14204"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToMarketingSiteMapPage"> + <argument name="menuUiId" value="{{AdminMenuMarketing.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSEOAndSearchSiteMap.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSEOAndSearchSiteMap.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Sitemap/Test/Unit/Block/RobotsTest.php b/app/code/Magento/Sitemap/Test/Unit/Block/RobotsTest.php index 6fcd247ab1f0..b7cfd2028b75 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Block/RobotsTest.php +++ b/app/code/Magento/Sitemap/Test/Unit/Block/RobotsTest.php @@ -5,6 +5,8 @@ */ namespace Magento\Sitemap\Test\Unit\Block; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -77,6 +79,7 @@ protected function setUp() $this->sitemapCollectionFactory = $this->getMockBuilder( \Magento\Sitemap\Model\ResourceModel\Sitemap\CollectionFactory::class ) + ->setMethods(['create']) ->disableOriginalConstructor() ->getMock(); @@ -109,12 +112,17 @@ public function testToHtmlRobotsSubmissionIsDisabled() $this->initEventManagerMock($expected); $this->scopeConfigMock->expects($this->once())->method('getValue')->willReturn(false); - $this->storeResolver->expects($this->once()) - ->method('getCurrentStoreId') - ->willReturn($defaultStoreId); - $storeMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) ->getMockForAbstractClass(); + + $storeMock->expects($this->once()) + ->method('getWebsiteId') + ->willReturn($defaultWebsiteId); + + $this->storeManager->expects($this->once()) + ->method('getDefaultStoreView') + ->willReturn($storeMock); + $storeMock->expects($this->any()) ->method('getWebsiteId') ->willReturn($defaultWebsiteId); @@ -126,10 +134,6 @@ public function testToHtmlRobotsSubmissionIsDisabled() ->method('getStoreIds') ->willReturn([$defaultStoreId]); - $this->storeManager->expects($this->once()) - ->method('getStore') - ->with($defaultStoreId) - ->willReturn($storeMock); $this->storeManager->expects($this->once()) ->method('getWebsite') ->with($defaultWebsiteId) @@ -165,12 +169,13 @@ public function testAfterGetDataRobotsSubmissionIsEnabled() $this->initEventManagerMock($expected); $this->scopeConfigMock->expects($this->once())->method('getValue')->willReturn(false); - $this->storeResolver->expects($this->once()) - ->method('getCurrentStoreId') - ->willReturn($defaultStoreId); - $storeMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class) ->getMockForAbstractClass(); + + $this->storeManager->expects($this->once()) + ->method('getDefaultStoreView') + ->willReturn($storeMock); + $storeMock->expects($this->any()) ->method('getWebsiteId') ->willReturn($defaultWebsiteId); @@ -182,10 +187,6 @@ public function testAfterGetDataRobotsSubmissionIsEnabled() ->method('getStoreIds') ->willReturn([$defaultStoreId, $secondStoreId]); - $this->storeManager->expects($this->once()) - ->method('getStore') - ->with($defaultStoreId) - ->willReturn($storeMock); $this->storeManager->expects($this->once()) ->method('getWebsite') ->with($defaultWebsiteId) @@ -228,8 +229,14 @@ public function testGetIdentities() { $storeId = 1; - $this->storeResolver->expects($this->once()) - ->method('getCurrentStoreId') + $storeMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class)->getMockForAbstractClass(); + + $this->storeManager->expects($this->once()) + ->method('getDefaultStoreView') + ->willReturn($storeMock); + + $storeMock->expects($this->once()) + ->method('getId') ->willReturn($storeId); $expected = [ 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 f77954101df7..00f51b7e6c23 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/Config/Backend/RobotsTest.php b/app/code/Magento/Sitemap/Test/Unit/Model/Config/Backend/RobotsTest.php index f3c2f90de286..cbf353d0a93c 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Model/Config/Backend/RobotsTest.php +++ b/app/code/Magento/Sitemap/Test/Unit/Model/Config/Backend/RobotsTest.php @@ -37,6 +37,11 @@ class RobotsTest extends \PHPUnit\Framework\TestCase */ private $storeResolver; + /** + * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $storeManager; + protected function setUp() { $this->context = $this->getMockBuilder(\Magento\Framework\Model\Context::class) @@ -57,12 +62,16 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); + $this->storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) + ->getMockForAbstractClass(); + $this->model = new \Magento\Sitemap\Model\Config\Backend\Robots( $this->context, $this->registry, $this->scopeConfig, $this->typeList, - $this->storeResolver + $this->storeResolver, + $this->storeManager ); } @@ -73,8 +82,14 @@ public function testGetIdentities() { $storeId = 1; - $this->storeResolver->expects($this->once()) - ->method('getCurrentStoreId') + $storeMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class)->getMock(); + + $this->storeManager->expects($this->once()) + ->method('getStore') + ->willReturn($storeMock); + + $storeMock->expects($this->once()) + ->method('getId') ->willReturn($storeId); $expected = [ 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 000000000000..eafb47c086ba --- /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 ac88f23ff9d6..09f5418bbd76 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Model/ObserverTest.php +++ b/app/code/Magento/Sitemap/Test/Unit/Model/ObserverTest.php @@ -5,10 +5,14 @@ */ namespace Magento\Sitemap\Test\Unit\Model; +use Magento\Framework\App\Area; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Sitemap\Model\EmailNotification; +use Magento\Store\Model\App\Emulation; /** * Class ObserverTest + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ObserverTest extends \PHPUnit\Framework\TestCase @@ -33,21 +37,6 @@ class ObserverTest extends \PHPUnit\Framework\TestCase */ private $collectionFactoryMock; - /** - * @var \Magento\Framework\Mail\Template\TransportBuilder|\PHPUnit_Framework_MockObject_MockObject - */ - private $transportBuilderMock; - - /** - * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $storeManagerMock; - - /** - * @var \Magento\Framework\Translate\Inline\StateInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $inlineTranslationMock; - /** * @var \Magento\Sitemap\Model\ResourceModel\Sitemap\Collection|\PHPUnit_Framework_MockObject_MockObject */ @@ -63,6 +52,16 @@ class ObserverTest extends \PHPUnit\Framework\TestCase */ private $objectManagerMock; + /** + * @var Emulation|\PHPUnit_Framework_MockObject_MockObject + */ + private $appEmulationMock; + + /** + * @var EmailNotification|\PHPUnit_Framework_MockObject_MockObject + */ + private $emailNotificationMock; + protected function setUp() { $this->objectManagerMock = $this->getMockBuilder(\Magento\Framework\ObjectManagerInterface::class) @@ -74,28 +73,28 @@ protected function setUp() )->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $this->transportBuilderMock = $this->getMockBuilder(\Magento\Framework\Mail\Template\TransportBuilder::class) - ->disableOriginalConstructor() - ->getMock(); - $this->storeManagerMock = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) - ->getMock(); - $this->inlineTranslationMock = $this->getMockBuilder(\Magento\Framework\Translate\Inline\StateInterface::class) - ->getMock(); $this->sitemapCollectionMock = $this->createPartialMock( \Magento\Sitemap\Model\ResourceModel\Sitemap\Collection::class, ['getIterator'] ); - $this->sitemapMock = $this->createPartialMock(\Magento\Sitemap\Model\Sitemap::class, ['generateXml']); - + $this->sitemapMock = $this->createPartialMock( + \Magento\Sitemap\Model\Sitemap::class, + [ + 'generateXml', + 'getStoreId', + ] + ); + $this->appEmulationMock = $this->createMock(Emulation::class); + $this->emailNotificationMock = $this->createMock(EmailNotification::class); $this->objectManager = new ObjectManager($this); + $this->observer = $this->objectManager->getObject( \Magento\Sitemap\Model\Observer::class, [ 'scopeConfig' => $this->scopeConfigMock, 'collectionFactory' => $this->collectionFactoryMock, - 'storeManager' => $this->storeManagerMock, - 'transportBuilder' => $this->transportBuilderMock, - 'inlineTranslation' => $this->inlineTranslationMock + 'appEmulation' => $this->appEmulationMock, + 'emailNotification' => $this->emailNotificationMock ] ); } @@ -103,7 +102,7 @@ protected function setUp() public function testScheduledGenerateSitemapsSendsExceptionEmail() { $exception = 'Sitemap Exception'; - $transport = $this->createMock(\Magento\Framework\Mail\TransportInterface::class); + $storeId = 1; $this->scopeConfigMock->expects($this->once())->method('isSetFlag')->willReturn(true); @@ -115,11 +114,15 @@ public function testScheduledGenerateSitemapsSendsExceptionEmail() ->method('getIterator') ->willReturn(new \ArrayIterator([$this->sitemapMock])); + $this->sitemapMock->expects($this->at(0)) + ->method('getStoreId') + ->willReturn($storeId); + $this->sitemapMock->expects($this->once()) ->method('generateXml') ->willThrowException(new \Exception($exception)); - $this->scopeConfigMock->expects($this->at(1)) + $this->scopeConfigMock->expects($this->at(0)) ->method('getValue') ->with( \Magento\Sitemap\Model\Observer::XML_PATH_ERROR_RECIPIENT, @@ -127,44 +130,6 @@ public function testScheduledGenerateSitemapsSendsExceptionEmail() ) ->willReturn('error-recipient@example.com'); - $this->inlineTranslationMock->expects($this->once()) - ->method('suspend'); - - $this->transportBuilderMock->expects($this->once()) - ->method('setTemplateIdentifier') - ->will($this->returnSelf()); - - $this->transportBuilderMock->expects($this->once()) - ->method('setTemplateOptions') - ->with([ - 'area' => \Magento\Backend\App\Area\FrontNameResolver::AREA_CODE, - 'store' => \Magento\Store\Model\Store::DEFAULT_STORE_ID, - ]) - ->will($this->returnSelf()); - - $this->transportBuilderMock->expects($this->once()) - ->method('setTemplateVars') - ->with(['warnings' => $exception]) - ->will($this->returnSelf()); - - $this->transportBuilderMock->expects($this->once()) - ->method('setFrom') - ->will($this->returnSelf()); - - $this->transportBuilderMock->expects($this->once()) - ->method('addTo') - ->will($this->returnSelf()); - - $this->transportBuilderMock->expects($this->once()) - ->method('getTransport') - ->willReturn($transport); - - $transport->expects($this->once()) - ->method('sendMessage'); - - $this->inlineTranslationMock->expects($this->once()) - ->method('resume'); - $this->observer->scheduledGenerateSitemaps(); } } diff --git a/app/code/Magento/Sitemap/etc/db_schema.xml b/app/code/Magento/Sitemap/etc/db_schema.xml index 82a9b8ef5148..b3c028b626b7 100644 --- a/app/code/Magento/Sitemap/etc/db_schema.xml +++ b/app/code/Magento/Sitemap/etc/db_schema.xml @@ -16,12 +16,12 @@ <column xsi:type="timestamp" name="sitemap_time" on_update="false" nullable="true" comment="Sitemap Time"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Store id"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="sitemap_id"/> </constraint> - <constraint xsi:type="foreign" name="SITEMAP_STORE_ID_STORE_STORE_ID" table="sitemap" column="store_id" + <constraint xsi:type="foreign" referenceId="SITEMAP_STORE_ID_STORE_STORE_ID" table="sitemap" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <index name="SITEMAP_STORE_ID" indexType="btree"> + <index referenceId="SITEMAP_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> diff --git a/app/code/Magento/Sitemap/view/adminhtml/layout/adminhtml_sitemap_index_grid_block.xml b/app/code/Magento/Sitemap/view/adminhtml/layout/adminhtml_sitemap_index_grid_block.xml index cdaa6575d559..632768e4a5f0 100644 --- a/app/code/Magento/Sitemap/view/adminhtml/layout/adminhtml_sitemap_index_grid_block.xml +++ b/app/code/Magento/Sitemap/view/adminhtml/layout/adminhtml_sitemap_index_grid_block.xml @@ -11,7 +11,7 @@ <block class="Magento\Backend\Block\Widget\Grid" name="adminhtml.sitemap.container.grid" as="grid"> <arguments> <argument name="id" xsi:type="string">sitemapGrid</argument> - <argument name="dataSource" xsi:type="object">Magento\Sitemap\Model\ResourceModel\Sitemap\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Sitemap\Model\ResourceModel\Sitemap\Collection</argument> <argument name="default_sort" xsi:type="string">sitemap_id</argument> </arguments> <block class="Magento\Backend\Block\Widget\Grid\ColumnSet" name="adminhtml.sitemap.container.grid.columnSet" as="grid.columnSet"> diff --git a/app/code/Magento/Store/Api/StoreResolverInterface.php b/app/code/Magento/Store/Api/StoreResolverInterface.php index 7eb28729ec23..7c32e321fa6c 100644 --- a/app/code/Magento/Store/Api/StoreResolverInterface.php +++ b/app/code/Magento/Store/Api/StoreResolverInterface.php @@ -8,8 +8,8 @@ /** * Store resolver interface * - * @api - * @since 100.0.2 + * @deprecated + * @see \Magento\Store\Model\StoreManagerInterface */ interface StoreResolverInterface { diff --git a/app/code/Magento/Store/App/Action/Plugin/Context.php b/app/code/Magento/Store/App/Action/Plugin/Context.php index 6ec6cf01bc71..0d34179d3c63 100644 --- a/app/code/Magento/Store/App/Action/Plugin/Context.php +++ b/app/code/Magento/Store/App/Action/Plugin/Context.php @@ -9,10 +9,8 @@ use Magento\Framework\App\Http\Context as HttpContext; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\NotFoundException; -use Magento\Framework\Phrase; use Magento\Store\Api\Data\StoreInterface; use Magento\Store\Api\StoreCookieManagerInterface; -use Magento\Store\Api\StoreResolverInterface; use Magento\Store\Model\StoreManagerInterface; use Magento\Framework\App\Action\AbstractAction; use Magento\Framework\App\RequestInterface; @@ -80,7 +78,7 @@ public function beforeDispatch( /** @var string|array|null $storeCode */ $storeCode = $request->getParam( - StoreResolverInterface::PARAM_NAME, + \Magento\Store\Model\StoreManagerInterface::PARAM_NAME, $this->storeCookieManager->getStoreCodeFromCookie() ); if (is_array($storeCode)) { @@ -106,7 +104,7 @@ public function beforeDispatch( /** * Take action in case of invalid store requested. * - * @param \Throwable|null $previousException + * @param \Throwable|null $previousException * @return void * @throws NotFoundException */ diff --git a/app/code/Magento/Store/App/Request/PathInfoProcessor.php b/app/code/Magento/Store/App/Request/PathInfoProcessor.php index 3fa78dc94aa3..fad0d07c3a0a 100644 --- a/app/code/Magento/Store/App/Request/PathInfoProcessor.php +++ b/app/code/Magento/Store/App/Request/PathInfoProcessor.php @@ -3,56 +3,75 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -namespace Magento\Store\App\Request; +declare(strict_types=1); -use Magento\Framework\Exception\NoSuchEntityException; -use Magento\Store\Model\Store; +namespace Magento\Store\App\Request; +/** + * Processes the path and looks for the store in the url and removes it and modifies the path accordingly. + */ class PathInfoProcessor implements \Magento\Framework\App\Request\PathInfoProcessorInterface { /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StorePathInfoValidator */ - private $storeManager; + private $storePathInfoValidator; /** - * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @var \Magento\Framework\App\Config\ReinitableConfigInterface */ - public function __construct(\Magento\Store\Model\StoreManagerInterface $storeManager) - { - $this->storeManager = $storeManager; + private $config; + + /** + * @param \Magento\Store\App\Request\StorePathInfoValidator $storePathInfoValidator + * @param \Magento\Framework\App\Config\ReinitableConfigInterface $config + */ + public function __construct( + \Magento\Store\App\Request\StorePathInfoValidator $storePathInfoValidator, + \Magento\Framework\App\Config\ReinitableConfigInterface $config + ) { + $this->storePathInfoValidator = $storePathInfoValidator; + $this->config = $config; } /** - * Process path info + * Process path info and remove store from pathInfo. + * + * This method also sets request to no route if store is not valid and store is present in url config is enabled * * @param \Magento\Framework\App\RequestInterface $request * @param string $pathInfo * @return string */ - public function process(\Magento\Framework\App\RequestInterface $request, $pathInfo) + public function process(\Magento\Framework\App\RequestInterface $request, $pathInfo) : string { - $pathParts = explode('/', ltrim($pathInfo, '/'), 2); - $storeCode = $pathParts[0]; - - try { - /** @var \Magento\Store\Api\Data\StoreInterface $store */ - $store = $this->storeManager->getStore($storeCode); - } catch (NoSuchEntityException $e) { - return $pathInfo; - } - - if ($store->isUseStoreInUrl()) { - if (!$request->isDirectAccessFrontendName($storeCode) && $storeCode != Store::ADMIN_CODE) { - $this->storeManager->setCurrentStore($store->getCode()); - $pathInfo = '/' . (isset($pathParts[1]) ? $pathParts[1] : ''); - return $pathInfo; - } elseif (!empty($storeCode)) { - $request->setActionName('noroute'); - return $pathInfo; + //can store code be used in url + if ((bool)$this->config->getValue(\Magento\Store\Model\Store::XML_PATH_STORE_IN_URL)) { + $storeCode = $this->storePathInfoValidator->getValidStoreCode($request, $pathInfo); + if (!empty($storeCode)) { + if (!$request->isDirectAccessFrontendName($storeCode)) { + $pathInfo = $this->trimStoreCodeFromPathInfo($pathInfo, $storeCode); + } else { + //no route in case we're trying to access a store that has the same code as a direct access + $request->setActionName(\Magento\Framework\App\Router\Base::NO_ROUTE); + } } - return $pathInfo; } return $pathInfo; } + + /** + * Trim store code from path info string if exists + * + * @param string $pathInfo + * @param string $storeCode + * @return string + */ + private function trimStoreCodeFromPathInfo(string $pathInfo, string $storeCode) : ?string + { + if (substr($pathInfo, 0, strlen('/' . $storeCode)) == '/'. $storeCode) { + $pathInfo = substr($pathInfo, strlen($storeCode)+1); + } + return empty($pathInfo) ? '/' : $pathInfo; + } } diff --git a/app/code/Magento/Store/App/Request/StorePathInfoValidator.php b/app/code/Magento/Store/App/Request/StorePathInfoValidator.php new file mode 100644 index 000000000000..0b66ba758600 --- /dev/null +++ b/app/code/Magento/Store/App/Request/StorePathInfoValidator.php @@ -0,0 +1,102 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\App\Request; + +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Store\Model\Store; + +/** + * Gets the store from the path if valid + */ +class StorePathInfoValidator +{ + /** + * Store Config + * + * @var \Magento\Framework\App\Config\ReinitableConfigInterface + */ + private $config; + + /** + * @var \Magento\Store\Api\StoreRepositoryInterface + */ + private $storeRepository; + + /** + * @var \Magento\Framework\App\Request\PathInfo + */ + private $pathInfo; + + /** + * @param \Magento\Framework\App\Config\ReinitableConfigInterface $config + * @param \Magento\Store\Api\StoreRepositoryInterface $storeRepository + * @param \Magento\Framework\App\Request\PathInfo $pathInfo + */ + public function __construct( + \Magento\Framework\App\Config\ReinitableConfigInterface $config, + \Magento\Store\Api\StoreRepositoryInterface $storeRepository, + \Magento\Framework\App\Request\PathInfo $pathInfo + ) { + $this->config = $config; + $this->storeRepository = $storeRepository; + $this->pathInfo = $pathInfo; + } + + /** + * Get store code from path info validate if config value. If path info is empty the try to calculate from request. + * + * @param \Magento\Framework\App\Request\Http $request + * @param string $pathInfo + * @return string|null + */ + public function getValidStoreCode( + \Magento\Framework\App\Request\Http $request, + string $pathInfo = '' + ) : ?string { + if (empty($pathInfo)) { + $pathInfo = $this->pathInfo->getPathInfo( + $request->getRequestUri(), + $request->getBaseUrl() + ); + } + $storeCode = $this->getStoreCode($pathInfo); + if (!empty($storeCode) + && $storeCode != Store::ADMIN_CODE + && (bool)$this->config->getValue(\Magento\Store\Model\Store::XML_PATH_STORE_IN_URL) + ) { + try { + $this->storeRepository->getActiveStoreByCode($storeCode); + + if ((bool)$this->config->getValue( + \Magento\Store\Model\Store::XML_PATH_STORE_IN_URL, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $storeCode + )) { + return $storeCode; + } + } catch (NoSuchEntityException $e) { + //return null; + } catch (\Magento\Store\Model\StoreIsInactiveException $e) { + //return null; + } + } + return null; + } + + /** + * Get store code from path info string + * + * @param string $pathInfo + * @return string + */ + private function getStoreCode(string $pathInfo) : string + { + $pathParts = explode('/', ltrim($pathInfo, '/'), 2); + return current($pathParts); + } +} diff --git a/app/code/Magento/Store/App/Response/Redirect.php b/app/code/Magento/Store/App/Response/Redirect.php index 534c6be41c93..3ad6a86db690 100644 --- a/app/code/Magento/Store/App/Response/Redirect.php +++ b/app/code/Magento/Store/App/Response/Redirect.php @@ -7,8 +7,9 @@ */ namespace Magento\Store\App\Response; -use Magento\Store\Api\StoreResolverInterface; - +/** + * Class Redirect computes redirect urls responses. + */ class Redirect implements \Magento\Framework\App\Response\RedirectInterface { /** @@ -76,6 +77,8 @@ public function __construct( } /** + * Get the referrer url. + * * @return string * @throws \Magento\Framework\Exception\NoSuchEntityException */ @@ -164,10 +167,7 @@ public function success($defaultUrl) } /** - * {@inheritdoc} - * - * @param array $arguments - * @return array + * @inheritdoc */ public function updatePathParams(array $arguments) { @@ -248,10 +248,10 @@ protected function normalizeRefererQueryParts($refererQuery) $store = $this->_storeManager->getStore(); if ($store - && !empty($refererQuery[StoreResolverInterface::PARAM_NAME]) - && ($refererQuery[StoreResolverInterface::PARAM_NAME] !== $store->getCode()) + && !empty($refererQuery[\Magento\Store\Model\StoreManagerInterface::PARAM_NAME]) + && ($refererQuery[\Magento\Store\Model\StoreManagerInterface::PARAM_NAME] !== $store->getCode()) ) { - $refererQuery[StoreResolverInterface::PARAM_NAME] = $store->getCode(); + $refererQuery[\Magento\Store\Model\StoreManagerInterface::PARAM_NAME] = $store->getCode(); } return $refererQuery; diff --git a/app/code/Magento/Store/Block/Switcher.php b/app/code/Magento/Store/Block/Switcher.php index 58a7ba3c1c28..f15349f11066 100644 --- a/app/code/Magento/Store/Block/Switcher.php +++ b/app/code/Magento/Store/Block/Switcher.php @@ -10,7 +10,6 @@ namespace Magento\Store\Block; use Magento\Directory\Helper\Data; -use Magento\Store\Api\StoreResolverInterface; use Magento\Store\Model\Group; use Magento\Store\Model\Store; use Magento\Framework\App\ActionInterface; @@ -18,6 +17,8 @@ use Magento\Framework\Url\Helper\Data as UrlHelper; /** + * Switcher block + * * @api * @since 100.0.2 */ @@ -39,8 +40,6 @@ class Switcher extends \Magento\Framework\View\Element\Template private $urlHelper; /** - * Constructs - * * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Framework\Data\Helper\PostHelper $postDataHelper * @param array $data @@ -58,6 +57,8 @@ public function __construct( } /** + * Get current website Id. + * * @return int|null|string */ public function getCurrentWebsiteId() @@ -66,6 +67,8 @@ public function getCurrentWebsiteId() } /** + * Get current group Id. + * * @return int|null|string */ public function getCurrentGroupId() @@ -74,6 +77,8 @@ public function getCurrentGroupId() } /** + * Get current Store Id. + * * @return int */ public function getCurrentStoreId() @@ -82,6 +87,8 @@ public function getCurrentStoreId() } /** + * Get raw groups. + * * @return array */ public function getRawGroups() @@ -99,6 +106,8 @@ public function getRawGroups() } /** + * Get raw stores. + * * @return array */ public function getRawStores() @@ -170,6 +179,8 @@ public function getGroups() } /** + * Get stores. + * * @return \Magento\Store\Model\Store[] */ public function getStores() @@ -189,6 +200,8 @@ public function getStores() } /** + * Get current store code. + * * @return string */ public function getCurrentStoreCode() @@ -197,6 +210,8 @@ public function getCurrentStoreCode() } /** + * Is store in url. + * * @return bool */ public function isStoreInUrl() @@ -208,7 +223,7 @@ public function isStoreInUrl() } /** - * Get store code + * Get store code. * * @return string */ @@ -218,7 +233,7 @@ public function getStoreCode() } /** - * Get store name + * Get store name. * * @return null|string */ @@ -228,7 +243,7 @@ public function getStoreName() } /** - * Returns target store post data + * Returns target store post data. * * @param Store $store * @param array $data @@ -237,7 +252,7 @@ public function getStoreName() */ public function getTargetStorePostData(Store $store, $data = []) { - $data[\Magento\Store\Api\StoreResolverInterface::PARAM_NAME] = $store->getCode(); + $data[\Magento\Store\Model\StoreManagerInterface::PARAM_NAME] = $store->getCode(); $data['___from_store'] = $this->_storeManager->getStore()->getCode(); $urlOnTargetStore = $store->getCurrentUrl(false); diff --git a/app/code/Magento/Store/Controller/Store/SwitchAction.php b/app/code/Magento/Store/Controller/Store/SwitchAction.php index df946b45d60e..de721869c5ab 100644 --- a/app/code/Magento/Store/Controller/Store/SwitchAction.php +++ b/app/code/Magento/Store/Controller/Store/SwitchAction.php @@ -15,13 +15,13 @@ use Magento\Store\Api\StoreCookieManagerInterface; use Magento\Store\Api\StoreRepositoryInterface; use Magento\Store\Model\StoreIsInactiveException; -use Magento\Store\Model\StoreResolver; use Magento\Store\Model\StoreManagerInterface; use Magento\Store\Model\StoreSwitcher; use Magento\Store\Model\StoreSwitcherInterface; /** * Handles store switching url and makes redirect. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class SwitchAction extends Action @@ -81,13 +81,15 @@ public function __construct( } /** + * Execute action + * * @return void * @throws StoreSwitcher\CannotSwitchStoreException */ public function execute() { $targetStoreCode = $this->_request->getParam( - StoreResolver::PARAM_NAME, + \Magento\Store\Model\StoreManagerInterface::PARAM_NAME, $this->storeCookieManager->getStoreCodeFromCookie() ); $fromStoreCode = $this->_request->getParam('___from_store'); diff --git a/app/code/Magento/Store/Model/Argument/Interpreter/ServiceUrl.php b/app/code/Magento/Store/Model/Argument/Interpreter/ServiceUrl.php index cfa13e65ea42..4d4021f5528a 100644 --- a/app/code/Magento/Store/Model/Argument/Interpreter/ServiceUrl.php +++ b/app/code/Magento/Store/Model/Argument/Interpreter/ServiceUrl.php @@ -6,8 +6,8 @@ namespace Magento\Store\Model\Argument\Interpreter; use Magento\Framework\Data\Argument\InterpreterInterface; -use Magento\Store\Api\StoreResolverInterface; use Magento\Store\Model\StoreRepository; +use Magento\Store\Model\StoreManagerInterface; /** * Interpreter that builds Service URL by input path and optional parameters @@ -25,9 +25,9 @@ class ServiceUrl implements InterpreterInterface private $service; /** - * @var StoreResolverInterface + * @var StoreManagerInterface */ - private $storeResolver; + private $storeManager; /** * @var string @@ -41,21 +41,21 @@ class ServiceUrl implements InterpreterInterface /** * @param \Magento\Framework\Url $url - * @param StoreResolverInterface $storeResolver + * @param StoreManagerInterface $storeManager * @param StoreRepository $storeRepository * @param string $service * @param string $version */ public function __construct( \Magento\Framework\Url $url, - StoreResolverInterface $storeResolver, + StoreManagerInterface $storeManager, StoreRepository $storeRepository, $service = "rest", $version = "V1" ) { $this->url = $url; $this->service = $service; - $this->storeResolver = $storeResolver; + $this->storeManager = $storeManager; $this->version = $version; $this->storeRepository = $storeRepository; } @@ -67,14 +67,16 @@ public function __construct( */ private function getServiceUrl() { - $store = $this->storeRepository->getById($this->storeResolver->getCurrentStoreId()); + $store = $this->storeRepository->getById($this->storeManager->getStore()->getId()); return $this->url->getUrl( $this->service . "/" . $store->getCode() . "/" . $this->version ); } /** - * {@inheritdoc} + * Compute and return effective value of an argument + * + * @param array $data * @return string * @throws \InvalidArgumentException */ diff --git a/app/code/Magento/Store/Model/BaseUrlChecker.php b/app/code/Magento/Store/Model/BaseUrlChecker.php index b65a76e8806f..dbdcd9ff17bf 100644 --- a/app/code/Magento/Store/Model/BaseUrlChecker.php +++ b/app/code/Magento/Store/Model/BaseUrlChecker.php @@ -5,6 +5,8 @@ */ namespace Magento\Store\Model; +use Magento\Store\Model\ScopeInterface; + /** * Verifies that the requested URL matches to base URL of store. */ @@ -16,6 +18,8 @@ class BaseUrlChecker private $scopeConfig; /** + * BaseUrlChecker constructor. + * * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig */ public function __construct( @@ -47,9 +51,9 @@ public function execute($uri, $request) */ public function isEnabled() { - return (bool) $this->scopeConfig->getValue( + return $this->scopeConfig->isSetFlag( 'web/url/redirect_to_base', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE ); } @@ -62,13 +66,13 @@ public function isFrontendSecure() { $baseUrl = $this->scopeConfig->getValue( 'web/unsecure/base_url', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE ); $baseUrlParts = explode('://', $baseUrl); $baseUrlProtocol = array_shift($baseUrlParts); - $isSecure = (bool) $this->scopeConfig->getValue( + $isSecure = $this->scopeConfig->isSetFlag( 'web/secure/use_in_frontend', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE ); return $isSecure && $baseUrlProtocol == 'https'; diff --git a/app/code/Magento/Store/Model/Config/Importer/Processor/Create.php b/app/code/Magento/Store/Model/Config/Importer/Processor/Create.php index 513ba0480298..1fb9f1c224d3 100644 --- a/app/code/Magento/Store/Model/Config/Importer/Processor/Create.php +++ b/app/code/Magento/Store/Model/Config/Importer/Processor/Create.php @@ -17,8 +17,6 @@ /** * The processor for creating of new entities. - * - * {@inheritdoc} */ class Create implements ProcessorInterface { @@ -85,7 +83,9 @@ public function __construct( /** * Creates entities in application according to the data set. * - * {@inheritdoc} + * @param array $data The data to be processed + * @return void + * @throws RuntimeException If processor was unable to finish execution */ public function run(array $data) { @@ -177,8 +177,11 @@ private function createGroups(array $items, array $data) ); $group = $this->groupFactory->create(); + if (!isset($groupData['root_category_id'])) { + $groupData['root_category_id'] = 0; + } + $group->setData($groupData); - $group->setRootCategoryId(0); $group->getResource()->save($group); $group->getResource()->addCommitCallback(function () use ($data, $group, $website) { @@ -227,8 +230,7 @@ private function createStores(array $items, array $data) } /** - * Searches through given websites and compares with current websites. - * Returns found website. + * Searches through given websites and compares with current websites and returns found website. * * @param array $data The data to be searched in * @param string $websiteId The website id @@ -250,8 +252,7 @@ private function detectWebsiteById(array $data, $websiteId) } /** - * Searches through given groups and compares with current websites. - * Returns found group. + * Searches through given groups and compares with current websites and returns found group. * * @param array $data The data to be searched in * @param string $groupId The group id @@ -273,8 +274,7 @@ private function detectGroupById(array $data, $groupId) } /** - * Searches through given stores and compares with current stores. - * Returns found store. + * Searches through given stores and compares with current stores and returns found store. * * @param array $data The data to be searched in * @param string $storeId The store id diff --git a/app/code/Magento/Store/Model/Group.php b/app/code/Magento/Store/Model/Group.php index ccc3c6549142..4cd7bc93d333 100644 --- a/app/code/Magento/Store/Model/Group.php +++ b/app/code/Magento/Store/Model/Group.php @@ -100,18 +100,24 @@ class Group extends \Magento\Framework\Model\AbstractExtensibleModel implements */ private $eventManager; + /** + * @var \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface + */ + private $pillPut; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry * @param \Magento\Framework\Api\ExtensionAttributesFactory $extensionFactory * @param \Magento\Framework\Api\AttributeValueFactory $customAttributeFactory * @param \Magento\Config\Model\ResourceModel\Config\Data $configDataResource - * @param \Magento\Store\Model\Store $store - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource - * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection + * @param ResourceModel\Store\CollectionFactory $storeListFactory + * @param StoreManagerInterface $storeManager + * @param \Magento\Framework\Model\ResourceModel\AbstractResource|null $resource + * @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection * @param array $data * @param \Magento\Framework\Event\ManagerInterface|null $eventManager + * @param \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface|null $pillPut * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -125,13 +131,16 @@ public function __construct( \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], - \Magento\Framework\Event\ManagerInterface $eventManager = null + \Magento\Framework\Event\ManagerInterface $eventManager = null, + \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface $pillPut = null ) { $this->_configDataResource = $configDataResource; $this->_storeListFactory = $storeListFactory; $this->_storeManager = $storeManager; $this->eventManager = $eventManager ?: \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Framework\Event\ManagerInterface::class); + $this->pillPut = $pillPut ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface::class); parent::__construct( $context, $registry, @@ -244,6 +253,8 @@ public function getStoreCodes() } /** + * Get stores count + * * @return int */ public function getStoresCount() @@ -349,6 +360,8 @@ public function isCanDelete() } /** + * Get default store id + * * @return mixed */ public function getDefaultStoreId() @@ -365,6 +378,8 @@ public function setDefaultStoreId($defaultStoreId) } /** + * Get root category id + * * @return mixed */ public function getRootCategoryId() @@ -381,6 +396,8 @@ public function setRootCategoryId($rootCategoryId) } /** + * Get website id + * * @return mixed */ public function getWebsiteId() @@ -397,7 +414,7 @@ public function setWebsiteId($websiteId) } /** - * @return $this + * @inheritdoc */ public function beforeDelete() { @@ -445,6 +462,7 @@ public function afterSave() $this->_storeManager->reinitStores(); $this->eventManager->dispatch($this->_eventPrefix . '_save', ['group' => $group]); }); + $this->pillPut->put(); return parent::afterSave(); } @@ -473,7 +491,7 @@ public function getIdentities() } /** - * {@inheritdoc} + * @inheritdoc */ public function getName() { @@ -507,7 +525,7 @@ public function setCode($code) } /** - * {@inheritdoc} + * @inheritdoc */ public function getExtensionAttributes() { @@ -515,7 +533,7 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * @inheritdoc */ public function setExtensionAttributes( \Magento\Store\Api\Data\GroupExtensionInterface $extensionAttributes @@ -524,7 +542,7 @@ public function setExtensionAttributes( } /** - * {@inheritdoc} + * @inheritdoc * @since 100.1.0 */ public function getScopeType() @@ -533,7 +551,7 @@ public function getScopeType() } /** - * {@inheritdoc} + * @inheritdoc * @since 100.1.0 */ public function getScopeTypeName() diff --git a/app/code/Magento/Store/Model/HeaderProvider/Hsts.php b/app/code/Magento/Store/Model/HeaderProvider/Hsts.php index 623fbed57e6c..b22775e0987c 100644 --- a/app/code/Magento/Store/Model/HeaderProvider/Hsts.php +++ b/app/code/Magento/Store/Model/HeaderProvider/Hsts.php @@ -33,6 +33,8 @@ class Hsts extends \Magento\Framework\App\Response\HeaderProvider\AbstractHeader protected $scopeConfig; /** + * Hsts constructor. + * * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig */ public function __construct(\Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig) @@ -41,11 +43,11 @@ public function __construct(\Magento\Framework\App\Config\ScopeConfigInterface $ } /** - * {@inheritdoc} + * @inheritdoc */ public function canApply() { - return (bool)$this->scopeConfig->isSetFlag(Store::XML_PATH_SECURE_IN_FRONTEND) + return $this->scopeConfig->isSetFlag(Store::XML_PATH_SECURE_IN_FRONTEND) && $this->scopeConfig->isSetFlag(Store::XML_PATH_SECURE_IN_ADMINHTML) && $this->scopeConfig->isSetFlag(Store::XML_PATH_ENABLE_HSTS); } diff --git a/app/code/Magento/Store/Model/HeaderProvider/UpgradeInsecure.php b/app/code/Magento/Store/Model/HeaderProvider/UpgradeInsecure.php index a0bd015c1506..0ef8726e07f1 100644 --- a/app/code/Magento/Store/Model/HeaderProvider/UpgradeInsecure.php +++ b/app/code/Magento/Store/Model/HeaderProvider/UpgradeInsecure.php @@ -33,6 +33,8 @@ class UpgradeInsecure extends \Magento\Framework\App\Response\HeaderProvider\Abs protected $scopeConfig; /** + * UpgradeInsecure constructor. + * * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig */ public function __construct(\Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig) @@ -41,11 +43,11 @@ public function __construct(\Magento\Framework\App\Config\ScopeConfigInterface $ } /** - * {@inheritdoc} + * @inheritdoc */ public function canApply() { - return (bool)$this->scopeConfig->isSetFlag(Store::XML_PATH_SECURE_IN_FRONTEND) + return $this->scopeConfig->isSetFlag(Store::XML_PATH_SECURE_IN_FRONTEND) && $this->scopeConfig->isSetFlag(Store::XML_PATH_SECURE_IN_ADMINHTML) && $this->scopeConfig->isSetFlag(Store::XML_PATH_ENABLE_UPGRADE_INSECURE); } diff --git a/app/code/Magento/Store/Model/Plugin/StoreCookie.php b/app/code/Magento/Store/Model/Plugin/StoreCookie.php index 9fca86ab7176..7aa2c91652e8 100644 --- a/app/code/Magento/Store/Model/Plugin/StoreCookie.php +++ b/app/code/Magento/Store/Model/Plugin/StoreCookie.php @@ -12,8 +12,6 @@ use Magento\Store\Model\StoreIsInactiveException; use Magento\Framework\Exception\NoSuchEntityException; use \InvalidArgumentException; -use Magento\Store\Api\StoreResolverInterface; -use Magento\Framework\App\ObjectManager; /** * Class StoreCookie @@ -35,27 +33,19 @@ class StoreCookie */ protected $storeRepository; - /** - * @var StoreResolverInterface - */ - private $storeResolver; - /** * @param StoreManagerInterface $storeManager * @param StoreCookieManagerInterface $storeCookieManager * @param StoreRepositoryInterface $storeRepository - * @param StoreResolverInterface $storeResolver */ public function __construct( StoreManagerInterface $storeManager, StoreCookieManagerInterface $storeCookieManager, - StoreRepositoryInterface $storeRepository, - StoreResolverInterface $storeResolver = null + StoreRepositoryInterface $storeRepository ) { $this->storeManager = $storeManager; $this->storeCookieManager = $storeCookieManager; $this->storeRepository = $storeRepository; - $this->storeResolver = $storeResolver ?: ObjectManager::getInstance()->get(StoreResolverInterface::class); } /** diff --git a/app/code/Magento/Store/Model/Store.php b/app/code/Magento/Store/Model/Store.php index af2595725742..f62762986cb3 100644 --- a/app/code/Magento/Store/Model/Store.php +++ b/app/code/Magento/Store/Model/Store.php @@ -33,6 +33,7 @@ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.ExcessivePublicCount) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @since 100.0.2 */ class Store extends AbstractExtensibleModel implements @@ -325,6 +326,11 @@ class Store extends AbstractExtensibleModel implements */ private $eventManager; + /** + * @var \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface + */ + private $pillPut; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -351,6 +357,7 @@ class Store extends AbstractExtensibleModel implements * @param bool $isCustomEntryPoint * @param array $data optional generic object data * @param \Magento\Framework\Event\ManagerInterface|null $eventManager + * @param \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface|null $pillPut * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -379,7 +386,8 @@ public function __construct( \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, $isCustomEntryPoint = false, array $data = [], - \Magento\Framework\Event\ManagerInterface $eventManager = null + \Magento\Framework\Event\ManagerInterface $eventManager = null, + \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface $pillPut = null ) { $this->_coreFileStorageDatabase = $coreFileStorageDatabase; $this->_config = $config; @@ -400,6 +408,8 @@ public function __construct( $this->websiteRepository = $websiteRepository; $this->eventManager = $eventManager ?: \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Framework\Event\ManagerInterface::class); + $this->pillPut = $pillPut ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface::class); parent::__construct( $context, $registry, @@ -412,10 +422,15 @@ public function __construct( } /** - * @return string[] + * @inheritdoc + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + $properties = parent::__sleep(); $properties = array_diff($properties, ['_coreFileStorageDatabase', '_config']); return $properties; @@ -425,9 +440,14 @@ public function __sleep() * Init not serializable fields * * @return void + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + parent::__wakeup(); $this->_coreFileStorageDatabase = ObjectManager::getInstance() ->get(\Magento\MediaStorage\Helper\File\Storage\Database::class); @@ -464,6 +484,7 @@ protected function _getSession() * Validation rules for store * * @return \Zend_Validate_Interface|null + * @throws \Zend_Validate_Exception */ protected function _getValidationRulesBeforeSave() { @@ -489,9 +510,11 @@ protected function _getValidationRulesBeforeSave() /** * Loading store data * - * @param mixed $key - * @param string $field - * @return $this + * @param mixed $key + * @param string $field + * + * @return $this + * @throws \Magento\Framework\Exception\LocalizedException */ public function load($key, $field = null) { @@ -566,6 +589,7 @@ public function setWebsite(Website $website) * Retrieve store website * * @return Website|bool + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function getWebsite() { @@ -578,9 +602,11 @@ public function getWebsite() /** * Retrieve url using store configuration specific * - * @param string $route - * @param array $params - * @return string + * @param string $route + * @param array $params + * + * @return string + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function getUrl($route = '', $params = []) { @@ -700,7 +726,8 @@ protected function _updatePathUseRewrites($url) if ($this->_isCustomEntryPoint()) { $indexFileName = 'index.php'; } else { - $indexFileName = basename($_SERVER['SCRIPT_FILENAME']); + $scriptFilename = $this->_request->getServer('SCRIPT_FILENAME'); + $indexFileName = basename($scriptFilename); } $url .= $indexFileName . '/'; } @@ -786,7 +813,7 @@ public function isFrontUrlSecure() } /** - * @return bool + * @inheritdoc */ public function isUrlSecure() { @@ -880,8 +907,10 @@ public function getDefaultCurrency() /** * Set current store currency code * - * @param string $code - * @return string + * @param string $code + * + * @return string + * @throws \Magento\Framework\Exception\LocalizedException */ public function setCurrentCurrencyCode($code) { @@ -889,7 +918,10 @@ public function setCurrentCurrencyCode($code) if (in_array($code, $this->getAvailableCurrencyCodes())) { $this->_getSession()->setCurrencyCode($code); - $defaultCode = $this->_storeManager->getWebsite()->getDefaultStore()->getDefaultCurrency()->getCode(); + $defaultCode = ($this->_storeManager->getStore() !== null) + ? $this->_storeManager->getStore()->getDefaultCurrency()->getCode() + : $this->_storeManager->getWebsite()->getDefaultStore()->getDefaultCurrency()->getCode(); + $this->_httpContext->setValue(Context::CONTEXT_CURRENCY, $code, $defaultCode); } return $this; @@ -902,23 +934,17 @@ public function setCurrentCurrencyCode($code) */ public function getCurrentCurrencyCode() { + $availableCurrencyCodes = \array_values($this->getAvailableCurrencyCodes(true)); // try to get currently set code among allowed - $code = $this->_httpContext->getValue(Context::CONTEXT_CURRENCY); - $code = $code === null ? $this->_getSession()->getCurrencyCode() : $code; - if (empty($code)) { + $code = $this->_httpContext->getValue(Context::CONTEXT_CURRENCY) ?? $this->_getSession()->getCurrencyCode(); + if (empty($code) || !\in_array($code, $availableCurrencyCodes)) { $code = $this->getDefaultCurrencyCode(); - } - if (in_array($code, $this->getAvailableCurrencyCodes(true))) { - return $code; + if (!\in_array($code, $availableCurrencyCodes) && !empty($availableCurrencyCodes)) { + $code = $availableCurrencyCodes[0]; + } } - // take first one of allowed codes - $codes = array_values($this->getAvailableCurrencyCodes(true)); - if (empty($codes)) { - // return default code, if no codes specified at all - return $this->getDefaultCurrencyCode(); - } - return array_shift($codes); + return $code; } /** @@ -972,6 +998,7 @@ public function getAllowedCurrencies() * Retrieve store current currency * * @return Currency + * @throws \Magento\Framework\Exception\LocalizedException */ public function getCurrentCurrency() { @@ -994,6 +1021,7 @@ public function getCurrentCurrency() * Retrieve current currency rate * * @return float + * @throws \Magento\Framework\Exception\LocalizedException */ public function getCurrentCurrencyRate() { @@ -1004,6 +1032,7 @@ public function getCurrentCurrencyRate() * Retrieve root category identifier * * @return int + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function getRootCategoryId() { @@ -1029,6 +1058,7 @@ public function setGroup(Group $group) * Retrieve group model * * @return Group|bool + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function getGroup() { @@ -1067,6 +1097,7 @@ public function afterSave() $this->getResource()->addCommitCallback(function () use ($event, $store) { $this->eventManager->dispatch($event, ['store' => $store]); }); + $this->pillPut->put(); return parent::afterSave(); } @@ -1136,6 +1167,7 @@ public function getDefaultGroupId() * Check if store can be deleted * * @return boolean + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function isCanDelete() { @@ -1151,6 +1183,7 @@ public function isCanDelete() * * @return boolean * @since 100.1.0 + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function isDefault() { @@ -1164,9 +1197,11 @@ public function isDefault() * Retrieve current url for store * * @param bool $fromStore + * * @return string * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function getCurrentUrl($fromStore = true) { @@ -1242,6 +1277,7 @@ public function isActive() * Protect delete from non admin area * * @return $this + * @throws \Magento\Framework\Exception\LocalizedException */ public function beforeDelete() { @@ -1253,6 +1289,7 @@ public function beforeDelete() * Rewrite in order to clear configuration cache * * @return $this + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function afterDelete() { @@ -1312,6 +1349,7 @@ public function isReadOnly($value = null) * Retrieve store group name * * @return string + * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function getFrontendName() { @@ -1344,17 +1382,18 @@ public function getIdentities() } /** + * Return Store Path + * * @return string */ public function getStorePath() { $parsedUrl = parse_url($this->getBaseUrl()); - return isset($parsedUrl['path']) ? $parsedUrl['path'] : '/'; + return $parsedUrl['path'] ?? '/'; } /** - * {@inheritdoc} - * @since 100.1.0 + * @inheritdoc */ public function getScopeType() { @@ -1362,8 +1401,7 @@ public function getScopeType() } /** - * {@inheritdoc} - * @since 100.1.0 + * @inheritdoc */ public function getScopeTypeName() { @@ -1371,7 +1409,7 @@ public function getScopeTypeName() } /** - * {@inheritdoc} + * @inheritdoc */ public function getExtensionAttributes() { @@ -1379,8 +1417,7 @@ public function getExtensionAttributes() } /** - * @param \Magento\Store\Api\Data\StoreExtensionInterface $extensionAttributes - * @return $this + * @inheritdoc */ public function setExtensionAttributes( \Magento\Store\Api\Data\StoreExtensionInterface $extensionAttributes diff --git a/app/code/Magento/Store/Model/StoreManager.php b/app/code/Magento/Store/Model/StoreManager.php index 445824baadfe..0fce3a521705 100644 --- a/app/code/Magento/Store/Model/StoreManager.php +++ b/app/code/Magento/Store/Model/StoreManager.php @@ -6,6 +6,7 @@ namespace Magento\Store\Model; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Store\Api\StoreResolverInterface; use Magento\Store\Model\ResourceModel\StoreWebsiteRelation; @@ -87,6 +88,8 @@ class StoreManager implements protected $isSingleStoreAllowed; /** + * StoreManager constructor. + * * @param \Magento\Store\Api\StoreRepositoryInterface $storeRepository * @param \Magento\Store\Api\GroupRepositoryInterface $groupRepository * @param \Magento\Store\Api\WebsiteRepositoryInterface $websiteRepository @@ -114,7 +117,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function setCurrentStore($store) { @@ -122,7 +125,7 @@ public function setCurrentStore($store) } /** - * {@inheritdoc} + * @inheritdoc */ public function setIsSingleStoreModeAllowed($value) { @@ -130,7 +133,7 @@ public function setIsSingleStoreModeAllowed($value) } /** - * {@inheritdoc} + * @inheritdoc */ public function hasSingleStore() { @@ -139,7 +142,7 @@ public function hasSingleStore() } /** - * {@inheritdoc} + * @inheritdoc */ public function isSingleStoreMode() { @@ -147,7 +150,7 @@ public function isSingleStoreMode() } /** - * {@inheritdoc} + * @inheritdoc */ public function getStore($storeId = null) { @@ -171,7 +174,7 @@ public function getStore($storeId = null) } /** - * {@inheritdoc} + * @inheritdoc */ public function getStores($withDefault = false, $codeKey = false) { @@ -190,7 +193,7 @@ public function getStores($withDefault = false, $codeKey = false) } /** - * {@inheritdoc} + * @inheritdoc */ public function getWebsite($websiteId = null) { @@ -210,7 +213,7 @@ public function getWebsite($websiteId = null) } /** - * {@inheritdoc} + * @inheritdoc */ public function getWebsites($withDefault = false, $codeKey = false) { @@ -229,7 +232,7 @@ public function getWebsites($withDefault = false, $codeKey = false) } /** - * {@inheritdoc} + * @inheritdoc */ public function reinitStores() { @@ -242,7 +245,7 @@ public function reinitStores() } /** - * {@inheritdoc} + * @inheritdoc */ public function getDefaultStoreView() { @@ -252,7 +255,7 @@ public function getDefaultStoreView() } /** - * {@inheritdoc} + * @inheritdoc */ public function getGroup($groupId = null) { @@ -267,7 +270,7 @@ public function getGroup($groupId = null) } /** - * {@inheritdoc} + * @inheritdoc */ public function getGroups($withDefault = false) { @@ -291,13 +294,15 @@ function ($item) { */ protected function isSingleStoreModeEnabled() { - return (bool)$this->scopeConfig->getValue( + return $this->scopeConfig->isSetFlag( self::XML_PATH_SINGLE_STORE_MODE_ENABLED, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE ); } /** + * Get Store Website Relation + * * @deprecated 100.2.0 * @return StoreWebsiteRelation */ diff --git a/app/code/Magento/Store/Model/StoreManagerInterface.php b/app/code/Magento/Store/Model/StoreManagerInterface.php index 220155c47f6d..f440515f23e0 100644 --- a/app/code/Magento/Store/Model/StoreManagerInterface.php +++ b/app/code/Magento/Store/Model/StoreManagerInterface.php @@ -21,6 +21,11 @@ interface StoreManagerInterface */ const CONTEXT_STORE = 'store'; + /** + * The store GET Param name + */ + const PARAM_NAME = '___store'; + /** * Allow or disallow single store mode * diff --git a/app/code/Magento/Store/Model/StoreResolver.php b/app/code/Magento/Store/Model/StoreResolver.php index 3f449f10c6d4..aafdd1513898 100644 --- a/app/code/Magento/Store/Model/StoreResolver.php +++ b/app/code/Magento/Store/Model/StoreResolver.php @@ -3,10 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -namespace Magento\Store\Model; +declare(strict_types=1); -use Magento\Framework\Serialize\SerializerInterface; +namespace Magento\Store\Model; +/** + * Class used to resolve store from url path or get parameters or cookie. + */ class StoreResolver implements \Magento\Store\Api\StoreResolverInterface { /** @@ -25,12 +28,12 @@ class StoreResolver implements \Magento\Store\Api\StoreResolverInterface protected $storeCookieManager; /** - * @var \Magento\Framework\Cache\FrontendInterface + * @deprecated */ protected $cache; /** - * @var \Magento\Store\Model\StoreResolver\ReaderList + * @deprecated */ protected $readerList; @@ -45,56 +48,70 @@ class StoreResolver implements \Magento\Store\Api\StoreResolverInterface protected $scopeCode; /** - * @var \Magento\Framework\App\RequestInterface + * @var \Magento\Framework\App\Request\Http */ protected $request; /** - * @var \Magento\Framework\Serialize\SerializerInterface + * @var StoresData + */ + private $storesData; + + /** + * @var \Magento\Store\App\Request\StorePathInfoValidator */ - private $serializer; + private $storePathInfoValidator; /** * @param \Magento\Store\Api\StoreRepositoryInterface $storeRepository * @param \Magento\Store\Api\StoreCookieManagerInterface $storeCookieManager - * @param \Magento\Framework\App\RequestInterface $request - * @param \Magento\Framework\Cache\FrontendInterface $cache - * @param \Magento\Store\Model\StoreResolver\ReaderList $readerList - * @param string $runMode - * @param null $scopeCode + * @param \Magento\Framework\App\Request\Http $request + * @param \Magento\Store\Model\StoresData $storesData + * @param \Magento\Store\App\Request\StorePathInfoValidator $storePathInfoValidator + * @param string|null $runMode + * @param string|null $scopeCode */ public function __construct( \Magento\Store\Api\StoreRepositoryInterface $storeRepository, \Magento\Store\Api\StoreCookieManagerInterface $storeCookieManager, - \Magento\Framework\App\RequestInterface $request, - \Magento\Framework\Cache\FrontendInterface $cache, - \Magento\Store\Model\StoreResolver\ReaderList $readerList, + \Magento\Framework\App\Request\Http $request, + \Magento\Store\Model\StoresData $storesData, + \Magento\Store\App\Request\StorePathInfoValidator $storePathInfoValidator, $runMode = ScopeInterface::SCOPE_STORE, $scopeCode = null ) { $this->storeRepository = $storeRepository; $this->storeCookieManager = $storeCookieManager; $this->request = $request; - $this->cache = $cache; - $this->readerList = $readerList; + $this->storePathInfoValidator = $storePathInfoValidator; + $this->storesData = $storesData; $this->runMode = $scopeCode ? $runMode : ScopeInterface::SCOPE_WEBSITE; $this->scopeCode = $scopeCode; } /** - * {@inheritdoc} + * @inheritdoc */ public function getCurrentStoreId() { list($stores, $defaultStoreId) = $this->getStoresData(); - $storeCode = $this->request->getParam(self::PARAM_NAME, $this->storeCookieManager->getStoreCodeFromCookie()); + $storeCode = $this->storePathInfoValidator->getValidStoreCode($this->request); + + if (!$storeCode) { + $storeCode = $this->request->getParam( + \Magento\Store\Model\StoreManagerInterface::PARAM_NAME, + $this->storeCookieManager->getStoreCodeFromCookie() + ); + } + if (is_array($storeCode)) { if (!isset($storeCode['_data']['code'])) { throw new \InvalidArgumentException(__('Invalid store parameter.')); } $storeCode = $storeCode['_data']['code']; } + if ($storeCode) { try { $store = $this->getRequestedStoreByCode($storeCode); @@ -116,28 +133,21 @@ public function getCurrentStoreId() * * @return array */ - protected function getStoresData() + protected function getStoresData() : array { - $cacheKey = 'resolved_stores_' . md5($this->runMode . $this->scopeCode); - $cacheData = $this->cache->load($cacheKey); - if ($cacheData) { - $storesData = $this->getSerializer()->unserialize($cacheData); - } else { - $storesData = $this->readStoresData(); - $this->cache->save($this->getSerializer()->serialize($storesData), $cacheKey, [self::CACHE_TAG]); - } - return $storesData; + return $this->storesData->getStoresData($this->runMode, $this->scopeCode); } /** * Read stores data. First element is allowed store ids, second is default store id * * @return array + * @deprecated + * @see \Magento\Store\Model\StoreResolver::getStoresData */ - protected function readStoresData() + protected function readStoresData() : array { - $reader = $this->readerList->getReader($this->runMode); - return [$reader->getAllowedStoreIds($this->scopeCode), $reader->getDefaultStoreId($this->scopeCode)]; + return $this->getStoresData(); } /** @@ -147,7 +157,7 @@ protected function readStoresData() * @return \Magento\Store\Api\Data\StoreInterface * @throws \Magento\Framework\Exception\NoSuchEntityException */ - protected function getRequestedStoreByCode($storeCode) + protected function getRequestedStoreByCode($storeCode) : \Magento\Store\Api\Data\StoreInterface { try { $store = $this->storeRepository->getActiveStoreByCode($storeCode); @@ -165,7 +175,7 @@ protected function getRequestedStoreByCode($storeCode) * @return \Magento\Store\Api\Data\StoreInterface * @throws \Magento\Framework\Exception\NoSuchEntityException */ - protected function getDefaultStoreById($id) + protected function getDefaultStoreById($id) : \Magento\Store\Api\Data\StoreInterface { try { $store = $this->storeRepository->getActiveStoreById($id); @@ -175,19 +185,4 @@ protected function getDefaultStoreById($id) return $store; } - - /** - * Get serializer - * - * @return \Magento\Framework\Serialize\SerializerInterface - * @deprecated 100.2.0 - */ - private function getSerializer() - { - if ($this->serializer === null) { - $this->serializer = \Magento\Framework\App\ObjectManager::getInstance() - ->get(SerializerInterface::class); - } - return $this->serializer; - } } diff --git a/app/code/Magento/Store/Model/StoreResolver/Website.php b/app/code/Magento/Store/Model/StoreResolver/Website.php index 29f85716fea2..e314538099c8 100644 --- a/app/code/Magento/Store/Model/StoreResolver/Website.php +++ b/app/code/Magento/Store/Model/StoreResolver/Website.php @@ -5,6 +5,9 @@ */ namespace Magento\Store\Model\StoreResolver; +/** + * Reader implementation for website. + */ class Website implements ReaderInterface { /** @@ -38,22 +41,25 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getAllowedStoreIds($scopeCode) { $stores = []; $website = $scopeCode ? $this->websiteRepository->get($scopeCode) : $this->websiteRepository->getDefault(); foreach ($this->storeRepository->getList() as $store) { - if ($store->isActive() && $store->getWebsiteId() == $website->getId()) { - $stores[] = $store->getId(); + if ($store->getIsActive()) { + if (($scopeCode && $store->getWebsiteId() == $website->getId()) || (!$scopeCode)) { + $stores[$store->getId()] = $store->getId(); + } } } + sort($stores); return $stores; } /** - * {@inheritdoc} + * @inheritdoc */ public function getDefaultStoreId($scopeCode) { diff --git a/app/code/Magento/Store/Model/StoresData.php b/app/code/Magento/Store/Model/StoresData.php new file mode 100644 index 000000000000..b3d00bc97cd2 --- /dev/null +++ b/app/code/Magento/Store/Model/StoresData.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model; + +/** + * Class that computes and stores into cache the active store ids. + */ +class StoresData +{ + /** + * Cache tag + */ + const CACHE_TAG = 'store_relations'; + + /** + * @var \Magento\Framework\Cache\FrontendInterface + */ + private $cache; + + /** + * @var \Magento\Store\Model\StoreResolver\ReaderList + */ + private $readerList; + + /** + * @var \Magento\Framework\Serialize\SerializerInterface + */ + private $serializer; + + /** + * @param \Magento\Framework\Cache\FrontendInterface $cache + * @param \Magento\Store\Model\StoreResolver\ReaderList $readerList + * @param \Magento\Framework\Serialize\SerializerInterface $serializer + */ + public function __construct( + \Magento\Framework\Cache\FrontendInterface $cache, + \Magento\Store\Model\StoreResolver\ReaderList $readerList, + \Magento\Framework\Serialize\SerializerInterface $serializer + ) { + $this->cache = $cache; + $this->readerList = $readerList; + $this->serializer = $serializer; + } + + /** + * Get stores data + * + * @param string $runMode + * @param string|null $scopeCode + * @return array + */ + public function getStoresData(string $runMode, string $scopeCode = null) : array + { + $cacheKey = 'resolved_stores_' . md5($runMode . $scopeCode); + $cacheData = $this->cache->load($cacheKey); + if ($cacheData) { + $storesData = $this->serializer->unserialize($cacheData); + } else { + $reader = $this->readerList->getReader($runMode); + $storesData = [$reader->getAllowedStoreIds($scopeCode), $reader->getDefaultStoreId($scopeCode)]; + $this->cache->save( + $this->serializer->serialize($storesData), + $cacheKey, + [ + self::CACHE_TAG, + \Magento\Store\Model\Store::CACHE_TAG + ] + ); + } + return $storesData; + } +} diff --git a/app/code/Magento/Store/Model/Website.php b/app/code/Magento/Store/Model/Website.php index c9a7d0013fe0..42c89bbe4210 100644 --- a/app/code/Magento/Store/Model/Website.php +++ b/app/code/Magento/Store/Model/Website.php @@ -159,6 +159,11 @@ class Website extends \Magento\Framework\Model\AbstractExtensibleModel implement */ protected $_currencyFactory; + /** + * @var \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface + */ + private $pillPut; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -174,6 +179,7 @@ class Website extends \Magento\Framework\Model\AbstractExtensibleModel implement * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data + * @param \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface|null $pillPut * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -190,7 +196,8 @@ public function __construct( \Magento\Directory\Model\CurrencyFactory $currencyFactory, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface $pillPut = null ) { parent::__construct( $context, @@ -208,10 +215,12 @@ public function __construct( $this->_websiteFactory = $websiteFactory; $this->_storeManager = $storeManager; $this->_currencyFactory = $currencyFactory; + $this->pillPut = $pillPut ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface::class); } /** - * init model + * Init model * * @return void */ @@ -495,6 +504,8 @@ public function getWebsiteGroupStore() } /** + * Get default group id + * * @return mixed */ public function getDefaultGroupId() @@ -511,6 +522,8 @@ public function setDefaultGroupId($defaultGroupId) } /** + * Get code + * * @return mixed */ public function getCode() @@ -543,7 +556,7 @@ public function setName($name) } /** - * @return $this + * @inheritdoc */ public function beforeDelete() { @@ -581,7 +594,7 @@ public function afterSave() if ($this->isObjectNew()) { $this->_storeManager->reinitStores(); } - + $this->pillPut->put(); return parent::afterSave(); } @@ -635,8 +648,7 @@ public function getDefaultStore() } /** - * Retrieve default stores select object - * Select fields website_id, store_id + * Retrieve default stores select object, select fields website_id, store_id * * @param bool $withDefault include/exclude default admin website * @return \Magento\Framework\DB\Select @@ -671,7 +683,7 @@ public function getIdentities() } /** - * {@inheritdoc} + * @inheritdoc * @since 100.1.0 */ public function getScopeType() @@ -680,7 +692,7 @@ public function getScopeType() } /** - * {@inheritdoc} + * @inheritdoc * @since 100.1.0 */ public function getScopeTypeName() @@ -689,7 +701,7 @@ public function getScopeTypeName() } /** - * {@inheritdoc} + * @inheritdoc */ public function getExtensionAttributes() { @@ -697,7 +709,7 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * @inheritdoc */ public function setExtensionAttributes( \Magento\Store\Api\Data\WebsiteExtensionInterface $extensionAttributes diff --git a/app/code/Magento/Store/Model/WebsiteRepository.php b/app/code/Magento/Store/Model/WebsiteRepository.php index b58936c32f59..e01bc8191b35 100644 --- a/app/code/Magento/Store/Model/WebsiteRepository.php +++ b/app/code/Magento/Store/Model/WebsiteRepository.php @@ -6,10 +6,10 @@ namespace Magento\Store\Model; +use Magento\Framework\App\Config; use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Store\Model\ResourceModel\Website\CollectionFactory; -use Magento\Framework\App\Config; /** * Information Expert in store websites handling @@ -64,7 +64,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function get($code) { @@ -78,7 +78,14 @@ public function get($code) ]); if ($website->getId() === null) { - throw new NoSuchEntityException(); + throw new NoSuchEntityException( + __( + sprintf( + "The website with code %s that was requested wasn't found. Verify the website and try again.", + $code + ) + ) + ); } $this->entities[$code] = $website; $this->entitiesById[$website->getId()] = $website; @@ -86,7 +93,7 @@ public function get($code) } /** - * {@inheritdoc} + * @inheritdoc */ public function getById($id) { @@ -100,7 +107,14 @@ public function getById($id) ]); if ($website->getId() === null) { - throw new NoSuchEntityException(); + throw new NoSuchEntityException( + __( + sprintf( + "The website with id %s that was requested wasn't found. Verify the website and try again.", + $id + ) + ) + ); } $this->entities[$website->getCode()] = $website; $this->entitiesById[$id] = $website; @@ -108,7 +122,7 @@ public function getById($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function getList() { @@ -127,7 +141,7 @@ public function getList() } /** - * {@inheritdoc} + * @inheritdoc */ public function getDefault() { @@ -150,7 +164,7 @@ public function getDefault() } /** - * {@inheritdoc} + * @inheritdoc */ public function clean() { @@ -176,6 +190,7 @@ private function getAppConfig() /** * Initialize default website. + * * @return void */ private function initDefaultWebsite() diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateNewStoreGroupActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateNewStoreGroupActionGroup.xml index 91fe4fccddb9..870b92d17e67 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateNewStoreGroupActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateNewStoreGroupActionGroup.xml @@ -25,4 +25,47 @@ <waitForElementVisible selector="{{AdminStoresGridSection.storeGrpFilterTextField}}" stepKey="waitForStoreGridReload"/> <see userInput="You saved the store." stepKey="seeSavedMessage" /> </actionGroup> + <actionGroup name="CreateCustomStore"> + <arguments> + <argument name="website" type="string"/> + <argument name="store" type="string"/> + <argument name="rootCategory" type="string"/> + </arguments> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <waitForPageLoad stepKey="waitForSystemStorePage"/> + <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> + <selectOption userInput="{{website}}" selector="{{AdminNewStoreGroupSection.storeGrpWebsiteDropdown}}" stepKey="selectMainWebsite"/> + <fillField userInput="{{store}}" selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" stepKey="fillStoreName"/> + <fillField userInput="{{store}}" selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" stepKey="fillStoreCode"/> + <selectOption userInput="{{rootCategory}}" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectStoreStatus"/> + <click selector="{{AdminStoreGroupActionsSection.saveButton}}" stepKey="clickSaveStoreGroup" /> + <waitForElementVisible selector="{{AdminStoresGridSection.storeGrpFilterTextField}}" stepKey="waitForStoreGridReload"/> + <see userInput="You saved the store." stepKey="seeSavedMessage"/> + </actionGroup> + <actionGroup name="AssertStoreGroupInGrid"> + <arguments> + <argument name="storeGroupName" type="string"/> + </arguments> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <waitForPageLoad stepKey="waitForAdminSystemStorePageLoad"/> + <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="resetSearchFilter"/> + <fillField userInput="{{storeGroupName}}" selector="{{AdminStoresGridSection.storeGrpFilterTextField}}" stepKey="fillSearchStoreGroupField"/> + <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForStoreToLoad"/> + <see selector="{{AdminStoresGridSection.nthRow('1')}}" userInput="{{storeGroupName}}" stepKey="seeAssertStoreGroupInGridMessage"/> + </actionGroup> + <actionGroup name="AssertStoreGroupForm"> + <arguments> + <argument name="website" type="string"/> + <argument name="storeGroupName" type="string"/> + <argument name="storeGroupCode" type="string"/> + <argument name="rootCategory" type="string"/> + </arguments> + <click selector="{{AdminStoresGridSection.storeGrpNameInFirstRow}}" stepKey="clickEditExistingStoreRow"/> + <waitForPageLoad stepKey="waitTillAdminSystemStoreGroupPage"/> + <seeInField selector="{{AdminNewStoreGroupSection.storeGrpWebsiteDropdown}}" userInput="{{website}}" stepKey="seeAssertWebsite"/> + <seeInField selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" userInput="{{storeGroupName}}" stepKey="seeAssertStoreGroupName"/> + <seeInField selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" userInput="{{storeGroupCode}}" stepKey="seeAssertStoreGroupCode"/> + <seeInField selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" userInput="{{rootCategory}}" stepKey="seeAssertRootCategory"/> + </actionGroup> </actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateStoreGroupActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateStoreGroupActionGroup.xml deleted file mode 100644 index b21c79692a7c..000000000000 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateStoreGroupActionGroup.xml +++ /dev/null @@ -1,26 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="AdminCreateStoreGroupActionGroup"> - <arguments> - <argument name="Website" defaultValue="_defaultWebsite"/> - </arguments> - <amOnPage url="{{AdminSystemStoreGroupPage.url}}" stepKey="navigateToNewStoreGroup"/> - <waitForPageLoad stepKey="waitForStoreGroupPageLoad" /> - - <comment userInput="Creating Store Group" stepKey="storeGroupCreationComment" /> - <selectOption selector="{{AdminNewStoreGroupSection.storeGrpWebsiteDropdown}}" userInput="{{Website.name}}" stepKey="selectWebsite" /> - <fillField selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" userInput="{{CustomStoreGroupCustomWebsite.name}}" stepKey="enterStoreGroupName" /> - <fillField selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" userInput="{{CustomStoreGroupCustomWebsite.code}}" stepKey="enterStoreGroupCode" /> - <selectOption selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" userInput="Default Category" stepKey="setRootCategory" /> - <click selector="{{AdminNewStoreGroupActionsSection.saveButton}}" stepKey="clickSaveStoreGroup" /> - <waitForElementVisible selector="{{AdminStoresGridSection.storeFilterTextField}}" stepKey="waitForPageReload"/> - <see userInput="You saved the store." stepKey="seeSavedMessage" /> - </actionGroup> -</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateStoreViewActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateStoreViewActionGroup.xml index 99ca7991e5e9..fda92810a13b 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateStoreViewActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateStoreViewActionGroup.xml @@ -8,6 +8,7 @@ <!-- Test XML Example --> <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCreateStoreViewActionGroup"> <arguments> <argument name="StoreGroup" defaultValue="_defaultStoreGroup"/> @@ -15,6 +16,7 @@ </arguments> <amOnPage url="{{AdminSystemStoreViewPage.url}}" stepKey="navigateToNewStoreView"/> <waitForPageLoad stepKey="waitForPageLoad1" /> + <comment userInput="Creating Store View" stepKey="storeViewCreationComment" /> <!--Create Store View--> <selectOption selector="{{AdminNewStoreSection.storeGrpDropdown}}" userInput="{{StoreGroup.name}}" stepKey="selectStore" /> <fillField selector="{{AdminNewStoreSection.storeNameTextField}}" userInput="{{customStore.name}}" stepKey="enterStoreViewName" /> @@ -22,9 +24,45 @@ <selectOption selector="{{AdminNewStoreSection.statusDropdown}}" userInput="Enabled" stepKey="setStatus" /> <click selector="{{AdminNewStoreViewActionsSection.saveButton}}" stepKey="clickSaveStoreView" /> <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForModal" /> - <see selector="{{AdminConfirmationModalSection.title}}" userInput="Warning message" stepKey="seeWarning" /> - <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="dismissModal" /> - <waitForElementVisible selector="{{AdminStoresGridSection.storeFilterTextField}}" stepKey="waitForPageReolad"/> + <see selector="{{AdminConfirmationModalSection.title}}" userInput="Warning message" stepKey="seeWarningAboutTakingALongTimeToComplete" /> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmModal" /> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForPageReload"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the store view." stepKey="seeSavedMessage" /> + </actionGroup> + + <actionGroup name="AdminCreateStoreViewWithoutCheckActionGroup" extends="AdminCreateStoreViewActionGroup"> + <remove keyForRemoval="waitForPageReload"/> + <remove keyForRemoval="seeSavedMessage"/> + </actionGroup> + + <!--Save the Store view--> + <actionGroup name="AdminCreateStoreViewActionSaveGroup"> + <waitForLoadingMaskToDisappear stepKey="waitForGridLoad"/> + <waitForElementVisible selector="{{AdminStoresGridSection.websiteFilterTextField}}" stepKey="waitForStoreGridToReload2"/> <see userInput="You saved the store view." stepKey="seeSavedMessage" /> </actionGroup> + + <actionGroup name="navigateToAdminContentManagementPage"> + <amOnPage url="{{AdminContentManagementPage.url}}" stepKey="navigateToConfigurationPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + </actionGroup> + <actionGroup name="saveStoreConfiguration"> + <comment userInput="saveStoreConfiguration" stepKey="comment"/> + <waitForElementVisible selector="{{StoreConfigSection.Save}}" stepKey="waitForSaveButton"/> + <click selector="{{StoreConfigSection.Save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> + <actionGroup name="saveStoreConfigurationAndValidateFieldError"> + <arguments> + <argument name="inputFieldError" type="string"/> + <argument name="errorMessageSelector" type="string"/> + <argument name="errorMessage" type="string"/> + </arguments> + <comment userInput="saveStoreConfigurationAndValidateFieldError" stepKey="comment"/> + <waitForElementVisible selector="{{StoreConfigSection.Save}}" stepKey="waitForSaveButton"/> + <click selector="{{StoreConfigSection.Save}}" stepKey="clickSaveButton"/> + <waitForElement selector="{{inputFieldError}}" stepKey="waitForErrorField"/> + <waitForElementVisible selector="{{errorMessageSelector}}" stepKey="waitForErrorMessage"/> + <see selector="{{errorMessageSelector}}" userInput="{{errorMessage}}" stepKey="seeErrorMessage"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateWebsiteActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateWebsiteActionGroup.xml index a87356303c6e..ca614ec24138 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateWebsiteActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateWebsiteActionGroup.xml @@ -22,4 +22,44 @@ <waitForElementVisible selector="{{AdminStoresGridSection.websiteFilterTextField}}" stepKey="waitForStoreGridToReload"/> <see userInput="You saved the website." stepKey="seeSavedMessage" /> </actionGroup> + + <!--Get Website_id--> + <actionGroup name="AdminGetWebsiteIdActionGroup"> + <arguments> + <argument name="website"/> + </arguments> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnTheStorePage"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters"/> + <fillField selector="{{AdminStoresGridSection.websiteFilterTextField}}" userInput="{{website.name}}" stepKey="fillSearchWebsiteField"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickApplyFilters" /> + <see selector="{{AdminStoresGridSection.websiteNameInFirstRow}}" userInput="{{website.name}}" stepKey="verifyThatCorrectWebsiteFound"/> + <click selector="{{AdminStoresGridSection.websiteNameInFirstRow}}" stepKey="clickEditExistingWebsite"/> + <grabFromCurrentUrl regex="~(\d+)/~" stepKey="grabFromCurrentUrl"/> + </actionGroup> + + <actionGroup name="AssertWebsiteInGrid"> + <arguments> + <argument name="websiteName" type="string"/> + </arguments> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <waitForPageLoad stepKey="waitForAdminSystemStorePageLoad"/> + <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="resetSearchFilter"/> + <fillField userInput="{{websiteName}}" selector="{{AdminStoresGridSection.websiteFilterTextField}}" stepKey="fillWebsiteField"/> + <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForStoreToLoad"/> + <seeElement selector="{{AdminStoresGridSection.websiteName(websiteName)}}" stepKey="seeAssertWebsiteInGrid"/> + </actionGroup> + + <actionGroup name="AssertWebsiteForm"> + <arguments> + <argument name="websiteName" type="string"/> + <argument name="websiteCode" type="string"/> + </arguments> + <click selector="{{AdminStoresGridSection.websiteName(websiteName)}}" stepKey="clickWebsiteFirstRowInGrid"/> + <waitForPageLoad stepKey="waitTillWebsiteFormPageIsOpened"/> + <grabFromCurrentUrl regex="~(\d+)/~" stepKey="grabWebsiteIdFromCurrentUrl"/> + <seeInCurrentUrl url="/system_store/editWebsite/website_id/{$grabWebsiteIdFromCurrentUrl}" stepKey="seeWebsiteId"/> + <seeInField selector="{{AdminNewWebsiteSection.name}}" userInput="{{websiteName}}" stepKey="seeAssertWebsiteName"/> + <seeInField selector="{{AdminNewWebsiteSection.code}}" userInput="{{websiteCode}}" stepKey="seeAssertWebsiteCode"/> + </actionGroup> </actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteStoreViewActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteStoreViewActionGroup.xml index 849dc91efedb..cf2cabdcc239 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteStoreViewActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteStoreViewActionGroup.xml @@ -23,7 +23,38 @@ <click selector="{{AdminNewStoreViewActionsSection.delete}}" stepKey="clickDeleteStoreViewAgain"/> <waitForElementVisible selector="{{AdminConfirmationModalSection.title}}" stepKey="waitingForWarningModal"/> <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmStoreDelete"/> - <wait time="10" stepKey="extraWait"/> + <waitForPageLoad stepKey="waitForSuccessMessage"/> <see userInput="You deleted the store view." stepKey="seeDeleteMessage"/> </actionGroup> -</actionGroups> + <actionGroup name="DeleteCustomStoreViewBackupEnabledYesActionGroup"> + <arguments> + <argument name="storeViewName" type="string"/> + </arguments> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="resetSearchFilter"/> + <fillField userInput="{{storeViewName}}" selector="{{AdminStoresGridSection.storeFilterTextField}}" stepKey="fillSearchStoreViewField"/> + <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearchButton"/> + <click selector="{{AdminStoresGridSection.storeNameInFirstRow}}" stepKey="clickEditExistingStoreViewRow"/> + <waitForPageLoad stepKey="waitForStoreToLoad"/> + <click selector="{{AdminNewStoreViewActionsSection.delete}}" stepKey="clickDeleteStoreViewButtonOnEditStorePage"/> + <selectOption userInput="Yes" selector="{{AdminStoreBackupOptionsSection.createBackupSelect}}" stepKey="setCreateDbBackupToYes"/> + <click selector="{{AdminNewStoreViewActionsSection.delete}}" stepKey="clickDeleteStoreViewButtonOnDeleteStorePage"/> + <waitForElementVisible selector="{{AdminConfirmationModalSection.title}}" stepKey="waitingForWarningModal"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmStoreViewDelete"/> + <waitForPageLoad stepKey="waitForSuccessMessage"/> + <see selector="{{AdminStoresGridSection.successMessage}}" userInput="The database was backed up." stepKey="seeAssertDatabaseBackedUpMessage"/> + <see selector="{{AdminStoresGridSection.successMessage}}" userInput="You deleted the store view." stepKey="seeAssertSuccessDeleteStoreViewMessage"/> + </actionGroup> + <actionGroup name="AssertStoreViewNotInGrid"> + <arguments> + <argument name="storeViewName" type="string"/> + </arguments> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <waitForPageLoad stepKey="waitForAdminSystemStorePageLoad"/> + <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="resetSearchFilter"/> + <fillField userInput="{{storeViewName}}" selector="{{AdminStoresGridSection.storeFilterTextField}}" stepKey="fillSearchStoreViewField"/> + <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForStoreToLoad"/> + <see selector="{{AdminStoresGridSection.emptyText}}" userInput="We couldn't find any records." stepKey="seeAssertStoreViewNotInGridMessage"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteWebsiteActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteWebsiteActionGroup.xml index 1400fbf12c16..1721e3185402 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteWebsiteActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteWebsiteActionGroup.xml @@ -19,9 +19,11 @@ <click selector="{{AdminStoresGridSection.websiteNameInFirstRow}}" stepKey="clickEditExistingStoreRow"/> <waitForPageLoad stepKey="waitForStoreToLoad"/> <click selector="{{AdminStoresMainActionsSection.deleteButton}}" stepKey="clickDeleteWebsiteButtonOnEditWebsitePage"/> + <waitForPageLoad stepKey="waitForDeleteStoreGroupSectionLoad" time="30"/> <selectOption userInput="No" selector="{{AdminStoresDeleteStoreGroupSection.createDbBackup}}" stepKey="setCreateDbBackupToNo"/> <click selector="{{AdminStoresDeleteStoreGroupSection.deleteStoreGroupButton}}" stepKey="clickDeleteWebsiteButton"/> <waitForElementVisible selector="{{AdminStoresGridSection.websiteFilterTextField}}" stepKey="waitForStoreGridToReload"/> <see userInput="You deleted the website." stepKey="seeSavedMessage"/> + <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="resetSearchFilter2"/> </actionGroup> -</actionGroups> \ No newline at end of file +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminFilterStoreViewActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminFilterStoreViewActionGroup.xml new file mode 100644 index 000000000000..e4cb26ea6ff7 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminFilterStoreViewActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminFilterStoreViewActionGroup"> + <arguments> + <argument name="StoreGroup" defaultValue="_defaultStoreGroup"/> + <argument name="customStore" defaultValue="customStore.name"/> + </arguments> + <click selector="{{AdminProductFiltersSection.filter}}" stepKey="ClickOnFilter"/> + <click selector="{{AdminProductFiltersSection.storeViewDropDown}}" stepKey="ClickOnStoreViewDropDown"/> + <click selector="{{AdminProductFiltersSection.storeViewOption(customStore)}}" stepKey="ClickOnStoreViewOption"/> + <click selector="{{AdminProductFiltersSection.applyFilters}}" stepKey="ClickOnApplyFilters"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminStoreGroupCreateActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminStoreGroupCreateActionGroup.xml new file mode 100644 index 000000000000..1a7f24ed2aaa --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminStoreGroupCreateActionGroup.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See 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="AdminStoreGroupCreateActionGroup"> + <arguments> + <argument name="Website" defaultValue="_defaultWebsite"/> + <argument name="storeGroup"/> + </arguments> + <amOnPage url="{{AdminSystemStoreGroupPage.url}}" stepKey="navigateToNewStoreGroup"/> + <waitForPageLoad stepKey="waitForStoreGroupPageLoad" /> + + <comment userInput="Creating Store Group" stepKey="storeGroupCreationComment" /> + <selectOption selector="{{AdminNewStoreGroupSection.storeGrpWebsiteDropdown}}" userInput="{{Website.name}}" stepKey="selectWebsite" /> + <fillField selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" userInput="{{storeGroup.name}}" stepKey="enterStoreGroupName" /> + <fillField selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" userInput="{{storeGroup.code}}" stepKey="enterStoreGroupCode" /> + <selectOption selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" userInput="Default Category" stepKey="setRootCategory" /> + <click selector="{{AdminNewStoreGroupActionsSection.saveButton}}" stepKey="clickSaveStoreGroup" /> + <waitForElementVisible selector="{{AdminStoresGridSection.storeFilterTextField}}" stepKey="waitForPageReload"/> + <see userInput="You saved the store." stepKey="seeSavedMessage" /> + </actionGroup> + + <actionGroup name="AdminAddCustomWebSiteToStoreGroup"> + <arguments> + <argument name="storeGroup" defaultValue="customStoreGroup"/> + <argument name="website" defaultValue="customWebsite"/> + </arguments> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="resetSearchFilter"/> + <fillField userInput="{{storeGroup.name}}" selector="{{AdminStoresGridSection.storeGrpFilterTextField}}" stepKey="fillSearchStoreGroupField"/> + <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearchButton"/> + <see userInput="{{storeGroup.name}}" selector="{{AdminStoresGridSection.storeGrpNameInFirstRow}}" stepKey="verifyThatCorrectStoreGroupFound"/> + <click selector="{{AdminStoresGridSection.storeGrpNameInFirstRow}}" stepKey="clickEditExistingStoreRow"/> + <waitForPageLoad stepKey="waitForStoreGroupPageLoad" /> + <selectOption selector="{{AdminNewStoreGroupSection.storeGrpWebsiteDropdown}}" userInput="{{website.name}}" stepKey="selectWebsite" /> + <selectOption selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" userInput="Default Category" stepKey="chooseRootCategory" /> + <click selector="{{AdminNewStoreGroupActionsSection.saveButton}}" stepKey="clickSaveStoreGroup" /> + <conditionalClick selector="{{AdminNewStoreGroupSection.acceptNewStoreGroupCreation}}" dependentSelector="{{AdminNewStoreGroupSection.acceptNewStoreGroupCreation}}" visible="true" stepKey="clickAcceptNewStoreGroupCreationButton"/> + <waitForElementVisible selector="{{AdminStoresGridSection.storeFilterTextField}}" stepKey="waitForPageReload"/> + <see userInput="You saved the store." stepKey="seeSavedMessage" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSwitchStoreViewActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSwitchStoreViewActionGroup.xml index 860f094a48ec..47236376f020 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSwitchStoreViewActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSwitchStoreViewActionGroup.xml @@ -18,6 +18,11 @@ <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitingForInformationModal"/> <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmStoreSwitch"/> <waitForPageLoad stepKey="waitForStoreViewSwitched"/> + <scrollToTopOfPage stepKey="scrollToStoreSwitcher"/> <see userInput="{{storeView}}" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewStoreViewName"/> </actionGroup> + <actionGroup name="AdminSwitchToAllStoreViewActionGroup" extends="AdminSwitchStoreViewActionGroup"> + <click selector="{{AdminMainActionsSection.allStoreViews}}" stepKey="clickStoreViewByName" after="waitForStoreViewsAreVisible"/> + <see selector="{{AdminMainActionsSection.storeSwitcher}}" userInput="All Store Views" stepKey="seeNewStoreViewName" after="waitForStoreViewSwitched"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSwitchWebsiteActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSwitchWebsiteActionGroup.xml new file mode 100644 index 000000000000..0960dfb47c36 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSwitchWebsiteActionGroup.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="AdminSwitchWebsiteActionGroup"> + <arguments> + <argument name="website"/> + </arguments> + <scrollToTopOfPage stepKey="scrollToTop"/> + <click selector="{{AdminMainActionsSection.storeViewDropdown}}" stepKey="clickWebsiteSwitchDropdown"/> + <waitForElementVisible selector="{{AdminMainActionsSection.websiteByName('Main Website')}}" stepKey="waitForWebsiteAreVisible"/> + <click selector="{{AdminMainActionsSection.websiteByName(website.name)}}" stepKey="clickWebsiteByName"/> + <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitingForInformationModal"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmStoreSwitch"/> + <see userInput="{{website.name}}" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewWebsiteName"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreConfigurationBackendActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreConfigurationBackendActionGroup.xml new file mode 100644 index 000000000000..38030f59a7a3 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreConfigurationBackendActionGroup.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStoreConfigurationBackendActionGroup"> + <arguments> + <argument name="website" type="string"/> + <argument name="customStore" type="string"/> + <argument name="storeView1" type="string"/> + <argument name="storeView2" type="string"/> + </arguments> + <amOnPage url="{{AdminConfigPage.url}}" stepKey="goToConfigStoreConfigurationPage"/> + <waitForPageLoad stepKey="waitForSystemStoreConfigurationPageLoad"/> + <click selector="{{AdminConfigSection.defaultConfigButton}}" stepKey="clickDefaultConfigButton"/> + <see selector="{{AdminConfigSection.defaultConfigDropdown}}" userInput="{{website}}" stepKey="seeAssertWebsiteInDefaultConfigDropdown"/> + <see selector="{{AdminConfigSection.defaultConfigDropdown}}" userInput="{{customStore}}" stepKey="seeAssertSecondStoreInDefaultConfigDropdown"/> + <see selector="{{AdminConfigSection.defaultConfigDropdown}}" userInput="{{storeView1}}" stepKey="seeAssertFirstStoreViewInDefaultConfigDropdown"/> + <see selector="{{AdminConfigSection.defaultConfigDropdown}}" userInput="{{storeView2}}" stepKey="seeAssertSecondStoreViewInDefaultConfigDropdown"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreFrontendActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreFrontendActionGroup.xml new file mode 100644 index 000000000000..afd64e33e8b3 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreFrontendActionGroup.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStoreFrontendActionGroup"> + <arguments> + <argument name="customStore" type="string"/> + <argument name="storeView1" type="string"/> + <argument name="storeView2" type="string"/> + </arguments> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefrontPage"/> + <waitForPageLoad stepKey="waitForStorefrontHomePageLoad"/> + <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="clickSwitchStoreButton"/> + <waitForElementVisible selector="{{StorefrontFooterSection.storeLink(customStore)}}" stepKey="waitForStoreLinkToVosible"/> + <click selector="{{StorefrontFooterSection.storeLink(customStore)}}" stepKey="clickStoreLinkCustomStore"/> + <waitForPageLoad stepKey="waitForCustomStoreToLoad"/> + <see selector="{{StorefrontHeaderSection.storeViewName}}" userInput="{{storeView1}}" stepKey="seeAssertFirstStoreViewOnStorefront"/> + <click selector="{{StorefrontHeaderSection.storeViewName}}" stepKey="clickStoreViewName"/> + <see selector="{{StorefrontHeaderSection.storeViewDropdown}}" userInput="{{storeView2}}" stepKey="seeAssertSecondStoreViewOnStorefront"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreGroupFormActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreGroupFormActionGroup.xml new file mode 100644 index 000000000000..a8b8b7bb6d07 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreGroupFormActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Admin creates new Store group --> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStoreGroupFormActionGroup"> + <arguments> + <argument name="website" type="string"/> + <argument name="storeGroupName" type="string"/> + <argument name="storeGroupCode" type="string"/> + <argument name="rootCategory" type="string"/> + </arguments> + <click selector="{{AdminStoresGridSection.storeGrpNameInFirstRow}}" stepKey="clickEditExistingStoreRow"/> + <waitForPageLoad stepKey="waitTillAdminSystemStoreGroupPage"/> + <seeInField selector="{{AdminNewStoreGroupSection.storeGrpWebsiteDropdown}}" userInput="{{website}}" stepKey="seeAssertWebsite"/> + <seeInField selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" userInput="{{storeGroupName}}" stepKey="seeAssertStoreGroupName"/> + <seeInField selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" userInput="{{storeGroupCode}}" stepKey="seeAssertStoreGroupCode"/> + <seeInField selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" userInput="{{rootCategory}}" stepKey="seeAssertRootCategory"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreGroupInGridActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreGroupInGridActionGroup.xml new file mode 100644 index 000000000000..fb36cbd0d181 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreGroupInGridActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Admin creates new Store group --> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStoreGroupInGridActionGroup"> + <arguments> + <argument name="storeGroupName" type="string"/> + </arguments> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <waitForPageLoad stepKey="waitForAdminSystemStorePageLoad"/> + <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="resetSearchFilter"/> + <fillField userInput="{{storeGroupName}}" selector="{{AdminStoresGridSection.storeGrpFilterTextField}}" stepKey="fillSearchStoreGroupField"/> + <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForStoreToLoad"/> + <see selector="{{AdminStoresGridSection.firstRow}}" userInput="{{storeGroupName}}" stepKey="seeAssertStoreGroupInGridMessage"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreViewFormActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreViewFormActionGroup.xml new file mode 100644 index 000000000000..92ebbc395013 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreViewFormActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStoreViewFormActionGroup"> + <arguments> + <argument name="storeDropdown" type="string"/> + <argument name="storeViewName" type="string"/> + <argument name="storeViewCode" type="string"/> + <argument name="status" type="string"/> + </arguments> + <click selector="{{AdminStoresGridSection.storeNameInFirstRow}}" stepKey="clickStoreViewFirstRowInGrid"/> + <waitForPageLoad stepKey="waitForAdminSystemStoreViewPageLoad"/> + <seeInField selector="{{AdminNewStoreSection.storeGrpDropdown}}" userInput="{{storeDropdown}}" stepKey="seeAssertStore"/> + <seeInField selector="{{AdminNewStoreSection.storeNameTextField}}" userInput="{{storeViewName}}" stepKey="seeAssertStoreViewName"/> + <seeInField selector="{{AdminNewStoreSection.storeCodeTextField}}" userInput="{{storeViewCode}}" stepKey="seeAssertStoreViewCode"/> + <seeInField selector="{{AdminNewStoreSection.statusDropdown}}" userInput="{{status}}" stepKey="seeAssertStatus"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreViewInGridActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreViewInGridActionGroup.xml new file mode 100644 index 000000000000..fad8eec9990c --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStoreViewInGridActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStoreViewInGridActionGroup"> + <arguments> + <argument name="storeViewName" type="string"/> + </arguments> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <waitForPageLoad stepKey="waitForAdminSystemStorePageLoad"/> + <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="resetSearchFilter"/> + <fillField userInput="{{storeViewName}}" selector="{{AdminStoresGridSection.storeFilterTextField}}" stepKey="fillSearchStoreViewField"/> + <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForStoreToLoad"/> + <see selector="{{AdminStoresGridSection.firstRow}}" userInput="{{storeViewName}}" stepKey="seeAssertStoreViewInGridMessage"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStorefrontStoreCodeInUrlActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStorefrontStoreCodeInUrlActionGroup.xml new file mode 100644 index 000000000000..3daa1c25a173 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AssertStorefrontStoreCodeInUrlActionGroup.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStorefrontStoreCodeInUrlActionGroup"> + <arguments> + <argument name="storeCode" type="string" defaultValue="{{_defaultStore.code}}" /> + </arguments> + <seeInCurrentUrl url="{{StorefrontHomePage.url}}{{storeCode}}" stepKey="seeStoreCodeInURL"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/ChangeStoreInStoreViewActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/ChangeStoreInStoreViewActionGroup.xml new file mode 100644 index 000000000000..15784235d591 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/ChangeStoreInStoreViewActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="ChangeStoreInStoreViewActionGroup"> + <arguments> + <argument name="storeDropdown" type="string"/> + </arguments> + <click selector="{{AdminStoresGridSection.storeNameInFirstRow}}" stepKey="clickStoreViewFirstRowInGrid"/> + <waitForPageLoad stepKey="waitForAdminSystemStoreViewPageLoad"/> + <selectOption selector="{{AdminNewStoreSection.storeGrpDropdown}}" userInput="{{storeDropdown}}" stepKey="selectStoreGrpDropdown"/> + <click selector="{{AdminNewStoreViewActionsSection.saveButton}}" stepKey="clickSaveButton"/> + <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForModal"/> + <see selector="{{AdminConfirmationModalSection.title}}" userInput="Warning message" stepKey="seeWarningAboutTakingALongTimeToComplete"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmModal"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/CreateCustomStoreViewActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/CreateCustomStoreViewActionGroup.xml index 31bbe7550e5a..86d3963bc42b 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/CreateCustomStoreViewActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/CreateCustomStoreViewActionGroup.xml @@ -5,6 +5,7 @@ * See COPYING.txt for license details. */ --> + <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="CreateCustomStoreViewActionGroup"> @@ -18,7 +19,24 @@ <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreSection.storeCodeTextField}}" stepKey="fillStoreViewCode"/> <selectOption userInput="{{customStore.is_active}}" selector="{{AdminNewStoreSection.statusDropdown}}" stepKey="selectStoreViewStatus"/> <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreViewButton"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <conditionalClick selector="{{AdminNewStoreSection.acceptNewStoreViewCreation}}" dependentSelector="{{AdminNewStoreSection.acceptNewStoreViewCreation}}" visible="true" stepKey="clickAcceptNewStoreViewCreationButton"/> + </actionGroup> + <actionGroup name="CreateStoreView"> + <arguments> + <argument name="storeView" defaultValue="customStore"/> + <argument name="storeGroupName" defaultValue="_defaultStoreGroup.name"/> + <argument name="storeViewStatus" defaultValue="_defaultStore.is_active"/> + </arguments> + <amOnPage url="{{AdminSystemStoreViewPage.url}}" stepKey="amOnAdminSystemStoreViewPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <selectOption userInput="{{storeGroupName}}" selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="selectStoreGroup"/> + <fillField userInput="{{storeView.name}}" selector="{{AdminNewStoreSection.storeNameTextField}}" stepKey="fillStoreViewName"/> + <fillField userInput="{{storeView.code}}" selector="{{AdminNewStoreSection.storeCodeTextField}}" stepKey="fillStoreViewCode"/> + <selectOption userInput="{{storeViewStatus}}" selector="{{AdminNewStoreSection.statusDropdown}}" stepKey="selectStoreViewStatus"/> + <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreViewButton"/> <waitForElementVisible selector="{{AdminNewStoreSection.acceptNewStoreViewCreation}}" stepKey="waitForAcceptNewStoreViewCreationButton" /> <conditionalClick selector="{{AdminNewStoreSection.acceptNewStoreViewCreation}}" dependentSelector="{{AdminNewStoreSection.acceptNewStoreViewCreation}}" visible="true" stepKey="clickAcceptNewStoreViewCreationButton"/> + <see userInput="You saved the store view." stepKey="seeSavedMessage"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/DeleteCustomStoreActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/DeleteCustomStoreActionGroup.xml index 8e32b819aa95..8a1d830661aa 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/DeleteCustomStoreActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/DeleteCustomStoreActionGroup.xml @@ -23,4 +23,33 @@ <selectOption userInput="No" selector="{{AdminStoresDeleteStoreGroupSection.createDbBackup}}" stepKey="setCreateDbBackupToNo"/> <click selector="{{AdminStoresDeleteStoreGroupSection.deleteStoreGroupButton}}" stepKey="clickDeleteStoreGroupButtonOnDeleteStorePage"/> </actionGroup> -</actionGroups> + <actionGroup name="DeleteCustomStoreBackupEnabledYesActionGroup"> + <arguments> + <argument name="storeGroupName" type="string"/> + </arguments> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="resetSearchFilter"/> + <fillField userInput="{{storeGroupName}}" selector="{{AdminStoresGridSection.storeGrpFilterTextField}}" stepKey="fillSearchStoreGroupField"/> + <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearchButton"/> + <see userInput="{{storeGroupName}}" selector="{{AdminStoresGridSection.storeGrpNameInFirstRow}}" stepKey="verifyThatCorrectStoreGroupFound"/> + <click selector="{{AdminStoresGridSection.storeGrpNameInFirstRow}}" stepKey="clickEditExistingStoreRow"/> + <waitForPageLoad stepKey="waitForStoreToLoad"/> + <click selector="{{AdminStoresMainActionsSection.deleteButton}}" stepKey="clickDeleteStoreGroupButtonOnEditStorePage"/> + <selectOption userInput="Yes" selector="{{AdminStoresDeleteStoreGroupSection.createDbBackup}}" stepKey="setCreateDbBackupToNo"/> + <click selector="{{AdminStoresDeleteStoreGroupSection.deleteStoreGroupButton}}" stepKey="clickDeleteStoreGroupButtonOnDeleteStorePage"/> + <see selector="{{AdminStoresGridSection.successMessage}}" userInput="The database was backed up." stepKey="seeAssertDatabaseBackedUpMessage"/> + <see selector="{{AdminStoresGridSection.successMessage}}" userInput="You deleted the store." stepKey="seeAssertSuccessDeleteStoreGroupMessage"/> + </actionGroup> + <actionGroup name="AssertStoreNotInGrid"> + <arguments> + <argument name="storeGroupName" type="string"/> + </arguments> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <waitForPageLoad stepKey="waitForAdminSystemStorePageLoad"/> + <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="resetSearchFilter"/> + <fillField userInput="{{storeGroupName}}" selector="{{AdminStoresGridSection.storeGrpFilterTextField}}" stepKey="fillSearchStoreGroupField"/> + <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForStoreToLoad"/> + <see selector="{{AdminStoresGridSection.emptyText}}" userInput="We couldn't find any records." stepKey="seeAssertStoreGroupNotInGridMessage"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/DeleteCustomWebsiteActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/DeleteCustomWebsiteActionGroup.xml index cc6a1fb62ea5..da3ce02a80f2 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/DeleteCustomWebsiteActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/DeleteCustomWebsiteActionGroup.xml @@ -13,15 +13,14 @@ </arguments> <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnTheStorePage"/> <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="clickOnResetButton"/> - <waitForPageLoad stepKey="waitForPageLoadAfterResetButtonClicked" time="10"/> <fillField userInput="{{websiteName}}" selector="{{AdminStoresGridSection.websiteFilterTextField}}" stepKey="fillSearchWebsiteField"/> <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearchButton" /> - <waitForPageLoad stepKey="waitForPageLoadAfterSearch" time="10"/> <see userInput="{{websiteName}}" selector="{{AdminStoresGridSection.websiteNameInFirstRow}}" stepKey="verifyThatCorrectWebsiteFound"/> <click selector="{{AdminStoresGridSection.websiteNameInFirstRow}}" stepKey="clickEditExistingWebsite"/> - + <waitForPageLoad stepKey="waitForPageLoadAfterWebsiteSelected" time="30"/> <click selector="{{AdminStoresMainActionsSection.deleteButton}}" stepKey="clickDeleteWebsiteButtonOnEditStorePage"/> <selectOption userInput="No" selector="{{AdminStoresDeleteWebsiteSection.createDbBackup}}" stepKey="setCreateDbBackupToNo"/> <click selector="{{AdminStoresDeleteWebsiteSection.deleteButton}}" stepKey="clickDeleteButtonOnDeleteWebsitePage"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="You deleted the website." stepKey="checkSuccessMessage"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/EditCustomStoreGroupAcceptWarningMessageActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/EditCustomStoreGroupAcceptWarningMessageActionGroup.xml new file mode 100644 index 000000000000..8889795c8acb --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/EditCustomStoreGroupAcceptWarningMessageActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Admin creates new Store group --> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="EditCustomStoreGroupAcceptWarningMessageActionGroup" extends="CreateCustomStore"> + <arguments> + <argument name="website" type="string"/> + <argument name="store" type="string"/> + <argument name="rootCategory" type="string"/> + </arguments> + <remove keyForRemoval="selectCreateStore"/> + <click selector="{{AdminStoresGridSection.storeGrpNameInFirstRow}}" stepKey="clickFirstRow" before="selectMainWebsite"/> + <waitForPageLoad stepKey="waitForWarningMessageToAppear" before="seeAssertAlertWarningMessage" /> + <click selector="{{AdminStoreGroupActionsSection.okButton}}" stepKey="seeAssertAlertWarningMessage" before="waitForStoreGridReload"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/StoreFrontProductValidationActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/StoreFrontProductValidationActionGroup.xml new file mode 100644 index 000000000000..f11394c643ad --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/StoreFrontProductValidationActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StoreFrontProductValidationActionGroup"> + <arguments> + <argument name="product" type="entity"/> + </arguments> + <amOnPage url="{{StorefrontProductPage.url(product.urlKey)}}" stepKey="seeProductPage"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageToLoad"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{product.name}}" stepKey="seeProductInStoreFrontPage"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{product.sku}}" stepKey="seeCorrectSku"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{product.price}}" stepKey="seeCorrectPrice"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreViewActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreViewActionGroup.xml index cfcd25086e06..d30fc1e5a2a3 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreViewActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreViewActionGroup.xml @@ -14,7 +14,12 @@ </arguments> <click selector="{{StorefrontHeaderSection.storeViewSwitcher}}" stepKey="clickStoreViewSwitcher"/> <waitForElementVisible selector="{{StorefrontHeaderSection.storeViewDropdown}}" stepKey="waitForStoreViewDropdown"/> - <click selector="{{StorefrontHeaderSection.storeViewOption(storeView.name)}}" stepKey="clickSelectStoreView"/> + <click selector="{{StorefrontHeaderSection.storeViewOption(storeView.code)}}" stepKey="clickSelectStoreView"/> <waitForPageLoad stepKey="waitForPageLoad"/> </actionGroup> + + <actionGroup name="StorefrontSwitchDefaultStoreViewActionGroup" extends="StorefrontSwitchStoreViewActionGroup"> + <remove keyForRemoval="clickSelectStoreView"/> + <click selector="{{StorefrontHeaderSection.storeViewOption('default')}}" stepKey="clickSelectDefaultStoreView" after="waitForStoreViewDropdown"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/Data/ProductWebsiteLinkData.xml b/app/code/Magento/Store/Test/Mftf/Data/ProductWebsiteLinkData.xml new file mode 100644 index 000000000000..8e84b84c8aa4 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Data/ProductWebsiteLinkData.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="ProductAssignToWebsite" type="product_website_link"> + <var key="sku" entityKey="sku" entityType="product"/> + </entity> +</entities> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/Data/StoreConfigData.xml b/app/code/Magento/Store/Test/Mftf/Data/StoreConfigData.xml new file mode 100644 index 000000000000..433344692547 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Data/StoreConfigData.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="StorefrontDisableAddStoreCodeToUrls"> + <!-- Magento default value --> + <data key="path">web/url/use_store</data> + <data key="scope_id">0</data> + <data key="label">No</data> + <data key="value">0</data> + </entity> + <entity name="StorefrontEnableAddStoreCodeToUrls"> + <data key="path">web/url/use_store</data> + <data key="scope_id">0</data> + <data key="label">Yes</data> + <data key="value">1</data> + </entity> +</entities> diff --git a/app/code/Magento/Store/Test/Mftf/Data/StoreData.xml b/app/code/Magento/Store/Test/Mftf/Data/StoreData.xml index 2bff20e05435..4e043f9ff27d 100644 --- a/app/code/Magento/Store/Test/Mftf/Data/StoreData.xml +++ b/app/code/Magento/Store/Test/Mftf/Data/StoreData.xml @@ -22,8 +22,8 @@ <requiredEntity type="storeGroup">customStoreGroup</requiredEntity> </entity> <entity name="customStoreEN" type="store"> - <data key="name">EN</data> - <data key="code">en</data> + <data key="name" unique="suffix">EN</data> + <data key="code" unique="suffix">en</data> <data key="is_active">1</data> <data key="store_id">null</data> <data key="store_action">add</data> @@ -31,22 +31,139 @@ <requiredEntity type="storeGroup">customStoreGroup</requiredEntity> </entity> <entity name="customStoreFR" type="store"> - <data key="name">FR</data> - <data key="code">fr</data> + <data key="name" unique="suffix">FR</data> + <data key="code" unique="suffix">fr</data> <data key="is_active">1</data> <data key="store_id">null</data> <data key="store_action">add</data> <data key="store_type">group</data> <requiredEntity type="storeGroup">customStoreGroup</requiredEntity> </entity> + <entity name="customStoreENNotUnique" type="store"> + <data key="name">EN</data> + <data key="code">en</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_action">add</data> + <data key="store_type">store</data> + <requiredEntity type="storeGroup">customStoreGroup</requiredEntity> + </entity> + <entity name="customStoreNLNotUnique" type="store"> + <data key="name">NL</data> + <data key="code">nl</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_action">add</data> + <data key="store_type">store</data> + <requiredEntity type="storeGroup">customStoreGroup</requiredEntity> + </entity> <entity name="staticStore" type="store"> <!--data key="group_id">customStoreGroup.id</data--> - <data key="name" >Second Store View</data> - <data key="code" >store123</data> + <data key="name">Second Store View</data> + <data key="code">store123</data> <data key="is_active">1</data> <data key="store_id">null</data> <data key="store_action">add</data> <data key="store_type">store</data> <requiredEntity type="storeGroup">customStoreGroup</requiredEntity> </entity> + <entity name="staticSecondStore" extends="staticStore"> + <data key="name">Store View</data> + <data key="code">store2</data> + </entity> + <entity name="NewStoreData" type="store"> + <data key="name" unique="suffix">Store</data> + <data key="code" unique="suffix">StoreCode</data> + </entity> + <entity name="NewWebSiteData" type="webSite"> + <data key="name" unique="suffix">WebSite</data> + <data key="code" unique="suffix">WebSiteCode</data> + </entity> + <entity name="NewStoreViewData"> + <data key="name" unique="suffix">StoreView</data> + <data key="code" unique="suffix">StoreViewCode</data> + </entity> + + <!-- For creation 10 Store Views--> + <entity name="storeViewData" type="store"> + <data key="group_id">1</data> + <data key="name" unique="suffix">storeView</data> + <data key="code" unique="suffix">storeView</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_type">store</data> + <data key="store_action">add</data> + </entity> + <entity name="storeViewData1" type="store"> + <data key="group_id">1</data> + <data key="name" unique="suffix">storeView</data> + <data key="code" unique="suffix">storeView</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_type">store</data> + <data key="store_action">add</data> + </entity> + <entity name="storeViewData2" type="store"> + <data key="group_id">1</data> + <data key="name" unique="suffix">storeView</data> + <data key="code" unique="suffix">storeView</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_type">store</data> + <data key="store_action">add</data> + </entity> + <entity name="storeViewData3" type="store"> + <data key="group_id">1</data> + <data key="name" unique="suffix">storeView</data> + <data key="code" unique="suffix">storeView</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_type">store</data> + <data key="store_action">add</data> + </entity> + <entity name="storeViewData4" type="store"> + <data key="group_id">1</data> + <data key="name" unique="suffix">storeView</data> + <data key="code" unique="suffix">storeView</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_type">store</data> + <data key="store_action">add</data> + </entity> + <entity name="storeViewData5" type="store"> + <data key="group_id">1</data> + <data key="name" unique="suffix">storeView</data> + <data key="code" unique="suffix">storeView</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_type">store</data> + <data key="store_action">add</data> + </entity> + <entity name="storeViewData6" type="store"> + <data key="group_id">1</data> + <data key="name" unique="suffix">storeView</data> + <data key="code" unique="suffix">storeView</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_type">store</data> + <data key="store_action">add</data> + </entity> + <entity name="storeViewData7" type="store"> + <data key="group_id">1</data> + <data key="name" unique="suffix">storeView</data> + <data key="code" unique="suffix">storeView</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_type">store</data> + <data key="store_action">add</data> + </entity> + <entity name="SecondStoreUnique" type="store"> + <data key="name" unique="suffix">Second Store View </data> + <data key="code" unique="suffix">second_store_view_</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_action">add</data> + <data key="store_type">store</data> + <requiredEntity type="storeGroup">SecondStoreGroup</requiredEntity> + </entity> </entities> diff --git a/app/code/Magento/Store/Test/Mftf/Data/StoreGroupData.xml b/app/code/Magento/Store/Test/Mftf/Data/StoreGroupData.xml index 83ca12875d09..7cbd686dea09 100644 --- a/app/code/Magento/Store/Test/Mftf/Data/StoreGroupData.xml +++ b/app/code/Magento/Store/Test/Mftf/Data/StoreGroupData.xml @@ -21,10 +21,43 @@ <data key="store_action">add</data> <data key="store_type">group</data> </entity> + <entity name="SecondStoreGroup" type="group"> + <data key="group_id">null</data> + <data key="name">Second Store</data> + <data key="code">second_store</data> + <var key="root_category_id" entityKey="id" entityType="category"/> + <data key="store_action">add</data> + <data key="store_type">group</data> + </entity> + <entity name="SecondStoreGroupUnique" type="group"> + <data key="group_id">null</data> + <data key="name" unique="suffix">Second Store </data> + <data key="code" unique="suffix">second_store_</data> + <var key="root_category_id" entityKey="id" entityType="category"/> + <data key="store_action">add</data> + <data key="store_type">group</data> + </entity> <entity name="staticStoreGroup" type="group"> <data key="name">NewStore</data> - <data key="code">Base12</data> + <data key="code" unique="suffix">Base12</data> <data key="root_category_id">2</data> <data key="website_id">1</data> </entity> + <entity name="finnishStoreGroup" type="group"> + <data key="name">Finnish</data> + <data key="code">fin</data> + <data key="root_category_id">2</data> + <data key="website_id">1</data> + </entity> + <entity name="swedishStoreGroup" type="group"> + <data key="name">Swedish</data> + <data key="code">swd</data> + <data key="root_category_id">2</data> + <data key="website_id">1</data> + </entity> + + <entity name="staticFirstStoreGroup" extends="staticStoreGroup"> + <data key="name">NewStore</data> + <data key="code">Base1</data> + </entity> </entities> diff --git a/app/code/Magento/Store/Test/Mftf/Data/StorePaymentMethodsData.xml b/app/code/Magento/Store/Test/Mftf/Data/StorePaymentMethodsData.xml new file mode 100644 index 000000000000..912399142fa6 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Data/StorePaymentMethodsData.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="PaymentMethodsSettingConfig" type="zero_subtotal_checkout_config_state"> + <requiredEntity type="active">active</requiredEntity> + <requiredEntity type="order_status">orderStatus</requiredEntity> + </entity> + <entity name="active" type="active"> + <data key="value">1</data> + </entity> + <entity name="orderStatus" type="order_status"> + <data key="value">processing</data> + </entity> + + <entity name="DisablePaymentMethodsSettingConfig" type="use_system_value_config_state"> + <requiredEntity type="zeroSubEnable">zeroSubEnable</requiredEntity> + <requiredEntity type="zeroSubOrderStatus">zeroSubOrderStatus</requiredEntity> + </entity> + <entity name="zeroSubEnable" type="zeroSubEnable"> + <data key="value">1</data> + </entity> + <entity name="zeroSubOrderStatus" type="zeroSubOrderStatus"> + <data key="value">1</data> + </entity> +</entities> diff --git a/app/code/Magento/Store/Test/Mftf/Data/StoreShippingMethodsData.xml b/app/code/Magento/Store/Test/Mftf/Data/StoreShippingMethodsData.xml new file mode 100644 index 000000000000..bc9746c132d4 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Data/StoreShippingMethodsData.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="FreeShippingMethodsSettingConfig" type="free_shipping_config_state"> + <requiredEntity type="active">active</requiredEntity> + </entity> + <entity name="active" type="active"> + <data key="value">1</data> + </entity> + + <!-- default configuration used to restore Magento config --> + <entity name="DefaultShippingMethodsConfig" type="free_shipping_config_state"> + <requiredEntity type="active">DefaultFreeShipping</requiredEntity> + </entity> + <entity name="DefaultFreeShipping" type="active"> + <data key="value">0</data> + </entity> + + <entity name="DisableFreeShippingConfig" type="disable_free_shipping_config_state"> + <requiredEntity type="disableFreeShipping">disableFreeShipping</requiredEntity> + </entity> + <entity name="disableFreeShipping" type="disableFreeShipping"> + <data key="value">1</data> + </entity> + + <entity name="MinimumOrderAmount90" type="minimum_order_amount"> + <requiredEntity type="free_shipping_subtotal">Price</requiredEntity> + </entity> + <entity name="Price" type="free_shipping_subtotal"> + <data key="value">90</data> + </entity> + + <entity name="DefaultMinimumOrderAmount" type="minimum_order_amount"> + <requiredEntity type="free_shipping_subtotal">DefaultPrice</requiredEntity> + </entity> + <entity name="DefaultPrice" type="free_shipping_subtotal"> + <data key="value">0</data> + </entity> +</entities> diff --git a/app/code/Magento/Store/Test/Mftf/Data/WebsiteData.xml b/app/code/Magento/Store/Test/Mftf/Data/WebsiteData.xml index ee137d78d7fd..ae605256a281 100644 --- a/app/code/Magento/Store/Test/Mftf/Data/WebsiteData.xml +++ b/app/code/Magento/Store/Test/Mftf/Data/WebsiteData.xml @@ -19,4 +19,12 @@ <data key="store_type">website</data> <data key="website_id">null</data> </entity> -</entities> + <entity name="secondCustomWebsite" extends="customWebsite"> + <data key="name" unique="suffix">Custom Website</data> + <data key="code" unique="suffix">custom_website</data> + </entity> + <entity name="updateCustomWebsite" extends="customWebsite"> + <data key="name" unique="suffix">website_upd</data> + <data key="code" unique="suffix">code_upd</data> + </entity> +</entities> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/Metadata/product_website_link-meta.xml b/app/code/Magento/Store/Test/Mftf/Metadata/product_website_link-meta.xml new file mode 100644 index 000000000000..ca0725f86c28 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Metadata/product_website_link-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="ProductWebsiteLink" dataType="product_website_link" type="create" auth="adminOauth" url="/V1/products/{sku}/websites" method="POST"> + <contentType>application/json</contentType> + <object dataType="product_website_link" key="productWebsiteLink"> + <field key="sku">string</field> + <field key="websiteId">integer</field> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Store/Test/Mftf/Metadata/store_payment_methods-meta.xml b/app/code/Magento/Store/Test/Mftf/Metadata/store_payment_methods-meta.xml new file mode 100644 index 000000000000..cbad7265cbbd --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Metadata/store_payment_methods-meta.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="EnableZeroSubtotalCheckoutConfigState" dataType="zero_subtotal_checkout_config_state" type="create" auth="adminFormKey" url="/admin/system_config/save/section/payment/" method="POST"> + <object key="groups" dataType="zero_subtotal_checkout_config_state"> + <object key="free" dataType="zero_subtotal_checkout_config_state"> + <object key="fields" dataType="zero_subtotal_checkout_config_state"> + <object key="active" dataType="active"> + <field key="value">string</field> + </object> + <object key="order_status" dataType="order_status"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> + + <operation name="DisablePaymentMethodsSettingConfig" dataType="use_system_value_config_state" type="create" auth="adminFormKey" url="/admin/system_config/save/section/payment/" method="POST"> + <object key="groups" dataType="use_system_value_config_state"> + <object key="free" dataType="use_system_value_config_state"> + <object key="fields" dataType="use_system_value_config_state"> + <object key="active" dataType="use_system_value_config_state"> + <object key="inherit" dataType="zeroSubEnable"> + <field key="value">integer</field> + </object> + </object> + <object key="order_status" dataType="use_system_value_config_state"> + <object key="inherit" dataType="zeroSubOrderStatus"> + <field key="value">integer</field> + </object> + </object> + </object> + </object> + </object> + </operation> + +</operations> + diff --git a/app/code/Magento/Store/Test/Mftf/Metadata/store_shipping_methods-meta.xml b/app/code/Magento/Store/Test/Mftf/Metadata/store_shipping_methods-meta.xml new file mode 100644 index 000000000000..6f88bca76020 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Metadata/store_shipping_methods-meta.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="EnableFreeShippingConfigState" dataType="free_shipping_config_state" type="create" auth="adminFormKey" url="/admin/system_config/save/section/carriers/" method="POST"> + <object key="groups" dataType="free_shipping_config_state"> + <object key="freeshipping" dataType="free_shipping_config_state"> + <object key="fields" dataType="free_shipping_config_state"> + <object key="active" dataType="active"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> + + <operation name="DisableFreeShippingConfigState" dataType="disable_free_shipping_config_state" type="create" auth="adminFormKey" url="/admin/system_config/save/section/carriers/" method="POST"> + <object key="groups" dataType="disable_free_shipping_config_state"> + <object key="freeshipping" dataType="disable_free_shipping_config_state"> + <object key="fields" dataType="disable_free_shipping_config_state"> + <object key="active" dataType="disable_free_shipping_config_state"> + <object key="inherit" dataType="disableFreeShipping"> + <field key="value">integer</field> + </object> + </object> + </object> + </object> + </object> + </operation> + + <operation name="MinimumOrderAmount" dataType="minimum_order_amount" type="create" auth="adminFormKey" url="/admin/system_config/save/section/carriers/" method="POST"> + <object key="groups" dataType="minimum_order_amount"> + <object key="freeshipping" dataType="minimum_order_amount"> + <object key="fields" dataType="minimum_order_amount"> + <object key="free_shipping_subtotal" dataType="free_shipping_subtotal"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> +</operations> + diff --git a/app/code/Magento/Store/Test/Mftf/Page/StorefrontStoreHomePage.xml b/app/code/Magento/Store/Test/Mftf/Page/StorefrontStoreHomePage.xml new file mode 100644 index 000000000000..0e6b578882f7 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Page/StorefrontStoreHomePage.xml @@ -0,0 +1,12 @@ +<?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="StorefrontStoreHomePage" url="/{{store_view}}/" area="storefront" module="Magento_Store" parameterized="true"> + <section name="StorefrontHeaderSection"/> + </page> +</pages> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminMainActionsSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminMainActionsSection.xml index fda182246db4..14160835af3e 100644 --- a/app/code/Magento/Store/Test/Mftf/Section/AdminMainActionsSection.xml +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminMainActionsSection.xml @@ -11,6 +11,8 @@ <section name="AdminMainActionsSection"> <element name="storeSwitcher" type="text" selector=".store-switcher"/> <element name="storeViewDropdown" type="button" selector="#store-change-button"/> - <element name="storeViewByName" type="button" selector="//*[@class='store-switcher-store-view ']/a[contains(text(), '{{storeViewName}}')]" timeout="30" parameterized="true"/> + <element name="storeViewByName" type="button" selector="//*[contains(@class,'store-switcher-store-view')]/*[contains(text(), '{{storeViewName}}')]" timeout="30" parameterized="true"/> + <element name="websiteByName" type="button" selector="//*[@class='store-switcher-website ']/a[contains(text(), '{{websiteName}}')]" timeout="30" parameterized="true"/> + <element name="allStoreViews" type="button" selector=".store-switcher .store-switcher-all" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminNewStoreGroupSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminNewStoreGroupSection.xml index ea5d9aab8b26..fb98c6698377 100644 --- a/app/code/Magento/Store/Test/Mftf/Section/AdminNewStoreGroupSection.xml +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminNewStoreGroupSection.xml @@ -11,5 +11,6 @@ <element name="storeGrpNameTextField" type="input" selector="#group_name"/> <element name="storeGrpCodeTextField" type="input" selector="#group_code"/> <element name="storeRootCategoryDropdown" type="select" selector="#group_root_category_id"/> + <element name="acceptNewStoreGroupCreation" type="button" selector=".action-primary.action-accept" /> </section> </sections> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminNewStoreViewActionsSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminNewStoreViewActionsSection.xml index faffc69dc697..0e3a74fccf9f 100644 --- a/app/code/Magento/Store/Test/Mftf/Section/AdminNewStoreViewActionsSection.xml +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminNewStoreViewActionsSection.xml @@ -11,5 +11,6 @@ <element name="delete" type="button" selector="#delete" timeout="30"/> <element name="resetButton" type="button" selector="#reset" timeout="30"/> <element name="saveButton" type="button" selector="#save" timeout="60"/> + <element name="loadingMask" type="text" selector=".loading-mask"/> </section> </sections> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminStoreGroupActionsSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminStoreGroupActionsSection.xml index f79ea080ed1c..9ad0f40cc4d0 100644 --- a/app/code/Magento/Store/Test/Mftf/Section/AdminStoreGroupActionsSection.xml +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminStoreGroupActionsSection.xml @@ -8,5 +8,6 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminStoreGroupActionsSection"> <element name="saveButton" type="button" selector="#save" timeout="60" /> + <element name="okButton" type="button" selector="//footer[@class='modal-footer']//button[@class='action-primary action-accept']/span" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminStoresDeleteWebsiteSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresDeleteWebsiteSection.xml index fea7dc07c828..1bdf7f0c22c4 100644 --- a/app/code/Magento/Store/Test/Mftf/Section/AdminStoresDeleteWebsiteSection.xml +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresDeleteWebsiteSection.xml @@ -8,6 +8,6 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminStoresDeleteWebsiteSection"> <element name="createDbBackup" type="select" selector="#store_create_backup"/> - <element name="deleteButton" type="button" selector="#delete" timeout="30"/> + <element name="deleteButton" type="button" selector="#delete" timeout="120"/> </section> </sections> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection.xml index 04cbeb5bc596..e8333bd30753 100644 --- a/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection.xml +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection.xml @@ -14,12 +14,17 @@ <section name="AdminStoresGridSection"> <element name="storeGrpFilterTextField" type="input" selector="#storeGrid_filter_group_title"/> <element name="websiteFilterTextField" type="input" selector="#storeGrid_filter_website_title"/> - <element name="storeFilterTextField" type="input" selector="#storeGrid_filter_store_title"/> + <element name="storeFilterTextField" type="input" selector="#storeGrid_filter_store_title" timeout="90"/> <element name="searchButton" type="button" selector=".admin__data-grid-header button[title=Search]" timeout="30"/> - <element name="resetButton" type="button" selector="button[title='Reset Filter']"/> + <element name="resetButton" type="button" selector="button[title='Reset Filter']" timeout="30"/> <element name="websiteNameInFirstRow" type="text" selector=".col-website_title>a"/> <element name="storeGrpNameInFirstRow" type="text" selector=".col-group_title>a"/> <element name="storeNameInFirstRow" type="text" selector=".col-store_title>a"/> <element name="firstRow" type="textarea" selector="(//*[@id='storeGrid_table']/tbody/tr)[1]"/> + <element name="nthRow" type="textarea" selector="(//*[@id='storeGrid_table']/tbody/tr)[{{rownum}}]" parameterized="true"/> + <element name="successMessage" type="text" selector="//div[@class='message message-success success']/div"/> + <element name="emptyText" type="text" selector="//tr[@class='data-grid-tr-no-data even']/td[@class='empty-text']"/> + <element name="websiteName" type="text" selector="//td[@class='a-left col-website_title ']/a[contains(.,'{{websiteName}}')]" parameterized="true"/> + <element name="gridCell" type="text" selector="//table[@class='data-grid']//tr[{{row}}]//td[count(//table[@class='data-grid']//tr//th[contains(., '{{column}}')]/preceding-sibling::th) +1 ]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminStoresMainActionsSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresMainActionsSection.xml index 98ad1db46732..e40aa76967be 100644 --- a/app/code/Magento/Store/Test/Mftf/Section/AdminStoresMainActionsSection.xml +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresMainActionsSection.xml @@ -10,7 +10,7 @@ <element name="createStoreViewButton" type="button" selector="#add_store" timeout="30"/> <element name="createStoreButton" type="button" selector="#add_group" timeout="30"/> <element name="createWebsiteButton" type="button" selector="#add" timeout="30"/> - <element name="saveButton" type="button" selector="#save" timeout="30"/> + <element name="saveButton" type="button" selector="#save" timeout="90"/> <element name="backButton" type="button" selector="#back" timeout="30"/> <element name="deleteButton" type="button" selector="#delete" timeout="30"/> </section> diff --git a/app/code/Magento/Store/Test/Mftf/Section/StorefrontHeaderSection.xml b/app/code/Magento/Store/Test/Mftf/Section/StorefrontHeaderSection.xml index af18e858e105..416808e58e6b 100644 --- a/app/code/Magento/Store/Test/Mftf/Section/StorefrontHeaderSection.xml +++ b/app/code/Magento/Store/Test/Mftf/Section/StorefrontHeaderSection.xml @@ -9,7 +9,10 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontHeaderSection"> <element name="storeViewSwitcher" type="button" selector="#switcher-language-trigger"/> - <element name="storeViewDropdown" type="button" selector="ul.switcher-dropdown"/> + <element name="storeViewDropdown" type="button" selector=".active ul.switcher-dropdown"/> <element name="storeViewOption" type="button" selector="li.view-{{var1}}>a" parameterized="true"/> + <element name="storeView" type="button" selector="//div[@class='actions dropdown options switcher-options active']//ul//li//a[contains(text(),'{{var}}')]" parameterized="true"/> + <element name="storeViewList" type="button" selector="//li[contains(.,'{{storeViewName}}')]//a" parameterized="true"/> + <element name="storeViewName" type="text" selector="//*[@id='switcher-language-trigger']//span"/> </section> </sections> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithCustomWebsiteAndDefaultCategoryTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithCustomWebsiteAndDefaultCategoryTest.xml new file mode 100644 index 000000000000..8e8f31eaca86 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithCustomWebsiteAndDefaultCategoryTest.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateStoreGroupWithCustomWebsiteAndDefaultCategoryTest"> + <annotations> + <stories value="Create Store Group"/> + <title value="Create Store Group with Custom Website and Default Category"/> + <description value="Test log in to Stores and Create Store Group with Custom Website and Default Category Test"/> + <testCaseId value="MC-14300"/> + <severity value="CRITICAL"/> + <group value="store"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create website--> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> + </actionGroup> + </before> + <after> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="{{customWebsite.name}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create custom store group with custom website and default category and verify AssertStoreGroupSuccessSaveMessage--> + <actionGroup ref="CreateCustomStore" stepKey="createCustomStoreGroup"> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="store" value="{{customStoreGroup.name}}"/> + <argument name="rootCategory" value="Default Category"/> + </actionGroup> + + <!--Search created store group(from above step) in grid and verify AssertStoreGroupInGrid message--> + <actionGroup ref="AssertStoreGroupInGrid" stepKey="seeCreatedStoreGroupInGrid"> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + </actionGroup> + + <!--Go to store group form page and verify AssertStoreGroupForm--> + <actionGroup ref="AssertStoreGroupForm" stepKey="seeCreatedStoreGroupForm"> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> + <argument name="rootCategory" value="Default Category"/> + </actionGroup> + <!--Also verify absence of delete button on store group form page(AssertStoreGroupNoDeleteButton)--> + <dontSee selector="{{AdminStoresMainActionsSection.deleteButton}}" stepKey="AssertStoreGroupNoDeleteButton"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithCustomWebsiteAndRootCategoryTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithCustomWebsiteAndRootCategoryTest.xml new file mode 100644 index 000000000000..18f9822145de --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithCustomWebsiteAndRootCategoryTest.xml @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateStoreGroupWithCustomWebsiteAndRootCategoryTest"> + <annotations> + <stories value="Create Store Group"/> + <title value="Create Store Group with Custom Website and Root Category"/> + <description value="Test log in to Stores and Create Store Group with Custom Website and Root Category Test"/> + <testCaseId value="MC-14299"/> + <severity value="CRITICAL"/> + <group value="store"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create root category--> + <createData entity="NewRootCategory" stepKey="rootCategory"/> + <createData entity="SimpleRootSubCategory" stepKey="category"> + <requiredEntity createDataKey="rootCategory"/> + </createData> + <!--Create website--> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> + </actionGroup> + </before> + <after> + <!--Delete website--> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="{{customWebsite.name}}"/> + </actionGroup> + <!--Delete root category--> + <deleteData stepKey="deleteRootCategory" createDataKey="rootCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create custom store group with custom website and root category and verify AssertStoreGroupSuccessSaveMessage--> + <actionGroup ref="CreateCustomStore" stepKey="createCustomStoreGroup"> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="store" value="{{customStoreGroup.name}}"/> + <argument name="rootCategory" value="$$rootCategory.name$$"/> + </actionGroup> + + <!--Search created store group(from above step) in grid and verify AssertStoreGroupInGrid--> + <actionGroup ref="AssertStoreGroupInGrid" stepKey="seeCreatedStoreGroupInGrid"> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + </actionGroup> + + <!--Go to store group form page and verify AssertStoreGroupForm and AssertStoreGroupOnStoreViewForm--> + <actionGroup ref="AssertStoreGroupForm" stepKey="seeCreatedStoreGroupInForm"> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> + <argument name="rootCategory" value="$$rootCategory.name$$"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithDefaultWebsiteAndDefaultCategoryTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithDefaultWebsiteAndDefaultCategoryTest.xml new file mode 100644 index 000000000000..ddc5d061c1db --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithDefaultWebsiteAndDefaultCategoryTest.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateStoreGroupWithDefaultWebsiteAndDefaultCategoryTest"> + <annotations> + <stories value="Create Store Group"/> + <title value="Create Store Group with Default Website and Default Category"/> + <description value="Test log in to Stores and Create Store Group with Default Website and Default Category Test"/> + <testCaseId value="MC-14298"/> + <severity value="CRITICAL"/> + <group value="store"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteStoreGroup"> + <argument name="storeGroupName" value="SecondStoreGroupUnique.name"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create custom store group with default website and default category and verify AssertStoreGroupSuccessSaveMessage--> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewCustomStoreGroup"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="storeGroupName" value="{{SecondStoreGroupUnique.name}}"/> + <argument name="storeGroupCode" value="{{SecondStoreGroupUnique.code}}"/> + </actionGroup> + + <!--Search created store group(from above step) in grid and verify AssertStoreGroupInGrid--> + <actionGroup ref="AssertStoreGroupInGrid" stepKey="seeCreatedStoreGroupInGrid"> + <argument name="storeGroupName" value="{{SecondStoreGroupUnique.name}}"/> + </actionGroup> + + <!--Go to store group form page and verify AssertStoreGroupForm and AssertStoreGroupOnStoreViewForm--> + <actionGroup ref="AssertStoreGroupForm" stepKey="seeCreatedStoreGroupForm"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="storeGroupName" value="{{SecondStoreGroupUnique.name}}"/> + <argument name="storeGroupCode" value="{{SecondStoreGroupUnique.code}}"/> + <argument name="rootCategory" value="Default Category"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreViewTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreViewTest.xml index 54d392d0c06f..e20eb70ae6f4 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreViewTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreViewTest.xml @@ -11,19 +11,36 @@ <annotations> <features value="Store"/> <stories value="Create a store view in admin"/> - <title value="Admin should be able to create a store view"/> - <description value="Admin should be able to create a store view"/> + <title value="Admin shouldn't be able to create a Store View with the same code"/> + <description value="Admin shouldn't be able to create a Store View with the same code"/> <group value="storeView"/> <severity value="AVERAGE"/> + <useCaseId value="MAGETWO-95111"/> + <testCaseId value="MC-15422"/> </annotations> + <before> - <actionGroup ref="LoginActionGroup" stepKey="login"/> + <actionGroup ref="LoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView" /> </before> - <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView" /> - <!--Confirm new store view on Store Grid--> - <fillField selector="{{AdminStoresGridSection.storeFilterTextField}}" userInput="{{customStore.name}}" stepKey="fillStoreViewFilter"/> - <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearch" /> - <waitForPageLoad stepKey="waitForPageLoad"/> - <see selector="{{AdminStoresGridSection.storeNameInFirstRow}}" userInput="{{customStore.name}}" stepKey="seeNewStoreView" /> + + <after> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"> + <argument name="customStore" value="customStore"/> + </actionGroup> + <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="resetSearchFilter"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Filter grid and see created store view--> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="navigateToStoresIndex"/> + <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="resetSearchFilter"/> + <fillField selector="{{AdminStoresGridSection.storeFilterTextField}}" userInput="{{customStore.name}}" stepKey="fillStoreViewFilterField"/> + <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearch"/> + <see selector="{{AdminStoresGridSection.gridCell('1', 'Store View')}}" userInput="{{customStore.name}}" stepKey="seeNewStoreView"/> + <!--Try to create store view with the same code--> + <actionGroup ref="AdminCreateStoreViewWithoutCheckActionGroup" stepKey="createSameStoreView"/> + <dontSeeElement selector="{{AdminMessagesSection.success}}" stepKey="dontSeeSuccessMessage"/> + <see selector="{{AdminMessagesSection.error}}" userInput="Store with the same code already exists." stepKey="seeErrorMessage"/> </test> </tests> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateWebsiteTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateWebsiteTest.xml new file mode 100644 index 000000000000..1608d0b7b5a2 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateWebsiteTest.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateWebsiteTest"> + <annotations> + <stories value="Create Website"/> + <title value="Create Website and Verify Store Form"/> + <description value="Test log in to Stores and Create Website Test"/> + <testCaseId value="MC-14302"/> + <severity value="CRITICAL"/> + <group value="store"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="{{customWebsite.name}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create website and AssertWebsiteSuccessSaveMessage--> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> + </actionGroup> + + <!--Search created website in grid and verify AssertWebsiteInGrid--> + <actionGroup ref="AssertWebsiteInGrid" stepKey="seeWebsiteInGrid"> + <argument name="websiteName" value="{{customWebsite.name}}"/> + </actionGroup> + + <!--Verify website name and websitecode on website form (AssertWebsiteForm and AssertWebsiteOnStoreForm)--> + <actionGroup ref="AssertWebsiteForm" stepKey="seeWebsiteForm"> + <argument name="websiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminDeleteStoreGroupTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminDeleteStoreGroupTest.xml new file mode 100644 index 000000000000..652537f7864c --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminDeleteStoreGroupTest.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminDeleteStoreGroupTest"> + <annotations> + <stories value="Delete Store Group"/> + <title value="Delete store group and save backup"/> + <description value="Test log in to Stores and Delete Store Group Test"/> + <testCaseId value="MC-14297"/> + <severity value="CRITICAL"/> + <group value="store"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <magentoCLI command="config:set system/backup/functionality_enabled 1" stepKey="setEnableBackupToYes"/> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create custom store group--> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewCustomStoreGroup"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStore.name}}"/> + <argument name="storeGroupCode" value="{{customStore.code}}"/> + </actionGroup> + </before> + <after> + <magentoCLI command="config:set system/backup/functionality_enabled 0" stepKey="setEnableBackupToNo"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Delete custom store group and verify AssertStoreGroupSuccessDeleteAndBackupMessages--> + <actionGroup ref="DeleteCustomStoreBackupEnabledYesActionGroup" stepKey="deleteCustomStoreGroup"> + <argument name="storeGroupName" value="{{customStore.name}}"/> + </actionGroup> + + <!--Verify deleted Store group is not present in grid and verify AssertStoreGroupNotInGrid message--> + <actionGroup ref="AssertStoreNotInGrid" stepKey="verifyDeletedStoreGroupNotInGrid"> + <argument name="storeGroupName" value="{{customStore.name}}"/> + </actionGroup> + + <!--Go to backup index page and verify AssertBackupInGrid--> + <amOnPage url="{{BackupIndexPage.url}}" stepKey="goToBackupIndexPage"/> + <waitForPageLoad stepKey="waitForBackupIndexPageLoad"/> + <see selector="{{AdminGridTableSection.backupNameColumn}}" userInput="{{WebSetupWizardBackup.name}}" stepKey="seeBackupInGrid"/> + <!--Delete database backup--> + <actionGroup ref="deleteBackup" stepKey="deleteDatabaseBackup"> + <argument name="backup" value="WebSetupWizardBackup"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminDeleteStoreViewTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminDeleteStoreViewTest.xml new file mode 100644 index 000000000000..fc1dcb5ee1a2 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminDeleteStoreViewTest.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminDeleteStoreViewTest"> + <annotations> + <stories value="Delete Store View"/> + <title value="Delete Store View and Save Backup"/> + <description value="Test log in to Stores and Delete Store View"/> + <testCaseId value="MC-14303"/> + <severity value="CRITICAL"/> + <group value="store"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <magentoCLI command="config:set system/backup/functionality_enabled 1" stepKey="setEnableBackupToYes"/> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create custom store view--> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createNewStoreView"> + <argument name="StoreGroup" value="_defaultStoreGroup"/> + <argument name="customStore" value="storeViewData"/> + </actionGroup> + </before> + <after> + <magentoCLI command="config:set system/backup/functionality_enabled 0" stepKey="setEnableBackupToNo"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Delete custom store view and verify AssertStoreSuccessDeleteMessage And BackupMessage--> + <actionGroup ref="DeleteCustomStoreViewBackupEnabledYesActionGroup" stepKey="deleteCustomStoreView"> + <argument name="storeViewName" value="{{storeViewData.name}}"/> + </actionGroup> + + <!--Verify deleted store view not present in grid and verify AssertStoreNotInGrid Message--> + <actionGroup ref="AssertStoreViewNotInGrid" stepKey="verifyDeletedStoreViewNotInGrid"> + <argument name="storeViewName" value="{{storeViewData.name}}"/> + </actionGroup> + + <!--Go to backup index page and verify AssertBackupInGrid--> + <amOnPage url="{{BackupIndexPage.url}}" stepKey="goToBackupIndexPage"/> + <waitForPageLoad stepKey="waitForBackupIndexPageLoad"/> + <see selector="{{AdminGridTableSection.backupNameColumn}}" userInput="{{WebSetupWizardBackup.name}}" stepKey="seeBackupInGrid"/> + <!--Delete database backup--> + <actionGroup ref="deleteBackup" stepKey="deleteDatabaseBackup"> + <argument name="backup" value="WebSetupWizardBackup"/> + </actionGroup> + + <!--Go to storefront and verify AssertStoreNotOnFrontend--> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefrontPage"/> + <waitForPageLoad stepKey="waitForStorefrontHomePageLoad"/> + <dontSee selector="{{StorefrontHeaderSection.storeViewList(storeViewData.name)}}" stepKey="dontSeeAssertStoreViewNameOnStorefront"/> + </test> +</tests> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminMoveStoreToOtherGroupSameWebsiteTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminMoveStoreToOtherGroupSameWebsiteTest.xml new file mode 100644 index 000000000000..b86b99936dbe --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminMoveStoreToOtherGroupSameWebsiteTest.xml @@ -0,0 +1,95 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMoveStoreToOtherGroupSameWebsiteTest"> + <annotations> + <stories value="Move Store"/> + <title value="Move Store To Other Group Same Website and Verify Backend and Frontend"/> + <description value="Test log in to Stores and Move Store To Other Group Same Website Test"/> + <testCaseId value="MC-14294"/> + <severity value="CRITICAL"/> + <group value="store"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <!-- Create first store --> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createFirstStore"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStore.name}}"/> + <argument name="storeGroupCode" value="{{customStore.code}}"/> + </actionGroup> + <!-- Create first store view --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createFirstStoreView"> + <argument name="StoreGroup" value="customStore"/> + <argument name="customStore" value="storeViewData1"/> + </actionGroup> + <!-- Create second store --> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createSecondStore"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> + </actionGroup> + <!-- Create second store view --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createSecondStoreView"> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="storeViewData2"/> + </actionGroup> + </before> + <after> + <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteFirstStore"> + <argument name="storeGroupName" value="customStore.name"/> + </actionGroup> + <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteSecondStore"> + <argument name="storeGroupName" value="customStoreGroup.name"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Search created second store view in grid--> + <actionGroup ref="AssertStoreViewInGridActionGroup" stepKey="searchCreatedStoreViewInGrid"> + <argument name="storeViewName" value="{{storeViewData2.name}}"/> + </actionGroup> + <!--Move created store view to other store keeping website same--> + <actionGroup ref="ChangeStoreInStoreViewActionGroup" stepKey="moveStoreView"> + <argument name="storeDropdown" value="{{customStore.name}}"/> + </actionGroup> + <!--Save the above store view and verify AssertStoreViewSuccessSaveMessage--> + <actionGroup ref="AdminCreateStoreViewActionSaveGroup" stepKey="verifyAssertStoreViewSuccessSaveMessage"/> + + <!--Search moved store view(from above step) in grid and verify AssertStoreInGrid--> + <actionGroup ref="AssertStoreViewInGridActionGroup" stepKey="searchMovedStoreViewInGrid"> + <argument name="storeViewName" value="{{storeViewData2.name}}"/> + </actionGroup> + + <!--Go to store view form page and verify AssertStoreForm--> + <actionGroup ref="AssertStoreViewFormActionGroup" stepKey="verifyStoreViewForm"> + <argument name="storeDropdown" value="{{customStore.name}}"/> + <argument name="storeViewName" value="{{storeViewData2.name}}"/> + <argument name="storeViewCode" value="{{storeViewData2.code}}"/> + <argument name="status" value="Enabled"/> + </actionGroup> + + <!--Go to store configuration page and verify AssertStoreBackend--> + <actionGroup ref="AssertStoreConfigurationBackendActionGroup" stepKey="verifyValuesOnStoreBackend"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="customStore" value="{{customStore.name}}"/> + <argument name="storeView1" value="{{storeViewData1.name}}"/> + <argument name="storeView2" value="{{storeViewData2.name}}"/> + </actionGroup> + + <!--Go to storefront and verify AssertStoreFrontend--> + <actionGroup ref="AssertStoreFrontendActionGroup" stepKey="verifyValuesOnStoreFrontend"> + <argument name="customStore" value="{{customStore.name}}"/> + <argument name="storeView1" value="{{storeViewData1.name}}"/> + <argument name="storeView2" value="{{storeViewData2.name}}"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAcceptAlertAndVerifyStoreViewFormTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAcceptAlertAndVerifyStoreViewFormTest.xml new file mode 100644 index 000000000000..9c84388d86f9 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAcceptAlertAndVerifyStoreViewFormTest.xml @@ -0,0 +1,76 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateStoreGroupAcceptAlertAndVerifyStoreViewFormTest"> + <annotations> + <stories value="Update Store Group"/> + <title value="Update Store Group, Accept Alert and Verify Store View Form"/> + <description value="Test log in to Stores and Update Store Group Test"/> + <testCaseId value="MC-14296"/> + <severity value="CRITICAL"/> + <group value="store"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create root category--> + <createData entity="NewRootCategory" stepKey="rootCategory"/> + <createData entity="SimpleRootSubCategory" stepKey="category"> + <requiredEntity createDataKey="rootCategory"/> + </createData> + <!--Create website--> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> + </actionGroup> + <!--Create custom store group--> + <actionGroup ref="CreateCustomStore" stepKey="createCustomStoreGroup"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="store" value="{{staticStoreGroup.name}}"/> + <argument name="rootCategory" value="Default Category"/> + </actionGroup> + </before> + <after> + <!--Delete website--> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="{{customWebsite.name}}"/> + </actionGroup> + <!--Delete root category--> + <deleteData stepKey="deleteRootCategory" createDataKey="rootCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open created Store group in grid--> + <actionGroup ref="AssertStoreGroupInGridActionGroup" stepKey="openCreatedStoreGroupInGrid"> + <argument name="storeGroupName" value="{{staticStoreGroup.name}}"/> + </actionGroup> + <click selector="{{AdminStoresGridSection.firstRow}}" stepKey="clickFirstRow"/> + <waitForPageLoad stepKey="AdminSystemStoreGroupPageToOpen"/> + <!--Update created Store group as per requirement and accept alert message--> + <actionGroup ref="EditCustomStoreGroupAcceptWarningMessageActionGroup" stepKey="updateCustomStoreGroup"> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="store" value="{{customStoreGroup.name}}"/> + <argument name="rootCategory" value="$$rootCategory.name$$"/> + </actionGroup> + + <!--Search updated store group(from above step) in grid and verify AssertStoreGroupInGrid--> + <actionGroup ref="AssertStoreGroupInGridActionGroup" stepKey="seeUpdatedStoreGroupInGrid"> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + </actionGroup> + + <!--Verify updated website name and updated websitecode on website form (AssertStoreGroupForm and AssertStoreGroupOnStoreViewForm)--> + <actionGroup ref="AssertStoreGroupFormActionGroup" stepKey="seeUpdatedStoreGroupForm"> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> + <argument name="rootCategory" value="$$rootCategory.name$$"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAndVerifyStoreViewFormTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAndVerifyStoreViewFormTest.xml new file mode 100644 index 000000000000..3d85a3490143 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAndVerifyStoreViewFormTest.xml @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateStoreGroupAndVerifyStoreViewFormTest"> + <annotations> + <stories value="Update Store Group"/> + <title value="Update Store Group and Verify Store View Form"/> + <description value="Test log in to Stores and Update Store Group Test"/> + <testCaseId value="MC-14295"/> + <severity value="CRITICAL"/> + <group value="store"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create custom store group--> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewCustomStoreGroup"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="storeGroupName" value="{{SecondStoreGroupUnique.name}}"/> + <argument name="storeGroupCode" value="{{SecondStoreGroupUnique.code}}"/> + </actionGroup> + </before> + <after> + <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteStoreGroup"> + <argument name="storeGroupName" value="customStoreGroup.name"/> + </actionGroup> + <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteUpdatedStoreGroup"> + <argument name="storeGroupName" value="SecondStoreGroupUnique.name"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open created Store group in grid--> + <actionGroup ref="AssertStoreGroupInGridActionGroup" stepKey="openCreatedStoreGroupInGrid"> + <argument name="storeGroupName" value="{{SecondStoreGroupUnique.name}}"/> + </actionGroup> + <click selector="{{AdminStoresGridSection.firstRow}}" stepKey="clickFirstRow"/> + <waitForPageLoad stepKey="AdminSystemStoreGroupPageToOpen"/> + <!--Update created Store group as per requirement--> + <actionGroup ref="CreateCustomStore" stepKey="createNewCustomStoreGroup"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="store" value="{{customStoreGroup.name}}"/> + <argument name="rootCategory" value="Default Category"/> + </actionGroup> + + <!--Search updated store group(from above step) in grid and verify AssertStoreGroupInGrid--> + <actionGroup ref="AssertStoreGroupInGridActionGroup" stepKey="seeUpdatedStoreGroupInGrid"> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + </actionGroup> + + <!--Verify updated website name and updated websitecode on website form (AssertStoreGroupForm and AssertStoreGroupOnStoreViewForm)--> + <actionGroup ref="AssertStoreGroupFormActionGroup" stepKey="seeUpdatedStoreGroupForm"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> + <argument name="rootCategory" value="Default Category"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreViewTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreViewTest.xml new file mode 100644 index 000000000000..054ee789fbdc --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreViewTest.xml @@ -0,0 +1,81 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateStoreViewTest"> + <annotations> + <stories value="Update Store View"/> + <title value="Update Store View and Verify Backend and Frontend"/> + <description value="Test log in to Stores and Update Store View Test"/> + <testCaseId value="MC-14316"/> + <severity value="CRITICAL"/> + <group value="store"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create custom store view--> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createNewStoreView"> + <argument name="StoreGroup" value="_defaultStoreGroup"/> + <argument name="customStore" value="storeViewData"/> + </actionGroup> + </before> + <after> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"> + <argument name="customStore" value="storeViewData"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteUpdatedStoreView"> + <argument name="customStore" value="SecondStoreUnique"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Search created store view in grid--> + <actionGroup ref="AssertStoreViewInGridActionGroup" stepKey="searchCreatedStoreViewInGrid"> + <argument name="storeViewName" value="{{storeViewData.name}}"/> + </actionGroup> + <click selector="{{AdminStoresGridSection.storeNameInFirstRow}}" stepKey="clickStoreViewFirstRowInGrid"/> + <waitForPageLoad stepKey="waitForAdminSystemStoreViewPageLoad"/> + <!--Update created store view as per requirements--> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="updateStoreView"> + <argument name="StoreGroup" value="_defaultStoreGroup"/> + <argument name="customStore" value="SecondStoreUnique"/> + </actionGroup> + <!--Save the updated Store view and verify AssertStoreViewSuccessSaveMessage--> + <actionGroup ref="AdminCreateStoreViewActionSaveGroup" stepKey="verifyAssertStoreViewSuccessSaveMessage"> + </actionGroup> + + <!--Search updated store view in grid and verify AssertStoreViewInGridMessage--> + <actionGroup ref="AssertStoreViewInGridActionGroup" stepKey="verifyUpdatedStoreViewInGrid"> + <argument name="storeViewName" value="{{SecondStoreUnique.name}}"/> + </actionGroup> + + <!--Go to store view form page and verify AssertStoreForm--> + <actionGroup ref="AssertStoreViewFormActionGroup" stepKey="verifyStoreViewForm"> + <argument name="storeDropdown" value="{{_defaultStoreGroup.name}}"/> + <argument name="storeViewName" value="{{SecondStoreUnique.name}}"/> + <argument name="storeViewCode" value="{{SecondStoreUnique.code}}"/> + <argument name="status" value="Enabled"/> + </actionGroup> + + <!--Go to store configuration page and verify AssertStoreBackend--> + <amOnPage url="{{AdminConfigPage.url}}" stepKey="goToConfigStoreConfigurationPage"/> + <waitForPageLoad stepKey="waitForSystemStoreConfigurationPageLoad" /> + <click selector="{{AdminConfigSection.defaultConfigButton}}" stepKey="clickDefaultConfigButton"/> + <see selector="{{AdminConfigSection.defaultConfigDropdown}}" userInput="{{storeViewData.name}}" stepKey="seeAssertStoreViewInDefaultConfigDropdown"/> + <see selector="{{AdminConfigSection.defaultConfigDropdown}}" userInput="{{SecondStoreUnique.name}}" stepKey="seeAssertUpdateStoreViewInDefaultConfigDropdown"/> + + <!--Go to storefront and verify AssertStoreFrontend--> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefrontPage"/> + <waitForPageLoad stepKey="waitForStorefrontHomePageLoad"/> + <click selector="{{StorefrontHeaderSection.storeViewSwitcher}}" stepKey="selectStoreSwitcher"/> + <waitForPageLoad stepKey="waitForFirstStoreView"/> + <see selector="{{StorefrontHeaderSection.storeViewDropdown}}" userInput="{{storeViewData.name}}" stepKey="seeAssertStoreViewOnStorefront"/> + <see selector="{{StorefrontHeaderSection.storeViewDropdown}}" userInput="{{SecondStoreUnique.name}}" stepKey="seeAssertUpdatedStoreViewOnStorefront"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateWebsiteTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateWebsiteTest.xml new file mode 100644 index 000000000000..6b666126569a --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateWebsiteTest.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateWebsiteTest"> + <annotations> + <stories value="Update Website"/> + <title value="Update Website and Verify Store Form"/> + <description value="Test log in to Stores and Update Website Test"/> + <testCaseId value="MC-14301"/> + <severity value="CRITICAL"/> + <group value="store"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref = "LoginAsAdmin" stepKey="loginAsAdmin"/> + <!--Create website--> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> + </actionGroup> + </before> + <after> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="{{updateCustomWebsite.name}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Search created custom website in grid--> + <actionGroup ref="AssertWebsiteInGrid" stepKey="seeWebsiteInGrid"> + <argument name="websiteName" value="{{customWebsite.name}}"/> + </actionGroup> + <click selector="{{AdminStoresGridSection.websiteName(customWebsite.name)}}" stepKey="clickWebsiteFirstRowInGrid"/> + <waitForPageLoad stepKey="waitForWebsiteFormPageToOpen"/> + <!--Update website name and website code as per data created and verify AssertWebsiteSuccessSaveMessage--> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> + <argument name="newWebsiteName" value="{{updateCustomWebsite.name}}"/> + <argument name="websiteCode" value="{{updateCustomWebsite.code}}"/> + </actionGroup> + + <!--Search updated custom website(from above step) in grid and verify AssertWebsiteInGrid--> + <actionGroup ref="AssertWebsiteInGrid" stepKey="seeUpdatedWebsiteInGrid"> + <argument name="websiteName" value="{{updateCustomWebsite.name}}"/> + </actionGroup> + + <!--Verify updated website name and updated websitecode on website form (AssertWebsiteForm and AssertWebsiteOnStoreForm)--> + <actionGroup ref="AssertWebsiteForm" stepKey="seeUpdatedWebsiteForm"> + <argument name="websiteName" value="{{updateCustomWebsite.name}}"/> + <argument name="websiteCode" value="{{updateCustomWebsite.code}}"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/Test/StorefrontAddStoreCodeInUrlTest.xml b/app/code/Magento/Store/Test/Mftf/Test/StorefrontAddStoreCodeInUrlTest.xml new file mode 100644 index 000000000000..95f5a9cd2d66 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Test/StorefrontAddStoreCodeInUrlTest.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontAddStoreCodeInUrlTest"> + <annotations> + <features value="Backend"/> + <title value="Store code should be added to storefront URL if the corresponding configuration is enabled"/> + <description value="Store code should be added to storefront URL if the corresponding configuration is enabled"/> + <testCaseId value="MC-15944" /> + <group value="store"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <magentoCLI command="config:set {{StorefrontEnableAddStoreCodeToUrls.path}} {{StorefrontEnableAddStoreCodeToUrls.value}}" stepKey="addStoreCodeToUrlEnable"/> + </before> + <after> + <magentoCLI command="config:set {{StorefrontDisableAddStoreCodeToUrls.path}} {{StorefrontDisableAddStoreCodeToUrls.value}}" stepKey="addStoreCodeToUrlDisable"/> + </after> + + <actionGroup ref="StorefrontClickOnHeaderLogoActionGroup" stepKey="clickOnStorefrontHeaderLogo"/> + <actionGroup ref="AssertStorefrontStoreCodeInUrlActionGroup" stepKey="seeStoreCodeInUrl"/> + </test> +</tests> diff --git a/app/code/Magento/Store/Test/Unit/App/Request/PathInfoProcessorTest.php b/app/code/Magento/Store/Test/Unit/App/Request/PathInfoProcessorTest.php index 4a9b7d798c32..5d0f11b8a98d 100644 --- a/app/code/Magento/Store/Test/Unit/App/Request/PathInfoProcessorTest.php +++ b/app/code/Magento/Store/Test/Unit/App/Request/PathInfoProcessorTest.php @@ -12,45 +12,83 @@ class PathInfoProcessorTest extends \PHPUnit\Framework\TestCase /** * @var \Magento\Store\App\Request\PathInfoProcessor */ - protected $_model; + private $model; /** * @var \PHPUnit_Framework_MockObject_MockObject */ - protected $_storeManagerMock; + private $requestMock; /** * @var \PHPUnit_Framework_MockObject_MockObject */ - protected $_requestMock; + private $validatorConfigMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $processorConfigMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $pathInfoMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $storeRepositoryMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $storePathInfoValidator; /** * @var string */ - protected $_pathInfo = '/storeCode/node_one/'; + protected $pathInfo = '/storeCode/node_one/'; protected function setUp() { - $this->_requestMock = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) + $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) + ->disableOriginalConstructor()->getMock(); + + $this->validatorConfigMock = $this->createMock(\Magento\Framework\App\Config\ReinitableConfigInterface::class); + + $this->processorConfigMock = $this->createMock(\Magento\Framework\App\Config\ReinitableConfigInterface::class); + + $this->storeRepositoryMock = $this->createMock(\Magento\Store\Api\StoreRepositoryInterface::class); + + $this->pathInfoMock = $this->getMockBuilder(\Magento\Framework\App\Request\PathInfo ::class) ->disableOriginalConstructor()->getMock(); - $this->_storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManager::class); - $this->_model = new \Magento\Store\App\Request\PathInfoProcessor($this->_storeManagerMock); + + $this->storePathInfoValidator = new \Magento\Store\App\Request\StorePathInfoValidator( + $this->validatorConfigMock, + $this->storeRepositoryMock, + $this->pathInfoMock + ); + + $this->model = new \Magento\Store\App\Request\PathInfoProcessor( + $this->storePathInfoValidator, + $this->validatorConfigMock + ); } public function testProcessIfStoreExistsAndIsNotDirectAccessToFrontName() { + $this->validatorConfigMock->expects($this->any())->method('getValue')->willReturn(true); + $store = $this->createMock(\Magento\Store\Model\Store::class); - $this->_storeManagerMock->expects( - $this->once() + $this->storeRepositoryMock->expects( + $this->atLeastOnce() )->method( - 'getStore' + 'getActiveStoreByCode' )->with( 'storeCode' )->willReturn($store); - $store->expects($this->once())->method('getCode')->will($this->returnValue('storeCode')); - $store->expects($this->once())->method('isUseStoreInUrl')->will($this->returnValue(true)); - $this->_requestMock->expects( - $this->once() + $this->requestMock->expects( + $this->atLeastOnce() )->method( 'isDirectAccessFrontendName' )->with( @@ -58,67 +96,69 @@ public function testProcessIfStoreExistsAndIsNotDirectAccessToFrontName() )->will( $this->returnValue(false) ); - $this->_storeManagerMock->expects($this->once())->method('setCurrentStore')->with('storeCode'); - $this->assertEquals('/node_one/', $this->_model->process($this->_requestMock, $this->_pathInfo)); + $this->assertEquals('/node_one/', $this->model->process($this->requestMock, $this->pathInfo)); } public function testProcessIfStoreExistsAndDirectAccessToFrontName() { - $store = $this->createMock(\Magento\Store\Model\Store::class); - $this->_storeManagerMock->expects( - $this->once() + $this->validatorConfigMock->expects($this->atLeastOnce())->method('getValue')->willReturn(true); + + $this->storeRepositoryMock->expects( + $this->any() )->method( - 'getStore' - )->with( - 'storeCode' - )->willReturn($store); - $store->expects($this->once())->method('isUseStoreInUrl')->will($this->returnValue(true)); - $this->_requestMock->expects( - $this->once() + 'getActiveStoreByCode' + ); + $this->requestMock->expects( + $this->atLeastOnce() )->method( 'isDirectAccessFrontendName' )->with( 'storeCode' - )->will( - $this->returnValue(true) - ); - $this->_requestMock->expects($this->once())->method('setActionName')->with('noroute'); - $this->assertEquals($this->_pathInfo, $this->_model->process($this->_requestMock, $this->_pathInfo)); + )->willReturn(true); + $this->requestMock->expects($this->once())->method('setActionName')->with('noroute'); + $this->assertEquals($this->pathInfo, $this->model->process($this->requestMock, $this->pathInfo)); } public function testProcessIfStoreIsEmpty() { + $this->validatorConfigMock->expects($this->any())->method('getValue')->willReturn(true); + $path = '/0/node_one/'; - $store = $this->createMock(\Magento\Store\Model\Store::class); - $this->_storeManagerMock->expects( - $this->once() + $this->storeRepositoryMock->expects( + $this->never() )->method( - 'getStore' - )->with( - '0' - )->willReturn($store); - $store->expects($this->once())->method('isUseStoreInUrl')->will($this->returnValue(true)); - $this->_requestMock->expects( - $this->once() + 'getActiveStoreByCode' + ); + $this->requestMock->expects( + $this->never() )->method( 'isDirectAccessFrontendName' - )->with( - '0' - )->will( - $this->returnValue(true) ); - $this->_requestMock->expects($this->never())->method('setActionName'); - $this->assertEquals($path, $this->_model->process($this->_requestMock, $path)); + $this->requestMock->expects($this->never())->method('setActionName'); + $this->assertEquals($path, $this->model->process($this->requestMock, $path)); } public function testProcessIfStoreCodeIsNotExist() { - $store = $this->createMock(\Magento\Store\Model\Store::class); - $this->_storeManagerMock->expects($this->once())->method('getStore')->with('storeCode') + $this->validatorConfigMock->expects($this->atLeastOnce())->method('getValue')->willReturn(true); + + $this->storeRepositoryMock->expects($this->once())->method('getActiveStoreByCode')->with('storeCode') ->willThrowException(new NoSuchEntityException()); - $store->expects($this->never())->method('isUseStoreInUrl'); - $this->_requestMock->expects($this->never())->method('isDirectAccessFrontendName'); + $this->requestMock->expects($this->never())->method('isDirectAccessFrontendName'); + + $this->assertEquals($this->pathInfo, $this->model->process($this->requestMock, $this->pathInfo)); + } + + public function testProcessIfStoreUrlNotEnabled() + { + $this->validatorConfigMock->expects($this->at(0))->method('getValue')->willReturn(true); + + $this->validatorConfigMock->expects($this->at(1))->method('getValue')->willReturn(true); + + $this->validatorConfigMock->expects($this->at(2))->method('getValue')->willReturn(false); + + $this->storeRepositoryMock->expects($this->once())->method('getActiveStoreByCode')->willReturn(1); - $this->assertEquals($this->_pathInfo, $this->_model->process($this->_requestMock, $this->_pathInfo)); + $this->assertEquals($this->pathInfo, $this->model->process($this->requestMock, $this->pathInfo)); } } diff --git a/app/code/Magento/Store/Test/Unit/Model/BaseUrlCheckerTest.php b/app/code/Magento/Store/Test/Unit/Model/BaseUrlCheckerTest.php index 0eea86da61b3..78a627c195f8 100644 --- a/app/code/Magento/Store/Test/Unit/Model/BaseUrlCheckerTest.php +++ b/app/code/Magento/Store/Test/Unit/Model/BaseUrlCheckerTest.php @@ -78,9 +78,9 @@ public function testExecute() public function testIsEnabled() { $this->scopeConfig->expects($this->once()) - ->method('getValue') + ->method('isSetFlag') ->with('web/url/redirect_to_base', \Magento\Store\Model\ScopeInterface::SCOPE_STORE) - ->willReturn(1); + ->willReturn(!!1); $this->assertTrue($this->baseUrlChecker->isEnabled()); } @@ -89,15 +89,16 @@ public function testIsEnabled() */ public function testIsFrontendSecure() { - $this->scopeConfig->expects($this->exactly(2)) + $this->scopeConfig->expects($this->once()) ->method('getValue') - ->withConsecutive( - ['web/unsecure/base_url', \Magento\Store\Model\ScopeInterface::SCOPE_STORE], - ['web/secure/use_in_frontend', \Magento\Store\Model\ScopeInterface::SCOPE_STORE] - )->will($this->onConsecutiveCalls( - $this->returnValue('https://localhost'), - 1 - )); + ->with('web/unsecure/base_url', \Magento\Store\Model\ScopeInterface::SCOPE_STORE) + ->willReturn('https://localhost'); + + $this->scopeConfig->expects($this->once()) + ->method('isSetFlag') + ->with('web/secure/use_in_frontend', \Magento\Store\Model\ScopeInterface::SCOPE_STORE) + ->willReturn(!!1); + $this->assertTrue($this->baseUrlChecker->isFrontendSecure()); } } diff --git a/app/code/Magento/Store/Test/Unit/Model/Config/Importer/Processor/CreateTest.php b/app/code/Magento/Store/Test/Unit/Model/Config/Importer/Processor/CreateTest.php index 2c2b0b00aec4..0fbf7bb7f044 100644 --- a/app/code/Magento/Store/Test/Unit/Model/Config/Importer/Processor/CreateTest.php +++ b/app/code/Magento/Store/Test/Unit/Model/Config/Importer/Processor/CreateTest.php @@ -196,14 +196,30 @@ private function initTestData() 'root_category_id' => '1', 'default_store_id' => '1', 'code' => 'default', + ], + 2 => [ + 'group_id' => '1', + 'website_id' => '1', + 'name' => 'Default1', + 'default_store_id' => '1', + 'code' => 'default1', ] ]; - $this->trimmedGroup = [ - 'name' => 'Default', - 'root_category_id' => '1', - 'code' => 'default', - 'default_store_id' => '1', - ]; + $this->trimmedGroup = + [ + 0 => [ + 'name' => 'Default', + 'root_category_id' => '1', + 'code' => 'default', + 'default_store_id' => '1', + ], + 1 => [ + 'name' => 'Default1', + 'root_category_id' => '0', + 'code' => 'default1', + 'default_store_id' => '1' + ] + ]; $this->stores = [ 'default' => [ 'store_id' => '1', @@ -280,34 +296,34 @@ public function testRunGroup() [ScopeInterface::SCOPE_GROUPS, $this->groups, $this->groups], ]); - $this->websiteMock->expects($this->once()) + $this->websiteMock->expects($this->exactly(2)) ->method('getResource') ->willReturn($this->abstractDbMock); - $this->groupMock->expects($this->once()) + $this->groupMock->expects($this->exactly(2)) ->method('setData') - ->with($this->trimmedGroup) - ->willReturnSelf(); - $this->groupMock->expects($this->exactly(3)) + ->withConsecutive( + [$this->equalTo($this->trimmedGroup[0])], + [$this->equalTo($this->trimmedGroup[1])] + )->willReturnSelf(); + + $this->groupMock->expects($this->exactly(6)) ->method('getResource') ->willReturn($this->abstractDbMock); - $this->groupMock->expects($this->once()) - ->method('setRootCategoryId') - ->with(0); - $this->groupMock->expects($this->once()) + $this->groupMock->expects($this->exactly(2)) ->method('getDefaultStoreId') ->willReturn($defaultStoreId); - $this->groupMock->expects($this->once()) + $this->groupMock->expects($this->exactly(2)) ->method('setDefaultStoreId') ->with($storeId); - $this->groupMock->expects($this->once()) + $this->groupMock->expects($this->exactly(2)) ->method('setWebsite') ->with($this->websiteMock); - $this->storeMock->expects($this->once()) + $this->storeMock->expects($this->exactly(2)) ->method('getResource') ->willReturn($this->abstractDbMock); - $this->storeMock->expects($this->once()) + $this->storeMock->expects($this->exactly(2)) ->method('getStoreId') ->willReturn($storeId); @@ -315,11 +331,11 @@ public function testRunGroup() ->method('load') ->withConsecutive([$this->websiteMock, 'base', 'code'], [$this->storeMock, 'default', 'code']) ->willReturnSelf(); - $this->abstractDbMock->expects($this->exactly(2)) + $this->abstractDbMock->expects($this->exactly(4)) ->method('save') ->with($this->groupMock) ->willReturnSelf(); - $this->abstractDbMock->expects($this->once()) + $this->abstractDbMock->expects($this->exactly(2)) ->method('addCommitCallback') ->willReturnCallback(function ($function) { return $function(); diff --git a/app/code/Magento/Store/Test/Unit/Model/Plugin/StoreCookieTest.php b/app/code/Magento/Store/Test/Unit/Model/Plugin/StoreCookieTest.php index 0454c0096a6a..9f4be98ab558 100644 --- a/app/code/Magento/Store/Test/Unit/Model/Plugin/StoreCookieTest.php +++ b/app/code/Magento/Store/Test/Unit/Model/Plugin/StoreCookieTest.php @@ -54,11 +54,6 @@ class StoreCookieTest extends \PHPUnit\Framework\TestCase */ protected $storeRepositoryMock; - /** - * @var \Magento\Store\Api\StoreResolverInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $storeResolverMock; - /** * Set up */ @@ -94,18 +89,12 @@ protected function setUp() ->setMethods([]) ->getMock(); - $this->storeResolverMock = $this->getMockBuilder(\Magento\Store\Api\StoreResolverInterface::class) - ->disableOriginalConstructor() - ->setMethods([]) - ->getMock(); - $this->plugin = (new ObjectManager($this))->getObject( \Magento\Store\Model\Plugin\StoreCookie::class, [ 'storeManager' => $this->storeManagerMock, 'storeCookieManager' => $this->storeCookieManagerMock, - 'storeRepository' => $this->storeRepositoryMock, - 'storeResolver' => $this->storeResolverMock + 'storeRepository' => $this->storeRepositoryMock ] ); } diff --git a/app/code/Magento/Store/Test/Unit/Model/StoreTest.php b/app/code/Magento/Store/Test/Unit/Model/StoreTest.php index f98cf5d892e0..a83ca833a15e 100644 --- a/app/code/Magento/Store/Test/Unit/Model/StoreTest.php +++ b/app/code/Magento/Store/Test/Unit/Model/StoreTest.php @@ -3,11 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Store\Test\Unit\Model; use Magento\Framework\App\Config\ReinitableConfigInterface; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Session\SessionManagerInterface; use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\Store; @@ -38,6 +38,16 @@ class StoreTest extends \PHPUnit\Framework\TestCase */ protected $filesystemMock; + /** + * @var ReinitableConfigInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $configMock; + + /** + * @var SessionManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $sessionMock; + /** * @var \Magento\Framework\Url\ModifierInterface|\PHPUnit_Framework_MockObject_MockObject */ @@ -61,12 +71,22 @@ protected function setUp() 'isSecure', 'getServer', ]); + $this->filesystemMock = $this->getMockBuilder(\Magento\Framework\Filesystem::class) ->disableOriginalConstructor() ->getMock(); + $this->configMock = $this->getMockBuilder(ReinitableConfigInterface::class) + ->getMock(); + $this->sessionMock = $this->getMockBuilder(SessionManagerInterface::class) + ->setMethods(['getCurrencyCode']) + ->getMockForAbstractClass(); $this->store = $this->objectManagerHelper->getObject( \Magento\Store\Model\Store::class, - ['filesystem' => $this->filesystemMock] + [ + 'filesystem' => $this->filesystemMock, + 'config' => $this->configMock, + 'session' => $this->sessionMock, + ] ); $this->urlModifierMock = $this->createMock(\Magento\Framework\Url\ModifierInterface::class); @@ -140,7 +160,7 @@ public function testGetWebsite() /** @var \Magento\Store\Model\Store $model */ $model = $this->objectManagerHelper->getObject( \Magento\Store\Model\Store::class, - ['websiteRepository' => $websiteRepository,] + ['websiteRepository' => $websiteRepository] ); $model->setWebsiteId($websiteId); @@ -161,7 +181,7 @@ public function testGetWebsiteIfWebsiteIsNotExist() /** @var \Magento\Store\Model\Store $model */ $model = $this->objectManagerHelper->getObject( \Magento\Store\Model\Store::class, - ['websiteRepository' => $websiteRepository,] + ['websiteRepository' => $websiteRepository] ); $model->setWebsiteId(null); @@ -187,7 +207,7 @@ public function testGetGroup() /** @var \Magento\Store\Model\Store $model */ $model = $this->objectManagerHelper->getObject( \Magento\Store\Model\Store::class, - ['groupRepository' => $groupRepository,] + ['groupRepository' => $groupRepository] ); $model->setGroupId($groupId); @@ -208,7 +228,7 @@ public function testGetGroupIfGroupIsNotExist() /** @var \Magento\Store\Model\Store $model */ $model = $this->objectManagerHelper->getObject( \Magento\Store\Model\Store::class, - ['groupRepository' => $groupRepository,] + ['groupRepository' => $groupRepository] ); $model->setGroupId(null); @@ -357,30 +377,31 @@ public function testGetBaseUrlEntryPoint() $configMock = $this->getMockForAbstractClass(\Magento\Framework\App\Config\ReinitableConfigInterface::class); $configMock->expects($this->atLeastOnce()) ->method('getValue') - ->will($this->returnCallback( - function ($path, $scope, $scopeCode) use ($expectedPath) { - return $expectedPath == $path ? 'http://domain.com/' . $path . '/' : null; - } - )); + ->willReturnCallback(function ($path, $scope, $scopeCode) use ($expectedPath) { + return $expectedPath == $path ? 'http://domain.com/' . $path . '/' : null; + }); + $this->requestMock->expects($this->once()) + ->method('getServer') + ->with('SCRIPT_FILENAME') + ->willReturn('test_script.php'); + /** @var \Magento\Store\Model\Store $model */ $model = $this->objectManagerHelper->getObject( \Magento\Store\Model\Store::class, [ 'config' => $configMock, 'isCustomEntryPoint' => false, + 'request' => $this->requestMock ] ); $model->setCode('scopeCode'); $this->setUrlModifier($model); - $server = $_SERVER; - $_SERVER['SCRIPT_FILENAME'] = 'test_script.php'; $this->assertEquals( $expectedBaseUrl, $model->getBaseUrl(\Magento\Framework\UrlInterface::URL_TYPE_LINK, false) ); - $_SERVER = $server; } /** @@ -572,7 +593,7 @@ public function testGetAllowedCurrencies() /** @var \Magento\Store\Model\Store $model */ $model = $this->objectManagerHelper->getObject( \Magento\Store\Model\Store::class, - ['config' => $configMock, 'currencyInstalled' => $currencyPath,] + ['config' => $configMock, 'currencyInstalled' => $currencyPath] ); $this->assertEquals($expectedResult, $model->getAllowedCurrencies()); @@ -694,6 +715,80 @@ public function testGetScopeTypeName() $this->assertEquals('Store View', $this->store->getScopeTypeName()); } + /** + * @param array $availableCodes + * @param string $currencyCode + * @param string $defaultCode + * @param string $expectedCode + * @return void + * @dataProvider currencyCodeDataProvider + */ + public function testGetCurrentCurrencyCode( + array $availableCodes, + string $currencyCode, + string $defaultCode, + string $expectedCode + ): void { + $this->store->setData('available_currency_codes', $availableCodes); + $this->sessionMock->method('getCurrencyCode') + ->willReturn($currencyCode); + $this->configMock->method('getValue') + ->with(\Magento\Directory\Model\Currency::XML_PATH_CURRENCY_DEFAULT) + ->willReturn($defaultCode); + + $code = $this->store->getCurrentCurrencyCode(); + $this->assertEquals($expectedCode, $code); + } + + /** + * @return array + */ + public function currencyCodeDataProvider(): array + { + return [ + [ + [ + 'USD', + ], + 'USD', + 'USD', + 'USD', + ], + [ + [ + 'USD', + 'EUR', + ], + 'EUR', + 'USD', + 'EUR', + ], + [ + [ + 'EUR', + 'USD', + ], + 'GBP', + 'USD', + 'USD', + ], + [ + [ + 'USD', + ], + 'GBP', + 'EUR', + 'USD', + ], + [ + [], + 'GBP', + 'EUR', + 'EUR', + ], + ]; + } + /** * @param \Magento\Store\Model\Store $model */ diff --git a/app/code/Magento/Store/Test/Unit/Url/Plugin/RouteParamsResolverTest.php b/app/code/Magento/Store/Test/Unit/Url/Plugin/RouteParamsResolverTest.php index f31acd40a2e6..9b83714166b1 100644 --- a/app/code/Magento/Store/Test/Unit/Url/Plugin/RouteParamsResolverTest.php +++ b/app/code/Magento/Store/Test/Unit/Url/Plugin/RouteParamsResolverTest.php @@ -80,7 +80,7 @@ public function testBeforeSetRouteParamsScopeInParams() $routeParamsResolverMock->expects($this->once())->method('setScope')->with($storeCode); $routeParamsResolverMock->expects($this->once())->method('getScope')->willReturn($storeCode); - $this->queryParamsResolverMock->expects($this->never())->method('setQueryParam'); + $this->queryParamsResolverMock->expects($this->any())->method('setQueryParam'); $this->model->beforeSetRouteParams( $routeParamsResolverMock, @@ -113,7 +113,7 @@ public function testBeforeSetRouteParamsScopeUseStoreInUrl() $routeParamsResolverMock->expects($this->once())->method('setScope')->with($storeCode); $routeParamsResolverMock->expects($this->once())->method('getScope')->willReturn($storeCode); - $this->queryParamsResolverMock->expects($this->once())->method('setQueryParam')->with('___store', $storeCode); + $this->queryParamsResolverMock->expects($this->never())->method('setQueryParam')->with('___store', $storeCode); $this->model->beforeSetRouteParams( $routeParamsResolverMock, @@ -178,7 +178,7 @@ public function testBeforeSetRouteParamsNoScopeInParams() $routeParamsResolverMock->expects($this->never())->method('setScope'); $routeParamsResolverMock->expects($this->once())->method('getScope')->willReturn(false); - $this->queryParamsResolverMock->expects($this->once())->method('setQueryParam')->with('___store', $storeCode); + $this->queryParamsResolverMock->expects($this->never())->method('setQueryParam')->with('___store', $storeCode); $this->model->beforeSetRouteParams( $routeParamsResolverMock, diff --git a/app/code/Magento/Store/Url/Plugin/RouteParamsResolver.php b/app/code/Magento/Store/Url/Plugin/RouteParamsResolver.php index 468352af78cb..9c9d1e6023af 100644 --- a/app/code/Magento/Store/Url/Plugin/RouteParamsResolver.php +++ b/app/code/Magento/Store/Url/Plugin/RouteParamsResolver.php @@ -78,7 +78,7 @@ public function beforeSetRouteParams( $storeCode ); - if ($useStoreInUrl && !$this->storeManager->hasSingleStore()) { + if (!$useStoreInUrl && !$this->storeManager->hasSingleStore()) { $this->queryParamsResolver->setQueryParam('___store', $storeCode); } } diff --git a/app/code/Magento/Store/ViewModel/SwitcherUrlProvider.php b/app/code/Magento/Store/ViewModel/SwitcherUrlProvider.php new file mode 100644 index 000000000000..b0d08f064073 --- /dev/null +++ b/app/code/Magento/Store/ViewModel/SwitcherUrlProvider.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Store\ViewModel; + +use Magento\Framework\App\ActionInterface; +use Magento\Framework\Url\EncoderInterface; +use Magento\Framework\UrlInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Provides target store redirect url. + */ +class SwitcherUrlProvider implements \Magento\Framework\View\Element\Block\ArgumentInterface +{ + /** + * @var EncoderInterface + */ + private $encoder; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var UrlInterface + */ + private $urlBuilder; + + /** + * @param EncoderInterface $encoder + * @param StoreManagerInterface $storeManager + * @param UrlInterface $urlBuilder + */ + public function __construct( + EncoderInterface $encoder, + StoreManagerInterface $storeManager, + UrlInterface $urlBuilder + ) { + $this->encoder = $encoder; + $this->storeManager = $storeManager; + $this->urlBuilder = $urlBuilder; + } + + /** + * Returns target store redirect url. + * + * @param Store $store + * @return string + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function getTargetStoreRedirectUrl(Store $store): string + { + return $this->urlBuilder->getUrl( + 'stores/store/redirect', + [ + '___store' => $store->getCode(), + '___from_store' => $this->storeManager->getStore()->getCode(), + ActionInterface::PARAM_NAME_URL_ENCODED => $this->encoder->encode( + $store->getCurrentUrl(false) + ), + ] + ); + } +} diff --git a/app/code/Magento/Store/etc/config.xml b/app/code/Magento/Store/etc/config.xml index d69fcad7aac6..b9e7ac1c6aca 100644 --- a/app/code/Magento/Store/etc/config.xml +++ b/app/code/Magento/Store/etc/config.xml @@ -130,6 +130,8 @@ <html>html</html> <phtml>phtml</phtml> <shtml>shtml</shtml> + <phpt>phpt</phpt> + <pht>pht</pht> </protected_extensions> <public_files_valid_paths> <protected> diff --git a/app/code/Magento/Store/etc/db_schema.xml b/app/code/Magento/Store/etc/db_schema.xml index 3c2bc3e3c117..6eea94b8deec 100644 --- a/app/code/Magento/Store/etc/db_schema.xml +++ b/app/code/Magento/Store/etc/db_schema.xml @@ -18,16 +18,16 @@ identity="false" default="0" comment="Default Group Id"/> <column xsi:type="smallint" name="is_default" padding="5" unsigned="true" nullable="true" identity="false" default="0" comment="Defines Is Website Default"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="website_id"/> </constraint> - <constraint xsi:type="unique" name="STORE_WEBSITE_CODE"> + <constraint xsi:type="unique" referenceId="STORE_WEBSITE_CODE"> <column name="code"/> </constraint> - <index name="STORE_WEBSITE_SORT_ORDER" indexType="btree"> + <index referenceId="STORE_WEBSITE_SORT_ORDER" indexType="btree"> <column name="sort_order"/> </index> - <index name="STORE_WEBSITE_DEFAULT_GROUP_ID" indexType="btree"> + <index referenceId="STORE_WEBSITE_DEFAULT_GROUP_ID" indexType="btree"> <column name="default_group_id"/> </index> </table> @@ -42,18 +42,18 @@ <column xsi:type="smallint" name="default_store_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Default Store Id"/> <column xsi:type="varchar" name="code" nullable="true" length="32" comment="Store group unique code"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="group_id"/> </constraint> - <constraint xsi:type="foreign" name="STORE_GROUP_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID" table="store_group" + <constraint xsi:type="foreign" referenceId="STORE_GROUP_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID" table="store_group" column="website_id" referenceTable="store_website" referenceColumn="website_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="STORE_GROUP_CODE"> + <constraint xsi:type="unique" referenceId="STORE_GROUP_CODE"> <column name="code"/> </constraint> - <index name="STORE_GROUP_WEBSITE_ID" indexType="btree"> + <index referenceId="STORE_GROUP_WEBSITE_ID" indexType="btree"> <column name="website_id"/> </index> - <index name="STORE_GROUP_DEFAULT_STORE_ID" indexType="btree"> + <index referenceId="STORE_GROUP_DEFAULT_STORE_ID" indexType="btree"> <column name="default_store_id"/> </index> </table> @@ -70,24 +70,24 @@ default="0" comment="Store Sort Order"/> <column xsi:type="smallint" name="is_active" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Store Activity"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="store_id"/> </constraint> - <constraint xsi:type="foreign" name="STORE_GROUP_ID_STORE_GROUP_GROUP_ID" table="store" column="group_id" + <constraint xsi:type="foreign" referenceId="STORE_GROUP_ID_STORE_GROUP_GROUP_ID" table="store" column="group_id" referenceTable="store_group" referenceColumn="group_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="STORE_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID" table="store" + <constraint xsi:type="foreign" referenceId="STORE_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID" table="store" column="website_id" referenceTable="store_website" referenceColumn="website_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="STORE_CODE"> + <constraint xsi:type="unique" referenceId="STORE_CODE"> <column name="code"/> </constraint> - <index name="STORE_WEBSITE_ID" indexType="btree"> + <index referenceId="STORE_WEBSITE_ID" indexType="btree"> <column name="website_id"/> </index> - <index name="STORE_IS_ACTIVE_SORT_ORDER" indexType="btree"> + <index referenceId="STORE_IS_ACTIVE_SORT_ORDER" indexType="btree"> <column name="is_active"/> <column name="sort_order"/> </index> - <index name="STORE_GROUP_ID" indexType="btree"> + <index referenceId="STORE_GROUP_ID" indexType="btree"> <column name="group_id"/> </index> </table> diff --git a/app/code/Magento/Store/etc/di.xml b/app/code/Magento/Store/etc/di.xml index 3d740bfee209..defe0694d018 100644 --- a/app/code/Magento/Store/etc/di.xml +++ b/app/code/Magento/Store/etc/di.xml @@ -102,11 +102,15 @@ </type> <type name="Magento\Store\Model\StoreResolver"> <arguments> - <argument name="cache" xsi:type="object">Magento\Framework\App\Cache\Type\Config</argument> <argument name="runMode" xsi:type="init_parameter">Magento\Store\Model\StoreManager::PARAM_RUN_TYPE</argument> <argument name="scopeCode" xsi:type="init_parameter">Magento\Store\Model\StoreManager::PARAM_RUN_CODE</argument> </arguments> </type> + <type name="Magento\Store\Model\StoresData"> + <arguments> + <argument name="cache" xsi:type="object">Magento\Framework\App\Cache\Type\Config</argument> + </arguments> + </type> <type name="Magento\Store\App\FrontController\Plugin\DefaultStore"> <arguments> <argument name="runMode" xsi:type="init_parameter">Magento\Store\Model\StoreManager::PARAM_RUN_TYPE</argument> @@ -232,6 +236,7 @@ <type name="Magento\Framework\App\ScopeResolverPool"> <arguments> <argument name="scopeResolvers" xsi:type="array"> + <item name="default" xsi:type="object">Magento\Framework\App\ScopeResolver</item> <item name="store" xsi:type="object">Magento\Store\Model\Resolver\Store</item> <item name="stores" xsi:type="object">Magento\Store\Model\Resolver\Store</item> <item name="group" xsi:type="object">Magento\Store\Model\Resolver\Group</item> diff --git a/app/code/Magento/Store/etc/frontend/di.xml b/app/code/Magento/Store/etc/frontend/di.xml index c39d5df86393..917aedad3d96 100644 --- a/app/code/Magento/Store/etc/frontend/di.xml +++ b/app/code/Magento/Store/etc/frontend/di.xml @@ -9,7 +9,7 @@ <type name="Magento\Framework\App\FrontController"> <plugin name="requestPreprocessor" type="Magento\Store\App\FrontController\Plugin\RequestPreprocessor" sortOrder="50"/> </type> - <type name="Magento\Framework\App\Action\Action"> + <type name="Magento\Framework\App\Action\AbstractAction"> <plugin name="contextPlugin" type="Magento\Store\App\Action\Plugin\Context" sortOrder="10"/> </type> <type name="Magento\Framework\App\RouterList" shared="true"> diff --git a/app/code/Magento/Store/view/frontend/templates/switch/languages.phtml b/app/code/Magento/Store/view/frontend/templates/switch/languages.phtml index 80152dbb9a08..a620c2ce7140 100644 --- a/app/code/Magento/Store/view/frontend/templates/switch/languages.phtml +++ b/app/code/Magento/Store/view/frontend/templates/switch/languages.phtml @@ -27,7 +27,7 @@ <?php foreach ($block->getStores() as $_lang): ?> <?php if ($_lang->getId() != $block->getCurrentStoreId()): ?> <li class="view-<?= $block->escapeHtml($_lang->getCode()) ?> switcher-option"> - <a href="#" data-post='<?= /* @noEscape */ $block->getTargetStorePostData($_lang) ?>'> + <a href="<?= $block->escapeUrl($block->getViewModel()->getTargetStoreRedirectUrl($_lang)) ?>"> <?= $block->escapeHtml($_lang->getName()) ?> </a> </li> diff --git a/app/code/Magento/StoreGraphQl/Controller/HttpHeaderProcessor/StoreProcessor.php b/app/code/Magento/StoreGraphQl/Controller/HttpHeaderProcessor/StoreProcessor.php new file mode 100644 index 000000000000..7999a96917cd --- /dev/null +++ b/app/code/Magento/StoreGraphQl/Controller/HttpHeaderProcessor/StoreProcessor.php @@ -0,0 +1,98 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\StoreGraphQl\Controller\HttpHeaderProcessor; + +use Magento\GraphQl\Controller\HttpHeaderProcessorInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\App\Http\Context as HttpContext; +use Magento\Store\Api\StoreCookieManagerInterface; + +/** + * Process the "Store" header entry + */ +class StoreProcessor implements HttpHeaderProcessorInterface +{ + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var HttpContext + */ + private $httpContext; + + /** + * @var StoreCookieManagerInterface + */ + private $storeCookieManager; + + /** + * @param StoreManagerInterface $storeManager + * @param HttpContext $httpContext + * @param StoreCookieManagerInterface $storeCookieManager + */ + public function __construct( + StoreManagerInterface $storeManager, + HttpContext $httpContext, + StoreCookieManagerInterface $storeCookieManager + ) { + $this->storeManager = $storeManager; + $this->httpContext = $httpContext; + $this->storeCookieManager = $storeCookieManager; + } + + /** + * Handle the value of the store and set the scope + * + * @see \Magento\Store\App\Action\Plugin\Context::beforeDispatch + * + * @param string $headerValue + * @return void + */ + public function processHeaderValue(string $headerValue) : void + { + if (!empty($headerValue)) { + $storeCode = ltrim(rtrim($headerValue)); + $this->storeManager->setCurrentStore($storeCode); + $this->updateContext($storeCode); + } elseif (!$this->isAlreadySet()) { + $storeCode = $this->storeCookieManager->getStoreCodeFromCookie() + ?: $this->storeManager->getDefaultStoreView()->getCode(); + $this->storeManager->setCurrentStore($storeCode); + $this->updateContext($storeCode); + } + } + + /** + * Update context accordingly to the store code found. + * + * @param string $storeCode + * @return void + */ + private function updateContext(string $storeCode) : void + { + $this->httpContext->setValue( + StoreManagerInterface::CONTEXT_STORE, + $storeCode, + $this->storeManager->getDefaultStoreView()->getCode() + ); + } + + /** + * Check if there is a need to find the current store. + * + * @return bool + */ + private function isAlreadySet(): bool + { + $storeKey = StoreManagerInterface::CONTEXT_STORE; + + return $this->httpContext->getValue($storeKey) !== null; + } +} diff --git a/app/code/Magento/StoreGraphQl/Controller/HttpRequestValidator/StoreValidator.php b/app/code/Magento/StoreGraphQl/Controller/HttpRequestValidator/StoreValidator.php new file mode 100644 index 000000000000..144905d72814 --- /dev/null +++ b/app/code/Magento/StoreGraphQl/Controller/HttpRequestValidator/StoreValidator.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\StoreGraphQl\Controller\HttpRequestValidator; + +use Magento\Framework\App\HttpRequestInterface; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\GraphQl\Controller\HttpRequestValidatorInterface; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Validate the "Store" header entry + */ +class StoreValidator implements HttpRequestValidatorInterface +{ + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @param StoreManagerInterface $storeManager + */ + public function __construct( + StoreManagerInterface $storeManager + ) { + $this->storeManager = $storeManager; + } + + /** + * Validate the header 'Store' value. + * + * @param HttpRequestInterface $request + * @return void + * @throws GraphQlInputException + */ + public function validate(HttpRequestInterface $request): void + { + $headerValue = $request->getHeader('Store'); + if (!empty($headerValue)) { + $storeCode = ltrim(rtrim($headerValue)); + $stores = $this->storeManager->getStores(false, true); + if (!isset($stores[$storeCode])) { + if (strtolower($storeCode) !== 'default') { + $this->storeManager->setCurrentStore(null); + throw new GraphQlInputException( + __("Requested store is not found") + ); + } + } + } + } +} diff --git a/app/code/Magento/StoreGraphQl/Model/Resolver/Store/StoreConfigDataProvider.php b/app/code/Magento/StoreGraphQl/Model/Resolver/Store/StoreConfigDataProvider.php index 0252d7898bee..8c2d6c36591d 100644 --- a/app/code/Magento/StoreGraphQl/Model/Resolver/Store/StoreConfigDataProvider.php +++ b/app/code/Magento/StoreGraphQl/Model/Resolver/Store/StoreConfigDataProvider.php @@ -7,10 +7,10 @@ namespace Magento\StoreGraphQl\Model\Resolver\Store; -use Magento\Store\Api\Data\StoreConfigInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Store\Api\StoreConfigManagerInterface; -use Magento\Store\Api\StoreRepositoryInterface; -use Magento\Store\Api\StoreResolverInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; /** * StoreConfig field data provider, used for GraphQL request processing. @@ -23,53 +23,62 @@ class StoreConfigDataProvider private $storeConfigManager; /** - * @var StoreResolverInterface + * @var StoreManagerInterface */ - private $storeResolver; + private $storeManager; /** - * @var StoreRepositoryInterface + * @var ScopeConfigInterface */ - private $storeRepository; + private $scopeConfig; + + /** + * @var array + */ + private $extendedConfigData; /** * @param StoreConfigManagerInterface $storeConfigManager - * @param StoreResolverInterface $storeResolver - * @param StoreRepositoryInterface $storeRepository + * @param StoreManagerInterface $storeManager + * @param ScopeConfigInterface $scopeConfig + * @param array $extendedConfigData */ public function __construct( StoreConfigManagerInterface $storeConfigManager, - StoreResolverInterface $storeResolver, - StoreRepositoryInterface $storeRepository + StoreManagerInterface $storeManager, + ScopeConfigInterface $scopeConfig, + array $extendedConfigData = [] ) { $this->storeConfigManager = $storeConfigManager; - $this->storeResolver = $storeResolver; - $this->storeRepository = $storeRepository; + $this->storeManager = $storeManager; + $this->scopeConfig = $scopeConfig; + $this->extendedConfigData = $extendedConfigData; } /** - * Get store config for current store + * Get store config data * * @return array */ - public function getStoreConfig() : array + public function getStoreConfigData(): array { - $storeId = $this->storeResolver->getCurrentStoreId(); - $store = $this->storeRepository->getById($storeId); - $storeConfig = current($this->storeConfigManager->getStoreConfigs([$store->getCode()])); - - return $this->hidrateStoreConfig($storeConfig); + $storeConfigData = array_merge( + $this->getBaseConfigData(), + $this->getExtendedConfigData() + ); + return $storeConfigData; } /** - * Transform StoreConfig object to in array format + * Get base config data * - * @param StoreConfigInterface $storeConfig * @return array */ - private function hidrateStoreConfig($storeConfig): array + private function getBaseConfigData() : array { - /** @var StoreConfigInterface $storeConfig */ + $store = $this->storeManager->getStore(); + $storeConfig = current($this->storeConfigManager->getStoreConfigs([$store->getCode()])); + $storeConfigData = [ 'id' => $storeConfig->getId(), 'code' => $storeConfig->getCode(), @@ -88,7 +97,25 @@ private function hidrateStoreConfig($storeConfig): array 'secure_base_static_url' => $storeConfig->getSecureBaseStaticUrl(), 'secure_base_media_url' => $storeConfig->getSecureBaseMediaUrl() ]; - return $storeConfigData; } + + /** + * Get extended config data + * + * @return array + */ + private function getExtendedConfigData() + { + $store = $this->storeManager->getStore(); + $extendedConfigData = []; + foreach ($this->extendedConfigData as $key => $path) { + $extendedConfigData[$key] = $this->scopeConfig->getValue( + $path, + ScopeInterface::SCOPE_STORE, + $store->getId() + ); + } + return $extendedConfigData; + } } diff --git a/app/code/Magento/StoreGraphQl/Model/Resolver/StoreConfigResolver.php b/app/code/Magento/StoreGraphQl/Model/Resolver/StoreConfigResolver.php index 8e5bf0120b8b..9c426172de85 100644 --- a/app/code/Magento/StoreGraphQl/Model/Resolver/StoreConfigResolver.php +++ b/app/code/Magento/StoreGraphQl/Model/Resolver/StoreConfigResolver.php @@ -8,8 +8,6 @@ namespace Magento\StoreGraphQl\Model\Resolver; use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Query\Resolver\Value; -use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\StoreGraphQl\Model\Resolver\Store\StoreConfigDataProvider; @@ -24,25 +22,17 @@ class StoreConfigResolver implements ResolverInterface */ private $storeConfigDataProvider; - /** - * @var ValueFactory - */ - private $valueFactory; - /** * @param StoreConfigDataProvider $storeConfigsDataProvider - * @param ValueFactory $valueFactory */ public function __construct( - StoreConfigDataProvider $storeConfigsDataProvider, - ValueFactory $valueFactory + StoreConfigDataProvider $storeConfigsDataProvider ) { - $this->valueFactory = $valueFactory; $this->storeConfigDataProvider = $storeConfigsDataProvider; } /** - * {@inheritdoc} + * @inheritdoc */ public function resolve( Field $field, @@ -50,14 +40,7 @@ public function resolve( ResolveInfo $info, array $value = null, array $args = null - ) : Value { - - $storeConfigData = $this->storeConfigDataProvider->getStoreConfig(); - - $result = function () use ($storeConfigData) { - return !empty($storeConfigData) ? $storeConfigData : []; - }; - - return $this->valueFactory->create($result); + ) { + return $this->storeConfigDataProvider->getStoreConfigData(); } } diff --git a/app/code/Magento/StoreGraphQl/composer.json b/app/code/Magento/StoreGraphQl/composer.json index d03d759babd2..aa36a4891343 100644 --- a/app/code/Magento/StoreGraphQl/composer.json +++ b/app/code/Magento/StoreGraphQl/composer.json @@ -5,11 +5,8 @@ "require": { "php": "~7.1.3||~7.2.0", "magento/framework": "*", - "magento/module-store": "*" - }, - "suggest": { - "magento/module-graph-ql": "*", - "magento/module-catalog-graph-ql": "*" + "magento/module-store": "*", + "magento/module-graph-ql": "*" }, "license": [ "OSL-3.0", diff --git a/app/code/Magento/StoreGraphQl/etc/graphql/di.xml b/app/code/Magento/StoreGraphQl/etc/graphql/di.xml new file mode 100644 index 000000000000..c2191164287f --- /dev/null +++ b/app/code/Magento/StoreGraphQl/etc/graphql/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\GraphQl\Controller\HttpRequestProcessor"> + <arguments> + <argument name="graphQlHeaders" xsi:type="array"> + <item name="Store" xsi:type="object">Magento\StoreGraphQl\Controller\HttpHeaderProcessor\StoreProcessor</item> + </argument> + <argument name="requestValidators" xsi:type="array"> + <item name="storeValidator" xsi:type="object">Magento\StoreGraphQl\Controller\HttpRequestValidator\StoreValidator</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/StoreGraphQl/etc/module.xml b/app/code/Magento/StoreGraphQl/etc/module.xml index f53379ac3bbf..bbec6a85a1a1 100644 --- a/app/code/Magento/StoreGraphQl/etc/module.xml +++ b/app/code/Magento/StoreGraphQl/etc/module.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> <module name="Magento_StoreGraphQl"> <sequence> + <module name="Magento_Store"/> <module name="Magento_GraphQl"/> </sequence> </module> diff --git a/app/code/Magento/StoreGraphQl/etc/schema.graphqls b/app/code/Magento/StoreGraphQl/etc/schema.graphqls index af79d0e3e28b..376635e5c8f7 100644 --- a/app/code/Magento/StoreGraphQl/etc/schema.graphqls +++ b/app/code/Magento/StoreGraphQl/etc/schema.graphqls @@ -1,15 +1,15 @@ # Copyright © Magento, Inc. All rights reserved. # See COPYING.txt for license details. type Query { - storeConfig : StoreConfig @resolver(class: "Magento\\StoreGraphQl\\Model\\Resolver\\StoreConfigResolver") @doc(description: "The store config query") + storeConfig : StoreConfig @resolver(class: "Magento\\StoreGraphQl\\Model\\Resolver\\StoreConfigResolver") @doc(description: "The store config query") @cache(cacheable: false) } type Website @doc(description: "The type contains information about a website") { id : Int @doc(description: "The ID number assigned to the website") - name : String @doc(description: "The website name. Websites use this name to identify it easyer.") + name : String @doc(description: "The website name. Websites use this name to identify it easier.") code : String @doc(description: "A code assigned to the website to identify it") sort_order : Int @doc(description: "The attribute to use for sorting websites") - default_group_id : String @doc(description: "The default group id that the website has") + default_group_id : String @doc(description: "The default group ID that the website has") is_default : Boolean @doc(description: "Specifies if this is the default website") } diff --git a/app/code/Magento/Swagger/view/frontend/templates/swagger-ui/index.phtml b/app/code/Magento/Swagger/view/frontend/templates/swagger-ui/index.phtml index b20da6873457..26ef4847a126 100644 --- a/app/code/Magento/Swagger/view/frontend/templates/swagger-ui/index.phtml +++ b/app/code/Magento/Swagger/view/frontend/templates/swagger-ui/index.phtml @@ -58,7 +58,7 @@ $schemaUrl = $block->getSchemaUrl(); <div class="swagger-ui-wrap"> <a id="logo" href="http://swagger.io">swagger</a> <form id='api_selector'> - <input id="input_baseUrl" type="hidden" value="<?= /* @escapeNotVerified */ $schemaUrl ?>"/> + <input id="input_baseUrl" type="hidden" value="<?= $block->escapeUrl($schemaUrl) ?>"/> <div class='input'><input placeholder="api_key" id="input_apiKey" name="apiKey" type="text"/></div> <div class='input'><a id="explore" href="#" data-sw-translate>apply</a></div> </form> diff --git a/app/code/Magento/Swagger/view/frontend/web/swagger-ui/js/swagger-ui-bundle.min.js b/app/code/Magento/Swagger/view/frontend/web/swagger-ui/js/swagger-ui-bundle.min.js index 6d9e786f707e..453934ea5362 100644 --- a/app/code/Magento/Swagger/view/frontend/web/swagger-ui/js/swagger-ui-bundle.min.js +++ b/app/code/Magento/Swagger/view/frontend/web/swagger-ui/js/swagger-ui-bundle.min.js @@ -96,4 +96,4 @@ if(String.fromCodePoint)e.exports=function(e){try{return String.fromCodePoint(e) * Licensed under the MIT License. */ var i,o="";e.exports=r},function(e,t,n){"use strict";e.exports=function(e,t){if(t=t.split(":")[0],!(e=+e))return!1;switch(t){case"http":case"ws":return 80!==e;case"https":case"wss":return 443!==e;case"ftp":return 21!==e;case"gopher":return 70!==e;case"file":return!1}return 0!==e}},function(e,t,n){"use strict";function r(e,t){e&&Object.keys(e).forEach(function(n){t(e[n],n)})}function i(e,t){return{}.hasOwnProperty.call(e,t)}function o(e,t){var n=[];return r(e,function(e){t(e)&&n.push(e)}),n}function a(e,t,n){function g(e,t){var n=this;this.tag=e,this.attribs=t||{},this.tagPosition=E.length,this.text="",this.updateParentNodeText=function(){if(P.length){P[P.length-1].text+=n.text}}}function y(e){return"string"!=typeof e&&(e+=""),e.replace(/\&/g,"&").replace(/</g,"<").replace(/\>/g,">").replace(/\"/g,""")}function _(e,n){n=n.replace(/[\x00-\x20]+/g,""),n=n.replace(/<\!\-\-.*?\-\-\>/g,"");var r=n.match(/^([a-zA-Z]+)\:/);if(!r)return!!n.match(/^[\/\\]{2}/)&&!t.allowProtocolRelative;var o=r[1].toLowerCase();return i(t.allowedSchemesByTag,e)?-1===t.allowedSchemesByTag[e].indexOf(o):!t.allowedSchemes||-1===t.allowedSchemes.indexOf(o)}function b(e,t){if(!t)return e;var n,r=c(e),i=e.nodes[0];return n=t[i.selector]&&t["*"]?p(c(t[i.selector]),t["*"],function(e,t){if(Array.isArray(e))return e.concat(t)}):t[i.selector]||t["*"],n&&(r.nodes[0].nodes=i.nodes.reduce(w(n),[])),r}function x(e){return e.nodes[0].nodes.reduce(function(e,t){return e.push(t.prop+":"+t.value+";"),e},[]).join("")}function w(e){return function(t,n){if(e.hasOwnProperty(n.prop)){e[n.prop].some(function(e){return e.test(n.value)})&&t.push(n)}return t}}function k(e,t){return t?(e=e.split(/\s+/),e.filter(function(e){return-1!==t.indexOf(e)}).join(" ")):e}var E="";t?(t=u(a.defaults,t),t.parser?t.parser=u(v,t.parser):t.parser=v):(t=a.defaults,t.parser=v);var S,C,A=t.nonTextTags||["script","style","textarea"];t.allowedAttributes&&(S={},C={},r(t.allowedAttributes,function(e,t){S[t]=[];var n=[];e.forEach(function(e){e.indexOf("*")>=0?n.push(l(e).replace(/\\\*/g,".*")):S[t].push(e)}),C[t]=new RegExp("^("+n.join("|")+")$")}));var D={};r(t.allowedClasses,function(e,t){S&&(i(S,t)||(S[t]=[]),S[t].push("class")),D[t]=e});var O,M={};r(t.transformTags,function(e,t){var n;"function"==typeof e?n=e:"string"==typeof e&&(n=a.simpleTransform(e)),"*"===t?O=n:M[t]=n});var T=0,P=[],I={},R={},j=!1,F=0,N=new s.Parser({onopentag:function(e,n){if(j)return void F++;var a=new g(e,n);P.push(a);var s,u=!1,l=!!a.text;i(M,e)&&(s=M[e](e,n),a.attribs=n=s.attribs,void 0!==s.text&&(a.innerText=s.text),e!==s.tagName&&(a.name=e=s.tagName,R[T]=s.tagName)),O&&(s=O(e,n),a.attribs=n=s.attribs,e!==s.tagName&&(a.name=e=s.tagName,R[T]=s.tagName)),t.allowedTags&&-1===t.allowedTags.indexOf(e)&&(u=!0,-1!==A.indexOf(e)&&(j=!0,F=1),I[T]=!0),T++,u||(E+="<"+e,(!S||i(S,e)||S["*"])&&r(n,function(n,s){if(!m.test(s))return void delete a.attribs[s];var u;if(!S||i(S,e)&&-1!==S[e].indexOf(s)||S["*"]&&-1!==S["*"].indexOf(s)||i(C,e)&&C[e].test(s)||C["*"]&&C["*"].test(s)){if(("href"===s||"src"===s)&&_(e,n))return void delete a.attribs[s];if("iframe"===e&&"src"===s){if("//"===n.substring(0,2)){n="https:".concat(n)}try{if(u=d.parse(n),t.allowedIframeHostnames){if(!t.allowedIframeHostnames.find(function(e){return e===u.hostname}))return void delete a.attribs[s]}}catch(e){return void delete a.attribs[s]}}if("srcset"===s)try{if(u=f.parse(n),r(u,function(e){_("srcset",e.url)&&(e.evil=!0)}),u=o(u,function(e){return!e.evil}),!u.length)return void delete a.attribs[s];n=f.stringify(o(u,function(e){return!e.evil})),a.attribs[s]=n}catch(e){return void delete a.attribs[s]}if("class"===s&&(n=k(n,D[e]),!n.length))return void delete a.attribs[s];if("style"===s)try{if(n=x(b(h.parse(e+" {"+n+"}"),t.allowedStyles)),0===n.length)return void delete a.attribs[s]}catch(e){return void delete a.attribs[s]}E+=" "+s,n.length&&(E+='="'+y(n)+'"')}else delete a.attribs[s]}),-1!==t.selfClosing.indexOf(e)?E+=" />":(E+=">",!a.innerText||l||t.textFilter||(E+=a.innerText)))},ontext:function(e){if(!j){var n,r=P[P.length-1];if(r&&(n=r.tag,e=void 0!==r.innerText?r.innerText:e),"script"===n||"style"===n)E+=e;else{var i=y(e);t.textFilter?E+=t.textFilter(i):E+=i}if(P.length){P[P.length-1].text+=e}}},onclosetag:function(e){if(j){if(--F)return;j=!1}var n=P.pop();if(n){if(j=!1,T--,I[T])return delete I[T],void n.updateParentNodeText();if(R[T]&&(e=R[T],delete R[T]),t.exclusiveFilter&&t.exclusiveFilter(n))return void(E=E.substr(0,n.tagPosition));n.updateParentNodeText(),-1===t.selfClosing.indexOf(e)&&(E+="</"+e+">")}}},t.parser);return N.write(e),N.end(),E}var s=n(116),u=n(1200),l=n(825),c=n(824),p=n(827),f=n(1181),h=n(982),d=n(497);e.exports=a;var m=/^[^\0\t\n\f\r \/<=>]+$/,v={decodeEntities:!0};a.defaults={allowedTags:["h3","h4","h5","h6","blockquote","p","a","ul","ol","nl","li","b","i","strong","em","strike","code","hr","br","div","table","thead","caption","tbody","tr","th","td","pre","iframe"],allowedAttributes:{a:["href","name","target"],img:["src"]},selfClosing:["img","br","hr","area","base","basefont","input","link","meta"],allowedSchemes:["http","https","ftp","mailto"],allowedSchemesByTag:{},allowProtocolRelative:!0},a.simpleTransform=function(e,t,n){return n=void 0===n||n,t=t||{},function(r,i){var o;if(n)for(o in t)i[o]=t[o];else i=t;return{tagName:e,attribs:i}}}},function(e,t,n){(function(e,t){!function(e,n){"use strict";function r(e){"function"!=typeof e&&(e=new Function(""+e));for(var t=new Array(arguments.length-1),n=0;n<t.length;n++)t[n]=arguments[n+1];var r={callback:e,args:t};return l[u]=r,s(u),u++}function i(e){delete l[e]}function o(e){var t=e.callback,r=e.args;switch(r.length){case 0:t();break;case 1:t(r[0]);break;case 2:t(r[0],r[1]);break;case 3:t(r[0],r[1],r[2]);break;default:t.apply(n,r)}}function a(e){if(c)setTimeout(a,0,e);else{var t=l[e];if(t){c=!0;try{o(t)}finally{i(e),c=!1}}}}if(!e.setImmediate){var s,u=1,l={},c=!1,p=e.document,f=Object.getPrototypeOf&&Object.getPrototypeOf(e);f=f&&f.setTimeout?f:e,"[object process]"==={}.toString.call(e.process)?function(){s=function(e){t.nextTick(function(){a(e)})}}():function(){if(e.postMessage&&!e.importScripts){var t=!0,n=e.onmessage;return e.onmessage=function(){t=!1},e.postMessage("","*"),e.onmessage=n,t}}()?function(){var t="setImmediate$"+Math.random()+"$",n=function(n){n.source===e&&"string"==typeof n.data&&0===n.data.indexOf(t)&&a(+n.data.slice(t.length))};e.addEventListener?e.addEventListener("message",n,!1):e.attachEvent("onmessage",n),s=function(n){e.postMessage(t+n,"*")}}():e.MessageChannel?function(){var e=new MessageChannel;e.port1.onmessage=function(e){a(e.data)},s=function(t){e.port2.postMessage(t)}}():p&&"onreadystatechange"in p.createElement("script")?function(){var e=p.documentElement;s=function(t){var n=p.createElement("script");n.onreadystatechange=function(){a(t),n.onreadystatechange=null,e.removeChild(n),n=null},e.appendChild(n)}}():function(){s=function(e){setTimeout(a,0,e)}}(),f.setImmediate=r,f.clearImmediate=i}}("undefined"==typeof self?void 0===e?this:e:self)}).call(t,n(17),n(33))},function(e,t,n){"use strict";function r(e){return e.sort().filter(function(t,n){return JSON.stringify(t)!==JSON.stringify(e[n-1])})}var i=n(978),o=n(507),a=/^\d+$/;t.parse=function(e){return r(e.split(",").map(function(e){var t={};return e.trim().split(/\s+/).forEach(function(e,n){if(0===n)return t.url=e;var r=e.substring(0,e.length-1),o=e[e.length-1],s=parseInt(r,10),u=parseFloat(r);if("w"===o&&a.test(r))t.width=s;else if("h"===o&&a.test(r))t.height=s;else{if("x"!==o||i(u))throw new Error("Invalid srcset descriptor: "+e+".");t.density=u}}),t}))},t.stringify=function(e){return o(e.map(function(e){if(!e.url)throw new Error("URL is required.");var t=[e.url];return e.width&&t.push(e.width+"w"),e.height&&t.push(e.height+"h"),e.density&&t.push(e.density+"x"),t.join(" ")})).join(", ")}},function(e,t,n){e.exports=n(1183)},function(e,t,n){"use strict";(function(e,r){Object.defineProperty(t,"__esModule",{value:!0});var i,o=n(1184),a=function(e){return e&&e.__esModule?e:{default:e}}(o);i="undefined"!=typeof self?self:"undefined"!=typeof window?window:void 0!==e?e:r;var s=(0,a.default)(i);t.default=s}).call(t,n(17),n(72)(e))},function(e,t,n){"use strict";function r(e){var t,n=e.Symbol;return"function"==typeof n?n.observable?t=n.observable:(t=n("observable"),n.observable=t):t="@@observable",t}Object.defineProperty(t,"__esModule",{value:!0}),t.default=r},function(e,t,n){"use strict";e.exports=2147483647},function(e,t,n){"use strict";var r=n(65),i=n(1185);e.exports=function(e){if((e=r(e))>i)throw new TypeError(e+" exceeds maximum possible timeout");return e}},function(e,t,n){"use strict";(function(t){function r(e){e=e||t.location||{};var n,r={},i=typeof e;if("blob:"===e.protocol)r=new a(unescape(e.pathname),{});else if("string"===i){r=new a(e,{});for(n in d)delete r[n]}else if("object"===i){for(n in e)n in d||(r[n]=e[n]);void 0===r.slashes&&(r.slashes=f.test(e.href))}return r}function i(e){var t=p.exec(e);return{protocol:t[1]?t[1].toLowerCase():"",slashes:!!t[2],rest:t[3]}}function o(e,t){for(var n=(t||"/").split("/").slice(0,-1).concat(e.split("/")),r=n.length,i=n[r-1],o=!1,a=0;r--;)"."===n[r]?n.splice(r,1):".."===n[r]?(n.splice(r,1),a++):a&&(0===r&&(o=!0),n.splice(r,1),a--);return o&&n.unshift(""),"."!==i&&".."!==i||n.push(""),n.join("/")}function a(e,t,n){if(!(this instanceof a))return new a(e,t,n);var s,u,p,f,d,m,v=h.slice(),g=typeof t,y=this,_=0;for("object"!==g&&"string"!==g&&(n=t,t=null),n&&"function"!=typeof n&&(n=c.parse),t=r(t),u=i(e||""),s=!u.protocol&&!u.slashes,y.slashes=u.slashes||s&&t.slashes,y.protocol=u.protocol||t.protocol||"",e=u.rest,u.slashes||(v[2]=[/(.*)/,"pathname"]);_<v.length;_++)f=v[_],p=f[0],m=f[1],p!==p?y[m]=e:"string"==typeof p?~(d=e.indexOf(p))&&("number"==typeof f[2]?(y[m]=e.slice(0,d),e=e.slice(d+f[2])):(y[m]=e.slice(d),e=e.slice(0,d))):(d=p.exec(e))&&(y[m]=d[1],e=e.slice(0,d.index)),y[m]=y[m]||(s&&f[3]?t[m]||"":""),f[4]&&(y[m]=y[m].toLowerCase());n&&(y.query=n(y.query)),s&&t.slashes&&"/"!==y.pathname.charAt(0)&&(""!==y.pathname||""!==t.pathname)&&(y.pathname=o(y.pathname,t.pathname)),l(y.port,y.protocol)||(y.host=y.hostname,y.port=""),y.username=y.password="",y.auth&&(f=y.auth.split(":"),y.username=f[0]||"",y.password=f[1]||""),y.origin=y.protocol&&y.host&&"file:"!==y.protocol?y.protocol+"//"+y.host:"null",y.href=y.toString()}function s(e,t,n){var r=this;switch(e){case"query":"string"==typeof t&&t.length&&(t=(n||c.parse)(t)),r[e]=t;break;case"port":r[e]=t,l(t,r.protocol)?t&&(r.host=r.hostname+":"+t):(r.host=r.hostname,r[e]="");break;case"hostname":r[e]=t,r.port&&(t+=":"+r.port),r.host=t;break;case"host":r[e]=t,/:\d+$/.test(t)?(t=t.split(":"),r.port=t.pop(),r.hostname=t.join(":")):(r.hostname=t,r.port="");break;case"protocol":r.protocol=t.toLowerCase(),r.slashes=!n;break;case"pathname":case"hash":if(t){var i="pathname"===e?"/":"#";r[e]=t.charAt(0)!==i?i+t:t}else r[e]=t;break;default:r[e]=t}for(var o=0;o<h.length;o++){var a=h[o];a[4]&&(r[a[1]]=r[a[1]].toLowerCase())}return r.origin=r.protocol&&r.host&&"file:"!==r.protocol?r.protocol+"//"+r.host:"null",r.href=r.toString(),r}function u(e){e&&"function"==typeof e||(e=c.stringify);var t,n=this,r=n.protocol;r&&":"!==r.charAt(r.length-1)&&(r+=":");var i=r+(n.slashes?"//":"");return n.username&&(i+=n.username,n.password&&(i+=":"+n.password),i+="@"),i+=n.host+n.pathname,t="object"==typeof n.query?e(n.query):n.query,t&&(i+="?"!==t.charAt(0)?"?"+t:t),n.hash&&(i+=n.hash),i}var l=n(1178),c=n(1005),p=/^([a-z][a-z0-9.+-]*:)?(\/\/)?([\S\s]*)/i,f=/^[A-Za-z][A-Za-z0-9+-.]*:\/\//,h=[["#","hash"],["?","query"],["/","pathname"],["@","auth",1],[NaN,"host",void 0,1,1],[/:(\d+)$/,"port",void 0,1],[NaN,"hostname",void 0,1,1]],d={hash:1,query:1};a.prototype={set:s,toString:u},a.extractProtocol=i,a.location=r,a.qs=c,e.exports=a}).call(t,n(17))},function(e,t,n){"use strict";e.exports={isString:function(e){return"string"==typeof e},isObject:function(e){return"object"==typeof e&&null!==e},isNull:function(e){return null===e},isNullOrUndefined:function(e){return null==e}}},function(e,t){e.exports=function(e){for(var t=[],n=0;n<e.length;n++){var r=e.charCodeAt(n);if(r>=55296&&r<=56319&&n+1<e.length){var i=e.charCodeAt(n+1);if(i>=56320&&i<=57343){var o=1024*(r-55296)+i-56320+65536;t.push(240+Math.floor(o/64/64/64),128+Math.floor(o/64/64)%64,128+Math.floor(o/64)%64,128+o%64),n+=1;continue}}r>=2048?t.push(224+Math.floor(r/64/64),128+Math.floor(r/64)%64,128+r%64):r>=128?t.push(192+Math.floor(r/64),128+r%64):t.push(r)}return t}},function(e,t){!function(){function e(e,t){function n(e,t){return r(e,new RegExp(a.source,"g"),t)}function r(e,t,n){if(!i(e))return n;var r=0,o=0;do{var a=t.exec(e);if(null===a)break;if(!(o<n))break;r+=a[0].length,o++}while(null!==a);return r>=e.length?-1:r}function i(e){return s.test(e)}function o(e,n){void 0==e&&(e=["[^]"]),void 0==n&&(n="g");var r=[];return t.forEach(function(e){r.push(e.source)}),r.push(a.source),r=r.concat(e),new RegExp(r.join("|"),n)}e.findCharIndex=function(e,t){if(t>=e.length)return-1;if(!i(e))return t;for(var n=o(),r=0;null!==n.exec(e)&&!(n.lastIndex>t);)r++;return r},e.findByteIndex=function(e,t){return t>=this.length(e)?-1:r(e,o(),t)},e.charAt=function(e,t){var n=this.findByteIndex(e,t);if(n<0||n>=e.length)return"";var r=e.slice(n,n+8),i=s.exec(r);return null===i?r[0]:i[0]},e.charCodeAt=function(e,t){var r=n(e,t);if(r<0)return NaN;var i=e.charCodeAt(r);if(55296<=i&&i<=56319){return 1024*(i-55296)+(e.charCodeAt(r+1)-56320)+65536}return i},e.fromCharCode=function(e){return e>65535?(e-=65536,String.fromCharCode(55296+(e>>10),56320+(1023&e))):String.fromCharCode(e)},e.indexOf=function(e,t,n){void 0!==n&&null!==n||(n=0);var r=this.findByteIndex(e,n),i=e.indexOf(t,r);return i<0?-1:this.findCharIndex(e,i)},e.lastIndexOf=function(e,t,n){var r;if(void 0===n||null===n)r=e.lastIndexOf(t);else{var i=this.findByteIndex(e,n);r=e.lastIndexOf(t,i)}return r<0?-1:this.findCharIndex(e,r)},e.slice=function(e,t,n){var r,i=this.findByteIndex(e,t);return i<0&&(i=e.length),void 0===n||null===n?r=e.length:(r=this.findByteIndex(e,n))<0&&(r=e.length),e.slice(i,r)},e.substr=function(e,t,n){return t<0&&(t=this.length(e)+t),void 0===n||null===n?this.slice(e,t):this.slice(e,t,t+n)},e.substring=e.slice,e.length=function(e){return this.findCharIndex(e,e.length-1)+1},e.stringToCodePoints=function(e){for(var t=[],n=0;n<e.length&&(codePoint=this.charCodeAt(e,n),codePoint);n++)t.push(codePoint);return t},e.codePointsToString=function(e){for(var t=[],n=0;n<e.length;n++)t.push(this.fromCharCode(e[n]));return t.join("")},e.stringToBytes=function(e){for(var t=[],n=0;n<e.length;n++){for(var r=e.charCodeAt(n),i=[];r>0;)i.push(255&r),r>>=8;1==i.length&&i.push(0),t=t.concat(i.reverse())}return t},e.bytesToString=function(e){for(var t=[],n=0;n<e.length;n+=2){var r=e[n],i=e[n+1],o=r<<8|i;t.push(String.fromCharCode(o))}return t.join("")},e.stringToCharArray=function(e){var t=[],n=o();do{var r=n.exec(e);if(null===r)break;t.push(r[0])}while(null!==r);return t};var a=/[\uD800-\uDBFF][\uDC00-\uDFFF]/,s=o([],"")}var n;void 0!==t&&null!==t?n=t:"undefined"!=typeof window&&null!==window&&(void 0!==window.UtfString&&null!==window.UtfString||(window.UtfString={}),n=window.UtfString);var r=/\uD83C[\uDDE6-\uDDFF]\uD83C[\uDDE6-\uDDFF]/;n.visual={},e(n,[]),e(n.visual,[r])}()},function(e,t,n){(function(t){function n(e,t){function n(){if(!i){if(r("throwDeprecation"))throw new Error(t);r("traceDeprecation")?console.trace(t):console.warn(t),i=!0}return e.apply(this,arguments)}if(r("noDeprecation"))return e;var i=!1;return n}function r(e){try{if(!t.localStorage)return!1}catch(e){return!1}var n=t.localStorage[e];return null!=n&&"true"===String(n).toLowerCase()}e.exports=n}).call(t,n(17))},function(e,t){"function"==typeof Object.create?e.exports=function(e,t){e.super_=t,e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}})}:e.exports=function(e,t){e.super_=t;var n=function(){};n.prototype=t.prototype,e.prototype=new n,e.prototype.constructor=e}},function(e,t){e.exports=function(e){return e&&"object"==typeof e&&"function"==typeof e.copy&&"function"==typeof e.fill&&"function"==typeof e.readUInt8}},function(e,t,n){(function(e,r){function i(e,n){var r={seen:[],stylize:a};return arguments.length>=3&&(r.depth=arguments[2]),arguments.length>=4&&(r.colors=arguments[3]),m(n)?r.showHidden=n:n&&t._extend(r,n),x(r.showHidden)&&(r.showHidden=!1),x(r.depth)&&(r.depth=2),x(r.colors)&&(r.colors=!1),x(r.customInspect)&&(r.customInspect=!0),r.colors&&(r.stylize=o),u(r,e,r.depth)}function o(e,t){var n=i.styles[t];return n?"["+i.colors[n][0]+"m"+e+"["+i.colors[n][1]+"m":e}function a(e,t){return e}function s(e){var t={};return e.forEach(function(e,n){t[e]=!0}),t}function u(e,n,r){if(e.customInspect&&n&&C(n.inspect)&&n.inspect!==t.inspect&&(!n.constructor||n.constructor.prototype!==n)){var i=n.inspect(r,e);return _(i)||(i=u(e,i,r)),i}var o=l(e,n);if(o)return o;var a=Object.keys(n),m=s(a);if(e.showHidden&&(a=Object.getOwnPropertyNames(n)),S(n)&&(a.indexOf("message")>=0||a.indexOf("description")>=0))return c(n);if(0===a.length){if(C(n)){var v=n.name?": "+n.name:"";return e.stylize("[Function"+v+"]","special")}if(w(n))return e.stylize(RegExp.prototype.toString.call(n),"regexp");if(E(n))return e.stylize(Date.prototype.toString.call(n),"date");if(S(n))return c(n)}var g="",y=!1,b=["{","}"];if(d(n)&&(y=!0,b=["[","]"]),C(n)){g=" [Function"+(n.name?": "+n.name:"")+"]"}if(w(n)&&(g=" "+RegExp.prototype.toString.call(n)),E(n)&&(g=" "+Date.prototype.toUTCString.call(n)),S(n)&&(g=" "+c(n)),0===a.length&&(!y||0==n.length))return b[0]+g+b[1];if(r<0)return w(n)?e.stylize(RegExp.prototype.toString.call(n),"regexp"):e.stylize("[Object]","special");e.seen.push(n);var x;return x=y?p(e,n,r,m,a):a.map(function(t){return f(e,n,r,m,t,y)}),e.seen.pop(),h(x,g,b)}function l(e,t){if(x(t))return e.stylize("undefined","undefined");if(_(t)){var n="'"+JSON.stringify(t).replace(/^"|"$/g,"").replace(/'/g,"\\'").replace(/\\"/g,'"')+"'";return e.stylize(n,"string")}return y(t)?e.stylize(""+t,"number"):m(t)?e.stylize(""+t,"boolean"):v(t)?e.stylize("null","null"):void 0}function c(e){return"["+Error.prototype.toString.call(e)+"]"}function p(e,t,n,r,i){for(var o=[],a=0,s=t.length;a<s;++a)T(t,String(a))?o.push(f(e,t,n,r,String(a),!0)):o.push("");return i.forEach(function(i){i.match(/^\d+$/)||o.push(f(e,t,n,r,i,!0))}),o}function f(e,t,n,r,i,o){var a,s,l;if(l=Object.getOwnPropertyDescriptor(t,i)||{value:t[i]},l.get?s=l.set?e.stylize("[Getter/Setter]","special"):e.stylize("[Getter]","special"):l.set&&(s=e.stylize("[Setter]","special")),T(r,i)||(a="["+i+"]"),s||(e.seen.indexOf(l.value)<0?(s=v(n)?u(e,l.value,null):u(e,l.value,n-1),s.indexOf("\n")>-1&&(s=o?s.split("\n").map(function(e){return" "+e}).join("\n").substr(2):"\n"+s.split("\n").map(function(e){return" "+e}).join("\n"))):s=e.stylize("[Circular]","special")),x(a)){if(o&&i.match(/^\d+$/))return s;a=JSON.stringify(""+i),a.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)?(a=a.substr(1,a.length-2),a=e.stylize(a,"name")):(a=a.replace(/'/g,"\\'").replace(/\\"/g,'"').replace(/(^"|"$)/g,"'"),a=e.stylize(a,"string"))}return a+": "+s}function h(e,t,n){var r=0;return e.reduce(function(e,t){return r++,t.indexOf("\n")>=0&&r++,e+t.replace(/\u001b\[\d\d?m/g,"").length+1},0)>60?n[0]+(""===t?"":t+"\n ")+" "+e.join(",\n ")+" "+n[1]:n[0]+t+" "+e.join(", ")+" "+n[1]}function d(e){return Array.isArray(e)}function m(e){return"boolean"==typeof e}function v(e){return null===e}function g(e){return null==e}function y(e){return"number"==typeof e}function _(e){return"string"==typeof e}function b(e){return"symbol"==typeof e}function x(e){return void 0===e}function w(e){return k(e)&&"[object RegExp]"===D(e)}function k(e){return"object"==typeof e&&null!==e}function E(e){return k(e)&&"[object Date]"===D(e)}function S(e){return k(e)&&("[object Error]"===D(e)||e instanceof Error)}function C(e){return"function"==typeof e}function A(e){return null===e||"boolean"==typeof e||"number"==typeof e||"string"==typeof e||"symbol"==typeof e||void 0===e}function D(e){return Object.prototype.toString.call(e)}function O(e){return e<10?"0"+e.toString(10):e.toString(10)}function M(){var e=new Date,t=[O(e.getHours()),O(e.getMinutes()),O(e.getSeconds())].join(":");return[e.getDate(),j[e.getMonth()],t].join(" ")}function T(e,t){return Object.prototype.hasOwnProperty.call(e,t)}var P=/%[sdj%]/g;t.format=function(e){if(!_(e)){for(var t=[],n=0;n<arguments.length;n++)t.push(i(arguments[n]));return t.join(" ")}for(var n=1,r=arguments,o=r.length,a=String(e).replace(P,function(e){if("%%"===e)return"%";if(n>=o)return e;switch(e){case"%s":return String(r[n++]);case"%d":return Number(r[n++]);case"%j":try{return JSON.stringify(r[n++])}catch(e){return"[Circular]"}default:return e}}),s=r[n];n<o;s=r[++n])v(s)||!k(s)?a+=" "+s:a+=" "+i(s);return a},t.deprecate=function(n,i){function o(){if(!a){if(r.throwDeprecation)throw new Error(i);r.traceDeprecation?console.trace(i):console.error(i),a=!0}return n.apply(this,arguments)}if(x(e.process))return function(){return t.deprecate(n,i).apply(this,arguments)};if(!0===r.noDeprecation)return n;var a=!1;return o};var I,R={};t.debuglog=function(e){if(x(I)&&(I=n.i({NODE_ENV:"production",WEBPACK_INLINE_STYLES:!1}).NODE_DEBUG||""),e=e.toUpperCase(),!R[e])if(new RegExp("\\b"+e+"\\b","i").test(I)){var i=r.pid;R[e]=function(){var n=t.format.apply(t,arguments);console.error("%s %d: %s",e,i,n)}}else R[e]=function(){};return R[e]},t.inspect=i,i.colors={bold:[1,22],italic:[3,23],underline:[4,24],inverse:[7,27],white:[37,39],grey:[90,39],black:[30,39],blue:[34,39],cyan:[36,39],green:[32,39],magenta:[35,39],red:[31,39],yellow:[33,39]},i.styles={special:"cyan",number:"yellow",boolean:"yellow",undefined:"grey",null:"bold",string:"green",date:"magenta",regexp:"red"},t.isArray=d,t.isBoolean=m,t.isNull=v,t.isNullOrUndefined=g,t.isNumber=y,t.isString=_,t.isSymbol=b,t.isUndefined=x,t.isRegExp=w,t.isObject=k,t.isDate=E,t.isError=S,t.isFunction=C,t.isPrimitive=A,t.isBuffer=n(1193);var j=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];t.log=function(){console.log("%s - %s",M(),t.format.apply(t,arguments))},t.inherits=n(1192),t._extend=function(e,t){if(!t||!k(t))return e;for(var n=Object.keys(t),r=n.length;r--;)e[n[r]]=t[n[r]];return e}}).call(t,n(17),n(33))},function(e,t){e.exports=function(){throw new Error("define cannot be used indirect")}},function(e,t,n){"use strict";function r(e){return a(e).map(function(e){return{value:e,type:i(e)}})}function i(e){return u(e)?"ClosingTag":c(e)?"OpeningTag":l(e)?"SelfClosingTag":"Text"}var o=n(1177),a=function(e){return e.split(/(<\/?[^>]+>)/g).filter(function(e){return""!==e.trim()})},s=function(e){return/<[^>!]+>/.test(e)},u=function(e){return/<\/+[^>]+>/.test(e)},l=function(e){return/<[^>]+\/>/.test(e)},c=function(e){return s(e)&&!u(e)&&!l(e)};e.exports=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=t.indentor,i=t.textNodesOnSameLine,a=0,s=[];n=n||" ";var u=r(e).map(function(e,t,r){var u=e.value,l=e.type;"ClosingTag"===l&&a--;var c=o(n,a),p=c+u;if("OpeningTag"===l&&a++,i){var f=r[t-1],h=r[t-2];"ClosingTag"===l&&"Text"===f.type&&"OpeningTag"===h.type&&(p=""+c+h.value+f.value+u,s.push(t-2,t-1))}return p});return s.forEach(function(e){return u[e]=null}),u.filter(function(e){return!!e}).join("\n")}},function(e,t){function n(e){return e&&e.replace?e.replace(/([&"<>'])/g,function(e,t){return r[t]}):e}var r={"&":"&",'"':""","'":"'","<":"<",">":">"};e.exports=n},function(e,t,n){(function(t){function r(e,n){function r(e){m?t.nextTick(e):e()}function i(e,t){if(void 0!==t&&(f+=t),e&&!h&&(l=l||new c,h=!0),e&&h){var n=f;r(function(){l.emit("data",n)}),f=""}}function o(e,t){s(i,a(e,d,d?1:0),t)}function u(){if(l){var e=f;r(function(){l.emit("data",e),l.emit("end"),l.readable=!1,l.emit("close")})}}"object"!=typeof n&&(n={indent:n});var l=n.stream?new c:null,f="",h=!1,d=n.indent?!0===n.indent?p:n.indent:"",m=!0;return r(function(){m=!1}),n.declaration&&function(e){var t=e.encoding||"UTF-8",n={version:"1.0",encoding:t};e.standalone&&(n.standalone=e.standalone),o({"?xml":{_attr:n}}),f=f.replace("/>","?>")}(n.declaration),e&&e.forEach?e.forEach(function(t,n){var r;n+1===e.length&&(r=u),o(t,r)}):o(e,u),l?(l.readable=!0,l):f}function i(){var e=Array.prototype.slice.call(arguments),t={_elem:a(e)};return t.push=function(e){if(!this.append)throw new Error("not assigned to a parent!");var t=this,n=this._elem.indent;s(this.append,a(e,n,this._elem.icount+(n?1:0)),function(){t.append(!0)})},t.close=function(e){void 0!==e&&this.push(e),this.end&&this.end()},t}function o(e,t){return new Array(t||0).join(e||"")}function a(e,t,n){function r(e){Object.keys(e).forEach(function(t){f.push(u(t,e[t]))})}n=n||0;var i,s=o(t,n),c=e;if("object"==typeof e){if(i=Object.keys(e)[0],(c=e[i])&&c._elem)return c._elem.name=i,c._elem.icount=n,c._elem.indent=t,c._elem.indents=s,c._elem.interrupt=c,c._elem}var p,f=[],h=[];switch(typeof c){case"object":if(null===c)break;c._attr&&r(c._attr),c._cdata&&h.push(("<![CDATA["+c._cdata).replace(/\]\]>/g,"]]]]><![CDATA[>")+"]]>"),c.forEach&&(p=!1,h.push(""),c.forEach(function(e){if("object"==typeof e){"_attr"==Object.keys(e)[0]?r(e._attr):h.push(a(e,t,n+1))}else h.pop(),p=!0,h.push(l(e))}),p||h.push(""));break;default:h.push(l(c))}return{name:i,interrupt:!1,attributes:f,content:h,icount:n,indents:s,indent:t}}function s(e,t,n){function r(){for(;t.content.length;){var r=t.content.shift();if(void 0!==r){if(i(r))return;s(e,r)}}e(!1,(o>1?t.indents:"")+(t.name?"</"+t.name+">":"")+(t.indent&&!n?"\n":"")),n&&n()}function i(t){return!!t.interrupt&&(t.interrupt.append=e,t.interrupt.end=r,t.interrupt=!1,e(!0),!0)}if("object"!=typeof t)return e(!1,t);var o=t.interrupt?1:t.content.length;if(e(!1,t.indents+(t.name?"<"+t.name:"")+(t.attributes.length?" "+t.attributes.join(" "):"")+(o?t.name?">":"":t.name?"/>":"")+(t.indent&&o>1?"\n":"")),!o)return e(!1,t.indent?"\n":"");i(t)||r()}function u(e,t){return e+'="'+l(t)+'"'}var l=n(1197),c=n(493).Stream,p=" ";e.exports=r,e.exports.element=e.exports.Element=i}).call(t,n(33))},function(e,t){function n(e,t,n){return r.yubl(t((n||r.yufull)(e)))}t._getPrivFilters=function(){function e(e){var t=e.split(k,2);return!t[0]||2!==t.length&&e.length===t[0].length?null:t[0]}function t(e,t,n,r){function i(e,n,i,a){return n?(n=Number(n[0]<="9"?n:"0"+n),r?A(n):128===n?"€":130===n?"‚":131===n?"ƒ":132===n?"„":133===n?"…":134===n?"†":135===n?"‡":136===n?"ˆ":137===n?"‰":138===n?"Š":139===n?"‹":140===n?"Œ":142===n?"Ž":145===n?"‘":146===n?"’":147===n?"“":148===n?"”":149===n?"•":150===n?"–":151===n?"—":152===n?"˜":153===n?"™":154===n?"š":155===n?"›":156===n?"œ":158===n?"ž":159===n?"Ÿ":n>=55296&&n<=57343||13===n?"�":o.frCoPt(n)):t[i||a]||e}return t=t||m,n=n||d,void 0===e?"undefined":null===e?"null":e.toString().replace(c,"�").replace(n,i)}function n(e){return"\\"+e.charCodeAt(0).toString(16).toLowerCase()+" "}function r(e){return e.replace(_,function(e){return"-x-"+e})}function i(n){n=o.yufull(t(n));var r=e(n);return r&&w[r.toLowerCase()]?"##"+n:n}var o,a=/</g,s=/"/g,u=/'/g,l=/&/g,c=/\x00/g,p=/(?:^$|[\x00\x09-\x0D "'`=<>])/g,f=/[&<>"'`]/g,h=/(?:\x00|^-*!?>|--!?>|--?!?$|\]>|\]$)/g,d=/&(?:#([xX][0-9A-Fa-f]+|\d+);?|(Tab|NewLine|colon|semi|lpar|rpar|apos|sol|comma|excl|ast|midast|ensp|emsp|thinsp);|(nbsp|amp|AMP|lt|LT|gt|GT|quot|QUOT);?)/g,m={Tab:"\t",NewLine:"\n",colon:":",semi:";",lpar:"(",rpar:")",apos:"'",sol:"/",comma:",",excl:"!",ast:"*",midast:"*",ensp:" ",emsp:" ",thinsp:" ",nbsp:" ",amp:"&",lt:"<",gt:">",quot:'"',QUOT:'"'},v=/^(?:(?!-*expression)#?[-\w]+|[+-]?(?:\d+|\d*\.\d+)(?:r?em|ex|ch|cm|mm|in|px|pt|pc|%|vh|vw|vmin|vmax)?|!important|)$/i,g=/[\x00-\x1F\x7F\[\]{}\\"]/g,y=/[\x00-\x1F\x7F\[\]{}\\']/g,_=/url[\(\u207D\u208D]+/g,b=/['\(\)]/g,x=/\/\/%5[Bb]([A-Fa-f0-9:]+)%5[Dd]/,w={javascript:1,data:1,vbscript:1,mhtml:1,"x-schema":1},k=/(?::|&#[xX]0*3[aA];?|�*58;?|:)/,E=/(?:^[\x00-\x20]+|[\t\n\r\x00]+)/g,S={Tab:"\t",NewLine:"\n"},C=function(e,t,n){return void 0===e?"undefined":null===e?"null":e.toString().replace(t,n)},A=String.fromCodePoint||function(e){return 0===arguments.length?"":e<=65535?String.fromCharCode(e):(e-=65536,String.fromCharCode(55296+(e>>10),e%1024+56320))};return o={frCoPt:function(e){return void 0===e||null===e?"":!isFinite(e=Number(e))||e<=0||e>1114111||e>=1&&e<=8||e>=14&&e<=31||e>=127&&e<=159||e>=64976&&e<=65007||11===e||65535==(65535&e)||65534==(65535&e)?"�":A(e)},d:t,yup:function(n){return n=e(n.replace(c,"")),n?t(n,S,null,!0).replace(E,"").toLowerCase():null},y:function(e){return C(e,f,function(e){return"&"===e?"&":"<"===e?"<":">"===e?">":'"'===e?""":"'"===e?"'":"`"})},ya:function(e){return C(e,l,"&")},yd:function(e){return C(e,a,"<")},yc:function(e){return C(e,h,function(e){return"\0"===e?"�":"--!"===e||"--"===e||"-"===e||"]"===e?e+" ":e.slice(0,-1)+" >"})},yavd:function(e){return C(e,s,""")},yavs:function(e){return C(e,u,"'")},yavu:function(e){return C(e,p,function(e){return"\t"===e?" ":"\n"===e?" ":"\v"===e?" ":"\f"===e?" ":"\r"===e?" ":" "===e?" ":"="===e?"=":"<"===e?"<":">"===e?">":'"'===e?""":"'"===e?"'":"`"===e?"`":"�"})},yu:encodeURI,yuc:encodeURIComponent,yubl:function(e){return w[o.yup(e)]?"x-"+e:e},yufull:function(e){return o.yu(e).replace(x,function(e,t){return"//["+t+"]"})},yublf:function(e){return o.yubl(o.yufull(e))},yceu:function(e){return e=t(e),v.test(e)?e:";-x:'"+r(e.replace(y,n))+"';-v:"},yced:function(e){return r(t(e).replace(g,n))},yces:function(e){return r(t(e).replace(y,n))},yceuu:function(e){return i(e).replace(b,function(e){return"'"===e?"\\27 ":"("===e?"%28":"%29"})},yceud:function(e){return i(e)},yceus:function(e){return i(e).replace(u,"\\27 ")}}};var r=t._privFilters=t._getPrivFilters();t.inHTMLData=r.yd,t.inHTMLComment=r.yc,t.inSingleQuotedAttr=r.yavs,t.inDoubleQuotedAttr=r.yavd,t.inUnQuotedAttr=r.yavu,t.uriInSingleQuotedAttr=function(e){return n(e,r.yavs)},t.uriInDoubleQuotedAttr=function(e){return n(e,r.yavd)},t.uriInUnQuotedAttr=function(e){return n(e,r.yavu)},t.uriInHTMLData=r.yufull,t.uriInHTMLComment=function(e){return r.yc(r.yufull(e))},t.uriPathInSingleQuotedAttr=function(e){return n(e,r.yavs,r.yu)},t.uriPathInDoubleQuotedAttr=function(e){return n(e,r.yavd,r.yu)},t.uriPathInUnQuotedAttr=function(e){return n(e,r.yavu,r.yu)},t.uriPathInHTMLData=r.yu,t.uriPathInHTMLComment=function(e){return r.yc(r.yu(e))},t.uriQueryInSingleQuotedAttr=t.uriPathInSingleQuotedAttr,t.uriQueryInDoubleQuotedAttr=t.uriPathInDoubleQuotedAttr,t.uriQueryInUnQuotedAttr=t.uriPathInUnQuotedAttr,t.uriQueryInHTMLData=t.uriPathInHTMLData,t.uriQueryInHTMLComment=t.uriPathInHTMLComment,t.uriComponentInSingleQuotedAttr=function(e){return r.yavs(r.yuc(e))},t.uriComponentInDoubleQuotedAttr=function(e){return r.yavd(r.yuc(e))},t.uriComponentInUnQuotedAttr=function(e){return r.yavu(r.yuc(e))},t.uriComponentInHTMLData=r.yuc,t.uriComponentInHTMLComment=function(e){return r.yc(r.yuc(e))},t.uriFragmentInSingleQuotedAttr=function(e){return r.yubl(r.yavs(r.yuc(e)))},t.uriFragmentInDoubleQuotedAttr=function(e){return r.yubl(r.yavd(r.yuc(e)))},t.uriFragmentInUnQuotedAttr=function(e){return r.yubl(r.yavu(r.yuc(e)))},t.uriFragmentInHTMLData=t.uriComponentInHTMLData,t.uriFragmentInHTMLComment=t.uriComponentInHTMLComment},function(e,t){function n(){for(var e={},t=0;t<arguments.length;t++){var n=arguments[t];for(var i in n)r.call(n,i)&&(e[i]=n[i])}return e}e.exports=n;var r=Object.prototype.hasOwnProperty},function(e,t,n){(function(){var e,t,r,i,o,a=[].slice;o=n(61),e=n(1202),i=n(1205),t=n(1204),r=n(266),this.make_dumper=function(n,s,u,l){var c;return null==n&&(n=e.Emitter),null==s&&(s=i.Serializer),null==u&&(u=t.Representer),null==l&&(l=r.Resolver),c=[n,s,u,l],function(){function e(e,n){var r,i,o;for(null==n&&(n={}),c[0].call(this,e,n),o=c.slice(1),r=0,i=o.length;r<i;r++)t=o[r],t.call(this,n)}var t;return o.extend.apply(o,[e.prototype].concat(a.call(function(){var e,n,r;for(r=[],e=0,n=c.length;e<n;e++)t=c[e],r.push(t.prototype);return r}()))),e}()},this.Dumper=this.make_dumper()}).call(this)},function(e,t,n){(function(){var e,r,i,o,a=function(e,t){function n(){this.constructor=e}for(var r in t)s.call(t,r)&&(e[r]=t[r]);return n.prototype=t.prototype,e.prototype=new n,e.__super__=t.prototype,e},s={}.hasOwnProperty,u=[].indexOf||function(e){for(var t=0,n=this.length;t<n;t++)if(t in this&&this[t]===e)return t;return-1};i=n(127),o=n(61),r=n(45).YAMLError,this.EmitterError=function(e){function t(){return t.__super__.constructor.apply(this,arguments)}return a(t,e),t}(r),this.Emitter=function(){function n(e,t){var n;this.stream=e,this.encoding=null,this.states=[],this.state=this.expect_stream_start,this.events=[],this.event=null,this.indents=[],this.indent=null,this.flow_level=0,this.root_context=!1,this.sequence_context=!1,this.mapping_context=!1,this.simple_key_context=!1,this.line=0,this.column=0,this.whitespace=!0,this.indentation=!0,this.open_ended=!1,this.canonical=t.canonical,this.allow_unicode=t.allow_unicode,null==this.canonical&&(this.canonical=!1),null==this.allow_unicode&&(this.allow_unicode=!0),this.best_indent=1<t.indent&&t.indent<10?t.indent:2,this.best_width=t.width>2*this.indent?t.width:80,this.best_line_break="\r"===(n=t.line_break)||"\n"===n||"\r\n"===n?t.line_break:"\n",this.tag_prefixes=null,this.prepared_anchor=null,this.prepared_tag=null,this.analysis=null,this.style=null}var r,a,l;return r="\0 \t\r\n…\u2028\u2029",a={"!":"!","tag:yaml.org,2002:":"!!"},l={"\0":"0","":"a","\b":"b","\t":"t","\n":"n","\v":"v","\f":"f","\r":"r","":"e",'"':'"',"\\":"\\","…":"N"," ":"_","\u2028":"L","\u2029":"P"},n.prototype.dispose=function(){return this.states=[],this.state=null},n.prototype.emit=function(e){var t;for(this.events.push(e),t=[];!this.need_more_events();)this.event=this.events.shift(),this.state(),t.push(this.event=null);return t},n.prototype.need_more_events=function(){var e;return 0===this.events.length||(e=this.events[0],e instanceof i.DocumentStartEvent?this.need_events(1):e instanceof i.SequenceStartEvent?this.need_events(2):e instanceof i.MappingStartEvent&&this.need_events(3))},n.prototype.need_events=function(e){var t,n,r,o,a;for(o=0,a=this.events.slice(1),n=0,r=a.length;n<r;n++)if(t=a[n],t instanceof i.DocumentStartEvent||t instanceof i.CollectionStartEvent?o++:t instanceof i.DocumentEndEvent||t instanceof i.CollectionEndEvent?o--:t instanceof i.StreamEndEvent&&(o=-1),o<0)return!1;return this.events.length<e+1},n.prototype.increase_indent=function(e){return null==e&&(e={}),this.indents.push(this.indent),null==this.indent?this.indent=e.flow?this.best_indent:0:e.indentless?void 0:this.indent+=this.best_indent},n.prototype.expect_stream_start=function(){return this.event instanceof i.StreamStartEvent?(!this.event.encoding||"encoding"in this.stream||(this.encoding=this.event.encoding),this.write_stream_start(),this.state=this.expect_first_document_start):this.error("expected StreamStartEvent, but got",this.event)},n.prototype.expect_nothing=function(){return this.error("expected nothing, but got",this.event)},n.prototype.expect_first_document_start=function(){return this.expect_document_start(!0)},n.prototype.expect_document_start=function(e){var t,n,r,u,l,c,p;if(null==e&&(e=!1),this.event instanceof i.DocumentStartEvent){if((this.event.version||this.event.tags)&&this.open_ended&&(this.write_indicator("...",!0),this.write_indent()),this.event.version&&this.write_version_directive(this.prepare_version(this.event.version)),this.tag_prefixes=o.clone(a),this.event.tags)for(p=function(){var e,t;e=this.event.tags,t=[];for(u in e)s.call(e,u)&&t.push(u);return t}.call(this).sort(),r=0,l=p.length;r<l;r++)n=p[r],c=this.event.tags[n],this.tag_prefixes[c]=n,this.write_tag_directive(this.prepare_tag_handle(n),this.prepare_tag_prefix(c));return t=!e||this.event.explicit||this.canonical||this.event.version||this.event.tags||this.check_empty_document(),t&&(this.write_indent(),this.write_indicator("---",!0),this.canonical&&this.write_indent()),this.state=this.expect_document_root}return this.event instanceof i.StreamEndEvent?(this.open_ended&&(this.write_indicator("...",!0),this.write_indent()),this.write_stream_end(),this.state=this.expect_nothing):this.error("expected DocumentStartEvent, but got",this.event)},n.prototype.expect_document_end=function(){return this.event instanceof i.DocumentEndEvent?(this.write_indent(),this.event.explicit&&(this.write_indicator("...",!0),this.write_indent()),this.flush_stream(),this.state=this.expect_document_start):this.error("expected DocumentEndEvent, but got",this.event)},n.prototype.expect_document_root=function(){return this.states.push(this.expect_document_end),this.expect_node({root:!0})},n.prototype.expect_node=function(e){return null==e&&(e={}),this.root_context=!!e.root,this.sequence_context=!!e.sequence,this.mapping_context=!!e.mapping,this.simple_key_context=!!e.simple_key,this.event instanceof i.AliasEvent?this.expect_alias():this.event instanceof i.ScalarEvent||this.event instanceof i.CollectionStartEvent?(this.process_anchor("&"),this.process_tag(),this.event instanceof i.ScalarEvent?this.expect_scalar():this.event instanceof i.SequenceStartEvent?this.flow_level||this.canonical||this.event.flow_style||this.check_empty_sequence()?this.expect_flow_sequence():this.expect_block_sequence():this.event instanceof i.MappingStartEvent?this.flow_level||this.canonical||this.event.flow_style||this.check_empty_mapping()?this.expect_flow_mapping():this.expect_block_mapping():void 0):this.error("expected NodeEvent, but got",this.event)},n.prototype.expect_alias=function(){return this.event.anchor||this.error("anchor is not specified for alias"),this.process_anchor("*"),this.state=this.states.pop()},n.prototype.expect_scalar=function(){return this.increase_indent({flow:!0}),this.process_scalar(),this.indent=this.indents.pop(),this.state=this.states.pop()},n.prototype.expect_flow_sequence=function(){return this.write_indicator("[",!0,{whitespace:!0}),this.flow_level++,this.increase_indent({flow:!0}),this.state=this.expect_first_flow_sequence_item},n.prototype.expect_first_flow_sequence_item=function(){return this.event instanceof i.SequenceEndEvent?(this.indent=this.indents.pop(),this.flow_level--,this.write_indicator("]",!1),this.state=this.states.pop()):((this.canonical||this.column>this.best_width)&&this.write_indent(),this.states.push(this.expect_flow_sequence_item),this.expect_node({sequence:!0}))},n.prototype.expect_flow_sequence_item=function(){return this.event instanceof i.SequenceEndEvent?(this.indent=this.indents.pop(),this.flow_level--,this.canonical&&(this.write_indicator(",",!1),this.write_indent()),this.write_indicator("]",!1),this.state=this.states.pop()):(this.write_indicator(",",!1),(this.canonical||this.column>this.best_width)&&this.write_indent(),this.states.push(this.expect_flow_sequence_item),this.expect_node({sequence:!0}))},n.prototype.expect_flow_mapping=function(){return this.write_indicator("{",!0,{whitespace:!0}),this.flow_level++,this.increase_indent({flow:!0}),this.state=this.expect_first_flow_mapping_key},n.prototype.expect_first_flow_mapping_key=function(){return this.event instanceof i.MappingEndEvent?(this.indent=this.indents.pop(),this.flow_level--,this.write_indicator("}",!1),this.state=this.states.pop()):((this.canonical||this.column>this.best_width)&&this.write_indent(),!this.canonical&&this.check_simple_key()?(this.states.push(this.expect_flow_mapping_simple_value),this.expect_node({mapping:!0,simple_key:!0})):(this.write_indicator("?",!0),this.states.push(this.expect_flow_mapping_value),this.expect_node({mapping:!0})))},n.prototype.expect_flow_mapping_key=function(){return this.event instanceof i.MappingEndEvent?(this.indent=this.indents.pop(),this.flow_level--,this.canonical&&(this.write_indicator(",",!1),this.write_indent()),this.write_indicator("}",!1),this.state=this.states.pop()):(this.write_indicator(",",!1),(this.canonical||this.column>this.best_width)&&this.write_indent(),!this.canonical&&this.check_simple_key()?(this.states.push(this.expect_flow_mapping_simple_value),this.expect_node({mapping:!0,simple_key:!0})):(this.write_indicator("?",!0),this.states.push(this.expect_flow_mapping_value),this.expect_node({mapping:!0})))},n.prototype.expect_flow_mapping_simple_value=function(){return this.write_indicator(":",!1),this.states.push(this.expect_flow_mapping_key),this.expect_node({mapping:!0})},n.prototype.expect_flow_mapping_value=function(){return(this.canonical||this.column>this.best_width)&&this.write_indent(),this.write_indicator(":",!0),this.states.push(this.expect_flow_mapping_key),this.expect_node({mapping:!0})},n.prototype.expect_block_sequence=function(){var e;return e=this.mapping_context&&!this.indentation,this.increase_indent({indentless:e}),this.state=this.expect_first_block_sequence_item},n.prototype.expect_first_block_sequence_item=function(){return this.expect_block_sequence_item(!0)},n.prototype.expect_block_sequence_item=function(e){return null==e&&(e=!1),!e&&this.event instanceof i.SequenceEndEvent?(this.indent=this.indents.pop(),this.state=this.states.pop()):(this.write_indent(),this.write_indicator("-",!0,{indentation:!0}),this.states.push(this.expect_block_sequence_item),this.expect_node({sequence:!0}))},n.prototype.expect_block_mapping=function(){return this.increase_indent(),this.state=this.expect_first_block_mapping_key},n.prototype.expect_first_block_mapping_key=function(){return this.expect_block_mapping_key(!0)},n.prototype.expect_block_mapping_key=function(e){return null==e&&(e=!1),!e&&this.event instanceof i.MappingEndEvent?(this.indent=this.indents.pop(),this.state=this.states.pop()):(this.write_indent(),this.check_simple_key()?(this.states.push(this.expect_block_mapping_simple_value),this.expect_node({mapping:!0,simple_key:!0})):(this.write_indicator("?",!0,{indentation:!0}),this.states.push(this.expect_block_mapping_value),this.expect_node({mapping:!0})))},n.prototype.expect_block_mapping_simple_value=function(){return this.write_indicator(":",!1),this.states.push(this.expect_block_mapping_key),this.expect_node({mapping:!0})},n.prototype.expect_block_mapping_value=function(){return this.write_indent(),this.write_indicator(":",!0,{indentation:!0}),this.states.push(this.expect_block_mapping_key),this.expect_node({mapping:!0})},n.prototype.check_empty_document=function(){var e;return this.event instanceof i.DocumentStartEvent&&0!==this.events.length&&((e=this.events[0])instanceof i.ScalarEvent&&null==e.anchor&&null==e.tag&&e.implicit&&""===e.value)},n.prototype.check_empty_sequence=function(){return this.event instanceof i.SequenceStartEvent&&this.events[0]instanceof i.SequenceEndEvent},n.prototype.check_empty_mapping=function(){return this.event instanceof i.MappingStartEvent&&this.events[0]instanceof i.MappingEndEvent},n.prototype.check_simple_key=function(){var e;return e=0,this.event instanceof i.NodeEvent&&null!=this.event.anchor&&(null==this.prepared_anchor&&(this.prepared_anchor=this.prepare_anchor(this.event.anchor)),e+=this.prepared_anchor.length),null!=this.event.tag&&(this.event instanceof i.ScalarEvent||this.event instanceof i.CollectionStartEvent)&&(null==this.prepared_tag&&(this.prepared_tag=this.prepare_tag(this.event.tag)),e+=this.prepared_tag.length),this.event instanceof i.ScalarEvent&&(null==this.analysis&&(this.analysis=this.analyze_scalar(this.event.value)),e+=this.analysis.scalar.length),e<128&&(this.event instanceof i.AliasEvent||this.event instanceof i.ScalarEvent&&!this.analysis.empty&&!this.analysis.multiline||this.check_empty_sequence()||this.check_empty_mapping())},n.prototype.process_anchor=function(e){return null==this.event.anchor?void(this.prepared_anchor=null):(null==this.prepared_anchor&&(this.prepared_anchor=this.prepare_anchor(this.event.anchor)),this.prepared_anchor&&this.write_indicator(""+e+this.prepared_anchor,!0),this.prepared_anchor=null)},n.prototype.process_tag=function(){var e;if(e=this.event.tag,this.event instanceof i.ScalarEvent){if(null==this.style&&(this.style=this.choose_scalar_style()),(!this.canonical||null==e)&&(""===this.style&&this.event.implicit[0]||""!==this.style&&this.event.implicit[1]))return void(this.prepared_tag=null);this.event.implicit[0]&&null==e&&(e="!",this.prepared_tag=null)}else if((!this.canonical||null==e)&&this.event.implicit)return void(this.prepared_tag=null);return null==e&&this.error("tag is not specified"),null==this.prepared_tag&&(this.prepared_tag=this.prepare_tag(e)),this.write_indicator(this.prepared_tag,!0),this.prepared_tag=null},n.prototype.process_scalar=function(){var e;switch(null==this.analysis&&(this.analysis=this.analyze_scalar(this.event.value)),null==this.style&&(this.style=this.choose_scalar_style()),e=!this.simple_key_context,this.style){case'"':this.write_double_quoted(this.analysis.scalar,e);break;case"'":this.write_single_quoted(this.analysis.scalar,e);break;case">":this.write_folded(this.analysis.scalar);break;case"|":this.write_literal(this.analysis.scalar);break;default:this.write_plain(this.analysis.scalar,e)}return this.analysis=null,this.style=null},n.prototype.choose_scalar_style=function(){var e;return null==this.analysis&&(this.analysis=this.analyze_scalar(this.event.value)),'"'===this.event.style||this.canonical?'"':this.event.style||!this.event.implicit[0]||this.simple_key_context&&(this.analysis.empty||this.analysis.multiline)||!(this.flow_level&&this.analysis.allow_flow_plain||!this.flow_level&&this.analysis.allow_block_plain)?this.event.style&&(e=this.event.style,u.call("|>",e)>=0)&&!this.flow_level&&!this.simple_key_context&&this.analysis.allow_block?this.event.style:this.event.style&&"'"!==this.event.style||!this.analysis.allow_single_quoted||this.simple_key_context&&this.analysis.multiline?'"':"'":""},n.prototype.prepare_version=function(e){var t,n,r;return t=e[0],n=e[1],r=t+"."+n,1===t?r:this.error("unsupported YAML version",r)},n.prototype.prepare_tag_handle=function(e){var t,n,r,i;for(e||this.error("tag handle must not be empty"),"!"===e[0]&&"!"===e.slice(-1)||this.error("tag handle must start and end with '!':",e),i=e.slice(1,-1),n=0,r=i.length;n<r;n++)"0"<=(t=i[n])&&t<="9"||"A"<=t&&t<="Z"||"a"<=t&&t<="z"||u.call("-_",t)>=0||this.error("invalid character '"+t+"' in the tag handle:",e);return e},n.prototype.prepare_tag_prefix=function(e){var t,n,r,i;for(e||this.error("tag prefix must not be empty"),n=[],i=0,r=+("!"===e[0]);r<e.length;)t=e[r],"0"<=t&&t<="9"||"A"<=t&&t<="Z"||"a"<=t&&t<="z"||u.call("-;/?!:@&=+$,_.~*'()[]",t)>=0?r++:(i<r&&n.push(e.slice(i,r)),i=r+=1,n.push(t));return i<r&&n.push(e.slice(i,r)),n.join("")},n.prototype.prepare_tag=function(e){var t,n,r,i,o,a,l,c,p,f,h,d;if(e||this.error("tag must not be empty"),"!"===e)return e;for(i=null,h=e,p=function(){var e,t;e=this.tag_prefixes,t=[];for(a in e)s.call(e,a)&&t.push(a);return t}.call(this).sort(),o=0,l=p.length;o<l;o++)c=p[o],0===e.indexOf(c)&&("!"===c||c.length<e.length)&&(i=this.tag_prefixes[c],h=e.slice(c.length));for(n=[],f=r=0;r<h.length;)t=h[r],"0"<=t&&t<="9"||"A"<=t&&t<="Z"||"a"<=t&&t<="z"||u.call("-;/?!:@&=+$,_.~*'()[]",t)>=0||"!"===t&&"!"!==i?r++:(f<r&&n.push(h.slice(f,r)),f=r+=1,n.push(t));return f<r&&n.push(h.slice(f,r)),d=n.join(""),i?""+i+d:"!<"+d+">"},n.prototype.prepare_anchor=function(e){var t,n,r;for(e||this.error("anchor must not be empty"),n=0,r=e.length;n<r;n++)"0"<=(t=e[n])&&t<="9"||"A"<=t&&t<="Z"||"a"<=t&&t<="z"||u.call("-_",t)>=0||this.error("invalid character '"+t+"' in the anchor:",e);return e},n.prototype.analyze_scalar=function(t){var n,i,o,a,s,l,c,p,f,h,d,m,v,g,y,_,b,x,w,k,E,S,C,A,D;for(t||new e(t,!0,!1,!1,!0,!0,!0,!1),l=!1,f=!1,_=!1,C=!1,!1,g=!1,v=!1,D=!1,A=!1,c=!1,S=!1,0!==t.indexOf("---")&&0!==t.indexOf("...")||(l=!0,f=!0),b=!0,h=1===t.length||(k=t[1],u.call("\0 \t\r\n…\u2028\u2029",k)>=0),w=!1,x=!1,m=0,m=d=0,y=t.length;d<y;m=++d)p=t[m],0===m?u.call("#,[]{}&*!|>'\"%@`",p)>=0||"-"===p&&h?(f=!0,l=!0):u.call("?:",p)>=0&&(f=!0,h&&(l=!0)):u.call(",?[]{}",p)>=0?f=!0:":"===p?(f=!0,h&&(l=!0)):"#"===p&&b&&(f=!0,l=!0),u.call("\n…\u2028\u2029",p)>=0&&(_=!0),"\n"===p||" "<=p&&p<="~"||("\ufeff"!==p&&("…"===p||" "<=p&&p<="퟿"||""<=p&&p<="�")?(!0,this.allow_unicode||(C=!0)):C=!0)," "===p?(0===m&&(g=!0),m===t.length-1&&(D=!0),x&&(c=!0),x=!1,w=!0):u.call("\n…\u2028\u2029",p)>=0?(0===m&&(v=!0),m===t.length-1&&(A=!0),w&&(S=!0),x=!0,w=!1):(x=!1,w=!1),b=u.call(r,p)>=0,h=m+2>=t.length||(E=t[m+2],u.call(r,E)>=0);return a=!0,i=!0,s=!0,o=!0,n=!0,(g||v||D||A)&&(a=i=!1),D&&(n=!1),c&&(a=i=s=!1),(S||C)&&(a=i=s=n=!1),_&&(a=i=!1),f&&(a=!1),l&&(i=!1),new e(t,!1,_,a,i,s,o,n)},n.prototype.write_stream_start=function(){if(this.encoding&&0===this.encoding.indexOf("utf-16"))return this.stream.write("\ufeff",this.encoding)},n.prototype.write_stream_end=function(){return this.flush_stream()},n.prototype.write_indicator=function(e,t,n){var r;return null==n&&(n={}),r=this.whitespace||!t?e:" "+e,this.whitespace=!!n.whitespace,this.indentation&&(this.indentation=!!n.indentation),this.column+=r.length,this.open_ended=!1,this.stream.write(r,this.encoding)},n.prototype.write_indent=function(){var e,t,n;if(t=null!=(n=this.indent)?n:0,(!this.indentation||this.column>t||this.column===t&&!this.whitespace)&&this.write_line_break(),this.column<t)return this.whitespace=!0,e=new Array(t-this.column+1).join(" "),this.column=t,this.stream.write(e,this.encoding)},n.prototype.write_line_break=function(e){return this.whitespace=!0,this.indentation=!0,this.line+=1,this.column=0,this.stream.write(null!=e?e:this.best_line_break,this.encoding)},n.prototype.write_version_directive=function(e){return this.stream.write("%YAML "+e,this.encoding),this.write_line_break()},n.prototype.write_tag_directive=function(e,t){return this.stream.write("%TAG "+e+" "+t,this.encoding),this.write_line_break()},n.prototype.write_single_quoted=function(e,t){var n,r,i,o,a,s,l,c,p,f;for(null==t&&(t=!0),this.write_indicator("'",!0),p=!1,r=!1,f=a=0;a<=e.length;){if(i=e[a],p)null!=i&&" "===i||(f+1===a&&this.column>this.best_width&&t&&0!==f&&a!==e.length?this.write_indent():(o=e.slice(f,a),this.column+=o.length,this.stream.write(o,this.encoding)),f=a);else if(r){if(null==i||u.call("\n…\u2028\u2029",i)<0){for("\n"===e[f]&&this.write_line_break(),c=e.slice(f,a),s=0,l=c.length;s<l;s++)n=c[s],"\n"===n?this.write_line_break():this.write_line_break(n);this.write_indent(),f=a}}else(null==i||u.call(" \n…\u2028\u2029",i)>=0||"'"===i)&&f<a&&(o=e.slice(f,a),this.column+=o.length,this.stream.write(o,this.encoding),f=a);"'"===i&&(this.column+=2,this.stream.write("''",this.encoding),f=a+1),null!=i&&(p=" "===i,r=u.call("\n…\u2028\u2029",i)>=0),a++}return this.write_indicator("'",!1)},n.prototype.write_double_quoted=function(e,t){var n,r,i,a;for(null==t&&(t=!0),this.write_indicator('"',!0),a=i=0;i<=e.length;)n=e[i],(null==n||u.call('"\\…\u2028\u2029\ufeff',n)>=0||!(" "<=n&&n<="~"||this.allow_unicode&&(" "<=n&&n<="퟿"||""<=n&&n<="�")))&&(a<i&&(r=e.slice(a,i),this.column+=r.length,this.stream.write(r,this.encoding),a=i),null!=n&&(r=n in l?"\\"+l[n]:n<="ÿ"?"\\x"+o.pad_left(o.to_hex(n),"0",2):n<="￿"?"\\u"+o.pad_left(o.to_hex(n),"0",4):"\\U"+o.pad_left(o.to_hex(n),"0",16),this.column+=r.length,this.stream.write(r,this.encoding),a=i+1)),t&&0<i&&i<e.length-1&&(" "===n||a>=i)&&this.column+(i-a)>this.best_width&&(r=e.slice(a,i)+"\\",a<i&&(a=i),this.column+=r.length,this.stream.write(r,this.encoding),this.write_indent(),this.whitespace=!1,this.indentation=!1," "===e[a]&&(r="\\",this.column+=r.length,this.stream.write(r,this.encoding))),i++;return this.write_indicator('"',!1)},n.prototype.write_folded=function(e){var t,n,r,i,o,a,s,l,c,p,f,h,d;for(a=this.determine_block_hints(e),this.write_indicator(">"+a,!0),"+"===a.slice(-1)&&(this.open_ended=!0),this.write_line_break(),l=!0,n=!0,h=!1,d=o=0,f=[];o<=e.length;){if(r=e[o],n){if(null==r||u.call("\n…\u2028\u2029",r)<0){for(l||null==r||" "===r||"\n"!==e[d]||this.write_line_break(),l=" "===r,p=e.slice(d,o),s=0,c=p.length;s<c;s++)t=p[s],"\n"===t?this.write_line_break():this.write_line_break(t);null!=r&&this.write_indent(),d=o}}else h?" "!==r&&(d+1===o&&this.column>this.best_width?this.write_indent():(i=e.slice(d,o),this.column+=i.length,this.stream.write(i,this.encoding)),d=o):(null==r||u.call(" \n…\u2028\u2029",r)>=0)&&(i=e.slice(d,o),this.column+=i.length,this.stream.write(i,this.encoding),null==r&&this.write_line_break(),d=o);null!=r&&(n=u.call("\n…\u2028\u2029",r)>=0,h=" "===r),f.push(o++)}return f},n.prototype.write_literal=function(e){var t,n,r,i,o,a,s,l,c,p,f;for(a=this.determine_block_hints(e),this.write_indicator("|"+a,!0),"+"===a.slice(-1)&&(this.open_ended=!0),this.write_line_break(),n=!0,f=o=0,p=[];o<=e.length;){if(r=e[o],n){if(null==r||u.call("\n…\u2028\u2029",r)<0){for(c=e.slice(f,o),s=0,l=c.length;s<l;s++)t=c[s],"\n"===t?this.write_line_break():this.write_line_break(t);null!=r&&this.write_indent(),f=o}}else(null==r||u.call("\n…\u2028\u2029",r)>=0)&&(i=e.slice(f,o),this.stream.write(i,this.encoding),null==r&&this.write_line_break(),f=o);null!=r&&(n=u.call("\n…\u2028\u2029",r)>=0),p.push(o++)}return p},n.prototype.write_plain=function(e,t){var n,r,i,o,a,s,l,c,p,f,h;if(null==t&&(t=!0),e){for(this.root_context&&(this.open_ended=!0),this.whitespace||(o=" ",this.column+=o.length,this.stream.write(o,this.encoding)),this.whitespace=!1,this.indentation=!1,f=!1,r=!1,h=a=0,p=[];a<=e.length;){if(i=e[a],f)" "!==i&&(h+1===a&&this.column>this.best_width&&t?(this.write_indent(),this.whitespace=!1,this.indentation=!1):(o=e.slice(h,a),this.column+=o.length,this.stream.write(o,this.encoding)),h=a);else if(r){if(u.call("\n…\u2028\u2029",i)<0){for("\n"===e[h]&&this.write_line_break(),c=e.slice(h,a),s=0,l=c.length;s<l;s++)n=c[s],"\n"===n?this.write_line_break():this.write_line_break(n);this.write_indent(),this.whitespace=!1,this.indentation=!1,h=a}}else(null==i||u.call(" \n…\u2028\u2029",i)>=0)&&(o=e.slice(h,a),this.column+=o.length,this.stream.write(o,this.encoding),h=a);null!=i&&(f=" "===i,r=u.call("\n…\u2028\u2029",i)>=0),p.push(a++)}return p}},n.prototype.determine_block_hints=function(e){var t,n,r,i,o;return n="",t=e[0],r=e.length-2,o=e[r++],i=e[r++],u.call(" \n…\u2028\u2029",t)>=0&&(n+=this.best_indent),u.call("\n…\u2028\u2029",i)<0?n+="-":(1===e.length||u.call("\n…\u2028\u2029",o)>=0)&&(n+="+"),n},n.prototype.flush_stream=function(){var e;return"function"==typeof(e=this.stream).flush?e.flush():void 0},n.prototype.error=function(e,n){var r,i;throw n&&(n=null!=(r=null!=n&&null!=(i=n.constructor)?i.name:void 0)?r:o.inspect(n)),new t.EmitterError(e+(n?" "+n:""))},n}(),e=function(){function e(e,t,n,r,i,o,a,s){this.scalar=e,this.empty=t,this.multiline=n,this.allow_flow_plain=r,this.allow_block_plain=i,this.allow_single_quoted=o,this.allow_double_quoted=a,this.allow_block=s}return e}()}).call(this)},function(e,t,n){(function(){var e,t,r,i,o,a,s,u=[].slice;s=n(61),i=n(501),a=n(502),r=n(500),e=n(498),o=n(266),t=n(499),this.make_loader=function(n,l,c,p,f,h){var d;return null==n&&(n=i.Reader),null==l&&(l=a.Scanner),null==c&&(c=r.Parser),null==p&&(p=e.Composer),null==f&&(f=o.Resolver),null==h&&(h=t.Constructor),d=[n,l,c,p,f,h],function(){function e(e){var n,r,i;for(d[0].call(this,e),i=d.slice(1),n=0,r=i.length;n<r;n++)t=i[n],t.call(this)}var t;return s.extend.apply(s,[e.prototype].concat(u.call(function(){var e,n,r;for(r=[],e=0,n=d.length;e<n;e++)t=d[e],r.push(t.prototype);return r}()))),e}()},this.Loader=this.make_loader()}).call(this)},function(e,t,n){(function(){var e,r,i=function(e,t){function n(){this.constructor=e}for(var r in t)o.call(t,r)&&(e[r]=t[r]);return n.prototype=t.prototype,e.prototype=new n,e.__super__=t.prototype,e},o={}.hasOwnProperty;r=n(94),e=n(45).YAMLError,this.RepresenterError=function(e){function t(){return t.__super__.constructor.apply(this,arguments)}return i(t,e),t}(e),this.BaseRepresenter=function(){function e(e){var t;t=null!=e?e:{},this.default_style=t.default_style,this.default_flow_style=t.default_flow_style,this.represented_objects={},this.object_keeper=[],this.alias_key=null}return e.prototype.yaml_representers_types=[],e.prototype.yaml_representers_handlers=[],e.prototype.yaml_multi_representers_types=[],e.prototype.yaml_multi_representers_handlers=[],e.add_representer=function(e,t){return this.prototype.hasOwnProperty("yaml_representers_types")||(this.prototype.yaml_representers_types=[].concat(this.prototype.yaml_representers_types)),this.prototype.hasOwnProperty("yaml_representers_handlers")||(this.prototype.yaml_representers_handlers=[].concat(this.prototype.yaml_representers_handlers)),this.prototype.yaml_representers_types.push(e),this.prototype.yaml_representers_handlers.push(t)},e.add_multi_representer=function(e,t){return this.prototype.hasOwnProperty("yaml_multi_representers_types")||(this.prototype.yaml_multi_representers_types=[].concat(this.prototype.yaml_multi_representers_types)),this.prototype.hasOwnProperty("yaml_multi_representers_handlers")||(this.prototype.yaml_multi_representers_handlers=[].concat(this.prototype.yaml_multi_representers_handlers)),this.prototype.yaml_multi_representers_types.push(e),this.prototype.yaml_multi_representers_handlers.push(t)},e.prototype.represent=function(e){var t;return t=this.represent_data(e),this.serialize(t),this.represented_objects={},this.object_keeper=[],this.alias_key=null},e.prototype.represent_data=function(e){var t,n,i,o,a,s,u;if(this.ignore_aliases(e))this.alias_key=null;else if(-1!==(n=this.object_keeper.indexOf(e))){if(this.alias_key=n,this.alias_key in this.represented_objects)return this.represented_objects[this.alias_key]}else this.alias_key=this.object_keeper.length,this.object_keeper.push(e);if(s=null,t=null===e?"null":typeof e,"object"===t&&(t=e.constructor),-1!==(n=this.yaml_representers_types.lastIndexOf(t))&&(s=this.yaml_representers_handlers[n]),null==s)for(a=this.yaml_multi_representers_types,n=i=0,o=a.length;i<o;n=++i)if(u=a[n],e instanceof u){s=this.yaml_multi_representers_handlers[n];break}return null==s&&(-1!==(n=this.yaml_multi_representers_types.lastIndexOf(void 0))?s=this.yaml_multi_representers_handlers[n]:-1!==(n=this.yaml_representers_types.lastIndexOf(void 0))&&(s=this.yaml_representers_handlers[n])),null!=s?s.call(this,e):new r.ScalarNode(null,""+e)},e.prototype.represent_scalar=function(e,t,n){var i;return null==n&&(n=this.default_style),i=new r.ScalarNode(e,t,null,null,n),null!=this.alias_key&&(this.represented_objects[this.alias_key]=i),i},e.prototype.represent_sequence=function(e,t,n){var i,o,a,s,u,l,c,p;for(p=[],u=new r.SequenceNode(e,p,null,null,n),null!=this.alias_key&&(this.represented_objects[this.alias_key]=u),i=!0,a=0,s=t.length;a<s;a++)o=t[a],l=this.represent_data(o),l instanceof r.ScalarNode||l.style||(i=!1),p.push(l);return null==n&&(u.flow_style=null!=(c=this.default_flow_style)?c:i),u},e.prototype.represent_mapping=function(e,t,n){var i,a,s,u,l,c,p,f;f=[],u=new r.MappingNode(e,f,n),this.alias_key&&(this.represented_objects[this.alias_key]=u),i=!0;for(a in t)o.call(t,a)&&(s=t[a],l=this.represent_data(a),c=this.represent_data(s),l instanceof r.ScalarNode||l.style||(i=!1),c instanceof r.ScalarNode||c.style||(i=!1),f.push([l,c]));return n||(u.flow_style=null!=(p=this.default_flow_style)?p:i),u},e.prototype.ignore_aliases=function(e){return!1},e}(),this.Representer=function(e){function n(){return n.__super__.constructor.apply(this,arguments)}return i(n,e),n.prototype.represent_boolean=function(e){return this.represent_scalar("tag:yaml.org,2002:bool",e?"true":"false")},n.prototype.represent_null=function(e){return this.represent_scalar("tag:yaml.org,2002:null","null")},n.prototype.represent_number=function(e){var t,n;return t="tag:yaml.org,2002:"+(e%1==0?"int":"float"),n=e!==e?".nan":Infinity===e?".inf":-Infinity===e?"-.inf":e.toString(),this.represent_scalar(t,n)},n.prototype.represent_string=function(e){return this.represent_scalar("tag:yaml.org,2002:str",e)},n.prototype.represent_array=function(e){return this.represent_sequence("tag:yaml.org,2002:seq",e)},n.prototype.represent_date=function(e){return this.represent_scalar("tag:yaml.org,2002:timestamp",e.toISOString())},n.prototype.represent_object=function(e){return this.represent_mapping("tag:yaml.org,2002:map",e)},n.prototype.represent_undefined=function(e){throw new t.RepresenterError("cannot represent an onbject: "+e)},n.prototype.ignore_aliases=function(e){var t;return null==e||("boolean"==(t=typeof e)||"number"===t||"string"===t)},n}(this.BaseRepresenter),this.Representer.add_representer("boolean",this.Representer.prototype.represent_boolean),this.Representer.add_representer("null",this.Representer.prototype.represent_null),this.Representer.add_representer("number",this.Representer.prototype.represent_number),this.Representer.add_representer("string",this.Representer.prototype.represent_string),this.Representer.add_representer(Array,this.Representer.prototype.represent_array),this.Representer.add_representer(Date,this.Representer.prototype.represent_date),this.Representer.add_representer(Object,this.Representer.prototype.represent_object),this.Representer.add_representer(null,this.Representer.prototype.represent_undefined)}).call(this)},function(e,t,n){(function(){var e,t,r,i,o=function(e,t){function n(){this.constructor=e}for(var r in t)a.call(t,r)&&(e[r]=t[r]);return n.prototype=t.prototype,e.prototype=new n,e.__super__=t.prototype,e},a={}.hasOwnProperty;t=n(127),r=n(94),i=n(61),e=n(45).YAMLError,this.SerializerError=function(e){function t(){return t.__super__.constructor.apply(this,arguments)}return o(t,e),t}(e),this.Serializer=function(){function e(e){var t;t=null!=e?e:{},this.encoding=t.encoding,this.explicit_start=t.explicit_start,this.explicit_end=t.explicit_end,this.version=t.version,this.tags=t.tags,this.serialized_nodes={},this.anchors={},this.last_anchor_id=0,this.closed=null}return e.prototype.open=function(){if(null===this.closed)return this.emit(new t.StreamStartEvent(this.encoding)),this.closed=!1;throw this.closed?new SerializerError("serializer is closed"):new SerializerError("serializer is already open")},e.prototype.close=function(){if(null===this.closed)throw new SerializerError("serializer is not opened");if(!this.closed)return this.emit(new t.StreamEndEvent),this.closed=!0},e.prototype.serialize=function(e){if(null===this.closed)throw new SerializerError("serializer is not opened");if(this.closed)throw new SerializerError("serializer is closed");return null!=e&&(this.emit(new t.DocumentStartEvent(void 0,void 0,this.explicit_start,this.version,this.tags)),this.anchor_node(e),this.serialize_node(e),this.emit(new t.DocumentEndEvent(void 0,void 0,this.explicit_end))),this.serialized_nodes={},this.anchors={},this.last_anchor_id=0},e.prototype.anchor_node=function(e){var t,n,i,o,a,s,u,l,c,p,f,h,d,m;if(e.unique_id in this.anchors)return null!=(t=this.anchors)[l=e.unique_id]?t[l]:t[l]=this.generate_anchor(e);if(this.anchors[e.unique_id]=null,e instanceof r.SequenceNode){for(c=e.value,h=[],n=0,s=c.length;n<s;n++)i=c[n],h.push(this.anchor_node(i));return h}if(e instanceof r.MappingNode){for(p=e.value,d=[],o=0,u=p.length;o<u;o++)f=p[o],a=f[0],m=f[1],this.anchor_node(a),d.push(this.anchor_node(m));return d}},e.prototype.generate_anchor=function(e){return"id"+i.pad_left(++this.last_anchor_id,"0",4)},e.prototype.serialize_node=function(e,n,i){var o,a,s,u,l,c,p,f,h,d,m,v,g,y;if(o=this.anchors[e.unique_id],e.unique_id in this.serialized_nodes)return this.emit(new t.AliasEvent(o));if(this.serialized_nodes[e.unique_id]=!0,this.descend_resolver(n,i),e instanceof r.ScalarNode)s=this.resolve(r.ScalarNode,e.value,[!0,!1]),a=this.resolve(r.ScalarNode,e.value,[!1,!0]),l=[e.tag===s,e.tag===a],this.emit(new t.ScalarEvent(o,e.tag,l,e.value,void 0,void 0,e.style));else if(e instanceof r.SequenceNode){for(l=e.tag===this.resolve(r.SequenceNode,e.value,!0),this.emit(new t.SequenceStartEvent(o,e.tag,l,void 0,void 0,e.flow_style)),m=e.value,i=u=0,h=m.length;u<h;i=++u)c=m[i],this.serialize_node(c,e,i);this.emit(new t.SequenceEndEvent)}else if(e instanceof r.MappingNode){for(l=e.tag===this.resolve(r.MappingNode,e.value,!0),this.emit(new t.MappingStartEvent(o,e.tag,l,void 0,void 0,e.flow_style)),v=e.value,p=0,d=v.length;p<d;p++)g=v[p],f=g[0],y=g[1],this.serialize_node(f,e,null),this.serialize_node(y,e,f);this.emit(new t.MappingEndEvent)}return this.ascend_resolver()},e}()}).call(this)},function(e,t,n){(function(){var e,r,i;this.composer=n(498),this.constructor=n(499),e=this.dumper=n(1201),this.errors=n(45),this.events=n(127),r=this.loader=n(1203),this.nodes=n(94),this.parser=n(500),this.reader=n(501),this.resolver=n(266),this.scanner=n(502),this.tokens=n(267),i=n(61),this.scan=function(e,t){var n,i;for(null==t&&(t=r.Loader),n=new t(e),i=[];n.check_token();)i.push(n.get_token());return i},this.parse=function(e,t){var n,i;for(null==t&&(t=r.Loader),n=new t(e),i=[];n.check_event();)i.push(n.get_event());return i},this.compose=function(e,t){var n;return null==t&&(t=r.Loader),n=new t(e),n.get_single_node()},this.compose_all=function(e,t){var n,i;for(null==t&&(t=r.Loader),n=new t(e),i=[];n.check_node();)i.push(n.get_node());return i},this.load=function(e,t){var n;return null==t&&(t=r.Loader),n=new t(e),n.get_single_data()},this.load_all=function(e,t){var n,i;for(null==t&&(t=r.Loader),n=new t(e),i=[];n.check_data();)i.push(n.get_data());return i},this.emit=function(t,n,r,o){var a,s,u,l,c;null==r&&(r=e.Dumper),null==o&&(o={}),s=n||new i.StringStream,a=new r(s,o);try{for(l=0,c=t.length;l<c;l++)u=t[l],a.emit(u)}finally{a.dispose()}return n||s.string},this.serialize=function(n,r,i,o){return null==i&&(i=e.Dumper),null==o&&(o={}),t.serialize_all([n],r,i,o)},this.serialize_all=function(t,n,r,o){var a,s,u,l,c;null==r&&(r=e.Dumper),null==o&&(o={}),s=n||new i.StringStream,a=new r(s,o);try{for(a.open(),u=0,l=t.length;u<l;u++)c=t[u],a.serialize(c);a.close()}finally{a.dispose()}return n||s.string},this.dump=function(n,r,i,o){return null==i&&(i=e.Dumper),null==o&&(o={}),t.dump_all([n],r,i,o)},this.dump_all=function(t,n,r,o){var a,s,u,l,c;null==r&&(r=e.Dumper),null==o&&(o={}),s=n||new i.StringStream,a=new r(s,o);try{for(a.open(),l=0,c=t.length;l<c;l++)u=t[l],a.represent(u);a.close()}finally{a.dispose()}return n||s.string}}).call(this)},function(e,t,n){var r,i,o;!function(n,a){i=[],r=a(),void 0!==(o="function"==typeof r?r.apply(t,i):r)&&(e.exports=o)}(0,function(){"use strict";var e=function(e){return"getComputedStyle"in window&&"smooth"===window.getComputedStyle(e)["scroll-behavior"]};if("undefined"==typeof window||!("document"in window))return{};var t=function(t,n,r){n=n||999,r||0===r||(r=9);var i,o=function(e){i=e},a=function(){clearTimeout(i),o(0)},s=function(e){return Math.max(0,t.getTopOf(e)-r)},u=function(r,i,s){if(a(),0===i||i&&i<0||e(t.body))t.toY(r),s&&s();else{var u=t.getY(),l=Math.max(0,r)-u,c=(new Date).getTime();i=i||Math.min(Math.abs(l),n),function e(){o(setTimeout(function(){var n=Math.min(1,((new Date).getTime()-c)/i),r=Math.max(0,Math.floor(u+l*(n<.5?2*n*n:n*(4-2*n)-1)));t.toY(r),n<1&&t.getHeight()+r<t.body.scrollHeight?e():(setTimeout(a,99),s&&s())},9))}()}},l=function(e,t,n){u(s(e),t,n)},c=function(e,n,i){var o=e.getBoundingClientRect().height,a=t.getTopOf(e)+o,c=t.getHeight(),p=t.getY(),f=p+c;s(e)<p||o+r>c?l(e,n,i):a+r>f?u(a-c+r,n,i):i&&i()},p=function(e,n,r,i){u(Math.max(0,t.getTopOf(e)-t.getHeight()/2+(r||e.getBoundingClientRect().height/2)),n,i)};return{setup:function(e,t){return(0===e||e)&&(n=e),(0===t||t)&&(r=t),{defaultDuration:n,edgeOffset:r}},to:l,toY:u,intoView:c,center:p,stop:a,moving:function(){return!!i},getY:t.getY,getTopOf:t.getTopOf}},n=document.documentElement,r=function(){return window.scrollY||n.scrollTop},i=t({body:document.scrollingElement||document.body,toY:function(e){window.scrollTo(0,e)},getY:r,getHeight:function(){return window.innerHeight||n.clientHeight},getTopOf:function(e){return e.getBoundingClientRect().top+r()-n.offsetTop}});if(i.createScroller=function(e,r,i){return t({body:e,toY:function(t){e.scrollTop=t},getY:function(){return e.scrollTop},getHeight:function(){return Math.min(e.clientHeight,window.innerHeight||n.clientHeight)},getTopOf:function(e){return e.offsetTop}},r,i)},"addEventListener"in window&&!window.noZensmooth&&!e(document.body)){var o="scrollRestoration"in history;o&&(history.scrollRestoration="auto"),window.addEventListener("load",function(){o&&(setTimeout(function(){history.scrollRestoration="manual"},9),window.addEventListener("popstate",function(e){e.state&&"zenscrollY"in e.state&&i.toY(e.state.zenscrollY)},!1)),window.location.hash&&setTimeout(function(){var e=i.setup().edgeOffset;if(e){var t=document.getElementById(window.location.href.split("#")[1]);if(t){var n=Math.max(0,i.getTopOf(t)-e),r=i.getY()-n;0<=r&&r<9&&window.scrollTo(0,n)}}},9)},!1);var a=new RegExp("(^|\\s)noZensmooth(\\s|$)");window.addEventListener("click",function(e){for(var t=e.target;t&&"A"!==t.tagName;)t=t.parentNode;if(!(!t||1!==e.which||e.shiftKey||e.metaKey||e.ctrlKey||e.altKey)){if(o)try{history.replaceState({zenscrollY:i.getY()},"")}catch(e){}var n=t.getAttribute("href")||"";if(0===n.indexOf("#")&&!a.test(t.className)){var r=0,s=document.getElementById(n.substring(1));if("#"!==n){if(!s)return;r=i.getTopOf(s)}e.preventDefault();var u=function(){window.location=n},l=i.setup().edgeOffset;l&&(r=Math.max(0,r-l),u=function(){history.pushState(null,"",n)}),i.toY(r,null,u)}}},!1)}return i})},function(e,t,n){function r(e){return n(i(e))}function i(e){var t=o[e];if(!(t+1))throw new Error("Cannot find module '"+e+"'.");return t}var o={"./all.js":271,"./ast/ast.js":272,"./ast/index.js":273,"./ast/jump-to-path.jsx":274,"./auth/actions.js":168,"./auth/index.js":275,"./auth/reducers.js":276,"./auth/selectors.js":277,"./auth/spec-wrap-actions.js":278,"./configs/actions.js":169,"./configs/index.js":279,"./configs/reducers.js":280,"./configs/selectors.js":281,"./deep-linking/helpers.js":282,"./deep-linking/index.js":283,"./deep-linking/layout-wrap-actions.js":284,"./deep-linking/spec-wrap-actions.js":285,"./download-url.js":286,"./err/actions.js":128,"./err/error-transformers/hook.js":287,"./err/error-transformers/transformers/not-of-type.js":288,"./err/error-transformers/transformers/parameter-oneof.js":289,"./err/error-transformers/transformers/strip-instance.js":290,"./err/index.js":291,"./err/reducers.js":292,"./err/selectors.js":293,"./layout/actions.js":170,"./layout/index.js":294,"./layout/reducers.js":295,"./layout/selectors.js":296,"./logs/index.js":297,"./oas3/actions.js":171,"./oas3/auth-extensions/wrap-selectors.js":298,"./oas3/components/callbacks.jsx":299,"./oas3/components/http-auth.jsx":300,"./oas3/components/index.js":301,"./oas3/components/operation-link.jsx":302,"./oas3/components/operation-servers.jsx":303,"./oas3/components/request-body-editor.jsx":304,"./oas3/components/request-body.jsx":305,"./oas3/components/servers.jsx":306,"./oas3/helpers.js":34,"./oas3/index.js":307,"./oas3/reducers.js":308,"./oas3/selectors.js":309,"./oas3/spec-extensions/selectors.js":310,"./oas3/spec-extensions/wrap-selectors.js":311,"./oas3/wrap-components/auth-item.jsx":312,"./oas3/wrap-components/index.js":313,"./oas3/wrap-components/markdown.js":314,"./oas3/wrap-components/model.jsx":315,"./oas3/wrap-components/online-validator-badge.js":316,"./oas3/wrap-components/parameters.jsx":317,"./oas3/wrap-components/version-stamp.jsx":318,"./samples/fn.js":172,"./samples/index.js":319,"./spec/actions.js":173,"./spec/index.js":320,"./spec/reducers.js":321,"./spec/selectors.js":322,"./spec/wrap-actions.js":323,"./split-pane-mode/components/split-pane-mode.jsx":324,"./split-pane-mode/index.js":325,"./swagger-js/index.js":326,"./util/index.js":327,"./view/index.js":328,"./view/root-injects.js":329};r.keys=function(){return Object.keys(o)},r.resolve=i,e.exports=r,r.id=1208},function(e,t){},function(e,t){},function(e,t){},function(e,t){},function(e,t,n){n(505),e.exports=n(504)}])}); -//# sourceMappingURL=swagger-ui-bundle.js.map \ No newline at end of file +//# sourceMappingURL=swagger-ui-bundle.js.map diff --git a/app/code/Magento/Swagger/view/frontend/web/swagger-ui/js/swagger-ui-standalone-preset.min.js b/app/code/Magento/Swagger/view/frontend/web/swagger-ui/js/swagger-ui-standalone-preset.min.js index b24fd08faf24..cd5a1d085991 100644 --- a/app/code/Magento/Swagger/view/frontend/web/swagger-ui/js/swagger-ui-standalone-preset.min.js +++ b/app/code/Magento/Swagger/view/frontend/web/swagger-ui/js/swagger-ui-standalone-preset.min.js @@ -10,4 +10,4 @@ var i=Object.getOwnPropertySymbols,o=Object.prototype.hasOwnProperty,s=Object.pr * @license MIT */ var V=n(134),$=n(232),Z=n(234);e.Buffer=o,e.SlowBuffer=m,e.INSPECT_MAX_BYTES=50,o.TYPED_ARRAY_SUPPORT=void 0!==t.TYPED_ARRAY_SUPPORT?t.TYPED_ARRAY_SUPPORT:function(){try{var t=new Uint8Array(1);return t.__proto__={__proto__:Uint8Array.prototype,foo:function(){return 42}},42===t.foo()&&"function"==typeof t.subarray&&0===t.subarray(1,1).byteLength}catch(t){return!1}}(),e.kMaxLength=r(),o.poolSize=8192,o._augment=function(t){return t.__proto__=o.prototype,t},o.from=function(t,e,n){return s(null,t,e,n)},o.TYPED_ARRAY_SUPPORT&&(o.prototype.__proto__=Uint8Array.prototype,o.__proto__=Uint8Array,"undefined"!=typeof Symbol&&Symbol.species&&o[Symbol.species]===o&&Object.defineProperty(o,Symbol.species,{value:null,configurable:!0})),o.alloc=function(t,e,n){return u(null,t,e,n)},o.allocUnsafe=function(t){return c(null,t)},o.allocUnsafeSlow=function(t){return c(null,t)},o.isBuffer=function(t){return!(null==t||!t._isBuffer)},o.compare=function(t,e){if(!o.isBuffer(t)||!o.isBuffer(e))throw new TypeError("Arguments must be Buffers");if(t===e)return 0;for(var n=t.length,r=e.length,i=0,s=Math.min(n,r);i<s;++i)if(t[i]!==e[i]){n=t[i],r=e[i];break}return n<r?-1:r<n?1:0},o.isEncoding=function(t){switch(String(t).toLowerCase()){case"hex":case"utf8":case"utf-8":case"ascii":case"latin1":case"binary":case"base64":case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return!0;default:return!1}},o.concat=function(t,e){if(!Z(t))throw new TypeError('"list" argument must be an Array of Buffers');if(0===t.length)return o.alloc(0);var n;if(void 0===e)for(e=0,n=0;n<t.length;++n)e+=t[n].length;var r=o.allocUnsafe(e),i=0;for(n=0;n<t.length;++n){var s=t[n];if(!o.isBuffer(s))throw new TypeError('"list" argument must be an Array of Buffers');s.copy(r,i),i+=s.length}return r},o.byteLength=y,o.prototype._isBuffer=!0,o.prototype.swap16=function(){var t=this.length;if(t%2!=0)throw new RangeError("Buffer size must be a multiple of 16-bits");for(var e=0;e<t;e+=2)x(this,e,e+1);return this},o.prototype.swap32=function(){var t=this.length;if(t%4!=0)throw new RangeError("Buffer size must be a multiple of 32-bits");for(var e=0;e<t;e+=4)x(this,e,e+3),x(this,e+1,e+2);return this},o.prototype.swap64=function(){var t=this.length;if(t%8!=0)throw new RangeError("Buffer size must be a multiple of 64-bits");for(var e=0;e<t;e+=8)x(this,e,e+7),x(this,e+1,e+6),x(this,e+2,e+5),x(this,e+3,e+4);return this},o.prototype.toString=function(){var t=0|this.length;return 0===t?"":0===arguments.length?F(this,0,t):v.apply(this,arguments)},o.prototype.equals=function(t){if(!o.isBuffer(t))throw new TypeError("Argument must be a Buffer");return this===t||0===o.compare(this,t)},o.prototype.inspect=function(){var t="",n=e.INSPECT_MAX_BYTES;return this.length>0&&(t=this.toString("hex",0,n).match(/.{2}/g).join(" "),this.length>n&&(t+=" ... ")),"<Buffer "+t+">"},o.prototype.compare=function(t,e,n,r,i){if(!o.isBuffer(t))throw new TypeError("Argument must be a Buffer");if(void 0===e&&(e=0),void 0===n&&(n=t?t.length:0),void 0===r&&(r=0),void 0===i&&(i=this.length),e<0||n>t.length||r<0||i>this.length)throw new RangeError("out of range index");if(r>=i&&e>=n)return 0;if(r>=i)return-1;if(e>=n)return 1;if(e>>>=0,n>>>=0,r>>>=0,i>>>=0,this===t)return 0;for(var s=i-r,a=n-e,u=Math.min(s,a),c=this.slice(r,i),h=t.slice(e,n),l=0;l<u;++l)if(c[l]!==h[l]){s=c[l],a=h[l];break}return s<a?-1:a<s?1:0},o.prototype.includes=function(t,e,n){return-1!==this.indexOf(t,e,n)},o.prototype.indexOf=function(t,e,n){return g(this,t,e,n,!0)},o.prototype.lastIndexOf=function(t,e,n){return g(this,t,e,n,!1)},o.prototype.write=function(t,e,n,r){if(void 0===e)r="utf8",n=this.length,e=0;else if(void 0===n&&"string"==typeof e)r=e,n=this.length,e=0;else{if(!isFinite(e))throw new Error("Buffer.write(string, encoding, offset[, length]) is no longer supported");e|=0,isFinite(n)?(n|=0,void 0===r&&(r="utf8")):(r=n,n=void 0)}var i=this.length-e;if((void 0===n||n>i)&&(n=i),t.length>0&&(n<0||e<0)||e>this.length)throw new RangeError("Attempt to write outside buffer bounds");r||(r="utf8");for(var o=!1;;)switch(r){case"hex":return E(this,t,e,n);case"utf8":case"utf-8":return A(this,t,e,n);case"ascii":return S(this,t,e,n);case"latin1":case"binary":return w(this,t,e,n);case"base64":return C(this,t,e,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return _(this,t,e,n);default:if(o)throw new TypeError("Unknown encoding: "+r);r=(""+r).toLowerCase(),o=!0}},o.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};var Q=4096;o.prototype.slice=function(t,e){var n=this.length;t=~~t,e=void 0===e?n:~~e,t<0?(t+=n)<0&&(t=0):t>n&&(t=n),e<0?(e+=n)<0&&(e=0):e>n&&(e=n),e<t&&(e=t);var r;if(o.TYPED_ARRAY_SUPPORT)r=this.subarray(t,e),r.__proto__=o.prototype;else{var i=e-t;r=new o(i,void 0);for(var s=0;s<i;++s)r[s]=this[s+t]}return r},o.prototype.readUIntLE=function(t,e,n){t|=0,e|=0,n||P(t,e,this.length);for(var r=this[t],i=1,o=0;++o<e&&(i*=256);)r+=this[t+o]*i;return r},o.prototype.readUIntBE=function(t,e,n){t|=0,e|=0,n||P(t,e,this.length);for(var r=this[t+--e],i=1;e>0&&(i*=256);)r+=this[t+--e]*i;return r},o.prototype.readUInt8=function(t,e){return e||P(t,1,this.length),this[t]},o.prototype.readUInt16LE=function(t,e){return e||P(t,2,this.length),this[t]|this[t+1]<<8},o.prototype.readUInt16BE=function(t,e){return e||P(t,2,this.length),this[t]<<8|this[t+1]},o.prototype.readUInt32LE=function(t,e){return e||P(t,4,this.length),(this[t]|this[t+1]<<8|this[t+2]<<16)+16777216*this[t+3]},o.prototype.readUInt32BE=function(t,e){return e||P(t,4,this.length),16777216*this[t]+(this[t+1]<<16|this[t+2]<<8|this[t+3])},o.prototype.readIntLE=function(t,e,n){t|=0,e|=0,n||P(t,e,this.length);for(var r=this[t],i=1,o=0;++o<e&&(i*=256);)r+=this[t+o]*i;return i*=128,r>=i&&(r-=Math.pow(2,8*e)),r},o.prototype.readIntBE=function(t,e,n){t|=0,e|=0,n||P(t,e,this.length);for(var r=e,i=1,o=this[t+--r];r>0&&(i*=256);)o+=this[t+--r]*i;return i*=128,o>=i&&(o-=Math.pow(2,8*e)),o},o.prototype.readInt8=function(t,e){return e||P(t,1,this.length),128&this[t]?-1*(255-this[t]+1):this[t]},o.prototype.readInt16LE=function(t,e){e||P(t,2,this.length);var n=this[t]|this[t+1]<<8;return 32768&n?4294901760|n:n},o.prototype.readInt16BE=function(t,e){e||P(t,2,this.length);var n=this[t+1]|this[t]<<8;return 32768&n?4294901760|n:n},o.prototype.readInt32LE=function(t,e){return e||P(t,4,this.length),this[t]|this[t+1]<<8|this[t+2]<<16|this[t+3]<<24},o.prototype.readInt32BE=function(t,e){return e||P(t,4,this.length),this[t]<<24|this[t+1]<<16|this[t+2]<<8|this[t+3]},o.prototype.readFloatLE=function(t,e){return e||P(t,4,this.length),$.read(this,t,!0,23,4)},o.prototype.readFloatBE=function(t,e){return e||P(t,4,this.length),$.read(this,t,!1,23,4)},o.prototype.readDoubleLE=function(t,e){return e||P(t,8,this.length),$.read(this,t,!0,52,8)},o.prototype.readDoubleBE=function(t,e){return e||P(t,8,this.length),$.read(this,t,!1,52,8)},o.prototype.writeUIntLE=function(t,e,n,r){if(t=+t,e|=0,n|=0,!r){N(this,t,e,n,Math.pow(2,8*n)-1,0)}var i=1,o=0;for(this[e]=255&t;++o<n&&(i*=256);)this[e+o]=t/i&255;return e+n},o.prototype.writeUIntBE=function(t,e,n,r){if(t=+t,e|=0,n|=0,!r){N(this,t,e,n,Math.pow(2,8*n)-1,0)}var i=n-1,o=1;for(this[e+i]=255&t;--i>=0&&(o*=256);)this[e+i]=t/o&255;return e+n},o.prototype.writeUInt8=function(t,e,n){return t=+t,e|=0,n||N(this,t,e,1,255,0),o.TYPED_ARRAY_SUPPORT||(t=Math.floor(t)),this[e]=255&t,e+1},o.prototype.writeUInt16LE=function(t,e,n){return t=+t,e|=0,n||N(this,t,e,2,65535,0),o.TYPED_ARRAY_SUPPORT?(this[e]=255&t,this[e+1]=t>>>8):O(this,t,e,!0),e+2},o.prototype.writeUInt16BE=function(t,e,n){return t=+t,e|=0,n||N(this,t,e,2,65535,0),o.TYPED_ARRAY_SUPPORT?(this[e]=t>>>8,this[e+1]=255&t):O(this,t,e,!1),e+2},o.prototype.writeUInt32LE=function(t,e,n){return t=+t,e|=0,n||N(this,t,e,4,4294967295,0),o.TYPED_ARRAY_SUPPORT?(this[e+3]=t>>>24,this[e+2]=t>>>16,this[e+1]=t>>>8,this[e]=255&t):R(this,t,e,!0),e+4},o.prototype.writeUInt32BE=function(t,e,n){return t=+t,e|=0,n||N(this,t,e,4,4294967295,0),o.TYPED_ARRAY_SUPPORT?(this[e]=t>>>24,this[e+1]=t>>>16,this[e+2]=t>>>8,this[e+3]=255&t):R(this,t,e,!1),e+4},o.prototype.writeIntLE=function(t,e,n,r){if(t=+t,e|=0,!r){var i=Math.pow(2,8*n-1);N(this,t,e,n,i-1,-i)}var o=0,s=1,a=0;for(this[e]=255&t;++o<n&&(s*=256);)t<0&&0===a&&0!==this[e+o-1]&&(a=1),this[e+o]=(t/s>>0)-a&255;return e+n},o.prototype.writeIntBE=function(t,e,n,r){if(t=+t,e|=0,!r){var i=Math.pow(2,8*n-1);N(this,t,e,n,i-1,-i)}var o=n-1,s=1,a=0;for(this[e+o]=255&t;--o>=0&&(s*=256);)t<0&&0===a&&0!==this[e+o+1]&&(a=1),this[e+o]=(t/s>>0)-a&255;return e+n},o.prototype.writeInt8=function(t,e,n){return t=+t,e|=0,n||N(this,t,e,1,127,-128),o.TYPED_ARRAY_SUPPORT||(t=Math.floor(t)),t<0&&(t=255+t+1),this[e]=255&t,e+1},o.prototype.writeInt16LE=function(t,e,n){return t=+t,e|=0,n||N(this,t,e,2,32767,-32768),o.TYPED_ARRAY_SUPPORT?(this[e]=255&t,this[e+1]=t>>>8):O(this,t,e,!0),e+2},o.prototype.writeInt16BE=function(t,e,n){return t=+t,e|=0,n||N(this,t,e,2,32767,-32768),o.TYPED_ARRAY_SUPPORT?(this[e]=t>>>8,this[e+1]=255&t):O(this,t,e,!1),e+2},o.prototype.writeInt32LE=function(t,e,n){return t=+t,e|=0,n||N(this,t,e,4,2147483647,-2147483648),o.TYPED_ARRAY_SUPPORT?(this[e]=255&t,this[e+1]=t>>>8,this[e+2]=t>>>16,this[e+3]=t>>>24):R(this,t,e,!0),e+4},o.prototype.writeInt32BE=function(t,e,n){return t=+t,e|=0,n||N(this,t,e,4,2147483647,-2147483648),t<0&&(t=4294967295+t+1),o.TYPED_ARRAY_SUPPORT?(this[e]=t>>>24,this[e+1]=t>>>16,this[e+2]=t>>>8,this[e+3]=255&t):R(this,t,e,!1),e+4},o.prototype.writeFloatLE=function(t,e,n){return j(this,t,e,!0,n)},o.prototype.writeFloatBE=function(t,e,n){return j(this,t,e,!1,n)},o.prototype.writeDoubleLE=function(t,e,n){return L(this,t,e,!0,n)},o.prototype.writeDoubleBE=function(t,e,n){return L(this,t,e,!1,n)},o.prototype.copy=function(t,e,n,r){if(n||(n=0),r||0===r||(r=this.length),e>=t.length&&(e=t.length),e||(e=0),r>0&&r<n&&(r=n),r===n)return 0;if(0===t.length||0===this.length)return 0;if(e<0)throw new RangeError("targetStart out of bounds");if(n<0||n>=this.length)throw new RangeError("sourceStart out of bounds");if(r<0)throw new RangeError("sourceEnd out of bounds");r>this.length&&(r=this.length),t.length-e<r-n&&(r=t.length-e+n);var i,s=r-n;if(this===t&&n<e&&e<r)for(i=s-1;i>=0;--i)t[i+e]=this[i+n];else if(s<1e3||!o.TYPED_ARRAY_SUPPORT)for(i=0;i<s;++i)t[i+e]=this[i+n];else Uint8Array.prototype.set.call(t,this.subarray(n,n+s),e);return s},o.prototype.fill=function(t,e,n,r){if("string"==typeof t){if("string"==typeof e?(r=e,e=0,n=this.length):"string"==typeof n&&(r=n,n=this.length),1===t.length){var i=t.charCodeAt(0);i<256&&(t=i)}if(void 0!==r&&"string"!=typeof r)throw new TypeError("encoding must be a string");if("string"==typeof r&&!o.isEncoding(r))throw new TypeError("Unknown encoding: "+r)}else"number"==typeof t&&(t&=255);if(e<0||this.length<e||this.length<n)throw new RangeError("Out of range index");if(n<=e)return this;e>>>=0,n=void 0===n?this.length:n>>>0,t||(t=0);var s;if("number"==typeof t)for(s=e;s<n;++s)this[s]=t;else{var a=o.isBuffer(t)?t:q(new o(t,r).toString()),u=a.length;for(s=0;s<n-e;++s)this[s+e]=a[s%u]}return this};var tt=/[^+\/0-9A-Za-z-_]/g}).call(e,n(274))},function(t,e,n){n(215),n(219),n(226),n(108),n(210),n(211),n(216),n(220),n(222),n(206),n(207),n(208),n(209),n(212),n(213),n(214),n(217),n(218),n(221),n(223),n(224),n(225),n(202),n(203),n(204),n(205),t.exports=n(13).String},function(t,e,n){n(200),n(108),n(229),n(201),n(227),n(228),t.exports=n(13).Promise},function(t,e,n){n(93),n(92),t.exports=n(164)},function(t,e,n){n(166);var r=n(5).Object;t.exports=function(t,e){return r.create(t,e)}},function(t,e,n){n(167);var r=n(5).Object;t.exports=function(t,e,n){return r.defineProperty(t,e,n)}},function(t,e,n){n(168),t.exports=n(5).Object.getPrototypeOf},function(t,e,n){n(169),t.exports=n(5).Object.setPrototypeOf},function(t,e,n){n(171),n(170),n(172),n(173),t.exports=n(5).Symbol},function(t,e,n){n(92),n(93),t.exports=n(62).f("iterator")},function(t,e){t.exports=function(t){if("function"!=typeof t)throw TypeError(t+" is not a function!");return t}},function(t,e){t.exports=function(){}},function(t,e,n){var r=n(20),i=n(162),o=n(161);t.exports=function(t){return function(e,n,s){var a,u=r(e),c=i(u.length),h=o(s,c);if(t&&n!=n){for(;c>h;)if((a=u[h++])!=a)return!0}else for(;c>h;h++)if((t||h in u)&&u[h]===n)return t||h||0;return!t&&-1}}},function(t,e,n){var r=n(49),i=n(7)("toStringTag"),o="Arguments"==r(function(){return arguments}()),s=function(t,e){try{return t[e]}catch(t){}};t.exports=function(t){var e,n,a;return void 0===t?"Undefined":null===t?"Null":"string"==typeof(n=s(e=Object(t),i))?n:o?r(e):"Object"==(a=r(e))&&"function"==typeof e.callee?"Arguments":a}},function(t,e,n){var r=n(54),i=n(87),o=n(55);t.exports=function(t){var e=r(t),n=i.f;if(n)for(var s,a=n(t),u=o.f,c=0;a.length>c;)u.call(t,s=a[c++])&&e.push(s);return e}},function(t,e,n){var r=n(6).document;t.exports=r&&r.documentElement},function(t,e,n){var r=n(49);t.exports=Object("z").propertyIsEnumerable(0)?Object:function(t){return"String"==r(t)?t.split(""):Object(t)}},function(t,e,n){var r=n(49);t.exports=Array.isArray||function(t){return"Array"==r(t)}},function(t,e,n){"use strict";var r=n(53),i=n(37),o=n(56),s={};n(18)(s,n(7)("iterator"),function(){return this}),t.exports=function(t,e,n){t.prototype=r(s,{next:i(1,n)}),o(t,e+" Iterator")}},function(t,e){t.exports=function(t,e){return{value:e,done:!!t}}},function(t,e,n){var r=n(38)("meta"),i=n(19),o=n(10),s=n(11).f,a=0,u=Object.isExtensible||function(){return!0},c=!n(26)(function(){return u(Object.preventExtensions({}))}),h=function(t){s(t,r,{value:{i:"O"+ ++a,w:{}}})},l=function(t,e){if(!i(t))return"symbol"==typeof t?t:("string"==typeof t?"S":"P")+t;if(!o(t,r)){if(!u(t))return"F";if(!e)return"E";h(t)}return t[r].i},p=function(t,e){if(!o(t,r)){if(!u(t))return!0;if(!e)return!1;h(t)}return t[r].w},f=function(t){return c&&d.NEED&&u(t)&&!o(t,r)&&h(t),t},d=t.exports={KEY:r,NEED:!1,fastKey:l,getWeak:p,onFreeze:f}},function(t,e,n){var r=n(11),i=n(16),o=n(54);t.exports=n(9)?Object.defineProperties:function(t,e){i(t);for(var n,s=o(e),a=s.length,u=0;a>u;)r.f(t,n=s[u++],e[n]);return t}},function(t,e,n){var r=n(20),i=n(86).f,o={}.toString,s="object"==typeof window&&window&&Object.getOwnPropertyNames?Object.getOwnPropertyNames(window):[],a=function(t){try{return i(t)}catch(t){return s.slice()}};t.exports.f=function(t){return s&&"[object Window]"==o.call(t)?a(t):i(r(t))}},function(t,e,n){var r=n(17),i=n(5),o=n(26);t.exports=function(t,e){var n=(i.Object||{})[t]||Object[t],s={};s[t]=e(n),r(r.S+r.F*o(function(){n(1)}),"Object",s)}},function(t,e,n){var r=n(19),i=n(16),o=function(t,e){if(i(t),!r(e)&&null!==e)throw TypeError(e+": can't set as prototype!")};t.exports={set:Object.setPrototypeOf||("__proto__"in{}?function(t,e,r){try{r=n(81)(Function.call,n(85).f(Object.prototype,"__proto__").set,2),r(t,[]),e=!(t instanceof Array)}catch(t){e=!0}return function(t,n){return o(t,n),e?t.__proto__=n:r(t,n),t}}({},!1):void 0),check:o}},function(t,e,n){var r=n(59),i=n(50);t.exports=function(t){return function(e,n){var o,s,a=String(i(e)),u=r(n),c=a.length;return u<0||u>=c?t?"":void 0:(o=a.charCodeAt(u),o<55296||o>56319||u+1===c||(s=a.charCodeAt(u+1))<56320||s>57343?t?a.charAt(u):o:t?a.slice(u,u+2):s-56320+(o-55296<<10)+65536)}}},function(t,e,n){var r=n(59),i=Math.max,o=Math.min;t.exports=function(t,e){return t=r(t),t<0?i(t+e,0):o(t,e)}},function(t,e,n){var r=n(59),i=Math.min;t.exports=function(t){return t>0?i(r(t),9007199254740991):0}},function(t,e,n){var r=n(148),i=n(7)("iterator"),o=n(36);t.exports=n(5).getIteratorMethod=function(t){if(void 0!=t)return t[i]||t["@@iterator"]||o[r(t)]}},function(t,e,n){var r=n(16),i=n(163);t.exports=n(5).getIterator=function(t){var e=i(t);if("function"!=typeof e)throw TypeError(t+" is not iterable!");return r(e.call(t))}},function(t,e,n){"use strict";var r=n(146),i=n(154),o=n(36),s=n(20);t.exports=n(84)(Array,"Array",function(t,e){this._t=s(t),this._i=0,this._k=e},function(){var t=this._t,e=this._k,n=this._i++;return!t||n>=t.length?(this._t=void 0,i(1)):"keys"==e?i(0,n):"values"==e?i(0,t[n]):i(0,[n,t[n]])},"values"),o.Arguments=o.Array,r("keys"),r("values"),r("entries")},function(t,e,n){var r=n(17);r(r.S,"Object",{create:n(53)})},function(t,e,n){var r=n(17);r(r.S+r.F*!n(9),"Object",{defineProperty:n(11).f})},function(t,e,n){var r=n(91),i=n(88);n(158)("getPrototypeOf",function(){return function(t){return i(r(t))}})},function(t,e,n){var r=n(17);r(r.S,"Object",{setPrototypeOf:n(159).set})},function(t,e){},function(t,e,n){"use strict";var r=n(6),i=n(10),o=n(9),s=n(17),a=n(90),u=n(155).KEY,c=n(26),h=n(58),l=n(56),p=n(38),f=n(7),d=n(62),m=n(61),y=n(149),v=n(152),x=n(16),g=n(19),D=n(20),E=n(60),A=n(37),S=n(53),w=n(157),C=n(85),_=n(11),b=n(54),F=C.f,k=_.f,I=w.f,T=r.Symbol,B=r.JSON,M=B&&B.stringify,P=f("_hidden"),N=f("toPrimitive"),O={}.propertyIsEnumerable,R=h("symbol-registry"),U=h("symbols"),j=h("op-symbols"),L=Object.prototype,z="function"==typeof T,J=r.QObject,X=!J||!J.prototype||!J.prototype.findChild,q=o&&c(function(){return 7!=S(k({},"a",{get:function(){return k(this,"a",{value:7}).a}})).a})?function(t,e,n){var r=F(L,e);r&&delete L[e],k(t,e,n),r&&t!==L&&k(L,e,r)}:k,K=function(t){var e=U[t]=S(T.prototype);return e._k=t,e},Y=z&&"symbol"==typeof T.iterator?function(t){return"symbol"==typeof t}:function(t){return t instanceof T},W=function(t,e,n){return t===L&&W(j,e,n),x(t),e=E(e,!0),x(n),i(U,e)?(n.enumerable?(i(t,P)&&t[P][e]&&(t[P][e]=!1),n=S(n,{enumerable:A(0,!1)})):(i(t,P)||k(t,P,A(1,{})),t[P][e]=!0),q(t,e,n)):k(t,e,n)},G=function(t,e){x(t);for(var n,r=y(e=D(e)),i=0,o=r.length;o>i;)W(t,n=r[i++],e[n]);return t},H=function(t,e){return void 0===e?S(t):G(S(t),e)},V=function(t){var e=O.call(this,t=E(t,!0));return!(this===L&&i(U,t)&&!i(j,t))&&(!(e||!i(this,t)||!i(U,t)||i(this,P)&&this[P][t])||e)},$=function(t,e){if(t=D(t),e=E(e,!0),t!==L||!i(U,e)||i(j,e)){var n=F(t,e);return!n||!i(U,e)||i(t,P)&&t[P][e]||(n.enumerable=!0),n}},Z=function(t){for(var e,n=I(D(t)),r=[],o=0;n.length>o;)i(U,e=n[o++])||e==P||e==u||r.push(e);return r},Q=function(t){for(var e,n=t===L,r=I(n?j:D(t)),o=[],s=0;r.length>s;)!i(U,e=r[s++])||n&&!i(L,e)||o.push(U[e]);return o};z||(T=function(){if(this instanceof T)throw TypeError("Symbol is not a constructor!");var t=p(arguments.length>0?arguments[0]:void 0),e=function(n){this===L&&e.call(j,n),i(this,P)&&i(this[P],t)&&(this[P][t]=!1),q(this,t,A(1,n))};return o&&X&&q(L,t,{configurable:!0,set:e}),K(t)},a(T.prototype,"toString",function(){return this._k}),C.f=$,_.f=W,n(86).f=w.f=Z,n(55).f=V,n(87).f=Q,o&&!n(52)&&a(L,"propertyIsEnumerable",V,!0),d.f=function(t){return K(f(t))}),s(s.G+s.W+s.F*!z,{Symbol:T});for(var tt="hasInstance,isConcatSpreadable,iterator,match,replace,search,species,split,toPrimitive,toStringTag,unscopables".split(","),et=0;tt.length>et;)f(tt[et++]);for(var nt=b(f.store),rt=0;nt.length>rt;)m(nt[rt++]);s(s.S+s.F*!z,"Symbol",{for:function(t){return i(R,t+="")?R[t]:R[t]=T(t)},keyFor:function(t){if(!Y(t))throw TypeError(t+" is not a symbol!");for(var e in R)if(R[e]===t)return e},useSetter:function(){X=!0},useSimple:function(){X=!1}}),s(s.S+s.F*!z,"Object",{create:H,defineProperty:W,defineProperties:G,getOwnPropertyDescriptor:$,getOwnPropertyNames:Z,getOwnPropertySymbols:Q}),B&&s(s.S+s.F*(!z||c(function(){var t=T();return"[null]"!=M([t])||"{}"!=M({a:t})||"{}"!=M(Object(t))})),"JSON",{stringify:function(t){for(var e,n,r=[t],i=1;arguments.length>i;)r.push(arguments[i++]);if(n=e=r[1],(g(e)||void 0!==t)&&!Y(t))return v(e)||(e=function(t,e){if("function"==typeof n&&(e=n.call(this,t,e)),!Y(e))return e}),r[1]=e,M.apply(B,r)}}),T.prototype[N]||n(18)(T.prototype,N,T.prototype.valueOf),l(T,"Symbol"),l(Math,"Math",!0),l(r.JSON,"JSON",!0)},function(t,e,n){n(61)("asyncIterator")},function(t,e,n){n(61)("observable")},function(t,e,n){var r=n(1)("unscopables"),i=Array.prototype;void 0==i[r]&&n(14)(i,r,{}),t.exports=function(t){i[r][t]=!0}},function(t,e){t.exports=function(t,e,n,r){if(!(t instanceof e)||void 0!==r&&r in t)throw TypeError(n+": incorrect invocation!");return t}},function(t,e,n){var r=n(44),i=n(32),o=n(107);t.exports=function(t){return function(e,n,s){var a,u=r(e),c=i(u.length),h=o(s,c);if(t&&n!=n){for(;c>h;)if((a=u[h++])!=a)return!0}else for(;c>h;h++)if((t||h in u)&&u[h]===n)return t||h||0;return!t&&-1}}},function(t,e,n){var r=n(40),i=n(182),o=n(181),s=n(12),a=n(32),u=n(198),c={},h={},e=t.exports=function(t,e,n,l,p){var f,d,m,y,v=p?function(){return t}:u(t),x=r(n,l,e?2:1),g=0;if("function"!=typeof v)throw TypeError(t+" is not iterable!");if(o(v)){for(f=a(t.length);f>g;g++)if((y=e?x(s(d=t[g])[0],d[1]):x(t[g]))===c||y===h)return y}else for(m=v.call(t);!(d=m.next()).done;)if((y=i(m,x,d.value,e))===c||y===h)return y};e.BREAK=c,e.RETURN=h},function(t,e,n){t.exports=!n(28)&&!n(29)(function(){return 7!=Object.defineProperty(n(64)("div"),"a",{get:function(){return 7}}).a})},function(t,e){t.exports=function(t,e,n){var r=void 0===n;switch(e.length){case 0:return r?t():t.call(n);case 1:return r?t(e[0]):t.call(n,e[0]);case 2:return r?t(e[0],e[1]):t.call(n,e[0],e[1]);case 3:return r?t(e[0],e[1],e[2]):t.call(n,e[0],e[1],e[2]);case 4:return r?t(e[0],e[1],e[2],e[3]):t.call(n,e[0],e[1],e[2],e[3])}return t.apply(n,e)}},function(t,e,n){var r=n(27);t.exports=Object("z").propertyIsEnumerable(0)?Object:function(t){return"String"==r(t)?t.split(""):Object(t)}},function(t,e,n){var r=n(31),i=n(1)("iterator"),o=Array.prototype;t.exports=function(t){return void 0!==t&&(r.Array===t||o[i]===t)}},function(t,e,n){var r=n(12);t.exports=function(t,e,n,i){try{return i?e(r(n)[0],n[1]):e(n)}catch(e){var o=t.return;throw void 0!==o&&r(o.call(t)),e}}},function(t,e,n){"use strict";var r=n(187),i=n(102),o=n(67),s={};n(14)(s,n(1)("iterator"),function(){return this}),t.exports=function(t,e,n){t.prototype=r(s,{next:i(1,n)}),o(t,e+" Iterator")}},function(t,e,n){var r=n(1)("iterator"),i=!1;try{var o=[7][r]();o.return=function(){i=!0},Array.from(o,function(){throw 2})}catch(t){}t.exports=function(t,e){if(!e&&!i)return!1;var n=!1;try{var o=[7],s=o[r]();s.next=function(){return{done:n=!0}},o[r]=function(){return s},t(o)}catch(t){}return n}},function(t,e){t.exports=function(t,e){return{value:e,done:!!t}}},function(t,e,n){var r=n(4),i=n(106).set,o=r.MutationObserver||r.WebKitMutationObserver,s=r.process,a=r.Promise,u="process"==n(27)(s);t.exports=function(){var t,e,n,c=function(){var r,i;for(u&&(r=s.domain)&&r.exit();t;){i=t.fn,t=t.next;try{i()}catch(r){throw t?n():e=void 0,r}}e=void 0,r&&r.enter()};if(u)n=function(){s.nextTick(c)};else if(!o||r.navigator&&r.navigator.standalone)if(a&&a.resolve){var h=a.resolve();n=function(){h.then(c)}}else n=function(){i.call(r,c)};else{var l=!0,p=document.createTextNode("");new o(c).observe(p,{characterData:!0}),n=function(){p.data=l=!l}}return function(r){var i={fn:r,next:void 0};e&&(e.next=i),t||(t=i,n()),e=i}}},function(t,e,n){var r=n(12),i=n(188),o=n(94),s=n(68)("IE_PROTO"),a=function(){},u=function(){var t,e=n(64)("iframe"),r=o.length;for(e.style.display="none",n(95).appendChild(e),e.src="javascript:",t=e.contentWindow.document,t.open(),t.write("<script>document.F=Object<\/script>"),t.close(),u=t.F;r--;)delete u.prototype[o[r]];return u()};t.exports=Object.create||function(t,e){var n;return null!==t?(a.prototype=r(t),n=new a,a.prototype=null,n[s]=t):n=u(),void 0===e?n:i(n,e)}},function(t,e,n){var r=n(42),i=n(12),o=n(99);t.exports=n(28)?Object.defineProperties:function(t,e){i(t);for(var n,s=o(e),a=s.length,u=0;a>u;)r.f(t,n=s[u++],e[n]);return t}},function(t,e,n){var r=n(30),i=n(196),o=n(68)("IE_PROTO"),s=Object.prototype;t.exports=Object.getPrototypeOf||function(t){return t=i(t),r(t,o)?t[o]:"function"==typeof t.constructor&&t instanceof t.constructor?t.constructor.prototype:t instanceof Object?s:null}},function(t,e,n){var r=n(30),i=n(44),o=n(176)(!1),s=n(68)("IE_PROTO");t.exports=function(t,e){var n,a=i(t),u=0,c=[];for(n in a)n!=s&&r(a,n)&&c.push(n);for(;e.length>u;)r(a,n=e[u++])&&(~o(c,n)||c.push(n));return c}},function(t,e,n){var r=n(22);t.exports=function(t,e,n){for(var i in e)r(t,i,e[i],n);return t}},function(t,e,n){"use strict";var r=n(4),i=n(42),o=n(28),s=n(1)("species");t.exports=function(t){var e=r[t];o&&e&&!e[s]&&i.f(e,s,{configurable:!0,get:function(){return this}})}},function(t,e,n){"use strict";var r=n(43),i=n(8);t.exports=function(t){var e=String(i(this)),n="",o=r(t);if(o<0||o==1/0)throw RangeError("Count can't be negative");for(;o>0;(o>>>=1)&&(e+=e))1&o&&(n+=e);return n}},function(t,e,n){var r=n(2),i=n(8),o=n(29),s=n(195),a="["+s+"]",u="​…",c=RegExp("^"+a+a+"*"),h=RegExp(a+a+"*$"),l=function(t,e,n){var i={},a=o(function(){return!!s[t]()||u[t]()!=u}),c=i[t]=a?e(p):s[t];n&&(i[n]=c),r(r.P+r.F*a,"String",i)},p=l.trim=function(t,e){return t=String(i(t)),1&e&&(t=t.replace(c,"")),2&e&&(t=t.replace(h,"")),t};t.exports=l},function(t,e){t.exports="\t\n\v\f\r   ᠎              \u2028\u2029\ufeff"},function(t,e,n){var r=n(8);t.exports=function(t){return Object(r(t))}},function(t,e,n){var r=n(21);t.exports=function(t,e){if(!r(t))return t;var n,i;if(e&&"function"==typeof(n=t.toString)&&!r(i=n.call(t)))return i;if("function"==typeof(n=t.valueOf)&&!r(i=n.call(t)))return i;if(!e&&"function"==typeof(n=t.toString)&&!r(i=n.call(t)))return i;throw TypeError("Can't convert object to primitive value")}},function(t,e,n){var r=n(63),i=n(1)("iterator"),o=n(31);t.exports=n(13).getIteratorMethod=function(t){if(void 0!=t)return t[i]||t["@@iterator"]||o[r(t)]}},function(t,e,n){"use strict";var r=n(174),i=n(185),o=n(31),s=n(44);t.exports=n(97)(Array,"Array",function(t,e){this._t=s(t),this._i=0,this._k=e},function(){var t=this._t,e=this._k,n=this._i++;return!t||n>=t.length?(this._t=void 0,i(1)):"keys"==e?i(0,n):"values"==e?i(0,t[n]):i(0,[n,t[n]])},"values"),o.Arguments=o.Array,r("keys"),r("values"),r("entries")},function(t,e,n){"use strict";var r=n(63),i={};i[n(1)("toStringTag")]="z",i+""!="[object z]"&&n(22)(Object.prototype,"toString",function(){return"[object "+r(this)+"]"},!0)},function(t,e,n){"use strict";var r,i,o,s,a=n(98),u=n(4),c=n(40),h=n(63),l=n(2),p=n(21),f=n(39),d=n(175),m=n(177),y=n(104),v=n(106).set,x=n(186)(),g=n(66),D=n(100),E=n(101),A=u.TypeError,S=u.process,w=u.Promise,C="process"==h(S),_=function(){},b=i=g.f,F=!!function(){try{var t=w.resolve(1),e=(t.constructor={})[n(1)("species")]=function(t){t(_,_)};return(C||"function"==typeof PromiseRejectionEvent)&&t.then(_)instanceof e}catch(t){}}(),k=function(t){var e;return!(!p(t)||"function"!=typeof(e=t.then))&&e},I=function(t,e){if(!t._n){t._n=!0;var n=t._c;x(function(){for(var r=t._v,i=1==t._s,o=0;n.length>o;)!function(e){var n,o,s=i?e.ok:e.fail,a=e.resolve,u=e.reject,c=e.domain;try{s?(i||(2==t._h&&M(t),t._h=1),!0===s?n=r:(c&&c.enter(),n=s(r),c&&c.exit()),n===e.promise?u(A("Promise-chain cycle")):(o=k(n))?o.call(n,a,u):a(n)):u(r)}catch(t){u(t)}}(n[o++]);t._c=[],t._n=!1,e&&!t._h&&T(t)})}},T=function(t){v.call(u,function(){var e,n,r,i=t._v,o=B(t);if(o&&(e=D(function(){C?S.emit("unhandledRejection",i,t):(n=u.onunhandledrejection)?n({promise:t,reason:i}):(r=u.console)&&r.error&&r.error("Unhandled promise rejection",i)}),t._h=C||B(t)?2:1),t._a=void 0,o&&e.e)throw e.v})},B=function(t){return 1!==t._h&&0===(t._a||t._c).length},M=function(t){v.call(u,function(){var e;C?S.emit("rejectionHandled",t):(e=u.onrejectionhandled)&&e({promise:t,reason:t._v})})},P=function(t){var e=this;e._d||(e._d=!0,e=e._w||e,e._v=t,e._s=2,e._a||(e._a=e._c.slice()),I(e,!0))},N=function(t){var e,n=this;if(!n._d){n._d=!0,n=n._w||n;try{if(n===t)throw A("Promise can't be resolved itself");(e=k(t))?x(function(){var r={_w:n,_d:!1};try{e.call(t,c(N,r,1),c(P,r,1))}catch(t){P.call(r,t)}}):(n._v=t,n._s=1,I(n,!1))}catch(t){P.call({_w:n,_d:!1},t)}}};F||(w=function(t){d(this,w,"Promise","_h"),f(t),r.call(this);try{t(c(N,this,1),c(P,this,1))}catch(t){P.call(this,t)}},r=function(t){this._c=[],this._a=void 0,this._s=0,this._d=!1,this._v=void 0,this._h=0,this._n=!1},r.prototype=n(191)(w.prototype,{then:function(t,e){var n=b(y(this,w));return n.ok="function"!=typeof t||t,n.fail="function"==typeof e&&e,n.domain=C?S.domain:void 0,this._c.push(n),this._a&&this._a.push(n),this._s&&I(this,!1),n.promise},catch:function(t){return this.then(void 0,t)}}),o=function(){var t=new r;this.promise=t,this.resolve=c(N,t,1),this.reject=c(P,t,1)},g.f=b=function(t){return t===w||t===s?new o(t):i(t)}),l(l.G+l.W+l.F*!F,{Promise:w}),n(67)(w,"Promise"),n(192)("Promise"),s=n(13).Promise,l(l.S+l.F*!F,"Promise",{reject:function(t){var e=b(this);return(0,e.reject)(t),e.promise}}),l(l.S+l.F*(a||!F),"Promise",{resolve:function(t){return E(a&&this===s?w:this,t)}}),l(l.S+l.F*!(F&&n(184)(function(t){w.all(t).catch(_)})),"Promise",{all:function(t){var e=this,n=b(e),r=n.resolve,i=n.reject,o=D(function(){var n=[],o=0,s=1;m(t,!1,function(t){var a=o++,u=!1;n.push(void 0),s++,e.resolve(t).then(function(t){u||(u=!0,n[a]=t,--s||r(n))},i)}),--s||r(n)});return o.e&&i(o.v),n.promise},race:function(t){var e=this,n=b(e),r=n.reject,i=D(function(){m(t,!1,function(t){e.resolve(t).then(n.resolve,r)})});return i.e&&r(i.v),n.promise}})},function(t,e,n){n(41)("match",1,function(t,e,n){return[function(n){"use strict";var r=t(this),i=void 0==n?void 0:n[e];return void 0!==i?i.call(n,r):new RegExp(n)[e](String(r))},n]})},function(t,e,n){n(41)("replace",2,function(t,e,n){return[function(r,i){"use strict";var o=t(this),s=void 0==r?void 0:r[e];return void 0!==s?s.call(r,o,i):n.call(String(o),r,i)},n]})},function(t,e,n){n(41)("search",1,function(t,e,n){return[function(n){"use strict";var r=t(this),i=void 0==n?void 0:n[e];return void 0!==i?i.call(n,r):new RegExp(n)[e](String(r))},n]})},function(t,e,n){n(41)("split",2,function(t,e,r){"use strict";var i=n(96),o=r,s=[].push,a="length";if("c"=="abbc".split(/(b)*/)[1]||4!="test".split(/(?:)/,-1)[a]||2!="ab".split(/(?:ab)*/)[a]||4!=".".split(/(.?)(.?)/)[a]||".".split(/()()/)[a]>1||"".split(/.?/)[a]){var u=void 0===/()??/.exec("")[1];r=function(t,e){var n=String(this);if(void 0===t&&0===e)return[];if(!i(t))return o.call(n,t,e);var r,c,h,l,p,f=[],d=(t.ignoreCase?"i":"")+(t.multiline?"m":"")+(t.unicode?"u":"")+(t.sticky?"y":""),m=0,y=void 0===e?4294967295:e>>>0,v=new RegExp(t.source,d+"g");for(u||(r=new RegExp("^"+v.source+"$(?!\\s)",d));(c=v.exec(n))&&!((h=c.index+c[0][a])>m&&(f.push(n.slice(m,c.index)),!u&&c[a]>1&&c[0].replace(r,function(){for(p=1;p<arguments[a]-2;p++)void 0===arguments[p]&&(c[p]=void 0)}),c[a]>1&&c.index<n[a]&&s.apply(f,c.slice(1)),l=c[0][a],m=h,f[a]>=y));)v.lastIndex===c.index&&v.lastIndex++;return m===n[a]?!l&&v.test("")||f.push(""):f.push(n.slice(m)),f[a]>y?f.slice(0,y):f}}else"0".split(void 0,0)[a]&&(r=function(t,e){return void 0===t&&0===e?[]:o.call(this,t,e)});return[function(n,i){var o=t(this),s=void 0==n?void 0:n[e];return void 0!==s?s.call(n,o,i):r.call(String(o),n,i)},r]})},function(t,e,n){"use strict";n(3)("anchor",function(t){return function(e){return t(this,"a","name",e)}})},function(t,e,n){"use strict";n(3)("big",function(t){return function(){return t(this,"big","","")}})},function(t,e,n){"use strict";n(3)("blink",function(t){return function(){return t(this,"blink","","")}})},function(t,e,n){"use strict";n(3)("bold",function(t){return function(){return t(this,"b","","")}})},function(t,e,n){"use strict";var r=n(2),i=n(105)(!1);r(r.P,"String",{codePointAt:function(t){return i(this,t)}})},function(t,e,n){"use strict";var r=n(2),i=n(32),o=n(69),s="".endsWith;r(r.P+r.F*n(65)("endsWith"),"String",{endsWith:function(t){var e=o(this,t,"endsWith"),n=arguments.length>1?arguments[1]:void 0,r=i(e.length),a=void 0===n?r:Math.min(i(n),r),u=String(t);return s?s.call(e,u,a):e.slice(a-u.length,a)===u}})},function(t,e,n){"use strict";n(3)("fixed",function(t){return function(){return t(this,"tt","","")}})},function(t,e,n){"use strict";n(3)("fontcolor",function(t){return function(e){return t(this,"font","color",e)}})},function(t,e,n){"use strict";n(3)("fontsize",function(t){return function(e){return t(this,"font","size",e)}})},function(t,e,n){var r=n(2),i=n(107),o=String.fromCharCode,s=String.fromCodePoint;r(r.S+r.F*(!!s&&1!=s.length),"String",{fromCodePoint:function(t){for(var e,n=[],r=arguments.length,s=0;r>s;){if(e=+arguments[s++],i(e,1114111)!==e)throw RangeError(e+" is not a valid code point");n.push(e<65536?o(e):o(55296+((e-=65536)>>10),e%1024+56320))}return n.join("")}})},function(t,e,n){"use strict";var r=n(2),i=n(69);r(r.P+r.F*n(65)("includes"),"String",{includes:function(t){return!!~i(this,t,"includes").indexOf(t,arguments.length>1?arguments[1]:void 0)}})},function(t,e,n){"use strict";n(3)("italics",function(t){return function(){return t(this,"i","","")}})},function(t,e,n){"use strict";n(3)("link",function(t){return function(e){return t(this,"a","href",e)}})},function(t,e,n){var r=n(2),i=n(44),o=n(32);r(r.S,"String",{raw:function(t){for(var e=i(t.raw),n=o(e.length),r=arguments.length,s=[],a=0;n>a;)s.push(String(e[a++])),a<r&&s.push(String(arguments[a]));return s.join("")}})},function(t,e,n){var r=n(2);r(r.P,"String",{repeat:n(193)})},function(t,e,n){"use strict";n(3)("small",function(t){return function(){return t(this,"small","","")}})},function(t,e,n){"use strict";var r=n(2),i=n(32),o=n(69),s="".startsWith;r(r.P+r.F*n(65)("startsWith"),"String",{startsWith:function(t){var e=o(this,t,"startsWith"),n=i(Math.min(arguments.length>1?arguments[1]:void 0,e.length)),r=String(t);return s?s.call(e,r,n):e.slice(n,n+r.length)===r}})},function(t,e,n){"use strict";n(3)("strike",function(t){return function(){return t(this,"strike","","")}})},function(t,e,n){"use strict";n(3)("sub",function(t){return function(){return t(this,"sub","","")}})},function(t,e,n){"use strict";n(3)("sup",function(t){return function(){return t(this,"sup","","")}})},function(t,e,n){"use strict";n(194)("trim",function(t){return function(){return t(this,3)}})},function(t,e,n){"use strict";var r=n(2),i=n(13),o=n(4),s=n(104),a=n(101);r(r.P+r.R,"Promise",{finally:function(t){var e=s(this,i.Promise||o.Promise),n="function"==typeof t;return this.then(n?function(n){return a(e,t()).then(function(){return n})}:t,n?function(n){return a(e,t()).then(function(){throw n})}:t)}})},function(t,e,n){"use strict";var r=n(2),i=n(66),o=n(100);r(r.S,"Promise",{try:function(t){var e=i.f(this),n=o(t);return(n.e?e.reject:e.resolve)(n.v),e.promise}})},function(t,e,n){for(var r=n(199),i=n(99),o=n(22),s=n(4),a=n(14),u=n(31),c=n(1),h=c("iterator"),l=c("toStringTag"),p=u.Array,f={CSSRuleList:!0,CSSStyleDeclaration:!1,CSSValueList:!1,ClientRectList:!1,DOMRectList:!1,DOMStringList:!1,DOMTokenList:!0,DataTransferItemList:!1,FileList:!1,HTMLAllCollection:!1,HTMLCollection:!1,HTMLFormElement:!1,HTMLSelectElement:!1,MediaList:!0,MimeTypeArray:!1,NamedNodeMap:!1,NodeList:!0,PaintRequestList:!1,Plugin:!1,PluginArray:!1,SVGLengthList:!1,SVGNumberList:!1,SVGPathSegList:!1,SVGPointList:!1,SVGStringList:!1,SVGTransformList:!1,SourceBufferList:!1,StyleSheetList:!0,TextTrackCueList:!1,TextTrackList:!1,TouchList:!1},d=i(f),m=0;m<d.length;m++){var y,v=d[m],x=f[v],g=s[v],D=g&&g.prototype;if(D&&(D[h]||a(D,h,p),D[l]||a(D,l,v),u[v]=p,x))for(y in r)D[y]||o(D,y,r[y],!0)}},function(t,e,n){"use strict";function r(t){return t}function i(t,e,n){function i(t,e){var n=x.hasOwnProperty(e)?x[e]:null;A.hasOwnProperty(e)&&a("OVERRIDE_BASE"===n,"ReactClassInterface: You are attempting to override `%s` from your class specification. Ensure that your method names do not overlap with React methods.",e),t&&a("DEFINE_MANY"===n||"DEFINE_MANY_MERGED"===n,"ReactClassInterface: You are attempting to define `%s` on your component more than once. This conflict may be due to a mixin.",e)}function c(t,n){if(n){a("function"!=typeof n,"ReactClass: You're attempting to use a component class or function as a mixin. Instead, just use a regular object."),a(!e(n),"ReactClass: You're attempting to use a component as a mixin. Instead, just use a regular object.");var r=t.prototype,o=r.__reactAutoBindPairs;n.hasOwnProperty(u)&&g.mixins(t,n.mixins);for(var s in n)if(n.hasOwnProperty(s)&&s!==u){var c=n[s],h=r.hasOwnProperty(s);if(i(h,s),g.hasOwnProperty(s))g[s](t,c);else{var l=x.hasOwnProperty(s),d="function"==typeof c,m=d&&!l&&!h&&!1!==n.autobind;if(m)o.push(s,c),r[s]=c;else if(h){var y=x[s];a(l&&("DEFINE_MANY_MERGED"===y||"DEFINE_MANY"===y),"ReactClass: Unexpected spec policy %s for key %s when mixing in component specs.",y,s),"DEFINE_MANY_MERGED"===y?r[s]=p(r[s],c):"DEFINE_MANY"===y&&(r[s]=f(r[s],c))}else r[s]=c}}}else;}function h(t,e){if(e)for(var n in e){var r=e[n];if(e.hasOwnProperty(n)){var i=n in g;a(!i,'ReactClass: You are attempting to define a reserved property, `%s`, that shouldn\'t be on the "statics" key. Define it as an instance property instead; it will still be accessible on the constructor.',n);var o=n in t;a(!o,"ReactClass: You are attempting to define `%s` on your component more than once. This conflict may be due to a mixin.",n),t[n]=r}}}function l(t,e){a(t&&e&&"object"==typeof t&&"object"==typeof e,"mergeIntoWithNoDuplicateKeys(): Cannot merge non-objects.");for(var n in e)e.hasOwnProperty(n)&&(a(void 0===t[n],"mergeIntoWithNoDuplicateKeys(): Tried to merge two objects with the same key: `%s`. This conflict may be due to a mixin; in particular, this may be caused by two getInitialState() or getDefaultProps() methods returning objects with clashing keys.",n),t[n]=e[n]);return t}function p(t,e){return function(){var n=t.apply(this,arguments),r=e.apply(this,arguments);if(null==n)return r;if(null==r)return n;var i={};return l(i,n),l(i,r),i}}function f(t,e){return function(){t.apply(this,arguments),e.apply(this,arguments)}}function d(t,e){var n=e.bind(t);return n}function m(t){for(var e=t.__reactAutoBindPairs,n=0;n<e.length;n+=2){var r=e[n],i=e[n+1];t[r]=d(t,i)}}function y(t){var e=r(function(t,r,i){this.__reactAutoBindPairs.length&&m(this),this.props=t,this.context=r,this.refs=s,this.updater=i||n,this.state=null;var o=this.getInitialState?this.getInitialState():null;a("object"==typeof o&&!Array.isArray(o),"%s.getInitialState(): must return an object or null",e.displayName||"ReactCompositeComponent"),this.state=o});e.prototype=new S,e.prototype.constructor=e,e.prototype.__reactAutoBindPairs=[],v.forEach(c.bind(null,e)),c(e,D),c(e,t),c(e,E),e.getDefaultProps&&(e.defaultProps=e.getDefaultProps()),a(e.prototype.render,"createClass(...): Class specification must implement a `render` method.");for(var i in x)e.prototype[i]||(e.prototype[i]=null);return e}var v=[],x={mixins:"DEFINE_MANY",statics:"DEFINE_MANY",propTypes:"DEFINE_MANY",contextTypes:"DEFINE_MANY",childContextTypes:"DEFINE_MANY",getDefaultProps:"DEFINE_MANY_MERGED",getInitialState:"DEFINE_MANY_MERGED",getChildContext:"DEFINE_MANY_MERGED",render:"DEFINE_ONCE",componentWillMount:"DEFINE_MANY",componentDidMount:"DEFINE_MANY",componentWillReceiveProps:"DEFINE_MANY",shouldComponentUpdate:"DEFINE_ONCE",componentWillUpdate:"DEFINE_MANY",componentDidUpdate:"DEFINE_MANY",componentWillUnmount:"DEFINE_MANY",updateComponent:"OVERRIDE_BASE"},g={displayName:function(t,e){t.displayName=e},mixins:function(t,e){if(e)for(var n=0;n<e.length;n++)c(t,e[n])},childContextTypes:function(t,e){t.childContextTypes=o({},t.childContextTypes,e)},contextTypes:function(t,e){t.contextTypes=o({},t.contextTypes,e)},getDefaultProps:function(t,e){t.getDefaultProps?t.getDefaultProps=p(t.getDefaultProps,e):t.getDefaultProps=e},propTypes:function(t,e){t.propTypes=o({},t.propTypes,e)},statics:function(t,e){h(t,e)},autobind:function(){}},D={componentDidMount:function(){this.__isMounted=!0}},E={componentWillUnmount:function(){this.__isMounted=!1}},A={replaceState:function(t,e){this.updater.enqueueReplaceState(this,t,e)},isMounted:function(){return!!this.__isMounted}},S=function(){};return o(S.prototype,t.prototype,A),y}var o=n(35),s=n(109),a=n(15),u="mixins";t.exports=i},function(t,e,n){!function(e,n){t.exports=n()}(0,function(){return function(t){function e(r){if(n[r])return n[r].exports;var i=n[r]={exports:{},id:r,loaded:!1};return t[r].call(i.exports,i,i.exports,e),i.loaded=!0,i.exports}var n={};return e.m=t,e.c=n,e.p="",e(0)}([function(t,e,n){"use strict";function r(t,e,n){var r=null,i=function(t,e){n&&n(t,e),r&&r.visit(t,e)},o="function"==typeof n?i:null,s=!1;if(e){s="boolean"==typeof e.comment&&e.comment;var h="boolean"==typeof e.attachComment&&e.attachComment;(s||h)&&(r=new a.CommentHandler,r.attach=h,e.comment=!0,o=i)}var l=!1;e&&"string"==typeof e.sourceType&&(l="module"===e.sourceType);var p;p=e&&"boolean"==typeof e.jsx&&e.jsx?new u.JSXParser(t,e,o):new c.Parser(t,e,o);var f=l?p.parseModule():p.parseScript(),d=f;return s&&r&&(d.comments=r.comments),p.config.tokens&&(d.tokens=p.tokens),p.config.tolerant&&(d.errors=p.errorHandler.errors),d}function i(t,e,n){var i=e||{};return i.sourceType="module",r(t,i,n)}function o(t,e,n){var i=e||{};return i.sourceType="script",r(t,i,n)}function s(t,e,n){var r,i=new h.Tokenizer(t,e);r=[];try{for(;;){var o=i.getNextToken();if(!o)break;n&&(o=n(o)),r.push(o)}}catch(t){i.errorHandler.tolerate(t)}return i.errorHandler.tolerant&&(r.errors=i.errors()),r}Object.defineProperty(e,"__esModule",{value:!0});var a=n(1),u=n(3),c=n(8),h=n(15);e.parse=r,e.parseModule=i,e.parseScript=o,e.tokenize=s;var l=n(2);e.Syntax=l.Syntax,e.version="4.0.0"},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n(2),i=function(){function t(){this.attach=!1,this.comments=[],this.stack=[],this.leading=[],this.trailing=[]}return t.prototype.insertInnerComments=function(t,e){if(t.type===r.Syntax.BlockStatement&&0===t.body.length){for(var n=[],i=this.leading.length-1;i>=0;--i){var o=this.leading[i];e.end.offset>=o.start&&(n.unshift(o.comment),this.leading.splice(i,1),this.trailing.splice(i,1))}n.length&&(t.innerComments=n)}},t.prototype.findTrailingComments=function(t){var e=[];if(this.trailing.length>0){for(var n=this.trailing.length-1;n>=0;--n){var r=this.trailing[n];r.start>=t.end.offset&&e.unshift(r.comment)}return this.trailing.length=0,e}var i=this.stack[this.stack.length-1];if(i&&i.node.trailingComments){var o=i.node.trailingComments[0];o&&o.range[0]>=t.end.offset&&(e=i.node.trailingComments,delete i.node.trailingComments)}return e},t.prototype.findLeadingComments=function(t){for(var e,n=[];this.stack.length>0;){var r=this.stack[this.stack.length-1];if(!(r&&r.start>=t.start.offset))break;e=r.node,this.stack.pop()}if(e){for(var i=e.leadingComments?e.leadingComments.length:0,o=i-1;o>=0;--o){var s=e.leadingComments[o];s.range[1]<=t.start.offset&&(n.unshift(s),e.leadingComments.splice(o,1))}return e.leadingComments&&0===e.leadingComments.length&&delete e.leadingComments,n}for(var o=this.leading.length-1;o>=0;--o){var r=this.leading[o];r.start<=t.start.offset&&(n.unshift(r.comment),this.leading.splice(o,1))}return n},t.prototype.visitNode=function(t,e){if(!(t.type===r.Syntax.Program&&t.body.length>0)){this.insertInnerComments(t,e);var n=this.findTrailingComments(e),i=this.findLeadingComments(e);i.length>0&&(t.leadingComments=i),n.length>0&&(t.trailingComments=n),this.stack.push({node:t,start:e.start.offset})}},t.prototype.visitComment=function(t,e){var n="L"===t.type[0]?"Line":"Block",r={type:n,value:t.value};if(t.range&&(r.range=t.range),t.loc&&(r.loc=t.loc),this.comments.push(r),this.attach){var i={comment:{type:n,value:t.value,range:[e.start.offset,e.end.offset]},start:e.start.offset};t.loc&&(i.comment.loc=t.loc),t.type=n,this.leading.push(i),this.trailing.push(i)}},t.prototype.visit=function(t,e){"LineComment"===t.type?this.visitComment(t,e):"BlockComment"===t.type?this.visitComment(t,e):this.attach&&this.visitNode(t,e)},t}();e.CommentHandler=i},function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Syntax={AssignmentExpression:"AssignmentExpression",AssignmentPattern:"AssignmentPattern",ArrayExpression:"ArrayExpression",ArrayPattern:"ArrayPattern",ArrowFunctionExpression:"ArrowFunctionExpression",AwaitExpression:"AwaitExpression",BlockStatement:"BlockStatement",BinaryExpression:"BinaryExpression",BreakStatement:"BreakStatement",CallExpression:"CallExpression",CatchClause:"CatchClause",ClassBody:"ClassBody",ClassDeclaration:"ClassDeclaration",ClassExpression:"ClassExpression",ConditionalExpression:"ConditionalExpression",ContinueStatement:"ContinueStatement",DoWhileStatement:"DoWhileStatement",DebuggerStatement:"DebuggerStatement",EmptyStatement:"EmptyStatement",ExportAllDeclaration:"ExportAllDeclaration",ExportDefaultDeclaration:"ExportDefaultDeclaration",ExportNamedDeclaration:"ExportNamedDeclaration",ExportSpecifier:"ExportSpecifier",ExpressionStatement:"ExpressionStatement",ForStatement:"ForStatement",ForOfStatement:"ForOfStatement",ForInStatement:"ForInStatement",FunctionDeclaration:"FunctionDeclaration",FunctionExpression:"FunctionExpression",Identifier:"Identifier",IfStatement:"IfStatement",ImportDeclaration:"ImportDeclaration",ImportDefaultSpecifier:"ImportDefaultSpecifier",ImportNamespaceSpecifier:"ImportNamespaceSpecifier",ImportSpecifier:"ImportSpecifier",Literal:"Literal",LabeledStatement:"LabeledStatement",LogicalExpression:"LogicalExpression",MemberExpression:"MemberExpression",MetaProperty:"MetaProperty",MethodDefinition:"MethodDefinition",NewExpression:"NewExpression",ObjectExpression:"ObjectExpression",ObjectPattern:"ObjectPattern",Program:"Program",Property:"Property",RestElement:"RestElement",ReturnStatement:"ReturnStatement",SequenceExpression:"SequenceExpression",SpreadElement:"SpreadElement",Super:"Super",SwitchCase:"SwitchCase",SwitchStatement:"SwitchStatement",TaggedTemplateExpression:"TaggedTemplateExpression",TemplateElement:"TemplateElement",TemplateLiteral:"TemplateLiteral",ThisExpression:"ThisExpression",ThrowStatement:"ThrowStatement",TryStatement:"TryStatement",UnaryExpression:"UnaryExpression",UpdateExpression:"UpdateExpression",VariableDeclaration:"VariableDeclaration",VariableDeclarator:"VariableDeclarator",WhileStatement:"WhileStatement",WithStatement:"WithStatement",YieldExpression:"YieldExpression"}},function(t,e,n){"use strict";function r(t){var e;switch(t.type){case a.JSXSyntax.JSXIdentifier:e=t.name;break;case a.JSXSyntax.JSXNamespacedName:var n=t;e=r(n.namespace)+":"+r(n.name);break;case a.JSXSyntax.JSXMemberExpression:var i=t;e=r(i.object)+"."+r(i.property)}return e}var i=this&&this.__extends||function(){var t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n])};return function(e,n){function r(){this.constructor=e}t(e,n),e.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}}();Object.defineProperty(e,"__esModule",{value:!0});var o=n(4),s=n(5),a=n(6),u=n(7),c=n(8),h=n(13),l=n(14);h.TokenName[100]="JSXIdentifier",h.TokenName[101]="JSXText";var p=function(t){function e(e,n,r){return t.call(this,e,n,r)||this}return i(e,t),e.prototype.parsePrimaryExpression=function(){return this.match("<")?this.parseJSXRoot():t.prototype.parsePrimaryExpression.call(this)},e.prototype.startJSX=function(){this.scanner.index=this.startMarker.index,this.scanner.lineNumber=this.startMarker.line,this.scanner.lineStart=this.startMarker.index-this.startMarker.column},e.prototype.finishJSX=function(){this.nextToken()},e.prototype.reenterJSX=function(){this.startJSX(),this.expectJSX("}"),this.config.tokens&&this.tokens.pop()},e.prototype.createJSXNode=function(){return this.collectComments(),{index:this.scanner.index,line:this.scanner.lineNumber,column:this.scanner.index-this.scanner.lineStart}},e.prototype.createJSXChildNode=function(){return{index:this.scanner.index,line:this.scanner.lineNumber,column:this.scanner.index-this.scanner.lineStart}},e.prototype.scanXHTMLEntity=function(t){for(var e="&",n=!0,r=!1,i=!1,s=!1;!this.scanner.eof()&&n&&!r;){var a=this.scanner.source[this.scanner.index];if(a===t)break;if(r=";"===a,e+=a,++this.scanner.index,!r)switch(e.length){case 2:i="#"===a;break;case 3:i&&(s="x"===a,n=s||o.Character.isDecimalDigit(a.charCodeAt(0)),i=i&&!s);break;default:n=n&&!(i&&!o.Character.isDecimalDigit(a.charCodeAt(0))),n=n&&!(s&&!o.Character.isHexDigit(a.charCodeAt(0)))}}if(n&&r&&e.length>2){var u=e.substr(1,e.length-2);i&&u.length>1?e=String.fromCharCode(parseInt(u.substr(1),10)):s&&u.length>2?e=String.fromCharCode(parseInt("0"+u.substr(1),16)):i||s||!l.XHTMLEntities[u]||(e=l.XHTMLEntities[u])}return e},e.prototype.lexJSX=function(){var t=this.scanner.source.charCodeAt(this.scanner.index);if(60===t||62===t||47===t||58===t||61===t||123===t||125===t){var e=this.scanner.source[this.scanner.index++];return{type:7,value:e,lineNumber:this.scanner.lineNumber,lineStart:this.scanner.lineStart,start:this.scanner.index-1,end:this.scanner.index}}if(34===t||39===t){for(var n=this.scanner.index,r=this.scanner.source[this.scanner.index++],i="";!this.scanner.eof();){var s=this.scanner.source[this.scanner.index++];if(s===r)break;i+="&"===s?this.scanXHTMLEntity(r):s}return{type:8,value:i,lineNumber:this.scanner.lineNumber,lineStart:this.scanner.lineStart,start:n,end:this.scanner.index}}if(46===t){var a=this.scanner.source.charCodeAt(this.scanner.index+1),u=this.scanner.source.charCodeAt(this.scanner.index+2),e=46===a&&46===u?"...":".",n=this.scanner.index;return this.scanner.index+=e.length,{type:7,value:e,lineNumber:this.scanner.lineNumber,lineStart:this.scanner.lineStart,start:n,end:this.scanner.index}}if(96===t)return{type:10,value:"",lineNumber:this.scanner.lineNumber,lineStart:this.scanner.lineStart,start:this.scanner.index,end:this.scanner.index};if(o.Character.isIdentifierStart(t)&&92!==t){var n=this.scanner.index;for(++this.scanner.index;!this.scanner.eof();){var s=this.scanner.source.charCodeAt(this.scanner.index);if(o.Character.isIdentifierPart(s)&&92!==s)++this.scanner.index;else{if(45!==s)break;++this.scanner.index}}return{type:100,value:this.scanner.source.slice(n,this.scanner.index),lineNumber:this.scanner.lineNumber,lineStart:this.scanner.lineStart,start:n,end:this.scanner.index}}return this.scanner.lex()},e.prototype.nextJSXToken=function(){this.collectComments(),this.startMarker.index=this.scanner.index,this.startMarker.line=this.scanner.lineNumber,this.startMarker.column=this.scanner.index-this.scanner.lineStart;var t=this.lexJSX();return this.lastMarker.index=this.scanner.index,this.lastMarker.line=this.scanner.lineNumber,this.lastMarker.column=this.scanner.index-this.scanner.lineStart,this.config.tokens&&this.tokens.push(this.convertToken(t)),t},e.prototype.nextJSXText=function(){this.startMarker.index=this.scanner.index,this.startMarker.line=this.scanner.lineNumber,this.startMarker.column=this.scanner.index-this.scanner.lineStart;for(var t=this.scanner.index,e="";!this.scanner.eof();){var n=this.scanner.source[this.scanner.index];if("{"===n||"<"===n)break;++this.scanner.index,e+=n,o.Character.isLineTerminator(n.charCodeAt(0))&&(++this.scanner.lineNumber,"\r"===n&&"\n"===this.scanner.source[this.scanner.index]&&++this.scanner.index,this.scanner.lineStart=this.scanner.index)}this.lastMarker.index=this.scanner.index,this.lastMarker.line=this.scanner.lineNumber,this.lastMarker.column=this.scanner.index-this.scanner.lineStart;var r={type:101,value:e,lineNumber:this.scanner.lineNumber,lineStart:this.scanner.lineStart,start:t,end:this.scanner.index};return e.length>0&&this.config.tokens&&this.tokens.push(this.convertToken(r)),r},e.prototype.peekJSXToken=function(){var t=this.scanner.saveState();this.scanner.scanComments();var e=this.lexJSX();return this.scanner.restoreState(t),e},e.prototype.expectJSX=function(t){var e=this.nextJSXToken();7===e.type&&e.value===t||this.throwUnexpectedToken(e)},e.prototype.matchJSX=function(t){var e=this.peekJSXToken();return 7===e.type&&e.value===t},e.prototype.parseJSXIdentifier=function(){var t=this.createJSXNode(),e=this.nextJSXToken();return 100!==e.type&&this.throwUnexpectedToken(e),this.finalize(t,new s.JSXIdentifier(e.value))},e.prototype.parseJSXElementName=function(){var t=this.createJSXNode(),e=this.parseJSXIdentifier();if(this.matchJSX(":")){var n=e;this.expectJSX(":");var r=this.parseJSXIdentifier();e=this.finalize(t,new s.JSXNamespacedName(n,r))}else if(this.matchJSX("."))for(;this.matchJSX(".");){var i=e;this.expectJSX(".");var o=this.parseJSXIdentifier();e=this.finalize(t,new s.JSXMemberExpression(i,o))}return e},e.prototype.parseJSXAttributeName=function(){var t,e=this.createJSXNode(),n=this.parseJSXIdentifier();if(this.matchJSX(":")){var r=n;this.expectJSX(":");var i=this.parseJSXIdentifier();t=this.finalize(e,new s.JSXNamespacedName(r,i))}else t=n;return t},e.prototype.parseJSXStringLiteralAttribute=function(){var t=this.createJSXNode(),e=this.nextJSXToken();8!==e.type&&this.throwUnexpectedToken(e);var n=this.getTokenRaw(e);return this.finalize(t,new u.Literal(e.value,n))},e.prototype.parseJSXExpressionAttribute=function(){var t=this.createJSXNode();this.expectJSX("{"),this.finishJSX(),this.match("}")&&this.tolerateError("JSX attributes must only be assigned a non-empty expression");var e=this.parseAssignmentExpression();return this.reenterJSX(),this.finalize(t,new s.JSXExpressionContainer(e))},e.prototype.parseJSXAttributeValue=function(){return this.matchJSX("{")?this.parseJSXExpressionAttribute():this.matchJSX("<")?this.parseJSXElement():this.parseJSXStringLiteralAttribute()},e.prototype.parseJSXNameValueAttribute=function(){var t=this.createJSXNode(),e=this.parseJSXAttributeName(),n=null;return this.matchJSX("=")&&(this.expectJSX("="),n=this.parseJSXAttributeValue()),this.finalize(t,new s.JSXAttribute(e,n))},e.prototype.parseJSXSpreadAttribute=function(){var t=this.createJSXNode();this.expectJSX("{"),this.expectJSX("..."),this.finishJSX();var e=this.parseAssignmentExpression();return this.reenterJSX(),this.finalize(t,new s.JSXSpreadAttribute(e))},e.prototype.parseJSXAttributes=function(){for(var t=[];!this.matchJSX("/")&&!this.matchJSX(">");){var e=this.matchJSX("{")?this.parseJSXSpreadAttribute():this.parseJSXNameValueAttribute();t.push(e)}return t},e.prototype.parseJSXOpeningElement=function(){var t=this.createJSXNode();this.expectJSX("<");var e=this.parseJSXElementName(),n=this.parseJSXAttributes(),r=this.matchJSX("/");return r&&this.expectJSX("/"),this.expectJSX(">"),this.finalize(t,new s.JSXOpeningElement(e,r,n))},e.prototype.parseJSXBoundaryElement=function(){var t=this.createJSXNode();if(this.expectJSX("<"),this.matchJSX("/")){this.expectJSX("/");var e=this.parseJSXElementName();return this.expectJSX(">"),this.finalize(t,new s.JSXClosingElement(e))}var n=this.parseJSXElementName(),r=this.parseJSXAttributes(),i=this.matchJSX("/");return i&&this.expectJSX("/"),this.expectJSX(">"),this.finalize(t,new s.JSXOpeningElement(n,i,r))},e.prototype.parseJSXEmptyExpression=function(){var t=this.createJSXChildNode();return this.collectComments(),this.lastMarker.index=this.scanner.index,this.lastMarker.line=this.scanner.lineNumber,this.lastMarker.column=this.scanner.index-this.scanner.lineStart,this.finalize(t,new s.JSXEmptyExpression)},e.prototype.parseJSXExpressionContainer=function(){var t=this.createJSXNode();this.expectJSX("{");var e;return this.matchJSX("}")?(e=this.parseJSXEmptyExpression(),this.expectJSX("}")):(this.finishJSX(),e=this.parseAssignmentExpression(),this.reenterJSX()),this.finalize(t,new s.JSXExpressionContainer(e))},e.prototype.parseJSXChildren=function(){for(var t=[];!this.scanner.eof();){var e=this.createJSXChildNode(),n=this.nextJSXText();if(n.start<n.end){var r=this.getTokenRaw(n),i=this.finalize(e,new s.JSXText(n.value,r));t.push(i)}if("{"!==this.scanner.source[this.scanner.index])break;var o=this.parseJSXExpressionContainer();t.push(o)}return t},e.prototype.parseComplexJSXElement=function(t){for(var e=[];!this.scanner.eof();){t.children=t.children.concat(this.parseJSXChildren());var n=this.createJSXChildNode(),i=this.parseJSXBoundaryElement();if(i.type===a.JSXSyntax.JSXOpeningElement){var o=i;if(o.selfClosing){var u=this.finalize(n,new s.JSXElement(o,[],null));t.children.push(u)}else e.push(t),t={node:n,opening:o,closing:null,children:[]}}if(i.type===a.JSXSyntax.JSXClosingElement){t.closing=i;var c=r(t.opening.name);if(c!==r(t.closing.name)&&this.tolerateError("Expected corresponding JSX closing tag for %0",c),!(e.length>0))break;var u=this.finalize(t.node,new s.JSXElement(t.opening,t.children,t.closing));t=e[e.length-1],t.children.push(u),e.pop()}}return t},e.prototype.parseJSXElement=function(){var t=this.createJSXNode(),e=this.parseJSXOpeningElement(),n=[],r=null;if(!e.selfClosing){var i=this.parseComplexJSXElement({node:t,opening:e,closing:r,children:n});n=i.children,r=i.closing}return this.finalize(t,new s.JSXElement(e,n,r))},e.prototype.parseJSXRoot=function(){this.config.tokens&&this.tokens.pop(),this.startJSX();var t=this.parseJSXElement();return this.finishJSX(),t},e.prototype.isStartOfExpression=function(){return t.prototype.isStartOfExpression.call(this)||this.match("<")},e}(c.Parser);e.JSXParser=p},function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var n={NonAsciiIdentifierStart:/[\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B4\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2118-\u211D\u2124\u2126\u2128\u212A-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309B-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FD5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AD\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF30-\uDF4A\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC03-\uDC37\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDF00-\uDF19]|\uD806[\uDCA0-\uDCDF\uDCFF\uDEC0-\uDEF8]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50\uDF93-\uDF9F]|\uD82C[\uDC00\uDC01]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD83A[\uDC00-\uDCC4]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1]|\uD87E[\uDC00-\uDE1D]/,NonAsciiIdentifierPart:/[\xAA\xB5\xB7\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0300-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u0483-\u0487\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u05D0-\u05EA\u05F0-\u05F2\u0610-\u061A\u0620-\u0669\u066E-\u06D3\u06D5-\u06DC\u06DF-\u06E8\u06EA-\u06FC\u06FF\u0710-\u074A\u074D-\u07B1\u07C0-\u07F5\u07FA\u0800-\u082D\u0840-\u085B\u08A0-\u08B4\u08E3-\u0963\u0966-\u096F\u0971-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC-\u09C4\u09C7\u09C8\u09CB-\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E3\u09E6-\u09F1\u0A01-\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A59-\u0A5C\u0A5E\u0A66-\u0A75\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AD0\u0AE0-\u0AE3\u0AE6-\u0AEF\u0AF9\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3C-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B5C\u0B5D\u0B5F-\u0B63\u0B66-\u0B6F\u0B71\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD0\u0BD7\u0BE6-\u0BEF\u0C00-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C58-\u0C5A\u0C60-\u0C63\u0C66-\u0C6F\u0C81-\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBC-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CDE\u0CE0-\u0CE3\u0CE6-\u0CEF\u0CF1\u0CF2\u0D01-\u0D03\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D-\u0D44\u0D46-\u0D48\u0D4A-\u0D4E\u0D57\u0D5F-\u0D63\u0D66-\u0D6F\u0D7A-\u0D7F\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2\u0DF3\u0E01-\u0E3A\u0E40-\u0E4E\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB9\u0EBB-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECD\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F18\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E-\u0F47\u0F49-\u0F6C\u0F71-\u0F84\u0F86-\u0F97\u0F99-\u0FBC\u0FC6\u1000-\u1049\u1050-\u109D\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135D-\u135F\u1369-\u1371\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176C\u176E-\u1770\u1772\u1773\u1780-\u17D3\u17D7\u17DC\u17DD\u17E0-\u17E9\u180B-\u180D\u1810-\u1819\u1820-\u1877\u1880-\u18AA\u18B0-\u18F5\u1900-\u191E\u1920-\u192B\u1930-\u193B\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19DA\u1A00-\u1A1B\u1A20-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AA7\u1AB0-\u1ABD\u1B00-\u1B4B\u1B50-\u1B59\u1B6B-\u1B73\u1B80-\u1BF3\u1C00-\u1C37\u1C40-\u1C49\u1C4D-\u1C7D\u1CD0-\u1CD2\u1CD4-\u1CF6\u1CF8\u1CF9\u1D00-\u1DF5\u1DFC-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u200C\u200D\u203F\u2040\u2054\u2071\u207F\u2090-\u209C\u20D0-\u20DC\u20E1\u20E5-\u20F0\u2102\u2107\u210A-\u2113\u2115\u2118-\u211D\u2124\u2126\u2128\u212A-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D7F-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2DE0-\u2DFF\u3005-\u3007\u3021-\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u3099-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FD5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66F\uA674-\uA67D\uA67F-\uA6F1\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AD\uA7B0-\uA7B7\uA7F7-\uA827\uA840-\uA873\uA880-\uA8C4\uA8D0-\uA8D9\uA8E0-\uA8F7\uA8FB\uA8FD\uA900-\uA92D\uA930-\uA953\uA960-\uA97C\uA980-\uA9C0\uA9CF-\uA9D9\uA9E0-\uA9FE\uAA00-\uAA36\uAA40-\uAA4D\uAA50-\uAA59\uAA60-\uAA76\uAA7A-\uAAC2\uAADB-\uAADD\uAAE0-\uAAEF\uAAF2-\uAAF6\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABEA\uABEC\uABED\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE00-\uFE0F\uFE20-\uFE2F\uFE33\uFE34\uFE4D-\uFE4F\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF3F\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDDFD\uDE80-\uDE9C\uDEA0-\uDED0\uDEE0\uDF00-\uDF1F\uDF30-\uDF4A\uDF50-\uDF7A\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCA0-\uDCA9\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00-\uDE03\uDE05\uDE06\uDE0C-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE38-\uDE3A\uDE3F\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE6\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC00-\uDC46\uDC66-\uDC6F\uDC7F-\uDCBA\uDCD0-\uDCE8\uDCF0-\uDCF9\uDD00-\uDD34\uDD36-\uDD3F\uDD50-\uDD73\uDD76\uDD80-\uDDC4\uDDCA-\uDDCC\uDDD0-\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE37\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEEA\uDEF0-\uDEF9\uDF00-\uDF03\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3C-\uDF44\uDF47\uDF48\uDF4B-\uDF4D\uDF50\uDF57\uDF5D-\uDF63\uDF66-\uDF6C\uDF70-\uDF74]|\uD805[\uDC80-\uDCC5\uDCC7\uDCD0-\uDCD9\uDD80-\uDDB5\uDDB8-\uDDC0\uDDD8-\uDDDD\uDE00-\uDE40\uDE44\uDE50-\uDE59\uDE80-\uDEB7\uDEC0-\uDEC9\uDF00-\uDF19\uDF1D-\uDF2B\uDF30-\uDF39]|\uD806[\uDCA0-\uDCE9\uDCFF\uDEC0-\uDEF8]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE60-\uDE69\uDED0-\uDEED\uDEF0-\uDEF4\uDF00-\uDF36\uDF40-\uDF43\uDF50-\uDF59\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50-\uDF7E\uDF8F-\uDF9F]|\uD82C[\uDC00\uDC01]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99\uDC9D\uDC9E]|\uD834[\uDD65-\uDD69\uDD6D-\uDD72\uDD7B-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD\uDE42-\uDE44]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB\uDFCE-\uDFFF]|\uD836[\uDE00-\uDE36\uDE3B-\uDE6C\uDE75\uDE84\uDE9B-\uDE9F\uDEA1-\uDEAF]|\uD83A[\uDC00-\uDCC4\uDCD0-\uDCD6]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1]|\uD87E[\uDC00-\uDE1D]|\uDB40[\uDD00-\uDDEF]/};e.Character={fromCodePoint:function(t){return t<65536?String.fromCharCode(t):String.fromCharCode(55296+(t-65536>>10))+String.fromCharCode(56320+(t-65536&1023))},isWhiteSpace:function(t){return 32===t||9===t||11===t||12===t||160===t||t>=5760&&[5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8239,8287,12288,65279].indexOf(t)>=0},isLineTerminator:function(t){return 10===t||13===t||8232===t||8233===t},isIdentifierStart:function(t){return 36===t||95===t||t>=65&&t<=90||t>=97&&t<=122||92===t||t>=128&&n.NonAsciiIdentifierStart.test(e.Character.fromCodePoint(t))},isIdentifierPart:function(t){return 36===t||95===t||t>=65&&t<=90||t>=97&&t<=122||t>=48&&t<=57||92===t||t>=128&&n.NonAsciiIdentifierPart.test(e.Character.fromCodePoint(t))},isDecimalDigit:function(t){return t>=48&&t<=57},isHexDigit:function(t){return t>=48&&t<=57||t>=65&&t<=70||t>=97&&t<=102},isOctalDigit:function(t){return t>=48&&t<=55}}},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n(6),i=function(){function t(t){this.type=r.JSXSyntax.JSXClosingElement,this.name=t}return t}();e.JSXClosingElement=i;var o=function(){function t(t,e,n){this.type=r.JSXSyntax.JSXElement,this.openingElement=t,this.children=e,this.closingElement=n}return t}();e.JSXElement=o;var s=function(){function t(){this.type=r.JSXSyntax.JSXEmptyExpression}return t}();e.JSXEmptyExpression=s;var a=function(){function t(t){this.type=r.JSXSyntax.JSXExpressionContainer,this.expression=t}return t}();e.JSXExpressionContainer=a;var u=function(){function t(t){this.type=r.JSXSyntax.JSXIdentifier,this.name=t}return t}();e.JSXIdentifier=u;var c=function(){function t(t,e){this.type=r.JSXSyntax.JSXMemberExpression,this.object=t,this.property=e}return t}();e.JSXMemberExpression=c;var h=function(){function t(t,e){this.type=r.JSXSyntax.JSXAttribute,this.name=t,this.value=e}return t}();e.JSXAttribute=h;var l=function(){function t(t,e){this.type=r.JSXSyntax.JSXNamespacedName,this.namespace=t,this.name=e}return t}();e.JSXNamespacedName=l;var p=function(){function t(t,e,n){this.type=r.JSXSyntax.JSXOpeningElement,this.name=t,this.selfClosing=e,this.attributes=n}return t}();e.JSXOpeningElement=p;var f=function(){function t(t){this.type=r.JSXSyntax.JSXSpreadAttribute,this.argument=t}return t}();e.JSXSpreadAttribute=f;var d=function(){function t(t,e){this.type=r.JSXSyntax.JSXText,this.value=t,this.raw=e}return t}();e.JSXText=d},function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.JSXSyntax={JSXAttribute:"JSXAttribute",JSXClosingElement:"JSXClosingElement",JSXElement:"JSXElement",JSXEmptyExpression:"JSXEmptyExpression",JSXExpressionContainer:"JSXExpressionContainer",JSXIdentifier:"JSXIdentifier",JSXMemberExpression:"JSXMemberExpression",JSXNamespacedName:"JSXNamespacedName",JSXOpeningElement:"JSXOpeningElement",JSXSpreadAttribute:"JSXSpreadAttribute",JSXText:"JSXText"}},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n(2),i=function(){function t(t){this.type=r.Syntax.ArrayExpression,this.elements=t}return t}();e.ArrayExpression=i;var o=function(){function t(t){this.type=r.Syntax.ArrayPattern,this.elements=t}return t}();e.ArrayPattern=o;var s=function(){function t(t,e,n){this.type=r.Syntax.ArrowFunctionExpression,this.id=null,this.params=t,this.body=e,this.generator=!1,this.expression=n,this.async=!1}return t}();e.ArrowFunctionExpression=s;var a=function(){function t(t,e,n){this.type=r.Syntax.AssignmentExpression,this.operator=t,this.left=e,this.right=n}return t}();e.AssignmentExpression=a;var u=function(){function t(t,e){this.type=r.Syntax.AssignmentPattern,this.left=t,this.right=e}return t}();e.AssignmentPattern=u;var c=function(){function t(t,e,n){this.type=r.Syntax.ArrowFunctionExpression,this.id=null,this.params=t,this.body=e,this.generator=!1,this.expression=n,this.async=!0}return t}();e.AsyncArrowFunctionExpression=c;var h=function(){function t(t,e,n){this.type=r.Syntax.FunctionDeclaration,this.id=t,this.params=e,this.body=n,this.generator=!1,this.expression=!1,this.async=!0}return t}();e.AsyncFunctionDeclaration=h;var l=function(){function t(t,e,n){this.type=r.Syntax.FunctionExpression,this.id=t,this.params=e,this.body=n,this.generator=!1,this.expression=!1,this.async=!0}return t}();e.AsyncFunctionExpression=l;var p=function(){function t(t){this.type=r.Syntax.AwaitExpression,this.argument=t}return t}();e.AwaitExpression=p;var f=function(){function t(t,e,n){var i="||"===t||"&&"===t;this.type=i?r.Syntax.LogicalExpression:r.Syntax.BinaryExpression,this.operator=t,this.left=e,this.right=n}return t}();e.BinaryExpression=f;var d=function(){function t(t){this.type=r.Syntax.BlockStatement,this.body=t}return t}();e.BlockStatement=d;var m=function(){function t(t){this.type=r.Syntax.BreakStatement,this.label=t}return t}();e.BreakStatement=m;var y=function(){function t(t,e){this.type=r.Syntax.CallExpression,this.callee=t,this.arguments=e}return t}();e.CallExpression=y;var v=function(){function t(t,e){this.type=r.Syntax.CatchClause,this.param=t,this.body=e}return t}();e.CatchClause=v;var x=function(){function t(t){this.type=r.Syntax.ClassBody,this.body=t}return t}();e.ClassBody=x;var g=function(){function t(t,e,n){this.type=r.Syntax.ClassDeclaration,this.id=t,this.superClass=e,this.body=n}return t}();e.ClassDeclaration=g;var D=function(){function t(t,e,n){this.type=r.Syntax.ClassExpression,this.id=t,this.superClass=e,this.body=n}return t}();e.ClassExpression=D;var E=function(){function t(t,e){this.type=r.Syntax.MemberExpression,this.computed=!0,this.object=t,this.property=e}return t}();e.ComputedMemberExpression=E;var A=function(){function t(t,e,n){this.type=r.Syntax.ConditionalExpression,this.test=t,this.consequent=e,this.alternate=n}return t}();e.ConditionalExpression=A;var S=function(){function t(t){this.type=r.Syntax.ContinueStatement,this.label=t}return t}();e.ContinueStatement=S;var w=function(){function t(){this.type=r.Syntax.DebuggerStatement}return t}();e.DebuggerStatement=w;var C=function(){function t(t,e){this.type=r.Syntax.ExpressionStatement,this.expression=t,this.directive=e}return t}();e.Directive=C;var _=function(){function t(t,e){this.type=r.Syntax.DoWhileStatement,this.body=t,this.test=e}return t}();e.DoWhileStatement=_;var b=function(){function t(){this.type=r.Syntax.EmptyStatement}return t}();e.EmptyStatement=b;var F=function(){function t(t){this.type=r.Syntax.ExportAllDeclaration,this.source=t}return t}();e.ExportAllDeclaration=F;var k=function(){function t(t){this.type=r.Syntax.ExportDefaultDeclaration,this.declaration=t}return t}();e.ExportDefaultDeclaration=k;var I=function(){function t(t,e,n){this.type=r.Syntax.ExportNamedDeclaration,this.declaration=t,this.specifiers=e,this.source=n}return t}();e.ExportNamedDeclaration=I;var T=function(){function t(t,e){this.type=r.Syntax.ExportSpecifier,this.exported=e,this.local=t}return t}();e.ExportSpecifier=T;var B=function(){function t(t){this.type=r.Syntax.ExpressionStatement,this.expression=t}return t}();e.ExpressionStatement=B;var M=function(){function t(t,e,n){this.type=r.Syntax.ForInStatement,this.left=t,this.right=e,this.body=n,this.each=!1}return t}();e.ForInStatement=M;var P=function(){function t(t,e,n){this.type=r.Syntax.ForOfStatement,this.left=t,this.right=e,this.body=n}return t}();e.ForOfStatement=P;var N=function(){function t(t,e,n,i){this.type=r.Syntax.ForStatement,this.init=t,this.test=e,this.update=n,this.body=i}return t}();e.ForStatement=N;var O=function(){function t(t,e,n,i){this.type=r.Syntax.FunctionDeclaration,this.id=t,this.params=e,this.body=n,this.generator=i,this.expression=!1,this.async=!1}return t}();e.FunctionDeclaration=O;var R=function(){function t(t,e,n,i){this.type=r.Syntax.FunctionExpression,this.id=t,this.params=e,this.body=n,this.generator=i,this.expression=!1,this.async=!1}return t}();e.FunctionExpression=R;var U=function(){function t(t){this.type=r.Syntax.Identifier,this.name=t}return t}();e.Identifier=U;var j=function(){function t(t,e,n){this.type=r.Syntax.IfStatement,this.test=t,this.consequent=e,this.alternate=n}return t}();e.IfStatement=j;var L=function(){function t(t,e){this.type=r.Syntax.ImportDeclaration,this.specifiers=t,this.source=e}return t}();e.ImportDeclaration=L;var z=function(){function t(t){this.type=r.Syntax.ImportDefaultSpecifier,this.local=t}return t}();e.ImportDefaultSpecifier=z;var J=function(){function t(t){this.type=r.Syntax.ImportNamespaceSpecifier,this.local=t}return t}();e.ImportNamespaceSpecifier=J;var X=function(){function t(t,e){this.type=r.Syntax.ImportSpecifier,this.local=t,this.imported=e}return t}();e.ImportSpecifier=X;var q=function(){function t(t,e){this.type=r.Syntax.LabeledStatement,this.label=t,this.body=e}return t}();e.LabeledStatement=q;var K=function(){function t(t,e){this.type=r.Syntax.Literal,this.value=t,this.raw=e}return t}();e.Literal=K;var Y=function(){function t(t,e){this.type=r.Syntax.MetaProperty,this.meta=t,this.property=e}return t}();e.MetaProperty=Y;var W=function(){function t(t,e,n,i,o){this.type=r.Syntax.MethodDefinition,this.key=t,this.computed=e,this.value=n,this.kind=i,this.static=o}return t}();e.MethodDefinition=W;var G=function(){function t(t){this.type=r.Syntax.Program,this.body=t,this.sourceType="module"}return t}();e.Module=G;var H=function(){function t(t,e){this.type=r.Syntax.NewExpression,this.callee=t,this.arguments=e}return t}();e.NewExpression=H;var V=function(){function t(t){this.type=r.Syntax.ObjectExpression,this.properties=t}return t}();e.ObjectExpression=V;var $=function(){function t(t){this.type=r.Syntax.ObjectPattern,this.properties=t}return t}();e.ObjectPattern=$;var Z=function(){function t(t,e,n,i,o,s){this.type=r.Syntax.Property,this.key=e,this.computed=n,this.value=i,this.kind=t,this.method=o,this.shorthand=s}return t}();e.Property=Z;var Q=function(){function t(t,e,n,i){this.type=r.Syntax.Literal,this.value=t,this.raw=e,this.regex={pattern:n,flags:i}}return t}();e.RegexLiteral=Q;var tt=function(){function t(t){this.type=r.Syntax.RestElement,this.argument=t}return t}();e.RestElement=tt;var et=function(){function t(t){this.type=r.Syntax.ReturnStatement,this.argument=t}return t}();e.ReturnStatement=et;var nt=function(){function t(t){this.type=r.Syntax.Program,this.body=t,this.sourceType="script"}return t}();e.Script=nt;var rt=function(){function t(t){this.type=r.Syntax.SequenceExpression,this.expressions=t}return t}();e.SequenceExpression=rt;var it=function(){function t(t){this.type=r.Syntax.SpreadElement,this.argument=t}return t}();e.SpreadElement=it;var ot=function(){function t(t,e){this.type=r.Syntax.MemberExpression,this.computed=!1,this.object=t,this.property=e}return t}();e.StaticMemberExpression=ot;var st=function(){function t(){this.type=r.Syntax.Super}return t}();e.Super=st;var at=function(){function t(t,e){this.type=r.Syntax.SwitchCase,this.test=t,this.consequent=e}return t}();e.SwitchCase=at;var ut=function(){function t(t,e){this.type=r.Syntax.SwitchStatement,this.discriminant=t,this.cases=e}return t}();e.SwitchStatement=ut;var ct=function(){function t(t,e){this.type=r.Syntax.TaggedTemplateExpression,this.tag=t,this.quasi=e}return t}();e.TaggedTemplateExpression=ct;var ht=function(){function t(t,e){this.type=r.Syntax.TemplateElement,this.value=t,this.tail=e}return t}();e.TemplateElement=ht;var lt=function(){function t(t,e){this.type=r.Syntax.TemplateLiteral,this.quasis=t,this.expressions=e}return t}();e.TemplateLiteral=lt;var pt=function(){function t(){this.type=r.Syntax.ThisExpression}return t}();e.ThisExpression=pt;var ft=function(){function t(t){this.type=r.Syntax.ThrowStatement,this.argument=t}return t}();e.ThrowStatement=ft;var dt=function(){function t(t,e,n){this.type=r.Syntax.TryStatement,this.block=t,this.handler=e,this.finalizer=n}return t}();e.TryStatement=dt;var mt=function(){function t(t,e){this.type=r.Syntax.UnaryExpression,this.operator=t,this.argument=e,this.prefix=!0}return t}();e.UnaryExpression=mt;var yt=function(){function t(t,e,n){this.type=r.Syntax.UpdateExpression,this.operator=t,this.argument=e,this.prefix=n}return t}();e.UpdateExpression=yt;var vt=function(){function t(t,e){this.type=r.Syntax.VariableDeclaration,this.declarations=t,this.kind=e}return t}();e.VariableDeclaration=vt;var xt=function(){function t(t,e){this.type=r.Syntax.VariableDeclarator,this.id=t,this.init=e}return t}();e.VariableDeclarator=xt;var gt=function(){function t(t,e){this.type=r.Syntax.WhileStatement,this.test=t,this.body=e}return t}();e.WhileStatement=gt;var Dt=function(){function t(t,e){this.type=r.Syntax.WithStatement,this.object=t,this.body=e}return t}();e.WithStatement=Dt;var Et=function(){function t(t,e){this.type=r.Syntax.YieldExpression,this.argument=t,this.delegate=e}return t}();e.YieldExpression=Et},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n(9),i=n(10),o=n(11),s=n(7),a=n(12),u=n(2),c=n(13),h=function(){function t(t,e,n){void 0===e&&(e={}),this.config={range:"boolean"==typeof e.range&&e.range,loc:"boolean"==typeof e.loc&&e.loc,source:null,tokens:"boolean"==typeof e.tokens&&e.tokens,comment:"boolean"==typeof e.comment&&e.comment,tolerant:"boolean"==typeof e.tolerant&&e.tolerant},this.config.loc&&e.source&&null!==e.source&&(this.config.source=String(e.source)),this.delegate=n,this.errorHandler=new i.ErrorHandler,this.errorHandler.tolerant=this.config.tolerant,this.scanner=new a.Scanner(t,this.errorHandler),this.scanner.trackComment=this.config.comment,this.operatorPrecedence={")":0,";":0,",":0,"=":0,"]":0,"||":1,"&&":2,"|":3,"^":4,"&":5,"==":6,"!=":6,"===":6,"!==":6,"<":7,">":7,"<=":7,">=":7,"<<":8,">>":8,">>>":8,"+":9,"-":9,"*":11,"/":11,"%":11},this.lookahead={type:2,value:"",lineNumber:this.scanner.lineNumber,lineStart:0,start:0,end:0},this.hasLineTerminator=!1,this.context={isModule:!1,await:!1,allowIn:!0,allowStrictDirective:!0,allowYield:!0,firstCoverInitializedNameError:null,isAssignmentTarget:!1,isBindingElement:!1,inFunctionBody:!1,inIteration:!1,inSwitch:!1,labelSet:{},strict:!1},this.tokens=[],this.startMarker={index:0,line:this.scanner.lineNumber,column:0},this.lastMarker={index:0,line:this.scanner.lineNumber,column:0},this.nextToken(),this.lastMarker={index:this.scanner.index,line:this.scanner.lineNumber,column:this.scanner.index-this.scanner.lineStart}}return t.prototype.throwError=function(t){for(var e=[],n=1;n<arguments.length;n++)e[n-1]=arguments[n];var i=Array.prototype.slice.call(arguments,1),o=t.replace(/%(\d)/g,function(t,e){return r.assert(e<i.length,"Message reference must be in range"),i[e]}),s=this.lastMarker.index,a=this.lastMarker.line,u=this.lastMarker.column+1;throw this.errorHandler.createError(s,a,u,o)},t.prototype.tolerateError=function(t){for(var e=[],n=1;n<arguments.length;n++)e[n-1]=arguments[n];var i=Array.prototype.slice.call(arguments,1),o=t.replace(/%(\d)/g,function(t,e){return r.assert(e<i.length,"Message reference must be in range"),i[e]}),s=this.lastMarker.index,a=this.scanner.lineNumber,u=this.lastMarker.column+1;this.errorHandler.tolerateError(s,a,u,o)},t.prototype.unexpectedTokenError=function(t,e){var n,r=e||o.Messages.UnexpectedToken;if(t?(e||(r=2===t.type?o.Messages.UnexpectedEOS:3===t.type?o.Messages.UnexpectedIdentifier:6===t.type?o.Messages.UnexpectedNumber:8===t.type?o.Messages.UnexpectedString:10===t.type?o.Messages.UnexpectedTemplate:o.Messages.UnexpectedToken,4===t.type&&(this.scanner.isFutureReservedWord(t.value)?r=o.Messages.UnexpectedReserved:this.context.strict&&this.scanner.isStrictModeReservedWord(t.value)&&(r=o.Messages.StrictReservedWord))),n=t.value):n="ILLEGAL",r=r.replace("%0",n),t&&"number"==typeof t.lineNumber){var i=t.start,s=t.lineNumber,a=this.lastMarker.index-this.lastMarker.column,u=t.start-a+1;return this.errorHandler.createError(i,s,u,r)}var i=this.lastMarker.index,s=this.lastMarker.line,u=this.lastMarker.column+1;return this.errorHandler.createError(i,s,u,r)},t.prototype.throwUnexpectedToken=function(t,e){throw this.unexpectedTokenError(t,e)},t.prototype.tolerateUnexpectedToken=function(t,e){this.errorHandler.tolerate(this.unexpectedTokenError(t,e))},t.prototype.collectComments=function(){if(this.config.comment){var t=this.scanner.scanComments();if(t.length>0&&this.delegate)for(var e=0;e<t.length;++e){var n=t[e],r=void 0;r={type:n.multiLine?"BlockComment":"LineComment",value:this.scanner.source.slice(n.slice[0],n.slice[1])},this.config.range&&(r.range=n.range),this.config.loc&&(r.loc=n.loc);var i={start:{line:n.loc.start.line,column:n.loc.start.column,offset:n.range[0]},end:{line:n.loc.end.line,column:n.loc.end.column,offset:n.range[1]}};this.delegate(r,i)}}else this.scanner.scanComments()},t.prototype.getTokenRaw=function(t){return this.scanner.source.slice(t.start,t.end)},t.prototype.convertToken=function(t){var e={type:c.TokenName[t.type],value:this.getTokenRaw(t)};if(this.config.range&&(e.range=[t.start,t.end]),this.config.loc&&(e.loc={start:{line:this.startMarker.line,column:this.startMarker.column},end:{line:this.scanner.lineNumber,column:this.scanner.index-this.scanner.lineStart}}),9===t.type){var n=t.pattern,r=t.flags;e.regex={pattern:n,flags:r}}return e},t.prototype.nextToken=function(){var t=this.lookahead;this.lastMarker.index=this.scanner.index,this.lastMarker.line=this.scanner.lineNumber,this.lastMarker.column=this.scanner.index-this.scanner.lineStart,this.collectComments(),this.scanner.index!==this.startMarker.index&&(this.startMarker.index=this.scanner.index,this.startMarker.line=this.scanner.lineNumber,this.startMarker.column=this.scanner.index-this.scanner.lineStart);var e=this.scanner.lex();return this.hasLineTerminator=t.lineNumber!==e.lineNumber,e&&this.context.strict&&3===e.type&&this.scanner.isStrictModeReservedWord(e.value)&&(e.type=4),this.lookahead=e,this.config.tokens&&2!==e.type&&this.tokens.push(this.convertToken(e)),t},t.prototype.nextRegexToken=function(){this.collectComments();var t=this.scanner.scanRegExp();return this.config.tokens&&(this.tokens.pop(),this.tokens.push(this.convertToken(t))),this.lookahead=t,this.nextToken(),t},t.prototype.createNode=function(){return{index:this.startMarker.index,line:this.startMarker.line,column:this.startMarker.column}},t.prototype.startNode=function(t){return{index:t.start,line:t.lineNumber,column:t.start-t.lineStart}},t.prototype.finalize=function(t,e){if(this.config.range&&(e.range=[t.index,this.lastMarker.index]),this.config.loc&&(e.loc={start:{line:t.line,column:t.column},end:{line:this.lastMarker.line,column:this.lastMarker.column}},this.config.source&&(e.loc.source=this.config.source)),this.delegate){var n={start:{line:t.line,column:t.column,offset:t.index},end:{line:this.lastMarker.line,column:this.lastMarker.column,offset:this.lastMarker.index}};this.delegate(e,n)}return e},t.prototype.expect=function(t){var e=this.nextToken();7===e.type&&e.value===t||this.throwUnexpectedToken(e)},t.prototype.expectCommaSeparator=function(){if(this.config.tolerant){var t=this.lookahead;7===t.type&&","===t.value?this.nextToken():7===t.type&&";"===t.value?(this.nextToken(),this.tolerateUnexpectedToken(t)):this.tolerateUnexpectedToken(t,o.Messages.UnexpectedToken)}else this.expect(",")},t.prototype.expectKeyword=function(t){var e=this.nextToken();4===e.type&&e.value===t||this.throwUnexpectedToken(e)},t.prototype.match=function(t){return 7===this.lookahead.type&&this.lookahead.value===t},t.prototype.matchKeyword=function(t){return 4===this.lookahead.type&&this.lookahead.value===t},t.prototype.matchContextualKeyword=function(t){return 3===this.lookahead.type&&this.lookahead.value===t},t.prototype.matchAssign=function(){if(7!==this.lookahead.type)return!1;var t=this.lookahead.value;return"="===t||"*="===t||"**="===t||"/="===t||"%="===t||"+="===t||"-="===t||"<<="===t||">>="===t||">>>="===t||"&="===t||"^="===t||"|="===t},t.prototype.isolateCoverGrammar=function(t){var e=this.context.isBindingElement,n=this.context.isAssignmentTarget,r=this.context.firstCoverInitializedNameError;this.context.isBindingElement=!0,this.context.isAssignmentTarget=!0,this.context.firstCoverInitializedNameError=null;var i=t.call(this);return null!==this.context.firstCoverInitializedNameError&&this.throwUnexpectedToken(this.context.firstCoverInitializedNameError),this.context.isBindingElement=e,this.context.isAssignmentTarget=n,this.context.firstCoverInitializedNameError=r,i},t.prototype.inheritCoverGrammar=function(t){var e=this.context.isBindingElement,n=this.context.isAssignmentTarget,r=this.context.firstCoverInitializedNameError;this.context.isBindingElement=!0,this.context.isAssignmentTarget=!0,this.context.firstCoverInitializedNameError=null;var i=t.call(this);return this.context.isBindingElement=this.context.isBindingElement&&e,this.context.isAssignmentTarget=this.context.isAssignmentTarget&&n,this.context.firstCoverInitializedNameError=r||this.context.firstCoverInitializedNameError,i},t.prototype.consumeSemicolon=function(){this.match(";")?this.nextToken():this.hasLineTerminator||(2===this.lookahead.type||this.match("}")||this.throwUnexpectedToken(this.lookahead),this.lastMarker.index=this.startMarker.index,this.lastMarker.line=this.startMarker.line,this.lastMarker.column=this.startMarker.column)},t.prototype.parsePrimaryExpression=function(){var t,e,n,r=this.createNode();switch(this.lookahead.type){case 3:(this.context.isModule||this.context.await)&&"await"===this.lookahead.value&&this.tolerateUnexpectedToken(this.lookahead),t=this.matchAsyncFunction()?this.parseFunctionExpression():this.finalize(r,new s.Identifier(this.nextToken().value));break;case 6:case 8:this.context.strict&&this.lookahead.octal&&this.tolerateUnexpectedToken(this.lookahead,o.Messages.StrictOctalLiteral),this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1,e=this.nextToken(),n=this.getTokenRaw(e),t=this.finalize(r,new s.Literal(e.value,n));break;case 1:this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1,e=this.nextToken(),n=this.getTokenRaw(e),t=this.finalize(r,new s.Literal("true"===e.value,n));break;case 5:this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1,e=this.nextToken(),n=this.getTokenRaw(e),t=this.finalize(r,new s.Literal(null,n));break;case 10:t=this.parseTemplateLiteral();break;case 7:switch(this.lookahead.value){case"(":this.context.isBindingElement=!1,t=this.inheritCoverGrammar(this.parseGroupExpression);break;case"[":t=this.inheritCoverGrammar(this.parseArrayInitializer);break;case"{":t=this.inheritCoverGrammar(this.parseObjectInitializer);break;case"/":case"/=":this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1,this.scanner.index=this.startMarker.index,e=this.nextRegexToken(),n=this.getTokenRaw(e),t=this.finalize(r,new s.RegexLiteral(e.regex,n,e.pattern,e.flags));break;default:t=this.throwUnexpectedToken(this.nextToken())}break;case 4:!this.context.strict&&this.context.allowYield&&this.matchKeyword("yield")?t=this.parseIdentifierName():!this.context.strict&&this.matchKeyword("let")?t=this.finalize(r,new s.Identifier(this.nextToken().value)):(this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1,this.matchKeyword("function")?t=this.parseFunctionExpression():this.matchKeyword("this")?(this.nextToken(),t=this.finalize(r,new s.ThisExpression)):t=this.matchKeyword("class")?this.parseClassExpression():this.throwUnexpectedToken(this.nextToken()));break;default:t=this.throwUnexpectedToken(this.nextToken())}return t},t.prototype.parseSpreadElement=function(){var t=this.createNode();this.expect("...");var e=this.inheritCoverGrammar(this.parseAssignmentExpression);return this.finalize(t,new s.SpreadElement(e))},t.prototype.parseArrayInitializer=function(){var t=this.createNode(),e=[];for(this.expect("[");!this.match("]");)if(this.match(","))this.nextToken(),e.push(null);else if(this.match("...")){var n=this.parseSpreadElement();this.match("]")||(this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1,this.expect(",")),e.push(n)}else e.push(this.inheritCoverGrammar(this.parseAssignmentExpression)),this.match("]")||this.expect(",");return this.expect("]"),this.finalize(t,new s.ArrayExpression(e))},t.prototype.parsePropertyMethod=function(t){this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1;var e=this.context.strict,n=this.context.allowStrictDirective;this.context.allowStrictDirective=t.simple;var r=this.isolateCoverGrammar(this.parseFunctionSourceElements);return this.context.strict&&t.firstRestricted&&this.tolerateUnexpectedToken(t.firstRestricted,t.message),this.context.strict&&t.stricted&&this.tolerateUnexpectedToken(t.stricted,t.message),this.context.strict=e,this.context.allowStrictDirective=n,r},t.prototype.parsePropertyMethodFunction=function(){var t=this.createNode(),e=this.context.allowYield;this.context.allowYield=!1;var n=this.parseFormalParameters(),r=this.parsePropertyMethod(n);return this.context.allowYield=e,this.finalize(t,new s.FunctionExpression(null,n.params,r,!1))},t.prototype.parsePropertyMethodAsyncFunction=function(){var t=this.createNode(),e=this.context.allowYield,n=this.context.await;this.context.allowYield=!1,this.context.await=!0;var r=this.parseFormalParameters(),i=this.parsePropertyMethod(r);return this.context.allowYield=e,this.context.await=n,this.finalize(t,new s.AsyncFunctionExpression(null,r.params,i))},t.prototype.parseObjectPropertyKey=function(){var t,e=this.createNode(),n=this.nextToken();switch(n.type){case 8:case 6:this.context.strict&&n.octal&&this.tolerateUnexpectedToken(n,o.Messages.StrictOctalLiteral);var r=this.getTokenRaw(n);t=this.finalize(e,new s.Literal(n.value,r));break;case 3:case 1:case 5:case 4:t=this.finalize(e,new s.Identifier(n.value));break;case 7:"["===n.value?(t=this.isolateCoverGrammar(this.parseAssignmentExpression),this.expect("]")):t=this.throwUnexpectedToken(n);break;default:t=this.throwUnexpectedToken(n)}return t},t.prototype.isPropertyKey=function(t,e){return t.type===u.Syntax.Identifier&&t.name===e||t.type===u.Syntax.Literal&&t.value===e},t.prototype.parseObjectProperty=function(t){var e,n=this.createNode(),r=this.lookahead,i=null,a=null,u=!1,c=!1,h=!1,l=!1;if(3===r.type){var p=r.value;this.nextToken(),u=this.match("["),l=!(this.hasLineTerminator||"async"!==p||this.match(":")||this.match("(")||this.match("*")),i=l?this.parseObjectPropertyKey():this.finalize(n,new s.Identifier(p))}else this.match("*")?this.nextToken():(u=this.match("["),i=this.parseObjectPropertyKey());var f=this.qualifiedPropertyName(this.lookahead);if(3===r.type&&!l&&"get"===r.value&&f)e="get",u=this.match("["),i=this.parseObjectPropertyKey(),this.context.allowYield=!1,a=this.parseGetterMethod();else if(3===r.type&&!l&&"set"===r.value&&f)e="set",u=this.match("["),i=this.parseObjectPropertyKey(),a=this.parseSetterMethod();else if(7===r.type&&"*"===r.value&&f)e="init",u=this.match("["),i=this.parseObjectPropertyKey(),a=this.parseGeneratorMethod(),c=!0;else if(i||this.throwUnexpectedToken(this.lookahead),e="init",this.match(":")&&!l)!u&&this.isPropertyKey(i,"__proto__")&&(t.value&&this.tolerateError(o.Messages.DuplicateProtoProperty),t.value=!0),this.nextToken(),a=this.inheritCoverGrammar(this.parseAssignmentExpression);else if(this.match("("))a=l?this.parsePropertyMethodAsyncFunction():this.parsePropertyMethodFunction(),c=!0;else if(3===r.type){var p=this.finalize(n,new s.Identifier(r.value));if(this.match("=")){this.context.firstCoverInitializedNameError=this.lookahead,this.nextToken(),h=!0;var d=this.isolateCoverGrammar(this.parseAssignmentExpression);a=this.finalize(n,new s.AssignmentPattern(p,d))}else h=!0,a=p}else this.throwUnexpectedToken(this.nextToken());return this.finalize(n,new s.Property(e,i,u,a,c,h))},t.prototype.parseObjectInitializer=function(){var t=this.createNode();this.expect("{");for(var e=[],n={value:!1};!this.match("}");)e.push(this.parseObjectProperty(n)),this.match("}")||this.expectCommaSeparator();return this.expect("}"),this.finalize(t,new s.ObjectExpression(e))},t.prototype.parseTemplateHead=function(){r.assert(this.lookahead.head,"Template literal must start with a template head");var t=this.createNode(),e=this.nextToken(),n=e.value,i=e.cooked;return this.finalize(t,new s.TemplateElement({raw:n,cooked:i},e.tail))},t.prototype.parseTemplateElement=function(){10!==this.lookahead.type&&this.throwUnexpectedToken();var t=this.createNode(),e=this.nextToken(),n=e.value,r=e.cooked;return this.finalize(t,new s.TemplateElement({raw:n,cooked:r},e.tail))},t.prototype.parseTemplateLiteral=function(){var t=this.createNode(),e=[],n=[],r=this.parseTemplateHead();for(n.push(r);!r.tail;)e.push(this.parseExpression()),r=this.parseTemplateElement(),n.push(r);return this.finalize(t,new s.TemplateLiteral(n,e))},t.prototype.reinterpretExpressionAsPattern=function(t){switch(t.type){case u.Syntax.Identifier:case u.Syntax.MemberExpression:case u.Syntax.RestElement:case u.Syntax.AssignmentPattern:break;case u.Syntax.SpreadElement:t.type=u.Syntax.RestElement,this.reinterpretExpressionAsPattern(t.argument);break;case u.Syntax.ArrayExpression:t.type=u.Syntax.ArrayPattern;for(var e=0;e<t.elements.length;e++)null!==t.elements[e]&&this.reinterpretExpressionAsPattern(t.elements[e]);break;case u.Syntax.ObjectExpression:t.type=u.Syntax.ObjectPattern;for(var e=0;e<t.properties.length;e++)this.reinterpretExpressionAsPattern(t.properties[e].value);break;case u.Syntax.AssignmentExpression:t.type=u.Syntax.AssignmentPattern,delete t.operator,this.reinterpretExpressionAsPattern(t.left)}},t.prototype.parseGroupExpression=function(){var t;if(this.expect("("),this.match(")"))this.nextToken(),this.match("=>")||this.expect("=>"),t={type:"ArrowParameterPlaceHolder",params:[],async:!1};else{var e=this.lookahead,n=[];if(this.match("..."))t=this.parseRestElement(n),this.expect(")"),this.match("=>")||this.expect("=>"),t={type:"ArrowParameterPlaceHolder",params:[t],async:!1};else{var r=!1;if(this.context.isBindingElement=!0,t=this.inheritCoverGrammar(this.parseAssignmentExpression),this.match(",")){var i=[];for(this.context.isAssignmentTarget=!1,i.push(t);2!==this.lookahead.type&&this.match(",");){if(this.nextToken(),this.match(")")){this.nextToken();for(var o=0;o<i.length;o++)this.reinterpretExpressionAsPattern(i[o]);r=!0,t={type:"ArrowParameterPlaceHolder",params:i,async:!1}}else if(this.match("...")){this.context.isBindingElement||this.throwUnexpectedToken(this.lookahead),i.push(this.parseRestElement(n)),this.expect(")"),this.match("=>")||this.expect("=>"),this.context.isBindingElement=!1;for(var o=0;o<i.length;o++)this.reinterpretExpressionAsPattern(i[o]);r=!0,t={type:"ArrowParameterPlaceHolder",params:i,async:!1}}else i.push(this.inheritCoverGrammar(this.parseAssignmentExpression));if(r)break}r||(t=this.finalize(this.startNode(e),new s.SequenceExpression(i)))}if(!r){if(this.expect(")"),this.match("=>")&&(t.type===u.Syntax.Identifier&&"yield"===t.name&&(r=!0,t={type:"ArrowParameterPlaceHolder",params:[t],async:!1}),!r)){if(this.context.isBindingElement||this.throwUnexpectedToken(this.lookahead),t.type===u.Syntax.SequenceExpression)for(var o=0;o<t.expressions.length;o++)this.reinterpretExpressionAsPattern(t.expressions[o]);else this.reinterpretExpressionAsPattern(t);t={type:"ArrowParameterPlaceHolder",params:t.type===u.Syntax.SequenceExpression?t.expressions:[t],async:!1}}this.context.isBindingElement=!1}}}return t},t.prototype.parseArguments=function(){this.expect("(");var t=[];if(!this.match(")"))for(;;){var e=this.match("...")?this.parseSpreadElement():this.isolateCoverGrammar(this.parseAssignmentExpression);if(t.push(e),this.match(")"))break;if(this.expectCommaSeparator(),this.match(")"))break}return this.expect(")"),t},t.prototype.isIdentifierName=function(t){return 3===t.type||4===t.type||1===t.type||5===t.type},t.prototype.parseIdentifierName=function(){var t=this.createNode(),e=this.nextToken();return this.isIdentifierName(e)||this.throwUnexpectedToken(e),this.finalize(t,new s.Identifier(e.value))},t.prototype.parseNewExpression=function(){var t=this.createNode(),e=this.parseIdentifierName();r.assert("new"===e.name,"New expression must start with `new`");var n;if(this.match("."))if(this.nextToken(),3===this.lookahead.type&&this.context.inFunctionBody&&"target"===this.lookahead.value){var i=this.parseIdentifierName();n=new s.MetaProperty(e,i)}else this.throwUnexpectedToken(this.lookahead);else{var o=this.isolateCoverGrammar(this.parseLeftHandSideExpression),a=this.match("(")?this.parseArguments():[];n=new s.NewExpression(o,a),this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1}return this.finalize(t,n)},t.prototype.parseAsyncArgument=function(){var t=this.parseAssignmentExpression();return this.context.firstCoverInitializedNameError=null,t},t.prototype.parseAsyncArguments=function(){this.expect("(");var t=[];if(!this.match(")"))for(;;){var e=this.match("...")?this.parseSpreadElement():this.isolateCoverGrammar(this.parseAsyncArgument);if(t.push(e),this.match(")"))break;if(this.expectCommaSeparator(),this.match(")"))break}return this.expect(")"),t},t.prototype.parseLeftHandSideExpressionAllowCall=function(){var t=this.lookahead,e=this.matchContextualKeyword("async"),n=this.context.allowIn;this.context.allowIn=!0;var r;for(this.matchKeyword("super")&&this.context.inFunctionBody?(r=this.createNode(),this.nextToken(),r=this.finalize(r,new s.Super),this.match("(")||this.match(".")||this.match("[")||this.throwUnexpectedToken(this.lookahead)):r=this.inheritCoverGrammar(this.matchKeyword("new")?this.parseNewExpression:this.parsePrimaryExpression);;)if(this.match(".")){this.context.isBindingElement=!1,this.context.isAssignmentTarget=!0,this.expect(".");var i=this.parseIdentifierName();r=this.finalize(this.startNode(t),new s.StaticMemberExpression(r,i))}else if(this.match("(")){var o=e&&t.lineNumber===this.lookahead.lineNumber;this.context.isBindingElement=!1,this.context.isAssignmentTarget=!1;var a=o?this.parseAsyncArguments():this.parseArguments();if(r=this.finalize(this.startNode(t),new s.CallExpression(r,a)),o&&this.match("=>")){for(var u=0;u<a.length;++u)this.reinterpretExpressionAsPattern(a[u]);r={type:"ArrowParameterPlaceHolder",params:a,async:!0}}}else if(this.match("[")){this.context.isBindingElement=!1,this.context.isAssignmentTarget=!0,this.expect("[");var i=this.isolateCoverGrammar(this.parseExpression);this.expect("]"),r=this.finalize(this.startNode(t),new s.ComputedMemberExpression(r,i))}else{if(10!==this.lookahead.type||!this.lookahead.head)break;var c=this.parseTemplateLiteral();r=this.finalize(this.startNode(t),new s.TaggedTemplateExpression(r,c))}return this.context.allowIn=n,r},t.prototype.parseSuper=function(){var t=this.createNode();return this.expectKeyword("super"),this.match("[")||this.match(".")||this.throwUnexpectedToken(this.lookahead),this.finalize(t,new s.Super)},t.prototype.parseLeftHandSideExpression=function(){r.assert(this.context.allowIn,"callee of new expression always allow in keyword.");for(var t=this.startNode(this.lookahead),e=this.matchKeyword("super")&&this.context.inFunctionBody?this.parseSuper():this.inheritCoverGrammar(this.matchKeyword("new")?this.parseNewExpression:this.parsePrimaryExpression);;)if(this.match("[")){this.context.isBindingElement=!1,this.context.isAssignmentTarget=!0,this.expect("[");var n=this.isolateCoverGrammar(this.parseExpression);this.expect("]"),e=this.finalize(t,new s.ComputedMemberExpression(e,n))}else if(this.match(".")){this.context.isBindingElement=!1,this.context.isAssignmentTarget=!0,this.expect(".");var n=this.parseIdentifierName();e=this.finalize(t,new s.StaticMemberExpression(e,n))}else{if(10!==this.lookahead.type||!this.lookahead.head)break;var i=this.parseTemplateLiteral();e=this.finalize(t,new s.TaggedTemplateExpression(e,i))}return e},t.prototype.parseUpdateExpression=function(){var t,e=this.lookahead;if(this.match("++")||this.match("--")){var n=this.startNode(e),r=this.nextToken();t=this.inheritCoverGrammar(this.parseUnaryExpression),this.context.strict&&t.type===u.Syntax.Identifier&&this.scanner.isRestrictedWord(t.name)&&this.tolerateError(o.Messages.StrictLHSPrefix),this.context.isAssignmentTarget||this.tolerateError(o.Messages.InvalidLHSInAssignment);var i=!0;t=this.finalize(n,new s.UpdateExpression(r.value,t,i)),this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1}else if(t=this.inheritCoverGrammar(this.parseLeftHandSideExpressionAllowCall),!this.hasLineTerminator&&7===this.lookahead.type&&(this.match("++")||this.match("--"))){this.context.strict&&t.type===u.Syntax.Identifier&&this.scanner.isRestrictedWord(t.name)&&this.tolerateError(o.Messages.StrictLHSPostfix),this.context.isAssignmentTarget||this.tolerateError(o.Messages.InvalidLHSInAssignment),this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1;var a=this.nextToken().value,i=!1;t=this.finalize(this.startNode(e),new s.UpdateExpression(a,t,i))}return t},t.prototype.parseAwaitExpression=function(){var t=this.createNode();this.nextToken();var e=this.parseUnaryExpression();return this.finalize(t,new s.AwaitExpression(e))},t.prototype.parseUnaryExpression=function(){var t;if(this.match("+")||this.match("-")||this.match("~")||this.match("!")||this.matchKeyword("delete")||this.matchKeyword("void")||this.matchKeyword("typeof")){var e=this.startNode(this.lookahead),n=this.nextToken();t=this.inheritCoverGrammar(this.parseUnaryExpression),t=this.finalize(e,new s.UnaryExpression(n.value,t)),this.context.strict&&"delete"===t.operator&&t.argument.type===u.Syntax.Identifier&&this.tolerateError(o.Messages.StrictDelete),this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1}else t=this.context.await&&this.matchContextualKeyword("await")?this.parseAwaitExpression():this.parseUpdateExpression();return t},t.prototype.parseExponentiationExpression=function(){var t=this.lookahead,e=this.inheritCoverGrammar(this.parseUnaryExpression);if(e.type!==u.Syntax.UnaryExpression&&this.match("**")){this.nextToken(),this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1;var n=e,r=this.isolateCoverGrammar(this.parseExponentiationExpression);e=this.finalize(this.startNode(t),new s.BinaryExpression("**",n,r))}return e},t.prototype.binaryPrecedence=function(t){var e=t.value;return 7===t.type?this.operatorPrecedence[e]||0:4===t.type&&("instanceof"===e||this.context.allowIn&&"in"===e)?7:0},t.prototype.parseBinaryExpression=function(){var t=this.lookahead,e=this.inheritCoverGrammar(this.parseExponentiationExpression),n=this.lookahead,r=this.binaryPrecedence(n);if(r>0){this.nextToken(),this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1;for(var i=[t,this.lookahead],o=e,a=this.isolateCoverGrammar(this.parseExponentiationExpression),u=[o,n.value,a],c=[r];;){if((r=this.binaryPrecedence(this.lookahead))<=0)break;for(;u.length>2&&r<=c[c.length-1];){a=u.pop();var h=u.pop();c.pop(),o=u.pop(),i.pop();var l=this.startNode(i[i.length-1]);u.push(this.finalize(l,new s.BinaryExpression(h,o,a)))}u.push(this.nextToken().value),c.push(r),i.push(this.lookahead),u.push(this.isolateCoverGrammar(this.parseExponentiationExpression))}var p=u.length-1;for(e=u[p],i.pop();p>1;){var l=this.startNode(i.pop()),h=u[p-1];e=this.finalize(l,new s.BinaryExpression(h,u[p-2],e)),p-=2}}return e},t.prototype.parseConditionalExpression=function(){var t=this.lookahead,e=this.inheritCoverGrammar(this.parseBinaryExpression);if(this.match("?")){this.nextToken();var n=this.context.allowIn;this.context.allowIn=!0;var r=this.isolateCoverGrammar(this.parseAssignmentExpression);this.context.allowIn=n,this.expect(":");var i=this.isolateCoverGrammar(this.parseAssignmentExpression);e=this.finalize(this.startNode(t),new s.ConditionalExpression(e,r,i)),this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1}return e},t.prototype.checkPatternParam=function(t,e){switch(e.type){case u.Syntax.Identifier:this.validateParam(t,e,e.name);break;case u.Syntax.RestElement:this.checkPatternParam(t,e.argument);break;case u.Syntax.AssignmentPattern:this.checkPatternParam(t,e.left);break;case u.Syntax.ArrayPattern:for(var n=0;n<e.elements.length;n++)null!==e.elements[n]&&this.checkPatternParam(t,e.elements[n]);break;case u.Syntax.ObjectPattern:for(var n=0;n<e.properties.length;n++)this.checkPatternParam(t,e.properties[n].value)}t.simple=t.simple&&e instanceof s.Identifier},t.prototype.reinterpretAsCoverFormalsList=function(t){var e,n=[t],r=!1;switch(t.type){case u.Syntax.Identifier:break;case"ArrowParameterPlaceHolder":n=t.params,r=t.async;break;default:return null}e={simple:!0,paramSet:{}};for(var i=0;i<n.length;++i){var s=n[i];s.type===u.Syntax.AssignmentPattern?s.right.type===u.Syntax.YieldExpression&&(s.right.argument&&this.throwUnexpectedToken(this.lookahead),s.right.type=u.Syntax.Identifier,s.right.name="yield",delete s.right.argument,delete s.right.delegate):r&&s.type===u.Syntax.Identifier&&"await"===s.name&&this.throwUnexpectedToken(this.lookahead),this.checkPatternParam(e,s),n[i]=s}if(this.context.strict||!this.context.allowYield)for(var i=0;i<n.length;++i){var s=n[i];s.type===u.Syntax.YieldExpression&&this.throwUnexpectedToken(this.lookahead)}if(e.message===o.Messages.StrictParamDupe){var a=this.context.strict?e.stricted:e.firstRestricted;this.throwUnexpectedToken(a,e.message)}return{simple:e.simple,params:n,stricted:e.stricted,firstRestricted:e.firstRestricted,message:e.message}},t.prototype.parseAssignmentExpression=function(){var t;if(!this.context.allowYield&&this.matchKeyword("yield"))t=this.parseYieldExpression();else{var e=this.lookahead,n=e;if(t=this.parseConditionalExpression(),3===n.type&&n.lineNumber===this.lookahead.lineNumber&&"async"===n.value&&(3===this.lookahead.type||this.matchKeyword("yield"))){var r=this.parsePrimaryExpression();this.reinterpretExpressionAsPattern(r),t={type:"ArrowParameterPlaceHolder",params:[r],async:!0}}if("ArrowParameterPlaceHolder"===t.type||this.match("=>")){this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1;var i=t.async,a=this.reinterpretAsCoverFormalsList(t);if(a){this.hasLineTerminator&&this.tolerateUnexpectedToken(this.lookahead),this.context.firstCoverInitializedNameError=null;var c=this.context.strict,h=this.context.allowStrictDirective;this.context.allowStrictDirective=a.simple;var l=this.context.allowYield,p=this.context.await;this.context.allowYield=!0,this.context.await=i;var f=this.startNode(e);this.expect("=>");var d=void 0;if(this.match("{")){var m=this.context.allowIn;this.context.allowIn=!0,d=this.parseFunctionSourceElements(),this.context.allowIn=m}else d=this.isolateCoverGrammar(this.parseAssignmentExpression);var y=d.type!==u.Syntax.BlockStatement;this.context.strict&&a.firstRestricted&&this.throwUnexpectedToken(a.firstRestricted,a.message),this.context.strict&&a.stricted&&this.tolerateUnexpectedToken(a.stricted,a.message),t=i?this.finalize(f,new s.AsyncArrowFunctionExpression(a.params,d,y)):this.finalize(f,new s.ArrowFunctionExpression(a.params,d,y)),this.context.strict=c,this.context.allowStrictDirective=h,this.context.allowYield=l,this.context.await=p}}else if(this.matchAssign()){if(this.context.isAssignmentTarget||this.tolerateError(o.Messages.InvalidLHSInAssignment),this.context.strict&&t.type===u.Syntax.Identifier){var v=t;this.scanner.isRestrictedWord(v.name)&&this.tolerateUnexpectedToken(n,o.Messages.StrictLHSAssignment),this.scanner.isStrictModeReservedWord(v.name)&&this.tolerateUnexpectedToken(n,o.Messages.StrictReservedWord)}this.match("=")?this.reinterpretExpressionAsPattern(t):(this.context.isAssignmentTarget=!1,this.context.isBindingElement=!1),n=this.nextToken();var x=n.value,g=this.isolateCoverGrammar(this.parseAssignmentExpression);t=this.finalize(this.startNode(e),new s.AssignmentExpression(x,t,g)),this.context.firstCoverInitializedNameError=null}}return t},t.prototype.parseExpression=function(){var t=this.lookahead,e=this.isolateCoverGrammar(this.parseAssignmentExpression);if(this.match(",")){var n=[];for(n.push(e);2!==this.lookahead.type&&this.match(",");)this.nextToken(),n.push(this.isolateCoverGrammar(this.parseAssignmentExpression));e=this.finalize(this.startNode(t),new s.SequenceExpression(n))}return e},t.prototype.parseStatementListItem=function(){var t;if(this.context.isAssignmentTarget=!0,this.context.isBindingElement=!0,4===this.lookahead.type)switch(this.lookahead.value){case"export":this.context.isModule||this.tolerateUnexpectedToken(this.lookahead,o.Messages.IllegalExportDeclaration),t=this.parseExportDeclaration();break;case"import":this.context.isModule||this.tolerateUnexpectedToken(this.lookahead,o.Messages.IllegalImportDeclaration),t=this.parseImportDeclaration();break;case"const":t=this.parseLexicalDeclaration({inFor:!1});break;case"function":t=this.parseFunctionDeclaration();break;case"class":t=this.parseClassDeclaration();break;case"let":t=this.isLexicalDeclaration()?this.parseLexicalDeclaration({inFor:!1}):this.parseStatement();break;default:t=this.parseStatement()}else t=this.parseStatement();return t},t.prototype.parseBlock=function(){var t=this.createNode();this.expect("{");for(var e=[];;){if(this.match("}"))break;e.push(this.parseStatementListItem())}return this.expect("}"),this.finalize(t,new s.BlockStatement(e))},t.prototype.parseLexicalBinding=function(t,e){var n=this.createNode(),r=[],i=this.parsePattern(r,t);this.context.strict&&i.type===u.Syntax.Identifier&&this.scanner.isRestrictedWord(i.name)&&this.tolerateError(o.Messages.StrictVarName);var a=null;return"const"===t?this.matchKeyword("in")||this.matchContextualKeyword("of")||(this.match("=")?(this.nextToken(),a=this.isolateCoverGrammar(this.parseAssignmentExpression)):this.throwError(o.Messages.DeclarationMissingInitializer,"const")):(!e.inFor&&i.type!==u.Syntax.Identifier||this.match("="))&&(this.expect("="),a=this.isolateCoverGrammar(this.parseAssignmentExpression)),this.finalize(n,new s.VariableDeclarator(i,a))},t.prototype.parseBindingList=function(t,e){for(var n=[this.parseLexicalBinding(t,e)];this.match(",");)this.nextToken(),n.push(this.parseLexicalBinding(t,e));return n},t.prototype.isLexicalDeclaration=function(){var t=this.scanner.saveState();this.scanner.scanComments();var e=this.scanner.lex();return this.scanner.restoreState(t),3===e.type||7===e.type&&"["===e.value||7===e.type&&"{"===e.value||4===e.type&&"let"===e.value||4===e.type&&"yield"===e.value},t.prototype.parseLexicalDeclaration=function(t){var e=this.createNode(),n=this.nextToken().value;r.assert("let"===n||"const"===n,"Lexical declaration must be either let or const");var i=this.parseBindingList(n,t);return this.consumeSemicolon(),this.finalize(e,new s.VariableDeclaration(i,n))},t.prototype.parseBindingRestElement=function(t,e){var n=this.createNode();this.expect("...");var r=this.parsePattern(t,e);return this.finalize(n,new s.RestElement(r))},t.prototype.parseArrayPattern=function(t,e){var n=this.createNode();this.expect("[");for(var r=[];!this.match("]");)if(this.match(","))this.nextToken(),r.push(null);else{if(this.match("...")){r.push(this.parseBindingRestElement(t,e));break}r.push(this.parsePatternWithDefault(t,e)),this.match("]")||this.expect(",")}return this.expect("]"),this.finalize(n,new s.ArrayPattern(r))},t.prototype.parsePropertyPattern=function(t,e){var n,r,i=this.createNode(),o=!1,a=!1;if(3===this.lookahead.type){var u=this.lookahead;n=this.parseVariableIdentifier();var c=this.finalize(i,new s.Identifier(u.value));if(this.match("=")){t.push(u),a=!0,this.nextToken();var h=this.parseAssignmentExpression();r=this.finalize(this.startNode(u),new s.AssignmentPattern(c,h))}else this.match(":")?(this.expect(":"),r=this.parsePatternWithDefault(t,e)):(t.push(u),a=!0,r=c)}else o=this.match("["),n=this.parseObjectPropertyKey(),this.expect(":"),r=this.parsePatternWithDefault(t,e);return this.finalize(i,new s.Property("init",n,o,r,!1,a))},t.prototype.parseObjectPattern=function(t,e){var n=this.createNode(),r=[];for(this.expect("{");!this.match("}");)r.push(this.parsePropertyPattern(t,e)),this.match("}")||this.expect(",");return this.expect("}"),this.finalize(n,new s.ObjectPattern(r))},t.prototype.parsePattern=function(t,e){var n;return this.match("[")?n=this.parseArrayPattern(t,e):this.match("{")?n=this.parseObjectPattern(t,e):(!this.matchKeyword("let")||"const"!==e&&"let"!==e||this.tolerateUnexpectedToken(this.lookahead,o.Messages.LetInLexicalBinding),t.push(this.lookahead),n=this.parseVariableIdentifier(e)),n},t.prototype.parsePatternWithDefault=function(t,e){var n=this.lookahead,r=this.parsePattern(t,e);if(this.match("=")){this.nextToken();var i=this.context.allowYield;this.context.allowYield=!0;var o=this.isolateCoverGrammar(this.parseAssignmentExpression);this.context.allowYield=i,r=this.finalize(this.startNode(n),new s.AssignmentPattern(r,o))}return r},t.prototype.parseVariableIdentifier=function(t){var e=this.createNode(),n=this.nextToken();return 4===n.type&&"yield"===n.value?this.context.strict?this.tolerateUnexpectedToken(n,o.Messages.StrictReservedWord):this.context.allowYield||this.throwUnexpectedToken(n):3!==n.type?this.context.strict&&4===n.type&&this.scanner.isStrictModeReservedWord(n.value)?this.tolerateUnexpectedToken(n,o.Messages.StrictReservedWord):(this.context.strict||"let"!==n.value||"var"!==t)&&this.throwUnexpectedToken(n):(this.context.isModule||this.context.await)&&3===n.type&&"await"===n.value&&this.tolerateUnexpectedToken(n),this.finalize(e,new s.Identifier(n.value))},t.prototype.parseVariableDeclaration=function(t){var e=this.createNode(),n=[],r=this.parsePattern(n,"var");this.context.strict&&r.type===u.Syntax.Identifier&&this.scanner.isRestrictedWord(r.name)&&this.tolerateError(o.Messages.StrictVarName);var i=null;return this.match("=")?(this.nextToken(),i=this.isolateCoverGrammar(this.parseAssignmentExpression)):r.type===u.Syntax.Identifier||t.inFor||this.expect("="),this.finalize(e,new s.VariableDeclarator(r,i))},t.prototype.parseVariableDeclarationList=function(t){var e={inFor:t.inFor},n=[];for(n.push(this.parseVariableDeclaration(e));this.match(",");)this.nextToken(),n.push(this.parseVariableDeclaration(e));return n},t.prototype.parseVariableStatement=function(){var t=this.createNode();this.expectKeyword("var");var e=this.parseVariableDeclarationList({inFor:!1});return this.consumeSemicolon(),this.finalize(t,new s.VariableDeclaration(e,"var"))},t.prototype.parseEmptyStatement=function(){var t=this.createNode();return this.expect(";"),this.finalize(t,new s.EmptyStatement)},t.prototype.parseExpressionStatement=function(){var t=this.createNode(),e=this.parseExpression();return this.consumeSemicolon(),this.finalize(t,new s.ExpressionStatement(e))},t.prototype.parseIfClause=function(){return this.context.strict&&this.matchKeyword("function")&&this.tolerateError(o.Messages.StrictFunction),this.parseStatement()},t.prototype.parseIfStatement=function(){var t,e=this.createNode(),n=null;this.expectKeyword("if"),this.expect("(");var r=this.parseExpression();return!this.match(")")&&this.config.tolerant?(this.tolerateUnexpectedToken(this.nextToken()),t=this.finalize(this.createNode(),new s.EmptyStatement)):(this.expect(")"),t=this.parseIfClause(),this.matchKeyword("else")&&(this.nextToken(),n=this.parseIfClause())),this.finalize(e,new s.IfStatement(r,t,n))},t.prototype.parseDoWhileStatement=function(){var t=this.createNode();this.expectKeyword("do");var e=this.context.inIteration;this.context.inIteration=!0;var n=this.parseStatement();this.context.inIteration=e,this.expectKeyword("while"),this.expect("(");var r=this.parseExpression();return!this.match(")")&&this.config.tolerant?this.tolerateUnexpectedToken(this.nextToken()):(this.expect(")"),this.match(";")&&this.nextToken()),this.finalize(t,new s.DoWhileStatement(n,r))},t.prototype.parseWhileStatement=function(){var t,e=this.createNode();this.expectKeyword("while"),this.expect("(");var n=this.parseExpression();if(!this.match(")")&&this.config.tolerant)this.tolerateUnexpectedToken(this.nextToken()),t=this.finalize(this.createNode(),new s.EmptyStatement);else{this.expect(")");var r=this.context.inIteration;this.context.inIteration=!0,t=this.parseStatement(),this.context.inIteration=r}return this.finalize(e,new s.WhileStatement(n,t))},t.prototype.parseForStatement=function(){var t,e,n=null,r=null,i=null,a=!0,c=this.createNode();if(this.expectKeyword("for"),this.expect("("),this.match(";"))this.nextToken();else if(this.matchKeyword("var")){n=this.createNode(),this.nextToken();var h=this.context.allowIn;this.context.allowIn=!1;var l=this.parseVariableDeclarationList({inFor:!0});if(this.context.allowIn=h,1===l.length&&this.matchKeyword("in")){var p=l[0];p.init&&(p.id.type===u.Syntax.ArrayPattern||p.id.type===u.Syntax.ObjectPattern||this.context.strict)&&this.tolerateError(o.Messages.ForInOfLoopInitializer,"for-in"),n=this.finalize(n,new s.VariableDeclaration(l,"var")),this.nextToken(),t=n,e=this.parseExpression(),n=null}else 1===l.length&&null===l[0].init&&this.matchContextualKeyword("of")?(n=this.finalize(n,new s.VariableDeclaration(l,"var")),this.nextToken(),t=n,e=this.parseAssignmentExpression(),n=null,a=!1):(n=this.finalize(n,new s.VariableDeclaration(l,"var")),this.expect(";"))}else if(this.matchKeyword("const")||this.matchKeyword("let")){n=this.createNode();var f=this.nextToken().value;if(this.context.strict||"in"!==this.lookahead.value){var h=this.context.allowIn;this.context.allowIn=!1;var l=this.parseBindingList(f,{inFor:!0});this.context.allowIn=h,1===l.length&&null===l[0].init&&this.matchKeyword("in")?(n=this.finalize(n,new s.VariableDeclaration(l,f)),this.nextToken(),t=n,e=this.parseExpression(),n=null):1===l.length&&null===l[0].init&&this.matchContextualKeyword("of")?(n=this.finalize(n,new s.VariableDeclaration(l,f)),this.nextToken(),t=n,e=this.parseAssignmentExpression(),n=null,a=!1):(this.consumeSemicolon(),n=this.finalize(n,new s.VariableDeclaration(l,f)))}else n=this.finalize(n,new s.Identifier(f)),this.nextToken(),t=n,e=this.parseExpression(),n=null}else{var d=this.lookahead,h=this.context.allowIn;if(this.context.allowIn=!1,n=this.inheritCoverGrammar(this.parseAssignmentExpression),this.context.allowIn=h,this.matchKeyword("in"))this.context.isAssignmentTarget&&n.type!==u.Syntax.AssignmentExpression||this.tolerateError(o.Messages.InvalidLHSInForIn),this.nextToken(),this.reinterpretExpressionAsPattern(n),t=n,e=this.parseExpression(),n=null;else if(this.matchContextualKeyword("of"))this.context.isAssignmentTarget&&n.type!==u.Syntax.AssignmentExpression||this.tolerateError(o.Messages.InvalidLHSInForLoop),this.nextToken(),this.reinterpretExpressionAsPattern(n),t=n,e=this.parseAssignmentExpression(),n=null,a=!1;else{if(this.match(",")){for(var m=[n];this.match(",");)this.nextToken(),m.push(this.isolateCoverGrammar(this.parseAssignmentExpression));n=this.finalize(this.startNode(d),new s.SequenceExpression(m))}this.expect(";")}}void 0===t&&(this.match(";")||(r=this.parseExpression()),this.expect(";"),this.match(")")||(i=this.parseExpression()));var y;if(!this.match(")")&&this.config.tolerant)this.tolerateUnexpectedToken(this.nextToken()),y=this.finalize(this.createNode(),new s.EmptyStatement);else{this.expect(")");var v=this.context.inIteration;this.context.inIteration=!0,y=this.isolateCoverGrammar(this.parseStatement),this.context.inIteration=v}return void 0===t?this.finalize(c,new s.ForStatement(n,r,i,y)):a?this.finalize(c,new s.ForInStatement(t,e,y)):this.finalize(c,new s.ForOfStatement(t,e,y))},t.prototype.parseContinueStatement=function(){var t=this.createNode();this.expectKeyword("continue");var e=null;if(3===this.lookahead.type&&!this.hasLineTerminator){var n=this.parseVariableIdentifier();e=n;var r="$"+n.name;Object.prototype.hasOwnProperty.call(this.context.labelSet,r)||this.throwError(o.Messages.UnknownLabel,n.name)}return this.consumeSemicolon(),null!==e||this.context.inIteration||this.throwError(o.Messages.IllegalContinue),this.finalize(t,new s.ContinueStatement(e))},t.prototype.parseBreakStatement=function(){var t=this.createNode();this.expectKeyword("break");var e=null;if(3===this.lookahead.type&&!this.hasLineTerminator){var n=this.parseVariableIdentifier(),r="$"+n.name;Object.prototype.hasOwnProperty.call(this.context.labelSet,r)||this.throwError(o.Messages.UnknownLabel,n.name),e=n}return this.consumeSemicolon(),null!==e||this.context.inIteration||this.context.inSwitch||this.throwError(o.Messages.IllegalBreak),this.finalize(t,new s.BreakStatement(e))},t.prototype.parseReturnStatement=function(){this.context.inFunctionBody||this.tolerateError(o.Messages.IllegalReturn);var t=this.createNode();this.expectKeyword("return");var e=!this.match(";")&&!this.match("}")&&!this.hasLineTerminator&&2!==this.lookahead.type,n=e?this.parseExpression():null;return this.consumeSemicolon(),this.finalize(t,new s.ReturnStatement(n))},t.prototype.parseWithStatement=function(){this.context.strict&&this.tolerateError(o.Messages.StrictModeWith);var t,e=this.createNode();this.expectKeyword("with"),this.expect("(");var n=this.parseExpression();return!this.match(")")&&this.config.tolerant?(this.tolerateUnexpectedToken(this.nextToken()),t=this.finalize(this.createNode(),new s.EmptyStatement)):(this.expect(")"),t=this.parseStatement()),this.finalize(e,new s.WithStatement(n,t))},t.prototype.parseSwitchCase=function(){var t,e=this.createNode();this.matchKeyword("default")?(this.nextToken(),t=null):(this.expectKeyword("case"),t=this.parseExpression()),this.expect(":");for(var n=[];;){if(this.match("}")||this.matchKeyword("default")||this.matchKeyword("case"))break;n.push(this.parseStatementListItem())}return this.finalize(e,new s.SwitchCase(t,n))},t.prototype.parseSwitchStatement=function(){var t=this.createNode();this.expectKeyword("switch"),this.expect("(");var e=this.parseExpression();this.expect(")");var n=this.context.inSwitch;this.context.inSwitch=!0;var r=[],i=!1;for(this.expect("{");;){if(this.match("}"))break;var a=this.parseSwitchCase();null===a.test&&(i&&this.throwError(o.Messages.MultipleDefaultsInSwitch),i=!0),r.push(a)}return this.expect("}"),this.context.inSwitch=n,this.finalize(t,new s.SwitchStatement(e,r))},t.prototype.parseLabelledStatement=function(){var t,e=this.createNode(),n=this.parseExpression();if(n.type===u.Syntax.Identifier&&this.match(":")){this.nextToken();var r=n,i="$"+r.name;Object.prototype.hasOwnProperty.call(this.context.labelSet,i)&&this.throwError(o.Messages.Redeclaration,"Label",r.name),this.context.labelSet[i]=!0;var a=void 0;if(this.matchKeyword("class"))this.tolerateUnexpectedToken(this.lookahead),a=this.parseClassDeclaration();else if(this.matchKeyword("function")){var c=this.lookahead,h=this.parseFunctionDeclaration();this.context.strict?this.tolerateUnexpectedToken(c,o.Messages.StrictFunction):h.generator&&this.tolerateUnexpectedToken(c,o.Messages.GeneratorInLegacyContext),a=h}else a=this.parseStatement();delete this.context.labelSet[i],t=new s.LabeledStatement(r,a)}else this.consumeSemicolon(),t=new s.ExpressionStatement(n);return this.finalize(e,t)},t.prototype.parseThrowStatement=function(){var t=this.createNode();this.expectKeyword("throw"),this.hasLineTerminator&&this.throwError(o.Messages.NewlineAfterThrow);var e=this.parseExpression();return this.consumeSemicolon(),this.finalize(t,new s.ThrowStatement(e))},t.prototype.parseCatchClause=function(){var t=this.createNode();this.expectKeyword("catch"),this.expect("("),this.match(")")&&this.throwUnexpectedToken(this.lookahead);for(var e=[],n=this.parsePattern(e),r={},i=0;i<e.length;i++){var a="$"+e[i].value;Object.prototype.hasOwnProperty.call(r,a)&&this.tolerateError(o.Messages.DuplicateBinding,e[i].value),r[a]=!0}this.context.strict&&n.type===u.Syntax.Identifier&&this.scanner.isRestrictedWord(n.name)&&this.tolerateError(o.Messages.StrictCatchVariable),this.expect(")");var c=this.parseBlock();return this.finalize(t,new s.CatchClause(n,c))},t.prototype.parseFinallyClause=function(){return this.expectKeyword("finally"),this.parseBlock()},t.prototype.parseTryStatement=function(){var t=this.createNode();this.expectKeyword("try");var e=this.parseBlock(),n=this.matchKeyword("catch")?this.parseCatchClause():null,r=this.matchKeyword("finally")?this.parseFinallyClause():null;return n||r||this.throwError(o.Messages.NoCatchOrFinally),this.finalize(t,new s.TryStatement(e,n,r))},t.prototype.parseDebuggerStatement=function(){var t=this.createNode();return this.expectKeyword("debugger"),this.consumeSemicolon(),this.finalize(t,new s.DebuggerStatement)},t.prototype.parseStatement=function(){var t;switch(this.lookahead.type){case 1:case 5:case 6:case 8:case 10:case 9:t=this.parseExpressionStatement();break;case 7:var e=this.lookahead.value;t="{"===e?this.parseBlock():"("===e?this.parseExpressionStatement():";"===e?this.parseEmptyStatement():this.parseExpressionStatement();break;case 3:t=this.matchAsyncFunction()?this.parseFunctionDeclaration():this.parseLabelledStatement();break;case 4:switch(this.lookahead.value){case"break":t=this.parseBreakStatement();break;case"continue":t=this.parseContinueStatement();break;case"debugger":t=this.parseDebuggerStatement();break;case"do":t=this.parseDoWhileStatement();break;case"for":t=this.parseForStatement();break;case"function":t=this.parseFunctionDeclaration();break;case"if":t=this.parseIfStatement();break;case"return":t=this.parseReturnStatement();break;case"switch":t=this.parseSwitchStatement();break;case"throw":t=this.parseThrowStatement();break;case"try":t=this.parseTryStatement();break;case"var":t=this.parseVariableStatement();break;case"while":t=this.parseWhileStatement();break;case"with":t=this.parseWithStatement();break;default:t=this.parseExpressionStatement()}break;default:t=this.throwUnexpectedToken(this.lookahead)}return t},t.prototype.parseFunctionSourceElements=function(){var t=this.createNode();this.expect("{");var e=this.parseDirectivePrologues(),n=this.context.labelSet,r=this.context.inIteration,i=this.context.inSwitch,o=this.context.inFunctionBody;for(this.context.labelSet={},this.context.inIteration=!1,this.context.inSwitch=!1,this.context.inFunctionBody=!0;2!==this.lookahead.type&&!this.match("}");)e.push(this.parseStatementListItem());return this.expect("}"),this.context.labelSet=n,this.context.inIteration=r,this.context.inSwitch=i,this.context.inFunctionBody=o,this.finalize(t,new s.BlockStatement(e))},t.prototype.validateParam=function(t,e,n){var r="$"+n;this.context.strict?(this.scanner.isRestrictedWord(n)&&(t.stricted=e,t.message=o.Messages.StrictParamName),Object.prototype.hasOwnProperty.call(t.paramSet,r)&&(t.stricted=e,t.message=o.Messages.StrictParamDupe)):t.firstRestricted||(this.scanner.isRestrictedWord(n)?(t.firstRestricted=e,t.message=o.Messages.StrictParamName):this.scanner.isStrictModeReservedWord(n)?(t.firstRestricted=e,t.message=o.Messages.StrictReservedWord):Object.prototype.hasOwnProperty.call(t.paramSet,r)&&(t.stricted=e,t.message=o.Messages.StrictParamDupe)),"function"==typeof Object.defineProperty?Object.defineProperty(t.paramSet,r,{value:!0,enumerable:!0,writable:!0,configurable:!0}):t.paramSet[r]=!0},t.prototype.parseRestElement=function(t){var e=this.createNode();this.expect("...");var n=this.parsePattern(t);return this.match("=")&&this.throwError(o.Messages.DefaultRestParameter),this.match(")")||this.throwError(o.Messages.ParameterAfterRestParameter),this.finalize(e,new s.RestElement(n))},t.prototype.parseFormalParameter=function(t){for(var e=[],n=this.match("...")?this.parseRestElement(e):this.parsePatternWithDefault(e),r=0;r<e.length;r++)this.validateParam(t,e[r],e[r].value);t.simple=t.simple&&n instanceof s.Identifier,t.params.push(n)},t.prototype.parseFormalParameters=function(t){var e;if(e={simple:!0,params:[],firstRestricted:t},this.expect("("),!this.match(")"))for(e.paramSet={};2!==this.lookahead.type&&(this.parseFormalParameter(e),!this.match(")"))&&(this.expect(","),!this.match(")")););return this.expect(")"),{simple:e.simple,params:e.params,stricted:e.stricted,firstRestricted:e.firstRestricted,message:e.message}},t.prototype.matchAsyncFunction=function(){var t=this.matchContextualKeyword("async");if(t){var e=this.scanner.saveState();this.scanner.scanComments();var n=this.scanner.lex();this.scanner.restoreState(e),t=e.lineNumber===n.lineNumber&&4===n.type&&"function"===n.value}return t},t.prototype.parseFunctionDeclaration=function(t){var e=this.createNode(),n=this.matchContextualKeyword("async");n&&this.nextToken(),this.expectKeyword("function");var r=!n&&this.match("*");r&&this.nextToken();var i,a=null,u=null;if(!t||!this.match("(")){var c=this.lookahead;a=this.parseVariableIdentifier(),this.context.strict?this.scanner.isRestrictedWord(c.value)&&this.tolerateUnexpectedToken(c,o.Messages.StrictFunctionName):this.scanner.isRestrictedWord(c.value)?(u=c,i=o.Messages.StrictFunctionName):this.scanner.isStrictModeReservedWord(c.value)&&(u=c,i=o.Messages.StrictReservedWord)}var h=this.context.await,l=this.context.allowYield;this.context.await=n,this.context.allowYield=!r;var p=this.parseFormalParameters(u),f=p.params,d=p.stricted;u=p.firstRestricted,p.message&&(i=p.message);var m=this.context.strict,y=this.context.allowStrictDirective;this.context.allowStrictDirective=p.simple;var v=this.parseFunctionSourceElements();return this.context.strict&&u&&this.throwUnexpectedToken(u,i),this.context.strict&&d&&this.tolerateUnexpectedToken(d,i),this.context.strict=m,this.context.allowStrictDirective=y,this.context.await=h,this.context.allowYield=l,n?this.finalize(e,new s.AsyncFunctionDeclaration(a,f,v)):this.finalize(e,new s.FunctionDeclaration(a,f,v,r))},t.prototype.parseFunctionExpression=function(){var t=this.createNode(),e=this.matchContextualKeyword("async");e&&this.nextToken(),this.expectKeyword("function");var n=!e&&this.match("*");n&&this.nextToken();var r,i,a=null,u=this.context.await,c=this.context.allowYield;if(this.context.await=e,this.context.allowYield=!n,!this.match("(")){var h=this.lookahead;a=this.context.strict||n||!this.matchKeyword("yield")?this.parseVariableIdentifier():this.parseIdentifierName(),this.context.strict?this.scanner.isRestrictedWord(h.value)&&this.tolerateUnexpectedToken(h,o.Messages.StrictFunctionName):this.scanner.isRestrictedWord(h.value)?(i=h,r=o.Messages.StrictFunctionName):this.scanner.isStrictModeReservedWord(h.value)&&(i=h,r=o.Messages.StrictReservedWord)}var l=this.parseFormalParameters(i),p=l.params,f=l.stricted;i=l.firstRestricted,l.message&&(r=l.message);var d=this.context.strict,m=this.context.allowStrictDirective;this.context.allowStrictDirective=l.simple;var y=this.parseFunctionSourceElements();return this.context.strict&&i&&this.throwUnexpectedToken(i,r),this.context.strict&&f&&this.tolerateUnexpectedToken(f,r),this.context.strict=d,this.context.allowStrictDirective=m,this.context.await=u,this.context.allowYield=c,e?this.finalize(t,new s.AsyncFunctionExpression(a,p,y)):this.finalize(t,new s.FunctionExpression(a,p,y,n))},t.prototype.parseDirective=function(){var t=this.lookahead,e=this.createNode(),n=this.parseExpression(),r=n.type===u.Syntax.Literal?this.getTokenRaw(t).slice(1,-1):null;return this.consumeSemicolon(),this.finalize(e,r?new s.Directive(n,r):new s.ExpressionStatement(n))},t.prototype.parseDirectivePrologues=function(){for(var t=null,e=[];;){var n=this.lookahead;if(8!==n.type)break;var r=this.parseDirective();e.push(r);var i=r.directive;if("string"!=typeof i)break;"use strict"===i?(this.context.strict=!0,t&&this.tolerateUnexpectedToken(t,o.Messages.StrictOctalLiteral),this.context.allowStrictDirective||this.tolerateUnexpectedToken(n,o.Messages.IllegalLanguageModeDirective)):!t&&n.octal&&(t=n)}return e},t.prototype.qualifiedPropertyName=function(t){switch(t.type){case 3:case 8:case 1:case 5:case 6:case 4:return!0;case 7:return"["===t.value}return!1},t.prototype.parseGetterMethod=function(){var t=this.createNode(),e=this.context.allowYield;this.context.allowYield=!1;var n=this.parseFormalParameters();n.params.length>0&&this.tolerateError(o.Messages.BadGetterArity);var r=this.parsePropertyMethod(n);return this.context.allowYield=e,this.finalize(t,new s.FunctionExpression(null,n.params,r,!1))},t.prototype.parseSetterMethod=function(){var t=this.createNode(),e=this.context.allowYield;this.context.allowYield=!1;var n=this.parseFormalParameters();1!==n.params.length?this.tolerateError(o.Messages.BadSetterArity):n.params[0]instanceof s.RestElement&&this.tolerateError(o.Messages.BadSetterRestParameter);var r=this.parsePropertyMethod(n);return this.context.allowYield=e,this.finalize(t,new s.FunctionExpression(null,n.params,r,!1))},t.prototype.parseGeneratorMethod=function(){var t=this.createNode(),e=this.context.allowYield;this.context.allowYield=!0;var n=this.parseFormalParameters();this.context.allowYield=!1;var r=this.parsePropertyMethod(n);return this.context.allowYield=e,this.finalize(t,new s.FunctionExpression(null,n.params,r,!0))},t.prototype.isStartOfExpression=function(){var t=!0,e=this.lookahead.value;switch(this.lookahead.type){case 7:t="["===e||"("===e||"{"===e||"+"===e||"-"===e||"!"===e||"~"===e||"++"===e||"--"===e||"/"===e||"/="===e;break;case 4:t="class"===e||"delete"===e||"function"===e||"let"===e||"new"===e||"super"===e||"this"===e||"typeof"===e||"void"===e||"yield"===e}return t},t.prototype.parseYieldExpression=function(){var t=this.createNode();this.expectKeyword("yield");var e=null,n=!1;if(!this.hasLineTerminator){var r=this.context.allowYield;this.context.allowYield=!1,n=this.match("*"),n?(this.nextToken(),e=this.parseAssignmentExpression()):this.isStartOfExpression()&&(e=this.parseAssignmentExpression()),this.context.allowYield=r}return this.finalize(t,new s.YieldExpression(e,n))},t.prototype.parseClassElement=function(t){var e=this.lookahead,n=this.createNode(),r="",i=null,a=null,u=!1,c=!1,h=!1,l=!1;if(this.match("*"))this.nextToken();else{u=this.match("["),i=this.parseObjectPropertyKey();if("static"===i.name&&(this.qualifiedPropertyName(this.lookahead)||this.match("*"))&&(e=this.lookahead,h=!0,u=this.match("["),this.match("*")?this.nextToken():i=this.parseObjectPropertyKey()),3===e.type&&!this.hasLineTerminator&&"async"===e.value){var p=this.lookahead.value;":"!==p&&"("!==p&&"*"!==p&&(l=!0,e=this.lookahead,i=this.parseObjectPropertyKey(),3===e.type&&("get"===e.value||"set"===e.value?this.tolerateUnexpectedToken(e):"constructor"===e.value&&this.tolerateUnexpectedToken(e,o.Messages.ConstructorIsAsync)))}}var f=this.qualifiedPropertyName(this.lookahead);return 3===e.type?"get"===e.value&&f?(r="get",u=this.match("["),i=this.parseObjectPropertyKey(),this.context.allowYield=!1,a=this.parseGetterMethod()):"set"===e.value&&f&&(r="set",u=this.match("["),i=this.parseObjectPropertyKey(),a=this.parseSetterMethod()):7===e.type&&"*"===e.value&&f&&(r="init",u=this.match("["),i=this.parseObjectPropertyKey(),a=this.parseGeneratorMethod(),c=!0),!r&&i&&this.match("(")&&(r="init",a=l?this.parsePropertyMethodAsyncFunction():this.parsePropertyMethodFunction(),c=!0),r||this.throwUnexpectedToken(this.lookahead),"init"===r&&(r="method"),u||(h&&this.isPropertyKey(i,"prototype")&&this.throwUnexpectedToken(e,o.Messages.StaticPrototype),!h&&this.isPropertyKey(i,"constructor")&&(("method"!==r||!c||a&&a.generator)&&this.throwUnexpectedToken(e,o.Messages.ConstructorSpecialMethod),t.value?this.throwUnexpectedToken(e,o.Messages.DuplicateConstructor):t.value=!0,r="constructor")),this.finalize(n,new s.MethodDefinition(i,u,a,r,h))},t.prototype.parseClassElementList=function(){var t=[],e={value:!1};for(this.expect("{");!this.match("}");)this.match(";")?this.nextToken():t.push(this.parseClassElement(e));return this.expect("}"),t},t.prototype.parseClassBody=function(){var t=this.createNode(),e=this.parseClassElementList();return this.finalize(t,new s.ClassBody(e))},t.prototype.parseClassDeclaration=function(t){var e=this.createNode(),n=this.context.strict;this.context.strict=!0,this.expectKeyword("class");var r=t&&3!==this.lookahead.type?null:this.parseVariableIdentifier(),i=null;this.matchKeyword("extends")&&(this.nextToken(),i=this.isolateCoverGrammar(this.parseLeftHandSideExpressionAllowCall));var o=this.parseClassBody();return this.context.strict=n,this.finalize(e,new s.ClassDeclaration(r,i,o))},t.prototype.parseClassExpression=function(){var t=this.createNode(),e=this.context.strict;this.context.strict=!0,this.expectKeyword("class");var n=3===this.lookahead.type?this.parseVariableIdentifier():null,r=null;this.matchKeyword("extends")&&(this.nextToken(),r=this.isolateCoverGrammar(this.parseLeftHandSideExpressionAllowCall));var i=this.parseClassBody();return this.context.strict=e,this.finalize(t,new s.ClassExpression(n,r,i))},t.prototype.parseModule=function(){this.context.strict=!0,this.context.isModule=!0;for(var t=this.createNode(),e=this.parseDirectivePrologues();2!==this.lookahead.type;)e.push(this.parseStatementListItem());return this.finalize(t,new s.Module(e))},t.prototype.parseScript=function(){for(var t=this.createNode(),e=this.parseDirectivePrologues();2!==this.lookahead.type;)e.push(this.parseStatementListItem());return this.finalize(t,new s.Script(e))},t.prototype.parseModuleSpecifier=function(){var t=this.createNode();8!==this.lookahead.type&&this.throwError(o.Messages.InvalidModuleSpecifier);var e=this.nextToken(),n=this.getTokenRaw(e);return this.finalize(t,new s.Literal(e.value,n))},t.prototype.parseImportSpecifier=function(){var t,e,n=this.createNode();return 3===this.lookahead.type?(t=this.parseVariableIdentifier(),e=t,this.matchContextualKeyword("as")&&(this.nextToken(),e=this.parseVariableIdentifier())):(t=this.parseIdentifierName(),e=t,this.matchContextualKeyword("as")?(this.nextToken(),e=this.parseVariableIdentifier()):this.throwUnexpectedToken(this.nextToken())),this.finalize(n,new s.ImportSpecifier(e,t))},t.prototype.parseNamedImports=function(){this.expect("{");for(var t=[];!this.match("}");)t.push(this.parseImportSpecifier()),this.match("}")||this.expect(",");return this.expect("}"),t},t.prototype.parseImportDefaultSpecifier=function(){var t=this.createNode(),e=this.parseIdentifierName();return this.finalize(t,new s.ImportDefaultSpecifier(e))},t.prototype.parseImportNamespaceSpecifier=function(){var t=this.createNode();this.expect("*"),this.matchContextualKeyword("as")||this.throwError(o.Messages.NoAsAfterImportNamespace),this.nextToken();var e=this.parseIdentifierName();return this.finalize(t,new s.ImportNamespaceSpecifier(e))},t.prototype.parseImportDeclaration=function(){this.context.inFunctionBody&&this.throwError(o.Messages.IllegalImportDeclaration);var t=this.createNode();this.expectKeyword("import");var e,n=[];if(8===this.lookahead.type)e=this.parseModuleSpecifier();else{if(this.match("{")?n=n.concat(this.parseNamedImports()):this.match("*")?n.push(this.parseImportNamespaceSpecifier()):this.isIdentifierName(this.lookahead)&&!this.matchKeyword("default")?(n.push(this.parseImportDefaultSpecifier()),this.match(",")&&(this.nextToken(),this.match("*")?n.push(this.parseImportNamespaceSpecifier()):this.match("{")?n=n.concat(this.parseNamedImports()):this.throwUnexpectedToken(this.lookahead))):this.throwUnexpectedToken(this.nextToken()),!this.matchContextualKeyword("from")){var r=this.lookahead.value?o.Messages.UnexpectedToken:o.Messages.MissingFromClause;this.throwError(r,this.lookahead.value)}this.nextToken(),e=this.parseModuleSpecifier()}return this.consumeSemicolon(),this.finalize(t,new s.ImportDeclaration(n,e))},t.prototype.parseExportSpecifier=function(){var t=this.createNode(),e=this.parseIdentifierName(),n=e;return this.matchContextualKeyword("as")&&(this.nextToken(),n=this.parseIdentifierName()),this.finalize(t,new s.ExportSpecifier(e,n))},t.prototype.parseExportDeclaration=function(){this.context.inFunctionBody&&this.throwError(o.Messages.IllegalExportDeclaration);var t=this.createNode();this.expectKeyword("export");var e;if(this.matchKeyword("default"))if(this.nextToken(),this.matchKeyword("function")){var n=this.parseFunctionDeclaration(!0);e=this.finalize(t,new s.ExportDefaultDeclaration(n))}else if(this.matchKeyword("class")){var n=this.parseClassDeclaration(!0);e=this.finalize(t,new s.ExportDefaultDeclaration(n))}else if(this.matchContextualKeyword("async")){var n=this.matchAsyncFunction()?this.parseFunctionDeclaration(!0):this.parseAssignmentExpression();e=this.finalize(t,new s.ExportDefaultDeclaration(n))}else{this.matchContextualKeyword("from")&&this.throwError(o.Messages.UnexpectedToken,this.lookahead.value);var n=this.match("{")?this.parseObjectInitializer():this.match("[")?this.parseArrayInitializer():this.parseAssignmentExpression();this.consumeSemicolon(),e=this.finalize(t,new s.ExportDefaultDeclaration(n))}else if(this.match("*")){if(this.nextToken(),!this.matchContextualKeyword("from")){var r=this.lookahead.value?o.Messages.UnexpectedToken:o.Messages.MissingFromClause;this.throwError(r,this.lookahead.value)}this.nextToken();var i=this.parseModuleSpecifier();this.consumeSemicolon(),e=this.finalize(t,new s.ExportAllDeclaration(i))}else if(4===this.lookahead.type){var n=void 0;switch(this.lookahead.value){case"let":case"const":n=this.parseLexicalDeclaration({inFor:!1});break;case"var":case"class":case"function":n=this.parseStatementListItem();break;default:this.throwUnexpectedToken(this.lookahead)}e=this.finalize(t,new s.ExportNamedDeclaration(n,[],null))}else if(this.matchAsyncFunction()){var n=this.parseFunctionDeclaration();e=this.finalize(t,new s.ExportNamedDeclaration(n,[],null))}else{var a=[],u=null,c=!1;for(this.expect("{");!this.match("}");)c=c||this.matchKeyword("default"),a.push(this.parseExportSpecifier()),this.match("}")||this.expect(",");if(this.expect("}"),this.matchContextualKeyword("from"))this.nextToken(),u=this.parseModuleSpecifier(),this.consumeSemicolon();else if(c){var r=this.lookahead.value?o.Messages.UnexpectedToken:o.Messages.MissingFromClause;this.throwError(r,this.lookahead.value)}else this.consumeSemicolon();e=this.finalize(t,new s.ExportNamedDeclaration(null,a,u))}return e},t}();e.Parser=h},function(t,e){"use strict";function n(t,e){if(!t)throw new Error("ASSERT: "+e)}Object.defineProperty(e,"__esModule",{value:!0}),e.assert=n},function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var n=function(){function t(){this.errors=[],this.tolerant=!1}return t.prototype.recordError=function(t){this.errors.push(t)},t.prototype.tolerate=function(t){if(!this.tolerant)throw t;this.recordError(t)},t.prototype.constructError=function(t,e){var n=new Error(t);try{throw n}catch(t){Object.create&&Object.defineProperty&&(n=Object.create(t),Object.defineProperty(n,"column",{value:e}))}return n},t.prototype.createError=function(t,e,n,r){var i="Line "+e+": "+r,o=this.constructError(i,n);return o.index=t,o.lineNumber=e,o.description=r,o},t.prototype.throwError=function(t,e,n,r){throw this.createError(t,e,n,r)},t.prototype.tolerateError=function(t,e,n,r){var i=this.createError(t,e,n,r);if(!this.tolerant)throw i;this.recordError(i)},t}();e.ErrorHandler=n},function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Messages={BadGetterArity:"Getter must not have any formal parameters",BadSetterArity:"Setter must have exactly one formal parameter",BadSetterRestParameter:"Setter function argument must not be a rest parameter",ConstructorIsAsync:"Class constructor may not be an async method",ConstructorSpecialMethod:"Class constructor may not be an accessor",DeclarationMissingInitializer:"Missing initializer in %0 declaration",DefaultRestParameter:"Unexpected token =",DuplicateBinding:"Duplicate binding %0",DuplicateConstructor:"A class may only have one constructor",DuplicateProtoProperty:"Duplicate __proto__ fields are not allowed in object literals",ForInOfLoopInitializer:"%0 loop variable declaration may not have an initializer",GeneratorInLegacyContext:"Generator declarations are not allowed in legacy contexts",IllegalBreak:"Illegal break statement",IllegalContinue:"Illegal continue statement",IllegalExportDeclaration:"Unexpected token",IllegalImportDeclaration:"Unexpected token",IllegalLanguageModeDirective:"Illegal 'use strict' directive in function with non-simple parameter list",IllegalReturn:"Illegal return statement",InvalidEscapedReservedWord:"Keyword must not contain escaped characters",InvalidHexEscapeSequence:"Invalid hexadecimal escape sequence",InvalidLHSInAssignment:"Invalid left-hand side in assignment",InvalidLHSInForIn:"Invalid left-hand side in for-in",InvalidLHSInForLoop:"Invalid left-hand side in for-loop",InvalidModuleSpecifier:"Unexpected token",InvalidRegExp:"Invalid regular expression",LetInLexicalBinding:"let is disallowed as a lexically bound name",MissingFromClause:"Unexpected token",MultipleDefaultsInSwitch:"More than one default clause in switch statement",NewlineAfterThrow:"Illegal newline after throw",NoAsAfterImportNamespace:"Unexpected token",NoCatchOrFinally:"Missing catch or finally after try",ParameterAfterRestParameter:"Rest parameter must be last formal parameter",Redeclaration:"%0 '%1' has already been declared",StaticPrototype:"Classes may not have static property named prototype",StrictCatchVariable:"Catch variable may not be eval or arguments in strict mode",StrictDelete:"Delete of an unqualified identifier in strict mode.",StrictFunction:"In strict mode code, functions can only be declared at top level or inside a block",StrictFunctionName:"Function name may not be eval or arguments in strict mode",StrictLHSAssignment:"Assignment to eval or arguments is not allowed in strict mode",StrictLHSPostfix:"Postfix increment/decrement may not have eval or arguments operand in strict mode",StrictLHSPrefix:"Prefix increment/decrement may not have eval or arguments operand in strict mode",StrictModeWith:"Strict mode code may not include a with statement",StrictOctalLiteral:"Octal literals are not allowed in strict mode.",StrictParamDupe:"Strict mode function may not have duplicate parameter names",StrictParamName:"Parameter name eval or arguments is not allowed in strict mode",StrictReservedWord:"Use of future reserved word in strict mode",StrictVarName:"Variable name may not be eval or arguments in strict mode",TemplateOctalLiteral:"Octal literals are not allowed in template strings.",UnexpectedEOS:"Unexpected end of input",UnexpectedIdentifier:"Unexpected identifier",UnexpectedNumber:"Unexpected number",UnexpectedReserved:"Unexpected reserved word",UnexpectedString:"Unexpected string",UnexpectedTemplate:"Unexpected quasi %0",UnexpectedToken:"Unexpected token %0",UnexpectedTokenIllegal:"Unexpected token ILLEGAL",UnknownLabel:"Undefined label '%0'",UnterminatedRegExp:"Invalid regular expression: missing /"}},function(t,e,n){"use strict";function r(t){return"0123456789abcdef".indexOf(t.toLowerCase())}function i(t){return"01234567".indexOf(t)}Object.defineProperty(e,"__esModule",{value:!0});var o=n(9),s=n(4),a=n(11),u=function(){function t(t,e){this.source=t,this.errorHandler=e,this.trackComment=!1,this.length=t.length,this.index=0,this.lineNumber=t.length>0?1:0,this.lineStart=0,this.curlyStack=[]}return t.prototype.saveState=function(){return{index:this.index,lineNumber:this.lineNumber,lineStart:this.lineStart}},t.prototype.restoreState=function(t){this.index=t.index,this.lineNumber=t.lineNumber,this.lineStart=t.lineStart},t.prototype.eof=function(){return this.index>=this.length},t.prototype.throwUnexpectedToken=function(t){return void 0===t&&(t=a.Messages.UnexpectedTokenIllegal),this.errorHandler.throwError(this.index,this.lineNumber,this.index-this.lineStart+1,t)},t.prototype.tolerateUnexpectedToken=function(t){void 0===t&&(t=a.Messages.UnexpectedTokenIllegal),this.errorHandler.tolerateError(this.index,this.lineNumber,this.index-this.lineStart+1,t)},t.prototype.skipSingleLineComment=function(t){var e,n,r=[];for(this.trackComment&&(r=[],e=this.index-t,n={start:{line:this.lineNumber,column:this.index-this.lineStart-t},end:{}});!this.eof();){var i=this.source.charCodeAt(this.index);if(++this.index,s.Character.isLineTerminator(i)){if(this.trackComment){n.end={line:this.lineNumber,column:this.index-this.lineStart-1};var o={multiLine:!1,slice:[e+t,this.index-1],range:[e,this.index-1],loc:n};r.push(o)}return 13===i&&10===this.source.charCodeAt(this.index)&&++this.index,++this.lineNumber,this.lineStart=this.index,r}}if(this.trackComment){n.end={line:this.lineNumber,column:this.index-this.lineStart};var o={multiLine:!1,slice:[e+t,this.index],range:[e,this.index],loc:n};r.push(o)}return r},t.prototype.skipMultiLineComment=function(){var t,e,n=[];for(this.trackComment&&(n=[],t=this.index-2,e={start:{line:this.lineNumber,column:this.index-this.lineStart-2},end:{}});!this.eof();){var r=this.source.charCodeAt(this.index);if(s.Character.isLineTerminator(r))13===r&&10===this.source.charCodeAt(this.index+1)&&++this.index,++this.lineNumber,++this.index,this.lineStart=this.index;else if(42===r){if(47===this.source.charCodeAt(this.index+1)){if(this.index+=2,this.trackComment){e.end={line:this.lineNumber,column:this.index-this.lineStart};var i={multiLine:!0,slice:[t+2,this.index-2],range:[t,this.index],loc:e};n.push(i)}return n}++this.index}else++this.index}if(this.trackComment){e.end={line:this.lineNumber,column:this.index-this.lineStart};var i={multiLine:!0,slice:[t+2,this.index],range:[t,this.index],loc:e};n.push(i)}return this.tolerateUnexpectedToken(),n},t.prototype.scanComments=function(){var t;this.trackComment&&(t=[]);for(var e=0===this.index;!this.eof();){var n=this.source.charCodeAt(this.index);if(s.Character.isWhiteSpace(n))++this.index;else if(s.Character.isLineTerminator(n))++this.index,13===n&&10===this.source.charCodeAt(this.index)&&++this.index,++this.lineNumber,this.lineStart=this.index,e=!0;else if(47===n)if(47===(n=this.source.charCodeAt(this.index+1))){this.index+=2;var r=this.skipSingleLineComment(2);this.trackComment&&(t=t.concat(r)),e=!0}else{if(42!==n)break;this.index+=2;var r=this.skipMultiLineComment();this.trackComment&&(t=t.concat(r))}else if(e&&45===n){if(45!==this.source.charCodeAt(this.index+1)||62!==this.source.charCodeAt(this.index+2))break;this.index+=3;var r=this.skipSingleLineComment(3);this.trackComment&&(t=t.concat(r))}else{if(60!==n)break;if("!--"!==this.source.slice(this.index+1,this.index+4))break;this.index+=4;var r=this.skipSingleLineComment(4);this.trackComment&&(t=t.concat(r))}}return t},t.prototype.isFutureReservedWord=function(t){switch(t){case"enum":case"export":case"import":case"super":return!0;default:return!1}},t.prototype.isStrictModeReservedWord=function(t){switch(t){case"implements":case"interface":case"package":case"private":case"protected":case"public":case"static":case"yield":case"let":return!0;default:return!1}},t.prototype.isRestrictedWord=function(t){return"eval"===t||"arguments"===t},t.prototype.isKeyword=function(t){switch(t.length){case 2:return"if"===t||"in"===t||"do"===t;case 3:return"var"===t||"for"===t||"new"===t||"try"===t||"let"===t;case 4:return"this"===t||"else"===t||"case"===t||"void"===t||"with"===t||"enum"===t;case 5:return"while"===t||"break"===t||"catch"===t||"throw"===t||"const"===t||"yield"===t||"class"===t||"super"===t;case 6:return"return"===t||"typeof"===t||"delete"===t||"switch"===t||"export"===t||"import"===t;case 7:return"default"===t||"finally"===t||"extends"===t;case 8:return"function"===t||"continue"===t||"debugger"===t;case 10:return"instanceof"===t;default:return!1}},t.prototype.codePointAt=function(t){var e=this.source.charCodeAt(t);if(e>=55296&&e<=56319){var n=this.source.charCodeAt(t+1);if(n>=56320&&n<=57343){e=1024*(e-55296)+n-56320+65536}}return e},t.prototype.scanHexEscape=function(t){for(var e="u"===t?4:2,n=0,i=0;i<e;++i){if(this.eof()||!s.Character.isHexDigit(this.source.charCodeAt(this.index)))return null;n=16*n+r(this.source[this.index++])}return String.fromCharCode(n)},t.prototype.scanUnicodeCodePointEscape=function(){var t=this.source[this.index],e=0;for("}"===t&&this.throwUnexpectedToken();!this.eof()&&(t=this.source[this.index++],s.Character.isHexDigit(t.charCodeAt(0)));)e=16*e+r(t);return(e>1114111||"}"!==t)&&this.throwUnexpectedToken(),s.Character.fromCodePoint(e)},t.prototype.getIdentifier=function(){for(var t=this.index++;!this.eof();){var e=this.source.charCodeAt(this.index);if(92===e)return this.index=t,this.getComplexIdentifier();if(e>=55296&&e<57343)return this.index=t,this.getComplexIdentifier();if(!s.Character.isIdentifierPart(e))break;++this.index}return this.source.slice(t,this.index)},t.prototype.getComplexIdentifier=function(){var t=this.codePointAt(this.index),e=s.Character.fromCodePoint(t);this.index+=e.length;var n;for(92===t&&(117!==this.source.charCodeAt(this.index)&&this.throwUnexpectedToken(),++this.index,"{"===this.source[this.index]?(++this.index,n=this.scanUnicodeCodePointEscape()):null!==(n=this.scanHexEscape("u"))&&"\\"!==n&&s.Character.isIdentifierStart(n.charCodeAt(0))||this.throwUnexpectedToken(),e=n);!this.eof()&&(t=this.codePointAt(this.index),s.Character.isIdentifierPart(t));)n=s.Character.fromCodePoint(t),e+=n,this.index+=n.length,92===t&&(e=e.substr(0,e.length-1),117!==this.source.charCodeAt(this.index)&&this.throwUnexpectedToken(),++this.index,"{"===this.source[this.index]?(++this.index,n=this.scanUnicodeCodePointEscape()):null!==(n=this.scanHexEscape("u"))&&"\\"!==n&&s.Character.isIdentifierPart(n.charCodeAt(0))||this.throwUnexpectedToken(),e+=n);return e},t.prototype.octalToDecimal=function(t){var e="0"!==t,n=i(t);return!this.eof()&&s.Character.isOctalDigit(this.source.charCodeAt(this.index))&&(e=!0,n=8*n+i(this.source[this.index++]),"0123".indexOf(t)>=0&&!this.eof()&&s.Character.isOctalDigit(this.source.charCodeAt(this.index))&&(n=8*n+i(this.source[this.index++]))),{code:n,octal:e}},t.prototype.scanIdentifier=function(){var t,e=this.index,n=92===this.source.charCodeAt(e)?this.getComplexIdentifier():this.getIdentifier();if(3!==(t=1===n.length?3:this.isKeyword(n)?4:"null"===n?5:"true"===n||"false"===n?1:3)&&e+n.length!==this.index){var r=this.index;this.index=e,this.tolerateUnexpectedToken(a.Messages.InvalidEscapedReservedWord),this.index=r}return{type:t,value:n,lineNumber:this.lineNumber,lineStart:this.lineStart,start:e,end:this.index}},t.prototype.scanPunctuator=function(){var t=this.index,e=this.source[this.index];switch(e){case"(":case"{":"{"===e&&this.curlyStack.push("{"),++this.index;break;case".":++this.index,"."===this.source[this.index]&&"."===this.source[this.index+1]&&(this.index+=2,e="...");break;case"}":++this.index,this.curlyStack.pop();break;case")":case";":case",":case"[":case"]":case":":case"?":case"~":++this.index;break;default:e=this.source.substr(this.index,4),">>>="===e?this.index+=4:(e=e.substr(0,3),"==="===e||"!=="===e||">>>"===e||"<<="===e||">>="===e||"**="===e?this.index+=3:(e=e.substr(0,2),"&&"===e||"||"===e||"=="===e||"!="===e||"+="===e||"-="===e||"*="===e||"/="===e||"++"===e||"--"===e||"<<"===e||">>"===e||"&="===e||"|="===e||"^="===e||"%="===e||"<="===e||">="===e||"=>"===e||"**"===e?this.index+=2:(e=this.source[this.index],"<>=!+-*%&|^/".indexOf(e)>=0&&++this.index)))}return this.index===t&&this.throwUnexpectedToken(),{type:7,value:e,lineNumber:this.lineNumber,lineStart:this.lineStart,start:t,end:this.index}},t.prototype.scanHexLiteral=function(t){for(var e="";!this.eof()&&s.Character.isHexDigit(this.source.charCodeAt(this.index));)e+=this.source[this.index++];return 0===e.length&&this.throwUnexpectedToken(),s.Character.isIdentifierStart(this.source.charCodeAt(this.index))&&this.throwUnexpectedToken(),{type:6,value:parseInt("0x"+e,16),lineNumber:this.lineNumber,lineStart:this.lineStart,start:t,end:this.index}},t.prototype.scanBinaryLiteral=function(t){for(var e,n="";!this.eof()&&("0"===(e=this.source[this.index])||"1"===e);)n+=this.source[this.index++];return 0===n.length&&this.throwUnexpectedToken(),this.eof()||(e=this.source.charCodeAt(this.index),(s.Character.isIdentifierStart(e)||s.Character.isDecimalDigit(e))&&this.throwUnexpectedToken()),{type:6,value:parseInt(n,2),lineNumber:this.lineNumber,lineStart:this.lineStart,start:t,end:this.index}},t.prototype.scanOctalLiteral=function(t,e){var n="",r=!1;for(s.Character.isOctalDigit(t.charCodeAt(0))?(r=!0,n="0"+this.source[this.index++]):++this.index;!this.eof()&&s.Character.isOctalDigit(this.source.charCodeAt(this.index));)n+=this.source[this.index++];return r||0!==n.length||this.throwUnexpectedToken(),(s.Character.isIdentifierStart(this.source.charCodeAt(this.index))||s.Character.isDecimalDigit(this.source.charCodeAt(this.index)))&&this.throwUnexpectedToken(),{type:6,value:parseInt(n,8),octal:r,lineNumber:this.lineNumber,lineStart:this.lineStart,start:e,end:this.index}},t.prototype.isImplicitOctalLiteral=function(){for(var t=this.index+1;t<this.length;++t){var e=this.source[t];if("8"===e||"9"===e)return!1;if(!s.Character.isOctalDigit(e.charCodeAt(0)))return!0}return!0},t.prototype.scanNumericLiteral=function(){var t=this.index,e=this.source[t];o.assert(s.Character.isDecimalDigit(e.charCodeAt(0))||"."===e,"Numeric literal must start with a decimal digit or a decimal point");var n="";if("."!==e){if(n=this.source[this.index++],e=this.source[this.index],"0"===n){if("x"===e||"X"===e)return++this.index,this.scanHexLiteral(t);if("b"===e||"B"===e)return++this.index,this.scanBinaryLiteral(t);if("o"===e||"O"===e)return this.scanOctalLiteral(e,t);if(e&&s.Character.isOctalDigit(e.charCodeAt(0))&&this.isImplicitOctalLiteral())return this.scanOctalLiteral(e,t)}for(;s.Character.isDecimalDigit(this.source.charCodeAt(this.index));)n+=this.source[this.index++];e=this.source[this.index]}if("."===e){for(n+=this.source[this.index++];s.Character.isDecimalDigit(this.source.charCodeAt(this.index));)n+=this.source[this.index++];e=this.source[this.index]}if("e"===e||"E"===e)if(n+=this.source[this.index++],e=this.source[this.index],"+"!==e&&"-"!==e||(n+=this.source[this.index++]),s.Character.isDecimalDigit(this.source.charCodeAt(this.index)))for(;s.Character.isDecimalDigit(this.source.charCodeAt(this.index));)n+=this.source[this.index++];else this.throwUnexpectedToken();return s.Character.isIdentifierStart(this.source.charCodeAt(this.index))&&this.throwUnexpectedToken(),{type:6,value:parseFloat(n),lineNumber:this.lineNumber,lineStart:this.lineStart,start:t,end:this.index}},t.prototype.scanStringLiteral=function(){var t=this.index,e=this.source[t];o.assert("'"===e||'"'===e,"String literal must starts with a quote"),++this.index;for(var n=!1,r="";!this.eof();){var i=this.source[this.index++];if(i===e){e="";break}if("\\"===i)if((i=this.source[this.index++])&&s.Character.isLineTerminator(i.charCodeAt(0)))++this.lineNumber,"\r"===i&&"\n"===this.source[this.index]&&++this.index,this.lineStart=this.index;else switch(i){case"u":if("{"===this.source[this.index])++this.index,r+=this.scanUnicodeCodePointEscape();else{var u=this.scanHexEscape(i);null===u&&this.throwUnexpectedToken(),r+=u}break;case"x":var c=this.scanHexEscape(i);null===c&&this.throwUnexpectedToken(a.Messages.InvalidHexEscapeSequence),r+=c;break;case"n":r+="\n";break;case"r":r+="\r";break;case"t":r+="\t";break;case"b":r+="\b";break;case"f":r+="\f";break;case"v":r+="\v";break;case"8":case"9":r+=i,this.tolerateUnexpectedToken();break;default:if(i&&s.Character.isOctalDigit(i.charCodeAt(0))){var h=this.octalToDecimal(i);n=h.octal||n,r+=String.fromCharCode(h.code)}else r+=i}else{if(s.Character.isLineTerminator(i.charCodeAt(0)))break;r+=i}}return""!==e&&(this.index=t,this.throwUnexpectedToken()),{type:8,value:r,octal:n,lineNumber:this.lineNumber,lineStart:this.lineStart,start:t,end:this.index}},t.prototype.scanTemplate=function(){var t="",e=!1,n=this.index,r="`"===this.source[n],i=!1,o=2;for(++this.index;!this.eof();){var u=this.source[this.index++];if("`"===u){o=1,i=!0,e=!0;break}if("$"===u){if("{"===this.source[this.index]){this.curlyStack.push("${"),++this.index,e=!0;break}t+=u}else if("\\"===u)if(u=this.source[this.index++],s.Character.isLineTerminator(u.charCodeAt(0)))++this.lineNumber,"\r"===u&&"\n"===this.source[this.index]&&++this.index,this.lineStart=this.index;else switch(u){case"n":t+="\n";break;case"r":t+="\r";break;case"t":t+="\t";break;case"u":if("{"===this.source[this.index])++this.index,t+=this.scanUnicodeCodePointEscape();else{var c=this.index,h=this.scanHexEscape(u);null!==h?t+=h:(this.index=c,t+=u)}break;case"x":var l=this.scanHexEscape(u);null===l&&this.throwUnexpectedToken(a.Messages.InvalidHexEscapeSequence),t+=l;break;case"b":t+="\b";break;case"f":t+="\f";break;case"v":t+="\v";break;default:"0"===u?(s.Character.isDecimalDigit(this.source.charCodeAt(this.index))&&this.throwUnexpectedToken(a.Messages.TemplateOctalLiteral),t+="\0"):s.Character.isOctalDigit(u.charCodeAt(0))?this.throwUnexpectedToken(a.Messages.TemplateOctalLiteral):t+=u}else s.Character.isLineTerminator(u.charCodeAt(0))?(++this.lineNumber,"\r"===u&&"\n"===this.source[this.index]&&++this.index,this.lineStart=this.index,t+="\n"):t+=u}return e||this.throwUnexpectedToken(),r||this.curlyStack.pop(),{type:10,value:this.source.slice(n+1,this.index-o),cooked:t,head:r,tail:i,lineNumber:this.lineNumber,lineStart:this.lineStart,start:n,end:this.index}},t.prototype.testRegExp=function(t,e){var n=t,r=this;e.indexOf("u")>=0&&(n=n.replace(/\\u\{([0-9a-fA-F]+)\}|\\u([a-fA-F0-9]{4})/g,function(t,e,n){var i=parseInt(e||n,16);return i>1114111&&r.throwUnexpectedToken(a.Messages.InvalidRegExp),i<=65535?String.fromCharCode(i):"￿"}).replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g,"￿"));try{RegExp(n)}catch(t){this.throwUnexpectedToken(a.Messages.InvalidRegExp)}try{return new RegExp(t,e)}catch(t){return null}},t.prototype.scanRegExpBody=function(){var t=this.source[this.index];o.assert("/"===t,"Regular expression literal must start with a slash");for(var e=this.source[this.index++],n=!1,r=!1;!this.eof();)if(t=this.source[this.index++],e+=t,"\\"===t)t=this.source[this.index++],s.Character.isLineTerminator(t.charCodeAt(0))&&this.throwUnexpectedToken(a.Messages.UnterminatedRegExp),e+=t;else if(s.Character.isLineTerminator(t.charCodeAt(0)))this.throwUnexpectedToken(a.Messages.UnterminatedRegExp);else if(n)"]"===t&&(n=!1);else{if("/"===t){r=!0;break}"["===t&&(n=!0)}return r||this.throwUnexpectedToken(a.Messages.UnterminatedRegExp),e.substr(1,e.length-2)},t.prototype.scanRegExpFlags=function(){for(var t="",e="";!this.eof();){var n=this.source[this.index];if(!s.Character.isIdentifierPart(n.charCodeAt(0)))break;if(++this.index,"\\"!==n||this.eof())e+=n,t+=n;else if("u"===(n=this.source[this.index])){++this.index;var r=this.index,i=this.scanHexEscape("u");if(null!==i)for(e+=i,t+="\\u";r<this.index;++r)t+=this.source[r];else this.index=r,e+="u",t+="\\u";this.tolerateUnexpectedToken()}else t+="\\",this.tolerateUnexpectedToken()}return e},t.prototype.scanRegExp=function(){var t=this.index,e=this.scanRegExpBody(),n=this.scanRegExpFlags();return{type:9,value:"",pattern:e,flags:n,regex:this.testRegExp(e,n),lineNumber:this.lineNumber,lineStart:this.lineStart,start:t,end:this.index}},t.prototype.lex=function(){if(this.eof())return{type:2,value:"",lineNumber:this.lineNumber,lineStart:this.lineStart,start:this.index,end:this.index};var t=this.source.charCodeAt(this.index);return s.Character.isIdentifierStart(t)?this.scanIdentifier():40===t||41===t||59===t?this.scanPunctuator():39===t||34===t?this.scanStringLiteral():46===t?s.Character.isDecimalDigit(this.source.charCodeAt(this.index+1))?this.scanNumericLiteral():this.scanPunctuator():s.Character.isDecimalDigit(t)?this.scanNumericLiteral():96===t||125===t&&"${"===this.curlyStack[this.curlyStack.length-1]?this.scanTemplate():t>=55296&&t<57343&&s.Character.isIdentifierStart(this.codePointAt(this.index))?this.scanIdentifier():this.scanPunctuator()},t}();e.Scanner=u},function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.TokenName={},e.TokenName[1]="Boolean",e.TokenName[2]="<end>",e.TokenName[3]="Identifier",e.TokenName[4]="Keyword",e.TokenName[5]="Null",e.TokenName[6]="Numeric",e.TokenName[7]="Punctuator",e.TokenName[8]="String",e.TokenName[9]="RegularExpression",e.TokenName[10]="Template"},function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.XHTMLEntities={quot:'"',amp:"&",apos:"'",gt:">",nbsp:" ",iexcl:"¡",cent:"¢",pound:"£",curren:"¤",yen:"¥",brvbar:"¦",sect:"§",uml:"¨",copy:"©",ordf:"ª",laquo:"«",not:"¬",shy:"­",reg:"®",macr:"¯",deg:"°",plusmn:"±",sup2:"²",sup3:"³",acute:"´",micro:"µ",para:"¶",middot:"·",cedil:"¸",sup1:"¹",ordm:"º",raquo:"»",frac14:"¼",frac12:"½",frac34:"¾",iquest:"¿",Agrave:"À",Aacute:"Á",Acirc:"Â",Atilde:"Ã",Auml:"Ä",Aring:"Å",AElig:"Æ",Ccedil:"Ç",Egrave:"È",Eacute:"É",Ecirc:"Ê",Euml:"Ë",Igrave:"Ì",Iacute:"Í",Icirc:"Î",Iuml:"Ï",ETH:"Ð",Ntilde:"Ñ",Ograve:"Ò",Oacute:"Ó",Ocirc:"Ô",Otilde:"Õ",Ouml:"Ö",times:"×",Oslash:"Ø",Ugrave:"Ù",Uacute:"Ú",Ucirc:"Û",Uuml:"Ü",Yacute:"Ý",THORN:"Þ",szlig:"ß",agrave:"à",aacute:"á",acirc:"â",atilde:"ã",auml:"ä",aring:"å",aelig:"æ",ccedil:"ç",egrave:"è",eacute:"é",ecirc:"ê",euml:"ë",igrave:"ì",iacute:"í",icirc:"î",iuml:"ï",eth:"ð",ntilde:"ñ",ograve:"ò",oacute:"ó",ocirc:"ô",otilde:"õ",ouml:"ö",divide:"÷",oslash:"ø",ugrave:"ù",uacute:"ú",ucirc:"û",uuml:"ü",yacute:"ý",thorn:"þ",yuml:"ÿ",OElig:"Œ",oelig:"œ",Scaron:"Š",scaron:"š",Yuml:"Ÿ",fnof:"ƒ",circ:"ˆ",tilde:"˜",Alpha:"Α",Beta:"Β",Gamma:"Γ",Delta:"Δ",Epsilon:"Ε",Zeta:"Ζ",Eta:"Η",Theta:"Θ",Iota:"Ι",Kappa:"Κ",Lambda:"Λ",Mu:"Μ",Nu:"Ν",Xi:"Ξ",Omicron:"Ο",Pi:"Π",Rho:"Ρ",Sigma:"Σ",Tau:"Τ",Upsilon:"Υ",Phi:"Φ",Chi:"Χ",Psi:"Ψ",Omega:"Ω",alpha:"α",beta:"β",gamma:"γ",delta:"δ",epsilon:"ε",zeta:"ζ",eta:"η",theta:"θ",iota:"ι",kappa:"κ",lambda:"λ",mu:"μ",nu:"ν",xi:"ξ",omicron:"ο",pi:"π",rho:"ρ",sigmaf:"ς",sigma:"σ",tau:"τ",upsilon:"υ",phi:"φ",chi:"χ",psi:"ψ",omega:"ω",thetasym:"ϑ",upsih:"ϒ",piv:"ϖ",ensp:" ",emsp:" ",thinsp:" ",zwnj:"‌",zwj:"‍",lrm:"‎",rlm:"‏",ndash:"–",mdash:"—",lsquo:"‘",rsquo:"’",sbquo:"‚",ldquo:"“",rdquo:"”",bdquo:"„",dagger:"†",Dagger:"‡",bull:"•",hellip:"…",permil:"‰",prime:"′",Prime:"″",lsaquo:"‹",rsaquo:"›",oline:"‾",frasl:"⁄",euro:"€",image:"ℑ",weierp:"℘",real:"ℜ",trade:"™",alefsym:"ℵ",larr:"←",uarr:"↑",rarr:"→",darr:"↓",harr:"↔",crarr:"↵",lArr:"⇐",uArr:"⇑",rArr:"⇒",dArr:"⇓",hArr:"⇔",forall:"∀",part:"∂",exist:"∃",empty:"∅",nabla:"∇",isin:"∈",notin:"∉",ni:"∋",prod:"∏",sum:"∑",minus:"−",lowast:"∗",radic:"√",prop:"∝",infin:"∞",ang:"∠",and:"∧",or:"∨",cap:"∩",cup:"∪",int:"∫",there4:"∴",sim:"∼",cong:"≅",asymp:"≈",ne:"≠",equiv:"≡",le:"≤",ge:"≥",sub:"⊂",sup:"⊃",nsub:"⊄",sube:"⊆",supe:"⊇",oplus:"⊕",otimes:"⊗",perp:"⊥",sdot:"⋅",lceil:"⌈",rceil:"⌉",lfloor:"⌊",rfloor:"⌋",loz:"◊",spades:"♠",clubs:"♣",hearts:"♥",diams:"♦",lang:"⟨",rang:"⟩"}},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n(10),i=n(12),o=n(13),s=function(){function t(){this.values=[],this.curly=this.paren=-1}return t.prototype.beforeFunctionExpression=function(t){return["(","{","[","in","typeof","instanceof","new","return","case","delete","throw","void","=","+=","-=","*=","**=","/=","%=","<<=",">>=",">>>=","&=","|=","^=",",","+","-","*","**","/","%","++","--","<<",">>",">>>","&","|","^","!","~","&&","||","?",":","===","==",">=","<=","<",">","!=","!=="].indexOf(t)>=0},t.prototype.isRegexStart=function(){var t=this.values[this.values.length-1],e=null!==t;switch(t){case"this":case"]":e=!1;break;case")":var n=this.values[this.paren-1];e="if"===n||"while"===n||"for"===n||"with"===n;break;case"}":if(e=!1,"function"===this.values[this.curly-3]){var r=this.values[this.curly-4];e=!!r&&!this.beforeFunctionExpression(r)}else if("function"===this.values[this.curly-4]){var r=this.values[this.curly-5];e=!r||!this.beforeFunctionExpression(r)}}return e},t.prototype.push=function(t){7===t.type||4===t.type?("{"===t.value?this.curly=this.values.length:"("===t.value&&(this.paren=this.values.length),this.values.push(t.value)):this.values.push(null)},t}(),a=function(){function t(t,e){this.errorHandler=new r.ErrorHandler,this.errorHandler.tolerant=!!e&&("boolean"==typeof e.tolerant&&e.tolerant),this.scanner=new i.Scanner(t,this.errorHandler),this.scanner.trackComment=!!e&&("boolean"==typeof e.comment&&e.comment),this.trackRange=!!e&&("boolean"==typeof e.range&&e.range),this.trackLoc=!!e&&("boolean"==typeof e.loc&&e.loc),this.buffer=[],this.reader=new s}return t.prototype.errors=function(){return this.errorHandler.errors},t.prototype.getNextToken=function(){if(0===this.buffer.length){var t=this.scanner.scanComments();if(this.scanner.trackComment)for(var e=0;e<t.length;++e){var n=t[e],r=this.scanner.source.slice(n.slice[0],n.slice[1]),i={type:n.multiLine?"BlockComment":"LineComment",value:r};this.trackRange&&(i.range=n.range),this.trackLoc&&(i.loc=n.loc),this.buffer.push(i)}if(!this.scanner.eof()){var s=void 0;this.trackLoc&&(s={start:{line:this.scanner.lineNumber,column:this.scanner.index-this.scanner.lineStart},end:{}});var a="/"===this.scanner.source[this.scanner.index]&&this.reader.isRegexStart(),u=a?this.scanner.scanRegExp():this.scanner.lex();this.reader.push(u);var c={type:o.TokenName[u.type],value:this.scanner.source.slice(u.start,u.end)};if(this.trackRange&&(c.range=[u.start,u.end]),this.trackLoc&&(s.end={line:this.scanner.lineNumber,column:this.scanner.index-this.scanner.lineStart},c.loc=s),9===u.type){var h=u.pattern,l=u.flags;c.regex={pattern:h,flags:l}}this.buffer.push(c)}}return this.buffer.shift()},t}();e.Tokenizer=a}])})},function(t,e){e.read=function(t,e,n,r,i){var o,s,a=8*i-r-1,u=(1<<a)-1,c=u>>1,h=-7,l=n?i-1:0,p=n?-1:1,f=t[e+l];for(l+=p,o=f&(1<<-h)-1,f>>=-h,h+=a;h>0;o=256*o+t[e+l],l+=p,h-=8);for(s=o&(1<<-h)-1,o>>=-h,h+=r;h>0;s=256*s+t[e+l],l+=p,h-=8);if(0===o)o=1-c;else{if(o===u)return s?NaN:1/0*(f?-1:1);s+=Math.pow(2,r),o-=c}return(f?-1:1)*s*Math.pow(2,o-r)},e.write=function(t,e,n,r,i,o){var s,a,u,c=8*o-i-1,h=(1<<c)-1,l=h>>1,p=23===i?Math.pow(2,-24)-Math.pow(2,-77):0,f=r?0:o-1,d=r?1:-1,m=e<0||0===e&&1/e<0?1:0;for(e=Math.abs(e),isNaN(e)||e===1/0?(a=isNaN(e)?1:0,s=h):(s=Math.floor(Math.log(e)/Math.LN2),e*(u=Math.pow(2,-s))<1&&(s--,u*=2),e+=s+l>=1?p/u:p*Math.pow(2,1-l),e*u>=2&&(s++,u/=2),s+l>=h?(a=0,s=h):s+l>=1?(a=(e*u-1)*Math.pow(2,i),s+=l):(a=e*Math.pow(2,l-1)*Math.pow(2,i),s=0));i>=8;t[n+f]=255&a,f+=d,a/=256,i-=8);for(s=s<<i|a,c+=i;c>0;t[n+f]=255&s,f+=d,s/=256,c-=8);t[n+f-d]|=128*m}},function(t,e,n){!function(e,n){t.exports=n()}(0,function(){"use strict";function t(t,e){e&&(t.prototype=Object.create(e.prototype)),t.prototype.constructor=t}function e(t){return o(t)?t:k(t)}function n(t){return s(t)?t:I(t)}function r(t){return a(t)?t:T(t)}function i(t){return o(t)&&!u(t)?t:B(t)}function o(t){return!(!t||!t[cn])}function s(t){return!(!t||!t[hn])}function a(t){return!(!t||!t[ln])}function u(t){return s(t)||a(t)}function c(t){return!(!t||!t[pn])}function h(t){return t.value=!1,t}function l(t){t&&(t.value=!0)}function p(){}function f(t,e){e=e||0;for(var n=Math.max(0,t.length-e),r=new Array(n),i=0;i<n;i++)r[i]=t[i+e];return r}function d(t){return void 0===t.size&&(t.size=t.__iterate(y)),t.size}function m(t,e){if("number"!=typeof e){var n=e>>>0;if(""+n!==e||4294967295===n)return NaN;e=n}return e<0?d(t)+e:e}function y(){return!0}function v(t,e,n){return(0===t||void 0!==n&&t<=-n)&&(void 0===e||void 0!==n&&e>=n)}function x(t,e){return D(t,e,0)}function g(t,e){return D(t,e,e)}function D(t,e,n){return void 0===t?n:t<0?Math.max(0,e+t):void 0===e?t:Math.min(e,t)}function E(t){this.next=t}function A(t,e,n,r){var i=0===t?e:1===t?n:[e,n];return r?r.value=i:r={value:i,done:!1},r}function S(){return{value:void 0,done:!0}}function w(t){return!!b(t)}function C(t){return t&&"function"==typeof t.next}function _(t){var e=b(t);return e&&e.call(t)}function b(t){var e=t&&(An&&t[An]||t[Sn]);if("function"==typeof e)return e}function F(t){return t&&"number"==typeof t.length}function k(t){return null===t||void 0===t?U():o(t)?t.toSeq():z(t)}function I(t){return null===t||void 0===t?U().toKeyedSeq():o(t)?s(t)?t.toSeq():t.fromEntrySeq():j(t)}function T(t){return null===t||void 0===t?U():o(t)?s(t)?t.entrySeq():t.toIndexedSeq():L(t)}function B(t){return(null===t||void 0===t?U():o(t)?s(t)?t.entrySeq():t:L(t)).toSetSeq()}function M(t){this._array=t,this.size=t.length}function P(t){var e=Object.keys(t);this._object=t,this._keys=e,this.size=e.length}function N(t){this._iterable=t,this.size=t.length||t.size}function O(t){this._iterator=t,this._iteratorCache=[]}function R(t){return!(!t||!t[Cn])}function U(){return _n||(_n=new M([]))}function j(t){var e=Array.isArray(t)?new M(t).fromEntrySeq():C(t)?new O(t).fromEntrySeq():w(t)?new N(t).fromEntrySeq():"object"==typeof t?new P(t):void 0;if(!e)throw new TypeError("Expected Array or iterable object of [k, v] entries, or keyed object: "+t);return e}function L(t){var e=J(t);if(!e)throw new TypeError("Expected Array or iterable object of values: "+t);return e}function z(t){var e=J(t)||"object"==typeof t&&new P(t);if(!e)throw new TypeError("Expected Array or iterable object of values, or keyed object: "+t);return e}function J(t){return F(t)?new M(t):C(t)?new O(t):w(t)?new N(t):void 0}function X(t,e,n,r){var i=t._cache;if(i){for(var o=i.length-1,s=0;s<=o;s++){var a=i[n?o-s:s];if(!1===e(a[1],r?a[0]:s,t))return s+1}return s}return t.__iterateUncached(e,n)}function q(t,e,n,r){var i=t._cache;if(i){var o=i.length-1,s=0;return new E(function(){var t=i[n?o-s:s];return s++>o?S():A(e,r?t[0]:s-1,t[1])})}return t.__iteratorUncached(e,n)}function K(t,e){return e?Y(e,t,"",{"":t}):W(t)}function Y(t,e,n,r){return Array.isArray(e)?t.call(r,n,T(e).map(function(n,r){return Y(t,n,r,e)})):G(e)?t.call(r,n,I(e).map(function(n,r){return Y(t,n,r,e)})):e}function W(t){return Array.isArray(t)?T(t).map(W).toList():G(t)?I(t).map(W).toMap():t}function G(t){return t&&(t.constructor===Object||void 0===t.constructor)}function H(t,e){if(t===e||t!==t&&e!==e)return!0;if(!t||!e)return!1;if("function"==typeof t.valueOf&&"function"==typeof e.valueOf){if(t=t.valueOf(),e=e.valueOf(),t===e||t!==t&&e!==e)return!0;if(!t||!e)return!1}return!("function"!=typeof t.equals||"function"!=typeof e.equals||!t.equals(e))}function V(t,e){if(t===e)return!0;if(!o(e)||void 0!==t.size&&void 0!==e.size&&t.size!==e.size||void 0!==t.__hash&&void 0!==e.__hash&&t.__hash!==e.__hash||s(t)!==s(e)||a(t)!==a(e)||c(t)!==c(e))return!1;if(0===t.size&&0===e.size)return!0;var n=!u(t);if(c(t)){var r=t.entries();return e.every(function(t,e){var i=r.next().value;return i&&H(i[1],t)&&(n||H(i[0],e))})&&r.next().done}var i=!1;if(void 0===t.size)if(void 0===e.size)"function"==typeof t.cacheResult&&t.cacheResult();else{i=!0;var h=t;t=e,e=h}var l=!0,p=e.__iterate(function(e,r){if(n?!t.has(e):i?!H(e,t.get(r,yn)):!H(t.get(r,yn),e))return l=!1,!1});return l&&t.size===p}function $(t,e){if(!(this instanceof $))return new $(t,e);if(this._value=t,this.size=void 0===e?1/0:Math.max(0,e),0===this.size){if(bn)return bn;bn=this}}function Z(t,e){if(!t)throw new Error(e)}function Q(t,e,n){if(!(this instanceof Q))return new Q(t,e,n);if(Z(0!==n,"Cannot step a Range by 0"),t=t||0,void 0===e&&(e=1/0),n=void 0===n?1:Math.abs(n),e<t&&(n=-n),this._start=t,this._end=e,this._step=n,this.size=Math.max(0,Math.ceil((e-t)/n-1)+1),0===this.size){if(Fn)return Fn;Fn=this}}function tt(){throw TypeError("Abstract")}function et(){}function nt(){}function rt(){}function it(t){return t>>>1&1073741824|3221225471&t}function ot(t){if(!1===t||null===t||void 0===t)return 0;if("function"==typeof t.valueOf&&(!1===(t=t.valueOf())||null===t||void 0===t))return 0;if(!0===t)return 1;var e=typeof t;if("number"===e){if(t!==t||t===1/0)return 0;var n=0|t;for(n!==t&&(n^=4294967295*t);t>4294967295;)t/=4294967295,n^=t;return it(n)}if("string"===e)return t.length>On?st(t):at(t);if("function"==typeof t.hashCode)return t.hashCode();if("object"===e)return ut(t);if("function"==typeof t.toString)return at(t.toString());throw new Error("Value type "+e+" cannot be hashed.")}function st(t){var e=jn[t];return void 0===e&&(e=at(t),Un===Rn&&(Un=0,jn={}),Un++,jn[t]=e),e}function at(t){for(var e=0,n=0;n<t.length;n++)e=31*e+t.charCodeAt(n)|0;return it(e)}function ut(t){var e;if(Mn&&void 0!==(e=kn.get(t)))return e;if(void 0!==(e=t[Nn]))return e;if(!Bn){if(void 0!==(e=t.propertyIsEnumerable&&t.propertyIsEnumerable[Nn]))return e;if(void 0!==(e=ct(t)))return e}if(e=++Pn,1073741824&Pn&&(Pn=0),Mn)kn.set(t,e);else{if(void 0!==Tn&&!1===Tn(t))throw new Error("Non-extensible objects are not allowed as keys.");if(Bn)Object.defineProperty(t,Nn,{enumerable:!1,configurable:!1,writable:!1,value:e});else if(void 0!==t.propertyIsEnumerable&&t.propertyIsEnumerable===t.constructor.prototype.propertyIsEnumerable)t.propertyIsEnumerable=function(){return this.constructor.prototype.propertyIsEnumerable.apply(this,arguments)},t.propertyIsEnumerable[Nn]=e;else{if(void 0===t.nodeType)throw new Error("Unable to set a non-enumerable property on object.");t[Nn]=e}}return e}function ct(t){if(t&&t.nodeType>0)switch(t.nodeType){case 1:return t.uniqueID;case 9:return t.documentElement&&t.documentElement.uniqueID}}function ht(t){Z(t!==1/0,"Cannot perform this action with an infinite size.")}function lt(t){return null===t||void 0===t?At():pt(t)&&!c(t)?t:At().withMutations(function(e){var r=n(t);ht(r.size),r.forEach(function(t,n){return e.set(n,t)})})}function pt(t){return!(!t||!t[Ln])}function ft(t,e){this.ownerID=t,this.entries=e}function dt(t,e,n){this.ownerID=t,this.bitmap=e,this.nodes=n}function mt(t,e,n){this.ownerID=t,this.count=e,this.nodes=n}function yt(t,e,n){this.ownerID=t,this.keyHash=e,this.entries=n}function vt(t,e,n){this.ownerID=t,this.keyHash=e,this.entry=n}function xt(t,e,n){this._type=e,this._reverse=n,this._stack=t._root&&Dt(t._root)}function gt(t,e){return A(t,e[0],e[1])}function Dt(t,e){return{node:t,index:0,__prev:e}}function Et(t,e,n,r){var i=Object.create(zn);return i.size=t,i._root=e,i.__ownerID=n,i.__hash=r,i.__altered=!1,i}function At(){return Jn||(Jn=Et(0))}function St(t,e,n){var r,i;if(t._root){var o=h(vn),s=h(xn);if(r=wt(t._root,t.__ownerID,0,void 0,e,n,o,s),!s.value)return t;i=t.size+(o.value?n===yn?-1:1:0)}else{if(n===yn)return t;i=1,r=new ft(t.__ownerID,[[e,n]])}return t.__ownerID?(t.size=i,t._root=r,t.__hash=void 0,t.__altered=!0,t):r?Et(i,r):At()}function wt(t,e,n,r,i,o,s,a){return t?t.update(e,n,r,i,o,s,a):o===yn?t:(l(a),l(s),new vt(e,r,[i,o]))}function Ct(t){return t.constructor===vt||t.constructor===yt}function _t(t,e,n,r,i){if(t.keyHash===r)return new yt(e,r,[t.entry,i]);var o,s=(0===n?t.keyHash:t.keyHash>>>n)&mn,a=(0===n?r:r>>>n)&mn;return new dt(e,1<<s|1<<a,s===a?[_t(t,e,n+fn,r,i)]:(o=new vt(e,r,i),s<a?[t,o]:[o,t]))}function bt(t,e,n,r){t||(t=new p);for(var i=new vt(t,ot(n),[n,r]),o=0;o<e.length;o++){var s=e[o];i=i.update(t,0,void 0,s[0],s[1])}return i}function Ft(t,e,n,r){for(var i=0,o=0,s=new Array(n),a=0,u=1,c=e.length;a<c;a++,u<<=1){var h=e[a];void 0!==h&&a!==r&&(i|=u,s[o++]=h)}return new dt(t,i,s)}function kt(t,e,n,r,i){for(var o=0,s=new Array(dn),a=0;0!==n;a++,n>>>=1)s[a]=1&n?e[o++]:void 0;return s[r]=i,new mt(t,o+1,s)}function It(t,e,r){for(var i=[],s=0;s<r.length;s++){var a=r[s],u=n(a);o(a)||(u=u.map(function(t){return K(t)})),i.push(u)}return Mt(t,e,i)}function Tt(t,e,n){return t&&t.mergeDeep&&o(e)?t.mergeDeep(e):H(t,e)?t:e}function Bt(t){return function(e,n,r){if(e&&e.mergeDeepWith&&o(n))return e.mergeDeepWith(t,n);var i=t(e,n,r);return H(e,i)?e:i}}function Mt(t,e,n){return n=n.filter(function(t){return 0!==t.size}),0===n.length?t:0!==t.size||t.__ownerID||1!==n.length?t.withMutations(function(t){for(var r=e?function(n,r){t.update(r,yn,function(t){return t===yn?n:e(t,n,r)})}:function(e,n){t.set(n,e)},i=0;i<n.length;i++)n[i].forEach(r)}):t.constructor(n[0])}function Pt(t,e,n,r){var i=t===yn,o=e.next();if(o.done){var s=i?n:t,a=r(s);return a===s?t:a}Z(i||t&&t.set,"invalid keyPath");var u=o.value,c=i?yn:t.get(u,yn),h=Pt(c,e,n,r);return h===c?t:h===yn?t.remove(u):(i?At():t).set(u,h)}function Nt(t){return t-=t>>1&1431655765,t=(858993459&t)+(t>>2&858993459),t=t+(t>>4)&252645135,t+=t>>8,127&(t+=t>>16)}function Ot(t,e,n,r){var i=r?t:f(t);return i[e]=n,i}function Rt(t,e,n,r){var i=t.length+1;if(r&&e+1===i)return t[e]=n,t;for(var o=new Array(i),s=0,a=0;a<i;a++)a===e?(o[a]=n,s=-1):o[a]=t[a+s];return o}function Ut(t,e,n){var r=t.length-1;if(n&&e===r)return t.pop(),t;for(var i=new Array(r),o=0,s=0;s<r;s++)s===e&&(o=1),i[s]=t[s+o];return i}function jt(t){var e=qt();if(null===t||void 0===t)return e;if(Lt(t))return t;var n=r(t),i=n.size;return 0===i?e:(ht(i),i>0&&i<dn?Xt(0,i,fn,null,new zt(n.toArray())):e.withMutations(function(t){t.setSize(i),n.forEach(function(e,n){return t.set(n,e)})}))}function Lt(t){return!(!t||!t[Yn])}function zt(t,e){this.array=t,this.ownerID=e}function Jt(t,e){function n(t,e,n){return 0===e?r(t,n):i(t,e,n)}function r(t,n){var r=n===a?u&&u.array:t&&t.array,i=n>o?0:o-n,c=s-n;return c>dn&&(c=dn),function(){if(i===c)return Hn;var t=e?--c:i++;return r&&r[t]}}function i(t,r,i){var a,u=t&&t.array,c=i>o?0:o-i>>r,h=1+(s-i>>r);return h>dn&&(h=dn),function(){for(;;){if(a){var t=a();if(t!==Hn)return t;a=null}if(c===h)return Hn;var o=e?--h:c++;a=n(u&&u[o],r-fn,i+(o<<r))}}}var o=t._origin,s=t._capacity,a=$t(s),u=t._tail;return n(t._root,t._level,0)}function Xt(t,e,n,r,i,o,s){var a=Object.create(Wn);return a.size=e-t,a._origin=t,a._capacity=e,a._level=n,a._root=r,a._tail=i,a.__ownerID=o,a.__hash=s,a.__altered=!1,a}function qt(){return Gn||(Gn=Xt(0,0,fn))}function Kt(t,e,n){if((e=m(t,e))!==e)return t;if(e>=t.size||e<0)return t.withMutations(function(t){e<0?Ht(t,e).set(0,n):Ht(t,0,e+1).set(e,n)});e+=t._origin;var r=t._tail,i=t._root,o=h(xn);return e>=$t(t._capacity)?r=Yt(r,t.__ownerID,0,e,n,o):i=Yt(i,t.__ownerID,t._level,e,n,o),o.value?t.__ownerID?(t._root=i,t._tail=r,t.__hash=void 0,t.__altered=!0,t):Xt(t._origin,t._capacity,t._level,i,r):t}function Yt(t,e,n,r,i,o){var s=r>>>n&mn,a=t&&s<t.array.length;if(!a&&void 0===i)return t;var u;if(n>0){var c=t&&t.array[s],h=Yt(c,e,n-fn,r,i,o);return h===c?t:(u=Wt(t,e),u.array[s]=h,u)}return a&&t.array[s]===i?t:(l(o),u=Wt(t,e),void 0===i&&s===u.array.length-1?u.array.pop():u.array[s]=i,u)}function Wt(t,e){return e&&t&&e===t.ownerID?t:new zt(t?t.array.slice():[],e)}function Gt(t,e){if(e>=$t(t._capacity))return t._tail;if(e<1<<t._level+fn){for(var n=t._root,r=t._level;n&&r>0;)n=n.array[e>>>r&mn],r-=fn;return n}}function Ht(t,e,n){void 0!==e&&(e|=0),void 0!==n&&(n|=0);var r=t.__ownerID||new p,i=t._origin,o=t._capacity,s=i+e,a=void 0===n?o:n<0?o+n:i+n;if(s===i&&a===o)return t;if(s>=a)return t.clear();for(var u=t._level,c=t._root,h=0;s+h<0;)c=new zt(c&&c.array.length?[void 0,c]:[],r),u+=fn,h+=1<<u;h&&(s+=h,i+=h,a+=h,o+=h);for(var l=$t(o),f=$t(a);f>=1<<u+fn;)c=new zt(c&&c.array.length?[c]:[],r),u+=fn;var d=t._tail,m=f<l?Gt(t,a-1):f>l?new zt([],r):d;if(d&&f>l&&s<o&&d.array.length){c=Wt(c,r);for(var y=c,v=u;v>fn;v-=fn){var x=l>>>v&mn;y=y.array[x]=Wt(y.array[x],r)}y.array[l>>>fn&mn]=d}if(a<o&&(m=m&&m.removeAfter(r,0,a)),s>=f)s-=f,a-=f,u=fn,c=null,m=m&&m.removeBefore(r,0,s);else if(s>i||f<l){for(h=0;c;){var g=s>>>u&mn;if(g!==f>>>u&mn)break;g&&(h+=(1<<u)*g),u-=fn,c=c.array[g]}c&&s>i&&(c=c.removeBefore(r,u,s-h)),c&&f<l&&(c=c.removeAfter(r,u,f-h)),h&&(s-=h,a-=h)}return t.__ownerID?(t.size=a-s,t._origin=s,t._capacity=a,t._level=u,t._root=c,t._tail=m,t.__hash=void 0,t.__altered=!0,t):Xt(s,a,u,c,m)}function Vt(t,e,n){for(var i=[],s=0,a=0;a<n.length;a++){var u=n[a],c=r(u);c.size>s&&(s=c.size),o(u)||(c=c.map(function(t){return K(t)})),i.push(c)}return s>t.size&&(t=t.setSize(s)),Mt(t,e,i)}function $t(t){return t<dn?0:t-1>>>fn<<fn}function Zt(t){return null===t||void 0===t?ee():Qt(t)?t:ee().withMutations(function(e){var r=n(t);ht(r.size),r.forEach(function(t,n){return e.set(n,t)})})}function Qt(t){return pt(t)&&c(t)}function te(t,e,n,r){var i=Object.create(Zt.prototype);return i.size=t?t.size:0,i._map=t,i._list=e,i.__ownerID=n,i.__hash=r,i}function ee(){return Vn||(Vn=te(At(),qt()))}function ne(t,e,n){var r,i,o=t._map,s=t._list,a=o.get(e),u=void 0!==a;if(n===yn){if(!u)return t;s.size>=dn&&s.size>=2*o.size?(i=s.filter(function(t,e){return void 0!==t&&a!==e}),r=i.toKeyedSeq().map(function(t){return t[0]}).flip().toMap(),t.__ownerID&&(r.__ownerID=i.__ownerID=t.__ownerID)):(r=o.remove(e),i=a===s.size-1?s.pop():s.set(a,void 0))}else if(u){if(n===s.get(a)[1])return t;r=o,i=s.set(a,[e,n])}else r=o.set(e,s.size),i=s.set(s.size,[e,n]);return t.__ownerID?(t.size=r.size,t._map=r,t._list=i,t.__hash=void 0,t):te(r,i)}function re(t,e){this._iter=t,this._useKeys=e,this.size=t.size}function ie(t){this._iter=t,this.size=t.size}function oe(t){this._iter=t,this.size=t.size}function se(t){this._iter=t,this.size=t.size}function ae(t){var e=Fe(t);return e._iter=t,e.size=t.size,e.flip=function(){return t},e.reverse=function(){var e=t.reverse.apply(this);return e.flip=function(){return t.reverse()},e},e.has=function(e){return t.includes(e)},e.includes=function(e){return t.has(e)},e.cacheResult=ke,e.__iterateUncached=function(e,n){var r=this;return t.__iterate(function(t,n){return!1!==e(n,t,r)},n)},e.__iteratorUncached=function(e,n){if(e===En){var r=t.__iterator(e,n);return new E(function(){var t=r.next();if(!t.done){var e=t.value[0];t.value[0]=t.value[1],t.value[1]=e}return t})}return t.__iterator(e===Dn?gn:Dn,n)},e}function ue(t,e,n){var r=Fe(t);return r.size=t.size,r.has=function(e){return t.has(e)},r.get=function(r,i){var o=t.get(r,yn);return o===yn?i:e.call(n,o,r,t)},r.__iterateUncached=function(r,i){var o=this;return t.__iterate(function(t,i,s){return!1!==r(e.call(n,t,i,s),i,o)},i)},r.__iteratorUncached=function(r,i){var o=t.__iterator(En,i);return new E(function(){var i=o.next();if(i.done)return i;var s=i.value,a=s[0];return A(r,a,e.call(n,s[1],a,t),i)})},r}function ce(t,e){var n=Fe(t);return n._iter=t,n.size=t.size,n.reverse=function(){return t},t.flip&&(n.flip=function(){var e=ae(t);return e.reverse=function(){return t.flip()},e}),n.get=function(n,r){return t.get(e?n:-1-n,r)},n.has=function(n){return t.has(e?n:-1-n)},n.includes=function(e){return t.includes(e)},n.cacheResult=ke,n.__iterate=function(e,n){var r=this;return t.__iterate(function(t,n){return e(t,n,r)},!n)},n.__iterator=function(e,n){return t.__iterator(e,!n)},n}function he(t,e,n,r){var i=Fe(t);return r&&(i.has=function(r){var i=t.get(r,yn);return i!==yn&&!!e.call(n,i,r,t)},i.get=function(r,i){var o=t.get(r,yn);return o!==yn&&e.call(n,o,r,t)?o:i}),i.__iterateUncached=function(i,o){var s=this,a=0;return t.__iterate(function(t,o,u){if(e.call(n,t,o,u))return a++,i(t,r?o:a-1,s)},o),a},i.__iteratorUncached=function(i,o){var s=t.__iterator(En,o),a=0;return new E(function(){for(;;){var o=s.next();if(o.done)return o;var u=o.value,c=u[0],h=u[1];if(e.call(n,h,c,t))return A(i,r?c:a++,h,o)}})},i}function le(t,e,n){var r=lt().asMutable();return t.__iterate(function(i,o){r.update(e.call(n,i,o,t),0,function(t){return t+1})}),r.asImmutable()}function pe(t,e,n){var r=s(t),i=(c(t)?Zt():lt()).asMutable();t.__iterate(function(o,s){i.update(e.call(n,o,s,t),function(t){return t=t||[],t.push(r?[s,o]:o),t})});var o=be(t);return i.map(function(e){return we(t,o(e))})}function fe(t,e,n,r){var i=t.size;if(void 0!==e&&(e|=0),void 0!==n&&(n===1/0?n=i:n|=0),v(e,n,i))return t;var o=x(e,i),s=g(n,i);if(o!==o||s!==s)return fe(t.toSeq().cacheResult(),e,n,r);var a,u=s-o;u===u&&(a=u<0?0:u);var c=Fe(t);return c.size=0===a?a:t.size&&a||void 0,!r&&R(t)&&a>=0&&(c.get=function(e,n){return e=m(this,e),e>=0&&e<a?t.get(e+o,n):n}),c.__iterateUncached=function(e,n){var i=this;if(0===a)return 0;if(n)return this.cacheResult().__iterate(e,n);var s=0,u=!0,c=0;return t.__iterate(function(t,n){if(!u||!(u=s++<o))return c++,!1!==e(t,r?n:c-1,i)&&c!==a}),c},c.__iteratorUncached=function(e,n){if(0!==a&&n)return this.cacheResult().__iterator(e,n);var i=0!==a&&t.__iterator(e,n),s=0,u=0;return new E(function(){for(;s++<o;)i.next();if(++u>a)return S();var t=i.next();return r||e===Dn?t:e===gn?A(e,u-1,void 0,t):A(e,u-1,t.value[1],t)})},c}function de(t,e,n){var r=Fe(t);return r.__iterateUncached=function(r,i){var o=this;if(i)return this.cacheResult().__iterate(r,i);var s=0;return t.__iterate(function(t,i,a){return e.call(n,t,i,a)&&++s&&r(t,i,o)}),s},r.__iteratorUncached=function(r,i){var o=this;if(i)return this.cacheResult().__iterator(r,i);var s=t.__iterator(En,i),a=!0;return new E(function(){if(!a)return S();var t=s.next();if(t.done)return t;var i=t.value,u=i[0],c=i[1];return e.call(n,c,u,o)?r===En?t:A(r,u,c,t):(a=!1,S())})},r}function me(t,e,n,r){var i=Fe(t);return i.__iterateUncached=function(i,o){var s=this;if(o)return this.cacheResult().__iterate(i,o);var a=!0,u=0;return t.__iterate(function(t,o,c){if(!a||!(a=e.call(n,t,o,c)))return u++,i(t,r?o:u-1,s)}),u},i.__iteratorUncached=function(i,o){var s=this;if(o)return this.cacheResult().__iterator(i,o);var a=t.__iterator(En,o),u=!0,c=0;return new E(function(){var t,o,h;do{if(t=a.next(),t.done)return r||i===Dn?t:i===gn?A(i,c++,void 0,t):A(i,c++,t.value[1],t);var l=t.value;o=l[0],h=l[1],u&&(u=e.call(n,h,o,s))}while(u);return i===En?t:A(i,o,h,t)})},i}function ye(t,e){var r=s(t),i=[t].concat(e).map(function(t){return o(t)?r&&(t=n(t)):t=r?j(t):L(Array.isArray(t)?t:[t]),t}).filter(function(t){return 0!==t.size});if(0===i.length)return t;if(1===i.length){var u=i[0];if(u===t||r&&s(u)||a(t)&&a(u))return u}var c=new M(i);return r?c=c.toKeyedSeq():a(t)||(c=c.toSetSeq()),c=c.flatten(!0),c.size=i.reduce(function(t,e){if(void 0!==t){var n=e.size;if(void 0!==n)return t+n}},0),c}function ve(t,e,n){var r=Fe(t);return r.__iterateUncached=function(r,i){function s(t,c){var h=this;t.__iterate(function(t,i){return(!e||c<e)&&o(t)?s(t,c+1):!1===r(t,n?i:a++,h)&&(u=!0),!u},i)}var a=0,u=!1;return s(t,0),a},r.__iteratorUncached=function(r,i){var s=t.__iterator(r,i),a=[],u=0;return new E(function(){for(;s;){var t=s.next();if(!1===t.done){var c=t.value;if(r===En&&(c=c[1]),e&&!(a.length<e)||!o(c))return n?t:A(r,u++,c,t);a.push(s),s=c.__iterator(r,i)}else s=a.pop()}return S()})},r}function xe(t,e,n){var r=be(t);return t.toSeq().map(function(i,o){return r(e.call(n,i,o,t))}).flatten(!0)}function ge(t,e){var n=Fe(t);return n.size=t.size&&2*t.size-1,n.__iterateUncached=function(n,r){var i=this,o=0;return t.__iterate(function(t,r){return(!o||!1!==n(e,o++,i))&&!1!==n(t,o++,i)},r),o},n.__iteratorUncached=function(n,r){var i,o=t.__iterator(Dn,r),s=0;return new E(function(){return(!i||s%2)&&(i=o.next(),i.done)?i:s%2?A(n,s++,e):A(n,s++,i.value,i)})},n}function De(t,e,n){e||(e=Ie);var r=s(t),i=0,o=t.toSeq().map(function(e,r){return[r,e,i++,n?n(e,r,t):e]}).toArray();return o.sort(function(t,n){return e(t[3],n[3])||t[2]-n[2]}).forEach(r?function(t,e){o[e].length=2}:function(t,e){o[e]=t[1]}),r?I(o):a(t)?T(o):B(o)}function Ee(t,e,n){if(e||(e=Ie),n){var r=t.toSeq().map(function(e,r){return[e,n(e,r,t)]}).reduce(function(t,n){return Ae(e,t[1],n[1])?n:t});return r&&r[0]}return t.reduce(function(t,n){return Ae(e,t,n)?n:t})}function Ae(t,e,n){var r=t(n,e);return 0===r&&n!==e&&(void 0===n||null===n||n!==n)||r>0}function Se(t,n,r){var i=Fe(t);return i.size=new M(r).map(function(t){return t.size}).min(),i.__iterate=function(t,e){for(var n,r=this.__iterator(Dn,e),i=0;!(n=r.next()).done&&!1!==t(n.value,i++,this););return i},i.__iteratorUncached=function(t,i){var o=r.map(function(t){return t=e(t),_(i?t.reverse():t)}),s=0,a=!1;return new E(function(){var e;return a||(e=o.map(function(t){return t.next()}),a=e.some(function(t){return t.done})),a?S():A(t,s++,n.apply(null,e.map(function(t){return t.value})))})},i}function we(t,e){return R(t)?e:t.constructor(e)}function Ce(t){if(t!==Object(t))throw new TypeError("Expected [K, V] tuple: "+t)}function _e(t){return ht(t.size),d(t)}function be(t){return s(t)?n:a(t)?r:i}function Fe(t){return Object.create((s(t)?I:a(t)?T:B).prototype)}function ke(){return this._iter.cacheResult?(this._iter.cacheResult(),this.size=this._iter.size,this):k.prototype.cacheResult.call(this)}function Ie(t,e){return t>e?1:t<e?-1:0}function Te(t){var n=_(t);if(!n){if(!F(t))throw new TypeError("Expected iterable or array-like: "+t);n=_(e(t))}return n}function Be(t,e){var n,r=function(o){if(o instanceof r)return o;if(!(this instanceof r))return new r(o);if(!n){n=!0;var s=Object.keys(t);Ne(i,s),i.size=s.length,i._name=e,i._keys=s,i._defaultValues=t}this._map=lt(o)},i=r.prototype=Object.create($n);return i.constructor=r,r}function Me(t,e,n){var r=Object.create(Object.getPrototypeOf(t));return r._map=e,r.__ownerID=n,r}function Pe(t){return t._name||t.constructor.name||"Record"}function Ne(t,e){try{e.forEach(Oe.bind(void 0,t))}catch(t){}}function Oe(t,e){Object.defineProperty(t,e,{get:function(){return this.get(e)},set:function(t){Z(this.__ownerID,"Cannot set on an immutable record."),this.set(e,t)}})}function Re(t){return null===t||void 0===t?ze():Ue(t)&&!c(t)?t:ze().withMutations(function(e){var n=i(t);ht(n.size),n.forEach(function(t){return e.add(t)})})}function Ue(t){return!(!t||!t[Zn])}function je(t,e){return t.__ownerID?(t.size=e.size,t._map=e,t):e===t._map?t:0===e.size?t.__empty():t.__make(e)}function Le(t,e){var n=Object.create(Qn);return n.size=t?t.size:0,n._map=t,n.__ownerID=e,n}function ze(){return tr||(tr=Le(At()))}function Je(t){return null===t||void 0===t?Ke():Xe(t)?t:Ke().withMutations(function(e){var n=i(t);ht(n.size),n.forEach(function(t){return e.add(t)})})}function Xe(t){return Ue(t)&&c(t)}function qe(t,e){var n=Object.create(er);return n.size=t?t.size:0,n._map=t,n.__ownerID=e,n}function Ke(){return nr||(nr=qe(ee()))}function Ye(t){return null===t||void 0===t?He():We(t)?t:He().unshiftAll(t)}function We(t){return!(!t||!t[rr])}function Ge(t,e,n,r){var i=Object.create(ir);return i.size=t,i._head=e,i.__ownerID=n,i.__hash=r,i.__altered=!1,i}function He(){return or||(or=Ge(0))}function Ve(t,e){var n=function(n){t.prototype[n]=e[n]};return Object.keys(e).forEach(n),Object.getOwnPropertySymbols&&Object.getOwnPropertySymbols(e).forEach(n),t}function $e(t,e){return e}function Ze(t,e){return[e,t]}function Qe(t){return function(){return!t.apply(this,arguments)}}function tn(t){return function(){return-t.apply(this,arguments)}}function en(t){return"string"==typeof t?JSON.stringify(t):String(t)}function nn(){return f(arguments)}function rn(t,e){return t<e?1:t>e?-1:0}function on(t){if(t.size===1/0)return 0;var e=c(t),n=s(t),r=e?1:0;return sn(t.__iterate(n?e?function(t,e){r=31*r+an(ot(t),ot(e))|0}:function(t,e){r=r+an(ot(t),ot(e))|0}:e?function(t){r=31*r+ot(t)|0}:function(t){r=r+ot(t)|0}),r)}function sn(t,e){return e=In(e,3432918353),e=In(e<<15|e>>>-15,461845907),e=In(e<<13|e>>>-13,5),e=(e+3864292196|0)^t,e=In(e^e>>>16,2246822507),e=In(e^e>>>13,3266489909),e=it(e^e>>>16)}function an(t,e){return t^e+2654435769+(t<<6)+(t>>2)|0}var un=Array.prototype.slice;t(n,e),t(r,e),t(i,e),e.isIterable=o,e.isKeyed=s,e.isIndexed=a,e.isAssociative=u,e.isOrdered=c,e.Keyed=n,e.Indexed=r,e.Set=i;var cn="@@__IMMUTABLE_ITERABLE__@@",hn="@@__IMMUTABLE_KEYED__@@",ln="@@__IMMUTABLE_INDEXED__@@",pn="@@__IMMUTABLE_ORDERED__@@",fn=5,dn=1<<fn,mn=dn-1,yn={},vn={value:!1},xn={value:!1},gn=0,Dn=1,En=2,An="function"==typeof Symbol&&Symbol.iterator,Sn="@@iterator",wn=An||Sn;E.prototype.toString=function(){return"[Iterator]"},E.KEYS=gn,E.VALUES=Dn,E.ENTRIES=En,E.prototype.inspect=E.prototype.toSource=function(){return this.toString()},E.prototype[wn]=function(){return this},t(k,e),k.of=function(){return k(arguments)},k.prototype.toSeq=function(){return this},k.prototype.toString=function(){return this.__toString("Seq {","}")},k.prototype.cacheResult=function(){return!this._cache&&this.__iterateUncached&&(this._cache=this.entrySeq().toArray(),this.size=this._cache.length),this},k.prototype.__iterate=function(t,e){return X(this,t,e,!0)},k.prototype.__iterator=function(t,e){return q(this,t,e,!0)},t(I,k),I.prototype.toKeyedSeq=function(){return this},t(T,k),T.of=function(){return T(arguments)},T.prototype.toIndexedSeq=function(){return this},T.prototype.toString=function(){return this.__toString("Seq [","]")},T.prototype.__iterate=function(t,e){return X(this,t,e,!1)},T.prototype.__iterator=function(t,e){return q(this,t,e,!1)},t(B,k),B.of=function(){return B(arguments)},B.prototype.toSetSeq=function(){return this},k.isSeq=R,k.Keyed=I,k.Set=B,k.Indexed=T;var Cn="@@__IMMUTABLE_SEQ__@@";k.prototype[Cn]=!0,t(M,T),M.prototype.get=function(t,e){return this.has(t)?this._array[m(this,t)]:e},M.prototype.__iterate=function(t,e){for(var n=this._array,r=n.length-1,i=0;i<=r;i++)if(!1===t(n[e?r-i:i],i,this))return i+1;return i},M.prototype.__iterator=function(t,e){var n=this._array,r=n.length-1,i=0;return new E(function(){return i>r?S():A(t,i,n[e?r-i++:i++])})},t(P,I),P.prototype.get=function(t,e){return void 0===e||this.has(t)?this._object[t]:e},P.prototype.has=function(t){return this._object.hasOwnProperty(t)},P.prototype.__iterate=function(t,e){for(var n=this._object,r=this._keys,i=r.length-1,o=0;o<=i;o++){var s=r[e?i-o:o];if(!1===t(n[s],s,this))return o+1}return o},P.prototype.__iterator=function(t,e){var n=this._object,r=this._keys,i=r.length-1,o=0;return new E(function(){var s=r[e?i-o:o];return o++>i?S():A(t,s,n[s])})},P.prototype[pn]=!0,t(N,T),N.prototype.__iterateUncached=function(t,e){if(e)return this.cacheResult().__iterate(t,e);var n=this._iterable,r=_(n),i=0;if(C(r))for(var o;!(o=r.next()).done&&!1!==t(o.value,i++,this););return i},N.prototype.__iteratorUncached=function(t,e){if(e)return this.cacheResult().__iterator(t,e);var n=this._iterable,r=_(n);if(!C(r))return new E(S);var i=0;return new E(function(){var e=r.next();return e.done?e:A(t,i++,e.value)})},t(O,T),O.prototype.__iterateUncached=function(t,e){if(e)return this.cacheResult().__iterate(t,e);for(var n=this._iterator,r=this._iteratorCache,i=0;i<r.length;)if(!1===t(r[i],i++,this))return i;for(var o;!(o=n.next()).done;){var s=o.value;if(r[i]=s,!1===t(s,i++,this))break}return i},O.prototype.__iteratorUncached=function(t,e){if(e)return this.cacheResult().__iterator(t,e);var n=this._iterator,r=this._iteratorCache,i=0;return new E(function(){if(i>=r.length){var e=n.next();if(e.done)return e;r[i]=e.value}return A(t,i,r[i++])})};var _n;t($,T),$.prototype.toString=function(){return 0===this.size?"Repeat []":"Repeat [ "+this._value+" "+this.size+" times ]"},$.prototype.get=function(t,e){return this.has(t)?this._value:e},$.prototype.includes=function(t){return H(this._value,t)},$.prototype.slice=function(t,e){var n=this.size;return v(t,e,n)?this:new $(this._value,g(e,n)-x(t,n))},$.prototype.reverse=function(){return this},$.prototype.indexOf=function(t){return H(this._value,t)?0:-1},$.prototype.lastIndexOf=function(t){return H(this._value,t)?this.size:-1},$.prototype.__iterate=function(t,e){for(var n=0;n<this.size;n++)if(!1===t(this._value,n,this))return n+1;return n},$.prototype.__iterator=function(t,e){var n=this,r=0;return new E(function(){return r<n.size?A(t,r++,n._value):S()})},$.prototype.equals=function(t){return t instanceof $?H(this._value,t._value):V(t)};var bn;t(Q,T),Q.prototype.toString=function(){return 0===this.size?"Range []":"Range [ "+this._start+"..."+this._end+(1!==this._step?" by "+this._step:"")+" ]"},Q.prototype.get=function(t,e){return this.has(t)?this._start+m(this,t)*this._step:e},Q.prototype.includes=function(t){var e=(t-this._start)/this._step;return e>=0&&e<this.size&&e===Math.floor(e)},Q.prototype.slice=function(t,e){return v(t,e,this.size)?this:(t=x(t,this.size),e=g(e,this.size),e<=t?new Q(0,0):new Q(this.get(t,this._end),this.get(e,this._end),this._step))},Q.prototype.indexOf=function(t){var e=t-this._start;if(e%this._step==0){var n=e/this._step;if(n>=0&&n<this.size)return n}return-1},Q.prototype.lastIndexOf=function(t){return this.indexOf(t)},Q.prototype.__iterate=function(t,e){for(var n=this.size-1,r=this._step,i=e?this._start+n*r:this._start,o=0;o<=n;o++){if(!1===t(i,o,this))return o+1;i+=e?-r:r}return o},Q.prototype.__iterator=function(t,e){var n=this.size-1,r=this._step,i=e?this._start+n*r:this._start,o=0;return new E(function(){var s=i;return i+=e?-r:r,o>n?S():A(t,o++,s)})},Q.prototype.equals=function(t){return t instanceof Q?this._start===t._start&&this._end===t._end&&this._step===t._step:V(this,t)};var Fn;t(tt,e),t(et,tt),t(nt,tt),t(rt,tt),tt.Keyed=et,tt.Indexed=nt,tt.Set=rt;var kn,In="function"==typeof Math.imul&&-2===Math.imul(4294967295,2)?Math.imul:function(t,e){t|=0,e|=0;var n=65535&t,r=65535&e;return n*r+((t>>>16)*r+n*(e>>>16)<<16>>>0)|0},Tn=Object.isExtensible,Bn=function(){try{return Object.defineProperty({},"@",{}),!0}catch(t){return!1}}(),Mn="function"==typeof WeakMap;Mn&&(kn=new WeakMap);var Pn=0,Nn="__immutablehash__";"function"==typeof Symbol&&(Nn=Symbol(Nn));var On=16,Rn=255,Un=0,jn={};t(lt,et),lt.of=function(){var t=un.call(arguments,0);return At().withMutations(function(e){for(var n=0;n<t.length;n+=2){if(n+1>=t.length)throw new Error("Missing value for key: "+t[n]);e.set(t[n],t[n+1])}})},lt.prototype.toString=function(){return this.__toString("Map {","}")},lt.prototype.get=function(t,e){return this._root?this._root.get(0,void 0,t,e):e},lt.prototype.set=function(t,e){return St(this,t,e)},lt.prototype.setIn=function(t,e){return this.updateIn(t,yn,function(){return e})},lt.prototype.remove=function(t){return St(this,t,yn)},lt.prototype.deleteIn=function(t){return this.updateIn(t,function(){return yn})},lt.prototype.update=function(t,e,n){return 1===arguments.length?t(this):this.updateIn([t],e,n)},lt.prototype.updateIn=function(t,e,n){n||(n=e,e=void 0);var r=Pt(this,Te(t),e,n);return r===yn?void 0:r},lt.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._root=null,this.__hash=void 0,this.__altered=!0,this):At()},lt.prototype.merge=function(){return It(this,void 0,arguments)},lt.prototype.mergeWith=function(t){return It(this,t,un.call(arguments,1))},lt.prototype.mergeIn=function(t){var e=un.call(arguments,1);return this.updateIn(t,At(),function(t){return"function"==typeof t.merge?t.merge.apply(t,e):e[e.length-1]})},lt.prototype.mergeDeep=function(){return It(this,Tt,arguments)},lt.prototype.mergeDeepWith=function(t){var e=un.call(arguments,1);return It(this,Bt(t),e)},lt.prototype.mergeDeepIn=function(t){var e=un.call(arguments,1);return this.updateIn(t,At(),function(t){return"function"==typeof t.mergeDeep?t.mergeDeep.apply(t,e):e[e.length-1]})},lt.prototype.sort=function(t){return Zt(De(this,t))},lt.prototype.sortBy=function(t,e){return Zt(De(this,e,t))},lt.prototype.withMutations=function(t){var e=this.asMutable();return t(e),e.wasAltered()?e.__ensureOwner(this.__ownerID):this},lt.prototype.asMutable=function(){return this.__ownerID?this:this.__ensureOwner(new p)},lt.prototype.asImmutable=function(){return this.__ensureOwner()},lt.prototype.wasAltered=function(){return this.__altered},lt.prototype.__iterator=function(t,e){return new xt(this,t,e)},lt.prototype.__iterate=function(t,e){var n=this,r=0;return this._root&&this._root.iterate(function(e){return r++,t(e[1],e[0],n)},e),r},lt.prototype.__ensureOwner=function(t){return t===this.__ownerID?this:t?Et(this.size,this._root,t,this.__hash):(this.__ownerID=t,this.__altered=!1,this)},lt.isMap=pt;var Ln="@@__IMMUTABLE_MAP__@@",zn=lt.prototype;zn[Ln]=!0,zn.delete=zn.remove,zn.removeIn=zn.deleteIn,ft.prototype.get=function(t,e,n,r){for(var i=this.entries,o=0,s=i.length;o<s;o++)if(H(n,i[o][0]))return i[o][1];return r},ft.prototype.update=function(t,e,n,r,i,o,s){for(var a=i===yn,u=this.entries,c=0,h=u.length;c<h&&!H(r,u[c][0]);c++);var p=c<h;if(p?u[c][1]===i:a)return this;if(l(s),(a||!p)&&l(o),!a||1!==u.length){if(!p&&!a&&u.length>=Xn)return bt(t,u,r,i);var d=t&&t===this.ownerID,m=d?u:f(u);return p?a?c===h-1?m.pop():m[c]=m.pop():m[c]=[r,i]:m.push([r,i]),d?(this.entries=m,this):new ft(t,m)}},dt.prototype.get=function(t,e,n,r){void 0===e&&(e=ot(n));var i=1<<((0===t?e:e>>>t)&mn),o=this.bitmap;return 0==(o&i)?r:this.nodes[Nt(o&i-1)].get(t+fn,e,n,r)},dt.prototype.update=function(t,e,n,r,i,o,s){void 0===n&&(n=ot(r));var a=(0===e?n:n>>>e)&mn,u=1<<a,c=this.bitmap,h=0!=(c&u);if(!h&&i===yn)return this;var l=Nt(c&u-1),p=this.nodes,f=h?p[l]:void 0,d=wt(f,t,e+fn,n,r,i,o,s);if(d===f)return this;if(!h&&d&&p.length>=qn)return kt(t,p,c,a,d);if(h&&!d&&2===p.length&&Ct(p[1^l]))return p[1^l];if(h&&d&&1===p.length&&Ct(d))return d;var m=t&&t===this.ownerID,y=h?d?c:c^u:c|u,v=h?d?Ot(p,l,d,m):Ut(p,l,m):Rt(p,l,d,m);return m?(this.bitmap=y,this.nodes=v,this):new dt(t,y,v)},mt.prototype.get=function(t,e,n,r){void 0===e&&(e=ot(n));var i=(0===t?e:e>>>t)&mn,o=this.nodes[i];return o?o.get(t+fn,e,n,r):r},mt.prototype.update=function(t,e,n,r,i,o,s){void 0===n&&(n=ot(r));var a=(0===e?n:n>>>e)&mn,u=i===yn,c=this.nodes,h=c[a];if(u&&!h)return this;var l=wt(h,t,e+fn,n,r,i,o,s);if(l===h)return this;var p=this.count;if(h){if(!l&&--p<Kn)return Ft(t,c,p,a)}else p++;var f=t&&t===this.ownerID,d=Ot(c,a,l,f);return f?(this.count=p,this.nodes=d,this):new mt(t,p,d)},yt.prototype.get=function(t,e,n,r){for(var i=this.entries,o=0,s=i.length;o<s;o++)if(H(n,i[o][0]))return i[o][1];return r},yt.prototype.update=function(t,e,n,r,i,o,s){void 0===n&&(n=ot(r));var a=i===yn;if(n!==this.keyHash)return a?this:(l(s),l(o),_t(this,t,e,n,[r,i]));for(var u=this.entries,c=0,h=u.length;c<h&&!H(r,u[c][0]);c++);var p=c<h;if(p?u[c][1]===i:a)return this;if(l(s),(a||!p)&&l(o),a&&2===h)return new vt(t,this.keyHash,u[1^c]);var d=t&&t===this.ownerID,m=d?u:f(u);return p?a?c===h-1?m.pop():m[c]=m.pop():m[c]=[r,i]:m.push([r,i]),d?(this.entries=m,this):new yt(t,this.keyHash,m)},vt.prototype.get=function(t,e,n,r){return H(n,this.entry[0])?this.entry[1]:r},vt.prototype.update=function(t,e,n,r,i,o,s){var a=i===yn,u=H(r,this.entry[0]);return(u?i===this.entry[1]:a)?this:(l(s),a?void l(o):u?t&&t===this.ownerID?(this.entry[1]=i,this):new vt(t,this.keyHash,[r,i]):(l(o),_t(this,t,e,ot(r),[r,i])))},ft.prototype.iterate=yt.prototype.iterate=function(t,e){for(var n=this.entries,r=0,i=n.length-1;r<=i;r++)if(!1===t(n[e?i-r:r]))return!1},dt.prototype.iterate=mt.prototype.iterate=function(t,e){for(var n=this.nodes,r=0,i=n.length-1;r<=i;r++){var o=n[e?i-r:r];if(o&&!1===o.iterate(t,e))return!1}},vt.prototype.iterate=function(t,e){return t(this.entry)},t(xt,E),xt.prototype.next=function(){for(var t=this._type,e=this._stack;e;){var n,r=e.node,i=e.index++;if(r.entry){if(0===i)return gt(t,r.entry)}else if(r.entries){if(n=r.entries.length-1,i<=n)return gt(t,r.entries[this._reverse?n-i:i])}else if(n=r.nodes.length-1,i<=n){var o=r.nodes[this._reverse?n-i:i];if(o){if(o.entry)return gt(t,o.entry);e=this._stack=Dt(o,e)}continue}e=this._stack=this._stack.__prev}return S()};var Jn,Xn=dn/4,qn=dn/2,Kn=dn/4;t(jt,nt),jt.of=function(){return this(arguments)},jt.prototype.toString=function(){return this.__toString("List [","]")},jt.prototype.get=function(t,e){if((t=m(this,t))>=0&&t<this.size){t+=this._origin;var n=Gt(this,t);return n&&n.array[t&mn]}return e},jt.prototype.set=function(t,e){return Kt(this,t,e)},jt.prototype.remove=function(t){return this.has(t)?0===t?this.shift():t===this.size-1?this.pop():this.splice(t,1):this},jt.prototype.insert=function(t,e){return this.splice(t,0,e)},jt.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=this._origin=this._capacity=0,this._level=fn,this._root=this._tail=null,this.__hash=void 0,this.__altered=!0,this):qt()},jt.prototype.push=function(){var t=arguments,e=this.size;return this.withMutations(function(n){Ht(n,0,e+t.length);for(var r=0;r<t.length;r++)n.set(e+r,t[r])})},jt.prototype.pop=function(){return Ht(this,0,-1)},jt.prototype.unshift=function(){var t=arguments;return this.withMutations(function(e){Ht(e,-t.length);for(var n=0;n<t.length;n++)e.set(n,t[n])})},jt.prototype.shift=function(){return Ht(this,1)},jt.prototype.merge=function(){return Vt(this,void 0,arguments)},jt.prototype.mergeWith=function(t){return Vt(this,t,un.call(arguments,1))},jt.prototype.mergeDeep=function(){return Vt(this,Tt,arguments)},jt.prototype.mergeDeepWith=function(t){var e=un.call(arguments,1);return Vt(this,Bt(t),e)},jt.prototype.setSize=function(t){return Ht(this,0,t)},jt.prototype.slice=function(t,e){var n=this.size;return v(t,e,n)?this:Ht(this,x(t,n),g(e,n))},jt.prototype.__iterator=function(t,e){var n=0,r=Jt(this,e);return new E(function(){var e=r();return e===Hn?S():A(t,n++,e)})},jt.prototype.__iterate=function(t,e){for(var n,r=0,i=Jt(this,e);(n=i())!==Hn&&!1!==t(n,r++,this););return r},jt.prototype.__ensureOwner=function(t){return t===this.__ownerID?this:t?Xt(this._origin,this._capacity,this._level,this._root,this._tail,t,this.__hash):(this.__ownerID=t,this)},jt.isList=Lt;var Yn="@@__IMMUTABLE_LIST__@@",Wn=jt.prototype;Wn[Yn]=!0,Wn.delete=Wn.remove,Wn.setIn=zn.setIn,Wn.deleteIn=Wn.removeIn=zn.removeIn,Wn.update=zn.update,Wn.updateIn=zn.updateIn,Wn.mergeIn=zn.mergeIn,Wn.mergeDeepIn=zn.mergeDeepIn,Wn.withMutations=zn.withMutations,Wn.asMutable=zn.asMutable,Wn.asImmutable=zn.asImmutable,Wn.wasAltered=zn.wasAltered,zt.prototype.removeBefore=function(t,e,n){if(n===e?1<<e:0===this.array.length)return this;var r=n>>>e&mn;if(r>=this.array.length)return new zt([],t);var i,o=0===r;if(e>0){var s=this.array[r];if((i=s&&s.removeBefore(t,e-fn,n))===s&&o)return this}if(o&&!i)return this;var a=Wt(this,t);if(!o)for(var u=0;u<r;u++)a.array[u]=void 0;return i&&(a.array[r]=i),a},zt.prototype.removeAfter=function(t,e,n){if(n===(e?1<<e:0)||0===this.array.length)return this;var r=n-1>>>e&mn;if(r>=this.array.length)return this;var i;if(e>0){var o=this.array[r];if((i=o&&o.removeAfter(t,e-fn,n))===o&&r===this.array.length-1)return this}var s=Wt(this,t);return s.array.splice(r+1),i&&(s.array[r]=i),s};var Gn,Hn={};t(Zt,lt),Zt.of=function(){return this(arguments)},Zt.prototype.toString=function(){return this.__toString("OrderedMap {","}")},Zt.prototype.get=function(t,e){var n=this._map.get(t);return void 0!==n?this._list.get(n)[1]:e},Zt.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._map.clear(),this._list.clear(),this):ee()},Zt.prototype.set=function(t,e){return ne(this,t,e)},Zt.prototype.remove=function(t){return ne(this,t,yn)},Zt.prototype.wasAltered=function(){return this._map.wasAltered()||this._list.wasAltered()},Zt.prototype.__iterate=function(t,e){var n=this;return this._list.__iterate(function(e){return e&&t(e[1],e[0],n)},e)},Zt.prototype.__iterator=function(t,e){return this._list.fromEntrySeq().__iterator(t,e)},Zt.prototype.__ensureOwner=function(t){if(t===this.__ownerID)return this;var e=this._map.__ensureOwner(t),n=this._list.__ensureOwner(t);return t?te(e,n,t,this.__hash):(this.__ownerID=t,this._map=e,this._list=n,this)},Zt.isOrderedMap=Qt,Zt.prototype[pn]=!0,Zt.prototype.delete=Zt.prototype.remove;var Vn;t(re,I),re.prototype.get=function(t,e){return this._iter.get(t,e)},re.prototype.has=function(t){return this._iter.has(t)},re.prototype.valueSeq=function(){return this._iter.valueSeq()},re.prototype.reverse=function(){var t=this,e=ce(this,!0);return this._useKeys||(e.valueSeq=function(){return t._iter.toSeq().reverse()}),e},re.prototype.map=function(t,e){var n=this,r=ue(this,t,e);return this._useKeys||(r.valueSeq=function(){return n._iter.toSeq().map(t,e)}),r},re.prototype.__iterate=function(t,e){var n,r=this;return this._iter.__iterate(this._useKeys?function(e,n){return t(e,n,r)}:(n=e?_e(this):0,function(i){return t(i,e?--n:n++,r)}),e)},re.prototype.__iterator=function(t,e){if(this._useKeys)return this._iter.__iterator(t,e);var n=this._iter.__iterator(Dn,e),r=e?_e(this):0;return new E(function(){var i=n.next();return i.done?i:A(t,e?--r:r++,i.value,i)})},re.prototype[pn]=!0,t(ie,T),ie.prototype.includes=function(t){return this._iter.includes(t)},ie.prototype.__iterate=function(t,e){var n=this,r=0;return this._iter.__iterate(function(e){return t(e,r++,n)},e)},ie.prototype.__iterator=function(t,e){var n=this._iter.__iterator(Dn,e),r=0;return new E(function(){var e=n.next();return e.done?e:A(t,r++,e.value,e)})},t(oe,B),oe.prototype.has=function(t){return this._iter.includes(t)},oe.prototype.__iterate=function(t,e){var n=this;return this._iter.__iterate(function(e){return t(e,e,n)},e)},oe.prototype.__iterator=function(t,e){var n=this._iter.__iterator(Dn,e);return new E(function(){var e=n.next();return e.done?e:A(t,e.value,e.value,e)})},t(se,I),se.prototype.entrySeq=function(){return this._iter.toSeq()},se.prototype.__iterate=function(t,e){var n=this;return this._iter.__iterate(function(e){if(e){Ce(e);var r=o(e);return t(r?e.get(1):e[1],r?e.get(0):e[0],n)}},e)},se.prototype.__iterator=function(t,e){var n=this._iter.__iterator(Dn,e);return new E(function(){for(;;){var e=n.next();if(e.done)return e;var r=e.value;if(r){Ce(r);var i=o(r);return A(t,i?r.get(0):r[0],i?r.get(1):r[1],e)}}})},ie.prototype.cacheResult=re.prototype.cacheResult=oe.prototype.cacheResult=se.prototype.cacheResult=ke,t(Be,et),Be.prototype.toString=function(){return this.__toString(Pe(this)+" {","}")},Be.prototype.has=function(t){return this._defaultValues.hasOwnProperty(t)},Be.prototype.get=function(t,e){if(!this.has(t))return e;var n=this._defaultValues[t];return this._map?this._map.get(t,n):n},Be.prototype.clear=function(){if(this.__ownerID)return this._map&&this._map.clear(),this;var t=this.constructor;return t._empty||(t._empty=Me(this,At()))},Be.prototype.set=function(t,e){if(!this.has(t))throw new Error('Cannot set unknown key "'+t+'" on '+Pe(this));if(this._map&&!this._map.has(t)){if(e===this._defaultValues[t])return this}var n=this._map&&this._map.set(t,e);return this.__ownerID||n===this._map?this:Me(this,n)},Be.prototype.remove=function(t){if(!this.has(t))return this;var e=this._map&&this._map.remove(t);return this.__ownerID||e===this._map?this:Me(this,e)},Be.prototype.wasAltered=function(){return this._map.wasAltered()},Be.prototype.__iterator=function(t,e){var r=this;return n(this._defaultValues).map(function(t,e){return r.get(e)}).__iterator(t,e)},Be.prototype.__iterate=function(t,e){var r=this;return n(this._defaultValues).map(function(t,e){return r.get(e)}).__iterate(t,e)},Be.prototype.__ensureOwner=function(t){if(t===this.__ownerID)return this;var e=this._map&&this._map.__ensureOwner(t);return t?Me(this,e,t):(this.__ownerID=t,this._map=e,this)};var $n=Be.prototype;$n.delete=$n.remove,$n.deleteIn=$n.removeIn=zn.removeIn,$n.merge=zn.merge,$n.mergeWith=zn.mergeWith,$n.mergeIn=zn.mergeIn,$n.mergeDeep=zn.mergeDeep,$n.mergeDeepWith=zn.mergeDeepWith,$n.mergeDeepIn=zn.mergeDeepIn,$n.setIn=zn.setIn,$n.update=zn.update,$n.updateIn=zn.updateIn,$n.withMutations=zn.withMutations,$n.asMutable=zn.asMutable,$n.asImmutable=zn.asImmutable,t(Re,rt),Re.of=function(){return this(arguments)},Re.fromKeys=function(t){return this(n(t).keySeq())},Re.prototype.toString=function(){return this.__toString("Set {","}")},Re.prototype.has=function(t){return this._map.has(t)},Re.prototype.add=function(t){return je(this,this._map.set(t,!0))},Re.prototype.remove=function(t){return je(this,this._map.remove(t))},Re.prototype.clear=function(){return je(this,this._map.clear())},Re.prototype.union=function(){var t=un.call(arguments,0);return t=t.filter(function(t){return 0!==t.size}),0===t.length?this:0!==this.size||this.__ownerID||1!==t.length?this.withMutations(function(e){for(var n=0;n<t.length;n++)i(t[n]).forEach(function(t){return e.add(t)})}):this.constructor(t[0])},Re.prototype.intersect=function(){var t=un.call(arguments,0);if(0===t.length)return this;t=t.map(function(t){return i(t)});var e=this;return this.withMutations(function(n){e.forEach(function(e){t.every(function(t){return t.includes(e)})||n.remove(e)})})},Re.prototype.subtract=function(){var t=un.call(arguments,0);if(0===t.length)return this;t=t.map(function(t){return i(t)});var e=this;return this.withMutations(function(n){e.forEach(function(e){t.some(function(t){return t.includes(e)})&&n.remove(e)})})},Re.prototype.merge=function(){return this.union.apply(this,arguments)},Re.prototype.mergeWith=function(t){var e=un.call(arguments,1);return this.union.apply(this,e)},Re.prototype.sort=function(t){return Je(De(this,t))},Re.prototype.sortBy=function(t,e){return Je(De(this,e,t))},Re.prototype.wasAltered=function(){return this._map.wasAltered()},Re.prototype.__iterate=function(t,e){var n=this;return this._map.__iterate(function(e,r){return t(r,r,n)},e)},Re.prototype.__iterator=function(t,e){return this._map.map(function(t,e){return e}).__iterator(t,e)},Re.prototype.__ensureOwner=function(t){if(t===this.__ownerID)return this;var e=this._map.__ensureOwner(t);return t?this.__make(e,t):(this.__ownerID=t,this._map=e,this)},Re.isSet=Ue;var Zn="@@__IMMUTABLE_SET__@@",Qn=Re.prototype;Qn[Zn]=!0,Qn.delete=Qn.remove,Qn.mergeDeep=Qn.merge,Qn.mergeDeepWith=Qn.mergeWith,Qn.withMutations=zn.withMutations,Qn.asMutable=zn.asMutable,Qn.asImmutable=zn.asImmutable,Qn.__empty=ze,Qn.__make=Le;var tr;t(Je,Re),Je.of=function(){return this(arguments)},Je.fromKeys=function(t){return this(n(t).keySeq())},Je.prototype.toString=function(){return this.__toString("OrderedSet {","}")},Je.isOrderedSet=Xe;var er=Je.prototype;er[pn]=!0,er.__empty=Ke,er.__make=qe;var nr;t(Ye,nt),Ye.of=function(){return this(arguments)},Ye.prototype.toString=function(){return this.__toString("Stack [","]")},Ye.prototype.get=function(t,e){var n=this._head;for(t=m(this,t);n&&t--;)n=n.next;return n?n.value:e},Ye.prototype.peek=function(){return this._head&&this._head.value},Ye.prototype.push=function(){if(0===arguments.length)return this;for(var t=this.size+arguments.length,e=this._head,n=arguments.length-1;n>=0;n--)e={value:arguments[n],next:e};return this.__ownerID?(this.size=t,this._head=e,this.__hash=void 0,this.__altered=!0,this):Ge(t,e)},Ye.prototype.pushAll=function(t){if(t=r(t),0===t.size)return this;ht(t.size);var e=this.size,n=this._head;return t.reverse().forEach(function(t){e++,n={value:t,next:n}}),this.__ownerID?(this.size=e,this._head=n,this.__hash=void 0,this.__altered=!0,this):Ge(e,n)},Ye.prototype.pop=function(){return this.slice(1)},Ye.prototype.unshift=function(){return this.push.apply(this,arguments)},Ye.prototype.unshiftAll=function(t){return this.pushAll(t)},Ye.prototype.shift=function(){return this.pop.apply(this,arguments)},Ye.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._head=void 0,this.__hash=void 0,this.__altered=!0,this):He()},Ye.prototype.slice=function(t,e){if(v(t,e,this.size))return this;var n=x(t,this.size);if(g(e,this.size)!==this.size)return nt.prototype.slice.call(this,t,e);for(var r=this.size-n,i=this._head;n--;)i=i.next;return this.__ownerID?(this.size=r,this._head=i,this.__hash=void 0,this.__altered=!0,this):Ge(r,i)},Ye.prototype.__ensureOwner=function(t){return t===this.__ownerID?this:t?Ge(this.size,this._head,t,this.__hash):(this.__ownerID=t,this.__altered=!1,this)},Ye.prototype.__iterate=function(t,e){if(e)return this.reverse().__iterate(t);for(var n=0,r=this._head;r&&!1!==t(r.value,n++,this);)r=r.next;return n},Ye.prototype.__iterator=function(t,e){if(e)return this.reverse().__iterator(t);var n=0,r=this._head;return new E(function(){if(r){var e=r.value;return r=r.next,A(t,n++,e)}return S()})},Ye.isStack=We;var rr="@@__IMMUTABLE_STACK__@@",ir=Ye.prototype;ir[rr]=!0,ir.withMutations=zn.withMutations,ir.asMutable=zn.asMutable,ir.asImmutable=zn.asImmutable,ir.wasAltered=zn.wasAltered;var or;e.Iterator=E,Ve(e,{toArray:function(){ht(this.size);var t=new Array(this.size||0);return this.valueSeq().__iterate(function(e,n){t[n]=e}),t},toIndexedSeq:function(){return new ie(this)},toJS:function(){return this.toSeq().map(function(t){return t&&"function"==typeof t.toJS?t.toJS():t}).__toJS()},toJSON:function(){return this.toSeq().map(function(t){return t&&"function"==typeof t.toJSON?t.toJSON():t}).__toJS()},toKeyedSeq:function(){return new re(this,!0)},toMap:function(){return lt(this.toKeyedSeq())},toObject:function(){ht(this.size);var t={};return this.__iterate(function(e,n){t[n]=e}),t},toOrderedMap:function(){return Zt(this.toKeyedSeq())},toOrderedSet:function(){return Je(s(this)?this.valueSeq():this)},toSet:function(){return Re(s(this)?this.valueSeq():this)},toSetSeq:function(){return new oe(this)},toSeq:function(){return a(this)?this.toIndexedSeq():s(this)?this.toKeyedSeq():this.toSetSeq()},toStack:function(){return Ye(s(this)?this.valueSeq():this)},toList:function(){return jt(s(this)?this.valueSeq():this)},toString:function(){return"[Iterable]"},__toString:function(t,e){return 0===this.size?t+e:t+" "+this.toSeq().map(this.__toStringMapper).join(", ")+" "+e},concat:function(){return we(this,ye(this,un.call(arguments,0)))},includes:function(t){return this.some(function(e){return H(e,t)})},entries:function(){return this.__iterator(En)},every:function(t,e){ht(this.size);var n=!0;return this.__iterate(function(r,i,o){if(!t.call(e,r,i,o))return n=!1,!1}),n},filter:function(t,e){return we(this,he(this,t,e,!0))},find:function(t,e,n){var r=this.findEntry(t,e);return r?r[1]:n},forEach:function(t,e){return ht(this.size),this.__iterate(e?t.bind(e):t)},join:function(t){ht(this.size),t=void 0!==t?""+t:",";var e="",n=!0;return this.__iterate(function(r){n?n=!1:e+=t,e+=null!==r&&void 0!==r?r.toString():""}),e},keys:function(){return this.__iterator(gn)},map:function(t,e){return we(this,ue(this,t,e))},reduce:function(t,e,n){ht(this.size);var r,i;return arguments.length<2?i=!0:r=e,this.__iterate(function(e,o,s){i?(i=!1,r=e):r=t.call(n,r,e,o,s)}),r},reduceRight:function(t,e,n){var r=this.toKeyedSeq().reverse();return r.reduce.apply(r,arguments)},reverse:function(){return we(this,ce(this,!0))},slice:function(t,e){return we(this,fe(this,t,e,!0))},some:function(t,e){return!this.every(Qe(t),e)},sort:function(t){return we(this,De(this,t))},values:function(){return this.__iterator(Dn)},butLast:function(){return this.slice(0,-1)},isEmpty:function(){return void 0!==this.size?0===this.size:!this.some(function(){return!0})},count:function(t,e){return d(t?this.toSeq().filter(t,e):this)},countBy:function(t,e){return le(this,t,e)},equals:function(t){return V(this,t)},entrySeq:function(){var t=this;if(t._cache)return new M(t._cache);var e=t.toSeq().map(Ze).toIndexedSeq();return e.fromEntrySeq=function(){return t.toSeq()},e},filterNot:function(t,e){return this.filter(Qe(t),e)},findEntry:function(t,e,n){var r=n;return this.__iterate(function(n,i,o){if(t.call(e,n,i,o))return r=[i,n],!1}),r},findKey:function(t,e){var n=this.findEntry(t,e);return n&&n[0]},findLast:function(t,e,n){return this.toKeyedSeq().reverse().find(t,e,n)},findLastEntry:function(t,e,n){return this.toKeyedSeq().reverse().findEntry(t,e,n)},findLastKey:function(t,e){return this.toKeyedSeq().reverse().findKey(t,e)},first:function(){return this.find(y)},flatMap:function(t,e){return we(this,xe(this,t,e))},flatten:function(t){return we(this,ve(this,t,!0))},fromEntrySeq:function(){return new se(this)},get:function(t,e){return this.find(function(e,n){return H(n,t)},void 0,e)},getIn:function(t,e){for(var n,r=this,i=Te(t);!(n=i.next()).done;){var o=n.value;if((r=r&&r.get?r.get(o,yn):yn)===yn)return e}return r},groupBy:function(t,e){return pe(this,t,e)},has:function(t){return this.get(t,yn)!==yn},hasIn:function(t){return this.getIn(t,yn)!==yn},isSubset:function(t){return t="function"==typeof t.includes?t:e(t),this.every(function(e){return t.includes(e)})},isSuperset:function(t){return t="function"==typeof t.isSubset?t:e(t),t.isSubset(this)},keyOf:function(t){return this.findKey(function(e){return H(e,t)})},keySeq:function(){return this.toSeq().map($e).toIndexedSeq()},last:function(){return this.toSeq().reverse().first()},lastKeyOf:function(t){return this.toKeyedSeq().reverse().keyOf(t)},max:function(t){return Ee(this,t)},maxBy:function(t,e){return Ee(this,e,t)},min:function(t){return Ee(this,t?tn(t):rn)},minBy:function(t,e){return Ee(this,e?tn(e):rn,t)},rest:function(){return this.slice(1)},skip:function(t){return this.slice(Math.max(0,t))},skipLast:function(t){return we(this,this.toSeq().reverse().skip(t).reverse())},skipWhile:function(t,e){return we(this,me(this,t,e,!0))},skipUntil:function(t,e){return this.skipWhile(Qe(t),e)},sortBy:function(t,e){return we(this,De(this,e,t))},take:function(t){return this.slice(0,Math.max(0,t))},takeLast:function(t){return we(this,this.toSeq().reverse().take(t).reverse())},takeWhile:function(t,e){return we(this,de(this,t,e))},takeUntil:function(t,e){return this.takeWhile(Qe(t),e)},valueSeq:function(){return this.toIndexedSeq()},hashCode:function(){return this.__hash||(this.__hash=on(this))}});var sr=e.prototype;sr[cn]=!0,sr[wn]=sr.values,sr.__toJS=sr.toArray,sr.__toStringMapper=en,sr.inspect=sr.toSource=function(){return this.toString()},sr.chain=sr.flatMap,sr.contains=sr.includes,Ve(n,{flip:function(){return we(this,ae(this))},mapEntries:function(t,e){var n=this,r=0;return we(this,this.toSeq().map(function(i,o){return t.call(e,[o,i],r++,n)}).fromEntrySeq())},mapKeys:function(t,e){var n=this;return we(this,this.toSeq().flip().map(function(r,i){return t.call(e,r,i,n)}).flip())}});var ar=n.prototype;return ar[hn]=!0,ar[wn]=sr.entries,ar.__toJS=sr.toObject,ar.__toStringMapper=function(t,e){return JSON.stringify(e)+": "+en(t)},Ve(r,{toKeyedSeq:function(){return new re(this,!1)},filter:function(t,e){return we(this,he(this,t,e,!1))},findIndex:function(t,e){var n=this.findEntry(t,e);return n?n[0]:-1},indexOf:function(t){var e=this.keyOf(t);return void 0===e?-1:e},lastIndexOf:function(t){var e=this.lastKeyOf(t);return void 0===e?-1:e},reverse:function(){return we(this,ce(this,!1))},slice:function(t,e){return we(this,fe(this,t,e,!1))},splice:function(t,e){var n=arguments.length;if(e=Math.max(0|e,0),0===n||2===n&&!e)return this;t=x(t,t<0?this.count():this.size);var r=this.slice(0,t);return we(this,1===n?r:r.concat(f(arguments,2),this.slice(t+e)))},findLastIndex:function(t,e){var n=this.findLastEntry(t,e);return n?n[0]:-1},first:function(){return this.get(0)},flatten:function(t){return we(this,ve(this,t,!1))},get:function(t,e){return t=m(this,t),t<0||this.size===1/0||void 0!==this.size&&t>this.size?e:this.find(function(e,n){return n===t},void 0,e)},has:function(t){return(t=m(this,t))>=0&&(void 0!==this.size?this.size===1/0||t<this.size:-1!==this.indexOf(t))},interpose:function(t){return we(this,ge(this,t))},interleave:function(){var t=[this].concat(f(arguments)),e=Se(this.toSeq(),T.of,t),n=e.flatten(!0);return e.size&&(n.size=e.size*t.length),we(this,n)},keySeq:function(){return Q(0,this.size)},last:function(){return this.get(-1)},skipWhile:function(t,e){return we(this,me(this,t,e,!1))},zip:function(){return we(this,Se(this,nn,[this].concat(f(arguments))))},zipWith:function(t){var e=f(arguments);return e[0]=this,we(this,Se(this,t,e))}}),r.prototype[ln]=!0,r.prototype[pn]=!0,Ve(i,{get:function(t,e){return this.has(t)?t:e},includes:function(t){return this.has(t)},keySeq:function(){return this.valueSeq()}}),i.prototype.has=sr.includes,i.prototype.contains=i.prototype.includes,Ve(I,n.prototype),Ve(T,r.prototype),Ve(B,i.prototype),Ve(et,n.prototype),Ve(nt,r.prototype),Ve(rt,i.prototype),{Iterable:e,Seq:k,Collection:tt,Map:lt,OrderedMap:Zt,List:jt,Stack:Ye,Set:Re,OrderedSet:Je,Record:Be,Range:Q,Repeat:$,is:H,fromJS:K}})},function(t,e){var n={}.toString;t.exports=Array.isArray||function(t){return"[object Array]"==n.call(t)}},function(t,e,n){"use strict";var r=n(236);t.exports=r},function(t,e,n){"use strict";function r(t){return function(){throw new Error("Function "+t+" is deprecated and cannot be used.")}}var i=n(238),o=n(237);t.exports.Type=n(0),t.exports.Schema=n(24),t.exports.FAILSAFE_SCHEMA=n(71),t.exports.JSON_SCHEMA=n(111),t.exports.CORE_SCHEMA=n(110),t.exports.DEFAULT_SAFE_SCHEMA=n(34),t.exports.DEFAULT_FULL_SCHEMA=n(47),t.exports.load=i.load,t.exports.loadAll=i.loadAll,t.exports.safeLoad=i.safeLoad,t.exports.safeLoadAll=i.safeLoadAll,t.exports.dump=o.dump,t.exports.safeDump=o.safeDump,t.exports.YAMLException=n(33),t.exports.MINIMAL_SCHEMA=n(71),t.exports.SAFE_SCHEMA=n(34),t.exports.DEFAULT_SCHEMA=n(47),t.exports.scan=r("scan"),t.exports.parse=r("parse"),t.exports.compose=r("compose"),t.exports.addConstructor=r("addConstructor")},function(t,e,n){"use strict";function r(t,e){var n,r,i,o,s,a,u;if(null===e)return{};for(n={},r=Object.keys(e),i=0,o=r.length;i<o;i+=1)s=r[i],a=String(e[s]),"!!"===s.slice(0,2)&&(s="tag:yaml.org,2002:"+s.slice(2)),u=t.compiledTypeMap.fallback[s],u&&N.call(u.styleAliases,a)&&(a=u.styleAliases[a]),n[s]=a;return n}function i(t){var e,n,r;if(e=t.toString(16).toUpperCase(),t<=255)n="x",r=2;else if(t<=65535)n="u",r=4;else{if(!(t<=4294967295))throw new T("code point within a string may not be greater than 0xFFFFFFFF");n="U",r=8}return"\\"+n+I.repeat("0",r-e.length)+e}function o(t){this.schema=t.schema||B,this.indent=Math.max(1,t.indent||2),this.skipInvalid=t.skipInvalid||!1,this.flowLevel=I.isNothing(t.flowLevel)?-1:t.flowLevel,this.styleMap=r(this.schema,t.styles||null),this.sortKeys=t.sortKeys||!1,this.lineWidth=t.lineWidth||80,this.noRefs=t.noRefs||!1,this.noCompatMode=t.noCompatMode||!1,this.condenseFlow=t.condenseFlow||!1,this.implicitTypes=this.schema.compiledImplicit,this.explicitTypes=this.schema.compiledExplicit,this.tag=null,this.result="",this.duplicates=[],this.usedDuplicates=null}function s(t,e){for(var n,r=I.repeat(" ",e),i=0,o=-1,s="",a=t.length;i<a;)o=t.indexOf("\n",i),-1===o?(n=t.slice(i),i=a):(n=t.slice(i,o+1),i=o+1),n.length&&"\n"!==n&&(s+=r),s+=n;return s}function a(t,e){return"\n"+I.repeat(" ",t.indent*e)}function u(t,e){var n,r,i;for(n=0,r=t.implicitTypes.length;n<r;n+=1)if(i=t.implicitTypes[n],i.resolve(e))return!0;return!1}function c(t){return t===U||t===O}function h(t){return 32<=t&&t<=126||161<=t&&t<=55295&&8232!==t&&8233!==t||57344<=t&&t<=65533&&65279!==t||65536<=t&&t<=1114111}function l(t){return h(t)&&65279!==t&&t!==Y&&t!==Z&&t!==Q&&t!==et&&t!==rt&&t!==G&&t!==z}function p(t){return h(t)&&65279!==t&&!c(t)&&t!==W&&t!==V&&t!==G&&t!==Y&&t!==Z&&t!==Q&&t!==et&&t!==rt&&t!==z&&t!==X&&t!==K&&t!==j&&t!==nt&&t!==H&&t!==q&&t!==L&&t!==J&&t!==$&&t!==tt}function f(t,e,n,r,i){var o,s,a=!1,u=!1,f=-1!==r,d=-1,m=p(t.charCodeAt(0))&&!c(t.charCodeAt(t.length-1));if(e)for(o=0;o<t.length;o++){if(s=t.charCodeAt(o),!h(s))return ht;m=m&&l(s)}else{for(o=0;o<t.length;o++){if((s=t.charCodeAt(o))===R)a=!0,f&&(u=u||o-d-1>r&&" "!==t[d+1],d=o);else if(!h(s))return ht;m=m&&l(s)}u=u||f&&o-d-1>r&&" "!==t[d+1]}return a||u?" "===t[0]&&n>9?ht:u?ct:ut:m&&!i(t)?st:at}function d(t,e,n,r){t.dump=function(){function i(e){return u(t,e)}if(0===e.length)return"''";if(!t.noCompatMode&&-1!==ot.indexOf(e))return"'"+e+"'";var o=t.indent*Math.max(1,n),a=-1===t.lineWidth?-1:Math.max(Math.min(t.lineWidth,40),t.lineWidth-o),c=r||t.flowLevel>-1&&n>=t.flowLevel;switch(f(e,c,t.indent,a,i)){case st:return e;case at:return"'"+e.replace(/'/g,"''")+"'";case ut:return"|"+m(e,t.indent)+y(s(e,o));case ct:return">"+m(e,t.indent)+y(s(v(e,a),o));case ht:return'"'+g(e)+'"';default:throw new T("impossible error: invalid scalar style")}}()}function m(t,e){var n=" "===t[0]?String(e):"",r="\n"===t[t.length-1];return n+(!r||"\n"!==t[t.length-2]&&"\n"!==t?r?"":"-":"+")+"\n"}function y(t){return"\n"===t[t.length-1]?t.slice(0,-1):t}function v(t,e){for(var n,r,i=/(\n+)([^\n]*)/g,o=function(){var n=t.indexOf("\n");return n=-1!==n?n:t.length,i.lastIndex=n,x(t.slice(0,n),e)}(),s="\n"===t[0]||" "===t[0];r=i.exec(t);){var a=r[1],u=r[2];n=" "===u[0],o+=a+(s||n||""===u?"":"\n")+x(u,e),s=n}return o}function x(t,e){if(""===t||" "===t[0])return t;for(var n,r,i=/ [^ ]/g,o=0,s=0,a=0,u="";n=i.exec(t);)a=n.index,a-o>e&&(r=s>o?s:a,u+="\n"+t.slice(o,r),o=r+1),s=a;return u+="\n",t.length-o>e&&s>o?u+=t.slice(o,s)+"\n"+t.slice(s+1):u+=t.slice(o),u.slice(1)}function g(t){for(var e,n,r,o="",s=0;s<t.length;s++)e=t.charCodeAt(s),e>=55296&&e<=56319&&(n=t.charCodeAt(s+1))>=56320&&n<=57343?(o+=i(1024*(e-55296)+n-56320+65536),s++):(r=it[e],o+=!r&&h(e)?t[s]:r||i(e));return o}function D(t,e,n){var r,i,o="",s=t.tag;for(r=0,i=n.length;r<i;r+=1)C(t,e,n[r],!1,!1)&&(0!==r&&(o+=","+(t.condenseFlow?"":" ")),o+=t.dump);t.tag=s,t.dump="["+o+"]"}function E(t,e,n,r){var i,o,s="",u=t.tag;for(i=0,o=n.length;i<o;i+=1)C(t,e+1,n[i],!0,!0)&&(r&&0===i||(s+=a(t,e)),t.dump&&R===t.dump.charCodeAt(0)?s+="-":s+="- ",s+=t.dump);t.tag=u,t.dump=s||"[]"}function A(t,e,n){var r,i,o,s,a,u="",c=t.tag,h=Object.keys(n);for(r=0,i=h.length;r<i;r+=1)a=t.condenseFlow?'"':"",0!==r&&(a+=", "),o=h[r],s=n[o],C(t,e,o,!1,!1)&&(t.dump.length>1024&&(a+="? "),a+=t.dump+(t.condenseFlow?'"':"")+":"+(t.condenseFlow?"":" "),C(t,e,s,!1,!1)&&(a+=t.dump,u+=a));t.tag=c,t.dump="{"+u+"}"}function S(t,e,n,r){var i,o,s,u,c,h,l="",p=t.tag,f=Object.keys(n);if(!0===t.sortKeys)f.sort();else if("function"==typeof t.sortKeys)f.sort(t.sortKeys);else if(t.sortKeys)throw new T("sortKeys must be a boolean or a function");for(i=0,o=f.length;i<o;i+=1)h="",r&&0===i||(h+=a(t,e)),s=f[i],u=n[s],C(t,e+1,s,!0,!0,!0)&&(c=null!==t.tag&&"?"!==t.tag||t.dump&&t.dump.length>1024,c&&(t.dump&&R===t.dump.charCodeAt(0)?h+="?":h+="? "),h+=t.dump,c&&(h+=a(t,e)),C(t,e+1,u,!0,c)&&(t.dump&&R===t.dump.charCodeAt(0)?h+=":":h+=": ",h+=t.dump,l+=h));t.tag=p,t.dump=l||"{}"}function w(t,e,n){var r,i,o,s,a,u;for(i=n?t.explicitTypes:t.implicitTypes,o=0,s=i.length;o<s;o+=1)if(a=i[o],(a.instanceOf||a.predicate)&&(!a.instanceOf||"object"==typeof e&&e instanceof a.instanceOf)&&(!a.predicate||a.predicate(e))){if(t.tag=n?a.tag:"?",a.represent){if(u=t.styleMap[a.tag]||a.defaultStyle,"[object Function]"===P.call(a.represent))r=a.represent(e,u);else{if(!N.call(a.represent,u))throw new T("!<"+a.tag+'> tag resolver accepts not "'+u+'" style');r=a.represent[u](e,u)}t.dump=r}return!0}return!1}function C(t,e,n,r,i,o){t.tag=null,t.dump=n,w(t,n,!1)||w(t,n,!0);var s=P.call(t.dump);r&&(r=t.flowLevel<0||t.flowLevel>e);var a,u,c="[object Object]"===s||"[object Array]"===s;if(c&&(a=t.duplicates.indexOf(n),u=-1!==a),(null!==t.tag&&"?"!==t.tag||u||2!==t.indent&&e>0)&&(i=!1),u&&t.usedDuplicates[a])t.dump="*ref_"+a;else{if(c&&u&&!t.usedDuplicates[a]&&(t.usedDuplicates[a]=!0),"[object Object]"===s)r&&0!==Object.keys(t.dump).length?(S(t,e,t.dump,i),u&&(t.dump="&ref_"+a+t.dump)):(A(t,e,t.dump),u&&(t.dump="&ref_"+a+" "+t.dump));else if("[object Array]"===s)r&&0!==t.dump.length?(E(t,e,t.dump,i),u&&(t.dump="&ref_"+a+t.dump)):(D(t,e,t.dump),u&&(t.dump="&ref_"+a+" "+t.dump));else{if("[object String]"!==s){if(t.skipInvalid)return!1;throw new T("unacceptable kind of an object to dump "+s)}"?"!==t.tag&&d(t,t.dump,e,o)}null!==t.tag&&"?"!==t.tag&&(t.dump="!<"+t.tag+"> "+t.dump)}return!0}function _(t,e){var n,r,i=[],o=[];for(b(t,i,o),n=0,r=o.length;n<r;n+=1)e.duplicates.push(i[o[n]]);e.usedDuplicates=new Array(r)}function b(t,e,n){var r,i,o;if(null!==t&&"object"==typeof t)if(-1!==(i=e.indexOf(t)))-1===n.indexOf(i)&&n.push(i);else if(e.push(t),Array.isArray(t))for(i=0,o=t.length;i<o;i+=1)b(t[i],e,n);else for(r=Object.keys(t),i=0,o=r.length;i<o;i+=1)b(t[r[i]],e,n)}function F(t,e){e=e||{};var n=new o(e);return n.noRefs||_(t,n),C(n,0,t,!0,!0)?n.dump+"\n":""}function k(t,e){return F(t,I.extend({schema:M},e))}var I=n(23),T=n(33),B=n(47),M=n(34),P=Object.prototype.toString,N=Object.prototype.hasOwnProperty,O=9,R=10,U=32,j=33,L=34,z=35,J=37,X=38,q=39,K=42,Y=44,W=45,G=58,H=62,V=63,$=64,Z=91,Q=93,tt=96,et=123,nt=124,rt=125,it={};it[0]="\\0",it[7]="\\a",it[8]="\\b",it[9]="\\t",it[10]="\\n",it[11]="\\v",it[12]="\\f",it[13]="\\r",it[27]="\\e",it[34]='\\"',it[92]="\\\\",it[133]="\\N",it[160]="\\_",it[8232]="\\L",it[8233]="\\P";var ot=["y","Y","yes","Yes","YES","on","On","ON","n","N","no","No","NO","off","Off","OFF"],st=1,at=2,ut=3,ct=4,ht=5;t.exports.dump=F,t.exports.safeDump=k},function(t,e,n){"use strict";function r(t){return 10===t||13===t}function i(t){return 9===t||32===t}function o(t){return 9===t||32===t||10===t||13===t}function s(t){return 44===t||91===t||93===t||123===t||125===t}function a(t){var e;return 48<=t&&t<=57?t-48:(e=32|t,97<=e&&e<=102?e-97+10:-1)}function u(t){return 120===t?2:117===t?4:85===t?8:0}function c(t){return 48<=t&&t<=57?t-48:-1}function h(t){return 48===t?"\0":97===t?"":98===t?"\b":116===t?"\t":9===t?"\t":110===t?"\n":118===t?"\v":102===t?"\f":114===t?"\r":101===t?"":32===t?" ":34===t?'"':47===t?"/":92===t?"\\":78===t?"…":95===t?" ":76===t?"\u2028":80===t?"\u2029":""}function l(t){return t<=65535?String.fromCharCode(t):String.fromCharCode(55296+(t-65536>>10),56320+(t-65536&1023))}function p(t,e){this.input=t,this.filename=e.filename||null,this.schema=e.schema||q,this.onWarning=e.onWarning||null,this.legacy=e.legacy||!1,this.json=e.json||!1,this.listener=e.listener||null,this.implicitTypes=this.schema.compiledImplicit,this.typeMap=this.schema.compiledTypeMap,this.length=t.length,this.position=0,this.line=0,this.lineStart=0,this.lineIndent=0,this.documents=[]}function f(t,e){return new z(e,new J(t.filename,t.input,t.position,t.line,t.position-t.lineStart))}function d(t,e){throw f(t,e)}function m(t,e){t.onWarning&&t.onWarning.call(null,f(t,e))}function y(t,e,n,r){var i,o,s,a;if(e<n){if(a=t.input.slice(e,n),r)for(i=0,o=a.length;i<o;i+=1)9===(s=a.charCodeAt(i))||32<=s&&s<=1114111||d(t,"expected valid JSON character");else Q.test(a)&&d(t,"the stream contains non-printable characters");t.result+=a}}function v(t,e,n,r){var i,o,s,a;for(L.isObject(n)||d(t,"cannot merge mappings; the provided source object is unacceptable"),i=Object.keys(n),s=0,a=i.length;s<a;s+=1)o=i[s],K.call(e,o)||(e[o]=n[o],r[o]=!0)}function x(t,e,n,r,i,o,s,a){var u,c;if(i=String(i),null===e&&(e={}),"tag:yaml.org,2002:merge"===r)if(Array.isArray(o))for(u=0,c=o.length;u<c;u+=1)v(t,e,o[u],n);else v(t,e,o,n);else t.json||K.call(n,i)||!K.call(e,i)||(t.line=s||t.line,t.position=a||t.position,d(t,"duplicated mapping key")),e[i]=o,delete n[i];return e}function g(t){var e;e=t.input.charCodeAt(t.position),10===e?t.position++:13===e?(t.position++,10===t.input.charCodeAt(t.position)&&t.position++):d(t,"a line break is expected"),t.line+=1,t.lineStart=t.position}function D(t,e,n){for(var o=0,s=t.input.charCodeAt(t.position);0!==s;){for(;i(s);)s=t.input.charCodeAt(++t.position);if(e&&35===s)do{s=t.input.charCodeAt(++t.position)}while(10!==s&&13!==s&&0!==s);if(!r(s))break;for(g(t),s=t.input.charCodeAt(t.position),o++,t.lineIndent=0;32===s;)t.lineIndent++,s=t.input.charCodeAt(++t.position)}return-1!==n&&0!==o&&t.lineIndent<n&&m(t,"deficient indentation"),o}function E(t){var e,n=t.position;return!(45!==(e=t.input.charCodeAt(n))&&46!==e||e!==t.input.charCodeAt(n+1)||e!==t.input.charCodeAt(n+2)||(n+=3,0!==(e=t.input.charCodeAt(n))&&!o(e)))}function A(t,e){1===e?t.result+=" ":e>1&&(t.result+=L.repeat("\n",e-1))}function S(t,e,n){var a,u,c,h,l,p,f,d,m,v=t.kind,x=t.result;if(m=t.input.charCodeAt(t.position),o(m)||s(m)||35===m||38===m||42===m||33===m||124===m||62===m||39===m||34===m||37===m||64===m||96===m)return!1;if((63===m||45===m)&&(u=t.input.charCodeAt(t.position+1),o(u)||n&&s(u)))return!1;for(t.kind="scalar",t.result="",c=h=t.position,l=!1;0!==m;){if(58===m){if(u=t.input.charCodeAt(t.position+1),o(u)||n&&s(u))break}else if(35===m){if(a=t.input.charCodeAt(t.position-1),o(a))break}else{if(t.position===t.lineStart&&E(t)||n&&s(m))break;if(r(m)){if(p=t.line,f=t.lineStart,d=t.lineIndent,D(t,!1,-1),t.lineIndent>=e){l=!0,m=t.input.charCodeAt(t.position);continue}t.position=h,t.line=p,t.lineStart=f,t.lineIndent=d;break}}l&&(y(t,c,h,!1),A(t,t.line-p),c=h=t.position,l=!1),i(m)||(h=t.position+1),m=t.input.charCodeAt(++t.position)}return y(t,c,h,!1),!!t.result||(t.kind=v,t.result=x,!1)}function w(t,e){var n,i,o;if(39!==(n=t.input.charCodeAt(t.position)))return!1;for(t.kind="scalar",t.result="",t.position++,i=o=t.position;0!==(n=t.input.charCodeAt(t.position));)if(39===n){if(y(t,i,t.position,!0),39!==(n=t.input.charCodeAt(++t.position)))return!0;i=t.position,t.position++,o=t.position}else r(n)?(y(t,i,o,!0),A(t,D(t,!1,e)),i=o=t.position):t.position===t.lineStart&&E(t)?d(t,"unexpected end of the document within a single quoted scalar"):(t.position++,o=t.position);d(t,"unexpected end of the stream within a single quoted scalar")}function C(t,e){var n,i,o,s,c,h;if(34!==(h=t.input.charCodeAt(t.position)))return!1;for(t.kind="scalar",t.result="",t.position++,n=i=t.position;0!==(h=t.input.charCodeAt(t.position));){if(34===h)return y(t,n,t.position,!0),t.position++,!0;if(92===h){if(y(t,n,t.position,!0),h=t.input.charCodeAt(++t.position),r(h))D(t,!1,e);else if(h<256&&it[h])t.result+=ot[h],t.position++;else if((c=u(h))>0){for(o=c,s=0;o>0;o--)h=t.input.charCodeAt(++t.position),(c=a(h))>=0?s=(s<<4)+c:d(t,"expected hexadecimal character");t.result+=l(s),t.position++}else d(t,"unknown escape sequence");n=i=t.position}else r(h)?(y(t,n,i,!0),A(t,D(t,!1,e)),n=i=t.position):t.position===t.lineStart&&E(t)?d(t,"unexpected end of the document within a double quoted scalar"):(t.position++,i=t.position)}d(t,"unexpected end of the stream within a double quoted scalar")}function _(t,e){var n,r,i,s,a,u,c,h,l,p,f,m=!0,y=t.tag,v=t.anchor,g={};if(91===(f=t.input.charCodeAt(t.position)))s=93,c=!1,r=[];else{if(123!==f)return!1;s=125,c=!0,r={}}for(null!==t.anchor&&(t.anchorMap[t.anchor]=r),f=t.input.charCodeAt(++t.position);0!==f;){if(D(t,!0,e),(f=t.input.charCodeAt(t.position))===s)return t.position++,t.tag=y,t.anchor=v,t.kind=c?"mapping":"sequence",t.result=r,!0;m||d(t,"missed comma between flow collection entries"),l=h=p=null,a=u=!1,63===f&&(i=t.input.charCodeAt(t.position+1),o(i)&&(a=u=!0,t.position++,D(t,!0,e))),n=t.line,M(t,e,Y,!1,!0),l=t.tag,h=t.result,D(t,!0,e),f=t.input.charCodeAt(t.position),!u&&t.line!==n||58!==f||(a=!0,f=t.input.charCodeAt(++t.position),D(t,!0,e),M(t,e,Y,!1,!0),p=t.result),c?x(t,r,g,l,h,p):a?r.push(x(t,null,g,l,h,p)):r.push(h),D(t,!0,e),f=t.input.charCodeAt(t.position),44===f?(m=!0,f=t.input.charCodeAt(++t.position)):m=!1}d(t,"unexpected end of the stream within a flow collection")}function b(t,e){var n,o,s,a,u=V,h=!1,l=!1,p=e,f=0,m=!1;if(124===(a=t.input.charCodeAt(t.position)))o=!1;else{if(62!==a)return!1;o=!0}for(t.kind="scalar",t.result="";0!==a;)if(43===(a=t.input.charCodeAt(++t.position))||45===a)V===u?u=43===a?Z:$:d(t,"repeat of a chomping mode identifier");else{if(!((s=c(a))>=0))break;0===s?d(t,"bad explicit indentation width of a block scalar; it cannot be less than one"):l?d(t,"repeat of an indentation width identifier"):(p=e+s-1,l=!0)}if(i(a)){do{a=t.input.charCodeAt(++t.position)}while(i(a));if(35===a)do{a=t.input.charCodeAt(++t.position)}while(!r(a)&&0!==a)}for(;0!==a;){for(g(t),t.lineIndent=0,a=t.input.charCodeAt(t.position);(!l||t.lineIndent<p)&&32===a;)t.lineIndent++,a=t.input.charCodeAt(++t.position);if(!l&&t.lineIndent>p&&(p=t.lineIndent),r(a))f++;else{if(t.lineIndent<p){u===Z?t.result+=L.repeat("\n",h?1+f:f):u===V&&h&&(t.result+="\n");break}for(o?i(a)?(m=!0,t.result+=L.repeat("\n",h?1+f:f)):m?(m=!1,t.result+=L.repeat("\n",f+1)):0===f?h&&(t.result+=" "):t.result+=L.repeat("\n",f):t.result+=L.repeat("\n",h?1+f:f),h=!0,l=!0,f=0,n=t.position;!r(a)&&0!==a;)a=t.input.charCodeAt(++t.position);y(t,n,t.position,!1)}}return!0}function F(t,e){var n,r,i,s=t.tag,a=t.anchor,u=[],c=!1;for(null!==t.anchor&&(t.anchorMap[t.anchor]=u),i=t.input.charCodeAt(t.position);0!==i&&45===i&&(r=t.input.charCodeAt(t.position+1),o(r));)if(c=!0,t.position++,D(t,!0,-1)&&t.lineIndent<=e)u.push(null),i=t.input.charCodeAt(t.position);else if(n=t.line,M(t,e,G,!1,!0),u.push(t.result),D(t,!0,-1),i=t.input.charCodeAt(t.position),(t.line===n||t.lineIndent>e)&&0!==i)d(t,"bad indentation of a sequence entry");else if(t.lineIndent<e)break;return!!c&&(t.tag=s,t.anchor=a,t.kind="sequence",t.result=u,!0)}function k(t,e,n){var r,s,a,u,c,h=t.tag,l=t.anchor,p={},f={},m=null,y=null,v=null,g=!1,E=!1;for(null!==t.anchor&&(t.anchorMap[t.anchor]=p),c=t.input.charCodeAt(t.position);0!==c;){if(r=t.input.charCodeAt(t.position+1),a=t.line,u=t.position,63!==c&&58!==c||!o(r)){if(!M(t,n,W,!1,!0))break;if(t.line===a){for(c=t.input.charCodeAt(t.position);i(c);)c=t.input.charCodeAt(++t.position);if(58===c)c=t.input.charCodeAt(++t.position),o(c)||d(t,"a whitespace character is expected after the key-value separator within a block mapping"),g&&(x(t,p,f,m,y,null),m=y=v=null),E=!0,g=!1,s=!1,m=t.tag,y=t.result;else{if(!E)return t.tag=h,t.anchor=l,!0;d(t,"can not read an implicit mapping pair; a colon is missed")}}else{if(!E)return t.tag=h,t.anchor=l,!0;d(t,"can not read a block mapping entry; a multiline key may not be an implicit key")}}else 63===c?(g&&(x(t,p,f,m,y,null),m=y=v=null),E=!0,g=!0,s=!0):g?(g=!1,s=!0):d(t,"incomplete explicit mapping pair; a key node is missed; or followed by a non-tabulated empty line"),t.position+=1,c=r;if((t.line===a||t.lineIndent>e)&&(M(t,e,H,!0,s)&&(g?y=t.result:v=t.result),g||(x(t,p,f,m,y,v,a,u),m=y=v=null),D(t,!0,-1),c=t.input.charCodeAt(t.position)),t.lineIndent>e&&0!==c)d(t,"bad indentation of a mapping entry");else if(t.lineIndent<e)break}return g&&x(t,p,f,m,y,null),E&&(t.tag=h,t.anchor=l,t.kind="mapping",t.result=p),E}function I(t){var e,n,r,i,s=!1,a=!1;if(33!==(i=t.input.charCodeAt(t.position)))return!1;if(null!==t.tag&&d(t,"duplication of a tag property"),i=t.input.charCodeAt(++t.position),60===i?(s=!0,i=t.input.charCodeAt(++t.position)):33===i?(a=!0,n="!!",i=t.input.charCodeAt(++t.position)):n="!",e=t.position,s){do{i=t.input.charCodeAt(++t.position)}while(0!==i&&62!==i);t.position<t.length?(r=t.input.slice(e,t.position),i=t.input.charCodeAt(++t.position)):d(t,"unexpected end of the stream within a verbatim tag")}else{for(;0!==i&&!o(i);)33===i&&(a?d(t,"tag suffix cannot contain exclamation marks"):(n=t.input.slice(e-1,t.position+1),nt.test(n)||d(t,"named tag handle cannot contain such characters"),a=!0,e=t.position+1)),i=t.input.charCodeAt(++t.position);r=t.input.slice(e,t.position),et.test(r)&&d(t,"tag suffix cannot contain flow indicator characters")}return r&&!rt.test(r)&&d(t,"tag name cannot contain such characters: "+r),s?t.tag=r:K.call(t.tagMap,n)?t.tag=t.tagMap[n]+r:"!"===n?t.tag="!"+r:"!!"===n?t.tag="tag:yaml.org,2002:"+r:d(t,'undeclared tag handle "'+n+'"'),!0}function T(t){var e,n;if(38!==(n=t.input.charCodeAt(t.position)))return!1;for(null!==t.anchor&&d(t,"duplication of an anchor property"),n=t.input.charCodeAt(++t.position),e=t.position;0!==n&&!o(n)&&!s(n);)n=t.input.charCodeAt(++t.position);return t.position===e&&d(t,"name of an anchor node must contain at least one character"),t.anchor=t.input.slice(e,t.position),!0}function B(t){var e,n,r;if(42!==(r=t.input.charCodeAt(t.position)))return!1;for(r=t.input.charCodeAt(++t.position),e=t.position;0!==r&&!o(r)&&!s(r);)r=t.input.charCodeAt(++t.position);return t.position===e&&d(t,"name of an alias node must contain at least one character"),n=t.input.slice(e,t.position),t.anchorMap.hasOwnProperty(n)||d(t,'unidentified alias "'+n+'"'),t.result=t.anchorMap[n],D(t,!0,-1),!0}function M(t,e,n,r,i){var o,s,a,u,c,h,l,p,f=1,m=!1,y=!1;if(null!==t.listener&&t.listener("open",t),t.tag=null,t.anchor=null,t.kind=null,t.result=null,o=s=a=H===n||G===n,r&&D(t,!0,-1)&&(m=!0,t.lineIndent>e?f=1:t.lineIndent===e?f=0:t.lineIndent<e&&(f=-1)),1===f)for(;I(t)||T(t);)D(t,!0,-1)?(m=!0,a=o,t.lineIndent>e?f=1:t.lineIndent===e?f=0:t.lineIndent<e&&(f=-1)):a=!1;if(a&&(a=m||i),1!==f&&H!==n||(l=Y===n||W===n?e:e+1,p=t.position-t.lineStart,1===f?a&&(F(t,p)||k(t,p,l))||_(t,l)?y=!0:(s&&b(t,l)||w(t,l)||C(t,l)?y=!0:B(t)?(y=!0,null===t.tag&&null===t.anchor||d(t,"alias node should not have any properties")):S(t,l,Y===n)&&(y=!0,null===t.tag&&(t.tag="?")),null!==t.anchor&&(t.anchorMap[t.anchor]=t.result)):0===f&&(y=a&&F(t,p))),null!==t.tag&&"!"!==t.tag)if("?"===t.tag){for(u=0,c=t.implicitTypes.length;u<c;u+=1)if(h=t.implicitTypes[u],h.resolve(t.result)){t.result=h.construct(t.result),t.tag=h.tag,null!==t.anchor&&(t.anchorMap[t.anchor]=t.result);break}}else K.call(t.typeMap[t.kind||"fallback"],t.tag)?(h=t.typeMap[t.kind||"fallback"][t.tag],null!==t.result&&h.kind!==t.kind&&d(t,"unacceptable node kind for !<"+t.tag+'> tag; it should be "'+h.kind+'", not "'+t.kind+'"'),h.resolve(t.result)?(t.result=h.construct(t.result),null!==t.anchor&&(t.anchorMap[t.anchor]=t.result)):d(t,"cannot resolve a node with !<"+t.tag+"> explicit tag")):d(t,"unknown tag !<"+t.tag+">");return null!==t.listener&&t.listener("close",t),null!==t.tag||null!==t.anchor||y}function P(t){var e,n,s,a,u=t.position,c=!1;for(t.version=null,t.checkLineBreaks=t.legacy,t.tagMap={},t.anchorMap={};0!==(a=t.input.charCodeAt(t.position))&&(D(t,!0,-1),a=t.input.charCodeAt(t.position),!(t.lineIndent>0||37!==a));){for(c=!0,a=t.input.charCodeAt(++t.position),e=t.position;0!==a&&!o(a);)a=t.input.charCodeAt(++t.position);for(n=t.input.slice(e,t.position),s=[],n.length<1&&d(t,"directive name must not be less than one character in length");0!==a;){for(;i(a);)a=t.input.charCodeAt(++t.position);if(35===a){do{a=t.input.charCodeAt(++t.position)}while(0!==a&&!r(a));break}if(r(a))break;for(e=t.position;0!==a&&!o(a);)a=t.input.charCodeAt(++t.position);s.push(t.input.slice(e,t.position))}0!==a&&g(t),K.call(at,n)?at[n](t,n,s):m(t,'unknown document directive "'+n+'"')}if(D(t,!0,-1),0===t.lineIndent&&45===t.input.charCodeAt(t.position)&&45===t.input.charCodeAt(t.position+1)&&45===t.input.charCodeAt(t.position+2)?(t.position+=3,D(t,!0,-1)):c&&d(t,"directives end mark is expected"),M(t,t.lineIndent-1,H,!1,!0),D(t,!0,-1),t.checkLineBreaks&&tt.test(t.input.slice(u,t.position))&&m(t,"non-ASCII line breaks are interpreted as content"),t.documents.push(t.result),t.position===t.lineStart&&E(t))return void(46===t.input.charCodeAt(t.position)&&(t.position+=3,D(t,!0,-1)));t.position<t.length-1&&d(t,"end of the stream or a document separator is expected")}function N(t,e){t=String(t),e=e||{},0!==t.length&&(10!==t.charCodeAt(t.length-1)&&13!==t.charCodeAt(t.length-1)&&(t+="\n"),65279===t.charCodeAt(0)&&(t=t.slice(1)));var n=new p(t,e);for(n.input+="\0";32===n.input.charCodeAt(n.position);)n.lineIndent+=1,n.position+=1;for(;n.position<n.length-1;)P(n);return n.documents}function O(t,e,n){var r,i,o=N(t,n);if("function"!=typeof e)return o;for(r=0,i=o.length;r<i;r+=1)e(o[r])}function R(t,e){var n=N(t,e);if(0!==n.length){if(1===n.length)return n[0];throw new z("expected a single document in the stream, but found more")}}function U(t,e,n){if("function"!=typeof e)return O(t,L.extend({schema:X},n));O(t,e,L.extend({schema:X},n))}function j(t,e){return R(t,L.extend({schema:X},e))}for(var L=n(23),z=n(33),J=n(239),X=n(34),q=n(47),K=Object.prototype.hasOwnProperty,Y=1,W=2,G=3,H=4,V=1,$=2,Z=3,Q=/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x84\x86-\x9F\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/,tt=/[\x85\u2028\u2029]/,et=/[,\[\]\{\}]/,nt=/^(?:!|!!|![a-z\-]+!)$/i,rt=/^(?:!|[^,\[\]\{\}])(?:%[0-9a-f]{2}|[0-9a-z\-#;\/\?:@&=\+\$,_\.!~\*'\(\)\[\]])*$/i,it=new Array(256),ot=new Array(256),st=0;st<256;st++)it[st]=h(st)?1:0,ot[st]=h(st);var at={YAML:function(t,e,n){var r,i,o;null!==t.version&&d(t,"duplication of %YAML directive"),1!==n.length&&d(t,"YAML directive accepts exactly one argument"),r=/^([0-9]+)\.([0-9]+)$/.exec(n[0]),null===r&&d(t,"ill-formed argument of the YAML directive"),i=parseInt(r[1],10),o=parseInt(r[2],10),1!==i&&d(t,"unacceptable YAML version of the document"),t.version=n[0],t.checkLineBreaks=o<2,1!==o&&2!==o&&m(t,"unsupported YAML version of the document")},TAG:function(t,e,n){var r,i;2!==n.length&&d(t,"TAG directive accepts exactly two arguments"),r=n[0],i=n[1],nt.test(r)||d(t,"ill-formed tag handle (first argument) of the TAG directive"),K.call(t.tagMap,r)&&d(t,'there is a previously declared suffix for "'+r+'" tag handle'),rt.test(i)||d(t,"ill-formed tag prefix (second argument) of the TAG directive"),t.tagMap[r]=i}};t.exports.loadAll=O,t.exports.load=R,t.exports.safeLoadAll=U,t.exports.safeLoad=j},function(t,e,n){"use strict";function r(t,e,n,r,i){this.name=t,this.buffer=e,this.position=n,this.line=r,this.column=i}var i=n(23);r.prototype.getSnippet=function(t,e){var n,r,o,s,a;if(!this.buffer)return null;for(t=t||4,e=e||75,n="",r=this.position;r>0&&-1==="\0\r\n…\u2028\u2029".indexOf(this.buffer.charAt(r-1));)if(r-=1,this.position-r>e/2-1){n=" ... ",r+=5;break}for(o="",s=this.position;s<this.buffer.length&&-1==="\0\r\n…\u2028\u2029".indexOf(this.buffer.charAt(s));)if((s+=1)-this.position>e/2-1){o=" ... ",s-=5;break}return a=this.buffer.slice(r,s),i.repeat(" ",t)+n+a+o+"\n"+i.repeat(" ",t+this.position-r+n.length)+"^"},r.prototype.toString=function(t){var e,n="";return this.name&&(n+='in "'+this.name+'" '),n+="at line "+(this.line+1)+", column "+(this.column+1),t||(e=this.getSnippet())&&(n+=":\n"+e),n},t.exports=r},function(t,e,n){"use strict";function r(t){if(null===t)return!1;var e,n,r=0,i=t.length,o=c;for(n=0;n<i;n++)if(!((e=o.indexOf(t.charAt(n)))>64)){if(e<0)return!1;r+=6}return r%8==0}function i(t){var e,n,r=t.replace(/[\r\n=]/g,""),i=r.length,o=c,s=0,u=[];for(e=0;e<i;e++)e%4==0&&e&&(u.push(s>>16&255),u.push(s>>8&255),u.push(255&s)),s=s<<6|o.indexOf(r.charAt(e));return n=i%4*6,0===n?(u.push(s>>16&255),u.push(s>>8&255),u.push(255&s)):18===n?(u.push(s>>10&255),u.push(s>>2&255)):12===n&&u.push(s>>4&255),a?a.from?a.from(u):new a(u):u}function o(t){var e,n,r="",i=0,o=t.length,s=c;for(e=0;e<o;e++)e%3==0&&e&&(r+=s[i>>18&63],r+=s[i>>12&63],r+=s[i>>6&63],r+=s[63&i]),i=(i<<8)+t[e];return n=o%3,0===n?(r+=s[i>>18&63],r+=s[i>>12&63],r+=s[i>>6&63],r+=s[63&i]):2===n?(r+=s[i>>10&63],r+=s[i>>4&63],r+=s[i<<2&63],r+=s[64]):1===n&&(r+=s[i>>2&63],r+=s[i<<4&63],r+=s[64],r+=s[64]),r}function s(t){return a&&a.isBuffer(t)}var a;try{a=n(135).Buffer}catch(t){}var u=n(0),c="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\n\r";t.exports=new u("tag:yaml.org,2002:binary",{kind:"scalar",resolve:r,construct:i,predicate:s,represent:o})},function(t,e,n){"use strict";function r(t){if(null===t)return!1;var e=t.length;return 4===e&&("true"===t||"True"===t||"TRUE"===t)||5===e&&("false"===t||"False"===t||"FALSE"===t)}function i(t){return"true"===t||"True"===t||"TRUE"===t}function o(t){return"[object Boolean]"===Object.prototype.toString.call(t)}var s=n(0);t.exports=new s("tag:yaml.org,2002:bool",{kind:"scalar",resolve:r,construct:i,predicate:o,represent:{lowercase:function(t){return t?"true":"false"},uppercase:function(t){return t?"TRUE":"FALSE"},camelcase:function(t){return t?"True":"False"}},defaultStyle:"lowercase"})},function(t,e,n){"use strict";function r(t){return null!==t&&!(!c.test(t)||"_"===t[t.length-1])}function i(t){var e,n,r,i;return e=t.replace(/_/g,"").toLowerCase(),n="-"===e[0]?-1:1,i=[],"+-".indexOf(e[0])>=0&&(e=e.slice(1)),".inf"===e?1===n?Number.POSITIVE_INFINITY:Number.NEGATIVE_INFINITY:".nan"===e?NaN:e.indexOf(":")>=0?(e.split(":").forEach(function(t){i.unshift(parseFloat(t,10))}),e=0,r=1,i.forEach(function(t){e+=t*r,r*=60}),n*e):n*parseFloat(e,10)}function o(t,e){var n;if(isNaN(t))switch(e){case"lowercase":return".nan";case"uppercase":return".NAN";case"camelcase":return".NaN"}else if(Number.POSITIVE_INFINITY===t)switch(e){case"lowercase":return".inf";case"uppercase":return".INF";case"camelcase":return".Inf"}else if(Number.NEGATIVE_INFINITY===t)switch(e){case"lowercase":return"-.inf";case"uppercase":return"-.INF";case"camelcase":return"-.Inf"}else if(a.isNegativeZero(t))return"-0.0";return n=t.toString(10),h.test(n)?n.replace("e",".e"):n}function s(t){return"[object Number]"===Object.prototype.toString.call(t)&&(t%1!=0||a.isNegativeZero(t))}var a=n(23),u=n(0),c=new RegExp("^(?:[-+]?(?:0|[1-9][0-9_]*)(?:\\.[0-9_]*)?(?:[eE][-+]?[0-9]+)?|\\.[0-9_]+(?:[eE][-+]?[0-9]+)?|[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\\.[0-9_]*|[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$"),h=/^[-+]?[0-9]+e/;t.exports=new u("tag:yaml.org,2002:float",{kind:"scalar",resolve:r,construct:i,predicate:s,represent:o,defaultStyle:"lowercase"})},function(t,e,n){"use strict";function r(t){return 48<=t&&t<=57||65<=t&&t<=70||97<=t&&t<=102}function i(t){return 48<=t&&t<=55}function o(t){return 48<=t&&t<=57}function s(t){if(null===t)return!1;var e,n=t.length,s=0,a=!1;if(!n)return!1;if(e=t[s],"-"!==e&&"+"!==e||(e=t[++s]),"0"===e){if(s+1===n)return!0;if("b"===(e=t[++s])){for(s++;s<n;s++)if("_"!==(e=t[s])){if("0"!==e&&"1"!==e)return!1;a=!0}return a&&"_"!==e}if("x"===e){for(s++;s<n;s++)if("_"!==(e=t[s])){if(!r(t.charCodeAt(s)))return!1;a=!0}return a&&"_"!==e}for(;s<n;s++)if("_"!==(e=t[s])){if(!i(t.charCodeAt(s)))return!1;a=!0}return a&&"_"!==e}if("_"===e)return!1;for(;s<n;s++)if("_"!==(e=t[s])){if(":"===e)break;if(!o(t.charCodeAt(s)))return!1;a=!0}return!(!a||"_"===e)&&(":"!==e||/^(:[0-5]?[0-9])+$/.test(t.slice(s)))}function a(t){var e,n,r=t,i=1,o=[];return-1!==r.indexOf("_")&&(r=r.replace(/_/g,"")),e=r[0],"-"!==e&&"+"!==e||("-"===e&&(i=-1),r=r.slice(1),e=r[0]),"0"===r?0:"0"===e?"b"===r[1]?i*parseInt(r.slice(2),2):"x"===r[1]?i*parseInt(r,16):i*parseInt(r,8):-1!==r.indexOf(":")?(r.split(":").forEach(function(t){o.unshift(parseInt(t,10))}),r=0,n=1,o.forEach(function(t){r+=t*n,n*=60}),i*r):i*parseInt(r,10)}function u(t){return"[object Number]"===Object.prototype.toString.call(t)&&t%1==0&&!c.isNegativeZero(t)}var c=n(23),h=n(0);t.exports=new h("tag:yaml.org,2002:int",{kind:"scalar",resolve:s,construct:a,predicate:u,represent:{binary:function(t){return"0b"+t.toString(2)},octal:function(t){return"0"+t.toString(8)},decimal:function(t){return t.toString(10)},hexadecimal:function(t){return"0x"+t.toString(16).toUpperCase()}},defaultStyle:"decimal",styleAliases:{binary:[2,"bin"],octal:[8,"oct"],decimal:[10,"dec"],hexadecimal:[16,"hex"]}})},function(t,e,n){"use strict";function r(t){if(null===t)return!1;try{var e="("+t+")",n=a.parse(e,{range:!0});return"Program"===n.type&&1===n.body.length&&"ExpressionStatement"===n.body[0].type&&"FunctionExpression"===n.body[0].expression.type}catch(t){return!1}}function i(t){var e,n="("+t+")",r=a.parse(n,{range:!0}),i=[];if("Program"!==r.type||1!==r.body.length||"ExpressionStatement"!==r.body[0].type||"FunctionExpression"!==r.body[0].expression.type)throw new Error("Failed to resolve function");return r.body[0].expression.params.forEach(function(t){i.push(t.name)}),e=r.body[0].expression.body.range,new Function(i,n.slice(e[0]+1,e[1]-1))}function o(t){return t.toString()}function s(t){return"[object Function]"===Object.prototype.toString.call(t)}var a;try{a=n(231)}catch(t){"undefined"!=typeof window&&(a=window.esprima)}var u=n(0);t.exports=new u("tag:yaml.org,2002:js/function",{kind:"scalar",resolve:r,construct:i,predicate:s,represent:o})},function(t,e,n){"use strict";function r(t){if(null===t)return!1;if(0===t.length)return!1;var e=t,n=/\/([gim]*)$/.exec(t),r="";if("/"===e[0]){if(n&&(r=n[1]),r.length>3)return!1;if("/"!==e[e.length-r.length-1])return!1}return!0}function i(t){var e=t,n=/\/([gim]*)$/.exec(t),r="";return"/"===e[0]&&(n&&(r=n[1]),e=e.slice(1,e.length-r.length-1)),new RegExp(e,r)}function o(t){var e="/"+t.source+"/";return t.global&&(e+="g"),t.multiline&&(e+="m"),t.ignoreCase&&(e+="i"),e}function s(t){return"[object RegExp]"===Object.prototype.toString.call(t)}var a=n(0);t.exports=new a("tag:yaml.org,2002:js/regexp",{kind:"scalar",resolve:r,construct:i,predicate:s,represent:o})},function(t,e,n){"use strict";function r(){return!0}function i(){}function o(){return""}function s(t){return void 0===t}var a=n(0);t.exports=new a("tag:yaml.org,2002:js/undefined",{kind:"scalar",resolve:r,construct:i,predicate:s,represent:o})},function(t,e,n){"use strict";var r=n(0);t.exports=new r("tag:yaml.org,2002:map",{kind:"mapping",construct:function(t){return null!==t?t:{}}})},function(t,e,n){"use strict";function r(t){return"<<"===t||null===t}var i=n(0);t.exports=new i("tag:yaml.org,2002:merge",{kind:"scalar",resolve:r})},function(t,e,n){"use strict";function r(t){if(null===t)return!0;var e=t.length;return 1===e&&"~"===t||4===e&&("null"===t||"Null"===t||"NULL"===t)}function i(){return null}function o(t){return null===t}var s=n(0);t.exports=new s("tag:yaml.org,2002:null",{kind:"scalar",resolve:r,construct:i,predicate:o,represent:{canonical:function(){return"~"},lowercase:function(){return"null"},uppercase:function(){return"NULL"},camelcase:function(){return"Null"}},defaultStyle:"lowercase"})},function(t,e,n){"use strict";function r(t){if(null===t)return!0;var e,n,r,i,o,u=[],c=t;for(e=0,n=c.length;e<n;e+=1){if(r=c[e],o=!1,"[object Object]"!==a.call(r))return!1;for(i in r)if(s.call(r,i)){if(o)return!1;o=!0}if(!o)return!1;if(-1!==u.indexOf(i))return!1;u.push(i)}return!0}function i(t){return null!==t?t:[]}var o=n(0),s=Object.prototype.hasOwnProperty,a=Object.prototype.toString;t.exports=new o("tag:yaml.org,2002:omap",{kind:"sequence",resolve:r,construct:i})},function(t,e,n){"use strict";function r(t){if(null===t)return!0;var e,n,r,i,o,a=t;for(o=new Array(a.length),e=0,n=a.length;e<n;e+=1){if(r=a[e],"[object Object]"!==s.call(r))return!1;if(i=Object.keys(r),1!==i.length)return!1;o[e]=[i[0],r[i[0]]]}return!0}function i(t){if(null===t)return[];var e,n,r,i,o,s=t;for(o=new Array(s.length),e=0,n=s.length;e<n;e+=1)r=s[e],i=Object.keys(r),o[e]=[i[0],r[i[0]]];return o}var o=n(0),s=Object.prototype.toString;t.exports=new o("tag:yaml.org,2002:pairs",{kind:"sequence",resolve:r,construct:i})},function(t,e,n){"use strict";var r=n(0);t.exports=new r("tag:yaml.org,2002:seq",{kind:"sequence",construct:function(t){return null!==t?t:[]}})},function(t,e,n){"use strict";function r(t){if(null===t)return!0;var e,n=t;for(e in n)if(s.call(n,e)&&null!==n[e])return!1;return!0}function i(t){return null!==t?t:{}}var o=n(0),s=Object.prototype.hasOwnProperty;t.exports=new o("tag:yaml.org,2002:set",{kind:"mapping",resolve:r,construct:i})},function(t,e,n){"use strict";var r=n(0);t.exports=new r("tag:yaml.org,2002:str",{kind:"scalar",construct:function(t){return null!==t?t:""}})},function(t,e,n){"use strict";function r(t){return null!==t&&(null!==a.exec(t)||null!==u.exec(t))}function i(t){var e,n,r,i,o,s,c,h,l,p,f=0,d=null;if(e=a.exec(t),null===e&&(e=u.exec(t)),null===e)throw new Error("Date resolve error");if(n=+e[1],r=+e[2]-1,i=+e[3],!e[4])return new Date(Date.UTC(n,r,i));if(o=+e[4],s=+e[5],c=+e[6],e[7]){for(f=e[7].slice(0,3);f.length<3;)f+="0";f=+f}return e[9]&&(h=+e[10],l=+(e[11]||0),d=6e4*(60*h+l),"-"===e[9]&&(d=-d)),p=new Date(Date.UTC(n,r,i,o,s,c,f)),d&&p.setTime(p.getTime()-d),p}function o(t){return t.toISOString()}var s=n(0),a=new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])$"),u=new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)(?:[Tt]|[ \\t]+)([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(?:\\.([0-9]*))?(?:[ \\t]*(Z|([-+])([0-9][0-9]?)(?::([0-9][0-9]))?))?$");t.exports=new s("tag:yaml.org,2002:timestamp",{kind:"scalar",resolve:r,construct:i,instanceOf:Date,represent:o})},function(t,e,n){"use strict";function r(t,e,n,r,i){}t.exports=r},function(t,e,n){"use strict";var r=n(259);t.exports=function(t){return r(t,!1)}},function(t,e,n){"use strict";var r=n(45),i=n(15),o=n(113);t.exports=function(){function t(t,e,n,r,s,a){a!==o&&i(!1,"Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types")}function e(){return t}t.isRequired=t;var n={array:t,bool:t,func:t,number:t,object:t,string:t,symbol:t,any:t,arrayOf:e,element:t,instanceOf:e,node:t,objectOf:e,oneOf:e,oneOfType:e,shape:e,exact:e};return n.checkPropTypes=r,n.PropTypes=n,n}},function(t,e,n){"use strict";var r=n(45),i=n(15),o=n(46),s=n(35),a=n(113),u=n(256);t.exports=function(t,e){function n(t){var e=t&&(_&&t[_]||t[b]);if("function"==typeof e)return e}function c(t,e){return t===e?0!==t||1/t==1/e:t!==t&&e!==e}function h(t){this.message=t,this.stack=""}function l(t){function n(n,r,o,s,u,c,l){if(s=s||F,c=c||o,l!==a)if(e)i(!1,"Calling PropTypes validators directly is not supported by the `prop-types` package. Use `PropTypes.checkPropTypes()` to call them. Read more at http://fb.me/use-check-prop-types");else;return null==r[o]?n?new h(null===r[o]?"The "+u+" `"+c+"` is marked as required in `"+s+"`, but its value is `null`.":"The "+u+" `"+c+"` is marked as required in `"+s+"`, but its value is `undefined`."):null:t(r,o,s,u,c)}var r=n.bind(null,!1);return r.isRequired=n.bind(null,!0),r}function p(t){function e(e,n,r,i,o,s){var a=e[n];if(A(a)!==t)return new h("Invalid "+i+" `"+o+"` of type `"+S(a)+"` supplied to `"+r+"`, expected `"+t+"`.");return null}return l(e)}function f(t){function e(e,n,r,i,o){if("function"!=typeof t)return new h("Property `"+o+"` of component `"+r+"` has invalid PropType notation inside arrayOf.");var s=e[n];if(!Array.isArray(s)){return new h("Invalid "+i+" `"+o+"` of type `"+A(s)+"` supplied to `"+r+"`, expected an array.")}for(var u=0;u<s.length;u++){var c=t(s,u,r,i,o+"["+u+"]",a);if(c instanceof Error)return c}return null}return l(e)}function d(t){function e(e,n,r,i,o){if(!(e[n]instanceof t)){var s=t.name||F;return new h("Invalid "+i+" `"+o+"` of type `"+C(e[n])+"` supplied to `"+r+"`, expected instance of `"+s+"`.")}return null}return l(e)}function m(t){function e(e,n,r,i,o){for(var s=e[n],a=0;a<t.length;a++)if(c(s,t[a]))return null;return new h("Invalid "+i+" `"+o+"` of value `"+s+"` supplied to `"+r+"`, expected one of "+JSON.stringify(t)+".")}return Array.isArray(t)?l(e):r.thatReturnsNull}function y(t){function e(e,n,r,i,o){if("function"!=typeof t)return new h("Property `"+o+"` of component `"+r+"` has invalid PropType notation inside objectOf.");var s=e[n],u=A(s);if("object"!==u)return new h("Invalid "+i+" `"+o+"` of type `"+u+"` supplied to `"+r+"`, expected an object.");for(var c in s)if(s.hasOwnProperty(c)){var l=t(s,c,r,i,o+"."+c,a);if(l instanceof Error)return l}return null}return l(e)}function v(t){function e(e,n,r,i,o){for(var s=0;s<t.length;s++){if(null==(0,t[s])(e,n,r,i,o,a))return null}return new h("Invalid "+i+" `"+o+"` supplied to `"+r+"`.")}if(!Array.isArray(t))return r.thatReturnsNull;for(var n=0;n<t.length;n++){var i=t[n];if("function"!=typeof i)return o(!1,"Invalid argument supplied to oneOfType. Expected an array of check functions, but received %s at index %s.",w(i),n),r.thatReturnsNull}return l(e)}function x(t){function e(e,n,r,i,o){var s=e[n],u=A(s);if("object"!==u)return new h("Invalid "+i+" `"+o+"` of type `"+u+"` supplied to `"+r+"`, expected `object`.");for(var c in t){var l=t[c];if(l){var p=l(s,c,r,i,o+"."+c,a);if(p)return p}}return null}return l(e)}function g(t){function e(e,n,r,i,o){var u=e[n],c=A(u);if("object"!==c)return new h("Invalid "+i+" `"+o+"` of type `"+c+"` supplied to `"+r+"`, expected `object`.");var l=s({},e[n],t);for(var p in l){var f=t[p];if(!f)return new h("Invalid "+i+" `"+o+"` key `"+p+"` supplied to `"+r+"`.\nBad object: "+JSON.stringify(e[n],null," ")+"\nValid keys: "+JSON.stringify(Object.keys(t),null," "));var d=f(u,p,r,i,o+"."+p,a);if(d)return d}return null}return l(e)}function D(e){switch(typeof e){case"number":case"string":case"undefined":return!0;case"boolean":return!e;case"object":if(Array.isArray(e))return e.every(D);if(null===e||t(e))return!0;var r=n(e);if(!r)return!1;var i,o=r.call(e);if(r!==e.entries){for(;!(i=o.next()).done;)if(!D(i.value))return!1}else for(;!(i=o.next()).done;){var s=i.value;if(s&&!D(s[1]))return!1}return!0;default:return!1}}function E(t,e){return"symbol"===t||("Symbol"===e["@@toStringTag"]||"function"==typeof Symbol&&e instanceof Symbol)}function A(t){var e=typeof t;return Array.isArray(t)?"array":t instanceof RegExp?"object":E(e,t)?"symbol":e}function S(t){if(void 0===t||null===t)return""+t;var e=A(t);if("object"===e){if(t instanceof Date)return"date";if(t instanceof RegExp)return"regexp"}return e}function w(t){var e=S(t);switch(e){case"array":case"object":return"an "+e;case"boolean":case"date":case"regexp":return"a "+e;default:return e}}function C(t){return t.constructor&&t.constructor.name?t.constructor.name:F}var _="function"==typeof Symbol&&Symbol.iterator,b="@@iterator",F="<<anonymous>>",k={array:p("array"),bool:p("boolean"),func:p("function"),number:p("number"),object:p("object"),string:p("string"),symbol:p("symbol"),any:function(){return l(r.thatReturnsNull)}(),arrayOf:f,element:function(){function e(e,n,r,i,o){var s=e[n];if(!t(s)){return new h("Invalid "+i+" `"+o+"` of type `"+A(s)+"` supplied to `"+r+"`, expected a single ReactElement.")}return null}return l(e)}(),instanceOf:d,node:function(){function t(t,e,n,r,i){return D(t[e])?null:new h("Invalid "+r+" `"+i+"` supplied to `"+n+"`, expected a ReactNode.")}return l(t)}(),objectOf:y,oneOf:m,oneOfType:v,shape:x,exact:g};return h.prototype=Error.prototype,k.checkPropTypes=u,k.PropTypes=k,k}},function(t,e){t.exports='---\nurl: "http://petstore.swagger.io/v2/swagger.json"\ndom_id: "#swagger-ui"\nvalidatorUrl: "https://online.swagger.io/validator"\noauth2RedirectUrl: "http://localhost:3200/oauth2-redirect.html"\n'},function(t,e,n){"use strict";function r(t){var e={"=":"=0",":":"=2"};return"$"+(""+t).replace(/[=:]/g,function(t){return e[t]})}function i(t){var e=/(=0|=2)/g,n={"=0":"=","=2":":"};return(""+("."===t[0]&&"$"===t[1]?t.substring(2):t.substring(1))).replace(e,function(t){return n[t]})}var o={escape:r,unescape:i};t.exports=o},function(t,e,n){"use strict";var r=n(48),i=(n(15),function(t){var e=this;if(e.instancePool.length){var n=e.instancePool.pop();return e.call(n,t),n}return new e(t)}),o=function(t,e){var n=this;if(n.instancePool.length){var r=n.instancePool.pop();return n.call(r,t,e),r}return new n(t,e)},s=function(t,e,n){var r=this;if(r.instancePool.length){var i=r.instancePool.pop();return r.call(i,t,e,n),i}return new r(t,e,n)},a=function(t,e,n,r){var i=this;if(i.instancePool.length){var o=i.instancePool.pop();return i.call(o,t,e,n,r),o}return new i(t,e,n,r)},u=function(t){var e=this;t instanceof e||r("25"),t.destructor(),e.instancePool.length<e.poolSize&&e.instancePool.push(t)},c=i,h=function(t,e){var n=t;return n.instancePool=[],n.getPooled=e||c,n.poolSize||(n.poolSize=10),n.release=u,n},l={addPoolingTo:h,oneArgumentPooler:i,twoArgumentPooler:o,threeArgumentPooler:s,fourArgumentPooler:a};t.exports=l},function(t,e,n){"use strict";var r=n(35),i=n(114),o=n(264),s=n(265),a=n(25),u=n(266),c=n(267),h=n(268),l=n(271),p=a.createElement,f=a.createFactory,d=a.cloneElement,m=r,y=function(t){return t},v={Children:{map:o.map,forEach:o.forEach,count:o.count,toArray:o.toArray,only:l},Component:i.Component,PureComponent:i.PureComponent,createElement:p,cloneElement:d,isValidElement:a.isValidElement,PropTypes:u,createClass:h,createFactory:f,createMixin:y,DOM:s,version:c,__spread:m};t.exports=v},function(t,e,n){"use strict";function r(t){return(""+t).replace(D,"$&/")}function i(t,e){this.func=t,this.context=e,this.count=0}function o(t,e,n){var r=t.func,i=t.context;r.call(i,e,t.count++)}function s(t,e,n){if(null==t)return t;var r=i.getPooled(e,n);v(t,o,r),i.release(r)}function a(t,e,n,r){this.result=t,this.keyPrefix=e,this.func=n,this.context=r,this.count=0}function u(t,e,n){var i=t.result,o=t.keyPrefix,s=t.func,a=t.context,u=s.call(a,e,t.count++);Array.isArray(u)?c(u,i,n,y.thatReturnsArgument):null!=u&&(m.isValidElement(u)&&(u=m.cloneAndReplaceKey(u,o+(!u.key||e&&e.key===u.key?"":r(u.key)+"/")+n)),i.push(u))}function c(t,e,n,i,o){var s="";null!=n&&(s=r(n)+"/");var c=a.getPooled(e,s,i,o);v(t,u,c),a.release(c)}function h(t,e,n){if(null==t)return t;var r=[];return c(t,r,null,e,n),r}function l(t,e,n){return null}function p(t,e){return v(t,l,null)}function f(t){var e=[];return c(t,e,null,y.thatReturnsArgument),e}var d=n(262),m=n(25),y=n(45),v=n(272),x=d.twoArgumentPooler,g=d.fourArgumentPooler,D=/\/+/g;i.prototype.destructor=function(){this.func=null,this.context=null,this.count=0},d.addPoolingTo(i,x),a.prototype.destructor=function(){this.result=null,this.keyPrefix=null,this.func=null,this.context=null,this.count=0},d.addPoolingTo(a,g);var E={forEach:s,map:h,mapIntoWithKeyPrefixInternal:c,count:p,toArray:f};t.exports=E},function(t,e,n){"use strict";var r=n(25),i=r.createFactory,o={a:i("a"),abbr:i("abbr"),address:i("address"),area:i("area"),article:i("article"),aside:i("aside"),audio:i("audio"),b:i("b"),base:i("base"),bdi:i("bdi"),bdo:i("bdo"),big:i("big"),blockquote:i("blockquote"),body:i("body"),br:i("br"),button:i("button"),canvas:i("canvas"),caption:i("caption"),cite:i("cite"),code:i("code"),col:i("col"),colgroup:i("colgroup"),data:i("data"),datalist:i("datalist"),dd:i("dd"),del:i("del"),details:i("details"),dfn:i("dfn"),dialog:i("dialog"),div:i("div"),dl:i("dl"),dt:i("dt"),em:i("em"),embed:i("embed"),fieldset:i("fieldset"),figcaption:i("figcaption"),figure:i("figure"),footer:i("footer"),form:i("form"),h1:i("h1"),h2:i("h2"),h3:i("h3"),h4:i("h4"),h5:i("h5"),h6:i("h6"),head:i("head"),header:i("header"),hgroup:i("hgroup"),hr:i("hr"),html:i("html"),i:i("i"),iframe:i("iframe"),img:i("img"),input:i("input"),ins:i("ins"),kbd:i("kbd"),keygen:i("keygen"),label:i("label"),legend:i("legend"),li:i("li"),link:i("link"),main:i("main"),map:i("map"),mark:i("mark"),menu:i("menu"),menuitem:i("menuitem"),meta:i("meta"),meter:i("meter"),nav:i("nav"),noscript:i("noscript"),object:i("object"),ol:i("ol"),optgroup:i("optgroup"),option:i("option"),output:i("output"),p:i("p"),param:i("param"),picture:i("picture"),pre:i("pre"),progress:i("progress"),q:i("q"),rp:i("rp"),rt:i("rt"),ruby:i("ruby"),s:i("s"),samp:i("samp"),script:i("script"),section:i("section"),select:i("select"),small:i("small"),source:i("source"),span:i("span"),strong:i("strong"),style:i("style"),sub:i("sub"),summary:i("summary"),sup:i("sup"),table:i("table"),tbody:i("tbody"),td:i("td"),textarea:i("textarea"),tfoot:i("tfoot"),th:i("th"),thead:i("thead"),time:i("time"),title:i("title"),tr:i("tr"),track:i("track"),u:i("u"),ul:i("ul"),var:i("var"),video:i("video"),wbr:i("wbr"),circle:i("circle"),clipPath:i("clipPath"),defs:i("defs"),ellipse:i("ellipse"),g:i("g"),image:i("image"),line:i("line"),linearGradient:i("linearGradient"),mask:i("mask"),path:i("path"),pattern:i("pattern"),polygon:i("polygon"),polyline:i("polyline"),radialGradient:i("radialGradient"),rect:i("rect"),stop:i("stop"),svg:i("svg"),text:i("text"),tspan:i("tspan")};t.exports=o},function(t,e,n){"use strict";var r=n(25),i=r.isValidElement,o=n(257);t.exports=o(i)},function(t,e,n){"use strict";t.exports="15.6.2"},function(t,e,n){"use strict";var r=n(114),i=r.Component,o=n(25),s=o.isValidElement,a=n(117),u=n(230);t.exports=u(i,s,a)},function(t,e,n){"use strict";function r(t){var e=t&&(i&&t[i]||t[o]);if("function"==typeof e)return e}var i="function"==typeof Symbol&&Symbol.iterator,o="@@iterator";t.exports=r},function(t,e,n){"use strict";var r=function(){};t.exports=r},function(t,e,n){"use strict";function r(t){return o.isValidElement(t)||i("143"),t}var i=n(48),o=n(25);n(15);t.exports=r},function(t,e,n){"use strict";function r(t,e){return t&&"object"==typeof t&&null!=t.key?c.escape(t.key):e.toString(36)}function i(t,e,n,o){var p=typeof t;if("undefined"!==p&&"boolean"!==p||(t=null),null===t||"string"===p||"number"===p||"object"===p&&t.$$typeof===a)return n(o,t,""===e?h+r(t,0):e),1;var f,d,m=0,y=""===e?h:e+l;if(Array.isArray(t))for(var v=0;v<t.length;v++)f=t[v],d=y+r(f,v),m+=i(f,d,n,o);else{var x=u(t);if(x){var g,D=x.call(t);if(x!==t.entries)for(var E=0;!(g=D.next()).done;)f=g.value,d=y+r(f,E++),m+=i(f,d,n,o);else for(;!(g=D.next()).done;){var A=g.value;A&&(f=A[1],d=y+c.escape(A[0])+l+r(f,0),m+=i(f,d,n,o))}}else if("object"===p){var S="",w=String(t);s("31","[object Object]"===w?"object with keys {"+Object.keys(t).join(", ")+"}":w,S)}}return m}function o(t,e,n){return null==t?0:i(t,"",e,n)}var s=n(48),a=(n(115),n(116)),u=n(269),c=(n(15),n(261)),h=(n(46),"."),l=":";t.exports=o},function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAMAAAAM7l6QAAAAYFBMVEUAAABUfwBUfwBUfwBUfwBUfwBUfwBUfwBUfwBUfwBUfwBUfwBUfwBUfwBUfwB0lzB/n0BfhxBpjyC0x4////+qv4CJp1D09++ft3C/z5/K16/U379UfwDf58/q79+Ur2D2RCk9AAAAHXRSTlMAEEAwn9//z3Agv4/vYID/////////////////UMeji1kAAAD8SURBVHgBlZMFAoQwDATRxbXB7f+vPKnlXAZn6k2cf3A9z/PfOC8IIYni5FmmABM8FMhwT17c9hnhiZL1CwvEL1tmPD0qSKq6gaStW/kMXanVmAVRDUlH1OvuuTINo6k90Sxf8qsOtF6g4ff1osP3OnMcV7d4pzdIUtu1oA4V0DZoKmxmlEYvtDUjjS3tmKmqB+pYy8pD1VPf7jPE0I40HHcaBwnue6fGzgyS5tXIU96PV7rkDWHNLV0DK4FkoKmFpN5oUnvi8KoeA2/JXsmXQuokx0siR1G8tLkN6eB9sLwJp/yymcyaP/TrP+RPmbMMixcJVgTR1aUZ93oGXsgXQAaG6EwAAAAASUVORK5CYII="},function(t,e){var n;n=function(){return this}();try{n=n||Function("return this")()||(0,eval)("this")}catch(t){"object"==typeof window&&(n=window)}t.exports=n},function(t,e,n){n(120),t.exports=n(121)}])}); -//# sourceMappingURL=swagger-ui-standalone-preset.js.map \ No newline at end of file +//# sourceMappingURL=swagger-ui-standalone-preset.js.map diff --git a/app/code/Magento/Swatches/Block/Product/Renderer/Listing/Configurable.php b/app/code/Magento/Swatches/Block/Product/Renderer/Listing/Configurable.php index e13373fb7255..2e4980c2fbfd 100644 --- a/app/code/Magento/Swatches/Block/Product/Renderer/Listing/Configurable.php +++ b/app/code/Magento/Swatches/Block/Product/Renderer/Listing/Configurable.php @@ -8,7 +8,8 @@ use Magento\Catalog\Block\Product\Context; use Magento\Catalog\Helper\Product as CatalogProduct; use Magento\Catalog\Model\Product; -use Magento\Catalog\Model\Product\Image\UrlBuilder; +use Magento\Catalog\Model\Layer\Resolver; +use Magento\Catalog\Model\Layer\Category as CategoryLayer; use Magento\ConfigurableProduct\Helper\Data; use Magento\ConfigurableProduct\Model\ConfigurableAttributeData; use Magento\Customer\Helper\Session\CurrentCustomer; @@ -39,6 +40,11 @@ class Configurable extends \Magento\Swatches\Block\Product\Renderer\Configurable */ private $variationPrices; + /** + * @var \Magento\Catalog\Model\Layer\Resolver + */ + private $layerResolver; + /** * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @param Context $context @@ -55,6 +61,7 @@ class Configurable extends \Magento\Swatches\Block\Product\Renderer\Configurable * @param SwatchAttributesProvider|null $swatchAttributesProvider * @param \Magento\Framework\Locale\Format|null $localeFormat * @param \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Variations\Prices|null $variationPrices + * @param Resolver $layerResolver */ public function __construct( Context $context, @@ -70,7 +77,8 @@ public function __construct( array $data = [], SwatchAttributesProvider $swatchAttributesProvider = null, \Magento\Framework\Locale\Format $localeFormat = null, - \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Variations\Prices $variationPrices = null + \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Variations\Prices $variationPrices = null, + Resolver $layerResolver = null ) { parent::__construct( $context, @@ -92,10 +100,11 @@ public function __construct( $this->variationPrices = $variationPrices ?: ObjectManager::getInstance()->get( \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Variations\Prices::class ); + $this->layerResolver = $layerResolver ?: ObjectManager::getInstance()->get(Resolver::class); } /** - * @return string + * @inheritdoc */ protected function getRendererTemplate() { @@ -121,7 +130,7 @@ protected function _toHtml() } /** - * @return array + * @inheritdoc */ protected function getSwatchAttributesData() { @@ -183,6 +192,7 @@ protected function getOptionImages() * Add images to result json config in case of Layered Navigation is used * * @return array + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) * @since 100.2.0 */ protected function _getAdditionalConfig() @@ -247,4 +257,16 @@ private function getLayeredAttributesIfExists(Product $configurableProduct, arra return $layeredAttributes; } + + /** + * @inheritdoc + */ + public function getCacheKeyInfo() + { + $cacheKeyInfo = parent::getCacheKeyInfo(); + /** @var CategoryLayer $catalogLayer */ + $catalogLayer = $this->layerResolver->get(); + $cacheKeyInfo[] = $catalogLayer->getStateKey(); + return $cacheKeyInfo; + } } diff --git a/app/code/Magento/Swatches/Controller/Adminhtml/Product/Attribute/Plugin/Save.php b/app/code/Magento/Swatches/Controller/Adminhtml/Product/Attribute/Plugin/Save.php index 383c97a166d3..72d27152d639 100644 --- a/app/code/Magento/Swatches/Controller/Adminhtml/Product/Attribute/Plugin/Save.php +++ b/app/code/Magento/Swatches/Controller/Adminhtml/Product/Attribute/Plugin/Save.php @@ -16,6 +16,8 @@ class Save { /** + * Performs the conversion of the frontend input value. + * * @param Attribute\Save $subject * @param RequestInterface $request * @return array @@ -26,15 +28,6 @@ public function beforeDispatch(Attribute\Save $subject, RequestInterface $reques $data = $request->getPostValue(); if (isset($data['frontend_input'])) { - //Data is serialized to overcome issues caused by max_input_vars value if it's modification is unavailable. - //See subject controller code and comments for more info. - if (isset($data['serialized_swatch_values']) - && in_array($data['frontend_input'], ['swatch_visual', 'swatch_text']) - ) { - $data['serialized_options'] = $data['serialized_swatch_values']; - unset($data['serialized_swatch_values']); - } - switch ($data['frontend_input']) { case 'swatch_visual': $data[Swatch::SWATCH_INPUT_TYPE_KEY] = Swatch::SWATCH_INPUT_TYPE_VISUAL; diff --git a/app/code/Magento/Swatches/Controller/Ajax/Media.php b/app/code/Magento/Swatches/Controller/Ajax/Media.php index 079ba8f89712..2d20b9a9a4b7 100644 --- a/app/code/Magento/Swatches/Controller/Ajax/Media.php +++ b/app/code/Magento/Swatches/Controller/Ajax/Media.php @@ -12,7 +12,7 @@ /** * Class Media */ -class Media extends \Magento\Framework\App\Action\Action +class Media extends \Magento\Framework\App\Action\Action implements \Magento\Framework\App\Action\HttpGetActionInterface { /** * @var \Magento\Catalog\Model\Product Factory @@ -24,18 +24,26 @@ class Media extends \Magento\Framework\App\Action\Action */ private $swatchHelper; + /** + * @var \Magento\PageCache\Model\Config + */ + protected $config; + /** * @param Context $context * @param \Magento\Catalog\Model\ProductFactory $productModelFactory * @param \Magento\Swatches\Helper\Data $swatchHelper + * @param \Magento\PageCache\Model\Config $config */ public function __construct( Context $context, \Magento\Catalog\Model\ProductFactory $productModelFactory, - \Magento\Swatches\Helper\Data $swatchHelper + \Magento\Swatches\Helper\Data $swatchHelper, + \Magento\PageCache\Model\Config $config ) { $this->productModelFactory = $productModelFactory; $this->swatchHelper = $swatchHelper; + $this->config = $config; parent::__construct($context); } @@ -44,18 +52,28 @@ public function __construct( * Get product media for specified configurable product variation * * @return string + * @throws \Magento\Framework\Exception\LocalizedException */ public function execute() { $productMedia = []; + + /** @var \Magento\Framework\Controller\Result\Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + + /** @var \Magento\Framework\App\ResponseInterface $response */ + $response = $this->getResponse(); + if ($productId = (int)$this->getRequest()->getParam('product_id')) { + $product = $this->productModelFactory->create()->load($productId); $productMedia = $this->swatchHelper->getProductMediaGallery( - $this->productModelFactory->create()->load($productId) + $product ); + $resultJson->setHeader('X-Magento-Tags', implode(',', $product->getIdentities())); + + $response->setPublicHeaders($this->config->getTtl()); } - /** @var \Magento\Framework\Controller\Result\Json $resultJson */ - $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); $resultJson->setData($productMedia); return $resultJson; } diff --git a/app/code/Magento/Swatches/Helper/Data.php b/app/code/Magento/Swatches/Helper/Data.php index 38879178235c..f9a600925b2a 100644 --- a/app/code/Magento/Swatches/Helper/Data.php +++ b/app/code/Magento/Swatches/Helper/Data.php @@ -5,9 +5,9 @@ */ namespace Magento\Swatches\Helper; +use Magento\Catalog\Api\Data\ProductAttributeMediaGalleryEntryInterface; use Magento\Catalog\Api\Data\ProductInterface as Product; use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Catalog\Helper\Image; use Magento\Catalog\Model\Product as ModelProduct; use Magento\Catalog\Model\Product\Image\UrlBuilder; use Magento\Catalog\Model\ResourceModel\Eav\Attribute; @@ -132,6 +132,8 @@ public function __construct( } /** + * Assemble Additional Data for Eav Attribute + * * @param Attribute $attribute * @return $this */ @@ -181,6 +183,8 @@ private function isMediaAvailable(ModelProduct $product, string $attributeCode): } /** + * Load first variation + * * @param string $attributeCode swatch_image|image * @param ModelProduct $configurableProduct * @param array $requiredAttributes @@ -204,6 +208,8 @@ private function loadFirstVariation($attributeCode, ModelProduct $configurablePr } /** + * Load first variation with swatch image + * * @param Product $configurableProduct * @param array $requiredAttributes * @return bool|Product @@ -214,6 +220,8 @@ public function loadFirstVariationWithSwatchImage(Product $configurableProduct, } /** + * Load first variation with image + * * @param Product $configurableProduct * @param array $requiredAttributes * @return bool|Product @@ -246,18 +254,15 @@ public function loadVariationByFallback(Product $parentProduct, array $attribute $this->addFilterByParent($productCollection, $parentId); $configurableAttributes = $this->getAttributesFromConfigurable($parentProduct); - $allAttributesArray = []; + + $resultAttributesToFilter = []; foreach ($configurableAttributes as $attribute) { - if (!empty($attribute['default_value'])) { - $allAttributesArray[$attribute['attribute_code']] = $attribute['default_value']; + $attributeCode = $attribute->getData('attribute_code'); + if (array_key_exists($attributeCode, $attributes)) { + $resultAttributesToFilter[$attributeCode] = $attributes[$attributeCode]; } } - $resultAttributesToFilter = array_merge( - $attributes, - array_diff_key($allAttributesArray, $attributes) - ); - $this->addFilterByAttributes($productCollection, $resultAttributesToFilter); $variationProduct = $productCollection->getFirstItem(); @@ -269,6 +274,8 @@ public function loadVariationByFallback(Product $parentProduct, array $attribute } /** + * Add filter by attribute + * * @param ProductCollection $productCollection * @param array $attributes * @return void @@ -281,6 +288,8 @@ private function addFilterByAttributes(ProductCollection $productCollection, arr } /** + * Add filter by parent + * * @param ProductCollection $productCollection * @param integer $parentId * @return void @@ -299,6 +308,7 @@ private function addFilterByParent(ProductCollection $productCollection, $parent /** * Method getting full media gallery for current Product + * * Array structure: [ * ['image'] => 'http://url/pub/media/catalog/product/2/0/blabla.jpg', * ['mediaGallery'] => [ @@ -307,38 +317,68 @@ private function addFilterByParent(ProductCollection $productCollection, $parent * ..., * ] * ] + * * @param ModelProduct $product + * * @return array + * @throws \Magento\Framework\Exception\LocalizedException */ - public function getProductMediaGallery(ModelProduct $product) + public function getProductMediaGallery(ModelProduct $product): array { $baseImage = null; $gallery = []; $mediaGallery = $product->getMediaGalleryEntries(); + /** @var ProductAttributeMediaGalleryEntryInterface $mediaEntry */ foreach ($mediaGallery as $mediaEntry) { if ($mediaEntry->isDisabled()) { continue; } - - if (in_array('image', $mediaEntry->getTypes(), true) || !$baseImage) { - $baseImage = $mediaEntry->getFile(); + if (!$baseImage || $this->isMainImage($mediaEntry)) { + $baseImage = $mediaEntry; } - $gallery[$mediaEntry->getId()] = $this->getAllSizeImages($mediaEntry->getFile()); + $gallery[$mediaEntry->getId()] = $this->collectImageData($mediaEntry); } if (!$baseImage) { return []; } - $resultGallery = $this->getAllSizeImages($baseImage); + $resultGallery = $this->collectImageData($baseImage); $resultGallery['gallery'] = $gallery; return $resultGallery; } /** + * Checks if image is main image in gallery + * + * @param ProductAttributeMediaGalleryEntryInterface $mediaEntry + * @return bool + */ + private function isMainImage(ProductAttributeMediaGalleryEntryInterface $mediaEntry): bool + { + return in_array('image', $mediaEntry->getTypes(), true); + } + + /** + * Returns image data for swatches + * + * @param ProductAttributeMediaGalleryEntryInterface $mediaEntry + * @return array + */ + private function collectImageData(ProductAttributeMediaGalleryEntryInterface $mediaEntry): array + { + $image = $this->getAllSizeImages($mediaEntry->getFile()); + $image[ProductAttributeMediaGalleryEntryInterface::POSITION] = $mediaEntry->getPosition(); + $image['isMain'] =$this->isMainImage($mediaEntry); + return $image; + } + + /** + * Get all size images + * * @param string $imageFile * @return array */ @@ -476,6 +516,8 @@ private function setCachedSwatches(array $optionIds, array $swatches) } /** + * Add fallback options + * * @param array $fallbackValues * @param array $swatches * @return array @@ -488,6 +530,8 @@ private function addFallbackOptions(array $fallbackValues, array $swatches) && $swatches[$optionId]['type'] === $optionsArray[$currentStoreId]['type'] ) { $swatches[$optionId] = $optionsArray[$currentStoreId]; + } elseif (isset($optionsArray[$currentStoreId])) { + $swatches[$optionId] = $optionsArray[$currentStoreId]; } elseif (isset($optionsArray[self::DEFAULT_STORE_ID])) { $swatches[$optionId] = $optionsArray[self::DEFAULT_STORE_ID]; } diff --git a/app/code/Magento/Swatches/Model/Plugin/EavAttribute.php b/app/code/Magento/Swatches/Model/Plugin/EavAttribute.php index 599406f45528..ebbb1775aa7f 100644 --- a/app/code/Magento/Swatches/Model/Plugin/EavAttribute.php +++ b/app/code/Magento/Swatches/Model/Plugin/EavAttribute.php @@ -9,6 +9,7 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\InputException; use Magento\Framework\Serialize\Serializer\Json; +use Magento\Swatches\Model\ResourceModel\Swatch as SwatchResource; use Magento\Swatches\Model\Swatch; /** @@ -18,6 +19,11 @@ class EavAttribute { const DEFAULT_STORE_ID = 0; + /** + * @var SwatchResource + */ + private $swatchResource; + /** * Base option title used for string operations to detect is option already exists or new */ @@ -64,17 +70,20 @@ class EavAttribute * @param \Magento\Swatches\Model\SwatchFactory $swatchFactory * @param \Magento\Swatches\Helper\Data $swatchHelper * @param Json|null $serializer + * @param SwatchResource|null $swatchResource */ public function __construct( \Magento\Swatches\Model\ResourceModel\Swatch\CollectionFactory $collectionFactory, \Magento\Swatches\Model\SwatchFactory $swatchFactory, \Magento\Swatches\Helper\Data $swatchHelper, - Json $serializer = null + Json $serializer = null, + SwatchResource $swatchResource = null ) { $this->swatchCollectionFactory = $collectionFactory; $this->swatchFactory = $swatchFactory; $this->swatchHelper = $swatchHelper; $this->serializer = $serializer ?: ObjectManager::getInstance()->create(Json::class); + $this->swatchResource = $swatchResource ?: ObjectManager::getInstance()->create(SwatchResource::class); } /** @@ -148,6 +157,7 @@ protected function setProperOptionsArray(Attribute $attribute) * Prepare attribute for conversion from any swatch type to dropdown * * @param Attribute $attribute + * @throws \Magento\Framework\Exception\LocalizedException * @return void */ protected function convertSwatchToDropdown(Attribute $attribute) @@ -157,6 +167,8 @@ protected function convertSwatchToDropdown(Attribute $attribute) if (!empty($additionalData)) { $additionalData = $this->serializer->unserialize($additionalData); if (is_array($additionalData) && isset($additionalData[Swatch::SWATCH_INPUT_TYPE_KEY])) { + $option = $attribute->getOption() ?: []; + $this->cleanEavAttributeOptionSwatchValues($option); unset($additionalData[Swatch::SWATCH_INPUT_TYPE_KEY]); $attribute->setData('additional_data', $this->serializer->serialize($additionalData)); } @@ -235,6 +247,8 @@ protected function saveSwatchParams(Attribute $attribute) { if ($this->swatchHelper->isVisualSwatch($attribute)) { $this->processVisualSwatch($attribute); + $attributeOptions = $attribute->getOptiontext() ?: []; + $this->cleanTextSwatchValuesAfterSwitch($attributeOptions); } elseif ($this->swatchHelper->isTextSwatch($attribute)) { $this->processTextualSwatch($attribute); } @@ -267,6 +281,33 @@ protected function processVisualSwatch(Attribute $attribute) } } + /** + * Clean swatch option values after switching to the dropdown type. + * + * @param array $attributeOptions + * @param int|null $swatchType + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function cleanEavAttributeOptionSwatchValues(array $attributeOptions, int $swatchType = null) + { + if (count($attributeOptions) && isset($attributeOptions['value'])) { + $optionsIDs = array_keys($attributeOptions['value']); + + $this->swatchResource->clearSwatchOptionByOptionIdAndType($optionsIDs, $swatchType); + } + } + + /** + * Cleaning the text type of swatch option values after switching. + * + * @param array $attributeOptions + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function cleanTextSwatchValuesAfterSwitch(array $attributeOptions) + { + $this->cleanEavAttributeOptionSwatchValues($attributeOptions, Swatch::SWATCH_TYPE_TEXTUAL); + } + /** * @param string $value * @return int @@ -432,7 +473,7 @@ protected function validateOptions(Attribute $attribute) $options = $attribute->getData('optiontext'); } if ($options && !$this->isOptionsValid($options, $attribute)) { - throw new InputException(__('Admin is a required field in the each row')); + throw new InputException(__('Admin is a required field in each row')); } return true; } diff --git a/app/code/Magento/Swatches/Model/ResourceModel/Swatch.php b/app/code/Magento/Swatches/Model/ResourceModel/Swatch.php index 22eb1d4c7eef..9ad62265be21 100644 --- a/app/code/Magento/Swatches/Model/ResourceModel/Swatch.php +++ b/app/code/Magento/Swatches/Model/ResourceModel/Swatch.php @@ -7,8 +7,9 @@ namespace Magento\Swatches\Model\ResourceModel; /** - * @codeCoverageIgnore * Swatch Resource Model + * + * @codeCoverageIgnore * @api * @since 100.0.2 */ @@ -25,8 +26,10 @@ protected function _construct() } /** - * @param string $defaultValue + * Update default swatch option value. + * * @param integer $id + * @param string $defaultValue * @return void */ public function saveDefaultSwatchOption($id, $defaultValue) @@ -37,4 +40,24 @@ public function saveDefaultSwatchOption($id, $defaultValue) $this->getConnection()->update($this->getTable('eav_attribute'), $bind, $where); } } + + /** + * Cleaned swatch option values when switching to dropdown input type. + * + * @param array $optionIDs + * @param int $type + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function clearSwatchOptionByOptionIdAndType($optionIDs, $type = null) + { + if (count($optionIDs)) { + foreach ($optionIDs as $optionId) { + $where = ['option_id = ?' => $optionId]; + if ($type !== null) { + $where['type = ?'] = $type; + } + $this->getConnection()->delete($this->getMainTable(), $where); + } + } + } } diff --git a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddSwatchToProductActionGroup.xml b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddSwatchToProductActionGroup.xml index 60a8035dedec..2c91bba75fec 100644 --- a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddSwatchToProductActionGroup.xml +++ b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddSwatchToProductActionGroup.xml @@ -62,4 +62,21 @@ <seeElement selector="{{AdminMessagesSection.success}}" stepKey="seeSaveProductMessage"/> </actionGroup> + <actionGroup name="AddVisualSwatchToProductWithStorefrontConfigActionGroup" extends="AddVisualSwatchToProductActionGroup"> + <arguments> + <argument name="attribute" defaultValue="visualSwatchAttribute"/> + <argument name="option1" defaultValue="visualSwatchOption1"/> + <argument name="option2" defaultValue="visualSwatchOption2"/> + </arguments> + + <!-- Go to Storefront Properties tab --> + <click selector="{{AdminNewAttributePanel.storefrontPropertiesTab}}" stepKey="goToStorefrontPropertiesTab" after="fillDefaultStoreLabel2"/> + <waitForElementVisible selector="{{AdminNewAttributePanel.storefrontPropertiesTitle}}" stepKey="waitTabLoad" after="goToStorefrontPropertiesTab"/> + <selectOption selector="{{AdminNewAttributePanel.useInSearch}}" stepKey="switchOnUsInSearch" userInput="Yes" after="waitTabLoad"/> + <selectOption selector="{{AdminNewAttributePanel.visibleInAdvancedSearch}}" stepKey="switchOnVisibleInAdvancedSearch" userInput="Yes" after="switchOnUsInSearch"/> + <selectOption selector="{{AdminNewAttributePanel.comparableOnStorefront}}" stepKey="switchOnComparableOnStorefront" userInput="Yes" after="switchOnVisibleInAdvancedSearch"/> + <selectOption selector="{{AdminNewAttributePanel.useInLayeredNavigation}}" stepKey="selectUseInLayer" userInput="Filterable (with results)" after="switchOnComparableOnStorefront"/> + <selectOption selector="{{AdminNewAttributePanel.visibleOnCatalogPagesOnStorefront}}" stepKey="switchOnVisibleOnCatalogPagesOnStorefront" userInput="Yes" after="selectUseInLayer"/> + <selectOption selector="{{AdminNewAttributePanel.useInProductListing}}" stepKey="switchOnUsedInProductListing" userInput="Yes" after="switchOnVisibleOnCatalogPagesOnStorefront"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Swatches/Test/Mftf/Data/SwatchAttributeData.xml b/app/code/Magento/Swatches/Test/Mftf/Data/SwatchAttributeData.xml index 08e24cfeb38f..d7ee6dabce7e 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Data/SwatchAttributeData.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Data/SwatchAttributeData.xml @@ -11,5 +11,6 @@ <entity name="visualSwatchAttribute" type="SwatchAttribute"> <data key="default_label" unique="suffix">VisualSwatchAttr</data> <data key="input_type">Visual Swatch</data> + <data key="attribute_code" unique="suffix">visual_swatch_attr</data> </entity> </entities> diff --git a/app/code/Magento/Swatches/Test/Mftf/Section/AdminManageSwatchSection.xml b/app/code/Magento/Swatches/Test/Mftf/Section/AdminManageSwatchSection.xml index 25e03676f687..c3ef0a7324bf 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Section/AdminManageSwatchSection.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Section/AdminManageSwatchSection.xml @@ -15,11 +15,13 @@ <element name="addSwatchText" type="button" selector="#add_new_swatch_text_option_button"/> <element name="swatchTextByIndex" type="input" selector="input[name='swatchtext[value][option_{{index}}][0]']" parameterized="true"/> <element name="nthSwatchText" type="input" selector="#swatch-text-options-panel table tbody tr:nth-of-type({{var}}) td:nth-of-type(3) input" parameterized="true"/> + <element name="nthIsDefault" type="input" selector="(//input[@name='defaultvisual[]'])[{{var}}]" parameterized="true"/> <element name="nthSwatchAdminDescription" type="input" selector="#swatch-text-options-panel table tbody tr:nth-of-type({{var}}) td:nth-of-type(4) input" parameterized="true"/> <!-- Selector for Admin Description input where the index is zero-based --> <element name="swatchAdminDescriptionByIndex" type="input" selector="input[name='optiontext[value][option_{{index}}][0]']" parameterized="true"/> <element name="nthChooseColor" type="button" selector="#swatch-visual-options-panel table tbody tr:nth-of-type({{var}}) .swatch_row_name.colorpicker_handler" parameterized="true"/> <element name="nthUploadFile" type="button" selector="#swatch-visual-options-panel table tbody tr:nth-of-type({{var}}) .swatch_row_name.btn_choose_file_upload" parameterized="true"/> <element name="nthDelete" type="button" selector="#swatch-visual-options-panel table tbody tr:nth-of-type({{var}}) button.delete-option" parameterized="true"/> + <element name="deleteBtn" type="button" selector="#manage-options-panel:nth-of-type({{var}}) button.delete-option" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontProductInfoMainSection.xml index 415ae88fceb5..4290ddbbd8dd 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontProductInfoMainSection.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -9,10 +9,11 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontProductInfoMainSection"> - <element name="swatchOptionByLabel" type="button" selector="div.swatch-option[option-label={{opt}}]" parameterized="true"/> + <element name="swatchOptionByLabel" type="button" selector="div.swatch-option[option-label='{{opt}}']" parameterized="true"/> <element name="nthSwatchOption" type="button" selector="div.swatch-option:nth-of-type({{var}})" parameterized="true"/> <element name="selectedSwatchValue" type="text" selector="//div[contains(@class, 'swatch-attribute') and contains(., '{{attr}}')]//span[contains(@class, 'swatch-attribute-selected-option')]" parameterized="true"/> <element name="swatchAttributeOptions" type="text" selector="div.swatch-attribute-options"/> <element name="nthSwatchOptionText" type="button" selector="div.swatch-option.text:nth-of-type({{n}})" parameterized="true"/> + <element name="productSwatch" type="button" selector="//div[@class='swatch-option'][@aria-label='{{var1}}']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateImageSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateImageSwatchTest.xml index a763bda2e494..0e294153e881 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateImageSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateImageSwatchTest.xml @@ -19,10 +19,12 @@ <group value="Swatches"/> </annotations> <before> + <createData entity="ApiCategory" stepKey="createCategory"/> <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> </before> <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> </after> <!-- Begin creating a new product attribute of type "Image Swatch" --> @@ -67,6 +69,11 @@ <click selector="{{AttributePropertiesSection.AdvancedProperties}}" stepKey="expandAdvancedProperties"/> <selectOption selector="{{AttributePropertiesSection.Scope}}" userInput="1" stepKey="selectGlobalScope"/> + <scrollToTopOfPage stepKey="scrollToTabs"/> + <click selector="{{StorefrontPropertiesSection.StoreFrontPropertiesTab}}" stepKey="clickStorefrontPropertiesTab"/> + <waitForElementVisible selector="{{AdvancedAttributePropertiesSection.UseInProductListing}}" stepKey="waitForTabSwitch"/> + <selectOption selector="{{AdvancedAttributePropertiesSection.UseInProductListing}}" userInput="Yes" stepKey="useInProductListing"/> + <!-- Save the new product attribute --> <click selector="{{AttributePropertiesSection.SaveAndEdit}}" stepKey="clickSaveAndEdit1"/> <waitForElementVisible selector="{{AdminProductMessagesSection.successMessage}}" stepKey="waitForSuccess"/> @@ -97,6 +104,7 @@ <actionGroup ref="fillMainProductForm" stepKey="fillProductForm"> <argument name="product" value="BaseConfigurableProduct"/> </actionGroup> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[$$createCategory.name$$]" stepKey="fillCategory"/> <!-- Create configurations based off the Image Swatch we created earlier --> <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickCreateConfigurations"/> @@ -134,5 +142,26 @@ <expectedResult type="string">adobe-base</expectedResult> <actualResult type="string">{$grabSwatch6}</actualResult> </assertContains> + + <!-- Go to the product listing page and see text swatch options --> + <amOnPage url="$$createCategory.name$$.html" stepKey="goToCategoryPageStorefront"/> + <waitForPageLoad stepKey="waitForProductListingPage"/> + + <!-- Verify the storefront --> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.nthSwatchOption('1')}}" userInput="style" stepKey="grabSwatch7"/> + <assertContains stepKey="assertSwatch7"> + <expectedResult type="string">adobe-thumb</expectedResult> + <actualResult type="string">{$grabSwatch7}</actualResult> + </assertContains> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.nthSwatchOption('2')}}" userInput="style" stepKey="grabSwatch8"/> + <assertContains stepKey="assertSwatch8"> + <expectedResult type="string">adobe-small</expectedResult> + <actualResult type="string">{$grabSwatch8}</actualResult> + </assertContains> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.nthSwatchOption('3')}}" userInput="style" stepKey="grabSwatch9"/> + <assertContains stepKey="assertSwatch9"> + <expectedResult type="string">adobe-base</expectedResult> + <actualResult type="string">{$grabSwatch9}</actualResult> + </assertContains> </test> </tests> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateVisualSwatchWithNonValidOptionsTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateVisualSwatchWithNonValidOptionsTest.xml new file mode 100644 index 000000000000..b03f77187595 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateVisualSwatchWithNonValidOptionsTest.xml @@ -0,0 +1,112 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateVisualSwatchWithNonValidOptionsTest"> + <annotations> + <features value="Swatches"/> + <stories value="Create/configure swatches product attribute"/> + <title value="Admin should be able to create swatch product attribute"/> + <description value="Admin should be able to create swatch product attribute"/> + <severity value="BLOCKER"/> + <testCaseId value="MC-4140"/> + <group value="Swatches"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <!-- Remove attribute --> + <actionGroup ref="deleteProductAttribute" stepKey="deleteAttribute"> + <argument name="ProductAttribute" value="visualSwatchAttribute"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <amOnPage url="{{ProductAttributePage.url}}" stepKey="navigateToNewProductAttributePage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + + <!-- Set attribute properties --> + <fillField selector="{{AttributePropertiesSection.DefaultLabel}}" + userInput="{{visualSwatchAttribute.default_label}}" stepKey="fillDefaultLabel"/> + <selectOption selector="{{AttributePropertiesSection.InputType}}" + userInput="{{visualSwatchAttribute.input_type}}" stepKey="fillInputType"/> + + <!-- Set advanced attribute properties --> + <click selector="{{AdvancedAttributePropertiesSection.AdvancedAttributePropertiesSectionToggle}}" + stepKey="showAdvancedAttributePropertiesSection"/> + <waitForElementVisible selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" + stepKey="waitForSlideOut"/> + <fillField selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" + userInput="{{visualSwatchAttribute.attribute_code}}" + stepKey="fillAttributeCode"/> + + <!-- Add new swatch option without label --> + <click selector="{{AdminManageSwatchSection.addSwatch}}" stepKey="clickAddSwatch1"/> + <actionGroup ref="openSwatchMenuByIndex" stepKey="clickSwatch1"> + <argument name="index" value="0"/> + </actionGroup> + <click selector="{{AdminManageSwatchSection.nthChooseColor('1')}}" stepKey="clickChooseColor1"/> + <actionGroup ref="setColorPickerByHex" stepKey="fillHex1"> + <argument name="nthColorPicker" value="1"/> + <argument name="hexColor" value="ff0000"/> + </actionGroup> + + <!-- Save the new product attribute --> + <click selector="{{AttributePropertiesSection.Save}}" stepKey="clickSave1"/> + <waitForElementVisible selector="{{AdminProductMessagesSection.errorMessage}}" stepKey="waitForError"/> + + <!-- Fill options data --> + <fillField selector="{{AdminManageSwatchSection.adminInputByIndex('0')}}" + userInput="red" stepKey="fillAdmin1"/> + + <!-- Add 2 additional new swatch options --> + <click selector="{{AdminManageSwatchSection.addSwatch}}" stepKey="clickAddSwatch2"/> + <actionGroup ref="openSwatchMenuByIndex" stepKey="clickSwatch2"> + <argument name="index" value="1"/> + </actionGroup> + <click selector="{{AdminManageSwatchSection.nthChooseColor('2')}}" stepKey="clickChooseColor2"/> + <actionGroup ref="setColorPickerByHex" stepKey="fillHex2"> + <argument name="nthColorPicker" value="2"/> + <argument name="hexColor" value="00ff00"/> + </actionGroup> + <fillField selector="{{AdminManageSwatchSection.adminInputByIndex('1')}}" + userInput="green" stepKey="fillAdmin2"/> + + <click selector="{{AdminManageSwatchSection.addSwatch}}" stepKey="clickAddSwatch3"/> + <actionGroup ref="openSwatchMenuByIndex" stepKey="clickSwatch3"> + <argument name="index" value="2"/> + </actionGroup> + <click selector="{{AdminManageSwatchSection.nthChooseColor('3')}}" stepKey="clickChooseColor3"/> + <actionGroup ref="setColorPickerByHex" stepKey="fillHex3"> + <argument name="nthColorPicker" value="3"/> + <argument name="hexColor" value="0000ff"/> + </actionGroup> + <fillField selector="{{AdminManageSwatchSection.adminInputByIndex('2')}}" + userInput="blue" stepKey="fillAdmin3"/> + + <!-- Mark second option as default --> + <click selector="{{AdminManageSwatchSection.nthIsDefault('2')}}" stepKey="setSecondOptionAsDefault"/> + + <!-- Go to Storefront Properties tab --> + <scrollToTopOfPage stepKey="scrollToTop"/> + <click selector="{{StorefrontPropertiesSection.StoreFrontPropertiesTab}}" stepKey="goToStorefrontPropertiesTab"/> + <waitForElementVisible selector="{{StorefrontPropertiesSection.PageTitle}}" stepKey="waitTabLoad"/> + + <!-- Save the new product attribute --> + <click selector="{{AttributePropertiesSection.Save}}" stepKey="clickSave2"/> + <waitForPageLoad stepKey="waitForGridPageLoad"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" + stepKey="waitForSuccessMessage"/> + + <actionGroup ref="navigateToCreatedProductAttribute" stepKey="navigateToAttribute"> + <argument name="ProductAttribute" value="visualSwatchAttribute"/> + </actionGroup> + <!-- Check attribute data --> + <seeCheckboxIsChecked selector="{{AdminManageSwatchSection.nthIsDefault('2')}}" stepKey="CheckDefaultOption"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminWatermarkUploadTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminWatermarkUploadTest.xml new file mode 100644 index 000000000000..e9df186bae5e --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminWatermarkUploadTest.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminWatermarkUploadTest"> + <waitForElement selector="{{AdminDesignConfigSection.imageUploadInputByFieldsetName('Swatch Image')}}" stepKey="waitForInputVisible4" after="waitForPreviewImage3"/> + <attachFile selector="{{AdminDesignConfigSection.imageUploadInputByFieldsetName('Swatch Image')}}" userInput="adobe-small.jpg" stepKey="attachFile4" after="waitForInputVisible4"/> + <waitForElementVisible selector="{{AdminDesignConfigSection.imageUploadPreviewByFieldsetName('Swatch Image')}}" stepKey="waitForPreviewImage4" after="attachFile4"/> + </test> +</tests> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontDisplayAllCharactersOnTextSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontDisplayAllCharactersOnTextSwatchTest.xml new file mode 100644 index 000000000000..470421776cf8 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontDisplayAllCharactersOnTextSwatchTest.xml @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontDisplayAllCharactersOnTextSwatchTest" extends="StorefrontFilterByTextSwatchTest"> + <annotations> + <features value="Swatches"/> + <stories value="Create/configure swatches and check the display characters length"/> + <title value="Admin can create product attribute with text swatch and view the display characters in full"/> + <description value="Admin can create product attribute with text swatch and check the display characters in full"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-5975"/> + <group value="Swatches"/> + </annotations> + + <!-- Create swatch #3 --> + <click selector="{{AdminManageSwatchSection.addSwatchText}}" stepKey="clickAddSwatch2" after="fillDescription1"/> + <fillField selector="{{AdminManageSwatchSection.swatchTextByIndex('2')}}" userInput="1234567890123456789012341234" stepKey="fillSwatch2" after="clickAddSwatch2"/> + <fillField selector="{{AdminManageSwatchSection.swatchAdminDescriptionByIndex('2')}}" userInput="1234567890123456789012341234GreenD" stepKey="fillDescription2" after="fillSwatch2"/> + + <!-- Create swatch #4 --> + <click selector="{{AdminManageSwatchSection.addSwatchText}}" stepKey="clickAddSwatch3" after="fillDescription2"/> + <fillField selector="{{AdminManageSwatchSection.swatchTextByIndex('3')}}" userInput="123456789012345678901" stepKey="fillSwatch3" after="clickAddSwatch3"/> + <fillField selector="{{AdminManageSwatchSection.swatchAdminDescriptionByIndex('3')}}" userInput="123456789012345678901BrownD" stepKey="fillDescription3" after="fillSwatch3"/> + + <see selector="{{StorefrontCategorySidebarSection.attributeNthOption(ProductAttributeFrontendLabel.label, '3')}}" userInput="123456789012345678901" stepKey="seeGreen" after="seeBlue"/> + <see selector="{{StorefrontCategorySidebarSection.attributeNthOption(ProductAttributeFrontendLabel.label, '4')}}" userInput="123456789012345678901" stepKey="seeBrown" after="seeGreen"/> + + <!-- Go to the category page --> + <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage2"/> + <waitForPageLoad stepKey="waitForCategoryPage2"/> + + <!-- Verify swatch2 is present and shown in full display text characters on storefront in the layered navigation --> + <see selector="{{StorefrontCategorySidebarSection.layeredFilterBlock}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="seeAttributeInLayeredNav2"/> + <click selector="{{StorefrontCategorySidebarSection.filterOptionTitle(ProductAttributeFrontendLabel.label)}}" stepKey="expandAttribute2"/> + <click selector="{{StorefrontCategorySidebarSection.attributeNthOption(ProductAttributeFrontendLabel.label, '2')}}" stepKey="filterBySwatch2"/> + + <!-- Go to the category page --> + <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage3"/> + <waitForPageLoad stepKey="waitForCategoryPage3"/> + + <!-- Verify swatch3 is present and shown in full display text characters on storefront in the layered navigation --> + <see selector="{{StorefrontCategorySidebarSection.layeredFilterBlock}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="seeAttributeInLayeredNav3"/> + <click selector="{{StorefrontCategorySidebarSection.filterOptionTitle(ProductAttributeFrontendLabel.label)}}" stepKey="expandAttribute3"/> + <click selector="{{StorefrontCategorySidebarSection.attributeNthOption(ProductAttributeFrontendLabel.label, '3')}}" stepKey="filterBySwatch3"/> + + <!-- Go to the category page --> + <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage4"/> + <waitForPageLoad stepKey="waitForCategoryPage4"/> + + <!-- Verify swatch4 is present and shown in full display text characters on storefront in the layered navigation --> + <see selector="{{StorefrontCategorySidebarSection.layeredFilterBlock}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="seeAttributeInLayeredNav4"/> + <click selector="{{StorefrontCategorySidebarSection.filterOptionTitle(ProductAttributeFrontendLabel.label)}}" stepKey="expandAttribute4"/> + <click selector="{{StorefrontCategorySidebarSection.attributeNthOption(ProductAttributeFrontendLabel.label, '4')}}" stepKey="filterBySwatch4"/> + + <!-- Deletes the created configurable product--> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteConfigurableProduct"> + <argument name="product" value="BaseConfigurableProduct"/> + </actionGroup> + + </test> + +</tests> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByImageSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByImageSwatchTest.xml index e4c96ab3a2ba..5347a1a1f870 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByImageSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByImageSwatchTest.xml @@ -79,16 +79,25 @@ <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> <waitForPageLoad time="30" stepKey="waitForProductGrid"/> <actionGroup ref="goToCreateProductPage" stepKey="goToCreateConfigurableProduct"> - <argument name="product" value="BaseConfigurableProduct"/> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + <!-- Add image to configurable product --> + <actionGroup ref="addProductImage" stepKey="addFirstImageForProductConfigurable"> + <argument name="image" value="MagentoLogo"/> + </actionGroup> + <!-- Add image to configurable product --> + <actionGroup ref="addProductImage" stepKey="addSecondImageForProductConfigurable"> + <argument name="image" value="TestImageNew"/> </actionGroup> <actionGroup ref="fillMainProductForm" stepKey="fillProductForm"> - <argument name="product" value="BaseConfigurableProduct"/> + <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[$$createCategory.name$$]" stepKey="fillCategory"/> <!-- Create configurations based off the visual swatch we created earlier --> - <actionGroup ref="createConfigurationsForAttribute" stepKey="createConfigurations"> + <actionGroup ref="createConfigurationsForAttributeWithImages" stepKey="createConfigurations"> <argument name="attributeCode" value="{{ProductAttributeFrontendLabel.label}}"/> + <argument name="image" value="TestImageAdobe"/> </actionGroup> <!-- Go to the category page --> @@ -111,7 +120,33 @@ <!-- Click a swatch and expect to see the configurable product, not see the simple product --> <click selector="{{StorefrontCategorySidebarSection.attributeNthOption(ProductAttributeFrontendLabel.label, '1')}}" stepKey="filterBySwatch1"/> - <see selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" userInput="{{BaseConfigurableProduct.name}}" stepKey="seeConfigurableProduct"/> + <see selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" userInput="{{ApiConfigurableProduct.name}}" stepKey="seeConfigurableProduct"/> <dontSee selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" userInput="$$createSimpleProduct.name$$" stepKey="dontSeeSimpleProduct"/> + + <!-- Assert configurable product in storefront product page --> + <actionGroup ref="AssertProductInStorefrontProductPage" stepKey="AssertProductInStorefrontProductPage"> + <argument name="product" value="ApiConfigurableProduct"/> + </actionGroup> + + <!-- Assert configurable product image in storefront product page --> + <actionGroup ref="assertProductImageStorefrontProductPage" stepKey="assertProductImageStorefrontProductPage"> + <argument name="product" value="ApiConfigurableProduct"/> + <argument name="image" value="MagentoLogo"/> + </actionGroup> + + <!-- Assert configurable product image in storefront product page --> + <actionGroup ref="assertProductImageStorefrontProductPage" stepKey="assertProductSecondImageStorefrontProductPage"> + <argument name="product" value="ApiConfigurableProduct"/> + <argument name="image" value="TestImageNew"/> + </actionGroup> + + <!-- Click a swatch and expect to see the image from the swatch from the configurable product --> + <click selector="{{StorefrontProductInfoMainSection.swatchOptionByLabel('adobe-thumb')}}" stepKey="clickSwatchOption"/> + + <!-- Assert swatch option image for configurable product image in storefront product page --> + <actionGroup ref="assertProductImageStorefrontProductPage" stepKey="assertSwatchImageStorefrontProductPage"> + <argument name="product" value="ApiConfigurableProduct"/> + <argument name="image" value="TestImageAdobe"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchAttributesDisplayInWidgetCMSTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchAttributesDisplayInWidgetCMSTest.xml new file mode 100644 index 000000000000..1ab2cd793f3b --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchAttributesDisplayInWidgetCMSTest.xml @@ -0,0 +1,93 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontSwatchAttributesDisplayInWidgetCMSTest"> + <annotations> + <features value="ConfigurableProduct"/> + <title value="Swatch Attribute is not displayed in the Widget CMS"/> + <description value="Swatch Attribute is not displayed in the Widget CMS"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-96469"/> + <useCaseId value="MAGETWO-96406"/> + <group value="ConfigurableProduct"/> + <skip> + <issueId value="MQE-1424" /> + </skip> + </annotations> + + <before> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + <createData entity="NewRootCategory" stepKey="createRootCategory"/> + </before> + + <after> + <!--delete created configurable product--> + <actionGroup ref="deleteProductUsingProductGrid" stepKey="deleteProduct"> + <argument name="product" value="BaseConfigurableProduct"/> + </actionGroup> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <waitForPageLoad stepKey="waitForAdminProductGridLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> + <actionGroup ref="deleteProductAttributeByLabel" stepKey="deleteAttribute"> + <argument name="ProductAttribute" value="visualSwatchAttribute"/> + </actionGroup> + <!--delete root category--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="navigateToCategoryPage"/> + <waitForPageLoad time="30" stepKey="waitForPageCategoryLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree('$$createRootCategory.name$$')}}" stepKey="clickOnDefaultRootCategory"/> + <waitForPageLoad stepKey="waitForPageDefaultCategoryEditLoad" /> + <seeElement selector="{{AdminCategoryMainActionsSection.DeleteButton}}" stepKey="assertDeleteButtonIsPresent1"/> + <click selector="{{AdminCategoryMainActionsSection.DeleteButton}}" stepKey="DeleteDefaultRootCategory"/> + <waitForElementVisible selector="{{AdminCategoryModalSection.ok}}" stepKey="waitForModalDeleteDefaultRootCategory" /> + <click selector="{{AdminCategoryModalSection.ok}}" stepKey="acceptModal1"/> + <waitForElementVisible selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="waitForPageReloadAfterDeleteDefaultCategory"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <!--logout--> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + <!--Login--> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdmin"/> + <!--Create a configurable swatch product via the UI --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex"/> + <waitForPageLoad stepKey="waitForProductPage"/> + <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProductPage"> + <argument name="product" value="BaseConfigurableProduct"/> + </actionGroup> + <actionGroup ref="fillMainProductForm" stepKey="fillProductForm"> + <argument name="product" value="BaseConfigurableProduct"/> + </actionGroup> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[$$createRootCategory.name$$]" stepKey="searchAndSelectCategory"/> + <!--Add swatch attribute to configurable product--> + <actionGroup ref="AddVisualSwatchToProductWithStorefrontConfigActionGroup" stepKey="addSwatchToProduct"/> + + <!--Create CMS page--> + <actionGroup ref="CreateNewPageWithWidget" stepKey="createCMSPageWithWidget"> + <argument name="category" value="$$createRootCategory.name$$"/> + <argument name="condition" value="Category"/> + <argument name="widgetType" value="Catalog Products List"/> + </actionGroup> + <click selector="{{CmsNewPagePageSeoSection.header}}" stepKey="clickToExpandSEOSection"/> + <scrollTo selector="{{CmsNewPagePageSeoSection.urlKey}}" stepKey="scrollToUrlKey"/> + <grabValueFrom selector="{{CmsNewPagePageSeoSection.urlKey}}" stepKey="grabTextFromUrlKey"/> + <actionGroup ref="logout" stepKey="logout"/> + + <!--Open Storefront page for the new created page--> + <amOnPage url="{{StorefrontHomePage.url}}$grabTextFromUrlKey" stepKey="gotToCreatedCmsPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productSwatch(visualSwatchOption1.default_label)}}" stepKey="assertAddedWidgetS"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productSwatch(visualSwatchOption2.default_label)}}" stepKey="assertAddedWidgetM"/> + + <!--Login to delete CMS page--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="DeletePageByUrlKeyActionGroup" stepKey="deletePage"> + <argument name="UrlKey" value="$grabTextFromUrlKey"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Swatches/Test/Unit/Controller/Ajax/MediaTest.php b/app/code/Magento/Swatches/Test/Unit/Controller/Ajax/MediaTest.php index 7a110c63da79..5a11e2787bc6 100644 --- a/app/code/Magento/Swatches/Test/Unit/Controller/Ajax/MediaTest.php +++ b/app/code/Magento/Swatches/Test/Unit/Controller/Ajax/MediaTest.php @@ -20,6 +20,9 @@ class MediaTest extends \PHPUnit\Framework\TestCase /** @var \Magento\Catalog\Model\ProductFactory|\PHPUnit_Framework_MockObject_MockObject */ private $productModelFactoryMock; + /** @var \Magento\PageCache\Model\Config|\PHPUnit_Framework_MockObject_MockObject */ + private $config; + /** @var \Magento\Catalog\Model\Product|\PHPUnit_Framework_MockObject_MockObject */ private $productMock; @@ -29,6 +32,9 @@ class MediaTest extends \PHPUnit\Framework\TestCase /** @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject */ private $requestMock; + /** @var \Magento\Framework\App\ResponseInterface|\PHPUnit_Framework_MockObject_MockObject */ + private $responseMock; + /** @var \Magento\Framework\Controller\ResultFactory|\PHPUnit_Framework_MockObject_MockObject */ private $resultFactory; @@ -57,11 +63,20 @@ protected function setUp() \Magento\Catalog\Model\ProductFactory::class, ['create'] ); + $this->config = $this->createMock(\Magento\PageCache\Model\Config::class); + $this->config->method('getTtl')->willReturn(1); + $this->productMock = $this->createMock(\Magento\Catalog\Model\Product::class); $this->contextMock = $this->createMock(\Magento\Framework\App\Action\Context::class); $this->requestMock = $this->createMock(\Magento\Framework\App\RequestInterface::class); $this->contextMock->method('getRequest')->willReturn($this->requestMock); + $this->responseMock = $this->getMockBuilder(\Magento\Framework\App\ResponseInterface::class) + ->disableOriginalConstructor() + ->setMethods(['setPublicHeaders']) + ->getMockForAbstractClass(); + $this->responseMock->method('setPublicHeaders')->willReturnSelf(); + $this->contextMock->method('getResponse')->willReturn($this->responseMock); $this->resultFactory = $this->createPartialMock(\Magento\Framework\Controller\ResultFactory::class, ['create']); $this->contextMock->method('getResultFactory')->willReturn($this->resultFactory); @@ -73,7 +88,8 @@ protected function setUp() [ 'context' => $this->contextMock, 'swatchHelper' => $this->swatchHelperMock, - 'productModelFactory' => $this->productModelFactoryMock + 'productModelFactory' => $this->productModelFactoryMock, + 'config' => $this->config ] ); } @@ -86,6 +102,10 @@ public function testExecute() ->method('load') ->with(59) ->willReturn($this->productMock); + $this->productMock + ->expects($this->once()) + ->method('getIdentities') + ->willReturn(['tags']); $this->productModelFactoryMock ->expects($this->once()) diff --git a/app/code/Magento/Swatches/Test/Unit/Model/Plugin/EavAttributeTest.php b/app/code/Magento/Swatches/Test/Unit/Model/Plugin/EavAttributeTest.php index 21df8c795506..317ea7710722 100644 --- a/app/code/Magento/Swatches/Test/Unit/Model/Plugin/EavAttributeTest.php +++ b/app/code/Magento/Swatches/Test/Unit/Model/Plugin/EavAttributeTest.php @@ -191,7 +191,7 @@ public function testBeforeSaveTextSwatch() /** * @expectedException \Magento\Framework\Exception\InputException - * @expectedExceptionMessage Admin is a required field in the each row + * @expectedExceptionMessage Admin is a required field in each row */ public function testBeforeSaveWithFailedValidation() { diff --git a/app/code/Magento/Swatches/Test/Unit/Model/SwatchAttributesProviderTest.php b/app/code/Magento/Swatches/Test/Unit/Model/SwatchAttributesProviderTest.php index b87fd84ce892..e9f5b580204d 100644 --- a/app/code/Magento/Swatches/Test/Unit/Model/SwatchAttributesProviderTest.php +++ b/app/code/Magento/Swatches/Test/Unit/Model/SwatchAttributesProviderTest.php @@ -3,10 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Swatches\Test\Unit\Model; use Magento\ConfigurableProduct\Model\Product\Type\Configurable; -use Magento\Catalog\Model\ResourceModel\Eav\Attribute; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Swatches\Model\SwatchAttributeCodes; use Magento\Swatches\Model\SwatchAttributesProvider; @@ -35,26 +35,26 @@ class SwatchAttributesProviderTest extends \PHPUnit\Framework\TestCase private $productMock; /** - * @var SwatchAttributeType|\PHPUnit_Framework_MockObject_MockObject + * @var SwatchAttributeType | \PHPUnit_Framework_MockObject_MockObject */ - private $swatchTypeCheckerMock; + private $swatchTypeChecker; protected function setUp() { $this->typeConfigurable = $this->createPartialMock( Configurable::class, - ['getConfigurableAttributes', 'getCodes'] + ['getConfigurableAttributes', 'getCodes', 'getProductAttribute'] ); $this->swatchAttributeCodes = $this->createMock(SwatchAttributeCodes::class); $this->productMock = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['getId', 'getTypeId']); - $this->swatchTypeCheckerMock = $this->createMock(SwatchAttributeType::class); + $this->swatchTypeChecker = $this->createMock(SwatchAttributeType::class); $this->swatchAttributeProvider = (new ObjectManager($this))->getObject(SwatchAttributesProvider::class, [ 'typeConfigurable' => $this->typeConfigurable, 'swatchAttributeCodes' => $this->swatchAttributeCodes, - 'swatchTypeChecker' => $this->swatchTypeCheckerMock, + 'swatchTypeChecker' => $this->swatchTypeChecker, ]); } @@ -64,8 +64,9 @@ public function testProvide() $this->productMock->method('getTypeId') ->willReturn(Configurable::TYPE_CODE); - $productAttributeMock = $this->getMockBuilder(Attribute::class) + $attributeMock = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class) ->disableOriginalConstructor() + ->setMethods(['setStoreId', 'getData', 'setData', 'getSource', 'hasData']) ->getMock(); $configAttributeMock = $this->createPartialMock( @@ -78,7 +79,7 @@ public function testProvide() $configAttributeMock ->method('getProductAttribute') - ->willReturn($productAttributeMock); + ->willReturn($attributeMock); $this->typeConfigurable ->method('getConfigurableAttributes') @@ -90,12 +91,10 @@ public function testProvide() ->method('getCodes') ->willReturn($swatchAttributes); - $this->swatchTypeCheckerMock->expects($this->once())->method('isSwatchAttribute')->willReturn(true); + $this->swatchTypeChecker->expects($this->once())->method('isSwatchAttribute')->willReturn(true); + $result = $this->swatchAttributeProvider->provide($this->productMock); - $this->assertEquals( - [1 => $productAttributeMock], - $result - ); + $this->assertEquals([1 => $attributeMock], $result); } } diff --git a/app/code/Magento/Swatches/composer.json b/app/code/Magento/Swatches/composer.json index 0940003b6a72..09d3e05008af 100644 --- a/app/code/Magento/Swatches/composer.json +++ b/app/code/Magento/Swatches/composer.json @@ -13,6 +13,7 @@ "magento/module-configurable-product": "*", "magento/module-customer": "*", "magento/module-eav": "*", + "magento/module-page-cache": "*", "magento/module-media-storage": "*", "magento/module-store": "*", "magento/module-theme": "*" diff --git a/app/code/Magento/Swatches/etc/db_schema.xml b/app/code/Magento/Swatches/etc/db_schema.xml index c960f27b0fb5..3dafbc387649 100644 --- a/app/code/Magento/Swatches/etc/db_schema.xml +++ b/app/code/Magento/Swatches/etc/db_schema.xml @@ -20,20 +20,20 @@ <column xsi:type="smallint" name="type" padding="5" unsigned="true" nullable="false" identity="false" comment="Swatch type: 0 - text, 1 - visual color, 2 - visual image"/> <column xsi:type="varchar" name="value" nullable="true" length="255" comment="Swatch Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="swatch_id"/> </constraint> - <constraint xsi:type="foreign" name="EAV_ATTRIBUTE_OPTION_SWATCH_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="EAV_ATTRIBUTE_OPTION_SWATCH_STORE_ID_STORE_STORE_ID" table="eav_attribute_option_swatch" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="EAV_ATTR_OPT_SWATCH_OPT_ID_EAV_ATTR_OPT_OPT_ID" + <constraint xsi:type="foreign" referenceId="EAV_ATTR_OPT_SWATCH_OPT_ID_EAV_ATTR_OPT_OPT_ID" table="eav_attribute_option_swatch" column="option_id" referenceTable="eav_attribute_option" referenceColumn="option_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="EAV_ATTRIBUTE_OPTION_SWATCH_STORE_ID_OPTION_ID"> + <constraint xsi:type="unique" referenceId="EAV_ATTRIBUTE_OPTION_SWATCH_STORE_ID_OPTION_ID"> <column name="store_id"/> <column name="option_id"/> </constraint> - <index name="EAV_ATTRIBUTE_OPTION_SWATCH_SWATCH_ID" indexType="btree"> + <index referenceId="EAV_ATTRIBUTE_OPTION_SWATCH_SWATCH_ID" indexType="btree"> <column name="swatch_id"/> </index> </table> diff --git a/app/code/Magento/Swatches/i18n/de_DE.csv b/app/code/Magento/Swatches/i18n/de_DE.csv deleted file mode 100644 index f0aa34bbef26..000000000000 --- a/app/code/Magento/Swatches/i18n/de_DE.csv +++ /dev/null @@ -1,2 +0,0 @@ -"Swatch","Swatch" -"Swatches per Product","Swatches per Product" diff --git a/app/code/Magento/Swatches/i18n/en_US.csv b/app/code/Magento/Swatches/i18n/en_US.csv index 9fe2ec706757..f35e9a0a46d4 100644 --- a/app/code/Magento/Swatches/i18n/en_US.csv +++ b/app/code/Magento/Swatches/i18n/en_US.csv @@ -1,4 +1,4 @@ -"Admin is a required field in the each row","Admin is a required field in the each row" +"Admin is a required field in each row","Admin is a required field in each row" "Update Product Preview Image","Update Product Preview Image" "Filtering by this attribute will update the product image on catalog page","Filtering by this attribute will update the product image on catalog page" "Use Product Image for Swatch if Possible","Use Product Image for Swatch if Possible" diff --git a/app/code/Magento/Swatches/i18n/es_ES.csv b/app/code/Magento/Swatches/i18n/es_ES.csv deleted file mode 100644 index f0aa34bbef26..000000000000 --- a/app/code/Magento/Swatches/i18n/es_ES.csv +++ /dev/null @@ -1,2 +0,0 @@ -"Swatch","Swatch" -"Swatches per Product","Swatches per Product" diff --git a/app/code/Magento/Swatches/i18n/fr_FR.csv b/app/code/Magento/Swatches/i18n/fr_FR.csv deleted file mode 100644 index f0aa34bbef26..000000000000 --- a/app/code/Magento/Swatches/i18n/fr_FR.csv +++ /dev/null @@ -1,2 +0,0 @@ -"Swatch","Swatch" -"Swatches per Product","Swatches per Product" diff --git a/app/code/Magento/Swatches/i18n/nl_NL.csv b/app/code/Magento/Swatches/i18n/nl_NL.csv deleted file mode 100644 index f0aa34bbef26..000000000000 --- a/app/code/Magento/Swatches/i18n/nl_NL.csv +++ /dev/null @@ -1,2 +0,0 @@ -"Swatch","Swatch" -"Swatches per Product","Swatches per Product" diff --git a/app/code/Magento/Swatches/i18n/pt_BR.csv b/app/code/Magento/Swatches/i18n/pt_BR.csv deleted file mode 100644 index f0aa34bbef26..000000000000 --- a/app/code/Magento/Swatches/i18n/pt_BR.csv +++ /dev/null @@ -1,2 +0,0 @@ -"Swatch","Swatch" -"Swatches per Product","Swatches per Product" diff --git a/app/code/Magento/Swatches/i18n/zh_Hans_CN.csv b/app/code/Magento/Swatches/i18n/zh_Hans_CN.csv deleted file mode 100644 index f0aa34bbef26..000000000000 --- a/app/code/Magento/Swatches/i18n/zh_Hans_CN.csv +++ /dev/null @@ -1,2 +0,0 @@ -"Swatch","Swatch" -"Swatches per Product","Swatches per Product" diff --git a/app/code/Magento/Swatches/view/adminhtml/templates/catalog/product/attribute/text.phtml b/app/code/Magento/Swatches/view/adminhtml/templates/catalog/product/attribute/text.phtml index 8d4400b3d047..e00c41d371c9 100644 --- a/app/code/Magento/Swatches/view/adminhtml/templates/catalog/product/attribute/text.phtml +++ b/app/code/Magento/Swatches/view/adminhtml/templates/catalog/product/attribute/text.phtml @@ -21,7 +21,7 @@ $stores = $block->getStoresSortedBySortOrder(); <th class="col-draggable"></th> <th class="col-default"><span><?= $block->escapeHtml(__('Is Default')) ?></span></th> <?php foreach ($stores as $_store): ?> - <th class="col-swatch col-<%- data.id %> + <th class="col-swatch col-swatch-min-width col-<%- data.id %> <?php if ($_store->getId() == \Magento\Store\Model\Store::DEFAULT_STORE_ID): ?> _required<?php endif; ?>" colspan="2"> <span><?= $block->escapeHtml($_store->getName()) ?></span> @@ -75,7 +75,7 @@ $stores = $block->getStoresSortedBySortOrder(); </td> <?php foreach ($stores as $_store): ?> <?php $storeId = (int)$_store->getId(); ?> - <td class="col-swatch col-<%- data.id %>"> + <td class="col-swatch col-swatch-min-width col-<%- data.id %>"> <input class="input-text swatch-text-field-<?= /* @noEscape */ $storeId ?> <?php if ($storeId == \Magento\Store\Model\Store::DEFAULT_STORE_ID): ?> required-option required-unique<?php endif; ?>" @@ -83,7 +83,7 @@ $stores = $block->getStoresSortedBySortOrder(); type="text" value="<%- data.swatch<?= /* @noEscape */ $storeId ?> %>" placeholder="<?= $block->escapeHtml(__("Swatch")) ?>"/> </td> - <td class="swatch-col-<%- data.id %>"> + <td class="col-swatch-min-width swatch-col-<%- data.id %>"> <input name="optiontext[value][<%- data.id %>][<?= /* @noEscape */ $storeId ?>]" value="<%- data.store<?= /* @noEscape */ $storeId ?> %>" class="input-text<?php if ($storeId == \Magento\Store\Model\Store::DEFAULT_STORE_ID): ?> required-option<?php endif; ?>" diff --git a/app/code/Magento/Swatches/view/adminhtml/ui_component/design_config_form.xml b/app/code/Magento/Swatches/view/adminhtml/ui_component/design_config_form.xml index 1c58243be326..b38e8ecc6e20 100644 --- a/app/code/Magento/Swatches/view/adminhtml/ui_component/design_config_form.xml +++ b/app/code/Magento/Swatches/view/adminhtml/ui_component/design_config_form.xml @@ -13,7 +13,7 @@ <level>2</level> <label translate="true">Swatch Image</label> </settings> - <field name="watermark_swatch_image_image" formElement="fileUploader"> + <field name="watermark_swatch_image_image" formElement="imageUploader"> <settings> <label translate="true">Image</label> <componentType>imageUploader</componentType> 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 d170ed0345a0..b0ea10b1ed96 100644 --- a/app/code/Magento/Swatches/view/adminhtml/web/css/swatches.css +++ b/app/code/Magento/Swatches/view/adminhtml/web/css/swatches.css @@ -149,6 +149,26 @@ width: 50px; } +.col-swatch-min-width { + min-width: 65px; +} + +[class^=swatch-col], +[class^=col-]:not(.col-draggable):not(.col-default) { + min-width: 150px; +} + +#swatch-visual-options-panel, +#swatch-text-options-panel, +#manage-options-panel { + overflow: auto; + width: 100%; +} + +.data-table .col-swatch-min-width input[type="text"] { + padding: inherit; +} + .swatches-visual-col.unavailable:after { content: ''; position: absolute; diff --git a/app/code/Magento/Swatches/view/adminhtml/web/js/product-attributes.js b/app/code/Magento/Swatches/view/adminhtml/web/js/product-attributes.js index 01411523108c..f795f99e8112 100644 --- a/app/code/Magento/Swatches/view/adminhtml/web/js/product-attributes.js +++ b/app/code/Magento/Swatches/view/adminhtml/web/js/product-attributes.js @@ -16,7 +16,8 @@ define([ 'use strict'; return function (optionConfig) { - var swatchProductAttributes = { + var activePanelClass = 'selected-type-options', + swatchProductAttributes = { frontendInput: $('#frontend_input'), isFilterable: $('#is_filterable'), isFilterableInSearch: $('#is_filterable_in_search'), @@ -45,7 +46,7 @@ define([ get tabsFront() { return this.attrTabsFront.length ? this.attrTabsFront.closest('li') : $('#front_fieldset-wrapper'); }, - selectFields: ['select', 'multiselect', 'price', 'swatch_text', 'swatch_visual'], + selectFields: ['boolean', 'select', 'multiselect', 'price', 'swatch_text', 'swatch_visual'], /** * @this {swatchProductAttributes} @@ -338,6 +339,7 @@ define([ */ _showPanel: function (el) { el.closest('.fieldset').show(); + el.addClass(activePanelClass); this._render(el.attr('id')); }, @@ -347,6 +349,7 @@ define([ */ _hidePanel: function (el) { el.closest('.fieldset').hide(); + el.removeClass(activePanelClass); }, /** @@ -414,7 +417,11 @@ define([ }; $(function () { - var editForm = $('#edit_form'); + var editForm = $('#edit_form'), + swatchVisualPanel = $('#swatch-visual-options-panel'), + swatchTextPanel = $('#swatch-text-options-panel'), + tableBody = $(), + activePanel = $(); $('#frontend_input').bind('change', function () { swatchProductAttributes.bindAttributeInputType(); @@ -425,37 +432,41 @@ define([ swatchProductAttributes.bindAttributeInputType(); - // @todo: refactor collapsable component + // @todo: refactor collapsible component $('.attribute-popup .collapse, [data-role="advanced_fieldset-content"]') .collapsable() .collapse('hide'); - editForm.on('submit', function () { - var activePanel, - swatchValues = [], - swatchVisualPanel = $('#swatch-visual-options-panel'), - swatchTextPanel = $('#swatch-text-options-panel'); + editForm.on('beforeSubmit', function () { + var optionContainer, optionsValues; - activePanel = swatchTextPanel.is(':visible') ? swatchTextPanel : swatchVisualPanel; + activePanel = swatchTextPanel.hasClass(activePanelClass) ? swatchTextPanel : swatchVisualPanel; + optionContainer = activePanel.find('table tbody'); - activePanel - .find('table input') - .each(function () { - swatchValues.push(this.name + '=' + $(this).val()); - }); + if (activePanel.hasClass(activePanelClass)) { + optionsValues = $.map( + optionContainer.find('tr'), + function (row) { + return $(row).find('input, select, textarea').serialize(); + } + ); + $('<input>') + .attr({ + type: 'hidden', + name: 'serialized_options' + }) + .val(JSON.stringify(optionsValues)) + .prependTo(editForm); + } - $('<input>') - .attr({ - type: 'hidden', - name: 'serialized_swatch_values' - }) - .val(JSON.stringify(swatchValues)) - .prependTo(editForm); - - [swatchVisualPanel, swatchTextPanel].forEach(function (el) { - $(el).find('table') - .replaceWith($('<div>').text($.mage.__('Sending swatch values as package.'))); - }); + tableBody = optionContainer.detach(); + }); + + editForm.on('afterValidate.error highlight.validate', function () { + if (activePanel.hasClass(activePanelClass)) { + activePanel.find('table').append(tableBody); + $('input[name="serialized_options"]').remove(); + } }); }); diff --git a/app/code/Magento/Swatches/view/frontend/layout/catalog_widget_product_list.xml b/app/code/Magento/Swatches/view/frontend/layout/catalog_widget_product_list.xml new file mode 100644 index 000000000000..91798cbd9947 --- /dev/null +++ b/app/code/Magento/Swatches/view/frontend/layout/catalog_widget_product_list.xml @@ -0,0 +1,12 @@ +<!-- + ~ Copyright © Magento, Inc. All rights reserved. + ~ See COPYING.txt for license details. + --> + +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceBlock name="category.product.type.widget.details.renderers"> + <block class="Magento\Swatches\Block\Product\Renderer\Listing\Configurable" name="category.product.type.details.renderers.configurable" as="configurable" template="Magento_Swatches::product/listing/renderer.phtml" ifconfig="catalog/frontend/show_swatches_in_product_list"/> + </referenceBlock> + </body> +</page> \ No newline at end of file diff --git a/app/code/Magento/Swatches/view/frontend/templates/product/layered/renderer.phtml b/app/code/Magento/Swatches/view/frontend/templates/product/layered/renderer.phtml index 3492f83fd182..d817000f7bc4 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/templates/product/listing/renderer.phtml b/app/code/Magento/Swatches/view/frontend/templates/product/listing/renderer.phtml index e65345b38d9b..c30c96fc890f 100644 --- a/app/code/Magento/Swatches/view/frontend/templates/product/listing/renderer.phtml +++ b/app/code/Magento/Swatches/view/frontend/templates/product/listing/renderer.phtml @@ -21,7 +21,8 @@ $productId = $block->getProduct()->getId(); "numberToShow": <?= /* @escapeNotVerified */ $block->getNumberSwatchesPerProduct(); ?>, "jsonConfig": <?= /* @escapeNotVerified */ $block->getJsonConfig(); ?>, "jsonSwatchConfig": <?= /* @escapeNotVerified */ $block->getJsonSwatchConfig(); ?>, - "mediaCallback": "<?= /* @escapeNotVerified */ $block->getMediaCallback() ?>" + "mediaCallback": "<?= /* @escapeNotVerified */ $block->getMediaCallback() ?>", + "jsonSwatchImageSizeConfig": <?= /* @noEscape */ $block->getJsonSwatchSizeConfig() ?> } } } 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 853eba3c98df..2571c0385dab 100644 --- a/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js +++ b/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js @@ -406,7 +406,7 @@ define([ if ($widget.options.enableControlLabel) { label += '<span id="' + controlLabelId + '" class="' + classes.attributeLabelClass + '">' + - item.label + + $('<i></i>').text(item.label).html() + '</span>' + '<span class="' + classes.attributeSelectedOptionLabelClass + '"></span>'; } @@ -414,7 +414,7 @@ define([ if ($widget.inProductList) { $widget.productForm.append(input); input = ''; - listLabel = 'aria-label="' + item.label + '"'; + listLabel = 'aria-label="' + $('<i></i>').text(item.label).html() + '"'; } else { listLabel = 'aria-labelledby="' + controlLabelId + '"'; } @@ -493,7 +493,7 @@ define([ return ''; } - $.each(config.options, function () { + $.each(config.options, function (index) { var id, type, value, @@ -501,7 +501,9 @@ define([ label, width, height, - attr; + attr, + swatchImageWidth, + swatchImageHeight; if (!optionConfig.hasOwnProperty(this.id)) { return ''; @@ -509,18 +511,20 @@ define([ // Add more button if (moreLimit === countAttributes++) { - html += '<a href="#" class="' + moreClass + '">' + moreText + '</a>'; + html += '<a href="#" class="' + moreClass + '"><span>' + moreText + '</span></a>'; } id = this.id; type = parseInt(optionConfig[id].type, 10); - value = optionConfig[id].hasOwnProperty('value') ? optionConfig[id].value : ''; + value = optionConfig[id].hasOwnProperty('value') ? + $('<i></i>').text(optionConfig[id].value).html() : ''; thumb = optionConfig[id].hasOwnProperty('thumb') ? optionConfig[id].thumb : ''; width = _.has(sizeConfig, 'swatchThumb') ? sizeConfig.swatchThumb.width : 110; height = _.has(sizeConfig, 'swatchThumb') ? sizeConfig.swatchThumb.height : 90; - label = this.label ? this.label : ''; + label = this.label ? $('<i></i>').text(this.label).html() : ''; attr = ' id="' + controlId + '-item-' + id + '"' + + ' index="' + index + '"' + ' aria-checked="false"' + ' aria-describedby="' + controlId + '"' + ' tabindex="0"' + @@ -534,6 +538,9 @@ define([ ' thumb-width="' + width + '"' + ' thumb-height="' + height + '"'; + swatchImageWidth = _.has(sizeConfig, 'swatchImage') ? sizeConfig.swatchImage.width : 30; + swatchImageHeight = _.has(sizeConfig, 'swatchImage') ? sizeConfig.swatchImage.height : 20; + if (!this.hasOwnProperty('products') || this.products.length <= 0) { attr += ' option-empty="true"'; } @@ -552,7 +559,7 @@ define([ // Image html += '<div class="' + optionClass + ' image" ' + attr + ' style="background: url(' + value + ') no-repeat center; background-size: initial;width:' + - sizeConfig.swatchImage.width + 'px; height:' + sizeConfig.swatchImage.height + 'px">' + '' + + swatchImageWidth + 'px; height:' + swatchImageHeight + 'px">' + '' + '</div>'; } else if (type === 3) { // Clear @@ -679,11 +686,21 @@ define([ if (!images) { images = this.options.mediaGalleryInitial; } - - this.updateBaseImage(images, $main, !this.inProductList); + this.updateBaseImage(this._sortImages(images), $main, !this.inProductList); } }, + /** + * Sorting images array + * + * @private + */ + _sortImages: function (images) { + return _.sortBy(images, function (image) { + return image.position; + }); + }, + /** * Event for swatch options * @@ -730,6 +747,12 @@ define([ $widget._UpdatePrice(); } + $(document).trigger('updateMsrpPriceBlock', + [ + parseInt($this.attr('index'), 10) + 1, + $widget.options.jsonConfig.optionPrices + ]); + $widget._loadMedia(); $input.trigger('change'); }, @@ -902,7 +925,8 @@ define([ $productPrice = $product.find(this.options.selectorProductPrice), options = _.object(_.keys($widget.optionsMap), {}), result, - tierPriceHtml; + tierPriceHtml, + isShow; $widget.element.find('.' + $widget.options.classes.attributeClass + '[option-selected]').each(function () { var attributeId = $(this).attr('attribute-id'); @@ -919,11 +943,9 @@ define([ } ); - if (typeof result != 'undefined' && result.oldPrice.amount !== result.finalPrice.amount) { - $(this.options.slyOldPriceSelector).show(); - } else { - $(this.options.slyOldPriceSelector).hide(); - } + isShow = typeof result != 'undefined' && result.oldPrice.amount !== result.finalPrice.amount; + + $product.find(this.options.slyOldPriceSelector)[isShow ? 'show' : 'hide'](); if (typeof result != 'undefined' && result.tierPrices.length) { if (this.options.tierPriceTemplate) { @@ -968,13 +990,29 @@ define([ * @private */ _getPrices: function (newPrices, displayPrices) { - var $widget = this; + var $widget = this, + optionPriceDiff = 0, + allowedProduct, optionPrices, basePrice, optionFinalPrice; if (_.isEmpty(newPrices)) { - newPrices = $widget.options.jsonConfig.prices; + allowedProduct = this._getAllowedProductWithMinPrice(this._CalcProducts()); + optionPrices = this.options.jsonConfig.optionPrices; + basePrice = parseFloat(this.options.jsonConfig.prices.basePrice.amount); + + if (!_.isEmpty(allowedProduct)) { + optionFinalPrice = parseFloat(optionPrices[allowedProduct].finalPrice.amount); + optionPriceDiff = optionFinalPrice - basePrice; + } + + if (optionPriceDiff !== 0) { + newPrices = this.options.jsonConfig.optionPrices[allowedProduct]; + } else { + newPrices = $widget.options.jsonConfig.prices; + } } _.each(displayPrices, function (price, code) { + if (newPrices[code]) { displayPrices[code].amount = newPrices[code].amount - displayPrices[code].amount; } @@ -983,6 +1021,30 @@ define([ return displayPrices; }, + /** + * Get product with minimum price from selected options. + * + * @param {Array} allowedProducts + * @returns {String} + * @private + */ + _getAllowedProductWithMinPrice: function (allowedProducts) { + var optionPrices = this.options.jsonConfig.optionPrices, + product = {}, + optionFinalPrice, optionMinPrice; + + _.each(allowedProducts, function (allowedProduct) { + optionFinalPrice = parseFloat(optionPrices[allowedProduct].finalPrice.amount); + + if (_.isEmpty(product) || optionFinalPrice < optionMinPrice) { + optionMinPrice = optionFinalPrice; + product = allowedProduct; + } + }, this); + + return product; + }, + /** * Gets all product media and change current to the needed one * @@ -1029,12 +1091,14 @@ define([ mediaCallData.isAjax = true; $widget._XhrKiller(); $widget._EnableProductMediaLoader($this); - $widget.xhr = $.get( - $widget.options.mediaCallback, - mediaCallData, - mediaSuccessCallback, - 'json' - ).done(function () { + $widget.xhr = $.ajax({ + url: $widget.options.mediaCallback, + cache: true, + type: 'GET', + dataType: 'json', + data: mediaCallData, + success: mediaSuccessCallback + }).done(function () { $widget._XhrKiller(); }); } @@ -1159,8 +1223,8 @@ define([ updateBaseImage: function (images, context, isInProductView) { var justAnImage = images[0], initialImages = this.options.mediaGalleryInitial, - gallery = context.find(this.options.mediaGallerySelector).data('gallery'), imagesToUpdate, + gallery = context.find(this.options.mediaGallerySelector).data('gallery'), isInitial; if (isInProductView) { @@ -1172,7 +1236,15 @@ define([ } imagesToUpdate = this._setImageIndex(imagesToUpdate); - gallery.updateData(imagesToUpdate); + + if (!_.isUndefined(gallery)) { + gallery.updateData(imagesToUpdate); + } else { + context.find(this.options.mediaGallerySelector).on('gallery:loaded', function (loadedGallery) { + loadedGallery = context.find(this.options.mediaGallerySelector).data('gallery'); + loadedGallery.updateData(imagesToUpdate); + }.bind(this)); + } if (isInitial) { $(this.options.mediaGallerySelector).AddFotoramaVideoEvents(); @@ -1183,8 +1255,6 @@ define([ }); } - gallery.first(); - } else if (justAnImage && justAnImage.img) { context.find('.product-image-photo').attr('src', justAnImage.img); } diff --git a/app/code/Magento/Tax/Api/TaxClassRepositoryInterface.php b/app/code/Magento/Tax/Api/TaxClassRepositoryInterface.php index d8b251f3c8c9..f841f9c047b8 100644 --- a/app/code/Magento/Tax/Api/TaxClassRepositoryInterface.php +++ b/app/code/Magento/Tax/Api/TaxClassRepositoryInterface.php @@ -27,7 +27,7 @@ public function get($taxClassId); * Retrieve tax classes which match a specific criteria. * * This call returns an array of objects, but detailed information about each object’s attributes might not be - * included. See http://devdocs.magento.com/codelinks/attributes.html#TaxClassRepositoryInterface to + * included. See https://devdocs.magento.com/codelinks/attributes.html#TaxClassRepositoryInterface to * determine which call to use to get detailed information about all attributes for an object. * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria diff --git a/app/code/Magento/Tax/Api/TaxRateRepositoryInterface.php b/app/code/Magento/Tax/Api/TaxRateRepositoryInterface.php index 252bc0fc715f..c0f5ccd95ba9 100644 --- a/app/code/Magento/Tax/Api/TaxRateRepositoryInterface.php +++ b/app/code/Magento/Tax/Api/TaxRateRepositoryInterface.php @@ -47,7 +47,7 @@ public function deleteById($rateId); * Search TaxRates * * This call returns an array of objects, but detailed information about each object’s attributes might not be - * included. See http://devdocs.magento.com/codelinks/attributes.html#TaxRateRepositoryInterface to + * included. See https://devdocs.magento.com/codelinks/attributes.html#TaxRateRepositoryInterface to * determine which call to use to get detailed information about all attributes for an object. * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria diff --git a/app/code/Magento/Tax/Api/TaxRuleRepositoryInterface.php b/app/code/Magento/Tax/Api/TaxRuleRepositoryInterface.php index 1d69f932573b..5e045d94de45 100644 --- a/app/code/Magento/Tax/Api/TaxRuleRepositoryInterface.php +++ b/app/code/Magento/Tax/Api/TaxRuleRepositoryInterface.php @@ -55,7 +55,7 @@ public function deleteById($ruleId); * Search TaxRules * * This call returns an array of objects, but detailed information about each object’s attributes might not be - * included. See http://devdocs.magento.com/codelinks/attributes.html#TaxRuleRepositoryInterface to + * included. See https://devdocs.magento.com/codelinks/attributes.html#TaxRuleRepositoryInterface to * determine which call to use to get detailed information about all attributes for an object. * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria diff --git a/app/code/Magento/Tax/Block/Adminhtml/Rate/Toolbar/Save.php b/app/code/Magento/Tax/Block/Adminhtml/Rate/Toolbar/Save.php index 4eaaa3be8a8f..87e9d9e00606 100644 --- a/app/code/Magento/Tax/Block/Adminhtml/Rate/Toolbar/Save.php +++ b/app/code/Magento/Tax/Block/Adminhtml/Rate/Toolbar/Save.php @@ -11,6 +11,9 @@ */ namespace Magento\Tax\Block\Adminhtml\Rate\Toolbar; +/** + * Rate toolbar block + */ class Save extends \Magento\Backend\Block\Template implements \Magento\Backend\Block\Widget\ContainerInterface { /** @@ -46,6 +49,8 @@ public function __construct( } /** + * Init model + * * @return void */ protected function _construct() @@ -97,6 +102,8 @@ public function updateButton($buttonId, $key, $data) } /** + * Prepare layout + * * @return $this */ protected function _prepareLayout() @@ -115,7 +122,7 @@ protected function _prepareLayout() ['label' => __('Reset'), 'onclick' => 'window.location.reload()', 'class' => 'reset'] ); - $rate = intval($this->getRequest()->getParam('rate')); + $rate = (int)$this->getRequest()->getParam('rate'); if ($rate) { $this->buttonList->add( 'delete', diff --git a/app/code/Magento/Tax/Controller/Adminhtml/Rate/Index.php b/app/code/Magento/Tax/Controller/Adminhtml/Rate/Index.php index b91eef7b6f70..565af0e0bf93 100644 --- a/app/code/Magento/Tax/Controller/Adminhtml/Rate/Index.php +++ b/app/code/Magento/Tax/Controller/Adminhtml/Rate/Index.php @@ -6,7 +6,9 @@ */ namespace Magento\Tax\Controller\Adminhtml\Rate; -class Index extends \Magento\Tax\Controller\Adminhtml\Rate +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\Tax\Controller\Adminhtml\Rate implements HttpGetActionInterface { /** * Show Main Grid diff --git a/app/code/Magento/Tax/Controller/Adminhtml/Rule/Delete.php b/app/code/Magento/Tax/Controller/Adminhtml/Rule/Delete.php index 71b6d7bf3939..1e46f0ea3d24 100644 --- a/app/code/Magento/Tax/Controller/Adminhtml/Rule/Delete.php +++ b/app/code/Magento/Tax/Controller/Adminhtml/Rule/Delete.php @@ -6,9 +6,10 @@ */ namespace Magento\Tax\Controller\Adminhtml\Rule; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\Controller\ResultFactory; -class Delete extends \Magento\Tax\Controller\Adminhtml\Rule +class Delete extends \Magento\Tax\Controller\Adminhtml\Rule implements HttpPostActionInterface { /** * @return \Magento\Backend\Model\View\Result\Redirect diff --git a/app/code/Magento/Tax/Controller/Adminhtml/Rule/Edit.php b/app/code/Magento/Tax/Controller/Adminhtml/Rule/Edit.php index dc0f51880252..740d7e8afe62 100644 --- a/app/code/Magento/Tax/Controller/Adminhtml/Rule/Edit.php +++ b/app/code/Magento/Tax/Controller/Adminhtml/Rule/Edit.php @@ -6,9 +6,10 @@ */ namespace Magento\Tax\Controller\Adminhtml\Rule; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Framework\Controller\ResultFactory; -class Edit extends \Magento\Tax\Controller\Adminhtml\Rule +class Edit extends \Magento\Tax\Controller\Adminhtml\Rule implements HttpGetActionInterface { /** * @return \Magento\Backend\Model\View\Result\Page|\Magento\Backend\Model\View\Result\Redirect diff --git a/app/code/Magento/Tax/Controller/Adminhtml/Rule/Index.php b/app/code/Magento/Tax/Controller/Adminhtml/Rule/Index.php index ddf6099a4f83..fd774777e3ef 100644 --- a/app/code/Magento/Tax/Controller/Adminhtml/Rule/Index.php +++ b/app/code/Magento/Tax/Controller/Adminhtml/Rule/Index.php @@ -6,7 +6,9 @@ */ namespace Magento\Tax\Controller\Adminhtml\Rule; -class Index extends \Magento\Tax\Controller\Adminhtml\Rule +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\Tax\Controller\Adminhtml\Rule implements HttpGetActionInterface { /** * @return \Magento\Backend\Model\View\Result\Page diff --git a/app/code/Magento/Tax/Helper/Data.php b/app/code/Magento/Tax/Helper/Data.php index 1a531858797a..0e950460a26d 100644 --- a/app/code/Magento/Tax/Helper/Data.php +++ b/app/code/Magento/Tax/Helper/Data.php @@ -733,7 +733,7 @@ protected function calculateTaxForItems(EntityInterface $order, EntityInterface $orderItemId = $orderItem->getId(); $orderItemTax = $orderItem->getTaxAmount(); $itemTax = $item->getTaxAmount(); - if (!$itemTax || !floatval($orderItemTax)) { + if (!$itemTax || !(float)$orderItemTax) { continue; } //An invoiced item or credit memo item can have a different qty than its order item qty @@ -761,7 +761,7 @@ protected function calculateTaxForItems(EntityInterface $order, EntityInterface $shippingTaxAmount = $salesItem->getShippingTaxAmount(); $originalShippingTaxAmount = $order->getShippingTaxAmount(); if ($shippingTaxAmount && $originalShippingTaxAmount && - $shippingTaxAmount != 0 && floatval($originalShippingTaxAmount) + $shippingTaxAmount != 0 && (float)$originalShippingTaxAmount ) { //An invoice or credit memo can have a different qty than its order $shippingRatio = $shippingTaxAmount / $originalShippingTaxAmount; diff --git a/app/code/Magento/Tax/Model/Calculation/AbstractAggregateCalculator.php b/app/code/Magento/Tax/Model/Calculation/AbstractAggregateCalculator.php index bad64260cf58..939facd02c02 100644 --- a/app/code/Magento/Tax/Model/Calculation/AbstractAggregateCalculator.php +++ b/app/code/Magento/Tax/Model/Calculation/AbstractAggregateCalculator.php @@ -7,6 +7,9 @@ use Magento\Tax\Api\Data\QuoteDetailsItemInterface; +/** + * Abstract aggregate calculator. + */ abstract class AbstractAggregateCalculator extends AbstractCalculator { /** @@ -106,11 +109,12 @@ protected function calculateWithTaxNotInPrice(QuoteDetailsItemInterface $item, $ $rowTaxes = []; $rowTaxesBeforeDiscount = []; $appliedTaxes = []; + $rowTotalForTaxCalculation = $this->getPriceForTaxCalculation($item, $price) * $quantity; //Apply each tax rate separately foreach ($appliedRates as $appliedRate) { $taxId = $appliedRate['id']; $taxRate = $appliedRate['percent']; - $rowTaxPerRate = $this->calculationTool->calcTaxAmount($rowTotal, $taxRate, false, false); + $rowTaxPerRate = $this->calculationTool->calcTaxAmount($rowTotalForTaxCalculation, $taxRate, false, false); $deltaRoundingType = self::KEY_REGULAR_DELTA_ROUNDING; if ($applyTaxAfterDiscount) { $deltaRoundingType = self::KEY_TAX_BEFORE_DISCOUNT_DELTA_ROUNDING; @@ -121,7 +125,10 @@ protected function calculateWithTaxNotInPrice(QuoteDetailsItemInterface $item, $ //Handle discount if ($applyTaxAfterDiscount) { //TODO: handle originalDiscountAmount - $taxableAmount = max($rowTotal - $discountAmount, 0); + $taxableAmount = max($rowTotalForTaxCalculation - $discountAmount, 0); + if ($taxableAmount && !$applyTaxAfterDiscount) { + $taxableAmount = $rowTotalForTaxCalculation; + } $rowTaxAfterDiscount = $this->calculationTool->calcTaxAmount( $taxableAmount, $taxRate, @@ -168,6 +175,26 @@ protected function calculateWithTaxNotInPrice(QuoteDetailsItemInterface $item, $ ->setAppliedTaxes($appliedTaxes); } + /** + * Get price for tax calculation. + * + * @param QuoteDetailsItemInterface $item + * @param float $price + * @return float + */ + private function getPriceForTaxCalculation(QuoteDetailsItemInterface $item, float $price) + { + if ($item->getExtensionAttributes() && $item->getExtensionAttributes()->getPriceForTaxCalculation()) { + $priceForTaxCalculation = $this->calculationTool->round( + $item->getExtensionAttributes()->getPriceForTaxCalculation() + ); + } else { + $priceForTaxCalculation = $price; + } + + return $priceForTaxCalculation; + } + /** * Round amount * diff --git a/app/code/Magento/Tax/Model/ResourceModel/Report/Tax/Createdat.php b/app/code/Magento/Tax/Model/ResourceModel/Report/Tax/Createdat.php index ebf699db552d..60cb6fe2898a 100644 --- a/app/code/Magento/Tax/Model/ResourceModel/Report/Tax/Createdat.php +++ b/app/code/Magento/Tax/Model/ResourceModel/Report/Tax/Createdat.php @@ -11,6 +11,9 @@ */ namespace Magento\Tax\Model\ResourceModel\Report\Tax; +/** + * Class for tax report resource model with aggregation by created at + */ class Createdat extends \Magento\Reports\Model\ResourceModel\Report\AbstractReport { /** @@ -84,7 +87,7 @@ protected function _aggregateByOrder($aggregationField, $from, $to) 'order_status' => 'e.status', 'percent' => 'MAX(tax.' . $connection->quoteIdentifier('percent') . ')', 'orders_count' => 'COUNT(DISTINCT e.entity_id)', - 'tax_base_amount_sum' => 'SUM(tax.base_amount * e.base_to_global_rate)', + 'tax_base_amount_sum' => 'SUM(tax.base_real_amount * e.base_to_global_rate)', ]; $select = $connection->select()->from( diff --git a/app/code/Magento/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php b/app/code/Magento/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php index 0901e1b7bc78..77b3cfa3a08b 100644 --- a/app/code/Magento/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php +++ b/app/code/Magento/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php @@ -15,12 +15,17 @@ use Magento\Quote\Model\Quote\Item\AbstractItem; use Magento\Store\Model\Store; use Magento\Tax\Api\Data\QuoteDetailsInterfaceFactory; +use Magento\Tax\Api\Data\QuoteDetailsItemInterface; use Magento\Tax\Api\Data\TaxClassKeyInterfaceFactory; use Magento\Tax\Api\Data\TaxClassKeyInterface; use Magento\Tax\Api\Data\TaxDetailsInterface; use Magento\Tax\Api\Data\TaxDetailsItemInterface; use Magento\Tax\Api\Data\QuoteDetailsInterface; use Magento\Quote\Api\Data\ShippingAssignmentInterface; +use Magento\Tax\Helper\Data as TaxHelper; +use Magento\Framework\App\ObjectManager; +use Magento\Tax\Api\Data\QuoteDetailsItemExtensionInterface; +use Magento\Tax\Api\Data\QuoteDetailsItemExtensionInterfaceFactory; /** * Tax totals calculation model @@ -129,6 +134,16 @@ class CommonTaxCollector extends AbstractTotal */ protected $quoteDetailsItemDataObjectFactory; + /** + * @var TaxHelper + */ + private $taxHelper; + + /** + * @var QuoteDetailsItemExtensionInterfaceFactory + */ + private $quoteDetailsItemExtensionFactory; + /** * Class constructor * @@ -139,6 +154,8 @@ class CommonTaxCollector extends AbstractTotal * @param \Magento\Tax\Api\Data\TaxClassKeyInterfaceFactory $taxClassKeyDataObjectFactory * @param CustomerAddressFactory $customerAddressFactory * @param CustomerAddressRegionFactory $customerAddressRegionFactory + * @param TaxHelper|null $taxHelper + * @param QuoteDetailsItemExtensionInterfaceFactory|null $quoteDetailsItemExtensionInterfaceFactory */ public function __construct( \Magento\Tax\Model\Config $taxConfig, @@ -147,7 +164,9 @@ public function __construct( \Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory $quoteDetailsItemDataObjectFactory, \Magento\Tax\Api\Data\TaxClassKeyInterfaceFactory $taxClassKeyDataObjectFactory, CustomerAddressFactory $customerAddressFactory, - CustomerAddressRegionFactory $customerAddressRegionFactory + CustomerAddressRegionFactory $customerAddressRegionFactory, + TaxHelper $taxHelper = null, + QuoteDetailsItemExtensionInterfaceFactory $quoteDetailsItemExtensionInterfaceFactory = null ) { $this->taxCalculationService = $taxCalculationService; $this->quoteDetailsDataObjectFactory = $quoteDetailsDataObjectFactory; @@ -156,6 +175,9 @@ public function __construct( $this->quoteDetailsItemDataObjectFactory = $quoteDetailsItemDataObjectFactory; $this->customerAddressFactory = $customerAddressFactory; $this->customerAddressRegionFactory = $customerAddressRegionFactory; + $this->taxHelper = $taxHelper ?: ObjectManager::getInstance()->get(TaxHelper::class); + $this->quoteDetailsItemExtensionFactory = $quoteDetailsItemExtensionInterfaceFactory ?: + ObjectManager::getInstance()->get(QuoteDetailsItemExtensionInterfaceFactory::class); } /** @@ -186,7 +208,7 @@ public function mapAddress(QuoteAddress $address) * @param bool $priceIncludesTax * @param bool $useBaseCurrency * @param string $parentCode - * @return \Magento\Tax\Api\Data\QuoteDetailsItemInterface + * @return QuoteDetailsItemInterface */ public function mapItem( \Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory $itemDataObjectFactory, @@ -199,7 +221,7 @@ public function mapItem( $sequence = 'sequence-' . $this->getNextIncrement(); $item->setTaxCalculationItemId($sequence); } - /** @var \Magento\Tax\Api\Data\QuoteDetailsItemInterface $itemDataObject */ + /** @var QuoteDetailsItemInterface $itemDataObject */ $itemDataObject = $itemDataObjectFactory->create(); $itemDataObject->setCode($item->getTaxCalculationItemId()) ->setQuantity($item->getQty()) @@ -215,12 +237,28 @@ public function mapItem( if (!$item->getBaseTaxCalculationPrice()) { $item->setBaseTaxCalculationPrice($item->getBaseCalculationPriceOriginal()); } + + if ($this->taxHelper->applyTaxOnOriginalPrice()) { + $baseTaxCalculationPrice = $item->getBaseOriginalPrice(); + } else { + $baseTaxCalculationPrice = $item->getBaseCalculationPriceOriginal(); + } + $this->setPriceForTaxCalculation($itemDataObject, (float)$baseTaxCalculationPrice); + $itemDataObject->setUnitPrice($item->getBaseTaxCalculationPrice()) ->setDiscountAmount($item->getBaseDiscountAmount()); } else { if (!$item->getTaxCalculationPrice()) { $item->setTaxCalculationPrice($item->getCalculationPriceOriginal()); } + + if ($this->taxHelper->applyTaxOnOriginalPrice()) { + $taxCalculationPrice = $item->getOriginalPrice(); + } else { + $taxCalculationPrice = $item->getCalculationPriceOriginal(); + } + $this->setPriceForTaxCalculation($itemDataObject, (float)$taxCalculationPrice); + $itemDataObject->setUnitPrice($item->getTaxCalculationPrice()) ->setDiscountAmount($item->getDiscountAmount()); } @@ -230,6 +268,23 @@ public function mapItem( return $itemDataObject; } + /** + * Set price for tax calculation. + * + * @param QuoteDetailsItemInterface $quoteDetailsItem + * @param float $taxCalculationPrice + * @return void + */ + private function setPriceForTaxCalculation(QuoteDetailsItemInterface $quoteDetailsItem, float $taxCalculationPrice) + { + $extensionAttributes = $quoteDetailsItem->getExtensionAttributes(); + if (!$extensionAttributes) { + $extensionAttributes = $this->quoteDetailsItemExtensionFactory->create(); + } + $extensionAttributes->setPriceForTaxCalculation($taxCalculationPrice); + $quoteDetailsItem->setExtensionAttributes($extensionAttributes); + } + /** * Map item extra taxables * @@ -237,7 +292,7 @@ public function mapItem( * @param AbstractItem $item * @param bool $priceIncludesTax * @param bool $useBaseCurrency - * @return \Magento\Tax\Api\Data\QuoteDetailsItemInterface[] + * @return QuoteDetailsItemInterface[] */ public function mapItemExtraTaxables( \Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory $itemDataObjectFactory, @@ -260,7 +315,7 @@ public function mapItemExtraTaxables( } else { $unitPrice = $extraTaxable[self::KEY_ASSOCIATED_TAXABLE_UNIT_PRICE]; } - /** @var \Magento\Tax\Api\Data\QuoteDetailsItemInterface $itemDataObject */ + /** @var QuoteDetailsItemInterface $itemDataObject */ $itemDataObject = $itemDataObjectFactory->create(); $itemDataObject->setCode($extraTaxable[self::KEY_ASSOCIATED_TAXABLE_CODE]) ->setType($extraTaxable[self::KEY_ASSOCIATED_TAXABLE_TYPE]) @@ -283,9 +338,9 @@ public function mapItemExtraTaxables( * Add quote items * * @param ShippingAssignmentInterface $shippingAssignment - * @param bool $useBaseCurrency * @param bool $priceIncludesTax - * @return \Magento\Tax\Api\Data\QuoteDetailsItemInterface[] + * @param bool $useBaseCurrency + * @return QuoteDetailsItemInterface[] */ public function mapItems( ShippingAssignmentInterface $shippingAssignment, @@ -293,7 +348,7 @@ public function mapItems( $useBaseCurrency ) { $items = $shippingAssignment->getItems(); - if (!count($items)) { + if (empty($items)) { return []; } @@ -361,10 +416,12 @@ public function populateAddressData(QuoteDetailsInterface $quoteDetails, QuoteAd } /** + * Get shipping data object. + * * @param ShippingAssignmentInterface $shippingAssignment * @param QuoteAddress\Total $total * @param bool $useBaseCurrency - * @return \Magento\Tax\Api\Data\QuoteDetailsItemInterface + * @return QuoteDetailsItemInterface */ public function getShippingDataObject( ShippingAssignmentInterface $shippingAssignment, @@ -379,7 +436,7 @@ public function getShippingDataObject( $total->setBaseShippingTaxCalculationAmount($total->getBaseShippingAmount()); } if ($total->getShippingTaxCalculationAmount() !== null) { - /** @var \Magento\Tax\Api\Data\QuoteDetailsItemInterface $itemDataObject */ + /** @var QuoteDetailsItemInterface $itemDataObject */ $itemDataObject = $this->quoteDetailsItemDataObjectFactory->create() ->setType(self::ITEM_TYPE_SHIPPING) ->setCode(self::ITEM_CODE_SHIPPING) @@ -414,14 +471,14 @@ public function getShippingDataObject( * Populate QuoteDetails object from quote address object * * @param ShippingAssignmentInterface $shippingAssignment - * @param \Magento\Tax\Api\Data\QuoteDetailsItemInterface[] $itemDataObjects + * @param QuoteDetailsItemInterface[] $itemDataObjects * @return \Magento\Tax\Api\Data\QuoteDetailsInterface */ protected function prepareQuoteDetails(ShippingAssignmentInterface $shippingAssignment, $itemDataObjects) { $items = $shippingAssignment->getItems(); $address = $shippingAssignment->getShipping()->getAddress(); - if (!count($items)) { + if (empty($items)) { return $this->quoteDetailsDataObjectFactory->create(); } @@ -543,6 +600,7 @@ protected function processProductItems( * Process applied taxes for items and quote * * @param QuoteAddress\Total $total + * @param ShippingAssignmentInterface $shippingAssignment * @param array $itemsByType * @return $this */ @@ -630,6 +688,9 @@ public function updateItemTaxInfo($quoteItem, $itemTaxDetails, $baseItemTaxDetai { //The price should be base price $quoteItem->setPrice($baseItemTaxDetails->getPrice()); + if ($quoteItem->getCustomPrice() && $this->taxHelper->applyTaxOnCustomPrice()) { + $quoteItem->setCustomPrice($baseItemTaxDetails->getPrice()); + } $quoteItem->setConvertedPrice($itemTaxDetails->getPrice()); $quoteItem->setPriceInclTax($itemTaxDetails->getPriceInclTax()); $quoteItem->setRowTotal($itemTaxDetails->getRowTotal()); @@ -846,8 +907,9 @@ protected function saveAppliedTaxes() } /** - * Increment and return counter. This function is intended to be used to generate temporary - * id for an item. + * Increment and return counter. + * + * This function is intended to be used to generate temporary id for an item. * * @return int */ diff --git a/app/code/Magento/Tax/Model/Sales/Total/Quote/Tax.php b/app/code/Magento/Tax/Model/Sales/Total/Quote/Tax.php index 4aea7ab4c5a7..52061fd5d388 100755 --- a/app/code/Magento/Tax/Model/Sales/Total/Quote/Tax.php +++ b/app/code/Magento/Tax/Model/Sales/Total/Quote/Tax.php @@ -265,7 +265,7 @@ protected function processExtraTaxables(Address\Total $total, array $itemsByType { $extraTaxableDetails = []; foreach ($itemsByType as $itemType => $itemTaxDetails) { - if ($itemType != self::ITEM_TYPE_PRODUCT and $itemType != self::ITEM_TYPE_SHIPPING) { + if ($itemType != self::ITEM_TYPE_PRODUCT && $itemType != self::ITEM_TYPE_SHIPPING) { foreach ($itemTaxDetails as $itemCode => $itemTaxDetail) { /** @var \Magento\Tax\Api\Data\TaxDetailsInterface $taxDetails */ $taxDetails = $itemTaxDetail[self::KEY_ITEM]; @@ -408,6 +408,7 @@ protected function enhanceTotalData( /** * Process model configuration array. + * * This method can be used for changing totals collect sort order * * @param array $config diff --git a/app/code/Magento/Tax/Plugin/Checkout/CustomerData/Cart.php b/app/code/Magento/Tax/Plugin/Checkout/CustomerData/Cart.php index 87f65ef311ac..208833733ae3 100644 --- a/app/code/Magento/Tax/Plugin/Checkout/CustomerData/Cart.php +++ b/app/code/Magento/Tax/Plugin/Checkout/CustomerData/Cart.php @@ -6,6 +6,10 @@ namespace Magento\Tax\Plugin\Checkout\CustomerData; +/** + * Process quote items price, considering tax configuration. + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ class Cart { /** @@ -68,6 +72,16 @@ public function afterGetSectionData(\Magento\Checkout\CustomerData\Cart $subject $this->itemPriceRenderer->setItem($item); $this->itemPriceRenderer->setTemplate('checkout/cart/item/price/sidebar.phtml'); $result['items'][$key]['product_price']=$this->itemPriceRenderer->toHtml(); + if ($this->itemPriceRenderer->displayPriceExclTax()) { + $result['items'][$key]['product_price_value'] = $item->getCalculationPrice(); + } elseif ($this->itemPriceRenderer->displayPriceInclTax()) { + $result['items'][$key]['product_price_value'] = $item->getPriceInclTax(); + } elseif ($this->itemPriceRenderer->displayBothPrices()) { + //unset product price value in case price already has been set as scalar value. + unset($result['items'][$key]['product_price_value']); + $result['items'][$key]['product_price_value']['incl_tax'] = $item->getPriceInclTax(); + $result['items'][$key]['product_price_value']['excl_tax'] = $item->getCalculationPrice(); + } } } } diff --git a/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminCustomerTaxClassActionGroup.xml b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminCustomerTaxClassActionGroup.xml new file mode 100644 index 000000000000..04d497b4b524 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminCustomerTaxClassActionGroup.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"> + <!--Add Customer Tax Class--> + <actionGroup name="addCustomerTaxClass"> + <arguments> + <argument name="customerTaxClassName" type="string"/> + </arguments> + <!--Click Additional Settings--> + <click stepKey="clickAdditionalSettings" selector="{{AdminTaxRulesSection.additionalSettings}}"/> + <!--Click Product Add New Tax Class Button--> + <click stepKey="clickCustomerAddNewTaxClassBtn" selector="{{AdminTaxRulesSection.customerAddNewTaxClass}}"/> + <!--Fill field--> + <fillField stepKey="fillCustomerNewTaxClass" selector="{{AdminTaxRulesSection.fieldCustomerNewTaxClass}}" userInput="{{customerTaxClassName}}"/> + <!-- Save Product tax rate --> + <click stepKey="saveProdTaxRate" selector="{{AdminTaxRulesSection.saveCustomerNewTaxClass}}"/> + </actionGroup> + + <!--Delete Product Tax Class--> + <actionGroup name="deleteCustomerTaxClass"> + <arguments> + <argument name="taxClassName" type="string"/> + </arguments> + <!-- Go to tax rule page --> + <amOnPage url="{{AdminNewTaxRulePage.url}}" stepKey="goToNewTaxRulePage"/> + <waitForPageLoad stepKey="waitForTaxRatePage"/> + <click stepKey="clickAdditionalSettings" selector="{{AdminTaxRulesSection.additionalSettings}}"/> + <scrollTo stepKey="scrollToAdditionalSettings" selector="{{AdminTaxRulesSection.additionalSettings}}"/> + <moveMouseOver stepKey="hoverDeleteElement" selector="{{AdminTaxRulesSection.deleteTaxClassName(taxClassName)}}"/> + <click stepKey="deleteFirstTaxClass" selector="{{AdminTaxRulesSection.deleteTaxClass(taxClassName)}}"/> + <waitForElementVisible selector="{{AdminTaxRulesSection.popUpDialogOK}}" stepKey="waitForElementBecomeVisible"/> + <click stepKey="acceptPopUpDialog" selector="{{AdminTaxRulesSection.popUpDialogOK}}"/> + <waitForPageLoad stepKey="waitForProductTaxClassDeleted"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminDeleteTaxRuleActionGroup.xml b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminDeleteTaxRuleActionGroup.xml new file mode 100644 index 000000000000..f6b2b7a4785a --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminDeleteTaxRuleActionGroup.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminDeleteTaxRule"> + <arguments> + <argument name="taxRuleCode" type="string" /> + </arguments> + <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleGridPage"/> + <waitForPageLoad stepKey="waitForPageLoad1" /> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters"/> + <fillField selector="{{AdminTaxRuleGridSection.code}}" userInput="{{taxRuleCode}}" stepKey="fillTaxRuleCode"/> + <click selector="{{AdminTaxRuleGridSection.search}}" stepKey="clickSearch"/> + <waitForPageLoad stepKey="waitForTaxRuleSearch" /> + <click selector="{{AdminTaxRuleGridSection.nthRow('1')}}" stepKey="clickFirstRow"/> + <waitForPageLoad stepKey="waitForPageLoad" /> + <click selector="{{AdminTaxRuleFormSection.deleteRule}}" stepKey="clickDeleteRule"/> + <click selector="{{AdminTaxRuleFormSection.ok}}" stepKey="clickOk"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminTaxActionGroup.xml b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminTaxActionGroup.xml index 6c535e3004e6..3986ede9acf6 100644 --- a/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminTaxActionGroup.xml +++ b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminTaxActionGroup.xml @@ -15,13 +15,19 @@ <waitForPageLoad stepKey="waitForTaxConfigLoad"/> <!-- change the default state to California --> - <conditionalClick stepKey="clickCalculationSettings" selector="{{AdminConfigureTaxSection.defaultDestination}}" dependentSelector="{{AdminConfigureTaxSection.systemValueDefaultState}}" visible="false" /> + <scrollTo selector="#tax_defaults-head" x="0" y="-80" stepKey="scrollToTaxDefaults" /> + <!-- conditionalClick twice to fix some flaky behavior --> + <conditionalClick stepKey="clickCalculationSettings" selector="{{AdminConfigureTaxSection.defaultDestination}}" dependentSelector="#tax_defaults" visible="false" /> + <conditionalClick stepKey="clickCalculationSettingsAgain" selector="{{AdminConfigureTaxSection.defaultDestination}}" dependentSelector="#tax_defaults" visible="false" /> <uncheckOption stepKey="clickDefaultState" selector="{{AdminConfigureTaxSection.systemValueDefaultState}}"/> <selectOption stepKey="selectDefaultState" selector="{{AdminConfigureTaxSection.dropdownDefaultState}}" userInput="California"/> <fillField stepKey="fillDefaultPostCode" selector="{{AdminConfigureTaxSection.defaultPostCode}}" userInput="*"/> <!-- change the options for shopping cart display to show tax --> - <conditionalClick stepKey="clickShoppingCartDisplaySettings" selector="{{AdminConfigureTaxSection.shoppingCartDisplay}}" dependentSelector="{{AdminConfigureTaxSection.systemValueIncludeTaxTotalCart}}" visible="false"/> + <scrollTo selector="#tax_cart_display-head" x="0" y="-80" stepKey="scrollToTaxShoppingCartDisplay" /> + <!-- conditionalClick twice to fix some flaky behavior --> + <conditionalClick stepKey="clickShoppingCartDisplaySettings" selector="{{AdminConfigureTaxSection.shoppingCartDisplay}}" dependentSelector="#tax_cart_display" visible="false"/> + <conditionalClick stepKey="clickShoppingCartDisplaySettingsAgain" selector="{{AdminConfigureTaxSection.shoppingCartDisplay}}" dependentSelector="#tax_cart_display" visible="false"/> <uncheckOption stepKey="clickTaxTotalCart" selector="{{AdminConfigureTaxSection.systemValueIncludeTaxTotalCart}}"/> <selectOption stepKey="selectTaxTotalCart" selector="{{AdminConfigureTaxSection.dropdownIncludeTaxTotalCart}}" userInput="Yes"/> <uncheckOption stepKey="clickDisplayTaxSummaryCart" selector="{{AdminConfigureTaxSection.systemValueDisplayTaxSummaryCart}}"/> @@ -30,7 +36,10 @@ <selectOption stepKey="selectDisplayZeroTaxCart" selector="{{AdminConfigureTaxSection.dropdownDisplayZeroTaxCart}}" userInput="Yes"/> <!-- change the options for orders, invoices, credit memos display to show tax --> - <conditionalClick stepKey="clickOrdersInvoicesCreditSales" selector="{{AdminConfigureTaxSection.ordersInvoicesCreditSales}}" dependentSelector="{{AdminConfigureTaxSection.systemValueIncludeTaxTotalSales}}" visible="false"/> + <scrollTo selector="#tax_sales_display-head" x="0" y="-80" stepKey="scrollToTaxSalesDisplay" /> + <!-- conditionalClick twice to fix some flaky behavior --> + <conditionalClick stepKey="clickOrdersInvoicesCreditSales" selector="{{AdminConfigureTaxSection.ordersInvoicesCreditSales}}" dependentSelector="#tax_sales_display" visible="false"/> + <conditionalClick stepKey="clickOrdersInvoicesCreditSalesAgain" selector="{{AdminConfigureTaxSection.ordersInvoicesCreditSales}}" dependentSelector="#tax_sales_display" visible="false"/> <uncheckOption stepKey="clickTaxTotalSales" selector="{{AdminConfigureTaxSection.systemValueIncludeTaxTotalSales}}"/> <selectOption stepKey="selectTaxTotalSales" selector="{{AdminConfigureTaxSection.dropdownIncludeTaxTotalSales}}" userInput="Yes"/> <uncheckOption stepKey="clickDisplayTaxSummarySales" selector="{{AdminConfigureTaxSection.systemValueDisplayTaxSummarySales}}"/> @@ -51,13 +60,17 @@ <waitForPageLoad stepKey="waitForTaxConfigLoad"/> <!-- change the default state to none --> + <!-- conditionalClick twice to fix some flaky behavior --> <conditionalClick stepKey="clickCalculationSettings" selector="{{AdminConfigureTaxSection.defaultDestination}}" dependentSelector="{{AdminConfigureTaxSection.systemValueDefaultState}}" visible="false" /> + <conditionalClick stepKey="clickCalculationSettingsAgain" selector="{{AdminConfigureTaxSection.defaultDestination}}" dependentSelector="{{AdminConfigureTaxSection.systemValueDefaultState}}" visible="false" /> <checkOption stepKey="clickDefaultState" selector="{{AdminConfigureTaxSection.systemValueDefaultState}}"/> <selectOption stepKey="selectDefaultState" selector="{{AdminConfigureTaxSection.dropdownDefaultState}}" userInput="California"/> <fillField stepKey="fillDefaultPostCode" selector="{{AdminConfigureTaxSection.defaultPostCode}}" userInput=""/> <!-- change the options for shopping cart display to not show tax --> + <!-- conditionalClick twice to fix some flaky behavior --> <conditionalClick stepKey="clickShoppingCartDisplaySettings" selector="{{AdminConfigureTaxSection.shoppingCartDisplay}}" dependentSelector="{{AdminConfigureTaxSection.systemValueIncludeTaxTotalCart}}" visible="false"/> + <conditionalClick stepKey="clickShoppingCartDisplaySettingsAgain" selector="{{AdminConfigureTaxSection.shoppingCartDisplay}}" dependentSelector="{{AdminConfigureTaxSection.systemValueIncludeTaxTotalCart}}" visible="false"/> <checkOption stepKey="clickTaxTotalCart" selector="{{AdminConfigureTaxSection.systemValueIncludeTaxTotalCart}}"/> <selectOption stepKey="selectTaxTotalCart" selector="{{AdminConfigureTaxSection.dropdownIncludeTaxTotalCart}}" userInput="Yes"/> <checkOption stepKey="clickDisplayTaxSummaryCart" selector="{{AdminConfigureTaxSection.systemValueDisplayTaxSummaryCart}}"/> @@ -66,7 +79,9 @@ <selectOption stepKey="selectDisplayZeroTaxCart" selector="{{AdminConfigureTaxSection.dropdownDisplayZeroTaxCart}}" userInput="Yes"/> <!-- change the options for orders, invoices, credit memos display to not show tax --> + <!-- conditionalClick twice to fix some flaky behavior --> <conditionalClick stepKey="clickOrdersInvoicesCreditSales" selector="{{AdminConfigureTaxSection.ordersInvoicesCreditSales}}" dependentSelector="{{AdminConfigureTaxSection.systemValueIncludeTaxTotalSales}}" visible="false"/> + <conditionalClick stepKey="clickOrdersInvoicesCreditSalesAgain" selector="{{AdminConfigureTaxSection.ordersInvoicesCreditSales}}" dependentSelector="{{AdminConfigureTaxSection.systemValueIncludeTaxTotalSales}}" visible="false"/> <checkOption stepKey="clickTaxTotalSales" selector="{{AdminConfigureTaxSection.systemValueIncludeTaxTotalSales}}"/> <selectOption stepKey="selectTaxTotalSales" selector="{{AdminConfigureTaxSection.dropdownIncludeTaxTotalSales}}" userInput="Yes"/> <checkOption stepKey="clickDisplayTaxSummarySales" selector="{{AdminConfigureTaxSection.systemValueDisplayTaxSummarySales}}"/> @@ -80,6 +95,13 @@ <see stepKey="seeSuccess" selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the configuration."/> </actionGroup> + <actionGroup name="addCustomTaxRate" extends="addNewTaxRateNoZip"> + <remove keyForRemoval="fillZipCode"/> + <remove keyForRemoval="fillRate"/> + <fillField stepKey="fillZipCode" selector="{{AdminTaxRulesSection.zipCode}}" userInput="US-NY-*-Rate 2" after="fillTaxIdentifier"/> + <fillField stepKey="fillRate" selector="{{AdminTaxRulesSection.rate}}" userInput="0" after="selectCountry"/> + </actionGroup> + <!-- Action group to add a tax rate when on a tax rule configuration page --> <!-- Must already be on a tax rule configuration page or a new tax rule page --> <actionGroup name="addNewTaxRateNoZip"> @@ -100,4 +122,99 @@ <!-- Save the tax rate --> <click stepKey="saveTaxRate" selector="{{AdminTaxRulesSection.save}}"/> </actionGroup> + + <!--Set Tax Class for Shipping--> + <actionGroup name="changeShippingTaxClass"> + <!--Select Configuration menu from Store--> + <click selector="{{AdminMenuSection.stores}}" stepKey="clickOnSTORES" /> + <waitForPageLoad stepKey="waitForConfiguration"/> + <click selector="{{AdminMenuSection.configuration}}" stepKey="clickOnConfigurations"/> + <waitForPageLoad stepKey="waitForSales"/> + <!--Double click the same to fix flaky issue with redirection to Dashboard--> + <click selector="{{AdminMenuSection.stores}}" stepKey="clickOnSTORES1" /> + <waitForPageLoad stepKey="waitForConfiguration1"/> + <click selector="{{AdminMenuSection.configuration}}" stepKey="clickOnConfigurations1"/> + <waitForPageLoad stepKey="waitForSales1" time="5"/> + <!--Change default tax class for Shipping on Taxable Goods--> + <click selector="{{ConfigurationListSection.sales}}" stepKey="clickOnSales" /> + <waitForPageLoad stepKey="waitForPaymentMethods"/> + <click selector="{{AdminConfigureTaxSection.salesTax}}" stepKey="clickOnTax"/> + <waitForPageLoad stepKey="waitForTax"/> + <seeInCurrentUrl url="{{AdminTaxConfigurationPage.url}}" stepKey="adminTaxConfiguration"/> + <seeElement selector="{{AdminConfigureTaxSection.taxClasses}}" stepKey="taxClassSectionC"/> + <click selector="{{AdminConfigureTaxSection.taxClasses}}" stepKey="openTaxClassSection"/> + <click selector="{{AdminConfigureTaxSection.taxShippingClassSystem}}" stepKey="uncheckSystemValue"/> + <selectOption selector="{{AdminConfigureTaxSection.taxClassShipping}}" userInput="Taxable Goods" stepKey="setTaxClassForShipping"/> + <!-- Save the settings --> + <scrollToTopOfPage stepKey="scrollToTop"/> + <click stepKey="saveTaxOptions" selector="{{AdminCategoryMainActionsSection.SaveButton}}"/> + <waitForPageLoad stepKey="waitForTaxSaved"/> + <see stepKey="seeSuccess" selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the configuration."/> + </actionGroup> + + <actionGroup name="setDefaultShippingTaxClass"> + <!--Select Configuration menu from Store--> + <click selector="{{AdminMenuSection.stores}}" stepKey="clickOnSTORES" /> + <waitForPageLoad stepKey="waitForConfiguration"/> + <click selector="{{AdminMenuSection.configuration}}" stepKey="clickOnConfigurations"/> + <waitForPageLoad stepKey="waitForSales"/> + <!--Double click the same to fix flaky issue with redirection to Dashboard--> + <click selector="{{AdminMenuSection.stores}}" stepKey="clickOnSTORES1" /> + <waitForPageLoad stepKey="waitForConfiguration1"/> + <click selector="{{AdminMenuSection.configuration}}" stepKey="clickOnConfigurations1"/> + <waitForPageLoad stepKey="waitForSales1"/> + <!--Change default tax class for Shipping on Taxable Goods--> + <click selector="{{ConfigurationListSection.sales}}" stepKey="clickOnSales" /> + <waitForPageLoad stepKey="waitForPaymentMethods"/> + <click selector="{{AdminConfigureTaxSection.salesTax}}" stepKey="clickOnTax"/> + <waitForPageLoad stepKey="waitForTax"/> + <seeElement selector="{{AdminConfigureTaxSection.taxClasses}}" stepKey="taxClassSectionC"/> + <click selector="{{AdminConfigureTaxSection.taxShippingClassSystem}}" stepKey="checkSystemDefaultValue"/> + <click selector="{{AdminConfigureTaxSection.taxClasses}}" stepKey="closeTaxClassSection"/> + <!-- Save the settings --> + <scrollToTopOfPage stepKey="scrollToTop"/> + <click stepKey="saveTaxOptions" selector="{{AdminCategoryMainActionsSection.SaveButton}}"/> + <waitForPageLoad stepKey="waitForTaxSaved"/> + <see stepKey="seeSuccess" selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the configuration."/> + </actionGroup> + + <!--Add Product Tax Class--> + <actionGroup name="addProductTaxClass"> + <arguments> + <argument name="prodTaxClassName" type="string"/> + </arguments> + <!--Click Additional Settings--> + <click stepKey="clickAdditionalSettings" selector="{{AdminTaxRulesSection.additionalSettings}}"/> + <!--Click Product Add New Tax Class Button--> + <click stepKey="clickProdAddNewTaxClassBtn" selector="{{AdminTaxRulesSection.productAddNewTaxClass}}"/> + <!--Fill field--> + <fillField stepKey="fillProdNewTaxClass" selector="{{AdminTaxRulesSection.fieldProdNewTaxClass}}" userInput="{{prodTaxClassName}}"/> + <!-- Save Product tax rate --> + <click stepKey="saveProdTaxRate" selector="{{AdminTaxRulesSection.saveProdNewTaxClass}}"/> + </actionGroup> + + <!--Add New Tax Rule --> + <actionGroup name="addNewTaxRuleActionGroup"> + <!-- Go to tax rule page --> + <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulePage"/> + <waitForPageLoad stepKey="waitForTaxRatePage"/> + <click stepKey="addNewTaxRate" selector="{{AdminGridMainControls.add}}"/> + </actionGroup> + + <!--Delete Product Tax Class--> + <actionGroup name="deleteProductTaxClass"> + <arguments> + <argument name="taxClassName" type="string"/> + </arguments> + <!-- Go to tax rule page --> + <amOnPage url="{{AdminNewTaxRulePage.url}}" stepKey="goToNewTaxRulePage"/> + <waitForPageLoad stepKey="waitForTaxRatePage"/> + <click stepKey="clickAdditionalSettings" selector="{{AdminTaxRulesSection.additionalSettings}}"/> + <scrollTo stepKey="scrollToAdditionalSettings" selector="{{AdminTaxRulesSection.additionalSettings}}"/> + <moveMouseOver stepKey="hoverDeleteElement" selector="{{AdminTaxRulesSection.deleteTaxClassName(taxClassName)}}"/> + <click stepKey="deleteFirstTaxClass" selector="{{AdminTaxRulesSection.deleteTaxClass(taxClassName)}}"/> + <waitForElementVisible selector="{{AdminTaxRulesSection.popUpDialogOK}}" stepKey="waitForElementBecomeVisible"/> + <click stepKey="acceptPopUpDialog" selector="{{AdminTaxRulesSection.popUpDialogOK}}"/> + <waitForPageLoad stepKey="waitForProductTaxClassDeleted"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Tax/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Tax/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 000000000000..2bed9b0d0791 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminMenuStoresTaxesTaxRules"> + <data key="pageTitle">Tax Rules</data> + <data key="title">Tax Rules</data> + <data key="dataUiId">magento-tax-sales-tax-rules</data> + </entity> + <entity name="AdminMenuStoresTaxZonesAndRates"> + <data key="pageTitle">Tax Zones and Rates</data> + <data key="title">Tax Zones and Rates</data> + <data key="dataUiId">magento-tax-sales-tax-rates</data> + </entity> + <entity name="AdminMenuSystemDataTransferImportAndExportTaxRates"> + <data key="pageTitle">Import and Export Tax Rates</data> + <data key="title">Import/Export Tax Rates</data> + <data key="dataUiId">magento-taximportexport-system-convert-tax</data> + </entity> +</entities> diff --git a/app/code/Magento/Tax/Test/Mftf/Data/TaxClassData.xml b/app/code/Magento/Tax/Test/Mftf/Data/TaxClassData.xml new file mode 100644 index 000000000000..0edf2d6cac14 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Data/TaxClassData.xml @@ -0,0 +1,29 @@ +<?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="defaultTaxClass" type="taxClass"> + <data key="class_name" unique="suffix">Tax Class </data> + </entity> + <entity name="customerTaxClass" type="taxClass"> + <data key="class_name" unique="suffix">Customer Tax Class </data> + <data key="class_type">CUSTOMER</data> + </entity> + <entity name="productTaxClass" type="taxClass"> + <data key="class_name" unique="suffix">Product Tax Class </data> + <data key="class_type">PRODUCT</data> + </entity> + <entity name="retailCustomerTaxClass"> + <data key="class_name">Retail Customer</data> + <data key="class_type">CUSTOMER</data> + </entity> + <entity name="taxableGoodsTaxClass"> + <data key="class_name">Taxable Goods</data> + <data key="class_type">PRODUCT</data> + </entity> +</entities> diff --git a/app/code/Magento/Tax/Test/Mftf/Data/TaxCodeData.xml b/app/code/Magento/Tax/Test/Mftf/Data/TaxCodeData.xml index 42fd01357375..27c89162b5ce 100644 --- a/app/code/Magento/Tax/Test/Mftf/Data/TaxCodeData.xml +++ b/app/code/Magento/Tax/Test/Mftf/Data/TaxCodeData.xml @@ -8,6 +8,18 @@ <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="SimpleTaxUK" type="tax"> + <data key="state">California</data> + <data key="country">United Kingdom</data> + <data key="zip">*</data> + <data key="rate">50</data> + </entity> + <entity name="SimpleTaxUKZeroRate" type="tax"> + <data key="state">California</data> + <data key="country">United Kingdom</data> + <data key="zip">*</data> + <data key="rate">0</data> + </entity> <entity name="SimpleTaxNY" type="tax"> <data key="state">New York</data> <data key="country">United States</data> @@ -26,4 +38,16 @@ <data key="zip">*</data> <data key="rate">0</data> </entity> + <entity name="SimpleTaxWithZipCode" type="tax"> + <data key="state">Texas</data> + <data key="country">United States</data> + <data key="zip">78729</data> + <data key="rate">7.25</data> + </entity> + <entity name="SimpleSecondTaxWithZipCode" type="tax"> + <data key="state">Texas</data> + <data key="country">United States</data> + <data key="zip">78729</data> + <data key="rate">0.125</data> + </entity> </entities> diff --git a/app/code/Magento/Tax/Test/Mftf/Data/TaxRateData.xml b/app/code/Magento/Tax/Test/Mftf/Data/TaxRateData.xml new file mode 100644 index 000000000000..4409ea0a21df --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Data/TaxRateData.xml @@ -0,0 +1,113 @@ +<?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="SimpleTaxRate" type="taxRate"> + <data key="code" unique="suffix">TaxRate</data> + </entity> + <entity name="defaultTaxRate" type="taxRate"> + <data key="code" unique="suffix">Tax Rate </data> + <data key="tax_country_id">US</data> + <data key="tax_region_id">12</data> + <data key="tax_postcode">*</data> + <data key="zip_is_range">0</data> + <data key="rate">10</data> + </entity> + <entity name="US_CA_Rate_1" type="taxRate"> + <data key="id">1</data> + <data key="code">US-CA-*-Rate 1</data> + <data key="tax_country_id">US</data> + <data key="tax_postcode">*</data> + <data key="rate">8.2500</data> + </entity> + <entity name="US_NY_Rate_1" type="taxRate"> + <data key="code">US-NY-*-Rate 1</data> + <data key="tax_country_id">US</data> + <data key="tax_postcode">*</data> + <data key="rate">8.3750</data> + <data key="id">2</data> + </entity> + <entity name="taxRate_US_NY_8_1" type="taxRate"> + <data key="code" unique="suffix">US-NY-*-</data> + <data key="tax_country_id">US</data> + <data key="tax_region_id">43</data> + <data key="tax_postcode">*</data> + <data key="rate">8.1</data> + </entity> + <entity name="taxRateCustomRateUS" type="taxRate"> + <data key="code" unique="suffix">Tax Rate </data> + <data key="tax_country_id">US</data> + <data key="tax_country">United States</data> + <data key="tax_region_id">12</data> + <data key="tax_region">California</data> + <data key="tax_postcode">90001</data> + <data key="zip_is_range">0</data> + <data key="rate">100.0000</data> + </entity> + <entity name="defaultTaxRateWithZipRange" type="taxRate"> + <data key="code" unique="suffix">Tax Rate </data> + <data key="tax_country_id">US</data> + <data key="tax_country">United States</data> + <data key="tax_region_id">12</data> + <data key="tax_region">California</data> + <data key="zip_is_range">1</data> + <data key="zip_from">90001</data> + <data key="zip_to">96162</data> + <data key="rate">15.05</data> + </entity> + <entity name="TaxRateWithFixedZipUtah" type="taxRate"> + <data key="code" unique="suffix">Tax Rate </data> + <data key="tax_country_id">US</data> + <data key="tax_country">United States</data> + <data key="tax_region_id">58</data> + <data key="tax_region">Utah</data> + <data key="zip_is_range">0</data> + <data key="tax_postcode">84001</data> + <data key="rate">20</data> + </entity> + <entity name="defaultTaxRateWithLargeRate" type="taxRate"> + <data key="code" unique="suffix">TaxRate</data> + <data key="tax_country_id">GB</data> + <data key="tax_country">United Kingdom</data> + <data key="tax_postcode">*</data> + <data key="zip_is_range">0</data> + <data key="rate">777</data> + </entity> + <entity name="taxRateCustomRateCanada" type="taxRate"> + <data key="code" unique="suffix">TaxRate</data> + <data key="tax_country_id">CA</data> + <data key="tax_country">Canada</data> + <data key="tax_region_id">*</data> + <data key="tax_postcode">180</data> + <data key="zip_is_range">0</data> + <data key="rate">25</data> + </entity> + <entity name="taxRateCustomRateUK" type="taxRate"> + <data key="code" unique="suffix">TaxRate</data> + <data key="tax_country_id">GB</data> + <data key="tax_country">United Kingdom</data> + <data key="zip_is_range">1</data> + <data key="zip_from">1</data> + <data key="zip_to">7800935</data> + <data key="rate">12.99</data> + </entity> + <entity name="taxRateCustomRateFrance" type="taxRate"> + <data key="code" unique="suffix">TaxRate</data> + <data key="tax_country_id">FR</data> + <data key="tax_country">France</data> + <data key="tax_region_id">277</data> + <data key="tax_region">Val-d'Oise</data> + <data key="tax_postcode">*</data> + <data key="zip_is_range">0</data> + <data key="rate">0.1</data> + </entity> + <entity name="taxRateForPensylvannia" extends="defaultTaxRate"> + <data key="tax_region_id">51</data> + <data key="rate">6</data> + </entity> +</entities> diff --git a/app/code/Magento/Tax/Test/Mftf/Data/TaxRuleData.xml b/app/code/Magento/Tax/Test/Mftf/Data/TaxRuleData.xml index b3f341b687ba..fde43cd10e3e 100644 --- a/app/code/Magento/Tax/Test/Mftf/Data/TaxRuleData.xml +++ b/app/code/Magento/Tax/Test/Mftf/Data/TaxRuleData.xml @@ -7,6 +7,20 @@ --> <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="defaultTaxRule" type="taxRule"> + <data key="code" unique="suffix">TaxIdentifier</data> + <data key="position">1</data> + <data key="priority">1</data> + <array key="customer_tax_class_ids"> + <item>3</item> + </array> + <array key="product_tax_class_ids"> + <item>2</item> + </array> + <array key="tax_rate_ids"> + <item>1</item> + </array> + </entity> <entity name="SimpleTaxRule" type="taxRule"> <data key="code" unique="suffix">TaxRule</data> <data key="position">0</data> @@ -23,4 +37,90 @@ </array> <data key="calculate_subtotal">true</data> </entity> + <entity name="taxRuleWithCustomPriorityPosition" type="taxRule"> + <data key="code" unique="suffix">TaxRule</data> + <data key="position">1</data> + <data key="priority">1</data> + <array key="customer_tax_class_ids"> + <item>3</item> + </array> + <array key="product_tax_class_ids"> + <item>2</item> + </array> + <array key="tax_rate_ids"> + <item>1</item> + <item>2</item> + </array> + <data key="calculate_subtotal">true</data> + </entity> + <entity name="taxRuleWithCustomPriority" type="taxRule"> + <data key="code" unique="suffix">TaxRule</data> + <data key="position">0</data> + <data key="priority">1</data> + <array key="customer_tax_class_ids"> + <item>3</item> + </array> + <array key="product_tax_class_ids"> + <item>2</item> + </array> + <array key="tax_rate_ids"> + <item>1</item> + <item>2</item> + </array> + <data key="calculate_subtotal">true</data> + </entity> + <entity name="taxRuleWithCustomPosition" type="taxRule"> + <data key="code" unique="suffix">TaxRule</data> + <data key="position">1</data> + <data key="priority">0</data> + <array key="customer_tax_class_ids"> + <item>3</item> + </array> + <array key="product_tax_class_ids"> + <item>2</item> + </array> + <array key="tax_rate_ids"> + <item>1</item> + <item>2</item> + </array> + <data key="calculate_subtotal">true</data> + </entity> + <entity name="taxRuleWithUpdatePriorityPosition" type="taxRule"> + <data key="code" unique="suffix">TaxRule</data> + <data key="position">2</data> + <data key="priority">2</data> + <array key="customer_tax_class_ids"> + <item>3</item> + </array> + <array key="product_tax_class_ids"> + <item>2</item> + </array> + <array key="tax_rate_ids"> + <item>1</item> + <item>2</item> + </array> + <data key="calculate_subtotal">true</data> + </entity> + <entity name="taxRuleWithCustomTaxClasses" type="taxRule"> + <data key="code" unique="suffix">TaxRule</data> + <data key="position">0</data> + <data key="priority">0</data> + <array key="customer_tax_class_ids"> + <item>3</item> + </array> + <array key="product_tax_class_ids"> + <item>2</item> + </array> + <array key="tax_rate_ids"> + <item>1</item> + <item>2</item> + </array> + <data key="calculate_subtotal">true</data> + </entity> + <entity name="TaxRule" type="taxRule"> + <data key="name" unique="suffix">TaxName</data> + </entity> + <entity name="TaxRuleZeroRate" type="taxRule"> + <data key="name" unique="suffix">TaxNameZeroRate</data> + </entity> </entities> diff --git a/app/code/Magento/Tax/Test/Mftf/Metadata/tax_class-meta.xml b/app/code/Magento/Tax/Test/Mftf/Metadata/tax_class-meta.xml new file mode 100644 index 000000000000..c2e5aac8ce6f --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Metadata/tax_class-meta.xml @@ -0,0 +1,25 @@ +<?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="CreateTaxClass" dataType="taxClass" type="create" auth="adminOauth" url="/V1/taxClasses" method="POST" returnRegex="/\d+/"> + <contentType>application/json</contentType> + <object dataType="taxClass" key="taxClass"> + <field key="id">integer</field> + <field key="class_id">integer</field> + <field key="class_name" required="true">string</field> + <field key="class_type">string</field> + </object> + </operation> + <operation name="GetTaxClass" dataType="taxClass" type="get" auth="adminOauth" url="/V1/taxClasses/{return}" method="GET"> + <contentType>application/json</contentType> + </operation> + <operation name="DeleteTaxClass" dataType="taxClass" type="delete" auth="adminOauth" url="/V1/taxClasses/{return}" method="DELETE"> + <contentType>application/json</contentType> + </operation> +</operations> diff --git a/app/code/Magento/Tax/Test/Mftf/Metadata/tax_rate-meta.xml b/app/code/Magento/Tax/Test/Mftf/Metadata/tax_rate-meta.xml new file mode 100644 index 000000000000..3f192920c5cc --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Metadata/tax_rate-meta.xml @@ -0,0 +1,27 @@ +<?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="CreateTaxRate" dataType="taxRate" type="create" auth="adminOauth" url="/V1/taxRates" method="POST"> + <contentType>application/json</contentType> + <object key="taxRate" dataType="taxRate"> + <field key="id">integer</field> + <field key="tax_country_id">string</field> + <field key="tax_region_id">integer</field> + <field key="tax_postcode">string</field> + <field key="zip_is_range">integer</field> + <field key="zip_from">integer</field> + <field key="zip_to">integer</field> + <field key="rate">integer</field> + <field key="code">string</field> + </object> + </operation> + <operation name="DeleteTaxRate" dataType="taxRate" type="delete" auth="adminOauth" url="/V1/taxRates/{id}" method="DELETE"> + <contentType>application/json</contentType> + </operation> +</operations> \ No newline at end of file diff --git a/app/code/Magento/Tax/Test/Mftf/Page/AdminEditTaxRatePage.xml b/app/code/Magento/Tax/Test/Mftf/Page/AdminEditTaxRatePage.xml new file mode 100644 index 000000000000..26152d5497a9 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Page/AdminEditTaxRatePage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminEditTaxRatePage" url="tax/rate/edit/rate/{{var1}}/" module="Magento_Tax" area="admin" parameterized="true"> + <section name="AdminTaxRateFormSection"/> + </page> +</pages> diff --git a/app/code/Magento/Tax/Test/Mftf/Page/AdminEditTaxRulePage.xml b/app/code/Magento/Tax/Test/Mftf/Page/AdminEditTaxRulePage.xml new file mode 100644 index 000000000000..c0e4958619c8 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Page/AdminEditTaxRulePage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminEditTaxRulePage" url="tax/rule/edit/rule/{{var}}/" module="Magento_Tax" area="admin" parameterized="true"> + <section name="AdminTaxRulesSection"/> + </page> +</pages> diff --git a/app/code/Magento/Tax/Test/Mftf/Section/AdminConfigureTaxSection.xml b/app/code/Magento/Tax/Test/Mftf/Section/AdminConfigureTaxSection.xml index 896d719a436c..e69bfbaebbfd 100644 --- a/app/code/Magento/Tax/Test/Mftf/Section/AdminConfigureTaxSection.xml +++ b/app/code/Magento/Tax/Test/Mftf/Section/AdminConfigureTaxSection.xml @@ -10,7 +10,12 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminConfigureTaxSection"> <!-- on page /admin/admin/system_config/edit/section/tax/ --> + <element name="salesTax" type="button" selector="//a[contains(@class, 'admin__page-nav-link item-nav')]/span[text()='Tax']"/> <element name="taxClasses" type="block" selector="#tax_classes-head" timeout="30"/> + <element name="taxShippingClassSystem" type="checkbox" selector="#tax_classes_shipping_tax_class_inherit"/> + <element name="taxClassShipping" type="select" selector="#tax_classes_shipping_tax_class"/> + <element name="taxClassProduct" type="select" selector="#tax_classes_default_product_tax_class"/> + <element name="taxClassCustomer" type="select" selector="#tax_classes_default_customer_tax_class"/> <element name="taxCalculationSettings" type="block" selector="#tax_calculation-head" timeout="30"/> <element name="taxCalculationSettingsOpened" type="block" selector="#tax_calculation-head.open" timeout="30"/> @@ -23,6 +28,8 @@ <element name="taxCalculationPrices" type="select" selector="#tax_calculation_price_includes_tax"/> <element name="taxCalculationPricesDisabled" type="select" selector="#tax_calculation_price_includes_tax[disabled='disabled']"/> <element name="taxCalculationPricesInherit" type="checkbox" selector="#tax_calculation_price_includes_tax_inherit"/> + <element name="taxCalculationApplyTaxOn" type="select" selector="#tax_calculation_apply_tax_on"/> + <element name="taxCalculationApplyTaxOnInherit" type="checkbox" selector="#tax_calculation_apply_tax_on_inherit"/> <element name="defaultDestination" type="block" selector="#tax_defaults-head" timeout="30"/> <element name="systemValueDefaultState" type="checkbox" selector="#row_tax_defaults_region input[type='checkbox']"/> @@ -35,6 +42,7 @@ <element name="taxDisplayProductPricesDisabled" type="select" selector="#tax_display_type[disabled='disabled']"/> <element name="taxDisplayProductPricesInherit" type="checkbox" selector="#tax_display_type_inherit"/> + <element name="taxSalesDisplay" type="block" selector=".config.admin__collapsible-block#tax_sales_display" timeout="30"/> <element name="shoppingCartDisplay" type="block" selector="#tax_cart_display-head" timeout="30"/> <element name="systemValueIncludeTaxTotalCart" type="checkbox" selector="#row_tax_cart_display_grandtotal input[type='checkbox']"/> <element name="dropdownIncludeTaxTotalCart" type="checkbox" selector="#row_tax_cart_display_grandtotal select"/> @@ -51,5 +59,10 @@ <element name="systemValueDisplayZeroTaxSales" type="checkbox" selector="#row_tax_sales_display_zero_tax input[type='checkbox']"/> <element name="dropdownDisplayZeroTaxSales" type="checkbox" selector="#row_tax_sales_display_zero_tax select"/> <element name="fixedProductTaxes" type="block" selector="#tax_weee-head" timeout="30"/> + + <element name="taxClassesCondition" type="block" selector="//a[@id='tax_classes-head' and @class='open']" timeout="30"/> + <element name="useSystemValue" type="checkbox" selector="#tax_classes_default_product_tax_class_inherit"/> + <element name="productTaxClass" type="select" selector="#tax_classes_default_product_tax_class"/> + <element name="save" type="button" selector="#save"/> </section> </sections> diff --git a/app/code/Magento/Tax/Test/Mftf/Section/AdminProductTaxClassSection.xml b/app/code/Magento/Tax/Test/Mftf/Section/AdminProductTaxClassSection.xml new file mode 100644 index 000000000000..f32caf88f213 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Section/AdminProductTaxClassSection.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="AdminProductTaxClassSection"> + <element name="additionalSettings" type="text" selector="#details-summarybase_fieldset"/> + <element name="additionalSettingsCond" type="text" selector="//summary[@id='details-summarybase_fieldset' and @aria-expanded='true']"/> + <element name="productTaxClass" type="button" selector="//div[contains(@class, 'field-tax_product_class')]//span[text()='Add New Tax Class']"/> + <element name="TaxClassName" type="block" selector="//div[contains(@class, 'field-tax_product_class')]//input[@class='mselect-input']"/> + <element name="confirm" type="button" selector="//div[contains(@class, 'field-tax_product_class')]//span[@class='mselect-save']"/> + </section> +</sections> diff --git a/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRateFormSection.xml b/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRateFormSection.xml new file mode 100644 index 000000000000..b2efe5c47b0c --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRateFormSection.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminTaxRateFormSection"> + <element name="taxIdentifier" type="input" selector="#code" /> + <element name="deleteRate" type="button" selector="#delete" timeout="30"/> + <element name="ok" type="button" selector="button.action-primary.action-accept" timeout="30"/> + <element name="save" type="button" selector="#save" timeout="30"/> + <element name="country" type="select" selector="#tax_country_id"/> + <element name="state" type="select" selector="#tax_region_id"/> + <element name="rate" type="text" selector="#rate"/> + <element name="zipRange" type="checkbox" selector="#zip_is_range"/> + <element name="rangeFrom" type="text" selector="#zip_from"/> + <element name="rangeTo" type="text" selector="#zip_to"/> + <element name="zipCode" type="input" selector="#tax_postcode"/> + </section> +</sections> diff --git a/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRateGridSection.xml b/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRateGridSection.xml new file mode 100644 index 000000000000..ba9da182d735 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRateGridSection.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminTaxRateGridSection"> + <element name="grid" type="block" selector="#tax_rate_grid"/> + <element name="add" type="button" selector="#add" timeout="30"/> + <element name="search" type="button" selector="button[data-action='grid-filter-apply']" timeout="30"/> + <element name="filterByTaxIdentifier" type="input" selector="#tax_rate_grid_filter_code"/> + <element name="filterByCountry" type="input" selector="#tax_rate_grid_filter_tax_country_id"/> + <element name="filterByPostCode" type="input" selector="#tax_rate_grid_filter_tax_postcode"/> + <element name="nthRow" type="block" selector="tr[data-role='row']:nth-of-type({{var}})" parameterized="true" timeout="30"/> + <element name="emptyText" type="text" selector=".empty-text"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxReportsSection.xml b/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxReportsSection.xml new file mode 100644 index 000000000000..80101687e173 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxReportsSection.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="../../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminTaxReportsSection"> + <element name="refreshStatistics" type="button" selector="//a[contains(text(),'here')]"/> + <element name="fromDate" type="input" selector="#sales_report_from"/> + <element name="toDate" type="input" selector="//*[@id='sales_report_to']/following-sibling::button"/> + <element name="goTodayButton" type="input" selector="//button[contains(text(),'Go Today')]"/> + <element name="showReportButton" type="button" selector="#filter_form_submit"/> + <element name="taxRuleAmount" type="textarea" selector="//*[contains(text(),'{{var1}}')]/following-sibling::td[contains(@class, 'col-tax-amount')]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRuleFormSection.xml b/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRuleFormSection.xml new file mode 100644 index 000000000000..c77d3ad0d944 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRuleFormSection.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminTaxRuleFormSection"> + <element name="fieldTaxRate" type="block" selector="div.field-tax_rate"/> + <element name="taxIdentifier" type="input" selector="input.admin__control-text admin__action-multiselect-search" timeout="30"/> + <element name="code" type="input" selector="#code"/> + <element name="taxRateSearch" type="input" selector="input[data-role='advanced-select-text']"/> + <element name="taxRateSelected" type="input" selector="//span[contains(., '{{taxRateCode}}') and preceding-sibling::input[contains(@class, 'mselect-checked')]]" parameterized="true" /> + <element name="taxRateOption" type="multiselect" selector="//*[@data-ui-id='tax-rate-form-fieldset-element-form-field-tax-rate']//span[.='{{taxRateCode}}']" parameterized="true" /> + <element name="save" type="button" selector="#save" timeout="30"/> + <element name="deleteRule" type="button" selector="#delete" timeout="30"/> + <element name="ok" type="button" selector="button.action-primary.action-accept" timeout="30"/> + <element name="additionalSettings" type="button" selector="#details-summarybase_fieldset" timeout="30"/> + <element name="customerTaxClassOption" type="checkbox" selector="//*[@id='tax_customer_class']/..//span[.='{{taxCustomerClass}}']" parameterized="true"/> + <element name="productTaxClassOption" type="checkbox" selector="//*[@id='tax_product_class']/..//span[.='{{taxProductClass}}']" parameterized="true"/> + <element name="customerTaxClassSelected" type="checkbox" selector="//*[@id='tax_customer_class']/..//span[.='{{taxCustomerClass}}' and preceding-sibling::input[contains(@class, 'mselect-checked')]]" parameterized="true"/> + <element name="productTaxClassSelected" type="checkbox" selector="//*[@id='tax_product_class']/..//span[.='{{taxProductClass}}' and preceding-sibling::input[contains(@class, 'mselect-checked')]]" parameterized="true"/> + <element name="priority" type="text" selector="#priority"/> + <element name="sortOrder" type="text" selector="#position"/> + </section> +</sections> diff --git a/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRuleGridSection.xml b/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRuleGridSection.xml new file mode 100644 index 000000000000..dfcaba832917 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRuleGridSection.xml @@ -0,0 +1,20 @@ +<?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="AdminTaxRuleGridSection"> + <element name="add" type="button" selector="#add" timeout="30"/> + <element name="search" type="button" selector="button[data-action='grid-filter-apply']" timeout="30"/> + <element name="code" type="input" selector="#taxRuleGrid_filter_code"/> + <element name="taxRate" type="input" selector="#taxRuleGrid_filter_tax_rates_codes"/> + <element name="nthRow" type="block" selector="tr[data-role='row']:nth-of-type({{var}})" parameterized="true" timeout="30"/> + <element name="successMessage" type="text" selector="#messages"/> + <element name="emptyText" type="text" selector="td.empty-text"/> + </section> +</sections> diff --git a/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRulesSection.xml b/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRulesSection.xml index 9727c649d7e6..46d92e30395e 100644 --- a/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRulesSection.xml +++ b/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRulesSection.xml @@ -10,6 +10,10 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminTaxRulesSection"> <!-- on page /admin/tax/rule/new/ --> + <element name="customerAddNewTaxClass" type="button" selector="//*[@id='tax_customer_class']/following-sibling::section//*[contains(text(),'Add New Tax Class')]"/> + <element name="fieldCustomerNewTaxClass" type="input" selector="//*[@id='tax_customer_class']/following-sibling::section//input[@class='mselect-input']"/> + <element name="saveCustomerNewTaxClass" type="button" selector="//*[@id='tax_customer_class']/following-sibling::section//span[@class='mselect-save']"/> + <element name="defaultCustomerTaxClass" type="button" selector="//*[@id='tax_customer_class']/following-sibling::section//div[@class='mselect-items-wrapper']/div[1]"/> <element name="ruleName" type="input" selector="#anchor-content #code"/> <element name="addNewTaxRate" type="block" selector="//*[text()='Add New Tax Rate']" timeout="30"/> <element name="taxIdentifier" type="input" selector="aside #code"/> @@ -19,5 +23,16 @@ <element name="country" type="select" selector="#tax_country_id"/> <element name="rate" type="input" selector="#rate"/> <element name="save" type="button" selector=".action-save" timeout="30"/> + <element name="saveRule" type="button" selector="#save" timeout="30"/> + <element name="additionalSettings" type="button" selector="#details-summarybase_fieldset"/> + <element name="productAddNewTaxClass" type="button" selector="//*[@id='tax_product_class']/following-sibling::section//*[contains(text(),'Add New Tax Class')]"/> + <element name="fieldProdNewTaxClass" type="input" selector="//*[@id='tax_product_class']/following-sibling::section//input[@class='mselect-input']"/> + <element name="saveProdNewTaxClass" type="button" selector="//*[@id='tax_product_class']/following-sibling::section//span[@class='mselect-save']"/> + <element name="defaultTaxClass" type="button" selector="//*[@id='tax_product_class']/following-sibling::section//div[@class='mselect-items-wrapper']/div[1]"/> + <element name="deleteTaxClassName" type="button" selector="//span[contains(text(),'{{var1}}')]" parameterized="true"/> + <element name="deleteTaxClass" type="button" selector="//span[contains(text(),'{{var1}}')]/../..//*[@class='mselect-delete']" parameterized="true"/> + <element name="popUpDialogOK" type="button" selector="//*[@class='modal-footer']//*[contains(text(),'OK')]"/> + <element name="taxRateMultiSelectItems" type="block" selector=".mselect-list-item"/> + <element name="taxRateNumber" type="button" selector="//div[@data-ui-id='tax-rate-form-fieldset-element-form-field-tax-rate']//div[@class='mselect-items-wrapper']//label[{{var}}]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateDefaultsTaxRuleTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateDefaultsTaxRuleTest.xml new file mode 100644 index 000000000000..e632f6265f43 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateDefaultsTaxRuleTest.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="AdminCreateDefaultsTaxRuleTest"> + <annotations> + <stories value="Create tax rule"/> + <title value="Create tax rule, defaults"/> + <description value="Test log in to Create Tax Rule and Create Defaults Tax Rule"/> + <testCaseId value="MC-5323"/> + <severity value="CRITICAL"/> + <group value="tax"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="defaultTaxRate" stepKey="initialTaxRate"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminDeleteTaxRule" stepKey="deleteTaxRule"> + <argument name="taxRuleCode" value="{{SimpleTaxRule.code}}" /> + </actionGroup> + <deleteData stepKey="deleteTaxRate" createDataKey="initialTaxRate" /> + </after> + + <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> + <waitForPageLoad stepKey="waitForTaxRuleIndex1"/> + <click selector="{{AdminTaxRuleGridSection.add}}" stepKey="clickAddNewTaxRuleButton"/> + <waitForPageLoad stepKey="waitForTaxRuleIndex2"/> + <!-- Create a tax rule with defaults --> + <fillField selector="{{AdminTaxRuleFormSection.code}}" userInput="{{SimpleTaxRule.code}}" stepKey="fillTaxRuleCode1"/> + <fillField selector="{{AdminTaxRuleFormSection.taxRateSearch}}" userInput="$$initialTaxRate.code$$" stepKey="fillTaxRateSearch"/> + <wait stepKey="waitForSearch" time="5" /> + <click selector="{{AdminTaxRuleFormSection.taxRateOption($$initialTaxRate.code$$)}}" stepKey="selectNeededItem" /> + <click selector="{{AdminTaxRuleFormSection.save}}" stepKey="saveTaxRule" /> + <waitForPageLoad stepKey="waitForTaxRuleSaved" /> + <!-- Verify we see success message --> + <see selector="{{AdminTaxRuleGridSection.successMessage}}" userInput="You saved the tax rule." stepKey="assertTaxRuleSuccessMessage" /> + + <!-- Verify we see created tax rule with defaults(from the above step) on the tax rule grid page --> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> + <fillField selector="{{AdminTaxRuleGridSection.code}}" userInput="{{SimpleTaxRule.code}}" stepKey="fillTaxRuleCode2"/> + <click selector="{{AdminTaxRuleGridSection.search}}" stepKey="clickSearch2"/> + <waitForPageLoad stepKey="waitForTaxRuleSearch" /> + <click selector="{{AdminTaxRuleGridSection.nthRow('1')}}" stepKey="clickFirstRow2"/> + + <!-- Verify we see created tax rule with defaults on the tax rule form page --> + <seeInField selector="{{AdminTaxRuleFormSection.code}}" userInput="{{SimpleTaxRule.code}}" stepKey="seeInTaxRuleField" /> + <seeElement selector="{{AdminTaxRuleFormSection.taxRateSelected($$initialTaxRate.code$$)}}" stepKey="seeTaxRateSelected" /> + </test> +</tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateAllPostCodesTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateAllPostCodesTest.xml new file mode 100644 index 000000000000..b63ca1953157 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateAllPostCodesTest.xml @@ -0,0 +1,77 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateTaxRateAllPostCodesTest"> + <annotations> + <stories value="Create tax rate"/> + <title value="Create tax rate, all postcodes"/> + <description value="Tests log into Create tax rate and create all postcodes"/> + <testCaseId value="MC-5318"/> + <severity value="CRITICAL"/> + <group value="tax"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> + <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillNameFilter"/> + <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch"/> + <click selector="{{AdminTaxRateGridSection.nthRow('1')}}" stepKey="clickFirstRow"/> + <click selector="{{AdminTaxRateFormSection.deleteRate}}" stepKey="clickDeleteRate"/> + <click selector="{{AdminTaxRateFormSection.ok}}" stepKey="clickOk"/> + </after> + + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> + <waitForPageLoad stepKey="waitForTaxRateIndex1"/> + <!-- Create a tax rate with * for postcodes --> + <click selector="{{AdminTaxRateGridSection.add}}" stepKey="clickAddNewTaxRateButton"/> + <fillField selector="{{AdminTaxRateFormSection.taxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillRuleName"/> + <fillField selector="{{AdminTaxRateFormSection.zipCode}}" userInput="*" stepKey="fillPostCode"/> + <selectOption selector="{{AdminTaxRateFormSection.country}}" userInput="Australia" stepKey="selectCountry1"/> + <fillField selector="{{AdminTaxRateFormSection.rate}}" userInput="20" stepKey="fillRate"/> + <click selector="{{AdminTaxRateFormSection.save}}" stepKey="clickSave"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rate." stepKey="seeSuccess"/> + + <!-- Verify the tax rate grid page shows the tax rate we just created --> + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex2"/> + <waitForPageLoad stepKey="waitForTaxRateIndex2"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> + <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillNameFilter"/> + <selectOption selector="{{AdminTaxRateGridSection.filterByCountry}}" userInput="Australia" stepKey="fillCountryFilter"/> + <fillField selector="{{AdminTaxRateGridSection.filterByPostCode}}" userInput="*" stepKey="fillPostCodeFilter"/> + <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch"/> + <see selector="{{AdminTaxRateGridSection.grid}}" userInput="{{SimpleTaxRate.code}}" stepKey="seeRuleName"/> + <see selector="{{AdminTaxRateGridSection.grid}}" userInput="Australia" stepKey="seeCountry"/> + <see selector="{{AdminTaxRateGridSection.grid}}" userInput="*" stepKey="seePostCode"/> + + <!-- Go to the tax rate edit page for our new tax rate --> + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex3"/> + <waitForPageLoad stepKey="waitForTaxRateIndex3"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> + <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillNameFilter2"/> + <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch2"/> + <click selector="{{AdminTaxRateGridSection.nthRow('1')}}" stepKey="clickFirstRow"/> + + <!-- Verify we see expected values on the tax rate edit page --> + <seeInField selector="{{AdminTaxRateFormSection.taxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="seeRuleName2"/> + <seeInField selector="{{AdminTaxRateFormSection.zipCode}}" userInput="*" stepKey="seeZipCode"/> + <seeOptionIsSelected selector="{{AdminTaxRateFormSection.country}}" userInput="Australia" stepKey="seeCountry2"/> + <seeInField selector="{{AdminTaxRateFormSection.rate}}" userInput="20" stepKey="seeRate"/> + + <!-- Go to the tax rule grid page and verify our tax rate can be used in the rule --> + <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> + <waitForPageLoad stepKey="waitForTaxRuleIndex"/> + <click selector="{{AdminGridMainControls.add}}" stepKey="clickAddNewTaxRule"/> + <see selector="{{AdminTaxRulesSection.taxRateMultiSelectItems}}" userInput="{{SimpleTaxRate.code}}" stepKey="seeTaxRateOnNewTaxRulePage"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateLargeRateTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateLargeRateTest.xml new file mode 100644 index 000000000000..cb79de19ce23 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateLargeRateTest.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="AdminCreateTaxRateLargeRateTest"> + <annotations> + <stories value="Create tax rate"/> + <title value="Create tax rate, large rate"/> + <description value="Test log in to Create Tax Rate and Create Large Rate"/> + <testCaseId value="MC-5322"/> + <severity value="CRITICAL"/> + <group value="tax"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> + <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillNameFilter"/> + <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch"/> + <click selector="{{AdminTaxRateGridSection.nthRow('1')}}" stepKey="clickFirstRow"/> + <click selector="{{AdminTaxRateFormSection.deleteRate}}" stepKey="clickDeleteRate"/> + <click selector="{{AdminTaxRateFormSection.ok}}" stepKey="clickOk"/> + </after> + + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> + <waitForPageLoad stepKey="waitForTaxRateIndex1"/> + <!-- Create a tax rate for large postcodes --> + <click selector="{{AdminTaxRateGridSection.add}}" stepKey="clickAddNewTaxRateButton"/> + <fillField selector="{{AdminTaxRateFormSection.taxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillRuleName"/> + <fillField selector="{{AdminTaxRateFormSection.zipCode}}" userInput="*" stepKey="fillPostCode"/> + <selectOption selector="{{AdminTaxRateFormSection.country}}" userInput="France" stepKey="selectCountry1"/> + <selectOption selector="{{AdminTaxRateFormSection.state}}" userInput="Val-d'Oise" stepKey="selectState" /> + <fillField selector="{{AdminTaxRateFormSection.rate}}" userInput="999" stepKey="fillRate"/> + <click selector="{{AdminTaxRateFormSection.save}}" stepKey="clickSave"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rate." stepKey="seeSuccess"/> + + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex2"/> + <waitForPageLoad stepKey="waitForTaxRateIndex3"/> + <!-- Create a tax rate for large postcodes and verify we see expected values on the tax rate grid page --> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> + <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillTaxIdentifierField2"/> + <selectOption selector="{{AdminTaxRateGridSection.filterByCountry}}" userInput="France" stepKey="selectCountry2" /> + <fillField selector="{{AdminTaxRateGridSection.filterByPostCode}}" userInput="*" stepKey="seeTaxPostCode1"/> + <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch2"/> + <click selector="{{AdminTaxRateGridSection.nthRow('1')}}" stepKey="clickFirstRow3"/> + <!-- Verify we see expected values on the tax rate form page --> + <seeInField selector="{{AdminTaxRateFormSection.taxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="seeTaxIdentifierField2"/> + <seeInField selector="{{AdminTaxRateFormSection.zipCode}}" userInput="*" stepKey="seeZipCode"/> + <seeOptionIsSelected selector="{{AdminTaxRateFormSection.country}}" userInput="France" stepKey="seeCountry2"/> + <seeOptionIsSelected selector="{{AdminTaxRateFormSection.state}}" userInput="Val-d'Oise" stepKey="seeState"/> + <seeInField selector="{{AdminTaxRateFormSection.rate}}" userInput="999" stepKey="seeRate"/> + + <!-- Verify we see expected values on the tax rule form page --> + <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> + <waitForPageLoad stepKey="waitForTaxRateIndex4"/> + <click selector="{{AdminTaxRuleGridSection.add}}" stepKey="clickAdd"/> + <see selector="{{AdminTaxRulesSection.taxRateMultiSelectItems}}" userInput="{{SimpleTaxRate.code}}" stepKey="seeTaxRateOnNewTaxRulePage"/> + </test> +</tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateSpecificPostcodeTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateSpecificPostcodeTest.xml new file mode 100644 index 000000000000..696d6c4c8776 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateSpecificPostcodeTest.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="AdminCreateTaxRateSpecificPostcodeTest"> + <annotations> + <stories value="Create tax rate"/> + <title value="Create tax rate, specific postcode"/> + <description value="Test log in to Create Tax Rate and Create specific Postcode"/> + <testCaseId value="MC-5320"/> + <severity value="CRITICAL"/> + <group value="tax"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> + <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillNameFilter"/> + <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch"/> + <click selector="{{AdminTaxRateGridSection.nthRow('1')}}" stepKey="clickFirstRow"/> + <click selector="{{AdminTaxRateFormSection.deleteRate}}" stepKey="clickDeleteRate"/> + <click selector="{{AdminTaxRateFormSection.ok}}" stepKey="clickOk"/> + </after> + + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> + <click selector="{{AdminTaxRateGridSection.add}}" stepKey="clickAddNewTaxRateButton"/> + <waitForPageLoad stepKey="waitForTaxRateIndex1"/> + <!-- Create a tax rate with specific postcode --> + <fillField selector="{{AdminTaxRateFormSection.taxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillTaxIdentifierField1"/> + <fillField selector="{{AdminTaxRateFormSection.zipCode}}" userInput="180" stepKey="fillTaxPostCode"/> + <selectOption selector="{{AdminTaxRateFormSection.country}}" userInput="Canada" stepKey="selectCountry" /> + <selectOption selector="{{AdminTaxRateFormSection.state}}" userInput="*" stepKey="selectState" /> + <fillField selector="{{AdminTaxRateFormSection.rate}}" userInput='25' stepKey="seeRate"/> + <click selector="{{AdminTaxRateFormSection.save}}" stepKey="clickSave"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rate." stepKey="seeSuccess"/> + + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex2"/> + <waitForPageLoad stepKey="waitForTaxRateIndex2"/> + <!-- Verify the tax rate grid page shows the specific postcode we just created --> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> + <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillTaxIdentifierField2"/> + <selectOption selector="{{AdminTaxRateGridSection.filterByCountry}}" userInput="Canada" stepKey="fillCountryFilter"/> + <fillField selector="{{AdminTaxRateGridSection.filterByPostCode}}" userInput="180" stepKey="fillPostCodeFilter"/> + <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch"/> + <click selector="{{AdminTaxRateGridSection.nthRow('1')}}" stepKey="clickFirstRow1"/> + <!-- Verify we see expected values on the tax rate form page --> + <seeInField selector="{{AdminTaxRateFormSection.taxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="seeTaxIdentifierField2"/> + <seeInField selector="{{AdminTaxRateFormSection.zipCode}}" userInput="180" stepKey="seePostCode"/> + <seeInField selector="{{AdminTaxRateFormSection.country}}" userInput="Canada" stepKey="seeCountry2"/> + + <!-- Verify we see expected values on the tax rule form page --> + <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> + <waitForPageLoad stepKey="waitForTaxRateIndex4"/> + <click selector="{{AdminTaxRuleGridSection.add}}" stepKey="clickAdd"/> + <see selector="{{AdminTaxRulesSection.taxRateMultiSelectItems}}" userInput="{{SimpleTaxRate.code}}" stepKey="seeTaxRateOnNewTaxRulePage"/> + </test> +</tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateWiderZipCodeRangeTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateWiderZipCodeRangeTest.xml new file mode 100644 index 000000000000..c6c5e318cc79 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateWiderZipCodeRangeTest.xml @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateTaxRateWiderZipCodeRangeTest"> + <annotations> + <stories value="Create tax rate"/> + <title value="Create tax rate, wider zip code range"/> + <description value="Test log in to Create Tax Rate and Create Wider Zip Code Range"/> + <testCaseId value="MC-5321"/> + <severity value="CRITICAL"/> + <group value="tax"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> + <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillNameFilter"/> + <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch"/> + <click selector="{{AdminTaxRateGridSection.nthRow('1')}}" stepKey="clickFirstRow"/> + <click selector="{{AdminTaxRateFormSection.deleteRate}}" stepKey="clickDeleteRate"/> + <click selector="{{AdminTaxRateFormSection.ok}}" stepKey="clickOk"/> + </after> + + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> + <waitForPageLoad stepKey="waitForTaxRateIndex1"/> + <!-- Create a tax rate with range from 1-7800935 for zipCodes --> + <click selector="{{AdminTaxRateGridSection.add}}" stepKey="clickAddNewTaxRateButton"/> + <fillField selector="{{AdminTaxRateFormSection.taxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillRuleName"/> + <checkOption selector="{{AdminTaxRateFormSection.zipRange}}" stepKey="checkZipRange" /> + <fillField selector="{{AdminTaxRateFormSection.rangeFrom}}" userInput="1" stepKey="fillZipFrom"/> + <fillField selector="{{AdminTaxRateFormSection.rangeTo}}" userInput="7800935" stepKey="fillZipTo"/> + <selectOption selector="{{AdminTaxRateFormSection.country}}" userInput="United Kingdom" stepKey="selectCountry1"/> + <fillField selector="{{AdminTaxRateFormSection.rate}}" userInput="7.75" stepKey="seeRate"/> + <click selector="{{AdminTaxRateFormSection.save}}" stepKey="clickSave"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rate." stepKey="seeSuccess"/> + + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex2"/> + <waitForPageLoad stepKey="waitForTaxRateIndex3"/> + <!-- Create a tax rate for zipCodeRange and verify we see expected values on the tax rate grid page --> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> + <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillTaxIdentifierField2"/> + <selectOption selector="{{AdminTaxRateGridSection.filterByCountry}}" userInput="United Kingdom" stepKey="selectCountry2" /> + <fillField selector="{{AdminTaxRateGridSection.filterByPostCode}}" userInput="1-7800935" stepKey="seeTaxPostCode1"/> + <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch2"/> + <click selector="{{AdminTaxRateGridSection.nthRow('1')}}" stepKey="clickFirstRow1"/> + <!-- Verify we see expected values on the tax rate form page --> + <seeInField selector="{{AdminTaxRateFormSection.taxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="seeTaxIdentifierField2"/> + <seeCheckboxIsChecked selector="{{AdminTaxRateFormSection.zipRange}}" stepKey="clickZipRange"/> + <seeInField selector="{{AdminTaxRateFormSection.rangeFrom}}" userInput="1" stepKey="seeTaxPostCode2"/> + <seeInField selector="{{AdminTaxRateFormSection.rangeTo}}" userInput="7800935" stepKey="seeTaxPostCode3"/> + <seeOptionIsSelected selector="{{AdminTaxRateFormSection.country}}" userInput="United Kingdom" stepKey="seeCountry2"/> + + <!-- Verify we see expected values on the tax rule form page --> + <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> + <waitForPageLoad stepKey="waitForTaxRateIndex4"/> + <click selector="{{AdminTaxRuleGridSection.add}}" stepKey="clickAdd"/> + <see selector="{{AdminTaxRulesSection.taxRateMultiSelectItems}}" userInput="{{SimpleTaxRate.code}}" stepKey="seeTaxRateOnNewTaxRulePage"/> + </test> +</tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateZipCodeRangeTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateZipCodeRangeTest.xml new file mode 100644 index 000000000000..f75fa716e9d3 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateZipCodeRangeTest.xml @@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateTaxRateZipCodeRangeTest"> + <annotations> + <stories value="Create tax rate"/> + <title value="Create tax rate, zip code range"/> + <description value="Test log in to Create Tax Rate and Create Zip Code Range"/> + <testCaseId value="MC-5319"/> + <severity value="CRITICAL"/> + <group value="tax"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> + <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillNameFilter"/> + <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch"/> + <click selector="{{AdminTaxRateGridSection.nthRow('1')}}" stepKey="clickFirstRow1"/> + <click selector="{{AdminTaxRateFormSection.deleteRate}}" stepKey="clickDeleteRate"/> + <click selector="{{AdminTaxRateFormSection.ok}}" stepKey="clickOk"/> + </after> + + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> + <waitForPageLoad stepKey="waitForTaxRateIndex1"/> + <!-- Create a tax rate with range from 90001-96162 for zipCodes --> + <click selector="{{AdminTaxRateGridSection.add}}" stepKey="clickAddNewTaxRateButton"/> + <fillField selector="{{AdminTaxRateFormSection.taxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillRuleName"/> + <checkOption selector="{{AdminTaxRateFormSection.zipRange}}" stepKey="checkZipRange" /> + <fillField selector="{{AdminTaxRateFormSection.rangeFrom}}" userInput="90001" stepKey="fillZipFrom"/> + <fillField selector="{{AdminTaxRateFormSection.rangeTo}}" userInput="96162" stepKey="fillZipTo"/> + <selectOption selector="{{AdminTaxRateFormSection.country}}" userInput="United States" stepKey="selectCountry1"/> + <selectOption selector="{{AdminTaxRateFormSection.state}}" userInput="California" stepKey="selectState" /> + <fillField selector="{{AdminTaxRateFormSection.rate}}" userInput='15.5' stepKey="seeRate"/> + <click selector="{{AdminTaxRateFormSection.save}}" stepKey="clickSave"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rate." stepKey="seeSuccess"/> + + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex2"/> + <waitForPageLoad stepKey="waitForTaxRateIndex3"/> + <!-- Create a tax rate for zipCodeRange and verify we see expected values on the tax rate grid page --> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> + <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillTaxIdentifierField2"/> + <selectOption selector="{{AdminTaxRateGridSection.filterByCountry}}" userInput="United States" stepKey="selectCountry2" /> + <fillField selector="{{AdminTaxRateGridSection.filterByPostCode}}" userInput="90001-96162" stepKey="seeTaxPostCode1"/> + <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch2"/> + <click selector="{{AdminTaxRateGridSection.nthRow('1')}}" stepKey="clickFirstRow3"/> + <!-- Verify we see expected values on the tax rate form page --> + <seeInField selector="{{AdminTaxRateFormSection.taxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="seeTaxIdentifierField2"/> + <seeCheckboxIsChecked selector="{{AdminTaxRateFormSection.zipRange}}" stepKey="clickZipRange"/> + <seeInField selector="{{AdminTaxRateFormSection.rangeFrom}}" userInput="90001" stepKey="seeTaxPostCode2"/> + <seeInField selector="{{AdminTaxRateFormSection.rangeTo}}" userInput="96162" stepKey="seeTaxPostCode3"/> + <seeOptionIsSelected selector="{{AdminTaxRateFormSection.state}}" userInput="California" stepKey="seeState"/> + <seeOptionIsSelected selector="{{AdminTaxRateFormSection.country}}" userInput="United States" stepKey="seeCountry2"/> + + <!-- Verify we see expected values on the tax rule form page --> + <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> + <waitForPageLoad stepKey="waitForTaxRateIndex4"/> + <click selector="{{AdminTaxRuleGridSection.add}}" stepKey="clickAdd"/> + <see selector="{{AdminTaxRulesSection.taxRateMultiSelectItems}}" userInput="{{SimpleTaxRate.code}}" stepKey="seeTaxRateOnNewTaxRulePage"/> + </test> +</tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithCustomerAndProductTaxClassTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithCustomerAndProductTaxClassTest.xml new file mode 100644 index 000000000000..03b12c8f2809 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithCustomerAndProductTaxClassTest.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="AdminCreateTaxRuleWithCustomerAndProductTaxClassTest"> + <annotations> + <stories value="Create tax rule"/> + <title value="Create tax rule, with customer and product tax class"/> + <description value="Test log in to Tax Rule and Create tax rule with customer and product tax class"/> + <testCaseId value="MC-5324"/> + <severity value="CRITICAL"/> + <group value="tax"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="defaultTaxRate" stepKey="initialTaxRate"/> + <createData entity="customerTaxClass" stepKey="createCustomerTaxClass"/> + <createData entity="productTaxClass" stepKey="createProductTaxClass"/> + <getData entity="customerTaxClass" stepKey="customerTaxClass"> + <requiredEntity createDataKey="createCustomerTaxClass"/> + </getData> + <getData entity="productTaxClass" stepKey="productTaxClass"> + <requiredEntity createDataKey="createProductTaxClass"/> + </getData> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminDeleteTaxRule" stepKey="deleteTaxRule"> + <argument name="taxRuleCode" value="{{SimpleTaxRule.code}}" /> + </actionGroup> + <deleteData stepKey="deleteTaxRate" createDataKey="initialTaxRate"/> + <deleteData stepKey="deleteCustomerTaxClass" createDataKey="createCustomerTaxClass"/> + <deleteData stepKey="deleteProductTaxClass" createDataKey="createProductTaxClass"/> + </after> + + <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> + <waitForPageLoad stepKey="waitForTaxRuleIndex1"/> + <click selector="{{AdminTaxRuleGridSection.add}}" stepKey="clickAddNewTaxRuleButton"/> + <waitForPageLoad stepKey="waitForTaxRuleIndex2"/> + + <!-- Create a tax rule with customer and product class --> + <fillField selector="{{AdminTaxRuleFormSection.code}}" userInput="{{SimpleTaxRule.code}}" stepKey="fillTaxRuleCode1"/> + <fillField selector="{{AdminTaxRuleFormSection.taxRateSearch}}" userInput="$$initialTaxRate.code$$" stepKey="fillTaxRateSearch"/> + <wait stepKey="waitForSearch" time="5" /> + <click selector="{{AdminTaxRuleFormSection.taxRateOption($$initialTaxRate.code$$)}}" stepKey="clickSelectNeededItem"/> + <fillField selector="{{AdminTaxRuleFormSection.taxRateSearch}}" userInput="{{US_CA_Rate_1.code}}" stepKey="fillTaxRateSearch1"/> + <wait stepKey="waitForSearch2" time="5" /> + <conditionalClick selector="{{AdminTaxRuleFormSection.taxRateOption(US_CA_Rate_1.code)}}" dependentSelector="{{AdminTaxRuleFormSection.taxRateSelected(US_CA_Rate_1.code)}}" visible="false" stepKey="clickSelectNeededItem1"/> + <fillField selector="{{AdminTaxRuleFormSection.taxRateSearch}}" userInput="{{US_NY_Rate_1.code}}" stepKey="fillTaxRateSearch2"/> + <wait stepKey="waitForSearch3" time="5" /> + <conditionalClick selector="{{AdminTaxRuleFormSection.taxRateOption(US_NY_Rate_1.code)}}" dependentSelector="{{AdminTaxRuleFormSection.taxRateSelected(US_NY_Rate_1.code)}}" visible="false" stepKey="clickSelectNeededItem2"/> + <click selector="{{AdminTaxRuleFormSection.additionalSettings}}" stepKey="clickAdditionalSettings"/> + <scrollTo selector="{{AdminTaxRuleFormSection.additionalSettings}}" x="0" y="-80" stepKey="scrollToAdvancedSettings"/> + <wait stepKey="waitForAdditionalSettings" time="5" /> + <conditionalClick selector="{{AdminTaxRuleFormSection.customerTaxClassOption(retailCustomerTaxClass.class_name)}}" dependentSelector="{{AdminTaxRuleFormSection.customerTaxClassSelected(retailCustomerTaxClass.class_name)}}" visible="false" stepKey="checkRetailCustomerTaxClass" /> + <conditionalClick selector="{{AdminTaxRuleFormSection.productTaxClassOption(taxableGoodsTaxClass.class_name)}}" dependentSelector="{{AdminTaxRuleFormSection.productTaxClassSelected(taxableGoodsTaxClass.class_name)}}" visible="false" stepKey="checkTaxableGoodsTaxClass" /> + <click selector="{{AdminTaxRuleFormSection.customerTaxClassOption($$customerTaxClass.class_name$$)}}" stepKey="clickSelectCustomerTaxClass"/> + <click selector="{{AdminTaxRuleFormSection.productTaxClassOption($$productTaxClass.class_name$$)}}" stepKey="clickSelectProductTaxClass"/> + <fillField selector="{{AdminTaxRuleFormSection.priority}}" userInput="{{taxRuleWithCustomPriorityPosition.priority}}" stepKey="fillPriority"/> + <fillField selector="{{AdminTaxRuleFormSection.sortOrder}}" userInput="{{taxRuleWithCustomPriorityPosition.position}}" stepKey="fillSortOrder"/> + <click selector="{{AdminTaxRuleFormSection.save}}" stepKey="clickSaveTaxRule"/> + <waitForPageLoad stepKey="waitForTaxRuleSaved" /> + <!-- Verify we see success message --> + <see selector="{{AdminTaxRuleGridSection.successMessage}}" userInput="You saved the tax rule." stepKey="seeAssertTaxRuleSuccessMessage"/> + + <!-- Verify we see created tax rule with customer and product class(from the above step) on the tax rule grid page --> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> + <fillField selector="{{AdminTaxRuleGridSection.code}}" userInput="{{SimpleTaxRule.code}}" stepKey="fillTaxRuleCode2"/> + <click selector="{{AdminTaxRuleGridSection.search}}" stepKey="clickSearch2"/> + <waitForPageLoad stepKey="waitForTaxRuleSearch"/> + <click selector="{{AdminTaxRuleGridSection.nthRow('1')}}" stepKey="clickFirstRow2"/> + + <!-- Verify we see created tax rule with customer and product class on the tax rule form page --> + <seeInField selector="{{AdminTaxRuleFormSection.code}}" userInput="{{SimpleTaxRule.code}}" stepKey="seeInTaxRuleCode"/> + <seeElement selector="{{AdminTaxRuleFormSection.taxRateSelected($$initialTaxRate.code$$)}}" stepKey="seeTaxRateSelected"/> + <seeElement selector="{{AdminTaxRuleFormSection.taxRateSelected(US_CA_Rate_1.code)}}" stepKey="seeSelectNeededItem1"/> + <seeElement selector="{{AdminTaxRuleFormSection.taxRateSelected(US_NY_Rate_1.code)}}" stepKey="seeSelectNeededItem2"/> + <click selector="{{AdminTaxRuleFormSection.additionalSettings}}" stepKey="clickAdditionalSettings1"/> + <scrollTo selector="{{AdminTaxRuleFormSection.additionalSettings}}" x="0" y="-80" stepKey="scrollToAdvancedSettings1"/> + <seeElement selector="{{AdminTaxRuleFormSection.customerTaxClassSelected($$customerTaxClass.class_name$$)}}" stepKey="seeCustomerTaxClass"/> + <seeElement selector="{{AdminTaxRuleFormSection.customerTaxClassSelected(retailCustomerTaxClass.class_name)}}" stepKey="seeRetailCustomerTaxClass" /> + <seeElement selector="{{AdminTaxRuleFormSection.productTaxClassSelected($$productTaxClass.class_name$$)}}" stepKey="seeProductTaxClass"/> + <seeElement selector="{{AdminTaxRuleFormSection.productTaxClassSelected(taxableGoodsTaxClass.class_name)}}" stepKey="seeTaxableGoodsTaxClass" /> + <seeInField selector="{{AdminTaxRuleFormSection.priority}}" userInput="{{taxRuleWithCustomPriorityPosition.priority}}" stepKey="seePriority"/> + <seeInField selector="{{AdminTaxRuleFormSection.sortOrder}}" userInput="{{taxRuleWithCustomPriorityPosition.position}}" stepKey="seeSortOrder"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewAndExistingTaxRateAndCustomerAndProductTaxClassTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewAndExistingTaxRateAndCustomerAndProductTaxClassTest.xml new file mode 100644 index 000000000000..abff31803a16 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewAndExistingTaxRateAndCustomerAndProductTaxClassTest.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="AdminCreateTaxRuleWithNewAndExistingTaxRateAndCustomerAndProductTaxClassTest"> + <annotations> + <stories value="Create tax rule"/> + <title value="Test log in to Create Tax Rule and Create Tax Rule with New and Existing Tax Rate, Customer Tax Class, Product Tax Class"/> + <description value="Test log in to Create tax rule and Create tax rule with New and Existing Tax Rate, Customer Tax Class, Product Tax Class"/> + <testCaseId value="MC-5327"/> + <severity value="CRITICAL"/> + <group value="tax"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="taxRate_US_NY_8_1" stepKey="TaxRateWithCustomRate"/> + <createData entity="customerTaxClass" stepKey="createCustomerTaxClass"/> + <createData entity="productTaxClass" stepKey="createProductTaxClass"/> + <getData entity="customerTaxClass" stepKey="customerTaxClass"> + <requiredEntity createDataKey="createCustomerTaxClass"/> + </getData> + <getData entity="productTaxClass" stepKey="productTaxClass"> + <requiredEntity createDataKey="createProductTaxClass"/> + </getData> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminDeleteTaxRule" stepKey="deleteTaxRule"> + <argument name="taxRuleCode" value="{{SimpleTaxRule.code}}" /> + </actionGroup> + <deleteData stepKey="deleteTaxRate" createDataKey="TaxRateWithCustomRate"/> + <deleteData stepKey="deleteCustomerTaxClass" createDataKey="createCustomerTaxClass"/> + <deleteData stepKey="deleteProductTaxClass" createDataKey="createProductTaxClass"/> + </after> + + <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> + <waitForPageLoad stepKey="waitForTaxRuleIndex1"/> + <click selector="{{AdminTaxRuleGridSection.add}}" stepKey="clickAddNewTaxRuleButton"/> + <waitForPageLoad stepKey="waitForTaxRuleIndex2"/> + + <!-- Create a tax rule with new and existing tax rate, customer tax class, product tax class --> + <fillField selector="{{AdminTaxRuleFormSection.code}}" userInput="{{SimpleTaxRule.code}}" stepKey="fillTaxRuleCode1"/> + <fillField selector="{{AdminTaxRuleFormSection.taxRateSearch}}" userInput="$$TaxRateWithCustomRate.code$$" stepKey="fillTaxRateSearch"/> + <wait stepKey="waitForSearch" time="5" /> + <click selector="{{AdminTaxRuleFormSection.taxRateOption($$TaxRateWithCustomRate.code$$)}}" stepKey="clickSelectNeededItem"/> + <fillField selector="{{AdminTaxRuleFormSection.taxRateSearch}}" userInput="{{US_CA_Rate_1.code}}" stepKey="fillTaxRateSearch1"/> + <wait stepKey="waitForSearch2" time="5" /> + <conditionalClick selector="{{AdminTaxRuleFormSection.taxRateOption(US_CA_Rate_1.code)}}" dependentSelector="{{AdminTaxRuleFormSection.taxRateSelected(US_CA_Rate_1.code)}}" visible="false" stepKey="clickSelectNeededItem1"/> + <click selector="{{AdminTaxRuleFormSection.additionalSettings}}" stepKey="clickAdditionalSettings"/> + <scrollTo selector="{{AdminTaxRuleFormSection.additionalSettings}}" x="0" y="-80" stepKey="scrollToAdvancedSettings"/> + <conditionalClick selector="{{AdminTaxRuleFormSection.customerTaxClassOption(retailCustomerTaxClass.class_name)}}" dependentSelector="{{AdminTaxRuleFormSection.customerTaxClassSelected(retailCustomerTaxClass.class_name)}}" visible="false" stepKey="checkRetailCustomerTaxClass" /> + <conditionalClick selector="{{AdminTaxRuleFormSection.productTaxClassOption(taxableGoodsTaxClass.class_name)}}" dependentSelector="{{AdminTaxRuleFormSection.productTaxClassSelected(taxableGoodsTaxClass.class_name)}}" visible="false" stepKey="checkTaxableGoodsTaxClass" /> + <click selector="{{AdminTaxRuleFormSection.customerTaxClassOption($$customerTaxClass.class_name$$)}}" stepKey="clickSelectCustomerTaxClass"/> + <click selector="{{AdminTaxRuleFormSection.productTaxClassOption($$productTaxClass.class_name$$)}}" stepKey="clickSelectProductTaxClass"/> + <click selector="{{AdminTaxRuleFormSection.save}}" stepKey="clickSaveTaxRule"/> + <waitForPageLoad stepKey="waitForTaxRuleSaved" /> + <!-- Verify we see success message --> + <see selector="{{AdminTaxRuleGridSection.successMessage}}" userInput="You saved the tax rule." stepKey="seeAssertTaxRuleSuccessMessage"/> + + <!-- Verify we see created tax rule with new and existing tax rate, customer tax class, product tax class(from the above step) on the tax rule grid page --> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> + <fillField selector="{{AdminTaxRuleGridSection.code}}" userInput="{{SimpleTaxRule.code}}" stepKey="fillTaxRuleCode2"/> + <click selector="{{AdminTaxRuleGridSection.search}}" stepKey="clickSearch2"/> + <waitForPageLoad stepKey="waitForTaxRuleSearch"/> + <click selector="{{AdminTaxRuleGridSection.nthRow('1')}}" stepKey="clickFirstRow2"/> + + <!-- Verify we see created tax rule with new and existing tax rate, customer tax class, product tax class on the tax rule form page --> + <seeInField selector="{{AdminTaxRuleFormSection.code}}" userInput="{{SimpleTaxRule.code}}" stepKey="seeInTaxRuleCode"/> + <fillField selector="{{AdminTaxRuleFormSection.taxRateSearch}}" userInput="$$TaxRateWithCustomRate.code$$" stepKey="fillTaxRateSearch3"/> + <seeElement selector="{{AdminTaxRuleFormSection.taxRateSelected($$TaxRateWithCustomRate.code$$)}}" stepKey="seeTaxRateSelected"/> + <fillField selector="{{AdminTaxRuleFormSection.taxRateSearch}}" userInput="{{US_CA_Rate_1.code}}" stepKey="fillTaxRateSearch4"/> + <seeElement selector="{{AdminTaxRuleFormSection.taxRateSelected(US_CA_Rate_1.code)}}" stepKey="seeSelectNeededItem1"/> + <click selector="{{AdminTaxRuleFormSection.additionalSettings}}" stepKey="clickAdditionalSettings1"/> + <scrollTo selector="{{AdminTaxRuleFormSection.additionalSettings}}" x="0" y="-80" stepKey="scrollToAdvancedSettings1"/> + <seeElement selector="{{AdminTaxRuleFormSection.customerTaxClassSelected($$customerTaxClass.class_name$$)}}" stepKey="seeCustomerTaxClass"/> + <seeElement selector="{{AdminTaxRuleFormSection.customerTaxClassSelected(retailCustomerTaxClass.class_name)}}" stepKey="seeRetailCustomerTaxClass" /> + <seeElement selector="{{AdminTaxRuleFormSection.productTaxClassSelected($$productTaxClass.class_name$$)}}" stepKey="seeProductTaxClass"/> + <seeElement selector="{{AdminTaxRuleFormSection.productTaxClassSelected(taxableGoodsTaxClass.class_name)}}" stepKey="seeTaxableGoodsTaxClass" /> + </test> +</tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewTaxClassesAndTaxRateTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewTaxClassesAndTaxRateTest.xml new file mode 100644 index 000000000000..6e9b81743ddf --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewTaxClassesAndTaxRateTest.xml @@ -0,0 +1,90 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateTaxRuleWithNewTaxClassesAndTaxRateTest"> + <annotations> + <stories value="Create tax rule"/> + <title value="Creating tax rule with new tax classes and tax rate"/> + <description value="Test log in to Create Tax Rule and Create tax rule with New Tax Classes and Tax Rate"/> + <testCaseId value="MC-5325"/> + <severity value="CRITICAL"/> + <group value="tax"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="defaultTaxRate" stepKey="initialTaxRate"/> + <createData entity="customerTaxClass" stepKey="createCustomerTaxClass"/> + <createData entity="productTaxClass" stepKey="createProductTaxClass"/> + <getData entity="customerTaxClass" stepKey="customerTaxClass"> + <requiredEntity createDataKey="createCustomerTaxClass"/> + </getData> + <getData entity="productTaxClass" stepKey="productTaxClass"> + <requiredEntity createDataKey="createProductTaxClass"/> + </getData> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminDeleteTaxRule" stepKey="deleteTaxRule"> + <argument name="taxRuleCode" value="{{SimpleTaxRule.code}}" /> + </actionGroup> + <deleteData stepKey="deleteTaxRate" createDataKey="initialTaxRate"/> + <deleteData stepKey="deleteCustomerTaxClass" createDataKey="createCustomerTaxClass"/> + <deleteData stepKey="deleteProductTaxClass" createDataKey="createProductTaxClass"/> + </after> + + <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> + <waitForPageLoad stepKey="waitForTaxRuleIndex1"/> + <click selector="{{AdminTaxRuleGridSection.add}}" stepKey="clickAddNewTaxRuleButton"/> + <waitForPageLoad stepKey="waitForTaxRuleIndex2"/> + + <!-- Create a tax rule with new tax classes and tax rate --> + <fillField selector="{{AdminTaxRuleFormSection.code}}" userInput="{{SimpleTaxRule.code}}" stepKey="fillTaxRuleCode1"/> + <fillField selector="{{AdminTaxRuleFormSection.taxRateSearch}}" userInput="$$initialTaxRate.code$$" stepKey="fillTaxRateSearch"/> + <wait stepKey="waitForSearch" time="5" /> + <click selector="{{AdminTaxRuleFormSection.taxRateOption($$initialTaxRate.code$$)}}" stepKey="clickSelectNeededItem"/> + <fillField selector="{{AdminTaxRuleFormSection.taxRateSearch}}" userInput="{{US_CA_Rate_1.code}}" stepKey="fillTaxRateSearch1"/> + <wait stepKey="waitForSearch2" time="5" /> + <conditionalClick selector="{{AdminTaxRuleFormSection.taxRateOption(US_CA_Rate_1.code)}}" dependentSelector="{{AdminTaxRuleFormSection.taxRateSelected(US_CA_Rate_1.code)}}" visible="false" stepKey="clickSelectNeededItem1"/> + <fillField selector="{{AdminTaxRuleFormSection.taxRateSearch}}" userInput="{{US_NY_Rate_1.code}}" stepKey="fillTaxRateSearch2"/> + <wait stepKey="waitForSearch3" time="5" /> + <conditionalClick selector="{{AdminTaxRuleFormSection.taxRateOption(US_NY_Rate_1.code)}}" dependentSelector="{{AdminTaxRuleFormSection.taxRateSelected(US_NY_Rate_1.code)}}" visible="false" stepKey="clickSelectNeededItem2"/> + <click selector="{{AdminTaxRuleFormSection.additionalSettings}}" stepKey="clickAdditionalSettings"/> + <scrollTo selector="{{AdminTaxRuleFormSection.additionalSettings}}" x="0" y="-80" stepKey="scrollToAdvancedSettings"/> + <conditionalClick selector="{{AdminTaxRuleFormSection.customerTaxClassOption(retailCustomerTaxClass.class_name)}}" dependentSelector="{{AdminTaxRuleFormSection.customerTaxClassSelected(retailCustomerTaxClass.class_name)}}" visible="false" stepKey="checkRetailCustomerTaxClass" /> + <conditionalClick selector="{{AdminTaxRuleFormSection.productTaxClassOption(taxableGoodsTaxClass.class_name)}}" dependentSelector="{{AdminTaxRuleFormSection.productTaxClassSelected(taxableGoodsTaxClass.class_name)}}" visible="false" stepKey="checkTaxableGoodsTaxClass" /> + <click selector="{{AdminTaxRuleFormSection.customerTaxClassOption($$customerTaxClass.class_name$$)}}" stepKey="clickSelectCustomerTaxClass"/> + <click selector="{{AdminTaxRuleFormSection.productTaxClassOption($$productTaxClass.class_name$$)}}" stepKey="clickSelectProductTaxClass"/> + <fillField selector="{{AdminTaxRuleFormSection.sortOrder}}" userInput="{{taxRuleWithCustomPosition.position}}" stepKey="fillSortOrder"/> + <click selector="{{AdminTaxRuleFormSection.save}}" stepKey="clickSaveTaxRule"/> + <waitForPageLoad stepKey="waitForTaxRuleSaved" /> + <!-- Verify we see success message --> + <see selector="{{AdminTaxRuleGridSection.successMessage}}" userInput="You saved the tax rule." stepKey="seeAssertTaxRuleSuccessMessage"/> + + <!-- Verify we see created tax rule with new tax classes and tax rate(from the above step) on the tax rule grid page --> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> + <fillField selector="{{AdminTaxRuleGridSection.code}}" userInput="{{SimpleTaxRule.code}}" stepKey="fillTaxRuleCode2"/> + <click selector="{{AdminTaxRuleGridSection.search}}" stepKey="clickSearch2"/> + <waitForPageLoad stepKey="waitForTaxRuleSearch"/> + <click selector="{{AdminTaxRuleGridSection.nthRow('1')}}" stepKey="clickFirstRow2"/> + + <!-- Verify we see created tax rule with new tax classes and tax rate on the tax rule form page --> + <seeInField selector="{{AdminTaxRuleFormSection.code}}" userInput="{{SimpleTaxRule.code}}" stepKey="seeInTaxRuleCode"/> + <seeElement selector="{{AdminTaxRuleFormSection.taxRateSelected($$initialTaxRate.code$$)}}" stepKey="seeTaxRateSelected"/> + <seeElement selector="{{AdminTaxRuleFormSection.taxRateSelected(US_CA_Rate_1.code)}}" stepKey="seeSelectNeededItem1"/> + <seeElement selector="{{AdminTaxRuleFormSection.taxRateSelected(US_NY_Rate_1.code)}}" stepKey="seeSelectNeededItem2"/> + <click selector="{{AdminTaxRuleFormSection.additionalSettings}}" stepKey="clickAdditionalSettings1"/> + <scrollTo selector="{{AdminTaxRuleFormSection.additionalSettings}}" x="0" y="-80" stepKey="scrollToAdvancedSettings1"/> + <seeElement selector="{{AdminTaxRuleFormSection.customerTaxClassSelected($$customerTaxClass.class_name$$)}}" stepKey="seeCustomerTaxClass"/> + <seeElement selector="{{AdminTaxRuleFormSection.customerTaxClassSelected(retailCustomerTaxClass.class_name)}}" stepKey="seeRetailCustomerTaxClass" /> + <seeElement selector="{{AdminTaxRuleFormSection.productTaxClassSelected(taxableGoodsTaxClass.class_name)}}" stepKey="seeTaxableGoodsTaxClass" /> + <seeInField selector="{{AdminTaxRuleFormSection.sortOrder}}" userInput="{{taxRuleWithCustomPosition.position}}" stepKey="seeSortOrder"/> + </test> +</tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithZipRangeTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithZipRangeTest.xml new file mode 100644 index 000000000000..6a9bf30811ff --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithZipRangeTest.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="AdminCreateTaxRuleWithZipRangeTest"> + <annotations> + <stories value="Create tax rule"/> + <title value="Create tax rule, with zip range"/> + <description value="Test log in to Create Tax Rule and Create tax rule with Zip Range"/> + <testCaseId value="MC-5326"/> + <severity value="CRITICAL"/> + <group value="tax"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="defaultTaxRateWithZipRange" stepKey="taxRateWithZipRange"/> + <createData entity="customerTaxClass" stepKey="createCustomerTaxClass"/> + <createData entity="productTaxClass" stepKey="createProductTaxClass"/> + <getData entity="customerTaxClass" stepKey="customerTaxClass"> + <requiredEntity createDataKey="createCustomerTaxClass"/> + </getData> + <getData entity="productTaxClass" stepKey="productTaxClass"> + <requiredEntity createDataKey="createProductTaxClass"/> + </getData> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminDeleteTaxRule" stepKey="deleteTaxRule"> + <argument name="taxRuleCode" value="{{SimpleTaxRule.code}}" /> + </actionGroup> + <deleteData stepKey="deleteTaxRate" createDataKey="taxRateWithZipRange"/> + <deleteData stepKey="deleteCustomerTaxClass" createDataKey="createCustomerTaxClass"/> + <deleteData stepKey="deleteProductTaxClass" createDataKey="createProductTaxClass"/> + </after> + + <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> + <waitForPageLoad stepKey="waitForTaxRuleIndex1"/> + <click selector="{{AdminTaxRuleGridSection.add}}" stepKey="clickAddNewTaxRuleButton"/> + <waitForPageLoad stepKey="waitForTaxRuleIndex2"/> + + <!-- Create a tax rule with new tax classes and tax rate --> + <fillField selector="{{AdminTaxRuleFormSection.code}}" userInput="{{SimpleTaxRule.code}}" stepKey="fillTaxRuleCode1"/> + <fillField selector="{{AdminTaxRuleFormSection.taxRateSearch}}" userInput="$$taxRateWithZipRange.code$$" stepKey="fillTaxRateSearch"/> + <wait stepKey="waitForSearch" time="5" /> + <click selector="{{AdminTaxRuleFormSection.taxRateOption($$taxRateWithZipRange.code$$)}}" stepKey="clickSelectNeededItem"/> + <fillField selector="{{AdminTaxRuleFormSection.taxRateSearch}}" userInput="{{US_CA_Rate_1.code}}" stepKey="fillTaxRateSearch1"/> + <wait stepKey="waitForSearch2" time="5" /> + <conditionalClick selector="{{AdminTaxRuleFormSection.taxRateOption(US_CA_Rate_1.code)}}" dependentSelector="{{AdminTaxRuleFormSection.taxRateSelected(US_CA_Rate_1.code)}}" visible="false" stepKey="clickSelectNeededItem1"/> + <click selector="{{AdminTaxRuleFormSection.additionalSettings}}" stepKey="clickAdditionalSettings"/> + <scrollTo selector="{{AdminTaxRuleFormSection.additionalSettings}}" x="0" y="-80" stepKey="scrollToAdvancedSettings"/> + <wait stepKey="waitForAdditionalSettings" time="5" /> + <conditionalClick selector="{{AdminTaxRuleFormSection.customerTaxClassOption(retailCustomerTaxClass.class_name)}}" dependentSelector="{{AdminTaxRuleFormSection.customerTaxClassSelected(retailCustomerTaxClass.class_name)}}" visible="false" stepKey="checkRetailCustomerTaxClass" /> + <conditionalClick selector="{{AdminTaxRuleFormSection.productTaxClassOption(taxableGoodsTaxClass.class_name)}}" dependentSelector="{{AdminTaxRuleFormSection.productTaxClassSelected(taxableGoodsTaxClass.class_name)}}" visible="false" stepKey="checkTaxableGoodsTaxClass" /> + <click selector="{{AdminTaxRuleFormSection.customerTaxClassOption($$customerTaxClass.class_name$$)}}" stepKey="clickSelectCustomerTaxClass"/> + <click selector="{{AdminTaxRuleFormSection.productTaxClassOption($$productTaxClass.class_name$$)}}" stepKey="clickSelectProductTaxClass"/> + <fillField selector="{{AdminTaxRuleFormSection.priority}}" userInput="{{taxRuleWithCustomPriority.priority}}" stepKey="fillPriority"/> + <click selector="{{AdminTaxRuleFormSection.save}}" stepKey="clickSaveTaxRule"/> + <waitForPageLoad stepKey="waitForTaxRuleSaved" /> + <!-- Verify we see success message --> + <see selector="{{AdminTaxRuleGridSection.successMessage}}" userInput="You saved the tax rule." stepKey="seeAssertTaxRuleSuccessMessage"/> + + <!-- Verify we see created tax rule with zip range(from the above step) on the tax rule grid page --> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> + <fillField selector="{{AdminTaxRuleGridSection.code}}" userInput="{{SimpleTaxRule.code}}" stepKey="fillTaxRuleCode2"/> + <click selector="{{AdminTaxRuleGridSection.search}}" stepKey="clickSearch2"/> + <waitForPageLoad stepKey="waitForTaxRuleSearch"/> + <click selector="{{AdminTaxRuleGridSection.nthRow('1')}}" stepKey="clickFirstRow2"/> + + <!-- Verify we see created tax rule with zip range on the tax rule form page --> + <seeInField selector="{{AdminTaxRuleFormSection.code}}" userInput="{{SimpleTaxRule.code}}" stepKey="seeInTaxRuleCode"/> + <seeElement selector="{{AdminTaxRuleFormSection.taxRateSelected($$taxRateWithZipRange.code$$)}}" stepKey="seeTaxRateSelected"/> + <seeElement selector="{{AdminTaxRuleFormSection.taxRateSelected(US_CA_Rate_1.code)}}" stepKey="seeSelectNeededItem1"/> + <click selector="{{AdminTaxRuleFormSection.additionalSettings}}" stepKey="clickAdditionalSettings1"/> + <scrollTo selector="{{AdminTaxRuleFormSection.additionalSettings}}" x="0" y="-80" stepKey="scrollToAdvancedSettings1"/> + <seeElement selector="{{AdminTaxRuleFormSection.customerTaxClassSelected($$customerTaxClass.class_name$$)}}" stepKey="seeCustomerTaxClass"/> + <seeElement selector="{{AdminTaxRuleFormSection.customerTaxClassSelected(retailCustomerTaxClass.class_name)}}" stepKey="seeRetailCustomerTaxClass" /> + <seeElement selector="{{AdminTaxRuleFormSection.productTaxClassSelected($$productTaxClass.class_name$$)}}" stepKey="seeProductTaxClass"/> + <seeElement selector="{{AdminTaxRuleFormSection.productTaxClassSelected(taxableGoodsTaxClass.class_name)}}" stepKey="seeTaxableGoodsTaxClass" /> + <seeInField selector="{{AdminTaxRuleFormSection.priority}}" userInput="{{taxRuleWithCustomPriority.priority}}" stepKey="seePriority"/> + </test> +</tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminDeleteTaxRuleTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminDeleteTaxRuleTest.xml new file mode 100644 index 000000000000..72adf7b0dae1 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminDeleteTaxRuleTest.xml @@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminDeleteTaxRuleTest"> + <annotations> + <stories value="Delete tax rule"/> + <title value="Delete tax rule"/> + <description value="Test log in to tax rule and Delete tax rule"/> + <testCaseId value="MC-5823"/> + <severity value="CRITICAL"/> + <group value="tax"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="defaultTaxRule" stepKey="initialTaxRule"/> + <createData entity="ApiSimplePrice100Qty100v2" stepKey="simpleProduct"/> + <createData entity="Simple_US_Utah_Customer" stepKey="customer" /> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData stepKey="deleteSimpleProduct" createDataKey="simpleProduct" /> + <deleteData stepKey="deleteCustomer" createDataKey="customer" /> + </after> + + <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> + <waitForPageLoad stepKey="waitForTaxRuleIndex1"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> + <fillField selector="{{AdminTaxRuleGridSection.code}}" userInput="$$initialTaxRule.code$$" stepKey="fillTaxCodeSearch"/> + <click selector="{{AdminTaxRuleGridSection.search}}" stepKey="clickSearch1"/> + <click selector="{{AdminTaxRuleGridSection.nthRow('1')}}" stepKey="clickFirstRow1"/> + + <!-- Delete values on the tax rule form page --> + <click selector="{{AdminTaxRuleFormSection.deleteRule}}" stepKey="clickDeleteRule"/> + <click selector="{{AdminTaxRuleFormSection.ok}}" stepKey="clickOk"/> + <waitForPageLoad stepKey="waitForTaxRuleDeleted" /> + + <!-- Verify we see success message --> + <see selector="{{AdminMessagesSection.success}}" userInput="The tax rule has been deleted." stepKey="seeAssertTaxRuleDeleteMessage"/> + + <!-- Confirm Deleted Tax Rule(from the above step) on the tax rule grid page --> + <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex2"/> + <waitForPageLoad stepKey="waitForTaxRuleIndex2"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> + <fillField selector="{{AdminTaxRuleGridSection.code}}" userInput="$$initialTaxRule.code$$" stepKey="fillTaxCodeSearch2"/> + <click selector="{{AdminTaxRuleGridSection.search}}" stepKey="clickSearch2"/> + <see selector="{{AdminTaxRuleGridSection.emptyText}}" userInput="We couldn't find any records." stepKey="seeAssertTaxRuleNotFound"/> + + <!-- Verify customer don't tax on the store front product page --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> + <argument name="Customer" value="$$customer$$" /> + </actionGroup> + <amOnPage url="{{StorefrontProductPage.url($$simpleProduct.custom_attributes[url_key]$$)}}" stepKey="goToProductPageOnStorefront"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$simpleProduct$$" /> + <argument name="productCount" value="1" /> + </actionGroup> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openShoppingCart" /> + <actionGroup ref="FillShippingZipForm" stepKey="fillShippingZipForm"> + <argument name="address" value="US_Address_Utah" /> + </actionGroup> + <scrollTo selector="{{StorefrontProductPageSection.orderTotal}}" x="0" y="-80" stepKey="scrollToOrderTotal"/> + <waitForElementVisible selector="{{StorefrontProductPageSection.subTotal}}" time="30" stepKey="waitSubtotalAppears"/> + <see selector="{{StorefrontProductPageSection.subTotal}}" userInput="$100.00" stepKey="seeSubTotal"/> + <waitForElementVisible selector="{{StorefrontProductPageSection.shipping}}" time="30" stepKey="waitShippingAppears"/> + <see selector="{{StorefrontProductPageSection.shipping}}" userInput="$5.00" stepKey="seeShipping"/> + <dontSee selector="{{StorefrontProductPageSection.tax}}" stepKey="dontSeeAssertTaxAmount" /> + <see selector="{{StorefrontProductPageSection.orderTotal}}" userInput="$105.00" stepKey="seeAssertOrderTotal"/> + </test> +</tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminStoresTaxRulesNavigateMenuTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminStoresTaxRulesNavigateMenuTest.xml new file mode 100644 index 000000000000..1277d6e5f9fd --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminStoresTaxRulesNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminStoresTaxRulesNavigateMenuTest"> + <annotations> + <features value="Tax"/> + <stories value="Menu Navigation"/> + <title value="Admin stores tax rules navigate menu test"/> + <description value="Admin should be able to navigate to Stores > Tax Rules"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14175"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToStoresTaxRulesPage"> + <argument name="menuUiId" value="{{AdminMenuStores.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuStoresTaxesTaxRules.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuStoresTaxesTaxRules.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminStoresTaxZonesAndRatesNavigateMenuTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminStoresTaxZonesAndRatesNavigateMenuTest.xml new file mode 100644 index 000000000000..e0a4d5d9a401 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminStoresTaxZonesAndRatesNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminStoresTaxZonesAndRatesNavigateMenuTest"> + <annotations> + <features value="Tax"/> + <stories value="Menu Navigation"/> + <title value="Admin stores tax zones and rates navigate menu test"/> + <description value="Admin should be able to navigate to Stores > Tax Zones and Rates"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14176"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToStoresTaxZonesAndRatesPage"> + <argument name="menuUiId" value="{{AdminMenuStores.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuStoresTaxZonesAndRates.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuStoresTaxZonesAndRates.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminSystemImportExportTaxRatesNavigateMenuTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminSystemImportExportTaxRatesNavigateMenuTest.xml new file mode 100644 index 000000000000..a84ae61d6630 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminSystemImportExportTaxRatesNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminSystemImportExportTaxRatesNavigateMenuTest"> + <annotations> + <features value="Tax"/> + <stories value="Menu Navigation"/> + <title value="Admin system import export tax rates navigate menu test"/> + <description value="Admin should be able to navigate to System > Import/Export Tax Rates"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14177"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToSystemImportExportTaxRatesPage"> + <argument name="menuUiId" value="{{AdminMenuSystem.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSystemDataTransferImportAndExportTaxRates.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSystemDataTransferImportAndExportTaxRates.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminTaxCalcWithApplyTaxOnSettingTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminTaxCalcWithApplyTaxOnSettingTest.xml new file mode 100644 index 000000000000..732470d2558c --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminTaxCalcWithApplyTaxOnSettingTest.xml @@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminTaxCalcWithApplyTaxOnSettingTest"> + <annotations> + <features value="AdminTaxCalcWithApplyTaxOnSettingTest"/> + <title value="Tax calculation process following 'Apply Tax On' setting"/> + <description value="Tax calculation process following 'Apply Tax On' setting"/> + <severity value="MAJOR"/> + <testCaseId value="MC-11026"/> + <useCaseId value="MC-4316"/> + <group value="Tax"/> + </annotations> + + <before> + <createData entity="taxRateForPensylvannia" stepKey="initialTaxRate"/> + <createData entity="defaultTaxRule" stepKey="createTaxRule"/> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProductWithCustomPrice" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="LoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="SetTaxClassForShipping" stepKey="setTaxClass"/> + <actionGroup ref="SetTaxApplyOnSetting" stepKey="setApplyTaxOnSetting"> + <argument name="userInput" value="Original price only"/> + </actionGroup> + <amOnPage url="{{AdminEditTaxRulePage.url($$createTaxRule.id$$)}}" stepKey="navigateToEditTaxRulePage"/> + <waitForPageLoad stepKey="waitEditTaxRulePageToLoad"/> + <click selector="{{AdminTaxRulesSection.taxRateNumber('1')}}" stepKey="clickonTaxRate"/> + <click selector="{{AdminTaxRulesSection.deleteTaxClassName($$initialTaxRate.code$$)}}" stepKey="checkTaxRate"/> + <click selector="{{AdminTaxRulesSection.saveRule}}" stepKey="saveChanges"/> + <waitForPageLoad stepKey="waitTaxRulesToBeSaved"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rule." stepKey="seeSuccessMessage2"/> + </before> + <after> + <deleteData createDataKey="createProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createTaxRule" stepKey="deleteTaxRule"/> + <deleteData stepKey="deleteTaxRate" createDataKey="initialTaxRate" /> + <actionGroup ref="DisableTaxApplyOnOriginalPrice" stepKey="setApplyTaxOffSetting"> + <argument name="userInput" value="Custom price if available"/> + </actionGroup> + <actionGroup ref="ResetTaxClassForShipping" stepKey="resetTaxClassForShipping"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <actionGroup ref="navigateToNewOrderPageNewCustomerSingleStore" stepKey="gotoNewOrderCreationPage"/> + <actionGroup ref="addSimpleProductToOrder" stepKey="addSimpleProductToOrder"> + <argument name="product" value="$$createProduct$$"></argument> + </actionGroup> + <fillField selector="{{AdminOrderFormAccountSection.email}}" userInput="{{Simple_US_Customer.email}}" stepKey="fillEmailField"/> + <actionGroup ref="fillOrderCustomerInformation" stepKey="fillCustomerAddress"> + <argument name="customer" value="Simple_US_Customer"/> + <argument name="address" value="US_Address_CA"/> + </actionGroup> + <scrollTo selector="{{AdminOrderFormAccountSection.email}}" stepKey="scrollToEmailField"/> + <waitForElementVisible selector="{{AdminOrderFormAccountSection.email}}" stepKey="waitEmailFieldToBeVisible"/> + <click selector="{{AdminOrderFormShippingAddressSection.SameAsBilling}}" stepKey="uncheckSameAsBillingAddressCheckbox"/> + <waitForPageLoad stepKey="waitSectionToReload"/> + <selectOption selector="{{AdminOrderFormShippingAddressSection.State}}" stepKey="switchOnVisibleInAdvancedSearch" userInput="Pennsylvania"/> + <click selector="{{AdminOrderFormPaymentSection.getShippingMethods}}" stepKey="getShippingMethods"/> + <waitForPageLoad stepKey="waitForApplyingShippingMethods"/> + <grabTextFrom selector="{{AdminOrderFormTotalSection.subtotalRow('3')}}" stepKey="grabTaxCost"/> + <assertEquals expected='$6.00' expectedType="string" actual="($grabTaxCost)" stepKey="assertTax"/> + <scrollTo selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="scrollToSubmitButton"/> + <waitForElementVisible selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="waitElementToBeVisble"/> + <click selector="{{AdminOrderFormItemsSection.customPriceCheckbox}}" stepKey="clickOnCustomPriceCheckbox"/> + <fillField selector="{{AdminOrderFormItemsSection.customPriceField}}" userInput="{{SimpleProductNameWithDoubleQuote.price}}" stepKey="changePrice"/> + <click selector="{{AdminOrderFormItemsSection.updateItemsAndQuantities}}" stepKey="updateItemsAndQunatities"/> + <assertEquals expected='$6.00' expectedType="string" actual="($grabTaxCost)" stepKey="assertTaxAfterCustomPrice"/> + </test> +</tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminTaxReportGridTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminTaxReportGridTest.xml new file mode 100644 index 000000000000..628d189823a5 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminTaxReportGridTest.xml @@ -0,0 +1,225 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminTaxReportGridTest"> + <annotations> + <features value="Tax"/> + <stories value="MAGETWO-91521: Reports / Sales / Tax report show incorrect amount"/> + <title value="Checking Tax Report grid"/> + <description value="Tax Report Grid displays Tax amount in rows 'Total' and 'Subtotal' is a sum of all tax amounts"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-94338"/> + <group value="Tax"/> + <skip> + <issueId value="MAGETWO-96193"/> + </skip> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Go to tax rule page --> + <actionGroup ref="addNewTaxRuleActionGroup" stepKey="addFirstTaxRuleActionGroup"/> + <fillField stepKey="fillRuleName" selector="{{AdminTaxRulesSection.ruleName}}" userInput="TaxRule1"/> + + <!-- Add NY and CA tax rules --> + <actionGroup ref="addNewTaxRateNoZip" stepKey="addNYTaxRate"> + <argument name="taxCode" value="SimpleTaxWithZipCode"/> + </actionGroup> + + <actionGroup ref="addProductTaxClass" stepKey="addProductTaxClass"> + <argument name="prodTaxClassName" value="TaxClasses1"/> + </actionGroup> + + <click stepKey="disableDefaultProdTaxClass" selector="{{AdminTaxRulesSection.defaultTaxClass}}"/> + <waitForPageLoad stepKey="waitForTaxRulePage"/> + <click stepKey="clickSave" selector="{{AdminTaxRulesSection.saveRule}}"/> + <waitForPageLoad stepKey="waitForNewTaxRuleCreated"/> + + <!-- Go to tax rule page to create second Tax Rule--> + <actionGroup ref="addNewTaxRuleActionGroup" stepKey="addSecondTaxRuleActionGroup"/> + <fillField stepKey="fillSecondRuleName" selector="{{AdminTaxRulesSection.ruleName}}" userInput="TaxRule2"/> + + <actionGroup ref="addNewTaxRateNoZip" stepKey="addCATaxRate"> + <argument name="taxCode" value="SimpleSecondTaxWithZipCode"/> + </actionGroup> + + <actionGroup ref="addProductTaxClass" stepKey="addSecondProductTaxClass"> + <argument name="prodTaxClassName" value="TaxClasses2"/> + </actionGroup> + + <click stepKey="disableSecondProdTaxClass" selector="{{AdminTaxRulesSection.defaultTaxClass}}"/> + <waitForPageLoad stepKey="waitForTaxRulePage2"/> + <click stepKey="clickSaveBtn" selector="{{AdminTaxRulesSection.saveRule}}"/> + <waitForPageLoad stepKey="waitForSecondTaxRuleCreated"/> + + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="firstProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <createData entity="_defaultProduct" stepKey="secondProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + + <!--Open Created products. In Tax Class select new created Product Tax classes.--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex"/> + <waitForPageLoad stepKey="wait1"/> + <actionGroup ref="resetProductGridToDefaultView" stepKey="resetProductGrid"/> + <actionGroup ref="filterProductGridBySku" stepKey="filterProductGridBySku"> + <argument name="product" value="$$firstProduct$$"/> + </actionGroup> + <actionGroup ref="openProducForEditByClickingRowXColumnYInProductGrid" stepKey="openFirstProductForEdit"/> + <selectOption selector="{{AdminProductFormSection.productTaxClass}}" stepKey="selectTexClassForFirstProduct" userInput="TaxClasses1"/> + <!-- Save the second product --> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveFirstProduct"/> + <waitForPageLoad stepKey="waitForFirstProductSaved"/> + + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="againGoToProductIndex"/> + <waitForPageLoad stepKey="wait2"/> + <actionGroup ref="resetProductGridToDefaultView" stepKey="resetSecondProductGrid"/> + <actionGroup ref="filterProductGridBySku" stepKey="filterSecondProductGridBySku"> + <argument name="product" value="$$secondProduct$$"/> + </actionGroup> + <actionGroup ref="openProducForEditByClickingRowXColumnYInProductGrid" stepKey="openSecondProductForEdit"/> + <selectOption selector="{{AdminProductFormSection.productTaxClass}}" stepKey="selectTexClassForSecondProduct" userInput="TaxClasses2"/> + <!-- Save the second product --> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveSecondProduct"/> + <waitForPageLoad stepKey="waitForSecondProductSaved"/> + + <!--Create an order with these 2 products in that zip code.--> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="navigateToOrderIndexPage"/> + <waitForPageLoad stepKey="waitForIndexPageLoad"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Orders" stepKey="seeIndexPageTitle"/> + <click selector="{{AdminOrdersGridSection.createNewOrder}}" stepKey="clickCreateNewOrder"/> + <click selector="{{AdminOrderFormActionSection.CreateNewCustomer}}" stepKey="clickCreateCustomer"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Create New Order" stepKey="seeNewOrderPageTitle"/> + <waitForPageLoad stepKey="waitForPage" time="60"/> + <!--Check if order can be submitted without the required fields including email address--> + <scrollToTopOfPage stepKey="scrollToTopOfOrderFormPage" after="seeNewOrderPageTitle"/> + <actionGroup ref="addSimpleProductToOrder" stepKey="addFirstProductToOrder" after="scrollToTopOfOrderFormPage"> + <argument name="product" value="$$firstProduct$$"/> + </actionGroup> + <actionGroup ref="addSimpleProductToOrder" stepKey="addSecondProductToOrder" after="addFirstProductToOrder"> + <argument name="product" value="$$secondProduct$$"/> + </actionGroup> + + <!--Fill customer group and customer email--> + <selectOption selector="{{AdminOrderFormAccountSection.group}}" userInput="{{GeneralCustomerGroup.code}}" stepKey="selectCustomerGroup" after="addSecondProductToOrder"/> + <fillField selector="{{AdminOrderFormAccountSection.email}}" userInput="{{Simple_US_Customer.email}}" stepKey="fillCustomerEmail" after="selectCustomerGroup"/> + + <!--Fill customer address information--> + <actionGroup ref="fillOrderCustomerInformation" stepKey="fillCustomerAddress" after="fillCustomerEmail"> + <argument name="customer" value="Simple_US_Customer"/> + <argument name="address" value="US_Address_TX"/> + </actionGroup> + + <!-- Select shipping --> + <actionGroup ref="orderSelectFlatRateShipping" stepKey="selectFlatRateShipping" after="fillCustomerAddress"/> + <!-- Checkout select Check/Money Order payment --> + <actionGroup ref="SelectCheckMoneyPaymentMethod" after="selectFlatRateShipping" stepKey="selectCheckMoneyPayment"/> + <!--Submit Order and verify information--> + <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" after="selectCheckMoneyPayment" stepKey="clickSubmitOrder"/> + <seeInCurrentUrl url="{{AdminOrderDetailsPage.url}}" stepKey="seeViewOrderPage" after="clickSubmitOrder"/> + <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="You created the order." stepKey="seeSuccessMessage" after="seeViewOrderPage"/> + + <!--Create Invoice and Shipment for this Order.--> + <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceButton"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Invoice" stepKey="seeNewInvoiceInPageTitle" after="clickInvoiceButton"/> + <waitForPageLoad stepKey="waitForInvoicePageOpened"/> + + <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + + <click selector="{{AdminOrderDetailsMainActionsSection.ship}}" stepKey="clickShipAction"/> + <seeInCurrentUrl url="{{AdminShipmentNewPage.url}}" stepKey="seeOrderShipmentUrl" after="clickShipAction"/> + <!--Submit Shipment--> + <click selector="{{AdminShipmentMainActionsSection.submitShipment}}" stepKey="clickSubmitShipment" after="seeOrderShipmentUrl"/> + <waitForPageLoad stepKey="waitForShipmentSaved"/> + + <!--Go to "Reports" -> "Sales" -> "Tax"--> + <amOnPage url="/admin/reports/report_sales/tax/" stepKey="navigateToReportsTaxPage"/> + <waitForPageLoad stepKey="waitForReportsTaxPageLoad"/> + + <!--click "here" to refresh last day's statistics --> + <click stepKey="clickRefrashStatisticsHere" selector="{{AdminTaxReportsSection.refreshStatistics}}"/> + <waitForPageLoad stepKey="waitForRefresh"/> + + <!--Select Dates--> + <fillField selector="{{AdminTaxReportsSection.fromDate}}" userInput="05/16/2018" stepKey="fillDateFrom"/> + <click selector="{{AdminTaxReportsSection.toDate}}" stepKey="clickDateTo"/> + <click selector="{{AdminTaxReportsSection.goTodayButton}}" stepKey="clickGoTodayDate"/> + <!--Click "Show report" in the upper right corner.--> + <click selector="{{AdminTaxReportsSection.showReportButton}}" stepKey="clickShowReportButton"/> + <waitForPageLoad time="60" stepKey="waitForReload"/> + <!--Tax Report Grid displays Tax amount in rows. "Total" and "Subtotal" is a sum of all tax amounts--> + <grabTextFrom selector="{{AdminTaxReportsSection.taxRuleAmount('Texas-0.125')}}" stepKey="amountOfFirstTaxRate"/> + <grabTextFrom selector="{{AdminTaxReportsSection.taxRuleAmount('Texas-7.25')}}" stepKey="amountOfSecondTaxRate"/> + <grabTextFrom selector="{{AdminTaxReportsSection.taxRuleAmount('Subtotal')}}" stepKey="amountOfSubtotalTaxRate"/> + <assertEquals stepKey="assertSubtotalFirstField"> + <expectedResult type="string">$0.15</expectedResult> + <actualResult type="variable">amountOfFirstTaxRate</actualResult> + </assertEquals> + + <assertEquals stepKey="assertSubtotalSecondField"> + <expectedResult type="string">$8.92</expectedResult> + <actualResult type="variable">amountOfSecondTaxRate</actualResult> + </assertEquals> + + <assertEquals stepKey="assertSubtotalField"> + <expectedResult type="string">$9.07</expectedResult> + <actualResult type="variable">amountOfSubtotalTaxRate</actualResult> + </assertEquals> + + <after> + <!-- Go to the tax rule page and delete the row we created--> + <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulesPage"/> + <waitForPageLoad stepKey="waitForRulesPage"/> + + <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteRule"> + <argument name="name" value="TaxRule1"/> + <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> + </actionGroup> + + <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteSecondRule"> + <argument name="name" value="TaxRule2"/> + <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> + </actionGroup> + + <!-- Go to the tax rate page --> + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRatesPage"/> + <waitForPageLoad stepKey="waitForRatesPage"/> + + <!-- Delete the two tax rates that were created --> + <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteNYRate"> + <argument name="name" value="{{SimpleTaxWithZipCode.state}}-{{SimpleTaxWithZipCode.rate}}"/> + <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> + </actionGroup> + + <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteCARate"> + <argument name="name" value="{{SimpleSecondTaxWithZipCode.state}}-{{SimpleSecondTaxWithZipCode.rate}}"/> + <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> + </actionGroup> + + <deleteData createDataKey="firstProduct" stepKey="deleteFirstProduct"/> + <deleteData createDataKey="secondProduct" stepKey="deleteSecondProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <actionGroup ref="deleteProductTaxClass" stepKey="deleteFirstProductTaxClass"> + <argument name="taxClassName" value="TaxClasses1"/> + </actionGroup> + + <actionGroup ref="deleteProductTaxClass" stepKey="deleteSecondProductTaxClass"> + <argument name="taxClassName" value="TaxClasses2"/> + </actionGroup> + + <actionGroup ref="logout" stepKey="logout"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateDefaultTaxRuleTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateDefaultTaxRuleTest.xml new file mode 100644 index 000000000000..039054254406 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateDefaultTaxRuleTest.xml @@ -0,0 +1,84 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateDefaultTaxRuleTest"> + <annotations> + <stories value="Update tax rule"/> + <title value="Update tax rule, tax rule default"/> + <description value="Test log in to Update tax rule and Update default tax rule"/> + <testCaseId value="MC-5819"/> + <severity value="CRITICAL"/> + <group value="tax"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="defaultTaxRule" stepKey="initialTaxRule"/> + <createData entity="defaultTaxRate" stepKey="initialTaxRate"/> + <createData entity="customerTaxClass" stepKey="createCustomerTaxClass"/> + <createData entity="productTaxClass" stepKey="createProductTaxClass"/> + <getData entity="customerTaxClass" stepKey="customerTaxClass"> + <requiredEntity createDataKey="createCustomerTaxClass"/> + </getData> + <getData entity="productTaxClass" stepKey="productTaxClass"> + <requiredEntity createDataKey="createProductTaxClass"/> + </getData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData stepKey="deleteTaxRule" createDataKey="initialTaxRule" /> + <deleteData stepKey="deleteTaxRate" createDataKey="initialTaxRate" /> + <deleteData stepKey="deleteCustomerTaxClass" createDataKey="createCustomerTaxClass"/> + <deleteData stepKey="deleteProductTaxClass" createDataKey="createProductTaxClass"/> + </after> + + <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> + <waitForPageLoad stepKey="waitForTaxRuleIndex1"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> + <fillField selector="{{AdminTaxRuleGridSection.code}}" userInput="$$initialTaxRule.code$$" stepKey="fillTaxCodeSearch"/> + <click selector="{{AdminTaxRuleGridSection.search}}" stepKey="clickSearch1"/> + <click selector="{{AdminTaxRuleGridSection.nthRow('1')}}" stepKey="clickFirstRow1"/> + + <!-- Update tax rule with default --> + <fillField selector="{{AdminTaxRuleFormSection.code}}" userInput="{{SimpleTaxRule.code}}" stepKey="fillTaxRuleCode1"/> + <fillField selector="{{AdminTaxRuleFormSection.taxRateSearch}}" userInput="$$initialTaxRate.code$$" stepKey="fillTaxRateSearch"/> + <wait stepKey="waitForSearch" time="5" /> + <click selector="{{AdminTaxRuleFormSection.taxRateOption($$initialTaxRate.code$$)}}" stepKey="clickSelectNeededItem"/> + <click selector="{{AdminTaxRuleFormSection.additionalSettings}}" stepKey="clickAdditionalSettings"/> + <scrollTo selector="{{AdminTaxRuleFormSection.additionalSettings}}" x="0" y="-80" stepKey="scrollToAdvancedSettings"/> + <wait stepKey="waitForAdditionalSettings" time="5" /> + <conditionalClick selector="{{AdminTaxRuleFormSection.customerTaxClassOption(retailCustomerTaxClass.class_name)}}" dependentSelector="{{AdminTaxRuleFormSection.customerTaxClassSelected(retailCustomerTaxClass.class_name)}}" visible="false" stepKey="checkRetailCustomerTaxClass" /> + <conditionalClick selector="{{AdminTaxRuleFormSection.productTaxClassOption(taxableGoodsTaxClass.class_name)}}" dependentSelector="{{AdminTaxRuleFormSection.productTaxClassSelected(taxableGoodsTaxClass.class_name)}}" visible="false" stepKey="checkTaxableGoodsTaxClass" /> + <click selector="{{AdminTaxRuleFormSection.customerTaxClassOption($$customerTaxClass.class_name$$)}}" stepKey="clickSelectCustomerTaxClass"/> + <click selector="{{AdminTaxRuleFormSection.productTaxClassOption($$productTaxClass.class_name$$)}}" stepKey="clickSelectProductTaxClass"/> + <fillField selector="{{AdminTaxRuleFormSection.priority}}" userInput="{{taxRuleWithUpdatePriorityPosition.priority}}" stepKey="fillPriority"/> + <fillField selector="{{AdminTaxRuleFormSection.sortOrder}}" userInput="{{taxRuleWithUpdatePriorityPosition.position}}" stepKey="fillPosition"/> + <click selector="{{AdminTaxRuleFormSection.save}}" stepKey="clickSaveTaxRule"/> + <waitForPageLoad stepKey="waitForTaxRuleSaved" /> + <!-- Verify we see success message --> + <see selector="{{AdminTaxRuleGridSection.successMessage}}" userInput="You saved the tax rule." stepKey="seeAssertTaxRuleSuccessMessage"/> + + <!-- Verify we see updated tax rule with default(from the above step) on the tax rule grid page --> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> + <fillField selector="{{AdminTaxRuleGridSection.code}}" userInput="{{SimpleTaxRule.code}}" stepKey="fillTaxRuleCode2"/> + <click selector="{{AdminTaxRuleGridSection.search}}" stepKey="clickSearch2"/> + <waitForPageLoad stepKey="waitForTaxRuleSearch"/> + <click selector="{{AdminTaxRuleGridSection.nthRow('1')}}" stepKey="clickFirstRow2"/> + + <!-- Verify we see updated tax rule with default on the tax rule form page --> + <seeInField selector="{{AdminTaxRuleFormSection.code}}" userInput="{{SimpleTaxRule.code}}" stepKey="seeInTaxRuleCode"/> + <seeElement selector="{{AdminTaxRuleFormSection.taxRateSelected($$initialTaxRate.code$$)}}" stepKey="seeTaxRateSelected"/> + <click selector="{{AdminTaxRuleFormSection.additionalSettings}}" stepKey="clickAdditionalSettings1"/> + <scrollTo selector="{{AdminTaxRuleFormSection.additionalSettings}}" x="0" y="-80" stepKey="scrollToAdvancedSettings1"/> + <seeElement selector="{{AdminTaxRuleFormSection.customerTaxClassSelected($$customerTaxClass.class_name$$)}}" stepKey="seeCustomerTaxClass"/> + <seeElement selector="{{AdminTaxRuleFormSection.productTaxClassSelected($$productTaxClass.class_name$$)}}" stepKey="seeProductTaxClass"/> + <seeInField selector="{{AdminTaxRuleFormSection.priority}}" userInput="{{taxRuleWithUpdatePriorityPosition.priority}}" stepKey="seePriority"/> + <seeInField selector="{{AdminTaxRuleFormSection.sortOrder}}" userInput="{{taxRuleWithUpdatePriorityPosition.position}}" stepKey="seePosition"/> + </test> +</tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithCustomClassesTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithCustomClassesTest.xml new file mode 100644 index 000000000000..0e87f268f882 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithCustomClassesTest.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="AdminUpdateTaxRuleWithCustomClassesTest"> + <annotations> + <stories value="Update tax rule"/> + <title value="Update tax rule, tax rule with custom classes"/> + <description value="Test log in to Update tax rule and Update tax rule with custom classes"/> + <testCaseId value="MC-5820"/> + <severity value="CRITICAL"/> + <group value="tax"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="defaultTaxRule" stepKey="initialTaxRule"/> + <createData entity="defaultTaxRateWithZipRange" stepKey="taxRateWithZipRange"/> + <createData entity="customerTaxClass" stepKey="createCustomerTaxClass"/> + <createData entity="productTaxClass" stepKey="createProductTaxClass"/> + <getData entity="customerTaxClass" stepKey="customerTaxClass"> + <requiredEntity createDataKey="createCustomerTaxClass"/> + </getData> + <getData entity="productTaxClass" stepKey="productTaxClass"> + <requiredEntity createDataKey="createProductTaxClass"/> + </getData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData stepKey="deleteTaxRule" createDataKey="initialTaxRule" /> + <deleteData stepKey="deleteTaxRate" createDataKey="taxRateWithZipRange" /> + <deleteData stepKey="deleteCustomerTaxClass" createDataKey="createCustomerTaxClass"/> + <deleteData stepKey="deleteProductTaxClass" createDataKey="createProductTaxClass"/> + </after> + + <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> + <waitForPageLoad stepKey="waitForTaxRuleIndex1"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> + <fillField selector="{{AdminTaxRuleGridSection.code}}" userInput="$$initialTaxRule.code$$" stepKey="fillTaxCodeSearch"/> + <click selector="{{AdminTaxRuleGridSection.search}}" stepKey="clickSearch1"/> + <click selector="{{AdminTaxRuleGridSection.nthRow('1')}}" stepKey="clickFirstRow1"/> + + <!-- Update tax rule with custom classes --> + <fillField selector="{{AdminTaxRuleFormSection.code}}" userInput="{{SimpleTaxRule.code}}" stepKey="fillTaxRuleCode1"/> + <fillField selector="{{AdminTaxRuleFormSection.taxRateSearch}}" userInput="$$taxRateWithZipRange.code$$" stepKey="fillTaxRateSearch"/> + <wait stepKey="waitForSearch" time="5" /> + <click selector="{{AdminTaxRuleFormSection.taxRateOption($$taxRateWithZipRange.code$$)}}" stepKey="clickSelectNeededItem"/> + <click selector="{{AdminTaxRuleFormSection.additionalSettings}}" stepKey="clickAdditionalSettings"/> + <scrollTo selector="{{AdminTaxRuleFormSection.additionalSettings}}" x="0" y="-80" stepKey="scrollToAdvancedSettings"/> + <wait stepKey="waitForAdditionalSettings" time="5" /> + <conditionalClick selector="{{AdminTaxRuleFormSection.customerTaxClassOption(retailCustomerTaxClass.class_name)}}" dependentSelector="{{AdminTaxRuleFormSection.customerTaxClassSelected(retailCustomerTaxClass.class_name)}}" visible="false" stepKey="checkRetailCustomerTaxClass" /> + <conditionalClick selector="{{AdminTaxRuleFormSection.productTaxClassOption(taxableGoodsTaxClass.class_name)}}" dependentSelector="{{AdminTaxRuleFormSection.productTaxClassSelected(taxableGoodsTaxClass.class_name)}}" visible="false" stepKey="checkTaxableGoodsTaxClass" /> + <click selector="{{AdminTaxRuleFormSection.productTaxClassOption($$productTaxClass.class_name$$)}}" stepKey="clickSelectProductTaxClass"/> + <click selector="{{AdminTaxRuleFormSection.save}}" stepKey="clickSaveTaxRule"/> + <waitForPageLoad stepKey="waitForTaxRuleSaved" /> + <!-- Verify we see success message --> + <see selector="{{AdminTaxRuleGridSection.successMessage}}" userInput="You saved the tax rule." stepKey="seeAssertTaxRuleSuccessMessage"/> + + <!-- Verify we see updated tax rule with default(from the above step) on the tax rule grid page --> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> + <fillField selector="{{AdminTaxRuleGridSection.code}}" userInput="{{SimpleTaxRule.code}}" stepKey="fillTaxRuleCode2"/> + <click selector="{{AdminTaxRuleGridSection.search}}" stepKey="clickSearch2"/> + <waitForPageLoad stepKey="waitForTaxRuleSearch"/> + <click selector="{{AdminTaxRuleGridSection.nthRow('1')}}" stepKey="clickFirstRow2"/> + + <!-- Verify we see updated tax rule with default on the tax rule form page --> + <seeInField selector="{{AdminTaxRuleFormSection.code}}" userInput="{{SimpleTaxRule.code}}" stepKey="seeInTaxRuleCode"/> + <seeElement selector="{{AdminTaxRuleFormSection.taxRateSelected($$taxRateWithZipRange.code$$)}}" stepKey="seeTaxRateSelected"/> + <click selector="{{AdminTaxRuleFormSection.additionalSettings}}" stepKey="clickAdditionalSettings1"/> + <scrollTo selector="{{AdminTaxRuleFormSection.additionalSettings}}" x="0" y="-80" stepKey="scrollToAdvancedSettings1"/> + <seeElement selector="{{AdminTaxRuleFormSection.productTaxClassSelected($$productTaxClass.class_name$$)}}" stepKey="seeProductTaxClass"/> + <seeElement selector="{{AdminTaxRuleFormSection.customerTaxClassSelected(retailCustomerTaxClass.class_name)}}" stepKey="seeRetailCustomerTaxClass" /> + <seeElement selector="{{AdminTaxRuleFormSection.productTaxClassSelected(taxableGoodsTaxClass.class_name)}}" stepKey="seeTaxableGoodsTaxClass" /> + </test> +</tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithFixedZipUtahTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithFixedZipUtahTest.xml new file mode 100644 index 000000000000..a96a57cbfec5 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithFixedZipUtahTest.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="AdminUpdateTaxRuleWithFixedZipUtahTest"> + <annotations> + <stories value="Update tax rule"/> + <title value="Update tax rule, fixed zip Utah"/> + <description value="Test log in to Update tax rule and Update tax rule with fixed zip Utah"/> + <testCaseId value="MC-5821"/> + <severity value="CRITICAL"/> + <group value="tax"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="defaultTaxRule" stepKey="initialTaxRule"/> + <createData entity="TaxRateWithFixedZipUtah" stepKey="taxRateWithFixedZipUtah"/> + <createData entity="customerTaxClass" stepKey="createCustomerTaxClass"/> + <createData entity="productTaxClass" stepKey="createProductTaxClass"/> + <getData entity="customerTaxClass" stepKey="customerTaxClass"> + <requiredEntity createDataKey="createCustomerTaxClass"/> + </getData> + <getData entity="productTaxClass" stepKey="productTaxClass"> + <requiredEntity createDataKey="createProductTaxClass"/> + </getData> + <createData entity="ApiSimplePrice100Qty100v2" stepKey="simpleProduct"/> + <createData entity="Simple_US_Utah_Customer" stepKey="customer" /> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData stepKey="deleteTaxRule" createDataKey="initialTaxRule" /> + <deleteData stepKey="deleteTaxRate" createDataKey="taxRateWithFixedZipUtah" /> + <deleteData stepKey="deleteCustomerTaxClass" createDataKey="createCustomerTaxClass"/> + <deleteData stepKey="deleteProductTaxClass" createDataKey="createProductTaxClass"/> + <deleteData stepKey="deleteSimpleProduct" createDataKey="simpleProduct" /> + <deleteData stepKey="deleteCustomer" createDataKey="customer" /> + </after> + + <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> + <waitForPageLoad stepKey="waitForTaxRuleIndex1"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> + <fillField selector="{{AdminTaxRuleGridSection.code}}" userInput="$$initialTaxRule.code$$" stepKey="fillTaxCodeSearch"/> + <click selector="{{AdminTaxRuleGridSection.search}}" stepKey="clickSearch1"/> + <click selector="{{AdminTaxRuleGridSection.nthRow('1')}}" stepKey="clickFirstRow1"/> + + <!-- Update tax rule with fixed zip Utah --> + <fillField selector="{{AdminTaxRuleFormSection.code}}" userInput="{{SimpleTaxRule.code}}" stepKey="fillTaxRuleCode1"/> + <fillField selector="{{AdminTaxRuleFormSection.taxRateSearch}}" userInput="$$taxRateWithFixedZipUtah.code$$" stepKey="fillTaxRateSearch"/> + <wait stepKey="waitForSearch" time="5" /> + <click selector="{{AdminTaxRuleFormSection.taxRateOption($$taxRateWithFixedZipUtah.code$$)}}" stepKey="clickSelectNeededItem"/> + <click selector="{{AdminTaxRuleFormSection.additionalSettings}}" stepKey="clickAdditionalSettings"/> + <scrollTo selector="{{AdminTaxRuleFormSection.additionalSettings}}" x="0" y="-80" stepKey="scrollToAdvancedSettings"/> + <wait stepKey="waitForAdditionalSettings" time="5" /> + <conditionalClick selector="{{AdminTaxRuleFormSection.customerTaxClassOption(retailCustomerTaxClass.class_name)}}" dependentSelector="{{AdminTaxRuleFormSection.customerTaxClassSelected(retailCustomerTaxClass.class_name)}}" visible="false" stepKey="checkRetailCustomerTaxClass" /> + <conditionalClick selector="{{AdminTaxRuleFormSection.productTaxClassOption(taxableGoodsTaxClass.class_name)}}" dependentSelector="{{AdminTaxRuleFormSection.productTaxClassSelected(taxableGoodsTaxClass.class_name)}}" visible="false" stepKey="checkTaxableGoodsTaxClass" /> + <click selector="{{AdminTaxRuleFormSection.productTaxClassOption($$productTaxClass.class_name$$)}}" stepKey="clickSelectProductTaxClass"/> + <click selector="{{AdminTaxRuleFormSection.save}}" stepKey="clickSaveTaxRule"/> + <waitForPageLoad stepKey="waitForTaxRuleSaved" /> + <!-- Verify we see success message --> + <see selector="{{AdminTaxRuleGridSection.successMessage}}" userInput="You saved the tax rule." stepKey="seeAssertTaxRuleSuccessMessage"/> + + <!-- Verify we see updated tax rule with fixed zip Utah(from the above step) on the tax rule grid page --> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> + <fillField selector="{{AdminTaxRuleGridSection.code}}" userInput="{{SimpleTaxRule.code}}" stepKey="fillTaxRuleCode2"/> + <click selector="{{AdminTaxRuleGridSection.search}}" stepKey="clickSearch2"/> + <waitForPageLoad stepKey="waitForTaxRuleSearch"/> + <click selector="{{AdminTaxRuleGridSection.nthRow('1')}}" stepKey="clickFirstRow2"/> + + <!-- Verify we see updated tax rule with fixed zip Utah on the tax rule form page --> + <seeInField selector="{{AdminTaxRuleFormSection.code}}" userInput="{{SimpleTaxRule.code}}" stepKey="seeInTaxRuleCode"/> + <seeElement selector="{{AdminTaxRuleFormSection.taxRateSelected($$taxRateWithFixedZipUtah.code$$)}}" stepKey="seeTaxRateSelected"/> + <click selector="{{AdminTaxRuleFormSection.additionalSettings}}" stepKey="clickAdditionalSettings1"/> + <scrollTo selector="{{AdminTaxRuleFormSection.additionalSettings}}" x="0" y="-80" stepKey="scrollToAdvancedSettings1"/> + <seeElement selector="{{AdminTaxRuleFormSection.productTaxClassSelected($$productTaxClass.class_name$$)}}" stepKey="seeProductTaxClass"/> + + <!-- Verify if tax rule is applied on the store front product page --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> + <argument name="Customer" value="$$customer$$" /> + </actionGroup> + <amOnPage url="{{StorefrontProductPage.url($$simpleProduct.custom_attributes[url_key]$$)}}" stepKey="goProductPageOnStorefront"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$simpleProduct$$" /> + <argument name="productCount" value="1" /> + </actionGroup> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openShoppingCart" /> + <actionGroup ref="FillShippingZipForm" stepKey="fillShippingZipForm"> + <argument name="address" value="US_Address_Utah" /> + </actionGroup> + <scrollTo selector="{{StorefrontProductPageSection.orderTotal}}" x="0" y="-80" stepKey="scrollToOrderTotal"/> + <waitForElementVisible selector="{{StorefrontProductPageSection.shipping}}" time="30" stepKey="waitForShipping"/> + <see selector="{{StorefrontProductPageSection.shipping}}" userInput="$5.00" stepKey="seeShipping"/> + <waitForElementVisible selector="{{StorefrontProductPageSection.tax}}" time="30" stepKey="waitForTax"/> + <see selector="{{StorefrontProductPageSection.tax}}" userInput="$20.00" stepKey="seeAssertTaxAmount" /> + <waitForElementVisible selector="{{StorefrontProductPageSection.orderTotal}}" time="30" stepKey="waitForOrderTotal"/> + <see selector="{{StorefrontProductPageSection.orderTotal}}" userInput="$125.00" stepKey="seeAssertGrandTotal"/> + </test> +</tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/CheckCreditMemoTotalsTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/CheckCreditMemoTotalsTest.xml new file mode 100644 index 000000000000..83fcfbff6de6 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Test/CheckCreditMemoTotalsTest.xml @@ -0,0 +1,145 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="CheckCreditMemoTotalsTest"> + <annotations> + <features value="Tax"/> + <stories value="MAGETWO-91769 - Credit Memo - Wrong tax calculation! #10982"/> + <title value="Checking Credit memo Totals"/> + <description value="Checking Credit memo Totals"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-95175"/> + <group value="creditMemo"/> + <group value="tax"/> + </annotations> + <before> + <!--Create category and product--> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!--Create customer--> + <createData entity="Simple_US_Customer_NY" stepKey="createCustomer"/> + <!--Login as admin--> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + <!--Create tax rule and tax rate--> + <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulePage"/> + <waitForPageLoad stepKey="waitForTaxRatePage"/> + <click stepKey="addNewTaxRate" selector="{{AdminGridMainControls.add}}"/> + <fillField stepKey="fillRuleName" selector="{{AdminTaxRulesSection.ruleName}}" userInput="SampleRule"/> + <actionGroup ref="addCustomTaxRate" stepKey="addCustomTaxRate"> + <argument name="taxCode" value="SimpleTaxNY"/> + </actionGroup> + <click stepKey="expandAdditionalSettings" selector="{{AdminProductTaxClassSection.additionalSettings}}"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <click selector="{{AdminProductTaxClassSection.productTaxClass}}" stepKey="ClickToAddTaxClass"/> + <fillField selector="{{AdminProductTaxClassSection.TaxClassName}}" userInput="NewTaxClass" stepKey="setName"/> + <click selector="{{AdminProductTaxClassSection.confirm}}" stepKey="ClickToDone"/> + <click stepKey="clickSave" selector="{{AdminStoresMainActionsSection.saveButton}}"/> + <waitForPageLoad stepKey="waitForTaxRatePage1"/> + <see userInput="You saved the tax rule." stepKey="VerifyRuleSaved"/> + <!--Search and edit product to add new created tax rule--> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProduct"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="OpenEditProductOnBackend"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <selectOption selector="{{AdminProductFormActionSection.selectTaxClass}}" userInput="NewTaxClass" stepKey="SetNewTaxClass" /> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <!--Set configs--> + <amOnPage url="{{AdminTaxConfigurationPage.url}}" stepKey="goToTaxConfigPage"/> + <conditionalClick stepKey="clickOrdersInvoicesCreditSales" selector="{{AdminConfigureTaxSection.taxClasses}}" dependentSelector="{{AdminConfigureTaxSection.taxClassesCondition}}" visible="false"/> + <click selector="{{AdminConfigureTaxSection.useSystemValue}}" stepKey="UncheckUseSystemValue"/> + <selectOption selector="{{AdminConfigureTaxSection.productTaxClass}}" userInput="NewTaxClass" stepKey="selectClass"/> + <click selector="{{AdminConfigureTaxSection.save}}" stepKey="saveConfig"/> + <!--flash cache--> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <!--Delete category and product--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <!--Delete customer--> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <!-- Reset admin order filter --> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderFilters"/> + <!--Roll Back configuration--> + <amOnPage url="{{AdminTaxConfigurationPage.url}}" stepKey="goToTaxConfigPage"/> + <conditionalClick stepKey="clickOrdersInvoicesCreditSales" selector="{{AdminConfigureTaxSection.taxClasses}}" dependentSelector="{{AdminConfigureTaxSection.taxClassesCondition}}" visible="false"/> + <selectOption selector="{{AdminConfigureTaxSection.productTaxClass}}" userInput="Taxable Goods" stepKey="selectClass"/> + <click selector="{{AdminConfigureTaxSection.save}}" stepKey="saveConfig"/> + <!-- Go to the tax rule page and delete the row we created--> + <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulesPage"/> + <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteRule"> + <argument name="name" value="SampleRule"/> + <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> + </actionGroup> + <!-- Go to the tax rate page --> + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRatesPage"/> + <waitForPageLoad stepKey="waitForRatesPage"/> + <!-- Delete tax rate that were created --> + <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteNYRate"> + <argument name="name" value="{{SimpleTaxNY.state}}"/> + <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> + </actionGroup> + <actionGroup ref="deleteProductTaxClass" stepKey="deleteFirstProductTaxClass"> + <argument name="taxClassName" value="NewTaxClass"/> + </actionGroup> + <!--logout--> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Create new order--> + <actionGroup stepKey="CreateNewOrder" ref="navigateToNewOrderPageExistingCustomer"> + <argument name="customer" value="Simple_US_Customer_NY"/> + </actionGroup> + <!--Add product to order--> + <click stepKey="clickToAddProduct" selector="{{AdminOrderFormItemsSection.addProducts}}"/> + <waitForPageLoad stepKey="waitForProductsOpened"/> + <checkOption selector="{{AdminOrderFormItemsSection.rowCheck('1')}}" stepKey="selectProduct"/> + <click selector="{{AdminOrderFormItemsSection.addSelected}}" stepKey="clickAddSelectedProducts"/> + <waitForPageLoad stepKey="waitForPageLoad3"/> + <!--Set shipping method--> + <actionGroup stepKey="orderSelectFlatRateShipping" ref="orderSelectFlatRateShipping"/> + <!--Submit order--> + <click stepKey="SubmitOrder" selector="{{AdminOrderFormActionSection.SubmitOrder}}"/> + <waitForPageLoad stepKey="waitForPageLoad4"/> + <grabTextFrom stepKey="getOrderId" selector="|Order # (\d+)|"/> + <!--Open new created order--> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> + <waitForPageLoad stepKey="waitForPageLoad5"/> + <actionGroup ref="filterOrderGridById" stepKey="filterOrderGridById"> + <argument name="orderId" value="$getOrderId"/> + </actionGroup> + <!--Create order invoice--> + <click selector="{{AdminDataGridTableSection.rowViewAction('1')}}" stepKey="clickCreatedOrderInGrid"/> + <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceAction"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Invoice" stepKey="seePageNameNewInvoicePage"/> + <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeInvoiceCreateSuccess"/> + <see selector="{{AdminInvoiceOrderInformationSection.orderStatus}}" userInput="Processing" stepKey="seeOrderProcessing"/> + <!--Create Credit Memo--> + <click selector="{{AdminOrderDetailsMainActionsSection.creditMemo}}" stepKey="clickCreditMemoAction"/> + <fillField selector="{{AdminCreditMemoTotalSection.refundShipping}}" userInput="0" stepKey="setRefundShipping"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Memo" stepKey="seeNewMemoInPageTitle"/> + <click selector="{{AdminCreditMemoTotalSection.submitRefundOffline}}" stepKey="clickRefundOffline"/> + <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="You created the credit memo." stepKey="seeCreditMemoSuccess"/> + <click selector="{{AdminCreditMemoTotalSection.creditMemoItem}}" stepKey="goToCreatedCreditMemo"/> + <waitForPageLoad stepKey="waitForPageLoad6"/> + <!--View created memo and verify tax for product--> + <click selector="{{AdminCreditMemoTotalSection.viewMemo}}" stepKey="ViewMemo"/> + <waitForPageLoad stepKey="waitForPageLoad7"/> + <grabTextFrom selector="{{AdminCreditMemoTotalSection.grandTotal}}" stepKey="getGrandTotal"/> + <assertEquals expected='$123.00' expectedType="string" actual="($getGrandTotal)" stepKey="assertGrandTotalValue"/> + + </test> +</tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/DeleteTaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/DeleteTaxRateEntityTest.xml new file mode 100644 index 000000000000..5f855f5bb750 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Test/DeleteTaxRateEntityTest.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="DeleteTaxRateEntityTest"> + <annotations> + <stories value="Delete Tax Rate"/> + <title value="Delete tax rate"/> + <description value="Test log in to Tax Rate and Delete Tax Rate"/> + <testCaseId value="MC-5801"/> + <severity value="CRITICAL"/> + <group value="tax"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="defaultTaxRate" stepKey="initialTaxRate"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + + <!-- Search the tax rate on tax grid page --> + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> + <waitForPageLoad stepKey="waitForTaxRateIndex1"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> + <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="$$initialTaxRate.code$$" stepKey="fillCode"/> + <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch1"/> + <click selector="{{AdminTaxRateGridSection.nthRow('1')}}" stepKey="clickFirstRow1"/> + + <!-- Delete values on the tax rate form page --> + <click selector="{{AdminTaxRateFormSection.deleteRate}}" stepKey="clickDeleteRate"/> + <click selector="{{AdminTaxRateFormSection.ok}}" stepKey="clickOk"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You Deleted the tax rate." stepKey="seeSuccess1"/> + + <!-- Confirm Deleted TaxIdentifier(from the above step) on the tax rate grid page --> + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex2"/> + <waitForPageLoad stepKey="waitForTaxRateIndex2"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> + <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{defaultTaxRate.code}}" stepKey="fillTaxIdentifierField3"/> + <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch2"/> + <see selector="{{AdminTaxRateGridSection.emptyText}}" userInput="We couldn't find any records." stepKey="seeSuccess"/> + + <!-- Confirm Deleted TaxIdentifier on the tax rule grid page --> + <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex3"/> + <click selector="{{AdminTaxRuleGridSection.add}}" stepKey="clickAddNewTaxRuleButton"/> + <waitForPageLoad stepKey="waitForTaxRuleIndex1"/> + <fillField selector="{{AdminTaxRuleFormSection.taxRateSearch}}" userInput="$$initialTaxRate.code$$" stepKey="fillTaxRateSearch"/> + <wait stepKey="waitForSearch" time="5" /> + <dontSee selector="{{AdminTaxRuleFormSection.fieldTaxRate}}" userInput="$$initialTaxRate.code$$" stepKey="dontSeeInTaxRuleForm"/> + </test> +</tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxInformationInShoppingCartForCustomerPhysicalQuoteTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxInformationInShoppingCartForCustomerPhysicalQuoteTest.xml index 1b3422011a9a..aa44593400a8 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxInformationInShoppingCartForCustomerPhysicalQuoteTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxInformationInShoppingCartForCustomerPhysicalQuoteTest.xml @@ -63,6 +63,7 @@ <actionGroup ref="resetProductGridToDefaultView" stepKey="resetGridToDefaultKeywordSearch"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> <deleteData createDataKey="createTaxRule" stepKey="deleteTaxRule"/> <deleteData createDataKey="createProductFPTAttribute" stepKey="deleteProductFPTAttribute"/> <createData entity="DefaultTaxConfig" stepKey="defaultTaxConfiguration"/> @@ -88,9 +89,13 @@ <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="goToShoppingCartFromMinicart"/> <!-- Step 4: Open Estimate Shipping and Tax section --> <conditionalClick selector="{{CheckoutCartSummarySection.estimateShippingAndTax}}" dependentSelector="{{CheckoutCartSummarySection.country}}" visible="false" stepKey="expandEstimateShippingandTax" /> - <see selector="{{CheckoutCartSummarySection.country}}" userInput="$$createCustomer.country_id$$" stepKey="checkCustomerCountry" /> - <see selector="{{CheckoutCartSummarySection.stateProvince}}" userInput="$$createCustomer.state$$" stepKey="checkCustomerRegion" /> - <see selector="{{CheckoutCartSummarySection.postcode}}" userInput="$$createCustomer.postcode$$" stepKey="checkCustomerPostcode" /> + <seeOptionIsSelected selector="{{CheckoutCartSummarySection.country}}" userInput="{{US_Address_CA.country}}" stepKey="checkCustomerCountry" /> + <seeOptionIsSelected selector="{{CheckoutCartSummarySection.stateProvince}}" userInput="{{US_Address_CA.state}}" stepKey="checkCustomerRegion" /> + <grabValueFrom selector="{{CheckoutCartSummarySection.postcode}}" stepKey="grabTextPostCode"/> + <assertEquals message="Customer postcode is invalid" stepKey="checkCustomerPostcode"> + <expectedResult type="string">{{US_Address_CA.postcode}}</expectedResult> + <actualResult type="variable">grabTextPostCode</actualResult> + </assertEquals> <see selector="{{CheckoutCartSummarySection.amountFPT}}" userInput="$10" stepKey="checkFPTAmountCA" /> <see selector="{{CheckoutCartSummarySection.taxAmount}}" userInput="$0.83" stepKey="checkTaxAmountCA" /> <scrollTo selector="{{CheckoutCartSummarySection.taxSummary}}" stepKey="scrollToTaxSummary" /> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxInformationInShoppingCartForCustomerVirtualQuoteTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxInformationInShoppingCartForCustomerVirtualQuoteTest.xml index 3fa982651293..ac090fd4fe9c 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxInformationInShoppingCartForCustomerVirtualQuoteTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxInformationInShoppingCartForCustomerVirtualQuoteTest.xml @@ -38,6 +38,7 @@ <createData entity="Simple_US_Customer_NY" stepKey="createCustomer"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> <deleteData createDataKey="createTaxRule" stepKey="deleteTaxRule"/> <deleteData createDataKey="createProductFPTAttribute" stepKey="deleteProductFPTAttribute"/> <createData entity="DefaultTaxConfig" stepKey="defaultTaxConfiguration"/> @@ -60,9 +61,13 @@ <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="goToShoppingCartFromMinicart"/> <!-- Step 4: Open Estimate Shipping and Tax section --> <conditionalClick selector="{{CheckoutCartSummarySection.estimateShippingAndTax}}" dependentSelector="{{CheckoutCartSummarySection.country}}" visible="false" stepKey="expandEstimateShippingandTax" /> - <see selector="{{CheckoutCartSummarySection.country}}" userInput="$$createCustomer.country_id$$" stepKey="checkCustomerCountry" /> - <see selector="{{CheckoutCartSummarySection.stateProvince}}" userInput="$$createCustomer.state$$" stepKey="checkCustomerRegion" /> - <see selector="{{CheckoutCartSummarySection.postcode}}" userInput="$$createCustomer.postcode$$" stepKey="checkCustomerPostcode" /> + <seeOptionIsSelected selector="{{CheckoutCartSummarySection.country}}" userInput="{{US_Address_NY.country}}" stepKey="checkCustomerCountry" /> + <seeOptionIsSelected selector="{{CheckoutCartSummarySection.stateProvince}}" userInput="{{US_Address_NY.state}}" stepKey="checkCustomerRegion" /> + <grabValueFrom selector="{{CheckoutCartSummarySection.postcode}}" stepKey="grabTextPostCode"/> + <assertEquals message="Customer postcode is invalid" stepKey="checkCustomerPostcode"> + <expectedResult type="string">{{US_Address_NY.postcode}}</expectedResult> + <actualResult type="variable">grabTextPostCode</actualResult> + </assertEquals> <scrollTo selector="{{CheckoutCartSummarySection.taxSummary}}" stepKey="scrollToTaxSummary" /> <click selector="{{CheckoutCartSummarySection.taxSummary}}" stepKey="expandTaxSummary"/> <see selector="{{CheckoutCartSummarySection.rate}}" userInput="US-NY-*-Rate 1 (8.375%)" stepKey="checkRateNY" /> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxInformationInShoppingCartForGuestPhysicalQuoteTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxInformationInShoppingCartForGuestPhysicalQuoteTest.xml index 47161001219e..bce9d895e311 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxInformationInShoppingCartForGuestPhysicalQuoteTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxInformationInShoppingCartForGuestPhysicalQuoteTest.xml @@ -16,8 +16,6 @@ <description value="Tax information are updating/recalculating on fly in shopping cart for Guest (physical quote)"/> <severity value="CRITICAL"/> <testCaseId value="MAGETWO-41930"/> - <!--Skip because of issue MAGETWO-90966 on 4 step--> - <group value="skip"/> <group value="checkout"/> <group value="tax"/> </annotations> @@ -86,6 +84,8 @@ <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="goToShoppingCartFromMinicart"/> <!-- Step 4: Open Estimate Shipping and Tax section --> <conditionalClick selector="{{CheckoutCartSummarySection.estimateShippingAndTax}}" dependentSelector="{{CheckoutCartSummarySection.country}}" visible="false" stepKey="expandEstimateShippingandTax" /> + <selectOption selector="{{CheckoutCartSummarySection.country}}" userInput="United States" stepKey="selectUSCountry"/> + <selectOption selector="{{CheckoutCartSummarySection.stateProvince}}" userInput="California" stepKey="selectCaliforniaRegion"/> <see selector="{{CheckoutCartSummarySection.amountFPT}}" userInput="$10" stepKey="checkFPTAmountCA" /> <see selector="{{CheckoutCartSummarySection.taxAmount}}" userInput="$0.83" stepKey="checkTaxAmountCA" /> <scrollTo selector="{{CheckoutCartSummarySection.taxSummary}}" stepKey="scrollToTaxSummary" /> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxInformationInShoppingCartForGuestVirtualQuoteTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxInformationInShoppingCartForGuestVirtualQuoteTest.xml index 88496b8e2cd2..74233bbff4a6 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxInformationInShoppingCartForGuestVirtualQuoteTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxInformationInShoppingCartForGuestVirtualQuoteTest.xml @@ -16,8 +16,6 @@ <description value="Tax information are updating/recalculating on fly in shopping cart for Guest (virtual quote)"/> <severity value="CRITICAL"/> <testCaseId value="MAGETWO-41931"/> - <!--Skip because of issue MAGETWO-90966 on 4 step--> - <group value="skip"/> <group value="checkout"/> <group value="tax"/> </annotations> @@ -58,6 +56,8 @@ <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="goToShoppingCartFromMinicart"/> <!-- Step 4: Open Estimate Shipping and Tax section --> <conditionalClick selector="{{CheckoutCartSummarySection.estimateShippingAndTax}}" dependentSelector="{{CheckoutCartSummarySection.country}}" visible="false" stepKey="expandEstimateShippingandTax" /> + <selectOption selector="{{CheckoutCartSummarySection.country}}" userInput="United States" stepKey="selectUSCountry"/> + <selectOption selector="{{CheckoutCartSummarySection.stateProvince}}" userInput="California" stepKey="selectCaliforniaRegion"/> <scrollTo selector="{{CheckoutCartSummarySection.taxSummary}}" stepKey="scrollToTaxSummary" /> <see selector="{{CheckoutCartSummarySection.taxAmount}}" userInput="$3.30" stepKey="checkTaxAmountCA" /> <click selector="{{CheckoutCartSummarySection.taxSummary}}" stepKey="taxSummary"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest.xml index 3b741e7bf79e..05ced7e61b3b 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest.xml @@ -75,7 +75,7 @@ <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> - <!-- Ensure tax wont be shown in the cart --> + <!-- Ensure tax won't be shown in the cart --> <actionGroup ref="changeToDefaultTaxConfigurationUI" stepKey="changeToDefaultTaxConfiguration"/> <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> @@ -92,8 +92,10 @@ <!-- Assert that taxes are applied correctly for NY --> <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCheckout"/> <waitForPageLoad stepKey="waitForCart"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> <waitForElementVisible stepKey="waitForOverviewVisible" selector="{{CheckoutPaymentSection.tax}}"/> + <waitForText userInput="$5.00" selector="{{CheckoutPaymentSection.orderSummaryShippingTotal}}" time="30" stepKey="waitForCorrectShippingAmount"/> <see stepKey="seeTax" selector="{{CheckoutPaymentSection.tax}}" userInput="$10.30"/> <click stepKey="expandTax" selector="{{CheckoutPaymentSection.tax}}"/> <see stepKey="seeTaxPercent" selector="{{CheckoutPaymentSection.taxPercentage}}" userInput="({{SimpleTaxNY.rate}}%)"/> @@ -191,7 +193,7 @@ <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> - <!-- Ensure tax wont be shown in the cart --> + <!-- Ensure tax won't be shown in the cart --> <actionGroup ref="changeToDefaultTaxConfigurationUI" stepKey="changeToDefaultTaxConfiguration"/> <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> @@ -208,6 +210,7 @@ <!-- Assert that taxes are applied correctly for NY --> <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCheckout"/> <waitForPageLoad stepKey="waitForCart"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> <waitForElementVisible stepKey="waitForOverviewVisible" selector="{{CheckoutPaymentSection.tax}}"/> <see stepKey="seeTax" selector="{{CheckoutPaymentSection.tax}}" userInput="$8.37"/> @@ -297,7 +300,7 @@ <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> - <!-- Ensure tax wont be shown in the cart --> + <!-- Ensure tax won't be shown in the cart --> <actionGroup ref="changeToDefaultTaxConfigurationUI" stepKey="changeToDefaultTaxConfiguration"/> <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> @@ -314,8 +317,12 @@ <!-- Assert that taxes are applied correctly for CA --> <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCheckout"/> <waitForPageLoad stepKey="waitForCart"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> <waitForElementVisible stepKey="waitForOverviewVisible" selector="{{CheckoutPaymentSection.tax}}"/> + <conditionalClick selector="{{CheckoutCartSummarySection.estimateShippingAndTax}}" dependentSelector="{{CheckoutCartSummarySection.country}}" visible="false" stepKey="expandEstimateShippingandTax" /> + <selectOption selector="{{CheckoutCartSummarySection.country}}" userInput="United States" stepKey="selectUSCountry"/> + <selectOption selector="{{CheckoutCartSummarySection.stateProvince}}" userInput="California" stepKey="selectCaliforniaRegion"/> <see stepKey="seeTax3" selector="{{CheckoutPaymentSection.tax}}" userInput="$10.15"/> <click stepKey="expandTax2" selector="{{CheckoutPaymentSection.tax}}"/> <see stepKey="seeTaxPercent2" selector="{{CheckoutPaymentSection.taxPercentage}}" userInput="({{SimpleTaxCA.rate}}%)"/> @@ -403,7 +410,7 @@ <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> - <!-- Ensure tax wont be shown in the cart --> + <!-- Ensure tax won't be shown in the cart --> <actionGroup ref="changeToDefaultTaxConfigurationUI" stepKey="changeToDefaultTaxConfiguration"/> <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> @@ -420,9 +427,13 @@ <!-- Assert that taxes are applied correctly for NY --> <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCheckout"/> <waitForPageLoad stepKey="waitForCart"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> <!-- Assert that taxes are applied correctly for CA --> <waitForElementVisible stepKey="waitForOverviewVisible" selector="{{CheckoutPaymentSection.tax}}"/> + <conditionalClick selector="{{CheckoutCartSummarySection.estimateShippingAndTax}}" dependentSelector="{{CheckoutCartSummarySection.country}}" visible="false" stepKey="expandEstimateShippingandTax" /> + <selectOption selector="{{CheckoutCartSummarySection.country}}" userInput="United States" stepKey="selectUSCountry"/> + <selectOption selector="{{CheckoutCartSummarySection.stateProvince}}" userInput="California" stepKey="selectCaliforniaRegion"/> <see stepKey="seeTax3" selector="{{CheckoutPaymentSection.tax}}" userInput="$8.25"/> <click stepKey="expandTax2" selector="{{CheckoutPaymentSection.tax}}"/> <see stepKey="seeTaxPercent2" selector="{{CheckoutPaymentSection.taxPercentage}}" userInput="({{SimpleTaxCA.rate}}%)"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest.xml index 000fba1d8869..e7bf08257ea6 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest.xml @@ -66,7 +66,7 @@ <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> - <!-- Ensure tax wont be shown in the cart --> + <!-- Ensure tax won't be shown in the cart --> <actionGroup ref="changeToDefaultTaxConfigurationUI" stepKey="changeToDefaultTaxConfiguration"/> <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> @@ -92,9 +92,10 @@ <see stepKey="seeTotalExcl2" selector="{{CheckoutPaymentSection.orderSummaryTotalExcluding}}" userInput="$$virtualProduct1.price$$"/> <!-- Change the address --> - <actionGroup ref="GuestCheckoutFillNewBillingAddressActionGroup" stepKey="changeAddress"> + <actionGroup ref="GuestCheckoutSelectPaymentAndFillNewBillingAddressActionGroup" stepKey="changeAddress"> <argument name="customerVar" value="Simple_US_Customer_NY"/> <argument name="customerAddressVar" value="US_Address_NY"/> + <argument name="paymentMethod" value="Check / Money order"/> </actionGroup> <click stepKey="saveAddress" selector="{{CheckoutShippingSection.updateAddress}}"/> @@ -178,7 +179,7 @@ <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> - <!-- Ensure tax wont be shown in the cart --> + <!-- Ensure tax won't be shown in the cart --> <actionGroup ref="changeToDefaultTaxConfigurationUI" stepKey="changeToDefaultTaxConfiguration"/> <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> @@ -289,7 +290,7 @@ <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> - <!-- Ensure tax wont be shown in the cart --> + <!-- Ensure tax won't be shown in the cart --> <actionGroup ref="changeToDefaultTaxConfigurationUI" stepKey="changeToDefaultTaxConfiguration"/> <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> @@ -312,6 +313,9 @@ <argument name="Address" value="US_Address_CA"/> </actionGroup> <click stepKey="clickNext" selector="{{CheckoutShippingSection.next}}"/> + <waitForPageLoad stepKey="waitForAddressToLoad"/> + <!-- Checkout select Check/Money Order payment --> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> <see stepKey="seeAddress" selector="{{CheckoutShippingSection.defaultShipping}}" userInput="{{SimpleTaxCA.state}}"/> <see stepKey="seeShipTo" selector="{{CheckoutPaymentSection.shipToInformation}}" userInput="{{SimpleTaxCA.state}}"/> @@ -329,6 +333,9 @@ <argument name="Address" value="US_Address_NY"/> </actionGroup> <click stepKey="clickNext2" selector="{{CheckoutShippingSection.next}}"/> + <waitForPageLoad stepKey="waitForShippingToLoad"/> + <!-- Checkout select Check/Money Order payment --> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment2"/> <see stepKey="seeShipTo2" selector="{{CheckoutPaymentSection.shipToInformation}}" userInput="{{SimpleTaxNY.state}}"/> <!-- Assert that taxes are applied correctly for NY --> @@ -407,7 +414,7 @@ <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> - <!-- Ensure tax wont be shown in the cart --> + <!-- Ensure tax won't be shown in the cart --> <actionGroup ref="changeToDefaultTaxConfigurationUI" stepKey="changeToDefaultTaxConfiguration"/> <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> @@ -424,6 +431,8 @@ <!-- Assert that taxes are applied correctly for NY --> <amOnPage url="{{CheckoutPage.url}}" stepKey="goToCheckout"/> <waitForPageLoad stepKey="waitForShippingSection"/> + <!-- Checkout select Check/Money Order payment --> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> <see stepKey="seeAddress" selector="{{CheckoutShippingSection.defaultShipping}}" userInput="{{SimpleTaxNY.state}}"/> <waitForElementVisible stepKey="waitForOverviewVisible" selector="{{CheckoutPaymentSection.tax}}"/> @@ -445,6 +454,8 @@ <click stepKey="saveAddress" selector="{{CheckoutShippingSection.updateAddress}}"/> <waitForPageLoad stepKey="waitForAddressSaved"/> + <!-- Checkout select Check/Money Order payment --> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment2"/> <see stepKey="seeAddress2" selector="{{CheckoutShippingSection.defaultShipping}}" userInput="{{SimpleTaxCA.state}}"/> <!-- Assert that taxes are applied correctly for CA --> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/Update01TaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/Update01TaxRateEntityTest.xml new file mode 100644 index 000000000000..2ed31c2e2048 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Test/Update01TaxRateEntityTest.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="Update01TaxRateEntityTest"> + <annotations> + <stories value="Update Tax Rate"/> + <title value="Update tax rate, 0.1 rate"/> + <description value="Test log in to Tax Rate and Update 0.1 Rate"/> + <testCaseId value="MC-5333"/> + <severity value="CRITICAL"/> + <group value="tax"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="defaultTaxRate" stepKey="initialTaxRate"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData stepKey="deleteTaxRate" createDataKey="initialTaxRate" /> + </after> + + <!-- Search the tax rate on tax grid page --> + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> + <waitForPageLoad stepKey="waitForTaxRateIndex1"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> + <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="$$initialTaxRate.code$$" stepKey="fillCode"/> + <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch1"/> + <click selector="{{AdminTaxRateGridSection.nthRow('1')}}" stepKey="clickFirstRow1"/> + + <!-- Update 0.1 tax rate on the tax rate form page --> + <fillField selector="{{AdminTaxRateFormSection.taxIdentifier}}" userInput="{{taxRateCustomRateFrance.code}}" stepKey="fillTaxIdentifierField2"/> + <selectOption selector="{{AdminTaxRateFormSection.country}}" userInput="{{taxRateCustomRateFrance.tax_country_id}}" stepKey="selectCountry1"/> + <wait time="10" stepKey="waitForRegionsLoaded" /> + <selectOption selector="{{AdminTaxRateFormSection.state}}" userInput="{{taxRateCustomRateFrance.tax_region_id}}" stepKey="selectState"/> + <fillField selector="{{AdminTaxRateFormSection.zipCode}}" userInput="{{taxRateCustomRateFrance.tax_postcode}}" stepKey="fillPostCode"/> + <fillField selector="{{AdminTaxRateFormSection.rate}}" userInput="{{taxRateCustomRateFrance.rate}}" stepKey="fillRate1"/> + <click selector="{{AdminTaxRateFormSection.save}}" stepKey="clickSave"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rate." stepKey="seeSuccess"/> + + <!-- Verify we see updated 0.1 tax rate(from the above step) on the tax rate grid page --> + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex4"/> + <waitForPageLoad stepKey="waitForTaxRateIndex2"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> + <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{taxRateCustomRateFrance.code}}" stepKey="fillTaxIdentifierField3"/> + <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch2"/> + <click selector="{{AdminTaxRateGridSection.nthRow('1')}}" stepKey="clickFirstRow2"/> + <!-- Verify we see updated 0.1 tax rate on the tax rate form page --> + <seeInField selector="{{AdminTaxRateFormSection.taxIdentifier}}" userInput="{{taxRateCustomRateFrance.code}}" stepKey="seeRTaxIdentifier"/> + <seeOptionIsSelected selector="{{AdminTaxRateFormSection.country}}" userInput="{{taxRateCustomRateFrance.tax_country}}" stepKey="seeCountry2"/> + <seeOptionIsSelected selector="{{AdminTaxRateFormSection.state}}" userInput="{{taxRateCustomRateFrance.tax_region}}" stepKey="seeState2"/> + <seeInField selector="{{AdminTaxRateFormSection.zipCode}}" userInput="{{taxRateCustomRateFrance.tax_postcode}}" stepKey="seeZipCode"/> + <seeInField selector="{{AdminTaxRateFormSection.rate}}" userInput="{{taxRateCustomRateFrance.rate}}" stepKey="seeRate2"/> + </test> +</tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/Update100TaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/Update100TaxRateEntityTest.xml new file mode 100644 index 000000000000..27a7f2c51724 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Test/Update100TaxRateEntityTest.xml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="Update100TaxRateEntityTest"> + <annotations> + <stories value="Update Tax Rate"/> + <title value="Update tax rate, 100 rate"/> + <description value="Test log in to Tax Rate and Update 100 Rate"/> + <testCaseId value="MC-5328"/> + <severity value="CRITICAL"/> + <group value="tax"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="defaultTaxRate" stepKey="initialTaxRate"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData stepKey="deleteTaxRate" createDataKey="initialTaxRate" /> + </after> + + <!-- Search the tax rate on tax grid page --> + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> + <waitForPageLoad stepKey="waitForTaxRateIndex1"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> + <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="$$initialTaxRate.code$$" stepKey="fillCode"/> + <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch1"/> + <click selector="{{AdminTaxRateGridSection.nthRow('1')}}" stepKey="clickFirstRow1"/> + + <!-- Update values on the tax rate form page --> + <fillField selector="{{AdminTaxRateFormSection.taxIdentifier}}" userInput="{{taxRateCustomRateUS.code}}" stepKey="fillTaxIdentifierField2"/> + <selectOption selector="{{AdminTaxRateFormSection.country}}" userInput="{{taxRateCustomRateUS.tax_country_id}}" stepKey="selectCountry1"/> + <selectOption selector="{{AdminTaxRateFormSection.state}}" userInput="{{taxRateCustomRateUS.tax_region_id}}" stepKey="selectState"/> + <fillField selector="{{AdminTaxRateFormSection.zipCode}}" userInput="{{taxRateCustomRateUS.tax_postcode}}" stepKey="fillPostCode"/> + <fillField selector="{{AdminTaxRateFormSection.rate}}" userInput="{{taxRateCustomRateUS.rate}}" stepKey="fillRate1"/> + <click selector="{{AdminTaxRateFormSection.save}}" stepKey="clickSave"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rate." stepKey="seeSuccess"/> + + <!-- Verify we see updated TaxIdentifier(from the above step) on the tax rate grid page --> + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex4"/> + <waitForPageLoad stepKey="waitForTaxRateIndex2"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> + <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{taxRateCustomRateUS.code}}" stepKey="fillTaxIdentifierField3"/> + <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch2"/> + <click selector="{{AdminTaxRateGridSection.nthRow('1')}}" stepKey="clickFirstRow2"/> + <!-- Verify we see updated values on the tax rate form page --> + <seeInField selector="{{AdminTaxRateFormSection.taxIdentifier}}" userInput="{{taxRateCustomRateUS.code}}" stepKey="seeRTaxIdentifier"/> + <seeOptionIsSelected selector="{{AdminTaxRateFormSection.country}}" userInput="{{taxRateCustomRateUS.tax_country}}" stepKey="seeCountry2"/> + <seeOptionIsSelected selector="{{AdminTaxRateFormSection.state}}" userInput="{{taxRateCustomRateUS.tax_region}}" stepKey="seeState2"/> + <seeInField selector="{{AdminTaxRateFormSection.zipCode}}" userInput="{{taxRateCustomRateUS.tax_postcode}}" stepKey="seeZipCode"/> + <seeInField selector="{{AdminTaxRateFormSection.rate}}" userInput="{{taxRateCustomRateUS.rate}}" stepKey="seeRate2"/> + </test> +</tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/Update1299TaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/Update1299TaxRateEntityTest.xml new file mode 100644 index 000000000000..aa46a5fdae0e --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Test/Update1299TaxRateEntityTest.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="Update1299TaxRateEntityTest"> + <annotations> + <stories value="Update Tax Rate"/> + <title value="Update tax rate, 12.99 rate"/> + <description value="Test log in to Tax Rate and Update 12.99 Rate"/> + <testCaseId value="MC-5332"/> + <severity value="CRITICAL"/> + <group value="tax"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="defaultTaxRate" stepKey="initialTaxRate"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData stepKey="deleteTaxRate" createDataKey="initialTaxRate" /> + </after> + + <!-- Search the tax identifier on tax grid page --> + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> + <waitForPageLoad stepKey="waitForTaxRateIndex1"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> + <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="$$initialTaxRate.code$$" stepKey="fillCode1"/> + <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch1"/> + <click selector="{{AdminTaxRateGridSection.nthRow('1')}}" stepKey="clickFirstRow1"/> + + <!-- Update 12.99 tax rate on the tax rate form page --> + <fillField selector="{{AdminTaxRateFormSection.taxIdentifier}}" userInput="{{taxRateCustomRateUK.code}}" stepKey="fillTaxIdentifierField1"/> + <selectOption selector="{{AdminTaxRateFormSection.country}}" userInput="{{taxRateCustomRateUK.tax_country_id}}" stepKey="selectCountry1"/> + <checkOption selector="{{AdminTaxRateFormSection.zipRange}}" stepKey="checkZipRange"/> + <fillField selector="{{AdminTaxRateFormSection.rangeFrom}}" userInput="{{taxRateCustomRateUK.zip_from}}" stepKey="fillZipFrom"/> + <fillField selector="{{AdminTaxRateFormSection.rangeTo}}" userInput="{{taxRateCustomRateUK.zip_to}}" stepKey="fillZipTo"/> + <fillField selector="{{AdminTaxRateFormSection.rate}}" userInput="{{taxRateCustomRateUK.rate}}" stepKey="fillRate1"/> + <click selector="{{AdminTaxRateFormSection.save}}" stepKey="clickSave"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rate." stepKey="seeSuccess"/> + + <!-- Verify we see updated tax rate(from the above step) on the tax rate grid page --> + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex2"/> + <waitForPageLoad stepKey="waitForTaxRateIndex2"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> + <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{taxRateCustomRateUK.code}}" stepKey="fillTaxIdentifierField2"/> + <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch2"/> + <click selector="{{AdminTaxRateGridSection.nthRow('1')}}" stepKey="clickFirstRow2"/> + <!-- Verify we see updated tax rate on the tax rate form page --> + <seeInField selector="{{AdminTaxRateFormSection.taxIdentifier}}" userInput="{{taxRateCustomRateUK.code}}" stepKey="seeRTaxIdentifier"/> + <seeOptionIsSelected selector="{{AdminTaxRateFormSection.country}}" userInput="{{taxRateCustomRateUK.tax_country}}" stepKey="seeCountry2"/> + <seeCheckboxIsChecked selector="{{AdminTaxRateFormSection.zipRange}}" stepKey="seeZipRange"/> + <seeInField selector="{{AdminTaxRateFormSection.rangeFrom}}" userInput="{{taxRateCustomRateUK.zip_from}}" stepKey="seeZipFrom"/> + <seeInField selector="{{AdminTaxRateFormSection.rangeTo}}" userInput="{{taxRateCustomRateUK.zip_to}}" stepKey="seeZipTo"/> + <seeInField selector="{{AdminTaxRateFormSection.rate}}" userInput="{{taxRateCustomRateUK.rate}}" stepKey="seeRate"/> + </test> +</tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/UpdateAnyRegionTaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/UpdateAnyRegionTaxRateEntityTest.xml new file mode 100644 index 000000000000..d7b25da6c14a --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Test/UpdateAnyRegionTaxRateEntityTest.xml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="UpdateAnyRegionTaxRateEntityTest"> + <annotations> + <stories value="Update Tax Rate"/> + <title value="Update tax rate, any region"/> + <description value="Test log in to Tax Rate and Update Any Region"/> + <testCaseId value="MC-5331"/> + <severity value="CRITICAL"/> + <group value="tax"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="defaultTaxRate" stepKey="initialTaxRate"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData stepKey="deleteTaxRate" createDataKey="initialTaxRate" /> + </after> + + <!-- Search the tax rate on tax grid page --> + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> + <waitForPageLoad stepKey="waitForTaxRateIndex1"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> + <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="$$initialTaxRate.code$$" stepKey="fillCode"/> + <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch1"/> + <click selector="{{AdminTaxRateGridSection.nthRow('1')}}" stepKey="clickFirstRow1"/> + + <!-- Update any region tax rate on the tax rate form page --> + <fillField selector="{{AdminTaxRateFormSection.taxIdentifier}}" userInput="{{taxRateCustomRateCanada.code}}" stepKey="fillTaxIdentifierField2"/> + <selectOption selector="{{AdminTaxRateFormSection.country}}" userInput="{{taxRateCustomRateCanada.tax_country_id}}" stepKey="selectCountry1"/> + <selectOption selector="{{AdminTaxRateFormSection.state}}" userInput="{{taxRateCustomRateCanada.tax_region_id}}" stepKey="selectState"/> + <fillField selector="{{AdminTaxRateFormSection.zipCode}}" userInput="{{taxRateCustomRateCanada.tax_postcode}}" stepKey="fillPostCode"/> + <fillField selector="{{AdminTaxRateFormSection.rate}}" userInput="{{taxRateCustomRateCanada.rate}}" stepKey="fillRate1"/> + <click selector="{{AdminTaxRateFormSection.save}}" stepKey="clickSave"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rate." stepKey="seeSuccess"/> + + <!-- Verify we see updated any region tax rate(from the above step) on the tax rate grid page --> + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex2"/> + <waitForPageLoad stepKey="waitForTaxRateIndex2"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> + <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{taxRateCustomRateCanada.code}}" stepKey="fillTaxIdentifierField3"/> + <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch2"/> + <click selector="{{AdminTaxRateGridSection.nthRow('1')}}" stepKey="clickFirstRow2"/> + <!-- Verify we see updated any region tax rate on the tax rate form page --> + <seeInField selector="{{AdminTaxRateFormSection.taxIdentifier}}" userInput="{{taxRateCustomRateCanada.code}}" stepKey="seeRTaxIdentifier"/> + <seeOptionIsSelected selector="{{AdminTaxRateFormSection.country}}" userInput="{{taxRateCustomRateCanada.tax_country}}" stepKey="seeCountry2"/> + <seeOptionIsSelected selector="{{AdminTaxRateFormSection.state}}" userInput="{{taxRateCustomRateCanada.tax_region_id}}" stepKey="seeState2"/> + <seeInField selector="{{AdminTaxRateFormSection.zipCode}}" userInput="{{taxRateCustomRateCanada.tax_postcode}}" stepKey="seeZipCode"/> + <seeInField selector="{{AdminTaxRateFormSection.rate}}" userInput="{{taxRateCustomRateCanada.rate}}" stepKey="seeRate2"/> + </test> +</tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/UpdateDecimalTaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/UpdateDecimalTaxRateEntityTest.xml new file mode 100644 index 000000000000..300245219690 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Test/UpdateDecimalTaxRateEntityTest.xml @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="UpdateDecimalTaxRateEntityTest"> + <annotations> + <stories value="Update Tax Rate"/> + <title value="Update tax rate, decimal rate"/> + <description value="Test log in to Tax Rate and Update Decimal Tax Rate"/> + <testCaseId value="MC-5329"/> + <severity value="CRITICAL"/> + <group value="tax"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="defaultTaxRate" stepKey="initialTaxRate"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData stepKey="deleteTaxRate" createDataKey="initialTaxRate" /> + </after> + + <!-- Search the tax rate on tax grid page --> + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> + <waitForPageLoad stepKey="waitForTaxRateIndex1"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> + <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="$$initialTaxRate.code$$" stepKey="fillCode"/> + <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch1"/> + <click selector="{{AdminTaxRateGridSection.nthRow('1')}}" stepKey="clickFirstRow1"/> + + <!-- Update decimal tax rate on the tax rate form page --> + <fillField selector="{{AdminTaxRateFormSection.taxIdentifier}}" userInput="{{defaultTaxRateWithZipRange.code}}" stepKey="fillTaxIdentifierField2"/> + <selectOption selector="{{AdminTaxRateFormSection.country}}" userInput="{{defaultTaxRateWithZipRange.tax_country_id}}" stepKey="selectCountry1"/> + <selectOption selector="{{AdminTaxRateFormSection.state}}" userInput="{{defaultTaxRateWithZipRange.tax_region_id}}" stepKey="selectState"/> + <checkOption selector="{{AdminTaxRateFormSection.zipRange}}" stepKey="checkZipRange"/> + <fillField selector="{{AdminTaxRateFormSection.rangeFrom}}" userInput="{{defaultTaxRateWithZipRange.zip_from}}" stepKey="fillZipFrom"/> + <fillField selector="{{AdminTaxRateFormSection.rangeTo}}" userInput="{{defaultTaxRateWithZipRange.zip_to}}" stepKey="fillZipTo"/> + <fillField selector="{{AdminTaxRateFormSection.rate}}" userInput="{{defaultTaxRateWithZipRange.rate}}" stepKey="fillRate1"/> + <click selector="{{AdminTaxRateFormSection.save}}" stepKey="clickSave"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rate." stepKey="seeSuccess"/> + + <!-- Verify we see updated tax rate(from the above step) on the tax rate grid page --> + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex2"/> + <waitForPageLoad stepKey="waitForTaxRateIndex2"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> + <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{defaultTaxRateWithZipRange.code}}" stepKey="fillTaxIdentifierField3"/> + <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch2"/> + <click selector="{{AdminTaxRateGridSection.nthRow('1')}}" stepKey="clickFirstRow2"/> + <!-- Verify we see updated tax rate on the tax rate form page --> + <seeInField selector="{{AdminTaxRateFormSection.taxIdentifier}}" userInput="{{defaultTaxRateWithZipRange.code}}" stepKey="seeRTaxIdentifier"/> + <seeOptionIsSelected selector="{{AdminTaxRateFormSection.country}}" userInput="{{defaultTaxRateWithZipRange.tax_country}}" stepKey="seeCountry2"/> + <seeOptionIsSelected selector="{{AdminTaxRateFormSection.state}}" userInput="{{defaultTaxRateWithZipRange.tax_region}}" stepKey="seeState2"/> + <seeCheckboxIsChecked selector="{{AdminTaxRateFormSection.zipRange}}" stepKey="seeZipRange"/> + <seeInField selector="{{AdminTaxRateFormSection.rangeFrom}}" userInput="{{defaultTaxRateWithZipRange.zip_from}}" stepKey="seeTaxPostCode2"/> + <seeInField selector="{{AdminTaxRateFormSection.rangeTo}}" userInput="{{defaultTaxRateWithZipRange.zip_to}}" stepKey="seeTaxPostCode3"/> + <seeInField selector="{{AdminTaxRateFormSection.rate}}" userInput="{{defaultTaxRateWithZipRange.rate}}" stepKey="seeRate2"/> + </test> +</tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/UpdateLargeTaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/UpdateLargeTaxRateEntityTest.xml new file mode 100644 index 000000000000..6c81a6aeb3f1 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Test/UpdateLargeTaxRateEntityTest.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="UpdateLargeTaxRateEntityTest"> + <annotations> + <stories value="Update Tax Rate"/> + <title value="Update tax rate, large rate"/> + <description value="Test log in to Tax Rate and Update Large Rate"/> + <testCaseId value="MC-5330"/> + <severity value="CRITICAL"/> + <group value="tax"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="defaultTaxRate" stepKey="initialTaxRate"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData stepKey="deleteTaxRate" createDataKey="initialTaxRate" /> + </after> + + <!-- Search the tax rate on tax grid page --> + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> + <waitForPageLoad stepKey="waitForTaxRateIndex1"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> + <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="$$initialTaxRate.code$$" stepKey="fillCode"/> + <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch1"/> + <click selector="{{AdminTaxRateGridSection.nthRow('1')}}" stepKey="clickFirstRow1"/> + + <!-- Update large tax rate on the tax rate form page --> + <fillField selector="{{AdminTaxRateFormSection.taxIdentifier}}" userInput="{{defaultTaxRateWithLargeRate.code}}" stepKey="fillTaxIdentifierField2"/> + <selectOption selector="{{AdminTaxRateFormSection.country}}" userInput="{{defaultTaxRateWithLargeRate.tax_country_id}}" stepKey="selectCountry1"/> + <fillField selector="{{AdminTaxRateFormSection.zipCode}}" userInput="{{defaultTaxRateWithLargeRate.tax_postcode}}" stepKey="fillPostCode"/> + <fillField selector="{{AdminTaxRateFormSection.rate}}" userInput="{{defaultTaxRateWithLargeRate.rate}}" stepKey="fillRate1"/> + <click selector="{{AdminTaxRateFormSection.save}}" stepKey="clickSave"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rate." stepKey="seeSuccess"/> + + <!-- Verify we see updated large tax rate(from the above step) on the tax rate grid page --> + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex4"/> + <waitForPageLoad stepKey="waitForTaxRateIndex2"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> + <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{defaultTaxRateWithLargeRate.code}}" stepKey="fillTaxIdentifierField3"/> + <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch2"/> + <click selector="{{AdminTaxRateGridSection.nthRow('1')}}" stepKey="clickFirstRow2"/> + <!-- Verify we see updated large tax rate on the tax rate form page --> + <seeInField selector="{{AdminTaxRateFormSection.taxIdentifier}}" userInput="{{defaultTaxRateWithLargeRate.code}}" stepKey="seeRTaxIdentifier"/> + <seeOptionIsSelected selector="{{AdminTaxRateFormSection.country}}" userInput="{{defaultTaxRateWithLargeRate.tax_country}}" stepKey="seeCountry2"/> + <seeInField selector="{{AdminTaxRateFormSection.zipCode}}" userInput="{{defaultTaxRateWithLargeRate.tax_postcode}}" stepKey="seeZipCode"/> + <seeInField selector="{{AdminTaxRateFormSection.rate}}" userInput="{{defaultTaxRateWithLargeRate.rate}}" stepKey="seeRate2"/> + </test> +</tests> diff --git a/app/code/Magento/Tax/Test/Unit/Model/Calculation/RateRepositoryTest.php b/app/code/Magento/Tax/Test/Unit/Model/Calculation/RateRepositoryTest.php index bf49f3d47913..77da6950fecf 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/Calculation/RateRepositoryTest.php +++ b/app/code/Magento/Tax/Test/Unit/Model/Calculation/RateRepositoryTest.php @@ -252,7 +252,7 @@ private function getTaxRateMock(array $taxRateData) foreach ($taxRateData as $key => $value) { // convert key from snake case to upper case $taxRateMock->expects($this->any()) - ->method('get' . str_replace(' ', '', ucwords(str_replace('_', ' ', $key)))) + ->method('get' . str_replace('_', '', ucwords($key, '_'))) ->will($this->returnValue($value)); } diff --git a/app/code/Magento/Tax/Test/Unit/Model/Calculation/RowBaseAndTotalBaseCalculatorTestCase.php b/app/code/Magento/Tax/Test/Unit/Model/Calculation/RowBaseAndTotalBaseCalculatorTestCase.php index cbd7ed46e38d..2a7eeb27ee07 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/Calculation/RowBaseAndTotalBaseCalculatorTestCase.php +++ b/app/code/Magento/Tax/Test/Unit/Model/Calculation/RowBaseAndTotalBaseCalculatorTestCase.php @@ -9,6 +9,7 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Tax\Model\Calculation\RowBaseCalculator; use Magento\Tax\Model\Calculation\TotalBaseCalculator; +use Magento\Tax\Api\Data\QuoteDetailsItemExtensionInterface; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -66,6 +67,11 @@ class RowBaseAndTotalBaseCalculatorTestCase extends \PHPUnit\Framework\TestCase */ protected $taxDetailsItem; + /** + * @var \Magento\Tax\Api\Data\QuoteDetailsItemExtensionInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $quoteDetailsItemExtension; + /** * initialize all mocks * @@ -101,7 +107,14 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $this->mockItem = $this->getMockBuilder(\Magento\Tax\Api\Data\QuoteDetailsItemInterface::class)->getMock(); + $this->mockItem = $this->getMockBuilder(\Magento\Tax\Api\Data\QuoteDetailsItemInterface::class) + ->disableOriginalConstructor()->setMethods(['getExtensionAttributes', 'getUnitPrice']) + ->getMockForAbstractClass(); + $this->quoteDetailsItemExtension = $this->getMockBuilder(QuoteDetailsItemExtensionInterface::class) + ->disableOriginalConstructor()->setMethods(['getPriceForTaxCalculation']) + ->getMockForAbstractClass(); + $this->mockItem->expects($this->any())->method('getExtensionAttributes') + ->willReturn($this->quoteDetailsItemExtension); $this->appliedTaxDataObjectFactory = $this->createPartialMock( \Magento\Tax\Api\Data\AppliedTaxInterfaceFactory::class, diff --git a/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/CommonTaxCollectorTest.php b/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/CommonTaxCollectorTest.php index 9325ec10dc62..50d45ad662bd 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/CommonTaxCollectorTest.php +++ b/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/CommonTaxCollectorTest.php @@ -3,79 +3,106 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Tax\Test\Unit\Model\Sales\Total\Quote; -/** - * Test class for \Magento\Tax\Model\Sales\Total\Quote\Tax - */ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Tax\Helper\Data as TaxHelper; +use Magento\Tax\Api\Data\TaxDetailsItemInterface; +use Magento\Quote\Model\Quote\Item as QuoteItem; +use Magento\Store\Model\Store; +use Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector; +use Magento\Tax\Model\Config; +use Magento\Quote\Model\Quote\Address as QuoteAddress; +use Magento\Quote\Model\Quote; +use Magento\Tax\Api\Data\QuoteDetailsItemInterface; +use Magento\Tax\Api\Data\TaxClassKeyInterface; +use Magento\Tax\Model\Sales\Quote\ItemDetails; +use Magento\Tax\Model\TaxClass\Key as TaxClassKey; +use Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory; +use Magento\Tax\Api\Data\TaxClassKeyInterfaceFactory; +use Magento\Quote\Api\Data\ShippingAssignmentInterface; +use Magento\Quote\Api\Data\ShippingInterface; +use Magento\Quote\Model\Quote\Address\Total as QuoteAddressTotal; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; /** + * Common tax collector test + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class CommonTaxCollectorTest extends \PHPUnit\Framework\TestCase +class CommonTaxCollectorTest extends TestCase { /** - * @var \Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector + * @var CommonTaxCollector */ private $commonTaxCollector; /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Tax\Model\Config + * @var MockObject|Config */ private $taxConfig; /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Quote\Model\Quote\Address + * @var MockObject|QuoteAddress */ private $address; /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Quote\Model\Quote + * @var MockObject|Quote */ private $quote; /** - * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Store\Model\Store + * @var MockObject|Store */ private $store; /** - * @var \PHPUnit_Framework_MockObject_MockObject| + * @var MockObject */ protected $taxClassKeyDataObjectFactoryMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject| + * @var MockObject */ protected $quoteDetailsItemDataObjectFactoryMock; /** - * @var \Magento\Tax\Api\Data\QuoteDetailsItemInterface + * @var QuoteDetailsItemInterface */ protected $quoteDetailsItemDataObject; /** - * @var \Magento\Tax\Api\Data\TaxClassKeyInterface + * @var TaxClassKeyInterface */ protected $taxClassKeyDataObject; + /** + * @var TaxHelper + */ + private $taxHelper; + + /** + * {@inheritdoc} + */ protected function setUp() { $objectManager = new ObjectManager($this); - $this->taxConfig = $this->getMockBuilder(\Magento\Tax\Model\Config::class) + $this->taxConfig = $this->getMockBuilder(Config::class) ->disableOriginalConstructor() - ->setMethods(['getShippingTaxClass', 'shippingPriceIncludesTax']) + ->setMethods(['getShippingTaxClass', 'shippingPriceIncludesTax', 'discountTax']) ->getMock(); - $this->store = $this->getMockBuilder(\Magento\Store\Model\Store::class) + $this->store = $this->getMockBuilder(Store::class) ->disableOriginalConstructor() ->setMethods(['__wakeup']) ->getMock(); - $this->quote = $this->getMockBuilder(\Magento\Quote\Model\Quote::class) + $this->quote = $this->getMockBuilder(Quote::class) ->disableOriginalConstructor() ->setMethods(['__wakeup', 'getStore']) ->getMock(); @@ -84,7 +111,7 @@ protected function setUp() ->method('getStore') ->will($this->returnValue($this->store)); - $this->address = $this->getMockBuilder(\Magento\Quote\Model\Quote\Address::class) + $this->address = $this->getMockBuilder(QuoteAddress::class) ->disableOriginalConstructor() ->getMock(); @@ -92,35 +119,41 @@ protected function setUp() ->method('getQuote') ->will($this->returnValue($this->quote)); $methods = ['create']; - $this->quoteDetailsItemDataObject = $objectManager->getObject( - \Magento\Tax\Model\Sales\Quote\ItemDetails::class - ); - $this->taxClassKeyDataObject = $objectManager->getObject(\Magento\Tax\Model\TaxClass\Key::class); + $this->quoteDetailsItemDataObject = $objectManager->getObject(ItemDetails::class); + $this->taxClassKeyDataObject = $objectManager->getObject(TaxClassKey::class); $this->quoteDetailsItemDataObjectFactoryMock - = $this->createPartialMock(\Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory::class, $methods); + = $this->createPartialMock(QuoteDetailsItemInterfaceFactory::class, $methods); $this->quoteDetailsItemDataObjectFactoryMock->expects($this->any()) ->method('create') ->willReturn($this->quoteDetailsItemDataObject); $this->taxClassKeyDataObjectFactoryMock = - $this->createPartialMock(\Magento\Tax\Api\Data\TaxClassKeyInterfaceFactory::class, $methods); + $this->createPartialMock(TaxClassKeyInterfaceFactory::class, $methods); $this->taxClassKeyDataObjectFactoryMock->expects($this->any()) ->method('create') ->willReturn($this->taxClassKeyDataObject); + $this->taxHelper = $this->getMockBuilder(TaxHelper::class) + ->disableOriginalConstructor() + ->getMock(); $this->commonTaxCollector = $objectManager->getObject( - \Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector::class, + CommonTaxCollector::class, [ 'taxConfig' => $this->taxConfig, 'quoteDetailsItemDataObjectFactory' => $this->quoteDetailsItemDataObjectFactoryMock, - 'taxClassKeyDataObjectFactory' => $this->taxClassKeyDataObjectFactoryMock + 'taxClassKeyDataObjectFactory' => $this->taxClassKeyDataObjectFactoryMock, + 'taxHelper' => $this->taxHelper, ] ); } /** + * Test for GetShippingDataObject + * * @param array $addressData * @param bool $useBaseCurrency * @param string $shippingTaxClass * @param bool $shippingPriceInclTax + * + * @return void * @dataProvider getShippingDataObjectDataProvider */ public function testGetShippingDataObject( @@ -128,8 +161,8 @@ public function testGetShippingDataObject( $useBaseCurrency, $shippingTaxClass, $shippingPriceInclTax - ) { - $shippingAssignmentMock = $this->createMock(\Magento\Quote\Api\Data\ShippingAssignmentInterface::class); + ): void { + $shippingAssignmentMock = $this->createMock(ShippingAssignmentInterface::class); $methods = [ 'getShippingDiscountAmount', 'getShippingTaxCalculationAmount', @@ -139,8 +172,10 @@ public function testGetShippingDataObject( 'getBaseShippingAmount', 'getBaseShippingDiscountAmount' ]; - $totalsMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Address\Total::class, $methods); - $shippingMock = $this->createMock(\Magento\Quote\Api\Data\ShippingInterface::class); + /** @var MockObject|QuoteAddressTotal $totalsMock */ + $totalsMock = $this->createPartialMock(QuoteAddressTotal::class, $methods); + $shippingMock = $this->createMock(ShippingInterface::class); + /** @var MockObject|ShippingAssignmentInterface $shippingAssignmentMock */ $shippingAssignmentMock->expects($this->once())->method('getShipping')->willReturn($shippingMock); $shippingMock->expects($this->once())->method('getAddress')->willReturn($this->address); $baseShippingAmount = $addressData['base_shipping_amount']; @@ -184,9 +219,44 @@ public function testGetShippingDataObject( } /** + * Update item tax info + * + * @return void + */ + public function testUpdateItemTaxInfo(): void + { + /** @var MockObject|QuoteItem $quoteItem */ + $quoteItem = $this->getMockBuilder(QuoteItem::class) + ->disableOriginalConstructor() + ->setMethods(['getPrice', 'setPrice', 'getCustomPrice', 'setCustomPrice']) + ->getMock(); + $this->taxHelper->method('applyTaxOnCustomPrice')->willReturn(true); + $quoteItem->method('getCustomPrice')->willReturn(true); + /** @var MockObject|TaxDetailsItemInterface $itemTaxDetails */ + $itemTaxDetails = $this->getMockBuilder(TaxDetailsItemInterface::class) + ->disableOriginalConstructor() + ->getMock(); + /** @var MockObject|TaxDetailsItemInterface $baseItemTaxDetails */ + $baseItemTaxDetails = $this->getMockBuilder(TaxDetailsItemInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $quoteItem->expects($this->once())->method('setCustomPrice'); + + $this->commonTaxCollector->updateItemTaxInfo( + $quoteItem, + $itemTaxDetails, + $baseItemTaxDetails, + $this->store + ); + } + + /** + * Data for testGetShippingDataObject + * * @return array */ - public function getShippingDataObjectDataProvider() + public function getShippingDataObjectDataProvider(): array { $data = [ 'free_shipping' => [ diff --git a/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/ShippingTest.php b/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/ShippingTest.php index 77e25d6f1457..2bfebc984bb8 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/ShippingTest.php +++ b/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/ShippingTest.php @@ -133,7 +133,7 @@ private function getMockObject($className, array $objectState) $getterValueMap = []; $methods = ['__wakeup']; foreach ($objectState as $key => $value) { - $getterName = 'get' . str_replace(' ', '', ucwords(str_replace('_', ' ', $key))); + $getterName = 'get' . str_replace('_', '', ucwords($key, '_')); $getterValueMap[$getterName] = $value; $methods[] = $getterName; } diff --git a/app/code/Magento/Tax/etc/db_schema.xml b/app/code/Magento/Tax/etc/db_schema.xml index 6cc4041f75a6..f5227a9ef3a6 100644 --- a/app/code/Magento/Tax/etc/db_schema.xml +++ b/app/code/Magento/Tax/etc/db_schema.xml @@ -13,7 +13,7 @@ <column xsi:type="varchar" name="class_name" nullable="false" length="255" comment="Class Name"/> <column xsi:type="varchar" name="class_type" nullable="false" length="8" default="CUSTOMER" comment="Class Type"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="class_id"/> </constraint> </table> @@ -27,14 +27,14 @@ comment="Position"/> <column xsi:type="int" name="calculate_subtotal" padding="11" unsigned="false" nullable="false" identity="false" comment="Calculate off subtotal option"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="tax_calculation_rule_id"/> </constraint> - <index name="TAX_CALCULATION_RULE_PRIORITY_POSITION" indexType="btree"> + <index referenceId="TAX_CALCULATION_RULE_PRIORITY_POSITION" indexType="btree"> <column name="priority"/> <column name="position"/> </index> - <index name="TAX_CALCULATION_RULE_CODE" indexType="btree"> + <index referenceId="TAX_CALCULATION_RULE_CODE" indexType="btree"> <column name="code"/> </index> </table> @@ -54,18 +54,18 @@ comment="Zip From"/> <column xsi:type="int" name="zip_to" padding="10" unsigned="true" nullable="true" identity="false" comment="Zip To"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="tax_calculation_rate_id"/> </constraint> - <index name="TAX_CALCULATION_RATE_TAX_COUNTRY_ID_TAX_REGION_ID_TAX_POSTCODE" indexType="btree"> + <index referenceId="TAX_CALCULATION_RATE_TAX_COUNTRY_ID_TAX_REGION_ID_TAX_POSTCODE" indexType="btree"> <column name="tax_country_id"/> <column name="tax_region_id"/> <column name="tax_postcode"/> </index> - <index name="TAX_CALCULATION_RATE_CODE" indexType="btree"> + <index referenceId="TAX_CALCULATION_RATE_CODE" indexType="btree"> <column name="code"/> </index> - <index name="IDX_CA799F1E2CB843495F601E56C84A626D" indexType="btree"> + <index referenceId="IDX_CA799F1E2CB843495F601E56C84A626D" indexType="btree"> <column name="tax_calculation_rate_id"/> <column name="tax_country_id"/> <column name="tax_region_id"/> @@ -84,31 +84,31 @@ identity="false" comment="Customer Tax Class Id"/> <column xsi:type="smallint" name="product_tax_class_id" padding="6" unsigned="false" nullable="false" identity="false" comment="Product Tax Class Id"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="tax_calculation_id"/> </constraint> - <constraint xsi:type="foreign" name="TAX_CALCULATION_PRODUCT_TAX_CLASS_ID_TAX_CLASS_CLASS_ID" + <constraint xsi:type="foreign" referenceId="TAX_CALCULATION_PRODUCT_TAX_CLASS_ID_TAX_CLASS_CLASS_ID" table="tax_calculation" column="product_tax_class_id" referenceTable="tax_class" referenceColumn="class_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="TAX_CALCULATION_CUSTOMER_TAX_CLASS_ID_TAX_CLASS_CLASS_ID" + <constraint xsi:type="foreign" referenceId="TAX_CALCULATION_CUSTOMER_TAX_CLASS_ID_TAX_CLASS_CLASS_ID" table="tax_calculation" column="customer_tax_class_id" referenceTable="tax_class" referenceColumn="class_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="TAX_CALC_TAX_CALC_RATE_ID_TAX_CALC_RATE_TAX_CALC_RATE_ID" + <constraint xsi:type="foreign" referenceId="TAX_CALC_TAX_CALC_RATE_ID_TAX_CALC_RATE_TAX_CALC_RATE_ID" table="tax_calculation" column="tax_calculation_rate_id" referenceTable="tax_calculation_rate" referenceColumn="tax_calculation_rate_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="TAX_CALC_TAX_CALC_RULE_ID_TAX_CALC_RULE_TAX_CALC_RULE_ID" + <constraint xsi:type="foreign" referenceId="TAX_CALC_TAX_CALC_RULE_ID_TAX_CALC_RULE_TAX_CALC_RULE_ID" table="tax_calculation" column="tax_calculation_rule_id" referenceTable="tax_calculation_rule" referenceColumn="tax_calculation_rule_id" onDelete="CASCADE"/> - <index name="TAX_CALCULATION_TAX_CALCULATION_RULE_ID" indexType="btree"> + <index referenceId="TAX_CALCULATION_TAX_CALCULATION_RULE_ID" indexType="btree"> <column name="tax_calculation_rule_id"/> </index> - <index name="TAX_CALCULATION_CUSTOMER_TAX_CLASS_ID" indexType="btree"> + <index referenceId="TAX_CALCULATION_CUSTOMER_TAX_CLASS_ID" indexType="btree"> <column name="customer_tax_class_id"/> </index> - <index name="TAX_CALCULATION_PRODUCT_TAX_CLASS_ID" indexType="btree"> + <index referenceId="TAX_CALCULATION_PRODUCT_TAX_CLASS_ID" indexType="btree"> <column name="product_tax_class_id"/> </index> - <index name="TAX_CALC_TAX_CALC_RATE_ID_CSTR_TAX_CLASS_ID_PRD_TAX_CLASS_ID" indexType="btree"> + <index referenceId="TAX_CALC_TAX_CALC_RATE_ID_CSTR_TAX_CLASS_ID_PRD_TAX_CLASS_ID" indexType="btree"> <column name="tax_calculation_rate_id"/> <column name="customer_tax_class_id"/> <column name="product_tax_class_id"/> @@ -122,24 +122,24 @@ <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Store Id"/> <column xsi:type="varchar" name="value" nullable="false" length="255" comment="Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="tax_calculation_rate_title_id"/> </constraint> - <constraint xsi:type="foreign" name="TAX_CALCULATION_RATE_TITLE_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="TAX_CALCULATION_RATE_TITLE_STORE_ID_STORE_STORE_ID" table="tax_calculation_rate_title" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="FK_37FB965F786AD5897BB3AE90470C42AB" table="tax_calculation_rate_title" + <constraint xsi:type="foreign" referenceId="FK_37FB965F786AD5897BB3AE90470C42AB" table="tax_calculation_rate_title" column="tax_calculation_rate_id" referenceTable="tax_calculation_rate" referenceColumn="tax_calculation_rate_id" onDelete="CASCADE"/> - <index name="TAX_CALCULATION_RATE_TITLE_TAX_CALCULATION_RATE_ID_STORE_ID" indexType="btree"> + <index referenceId="TAX_CALCULATION_RATE_TITLE_TAX_CALCULATION_RATE_ID_STORE_ID" indexType="btree"> <column name="tax_calculation_rate_id"/> <column name="store_id"/> </index> - <index name="TAX_CALCULATION_RATE_TITLE_STORE_ID" indexType="btree"> + <index referenceId="TAX_CALCULATION_RATE_TITLE_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> - <table name="tax_order_aggregated_created" resource="default" engine="innodb" comment="Tax Order Aggregation"> + <table name="tax_order_aggregated_created" resource="sales" engine="innodb" comment="Tax Order Aggregation"> <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> <column xsi:type="date" name="period" comment="Period"/> <column xsi:type="smallint" name="store_id" padding="5" unsigned="true" nullable="true" identity="false" @@ -151,24 +151,24 @@ default="0" comment="Orders Count"/> <column xsi:type="float" name="tax_base_amount_sum" unsigned="false" nullable="true" comment="Tax Base Amount Sum"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> </constraint> - <constraint xsi:type="foreign" name="TAX_ORDER_AGGREGATED_CREATED_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="TAX_ORDER_AGGREGATED_CREATED_STORE_ID_STORE_STORE_ID" table="tax_order_aggregated_created" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="TAX_ORDER_AGGRED_CREATED_PERIOD_STORE_ID_CODE_PERCENT_ORDER_STS"> + <constraint xsi:type="unique" referenceId="TAX_ORDER_AGGRED_CREATED_PERIOD_STORE_ID_CODE_PERCENT_ORDER_STS"> <column name="period"/> <column name="store_id"/> <column name="code"/> <column name="percent"/> <column name="order_status"/> </constraint> - <index name="TAX_ORDER_AGGREGATED_CREATED_STORE_ID" indexType="btree"> + <index referenceId="TAX_ORDER_AGGREGATED_CREATED_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> - <table name="tax_order_aggregated_updated" resource="default" engine="innodb" + <table name="tax_order_aggregated_updated" resource="sales" engine="innodb" comment="Tax Order Aggregated Updated"> <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Id"/> <column xsi:type="date" name="period" comment="Period"/> @@ -181,20 +181,20 @@ default="0" comment="Orders Count"/> <column xsi:type="float" name="tax_base_amount_sum" unsigned="false" nullable="true" comment="Tax Base Amount Sum"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="id"/> </constraint> - <constraint xsi:type="foreign" name="TAX_ORDER_AGGREGATED_UPDATED_STORE_ID_STORE_STORE_ID" + <constraint xsi:type="foreign" referenceId="TAX_ORDER_AGGREGATED_UPDATED_STORE_ID_STORE_STORE_ID" table="tax_order_aggregated_updated" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="TAX_ORDER_AGGRED_UPDATED_PERIOD_STORE_ID_CODE_PERCENT_ORDER_STS"> + <constraint xsi:type="unique" referenceId="TAX_ORDER_AGGRED_UPDATED_PERIOD_STORE_ID_CODE_PERCENT_ORDER_STS"> <column name="period"/> <column name="store_id"/> <column name="code"/> <column name="percent"/> <column name="order_status"/> </constraint> - <index name="TAX_ORDER_AGGREGATED_UPDATED_STORE_ID" indexType="btree"> + <index referenceId="TAX_ORDER_AGGREGATED_UPDATED_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> diff --git a/app/code/Magento/Tax/etc/di.xml b/app/code/Magento/Tax/etc/di.xml index 096f8359fadd..3b46b0f9e258 100644 --- a/app/code/Magento/Tax/etc/di.xml +++ b/app/code/Magento/Tax/etc/di.xml @@ -143,6 +143,7 @@ <arguments> <argument name="fieldMapping" xsi:type="array"> <item name="id" xsi:type="string">tax_calculation_rule_id</item> + <item name="code" xsi:type="string">main_table.code</item> <item name="tax_rate_ids" xsi:type="string">tax_calculation_rate_id</item> <item name="customer_tax_class_ids" xsi:type="string">cd.customer_tax_class_id</item> <item name="product_tax_class_ids" xsi:type="string">cd.product_tax_class_id</item> @@ -154,6 +155,7 @@ <arguments> <argument name="fieldMapping" xsi:type="array"> <item name="id" xsi:type="string">tax_calculation_rule_id</item> + <item name="code" xsi:type="string">main_table.code</item> <item name="tax_rate_ids" xsi:type="string">tax_calculation_rate_id</item> <item name="customer_tax_class_ids" xsi:type="string">cd.customer_tax_class_id</item> <item name="product_tax_class_ids" xsi:type="string">cd.product_tax_class_id</item> diff --git a/app/code/Magento/Tax/etc/extension_attributes.xml b/app/code/Magento/Tax/etc/extension_attributes.xml index 90a5e6d2ecee..41af1df836d6 100644 --- a/app/code/Magento/Tax/etc/extension_attributes.xml +++ b/app/code/Magento/Tax/etc/extension_attributes.xml @@ -20,4 +20,7 @@ <extension_attributes for="Magento\Catalog\Api\Data\ProductRender\PriceInfoInterface"> <attribute code="tax_adjustments" type="Magento\Catalog\Api\Data\ProductRender\PriceInfoInterface" /> </extension_attributes> + <extension_attributes for="Magento\Tax\Api\Data\QuoteDetailsItemInterface"> + <attribute code="price_for_tax_calculation" type="float" /> + </extension_attributes> </config> diff --git a/app/code/Magento/Tax/etc/sales.xml b/app/code/Magento/Tax/etc/sales.xml index 64d29ece898d..15afd499bce3 100644 --- a/app/code/Magento/Tax/etc/sales.xml +++ b/app/code/Magento/Tax/etc/sales.xml @@ -9,7 +9,7 @@ <section name="quote"> <group name="totals"> <item name="tax_subtotal" instance="Magento\Tax\Model\Sales\Total\Quote\Subtotal" sort_order="200"/> - <item name="tax_shipping" instance="Magento\Tax\Model\Sales\Total\Quote\Shipping" sort_order="300"/> + <item name="tax_shipping" instance="Magento\Tax\Model\Sales\Total\Quote\Shipping" sort_order="375"/> <item name="tax" instance="Magento\Tax\Model\Sales\Total\Quote\Tax" sort_order="450"> <renderer name="adminhtml" instance="Magento\Sales\Block\Adminhtml\Order\Create\Totals\Tax"/> <renderer name="frontend" instance="Magento\Tax\Block\Checkout\Tax"/> diff --git a/app/code/Magento/Tax/i18n/en_US.csv b/app/code/Magento/Tax/i18n/en_US.csv index e6d89deb7696..836221e2ec97 100644 --- a/app/code/Magento/Tax/i18n/en_US.csv +++ b/app/code/Magento/Tax/i18n/en_US.csv @@ -178,3 +178,4 @@ Rate,Rate "Your credit card will be charged for","Your credit card will be charged for" "An error occurred while loading tax rates.","An error occurred while loading tax rates." "You will be charged for","You will be charged for" +"Not yet calculated", "Not yet calculated" diff --git a/app/code/Magento/Tax/view/adminhtml/layout/tax_rate_block.xml b/app/code/Magento/Tax/view/adminhtml/layout/tax_rate_block.xml index 463994262b7d..5c50b306411e 100644 --- a/app/code/Magento/Tax/view/adminhtml/layout/tax_rate_block.xml +++ b/app/code/Magento/Tax/view/adminhtml/layout/tax_rate_block.xml @@ -11,7 +11,7 @@ <block class="Magento\Backend\Block\Widget\Grid" name="adminhtml.tax.rate.grid" as="grid"> <arguments> <argument name="id" xsi:type="string">tax_rate_grid</argument> - <argument name="dataSource" xsi:type="object">Magento\Tax\Model\TaxRateCollection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Tax\Model\TaxRateCollection</argument> <argument name="default_sort" xsi:type="string">region_name</argument> <argument name="default_dir" xsi:type="string">ASC</argument> <argument name="save_parameters_in_session" xsi:type="string">1</argument> diff --git a/app/code/Magento/Tax/view/adminhtml/layout/tax_rule_block.xml b/app/code/Magento/Tax/view/adminhtml/layout/tax_rule_block.xml index 5e42b835c035..07afd05e63f8 100644 --- a/app/code/Magento/Tax/view/adminhtml/layout/tax_rule_block.xml +++ b/app/code/Magento/Tax/view/adminhtml/layout/tax_rule_block.xml @@ -11,7 +11,7 @@ <block class="Magento\Backend\Block\Widget\Grid" name="adminhtml.block.tax.rule.grid" as="grid"> <arguments> <argument name="id" xsi:type="string">taxRuleGrid</argument> - <argument name="dataSource" xsi:type="object">Magento\Tax\Model\TaxRuleCollection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Tax\Model\TaxRuleCollection</argument> <argument name="default_sort" xsi:type="string">tax_rule_id</argument> <argument name="default_dir" xsi:type="string">ASC</argument> <argument name="save_parameters_in_session" xsi:type="string">1</argument> 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 18e86549a1ff..000000000000 --- 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 a49f199ba56b..000000000000 --- 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/base/web/js/price/adjustment.js b/app/code/Magento/Tax/view/base/web/js/price/adjustment.js index 9af15f84562f..a17d130d9282 100644 --- a/app/code/Magento/Tax/view/base/web/js/price/adjustment.js +++ b/app/code/Magento/Tax/view/base/web/js/price/adjustment.js @@ -62,7 +62,7 @@ define([ }, /** - * Set price taax type. + * Set price tax type. * * @param {String} priceType * @return {Object} 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 c86c3b4d1ab0..2b1f387f5c8c 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 @@ -11,18 +11,22 @@ define([ 'ko', 'Magento_Checkout/js/view/summary/abstract-total', 'Magento_Checkout/js/model/quote', - 'Magento_Checkout/js/model/totals' -], function (ko, Component, quote, totals) { + 'Magento_Checkout/js/model/totals', + '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: { isTaxDisplayedInGrandTotal: isTaxDisplayedInGrandTotal, - notCalculatedMessage: 'Not yet calculated', + notCalculatedMessage: $t('Not yet calculated'), template: 'Magento_Tax/checkout/summary/tax' }, totals: quote.getTotals(), @@ -97,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 9c45e73db6fa..45c468096abe 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 0f2e3251bcfd..5f1ac86e38ff 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/TaxImportExport/Controller/Adminhtml/Rate/ImportExport.php b/app/code/Magento/TaxImportExport/Controller/Adminhtml/Rate/ImportExport.php index 929c9db01a29..4264f5c3b776 100644 --- a/app/code/Magento/TaxImportExport/Controller/Adminhtml/Rate/ImportExport.php +++ b/app/code/Magento/TaxImportExport/Controller/Adminhtml/Rate/ImportExport.php @@ -5,9 +5,10 @@ */ namespace Magento\TaxImportExport\Controller\Adminhtml\Rate; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Framework\Controller\ResultFactory; -class ImportExport extends \Magento\TaxImportExport\Controller\Adminhtml\Rate +class ImportExport extends \Magento\TaxImportExport\Controller\Adminhtml\Rate implements HttpGetActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/TaxImportExport/view/adminhtml/templates/importExportHeader.phtml b/app/code/Magento/TaxImportExport/view/adminhtml/templates/importExportHeader.phtml index 223b3e9888ee..75f04eae8215 100644 --- a/app/code/Magento/TaxImportExport/view/adminhtml/templates/importExportHeader.phtml +++ b/app/code/Magento/TaxImportExport/view/adminhtml/templates/importExportHeader.phtml @@ -2,4 +2,4 @@ /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. - */; + */ diff --git a/app/code/Magento/Theme/Block/Html/Header/Logo.php b/app/code/Magento/Theme/Block/Html/Header/Logo.php index 0a0e71f44ba3..b51f624c2033 100644 --- a/app/code/Magento/Theme/Block/Html/Header/Logo.php +++ b/app/code/Magento/Theme/Block/Html/Header/Logo.php @@ -43,6 +43,10 @@ public function __construct( /** * Check if current url is url for home page * + * @deprecated This function is no longer used. It was previously used by + * Magento/Theme/view/frontend/templates/html/header/logo.phtml + * to check if the logo should be clickable on the homepage. + * * @return bool */ public function isHomePage() diff --git a/app/code/Magento/Theme/Block/Html/Pager.php b/app/code/Magento/Theme/Block/Html/Pager.php index d4cfe544f479..ad3f4aad676e 100644 --- a/app/code/Magento/Theme/Block/Html/Pager.php +++ b/app/code/Magento/Theme/Block/Html/Pager.php @@ -184,6 +184,8 @@ public function setCollection($collection) } /** + * Returns data collection + * * @return \Magento\Framework\Data\Collection */ public function getCollection() @@ -192,7 +194,10 @@ public function getCollection() } /** + * Set page variable name + * * @param string $varName + * * @return $this */ public function setPageVarName($varName) @@ -202,6 +207,8 @@ public function setPageVarName($varName) } /** + * Get page variable name + * * @return string */ public function getPageVarName() @@ -210,7 +217,10 @@ public function getPageVarName() } /** + * Set show per page param + * * @param bool $varName + * * @return $this */ public function setShowPerPage($varName) @@ -220,6 +230,8 @@ public function setShowPerPage($varName) } /** + * Is show per page + * * @return bool */ public function isShowPerPage() @@ -234,6 +246,7 @@ public function isShowPerPage() * Set the name for pager limit data * * @param string $varName + * * @return $this */ public function setLimitVarName($varName) @@ -275,6 +288,8 @@ public function getAvailableLimit() } /** + * Get first number + * * @return int */ public function getFirstNum() @@ -284,6 +299,8 @@ public function getFirstNum() } /** + * Get last number + * * @return int */ public function getLastNum() @@ -333,7 +350,10 @@ public function isLastPage() } /** + * Is limit current + * * @param int $limit + * * @return bool */ public function isLimitCurrent($limit) @@ -342,7 +362,10 @@ public function isLimitCurrent($limit) } /** + * Is page current + * * @param int $page + * * @return bool */ public function isPageCurrent($page) @@ -351,6 +374,8 @@ public function isPageCurrent($page) } /** + * Get pages + * * @return array */ public function getPages() @@ -377,6 +402,8 @@ public function getPages() } /** + * Get first page url + * * @return string */ public function getFirstPageUrl() @@ -418,6 +445,7 @@ public function getLastPageUrl() * Retrieve page URL * * @param string $page + * * @return string */ public function getPageUrl($page) @@ -426,7 +454,10 @@ public function getPageUrl($page) } /** + * Get limit url + * * @param int $limit + * * @return string */ public function getLimitUrl($limit) @@ -438,6 +469,7 @@ public function getLimitUrl($limit) * Retrieve page URL by defined parameters * * @param array $params + * * @return string */ public function getPagerUrl($params = []) @@ -453,6 +485,8 @@ public function getPagerUrl($params = []) } /** + * Get path + * * @return string */ protected function getPath() @@ -580,7 +614,7 @@ public function getJump() */ public function setFrameLength($frame) { - $frame = abs(intval($frame)); + $frame = abs((int)$frame); if ($frame == 0) { $frame = $this->_frameLength; } @@ -600,7 +634,7 @@ public function setFrameLength($frame) */ public function setJump($jump) { - $jump = abs(intval($jump)); + $jump = abs((int)$jump); if ($this->getJump() != $jump) { $this->_setFrameInitialized(false); $this->_jump = $jump; diff --git a/app/code/Magento/Theme/Block/Html/Topmenu.php b/app/code/Magento/Theme/Block/Html/Topmenu.php index 0dca0f8606a8..242947d19b32 100644 --- a/app/code/Magento/Theme/Block/Html/Topmenu.php +++ b/app/code/Magento/Theme/Block/Html/Topmenu.php @@ -361,19 +361,6 @@ public function getIdentities() return $this->identities; } - /** - * Get cache key informative items - * - * @return array - * @since 100.1.0 - */ - public function getCacheKeyInfo() - { - $keyInfo = parent::getCacheKeyInfo(); - $keyInfo[] = $this->getUrl('*/*/*', ['_current' => true, '_query' => '']); - return $keyInfo; - } - /** * Get tags array for saving cache * diff --git a/app/code/Magento/Theme/Controller/Adminhtml/System/Design/Theme/Index.php b/app/code/Magento/Theme/Controller/Adminhtml/System/Design/Theme/Index.php index d51fd658075f..1a136cca4d62 100644 --- a/app/code/Magento/Theme/Controller/Adminhtml/System/Design/Theme/Index.php +++ b/app/code/Magento/Theme/Controller/Adminhtml/System/Design/Theme/Index.php @@ -6,11 +6,13 @@ */ namespace Magento\Theme\Controller\Adminhtml\System\Design\Theme; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + /** * Class Index * @deprecated 100.2.0 */ -class Index extends \Magento\Theme\Controller\Adminhtml\System\Design\Theme +class Index extends \Magento\Theme\Controller\Adminhtml\System\Design\Theme implements HttpGetActionInterface { /** * Index action diff --git a/app/code/Magento/Theme/Controller/Adminhtml/System/Design/Theme/UploadJs.php b/app/code/Magento/Theme/Controller/Adminhtml/System/Design/Theme/UploadJs.php index e87e70e21b9d..89636ad3de50 100644 --- a/app/code/Magento/Theme/Controller/Adminhtml/System/Design/Theme/UploadJs.php +++ b/app/code/Magento/Theme/Controller/Adminhtml/System/Design/Theme/UploadJs.php @@ -4,19 +4,21 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Theme\Controller\Adminhtml\System\Design\Theme; +use Magento\Framework\App\Action\HttpGetActionInterface; + /** * Class UploadJs * @deprecated */ -class UploadJs extends \Magento\Theme\Controller\Adminhtml\System\Design\Theme +class UploadJs extends \Magento\Theme\Controller\Adminhtml\System\Design\Theme implements HttpGetActionInterface { /** * Upload js file * * @return void - * @throws \Magento\Framework\Exception\LocalizedException */ public function execute() { @@ -50,6 +52,7 @@ public function execute() \Magento\Framework\View\Design\Theme\Customization\File\Js::TYPE ); $result = ['error' => false, 'files' => $customization->generateFileInfo($customJsFiles)]; + // phpcs:disable Magento2.Exceptions.ThrowCatch } catch (\Magento\Framework\Exception\LocalizedException $e) { $result = ['error' => true, 'message' => $e->getMessage()]; } catch (\Exception $e) { diff --git a/app/code/Magento/Theme/Controller/Result/JsFooterPlugin.php b/app/code/Magento/Theme/Controller/Result/JsFooterPlugin.php new file mode 100644 index 000000000000..a234cd589904 --- /dev/null +++ b/app/code/Magento/Theme/Controller/Result/JsFooterPlugin.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Theme\Controller\Result; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Framework\App\Response\Http; + +/** + * Plugin for putting all js to footer. + */ +class JsFooterPlugin +{ + private const XML_PATH_DEV_MOVE_JS_TO_BOTTOM = 'dev/js/move_inline_to_bottom'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct(ScopeConfigInterface $scopeConfig) + { + $this->scopeConfig = $scopeConfig; + } + + /** + * Put all javascript to footer before sending the response. + * + * @param Http $subject + * @return void + */ + public function beforeSendResponse(Http $subject) + { + $content = $subject->getContent(); + $script = []; + if (strpos($content, '</body') !== false) { + if ($this->scopeConfig->isSetFlag( + self::XML_PATH_DEV_MOVE_JS_TO_BOTTOM, + ScopeInterface::SCOPE_STORE + ) + ) { + $pattern = '#<script[^>]*+(?<!text/x-magento-template.)>.*?</script>#is'; + $content = preg_replace_callback( + $pattern, + function ($matchPart) use (&$script) { + $script[] = $matchPart[0]; + return ''; + }, + $content + ); + $subject->setContent( + str_replace('</body', implode("\n", $script) . "\n</body", $content) + ); + } + } + } +} diff --git a/app/code/Magento/Theme/Model/Design/Backend/File.php b/app/code/Magento/Theme/Model/Design/Backend/File.php index b37628e54aa3..27636b4d574c 100644 --- a/app/code/Magento/Theme/Model/Design/Backend/File.php +++ b/app/code/Magento/Theme/Model/Design/Backend/File.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Theme\Model\Design\Backend; use Magento\Config\Model\Config\Backend\File\RequestData\RequestDataInterface; @@ -22,6 +23,8 @@ use Magento\Theme\Model\Design\Config\FileUploader\FileProcessor; /** + * File Backend + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class File extends BackendFile @@ -88,41 +91,33 @@ public function beforeSave() { $values = $this->getValue(); $value = reset($values) ?: []; - if (!isset($value['file'])) { + + // Need to check name when it is uploaded in the media gallary + $file = $value['file'] ?? $value['name'] ?? null; + if (!isset($file)) { throw new LocalizedException( __('%1 does not contain field \'file\'', $this->getData('field_config/field')) ); } if (isset($value['exists'])) { - $this->setValue($value['file']); + $this->setValue($file); return $this; } - $filename = basename($value['file']); - $result = $this->_mediaDirectory->copyFile( - $this->getTmpMediaPath($filename), - $this->_getUploadDir() . '/' . $filename - ); - if ($result) { - $this->_mediaDirectory->delete($this->getTmpMediaPath($filename)); - if ($this->_addWhetherScopeInfo()) { - $filename = $this->_prependScopeInfo($filename); - } - $this->setValue($filename); - } else { - $this->unsValue(); - } + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $this->updateMediaDirectory(basename($file), $value['url']); return $this; } /** - * @return array + * @inheritDoc */ public function afterLoad() { $value = $this->getValue(); if ($value && !is_array($value)) { + //phpcs:ignore Magento2.Functions.DiscouragedFunction $fileName = $this->_getUploadDir() . '/' . basename($value); $fileInfo = null; if ($this->_mediaDirectory->isExist($fileName)) { @@ -133,6 +128,7 @@ public function afterLoad() 'url' => $url, 'file' => $value, 'size' => is_array($stat) ? $stat['size'] : 0, + //phpcs:ignore Magento2.Functions.DiscouragedFunction 'name' => basename($value), 'type' => $this->getMimeType($fileName), 'exists' => true, @@ -166,6 +162,8 @@ protected function getUploadDirPath($uploadDir) } /** + * Get Value + * * @return array */ public function getValue() @@ -231,4 +229,49 @@ private function getMime() } return $this->mime; } + + /** + * Get Relative Media Path + * + * @param string $path + * @return string + */ + private function getRelativeMediaPath(string $path): string + { + return preg_replace('/\/(pub\/)?media\//', '', $path); + } + + /** + * Move file to the correct media directory + * + * @param string $filename + * @param string $url + * @throws LocalizedException + */ + private function updateMediaDirectory(string $filename, string $url) + { + $relativeMediaPath = $this->getRelativeMediaPath($url); + $tmpMediaPath = $this->getTmpMediaPath($filename); + $mediaPath = $this->_mediaDirectory->isFile($relativeMediaPath) ? $relativeMediaPath : $tmpMediaPath; + $destinationMediaPath = $this->_getUploadDir() . '/' . $filename; + + $result = $mediaPath === $destinationMediaPath; + if (!$result) { + $result = $this->_mediaDirectory->copyFile( + $mediaPath, + $destinationMediaPath + ); + } + if ($result) { + if ($mediaPath === $tmpMediaPath) { + $this->_mediaDirectory->delete($mediaPath); + } + if ($this->_addWhetherScopeInfo()) { + $filename = $this->_prependScopeInfo($filename); + } + $this->setValue($filename); + } else { + $this->unsValue(); + } + } } diff --git a/app/code/Magento/Theme/Model/Design/Config/FileUploader/FileProcessor.php b/app/code/Magento/Theme/Model/Design/Config/FileUploader/FileProcessor.php index ecf5bcbea6df..901dbf7d88f3 100644 --- a/app/code/Magento/Theme/Model/Design/Config/FileUploader/FileProcessor.php +++ b/app/code/Magento/Theme/Model/Design/Config/FileUploader/FileProcessor.php @@ -18,6 +18,8 @@ use Magento\Store\Model\StoreManagerInterface; /** + * Design file processor. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class FileProcessor @@ -79,8 +81,8 @@ public function __construct( * Save file to temp media directory * * @param string $fileId + * * @return array - * @throws LocalizedException */ public function saveToTmp($fileId) { diff --git a/app/code/Magento/Theme/Model/Design/Config/Validator.php b/app/code/Magento/Theme/Model/Design/Config/Validator.php index 994eeba317a3..1279d9d9ccd2 100644 --- a/app/code/Magento/Theme/Model/Design/Config/Validator.php +++ b/app/code/Magento/Theme/Model/Design/Config/Validator.php @@ -80,7 +80,7 @@ public function validate(DesignConfigInterface $designConfig) ["templateName" => $name] ) ); - }; + } } } } diff --git a/app/code/Magento/Theme/Model/PageLayout/Config/Builder.php b/app/code/Magento/Theme/Model/PageLayout/Config/Builder.php index 98fa12ab987b..13b8aa23073c 100644 --- a/app/code/Magento/Theme/Model/PageLayout/Config/Builder.php +++ b/app/code/Magento/Theme/Model/PageLayout/Config/Builder.php @@ -27,6 +27,11 @@ class Builder implements \Magento\Framework\View\Model\PageLayout\Config\Builder */ protected $themeCollection; + /** + * @var array + */ + private $configFiles = []; + /** * @param \Magento\Framework\View\PageLayout\ConfigFactory $configFactory * @param \Magento\Framework\View\PageLayout\File\Collector\Aggregated $fileCollector @@ -44,7 +49,7 @@ public function __construct( } /** - * @return \Magento\Framework\View\PageLayout\Config + * @inheritdoc */ public function getPageLayoutsConfig() { @@ -52,15 +57,20 @@ public function getPageLayoutsConfig() } /** + * Retrieve configuration files. + * * @return array */ protected function getConfigFiles() { - $configFiles = []; - foreach ($this->themeCollection->loadRegisteredThemes() as $theme) { - $configFiles = array_merge($configFiles, $this->fileCollector->getFilesContent($theme, 'layouts.xml')); + if (!$this->configFiles) { + $configFiles = []; + foreach ($this->themeCollection->loadRegisteredThemes() as $theme) { + $configFiles[] = $this->fileCollector->getFilesContent($theme, 'layouts.xml'); + } + $this->configFiles = array_merge(...$configFiles); } - return $configFiles; + return $this->configFiles; } } diff --git a/app/code/Magento/Theme/Model/ResourceModel/Design.php b/app/code/Magento/Theme/Model/ResourceModel/Design.php index c36640b98ef4..6711f4a2117b 100644 --- a/app/code/Magento/Theme/Model/ResourceModel/Design.php +++ b/app/code/Magento/Theme/Model/ResourceModel/Design.php @@ -46,7 +46,7 @@ protected function _construct() * Perform actions before object save * * @param \Magento\Framework\Model\AbstractModel $object - * @return $this + * @return void * @throws \Magento\Framework\Exception\LocalizedException */ public function _beforeSave(\Magento\Framework\Model\AbstractModel $object) @@ -152,7 +152,6 @@ protected function _checkIntersection($storeId, $dateFrom, $dateTo, $currentId) $dateConditions = []; } - $condition = ''; if (!empty($dateConditions)) { $condition = '(' . implode(') OR (', $dateConditions) . ')'; $select->where($condition); diff --git a/app/code/Magento/Theme/Model/Wysiwyg/Storage.php b/app/code/Magento/Theme/Model/Wysiwyg/Storage.php index 82206b8eb1c6..0519460c0242 100644 --- a/app/code/Magento/Theme/Model/Wysiwyg/Storage.php +++ b/app/code/Magento/Theme/Model/Wysiwyg/Storage.php @@ -4,13 +4,13 @@ * See COPYING.txt for license details. */ -/** - * Theme wysiwyg storage model - */ namespace Magento\Theme\Model\Wysiwyg; use Magento\Framework\App\Filesystem\DirectoryList; +/** + * Theme wysiwyg storage model + */ class Storage { /** @@ -105,7 +105,7 @@ public function __construct( * Upload file * * @param string $targetPath - * @return bool + * @return array * @throws \Magento\Framework\Exception\LocalizedException */ public function uploadFile($targetPath) @@ -127,14 +127,6 @@ public function uploadFile($targetPath) $this->_createThumbnail($targetPath . '/' . $uploader->getUploadedFileName()); - $result['cookie'] = [ - 'name' => $this->_helper->getSession()->getName(), - 'value' => $this->_helper->getSession()->getSessionId(), - 'lifetime' => $this->_helper->getSession()->getCookieLifetime(), - 'path' => $this->_helper->getSession()->getCookiePath(), - 'domain' => $this->_helper->getSession()->getCookieDomain() - ]; - return $result; } @@ -274,7 +266,7 @@ public function getFilesCollection() if (self::TYPE_IMAGE == $storageType) { $requestParams['file'] = $fileName; $file['thumbnailParams'] = $requestParams; - + //phpcs:ignore Generic.PHP.NoSilencedErrors $size = @getimagesize($path); if (is_array($size)) { $file['width'] = $size[0]; diff --git a/app/code/Magento/Theme/Test/Mftf/ActionGroup/NavigateToFaviconMediaFolderActionGroup.xml b/app/code/Magento/Theme/Test/Mftf/ActionGroup/NavigateToFaviconMediaFolderActionGroup.xml new file mode 100644 index 000000000000..6b9868657432 --- /dev/null +++ b/app/code/Magento/Theme/Test/Mftf/ActionGroup/NavigateToFaviconMediaFolderActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="NavigateToFaviconMediaFolderActionGroup"> + <arguments> + <argument name="StoreFolder" type="string"/> + </arguments> + <conditionalClick selector="{{MediaGallerySection.StorageRootArrow}}" dependentSelector="{{MediaGallerySection.checkIfArrowExpand}}" stepKey="clickArrowIfClosed" visible="true"/> + <waitForElement selector="{{AdminDesignConfigSection.faviconArrow}}" stepKey="waitForFaviconFolder"/> + <conditionalClick selector="{{AdminDesignConfigSection.faviconArrow}}" dependentSelector="{{AdminDesignConfigSection.checkIfFaviconArrowExpand}}" stepKey="clickFaviconArrowIfClosed" visible="true"/> + <waitForElement selector="{{AdminDesignConfigSection.storesArrow}}" stepKey="waitForStoresFolder"/> + <conditionalClick selector="{{AdminDesignConfigSection.storesArrow}}" dependentSelector="{{AdminDesignConfigSection.checkIfStoresArrowExpand}}" stepKey="clickStoresArrowIfClosed" visible="true"/> + <waitForElement selector="{{StoreFolder}}" stepKey="waitForStoreFolder"/> + <click selector="{{StoreFolder}}" stepKey="clickOnCreatedFolder"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Theme/Test/Mftf/ActionGroup/StorefrontClickOnHeaderLogoActionGroup.xml b/app/code/Magento/Theme/Test/Mftf/ActionGroup/StorefrontClickOnHeaderLogoActionGroup.xml new file mode 100644 index 000000000000..cd4117a4cfa6 --- /dev/null +++ b/app/code/Magento/Theme/Test/Mftf/ActionGroup/StorefrontClickOnHeaderLogoActionGroup.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontClickOnHeaderLogoActionGroup"> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToHomePage"/> + <waitForPageLoad stepKey="waitForHomePageLoaded"/> + <click selector="{{StorefrontHeaderSection.logoLink}}" stepKey="clickOnLogo"/> + <waitForPageLoad stepKey="waitForHomePageLoadedAfterClickOnLogo"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Theme/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Theme/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 000000000000..e82665106256 --- /dev/null +++ b/app/code/Magento/Theme/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminMenuContentDesignThemes"> + <data key="pageTitle">Themes</data> + <data key="title">Themes</data> + <data key="dataUiId">magento-theme-system-design-theme</data> + </entity> +</entities> diff --git a/app/code/Magento/Theme/Test/Mftf/Page/DesignConfigPage.xml b/app/code/Magento/Theme/Test/Mftf/Page/DesignConfigPage.xml new file mode 100644 index 000000000000..7a802aee73e3 --- /dev/null +++ b/app/code/Magento/Theme/Test/Mftf/Page/DesignConfigPage.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="DesignConfigPage" url="theme/design_config/" area="admin" module="Magento_Theme"> + <section name="AdminDesignConfigSection"/> + </page> +</pages> diff --git a/app/code/Magento/Theme/Test/Mftf/Section/AdminDesignConfigSection.xml b/app/code/Magento/Theme/Test/Mftf/Section/AdminDesignConfigSection.xml new file mode 100644 index 000000000000..c2652f33f760 --- /dev/null +++ b/app/code/Magento/Theme/Test/Mftf/Section/AdminDesignConfigSection.xml @@ -0,0 +1,35 @@ +<?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="AdminDesignConfigSection"> + <element name="scopeRow" type="button" selector="//*[contains(@class,'data-row')][{{arg1}}]//*[contains(@class,'action-menu-item')]" parameterized="true"/> + <element name="watermarkSectionHeader" type="text" selector="[data-index='watermark']"/> + <element name="watermarkSection" type="text" selector="[data-index='watermark'] .admin__fieldset-wrapper-content"/> + <element name="imageUploadInputByFieldsetName" type="input" selector="//*[contains(@class,'fieldset-wrapper')][child::*[contains(@class,'fieldset-wrapper-title')]//*[contains(text(),'{{arg1}}')]]//*[contains(@class,'file-uploader')]//input" parameterized="true"/> + <element name="imageUploadPreviewByFieldsetName" type="input" selector="//*[contains(@class,'fieldset-wrapper')][child::*[contains(@class,'fieldset-wrapper-title')]//*[contains(text(),'{{arg1}}')]]//*[contains(@class,'file-uploader-preview')]//img" parameterized="true"/> + <element name="addSelectedFromMediaGallery" type="input" selector="//button[contains(@title,'Add Selected')]"/> + <element name="htmlHeaderSection" type="text" selector="[data-index='head']"/> + <element name="selectFromGalleryByFieldsetName" type="input" selector="//*[contains(@class,'fieldset-wrapper')][child::*[contains(@class,'fieldset-wrapper-title')]//*[contains(text(),'{{arg1}}')]]//*[contains(@class,'file-uploader')]//label[contains(text(), 'Select from Gallery')]" parameterized="true"/> + <element name="imageUploadFromMediaGallery" type="input" selector="//input[contains(@class,'fileupload')]" /> + <element name="saveConfiguration" type="input" selector="//button[contains(@title, 'Save Configuration')]" /> + <element name="successNotification" type="text" selector="//div[contains(@data-ui-id, 'messages-message-success')]" /> + <element name="useDefaultByFieldsetName" type="input" selector="//*[contains(@class,'fieldset-wrapper')][child::*[contains(@class,'fieldset-wrapper-title')]//*[contains(text(),'{{arg1}}')]]//*[contains(@class,'file-uploader')]//span[contains(text(), 'Use Default Value')]" parameterized="true" /> + <element name="logoSectionHeader" type="text" selector="[data-index='email']"/> + <element name="logoSection" type="text" selector="[data-index='email'] .admin__fieldset-wrapper-content"/> + <element name="logoUpload" type ="input" selector="[name='email_logo']" /> + <element name="logoWrapperOpen" type ="text" selector="[data-index='email'] [data-state-collapsible ='closed']"/> + <element name="logoPreview" type ="text" selector="[alt ='magento-logo.png']"/> + <element name="faviconArrow" type="button" selector="#ZmF2aWNvbg-- > .jstree-icon" /> + <element name="checkIfFaviconArrowExpand" type="button" selector="//li[@id='ZmF2aWNvbg--' and contains(@class,'jstree-closed')]" /> + <element name="storesArrow" type="button" selector="#ZmF2aWNvbi9zdG9yZXM- > .jstree-icon" /> + <element name="checkIfStoresArrowExpand" type="button" selector="//li[@id='ZmF2aWNvbi9zdG9yZXM-' and contains(@class,'jstree-closed')]" /> + <element name="storeLink" type="button" selector="#ZmF2aWNvbi9zdG9yZXMvMQ-- > a"/> + </section> +</sections> diff --git a/app/code/Magento/Theme/Test/Mftf/Section/StorefrontHeaderSection.xml b/app/code/Magento/Theme/Test/Mftf/Section/StorefrontHeaderSection.xml new file mode 100644 index 000000000000..e2f0a01fc733 --- /dev/null +++ b/app/code/Magento/Theme/Test/Mftf/Section/StorefrontHeaderSection.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="StorefrontHeaderSection"> + <element name="welcomeMessage" type="text" selector=".greet.welcome"/> + <element name="logoLink" type="button" selector=".header .logo"/> + </section> +</sections> diff --git a/app/code/Magento/Theme/Test/Mftf/Test/AdminContentThemesNavigateMenuTest.xml b/app/code/Magento/Theme/Test/Mftf/Test/AdminContentThemesNavigateMenuTest.xml new file mode 100644 index 000000000000..8e7bfd71b07d --- /dev/null +++ b/app/code/Magento/Theme/Test/Mftf/Test/AdminContentThemesNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminContentThemesNavigateMenuTest"> + <annotations> + <features value="Theme"/> + <stories value="Menu Navigation"/> + <title value="Admin content themes navigate menu test"/> + <description value="Admin should be able to navigate to Content > Themes"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14112"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToContentThemesPage"> + <argument name="menuUiId" value="{{AdminMenuContent.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuContentDesignThemes.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuContentDesignThemes.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Theme/Test/Mftf/Test/AdminDesignConfigMediaGalleryImageUploadTest.xml b/app/code/Magento/Theme/Test/Mftf/Test/AdminDesignConfigMediaGalleryImageUploadTest.xml new file mode 100644 index 000000000000..f46328ac151b --- /dev/null +++ b/app/code/Magento/Theme/Test/Mftf/Test/AdminDesignConfigMediaGalleryImageUploadTest.xml @@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminDesignConfigMediaGalleryImageUploadTest"> + <annotations> + <features value="Content"/> + <stories value="Content"/> + <title value="MC-5784: Image fields using imageUploader UIComponent cannot use gallery image"/> + <description value="Admin should be able to use Image Uploader to add Gallery Images"/> + <severity value="MAJOR"/> + <testCaseId value="MC-13832"/> + <group value="Content"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminArea"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + <!--Edit Store View--> + <comment userInput="Edit Store View" stepKey="editStoreViewComment"/> + <amOnPage url="{{DesignConfigPage.url}}" stepKey="navigateToDesignConfigPage" /> + <waitForPageLoad stepKey="waitForPageload1"/> + <click selector="{{AdminDesignConfigSection.scopeRow('3')}}" stepKey="editStoreView"/> + <waitForPageLoad stepKey="waitForPageload2"/> + <scrollTo selector="{{AdminDesignConfigSection.htmlHeaderSection}}" stepKey="scrollToHtmlHeadSection"/> + <click selector="{{AdminDesignConfigSection.htmlHeaderSection}}" stepKey="openHtmlHeadSection"/> + <!--Upload Image--> + <comment userInput="Upload Image" stepKey="uploadImageComment"/> + <click selector="{{AdminDesignConfigSection.selectFromGalleryByFieldsetName('Head')}}" stepKey="openMediaGallery"/> + <actionGroup ref="VerifyMediaGalleryStorageActions" stepKey="verifyMediaGalleryStorageBtn"/> + <actionGroup ref="NavigateToMediaFolderActionGroup" stepKey="navigateToFolder"> + <argument name="FolderName" value="Storage Root"/> + </actionGroup> + <actionGroup ref="attachImage" stepKey="selectImageFromMediaStorage"> + <argument name="Image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="saveImage" stepKey="insertImage"/> + <click selector="{{AdminDesignConfigSection.saveConfiguration}}" stepKey="saveConfiguration"/> + <waitForElementVisible selector="{{AdminDesignConfigSection.successNotification}}" stepKey="waitForSuccessNotification"/> + <waitForPageLoad stepKey="waitForPageloadSuccess"/> + <!--Edit Store View--> + <comment userInput="Edit Store View" stepKey="editStoreViewComment2"/> + <click selector="{{AdminDesignConfigSection.scopeRow('3')}}" stepKey="editStoreView2"/> + <waitForPageLoad stepKey="waitForPageload3"/> + <scrollTo selector="{{AdminDesignConfigSection.htmlHeaderSection}}" stepKey="scrollToHtmlHeadSection2"/> + <click selector="{{AdminDesignConfigSection.htmlHeaderSection}}" stepKey="openHtmlHeadSection2"/> + <!--Save Default Configuration--> + <comment userInput="Save Default Configuration" stepKey="saveDefaultConfigurationComment"/> + <click selector="{{AdminDesignConfigSection.useDefaultByFieldsetName('Head')}}" stepKey="clickUseDefault"/> + <waitForElementVisible selector="{{AdminDesignConfigSection.saveConfiguration}}" stepKey="waitForWrapperToClose2"/> + <click selector="{{AdminDesignConfigSection.saveConfiguration}}" stepKey="saveConfiguration2"/> + <waitForElementVisible selector="{{AdminDesignConfigSection.successNotification}}" stepKey="waitForSuccessNotification2"/> + <waitForPageLoad stepKey="waitForPageloadSuccess2"/> + <!--Delete Image: will be in both root and favicon--> + <comment userInput="Delete Image" stepKey="deleteImageComment"/> + <actionGroup ref="navigateToMediaGallery" stepKey="navigateToMediaGallery"/> + <actionGroup ref="NavigateToMediaFolderActionGroup" stepKey="navigateToFolder2"> + <argument name="FolderName" value="Storage Root"/> + </actionGroup> + <actionGroup ref="DeleteImageFromStorageActionGroup" stepKey="deleteImageFromStorage"> + <argument name="Image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="NavigateToFaviconMediaFolderActionGroup" stepKey="navigateToFolder3"> + <argument name="StoreFolder" value="{{AdminDesignConfigSection.storeLink}}"/> + </actionGroup> + <actionGroup ref="DeleteImageFromStorageActionGroup" stepKey="deleteImageFromStorage2"> + <argument name="Image" value="ImageUpload3"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Theme/Test/Mftf/Test/AdminWatermarkUploadTest.xml b/app/code/Magento/Theme/Test/Mftf/Test/AdminWatermarkUploadTest.xml new file mode 100644 index 000000000000..a667f40ad327 --- /dev/null +++ b/app/code/Magento/Theme/Test/Mftf/Test/AdminWatermarkUploadTest.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminWatermarkUploadTest"> + <annotations> + <features value="Watermark"/> + <stories value="Watermark"/> + <title value="MAGETWO-95934: Can't upload Watermark Image"/> + <description value="Watermark images should be able to be uploaded in the admin"/> + <severity value="MAJOR"/> + <testCaseId value="MC-5796"/> + <group value="Watermark"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminArea"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logoutOfAdmin"/> + </after> + <amOnPage url="{{DesignConfigPage.url}}" stepKey="navigateToDesignConfigPage" /> + <waitForPageLoad stepKey="waitForPageload1"/> + <click selector="{{AdminDesignConfigSection.scopeRow('3')}}" stepKey="editStoreView"/> + <waitForPageLoad stepKey="waitForPageload2"/> + <scrollTo selector="{{AdminDesignConfigSection.watermarkSectionHeader}}" stepKey="scrollToWatermarkSection"/> + <click selector="{{AdminDesignConfigSection.watermarkSectionHeader}}" stepKey="openWatermarkSection"/> + + <waitForElement selector="{{AdminDesignConfigSection.imageUploadInputByFieldsetName('Base')}}" stepKey="waitForInputVisible1"/> + <attachFile selector="{{AdminDesignConfigSection.imageUploadInputByFieldsetName('Base')}}" userInput="adobe-base.jpg" stepKey="attachFile1"/> + <waitForElementVisible selector="{{AdminDesignConfigSection.imageUploadPreviewByFieldsetName('Base')}}" stepKey="waitForPreviewImage"/> + + <waitForElement selector="{{AdminDesignConfigSection.imageUploadInputByFieldsetName('Thumbnail')}}" stepKey="waitForInputVisible2"/> + <attachFile selector="{{AdminDesignConfigSection.imageUploadInputByFieldsetName('Thumbnail')}}" userInput="adobe-thumb.jpg" stepKey="attachFile2"/> + <waitForElementVisible selector="{{AdminDesignConfigSection.imageUploadPreviewByFieldsetName('Thumbnail')}}" stepKey="waitForPreviewImage2"/> + + <waitForElement selector="{{AdminDesignConfigSection.imageUploadInputByFieldsetName('Small')}}" stepKey="waitForInputVisible3"/> + <attachFile selector="{{AdminDesignConfigSection.imageUploadInputByFieldsetName('Small')}}" userInput="adobe-small.jpg" stepKey="attachFile3"/> + <waitForElementVisible selector="{{AdminDesignConfigSection.imageUploadPreviewByFieldsetName('Small')}}" stepKey="waitForPreviewImage3"/> + </test> +</tests> diff --git a/app/code/Magento/Theme/Test/Unit/Block/Html/TopmenuTest.php b/app/code/Magento/Theme/Test/Unit/Block/Html/TopmenuTest.php index 91c3ce47fc8b..023c74149275 100644 --- a/app/code/Magento/Theme/Test/Unit/Block/Html/TopmenuTest.php +++ b/app/code/Magento/Theme/Test/Unit/Block/Html/TopmenuTest.php @@ -189,7 +189,6 @@ public function testGetCacheKeyInfo() $treeFactory = $this->createMock(\Magento\Framework\Data\TreeFactory::class); $topmenu = new Topmenu($this->context, $nodeFactory, $treeFactory); - $this->urlBuilder->expects($this->once())->method('getUrl')->with('*/*/*')->willReturn('123'); $this->urlBuilder->expects($this->once())->method('getBaseUrl')->willReturn('baseUrl'); $store = $this->getMockBuilder(\Magento\Store\Model\Store::class) ->disableOriginalConstructor() @@ -199,7 +198,7 @@ public function testGetCacheKeyInfo() $this->storeManager->expects($this->once())->method('getStore')->willReturn($store); $this->assertEquals( - ['BLOCK_TPL', '321', null, 'base_url' => 'baseUrl', 'template' => null, '123'], + ['BLOCK_TPL', '321', null, 'base_url' => 'baseUrl', 'template' => null], $topmenu->getCacheKeyInfo() ); } diff --git a/app/code/Magento/Theme/Test/Unit/Model/PageLayout/Config/BuilderTest.php b/app/code/Magento/Theme/Test/Unit/Model/PageLayout/Config/BuilderTest.php index e5d69cbc820a..8429be84cae4 100644 --- a/app/code/Magento/Theme/Test/Unit/Model/PageLayout/Config/BuilderTest.php +++ b/app/code/Magento/Theme/Test/Unit/Model/PageLayout/Config/BuilderTest.php @@ -83,7 +83,7 @@ public function testGetPageLayoutsConfig() ->disableOriginalConstructor() ->getMock(); - $this->themeCollection->expects($this->any()) + $this->themeCollection->expects($this->once()) ->method('loadRegisteredThemes') ->willReturn([$theme1, $theme2]); diff --git a/app/code/Magento/Theme/Test/Unit/Model/Theme/Source/ThemeTest.php b/app/code/Magento/Theme/Test/Unit/Model/Theme/Source/ThemeTest.php index 5cb265d3d628..c06e2626034a 100644 --- a/app/code/Magento/Theme/Test/Unit/Model/Theme/Source/ThemeTest.php +++ b/app/code/Magento/Theme/Test/Unit/Model/Theme/Source/ThemeTest.php @@ -10,7 +10,6 @@ class ThemeTest extends \PHPUnit\Framework\TestCase { /** - * @true * @return void * @covers \Magento\Theme\Model\Theme\Source\Theme::__construct * @covers \Magento\Theme\Model\Theme\Source\Theme::getAllOptions diff --git a/app/code/Magento/Theme/Test/Unit/Model/Wysiwyg/StorageTest.php b/app/code/Magento/Theme/Test/Unit/Model/Wysiwyg/StorageTest.php index 0951f7b50f28..ddd6ebc43ce8 100644 --- a/app/code/Magento/Theme/Test/Unit/Model/Wysiwyg/StorageTest.php +++ b/app/code/Magento/Theme/Test/Unit/Model/Wysiwyg/StorageTest.php @@ -152,8 +152,7 @@ public function testUploadFile() $this->_helperStorage->expects($this->any())->method('getSession')->will($this->returnValue($session)); $expectedResult = [ - 'not_empty', - 'cookie' => ['name' => null, 'value' => null, 'lifetime' => null, 'path' => null, 'domain' => null], + 'not_empty' ]; $this->assertEquals($expectedResult, $this->_storageModel->uploadFile($this->_storageRoot)); diff --git a/app/code/Magento/Theme/etc/config.xml b/app/code/Magento/Theme/etc/config.xml index a6984b449d94..37841789c0e1 100644 --- a/app/code/Magento/Theme/etc/config.xml +++ b/app/code/Magento/Theme/etc/config.xml @@ -46,6 +46,7 @@ Disallow: /*SID= </header> <footer translate="copyright"> <copyright>Copyright © 2013-present Magento, Inc. All rights reserved.</copyright> + <report_bugs>1</report_bugs> </footer> </design> <theme> @@ -65,6 +66,11 @@ Disallow: /*SID= <static> <sign>1</sign> </static> + <dev> + <js> + <move_inline_to_bottom>0</move_inline_to_bottom> + </js> + </dev> </dev> </default> </config> diff --git a/app/code/Magento/Theme/etc/db_schema.xml b/app/code/Magento/Theme/etc/db_schema.xml index ee1185e6e576..7f3a3fc60794 100644 --- a/app/code/Magento/Theme/etc/db_schema.xml +++ b/app/code/Magento/Theme/etc/db_schema.xml @@ -20,7 +20,7 @@ <column xsi:type="smallint" name="type" padding="6" unsigned="false" nullable="false" identity="false" comment="Theme type: 0:physical, 1:virtual, 2:staging"/> <column xsi:type="text" name="code" nullable="true" comment="Full theme code, including package"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="theme_id"/> </constraint> </table> @@ -35,10 +35,10 @@ <column xsi:type="smallint" name="sort_order" padding="6" unsigned="false" nullable="false" identity="false" default="0" comment="Sort Order"/> <column xsi:type="boolean" name="is_temporary" nullable="false" default="false" comment="Is Temporary File"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="theme_files_id"/> </constraint> - <constraint xsi:type="foreign" name="THEME_FILE_THEME_ID_THEME_THEME_ID" table="theme_file" column="theme_id" + <constraint xsi:type="foreign" referenceId="THEME_FILE_THEME_ID_THEME_THEME_ID" table="theme_file" column="theme_id" referenceTable="theme" referenceColumn="theme_id" onDelete="CASCADE"/> </table> <table name="design_change" resource="default" engine="innodb" comment="Design Changes"> @@ -49,12 +49,12 @@ <column xsi:type="varchar" name="design" nullable="true" length="255" comment="Design"/> <column xsi:type="date" name="date_from" comment="First Date of Design Activity"/> <column xsi:type="date" name="date_to" comment="Last Date of Design Activity"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="design_change_id"/> </constraint> - <constraint xsi:type="foreign" name="DESIGN_CHANGE_STORE_ID_STORE_STORE_ID" table="design_change" + <constraint xsi:type="foreign" referenceId="DESIGN_CHANGE_STORE_ID_STORE_STORE_ID" table="design_change" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <index name="DESIGN_CHANGE_STORE_ID" indexType="btree"> + <index referenceId="DESIGN_CHANGE_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> diff --git a/app/code/Magento/Theme/etc/di.xml b/app/code/Magento/Theme/etc/di.xml index 5ff82ce2db6f..62f51e74b600 100644 --- a/app/code/Magento/Theme/etc/di.xml +++ b/app/code/Magento/Theme/etc/di.xml @@ -214,6 +214,10 @@ <item name="path" xsi:type="string">design/footer/absolute_footer</item> <item name="fieldset" xsi:type="string">other_settings/footer</item> </item> + <item name="footer_report_bugs" xsi:type="array"> + <item name="path" xsi:type="string">design/footer/report_bugs</item> + <item name="fieldset" xsi:type="string">other_settings/footer</item> + </item> <item name="default_robots" xsi:type="array"> <item name="path" xsi:type="string">design/search_engine_robots/default_robots</item> <item name="fieldset" xsi:type="string">other_settings/search_engine_robots</item> @@ -269,7 +273,7 @@ <type name="Magento\Config\App\Config\Source\DumpConfigSourceAggregated"> <plugin name="designConfigTheme" type="Magento\Theme\Model\Design\Config\Plugin\Dump" sortOrder="50"/> </type> - <type name="\Magento\Theme\Model\Design\Config\Plugin\Dump"> + <type name="Magento\Theme\Model\Design\Config\Plugin\Dump"> <arguments> <argument name="themeList" xsi:type="object">Magento\Theme\Model\ResourceModel\Theme\Collection</argument> </arguments> diff --git a/app/code/Magento/Theme/etc/frontend/di.xml b/app/code/Magento/Theme/etc/frontend/di.xml index 7db2783cd8df..3837c6f717b5 100644 --- a/app/code/Magento/Theme/etc/frontend/di.xml +++ b/app/code/Magento/Theme/etc/frontend/di.xml @@ -26,4 +26,7 @@ <type name="Magento\Framework\Controller\ResultInterface"> <plugin name="result-messages" type="Magento\Theme\Controller\Result\MessagePlugin"/> </type> + <type name="Magento\Framework\App\Response\Http"> + <plugin name="result-js-footer" type="Magento\Theme\Controller\Result\JsFooterPlugin"/> + </type> </config> diff --git a/app/code/Magento/Theme/etc/system.xml b/app/code/Magento/Theme/etc/system.xml new file mode 100644 index 000000000000..4abc87e84512 --- /dev/null +++ b/app/code/Magento/Theme/etc/system.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:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="dev" translate="label" type="text" sortOrder="920" showInDefault="1" showInWebsite="1" showInStore="1"> + <group id="js" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="1"> + <field id="move_inline_to_bottom" translate="label" type="select" sortOrder="25" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <label>Move JS code to the bottom of the page</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + </field> + </group> + </section> + </system> +</config> diff --git a/app/code/Magento/Theme/i18n/en_US.csv b/app/code/Magento/Theme/i18n/en_US.csv index db641b5da1f7..c8c586f0bc68 100644 --- a/app/code/Magento/Theme/i18n/en_US.csv +++ b/app/code/Magento/Theme/i18n/en_US.csv @@ -188,3 +188,4 @@ Settings,Settings ID,ID View,View Action,Action +"Display Report Bugs Link","Display Report Bugs Link" diff --git a/app/code/Magento/Theme/view/adminhtml/layout/adminhtml_system_design_theme_block.xml b/app/code/Magento/Theme/view/adminhtml/layout/adminhtml_system_design_theme_block.xml index bb8fbd44629c..e19460d56a89 100644 --- a/app/code/Magento/Theme/view/adminhtml/layout/adminhtml_system_design_theme_block.xml +++ b/app/code/Magento/Theme/view/adminhtml/layout/adminhtml_system_design_theme_block.xml @@ -11,7 +11,7 @@ <block class="Magento\Backend\Block\Widget\Grid" name="theme.grid" as="grid"> <arguments> <argument name="id" xsi:type="string">theme_grid</argument> - <argument name="dataSource" xsi:type="object">Magento\Theme\Model\ResourceModel\Theme\Grid\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Theme\Model\ResourceModel\Theme\Grid\Collection</argument> <argument name="save_parameters_in_session" xsi:type="string">1</argument> <argument name="use_ajax" xsi:type="string">1</argument> <argument name="grid_url" xsi:type="url" path="*/*/grid"> diff --git a/app/code/Magento/Theme/view/adminhtml/ui_component/design_config_form.xml b/app/code/Magento/Theme/view/adminhtml/ui_component/design_config_form.xml index 48adca3b1a12..bc1f36222dd6 100644 --- a/app/code/Magento/Theme/view/adminhtml/ui_component/design_config_form.xml +++ b/app/code/Magento/Theme/view/adminhtml/ui_component/design_config_form.xml @@ -54,7 +54,7 @@ <collapsible>true</collapsible> <label translate="true">HTML Head</label> </settings> - <field name="head_shortcut_icon" formElement="fileUploader"> + <field name="head_shortcut_icon" formElement="imageUploader"> <settings> <notice translate="true">Not all browsers support all these formats!</notice> <label translate="true">Favicon Icon</label> @@ -151,7 +151,7 @@ <collapsible>true</collapsible> <label translate="true">Header</label> </settings> - <field name="header_logo_src" formElement="fileUploader"> + <field name="header_logo_src" formElement="imageUploader"> <settings> <label translate="true">Logo Image</label> <componentType>imageUploader</componentType> @@ -233,6 +233,20 @@ <dataScope>footer_copyright</dataScope> </settings> </field> + <field name="footer_report_bugs" formElement="select"> + <settings> + <dataType>text</dataType> + <label translate="true">Display Report Bugs Link</label> + <dataScope>footer_report_bugs</dataScope> + </settings> + <formElements> + <select> + <settings> + <options class="Magento\Config\Model\Config\Source\Yesno"/> + </settings> + </select> + </formElements> + </field> </fieldset> <fieldset name="search_engine_robots" sortOrder="120"> <settings> diff --git a/app/code/Magento/Theme/view/base/requirejs-config.js b/app/code/Magento/Theme/view/base/requirejs-config.js index fdc08b9adf66..52e9270952a9 100644 --- a/app/code/Magento/Theme/view/base/requirejs-config.js +++ b/app/code/Magento/Theme/view/base/requirejs-config.js @@ -52,6 +52,9 @@ var config = { 'mixins': { 'jquery/jstree/jquery.jstree': { 'mage/backend/jstree-mixin': true + }, + 'jquery': { + 'jquery/patches/jquery': true } }, 'text': { diff --git a/app/code/Magento/Theme/view/frontend/layout/default.xml b/app/code/Magento/Theme/view/frontend/layout/default.xml index 716341f5a64a..f19f20861dcf 100644 --- a/app/code/Magento/Theme/view/frontend/layout/default.xml +++ b/app/code/Magento/Theme/view/frontend/layout/default.xml @@ -39,7 +39,11 @@ <argument name="label" translate="true" xsi:type="string">Skip to Content</argument> </arguments> </block> - <block class="Magento\Store\Block\Switcher" name="store_language" as="store_language" template="Magento_Store::switch/languages.phtml"/> + <block class="Magento\Store\Block\Switcher" name="store_language" as="store_language" template="Magento_Store::switch/languages.phtml"> + <arguments> + <argument name="view_model" xsi:type="object">Magento\Store\ViewModel\SwitcherUrlProvider</argument> + </arguments> + </block> <block class="Magento\Customer\Block\Account\Navigation" name="top.links"> <arguments> <argument name="css_class" xsi:type="string">header links</argument> @@ -82,6 +86,7 @@ <block class="Magento\Store\Block\Switcher" name="store.settings.language" template="Magento_Store::switch/languages.phtml"> <arguments> <argument name="id_modifier" xsi:type="string">nav</argument> + <argument name="view_model" xsi:type="object">Magento\Store\ViewModel\SwitcherUrlProvider</argument> </arguments> </block> <block class="Magento\Directory\Block\Currency" name="store.settings.currency" template="Magento_Directory::currency.phtml"> @@ -119,7 +124,7 @@ </arguments> </block> <block class="Magento\Theme\Block\Html\Footer" name="copyright" template="Magento_Theme::html/copyright.phtml"/> - <block class="Magento\Framework\View\Element\Template" name="report.bugs" template="Magento_Theme::html/bugreport.phtml" /> + <block class="Magento\Framework\View\Element\Template" name="report.bugs" template="Magento_Theme::html/bugreport.phtml" ifconfig="design/footer/report_bugs"/> </container> </referenceContainer> <referenceContainer name="before.body.end"> diff --git a/app/code/Magento/Theme/view/frontend/requirejs-config.js b/app/code/Magento/Theme/view/frontend/requirejs-config.js index bf38d3cbaae0..ef46f4bfed82 100644 --- a/app/code/Magento/Theme/view/frontend/requirejs-config.js +++ b/app/code/Magento/Theme/view/frontend/requirejs-config.js @@ -44,6 +44,9 @@ var config = { mixins: { 'Magento_Theme/js/view/breadcrumbs': { 'Magento_Theme/js/view/add-home-breadcrumb': true + }, + 'jquery/jquery-ui': { + 'jquery/patches/jquery-ui': true } } } diff --git a/app/code/Magento/Theme/view/frontend/templates/html/header.phtml b/app/code/Magento/Theme/view/frontend/templates/html/header.phtml index 4395cab651de..1103ae28741c 100644 --- a/app/code/Magento/Theme/view/frontend/templates/html/header.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/html/header.phtml @@ -15,11 +15,11 @@ $welcomeMessage = $block->getWelcome(); case 'welcome': ?> <li class="greet welcome" data-bind="scope: 'customer'"> <!-- ko if: customer().fullname --> - <span data-bind="text: new String('<?= $block->escapeHtml(__('Welcome, %1!', '%1')) ?>').replace('%1', customer().fullname)"> + <span class="logged-in" data-bind="text: new String('<?= $block->escapeHtml(__('Welcome, %1!', '%1')) ?>').replace('%1', customer().fullname)"> </span> <!-- /ko --> <!-- ko ifnot: customer().fullname --> - <span data-bind='html:"<?= $block->escapeHtml($welcomeMessage) ?>"'></span> + <span class="not-logged-in" data-bind='html:"<?= $block->escapeHtml($welcomeMessage) ?>"'></span> <?= $block->getBlockHtml('header.additional') ?> <!-- /ko --> </li> diff --git a/app/code/Magento/Theme/view/frontend/templates/html/header/logo.phtml b/app/code/Magento/Theme/view/frontend/templates/html/header/logo.phtml index 17f8d7c70f57..9c34dfea3218 100644 --- a/app/code/Magento/Theme/view/frontend/templates/html/header/logo.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/html/header/logo.phtml @@ -12,19 +12,15 @@ ?> <?php $storeName = $block->getThemeName() ? $block->getThemeName() : $block->getLogoAlt();?> <span data-action="toggle-nav" class="action nav-toggle"><span><?= /* @escapeNotVerified */ __('Toggle Nav') ?></span></span> -<?php if ($block->isHomePage()):?> - <strong class="logo"> -<?php else: ?> - <a class="logo" href="<?= $block->getUrl('') ?>" title="<?= /* @escapeNotVerified */ $storeName ?>"> -<?php endif ?> - <img src="<?= /* @escapeNotVerified */ $block->getLogoSrc() ?>" - title="<?= /* @escapeNotVerified */ $block->getLogoAlt() ?>" - alt="<?= /* @escapeNotVerified */ $block->getLogoAlt() ?>" - <?= $block->getLogoWidth() ? 'width="' . $block->getLogoWidth() . '"' : '' ?> - <?= $block->getLogoHeight() ? 'height="' . $block->getLogoHeight() . '"' : '' ?> - /> -<?php if ($block->isHomePage()):?> - </strong> -<?php else:?> - </a> -<?php endif?> +<a + class="logo" + href="<?= $block->getUrl('') ?>" + title="<?= /* @escapeNotVerified */ $storeName ?>" + aria-label="store logo"> + <img src="<?= /* @escapeNotVerified */ $block->getLogoSrc() ?>" + title="<?= $block->escapeHtmlAttr($block->getLogoAlt()) ?>" + alt="<?= $block->escapeHtmlAttr($block->getLogoAlt()) ?>" + <?= $block->getLogoWidth() ? 'width="' . $block->getLogoWidth() . '"' : '' ?> + <?= $block->getLogoHeight() ? 'height="' . $block->getLogoHeight() . '"' : '' ?> + /> +</a> diff --git a/app/code/Magento/Theme/view/frontend/templates/text.phtml b/app/code/Magento/Theme/view/frontend/templates/text.phtml index 4c4b38132c88..7d00235c0362 100644 --- a/app/code/Magento/Theme/view/frontend/templates/text.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/text.phtml @@ -10,6 +10,6 @@ if (!empty($attr)) { foreach ($block->getAttributes() as $attribute => $value) { $attributes .= ' ' . $attribute . '="' . $value . '"'; } -}; +} echo '<' . $block->getTag() . $attributes . '>' . $block->getText() . '</' . $block->getTag() . '>'; diff --git a/app/code/Magento/ThemeGraphQl/README.md b/app/code/Magento/ThemeGraphQl/README.md new file mode 100644 index 000000000000..2b3d54d9b573 --- /dev/null +++ b/app/code/Magento/ThemeGraphQl/README.md @@ -0,0 +1,4 @@ +# ThemeGraphQl + +**ThemeGraphQl** provides type information for the GraphQl module +to generate theme fields information endpoints. diff --git a/app/code/Magento/ThemeGraphQl/composer.json b/app/code/Magento/ThemeGraphQl/composer.json new file mode 100644 index 000000000000..e3aac55aea37 --- /dev/null +++ b/app/code/Magento/ThemeGraphQl/composer.json @@ -0,0 +1,24 @@ +{ + "name": "magento/module-theme-graph-ql", + "description": "N/A", + "type": "magento2-module", + "require": { + "php": "~7.1.3||~7.2.0", + "magento/framework": "*" + }, + "suggest": { + "magento/module-store-graph-ql": "*" + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\ThemeGraphQl\\": "" + } + } +} diff --git a/app/code/Magento/ThemeGraphQl/etc/graphql/di.xml b/app/code/Magento/ThemeGraphQl/etc/graphql/di.xml new file mode 100644 index 000000000000..9f55e522bf5a --- /dev/null +++ b/app/code/Magento/ThemeGraphQl/etc/graphql/di.xml @@ -0,0 +1,30 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\StoreGraphQl\Model\Resolver\Store\StoreConfigDataProvider"> + <arguments> + <argument name="extendedConfigData" xsi:type="array"> + <item name="head_shortcut_icon" xsi:type="string">design/head/shortcut_icon</item> + <item name="default_title" xsi:type="string">design/head/default_title</item> + <item name="title_prefix" xsi:type="string">design/head/title_prefix</item> + <item name="title_suffix" xsi:type="string">design/head/title_suffix</item> + <item name="default_description" xsi:type="string">design/head/default_description</item> + <item name="default_keywords" xsi:type="string">design/head/default_keywords</item> + <item name="head_includes" xsi:type="string">design/head/includes</item> + <item name="demonotice" xsi:type="string">design/head/demonotice</item> + <item name="header_logo_src" xsi:type="string">design/header/logo_src</item> + <item name="logo_width" xsi:type="string">design/header/logo_width</item> + <item name="logo_height" xsi:type="string">design/header/logo_height</item> + <item name="logo_alt" xsi:type="string">design/header/logo_alt</item> + <item name="welcome" xsi:type="string">design/header/welcome</item> + <item name="absolute_footer" xsi:type="string">design/footer/absolute_footer</item> + <item name="copyright" xsi:type="string">design/footer/copyright</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/ThemeGraphQl/etc/module.xml b/app/code/Magento/ThemeGraphQl/etc/module.xml new file mode 100644 index 000000000000..0e10b776af9c --- /dev/null +++ b/app/code/Magento/ThemeGraphQl/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_ThemeGraphQl"/> +</config> diff --git a/app/code/Magento/ThemeGraphQl/etc/schema.graphqls b/app/code/Magento/ThemeGraphQl/etc/schema.graphqls new file mode 100644 index 000000000000..325fcc8bd983 --- /dev/null +++ b/app/code/Magento/ThemeGraphQl/etc/schema.graphqls @@ -0,0 +1,19 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. +type StoreConfig @doc(description: "The type contains information about a store config") { + head_shortcut_icon : String @doc(description: "Favicon Icon") + default_title : String @doc(description: "Default Page Title") + title_prefix : String @doc(description: "Page Title Prefix") + title_suffix : String @doc(description: "Page Title Suffix") + default_description : String @doc(description: "Default Meta Description") + default_keywords : String @doc(description: "Default Meta Keywords") + head_includes : String @doc(description: "Scripts and Style Sheets") + demonotice : Int @doc(description: "Display Demo Store Notice") + header_logo_src : String @doc(description: "Logo Image") + logo_width : Int @doc(description: "Logo Attribute Width") + logo_height : Int @doc(description: "Logo Attribute Height") + welcome : String @doc(description: "Welcome Text") + logo_alt : String @doc(description: "Logo Image Alt") + absolute_footer : String @doc(description: "Footer Miscellaneous HTML") + copyright : String @doc(description: "Copyright") +} diff --git a/app/code/Magento/ThemeGraphQl/registration.php b/app/code/Magento/ThemeGraphQl/registration.php new file mode 100644 index 000000000000..e320fbc9868e --- /dev/null +++ b/app/code/Magento/ThemeGraphQl/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_ThemeGraphQl', __DIR__); diff --git a/app/code/Magento/Tinymce3/Model/Config/Gallery/Config.php b/app/code/Magento/Tinymce3/Model/Config/Gallery/Config.php new file mode 100644 index 000000000000..d11a3fa6e8a0 --- /dev/null +++ b/app/code/Magento/Tinymce3/Model/Config/Gallery/Config.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Tinymce3\Model\Config\Gallery; + +/** + * Class Config adds information about required configurations to display media gallery of tinymce3 editor + * + * @deprecated use \Magento\Cms\Model\Wysiwyg\DefaultConfigProvider instead + */ +class Config implements \Magento\Framework\Data\Wysiwyg\ConfigProviderInterface +{ + /** + * @var \Magento\Backend\Model\UrlInterface + */ + private $backendUrl; + + /** + * @param \Magento\Backend\Model\UrlInterface $backendUrl + */ + public function __construct( + \Magento\Backend\Model\UrlInterface $backendUrl + ) { + $this->backendUrl = $backendUrl; + } + + /** + * Returns media gallery config + * + * @param \Magento\Framework\DataObject $config + * @return \Magento\Framework\DataObject + */ + public function getConfig(\Magento\Framework\DataObject $config) : \Magento\Framework\DataObject + { + $config->addData( + [ + 'add_images' => true, + 'files_browser_window_url' => $this->backendUrl->getUrl('cms/wysiwyg_images/index'), + ] + ); + + return $config; + } +} diff --git a/app/code/Magento/Tinymce3/Test/Mftf/Section/AdminTinymce3FileldsSection.xml b/app/code/Magento/Tinymce3/Test/Mftf/Section/AdminTinymce3FileldsSection.xml index 1730996937ca..76c0a9e1fe79 100644 --- a/app/code/Magento/Tinymce3/Test/Mftf/Section/AdminTinymce3FileldsSection.xml +++ b/app/code/Magento/Tinymce3/Test/Mftf/Section/AdminTinymce3FileldsSection.xml @@ -13,6 +13,7 @@ </section> <section name="TinyMCESection"> <element name="TinyMCE3" type="text" selector="#cms_page_form_content_tbl"/> + <element name="InsertImageBtnTinyMCE3" type="button" selector="#cms_page_form_content_image"/> </section> <section name="NewsletterWYSIWYGSection"> <element name="TinyMCE3" type="text" selector="#cms_page_form_content_tbl"/> diff --git a/app/code/Magento/Tinymce3/Test/Mftf/Test/AdminSwitchWYSIWYGOptionsTest.xml b/app/code/Magento/Tinymce3/Test/Mftf/Test/AdminSwitchWYSIWYGOptionsTest.xml index ed3eea30c8b4..02b8d3686fd6 100644 --- a/app/code/Magento/Tinymce3/Test/Mftf/Test/AdminSwitchWYSIWYGOptionsTest.xml +++ b/app/code/Magento/Tinymce3/Test/Mftf/Test/AdminSwitchWYSIWYGOptionsTest.xml @@ -17,9 +17,6 @@ <description value="Admin should able to switch between versions of TinyMCE"/> <severity value="CRITICAL"/> <testCaseId value="MAGETWO-82936"/> - <skip> - <issueId value="MAGETWO-89417"/> - </skip> </annotations> <before> <actionGroup ref="LoginActionGroup" stepKey="loginGetFromGeneralFile"/> diff --git a/app/code/Magento/Tinymce3/composer.json b/app/code/Magento/Tinymce3/composer.json index 946bfe13f347..52e980052a87 100644 --- a/app/code/Magento/Tinymce3/composer.json +++ b/app/code/Magento/Tinymce3/composer.json @@ -4,9 +4,11 @@ "require": { "php": "~7.1.3||~7.2.0", "magento/framework": "*", + "magento/module-backend": "*", "magento/module-ui": "*", "magento/module-variable": "*", "magento/module-widget": "*" + }, "suggest": { "magento/module-cms": "*" diff --git a/app/code/Magento/Tinymce3/etc/adminhtml/di.xml b/app/code/Magento/Tinymce3/etc/adminhtml/di.xml index bf1ec7748294..53ab66c7ef21 100644 --- a/app/code/Magento/Tinymce3/etc/adminhtml/di.xml +++ b/app/code/Magento/Tinymce3/etc/adminhtml/di.xml @@ -22,7 +22,7 @@ <item name="Magento_Tinymce3/tinymce3Adapter" xsi:type="string">Magento\Tinymce3\Model\Config\Widget\Config</item> </argument> <argument name="galleryConfigProvider" xsi:type="array"> - <item name="Magento_Tinymce3/tinymce3Adapter" xsi:type="string">Magento\Cms\Model\WysiwygDefaultConfig</item> + <item name="Magento_Tinymce3/tinymce3Adapter" xsi:type="string">Magento\Tinymce3\Model\Config\Gallery\Config</item> </argument> </arguments> </type> diff --git a/app/code/Magento/Tinymce3/view/base/requirejs-config.js b/app/code/Magento/Tinymce3/view/base/requirejs-config.js index b9b43ab3896b..f96cf7d4f720 100644 --- a/app/code/Magento/Tinymce3/view/base/requirejs-config.js +++ b/app/code/Magento/Tinymce3/view/base/requirejs-config.js @@ -5,11 +5,13 @@ var config = { shim: { - 'tinymceDeprecated': { + 'Magento_Tinymce3/tiny_mce/tiny_mce_src': { 'exports': 'tinymce' } }, - paths: { - 'tinymceDeprecated': 'Magento_Tinymce3/tiny_mce/tiny_mce_src' + map: { + '*': { + 'tinymceDeprecated': 'Magento_Tinymce3/tiny_mce/tiny_mce_src' + } } }; 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 5f05d3f3015a..b7266779d3a3 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 @@ -1856,7 +1856,7 @@ } }; - // Applies formatting to the caret postion + // Applies formatting to the caret position function applyCaretFormat() { var rng, caretContainer, textNode, offset, bookmark, container, text; @@ -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/classes/html/Writer.js b/app/code/Magento/Tinymce3/view/base/web/tiny_mce/classes/html/Writer.js index 2abfac6478a0..f37607f73270 100644 --- a/app/code/Magento/Tinymce3/view/base/web/tiny_mce/classes/html/Writer.js +++ b/app/code/Magento/Tinymce3/view/base/web/tiny_mce/classes/html/Writer.js @@ -110,7 +110,7 @@ tinymce.html.Writer = function(settings) { * * @method text * @param {String} text String to write out. - * @param {Boolean} raw Optional raw state if true the contents wont get encoded. + * @param {Boolean} raw Optional raw state if true the contents won't get encoded. */ text: function(text, raw) { if (text.length > 0) diff --git a/app/code/Magento/Tinymce3/view/base/web/tiny_mce/plugins/example/editor_plugin_src.js b/app/code/Magento/Tinymce3/view/base/web/tiny_mce/plugins/example/editor_plugin_src.js index edc1e776e552..bd805e637027 100644 --- a/app/code/Magento/Tinymce3/view/base/web/tiny_mce/plugins/example/editor_plugin_src.js +++ b/app/code/Magento/Tinymce3/view/base/web/tiny_mce/plugins/example/editor_plugin_src.js @@ -49,7 +49,7 @@ }, /** - * Creates control instances based in the incomming name. This method is normally not + * Creates control instances based in the incoming name. This method is normally not * needed since the addButton method of the tinymce.Editor class is a more easy way of adding buttons * but you sometimes need to create more complex controls like listboxes, split buttons etc then this * method can be used to create those. 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 b3d77f6ce313..60dd358414be 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 @@ -15682,7 +15682,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } }; - // Applies formatting to the caret postion + // Applies formatting to the caret position function applyCaretFormat() { var rng, caretContainer, textNode, offset, bookmark, container, text; @@ -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 daf2ad4e71ba..ed0b7cb0e50a 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 @@ -16532,7 +16532,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } }; - // Applies formatting to the caret postion + // Applies formatting to the caret position function applyCaretFormat() { var rng, caretContainer, textNode, offset, bookmark, container, text; @@ -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 2634633d8eee..1c53062dd969 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 @@ -16506,7 +16506,7 @@ tinymce.create('tinymce.ui.Toolbar:tinymce.ui.Container', { } }; - // Applies formatting to the caret postion + // Applies formatting to the caret position function applyCaretFormat() { var rng, caretContainer, textNode, offset, bookmark, container, text; @@ -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/Tinymce3/view/base/web/tinymce3Adapter.js b/app/code/Magento/Tinymce3/view/base/web/tinymce3Adapter.js index 7c587c1c8768..bb3300baf988 100644 --- a/app/code/Magento/Tinymce3/view/base/web/tinymce3Adapter.js +++ b/app/code/Magento/Tinymce3/view/base/web/tinymce3Adapter.js @@ -37,7 +37,15 @@ define([ this.config = config; this.schema = config.schema || html5Schema; - _.bindAll(this, 'beforeSetContent', 'saveContent', 'onChangeContent', 'openFileBrowser', 'updateTextArea'); + _.bindAll( + this, + 'beforeSetContent', + 'saveContent', + 'onChangeContent', + 'openFileBrowser', + 'updateTextArea', + 'removeEvents' + ); varienGlobalEvents.attachEventHandler('tinymceChange', this.onChangeContent); varienGlobalEvents.attachEventHandler('tinymceBeforeSetContent', this.beforeSetContent); @@ -72,6 +80,17 @@ define([ tinyMCE3.init(this.getSettings(mode)); }, + /** + * Remove events from instance. + * + * @param {String} wysiwygId + */ + removeEvents: function (wysiwygId) { + var editor = tinyMceEditors.get(wysiwygId); + + varienGlobalEvents.removeEventHandler('tinymceChange', editor.onChangeContent); + }, + /** * @param {*} mode * @return {Object} diff --git a/app/code/Magento/Translation/Block/Js.php b/app/code/Magento/Translation/Block/Js.php index 86bb416524d8..db26feb8067f 100644 --- a/app/code/Magento/Translation/Block/Js.php +++ b/app/code/Magento/Translation/Block/Js.php @@ -8,9 +8,10 @@ use Magento\Framework\View\Element\Template; use Magento\Translation\Model\Js\Config; -use Magento\Framework\Escaper; /** + * JS translation block + * * @api * @since 100.0.2 */ @@ -54,7 +55,7 @@ public function dictionaryEnabled() } /** - * gets current js-translation.json timestamp + * Gets current js-translation.json timestamp * * @return string */ @@ -64,6 +65,8 @@ public function getTranslationFileTimestamp() } /** + * Get translation file path + * * @return string */ public function getTranslationFilePath() diff --git a/app/code/Magento/Translation/Model/Json/PreProcessor.php b/app/code/Magento/Translation/Model/Json/PreProcessor.php index 5d46c3c8b061..c178a324cb40 100644 --- a/app/code/Magento/Translation/Model/Json/PreProcessor.php +++ b/app/code/Magento/Translation/Model/Json/PreProcessor.php @@ -6,6 +6,7 @@ namespace Magento\Translation\Model\Json; +use Magento\Framework\App\Area; use Magento\Framework\App\AreaList; use Magento\Framework\App\ObjectManager; use Magento\Framework\TranslateInterface; @@ -13,6 +14,7 @@ use Magento\Framework\View\Asset\PreProcessor\Chain; use Magento\Framework\View\Asset\PreProcessorInterface; use Magento\Framework\View\DesignInterface; +use Magento\Backend\App\Area\FrontNameResolver; use Magento\Translation\Model\Js\Config; use Magento\Translation\Model\Js\DataProviderInterface; @@ -83,7 +85,7 @@ public function process(Chain $chain) $context = $chain->getAsset()->getContext(); $themePath = '*/*'; - $areaCode = \Magento\Backend\App\Area\FrontNameResolver::AREA_CODE; + $areaCode = FrontNameResolver::AREA_CODE; if ($context instanceof FallbackContext) { $themePath = $context->getThemePath(); @@ -92,8 +94,10 @@ public function process(Chain $chain) $this->viewDesign->setDesignTheme($themePath, $areaCode); } - $area = $this->areaList->getArea($areaCode); - $area->load(\Magento\Framework\App\Area::PART_TRANSLATE); + if ($areaCode !== FrontNameResolver::AREA_CODE) { + $area = $this->areaList->getArea($areaCode); + $area->load(Area::PART_TRANSLATE); + } $this->translate->setLocale($context->getLocale())->loadData($areaCode, true); diff --git a/app/code/Magento/Translation/Test/Unit/Model/Json/PreProcessorTest.php b/app/code/Magento/Translation/Test/Unit/Model/Json/PreProcessorTest.php index d9340e03dc99..cbeeefed6be6 100644 --- a/app/code/Magento/Translation/Test/Unit/Model/Json/PreProcessorTest.php +++ b/app/code/Magento/Translation/Test/Unit/Model/Json/PreProcessorTest.php @@ -8,39 +8,43 @@ use Magento\Translation\Model\Js\Config; use Magento\Translation\Model\Js\DataProvider; use Magento\Translation\Model\Json\PreProcessor; +use Magento\Backend\App\Area\FrontNameResolver; class PreProcessorTest extends \PHPUnit\Framework\TestCase { /** * @var PreProcessor */ - protected $model; + private $model; /** * @var Config|\PHPUnit_Framework_MockObject_MockObject */ - protected $configMock; + private $configMock; /** * @var DataProvider|\PHPUnit_Framework_MockObject_MockObject */ - protected $dataProviderMock; + private $dataProviderMock; /** * @var \Magento\Framework\App\AreaList|\PHPUnit_Framework_MockObject_MockObject */ - protected $areaListMock; + private $areaListMock; /** * @var \Magento\Framework\TranslateInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $translateMock; + private $translateMock; /** * @var \Magento\Framework\View\DesignInterface|\PHPUnit_Framework_MockObject_MockObject */ private $designMock; + /** + * @inheritdoc + */ protected function setUp() { $this->configMock = $this->createMock(\Magento\Translation\Model\Js\Config::class); @@ -57,7 +61,14 @@ protected function setUp() ); } - public function testGetData() + /** + * Test 'process' method. + * + * @param array $data + * @param array $expects + * @dataProvider processDataProvider + */ + public function testProcess(array $data, array $expects) { $chain = $this->createMock(\Magento\Framework\View\Asset\PreProcessor\Chain::class); $asset = $this->createMock(\Magento\Framework\View\Asset\File::class); @@ -66,8 +77,10 @@ public function testGetData() $targetPath = 'path/js-translation.json'; $themePath = '*/*'; $dictionary = ['hello' => 'bonjour']; - $areaCode = 'adminhtml'; + $areaCode = $data['area_code']; + $area = $this->createMock(\Magento\Framework\App\Area::class); + $area->expects($expects['area_load'])->method('load')->willReturnSelf(); $chain->expects($this->once()) ->method('getTargetAssetPath') @@ -93,7 +106,7 @@ public function testGetData() $this->designMock->expects($this->once())->method('setDesignTheme')->with($themePath, $areaCode); - $this->areaListMock->expects($this->once()) + $this->areaListMock->expects($expects['areaList_getArea']) ->method('getArea') ->with($areaCode) ->willReturn($area); @@ -114,4 +127,33 @@ public function testGetData() $this->model->process($chain); } + + /** + * Data provider for 'process' method test. + * + * @return array + */ + public function processDataProvider() + { + return [ + [ + [ + 'area_code' => FrontNameResolver::AREA_CODE + ], + [ + 'areaList_getArea' => $this->never(), + 'area_load' => $this->never(), + ] + ], + [ + [ + 'area_code' => 'frontend' + ], + [ + 'areaList_getArea' => $this->once(), + 'area_load' => $this->once(), + ] + ], + ]; + } } diff --git a/app/code/Magento/Translation/etc/db_schema.xml b/app/code/Magento/Translation/etc/db_schema.xml index 523ef3a1279d..a0d08467acf0 100644 --- a/app/code/Magento/Translation/etc/db_schema.xml +++ b/app/code/Magento/Translation/etc/db_schema.xml @@ -18,12 +18,12 @@ <column xsi:type="varchar" name="locale" nullable="false" length="20" default="en_US" comment="Locale"/> <column xsi:type="bigint" name="crc_string" padding="20" unsigned="false" nullable="false" identity="false" default="1591228201" comment="Translation String CRC32 Hash"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="key_id"/> </constraint> - <constraint xsi:type="foreign" name="TRANSLATION_STORE_ID_STORE_STORE_ID" table="translation" column="store_id" + <constraint xsi:type="foreign" referenceId="TRANSLATION_STORE_ID_STORE_STORE_ID" table="translation" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="TRANSLATION_STORE_ID_LOCALE_CRC_STRING_STRING"> + <constraint xsi:type="unique" referenceId="TRANSLATION_STORE_ID_LOCALE_CRC_STRING_STRING"> <column name="store_id"/> <column name="locale"/> <column name="crc_string"/> diff --git a/app/code/Magento/Translation/view/base/templates/translate.phtml b/app/code/Magento/Translation/view/base/templates/translate.phtml index c8366037e229..ec88b1d09202 100644 --- a/app/code/Magento/Translation/view/base/templates/translate.phtml +++ b/app/code/Magento/Translation/view/base/templates/translate.phtml @@ -9,19 +9,50 @@ /** @var \Magento\Translation\Block\Js $block */ ?> <?php if ($block->dictionaryEnabled()): ?> + <script> + require.config({ + deps: [ + 'jquery', + 'mage/translate', + 'jquery/jquery-storageapi' + ], + callback: function ($) { + 'use strict'; + + var dependencies = [], + versionObj; + + $.initNamespaceStorage('mage-translation-storage'); + $.initNamespaceStorage('mage-translation-file-version'); + versionObj = $.localStorage.get('mage-translation-file-version'); + + <?php $version = $block->getTranslationFileVersion(); ?> + + if (versionObj.version !== '<?= /* @escapeNotVerified */ $block->escapeJsQuote($version) ?>') { + dependencies.push( + 'text!<?= /* @noEscape */ Magento\Translation\Model\Js\Config::DICTIONARY_FILE_NAME ?>' + ); -<?php - $version = $block->getTranslationFileVersion(); - $fileName = Magento\Translation\Model\Js\Config::DICTIONARY_FILE_NAME; -?> - <script type="text/x-magento-init"> - { - "*": { - "mage/translate-init": { - "dictionaryFile": "text!<?= $block->escapeJs($fileName); ?>", - "version": "<?= $block->escapeJs($version) ?>" } + + require.config({ + deps: dependencies, + callback: function (string) { + if (typeof string === 'string') { + $.mage.translate.add(JSON.parse(string)); + $.localStorage.set('mage-translation-storage', string); + $.localStorage.set( + 'mage-translation-file-version', + { + version: '<?= /* @escapeNotVerified */ $block->escapeJsQuote($version) ?>' + } + ); + } else { + $.mage.translate.add($.localStorage.get('mage-translation-storage')); + } + } + }); } - } + }); </script> <?php endif; ?> diff --git a/app/code/Magento/Translation/view/frontend/requirejs-config.js b/app/code/Magento/Translation/view/frontend/requirejs-config.js index 47ccaf5a33e5..b4b3ce0f8c55 100644 --- a/app/code/Magento/Translation/view/frontend/requirejs-config.js +++ b/app/code/Magento/Translation/view/frontend/requirejs-config.js @@ -7,7 +7,8 @@ var config = { map: { '*': { editTrigger: 'mage/edit-trigger', - addClass: 'Magento_Translation/add-class' + addClass: 'Magento_Translation/js/add-class', + 'Magento_Translation/add-class': 'Magento_Translation/js/add-class' } }, deps: [ diff --git a/app/code/Magento/Translation/view/frontend/web/add-class.js b/app/code/Magento/Translation/view/frontend/web/js/add-class.js similarity index 100% rename from app/code/Magento/Translation/view/frontend/web/add-class.js rename to app/code/Magento/Translation/view/frontend/web/js/add-class.js diff --git a/app/code/Magento/Ui/Component/Form.php b/app/code/Magento/Ui/Component/Form.php index 4033abba820e..dc6e7b5ca06a 100644 --- a/app/code/Magento/Ui/Component/Form.php +++ b/app/code/Magento/Ui/Component/Form.php @@ -10,6 +10,7 @@ use Magento\Framework\View\Element\UiComponentInterface; /** + * Ui component Form * @api * @since 100.0.2 */ @@ -53,14 +54,15 @@ public function getComponentName() } /** - * {@inheritdoc} + * @inheritdoc */ public function getDataSourceData() { $dataSource = []; $id = $this->getContext()->getRequestParam($this->getContext()->getDataProvider()->getRequestFieldName(), null); - $filter = $this->filterBuilder->setField($this->getContext()->getDataProvider()->getPrimaryFieldName()) + $idFieldName = $this->getContext()->getDataProvider()->getPrimaryFieldName(); + $filter = $this->filterBuilder->setField($idFieldName) ->setValue($id) ->create(); $this->getContext()->getDataProvider() @@ -74,7 +76,7 @@ public function getDataSourceData() ]; } elseif (isset($data['items'])) { foreach ($data['items'] as $item) { - if ($item[$item['id_field_name']] == $id) { + if ($item[$idFieldName] == $id) { $dataSource = ['data' => ['general' => $item]]; } } diff --git a/app/code/Magento/Ui/Component/Form/Element/DataType/Media.php b/app/code/Magento/Ui/Component/Form/Element/DataType/Media.php index c2460fd8385c..d4b1937bd06b 100644 --- a/app/code/Magento/Ui/Component/Form/Element/DataType/Media.php +++ b/app/code/Magento/Ui/Component/Form/Element/DataType/Media.php @@ -31,14 +31,16 @@ public function prepare() { if ($this->getData('config/uploaderConfig/url')) { $url = $this->getContext()->getUrl($this->getData('config/uploaderConfig/url'), ['_secure' => true]); + $updateConfig = [ + 'uploaderConfig' => ['url' => $url] + ]; + if (!isset($this->getConfiguration()['dataScope'])) { + $updateConfig['dataScope'] = $this->getName(); + } $data = array_replace_recursive( $this->getData(), [ - 'config' => [ - 'uploaderConfig' => [ - 'url' => $url - ], - ], + 'config' => $updateConfig, ] ); $this->setData($data); diff --git a/app/code/Magento/Ui/Component/Form/Element/Wysiwyg.php b/app/code/Magento/Ui/Component/Form/Element/Wysiwyg.php index c79dfb3f5cf8..d39d2dc3cd93 100644 --- a/app/code/Magento/Ui/Component/Form/Element/Wysiwyg.php +++ b/app/code/Magento/Ui/Component/Form/Element/Wysiwyg.php @@ -13,6 +13,8 @@ use Magento\Ui\Component\Wysiwyg\ConfigInterface; /** + * WYSIWYG form element + * * @api * @since 100.1.0 */ @@ -50,8 +52,9 @@ public function __construct( ) { $wysiwygConfigData = isset($config['wysiwygConfigData']) ? $config['wysiwygConfigData'] : []; $this->form = $formFactory->create(); + $wysiwygId = $context->getNamespace() . '_' . $data['name']; $this->editor = $this->form->addField( - $context->getNamespace() . '_' . $data['name'], + $wysiwygId, \Magento\Framework\Data\Form\Element\Editor::class, [ 'force_load' => true, @@ -62,6 +65,7 @@ public function __construct( ] ); $data['config']['content'] = $this->editor->getElementHtml(); + $data['config']['wysiwygId'] = $wysiwygId; parent::__construct($context, $components, $data); } diff --git a/app/code/Magento/Ui/Component/Form/Fieldset.php b/app/code/Magento/Ui/Component/Form/Fieldset.php index 745068ca0fa8..2525d0a4923c 100644 --- a/app/code/Magento/Ui/Component/Form/Fieldset.php +++ b/app/code/Magento/Ui/Component/Form/Fieldset.php @@ -5,13 +5,11 @@ */ namespace Magento\Ui\Component\Form; -use Magento\Ui\Component\Container; use Magento\Ui\Component\AbstractComponent; -use Magento\Framework\View\Element\UiComponentFactory; -use Magento\Framework\View\Element\UiComponentInterface; -use Magento\Framework\View\Element\UiComponent\ContextInterface; /** + * Fieldset UI Component. + * * @api * @since 100.0.2 */ diff --git a/app/code/Magento/Ui/Component/Layout/Tabs.php b/app/code/Magento/Ui/Component/Layout/Tabs.php index 8ceac716ae21..0d176d724165 100644 --- a/app/code/Magento/Ui/Component/Layout/Tabs.php +++ b/app/code/Magento/Ui/Component/Layout/Tabs.php @@ -5,18 +5,17 @@ */ namespace Magento\Ui\Component\Layout; -use Magento\Framework\View\Element\Template; use Magento\Framework\View\Element\UiComponent\BlockWrapperInterface; use Magento\Framework\View\Element\UiComponent\DataSourceInterface; -use Magento\Framework\View\Element\UiComponent\LayoutInterface; use Magento\Framework\View\Element\UiComponentFactory; use Magento\Framework\View\Element\UiComponentInterface; +use Magento\Framework\View\Element\ComponentVisibilityInterface; use Magento\Ui\Component\Layout\Tabs\TabInterface; /** * Class Tabs */ -class Tabs extends \Magento\Framework\View\Layout\Generic implements LayoutInterface +class Tabs extends \Magento\Framework\View\Layout\Generic { /** * @var string @@ -89,58 +88,15 @@ protected function addChildren(array &$topNode, UiComponentInterface $component, $this->addWrappedBlock($childComponent, $childrenAreas); continue; } + if ($childComponent instanceof ComponentVisibilityInterface && !$childComponent->isComponentVisible()) { + continue; + } $name = $childComponent->getName(); $config = $childComponent->getData('config'); $collectedComponents[$name] = true; - if (isset($config['is_collection']) && $config['is_collection'] === true) { - $label = $childComponent->getData('config/label'); - $this->component->getContext()->addComponentDefinition( - 'collection', - [ - 'component' => 'Magento_Ui/js/form/components/collection', - 'extends' => $this->namespace - ] - ); - - /** - * @var UiComponentInterface $childComponent - * @var array $structure - */ - list($childComponent, $structure) = $this->prepareChildComponents($childComponent, $name); - - $childrenStructure = $structure[$name]['children']; - - $structure[$name]['children'] = [ - $name . '_collection' => [ - 'type' => 'collection', - 'config' => [ - 'active' => 1, - 'removeLabel' => __('Remove %1', $label), - 'addLabel' => __('Add New %1', $label), - 'removeMessage' => $childComponent->getData('config/removeMessage'), - 'itemTemplate' => 'item_template', - ], - 'children' => [ - 'item_template' => ['type' => $this->namespace, - 'isTemplate' => true, - 'component' => 'Magento_Ui/js/form/components/collection/item', - 'childType' => 'group', - 'config' => [ - 'label' => __('New %1', $label), - ], - 'children' => $childrenStructure - ] - ] - ] - ]; - } else { - /** - * @var UiComponentInterface $childComponent - * @var array $structure - */ - list($childComponent, $structure) = $this->prepareChildComponents($childComponent, $name); - } + + [$childComponent, $structure] = $this->buildChildComponentStructure($config, $childComponent); $tabComponent = $this->createTabComponent($childComponent, $name); @@ -168,6 +124,67 @@ protected function addChildren(array &$topNode, UiComponentInterface $component, $topNode = $this->structure; } + /** + * Build child components structure of the tab + * + * @param array $config + * @param UiComponentInterface $childComponent + * @return array + */ + private function buildChildComponentStructure(array $config, $childComponent): array + { + $name = $childComponent->getName(); + if (isset($config['is_collection']) && $config['is_collection'] === true) { + $label = $childComponent->getData('config/label'); + $this->component->getContext()->addComponentDefinition( + 'collection', + [ + 'component' => 'Magento_Ui/js/form/components/collection', + 'extends' => $this->namespace + ] + ); + /** + * @var UiComponentInterface $childComponent + * @var array $structure + */ + [$childComponent, $structure] = $this->prepareChildComponents($childComponent, $name); + + $childrenStructure = $structure[$name]['children']; + + $structure[$name]['children'] = [ + $name . '_collection' => [ + 'type' => 'collection', + 'config' => [ + 'active' => 1, + 'removeLabel' => __('Remove %1', $label), + 'addLabel' => __('Add New %1', $label), + 'removeMessage' => $childComponent->getData('config/removeMessage'), + 'itemTemplate' => 'item_template', + ], + 'children' => [ + 'item_template' => ['type' => $this->namespace, + 'isTemplate' => true, + 'component' => 'Magento_Ui/js/form/components/collection/item', + 'childType' => 'group', + 'config' => [ + 'label' => __('New %1', $label), + ], + 'children' => $childrenStructure + ] + ] + ] + ]; + } else { + /** + * @var UiComponentInterface $childComponent + * @var array $structure + */ + [$childComponent, $structure] = $this->prepareChildComponents($childComponent, $name); + } + + return [$childComponent, $structure]; + } + /** * Add wrapped layout block * diff --git a/app/code/Magento/Ui/Component/Listing/Columns/Date.php b/app/code/Magento/Ui/Component/Listing/Columns/Date.php index 1c9916a94145..ad876b66b603 100644 --- a/app/code/Magento/Ui/Component/Listing/Columns/Date.php +++ b/app/code/Magento/Ui/Component/Listing/Columns/Date.php @@ -11,6 +11,8 @@ use Magento\Framework\Stdlib\DateTime\TimezoneInterface; /** + * Date format column + * * @api * @since 100.0.2 */ @@ -47,6 +49,27 @@ public function __construct( parent::__construct($context, $uiComponentFactory, $components, $data); } + /** + * @inheritdoc + */ + public function prepare() + { + $config = $this->getData('config'); + $config['filter'] = [ + 'filterType' => 'dateRange', + 'templates' => [ + 'date' => [ + 'options' => [ + 'dateFormat' => $this->timezone->getDateFormatWithLongYear() + ] + ] + ] + ]; + $this->setData('config', $config); + + parent::prepare(); + } + /** * @inheritdoc */ diff --git a/app/code/Magento/Ui/Component/MassAction.php b/app/code/Magento/Ui/Component/MassAction.php index ea39e19a65f5..4cca8d4c012b 100644 --- a/app/code/Magento/Ui/Component/MassAction.php +++ b/app/code/Magento/Ui/Component/MassAction.php @@ -6,6 +6,8 @@ namespace Magento\Ui\Component; /** + * Mass action UI component. + * * @api * @since 100.0.2 */ @@ -21,7 +23,12 @@ public function prepare() $config = $this->getConfiguration(); foreach ($this->getChildComponents() as $actionComponent) { - $config['actions'][] = $actionComponent->getConfiguration(); + $componentConfig = $actionComponent->getConfiguration(); + $disabledAction = $componentConfig['actionDisable'] ?? false; + if ($disabledAction) { + continue; + } + $config['actions'][] = $componentConfig; } $origConfig = $this->getConfiguration(); diff --git a/app/code/Magento/Ui/Component/MassAction/Filter.php b/app/code/Magento/Ui/Component/MassAction/Filter.php index a8ed5d901d86..c512c82d694b 100644 --- a/app/code/Magento/Ui/Component/MassAction/Filter.php +++ b/app/code/Magento/Ui/Component/MassAction/Filter.php @@ -7,14 +7,16 @@ namespace Magento\Ui\Component\MassAction; use Magento\Framework\Api\FilterBuilder; -use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\View\Element\UiComponentFactory; use Magento\Framework\App\RequestInterface; -use Magento\Framework\View\Element\UiComponentInterface; use Magento\Framework\Data\Collection\AbstractDb; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\View\Element\UiComponent\DataProvider\DataProviderInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Framework\View\Element\UiComponentInterface; /** + * Filter component. + * * @api * @since 100.0.2 */ @@ -100,9 +102,13 @@ public function getCollection(AbstractDb $collection) } } + $filterIds = $this->getFilterIds(); + if (\is_array($selected)) { + $filterIds = array_unique(array_merge($filterIds, $selected)); + } $collection->addFieldToFilter( $collection->getIdFieldName(), - ['in' => $this->getFilterIds()] + ['in' => $filterIds] ); return $collection; diff --git a/app/code/Magento/Ui/Component/Wysiwyg/Config.php b/app/code/Magento/Ui/Component/Wysiwyg/Config.php index 48014a0160c4..d88a25592787 100644 --- a/app/code/Magento/Ui/Component/Wysiwyg/Config.php +++ b/app/code/Magento/Ui/Component/Wysiwyg/Config.php @@ -13,7 +13,7 @@ class Config implements ConfigInterface /** * Return WYSIWYG configuration * - * @return \Magento\Framework\DataObject + * @return array */ public function getConfig() { diff --git a/app/code/Magento/Ui/Controller/Adminhtml/Bookmark/Save.php b/app/code/Magento/Ui/Controller/Adminhtml/Bookmark/Save.php index 60d374fbf26c..b45880c1ce72 100644 --- a/app/code/Magento/Ui/Controller/Adminhtml/Bookmark/Save.php +++ b/app/code/Magento/Ui/Controller/Adminhtml/Bookmark/Save.php @@ -5,6 +5,7 @@ */ namespace Magento\Ui\Controller\Adminhtml\Bookmark; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Authorization\Model\UserContextInterface; use Magento\Backend\App\Action\Context; use Magento\Framework\Json\DecoderInterface; @@ -20,7 +21,7 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Save extends AbstractAction +class Save extends AbstractAction implements HttpPostActionInterface { /** * Identifier for current bookmark diff --git a/app/code/Magento/Ui/Controller/Adminhtml/Index/Render.php b/app/code/Magento/Ui/Controller/Adminhtml/Index/Render.php index fb99cef8e53c..b06c655939b1 100644 --- a/app/code/Magento/Ui/Controller/Adminhtml/Index/Render.php +++ b/app/code/Magento/Ui/Controller/Adminhtml/Index/Render.php @@ -14,6 +14,11 @@ use Magento\Framework\Escaper; use Magento\Framework\Controller\Result\JsonFactory; +/** + * Render a component. + * + * @SuppressWarnings(PHPMD.AllPurposeAction) + */ class Render extends AbstractAction { /** @@ -81,6 +86,18 @@ public function execute() $contentType = $this->contentTypeResolver->resolve($component->getContext()); $this->getResponse()->setHeader('Content-Type', $contentType, true); + } else { + /** @var \Magento\Framework\Controller\Result\Json $resultJson */ + $resultJson = $this->resultJsonFactory->create(); + $resultJson->setStatusHeader( + \Zend\Http\Response::STATUS_CODE_403, + \Zend\Http\AbstractMessage::VERSION_11, + 'Forbidden' + ); + return $resultJson->setData([ + 'error' => $this->escaper->escapeHtml('Forbidden'), + 'errorcode' => 403 + ]); } } catch (\Magento\Framework\Exception\LocalizedException $e) { $this->logger->critical($e); diff --git a/app/code/Magento/Ui/Controller/Adminhtml/Index/Render/Handle.php b/app/code/Magento/Ui/Controller/Adminhtml/Index/Render/Handle.php index cc028a455456..6ea14448c4ae 100644 --- a/app/code/Magento/Ui/Controller/Adminhtml/Index/Render/Handle.php +++ b/app/code/Magento/Ui/Controller/Adminhtml/Index/Render/Handle.php @@ -5,16 +5,42 @@ */ namespace Magento\Ui\Controller\Adminhtml\Index\Render; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Framework\View\Element\Template; use Magento\Ui\Component\Control\ActionPool; use Magento\Ui\Component\Wrapper\UiComponent; use Magento\Ui\Controller\Adminhtml\AbstractAction; +use Magento\Backend\App\Action\Context; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Framework\View\Element\UiComponent\ContextFactory; +use Magento\Framework\App\ObjectManager; /** * Class Handle + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Handle extends AbstractAction +class Handle extends AbstractAction implements HttpGetActionInterface { + /** + * @var ContextFactory + */ + private $contextFactory; + + /** + * @param Context $context + * @param UiComponentFactory $factory + * @param ContextFactory|null $contextFactory + */ + public function __construct( + Context $context, + UiComponentFactory $factory, + ContextFactory $contextFactory = null + ) { + parent::__construct($context, $factory); + $this->contextFactory = $contextFactory + ?: ObjectManager::getInstance()->get(ContextFactory::class); + } + /** * Render UI component by namespace in handle context * @@ -22,20 +48,50 @@ class Handle extends AbstractAction */ public function execute() { + $response = ''; $handle = $this->_request->getParam('handle'); $namespace = $this->_request->getParam('namespace'); $buttons = $this->_request->getParam('buttons', false); - $this->_view->loadLayout(['default', $handle], true, true, false); + $layout = $this->_view->getLayout(); + $context = $this->contextFactory->create( + [ + 'namespace' => $namespace, + 'pageLayout' => $layout + ] + ); - $uiComponent = $this->_view->getLayout()->getBlock($namespace); - $response = $uiComponent instanceof UiComponent ? $uiComponent->toHtml() : ''; + $component = $this->factory->create($namespace, null, ['context' => $context]); + if ($this->validateAclResource($component->getContext()->getDataProvider()->getConfigData())) { + $uiComponent = $layout->getBlock($namespace); + $response = $uiComponent instanceof UiComponent ? $uiComponent->toHtml() : ''; + } if ($buttons) { - $actionsToolbar = $this->_view->getLayout()->getBlock(ActionPool::ACTIONS_PAGE_TOOLBAR); + $actionsToolbar = $layout->getBlock(ActionPool::ACTIONS_PAGE_TOOLBAR); $response .= $actionsToolbar instanceof Template ? $actionsToolbar->toHtml() : ''; } $this->_response->appendBody($response); } + + /** + * Optionally validate ACL resource of components with a DataSource/DataProvider + * + * @param mixed $dataProviderConfigData + * @return bool + */ + private function validateAclResource($dataProviderConfigData) + { + if (isset($dataProviderConfigData['aclResource']) + && !$this->_authorization->isAllowed($dataProviderConfigData['aclResource']) + ) { + if (!$this->_request->isAjax()) { + $this->_redirect('admin/denied'); + } + return false; + } + + return true; + } } diff --git a/app/code/Magento/Ui/DataProvider/Modifier/Pool.php b/app/code/Magento/Ui/DataProvider/Modifier/Pool.php index 34a24499834d..882f97d57e9f 100644 --- a/app/code/Magento/Ui/DataProvider/Modifier/Pool.php +++ b/app/code/Magento/Ui/DataProvider/Modifier/Pool.php @@ -88,14 +88,7 @@ public function getModifiersInstances() protected function sort(array $data) { usort($data, function (array $a, array $b) { - $a['sortOrder'] = $this->getSortOrder($a); - $b['sortOrder'] = $this->getSortOrder($b); - - if ($a['sortOrder'] == $b['sortOrder']) { - return 0; - } - - return ($a['sortOrder'] < $b['sortOrder']) ? -1 : 1; + return $this->getSortOrder($a) <=> $this->getSortOrder($b); }); return $data; diff --git a/app/code/Magento/Ui/Model/UiComponentGenerator.php b/app/code/Magento/Ui/Model/UiComponentGenerator.php index f699cff7aa52..ce51c4241e86 100644 --- a/app/code/Magento/Ui/Model/UiComponentGenerator.php +++ b/app/code/Magento/Ui/Model/UiComponentGenerator.php @@ -32,7 +32,6 @@ class UiComponentGenerator * UiComponentGenerator constructor. * @param ContextFactory $contextFactory * @param UiComponentFactory $uiComponentFactory - * @param array $data */ public function __construct( ContextFactory $contextFactory, @@ -48,6 +47,7 @@ public function __construct( * @param string $name * @param \Magento\Framework\View\LayoutInterface $layout * @return UiComponentInterface + * @throws \Magento\Framework\Exception\LocalizedException */ public function generateUiComponent($name, \Magento\Framework\View\LayoutInterface $layout) { diff --git a/app/code/Magento/Ui/TemplateEngine/Xhtml/Result.php b/app/code/Magento/Ui/TemplateEngine/Xhtml/Result.php index e249a64861d4..d16b1eaad7f3 100644 --- a/app/code/Magento/Ui/TemplateEngine/Xhtml/Result.php +++ b/app/code/Magento/Ui/TemplateEngine/Xhtml/Result.php @@ -5,6 +5,8 @@ */ namespace Magento\Ui\TemplateEngine\Xhtml; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Serialize\Serializer\JsonHexTag; use Magento\Framework\View\Layout\Generator\Structure; use Magento\Framework\View\Element\UiComponentInterface; use Magento\Framework\View\TemplateEngine\Xhtml\Template; @@ -42,25 +44,33 @@ class Result implements ResultInterface */ protected $logger; + /** + * @var JsonHexTag + */ + private $jsonSerializer; + /** * @param Template $template * @param CompilerInterface $compiler * @param UiComponentInterface $component * @param Structure $structure * @param LoggerInterface $logger + * @param JsonHexTag $jsonSerializer */ public function __construct( Template $template, CompilerInterface $compiler, UiComponentInterface $component, Structure $structure, - LoggerInterface $logger + LoggerInterface $logger, + JsonHexTag $jsonSerializer = null ) { $this->template = $template; $this->compiler = $compiler; $this->component = $component; $this->structure = $structure; $this->logger = $logger; + $this->jsonSerializer = $jsonSerializer ?? ObjectManager::getInstance()->get(JsonHexTag::class); } /** @@ -81,7 +91,7 @@ public function getDocumentElement() public function appendLayoutConfiguration() { $layoutConfiguration = $this->wrapContent( - json_encode($this->structure->generate($this->component), JSON_HEX_TAG) + $this->jsonSerializer->serialize($this->structure->generate($this->component)) ); $this->template->append($layoutConfiguration); } @@ -105,7 +115,7 @@ public function __toString() $this->compiler->compile($templateRootElement, $this->component, $this->component); $this->appendLayoutConfiguration(); $result = $this->compiler->postprocessing($this->template->__toString()); - } catch (\Exception $e) { + } catch (\Throwable $e) { $this->logger->critical($e->getMessage()); $result = $e->getMessage(); } diff --git a/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminDataGridPaginationActionGroup.xml b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminDataGridPaginationActionGroup.xml index 9148c22976c1..fbb543a6cab9 100644 --- a/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminDataGridPaginationActionGroup.xml +++ b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminDataGridPaginationActionGroup.xml @@ -23,8 +23,23 @@ </arguments> <click selector="{{AdminDataGridPaginationSection.perPageDropdown}}" stepKey="clickPerPageDropdown"/> <click selector="{{AdminDataGridPaginationSection.perPageOption('Custom')}}" stepKey="selectCustomPerPage"/> + <waitForElementVisible selector="{{AdminDataGridPaginationSection.perPageInput}}" time="30" stepKey="waitForInputVisible"/> <fillField selector="{{AdminDataGridPaginationSection.perPageInput}}" userInput="{{perPage}}" stepKey="fillCustomPerPage"/> <click selector="{{AdminDataGridPaginationSection.perPageApplyInput}}" stepKey="applyCustomPerPage"/> <waitForLoadingMaskToDisappear stepKey="waitForGridLoad"/> + <seeInField selector="{{AdminDataGridPaginationSection.perPageDropDownValue}}" userInput="{{perPage}}" stepKey="seePerPageValueInDropDown"/> + </actionGroup> + + <actionGroup name="adminDataGridDeleteCustomPerPage"> + <arguments> + <argument name="perPage"/> + </arguments> + <click selector="{{AdminDataGridPaginationSection.perPageDropdown}}" stepKey="clickPerPageDropdown1"/> + <click selector="{{AdminDataGridPaginationSection.perPageEditCustomValue(perPage)}}" stepKey="clickToEditCustomPerPage"/> + <waitForElementVisible selector="{{AdminDataGridPaginationSection.perPageDeleteCustomValue(perPage)}}" time="30" stepKey="waitForDeleteButtonVisible"/> + <click selector="{{AdminDataGridPaginationSection.perPageDeleteCustomValue(perPage)}}" stepKey="clickToDeleteCustomPerPage"/> + <waitForLoadingMaskToDisappear stepKey="waitForGridLoad"/> + <click selector="{{AdminDataGridPaginationSection.perPageDropdown}}" stepKey="clickPerPageDropdown"/> + <dontSeeElement selector="{{AdminDataGridPaginationSection.perPageDropDownItem(perPage)}}" stepKey="dontSeeDropDownItem"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminGridFilterSearchResultsActionGroup.xml b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminGridFilterSearchResultsActionGroup.xml index 73d441dd96d1..85f02e42345a 100644 --- a/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminGridFilterSearchResultsActionGroup.xml +++ b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminGridFilterSearchResultsActionGroup.xml @@ -14,7 +14,7 @@ <argument name="value" type="string"/> </arguments> - <conditionalClick selector="{{AdminGridFilterControls.clearAll}}" dependentSelector="(//*[contains(@class, 'admin__data-grid-header')][contains(@data-bind, 'afterRender: \$data.setToolbarNode')]//*[contains(@class, 'admin__data-grid-filters-current')][contains(@class, '_show')])[1]" visible="true" stepKey="clearTheFiltersIfPresent"/> + <conditionalClick selector="{{AdminGridFilterControls.clearAll}}" dependentSelector="{{AdminGridFilterControls.clearAll}}" visible="true" stepKey="clearTheFiltersIfPresent"/> <waitForPageLoad stepKey="waitForPageLoad" time="5"/> <click selector="{{AdminGridFilterControls.filters}}" stepKey="clickOnFilters1"/> diff --git a/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminSaveAndCloseActionGroup.xml b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminSaveAndCloseActionGroup.xml index 9a9458ab34d2..20c8927d4917 100644 --- a/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminSaveAndCloseActionGroup.xml +++ b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminSaveAndCloseActionGroup.xml @@ -13,4 +13,10 @@ <click selector="{{AdminProductFormActionSection.saveAndClose}}" stepKey="clickOnSaveAndClose"/> <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="assertSaveMessageSuccess"/> </actionGroup> + <actionGroup name="AdminFormSaveAndDuplicate"> + <click selector="{{AdminProductFormActionSection.saveArrow}}" stepKey="openSaveDropDown"/> + <click selector="{{AdminProductFormActionSection.saveAndDuplicate}}" stepKey="clickOnSaveAndDuplicate"/> + <see selector="{{AdminProductMessagesSection.successMessage}}" stepKey="assertSaveSuccess" userInput="You saved the product."/> + <see selector="{{AdminProductMessagesSection.successMessage}}" stepKey="assertDuplicateSuccess" userInput="You duplicated the product."/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridHeaderSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridHeaderSection.xml index 3e917a5944f9..4ee38e30f98e 100644 --- a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridHeaderSection.xml +++ b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridHeaderSection.xml @@ -25,5 +25,7 @@ <!--Visible columns management--> <element name="columnsToggle" type="button" selector="div.admin__data-grid-action-columns button[data-bind='toggleCollapsible']" timeout="30"/> <element name="columnCheckbox" type="checkbox" selector="//div[contains(@class,'admin__data-grid-action-columns')]//div[contains(@class, 'admin__field-option')]//label[text() = '{{column}}']/preceding-sibling::input" parameterized="true"/> + <element name="perPage" type="select" selector="#product_attributes_listing.product_attributes_listing.listing_top.listing_paging_sizes"/> + <element name="attributeName" type="input" selector="//div[text()='{{arg}}']/../preceding-sibling::td//input" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridPaginationSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridPaginationSection.xml index 0f54f51549e7..133836761174 100644 --- a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridPaginationSection.xml +++ b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridPaginationSection.xml @@ -11,11 +11,15 @@ <section name="AdminDataGridPaginationSection"> <element name="perPageDropdown" type="select" selector=".admin__data-grid-pager-wrap .selectmenu"/> <element name="perPageOption" type="button" selector="//div[@class='admin__data-grid-pager-wrap']//div[@class='selectmenu-items _active']//li//button[text()='{{var1}}']" parameterized="true"/> - <element name="perPageInput" type="input" selector="//div[@class='admin__data-grid-pager-wrap']//div[@class='selectmenu-items _active']//li//div[@class='selectmenu-item-edit']//input"/> - <element name="perPageApplyInput" type="button" selector="//div[@class='admin__data-grid-pager-wrap']//div[@class='selectmenu-items _active']//li//div[@class='selectmenu-item-edit']//button"/> + <element name="perPageInput" type="input" selector="//div[contains(@class, 'admin__data-grid-pager-wrap')]//div[contains(@class, 'selectmenu-items _active')]//li[contains(@class, '_edit')]//div[contains(@class, 'selectmenu-item-edit')]//input"/> + <element name="perPageApplyInput" type="button" selector="//div[contains(@class, 'admin__data-grid-pager-wrap')]//div[contains(@class, 'selectmenu-items _active')]//li[@class='_edit']//div[contains(@class, 'selectmenu-item-edit')]//button"/> + <element name="perPageDropDownItem" type="button" selector="//*[contains(@class, 'selectmenu-items _active')]//button[contains(@class, 'selectmenu-item-action') and text()='{{dropDownItem}}']" timeout="30" parameterized="true"/> + <element name="perPageEditCustomValue" type="button" selector="//div[contains(@class, 'selectmenu-items _active')]//div[contains(@class, 'selectmenu-item')]//button[text()='{{perPageCustomValue}}']/following-sibling::button[contains(@class, 'action-edit')]" parameterized="true"/> + <element name="perPageDeleteCustomValue" type="button" selector="//div[contains(@class, 'selectmenu-items _active')]//div[contains(@class, 'selectmenu-item')]//button[text()='{{perPageCustomValue}}']/parent::div/preceding-sibling::div/button[contains(@class, 'action-delete')]" parameterized="true"/> <element name="nextPage" type="button" selector="div.admin__data-grid-pager > button.action-next" timeout="30"/> <element name="previousPage" type="button" selector="div.admin__data-grid-pager > button.action-previous" timeout="30"/> <element name="currentPage" type="input" selector="div.admin__data-grid-pager > input[data-ui-id='current-page-input']"/> <element name="totalPages" type="text" selector="div.admin__data-grid-pager > label"/> + <element name="perPageDropDownValue" type="input" selector=".selectmenu-value input" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridTableSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridTableSection.xml index edcc70a82396..a2b7ba8c1ffd 100644 --- a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridTableSection.xml +++ b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridTableSection.xml @@ -18,5 +18,6 @@ <!--Specific cell e.g. {{Section.gridCell('1', 'Name')}}--> <element name="gridCell" type="text" selector="//tr[{{row}}]//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., '{{column}}')]/preceding-sibling::th) +1 ]" parameterized="true"/> <element name="rowViewAction" type="button" selector=".data-grid tbody > tr:nth-of-type({{row}}) .action-menu-item" parameterized="true" timeout="30"/> + <element name="dataGridEmpty" type="block" selector=".data-grid-tr-no-data td"/> </section> </sections> diff --git a/app/code/Magento/Ui/Test/Mftf/Section/AdminMessagesSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/AdminMessagesSection.xml index cb1fdd716b17..3d4efa13ce3a 100644 --- a/app/code/Magento/Ui/Test/Mftf/Section/AdminMessagesSection.xml +++ b/app/code/Magento/Ui/Test/Mftf/Section/AdminMessagesSection.xml @@ -11,5 +11,7 @@ <section name="AdminMessagesSection"> <element name="successMessage" type="text" selector=".message-success"/> <element name="errorMessage" type="text" selector=".message.message-error.error"/> + <element name="warningMessage" type="text" selector=".message-warning"/> + <element name="noticeMessage" type="text" selector=".message-notice"/> </section> </sections> diff --git a/app/code/Magento/Ui/Test/Mftf/Section/ModalConfirmationSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/ModalConfirmationSection.xml index 35ec242f05c5..4bf84d9ee63d 100644 --- a/app/code/Magento/Ui/Test/Mftf/Section/ModalConfirmationSection.xml +++ b/app/code/Magento/Ui/Test/Mftf/Section/ModalConfirmationSection.xml @@ -9,6 +9,7 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="ModalConfirmationSection"> + <element name="modalContent" type="text" selector="aside.confirm div.modal-content"/> <element name="CancelButton" type="button" selector="//footer[@class='modal-footer']/button[contains(@class, 'action-dismiss')]"/> <element name="OkButton" type="button" selector="//footer[@class='modal-footer']/button[contains(@class, 'action-accept')]"/> </section> diff --git a/app/code/Magento/Ui/Test/Unit/Component/Filters/FilterModifierTest.php b/app/code/Magento/Ui/Test/Unit/Component/Filters/FilterModifierTest.php index f91401e43ea8..50d82b19d104 100644 --- a/app/code/Magento/Ui/Test/Unit/Component/Filters/FilterModifierTest.php +++ b/app/code/Magento/Ui/Test/Unit/Component/Filters/FilterModifierTest.php @@ -66,7 +66,8 @@ public function testNotApplyFilterModifier() /** * @return void - * @assertException \Magento\Framework\Exception\LocalizedException + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Condition type "not_allowed" is not allowed */ public function testApplyFilterModifierWithNotAllowedCondition() { @@ -78,7 +79,7 @@ public function testApplyFilterModifierWithNotAllowedCondition() ] ]); $this->dataProvider->expects($this->never())->method('addFilter'); - $this->unit->applyFilterModifier($this->dataProvider, 'test'); + $this->unit->applyFilterModifier($this->dataProvider, 'filter'); } /** diff --git a/app/code/Magento/Ui/Test/Unit/Component/Form/Element/ActionDeleteTest.php b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/ActionDeleteTest.php index 6f45c192d6c4..2cb35c7b85dd 100644 --- a/app/code/Magento/Ui/Test/Unit/Component/Form/Element/ActionDeleteTest.php +++ b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/ActionDeleteTest.php @@ -13,13 +13,16 @@ class ActionDeleteTest extends AbstractElementTest { /** - * {@inheritdoc} + * @inheritdoc */ protected function getModelName() { return ActionDelete::class; } + /** + * @inheritdoc + */ public function testGetComponentName() { $this->assertSame(ActionDelete::NAME, $this->getModel()->getComponentName()); diff --git a/app/code/Magento/Ui/Test/Unit/Component/Form/Element/CheckboxSetTest.php b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/CheckboxSetTest.php index 025f4a158245..3f00fa6c7ff3 100644 --- a/app/code/Magento/Ui/Test/Unit/Component/Form/Element/CheckboxSetTest.php +++ b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/CheckboxSetTest.php @@ -15,13 +15,16 @@ class CheckboxSetTest extends AbstractElementTest { /** - * {@inheritdoc} + * @inheritdoc */ protected function getModelName() { return CheckboxSet::class; } + /** + * @inheritdoc + */ public function testGetComponentName() { $this->assertSame(CheckboxSet::NAME, $this->getModel()->getComponentName()); diff --git a/app/code/Magento/Ui/Test/Unit/Component/Form/Element/DataType/MediaTest.php b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/DataType/MediaTest.php index 7af173cf2a96..9653ecd7ded4 100644 --- a/app/code/Magento/Ui/Test/Unit/Component/Form/Element/DataType/MediaTest.php +++ b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/DataType/MediaTest.php @@ -21,6 +21,9 @@ class MediaTest extends \PHPUnit\Framework\TestCase /** @var Media */ protected $media; + /** + * @inheritdoc + */ public function setUp() { $this->context = $this->getMockBuilder(\Magento\Framework\View\Element\UiComponent\ContextInterface::class) @@ -29,8 +32,13 @@ public function setUp() ->getMockForAbstractClass(); $this->media = new Media($this->context); + } + + public function testPrepareWithoutDataScope() + { $this->media->setData( [ + 'name' => 'test_name', 'config' => [ 'uploaderConfig' => [ 'url' => 'module/actionPath/path' @@ -38,19 +46,54 @@ public function setUp() ], ] ); + $url = 'http://magento2.com/module/actionPath/path/key/34523456234523trdg'; + $expectedConfig = [ + 'uploaderConfig' => ['url' => $url], + 'dataScope' => 'test_name' + ]; + + $this->processor = $this->getMockBuilder(\Magento\Framework\View\Element\UiComponent\Processor::class) + ->disableOriginalConstructor() + ->getMock(); + $this->context->expects($this->atLeastOnce())->method('getProcessor')->willReturn($this->processor); + $this->context->expects($this->once()) + ->method('getUrl') + ->with('module/actionPath/path', ['_secure' => true]) + ->willReturn($url); + $this->media->prepare(); + $configuration = $this->media->getConfiguration(); + $this->assertEquals($expectedConfig, $configuration); } - public function testPrepare() + public function testPrepareWithDataScope() { + $this->media->setData( + [ + 'name' => 'test_name', + 'config' => [ + 'dataScope' => 'other_data_scope', + 'uploaderConfig' => [ + 'url' => 'module/actionPath/path' + ], + ], + ] + ); + $url = 'http://magento2.com/module/actionPath/path/key/34523456234523trdg'; + $expectedConfig = [ + 'uploaderConfig' => ['url' => $url], + 'dataScope' => 'other_data_scope' + ]; + $this->processor = $this->getMockBuilder(\Magento\Framework\View\Element\UiComponent\Processor::class) ->disableOriginalConstructor() ->getMock(); $this->context->expects($this->atLeastOnce())->method('getProcessor')->willReturn($this->processor); - $url = 'http://magento2.com/module/actionPath/path/key/34523456234523trdg'; $this->context->expects($this->once()) ->method('getUrl') ->with('module/actionPath/path', ['_secure' => true]) ->willReturn($url); $this->media->prepare(); + $configuration = $this->media->getConfiguration(); + $this->assertEquals($expectedConfig, $configuration); } } diff --git a/app/code/Magento/Ui/Test/Unit/Component/Form/Element/MultiSelectTest.php b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/MultiSelectTest.php index cb91fbb945bb..f37ca38a8d9b 100644 --- a/app/code/Magento/Ui/Test/Unit/Component/Form/Element/MultiSelectTest.php +++ b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/MultiSelectTest.php @@ -15,13 +15,16 @@ class MultiSelectTest extends AbstractElementTest { /** - * {@inheritdoc} + * @inheritdoc */ protected function getModelName() { return MultiSelect::class; } + /** + * @inheritdoc + */ public function testGetComponentName() { $this->contextMock->expects($this->never())->method('getProcessor'); diff --git a/app/code/Magento/Ui/Test/Unit/Component/Form/Element/RadioSetTest.php b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/RadioSetTest.php index 0e0fef60df60..67150e3c8fd3 100644 --- a/app/code/Magento/Ui/Test/Unit/Component/Form/Element/RadioSetTest.php +++ b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/RadioSetTest.php @@ -15,13 +15,16 @@ class RadioSetTest extends AbstractElementTest { /** - * {@inheritdoc} + * @inheritdoc */ protected function getModelName() { return RadioSet::class; } + /** + * @inheritdoc + */ public function testGetComponentName() { $this->assertSame(RadioSet::NAME, $this->getModel()->getComponentName()); diff --git a/app/code/Magento/Ui/Test/Unit/Component/Form/Element/SelectTest.php b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/SelectTest.php index c69526268106..d4677192cc08 100644 --- a/app/code/Magento/Ui/Test/Unit/Component/Form/Element/SelectTest.php +++ b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/SelectTest.php @@ -15,13 +15,16 @@ class SelectTest extends AbstractElementTest { /** - * {@inheritdoc} + * @inheritdoc */ protected function getModelName() { return Select::class; } + /** + * @inheritdoc + */ public function testGetComponentName() { $this->assertSame(Select::NAME, $this->getModel()->getComponentName()); diff --git a/app/code/Magento/Ui/Test/Unit/Component/Form/Element/WysiwygTest.php b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/WysiwygTest.php index b345989ba05e..4bfd952a6c56 100644 --- a/app/code/Magento/Ui/Test/Unit/Component/Form/Element/WysiwygTest.php +++ b/app/code/Magento/Ui/Test/Unit/Component/Form/Element/WysiwygTest.php @@ -85,13 +85,16 @@ protected function getModel() } /** - * {@inheritdoc} + * @inheritdoc */ protected function getModelName() { return Wysiwyg::class; } + /** + * @inheritdoc + */ public function testGetComponentName() { $this->assertSame(Wysiwyg::NAME, $this->getModel()->getComponentName()); diff --git a/app/code/Magento/Ui/Test/Unit/Component/Form/FieldsetTest.php b/app/code/Magento/Ui/Test/Unit/Component/Form/FieldsetTest.php new file mode 100644 index 000000000000..d24350758080 --- /dev/null +++ b/app/code/Magento/Ui/Test/Unit/Component/Form/FieldsetTest.php @@ -0,0 +1,58 @@ +<?php +declare(strict_types=1); +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Ui\Test\Unit\Component\Form; + +use Magento\Ui\Component\Form\Fieldset; +use Magento\Framework\View\Element\UiComponent\ContextInterface; + +/** + * Class FieldTest + * + * Test for class \Magento\Ui\Component\Form\Fieldset + */ +class FieldsetTest extends \PHPUnit\Framework\TestCase +{ + const NAME = 'fieldset'; + + /** + * @var Fieldset + */ + protected $fieldset; + + /** + * @var ContextInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $context; + + /** + * Set up + * + * @return void + */ + protected function setUp() + { + $this->context = $this->getMockBuilder(\Magento\Framework\View\Element\UiComponent\ContextInterface::class) + ->getMockForAbstractClass(); + + $this->fieldset = new Fieldset( + $this->context, + [], + [] + ); + } + + /** + * Run test for getComponentName() method + * + * @return void + * + */ + public function testGetComponentName() + { + $this->assertEquals(self::NAME, $this->fieldset->getComponentName()); + } +} diff --git a/app/code/Magento/Ui/Test/Unit/Component/FormTest.php b/app/code/Magento/Ui/Test/Unit/Component/FormTest.php index 6951583291df..6df69c7d0e48 100644 --- a/app/code/Magento/Ui/Test/Unit/Component/FormTest.php +++ b/app/code/Magento/Ui/Test/Unit/Component/FormTest.php @@ -9,7 +9,6 @@ use Magento\Framework\Api\FilterBuilder; use Magento\Framework\View\Element\UiComponent\ContextInterface; use Magento\Framework\View\Element\UiComponent\DataProvider\DataProviderInterface; -use Magento\Framework\View\Element\UiComponent\Processor; use Magento\Ui\Component\Form; class FormTest extends \PHPUnit\Framework\TestCase @@ -215,4 +214,65 @@ public function testGetDataSourceDataWithoutId() $this->assertEquals($dataSource, $this->model->getDataSourceData()); } + + public function testGetDataSourceDataWithAbstractDataProvider() + { + $requestFieldName = 'request_id'; + $primaryFieldName = 'primary_id'; + $fieldId = 44; + $row = ['key' => 'value', $primaryFieldName => $fieldId]; + $data = [ + 'items' => [$row], + ]; + $dataSource = [ + 'data' => [ + 'general' => $row + ], + ]; + + /** @var DataProviderInterface|\PHPUnit_Framework_MockObject_MockObject $dataProviderMock */ + $dataProviderMock = + $this->getMockBuilder(\Magento\Framework\View\Element\UiComponent\DataProvider\DataProviderInterface::class) + ->getMock(); + $dataProviderMock->expects($this->once()) + ->method('getRequestFieldName') + ->willReturn($requestFieldName); + $dataProviderMock->expects($this->once()) + ->method('getPrimaryFieldName') + ->willReturn($primaryFieldName); + + $this->contextMock->expects($this->any()) + ->method('getDataProvider') + ->willReturn($dataProviderMock); + $this->contextMock->expects($this->once()) + ->method('getRequestParam') + ->with($requestFieldName) + ->willReturn($fieldId); + + /** @var Filter|\PHPUnit_Framework_MockObject_MockObject $filterMock */ + $filterMock = $this->getMockBuilder(\Magento\Framework\Api\Filter::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->filterBuilderMock->expects($this->once()) + ->method('setField') + ->with($primaryFieldName) + ->willReturnSelf(); + $this->filterBuilderMock->expects($this->once()) + ->method('setValue') + ->with($fieldId) + ->willReturnSelf(); + $this->filterBuilderMock->expects($this->once()) + ->method('create') + ->willReturn($filterMock); + + $dataProviderMock->expects($this->once()) + ->method('addFilter') + ->with($filterMock); + $dataProviderMock->expects($this->once()) + ->method('getData') + ->willReturn($data); + + $this->assertEquals($dataSource, $this->model->getDataSourceData()); + } } diff --git a/app/code/Magento/Ui/Test/Unit/Controller/Adminhtml/Index/Render/HandleTest.php b/app/code/Magento/Ui/Test/Unit/Controller/Adminhtml/Index/Render/HandleTest.php index 31d5b421013f..d31537458f21 100644 --- a/app/code/Magento/Ui/Test/Unit/Controller/Adminhtml/Index/Render/HandleTest.php +++ b/app/code/Magento/Ui/Test/Unit/Controller/Adminhtml/Index/Render/HandleTest.php @@ -6,7 +6,11 @@ namespace Magento\Ui\Test\Unit\Controller\Adminhtml\Index\Render; use Magento\Ui\Controller\Adminhtml\Index\Render\Handle; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class HandleTest extends \PHPUnit\Framework\TestCase { /** @@ -27,22 +31,42 @@ class HandleTest extends \PHPUnit\Framework\TestCase /** * @var \PHPUnit_Framework_MockObject_MockObject */ - protected $componentFactoryMock; + protected $viewMock; + + /** + * @var Handle + */ + protected $controller; + + /** + * @var \Magento\Framework\AuthorizationInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $authorizationMock; /** * @var \PHPUnit_Framework_MockObject_MockObject */ - protected $viewMock; + private $uiComponentContextMock; /** - * @var Handle + * @var \Magento\Framework\View\Element\UiComponentInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $controller; + private $uiComponentMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $uiFactoryMock; + + /** + * @var \Magento\Framework\View\Element\UiComponent\DataProvider\DataProviderInterface| + * \PHPUnit_Framework_MockObject_MockObject + */ + private $dataProviderMock; public function setUp() { $this->contextMock = $this->createMock(\Magento\Backend\App\Action\Context::class); - $this->componentFactoryMock = $this->createMock(\Magento\Framework\View\Element\UiComponentFactory::class); $this->requestMock = $this->createMock(\Magento\Framework\App\RequestInterface::class); $this->contextMock->expects($this->atLeastOnce())->method('getRequest')->willReturn($this->requestMock); @@ -52,8 +76,37 @@ public function setUp() $this->viewMock = $this->createMock(\Magento\Framework\App\ViewInterface::class); $this->contextMock->expects($this->atLeastOnce())->method('getView')->willReturn($this->viewMock); - - $this->controller = new Handle($this->contextMock, $this->componentFactoryMock); + $this->authorizationMock = $this->getMockBuilder(\Magento\Framework\AuthorizationInterface::class) + ->getMockForAbstractClass(); + $this->authorizationMock->expects($this->any()) + ->method('isAllowed') + ->willReturn(true); + $this->uiComponentContextMock = $this->getMockForAbstractClass( + ContextInterface::class + ); + $this->uiComponentMock = $this->getMockForAbstractClass( + \Magento\Framework\View\Element\UiComponentInterface::class + ); + $this->dataProviderMock = $this->getMockForAbstractClass( + \Magento\Framework\View\Element\UiComponent\DataProvider\DataProviderInterface::class + ); + $this->uiComponentContextMock->expects($this->once()) + ->method('getDataProvider') + ->willReturn($this->dataProviderMock); + $this->uiFactoryMock = $this->getMockBuilder(\Magento\Framework\View\Element\UiComponentFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->uiComponentMock->expects($this->any()) + ->method('getContext') + ->willReturn($this->uiComponentContextMock); + $this->uiFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->uiComponentMock); + $this->dataProviderMock->expects($this->once()) + ->method('getConfigData') + ->willReturn([]); + $contextMock = $this->createMock(\Magento\Framework\View\Element\UiComponent\ContextFactory::class); + $this->controller = new Handle($this->contextMock, $this->uiFactoryMock, $contextMock); } public function testExecuteNoButtons() @@ -83,7 +136,7 @@ public function testExecute() ->with(['default', $result], true, true, false); $layoutMock = $this->createMock(\Magento\Framework\View\LayoutInterface::class); - $this->viewMock->expects($this->exactly(2))->method('getLayout')->willReturn($layoutMock); + $this->viewMock->expects($this->once())->method('getLayout')->willReturn($layoutMock); $layoutMock->expects($this->exactly(2))->method('getBlock'); diff --git a/app/code/Magento/Ui/Test/Unit/Controller/Adminhtml/Index/RenderTest.php b/app/code/Magento/Ui/Test/Unit/Controller/Adminhtml/Index/RenderTest.php index 05b35fb017b4..2bba8686490b 100644 --- a/app/code/Magento/Ui/Test/Unit/Controller/Adminhtml/Index/RenderTest.php +++ b/app/code/Magento/Ui/Test/Unit/Controller/Adminhtml/Index/RenderTest.php @@ -3,12 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Ui\Test\Unit\Controller\Adminhtml\Index; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Escaper; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Framework\View\Element\UiComponent\ContextInterface; use Magento\Ui\Controller\Adminhtml\Index\Render; use Magento\Ui\Model\UiComponentTypeResolver; -use Magento\Framework\View\Element\UiComponent\ContextInterface; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Zend\Http\AbstractMessage; +use Zend\Http\Response; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -97,6 +102,11 @@ class RenderTest extends \PHPUnit\Framework\TestCase */ private $loggerMock; + /** + * @var Escaper|\PHPUnit_Framework_MockObject_MockObject + */ + private $escaperMock; + protected function setUp() { $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) @@ -170,6 +180,10 @@ protected function setUp() $this->uiComponentTypeResolverMock = $this->getMockBuilder(UiComponentTypeResolver::class) ->disableOriginalConstructor() ->getMock(); + $this->escaperMock = $this->createMock(Escaper::class); + $this->escaperMock->expects($this->any()) + ->method('escapeHtml') + ->willReturnArgument(0); $this->objectManagerHelper = new ObjectManagerHelper($this); @@ -181,6 +195,7 @@ protected function setUp() 'contentTypeResolver' => $this->uiComponentTypeResolverMock, 'resultJsonFactory' => $this->resultJsonFactoryMock, 'logger' => $this->loggerMock, + 'escaper' => $this->escaperMock, ] ); } @@ -201,7 +216,7 @@ public function testExecuteAjaxRequestException() ->method('appendBody') ->willThrowException(new \Exception('exception')); - $jsonResultMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Json::class) + $jsonResultMock = $this->getMockBuilder(Json::class) ->disableOriginalConstructor() ->setMethods(['setData']) ->getMock(); @@ -290,6 +305,34 @@ public function testExecuteAjaxRequestWithoutPermissions(array $dataProviderConf $name = 'test-name'; $renderedData = '<html>data</html>'; + if (false === $isAllowed) { + $jsonResultMock = $this->getMockBuilder(Json::class) + ->disableOriginalConstructor() + ->setMethods(['setStatusHeader', 'setData']) + ->getMock(); + + $jsonResultMock->expects($this->at(0)) + ->method('setStatusHeader') + ->with( + Response::STATUS_CODE_403, + AbstractMessage::VERSION_11, + 'Forbidden' + ) + ->willReturnSelf(); + + $jsonResultMock->expects($this->at(1)) + ->method('setData') + ->with([ + 'error' => 'Forbidden', + 'errorcode' => 403 + ]) + ->willReturnSelf(); + + $this->resultJsonFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($jsonResultMock); + } + $this->requestMock->expects($this->any()) ->method('getParam') ->with('namespace') diff --git a/app/code/Magento/Ui/etc/db_schema.xml b/app/code/Magento/Ui/etc/db_schema.xml index d07329df9eb2..13a384024f18 100644 --- a/app/code/Magento/Ui/etc/db_schema.xml +++ b/app/code/Magento/Ui/etc/db_schema.xml @@ -18,14 +18,14 @@ comment="Mark current bookmark per user and identifier"/> <column xsi:type="varchar" name="title" nullable="true" length="255" comment="Bookmark title"/> <column xsi:type="longtext" name="config" nullable="true" comment="Bookmark config"/> - <column xsi:type="datetime" name="created_at" on_update="false" nullable="false" comment="Bookmark created at"/> - <column xsi:type="datetime" name="updated_at" on_update="false" nullable="false" comment="Bookmark updated at"/> - <constraint xsi:type="primary" name="PRIMARY"> + <column xsi:type="datetime" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Bookmark created at"/> + <column xsi:type="datetime" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" comment="Bookmark updated at"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="bookmark_id"/> </constraint> - <constraint xsi:type="foreign" name="UI_BOOKMARK_USER_ID_ADMIN_USER_USER_ID" table="ui_bookmark" + <constraint xsi:type="foreign" referenceId="UI_BOOKMARK_USER_ID_ADMIN_USER_USER_ID" table="ui_bookmark" column="user_id" referenceTable="admin_user" referenceColumn="user_id" onDelete="CASCADE"/> - <index name="UI_BOOKMARK_USER_ID_NAMESPACE_IDENTIFIER" indexType="btree"> + <index referenceId="UI_BOOKMARK_USER_ID_NAMESPACE_IDENTIFIER" indexType="btree"> <column name="user_id"/> <column name="namespace"/> <column name="identifier"/> diff --git a/app/code/Magento/Ui/i18n/de_DE.csv b/app/code/Magento/Ui/i18n/de_DE.csv deleted file mode 100644 index 2efac126b857..000000000000 --- a/app/code/Magento/Ui/i18n/de_DE.csv +++ /dev/null @@ -1,4 +0,0 @@ -"Log JS Errors to Session Storage","Log JS Errors to Session Storage" -"If enabled, can be used by functional tests for extended reporting","If enabled, can be used by functional tests for extended reporting" -"Log JS Errors to Session Storage Key","Log JS Errors to Session Storage Key" -"Use this key to retrieve collected js errors","Use this key to retrieve collected js errors" diff --git a/app/code/Magento/Ui/i18n/en_US.csv b/app/code/Magento/Ui/i18n/en_US.csv index 106526f66e70..1ce2692886ae 100644 --- a/app/code/Magento/Ui/i18n/en_US.csv +++ b/app/code/Magento/Ui/i18n/en_US.csv @@ -58,6 +58,7 @@ Keyword,Keyword "Letters, numbers, spaces or underscores only please","Letters, numbers, spaces or underscores only please" "Letters only please","Letters only please" "No white space please","No white space please" +"No marginal white space please","No marginal white space please" "Your ZIP-code must be in the range 902xx-xxxx to 905-xx-xxxx","Your ZIP-code must be in the range 902xx-xxxx to 905-xx-xxxx" "A positive or negative non-decimal number please","A positive or negative non-decimal number please" "The specified vehicle identification number (VIN) is invalid.","The specified vehicle identification number (VIN) is invalid." @@ -77,6 +78,7 @@ Keyword,Keyword "Empty Value.","Empty Value." "Please use only letters (a-z or A-Z), numbers (0-9) or spaces only in this field.","Please use only letters (a-z or A-Z), numbers (0-9) or spaces only in this field." "Please use only letters (a-z or A-Z), numbers (0-9) or underscore (_) in this field, and the first character should be a letter.","Please use only letters (a-z or A-Z), numbers (0-9) or underscore (_) in this field, and the first character should be a letter." +"Attribute code ""%1"" is invalid. Please use only letters (a-z or A-Z), numbers (0-9) or underscore (_) in this field, and the first character should be a letter.","Attribute code ""%1"" is invalid. Please use only letters (a-z or A-Z), numbers (0-9) or underscore (_) in this field, and the first character should be a letter." "Please use only letters (a-z or A-Z), numbers (0-9), spaces and ""#"" in this field.","Please use only letters (a-z or A-Z), numbers (0-9), spaces and ""#"" in this field." "Please enter a valid phone number. For example (123) 456-7890 or 123-456-7890.","Please enter a valid phone number. For example (123) 456-7890 or 123-456-7890." "Please enter a valid fax number (Ex: 123-456-7890).","Please enter a valid fax number (Ex: 123-456-7890)." @@ -180,7 +182,7 @@ CSV,CSV "Please enter a valid value from list","Please enter a valid value from list" "Please enter valid SKU key.","Please enter valid SKU key." "Please enter a valid number.","Please enter a valid number." -"Admin is a required field in the each row.","Admin is a required field in the each row." +"Admin is a required field in each row.","Admin is a required field in each row." "Please fix this field.","Please fix this field." "Please enter a valid date (ISO).","Please enter a valid date (ISO)." "Please enter only digits.","Please enter only digits." @@ -189,4 +191,5 @@ CSV,CSV "Please enter at least {0} characters.","Please enter at least {0} characters." "Please enter a value between {0} and {1} characters long.","Please enter a value between {0} and {1} characters long." "Please enter a value between {0} and {1}.","Please enter a value between {0} and {1}." -"was not uploaded","was not uploaded" \ No newline at end of file +"was not uploaded","was not uploaded" +"The file upload field is disabled.","The file upload field is disabled." diff --git a/app/code/Magento/Ui/i18n/es_ES.csv b/app/code/Magento/Ui/i18n/es_ES.csv deleted file mode 100644 index 2efac126b857..000000000000 --- a/app/code/Magento/Ui/i18n/es_ES.csv +++ /dev/null @@ -1,4 +0,0 @@ -"Log JS Errors to Session Storage","Log JS Errors to Session Storage" -"If enabled, can be used by functional tests for extended reporting","If enabled, can be used by functional tests for extended reporting" -"Log JS Errors to Session Storage Key","Log JS Errors to Session Storage Key" -"Use this key to retrieve collected js errors","Use this key to retrieve collected js errors" diff --git a/app/code/Magento/Ui/i18n/fr_FR.csv b/app/code/Magento/Ui/i18n/fr_FR.csv deleted file mode 100644 index 2efac126b857..000000000000 --- a/app/code/Magento/Ui/i18n/fr_FR.csv +++ /dev/null @@ -1,4 +0,0 @@ -"Log JS Errors to Session Storage","Log JS Errors to Session Storage" -"If enabled, can be used by functional tests for extended reporting","If enabled, can be used by functional tests for extended reporting" -"Log JS Errors to Session Storage Key","Log JS Errors to Session Storage Key" -"Use this key to retrieve collected js errors","Use this key to retrieve collected js errors" diff --git a/app/code/Magento/Ui/i18n/nl_NL.csv b/app/code/Magento/Ui/i18n/nl_NL.csv deleted file mode 100644 index 2efac126b857..000000000000 --- a/app/code/Magento/Ui/i18n/nl_NL.csv +++ /dev/null @@ -1,4 +0,0 @@ -"Log JS Errors to Session Storage","Log JS Errors to Session Storage" -"If enabled, can be used by functional tests for extended reporting","If enabled, can be used by functional tests for extended reporting" -"Log JS Errors to Session Storage Key","Log JS Errors to Session Storage Key" -"Use this key to retrieve collected js errors","Use this key to retrieve collected js errors" diff --git a/app/code/Magento/Ui/i18n/pt_BR.csv b/app/code/Magento/Ui/i18n/pt_BR.csv deleted file mode 100644 index 2efac126b857..000000000000 --- a/app/code/Magento/Ui/i18n/pt_BR.csv +++ /dev/null @@ -1,4 +0,0 @@ -"Log JS Errors to Session Storage","Log JS Errors to Session Storage" -"If enabled, can be used by functional tests for extended reporting","If enabled, can be used by functional tests for extended reporting" -"Log JS Errors to Session Storage Key","Log JS Errors to Session Storage Key" -"Use this key to retrieve collected js errors","Use this key to retrieve collected js errors" diff --git a/app/code/Magento/Ui/i18n/zh_Hans_CN.csv b/app/code/Magento/Ui/i18n/zh_Hans_CN.csv deleted file mode 100644 index 2efac126b857..000000000000 --- a/app/code/Magento/Ui/i18n/zh_Hans_CN.csv +++ /dev/null @@ -1,4 +0,0 @@ -"Log JS Errors to Session Storage","Log JS Errors to Session Storage" -"If enabled, can be used by functional tests for extended reporting","If enabled, can be used by functional tests for extended reporting" -"Log JS Errors to Session Storage Key","Log JS Errors to Session Storage Key" -"Use this key to retrieve collected js errors","Use this key to retrieve collected js errors" diff --git a/app/code/Magento/Ui/view/base/requirejs-config.js b/app/code/Magento/Ui/view/base/requirejs-config.js index e91fe309dd19..cdbbd03c93ae 100644 --- a/app/code/Magento/Ui/view/base/requirejs-config.js +++ b/app/code/Magento/Ui/view/base/requirejs-config.js @@ -5,14 +5,12 @@ var config = { shim: { - 'tinymce4': { - 'exports': 'tinymce' + 'tiny_mce_4/tinymce.min': { + exports: 'tinyMCE' } }, paths: { - 'ui/template': 'Magento_Ui/templates', - 'tinymce4': 'tiny_mce_4/tinymce.min', - 'wysiwygAdapter': 'mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter' + 'ui/template': 'Magento_Ui/templates' }, map: { '*': { @@ -24,7 +22,9 @@ var config = { uiRegistry: 'Magento_Ui/js/lib/registry/registry', consoleLogger: 'Magento_Ui/js/lib/logger/console-logger', uiLayout: 'Magento_Ui/js/core/renderer/layout', - buttonAdapter: 'Magento_Ui/js/form/button-adapter' + buttonAdapter: 'Magento_Ui/js/form/button-adapter', + tinymce4: 'tiny_mce_4/tinymce.min', + wysiwygAdapter: 'mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter' } } }; diff --git a/app/code/Magento/Ui/view/base/templates/stepswizard.phtml b/app/code/Magento/Ui/view/base/templates/stepswizard.phtml index 05a537b9a655..78e73e0cd9a6 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/templates/wysiwyg/active_editor.phtml b/app/code/Magento/Ui/view/base/templates/wysiwyg/active_editor.phtml index a4473adc099b..22607921296c 100644 --- a/app/code/Magento/Ui/view/base/templates/wysiwyg/active_editor.phtml +++ b/app/code/Magento/Ui/view/base/templates/wysiwyg/active_editor.phtml @@ -13,8 +13,10 @@ <script> require.config({ - paths: { - wysiwygAdapter: '<?php /* @noEscape */ echo $block->getWysiwygAdapterPath(); ?>' + map: { + '*': { + wysiwygAdapter: '<?php /* @noEscape */ echo $block->getWysiwygAdapterPath(); ?>' + } } }); </script> 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 ccd702c23ea6..8f82b98112f1 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 b10ee00818eb..4dc97910935d 100644 --- a/app/code/Magento/Ui/view/base/ui_component/etc/definition/action.xsd +++ b/app/code/Magento/Ui/view/base/ui_component/etc/definition/action.xsd @@ -49,6 +49,13 @@ </xs:documentation> </xs:annotation> </xs:element> + <xs:element name="actionDisable" type="xs:boolean"> + <xs:annotation> + <xs:documentation> + Disable and remove this action. + </xs:documentation> + </xs:annotation> + </xs:element> </xs:choice> <xs:attribute name="name" use="required"/> </xs:complexType> @@ -82,6 +89,13 @@ </xs:documentation> </xs:annotation> </xs:element> + <xs:element name="actionDisable" type="xs:boolean"> + <xs:annotation> + <xs:documentation> + Disable and remove this action. + </xs:documentation> + </xs:annotation> + </xs:element> <xs:element name="confirm" type="confirmType"> <xs:annotation> <xs:documentation> diff --git a/app/code/Magento/Ui/view/base/ui_component/etc/definition/ui_settings.xsd b/app/code/Magento/Ui/view/base/ui_component/etc/definition/ui_settings.xsd index cbf69e604694..ff4d530b5bfd 100644 --- a/app/code/Magento/Ui/view/base/ui_component/etc/definition/ui_settings.xsd +++ b/app/code/Magento/Ui/view/base/ui_component/etc/definition/ui_settings.xsd @@ -476,6 +476,13 @@ </xs:documentation> </xs:annotation> </xs:element> + <xs:element name="aclResource" type="xs:string" minOccurs="0" maxOccurs="1"> + <xs:annotation> + <xs:documentation> + ACL Resource used to validate access to UI Component data + </xs:documentation> + </xs:annotation> + </xs:element> <xs:element ref="param"/> </xs:choice> <xs:attribute name="name" type="xs:string" use="required"> diff --git a/app/code/Magento/Ui/view/base/web/js/core/renderer/layout.js b/app/code/Magento/Ui/view/base/web/js/core/renderer/layout.js index fe312738469e..ac1de4631e90 100644 --- a/app/code/Magento/Ui/view/base/web/js/core/renderer/layout.js +++ b/app/code/Magento/Ui/view/base/web/js/core/renderer/layout.js @@ -224,7 +224,7 @@ define([ */ build: function (parent, node, name) { var defaults = parent && parent.childDefaults || {}, - children = node.children, + children = this.filterDisabledChildren(node.children), type = getNodeType(parent, node), dataScope = getDataScope(parent, node), component, @@ -294,6 +294,35 @@ define([ return node; }, + /** + * Filter out all disabled components. + * + * @param {Object} children + * @returns {*} + */ + filterDisabledChildren: function (children) { + var cIds; + + //cleanup children config.componentDisabled = true + if (children && typeof children === 'object') { + cIds = Object.keys(children); + + if (cIds) { + _.each(cIds, function (cId) { + if (typeof children[cId] === 'object' && + children[cId].hasOwnProperty('config') && + typeof children[cId].config === 'object' && + children[cId].config.hasOwnProperty('componentDisabled') && + children[cId].config.componentDisabled === true) { + delete children[cId]; + } + }); + } + } + + return children; + }, + /** * Init component. * diff --git a/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dnd.js b/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dnd.js index dee9ba7acc17..583e97b7e944 100644 --- a/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dnd.js +++ b/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dnd.js @@ -15,8 +15,7 @@ define([ ], function (ko, $, _, Element) { 'use strict'; - var transformProp, - isTouchDevice = typeof document.ontouchstart !== 'undefined'; + var transformProp; /** * Get element context @@ -110,11 +109,7 @@ define([ * @param {Object} data - element data */ initListeners: function (elem, data) { - if (isTouchDevice) { - $(elem).on('touchstart', this.mousedownHandler.bind(this, data, elem)); - } else { - $(elem).on('mousedown', this.mousedownHandler.bind(this, data, elem)); - } + $(elem).on('mousedown touchstart', this.mousedownHandler.bind(this, data, elem)); }, /** @@ -131,26 +126,20 @@ define([ $table = $(elem).parents('table').eq(0), $tableWrapper = $table.parent(); + this.disableScroll(); $(recordNode).addClass(this.draggableElementClass); $(originRecord).addClass(this.draggableElementClass); this.step = this.step === 'auto' ? originRecord.height() / 2 : this.step; drEl.originRow = originRecord; drEl.instance = recordNode = this.processingStyles(recordNode, elem); drEl.instanceCtx = this.getRecord(originRecord[0]); - drEl.eventMousedownY = isTouchDevice ? event.originalEvent.touches[0].pageY : event.pageY; + drEl.eventMousedownY = this.getPageY(event); drEl.minYpos = $table.offset().top - originRecord.offset().top + $table.children('thead').outerHeight(); drEl.maxYpos = drEl.minYpos + $table.children('tbody').outerHeight() - originRecord.outerHeight(); $tableWrapper.append(recordNode); - - if (isTouchDevice) { - this.body.bind('touchmove', this.mousemoveHandler); - this.body.bind('touchend', this.mouseupHandler); - } else { - this.body.bind('mousemove', this.mousemoveHandler); - this.body.bind('mouseup', this.mouseupHandler); - } - + this.body.bind('mousemove touchmove', this.mousemoveHandler); + this.body.bind('mouseup touchend', this.mouseupHandler); }, /** @@ -160,16 +149,13 @@ define([ */ mousemoveHandler: function (event) { var depEl = this.draggableElement, - pageY = isTouchDevice ? event.originalEvent.touches[0].pageY : event.pageY, + pageY = this.getPageY(event), positionY = pageY - depEl.eventMousedownY, processingPositionY = positionY + 'px', processingMaxYpos = depEl.maxYpos + 'px', processingMinYpos = depEl.minYpos + 'px', depElement = this.getDepElement(depEl.instance, positionY, depEl.originRow); - event.stopPropagation(); - event.preventDefault(); - if (depElement) { depEl.depElement ? depEl.depElement.elem.removeClass(depEl.depElement.className) : false; depEl.depElement = depElement; @@ -194,9 +180,10 @@ define([ mouseupHandler: function (event) { var depElementCtx, drEl = this.draggableElement, - pageY = isTouchDevice ? event.originalEvent.touches[0].pageY : event.pageY, + pageY = this.getPageY(event), positionY = pageY - drEl.eventMousedownY; + this.enableScroll(); drEl.depElement = this.getDepElement(drEl.instance, positionY, this.draggableElement.originRow); drEl.instance.remove(); @@ -212,13 +199,8 @@ define([ drEl.originRow.removeClass(this.draggableElementClass); - if (isTouchDevice) { - this.body.unbind('touchmove', this.mousemoveHandler); - this.body.unbind('touchend', this.mouseupHandler); - } else { - this.body.unbind('mousemove', this.mousemoveHandler); - this.body.unbind('mouseup', this.mouseupHandler); - } + this.body.unbind('mousemove touchmove', this.mousemoveHandler); + this.body.unbind('mouseup touchend', this.mouseupHandler); this.draggableElement = {}; }, @@ -402,6 +384,55 @@ define([ index = _.isFunction(ctx.$index) ? ctx.$index() : ctx.$index; return this.recordsCache()[index]; + }, + + /** + * Get correct page Y + * + * @param {Object} event - current event + * @returns {integer} + */ + getPageY: function (event) { + var pageY; + + if (event.type.indexOf('touch') >= 0) { + if (event.originalEvent.touches[0]) { + pageY = event.originalEvent.touches[0].pageY; + } else { + pageY = event.originalEvent.changedTouches[0].pageY; + } + } else { + pageY = event.pageY; + } + + return pageY; + }, + + /** + * Disable page scrolling + */ + disableScroll: function () { + document.body.addEventListener('touchmove', this.preventDefault, { + passive: false + }); + }, + + /** + * Enable page scrolling + */ + enableScroll: function () { + document.body.removeEventListener('touchmove', this.preventDefault, { + passive: false + }); + }, + + /** + * Prevent default function + * + * @param {Object} event - event object + */ + preventDefault: function (event) { + event.preventDefault(); } }); diff --git a/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dynamic-rows-grid.js b/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dynamic-rows-grid.js index dc6f8d930a14..17b2d1db4eb1 100644 --- a/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dynamic-rows-grid.js +++ b/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dynamic-rows-grid.js @@ -33,6 +33,15 @@ define([ } }, + /** + * @inheritdoc + */ + initialize: function () { + this.setToInsertData = _.debounce(this.setToInsertData, 200); + + return this._super(); + }, + /** * Calls 'initObservable' of parent * diff --git a/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dynamic-rows.js b/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dynamic-rows.js index 1d52fc78d7a8..cbbfbdb127ad 100644 --- a/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dynamic-rows.js +++ b/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dynamic-rows.js @@ -543,6 +543,7 @@ define([ data = this.createHeaderTemplate(cell.config); cell.config.labelVisible = false; _.extend(data, { + defaultLabelVisible: data.visible(), label: cell.config.label, name: cell.name, required: !!cell.config.validation, diff --git a/app/code/Magento/Ui/view/base/web/js/dynamic-rows/record.js b/app/code/Magento/Ui/view/base/web/js/dynamic-rows/record.js index 54309ca06851..9a9d47890477 100644 --- a/app/code/Magento/Ui/view/base/web/js/dynamic-rows/record.js +++ b/app/code/Magento/Ui/view/base/web/js/dynamic-rows/record.js @@ -25,7 +25,7 @@ define([ }, listens: { position: 'initPosition', - elems: 'setColumnVisibileListener' + elems: 'setColumnVisibleListener' }, links: { position: '${ $.name }.${ $.positionProvider }:value' @@ -123,7 +123,7 @@ define([ /** * Set column visibility listener */ - setColumnVisibileListener: function () { + setColumnVisibleListener: function () { var elem = _.find(this.elems(), function (curElem) { return !curElem.hasOwnProperty('visibleListener'); }); @@ -245,7 +245,7 @@ define([ label = _.findWhere(this.parentComponent().labels(), { name: index }); - label.visible() !== state ? label.visible(state) : false; + label.defaultLabelVisible && label.visible(state); } else { elems[curElem].visible(state); } diff --git a/app/code/Magento/Ui/view/base/web/js/form/components/area.js b/app/code/Magento/Ui/view/base/web/js/form/components/area.js index 5967b8c57b5d..741e6d306502 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/components/area.js +++ b/app/code/Magento/Ui/view/base/web/js/form/components/area.js @@ -44,7 +44,7 @@ define([ /** * Calls parent's initElement method. - * Assignes callbacks on various events of incoming element. + * Assigns callbacks on various events of incoming element. * @param {Object} elem * @return {Object} - reference to instance */ 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 df85af5824d9..bfc2eb2b8852 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/components/button.js +++ b/app/code/Magento/Ui/view/base/web/js/form/components/button.js @@ -45,7 +45,8 @@ define([ .observe([ 'visible', 'disabled', - 'title' + 'title', + 'childError' ]); }, diff --git a/app/code/Magento/Ui/view/base/web/js/form/components/collection.js b/app/code/Magento/Ui/view/base/web/js/form/components/collection.js index 2c12486ceb51..c31963e98627 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/components/collection.js +++ b/app/code/Magento/Ui/view/base/web/js/form/components/collection.js @@ -91,7 +91,7 @@ define([ }, /** - * Returnes true if current set of items differ from initial one, + * Returns true if current set of items differ from initial one, * or if some child has been changed. * * @returns {Boolean} @@ -153,6 +153,7 @@ define([ * Creates function that removes element * from collection using '_removeChild' method. * @param {Object} elem - Element that should be removed. + * @deprecated Not used anymore */ removeAddress: function (elem) { var self = this; @@ -169,7 +170,7 @@ define([ }, /** - * Removes elememt from both collection and data storage, + * Removes element from both collection and data storage, * activates first element if removed one was active, * triggers 'update' event. * diff --git a/app/code/Magento/Ui/view/base/web/js/form/components/collection/item.js b/app/code/Magento/Ui/view/base/web/js/form/components/collection/item.js index c2a65371471c..045c25ab7911 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/components/collection/item.js +++ b/app/code/Magento/Ui/view/base/web/js/form/components/collection/item.js @@ -19,7 +19,7 @@ define([ }; /** - * Parses incoming data and returnes result merged with default preview config + * Parses incoming data and returns result merged with default preview config * * @param {Object|String} data * @return {Object} diff --git a/app/code/Magento/Ui/view/base/web/js/form/components/fieldset.js b/app/code/Magento/Ui/view/base/web/js/form/components/fieldset.js index 6d33386fa1f1..9ee387e0e6a7 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/components/fieldset.js +++ b/app/code/Magento/Ui/view/base/web/js/form/components/fieldset.js @@ -68,7 +68,7 @@ define([ /** * Calls parent's initElement method. - * Assignes callbacks on various events of incoming element. + * Assigns callbacks on various events of incoming element. * * @param {Object} elem * @return {Object} - reference to instance @@ -162,6 +162,10 @@ define([ } this.error(hasErrors || message); + + if (hasErrors || message) { + this.open(); + } }, /** diff --git a/app/code/Magento/Ui/view/base/web/js/form/components/insert.js b/app/code/Magento/Ui/view/base/web/js/form/components/insert.js index 26ad9fbfec01..d8cbcc9cc173 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/components/insert.js +++ b/app/code/Magento/Ui/view/base/web/js/form/components/insert.js @@ -237,10 +237,21 @@ define([ * @param {*} data */ onRender: function (data) { + var resp; + this.loading(false); - this.set('content', data); - this.isRendered = true; - this.startRender = false; + + try { + resp = JSON.parse(data); + + if (resp.ajaxExpired) { + window.location.href = resp.ajaxRedirect; + } + } catch (e) { + this.set('content', data); + this.isRendered = true; + this.startRender = false; + } }, /** diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/abstract.js b/app/code/Magento/Ui/view/base/web/js/form/element/abstract.js index 8f1a75d2be0d..ca3d383accca 100755 --- a/app/code/Magento/Ui/view/base/web/js/form/element/abstract.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/abstract.js @@ -57,6 +57,9 @@ define([ '${ $.provider }:${ $.customScope ? $.customScope + "." : ""}data.validate': 'validate', 'isUseDefault': 'toggleUseDefault' }, + ignoreTmpls: { + value: true + }, links: { value: '${ $.provider }:${ $.dataScope }' @@ -405,6 +408,7 @@ define([ isValid = this.disabled() || !this.visible() || result.passed; this.error(message); + this.error.valueHasMutated(); this.bubble('error', message); //TODO: Implement proper result propagation for form diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/checkbox-set.js b/app/code/Magento/Ui/view/base/web/js/form/element/checkbox-set.js index d0c8c13edff5..254585a62491 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/checkbox-set.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/checkbox-set.js @@ -124,7 +124,7 @@ define([ }, /** - * Returns option object assoctiated with provided value. + * Returns option object associated with provided value. * * @param {String} value * @returns {Object} diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/country.js b/app/code/Magento/Ui/view/base/web/js/form/element/country.js index f64a80bf535e..c75301018e19 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/country.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/country.js @@ -49,7 +49,7 @@ define([ if (!this.value()) { defaultCountry = _.filter(result, function (item) { - return item['is_default'] && item['is_default'].includes(value); + return item['is_default'] && _.contains(item['is_default'], value); }); if (defaultCountry.length) { 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 a5eb7d5d1f57..4e532c9d48cc 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 6492d0821fc2..f28569caa005 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'; @@ -324,7 +325,7 @@ define([ /** * Handler which is invoked when files are choosed for upload. - * May be used for implementation of aditional validation rules, + * May be used for implementation of additional validation rules, * e.g. total files and a total size rules. * * @param {Event} e - Event object. @@ -348,6 +349,12 @@ define([ allowed = this.isFileAllowed(file), target = $(e.target); + if (this.disabled()) { + this.notifyError($t('The file upload field is disabled.')); + + return; + } + if (allowed.passed) { target.on('fileuploadsend', function (event, postData) { postData.data.append('param_name', this.paramName); diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/image-uploader.js b/app/code/Magento/Ui/view/base/web/js/form/element/image-uploader.js index 0ae09f14fa94..b490ac557e71 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/image-uploader.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/image-uploader.js @@ -11,8 +11,7 @@ define([ 'Magento_Ui/js/modal/alert', 'Magento_Ui/js/lib/validation/validator', 'Magento_Ui/js/form/element/file-uploader', - 'mage/adminhtml/browser', - 'mage/adminhtml/tools' + 'mage/adminhtml/browser' ], function ($, _, utils, uiAlert, validator, Element, browser) { 'use strict'; diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/post-code.js b/app/code/Magento/Ui/view/base/web/js/form/element/post-code.js index 911574a0fb43..0eaacdc32567 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/post-code.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/post-code.js @@ -20,13 +20,33 @@ define([ } }, + /** + * Initializes observable properties of instance + * + * @returns {Abstract} Chainable. + */ + initObservable: function () { + this._super(); + + /** + * equalityComparer function + * + * @returns boolean. + */ + this.value.equalityComparer = function (oldValue, newValue) { + return !oldValue && !newValue || oldValue === newValue; + }; + + return this; + }, + /** * @param {String} value */ update: function (value) { var country = registry.get(this.parentName + '.' + 'country_id'), options = country.indexedOptions, - option; + option = null; if (!value) { return; @@ -34,6 +54,10 @@ define([ option = options[value]; + if (!option) { + return; + } + if (option['is_zipcode_optional']) { this.error(false); this.validation = _.omit(this.validation, 'required-entry'); diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/region.js b/app/code/Magento/Ui/view/base/web/js/form/element/region.js index 45d38b339b50..f6eafcf49284 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/region.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/region.js @@ -78,13 +78,12 @@ define([ * @param {String} field */ filter: function (value, field) { - var country = registry.get(this.parentName + '.' + 'country_id'), - option; + var superFn = this._super; - if (country) { - option = country.indexedOptions[value]; + registry.get(this.parentName + '.' + 'country_id', function (country) { + var option = country.indexedOptions[value]; - this._super(value, field); + superFn.call(this, value, field); if (option && option['is_region_visible'] === false) { // hide select and corresponding text input field if region must not be shown for selected country @@ -94,7 +93,7 @@ define([ this.toggleInput(false); } } - } + }.bind(this)); } }); }); diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/ui-select.js b/app/code/Magento/Ui/view/base/web/js/form/element/ui-select.js index dba0992c5ba5..4479cff5135d 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/ui-select.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/ui-select.js @@ -131,6 +131,7 @@ define([ return Abstract.extend({ defaults: { options: [], + total: 0, listVisible: false, value: [], filterOptions: false, @@ -153,6 +154,7 @@ define([ labelsDecoration: false, disableLabel: false, filterRateLimit: 500, + filterRateLimitMethod: 'notifyAtFixedRate', closeBtnLabel: $t('Done'), optgroupTmpl: 'ui/grid/filters/elements/ui-select-optgroup', quantityPlaceholder: $t('options'), @@ -180,6 +182,7 @@ define([ debounce: 300, missingValuePlaceholder: $t('Entity with ID: %s doesn\'t exist'), isDisplayMissingValuePlaceholder: false, + currentSearchKey: '', listens: { listVisible: 'cleanHoveredElement', filterInputValue: 'filterOptionsList', @@ -330,7 +333,10 @@ define([ ]); this.filterInputValue.extend({ - rateLimit: this.filterRateLimit + rateLimit: { + timeout: this.filterRateLimit, + method: this.filterRateLimitMethod + } }); return this; @@ -460,7 +466,7 @@ define([ } if (this.searchOptions) { - return _.debounce(this.loadOptions.bind(this, value), this.debounce)(); + return this.loadOptions(value); } this.cleanHoveredElement(); @@ -547,11 +553,21 @@ define([ _setItemsQuantity: function (data) { if (this.showFilteredQuantity) { data || parseInt(data, 10) === 0 ? - this.itemsQuantity(data + ' ' + this.quantityPlaceholder) : + this.itemsQuantity(this.getItemsPlaceholder(data)) : this.itemsQuantity(''); } }, + /** + * Return formatted items placeholder. + * + * @param {Object} data - option data + * @returns {String} + */ + getItemsPlaceholder: function (data) { + return data + ' ' + this.quantityPlaceholder; + }, + /** * Remove element from selected array */ @@ -1234,13 +1250,11 @@ define([ * @param {Number} page */ processRequest: function (searchKey, page) { - var total = 0, - existingOptions = this.options(); - this.loading(true); + this.currentSearchKey = searchKey; $.ajax({ url: this.searchUrl, - type: 'post', + type: 'get', dataType: 'json', context: this, data: { @@ -1248,27 +1262,39 @@ define([ page: page, limit: this.pageLimit }, + success: $.proxy(this.success, this), + error: $.proxy(this.error, this), + beforeSend: $.proxy(this.beforeSend, this), + complete: $.proxy(this.complete, this, searchKey, page) + }); + }, - /** @param {Object} response */ - success: function (response) { - _.each(response.options, function (opt) { - existingOptions.push(opt); - }); - total = response.total; - this.options(existingOptions); - }, - - /** set empty array if error occurs */ - error: function () { - this.options([]); - }, + /** @param {Object} response */ + success: function (response) { + var existingOptions = this.options(); - /** cache options and stop loading*/ - complete: function () { - this.setCachedSearchResults(searchKey, this.options(), page, total); - this.afterLoadOptions(searchKey, page, total); - } + _.each(response.options, function (opt) { + existingOptions.push(opt); }); + + this.total = response.total; + this.options(existingOptions); + }, + + /** add actions before ajax request */ + beforeSend: function () { + + }, + + /** set empty array if error occurs */ + error: function () { + this.options([]); + }, + + /** cache options and stop loading*/ + complete: function (searchKey, page) { + this.setCachedSearchResults(searchKey, this.options(), page, this.total); + this.afterLoadOptions(searchKey, page, this.total); }, /** @@ -1279,9 +1305,9 @@ define([ * @param {Number} total */ afterLoadOptions: function (searchKey, page, total) { - this._setItemsQuantity(total); - this.lastSearchPage = page; this.lastSearchKey = searchKey; + this.lastSearchPage = page; + this._setItemsQuantity(total); this.loading(false); } }); diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/wysiwyg.js b/app/code/Magento/Ui/view/base/web/js/form/element/wysiwyg.js index de899fd8f2ef..ce19899cd12c 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/wysiwyg.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/wysiwyg.js @@ -18,9 +18,10 @@ define([ 'use strict'; return Abstract.extend({ + currentWysiwyg: undefined, defaults: { elementSelector: 'textarea', - value: '', + suffixRegExpPattern: '${ $.wysiwygUniqueSuffix }', $wysiwygEditorButton: '', links: { value: '${ $.provider }:${ $.dataScope }' @@ -53,6 +54,10 @@ define([ // disable editor completely after initialization is field is disabled varienGlobalEvents.attachEventHandler('wysiwygEditorInitialized', function () { + if (!_.isUndefined(window.tinyMceEditors)) { + this.currentWysiwyg = window.tinyMceEditors[this.wysiwygId]; + } + if (this.disabled()) { this.setDisabled(true); } @@ -61,6 +66,34 @@ define([ return this; }, + /** @inheritdoc */ + initConfig: function (config) { + var pattern = config.suffixRegExpPattern || this.constructor.defaults.suffixRegExpPattern; + + pattern = pattern.replace(/\$/g, '\\$&'); + config.content = config.content.replace(new RegExp(pattern, 'g'), this.getUniqueSuffix(config)); + this._super(); + + return this; + }, + + /** + * Build unique id based on name, underscore separated. + * + * @param {Object} config + */ + getUniqueSuffix: function (config) { + return config.name.replace(/(\.|-)/g, '_'); + }, + + /** + * @inheritdoc + */ + destroy: function () { + this._super(); + wysiwyg.removeEvents(this.wysiwygId); + }, + /** * * @returns {exports} @@ -108,14 +141,9 @@ define([ } /* eslint-disable no-undef */ - if (typeof wysiwyg !== 'undefined' && wysiwyg.activeEditor()) { - if (wysiwyg && disabled) { - wysiwyg.setEnabledStatus(false); - wysiwyg.getPluginButtons().prop('disabled', 'disabled'); - } else if (wysiwyg) { - wysiwyg.setEnabledStatus(true); - wysiwyg.getPluginButtons().removeProp('disabled'); - } + if (!_.isUndefined(this.currentWysiwyg) && this.currentWysiwyg.activeEditor()) { + this.currentWysiwyg.setEnabledStatus(!disabled); + this.currentWysiwyg.getPluginButtons().prop('disabled', disabled); } } }); diff --git a/app/code/Magento/Ui/view/base/web/js/form/provider.js b/app/code/Magento/Ui/view/base/web/js/form/provider.js index d070e23c708b..49765f589bd9 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/provider.js +++ b/app/code/Magento/Ui/view/base/web/js/form/provider.js @@ -82,7 +82,7 @@ define([ }, /** - * Set data to provder based on current data. + * Set data to provider based on current data. * * @param {Object} oldData * @param {Object} newData diff --git a/app/code/Magento/Ui/view/base/web/js/grid/columns/actions.js b/app/code/Magento/Ui/view/base/web/js/grid/columns/actions.js index be7a1a13fbd6..c75f7797cf0f 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/columns/actions.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/columns/actions.js @@ -11,8 +11,9 @@ define([ 'mageUtils', 'uiRegistry', './column', - 'Magento_Ui/js/modal/confirm' -], function (_, utils, registry, Column, confirm) { + 'Magento_Ui/js/modal/confirm', + 'mage/dataPost' +], function (_, utils, registry, Column, confirm, dataPost) { 'use strict'; return Column.extend({ @@ -267,7 +268,14 @@ define([ * @param {Object} action - Action's data. */ defaultCallback: function (actionIndex, recordId, action) { - window.location.href = action.href; + if (action.post) { + dataPost().postData({ + action: action.href, + data: {} + }); + } else { + window.location.href = action.href; + } }, /** diff --git a/app/code/Magento/Ui/view/base/web/js/grid/controls/bookmarks/bookmarks.js b/app/code/Magento/Ui/view/base/web/js/grid/controls/bookmarks/bookmarks.js index 076465351cde..cfcd37a65b8c 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/controls/bookmarks/bookmarks.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/controls/bookmarks/bookmarks.js @@ -382,7 +382,7 @@ define([ * Checks if specified view is in editing state. * * @param {String} index - Index of a view to be checked. - * @returns {Bollean} + * @returns {Boolean} */ isEditing: function (index) { return this.editing === index; diff --git a/app/code/Magento/Ui/view/base/web/js/grid/data-storage.js b/app/code/Magento/Ui/view/base/web/js/grid/data-storage.js index dad67da3ea8a..547cdab16cdf 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/data-storage.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/data-storage.js @@ -199,7 +199,7 @@ define([ }, /** - * Caches requests object with provdided parameters + * Caches requests object with provided parameters * and data object associated with it. * * @param {Object} data - Data associated with request. diff --git a/app/code/Magento/Ui/view/base/web/js/grid/dnd.js b/app/code/Magento/Ui/view/base/web/js/grid/dnd.js index 5a71fca673ec..660e3cb3bd2b 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/dnd.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/dnd.js @@ -406,7 +406,7 @@ define([ * * @param {Number} x - X coordinate of a grabbed point. * @param {Number} y - Y coordinate of a grabbed point. - * @param {HTMLElement} elem - Grabbed elemenet. + * @param {HTMLElement} elem - Grabbed element. */ grab: function (x, y, elem) { this.initDrag = true; diff --git a/app/code/Magento/Ui/view/base/web/js/grid/editing/client.js b/app/code/Magento/Ui/view/base/web/js/grid/editing/client.js index f68a6f97d964..ca82ff81d3b6 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/editing/client.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/editing/client.js @@ -54,7 +54,7 @@ define([ /** * Proxy save method which might invoke - * data valiation prior to its' saving. + * data validation prior to its' saving. * * @param {Object} data - Data to be processed. * @returns {jQueryPromise} @@ -128,7 +128,7 @@ define([ /** * Handles ajax success callback. * - * @param {jQueryPromise} promise - Promise to be resoloved. + * @param {jQueryPromise} promise - Promise to be resolved. * @param {*} data - See 'jquery' ajax success callback. */ onSuccess: function (promise, data) { diff --git a/app/code/Magento/Ui/view/base/web/js/grid/editing/editor.js b/app/code/Magento/Ui/view/base/web/js/grid/editing/editor.js index 589aed4c9837..ece49cc8fe27 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/editing/editor.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/editing/editor.js @@ -164,7 +164,7 @@ define([ }, /** - * Adds listeners on a new recrod. + * Adds listeners on a new record. * * @param {Record} record * @returns {Editor} Chainable. @@ -208,7 +208,7 @@ define([ }, /** - * Starts editing of a specfied record. If records' + * Starts editing of a specified record. If records' * instance doesn't exist, than it will be created. * * @param {(Number|String)} id - See 'getId' method. @@ -357,7 +357,7 @@ define([ /** * Resets specific records' data - * to the data present in asscotiated row. + * to the data present in associated row. * * @param {(Number|String)} id - See 'getId' method. * @param {Boolean} [isIndex=false] - See 'getId' method. @@ -401,9 +401,9 @@ define([ }, /** - * Disables editing of specfied fields. + * Disables editing of specified fields. * - * @param {Array} fields - An array of fields indeces to be disabled. + * @param {Array} fields - An array of fields indexes to be disabled. * @returns {Editor} Chainable. */ disableFields: function (fields) { diff --git a/app/code/Magento/Ui/view/base/web/js/grid/editing/record.js b/app/code/Magento/Ui/view/base/web/js/grid/editing/record.js index 18b383611314..9b8998368c5f 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/editing/record.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/editing/record.js @@ -50,6 +50,9 @@ define([ } } }, + ignoreTmpls: { + data: true + }, listens: { elems: 'updateFields', data: 'updateState' @@ -138,7 +141,7 @@ define([ }, /** - * Creates fields for the specfied columns. + * Creates fields for the specified columns. * * @param {Array} columns - An array of column instances. * @returns {Record} Chainable. @@ -261,7 +264,7 @@ define([ /** * Validates all of the available fields. * - * @returns {Array} An array with validatation results. + * @returns {Array} An array with validation results. */ validate: function () { return this.elems.map('validate'); @@ -277,7 +280,7 @@ define([ }, /** - * Counts total errors ammount accros all fields. + * Counts total errors amount across all fields. * * @returns {Number} */ @@ -303,7 +306,7 @@ define([ }, /** - * Updates 'fields' array filling it with available edtiors + * Updates 'fields' array filling it with available editors * or with column instances if associated field is not present. * * @returns {Record} Chainable. diff --git a/app/code/Magento/Ui/view/base/web/js/grid/massactions.js b/app/code/Magento/Ui/view/base/web/js/grid/massactions.js index 48c04458ff49..3626f5280688 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/massactions.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/massactions.js @@ -95,7 +95,7 @@ define([ }, /** - * Adds new action. If action with a specfied identifier + * Adds new action. If action with a specified identifier * already exists, than the original one will be overrided. * * @param {Object} action - Action object. @@ -153,6 +153,11 @@ define([ var itemsType = data.excludeMode ? 'excluded' : 'selected', selections = {}; + if (itemsType === 'excluded' && data.selected && data.selected.length) { + itemsType = 'selected'; + data[itemsType] = _.difference(data.selected, data.excluded); + } + selections[itemsType] = data[itemsType]; if (!selections[itemsType].length) { diff --git a/app/code/Magento/Ui/view/base/web/js/grid/paging/paging.js b/app/code/Magento/Ui/view/base/web/js/grid/paging/paging.js index abd79e797e41..8e6f1496495c 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/paging/paging.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/paging/paging.js @@ -19,7 +19,9 @@ define([ defaults: { template: 'ui/grid/paging/paging', totalTmpl: 'ui/grid/paging-total', + totalRecords: 0, pageSize: 20, + pages: 1, current: 1, selectProvider: 'ns = ${ $.ns }, index = ids', @@ -35,7 +37,8 @@ define([ imports: { pageSize: '${ $.sizesConfig.name }:value', totalSelected: '${ $.selectProvider }:totalSelected', - totalRecords: '${ $.provider }:data.totalRecords' + totalRecords: '${ $.provider }:data.totalRecords', + filters: '${ $.provider }:params.filters' }, exports: { @@ -43,6 +46,11 @@ define([ current: '${ $.provider }:params.paging.current' }, + statefull: { + pageSize: true, + current: true + }, + listens: { 'pages': 'onPagesChange', 'pageSize': 'onPageSizeChange', @@ -173,7 +181,9 @@ define([ * @returns {Paging} Chainable. */ goFirst: function () { - this.current = 1; + if (!_.isUndefined(this.filters)) { + this.current = 1; + } return this; }, @@ -219,13 +229,11 @@ define([ /** * Calculates new page cursor based on the * previous and current page size values. - * - * @returns {Number} Updated cursor value. */ updateCursor: function () { var cursor = this.current - 1, size = this.pageSize, - oldSize = this.previousSize, + oldSize = _.isUndefined(this.previousSize) ? this.pageSize : this.previousSize, delta = cursor * (oldSize - size) / size; delta = size > oldSize ? diff --git a/app/code/Magento/Ui/view/base/web/js/grid/resize.js b/app/code/Magento/Ui/view/base/web/js/grid/resize.js index 50781c9afd8c..792c997f060c 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/resize.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/resize.js @@ -624,7 +624,7 @@ define([ }, /** - * Generate index that will indentify context + * Generate index that will identify context * * @param {Object} ctx * @return {String} diff --git a/app/code/Magento/Ui/view/base/web/js/grid/search/search.js b/app/code/Magento/Ui/view/base/web/js/grid/search/search.js index fa445a2577ad..ce53b23b79e1 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/search/search.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/search/search.js @@ -11,14 +11,15 @@ define([ 'uiLayout', 'mage/translate', 'mageUtils', - 'uiElement' -], function (_, layout, $t, utils, Element) { + 'uiElement', + 'jquery' +], function (_, layout, $t, utils, Element, $) { 'use strict'; return Element.extend({ defaults: { template: 'ui/grid/search/search', - placeholder: $t('Search by keyword'), + placeholder: 'Search by keyword', label: $t('Keyword'), value: '', previews: [], @@ -29,11 +30,13 @@ define([ tracks: { value: true, previews: true, - inputValue: true + inputValue: true, + focused: true }, imports: { inputValue: 'value', - updatePreview: 'value' + updatePreview: 'value', + focused: false }, exports: { value: '${ $.provider }:params.search' @@ -88,6 +91,18 @@ define([ return this; }, + /** + * Click To ScrollTop. + */ + scrollTo: function ($data) { + $('html, body').animate({ + scrollTop: 0 + }, 'slow', function () { + $data.focused = false; + $data.focused = true; + }); + }, + /** * Resets input value to the last applied state. * @@ -102,7 +117,7 @@ define([ /** * Applies search query. * - * @param {String} [value=inputValue] - If not specfied, then + * @param {String} [value=inputValue] - If not specified, then * value of the input field will be used. * @returns {Search} Chainable. */ diff --git a/app/code/Magento/Ui/view/base/web/js/grid/toolbar.js b/app/code/Magento/Ui/view/base/web/js/grid/toolbar.js index 8a17e3ed46f2..fd2d09c6a65f 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/toolbar.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/toolbar.js @@ -557,7 +557,7 @@ define([ }, /** - * Handles changes of windows' top scroll postion. + * Handles changes of windows' top scroll position. */ onWindowScrollTop: function () { this.updateTableOffset() diff --git a/app/code/Magento/Ui/view/base/web/js/lib/core/collection.js b/app/code/Magento/Ui/view/base/web/js/lib/core/collection.js index 607d217837fc..0491390d2b6c 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/core/collection.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/core/collection.js @@ -15,7 +15,7 @@ define([ 'use strict'; /** - * Removes non plain object items from the specfied array. + * Removes non plain object items from the specified array. * * @param {Array} container - Array whose value should be filtered. * @returns {Array} @@ -310,7 +310,7 @@ define([ * @private * * @param {Array} args - An array of arguments to pass to the next delegation call. - * @returns {Array} An array of delegation resutls. + * @returns {Array} An array of delegation results. */ _delegate: function (args) { var result; diff --git a/app/code/Magento/Ui/view/base/web/js/lib/core/storage/local.js b/app/code/Magento/Ui/view/base/web/js/lib/core/storage/local.js index 34ceced2c687..adeb510ab3e4 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/core/storage/local.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/core/storage/local.js @@ -51,7 +51,7 @@ define([ }, /** - * Retrieves specfied item. + * Retrieves specified item. * * @param {String} key - Key of the property to be retrieved. */ @@ -60,7 +60,7 @@ define([ }, /** - * Removes specfied item. + * Removes specified item. * * @param {String} key - Key of the property to be removed. */ @@ -79,7 +79,7 @@ define([ /** * Extracts and parses data stored in localStorage by the - * key specified in 'root' varaible. + * key specified in 'root' variable. * * @returns {Object} */ @@ -114,8 +114,8 @@ define([ * * @param {String} path - Path to the property. * - * @example Retrieveing data. - * localStoarge => + * @example Retrieving data. + * localStorage => * 'appData' => ' * "one": {"two": "three"} * ' @@ -139,7 +139,7 @@ define([ * * @example Setting data. * storage.set('one.two', 'four'); - * => localStoarge => + * => localStorage => * 'appData' => ' * "one": {"two": "four"} * ' @@ -159,7 +159,7 @@ define([ * * @example Removing data. * storage.remove('one.two', 'four'); - * => localStoarge => + * => localStorage => * 'appData' => ' * "one": {} * ' diff --git a/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/bootstrap.js b/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/bootstrap.js index 4518db598b4d..83d43d2691b3 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/bootstrap.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/bootstrap.js @@ -26,7 +26,7 @@ define(function (require) { mageInit: require('./mage-init'), keyboard: require('./keyboard'), optgroup: require('./optgroup'), - aferRender: require('./after-render'), + afterRender: require('./after-render'), autoselect: require('./autoselect'), datepicker: require('./datepicker'), outerClick: require('./outer_click'), diff --git a/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/outer_click.js b/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/outer_click.js index cc9dc5e0a220..98e86de23221 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/outer_click.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/outer_click.js @@ -42,7 +42,7 @@ define([ /** * Document click handler which in case if event target is not * a descendant of provided container element, - * invokes specfied in configuration callback. + * invokes specified in configuration callback. * * @param {HTMLElement} container * @param {Object} config diff --git a/app/code/Magento/Ui/view/base/web/js/lib/knockout/extender/bound-nodes.js b/app/code/Magento/Ui/view/base/web/js/lib/knockout/extender/bound-nodes.js index a332b595bdf3..6b3c437b9050 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/knockout/extender/bound-nodes.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/knockout/extender/bound-nodes.js @@ -109,7 +109,7 @@ define([ wrapper.extend(ko, { /** - * Extends kncokouts' 'applyBindings' + * Extends knockouts' 'applyBindings' * to track nodes associated with model. * * @param {Function} orig - Original 'applyBindings' method. @@ -136,7 +136,7 @@ define([ }, /** - * Extends kncokouts' cleanNode + * Extends knockouts' cleanNode * to track nodes associated with model. * * @param {Function} orig - Original 'cleanNode' method. diff --git a/app/code/Magento/Ui/view/base/web/js/lib/knockout/template/observable_source.js b/app/code/Magento/Ui/view/base/web/js/lib/knockout/template/observable_source.js index 98ffd83b5646..916666d70d1b 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/knockout/template/observable_source.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/knockout/template/observable_source.js @@ -29,7 +29,7 @@ define([ * Else, writes into it. * @param {String} key - key to write to or to read from * @param {*} value - * @return {*} - if 1 arg provided, returnes _data[key] property + * @return {*} - if 1 arg provided, Returns _data[key] property */ data: function (key, value) { if (arguments.length === 1) { diff --git a/app/code/Magento/Ui/view/base/web/js/lib/knockout/template/renderer.js b/app/code/Magento/Ui/view/base/web/js/lib/knockout/template/renderer.js index 2e0c53373f80..2cfd96161924 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/knockout/template/renderer.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/knockout/template/renderer.js @@ -519,7 +519,7 @@ define([ }, /** - * Custom 'render' attrobute handler function. Wraps child elements + * Custom 'render' attribute handler function. Wraps child elements * of a node with knockout's 'ko template:' comment tag. * * @param {HTMLElement} node - Element to be processed. diff --git a/app/code/Magento/Ui/view/base/web/js/lib/logger/logger-utils.js b/app/code/Magento/Ui/view/base/web/js/lib/logger/logger-utils.js index fe83f600132e..bf7ae0cdc3e9 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/logger/logger-utils.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/logger/logger-utils.js @@ -42,14 +42,14 @@ define([], function () { * Method that creates object of messages * @param {String} requested - log message that showing that request for class is started * @param {String} loaded - log message that show when requested class is loaded - * @param {String} failded - log message that show when requested class is failed + * @param {String} failed - log message that show when requested class is failed * @returns {Object} */ - LogUtils.prototype.createMessages = function (requested, loaded, failded) { + LogUtils.prototype.createMessages = function (requested, loaded, failed) { return { requested: requested || '', loaded: loaded || '', - failed: failded || '' + failed: failed || '' }; }; @@ -57,14 +57,14 @@ define([], function () { * Method that creates object of log levels * @param {String} requested - log message that showing that request for class is started * @param {String} loaded - log message that show when requested class is loaded - * @param {String} failded - log message that show when requested class is failed + * @param {String} failed - log message that show when requested class is failed * @returns {Object} */ - LogUtils.prototype.createLevels = function (requested, loaded, failded) { + LogUtils.prototype.createLevels = function (requested, loaded, failed) { return { requested: requested || 'info', loaded: loaded || 'info', - failed: failded || 'warn' + failed: failed || 'warn' }; }; diff --git a/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js b/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js index 1103f3e54a4c..97b47f77beea 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js @@ -73,13 +73,13 @@ define([ ], 'max-words': [ function (value, params) { - return utils.stripHtml(value).match(/\b\w+\b/g).length < params; + return utils.isEmpty(value) || utils.stripHtml(value).match(/\b\w+\b/g).length < params; }, $.mage.__('Please enter {0} words or less.') ], 'min-words': [ function (value, params) { - return utils.stripHtml(value).match(/\b\w+\b/g).length >= params; + return utils.isEmpty(value) || utils.stripHtml(value).match(/\b\w+\b/g).length >= params; }, $.mage.__('Please enter at least {0} words.') ], @@ -87,49 +87,59 @@ define([ function (value, params) { var match = utils.stripHtml(value).match(/\b\w+\b/g) || []; - return match.length >= params[0] && + return utils.isEmpty(value) || match.length >= params[0] && match.length <= params[1]; }, $.mage.__('Please enter between {0} and {1} words.') ], 'letters-with-basic-punc': [ function (value) { - return /^[a-z\-.,()\u0027\u0022\s]+$/i.test(value); + return utils.isEmpty(value) || /^[a-z\-.,()\u0027\u0022\s]+$/i.test(value); }, $.mage.__('Letters or punctuation only please') ], 'alphanumeric': [ function (value) { - return /^\w+$/i.test(value); + return utils.isEmpty(value) || /^\w+$/i.test(value); }, $.mage.__('Letters, numbers, spaces or underscores only please') ], 'letters-only': [ function (value) { - return /^[a-z]+$/i.test(value); + return utils.isEmpty(value) || /^[a-z]+$/i.test(value); }, $.mage.__('Letters only please') ], 'no-whitespace': [ function (value) { - return /^\S+$/i.test(value); + return utils.isEmpty(value) || /^\S+$/i.test(value); }, $.mage.__('No white space please') ], + 'no-marginal-whitespace': [ + function (value) { + return !/^\s+|\s+$/i.test(value); + }, + $.mage.__('No marginal white space please') + ], 'zip-range': [ function (value) { - return /^90[2-5]-\d{2}-\d{4}$/.test(value); + return utils.isEmpty(value) || /^90[2-5]-\d{2}-\d{4}$/.test(value); }, $.mage.__('Your ZIP-code must be in the range 902xx-xxxx to 905-xx-xxxx') ], 'integer': [ function (value) { - return /^-?\d+$/.test(value); + return utils.isEmpty(value) || /^-?\d+$/.test(value); }, $.mage.__('A positive or negative non-decimal number please') ], 'vinUS': [ function (value) { + if (utils.isEmpty(value)) { + return true; + } + if (value.length !== 17) { return false; } @@ -215,13 +225,13 @@ define([ ], 'time': [ function (value) { - return /^([01]\d|2[0-3])(:[0-5]\d){0,2}$/.test(value); + return utils.isEmpty(value) || /^([01]\d|2[0-3])(:[0-5]\d){0,2}$/.test(value); }, $.mage.__('Please enter a valid time, between 00:00 and 23:59') ], 'time12h': [ function (value) { - return /^((0?[1-9]|1[012])(:[0-5]\d){0,2}(\s[AP]M))$/i.test(value); + return utils.isEmpty(value) || /^((0?[1-9]|1[012])(:[0-5]\d){0,2}(\s[AP]M))$/i.test(value); }, $.mage.__('Please enter a valid time, between 00:00 am and 12:00 pm') ], @@ -229,19 +239,21 @@ define([ function (value) { value = value.replace(/\s+/g, ''); - return value.length > 9 && value.match(/^(1-?)?(\([2-9]\d{2}\)|[2-9]\d{2})-?[2-9]\d{2}-?\d{4}$/); + return utils.isEmpty(value) || value.length > 9 && + value.match(/^(1-?)?(\([2-9]\d{2}\)|[2-9]\d{2})-?[2-9]\d{2}-?\d{4}$/); }, $.mage.__('Please specify a valid phone number') ], 'phoneUK': [ function (value) { - return value.length > 9 && value.match(/^(\(?(0|\+44)[1-9]{1}\d{1,4}?\)?\s?\d{3,4}\s?\d{3,4})$/); + return utils.isEmpty(value) || value.length > 9 && + value.match(/^(\(?(0|\+44)[1-9]{1}\d{1,4}?\)?\s?\d{3,4}\s?\d{3,4})$/); }, $.mage.__('Please specify a valid phone number') ], 'mobileUK': [ function (value) { - return value.length > 9 && value.match(/^((0|\+44)7\d{3}\s?\d{6})$/); + return utils.isEmpty(value) || value.length > 9 && value.match(/^((0|\+44)7\d{3}\s?\d{6})$/); }, $.mage.__('Please specify a valid mobile number') ], @@ -253,13 +265,13 @@ define([ ], 'email2': [ function (value) { - return /^((([a-z]|\d|[!#\$%&\u0027\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&\u0027\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\u0022)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\u0022)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)*(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/i.test(value);//eslint-disable-line max-len + return utils.isEmpty(value) || /^((([a-z]|\d|[!#\$%&\u0027\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&\u0027\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\u0022)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\u0022)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)*(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/i.test(value);//eslint-disable-line max-len }, $.validator.messages.email ], 'url2': [ function (value) { - return /^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&\u0027\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)*(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&\u0027\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&\u0027\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&\u0027\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&\u0027\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(value);//eslint-disable-line max-len + return utils.isEmpty(value) || /^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&\u0027\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)*(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&\u0027\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&\u0027\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&\u0027\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&\u0027\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(value);//eslint-disable-line max-len }, $.validator.messages.url ], @@ -267,6 +279,10 @@ define([ function (value, param) { var validTypes; + if (utils.isEmpty(value)) { + return true; + } + if (/[^0-9-]+/.test(value)) { return false; } @@ -351,19 +367,19 @@ define([ ], 'ipv4': [ function (value) { - return /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/i.test(value);//eslint-disable-line max-len + return utils.isEmpty(value) || /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/i.test(value);//eslint-disable-line max-len }, $.mage.__('Please enter a valid IP v4 address.') ], 'ipv6': [ function (value) { - return /^((([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(([0-9A-Fa-f]{1,4}:){0,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})|(::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){1,7}:))$/i.test(value);//eslint-disable-line max-len + return utils.isEmpty(value) || /^((([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(([0-9A-Fa-f]{1,4}:){0,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})|(::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){1,7}:))$/i.test(value);//eslint-disable-line max-len }, $.mage.__('Please enter a valid IP v6 address.') ], 'pattern': [ function (value, param) { - return new RegExp(param).test(value); + return utils.isEmpty(value) || new RegExp(param).test(value); }, $.mage.__('Invalid format.') ], @@ -676,6 +692,24 @@ define([ }, $.mage.__('The value is not within the specified range.') ], + 'validate-positive-percent-decimal': [ + function (value) { + var numValue; + + if (utils.isEmptyNoTrim(value) || !/^\s*-?\d*(\.\d*)?\s*$/.test(value)) { + return false; + } + + numValue = utils.parseNumber(value); + + if (isNaN(numValue)) { + return false; + } + + return utils.isBetween(numValue, 0.01, 100); + }, + $.mage.__('Please enter a valid percentage discount value greater than 0.') + ], 'validate-digits': [ function (value) { return utils.isEmptyNoTrim(value) || !/[^\d]/.test(value); @@ -747,7 +781,7 @@ define([ function (value) { return utils.isEmptyNoTrim(value) || /^[a-z]+[a-z0-9_]+$/.test(value); }, - $.mage.__('Please use only letters (a-z), numbers (0-9) or underscore (_) in this field, and the first character should be a letter.')//eslint-disable-line max-len + $.mage.__('Please use only lowercase letters (a-z), numbers (0-9) or underscore (_) in this field, and the first character should be a letter.')//eslint-disable-line max-len ], 'validate-alphanum': [ function (value) { @@ -845,7 +879,7 @@ define([ return validateCreditCard(value); } - return false; + return true; }, $.mage.__('Please enter a valid credit card number.') ], @@ -886,10 +920,14 @@ define([ ], 'validate-per-page-value-list': [ function (value) { - var isValid = !utils.isEmpty(value), + var isValid = true, values = value.split(','), i; + if (utils.isEmpty(value)) { + return isValid; + } + for (i = 0; i < values.length; i++) { if (!/^[0-9]+$/.test(values[i])) { isValid = false; 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 31362644d415..407984c7881a 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/validation/validator.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/validation/validator.js @@ -48,6 +48,10 @@ define([ params : [params]; + if (typeof message === 'function') { + message = message.call(rule); + } + message = params.reduce(function (msg, param, idx) { return msg.replace(new RegExp('\\{' + idx + '\\}', 'g'), param); }, message); @@ -60,7 +64,7 @@ define([ } /** - * Validates provied value by a specfied set of rules. + * Validates provided value by a specified set of rules. * * @param {(String|Object)} rules - One or many validation rules. * @param {*} value - Value to be checked. diff --git a/app/code/Magento/Ui/view/base/web/js/lib/view/utils/bindings.js b/app/code/Magento/Ui/view/base/web/js/lib/view/utils/bindings.js index f6afdf784638..48515b668f80 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 @@ -88,8 +88,8 @@ define([ }; /** - * Adds specfied bindings to each DOM elemenet in - * collection and evalutes them with provided context. + * Adds specified bindings to each DOM element in + * collection and evaluates them with provided context. * * @param {(Object|Function)} data - Either bindings object or a function * which returns bindings data for each element in collection. diff --git a/app/code/Magento/Ui/view/base/web/js/lib/view/utils/dom-observer.js b/app/code/Magento/Ui/view/base/web/js/lib/view/utils/dom-observer.js index 4590af4f2d63..f8e752fb77af 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/view/utils/dom-observer.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/view/utils/dom-observer.js @@ -12,7 +12,8 @@ define([ var counter = 1, watchers, - globalObserver; + globalObserver, + disabledNodes = []; watchers = { selectors: {}, @@ -140,7 +141,7 @@ define([ } /** - * Calls handlers assocoiated with an added node. + * Calls handlers associated with an added node. * Adds listeners for the node removal. * * @param {HTMLElement} node - Added node. @@ -162,7 +163,7 @@ define([ } /** - * Calls handlers assocoiated with a removed node. + * Calls handlers associated with a removed node. * * @param {HTMLElement} node - Removed node. */ @@ -238,11 +239,58 @@ define([ }; } + /** + * Verify if the DOM node is a child of a defined disabled node, if so we shouldn't observe provided mutation. + * + * @param {Object} mutation - a single mutation + * @returns {Boolean} + */ + function shouldObserveMutation(mutation) { + var isDisabled; + + if (disabledNodes.length > 0) { + // Iterate through the disabled nodes and determine if this mutation is occurring inside one of them + isDisabled = _.find(disabledNodes, function (node) { + return node === mutation.target || $.contains(node, mutation.target); + }); + + // If we find a matching node we should not observe the mutation + return !isDisabled; + } + + return true; + } + + /** + * Should we observe these mutations? Check the first and last mutation to determine if this is a disabled mutation, + * we check both the first and last in case one has been removed from the DOM during the process of the mutation. + * + * @param {Array} mutations - An array of mutation records. + * @returns {Boolean} + */ + function shouldObserveMutations(mutations) { + var firstMutation, + lastMutation; + + if (mutations.length > 0) { + firstMutation = mutations[0]; + lastMutation = mutations[mutations.length - 1]; + + return shouldObserveMutation(firstMutation) && shouldObserveMutation(lastMutation); + } + + return true; + } + globalObserver = new MutationObserver(function (mutations) { - var changes = formChangesLists(mutations); + var changes; + + if (shouldObserveMutations(mutations)) { + changes = formChangesLists(mutations); - changes.removed.forEach(processRemoved); - changes.added.forEach(processAdded); + changes.removed.forEach(processRemoved); + changes.added.forEach(processAdded); + } }); globalObserver.observe(document.body, { @@ -251,6 +299,16 @@ define([ }); return { + /** + * Disable a node from being observed by the mutations, you may want to disable specific aspects of the + * application which are heavy on DOM changes. The observer running on some actions could cause significant + * delays and degrade the performance of that specific part of the application exponentially. + * + * @param {HTMLElement} node - a HTML node within the document + */ + disableNode: function (node) { + disabledNodes.push(node); + }, /** * Adds listener for the appearance of nodes that matches provided diff --git a/app/code/Magento/Ui/view/base/web/js/modal/modal.js b/app/code/Magento/Ui/view/base/web/js/modal/modal.js index f016b0dbefd8..c81274337f41 100644 --- a/app/code/Magento/Ui/view/base/web/js/modal/modal.js +++ b/app/code/Magento/Ui/view/base/web/js/modal/modal.js @@ -363,14 +363,7 @@ define([ this.modal.data('active', false); if (this.overlay) { - // In cases when one modal is closed but there is another modal open (e.g. admin notifications) - // to avoid collisions between overlay and modal zIndexes - // overlay zIndex is set to be less than modal one - if (this._getVisibleCount() === 1) { - this.overlay.zIndex(this.prevOverlayIndex - 1); - } else { - this.overlay.zIndex(this.prevOverlayIndex); - } + this.overlay.zIndex(this.prevOverlayIndex - 1); } }, diff --git a/app/code/Magento/Ui/view/base/web/templates/dynamic-rows/templates/default.html b/app/code/Magento/Ui/view/base/web/templates/dynamic-rows/templates/default.html index 1a21e1b2f1c7..6da4f82fa8b9 100644 --- a/app/code/Magento/Ui/view/base/web/templates/dynamic-rows/templates/default.html +++ b/app/code/Magento/Ui/view/base/web/templates/dynamic-rows/templates/default.html @@ -41,7 +41,7 @@ <!-- ko foreach: { data: $record().elems(), as: 'elem'} --> <td if="elem.template" css="$parent.setClasses(elem)" - visible="elem.visible" + visible="elem.visible() && elem.formElement !== 'hidden'" disable="elem.disabled" template="elem.template"/> <!-- /ko --> diff --git a/app/code/Magento/Ui/view/base/web/templates/dynamic-rows/templates/grid.html b/app/code/Magento/Ui/view/base/web/templates/dynamic-rows/templates/grid.html index d0b12549bd66..e5d73a62b329 100644 --- a/app/code/Magento/Ui/view/base/web/templates/dynamic-rows/templates/grid.html +++ b/app/code/Magento/Ui/view/base/web/templates/dynamic-rows/templates/grid.html @@ -58,7 +58,7 @@ <!-- ko foreach: { data: $record().elems(), as: 'elem'} --> <td if="elem.template" - visible="elem.visible" + visible="elem.visible() && elem.formElement !== 'hidden'" disable="elem.disabled" css="$parent.setClasses(elem)" template="elem.template" diff --git a/app/code/Magento/Ui/view/base/web/templates/form/element/button.html b/app/code/Magento/Ui/view/base/web/templates/form/element/button.html index a8b6c3a85895..766df35f5764 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 a92b85cb4740..cf4e2243b588 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/form/field.html b/app/code/Magento/Ui/view/base/web/templates/form/field.html index ed84e158819a..6a095b4da14e 100644 --- a/app/code/Magento/Ui/view/base/web/templates/form/field.html +++ b/app/code/Magento/Ui/view/base/web/templates/form/field.html @@ -8,8 +8,8 @@ visible="visible" css="$data.additionalClasses" attr="'data-index': index"> - <div class="admin__field-label"> - <label if="$data.label" visible="$data.labelVisible" attr="for: uid"> + <div class="admin__field-label" visible="$data.labelVisible"> + <label if="$data.label" attr="for: uid"> <span translate="label" attr="'data-config-scope': $data.scopeLabel" /> </label> </div> diff --git a/app/code/Magento/Ui/view/base/web/templates/form/insert.html b/app/code/Magento/Ui/view/base/web/templates/form/insert.html index e19b2784e6bc..e590b5e2aded 100644 --- a/app/code/Magento/Ui/view/base/web/templates/form/insert.html +++ b/app/code/Magento/Ui/view/base/web/templates/form/insert.html @@ -6,9 +6,6 @@ --> <div data-bind="bindHtml: content, - attr: { - class: element.cssclass ? element.cssclass : 'admin__scope-old' - }, visible: visible, css: contentSelector"></div> diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/controls/bookmarks/bookmarks.html b/app/code/Magento/Ui/view/base/web/templates/grid/controls/bookmarks/bookmarks.html index 3ef64fd4b537..36a3232c3e61 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/controls/bookmarks/bookmarks.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/controls/bookmarks/bookmarks.html @@ -6,7 +6,7 @@ --> <div class="admin__action-dropdown-wrap admin__data-grid-action-bookmarks" collapsible> <button class="admin__action-dropdown" type="button" toggleCollapsible> - <span class="admin__action-dropdown-text" text="activeView.label"/> + <span class="admin__action-dropdown-text" translate="activeView.label"/> </button> <ul class="admin__action-dropdown-menu"> <repeat args="foreach: viewsArray, item: '$view'"> diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/controls/bookmarks/view.html b/app/code/Magento/Ui/view/base/web/templates/grid/controls/bookmarks/view.html index b52669e2cd28..521ce9fc806a 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/controls/bookmarks/view.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/controls/bookmarks/view.html @@ -30,7 +30,7 @@ </div> <div class="action-dropdown-menu-item"> - <a href="" class="action-dropdown-menu-link" text="$view().label" click="applyView.bind($data, $view().index)" closeCollapsible/> + <a href="" class="action-dropdown-menu-link" translate="$view().label" click="applyView.bind($data, $view().index)" closeCollapsible/> <div class="action-dropdown-menu-item-actions" if="$view().editable"> <button class="action-edit" type="button" attr="title: $t('Edit bookmark')" click="editView.bind($data, $view().index)"> diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/filters/elements/ui-select-optgroup.html b/app/code/Magento/Ui/view/base/web/templates/grid/filters/elements/ui-select-optgroup.html index 56244422a6b4..1ad0e7505ec9 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 bf3e2df8a82d..b9425c020c0e 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/filters/elements/ui-select.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/filters/elements/ui-select.html @@ -36,7 +36,7 @@ class="action-select admin__action-multiselect" data-role="advanced-select" data-bind=" - css: {_active: multiselectFocus}, + css: {_active: listVisible}, click: function(data, event) { toggleListVisible(data, event) } @@ -73,7 +73,7 @@ class="action-select admin__action-multiselect" data-role="advanced-select" data-bind=" - css: {_active: multiselectFocus}, + css: {_active: listVisible}, click: function(data, event) { toggleListVisible(data, event) } @@ -160,7 +160,7 @@ css: { _selected: $parent.isSelectedValue(option), _hover: $parent.isHovered(option, $element), - _expended: $parent.getLevelVisibility($data), + _expended: $parent.getLevelVisibility($data) && $parent.showLevels($data), _unclickable: $parent.isLabelDecoration($data), _last: $parent.addLastElement($data), '_with-checkbox': $parent.showCheckbox @@ -174,6 +174,7 @@ <div class="admin__action-multiselect-dropdown" data-bind=" click: function(event){ + $parent.showLevels($data); $parent.openChildLevel($data, $element, event); }, clickBubble: false diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/search/search.html b/app/code/Magento/Ui/view/base/web/templates/grid/search/search.html index 39d996e05c3a..fcad729a95fb 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/search/search.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/search/search.html @@ -5,16 +5,18 @@ */ --> <div class="data-grid-search-control-wrap"> - <label class="data-grid-search-label" attr="title: $t('Search'), for: index"> + <label class="data-grid-search-label" attr="title: $t('Search'), for: index" data-bind="click: scrollTo"> <span translate="'Search'"/> </label> <input class="admin__control-text data-grid-search-control" type="text" data-bind=" + i18n: placeholder, attr: { id: index, - placeholder: placeholder + placeholder: $t(placeholder) }, textInput: inputValue, + hasFocus: focused, keyboard: { 13: apply.bind($data, false), 27: cancel diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/submenu.html b/app/code/Magento/Ui/view/base/web/templates/grid/submenu.html index c5d87a4b16c4..610d78e00b81 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/submenu.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/submenu.html @@ -6,7 +6,7 @@ --> <ul class="action-submenu" each="data: action.actions, as: 'action'" css="_active: action.visible"> <li css="_visible: $data.visible"> - <span class="action-menu-item" text="label" click="$parent.applyAction.bind($parent, type)"/> + <span class="action-menu-item" translate="label" click="$parent.applyAction.bind($parent, type)"/> <render args="name: $parent.submenuTemplate, data: $parent" if="$data.actions"/> </li> </ul> diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/tree-massactions.html b/app/code/Magento/Ui/view/base/web/templates/grid/tree-massactions.html index 1aeb48b7c769..d11d4aa24373 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/tree-massactions.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/tree-massactions.html @@ -11,7 +11,7 @@ <div class="action-menu-items"> <ul class="action-menu" each="data: actions, as: 'action'" css="_active: opened"> <li css="_visible: $data.visible, _parent: $data.actions"> - <span class="action-menu-item" text="label" click="$parent.applyAction.bind($parent, type)"/> + <span class="action-menu-item" translate="label" click="$parent.applyAction.bind($parent, type)"/> <render args="name: $parent.submenuTemplate, data: $parent" if="$data.actions"/> </li> </ul> diff --git a/app/code/Magento/Ui/view/frontend/web/templates/form/element/helper/tooltip.html b/app/code/Magento/Ui/view/frontend/web/templates/form/element/helper/tooltip.html index 3d4f9eefe5af..f8d1cbbcad5c 100644 --- a/app/code/Magento/Ui/view/frontend/web/templates/form/element/helper/tooltip.html +++ b/app/code/Magento/Ui/view/frontend/web/templates/form/element/helper/tooltip.html @@ -13,14 +13,20 @@ data-bind="attr: {href: tooltip.link}, mageInit: {'dropdown':{'activeClass': '_active'}}"></a> <!-- /ko --> + <span id="tooltip-label" class="label"><!-- ko i18n: 'Tooltip' --><!-- /ko --></span> <!-- ko if: (!tooltip.link)--> - <span class="field-tooltip-action action-help" - tabindex="0" - data-toggle="dropdown" - data-bind="mageInit: {'dropdown':{'activeClass': '_active'}}"></span> + <span + id="tooltip" + class="field-tooltip-action action-help" + tabindex="0" + data-toggle="dropdown" + data-bind="mageInit: {'dropdown':{'activeClass': '_active', 'parent': '.field-tooltip.toggle'}}" + aria-labelledby="tooltip-label" + > + </span> <!-- /ko --> - <div class="field-tooltip-content" + <div class="field-tooltip-content" data-target="dropdown" translate="tooltip.description"> </div> </div> diff --git a/app/code/Magento/Ui/view/frontend/web/templates/form/element/uploader/uploader.html b/app/code/Magento/Ui/view/frontend/web/templates/form/element/uploader/uploader.html new file mode 100644 index 000000000000..226ad2915bb6 --- /dev/null +++ b/app/code/Magento/Ui/view/frontend/web/templates/form/element/uploader/uploader.html @@ -0,0 +1,37 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<div class="field-control" css="'_with-tooltip': $data.tooltip"> + <div class="file-uploader" data-role="drop-zone" css="_loading: isLoading"> + <div class="file-uploader-area"> + <input type="file" afterRender="onElementRender" attr="id: uid, name: inputName, multiple: isMultipleFiles" + disable="disabled"/> + <label class="file-uploader-button action-default" attr="for: uid" translate="'Upload'"/> + + <span class="file-uploader-spinner"/> + <render args="fallbackResetTpl" if="$data.showFallbackReset && $data.isDifferedFromDefault"/> + </div> + + <render args="tooltipTpl" if="$data.tooltip"/> + + <div class="field-note" if="$data.notice" attr="id: noticeId"> + <span html="notice"/> + </div> + + <each args="data: value, as: '$file'" render="$parent.getPreviewTmpl($file)"/> + + <div if="isMultipleFiles" class="file-uploader-summary"> + <label attr="for: uid" + class="file-uploader-placeholder" + css="'placeholder-' + placeholderType"> + <span class="file-uploader-placeholder-text" + translate="'Click here or drag and drop to add files.'"/> + </label> + </div> + </div> + <render args="$data.service.template" if="$data.hasService()"/> +</div> diff --git a/app/code/Magento/Ups/Model/Carrier.php b/app/code/Magento/Ups/Model/Carrier.php index e2b9d4694abc..0e2ce05f2d07 100644 --- a/app/code/Magento/Ups/Model/Carrier.php +++ b/app/code/Magento/Ups/Model/Carrier.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Ups\Model; @@ -77,7 +78,7 @@ class Carrier extends AbstractCarrierOnline implements CarrierInterface * * @var string */ - protected $_defaultCgiGatewayUrl = 'http://www.ups.com:80/using/services/rave/qcostcgi.cgi'; + protected $_defaultCgiGatewayUrl = 'https://www.ups.com/using/services/rave/qcostcgi.cgi'; /** * Test urls for shipment @@ -332,6 +333,14 @@ public function setRequest(RateRequest $request) $destCountry = self::GUAM_COUNTRY_ID; } + // For UPS, Las Palmas and Santa Cruz de Tenerife will be represented by Canary Islands country + if ($destCountry === 'ES' && + ($request->getDestRegionCode() === 'Las Palmas' + || $request->getDestRegionCode() === 'Santa Cruz de Tenerife') + ) { + $destCountry = 'IC'; + } + $country = $this->_countryFactory->create()->load($destCountry); $rowRequest->setDestCountry($country->getData('iso2_code') ?: $destCountry); @@ -446,7 +455,7 @@ protected function _getCgiQuotes() { $rowRequest = $this->_rawRequest; if (self::USA_COUNTRY_ID == $rowRequest->getDestCountry()) { - $destPostal = substr($rowRequest->getDestPostal(), 0, 5); + $destPostal = substr((string)$rowRequest->getDestPostal(), 0, 5); } else { $destPostal = $rowRequest->getDestPostal(); } @@ -464,7 +473,7 @@ protected function _getCgiQuotes() '47_rate_chart' => $rowRequest->getPickup(), '48_container' => $rowRequest->getContainer(), '49_residential' => $rowRequest->getDestType(), - 'weight_std' => strtolower($rowRequest->getUnitMeasure()), + 'weight_std' => strtolower((string)$rowRequest->getUnitMeasure()), ]; $params['47_rate_chart'] = $params['47_rate_chart']['label']; @@ -528,7 +537,7 @@ protected function _parseCgiResponse($response) $priceArr = []; if (strlen(trim($response)) > 0) { $rRows = explode("\n", $response); - $allowedMethods = explode(",", $this->getConfigData('allowed_methods')); + $allowedMethods = explode(",", (string)$this->getConfigData('allowed_methods')); foreach ($rRows as $rRow) { $row = explode('%', $rRow); switch (substr($row[0], -1)) { @@ -604,7 +613,7 @@ protected function _getXmlQuotes() $rowRequest = $this->_rawRequest; if (self::USA_COUNTRY_ID == $rowRequest->getDestCountry()) { - $destPostal = substr($rowRequest->getDestPostal(), 0, 5); + $destPostal = substr((string)$rowRequest->getDestPostal(), 0, 5); } else { $destPostal = $rowRequest->getDestPostal(); } @@ -630,7 +639,7 @@ protected function _getXmlQuotes() $serviceCode = null; } else { $params['10_action'] = 'Rate'; - $serviceCode = $rowRequest->getProduct() ? $rowRequest->getProduct() : ''; + $serviceCode = $rowRequest->getProduct() ? $rowRequest->getProduct() : null; } $serviceDescription = $serviceCode ? $this->getShipmentByCode($serviceCode) : ''; @@ -664,8 +673,8 @@ protected function _getXmlQuotes() <Shipper> XMLRequest; - if ($this->getConfigFlag('negotiated_active') && ($shipper = $this->getConfigData('shipper_number'))) { - $xmlParams .= "<ShipperNumber>{$shipper}</ShipperNumber>"; + if ($this->getConfigFlag('negotiated_active') && ($shipperNumber = $this->getConfigData('shipper_number'))) { + $xmlParams .= "<ShipperNumber>{$shipperNumber}</ShipperNumber>"; } if ($rowRequest->getIsReturn()) { @@ -688,6 +697,7 @@ protected function _getXmlQuotes() <StateProvinceCode>{$shipperStateProvince}</StateProvinceCode> </Address> </Shipper> + <ShipTo> <Address> <PostalCode>{$params['19_destPostal']}</PostalCode> @@ -703,8 +713,7 @@ protected function _getXmlQuotes() $xmlParams .= <<<XMLRequest </Address> </ShipTo> - - + <ShipFrom> <Address> <PostalCode>{$params['15_origPostal']}</PostalCode> @@ -714,9 +723,13 @@ protected function _getXmlQuotes() </ShipFrom> <Package> - <PackagingType><Code>{$params['48_container']}</Code></PackagingType> + <PackagingType> + <Code>{$params['48_container']}</Code> + </PackagingType> <PackageWeight> - <UnitOfMeasurement><Code>{$rowRequest->getUnitMeasure()}</Code></UnitOfMeasurement> + <UnitOfMeasurement> + <Code>{$rowRequest->getUnitMeasure()}</Code> + </UnitOfMeasurement> <Weight>{$params['23_weight']}</Weight> </PackageWeight> </Package> @@ -730,8 +743,8 @@ protected function _getXmlQuotes() } $xmlParams .= <<<XMLRequest - </Shipment> -</RatingServiceSelectionRequest> + </Shipment> + </RatingServiceSelectionRequest> XMLRequest; $xmlRequest .= $xmlParams; @@ -820,76 +833,15 @@ protected function _parseXmlResponse($xmlResponse) $allowedCurrencies = $this->_currencyFactory->create()->getConfigAllowCurrencies(); foreach ($arr as $shipElement) { - $code = (string)$shipElement->Service->Code; - if (in_array($code, $allowedMethods)) { - //The location of tax information is in a different place - // depending on whether we are using negotiated rates or not - if ($negotiatedActive) { - $includeTaxesArr = $xml->getXpath( - "//RatingServiceSelectionResponse/RatedShipment/NegotiatedRates" - . "/NetSummaryCharges/TotalChargesWithTaxes" - ); - $includeTaxesActive = $this->getConfigFlag('include_taxes') && !empty($includeTaxesArr); - if ($includeTaxesActive) { - $cost = $shipElement->NegotiatedRates - ->NetSummaryCharges - ->TotalChargesWithTaxes - ->MonetaryValue; - - $responseCurrencyCode = $this->mapCurrencyCode( - (string)$shipElement->NegotiatedRates - ->NetSummaryCharges - ->TotalChargesWithTaxes - ->CurrencyCode - ); - } else { - $cost = $shipElement->NegotiatedRates->NetSummaryCharges->GrandTotal->MonetaryValue; - $responseCurrencyCode = $this->mapCurrencyCode( - (string)$shipElement->NegotiatedRates->NetSummaryCharges->GrandTotal->CurrencyCode - ); - } - } else { - $includeTaxesArr = $xml->getXpath( - "//RatingServiceSelectionResponse/RatedShipment/TotalChargesWithTaxes" - ); - $includeTaxesActive = $this->getConfigFlag('include_taxes') && !empty($includeTaxesArr); - if ($includeTaxesActive) { - $cost = $shipElement->TotalChargesWithTaxes->MonetaryValue; - $responseCurrencyCode = $this->mapCurrencyCode( - (string)$shipElement->TotalChargesWithTaxes->CurrencyCode - ); - } else { - $cost = $shipElement->TotalCharges->MonetaryValue; - $responseCurrencyCode = $this->mapCurrencyCode( - (string)$shipElement->TotalCharges->CurrencyCode - ); - } - } - - //convert price with Origin country currency code to base currency code - $successConversion = true; - if ($responseCurrencyCode) { - if (in_array($responseCurrencyCode, $allowedCurrencies)) { - $cost = (double)$cost * $this->_getBaseCurrencyRate($responseCurrencyCode); - } else { - $errorTitle = __( - 'We can\'t convert a rate from "%1-%2".', - $responseCurrencyCode, - $this->_request->getPackageCurrency()->getCode() - ); - $error = $this->_rateErrorFactory->create(); - $error->setCarrier('ups'); - $error->setCarrierTitle($this->getConfigData('title')); - $error->setErrorMessage($errorTitle); - $successConversion = false; - } - } - - if ($successConversion) { - $costArr[$code] = $cost; - $priceArr[$code] = $this->getMethodPrice(floatval($cost), $code); - } - } + $this->processShippingRateForItem( + $shipElement, + $allowedMethods, + $allowedCurrencies, + $costArr, + $priceArr, + $negotiatedActive, + $xml + ); } } else { $arr = $xml->getXpath("//RatingServiceSelectionResponse/Response/Error/ErrorDescription/text()"); @@ -907,10 +859,13 @@ protected function _parseXmlResponse($xmlResponse) $error = $this->_rateErrorFactory->create(); $error->setCarrier('ups'); $error->setCarrierTitle($this->getConfigData('title')); + if ($this->getConfigData('specificerrmsg') !== '') { + $errorTitle = $this->getConfigData('specificerrmsg'); + } if (!isset($errorTitle)) { $errorTitle = __('Cannot retrieve shipping rates'); } - $error->setErrorMessage($this->getConfigData('specificerrmsg')); + $error->setErrorMessage($errorTitle); $result->append($error); } else { foreach ($priceArr as $method => $price) { @@ -929,6 +884,99 @@ protected function _parseXmlResponse($xmlResponse) return $result; } + /** + * Processing rate for ship element + * + * @param \Magento\Framework\Simplexml\Element $shipElement + * @param array $allowedMethods + * @param array $allowedCurrencies + * @param array $costArr + * @param array $priceArr + * @param bool $negotiatedActive + * @param \Magento\Framework\Simplexml\Config $xml + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + private function processShippingRateForItem( + \Magento\Framework\Simplexml\Element $shipElement, + array $allowedMethods, + array $allowedCurrencies, + array &$costArr, + array &$priceArr, + bool $negotiatedActive, + \Magento\Framework\Simplexml\Config $xml + ): void { + $code = (string)$shipElement->Service->Code; + if (in_array($code, $allowedMethods)) { + //The location of tax information is in a different place + // depending on whether we are using negotiated rates or not + if ($negotiatedActive) { + $includeTaxesArr = $xml->getXpath( + "//RatingServiceSelectionResponse/RatedShipment/NegotiatedRates" + . "/NetSummaryCharges/TotalChargesWithTaxes" + ); + $includeTaxesActive = $this->getConfigFlag('include_taxes') && !empty($includeTaxesArr); + if ($includeTaxesActive) { + $cost = $shipElement->NegotiatedRates + ->NetSummaryCharges + ->TotalChargesWithTaxes + ->MonetaryValue; + + $responseCurrencyCode = $this->mapCurrencyCode( + (string)$shipElement->NegotiatedRates + ->NetSummaryCharges + ->TotalChargesWithTaxes + ->CurrencyCode + ); + } else { + $cost = $shipElement->NegotiatedRates->NetSummaryCharges->GrandTotal->MonetaryValue; + $responseCurrencyCode = $this->mapCurrencyCode( + (string)$shipElement->NegotiatedRates->NetSummaryCharges->GrandTotal->CurrencyCode + ); + } + } else { + $includeTaxesArr = $xml->getXpath( + "//RatingServiceSelectionResponse/RatedShipment/TotalChargesWithTaxes" + ); + $includeTaxesActive = $this->getConfigFlag('include_taxes') && !empty($includeTaxesArr); + if ($includeTaxesActive) { + $cost = $shipElement->TotalChargesWithTaxes->MonetaryValue; + $responseCurrencyCode = $this->mapCurrencyCode( + (string)$shipElement->TotalChargesWithTaxes->CurrencyCode + ); + } else { + $cost = $shipElement->TotalCharges->MonetaryValue; + $responseCurrencyCode = $this->mapCurrencyCode( + (string)$shipElement->TotalCharges->CurrencyCode + ); + } + } + + //convert price with Origin country currency code to base currency code + $successConversion = true; + if ($responseCurrencyCode) { + if (in_array($responseCurrencyCode, $allowedCurrencies)) { + $cost = (double)$cost * $this->_getBaseCurrencyRate($responseCurrencyCode); + } else { + $errorTitle = __( + 'We can\'t convert a rate from "%1-%2".', + $responseCurrencyCode, + $this->_request->getPackageCurrency()->getCode() + ); + $error = $this->_rateErrorFactory->create(); + $error->setCarrier('ups'); + $error->setCarrierTitle($this->getConfigData('title')); + $error->setErrorMessage($errorTitle); + $successConversion = false; + } + } + + if ($successConversion) { + $costArr[$code] = $cost; + $priceArr[$code] = $this->getMethodPrice((float)$cost, $code); + } + } + } + /** * Get tracking * @@ -1014,14 +1062,14 @@ protected function _getXmlTracking($trackings) foreach ($trackings as $tracking) { /** - * RequestOption==>'activity' or '1' to request all activities + * RequestOption==>'1' to request all activities */ $xmlRequest = <<<XMLAuth <?xml version="1.0" ?> <TrackRequest xml:lang="en-US"> <Request> <RequestAction>Track</RequestAction> - <RequestOption>activity</RequestOption> + <RequestOption>1</RequestOption> </Request> <TrackingNumber>$tracking</TrackingNumber> <IncludeFreight>01</IncludeFreight> @@ -1085,54 +1133,7 @@ protected function _parseXmlTrackingResponse($trackingValue, $xmlResponse) if ($activityTags) { $index = 1; foreach ($activityTags as $activityTag) { - $addArr = []; - if (isset($activityTag->ActivityLocation->Address->City)) { - $addArr[] = (string)$activityTag->ActivityLocation->Address->City; - } - if (isset($activityTag->ActivityLocation->Address->StateProvinceCode)) { - $addArr[] = (string)$activityTag->ActivityLocation->Address->StateProvinceCode; - } - if (isset($activityTag->ActivityLocation->Address->CountryCode)) { - $addArr[] = (string)$activityTag->ActivityLocation->Address->CountryCode; - } - $dateArr = []; - $date = (string)$activityTag->Date; - //YYYYMMDD - $dateArr[] = substr($date, 0, 4); - $dateArr[] = substr($date, 4, 2); - $dateArr[] = substr($date, -2, 2); - - $timeArr = []; - $time = (string)$activityTag->Time; - //HHMMSS - $timeArr[] = substr($time, 0, 2); - $timeArr[] = substr($time, 2, 2); - $timeArr[] = substr($time, -2, 2); - - if ($index === 1) { - $resultArr['status'] = (string)$activityTag->Status->StatusType->Description; - $resultArr['deliverydate'] = implode('-', $dateArr); - //YYYY-MM-DD - $resultArr['deliverytime'] = implode(':', $timeArr); - //HH:MM:SS - $resultArr['deliverylocation'] = (string)$activityTag->ActivityLocation->Description; - $resultArr['signedby'] = (string)$activityTag->ActivityLocation->SignedForByName; - if ($addArr) { - $resultArr['deliveryto'] = implode(', ', $addArr); - } - } else { - $tempArr = []; - $tempArr['activity'] = (string)$activityTag->Status->StatusType->Description; - $tempArr['deliverydate'] = implode('-', $dateArr); - //YYYY-MM-DD - $tempArr['deliverytime'] = implode(':', $timeArr); - //HH:MM:SS - if ($addArr) { - $tempArr['deliverylocation'] = implode(', ', $addArr); - } - $packageProgress[] = $tempArr; - } - $index++; + $this->processActivityTagInfo($activityTag, $index, $resultArr, $packageProgress); } $resultArr['progressdetail'] = $packageProgress; } @@ -1165,6 +1166,70 @@ protected function _parseXmlTrackingResponse($trackingValue, $xmlResponse) return $this->_result; } + /** + * Process tracking info from activity tag + * + * @param \Magento\Framework\Simplexml\Element $activityTag + * @param int $index + * @param array $resultArr + * @param array $packageProgress + */ + private function processActivityTagInfo( + \Magento\Framework\Simplexml\Element $activityTag, + int &$index, + array &$resultArr, + array &$packageProgress + ) { + $addressArr = []; + if (isset($activityTag->ActivityLocation->Address->City)) { + $addressArr[] = (string)$activityTag->ActivityLocation->Address->City; + } + if (isset($activityTag->ActivityLocation->Address->StateProvinceCode)) { + $addressArr[] = (string)$activityTag->ActivityLocation->Address->StateProvinceCode; + } + if (isset($activityTag->ActivityLocation->Address->CountryCode)) { + $addressArr[] = (string)$activityTag->ActivityLocation->Address->CountryCode; + } + $dateArr = []; + $date = (string)$activityTag->Date; + //YYYYMMDD + $dateArr[] = substr($date, 0, 4); + $dateArr[] = substr($date, 4, 2); + $dateArr[] = substr($date, -2, 2); + + $timeArr = []; + $time = (string)$activityTag->Time; + //HHMMSS + $timeArr[] = substr($time, 0, 2); + $timeArr[] = substr($time, 2, 2); + $timeArr[] = substr($time, -2, 2); + + if ($index === 1) { + $resultArr['status'] = (string)$activityTag->Status->StatusType->Description; + $resultArr['deliverydate'] = implode('-', $dateArr); + //YYYY-MM-DD + $resultArr['deliverytime'] = implode(':', $timeArr); + //HH:MM:SS + $resultArr['deliverylocation'] = (string)$activityTag->ActivityLocation->Description; + $resultArr['signedby'] = (string)$activityTag->ActivityLocation->SignedForByName; + if ($addressArr) { + $resultArr['deliveryto'] = implode(', ', $addressArr); + } + } else { + $tempArr = []; + $tempArr['activity'] = (string)$activityTag->Status->StatusType->Description; + $tempArr['deliverydate'] = implode('-', $dateArr); + //YYYY-MM-DD + $tempArr['deliverytime'] = implode(':', $timeArr); + //HH:MM:SS + if ($addressArr) { + $tempArr['deliverylocation'] = implode(', ', $addressArr); + } + $packageProgress[] = $tempArr; + } + $index++; + } + /** * Get tracking response * @@ -1463,6 +1528,7 @@ protected function _sendShipmentAcceptRequest(Element $shipmentConfirmResponse) $shippingLabelContent = (string)$response->ShipmentResults->PackageResults->LabelImage->GraphicImage; $trackingNumber = (string)$response->ShipmentResults->PackageResults->TrackingNumber; + // phpcs:ignore Magento2.Functions.DiscouragedFunction $result->setShippingLabelContent(base64_decode($shippingLabelContent)); $result->setTrackingNumber($trackingNumber); } @@ -1693,6 +1759,7 @@ public function getCustomizableContainerTypes() /** * Get delivery confirmation level based on origin/destination + * * Return null if delivery confirmation is not acceptable * * @param string|null $countyDestination diff --git a/app/code/Magento/Ups/Test/Mftf/Data/ShippingMethodsData.xml b/app/code/Magento/Ups/Test/Mftf/Data/ShippingMethodsData.xml new file mode 100644 index 000000000000..d4156d4f3358 --- /dev/null +++ b/app/code/Magento/Ups/Test/Mftf/Data/ShippingMethodsData.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="ShippingMethodsUpsTypeSetDefault" type="shipping_methods_ups_type_config"> + <requiredEntity type="ups_type_inherit">ShippingMethodsUpsTypeDefault</requiredEntity> + </entity> + <entity name="ShippingMethodsUpsTypeDefault" type="ups_type_inherit"> + <data key="inherit">true</data> + </entity> +</entities> diff --git a/app/code/Magento/Ups/Test/Mftf/Metadata/shipping_methods_ups_type_config-meta.xml b/app/code/Magento/Ups/Test/Mftf/Metadata/shipping_methods_ups_type_config-meta.xml new file mode 100644 index 000000000000..d642b7923282 --- /dev/null +++ b/app/code/Magento/Ups/Test/Mftf/Metadata/shipping_methods_ups_type_config-meta.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="ShippingMethodsUpsTypeConfig" dataType="shipping_methods_ups_type_config" type="create" auth="adminFormKey" url="admin/system_config/save/section/carriers/" method="POST"> + <object key="groups" dataType="shipping_methods_ups_type_config"> + <object key="ups" dataType="shipping_methods_ups_type_config"> + <object key="fields" dataType="shipping_methods_ups_type_config"> + <object key="type" dataType="ups_type_inherit"> + <field key="inherit">boolean</field> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Ups/Test/Mftf/Page/AdminShippingMethodsConfigPage.xml b/app/code/Magento/Ups/Test/Mftf/Page/AdminShippingMethodsConfigPage.xml new file mode 100644 index 000000000000..ebc44aace6df --- /dev/null +++ b/app/code/Magento/Ups/Test/Mftf/Page/AdminShippingMethodsConfigPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminShippingMethodsConfigPage" url="admin/system_config/edit/section/carriers/" area="admin" module="Magento_Ups"> + <section name="AdminShippingMethodsUpsSection"/> + </page> +</pages> diff --git a/app/code/Magento/Ups/Test/Mftf/Section/AdminShippingMethodsUpsSection.xml b/app/code/Magento/Ups/Test/Mftf/Section/AdminShippingMethodsUpsSection.xml new file mode 100644 index 000000000000..4107f17dbc18 --- /dev/null +++ b/app/code/Magento/Ups/Test/Mftf/Section/AdminShippingMethodsUpsSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminShippingMethodsUpsSection"> + <element name="carriersUpsTab" type="button" selector="#carriers_ups-head"/> + <element name="carriersUpsType" type="select" selector="#carriers_ups_type"/> + <element name="selectedUpsType" type="text" selector="#carriers_ups_type option[selected]"/> + </section> +</sections> diff --git a/app/code/Magento/Ups/Test/Mftf/Test/DefaultConfigForUPSTypeTest.xml b/app/code/Magento/Ups/Test/Mftf/Test/DefaultConfigForUPSTypeTest.xml new file mode 100644 index 000000000000..51db704a7abc --- /dev/null +++ b/app/code/Magento/Ups/Test/Mftf/Test/DefaultConfigForUPSTypeTest.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="DefaultConfigForUPSTypeTest"> + <annotations> + <title value="Default Configuration for UPS Type"/> + <description value="Default Configuration for UPS Type"/> + <features value="Ups"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-99012"/> + <useCaseId value="MAGETWO-98947"/> + <group value="ups"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!--Collapse UPS tab and logout--> + <comment userInput="Collapse UPS tab and logout" stepKey="collapseTabAndLogout"/> + <click selector="{{AdminShippingMethodsUpsSection.carriersUpsTab}}" stepKey="collapseTab"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!-- Set shipping methods UPS type to default --> + <comment userInput="Set shipping methods UPS type to default" stepKey="setToDefaultShippingMethodsUpsType"/> + <createData entity="ShippingMethodsUpsTypeSetDefault" stepKey="setShippingMethodsUpsTypeToDefault"/> + <!-- Navigate to Stores -> Configuration -> Sales -> Shipping Methods Page --> + <comment userInput="Navigate to Stores -> Configuration -> Sales -> Shipping Methods Page" stepKey="goToAdminShippingMethodsPage"/> + <amOnPage url="{{AdminShippingMethodsConfigPage.url}}" stepKey="navigateToAdminShippingMethodsPage"/> + <waitForPageLoad stepKey="waitPageToLoad"/> + <!-- Expand 'UPS' tab --> + <comment userInput="Expand UPS tab" stepKey="expandUpsTab"/> + <conditionalClick selector="{{AdminShippingMethodsUpsSection.carriersUpsTab}}" dependentSelector="{{AdminShippingMethodsUpsSection.carriersUpsType}}" visible="false" stepKey="expandTab"/> + <waitForElementVisible selector="{{AdminShippingMethodsUpsSection.carriersUpsType}}" stepKey="waitTabToExpand"/> + <!-- Assert that selected UPS type by default is 'United Parcel Service XML' --> + <comment userInput="Check that selected UPS type by default is 'United Parcel Service XML'" stepKey="assertDefUpsType"/> + <grabTextFrom selector="{{AdminShippingMethodsUpsSection.selectedUpsType}}" stepKey="grabSelectedOptionText"/> + <assertEquals expected='United Parcel Service XML' expectedType="string" actual="($grabSelectedOptionText)" stepKey="assertDefaultUpsType"/> + </test> +</tests> diff --git a/app/code/Magento/Ups/etc/config.xml b/app/code/Magento/Ups/etc/config.xml index 0f92ae4dad19..73b10dd5ff41 100644 --- a/app/code/Magento/Ups/etc/config.xml +++ b/app/code/Magento/Ups/etc/config.xml @@ -19,13 +19,13 @@ <cutoff_cost /> <dest_type>RES</dest_type> <free_method>GND</free_method> - <gateway_url>http://www.ups.com/using/services/rave/qcostcgi.cgi</gateway_url> + <gateway_url>https://www.ups.com/using/services/rave/qcostcgi.cgi</gateway_url> <gateway_xml_url>https://onlinetools.ups.com/ups.app/xml/Rate</gateway_xml_url> <handling>0</handling> <model>Magento\Ups\Model\Carrier</model> <pickup>CC</pickup> <title>United Parcel Service - https://www.ups.com/ups.app/xml/Track + https://onlinetools.ups.com/ups.app/xml/Track LBS @@ -37,7 +37,7 @@ 0 0 1 - UPS + UPS_XML 0 0 1 diff --git a/app/code/Magento/UrlRewrite/Block/Edit.php b/app/code/Magento/UrlRewrite/Block/Edit.php index 115c5db43a70..c8716f6c80f7 100644 --- a/app/code/Magento/UrlRewrite/Block/Edit.php +++ b/app/code/Magento/UrlRewrite/Block/Edit.php @@ -173,7 +173,7 @@ protected function _addDeleteButton() ['id' => $this->getUrlRewrite()->getId()] ) ) - . ')', + . ', {data: {}})', 'class' => 'scalable delete', 'level' => -1 ] @@ -243,7 +243,7 @@ private function _getSelectorBlock() * Since buttons are set as children, we remove them as children after generating them * not to duplicate them in future * - * @param string $area + * @param string|null $area * @return string * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ diff --git a/app/code/Magento/UrlRewrite/Controller/Adminhtml/Url/Rewrite/CmsPageGrid.php b/app/code/Magento/UrlRewrite/Controller/Adminhtml/Url/Rewrite/CmsPageGrid.php index 480efa823571..7240dfc36f1e 100644 --- a/app/code/Magento/UrlRewrite/Controller/Adminhtml/Url/Rewrite/CmsPageGrid.php +++ b/app/code/Magento/UrlRewrite/Controller/Adminhtml/Url/Rewrite/CmsPageGrid.php @@ -6,7 +6,11 @@ */ namespace Magento\UrlRewrite\Controller\Adminhtml\Url\Rewrite; -class CmsPageGrid extends \Magento\UrlRewrite\Controller\Adminhtml\Url\Rewrite +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; +use Magento\UrlRewrite\Controller\Adminhtml\Url\Rewrite as RewriteAction; + +class CmsPageGrid extends RewriteAction implements HttpPostActionInterface, HttpGetActionInterface { /** * Ajax CMS pages grid action diff --git a/app/code/Magento/UrlRewrite/Controller/Adminhtml/Url/Rewrite/Delete.php b/app/code/Magento/UrlRewrite/Controller/Adminhtml/Url/Rewrite/Delete.php index f8f7b145e280..dc49776a1ac0 100644 --- a/app/code/Magento/UrlRewrite/Controller/Adminhtml/Url/Rewrite/Delete.php +++ b/app/code/Magento/UrlRewrite/Controller/Adminhtml/Url/Rewrite/Delete.php @@ -6,7 +6,9 @@ */ namespace Magento\UrlRewrite\Controller\Adminhtml\Url\Rewrite; -class Delete extends \Magento\UrlRewrite\Controller\Adminhtml\Url\Rewrite +use Magento\Framework\App\Action\HttpPostActionInterface; + +class Delete extends \Magento\UrlRewrite\Controller\Adminhtml\Url\Rewrite implements HttpPostActionInterface { /** * URL rewrite delete action diff --git a/app/code/Magento/UrlRewrite/Controller/Adminhtml/Url/Rewrite/Edit.php b/app/code/Magento/UrlRewrite/Controller/Adminhtml/Url/Rewrite/Edit.php index b7ff92213003..e2161f4ffd9b 100644 --- a/app/code/Magento/UrlRewrite/Controller/Adminhtml/Url/Rewrite/Edit.php +++ b/app/code/Magento/UrlRewrite/Controller/Adminhtml/Url/Rewrite/Edit.php @@ -6,7 +6,9 @@ */ namespace Magento\UrlRewrite\Controller\Adminhtml\Url\Rewrite; -class Edit extends \Magento\UrlRewrite\Controller\Adminhtml\Url\Rewrite +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Edit extends \Magento\UrlRewrite\Controller\Adminhtml\Url\Rewrite implements HttpGetActionInterface { /**#@+ * Modes diff --git a/app/code/Magento/UrlRewrite/Controller/Adminhtml/Url/Rewrite/Index.php b/app/code/Magento/UrlRewrite/Controller/Adminhtml/Url/Rewrite/Index.php index 066377ff7bd7..4f3a1e7849ec 100644 --- a/app/code/Magento/UrlRewrite/Controller/Adminhtml/Url/Rewrite/Index.php +++ b/app/code/Magento/UrlRewrite/Controller/Adminhtml/Url/Rewrite/Index.php @@ -6,7 +6,9 @@ */ namespace Magento\UrlRewrite\Controller\Adminhtml\Url\Rewrite; -class Index extends \Magento\UrlRewrite\Controller\Adminhtml\Url\Rewrite +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\UrlRewrite\Controller\Adminhtml\Url\Rewrite implements HttpGetActionInterface { /** * Show URL rewrites index page diff --git a/app/code/Magento/UrlRewrite/Controller/Adminhtml/Url/Rewrite/Save.php b/app/code/Magento/UrlRewrite/Controller/Adminhtml/Url/Rewrite/Save.php index 6325fe162d9e..c508e85d87c3 100644 --- a/app/code/Magento/UrlRewrite/Controller/Adminhtml/Url/Rewrite/Save.php +++ b/app/code/Magento/UrlRewrite/Controller/Adminhtml/Url/Rewrite/Save.php @@ -7,11 +7,12 @@ namespace Magento\UrlRewrite\Controller\Adminhtml\Url\Rewrite; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\Exception\LocalizedException; use Magento\UrlRewrite\Model\UrlFinderInterface; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; -class Save extends \Magento\UrlRewrite\Controller\Adminhtml\Url\Rewrite +class Save extends \Magento\UrlRewrite\Controller\Adminhtml\Url\Rewrite implements HttpPostActionInterface { /** * @var \Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator diff --git a/app/code/Magento/UrlRewrite/Model/Exception/UrlAlreadyExistsException.php b/app/code/Magento/UrlRewrite/Model/Exception/UrlAlreadyExistsException.php index c7e83819f256..906ba1f62547 100644 --- a/app/code/Magento/UrlRewrite/Model/Exception/UrlAlreadyExistsException.php +++ b/app/code/Magento/UrlRewrite/Model/Exception/UrlAlreadyExistsException.php @@ -8,10 +8,12 @@ use Magento\Framework\Phrase; /** + * Exception for already created url. + * * @api * @since 100.2.0 */ -class UrlAlreadyExistsException extends \Magento\Framework\Exception\LocalizedException +class UrlAlreadyExistsException extends \Magento\Framework\Exception\AlreadyExistsException { /** * @var array @@ -34,6 +36,8 @@ public function __construct(Phrase $phrase = null, \Exception $cause = null, $co } /** + * Get URLs + * * @return array * @since 100.2.0 */ diff --git a/app/code/Magento/UrlRewrite/Model/Storage/DbStorage.php b/app/code/Magento/UrlRewrite/Model/Storage/DbStorage.php index f4aa61b145f6..2ac1bdd71211 100644 --- a/app/code/Magento/UrlRewrite/Model/Storage/DbStorage.php +++ b/app/code/Magento/UrlRewrite/Model/Storage/DbStorage.php @@ -87,7 +87,7 @@ protected function prepareSelect(array $data) } /** - * {@inheritdoc} + * @inheritdoc */ protected function doFindAllByData(array $data) { @@ -95,7 +95,7 @@ protected function doFindAllByData(array $data) } /** - * {@inheritdoc} + * @inheritdoc */ protected function doFindOneByData(array $data) { @@ -105,42 +105,18 @@ protected function doFindOneByData(array $data) $result = null; $requestPath = $data[UrlRewrite::REQUEST_PATH]; - - $data[UrlRewrite::REQUEST_PATH] = [ + $decodedRequestPath = urldecode($requestPath); + $data[UrlRewrite::REQUEST_PATH] = array_unique([ rtrim($requestPath, '/'), rtrim($requestPath, '/') . '/', - ]; + rtrim($decodedRequestPath, '/'), + rtrim($decodedRequestPath, '/') . '/', + ]); $resultsFromDb = $this->connection->fetchAll($this->prepareSelect($data)); - - if (count($resultsFromDb) === 1) { - $resultFromDb = current($resultsFromDb); - $redirectTypes = [OptionProvider::TEMPORARY, OptionProvider::PERMANENT]; - - // If request path matches the DB value or it's redirect - we can return result from DB - $canReturnResultFromDb = ($resultFromDb[UrlRewrite::REQUEST_PATH] === $requestPath - || in_array((int)$resultFromDb[UrlRewrite::REDIRECT_TYPE], $redirectTypes, true)); - - // Otherwise return 301 redirect to request path from DB results - $result = $canReturnResultFromDb ? $resultFromDb : [ - UrlRewrite::ENTITY_TYPE => 'custom', - UrlRewrite::ENTITY_ID => '0', - UrlRewrite::REQUEST_PATH => $requestPath, - UrlRewrite::TARGET_PATH => $resultFromDb[UrlRewrite::REQUEST_PATH], - UrlRewrite::REDIRECT_TYPE => OptionProvider::PERMANENT, - UrlRewrite::STORE_ID => $resultFromDb[UrlRewrite::STORE_ID], - UrlRewrite::DESCRIPTION => null, - UrlRewrite::IS_AUTOGENERATED => '0', - UrlRewrite::METADATA => null, - ]; - } else { - // If we have 2 results - return the row that matches request path - foreach ($resultsFromDb as $resultFromDb) { - if ($resultFromDb[UrlRewrite::REQUEST_PATH] === $requestPath) { - $result = $resultFromDb; - break; - } - } + if ($resultsFromDb) { + $urlRewrite = $this->extractMostRelevantUrlRewrite($requestPath, $resultsFromDb); + $result = $this->prepareUrlRewrite($requestPath, $urlRewrite); } return $result; @@ -149,6 +125,75 @@ protected function doFindOneByData(array $data) return $this->connection->fetchRow($this->prepareSelect($data)); } + /** + * Extract most relevant url rewrite from url rewrites list + * + * @param string $requestPath + * @param array $urlRewrites + * @return array|null + */ + private function extractMostRelevantUrlRewrite(string $requestPath, array $urlRewrites): ?array + { + $prioritizedUrlRewrites = []; + foreach ($urlRewrites as $urlRewrite) { + switch (true) { + case $urlRewrite[UrlRewrite::REQUEST_PATH] === $requestPath: + $priority = 1; + break; + case $urlRewrite[UrlRewrite::REQUEST_PATH] === urldecode($requestPath): + $priority = 2; + break; + case rtrim($urlRewrite[UrlRewrite::REQUEST_PATH], '/') === rtrim($requestPath, '/'): + $priority = 3; + break; + case rtrim($urlRewrite[UrlRewrite::REQUEST_PATH], '/') === rtrim(urldecode($requestPath), '/'): + $priority = 4; + break; + default: + $priority = 5; + break; + } + $prioritizedUrlRewrites[$priority] = $urlRewrite; + } + ksort($prioritizedUrlRewrites); + + return array_shift($prioritizedUrlRewrites); + } + + /** + * Prepare url rewrite + * + * If request path matches the DB value or it's redirect - we can return result from DB + * Otherwise return 301 redirect to request path from DB results + * + * @param string $requestPath + * @param array $urlRewrite + * @return array + */ + private function prepareUrlRewrite(string $requestPath, array $urlRewrite): array + { + $redirectTypes = [OptionProvider::TEMPORARY, OptionProvider::PERMANENT]; + $canReturnResultFromDb = ( + in_array($urlRewrite[UrlRewrite::REQUEST_PATH], [$requestPath, urldecode($requestPath)], true) + || in_array((int) $urlRewrite[UrlRewrite::REDIRECT_TYPE], $redirectTypes, true) + ); + if (!$canReturnResultFromDb) { + $urlRewrite = [ + UrlRewrite::ENTITY_TYPE => 'custom', + UrlRewrite::ENTITY_ID => '0', + UrlRewrite::REQUEST_PATH => $requestPath, + UrlRewrite::TARGET_PATH => $urlRewrite[UrlRewrite::REQUEST_PATH], + UrlRewrite::REDIRECT_TYPE => OptionProvider::PERMANENT, + UrlRewrite::STORE_ID => $urlRewrite[UrlRewrite::STORE_ID], + UrlRewrite::DESCRIPTION => null, + UrlRewrite::IS_AUTOGENERATED => '0', + UrlRewrite::METADATA => null, + ]; + } + + return $urlRewrite; + } + /** * Delete old URLs from DB. * @@ -161,26 +206,22 @@ private function deleteOldUrls(array $urls): void $oldUrlsSelect->from( $this->resource->getTableName(self::TABLE_NAME) ); - /** @var UrlRewrite $url */ - foreach ($urls as $url) { - $oldUrlsSelect->orWhere( - $this->connection->quoteIdentifier( - UrlRewrite::ENTITY_TYPE - ) . ' = ?', - $url->getEntityType() - ); - $oldUrlsSelect->where( - $this->connection->quoteIdentifier( - UrlRewrite::ENTITY_ID - ) . ' = ?', - $url->getEntityId() - ); - $oldUrlsSelect->where( - $this->connection->quoteIdentifier( - UrlRewrite::STORE_ID - ) . ' = ?', - $url->getStoreId() - ); + + $uniqueEntities = $this->prepareUniqueEntities($urls); + foreach ($uniqueEntities as $storeId => $entityTypes) { + foreach ($entityTypes as $entityType => $entities) { + $oldUrlsSelect->orWhere( + $this->connection->quoteIdentifier( + UrlRewrite::STORE_ID + ) . ' = ' . $this->connection->quote($storeId, 'INTEGER') . + ' AND ' . $this->connection->quoteIdentifier( + UrlRewrite::ENTITY_ID + ) . ' IN (' . $this->connection->quote($entities, 'INTEGER') . ')' . + ' AND ' . $this->connection->quoteIdentifier( + UrlRewrite::ENTITY_TYPE + ) . ' = ' . $this->connection->quote($entityType) + ); + } } // prevent query locking in a case when nothing to delete @@ -198,6 +239,28 @@ private function deleteOldUrls(array $urls): void } } + /** + * Prepare array with unique entities + * + * @param UrlRewrite[] $urls + * @return array + */ + private function prepareUniqueEntities(array $urls): array + { + $uniqueEntities = []; + /** @var UrlRewrite $url */ + foreach ($urls as $url) { + $entityIds = (!empty($uniqueEntities[$url->getStoreId()][$url->getEntityType()])) ? + $uniqueEntities[$url->getStoreId()][$url->getEntityType()] : []; + + if (!\in_array($url->getEntityId(), $entityIds)) { + $entityIds[] = $url->getEntityId(); + } + $uniqueEntities[$url->getStoreId()][$url->getEntityType()] = $entityIds; + } + return $uniqueEntities; + } + /** * @inheritDoc */ @@ -289,7 +352,7 @@ protected function createFilterDataBasedOnUrls($urls) } /** - * {@inheritdoc} + * @inheritdoc */ public function deleteByData(array $data) { diff --git a/app/code/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrl.php b/app/code/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrl.php index 2ce00d53588b..e1bb094e7fc3 100644 --- a/app/code/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrl.php +++ b/app/code/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrl.php @@ -40,6 +40,8 @@ public function __construct( } /** + * Switch to another store. + * * @param StoreInterface $fromStore * @param StoreInterface $targetStore * @param string $redirectUrl @@ -66,17 +68,29 @@ public function switch(StoreInterface $fromStore, StoreInterface $targetStore, s UrlRewrite::STORE_ID => $oldStoreId, ]); if ($oldRewrite) { + $targetUrl = $targetStore->getBaseUrl(); // look for url rewrite match on the target store + $currentRewrite = $this->urlFinder->findOneByData([ + UrlRewrite::TARGET_PATH => $oldRewrite->getTargetPath(), + UrlRewrite::STORE_ID => $targetStore->getId(), + ]); + if ($currentRewrite) { + $targetUrl .= $currentRewrite->getRequestPath(); + } + } else { + $existingRewrite = $this->urlFinder->findOneByData([ + UrlRewrite::REQUEST_PATH => $urlPath + ]); $currentRewrite = $this->urlFinder->findOneByData([ UrlRewrite::REQUEST_PATH => $urlPath, UrlRewrite::STORE_ID => $targetStore->getId(), ]); - if (null === $currentRewrite) { + + if ($existingRewrite && !$currentRewrite) { /** @var \Magento\Framework\App\Response\Http $response */ $targetUrl = $targetStore->getBaseUrl(); } } - return $targetUrl; } } diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminUrlRewriteActionGroup.xml b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminUrlRewriteActionGroup.xml new file mode 100644 index 000000000000..50b83641e19a --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminUrlRewriteActionGroup.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 000000000000..1a9248ef3678 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminUrlRewriteGridActionGroup.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 000000000000..a299e1689d6a --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/StorefrontUrlRewriteRedirectActionGroup.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 000000000000..66eb3c9ba9f4 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,16 @@ + + + + + + URL Rewrites + URL Rewrites + magento-urlrewrite-urlrewrite + + 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 000000000000..3692e82072af --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Data/UrlRewriteData.xml @@ -0,0 +1,47 @@ + + + + + + test-test-test.html + http://www.example.com/ + 302 + Temporary (302) + 1 + Default Store View + End To End Test + + + test-aspx-test.aspx + http://www.example.com/ + 302 + Temporary (302) + 1 + Default Store View + Update Url Rewrite + + + wishlist + https://marketplace.magento.com/ + 301 + Permanent (301) + 1 + Default Store View + test_description_relative path + + + wishlist + https://marketplace.magento.com/ + 302 + Temporary (302) + 1 + Default Store View + test_description_relative path + + 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 000000000000..0738b17d6e0f --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Metadata/url_rewrite-meta.xml @@ -0,0 +1,18 @@ + + + + + + integer + integer + string + string + string + + 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 000000000000..b43e0e05ad55 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Page/AdminUrlRewriteEditPage.xml @@ -0,0 +1,14 @@ + + + + + +
+ + diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Page/AdminUrlRewriteIndexPage.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Page/AdminUrlRewriteIndexPage.xml new file mode 100644 index 000000000000..c7c450a00a0c --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Page/AdminUrlRewriteIndexPage.xml @@ -0,0 +1,14 @@ + + + + + +
+ + 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 000000000000..645396bc778e --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Page/AdminUrlRewriteProductPage.xml @@ -0,0 +1,14 @@ + + + + + +
+ + \ 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 000000000000..52939607f537 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Section/AdminUrlRewriteEditSection.xml @@ -0,0 +1,27 @@ + + + + +
+ + + + + + + + + + + + + + +
+
diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Section/AdminUrlRewriteIndexSection.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Section/AdminUrlRewriteIndexSection.xml new file mode 100644 index 000000000000..7b789845fe24 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Section/AdminUrlRewriteIndexSection.xml @@ -0,0 +1,26 @@ + + + + +
+ + + + + + + + + + + + + +
+
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 000000000000..3650ec56d139 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Section/AdminUrlRewriteProductSection.xml @@ -0,0 +1,18 @@ + + + + +
+ + + + + +
+
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 000000000000..52d313b21f3e --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminAutoUpdateURLRewriteWhenCategoryIsDeletedTest.xml @@ -0,0 +1,73 @@ + + + + + + + + <description value="Login as admin,verify UrlRewrite auto update when subcategory is deleted "/> + <testCaseId value="MC-5342"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Filter and Select the created Product --> + <actionGroup ref="AdminSearchUrlRewriteProductBySku" stepKey="searchProduct"> + <argument name="productSku" value="$$createSimpleProduct.sku$$"/> + </actionGroup> + + <!-- Update the Store, RequestPath, RedirectType and Description --> + <actionGroup ref="AdminAddUrlRewriteForProduct" stepKey="addUrlRewriteForProduct"> + <argument name="storeValue" value="Default Store View"/> + <argument name="requestPath" value="{{_defaultProduct.urlKey}}.html"/> + <argument name="redirectTypeValue" value="Temporary (302)"/> + <argument name="description" value="End To End Test"/> + </actionGroup> + + <!-- Delete Category --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!--Filter Product in product page and get the Product ID --> + <actionGroup ref="filterAndSelectProduct" stepKey="filterProduct"> + <argument name="productSku" value="$$createSimpleProduct.sku$$"/> + </actionGroup> + <grabFromCurrentUrl stepKey="productId" regex="#\/([0-9]*)?\/$#"/> + + <!-- Assert Redirect path, Target Path and Redirect type in grid --> + <actionGroup ref="AdminSearchByRequestPath" stepKey="searchByRequestPath"> + <argument name="redirectPath" value="$$createSimpleProduct.name$$.html" /> + <argument name="redirectType" value="No" /> + <argument name="targetPath" value="catalog/product/view/id/{$productId}"/> + </actionGroup> + + <!-- Assert Redirect path, Target Path and Redirect type in grid --> + <actionGroup ref="AdminSearchByRequestPath" stepKey="searchByRequestPath1"> + <argument name="redirectPath" value="{{_defaultProduct.urlKey}}.html" /> + <argument name="redirectType" value="Temporary (302)"/> + <argument name="targetPath" value="$$createSimpleProduct.name$$.html"/> + </actionGroup> + + <!--Assert Category Url Redirect is not present --> + <actionGroup ref="AdminSearchDeletedUrlRewrite" stepKey="searchDeletedCategory"> + <argument name="requestPath" value="$$createCategory.name$$.html"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest.xml new file mode 100644 index 000000000000..2c2dd48caeaa --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest.xml @@ -0,0 +1,117 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest"> + <annotations> + <features value="Url Rewrite"/> + <stories value="Url Rewrites for Multiple Storeviews"/> + <title value="Url Rewrites Correctly Generated for Multiple Storeviews During Product Import"/> + <description value="Check Url Rewrites Correctly Generated for Multiple Storeviews During Product Import."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-68980"/> + <group value="urlRewrite"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!-- Create Store View EN --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreViewEn"> + <argument name="customStore" value="customStoreENNotUnique"/> + </actionGroup> + <!-- Create Store View NL --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreViewNl"> + <argument name="customStore" value="customStoreNLNotUnique"/> + </actionGroup> + <createData entity="ApiCategory" stepKey="createCategory"> + <field key="name">category-admin</field> + </createData> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="deleteProductByName" stepKey="deleteImportedProduct"> + <argument name="sku" value="productformagetwo68980"/> + <argument name="name" value="productformagetwo68980"/> + </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearFiltersIfSet"/> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> + <argument name="customStore" value="customStoreENNotUnique"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewNl"> + <argument name="customStore" value="customStoreNLNotUnique"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="switchCategoryStoreView" stepKey="switchToStoreViewEn"> + <argument name="Store" value="customStoreENNotUnique.name"/> + <argument name="CatName" value="$$createCategory.name$$"/> + </actionGroup> + <uncheckOption selector="{{AdminCategoryBasicFieldSection.categoryNameUseDefault}}" stepKey="uncheckUseDefaultValueENStoreView"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="category-english" stepKey="changeNameField"/> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="clickOnSectionHeader"/> + <actionGroup ref="ChangeSeoUrlKeyForSubCategory" stepKey="changeSeoUrlKeyENStoreView"> + <argument name="value" value="category-english"/> + </actionGroup> + <actionGroup ref="switchCategoryStoreView" stepKey="switchToStoreViewNl"> + <argument name="Store" value="customStoreNLNotUnique.name"/> + <argument name="CatName" value="$$createCategory.name$$"/> + </actionGroup> + <uncheckOption selector="{{AdminCategoryBasicFieldSection.categoryNameUseDefault}}" stepKey="uncheckUseDefaultValue1"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="category-dutch" stepKey="changeNameFieldNLStoreView"/> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="clickOnSectionHeader2"/> + <actionGroup ref="ChangeSeoUrlKeyForSubCategory" stepKey="changeSeoUrlKeyNLStoreView"> + <argument name="value" value="category-dutch"/> + </actionGroup> + <amOnPage url="{{AdminImportIndexPage.url}}" stepKey="navigateToSystemImport"/> + <selectOption selector="{{AdminImportMainSection.entityType}}" userInput="Products" stepKey="selectProductsOption"/> + <waitForElementVisible selector="{{AdminImportMainSection.importBehavior}}" stepKey="waitForImportBehaviorElementVisible"/> + <selectOption selector="{{AdminImportMainSection.importBehavior}}" userInput="Add/Update" stepKey="selectAddUpdateOption"/> + <attachFile selector="{{AdminImportMainSection.selectFileToImport}}" userInput="import_updated.csv" stepKey="attachFileForImport"/> + <click selector="{{AdminImportHeaderSection.checkDataButton}}" stepKey="clickCheckDataButton"/> + <see selector="{{AdminMessagesSection.notice}}" userInput="Checked rows: 3, checked entities: 1, invalid rows: 0, total errors: 0" stepKey="assertNotice"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="File is valid! To start import process press "Import" button" stepKey="assertSuccessMessage"/> + <click selector="{{AdminImportMainSection.importButton}}" stepKey="clickImportButton"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="Import successfully done" stepKey="assertSuccessMessage1"/> + <see selector="{{AdminMessagesSection.notice}}" userInput="Created: 1, Updated: 0, Deleted: 0" stepKey="assertNotice1"/> + <actionGroup ref="SearchForProductOnBackendByNameActionGroup" stepKey="searchForProductOnBackend"> + <argument name="productName" value="productformagetwo68980"/> + </actionGroup> + <click selector="{{AdminProductGridSection.productRowBySku('productformagetwo68980')}}" stepKey="clickOnProductRow"/> + <grabFromCurrentUrl regex="~/id/(\d+)/~" stepKey="grabProductIdFromUrl"/> + <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="goToUrlRewritesIndexPage"/> + + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="category-english.html" stepKey="inputCategoryUrlForENStoreView"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearchButton"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue('category-english.html')}}" stepKey="seeUrlInRequestPathColumn"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.targetPathColumnValue(catalog/category/view/id/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn"/> + + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="category-dutch.html" stepKey="inputCategoryUrlForNLStoreView"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearchButton1"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue('category-dutch.html')}}" stepKey="seeUrlInRequestPathColumn1"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.targetPathColumnValue(catalog/category/view/id/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn1"/> + + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="productformagetwo68980-english.html" stepKey="inputProductUrlForENStoreView"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearchButton2"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue('productformagetwo68980-english.html')}}" stepKey="seeUrlInRequestPathColumn2"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.targetPathColumnValue('catalog/product/view/id/$grabProductIdFromUrl')}}" stepKey="seeUrlInTargetPathColumn2"/> + + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="productformagetwo68980-dutch.html" stepKey="inputProductUrlForENStoreView1"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearchButton3"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue('productformagetwo68980-dutch.html')}}" stepKey="seeUrlInRequestPathColumn3"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.targetPathColumnValue('catalog/product/view/id/$grabProductIdFromUrl')}}" stepKey="seeUrlInTargetPathColumn3"/> + + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="category-english/productformagetwo68980-english.html" stepKey="inputProductUrlForENStoreView2"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearchButton4"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue('category-english/productformagetwo68980-english.html')}}" stepKey="seeUrlInRequestPathColumn4"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.targetPathColumnValue(catalog/product/view/id/$grabProductIdFromUrl/category/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn4"/> + + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="category-dutch/productformagetwo68980-dutch.html" stepKey="inputProductUrlForENStoreView3"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearchButton5"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue('category-dutch/productformagetwo68980-dutch.html')}}" stepKey="seeUrlInRequestPathColumn5"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.targetPathColumnValue(catalog/product/view/id/$grabProductIdFromUrl/category/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn5"/> + </test> +</tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesInCatalogCategoriesAfterChangingUrlKeyForStoreViewAndMovingCategoryTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesInCatalogCategoriesAfterChangingUrlKeyForStoreViewAndMovingCategoryTest.xml new file mode 100644 index 000000000000..52dce4d67f69 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesInCatalogCategoriesAfterChangingUrlKeyForStoreViewAndMovingCategoryTest.xml @@ -0,0 +1,82 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCheckUrlRewritesInCatalogCategoriesAfterChangingUrlKeyForStoreViewAndMovingCategoryTest"> + <annotations> + <features value="Url Rewrite"/> + <stories value="Update url rewrites"/> + <title value="Check url rewrites in catalog categories after changing url key"/> + <description value="Check url rewrites in catalog categories after changing url key for store view and moving category"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-5352"/> + <group value="url_rewrite"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Create two sub-categories in default category with simple products --> + <createData entity="_defaultCategory" stepKey="createFirstCategory"/> + <createData entity="_defaultProduct" stepKey="createFirstSimpleProduct"> + <requiredEntity createDataKey="createFirstCategory"/> + </createData> + <createData entity="_defaultCategory" stepKey="createSecondCategory"/> + <createData entity="_defaultProduct" stepKey="createSecondSimpleProduct"> + <requiredEntity createDataKey="createSecondCategory"/> + </createData> + + <!-- Log in to backend --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!--Create additional Store View in Main Website Store --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"/> + </before> + <after> + <deleteData createDataKey="createFirstCategory" stepKey="deleteFirstCategory"/> + <deleteData createDataKey="createSecondCategory" stepKey="deleteSecondCategory"/> + <deleteData createDataKey="createFirstSimpleProduct" stepKey="deleteFirstSimpleProduct"/> + <deleteData createDataKey="createSecondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- On the categories editing page change store view to created additional view --> + <actionGroup ref="switchCategoryStoreView" stepKey="switchStoreView"> + <argument name="Store" value="customStore.name"/> + <argument name="CatName" value="$$createFirstCategory.name$$"/> + </actionGroup> + + <!-- Change url key for category for first category; save --> + <actionGroup ref="ChangeSeoUrlKeyForSubCategory" stepKey="changeUrlKey"> + <argument name="value" value="{{SimpleRootSubCategory.url_key}}"/> + </actionGroup> + + <!-- Change store view to "All store views" for first category --> + <actionGroup ref="switchCategoryToAllStoreView" stepKey="switchToAllStoreViewProduct"> + <argument name="CatName" value="$$createFirstCategory.name$$"/> + </actionGroup> + + <!-- Move first category inside second category --> + <actionGroup ref="MoveCategoryActionGroup" stepKey="moveFirstCategoryToSecondCategory"> + <argument name="childCategory" value="$$createFirstCategory.name$$"/> + <argument name="parentCategory" value="$$createSecondCategory.name$$"/> + </actionGroup> + + <!-- Switch default store view on store view created below for first category --> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnStorefrontPage"/> + <waitForPageLoad stepKey="waitForStorefrontPageLoad"/> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="storefrontSwitchStoreView"> + <argument name="storeView" value="customStore"/> + </actionGroup> + + <!-- Assert category url with custom store view --> + <amOnPage url="{{StorefrontHomePage.url}}$$createSecondCategory.name$$/{{SimpleRootSubCategory.url_key}}.html" stepKey="amOnCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <dontSee userInput="$$createSecondSimpleProduct.name$$" selector="{{StorefrontCategoryMainSection.productsList}}" stepKey="dontSeeProductInCategory"/> + <see selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" userInput="We can't find products matching the selection." stepKey="seeEmptyProductMessage"/> + </test> +</tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCategoryUrlRewriteAndAddNoRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateCategoryUrlRewriteAndAddNoRedirectTest.xml new file mode 100644 index 000000000000..a7a7c0c73d82 --- /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 000000000000..974550bb9221 --- /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 000000000000..c64019ea38ac --- /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 000000000000..358aa58aba0f --- /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 000000000000..e6ee9b484059 --- /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 000000000000..b123bc14cb1e --- /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 000000000000..711d5389b013 --- /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 000000000000..f8d297c92a17 --- /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 000000000000..ae18ab33ba6c --- /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 000000000000..66c586d4fe89 --- /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 000000000000..2d797a12bedf --- /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 000000000000..83c1e5c0a5e0 --- /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 000000000000..cf4593102977 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminDeleteCustomUrlRewriteTest.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminDeleteCustomUrlRewriteTest"> + <annotations> + <stories value="Delete custom URL rewrite"/> + <title value="Delete custom URL rewrite"/> + <description value="Test log in to URL rewrite and Delete custom URL rewrite"/> + <testCaseId value="MC-5350"/> + <severity value="CRITICAL"/> + <group value="urlRewrite"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="defaultUrlRewrite" stepKey="urlRewrite" /> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Delete created custom url rewrite and verify AssertUrlRewriteDeletedMessage--> + <actionGroup ref="AdminDeleteUrlRewrite" stepKey="deleteUrlRewrite"> + <argument name="requestPath" value="$$urlRewrite.request_path$$"/> + </actionGroup> + + <!--Search and verify AssertUrlRewriteNotInGrid--> + <actionGroup ref="AdminSearchDeletedUrlRewrite" stepKey="searchDeletedUrlRewriteInGrid"> + <argument name="requestPath" value="$$urlRewrite.request_path$$"/> + </actionGroup> + + <!--Verify AssertPageByUrlRewriteIsNotFound--> + <actionGroup ref="AssertPageByUrlRewriteIsNotFound" stepKey="amOnPage"> + <argument name="requestPath" value="$$urlRewrite.request_path$$"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminMarketingUrlRewritesNavigateMenuTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminMarketingUrlRewritesNavigateMenuTest.xml new file mode 100644 index 000000000000..443307b427b4 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminMarketingUrlRewritesNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMarketingUrlRewritesNavigateMenuTest"> + <annotations> + <features value="UrlRewrite"/> + <stories value="Menu Navigation"/> + <title value="Admin marketing url rewrites navigate menu test"/> + <description value="Admin should be able to navigate to Marketing > URL Rewrites"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14202"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToMarketingURLRewritesPage"> + <argument name="menuUiId" value="{{AdminMenuMarketing.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuMarketingSEOAndSearchURLRewrites.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuMarketingSEOAndSearchURLRewrites.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCategoryUrlRewriteAndAddAspxRequestPathTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCategoryUrlRewriteAndAddAspxRequestPathTest.xml new file mode 100644 index 000000000000..072753505223 --- /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 000000000000..80b9dbe41bf5 --- /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 000000000000..be9fd1d83c8f --- /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 000000000000..7e1b9acbc47a --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCategoryUrlRewriteAndAddTemporaryRedirectTest.xml @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateCategoryUrlRewriteAndAddTemporaryRedirectTest"> + <annotations> + <stories value="Update URL rewrite"/> + <title value="Update Category URL Rewrites, Temporary redirect type"/> + <description value="Login as Admin and update category UrlRewrite and add Temporary redirect type"/> + <testCaseId value="MC-5356"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search and Select Edit option for created category in grid --> + <actionGroup ref="AdminSearchAndSelectUrlRewriteInGrid" stepKey="editUrlRewrite"> + <argument name="requestPath" value="$$category.custom_attributes[url_key]$$.html"/> + </actionGroup> + + <!-- Open UrlRewrite Edit page and update the fields --> + <actionGroup ref="AdminUpdateUrlRewrite" stepKey="updateCategoryUrlRewrite"> + <argument name="storeValue" value="Default Store View"/> + <argument name="requestPath" value="{{updateUrlRewrite.request_path}}"/> + <argument name="redirectTypeValue" value="Temporary (302)"/> + <argument name="description" value="Update Category Url Rewrite"/> + </actionGroup> + + <!-- Open Category Page and Get Category ID --> + <actionGroup ref="OpenCategoryFromCategoryTree" stepKey="getCategoryId"> + <argument name="category" value="$$category.name$$"/> + </actionGroup> + <grabFromCurrentUrl stepKey="categoryId" regex="#\/([0-9]*)?\/$#"/> + + <!--Assert category Url Rewrite in grid --> + <actionGroup ref="AdminSearchByRequestPath" stepKey="searchByNewRequestPath"> + <argument name="redirectPath" value="{{updateUrlRewrite.request_path}}" /> + <argument name="redirectType" value="Temporary (302)" /> + <argument name="targetPath" value="catalog/category/view/id/{$categoryId}"/> + </actionGroup> + + <!-- Assert category redirect in Store Front --> + <actionGroup ref="AssertStorefrontUrlRewriteRedirect" stepKey="verifyCategoryInStoreFront"> + <argument name="category" value="$$category.name$$"/> + <argument name="newRequestPath" value="{{updateUrlRewrite.request_path}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCustomURLRewritesPermanentTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCustomURLRewritesPermanentTest.xml new file mode 100644 index 000000000000..8339eb63abef --- /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 000000000000..07d578cbbeca --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateCustomURLRewritesTemporaryTest.xml @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateCustomURLRewritesTemporaryTest"> + <annotations> + <stories value="Update Custom URL Rewrites"/> + <title value="Update Custom URL Rewrites, temporary"/> + <description value="Test log in to URL Rewrites and Update Custom URL Rewrites, temporary"/> + <testCaseId value="MC-5354"/> + <severity value="CRITICAL"/> + <group value="urlRewrite"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="defaultUrlRewrite" stepKey="urlRewrite"/> + <createData entity="defaultSimpleProduct" stepKey="createProduct"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel"/> + </before> + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminDeleteUrlRewrite" stepKey="deleteCustomUrlRewrite"> + <argument name="requestPath" value="{{customTemporaryUrlRewrite.request_path}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Filter Product in product page and get the Product ID --> + <actionGroup ref="filterAndSelectProduct" stepKey="filterProduct"> + <argument name="productSku" value="$$createProduct.sku$$"/> + </actionGroup> + <grabFromCurrentUrl stepKey="productId" regex="#\/([0-9]*)?\/$#"/> + + <!--Search default custom url rewrite in grid--> + <actionGroup ref="AdminSearchAndSelectUrlRewriteInGrid" stepKey="searchUrlRewrite"> + <argument name="requestPath" value="$$urlRewrite.request_path$$"/> + </actionGroup> + + <!--Update default custom url rewrite as per requirement and verify AssertUrlRewriteSaveMessage--> + <actionGroup ref="AdminUpdateCustomUrlRewrite" stepKey="updateUrlRewrite"> + <argument name="storeValue" value="Default Store View"/> + <argument name="requestPath" value="{{customTemporaryUrlRewrite.request_path}}"/> + <argument name="targetPath" value="catalog/product/view/id/{$productId}"/> + <argument name="redirectTypeValue" value="{{customTemporaryUrlRewrite.redirect_type_label}}"/> + <argument name="description" value="{{customTemporaryUrlRewrite.description}}"/> + </actionGroup> + + <!--Search and verify AssertUrlRewriteInGrid--> + <actionGroup ref="AdminSearchByRequestPath" stepKey="verifyUpdatedUrlRewriteInGrid"> + <argument name="redirectPath" value="{{customTemporaryUrlRewrite.request_path}}"/> + <argument name="redirectType" value="{{customTemporaryUrlRewrite.redirect_type_label}}"/> + <argument name="targetPath" value="catalog/product/view/id/{$productId}"/> + </actionGroup> + + <!-- AssertUrlRewriteCustomSearchRedirect--> + <actionGroup ref="AssertStorefrontProductRedirect" stepKey="verifyProductInStoreFrontPage"> + <argument name="productName" value="$$createProduct.name$$"/> + <argument name="productSku" value="$$createProduct.sku$$"/> + <argument name="productRequestPath" value="$$createProduct.name$$.html"/> + </actionGroup> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateProductUrlRewriteAndAddTemporaryRedirectTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateProductUrlRewriteAndAddTemporaryRedirectTest.xml new file mode 100644 index 000000000000..ea370d841958 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUpdateProductUrlRewriteAndAddTemporaryRedirectTest.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateProductUrlRewriteAndAddTemporaryRedirectTest"> + <annotations> + <stories value="URL Rewrite"/> + <title value="Update Product URL Rewrites"/> + <description value="Login as Admin and update product UrlRewrite and add Temporary redirect type "/> + <testCaseId value="MC-5351"/> + <severity value="CRITICAL"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <createData entity="defaultSimpleProduct" stepKey="createProduct"/> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!-- Search and Select Edit option for created product in grid --> + <actionGroup ref="AdminSearchAndSelectUrlRewriteInGrid" stepKey="editUrlRewrite"> + <argument name="requestPath" value="$$createProduct.name$$"/> + </actionGroup> + + <!-- Open UrlRewrite Edit page and update the fields --> + <actionGroup ref="AdminUpdateUrlRewrite" stepKey="updateCategoryUrlRewrite"> + <argument name="storeValue" value="{{updateUrlRewrite.store}}"/> + <argument name="requestPath" value="{{updateUrlRewrite.request_path}}"/> + <argument name="redirectTypeValue" value="{{updateUrlRewrite.redirect_type_label}}"/> + <argument name="description" value="{{updateUrlRewrite.description}}"/> + </actionGroup> + + <!-- Assert product Url Rewrite in StoreFront --> + <actionGroup ref="AssertStorefrontProductRedirect" stepKey="assertProductUrlRewriteInStoreFront"> + <argument name="productName" value="$$createProduct.name$$"/> + <argument name="productSku" value="$$createProduct.sku$$"/> + <argument name="productRequestPath" value="{{updateUrlRewrite.request_path}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest.xml new file mode 100644 index 000000000000..0d9df9176d2b --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest.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="AdminUrlRewritesForProductInAnchorCategoriesTest"> + <annotations> + <features value="Url Rewrite"/> + <stories value="Url-rewrites for product in anchor categories"/> + <title value="Url-rewrites for product in anchor categories"/> + <description value="For a product with category that has parent anchor categories, the rewrites is created when the category/product is saved."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-69826"/> + <group value="urlRewrite"/> + </annotations> + + <!-- Preconditions--> + <!-- Create 3 categories --> + <before> + <createData entity="SimpleSubCategory" stepKey="simpleSubCategory1"/> + <createData entity="SubCategoryWithParent" stepKey="simpleSubCategory2"> + <requiredEntity createDataKey="simpleSubCategory1"/> + </createData> + <createData entity="SubCategoryWithParent" stepKey="simpleSubCategory3"> + <requiredEntity createDataKey="simpleSubCategory2"/> + </createData> + <!-- Create Simple product 1 and assign it to Category 3 --> + <createData entity="ApiSimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="simpleSubCategory3"/> + </createData> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="simpleSubCategory1" stepKey="deletesimpleSubCategory1"/> + <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + </after> + <!-- Steps --> + <!-- 1. Log in to Admin --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- 2. Open Marketing - SEO & Search - URL Rewrites --> + <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="amOnUrlRewriteIndexPage"/> + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="$createSimpleProduct.custom_attributes[url_key]$.html" stepKey="inputProductName"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearchButton"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue($createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeValue1"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue($simpleSubCategory1.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeValue2"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue($simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeValue3"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue($simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeValue4"/> + + <!-- 3. Edit Category 1 for DEFAULT Store View: --> + <actionGroup ref="switchCategoryStoreView" stepKey="switchStoreView"> + <argument name="Store" value="_defaultStore.name"/> + <argument name="CatName" value="$$simpleSubCategory1.name$$"/> + </actionGroup> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSeoSection2"/> + <uncheckOption selector="{{AdminCategorySEOSection.UrlKeyDefaultValueCheckbox}}" stepKey="uncheckRedirect2"/> + <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="$simpleSubCategory1.custom_attributes[url_key]$-new" stepKey="changeURLKey"/> + <checkOption selector="{{AdminCategorySEOSection.UrlKeyRedirectCheckbox}}" stepKey="checkUrlKeyRedirect"/> + <!-- 4. Save Category 1 --> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategory"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccessMessageAfterSaved"/> + + <!-- 5. Open Marketing - SEO & Search - URL Rewrites --> + <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="amOnUrlRewriteIndexPage2"/> + <fillField selector="{{AdminUrlRewriteIndexSection.requestPathFilter}}" userInput="$createSimpleProduct.custom_attributes[url_key]$.html" stepKey="inputProductName2"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearchButton2"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue($createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue1"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue($simpleSubCategory1.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue2"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue($simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue3"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue($simpleSubCategory1.custom_attributes[url_key]$/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue4"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue($simpleSubCategory1.custom_attributes[url_key]$-new/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue5"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue($simpleSubCategory1.custom_attributes[url_key]$-new/$simpleSubCategory2.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue6"/> + <seeElement selector="{{AdminUrlRewriteIndexSection.requestPathColumnValue($simpleSubCategory1.custom_attributes[url_key]$-new/$simpleSubCategory2.custom_attributes[url_key]$/$simpleSubCategory3.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue7"/> + </test> +</tests> diff --git a/app/code/Magento/UrlRewrite/Test/Unit/Model/Storage/DbStorageTest.php b/app/code/Magento/UrlRewrite/Test/Unit/Model/Storage/DbStorageTest.php index b96a8ef63740..697ce33be0fa 100644 --- a/app/code/Magento/UrlRewrite/Test/Unit/Model/Storage/DbStorageTest.php +++ b/app/code/Magento/UrlRewrite/Test/Unit/Model/Storage/DbStorageTest.php @@ -445,7 +445,6 @@ public function testReplace() $urlSecond = $this->createMock(\Magento\UrlRewrite\Service\V1\Data\UrlRewrite::class); // delete - $urlFirst->expects($this->any()) ->method('getEntityType') ->willReturn('product'); @@ -479,10 +478,6 @@ public function testReplace() ->with(DbStorage::TABLE_NAME) ->will($this->returnValue('table_name')); - $this->connectionMock->expects($this->any()) - ->method('query') - ->with('sql delete query'); - // insert $urlFirst->expects($this->any()) @@ -497,10 +492,6 @@ public function testReplace() ->with(DbStorage::TABLE_NAME) ->will($this->returnValue('table_name')); - $this->connectionMock->expects($this->once()) - ->method('insertMultiple') - ->with('table_name', [['row1'], ['row2']]); - $this->storage->replace([$urlFirst, $urlSecond]); } diff --git a/app/code/Magento/UrlRewrite/etc/db_schema.xml b/app/code/Magento/UrlRewrite/etc/db_schema.xml index af328873deee..6e0014873202 100644 --- a/app/code/Magento/UrlRewrite/etc/db_schema.xml +++ b/app/code/Magento/UrlRewrite/etc/db_schema.xml @@ -23,17 +23,17 @@ <column xsi:type="smallint" name="is_autogenerated" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Is rewrite generated automatically flag"/> <column xsi:type="varchar" name="metadata" nullable="true" length="255" comment="Meta data for url rewrite"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="url_rewrite_id"/> </constraint> - <constraint xsi:type="unique" name="URL_REWRITE_REQUEST_PATH_STORE_ID"> + <constraint xsi:type="unique" referenceId="URL_REWRITE_REQUEST_PATH_STORE_ID"> <column name="request_path"/> <column name="store_id"/> </constraint> - <index name="URL_REWRITE_TARGET_PATH" indexType="btree"> + <index referenceId="URL_REWRITE_TARGET_PATH" indexType="btree"> <column name="target_path"/> </index> - <index name="URL_REWRITE_STORE_ID_ENTITY_ID" indexType="btree"> + <index referenceId="URL_REWRITE_STORE_ID_ENTITY_ID" indexType="btree"> <column name="store_id"/> <column name="entity_id"/> </index> diff --git a/app/code/Magento/UrlRewrite/view/adminhtml/layout/adminhtml_url_rewrite_index.xml b/app/code/Magento/UrlRewrite/view/adminhtml/layout/adminhtml_url_rewrite_index.xml index 7d8151d27030..012409a8b6ac 100644 --- a/app/code/Magento/UrlRewrite/view/adminhtml/layout/adminhtml_url_rewrite_index.xml +++ b/app/code/Magento/UrlRewrite/view/adminhtml/layout/adminhtml_url_rewrite_index.xml @@ -12,8 +12,10 @@ <block class="Magento\Backend\Block\Widget\Grid" name="adminhtml.block.url_rewrite.grid" as="grid"> <arguments> <argument name="id" xsi:type="string">urlrewriteGrid</argument> - <argument name="dataSource" xsi:type="object">Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollection</argument> <argument name="default_sort" xsi:type="string">url_rewrite_id</argument> + <!-- Add below argument to save session parameter in URL rewrite grid --> + <argument name="save_parameters_in_session" xsi:type="string">1</argument> </arguments> <block class="Magento\Backend\Block\Widget\Grid\ColumnSet" as="grid.columnSet" name="adminhtml.url_rewrite.grid.columnSet"> <arguments> diff --git a/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php b/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php new file mode 100644 index 000000000000..c842d660a617 --- /dev/null +++ b/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php @@ -0,0 +1,149 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\UrlRewriteGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\UrlRewrite\Model\UrlFinderInterface; +use Magento\UrlRewriteGraphQl\Model\Resolver\UrlRewrite\CustomUrlLocatorInterface; + +/** + * UrlRewrite field resolver, used for GraphQL request processing. + */ +class EntityUrl implements ResolverInterface +{ + /** + * @var UrlFinderInterface + */ + private $urlFinder; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var CustomUrlLocatorInterface + */ + private $customUrlLocator; + + /** + * @param UrlFinderInterface $urlFinder + * @param StoreManagerInterface $storeManager + * @param CustomUrlLocatorInterface $customUrlLocator + */ + public function __construct( + UrlFinderInterface $urlFinder, + StoreManagerInterface $storeManager, + CustomUrlLocatorInterface $customUrlLocator + ) { + $this->urlFinder = $urlFinder; + $this->storeManager = $storeManager; + $this->customUrlLocator = $customUrlLocator; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($args['url']) || empty(trim($args['url']))) { + throw new GraphQlInputException(__('"url" argument should be specified and not empty')); + } + + $result = null; + $url = $args['url']; + if (substr($url, 0, 1) === '/' && $url !== '/') { + $url = ltrim($url, '/'); + } + $customUrl = $this->customUrlLocator->locateUrl($url); + $url = $customUrl ?: $url; + $urlRewrite = $this->findCanonicalUrl($url); + if ($urlRewrite) { + $result = [ + 'id' => $urlRewrite->getEntityId(), + 'canonical_url' => $urlRewrite->getTargetPath(), + 'relative_url' => $urlRewrite->getTargetPath(), + 'type' => $this->sanitizeType($urlRewrite->getEntityType()) + ]; + } + return $result; + } + + /** + * Find the canonical url passing through all redirects if any + * + * @param string $requestPath + * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite|null + */ + private function findCanonicalUrl(string $requestPath) : ?\Magento\UrlRewrite\Service\V1\Data\UrlRewrite + { + $urlRewrite = $this->findUrlFromRequestPath($requestPath); + if ($urlRewrite && $urlRewrite->getRedirectType() > 0) { + while ($urlRewrite && $urlRewrite->getRedirectType() > 0) { + $urlRewrite = $this->findUrlFromRequestPath($urlRewrite->getTargetPath()); + } + } + if (!$urlRewrite) { + $urlRewrite = $this->findUrlFromTargetPath($requestPath); + } + + return $urlRewrite; + } + + /** + * Find a url from a request url on the current store + * + * @param string $requestPath + * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite|null + */ + private function findUrlFromRequestPath(string $requestPath) : ?\Magento\UrlRewrite\Service\V1\Data\UrlRewrite + { + return $this->urlFinder->findOneByData( + [ + 'request_path' => $requestPath, + 'store_id' => $this->storeManager->getStore()->getId() + ] + ); + } + + /** + * Find a url from a target url on the current store + * + * @param string $targetPath + * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite|null + */ + private function findUrlFromTargetPath(string $targetPath) : ?\Magento\UrlRewrite\Service\V1\Data\UrlRewrite + { + return $this->urlFinder->findOneByData( + [ + 'target_path' => $targetPath, + 'store_id' => $this->storeManager->getStore()->getId() + ] + ); + } + + /** + * Sanitize the type to fit schema specifications + * + * @param string $type + * @return string + */ + private function sanitizeType(string $type) : string + { + return strtoupper(str_replace('-', '_', $type)); + } +} diff --git a/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/UrlRewrite.php b/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/UrlRewrite.php index 3f07edc60190..fb7bbd634d11 100644 --- a/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/UrlRewrite.php +++ b/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/UrlRewrite.php @@ -7,17 +7,16 @@ namespace Magento\UrlRewriteGraphQl\Model\Resolver; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Query\Resolver\Value; -use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; -use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\Model\AbstractModel; use Magento\UrlRewrite\Model\UrlFinderInterface; -use Magento\UrlRewriteGraphQl\Model\Resolver\UrlRewrite\CustomUrlLocatorInterface; +use Magento\UrlRewrite\Service\V1\Data\UrlRewrite as UrlRewriteDTO; /** - * UrlRewrite field resolver, used for GraphQL request processing. + * Returns URL rewrites list for the specified product */ class UrlRewrite implements ResolverInterface { @@ -26,41 +25,17 @@ class UrlRewrite implements ResolverInterface */ private $urlFinder; - /** - * @var StoreManagerInterface - */ - private $storeManager; - - /** - * @var ValueFactory - */ - private $valueFactory; - - /** - * @var CustomUrlLocatorInterface - */ - private $customUrlLocator; - /** * @param UrlFinderInterface $urlFinder - * @param StoreManagerInterface $storeManager - * @param ValueFactory $valueFactory - * @param CustomUrlLocatorInterface $customUrlLocator */ public function __construct( - UrlFinderInterface $urlFinder, - StoreManagerInterface $storeManager, - ValueFactory $valueFactory, - CustomUrlLocatorInterface $customUrlLocator + UrlFinderInterface $urlFinder ) { $this->urlFinder = $urlFinder; - $this->storeManager = $storeManager; - $this->valueFactory = $valueFactory; - $this->customUrlLocator = $customUrlLocator; } /** - * {@inheritdoc} + * @inheritdoc */ public function resolve( Field $field, @@ -68,94 +43,51 @@ public function resolve( ResolveInfo $info, array $value = null, array $args = null - ) : Value { - $result = function () { - return null; - }; - - if (isset($args['url'])) { - $url = $args['url']; - if (substr($url, 0, 1) === '/' && $url !== '/') { - $url = ltrim($url, '/'); - } - $customUrl = $this->customUrlLocator->locateUrl($url); - $url = $customUrl ?: $url; - $urlRewrite = $this->findCanonicalUrl($url); - if ($urlRewrite) { - $urlRewriteReturnArray = [ - 'id' => $urlRewrite->getEntityId(), - 'canonical_url' => $urlRewrite->getTargetPath(), - 'type' => $this->sanitizeType($urlRewrite->getEntityType()) - ]; - $result = function () use ($urlRewriteReturnArray) { - return $urlRewriteReturnArray; - }; - } + ): array { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); } - return $this->valueFactory->create($result); - } - /** - * Find the canonical url passing through all redirects if any - * - * @param string $requestPath - * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite|null - */ - private function findCanonicalUrl(string $requestPath) : ?\Magento\UrlRewrite\Service\V1\Data\UrlRewrite - { - $urlRewrite = $this->findUrlFromRequestPath($requestPath); - if ($urlRewrite && $urlRewrite->getRedirectType() > 0) { - while ($urlRewrite && $urlRewrite->getRedirectType() > 0) { - $urlRewrite = $this->findUrlFromRequestPath($urlRewrite->getTargetPath()); + /** @var AbstractModel $entity */ + $entity = $value['model']; + $entityId = $entity->getEntityId(); + + $urlRewriteCollection = $this->urlFinder->findAllByData([UrlRewriteDTO::ENTITY_ID => $entityId]); + $urlRewrites = []; + + /** @var UrlRewriteDTO $urlRewrite */ + foreach ($urlRewriteCollection as $urlRewrite) { + if ($urlRewrite->getRedirectType() !== 0) { + continue; } + + $urlRewrites[] = [ + 'url' => $urlRewrite->getRequestPath(), + 'parameters' => $this->getUrlParameters($urlRewrite->getTargetPath()) + ]; } - if (!$urlRewrite) { - $urlRewrite = $this->findUrlFromTargetPath($requestPath); - } - - return $urlRewrite; - } - /** - * Find a url from a request url on the current store - * - * @param string $requestPath - * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite|null - */ - private function findUrlFromRequestPath(string $requestPath) : ?\Magento\UrlRewrite\Service\V1\Data\UrlRewrite - { - return $this->urlFinder->findOneByData( - [ - 'request_path' => $requestPath, - 'store_id' => $this->storeManager->getStore()->getId() - ] - ); + return $urlRewrites; } /** - * Find a url from a target url on the current store + * Parses target path and extracts parameters * * @param string $targetPath - * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite|null + * @return array */ - private function findUrlFromTargetPath(string $targetPath) : ?\Magento\UrlRewrite\Service\V1\Data\UrlRewrite + private function getUrlParameters(string $targetPath): array { - return $this->urlFinder->findOneByData( - [ - 'target_path' => $targetPath, - 'store_id' => $this->storeManager->getStore()->getId() - ] - ); - } + $urlParameters = []; + $targetPathParts = explode('/', trim($targetPath, '/')); - /** - * Sanitize the type to fit schema specifications - * - * @param string $type - * @return string - */ - private function sanitizeType(string $type) : string - { - return strtoupper(str_replace('-', '_', $type)); + for ($i = 3; ($i < sizeof($targetPathParts) - 1); $i += 2) { + $urlParameters[] = [ + 'name' => $targetPathParts[$i], + 'value' => $targetPathParts[$i + 1] + ]; + } + + return $urlParameters; } } diff --git a/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls b/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls index 38f1d9c65637..e9033880704c 100644 --- a/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls @@ -1,15 +1,26 @@ # 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") { +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") @cache(cacheable: false) +} + +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 @doc(description: "The internal relative URL. If the specified url is a redirect, the query returns the redirected URL, not the original.") + 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.") } -type Query { - urlResolver(url: String!): EntityUrl @resolver(class: "Magento\\UrlRewriteGraphQl\\Model\\Resolver\\UrlRewrite") @doc(description: "The urlResolver query returns the canonical URL for a specified product, category or CMS page") +enum UrlRewriteEntityTypeEnum { } -enum UrlRewriteEntityTypeEnum { +type UrlRewrite @doc(description: "The object contains URL rewrite details") { + url: String @doc(description: "Request URL") + parameters: [HttpQueryParameter] @doc(description: "Request parameters") +} + +type HttpQueryParameter @doc(description: "The object details of target path parameters") { + name: String @doc(description: "Parameter name") + value: String @doc(description: "Parameter value") } diff --git a/app/code/Magento/User/Block/Buttons.php b/app/code/Magento/User/Block/Buttons.php index b411f1c0cf8c..f580c5cd72b9 100644 --- a/app/code/Magento/User/Block/Buttons.php +++ b/app/code/Magento/User/Block/Buttons.php @@ -6,6 +6,8 @@ namespace Magento\User\Block; /** + * Buttons block + * * @api * @since 100.0.2 */ @@ -33,6 +35,8 @@ public function __construct( } /** + * Prepare layout + * * @return $this */ protected function _prepareLayout() @@ -53,7 +57,7 @@ protected function _prepareLayout() ['label' => __('Reset'), 'onclick' => 'window.location.reload()', 'class' => 'reset'] ); - if (intval($this->getRequest()->getParam('rid'))) { + if ((int)$this->getRequest()->getParam('rid')) { $this->getToolbar()->addChild( 'deleteButton', \Magento\Backend\Block\Widget\Button::class, @@ -85,6 +89,8 @@ protected function _prepareLayout() } /** + * Get back button html + * * @return string */ public function getBackButtonHtml() @@ -93,6 +99,8 @@ public function getBackButtonHtml() } /** + * Get reset button html + * * @return string */ public function getResetButtonHtml() @@ -101,6 +109,8 @@ public function getResetButtonHtml() } /** + * Get save button html + * * @return string */ public function getSaveButtonHtml() @@ -109,17 +119,21 @@ public function getSaveButtonHtml() } /** + * Get delete button html + * * @return string|void */ public function getDeleteButtonHtml() { - if (intval($this->getRequest()->getParam('rid')) == 0) { + if ((int)$this->getRequest()->getParam('rid') == 0) { return; } return $this->getChildHtml('deleteButton'); } /** + * Get user + * * @return mixed */ public function getUser() diff --git a/app/code/Magento/User/Controller/Adminhtml/Auth/Forgotpassword.php b/app/code/Magento/User/Controller/Adminhtml/Auth/Forgotpassword.php index c0e78fae2c2c..565b312325dd 100644 --- a/app/code/Magento/User/Controller/Adminhtml/Auth/Forgotpassword.php +++ b/app/code/Magento/User/Controller/Adminhtml/Auth/Forgotpassword.php @@ -6,8 +6,10 @@ */ namespace Magento\User\Controller\Adminhtml\Auth; -use Magento\Security\Model\SecurityManager; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\App\ObjectManager; +use Magento\Security\Model\SecurityManager; use Magento\Backend\App\Action\Context; use Magento\User\Model\UserFactory; use Magento\User\Model\ResourceModel\User\CollectionFactory; @@ -16,14 +18,25 @@ use Magento\Framework\Exception\SecurityViolationException; use Magento\User\Controller\Adminhtml\Auth; use Magento\Backend\Helper\Data; +use Magento\User\Model\Spi\NotificatorInterface; -class Forgotpassword extends Auth +/** + * Initiate forgot-password process. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class Forgotpassword extends Auth implements HttpGetActionInterface, HttpPostActionInterface { /** * @var SecurityManager */ protected $securityManager; + /** + * @var NotificatorInterface + */ + private $notificator; + /** * User model factory * @@ -41,13 +54,16 @@ class Forgotpassword extends Auth * @param UserFactory $userFactory * @param SecurityManager $securityManager * @param CollectionFactory $userCollectionFactory + * @param Data $backendDataHelper + * @param NotificatorInterface|null $notificator */ public function __construct( Context $context, UserFactory $userFactory, SecurityManager $securityManager, CollectionFactory $userCollectionFactory = null, - Data $backendDataHelper = null + Data $backendDataHelper = null, + ?NotificatorInterface $notificator = null ) { parent::__construct($context, $userFactory); $this->securityManager = $securityManager; @@ -55,6 +71,8 @@ public function __construct( ObjectManager::getInstance()->get(CollectionFactory::class); $this->backendDataHelper = $backendDataHelper ?: ObjectManager::getInstance()->get(Data::class); + $this->notificator = $notificator + ?? ObjectManager::getInstance()->get(NotificatorInterface::class); } /** @@ -96,7 +114,7 @@ public function execute() $newPassResetToken = $this->backendDataHelper->generateResetPasswordLinkToken(); $user->changeResetPasswordLinkToken($newPassResetToken); $user->save(); - $user->sendPasswordResetConfirmationEmail(); + $this->notificator->sendForgotPassword($user); } break; } diff --git a/app/code/Magento/User/Controller/Adminhtml/Locks/Index.php b/app/code/Magento/User/Controller/Adminhtml/Locks/Index.php index 7c0e31076cac..e3e1e3def398 100644 --- a/app/code/Magento/User/Controller/Adminhtml/Locks/Index.php +++ b/app/code/Magento/User/Controller/Adminhtml/Locks/Index.php @@ -6,10 +6,12 @@ */ namespace Magento\User\Controller\Adminhtml\Locks; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + /** * Locks Index action */ -class Index extends \Magento\User\Controller\Adminhtml\Locks +class Index extends \Magento\User\Controller\Adminhtml\Locks implements HttpGetActionInterface { /** * Render page with grid diff --git a/app/code/Magento/User/Controller/Adminhtml/User/Index.php b/app/code/Magento/User/Controller/Adminhtml/User/Index.php index 7f79dd477373..bc4157c144a8 100644 --- a/app/code/Magento/User/Controller/Adminhtml/User/Index.php +++ b/app/code/Magento/User/Controller/Adminhtml/User/Index.php @@ -6,7 +6,9 @@ */ namespace Magento\User\Controller\Adminhtml\User; -class Index extends \Magento\User\Controller\Adminhtml\User +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\User\Controller\Adminhtml\User implements HttpGetActionInterface { /** * @return void diff --git a/app/code/Magento/User/Controller/Adminhtml/User/Role/Index.php b/app/code/Magento/User/Controller/Adminhtml/User/Role/Index.php index e1853be17a12..8d468d45f1ae 100644 --- a/app/code/Magento/User/Controller/Adminhtml/User/Role/Index.php +++ b/app/code/Magento/User/Controller/Adminhtml/User/Role/Index.php @@ -6,7 +6,9 @@ */ namespace Magento\User\Controller\Adminhtml\User\Role; -class Index extends \Magento\User\Controller\Adminhtml\User\Role +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\User\Controller\Adminhtml\User\Role implements HttpGetActionInterface { /** * Show grid with roles existing in systems diff --git a/app/code/Magento/User/Controller/Adminhtml/User/Role/RoleGrid.php b/app/code/Magento/User/Controller/Adminhtml/User/Role/RoleGrid.php index 4d97b62cd8b1..ad8c87b617d5 100644 --- a/app/code/Magento/User/Controller/Adminhtml/User/Role/RoleGrid.php +++ b/app/code/Magento/User/Controller/Adminhtml/User/Role/RoleGrid.php @@ -6,7 +6,11 @@ */ namespace Magento\User\Controller\Adminhtml\User\Role; -class RoleGrid extends \Magento\User\Controller\Adminhtml\User\Role +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\User\Controller\Adminhtml\User\Role as RoleAction; + +class RoleGrid extends RoleAction implements HttpGetActionInterface, HttpPostActionInterface { /** * Action for ajax request from grid diff --git a/app/code/Magento/User/Controller/Adminhtml/User/Role/SaveRole.php b/app/code/Magento/User/Controller/Adminhtml/User/Role/SaveRole.php index 9dfe34e43538..97ecb778b8cb 100644 --- a/app/code/Magento/User/Controller/Adminhtml/User/Role/SaveRole.php +++ b/app/code/Magento/User/Controller/Adminhtml/User/Role/SaveRole.php @@ -7,16 +7,20 @@ namespace Magento\User\Controller\Adminhtml\User\Role; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Authorization\Model\Acl\Role\Group as RoleGroup; use Magento\Authorization\Model\UserContextInterface; use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\State\UserLockedException; use Magento\Security\Model\SecurityCookie; /** + * Save role controller + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class SaveRole extends \Magento\User\Controller\Adminhtml\User\Role +class SaveRole extends \Magento\User\Controller\Adminhtml\User\Role implements HttpPostActionInterface { /** * Session keys for Info form data @@ -58,9 +62,8 @@ private function getSecurityCookie() { if (!($this->securityCookie instanceof SecurityCookie)) { return \Magento\Framework\App\ObjectManager::getInstance()->get(SecurityCookie::class); - } else { - return $this->securityCookie; } + return $this->securityCookie; } /** @@ -75,10 +78,8 @@ public function execute() $rid = $this->getRequest()->getParam('role_id', false); $resource = $this->getRequest()->getParam('resource', false); - $roleUsers = $this->getRequest()->getParam('in_role_user', null); - parse_str($roleUsers, $roleUsers); - $roleUsers = array_keys($roleUsers); - + $oldRoleUsers = $this->parseRequestVariable('in_role_user_old'); + $roleUsers = $this->parseRequestVariable('in_role_user'); $isAll = $this->getRequest()->getParam('all'); if ($isAll) { $resource = [$this->_objectManager->get(\Magento\Framework\Acl\RootResource::class)->getId()]; @@ -104,13 +105,9 @@ public function execute() $role->save(); $this->_rulesFactory->create()->setRoleId($role->getId())->setResources($resource)->saveRel(); - - $this->processPreviousUsers($role); - - foreach ($roleUsers as $nRuid) { - $this->_addUserToRole($nRuid, $role->getId()); - } - $this->messageManager->addSuccess(__('You saved the role.')); + $this->processPreviousUsers($role, $oldRoleUsers); + $this->processCurrentUsers($role, $roleUsers); + $this->messageManager->addSuccessMessage(__('You saved the role.')); } catch (UserLockedException $e) { $this->_auth->logout(); $this->getSecurityCookie()->setLogoutReasonCookie( @@ -118,14 +115,14 @@ public function execute() ); return $resultRedirect->setPath('*'); } catch (\Magento\Framework\Exception\AuthenticationException $e) { - $this->messageManager->addError( + $this->messageManager->addErrorMessage( __('The password entered for the current user is invalid. Verify the password and try again.') ); return $this->saveDataToSessionAndRedirect($role, $this->getRequest()->getPostValue(), $resultRedirect); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { - $this->messageManager->addError(__('An error occurred while saving this role.')); + $this->messageManager->addErrorMessage(__('An error occurred while saving this role.')); } return $resultRedirect->setPath('*/*/'); @@ -150,16 +147,29 @@ protected function validateUser() } /** + * Parse request value from string + * + * @param string $paramName + * @return array + */ + private function parseRequestVariable($paramName): array + { + $value = $this->getRequest()->getParam($paramName, null); + parse_str($value, $value); + $value = array_keys($value); + return $value; + } + + /** + * Process previous users + * * @param \Magento\Authorization\Model\Role $role + * @param array $oldRoleUsers * @return $this * @throws \Exception */ - protected function processPreviousUsers(\Magento\Authorization\Model\Role $role) + protected function processPreviousUsers(\Magento\Authorization\Model\Role $role, array $oldRoleUsers): self { - $oldRoleUsers = $this->getRequest()->getParam('in_role_user_old'); - parse_str($oldRoleUsers, $oldRoleUsers); - $oldRoleUsers = array_keys($oldRoleUsers); - foreach ($oldRoleUsers as $oUid) { $this->_deleteUserFromRole($oUid, $role->getId()); } @@ -167,12 +177,33 @@ protected function processPreviousUsers(\Magento\Authorization\Model\Role $role) return $this; } + /** + * Processes users to be assigned to roles + * + * @param \Magento\Authorization\Model\Role $role + * @param array $roleUsers + * @return $this + */ + private function processCurrentUsers(\Magento\Authorization\Model\Role $role, array $roleUsers): self + { + foreach ($roleUsers as $nRuid) { + try { + $this->_addUserToRole($nRuid, $role->getId()); + } catch (LocalizedException $e) { + $this->messageManager->addErrorMessage($e->getMessage()); + } + } + + return $this; + } + /** * Assign user to role * * @param int $userId * @param int $roleId * @return bool + * @throws LocalizedException */ protected function _addUserToRole($userId, $roleId) { @@ -206,6 +237,8 @@ protected function _deleteUserFromRole($userId, $roleId) } /** + * Save data to session and redirect + * * @param \Magento\Authorization\Model\Role $role * @param array $data * @param \Magento\Backend\Model\View\Result\Redirect $resultRedirect diff --git a/app/code/Magento/User/Controller/Adminhtml/User/RoleGrid.php b/app/code/Magento/User/Controller/Adminhtml/User/RoleGrid.php index 2bfbadd3a0d5..e171f0a8c2bc 100644 --- a/app/code/Magento/User/Controller/Adminhtml/User/RoleGrid.php +++ b/app/code/Magento/User/Controller/Adminhtml/User/RoleGrid.php @@ -6,7 +6,11 @@ */ namespace Magento\User\Controller\Adminhtml\User; -class RoleGrid extends \Magento\User\Controller\Adminhtml\User +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\User\Controller\Adminhtml\User as UserAction; + +class RoleGrid extends UserAction implements HttpGetActionInterface, HttpPostActionInterface { /** * @return void diff --git a/app/code/Magento/User/Controller/Adminhtml/User/Save.php b/app/code/Magento/User/Controller/Adminhtml/User/Save.php index 4b984b761c1f..521c09f7b770 100644 --- a/app/code/Magento/User/Controller/Adminhtml/User/Save.php +++ b/app/code/Magento/User/Controller/Adminhtml/User/Save.php @@ -6,14 +6,18 @@ namespace Magento\User\Controller\Adminhtml\User; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\Exception\AuthenticationException; use Magento\Framework\Exception\State\UserLockedException; use Magento\Security\Model\SecurityCookie; +use Magento\User\Model\Spi\NotificationExceptionInterface; /** + * Save admin user. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Save extends \Magento\User\Controller\Adminhtml\User +class Save extends \Magento\User\Controller\Adminhtml\User implements HttpPostActionInterface { /** * @var SecurityCookie @@ -36,7 +40,7 @@ private function getSecurityCookie() } /** - * @return void + * @inheritDoc * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -44,10 +48,14 @@ public function execute() { $userId = (int)$this->getRequest()->getParam('user_id'); $data = $this->getRequest()->getPostValue(); + if (array_key_exists('form_key', $data)) { + unset($data['form_key']); + } if (!$data) { $this->_redirect('adminhtml/*/'); return; } + /** @var $model \Magento\User\Model\User */ $model = $this->_userFactory->create()->load($userId); if ($userId && $model->isObjectNew()) { @@ -87,17 +95,19 @@ public function execute() $currentUser->performIdentityCheck($data[$currentUserPasswordField]); $model->save(); - $model->sendNotificationEmailsIfRequired(); - $this->messageManager->addSuccess(__('You saved the user.')); $this->_getSession()->setUserData(false); $this->_redirect('adminhtml/*/'); + + $model->sendNotificationEmailsIfRequired(); } catch (UserLockedException $e) { $this->_auth->logout(); $this->getSecurityCookie()->setLogoutReasonCookie( \Magento\Security\Model\AdminSessionsManager::LOGOUT_REASON_USER_LOCKED ); $this->_redirect('adminhtml/*/'); + } catch (NotificationExceptionInterface $exception) { + $this->messageManager->addErrorMessage($exception->getMessage()); } catch (\Magento\Framework\Exception\AuthenticationException $e) { $this->messageManager->addError( __('The password entered for the current user is invalid. Verify the password and try again.') @@ -116,6 +126,8 @@ public function execute() } /** + * Redirect to Edit form. + * * @param \Magento\User\Model\User $model * @param array $data * @return void diff --git a/app/code/Magento/User/Model/Notificator.php b/app/code/Magento/User/Model/Notificator.php new file mode 100644 index 000000000000..3a5522db4c53 --- /dev/null +++ b/app/code/Magento/User/Model/Notificator.php @@ -0,0 +1,194 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\User\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\MailException; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\User\Api\Data\UserInterface; +use Magento\User\Model\Spi\NotificatorInterface; +use Magento\Backend\App\ConfigInterface; +use Magento\Framework\Mail\Template\TransportBuilder; +use Magento\Framework\App\DeploymentConfig; +use Magento\Backend\App\Area\FrontNameResolver; +use Magento\Email\Model\BackendTemplate; + +/** + * @inheritDoc + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class Notificator implements NotificatorInterface +{ + /** + * @var TransportBuilder + */ + private $transportBuilder; + + /** + * @var ConfigInterface + */ + private $config; + + /** + * @var DeploymentConfig + */ + private $deployConfig; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @param TransportBuilder $transportBuilder + * @param ConfigInterface $config + * @param DeploymentConfig $deployConfig + * @param StoreManagerInterface $storeManager + */ + public function __construct( + TransportBuilder $transportBuilder, + ConfigInterface $config, + DeploymentConfig $deployConfig, + StoreManagerInterface $storeManager + ) { + $this->transportBuilder = $transportBuilder; + $this->config = $config; + $this->deployConfig = $deployConfig; + $this->storeManager = $storeManager; + } + + /** + * Send a notification. + * + * @param string $templateConfigId + * @param array $templateVars + * @param string $toEmail + * @param string $toName + * @throws MailException + * + * @return void + */ + private function sendNotification( + string $templateConfigId, + array $templateVars, + string $toEmail, + string $toName + ): void { + $transport = $this->transportBuilder + ->setTemplateIdentifier($this->config->getValue($templateConfigId)) + ->setTemplateModel(BackendTemplate::class) + ->setTemplateOptions([ + 'area' => FrontNameResolver::AREA_CODE, + 'store' => Store::DEFAULT_STORE_ID + ]) + ->setTemplateVars($templateVars) + ->setFrom( + $this->config->getValue('admin/emails/forgot_email_identity') + ) + ->addTo($toEmail, $toName) + ->getTransport(); + $transport->sendMessage(); + } + + /** + * @inheritDoc + */ + public function sendForgotPassword(UserInterface $user): void + { + try { + $this->sendNotification( + 'admin/emails/forgot_email_template', + [ + 'user' => $user, + 'store' => $this->storeManager->getStore( + Store::DEFAULT_STORE_ID + ) + ], + $user->getEmail(), + $user->getFirstName().' '.$user->getLastName() + ); + } catch (LocalizedException $exception) { + throw new NotificatorException( + __($exception->getMessage()), + $exception + ); + } + } + + /** + * @inheritDoc + */ + public function sendCreated(UserInterface $user): void + { + $toEmails = []; + $generalEmail = $this->config->getValue( + 'trans_email/ident_general/email' + ); + if ($generalEmail) { + $toEmails[] = $generalEmail; + } + if ($adminEmail = $this->deployConfig->get('user_admin_email')) { + $toEmails[] = $adminEmail; + } + + try { + foreach ($toEmails as $toEmail) { + $this->sendNotification( + 'admin/emails/new_user_notification_template', + [ + 'user' => $user, + 'store' => $this->storeManager->getStore( + Store::DEFAULT_STORE_ID + ) + ], + $toEmail, + __('Administrator')->getText() + ); + } + } catch (LocalizedException $exception) { + throw new NotificatorException( + __($exception->getMessage()), + $exception + ); + } + } + + /** + * @inheritDoc + */ + public function sendUpdated(UserInterface $user, array $changed): void + { + $email = $user->getEmail(); + if ($user instanceof User) { + $email = $user->getOrigData('email'); + } + + try { + $this->sendNotification( + 'admin/emails/user_notification_template', + [ + 'user' => $user, + 'store' => $this->storeManager->getStore( + Store::DEFAULT_STORE_ID + ), + 'changes' => implode(', ', $changed) + ], + $email, + $user->getFirstName().' '.$user->getLastName() + ); + } catch (LocalizedException $exception) { + throw new NotificatorException( + __($exception->getMessage()), + $exception + ); + } + } +} diff --git a/app/code/Magento/User/Model/NotificatorException.php b/app/code/Magento/User/Model/NotificatorException.php new file mode 100644 index 000000000000..5b581879814a --- /dev/null +++ b/app/code/Magento/User/Model/NotificatorException.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\User\Model; + +use Magento\Framework\Exception\MailException; +use Magento\User\Model\Spi\NotificationExceptionInterface; + +/** + * When notificator cannot send an email. + */ +class NotificatorException extends MailException implements NotificationExceptionInterface +{ + +} diff --git a/app/code/Magento/User/Model/Spi/NotificationExceptionInterface.php b/app/code/Magento/User/Model/Spi/NotificationExceptionInterface.php new file mode 100644 index 000000000000..47af7cbe6dd5 --- /dev/null +++ b/app/code/Magento/User/Model/Spi/NotificationExceptionInterface.php @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\User\Model\Spi; + +/** + * When a notification cannot be sent. + */ +interface NotificationExceptionInterface extends \Throwable +{ + +} diff --git a/app/code/Magento/User/Model/Spi/NotificatorInterface.php b/app/code/Magento/User/Model/Spi/NotificatorInterface.php new file mode 100644 index 000000000000..f5e8ee68e7ee --- /dev/null +++ b/app/code/Magento/User/Model/Spi/NotificatorInterface.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\User\Model\Spi; + +use Magento\User\Api\Data\UserInterface; + +/** + * Use to send out notifications about user related events. + */ +interface NotificatorInterface +{ + /** + * Send notification when a user requests password reset. + * + * @param UserInterface $user User that requested password reset. + * @throws NotificationExceptionInterface + * + * @return void + */ + public function sendForgotPassword(UserInterface $user): void; + + /** + * Send a notification when a new user is created. + * + * @param UserInterface $user The new user. + * @throws NotificationExceptionInterface + * + * @return void + */ + public function sendCreated(UserInterface $user): void; + + /** + * Send a notification when a user is updated. + * + * @param UserInterface $user The user updated. + * @param string[] $changed List of changed properties. + * @throws NotificationExceptionInterface + * + * @return void + */ + public function sendUpdated(UserInterface $user, array $changed): void; +} diff --git a/app/code/Magento/User/Model/User.php b/app/code/Magento/User/Model/User.php index 29618c981a2b..d8040b0bbaaa 100644 --- a/app/code/Magento/User/Model/User.php +++ b/app/code/Magento/User/Model/User.php @@ -6,14 +6,15 @@ namespace Magento\User\Model; -use Magento\Backend\App\Area\FrontNameResolver; use Magento\Backend\Model\Auth\Credential\StorageInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\Model\AbstractModel; use Magento\Framework\Exception\AuthenticationException; use Magento\Framework\Serialize\Serializer\Json; -use Magento\Store\Model\Store; use Magento\User\Api\Data\UserInterface; +use Magento\User\Model\Spi\NotificationExceptionInterface; +use Magento\User\Model\Spi\NotificatorInterface; +use Magento\Framework\App\DeploymentConfig; /** * Admin user model @@ -37,12 +38,21 @@ class User extends AbstractModel implements StorageInterface, UserInterface { /** - * Configuration paths for email templates and identities + * @deprecated + * @see \Magento\User\Model\Spi\NotificatorInterface */ const XML_PATH_FORGOT_EMAIL_TEMPLATE = 'admin/emails/forgot_email_template'; + /** + * @deprecated + * @see \Magento\User\Model\Spi\NotificatorInterface + */ const XML_PATH_FORGOT_EMAIL_IDENTITY = 'admin/emails/forgot_email_identity'; + /** + * @deprecated + * @see \Magento\User\Model\Spi\NotificatorInterface + */ const XML_PATH_USER_NOTIFICATION_TEMPLATE = 'admin/emails/user_notification_template'; /** @deprecated */ @@ -105,12 +115,12 @@ class User extends AbstractModel implements StorageInterface, UserInterface protected $_encryptor; /** - * @var \Magento\Framework\Mail\Template\TransportBuilder + * @deprecated */ protected $_transportBuilder; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @deprecated */ protected $_storeManager; @@ -124,6 +134,16 @@ class User extends AbstractModel implements StorageInterface, UserInterface */ private $serializer; + /** + * @var NotificatorInterface + */ + private $notificator; + + /** + * @deprecated + */ + private $deploymentConfig; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -139,6 +159,8 @@ class User extends AbstractModel implements StorageInterface, UserInterface * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data * @param Json $serializer + * @param DeploymentConfig|null $deploymentConfig + * @param NotificatorInterface|null $notificator * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -155,7 +177,9 @@ public function __construct( \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], - Json $serializer = null + Json $serializer = null, + DeploymentConfig $deploymentConfig = null, + ?NotificatorInterface $notificator = null ) { $this->_encryptor = $encryptor; parent::__construct($context, $registry, $resource, $resourceCollection, $data); @@ -166,7 +190,12 @@ public function __construct( $this->_transportBuilder = $transportBuilder; $this->_storeManager = $storeManager; $this->validationRules = $validationRules; - $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class); + $this->serializer = $serializer + ?: ObjectManager::getInstance()->get(Json::class); + $this->deploymentConfig = $deploymentConfig + ?: ObjectManager::getInstance()->get(DeploymentConfig::class); + $this->notificator = $notificator + ?: ObjectManager::getInstance()->get(NotificatorInterface::class); } /** @@ -180,10 +209,17 @@ protected function _construct() } /** + * Removing dependencies and leaving only entity's properties. + * * @return string[] + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + $properties = parent::__sleep(); return array_diff( $properties, @@ -196,16 +232,27 @@ public function __sleep() '_encryptor', '_transportBuilder', '_storeManager', - '_validatorBeforeSave' + '_validatorBeforeSave', + 'validationRules', + 'serializer', + 'deploymentConfig', + 'notificator' ] ); } /** + * Restoring required objects after serialization. + * * @return void + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + parent::__wakeup(); $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); $this->serializer = $objectManager->get(Json::class); @@ -218,6 +265,9 @@ public function __wakeup() $this->_encryptor = $objectManager->get(\Magento\Framework\Encryption\EncryptorInterface::class); $this->_transportBuilder = $objectManager->get(\Magento\Framework\Mail\Template\TransportBuilder::class); $this->_storeManager = $objectManager->get(\Magento\Store\Model\StoreManagerInterface::class); + $this->validationRules = $objectManager->get(UserValidationRules::class); + $this->deploymentConfig = $objectManager->get(DeploymentConfig::class); + $this->notificator = $objectManager->get(NotificatorInterface::class); } /** @@ -236,7 +286,7 @@ public function beforeSave() } if ($this->getIsActive() !== null) { - $data['is_active'] = intval($this->getIsActive()); + $data['is_active'] = (int)$this->getIsActive(); } $this->addData($data); @@ -366,6 +416,10 @@ public function getRoles() */ public function getRole() { + if ($this->getData('extracted_role')) { + $this->_role = $this->getData('extracted_role'); + $this->unsetData('extracted_role'); + } if (null === $this->_role) { $this->_role = $this->_roleFactory->create(); $roles = $this->getRoles(); @@ -388,7 +442,7 @@ public function deleteFromRole() } /** - * Check if such combination role/user exists + * Check if such combination role/user exists. * * @return bool */ @@ -399,28 +453,24 @@ public function roleUserExists() } /** - * Send email with reset password confirmation link + * Send email with reset password confirmation link. + * + * @deprecated + * @see NotificatorInterface::sendForgotPassword() * * @return $this */ public function sendPasswordResetConfirmationEmail() { - $templateId = $this->_config->getValue(self::XML_PATH_FORGOT_EMAIL_TEMPLATE); - $transport = $this->_transportBuilder->setTemplateIdentifier($templateId) - ->setTemplateModel(\Magento\Email\Model\BackendTemplate::class) - ->setTemplateOptions(['area' => FrontNameResolver::AREA_CODE, 'store' => Store::DEFAULT_STORE_ID]) - ->setTemplateVars(['user' => $this, 'store' => $this->_storeManager->getStore(Store::DEFAULT_STORE_ID)]) - ->setFrom($this->_config->getValue(self::XML_PATH_FORGOT_EMAIL_IDENTITY)) - ->addTo($this->getEmail(), $this->getName()) - ->getTransport(); + $this->notificator->sendForgotPassword($this); - $transport->sendMessage(); return $this; } /** * Send email to when password is resetting * + * @throws NotificationExceptionInterface * @return $this * @deprecated 100.1.0 */ @@ -431,20 +481,20 @@ public function sendPasswordResetNotificationEmail() } /** - * Check changes and send notification emails + * Check changes and send notification emails. * + * @throws NotificationExceptionInterface * @return $this * @since 100.1.0 */ public function sendNotificationEmailsIfRequired() { - $changes = $this->createChangesDescriptionString(); - - if ($changes) { - if ($this->getEmail() != $this->getOrigData('email') && $this->getOrigData('email')) { - $this->sendUserNotificationEmail($changes, $this->getOrigData('email')); - } - $this->sendUserNotificationEmail($changes); + if ($this->isObjectNew()) { + //Notification about a new user. + $this->notificator->sendCreated($this); + } elseif ($changes = $this->createChangesDescriptionString()) { + //User changed. + $this->notificator->sendUpdated($this, explode(', ', $changes)); } return $this; @@ -478,35 +528,21 @@ protected function createChangesDescriptionString() } /** - * Send user notification email + * Send user notification email. * * @param string $changes * @param string $email + * @throws NotificationExceptionInterface * @return $this * @since 100.1.0 + * @deprecated + * @see NotificatorInterface::sendUpdated() + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ protected function sendUserNotificationEmail($changes, $email = null) { - if ($email === null) { - $email = $this->getEmail(); - } + $this->notificator->sendUpdated($this, explode(', ', $changes)); - $transport = $this->_transportBuilder - ->setTemplateIdentifier($this->_config->getValue(self::XML_PATH_USER_NOTIFICATION_TEMPLATE)) - ->setTemplateModel(\Magento\Email\Model\BackendTemplate::class) - ->setTemplateOptions(['area' => FrontNameResolver::AREA_CODE, 'store' => Store::DEFAULT_STORE_ID]) - ->setTemplateVars( - [ - 'user' => $this, - 'store' => $this->_storeManager->getStore(Store::DEFAULT_STORE_ID), - 'changes' => $changes - ] - ) - ->setFrom($this->_config->getValue(self::XML_PATH_FORGOT_EMAIL_IDENTITY)) - ->addTo($email, $this->getName()) - ->getTransport(); - - $transport->sendMessage(); return $this; } @@ -738,7 +774,7 @@ public function setHasAvailableResources($hasResources) } /** - * {@inheritdoc} + * @inheritDoc */ public function getFirstName() { @@ -746,7 +782,7 @@ public function getFirstName() } /** - * {@inheritdoc} + * @inheritDoc */ public function setFirstName($firstName) { @@ -754,7 +790,7 @@ public function setFirstName($firstName) } /** - * {@inheritdoc} + * @inheritDoc */ public function getLastName() { @@ -762,7 +798,7 @@ public function getLastName() } /** - * {@inheritdoc} + * @inheritDoc */ public function setLastName($lastName) { @@ -770,7 +806,7 @@ public function setLastName($lastName) } /** - * {@inheritdoc} + * @inheritDoc */ public function getEmail() { @@ -778,7 +814,7 @@ public function getEmail() } /** - * {@inheritdoc} + * @inheritDoc */ public function setEmail($email) { @@ -786,7 +822,7 @@ public function setEmail($email) } /** - * {@inheritdoc} + * @inheritDoc */ public function getUserName() { @@ -794,7 +830,7 @@ public function getUserName() } /** - * {@inheritdoc} + * @inheritDoc */ public function setUserName($userName) { @@ -802,7 +838,7 @@ public function setUserName($userName) } /** - * {@inheritdoc} + * @inheritDoc */ public function getPassword() { @@ -810,7 +846,7 @@ public function getPassword() } /** - * {@inheritdoc} + * @inheritDoc */ public function setPassword($password) { @@ -818,7 +854,7 @@ public function setPassword($password) } /** - * {@inheritdoc} + * @inheritDoc */ public function getCreated() { @@ -826,7 +862,7 @@ public function getCreated() } /** - * {@inheritdoc} + * @inheritDoc */ public function setCreated($created) { @@ -834,7 +870,7 @@ public function setCreated($created) } /** - * {@inheritdoc} + * @inheritDoc */ public function getModified() { @@ -842,7 +878,7 @@ public function getModified() } /** - * {@inheritdoc} + * @inheritDoc */ public function setModified($modified) { @@ -850,7 +886,7 @@ public function setModified($modified) } /** - * {@inheritdoc} + * @inheritDoc */ public function getIsActive() { @@ -858,7 +894,7 @@ public function getIsActive() } /** - * {@inheritdoc} + * @inheritDoc */ public function setIsActive($isActive) { @@ -866,7 +902,7 @@ public function setIsActive($isActive) } /** - * {@inheritdoc} + * @inheritDoc */ public function getInterfaceLocale() { @@ -874,7 +910,7 @@ public function getInterfaceLocale() } /** - * {@inheritdoc} + * @inheritDoc */ public function setInterfaceLocale($interfaceLocale) { diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminClickSaveButtonOnUserFormActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminClickSaveButtonOnUserFormActionGroup.xml new file mode 100644 index 000000000000..e1edb16aba6e --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminClickSaveButtonOnUserFormActionGroup.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminClickSaveButtonOnUserFormActionGroup"> + <click selector="{{AdminNewUserFormSection.save}}" stepKey="saveNewUser"/> + <waitForPageLoad stepKey="waitForSaveResultLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateRoleActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateRoleActionGroup.xml new file mode 100644 index 000000000000..da08ac469b7c --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateRoleActionGroup.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCreateRoleActionGroup"> + <arguments> + <argument name="restrictedRole"/> + <argument name="User"/> + </arguments> + <amOnPage url="{{AdminEditRolePage.url}}" stepKey="navigateToNewRole"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <fillField selector="{{AdminEditRoleInfoSection.roleName}}" userInput="{{User.name}}" stepKey="fillRoleName" /> + <fillField selector="{{AdminEditRoleInfoSection.password}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}" stepKey="enterPassword" /> + <click selector="{{AdminEditRoleInfoSection.roleResourcesTab}}" stepKey="clickRoleResourcesTab" /> + <waitForElementVisible selector="{{AdminEditRoleResourcesSection.roleScopes}}" stepKey="waitForScopeSelection" /> + <selectOption selector="{{AdminEditRoleResourcesSection.resourceAccess}}" userInput="0" stepKey="selectResourceAccessCustom"/> + <waitForElementVisible stepKey="waitForElementVisible" selector="{{AdminEditRoleInfoSection.blockName('restrictedRole')}}" time="30"/> + <click stepKey="clickContentBlockCheckbox" selector="{{AdminEditRoleInfoSection.blockName('restrictedRole')}}"/> + <click selector="{{AdminEditRoleInfoSection.saveButton}}" stepKey="clickSaveRoleButton" /> + <waitForPageLoad stepKey="waitForPageLoad2" /> + </actionGroup> + <!--Create new role--> + <actionGroup name="AdminCreateRole"> + <arguments> + <argument name="role" type="string" defaultValue=""/> + <argument name="resource" type="string" defaultValue="All"/> + <argument name="scope" type="string" defaultValue="Custom"/> + <argument name="websites" type="string" defaultValue="Main Website"/> + </arguments> + <click selector="{{AdminCreateRoleSection.create}}" stepKey="clickToAddNewRole"/> + <fillField selector="{{AdminCreateRoleSection.name}}" userInput="{{role.name}}" stepKey="setRoleName"/> + <fillField stepKey="setPassword" selector="{{AdminCreateRoleSection.password}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}"/> + <click selector="{{AdminCreateRoleSection.roleResources}}" stepKey="clickToOpenRoleResources"/> + <waitForPageLoad stepKey="waitForRoleResourcePage" time="5"/> + <click stepKey="checkSales" selector="//a[text()='Sales']"/> + <click selector="{{AdminCreateRoleSection.save}}" stepKey="clickToSaveRole"/> + <waitForPageLoad stepKey="waitForPageLoad" time="10"/> + <see userInput="You saved the role." stepKey="seeSuccessMessage" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateUserActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateUserActionGroup.xml index de887d2de670..5d51dcc610f7 100644 --- a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateUserActionGroup.xml +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateUserActionGroup.xml @@ -10,25 +10,49 @@ <actionGroup name="AdminCreateUserActionGroup"> <arguments> <argument name="role"/> - <argument name="User" defaultValue="admin2"/> + <argument name="User" defaultValue="newAdmin"/> </arguments> - <amOnPage url="{{AdminEditUserPage.url}}" stepKey="navigateToNewUser"/> - <waitForPageLoad stepKey="waitForPageLoad1" /> - <fillField selector="{{AdminEditUserSection.usernameTextField}}" userInput="{{admin2.username}}" stepKey="enterUserName" /> - <fillField selector="{{AdminEditUserSection.firstNameTextField}}" userInput="{{admin2.firstName}}" stepKey="enterFirstName" /> - <fillField selector="{{AdminEditUserSection.lastNameTextField}}" userInput="{{admin2.lastName}}" stepKey="enterLastName" /> - <fillField selector="{{AdminEditUserSection.emailTextField}}" userInput="{{admin2.username}}@magento.com" stepKey="enterEmail" /> - <fillField selector="{{AdminEditUserSection.passwordTextField}}" userInput="{{admin2.password}}" stepKey="enterPassword" /> - <fillField selector="{{AdminEditUserSection.pwConfirmationTextField}}" userInput="{{admin2.password}}" stepKey="confirmPassword" /> + <amOnPage url="{{AdminUsersPage.url}}" stepKey="amOnAdminUsersPage"/> + <waitForPageLoad stepKey="waitForAdminUserPageLoad"/> + <click selector="{{AdminCreateUserSection.create}}" stepKey="clickToCreateNewUser"/> + <fillField selector="{{AdminEditUserSection.usernameTextField}}" userInput="{{newAdmin.username}}" stepKey="enterUserName" /> + <fillField selector="{{AdminEditUserSection.firstNameTextField}}" userInput="{{newAdmin.firstName}}" stepKey="enterFirstName" /> + <fillField selector="{{AdminEditUserSection.lastNameTextField}}" userInput="{{newAdmin.lastName}}" stepKey="enterLastName" /> + <fillField selector="{{AdminEditUserSection.emailTextField}}" userInput="{{newAdmin.username}}@magento.com" stepKey="enterEmail" /> + <fillField selector="{{AdminEditUserSection.passwordTextField}}" userInput="{{newAdmin.password}}" stepKey="enterPassword" /> + <fillField selector="{{AdminEditUserSection.pwConfirmationTextField}}" userInput="{{newAdmin.password}}" stepKey="confirmPassword" /> <fillField selector="{{AdminEditUserSection.currentPasswordField}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}" stepKey="enterCurrentPassword" /> <scrollToTopOfPage stepKey="scrollToTopOfPage" /> <click selector="{{AdminEditUserSection.userRoleTab}}" stepKey="clickUserRole" /> - <fillField selector="{{AdminEditUserRoleSection.roleNameFilterTextField}}" userInput="{{role.name}}" stepKey="filterRole" /> - <click selector="{{AdminEditUserRoleSection.searchButton}}" stepKey="clickSearch" /> + <fillField selector="{{AdminEditUserSection.roleNameFilterTextField}}" userInput="{{role.name}}" stepKey="filterRole" /> + <click selector="{{AdminEditUserSection.searchButton}}" stepKey="clickSearch" /> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear1"/> - <click selector="{{AdminEditUserRoleSection.searchResultFirstRow}}" stepKey="selectRole" /> + <click selector="{{AdminEditUserSection.searchResultFirstRow}}" stepKey="selectRole" /> <click selector="{{AdminEditUserSection.saveButton}}" stepKey="clickSaveUser" /> <waitForPageLoad stepKey="waitForPageLoad2" /> <see userInput="You saved the user." stepKey="seeSuccessMessage" /> </actionGroup> + + <!--Create new user with role--> + <actionGroup name="AdminCreateUserWithRoleActionGroup"> + <arguments> + <argument name="role"/> + <argument name="user" defaultValue="newAdmin"/> + </arguments> + <amOnPage url="{{AdminNewUserPage.url}}" stepKey="navigateToNewUser"/> + <waitForPageLoad stepKey="waitForUsersPage" /> + <fillField selector="{{AdminCreateUserSection.usernameTextField}}" userInput="{{user.username}}" stepKey="enterUserName" /> + <fillField selector="{{AdminCreateUserSection.firstNameTextField}}" userInput="{{user.firstName}}" stepKey="enterFirstName" /> + <fillField selector="{{AdminCreateUserSection.lastNameTextField}}" userInput="{{user.lastName}}" stepKey="enterLastName" /> + <fillField selector="{{AdminCreateUserSection.emailTextField}}" userInput="{{user.username}}@magento.com" stepKey="enterEmail" /> + <fillField selector="{{AdminCreateUserSection.passwordTextField}}" userInput="{{user.password}}" stepKey="enterPassword" /> + <fillField selector="{{AdminCreateUserSection.pwConfirmationTextField}}" userInput="{{user.password}}" stepKey="confirmPassword" /> + <fillField selector="{{AdminCreateUserSection.currentPasswordField}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}" stepKey="enterCurrentPassword" /> + <scrollToTopOfPage stepKey="scrollToTopOfPage" /> + <click stepKey="clickUserRole" selector="{{AdminCreateUserSection.userRoleTab}}"/> + <click stepKey="chooseRole" selector="{{AdminStoreSection.createdRoleInUserPage(role.name)}}"/> + <click selector="{{AdminCreateUserSection.saveButton}}" stepKey="clickSaveUser" /> + <waitForPageLoad stepKey="waitForSaveTheUser" /> + <see userInput="You saved the user." stepKey="seeSuccessMessage" /> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteCreatedRoleActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteCreatedRoleActionGroup.xml new file mode 100644 index 000000000000..813e22df227c --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteCreatedRoleActionGroup.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminDeleteCreatedRoleActionGroup"> + <arguments> + <argument name="role" defaultValue=""/> + </arguments> + <amOnPage url="{{AdminRolesPage.url}}" stepKey="amOnAdminUsersPage"/> + <waitForPageLoad stepKey="waitForUserRolePageLoad"/> + <click stepKey="clickToAddNewRole" selector="{{AdminDeleteRoleSection.role(role.name)}}"/> + <fillField stepKey="TypeCurrentPassword" selector="{{AdminDeleteRoleSection.current_pass}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}"/> + <click stepKey="clickToDeleteRole" selector="{{AdminDeleteRoleSection.delete}}"/> + <waitForElementVisible stepKey="wait" selector="{{AdminDeleteRoleSection.confirm}}" time="30"/> + <click stepKey="clickToConfirm" selector="{{AdminDeleteRoleSection.confirm}}"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see stepKey="seeSuccessMessage" userInput="You deleted the role."/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteCreatedUserActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteCreatedUserActionGroup.xml new file mode 100644 index 000000000000..74124f366a54 --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteCreatedUserActionGroup.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="AdminDeleteCreatedUserActionGroup"> + <arguments> + <argument name="user"/> + </arguments> + <amOnPage stepKey="amOnAdminUsersPage" url="{{AdminUsersPage.url}}"/> + <click stepKey="openTheUser" selector="{{AdminDeleteUserSection.role(user.username)}}"/> + <waitForPageLoad stepKey="waitForSingleUserPageToLoad" /> + <fillField stepKey="TypeCurrentPassword" selector="{{AdminDeleteUserSection.password}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}"/> + <scrollToTopOfPage stepKey="scrollToTop"/> + <click stepKey="clickToDeleteUser" selector="{{AdminDeleteUserSection.delete}}"/> + <waitForPageLoad stepKey="waitForConfirmationPopup"/> + <click stepKey="clickToConfirm" selector="{{AdminDeleteUserSection.confirm}}"/> + <see stepKey="seeDeleteMessageForUser" userInput="You deleted the user."/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteUserActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteUserActionGroup.xml new file mode 100644 index 000000000000..9b7342e531b6 --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteUserActionGroup.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See 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="AdminDeleteUserActionGroup"> + <arguments> + <argument name="user"/> + </arguments> + <amOnPage stepKey="amOnAdminUsersPage" url="{{AdminUsersPage.url}}"/> + <waitForPageLoad stepKey="waitForAdminUserPageLoad"/> + <click stepKey="openTheUser" selector="{{AdminDeleteUserSection.role(user.name)}}"/> + <fillField stepKey="TypeCurrentPassword" selector="{{AdminDeleteUserSection.password}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}"/> + <scrollToTopOfPage stepKey="scrollToTop"/> + <click stepKey="clickToDeleteRole" selector="{{AdminDeleteUserSection.delete}}"/> + <waitForElementVisible stepKey="wait" selector="{{AdminDeleteRoleSection.confirm}}" time="30"/> + <click stepKey="clickToConfirm" selector="{{AdminDeleteUserSection.confirm}}"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see stepKey="seeDeleteMessageForUser" userInput="You deleted the user."/> + </actionGroup> + <actionGroup name="AdminDeleteCustomUserActionGroup"> + <arguments> + <argument name="user"/> + </arguments> + <amOnPage url="{{AdminUsersPage.url}}" stepKey="navigateToUserGrid" /> + <fillField selector="{{AdminUserGridSection.usernameFilterTextField}}" userInput="{{user.username}}" stepKey="enterUserName" /> + <click selector="{{AdminUserGridSection.searchButton}}" stepKey="clickSearch" /> + <waitForPageLoad stepKey="waitForGridToLoad"/> + <see selector="{{AdminUserGridSection.usernameInFirstRow}}" userInput="{{user.username}}" stepKey="seeUser" /> + <click selector="{{AdminUserGridSection.searchResultFirstRow}}" stepKey="openUserEdit"/> + <waitForPageLoad stepKey="waitForUserEditPageLoad"/> + <fillField selector="{{AdminEditUserSection.currentPasswordField}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}" stepKey="enterThePassword" /> + <click selector="{{AdminMainActionsSection.delete}}" stepKey="deleteUser"/> + <waitForElementVisible selector="{{AdminConfirmationModalSection.message}}" stepKey="waitForConfirmModal"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmDelete"/> + <waitForPageLoad stepKey="waitForSave" /> + <see selector="{{AdminMessagesSection.success}}" userInput="You deleted the user." stepKey="seeUserDeleteMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminFillForgotPasswordFormActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminFillForgotPasswordFormActionGroup.xml new file mode 100644 index 000000000000..01be51e72ec6 --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminFillForgotPasswordFormActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminFillForgotPasswordFormActionGroup"> + <arguments> + <argument name="email" type="string"/> + </arguments> + + <fillField selector="{{AdminForgotPasswordFormSection.email}}" userInput="{{email}}" stepKey="fillAdminEmail"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminFillNewUserFormRequiredFieldsActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminFillNewUserFormRequiredFieldsActionGroup.xml new file mode 100644 index 000000000000..87bf1e003931 --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminFillNewUserFormRequiredFieldsActionGroup.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminFillNewUserFormRequiredFieldsActionGroup"> + <arguments> + <argument name="user" type="entity" /> + </arguments> + <fillField selector="{{AdminNewUserFormSection.username}}" userInput="{{user.username}}" stepKey="fillUser"/> + <fillField selector="{{AdminNewUserFormSection.firstname}}" userInput="{{user.firstname}}" stepKey="fillFirstName"/> + <fillField selector="{{AdminNewUserFormSection.lastname}}" userInput="{{user.lastname}}" stepKey="fillLastName"/> + <fillField selector="{{AdminNewUserFormSection.email}}" userInput="{{user.email}}" stepKey="fillEmail"/> + <fillField selector="{{AdminNewUserFormSection.password}}" userInput="{{user.password}}" stepKey="fillPassword"/> + <fillField selector="{{AdminNewUserFormSection.passwordConfirmation}}" userInput="{{user.password_confirmation}}" stepKey="fillPasswordConfirmation"/> + <fillField selector="{{AdminNewUserFormSection.currentPassword}}" userInput="{{user.current_password}}" stepKey="fillCurrentUserPassword"/> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <click selector="{{AdminNewUserFormSection.userRoleTab}}" stepKey="openUserRoleTab"/> + <waitForPageLoad stepKey="waitForUserRoleTabOpened" /> + <click selector="{{AdminNewUserFormSection.resetFilter}}" stepKey="resetGridFilter" /> + <waitForPageLoad stepKey="waitForFiltersReset" /> + <fillField userInput="{{user.role}}" selector="{{AdminNewUserFormSection.roleFilterField}}" stepKey="fillRoleFilterField" /> + <click selector="{{AdminNewUserFormSection.search}}" stepKey="clickSearchButton" /> + <waitForPageLoad stepKey="waitForFiltersApplied" /> + <checkOption selector="{{AdminNewUserFormSection.roleRadiobutton(user.role)}}" stepKey="assignRole"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminOpenForgotPasswordPageActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminOpenForgotPasswordPageActionGroup.xml new file mode 100644 index 000000000000..fa17c5a7f8b7 --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminOpenForgotPasswordPageActionGroup.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenForgotPasswordPageActionGroup"> + <amOnPage url="{{AdminLoginPage.url}}" stepKey="amOnAdminLoginPage"/> + <waitForPageLoad stepKey="waitForAdminLoginPage"/> + <click stepKey="clickForgotPasswordLink" selector="{{AdminLoginFormSection.forgotPasswordLink}}"/> + <waitForPageLoad stepKey="waitForAdminForgotPasswordPage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminOpenNewUserPageActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminOpenNewUserPageActionGroup.xml new file mode 100644 index 000000000000..67aef9379faa --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminOpenNewUserPageActionGroup.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenNewUserPageActionGroup"> + <amOnPage url="{{AdminNewUserPage.url}}" stepKey="amOnNewAdminUserPage"/> + <waitForPageLoad stepKey="waitForNewAdminUserPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminSubmitForgotPasswordFormActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminSubmitForgotPasswordFormActionGroup.xml new file mode 100644 index 000000000000..198bc713093e --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminSubmitForgotPasswordFormActionGroup.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSubmitForgotPasswordFormActionGroup"> + <click selector="{{AdminForgotPasswordFormSection.retrievePasswordButton}}" stepKey="clickOnRetrievePasswordButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AssertAdminUserSaveMessageActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AssertAdminUserSaveMessageActionGroup.xml new file mode 100644 index 000000000000..db4f0a89348a --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AssertAdminUserSaveMessageActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertAdminUserSaveMessageActionGroup"> + <arguments> + <argument name="message" type="string" defaultValue="You saved the user." /> + <argument name="messageType" type="string" defaultValue="success" /> + </arguments> + <waitForElementVisible selector="{{AdminUserFormMessagesSection.messageByType(messageType)}}" stepKey="waitForMessage" /> + <see userInput="{{message}}" selector="{{AdminUserFormMessagesSection.messageByType(messageType)}}" stepKey="verifyMessage" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/User/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 000000000000..e8b7d2aa8e04 --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminMenuSystemPermissionsAllUsers"> + <data key="pageTitle">Users</data> + <data key="title">All Users</data> + <data key="dataUiId">magento-user-system-acl-users</data> + </entity> + <entity name="AdminMenuSystemPermissionsLockedUsers"> + <data key="pageTitle">Locked Users</data> + <data key="title">Locked Users</data> + <data key="dataUiId">magento-user-system-acl-locks</data> + </entity> + <entity name="AdminMenuSystemOtherSettingsManageEncryptionKey"> + <data key="pageTitle">Encryption Key</data> + <data key="title">Manage Encryption Key</data> + <data key="dataUiId">magento-encryptionkey-system-crypt-key</data> + </entity> + <entity name="AdminMenuSystemPermissionsUserRoles"> + <data key="pageTitle">Roles</data> + <data key="title">User Roles</data> + <data key="dataUiId">magento-user-system-acl-roles</data> + </entity> +</entities> diff --git a/app/code/Magento/User/Test/Mftf/Data/UserData.xml b/app/code/Magento/User/Test/Mftf/Data/UserData.xml index 03ae3dba2184..e665736ae28f 100644 --- a/app/code/Magento/User/Test/Mftf/Data/UserData.xml +++ b/app/code/Magento/User/Test/Mftf/Data/UserData.xml @@ -8,6 +8,48 @@ <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="DefaultAdminUser" type="user"> + <data key="username">{{_ENV.MAGENTO_ADMIN_USERNAME}}</data> + <data key="password">{{_ENV.MAGENTO_ADMIN_PASSWORD}}</data> + </entity> + <entity name="AdminUserWrongCredentials"> + <data key="username" unique="suffix">username_</data> + <data key="password" unique="suffix">password_</data> + </entity> + <entity name="NewAdminUser" type="user"> + <data key="username" unique="suffix">admin</data> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="email" unique="prefix">admin@example.com</data> + <data key="password">123123q</data> + <data key="password_confirmation">123123q</data> + <data key="interface_local">en_US</data> + <data key="interface_local_label">English (United States)</data> + <data key="is_active">true</data> + <data key="is_active_label">Active</data> + <data key="current_password">{{_ENV.MAGENTO_ADMIN_PASSWORD}}</data> + <data key="role">Administrators</data> + <array key="roles"> + <item>1</item> + </array> + </entity> + <entity name="NewAdminUserWrongCurrentPassword" type="user"> + <data key="username" unique="suffix">admin</data> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="email" unique="prefix">admin@example.com</data> + <data key="password">123123q</data> + <data key="password_confirmation">123123q</data> + <data key="interface_local">en_US</data> + <data key="interface_local_label">English (United States)</data> + <data key="is_active">true</data> + <data key="is_active_label">Active</data> + <data key="current_password" unique="suffix">password_</data> + <data key="role">Administrators</data> + <array key="roles"> + <item>1</item> + </array> + </entity> <entity name="admin" type="user"> <data key="email">admin@magento.com</data> <data key="password">admin123</data> @@ -18,4 +60,18 @@ <data key="lastName">Smith</data> <data key="password">admin123</data> </entity> + <entity name="Admin3" type="user"> + <data key="username" unique="suffix">admin3</data> + <data key="firstname">admin3</data> + <data key="lastname">admin3</data> + <data key="email" unique="prefix">admin3WebUser@example.com</data> + <data key="password">123123q</data> + <data key="password_confirmation">123123q</data> + <data key="interface_local">en_US</data> + <data key="is_active">true</data> + <data key="current_password">123123q</data> + <array key="roles"> + <item>1</item> + </array> + </entity> </entities> diff --git a/app/code/Magento/Analytics/Test/Mftf/Metadata/user-meta.xml b/app/code/Magento/User/Test/Mftf/Metadata/user-meta.xml similarity index 84% rename from app/code/Magento/Analytics/Test/Mftf/Metadata/user-meta.xml rename to app/code/Magento/User/Test/Mftf/Metadata/user-meta.xml index 2e1e5f6f5a97..1ee29be6b3d7 100644 --- a/app/code/Magento/Analytics/Test/Mftf/Metadata/user-meta.xml +++ b/app/code/Magento/User/Test/Mftf/Metadata/user-meta.xml @@ -6,7 +6,7 @@ */ --> <operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> <operation name="CreateUser" dataType="user" type="create" auth="adminFormKey" url="/admin/user/save/" method="POST" successRegex="/messages-message-success/" returnRegex="" > <contentType>application/x-www-form-urlencoded</contentType> @@ -19,5 +19,8 @@ <field key="interface_locale">string</field> <field key="is_active">boolean</field> <field key="current_password">string</field> + <array key="roles"> + <value>string</value> + </array> </operation> </operations> diff --git a/app/code/Magento/Analytics/Test/Mftf/Metadata/user_role-meta.xml b/app/code/Magento/User/Test/Mftf/Metadata/user_role-meta.xml similarity index 100% rename from app/code/Magento/Analytics/Test/Mftf/Metadata/user_role-meta.xml rename to app/code/Magento/User/Test/Mftf/Metadata/user_role-meta.xml diff --git a/app/code/Magento/User/Test/Mftf/Page/AdminNewUserPage.xml b/app/code/Magento/User/Test/Mftf/Page/AdminNewUserPage.xml new file mode 100644 index 000000000000..6de094579344 --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/Page/AdminNewUserPage.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminNewUserPage" url="admin/user/new" area="admin" module="Magento_User"> + <section name="AdminNewUserFormSection" /> + <section name="AdminNewUserFormMessagesSection" /> + </page> +</pages> diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/AdminCreateRoleSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminCreateRoleSection.xml similarity index 83% rename from app/code/Magento/Braintree/Test/Mftf/Section/AdminCreateRoleSection.xml rename to app/code/Magento/User/Test/Mftf/Section/AdminCreateRoleSection.xml index 1158f471d51f..7dd313a2ba89 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Section/AdminCreateRoleSection.xml +++ b/app/code/Magento/User/Test/Mftf/Section/AdminCreateRoleSection.xml @@ -5,7 +5,9 @@ * See COPYING.txt for license details. */ --> -<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminCreateRoleSection"> <element name="create" type="button" selector="#add"/> <element name="name" type="button" selector="#role_name"/> @@ -21,4 +23,4 @@ <element name="searchButton" type="button" selector=".admin__data-grid-header button[title=Search]"/> <element name="searchResultFirstRow" type="text" selector=".data-grid>tbody>tr"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/User/Test/Mftf/Section/AdminDeleteRoleSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminDeleteRoleSection.xml new file mode 100644 index 000000000000..1b55d09d0597 --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/Section/AdminDeleteRoleSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminDeleteRoleSection"> + <element name="theRole" selector="//td[contains(text(), 'Role')]" type="button"/> + <element name="current_pass" type="button" selector="#current_password"/> + <element name="delete" selector="//button/span[contains(text(), 'Delete Role')]" type="button"/> + <element name="confirm" selector="//*[@class='action-primary action-accept']" type="button"/> + </section> +</sections> diff --git a/app/code/Magento/User/Test/Mftf/Section/AdminEditRoleInfoSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminEditRoleInfoSection.xml index e30a545649d1..57659e1aff07 100644 --- a/app/code/Magento/User/Test/Mftf/Section/AdminEditRoleInfoSection.xml +++ b/app/code/Magento/User/Test/Mftf/Section/AdminEditRoleInfoSection.xml @@ -17,5 +17,6 @@ <element name="message" type="text" selector=".modal-popup.confirm div.modal-content"/> <element name="cancel" type="button" selector=".modal-popup.confirm button.action-dismiss"/> <element name="ok" type="button" selector=".modal-popup.confirm button.action-accept" timeout="60"/> + <element name="blockName" type="checkbox" selector="//*[text()='{{var}}']//*[@class='jstree-checkbox']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/User/Test/Mftf/Section/AdminEditUserSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminEditUserSection.xml index 5b866b45e2fb..64068a0a5ef5 100644 --- a/app/code/Magento/User/Test/Mftf/Section/AdminEditUserSection.xml +++ b/app/code/Magento/User/Test/Mftf/Section/AdminEditUserSection.xml @@ -5,8 +5,12 @@ * See COPYING.txt for license details. */ --> -<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminEditUserSection"> + <element name="system" type="input" selector="#menu-magento-backend-system"/> + <element name="allUsers" type="input" selector="//span[contains(text(), 'All Users')]"/> + <element name="create" type="input" selector="#add"/> <element name="usernameTextField" type="input" selector="#user_username"/> <element name="firstNameTextField" type="input" selector="#user_firstname"/> <element name="lastNameTextField" type="input" selector="#user_lastname"/> diff --git a/app/code/Magento/User/Test/Mftf/Section/AdminNewUserFormSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminNewUserFormSection.xml new file mode 100644 index 000000000000..9b030b216ce2 --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/Section/AdminNewUserFormSection.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminNewUserFormSection"> + <element name="save" type="button" selector=".page-main-actions #save"/> + + <element name="userInfoTab" type="button" selector="#page_tabs_main_section"/> + <element name="username" type="input" selector="#page_tabs_main_section_content input[name='username']"/> + <element name="firstname" type="input" selector="#page_tabs_main_section_content input[name='firstname']"/> + <element name="lastname" type="input" selector="#page_tabs_main_section_content input[name='lastname']"/> + <element name="email" type="input" selector="#page_tabs_main_section_content input[name='email']"/> + <element name="password" type="input" selector="#page_tabs_main_section_content input[name='password']"/> + <element name="passwordConfirmation" type="input" selector="#page_tabs_main_section_content input[name='password_confirmation']"/> + <element name="interfaceLocale" type="select" selector="#page_tabs_main_section_content select[name='interface_locale']"/> + <element name="currentPassword" type="input" selector="#page_tabs_main_section_content input[name='current_password']"/> + + <element name="userRoleTab" type="button" selector="#page_tabs_roles_section"/> + <element name="search" type="button" selector="#page_tabs_roles_section_content #permissionsUserRolesGrid [data-action='grid-filter-apply']" /> + <element name="resetFilter" type="button" selector="#page_tabs_roles_section_content #permissionsUserRolesGrid [data-action='grid-filter-reset']" /> + <element name="roleFilterField" type="input" selector="#page_tabs_roles_section_content #permissionsUserRolesGrid input[name='role_name']" /> + <element name="roleRadiobutton" type="radio" selector="//table[@id='permissionsUserRolesGrid_table']//tr[./td[contains(@class, 'col-role_name') and contains(., '{{roleName}}')]]//input[@name='roles[]']" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/User/Test/Mftf/Section/AdminRoleGridSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminRoleGridSection.xml index 6db685850034..8413081237fd 100644 --- a/app/code/Magento/User/Test/Mftf/Section/AdminRoleGridSection.xml +++ b/app/code/Magento/User/Test/Mftf/Section/AdminRoleGridSection.xml @@ -14,4 +14,11 @@ <element name="roleNameInFirstRow" type="text" selector=".col-role_name"/> <element name="searchResultFirstRow" type="text" selector=".data-grid>tbody>tr"/> </section> + + <section name="AdminDeleteRoleSection"> + <element name="theRole" selector="//td[contains(text(), 'Role')]" type="button"/> + <element name="current_pass" type="button" selector="#current_password"/> + <element name="delete" selector="//button/span[contains(text(), 'Delete Role')]" type="button"/> + <element name="confirm" selector="//*[@class='action-primary action-accept']" type="button"/> + </section> </sections> diff --git a/app/code/Magento/User/Test/Mftf/Section/AdminUserFormMessagesSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminUserFormMessagesSection.xml new file mode 100644 index 000000000000..ec4f4d8bf3ad --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/Section/AdminUserFormMessagesSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminUserFormMessagesSection"> + <element name="messageByType" type="block" selector="#messages .message-{{messageType}}" parameterized="true" /> + </section> +</sections> diff --git a/app/code/Magento/User/Test/Mftf/Section/AdminUserGridSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminUserGridSection.xml index f429c390efe6..c21a8b875e95 100644 --- a/app/code/Magento/User/Test/Mftf/Section/AdminUserGridSection.xml +++ b/app/code/Magento/User/Test/Mftf/Section/AdminUserGridSection.xml @@ -14,4 +14,11 @@ <element name="searchResultFirstRow" type="text" selector=".data-grid>tbody>tr"/> <element name="successMessage" type="text" selector=".message-success"/> </section> + + <section name="AdminDeleteUserSection"> + <element name="theUser" selector="//td[contains(text(), 'John')]" type="button"/> + <element name="password" selector="#user_current_password" type="input"/> + <element name="delete" selector="//button/span[contains(text(), 'Delete User')]" type="button"/> + <element name="confirm" selector="//*[@class='action-primary action-accept']" type="button"/> + </section> </sections> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminResetUserPasswordFailedTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminResetUserPasswordFailedTest.xml new file mode 100644 index 000000000000..4b48c65a1899 --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/Test/AdminResetUserPasswordFailedTest.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminResetUserPasswordFailedTest"> + <annotations> + <features value="User"/> + <title value="Admin user should not be able to trigger the password reset procedure twice"/> + <description value="Admin user should not be able to trigger the password reset procedure twice"/> + <testCaseId value="MC-14389" /> + <group value="security"/> + <group value="mtf_migrated"/> + </annotations> + + <!-- First attempt to reset password --> + <actionGroup ref="AdminOpenForgotPasswordPageActionGroup" stepKey="openAdminForgotPasswordPage1"/> + <actionGroup ref="AdminFillForgotPasswordFormActionGroup" stepKey="fillAdminForgotPasswordForm1"> + <argument name="email" value="customer@example.com"/> + </actionGroup> + <actionGroup ref="AdminSubmitForgotPasswordFormActionGroup" stepKey="submitAdminForgotPasswordForm1"/> + <actionGroup ref="AssertMessageOnAdminLoginActionGroup" stepKey="seeSuccessMessage"> + <argument name="messageType" value="success"/> + <argument name="message" value="We'll email you a link to reset your password."/> + </actionGroup> + + <!-- Second attempt to reset password --> + <actionGroup ref="AdminOpenForgotPasswordPageActionGroup" stepKey="openAdminForgotPasswordPage2"/> + <actionGroup ref="AdminFillForgotPasswordFormActionGroup" stepKey="fillAdminForgotPasswordForm2"> + <argument name="email" value="customer@example.com"/> + </actionGroup> + <actionGroup ref="AdminSubmitForgotPasswordFormActionGroup" stepKey="submitAdminForgotPasswordForm2"/> + <actionGroup ref="AssertMessageOnAdminLoginActionGroup" stepKey="seeErrorMessage"> + <argument name="messageType" value="error"/> + <argument name="message" value="We received too many requests for password resets. Please wait and try again later or contact hello@example.com."/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminSystemAllUsersNavigateMenuTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminSystemAllUsersNavigateMenuTest.xml new file mode 100644 index 000000000000..b899320403d7 --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/Test/AdminSystemAllUsersNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminSystemAllUsersNavigateMenuTest"> + <annotations> + <features value="User"/> + <stories value="Menu Navigation"/> + <title value="Admin system all users navigate menu test"/> + <description value="Admin should be able to navigate to System > All Users"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14123"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToSystemAllUsersPage"> + <argument name="menuUiId" value="{{AdminMenuSystem.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSystemPermissionsAllUsers.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSystemPermissionsAllUsers.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminSystemLockedUsersNavigateMenuTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminSystemLockedUsersNavigateMenuTest.xml new file mode 100644 index 000000000000..aea46f327315 --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/Test/AdminSystemLockedUsersNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminSystemLockedUsersNavigateMenuTest"> + <annotations> + <features value="User"/> + <stories value="Menu Navigation"/> + <title value="Admin system locked users navigate menu test"/> + <description value="Admin should be able to navigate to System > Locked Users"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14121"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToSystemLockedUsersPage"> + <argument name="menuUiId" value="{{AdminMenuSystem.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSystemPermissionsLockedUsers.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSystemPermissionsLockedUsers.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminSystemManageEncryptionKeyNavigateMenuTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminSystemManageEncryptionKeyNavigateMenuTest.xml new file mode 100644 index 000000000000..f8013a54058c --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/Test/AdminSystemManageEncryptionKeyNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminSystemManageEncryptionKeyNavigateMenuTest"> + <annotations> + <features value="User"/> + <stories value="Menu Navigation"/> + <title value="Admin system manage encryption key navigate menu test"/> + <description value="Admin should be able to navigate to System > Manage Encryption Key"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14122"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToManageEncryptionKeyPage"> + <argument name="menuUiId" value="{{AdminMenuSystem.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSystemOtherSettingsManageEncryptionKey.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSystemOtherSettingsManageEncryptionKey.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminSystemUserRolesNavigateMenuTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminSystemUserRolesNavigateMenuTest.xml new file mode 100644 index 000000000000..c4052a7f4219 --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/Test/AdminSystemUserRolesNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminSystemUserRolesNavigateMenuTest"> + <annotations> + <features value="User"/> + <stories value="Menu Navigation"/> + <title value="Admin system user roles navigate menu test"/> + <description value="Admin should be able to navigate to System > User Roles"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14124"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToSystemUserRolesPage"> + <argument name="menuUiId" value="{{AdminMenuSystem.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSystemPermissionsUserRoles.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSystemPermissionsUserRoles.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/User/Test/Unit/Model/Authorization/AdminSessionUserContextTest.php b/app/code/Magento/User/Test/Unit/Model/Authorization/AdminSessionUserContextTest.php deleted file mode 100644 index 23681c4b8da2..000000000000 --- a/app/code/Magento/User/Test/Unit/Model/Authorization/AdminSessionUserContextTest.php +++ /dev/null @@ -1,89 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\User\Test\Unit\Model\Authorization; - -use Magento\Authorization\Model\UserContextInterface; - -/** - * Tests Magento\User\Model\Authorization\AdminSessionUserContext - */ -class AdminSessionUserContextTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager - */ - protected $objectManager; - - /** - * @var \Magento\User\Model\Authorization\AdminSessionUserContext - */ - protected $adminSessionUserContext; - - /** - * @var \Magento\Backend\Model\Auth\Session - */ - protected $adminSession; - - protected function setUp() - { - $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - - $this->adminSession = $this->getMockBuilder(\Magento\Backend\Model\Auth\Session::class) - ->disableOriginalConstructor() - ->setMethods(['hasUser', 'getUser', 'getId']) - ->getMock(); - - $this->adminSessionUserContext = $this->objectManager->getObject( - \Magento\User\Model\Authorization\AdminSessionUserContext::class, - ['adminSession' => $this->adminSession] - ); - } - - public function testGetUserIdExist() - { - $userId = 1; - - $this->setupUserId($userId); - - $this->assertEquals($userId, $this->adminSessionUserContext->getUserId()); - } - - public function testGetUserIdDoesNotExist() - { - $userId = null; - - $this->setupUserId($userId); - - $this->assertEquals($userId, $this->adminSessionUserContext->getUserId()); - } - - public function testGetUserType() - { - $this->assertEquals(UserContextInterface::USER_TYPE_ADMIN, $this->adminSessionUserContext->getUserType()); - } - - /** - * @param int|null $userId - * @return void - */ - public function setupUserId($userId) - { - $this->adminSession->expects($this->once()) - ->method('hasUser') - ->will($this->returnValue($userId)); - - if ($userId) { - $this->adminSession->expects($this->once()) - ->method('getUser') - ->will($this->returnSelf()); - - $this->adminSession->expects($this->once()) - ->method('getId') - ->will($this->returnValue($userId)); - } - } -} diff --git a/app/code/Magento/User/Test/Unit/Model/UserTest.php b/app/code/Magento/User/Test/Unit/Model/UserTest.php index 4bc5db138c7b..ab06c8754b2f 100644 --- a/app/code/Magento/User/Test/Unit/Model/UserTest.php +++ b/app/code/Magento/User/Test/Unit/Model/UserTest.php @@ -6,7 +6,9 @@ namespace Magento\User\Test\Unit\Model; -use Magento\Framework\Serialize\Serializer\Json; +use Magento\User\Helper\Data as UserHelper; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\User\Model\User; /** * Test class for \Magento\User\Model\User testing @@ -16,55 +18,11 @@ */ class UserTest extends \PHPUnit\Framework\TestCase { - /** @var \Magento\User\Model\User */ - protected $model; + /** @var User */ + private $model; - /** @var \Magento\User\Helper\Data|\PHPUnit_Framework_MockObject_MockObject */ - protected $userDataMock; - - /** @var \Magento\Framework\Mail\Template\TransportBuilder|\PHPUnit_Framework_MockObject_MockObject */ - protected $transportBuilderMock; - - /** @var \Magento\Framework\Model\Context|\PHPUnit_Framework_MockObject_MockObject */ - protected $contextMock; - - /** @var \Magento\User\Model\ResourceModel\User|\PHPUnit_Framework_MockObject_MockObject */ - protected $resourceMock; - - /** @var \Magento\Framework\Data\Collection\AbstractDb|\PHPUnit_Framework_MockObject_MockObject */ - protected $collectionMock; - - /** @var \Magento\Framework\Mail\TransportInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $transportMock; - - /** @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $storeManagerMock; - - /** @var \Magento\Store\Model\Store|\PHPUnit_Framework_MockObject_MockObject */ - protected $storeMock; - - /** @var \Magento\Backend\App\ConfigInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $configMock; - - /** @var \Magento\Framework\Encryption\EncryptorInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $encryptorMock; - - /** @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $eventManagerMock; - - /** @var \Magento\Framework\Validator\DataObjectFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $validatorObjectFactoryMock; - - /** @var \Magento\User\Model\UserValidationRules|\PHPUnit_Framework_MockObject_MockObject */ - protected $validationRulesMock; - - /** @var \Magento\Authorization\Model\RoleFactory|\PHPUnit_Framework_MockObject_MockObject */ - protected $roleFactoryMock; - - /** - * @var Json|\PHPUnit_Framework_MockObject_MockObject - */ - private $serializer; + /** @var UserHelper|\PHPUnit_Framework_MockObject_MockObject */ + private $userDataMock; /** * Set required values @@ -72,552 +30,35 @@ class UserTest extends \PHPUnit\Framework\TestCase */ protected function setUp() { - $this->userDataMock = $this->getMockBuilder(\Magento\User\Helper\Data::class) - ->disableOriginalConstructor() - ->setMethods([]) - ->getMock(); - $this->contextMock = $this->getMockBuilder(\Magento\Framework\Model\Context::class) - ->disableOriginalConstructor() - ->setMethods([]) - ->getMock(); - $this->resourceMock = $this->getMockBuilder(\Magento\User\Model\ResourceModel\User::class) - ->disableOriginalConstructor() - ->setMethods([]) - ->getMock(); - $this->collectionMock = $this->getMockBuilder(\Magento\Framework\Data\Collection\AbstractDb::class) - ->disableOriginalConstructor() - ->setMethods([]) - ->getMockForAbstractClass(); - $coreRegistry = $this->getMockBuilder(\Magento\Framework\Registry::class) - ->disableOriginalConstructor() - ->setMethods([]) - ->getMock(); - $this->eventManagerMock = $this->getMockBuilder(\Magento\Framework\Event\ManagerInterface::class) - ->disableOriginalConstructor() - ->setMethods(['dispatch']) - ->getMockForAbstractClass(); - $this->validatorObjectFactoryMock = $this->getMockBuilder(\Magento\Framework\Validator\DataObjectFactory::class) - ->disableOriginalConstructor()->setMethods(['create']) - ->getMock(); - $this->roleFactoryMock = $this->getMockBuilder(\Magento\Authorization\Model\RoleFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - $this->transportMock = $this->getMockBuilder(\Magento\Framework\Mail\TransportInterface::class) - ->disableOriginalConstructor() - ->setMethods([]) - ->getMock(); - $this->transportBuilderMock = $this->getMockBuilder(\Magento\Framework\Mail\Template\TransportBuilder::class) - ->disableOriginalConstructor() - ->setMethods([]) - ->getMock(); - $this->storeMock = $this->getMockBuilder(\Magento\Store\Model\Store::class) - ->disableOriginalConstructor() - ->setMethods([]) - ->getMock(); - $this->storeManagerMock = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) - ->disableOriginalConstructor() - ->setMethods([]) - ->getMock(); - - $this->configMock = $this->getMockBuilder(\Magento\Backend\App\ConfigInterface::class) + $this->userDataMock = $this->getMockBuilder(UserHelper::class) ->disableOriginalConstructor() ->setMethods([]) ->getMock(); - $this->validationRulesMock = $this->getMockBuilder(\Magento\User\Model\UserValidationRules::class) - ->disableOriginalConstructor() - ->setMethods([]) - ->getMock(); - - $this->encryptorMock = $this->getMockBuilder(\Magento\Framework\Encryption\EncryptorInterface::class) - ->setMethods(['validateHash']) - ->getMockForAbstractClass(); - - $this->serializer = $this->createPartialMock(Json::class, ['serialize', 'unserialize']); - - $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $objectManagerHelper = new ObjectManager($this); $this->model = $objectManagerHelper->getObject( - \Magento\User\Model\User::class, + User::class, [ - 'eventManager' => $this->eventManagerMock, 'userData' => $this->userDataMock, - 'registry' => $coreRegistry, - 'resource' => $this->resourceMock, - 'resourceCollection' => $this->collectionMock, - 'validatorObjectFactory' => $this->validatorObjectFactoryMock, - 'roleFactory' => $this->roleFactoryMock, - 'transportBuilder' => $this->transportBuilderMock, - 'storeManager' => $this->storeManagerMock, - 'validationRules' => $this->validationRulesMock, - 'config' => $this->configMock, - 'encryptor' => $this->encryptorMock, - 'serializer' => $this->serializer ] ); } - /** - * @return void - */ - public function testSendNotificationEmailsIfRequired() - { - $storeId = 0; - $email = 'test1@example.com'; - $origEmail = 'test2@example.com'; - - $password = '1234567'; - $origPassword = '123456789'; - - $username = 'admin1'; - $origUsername = 'admin2'; - - $firstName = 'Foo'; - $lastName = 'Bar'; - - $changes = __('email') . ', ' . __('password') . ', ' . __('username'); - - $this->model->setEmail($email); - $this->model->setOrigData('email', $origEmail); - - $this->model->setPassword($password); - $this->model->setOrigData('password', $origPassword); - - $this->model->setUserName($username); - $this->model->setOrigData('username', $origUsername); - - $this->model->setFirstName($firstName); - $this->model->setLastName($lastName); - - $this->configMock->expects($this->exactly(4)) - ->method('getValue') - ->withConsecutive( - [\Magento\User\Model\User::XML_PATH_USER_NOTIFICATION_TEMPLATE], - [\Magento\User\Model\User::XML_PATH_FORGOT_EMAIL_IDENTITY], - [\Magento\User\Model\User::XML_PATH_USER_NOTIFICATION_TEMPLATE], - [\Magento\User\Model\User::XML_PATH_FORGOT_EMAIL_IDENTITY] - )->willReturnOnConsecutiveCalls( - 'templateId', - 'sender', - 'templateId', - 'sender' - ); - - $this->transportBuilderMock->expects($this->exactly(2)) - ->method('setTemplateModel') - ->with($this->equalTo(\Magento\Email\Model\BackendTemplate::class)) - ->willReturnSelf(); - $this->transportBuilderMock->expects($this->exactly(2)) - ->method('setTemplateOptions') - ->willReturnSelf(); - $this->transportBuilderMock->expects($this->exactly(2)) - ->method('setTemplateVars') - ->with(['user' => $this->model, 'store' => $this->storeMock, 'changes' => $changes]) - ->willReturnSelf(); - $this->transportBuilderMock->expects($this->exactly(2)) - ->method('addTo') - ->withConsecutive( - $this->equalTo($email), - $this->equalTo($firstName . ' ' . $lastName), - $this->equalTo($origEmail), - $this->equalTo($firstName . ' ' . $lastName) - ) - ->willReturnSelf(); - $this->transportBuilderMock->expects($this->exactly(2)) - ->method('setFrom') - ->with('sender') - ->willReturnSelf(); - $this->transportBuilderMock->expects($this->exactly(2)) - ->method('setTemplateIdentifier') - ->with('templateId') - ->willReturnSelf(); - $this->transportBuilderMock->expects($this->exactly(2)) - ->method('getTransport') - ->willReturn($this->transportMock); - $this->transportMock->expects($this->exactly(2))->method('sendMessage'); - - $this->storeManagerMock->expects($this->exactly(2)) - ->method('getStore') - ->with($storeId) - ->willReturn($this->storeMock); - - $this->assertInstanceOf(\Magento\User\Model\User::class, $this->model->sendNotificationEmailsIfRequired()); - } - - /** - * @return void - */ - public function testSendPasswordResetConfirmationEmail() - { - $storeId = 0; - $email = 'test@example.com'; - $firstName = 'Foo'; - $lastName = 'Bar'; - - $this->model->setEmail($email); - $this->model->setFirstName($firstName); - $this->model->setLastName($lastName); - - $this->configMock->expects($this->at(0)) - ->method('getValue') - ->with(\Magento\User\Model\User::XML_PATH_FORGOT_EMAIL_TEMPLATE) - ->willReturn('templateId'); - $this->configMock->expects($this->at(1)) - ->method('getValue') - ->with(\Magento\User\Model\User::XML_PATH_FORGOT_EMAIL_IDENTITY) - ->willReturn('sender'); - $this->transportBuilderMock->expects($this->once()) - ->method('setTemplateModel') - ->with($this->equalTo(\Magento\Email\Model\BackendTemplate::class)) - ->willReturnSelf(); - $this->transportBuilderMock->expects($this->once()) - ->method('setTemplateOptions') - ->willReturnSelf(); - $this->transportBuilderMock->expects($this->once()) - ->method('setTemplateVars') - ->with(['user' => $this->model, 'store' => $this->storeMock]) - ->willReturnSelf(); - $this->transportBuilderMock->expects($this->once()) - ->method('addTo') - ->with($this->equalTo($email), $this->equalTo($firstName . ' ' . $lastName)) - ->willReturnSelf(); - $this->transportBuilderMock->expects($this->once()) - ->method('setFrom') - ->with('sender') - ->willReturnSelf(); - $this->transportBuilderMock->expects($this->once()) - ->method('setTemplateIdentifier') - ->with('templateId') - ->willReturnSelf(); - $this->transportBuilderMock->expects($this->once()) - ->method('getTransport') - ->willReturn($this->transportMock); - $this->transportMock->expects($this->once())->method('sendMessage'); - - $this->storeManagerMock->expects($this->once()) - ->method('getStore') - ->with($storeId) - ->willReturn($this->storeMock); - - $this->assertInstanceOf(\Magento\User\Model\User::class, $this->model->sendPasswordResetConfirmationEmail()); - } - - /** - * @return void - */ - public function testVerifyIdentity() - { - $password = 'password'; - $this->encryptorMock - ->expects($this->once()) - ->method('validateHash') - ->with($password, $this->model->getPassword()) - ->willReturn(true); - $this->model->setIsActive(true); - $this->resourceMock->expects($this->once())->method('hasAssigned2Role')->willReturn(true); - $this->assertTrue( - $this->model->verifyIdentity($password), - 'Identity verification failed while should have passed.' - ); - } - - /** - * @return void - */ - public function testVerifyIdentityFailure() - { - $password = 'password'; - $this->encryptorMock - ->expects($this->once()) - ->method('validateHash') - ->with($password, $this->model->getPassword()) - ->willReturn(false); - $this->assertFalse( - $this->model->verifyIdentity($password), - 'Identity verification passed while should have failed.' - ); - } - - /** - * @return void - */ - public function testVerifyIdentityInactiveRecord() - { - $password = 'password'; - $this->encryptorMock - ->expects($this->once()) - ->method('validateHash') - ->with($password, $this->model->getPassword()) - ->willReturn(true); - $this->model->setIsActive(false); - $this->expectException(\Magento\Framework\Exception\AuthenticationException::class); - $this->expectExceptionMessage('The account sign-in was incorrect or your account is disabled temporarily. ' - . 'Please wait and try again later.'); - $this->model->verifyIdentity($password); - } - - /** - * @return void - */ - public function testVerifyIdentityNoAssignedRoles() - { - $password = 'password'; - $this->encryptorMock - ->expects($this->once()) - ->method('validateHash') - ->with($password, $this->model->getPassword()) - ->willReturn(true); - $this->model->setIsActive(true); - $this->resourceMock->expects($this->once())->method('hasAssigned2Role')->willReturn(false); - $this->expectException(\Magento\Framework\Exception\AuthenticationException::class); - $this->expectExceptionMessage('More permissions are needed to access this.'); - $this->model->verifyIdentity($password); - } - - /** - * @return void - */ - public function testSleep() - { - $excludedProperties = [ - '_eventManager', - '_cacheManager', - '_registry', - '_appState', - '_userData', - '_config', - '_validatorObject', - '_roleFactory', - '_encryptor', - '_transportBuilder', - '_storeManager', - '_validatorBeforeSave' - ]; - $actualResult = $this->model->__sleep(); - $this->assertNotEmpty($actualResult); - $expectedResult = array_intersect($actualResult, $excludedProperties); - $this->assertEmpty($expectedResult); - } - - /** - * @return void - */ - public function testBeforeSave() - { - $this->eventManagerMock->expects($this->any())->method('dispatch'); - $this->model->setIsActive(1); - $actualData = $this->model->beforeSave()->getData(); - $this->assertArrayHasKey('extra', $actualData); - $this->assertArrayHasKey('password', $actualData); - $this->assertArrayHasKey('is_active', $actualData); - } - - /** - * @return void - */ - public function testValidateOk() - { - /** @var $validatorMock \Magento\Framework\Validator\DataObject|\PHPUnit_Framework_MockObject_MockObject */ - $validatorMock = $this->getMockBuilder(\Magento\Framework\Validator\DataObject::class) - ->disableOriginalConstructor() - ->setMethods([]) - ->getMock(); - $this->validatorObjectFactoryMock->expects($this->once())->method('create')->willReturn($validatorMock); - $this->validationRulesMock->expects($this->once()) - ->method('addUserInfoRules') - ->with($validatorMock); - $validatorMock->expects($this->once())->method('isValid')->willReturn(true); - $this->assertTrue($this->model->validate()); - } - - /** - * @return void - */ - public function testValidateInvalid() - { - $messages = ['Invalid username']; - /** @var $validatorMock \Magento\Framework\Validator\DataObject|\PHPUnit_Framework_MockObject_MockObject */ - $validatorMock = $this->getMockBuilder(\Magento\Framework\Validator\DataObject::class) - ->disableOriginalConstructor() - ->setMethods([]) - ->getMock(); - $this->validatorObjectFactoryMock->expects($this->once())->method('create')->willReturn($validatorMock); - $this->validationRulesMock->expects($this->once()) - ->method('addUserInfoRules') - ->with($validatorMock); - $validatorMock->expects($this->once())->method('isValid')->willReturn(false); - $validatorMock->expects($this->once())->method('getMessages')->willReturn($messages); - $this->assertEquals($messages, $this->model->validate()); - } - - /** - * @return void - */ - public function testSaveExtra() - { - $data = [1, 2, 3]; - $this->resourceMock->expects($this->once()) - ->method('saveExtra') - ->with($this->model, json_encode($data)); - - $this->serializer->expects($this->once()) - ->method('serialize') - ->with($data) - ->will($this->returnValue(json_encode($data))); - - $this->assertInstanceOf(\Magento\User\Model\User::class, $this->model->saveExtra($data)); - } - - /** - * @return void - */ - public function testGetRoles() - { - $this->resourceMock->expects($this->once())->method('getRoles')->with($this->model)->willReturn([]); - $this->assertInternalType('array', $this->model->getRoles()); - } - - /** - * @return void - */ - public function testGetRole() - { - $roles = ['role']; - $roleMock = $this->getMockBuilder(\Magento\Authorization\Model\Role::class) - ->disableOriginalConstructor() - ->setMethods([]) - ->getMock(); - $this->roleFactoryMock->expects($this->once())->method('create')->willReturn($roleMock); - $this->resourceMock->expects($this->once())->method('getRoles')->with($this->model)->willReturn($roles); - $roleMock->expects($this->once())->method('load')->with($roles[0]); - $this->assertInstanceOf(\Magento\Authorization\Model\Role::class, $this->model->getRole()); - } - - /** - * @return void - */ - public function testDeleteFromRole() - { - $this->resourceMock->expects($this->once())->method('deleteFromRole')->with($this->model); - $this->assertInstanceOf(\Magento\User\Model\User::class, $this->model->deleteFromRole()); - } - - /** - * @return void - */ - public function testRoleUserExistsTrue() - { - $result = ['role']; - $this->resourceMock->expects($this->once())->method('roleUserExists')->with($this->model)->willReturn($result); - $this->assertTrue($this->model->roleUserExists()); - } - - /** - * @return void - */ - public function testRoleUserExistsFalse() - { - $result = []; - $this->resourceMock->expects($this->once())->method('roleUserExists')->with($this->model)->willReturn($result); - $this->assertFalse($this->model->roleUserExists()); - } - - /** - * @return void - */ - public function testGetAclRole() - { - $roles = ['role']; - $result = 1; - $roleMock = $this->getMockBuilder(\Magento\Authorization\Model\Role::class) - ->disableOriginalConstructor() - ->setMethods([]) - ->getMock(); - $this->roleFactoryMock->expects($this->once())->method('create')->willReturn($roleMock); - $this->resourceMock->expects($this->once())->method('getRoles')->with($this->model)->willReturn($roles); - $roleMock->expects($this->once())->method('load')->with($roles[0]); - $roleMock->expects($this->once())->method('getId')->willReturn($result); - $this->assertEquals($result, $this->model->getAclRole()); - } - - /** - * @dataProvider authenticateDataProvider - * @param string $usernameIn - * @param string $usernameOut - * @param bool $expectedResult - * @return void - */ - public function testAuthenticate($usernameIn, $usernameOut, $expectedResult) - { - $password = 'password'; - $config = 'config'; - - $data = ['id' => 1, 'is_active' => 1, 'username' => $usernameOut]; - - $this->configMock->expects($this->once()) - ->method('isSetFlag') - ->with('admin/security/use_case_sensitive_login') - ->willReturn($config); - $this->eventManagerMock->expects($this->any())->method('dispatch'); - - $this->resourceMock->expects($this->any())->method('loadByUsername')->willReturn($data); - $this->model->setIdFieldName('id'); - - $this->encryptorMock->expects($this->any())->method('validateHash')->willReturn(true); - $this->resourceMock->expects($this->any())->method('hasAssigned2Role')->willReturn(true); - $this->assertEquals($expectedResult, $this->model->authenticate($usernameIn, $password)); - } - - /** - * @return array - */ - public function authenticateDataProvider() - { - return [ - 'success' => [ - 'usernameIn' => 'username', - 'usernameOut' => 'username', - 'expectedResult' => true - ], - 'failedUsername' => [ - 'usernameIn' => 'username1', - 'usernameOut' => 'username2', - 'expectedResult' => false - ] - ]; - } - - /** - * @expectedException \Magento\Framework\Exception\LocalizedException - * @return void - */ - public function testAuthenticateException() - { - $username = 'username'; - $password = 'password'; - $config = 'config'; - - $this->configMock->expects($this->once()) - ->method('isSetFlag') - ->with('admin/security/use_case_sensitive_login') - ->willReturn($config); - - $this->eventManagerMock->expects($this->any())->method('dispatch'); - $this->resourceMock->expects($this->once()) - ->method('loadByUsername') - ->willThrowException(new \Magento\Framework\Exception\LocalizedException(__())); - $this->model->authenticate($username, $password); - } - /** * @return void */ public function testChangeResetPasswordLinkToken() { $token = '1'; - $this->assertInstanceOf(\Magento\User\Model\User::class, $this->model->changeResetPasswordLinkToken($token)); + $this->assertInstanceOf( + User::class, + $this->model->changeResetPasswordLinkToken($token) + ); $this->assertEquals($token, $this->model->getRpToken()); - $this->assertInternalType('string', $this->model->getRpTokenCreatedAt()); + $this->assertInternalType( + 'string', + $this->model->getRpTokenCreatedAt() + ); } /** @@ -640,168 +81,4 @@ public function testIsResetPasswordLinkTokenExpiredIsExpiredToken() $this->userDataMock->expects($this->once())->method('getResetPasswordLinkExpirationPeriod')->willReturn(0); $this->assertTrue($this->model->isResetPasswordLinkTokenExpired()); } - - /** - * @return void - */ - public function testIsResetPasswordLinkTokenExpiredIsNotExpiredToken() - { - $this->model->setRpToken('1'); - $this->model->setRpTokenCreatedAt( - (new \DateTime())->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT) - ); - $this->userDataMock->expects($this->once())->method('getResetPasswordLinkExpirationPeriod')->willReturn(1); - $this->assertFalse($this->model->isResetPasswordLinkTokenExpired()); - } - - public function testCheckPasswordChangeEqualToCurrent() - { - /** @var $validatorMock \Magento\Framework\Validator\DataObject|\PHPUnit_Framework_MockObject_MockObject */ - $validatorMock = $this->getMockBuilder(\Magento\Framework\Validator\DataObject::class) - ->disableOriginalConstructor() - ->setMethods([]) - ->getMock(); - $this->validatorObjectFactoryMock->expects($this->once())->method('create')->willReturn($validatorMock); - $this->validationRulesMock->expects($this->once()) - ->method('addUserInfoRules') - ->with($validatorMock); - $validatorMock->expects($this->once())->method('isValid')->willReturn(true); - - $newPassword = "NEWmYn3wpassw0rd"; - $oldPassword = "OLDmYn3wpassw0rd"; - $this->model->setPassword($newPassword) - ->setId(1) - ->setOrigData('password', $oldPassword); - $this->encryptorMock->expects($this->once()) - ->method('isValidHash') - ->with($newPassword, $oldPassword) - ->willReturn(true); - $result = $this->model->validate(); - $this->assertInternalType('array', $result); - $this->assertCount(1, $result); - $this->assertContains("Sorry, but this password has already been used.", (string)$result[0]); - } - - public function testCheckPasswordChangeEqualToPrevious() - { - /** @var $validatorMock \Magento\Framework\Validator\DataObject|\PHPUnit_Framework_MockObject_MockObject */ - $validatorMock = $this->getMockBuilder(\Magento\Framework\Validator\DataObject::class) - ->disableOriginalConstructor() - ->setMethods([]) - ->getMock(); - $this->validatorObjectFactoryMock->expects($this->once())->method('create')->willReturn($validatorMock); - $this->validationRulesMock->expects($this->once()) - ->method('addUserInfoRules') - ->with($validatorMock); - $validatorMock->expects($this->once())->method('isValid')->willReturn(true); - - $newPassword = "NEWmYn3wpassw0rd"; - $newPasswordHash = "new password hash"; - $oldPassword = "OLDmYn3wpassw0rd"; - $this->model->setPassword($newPassword) - ->setId(1) - ->setOrigData('password', $oldPassword); - $this->encryptorMock->expects($this->atLeastOnce()) - ->method('isValidHash') - ->will($this->onConsecutiveCalls(false, true)); - - $this->resourceMock->expects($this->once())->method('getOldPasswords')->willReturn(['hash1', $newPasswordHash]); - - $result = $this->model->validate(); - $this->assertInternalType('array', $result); - $this->assertCount(1, $result); - $this->assertContains("Sorry, but this password has already been used.", (string)$result[0]); - } - - public function testCheckPasswordChangeValid() - { - /** @var $validatorMock \Magento\Framework\Validator\DataObject|\PHPUnit_Framework_MockObject_MockObject */ - $validatorMock = $this->getMockBuilder(\Magento\Framework\Validator\DataObject::class) - ->disableOriginalConstructor() - ->setMethods([]) - ->getMock(); - $this->validatorObjectFactoryMock->expects($this->once())->method('create')->willReturn($validatorMock); - $this->validationRulesMock->expects($this->once()) - ->method('addUserInfoRules') - ->with($validatorMock); - $validatorMock->expects($this->once())->method('isValid')->willReturn(true); - - $newPassword = "NEWmYn3wpassw0rd"; - $oldPassword = "OLDmYn3wpassw0rd"; - $this->model->setPassword($newPassword) - ->setId(1) - ->setOrigData('password', $oldPassword); - $this->encryptorMock->expects($this->atLeastOnce()) - ->method('isValidHash') - ->will($this->onConsecutiveCalls(false, false, false)); - - $this->resourceMock->expects($this->once())->method('getOldPasswords')->willReturn(['hash1', 'hash2']); - - $result = $this->model->validate(); - $this->assertTrue($result); - } - - /** - * Test for performIdentityCheck method - * - * @param bool $verifyIdentityResult - * @param bool $lockExpires - * @dataProvider dataProviderPerformIdentityCheck - */ - public function testPerformIdentityCheck($verifyIdentityResult, $lockExpires) - { - $password = 'qwerty1'; - $userName = 'John Doe'; - - $this->encryptorMock - ->expects($this->once()) - ->method('validateHash') - ->with($password, $this->model->getPassword()) - ->willReturn($verifyIdentityResult); - $this->model->setIsActive(true); - $this->resourceMock->expects($this->any())->method('hasAssigned2Role')->willReturn(true); - - $this->model->setUserName($userName); - $this->model->setLockExpires($lockExpires); - - $this->eventManagerMock->expects($this->any()) - ->method('dispatch') - ->with( - 'admin_user_authenticate_after', - [ - 'username' => $userName, - 'password' => $password, - 'user' => $this->model, - 'result' => $verifyIdentityResult - ] - ) - ->willReturnSelf(); - - if ($lockExpires) { - $this->expectException(\Magento\Framework\Exception\State\UserLockedException::class); - $this->expectExceptionMessage((string)__('Your account is temporarily disabled. Please try again later.')); - } - - if (!$lockExpires && !$verifyIdentityResult) { - $this->expectException(\Magento\Framework\Exception\AuthenticationException::class); - $this->expectExceptionMessage( - (string)__('The password entered for the current user is invalid. Verify the password and try again.') - ); - } - - $this->model->performIdentityCheck($password); - } - - /** - * @return array - */ - public function dataProviderPerformIdentityCheck() - { - return [ - ['verifyIdentityResult' => true, 'lockExpires' => false], - ['verifyIdentityResult' => false, 'lockExpires' => false], - ['verifyIdentityResult' => true, 'lockExpires' => true], - ['verifyIdentityResult' => false, 'lockExpires' => true] - ]; - } } diff --git a/app/code/Magento/User/etc/config.xml b/app/code/Magento/User/etc/config.xml index f6a3924b5a27..c1f51bcbecef 100644 --- a/app/code/Magento/User/etc/config.xml +++ b/app/code/Magento/User/etc/config.xml @@ -10,6 +10,7 @@ <admin> <emails> <forgot_email_template>admin_emails_forgot_email_template</forgot_email_template> + <new_user_notification_template>admin_emails_new_user_notification_template</new_user_notification_template> <forgot_email_identity>general</forgot_email_identity> <user_notification_template>admin_emails_user_notification_template</user_notification_template> </emails> diff --git a/app/code/Magento/User/etc/db_schema.xml b/app/code/Magento/User/etc/db_schema.xml index df9b39a27883..c3356a96b94a 100644 --- a/app/code/Magento/User/etc/db_schema.xml +++ b/app/code/Magento/User/etc/db_schema.xml @@ -37,10 +37,10 @@ <column xsi:type="timestamp" name="first_failure" on_update="false" nullable="true" comment="First Failure"/> <column xsi:type="timestamp" name="lock_expires" on_update="false" nullable="true" comment="Expiration Lock Dates"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="user_id"/> </constraint> - <constraint xsi:type="unique" name="ADMIN_USER_USERNAME"> + <constraint xsi:type="unique" referenceId="ADMIN_USER_USERNAME"> <column name="username"/> </constraint> </table> @@ -54,12 +54,12 @@ comment="Deprecated"/> <column xsi:type="int" name="last_updated" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Last Updated"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="password_id"/> </constraint> - <constraint xsi:type="foreign" name="ADMIN_PASSWORDS_USER_ID_ADMIN_USER_USER_ID" table="admin_passwords" + <constraint xsi:type="foreign" referenceId="ADMIN_PASSWORDS_USER_ID_ADMIN_USER_USER_ID" table="admin_passwords" column="user_id" referenceTable="admin_user" referenceColumn="user_id" onDelete="CASCADE"/> - <index name="ADMIN_PASSWORDS_USER_ID" indexType="btree"> + <index referenceId="ADMIN_PASSWORDS_USER_ID" indexType="btree"> <column name="user_id"/> </index> </table> diff --git a/app/code/Magento/User/etc/di.xml b/app/code/Magento/User/etc/di.xml index b92549b54dac..8fc85bc19e59 100644 --- a/app/code/Magento/User/etc/di.xml +++ b/app/code/Magento/User/etc/di.xml @@ -17,4 +17,5 @@ </argument> </arguments> </type> + <preference for="Magento\User\Model\Spi\NotificatorInterface" type="Magento\User\Model\Notificator" /> </config> diff --git a/app/code/Magento/User/etc/email_templates.xml b/app/code/Magento/User/etc/email_templates.xml index b998f304c249..637c2b799f37 100644 --- a/app/code/Magento/User/etc/email_templates.xml +++ b/app/code/Magento/User/etc/email_templates.xml @@ -8,4 +8,10 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Email:etc/email_templates.xsd"> <template id="admin_emails_forgot_email_template" label="Forgot Admin Password" file="password_reset_confirmation.html" type="text" module="Magento_User" area="adminhtml"/> <template id="admin_emails_user_notification_template" label="User Notification" file="user_notification.html" type="text" module="Magento_User" area="adminhtml"/> + <template id="admin_emails_new_user_notification_template" + label="New User Notification" + file="new_user_notification.html" + type="text" + module="Magento_User" + area="adminhtml"/> </config> diff --git a/app/code/Magento/User/etc/module.xml b/app/code/Magento/User/etc/module.xml index ad4c972ae79d..42be2fec021c 100644 --- a/app/code/Magento/User/etc/module.xml +++ b/app/code/Magento/User/etc/module.xml @@ -9,6 +9,7 @@ <module name="Magento_User" > <sequence> <module name="Magento_Backend"/> + <module name="Magento_Config"/> </sequence> </module> </config> diff --git a/app/code/Magento/User/view/adminhtml/email/new_user_notification.html b/app/code/Magento/User/view/adminhtml/email/new_user_notification.html new file mode 100644 index 000000000000..891faf5fb8c2 --- /dev/null +++ b/app/code/Magento/User/view/adminhtml/email/new_user_notification.html @@ -0,0 +1,18 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!--@subject {{trans "New admin user '%user_name' created" user_name=$user.name}} @--> +<!--@vars { +"var store.getFrontendName()|escape":"Store Name" +} @--> + +{{trans "Hello,"}} + +{{trans "A new admin account was created for %first_name, %last_name using %email." first_name=$user.first_name last_name=$user.last_name email=$user.email}} +{{trans "If you have not authorized this action, please contact us immediately at %store_email" store_email=$store_email |escape}}{{depend store_phone}} {{trans "or call us at %store_phone" store_phone=$store_phone |escape}}{{/depend}}. + +{{trans "Thanks,"}} +{{var store.getFrontendName()}} diff --git a/app/code/Magento/User/view/adminhtml/layout/adminhtml_locks_block.xml b/app/code/Magento/User/view/adminhtml/layout/adminhtml_locks_block.xml index 13cfbfe85933..d195ac93c16e 100644 --- a/app/code/Magento/User/view/adminhtml/layout/adminhtml_locks_block.xml +++ b/app/code/Magento/User/view/adminhtml/layout/adminhtml_locks_block.xml @@ -12,7 +12,7 @@ <block class="Magento\Backend\Block\Widget\Grid" name="adminhtml.block.locks.grid" as="grid"> <arguments> <argument name="id" xsi:type="string">lockedAdminsGrid</argument> - <argument name="dataSource" xsi:type="object">Magento\User\Model\ResourceModel\User\Locked\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\User\Model\ResourceModel\User\Locked\Collection</argument> <argument name="default_sort" xsi:type="string">user_id</argument> <argument name="use_ajax" xsi:type="string">1</argument> <argument name="grid_url" xsi:type="url" path="*/*/grid"> diff --git a/app/code/Magento/User/view/adminhtml/layout/adminhtml_user_grid_block.xml b/app/code/Magento/User/view/adminhtml/layout/adminhtml_user_grid_block.xml index a4bc9aa5ed48..dca45de046ef 100644 --- a/app/code/Magento/User/view/adminhtml/layout/adminhtml_user_grid_block.xml +++ b/app/code/Magento/User/view/adminhtml/layout/adminhtml_user_grid_block.xml @@ -11,11 +11,12 @@ <block class="Magento\Backend\Block\Widget\Grid" name="adminhtml.user.grid" as="grid"> <arguments> <argument name="id" xsi:type="string">permissionsUserGrid</argument> - <argument name="dataSource" xsi:type="object">Magento\User\Model\ResourceModel\User\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\User\Model\ResourceModel\User\Collection</argument> <argument name="use_ajax" xsi:type="string">1</argument> <argument name="default_sort" xsi:type="string">username</argument> <argument name="default_dir" xsi:type="string">asc</argument> <argument name="grid_url" xsi:type="url" path="*/*/roleGrid"/> + <argument name="save_parameters_in_session" xsi:type="boolean">true</argument> </arguments> <block class="Magento\Backend\Block\Widget\Grid\ColumnSet" as="grid.columnSet" name="permission.user.grid.columnSet"> <arguments> diff --git a/app/code/Magento/User/view/adminhtml/layout/adminhtml_user_role_grid_block.xml b/app/code/Magento/User/view/adminhtml/layout/adminhtml_user_role_grid_block.xml index 6d7ce67e2352..f2d699b6cc08 100644 --- a/app/code/Magento/User/view/adminhtml/layout/adminhtml_user_role_grid_block.xml +++ b/app/code/Magento/User/view/adminhtml/layout/adminhtml_user_role_grid_block.xml @@ -11,7 +11,7 @@ <block class="Magento\Backend\Block\Widget\Grid" name="adminhtml.role.grid" as="grid"> <arguments> <argument name="id" xsi:type="string">roleGrid</argument> - <argument name="dataSource" xsi:type="object">Magento\Authorization\Model\ResourceModel\Role\Grid\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Authorization\Model\ResourceModel\Role\Grid\Collection</argument> <argument name="save_parameters_in_session" xsi:type="string">1</argument> <argument name="default_sort" xsi:type="string">role_id</argument> <argument name="default_dir" xsi:type="string">asc</argument> diff --git a/app/code/Magento/Usps/Model/Carrier.php b/app/code/Magento/Usps/Model/Carrier.php index 1e8faf3cc961..6e69b9c31794 100644 --- a/app/code/Magento/Usps/Model/Carrier.php +++ b/app/code/Magento/Usps/Model/Carrier.php @@ -366,6 +366,7 @@ public function getResult() /** * @inheritdoc + * * Starting from 23.02.2018 USPS doesn't allow to create free shipping labels via their API. */ public function isShippingLabelsAvailable() @@ -448,7 +449,7 @@ protected function _getXmlQuotes() $package->addChild('FirstClassMailType', 'PARCEL'); } $package->addChild('ZipOrigination', $r->getOrigPostal()); - //only 5 chars avaialble + //only 5 chars available $package->addChild('ZipDestination', substr($r->getDestPostal(), 0, 5)); $package->addChild('Pounds', $r->getWeightPounds()); $package->addChild('Ounces', $r->getWeightOunces()); @@ -1167,6 +1168,7 @@ public function getAllowedMethods() /** * Return USPS county name by country ISO 3166-1-alpha-2 code + * * Return false for unknown countries * * @param string $countryId @@ -1246,7 +1248,7 @@ protected function _getCountryName($countryId) 'FO' => 'Faroe Islands', 'FR' => 'France', 'GA' => 'Gabon', - 'GB' => 'Great Britain and Northern Ireland', + 'GB' => 'United Kingdom of Great Britain and Northern Ireland', 'GD' => 'Grenada', 'GE' => 'Georgia, Republic of', 'GF' => 'French Guiana', @@ -1364,7 +1366,7 @@ protected function _getCountryName($countryId) 'ST' => 'Sao Tome and Principe', 'SV' => 'El Salvador', 'SY' => 'Syrian Arab Republic', - 'SZ' => 'Swaziland', + 'SZ' => 'Eswatini', 'TC' => 'Turks and Caicos Islands', 'TD' => 'Chad', 'TG' => 'Togo', diff --git a/app/code/Magento/Variable/Controller/Adminhtml/System/Variable/Index.php b/app/code/Magento/Variable/Controller/Adminhtml/System/Variable/Index.php index f70cb31eb545..71cb76a67d15 100644 --- a/app/code/Magento/Variable/Controller/Adminhtml/System/Variable/Index.php +++ b/app/code/Magento/Variable/Controller/Adminhtml/System/Variable/Index.php @@ -6,12 +6,14 @@ */ namespace Magento\Variable\Controller\Adminhtml\System\Variable; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + /** * Display Variables list page * @api * @since 100.0.2 */ -class Index extends \Magento\Variable\Controller\Adminhtml\System\Variable +class Index extends \Magento\Variable\Controller\Adminhtml\System\Variable implements HttpGetActionInterface { /** * Index Action diff --git a/app/code/Magento/Variable/Model/Source/Variables.php b/app/code/Magento/Variable/Model/Source/Variables.php index 38c2f92d2554..e878a3172be4 100644 --- a/app/code/Magento/Variable/Model/Source/Variables.php +++ b/app/code/Magento/Variable/Model/Source/Variables.php @@ -23,6 +23,16 @@ class Variables implements \Magento\Framework\Option\ArrayInterface */ private $configVariables = []; + /** + * @var array + */ + private $configPaths = []; + + /** + * @var \Magento\Config\Model\Config\Structure\SearchInterface + */ + private $configStructure; + /** * Constructor. * @@ -33,25 +43,8 @@ public function __construct( \Magento\Config\Model\Config\Structure\SearchInterface $configStructure, array $configPaths = [] ) { - foreach ($configPaths as $groupPath => $groupElements) { - $groupPathElements = explode('/', $groupPath); - $path = []; - $labels = []; - foreach ($groupPathElements as $groupPathElement) { - $path[] = $groupPathElement; - $labels[] = __( - $configStructure->getElementByConfigPath(implode('/', $path))->getLabel() - ); - } - $this->configVariables[$groupPath]['label'] = implode(' / ', $labels); - foreach (array_keys($groupElements) as $elementPath) { - $this->configVariables[$groupPath]['elements'][] = [ - 'value' => $elementPath, - 'label' => __($configStructure->getElementByConfigPath($elementPath)->getLabel()), - ]; - } - } - $this->configVariables; + $this->configStructure = $configStructure; + $this->configPaths = $configPaths; } /** @@ -64,7 +57,7 @@ public function toOptionArray($withGroup = false) { $optionArray = []; if ($withGroup) { - foreach ($this->configVariables as $configVariableGroup) { + foreach ($this->getConfigVariables() as $configVariableGroup) { $group = [ 'label' => $configVariableGroup['label'] ]; @@ -79,7 +72,7 @@ public function toOptionArray($withGroup = false) $optionArray[] = $group; } } else { - foreach ($this->configVariables as $configVariableGroup) { + foreach ($this->getConfigVariables() as $configVariableGroup) { foreach ($configVariableGroup['elements'] as $element) { $optionArray[] = [ 'value' => '{{config path="' . $element['value'] . '"}}', @@ -110,7 +103,7 @@ public function getData() private function getFlatConfigVars() { $result = []; - foreach ($this->configVariables as $configVariableGroup) { + foreach ($this->getConfigVariables() as $configVariableGroup) { foreach ($configVariableGroup['elements'] as $element) { $element['group_label'] = $configVariableGroup['label']; $result[] = $element; @@ -118,4 +111,35 @@ private function getFlatConfigVars() } return $result; } + + /** + * Merge config with user defined data + * + * @return array + */ + private function getConfigVariables() + { + if (empty($this->configVariables)) { + foreach ($this->configPaths as $groupPath => $groupElements) { + $groupPathElements = explode('/', $groupPath); + $path = []; + $labels = []; + foreach ($groupPathElements as $groupPathElement) { + $path[] = $groupPathElement; + $labels[] = __( + $this->configStructure->getElementByConfigPath(implode('/', $path))->getLabel() + ); + } + $this->configVariables[$groupPath]['label'] = implode(' / ', $labels); + foreach (array_keys($groupElements) as $elementPath) { + $this->configVariables[$groupPath]['elements'][] = [ + 'value' => $elementPath, + 'label' => __($this->configStructure->getElementByConfigPath($elementPath)->getLabel()), + ]; + } + } + } + + return $this->configVariables; + } } diff --git a/app/code/Magento/Variable/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Variable/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 000000000000..e09423976748 --- /dev/null +++ b/app/code/Magento/Variable/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> + <!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + --> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> +<entity name="AdminMenuSystemOtherSettingsCustomVariables"> + <data key="pageTitle">Custom Variables</data> + <data key="title">Custom Variables</data> + <data key="dataUiId">magento-variable-system-variable</data> +</entity> +</entities> diff --git a/app/code/Magento/Variable/Test/Mftf/Test/AdminSystemCustomVariablesNavigateMenuTest.xml b/app/code/Magento/Variable/Test/Mftf/Test/AdminSystemCustomVariablesNavigateMenuTest.xml new file mode 100644 index 000000000000..74446cf60134 --- /dev/null +++ b/app/code/Magento/Variable/Test/Mftf/Test/AdminSystemCustomVariablesNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminSystemCustomVariablesNavigateMenuTest"> + <annotations> + <features value="Variable"/> + <stories value="Menu Navigation"/> + <title value="Admin system custom variables navigate menu test"/> + <description value="Admin should be able to navigate to System > Custom Variables"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14126"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToSystemCustomVariablesPage"> + <argument name="menuUiId" value="{{AdminMenuSystem.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSystemOtherSettingsCustomVariables.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuSystemOtherSettingsCustomVariables.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Variable/etc/db_schema.xml b/app/code/Magento/Variable/etc/db_schema.xml index d89bdb7e710b..239e3b49983c 100644 --- a/app/code/Magento/Variable/etc/db_schema.xml +++ b/app/code/Magento/Variable/etc/db_schema.xml @@ -12,10 +12,10 @@ comment="Variable Id"/> <column xsi:type="varchar" name="code" nullable="true" length="255" comment="Variable Code"/> <column xsi:type="varchar" name="name" nullable="true" length="255" comment="Variable Name"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="variable_id"/> </constraint> - <constraint xsi:type="unique" name="VARIABLE_CODE"> + <constraint xsi:type="unique" referenceId="VARIABLE_CODE"> <column name="code"/> </constraint> </table> @@ -28,18 +28,18 @@ default="0" comment="Store Id"/> <column xsi:type="text" name="plain_value" nullable="true" comment="Plain Text Value"/> <column xsi:type="text" name="html_value" nullable="true" comment="Html Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="unique" name="VARIABLE_VALUE_VARIABLE_ID_STORE_ID"> + <constraint xsi:type="unique" referenceId="VARIABLE_VALUE_VARIABLE_ID_STORE_ID"> <column name="variable_id"/> <column name="store_id"/> </constraint> - <constraint xsi:type="foreign" name="VARIABLE_VALUE_STORE_ID_STORE_STORE_ID" table="variable_value" + <constraint xsi:type="foreign" referenceId="VARIABLE_VALUE_STORE_ID_STORE_STORE_ID" table="variable_value" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="VARIABLE_VALUE_VARIABLE_ID_VARIABLE_VARIABLE_ID" table="variable_value" + <constraint xsi:type="foreign" referenceId="VARIABLE_VALUE_VARIABLE_ID_VARIABLE_VARIABLE_ID" table="variable_value" column="variable_id" referenceTable="variable" referenceColumn="variable_id" onDelete="CASCADE"/> - <index name="VARIABLE_VALUE_STORE_ID" indexType="btree"> + <index referenceId="VARIABLE_VALUE_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> diff --git a/app/code/Magento/Variable/etc/di.xml b/app/code/Magento/Variable/etc/di.xml index f0a24e89ef8d..41759e1f1582 100644 --- a/app/code/Magento/Variable/etc/di.xml +++ b/app/code/Magento/Variable/etc/di.xml @@ -42,6 +42,7 @@ <item name="general/store_information/merchant_vat_number" xsi:type="string">1</item> </item> </argument> + <argument name="configStructure" xsi:type="object">Magento\Config\Model\Config\Structure\Proxy</argument> </arguments> </type> -</config> \ No newline at end of file +</config> diff --git a/app/code/Magento/Variable/view/adminhtml/layout/adminhtml_system_variable_grid_block.xml b/app/code/Magento/Variable/view/adminhtml/layout/adminhtml_system_variable_grid_block.xml index d934e46117fb..edee7fcca822 100644 --- a/app/code/Magento/Variable/view/adminhtml/layout/adminhtml_system_variable_grid_block.xml +++ b/app/code/Magento/Variable/view/adminhtml/layout/adminhtml_system_variable_grid_block.xml @@ -11,7 +11,7 @@ <block class="Magento\Backend\Block\Widget\Grid" name="adminhtml.system.variable.grid" as="grid"> <arguments> <argument name="id" xsi:type="string">customVariablesGrid</argument> - <argument name="dataSource" xsi:type="object">Magento\Variable\Model\ResourceModel\Variable\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Variable\Model\ResourceModel\Variable\Collection</argument> <argument name="default_sort" xsi:type="string">variable_id</argument> <argument name="default_dir" xsi:type="string">ASC</argument> </arguments> diff --git a/app/code/Magento/Variable/view/adminhtml/web/variables.js b/app/code/Magento/Variable/view/adminhtml/web/variables.js index 47f027f27102..bf8bfbc570ce 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/Model/PaymentTokenRepository.php b/app/code/Magento/Vault/Model/PaymentTokenRepository.php index 8539d3515821..2ccd6181b9b8 100644 --- a/app/code/Magento/Vault/Model/PaymentTokenRepository.php +++ b/app/code/Magento/Vault/Model/PaymentTokenRepository.php @@ -58,13 +58,15 @@ class PaymentTokenRepository implements PaymentTokenRepositoryInterface private $collectionProcessor; /** - * @param \Magento\Vault\Model\ResourceModel\PaymentToken $resourceModel + * PaymentTokenRepository constructor. + * + * @param PaymentTokenResourceModel $resourceModel * @param PaymentTokenFactory $paymentTokenFactory * @param FilterBuilder $filterBuilder * @param SearchCriteriaBuilder $searchCriteriaBuilder * @param PaymentTokenSearchResultsInterfaceFactory $searchResultsFactory * @param CollectionFactory $collectionFactory - * @param CollectionProcessorInterface | null $collectionProcessor + * @param CollectionProcessorInterface|null $collectionProcessor */ public function __construct( PaymentTokenResourceModel $resourceModel, @@ -99,7 +101,7 @@ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCr $searchResults = $this->searchResultsFactory->create(); $searchResults->setSearchCriteria($searchCriteria); $searchResults->setItems($collection->getItems()); - + $searchResults->setTotalCount($collection->getSize()); return $searchResults; } @@ -140,7 +142,7 @@ public function delete(Data\PaymentTokenInterface $paymentToken) /** * Performs persist operations for a specified payment token. * - * @param \Magento\Vault\Api\Data\PaymentTokenInterface $entity The payment token. + * @param \Magento\Vault\Api\Data\PaymentTokenInterface $paymentToken The payment token. * @return \Magento\Vault\Api\Data\PaymentTokenInterface Saved payment token data. */ public function save(Data\PaymentTokenInterface $paymentToken) diff --git a/app/code/Magento/Vault/etc/db_schema.xml b/app/code/Magento/Vault/etc/db_schema.xml index ed1f2adba214..8a7c8dc4aa9f 100644 --- a/app/code/Magento/Vault/etc/db_schema.xml +++ b/app/code/Magento/Vault/etc/db_schema.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="vault_payment_token" resource="default" engine="innodb" comment="Vault tokens of payment"> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" - comment="Entity Id"/> + comment="Entity ID"/> <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="true" identity="false" comment="Customer Id"/> <column xsi:type="varchar" name="public_hash" nullable="false" length="128" @@ -24,18 +24,18 @@ <column xsi:type="text" name="details" nullable="true" comment="Details"/> <column xsi:type="boolean" name="is_active" nullable="false" default="true"/> <column xsi:type="boolean" name="is_visible" nullable="false" default="true"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> - <constraint xsi:type="foreign" name="VAULT_PAYMENT_TOKEN_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="VAULT_PAYMENT_TOKEN_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID" table="vault_payment_token" column="customer_id" referenceTable="customer_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="VAULT_PAYMENT_TOKEN_PAYMENT_METHOD_CODE_CSTR_ID_GATEWAY_TOKEN"> + <constraint xsi:type="unique" referenceId="VAULT_PAYMENT_TOKEN_PAYMENT_METHOD_CODE_CSTR_ID_GATEWAY_TOKEN"> <column name="payment_method_code"/> <column name="customer_id"/> <column name="gateway_token"/> </constraint> - <constraint xsi:type="unique" name="VAULT_PAYMENT_TOKEN_PUBLIC_HASH"> + <constraint xsi:type="unique" referenceId="VAULT_PAYMENT_TOKEN_PUBLIC_HASH"> <column name="public_hash"/> </constraint> </table> @@ -45,14 +45,14 @@ comment="Order payment Id"/> <column xsi:type="int" name="payment_token_id" padding="10" unsigned="true" nullable="false" identity="false" comment="Payment token Id"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="order_payment_id"/> <column name="payment_token_id"/> </constraint> - <constraint xsi:type="foreign" name="FK_CF37B9D854256534BE23C818F6291CA2" + <constraint xsi:type="foreign" referenceId="FK_CF37B9D854256534BE23C818F6291CA2" table="vault_payment_token_order_payment_link" column="order_payment_id" referenceTable="sales_order_payment" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="FK_4ED894655446D385894580BECA993862" + <constraint xsi:type="foreign" referenceId="FK_4ED894655446D385894580BECA993862" table="vault_payment_token_order_payment_link" column="payment_token_id" referenceTable="vault_payment_token" referenceColumn="entity_id" onDelete="CASCADE"/> </table> 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 0ef330cd3014..5f32281686a6 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> @@ -32,6 +33,11 @@ <div class="payment-method-content"> <each args="getRegion('messages')" render=""></each> + <div class="payment-method-billing-address"> + <each args="data: $parent.getRegion(getBillingAddressFormName()), as: '$item'"> + <render args="$item.getTemplate()"/> + </each> + </div> <div class="checkout-agreements-block"> <!-- ko foreach: $parent.getRegion('before-place-order') --> <!-- ko template: getTemplate() --><!-- /ko --> 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 000000000000..cbdbbdcf010b --- /dev/null +++ b/app/code/Magento/VaultGraphQl/Model/Resolver/DeletePaymentToken.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\VaultGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Vault\Api\PaymentTokenManagementInterface; +use Magento\Vault\Api\PaymentTokenRepositoryInterface; +use Magento\CustomerGraphQl\Model\Customer\GetCustomer; + +/** + * Delete Payment Token resolver, used for GraphQL mutation processing. + */ +class DeletePaymentToken implements ResolverInterface +{ + /** + * @var GetCustomer + */ + private $getCustomer; + + /** + * @var PaymentTokenManagementInterface + */ + private $paymentTokenManagement; + + /** + * @var PaymentTokenRepositoryInterface + */ + private $paymentTokenRepository; + + /** + * @param GetCustomer $getCustomer + * @param PaymentTokenManagementInterface $paymentTokenManagement + * @param PaymentTokenRepositoryInterface $paymentTokenRepository + */ + public function __construct( + GetCustomer $getCustomer, + PaymentTokenManagementInterface $paymentTokenManagement, + PaymentTokenRepositoryInterface $paymentTokenRepository + ) { + $this->getCustomer = $getCustomer; + $this->paymentTokenManagement = $paymentTokenManagement; + $this->paymentTokenRepository = $paymentTokenRepository; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($args['public_hash'])) { + throw new GraphQlInputException(__('Specify the "public_hash" value.')); + } + + $customer = $this->getCustomer->execute($context); + + $token = $this->paymentTokenManagement->getByPublicHash($args['public_hash'], $customer->getId()); + if (!$token) { + throw new GraphQlNoSuchEntityException( + __('Could not find a token using public hash: %1', $args['public_hash']) + ); + } + + return ['result' => $this->paymentTokenRepository->delete($token)]; + } +} diff --git a/app/code/Magento/VaultGraphQl/Model/Resolver/PaymentTokens.php b/app/code/Magento/VaultGraphQl/Model/Resolver/PaymentTokens.php new file mode 100644 index 000000000000..1563eaedf6b9 --- /dev/null +++ b/app/code/Magento/VaultGraphQl/Model/Resolver/PaymentTokens.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\VaultGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Vault\Model\PaymentTokenManagement; +use Magento\CustomerGraphQl\Model\Customer\GetCustomer; + +/** + * Customers Payment Tokens resolver, used for GraphQL request processing. + */ +class PaymentTokens implements ResolverInterface +{ + /** + * @var PaymentTokenManagement + */ + private $paymentTokenManagement; + + /** + * @var GetCustomer + */ + private $getCustomer; + + /** + * @param PaymentTokenManagement $paymentTokenManagement + * @param GetCustomer $getCustomer + */ + public function __construct( + PaymentTokenManagement $paymentTokenManagement, + GetCustomer $getCustomer + ) { + $this->paymentTokenManagement = $paymentTokenManagement; + $this->getCustomer = $getCustomer; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + $customer = $this->getCustomer->execute($context); + + $tokens = $this->paymentTokenManagement->getVisibleAvailableTokens($customer->getId()); + $result = []; + + foreach ($tokens as $token) { + $result[] = [ + 'public_hash' => $token->getPublicHash(), + 'payment_method_code' => $token->getPaymentMethodCode(), + 'type' => $token->getType(), + 'details' => $token->getTokenDetails(), + ]; + } + return ['items' => $result]; + } +} diff --git a/app/code/Magento/VaultGraphQl/README.md b/app/code/Magento/VaultGraphQl/README.md new file mode 100644 index 000000000000..afcb1d83f277 --- /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 000000000000..455d24bfc11f --- /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 000000000000..f821d9fa6704 --- /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 000000000000..64484fe9e814 --- /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") @cache(cacheable: false) +} + +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 000000000000..3f48c00f0709 --- /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/Version/Controller/Index/Index.php b/app/code/Magento/Version/Controller/Index/Index.php index c5d02cd9a1fd..0db9b5f80d48 100644 --- a/app/code/Magento/Version/Controller/Index/Index.php +++ b/app/code/Magento/Version/Controller/Index/Index.php @@ -6,6 +6,7 @@ */ namespace Magento\Version\Controller\Index; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Framework\App\Action\Action; use Magento\Framework\App\Action\Context; use Magento\Framework\App\ProductMetadataInterface; @@ -13,7 +14,7 @@ /** * Magento Version controller */ -class Index extends Action +class Index extends Action implements HttpGetActionInterface { /** * @var ProductMetadataInterface diff --git a/app/code/Magento/Webapi/Controller/PathProcessor.php b/app/code/Magento/Webapi/Controller/PathProcessor.php index 5e8c23aa1550..f32c93fb0c76 100644 --- a/app/code/Magento/Webapi/Controller/PathProcessor.php +++ b/app/code/Magento/Webapi/Controller/PathProcessor.php @@ -1,13 +1,17 @@ <?php - /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Webapi\Controller; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\NoSuchEntityException; +/** + * Class PathProcessor + */ class PathProcessor { /** Store code alias to indicate that all stores should be affected by action */ @@ -18,12 +22,23 @@ class PathProcessor */ private $storeManager; + /** + * @var \Magento\Framework\Locale\ResolverInterface + */ + private $localeResolver; + /** * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param \Magento\Framework\Locale\ResolverInterface $localeResolver */ - public function __construct(\Magento\Store\Model\StoreManagerInterface $storeManager) - { + public function __construct( + \Magento\Store\Model\StoreManagerInterface $storeManager, + \Magento\Framework\Locale\ResolverInterface $localeResolver = null + ) { $this->storeManager = $storeManager; + $this->localeResolver = $localeResolver ?: ObjectManager::getInstance()->get( + \Magento\Framework\Locale\ResolverInterface::class + ); } /** @@ -50,13 +65,15 @@ private function stripPathBeforeStorecode($pathInfo) public function process($pathInfo) { $pathParts = $this->stripPathBeforeStorecode($pathInfo); - $storeCode = $pathParts[0]; + $storeCode = current($pathParts); $stores = $this->storeManager->getStores(false, true); if (isset($stores[$storeCode])) { $this->storeManager->setCurrentStore($storeCode); + $this->localeResolver->emulate($this->storeManager->getStore()->getId()); $path = '/' . (isset($pathParts[1]) ? $pathParts[1] : ''); } elseif ($storeCode === self::ALL_STORE_CODE) { $this->storeManager->setCurrentStore(\Magento\Store\Model\Store::ADMIN_CODE); + $this->localeResolver->emulate($this->storeManager->getStore()->getId()); $path = '/' . (isset($pathParts[1]) ? $pathParts[1] : ''); } else { $path = '/' . implode('/', $pathParts); diff --git a/app/code/Magento/Webapi/Controller/Rest/ParamsOverrider.php b/app/code/Magento/Webapi/Controller/Rest/ParamsOverrider.php index 1d8ccc127cea..d1a3c0c0f086 100644 --- a/app/code/Magento/Webapi/Controller/Rest/ParamsOverrider.php +++ b/app/code/Magento/Webapi/Controller/Rest/ParamsOverrider.php @@ -197,7 +197,7 @@ private function isPropertyDeclaredInDataObject( $index = array_search($serviceMethodParamName, array_column($methodParams, 'name')); if ($index !== false) { $paramObjectType = $methodParams[$index][MethodsMap::METHOD_META_TYPE]; - $setter = 'set' . ucfirst(SimpleDataObjectConverter::snakeCaseToCamelCase($objectProperty)); + $setter = 'set' . SimpleDataObjectConverter::snakeCaseToUpperCamelCase($objectProperty); if (array_key_exists( $setter, $this->getMethodsMap()->getMethodsMap($paramObjectType) diff --git a/app/code/Magento/Webapi/Model/Config/ClassReflector.php b/app/code/Magento/Webapi/Model/Config/ClassReflector.php index 7ce94c9bc6ee..6748319f9f48 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/Webapi/Test/Unit/Controller/PathProcessorTest.php b/app/code/Magento/Webapi/Test/Unit/Controller/PathProcessorTest.php index c213c72b5185..e587756eec7b 100644 --- a/app/code/Magento/Webapi/Test/Unit/Controller/PathProcessorTest.php +++ b/app/code/Magento/Webapi/Test/Unit/Controller/PathProcessorTest.php @@ -8,11 +8,17 @@ use Magento\Store\Model\Store; +/** + * Test for Magento\Webapi\Controller\PathProcessor class. + */ class PathProcessorTest extends \PHPUnit\Framework\TestCase { /** @var \PHPUnit_Framework_MockObject_MockObject | \Magento\Store\Model\StoreManagerInterface */ private $storeManagerMock; + /** @var \PHPUnit_Framework_MockObject_MockObject | \Magento\Framework\Locale\ResolverInterface */ + private $localeResolverMock; + /** @var \Magento\Webapi\Controller\PathProcessor */ private $model; @@ -24,13 +30,22 @@ class PathProcessorTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->storeManagerMock = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->storeManagerMock->expects($this->once()) - ->method('getStores') - ->willReturn([$this->arbitraryStoreCode => 'store object', 'default' => 'default store object']); - $this->model = new \Magento\Webapi\Controller\PathProcessor($this->storeManagerMock); + $store = $this->createMock(\Magento\Store\Api\Data\StoreInterface::class); + $store->method('getId')->willReturn(2); + + $this->storeManagerMock = $this->createConfiguredMock( + \Magento\Store\Model\StoreManagerInterface::class, + [ + 'getStores' => [$this->arbitraryStoreCode => 'store object', 'default' => 'default store object'], + 'getStore' => $store, + ] + ); + $this->storeManagerMock->expects($this->once())->method('getStores'); + + $this->localeResolverMock = $this->createMock(\Magento\Framework\Locale\ResolverInterface::class); + $this->localeResolverMock->method('emulate')->with(2); + + $this->model = new \Magento\Webapi\Controller\PathProcessor($this->storeManagerMock, $this->localeResolverMock); } /** @@ -47,6 +62,10 @@ public function testAllStoreCode($storeCodeInPath, $storeCodeSet, $setCurrentSto $this->storeManagerMock->expects($this->exactly($setCurrentStoreCallCtr)) ->method('setCurrentStore') ->with($storeCodeSet); + if ($setCurrentStoreCallCtr > 0) { + $this->localeResolverMock->expects($this->once()) + ->method('emulate'); + } $result = $this->model->process($inPath); $this->assertSame($this->endpointPath, $result); } @@ -60,7 +79,7 @@ public function processPathDataProvider() 'All store code' => ['all', Store::ADMIN_CODE], 'Default store code' => ['', 'default', 0], 'Arbitrary store code' => [$this->arbitraryStoreCode, $this->arbitraryStoreCode], - 'Explicit default store code' => ['default', 'default'] + 'Explicit default store code' => ['default', 'default'], ]; } } diff --git a/app/code/Magento/WebapiAsync/Code/Generator/Config/RemoteServiceReader/Communication.php b/app/code/Magento/WebapiAsync/Code/Generator/Config/RemoteServiceReader/Communication.php index 39e7fa418a35..2a6cac09c22f 100644 --- a/app/code/Magento/WebapiAsync/Code/Generator/Config/RemoteServiceReader/Communication.php +++ b/app/code/Magento/WebapiAsync/Code/Generator/Config/RemoteServiceReader/Communication.php @@ -67,7 +67,8 @@ public function read($scope = null) CommunicationConfig::HANDLER_TYPE => $serviceClass, CommunicationConfig::HANDLER_METHOD => $serviceMethod, ], - ] + ], + false ); $rewriteTopicParams = [ CommunicationConfig::TOPIC_IS_SYNCHRONOUS => false, diff --git a/app/code/Magento/WebapiAsync/Controller/Rest/Asynchronous/InputParamsResolver.php b/app/code/Magento/WebapiAsync/Controller/Rest/Asynchronous/InputParamsResolver.php index e6df38c563ed..93bddd09faef 100644 --- a/app/code/Magento/WebapiAsync/Controller/Rest/Asynchronous/InputParamsResolver.php +++ b/app/code/Magento/WebapiAsync/Controller/Rest/Asynchronous/InputParamsResolver.php @@ -80,12 +80,14 @@ public function __construct( /** * Process and resolve input parameters + * * Return array with validated input params * or throw \Exception if at least one request entity params is not valid * * @return array * @throws \Magento\Framework\Exception\InputException if no value is provided for required parameters * @throws \Magento\Framework\Webapi\Exception + * @throws \Magento\Framework\Exception\AuthorizationException */ public function resolve() { @@ -95,6 +97,13 @@ public function resolve() $this->requestValidator->validate(); $webapiResolvedParams = []; $inputData = $this->request->getRequestData(); + + $httpMethod = $this->request->getHttpMethod(); + if ($httpMethod == \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_DELETE) { + $requestBodyParams = $this->request->getBodyParams(); + $inputData = array_merge($requestBodyParams, $inputData); + } + foreach ($inputData as $key => $singleEntityParams) { $webapiResolvedParams[$key] = $this->resolveBulkItemParams($singleEntityParams); } @@ -103,6 +112,8 @@ public function resolve() } /** + * Returns route. + * * @return \Magento\Webapi\Controller\Rest\Router\Route */ public function getRoute() @@ -111,6 +122,8 @@ public function getRoute() } /** + * Resolve parameters for service + * * Convert the input array from key-value format to a list of parameters * suitable for the specified class / method. * diff --git a/app/code/Magento/WebapiAsync/Model/Config.php b/app/code/Magento/WebapiAsync/Model/Config.php index 92343027adcf..16c24643ba35 100644 --- a/app/code/Magento/WebapiAsync/Model/Config.php +++ b/app/code/Magento/WebapiAsync/Model/Config.php @@ -15,6 +15,9 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Webapi\Model\Config\Converter; +/** + * Class for accessing to Webapi_Async configuration. + */ class Config implements \Magento\AsynchronousOperations\Model\ConfigInterface { /** @@ -55,7 +58,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getServices() { @@ -73,26 +76,30 @@ public function getServices() } /** - * {@inheritdoc} + * @inheritdoc */ public function getTopicName($routeUrl, $httpMethod) { $services = $this->getServices(); - $topicName = $this->generateTopicNameByRouteData( + $lookupKey = $this->generateLookupKeyByRouteData( $routeUrl, $httpMethod ); - if (array_key_exists($topicName, $services) === false) { + if (array_key_exists($lookupKey, $services) === false) { throw new LocalizedException( - __('WebapiAsync config for "%topicName" does not exist.', ['topicName' => $topicName]) + __('WebapiAsync config for "%lookupKey" does not exist.', ['lookupKey' => $lookupKey]) ); } - return $services[$topicName][self::SERVICE_PARAM_KEY_TOPIC]; + return $services[$lookupKey][self::SERVICE_PARAM_KEY_TOPIC]; } /** + * Generate topic data for all defined services + * + * Topic data is indexed by a lookup key that is derived from route data + * * @return array */ private function generateTopicsDataFromWebapiConfig() @@ -105,11 +112,18 @@ private function generateTopicsDataFromWebapiConfig() $serviceInterface = $httpMethodData[Converter::KEY_SERVICE][Converter::KEY_SERVICE_CLASS]; $serviceMethod = $httpMethodData[Converter::KEY_SERVICE][Converter::KEY_SERVICE_METHOD]; - $topicName = $this->generateTopicNameByRouteData( + $lookupKey = $this->generateLookupKeyByRouteData( $routeUrl, $httpMethod ); - $services[$topicName] = [ + + $topicName = $this->generateTopicNameFromService( + $serviceInterface, + $serviceMethod, + $httpMethod + ); + + $services[$lookupKey] = [ self::SERVICE_PARAM_KEY_INTERFACE => $serviceInterface, self::SERVICE_PARAM_KEY_METHOD => $serviceMethod, self::SERVICE_PARAM_KEY_TOPIC => $topicName, @@ -122,7 +136,7 @@ private function generateTopicsDataFromWebapiConfig() } /** - * Generate topic name based on service type and method name. + * Generate lookup key name based on route and method * * Perform the following conversion: * self::TOPIC_PREFIX + /V1/products + POST => async.V1.products.POST @@ -131,19 +145,39 @@ private function generateTopicsDataFromWebapiConfig() * @param string $httpMethod * @return string */ - private function generateTopicNameByRouteData($routeUrl, $httpMethod) + private function generateLookupKeyByRouteData($routeUrl, $httpMethod) { - return self::TOPIC_PREFIX . $this->generateTopicName($routeUrl, $httpMethod, '/', false); + return self::TOPIC_PREFIX . $this->generateKey($routeUrl, $httpMethod, '/', false); } /** + * Generate topic name based on service type and method name. + * + * Perform the following conversion: + * self::TOPIC_PREFIX + Magento\Catalog\Api\ProductRepositoryInterface + save + POST + * => async.magento.catalog.api.productrepositoryinterface.save.POST + * + * @param string $serviceInterface + * @param string $serviceMethod + * @param string $httpMethod + * @return string + */ + private function generateTopicNameFromService($serviceInterface, $serviceMethod, $httpMethod) + { + $typeName = strtolower(sprintf('%s.%s', $serviceInterface, $serviceMethod)); + return strtolower(self::TOPIC_PREFIX . $this->generateKey($typeName, $httpMethod, '\\', false)); + } + + /** + * Join and simplify input type and method into a string that can be used as an array key + * * @param string $typeName * @param string $methodName * @param string $delimiter * @param bool $lcfirst * @return string */ - private function generateTopicName($typeName, $methodName, $delimiter = '\\', $lcfirst = true) + private function generateKey($typeName, $methodName, $delimiter = '\\', $lcfirst = true) { $parts = explode($delimiter, ltrim($typeName, $delimiter)); foreach ($parts as &$part) { diff --git a/app/code/Magento/WebapiAsync/Plugin/Cache/Webapi.php b/app/code/Magento/WebapiAsync/Plugin/Cache/Webapi.php new file mode 100644 index 000000000000..ecc929b20484 --- /dev/null +++ b/app/code/Magento/WebapiAsync/Plugin/Cache/Webapi.php @@ -0,0 +1,106 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\WebapiAsync\Plugin\Cache; + +use Magento\WebapiAsync\Controller\Rest\AsynchronousSchemaRequestProcessor; +use Magento\Framework\Webapi\Rest\Request; + +/** + * Class Webapi + */ +class Webapi +{ + /** + * Cache key for Async Routes + */ + const ASYNC_ROUTES_CONFIG_CACHE_ID = 'async-routes-services-config'; + + /** + * @var AsynchronousSchemaRequestProcessor + */ + private $asynchronousSchemaRequestProcessor; + + /** + * @var \Magento\Framework\Webapi\Rest\Request + */ + private $request; + + /** + * ServiceMetadata constructor. + * + * @param Request $request + * @param AsynchronousSchemaRequestProcessor $asynchronousSchemaRequestProcessor + */ + public function __construct( + \Magento\Framework\Webapi\Rest\Request $request, + AsynchronousSchemaRequestProcessor $asynchronousSchemaRequestProcessor + ) { + $this->request = $request; + $this->asynchronousSchemaRequestProcessor = $asynchronousSchemaRequestProcessor; + } + + /** + * Change identifier in case if Async request before cache load + * + * @param \Magento\Webapi\Model\Cache\Type\Webapi $subject + * @param string $identifier + * @return null|string + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeLoad(\Magento\Webapi\Model\Cache\Type\Webapi $subject, $identifier) + { + if ($this->asynchronousSchemaRequestProcessor->canProcess($this->request) + && $identifier === \Magento\Webapi\Model\ServiceMetadata::ROUTES_CONFIG_CACHE_ID) { + return self::ASYNC_ROUTES_CONFIG_CACHE_ID; + } + return null; + } + + /** + * Change identifier in case if Async request before cache save + * + * @param \Magento\Webapi\Model\Cache\Type\Webapi $subject + * @param string $data + * @param string $identifier + * @param array $tags + * @param int|bool|null $lifeTime + * @return array|null + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeSave( + \Magento\Webapi\Model\Cache\Type\Webapi $subject, + $data, + $identifier, + array $tags = [], + $lifeTime = null + ) { + if ($this->asynchronousSchemaRequestProcessor->canProcess($this->request) + && $identifier === \Magento\Webapi\Model\ServiceMetadata::ROUTES_CONFIG_CACHE_ID) { + return [$data, self::ASYNC_ROUTES_CONFIG_CACHE_ID, $tags, $lifeTime]; + } + return null; + } + + /** + * Change identifier in case if Async request before remove cache + * + * @param \Magento\Webapi\Model\Cache\Type\Webapi $subject + * @param string $identifier + * @return null|string + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeRemove(\Magento\Webapi\Model\Cache\Type\Webapi $subject, $identifier) + { + if ($this->asynchronousSchemaRequestProcessor->canProcess($this->request) + && $identifier === \Magento\Webapi\Model\ServiceMetadata::ROUTES_CONFIG_CACHE_ID) { + return self::ASYNC_ROUTES_CONFIG_CACHE_ID; + } + return null; + } +} diff --git a/app/code/Magento/WebapiAsync/Test/Unit/Controller/PathProcessorTest.php b/app/code/Magento/WebapiAsync/Test/Unit/Controller/PathProcessorTest.php index e5453f757454..570df6afd3c5 100644 --- a/app/code/Magento/WebapiAsync/Test/Unit/Controller/PathProcessorTest.php +++ b/app/code/Magento/WebapiAsync/Test/Unit/Controller/PathProcessorTest.php @@ -10,11 +10,17 @@ use Magento\Store\Model\Store; +/** + * Test for Magento\Webapi\Controller\PathProcessor class. + */ class PathProcessorTest extends \PHPUnit\Framework\TestCase { /** @var \PHPUnit_Framework_MockObject_MockObject | \Magento\Store\Model\StoreManagerInterface */ private $storeManagerMock; + /** @var \PHPUnit_Framework_MockObject_MockObject | \Magento\Framework\Locale\ResolverInterface */ + private $localeResolverMock; + /** @var \Magento\Webapi\Controller\PathProcessor */ private $model; @@ -26,16 +32,22 @@ class PathProcessorTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->storeManagerMock = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->storeManagerMock->expects($this->once()) - ->method('getStores') - ->willReturn([ - $this->arbitraryStoreCode => 'store object', - 'default' => 'default store object', - ]); - $this->model = new \Magento\Webapi\Controller\PathProcessor($this->storeManagerMock); + $store = $this->createMock(\Magento\Store\Api\Data\StoreInterface::class); + $store->method('getId')->willReturn(2); + + $this->storeManagerMock = $this->createConfiguredMock( + \Magento\Store\Model\StoreManagerInterface::class, + [ + 'getStores' => [$this->arbitraryStoreCode => 'store object', 'default' => 'default store object'], + 'getStore' => $store, + ] + ); + $this->storeManagerMock->expects($this->once())->method('getStores'); + + $this->localeResolverMock = $this->createMock(\Magento\Framework\Locale\ResolverInterface::class); + $this->localeResolverMock->method('emulate')->with(2); + + $this->model = new \Magento\Webapi\Controller\PathProcessor($this->storeManagerMock, $this->localeResolverMock); } /** diff --git a/app/code/Magento/WebapiAsync/Test/Unit/Model/ConfigTest.php b/app/code/Magento/WebapiAsync/Test/Unit/Model/ConfigTest.php new file mode 100644 index 000000000000..47b75b205731 --- /dev/null +++ b/app/code/Magento/WebapiAsync/Test/Unit/Model/ConfigTest.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\WebapiAsync\Test\Unit\Model; + +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Webapi\Model\Cache\Type\Webapi; +use Magento\Webapi\Model\Config as WebapiConfig; +use Magento\WebapiAsync\Model\Config; +use Magento\Webapi\Model\Config\Converter; + +class ConfigTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Config + */ + private $config; + + /** + * @var Webapi|\PHPUnit_Framework_MockObject_MockObject + */ + private $webapiCacheMock; + + /** + * @var WebapiConfig|\PHPUnit_Framework_MockObject_MockObject + */ + private $configMock; + + /** + * @var SerializerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $serializerMock; + + protected function setUp() + { + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->webapiCacheMock = $this->createMock(\Magento\Webapi\Model\Cache\Type\Webapi::class); + $this->configMock = $this->createMock(WebapiConfig::class); + $this->serializerMock = $this->createMock(SerializerInterface::class); + + $this->config = $objectManager->getObject( + Config::class, + [ + 'cache' => $this->webapiCacheMock, + 'webApiConfig' => $this->configMock, + 'serializer' => $this->serializerMock + ] + ); + } + + public function testGetServicesSetsTopicFromServiceContractName() + { + $services = [ + Converter::KEY_ROUTES => [ + '/V1/products' => [ + 'POST' => [ + 'service' => [ + 'class' => \Magento\Catalog\Api\ProductRepositoryInterface::class, + 'method' => 'save', + ] + ] + ] + ] + ]; + $this->configMock->expects($this->once()) + ->method('getServices') + ->willReturn($services); + + /* example of what $this->config->getServices() returns + $result = [ + 'async.V1.products.POST' => [ + 'interface' => 'Magento\Catalog\Api\ProductRepositoryInterface', + 'method' => 'save', + 'topic' => 'async.magento.catalog.api.productrepositoryinterface.save.post', + ] + ]; + */ + $result = $this->config->getServices(); + + $expectedTopic = 'async.magento.catalog.api.productrepositoryinterface.save.post'; + $lookupKey = 'async.V1.products.POST'; + $this->assertArrayHasKey($lookupKey, $result); + $this->assertEquals($result[$lookupKey]['topic'], $expectedTopic); + } +} diff --git a/app/code/Magento/WebapiAsync/etc/di.xml b/app/code/Magento/WebapiAsync/etc/di.xml index 83f1d6a78f22..7411ec0561d2 100755 --- a/app/code/Magento/WebapiAsync/etc/di.xml +++ b/app/code/Magento/WebapiAsync/etc/di.xml @@ -10,6 +10,9 @@ <type name="Magento\Webapi\Model\ServiceMetadata"> <plugin name="webapiServiceMetadataAsync" type="Magento\WebapiAsync\Plugin\ServiceMetadata" /> </type> + <type name="Magento\Webapi\Model\Cache\Type\Webapi"> + <plugin name="webapiCacheAsync" type="Magento\WebapiAsync\Plugin\Cache\Webapi" /> + </type> <virtualType name="Magento\WebapiAsync\Model\VirtualType\Rest\Config" type="Magento\Webapi\Model\Rest\Config"> <arguments> <argument name="config" xsi:type="object">Magento\WebapiAsync\Model\BulkServiceConfig</argument> diff --git a/app/code/Magento/Weee/Model/Sales/Pdf/Weee.php b/app/code/Magento/Weee/Model/Sales/Pdf/Weee.php index fa71e8128176..2b8c74581d97 100644 --- a/app/code/Magento/Weee/Model/Sales/Pdf/Weee.php +++ b/app/code/Magento/Weee/Model/Sales/Pdf/Weee.php @@ -36,6 +36,7 @@ public function __construct( /** * Check if weee total amount should be included * + * Example: * array( * $index => array( * 'amount' => $amount, @@ -43,6 +44,7 @@ public function __construct( * 'font_size'=> $font_size * ) * ) + * * @return array */ public function getTotalsForDisplay() @@ -70,4 +72,17 @@ public function getTotalsForDisplay() return $totals; } + + /** + * Check if we can display Weee total information in PDF + * + * @return bool + */ + public function canDisplay() + { + $items = $this->getSource()->getAllItems(); + $store = $this->getSource()->getStore(); + $amount = $this->_weeeData->getTotalAmounts($items, $store); + return $this->getDisplayZero() === 'true' || $amount != 0; + } } diff --git a/app/code/Magento/Weee/Plugin/Catalog/Controller/Adminhtml/Product/Initialization/Helper/ProcessTaxAttribute.php b/app/code/Magento/Weee/Plugin/Catalog/Controller/Adminhtml/Product/Initialization/Helper/ProcessTaxAttribute.php new file mode 100644 index 000000000000..4aa941cd1379 --- /dev/null +++ b/app/code/Magento/Weee/Plugin/Catalog/Controller/Adminhtml/Product/Initialization/Helper/ProcessTaxAttribute.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Weee\Plugin\Catalog\Controller\Adminhtml\Product\Initialization\Helper; + +use Magento\Catalog\Model\Product; +use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper; +use Magento\Framework\App\RequestInterface; + +/** + * Handles product tax attributes data initialization. + */ +class ProcessTaxAttribute +{ + /** + * @var RequestInterface + */ + private $request; + + /** + * @param RequestInterface $request + */ + public function __construct(RequestInterface $request) + { + $this->request = $request; + } + + /** + * Handles product tax attributes data initialization. + * + * @param Helper $subject + * @param Product $result + * @param Product $product + * @param array $productData + * @return Product + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterInitializeFromData( + Helper $subject, + Product $result, + Product $product, + array $productData + ): Product { + $attributes = $result->getAttributes(); + if (!empty($attributes)) { + foreach ($attributes as $attribute) { + if ($attribute->getFrontendInput() == 'weee' && !isset($productData[$attribute->getAttributeCode()])) { + $result->setData($attribute->getAttributeCode(), []); + } + } + } + + return $result; + } +} diff --git a/app/code/Magento/Weee/Test/Mftf/Section/AdminProductAddFPTValueSection.xml b/app/code/Magento/Weee/Test/Mftf/Section/AdminProductAddFPTValueSection.xml index eee3f421910e..ebf9f4104712 100644 --- a/app/code/Magento/Weee/Test/Mftf/Section/AdminProductAddFPTValueSection.xml +++ b/app/code/Magento/Weee/Test/Mftf/Section/AdminProductAddFPTValueSection.xml @@ -10,6 +10,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminProductAddFPTValueSection"> <element name="addFPT" type="button" selector="[data-index='{{FPTAttributeCode}}'] [data-action='add_new_row']" parameterized="true"/> + <element name="removeRowByIndex" type="button" selector="[data-index='{{FPTAttributeCode}}'] [data-action='remove_row']:nth-of-type({{rowIndex}})" parameterized="true"/> <element name="selectCountryForFPT" type="select" selector="(//select[contains(@name, 'product[{{FPTAttributeCode}}]') and contains(@name, '[country]')])[last()]" parameterized="true"/> <element name="selectStateForFPT" type="select" selector="(//select[contains(@name, 'product[{{FPTAttributeCode}}]') and contains(@name, '[state]')])[last()]" parameterized="true"/> <element name="setTaxValueForFPT" type="text" selector="(//input[contains(@name, 'product[{{FPTAttributeCode}}]') and contains(@name, '[value]')])[last()]" parameterized="true"/> diff --git a/app/code/Magento/Weee/Test/Mftf/Test/AdminRemoveProductWeeeAttributeOptionTest.xml b/app/code/Magento/Weee/Test/Mftf/Test/AdminRemoveProductWeeeAttributeOptionTest.xml new file mode 100644 index 000000000000..3aeed3095dc4 --- /dev/null +++ b/app/code/Magento/Weee/Test/Mftf/Test/AdminRemoveProductWeeeAttributeOptionTest.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="AdminRemoveProductWeeeAttributeOptionTest"> + <annotations> + <stories value="Weee attribute options can be removed in product page"/> + <title value="Weee attribute options can be removed in product page"/> + <description value="Weee attribute options can be removed in product page"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-95033"/> + <group value="weee"/> + </annotations> + <before> + <createData entity="productFPTAttribute" stepKey="createProductFPTAttribute"/> + <createData entity="AddToDefaultSet" stepKey="addFPTToAttributeSet"> + <requiredEntity createDataKey="createProductFPTAttribute"/> + </createData> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProductInitial"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProductInitial"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="AdminProductAddFPTValueActionGroup" stepKey="addWeeeAttributeValue"> + <argument name="FPTAttributeCode" value="$$createProductFPTAttribute.attribute_code$$"/> + <argument name="stateForFPT" value="California"/> + <argument name="valueForFPT" value="10"/> + </actionGroup> + <actionGroup ref="saveProductForm" stepKey="saveProductInitial"/> + </before> + <after> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductListing"/> + <waitForPageLoad stepKey="waitForProductListingPageLoad"/> + <actionGroup ref="resetProductGridToDefaultView" stepKey="resetGridToDefaultKeywordSearch"/> + <actionGroup ref="logout" stepKey="logout"/> + <deleteData createDataKey="createProductFPTAttribute" stepKey="deleteProductFPTAttribute"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + </after> + + <!-- Test Steps --> + <!-- Step 1: Open created product edit page --> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProduct"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <!-- Step 2: Remove weee attribute options --> + <click selector="{{AdminProductAddFPTValueSection.removeRowByIndex('$$createProductFPTAttribute.attribute_code$$','1')}}" stepKey="removeAttributeOption"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + <!-- Assert weee attribute options are empty --> + <dontSeeElement selector="{{AdminProductAddFPTValueSection.removeRowByIndex('$$createProductFPTAttribute.attribute_code$$','1')}}" stepKey="dontSeeOptions"/> + </test> +</tests> diff --git a/app/code/Magento/Weee/Test/Unit/Observer/AddPaymentWeeeItemTest.php b/app/code/Magento/Weee/Test/Unit/Observer/AddPaymentWeeeItemTest.php new file mode 100644 index 000000000000..dd0547354cfa --- /dev/null +++ b/app/code/Magento/Weee/Test/Unit/Observer/AddPaymentWeeeItemTest.php @@ -0,0 +1,154 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Weee\Test\Unit\Observer; + +use Magento\Framework\Event; +use Magento\Framework\Event\Observer; +use Magento\Payment\Model\Cart; +use Magento\Payment\Model\Cart\SalesModel\SalesModelInterface; +use Magento\Quote\Model\Quote\Item; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Weee\Helper\Data; +use Magento\Weee\Observer\AddPaymentWeeeItem; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + +/** + * Class AddPaymentWeeeItemTest + */ +class AddPaymentWeeeItemTest extends TestCase +{ + /** + * Testable object + * + * @var AddPaymentWeeeItem + */ + private $observer; + + /** + * @var Data|MockObject + */ + private $weeeHelperMock; + + /** + * @var StoreManagerInterface|MockObject + */ + private $storeManagerMock; + + /** + * Set Up + */ + protected function setUp() + { + $this->weeeHelperMock = $this->createMock(Data::class); + $this->storeManagerMock = $this->createMock(StoreManagerInterface::class); + + $this->observer = new AddPaymentWeeeItem( + $this->weeeHelperMock, + $this->storeManagerMock + ); + } + + /** + * Test execute + * + * @dataProvider dataProvider + * @param bool $isEnabled + * @param bool $includeInSubtotal + * @return void + */ + public function testExecute(bool $isEnabled, bool $includeInSubtotal): void + { + /** @var Observer|MockObject $observerMock */ + $observerMock = $this->createMock(Observer::class); + $cartModelMock = $this->createMock(Cart::class); + $salesModelMock = $this->createMock(SalesModelInterface::class); + $itemMock = $this->createPartialMock(Item::class, ['getOriginalItem']); + $originalItemMock = $this->createPartialMock(Item::class, ['getParentItem']); + $parentItemMock = $this->createMock(Item::class); + $eventMock = $this->getMockBuilder(Event::class) + ->disableOriginalConstructor() + ->setMethods(['getCart']) + ->getMock(); + + $asCustomItem = $this->prepareShouldBeAddedAsCustomItem($isEnabled, $includeInSubtotal); + $toBeCalled = 1; + if (!$asCustomItem) { + $toBeCalled = 0; + } + + $eventMock->expects($this->exactly($toBeCalled)) + ->method('getCart') + ->willReturn($cartModelMock); + $observerMock->expects($this->exactly($toBeCalled)) + ->method('getEvent') + ->willReturn($eventMock); + $itemMock->expects($this->exactly($toBeCalled)) + ->method('getOriginalItem') + ->willReturn($originalItemMock); + $originalItemMock->expects($this->exactly($toBeCalled)) + ->method('getParentItem') + ->willReturn($parentItemMock); + $salesModelMock->expects($this->exactly($toBeCalled)) + ->method('getAllItems') + ->willReturn([$itemMock]); + $cartModelMock->expects($this->exactly($toBeCalled)) + ->method('getSalesModel') + ->willReturn($salesModelMock); + + $this->observer->execute($observerMock); + } + + /** + * @return array + */ + public function dataProvider(): array + { + return [ + [true, false], + [true, true], + [false, true], + [false, false], + ]; + } + + /** + * Prepare if FPT should be added to payment cart as custom item or not. + * + * @param bool $isEnabled + * @param bool $includeInSubtotal + * @return bool + */ + private function prepareShouldBeAddedAsCustomItem(bool $isEnabled, bool $includeInSubtotal): bool + { + $storeMock = $this->getMockBuilder(StoreInterface::class) + ->setMethods(['getId']) + ->getMockForAbstractClass(); + $storeMock->expects($this->once()) + ->method('getId') + ->willReturn(Store::DEFAULT_STORE_ID); + $this->storeManagerMock->expects($this->once()) + ->method('getStore') + ->willReturn($storeMock); + $this->weeeHelperMock->expects($this->once()) + ->method('isEnabled') + ->with(Store::DEFAULT_STORE_ID) + ->willReturn($isEnabled); + + if ($isEnabled) { + $this->weeeHelperMock->expects($this->once()) + ->method('includeInSubtotal') + ->with(Store::DEFAULT_STORE_ID) + ->willReturn($includeInSubtotal); + } + + return $isEnabled && !$includeInSubtotal; + } +} diff --git a/app/code/Magento/Weee/Test/Unit/Observer/SetWeeeRendererInFormObserverTest.php b/app/code/Magento/Weee/Test/Unit/Observer/SetWeeeRendererInFormObserverTest.php new file mode 100644 index 000000000000..188b42cb3790 --- /dev/null +++ b/app/code/Magento/Weee/Test/Unit/Observer/SetWeeeRendererInFormObserverTest.php @@ -0,0 +1,88 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Weee\Test\Unit\Observer; + +use Magento\Framework\Data\Form; +use Magento\Framework\Event; +use Magento\Framework\Event\Observer; +use Magento\Framework\View\LayoutInterface; +use Magento\Weee\Model\Tax; +use Magento\Weee\Observer\SetWeeeRendererInFormObserver; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + +/** + * Class AddPaymentWeeeItemTest + */ +class SetWeeeRendererInFormObserverTest extends TestCase +{ + /** + * Testable object + * + * @var SetWeeeRendererInFormObserver + */ + private $observer; + + /** + * @var LayoutInterface|MockObject + */ + private $layoutMock; + + /** + * @var Tax|MockObject + */ + private $taxModelMock; + + /** + * Set Up + */ + protected function setUp() + { + $this->layoutMock = $this->createMock(LayoutInterface::class); + $this->taxModelMock = $this->createMock(Tax::class); + $this->observer = new SetWeeeRendererInFormObserver( + $this->layoutMock, + $this->taxModelMock + ); + } + + /** + * Test assigning a custom renderer for product create/edit form weee attribute element + * + * @return void + */ + public function testExecute(): void + { + $attributes = new \ArrayIterator(['element_code_1', 'element_code_2']); + /** @var Event|MockObject $eventMock */ + $eventMock = $this->getMockBuilder(Event::class) + ->disableOriginalConstructor() + ->setMethods(['getForm']) + ->getMock(); + + /** @var Observer|MockObject $observerMock */ + $observerMock = $this->createMock(Observer::class); + /** @var Form|MockObject $formMock */ + $formMock = $this->createMock(Form::class); + + $eventMock->expects($this->once()) + ->method('getForm') + ->willReturn($formMock); + $observerMock->expects($this->once()) + ->method('getEvent') + ->willReturn($eventMock); + $this->taxModelMock->expects($this->once()) + ->method('getWeeeAttributeCodes') + ->willReturn($attributes); + $formMock->expects($this->exactly($attributes->count())) + ->method('getElement') + ->willReturnSelf(); + + $this->observer->execute($observerMock); + } +} diff --git a/app/code/Magento/Weee/Ui/DataProvider/Product/Form/Modifier/Weee.php b/app/code/Magento/Weee/Ui/DataProvider/Product/Form/Modifier/Weee.php index c96a7a020033..d05b8c216893 100644 --- a/app/code/Magento/Weee/Ui/DataProvider/Product/Form/Modifier/Weee.php +++ b/app/code/Magento/Weee/Ui/DataProvider/Product/Form/Modifier/Weee.php @@ -85,7 +85,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function modifyData(array $data) { @@ -93,7 +93,7 @@ public function modifyData(array $data) } /** - * {@inheritdoc} + * @inheritdoc */ public function modifyMeta(array $meta) { @@ -155,6 +155,7 @@ protected function modifyAttributeConfig($attributeCode, array $attributeConfig) 'dndConfig' => [ 'enabled' => false, ], + 'required' => (bool)$attributeConfig['arguments']['data']['config']['required'], ], ], ], @@ -180,6 +181,7 @@ protected function modifyAttributeConfig($attributeCode, array $attributeConfig) 'component' => 'Magento_Weee/js/fpt-group', 'visible' => true, 'label' => __('Country/State'), + 'showLabel' => false, ], ], ], @@ -197,6 +199,7 @@ protected function modifyAttributeConfig($attributeCode, array $attributeConfig) 'validation' => [ 'required-entry' => true, ], + 'showLabel' => false, ], ], ], @@ -216,6 +219,7 @@ protected function modifyAttributeConfig($attributeCode, array $attributeConfig) ], 'caption' => '*', 'visible' => true, + 'showLabel' => false, ], ], ], @@ -233,6 +237,7 @@ protected function modifyAttributeConfig($attributeCode, array $attributeConfig) 'validation' => [ 'validate-fpt-group' => true ], + 'showLabel' => false, ], ], ], @@ -252,6 +257,7 @@ protected function modifyAttributeConfig($attributeCode, array $attributeConfig) 'validation' => [ 'required-entry' => true ], + 'showLabel' => false, ], ], ], @@ -267,6 +273,7 @@ protected function modifyAttributeConfig($attributeCode, array $attributeConfig) 'label' => __('Website'), 'visible' => $this->websiteManager->isMultiWebsites(), 'options' => $this->websiteManager->getWebsites($product, $eavAttribute), + 'showLabel' => false, ], ], ], diff --git a/app/code/Magento/Weee/etc/adminhtml/system.xml b/app/code/Magento/Weee/etc/adminhtml/system.xml index ae02b27d10c7..d3e9efb8f0b4 100644 --- a/app/code/Magento/Weee/etc/adminhtml/system.xml +++ b/app/code/Magento/Weee/etc/adminhtml/system.xml @@ -44,6 +44,7 @@ <group id="totals_sort"> <field id="weee" translate="label" type="text" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1"> <label>Fixed Product Tax</label> + <validate>required-number validate-number</validate> </field> </group> </section> diff --git a/app/code/Magento/Weee/etc/db_schema.xml b/app/code/Magento/Weee/etc/db_schema.xml index 96c3be9740e5..1b0716824701 100644 --- a/app/code/Magento/Weee/etc/db_schema.xml +++ b/app/code/Magento/Weee/etc/db_schema.xml @@ -13,7 +13,7 @@ <column xsi:type="smallint" name="website_id" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Website Id"/> <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="false" - default="0" comment="Entity Id"/> + default="0" comment="Entity ID"/> <column xsi:type="varchar" name="country" nullable="true" length="2" comment="Country"/> <column xsi:type="decimal" name="value" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Value"/> @@ -21,30 +21,30 @@ comment="State"/> <column xsi:type="smallint" name="attribute_id" padding="5" unsigned="true" nullable="false" identity="false" comment="Attribute Id"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> - <constraint xsi:type="foreign" name="WEEE_TAX_COUNTRY_DIRECTORY_COUNTRY_COUNTRY_ID" table="weee_tax" + <constraint xsi:type="foreign" referenceId="WEEE_TAX_COUNTRY_DIRECTORY_COUNTRY_COUNTRY_ID" table="weee_tax" column="country" referenceTable="directory_country" referenceColumn="country_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="WEEE_TAX_ENTITY_ID_CATALOG_PRODUCT_ENTITY_ENTITY_ID" table="weee_tax" + <constraint xsi:type="foreign" referenceId="WEEE_TAX_ENTITY_ID_CATALOG_PRODUCT_ENTITY_ENTITY_ID" table="weee_tax" column="entity_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="WEEE_TAX_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID" table="weee_tax" + <constraint xsi:type="foreign" referenceId="WEEE_TAX_WEBSITE_ID_STORE_WEBSITE_WEBSITE_ID" table="weee_tax" column="website_id" referenceTable="store_website" referenceColumn="website_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="WEEE_TAX_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" table="weee_tax" + <constraint xsi:type="foreign" referenceId="WEEE_TAX_ATTRIBUTE_ID_EAV_ATTRIBUTE_ATTRIBUTE_ID" table="weee_tax" column="attribute_id" referenceTable="eav_attribute" referenceColumn="attribute_id" onDelete="CASCADE"/> - <index name="WEEE_TAX_WEBSITE_ID" indexType="btree"> + <index referenceId="WEEE_TAX_WEBSITE_ID" indexType="btree"> <column name="website_id"/> </index> - <index name="WEEE_TAX_ENTITY_ID" indexType="btree"> + <index referenceId="WEEE_TAX_ENTITY_ID" indexType="btree"> <column name="entity_id"/> </index> - <index name="WEEE_TAX_COUNTRY" indexType="btree"> + <index referenceId="WEEE_TAX_COUNTRY" indexType="btree"> <column name="country"/> </index> - <index name="WEEE_TAX_ATTRIBUTE_ID" indexType="btree"> + <index referenceId="WEEE_TAX_ATTRIBUTE_ID" indexType="btree"> <column name="attribute_id"/> </index> </table> diff --git a/app/code/Magento/Weee/etc/di.xml b/app/code/Magento/Weee/etc/di.xml index e52aebd3af8b..8b433163cad2 100644 --- a/app/code/Magento/Weee/etc/di.xml +++ b/app/code/Magento/Weee/etc/di.xml @@ -78,4 +78,7 @@ </argument> </arguments> </type> + <type name="Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper"> + <plugin name="weeeAttributeOptionsProcess" type="Magento\Weee\Plugin\Catalog\Controller\Adminhtml\Product\Initialization\Helper\ProcessTaxAttribute"/> + </type> </config> diff --git a/app/code/Magento/Weee/view/frontend/requirejs-config.js b/app/code/Magento/Weee/view/frontend/requirejs-config.js index 49dfb6b9d469..94c59da65216 100644 --- a/app/code/Magento/Weee/view/frontend/requirejs-config.js +++ b/app/code/Magento/Weee/view/frontend/requirejs-config.js @@ -6,7 +6,8 @@ var config = { map: { '*': { - 'taxToggle': 'Magento_Weee/tax-toggle' + 'taxToggle': 'Magento_Weee/js/tax-toggle', + 'Magento_Weee/tax-toggle': 'Magento_Weee/js/tax-toggle' } } }; diff --git a/app/code/Magento/Weee/view/frontend/web/tax-toggle.js b/app/code/Magento/Weee/view/frontend/web/js/tax-toggle.js similarity index 100% rename from app/code/Magento/Weee/view/frontend/web/tax-toggle.js rename to app/code/Magento/Weee/view/frontend/web/js/tax-toggle.js diff --git a/app/code/Magento/Widget/Block/Adminhtml/Widget.php b/app/code/Magento/Widget/Block/Adminhtml/Widget.php index e4854415d753..dad318f163b4 100644 --- a/app/code/Magento/Widget/Block/Adminhtml/Widget.php +++ b/app/code/Magento/Widget/Block/Adminhtml/Widget.php @@ -15,7 +15,7 @@ class Widget extends \Magento\Backend\Block\Widget\Form\Container { /** - * @return void + * @inheritdoc */ protected function _construct() { @@ -40,13 +40,16 @@ protected function _construct() $this->buttonList->update('reset', 'label', __('Cancel')); $this->buttonList->update('reset', 'onclick', 'wWidget.closeModal()'); - $this->_formScripts[] = 'require(["mage/adminhtml/wysiwyg/widget"],' - . ' function(){wWidget = new WysiwygWidget.Widget(' - . '"widget_options_form", "select_widget_type", "widget_options", "' - . $this->getUrl( - 'adminhtml/*/loadOptions' - ) . '", "' . $this->getRequest()->getParam( - 'widget_target_id' - ) . '");});'; + $this->_formScripts[] = <<<EOJS +require(['mage/adminhtml/wysiwyg/widget'], function() { + wWidget = new WysiwygWidget.Widget( + 'widget_options_form', + 'select_widget_type', + 'widget_options', + '{$this->getUrl('adminhtml/*/loadOptions')}', + '{$this->escapeJs($this->getRequest()->getParam('widget_target_id'))}' + ); +}); +EOJS; } } 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 7e6ba8786030..230598a7e263 100644 --- a/app/code/Magento/Widget/Block/Adminhtml/Widget/Catalog/Category/Chooser.php +++ b/app/code/Magento/Widget/Block/Adminhtml/Widget/Catalog/Category/Chooser.php @@ -3,14 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +namespace Magento\Widget\Block\Adminhtml\Widget\Catalog\Category; /** * Category chooser for widget's layout updates - * - * @author Magento Core Team <core@magentocommerce.com> */ -namespace Magento\Widget\Block\Adminhtml\Widget\Catalog\Category; - class Chooser extends \Magento\Catalog\Block\Adminhtml\Category\Widget\Chooser { /** @@ -18,7 +15,7 @@ class Chooser extends \Magento\Catalog\Block\Adminhtml\Category\Widget\Chooser * * @param \Magento\Framework\Data\Tree\Node|array $node * @param int $level - * @return string + * @return array */ protected function _getNodeJson($node, $level = 0) { diff --git a/app/code/Magento/Widget/Block/Adminhtml/Widget/Chooser.php b/app/code/Magento/Widget/Block/Adminhtml/Widget/Chooser.php index d813e9443732..45b3056eac68 100644 --- a/app/code/Magento/Widget/Block/Adminhtml/Widget/Chooser.php +++ b/app/code/Magento/Widget/Block/Adminhtml/Widget/Chooser.php @@ -11,6 +11,9 @@ */ namespace Magento\Widget\Block\Adminhtml\Widget; +/** + * Chooser widget block. + */ class Chooser extends \Magento\Backend\Block\Template { /** @@ -180,7 +183,7 @@ protected function _toHtml() <label class="widget-option-label" id="' . $chooserId . 'label">' . - ($this->getLabel() ? $this->getLabel() : __( + ($this->getLabel() ? $this->escapeHtml($this->getLabel()) : __( 'Not Selected' )) . '</label> diff --git a/app/code/Magento/Widget/Block/Adminhtml/Widget/Options.php b/app/code/Magento/Widget/Block/Adminhtml/Widget/Options.php index cc2af4996d56..32bae10c801c 100644 --- a/app/code/Magento/Widget/Block/Adminhtml/Widget/Options.php +++ b/app/code/Magento/Widget/Block/Adminhtml/Widget/Options.php @@ -91,7 +91,7 @@ public function getMainFieldset() if ($this->_getData('main_fieldset') instanceof \Magento\Framework\Data\Form\Element\Fieldset) { return $this->_getData('main_fieldset'); } - $mainFieldsetHtmlId = 'options_fieldset' . md5($this->getWidgetType()); + $mainFieldsetHtmlId = 'options_fieldset' . hash('sha256', $this->getWidgetType()); $this->setMainFieldsetHtmlId($mainFieldsetHtmlId); $fieldset = $this->getForm()->addFieldset( $mainFieldsetHtmlId, @@ -141,7 +141,6 @@ protected function _addField($parameter) { $form = $this->getForm(); $fieldset = $this->getMainFieldset(); - //$form->getElement('options_fieldset'); // prepare element data with values (either from request of from default values) $fieldName = $parameter->getKey(); @@ -166,9 +165,13 @@ protected function _addField($parameter) if (is_array($data['value'])) { foreach ($data['value'] as &$value) { - $value = html_entity_decode($value); + if (is_string($value)) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $value = html_entity_decode($value); + } } } else { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $data['value'] = html_entity_decode($data['value']); } diff --git a/app/code/Magento/Widget/Block/BlockInterface.php b/app/code/Magento/Widget/Block/BlockInterface.php index ddf810f433f3..4f795d949b8c 100644 --- a/app/code/Magento/Widget/Block/BlockInterface.php +++ b/app/code/Magento/Widget/Block/BlockInterface.php @@ -19,6 +19,7 @@ interface BlockInterface { /** * Add data to the widget. + * * Retains previous data in the widget. * * @param array $arr @@ -35,7 +36,7 @@ public function addData(array $arr); * * @param string|array $key * @param mixed $value - * @return \Magento\Framework\DataObject + * @return $this */ public function setData($key, $value = null); } diff --git a/app/code/Magento/Widget/Controller/Adminhtml/Widget/BuildWidget.php b/app/code/Magento/Widget/Controller/Adminhtml/Widget/BuildWidget.php index d9ef20aa90e4..0b9431f717e0 100644 --- a/app/code/Magento/Widget/Controller/Adminhtml/Widget/BuildWidget.php +++ b/app/code/Magento/Widget/Controller/Adminhtml/Widget/BuildWidget.php @@ -6,7 +6,9 @@ */ namespace Magento\Widget\Controller\Adminhtml\Widget; -class BuildWidget extends \Magento\Backend\App\Action +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; + +class BuildWidget extends \Magento\Backend\App\Action implements HttpPostActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Widget/Controller/Adminhtml/Widget/Index.php b/app/code/Magento/Widget/Controller/Adminhtml/Widget/Index.php index 61e44fe00db6..e7454faf925a 100644 --- a/app/code/Magento/Widget/Controller/Adminhtml/Widget/Index.php +++ b/app/code/Magento/Widget/Controller/Adminhtml/Widget/Index.php @@ -6,7 +6,9 @@ */ namespace Magento\Widget\Controller\Adminhtml\Widget; -class Index extends \Magento\Backend\App\Action +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; + +class Index extends \Magento\Backend\App\Action implements HttpPostActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Widget/Controller/Adminhtml/Widget/Instance/Index.php b/app/code/Magento/Widget/Controller/Adminhtml/Widget/Instance/Index.php index 40ade57f6a9e..50f3addaf7c5 100644 --- a/app/code/Magento/Widget/Controller/Adminhtml/Widget/Instance/Index.php +++ b/app/code/Magento/Widget/Controller/Adminhtml/Widget/Instance/Index.php @@ -6,7 +6,9 @@ */ namespace Magento\Widget\Controller\Adminhtml\Widget\Instance; -class Index extends \Magento\Widget\Controller\Adminhtml\Widget\Instance +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; + +class Index extends \Magento\Widget\Controller\Adminhtml\Widget\Instance implements HttpGetActionInterface { /** * Widget Instances Grid diff --git a/app/code/Magento/Widget/Controller/Adminhtml/Widget/Instance/Save.php b/app/code/Magento/Widget/Controller/Adminhtml/Widget/Instance/Save.php index b5d31dc10be3..dc137365498b 100644 --- a/app/code/Magento/Widget/Controller/Adminhtml/Widget/Instance/Save.php +++ b/app/code/Magento/Widget/Controller/Adminhtml/Widget/Instance/Save.php @@ -6,7 +6,9 @@ */ namespace Magento\Widget\Controller\Adminhtml\Widget\Instance; -class Save extends \Magento\Widget\Controller\Adminhtml\Widget\Instance +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; + +class Save extends \Magento\Widget\Controller\Adminhtml\Widget\Instance implements HttpPostActionInterface { /** * Save action diff --git a/app/code/Magento/Widget/Controller/Adminhtml/Widget/LoadOptions.php b/app/code/Magento/Widget/Controller/Adminhtml/Widget/LoadOptions.php index 054019079eba..03d9d1031138 100644 --- a/app/code/Magento/Widget/Controller/Adminhtml/Widget/LoadOptions.php +++ b/app/code/Magento/Widget/Controller/Adminhtml/Widget/LoadOptions.php @@ -6,9 +6,11 @@ */ namespace Magento\Widget\Controller\Adminhtml\Widget; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\App\ObjectManager; -class LoadOptions extends \Magento\Backend\App\Action +class LoadOptions extends \Magento\Backend\App\Action implements HttpGetActionInterface, HttpPostActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Widget/Model/ResourceModel/Layout/Update.php b/app/code/Magento/Widget/Model/ResourceModel/Layout/Update.php index d7e3bf94f548..6c0aef9e6718 100644 --- a/app/code/Magento/Widget/Model/ResourceModel/Layout/Update.php +++ b/app/code/Magento/Widget/Model/ResourceModel/Layout/Update.php @@ -51,7 +51,9 @@ protected function _construct() * @param string $handle * @param \Magento\Framework\View\Design\ThemeInterface $theme * @param \Magento\Framework\App\ScopeInterface $store + * * @return string + * @throws \Magento\Framework\Exception\LocalizedException */ public function fetchUpdatesByHandle( $handle, @@ -69,14 +71,16 @@ public function fetchUpdatesByHandle( $this->layoutUpdateCache[$cacheKey][$layout['handle']] .= $layout['xml']; } } - return isset($this->layoutUpdateCache[$cacheKey][$handle]) ? $this->layoutUpdateCache[$cacheKey][$handle] : ''; + return $this->layoutUpdateCache[$cacheKey][$handle] ?? ''; } /** * Get select to fetch updates by handle * * @param bool $loadAllUpdates + * * @return \Magento\Framework\DB\Select + * @throws \Magento\Framework\Exception\LocalizedException */ protected function _getFetchUpdatesByHandleSelect($loadAllUpdates = false) { diff --git a/app/code/Magento/Widget/Model/Template/Filter.php b/app/code/Magento/Widget/Model/Template/Filter.php index 7c3e8e467e03..c79334f67a9c 100644 --- a/app/code/Magento/Widget/Model/Template/Filter.php +++ b/app/code/Magento/Widget/Model/Template/Filter.php @@ -91,6 +91,10 @@ public function generateWidget($construction) $name = $params['name']; } + if (isset($this->_storeId) && !isset($params['store_id'])) { + $params['store_id'] = $this->_storeId; + } + // validate required parameter type or id if (!empty($params['type'])) { $type = $params['type']; diff --git a/app/code/Magento/Widget/Model/Widget.php b/app/code/Magento/Widget/Model/Widget.php index 085e6ec3b2d7..d07e84186b2c 100644 --- a/app/code/Magento/Widget/Model/Widget.php +++ b/app/code/Magento/Widget/Model/Widget.php @@ -84,6 +84,8 @@ public function __construct( } /** + * Get math random + * * @return \Magento\Framework\Math\Random * * @deprecated 100.1.0 @@ -149,8 +151,8 @@ public function getConfigAsObject($type) $widget = $this->getAsCanonicalArray($widget); // Save all nodes to object data - $object->setType($type); $object->setData($widget); + $object->setType($type); // Correct widget parameters and convert its data to objects $newParams = $this->prepareWidgetParameters($object); @@ -246,17 +248,13 @@ public function getWidgets($filters = []) $result = $widgets; // filter widgets by params - if (is_array($filters) && count($filters) > 0) { + if (is_array($filters) && !empty($filters)) { foreach ($widgets as $code => $widget) { - try { - foreach ($filters as $field => $value) { - if (!isset($widget[$field]) || (string)$widget[$field] != $value) { - throw new \Exception(); - } + foreach ($filters as $field => $value) { + if (!isset($widget[$field]) || (string)$widget[$field] != $value) { + unset($result[$code]); + break; } - } catch (\Exception $e) { - unset($result[$code]); - continue; } } } @@ -315,14 +313,12 @@ public function getWidgetDeclaration($type, $params = [], $asIs = true) } } if (isset($value)) { - $directive .= sprintf(' %s="%s"', $name, $this->escaper->escapeQuote($value)); + $directive .= sprintf(' %s="%s"', $name, $this->escaper->escapeHtmlAttr($value, false)); } } $directive .= $this->getWidgetPageVarName($params); - $directive .= sprintf(' type_name="%s"', $widget['name']); - $directive .= '}}'; if ($asIs) { @@ -339,6 +335,8 @@ public function getWidgetDeclaration($type, $params = [], $asIs = true) } /** + * Get widget page varname + * * @param array $params * @return string * @throws \Magento\Framework\Exception\LocalizedException diff --git a/app/code/Magento/Widget/Model/Widget/Config.php b/app/code/Magento/Widget/Model/Widget/Config.php index 4f81ef33f47f..00b055b35a69 100644 --- a/app/code/Magento/Widget/Model/Widget/Config.php +++ b/app/code/Magento/Widget/Model/Widget/Config.php @@ -120,6 +120,7 @@ public function getWidgetPlaceholderImageUrls() /** * Return url to error image + * * @return string */ public function getErrorImageUrl() @@ -129,6 +130,7 @@ public function getErrorImageUrl() /** * Return url to wysiwyg plugin + * * @return string */ public function getWysiwygJsPluginSrc() @@ -157,7 +159,7 @@ public function getWidgetWindowUrl($config) } } - if (count($skipped) > 0) { + if (!empty($skipped)) { $params['skip_widgets'] = $this->encodeWidgetsToQuery($skipped); } return $this->_backendUrl->getUrl('adminhtml/widget/index', $params); @@ -189,6 +191,8 @@ public function decodeWidgetsFromQuery($queryParam) } /** + * Get available widgets. + * * @param \Magento\Framework\DataObject $config Editor element config * @return array */ @@ -202,7 +206,7 @@ public function getAvailableWidgets($config) if (is_array($skipped) && in_array($widget['type'], $skipped)) { continue; } - $result[] = $widget['name']->getText(); + $result[$widget['type']] = $widget['name']->getText(); } } diff --git a/app/code/Magento/Widget/Model/Widget/Instance.php b/app/code/Magento/Widget/Model/Widget/Instance.php index afe7ef3766f7..4ca126e659e0 100644 --- a/app/code/Magento/Widget/Model/Widget/Instance.php +++ b/app/code/Magento/Widget/Model/Widget/Instance.php @@ -15,7 +15,7 @@ * @method string getTitle() * @method \Magento\Widget\Model\Widget\Instance setTitle(string $value) * @method \Magento\Widget\Model\Widget\Instance setStoreIds(string $value) - * @method \Magento\Widget\Model\Widget\Instance setWidgetParameters(string $value) + * @method \Magento\Widget\Model\Widget\Instance setWidgetParameters(string|array $value) * @method int getSortOrder() * @method \Magento\Widget\Model\Widget\Instance setSortOrder(int $value) * @method \Magento\Widget\Model\Widget\Instance setThemeId(int $value) @@ -340,6 +340,7 @@ public function setCode($code) /** * Setter + * * Prepare widget type * * @param string $type @@ -353,6 +354,7 @@ public function setType($type) /** * Getter + * * Prepare widget type * * @return string @@ -364,6 +366,7 @@ public function getType() /** * Getter. + * * If not set return default * * @return string @@ -379,6 +382,7 @@ public function getArea() /** * Getter + * * Explode to array if string setted * * @return array @@ -393,6 +397,7 @@ public function getStoreIds() /** * Getter + * * Unserialize if serialized string setted * * @return array diff --git a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml new file mode 100644 index 000000000000..969ab58b0487 --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml @@ -0,0 +1,81 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See 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="AdminCreateWidgetActionGroup"> + <arguments> + <argument name="widget"/> + </arguments> + <amOnPage url="{{AdminNewWidgetPage.url}}" stepKey="amOnAdminNewWidgetPage"/> + <selectOption selector="{{AdminNewWidgetSection.widgetType}}" userInput="{{widget.type}}" stepKey="setWidgetType"/> + <selectOption selector="{{AdminNewWidgetSection.widgetDesignTheme}}" userInput="{{widget.design_theme}}" stepKey="setWidgetDesignTheme"/> + <click selector="{{AdminNewWidgetSection.continue}}" stepKey="clickContinue"/> + <fillField selector="{{AdminNewWidgetSection.widgetTitle}}" userInput="{{widget.name}}" stepKey="fillTitle"/> + <selectOption selector="{{AdminNewWidgetSection.widgetStoreIds}}" userInput="{{widget.store_ids[0]}}" stepKey="setWidgetStoreIds"/> + <click selector="{{AdminNewWidgetSection.addLayoutUpdate}}" stepKey="clickAddLayoutUpdate"/> + <selectOption selector="{{AdminNewWidgetSection.selectDisplayOn}}" userInput="{{widget.display_on}}" stepKey="setDisplayOn"/> + <waitForAjaxLoad stepKey="waitForLoad"/> + <selectOption selector="{{AdminNewWidgetSection.selectContainer}}" userInput="{{widget.container}}" stepKey="setContainer"/> + <waitForAjaxLoad stepKey="waitForPageLoad"/> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <click selector="{{AdminNewWidgetSection.widgetOptions}}" stepKey="clickWidgetOptions"/> + </actionGroup> + + <!--Create Product List Widget--> + <actionGroup name="AdminCreateProductsListWidgetActionGroup" extends="AdminCreateWidgetActionGroup"> + <click selector="{{AdminNewWidgetSection.addNewCondition}}" stepKey="clickAddNewCondition"/> + <selectOption selector="{{AdminNewWidgetSection.selectCondition}}" userInput="{{widget.condition}}" stepKey="selectCondition"/> + <waitForElement selector="{{AdminNewWidgetSection.ruleParameter}}" stepKey="waitRuleParameter"/> + <click selector="{{AdminNewWidgetSection.ruleParameter}}" stepKey="clickRuleParameter"/> + <click selector="{{AdminNewWidgetSection.openChooser}}" stepKey="clickChooser"/> + <waitForAjaxLoad stepKey="waitForAjaxLoad"/> + <click selector="{{AdminNewWidgetSection.selectAll}}" stepKey="clickSelectAll"/> + <click selector="{{AdminNewWidgetSection.applyParameter}}" stepKey="clickApplyRuleParameter"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveWidget"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="The widget instance has been saved" stepKey="seeSuccess"/> + </actionGroup> + + <!--Create Dynamic Block Rotate Widget--> + <actionGroup name="AdminCreateDynamicBlocksRotatorWidgetActionGroup" extends="AdminCreateWidgetActionGroup"> + <selectOption selector="{{AdminNewWidgetSection.displayMode}}" userInput="{{widget.display_mode}}" stepKey="selectDisplayMode"/> + <selectOption selector="{{AdminNewWidgetSection.restrictTypes}}" userInput="{{widget.restrict_type}}" stepKey="selectRestrictType"/> + <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"/> + </arguments> + <amOnPage url="{{AdminWidgetsPage.url}}" stepKey="amOnAdmin"/> + <waitForPageLoad stepKey="waitWidgetsLoad"/> + <fillField selector="{{AdminWidgetsSection.widgetTitleSearch}}" userInput="{{widget.name}}" stepKey="fillTitle"/> + <click selector="{{AdminWidgetsSection.searchButton}}" stepKey="clickContinue"/> + <click selector="{{AdminWidgetsSection.searchResult}}" stepKey="clickSearchResult"/> + <waitForPageLoad stepKey="waitForResultLoad"/> + <click selector="{{AdminMainActionsSection.delete}}" stepKey="clickDelete"/> + <waitForAjaxLoad stepKey="waitForAjaxLoad"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmDelete"/> + <waitForPageLoad stepKey="waitForDeleteLoad"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="The widget instance has been deleted" stepKey="seeSuccess"/> + </actionGroup> + <actionGroup name="AdminCreateProductLinkWidgetActionGroup" extends="AdminCreateWidgetActionGroup"> + <arguments> + <argument name="product"/> + </arguments> + <selectOption selector="{{AdminNewWidgetSection.selectTemplate}}" userInput="{{widget.template}}" after="waitForPageLoad" stepKey="setTemplate"/> + <waitForAjaxLoad after="setTemplate" stepKey="waitForPageLoad2"/> + <click selector="{{AdminNewWidgetSection.selectProduct}}" after="clickWidgetOptions" stepKey="clickSelectProduct"/> + <fillField selector="{{AdminNewWidgetSelectProductPopupSection.filterBySku}}" userInput="{{product.sku}}" after="clickSelectProduct" stepKey="fillProductNameInFilter"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" after="fillProductNameInFilter" stepKey="applyFilter"/> + <click selector="{{AdminNewWidgetSelectProductPopupSection.firstRow}}" after="applyFilter" stepKey="selectProduct"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveWidget"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="The widget instance has been saved" stepKey="seeSuccess"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminWidgetActionGroup.xml b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminWidgetActionGroup.xml new file mode 100644 index 000000000000..c303b7cc8e90 --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminWidgetActionGroup.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCreateWidgetWithBlockActionGroup"> + <arguments> + <argument name="widget"/> + <argument name="block" type="string"/> + </arguments> + <amOnPage url="{{AdminNewWidgetPage.url}}" stepKey="createWidgetPage"/> + <selectOption selector="{{AdminNewWidgetSection.widgetType}}" userInput="{{widget.type}}" stepKey="selectWidgetType"/> + <selectOption selector="{{AdminNewWidgetSection.widgetDesignTheme}}" userInput="{{widget.designTheme}}" stepKey="selectWidgetDesignTheme"/> + <click selector="{{AdminNewWidgetSection.continue}}" stepKey="continue"/> + <waitForElement selector="{{AdminNewWidgetSection.widgetTitle}}" time="30" stepKey="waitForElement"/> + <fillField selector="{{AdminNewWidgetSection.widgetTitle}}" userInput="{{widget.name}}" stepKey="fillWidgetTitle"/> + <selectOption selector="{{AdminNewWidgetSection.widgetStoreIds}}" userInput="{{widget.store_id}}" stepKey="selectWidgetStoreView"/> + <click selector="{{AdminNewWidgetSection.addLayoutUpdate}}" stepKey="clickAddLayoutUpdate"/> + <waitForPageLoad stepKey="waitForLoad1"/> + <scrollTo selector="{{AdminNewWidgetSection.selectDisplayOn}}" stepKey="scrollToElement" /> + <selectOption selector="{{AdminNewWidgetSection.selectDisplayOn}}" userInput="{{widget.display}}" stepKey="selectWidgetDisplayOn"/> + <waitForElement selector="{{AdminNewWidgetSection.selectContainer}}" time="30" stepKey="waitForContainer"/> + <selectOption selector="{{AdminNewWidgetSection.selectContainer}}" userInput="{{widget.container}}" stepKey="selectWidgetContainer"/> + <scrollToTopOfPage stepKey="scrollToAddresses"/> + <waitForAjaxLoad stepKey="waitForAjaxLoad1"/> + <click selector="{{AdminNewWidgetSection.widgetOptions}}" stepKey="goToWidgetOptions"/> + <waitForElement selector="{{AdminNewWidgetSection.widgetSelectBlock}}" time="60" stepKey="waitForSelectBlock"/> + <click selector="{{AdminNewWidgetSection.widgetSelectBlock}}" stepKey="openSelectBlock"/> + <waitForPageLoad stepKey="waitForLoadBlocks"/> + <selectOption selector="{{AdminNewWidgetSection.blockStatus}}" userInput="Disable" stepKey="chooseStatus"/> + <fillField selector="{{AdminNewWidgetSection.selectBlockTitle}}" userInput="{{block}}" stepKey="fillBlockTitle"/> + <click selector="{{AdminNewWidgetSection.searchBlock}}" stepKey="searchBlock"/> + <waitForAjaxLoad stepKey="waitForAjaxLoad"/> + <click selector="{{AdminNewWidgetSection.searchedBlock}}" stepKey="clickSearchedBlock"/> + <waitForPageLoad stepKey="wait"/> + <click selector="{{AdminNewWidgetSection.saveWidget}}" stepKey="saveWidget"/> + <waitForPageLoad stepKey="waitForSaving"/> + <see userInput="The widget instance has been saved." stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Widget/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/Widget/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 000000000000..8fa652b8f73e --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminMenuContentElementsWidgets"> + <data key="pageTitle">Widgets</data> + <data key="title">Widgets</data> + <data key="dataUiId">magento-widget-cms-widget-instance</data> + </entity> +</entities> diff --git a/app/code/Magento/Widget/Test/Mftf/Data/WidgetData.xml b/app/code/Magento/Widget/Test/Mftf/Data/WidgetData.xml new file mode 100644 index 000000000000..4c6e98aafd76 --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/Data/WidgetData.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="WidgetWithBlock" type="widget"> + <data key="type">CMS Static Block</data> + <data key="designTheme">Magento Luma</data> + <data key="name" unique="suffix">testName</data> + <data key="store_id">All Store Views</data> + <data key="display">All Pages</data> + <data key="container">Page Top</data> + </entity> +</entities> diff --git a/app/code/Magento/Widget/Test/Mftf/Data/WidgetsData.xml b/app/code/Magento/Widget/Test/Mftf/Data/WidgetsData.xml new file mode 100644 index 000000000000..27222298408d --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/Data/WidgetsData.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="ProductsListWidget" type="widget"> + <data key="type">Catalog Products List</data> + <data key="design_theme">Magento Luma</data> + <data key="name" unique="suffix">TestWidget</data> + <array key="store_ids"> + <item>All Store Views</item> + </array> + <data key="condition">SKU</data> + <data key="display_on">All Pages</data> + <data key="container">Main Content Area</data> + </entity> + <entity name="DynamicBlocksRotatorWidget" type="widget"> + <data key="type">Dynamic Blocks Rotator</data> + <data key="design_theme">Magento Luma</data> + <data key="name" unique="suffix">TestBannerWidget</data> + <array key="store_ids"> + <item>All Store Views</item> + </array> + <data key="condition">SKU</data> + <data key="display_on">All Pages</data> + <data key="container">Main Content Area</data> + <data key="display_mode">Cart Price Rule Related</data> + <data key="restrict_type">Header</data> + </entity> +</entities> diff --git a/app/code/Magento/Widget/Test/Mftf/Page/AdminNewWidgetPage.xml b/app/code/Magento/Widget/Test/Mftf/Page/AdminNewWidgetPage.xml new file mode 100644 index 000000000000..d495a36f68d0 --- /dev/null +++ b/app/code/Magento/Widget/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="AdminNewWidgetSection"/> + </page> +</pages> diff --git a/app/code/Magento/Widget/Test/Mftf/Page/AdminWidgetsPage.xml b/app/code/Magento/Widget/Test/Mftf/Page/AdminWidgetsPage.xml new file mode 100644 index 000000000000..46209f9e5f01 --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/Page/AdminWidgetsPage.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="AdminWidgetsPage" url="admin/widget_instance/" area="admin" module="Magento_Widget"> + <section name="AdminWidgetsSection"/> + </page> +</pages> diff --git a/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml b/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml new file mode 100644 index 000000000000..eebd6c10b508 --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminNewWidgetSection"> + <element name="widgetType" type="select" selector="#code"/> + <element name="widgetDesignTheme" type="select" selector="#theme_id"/> + <element name="continue" type="button" selector="#continue_button"/> + <element name="widgetTitle" type="input" selector="#title"/> + <element name="widgetStoreIds" type="select" selector="#store_ids"/> + <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"/> + <element name="ruleParameter" type="select" selector="#conditions__1__children>li:nth-child(1)>span:nth-child(4)>a"/> + <element name="setRuleParameter" type="input" selector="#conditions__1--1__value"/> + <element name="applyParameter" type="button" selector=".rule-param-apply"/> + <element name="openChooser" type="button" selector=".rule-chooser-trigger"/> + <element name="selectAll" type="checkbox" selector=".admin__control-checkbox"/> + <element name="widgetSelectBlock" type="button" selector="//button[@class='action-default scalable btn-chooser']"/> + <element name="selectBlockTitle" type="input" selector="//input[@name='chooser_title']"/> + <element name="searchBlock" type="button" selector="//div[@class='admin__filter-actions']/button[@title='Search']"/> + <element name="blockStatus" type="select" selector="//select[@name='chooser_is_active']"/> + <element name="searchedBlock" type="button" selector="//*[@class='magento-message']//tbody/tr/td[1]"/> + <element name="saveWidget" type="select" selector="#save"/> + <element name="displayMode" type="select" selector="select[id*='display_mode']"/> + <element name="restrictTypes" type="select" selector="select[id*='types']"/> + <element name="saveAndContinue" type="button" selector="#save_and_edit_button" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Widget/Test/Mftf/Section/AdminWidgetsSection.xml b/app/code/Magento/Widget/Test/Mftf/Section/AdminWidgetsSection.xml new file mode 100644 index 000000000000..f3282362d9aa --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/Section/AdminWidgetsSection.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="AdminWidgetsSection"> + <element name="widgetTitleSearch" type="input" selector="#widgetInstanceGrid_filter_title"/> + <element name="searchButton" type="button" selector=".action-default.scalable.action-secondary"/> + <element name="searchResult" type="text" selector="#widgetInstanceGrid_table>tbody>tr:nth-child(1)"/> + </section> +</sections> diff --git a/app/code/Magento/Widget/Test/Mftf/Section/StorefrontWidgetsSection.xml b/app/code/Magento/Widget/Test/Mftf/Section/StorefrontWidgetsSection.xml new file mode 100644 index 000000000000..0e2f6cec73a9 --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/Section/StorefrontWidgetsSection.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="StorefrontWidgetsSection"> + <element name="widgetProductsGrid" type="block" selector=".block.widget.block-products-list.grid"/> + <element name="widgetProductName" type="text" selector=".product-item-name"/> + </section> +</sections> diff --git a/app/code/Magento/Widget/Test/Mftf/Test/AdminContentWidgetsNavigateMenuTest.xml b/app/code/Magento/Widget/Test/Mftf/Test/AdminContentWidgetsNavigateMenuTest.xml new file mode 100644 index 000000000000..f5af024ec1d5 --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/Test/AdminContentWidgetsNavigateMenuTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminContentWidgetsNavigateMenuTest"> + <annotations> + <features value="Widget"/> + <stories value="Menu Navigation"/> + <title value="Admin content widgets navigate menu test"/> + <description value="Admin should be able to navigate to Content > Widgets"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14147"/> + <group value="menu"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToContentWidgetsPage"> + <argument name="menuUiId" value="{{AdminMenuContent.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuContentElementsWidgets.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuContentElementsWidgets.pageTitle}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Widget/Test/Mftf/Test/ProductsListWidgetTest.xml b/app/code/Magento/Widget/Test/Mftf/Test/ProductsListWidgetTest.xml new file mode 100644 index 000000000000..4407991ff5a9 --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/Test/ProductsListWidgetTest.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="ProductsListWidgetTest"> + <annotations> + <features value="Widget"/> + <stories value="Products list widget"/> + <title value="Admin should be able to set Products List Widget"/> + <description value="Admin should be able to set Products List Widget"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-97041"/> + <group value="Widget"/> + <group value="WYSIWYGDisabled"/> + </annotations> + <before> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <conditionalClick selector="{{CmsPagesPageActionsSection.clearAllButton}}" dependentSelector="{{CmsPagesPageActionsSection.activeFilters}}" stepKey="clickToResetFilter" visible="true"/> + <actionGroup ref="logout" stepKey="logout"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + </after> + <!-- Create a CMS page containing the Products List widget --> + <amOnPage url="{{CmsPagesPage.url}}" stepKey="amOnCmsList"/> + <waitForPageLoad stepKey="waitForCmsList"/> + <click selector="{{CmsPagesPageActionsSection.addNewPageButton}}" stepKey="clickAddNewPageButton"/> + <fillField selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" userInput="{{_newDefaultCmsPage.title}}" stepKey="fillPageTitle"/> + <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="expandContentSection"/> + <waitForPageLoad stepKey="waitForContentSection"/> + <click selector="{{CmsWYSIWYGSection.InsertWidgetBtn}}" stepKey="clickInsertWidgetButton"/> + <waitForPageLoad stepKey="waitForSlideOut"/> + <selectOption selector="{{WidgetSection.WidgetType}}" userInput="Catalog Products List" stepKey="selectWidgetType"/> + <waitForPageLoad stepKey="waitForWidgetOptions"/> + <click selector="{{AdminNewWidgetSection.addNewCondition}}" stepKey="clickAddNewCondition"/> + <selectOption selector="{{AdminNewWidgetSection.selectCondition}}" userInput="Magento\CatalogWidget\Model\Rule\Condition\Product|category_ids" stepKey="selectCondition"/> + <waitForElement selector="{{AdminNewWidgetSection.ruleParameter}}" stepKey="waitRuleParameter"/> + <click selector="{{AdminNewWidgetSection.ruleParameter}}" stepKey="clickRuleParameter"/> + <click selector="{{AdminNewWidgetSection.openChooser}}" stepKey="clickChooser"/> + <waitForAjaxLoad stepKey="waitForAjaxLoad"/> + <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createCategory.name$$)}}" stepKey="clickCategoryToEditInitial"/> + <click selector="{{AdminNewWidgetSection.applyParameter}}" stepKey="clickApplyRuleParameter"/> + <click selector="{{WidgetSection.InsertWidget}}" stepKey="clickInsertWidget"/> + <click selector="{{CmsNewPagePageActionsSection.expandSplitButton}}" stepKey="expandSplitBtn"/> + <click selector="{{CmsNewPagePageActionsSection.saveAndClose}}" stepKey="clickSaveAndClose"/> + <waitForPageLoad stepKey="waitForCmsList2"/> + <see userInput="You saved the page." stepKey="seeSuccessMessage"/> + <!-- Verify CMS page on storefront --> + <waitForElementVisible selector="{{CmsPagesPageActionsSection.select(_newDefaultCmsPage.title)}}" stepKey="waitForCMSPageListItem" /> + <click selector="{{CmsPagesPageActionsSection.select(_newDefaultCmsPage.title)}}" stepKey="clickSelect" /> + <waitForElementVisible selector="{{CmsPagesPageActionsSection.edit(_newDefaultCmsPage.title)}}" stepKey="waitForEditLink" /> + <click selector="{{CmsPagesPageActionsSection.preview(_newDefaultCmsPage.title)}}" stepKey="clickEdit" /> + <waitForPageLoad stepKey="waitForCMSPage"/> + <seeInTitle userInput="{{_newDefaultCmsPage.title}}" stepKey="seePageTitle"/> + <see userInput="{{_newDefaultCmsPage.title}}" stepKey="seeProduct"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Widget/Test/Unit/Model/WidgetTest.php b/app/code/Magento/Widget/Test/Unit/Model/WidgetTest.php index b85a458ed412..850a3fbe8321 100644 --- a/app/code/Magento/Widget/Test/Unit/Model/WidgetTest.php +++ b/app/code/Magento/Widget/Test/Unit/Model/WidgetTest.php @@ -32,6 +32,9 @@ class WidgetTest extends \PHPUnit\Framework\TestCase */ private $conditionsHelper; + /** + * @inheritdoc + */ protected function setUp() { $this->dataStorageMock = $this->getMockBuilder(\Magento\Widget\Model\Config\Data::class) @@ -55,6 +58,9 @@ protected function setUp() ); } + /** + * Unit test for getWidget + */ public function testGetWidgets() { $expected = ['val1', 'val2']; @@ -65,6 +71,9 @@ public function testGetWidgets() $this->assertEquals($expected, $result); } + /** + * Unit test for getWidgetsWithFilter + */ public function testGetWidgetsWithFilter() { $configFile = __DIR__ . '/_files/mappedConfigArrayAll.php'; @@ -78,6 +87,9 @@ public function testGetWidgetsWithFilter() $this->assertEquals($expected, $result); } + /** + * Unit test for getWidgetsWithUnknownFilter + */ public function testGetWidgetsWithUnknownFilter() { $configFile = __DIR__ . '/_files/mappedConfigArrayAll.php'; @@ -90,6 +102,9 @@ public function testGetWidgetsWithUnknownFilter() $this->assertEquals($expected, $result); } + /** + * Unit test for getWidgetByClassType + */ public function testGetWidgetByClassType() { $widgetOne = ['@' => ['type' => 'type1']]; @@ -101,6 +116,9 @@ public function testGetWidgetByClassType() $this->assertNull($this->widget->getWidgetByClassType('type2')); } + /** + * Unit test for getConfigAsObject + */ public function testGetConfigAsObject() { $configFile = __DIR__ . '/_files/mappedConfigArrayAll.php'; @@ -135,6 +153,9 @@ public function testGetConfigAsObject() $this->assertSame($supportedContainersExpected, $resultObject->getSupportedContainers()); } + /** + * Unit test for getConfigAsObjectWidgetNoFound + */ public function testGetConfigAsObjectWidgetNoFound() { $this->dataStorageMock->expects($this->once()) @@ -146,6 +167,9 @@ public function testGetConfigAsObjectWidgetNoFound() $this->assertSame([], $resultObject->getData()); } + /** + * Unit test for getWidgetDeclaration + */ public function testGetWidgetDeclaration() { $mathRandomMock = $this->createPartialMock(\Magento\Framework\Math\Random::class, ['getRandomString']); @@ -175,7 +199,7 @@ public function testGetWidgetDeclaration() $this->conditionsHelper->expects($this->once())->method('encode')->with($conditions) ->willReturn('encoded-conditions-string'); $this->escaperMock->expects($this->atLeastOnce()) - ->method('escapeQuote') + ->method('escapeHtmlAttr') ->willReturnMap([ ['my "widget"', false, 'my "widget"'], ['1', false, '1'], @@ -200,9 +224,11 @@ public function testGetWidgetDeclaration() $this->assertContains('title="my "widget""', $result); $this->assertContains('conditions_encoded="encoded-conditions-string"', $result); $this->assertContains('page_var_name="pasdf"', $result); - $this->assertContains('type_name=""}}', $result); } + /** + * Unit test for getWidgetDeclarationWithZeroValueParam + */ public function testGetWidgetDeclarationWithZeroValueParam() { $mathRandomMock = $this->createPartialMock(\Magento\Framework\Math\Random::class, ['getRandomString']); @@ -248,7 +274,6 @@ public function testGetWidgetDeclarationWithZeroValueParam() ); $this->assertContains('{{widget type="Magento\CatalogWidget\Block\Product\ProductsList"', $result); $this->assertContains('page_var_name="pasdf"', $result); - $this->assertContains('type_name=""}}', $result); $this->assertContains('products_count=""', $result); } } diff --git a/app/code/Magento/Widget/etc/db_schema.xml b/app/code/Magento/Widget/etc/db_schema.xml index 149bb27fcdb2..a82e6aae2029 100644 --- a/app/code/Magento/Widget/etc/db_schema.xml +++ b/app/code/Magento/Widget/etc/db_schema.xml @@ -14,10 +14,10 @@ comment="Widget code for template directive"/> <column xsi:type="varchar" name="widget_type" nullable="true" length="255" comment="Widget Type"/> <column xsi:type="text" name="parameters" nullable="true" comment="Parameters"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="widget_id"/> </constraint> - <index name="WIDGET_WIDGET_CODE" indexType="btree"> + <index referenceId="WIDGET_WIDGET_CODE" indexType="btree"> <column name="widget_code"/> </index> </table> @@ -32,10 +32,10 @@ <column xsi:type="text" name="widget_parameters" nullable="true" comment="Widget parameters"/> <column xsi:type="smallint" name="sort_order" padding="5" unsigned="true" nullable="false" identity="false" default="0" comment="Sort order"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="instance_id"/> </constraint> - <constraint xsi:type="foreign" name="WIDGET_INSTANCE_THEME_ID_THEME_THEME_ID" table="widget_instance" + <constraint xsi:type="foreign" referenceId="WIDGET_INSTANCE_THEME_ID_THEME_THEME_ID" table="widget_instance" column="theme_id" referenceTable="theme" referenceColumn="theme_id" onDelete="CASCADE"/> </table> <table name="widget_instance_page" resource="default" engine="innodb" comment="Instance of Widget on Page"> @@ -49,13 +49,13 @@ <column xsi:type="varchar" name="page_for" nullable="true" length="25" comment="For instance entities"/> <column xsi:type="text" name="entities" nullable="true" comment="Catalog entities (comma separated)"/> <column xsi:type="varchar" name="page_template" nullable="true" length="255" comment="Path to widget template"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="page_id"/> </constraint> - <constraint xsi:type="foreign" name="WIDGET_INSTANCE_PAGE_INSTANCE_ID_WIDGET_INSTANCE_INSTANCE_ID" + <constraint xsi:type="foreign" referenceId="WIDGET_INSTANCE_PAGE_INSTANCE_ID_WIDGET_INSTANCE_INSTANCE_ID" table="widget_instance_page" column="instance_id" referenceTable="widget_instance" referenceColumn="instance_id" onDelete="CASCADE"/> - <index name="WIDGET_INSTANCE_PAGE_INSTANCE_ID" indexType="btree"> + <index referenceId="WIDGET_INSTANCE_PAGE_INSTANCE_ID" indexType="btree"> <column name="instance_id"/> </index> </table> @@ -64,17 +64,17 @@ comment="Page Id"/> <column xsi:type="int" name="layout_update_id" padding="10" unsigned="true" nullable="false" identity="false" default="0" comment="Layout Update Id"/> - <constraint xsi:type="foreign" name="WIDGET_INSTANCE_PAGE_LAYOUT_PAGE_ID_WIDGET_INSTANCE_PAGE_PAGE_ID" + <constraint xsi:type="foreign" referenceId="WIDGET_INSTANCE_PAGE_LAYOUT_PAGE_ID_WIDGET_INSTANCE_PAGE_PAGE_ID" table="widget_instance_page_layout" column="page_id" referenceTable="widget_instance_page" referenceColumn="page_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="WIDGET_INSTANCE_PAGE_LYT_LYT_UPDATE_ID_LYT_UPDATE_LYT_UPDATE_ID" + <constraint xsi:type="foreign" referenceId="WIDGET_INSTANCE_PAGE_LYT_LYT_UPDATE_ID_LYT_UPDATE_LYT_UPDATE_ID" table="widget_instance_page_layout" column="layout_update_id" referenceTable="layout_update" referenceColumn="layout_update_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="WIDGET_INSTANCE_PAGE_LAYOUT_LAYOUT_UPDATE_ID_PAGE_ID"> + <constraint xsi:type="unique" referenceId="WIDGET_INSTANCE_PAGE_LAYOUT_LAYOUT_UPDATE_ID_PAGE_ID"> <column name="layout_update_id"/> <column name="page_id"/> </constraint> - <index name="WIDGET_INSTANCE_PAGE_LAYOUT_PAGE_ID" indexType="btree"> + <index referenceId="WIDGET_INSTANCE_PAGE_LAYOUT_PAGE_ID" indexType="btree"> <column name="page_id"/> </index> </table> @@ -87,10 +87,10 @@ default="0" comment="Sort Order"/> <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="true" default="0" comment="Last Update Timestamp"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="layout_update_id"/> </constraint> - <index name="LAYOUT_UPDATE_HANDLE" indexType="btree"> + <index referenceId="LAYOUT_UPDATE_HANDLE" indexType="btree"> <column name="handle"/> </index> </table> @@ -105,20 +105,20 @@ default="0" comment="Layout Update Id"/> <column xsi:type="boolean" name="is_temporary" nullable="false" default="false" comment="Defines whether Layout Update is Temporary"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="layout_link_id"/> </constraint> - <constraint xsi:type="foreign" name="LAYOUT_LINK_LAYOUT_UPDATE_ID_LAYOUT_UPDATE_LAYOUT_UPDATE_ID" + <constraint xsi:type="foreign" referenceId="LAYOUT_LINK_LAYOUT_UPDATE_ID_LAYOUT_UPDATE_LAYOUT_UPDATE_ID" table="layout_link" column="layout_update_id" referenceTable="layout_update" referenceColumn="layout_update_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="LAYOUT_LINK_STORE_ID_STORE_STORE_ID" table="layout_link" column="store_id" + <constraint xsi:type="foreign" referenceId="LAYOUT_LINK_STORE_ID_STORE_STORE_ID" table="layout_link" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="LAYOUT_LINK_THEME_ID_THEME_THEME_ID" table="layout_link" column="theme_id" + <constraint xsi:type="foreign" referenceId="LAYOUT_LINK_THEME_ID_THEME_THEME_ID" table="layout_link" column="theme_id" referenceTable="theme" referenceColumn="theme_id" onDelete="CASCADE"/> - <index name="LAYOUT_LINK_LAYOUT_UPDATE_ID" indexType="btree"> + <index referenceId="LAYOUT_LINK_LAYOUT_UPDATE_ID" indexType="btree"> <column name="layout_update_id"/> </index> - <index name="LAYOUT_LINK_STORE_ID_THEME_ID_LAYOUT_UPDATE_ID_IS_TEMPORARY" indexType="btree"> + <index referenceId="LAYOUT_LINK_STORE_ID_THEME_ID_LAYOUT_UPDATE_ID_IS_TEMPORARY" indexType="btree"> <column name="store_id"/> <column name="theme_id"/> <column name="layout_update_id"/> diff --git a/app/code/Magento/Widget/view/adminhtml/layout/adminhtml_widget_instance_block.xml b/app/code/Magento/Widget/view/adminhtml/layout/adminhtml_widget_instance_block.xml index 934b0ca1a85b..8ca3fab413b2 100644 --- a/app/code/Magento/Widget/view/adminhtml/layout/adminhtml_widget_instance_block.xml +++ b/app/code/Magento/Widget/view/adminhtml/layout/adminhtml_widget_instance_block.xml @@ -13,7 +13,7 @@ <argument name="id" xsi:type="string">widgetInstanceGrid</argument> <argument name="default_sort" xsi:type="string">instance_id</argument> <argument name="default_dir" xsi:type="string">ASC</argument> - <argument name="dataSource" xsi:type="object">Magento\Widget\Model\ResourceModel\Widget\Instance\Collection</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Widget\Model\ResourceModel\Widget\Instance\Collection</argument> </arguments> <block class="Magento\Backend\Block\Widget\Grid\ColumnSet" name="adminhtml.widget.instance.grid.columnSet" as="grid.columnSet"> <arguments> diff --git a/app/code/Magento/Wishlist/Block/AbstractBlock.php b/app/code/Magento/Wishlist/Block/AbstractBlock.php index 8b4a8df1bf99..981a0da1d241 100644 --- a/app/code/Magento/Wishlist/Block/AbstractBlock.php +++ b/app/code/Magento/Wishlist/Block/AbstractBlock.php @@ -228,12 +228,24 @@ public function hasDescription($item) } /** - * Retrieve formated Date + * Retrieve formatted Date * * @param string $date + * @deprecated * @return string */ public function getFormatedDate($date) + { + return $this->getFormattedDate($date); + } + + /** + * Retrieve formatted Date + * + * @param string $date + * @return string + */ + public function getFormattedDate($date) { return $this->formatDate($date, \IntlDateFormatter::MEDIUM); } 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 823849ed4104..eba1f7da7274 100644 --- a/app/code/Magento/Wishlist/Block/Cart/Item/Renderer/Actions/MoveToWishlist.php +++ b/app/code/Magento/Wishlist/Block/Cart/Item/Renderer/Actions/MoveToWishlist.php @@ -10,6 +10,8 @@ use Magento\Wishlist\Helper\Data; /** + * Class MoveToWishlist + * * @api * @since 100.0.2 */ diff --git a/app/code/Magento/Wishlist/Block/Customer/Sharing.php b/app/code/Magento/Wishlist/Block/Customer/Sharing.php index 6fbf5a23dca2..40fd00d6143a 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Sharing.php +++ b/app/code/Magento/Wishlist/Block/Customer/Sharing.php @@ -11,9 +11,14 @@ */ namespace Magento\Wishlist\Block\Customer; +use Magento\Captcha\Block\Captcha; + /** + * Class Sharing + * * @api * @since 100.0.2 + * @package Magento\Wishlist\Block\Customer */ class Sharing extends \Magento\Framework\View\Element\Template { @@ -60,6 +65,20 @@ public function __construct( */ protected function _prepareLayout() { + if (!$this->getChildBlock('captcha')) { + $this->addChild( + 'captcha', + Captcha::class, + [ + 'cacheable' => false, + 'after' => '-', + 'form_id' => 'share_wishlist_form', + 'image_width' => 230, + 'image_height' => 230 + ] + ); + } + $this->pageConfig->getTitle()->set(__('Wish List Sharing')); } diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist.php index 2b1b6d44b425..d02f2229401c 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist.php @@ -5,13 +5,13 @@ */ /** - * Wishlist block customer items - * * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Wishlist\Block\Customer; /** + * Wishlist block customer items. + * * @api * @since 100.0.2 */ @@ -29,6 +29,11 @@ class Wishlist extends \Magento\Wishlist\Block\AbstractBlock */ protected $_helperPool; + /** + * @var \Magento\Wishlist\Model\ResourceModel\Item\Collection + */ + protected $_collection; + /** * @var \Magento\Customer\Helper\Session\CurrentCustomer */ @@ -78,14 +83,64 @@ protected function _prepareCollection($collection) } /** - * Preparing global layout + * Paginate Wishlist Product Items collection * * @return void + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) + */ + private function paginateCollection() + { + $page = $this->getRequest()->getParam("p", 1); + $limit = $this->getRequest()->getParam("limit", 10); + $this->_collection + ->setPageSize($limit) + ->setCurPage($page); + } + + /** + * Retrieve Wishlist Product Items collection + * + * @return \Magento\Wishlist\Model\ResourceModel\Item\Collection + */ + public function getWishlistItems() + { + if ($this->_collection === null) { + $this->_collection = $this->_createWishlistItemCollection(); + $this->_prepareCollection($this->_collection); + $this->paginateCollection(); + } + return $this->_collection; + } + + /** + * Preparing global layout + * + * @return $this */ protected function _prepareLayout() { parent::_prepareLayout(); $this->pageConfig->getTitle()->set(__('My Wish List')); + $this->getChildBlock('wishlist_item_pager') + ->setUseContainer( + true + )->setShowAmounts( + true + )->setFrameLength( + $this->_scopeConfig->getValue( + 'design/pagination/pagination_frame', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ) + )->setJump( + $this->_scopeConfig->getValue( + 'design/pagination/pagination_frame_skip', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ) + )->setLimit( + $this->getLimit() + ) + ->setCollection($this->getWishlistItems()); + return $this; } /** @@ -198,6 +253,7 @@ public function getAddToCartQty(\Magento\Wishlist\Model\Item $item) /** * Get add all to cart params for POST request + * * @return string */ public function getAddAllToCartParams() @@ -209,7 +265,7 @@ public function getAddAllToCartParams() } /** - * @return string + * @inheritdoc */ protected function _toHtml() { diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Actions.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Actions.php index cafb6a529148..40882ae00dae 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Actions.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Actions.php @@ -5,14 +5,15 @@ */ /** - * Wishlist for item column in customer wishlist - * * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Wishlist\Block\Customer\Wishlist\Item\Column; /** + * Model for item column in customer wishlist. + * * @api + * @deprecated * @since 100.0.2 */ class Actions extends \Magento\Wishlist\Block\Customer\Wishlist\Item\Column diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Cart.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Cart.php index b043a8d4b684..5625dd5aabb7 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Cart.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Cart.php @@ -6,6 +6,12 @@ namespace Magento\Wishlist\Block\Customer\Wishlist\Item\Column; +use Magento\Catalog\Block\Product\View; +use Magento\Catalog\Controller\Adminhtml\Product\Initialization\StockDataFilter; +use Magento\Catalog\Model\Product\Image\UrlBuilder; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\ConfigInterface; + /** * Wishlist block customer item cart column * @@ -14,6 +20,31 @@ */ class Cart extends \Magento\Wishlist\Block\Customer\Wishlist\Item\Column { + /** + * @var View + */ + private $productView; + + /** + * @param \Magento\Catalog\Block\Product\Context $context + * @param \Magento\Framework\App\Http\Context $httpContext + * @param array $data + * @param ConfigInterface|null $config + * @param UrlBuilder|null $urlBuilder + * @param View|null $productView + */ + public function __construct( + \Magento\Catalog\Block\Product\Context $context, + \Magento\Framework\App\Http\Context $httpContext, + array $data = [], + ?ConfigInterface $config = null, + ?UrlBuilder $urlBuilder = null, + ?View $productView = null + ) { + $this->productView = $productView ?: ObjectManager::getInstance()->get(View::class); + parent::__construct($context, $httpContext, $data, $config, $urlBuilder); + } + /** * Returns qty to show visually to user * @@ -23,7 +54,9 @@ class Cart extends \Magento\Wishlist\Block\Customer\Wishlist\Item\Column public function getAddToCartQty(\Magento\Wishlist\Model\Item $item) { $qty = $item->getQty(); - return $qty ? $qty : 1; + $qty = $qty < $this->productView->getProductDefaultQty($this->getProductItem()) + ? $this->productView->getProductDefaultQty($this->getProductItem()) : $qty; + return $qty ?: 1; } /** diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Comment.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Comment.php index 2d75956858a0..53f67626e956 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Comment.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Comment.php @@ -5,14 +5,15 @@ */ /** - * Wishlist block customer item cart column - * * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Wishlist\Block\Customer\Wishlist\Item\Column; /** + * Wishlist block customer item cart column. + * * @api + * @deprecated * @since 100.0.2 */ class Comment extends \Magento\Wishlist\Block\Customer\Wishlist\Item\Column diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Edit.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Edit.php index 53ca78c63524..c4c786961694 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Edit.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Edit.php @@ -5,14 +5,15 @@ */ /** - * Edit item in customer wishlist table - * * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Wishlist\Block\Customer\Wishlist\Item\Column; /** + * Edit item in customer wishlist table. + * * @api + * @deprecated * @since 100.0.2 */ class Edit extends \Magento\Wishlist\Block\Customer\Wishlist\Item\Column diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Info.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Info.php index 33fb0f7325cd..b7eaf53fc23b 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Info.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Info.php @@ -5,14 +5,15 @@ */ /** - * Wishlist block customer item cart column - * * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Wishlist\Block\Customer\Wishlist\Item\Column; /** + * Wishlist block customer item cart column. + * * @api + * @deprecated * @since 100.0.2 */ class Info extends \Magento\Wishlist\Block\Customer\Wishlist\Item\Column diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Remove.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Remove.php index 57703b9300db..09f5014edead 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Remove.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Remove.php @@ -5,14 +5,15 @@ */ /** - * Delete item column in customer wishlist table - * * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Wishlist\Block\Customer\Wishlist\Item\Column; /** + * Delete item column in customer wishlist table + * * @api + * @deprecated * @since 100.0.2 */ class Remove extends \Magento\Wishlist\Block\Customer\Wishlist\Item\Column diff --git a/app/code/Magento/Wishlist/Controller/Index/Allcart.php b/app/code/Magento/Wishlist/Controller/Index/Allcart.php index 8463a00c866c..958e0f49ca1b 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 0e826d83a52f..da37609d688e 100644 --- a/app/code/Magento/Wishlist/Controller/Index/Cart.php +++ b/app/code/Magento/Wishlist/Controller/Index/Cart.php @@ -3,16 +3,19 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Wishlist\Controller\Index; -use Magento\Framework\App\Action; use Magento\Catalog\Model\Product\Exception as ProductException; +use Magento\Framework\App\Action; use Magento\Framework\Controller\ResultFactory; /** + * Add wishlist item to shopping cart and remove from wishlist controller. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Cart extends \Magento\Wishlist\Controller\AbstractIndex +class Cart extends \Magento\Wishlist\Controller\AbstractIndex implements Action\HttpPostActionInterface { /** * @var \Magento\Wishlist\Controller\WishlistProviderInterface @@ -195,12 +198,12 @@ public function execute() } } } catch (ProductException $e) { - $this->messageManager->addError(__('This product(s) is out of stock.')); + $this->messageManager->addErrorMessage(__('This product(s) is out of stock.')); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addNotice($e->getMessage()); + $this->messageManager->addNoticeMessage($e->getMessage()); $redirectUrl = $configureUrl; } catch (\Exception $e) { - $this->messageManager->addException($e, __('We can\'t add the item to the cart right now.')); + $this->messageManager->addExceptionMessage($e, __('We can\'t add the item to the cart right now.')); } $this->helper->calculate(); diff --git a/app/code/Magento/Wishlist/Controller/Index/DownloadCustomOption.php b/app/code/Magento/Wishlist/Controller/Index/DownloadCustomOption.php index e43a03e72d87..742b2a91e931 100644 --- a/app/code/Magento/Wishlist/Controller/Index/DownloadCustomOption.php +++ b/app/code/Magento/Wishlist/Controller/Index/DownloadCustomOption.php @@ -3,15 +3,21 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Wishlist\Controller\Index; use Magento\Framework\App\Action; +use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Serialize\Serializer\Json; -class DownloadCustomOption extends \Magento\Wishlist\Controller\AbstractIndex +/** + * Class DownloadCustomOption. Represents request-flow logic for option's file download + */ +class DownloadCustomOption extends \Magento\Wishlist\Controller\AbstractIndex implements HttpGetActionInterface { /** * @var \Magento\Framework\App\Response\Http\FileFactory @@ -26,6 +32,8 @@ class DownloadCustomOption extends \Magento\Wishlist\Controller\AbstractIndex private $json; /** + * Constructor method + * * @param Action\Context $context * @param \Magento\Framework\App\Response\Http\FileFactory $fileResponseFactory * @param Json|null $json @@ -45,7 +53,6 @@ public function __construct( * * @return \Magento\Framework\Controller\Result\Forward * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.ExitExpression) */ public function execute() { @@ -92,7 +99,8 @@ public function execute() $this->_fileResponseFactory->create( $info['title'], ['value' => $info['quote_path'], 'type' => 'filename'], - DirectoryList::ROOT + DirectoryList::ROOT, + $info['type'] ); } } catch (\Exception $e) { diff --git a/app/code/Magento/Wishlist/Controller/Index/Fromcart.php b/app/code/Magento/Wishlist/Controller/Index/Fromcart.php index 49396004427f..52d0951f1670 100644 --- a/app/code/Magento/Wishlist/Controller/Index/Fromcart.php +++ b/app/code/Magento/Wishlist/Controller/Index/Fromcart.php @@ -8,7 +8,6 @@ use Magento\Checkout\Helper\Cart as CartHelper; use Magento\Checkout\Model\Cart as CheckoutCart; -use Magento\Customer\Model\Session; use Magento\Framework\App\Action; use Magento\Framework\Data\Form\FormKey\Validator; use Magento\Framework\Escaper; @@ -19,9 +18,11 @@ use Magento\Wishlist\Helper\Data as WishlistHelper; /** + * Add cart item to wishlist and remove from cart controller. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Fromcart extends \Magento\Wishlist\Controller\AbstractIndex +class Fromcart extends \Magento\Wishlist\Controller\AbstractIndex implements Action\HttpPostActionInterface { /** * @var WishlistProviderInterface diff --git a/app/code/Magento/Wishlist/Controller/Index/Send.php b/app/code/Magento/Wishlist/Controller/Index/Send.php index c2389af6a228..a4e8258b9d67 100644 --- a/app/code/Magento/Wishlist/Controller/Index/Send.php +++ b/app/code/Magento/Wishlist/Controller/Index/Send.php @@ -8,16 +8,28 @@ use Magento\Framework\App\Action; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ResponseInterface; use Magento\Framework\Exception\NotFoundException; use Magento\Framework\Session\Generic as WishlistSession; use Magento\Store\Model\StoreManagerInterface; use Magento\Framework\Controller\ResultFactory; use Magento\Framework\View\Result\Layout as ResultLayout; +use Magento\Captcha\Helper\Data as CaptchaHelper; +use Magento\Captcha\Observer\CaptchaStringResolver; +use Magento\Framework\Controller\Result\Redirect; +use Magento\Framework\Controller\ResultInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Captcha\Model\DefaultModel as CaptchaModel; +use Magento\Framework\Exception\LocalizedException; +use Magento\Customer\Model\Customer; /** + * Class Send + * + * @package Magento\Wishlist\Controller\Index * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Send extends \Magento\Wishlist\Controller\AbstractIndex +class Send extends \Magento\Wishlist\Controller\AbstractIndex implements Action\HttpPostActionInterface { /** * @var \Magento\Customer\Helper\View @@ -69,6 +81,16 @@ class Send extends \Magento\Wishlist\Controller\AbstractIndex */ protected $storeManager; + /** + * @var CaptchaHelper + */ + private $captchaHelper; + + /** + * @var CaptchaStringResolver + */ + private $captchaStringResolver; + /** * @param Action\Context $context * @param \Magento\Framework\Data\Form\FormKey\Validator $formKeyValidator @@ -81,6 +103,8 @@ class Send extends \Magento\Wishlist\Controller\AbstractIndex * @param WishlistSession $wishlistSession * @param ScopeConfigInterface $scopeConfig * @param StoreManagerInterface $storeManager + * @param CaptchaHelper|null $captchaHelper + * @param CaptchaStringResolver|null $captchaStringResolver * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -94,7 +118,9 @@ public function __construct( \Magento\Customer\Helper\View $customerHelperView, WishlistSession $wishlistSession, ScopeConfigInterface $scopeConfig, - StoreManagerInterface $storeManager + StoreManagerInterface $storeManager, + ?CaptchaHelper $captchaHelper = null, + ?CaptchaStringResolver $captchaStringResolver = null ) { $this->_formKeyValidator = $formKeyValidator; $this->_customerSession = $customerSession; @@ -106,6 +132,10 @@ public function __construct( $this->wishlistSession = $wishlistSession; $this->scopeConfig = $scopeConfig; $this->storeManager = $storeManager; + $this->captchaHelper = $captchaHelper ?: ObjectManager::getInstance()->get(CaptchaHelper::class); + $this->captchaStringResolver = $captchaStringResolver ? + : ObjectManager::getInstance()->get(CaptchaStringResolver::class); + parent::__construct($context); } @@ -114,6 +144,7 @@ public function __construct( * * @return \Magento\Framework\Controller\Result\Redirect * @throws NotFoundException + * @throws \Zend_Validate_Exception * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) @@ -122,11 +153,25 @@ public function execute() { /** @var \Magento\Framework\Controller\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); + $captchaForName = 'share_wishlist_form'; + /** @var CaptchaModel $captchaModel */ + $captchaModel = $this->captchaHelper->getCaptcha($captchaForName); + if (!$this->_formKeyValidator->validate($this->getRequest())) { $resultRedirect->setPath('*/*/'); return $resultRedirect; } + $isCorrectCaptcha = $this->validateCaptcha($captchaModel, $captchaForName); + + $this->logCaptchaAttempt($captchaModel); + + if (!$isCorrectCaptcha) { + $this->messageManager->addErrorMessage(__('Incorrect CAPTCHA')); + $resultRedirect->setPath('*/*/share'); + return $resultRedirect; + } + $wishlist = $this->wishlistProvider->getWishlist(); if (!$wishlist) { throw new NotFoundException(__('Page not found.')); @@ -288,4 +333,46 @@ protected function getWishlistItems(ResultLayout $resultLayout) ->getBlock('wishlist.email.items') ->toHtml(); } + + /** + * Log customer action attempts + * + * @param CaptchaModel $captchaModel + * @return void + */ + private function logCaptchaAttempt(CaptchaModel $captchaModel): void + { + /** @var Customer $customer */ + $customer = $this->_customerSession->getCustomer(); + $email = ''; + + if ($customer->getId()) { + $email = $customer->getEmail(); + } + + $captchaModel->logAttempt($email); + } + + /** + * Captcha validate logic + * + * @param CaptchaModel $captchaModel + * @param string $captchaFormName + * @return bool + */ + private function validateCaptcha(CaptchaModel $captchaModel, string $captchaFormName) : bool + { + if ($captchaModel->isRequired()) { + $word = $this->captchaStringResolver->resolve( + $this->getRequest(), + $captchaFormName + ); + + if (!$captchaModel->isCorrect($word)) { + return false; + } + } + + return true; + } } diff --git a/app/code/Magento/Wishlist/Controller/Index/Update.php b/app/code/Magento/Wishlist/Controller/Index/Update.php index 056d58b4c70b..b56aa4b5b3c8 100755 --- a/app/code/Magento/Wishlist/Controller/Index/Update.php +++ b/app/code/Magento/Wishlist/Controller/Index/Update.php @@ -6,10 +6,14 @@ namespace Magento\Wishlist\Controller\Index; use Magento\Framework\App\Action; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\Exception\NotFoundException; use Magento\Framework\Controller\ResultFactory; -class Update extends \Magento\Wishlist\Controller\AbstractIndex +/** + * Class Update + */ +class Update extends \Magento\Wishlist\Controller\AbstractIndex implements HttpPostActionInterface { /** * @var \Magento\Wishlist\Controller\WishlistProviderInterface @@ -83,8 +87,6 @@ public function execute() )->defaultCommentString() ) { $description = ''; - } elseif (!strlen($description)) { - $description = $item->getDescription(); } $qty = null; diff --git a/app/code/Magento/Wishlist/Helper/Data.php b/app/code/Magento/Wishlist/Helper/Data.php index f4c1aa9662bd..3d25e16294fc 100644 --- a/app/code/Magento/Wishlist/Helper/Data.php +++ b/app/code/Magento/Wishlist/Helper/Data.php @@ -13,6 +13,7 @@ * * @author Magento Core Team <core@magentocommerce.com> * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * * @api * @since 100.0.2 @@ -171,7 +172,7 @@ public function setCustomer(\Magento\Customer\Api\Data\CustomerInterface $custom public function getCustomer() { if (!$this->_currentCustomer && $this->_customerSession->isLoggedIn()) { - $this->_currentCustomer = $this->_customerSession->getCustomerDataObject(); + $this->_currentCustomer = $this->_customerSession->getCustomerData(); } return $this->_currentCustomer; } @@ -195,6 +196,7 @@ public function getWishlist() /** * Retrieve wishlist item count (include config settings) + * * Used in top link menu only * * @return int @@ -354,7 +356,7 @@ public function getMoveFromCartParams($itemId) * * @param \Magento\Catalog\Model\Product|\Magento\Wishlist\Model\Item $item * - * @return string|false + * @return string|false */ public function getUpdateParams($item) { @@ -381,7 +383,7 @@ public function getUpdateParams($item) * Retrieve params for adding item to shopping cart * * @param string|\Magento\Catalog\Model\Product|\Magento\Wishlist\Model\Item $item - * @return string + * @return string */ public function getAddToCartUrl($item) { @@ -427,7 +429,7 @@ public function addRefererToParams(array $params) * Retrieve URL for adding item to shopping cart from shared wishlist * * @param string|\Magento\Catalog\Model\Product|\Magento\Wishlist\Model\Item $item - * @return string + * @return string */ public function getSharedAddToCartUrl($item) { @@ -450,6 +452,8 @@ public function getSharedAddAllToCartUrl() } /** + * Get cart URL parameters + * * @param string|\Magento\Catalog\Model\Product|\Magento\Wishlist\Model\Item $item * @return array */ @@ -576,7 +580,7 @@ public function calculate() ) { $count = $collection->getItemsQty(); } else { - $count = $collection->getSize(); + $count = $collection->count(); } $this->_customerSession->setWishlistDisplayType( $this->scopeConfig->getValue( diff --git a/app/code/Magento/Wishlist/Helper/Rss.php b/app/code/Magento/Wishlist/Helper/Rss.php index 14ca52e92ad8..b4f761f03dd9 100644 --- a/app/code/Magento/Wishlist/Helper/Rss.php +++ b/app/code/Magento/Wishlist/Helper/Rss.php @@ -7,6 +7,8 @@ namespace Magento\Wishlist\Helper; /** + * Wishlist rss helper + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * * @api @@ -104,7 +106,7 @@ public function getCustomer() if ($this->_customer === null) { $params = $this->urlDecoder->decode($this->_getRequest()->getParam('data')); $data = explode(',', $params); - $customerId = abs(intval($data[0])); + $customerId = abs((int)$data[0]); if ($customerId && ($customerId == $this->_customerSession->getCustomerId())) { $this->_customer = $this->_customerRepository->getById($customerId); } else { diff --git a/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php b/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php index 225026f31a99..5c131d27fb8d 100644 --- a/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php +++ b/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php @@ -307,6 +307,7 @@ protected function _assignProducts() $checkInStock = $this->_productInStock && !$this->stockConfiguration->isShowOutOfStock(); + /** @var \Magento\Wishlist\Model\Item $item */ foreach ($this as $item) { $product = $productCollection->getItemById($item->getProductId()); if ($product) { @@ -320,7 +321,7 @@ protected function _assignProducts() $item->setPrice($product->getPrice()); } } else { - $item->isDeleted(true); + $this->removeItemByKey($item->getId()); } } @@ -418,6 +419,7 @@ public function setVisibilityFilter($flag = true) /** * Set Salable Filter. + * * This filter apply Salable Product Types Filter to product collection. * * @param bool $flag @@ -431,6 +433,7 @@ public function setSalableFilter($flag = true) /** * Set In Stock Filter. + * * This filter remove items with no salable product. * * @param bool $flag @@ -478,7 +481,7 @@ public function addDaysFilter($constraints) if (isset($constraints['to'])) { $firstDay = new \DateTime(); - $firstDay->modify('-' . $gmtOffset . ' second')->modify('-' . (intval($constraints['to']) + 1) . ' day'); + $firstDay->modify('-' . $gmtOffset . ' second')->modify('-' . ((int)($constraints['to']) + 1) . ' day'); $filter['from'] = $firstDay; } @@ -567,6 +570,8 @@ public function getItemsQty() } /** + * After load data + * * @return $this */ protected function _afterLoadData() diff --git a/app/code/Magento/Wishlist/Model/Rss/Wishlist.php b/app/code/Magento/Wishlist/Model/Rss/Wishlist.php index 75df3027ad9a..ff59e0cdd7b9 100644 --- a/app/code/Magento/Wishlist/Model/Rss/Wishlist.php +++ b/app/code/Magento/Wishlist/Model/Rss/Wishlist.php @@ -7,9 +7,11 @@ namespace Magento\Wishlist\Model\Rss; use Magento\Framework\App\Rss\DataProviderInterface; +use Magento\Store\Model\ScopeInterface; /** * Wishlist RSS model + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Wishlist implements DataProviderInterface @@ -70,6 +72,8 @@ class Wishlist implements DataProviderInterface protected $customerFactory; /** + * Wishlist constructor. + * * @param \Magento\Wishlist\Helper\Rss $wishlistHelper * @param \Magento\Wishlist\Block\Customer\Wishlist $wishlistBlock * @param \Magento\Catalog\Helper\Output $outputHelper @@ -114,16 +118,15 @@ public function __construct( */ public function isAllowed() { - return (bool)$this->scopeConfig->getValue( - 'rss/wishlist/active', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ); + return $this->scopeConfig->isSetFlag('rss/wishlist/active', ScopeInterface::SCOPE_STORE) + && $this->getWishlist()->getCustomerId() === $this->wishlistHelper->getCustomer()->getId(); } /** * Get RSS feed items * * @return array + * @throws \Magento\Framework\Exception\LocalizedException */ public function getRssData() { @@ -180,8 +183,8 @@ public function getRssData() } } else { $data = [ - 'title' => __('We cannot retrieve the Wish List.'), - 'description' => __('We cannot retrieve the Wish List.'), + 'title' => __('We cannot retrieve the Wish List.')->render(), + 'description' => __('We cannot retrieve the Wish List.')->render(), 'link' => $this->urlBuilder->getUrl(), 'charset' => 'UTF-8', ]; @@ -191,14 +194,18 @@ public function getRssData() } /** + * GetCacheKey + * * @return string */ public function getCacheKey() { - return 'rss_wishlist_data'; + return 'rss_wishlist_data_' . $this->getWishlist()->getId(); } /** + * Get Cache Lifetime + * * @return int */ public function getCacheLifetime() @@ -215,7 +222,7 @@ public function getHeader() { $customerId = $this->getWishlist()->getCustomerId(); $customer = $this->customerFactory->create()->load($customerId); - $title = __('%1\'s Wishlist', $customer->getName()); + $title = __('%1\'s Wishlist', $customer->getName())->render(); $newUrl = $this->urlBuilder->getUrl( 'wishlist/shared/index', ['code' => $this->getWishlist()->getSharingCode()] @@ -264,7 +271,7 @@ public function getProductPriceHtml(\Magento\Catalog\Model\Product $product) } /** - * @return array + * @inheritdoc */ public function getFeeds() { @@ -272,7 +279,7 @@ public function getFeeds() } /** - * {@inheritdoc} + * @inheritdoc */ public function isAuthRequired() { diff --git a/app/code/Magento/Wishlist/Model/Wishlist.php b/app/code/Magento/Wishlist/Model/Wishlist.php index ec0021c4949e..9797ab58b076 100644 --- a/app/code/Magento/Wishlist/Model/Wishlist.php +++ b/app/code/Magento/Wishlist/Model/Wishlist.php @@ -380,6 +380,7 @@ public function addItem(Item $item) /** * Adds new product to wishlist. + * * Returns new item or string on error. * * @param int|\Magento\Catalog\Model\Product $product @@ -581,7 +582,7 @@ public function setStore($store) */ public function getItemsCount() { - return $this->getItemCollection()->getSize(); + return $this->getItemCollection()->count(); } /** diff --git a/app/code/Magento/Wishlist/Setup/Patch/Data/ConvertSerializedData.php b/app/code/Magento/Wishlist/Setup/Patch/Data/ConvertSerializedData.php index 76f27756d827..0e809c470392 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/Setup/Patch/Schema/AddProductIdConstraint.php b/app/code/Magento/Wishlist/Setup/Patch/Schema/AddProductIdConstraint.php new file mode 100644 index 000000000000..5c65fce10ccd --- /dev/null +++ b/app/code/Magento/Wishlist/Setup/Patch/Schema/AddProductIdConstraint.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Setup\Patch\Schema; + +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\Setup\Patch\SchemaPatchInterface; +use Magento\Framework\Setup\SchemaSetupInterface; + +/** + * Class AddProductIdConstraint + */ +class AddProductIdConstraint implements SchemaPatchInterface +{ + /** + * @var SchemaSetupInterface + */ + private $schemaSetup; + + /** + * @param SchemaSetupInterface $schemaSetup + */ + public function __construct( + SchemaSetupInterface $schemaSetup + ) { + $this->schemaSetup = $schemaSetup; + } + + /** + * Run code inside patch. + * + * @return void + */ + public function apply() + { + $this->schemaSetup->startSetup(); + + $this->schemaSetup->getConnection()->addForeignKey( + $this->schemaSetup->getConnection()->getForeignKeyName( + $this->schemaSetup->getTable('wishlist_item_option'), + 'product_id', + $this->schemaSetup->getTable('catalog_product_entity'), + 'entity_id' + ), + $this->schemaSetup->getTable('wishlist_item_option'), + 'product_id', + $this->schemaSetup->getTable('catalog_product_entity'), + 'entity_id', + AdapterInterface::FK_ACTION_CASCADE, + true + ); + + $this->schemaSetup->endSetup(); + } + + /** + * Get array of patches that have to be executed prior to this. + * + * @return string[] + */ + public static function getDependencies() + { + return []; + } + + /** + * Get aliases (previous names) for the patch. + * + * @return string[] + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerWishlistActionGroup.xml b/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerWishlistActionGroup.xml index c4b3763af9b9..a1c5b9eae5c4 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerWishlistActionGroup.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerWishlistActionGroup.xml @@ -25,9 +25,10 @@ <arguments> <argument name="productVar"/> </arguments> + <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> @@ -87,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 811871bf685a..c6a9704698b0 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 000000000000..6d6151648c5e --- /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 7a767e42e82b..ef619726b76a 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 000000000000..76b99ba56a32 --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Section/StorefrontCustomerWishlistShareSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCustomerWishlistShareSection"> + <element name="ProductShareWishlistEmail" type="input" selector="#email_address"/> + <element name="ProductShareWishlistTextMessage" type="input" selector="#message"/> + <element name="ProductShareWishlistButton" type="button" selector=".action.submit.primary" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/ConfProdAddToCartWishListWithUnselectedAttrTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/ConfProdAddToCartWishListWithUnselectedAttrTest.xml new file mode 100644 index 000000000000..4e6a062c7993 --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/ConfProdAddToCartWishListWithUnselectedAttrTest.xml @@ -0,0 +1,72 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="ConfProdAddToCartWishListWithUnselectedAttrTest"> + <annotations> + <stories value="Wishlist"/> + <group value="wishlist"/> + <title value="Adding configurable product to Cart from Wish List with unselected attributes"/> + <description value="Verify adding configurable product to Cart from Wish List when attributes is unselected"/> + <severity value="AVERAGE"/> + <testCaseId value="MAGETWO-95897"/> + <useCaseId value="MAGETWO-95837"/> + </annotations> + + <before> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + <createData entity="ApiCategory" stepKey="createCategory"/> + <!--Create Configurable product--> + <actionGroup ref="createConfigurableProduct" stepKey="createProduct"> + <argument name="product" value="_defaultProduct"/> + <argument name="category" value="$$createCategory$$"/> + </actionGroup> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <!-- Delete the first simple product --> + <actionGroup stepKey="deleteProduct1" ref="deleteProductBySku"> + <argument name="sku" value="{{_defaultProduct.sku}}"/> + </actionGroup> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" + dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Login as customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForLogin"/> + + <!--Go To Created Product Page--> + <amOnPage stepKey="goToCreatedProductPage" url="{{_defaultProduct.urlKey}}.html"/> + <waitForPageLoad stepKey="waitForProductPageLoad2"/> + + <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptions1}}" stepKey="checkDropDownProductOption"/> + <selectOption userInput="{{colorProductAttribute1.name}}" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="selectOption1"/> + <selectOption userInput="{{colorProductAttribute2.name}}" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="selectOption2"/> + <click selector="{{StorefrontProductInfoMainSection.productAttributeOptions1}}" stepKey="clickDropDownProductOption"/> + + <!--Click Add to Wish List link--> + <click selector="{{StorefrontProductPageSection.addToWishlist}}" stepKey="addFirstPnroductToWishlist"/> + + <waitForPageLoad stepKey="waitForLoading"/> + + <!--Click "Add All to Cart" button--> + <click selector="{{StorefrontCustomerWishlistProductSection.ProductAddAllToCart}}" stepKey="addAllToCart"/> + <waitForElementVisible stepKey="waitForErrorAppears" selector="{{StorefrontMessagesSection.error}}"/> + + <!--Assert Correct Error Message--> + <see userInput="You need to choose options for your item for" stepKey="assertCorrectErrorMessage"/> + <dontSee userInput="1 product(s) have been added to shopping cart" stepKey="dontSeeSuccessMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/ConfigurableProductChildImageShouldBeShownOnWishListTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/ConfigurableProductChildImageShouldBeShownOnWishListTest.xml index 42d4203999a4..6b951c89208c 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/ConfigurableProductChildImageShouldBeShownOnWishListTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/ConfigurableProductChildImageShouldBeShownOnWishListTest.xml @@ -6,7 +6,8 @@ */ --> -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="ConfigurableProductChildImageShouldBeShownOnWishListTest"> <annotations> <features value="Wishlist"/> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml index 3d2d6d8781be..ede63322235f 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml @@ -15,6 +15,10 @@ <description value="All products added to wishlist should be visible on any store. Even if product visibility was set to 'Not Visible Individually' for this store"/> <group value="wishlist"/> <severity value="AVERAGE"/> + <testCaseId value="MAGETWO-95678"/> + <skip> + <issueId value="MC-13867"/> + </skip> </annotations> <before> <createData entity="customStoreGroup" stepKey="storeGroup"/> @@ -85,15 +89,12 @@ <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="ClickSwitchStoreButtonOnDefaultStore"/> <click selector="{{StorefrontFooterSection.storeLink($$storeGroup.group[name]$$)}}" stepKey="SelectSecondStoreToSwitchOn"/> <!-- Verify that both products are visible in wishlist on both stores --> - <see userInput="$$product.name$$" selector="{{StorefrontCustomerWishlistSection.productItemNameText}}" stepKey="seeProduct1InWishlist"/> <amOnPage url="$$secondProduct.name$$.html" stepKey="navigateToProductPageOnSecondStore"/> <see userInput="$$secondProduct.name$$" selector="{{StorefrontProductInfoMainSection.productName}}" stepKey="assertSecondProductNameTitle"/> <click selector="{{StorefrontProductPageSection.addToWishlist}}" stepKey="addSecondProductToWishlist"/> - <see userInput="$$product.name$$" selector="{{StorefrontCustomerWishlistSection.productItemNameText}}" stepKey="seeProduct1InWishlistOnSecondStore"/> <see userInput="$$secondProduct.name$$" selector="{{StorefrontCustomerWishlistSection.productItemNameText}}" stepKey="seeProduct2InWishlistOnSecondStore"/> <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="ClickSwitchStoreButtonOnSecondStore"/> <click selector="{{StorefrontFooterSection.storeLink('Main Website Store')}}" stepKey="SelectDefaultStoreToSwitchOn"/> <see userInput="$$product.name$$" selector="{{StorefrontCustomerWishlistSection.productItemNameText}}" stepKey="seeProduct1InWishlistOnDefaultStore"/> - <see userInput="$$secondProduct.name$$" selector="{{StorefrontCustomerWishlistSection.productItemNameText}}" stepKey="seeProduct2InWishlistOnDefaultStore"/> </test> </tests> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeletePersistedWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeletePersistedWishlistTest.xml index 0ae2b6af804b..0001bd9d6db7 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeletePersistedWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeletePersistedWishlistTest.xml @@ -13,12 +13,12 @@ <stories value="Delete a persist wishlist for a customer"/> <title value="Customer should be able to delete a persistent wishlist"/> <description value="Customer should be able to delete a persistent wishlist"/> + <severity value="AVERAGE"/> <group value="wishlist"/> - <group value="skip"/> + <testCaseId value="MC-4110"/> <skip> <issueId value="MQE-1145"/> </skip> - <severity value="AVERAGE"/> </annotations> <before> <createData stepKey="category" entity="SimpleSubCategory"/> 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 000000000000..87c5ed950949 --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistEntityTest.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontShareWishlistEntityTest"> + <annotations> + <features value="Wishlist"/> + <stories value="Customer wishlist"/> + <title value="Customer should be able to share a persistent wishlist"/> + <description value="Customer should be able to share a persistent wishlist"/> + <severity value="AVERAGE"/> + <group value="wishlist"/> + <testCaseId value="MC-13976"/> + <group value="wishlist"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <createData entity="SimpleProduct" stepKey="product"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="customer"/> + </before> + <after> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <deleteData createDataKey="product" stepKey="deleteProduct"/> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + </after> + + <!-- Sign in as customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$$customer$$"/> + </actionGroup> + + <actionGroup ref="OpenProductFromCategoryPageActionGroup" stepKey="openProductFromCategory"> + <argument name="category" value="$$category$$"/> + <argument name="product" value="$$product$$"/> + </actionGroup> + + <actionGroup ref="StorefrontCustomerAddProductToWishlistActionGroup" stepKey="addToWishlistProduct"> + <argument name="productVar" value="$$product$$"/> + </actionGroup> + + <actionGroup ref="StorefrontCustomerShareWishlistActionGroup" stepKey="shareWishlist"/> + + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + </test> +</tests> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateWishlistTest.xml index 9f11de49adcd..e482449f623f 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateWishlistTest.xml @@ -1,59 +1,58 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> - <test name="StorefrontUpdateWishlistTest"> - <annotations> - <title value="Displaying of message after Wish List update"/> - <stories value="MAGETWO-91666: Wishlist update does not return a success message"/> - <description value="Displaying of message after Wish List update"/> - <features value="Wishlist"/> - <severity value="MAJOR"/> - <testCaseId value="MAGETWO-94296"/> - <group value="Wishlist"/> - </annotations> - - <before> - <createData entity="SimpleSubCategory" stepKey="category"/> - <createData entity="SimpleProduct" stepKey="product"> - <requiredEntity createDataKey="category"/> - </createData> - <createData entity="Simple_US_Customer" stepKey="customer"/> - </before> - - <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> - <argument name="Customer" value="$$customer$$"/> - </actionGroup> - - <actionGroup ref="OpenProductFromCategoryPageActionGroup" stepKey="openProductFromCategory"> - <argument name="category" value="$$category$$"/> - <argument name="product" value="$$product$$"/> - </actionGroup> - - <actionGroup ref="StorefrontCustomerAddProductToWishlistActionGroup" stepKey="addProductToWishlist"> - <argument name="productVar" value="$$product$$"/> - </actionGroup> - - <actionGroup ref="StorefrontCustomerCheckProductInWishlist" stepKey="checkProductInWishlist"> - <argument name="productVar" value="$$product$$"/> - </actionGroup> - - <actionGroup ref="StorefrontCustomerEditProductInWishlist" stepKey="updateProductInWishlist"> - <argument name="product" value="$$product$$"/> - <argument name="description" value="some text"/> - <argument name="quantity" value="2"/> - </actionGroup> - - <after> - <deleteData createDataKey="category" stepKey="deleteCategory"/> - <deleteData createDataKey="product" stepKey="deleteProduct"/> - <deleteData createDataKey="customer" stepKey="deleteCustomer"/> - </after> - - </test> -</tests> \ No newline at end of file +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontUpdateWishlistTest"> + <annotations> + <title value="Displaying of message after Wish List update"/> + <stories value="MAGETWO-91666: Wishlist update does not return a success message"/> + <description value="Displaying of message after Wish List update"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-94296"/> + <group value="Wishlist"/> + </annotations> + + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <createData entity="SimpleProduct" stepKey="product"> + <requiredEntity createDataKey="category"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="customer"/> + </before> + + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$$customer$$"/> + </actionGroup> + + <actionGroup ref="OpenProductFromCategoryPageActionGroup" stepKey="openProductFromCategory"> + <argument name="category" value="$$category$$"/> + <argument name="product" value="$$product$$"/> + </actionGroup> + + <actionGroup ref="StorefrontCustomerAddProductToWishlistActionGroup" stepKey="addProductToWishlist"> + <argument name="productVar" value="$$product$$"/> + </actionGroup> + + <actionGroup ref="StorefrontCustomerCheckProductInWishlist" stepKey="checkProductInWishlist"> + <argument name="productVar" value="$$product$$"/> + </actionGroup> + + <actionGroup ref="StorefrontCustomerEditProductInWishlist" stepKey="updateProductInWishlist"> + <argument name="product" value="$$product$$"/> + <argument name="description" value="some text"/> + <argument name="quantity" value="2"/> + </actionGroup> + + <after> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <deleteData createDataKey="product" stepKey="deleteProduct"/> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/Wishlist/Test/Unit/Controller/Index/CartTest.php b/app/code/Magento/Wishlist/Test/Unit/Controller/Index/CartTest.php index d89f6e43e07b..e9061f1f3d5f 100644 --- a/app/code/Magento/Wishlist/Test/Unit/Controller/Index/CartTest.php +++ b/app/code/Magento/Wishlist/Test/Unit/Controller/Index/CartTest.php @@ -735,7 +735,7 @@ public function testExecuteWithoutQuantityArrayAndOutOfStock() ->willThrowException(new ProductException(__('Test Phrase'))); $this->messageManagerMock->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with('This product(s) is out of stock.', null) ->willReturnSelf(); @@ -901,7 +901,7 @@ public function testExecuteWithoutQuantityArrayAndConfigurable() ->willThrowException(new \Magento\Framework\Exception\LocalizedException(__('message'))); $this->messageManagerMock->expects($this->once()) - ->method('addNotice') + ->method('addNoticeMessage') ->with('message', null) ->willReturnSelf(); @@ -1073,7 +1073,7 @@ public function testExecuteWithEditQuantity() ->willThrowException(new \Magento\Framework\Exception\LocalizedException(__('message'))); $this->messageManagerMock->expects($this->once()) - ->method('addNotice') + ->method('addNoticeMessage') ->with('message', null) ->willReturnSelf(); diff --git a/app/code/Magento/Wishlist/Test/Unit/Controller/Index/DownloadCustomOptionTest.php b/app/code/Magento/Wishlist/Test/Unit/Controller/Index/DownloadCustomOptionTest.php deleted file mode 100644 index 6fefe18cf897..000000000000 --- a/app/code/Magento/Wishlist/Test/Unit/Controller/Index/DownloadCustomOptionTest.php +++ /dev/null @@ -1,154 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Wishlist\Test\Unit\Controller\Index; - -class DownloadCustomOptionTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Wishlist\Controller\Index\DownloadCustomOption - */ - protected $model; - - /** - * @var \Magento\Framework\App\Action\Context|\PHPUnit_Framework_MockObject_MockObject - */ - protected $contextMock; - - /** - * @var \Magento\Framework\App\Response\Http\FileFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $fileResponseFactoryMock; - - /** - * @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $requestMock; - - /** - * @var \Magento\Framework\ObjectManagerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $objectManagerMock; - - /** - * @var \Magento\Framework\Controller\ResultFactory|\PHPUnit_Framework_MockObject_MockObject - */ - protected $resultFactoryMock; - - /** - * @var \Magento\Framework\Serialize\Serializer\Json|\PHPUnit_Framework_MockObject_MockObject - */ - protected $jsonMock; - - protected function setUp() - { - $this->fileResponseFactoryMock = $this->getMockBuilder(\Magento\Framework\App\Response\Http\FileFactory::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->jsonMock = $this->getMockBuilder(\Magento\Framework\Serialize\Serializer\Json::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->objectManagerMock = $this->getMockBuilder(\Magento\Framework\ObjectManagerInterface::class) - ->disableOriginalConstructor() - ->setMethods(['create', 'get', 'configure']) - ->getMock(); - - $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock = $this->getMockBuilder(\Magento\Framework\Controller\ResultFactory::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->contextMock = $this->getMockBuilder(\Magento\Framework\App\Action\Context::class) - ->disableOriginalConstructor() - ->getMock(); - $this->contextMock->expects($this->any()) - ->method('getObjectManager') - ->willReturn($this->objectManagerMock); - $this->contextMock->expects($this->any()) - ->method('getRequest') - ->willReturn($this->requestMock); - $this->contextMock->expects($this->any()) - ->method('getResultFactory') - ->willReturn($this->resultFactoryMock); - - $this->model = new \Magento\Wishlist\Controller\Index\DownloadCustomOption( - $this->contextMock, - $this->fileResponseFactoryMock, - $this->jsonMock - ); - } - - public function testExecute() - { - $data = [ - 'number' => 42, - 'string' => 'string_value', - 'boolean' => true, - 'collection' => [1, 2, 3], - 'secret_key' => 999 - ]; - $serialized_data = json_encode($data); - - $optionMock = $this->getMockBuilder(\Magento\Wishlist\Model\Item\Option::class) - ->disableOriginalConstructor() - ->setMethods(['getProductId', 'load', 'getId', 'getValue']) - ->getMock(); - $optionMock->expects($this->any()) - ->method('load') - ->willReturnSelf(); - $optionMock->expects($this->any()) - ->method('getId') - ->willReturn(true); - $optionMock->expects($this->any()) - ->method('getProductId') - ->willReturn('some_value'); - $optionMock->expects($this->any()) - ->method('getValue') - ->willReturn($serialized_data); - - $productOptionMock = $this->getMockBuilder(\Magento\Catalog\Model\Product\Option::class) - ->disableOriginalConstructor() - ->setMethods(['getProductId', 'load', 'getId', 'getType']) - ->getMock(); - $productOptionMock->expects($this->any()) - ->method('load') - ->willReturnSelf(); - $productOptionMock->expects($this->any()) - ->method('getId') - ->willReturn(true); - $productOptionMock->expects($this->any()) - ->method('getProductId') - ->willReturn('some_value'); - $productOptionMock->expects($this->any()) - ->method('getType') - ->willReturn('file'); - - $this->objectManagerMock->expects($this->any()) - ->method('create') - ->willReturnMap( - [ - [\Magento\Wishlist\Model\Item\Option::class, [], $optionMock], - [\Magento\Catalog\Model\Product\Option::class, [], $productOptionMock] - ] - ); - - $this->requestMock->expects($this->any()) - ->method('getParam') - ->willReturn(1); - - $this->jsonMock->expects($this->once()) - ->method('unserialize') - ->willReturnCallback(function ($value) { - return json_decode($value, true); - }); - - $this->assertEquals(null, $this->model->execute()); - } -} diff --git a/app/code/Magento/Wishlist/Test/Unit/Controller/Index/SendTest.php b/app/code/Magento/Wishlist/Test/Unit/Controller/Index/SendTest.php index a8c0fbb951cc..47148f787813 100644 --- a/app/code/Magento/Wishlist/Test/Unit/Controller/Index/SendTest.php +++ b/app/code/Magento/Wishlist/Test/Unit/Controller/Index/SendTest.php @@ -5,32 +5,24 @@ */ namespace Magento\Wishlist\Test\Unit\Controller\Index; -use Magento\Customer\Helper\View as CustomerViewHelper; use Magento\Customer\Model\Data\Customer as CustomerData; -use Magento\Customer\Model\Session as CustomerSession; use Magento\Framework\App\Action\Context as ActionContext; -use Magento\Framework\App\Area; -use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\RequestInterface; use Magento\Framework\Controller\Result\Redirect as ResultRedirect; use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Data\Form\FormKey\Validator as FormKeyValidator; use Magento\Framework\Event\ManagerInterface as EventManagerInterface; -use Magento\Framework\Mail\Template\TransportBuilder; use Magento\Framework\Mail\TransportInterface; use Magento\Framework\Message\ManagerInterface; -use Magento\Framework\Session\Generic as WishlistSession; -use Magento\Framework\Translate\Inline\StateInterface as TranslateInlineStateInterface; use Magento\Framework\UrlInterface; -use Magento\Framework\View\Layout; use Magento\Framework\View\Result\Layout as ResultLayout; -use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\Store; -use Magento\Store\Model\StoreManagerInterface; use Magento\Wishlist\Controller\Index\Send; use Magento\Wishlist\Controller\WishlistProviderInterface; -use Magento\Wishlist\Model\Config as WishlistConfig; -use Magento\Wishlist\Model\Wishlist; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Captcha\Helper\Data as CaptchaHelper; +use Magento\Captcha\Model\DefaultModel as CaptchaModel; +use Magento\Customer\Model\Session; /** * @SuppressWarnings(PHPMD.TooManyFields) @@ -47,36 +39,12 @@ class SendTest extends \PHPUnit\Framework\TestCase /** @var FormKeyValidator |\PHPUnit_Framework_MockObject_MockObject */ protected $formKeyValidator; - /** @var CustomerSession |\PHPUnit_Framework_MockObject_MockObject */ - protected $customerSession; - /** @var WishlistProviderInterface |\PHPUnit_Framework_MockObject_MockObject */ protected $wishlistProvider; - /** @var WishlistConfig |\PHPUnit_Framework_MockObject_MockObject */ - protected $wishlistConfig; - - /** @var TransportBuilder |\PHPUnit_Framework_MockObject_MockObject */ - protected $transportBuilder; - - /** @var TranslateInlineStateInterface |\PHPUnit_Framework_MockObject_MockObject */ - protected $inlineTranslation; - - /** @var CustomerViewHelper |\PHPUnit_Framework_MockObject_MockObject */ - protected $customerViewHelper; - - /** @var WishlistSession |\PHPUnit_Framework_MockObject_MockObject */ - protected $wishlistSession; - - /** @var ScopeConfigInterface |\PHPUnit_Framework_MockObject_MockObject */ - protected $scopeConfig; - /** @var Store |\PHPUnit_Framework_MockObject_MockObject */ protected $store; - /** @var StoreManagerInterface |\PHPUnit_Framework_MockObject_MockObject */ - protected $storeManager; - /** @var ResultFactory |\PHPUnit_Framework_MockObject_MockObject */ protected $resultFactory; @@ -86,15 +54,9 @@ class SendTest extends \PHPUnit\Framework\TestCase /** @var ResultLayout |\PHPUnit_Framework_MockObject_MockObject */ protected $resultLayout; - /** @var Layout |\PHPUnit_Framework_MockObject_MockObject */ - protected $layout; - /** @var RequestInterface |\PHPUnit_Framework_MockObject_MockObject */ protected $request; - /** @var Wishlist |\PHPUnit_Framework_MockObject_MockObject */ - protected $wishlist; - /** @var ManagerInterface |\PHPUnit_Framework_MockObject_MockObject */ protected $messageManager; @@ -110,6 +72,15 @@ class SendTest extends \PHPUnit\Framework\TestCase /** @var EventManagerInterface |\PHPUnit_Framework_MockObject_MockObject */ protected $eventManager; + /** @var CaptchaHelper |\PHPUnit_Framework_MockObject_MockObject */ + protected $captchaHelper; + + /** @var CaptchaModel |\PHPUnit_Framework_MockObject_MockObject */ + protected $captchaModel; + + /** @var Session |\PHPUnit_Framework_MockObject_MockObject */ + protected $customerSession; + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -136,7 +107,7 @@ protected function setUp() $this->request = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) ->setMethods([ 'getPost', - 'getPostValue', + 'getPostValue' ]) ->getMockForAbstractClass(); @@ -172,90 +143,72 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $this->customerSession = $this->getMockBuilder(\Magento\Customer\Model\Session::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->wishlistProvider = $this->getMockBuilder(\Magento\Wishlist\Controller\WishlistProviderInterface::class) - ->getMockForAbstractClass(); - - $this->wishlistConfig = $this->getMockBuilder(\Magento\Wishlist\Model\Config::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->transportBuilder = $this->getMockBuilder(\Magento\Framework\Mail\Template\TransportBuilder::class) + $customerMock = $this->getMockBuilder(\Magento\Customer\Model\Customer::class) ->disableOriginalConstructor() + ->setMethods([ + 'getEmail', + 'getId' + ]) ->getMock(); - $this->inlineTranslation = $this->getMockBuilder(\Magento\Framework\Translate\Inline\StateInterface::class) - ->disableOriginalConstructor() - ->getMock(); + $customerMock->expects($this->any()) + ->method('getEmail') + ->willReturn('expamle@mail.com'); - $this->customerViewHelper = $this->getMockBuilder(\Magento\Customer\Helper\View::class) - ->disableOriginalConstructor() - ->getMock(); + $customerMock->expects($this->any()) + ->method('getId') + ->willReturn(false); - $this->wishlistSession = $this->getMockBuilder(\Magento\Framework\Session\Generic::class) + $this->customerSession = $this->getMockBuilder(\Magento\Customer\Model\Session::class) ->disableOriginalConstructor() - ->setMethods(['setSharingForm']) + ->setMethods([ + 'getCustomer', + 'getData' + ]) ->getMock(); - $this->scopeConfig = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class) - ->disableOriginalConstructor() - ->getMock(); + $this->customerSession->expects($this->any()) + ->method('getCustomer') + ->willReturn($customerMock); - $this->store = $this->getMockBuilder(\Magento\Store\Model\Store::class) - ->disableOriginalConstructor() - ->setMethods(['getStoreId']) - ->getMock(); + $this->customerSession->expects($this->any()) + ->method('getData') + ->willReturn(false); - $this->storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->storeManager->expects($this->any()) - ->method('getStore') - ->willReturn($this->store); + $this->wishlistProvider = $this->getMockBuilder(\Magento\Wishlist\Controller\WishlistProviderInterface::class) + ->getMockForAbstractClass(); - $this->wishlist = $this->getMockBuilder(\Magento\Wishlist\Model\Wishlist::class) + $this->captchaHelper = $this->getMockBuilder(CaptchaHelper::class) ->disableOriginalConstructor() ->setMethods([ - 'getShared', - 'setShared', - 'getId', - 'getSharingCode', - 'save', - 'isSalable', + 'getCaptcha' ]) ->getMock(); - $this->customerData = $this->getMockBuilder(\Magento\Customer\Model\Data\Customer::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->layout = $this->getMockBuilder(\Magento\Framework\View\Layout::class) + $this->captchaModel = $this->getMockBuilder(CaptchaModel::class) ->disableOriginalConstructor() ->setMethods([ - 'getBlock', - 'setWishlistId', - 'toHtml', + 'isRequired', + 'logAttempt' ]) ->getMock(); - $this->transport = $this->getMockBuilder(\Magento\Framework\Mail\TransportInterface::class) - ->getMockForAbstractClass(); + $objectHelper = new ObjectManager($this); + + $this->captchaHelper->expects($this->once())->method('getCaptcha') + ->willReturn($this->captchaModel); + $this->captchaModel->expects($this->any())->method('isRequired') + ->willReturn(false); - $this->model = new Send( - $this->context, - $this->formKeyValidator, - $this->customerSession, - $this->wishlistProvider, - $this->wishlistConfig, - $this->transportBuilder, - $this->inlineTranslation, - $this->customerViewHelper, - $this->wishlistSession, - $this->scopeConfig, - $this->storeManager + $this->model = $objectHelper->getObject( + Send::class, + [ + 'context' => $this->context, + 'formKeyValidator' => $this->formKeyValidator, + 'wishlistProvider' => $this->wishlistProvider, + 'captchaHelper' => $this->captchaHelper, + '_customerSession' => $this->customerSession + ] ); } @@ -291,409 +244,4 @@ public function testExecuteNoWishlistAvailable() $this->model->execute(); } - - /** - * @param string $text - * @param int $textLimit - * @param string $emails - * @param int $emailsLimit - * @param int $shared - * @param string $postValue - * @param string $errorMessage - * - * @dataProvider dataProviderExecuteWithError - */ - public function testExecuteWithError( - $text, - $textLimit, - $emails, - $emailsLimit, - $shared, - $postValue, - $errorMessage - ) { - $this->formKeyValidator->expects($this->once()) - ->method('validate') - ->with($this->request) - ->willReturn(true); - - $this->wishlist->expects($this->once()) - ->method('getShared') - ->willReturn($shared); - - $this->wishlistProvider->expects($this->once()) - ->method('getWishlist') - ->willReturn($this->wishlist); - - $this->wishlistConfig->expects($this->once()) - ->method('getSharingEmailLimit') - ->willReturn($emailsLimit); - $this->wishlistConfig->expects($this->once()) - ->method('getSharingTextLimit') - ->willReturn($textLimit); - - $this->request->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap([ - ['emails', $emails], - ['message', $text], - ]); - $this->request->expects($this->once()) - ->method('getPostValue') - ->willReturn($postValue); - - $this->messageManager->expects($this->once()) - ->method('addError') - ->with($errorMessage) - ->willReturnSelf(); - - $this->wishlistSession->expects($this->any()) - ->method('setSharingForm') - ->with($postValue) - ->willReturnSelf(); - - $this->resultRedirect->expects($this->once()) - ->method('setPath') - ->with('*/*/share') - ->willReturnSelf(); - - $this->assertEquals($this->resultRedirect, $this->model->execute()); - } - - /** - * 1. Text - * 2. Text limit - * 3. Emails - * 4. Emails limit - * 5. Shared wishlists counter - * 6. POST value - * 7. Error message (RESULT) - * - * @return array - */ - public function dataProviderExecuteWithError() - { - return [ - ['test text', 1, 'user1@example.com', 1, 0, '', 'Message length must not exceed 1 symbols'], - ['test text', 100, null, 1, 0, '', 'Please enter an email address.'], - ['test text', 100, '', 1, 0, '', 'Please enter an email address.'], - ['test text', 100, 'user1@example.com', 1, 1, '', 'This wish list can be shared 0 more times.'], - [ - 'test text', - 100, - 'u1@example.com, u2@example.com', - 3, - 2, - '', - 'This wish list can be shared 1 more times.' - ], - ['test text', 100, 'wrongEmailAddress', 1, 0, '', 'Please enter a valid email address.'], - ['test text', 100, 'user1@example.com, wrongEmailAddress', 2, 0, '', 'Please enter a valid email address.'], - ['test text', 100, 'wrongEmailAddress, user2@example.com', 2, 0, '', 'Please enter a valid email address.'], - ]; - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecuteWithException() - { - $text = 'test text'; - $textLimit = 100; - $emails = 'user1@example.com'; - $emailsLimit = 1; - $shared = 0; - $customerName = 'user1 user1'; - $wishlistId = 1; - $rssLink = 'rss link'; - $sharingCode = 'sharing code'; - $exceptionMessage = 'test exception message'; - $postValue = ''; - - $this->formKeyValidator->expects($this->once()) - ->method('validate') - ->with($this->request) - ->willReturn(true); - - $this->wishlist->expects($this->exactly(2)) - ->method('getShared') - ->willReturn($shared); - $this->wishlist->expects($this->once()) - ->method('setShared') - ->with($shared) - ->willReturnSelf(); - $this->wishlist->expects($this->once()) - ->method('getId') - ->willReturn($wishlistId); - $this->wishlist->expects($this->once()) - ->method('getSharingCode') - ->willReturn($sharingCode); - $this->wishlist->expects($this->once()) - ->method('save') - ->willReturnSelf(); - - $this->wishlistProvider->expects($this->once()) - ->method('getWishlist') - ->willReturn($this->wishlist); - - $this->wishlistConfig->expects($this->once()) - ->method('getSharingEmailLimit') - ->willReturn($emailsLimit); - $this->wishlistConfig->expects($this->once()) - ->method('getSharingTextLimit') - ->willReturn($textLimit); - - $this->request->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap([ - ['emails', $emails], - ['message', $text], - ]); - $this->request->expects($this->exactly(2)) - ->method('getParam') - ->with('rss_url') - ->willReturn(true); - $this->request->expects($this->once()) - ->method('getPostValue') - ->willReturn($postValue); - - $this->layout->expects($this->once()) - ->method('getBlock') - ->with('wishlist.email.rss') - ->willReturnSelf(); - $this->layout->expects($this->once()) - ->method('setWishlistId') - ->with($wishlistId) - ->willReturnSelf(); - $this->layout->expects($this->once()) - ->method('toHtml') - ->willReturn($rssLink); - - $this->resultLayout->expects($this->exactly(2)) - ->method('addHandle') - ->willReturnMap([ - ['wishlist_email_rss', null], - ['wishlist_email_items', null], - ]); - $this->resultLayout->expects($this->once()) - ->method('getLayout') - ->willReturn($this->layout); - - $this->inlineTranslation->expects($this->once()) - ->method('suspend') - ->willReturnSelf(); - $this->inlineTranslation->expects($this->once()) - ->method('resume') - ->willReturnSelf(); - - $this->customerSession->expects($this->once()) - ->method('getCustomerDataObject') - ->willReturn($this->customerData); - - $this->customerViewHelper->expects($this->once()) - ->method('getCustomerName') - ->with($this->customerData) - ->willReturn($customerName); - - // Throw Exception - $this->transportBuilder->expects($this->once()) - ->method('setTemplateIdentifier') - ->willThrowException(new \Exception($exceptionMessage)); - - $this->messageManager->expects($this->once()) - ->method('addError') - ->with($exceptionMessage) - ->willReturnSelf(); - - $this->wishlistSession->expects($this->any()) - ->method('setSharingForm') - ->with($postValue) - ->willReturnSelf(); - - $this->resultRedirect->expects($this->once()) - ->method('setPath') - ->with('*/*/share') - ->willReturnSelf(); - - $this->assertEquals($this->resultRedirect, $this->model->execute()); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecute() - { - $text = 'text'; - $textLimit = 100; - $emails = 'user1@example.com'; - $emailsLimit = 1; - $shared = 0; - $customerName = 'user1 user1'; - $wishlistId = 1; - $sharingCode = 'sharing code'; - $templateIdentifier = 'template identifier'; - $storeId = 1; - $viewOnSiteLink = 'view on site link'; - $from = 'user0@example.com'; - - $this->formKeyValidator->expects($this->once()) - ->method('validate') - ->with($this->request) - ->willReturn(true); - - $this->wishlist->expects($this->exactly(2)) - ->method('getShared') - ->willReturn($shared); - $this->wishlist->expects($this->once()) - ->method('setShared') - ->with(++$shared) - ->willReturnSelf(); - $this->wishlist->expects($this->exactly(2)) - ->method('getId') - ->willReturn($wishlistId); - $this->wishlist->expects($this->once()) - ->method('getSharingCode') - ->willReturn($sharingCode); - $this->wishlist->expects($this->once()) - ->method('save') - ->willReturnSelf(); - $this->wishlist->expects($this->once()) - ->method('isSalable') - ->willReturn(true); - - $this->wishlistProvider->expects($this->once()) - ->method('getWishlist') - ->willReturn($this->wishlist); - - $this->wishlistConfig->expects($this->once()) - ->method('getSharingEmailLimit') - ->willReturn($emailsLimit); - $this->wishlistConfig->expects($this->once()) - ->method('getSharingTextLimit') - ->willReturn($textLimit); - - $this->request->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap([ - ['emails', $emails], - ['message', $text], - ]); - $this->request->expects($this->exactly(2)) - ->method('getParam') - ->with('rss_url') - ->willReturn(true); - - $this->layout->expects($this->exactly(2)) - ->method('getBlock') - ->willReturnMap([ - ['wishlist.email.rss', $this->layout], - ['wishlist.email.items', $this->layout], - ]); - - $this->layout->expects($this->once()) - ->method('setWishlistId') - ->with($wishlistId) - ->willReturnSelf(); - $this->layout->expects($this->exactly(2)) - ->method('toHtml') - ->willReturn($text); - - $this->resultLayout->expects($this->exactly(2)) - ->method('addHandle') - ->willReturnMap([ - ['wishlist_email_rss', null], - ['wishlist_email_items', null], - ]); - $this->resultLayout->expects($this->exactly(2)) - ->method('getLayout') - ->willReturn($this->layout); - - $this->inlineTranslation->expects($this->once()) - ->method('suspend') - ->willReturnSelf(); - $this->inlineTranslation->expects($this->once()) - ->method('resume') - ->willReturnSelf(); - - $this->customerSession->expects($this->once()) - ->method('getCustomerDataObject') - ->willReturn($this->customerData); - - $this->customerViewHelper->expects($this->once()) - ->method('getCustomerName') - ->with($this->customerData) - ->willReturn($customerName); - - $this->scopeConfig->expects($this->exactly(2)) - ->method('getValue') - ->willReturnMap([ - ['wishlist/email/email_template', ScopeInterface::SCOPE_STORE, null, $templateIdentifier], - ['wishlist/email/email_identity', ScopeInterface::SCOPE_STORE, null, $from], - ]); - - $this->store->expects($this->once()) - ->method('getStoreId') - ->willReturn($storeId); - - $this->url->expects($this->once()) - ->method('getUrl') - ->with('*/shared/index', ['code' => $sharingCode]) - ->willReturn($viewOnSiteLink); - - $this->transportBuilder->expects($this->once()) - ->method('setTemplateIdentifier') - ->with($templateIdentifier) - ->willReturnSelf(); - $this->transportBuilder->expects($this->once()) - ->method('setTemplateOptions') - ->with([ - 'area' => Area::AREA_FRONTEND, - 'store' => $storeId, - ]) - ->willReturnSelf(); - $this->transportBuilder->expects($this->once()) - ->method('setTemplateVars') - ->with([ - 'customer' => $this->customerData, - 'customerName' => $customerName, - 'salable' => 'yes', - 'items' => $text, - 'viewOnSiteLink' => $viewOnSiteLink, - 'message' => $text . $text, - 'store' => $this->store, - ]) - ->willReturnSelf(); - $this->transportBuilder->expects($this->once()) - ->method('setFrom') - ->with($from) - ->willReturnSelf(); - $this->transportBuilder->expects($this->once()) - ->method('addTo') - ->with($emails) - ->willReturnSelf(); - $this->transportBuilder->expects($this->once()) - ->method('getTransport') - ->willReturn($this->transport); - - $this->transport->expects($this->once()) - ->method('sendMessage') - ->willReturnSelf(); - - $this->eventManager->expects($this->once()) - ->method('dispatch') - ->with('wishlist_share', ['wishlist' => $this->wishlist]) - ->willReturnSelf(); - - $this->messageManager->expects($this->once()) - ->method('addSuccess') - ->with(__('Your wish list has been shared.')) - ->willReturnSelf(); - - $this->resultRedirect->expects($this->once()) - ->method('setPath') - ->with('*/*', ['wishlist_id' => $wishlistId]) - ->willReturnSelf(); - - $this->assertEquals($this->resultRedirect, $this->model->execute()); - } } diff --git a/app/code/Magento/Wishlist/Test/Unit/Model/Product/AttributeValueProviderTest.php b/app/code/Magento/Wishlist/Test/Unit/Model/Product/AttributeValueProviderTest.php new file mode 100644 index 000000000000..fb0113eb6ae7 --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Unit/Model/Product/AttributeValueProviderTest.php @@ -0,0 +1,177 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Test\Unit\Model\Product; + +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Wishlist\Model\Product\AttributeValueProvider; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject; + +/** + * AttributeValueProviderTest + */ +class AttributeValueProviderTest extends TestCase +{ + /** + * @var AttributeValueProvider|PHPUnit_Framework_MockObject_MockObject + */ + private $attributeValueProvider; + + /** + * @var CollectionFactory|PHPUnit_Framework_MockObject_MockObject + */ + private $productCollectionFactoryMock; + + /** + * @var Product|PHPUnit_Framework_MockObject_MockObject + */ + private $productMock; + + /** + * @var AdapterInterface|PHPUnit_Framework_MockObject_MockObject + */ + private $connectionMock; + + /** + * Set Up + * + * @return void + */ + protected function setUp() + { + $this->productCollectionFactoryMock = $this->createPartialMock( + CollectionFactory::class, + ['create'] + ); + $this->attributeValueProvider = new AttributeValueProvider( + $this->productCollectionFactoryMock + ); + } + + /** + * Get attribute text when the flat table is disabled + * + * @param int $productId + * @param string $attributeCode + * @param string $attributeText + * @return void + * @dataProvider attributeDataProvider + */ + public function testGetAttributeTextWhenFlatIsDisabled(int $productId, string $attributeCode, string $attributeText) + { + $this->productMock = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->setMethods(['getData']) + ->getMock(); + + $this->productMock->expects($this->any()) + ->method('getData') + ->with($attributeCode) + ->willReturn($attributeText); + + $productCollection = $this->getMockBuilder(Collection::class) + ->disableOriginalConstructor() + ->setMethods([ + 'addIdFilter', 'addStoreFilter', 'addAttributeToSelect', 'isEnabledFlat', 'getFirstItem' + ])->getMock(); + + $productCollection->expects($this->any()) + ->method('addIdFilter') + ->willReturnSelf(); + $productCollection->expects($this->any()) + ->method('addStoreFilter') + ->willReturnSelf(); + $productCollection->expects($this->any()) + ->method('addAttributeToSelect') + ->willReturnSelf(); + $productCollection->expects($this->any()) + ->method('isEnabledFlat') + ->willReturn(false); + $productCollection->expects($this->any()) + ->method('getFirstItem') + ->willReturn($this->productMock); + + $this->productCollectionFactoryMock->expects($this->atLeastOnce()) + ->method('create') + ->willReturn($productCollection); + + $actual = $this->attributeValueProvider->getRawAttributeValue($productId, $attributeCode); + + $this->assertEquals($attributeText, $actual); + } + + /** + * Get attribute text when the flat table is enabled + * + * @dataProvider attributeDataProvider + * @param int $productId + * @param string $attributeCode + * @param string $attributeText + * @return void + */ + public function testGetAttributeTextWhenFlatIsEnabled(int $productId, string $attributeCode, string $attributeText) + { + $this->connectionMock = $this->getMockBuilder(AdapterInterface::class)->getMockForAbstractClass(); + $this->connectionMock->expects($this->any()) + ->method('fetchRow') + ->willReturn([ + $attributeCode => $attributeText + ]); + $this->productMock = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->setMethods(['getData']) + ->getMock(); + $this->productMock->expects($this->any()) + ->method('getData') + ->with($attributeCode) + ->willReturn($attributeText); + + $productCollection = $this->getMockBuilder(Collection::class) + ->disableOriginalConstructor() + ->setMethods([ + 'addIdFilter', 'addStoreFilter', 'addAttributeToSelect', 'isEnabledFlat', 'getConnection' + ])->getMock(); + + $productCollection->expects($this->any()) + ->method('addIdFilter') + ->willReturnSelf(); + $productCollection->expects($this->any()) + ->method('addStoreFilter') + ->willReturnSelf(); + $productCollection->expects($this->any()) + ->method('addAttributeToSelect') + ->willReturnSelf(); + $productCollection->expects($this->any()) + ->method('isEnabledFlat') + ->willReturn(true); + $productCollection->expects($this->any()) + ->method('getConnection') + ->willReturn($this->connectionMock); + + $this->productCollectionFactoryMock->expects($this->atLeastOnce()) + ->method('create') + ->willReturn($productCollection); + + $actual = $this->attributeValueProvider->getRawAttributeValue($productId, $attributeCode); + + $this->assertEquals($attributeText, $actual); + } + + /** + * @return array + */ + public function attributeDataProvider(): array + { + return [ + [1, 'attribute_code', 'Attribute Text'] + ]; + } +} diff --git a/app/code/Magento/Wishlist/Test/Unit/Model/Rss/WishlistTest.php b/app/code/Magento/Wishlist/Test/Unit/Model/Rss/WishlistTest.php index 98d36dea28a2..fc43baa0a67d 100644 --- a/app/code/Magento/Wishlist/Test/Unit/Model/Rss/WishlistTest.php +++ b/app/code/Magento/Wishlist/Test/Unit/Model/Rss/WishlistTest.php @@ -278,15 +278,35 @@ protected function processWishlistItemDescription($wishlistModelMock, $staticArg public function testIsAllowed() { - $this->scopeConfig->expects($this->once())->method('getValue') + $customerId = 1; + $customerServiceMock = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); + $wishlist = $this->getMockBuilder(\Magento\Wishlist\Model\Wishlist::class)->setMethods( + ['getId', '__wakeup', 'getCustomerId', 'getItemCollection', 'getSharingCode'] + )->disableOriginalConstructor()->getMock(); + $wishlist->expects($this->once())->method('getCustomerId')->willReturn($customerId); + $this->wishlistHelperMock->expects($this->any())->method('getWishlist') + ->will($this->returnValue($wishlist)); + $this->wishlistHelperMock->expects($this->any()) + ->method('getCustomer') + ->will($this->returnValue($customerServiceMock)); + $customerServiceMock->expects($this->once())->method('getId')->willReturn($customerId); + $this->scopeConfig->expects($this->once())->method('isSetFlag') ->with('rss/wishlist/active', \Magento\Store\Model\ScopeInterface::SCOPE_STORE) ->will($this->returnValue(true)); + $this->assertTrue($this->model->isAllowed()); } public function testGetCacheKey() { - $this->assertEquals('rss_wishlist_data', $this->model->getCacheKey()); + $wishlistId = 1; + $wishlist = $this->getMockBuilder(\Magento\Wishlist\Model\Wishlist::class)->setMethods( + ['getId', '__wakeup', 'getCustomerId', 'getItemCollection', 'getSharingCode'] + )->disableOriginalConstructor()->getMock(); + $wishlist->expects($this->once())->method('getId')->willReturn($wishlistId); + $this->wishlistHelperMock->expects($this->any())->method('getWishlist') + ->will($this->returnValue($wishlist)); + $this->assertEquals('rss_wishlist_data_1', $this->model->getCacheKey()); } public function testGetCacheLifetime() diff --git a/app/code/Magento/Wishlist/Test/Unit/Plugin/Ui/DataProvider/WishlistSettingsTest.php b/app/code/Magento/Wishlist/Test/Unit/Plugin/Ui/DataProvider/WishlistSettingsTest.php new file mode 100644 index 000000000000..aa3b956e1215 --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Unit/Plugin/Ui/DataProvider/WishlistSettingsTest.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Test\Unit\Plugin\Ui\DataProvider; + +use Magento\Catalog\Ui\DataProvider\Product\Listing\DataProvider; +use Magento\Wishlist\Helper\Data; +use Magento\Wishlist\Plugin\Ui\DataProvider\WishlistSettings; + +/** + * Covers \Magento\Wishlist\Plugin\Ui\DataProvider\WishlistSettings + */ +class WishlistSettingsTest extends \PHPUnit\Framework\TestCase +{ + /** + * Testable Object + * + * @var WishlistSettings + */ + private $wishlistSettings; + + /** + * @var Data|\PHPUnit_Framework_MockObject_MockObject + */ + private $helperMock; + + /** + * Set Up + * + * @return void + */ + protected function setUp() + { + $this->helperMock = $this->createMock(Data::class); + $this->wishlistSettings = new WishlistSettings($this->helperMock); + } + + /** + * Test afterGetData method + * + * @return void + */ + public function testAfterGetData() + { + /** @var DataProvider|\PHPUnit_Framework_MockObject_MockObject $subjectMock */ + $subjectMock = $this->createMock(DataProvider::class); + $result = []; + $isAllow = true; + $this->helperMock->expects($this->once())->method('isAllow')->willReturn(true); + + $expected = ['allowWishlist' => $isAllow]; + $actual = $this->wishlistSettings->afterGetData($subjectMock, $result); + self::assertEquals($expected, $actual); + } +} diff --git a/app/code/Magento/Wishlist/ViewModel/AllowedQuantity.php b/app/code/Magento/Wishlist/ViewModel/AllowedQuantity.php new file mode 100644 index 000000000000..5e4c6b39f3c3 --- /dev/null +++ b/app/code/Magento/Wishlist/ViewModel/AllowedQuantity.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\ViewModel; + +use Magento\Catalog\Controller\Adminhtml\Product\Initialization\StockDataFilter; +use Magento\Catalog\Model\Product\Configuration\Item\ItemInterface; +use Magento\CatalogInventory\Model\StockRegistry; +use Magento\Framework\View\Element\Block\ArgumentInterface; + +/** + * ViewModel for Wishlist Cart Block + */ +class AllowedQuantity implements ArgumentInterface +{ + /** + * @var StockRegistry + */ + private $stockRegistry; + + /** + * @var ItemInterface + */ + private $item; + + /** + * @param StockRegistry $stockRegistry + */ + public function __construct(StockRegistry $stockRegistry) + { + $this->stockRegistry = $stockRegistry; + } + + /** + * Set product configuration item + * + * @param ItemInterface $item + * @return self + */ + public function setItem(ItemInterface $item): self + { + $this->item = $item; + return $this; + } + + /** + * Get product configuration item + * + * @return ItemInterface + */ + public function getItem(): ItemInterface + { + return $this->item; + } + + /** + * Get min and max qty for wishlist form. + * + * @return array + */ + public function getMinMaxQty(): array + { + $product = $this->getItem()->getProduct(); + $stockItem = $this->stockRegistry->getStockItem($product->getId(), $product->getStore()->getWebsiteId()); + $params = []; + + $params['minAllowed'] = (float)$stockItem->getMinSaleQty(); + if ($stockItem->getMaxSaleQty()) { + $params['maxAllowed'] = (float)$stockItem->getMaxSaleQty(); + } else { + $params['maxAllowed'] = (float)StockDataFilter::MAX_QTY_VALUE; + } + + return $params; + } +} diff --git a/app/code/Magento/Wishlist/composer.json b/app/code/Magento/Wishlist/composer.json index ce43f6faae20..c9c74c8859e4 100644 --- a/app/code/Magento/Wishlist/composer.json +++ b/app/code/Magento/Wishlist/composer.json @@ -15,7 +15,9 @@ "magento/module-rss": "*", "magento/module-sales": "*", "magento/module-store": "*", - "magento/module-ui": "*" + "magento/module-theme": "*", + "magento/module-ui": "*", + "magento/module-captcha": "*" }, "suggest": { "magento/module-configurable-product": "*", diff --git a/app/code/Magento/Wishlist/etc/adminhtml/system.xml b/app/code/Magento/Wishlist/etc/adminhtml/system.xml index 50ea4b01fe12..1e26a1195a7f 100644 --- a/app/code/Magento/Wishlist/etc/adminhtml/system.xml +++ b/app/code/Magento/Wishlist/etc/adminhtml/system.xml @@ -39,6 +39,10 @@ <label>Enabled</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> + <field id="show_in_sidebar" translate="label" type="select" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <label>Show in Sidebar</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + </field> </group> <group id="wishlist_link" translate="label" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="0"> <label>My Wish List Link</label> diff --git a/app/code/Magento/Wishlist/etc/config.xml b/app/code/Magento/Wishlist/etc/config.xml index 1fec2d1baf9d..dd88e63bc90a 100644 --- a/app/code/Magento/Wishlist/etc/config.xml +++ b/app/code/Magento/Wishlist/etc/config.xml @@ -10,6 +10,7 @@ <wishlist> <general> <active>1</active> + <show_in_sidebar>1</show_in_sidebar> </general> <email> <email_identity>general</email_identity> @@ -18,5 +19,21 @@ <text_limit>255</text_limit> </email> </wishlist> + <captcha translate="label"> + <frontend> + <areas> + <share_wishlist_form> + <label>Share Wishlist Form</label> + </share_wishlist_form> + </areas> + </frontend> + </captcha> + <customer> + <captcha> + <shown_to_logged_in_user> + <share_wishlist_form>1</share_wishlist_form> + </shown_to_logged_in_user> + </captcha> + </customer> </default> </config> diff --git a/app/code/Magento/Wishlist/etc/db_schema.xml b/app/code/Magento/Wishlist/etc/db_schema.xml index 73fcb7ce0cab..8a02f411ad0d 100644 --- a/app/code/Magento/Wishlist/etc/db_schema.xml +++ b/app/code/Magento/Wishlist/etc/db_schema.xml @@ -16,16 +16,16 @@ default="0" comment="Sharing flag (0 or 1)"/> <column xsi:type="varchar" name="sharing_code" nullable="true" length="32" comment="Sharing encrypted code"/> <column xsi:type="timestamp" name="updated_at" on_update="false" nullable="true" comment="Last updated date"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="wishlist_id"/> </constraint> - <constraint xsi:type="foreign" name="WISHLIST_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID" table="wishlist" + <constraint xsi:type="foreign" referenceId="WISHLIST_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID" table="wishlist" column="customer_id" referenceTable="customer_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="unique" name="WISHLIST_CUSTOMER_ID"> + <constraint xsi:type="unique" referenceId="WISHLIST_CUSTOMER_ID"> <column name="customer_id"/> </constraint> - <index name="WISHLIST_SHARED" indexType="btree"> + <index referenceId="WISHLIST_SHARED" indexType="btree"> <column name="shared"/> </index> </table> @@ -42,23 +42,23 @@ <column xsi:type="text" name="description" nullable="true" comment="Short description of wish list item"/> <column xsi:type="decimal" name="qty" scale="4" precision="12" unsigned="false" nullable="false" comment="Qty"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="wishlist_item_id"/> </constraint> - <constraint xsi:type="foreign" name="WISHLIST_ITEM_WISHLIST_ID_WISHLIST_WISHLIST_ID" table="wishlist_item" + <constraint xsi:type="foreign" referenceId="WISHLIST_ITEM_WISHLIST_ID_WISHLIST_WISHLIST_ID" table="wishlist_item" column="wishlist_id" referenceTable="wishlist" referenceColumn="wishlist_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="WISHLIST_ITEM_PRODUCT_ID_CATALOG_PRODUCT_ENTITY_ENTITY_ID" + <constraint xsi:type="foreign" referenceId="WISHLIST_ITEM_PRODUCT_ID_CATALOG_PRODUCT_ENTITY_ENTITY_ID" table="wishlist_item" column="product_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/> - <constraint xsi:type="foreign" name="WISHLIST_ITEM_STORE_ID_STORE_STORE_ID" table="wishlist_item" + <constraint xsi:type="foreign" referenceId="WISHLIST_ITEM_STORE_ID_STORE_STORE_ID" table="wishlist_item" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="SET NULL"/> - <index name="WISHLIST_ITEM_WISHLIST_ID" indexType="btree"> + <index referenceId="WISHLIST_ITEM_WISHLIST_ID" indexType="btree"> <column name="wishlist_id"/> </index> - <index name="WISHLIST_ITEM_PRODUCT_ID" indexType="btree"> + <index referenceId="WISHLIST_ITEM_PRODUCT_ID" indexType="btree"> <column name="product_id"/> </index> - <index name="WISHLIST_ITEM_STORE_ID" indexType="btree"> + <index referenceId="WISHLIST_ITEM_STORE_ID" indexType="btree"> <column name="store_id"/> </index> </table> @@ -71,10 +71,10 @@ comment="Product Id"/> <column xsi:type="varchar" name="code" nullable="false" length="255" comment="Code"/> <column xsi:type="text" name="value" nullable="true" comment="Value"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="option_id"/> </constraint> - <constraint xsi:type="foreign" name="FK_A014B30B04B72DD0EAB3EECD779728D6" table="wishlist_item_option" + <constraint xsi:type="foreign" referenceId="FK_A014B30B04B72DD0EAB3EECD779728D6" table="wishlist_item_option" column="wishlist_item_id" referenceTable="wishlist_item" referenceColumn="wishlist_item_id" onDelete="CASCADE"/> </table> diff --git a/app/code/Magento/Wishlist/etc/db_schema_whitelist.json b/app/code/Magento/Wishlist/etc/db_schema_whitelist.json index beaab64280a7..36a294f26e14 100644 --- a/app/code/Magento/Wishlist/etc/db_schema_whitelist.json +++ b/app/code/Magento/Wishlist/etc/db_schema_whitelist.json @@ -35,8 +35,7 @@ "PRIMARY": true, "WISHLIST_ITEM_WISHLIST_ID_WISHLIST_WISHLIST_ID": true, "WISHLIST_ITEM_PRODUCT_ID_CATALOG_PRODUCT_ENTITY_ENTITY_ID": true, - "WISHLIST_ITEM_STORE_ID_STORE_STORE_ID": true, - "WISHLIST_ITEM_PRODUCT_ID_SEQUENCE_PRODUCT_SEQUENCE_VALUE": true + "WISHLIST_ITEM_STORE_ID_STORE_STORE_ID": true } }, "wishlist_item_option": { @@ -49,7 +48,8 @@ }, "constraint": { "PRIMARY": true, - "FK_A014B30B04B72DD0EAB3EECD779728D6": true + "FK_A014B30B04B72DD0EAB3EECD779728D6": true, + "WISHLIST_ITEM_OPTION_PRODUCT_ID_CATALOG_PRODUCT_ENTITY_ENTITY_ID": true } } } \ No newline at end of file diff --git a/app/code/Magento/Wishlist/etc/module.xml b/app/code/Magento/Wishlist/etc/module.xml index c5ece20d7956..ab48ee89b747 100644 --- a/app/code/Magento/Wishlist/etc/module.xml +++ b/app/code/Magento/Wishlist/etc/module.xml @@ -10,6 +10,7 @@ <sequence> <module name="Magento_Customer"/> <module name="Magento_Catalog"/> + <module name="Magento_Captcha"/> </sequence> </module> </config> diff --git a/app/code/Magento/Wishlist/i18n/en_US.csv b/app/code/Magento/Wishlist/i18n/en_US.csv index a1d33cbd574f..a9acce448c80 100644 --- a/app/code/Magento/Wishlist/i18n/en_US.csv +++ b/app/code/Magento/Wishlist/i18n/en_US.csv @@ -118,4 +118,5 @@ Action,Action Configure,Configure Delete,Delete "Product Details and Comment","Product Details and Comment" -"You must login or register to add items to your wishlist.","You must login or register to add items to your wishlist." \ No newline at end of file +"You must login or register to add items to your wishlist.","You must login or register to add items to your wishlist." +"Show in Sidebar","Show in Sidebar" diff --git a/app/code/Magento/Wishlist/view/adminhtml/layout/customer_index_wishlist.xml b/app/code/Magento/Wishlist/view/adminhtml/layout/customer_index_wishlist.xml index 95b786603390..e364087405ed 100644 --- a/app/code/Magento/Wishlist/view/adminhtml/layout/customer_index_wishlist.xml +++ b/app/code/Magento/Wishlist/view/adminhtml/layout/customer_index_wishlist.xml @@ -9,7 +9,7 @@ <container name="root"> <block class="Magento\Backend\Block\Widget\Grid" name="customer.wishlist.edit.tab"> <arguments> - <argument name="dataSource" xsi:type="object">Magento\Wishlist\Model\ResourceModel\Item\Collection\Grid</argument> + <argument name="dataSource" xsi:type="object" shared="false">Magento\Wishlist\Model\ResourceModel\Item\Collection\Grid</argument> <argument name="id" xsi:type="string">wishlistGrid</argument> <argument name="use_ajax" xsi:type="string">true</argument> <argument name="default_sort" xsi:type="string">added_at</argument> diff --git a/app/code/Magento/Wishlist/view/frontend/layout/default.xml b/app/code/Magento/Wishlist/view/frontend/layout/default.xml index f655c7034fe6..c4f0d01707b2 100644 --- a/app/code/Magento/Wishlist/view/frontend/layout/default.xml +++ b/app/code/Magento/Wishlist/view/frontend/layout/default.xml @@ -18,7 +18,7 @@ </block> </referenceBlock> <referenceContainer name="sidebar.additional"> - <block class="Magento\Wishlist\Block\Customer\Sidebar" name="wishlist_sidebar" as="wishlist" template="Magento_Wishlist::sidebar.phtml"/> + <block class="Magento\Wishlist\Block\Customer\Sidebar" name="wishlist_sidebar" as="wishlist" template="Magento_Wishlist::sidebar.phtml" ifconfig="wishlist/general/show_in_sidebar"/> </referenceContainer> </body> </page> diff --git a/app/code/Magento/Wishlist/view/frontend/layout/wishlist_index_index.xml b/app/code/Magento/Wishlist/view/frontend/layout/wishlist_index_index.xml index 243a06062425..d4c3cc7fadd8 100644 --- a/app/code/Magento/Wishlist/view/frontend/layout/wishlist_index_index.xml +++ b/app/code/Magento/Wishlist/view/frontend/layout/wishlist_index_index.xml @@ -13,10 +13,12 @@ </referenceBlock> <referenceContainer name="content"> <block class="Magento\Wishlist\Block\Customer\Wishlist" name="customer.wishlist" template="Magento_Wishlist::view.phtml" cacheable="false"> + <block class="Magento\Theme\Block\Html\Pager" name="wishlist_item_pager"/> <block class="Magento\Wishlist\Block\Rss\Link" name="wishlist.rss.link" template="Magento_Wishlist::rss/wishlist.phtml"/> <block class="Magento\Wishlist\Block\Customer\Wishlist\Items" name="customer.wishlist.items" as="items" template="Magento_Wishlist::item/list.phtml" cacheable="false"> <block class="Magento\Wishlist\Block\Customer\Wishlist\Item\Column\Image" name="customer.wishlist.item.image" template="Magento_Wishlist::item/column/image.phtml" cacheable="false"/> <block class="Magento\Wishlist\Block\Customer\Wishlist\Item\Column\Info" name="customer.wishlist.item.name" template="Magento_Wishlist::item/column/name.phtml" cacheable="false"/> + <block class="Magento\Wishlist\Block\Customer\Wishlist\Item\Column" name="customer.wishlist.item.review" template="Magento_Wishlist::item/column/review.phtml" cacheable="false"/> <block class="Magento\Wishlist\Block\Customer\Wishlist\Item\Column\Cart" name="customer.wishlist.item.price" template="Magento_Wishlist::item/column/price.phtml" cacheable="false"> <block class="Magento\Catalog\Pricing\Render" name="product.price.render.wishlist"> <arguments> @@ -39,6 +41,7 @@ </block> <block class="Magento\Wishlist\Block\Customer\Wishlist\Item\Column\Cart" name="customer.wishlist.item.cart" template="Magento_Wishlist::item/column/cart.phtml" cacheable="false"> <arguments> + <argument name="allowedQuantityViewModel" xsi:type="object">Magento\Wishlist\ViewModel\AllowedQuantity</argument> <argument name="title" translate="true" xsi:type="string">Add to Cart</argument> </arguments> </block> diff --git a/app/code/Magento/Wishlist/view/frontend/templates/item/column/cart.phtml b/app/code/Magento/Wishlist/view/frontend/templates/item/column/cart.phtml index 9ea0d1a82323..f296b950f3ab 100644 --- a/app/code/Magento/Wishlist/view/frontend/templates/item/column/cart.phtml +++ b/app/code/Magento/Wishlist/view/frontend/templates/item/column/cart.phtml @@ -11,6 +11,9 @@ /** @var \Magento\Wishlist\Model\Item $item */ $item = $block->getItem(); $product = $item->getProduct(); +/** @var \Magento\Wishlist\ViewModel\AllowedQuantity $viewModel */ +$viewModel = $block->getData('allowedQuantityViewModel'); +$allowedQty = $viewModel->setItem($item)->getMinMaxQty(); ?> <?php foreach ($block->getChildNames() as $childName): ?> <?= /* @noEscape */ $block->getLayout()->renderElement($childName, false) ?> @@ -21,8 +24,8 @@ $product = $item->getProduct(); <div class="field qty"> <label class="label" for="qty[<?= $block->escapeHtmlAttr($item->getId()) ?>]"><span><?= $block->escapeHtml(__('Qty')) ?></span></label> <div class="control"> - <input type="number" data-role="qty" id="qty[<?= $block->escapeHtmlAttr($item->getId()) ?>]" class="input-text qty" data-validate="{'required-number':true,'validate-greater-than-zero':true}" - name="qty[<?= $block->escapeHtmlAttr($item->getId()) ?>]" value="<?= /* @noEscape */ (int)($block->getAddToCartQty($item) * 1) ?>"> + <input type="number" data-role="qty" id="qty[<?= $block->escapeHtmlAttr($item->getId()) ?>]" class="input-text qty" data-validate="{'required-number':true,'validate-greater-than-zero':true, 'validate-item-quantity':{'minAllowed':<?= /* @noEscape */ $allowedQty['minAllowed'] ?>,'maxAllowed':<?= /* @noEscape */ $allowedQty['maxAllowed'] ?>}}" + name="qty[<?= $block->escapeHtmlAttr($item->getId()) ?>]" value="<?= /* @noEscape */ (int)($block->getAddToCartQty($item) * 1) ?>" <?= $product->isSaleable() ? '' : 'disabled="disabled"' ?>> </div> </div> <?php endif; ?> diff --git a/app/code/Magento/Wishlist/view/frontend/templates/item/column/comment.phtml b/app/code/Magento/Wishlist/view/frontend/templates/item/column/comment.phtml index 17e2404ee23c..5ab5bc5422e7 100644 --- a/app/code/Magento/Wishlist/view/frontend/templates/item/column/comment.phtml +++ b/app/code/Magento/Wishlist/view/frontend/templates/item/column/comment.phtml @@ -17,6 +17,6 @@ $item = $block->getItem(); <span><?= $block->escapeHtml(__('Comment')) ?></span> </label> <div class="control"> - <textarea id="product-item-comment-<?= $block->escapeHtmlAttr($item->getWishlistItemId()) ?>" placeholder="<?= /* @noEscape */ $this->helper('Magento\Wishlist\Helper\Data')->defaultCommentString() ?>" name="description[<?= $block->escapeHtmlAttr($item->getWishlistItemId()) ?>]" title="<?= $block->escapeHtmlAttr(__('Comment')) ?>" class="product-item-comment"><?= ($block->escapeHtml($item->getDescription())) ?></textarea> + <textarea id="product-item-comment-<?= $block->escapeHtmlAttr($item->getWishlistItemId()) ?>" placeholder="<?= /* @noEscape */ $this->helper('Magento\Wishlist\Helper\Data')->defaultCommentString() ?>" name="description[<?= $block->escapeHtmlAttr($item->getWishlistItemId()) ?>]" title="<?= $block->escapeHtmlAttr(__('Comment')) ?>" class="product-item-comment" <?= $item->getProduct()->isSaleable() ? '' : 'disabled="disabled"' ?>><?= ($block->escapeHtml($item->getDescription())) ?></textarea> </div> </div> diff --git a/app/code/Magento/Wishlist/view/frontend/templates/item/column/review.phtml b/app/code/Magento/Wishlist/view/frontend/templates/item/column/review.phtml new file mode 100644 index 000000000000..9120cc9fa684 --- /dev/null +++ b/app/code/Magento/Wishlist/view/frontend/templates/item/column/review.phtml @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** @var \Magento\Wishlist\Block\Customer\Wishlist\Item\Column $block */ +$product = $block->getItem()->getProduct(); +?> +<?= $block->getReviewsSummaryHtml($product, 'short') ?> diff --git a/app/code/Magento/Wishlist/view/frontend/templates/sharing.phtml b/app/code/Magento/Wishlist/view/frontend/templates/sharing.phtml index 430ebd384c82..ff01cb4532cc 100644 --- a/app/code/Magento/Wishlist/view/frontend/templates/sharing.phtml +++ b/app/code/Magento/Wishlist/view/frontend/templates/sharing.phtml @@ -40,6 +40,7 @@ </div> <?php endif; ?> </fieldset> + <?= $block->getChildHtml('captcha'); ?> <div class="actions-toolbar"> <div class="primary"> <button type="submit" title="<?= $block->escapeHtmlAttr(__('Share Wish List')) ?>" class="action submit primary"> diff --git a/app/code/Magento/Wishlist/view/frontend/templates/view.phtml b/app/code/Magento/Wishlist/view/frontend/templates/view.phtml index 8b2e1b1c9d80..4f4a1d302c15 100644 --- a/app/code/Magento/Wishlist/view/frontend/templates/view.phtml +++ b/app/code/Magento/Wishlist/view/frontend/templates/view.phtml @@ -10,6 +10,7 @@ ?> <?php if ($this->helper('Magento\Wishlist\Helper\Data')->isAllow()) : ?> + <div class="toolbar wishlist-toolbar"><?= $block->getChildHtml('wishlist_item_pager'); ?></div> <?= ($block->getChildHtml('wishlist.rss.link')) ?> <form class="form-wishlist-items" id="wishlist-view-form" data-mage-init='{"wishlist":{ @@ -51,5 +52,6 @@ <input name="entity" value="<%- data.entity %>"> <% } %> </form> - </script> + </script> + <div class="toolbar wishlist-toolbar"><br><?= $block->getChildHtml('wishlist_item_pager'); ?></div> <?php endif ?> diff --git a/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js b/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js index 7ce934317263..b38c5c2cda3a 100644 --- a/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js +++ b/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js @@ -162,18 +162,12 @@ define([ $.each(elementValue, function (key, option) { data[elementName + '[' + option + ']'] = option; }); + } else if (elementName.substr(elementName.length - 2) == '[]') { //eslint-disable-line eqeqeq, max-depth + elementName = elementName.substring(0, elementName.length - 2); + + data[elementName + '[' + elementValue + ']'] = elementValue; } else { - if (elementValue) { //eslint-disable-line no-lonely-if - if (elementName.substr(elementName.length - 2) == '[]') { //eslint-disable-line eqeqeq, max-depth - elementName = elementName.substring(0, elementName.length - 2); - - if (elementValue) { //eslint-disable-line max-depth - data[elementName + '[' + elementValue + ']'] = elementValue; - } - } else { - data[elementName] = elementValue; - } - } + data[elementName] = elementValue; } return data; diff --git a/app/code/Magento/WishlistAnalytics/README.md b/app/code/Magento/WishlistAnalytics/README.md index 999fc835626d..1ad889598297 100644 --- a/app/code/Magento/WishlistAnalytics/README.md +++ b/app/code/Magento/WishlistAnalytics/README.md @@ -1,3 +1,3 @@ # Magento_WishlistAnalytics module -The Magento_WishlistAnalytics module configures data definitions for a data collection related to the Wishlist module entities to be used in [Advanced Reporting](http://devdocs.magento.com/guides/v2.2/advanced-reporting/modules.html). +The Magento_WishlistAnalytics module configures data definitions for a data collection related to the Wishlist module entities to be used in [Advanced Reporting](https://devdocs.magento.com/guides/v2.2/advanced-reporting/modules.html). diff --git a/app/code/Magento/WishlistAnalytics/composer.json b/app/code/Magento/WishlistAnalytics/composer.json index fc69afe2907a..747f2a4baaaa 100644 --- a/app/code/Magento/WishlistAnalytics/composer.json +++ b/app/code/Magento/WishlistAnalytics/composer.json @@ -4,7 +4,8 @@ "require": { "php": "~7.1.3||~7.2.0", "magento/framework": "*", - "magento/module-wishlist": "*" + "magento/module-wishlist": "*", + "magento/module-analytics": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/ProductResolver.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/ProductResolver.php new file mode 100644 index 000000000000..65c8498fc89a --- /dev/null +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/ProductResolver.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\WishlistGraphQl\Model\Resolver; + +use Magento\CatalogGraphQl\Model\ProductDataProvider; +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\Wishlist\Model\Item; + +/** + * Fetches the Product data according to the GraphQL schema + */ +class ProductResolver implements ResolverInterface +{ + /** + * @var ProductDataProvider + */ + private $productDataProvider; + + /** + * @param ProductDataProvider $productDataProvider + */ + public function __construct(ProductDataProvider $productDataProvider) + { + $this->productDataProvider = $productDataProvider; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['model'])) { + throw new LocalizedException(__('Missing key "model" in Wishlist Item value data')); + } + /** @var Item $wishlistItem */ + $wishlistItem = $value['model']; + + return $this->productDataProvider->getProductDataById((int)$wishlistItem->getProductId()); + } +} diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistItemsResolver.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistItemsResolver.php new file mode 100644 index 000000000000..dfbbf6543f66 --- /dev/null +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistItemsResolver.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\WishlistGraphQl\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\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Wishlist\Model\ResourceModel\Item\Collection as WishlistItemCollection; +use Magento\Wishlist\Model\ResourceModel\Item\CollectionFactory as WishlistItemCollectionFactory; +use Magento\Wishlist\Model\Item; +use Magento\Wishlist\Model\Wishlist; + +/** + * Fetches the Wishlist Items data according to the GraphQL schema + */ +class WishlistItemsResolver implements ResolverInterface +{ + /** + * @var WishlistItemCollectionFactory + */ + private $wishlistItemCollectionFactory; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @param WishlistItemCollectionFactory $wishlistItemCollectionFactory + * @param StoreManagerInterface $storeManager + */ + public function __construct( + WishlistItemCollectionFactory $wishlistItemCollectionFactory, + StoreManagerInterface $storeManager + ) { + $this->wishlistItemCollectionFactory = $wishlistItemCollectionFactory; + $this->storeManager = $storeManager; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['model'])) { + throw new LocalizedException(__('Missing key "model" in Wishlist value data')); + } + /** @var Wishlist $wishlist */ + $wishlist = $value['model']; + + $wishlistItems = $this->getWishListItems($wishlist); + + $data = []; + foreach ($wishlistItems as $wishlistItem) { + $data[] = [ + 'id' => $wishlistItem->getId(), + 'qty' => $wishlistItem->getData('qty'), + 'description' => $wishlistItem->getDescription(), + 'added_at' => $wishlistItem->getAddedAt(), + 'model' => $wishlistItem, + ]; + } + return $data; + } + + /** + * Get wishlist items + * + * @param Wishlist $wishlist + * @return Item[] + */ + private function getWishListItems(Wishlist $wishlist): array + { + /** @var WishlistItemCollection $wishlistItemCollection */ + $wishlistItemCollection = $this->wishlistItemCollectionFactory->create(); + $wishlistItemCollection + ->addWishlistFilter($wishlist) + ->addStoreFilter(array_map(function (StoreInterface $store) { + return $store->getId(); + }, $this->storeManager->getStores())) + ->setVisibilityFilter(); + return $wishlistItemCollection->getItems(); + } +} diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php new file mode 100644 index 000000000000..792928ab61aa --- /dev/null +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\WishlistGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Wishlist\Model\ResourceModel\Wishlist as WishlistResourceModel; +use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\WishlistFactory; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; + +/** + * Fetches the Wishlist data according to the GraphQL schema + */ +class WishlistResolver implements ResolverInterface +{ + /** + * @var WishlistResourceModel + */ + private $wishlistResource; + + /** + * @var WishlistFactory + */ + private $wishlistFactory; + + /** + * @param WishlistResourceModel $wishlistResource + * @param WishlistFactory $wishlistFactory + */ + public function __construct(WishlistResourceModel $wishlistResource, WishlistFactory $wishlistFactory) + { + $this->wishlistResource = $wishlistResource; + $this->wishlistFactory = $wishlistFactory; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + $customerId = $context->getUserId(); + + /* Guest checking */ + if (!$customerId && 0 === $customerId) { + throw new GraphQlAuthorizationException(__('The current user cannot perform operations on wishlist')); + } + /** @var Wishlist $wishlist */ + $wishlist = $this->wishlistFactory->create(); + $this->wishlistResource->load($wishlist, $customerId, 'customer_id'); + + if (null === $wishlist->getId()) { + return []; + } + + return [ + 'sharing_code' => $wishlist->getSharingCode(), + 'updated_at' => $wishlist->getUpdatedAt(), + 'items_count' => $wishlist->getItemsCount(), + 'name' => $wishlist->getName(), + 'model' => $wishlist, + ]; + } +} diff --git a/app/code/Magento/WishlistGraphQl/README.md b/app/code/Magento/WishlistGraphQl/README.md new file mode 100644 index 000000000000..9121593e6a75 --- /dev/null +++ b/app/code/Magento/WishlistGraphQl/README.md @@ -0,0 +1,4 @@ +# WishlistGraphQl + +**WishlistGraphQl** provides type information for the GraphQl module +to generate wishlist fields. diff --git a/app/code/Magento/WishlistGraphQl/composer.json b/app/code/Magento/WishlistGraphQl/composer.json new file mode 100644 index 000000000000..630ee97acc2e --- /dev/null +++ b/app/code/Magento/WishlistGraphQl/composer.json @@ -0,0 +1,24 @@ +{ + "name": "magento/module-wishlist-graph-ql", + "description": "N/A", + "type": "magento2-module", + "require": { + "php": "~7.1.3||~7.2.0", + "magento/framework": "*", + "magento/module-catalog-graph-ql": "*", + "magento/module-wishlist": "*", + "magento/module-store": "*" + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\WishlistGraphQl\\": "" + } + } +} diff --git a/app/code/Magento/WishlistGraphQl/etc/module.xml b/app/code/Magento/WishlistGraphQl/etc/module.xml new file mode 100644 index 000000000000..337623cc85a9 --- /dev/null +++ b/app/code/Magento/WishlistGraphQl/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_WishlistGraphQl" /> +</config> diff --git a/app/code/Magento/WishlistGraphQl/etc/schema.graphqls b/app/code/Magento/WishlistGraphQl/etc/schema.graphqls new file mode 100644 index 000000000000..2aa5f03a787d --- /dev/null +++ b/app/code/Magento/WishlistGraphQl/etc/schema.graphqls @@ -0,0 +1,22 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +type Query { + wishlist: WishlistOutput @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\WishlistResolver") @doc(description: "The wishlist query returns the contents of a customer's wish list") @cache(cacheable: false) +} + +type WishlistOutput { + items: [WishlistItem] @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\WishlistItemsResolver") @doc(description: "An array of items in the customer's wish list"), + items_count: Int @doc(description: "The number of items in the wish list"), + name: String @doc(description: "When multiple wish lists are enabled, the name the customer assigns to the wishlist"), + sharing_code: String @doc(description: "An encrypted code that Magento uses to link to the wish list"), + updated_at: String @doc(description: "The time of the last modification to the wish list") +} + +type WishlistItem { + id: Int @doc(description: "The wish list item ID") + qty: Float @doc(description: "The quantity of this wish list item"), + description: String @doc(description: "The customer's comment about this item"), + added_at: String @doc(description: "The time when the customer added the item to the wish list"), + product: ProductInterface @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\ProductResolver") +} \ No newline at end of file diff --git a/app/code/Magento/WishlistGraphQl/registration.php b/app/code/Magento/WishlistGraphQl/registration.php new file mode 100644 index 000000000000..c5d468421f96 --- /dev/null +++ b/app/code/Magento/WishlistGraphQl/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_WishlistGraphQl', __DIR__); diff --git a/app/design/adminhtml/Magento/backend/Magento_AdminNotification/web/css/source/_module.less b/app/design/adminhtml/Magento/backend/Magento_AdminNotification/web/css/source/_module.less index c1b684aef354..afd91ed3dbde 100644 --- a/app/design/adminhtml/Magento/backend/Magento_AdminNotification/web/css/source/_module.less +++ b/app/design/adminhtml/Magento/backend/Magento_AdminNotification/web/css/source/_module.less @@ -83,7 +83,7 @@ .message-system-short-wrapper { overflow: hidden; - padding: 0 1.5rem 0 @indent__l; + padding: 0 1.5rem 0 1rem; } .message-system-collapsible { diff --git a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/_footer.less b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/_footer.less index 2958708e51de..0244775fad0b 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/_footer.less +++ b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/_footer.less @@ -25,7 +25,7 @@ border-top: @footer__border-width solid @footer__border-color; color: @footer__color; margin-top: auto; - padding: 2.6rem 2rem 6rem 3rem; + padding: 2.6rem 3rem 6rem; a { .lib-link( diff --git a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/_menu.less b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/_menu.less index ba2810832600..d0b17b3439d6 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/_menu.less +++ b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/_menu.less @@ -15,9 +15,9 @@ @menu__background-color: @color-very-dark-grayish-orange; -@menu-logo__padding-bottom: 2.2rem; +@menu-logo__padding-bottom: 1.7rem; @menu-logo__outer-size: @menu-logo__padding-top + @menu-logo-img__height + @menu-logo__padding-bottom; -@menu-logo__padding-top: 1.2rem; +@menu-logo__padding-top: 1.7rem; @menu-logo-img__height: 4.1rem; @menu-logo-img__width: 3.5rem; diff --git a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/header/_headings-group.less b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/header/_headings-group.less index 832c66b7988e..bf7ee7850f9d 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/header/_headings-group.less +++ b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/header/_headings-group.less @@ -42,4 +42,5 @@ color: @page-title__color; font-size: @page-title__font-size; margin-bottom: 0; + word-break: break-all; } diff --git a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/header/actions-group/_notifications.less b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/header/actions-group/_notifications.less index c9c97930297c..40ebb6f3c456 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/header/actions-group/_notifications.less +++ b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/header/actions-group/_notifications.less @@ -97,9 +97,11 @@ display: inline-block; font-size: @notifications__font-size; font-weight: @font-weight__bold; + height: 18px; left: 50%; margin-left: .3em; margin-top: -1.1em; + min-width: 18px; padding: .3em .5em; position: absolute; top: 50%; diff --git a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/header/actions-group/_search.less b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/header/actions-group/_search.less index 5e65faec60d4..ddc3cb455402 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/header/actions-group/_search.less +++ b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/header/actions-group/_search.less @@ -153,7 +153,7 @@ background-color: transparent; border: 1px solid transparent; font-size: @search-global-input__font-size; - height: @search-global-input__height; + height: @search-global-input__height + .2; padding: @search-global-input__padding-top @search-global-input__padding-side @search-global-input__padding-bottom; position: absolute; right: 0; diff --git a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_actions-bar.less b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_actions-bar.less index 07050c1e5111..8499ecaa48c1 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_actions-bar.less +++ b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_actions-bar.less @@ -44,7 +44,10 @@ .page-actions { @_page-action__indent: 1.3rem; - float: right; + + &.floating-header { + &:extend(.page-actions-buttons all); + } .page-main-actions & { &._fixed { diff --git a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_collapsible-blocks.less b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_collapsible-blocks.less index 6420738c6fb9..dec35d136483 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_collapsible-blocks.less +++ b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_collapsible-blocks.less @@ -127,6 +127,21 @@ } } } + + td.admin__collapsible-block-wrapper { + .admin__collapsible-title { + &:before { + content: @icon-expand-open__content; + } + } + &._show { + .admin__collapsible-title { + &:before { + content: @icon-expand-close__content; + } + } + } + } } &.fieldset-wrapper { @@ -147,6 +162,14 @@ &.collapsible-block-wrapper-last { border-bottom: 0; } + + .admin__dynamic-rows.admin__control-collapsible { + td { + &.admin__collapsible-block-wrapper { + border-bottom: none; + } + } + } } .admin__collapsible-content { @@ -322,7 +345,7 @@ } .value { - padding-right: 4rem; + padding-right: 2rem; } } @@ -472,6 +495,8 @@ width: 44%; &.with-tooltip { + font-size: 0; + .tooltip { bottom: 0; float: right; diff --git a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_page-nav.less b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_page-nav.less index 42b3ecfb7112..070ee6347508 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_page-nav.less +++ b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_page-nav.less @@ -203,7 +203,7 @@ font-weight: @font-weight__heavier; line-height: @line-height__s; margin: 0 0 -1px; - padding: @admin__page-nav-link__padding; + padding: 2rem 0 2rem 1rem; transition: @admin__page-nav-transition; word-wrap: break-word; } diff --git a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_store-scope.less b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_store-scope.less index c0c94eaaf350..05e6350c88d8 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_store-scope.less +++ b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/_store-scope.less @@ -38,7 +38,7 @@ .admin__legend { .admin__field-tooltip { margin-left: -@indent__base; - margin-top: -.2rem; + margin-top: .5rem; } } } diff --git a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/actions-bar/_store-switcher.less b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/actions-bar/_store-switcher.less index 80bebb22a904..22a584f1c8b8 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/actions-bar/_store-switcher.less +++ b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/actions-bar/_store-switcher.less @@ -7,13 +7,13 @@ // Main elements -> Actions bar -> Store Switcher // _____________________________________________ -// ToDo UI: Consist old styles, should be changed with new design +// ToDo UI: Consist old styles, should be changed with new design .store-switcher { - color: @text__color; // ToDo UI: Delete with admin scope + color: @text__color; float: left; font-size: round(@font-size__base - .1rem, 1); - margin-top: .7rem; + margin-top: .59rem; .admin__action-dropdown { background-color: @page-main-actions__background-color; @@ -47,8 +47,8 @@ width: 7px; } &::-webkit-scrollbar-thumb { - border-radius: 4px; background-color: rgba(0, 0, 0, .5); + border-radius: 4px; } li { @@ -235,11 +235,11 @@ .store-view { &:not(.store-switcher) { float: left; + margin-top: 13px; } .store-switcher-label { display: inline-block; - margin-top: @indent__s; } } diff --git a/app/design/adminhtml/Magento/backend/Magento_Backend/web/images/logo-magento.png b/app/design/adminhtml/Magento/backend/Magento_Backend/web/images/logo-magento.png index d9395c938cbf..0cca183e08da 100644 Binary files a/app/design/adminhtml/Magento/backend/Magento_Backend/web/images/logo-magento.png and b/app/design/adminhtml/Magento/backend/Magento_Backend/web/images/logo-magento.png differ diff --git a/app/design/adminhtml/Magento/backend/Magento_Catalog/web/css/source/_module.less b/app/design/adminhtml/Magento/backend/Magento_Catalog/web/css/source/_module.less index 335595025407..ffbbaeb08416 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Catalog/web/css/source/_module.less +++ b/app/design/adminhtml/Magento/backend/Magento_Catalog/web/css/source/_module.less @@ -15,6 +15,14 @@ } } +.catalog-category-edit { + .admin__grid-control { + .admin__grid-control-value { + display: none; + } + } +} + .product-composite-configure-inner { .admin__control-text { &.qty { diff --git a/app/design/adminhtml/Magento/backend/Magento_Customer/web/css/source/_module.less b/app/design/adminhtml/Magento/backend/Magento_Customer/web/css/source/_module.less index a80cc9a5163f..c8f2530df22e 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Customer/web/css/source/_module.less +++ b/app/design/adminhtml/Magento/backend/Magento_Customer/web/css/source/_module.less @@ -3,14 +3,77 @@ // * See COPYING.txt for license details. // */ -// General rule hides group legend and shows first field label instead -// in app/design/adminhtml/Magento/backend/web/css/source/forms/_fields.less -// This must be reset for Customer Address page -.address-item-edit-content { +.customer_form_areas_address_address_customer_address_update_modal_update_customer_address_form_loader { .admin__field { - legend { - &.admin__field-label { - opacity: 1; + .admin__field { + .admin__field-label { + background: none; + } + } + } +} + +.customer-address-form { + + *, + *:after, + *:before { + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; + } + + address { + font-style: normal; + } + + .customer-default-address-wrapper { + align-items: flex-start; + display: flex; + float: left; + position: relative; + width: 50%; + + .action-additional { + margin: 2px 0 0 2px; + } + } + + .edit-default-billing-address-button, + .edit-default-shipping-address-button { + position: absolute; + } + + .edit-default-billing-address-button { + left: 210px; + } + + .edit-default-shipping-address-button { + left: 230px; + } + + .customer_form_areas_address_address_customer_address_listing { + clear: both; + } + + .add-new-address-button { + clear: both; + float: right; + margin-bottom: 30px; + position: relative; + } + + .address-information { + float: left; + margin-bottom: 20px; + + address { + float: left; + + .address_caption { + font-size: 18px; + font-weight: bold; + margin-bottom: 16px; } } } diff --git a/app/design/adminhtml/Magento/backend/Magento_Review/web/css/source/_module.less b/app/design/adminhtml/Magento/backend/Magento_Review/web/css/source/_module.less index 17be2ca70607..08606402f7a0 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Review/web/css/source/_module.less +++ b/app/design/adminhtml/Magento/backend/Magento_Review/web/css/source/_module.less @@ -34,7 +34,7 @@ .admin__field-control { direction: rtl; display: inline-block; - margin: -4px 0 0; + margin: -1px 0 0; unicode-bidi: bidi-override; vertical-align: top; width: 125px; diff --git a/app/design/adminhtml/Magento/backend/Magento_Rma/web/css/source/_module.less b/app/design/adminhtml/Magento/backend/Magento_Rma/web/css/source/_module.less index c405707ee7bb..16c84047b529 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Rma/web/css/source/_module.less +++ b/app/design/adminhtml/Magento/backend/Magento_Rma/web/css/source/_module.less @@ -7,13 +7,21 @@ .rma-request-details, .rma-wrapper .order-shipping-address { float: left; - #mix-grid .width(6,12); + /** + * @codingStandardsIgnoreStart + */ + #mix-grid .width(6, 12); + //@codingStandardsIgnoreEnd } .rma-confirmation, - .rma-wrapper .order-return-address { + .rma-wrapper .order-return-address, .rma-wrapper .order-shipping-method { float: right; - #mix-grid .width(6,12); + /** + * @codingStandardsIgnoreStart + */ + #mix-grid .width(6, 12); + //@codingStandardsIgnoreEnd } } diff --git a/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/_order.less b/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/_order.less index 984556816b03..ffa5ee963952 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/_order.less +++ b/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/_order.less @@ -94,6 +94,14 @@ margin: 0; padding: 0; } + .admin__data-grid-pager-wrap{ + .selectmenu { + margin-bottom: 10px; + } + } + .data-grid-search-control-wrap { + margin-bottom: 10px; + } } // @@ -300,38 +308,4 @@ } } -// ToDo UI: review the collapsible block -//.order-subtotal { -// .summary-collapse { -// cursor: pointer; -// display: inline-block; -// &:before { -// @iconsize: 16px; -// -// -webkit-font-smoothing: antialiased; -// background: #f2ebde; -// border-radius: 2px; -// border: 1px solid #ada89e; -// color: #816063; -// content: '+'; -// display: inline-block; -// font-size: @iconsize; -// font-style: normal; -// font-weight: normal; -// height: @iconsize; -// line-height: @iconsize; -// margin-right: 7px; -// overflow: hidden; -// speak: none; -// text-indent: 0; -// vertical-align: top; -// width: @iconsize; -// } -// &:hover:before { -// background: #cac3b4; -// } -// } -// &.show-details .summary-collapse:before { -// content: '\e03a'; -// } -//} +// ToDo: MAGETWO-32299 UI: review the collapsible block 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 e14bcbcddd47..f66e94940c55 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_order-account.less +++ b/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_order-account.less @@ -27,3 +27,19 @@ width: 50%; } } + +.page-create-order { + .order-details { + &:not(.order-details-existing-customer) { + .order-account-information { + .field-email { + margin-left: -30px; + } + + .field-group_id { + margin-right: 30px; + } + } + } + } +} diff --git a/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_order-comments.less b/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_order-comments.less index 2f6aec0315e3..5bcf4d4953cc 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_order-comments.less +++ b/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_order-comments.less @@ -49,7 +49,7 @@ margin: 0 0 @order-create-sidebar__margin; .lib-typography( @_font-size: 1.9rem, - @_color: @color-brown-darkie, + @_color: @color-brown-darker, @_font-weight: @font-weight__semibold, @_line-height: @line-height__s, @_font-family: false, diff --git a/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_payment-shipping.less b/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_payment-shipping.less index 1e40fe1fa340..029594625ed1 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_payment-shipping.less +++ b/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_payment-shipping.less @@ -9,6 +9,7 @@ .admin__payment-method-wrapper { margin: 0; + width: calc(50% - @indent__l); .admin__field { margin-left: 0; &:first-child { @@ -34,6 +35,7 @@ margin: 0; } +.order-billing-method-summary, .order-shipping-method-summary { padding-top: @field-option__padding-top; } @@ -43,6 +45,7 @@ position: relative; } +.order-billing-method-summary, .order-shipping-method-summary, .order-shipping-method-info { .action-default { diff --git a/app/design/adminhtml/Magento/backend/Magento_Staging/web/css/source/module/_staging-preview.less b/app/design/adminhtml/Magento/backend/Magento_Staging/web/css/source/module/_staging-preview.less index 3e1f4e75031d..ae13c479cea4 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Staging/web/css/source/module/_staging-preview.less +++ b/app/design/adminhtml/Magento/backend/Magento_Staging/web/css/source/module/_staging-preview.less @@ -16,7 +16,7 @@ @staging-preview-header__font-size: 1.3rem; @staging-preview-header-item__active__background-color: @color-brownie-almost; -@staging-preview-header-item-actions__border-color: @color-darkie-gray; +@staging-preview-header-item-actions__border-color: @color-darker-gray; @staging-preview-form-element__background-color: @color-very-dark-brownie; @staging-preview-form-element__border-color: @color-lighter-grayish-almost; diff --git a/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/module/_data-grid.less b/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/module/_data-grid.less index d55608ade4a0..946d11db2d1a 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/module/_data-grid.less +++ b/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/module/_data-grid.less @@ -392,6 +392,7 @@ body._in-resize { overflow: hidden; padding: 0; vertical-align: top; + vertical-align: middle; width: @control-checkbox-radio__size + @data-grid-checkbox-cell-inner__padding-horizontal * 2; &:hover { @@ -1074,8 +1075,10 @@ body._in-resize { } .data-grid-checkbox-cell-inner { - margin: @data-grid-checkbox-cell-inner__padding-top @data-grid-checkbox-cell-inner__padding-horizontal .9rem; + display: unset; + margin: 0 @data-grid-checkbox-cell-inner__padding-horizontal 0; padding: 0; + text-align: center; } // Content Hierarchy specific diff --git a/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/module/data-grid/data-grid-header/_data-grid-filters.less b/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/module/data-grid/data-grid-header/_data-grid-filters.less index 6e03e1d0ceba..e37e08f3b667 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/module/data-grid/data-grid-header/_data-grid-filters.less +++ b/app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/module/data-grid/data-grid-header/_data-grid-filters.less @@ -99,7 +99,7 @@ } .action-menu { - max-height: 3.85rem * @data-grid-search-control-action-menu-item__quantity; // ToDo UI: change static item height + max-height: 3.85rem * @data-grid-search-control-action-menu-item__quantity; // @todo: change static item height overflow-y: auto; z-index: @data-grid-search-menu__z-index; } @@ -354,6 +354,7 @@ .admin__current-filters-list-wrap { width: 100%; + word-break: break-all; } .admin__current-filters-list { diff --git a/app/design/adminhtml/Magento/backend/Magento_VisualMerchandiser/web/css/source/_module.less b/app/design/adminhtml/Magento/backend/Magento_VisualMerchandiser/web/css/source/_module.less index 1cd867efdd13..554b6394a109 100644 --- a/app/design/adminhtml/Magento/backend/Magento_VisualMerchandiser/web/css/source/_module.less +++ b/app/design/adminhtml/Magento/backend/Magento_VisualMerchandiser/web/css/source/_module.less @@ -68,12 +68,14 @@ a { color: @color-gray85; + cursor: move; display: block; float: left; text-decoration: none; } a:last-child { + cursor: pointer; float: right; } } diff --git a/app/design/adminhtml/Magento/backend/etc/view.xml b/app/design/adminhtml/Magento/backend/etc/view.xml index f10f7789b088..18c2d8f1b172 100644 --- a/app/design/adminhtml/Magento/backend/etc/view.xml +++ b/app/design/adminhtml/Magento/backend/etc/view.xml @@ -23,6 +23,8 @@ </images> </media> <exclude> + <item type="file">Lib::mage/captcha.js</item> + <item type="file">Lib::mage/captcha.min.js</item> <item type="file">Lib::mage/common.js</item> <item type="file">Lib::mage/cookies.js</item> <item type="file">Lib::mage/dataPost.js</item> diff --git a/app/design/adminhtml/Magento/backend/web/app/setup/styles/less/components/tooltips/_tooltips.less b/app/design/adminhtml/Magento/backend/web/app/setup/styles/less/components/tooltips/_tooltips.less index c535047e3768..8b26177a05cc 100644 --- a/app/design/adminhtml/Magento/backend/web/app/setup/styles/less/components/tooltips/_tooltips.less +++ b/app/design/adminhtml/Magento/backend/web/app/setup/styles/less/components/tooltips/_tooltips.less @@ -10,7 +10,7 @@ @tooltip__background-color: @color-white; @tooltip__border-color: @color-gray68; @tooltip__border-radius: 0; -@tooltip__color: @color-brown-darkie; +@tooltip__color: @color-brown-darker; @tooltip__max-width: 31rem; @tooltip__opacity: .9; @tooltip__shadow-color: @color-gray80; @@ -93,7 +93,7 @@ } } -// ToDo UI: Only right tooltip arrow is styled, if we will neeed more need to add another ones +// ToDo UI: Only right tooltip arrow is styled, if we will need more need to add another ones .tooltip { &.top { diff --git a/app/design/adminhtml/Magento/backend/web/app/setup/styles/less/lib/_variables.less b/app/design/adminhtml/Magento/backend/web/app/setup/styles/less/lib/_variables.less index 39d7be029f81..be1378638180 100644 --- a/app/design/adminhtml/Magento/backend/web/app/setup/styles/less/lib/_variables.less +++ b/app/design/adminhtml/Magento/backend/web/app/setup/styles/less/lib/_variables.less @@ -28,7 +28,7 @@ @color-green-apple: #79a22e; @color-green-islamic: #090; @color-dark-brownie: #41362f; -@color-brown-darkie: #41362f; +@color-brown-darker: #41362f; @color-phoenix-down: #e04f00; @color-phoenix: #eb5202; @color-phoenix-almost-rise: #ef672f; diff --git a/app/design/adminhtml/Magento/backend/web/app/setup/styles/less/pages/_common.less b/app/design/adminhtml/Magento/backend/web/app/setup/styles/less/pages/_common.less index 2b33d1fa542b..b71c94f2917c 100644 --- a/app/design/adminhtml/Magento/backend/web/app/setup/styles/less/pages/_common.less +++ b/app/design/adminhtml/Magento/backend/web/app/setup/styles/less/pages/_common.less @@ -19,6 +19,12 @@ padding-top: @main__indent-top; } +.menu-wrapper { + .logo-static { + pointer-events: none; + } +} + // // Header // _____________________________________________ diff --git a/app/design/adminhtml/Magento/backend/web/app/updater/styles/less/pages/_extension-manager.less b/app/design/adminhtml/Magento/backend/web/app/updater/styles/less/pages/_extension-manager.less index 911ef55f3f2e..30500569c82a 100644 --- a/app/design/adminhtml/Magento/backend/web/app/updater/styles/less/pages/_extension-manager.less +++ b/app/design/adminhtml/Magento/backend/web/app/updater/styles/less/pages/_extension-manager.less @@ -15,7 +15,7 @@ @extension-manager-title__background-color: @color-white-fog; @extension-manager-title__border-color: @color-gray89; -@extension-manager-title__color: @color-brown-darkie; +@extension-manager-title__color: @color-brown-darker; @extension-manager-button__border-color: @color-gray68; diff --git a/app/design/adminhtml/Magento/backend/web/css/source/_tabs.less b/app/design/adminhtml/Magento/backend/web/css/source/_tabs.less index 475d3914a5ff..5658214a7698 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/_tabs.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/_tabs.less @@ -45,13 +45,13 @@ } .ui-tabs-anchor { - color: @color-brown-darkie; + color: @color-brown-darker; display: block; padding: 1.5rem 1.8rem 1.3rem; text-decoration: none; &:hover { // ToDo UI: should be deleted with old styles - color: @color-brown-darkie; + color: @color-brown-darker; text-decoration: none; } } diff --git a/app/design/adminhtml/Magento/backend/web/css/source/_typography.less b/app/design/adminhtml/Magento/backend/web/css/source/_typography.less index 54726d2d34bd..1f7d7f879c4a 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/_typography.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/_typography.less @@ -71,7 +71,7 @@ h1 { .lib-typography( @_font-size: 2.8rem, - @_color: @color-brown-darkie, + @_color: @color-brown-darker, @_font-weight: @font-weight__regular, @_line-height: @line-height__s, @_font-family: false, @@ -84,7 +84,7 @@ h2 { .lib-typography( @_font-size: 2rem, - @_color: @color-brown-darkie, + @_color: @color-brown-darker, @_font-weight: @font-weight__regular, @_line-height: @line-height__s, @_font-family: false, @@ -97,7 +97,7 @@ h3 { .lib-typography( @_font-size: 1.7rem, - @_color: @color-brown-darkie, + @_color: @color-brown-darker, @_font-weight: @font-weight__semibold, @_line-height: @line-height__s, @_font-family: false, diff --git a/app/design/adminhtml/Magento/backend/web/css/source/actions/_actions-dropdown.less b/app/design/adminhtml/Magento/backend/web/css/source/actions/_actions-dropdown.less index cd089232412d..d1fe33c4fe77 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 7b99d4f136d2..61bd94cf3f49 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 @@ -179,7 +179,7 @@ .admin__action-multiselect-search-label { display: block; font-size: 1.5rem; - height: 1em; + height: 1.3em; overflow: hidden; position: absolute; right: 2.2rem; @@ -199,8 +199,8 @@ .admin__action-multiselect-empty-area { color: @color-gray65-almost; - padding-top: 20px; padding-bottom: 20px; + padding-top: 20px; text-align: center; vertical-align: middle; } @@ -338,7 +338,7 @@ border-top: @action-multiselect-tree-lines; height: 1px; top: @action-multiselect-menu-item__padding + @action-multiselect-tree-arrow__size/2; - width: @action-multiselect-tree-menu-item__margin-left + @action-multiselect-menu-item__padding; + width: @action-multiselect-tree-menu-item__margin-left; } // Vertical dotted line diff --git a/app/design/adminhtml/Magento/backend/web/css/source/actions/_actions-select.less b/app/design/adminhtml/Magento/backend/web/css/source/actions/_actions-select.less index 1cc61bef5da0..1c45fe6946ba 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/actions/_actions-select.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/actions/_actions-select.less @@ -7,6 +7,14 @@ // Actions -> Action select // _____________________________________________ +// +// Variables +// _____________________________________________ + +@_dropdown__padding-right: @action__height; +@_triangle__height: @button-marker-triangle__height; +@_triangle__width: @button-marker-triangle__width; + // Action select have the same visual styles and functionality as native <select> .action-select-wrap { @_action-select__border-color: @button__border-color; @@ -18,9 +26,9 @@ .action-select { .action-toggle-triangle( - @_dropdown__padding-right: @_action-select-toggle__size; - @_triangle__height: @button-marker-triangle__height; - @_triangle__width: @button-marker-triangle__width; + @_dropdown__padding-right; + @_triangle__height; + @_triangle__width; ); .lib-text-overflow-ellipsis(); @@ -108,12 +116,9 @@ min-width: 100%; position: static; - ._parent._visible { - position: relative; - } - .action-submenu { position: absolute; + right: -100%; } } } diff --git a/app/design/adminhtml/Magento/backend/web/css/source/components/_file-insertion.less b/app/design/adminhtml/Magento/backend/web/css/source/components/_file-insertion.less index 88962a1019a1..84d9cb153089 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/components/_file-insertion.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/components/_file-insertion.less @@ -44,6 +44,7 @@ margin: 0 @indent__xs 15px 0; overflow: hidden; padding: 3px; + text-overflow: ellipsis; width: 100px; &.selected { diff --git a/app/design/adminhtml/Magento/backend/web/css/source/components/_file-uploader.less b/app/design/adminhtml/Magento/backend/web/css/source/components/_file-uploader.less index 9a88d5e3593b..ec276449263a 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/components/_file-uploader.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/components/_file-uploader.less @@ -48,7 +48,7 @@ @data-grid-file-uploader-menu-button__width: 2rem; -@data-grid-file-uploader-upload-icon__color: @color-darkie-gray; +@data-grid-file-uploader-upload-icon__color: @color-darker-gray; @data-grid-file-uploader-upload-icon__hover__color: @color-very-dark-gray; @data-grid-file-uploader-upload-icon__line-height: 48px; diff --git a/app/design/adminhtml/Magento/backend/web/css/source/components/_messages.less b/app/design/adminhtml/Magento/backend/web/css/source/components/_messages.less index de24bf89620d..15cd29588589 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/components/_messages.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/components/_messages.less @@ -76,7 +76,8 @@ position: absolute; speak: none; text-shadow: none; - top: 1.3rem; + top: 50%; + margin-top: -1.25rem; width: auto; } } @@ -110,7 +111,7 @@ content: @alert-icon__error__content; font-size: @alert-icon__error__font-size; left: 2.2rem; - margin-top: 0.5rem; + margin-top: -1.1rem; } } diff --git a/app/design/adminhtml/Magento/backend/web/css/source/components/_modals_extend.less b/app/design/adminhtml/Magento/backend/web/css/source/components/_modals_extend.less index 95d7f8f65fdc..efc747e4d714 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/components/_modals_extend.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/components/_modals_extend.less @@ -146,13 +146,13 @@ } .action-close { - padding: @modal-popup__padding; + padding: @modal-popup__padding - 2; &:active, &:focus { background: transparent; - padding-right: @modal-popup__padding + (@modal-action-close__font-size - @modal-action-close__active__font-size) / 2; - padding-top: @modal-popup__padding + (@modal-action-close__font-size - @modal-action-close__active__font-size) / 2; + padding-right: @modal-popup__padding - 2; + padding-top: @modal-popup__padding - 2; } } } diff --git a/app/design/adminhtml/Magento/backend/web/css/source/components/_popups.less b/app/design/adminhtml/Magento/backend/web/css/source/components/_popups.less index d1d1ff989163..bcdc96b6c175 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/components/_popups.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/components/_popups.less @@ -264,7 +264,7 @@ } } - #contents-uploader { + .contents-uploader { margin: 0 0 @indent__base; } @@ -299,6 +299,7 @@ margin: 0 @indent__xs 15px 0; overflow: hidden; padding: 3px; + text-overflow: ellipsis; width: 100px; &.selected { @@ -310,7 +311,7 @@ } } - #contents-uploader { + .contents-uploader { &:extend(.abs-clearfix all); } diff --git a/app/design/adminhtml/Magento/backend/web/css/source/forms/_controls.less b/app/design/adminhtml/Magento/backend/web/css/source/forms/_controls.less index bb51abaa0f15..6c3756370d9c 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/forms/_controls.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/forms/_controls.less @@ -75,9 +75,9 @@ max-width: 100%; min-width: 8.5rem; - padding-bottom: @field-control__padding-bottom - .1rem; + padding-bottom: @field-control__padding-bottom; padding-right: 4.4rem; // Distance between select switch and inner text - padding-top: @field-control__padding-top - .1rem; + padding-top: @field-control__padding-top; transition: @smooth__border-color; &:hover { @@ -85,7 +85,7 @@ cursor: pointer; } - &:focus { + &:active { background-image+: url('../images/arrows-bg.svg'); background-position+: ~'calc(100% - 12px)' 13px; diff --git a/app/design/adminhtml/Magento/backend/web/css/source/forms/_fields.less b/app/design/adminhtml/Magento/backend/web/css/source/forms/_fields.less index c87b8f2ca5cc..66c9086c15aa 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 @@ -59,7 +59,11 @@ } .abs-field-no-label { + /** + *@codingStandardsIgnoreStart + */ #mix-grid .return_length(@field-label-grid__column, @field-grid__columns, '+'); + //@codingStandardsIgnoreEnd margin-left: @_length; } @@ -77,7 +81,7 @@ min-width: 0; padding: 0; - // Filedset section + // Fieldset section .fieldset-wrapper { &.admin__fieldset-section { > .fieldset-wrapper-title { @@ -117,6 +121,9 @@ > .admin__field-control { #mix-grid .column(@field-control-grid__column, @field-grid__columns); + input[type="checkbox"] { + margin-top: @indent__s; + } } > .admin__field-label { @@ -143,6 +150,22 @@ } } } + + &.field-currently_used_for { + > .admin__field-control { + .admin__field { + margin-top: 8px; + } + } + } + } + &.composite-bundle { + .admin__field-control { + padding-top: 7px; + } + .admin__field-option { + padding-top: 0; + } } } @@ -166,6 +189,13 @@ .admin__control-text, .admin__control-textarea { width: 100%; + &.disabled { + background-color: #e9e9e9; + border-color: #adadad; + color: #303030; + cursor: not-allowed; + opacity: .5; + } } } @@ -277,6 +307,12 @@ .admin__fieldset > & { margin-bottom: 3rem; position: relative; + + &.field-import_file { + .input-file { + margin-top: 6px; + } + } } // Hide group legend and show first field label instead @@ -506,10 +542,8 @@ & > .admin__field { &:first-child { position: static; - & > .admin__field-label { #mix-grid .column(@field-label-grid__column, @field-grid__columns); - background: @color-white; cursor: pointer; left: 0; position: absolute; @@ -625,10 +659,11 @@ &.admin__field { > .admin__field-control { &:extend(.abs-field-size-small all); - float: left; position: relative; + display: inline-block; } } + + .admin__field:last-child { width: auto; @@ -661,9 +696,12 @@ margin: 0; opacity: 1; position: static; - text-align: left; + width: 100%; } } + & > .admin__field-label { + text-align: left; + } &:nth-child(n + 2) { &:not(.admin__field-option):not(.admin__field-group-show-label):not(.admin__field-date) { diff --git a/app/design/adminhtml/Magento/backend/web/css/source/forms/_temp.less b/app/design/adminhtml/Magento/backend/web/css/source/forms/_temp.less index df031bebeb24..71f57b765ff0 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/forms/_temp.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/forms/_temp.less @@ -11,7 +11,7 @@ .admin__fieldset-wrapper-title { &:extend(.abs-clearfix all); border-bottom: 1px solid @color-gray80; - line-height: 1.2; + line-height: 1.4; margin-bottom: 0; padding: 14px 0 16px; @@ -162,7 +162,7 @@ @_icon-font-line-height: 16px, @_icon-font-text-hide: true, @_icon-font-position: after, - @_icon-font-color: @color-brown-darkie + @_icon-font-color: @color-brown-darker ); span { @@ -175,7 +175,7 @@ z-index: 2; &:after { - color: darken(@color-brown-darkie, 20%); + color: darken(@color-brown-darker, 20%); } // @Todo ui - testing solution to show action hint without title attribute @@ -253,7 +253,7 @@ label.mage-error { .captcha-reload { float: right; - vertical-align: middle; + margin-top: 15px; } } } @@ -470,8 +470,6 @@ label.mage-error { } .admin__data-grid-header-row { - &:extend(.abs-cleafix); - .action-select-multiselect { -webkit-appearance: menulist-button; appearance: menulist-button; @@ -552,7 +550,7 @@ label.mage-error { } .admin__control-select-placeholder { - color: @color-darkie-gray; + color: @color-darker-gray; font-weight: @font-weight__bold; } } diff --git a/app/design/adminhtml/Magento/backend/web/css/source/forms/fields/_control-table.less b/app/design/adminhtml/Magento/backend/web/css/source/forms/fields/_control-table.less index a9035a9a7e47..697d11fb57d6 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/forms/fields/_control-table.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/forms/fields/_control-table.less @@ -118,7 +118,7 @@ } &._fit { - width: 1px; + width: auto; } } diff --git a/app/design/adminhtml/Magento/backend/web/css/source/forms/fields/_field-tooltips.less b/app/design/adminhtml/Magento/backend/web/css/source/forms/fields/_field-tooltips.less old mode 100644 new mode 100755 index 8184a5c4bb24..befd27fa57df --- a/app/design/adminhtml/Magento/backend/web/css/source/forms/fields/_field-tooltips.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/forms/fields/_field-tooltips.less @@ -22,7 +22,7 @@ @field-tooltip-content__width: 32rem; @field-tooltip-content__z-index: 1; -@field-tooltip-action__margin-left: 2rem; +@field-tooltip-action__margin-left: 0; // // Form Fields diff --git a/app/design/adminhtml/Magento/backend/web/css/source/variables/_colors.less b/app/design/adminhtml/Magento/backend/web/css/source/variables/_colors.less index b477384096b0..ad57d7b47113 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/variables/_colors.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/variables/_colors.less @@ -8,7 +8,7 @@ // _____________________________________________ @color-brown-dark: #4a3f39; -@color-brown-darkie: #41362f; +@color-brown-darker: #41362f; @color-very-dark-gray-black: #303030; @color-very-dark-gray-black2: #35302c; @color-very-dark-grayish-orange: #373330; @@ -23,7 +23,7 @@ @color-brownie-vanilla: #736963; @color-dark-gray0: #7f7c7a; @color-dark-gray: #808080; -@color-darkie-gray: #8a837f; +@color-darker-gray: #8a837f; @color-gray65: #a6a6a6; @color-gray65-almost: #a79d95; @color-gray65-lighten: #aaa6a0; @@ -73,5 +73,5 @@ @primary__color: @color-phoenix; @success__color: @color-green-apple; -@text__color: @color-brown-darkie; +@text__color: @color-brown-darker; @border__color: @color-gray89; diff --git a/app/design/adminhtml/Magento/backend/web/css/source/variables/_data-grid.less b/app/design/adminhtml/Magento/backend/web/css/source/variables/_data-grid.less index 69393a62200c..40831684adce 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/variables/_data-grid.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/variables/_data-grid.less @@ -30,7 +30,7 @@ @data-grid-td__odd__update__active__background-color: darken(@data-grid-td__update__active__background-color, 10%); @data-grid-td__odd__update__upcoming__background-color: darken(@data-grid-td__update__upcoming__background-color, 10%); -@data-grid-th__border-color: @color-darkie-gray; +@data-grid-th__border-color: @color-darker-gray; @data-grid-th__border-style: solid; @data-grid-th__background-color: @color-brownie; @data-grid-th__color: @color-white; diff --git a/app/design/adminhtml/Magento/backend/web/css/styles-old.less b/app/design/adminhtml/Magento/backend/web/css/styles-old.less index 74098f127eb7..c9e56abbba8d 100644 --- a/app/design/adminhtml/Magento/backend/web/css/styles-old.less +++ b/app/design/adminhtml/Magento/backend/web/css/styles-old.less @@ -783,7 +783,7 @@ box-sizing: border-box; border: 1px solid #adadad; border-radius: 1px; - padding: 4px; + padding: .6rem 1rem .6rem; color: #303030; background-color: @color-white; font-weight: 500; @@ -820,7 +820,7 @@ padding-right: 44px; - &:focus { + &:active { background-image+: url('../images/arrows-bg.svg'); background-position+: ~'calc(100% - 12px)' 13px; @@ -1188,7 +1188,7 @@ } // - // Collapsable fieldset-wrapper + // collapsible fieldset-wrapper // -------------------------------------- // Fieldset styles in another fieldset @@ -2544,6 +2544,8 @@ .order-summary:after, .order-methods:before, .order-methods:after, + .payment-methods:before, + .payment-methods:after, .grid-actions:before, .grid-actions:after, .fieldset-wrapper-title:before, @@ -2559,6 +2561,7 @@ .order-addresses:after, .order-summary:after, .order-methods:after, + .payment-methods:after, .grid-actions:after, .fieldset-wrapper-title:after { clear: both; @@ -2735,7 +2738,8 @@ // --------------------------------------------- #widget_instace_tabs_properties_section_content .widget-option-label { - margin-top: 6px; + margin-top: 7px; + display: inline-block; } // @@ -2898,6 +2902,7 @@ .style28(); } + #order-billing-method-summary a, #order-shipping-method-summary a { .style3(); } @@ -2936,7 +2941,8 @@ margin: 0 0 0 20px; } - #order-data .order-methods ul { + #order-data .order-methods ul, + #order-data .payment-methods ul { list-style: none; margin: 0; padding: 0; @@ -3840,6 +3846,26 @@ .rule-param-edit .element { display: inline; + position: relative; + } + + .rule-param-edit .element input.input-date, + .rule-param-edit .element input.input-date[readonly] { + background-color: @color-white; + min-width: 140px; + width: 140px !important; + cursor: pointer; + text-align: center; + opacity: 1; + margin-right: 10px; + padding-right: 40px; + + + .ui-datepicker-trigger { + position: absolute; + width: 140px; + text-align: right; + left: 0; + } } .rule-param-edit .element .addafter { diff --git a/app/design/adminhtml/Magento/backend/web/js/theme.js b/app/design/adminhtml/Magento/backend/web/js/theme.js index a2511e27e9f6..8e3b89dcf7e4 100644 --- a/app/design/adminhtml/Magento/backend/web/js/theme.js +++ b/app/design/adminhtml/Magento/backend/web/js/theme.js @@ -599,7 +599,7 @@ define('js/theme', [ ], function ($, keyboardHandler) { 'use strict'; - /* @TODO refactor collapsable as widget and avoid logic binding with such a general selectors */ + /* @TODO refactor collapsible as widget and avoid logic binding with such a general selectors */ $('.collapse').collapsable(); $.each($('.entry-edit'), function (i, entry) { diff --git a/app/design/frontend/Magento/blank/Magento_Banner/web/css/source/_widgets.less b/app/design/frontend/Magento/blank/Magento_Banner/web/css/source/_widgets.less index 80ac77e42fc0..c961d49c5b58 100644 --- a/app/design/frontend/Magento/blank/Magento_Banner/web/css/source/_widgets.less +++ b/app/design/frontend/Magento/blank/Magento_Banner/web/css/source/_widgets.less @@ -8,7 +8,6 @@ // _____________________________________________ & when (@media-common = true) { - .block-banners, .block-banners-inline { &:extend(.abs-margin-for-blocks-and-widgets); @@ -22,7 +21,7 @@ } .banner-item-content { - .lib-css(margin-bottom, @indent__base); + margin-bottom: @indent__base; img { display: block; diff --git a/app/design/frontend/Magento/blank/Magento_Braintree/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Braintree/web/css/source/_module.less index b70bab6f320d..8c1ec09afec1 100644 --- a/app/design/frontend/Magento/blank/Magento_Braintree/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Braintree/web/css/source/_module.less @@ -9,9 +9,9 @@ @braintree-input-border__color: @color-gray76; -@braintree-error__color: @color-red10; -@braintree-focus__color: @color-blue2; -@braintree-success__color: @color-dark-green1; +@braintree-error__color: @message-error__color; +@braintree-focus__color: @theme__color__primary-alt; +@braintree-success__color: @message-success__color; @braintree-paypal-icon__height: 16px; @braintree-paypal-icon__width: 16px; diff --git a/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_module.less index 08a9b6197792..299c13883206 100644 --- a/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_module.less @@ -488,6 +488,7 @@ .product-items-names { .product-item { + display: flex; margin-bottom: @indent__s; } @@ -506,17 +507,6 @@ min-height: inherit; } } - - // - // Category page 1 column layout - // --------------------------------------------- - - .catalog-category-view.page-layout-1column { - .column.main { - min-height: inherit; - } - } - } // diff --git a/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_widgets.less b/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_widgets.less index 42b1bf2d0cc0..7181606090cc 100644 --- a/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_widgets.less +++ b/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_widgets.less @@ -23,6 +23,15 @@ } .block.widget { + .products-grid .product-item { + margin-left: 2%; + width: calc(~'(100% - 2%)/2'); + + &:nth-child(2n + 1) { + margin-left: 0; + } + } + .product-item-info { width: auto; } @@ -60,6 +69,15 @@ .page-layout-3columns .block.widget .products-grid .product-item { width: 100%/3; } + + .page-layout-1column .block.widget .products-grid .product-item { + margin-left: 2%; + width: calc(~'(100% - 4%)/3'); + + &:nth-child(3n + 1) { + margin-left: 0; + } + } } // @@ -82,7 +100,16 @@ } .page-layout-1column .block.widget .products-grid .product-item { - width: 100%/4; + margin-left: 2%; + width: calc(~'(100% - 6%)/4'); + + &:nth-child(3n + 1) { + margin-left: 2%; + } + + &:nth-child(4n + 1) { + margin-left: 0; + } } .page-layout-3columns .block.widget .products-grid .product-item { @@ -96,11 +123,11 @@ } .page-layout-1column .block.widget .products-grid .product-item { - margin-left: calc(~'(100% - 5 * (100%/6)) / 4'); - width: 100%/6; + margin-left: 2%; + width: calc(~'(100% - 8%)/5'); &:nth-child(4n + 1) { - margin-left: calc(~'(100% - 5 * (100%/6)) / 4'); + margin-left: 2%; } &:nth-child(5n + 1) { diff --git a/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/module/_listings.less b/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/module/_listings.less index 8b727b0d9c28..b7af69fd5ca8 100644 --- a/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/module/_listings.less +++ b/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/module/_listings.less @@ -29,15 +29,23 @@ .product { &-items { + font-size: 0; &:extend(.abs-reset-list all); } &-item { + font-size: 1.4rem; vertical-align: top; .products-grid & { display: inline-block; - width: 100%/2; + margin-left: 2%; + padding: 0; + width: calc(~'(100% - 2%) / 2'); + } + + &:nth-child(2n + 1) { + margin-left: 0; } &:extend(.abs-add-box-sizing all); @@ -63,14 +71,26 @@ } &-actions { - display: none; + font-size: 0; + + > * { + font-size: 1.4rem; + } .actions-secondary { + display: inline-block; + font-size: 1.4rem; + vertical-align: middle; + white-space: nowrap; > button.action { .lib-button-reset(); } > .action { + line-height: 35px; + text-align: center; + width: 35px; + &:extend(.abs-actions-addto-gridlist all); &:before { margin: 0; @@ -81,6 +101,10 @@ } } } + + .actions-primary { + display: inline-block; + } } &-description { @@ -192,19 +216,6 @@ } } - .column.main { - .product { - &-items { - margin-left: -@indent__base; - } - - &-item { - padding-left: @indent__base; - } - } - - } - .price-container { .price { .lib-font-size(14); @@ -303,18 +314,10 @@ } .actions-primary + .actions-secondary { - display: table-cell; - padding-left: 5px; - white-space: nowrap; - width: 50%; > * { white-space: normal; } } - - .actions-primary { - display: table-cell; - } } } } @@ -330,7 +333,13 @@ .page-products.page-layout-3columns { .products-grid { .product-item { - width: 100%/3; + margin-left: 2%; + padding: 0; + width: calc(~'(100% - 4%) / 3'); + + &:nth-child(3n + 1) { + margin-left: 0; + } } } } @@ -344,7 +353,13 @@ .page-products { .products-grid { .product-item { - width: 100%/3; + margin-left: 2%; + padding: 0; + width: calc(~'(100% - 4%) / 3'); + + &:nth-child(3n + 1) { + margin-left: 0; + } } } } @@ -395,9 +410,13 @@ } .product-item { - margin-left: calc(~'(100% - 4 * 23.233%) / 3'); + margin-left: 2%; padding: 0; - width: 23.233%; + width: calc(~'(100% - 6%) / 4'); + + &:nth-child(3n + 1) { + margin-left: 2%; + } &:nth-child(4n + 1) { margin-left: 0; diff --git a/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/module/_toolbar.less b/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/module/_toolbar.less index aceccb06d47f..8e7f36389c41 100644 --- a/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/module/_toolbar.less +++ b/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/module/_toolbar.less @@ -4,14 +4,17 @@ // */ // -// Common -// _____________________________________________ +// Variables +// --------------------------------------------- @toolbar-mode-icon-font-size: 24px; @toolbar-element-background: @panel__background-color; -& when (@media-common = true) { +// +// Common +// _____________________________________________ +& when (@media-common = true) { .page-products { .columns { position: relative; @@ -62,6 +65,11 @@ .products.wrapper ~ .toolbar & { display: none; } + + .sorter-action { + position: relative; + top: -2px; + } } .sorter-options { @@ -72,12 +80,12 @@ .sorter-action { vertical-align: top; .lib-icon-font( - @icon-arrow-up, - @_icon-font-size: 28px, - @_icon-font-line-height: 32px, - @_icon-font-color: @header-icons-color, - @_icon-font-color-hover: @header-icons-color-hover, - @_icon-font-text-hide: true + @icon-arrow-up, + @_icon-font-size: 28px, + @_icon-font-line-height: 32px, + @_icon-font-color: @header-icons-color, + @_icon-font-color-hover: @header-icons-color-hover, + @_icon-font-text-hide: true ); } @@ -99,7 +107,7 @@ } .limiter-label { - font-weight: 400; + font-weight: @font-weight__regular; } .limiter { @@ -176,11 +184,11 @@ } .lib-icon-font( - @icon-grid, - @_icon-font-size: @toolbar-mode-icon-font-size, - @_icon-font-text-hide: true, - @_icon-font-color: @text__color__muted, - @_icon-font-color-hover: @text__color__muted + @icon-grid, + @_icon-font-size: @toolbar-mode-icon-font-size, + @_icon-font-text-hide: true, + @_icon-font-color: @text__color__muted, + @_icon-font-color-hover: @text__color__muted ); } diff --git a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/_minicart.less b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/_minicart.less index 7bd27106b67d..65f3eeef63b0 100644 --- a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/_minicart.less +++ b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/_minicart.less @@ -107,7 +107,7 @@ @_toggle-selector: ~'.action.showcart', @_options-selector: ~'.block-minicart', @_dropdown-list-width: 320px, - @_dropdown-list-position-right: 0px, + @_dropdown-list-position-right: 0, @_dropdown-list-pointer-position: right, @_dropdown-list-pointer-position-left-right: 26px, @_dropdown-list-z-index: 101, @@ -148,10 +148,10 @@ .action { &.close { .lib-button-icon( - @icon-remove, - @_icon-font-size: 32px, - @_icon-font-line-height: 32px, - @_icon-font-text-hide: true + @icon-remove, + @_icon-font-size: 32px, + @_icon-font-line-height: 32px, + @_icon-font-text-hide: true ); .lib-button-reset(); height: 40px; @@ -254,12 +254,12 @@ .toggle { .lib-icon-font( - @_icon-font-content: @icon-down, - @_icon-font-size: 28px, - @_icon-font-line-height: 16px, - @_icon-font-text-hide: false, - @_icon-font-position: after, - @_icon-font-display: block + @_icon-font-content: @icon-down, + @_icon-font-size: 28px, + @_icon-font-line-height: 16px, + @_icon-font-text-hide: false, + @_icon-font-position: after, + @_icon-font-display: block ); cursor: pointer; position: relative; @@ -274,8 +274,8 @@ &.active { > .toggle { .lib-icon-font-symbol( - @_icon-font-content: @icon-up, - @_icon-font-position: after + @_icon-font-content: @icon-up, + @_icon-font-position: after ); } } @@ -304,6 +304,7 @@ .weee[data-label] { .lib-font-size(11); + .label { &:extend(.abs-no-display all); } @@ -317,12 +318,12 @@ .product.options { .tooltip.toggle { .lib-icon-font( - @icon-down, - @_icon-font-size: 28px, - @_icon-font-line-height: 28px, - @_icon-font-text-hide: true, - @_icon-font-margin: -3px 0 0 7px, - @_icon-font-position: after + @icon-down, + @_icon-font-size: 28px, + @_icon-font-line-height: 28px, + @_icon-font-text-hide: true, + @_icon-font-margin: -3px 0 0 7px, + @_icon-font-position: after ); .details { @@ -341,7 +342,7 @@ .item-qty { margin-right: @indent__s; text-align: center; - width: 40px; + width: 45px; } .update-cart-item { @@ -357,19 +358,19 @@ &.edit, &.delete { .lib-icon-font( - @icon-settings, - @_icon-font-size: 28px, - @_icon-font-line-height: 28px, - @_icon-font-text-hide: true, - @_icon-font-color: @color-gray19, - @_icon-font-color-hover: @color-gray19, - @_icon-font-color-active: @color-gray19 + @icon-settings, + @_icon-font-size: 28px, + @_icon-font-line-height: 28px, + @_icon-font-text-hide: true, + @_icon-font-color: @color-gray19, + @_icon-font-color-hover: @color-gray19, + @_icon-font-color-active: @color-gray19 ); } &.delete { .lib-icon-font-symbol( - @_icon-font-content: @icon-trash + @_icon-font-content: @icon-trash ); } } @@ -399,6 +400,7 @@ .media-width(@extremum, @break) when (@extremum = 'min') and (@break = @screen__m) { .minicart-wrapper { margin-left: 13px; + .block-minicart { right: -15px; width: 390px; diff --git a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_fields.less b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_fields.less index 4479c070a4e1..8dec680b5872 100644 --- a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_fields.less +++ b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_fields.less @@ -55,31 +55,3 @@ } } } - -// -// Desktop -// _____________________________________________ - -.media-width(@extremum, @break) when (@extremum = 'min') and (@break = @screen__m) { - // ToDo UI: remove with global blank theme .field.required update - .opc-wrapper { - .fieldset { - > .field { - &.required, - &._required { - position: relative; - - > label { - padding-right: 25px; - - &:after { - margin-left: @indent__s; - position: absolute; - top: 9px; - } - } - } - } - } - } -} diff --git a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_order-summary.less b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_order-summary.less index 43c2ad50c7a6..3394e8a4b50c 100644 --- a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_order-summary.less +++ b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_order-summary.less @@ -118,6 +118,10 @@ .product { position: relative; + .item-options { + &:extend(.abs-product-options-list all); + &:extend(.abs-add-clearfix all); + } } } diff --git a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_payment-options.less b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_payment-options.less index 3ce46a73a11c..4d04b6e0b965 100644 --- a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_payment-options.less +++ b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_payment-options.less @@ -137,6 +137,7 @@ } } + .captcha, .number { .input-text { width: 225px; diff --git a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_payments.less b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_payments.less index 5f8134193c67..35445b0989e8 100644 --- a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_payments.less +++ b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_payments.less @@ -209,6 +209,13 @@ .fieldset { > .field { margin: 0 0 @indent__base; + + &.choice { + &:before { + padding: 0; + width: 0; + } + } &.type { .control { diff --git a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_progress-bar.less b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_progress-bar.less index 301517c6d0cd..4792cb7b1792 100644 --- a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_progress-bar.less +++ b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_progress-bar.less @@ -16,7 +16,7 @@ @checkout-progress-bar-item__color: @primary__color; @checkout-progress-bar-item__margin: @indent__s; @checkout-progress-bar-item__width: 185px; -@checkout-progress-bar-item__active__background-color: @color-orange-red1; +@checkout-progress-bar-item__active__background-color: @active__color; @checkout-progress-bar-item__complete__color: @link__color; @checkout-progress-bar-item-element__height: @checkout-progress-bar-item-element__width; diff --git a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_shipping.less b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_shipping.less index 0a463a95e318..af6127fd2ca9 100644 --- a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_shipping.less +++ b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_shipping.less @@ -22,7 +22,7 @@ @checkout-shipping-item__width: 100%/3; @checkout-shipping-item-tablet__width: 100%/2; @checkout-shipping-item-mobile__width: 100%; -@checkout-shipping-item__active__border-color: @color-orange-red1; +@checkout-shipping-item__active__border-color: @active__color; @checkout-shipping-item-icon__selected__height: 27px; @checkout-shipping-item-icon__selected__width: 29px; @@ -98,11 +98,6 @@ text-align: center; top: 0; } - - .action-select-shipping-item { - &:extend(.abs-no-display-s all); - visibility: hidden; - } } } diff --git a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_sidebar-shipping-information.less b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_sidebar-shipping-information.less index b54c0a264a03..0f2a7abcbaa1 100644 --- a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_sidebar-shipping-information.less +++ b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_sidebar-shipping-information.less @@ -67,3 +67,11 @@ } } } + +.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__s) { + .opc-block-shipping-information { + .shipping-information-title { + font-size: 2.3rem; + } + } +} diff --git a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_tooltip.less b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_tooltip.less index 273f626ec03d..423923d5e645 100644 --- a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_tooltip.less +++ b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_tooltip.less @@ -55,6 +55,10 @@ } } + .label { + .lib-visually-hidden(); + } + .field-tooltip-action { .lib-icon-font( @checkout-tooltip-icon__content, @@ -65,6 +69,10 @@ @_icon-font-color-active: false ); + &:before { + padding-left : 1px; + } + &:focus { ._keyfocus & { .lib-css(z-index, @checkout-tooltip__hover__z-index); @@ -137,10 +145,42 @@ } } -.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { +.media-width(@extremum, @break) when (@extremum = 'max') and (@break >= @screen__m) { .field-tooltip { .field-tooltip-content { + .lib-css(right, @checkout-tooltip-content-mobile__right); + .lib-css(top, @checkout-tooltip-content-mobile__top); + left: auto; &:extend(.abs-checkout-tooltip-content-position-top-mobile all); } } } + +// +// Tablet +// _____________________________________________ + +@media only screen and (max-width: @screen__m) { + .field-tooltip .field-tooltip-content { + left: auto; + right: -10px; + top: 40px; + } + .field-tooltip .field-tooltip-content::before, + .field-tooltip .field-tooltip-content::after { + border: 10px solid transparent; + height: 0; + left: auto; + margin-top: -21px; + right: 10px; + top: 0; + width: 0; + } + .field-tooltip .field-tooltip-content::before { + .lib-css(border-bottom-color, @checkout-tooltip-content__border-color); + } + .field-tooltip .field-tooltip-content::after { + .lib-css(border-bottom-color, @checkout-tooltip-content__background-color); + top: 1px; + } +} diff --git a/app/design/frontend/Magento/blank/Magento_Customer/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Customer/web/css/source/_module.less index 3ffaeb82cdc2..213b8131815b 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 @@ -11,7 +11,7 @@ @account-nav-color: false; @account-nav-current-border: 3px solid transparent; -@account-nav-current-border-color: @color-orange-red1; +@account-nav-current-border-color: @active__color; @account-nav-current-color: false; @account-nav-current-font-weight: @font-weight__semibold; @@ -65,7 +65,7 @@ } .form-address-edit { - #region_id { + .region_id { display: none; } @@ -165,7 +165,7 @@ // Checkout address (create shipping address) .field.street { - .field.additional { + .field { .label { &:extend(.abs-visually-hidden all); } @@ -367,8 +367,8 @@ } .account { - .page.messages { - margin-bottom: @indent__xl; + .messages { + margin-bottom: 0; } .toolbar { @@ -388,6 +388,17 @@ position: relative; } } + + .form.search.advanced { + .field.price { + .with-addon { + .input-text { + flex-basis: auto; + width: 100%; + } + } + } + } } // @@ -421,7 +432,7 @@ > .field { > .control { - width: 55%; + width: 80%; } } } @@ -451,7 +462,9 @@ .form.password.reset, .form.send.confirmation, .form.password.forget, - .form.create.account { + .form.create.account, + .form.search.advanced, + .form.form-orders-search { min-width: 600px; width: 50%; } diff --git a/app/design/frontend/Magento/blank/Magento_Msrp/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Msrp/web/css/source/_module.less index f0dd8a957e9b..6e2069c6e88e 100644 --- a/app/design/frontend/Magento/blank/Magento_Msrp/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Msrp/web/css/source/_module.less @@ -55,6 +55,10 @@ } } + .map-fallback-price { + display: none; + } + .map-old-price { text-decoration: none; diff --git a/app/design/frontend/Magento/blank/Magento_MultipleWishlist/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_MultipleWishlist/web/css/source/_module.less index 145dc10263fb..c572c983d80d 100644 --- a/app/design/frontend/Magento/blank/Magento_MultipleWishlist/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_MultipleWishlist/web/css/source/_module.less @@ -185,7 +185,7 @@ } .form-wishlist-search { - .lib-css(margin-bottom, @indent__l*2); + margin-bottom: @indent__l * 2; max-width: 500px; .fieldset { @@ -216,8 +216,9 @@ .block-wishlist-info-items { .block-title { - .lib-css(margin-bottom, @indent__base); .lib-font-size(22); + margin-bottom: @indent__base; + > strong { font-weight: @font-weight__light; } @@ -349,7 +350,7 @@ .product { &-item { &-checkbox { - left: 20px; + left: 0; position: absolute; top: 20px; } @@ -388,16 +389,16 @@ .media-width(@extremum, @break) when (@extremum = 'min') and (@break = @screen__m) { .wishlist { &.window.popup { + .field { + .lib-form-field-type-revert(@_type: block); + } + bottom: auto; .lib-css(top, @desktop-popup-position-top); .lib-css(left, @desktop-popup-position-left); .lib-css(margin-left, @desktop-popup-margin-left); .lib-css(width, @desktop-popup-width); right: auto; - - .field { - .lib-form-field-type-revert(@_type: block); - } } } diff --git a/app/design/frontend/Magento/blank/Magento_Multishipping/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Multishipping/web/css/source/_module.less index 96648f504af8..46f9661d8281 100644 --- a/app/design/frontend/Magento/blank/Magento_Multishipping/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Multishipping/web/css/source/_module.less @@ -47,7 +47,7 @@ } .error-block { - color: @color-red10; + color: @error__color; .error-label { font-weight: @font-weight__bold; @@ -222,7 +222,7 @@ } .error-description { - color: @color-red10; + color: @error__color; font-weight: @font-weight__regular; margin-bottom: @indent__s; margin-top: -@indent__s; diff --git a/app/design/frontend/Magento/blank/Magento_Review/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Review/web/css/source/_module.less index 7d5b79bc5a09..69ec01d71e10 100644 --- a/app/design/frontend/Magento/blank/Magento_Review/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Review/web/css/source/_module.less @@ -15,9 +15,9 @@ // _____________________________________________ & when (@media-common = true) { - .rating-summary { .lib-rating-summary(); + .rating-result { margin-left: -5px; } @@ -165,10 +165,10 @@ .review-details { .customer-review-rating { - .lib-css(margin-bottom, @indent__base); + margin-bottom: @indent__base; .item { - .lib-css(margin-bottom, @indent__s); + margin-bottom: @indent__s; &:last-child { margin-bottom: 0; @@ -178,12 +178,12 @@ .review-title { .lib-heading(h3); - .lib-css(font-weight, @font-weight__semibold); - .lib-css(margin-bottom, @indent__base); + font-weight: @font-weight__semibold; + margin-bottom: @indent__base; } .review-content { - .lib-css(margin-bottom, @indent__base); + margin-bottom: @indent__base; } } @@ -271,7 +271,7 @@ &-field-rating { .control { - margin-bottom: 1.2*@indent__xl; + margin-bottom: 1.2 * @indent__xl; margin-top: @indent__s; } } diff --git a/app/design/frontend/Magento/blank/Magento_Sales/web/css/source/_email.less b/app/design/frontend/Magento/blank/Magento_Sales/web/css/source/_email.less index 215d7d8b322b..b189d4e08ba1 100644 --- a/app/design/frontend/Magento/blank/Magento_Sales/web/css/source/_email.less +++ b/app/design/frontend/Magento/blank/Magento_Sales/web/css/source/_email.less @@ -68,8 +68,10 @@ } // Remove address and phone number link color on iOS -.address-details a { - &:extend(.no-link a); +.email-non-inline() { + .address-details a { + &:extend(.no-link a); + } } .media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__xs) { diff --git a/app/design/frontend/Magento/blank/Magento_Sales/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Sales/web/css/source/_module.less index 3847393a2f04..298ccbf58e68 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 b5d604597148..b7271e3c1e24 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 @@ -18,7 +18,7 @@ @swatch-option__selected__border: @swatch-option__hover__border; @swatch-option__selected__color: @swatch-option__hover__color; -@swatch-option__selected__outline: 2px solid @color-orange-red1; +@swatch-option__selected__outline: 2px solid @active__color; @swatch-option__disabled__background: @color-red10; @@ -26,7 +26,7 @@ @swatch-option-text__background: @swatch-option__background; @swatch-option-text__color: #686868; -@swatch-option-text__selected__background-color: @color-white !important; +@swatch-option-text__selected__background-color: @color-white; // Size and Manufacturer attributes @attr-swatch-option__background: @swatch-option__background; @@ -38,7 +38,7 @@ // Image and Color swatch @img-color-swatch-option__hover__border: @swatch-option__hover__border; -@img-color-swatch-option__hover__outline: 2px solid #e00; +@img-color-swatch-option__hover__outline: 2px solid darken(@active__color, 12%); // Tooltip @swatch-option-tooltip__background: @color-white; @@ -54,7 +54,7 @@ @swatch-option-tooltip-layered-title__color: @swatch-option-tooltip-title__color; // Layered Features -@swatch-option-link-layered__focus__box-shadow: 0 0 3px 1px @color-sky-blue1; +@swatch-option-link-layered__focus__box-shadow: 0 0 3px 1px @focus__color; // // Common @@ -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); } @@ -124,7 +128,7 @@ float: left; height: 20px; margin: 0 @indent__s @indent__xs 0; - max-width: 90px; + max-width: 100%; min-width: 30px; overflow: hidden; padding: 1px 2px; @@ -132,18 +136,22 @@ 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); + .lib-css(background-color, @swatch-option-text__selected__background-color) !important; } } @@ -214,7 +222,6 @@ .lib-css(color, @swatch-option-tooltip__color); display: none; max-height: 100%; - max-width: 140px; min-height: 20px; min-width: 20px; padding: @indent__xs; @@ -279,7 +286,6 @@ .lib-css(color, @swatch-option-tooltip-title__color); display: block; max-height: 200px; - max-width: 140px; min-height: 20px; overflow: hidden; text-align: center; diff --git a/app/design/frontend/Magento/blank/Magento_Theme/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Theme/web/css/source/_module.less index 8df5556e9772..b314bcf5b347 100644 --- a/app/design/frontend/Magento/blank/Magento_Theme/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Theme/web/css/source/_module.less @@ -296,10 +296,6 @@ box-sizing: border-box; width: 100%; } - - .ie11 & { - height: 100%; - } } .navigation ul { diff --git a/app/design/frontend/Magento/blank/Magento_Wishlist/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Wishlist/web/css/source/_module.less index 0e8350261e00..9cd0439c1395 100644 --- a/app/design/frontend/Magento/blank/Magento_Wishlist/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Wishlist/web/css/source/_module.less @@ -8,6 +8,24 @@ // _____________________________________________ & when (@media-common = true) { + .toolbar { + &.wishlist-toolbar { + .limiter { + float: right; + } + .main .pages { + display: inline-block; + position: relative; + z-index: 0; + } + .toolbar-amount, + .limiter { + display: inline-block; + z-index: 1; + } + } + } + .form.wishlist.items { .actions-toolbar { &:extend(.abs-reset-left-margin all); @@ -177,10 +195,10 @@ .media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { .products-grid.wishlist { margin-bottom: @indent__l; - margin-right: -@indent__s; + margin-right: 0; .product { &-item { - padding: @indent__base @indent__s @indent__base @indent__base; + padding: @indent__base 0 @indent__base 0; position: relative; &-photo { @@ -194,6 +212,7 @@ &-actions { display: block; + float: left; .action { margin-right: 15px; diff --git a/app/design/frontend/Magento/blank/etc/view.xml b/app/design/frontend/Magento/blank/etc/view.xml index 572632b6683e..e742ce0a21cd 100644 --- a/app/design/frontend/Magento/blank/etc/view.xml +++ b/app/design/frontend/Magento/blank/etc/view.xml @@ -250,7 +250,7 @@ <var name="product_list_image_size">166</var> <!-- New Product image size used in product list --> <var name="product_zoom_image_size">370</var> <!-- New Product image size used for zooming --> - <var name="product_image_white_borders">0</var> + <var name="product_image_white_borders">1</var> </vars> <vars module="Magento_Bundle"> <var name="product_summary_image_size">58</var> <!-- New Product image size used for summary block--> diff --git a/app/design/frontend/Magento/blank/web/css/source/_extends.less b/app/design/frontend/Magento/blank/web/css/source/_extends.less index 13da4a4d996c..74dfd48d87a8 100644 --- a/app/design/frontend/Magento/blank/web/css/source/_extends.less +++ b/app/design/frontend/Magento/blank/web/css/source/_extends.less @@ -162,7 +162,7 @@ & when (@media-common = true) { .abs-login-block-title { strong { - font-weight: 500; + font-weight: @font-weight__heavier; } .lib-font-size(18); @@ -178,11 +178,11 @@ & when (@media-common = true) { .abs-block-title { + margin-bottom: 15px; + > strong { .lib-heading(h3); } - - margin-bottom: 15px; } } @@ -194,6 +194,7 @@ .abs-account-blocks { .block-title { &:extend(.abs-block-title all); + > .action { margin-left: 15px; } @@ -208,7 +209,7 @@ } > .action { - font-weight: 400; + font-weight: @font-weight__regular; margin-left: @indent__s; } } @@ -232,10 +233,10 @@ & when (@media-common = true) { .abs-dropdown-simple { .lib-dropdown( - @_dropdown-list-item-padding: 5px 5px 5px 23px, - @_dropdown-list-min-width: 200px, - @_icon-font-margin: 0 0 0 5px, - @_icon-font-vertical-align: middle + @_dropdown-list-item-padding: 5px 5px 5px 23px, + @_dropdown-list-min-width: 200px, + @_icon-font-margin: 0 0 0 5px, + @_icon-font-vertical-align: middle ); } } @@ -252,7 +253,7 @@ } // -// Marging for blocks & widgets +// Margin for blocks & widgets // --------------------------------------------- & when (@media-common = true) { @@ -268,13 +269,13 @@ & when (@media-common = true) { .abs-remove-button-for-blocks { .lib-icon-font( - @icon-remove, - @_icon-font-size: 26px, - @_icon-font-line-height: 15px, - @_icon-font-text-hide: true, - @_icon-font-color: @color-gray19, - @_icon-font-color-hover: @color-gray19, - @_icon-font-color-active: @color-gray19 + @icon-remove, + @_icon-font-size: 26px, + @_icon-font-line-height: 15px, + @_icon-font-text-hide: true, + @_icon-font-color: @color-gray19, + @_icon-font-color-hover: @color-gray19, + @_icon-font-color-active: @color-gray19 ); } } @@ -289,14 +290,14 @@ > a { .lib-link( - @_link-color: @product-name-link__color, - @_link-text-decoration: @product-name-link__text-decoration, - @_link-color-visited: @product-name-link__color__visited, - @_link-text-decoration-visited: @product-name-link__text-decoration__visited, - @_link-color-hover: @product-name-link__color__hover, - @_link-text-decoration-hover: @product-name-link__text-decoration__hover, - @_link-color-active: @product-name-link__color__active, - @_link-text-decoration-active: @product-name-link__text-decoration__active + @_link-color: @product-name-link__color, + @_link-text-decoration: @product-name-link__text-decoration, + @_link-color-visited: @product-name-link__color__visited, + @_link-text-decoration-visited: @product-name-link__text-decoration__visited, + @_link-color-hover: @product-name-link__color__hover, + @_link-text-decoration-hover: @product-name-link__text-decoration__hover, + @_link-color-active: @product-name-link__color__active, + @_link-text-decoration-active: @product-name-link__text-decoration__active ); } } @@ -613,11 +614,11 @@ & when (@media-common = true) { .abs-navigation-icon { .lib-icon-font( - @_icon-font-content: @icon-down, - @_icon-font-size: 34px, - @_icon-font-line-height: 1.2, - @_icon-font-position: after, - @_icon-font-display: block + @_icon-font-content: @icon-down, + @_icon-font-size: 34px, + @_icon-font-line-height: 1.2, + @_icon-font-position: after, + @_icon-font-display: block ); &:after { @@ -635,8 +636,8 @@ & when (@media-common = true) { .abs-split-button { .lib-dropdown-split( - @_options-selector : ~'.items', - @_dropdown-split-button-border-radius-fix: true + @_options-selector : ~'.items', + @_dropdown-split-button-border-radius-fix: true ); vertical-align: middle; } @@ -654,13 +655,13 @@ .abs-actions-addto-gridlist { .lib-icon-font( - @_icon-font-content: '', - @_icon-font-size: 29px, - @_icon-font-color: @addto-color, - @_icon-font-color-hover: @addto-hover-color, - @_icon-font-text-hide: true, - @_icon-font-vertical-align: middle, - @_icon-font-line-height: 24px + @_icon-font-content: '', + @_icon-font-size: 29px, + @_icon-font-color: @addto-color, + @_icon-font-color-hover: @addto-hover-color, + @_icon-font-text-hide: true, + @_icon-font-vertical-align: middle, + @_icon-font-line-height: 24px ); } } @@ -762,11 +763,11 @@ padding-right: 12px; position: relative; .lib-icon-font( - @icon-down, - @_icon-font-size: 26px, - @_icon-font-line-height: 10px, - @_icon-font-margin: 3px 0 0 0, - @_icon-font-position: after + @icon-down, + @_icon-font-size: 26px, + @_icon-font-line-height: 10px, + @_icon-font-margin: 3px 0 0 0, + @_icon-font-position: after ); &:after { @@ -777,16 +778,16 @@ &-expanded { .lib-icon-font-symbol( - @_icon-font-content: @icon-up, - @_icon-font-position: after + @_icon-font-content: @icon-up, + @_icon-font-position: after ); } } .abs-tax-total-expanded { .lib-icon-font-symbol( - @_icon-font-content: @icon-up, - @_icon-font-position: after + @_icon-font-content: @icon-up, + @_icon-font-position: after ); } } @@ -867,10 +868,10 @@ & when (@media-common = true) { .abs-icon-add { .lib-icon-font( - @_icon-font-content: @icon-expand, - @_icon-font-size: 10px, - @_icon-font-line-height: 10px, - @_icon-font-vertical-align: middle + @_icon-font-content: @icon-expand, + @_icon-font-size: 10px, + @_icon-font-line-height: 10px, + @_icon-font-vertical-align: middle ); } } @@ -878,12 +879,12 @@ .media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { .abs-icon-add-mobile { .lib-icon-font( - @_icon-font-content: @icon-expand, - @_icon-font-size: 10px, - @_icon-font-line-height: 10px, - @_icon-font-vertical-align: middle, - @_icon-font-margin: 0 5px 0 0, - @_icon-font-display: block + @_icon-font-content: @icon-expand, + @_icon-font-size: 10px, + @_icon-font-line-height: 10px, + @_icon-font-vertical-align: middle, + @_icon-font-margin: 0 5px 0 0, + @_icon-font-display: block ); } } @@ -924,11 +925,11 @@ position: relative; .lib-icon-font( - @_icon-font-content: @icon-down, - @_icon-font-size: 28px, - @_icon-font-text-hide: false, - @_icon-font-position: after, - @_icon-font-display: block + @_icon-font-content: @icon-down, + @_icon-font-size: 28px, + @_icon-font-text-hide: false, + @_icon-font-position: after, + @_icon-font-display: block ); &:after { @@ -1068,12 +1069,12 @@ font-weight: @font-weight__bold; .lib-link-as-button(); .lib-button( - @_button-padding: 7px 15px 7px 0, - @_button-icon-use: true, - @_button-font-content: @icon-prev, - @_button-icon-font-size: 32px, - @_button-icon-font-line-height: 16px, - @_button-icon-font-position: before + @_button-padding: 7px 15px 7px 0, + @_button-icon-use: true, + @_button-font-content: @icon-prev, + @_button-icon-font-size: 32px, + @_button-icon-font-line-height: 16px, + @_button-icon-font-position: before ); &:active { @@ -1083,9 +1084,9 @@ &.update { .lib-button-icon( - @icon-update, - @_icon-font-size: 32px, - @_icon-font-line-height: 16px + @icon-update, + @_icon-font-size: 32px, + @_icon-font-line-height: 16px ); padding-left: @indent__xs; } @@ -1160,7 +1161,7 @@ & when (@media-common = true) { .abs-field-date-input { - .lib-css(margin-right, @indent__s); + margin-right: @indent__s; width: calc(~'100% -' @icon-calendar__font-size + @indent__s); } } @@ -1175,7 +1176,7 @@ position: relative; input { - .lib-css(margin-right, @indent__s); + margin-right: @indent__s; width: calc(~'100% -' @checkout-tooltip-icon__font-size + @indent__s + @indent__xs); } } @@ -1193,12 +1194,12 @@ &:before, &:after { .lib-arrow( - @_position: top, - @_size: @checkout-tooltip-icon-arrow__font-size, - @_color: @checkout-tooltip-content__background-color + @_position: top, + @_size: @checkout-tooltip-icon-arrow__font-size, + @_color: @checkout-tooltip-content__background-color ); .lib-css(margin-top, @checkout-tooltip-icon-arrow__left); - .lib-css(right, @indent__s); + right: @indent__s; left: auto; top: 0; } @@ -1234,11 +1235,11 @@ .lib-css(border-bottom, @checkout-step-title__border); .lib-css(padding-bottom, @checkout-step-title__padding); .lib-typography( - @_font-size: @checkout-step-title__font-size, - @_font-weight: @checkout-step-title__font-weight, - @_font-family: false, - @_font-style: false, - @_line-height: false + @_font-size: @checkout-step-title__font-size, + @_font-weight: @checkout-step-title__font-weight, + @_font-family: false, + @_font-style: false, + @_line-height: false ); } } @@ -1299,11 +1300,11 @@ .amount .price { .lib-icon-font( - @icon-down, - @_icon-font-size: 30px, - @_icon-font-text-hide: true, - @_icon-font-position: after, - @_icon-font-display: block + @icon-down, + @_icon-font-size: 30px, + @_icon-font-text-hide: true, + @_icon-font-position: after, + @_icon-font-display: block ); padding-right: @indent__m; position: relative; @@ -1323,16 +1324,16 @@ .amount .price { .lib-icon-font-symbol( - @_icon-font-content: @icon-up, - @_icon-font-position: after + @_icon-font-content: @icon-up, + @_icon-font-position: after ); } } } &-details { - display: none; .lib-css(border-bottom, @border-width__base solid @border-color__base); + display: none; &.shown { display: table-row; @@ -1357,10 +1358,10 @@ cursor: pointer; font-weight: @font-weight__semibold; .lib-icon-font( - @_icon-font-content: @icon-down, - @_icon-font-size: 30px, - @_icon-font-position: after, - @_icon-font-display: block + @_icon-font-content: @icon-down, + @_icon-font-size: 30px, + @_icon-font-position: after, + @_icon-font-display: block ); margin-bottom: 0; overflow: hidden; @@ -1388,8 +1389,8 @@ &.active { > .title { .lib-icon-font-symbol( - @_icon-font-content: @icon-up, - @_icon-font-position: after + @_icon-font-content: @icon-up, + @_icon-font-position: after ); } diff --git a/app/design/frontend/Magento/blank/web/css/source/_forms.less b/app/design/frontend/Magento/blank/web/css/source/_forms.less index 94b993b53b50..26f5ff89e99e 100644 --- a/app/design/frontend/Magento/blank/web/css/source/_forms.less +++ b/app/design/frontend/Magento/blank/web/css/source/_forms.less @@ -18,7 +18,7 @@ .fieldset { .lib-form-fieldset(); &:last-child { - margin-bottom: 0; + margin-bottom: @indent__base; } > .field, @@ -101,6 +101,18 @@ .lib-form-validation-note(); } + .product-options-wrapper { + .date { + &.required { + div[for*='options'] { + &.mage-error { + display: none !important; + } + } + } + } + } + .field .tooltip { .lib-tooltip(right); .tooltip-content { diff --git a/app/design/frontend/Magento/blank/web/css/source/_navigation.less b/app/design/frontend/Magento/blank/web/css/source/_navigation.less index 4499886ef0f1..21b731577976 100644 --- a/app/design/frontend/Magento/blank/web/css/source/_navigation.less +++ b/app/design/frontend/Magento/blank/web/css/source/_navigation.less @@ -131,12 +131,18 @@ ); } } - .switcher-dropdown { .lib-list-reset-styles(); + display: none; padding: @indent__s 0; } - + .switcher-options { + &.active { + .switcher-dropdown { + display: block; + } + } + } .header.links { .lib-list-reset-styles(); border-bottom: 1px solid @color-gray82; @@ -207,7 +213,7 @@ } .nav-toggle { - &:after{ + &:after { background: rgba(0, 0, 0, @overlay__opacity); content: ''; display: block; diff --git a/app/design/frontend/Magento/blank/web/css/source/_sections.less b/app/design/frontend/Magento/blank/web/css/source/_sections.less index f0a3518c92a8..1eee47bda817 100644 --- a/app/design/frontend/Magento/blank/web/css/source/_sections.less +++ b/app/design/frontend/Magento/blank/web/css/source/_sections.less @@ -31,8 +31,19 @@ .media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { .product.data.items { .lib-data-accordion(); + .data.item { display: block; } + + .item.title { + > .switch { + padding: 1px 15px 1px; + } + } + + > .item.content { + padding: 10px 15px 30px; + } } } diff --git a/app/design/frontend/Magento/blank/web/css/source/components/_modals_extend.less b/app/design/frontend/Magento/blank/web/css/source/components/_modals_extend.less index 5cdb1444094e..764a63bd8806 100644 --- a/app/design/frontend/Magento/blank/web/css/source/components/_modals_extend.less +++ b/app/design/frontend/Magento/blank/web/css/source/components/_modals_extend.less @@ -148,10 +148,9 @@ } } } -} -.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { .modal-popup { + pointer-events: auto; &.modal-slide { .modal-inner-wrap[class] { .lib-css(background-color, @modal-slide-mobile__background-color); diff --git a/app/design/frontend/Magento/blank/web/images/logo.svg b/app/design/frontend/Magento/blank/web/images/logo.svg index 013d6e7c5a10..0f29d4e3eef2 100644 --- a/app/design/frontend/Magento/blank/web/images/logo.svg +++ b/app/design/frontend/Magento/blank/web/images/logo.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" height="55.139px" viewBox="0 0 189 55.139" width="189px" version="1.1" y="0px" x="0px" xmlns:xlink="http://www.w3.org/1999/xlink" enable-background="new 0 0 189 55.139"><path d="m23.333 0l-23.333 14.135v26.865l6.06 3.568v-26.865l17.278-10.504 17.292 10.488 0.074 0.042-0.008 26.798 6.001-3.527v-26.865l-23.364-14.135zm3.088 16.538v31.407l-3.088 1.889-3.091-1.896v-31.376l-8.003 4.928v26.86l11.094 6.789 11.189-6.837v-26.829l-8.101-4.935z" fill="#ED7402"/><path d="m12.239 21.491l8.003 4.886v-9.814l-8.003 4.928zm14.182-4.953v9.902l8.101-4.967-8.101-4.935zm20.276-2.403l-23.364-14.135-23.333 14.135 6.06 3.568 17.278-10.504 17.365 10.53 5.994-3.594z" fill="#F8B97F"/><path d="m82.328 39.984l-1.608-20.54-8.156 20.651h-2.656l-8.156-20.651-1.571 20.541h-3.293l2.058-25.814h4.34l8.043 21.174 8.044-21.174h4.301l2.021 25.814h-3.367z" fill="#131108"/><path d="m99.91 39.984l-0.375-2.396c-1.421 1.458-3.366 2.769-6.284 2.769-3.218 0-5.237-1.945-5.237-4.977 0-4.452 3.815-6.209 11.261-6.996v-0.748c0-2.244-1.348-3.03-3.406-3.03-2.17 0-4.227 0.673-6.172 1.533l-0.449-2.88c2.133-0.861 4.153-1.496 6.922-1.496 4.339 0 6.436 1.757 6.436 5.723v12.498h-2.7zm-0.635-9.055c-6.586 0.636-7.97 2.432-7.97 4.266 0 1.457 0.973 2.394 2.657 2.394 1.945 0 3.815-0.974 5.312-2.508v-4.152h0.001z" fill="#131108"/><path d="m121.72 21.876l0.485 2.992-3.404 0.336c0.486 0.824 0.712 1.76 0.712 2.769 0 3.817-3.219 6.134-6.848 6.134-0.449 0-0.898-0.037-1.348-0.111-0.523 0.338-0.897 0.75-0.897 1.085 0 0.636 0.634 0.787 3.777 1.349l1.272 0.223c3.778 0.673 6.135 1.871 6.135 4.639 0 3.741-4.078 5.5-8.715 5.5-4.64 0-8.343-1.458-8.343-4.6 0-1.834 1.271-3.256 3.777-4.603-0.784-0.562-1.121-1.198-1.121-1.872 0-0.861 0.672-1.722 1.87-2.432-1.982-0.973-3.33-2.879-3.33-5.312 0-3.852 3.217-6.208 6.846-6.208 1.795 0 3.368 0.522 4.604 1.496l4.53-1.385zm-13.99 20.052c0 1.424 1.834 2.471 5.312 2.471 3.48 0 5.424-1.197 5.424-2.693 0-1.086-0.822-1.832-3.365-2.282l-2.134-0.375c-0.972-0.187-1.495-0.299-2.207-0.448-2.1 1.045-3.03 2.094-3.03 3.327zm4.86-17.733c-2.244 0-3.629 1.722-3.629 3.891 0 2.058 1.421 3.665 3.629 3.665 2.283 0 3.704-1.682 3.704-3.815 0.01-2.132-1.49-3.741-3.7-3.741z" fill="#131108"/><path d="m137.58 31.416h-12.122c0.112 4.15 2.094 6.098 5.2 6.098 2.583 0 4.453-1.009 6.397-2.544l0.484 2.994c-1.907 1.497-4.188 2.394-7.143 2.394-4.642 0-8.271-2.807-8.271-9.354 0-5.724 3.368-9.239 7.856-9.239 5.199 0 7.595 4.002 7.595 8.94v0.711zm-7.63-7.033c-2.059 0-3.817 1.459-4.34 4.526h8.604c-0.41-2.881-1.68-4.526-4.26-4.526z" fill="#131108"/><path d="m151.61 39.984v-12.16c0-1.832-0.786-3.067-2.73-3.067-1.759 0-3.555 1.161-5.163 2.88v12.347h-3.329v-17.846h2.655l0.412 2.582c1.683-1.533 3.78-2.955 6.321-2.955 3.369 0 5.166 2.019 5.166 5.236v12.983h-3.33z" fill="#131108"/><path d="m164.78 40.284c-3.143 0-5.199-1.124-5.199-4.716v-10.624h-2.694v-2.806h2.694v-5.949l3.255-0.485v6.434h3.853l0.449 2.806h-4.302v10.025c0 1.461 0.599 2.357 2.47 2.357 0.598 0 1.121-0.035 1.531-0.112l0.45 2.843c-0.57 0.112-1.35 0.227-2.51 0.227z" fill="#131108"/><path d="m175.82 40.357c-4.752 0-8.194-3.403-8.194-9.278 0-5.874 3.442-9.314 8.194-9.314 4.787 0 8.305 3.44 8.305 9.314 0 5.875-3.52 9.278-8.3 9.278zm0-15.788c-3.217 0-4.826 2.769-4.826 6.51 0 3.668 1.683 6.511 4.826 6.511 3.291 0 4.938-2.77 4.938-6.511-0.01-3.666-1.73-6.51-4.94-6.51z" fill="#131108"/><path d="m186.48 24.81c-1.338 0-2.268-0.929-2.268-2.318 0-1.379 0.95-2.328 2.268-2.328 1.34 0 2.27 0.939 2.27 2.328 0 1.378-0.95 2.318-2.27 2.318zm0-4.376c-1.078 0-1.938 0.739-1.938 2.058 0 1.31 0.859 2.049 1.938 2.049 1.09 0 1.949-0.739 1.949-2.049 0-1.319-0.87-2.058-1.95-2.058zm0.67 3.297l-0.768-1.099h-0.249v1.059h-0.44v-2.568h0.779c0.54 0 0.898 0.27 0.898 0.75 0 0.37-0.201 0.609-0.52 0.709l0.74 1.049-0.44 0.1zm-0.68-2.209h-0.34v0.759h0.319c0.29 0 0.471-0.12 0.471-0.379 0-0.25-0.16-0.38-0.45-0.38z" fill="#131108"/></svg> \ No newline at end of file +<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 179.073 50"><style>.st0{fill:#f26322}.st1{fill:#4d4d4d}</style><path class="st0" d="M21.432 0L0 12.373v24.713l6.117 3.533-.041-24.713 15.315-8.842 15.313 8.842v24.706l6.118-3.526V12.35z"/><path class="st0" d="M24.47 40.618l-3.058 1.772-3.071-1.759V15.906l-6.116 3.532.01 24.712 9.172 5.298 9.18-5.298V19.438l-6.117-3.532z"/><path class="st1" d="M56.838 12.522l8.415 21.258h.068l8.21-21.258h3.203V36.88h-2.215V15.656h-.068c-.114.386-.239.772-.374 1.158-.115.318-.246.67-.393 1.055-.147.387-.278.75-.391 1.09l-7.052 17.919h-2.01L57.11 18.96a19.913 19.913 0 0 1-.408-1.039 43.558 43.558 0 0 1-.375-1.073 68.067 68.067 0 0 0-.408-1.192h-.069v21.223h-2.112V12.522h3.1zM83.17 36.982a5.205 5.205 0 0 1-1.823-.92 4.327 4.327 0 0 1-1.209-1.533 4.881 4.881 0 0 1-.443-2.145 5.018 5.018 0 0 1 .579-2.556 4.472 4.472 0 0 1 1.568-1.584 7.927 7.927 0 0 1 2.299-.903 24.732 24.732 0 0 1 2.811-.477 36 36 0 0 0 2.198-.29 6.689 6.689 0 0 0 1.465-.392c.329-.123.614-.343.817-.63.187-.325.276-.698.256-1.073v-.34a3.212 3.212 0 0 0-1.09-2.674 4.93 4.93 0 0 0-3.134-.87c-3.135 0-4.781 1.306-4.941 3.918h-2.077a5.748 5.748 0 0 1 1.891-4.089 7.5 7.5 0 0 1 5.127-1.533 7.335 7.335 0 0 1 4.564 1.278 4.923 4.923 0 0 1 1.67 4.173v9.573c-.034.402.069.804.29 1.141.219.251.536.394.868.392.12-.001.24-.012.358-.034.124-.022.266-.057.425-.102h.103v1.533c-.188.077-.382.14-.58.188-.28.062-.566.091-.852.086a2.694 2.694 0 0 1-1.839-.597 2.575 2.575 0 0 1-.75-1.891v-.374h-.102c-.277.372-.578.725-.903 1.056-.38.385-.81.717-1.278.988a7.14 7.14 0 0 1-1.737.715 8.443 8.443 0 0 1-2.249.272 8.186 8.186 0 0 1-2.282-.306m5.195-1.857a5.971 5.971 0 0 0 1.857-1.175 4.767 4.767 0 0 0 1.499-3.441V27.34a7.295 7.295 0 0 1-2.061.732 33.21 33.21 0 0 1-2.504.426c-.749.114-1.441.233-2.077.358a5.237 5.237 0 0 0-1.652.596 3.055 3.055 0 0 0-1.108 1.107 3.579 3.579 0 0 0-.408 1.824c-.018.53.093 1.056.324 1.533.201.392.493.73.851.987a3.35 3.35 0 0 0 1.244.529c.493.104.995.155 1.499.153a6.555 6.555 0 0 0 2.536-.46M99.35 41.734a4.687 4.687 0 0 1-2.009-3.287h2.043c.14.966.763 1.794 1.652 2.197a7.522 7.522 0 0 0 3.288.664 5.688 5.688 0 0 0 4.173-1.345 4.997 4.997 0 0 0 1.345-3.697v-2.793h-.102a7.293 7.293 0 0 1-2.283 2.282 6.297 6.297 0 0 1-3.304.784 7.375 7.375 0 0 1-3.135-.647 6.921 6.921 0 0 1-2.385-1.806 8.095 8.095 0 0 1-1.516-2.777 11.429 11.429 0 0 1-.528-3.56 10.89 10.89 0 0 1 .613-3.799 8.483 8.483 0 0 1 1.636-2.777 6.75 6.75 0 0 1 2.402-1.703 7.45 7.45 0 0 1 2.913-.58 6.234 6.234 0 0 1 3.372.835 6.938 6.938 0 0 1 2.215 2.265h.102v-2.725h2.078v16.931a6.78 6.78 0 0 1-1.636 4.735 7.751 7.751 0 0 1-5.893 2.112 8.337 8.337 0 0 1-5.041-1.309m9.232-8.908a8.565 8.565 0 0 0 1.397-5.11 11.22 11.22 0 0 0-.34-2.862 6.239 6.239 0 0 0-1.056-2.231 4.828 4.828 0 0 0-1.789-1.448 5.772 5.772 0 0 0-2.503-.511 4.782 4.782 0 0 0-4.071 1.941 8.464 8.464 0 0 0-1.448 5.179c-.008.936.107 1.87.34 2.777a6.64 6.64 0 0 0 1.022 2.214 4.81 4.81 0 0 0 1.703 1.465 5.208 5.208 0 0 0 2.42.528 4.967 4.967 0 0 0 4.325-1.942M119.244 36.624a7.19 7.19 0 0 1-2.572-1.941 8.66 8.66 0 0 1-1.583-2.93 11.839 11.839 0 0 1-.546-3.662 11.179 11.179 0 0 1 .58-3.663 9.138 9.138 0 0 1 1.617-2.929 7.307 7.307 0 0 1 2.522-1.942 7.684 7.684 0 0 1 3.321-.698 7.275 7.275 0 0 1 3.56.8 6.678 6.678 0 0 1 2.351 2.146 8.806 8.806 0 0 1 1.278 3.083 16.87 16.87 0 0 1 .374 3.577h-13.422c.013.941.157 1.875.426 2.777a6.968 6.968 0 0 0 1.124 2.231 5.108 5.108 0 0 0 1.857 1.499 5.948 5.948 0 0 0 2.623.546 4.985 4.985 0 0 0 3.424-1.074 5.875 5.875 0 0 0 1.719-2.878h2.044a7.51 7.51 0 0 1-2.385 4.19 7.072 7.072 0 0 1-4.803 1.567 8.386 8.386 0 0 1-3.509-.699m8.312-12.264a5.986 5.986 0 0 0-.988-1.976 4.525 4.525 0 0 0-1.635-1.311 5.362 5.362 0 0 0-2.351-.478 5.623 5.623 0 0 0-2.368.478 5.064 5.064 0 0 0-1.754 1.311 6.566 6.566 0 0 0-1.141 1.96 9.615 9.615 0 0 0-.562 2.453h11.174a9.268 9.268 0 0 0-.375-2.437M134.879 19.267v2.691h.068a7.237 7.237 0 0 1 2.333-2.197 6.798 6.798 0 0 1 3.561-.868 5.834 5.834 0 0 1 4.037 1.413 5.174 5.174 0 0 1 1.584 4.071V36.88h-2.112V24.581a3.716 3.716 0 0 0-1.073-2.947 4.334 4.334 0 0 0-2.948-.937 5.896 5.896 0 0 0-2.111.375 5.558 5.558 0 0 0-1.738 1.039 4.717 4.717 0 0 0-1.601 3.593V36.88h-2.112V19.267h2.112zM151.912 36.284a2.934 2.934 0 0 1-.92-2.436V21.005h-2.657v-1.738h2.657v-5.416h2.112v5.416h3.271v1.738h-3.271v12.502a1.65 1.65 0 0 0 .426 1.312c.371.265.823.391 1.277.357.258-.001.515-.029.766-.085.215-.043.426-.106.63-.188h.103v1.806a5.907 5.907 0 0 1-1.942.306 3.819 3.819 0 0 1-2.452-.731M162.625 36.624a7.368 7.368 0 0 1-2.571-1.942 8.732 8.732 0 0 1-1.618-2.929 12.217 12.217 0 0 1 0-7.324 8.744 8.744 0 0 1 1.618-2.93 7.386 7.386 0 0 1 2.571-1.942 8.106 8.106 0 0 1 3.424-.698 7.989 7.989 0 0 1 3.406.698 7.424 7.424 0 0 1 2.556 1.942 8.504 8.504 0 0 1 1.601 2.93 12.57 12.57 0 0 1 0 7.324 8.5 8.5 0 0 1-1.601 2.929 7.415 7.415 0 0 1-2.556 1.942 7.959 7.959 0 0 1-3.406.698 8.075 8.075 0 0 1-3.424-.698m6.013-1.652a5.308 5.308 0 0 0 1.873-1.601 7.215 7.215 0 0 0 1.124-2.385 11.348 11.348 0 0 0 0-5.792 7.215 7.215 0 0 0-1.124-2.385 5.289 5.289 0 0 0-1.873-1.601 6.109 6.109 0 0 0-5.195 0 5.497 5.497 0 0 0-1.874 1.601 7.046 7.046 0 0 0-1.141 2.385 11.392 11.392 0 0 0 0 5.792c.227.86.614 1.669 1.141 2.385a5.817 5.817 0 0 0 7.069 1.601M176.856 22.191a2.128 2.128 0 0 1-2.213-2.265 2.216 2.216 0 1 1 4.431 0 2.146 2.146 0 0 1-2.218 2.265m0-4.277a1.845 1.845 0 0 0-1.892 2.012 1.9 1.9 0 1 0 3.797 0 1.854 1.854 0 0 0-1.905-2.012m.653 3.222l-.751-1.073h-.243v1.035h-.43v-2.509h.763c.526 0 .877.264.877.732a.681.681 0 0 1-.508.693l.724 1.025-.432.097zm-.661-2.157h-.333v.741h.312c.283 0 .46-.117.46-.371.001-.244-.158-.37-.439-.37"/></svg> diff --git a/app/design/frontend/Magento/luma/Magento_Bundle/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Bundle/web/css/source/_module.less index 43ae23bab789..45a01269bef6 100644 --- a/app/design/frontend/Magento/luma/Magento_Bundle/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Bundle/web/css/source/_module.less @@ -58,6 +58,7 @@ .field.choice { input { float: left; + margin-top: 4px; } .label { @@ -253,7 +254,7 @@ .box-tocart { .action.primary { margin-right: 1%; - width: 49%; + width: auto; } } diff --git a/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less index 501a1d2918d6..3b4da1d1ae6f 100644 --- a/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less @@ -294,6 +294,12 @@ } .product-options-wrapper { + .fieldset { + &:focus { + box-shadow: none; + } + } + .fieldset-product-options-inner { .legend { .lib-css(font-weight, @font-weight__semibold); @@ -391,6 +397,7 @@ .box-tocart { &:extend(.abs-box-tocart all); + .field.qty { } @@ -534,6 +541,15 @@ } } + .block-compare { + .action { + &.delete { + &:extend(.abs-remove-button-for-blocks all); + right: initial; + } + } + } + .action.tocart { border-radius: 0; } @@ -563,6 +579,7 @@ .product-items-names { .product-item { + display: flex; margin-bottom: @indent__s; } @@ -975,6 +992,15 @@ [class*='block-compare'] { display: none; } + .catalog-product_compare-index { + .columns { + .column { + &.main { + flex-basis: inherit; + } + } + } + } } // diff --git a/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/module/_listings.less b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/module/_listings.less index d0382d34d39f..77fb53a2ab02 100644 --- a/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/module/_listings.less +++ b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/module/_listings.less @@ -18,7 +18,7 @@ @product-name-link__text-decoration__visited: @link__hover__text-decoration; @product-item__hover__background-color: @color-white; -@product-item__hover__box-shadow: 3px 3px 4px 0 rgba(0, 0, 0, .3); +@product-item__hover__box-shadow: 3px 4px 4px 0 rgba(0, 0, 0, .3); @product-price__muted__color: @color-gray40; @@ -34,15 +34,22 @@ .product { &-items { + .lib-inline-block-space-container(); &:extend(.abs-reset-list all); } &-item { + .lib-inline-block-space-item(); vertical-align: top; .products-grid & { display: inline-block; - width: 100%/2; + margin-left: 2%; + width: calc(~'(100% - 2%)/2'); + } + + &:nth-child(2n + 1) { + margin-left: 0; } &:extend(.abs-add-box-sizing all); @@ -68,9 +75,17 @@ } &-actions { - display: none; + font-size: 0; + + > * { + font-size: 1.4rem; + } .actions-secondary { + display: inline-block; + font-size: 1.4rem; + vertical-align: middle; + > button.action { .lib-button-reset(); } @@ -80,12 +95,19 @@ &:before { margin: 0; } + line-height: 35px; + text-align: center; + width: 35px; span { &:extend(.abs-visually-hidden all); } } } + + .actions-primary { + display: inline-block; + } } &-description { @@ -292,7 +314,7 @@ border: 1px solid @color-gray-light2; border-top: none; left: 0; - margin: 9px 0 0 -1px; + margin: 10px 0 0 -1px; padding: 0 9px 9px; position: absolute; right: -1px; @@ -308,13 +330,13 @@ } .actions-primary + .actions-secondary { - display: table-cell; - padding-left: 10px; + display: inline-block; vertical-align: middle; - width: 50%; > .action { - margin-right: 10px; + line-height: 35px; + text-align: center; + width: 35px; &:last-child { margin-right: 0; @@ -323,7 +345,7 @@ } .actions-primary { - display: table-cell; + display: inline-block; } } @@ -375,10 +397,24 @@ .page-products.page-layout-3columns { .products-grid { .product-item { - width: 100%/3; + margin-left: 0; + width: calc(~'(100% - 4%) / 3'); + + &:nth-child(3n + 1) { + margin-left: 0; + } } } } + + .block.widget .products-grid .product-item, + .page-layout-1column .block.widget .products-grid .product-item, + .page-layout-3columns .block.widget .products-grid .product-item { + .product-item-inner { + box-shadow: 3px 6px 4px 0 rgba(0, 0, 0, .3); + margin: 9px 0 0 -1px; + } + } } // @@ -389,7 +425,12 @@ .page-products { .products-grid { .product-item { - width: 100%/3; + margin-left: 2%; + width: calc(~'(100% - 4%) / 3'); + + &:nth-child(3n + 1) { + margin-left: 0; + } } } } @@ -442,9 +483,13 @@ } .product-item { - margin-left: calc(~'(100% - 4 * 24.439%) / 3'); + margin-left: 2%; padding: 5px; - width: 24.439%; + width: calc(~'(100% - 6%)/4'); + + &:nth-child(3n + 1) { + margin-left: 2%; + } &:nth-child(4n + 1) { margin-left: 0; diff --git a/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/module/_toolbar.less b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/module/_toolbar.less index 0997b9739125..6bddc46003cb 100644 --- a/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/module/_toolbar.less +++ b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/module/_toolbar.less @@ -38,7 +38,10 @@ .lib-css(box-shadow, @button__shadow); border-radius: 3px; } - + .sorter-action { + position: relative; + top: -2px; + } &-amount { left: 0; line-height: @toolbar-mode-icon-font-size + 2; diff --git a/app/design/frontend/Magento/luma/Magento_CatalogSearch/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_CatalogSearch/web/css/source/_module.less index f785dd74d900..31859a46d3ef 100644 --- a/app/design/frontend/Magento/luma/Magento_CatalogSearch/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_CatalogSearch/web/css/source/_module.less @@ -18,6 +18,20 @@ // _____________________________________________ & when (@media-common = true) { + + .search { + .fieldset { + .control { + .addon { + input { + flex-basis: auto; + width: 100%; + } + } + } + } + } + .block-search { margin-bottom: 0; @@ -199,7 +213,7 @@ // Mobile // _____________________________________________ -.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__s) { +.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { .block-search { margin-top: @indent__s; } diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_cart.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_cart.less index 9e3a28be4c90..71814cd0f042 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 @@ -97,6 +97,7 @@ .field { .radio { float: left; + margin-top: 4px; } .radio { @@ -145,12 +146,6 @@ } &:extend(.abs-adjustment-incl-excl-tax all); - - .action { - &.multicheckout { - color: @color-blue2; - } - } } // Totals block @@ -497,6 +492,17 @@ } } } + + .cart.table-wrapper, + .order-items.table-wrapper { + .col.price, + .col.qty, + .col.subtotal, + .col.msrp { + text-align: left; + } + } + } // @@ -521,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 { @@ -626,12 +644,9 @@ width: 1%; } - &-item-details { - padding-bottom: 35px; - } - &-item-details { display: table-cell; + padding-bottom: 35px; vertical-align: top; white-space: normal; width: 99%; @@ -697,6 +712,9 @@ position: static; } } + &.discount { + width: auto; + } } } diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_minicart.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_minicart.less index 03e4bfff3ba9..5ca332240310 100644 --- a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_minicart.less +++ b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_minicart.less @@ -63,6 +63,13 @@ } } + dl { + &.product.options.list { + display: inline-block; + vertical-align: top; + } + } + .text { &.empty { text-align: center; @@ -107,23 +114,23 @@ .minicart-wrapper { .lib-dropdown( - @_toggle-selector: ~'.action.showcart', - @_options-selector: ~'.block-minicart', - @_dropdown-list-width: 320px, - @_dropdown-list-position-right: 0px, - @_dropdown-list-pointer-position: right, - @_dropdown-list-pointer-position-left-right: 26px, - @_dropdown-list-z-index: 101, - @_dropdown-toggle-icon-content: @icon-cart, - @_dropdown-toggle-active-icon-content: @icon-cart, - @_dropdown-list-item-padding: false, - @_dropdown-list-item-hover: false, - @_icon-font-position: before, - @_icon-font-size: 22px, - @_icon-font-line-height: 28px, - @_icon-font-color: @minicart-icons-color, - @_icon-font-color-hover: @minicart-icons-color-hover, - @_icon-font-color-active: @minicart-icons-color + @_toggle-selector: ~'.action.showcart', + @_options-selector: ~'.block-minicart', + @_dropdown-list-width: 320px, + @_dropdown-list-position-right: -10px, + @_dropdown-list-pointer-position: right, + @_dropdown-list-pointer-position-left-right: 12px, + @_dropdown-list-z-index: 101, + @_dropdown-toggle-icon-content: @icon-cart, + @_dropdown-toggle-active-icon-content: @icon-cart, + @_dropdown-list-item-padding: false, + @_dropdown-list-item-hover: false, + @_icon-font-position: before, + @_icon-font-size: 22px, + @_icon-font-line-height: 28px, + @_icon-font-color: @minicart-icons-color, + @_icon-font-color-hover: @minicart-icons-color-hover, + @_icon-font-color-active: @minicart-icons-color ); float: right; @@ -153,17 +160,17 @@ .action { &.close { - height: 40px; + height: 30px; position: absolute; right: 0; top: 0; - width: 40px; + width: 25px; .lib-button-reset(); .lib-button-icon( @icon-remove, @_icon-font-color: @minicart-icons-color, - @_icon-font-size: 16px, - @_icon-font-line-height: 16px, + @_icon-font-size: 14px, + @_icon-font-line-height: 14px, @_icon-font-text-hide: true ); } @@ -226,6 +233,7 @@ .minicart-items { .lib-list-reset-styles(); + .product-item { padding: @indent__base 0; @@ -287,6 +295,15 @@ .details-qty { margin-top: @indent__s; } + + .product { + .options { + &.list { + &:extend(.abs-product-options-list all); + &:extend(.abs-add-clearfix all); + } + } + } } .product { @@ -297,13 +314,14 @@ .toggle { &:extend(.abs-toggling-title all); + border: 0; + padding: 0 @indent__xl @indent__xs 0; + &:after { .lib-css(color, @color-gray56); margin: 0 0 0 @indent__xs; position: static; } - border: 0; - padding: 0 @indent__xl @indent__xs 0; } .active { @@ -323,14 +341,15 @@ .toggle { &.tooltip { .lib-icon-font( - @icon-down, - @_icon-font-size: 12px, - @_icon-font-line-height: 12px, - @_icon-font-text-hide: true, - @_icon-font-margin: -3px 0 0 7px, - @_icon-font-position: after + @icon-down, + @_icon-font-size: 12px, + @_icon-font-line-height: 12px, + @_icon-font-text-hide: true, + @_icon-font-margin: -3px 0 0 7px, + @_icon-font-position: after ); } + > span { &:extend(.abs-visually-hidden-reset all); } @@ -352,7 +371,7 @@ .item-qty { margin-right: @indent__s; text-align: center; - width: 40px; + width: 45px; } .update-cart-item { @@ -368,19 +387,19 @@ &.edit, &.delete { .lib-icon-font( - @icon-edit, - @_icon-font-size: 18px, - @_icon-font-line-height: 20px, - @_icon-font-text-hide: true, - @_icon-font-color: @minicart-icons-color, - @_icon-font-color-hover: @primary__color, - @_icon-font-color-active: @minicart-icons-color + @icon-edit, + @_icon-font-size: 18px, + @_icon-font-line-height: 20px, + @_icon-font-text-hide: true, + @_icon-font-color: @minicart-icons-color, + @_icon-font-color-hover: @primary__color, + @_icon-font-color-active: @minicart-icons-color ); } &.delete { .lib-icon-font-symbol( - @_icon-font-content: @icon-trash + @_icon-font-content: @icon-trash ); } } @@ -397,7 +416,7 @@ } } -.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__s) { +.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { .minicart-wrapper { margin-top: @indent__s; } @@ -412,7 +431,6 @@ margin-left: 13px; .block-minicart { - right: -15px; width: 390px; } } diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_checkout.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_checkout.less index 0df0cace338c..ac5ab0d87bf6 100644 --- a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_checkout.less +++ b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_checkout.less @@ -7,6 +7,8 @@ // Variables // _____________________________________________ +@import 'fields/_file-uploader.less'; + @checkout-wrapper__margin: @indent__base; @checkout-wrapper__columns: 16; @@ -48,6 +50,7 @@ .step-title { &:extend(.abs-checkout-title all); .lib-css(border-bottom, @checkout-step-title__border); + margin-bottom: 15px; } .step-content { diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_order-summary.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_order-summary.less index 5ecc4d4713bf..920e68994c66 100644 --- a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_order-summary.less +++ b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_order-summary.less @@ -118,6 +118,10 @@ .product { position: relative; + .item-options { + &:extend(.abs-product-options-list all); + &:extend(.abs-add-clearfix all); + } } } @@ -137,6 +141,10 @@ } .product-item { + .product-item-details { + &:extend(.abs-add-clearfix all); + } + .product-item-inner { display: table; margin: 0 0 @indent__s; @@ -144,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 { @@ -166,6 +174,10 @@ } } } + + .message { + margin-top: 10px; + } } .actions-toolbar { @@ -219,3 +231,27 @@ } } } + +// +// Tablet +// _____________________________________________ + +@media only screen and (max-width: @screen__m) { + .opc-block-summary { + .product-item { + .product-item-inner { + display: block; + } + + .product-item-name-block { + display: block; + text-align: left; + } + + .subtotal { + display: block; + text-align: left; + } + } + } +} diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_payment-options.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_payment-options.less index 0b27454b206e..23bb15e6fb4f 100644 --- a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_payment-options.less +++ b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_payment-options.less @@ -69,6 +69,13 @@ .payment-option-content { .lib-css(padding, 0 0 @indent__base @checkout-payment-option-content__padding__xl); + .primary { + .action { + &.action-apply { + margin-right: 0; + } + } + } } .payment-option-inner { @@ -136,6 +143,7 @@ } } + .captcha, .number { .input-text { width: 225px; diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_payments.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_payments.less index dd9db0e71530..eb9c06905366 100644 --- a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_payments.less +++ b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_payments.less @@ -63,10 +63,13 @@ } } } - + /** + * @codingStandardsIgnoreStart + */ #po_number { margin-bottom: 20px; } + // @codingStandardsIgnoreEnd } .payment-method-title { @@ -116,7 +119,8 @@ margin: 0 0 @indent__base; .primary { - .action-update { + .action-update { + margin-bottom: 20px; margin-right: 0; } } diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_progress-bar.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_progress-bar.less index e2378eeeef0c..ad1ac36237ca 100644 --- a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_progress-bar.less +++ b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_progress-bar.less @@ -18,7 +18,7 @@ @checkout-progress-bar-item__transition: background .3s; @checkout-progress-bar-item__width: 185px; -@checkout-progress-bar-item__active__background-color: @color-orange-red1; +@checkout-progress-bar-item__active__background-color: @active__color; @checkout-progress-bar-item__active__color: @primary__color; @checkout-progress-bar-item__active__font-weight: @font-weight__semibold; @checkout-progress-bar-item__complete__color: @link__color; @@ -32,7 +32,7 @@ @checkout-progress-bar-item-element-inner__color: @primary__color; @checkout-progress-bar-item-element-inner__height: @checkout-progress-bar-item-element-inner__width; @checkout-progress-bar-item-element-inner__width: @checkout-progress-bar-item-element__width - ( @checkout-progress-bar-item-element-outer-radius__width*2 ); -@checkout-progress-bar-item-element-inner__active__border-color: @color-orange-red1; +@checkout-progress-bar-item-element-inner__active__border-color: @active__color; @checkout-progress-bar-item-element-inner__active__content: @icon-checkmark; @checkout-progress-bar-item-element-outer-radius__width: 6px; diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_shipping.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_shipping.less index d0ce87beb6ad..d0edf245dc5b 100644 --- a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_shipping.less +++ b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_shipping.less @@ -22,7 +22,7 @@ @checkout-shipping-item__width: 100%/3; @checkout-shipping-item-tablet__width: 100%/2; @checkout-shipping-item-mobile__width: 100%; -@checkout-shipping-item__active__border-color: @color-orange-red1; +@checkout-shipping-item__active__border-color: @active__color; @checkout-shipping-item-icon__selected__height: 27px; @checkout-shipping-item-icon__selected__width: 29px; diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/fields/_file-uploader.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/fields/_file-uploader.less new file mode 100644 index 000000000000..7b06186ef9ad --- /dev/null +++ b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/fields/_file-uploader.less @@ -0,0 +1,450 @@ +// /** +// * Copyright © Magento, Inc. All rights reserved. +// * See COPYING.txt for license details. +// */ + +// +// Components -> Single File Uploader +// _____________________________________________ + +// +// Variables +// --------------------------------------------- + +@icon-delete__content: '\e604'; +@icon-file__content: '\e626'; + + +@file-uploader-preview__border-color: @color-lighter-grayish; +@file-uploader-preview__background-color: @color-white; +@file-uploader-preview-focus__color: @color-blue2; + +@file-uploader-document-icon__color: @color-gray80; +@file-uploader-document-icon__size: 7rem; +@file-uploader-document-icon__z-index: @data-grid-file-uploader-image__z-index + 1; + +@file-uploader-video-icon__color: @color-gray80; +@file-uploader-video-icon__size: 4rem; +@file-uploader-video-icon__z-index: @data-grid-file-uploader-image__z-index + 1; + +@file-uploader-placeholder-icon__color: @color-gray80; +@file-uploader-placeholder-icon__z-index: @data-grid-file-uploader-image__z-index + 1; + +@file-uploader-delete-icon__color: @color-brownie; +@file-uploader-delete-icon__hover__color: @color-brownie-vanilla; +@file-uploader-delete-icon__font-size: 1.6rem; + +@file-uploader-muted-text__color: @color-gray62; + +@file-uploader-preview__width: 150px; +@file-uploader-preview__height: @file-uploader-preview__width; +@file-uploader-preview__opacity: .7; + +@file-uploader-spinner-dimensions: 15px; + +@file-uploader-dragover__background: @color-gray83; +@file-uploader-dragover-focus__color: @color-blue2; + +// Grid uploader + +@data-grid-file-uploader-image__size: 5rem; +@data-grid-file-uploader-image__z-index: 1; + +@data-grid-file-uploader-menu-button__width: 2rem; + +@data-grid-file-uploader-upload-icon__color: @color-darkie-gray; +@data-grid-file-uploader-upload-icon__hover__color: @color-very-dark-gray; +@data-grid-file-uploader-upload-icon__line-height: 48px; + +@data-grid-file-uploader-wrapper__size: @data-grid-file-uploader-image__size + 2rem; + +// +// Single file uploader +// --------------------------------------------- + +.file-uploader-area { + position: relative; + + input[type='file'] { + cursor: pointer; + opacity: 0; + overflow: hidden; + position: absolute; + visibility: hidden; + width: 0; + + &:focus { + + .file-uploader-button { + box-shadow: 0 0 0 1px @file-uploader-preview-focus__color; + } + } + + &:disabled { + + .file-uploader-button { + cursor: default; + opacity: .5; + pointer-events: none; + } + } + } +} + +.file-uploader-summary { + display: inline-block; + vertical-align: top; +} + +.file-uploader-button { + background: @color-gray-darken0; + border: 1px solid @color-gray_light; + box-sizing: border-box; + color: @color-black_dark; + cursor: pointer; + display: inline-block; + font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-size: 1.4rem; + font-weight: 600; + line-height: 1.6rem; + margin: 0; + padding: 7px 15px; + vertical-align: middle; + + &._is-dragover { + background: @file-uploader-dragover__background; + border: 1px solid @file-uploader-preview-focus__color; + } +} + +.file-uploader-spinner { + background-image: url('@{baseDir}images/loader-1.gif'); + background-position: 50%; + background-repeat: no-repeat; + background-size: @file-uploader-spinner-dimensions; + display: none; + height: 30px; + margin-left: @indent__s; + vertical-align: top; + width: @file-uploader-spinner-dimensions; +} + +.file-uploader-preview { + .action-remove { + &:extend(.abs-action-reset all); + .lib-icon-font ( + @icon-delete__content, + @_icon-font: @icons__font-name, + @_icon-font-size: @file-uploader-delete-icon__font-size, + @_icon-font-color: @file-uploader-delete-icon__color, + @_icon-font-color-hover: @file-uploader-delete-icon__hover__color, + @_icon-font-text-hide: true, + @_icon-font-display: block + ); + bottom: 4px; + cursor: pointer; + display: block; + height: 27px; + left: 6px; + padding: 2px; + position: absolute; + text-decoration: none; + width: 25px; + z-index: 2; + } + + &:hover { + .preview-image img, + .preview-link:before { + opacity: @file-uploader-preview__opacity; + } + } + + .preview-link { + display: block; + height: 100%; + } + + .preview-image img { + bottom: 0; + left: 0; + margin: auto; + max-height: 100%; + max-width: 100%; + position: absolute; + right: 0; + top: 0; + z-index: 1; + } + + .preview-video { + .lib-icon-font( + @icon-file__content, + @_icon-font: @icons__font-name, + @_icon-font-size: @file-uploader-video-icon__size, + @_icon-font-color: @file-uploader-video-icon__color, + @_icon-font-color-hover: @file-uploader-video-icon__color + ); + + &:before { + left: 0; + margin-top: -@file-uploader-video-icon__size / 2; + position: absolute; + right: 0; + top: 50%; + z-index: @file-uploader-video-icon__z-index; + } + } + + .preview-document { + .lib-icon-font( + @icon-file__content, + @_icon-font: @icons__font-name, + @_icon-font-size: @file-uploader-document-icon__size, + @_icon-font-color: @file-uploader-document-icon__color, + @_icon-font-color-hover: @file-uploader-document-icon__color + ); + + &:before { + left: 0; + margin-top: -@file-uploader-document-icon__size / 2; + position: absolute; + right: 0; + top: 50%; + z-index: @file-uploader-document-icon__z-index; + } + } +} + +.file-uploader-preview, +.file-uploader-placeholder { + background: @file-uploader-preview__background-color; + border: 1px solid @file-uploader-preview__border-color; + box-sizing: border-box; + cursor: pointer; + display: block; + height: @file-uploader-preview__height; + line-height: 1; + margin: @indent__s @indent__m @indent__s 0; + overflow: hidden; + position: relative; + width: @file-uploader-preview__width; +} + +.file-uploader { + &._loading { + .file-uploader-spinner { + display: inline-block; + } + } + + .admin__field-note, + .admin__field-error { + margin-bottom: @indent__s; + } + + .file-uploader-filename { + .lib-text-overflow(); + max-width: @file-uploader-preview__width; + word-break: break-all; + + &:first-child { + margin-bottom: @indent__s; + } + } + + .file-uploader-meta { + color: @file-uploader-muted-text__color; + } + + .admin__field-fallback-reset { + margin-left: @indent__s; + } + + ._keyfocus & .action-remove { + &:focus { + box-shadow: 0 0 0 1px @file-uploader-preview-focus__color; + } + } +} + +// Placeholder for multiple uploader +.file-uploader-placeholder { + &.placeholder-document { + .lib-icon-font( + @icon-file__content, + @_icon-font: @icons__font-name, + @_icon-font-size: 5rem, + @_icon-font-color: @file-uploader-placeholder-icon__color, + @_icon-font-color-hover: @file-uploader-placeholder-icon__color + ); + + &:before { + left: 0; + position: absolute; + right: 0; + top: 20px; + z-index: @file-uploader-placeholder-icon__z-index; + } + } + + &.placeholder-image { + .lib-icon-font( + @icon-file__content, + @_icon-font: @icons__font-name, + @_icon-font-size: 5rem, + @_icon-font-color: @file-uploader-placeholder-icon__color, + @_icon-font-color-hover: @file-uploader-placeholder-icon__color + ); + + &:before { + left: 0; + position: absolute; + right: 0; + top: 20px; + z-index: @file-uploader-placeholder-icon__z-index; + } + } + + &.placeholder-video { + .lib-icon-font( + @icon-file__content, + @_icon-font: @icons__font-name, + @_icon-font-size: 3rem, + @_icon-font-color: @file-uploader-placeholder-icon__color, + @_icon-font-color-hover: @file-uploader-placeholder-icon__color + ); + + &:before { + left: 0; + position: absolute; + right: 0; + top: 30px; + z-index: @file-uploader-placeholder-icon__z-index; + } + } +} + +.file-uploader-placeholder-text { + bottom: 0; + color: @color-blue-dodger; + font-size: 1.1rem; + left: 0; + line-height: @line-height__base; + margin-bottom: 15%; + padding: 0 @indent__base; + position: absolute; + right: 0; + text-align: center; +} + +// +// Grid image uploader +// --------------------------------------------- + +.data-grid-file-uploader { + min-width: @data-grid-file-uploader-wrapper__size; + + &._loading { + .file-uploader-spinner { + display: block; + } + + .file-uploader-button { + &:before { + display: none; + } + } + } + + .file-uploader-image { + background: transparent; + bottom: 0; + left: 0; + margin: auto; + max-height: 100%; + max-width: 100%; + position: absolute; + right: 0; + top: 0; + z-index: @data-grid-file-uploader-image__z-index; + + + .file-uploader-area { + .file-uploader-button { + &:before { + display: none; + } + } + } + } + + .file-uploader-area { + z-index: @data-grid-file-uploader-image__z-index + 1; + } + + .file-uploader-spinner { + height: 100%; + margin: 0; + position: absolute; + top: 0; + width: 100%; + } + + .file-uploader-button { + display: block; + height: @data-grid-file-uploader-upload-icon__line-height; + text-align: center; + + .lib-icon-font ( + @icon-file__content, + @_icon-font: @icons__font-name, + @_icon-font-size: 1.3rem, + @_icon-font-line-height: @data-grid-file-uploader-upload-icon__line-height, + @_icon-font-color: @data-grid-file-uploader-upload-icon__color, + @_icon-font-color-hover: @data-grid-file-uploader-upload-icon__hover__color, + @_icon-font-text-hide: true, + @_icon-font-display: block + ); + } + + .action-select-wrap { + float: left; + + .action-select { + border: 1px solid @file-uploader-preview__border-color; + display: block; + height: @data-grid-file-uploader-image__size; + margin-left: -1px; + padding: 0; + width: @data-grid-file-uploader-menu-button__width; + + &:after { + border-color: @data-grid-file-uploader-upload-icon__color transparent transparent transparent; + left: 50%; + margin: 0 0 0 -5px; + } + + &:hover { + &:after { + border-color: @data-grid-file-uploader-upload-icon__hover__color transparent transparent transparent; + } + } + + > span { + display: none; + } + } + + .action-menu { + left: 4rem; + right: auto; + z-index: @data-grid-file-uploader-image__z-index + 1; + } + } +} + +.data-grid-file-uploader-inner { + border: 1px solid @file-uploader-preview__border-color; + float: left; + height: @data-grid-file-uploader-image__size; + position: relative; + width: @data-grid-file-uploader-image__size; +} diff --git a/app/design/frontend/Magento/luma/Magento_Customer/layout/default.xml b/app/design/frontend/Magento/luma/Magento_Customer/layout/default.xml index 1f8c162ef923..4b08bf28ece9 100644 --- a/app/design/frontend/Magento/luma/Magento_Customer/layout/default.xml +++ b/app/design/frontend/Magento/luma/Magento_Customer/layout/default.xml @@ -15,11 +15,6 @@ </arguments> </block> </referenceBlock> - <block class="Magento\Theme\Block\Html\Header" name="header" as="header"> - <arguments> - <argument name="show_part" xsi:type="string">welcome</argument> - </arguments> - </block> <move element="header" destination="header.links" before="-"/> <move element="register-link" destination="header.links"/> <move element="top.links" destination="customer"/> 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 d7ae6c3b28f4..6adf4b5b2f86 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 @@ -70,7 +70,7 @@ } .form-address-edit { - #region_id { + .region_id { display: none; } @@ -161,6 +161,7 @@ .table-wrapper { .lib-css(margin-bottom, @indent__base); border-bottom: 1px solid @account-table-border-bottom-color; + overflow-x: auto; &:last-child { margin-bottom: 0; @@ -199,7 +200,7 @@ // Checkout address (create shipping address) .field.street { - .field.additional { + .field { .label { &:extend(.abs-visually-hidden all); } @@ -334,13 +335,17 @@ } } - .order-products-toolbar { + .order-products-toolbar, + .customer-addresses-toolbar { position: relative; .toolbar-amount { position: relative; text-align: center; } + .pages { + position: relative; + } } } @@ -371,7 +376,7 @@ .fieldset { > .field { > .control { - width: 55%; + width: 80%; } } } @@ -402,7 +407,9 @@ .form.password.reset, .form.send.confirmation, .form.password.forget, - .form.create.account { + .form.create.account, + .form.search.advanced, + .form.form-orders-search { min-width: 600px; width: 50%; } @@ -417,6 +424,12 @@ .column.main { width: 77.7%; } + + .sidebar-main { + .block { + margin-bottom: 0; + } + } } .account { @@ -528,11 +541,18 @@ .column.main, .sidebar-additional { margin: 0; + padding: 0; } .data.table { &:extend(.abs-table-striped-mobile all); } + + .sidebar-main { + .account-nav { + margin-bottom: 0; + } + } } } @@ -550,8 +570,8 @@ } .account { - .page.messages { - margin-bottom: @indent__xl; + .messages { + margin-bottom: 0; } .column.main { @@ -589,4 +609,15 @@ position: relative; } } + + .form.search.advanced { + .field.price { + .with-addon { + .input-text { + flex-basis: auto; + width: 100%; + } + } + } + } } diff --git a/app/design/frontend/Magento/luma/Magento_GiftMessage/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_GiftMessage/web/css/source/_module.less index 388ec32b4fc5..ff377a4b88ac 100644 --- a/app/design/frontend/Magento/luma/Magento_GiftMessage/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_GiftMessage/web/css/source/_module.less @@ -12,7 +12,7 @@ @gift-item-block__border-color: @color-gray-light5; @gift-item-block__border-width: @border-width__base; -@gift-item-block-title__color: @color-blue1; +@gift-item-block-title__color: @link__color; @gift-item-block-title-icon__content: @icon-down; @gift-item-block-title-icon__active__content: @icon-up; @gift-item-block-title-icon__color: @color-gray52; @@ -60,8 +60,8 @@ } .gift-summary { - position: relative; margin-top: @indent__s; + position: relative; .actions-toolbar { > .secondary { @@ -246,6 +246,9 @@ .gift-messages-order { margin-bottom: @indent__m; } + .gift-message-summary { + padding-right: 7rem; + } } // @@ -282,10 +285,6 @@ } } - .gift-message-summary { - padding-right: 7rem; - } - // // In-table block // --------------------------------------------- diff --git a/app/design/frontend/Magento/luma/Magento_GroupedProduct/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_GroupedProduct/web/css/source/_module.less index 088372808aa6..e9c40f638624 100644 --- a/app/design/frontend/Magento/luma/Magento_GroupedProduct/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_GroupedProduct/web/css/source/_module.less @@ -70,6 +70,10 @@ clear: left; } } + + .box-tocart { + margin-top: @indent__s; + } } } @@ -133,9 +137,18 @@ } .media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { - .table-wrapper.grouped { - .lib-css(margin-left, -@layout__width-xs-indent); - .lib-css(margin-right, -@layout__width-xs-indent); + .product-add-form { + .table-wrapper.grouped { + .lib-css(margin-left, -@layout__width-xs-indent); + .lib-css(margin-right, -@layout__width-xs-indent); + .table.data.grouped { + tr { + td { + padding: 5px 10px 5px 15px; + } + } + } + } } } diff --git a/app/design/frontend/Magento/luma/Magento_LayeredNavigation/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_LayeredNavigation/web/css/source/_module.less index 48461892fa63..76baec87e005 100644 --- a/app/design/frontend/Magento/luma/Magento_LayeredNavigation/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_LayeredNavigation/web/css/source/_module.less @@ -32,7 +32,7 @@ &[data-count]:after { .lib-css(color, @color-white); - background: @color-orange-red1; + background: @theme__color__secondary; border-radius: 2px; content: attr(data-count); display: inline-block; diff --git a/app/design/frontend/Magento/luma/Magento_Msrp/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Msrp/web/css/source/_module.less index 112184b45fe8..475361c56afc 100644 --- a/app/design/frontend/Magento/luma/Magento_Msrp/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Msrp/web/css/source/_module.less @@ -74,6 +74,10 @@ } } + .map-fallback-price { + display: none; + } + .map-old-price, .product-item .map-old-price, .product-info-price .map-show-info { diff --git a/app/design/frontend/Magento/luma/Magento_MultipleWishlist/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_MultipleWishlist/web/css/source/_module.less index 6ab7a8e47a17..7ed4a9e64e94 100644 --- a/app/design/frontend/Magento/luma/Magento_MultipleWishlist/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_MultipleWishlist/web/css/source/_module.less @@ -40,6 +40,7 @@ .items { padding: 6px 0; text-align: left; + .item { > span { display: block; @@ -47,6 +48,7 @@ } } + li { padding: 0; } @@ -115,6 +117,7 @@ &.split, &.toggle { .lib-css(color, @link__color); + &:before { display: none; } @@ -135,6 +138,7 @@ &.overlay { .lib-window-overlay(); + &.active { display: block; } @@ -254,7 +258,7 @@ } .form-wishlist-search { - .lib-css(margin-bottom, @indent__l*2); + margin-bottom: @indent__l * 2; max-width: 500px; .fieldset { @@ -281,7 +285,7 @@ .block-wishlist-info-items { .block-title { - .lib-css(margin-bottom, @indent__base); + margin-bottom: @indent__base; .lib-font-size(22px); > strong { @@ -352,6 +356,7 @@ // Select wish list &-select { margin: 0 -@layout__width-xs-indent 20px; + .wishlist-name { .lib-font-size(16); &:extend(.abs-toggling-title-mobile all); @@ -424,7 +429,7 @@ .product { &-item { &-checkbox { - left: 20px; + left: 0; position: absolute; top: 20px; } diff --git a/app/design/frontend/Magento/luma/Magento_Multishipping/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Multishipping/web/css/source/_module.less index 7662c60734a1..a94e2cae46b1 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 @@ -48,7 +48,7 @@ } .error-block { - color: @color-red10; + color: @error__color; .error-label { font-weight: @font-weight__bold; @@ -230,7 +230,7 @@ } .error-description { - color: @color-red10; + color: @error__color; font-weight: @font-weight__regular; margin-bottom: @indent__s; margin-top: -@indent__s; @@ -345,6 +345,22 @@ .data.table { &:extend(.abs-checkout-order-review all); + &.table-order-review { + > tbody { + > tr { + > td { + &.col { + &.subtotal { + border-bottom: none; + } + &.qty { + text-align: center; + } + } + } + } + } + } } } @@ -374,7 +390,7 @@ text-align: right; .action { - margin-left: @indent__s; + margin-left: 0; &.back { display: block; @@ -496,4 +512,12 @@ margin-left: @indent__xl; } } + + .multicheckout { + .actions-toolbar { + > .primary { + margin-right: 0; + } + } + } } diff --git a/app/design/frontend/Magento/luma/Magento_Newsletter/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Newsletter/web/css/source/_module.less index 9ccd6c190ec0..d7ee1319c9a4 100644 --- a/app/design/frontend/Magento/luma/Magento_Newsletter/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Newsletter/web/css/source/_module.less @@ -81,3 +81,24 @@ width: 34%; } } + +// +// Mobile +// _____________________________________________ + +.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { + .block { + &.newsletter { + input { + font-size: 12px; + padding-left: 30px; + } + + .field { + .control:before { + font-size: 13px; + } + } + } + } +} diff --git a/app/design/frontend/Magento/luma/Magento_Review/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Review/web/css/source/_module.less index da78406f9221..4b5e03f8013b 100644 --- a/app/design/frontend/Magento/luma/Magento_Review/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Review/web/css/source/_module.less @@ -14,7 +14,6 @@ // _____________________________________________ & when (@media-common = true) { - .data.switch .counter { .lib-css(color, @text__color__muted); @@ -118,13 +117,13 @@ strong { display: block; - font-weight: 600; + font-weight: @font-weight__semibold; } } .fieldset &-field-ratings { > .label { - font-weight: 600; + font-weight: @font-weight__semibold; margin-bottom: @indent__s; padding: 0; } @@ -140,11 +139,11 @@ &-field-rating { .label { - font-weight: 600; + font-weight: @font-weight__semibold; } .control { - margin-bottom: 1.2*@indent__xl; + margin-bottom: 1.2 * @indent__xl; margin-top: @indent__s; } } @@ -181,7 +180,7 @@ display: inline; .review-details-value { - font-weight: 400; + font-weight: @font-weight__regular; } } @@ -298,6 +297,9 @@ a:not(:last-child) { margin-right: 30px; } + .action.add { + white-space: nowrap; + } } } @@ -341,7 +343,7 @@ .block-reviews-dashboard { .items { .item { - .lib-css(margin-bottom, @indent__base); + margin-bottom: @indent__base; &:last-child { margin-bottom: 0; @@ -353,14 +355,15 @@ display: inline-block; &:not(:last-child) { - .lib-css(margin-bottom, @indent__xs); + margin-bottom: @indent__xs; } } .rating-summary { .label { - .lib-css(font-weight, @font-weight__semibold); - .lib-css(margin-right, @indent__s); + font-weight: @font-weight__semibold; + margin-right: @indent__s; + vertical-align: middle; } } } @@ -368,7 +371,7 @@ .table-reviews, .block-reviews-dashboard { .product-name { - .lib-css(font-weight, @font-weight__regular); + font-weight: @font-weight__regular; } } @@ -416,6 +419,7 @@ .product-details { &:extend(.abs-add-clearfix all); &:extend(.abs-margin-for-blocks-and-widgets all); + .rating-average-label { &:extend(.abs-visually-hidden all); } @@ -436,9 +440,10 @@ } .customer-review-rating { - .lib-css(margin-bottom, @indent__base); + margin-bottom: @indent__base; + .item { - .lib-css(margin-bottom, @indent__s); + margin-bottom: @indent__s; &:last-child { margin-bottom: 0; @@ -450,13 +455,13 @@ .review-title { .lib-heading(h3); - .lib-css(font-weight, @font-weight__semibold); - .lib-css(margin-bottom, @indent__base); + font-weight: @font-weight__semibold; + margin-bottom: @indent__base; } .review-content { margin: 0; - .lib-css(margin-bottom, @indent__base); + margin-bottom: @indent__base; } .review-date { @@ -473,13 +478,13 @@ .media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__s) { .customer-review { .product-name { - .lib-css(margin-bottom, @indent__xs); + margin-bottom: @indent__xs; } .product-reviews-summary { .rating-summary { display: block; - .lib-css(margin-bottom, @indent__xs); + margin-bottom: @indent__xs; } } } diff --git a/app/design/frontend/Magento/luma/Magento_Rma/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Rma/web/css/source/_module.less index 7c96827c5f6b..e8adcc2f0e4f 100644 --- a/app/design/frontend/Magento/luma/Magento_Rma/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Rma/web/css/source/_module.less @@ -40,6 +40,14 @@ &:extend(.abs-status all); } + .table-wrapper.table-returns { + .returns-details { + &.hidden { + display: none; + } + } + } + .block-returns-comments { .returns-comments { dt, diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_update.html b/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_update.html index a7b9b330ab9c..269e46d75208 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_update.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_update.html @@ -11,7 +11,7 @@ "var this.getUrl($store, 'customer/account/')":"Customer Account URL", "var order.getCustomerName()":"Customer Name", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status" +"var order.getFrontendStatusLabel()":"Order Status" } @--> {{template config_path="design/email/header_template"}} @@ -24,7 +24,7 @@ "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order.getFrontendStatusLabel() |raw}} {{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}} </p> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_update_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_update_guest.html index 36279eb26005..c8bdae7b08fa 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_update_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_update_guest.html @@ -10,7 +10,7 @@ "var creditmemo.increment_id":"Credit Memo Id", "var billing.getName()":"Guest Customer Name", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status" +"var order.getFrontendStatusLabel()":"Order Status" } @--> {{template config_path="design/email/header_template"}} @@ -23,7 +23,7 @@ "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order.getFrontendStatusLabel() |raw}} </p> <p> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_update.html b/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_update.html index a739c9f54b08..8ec54f1e64d9 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_update.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_update.html @@ -11,7 +11,7 @@ "var comment":"Invoice Comment", "var invoice.increment_id":"Invoice Id", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status" +"var order.getFrontendStatusLabel()":"Order Status" } @--> {{template config_path="design/email/header_template"}} @@ -24,7 +24,7 @@ "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order.getFrontendStatusLabel() |raw}} {{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}} </p> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_update_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_update_guest.html index a56ee6da9fa2..6028db7b9773 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_update_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_update_guest.html @@ -10,7 +10,7 @@ "var comment":"Invoice Comment", "var invoice.increment_id":"Invoice Id", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status" +"var order.getFrontendStatusLabel()":"Order Status" } @--> {{template config_path="design/email/header_template"}} @@ -23,7 +23,7 @@ "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order.getFrontendStatusLabel() |raw}} </p> <p> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/order_update.html b/app/design/frontend/Magento/luma/Magento_Sales/email/order_update.html index 3e4bf8df2f10..fa16ac2196bf 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/order_update.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/order_update.html @@ -10,7 +10,7 @@ "var order.getCustomerName()":"Customer Name", "var comment":"Order Comment", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status" +"var order.getFrontendStatusLabel()":"Order Status" } @--> {{template config_path="design/email/header_template"}} @@ -23,7 +23,7 @@ "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order.getFrontendStatusLabel() |raw}} {{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}} </p> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/order_update_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/order_update_guest.html index 1075608db434..8ead615fe01c 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/order_update_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/order_update_guest.html @@ -9,7 +9,7 @@ "var billing.getName()":"Guest Customer Name", "var comment":"Order Comment", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status" +"var order.getFrontendStatusLabel()":"Order Status" } @--> {{template config_path="design/email/header_template"}} @@ -22,7 +22,7 @@ "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order.getFrontendStatusLabel() |raw}} </p> <p> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new.html b/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new.html index 8c4084fcaf49..e467aa843e2f 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 68f1886986c5..385110f8f037 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new_guest.html @@ -49,7 +49,7 @@ <h1>{{trans "Your Shipment #%shipment_id for Order #%order_id" shipment_id=$ship </tr> </table> {{/depend}} - {{block class='Magento\\Framework\\View\\Element\\Template' area='frontend' template='Magento_Sales::email/shipment/track.phtml' shipment=$shipment order=$order}} + {{layout handle="sales_email_order_shipment_track" shipment=$shipment order=$order}} <table class="order-details"> <tr> <td class="address-details"> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_update.html b/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_update.html index 37bf92b866c7..4f9b7286f3ae 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_update.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_update.html @@ -10,7 +10,7 @@ "var order.getCustomerName()":"Customer Name", "var comment":"Order Comment", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status", +"var order.getFrontendStatusLabel()":"Order Status", "var shipment.increment_id":"Shipment Id" } @--> {{template config_path="design/email/header_template"}} @@ -24,7 +24,7 @@ "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order.getFrontendStatusLabel() |raw}} {{trans 'You can check the status of your order by <a href="%account_url">logging into your account</a>.' account_url=$this.getUrl($store,'customer/account/',[_nosid:1]) |raw}} </p> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_update_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_update_guest.html index 954819949860..3ef26463ea75 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_update_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_update_guest.html @@ -9,7 +9,7 @@ "var billing.getName()":"Guest Customer Name", "var comment":"Order Comment", "var order.increment_id":"Order Id", -"var order.getStatusLabel()":"Order Status", +"var order.getFrontendStatusLabel()":"Order Status", "var shipment.increment_id":"Shipment Id" } @--> {{template config_path="design/email/header_template"}} @@ -23,7 +23,7 @@ "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." increment_id=$order.increment_id - order_status=$order.getStatusLabel() + order_status=$order.getFrontendStatusLabel() |raw}} </p> <p> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/web/css/source/_email.less b/app/design/frontend/Magento/luma/Magento_Sales/web/css/source/_email.less index 3f19d1020bab..31c128e07e3a 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/web/css/source/_email.less +++ b/app/design/frontend/Magento/luma/Magento_Sales/web/css/source/_email.less @@ -76,8 +76,10 @@ } // Remove address and phone number link color on iOS -.address-details a { - &:extend(.no-link a); +.email-non-inline() { + .address-details a { + &:extend(.no-link a); + } } .media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__xs) { diff --git a/app/design/frontend/Magento/luma/Magento_Sales/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Sales/web/css/source/_module.less index a0f734b05cbd..1be46c8239ee 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Sales/web/css/source/_module.less @@ -343,16 +343,22 @@ } .product-item-name { - display: inline-block; + float: left; + width: calc(100% - 20px); + } + .product-item::after { + clear: both; + content: ''; + display: table; } - .product-item { .label { &:extend(.abs-visually-hidden all); } .field.item { - display: inline-block; + float: left; + width: 20px; } } } @@ -392,6 +398,17 @@ &.orders-recent { &:extend(.abs-account-table-margin-mobile all); &:extend(.abs-no-border-top all); + .table-order-items { + &.table { + tbody { + > tr { + > td.col { + padding-left: 0; + } + } + } + } + } } } @@ -555,13 +572,13 @@ margin: 0 @tab-control__margin-right 0 0; a { - padding: @tab-control__padding-top @tab-control__padding-right; + padding: @tab-control__padding-top @indent__base; } strong { border-bottom: 0; margin-bottom: -1px; - padding: @tab-control__padding-top @tab-control__padding-right @tab-control__padding-bottom + 1 @tab-control__padding-left; + padding: @tab-control__padding-top @indent__base @tab-control__padding-bottom + 1 @indent__base; } } } @@ -612,10 +629,6 @@ padding: 25px; .col { - &.name { - padding-left: 0; - } - &.price { text-align: center; } @@ -691,3 +704,19 @@ } } } + +.media-width(@extremum, @break) when (@extremum = 'min') and (@break = @screen__l) { + .order-links { + .item { + margin: 0 @tab-control__margin-right 0 0; + + a { + padding: @tab-control__padding-top @tab-control__padding-right; + } + + strong { + padding: @tab-control__padding-top @tab-control__padding-right @tab-control__padding-bottom + 1 @tab-control__padding-left; + } + } + } +} diff --git a/app/design/frontend/Magento/luma/Magento_SendFriend/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_SendFriend/web/css/source/_module.less index baf5468b1848..3435736a54a6 100644 --- a/app/design/frontend/Magento/luma/Magento_SendFriend/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_SendFriend/web/css/source/_module.less @@ -10,6 +10,14 @@ & when (@media-common = true) { .form.send.friend { &:extend(.abs-add-fields all); + + .fieldset { + .field { + .control { + width: 100%; + } + } + } } .product-social-links .action.mailto.friend { @@ -44,3 +52,18 @@ } } } +.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { + .form.send.friend { + .fieldset { + padding-bottom: @indent__xs; + } + + .action { + &.remove { + margin-left: 0; + right: 0; + top: 100%; + } + } + } +} diff --git a/app/design/frontend/Magento/luma/Magento_Theme/layout/default_head_blocks.xml b/app/design/frontend/Magento/luma/Magento_Theme/layout/default_head_blocks.xml index 2dfb1d3ee6bf..7e64e5f3f01c 100644 --- a/app/design/frontend/Magento/luma/Magento_Theme/layout/default_head_blocks.xml +++ b/app/design/frontend/Magento/luma/Magento_Theme/layout/default_head_blocks.xml @@ -7,6 +7,6 @@ --> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <head> - <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no"/> + <meta name="viewport" content="width=device-width, initial-scale=1"/> </head> </page> diff --git a/app/design/frontend/Magento/luma/Magento_Theme/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Theme/web/css/source/_module.less index bafe1be57ee3..dfcc51e0a0a2 100644 --- a/app/design/frontend/Magento/luma/Magento_Theme/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Theme/web/css/source/_module.less @@ -77,6 +77,14 @@ .lib-vendor-prefix-flex-grow(1); } + .page-main { + > .page-title-wrapper { + .page-title { + word-break: break-all; + } + } + } + // // Header // --------------------------------------------- @@ -104,6 +112,10 @@ font-size: @font-size__base; margin: 0 0 0 15px; + &.customer-welcome { + margin: 0 0 0 5px; + } + > a { .lib-link( @_link-color: @header-panel__text-color, @@ -144,6 +156,12 @@ } } + .page-print { + .nav-toggle { + display: none; + } + } + .page-main { > .page-title-wrapper { .page-title + .action { @@ -312,6 +330,23 @@ } } } + .page-header { + .switcher { + .options { + ul.dropdown { + right: 0; + &:before { + left: auto; + right: 10px; + } + &:after { + left: auto; + right: 9px; + } + } + } + } + } // // Widgets @@ -422,7 +457,7 @@ } } -.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__s) { +.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { .logo { margin-bottom: 13px; margin-top: 4px; @@ -435,7 +470,7 @@ .media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { .cms-page-view .page-main { - padding-top: 41px; + padding-top: 0; position: relative; } } @@ -627,10 +662,6 @@ box-sizing: border-box; width: 100%; } - - .ie11 & { - height: 100%; - } } .page-footer { 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 6455921a71ad..85e8aeb0b515 100644 --- a/app/design/frontend/Magento/luma/Magento_Wishlist/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Wishlist/web/css/source/_module.less @@ -8,6 +8,24 @@ // _____________________________________________ & when (@media-common = true) { + .toolbar { + &.wishlist-toolbar { + .limiter { + float: right; + } + .main .pages { + display: inline-block; + position: relative; + z-index: 0; + } + .toolbar-amount, + .limiter { + display: inline-block; + z-index: 1; + } + } + } + .form.wishlist.items { .actions-toolbar { &:extend(.abs-reset-left-margin all); @@ -164,6 +182,30 @@ } } } + .products-grid.wishlist { + .product-item-actions { + .action { + &.edit, + &.delete { + .lib-icon-font( + @icon-edit, + @_icon-font-size: 18px, + @_icon-font-line-height: 20px, + @_icon-font-text-hide: true, + @_icon-font-color: @minicart-icons-color, + @_icon-font-color-hover: @primary__color, + @_icon-font-color-active: @minicart-icons-color + ); + } + + &.delete { + .lib-icon-font-symbol( + @_icon-font-content: @icon-trash + ); + } + } + } + } } // @@ -185,11 +227,11 @@ .media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { .products-grid.wishlist { margin-bottom: @indent__l; - margin-right: -@indent__s; + margin-right: 0; .product { &-item { - padding: @indent__base @indent__s @indent__base @indent__base; + padding: @indent__base 0 @indent__base 0; position: relative; &-photo { @@ -203,6 +245,7 @@ &-actions { display: block; + float: left; .action { margin-right: 15px; @@ -210,15 +253,7 @@ &:last-child { margin-right: 0; } - - &.edit { - float: left; - } - - &.delete { - float: right; - } - + &.edit, &.delete { margin-top: 7px; @@ -277,6 +312,9 @@ @_icon-font-color-hover: @primary__color, @_icon-font-color-active: @minicart-icons-color ); + &:before { + overflow: visible; + } } } } @@ -357,9 +395,7 @@ width: auto; } } -} -.media-width(@extremum, @break) when (@extremum = 'min') and (@break = @screen__m) { .wishlist-index-index { .product-item-inner { @_shadow: 3px 4px 4px 0 rgba(0, 0, 0, .3); diff --git a/app/design/frontend/Magento/luma/etc/view.xml b/app/design/frontend/Magento/luma/etc/view.xml index 55d43272caad..7aa2e51481bd 100644 --- a/app/design/frontend/Magento/luma/etc/view.xml +++ b/app/design/frontend/Magento/luma/etc/view.xml @@ -256,7 +256,7 @@ <var name="product_list_image_size">166</var> <!-- New Product image size used in product list --> <var name="product_zoom_image_size">370</var> <!-- New Product image size used for zooming --> - <var name="product_image_white_borders">0</var> + <var name="product_image_white_borders">1</var> </vars> <vars module="Magento_Bundle"> <var name="product_summary_image_size">58</var> <!-- New Product image size used for summary block--> diff --git a/app/design/frontend/Magento/luma/web/css/source/_extends.less b/app/design/frontend/Magento/luma/web/css/source/_extends.less index 7c9f5b7a65ab..a88ecbf5057b 100644 --- a/app/design/frontend/Magento/luma/web/css/source/_extends.less +++ b/app/design/frontend/Magento/luma/web/css/source/_extends.less @@ -277,7 +277,7 @@ } // -// Marging for blocks & widgets +// Margin for blocks & widgets // --------------------------------------------- & when (@media-common = true) { @@ -1506,6 +1506,10 @@ position: relative; z-index: 1; } + .limiter { + display: inline-block; + float: right; + } .toolbar-amount { .lib-css(line-height, @pager__line-height); @@ -1859,7 +1863,7 @@ > .title { strong { - color: @color-blue1; + color: @link__color; font-weight: @font-weight__regular; } } diff --git a/app/design/frontend/Magento/luma/web/css/source/_forms.less b/app/design/frontend/Magento/luma/web/css/source/_forms.less index 0c7150c18550..8533318a12d1 100644 --- a/app/design/frontend/Magento/luma/web/css/source/_forms.less +++ b/app/design/frontend/Magento/luma/web/css/source/_forms.less @@ -20,7 +20,7 @@ .lib-form-fieldset(); &:last-child { - margin-bottom: 0; + margin-bottom: @indent__base; } > .field, @@ -104,6 +104,10 @@ .select-styling(); } + select.admin__control-multiselect { + height: auto; + } + .field-error, div.mage-error[generated] { margin-top: 7px; @@ -113,6 +117,18 @@ .lib-form-validation-note(); } + .product-options-wrapper { + .date { + &.required { + div[for*='options'] { + &.mage-error { + display: none !important; + } + } + } + } + } + // TEMP .field .tooltip { diff --git a/app/design/frontend/Magento/luma/web/css/source/_sections.less b/app/design/frontend/Magento/luma/web/css/source/_sections.less index 73665fd22da2..95769c4f4b6b 100644 --- a/app/design/frontend/Magento/luma/web/css/source/_sections.less +++ b/app/design/frontend/Magento/luma/web/css/source/_sections.less @@ -19,16 +19,16 @@ a { position: relative; .lib-icon-font( - @_icon-font-content: @icon-down, - @_icon-font-size: @font-size__base, - @_icon-font-line-height: @icon-font__line-height, - @_icon-font-color: @icon-font__color, - @_icon-font-color-hover: @icon-font__color-hover, - @_icon-font-color-active: @icon-font__color-active, - @_icon-font-margin: @icon-font__margin, - @_icon-font-vertical-align: @icon-font__vertical-align, - @_icon-font-position: after, - @_icon-font-display: false + @_icon-font-content: @icon-down, + @_icon-font-size: @font-size__base, + @_icon-font-line-height: @icon-font__line-height, + @_icon-font-color: @icon-font__color, + @_icon-font-color-hover: @icon-font__color-hover, + @_icon-font-color-active: @icon-font__color-active, + @_icon-font-margin: @icon-font__margin, + @_icon-font-vertical-align: @icon-font__vertical-align, + @_icon-font-position: after, + @_icon-font-display: false ); &:after { @@ -75,3 +75,17 @@ } } } + +.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { + .product.data.items { + .item.title { + > .switch { + padding: 1px 15px 1px; + } + } + + > .item.content { + padding: 10px 15px 30px; + } + } +} diff --git a/app/design/frontend/Magento/luma/web/css/source/_theme.less b/app/design/frontend/Magento/luma/web/css/source/_theme.less index 957e622d6a0b..114fb7b81726 100644 --- a/app/design/frontend/Magento/luma/web/css/source/_theme.less +++ b/app/design/frontend/Magento/luma/web/css/source/_theme.less @@ -28,7 +28,7 @@ @h3__margin-top: @indent__base; // Links -@link__color: @color-blue2; +@link__color: @theme__color__primary-alt; // Focus @focus__color: @color-blue3; @@ -60,14 +60,9 @@ @navigation__background: @color-gray94; @navigation-level0-item__active__color: @primary__color; -@navigation-level0-item__active__border-color: @active__color; -@navigation-desktop-level0-item__active__border-color: @active__color; -@submenu-desktop-item__active__border-color: @active__color; -@submenu-item__active__border-color: @active__color; @submenu-item__active__color: @navigation-level0-item__active__color; - // Desktop navigation @navigation-desktop-level0-item__line-height: 47px; @submenu-desktop__font-weight: @font-weight__regular; @@ -225,7 +220,6 @@ @rating-icon__font-size: 16px; @rating-icon__letter-spacing: 2px; -@rating-icon__active__color: @active__color; // // Dropdowns @@ -252,7 +246,7 @@ @breadcrumbs-icon__font-margin: 0 @indent__s; @breadcrumbs-current__color: @color-gray-middle5; -@breadcrumbs-link__color: @color-blue2; +@breadcrumbs-link__color: @link__color; @breadcrumbs-link__visited__color: @breadcrumbs-link__color; @breadcrumbs-link__hover__color: @breadcrumbs-link__color; @breadcrumbs-link__active__color: @breadcrumbs-link__color; diff --git a/app/design/frontend/Magento/luma/web/css/source/components/_modals_extend.less b/app/design/frontend/Magento/luma/web/css/source/components/_modals_extend.less index ae68fc81acd6..855700806fdb 100644 --- a/app/design/frontend/Magento/luma/web/css/source/components/_modals_extend.less +++ b/app/design/frontend/Magento/luma/web/css/source/components/_modals_extend.less @@ -58,7 +58,7 @@ .modal-custom { .action-close { - .lib-css(margin, @indent__m); + .lib-css(margin, 15px); } } @@ -82,7 +82,8 @@ .modal-slide { .action-close { - padding: @modal-slide-action-close__padding; + margin: 15px; + padding: 0; } .page-main-actions { @@ -147,10 +148,9 @@ } } } -} -.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { .modal-popup { + pointer-events: auto; &.modal-slide { .modal-inner-wrap[class] { .lib-css(background-color, @modal-slide-mobile__background-color); diff --git a/app/etc/db_schema.xml b/app/etc/db_schema.xml index be13c28ee029..d7af9091b238 100644 --- a/app/etc/db_schema.xml +++ b/app/etc/db_schema.xml @@ -10,7 +10,7 @@ <table name="patch_list" resource="default" comment="List of data/schema patches"> <column xsi:type="int" name="patch_id" identity="true" comment="Patch Auto Increment" /> <column xsi:type="varchar" name="patch_name" length="1024" nullable="false" comment="Patch Class Name" /> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="patch_id" /> </constraint> </table> diff --git a/app/etc/di.xml b/app/etc/di.xml index 5bc25e6cb85f..200a56201239 100755 --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -28,6 +28,7 @@ <preference for="Magento\Framework\App\CacheInterface" type="Magento\Framework\App\Cache\Proxy" /> <preference for="Magento\Framework\App\Cache\StateInterface" type="Magento\Framework\App\Cache\State" /> <preference for="Magento\Framework\App\Cache\TypeListInterface" type="Magento\Framework\App\Cache\TypeList" /> + <preference for="Magento\Framework\App\ObjectManager\ConfigWriterInterface" type="Magento\Framework\App\ObjectManager\ConfigWriter\Filesystem" /> <preference for="Magento\Store\Model\StoreManagerInterface" type="Magento\Store\Model\StoreManager" /> <preference for="Magento\Framework\View\DesignInterface" type="Magento\Theme\Model\View\Design\Proxy" /> <preference for="Magento\Framework\View\Design\ThemeInterface" type="Magento\Theme\Model\Theme" /> @@ -37,6 +38,7 @@ <preference for="Magento\Framework\Locale\ListsInterface" type="Magento\Framework\Locale\TranslatedLists" /> <preference for="Magento\Framework\Locale\AvailableLocalesInterface" type="Magento\Framework\Locale\Deployed\Codes" /> <preference for="Magento\Framework\Locale\OptionInterface" type="Magento\Framework\Locale\Deployed\Options" /> + <preference for="Magento\Framework\Lock\LockManagerInterface" type="Magento\Framework\Lock\Proxy" /> <preference for="Magento\Framework\Api\AttributeTypeResolverInterface" type="Magento\Framework\Reflection\AttributeTypeResolver" /> <preference for="Magento\Framework\Api\Search\SearchResultInterface" type="Magento\Framework\Api\Search\SearchResult" /> <preference for="Magento\Framework\Api\Search\SearchCriteriaInterface" type="Magento\Framework\Api\Search\SearchCriteria"/> @@ -87,6 +89,7 @@ <preference for="Magento\Framework\View\Design\Theme\CustomizationInterface" type="Magento\Framework\View\Design\Theme\Customization" /> <preference for="Magento\Framework\View\Asset\ConfigInterface" type="Magento\Framework\View\Asset\Config" /> <preference for="Magento\Framework\Image\Adapter\ConfigInterface" type="Magento\Framework\Image\Adapter\Config" /> + <preference for="Magento\Framework\Image\Adapter\UploadConfigInterface" type="Magento\Framework\Image\Adapter\Config" /> <preference for="Magento\Framework\View\Design\Theme\Image\PathInterface" type="Magento\Theme\Model\Theme\Image\Path" /> <preference for="Magento\Framework\Session\Config\ConfigInterface" type="Magento\Framework\Session\Config" /> <preference for="Magento\Framework\Session\SidResolverInterface" type="Magento\Framework\Session\SidResolver\Proxy" /> @@ -144,6 +147,7 @@ <preference for="Magento\Framework\Locale\FormatInterface" type="Magento\Framework\Locale\Format" /> <preference for="Magento\Framework\Locale\ResolverInterface" type="Magento\Framework\Locale\Resolver" /> <preference for="Magento\Framework\Stdlib\DateTime\TimezoneInterface" type="Magento\Framework\Stdlib\DateTime\Timezone" /> + <preference for="Magento\Framework\Stdlib\DateTime\Timezone\LocalizedDateToUtcConverterInterface" type="Magento\Framework\Stdlib\DateTime\Timezone\LocalizedDateToUtcConverter" /> <preference for="Magento\Framework\Communication\ConfigInterface" type="Magento\Framework\Communication\Config" /> <preference for="Magento\Framework\Module\ResourceInterface" type="Magento\Framework\Module\ModuleResource" /> <preference for="Magento\Framework\Pricing\Amount\AmountInterface" type="Magento\Framework\Pricing\Amount\Base" /> @@ -204,9 +208,8 @@ <preference for="Magento\Framework\MessageQueue\ExchangeFactoryInterface" type="Magento\Framework\MessageQueue\ExchangeFactory" /> <preference for="Magento\Framework\MessageQueue\Bulk\ExchangeFactoryInterface" type="Magento\Framework\MessageQueue\Bulk\ExchangeFactory" /> <preference for="Magento\Framework\MessageQueue\QueueFactoryInterface" type="Magento\Framework\MessageQueue\QueueFactory" /> - <preference for="Magento\Framework\App\Request\ValidatorInterface" - type="Magento\Framework\App\Request\CsrfValidator" /> <preference for="Magento\Framework\Search\Request\IndexScopeResolverInterface" type="Magento\Framework\Indexer\ScopeResolver\IndexScopeResolver"/> + <preference for="Magento\Framework\HTTP\ClientInterface" type="Magento\Framework\HTTP\Client\Curl" /> <type name="Magento\Framework\Model\ResourceModel\Db\TransactionManager" shared="false" /> <type name="Magento\Framework\Acl\Data\Cache"> <arguments> @@ -412,6 +415,11 @@ <argument name="cacheId" xsi:type="string">interception</argument> </arguments> </type> + <type name="Magento\Framework\Interception\Config\CacheManager"> + <arguments> + <argument name="cache" xsi:type="object">Magento\Framework\App\Cache\Type\Config</argument> + </arguments> + </type> <type name="Magento\Framework\Interception\PluginList\PluginList"> <arguments> <argument name="cache" xsi:type="object">Magento\Framework\App\Cache\Type\Config</argument> @@ -1484,6 +1492,27 @@ </argument> </arguments> </type> + <virtualType name="Magento\Framework\Setup\Declaration\Schema\Config\SchemaLocator" type="Magento\Framework\Config\SchemaLocator"> + <arguments> + <argument name="realPath" xsi:type="string">urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd</argument> + </arguments> + </virtualType> + <virtualType name="Magento\Framework\Setup\Declaration\Schema\FileSystem\XmlReader" type="Magento\Framework\Config\Reader\Filesystem"> + <arguments> + <argument name="fileResolver" xsi:type="object">Magento\Framework\Config\FileResolverByModule</argument> + <argument name="converter" xsi:type="object">Magento\Framework\Setup\Declaration\Schema\Config\Converter</argument> + <argument name="schemaLocator" xsi:type="object">Magento\Framework\Setup\Declaration\Schema\Config\SchemaLocator</argument> + <argument name="fileName" xsi:type="string">db_schema.xml</argument> + <argument name="idAttributes" xsi:type="array"> + <item name="/schema/table" xsi:type="string">name</item> + <item name="/schema/table/column" xsi:type="string">name</item> + <item name="/schema/table/constraint" xsi:type="string">referenceId</item> + <item name="/schema/table/index" xsi:type="string">referenceId</item> + <item name="/schema/table/index/column" xsi:type="string">name</item> + <item name="/schema/table/constraint/column" xsi:type="string">name</item> + </argument> + </arguments> + </virtualType> <type name="Magento\Framework\Setup\Declaration\Schema\OperationsExecutor"> <arguments> <argument name="operations" xsi:type="array"> @@ -1693,4 +1722,50 @@ <argument name="scopeType" xsi:type="const">Magento\Framework\App\Config\ScopeConfigInterface::SCOPE_TYPE_DEFAULT</argument> </arguments> </type> + <virtualType name="CsrfRequestValidator" type="Magento\Framework\App\Request\CsrfValidator" /> + <virtualType name="RequestValidator" type="Magento\Framework\App\Request\CompositeValidator"> + <arguments> + <argument name="validators" xsi:type="array"> + <item name="csrf_validator" xsi:type="object">CsrfRequestValidator</item> + <item name="http_method_validator" xsi:type="object"> + Magento\Framework\App\Request\HttpMethodValidator + </item> + </argument> + </arguments> + </virtualType> + <preference for="Magento\Framework\App\Request\ValidatorInterface" type="RequestValidator" /> + <type name="Magento\Framework\App\Request\HttpMethodMap"> + <arguments> + <argument name="map" xsi:type="array"> + <item name="OPTIONS" xsi:type="string">\Magento\Framework\App\Action\HttpOptionsActionInterface</item> + <item name="GET" xsi:type="string">\Magento\Framework\App\Action\HttpGetActionInterface</item> + <item name="HEAD" xsi:type="string">\Magento\Framework\App\Action\HttpGetActionInterface</item> + <item name="POST" xsi:type="string">\Magento\Framework\App\Action\HttpPostActionInterface</item> + <item name="PUT" xsi:type="string">\Magento\Framework\App\Action\HttpPutActionInterface</item> + <item name="PATCH" xsi:type="string">\Magento\Framework\App\Action\HttpPatchActionInterface</item> + <item name="DELETE" xsi:type="string">\Magento\Framework\App\Action\HttpDeleteActionInterface</item> + <item name="CONNECT" xsi:type="string">\Magento\Framework\App\Action\HttpConnectActionInterface</item> + <item name="PROPFIND" xsi:type="string">\Magento\Framework\App\Action\HttpPropfindActionInterface</item> + <item name="TRACE" xsi:type="string">\Magento\Framework\App\Action\HttpTraceActionInterface</item> + </argument> + </arguments> + </type> + <type name="Magento\Framework\App\ScopeResolverPool"> + <arguments> + <argument name="scopeResolvers" xsi:type="array"> + <item name="default" xsi:type="object">Magento\Framework\App\ScopeResolver</item> + </argument> + </arguments> + </type> + <type name="Magento\Framework\Cache\LockGuardedCacheLoader"> + <arguments> + <argument name="locker" xsi:type="object">Magento\Framework\Lock\Backend\Cache</argument> + <argument name="lockTimeout" xsi:type="number">10000</argument> + <argument name="delayTimeout" xsi:type="number">20</argument> + </arguments> + </type> + <preference for="Magento\Framework\MessageQueue\PoisonPill\PoisonPillCompareInterface" type="Magento\Framework\MessageQueue\PoisonPill\PoisonPillCompare"/> + <preference for="Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface" type="Magento\Framework\MessageQueue\PoisonPill\PoisonPillPut"/> + <preference for="Magento\Framework\MessageQueue\PoisonPill\PoisonPillReadInterface" type="Magento\Framework\MessageQueue\PoisonPill\PoisonPillRead"/> + <preference for="Magento\Framework\MessageQueue\CallbackInvokerInterface" type="Magento\Framework\MessageQueue\CallbackInvoker"/> </config> diff --git a/app/functions.php b/app/functions.php index 0b5fa4de4100..4b00d01819f7 100644 --- a/app/functions.php +++ b/app/functions.php @@ -6,17 +6,22 @@ /** * Create value-object \Magento\Framework\Phrase - * + * @deprecated The global function __() is now loaded via Magento Framework, the below require is only + * for backwards compatibility reasons and this file will be removed in a future version + * @see Magento\Framework\Phrase\__.php + * @SuppressWarnings(PHPMD.ShortMethodName) * @return \Magento\Framework\Phrase */ -function __() -{ - $argc = func_get_args(); +if (!function_exists('__')) { + function __() + { + $argc = func_get_args(); - $text = array_shift($argc); - if (!empty($argc) && is_array($argc[0])) { - $argc = $argc[0]; - } + $text = array_shift($argc); + if (!empty($argc) && is_array($argc[0])) { + $argc = $argc[0]; + } - return new \Magento\Framework\Phrase($text, $argc); + return new \Magento\Framework\Phrase($text, $argc); + } } diff --git a/auth.json.sample b/auth.json.sample index 81c8fd220eae..be1c70cfe1e1 100644 --- a/auth.json.sample +++ b/auth.json.sample @@ -1,8 +1,8 @@ { - "http-basic": { - "repo.magento.com": { - "username": "<public-key>", - "password": "<private-key>" - } - } + "http-basic": { + "repo.magento.com": { + "username": "<public-key>", + "password": "<private-key>" + } + } } diff --git a/composer.json b/composer.json index 64d4bcf3b4fe..cff2d676038d 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,7 @@ }, "require": { "php": "~7.1.3||~7.2.0", + "ext-bcmath": "*", "ext-ctype": "*", "ext-curl": "*", "ext-dom": "*", @@ -27,15 +28,14 @@ "ext-spl": "*", "ext-xsl": "*", "ext-zip": "*", - "ext-bcmath": "*", "lib-libxml": "*", - "braintree/braintree_php": "3.34.0", + "braintree/braintree_php": "3.35.0", "colinmollenhour/cache-backend-file": "~1.4.1", - "colinmollenhour/cache-backend-redis": "1.10.5", + "colinmollenhour/cache-backend-redis": "1.10.6", "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", @@ -44,15 +44,15 @@ "paragonie/sodium_compat": "^1.6", "pelago/emogrifier": "^2.0.0", "php-amqplib/php-amqplib": "~2.7.0", - "phpseclib/mcrypt_compat": "1.0.5", + "phpseclib/mcrypt_compat": "1.0.8", "phpseclib/phpseclib": "2.0.*", - "ramsey/uuid": "~3.7.3", + "ramsey/uuid": "~3.8.0", "symfony/console": "~4.1.0", "symfony/event-dispatcher": "~4.1.0", "symfony/process": "~4.1.0", "tedivm/jshrink": "~1.3.0", "tubalmartin/cssmin": "4.1.1", - "webonyx/graphql-php": "^0.11.1", + "webonyx/graphql-php": "^0.12.6", "zendframework/zend-captcha": "^2.7.1", "zendframework/zend-code": "~3.3.0", "zendframework/zend-config": "^2.6.0", @@ -82,14 +82,16 @@ "zendframework/zend-view": "~2.10.0" }, "require-dev": { - "magento/magento2-functional-testing-framework": "2.3.5", - "friendsofphp/php-cs-fixer": "~2.12.0", + "allure-framework/allure-phpunit": "~1.2.0", + "friendsofphp/php-cs-fixer": "~2.13.0", "lusitanian/oauth": "~0.8.10", + "magento/magento-coding-standard": "~1.0.0", + "magento/magento2-functional-testing-framework": "~2.3.14", "pdepend/pdepend": "2.5.2", "phpmd/phpmd": "@stable", "phpunit/phpunit": "~6.5.0", "sebastian/phpcpd": "~3.0.0", - "squizlabs/php_codesniffer": "3.3.0" + "squizlabs/php_codesniffer": "3.3.1" }, "suggest": { "ext-pcntl": "Need for run processes in parallel mode" @@ -103,6 +105,7 @@ "magento/module-asynchronous-operations": "*", "magento/module-authorization": "*", "magento/module-authorizenet": "*", + "magento/module-authorizenet-acceptjs": "*", "magento/module-advanced-search": "*", "magento/module-backend": "*", "magento/module-backup": "*", @@ -116,6 +119,7 @@ "magento/module-catalog-analytics": "*", "magento/module-catalog-import-export": "*", "magento/module-catalog-inventory": "*", + "magento/module-catalog-inventory-graph-ql": "*", "magento/module-catalog-rule": "*", "magento/module-catalog-rule-configurable": "*", "magento/module-catalog-search": "*", @@ -140,11 +144,13 @@ "magento/module-developer": "*", "magento/module-dhl": "*", "magento/module-directory": "*", + "magento/module-directory-graph-ql": "*", "magento/module-downloadable": "*", "magento/module-downloadable-graph-ql": "*", "magento/module-downloadable-import-export": "*", "magento/module-eav": "*", "magento/module-elasticsearch": "*", + "magento/module-elasticsearch-6": "*", "magento/module-email": "*", "magento/module-encryption-key": "*", "magento/module-fedex": "*", @@ -153,6 +159,7 @@ "magento/module-google-analytics": "*", "magento/module-google-optimizer": "*", "magento/module-graph-ql": "*", + "magento/module-graph-ql-cache": "*", "magento/module-catalog-graph-ql": "*", "magento/module-catalog-url-rewrite-graph-ql": "*", "magento/module-configurable-product-graph-ql": "*", @@ -163,8 +170,10 @@ "magento/module-url-rewrite-graph-ql": "*", "magento/module-cms-url-rewrite-graph-ql": "*", "magento/module-weee-graph-ql": "*", + "magento/module-cms-graph-ql": "*", "magento/module-grouped-import-export": "*", "magento/module-grouped-product": "*", + "magento/module-grouped-catalog-inventory": "*", "magento/module-grouped-product-graph-ql": "*", "magento/module-import-export": "*", "magento/module-indexer": "*", @@ -174,6 +183,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": "*", @@ -183,11 +194,13 @@ "magento/module-page-cache": "*", "magento/module-payment": "*", "magento/module-paypal": "*", + "magento/module-paypal-captcha": "*", "magento/module-persistent": "*", "magento/module-product-alert": "*", "magento/module-product-video": "*", "magento/module-quote": "*", "magento/module-quote-analytics": "*", + "magento/module-quote-graph-ql": "*", "magento/module-release-notification": "*", "magento/module-reports": "*", "magento/module-require-js": "*", @@ -198,6 +211,7 @@ "magento/module-rule": "*", "magento/module-sales": "*", "magento/module-sales-analytics": "*", + "magento/module-sales-graph-ql": "*", "magento/module-sales-inventory": "*", "magento/module-sales-rule": "*", "magento/module-sales-sequence": "*", @@ -205,6 +219,7 @@ "magento/module-search": "*", "magento/module-security": "*", "magento/module-send-friend": "*", + "magento/module-send-friend-graph-ql": "*", "magento/module-shipping": "*", "magento/module-signifyd": "*", "magento/module-sitemap": "*", @@ -218,6 +233,7 @@ "magento/module-tax": "*", "magento/module-tax-import-export": "*", "magento/module-theme": "*", + "magento/module-theme-graph-ql": "*", "magento/module-translation": "*", "magento/module-ui": "*", "magento/module-ups": "*", @@ -226,6 +242,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": "*", @@ -233,6 +250,7 @@ "magento/module-weee": "*", "magento/module-widget": "*", "magento/module-wishlist": "*", + "magento/module-wishlist-graph-ql": "*", "magento/module-wishlist-analytics": "*", "magento/theme-adminhtml-backend": "*", "magento/theme-frontend-blank": "*", diff --git a/composer.lock b/composer.lock index 7af89dd62065..5d9f7fbdf695 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "84697de5cc19e39e480943f1333aef0a", + "content-hash": "597fe6a47b695221292482fead498d83", "packages": [ { "name": "braintree/braintree_php", - "version": "3.34.0", + "version": "3.35.0", "source": { "type": "git", "url": "https://github.com/braintree/braintree_php.git", - "reference": "fd55c466d0d0088c67705d7ba0b3876d9767bfda" + "reference": "6c4388199ce379432804a5c18b88585157ef2ed7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/braintree/braintree_php/zipball/fd55c466d0d0088c67705d7ba0b3876d9767bfda", - "reference": "fd55c466d0d0088c67705d7ba0b3876d9767bfda", + "url": "https://api.github.com/repos/braintree/braintree_php/zipball/6c4388199ce379432804a5c18b88585157ef2ed7", + "reference": "6c4388199ce379432804a5c18b88585157ef2ed7", "shasum": "" }, "require": { @@ -51,7 +51,7 @@ } ], "description": "Braintree PHP Client Library", - "time": "2018-05-21T18:14:47+00:00" + "time": "2018-07-26T14:37:38+00:00" }, { "name": "colinmollenhour/cache-backend-file", @@ -88,16 +88,16 @@ }, { "name": "colinmollenhour/cache-backend-redis", - "version": "1.10.5", + "version": "1.10.6", "source": { "type": "git", "url": "https://github.com/colinmollenhour/Cm_Cache_Backend_Redis.git", - "reference": "91d949e28d939e607484a4bbf9307cff5afa689b" + "reference": "cc941a5f4cc017e11d3eab9061811ba9583ed6bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/colinmollenhour/Cm_Cache_Backend_Redis/zipball/91d949e28d939e607484a4bbf9307cff5afa689b", - "reference": "91d949e28d939e607484a4bbf9307cff5afa689b", + "url": "https://api.github.com/repos/colinmollenhour/Cm_Cache_Backend_Redis/zipball/cc941a5f4cc017e11d3eab9061811ba9583ed6bf", + "reference": "cc941a5f4cc017e11d3eab9061811ba9583ed6bf", "shasum": "" }, "require": { @@ -120,7 +120,7 @@ ], "description": "Zend_Cache backend using Redis with full support for tags.", "homepage": "https://github.com/colinmollenhour/Cm_Cache_Backend_Redis", - "time": "2018-05-15T16:02:25+00:00" + "time": "2018-09-24T16:02:07+00:00" }, { "name": "colinmollenhour/credis", @@ -164,21 +164,21 @@ }, { "name": "colinmollenhour/php-redis-session-abstract", - "version": "v1.4.0", + "version": "v1.4.1", "source": { "type": "git", "url": "https://github.com/colinmollenhour/php-redis-session-abstract.git", - "reference": "4cb15d557f58f45ad257cbcce3c12511e6deb5bc" + "reference": "4949ca28b86037abb44984c77bab9d0a4e075643" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/colinmollenhour/php-redis-session-abstract/zipball/4cb15d557f58f45ad257cbcce3c12511e6deb5bc", - "reference": "4cb15d557f58f45ad257cbcce3c12511e6deb5bc", + "url": "https://api.github.com/repos/colinmollenhour/php-redis-session-abstract/zipball/4949ca28b86037abb44984c77bab9d0a4e075643", + "reference": "4949ca28b86037abb44984c77bab9d0a4e075643", "shasum": "" }, "require": { "colinmollenhour/credis": "~1.6", - "php": "~5.5.0|~5.6.0|~7.0.0|~7.1.0|~7.2.0" + "php": "^5.5 || ^7.0" }, "type": "library", "autoload": { @@ -197,20 +197,20 @@ ], "description": "A Redis-based session handler with optimistic locking", "homepage": "https://github.com/colinmollenhour/php-redis-session-abstract", - "time": "2018-03-29T15:54:15+00:00" + "time": "2019-03-18T14:43:14+00:00" }, { "name": "composer/ca-bundle", - "version": "1.1.2", + "version": "1.1.4", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "46afded9720f40b9dc63542af4e3e43a1177acb0" + "reference": "558f321c52faeb4828c03e7dc0cfe39a09e09a2d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/46afded9720f40b9dc63542af4e3e43a1177acb0", - "reference": "46afded9720f40b9dc63542af4e3e43a1177acb0", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/558f321c52faeb4828c03e7dc0cfe39a09e09a2d", + "reference": "558f321c52faeb4828c03e7dc0cfe39a09e09a2d", "shasum": "" }, "require": { @@ -253,20 +253,20 @@ "ssl", "tls" ], - "time": "2018-08-08T08:57:40+00:00" + "time": "2019-01-28T09:30:10+00:00" }, { "name": "composer/composer", - "version": "1.7.2", + "version": "1.8.4", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "576aab9b5abb2ed11a1c52353a759363216a4ad2" + "reference": "bc364c2480c17941e2135cfc568fa41794392534" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/576aab9b5abb2ed11a1c52353a759363216a4ad2", - "reference": "576aab9b5abb2ed11a1c52353a759363216a4ad2", + "url": "https://api.github.com/repos/composer/composer/zipball/bc364c2480c17941e2135cfc568fa41794392534", + "reference": "bc364c2480c17941e2135cfc568fa41794392534", "shasum": "" }, "require": { @@ -302,7 +302,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.7-dev" + "dev-master": "1.8-dev" } }, "autoload": { @@ -333,20 +333,20 @@ "dependency", "package" ], - "time": "2018-08-16T14:57:12+00:00" + "time": "2019-02-11T09:52:10+00:00" }, { "name": "composer/semver", - "version": "1.4.2", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573" + "reference": "46d9139568ccb8d9e7cdd4539cab7347568a5e2e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/c7cb9a2095a074d131b65a8a0cd294479d785573", - "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573", + "url": "https://api.github.com/repos/composer/semver/zipball/46d9139568ccb8d9e7cdd4539cab7347568a5e2e", + "reference": "46d9139568ccb8d9e7cdd4539cab7347568a5e2e", "shasum": "" }, "require": { @@ -395,28 +395,27 @@ "validation", "versioning" ], - "time": "2016-08-30T16:08:34+00:00" + "time": "2019-03-19T17:25:45+00:00" }, { "name": "composer/spdx-licenses", - "version": "1.4.0", + "version": "1.5.1", "source": { "type": "git", "url": "https://github.com/composer/spdx-licenses.git", - "reference": "cb17687e9f936acd7e7245ad3890f953770dec1b" + "reference": "a1aa51cf3ab838b83b0867b14e56fc20fbd55b3d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/cb17687e9f936acd7e7245ad3890f953770dec1b", - "reference": "cb17687e9f936acd7e7245ad3890f953770dec1b", + "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/a1aa51cf3ab838b83b0867b14e56fc20fbd55b3d", + "reference": "a1aa51cf3ab838b83b0867b14e56fc20fbd55b3d", "shasum": "" }, "require": { - "php": "^5.3.2 || ^7.0" + "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5", - "phpunit/phpunit-mock-objects": "2.3.0 || ^3.0" + "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 7" }, "type": "library", "extra": { @@ -456,20 +455,20 @@ "spdx", "validator" ], - "time": "2018-04-30T10:33:04+00:00" + "time": "2019-03-26T10:23:26+00:00" }, { "name": "composer/xdebug-handler", - "version": "1.2.1", + "version": "1.3.2", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "e37cbd80da64afe314c72de8d2d2fec0e40d9373" + "reference": "d17708133b6c276d6e42ef887a877866b909d892" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/e37cbd80da64afe314c72de8d2d2fec0e40d9373", - "reference": "e37cbd80da64afe314c72de8d2d2fec0e40d9373", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/d17708133b6c276d6e42ef887a877866b909d892", + "reference": "d17708133b6c276d6e42ef887a877866b909d892", "shasum": "" }, "require": { @@ -500,7 +499,7 @@ "Xdebug", "performance" ], - "time": "2018-08-23T12:00:19+00:00" + "time": "2019-01-28T20:25:53+00:00" }, { "name": "container-interop/container-interop", @@ -535,29 +534,31 @@ }, { "name": "elasticsearch/elasticsearch", - "version": "v5.3.2", + "version": "v6.1.0", "source": { "type": "git", "url": "https://github.com/elastic/elasticsearch-php.git", - "reference": "4b29a4121e790bbfe690d5ee77da348b62d48eb8" + "reference": "b237a37b2cdf23a5a17fd3576cdea771394ad00d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/elastic/elasticsearch-php/zipball/4b29a4121e790bbfe690d5ee77da348b62d48eb8", - "reference": "4b29a4121e790bbfe690d5ee77da348b62d48eb8", + "url": "https://api.github.com/repos/elastic/elasticsearch-php/zipball/b237a37b2cdf23a5a17fd3576cdea771394ad00d", + "reference": "b237a37b2cdf23a5a17fd3576cdea771394ad00d", "shasum": "" }, "require": { + "ext-json": ">=1.3.7", "guzzlehttp/ringphp": "~1.0", - "php": "^5.6|^7.0", + "php": "^7.0", "psr/log": "~1.0" }, "require-dev": { "cpliakas/git-wrapper": "~1.0", "doctrine/inflector": "^1.1", "mockery/mockery": "0.9.4", - "phpunit/phpunit": "^4.7|^5.4", - "sami/sami": "~3.2", + "phpstan/phpstan-shim": "0.8.3", + "phpunit/phpunit": "6.3.0", + "squizlabs/php_codesniffer": "3.0.2", "symfony/finder": "^2.8", "symfony/yaml": "^2.8" }, @@ -586,7 +587,7 @@ "elasticsearch", "search" ], - "time": "2017-11-08T17:04:47+00:00" + "time": "2019-01-08T18:53:46+00:00" }, { "name": "guzzlehttp/ringphp", @@ -691,23 +692,23 @@ }, { "name": "justinrainbow/json-schema", - "version": "5.2.7", + "version": "5.2.8", "source": { "type": "git", "url": "https://github.com/justinrainbow/json-schema.git", - "reference": "8560d4314577199ba51bf2032f02cd1315587c23" + "reference": "dcb6e1006bb5fd1e392b4daa68932880f37550d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/8560d4314577199ba51bf2032f02cd1315587c23", - "reference": "8560d4314577199ba51bf2032f02cd1315587c23", + "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/dcb6e1006bb5fd1e392b4daa68932880f37550d4", + "reference": "dcb6e1006bb5fd1e392b4daa68932880f37550d4", "shasum": "" }, "require": { "php": ">=5.3.3" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^2.1", + "friendsofphp/php-cs-fixer": "~2.2.20", "json-schema/json-schema-test-suite": "1.2.0", "phpunit/phpunit": "^4.8.35" }, @@ -753,7 +754,7 @@ "json", "schema" ], - "time": "2018-02-14T22:26:30+00:00" + "time": "2019-01-14T23:55:14+00:00" }, { "name": "magento/composer", @@ -919,16 +920,16 @@ }, { "name": "monolog/monolog", - "version": "1.23.0", + "version": "1.24.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "fd8c787753b3a2ad11bc60c063cff1358a32a3b4" + "reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/fd8c787753b3a2ad11bc60c063cff1358a32a3b4", - "reference": "fd8c787753b3a2ad11bc60c063cff1358a32a3b4", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266", + "reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266", "shasum": "" }, "require": { @@ -993,7 +994,7 @@ "logging", "psr-3" ], - "time": "2017-06-19T01:22:40+00:00" + "time": "2018-11-05T09:00:11+00:00" }, { "name": "oyejorge/less.php", @@ -1059,33 +1060,29 @@ }, { "name": "paragonie/random_compat", - "version": "v2.0.17", + "version": "v9.99.99", "source": { "type": "git", "url": "https://github.com/paragonie/random_compat.git", - "reference": "29af24f25bab834fcbb38ad2a69fa93b867e070d" + "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/29af24f25bab834fcbb38ad2a69fa93b867e070d", - "reference": "29af24f25bab834fcbb38ad2a69fa93b867e070d", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", + "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", "shasum": "" }, "require": { - "php": ">=5.2.0" + "php": "^7" }, "require-dev": { - "phpunit/phpunit": "4.*|5.*" + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" }, "suggest": { "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." }, "type": "library", - "autoload": { - "files": [ - "lib/random.php" - ] - }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" @@ -1104,25 +1101,25 @@ "pseudorandom", "random" ], - "time": "2018-07-04T16:31:37+00:00" + "time": "2018-07-02T15:55:56+00:00" }, { "name": "paragonie/sodium_compat", - "version": "v1.6.3", + "version": "v1.9.1", "source": { "type": "git", "url": "https://github.com/paragonie/sodium_compat.git", - "reference": "7d0549c3947eaea620f4e523f42ab236cf7fd304" + "reference": "87125d5b265f98c4d1b8d83a1f0726607c229421" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/7d0549c3947eaea620f4e523f42ab236cf7fd304", - "reference": "7d0549c3947eaea620f4e523f42ab236cf7fd304", + "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/87125d5b265f98c4d1b8d83a1f0726607c229421", + "reference": "87125d5b265f98c4d1b8d83a1f0726607c229421", "shasum": "" }, "require": { "paragonie/random_compat": ">=1", - "php": "^5.2.4|^5.3|^5.4|^5.5|^5.6|^7" + "php": "^5.2.4|^5.3|^5.4|^5.5|^5.6|^7|^8" }, "require-dev": { "phpunit/phpunit": "^3|^4|^5" @@ -1186,28 +1183,33 @@ "secret-key cryptography", "side-channel resistant" ], - "time": "2018-06-06T17:30:29+00:00" + "time": "2019-03-20T17:19:05+00:00" }, { "name": "pelago/emogrifier", - "version": "v2.0.0", + "version": "v2.1.1", "source": { "type": "git", "url": "https://github.com/MyIntervals/emogrifier.git", - "reference": "8babf8ddbf348f26b29674e2f84db66ff7e3d95e" + "reference": "8ee7fb5ad772915451ed3415c1992bd3697d4983" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/MyIntervals/emogrifier/zipball/8babf8ddbf348f26b29674e2f84db66ff7e3d95e", - "reference": "8babf8ddbf348f26b29674e2f84db66ff7e3d95e", + "url": "https://api.github.com/repos/MyIntervals/emogrifier/zipball/8ee7fb5ad772915451ed3415c1992bd3697d4983", + "reference": "8ee7fb5ad772915451ed3415c1992bd3697d4983", "shasum": "" }, "require": { - "php": "^5.5.0 || ~7.0.0 || ~7.1.0 || ~7.2.0" + "ext-dom": "*", + "ext-libxml": "*", + "php": "^5.5.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0", + "symfony/css-selector": "^3.4.0 || ^4.0.0" }, "require-dev": { + "friendsofphp/php-cs-fixer": "^2.2.0", + "phpmd/phpmd": "^2.6.0", "phpunit/phpunit": "^4.8.0", - "squizlabs/php_codesniffer": "^3.1.0" + "squizlabs/php_codesniffer": "^3.3.2" }, "type": "library", "extra": { @@ -1217,7 +1219,7 @@ }, "autoload": { "psr-4": { - "Pelago\\": "Classes/" + "Pelago\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1235,10 +1237,6 @@ { "name": "Jaime Prado" }, - { - "name": "Roman Ožana", - "email": "ozana@omdesign.cz" - }, { "name": "Oliver Klee", "email": "github@oliverklee.de" @@ -1246,6 +1244,10 @@ { "name": "Zoli Szabó", "email": "zoli.szabo+github@gmail.com" + }, + { + "name": "Jake Hotson", + "email": "jake@qzdesign.co.uk" } ], "description": "Converts CSS styles into inline style attributes in your HTML code", @@ -1255,20 +1257,20 @@ "email", "pre-processing" ], - "time": "2018-01-05T23:30:21+00:00" + "time": "2018-12-10T10:36:30+00:00" }, { "name": "php-amqplib/php-amqplib", - "version": "v2.7.2", + "version": "v2.7.3", "source": { "type": "git", "url": "https://github.com/php-amqplib/php-amqplib.git", - "reference": "dfd3694a86f1a7394d3693485259d4074a6ec79b" + "reference": "a8ba54bd35b973fc6861e4c2e105f71e9e95f43f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-amqplib/php-amqplib/zipball/dfd3694a86f1a7394d3693485259d4074a6ec79b", - "reference": "dfd3694a86f1a7394d3693485259d4074a6ec79b", + "url": "https://api.github.com/repos/php-amqplib/php-amqplib/zipball/a8ba54bd35b973fc6861e4c2e105f71e9e95f43f", + "reference": "a8ba54bd35b973fc6861e4c2e105f71e9e95f43f", "shasum": "" }, "require": { @@ -1326,25 +1328,25 @@ "queue", "rabbitmq" ], - "time": "2018-02-11T19:28:00+00:00" + "time": "2018-04-30T03:54:54+00:00" }, { "name": "phpseclib/mcrypt_compat", - "version": "1.0.5", + "version": "1.0.8", "source": { "type": "git", "url": "https://github.com/phpseclib/mcrypt_compat.git", - "reference": "9646c46c7286284bd6c774b0c02f172fa302b879" + "reference": "f74c7b1897b62f08f268184b8bb98d9d9ab723b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/mcrypt_compat/zipball/9646c46c7286284bd6c774b0c02f172fa302b879", - "reference": "9646c46c7286284bd6c774b0c02f172fa302b879", + "url": "https://api.github.com/repos/phpseclib/mcrypt_compat/zipball/f74c7b1897b62f08f268184b8bb98d9d9ab723b0", + "reference": "f74c7b1897b62f08f268184b8bb98d9d9ab723b0", "shasum": "" }, "require": { "php": ">=5.3.3", - "phpseclib/phpseclib": ">=2.0.10 <3.0.0" + "phpseclib/phpseclib": ">=2.0.11 <3.0.0" }, "require-dev": { "phpunit/phpunit": "^4.8.35|^5.7|^6.0" @@ -1375,20 +1377,20 @@ "encryption", "mcrypt" ], - "time": "2018-04-15T18:49:21+00:00" + "time": "2018-08-22T03:11:43+00:00" }, { "name": "phpseclib/phpseclib", - "version": "2.0.11", + "version": "2.0.15", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "7053f06f91b3de78e143d430e55a8f7889efc08b" + "reference": "11cf67cf78dc4acb18dc9149a57be4aee5036ce0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/7053f06f91b3de78e143d430e55a8f7889efc08b", - "reference": "7053f06f91b3de78e143d430e55a8f7889efc08b", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/11cf67cf78dc4acb18dc9149a57be4aee5036ce0", + "reference": "11cf67cf78dc4acb18dc9149a57be4aee5036ce0", "shasum": "" }, "require": { @@ -1467,7 +1469,7 @@ "x.509", "x509" ], - "time": "2018-04-15T16:55:05+00:00" + "time": "2019-03-10T16:53:45+00:00" }, { "name": "psr/container", @@ -1570,16 +1572,16 @@ }, { "name": "psr/log", - "version": "1.0.2", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" + "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", - "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", + "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", "shasum": "" }, "require": { @@ -1613,25 +1615,26 @@ "psr", "psr-3" ], - "time": "2016-10-10T12:19:37+00:00" + "time": "2018-11-20T15:27:04+00:00" }, { "name": "ramsey/uuid", - "version": "3.7.3", + "version": "3.8.0", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "44abcdad877d9a46685a3a4d221e3b2c4b87cb76" + "reference": "d09ea80159c1929d75b3f9c60504d613aeb4a1e3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/44abcdad877d9a46685a3a4d221e3b2c4b87cb76", - "reference": "44abcdad877d9a46685a3a4d221e3b2c4b87cb76", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/d09ea80159c1929d75b3f9c60504d613aeb4a1e3", + "reference": "d09ea80159c1929d75b3f9c60504d613aeb4a1e3", "shasum": "" }, "require": { - "paragonie/random_compat": "^1.0|^2.0", - "php": "^5.4 || ^7.0" + "paragonie/random_compat": "^1.0|^2.0|9.99.99", + "php": "^5.4 || ^7.0", + "symfony/polyfill-ctype": "^1.8" }, "replace": { "rhumsaa/uuid": "self.version" @@ -1639,16 +1642,17 @@ "require-dev": { "codeception/aspect-mock": "^1.0 | ~2.0.0", "doctrine/annotations": "~1.2.0", - "goaop/framework": "1.0.0-alpha.2 | ^1.0 | ^2.1", + "goaop/framework": "1.0.0-alpha.2 | ^1.0 | ~2.1.0", "ircmaxell/random-lib": "^1.1", "jakub-onderka/php-parallel-lint": "^0.9.0", "mockery/mockery": "^0.9.9", "moontoast/math": "^1.1", "php-mock/php-mock-phpunit": "^0.3|^1.1", - "phpunit/phpunit": "^4.7|^5.0", + "phpunit/phpunit": "^4.7|^5.0|^6.5", "squizlabs/php_codesniffer": "^2.3" }, "suggest": { + "ext-ctype": "Provides support for PHP Ctype functions", "ext-libsodium": "Provides the PECL libsodium extension for use with the SodiumRandomGenerator", "ext-uuid": "Provides the PECL UUID extension for use with the PeclUuidTimeGenerator and PeclUuidRandomGenerator", "ircmaxell/random-lib": "Provides RandomLib for use with the RandomLibAdapter", @@ -1693,20 +1697,20 @@ "identifier", "uuid" ], - "time": "2018-01-20T00:28:24+00:00" + "time": "2018-07-19T23:38:55+00:00" }, { "name": "react/promise", - "version": "v2.7.0", + "version": "v2.7.1", "source": { "type": "git", "url": "https://github.com/reactphp/promise.git", - "reference": "f4edc2581617431aea50430749db55cc3fc031b3" + "reference": "31ffa96f8d2ed0341a57848cbb84d88b89dd664d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/f4edc2581617431aea50430749db55cc3fc031b3", - "reference": "f4edc2581617431aea50430749db55cc3fc031b3", + "url": "https://api.github.com/repos/reactphp/promise/zipball/31ffa96f8d2ed0341a57848cbb84d88b89dd664d", + "reference": "31ffa96f8d2ed0341a57848cbb84d88b89dd664d", "shasum": "" }, "require": { @@ -1739,7 +1743,7 @@ "promise", "promises" ], - "time": "2018-06-13T15:59:06+00:00" + "time": "2019-01-07T21:25:54+00:00" }, { "name": "seld/jsonlint", @@ -1836,16 +1840,16 @@ }, { "name": "symfony/console", - "version": "v4.1.4", + "version": "v4.1.11", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "ca80b8ced97cf07390078b29773dc384c39eee1f" + "reference": "9e87c798f67dc9fceeb4f3d57847b52d945d1a02" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/ca80b8ced97cf07390078b29773dc384c39eee1f", - "reference": "ca80b8ced97cf07390078b29773dc384c39eee1f", + "url": "https://api.github.com/repos/symfony/console/zipball/9e87c798f67dc9fceeb4f3d57847b52d945d1a02", + "reference": "9e87c798f67dc9fceeb4f3d57847b52d945d1a02", "shasum": "" }, "require": { @@ -1856,6 +1860,9 @@ "symfony/dependency-injection": "<3.4", "symfony/process": "<3.3" }, + "provide": { + "psr/log-implementation": "1.0" + }, "require-dev": { "psr/log": "~1.0", "symfony/config": "~3.4|~4.0", @@ -1865,7 +1872,7 @@ "symfony/process": "~3.4|~4.0" }, "suggest": { - "psr/log-implementation": "For using the console logger", + "psr/log": "For using the console logger", "symfony/event-dispatcher": "", "symfony/lock": "", "symfony/process": "" @@ -1900,20 +1907,73 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2018-07-26T11:24:31+00:00" + "time": "2019-01-25T14:34:37+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v4.2.4", + "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/event-dispatcher", - "version": "v4.1.4", + "version": "v4.1.11", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "bfb30c2ad377615a463ebbc875eba64a99f6aa3e" + "reference": "51be1b61dfe04d64a260223f2b81475fa8066b97" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/bfb30c2ad377615a463ebbc875eba64a99f6aa3e", - "reference": "bfb30c2ad377615a463ebbc875eba64a99f6aa3e", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/51be1b61dfe04d64a260223f2b81475fa8066b97", + "reference": "51be1b61dfe04d64a260223f2b81475fa8066b97", "shasum": "" }, "require": { @@ -1963,20 +2023,20 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2018-07-26T09:10:45+00:00" + "time": "2019-01-16T18:35:49+00:00" }, { "name": "symfony/filesystem", - "version": "v4.1.4", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "c0f5f62db218fa72195b8b8700e4b9b9cf52eb5e" + "reference": "e16b9e471703b2c60b95f14d31c1239f68f11601" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/c0f5f62db218fa72195b8b8700e4b9b9cf52eb5e", - "reference": "c0f5f62db218fa72195b8b8700e4b9b9cf52eb5e", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/e16b9e471703b2c60b95f14d31c1239f68f11601", + "reference": "e16b9e471703b2c60b95f14d31c1239f68f11601", "shasum": "" }, "require": { @@ -1986,7 +2046,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -2013,20 +2073,20 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2018-08-18T16:52:46+00:00" + "time": "2019-02-07T11:40:08+00:00" }, { "name": "symfony/finder", - "version": "v4.1.4", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "e162f1df3102d0b7472805a5a9d5db9fcf0a8068" + "reference": "267b7002c1b70ea80db0833c3afe05f0fbde580a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/e162f1df3102d0b7472805a5a9d5db9fcf0a8068", - "reference": "e162f1df3102d0b7472805a5a9d5db9fcf0a8068", + "url": "https://api.github.com/repos/symfony/finder/zipball/267b7002c1b70ea80db0833c3afe05f0fbde580a", + "reference": "267b7002c1b70ea80db0833c3afe05f0fbde580a", "shasum": "" }, "require": { @@ -2035,7 +2095,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -2062,20 +2122,20 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2018-07-26T11:24:31+00:00" + "time": "2019-02-23T15:42:05+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.9.0", + "version": "v1.11.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "e3d826245268269cd66f8326bd8bc066687b4a19" + "reference": "82ebae02209c21113908c229e9883c419720738a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19", - "reference": "e3d826245268269cd66f8326bd8bc066687b4a19", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/82ebae02209c21113908c229e9883c419720738a", + "reference": "82ebae02209c21113908c229e9883c419720738a", "shasum": "" }, "require": { @@ -2087,7 +2147,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.9-dev" + "dev-master": "1.11-dev" } }, "autoload": { @@ -2109,7 +2169,7 @@ }, { "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" + "email": "backendtea@gmail.com" } ], "description": "Symfony polyfill for ctype functions", @@ -2120,20 +2180,20 @@ "polyfill", "portable" ], - "time": "2018-08-06T14:22:27+00:00" + "time": "2019-02-06T07:57:58+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.9.0", + "version": "v1.11.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "d0cd638f4634c16d8df4508e847f14e9e43168b8" + "reference": "fe5e94c604826c35a32fa832f35bd036b6799609" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d0cd638f4634c16d8df4508e847f14e9e43168b8", - "reference": "d0cd638f4634c16d8df4508e847f14e9e43168b8", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fe5e94c604826c35a32fa832f35bd036b6799609", + "reference": "fe5e94c604826c35a32fa832f35bd036b6799609", "shasum": "" }, "require": { @@ -2145,7 +2205,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.9-dev" + "dev-master": "1.11-dev" } }, "autoload": { @@ -2179,20 +2239,20 @@ "portable", "shim" ], - "time": "2018-08-06T14:22:27+00:00" + "time": "2019-02-06T07:57:58+00:00" }, { "name": "symfony/process", - "version": "v4.1.4", + "version": "v4.1.11", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "86cdb930a6a855b0ab35fb60c1504cb36184f843" + "reference": "72d838aafaa7c790330fe362b9cecec362c64629" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/86cdb930a6a855b0ab35fb60c1504cb36184f843", - "reference": "86cdb930a6a855b0ab35fb60c1504cb36184f843", + "url": "https://api.github.com/repos/symfony/process/zipball/72d838aafaa7c790330fe362b9cecec362c64629", + "reference": "72d838aafaa7c790330fe362b9cecec362c64629", "shasum": "" }, "require": { @@ -2228,20 +2288,20 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2018-08-03T11:13:38+00:00" + "time": "2019-01-16T19:07:26+00:00" }, { "name": "tedivm/jshrink", - "version": "v1.3.0", + "version": "v1.3.1", "source": { "type": "git", "url": "https://github.com/tedious/JShrink.git", - "reference": "68ce379b213741e86f02bf6053b0d26b9f833448" + "reference": "21254058dc3ce6aba6bef458cff4bfa25cf8b198" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tedious/JShrink/zipball/68ce379b213741e86f02bf6053b0d26b9f833448", - "reference": "68ce379b213741e86f02bf6053b0d26b9f833448", + "url": "https://api.github.com/repos/tedious/JShrink/zipball/21254058dc3ce6aba6bef458cff4bfa25cf8b198", + "reference": "21254058dc3ce6aba6bef458cff4bfa25cf8b198", "shasum": "" }, "require": { @@ -2274,7 +2334,7 @@ "javascript", "minifier" ], - "time": "2017-12-08T00:59:56+00:00" + "time": "2018-09-16T00:02:51+00:00" }, { "name": "true/punycode", @@ -2377,25 +2437,26 @@ }, { "name": "webonyx/graphql-php", - "version": "v0.11.6", + "version": "v0.12.6", "source": { "type": "git", "url": "https://github.com/webonyx/graphql-php.git", - "reference": "f438a726cd523bc584e78d866eca270165c42fd5" + "reference": "4c545e5ec4fc37f6eb36c19f5a0e7feaf5979c95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/f438a726cd523bc584e78d866eca270165c42fd5", - "reference": "f438a726cd523bc584e78d866eca270165c42fd5", + "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/4c545e5ec4fc37f6eb36c19f5a0e7feaf5979c95", + "reference": "4c545e5ec4fc37f6eb36c19f5a0e7feaf5979c95", "shasum": "" }, "require": { "ext-mbstring": "*", - "php": ">=5.5,<8.0-DEV" + "php": ">=5.6" }, "require-dev": { "phpunit/phpunit": "^4.8", - "psr/http-message": "^1.0" + "psr/http-message": "^1.0", + "react/promise": "2.*" }, "suggest": { "psr/http-message": "To use standard GraphQL server", @@ -2403,9 +2464,6 @@ }, "type": "library", "autoload": { - "files": [ - "src/deprecated.php" - ], "psr-4": { "GraphQL\\": "src/" } @@ -2420,7 +2478,7 @@ "api", "graphql" ], - "time": "2018-04-17T10:34:43+00:00" + "time": "2018-09-02T14:59:54+00:00" }, { "name": "zendframework/zend-captcha", @@ -2694,16 +2752,16 @@ }, { "name": "zendframework/zend-db", - "version": "2.9.3", + "version": "2.10.0", "source": { "type": "git", "url": "https://github.com/zendframework/zend-db.git", - "reference": "5b4f2c42f94c9f7f4b2f456a0ebe459fab12b3d9" + "reference": "77022f06f6ffd384fa86d22ab8d8bbdb925a1e8e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-db/zipball/5b4f2c42f94c9f7f4b2f456a0ebe459fab12b3d9", - "reference": "5b4f2c42f94c9f7f4b2f456a0ebe459fab12b3d9", + "url": "https://api.github.com/repos/zendframework/zend-db/zipball/77022f06f6ffd384fa86d22ab8d8bbdb925a1e8e", + "reference": "77022f06f6ffd384fa86d22ab8d8bbdb925a1e8e", "shasum": "" }, "require": { @@ -2714,7 +2772,7 @@ "phpunit/phpunit": "^5.7.25 || ^6.4.4", "zendframework/zend-coding-standard": "~1.0.0", "zendframework/zend-eventmanager": "^2.6.2 || ^3.0", - "zendframework/zend-hydrator": "^1.1 || ^2.1", + "zendframework/zend-hydrator": "^1.1 || ^2.1 || ^3.0", "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3" }, "suggest": { @@ -2748,7 +2806,7 @@ "db", "zf" ], - "time": "2018-04-09T13:21:36+00:00" + "time": "2019-02-25T11:37:45+00:00" }, { "name": "zendframework/zend-di", @@ -2799,16 +2857,16 @@ }, { "name": "zendframework/zend-diactoros", - "version": "1.8.5", + "version": "1.8.6", "source": { "type": "git", "url": "https://github.com/zendframework/zend-diactoros.git", - "reference": "3e4edb822c942f37ade0d09579cfbab11e2fee87" + "reference": "20da13beba0dde8fb648be3cc19765732790f46e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-diactoros/zipball/3e4edb822c942f37ade0d09579cfbab11e2fee87", - "reference": "3e4edb822c942f37ade0d09579cfbab11e2fee87", + "url": "https://api.github.com/repos/zendframework/zend-diactoros/zipball/20da13beba0dde8fb648be3cc19765732790f46e", + "reference": "20da13beba0dde8fb648be3cc19765732790f46e", "shasum": "" }, "require": { @@ -2821,6 +2879,7 @@ "require-dev": { "ext-dom": "*", "ext-libxml": "*", + "php-http/psr7-integration-tests": "dev-master", "phpunit/phpunit": "^5.7.16 || ^6.0.8 || ^7.2.7", "zendframework/zend-coding-standard": "~1.0" }, @@ -2858,7 +2917,7 @@ "psr", "psr-7" ], - "time": "2018-08-10T14:16:32+00:00" + "time": "2018-09-05T19:29:37+00:00" }, { "name": "zendframework/zend-escaper", @@ -3015,16 +3074,16 @@ }, { "name": "zendframework/zend-filter", - "version": "2.8.0", + "version": "2.9.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-filter.git", - "reference": "7b997dbe79459f1652deccc8786d7407fb66caa9" + "reference": "1c3e6d02f9cd5f6c929c9859498f5efbe216e86f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-filter/zipball/7b997dbe79459f1652deccc8786d7407fb66caa9", - "reference": "7b997dbe79459f1652deccc8786d7407fb66caa9", + "url": "https://api.github.com/repos/zendframework/zend-filter/zipball/1c3e6d02f9cd5f6c929c9859498f5efbe216e86f", + "reference": "1c3e6d02f9cd5f6c929c9859498f5efbe216e86f", "shasum": "" }, "require": { @@ -3037,12 +3096,14 @@ "require-dev": { "pear/archive_tar": "^1.4.3", "phpunit/phpunit": "^5.7.23 || ^6.4.3", + "psr/http-factory": "^1.0", "zendframework/zend-coding-standard": "~1.0.0", "zendframework/zend-crypt": "^3.2.1", "zendframework/zend-servicemanager": "^2.7.8 || ^3.3", "zendframework/zend-uri": "^2.6" }, "suggest": { + "psr/http-factory-implementation": "psr/http-factory-implementation, for creating file upload instances when consuming PSR-7 in file upload filters", "zendframework/zend-crypt": "Zend\\Crypt component, for encryption filters", "zendframework/zend-i18n": "Zend\\I18n component for filters depending on i18n functionality", "zendframework/zend-servicemanager": "Zend\\ServiceManager component, for using the filter chain functionality", @@ -3051,8 +3112,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.8.x-dev", - "dev-develop": "2.9.x-dev" + "dev-master": "2.9.x-dev", + "dev-develop": "2.10.x-dev" }, "zf": { "component": "Zend\\Filter", @@ -3074,25 +3135,25 @@ "filter", "zf" ], - "time": "2018-04-11T16:20:04+00:00" + "time": "2018-12-17T16:00:04+00:00" }, { "name": "zendframework/zend-form", - "version": "2.12.0", + "version": "2.13.0", "source": { "type": "git", "url": "https://github.com/zendframework/zend-form.git", - "reference": "565fb4f4bb3e0dbeea0173c923c4a8be77de9441" + "reference": "c713a12ccbd43148b71c9339e171ca11e3f8a1da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-form/zipball/565fb4f4bb3e0dbeea0173c923c4a8be77de9441", - "reference": "565fb4f4bb3e0dbeea0173c923c4a8be77de9441", + "url": "https://api.github.com/repos/zendframework/zend-form/zipball/c713a12ccbd43148b71c9339e171ca11e3f8a1da", + "reference": "c713a12ccbd43148b71c9339e171ca11e3f8a1da", "shasum": "" }, "require": { "php": "^5.6 || ^7.0", - "zendframework/zend-hydrator": "^1.1 || ^2.1", + "zendframework/zend-hydrator": "^1.1 || ^2.1 || ^3.0", "zendframework/zend-inputfilter": "^2.8", "zendframework/zend-stdlib": "^2.7 || ^3.0" }, @@ -3126,8 +3187,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.12.x-dev", - "dev-develop": "2.13.x-dev" + "dev-master": "2.13.x-dev", + "dev-develop": "2.14.x-dev" }, "zf": { "component": "Zend\\Form", @@ -3152,20 +3213,20 @@ "form", "zf" ], - "time": "2018-05-16T18:49:44+00:00" + "time": "2018-12-11T22:51:29+00:00" }, { "name": "zendframework/zend-http", - "version": "2.8.2", + "version": "2.8.4", "source": { "type": "git", "url": "https://github.com/zendframework/zend-http.git", - "reference": "2c8aed3d25522618573194e7cc51351f8cd4a45b" + "reference": "d160aedc096be230af0fe9c31151b2b33ad4e807" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-http/zipball/2c8aed3d25522618573194e7cc51351f8cd4a45b", - "reference": "2c8aed3d25522618573194e7cc51351f8cd4a45b", + "url": "https://api.github.com/repos/zendframework/zend-http/zipball/d160aedc096be230af0fe9c31151b2b33ad4e807", + "reference": "d160aedc096be230af0fe9c31151b2b33ad4e807", "shasum": "" }, "require": { @@ -3207,7 +3268,7 @@ "zend", "zf" ], - "time": "2018-08-13T18:47:03+00:00" + "time": "2019-02-07T17:47:08+00:00" }, { "name": "zendframework/zend-hydrator", @@ -3337,34 +3398,38 @@ }, { "name": "zendframework/zend-inputfilter", - "version": "2.8.2", + "version": "2.10.0", "source": { "type": "git", "url": "https://github.com/zendframework/zend-inputfilter.git", - "reference": "3f02179e014d9ef0faccda2ad6c65d38adc338d8" + "reference": "4f52b71ec9cef3a06e3bba8f5c2124e94055ec0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-inputfilter/zipball/3f02179e014d9ef0faccda2ad6c65d38adc338d8", - "reference": "3f02179e014d9ef0faccda2ad6c65d38adc338d8", + "url": "https://api.github.com/repos/zendframework/zend-inputfilter/zipball/4f52b71ec9cef3a06e3bba8f5c2124e94055ec0c", + "reference": "4f52b71ec9cef3a06e3bba8f5c2124e94055ec0c", "shasum": "" }, "require": { "php": "^5.6 || ^7.0", - "zendframework/zend-filter": "^2.6", + "zendframework/zend-filter": "^2.9.1", "zendframework/zend-servicemanager": "^2.7.10 || ^3.3.1", "zendframework/zend-stdlib": "^2.7 || ^3.0", - "zendframework/zend-validator": "^2.10.1" + "zendframework/zend-validator": "^2.11" }, "require-dev": { "phpunit/phpunit": "^5.7.23 || ^6.4.3", + "psr/http-message": "^1.0", "zendframework/zend-coding-standard": "~1.0.0" }, + "suggest": { + "psr/http-message-implementation": "PSR-7 is required if you wish to validate PSR-7 UploadedFileInterface payloads" + }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.8.x-dev", - "dev-develop": "2.9.x-dev" + "dev-master": "2.10.x-dev", + "dev-develop": "2.11.x-dev" }, "zf": { "component": "Zend\\InputFilter", @@ -3386,7 +3451,7 @@ "inputfilter", "zf" ], - "time": "2018-05-14T17:38:03+00:00" + "time": "2019-01-30T16:58:51+00:00" }, { "name": "zendframework/zend-json", @@ -3623,16 +3688,16 @@ }, { "name": "zendframework/zend-math", - "version": "2.7.0", + "version": "2.7.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-math.git", - "reference": "f4358090d5d23973121f1ed0b376184b66d9edec" + "reference": "1abce074004dacac1a32cd54de94ad47ef960d38" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-math/zipball/f4358090d5d23973121f1ed0b376184b66d9edec", - "reference": "f4358090d5d23973121f1ed0b376184b66d9edec", + "url": "https://api.github.com/repos/zendframework/zend-math/zipball/1abce074004dacac1a32cd54de94ad47ef960d38", + "reference": "1abce074004dacac1a32cd54de94ad47ef960d38", "shasum": "" }, "require": { @@ -3669,7 +3734,7 @@ "math", "zf2" ], - "time": "2016-04-07T16:29:53+00:00" + "time": "2018-12-04T15:34:17+00:00" }, { "name": "zendframework/zend-mime", @@ -4311,16 +4376,16 @@ }, { "name": "zendframework/zend-uri", - "version": "2.6.1", + "version": "2.7.0", "source": { "type": "git", "url": "https://github.com/zendframework/zend-uri.git", - "reference": "3b6463645c6766f78ce537c70cb4fdabee1e725f" + "reference": "b2785cd38fe379a784645449db86f21b7739b1ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-uri/zipball/3b6463645c6766f78ce537c70cb4fdabee1e725f", - "reference": "3b6463645c6766f78ce537c70cb4fdabee1e725f", + "url": "https://api.github.com/repos/zendframework/zend-uri/zipball/b2785cd38fe379a784645449db86f21b7739b1ee", + "reference": "b2785cd38fe379a784645449db86f21b7739b1ee", "shasum": "" }, "require": { @@ -4335,8 +4400,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.6.x-dev", - "dev-develop": "2.7.x-dev" + "dev-master": "2.7.x-dev", + "dev-develop": "2.8.x-dev" } }, "autoload": { @@ -4354,20 +4419,20 @@ "uri", "zf" ], - "time": "2018-04-30T13:40:08+00:00" + "time": "2019-02-27T21:39:04+00:00" }, { "name": "zendframework/zend-validator", - "version": "2.10.2", + "version": "2.11.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-validator.git", - "reference": "38109ed7d8e46cfa71bccbe7e6ca80cdd035f8c9" + "reference": "3c28dfe4e5951ba38059cea895244d9d206190b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-validator/zipball/38109ed7d8e46cfa71bccbe7e6ca80cdd035f8c9", - "reference": "38109ed7d8e46cfa71bccbe7e6ca80cdd035f8c9", + "url": "https://api.github.com/repos/zendframework/zend-validator/zipball/3c28dfe4e5951ba38059cea895244d9d206190b3", + "reference": "3c28dfe4e5951ba38059cea895244d9d206190b3", "shasum": "" }, "require": { @@ -4377,6 +4442,7 @@ }, "require-dev": { "phpunit/phpunit": "^6.0.8 || ^5.7.15", + "psr/http-message": "^1.0", "zendframework/zend-cache": "^2.6.1", "zendframework/zend-coding-standard": "~1.0.0", "zendframework/zend-config": "^2.6", @@ -4390,6 +4456,7 @@ "zendframework/zend-uri": "^2.5" }, "suggest": { + "psr/http-message": "psr/http-message, required when validating PSR-7 UploadedFileInterface instances via the Upload and UploadFile validators", "zendframework/zend-db": "Zend\\Db component, required by the (No)RecordExists validator", "zendframework/zend-filter": "Zend\\Filter component, required by the Digits validator", "zendframework/zend-i18n": "Zend\\I18n component to allow translation of validation error messages", @@ -4402,8 +4469,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.10.x-dev", - "dev-develop": "2.11.x-dev" + "dev-master": "2.11.x-dev", + "dev-develop": "2.12.x-dev" }, "zf": { "component": "Zend\\Validator", @@ -4425,25 +4492,26 @@ "validator", "zf2" ], - "time": "2018-02-01T17:05:33+00:00" + "time": "2019-01-29T22:26:39+00:00" }, { "name": "zendframework/zend-view", - "version": "2.10.0", + "version": "2.10.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-view.git", - "reference": "4478cc5dd960e2339d88b363ef99fa278700e80e" + "reference": "c1a3f2043fb75b5983ab9adfc369ae396601be7e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-view/zipball/4478cc5dd960e2339d88b363ef99fa278700e80e", - "reference": "4478cc5dd960e2339d88b363ef99fa278700e80e", + "url": "https://api.github.com/repos/zendframework/zend-view/zipball/c1a3f2043fb75b5983ab9adfc369ae396601be7e", + "reference": "c1a3f2043fb75b5983ab9adfc369ae396601be7e", "shasum": "" }, "require": { "php": "^5.6 || ^7.0", "zendframework/zend-eventmanager": "^2.6.2 || ^3.0", + "zendframework/zend-json": "^2.6.1", "zendframework/zend-loader": "^2.5", "zendframework/zend-stdlib": "^2.7 || ^3.0" }, @@ -4459,10 +4527,9 @@ "zendframework/zend-filter": "^2.6.1", "zendframework/zend-http": "^2.5.4", "zendframework/zend-i18n": "^2.6", - "zendframework/zend-json": "^2.6.1", "zendframework/zend-log": "^2.7", "zendframework/zend-modulemanager": "^2.7.1", - "zendframework/zend-mvc": "^2.7 || ^3.0", + "zendframework/zend-mvc": "^2.7.14 || ^3.0", "zendframework/zend-navigation": "^2.5", "zendframework/zend-paginator": "^2.5", "zendframework/zend-permissions-acl": "^2.6", @@ -4479,8 +4546,8 @@ "zendframework/zend-filter": "Zend\\Filter component", "zendframework/zend-http": "Zend\\Http component", "zendframework/zend-i18n": "Zend\\I18n component", - "zendframework/zend-json": "Zend\\Json component", "zendframework/zend-mvc": "Zend\\Mvc component", + "zendframework/zend-mvc-plugin-flashmessenger": "zend-mvc-plugin-flashmessenger component, if you want to use the FlashMessenger view helper with zend-mvc versions 3 and up", "zendframework/zend-navigation": "Zend\\Navigation component", "zendframework/zend-paginator": "Zend\\Paginator component", "zendframework/zend-permissions-acl": "Zend\\Permissions\\Acl component", @@ -4512,22 +4579,22 @@ "view", "zf2" ], - "time": "2018-01-17T22:21:50+00:00" + "time": "2018-12-06T21:20:01+00:00" } ], "packages-dev": [ { "name": "allure-framework/allure-codeception", - "version": "1.2.7", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/allure-framework/allure-codeception.git", - "reference": "48598f4b4603b50b663bfe977260113a40912131" + "reference": "9d31d781b3622b028f1f6210bc76ba88438bd518" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/allure-framework/allure-codeception/zipball/48598f4b4603b50b663bfe977260113a40912131", - "reference": "48598f4b4603b50b663bfe977260113a40912131", + "url": "https://api.github.com/repos/allure-framework/allure-codeception/zipball/9d31d781b3622b028f1f6210bc76ba88438bd518", + "reference": "9d31d781b3622b028f1f6210bc76ba88438bd518", "shasum": "" }, "require": { @@ -4565,7 +4632,7 @@ "steps", "testing" ], - "time": "2018-03-07T11:18:27+00:00" + "time": "2018-12-18T19:47:23+00:00" }, { "name": "allure-framework/allure-php-api", @@ -4619,18 +4686,68 @@ ], "time": "2016-12-07T12:15:46+00:00" }, + { + "name": "allure-framework/allure-phpunit", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/allure-framework/allure-phpunit.git", + "reference": "45504aeba41304cf155a898fa9ac1aae79f4a089" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/allure-framework/allure-phpunit/zipball/45504aeba41304cf155a898fa9ac1aae79f4a089", + "reference": "45504aeba41304cf155a898fa9ac1aae79f4a089", + "shasum": "" + }, + "require": { + "allure-framework/allure-php-api": "~1.1.0", + "mikey179/vfsstream": "1.*", + "php": ">=7.0.0", + "phpunit/phpunit": ">=6.0.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "Yandex": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Ivan Krutov", + "email": "vania-pooh@yandex-team.ru", + "role": "Developer" + } + ], + "description": "A PHPUnit adapter for Allure report.", + "homepage": "http://allure.qatools.ru/", + "keywords": [ + "allure", + "attachments", + "cases", + "phpunit", + "report", + "steps", + "testing" + ], + "time": "2017-11-03T13:08:21+00:00" + }, { "name": "behat/gherkin", - "version": "v4.4.5", + "version": "v4.6.0", "source": { "type": "git", "url": "https://github.com/Behat/Gherkin.git", - "reference": "5c14cff4f955b17d20d088dec1bde61c0539ec74" + "reference": "ab0a02ea14893860bca00f225f5621d351a3ad07" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Behat/Gherkin/zipball/5c14cff4f955b17d20d088dec1bde61c0539ec74", - "reference": "5c14cff4f955b17d20d088dec1bde61c0539ec74", + "url": "https://api.github.com/repos/Behat/Gherkin/zipball/ab0a02ea14893860bca00f225f5621d351a3ad07", + "reference": "ab0a02ea14893860bca00f225f5621d351a3ad07", "shasum": "" }, "require": { @@ -4638,8 +4755,8 @@ }, "require-dev": { "phpunit/phpunit": "~4.5|~5", - "symfony/phpunit-bridge": "~2.7|~3", - "symfony/yaml": "~2.3|~3" + "symfony/phpunit-bridge": "~2.7|~3|~4", + "symfony/yaml": "~2.3|~3|~4" }, "suggest": { "symfony/yaml": "If you want to parse features, represented in YAML files" @@ -4676,35 +4793,32 @@ "gherkin", "parser" ], - "time": "2016-10-30T11:50:56+00:00" + "time": "2019-01-16T14:22:17+00:00" }, { "name": "codeception/codeception", - "version": "2.3.9", + "version": "2.4.5", "source": { "type": "git", "url": "https://github.com/Codeception/Codeception.git", - "reference": "104f46fa0bde339f1bcc3a375aac21eb36e65a1e" + "reference": "5fee32d5c82791548931cbc34806b4de6aa1abfc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Codeception/zipball/104f46fa0bde339f1bcc3a375aac21eb36e65a1e", - "reference": "104f46fa0bde339f1bcc3a375aac21eb36e65a1e", + "url": "https://api.github.com/repos/Codeception/Codeception/zipball/5fee32d5c82791548931cbc34806b4de6aa1abfc", + "reference": "5fee32d5c82791548931cbc34806b4de6aa1abfc", "shasum": "" }, "require": { - "behat/gherkin": "~4.4.0", - "codeception/stub": "^1.0", + "behat/gherkin": "^4.4.0", + "codeception/phpunit-wrapper": "^6.0.9|^7.0.6", + "codeception/stub": "^2.0", "ext-json": "*", "ext-mbstring": "*", "facebook/webdriver": ">=1.1.3 <2.0", "guzzlehttp/guzzle": ">=4.1.4 <7.0", "guzzlehttp/psr7": "~1.0", - "php": ">=5.4.0 <8.0", - "phpunit/php-code-coverage": ">=2.2.4 <6.0", - "phpunit/phpunit": ">=4.8.28 <5.0.0 || >=5.6.3 <7.0", - "sebastian/comparator": ">1.1 <3.0", - "sebastian/diff": ">=1.4 <3.0", + "php": ">=5.6.0 <8.0", "symfony/browser-kit": ">=2.7 <5.0", "symfony/console": ">=2.7 <5.0", "symfony/css-selector": ">=2.7 <5.0", @@ -4770,27 +4884,70 @@ "functional testing", "unit testing" ], - "time": "2018-02-26T23:29:41+00:00" + "time": "2018-08-01T07:21:49+00:00" }, { - "name": "codeception/stub", - "version": "1.0.4", + "name": "codeception/phpunit-wrapper", + "version": "6.6.1", "source": { "type": "git", - "url": "https://github.com/Codeception/Stub.git", - "reference": "681b62348837a5ef07d10d8a226f5bc358cc8805" + "url": "https://github.com/Codeception/phpunit-wrapper.git", + "reference": "d0da25a98bcebeb15d97c2ad3b2de6166b6e7a0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Stub/zipball/681b62348837a5ef07d10d8a226f5bc358cc8805", - "reference": "681b62348837a5ef07d10d8a226f5bc358cc8805", + "url": "https://api.github.com/repos/Codeception/phpunit-wrapper/zipball/d0da25a98bcebeb15d97c2ad3b2de6166b6e7a0c", + "reference": "d0da25a98bcebeb15d97c2ad3b2de6166b6e7a0c", "shasum": "" }, "require": { - "phpunit/phpunit-mock-objects": ">2.3 <7.0" + "phpunit/php-code-coverage": ">=4.0.4 <6.0", + "phpunit/phpunit": ">=6.5.13 <7.0", + "sebastian/comparator": ">=1.2.4 <3.0", + "sebastian/diff": ">=1.4 <4.0" + }, + "replace": { + "codeception/phpunit-wrapper": "*" }, "require-dev": { - "phpunit/phpunit": ">=4.8 <8.0" + "codeception/specify": "*", + "vlucas/phpdotenv": "^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Codeception\\PHPUnit\\": "src\\" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Davert", + "email": "davert.php@resend.cc" + } + ], + "description": "PHPUnit classes used by Codeception", + "time": "2019-02-26T20:47:39+00:00" + }, + { + "name": "codeception/stub", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/Codeception/Stub.git", + "reference": "853657f988942f7afb69becf3fd0059f192c705a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Codeception/Stub/zipball/853657f988942f7afb69becf3fd0059f192c705a", + "reference": "853657f988942f7afb69becf3fd0059f192c705a", + "shasum": "" + }, + "require": { + "codeception/phpunit-wrapper": ">6.0.15 <6.1.0 | ^6.6.1 | ^7.7.1 | ^8.0.3" }, "type": "library", "autoload": { @@ -4803,38 +4960,82 @@ "MIT" ], "description": "Flexible Stub wrapper for PHPUnit's Mock Builder", - "time": "2018-05-17T09:31:08+00:00" + "time": "2019-03-02T15:35:10+00:00" }, { "name": "consolidation/annotated-command", - "version": "2.8.5", + "version": "2.12.0", "source": { "type": "git", "url": "https://github.com/consolidation/annotated-command.git", - "reference": "1e8ff512072422b850b44aa721b5b303e4a5ebb3" + "reference": "512a2e54c98f3af377589de76c43b24652bcb789" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/annotated-command/zipball/1e8ff512072422b850b44aa721b5b303e4a5ebb3", - "reference": "1e8ff512072422b850b44aa721b5b303e4a5ebb3", + "url": "https://api.github.com/repos/consolidation/annotated-command/zipball/512a2e54c98f3af377589de76c43b24652bcb789", + "reference": "512a2e54c98f3af377589de76c43b24652bcb789", "shasum": "" }, "require": { - "consolidation/output-formatters": "^3.1.12", - "php": ">=5.4.0", + "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": "^2", + "g1a/composer-test-scenarios": "^3", + "php-coveralls/php-coveralls": "^1", "phpunit/phpunit": "^6", - "satooshi/php-coveralls": "^2", "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" } @@ -4855,20 +5056,20 @@ } ], "description": "Initialize Symfony Console commands from annotated command class methods.", - "time": "2018-08-18T23:51:49+00:00" + "time": "2019-03-08T16:55:03+00:00" }, { "name": "consolidation/config", - "version": "1.1.0", + "version": "1.2.1", "source": { "type": "git", "url": "https://github.com/consolidation/config.git", - "reference": "c9fc25e9088a708637e18a256321addc0670e578" + "reference": "cac1279bae7efb5c7fb2ca4c3ba4b8eb741a96c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/config/zipball/c9fc25e9088a708637e18a256321addc0670e578", - "reference": "c9fc25e9088a708637e18a256321addc0670e578", + "url": "https://api.github.com/repos/consolidation/config/zipball/cac1279bae7efb5c7fb2ca4c3ba4b8eb741a96c1", + "reference": "cac1279bae7efb5c7fb2ca4c3ba4b8eb741a96c1", "shasum": "" }, "require": { @@ -4877,9 +5078,9 @@ "php": ">=5.4.0" }, "require-dev": { - "g1a/composer-test-scenarios": "^1", + "g1a/composer-test-scenarios": "^3", + "php-coveralls/php-coveralls": "^1", "phpunit/phpunit": "^5", - "satooshi/php-coveralls": "^1.0", "squizlabs/php_codesniffer": "2.*", "symfony/console": "^2.5|^3|^4", "symfony/yaml": "^2.8.11|^3|^4" @@ -4889,6 +5090,33 @@ }, "type": "library", "extra": { + "scenarios": { + "symfony4": { + "require-dev": { + "symfony/console": "^4.0" + }, + "config": { + "platform": { + "php": "7.1.3" + } + } + }, + "symfony2": { + "require-dev": { + "symfony/console": "^2.8", + "symfony/event-dispatcher": "^2.8", + "phpunit/phpunit": "^4.8.36" + }, + "remove": [ + "php-coveralls/php-coveralls" + ], + "config": { + "platform": { + "php": "5.4.8" + } + } + } + }, "branch-alias": { "dev-master": "1.x-dev" } @@ -4909,35 +5137,76 @@ } ], "description": "Provide configuration services for a commandline tool.", - "time": "2018-08-07T22:57:00+00:00" + "time": "2019-03-03T19:37:04+00:00" }, { "name": "consolidation/log", - "version": "1.0.6", + "version": "1.1.1", "source": { "type": "git", "url": "https://github.com/consolidation/log.git", - "reference": "dfd8189a771fe047bf3cd669111b2de5f1c79395" + "reference": "b2e887325ee90abc96b0a8b7b474cd9e7c896e3a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/log/zipball/dfd8189a771fe047bf3cd669111b2de5f1c79395", - "reference": "dfd8189a771fe047bf3cd669111b2de5f1c79395", + "url": "https://api.github.com/repos/consolidation/log/zipball/b2e887325ee90abc96b0a8b7b474cd9e7c896e3a", + "reference": "b2e887325ee90abc96b0a8b7b474cd9e7c896e3a", "shasum": "" }, "require": { - "php": ">=5.5.0", - "psr/log": "~1.0", + "php": ">=5.4.5", + "psr/log": "^1.0", "symfony/console": "^2.8|^3|^4" }, "require-dev": { - "g1a/composer-test-scenarios": "^1", - "phpunit/phpunit": "4.*", - "satooshi/php-coveralls": "^2", - "squizlabs/php_codesniffer": "2.*" + "g1a/composer-test-scenarios": "^3", + "php-coveralls/php-coveralls": "^1", + "phpunit/phpunit": "^6", + "squizlabs/php_codesniffer": "^2" }, "type": "library", "extra": { + "scenarios": { + "symfony4": { + "require": { + "symfony/console": "^4.0" + }, + "config": { + "platform": { + "php": "7.1.3" + } + } + }, + "symfony2": { + "require": { + "symfony/console": "^2.8" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.36" + }, + "remove": [ + "php-coveralls/php-coveralls" + ], + "config": { + "platform": { + "php": "5.4.8" + } + } + }, + "phpunit4": { + "require-dev": { + "phpunit/phpunit": "^4.8.36" + }, + "remove": [ + "php-coveralls/php-coveralls" + ], + "config": { + "platform": { + "php": "5.4.8" + } + } + } + }, "branch-alias": { "dev-master": "1.x-dev" } @@ -4958,33 +5227,33 @@ } ], "description": "Improved Psr-3 / Psr\\Log logger based on Symfony Console components.", - "time": "2018-05-25T18:14:39+00:00" + "time": "2019-01-01T17:30:51+00:00" }, { "name": "consolidation/output-formatters", - "version": "3.2.1", + "version": "3.4.1", "source": { "type": "git", "url": "https://github.com/consolidation/output-formatters.git", - "reference": "d78ef59aea19d3e2e5a23f90a055155ee78a0ad5" + "reference": "0881112642ad9059071f13f397f571035b527cb9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/output-formatters/zipball/d78ef59aea19d3e2e5a23f90a055155ee78a0ad5", - "reference": "d78ef59aea19d3e2e5a23f90a055155ee78a0ad5", + "url": "https://api.github.com/repos/consolidation/output-formatters/zipball/0881112642ad9059071f13f397f571035b527cb9", + "reference": "0881112642ad9059071f13f397f571035b527cb9", "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", + "g1a/composer-test-scenarios": "^3", + "php-coveralls/php-coveralls": "^1", "phpunit/phpunit": "^5.7.27", - "satooshi/php-coveralls": "^2", "squizlabs/php_codesniffer": "^2.7", - "symfony/console": "3.2.3", "symfony/var-dumper": "^2.8|^3|^4", "victorjonsson/markdowndocs": "^1.3" }, @@ -4993,6 +5262,52 @@ }, "type": "library", "extra": { + "scenarios": { + "symfony4": { + "require": { + "symfony/console": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^6" + }, + "config": { + "platform": { + "php": "7.1.3" + } + } + }, + "symfony3": { + "require": { + "symfony/console": "^3.4", + "symfony/finder": "^3.4", + "symfony/var-dumper": "^3.4" + }, + "config": { + "platform": { + "php": "5.6.32" + } + } + }, + "symfony2": { + "require": { + "symfony/console": "^2.8" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.36" + }, + "remove": [ + "php-coveralls/php-coveralls" + ], + "config": { + "platform": { + "php": "5.4.8" + } + }, + "scenario-options": { + "create-lockfile": "false" + } + } + }, "branch-alias": { "dev-master": "3.x-dev" } @@ -5013,29 +5328,28 @@ } ], "description": "Format text by applying transformations provided by plug-in formatters.", - "time": "2018-05-25T18:02:34+00:00" + "time": "2019-03-14T03:45:44+00:00" }, { "name": "consolidation/robo", - "version": "1.3.1", + "version": "1.4.9", "source": { "type": "git", "url": "https://github.com/consolidation/Robo.git", - "reference": "31f2d2562c4e1dcde70f2659eefd59aa9c7f5b2d" + "reference": "5c6b3840a45afda1cbffbb3bb1f94dd5f9f83345" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/Robo/zipball/31f2d2562c4e1dcde70f2659eefd59aa9c7f5b2d", - "reference": "31f2d2562c4e1dcde70f2659eefd59aa9c7f5b2d", + "url": "https://api.github.com/repos/consolidation/Robo/zipball/5c6b3840a45afda1cbffbb3bb1f94dd5f9f83345", + "reference": "5c6b3840a45afda1cbffbb3bb1f94dd5f9f83345", "shasum": "" }, "require": { - "consolidation/annotated-command": "^2.8.2", - "consolidation/config": "^1.0.10", + "consolidation/annotated-command": "^2.10.2", + "consolidation/config": "^1.2", "consolidation/log": "~1", "consolidation/output-formatters": "^3.1.13", "consolidation/self-update": "^1", - "g1a/composer-test-scenarios": "^2", "grasmash/yaml-expander": "^1.3", "league/container": "^2.2", "php": ">=5.5.0", @@ -5052,14 +5366,15 @@ "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.2", + "pear/archive_tar": "^1.4.4", + "php-coveralls/php-coveralls": "^1", "phpunit/php-code-coverage": "~2|~4", - "satooshi/php-coveralls": "^2", "squizlabs/php_codesniffer": "^2.8" }, "suggest": { @@ -5073,9 +5388,36 @@ ], "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": "1.x-dev", - "dev-state": "1.x-dev" + "dev-master": "2.x-dev" } }, "autoload": { @@ -5094,20 +5436,20 @@ } ], "description": "Modern task runner", - "time": "2018-08-17T18:44:18+00:00" + "time": "2019-03-19T18:07:19+00:00" }, { "name": "consolidation/self-update", - "version": "1.1.3", + "version": "1.1.5", "source": { "type": "git", "url": "https://github.com/consolidation/self-update.git", - "reference": "de33822f907e0beb0ffad24cf4b1b4fae5ada318" + "reference": "a1c273b14ce334789825a09d06d4c87c0a02ad54" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/self-update/zipball/de33822f907e0beb0ffad24cf4b1b4fae5ada318", - "reference": "de33822f907e0beb0ffad24cf4b1b4fae5ada318", + "url": "https://api.github.com/repos/consolidation/self-update/zipball/a1c273b14ce334789825a09d06d4c87c0a02ad54", + "reference": "a1c273b14ce334789825a09d06d4c87c0a02ad54", "shasum": "" }, "require": { @@ -5144,7 +5486,7 @@ } ], "description": "Provides a self:update command for Symfony Console applications.", - "time": "2018-08-24T17:01:46+00:00" + "time": "2018-10-28T01:52:03+00:00" }, { "name": "dflydev/dot-access-data", @@ -5207,16 +5549,16 @@ }, { "name": "doctrine/annotations", - "version": "v1.6.0", + "version": "v1.6.1", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "c7f2050c68a9ab0bdb0f98567ec08d80ea7d24d5" + "reference": "53120e0eb10355388d6ccbe462f1fea34ddadb24" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/c7f2050c68a9ab0bdb0f98567ec08d80ea7d24d5", - "reference": "c7f2050c68a9ab0bdb0f98567ec08d80ea7d24d5", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/53120e0eb10355388d6ccbe462f1fea34ddadb24", + "reference": "53120e0eb10355388d6ccbe462f1fea34ddadb24", "shasum": "" }, "require": { @@ -5271,38 +5613,40 @@ "docblock", "parser" ], - "time": "2017-12-06T07:11:42+00:00" + "time": "2019-03-25T19:12:02+00:00" }, { "name": "doctrine/collections", - "version": "v1.5.0", + "version": "v1.6.1", "source": { "type": "git", "url": "https://github.com/doctrine/collections.git", - "reference": "a01ee38fcd999f34d9bfbcee59dbda5105449cbf" + "reference": "d2ae4ef05e25197343b6a39bae1d3c427a2f6956" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/a01ee38fcd999f34d9bfbcee59dbda5105449cbf", - "reference": "a01ee38fcd999f34d9bfbcee59dbda5105449cbf", + "url": "https://api.github.com/repos/doctrine/collections/zipball/d2ae4ef05e25197343b6a39bae1d3c427a2f6956", + "reference": "d2ae4ef05e25197343b6a39bae1d3c427a2f6956", "shasum": "" }, "require": { - "php": "^7.1" + "php": "^7.1.3" }, "require-dev": { - "doctrine/coding-standard": "~0.1@dev", - "phpunit/phpunit": "^5.7" + "doctrine/coding-standard": "^6.0", + "phpstan/phpstan-shim": "^0.9.2", + "phpunit/phpunit": "^7.0", + "vimeo/psalm": "^3.2.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.3.x-dev" + "dev-master": "1.6.x-dev" } }, "autoload": { - "psr-0": { - "Doctrine\\Common\\Collections\\": "lib/" + "psr-4": { + "Doctrine\\Common\\Collections\\": "lib/Doctrine/Common/Collections" } }, "notification-url": "https://packagist.org/downloads/", @@ -5331,38 +5675,41 @@ "email": "schmittjoh@gmail.com" } ], - "description": "Collections Abstraction library", - "homepage": "http://www.doctrine-project.org", + "description": "PHP Doctrine Collections library that adds additional functionality on top of PHP arrays.", + "homepage": "https://www.doctrine-project.org/projects/collections.html", "keywords": [ "array", "collections", - "iterator" + "iterators", + "php" ], - "time": "2017-07-22T10:37:32+00:00" + "time": "2019-03-25T19:03:48+00:00" }, { "name": "doctrine/instantiator", - "version": "1.1.0", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda" + "reference": "a2c590166b2133a4633738648b6b064edae0814a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", - "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/a2c590166b2133a4633738648b6b064edae0814a", + "reference": "a2c590166b2133a4633738648b6b064edae0814a", "shasum": "" }, "require": { "php": "^7.1" }, "require-dev": { - "athletic/athletic": "~0.1.8", + "doctrine/coding-standard": "^6.0", "ext-pdo": "*", "ext-phar": "*", - "phpunit/phpunit": "^6.2.3", - "squizlabs/php_codesniffer": "^3.0.2" + "phpbench/phpbench": "^0.13", + "phpstan/phpstan-phpunit": "^0.11", + "phpstan/phpstan-shim": "^0.11", + "phpunit/phpunit": "^7.0" }, "type": "library", "extra": { @@ -5387,12 +5734,12 @@ } ], "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://github.com/doctrine/instantiator", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", "keywords": [ "constructor", "instantiate" ], - "time": "2017-07-22T11:58:36+00:00" + "time": "2019-03-17T17:37:11+00:00" }, { "name": "doctrine/lexer", @@ -5597,16 +5944,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v2.12.3", + "version": "v2.13.3", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "b23d49981cfc95497d03081aeb6df6575196a0d3" + "reference": "38d6f2e9be2aa80bf3c7365612af7f9eb9078719" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/b23d49981cfc95497d03081aeb6df6575196a0d3", - "reference": "b23d49981cfc95497d03081aeb6df6575196a0d3", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/38d6f2e9be2aa80bf3c7365612af7f9eb9078719", + "reference": "38d6f2e9be2aa80bf3c7365612af7f9eb9078719", "shasum": "" }, "require": { @@ -5617,7 +5964,7 @@ "ext-tokenizer": "*", "php": "^5.6 || >=7.0 <7.3", "php-cs-fixer/diff": "^1.3", - "symfony/console": "^3.2 || ^4.0", + "symfony/console": "^3.4.17 || ^4.1.6", "symfony/event-dispatcher": "^3.0 || ^4.0", "symfony/filesystem": "^3.0 || ^4.0", "symfony/finder": "^3.0 || ^4.0", @@ -5633,7 +5980,7 @@ "require-dev": { "johnkary/phpunit-speedtrap": "^1.1 || ^2.0 || ^3.0", "justinrainbow/json-schema": "^5.0", - "keradus/cli-executor": "^1.1", + "keradus/cli-executor": "^1.2", "mikey179/vfsstream": "^1.6", "php-coveralls/php-coveralls": "^2.1", "php-cs-fixer/accessible-object": "^1.0", @@ -5684,7 +6031,7 @@ } ], "description": "A tool to automatically fix PHP code style", - "time": "2018-08-19T22:33:38+00:00" + "time": "2019-01-04T18:24:28+00:00" }, { "name": "fzaninotto/faker", @@ -5736,39 +6083,6 @@ ], "time": "2018-07-12T10:23:15+00:00" }, - { - "name": "g1a/composer-test-scenarios", - "version": "2.2.0", - "source": { - "type": "git", - "url": "https://github.com/g1a/composer-test-scenarios.git", - "reference": "a166fd15191aceab89f30c097e694b7cf3db4880" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/g1a/composer-test-scenarios/zipball/a166fd15191aceab89f30c097e694b7cf3db4880", - "reference": "a166fd15191aceab89f30c097e694b7cf3db4880", - "shasum": "" - }, - "bin": [ - "scripts/create-scenario", - "scripts/dependency-licenses", - "scripts/install-scenario" - ], - "type": "library", - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Greg Anderson", - "email": "greg.1.anderson@greenknowe.org" - } - ], - "description": "Useful scripts for testing multiple sets of Composer dependencies.", - "time": "2018-08-08T23:37:23+00:00" - }, { "name": "grasmash/expander", "version": "1.0.0", @@ -5982,32 +6296,33 @@ }, { "name": "guzzlehttp/psr7", - "version": "1.4.2", + "version": "1.5.2", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c" + "reference": "9f83dded91781a01c63574e387eaa769be769115" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/f5b8a8512e2b58b0071a7280e39f14f72e05d87c", - "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/9f83dded91781a01c63574e387eaa769be769115", + "reference": "9f83dded91781a01c63574e387eaa769be769115", "shasum": "" }, "require": { "php": ">=5.4.0", - "psr/http-message": "~1.0" + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5" }, "provide": { "psr/http-message-implementation": "1.0" }, "require-dev": { - "phpunit/phpunit": "~4.0" + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "1.5-dev" } }, "autoload": { @@ -6037,26 +6352,27 @@ "keywords": [ "http", "message", + "psr-7", "request", "response", "stream", "uri", "url" ], - "time": "2017-03-20T17:10:46+00:00" + "time": "2018-12-04T20:46:45+00:00" }, { "name": "jms/metadata", - "version": "1.6.0", + "version": "1.7.0", "source": { "type": "git", "url": "https://github.com/schmittjoh/metadata.git", - "reference": "6a06970a10e0a532fb52d3959547123b84a3b3ab" + "reference": "e5854ab1aa643623dc64adde718a8eec32b957a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/metadata/zipball/6a06970a10e0a532fb52d3959547123b84a3b3ab", - "reference": "6a06970a10e0a532fb52d3959547123b84a3b3ab", + "url": "https://api.github.com/repos/schmittjoh/metadata/zipball/e5854ab1aa643623dc64adde718a8eec32b957a8", + "reference": "e5854ab1aa643623dc64adde718a8eec32b957a8", "shasum": "" }, "require": { @@ -6079,9 +6395,13 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "Apache-2.0" + "MIT" ], "authors": [ + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + }, { "name": "Johannes M. Schmitt", "email": "schmittjoh@gmail.com" @@ -6094,7 +6414,7 @@ "xml", "yaml" ], - "time": "2016-12-05T10:18:33+00:00" + "time": "2018-10-26T12:40:10+00:00" }, { "name": "jms/parser-lib", @@ -6347,25 +6667,56 @@ ], "time": "2018-02-14T22:37:14+00:00" }, + { + "name": "magento/magento-coding-standard", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/magento/magento-coding-standard.git", + "reference": "489029a285c637825294e272d31c3f4ac00a454e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/magento/magento-coding-standard/zipball/489029a285c637825294e272d31c3f4ac00a454e", + "reference": "489029a285c637825294e272d31c3f4ac00a454e", + "shasum": "" + }, + "require": { + "php": ">=5.6.0", + "squizlabs/php_codesniffer": "~3.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "A set of Magento specific PHP CodeSniffer rules.", + "time": "2019-04-01T17:03:33+00:00" + }, { "name": "magento/magento2-functional-testing-framework", - "version": "2.3.5", + "version": "2.3.14", "source": { "type": "git", "url": "https://github.com/magento/magento2-functional-testing-framework.git", - "reference": "bb1518aab82464e25ff97874da939d13ba4b6fac" + "reference": "b4002b3fe53884895921b44cf519d42918e3c7c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/bb1518aab82464e25ff97874da939d13ba4b6fac", - "reference": "bb1518aab82464e25ff97874da939d13ba4b6fac", + "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/b4002b3fe53884895921b44cf519d42918e3c7c6", + "reference": "b4002b3fe53884895921b44cf519d42918e3c7c6", "shasum": "" }, "require": { - "allure-framework/allure-codeception": "~1.2.6", - "codeception/codeception": "~2.3.4", + "allure-framework/allure-codeception": "~1.3.0", + "codeception/codeception": "~2.3.4 || ~2.4.0 ", "consolidation/robo": "^1.0.0", "epfremme/swagger-php": "^2.0", + "ext-curl": "*", "flow/jsonpath": ">0.2", "fzaninotto/faker": "^1.6", "monolog/monolog": "^1.0", @@ -6382,6 +6733,7 @@ "goaop/framework": "2.2.0", "php-coveralls/php-coveralls": "^1.0", "phpmd/phpmd": "^2.6.0", + "phpunit/phpunit": "~6.5.0 || ~7.0.0", "rregeer/phpunit-coverage-check": "^0.1.4", "sebastian/phpcpd": "~3.0 || ~4.0", "squizlabs/php_codesniffer": "~3.2", @@ -6416,7 +6768,53 @@ "magento", "testing" ], - "time": "2018-08-21T16:57:34+00:00" + "time": "2019-02-19T16:03:22+00:00" + }, + { + "name": "mikey179/vfsStream", + "version": "v1.6.5", + "source": { + "type": "git", + "url": "https://github.com/mikey179/vfsStream.git", + "reference": "d5fec95f541d4d71c4823bb5e30cf9b9e5b96145" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mikey179/vfsStream/zipball/d5fec95f541d4d71c4823bb5e30cf9b9e5b96145", + "reference": "d5fec95f541d4d71c4823bb5e30cf9b9e5b96145", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-0": { + "org\\bovigo\\vfs\\": "src/main/php" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Frank Kleine", + "homepage": "http://frankkleine.de/", + "role": "Developer" + } + ], + "description": "Virtual file system to mock the real file system in unit tests.", + "homepage": "http://vfs.bovigo.org/", + "time": "2017-08-01T08:02:14+00:00" }, { "name": "moontoast/math", @@ -7384,16 +7782,16 @@ }, { "name": "phpunit/phpunit", - "version": "6.5.12", + "version": "6.5.14", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "24da433d7384824d65ea93fbb462e2f31bbb494e" + "reference": "bac23fe7ff13dbdb461481f706f0e9fe746334b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/24da433d7384824d65ea93fbb462e2f31bbb494e", - "reference": "24da433d7384824d65ea93fbb462e2f31bbb494e", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/bac23fe7ff13dbdb461481f706f0e9fe746334b7", + "reference": "bac23fe7ff13dbdb461481f706f0e9fe746334b7", "shasum": "" }, "require": { @@ -7464,7 +7862,7 @@ "testing", "xunit" ], - "time": "2018-08-22T06:32:48+00:00" + "time": "2019-02-01T05:22:47+00:00" }, { "name": "phpunit/phpunit-mock-objects", @@ -7523,8 +7921,49 @@ "mock", "xunit" ], + "abandoned": true, "time": "2018-08-09T05:50:03+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", @@ -8175,16 +8614,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.3.0", + "version": "3.3.1", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "d86873af43b4aa9d1f39a3601cc0cfcf02b25266" + "reference": "628a481780561150481a9ec74709092b9759b3ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/d86873af43b4aa9d1f39a3601cc0cfcf02b25266", - "reference": "d86873af43b4aa9d1f39a3601cc0cfcf02b25266", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/628a481780561150481a9ec74709092b9759b3ec", + "reference": "628a481780561150481a9ec74709092b9759b3ec", "shasum": "" }, "require": { @@ -8222,20 +8661,20 @@ "phpcs", "standards" ], - "time": "2018-06-06T23:58:19+00:00" + "time": "2018-07-26T23:47:18+00:00" }, { "name": "symfony/browser-kit", - "version": "v4.1.4", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", - "reference": "c55fe9257003b2d95c0211b3f6941e8dfd26dffd" + "reference": "61d85c5af2fc058014c7c89504c3944e73a086f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/c55fe9257003b2d95c0211b3f6941e8dfd26dffd", - "reference": "c55fe9257003b2d95c0211b3f6941e8dfd26dffd", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/61d85c5af2fc058014c7c89504c3944e73a086f0", + "reference": "61d85c5af2fc058014c7c89504c3944e73a086f0", "shasum": "" }, "require": { @@ -8252,7 +8691,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -8279,20 +8718,20 @@ ], "description": "Symfony BrowserKit Component", "homepage": "https://symfony.com", - "time": "2018-07-26T09:10:45+00:00" + "time": "2019-02-23T15:17:42+00:00" }, { "name": "symfony/config", - "version": "v4.1.4", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "76015a3cc372b14d00040ff58e18e29f69eba717" + "reference": "7f70d79c7a24a94f8e98abb988049403a53d7b31" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/76015a3cc372b14d00040ff58e18e29f69eba717", - "reference": "76015a3cc372b14d00040ff58e18e29f69eba717", + "url": "https://api.github.com/repos/symfony/config/zipball/7f70d79c7a24a94f8e98abb988049403a53d7b31", + "reference": "7f70d79c7a24a94f8e98abb988049403a53d7b31", "shasum": "" }, "require": { @@ -8315,7 +8754,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -8342,37 +8781,48 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2018-08-08T06:37:38+00:00" + "time": "2019-02-23T15:17:42+00:00" }, { - "name": "symfony/css-selector", - "version": "v4.1.4", + "name": "symfony/contracts", + "version": "v1.0.2", "source": { "type": "git", - "url": "https://github.com/symfony/css-selector.git", - "reference": "2a4df7618f869b456f9096781e78c57b509d76c7" + "url": "https://github.com/symfony/contracts.git", + "reference": "1aa7ab2429c3d594dd70689604b5cf7421254cdf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/2a4df7618f869b456f9096781e78c57b509d76c7", - "reference": "2a4df7618f869b456f9096781e78c57b509d76c7", + "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": "4.1-dev" + "dev-master": "1.0-dev" } }, "autoload": { "psr-4": { - "Symfony\\Component\\CssSelector\\": "" + "Symfony\\Contracts\\": "" }, "exclude-from-classmap": [ - "/Tests/" + "**/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -8381,51 +8831,57 @@ ], "authors": [ { - "name": "Jean-François Simon", - "email": "jeanfrancois.simon@sensiolabs.com" - }, - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony CssSelector Component", + "description": "A set of abstractions extracted out of the Symfony components", "homepage": "https://symfony.com", - "time": "2018-07-26T09:10:45+00:00" + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "time": "2018-12-05T08:06:11+00:00" }, { "name": "symfony/dependency-injection", - "version": "v4.1.4", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "bae4983003c9d451e278504d7d9b9d7fc1846873" + "reference": "cdadb3765df7c89ac93628743913b92bb91f1704" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/bae4983003c9d451e278504d7d9b9d7fc1846873", - "reference": "bae4983003c9d451e278504d7d9b9d7fc1846873", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/cdadb3765df7c89ac93628743913b92bb91f1704", + "reference": "cdadb3765df7c89ac93628743913b92bb91f1704", "shasum": "" }, "require": { "php": "^7.1.3", - "psr/container": "^1.0" + "psr/container": "^1.0", + "symfony/contracts": "^1.0" }, "conflict": { - "symfony/config": "<4.1.1", + "symfony/config": "<4.2", "symfony/finder": "<3.4", "symfony/proxy-manager-bridge": "<3.4", "symfony/yaml": "<3.4" }, "provide": { - "psr/container-implementation": "1.0" + "psr/container-implementation": "1.0", + "symfony/service-contracts-implementation": "1.0" }, "require-dev": { - "symfony/config": "~4.1", + "symfony/config": "~4.2", "symfony/expression-language": "~3.4|~4.0", "symfony/yaml": "~3.4|~4.0" }, @@ -8439,7 +8895,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -8466,20 +8922,20 @@ ], "description": "Symfony DependencyInjection Component", "homepage": "https://symfony.com", - "time": "2018-08-08T11:48:58+00:00" + "time": "2019-02-23T15:17:42+00:00" }, { "name": "symfony/dom-crawler", - "version": "v4.1.4", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "1c4519d257e652404c3aa550207ccd8ada66b38e" + "reference": "53c97769814c80a84a8403efcf3ae7ae966d53bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/1c4519d257e652404c3aa550207ccd8ada66b38e", - "reference": "1c4519d257e652404c3aa550207ccd8ada66b38e", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/53c97769814c80a84a8403efcf3ae7ae966d53bb", + "reference": "53c97769814c80a84a8403efcf3ae7ae966d53bb", "shasum": "" }, "require": { @@ -8496,7 +8952,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -8523,20 +8979,20 @@ ], "description": "Symfony DomCrawler Component", "homepage": "https://symfony.com", - "time": "2018-07-26T11:00:49+00:00" + "time": "2019-02-23T15:17:42+00:00" }, { "name": "symfony/http-foundation", - "version": "v4.1.4", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "3a5c91e133b220bb882b3cd773ba91bf39989345" + "reference": "850a667d6254ccf6c61d853407b16f21c4579c77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/3a5c91e133b220bb882b3cd773ba91bf39989345", - "reference": "3a5c91e133b220bb882b3cd773ba91bf39989345", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/850a667d6254ccf6c61d853407b16f21c4579c77", + "reference": "850a667d6254ccf6c61d853407b16f21c4579c77", "shasum": "" }, "require": { @@ -8550,7 +9006,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -8577,20 +9033,20 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2018-08-27T17:47:02+00:00" + "time": "2019-02-26T08:03:39+00:00" }, { "name": "symfony/options-resolver", - "version": "v4.1.4", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "1913f1962477cdbb13df951f8147d5da1fe2412c" + "reference": "3896e5a7d06fd15fa4947694c8dcdd371ff147d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/1913f1962477cdbb13df951f8147d5da1fe2412c", - "reference": "1913f1962477cdbb13df951f8147d5da1fe2412c", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/3896e5a7d06fd15fa4947694c8dcdd371ff147d1", + "reference": "3896e5a7d06fd15fa4947694c8dcdd371ff147d1", "shasum": "" }, "require": { @@ -8599,7 +9055,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -8631,20 +9087,20 @@ "configuration", "options" ], - "time": "2018-07-26T08:55:25+00:00" + "time": "2019-02-23T15:17:42+00:00" }, { "name": "symfony/polyfill-php70", - "version": "v1.9.0", + "version": "v1.11.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php70.git", - "reference": "1e24b0c4a56d55aaf368763a06c6d1c7d3194934" + "reference": "bc4858fb611bda58719124ca079baff854149c89" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/1e24b0c4a56d55aaf368763a06c6d1c7d3194934", - "reference": "1e24b0c4a56d55aaf368763a06c6d1c7d3194934", + "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/bc4858fb611bda58719124ca079baff854149c89", + "reference": "bc4858fb611bda58719124ca079baff854149c89", "shasum": "" }, "require": { @@ -8654,7 +9110,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.9-dev" + "dev-master": "1.11-dev" } }, "autoload": { @@ -8690,20 +9146,20 @@ "portable", "shim" ], - "time": "2018-08-06T14:22:27+00:00" + "time": "2019-02-06T07:57:58+00:00" }, { "name": "symfony/polyfill-php72", - "version": "v1.9.0", + "version": "v1.11.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "95c50420b0baed23852452a7f0c7b527303ed5ae" + "reference": "ab50dcf166d5f577978419edd37aa2bb8eabce0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/95c50420b0baed23852452a7f0c7b527303ed5ae", - "reference": "95c50420b0baed23852452a7f0c7b527303ed5ae", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/ab50dcf166d5f577978419edd37aa2bb8eabce0c", + "reference": "ab50dcf166d5f577978419edd37aa2bb8eabce0c", "shasum": "" }, "require": { @@ -8712,7 +9168,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.9-dev" + "dev-master": "1.11-dev" } }, "autoload": { @@ -8745,29 +9201,30 @@ "portable", "shim" ], - "time": "2018-08-06T14:22:27+00:00" + "time": "2019-02-06T07:57:58+00:00" }, { "name": "symfony/stopwatch", - "version": "v4.1.4", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "966c982df3cca41324253dc0c7ffe76b6076b705" + "reference": "b1a5f646d56a3290230dbc8edf2a0d62cda23f67" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/966c982df3cca41324253dc0c7ffe76b6076b705", - "reference": "966c982df3cca41324253dc0c7ffe76b6076b705", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/b1a5f646d56a3290230dbc8edf2a0d62cda23f67", + "reference": "b1a5f646d56a3290230dbc8edf2a0d62cda23f67", "shasum": "" }, "require": { - "php": "^7.1.3" + "php": "^7.1.3", + "symfony/contracts": "^1.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -8794,20 +9251,20 @@ ], "description": "Symfony Stopwatch Component", "homepage": "https://symfony.com", - "time": "2018-07-26T11:00:49+00:00" + "time": "2019-01-16T20:31:39+00:00" }, { "name": "symfony/yaml", - "version": "v3.4.15", + "version": "v3.4.23", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "c2f4812ead9f847cb69e90917ca7502e6892d6b8" + "reference": "57f1ce82c997f5a8701b89ef970e36bb657fd09c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/c2f4812ead9f847cb69e90917ca7502e6892d6b8", - "reference": "c2f4812ead9f847cb69e90917ca7502e6892d6b8", + "url": "https://api.github.com/repos/symfony/yaml/zipball/57f1ce82c997f5a8701b89ef970e36bb657fd09c", + "reference": "57f1ce82c997f5a8701b89ef970e36bb657fd09c", "shasum": "" }, "require": { @@ -8853,7 +9310,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2018-08-10T07:34:36+00:00" + "time": "2019-02-23T15:06:07+00:00" }, { "name": "theseer/fdomdocument", @@ -8937,20 +9394,21 @@ }, { "name": "vlucas/phpdotenv", - "version": "v2.5.1", + "version": "v2.6.1", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "8abb4f9aa89ddea9d52112c65bbe8d0125e2fa8e" + "reference": "2a7dcf7e3e02dc5e701004e51a6f304b713107d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/8abb4f9aa89ddea9d52112c65bbe8d0125e2fa8e", - "reference": "8abb4f9aa89ddea9d52112c65bbe8d0125e2fa8e", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/2a7dcf7e3e02dc5e701004e51a6f304b713107d5", + "reference": "2a7dcf7e3e02dc5e701004e51a6f304b713107d5", "shasum": "" }, "require": { - "php": ">=5.3.9" + "php": ">=5.3.9", + "symfony/polyfill-ctype": "^1.9" }, "require-dev": { "phpunit/phpunit": "^4.8.35 || ^5.0" @@ -8958,7 +9416,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } }, "autoload": { @@ -8983,24 +9441,25 @@ "env", "environment" ], - "time": "2018-07-29T20:33:41+00:00" + "time": "2019-01-29T11:11:52+00:00" }, { "name": "webmozart/assert", - "version": "1.3.0", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/webmozart/assert.git", - "reference": "0df1908962e7a3071564e857d86874dad1ef204a" + "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/0df1908962e7a3071564e857d86874dad1ef204a", - "reference": "0df1908962e7a3071564e857d86874dad1ef204a", + "url": "https://api.github.com/repos/webmozart/assert/zipball/83e253c8e0be5b0257b881e1827274667c5c17a9", + "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0" + "php": "^5.3.3 || ^7.0", + "symfony/polyfill-ctype": "^1.8" }, "require-dev": { "phpunit/phpunit": "^4.6", @@ -9033,7 +9492,7 @@ "check", "validate" ], - "time": "2018-01-29T19:49:41+00:00" + "time": "2018-12-25T11:19:39+00:00" } ], "aliases": [], @@ -9045,6 +9504,7 @@ "prefer-lowest": false, "platform": { "php": "~7.1.3||~7.2.0", + "ext-bcmath": "*", "ext-ctype": "*", "ext-curl": "*", "ext-dom": "*", @@ -9060,7 +9520,6 @@ "ext-spl": "*", "ext-xsl": "*", "ext-zip": "*", - "ext-bcmath": "*", "lib-libxml": "*" }, "platform-dev": [] diff --git a/dev/tests/acceptance/.gitignore b/dev/tests/acceptance/.gitignore index e3ef78df82b9..87ce9d4ff35d 100755 --- a/dev/tests/acceptance/.gitignore +++ b/dev/tests/acceptance/.gitignore @@ -7,4 +7,5 @@ tests/functional.suite.yml tests/functional/Magento/FunctionalTest/_generated vendor/* mftf.log +/.credentials.example /utils/ \ No newline at end of file diff --git a/dev/tests/acceptance/RoboFile.php b/dev/tests/acceptance/RoboFile.php deleted file mode 100644 index e6e9e591bbd8..000000000000 --- 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 83cad123f856..000000000000 --- 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 8542402a98f5..000000000000 --- a/dev/tests/acceptance/composer.lock +++ /dev/null @@ -1,3262 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", - "This file is @generated automatically" - ], - "content-hash": "46ca2d50566f5069daef753664080c5a", - "packages": [ - { - "name": "behat/gherkin", - "version": "v4.4.5", - "source": { - "type": "git", - "url": "https://github.com/Behat/Gherkin.git", - "reference": "5c14cff4f955b17d20d088dec1bde61c0539ec74" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Behat/Gherkin/zipball/5c14cff4f955b17d20d088dec1bde61c0539ec74", - "reference": "5c14cff4f955b17d20d088dec1bde61c0539ec74", - "shasum": "" - }, - "require": { - "php": ">=5.3.1" - }, - "require-dev": { - "phpunit/phpunit": "~4.5|~5", - "symfony/phpunit-bridge": "~2.7|~3", - "symfony/yaml": "~2.3|~3" - }, - "suggest": { - "symfony/yaml": "If you want to parse features, represented in YAML files" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - }, - "autoload": { - "psr-0": { - "Behat\\Gherkin": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - } - ], - "description": "Gherkin DSL parser for PHP 5.3", - "homepage": "http://behat.org/", - "keywords": [ - "BDD", - "Behat", - "Cucumber", - "DSL", - "gherkin", - "parser" - ], - "time": "2016-10-30T11:50:56+00:00" - }, - { - "name": "codeception/codeception", - "version": "2.3.9", - "source": { - "type": "git", - "url": "https://github.com/Codeception/Codeception.git", - "reference": "104f46fa0bde339f1bcc3a375aac21eb36e65a1e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Codeception/Codeception/zipball/104f46fa0bde339f1bcc3a375aac21eb36e65a1e", - "reference": "104f46fa0bde339f1bcc3a375aac21eb36e65a1e", - "shasum": "" - }, - "require": { - "behat/gherkin": "~4.4.0", - "codeception/stub": "^1.0", - "ext-json": "*", - "ext-mbstring": "*", - "facebook/webdriver": ">=1.1.3 <2.0", - "guzzlehttp/guzzle": ">=4.1.4 <7.0", - "guzzlehttp/psr7": "~1.0", - "php": ">=5.4.0 <8.0", - "phpunit/php-code-coverage": ">=2.2.4 <6.0", - "phpunit/phpunit": ">=4.8.28 <5.0.0 || >=5.6.3 <7.0", - "sebastian/comparator": ">1.1 <3.0", - "sebastian/diff": ">=1.4 <3.0", - "symfony/browser-kit": ">=2.7 <5.0", - "symfony/console": ">=2.7 <5.0", - "symfony/css-selector": ">=2.7 <5.0", - "symfony/dom-crawler": ">=2.7 <5.0", - "symfony/event-dispatcher": ">=2.7 <5.0", - "symfony/finder": ">=2.7 <5.0", - "symfony/yaml": ">=2.7 <5.0" - }, - "require-dev": { - "codeception/specify": "~0.3", - "facebook/graph-sdk": "~5.3", - "flow/jsonpath": "~0.2", - "monolog/monolog": "~1.8", - "pda/pheanstalk": "~3.0", - "php-amqplib/php-amqplib": "~2.4", - "predis/predis": "^1.0", - "squizlabs/php_codesniffer": "~2.0", - "symfony/process": ">=2.7 <5.0", - "vlucas/phpdotenv": "^2.4.0" - }, - "suggest": { - "aws/aws-sdk-php": "For using AWS Auth in REST module and Queue module", - "codeception/phpbuiltinserver": "Start and stop PHP built-in web server for your tests", - "codeception/specify": "BDD-style code blocks", - "codeception/verify": "BDD-style assertions", - "flow/jsonpath": "For using JSONPath in REST module", - "league/factory-muffin": "For DataFactory module", - "league/factory-muffin-faker": "For Faker support in DataFactory module", - "phpseclib/phpseclib": "for SFTP option in FTP Module", - "stecman/symfony-console-completion": "For BASH autocompletion", - "symfony/phpunit-bridge": "For phpunit-bridge support" - }, - "bin": [ - "codecept" - ], - "type": "library", - "extra": { - "branch-alias": [] - }, - "autoload": { - "psr-4": { - "Codeception\\": "src\\Codeception", - "Codeception\\Extension\\": "ext" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Bodnarchuk", - "email": "davert@mail.ua", - "homepage": "http://codegyre.com" - } - ], - "description": "BDD-style testing framework", - "homepage": "http://codeception.com/", - "keywords": [ - "BDD", - "TDD", - "acceptance testing", - "functional testing", - "unit testing" - ], - "time": "2018-02-26T23:29:41+00:00" - }, - { - "name": "codeception/stub", - "version": "1.0.4", - "source": { - "type": "git", - "url": "https://github.com/Codeception/Stub.git", - "reference": "681b62348837a5ef07d10d8a226f5bc358cc8805" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Codeception/Stub/zipball/681b62348837a5ef07d10d8a226f5bc358cc8805", - "reference": "681b62348837a5ef07d10d8a226f5bc358cc8805", - "shasum": "" - }, - "require": { - "phpunit/phpunit-mock-objects": ">2.3 <7.0" - }, - "require-dev": { - "phpunit/phpunit": ">=4.8 <8.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Codeception\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Flexible Stub wrapper for PHPUnit's Mock Builder", - "time": "2018-05-17T09:31:08+00:00" - }, - { - "name": "consolidation/annotated-command", - "version": "2.8.4", - "source": { - "type": "git", - "url": "https://github.com/consolidation/annotated-command.git", - "reference": "651541a0b68318a2a202bda558a676e5ad92223c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/consolidation/annotated-command/zipball/651541a0b68318a2a202bda558a676e5ad92223c", - "reference": "651541a0b68318a2a202bda558a676e5ad92223c", - "shasum": "" - }, - "require": { - "consolidation/output-formatters": "^3.1.12", - "php": ">=5.4.0", - "psr/log": "^1", - "symfony/console": "^2.8|^3|^4", - "symfony/event-dispatcher": "^2.5|^3|^4", - "symfony/finder": "^2.5|^3|^4" - }, - "require-dev": { - "g1a/composer-test-scenarios": "^2", - "phpunit/phpunit": "^6", - "satooshi/php-coveralls": "^2", - "squizlabs/php_codesniffer": "^2.7" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "Consolidation\\AnnotatedCommand\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Greg Anderson", - "email": "greg.1.anderson@greenknowe.org" - } - ], - "description": "Initialize Symfony Console commands from annotated command class methods.", - "time": "2018-05-25T18:04:25+00:00" - }, - { - "name": "consolidation/config", - "version": "1.0.11", - "source": { - "type": "git", - "url": "https://github.com/consolidation/config.git", - "reference": "ede41d946078e97e7a9513aadc3352f1c26817af" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/consolidation/config/zipball/ede41d946078e97e7a9513aadc3352f1c26817af", - "reference": "ede41d946078e97e7a9513aadc3352f1c26817af", - "shasum": "" - }, - "require": { - "dflydev/dot-access-data": "^1.1.0", - "grasmash/expander": "^1", - "php": ">=5.4.0" - }, - "require-dev": { - "g1a/composer-test-scenarios": "^1", - "phpunit/phpunit": "^4", - "satooshi/php-coveralls": "^1.0", - "squizlabs/php_codesniffer": "2.*", - "symfony/console": "^2.5|^3|^4", - "symfony/yaml": "^2.8.11|^3|^4" - }, - "suggest": { - "symfony/yaml": "Required to use Consolidation\\Config\\Loader\\YamlConfigLoader" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Consolidation\\Config\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Greg Anderson", - "email": "greg.1.anderson@greenknowe.org" - } - ], - "description": "Provide configuration services for a commandline tool.", - "time": "2018-05-27T01:17:02+00:00" - }, - { - "name": "consolidation/log", - "version": "1.0.6", - "source": { - "type": "git", - "url": "https://github.com/consolidation/log.git", - "reference": "dfd8189a771fe047bf3cd669111b2de5f1c79395" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/consolidation/log/zipball/dfd8189a771fe047bf3cd669111b2de5f1c79395", - "reference": "dfd8189a771fe047bf3cd669111b2de5f1c79395", - "shasum": "" - }, - "require": { - "php": ">=5.5.0", - "psr/log": "~1.0", - "symfony/console": "^2.8|^3|^4" - }, - "require-dev": { - "g1a/composer-test-scenarios": "^1", - "phpunit/phpunit": "4.*", - "satooshi/php-coveralls": "^2", - "squizlabs/php_codesniffer": "2.*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Consolidation\\Log\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Greg Anderson", - "email": "greg.1.anderson@greenknowe.org" - } - ], - "description": "Improved Psr-3 / Psr\\Log logger based on Symfony Console components.", - "time": "2018-05-25T18:14:39+00:00" - }, - { - "name": "consolidation/output-formatters", - "version": "3.2.1", - "source": { - "type": "git", - "url": "https://github.com/consolidation/output-formatters.git", - "reference": "d78ef59aea19d3e2e5a23f90a055155ee78a0ad5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/consolidation/output-formatters/zipball/d78ef59aea19d3e2e5a23f90a055155ee78a0ad5", - "reference": "d78ef59aea19d3e2e5a23f90a055155ee78a0ad5", - "shasum": "" - }, - "require": { - "php": ">=5.4.0", - "symfony/console": "^2.8|^3|^4", - "symfony/finder": "^2.5|^3|^4" - }, - "require-dev": { - "g1a/composer-test-scenarios": "^2", - "phpunit/phpunit": "^5.7.27", - "satooshi/php-coveralls": "^2", - "squizlabs/php_codesniffer": "^2.7", - "symfony/console": "3.2.3", - "symfony/var-dumper": "^2.8|^3|^4", - "victorjonsson/markdowndocs": "^1.3" - }, - "suggest": { - "symfony/var-dumper": "For using the var_dump formatter" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Consolidation\\OutputFormatters\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Greg Anderson", - "email": "greg.1.anderson@greenknowe.org" - } - ], - "description": "Format text by applying transformations provided by plug-in formatters.", - "time": "2018-05-25T18:02:34+00:00" - }, - { - "name": "consolidation/robo", - "version": "1.3.0", - "source": { - "type": "git", - "url": "https://github.com/consolidation/Robo.git", - "reference": "ac563abfadf7cb7314b4e152f2b5033a6c255f6f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/consolidation/Robo/zipball/ac563abfadf7cb7314b4e152f2b5033a6c255f6f", - "reference": "ac563abfadf7cb7314b4e152f2b5033a6c255f6f", - "shasum": "" - }, - "require": { - "consolidation/annotated-command": "^2.8.2", - "consolidation/config": "^1.0.10", - "consolidation/log": "~1", - "consolidation/output-formatters": "^3.1.13", - "grasmash/yaml-expander": "^1.3", - "league/container": "^2.2", - "php": ">=5.5.0", - "symfony/console": "^2.8|^3|^4", - "symfony/event-dispatcher": "^2.5|^3|^4", - "symfony/filesystem": "^2.5|^3|^4", - "symfony/finder": "^2.5|^3|^4", - "symfony/process": "^2.5|^3|^4" - }, - "replace": { - "codegyre/robo": "< 1.0" - }, - "require-dev": { - "codeception/aspect-mock": "^1|^2.1.1", - "codeception/base": "^2.3.7", - "codeception/verify": "^0.3.2", - "g1a/composer-test-scenarios": "^2", - "goaop/framework": "~2.1.2", - "goaop/parser-reflection": "^1.1.0", - "natxet/cssmin": "3.0.4", - "nikic/php-parser": "^3.1.5", - "patchwork/jsqueeze": "~2", - "pear/archive_tar": "^1.4.2", - "phpunit/php-code-coverage": "~2|~4", - "satooshi/php-coveralls": "^2", - "squizlabs/php_codesniffer": "^2.8" - }, - "suggest": { - "henrikbjorn/lurker": "For monitoring filesystem changes in taskWatch", - "natxet/CssMin": "For minifying CSS files in taskMinify", - "patchwork/jsqueeze": "For minifying JS files in taskMinify", - "pear/archive_tar": "Allows tar archives to be created and extracted in taskPack and taskExtract, respectively." - }, - "bin": [ - "robo" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev", - "dev-state": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Robo\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Davert", - "email": "davert.php@resend.cc" - } - ], - "description": "Modern task runner", - "time": "2018-05-27T01:42:53+00:00" - }, - { - "name": "container-interop/container-interop", - "version": "1.2.0", - "source": { - "type": "git", - "url": "https://github.com/container-interop/container-interop.git", - "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/container-interop/container-interop/zipball/79cbf1341c22ec75643d841642dd5d6acd83bdb8", - "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8", - "shasum": "" - }, - "require": { - "psr/container": "^1.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Interop\\Container\\": "src/Interop/Container/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Promoting the interoperability of container objects (DIC, SL, etc.)", - "homepage": "https://github.com/container-interop/container-interop", - "time": "2017-02-14T19:40:03+00:00" - }, - { - "name": "dflydev/dot-access-data", - "version": "v1.1.0", - "source": { - "type": "git", - "url": "https://github.com/dflydev/dflydev-dot-access-data.git", - "reference": "3fbd874921ab2c041e899d044585a2ab9795df8a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/3fbd874921ab2c041e899d044585a2ab9795df8a", - "reference": "3fbd874921ab2c041e899d044585a2ab9795df8a", - "shasum": "" - }, - "require": { - "php": ">=5.3.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "psr-0": { - "Dflydev\\DotAccessData": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Dragonfly Development Inc.", - "email": "info@dflydev.com", - "homepage": "http://dflydev.com" - }, - { - "name": "Beau Simensen", - "email": "beau@dflydev.com", - "homepage": "http://beausimensen.com" - }, - { - "name": "Carlos Frutos", - "email": "carlos@kiwing.it", - "homepage": "https://github.com/cfrutos" - } - ], - "description": "Given a deep data structure, access data by dot notation.", - "homepage": "https://github.com/dflydev/dflydev-dot-access-data", - "keywords": [ - "access", - "data", - "dot", - "notation" - ], - "time": "2017-01-20T21:14:22+00:00" - }, - { - "name": "doctrine/instantiator", - "version": "1.1.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", - "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", - "shasum": "" - }, - "require": { - "php": "^7.1" - }, - "require-dev": { - "athletic/athletic": "~0.1.8", - "ext-pdo": "*", - "ext-phar": "*", - "phpunit/phpunit": "^6.2.3", - "squizlabs/php_codesniffer": "^3.0.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, - "autoload": { - "psr-4": { - "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "http://ocramius.github.com/" - } - ], - "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://github.com/doctrine/instantiator", - "keywords": [ - "constructor", - "instantiate" - ], - "time": "2017-07-22T11:58:36+00:00" - }, - { - "name": "facebook/webdriver", - "version": "1.6.0", - "source": { - "type": "git", - "url": "https://github.com/facebook/php-webdriver.git", - "reference": "bd8c740097eb9f2fc3735250fc1912bc811a954e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/facebook/php-webdriver/zipball/bd8c740097eb9f2fc3735250fc1912bc811a954e", - "reference": "bd8c740097eb9f2fc3735250fc1912bc811a954e", - "shasum": "" - }, - "require": { - "ext-curl": "*", - "ext-json": "*", - "ext-mbstring": "*", - "ext-zip": "*", - "php": "^5.6 || ~7.0", - "symfony/process": "^2.8 || ^3.1 || ^4.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^2.0", - "jakub-onderka/php-parallel-lint": "^0.9.2", - "php-coveralls/php-coveralls": "^2.0", - "php-mock/php-mock-phpunit": "^1.1", - "phpunit/phpunit": "^5.7", - "sebastian/environment": "^1.3.4 || ^2.0 || ^3.0", - "squizlabs/php_codesniffer": "^2.6", - "symfony/var-dumper": "^3.3 || ^4.0" - }, - "suggest": { - "ext-SimpleXML": "For Firefox profile creation" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-community": "1.5-dev" - } - }, - "autoload": { - "psr-4": { - "Facebook\\WebDriver\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "description": "A PHP client for Selenium WebDriver", - "homepage": "https://github.com/facebook/php-webdriver", - "keywords": [ - "facebook", - "php", - "selenium", - "webdriver" - ], - "time": "2018-05-16T17:37:13+00:00" - }, - { - "name": "grasmash/expander", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/grasmash/expander.git", - "reference": "95d6037344a4be1dd5f8e0b0b2571a28c397578f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/grasmash/expander/zipball/95d6037344a4be1dd5f8e0b0b2571a28c397578f", - "reference": "95d6037344a4be1dd5f8e0b0b2571a28c397578f", - "shasum": "" - }, - "require": { - "dflydev/dot-access-data": "^1.1.0", - "php": ">=5.4" - }, - "require-dev": { - "greg-1-anderson/composer-test-scenarios": "^1", - "phpunit/phpunit": "^4|^5.5.4", - "satooshi/php-coveralls": "^1.0.2|dev-master", - "squizlabs/php_codesniffer": "^2.7" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Grasmash\\Expander\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Matthew Grasmick" - } - ], - "description": "Expands internal property references in PHP arrays file.", - "time": "2017-12-21T22:14:55+00:00" - }, - { - "name": "grasmash/yaml-expander", - "version": "1.4.0", - "source": { - "type": "git", - "url": "https://github.com/grasmash/yaml-expander.git", - "reference": "3f0f6001ae707a24f4d9733958d77d92bf9693b1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/grasmash/yaml-expander/zipball/3f0f6001ae707a24f4d9733958d77d92bf9693b1", - "reference": "3f0f6001ae707a24f4d9733958d77d92bf9693b1", - "shasum": "" - }, - "require": { - "dflydev/dot-access-data": "^1.1.0", - "php": ">=5.4", - "symfony/yaml": "^2.8.11|^3|^4" - }, - "require-dev": { - "greg-1-anderson/composer-test-scenarios": "^1", - "phpunit/phpunit": "^4.8|^5.5.4", - "satooshi/php-coveralls": "^1.0.2|dev-master", - "squizlabs/php_codesniffer": "^2.7" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Grasmash\\YamlExpander\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Matthew Grasmick" - } - ], - "description": "Expands internal property references in a yaml file.", - "time": "2017-12-16T16:06:03+00:00" - }, - { - "name": "guzzlehttp/guzzle", - "version": "6.3.3", - "source": { - "type": "git", - "url": "https://github.com/guzzle/guzzle.git", - "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/407b0cb880ace85c9b63c5f9551db498cb2d50ba", - "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba", - "shasum": "" - }, - "require": { - "guzzlehttp/promises": "^1.0", - "guzzlehttp/psr7": "^1.4", - "php": ">=5.5" - }, - "require-dev": { - "ext-curl": "*", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0", - "psr/log": "^1.0" - }, - "suggest": { - "psr/log": "Required for using the Log middleware" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "6.3-dev" - } - }, - "autoload": { - "files": [ - "src/functions_include.php" - ], - "psr-4": { - "GuzzleHttp\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - } - ], - "description": "Guzzle is a PHP HTTP client library", - "homepage": "http://guzzlephp.org/", - "keywords": [ - "client", - "curl", - "framework", - "http", - "http client", - "rest", - "web service" - ], - "time": "2018-04-22T15:46:56+00:00" - }, - { - "name": "guzzlehttp/promises", - "version": "v1.3.1", - "source": { - "type": "git", - "url": "https://github.com/guzzle/promises.git", - "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646", - "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646", - "shasum": "" - }, - "require": { - "php": ">=5.5.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - } - }, - "autoload": { - "psr-4": { - "GuzzleHttp\\Promise\\": "src/" - }, - "files": [ - "src/functions_include.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - } - ], - "description": "Guzzle promises library", - "keywords": [ - "promise" - ], - "time": "2016-12-20T10:07:11+00:00" - }, - { - "name": "guzzlehttp/psr7", - "version": "1.4.2", - "source": { - "type": "git", - "url": "https://github.com/guzzle/psr7.git", - "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/f5b8a8512e2b58b0071a7280e39f14f72e05d87c", - "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c", - "shasum": "" - }, - "require": { - "php": ">=5.4.0", - "psr/http-message": "~1.0" - }, - "provide": { - "psr/http-message-implementation": "1.0" - }, - "require-dev": { - "phpunit/phpunit": "~4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - } - }, - "autoload": { - "psr-4": { - "GuzzleHttp\\Psr7\\": "src/" - }, - "files": [ - "src/functions_include.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - }, - { - "name": "Tobias Schultze", - "homepage": "https://github.com/Tobion" - } - ], - "description": "PSR-7 message implementation that also provides common utility methods", - "keywords": [ - "http", - "message", - "request", - "response", - "stream", - "uri", - "url" - ], - "time": "2017-03-20T17:10:46+00:00" - }, - { - "name": "league/container", - "version": "2.4.1", - "source": { - "type": "git", - "url": "https://github.com/thephpleague/container.git", - "reference": "43f35abd03a12977a60ffd7095efd6a7808488c0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thephpleague/container/zipball/43f35abd03a12977a60ffd7095efd6a7808488c0", - "reference": "43f35abd03a12977a60ffd7095efd6a7808488c0", - "shasum": "" - }, - "require": { - "container-interop/container-interop": "^1.2", - "php": "^5.4.0 || ^7.0" - }, - "provide": { - "container-interop/container-interop-implementation": "^1.2", - "psr/container-implementation": "^1.0" - }, - "replace": { - "orno/di": "~2.0" - }, - "require-dev": { - "phpunit/phpunit": "4.*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-2.x": "2.x-dev", - "dev-1.x": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "League\\Container\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Phil Bennett", - "email": "philipobenito@gmail.com", - "homepage": "http://www.philipobenito.com", - "role": "Developer" - } - ], - "description": "A fast and intuitive dependency injection container.", - "homepage": "https://github.com/thephpleague/container", - "keywords": [ - "container", - "dependency", - "di", - "injection", - "league", - "provider", - "service" - ], - "time": "2017-05-10T09:20:27+00:00" - }, - { - "name": "myclabs/deep-copy", - "version": "1.8.1", - "source": { - "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8", - "reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8", - "shasum": "" - }, - "require": { - "php": "^7.1" - }, - "replace": { - "myclabs/deep-copy": "self.version" - }, - "require-dev": { - "doctrine/collections": "^1.0", - "doctrine/common": "^2.6", - "phpunit/phpunit": "^7.1" - }, - "type": "library", - "autoload": { - "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - }, - "files": [ - "src/DeepCopy/deep_copy.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Create deep copies (clones) of your objects", - "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" - ], - "time": "2018-06-11T23:09:50+00:00" - }, - { - "name": "phar-io/manifest", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/phar-io/manifest.git", - "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/2df402786ab5368a0169091f61a7c1e0eb6852d0", - "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-phar": "*", - "phar-io/version": "^1.0.1", - "php": "^5.6 || ^7.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" - } - ], - "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", - "time": "2017-03-05T18:14:27+00:00" - }, - { - "name": "phar-io/version", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/phar-io/version.git", - "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/a70c0ced4be299a63d32fa96d9281d03e94041df", - "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df", - "shasum": "" - }, - "require": { - "php": "^5.6 || ^7.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" - } - ], - "description": "Library for handling version information and constraints", - "time": "2017-03-05T17:38:23+00:00" - }, - { - "name": "phpdocumentor/reflection-common", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", - "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", - "shasum": "" - }, - "require": { - "php": ">=5.5" - }, - "require-dev": { - "phpunit/phpunit": "^4.6" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } - ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", - "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" - ], - "time": "2017-09-11T18:02:19+00:00" - }, - { - "name": "phpdocumentor/reflection-docblock", - "version": "4.3.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "94fd0001232e47129dd3504189fa1c7225010d08" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94fd0001232e47129dd3504189fa1c7225010d08", - "reference": "94fd0001232e47129dd3504189fa1c7225010d08", - "shasum": "" - }, - "require": { - "php": "^7.0", - "phpdocumentor/reflection-common": "^1.0.0", - "phpdocumentor/type-resolver": "^0.4.0", - "webmozart/assert": "^1.0" - }, - "require-dev": { - "doctrine/instantiator": "~1.0.5", - "mockery/mockery": "^1.0", - "phpunit/phpunit": "^6.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2017-11-30T07:14:17+00:00" - }, - { - "name": "phpdocumentor/type-resolver", - "version": "0.4.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", - "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", - "shasum": "" - }, - "require": { - "php": "^5.5 || ^7.0", - "phpdocumentor/reflection-common": "^1.0" - }, - "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^5.2||^4.8.24" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "time": "2017-07-14T14:27:02+00:00" - }, - { - "name": "phpspec/prophecy", - "version": "1.7.6", - "source": { - "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/33a7e3c4fda54e912ff6338c48823bd5c0f0b712", - "reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.0.2", - "php": "^5.3|^7.0", - "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", - "sebastian/comparator": "^1.1|^2.0|^3.0", - "sebastian/recursion-context": "^1.0|^2.0|^3.0" - }, - "require-dev": { - "phpspec/phpspec": "^2.5|^3.2", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.7.x-dev" - } - }, - "autoload": { - "psr-0": { - "Prophecy\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - }, - { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" - } - ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "https://github.com/phpspec/prophecy", - "keywords": [ - "Double", - "Dummy", - "fake", - "mock", - "spy", - "stub" - ], - "time": "2018-04-18T13:57:24+00:00" - }, - { - "name": "phpunit/php-code-coverage", - "version": "5.3.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "c89677919c5dd6d3b3852f230a663118762218ac" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c89677919c5dd6d3b3852f230a663118762218ac", - "reference": "c89677919c5dd6d3b3852f230a663118762218ac", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-xmlwriter": "*", - "php": "^7.0", - "phpunit/php-file-iterator": "^1.4.2", - "phpunit/php-text-template": "^1.2.1", - "phpunit/php-token-stream": "^2.0.1", - "sebastian/code-unit-reverse-lookup": "^1.0.1", - "sebastian/environment": "^3.0", - "sebastian/version": "^2.0.1", - "theseer/tokenizer": "^1.1" - }, - "require-dev": { - "phpunit/phpunit": "^6.0" - }, - "suggest": { - "ext-xdebug": "^2.5.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.3.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", - "homepage": "https://github.com/sebastianbergmann/php-code-coverage", - "keywords": [ - "coverage", - "testing", - "xunit" - ], - "time": "2018-04-06T15:36:58+00:00" - }, - { - "name": "phpunit/php-file-iterator", - "version": "1.4.5", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4", - "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "FilterIterator implementation that filters files based on a list of suffixes.", - "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", - "keywords": [ - "filesystem", - "iterator" - ], - "time": "2017-11-27T13:52:08+00:00" - }, - { - "name": "phpunit/php-text-template", - "version": "1.2.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Simple template engine.", - "homepage": "https://github.com/sebastianbergmann/php-text-template/", - "keywords": [ - "template" - ], - "time": "2015-06-21T13:50:34+00:00" - }, - { - "name": "phpunit/php-timer", - "version": "1.0.9", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", - "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", - "shasum": "" - }, - "require": { - "php": "^5.3.3 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "Utility class for timing", - "homepage": "https://github.com/sebastianbergmann/php-timer/", - "keywords": [ - "timer" - ], - "time": "2017-02-26T11:10:40+00:00" - }, - { - "name": "phpunit/php-token-stream", - "version": "2.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "791198a2c6254db10131eecfe8c06670700904db" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/791198a2c6254db10131eecfe8c06670700904db", - "reference": "791198a2c6254db10131eecfe8c06670700904db", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": "^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.2.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Wrapper around PHP's tokenizer extension.", - "homepage": "https://github.com/sebastianbergmann/php-token-stream/", - "keywords": [ - "tokenizer" - ], - "time": "2017-11-27T05:48:46+00:00" - }, - { - "name": "phpunit/phpunit", - "version": "6.5.9", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "093ca5508174cd8ab8efe44fd1dde447adfdec8f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/093ca5508174cd8ab8efe44fd1dde447adfdec8f", - "reference": "093ca5508174cd8ab8efe44fd1dde447adfdec8f", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-json": "*", - "ext-libxml": "*", - "ext-mbstring": "*", - "ext-xml": "*", - "myclabs/deep-copy": "^1.6.1", - "phar-io/manifest": "^1.0.1", - "phar-io/version": "^1.0", - "php": "^7.0", - "phpspec/prophecy": "^1.7", - "phpunit/php-code-coverage": "^5.3", - "phpunit/php-file-iterator": "^1.4.3", - "phpunit/php-text-template": "^1.2.1", - "phpunit/php-timer": "^1.0.9", - "phpunit/phpunit-mock-objects": "^5.0.5", - "sebastian/comparator": "^2.1", - "sebastian/diff": "^2.0", - "sebastian/environment": "^3.1", - "sebastian/exporter": "^3.1", - "sebastian/global-state": "^2.0", - "sebastian/object-enumerator": "^3.0.3", - "sebastian/resource-operations": "^1.0", - "sebastian/version": "^2.0.1" - }, - "conflict": { - "phpdocumentor/reflection-docblock": "3.0.2", - "phpunit/dbunit": "<3.0" - }, - "require-dev": { - "ext-pdo": "*" - }, - "suggest": { - "ext-xdebug": "*", - "phpunit/php-invoker": "^1.1" - }, - "bin": [ - "phpunit" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "6.5.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "The PHP Unit Testing framework.", - "homepage": "https://phpunit.de/", - "keywords": [ - "phpunit", - "testing", - "xunit" - ], - "time": "2018-07-03T06:40:40+00:00" - }, - { - "name": "phpunit/phpunit-mock-objects", - "version": "5.0.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "6f9a3c8bf34188a2b53ce2ae7a126089c53e0a9f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/6f9a3c8bf34188a2b53ce2ae7a126089c53e0a9f", - "reference": "6f9a3c8bf34188a2b53ce2ae7a126089c53e0a9f", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.0.5", - "php": "^7.0", - "phpunit/php-text-template": "^1.2.1", - "sebastian/exporter": "^3.1" - }, - "conflict": { - "phpunit/phpunit": "<6.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.5" - }, - "suggest": { - "ext-soap": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Mock Object library for PHPUnit", - "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", - "keywords": [ - "mock", - "xunit" - ], - "time": "2018-07-13T03:27:23+00:00" - }, - { - "name": "psr/container", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", - "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Container\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", - "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" - ], - "time": "2017-02-14T16:28:37+00:00" - }, - { - "name": "psr/http-message", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-message.git", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP messages", - "homepage": "https://github.com/php-fig/http-message", - "keywords": [ - "http", - "http-message", - "psr", - "psr-7", - "request", - "response" - ], - "time": "2016-08-06T14:39:51+00:00" - }, - { - "name": "psr/log", - "version": "1.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", - "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Log\\": "Psr/Log/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", - "keywords": [ - "log", - "psr", - "psr-3" - ], - "time": "2016-10-10T12:19:37+00:00" - }, - { - "name": "sebastian/code-unit-reverse-lookup", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", - "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", - "shasum": "" - }, - "require": { - "php": "^5.6 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^5.7 || ^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Looks up which function or method a line of code belongs to", - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "time": "2017-03-04T06:30:41+00:00" - }, - { - "name": "sebastian/comparator", - "version": "2.1.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/34369daee48eafb2651bea869b4b15d75ccc35f9", - "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9", - "shasum": "" - }, - "require": { - "php": "^7.0", - "sebastian/diff": "^2.0 || ^3.0", - "sebastian/exporter": "^3.1" - }, - "require-dev": { - "phpunit/phpunit": "^6.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.1.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides the functionality to compare PHP values for equality", - "homepage": "https://github.com/sebastianbergmann/comparator", - "keywords": [ - "comparator", - "compare", - "equality" - ], - "time": "2018-02-01T13:46:46+00:00" - }, - { - "name": "sebastian/diff", - "version": "2.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", - "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", - "shasum": "" - }, - "require": { - "php": "^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", - "keywords": [ - "diff" - ], - "time": "2017-08-03T08:09:46+00:00" - }, - { - "name": "sebastian/environment", - "version": "3.1.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/cd0871b3975fb7fc44d11314fd1ee20925fce4f5", - "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5", - "shasum": "" - }, - "require": { - "php": "^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.1.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", - "keywords": [ - "Xdebug", - "environment", - "hhvm" - ], - "time": "2017-07-01T08:51:00+00:00" - }, - { - "name": "sebastian/exporter", - "version": "3.1.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "234199f4528de6d12aaa58b612e98f7d36adb937" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/234199f4528de6d12aaa58b612e98f7d36adb937", - "reference": "234199f4528de6d12aaa58b612e98f7d36adb937", - "shasum": "" - }, - "require": { - "php": "^7.0", - "sebastian/recursion-context": "^3.0" - }, - "require-dev": { - "ext-mbstring": "*", - "phpunit/phpunit": "^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.1.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - } - ], - "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "http://www.github.com/sebastianbergmann/exporter", - "keywords": [ - "export", - "exporter" - ], - "time": "2017-04-03T13:19:02+00:00" - }, - { - "name": "sebastian/global-state", - "version": "2.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", - "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", - "shasum": "" - }, - "require": { - "php": "^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.0" - }, - "suggest": { - "ext-uopz": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", - "keywords": [ - "global state" - ], - "time": "2017-04-27T15:39:26+00:00" - }, - { - "name": "sebastian/object-enumerator", - "version": "3.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5", - "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5", - "shasum": "" - }, - "require": { - "php": "^7.0", - "sebastian/object-reflector": "^1.1.1", - "sebastian/recursion-context": "^3.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Traverses array structures and object graphs to enumerate all referenced objects", - "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "time": "2017-08-03T12:35:26+00:00" - }, - { - "name": "sebastian/object-reflector", - "version": "1.1.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "773f97c67f28de00d397be301821b06708fca0be" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be", - "reference": "773f97c67f28de00d397be301821b06708fca0be", - "shasum": "" - }, - "require": { - "php": "^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Allows reflection of object attributes, including inherited and non-public ones", - "homepage": "https://github.com/sebastianbergmann/object-reflector/", - "time": "2017-03-29T09:07:27+00:00" - }, - { - "name": "sebastian/recursion-context", - "version": "3.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", - "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", - "shasum": "" - }, - "require": { - "php": "^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - } - ], - "description": "Provides functionality to recursively process PHP variables", - "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "time": "2017-03-03T06:23:57+00:00" - }, - { - "name": "sebastian/resource-operations", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", - "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", - "shasum": "" - }, - "require": { - "php": ">=5.6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides a list of PHP built-in functions that operate on resources", - "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "time": "2015-07-28T20:34:47+00:00" - }, - { - "name": "sebastian/version", - "version": "2.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", - "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", - "shasum": "" - }, - "require": { - "php": ">=5.6" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", - "time": "2016-10-03T07:35:21+00:00" - }, - { - "name": "symfony/browser-kit", - "version": "v4.1.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/browser-kit.git", - "reference": "ff9ac5d5808a530b2e7f6abcf3a2412d4f9bcd62" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/ff9ac5d5808a530b2e7f6abcf3a2412d4f9bcd62", - "reference": "ff9ac5d5808a530b2e7f6abcf3a2412d4f9bcd62", - "shasum": "" - }, - "require": { - "php": "^7.1.3", - "symfony/dom-crawler": "~3.4|~4.0" - }, - "require-dev": { - "symfony/css-selector": "~3.4|~4.0", - "symfony/process": "~3.4|~4.0" - }, - "suggest": { - "symfony/process": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.1-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\BrowserKit\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony BrowserKit Component", - "homepage": "https://symfony.com", - "time": "2018-06-04T17:31:56+00:00" - }, - { - "name": "symfony/console", - "version": "v4.1.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "5c31f6a97c1c240707f6d786e7e59bfacdbc0219" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/5c31f6a97c1c240707f6d786e7e59bfacdbc0219", - "reference": "5c31f6a97c1c240707f6d786e7e59bfacdbc0219", - "shasum": "" - }, - "require": { - "php": "^7.1.3", - "symfony/polyfill-mbstring": "~1.0" - }, - "conflict": { - "symfony/dependency-injection": "<3.4", - "symfony/process": "<3.3" - }, - "require-dev": { - "psr/log": "~1.0", - "symfony/config": "~3.4|~4.0", - "symfony/dependency-injection": "~3.4|~4.0", - "symfony/event-dispatcher": "~3.4|~4.0", - "symfony/lock": "~3.4|~4.0", - "symfony/process": "~3.4|~4.0" - }, - "suggest": { - "psr/log-implementation": "For using the console logger", - "symfony/event-dispatcher": "", - "symfony/lock": "", - "symfony/process": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.1-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Console\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Console Component", - "homepage": "https://symfony.com", - "time": "2018-07-16T14:05:40+00:00" - }, - { - "name": "symfony/css-selector", - "version": "v4.1.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/css-selector.git", - "reference": "03ac71606ecb0b0ce792faa17d74cc32c2949ef4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/03ac71606ecb0b0ce792faa17d74cc32c2949ef4", - "reference": "03ac71606ecb0b0ce792faa17d74cc32c2949ef4", - "shasum": "" - }, - "require": { - "php": "^7.1.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.1-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\CssSelector\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jean-François Simon", - "email": "jeanfrancois.simon@sensiolabs.com" - }, - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony CssSelector Component", - "homepage": "https://symfony.com", - "time": "2018-05-30T07:26:09+00:00" - }, - { - "name": "symfony/dom-crawler", - "version": "v4.1.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/dom-crawler.git", - "reference": "eb501fa8aab8c8e2db790d8d0f945697769f6c41" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/eb501fa8aab8c8e2db790d8d0f945697769f6c41", - "reference": "eb501fa8aab8c8e2db790d8d0f945697769f6c41", - "shasum": "" - }, - "require": { - "php": "^7.1.3", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-mbstring": "~1.0" - }, - "require-dev": { - "symfony/css-selector": "~3.4|~4.0" - }, - "suggest": { - "symfony/css-selector": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.1-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\DomCrawler\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony DomCrawler Component", - "homepage": "https://symfony.com", - "time": "2018-07-05T11:54:23+00:00" - }, - { - "name": "symfony/event-dispatcher", - "version": "v4.1.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "00d64638e4f0703a00ab7fc2c8ae5f75f3b4020f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/00d64638e4f0703a00ab7fc2c8ae5f75f3b4020f", - "reference": "00d64638e4f0703a00ab7fc2c8ae5f75f3b4020f", - "shasum": "" - }, - "require": { - "php": "^7.1.3" - }, - "conflict": { - "symfony/dependency-injection": "<3.4" - }, - "require-dev": { - "psr/log": "~1.0", - "symfony/config": "~3.4|~4.0", - "symfony/dependency-injection": "~3.4|~4.0", - "symfony/expression-language": "~3.4|~4.0", - "symfony/stopwatch": "~3.4|~4.0" - }, - "suggest": { - "symfony/dependency-injection": "", - "symfony/http-kernel": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.1-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\EventDispatcher\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony EventDispatcher Component", - "homepage": "https://symfony.com", - "time": "2018-07-10T11:02:47+00:00" - }, - { - "name": "symfony/filesystem", - "version": "v4.1.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/filesystem.git", - "reference": "562bf7005b55fd80d26b582d28e3e10f2dd5ae9c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/562bf7005b55fd80d26b582d28e3e10f2dd5ae9c", - "reference": "562bf7005b55fd80d26b582d28e3e10f2dd5ae9c", - "shasum": "" - }, - "require": { - "php": "^7.1.3", - "symfony/polyfill-ctype": "~1.8" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.1-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Filesystem\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Filesystem Component", - "homepage": "https://symfony.com", - "time": "2018-05-30T07:26:09+00:00" - }, - { - "name": "symfony/finder", - "version": "v4.1.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/finder.git", - "reference": "84714b8417d19e4ba02ea78a41a975b3efaafddb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/84714b8417d19e4ba02ea78a41a975b3efaafddb", - "reference": "84714b8417d19e4ba02ea78a41a975b3efaafddb", - "shasum": "" - }, - "require": { - "php": "^7.1.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.1-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Finder\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Finder Component", - "homepage": "https://symfony.com", - "time": "2018-06-19T21:38:16+00:00" - }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.8.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/7cc359f1b7b80fc25ed7796be7d96adc9b354bae", - "reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.8-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - }, - { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - } - ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], - "time": "2018-04-30T19:57:29+00:00" - }, - { - "name": "symfony/polyfill-mbstring", - "version": "v1.8.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "3296adf6a6454a050679cde90f95350ad604b171" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/3296adf6a6454a050679cde90f95350ad604b171", - "reference": "3296adf6a6454a050679cde90f95350ad604b171", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "suggest": { - "ext-mbstring": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.8-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for the Mbstring extension", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], - "time": "2018-04-26T10:06:28+00:00" - }, - { - "name": "symfony/process", - "version": "v4.1.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "1d1677391ecf00d1c5b9482d6050c0c27aa3ac3a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/1d1677391ecf00d1c5b9482d6050c0c27aa3ac3a", - "reference": "1d1677391ecf00d1c5b9482d6050c0c27aa3ac3a", - "shasum": "" - }, - "require": { - "php": "^7.1.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.1-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Process\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Process Component", - "homepage": "https://symfony.com", - "time": "2018-05-31T10:17:53+00:00" - }, - { - "name": "symfony/yaml", - "version": "v4.1.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/yaml.git", - "reference": "80e4bfa9685fc4a09acc4a857ec16974a9cd944e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/80e4bfa9685fc4a09acc4a857ec16974a9cd944e", - "reference": "80e4bfa9685fc4a09acc4a857ec16974a9cd944e", - "shasum": "" - }, - "require": { - "php": "^7.1.3", - "symfony/polyfill-ctype": "~1.8" - }, - "conflict": { - "symfony/console": "<3.4" - }, - "require-dev": { - "symfony/console": "~3.4|~4.0" - }, - "suggest": { - "symfony/console": "For validating YAML files using the lint command" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.1-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Yaml\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Yaml Component", - "homepage": "https://symfony.com", - "time": "2018-05-30T07:26:09+00:00" - }, - { - "name": "theseer/tokenizer", - "version": "1.1.0", - "source": { - "type": "git", - "url": "https://github.com/theseer/tokenizer.git", - "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/cb2f008f3f05af2893a87208fe6a6c4985483f8b", - "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": "^7.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - } - ], - "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", - "time": "2017-04-07T12:08:54+00:00" - }, - { - "name": "vlucas/phpdotenv", - "version": "v2.5.0", - "source": { - "type": "git", - "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "6ae3e2e6494bb5e58c2decadafc3de7f1453f70a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/6ae3e2e6494bb5e58c2decadafc3de7f1453f70a", - "reference": "6ae3e2e6494bb5e58c2decadafc3de7f1453f70a", - "shasum": "" - }, - "require": { - "php": ">=5.3.9" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.5-dev" - } - }, - "autoload": { - "psr-4": { - "Dotenv\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Vance Lucas", - "email": "vance@vancelucas.com", - "homepage": "http://www.vancelucas.com" - } - ], - "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", - "keywords": [ - "dotenv", - "env", - "environment" - ], - "time": "2018-07-01T10:25:50+00:00" - }, - { - "name": "webmozart/assert", - "version": "1.3.0", - "source": { - "type": "git", - "url": "https://github.com/webmozart/assert.git", - "reference": "0df1908962e7a3071564e857d86874dad1ef204a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/0df1908962e7a3071564e857d86874dad1ef204a", - "reference": "0df1908962e7a3071564e857d86874dad1ef204a", - "shasum": "" - }, - "require": { - "php": "^5.3.3 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.6", - "sebastian/version": "^1.0.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "time": "2018-01-29T19:49:41+00:00" - } - ], - "packages-dev": [], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], - "prefer-stable": true, - "prefer-lowest": false, - "platform": { - "php": "~7.1.3||~7.2.0" - }, - "platform-dev": [] -} diff --git a/dev/tests/acceptance/tests/_data/catalog_import_products.csv b/dev/tests/acceptance/tests/_data/catalog_import_products.csv new file mode 100644 index 000000000000..7732f15d4ce3 --- /dev/null +++ b/dev/tests/acceptance/tests/_data/catalog_import_products.csv @@ -0,0 +1,4 @@ +sku,store_view_code,attribute_set_code,product_type,categories,product_websites,name,description,short_description,weight,product_online,tax_class_name,visibility,price,special_price,special_price_from_date,special_price_to_date,url_key,meta_title,meta_keywords,meta_description,base_image,base_image_label,small_image,small_image_label,thumbnail_image,thumbnail_image_label,swatch_image,swatch_image_label,created_at,updated_at,new_from_date,new_to_date,display_product_options_in,map_price,msrp_price,map_enabled,gift_message_available,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_display_actual_price_type,country_of_manufacture,additional_attributes,qty,out_of_stock_qty,use_config_min_qty,is_qty_decimal,allow_backorders,use_config_backorders,min_cart_qty,use_config_min_sale_qty,max_cart_qty,use_config_max_sale_qty,is_in_stock,notify_on_stock_below,use_config_notify_stock_qty,manage_stock,use_config_manage_stock,use_config_qty_increments,qty_increments,use_config_enable_qty_inc,enable_qty_increments,is_decimal_divided,website_id,related_skus,related_position,crosssell_skus,crosssell_position,upsell_skus,upsell_position,additional_images,additional_image_labels,hide_from_product_page,custom_options,bundle_price_type,bundle_sku_type,bundle_price_view,bundle_weight_type,bundle_values,bundle_shipment_type,configurable_variations,configurable_variation_labels,associated_skus +SimpleProductForTest1,,Default,simple,"Default","base,second_website",SimpleProductAfterImport1,,,1.0000,1,"Taxable Goods","Catalog, Search",250.0000,,,,simple-product-for-test-1,,,,,,,,,,,,"3/4/19, 5:53 AM","3/4/19, 4:47 PM",,,"Block after Info Column",,,,,,,,,,,"Use config",,,100.0000,0.0000,1,0,0,1,1.0000,1,0.0000,1,1,,1,0,1,1,0.0000,1,0,0,0,,,,,,,,,,,,,,,,,,, +SimpleProductForTest2,,Default,simple,"Default",base,SimpleProductAfterImport2,,,1.0000,1,"Taxable Goods","Catalog, Search",300.0000,,,,simple-product-for-test-2,,,,,,,,,,,,"3/4/19, 5:53 AM","3/4/19, 4:47 PM",,,"Block after Info Column",,,,,,,,,,,"Use config",,,100.0000,0.0000,1,0,0,1,1.0000,1,0.0000,1,1,,1,0,1,1,0.0000,1,0,0,0,,,,,,,,,,,,,,,,,,, +SimpleProductForTest3,,Default,simple,"Default","base,second_website",SimpleProductAfterImport3,,,1.0000,1,"Taxable Goods","Catalog, Search",350.0000,,,,simple-product-for-test-3,,,,,,,,,,,,"3/4/19, 5:53 AM","3/4/19, 4:47 PM",,,"Block after Info Column",,,,,,,,,,,"Use config",,,100.0000,0.0000,1,0,0,1,1.0000,1,0.0000,1,1,,1,0,1,1,0.0000,1,0,0,0,,,,,,,,,,,,,,,,,,, \ No newline at end of file diff --git a/dev/tests/acceptance/tests/_data/catalog_products.csv b/dev/tests/acceptance/tests/_data/catalog_products.csv new file mode 100644 index 000000000000..3b580172d6a0 --- /dev/null +++ b/dev/tests/acceptance/tests/_data/catalog_products.csv @@ -0,0 +1,4 @@ +sku,store_view_code,attribute_set_code,product_type,categories,product_websites,name,description,short_description,weight,product_online,tax_class_name,visibility,price,special_price,special_price_from_date,special_price_to_date,url_key,meta_title,meta_keywords,meta_description,base_image,base_image_label,small_image,small_image_label,thumbnail_image,thumbnail_image_label,swatch_image,swatch_image_label,created_at,updated_at,new_from_date,new_to_date,display_product_options_in,map_price,msrp_price,map_enabled,gift_message_available,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_display_actual_price_type,country_of_manufacture,additional_attributes,qty,out_of_stock_qty,use_config_min_qty,is_qty_decimal,allow_backorders,use_config_backorders,min_cart_qty,use_config_min_sale_qty,max_cart_qty,use_config_max_sale_qty,is_in_stock,notify_on_stock_below,use_config_notify_stock_qty,manage_stock,use_config_manage_stock,use_config_qty_increments,qty_increments,use_config_enable_qty_inc,enable_qty_increments,is_decimal_divided,website_id,deferred_stock_update,use_config_deferred_stock_update,related_skus,related_position,crosssell_skus,crosssell_position,upsell_skus,upsell_position,additional_images,additional_image_labels,hide_from_product_page,custom_options,bundle_price_type,bundle_sku_type,bundle_price_view,bundle_weight_type,bundle_values,bundle_shipment_type,configurable_variations,configurable_variation_labels,associated_skus +"Simple Product for Test",,Default,simple,,base,"Simple Product for Test",,,,1,"Taxable Goods","Catalog, Search",123.0000,,,,simple-product-for-test,,,,,,,,,,,,"12/18/18, 7:50 AM","12/18/18, 7:50 AM",,,"Block after Info Column",,,,,,,,,,,,,,1000.0000,0.0000,1,0,0,1,1.0000,1,0.0000,1,1,,1,0,1,1,0.0000,1,0,0,0,0,1,,,,,,,,,,,,,,,,,,, +"Virtual Product for Test",,Default,virtual,,base,"Virtual Product for Test",,,0.0000,1,"Taxable Goods","Catalog, Search",99.9900,,,,virtual-product-for-test,,,,,,,,,,,,"12/18/18, 7:51 AM","12/18/18, 7:51 AM",,,"Block after Info Column",,,,,,,,,,,,,,1000.0000,0.0000,1,0,0,1,1.0000,1,0.0000,1,1,,1,0,1,1,0.0000,1,0,0,0,0,1,,,,,,,,,,,,,,,,,,, +"Api Downloadable Product for Test",,Default,downloadable,,base,"Api Downloadable Product for Test","API Product Description5c18fb47982621","API Product Short Description5c18fb47982e21",,1,"Taxable Goods","Catalog, Search",123.0000,,,,api-downloadable-product-for-test,,,,,,,,,,,,"12/18/18, 7:51 AM","12/18/18, 7:51 AM",,,"Block after Info Column",,,,,,,,,,,,,,1000.0000,0.0000,1,0,0,1,1.0000,1,0.0000,1,1,,1,0,1,1,0.0000,1,0,0,0,0,1,,,,,,,,,,,,,,,,,,, diff --git a/dev/tests/acceptance/tests/_data/import_updated.csv b/dev/tests/acceptance/tests/_data/import_updated.csv new file mode 100644 index 000000000000..b517150eec84 --- /dev/null +++ b/dev/tests/acceptance/tests/_data/import_updated.csv @@ -0,0 +1,4 @@ +product_websites,store_view_code,attribute_set_code,product_type,categories,sku,price,name,url_key +base,,Default,simple,Default Category/category-admin,productformagetwo68980,123,productformagetwo68980,productformagetwo68980 +,en,Default,simple,,productformagetwo68980,,productformagetwo68980-english,productformagetwo68980-english +,nl,Default,simple,,productformagetwo68980,,productformagetwo68980-dutch,productformagetwo68980-dutch diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProductCatalogSearch/Test/EndToEndB2CGuestUserTest.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProductCatalogSearch/Test/EndToEndB2CGuestUserTest.xml index f5cd41bda74d..f0bfec543f28 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProductCatalogSearch/Test/EndToEndB2CGuestUserTest.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProductCatalogSearch/Test/EndToEndB2CGuestUserTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="EndToEndB2CGuestUserTest"> <!-- Search configurable product --> <comment userInput="Search configurable product" stepKey="commentSearchConfigurableProduct" after="searchAssertSimpleProduct2ImageNotDefault" /> @@ -26,5 +26,5 @@ <!-- @TODO: Move Image check to action group after MQE-697 is fixed --> <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.productImage}}" userInput="src" stepKey="searchGrabConfigProductPageImageSrc" after="searchAssertConfigProductPage"/> <assertNotRegExp expected="'/placeholder\/image\.jpg/'" actual="$searchGrabConfigProductPageImageSrc" stepKey="searchAssertConfigProductPageImageNotDefault" after="searchGrabConfigProductPageImageSrc"/> - </test> + </test> </tests> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProductCatalogSearch/Test/EndToEndB2CLoggedInUserTest.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProductCatalogSearch/Test/EndToEndB2CLoggedInUserTest.xml index 3e386a034eec..9fe70c8b4dd3 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProductCatalogSearch/Test/EndToEndB2CLoggedInUserTest.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProductCatalogSearch/Test/EndToEndB2CLoggedInUserTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="EndToEndB2CLoggedInUserTest"> <!-- Search configurable product --> <comment userInput="Search configurable product" stepKey="commentSearchConfigurableProduct" after="searchAssertSimpleProduct2ImageNotDefault" /> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProductWishlist/Test/EndToEndB2CLoggedInUserTest.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProductWishlist/Test/EndToEndB2CLoggedInUserTest.xml index d3b009eecf87..cb3d9edbc1cb 100644 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProductWishlist/Test/EndToEndB2CLoggedInUserTest.xml +++ b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/ConfigurableProductWishlist/Test/EndToEndB2CLoggedInUserTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="EndToEndB2CLoggedInUserTest"> <!-- Step 5: Add products to wishlist --> <!-- Add Configurable Product to wishlist --> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Framework/README.md b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Framework/README.md deleted file mode 100644 index 7fbe454f709c..000000000000 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/Framework/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Magento 2 Functional Tests - -The Functional Tests Module for **Magento_Framework**. diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTemplates/ActionGroup/TemplateActionGroupFile.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTemplates/ActionGroup/TemplateActionGroupFile.xml deleted file mode 100644 index d1bc251f2632..000000000000 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTemplates/ActionGroup/TemplateActionGroupFile.xml +++ /dev/null @@ -1,14 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> - <actionGroup name=""> - <!-- ADD TEST STEPS HERE --> - </actionGroup> -</actionGroups> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTemplates/Data/TemplateDataFile.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTemplates/Data/TemplateDataFile.xml deleted file mode 100644 index 6f13f04aef31..000000000000 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTemplates/Data/TemplateDataFile.xml +++ /dev/null @@ -1,14 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> - <entity name="" type=""> - <data key=""></data> - </entity> -</entities> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTemplates/Metadata/TemplateMetaFile.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTemplates/Metadata/TemplateMetaFile.xml deleted file mode 100644 index 7293cea9a9b8..000000000000 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTemplates/Metadata/TemplateMetaFile.xml +++ /dev/null @@ -1,22 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> - <operation name="" dataType="" type="" auth="adminOauth" url="" method=""> - <contentType>application/json</contentType> - <param key=""></param> - <object dataType="" key=""> - <field key=""></field> - <array key=""> - <value></value> - </array> - </object> - <field key=""></field> - </operation> -</operations> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTemplates/Page/TemplatePageFile.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTemplates/Page/TemplatePageFile.xml deleted file mode 100644 index 27559a48bef0..000000000000 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTemplates/Page/TemplatePageFile.xml +++ /dev/null @@ -1,14 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> - <page name="" url="" area="" module=""> - <section name=""/> - </page> -</pages> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTemplates/README.md b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTemplates/README.md deleted file mode 100644 index 986c2af6164e..000000000000 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTemplates/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Magento 2 Functional Tests - -The Functional Tests Module for **Magento_SampleTemplates** Module. diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTemplates/Section/TemplateSectionFile.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTemplates/Section/TemplateSectionFile.xml deleted file mode 100644 index 4c1cd7300cf2..000000000000 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTemplates/Section/TemplateSectionFile.xml +++ /dev/null @@ -1,14 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> - <section name=""> - <element name="" type="" selector=""/> - </section> -</sections> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTemplates/Test/TemplateTestFile.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTemplates/Test/TemplateTestFile.xml deleted file mode 100644 index d9a98d4baa6c..000000000000 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTemplates/Test/TemplateTestFile.xml +++ /dev/null @@ -1,24 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> - <test name="TemplateTestFile"> - <annotations> - <features value=""/> - <stories value=""/> - </annotations> - <before> - - </before> - <after> - - </after> - <!-- ADD TEST STEPS HERE --> - </test> -</tests> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/ActionGroup/SampleActionGroup.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/ActionGroup/SampleActionGroup.xml deleted file mode 100644 index 45ec5f3adf21..000000000000 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/ActionGroup/SampleActionGroup.xml +++ /dev/null @@ -1,24 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> - <actionGroup name="SampleActionGroup"> - <arguments> - <argument name="person" defaultValue="SamplePerson"/> - </arguments> - <fillField selector="#foo" userInput="{{person.foo}}" stepKey="fillField1"/> - <fillField selector="#bar" userInput="{{person.bar}}" stepKey="fillField2"/> - </actionGroup> - <actionGroup name="ValidateSlideOutPanelField"> - <arguments> - <argument name="property" defaultValue=""/> - </arguments> - <see userInput="{{property.name}}" selector="{{ColumnSection.panelFieldLabel(property.section, property.fieldName, property.section, property.name)}}" stepKey="seePropertyLabel"/> - </actionGroup> -</actionGroups> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Data/SampleData.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Data/SampleData.xml deleted file mode 100644 index 361f06a6ff33..000000000000 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Data/SampleData.xml +++ /dev/null @@ -1,32 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> - -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> - <entity name="SamplePerson" type="samplePerson"> - <data key="foo">foo</data> - <data key="bar">bar</data> - <data key="lastName">Doe</data> - <data key="email" unique="prefix">.email@gmail.com</data> - </entity> - <entity name="OverrideDefaultPerson" type="samplePerson"> - <data key="foo">fizz</data> - <data key="bar">buzz</data> - </entity> - <entity name="AppearanceMinHeightProperty" type="min_height_property"> - <data key="name">Minimum Height</data> - <data key="section">appearance</data> - <data key="fieldName">min_height</data> - </entity> - <entity name="AssertThis" type="samplePerson"> - <data key="firstname">Well</data> - <data key="lastname">Done</data> - <data key="email" unique="prefix">.email@gmail.com</data> - </entity> -</entities> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Page/SamplePage.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Page/SamplePage.xml deleted file mode 100644 index 6fd2a54c6014..000000000000 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Page/SamplePage.xml +++ /dev/null @@ -1,14 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> - <page name="SamplePage" url="/{{var1}}/{{var2}}.html" area="storefront" module="SampleTests" parameterized="true"> - <section name="SampleSection"/> - </page> -</pages> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/README.md b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/README.md deleted file mode 100644 index f3340b205f1a..000000000000 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Magento 2 Functional Tests - -The Functional Tests Module for **Magento_SampleTests** Module. diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Section/SampleSection.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Section/SampleSection.xml deleted file mode 100644 index 6aa54d2fb9b8..000000000000 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Section/SampleSection.xml +++ /dev/null @@ -1,20 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> - <section name="SampleSection"> - <element name="oneParamElement" type="button" selector="#element .{{var1}}" parameterized="true"/> - <element name="twoParamElement" type="button" selector="#{{var1}} .{{var2}}" parameterized="true"/> - <element name="threeParamElement" type="button" selector="#{{var1}}-{{var2}} .{{var3}}" parameterized="true"/> - <element name="timeoutElement" type="button" selector="#foo" timeout="30"/> - </section> - <section name="ColumnSection"> - <element name="panelFieldLabel" type="text" selector='//div[@data-index="{{arg1}}"]/descendant::div[@data-index="{{arg2}}"]/label | //div[@data-index="{{arg3}}"]/descendant::*[@class="admin__field-label"]/span[text()="{{arg4}}"]' parameterized="true" /> - </section> -</sections> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Test/AdvancedSampleTest.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Test/AdvancedSampleTest.xml deleted file mode 100644 index 4fead0e51497..000000000000 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Test/AdvancedSampleTest.xml +++ /dev/null @@ -1,54 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> - -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> - <test name="AdvancedSampleTest"> - <annotations> - <features value="SampleTests"/> - <title value=""/> - <description value=""/> - <severity value="CRITICAL"/> - <testCaseId value="#"/> - <skip> - <issueId value="SAMPLE"/> - </skip> - </annotations> - <before> - <createData entity="SamplePerson" stepKey="beforeData"/> - </before> - <after> - - </after> - - <!-- Create an entity that depends on another entity --> - <createData entity="_defaultCategory" stepKey="createCategory"/> - <createData entity="_defaultProduct" stepKey="createProduct"> - <requiredEntity createDataKey="createCategory"/> - </createData> - - <!-- Parameterized url --> - <amOnPage url="{{SamplePage.url('foo', SamplePerson.bar)}}" stepKey="amOnPage"/> - - <!-- Parameterized selector --> - <grabTextFrom selector="{{SampleSection.twoParamElement(SamplePerson.foo, 'bar')}}" stepKey="grabTextFrom"/> - - <!-- Element with a timeout --> - <click selector="{{SampleSection.timeoutElement}}" stepKey="click"/> - - <!-- ActionGroup --> - <actionGroup ref="SampleActionGroup" stepKey="actionGroup"> - <argument name="person" value="OverrideDefaultPerson"/> - </actionGroup> - - <actionGroup ref="ValidateSlideOutPanelField" stepKey="seeAppearanceMinHeightProperty"> - <argument name="property" value="AppearanceMinHeightProperty"/> - </actionGroup> - </test> -</tests> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Test/AssertsTest.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Test/AssertsTest.xml deleted file mode 100644 index 200c52cab797..000000000000 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Test/AssertsTest.xml +++ /dev/null @@ -1,79 +0,0 @@ -<?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="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> - <test name="AssertsTest"> - <annotations> - <features value="SampleTests"/> - <skip> - <issueId value="SAMPLE"/> - </skip> - </annotations> - - <createData entity="Simple_US_Customer" stepKey="createData2"/> - <amOnUrl url="https://www.yahoo.com" stepKey="amOnPage"/> - <waitForElementVisible stepKey="wait1" selector="#uh-logo" time="10"/> - <grabTextFrom selector="#uh-logo" stepKey="grabTextFrom1"/> - - <!-- asserts without variable replacement --> - <assertArrayHasKey stepKey="assertArrayHasKey" message="pass"> - <expectedResult type="string">apple</expectedResult> - <actualResult type="const">['orange' => 2, 'apple' => 1]</actualResult> - </assertArrayHasKey> - <assertEquals stepKey="assertEquals1" message="pass"> - <expectedResult type="variable">grabTextFrom1</expectedResult> - <actualResult type="string">Copyright © 2013-2017 Magento, Inc. All rights reserved.</actualResult> - </assertEquals> - <assertFalse stepKey="assertFalse1" message="pass"> - <actualResult type="bool">0</actualResult> - </assertFalse> - <assertGreaterOrEquals stepKey="assertGreaterOrEquals" message="pass"> - <expectedResult type="int">2</expectedResult> - <actualResult type="int">5</actualResult> - </assertGreaterOrEquals> - <assertElementContainsAttribute stepKey="assertElementContainsAttribute1" selector="#username" attribute="class"> - <expectedResult type="string">admin__control-text</expectedResult> - </assertElementContainsAttribute> - - <!-- string type that use created data --> - <assertStringStartsWith stepKey="assert1" message="fail"> - <expectedResult type="string">D</expectedResult> - <actualResult type="string">$$createData1.lastname$$, $$createData1.firstname$$</actualResult> - </assertStringStartsWith> - <assertStringStartsNotWith stepKey="assert2" message="pass"> - <expectedResult type="string">W</expectedResult> - <actualResult type="string">$createData2.firstname$, $createData2.lastname$</actualResult> - </assertStringStartsNotWith> - <assertEquals stepKey="assert5" message="pass"> - <expectedResult type="string">$$createData1.lastname$$</expectedResult> - <actualResult type="string">$$createData1.lastname$$</actualResult> - </assertEquals> - <assertElementContainsAttribute stepKey="assertElementContainsAttribute7" selector="#username" attribute="value"> - <expectedResult type="const">$createData2.firstname$</expectedResult> - </assertElementContainsAttribute> - - <!-- array type that use created data --> - <assertArraySubset stepKey="assert9" message="pass"> - <expectedResult type="array">[$$createData1.lastname$$, $$createData1.firstname$$]</expectedResult> - <actualResult type="array">[$$createData1.lastname$$, $$createData1.firstname$$, 1]</actualResult> - </assertArraySubset> - <assertArraySubset stepKey="assert10" message="pass"> - <expectedResult type="array">[$createData2.firstname$, $createData2.lastname$]</expectedResult> - <actualResult type="array">[$createData2.firstname$, $createData2.lastname$, 1]</actualResult> - </assertArraySubset> - <assertArrayHasKey stepKey="assert3" message="pass"> - <expectedResult type="string">lastname</expectedResult> - <actualResult type="array">['lastname' => $$createData1.lastname$$, 'firstname' => $$createData1.firstname$$]</actualResult> - </assertArrayHasKey> - <assertArrayHasKey stepKey="assert4" message="pass"> - <expectedResult type="string">lastname</expectedResult> - <actualResult type="array">['lastname' => $createData2.lastname$, 'firstname' => $createData2.firstname$]</actualResult> - </assertArrayHasKey> - </test> -</tests> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Test/CreateConfigurableProductByApiTest.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Test/CreateConfigurableProductByApiTest.xml deleted file mode 100644 index d759d1ab2749..000000000000 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Test/CreateConfigurableProductByApiTest.xml +++ /dev/null @@ -1,77 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> - <test name="CreateConfigurableProductByApiTest"> - <annotations> - <features value="SampleTests"/> - <stories value="Create a Configurable Product By API"/> - <skip> - <issueId value="SAMPLE"/> - </skip> - </annotations> - <before> - <createData stepKey="categoryHandle" entity="SimpleSubCategory" /> - <createData stepKey="baseConfigProductHandle" entity="BaseConfigurableProduct" > - <requiredEntity createDataKey="categoryHandle"/> - </createData> - <createData stepKey="productAttributeHandle" entity="productDropDownAttribute"/> - - <createData stepKey="productAttributeOption1Handle" entity="productAttributeOption1"> - <requiredEntity createDataKey="productAttributeHandle"/> - </createData> - <createData stepKey="productAttributeOption2Handle" entity="productAttributeOption2"> - <requiredEntity createDataKey="productAttributeHandle"/> - </createData> - - <createData stepKey="addToAttributeSetHandle" entity="AddToDefaultSet"> - <requiredEntity createDataKey="productAttributeHandle"/> - </createData> - - <getData stepKey="getAttributeOption1Handle" entity="ProductAttributeOptionGetter" index="1"> - <requiredEntity createDataKey="productAttributeHandle"/> - </getData> - <getData stepKey="getAttributeOption2Handle" entity="ProductAttributeOptionGetter" index="2"> - <requiredEntity createDataKey="productAttributeHandle"/> - </getData> - - <createData stepKey="childProductHandle1" entity="SimpleOne"> - <requiredEntity createDataKey="productAttributeHandle"/> - <requiredEntity createDataKey="getAttributeOption1Handle"/> - </createData> - <createData stepKey="childProductHandle2" entity="SimpleOne"> - <requiredEntity createDataKey="productAttributeHandle"/> - <requiredEntity createDataKey="getAttributeOption2Handle"/> - </createData> - - <createData stepKey="configProductOptionHandle" entity="ConfigurableProductTwoOptions"> - <requiredEntity createDataKey="baseConfigProductHandle"/> - <requiredEntity createDataKey="productAttributeHandle"/> - <requiredEntity createDataKey="getAttributeOption1Handle"/> - <requiredEntity createDataKey="getAttributeOption2Handle"/> - </createData> - - <createData stepKey="configProductHandle1" entity="ConfigurableProductAddChild"> - <requiredEntity createDataKey="childProductHandle1"/> - <requiredEntity createDataKey="baseConfigProductHandle"/> - </createData> - <createData stepKey="configProductHandle2" entity="ConfigurableProductAddChild"> - <requiredEntity createDataKey="childProductHandle2"/> - <requiredEntity createDataKey="baseConfigProductHandle"/> - </createData> - </before> - <after> - <deleteData stepKey="d2" createDataKey="childProductHandle1"/> - <deleteData stepKey="d3" createDataKey="childProductHandle2"/> - <deleteData stepKey="d7" createDataKey="baseConfigProductHandle"/> - <deleteData stepKey="d8" createDataKey="categoryHandle"/> - <deleteData stepKey="d6" createDataKey="productAttributeHandle"/> - </after> - </test> -</tests> \ No newline at end of file diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Test/CreateSalesRuleByApiTest.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Test/CreateSalesRuleByApiTest.xml deleted file mode 100644 index 8cd83fd62067..000000000000 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Test/CreateSalesRuleByApiTest.xml +++ /dev/null @@ -1,24 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> - <test name="CreateSalesRuleByApiTest"> - <annotations> - <features value="SampleTests"/> - <stories value="Create a Sales Rule By API"/> - <skip> - <issueId value="SAMPLE"/> - </skip> - </annotations> - <before> - <createData stepKey="saleRule" entity="SimpleSalesRule" /> - </before> - <!--see stepKey="test" userInput="$$saleRule.store_labels[0][store_id]$$" selector="test"/--> - </test> -</tests> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Test/MinimumTest.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Test/MinimumTest.xml deleted file mode 100644 index f8702eeaf5e0..000000000000 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Test/MinimumTest.xml +++ /dev/null @@ -1,26 +0,0 @@ -<?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="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> - <test name="MinimumTest"> - <annotations> - <features value="SampleTests"/> - <title value="Minimum Test"/> - <description value="Minimum Test"/> - <group value="example"/> - </annotations> - <after> - <seeInCurrentUrl url="/admin/admin/" stepKey="seeInCurrentUrl"/> - </after> - <amOnPage url="{{AdminLoginPage.url}}" stepKey="navigateToAdmin"/> - <fillField selector="{{AdminLoginFormSection.username}}" userInput="{{_ENV.MAGENTO_ADMIN_USERNAME}}" stepKey="fillUsername"/> - <fillField selector="{{AdminLoginFormSection.password}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}" stepKey="fillPassword"/> - <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickLogin"/> - </test> -</tests> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Test/PersistMultipleEntitiesTest.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Test/PersistMultipleEntitiesTest.xml deleted file mode 100644 index 6a196c2c32c0..000000000000 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Test/PersistMultipleEntitiesTest.xml +++ /dev/null @@ -1,36 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> - <test name="PersistMultipleEntitiesTest"> - <annotations> - <features value="SampleTests"/> - <skip> - <issueId value="SAMPLE"/> - </skip> - </annotations> - <before> - <createData entity="simplesubcategory" stepKey="simplecategory"/> - <createData entity="simpleproduct" stepKey="simpleproduct1"> - <requiredEntity createDataKey="simplecategory"/> - </createData> - <createData entity="simpleproduct" stepKey="simpleproduct2"> - <requiredEntity createDataKey="categoryLink"/> - </createData> - </before> - <after> - <deleteData createDataKey="simpleproduct1" stepKey="deleteProduct1"/> - <deleteData createDataKey="simpleproduct2" stepKey="deleteProduct2"/> - <deleteData createDataKey="simplecategory" stepKey="deleteCategory"/> - </after> - <amOnPage stepKey="s11" url="/$$simplecategory.name$$.html" /> - <waitForPageLoad stepKey="s33"/> - <see stepKey="s35" selector="{{StorefrontCategoryMainSection.productCount}}" userInput="2"/> - </test> -</tests> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Test/SampleTest.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Test/SampleTest.xml deleted file mode 100644 index c107debc1aa6..000000000000 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Test/SampleTest.xml +++ /dev/null @@ -1,298 +0,0 @@ -<?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="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> - <test name="PreAndPostHooksTest"> - <before> - <amOnUrl url="http://127.0.0.1:32772/admin/" stepKey="amOnPage"/> - <createData entity="CustomerEntity1" stepKey="createData1"/> - <createData entity="AssertThis" stepKey="createData2"/> - </before> - <after> - <amOnUrl url="http://127.0.0.1:32772/admin/admin/auth/logout" stepKey="amOnPage"/> - <deleteData createDataKey="createData1" stepKey="deleteData1"/> - <deleteData createDataKey="createData2" stepKey="deleteData2"/> - </after> - </test> - <test name="AllCodeceptionMethodsTest"> - <annotations> - <features value="SampleTests"/> - <title value="Create all Codeception methods"/> - <description value="Exercises the Generator to make sure it creates every Codeception method correctly."/> - <severity value="CRITICAL"/> - <testCaseId value="#"/> - </annotations> - <acceptPopup stepKey="acceptPopup"/> - <amOnPage url="/admin" stepKey="amOnPage"/> - <amOnSubdomain url="admin" stepKey="amOnSubdomain"/> - <amOnUrl url="http://www.google.com/" stepKey="amOnUrl"/> - <appendField userInput="More Words" selector=".stuff" stepKey="appendField"/> - <attachFile userInput="filename.php" selector="#stuff" stepKey="attachFile"/> - <cancelPopup stepKey="cancelPopup"/> - <checkOption selector="#checkbox" stepKey="checkOption"/> - <clearField selector="#field" stepKey="clearField"/> - <click selector="#button" userInput="Context" stepKey="click1"/> - <click selectorArray="['link' => 'Login']" stepKey="click2"/> - <click selectorArray="['link' => 'Login']" userInput="stuff" stepKey="click3"/> - <clickWithLeftButton selector="#clickHere" stepKey="clickWithLeftButton1" x="23" y="324"/> - <clickWithLeftButton selectorArray="['css' => '.checkout']" stepKey="clickWithLeftButton2" x="23" y="324"/> - <clickWithLeftButton stepKey="clickWithLeftButton3" x="23" y="324"/> - <clickWithRightButton selector="#clickHere" stepKey="clickWithRightButton1" x="23" y="324"/> - <clickWithRightButton selectorArray="['css' => '.checkout']" stepKey="clickWithRightButton2" x="23" y="324"/> - <clickWithRightButton stepKey="clickWithRightButton3" x="23" y="324"/> - <closeTab stepKey="closeTab"/> - <comment userInput="This is a Comment." stepKey="comment"/> - <createData entity="CustomerEntity1" stepKey="createData1"/> - <deleteData createDataKey="createData1" stepKey="deleteData1"/> - <dontSee userInput="Text" stepKey="dontSee1"/> - <dontSee userInput="Text" selector=".title" stepKey="dontSee2"/> - <dontSee userInput="Text" selectorArray="['css' => 'body h1']" stepKey="dontSee3"/> - <dontSeeCheckboxIsChecked selector="#checkbox" stepKey="dontSeeCheckboxIsChecked"/> - <dontSeeCookie userInput="cookieName" stepKey="dontSeeCookie1"/> - <dontSeeCookie userInput="cookieName" parameterArray="['domainName' => 'stuff']" stepKey="dontSeeCookie2"/> - <dontSeeCurrentUrlEquals url="/stuff" stepKey="dontSeeCurrentUrlEquals"/> - <dontSeeCurrentUrlMatches regex="~$/users/(\d+)~" stepKey="dontSeeCurrentUrlMatches"/> - <dontSeeElement selector=".error" stepKey="dontSeeElement1"/> - <dontSeeElement selector="input" parameterArray="['name' => 'login']" stepKey="dontSeeElement2"/> - <dontSeeElementInDOM selector="#stuff" stepKey="dontSeeElementInDOM1"/> - <dontSeeElementInDOM selector="#stuff" parameterArray="['name' => 'login']" stepKey="dontSeeElementInDOM2"/> - <dontSeeInCurrentUrl url="/users/" stepKey="dontSeeInCurrentUrl"/> - <dontSeeInField selector=".field" userInput="stuff" stepKey="dontSeeInField1"/> - <dontSeeInField selectorArray="['name' => 'search']" userInput="Comment Here" stepKey="dontSeeInField2"/> - <dontSeeInFormFields selector="form[name=myform]" parameterArray="['input1' => 'non-existent value', 'input2' => 'other non-existent value']" stepKey="dontSeeInFormFields"/> - <dontSeeInPageSource userInput="Stuff in Page Source" stepKey="dontSeeInPageSource"/> - <!--<dontSeeInSource html="<h1></h1>" stepKey="dontSeeInSource"/>--> - <dontSeeInTitle userInput="Title" stepKey="dontSeeInTitle"/> - <dontSeeLink userInput="Logout" stepKey="dontSeeLink1"/> - <dontSeeLink userInput="Checkout" url="/store/cart.php" stepKey="dontSeeLink2"/> - <dontSeeOptionIsSelected selector="#form .stuff" userInput="Option Name" stepKey="dontSeeOptionIsSelected"/> - <doubleClick selector="#click .here" stepKey="doubleClick"/> - <dragAndDrop selector1="#number1" selector2="#number2" stepKey="dragAndDrop"/> - <executeInSelenium function="function(\Facebook\WebDriver\Remote\RemoteWebDriver $webdriver) {$webdriver->get('http://google.com');}" stepKey="executeInSelenium"/> - <executeJS function="return $('#myField').val()" stepKey="executeJS"/> - <fillField selector="#field" userInput="stuff" stepKey="fillField1"/> - <fillField selectorArray="['name' => 'email']" userInput="stuff" stepKey="fillField2"/> - <grabAttributeFrom selector="#target" userInput="title" stepKey="grabAttributeFrom"/> - <grabCookie userInput="cookie" parameterArray="['domain' => 'www.google.com']" stepKey="grabCookie"/> - <grabFromCurrentUrl regex="~$/user/(\d+)/~" stepKey="grabFromCurrentUrl"/> - <grabMultiple selector="a" userInput="href" stepKey="grabMultiple"/> - <grabPageSource stepKey="grabPageSource1"/> - <grabTextFrom selector="h1" stepKey="grabTextFrom1"/> - <grabValueFrom selector=".form" stepKey="grabValueFrom1"/> - <grabValueFrom selectorArray="['name' => 'username']" stepKey="grabValueFrom2"/> - <loadSessionSnapshot userInput="stuff" stepKey="loadSessionSnapshot1"/> - <loadSessionSnapshot userInput="stuff" stepKey="loadSessionSnapshot2"/> - <makeScreenshot userInput="ScreenshotName" stepKey="makeScreenshot"/> - <maximizeWindow stepKey="maximizeWindow"/> - <moveBack stepKey="moveBack"/> - <moveForward stepKey="moveForward"/> - <moveMouseOver selector="#stuff" stepKey="moveMouseOver1"/> - <moveMouseOver selectorArray="['css' => '.checkout']" stepKey="moveMouseOver2"/> - <moveMouseOver x="5" y="5" stepKey="moveMouseOver3"/> - <moveMouseOver selector="#stuff" x="5" y="5" stepKey="moveMouseOver4"/> - <moveMouseOver selectorArray="['css' => '.checkout']" x="5" y="5" stepKey="moveMouseOver5"/> - <openNewTab stepKey="openNewTab"/> - <pauseExecution stepKey="pauseExecution"/> - <performOn selector=".rememberMe" function="function (WebDriver $I) { $I->see('Remember me next time'); $I->seeElement('#LoginForm_rememberMe'); $I->dontSee('Login'); }" stepKey="performOn1"/> - <performOn selector=".rememberMe" function="ActionSequence::build()->see('Warning')->see('Are you sure you want to delete this?')->click('Yes')" stepKey="performOn2"/> - <pressKey selector="#page" userInput="a" stepKey="pressKey1"/> - <pressKey selector="#page" parameterArray="[['ctrl','a'],'new']" stepKey="pressKey2"/> - <pressKey selector="#page" parameterArray="[['shift','111'],'1','x']" stepKey="pressKey3"/> - <pressKey selector="#page" parameterArray="[['ctrl', 'a'], \Facebook\WebDriver\WebDriverKeys::DELETE]" stepKey="pressKey4"/> - <!--pressKey selector="descendant-or-self::*[@id='page']" userInput="u" stepKey="pressKey5"/--> - <reloadPage stepKey="reloadPage"/> - <resetCookie userInput="cookie" stepKey="resetCookie1"/> - <resetCookie userInput="cookie" parameterArray="['domainName' => 'www.google.com']" stepKey="resetCookie2"/> - <resizeWindow width="800" height="600" stepKey="resizeWindow"/> - <saveSessionSnapshot userInput="stuff" stepKey="saveSessionSnapshot"/> - <scrollTo selector="#place" x="20" y="50" stepKey="scrollTo1"/> - <scrollTo selectorArray="['css' => '.checkout']" x="20" y="50" stepKey="scrollTo2"/> - <see userInput="Stuff" stepKey="see1"/> - <see userInput="More Stuff" selector=".stuff" stepKey="see2"/> - <see userInput="More More Stuff" selectorArray="['css' => 'body h1']" stepKey="see3"/> - <seeCheckboxIsChecked selector="#checkbox" stepKey="seeCheckboxIsChecked"/> - <seeCookie userInput="PHPSESSID" stepKey="seeCookie1"/> - <seeCookie userInput="PHPSESSID" parameterArray="['domainName' => 'www.google.com']" stepKey="seeCookie2"/> - <seeCurrentUrlEquals url="/" stepKey="seeCurrentUrlEquals"/> - <seeCurrentUrlMatches regex="~$/users/(\d+)~" stepKey="seeCurrentUrlMatches"/> - <seeElement selector=".error" stepKey="seeElement1"/> - <seeElement selectorArray="['css' => 'form input']" stepKey="seeElement2"/> - <seeElement selector=".error" parameterArray="['name' => 'login']" stepKey="seeElement3"/> - <seeElement selectorArray="['css' => 'form input']" parameterArray="['name' => 'login']" stepKey="seeElement4"/> - <seeElementInDOM selector="//form/input[type=hidden]" stepKey="seeElementInDOM1"/> - <seeElementInDOM selector="//form/input[type=hidden]" parameterArray="['name' => 'form']" stepKey="seeElementInDOM2"/> - <seeInCurrentUrl url="home" stepKey="seeInCurrentUrl1"/> - <seeInCurrentUrl url="/home/" stepKey="seeInCurrentUrl2"/> - <seeInField userInput="Stuff" selector="#field" stepKey="seeInField1"/> - <seeInField userInput="Stuff" selectorArray="['name' => 'search']" stepKey="seeInField2"/> - <seeInFormFields selector="form[name=myform]" parameterArray="['input1' => 'value','input2' => 'other value']" stepKey="seeInFormFields1"/> - <seeInFormFields selector=".form-class" parameterArray="[['multiselect' => ['value1','value2'],'checkbox[]]' => ['a checked value','another checked value',]]" stepKey="seeInFormFields2"/> - <!--<seeInPageSource html="<h1></h1>" stepKey="seeInPageSource"/>--> - <seeInPopup userInput="Yes in Popup" stepKey="seeInPopup"/> - <!--<seeInSource html="<h1></h1>" stepKey="seeInSource"/>--> - <seeInTitle userInput="In Title" stepKey="seeInTitle"/> - <seeLink userInput="Logout" stepKey="seeLink1"/> - <seeLink userInput="Logout" url="/logout" stepKey="seeLink2"/> - <seeNumberOfElements selector="tr" userInput="10" stepKey="seeNumberOfElements1"/> - <seeNumberOfElements selector="tr" userInput="[0, 10]" stepKey="seeNumberOfElements2"/> - <seeOptionIsSelected selector=".option" userInput="Visa" stepKey="seeOptionIsSelected"/> - <selectOption selector=".dropDown" userInput="Option Name" stepKey="selectOption1"/> - <selectOption selector="//form/select[@name=account]" parameterArray="['Windows','Linux']" stepKey="selectOption2"/> - <selectOption selector="Which OS do you use?" parameterArray="['text' => 'Windows']" stepKey="selectOption3"/> - <setCookie userInput="PHPSESSID" value="stuff" stepKey="setCookie1"/> - <setCookie userInput="PHPSESSID" value="stuff" parameterArray="['domainName' => 'www.google.com']" stepKey="setCookie2"/> - <submitForm selector="#my-form" parameterArray="['field' => ['value','another value',]]" button="#submit" stepKey="submitForm2"/> - <switchToIFrame stepKey="switchToIFrame1"/> - <switchToIFrame userInput="another_frame" stepKey="switchToIFrame2"/> - <switchToNextTab stepKey="switchToNextTab1"/> - <switchToNextTab userInput="2" stepKey="switchToNextTab2"/> - <switchToPreviousTab stepKey="switchToPreviewTab1"/> - <switchToPreviousTab userInput="1" stepKey="switchToPreviewTab2"/> - <switchToWindow stepKey="switchToWindow1"/> - <switchToWindow userInput="another_window" stepKey="switchToWindow2"/> - <typeInPopup userInput="Stuff for popup" stepKey="typeInPopup"/> - <uncheckOption selector="#option" stepKey="uncheckOption"/> - <unselectOption selector="#dropDown" userInput="Option" stepKey="unselectOption"/> - <wait time="15" stepKey="wait"/> - <waitForElement selector="#button" time="10" stepKey="waitForElement"/> - <waitForElementChange selector="#menu" function="function(\WebDriverElement $el) {return $el->isDisplayed();}" time="100" stepKey="waitForElementChange"/> - <waitForElementNotVisible selector="#a_thing .className" time="30" stepKey="waitForElementNotVisible"/> - <waitForElementVisible selector="#a_thing .className" time="15" stepKey="waitForElementVisible"/> - <waitForJS function="return $.active == 0;" time="30" stepKey="waitForJS"/> - <waitForText userInput="foo" time="30" stepKey="waitForText1"/> - <waitForText userInput="foo" selector=".title" time="30" stepKey="waitForText2"/> - </test> - <test name="AllCustomMethodsTest"> - <annotations> - <title value="Create all Custom methods"/> - <description value="Exercises the Generator to make sure it creates every Custom method correctly."/> - <severity value="CRITICAL"/> - <testCaseId value="#"/> - </annotations> - <assertElementContainsAttribute selector="#username" attribute="class" expectedValue="admin__control-text" stepKey="assertElementContainsAttribute1"/> - <assertElementContainsAttribute selector="#username" attribute="type" expectedValue="text" stepKey="assertElementContainsAttribute2"/> - <assertElementContainsAttribute selector="#username" attribute="name" expectedValue="login[username]" stepKey="assertElementContainsAttribute3"/> - <assertElementContainsAttribute selector="#username" attribute="autofocus" expectedValue="" stepKey="assertElementContainsAttribute4"/> - <assertElementContainsAttribute selector="#username" attribute="data-validate" expectedValue="{required:true}" stepKey="assertElementContainsAttribute5"/> - <assertElementContainsAttribute selector="#username" attribute="placeholder" expectedValue="user name" stepKey="assertElementContainsAttribute6"/> - <assertElementContainsAttribute selector="#username" attribute="autocomplete" expectedValue="off" stepKey="assertElementContainsAttribute7"/> - <assertElementContainsAttribute selector=".admin__menu-overlay" attribute="style" expectedValue="display: none;" stepKey="assertElementContainsAttribute8"/> - <assertElementContainsAttribute selector=".admin__menu-overlay" attribute="border" expectedValue="0" stepKey="assertElementContainsAttribute9"/> - <closeAdminNotification stepKey="closeAdminNotification1"/> - <searchAndMultiSelectOption selector="#stuff" parameterArray="['Item 1', 'Item 2']" stepKey="searchAndMultiSelect1"/> - <searchAndMultiSelectOption selector="#stuff" parameterArray="['Item 1', 'Item 2']" requiredAction="true" stepKey="searchAndMultiSelect2"/> - <waitForPageLoad stepKey="waitForPageLoad1"/> - <waitForPageLoad time="15" stepKey="waitForPageLoad2"/> - <waitForAjaxLoad stepKey="waitForAjax1"/> - <waitForAjaxLoad time="15" stepKey="waitForAjax2"/> - <dontSeeJsError stepKey="dontSeeJsError"/> - <formatMoney userInput="$300,000" stepKey="formatMoney1"/> - <formatMoney userInput="$300,000" locale="en_US.UTF-8" stepKey="formatMoney2"/> - <mSetLocale userInput="300" locale="en_US.UTF-8" stepKey="mSetLocale1"/> - <mResetLocale stepKey="mResetLocale1"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear1"/> - <scrollToTopOfPage stepKey="scrollToTopOfPage"/> - <parseFloat userInput="300,000.2325" stepKey="parseFloat1"/> - <magentoCLI stepKey="enableMaintenance1" command="maintenance:enable"/> - </test> - <test name="AllVariableMethodsTest"> - <annotations> - <features value="SampleTests"/> - <title value="Create all Methods that support Variables."/> - <description value="Exercises the Generator to make sure it creates every Method that supports a Variable."/> - <severity value="CRITICAL"/> - <testCaseId value="#"/> - </annotations> - <grabFromCurrentUrl stepKey="grabFromCurrentUrl1"/> - <amOnPage url="{$randomStuff}" stepKey="amOnPage1"/> - <amOnSubdomain url="{$randomStuff}" stepKey="amOnSubdomain1"/> - <amOnUrl url="{$randomStuff}" stepKey="amOnUrl1"/> - <appendField userInput="{$randomStuff}" selector="#randomField" stepKey="appendField1"/> - <attachFile userInput="{$randomStuff}" selector="#filePathField" stepKey="attachFile1"/> - <click userInput="{$randomStuff}" stepKey="click1"/> - <dontSee userInput="{$randomStuff}" stepKey="dontSee1"/> - <dontSeeCookie userInput="{$randomStuff}" stepKey="dontSeeCookie1"/> - <dontSeeCurrentUrlEquals url="{$randomStuff}" stepKey="dontSeeCurrentUrlEquals1"/> - <dontSeeCurrentUrlMatches regex="{$randomStuff}" stepKey="dontSeeCurrentUrlMatches1"/> - <dontSeeInCurrentUrl url="{$randomStuff}" stepKey="dontSeeInCurrentUrl1"/> - <dontSeeInField selector="#stuff" userInput="{$randomStuff}" stepKey="dontSeeInField1"/> - <dontSeeInPageSource userInput="{$randomStuff}" stepKey="dontSeeInPageSource1"/> - <dontSeeInTitle userInput="{$randomStuff}" stepKey="dontSeeInTitle1"/> - <dontSeeLink userInput="{$randomStuff}" stepKey="dontSeeLink1"/> - <dontSeeOptionIsSelected selector="#dropdown" userInput="{$randomStuff}" stepKey="dontSeeOptionIsSelected1"/> - <fillField userInput="{$randomStuff}" selector="#field" stepKey="fillField1"/> - <grabAttributeFrom selector="#stuff" userInput="{$randomStuff}" stepKey="grabAttributeFrom1"/> - <grabCookie userInput="{$randomStuff}" stepKey="grabValueFrom1"/> - <grabFromCurrentUrl regex="{$randomStuff}" stepKey="grabFromCurrentUrl"/> - <grabMultiple selector="a" userInput="{$randomStuff}" stepKey="grabMultiple1"/> - <loadSessionSnapshot userInput="{$randomStuff}" stepKey="loadSessionSnapshot"/> - <pressKey selector="a" userInput="{$randomStuff}" stepKey="pressKey1"/> - <saveSessionSnapshot userInput="{$randomStuff}" stepKey="saveSessionSnapshot1"/> - <see userInput="{$randomStuff}" stepKey="see1"/> - <seeCookie userInput="{$randomStuff}" stepKey="seeCookie1"/> - <seeCurrentUrlEquals url="{$randomStuff}" stepKey="seeCurrentUrlEquals1"/> - <seeCurrentUrlMatches regex="{$randomStuff}" stepKey="seeCurrentUrlMatches1"/> - <seeInCurrentUrl url="{$randomStuff}" stepKey="seeInCurrentUrl1"/> - <seeInField selector="a" userInput="{$randomStuff}" stepKey="seeInField1"/> - <seeInPopup userInput="{$randomStuff}" stepKey="seeInPopup"/> - <seeInTitle userInput="{$randomStuff}" stepKey="seeInTitle1"/> - <seeLink userInput="{$randomStuff}" stepKey="seeLink1"/> - <seeNumberOfElements selector="#stuff" userInput="{$randomStuff}" stepKey="seeNumberOfElements1"/> - <seeOptionIsSelected selector="#stuff" userInput="{$randomStuff}" stepKey="seeOptionIsSelected1"/> - <selectOption selector="#stuff" userInput="{$randomStuff}" stepKey="selectOption1"/> - <switchToIFrame userInput="{$randomStuff}" stepKey="switchToIFrame1"/> - <switchToNextTab userInput="{$randomStuff}" stepKey="switchToNextTab2"/> - <switchToPreviousTab userInput="{$randomStuff}" stepKey="switchToPreviousTab1"/> - <switchToNextTab userInput="{$randomStuff}" stepKey="switchToNextTab3"/> - <switchToWindow userInput="{$randomStuff}" stepKey="switchToWindow1"/> - <typeInPopup userInput="{$randomStuff}" stepKey="typeInPopup"/> - <unselectOption selector="#option" userInput="{$randomStuff}" stepKey="unselectOption1"/> - <waitForText userInput="{$randomStuff}" time="5" stepKey="waitForText1"/> - </test> - <test name="AllReplacementTest"> - <annotations> - <features value="SampleTests"/> - <title value="Exercise reference replacement."/> - <description value="Exercises {{foo}}, $foo$, and $$foo$$ replacement."/> - <severity value="CRITICAL"/> - <testCaseId value="#"/> - </annotations> - - <createData entity="CustomerEntity1" stepKey="testScopeData"/> - <createData entity="AssertThis" stepKey="testScopeData2"/> - - <!-- parameterized url that uses literal params --> - <amOnPage url="{{SamplePage.url('success','success2')}}" stepKey="a0"/> - - <!-- url referencing data that was created in this <test> --> - <amOnPage url="$testScopeData.firstname$.html" stepKey="a1"/> - - <!-- url referencing data that was created in a <before> --> - <amOnPage url="$$createData1.firstname$$.html" stepKey="a2"/> - - <!-- parameterized url that uses created data params --> - <amOnPage url="{{SamplePage.url($testScopeData.firstname$,$testScopeData.lastname$)}}" stepKey="a3"/> - <amOnPage url="{{SamplePage.url($$createData1.firstname$$,$$createData1.lastname$$)}}" stepKey="a4"/> - - <!-- parameterized selector that uses literal params --> - <click selector="{{SampleSection.oneParamElement('success')}}" stepKey="c1"/> - <click selector="{{SampleSection.twoParamElement('success','success2')}}" stepKey="c2"/> - - <!-- parameterized selector with literal, static data, and created data --> - <click selector="{{SampleSection.threeParamElement('John', SamplePerson.lastname, $testScopeData.lastname$)}}" stepKey="c3"/> - - <!-- selector that uses created data --> - <click selector="#$testScopeData.firstname$ .$testScopeData.lastname$" stepKey="c4"/> - <click selector="#$$createData1.firstname$$ .$$createData1.lastname$$" stepKey="c5"/> - - <!-- userInput that uses created data --> - <fillField selector="#sample" userInput="Hello $testScopeData.firstname$ $testScopeData.lastname$" stepKey="f1"/> - <fillField selector="#sample" userInput="Hello $$createData1.firstname$$ $$createData1.lastname$$" stepKey="f2"/> - </test> -</tests> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Test/SetPaymentConfigurationTest.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Test/SetPaymentConfigurationTest.xml deleted file mode 100644 index f908c5cb4572..000000000000 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Test/SetPaymentConfigurationTest.xml +++ /dev/null @@ -1,31 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> - <test name="SetPaypalConfigurationTest"> - <annotations> - <features value="SampleTests"/> - <skip> - <issueId value="SAMPLE"/> - </skip> - </annotations> - <createData entity="SamplePaypalConfig" stepKey="createSamplePaypalConfig"/> - <createData entity="DefaultPayPalConfig" stepKey="restoreDefaultPaypalConfig"/> - </test> - <test name="SetBraintreeConfigurationTest"> - <annotations> - <features value="SampleTests"/> - <skip> - <issueId value="SAMPLE"/> - </skip> - </annotations> - <createData entity="SampleBraintreeConfig" stepKey="createSampleBraintreeConfig"/> - <createData entity="DefaultBraintreeConfig" stepKey="restoreDefaultBraintreeConfig"/> - </test> -</tests> diff --git a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Test/UpdateSimpleProductByApiTest.xml b/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Test/UpdateSimpleProductByApiTest.xml deleted file mode 100644 index b18c24050d3a..000000000000 --- a/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/SampleTests/Test/UpdateSimpleProductByApiTest.xml +++ /dev/null @@ -1,29 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> - <test name="UpdateSimpleProductByApiTest"> - <annotations> - <features value="SampleTests"/> - <stories value="Update simple product by api test."/> - <skip> - <issueId value="SAMPLE"/> - </skip> - </annotations> - <before> - <createData stepKey="categoryHandle" entity="SimpleSubCategory"/> - <createData stepKey="productHandle" entity="SimpleProduct" > - <requiredEntity createDataKey="categoryHandle"/> - </createData> - <updateData stepKey="updateProduct" entity="NewSimpleProduct" createDataKey="productHandle"/> - </before> - <after> - <deleteData stepKey="delete" createDataKey="updateProduct"/> - </after> - </test> -</tests> \ No newline at end of file diff --git a/dev/tests/api-functional/_files/Magento/TestModuleDefaultHydrator/etc/db_schema.xml b/dev/tests/api-functional/_files/Magento/TestModuleDefaultHydrator/etc/db_schema.xml index d9bba3ca5dbe..3c71e2fde48f 100644 --- a/dev/tests/api-functional/_files/Magento/TestModuleDefaultHydrator/etc/db_schema.xml +++ b/dev/tests/api-functional/_files/Magento/TestModuleDefaultHydrator/etc/db_schema.xml @@ -11,10 +11,10 @@ <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true"/> <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="false" identity="false"/> <column xsi:type="varchar" name="value" nullable="true" length="255"/> - <constraint xsi:type="primary" name="PRIMARY"> + <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> - <constraint xsi:type="foreign" name="CSTR_EXTENSION_ATTR_ENTT_CSTR_ID_CSTR_ENTT_ENTT_ID" + <constraint xsi:type="foreign" referenceId="CSTR_EXTENSION_ATTR_ENTT_CSTR_ID_CSTR_ENTT_ENTT_ID" table="testmodule_default_hydrator_extension_attribute_entity" column="customer_id" referenceTable="customer_entity" referenceColumn="entity_id" onDelete="CASCADE"/> </table> diff --git a/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQuery/Model/Resolver/Item.php b/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQuery/Model/Resolver/Item.php index 6170c8dea3dd..1731a974aaed 100644 --- a/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQuery/Model/Resolver/Item.php +++ b/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQuery/Model/Resolver/Item.php @@ -9,27 +9,10 @@ use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Query\PostFetchProcessorInterface; -use Magento\Framework\GraphQl\Query\Resolver\Value; -use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; class Item implements ResolverInterface { - /** - * @var ValueFactory - */ - private $valueFactory; - - /** - * @param ValueFactory $valueFactory - */ - public function __construct( - ValueFactory $valueFactory - ) { - $this->valueFactory = $valueFactory; - } - /** * @inheritdoc */ @@ -39,7 +22,7 @@ public function resolve( ResolveInfo $info, array $value = null, array $args = null - ) : Value { + ) { $id = 0; foreach ($args as $key => $argValue) { if ($key === "id") { @@ -50,11 +33,6 @@ public function resolve( 'item_id' => $id, 'name' => "itemName" ]; - - $result = function () use ($itemData) { - return $itemData; - }; - - return $this->valueFactory->create($result); + return $itemData; } } diff --git a/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQueryExtension/Model/Resolver/IntegerList.php b/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQueryExtension/Model/Resolver/IntegerList.php index 878212f49421..34dc92fcbbaa 100644 --- a/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQueryExtension/Model/Resolver/IntegerList.php +++ b/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQueryExtension/Model/Resolver/IntegerList.php @@ -9,8 +9,6 @@ use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Query\Resolver\Value; -use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; /** @@ -19,20 +17,7 @@ class IntegerList implements ResolverInterface { /** - * @var ValueFactory - */ - private $valueFactory; - - /** - * @param ValueFactory $valueFactory - */ - public function __construct(ValueFactory $valueFactory) - { - $this->valueFactory = $valueFactory; - } - - /** - * {@inheritdoc} + * @inheritdoc */ public function resolve( Field $field, @@ -40,21 +25,14 @@ public function resolve( ResolveInfo $info, array $value = null, array $args = null - ): Value { + ) { if (!isset($value['item_id'])) { - return $this->valueFactory->create(function () { - return null; - }); + return null; } $itemId = $value['item_id']; $resultData = [$itemId + 1, $itemId + 2, $itemId + 3]; - - $result = function () use ($resultData) { - return $resultData; - }; - - return $this->valueFactory->create($result); + return $resultData; } } diff --git a/dev/tests/api-functional/_files/Magento/TestModuleJoinDirectives/etc/extension_attributes.xml b/dev/tests/api-functional/_files/Magento/TestModuleJoinDirectives/etc/extension_attributes.xml index 3bd64a7aff72..8254a9d8e92c 100644 --- a/dev/tests/api-functional/_files/Magento/TestModuleJoinDirectives/etc/extension_attributes.xml +++ b/dev/tests/api-functional/_files/Magento/TestModuleJoinDirectives/etc/extension_attributes.xml @@ -31,4 +31,16 @@ </join> </attribute> </extension_attributes> + <extension_attributes for="Magento\Sales\Api\Data\OrderInterface"> + <attribute code="orderApiTestAttribute" type="Magento\User\Api\Data\UserInterface"> + <join reference_table="admin_user" + join_on_field="store_id" + reference_field="user_id" + > + <field>firstname</field> + <field>lastname</field> + <field>email</field> + </join> + </attribute> + </extension_attributes> </config> diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQl/Client.php b/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQl/Client.php index 64ad44528572..cdc9d5e8a4c9 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQl/Client.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQl/Client.php @@ -29,8 +29,6 @@ class Client private $json; /** - * CurlClient constructor. - * * @param CurlClient|null $curlClient * @param JsonSerializer|null $json */ @@ -53,34 +51,109 @@ public function __construct( * @return array|string|int|float|bool * @throws \Exception */ - public function postQuery(string $query, array $variables = [], string $operationName = '', array $headers = []) + public function post(string $query, array $variables = [], string $operationName = '', array $headers = []) { $url = $this->getEndpointUrl(); $headers = array_merge($headers, ['Accept: application/json', 'Content-Type: application/json']); $requestArray = [ 'query' => $query, - 'variables' => empty($variables) ? $variables : null, - 'operationName' => empty($operationName) ? $operationName : null + 'variables' => !empty($variables) ? $variables : null, + 'operationName' => !empty($operationName) ? $operationName : null ]; $postData = $this->json->jsonEncode($requestArray); $responseBody = $this->curlClient->post($url, $postData, $headers); - $responseBodyArray = $this->json->jsonDecode($responseBody); + return $this->processResponse($responseBody); + } - if (!is_array($responseBodyArray)) { - throw new \Exception('Unknown GraphQL response body: ' . json_encode($responseBodyArray)); + /** + * Perform HTTP GET request for query + * + * @param string $query + * @param array $variables + * @param string $operationName + * @param array $headers + * @return mixed + * @throws \Exception + */ + public function get(string $query, array $variables = [], string $operationName = '', array $headers = []) + { + $url = $this->getEndpointUrl(); + $requestArray = [ + 'query' => $query, + 'variables' => $variables ? $this->json->jsonEncode($variables) : null, + 'operationName' => $operationName ?? null + ]; + array_filter($requestArray); + + try { + $responseBody = $this->curlClient->get($url, $requestArray, $headers); + } catch (\Exception $e) { + // if response code > 400 then response is the exception message + $responseBody = $e->getMessage(); } + return $this->processResponse($responseBody); + } - $this->processErrors($responseBodyArray); + /** + * Process response from GraphQl server + * + * @param string $response + * @return mixed + * @throws \Exception + */ + private function processResponse(string $response) + { + $responseArray = $this->json->jsonDecode($response); + + if (!is_array($responseArray)) { + //phpcs:ignore Magento2.Exceptions.DirectThrow + throw new \Exception('Unknown GraphQL response body: ' . $response); + } - if (!isset($responseBodyArray['data'])) { - throw new \Exception('Unknown GraphQL response body: ' . json_encode($responseBodyArray)); - } else { - return $responseBodyArray['data']; + $this->processErrors($responseArray); + + if (!isset($responseArray['data'])) { + //phpcs:ignore Magento2.Exceptions.DirectThrow + throw new \Exception('Unknown GraphQL response body: ' . $response); } + + return $responseArray['data']; } /** + * Perform HTTP GET request, return response data and headers + * + * @param string $query + * @param array $variables + * @param string $operationName + * @param array $headers + * @return array + */ + public function getWithResponseHeaders( + string $query, + array $variables = [], + string $operationName = '', + array $headers = [] + ): array { + $url = $this->getEndpointUrl(); + $requestArray = [ + 'query' => $query, + 'variables' => $variables ? $this->json->jsonEncode($variables) : null, + 'operationName' => !empty($operationName) ? $operationName : null + ]; + array_filter($requestArray); + + $response = $this->curlClient->getWithFullResponse($url, $requestArray, $headers); + $responseBody = $this->processResponse($response['body']); + $responseHeaders = !empty($response['header']) ? $this->processResponseHeaders($response['header']) : []; + + return ['headers' => $responseHeaders, 'body' => $responseBody]; + } + + /** + * Process errors + * * @param array $responseBodyArray * @throws \Exception */ @@ -102,13 +175,19 @@ private function processErrors($responseBodyArray) } } - throw new \Exception('GraphQL response contains errors: ' . $errorMessage); + throw new ResponseContainsErrorsException( + 'GraphQL response contains errors: ' . $errorMessage, + $responseBodyArray + ); } + //phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception('GraphQL responded with an unknown error: ' . json_encode($responseBodyArray)); } } /** + * Get endpoint url + * * @return string resource URL * @throws \Exception */ @@ -116,4 +195,27 @@ public function getEndpointUrl() { return rtrim(TESTS_BASE_URL, '/') . '/graphql'; } + + /** + * Parse response headers into associative array + * + * @param string $headers + * @return array + */ + private function processResponseHeaders(string $headers): array + { + $headersArray = []; + + $headerLines = preg_split('/((\r?\n)|(\r\n?))/', $headers); + foreach ($headerLines as $headerLine) { + $headerParts = preg_split('/:/', $headerLine); + if (count($headerParts) == 2) { + $headersArray[trim($headerParts[0])] = trim($headerParts[1]); + } elseif (preg_match('/HTTP\/[\.0-9]+/', $headerLine)) { + $headersArray[trim('Status-Line')] = trim($headerLine); + } + } + + return $headersArray; + } } diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQl/ResponseContainsErrorsException.php b/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQl/ResponseContainsErrorsException.php new file mode 100644 index 000000000000..568de57543d8 --- /dev/null +++ b/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQl/ResponseContainsErrorsException.php @@ -0,0 +1,41 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\TestCase\GraphQl; + +/** + * Response contains errors exception + */ +class ResponseContainsErrorsException extends \Exception +{ + /** + * @var array + */ + private $responseData; + + /** + * @param string $message + * @param array $responseData + * @param \Exception|null $cause + * @param int $code + */ + public function __construct(string $message, array $responseData, \Exception $cause = null, int $code = 0) + { + parent::__construct($message, $code, $cause); + $this->responseData = $responseData; + } + + /** + * Get response data + * + * @return array + */ + public function getResponseData(): array + { + return $this->responseData; + } +} diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQlAbstract.php b/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQlAbstract.php index cfcd5dd1b51d..94eb5ddec860 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQlAbstract.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQlAbstract.php @@ -6,6 +6,7 @@ namespace Magento\TestFramework\TestCase; use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Request\Http; /** * Test case for Web API functional tests for Graphql. @@ -27,12 +28,13 @@ abstract class GraphQlAbstract extends WebapiAbstract private $appCache; /** - * Perform GraphQL call to the system under test. + * Perform GraphQL query call via GET to the system under test. * * @see \Magento\TestFramework\TestCase\GraphQl\Client::call() * @param string $query * @param array $variables * @param string $operationName + * @param array $headers * @return array|int|string|float|bool GraphQL call results * @throws \Exception */ @@ -42,7 +44,7 @@ public function graphQlQuery( string $operationName = '', array $headers = [] ) { - return $this->getGraphQlClient()->postQuery( + return $this->getGraphQlClient()->get( $query, $variables, $operationName, @@ -51,11 +53,62 @@ public function graphQlQuery( } /** + * Perform GraphQL mutations call via POST to the system under test. + * + * @see \Magento\TestFramework\TestCase\GraphQl\Client::call() + * @param string $query + * @param array $variables + * @param string $operationName + * @param array $headers + * @return array|int|string|float|bool GraphQL call results + * @throws \Exception + */ + public function graphQlMutation( + string $query, + array $variables = [], + string $operationName = '', + array $headers = [] + ) { + return $this->getGraphQlClient()->post( + $query, + $variables, + $operationName, + $this->composeHeaders($headers) + ); + } + + /** + * Perform GraphQL query via GET and returns only the response headers + * + * @param string $query + * @param array $variables + * @param string $operationName + * @param array $headers + * @return array + */ + public function graphQlQueryWithResponseHeaders( + string $query, + array $variables = [], + string $operationName = '', + array $headers = [] + ): array { + return $this->getGraphQlClient()->getWithResponseHeaders( + $query, + $variables, + $operationName, + $this->composeHeaders($headers) + ); + } + + /** + * Compose headers + * + * @param array $headers * @return string[] */ - private function composeHeaders($headers) + private function composeHeaders(array $headers): array { - $headersArray =[]; + $headersArray = []; foreach ($headers as $key => $value) { $headersArray[] = sprintf('%s: %s', $key, $value); } diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/HttpClient/CurlClient.php b/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/HttpClient/CurlClient.php index 787f207ef33e..67691a3c909b 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/HttpClient/CurlClient.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/HttpClient/CurlClient.php @@ -32,6 +32,25 @@ public function get($url, $data = [], $headers = []) return $resp["body"]; } + /** + * Perform a HTTP GET request and return the full response + * + * @param string $url + * @param array $data + * @param array $headers + * @return array + */ + public function getWithFullResponse($url, $data = [], $headers = []): array + { + if (!empty($data)) { + $url .= '?' . http_build_query($data); + } + + $curlOpts = []; + $curlOpts[CURLOPT_CUSTOMREQUEST] = \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET; + return $this->invokeApi($url, $curlOpts, $headers); + } + /** * Perform HTTP DELETE request * @@ -98,8 +117,10 @@ public function put($url, $data, $headers = []) public function invokeApi($url, $additionalCurlOpts, $headers = []) { // initialize cURL + // phpcs:ignore Magento2.Functions.DiscouragedFunction $curl = curl_init($url); if ($curl === false) { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception("Error Initializing cURL for baseUrl: " . $url); } @@ -108,28 +129,40 @@ public function invokeApi($url, $additionalCurlOpts, $headers = []) // add CURL opts foreach ($curlOpts as $opt => $val) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction curl_setopt($curl, $opt, $val); } + // phpcs:ignore Magento2.Functions.DiscouragedFunction $response = curl_exec($curl); if ($response === false) { - throw new \Exception(curl_error($curl)); + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $error = curl_error($curl); + // phpcs:ignore Magento2.Exceptions.DirectThrow + throw new \Exception($error); } $resp = []; + // phpcs:ignore Magento2.Functions.DiscouragedFunction $headerSize = curl_getinfo($curl, CURLINFO_HEADER_SIZE); $resp["header"] = substr($response, 0, $headerSize); $resp["body"] = substr($response, $headerSize); + // phpcs:ignore Magento2.Functions.DiscouragedFunction $resp["meta"] = curl_getinfo($curl); if ($resp["meta"] === false) { - throw new \Exception(curl_error($curl)); + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $error = curl_error($curl); + // phpcs:ignore Magento2.Exceptions.DirectThrow + throw new \Exception($error); } + // phpcs:ignore Magento2.Functions.DiscouragedFunction curl_close($curl); $meta = $resp["meta"]; if ($meta && $meta['http_code'] >= 400) { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception($resp["body"], $meta['http_code']); } diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/HttpClient/CurlClientWithCookies.php b/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/HttpClient/CurlClientWithCookies.php index ecd748d89bb2..1dd9d17f904b 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/HttpClient/CurlClientWithCookies.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/HttpClient/CurlClientWithCookies.php @@ -23,8 +23,6 @@ class CurlClientWithCookies protected $jsonSerializer; /** - * CurlClient constructor. - * * @param CurlClient $curlClient * @param \Magento\TestFramework\Helper\JsonSerializer $jsonSerializer */ @@ -38,6 +36,8 @@ public function __construct( } /** + * Compose the resource url + * * @param string $resourcePath Resource URL like /V1/Resource1/123 * @return string resource URL * @throws \Exception @@ -82,7 +82,7 @@ public function get($resourcePath, $data = [], $headers = []) * ], * ] * - * @param $headerBlock + * @param string $headerBlock * @return array */ private function cookieParse($headerBlock) diff --git a/dev/tests/api-functional/phpunit_graphql.xml.dist b/dev/tests/api-functional/phpunit_graphql.xml.dist index 4a57c338ca3a..aa1899d88f48 100644 --- a/dev/tests/api-functional/phpunit_graphql.xml.dist +++ b/dev/tests/api-functional/phpunit_graphql.xml.dist @@ -52,5 +52,58 @@ <!-- Test listeners --> <listeners> <listener class="Magento\TestFramework\Event\PhpUnit"/> + <listener class="Yandex\Allure\Adapter\AllureAdapter"> + <arguments> + <string>var/allure-results</string> <!-- XML files output folder --> + <boolean>true</boolean> <!-- Whether to delete previous results on rerun --> + <array> <!-- A list of custom annotations to ignore (optional) --> + <element key="codingStandardsIgnoreStart"> + <string>codingStandardsIgnoreStart</string> + </element> + <element key="codingStandardsIgnoreEnd"> + <string>codingStandardsIgnoreEnd</string> + </element> + <element key="expectedExceptionMessageRegExp"> + <string>expectedExceptionMessageRegExp</string> + </element> + <element key="magentoAdminConfigFixture"> + <string>magentoAdminConfigFixture</string> + </element> + <element key="magentoAppArea"> + <string>magentoAppArea</string> + </element> + <element key="magentoAppIsolation"> + <string>magentoAppIsolation</string> + </element> + <element key="magentoCache"> + <string>magentoCache</string> + </element> + <element key="magentoComponentsDir"> + <string>magentoComponentsDir</string> + </element> + <element key="magentoConfigFixture"> + <string>magentoConfigFixture</string> + </element> + <element key="magentoDataFixture"> + <string>magentoDataFixture</string> + </element> + <element key="magentoDataFixtureBeforeTransaction"> + <string>magentoDataFixtureBeforeTransaction</string> + </element> + <element key="magentoDbIsolation"> + <string>magentoDbIsolation</string> + </element> + <element key="magentoIndexerDimensionMode"> + <string>magentoIndexerDimensionMode</string> + </element> + <element key="magentoApiDataFixture"> + <string>magentoApiDataFixture</string> + </element> + <element key="Override"> + <string>Override</string> + </element> + </array> + </arguments> + </listener> </listeners> </phpunit> diff --git a/dev/tests/api-functional/phpunit_rest.xml.dist b/dev/tests/api-functional/phpunit_rest.xml.dist index a2bc077328e2..c5173e5dd432 100644 --- a/dev/tests/api-functional/phpunit_rest.xml.dist +++ b/dev/tests/api-functional/phpunit_rest.xml.dist @@ -58,5 +58,58 @@ <!-- Test listeners --> <listeners> <listener class="Magento\TestFramework\Event\PhpUnit"/> + <listener class="Yandex\Allure\Adapter\AllureAdapter"> + <arguments> + <string>var/allure-results</string> <!-- XML files output folder --> + <boolean>true</boolean> <!-- Whether to delete previous results on rerun --> + <array> <!-- A list of custom annotations to ignore (optional) --> + <element key="codingStandardsIgnoreStart"> + <string>codingStandardsIgnoreStart</string> + </element> + <element key="codingStandardsIgnoreEnd"> + <string>codingStandardsIgnoreEnd</string> + </element> + <element key="expectedExceptionMessageRegExp"> + <string>expectedExceptionMessageRegExp</string> + </element> + <element key="magentoAdminConfigFixture"> + <string>magentoAdminConfigFixture</string> + </element> + <element key="magentoAppArea"> + <string>magentoAppArea</string> + </element> + <element key="magentoAppIsolation"> + <string>magentoAppIsolation</string> + </element> + <element key="magentoCache"> + <string>magentoCache</string> + </element> + <element key="magentoComponentsDir"> + <string>magentoComponentsDir</string> + </element> + <element key="magentoConfigFixture"> + <string>magentoConfigFixture</string> + </element> + <element key="magentoDataFixture"> + <string>magentoDataFixture</string> + </element> + <element key="magentoDataFixtureBeforeTransaction"> + <string>magentoDataFixtureBeforeTransaction</string> + </element> + <element key="magentoDbIsolation"> + <string>magentoDbIsolation</string> + </element> + <element key="magentoIndexerDimensionMode"> + <string>magentoIndexerDimensionMode</string> + </element> + <element key="magentoApiDataFixture"> + <string>magentoApiDataFixture</string> + </element> + <element key="Override"> + <string>Override</string> + </element> + </array> + </arguments> + </listener> </listeners> </phpunit> diff --git a/dev/tests/api-functional/phpunit_soap.xml.dist b/dev/tests/api-functional/phpunit_soap.xml.dist index cc0d55553835..935f5113b67a 100644 --- a/dev/tests/api-functional/phpunit_soap.xml.dist +++ b/dev/tests/api-functional/phpunit_soap.xml.dist @@ -57,5 +57,58 @@ <!-- Test listeners --> <listeners> <listener class="Magento\TestFramework\Event\PhpUnit"/> + <listener class="Yandex\Allure\Adapter\AllureAdapter"> + <arguments> + <string>var/allure-results</string> <!-- XML files output folder --> + <boolean>true</boolean> <!-- Whether to delete previous results on rerun --> + <array> <!-- A list of custom annotations to ignore (optional) --> + <element key="codingStandardsIgnoreStart"> + <string>codingStandardsIgnoreStart</string> + </element> + <element key="codingStandardsIgnoreEnd"> + <string>codingStandardsIgnoreEnd</string> + </element> + <element key="expectedExceptionMessageRegExp"> + <string>expectedExceptionMessageRegExp</string> + </element> + <element key="magentoAdminConfigFixture"> + <string>magentoAdminConfigFixture</string> + </element> + <element key="magentoAppArea"> + <string>magentoAppArea</string> + </element> + <element key="magentoAppIsolation"> + <string>magentoAppIsolation</string> + </element> + <element key="magentoCache"> + <string>magentoCache</string> + </element> + <element key="magentoComponentsDir"> + <string>magentoComponentsDir</string> + </element> + <element key="magentoConfigFixture"> + <string>magentoConfigFixture</string> + </element> + <element key="magentoDataFixture"> + <string>magentoDataFixture</string> + </element> + <element key="magentoDataFixtureBeforeTransaction"> + <string>magentoDataFixtureBeforeTransaction</string> + </element> + <element key="magentoDbIsolation"> + <string>magentoDbIsolation</string> + </element> + <element key="magentoIndexerDimensionMode"> + <string>magentoIndexerDimensionMode</string> + </element> + <element key="magentoApiDataFixture"> + <string>magentoApiDataFixture</string> + </element> + <element key="Override"> + <string>Override</string> + </element> + </array> + </arguments> + </listener> </listeners> </phpunit> diff --git a/dev/tests/api-functional/testsuite/Magento/Analytics/Api/LinkProviderTest.php b/dev/tests/api-functional/testsuite/Magento/Analytics/Api/LinkProviderTest.php index 6fd755167666..4bf1335f2066 100644 --- a/dev/tests/api-functional/testsuite/Magento/Analytics/Api/LinkProviderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Analytics/Api/LinkProviderTest.php @@ -95,6 +95,6 @@ public function testGetAll() */ private function isTestBaseUrlSecure() { - return strpos('https://', TESTS_BASE_URL) !== false; + return strpos(TESTS_BASE_URL, 'https://') !== false; } } diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeOptionManagementInterfaceTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeOptionManagementInterfaceTest.php index f9442d8b6499..1d37ea9a2fc6 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeOptionManagementInterfaceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeOptionManagementInterfaceTest.php @@ -111,6 +111,9 @@ public function addDataProvider() 'option_with_value_node_that_starts_with_a_number' => [ array_merge($optionPayload, [AttributeOptionInterface::VALUE => '123_some_text']) ], + 'option_with_value_node_that_is_a_number' => [ + array_merge($optionPayload, [AttributeOptionInterface::VALUE => '123']) + ], ]; } diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductCustomOptionRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductCustomOptionRepositoryTest.php index e83811042fd8..f3be684f93a4 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductCustomOptionRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductCustomOptionRepositoryTest.php @@ -146,6 +146,10 @@ public function testGetList() public function testSave($optionData) { $productSku = 'simple'; + /** @var \Magento\Catalog\Model\ProductRepository $productRepository */ + $productRepository = $this->objectManager->create( + \Magento\Catalog\Model\ProductRepository::class + ); $optionDataPost = $optionData; $optionDataPost['product_sku'] = $productSku; @@ -162,6 +166,7 @@ public function testSave($optionData) ]; $result = $this->_webApiCall($serviceInfo, ['option' => $optionDataPost]); + $product = $productRepository->get($productSku); unset($result['product_sku']); unset($result['option_id']); if (!empty($result['values'])) { @@ -169,7 +174,12 @@ public function testSave($optionData) unset($result['values'][$key]['option_type_id']); } } + $this->assertEquals($optionData, $result); + $this->assertTrue($product->getHasOptions() == 1); + if ($optionDataPost['is_require']) { + $this->assertTrue($product->getRequiredOptions() == 1); + } } public function optionDataProvider() @@ -180,7 +190,7 @@ public function optionDataProvider() $fixtureOptions[$item['type']] = [ 'optionData' => $item, ]; - }; + } return $fixtureOptions; } @@ -230,7 +240,7 @@ public function optionNegativeDataProvider() $fixtureOptions[$key] = [ 'optionData' => $item, ]; - }; + } return $fixtureOptions; } diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php index 4968e31eb129..3e935e1d7ae9 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php @@ -8,9 +8,9 @@ namespace Magento\Catalog\Api; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\CatalogInventory\Api\Data\StockItemInterface; use Magento\Downloadable\Model\Link; use Magento\Store\Model\Store; -use Magento\CatalogInventory\Api\Data\StockItemInterface; use Magento\Store\Model\Website; use Magento\Store\Model\WebsiteRepository; use Magento\TestFramework\Helper\Bootstrap; @@ -20,8 +20,8 @@ use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\Api\SortOrder; use Magento\Framework\Api\SortOrderBuilder; -use Magento\Framework\Webapi\Exception as HTTPExceptionCodes; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Webapi\Exception as HTTPExceptionCodes; /** * @magentoAppIsolation enabled @@ -815,6 +815,7 @@ public function testGetList() $this->assertTrue(count($response['items']) > 0); $this->assertNotNull($response['items'][0]['sku']); + $this->assertNotNull($response['items'][0][ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY]['website_ids']); $this->assertEquals('simple', $response['items'][0]['sku']); $index = null; @@ -912,6 +913,7 @@ public function testGetListWithFilteringByWebsite() $this->assertTrue(count($response['items']) == 1); $this->assertTrue(isset($response['items'][0]['sku'])); $this->assertEquals('simple-2', $response['items'][0]['sku']); + $this->assertNotNull($response['items'][0][ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY]['website_ids']); } /** @@ -1058,6 +1060,9 @@ public function testGetListWithMultipleFilterGroupsAndSortingAndPagination() $this->assertEquals(3, $searchResult['total_count']); $this->assertEquals(1, count($searchResult['items'])); $this->assertEquals('search_product_4', $searchResult['items'][0][ProductInterface::SKU]); + $this->assertNotNull( + $searchResult['items'][0][ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY]['website_ids'] + ); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductSwatchAttributeOptionManagementInterfaceTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductSwatchAttributeOptionManagementInterfaceTest.php index 6b8388e2f434..237574dd6e22 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductSwatchAttributeOptionManagementInterfaceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductSwatchAttributeOptionManagementInterfaceTest.php @@ -45,6 +45,9 @@ public function testAdd($optionData) $this->assertNotNull($response); $updatedData = $this->getAttributeOptions($testAttributeCode); $lastOption = array_pop($updatedData); + foreach ($updatedData as $option) { + $this->assertNotContains('id', $option['value']); + } $this->assertEquals( $optionData[AttributeOptionInterface::STORE_LABELS][0][AttributeOptionLabelInterface::LABEL], $lastOption['label'] diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductTierPriceManagementTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductTierPriceManagementTest.php index 9562d6618d8d..d5c035733942 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductTierPriceManagementTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductTierPriceManagementTest.php @@ -49,7 +49,7 @@ public function testGetList($customerGroupId, $count, $value, $qty) public function getListDataProvider() { return [ - [0, 2, 5, 3], + [0, 3, 5, 3], [1, 0, null, null], ['all', 2, 8, 2], ]; diff --git a/dev/tests/api-functional/testsuite/Magento/Customer/Api/AccountManagementTest.php b/dev/tests/api-functional/testsuite/Magento/Customer/Api/AccountManagementTest.php index b0d1647d8b83..b2276d79f5ec 100644 --- a/dev/tests/api-functional/testsuite/Magento/Customer/Api/AccountManagementTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Customer/Api/AccountManagementTest.php @@ -213,6 +213,34 @@ public function testCreateCustomerWithErrors() } } + public function testCreateCustomerWithoutOptionalFields() + { + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH, + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'CreateAccount', + ], + ]; + + $customerDataArray = $this->dataObjectProcessor->buildOutputDataArray( + $this->customerHelper->createSampleCustomerDataObject(), + \Magento\Customer\Api\Data\CustomerInterface::class + ); + unset($customerDataArray['store_id']); + unset($customerDataArray['website_id']); + $requestData = ['customer' => $customerDataArray, 'password' => CustomerHelper::PASSWORD]; + try { + $customerData = $this->_webApiCall($serviceInfo, $requestData, null, 'all'); + $this->assertNotNull($customerData['id']); + } catch (\Exception $e) { + $this->fail('Customer should be created without optional fields.'); + } + } + /** * Test customer activation when it is required * diff --git a/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerMetadataTest.php b/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerMetadataTest.php index f2632aa1481e..3b1d43134298 100644 --- a/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerMetadataTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerMetadataTest.php @@ -9,6 +9,7 @@ use Magento\Customer\Api\Data\CustomerInterface as Customer; use Magento\Customer\Model\Data\AttributeMetadata; use Magento\TestFramework\TestCase\WebapiAbstract; +use Magento\TestFramework\Helper\Bootstrap; /** * Class CustomerMetadataTest @@ -19,6 +20,19 @@ class CustomerMetadataTest extends WebapiAbstract const SERVICE_VERSION = "V1"; const RESOURCE_PATH = "/V1/attributeMetadata/customer"; + /** + * @var CustomerMetadataInterface + */ + private $customerMetadata; + + /** + * Execute per test initialization. + */ + public function setUp() + { + $this->customerMetadata = Bootstrap::getObjectManager()->create(CustomerMetadataInterface::class); + } + /** * Test retrieval of attribute metadata for the customer entity type. * @@ -200,8 +214,7 @@ public function testGetCustomAttributesMetadata() $attributeMetadata = $this->_webApiCall($serviceInfo); - // There are no default custom attributes. - $this->assertCount(0, $attributeMetadata); + $this->assertCount(count($this->customerMetadata->getCustomAttributesMetadata()), $attributeMetadata); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/Downloadable/Api/ProductRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Downloadable/Api/ProductRepositoryTest.php index 4608b255459b..769abadf2058 100644 --- a/dev/tests/api-functional/testsuite/Magento/Downloadable/Api/ProductRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Downloadable/Api/ProductRepositoryTest.php @@ -51,11 +51,13 @@ protected function getLinkData() 'link_type' => 'file', 'link_file_content' => [ 'name' => 'link1_content.jpg', + // phpcs:ignore Magento2.Functions.DiscouragedFunction 'file_data' => base64_encode(file_get_contents($this->testImagePath)), ], 'sample_type' => 'file', 'sample_file_content' => [ 'name' => 'link1_sample.jpg', + // phpcs:ignore Magento2.Functions.DiscouragedFunction 'file_data' => base64_encode(file_get_contents($this->testImagePath)), ], ], @@ -114,6 +116,7 @@ protected function getSampleData() 'sample_type' => 'file', 'sample_file_content' => [ 'name' => 'sample2.jpg', + // phpcs:ignore Magento2.Functions.DiscouragedFunction 'file_data' => base64_encode(file_get_contents($this->testImagePath)), ], ], @@ -146,7 +149,9 @@ protected function createDownloadableProduct() "price" => 10, 'attribute_set_id' => 4, "extension_attributes" => [ + // phpcs:ignore Magento2.Functions.DiscouragedFunction "downloadable_product_links" => array_values($this->getLinkData()), + // phpcs:ignore Magento2.Functions.DiscouragedFunction "downloadable_product_samples" => array_values($this->getSampleData()), ], ]; @@ -301,11 +306,13 @@ public function testUpdateDownloadableProductLinksWithNewFile() 'link_type' => 'file', 'link_file_content' => [ 'name' => $linkFile . $extension, + // phpcs:ignore Magento2.Functions.DiscouragedFunction 'file_data' => base64_encode(file_get_contents($this->testImagePath)), ], 'sample_type' => 'file', 'sample_file_content' => [ 'name' => $sampleFile . $extension, + // phpcs:ignore Magento2.Functions.DiscouragedFunction 'file_data' => base64_encode(file_get_contents($this->testImagePath)), ], ]; @@ -319,11 +326,13 @@ public function testUpdateDownloadableProductLinksWithNewFile() 'link_type' => 'file', 'link_file_content' => [ 'name' => 'link2_content.jpg', + // phpcs:ignore Magento2.Functions.DiscouragedFunction 'file_data' => base64_encode(file_get_contents($this->testImagePath)), ], 'sample_type' => 'file', 'sample_file_content' => [ 'name' => 'link2_sample.jpg', + // phpcs:ignore Magento2.Functions.DiscouragedFunction 'file_data' => base64_encode(file_get_contents($this->testImagePath)), ], ]; @@ -463,6 +472,7 @@ public function testUpdateDownloadableProductSamplesWithNewFile() 'sample_type' => 'file', 'sample_file_content' => [ 'name' => 'sample1.jpg', + // phpcs:ignore Magento2.Functions.DiscouragedFunction 'file_data' => base64_encode(file_get_contents($this->testImagePath)), ], ]; @@ -472,6 +482,11 @@ public function testUpdateDownloadableProductSamplesWithNewFile() 'title' => 'sample2_updated', 'sort_order' => 2, 'sample_type' => 'file', + 'sample_file_content' => [ + 'name' => 'sample2.jpg', + // phpcs:ignore Magento2.Functions.DiscouragedFunction + 'file_data' => base64_encode(file_get_contents($this->testImagePath)), + ], ]; $response[ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY]["downloadable_product_samples"] = @@ -606,7 +621,7 @@ protected function deleteProductBySku($productSku) protected function saveProduct($product) { if (isset($product['custom_attributes'])) { - for ($i=0; $i<sizeof($product['custom_attributes']); $i++) { + for ($i = 0, $iMax = count($product['custom_attributes']); $i < $iMax; $i++) { if ($product['custom_attributes'][$i]['attribute_code'] == 'category_ids' && !is_array($product['custom_attributes'][$i]['value']) ) { diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryProductsCountTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryProductsCountTest.php new file mode 100644 index 000000000000..46309c6d97df --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryProductsCountTest.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\Catalog; + +use Magento\Catalog\Api\CategoryLinkManagementInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Attribute\Source\Status as productStatus; +use Magento\Catalog\Model\Product\Visibility; +use Magento\CatalogInventory\Model\Configuration; +use Magento\Config\Model\ResourceModel\Config; +use Magento\Framework\App\Config\ReinitableConfigInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Class CategoryProductsCountTest + * + * Test for Magento\CatalogGraphQl\Model\Resolver\Category\ProductsCount resolver + */ +class CategoryProductsCountTest extends GraphQlAbstract +{ + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var Config $config + */ + private $resourceConfig; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var ReinitableConfigInterface + */ + private $reinitConfig; + + /** + * @var CategoryLinkManagementInterface + */ + private $categoryLinkManagement; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + + $objectManager = ObjectManager::getInstance(); + $this->productRepository = $objectManager->create(ProductRepositoryInterface::class); + $this->resourceConfig = $objectManager->get(Config::class); + $this->scopeConfig = $objectManager->get(ScopeConfigInterface::class); + $this->reinitConfig = $objectManager->get(ReinitableConfigInterface::class); + $this->categoryLinkManagement = $objectManager->get(CategoryLinkManagementInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testCategoryWithSaleableProduct() + { + $categoryId = 2; + + $query = <<<QUERY +{ + category(id: {$categoryId}) { + id + product_count + } +} +QUERY; + $response = $this->graphQlQuery($query); + + self::assertEquals(1, $response['category']['product_count']); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/category_product.php + */ + public function testCategoryWithInvisibleProduct() + { + $categoryId = 333; + $sku = 'simple333'; + + $product = $this->productRepository->get($sku); + $product->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE); + $this->productRepository->save($product); + + $query = <<<QUERY +{ + category(id: {$categoryId}) { + id + product_count + } +} +QUERY; + $response = $this->graphQlQuery($query); + + self::assertEquals(0, $response['category']['product_count']); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_out_of_stock.php + */ + public function testCategoryWithOutOfStockProductManageStockEnabled() + { + $categoryId = 2; + $sku = 'simple-out-of-stock'; + $manageStock = $this->scopeConfig->getValue(Configuration::XML_PATH_MANAGE_STOCK); + + $this->resourceConfig->saveConfig(Configuration::XML_PATH_MANAGE_STOCK, 1); + $this->reinitConfig->reinit(); + + // need to resave product to reindex it with new configuration. + $product = $this->productRepository->get($sku); + $this->productRepository->save($product); + + $query = <<<QUERY +{ + category(id: {$categoryId}) { + id + product_count + } +} +QUERY; + $response = $this->graphQlQuery($query); + + $this->resourceConfig->saveConfig(Configuration::XML_PATH_MANAGE_STOCK, $manageStock); + $this->reinitConfig->reinit(); + + self::assertEquals(0, $response['category']['product_count']); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_out_of_stock.php + */ + public function testCategoryWithOutOfStockProductManageStockDisabled() + { + $categoryId = 2; + $sku = 'simple-out-of-stock'; + $manageStock = $this->scopeConfig->getValue(Configuration::XML_PATH_MANAGE_STOCK); + + $this->resourceConfig->saveConfig(Configuration::XML_PATH_MANAGE_STOCK, 0); + $this->reinitConfig->reinit(); + + // need to resave product to reindex it with new configuration. + $product = $this->productRepository->get($sku); + $this->productRepository->save($product); + + $query = <<<QUERY +{ + category(id: {$categoryId}) { + id + product_count + } +} +QUERY; + $response = $this->graphQlQuery($query); + + $this->resourceConfig->saveConfig(Configuration::XML_PATH_MANAGE_STOCK, $manageStock); + $this->reinitConfig->reinit(); + + self::assertEquals(1, $response['category']['product_count']); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/category_product.php + */ + public function testCategoryWithDisabledProduct() + { + $categoryId = 333; + $sku = 'simple333'; + + $product = $this->productRepository->get($sku); + $product->setStatus(ProductStatus::STATUS_DISABLED); + $this->productRepository->save($product); + + $query = <<<QUERY +{ + category(id: {$categoryId}) { + id + product_count + } +} +QUERY; + $response = $this->graphQlQuery($query); + + self::assertEquals(0, $response['category']['product_count']); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_out_of_stock.php + */ + public function testCategoryWithOutOfStockProductShowOutOfStockProduct() + { + $showOutOfStock = $this->scopeConfig->getValue(Configuration::XML_PATH_SHOW_OUT_OF_STOCK); + + $this->resourceConfig->saveConfig(Configuration::XML_PATH_SHOW_OUT_OF_STOCK, 1); + $this->reinitConfig->reinit(); + + $categoryId = 2; + + $query = <<<QUERY +{ + category(id: {$categoryId}) { + id + product_count + } +} +QUERY; + $response = $this->graphQlQuery($query); + + $this->resourceConfig->saveConfig(Configuration::XML_PATH_SHOW_OUT_OF_STOCK, $showOutOfStock); + $this->reinitConfig->reinit(); + + self::assertEquals(1, $response['category']['product_count']); + } + + /** + * @magentoApiDataFixture Magento/CatalogRule/_files/configurable_product.php + */ + public function testCategoryWithConfigurableChildrenOutOfStock() + { + $categoryId = 2; + + $this->categoryLinkManagement->assignProductToCategories('configurable', [$categoryId]); + + foreach (['simple1', 'simple2'] as $sku) { + $product = $this->productRepository->get($sku); + $product->setStockData(['is_in_stock' => 0]); + $this->productRepository->save($product); + } + + $query = <<<QUERY +{ + category(id: {$categoryId}) { + id + product_count + } +} +QUERY; + $response = $this->graphQlQuery($query); + + self::assertEquals(0, $response['category']['product_count']); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/category_product.php + */ + public function testCategoryWithProductNotAvailableOnWebsite() + { + $product = $this->productRepository->getById(333); + $product->setWebsiteIds([]); + $this->productRepository->save($product); + + $categoryId = 333; + + $query = <<<QUERY +{ + category(id: {$categoryId}) { + id + product_count + } +} +QUERY; + $response = $this->graphQlQuery($query); + + self::assertEquals(0, $response['category']['product_count']); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryProductsVariantsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryProductsVariantsTest.php index 6b57f84e4b9c..f62be7328481 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryProductsVariantsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryProductsVariantsTest.php @@ -18,13 +18,11 @@ class CategoryProductsVariantsTest extends GraphQlAbstract { /** - * * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable.php * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function testGetSimpleProductsFromCategory() { - $query = <<<QUERY { diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php index 8841fab75790..63073a389f27 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php @@ -8,13 +8,18 @@ namespace Magento\GraphQl\Catalog; use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Model\CategoryRepository; use Magento\Catalog\Model\ResourceModel\Category\Collection as CategoryCollection; use Magento\Framework\DataObject; +use Magento\TestFramework\TestCase\GraphQl\ResponseContainsErrorsException; use Magento\TestFramework\TestCase\GraphQlAbstract; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\TestFramework\ObjectManager; +/** + * Test loading of category tree + */ class CategoryTest extends GraphQlAbstract { /** @@ -22,13 +27,18 @@ class CategoryTest extends GraphQlAbstract */ private $objectManager; + /** + * @var CategoryRepository + */ + private $categoryRepository; + protected function setUp() { $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->categoryRepository = $this->objectManager->get(CategoryRepository::class); } /** - * @magentoApiDataFixture Magento/Customer/_files/customer.php * @magentoApiDataFixture Magento/Catalog/_files/categories.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -70,21 +80,12 @@ public function testCategoriesTree() } } QUERY; - - // get customer ID token - /** @var \Magento\Integration\Api\CustomerTokenServiceInterface $customerTokenService */ - $customerTokenService = $this->objectManager->create( - \Magento\Integration\Api\CustomerTokenServiceInterface::class - ); - $customerToken = $customerTokenService->createCustomerAccessToken('customer@example.com', 'password'); - - $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; - $response = $this->graphQlQuery($query, [], '', $headerMap); + $response = $this->graphQlQuery($query); $responseDataObject = new DataObject($response); //Some sort of smoke testing self::assertEquals( - 'Ololo', - $responseDataObject->getData('category/children/7/children/1/description') + 'Its a description of Test Category 1.2', + $responseDataObject->getData('category/children/0/children/1/description') ); self::assertEquals( 'default-category', @@ -99,19 +100,89 @@ public function testCategoriesTree() $responseDataObject->getData('category/children/0/default_sort_by') ); self::assertCount( - 8, + 7, $responseDataObject->getData('category/children') ); self::assertCount( 2, - $responseDataObject->getData('category/children/7/children') + $responseDataObject->getData('category/children/0/children') ); self::assertEquals( - 5, - $responseDataObject->getData('category/children/7/children/1/children/0/id') + 13, + $responseDataObject->getData('category/children/0/children/1/id') ); } + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testCategoriesTreeWithDisabledCategory() + { + $category = $this->categoryRepository->get(3); + $category->setIsActive(false); + $this->categoryRepository->save($category); + + $rootCategoryId = 2; + $query = <<<QUERY +{ + category(id: {$rootCategoryId}) { + id + name + level + description + children { + id + name + productImagePreview: products(pageSize: 1) { + items { + id + } + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + + $this->assertArrayHasKey('category', $response); + $this->assertArrayHasKey('children', $response['category']); + $this->assertSame(6, count($response['category']['children'])); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testGetCategoryById() + { + $categoryId = 13; + $query = <<<QUERY +{ + category(id: {$categoryId}) { + id + name + } +} +QUERY; + $response = $this->graphQlQuery($query); + self::assertEquals('Category 1.2', $response['category']['name']); + self::assertEquals(13, $response['category']['id']); + } + + public function testNonExistentCategoryWithProductCount() + { + $query = <<<QUERY +{ + category(id: 99) { + product_count + } +} +QUERY; + + $this->expectException(ResponseContainsErrorsException::class); + $this->expectExceptionMessage('GraphQL response contains errors: Category doesn\'t exist'); + $this->graphQlQuery($query); + } + /** * @magentoApiDataFixture Magento/Catalog/_files/categories.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) @@ -132,7 +203,9 @@ public function testCategoryProducts() attribute_set_id country_of_manufacture created_at - description + description { + html + } gift_message_available id categories { @@ -141,8 +214,7 @@ public function testCategoryProducts() available_sort_by level } - image - image_label + image { url, label } meta_description meta_keyword meta_title @@ -223,16 +295,16 @@ public function testCategoryProducts() position sku } - short_description + short_description { + html + } sku - small_image - small_image_label + small_image { url, label } + thumbnail { url, label } special_from_date special_price special_to_date swatch_image - thumbnail - thumbnail_label tier_price tier_prices { customer_group_id @@ -258,17 +330,14 @@ public function testCategoryProducts() } } QUERY; - $response = $this->graphQlQuery($query); $this->assertArrayHasKey('products', $response['category']); $this->assertArrayHasKey('total_count', $response['category']['products']); $this->assertGreaterThanOrEqual(1, $response['category']['products']['total_count']); $this->assertEquals(1, $response['category']['products']['page_info']['current_page']); $this->assertEquals(20, $response['category']['products']['page_info']['page_size']); - $this->assertArrayHasKey('sku', $response['category']['products']['items'][0]); $firstProductSku = $response['category']['products']['items'][0]['sku']; - /** * @var ProductRepositoryInterface $productRepository */ @@ -290,9 +359,7 @@ public function testAnchorCategory() /** @var CategoryInterface $category */ $category = $categoryCollection->getFirstItem(); $categoryId = $category->getId(); - $this->assertNotEmpty($categoryId, "Preconditions failed: category is not available."); - $query = <<<QUERY { category(id: {$categoryId}) { @@ -306,7 +373,6 @@ public function testAnchorCategory() } } QUERY; - $response = $this->graphQlQuery($query); $expectedResponse = [ 'category' => [ @@ -330,7 +396,6 @@ public function testAnchorCategory() */ private function assertBaseFields($product, $actualResponse) { - $assertionMap = [ ['response_field' => 'attribute_set_id', 'expected_value' => $product->getAttributeSetId()], ['response_field' => 'created_at', 'expected_value' => $product->getCreatedAt()], @@ -364,7 +429,6 @@ private function assertBaseFields($product, $actualResponse) ['response_field' => 'type_id', 'expected_value' => $product->getTypeId()], ['response_field' => 'updated_at', 'expected_value' => $product->getUpdatedAt()], ]; - $this->assertResponseFields($actualResponse, $assertionMap); } @@ -384,7 +448,6 @@ private function assertWebsites($product, $actualResponse) 'is_default' => true, ] ]; - $this->assertEquals($actualResponse, $assertionMap); } @@ -409,7 +472,6 @@ private function assertAttributes($actualResponse) 'special_from_date', 'special_to_date', ]; - foreach ($eavAttributes as $eavAttribute) { $this->assertArrayHasKey($eavAttribute, $actualResponse); } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryWithDescriptionDirectivesTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryWithDescriptionDirectivesTest.php new file mode 100644 index 000000000000..c115f7124c9f --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryWithDescriptionDirectivesTest.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Catalog; + +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for checking that category description directives are rendered correctly + */ +class CategoryWithDescriptionDirectivesTest extends GraphQlAbstract +{ + /** + * @var \Magento\TestFramework\ObjectManager + */ + private $objectManager; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/category.php + */ + public function testHtmlDirectivesRendered() + { + $categoryId = 333; + $mediaFilePath = '/path/to/mediafile'; + /** @var StoreManagerInterface $storeManager */ + $storeManager = ObjectManager::getInstance()->get(StoreManagerInterface::class); + $storeBaseUrl = $storeManager->getStore()->getBaseUrl(); + + /* Remove index.php from base URL */ + $storeBaseUrlParts = explode('/index.php', $storeBaseUrl); + $storeBaseUrl = $storeBaseUrlParts[0]; + + /** @var CategoryRepositoryInterface $categoryRepository */ + $categoryRepository = ObjectManager::getInstance()->get(CategoryRepositoryInterface::class); + /** @var CategoryInterface $category */ + $category = $categoryRepository->get($categoryId); + $category->setDescription('Test: {{media url="' . $mediaFilePath . '"}}'); + $categoryRepository->save($category); + + $query = <<<QUERY +{ + category(id: {$categoryId}) { + description + } +} +QUERY; + $response = $this->graphQlQuery($query); + + self::assertNotContains('media url', $response['category']['description']); + self::assertContains($storeBaseUrl, $response['category']['description']); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/MediaGalleryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/MediaGalleryTest.php new file mode 100644 index 000000000000..8da2702917af --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/MediaGalleryTest.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Catalog; + +use Magento\TestFramework\TestCase\GraphQlAbstract; + +class MediaGalleryTest extends GraphQlAbstract +{ + /** + * @var \Magento\TestFramework\ObjectManager + */ + private $objectManager; + + protected function setUp() + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_with_image.php + */ + public function testProductSmallImageUrlWithExistingImage() + { + $productSku = 'simple'; + $query = <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) { + items { + small_image { + url + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + + self::assertArrayHasKey('url', $response['products']['items'][0]['small_image']); + self::assertContains('magento_image.jpg', $response['products']['items'][0]['small_image']['url']); + self::assertTrue($this->checkImageExists($response['products']['items'][0]['small_image']['url'])); + } + + /** + * @param string $url + * @return bool + */ + private function checkImageExists(string $url): bool + { + $connection = curl_init($url); + curl_setopt($connection, CURLOPT_HEADER, true); + curl_setopt($connection, CURLOPT_NOBODY, true); + curl_setopt($connection, CURLOPT_RETURNTRANSFER, 1); + curl_exec($connection); + $responseStatus = curl_getinfo($connection, CURLINFO_HTTP_CODE); + + return $responseStatus === 200 ? true : false; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductImageTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductImageTest.php new file mode 100644 index 000000000000..b957292a3ac2 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductImageTest.php @@ -0,0 +1,149 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Catalog; + +use Magento\TestFramework\TestCase\GraphQlAbstract; + +class ProductImageTest extends GraphQlAbstract +{ + /** + * @var \Magento\TestFramework\ObjectManager + */ + private $objectManager; + + protected function setUp() + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_with_image.php + */ + public function testProductWithBaseImage() + { + $productSku = 'simple'; + $query = <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) { + items { + image { + url + label + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + + self::assertContains('magento_image.jpg', $response['products']['items'][0]['image']['url']); + self::assertTrue($this->checkImageExists($response['products']['items'][0]['image']['url'])); + self::assertEquals('Image Alt Text', $response['products']['items'][0]['image']['label']); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testProductWithoutBaseImage() + { + $productSku = 'simple'; + $query = <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) { + items { + image { + 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'] + ); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_with_image.php + */ + public function testProductWithSmallImage() + { + $productSku = 'simple'; + $query = <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) { + items { + small_image { + url + label + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + + self::assertContains('magento_image.jpg', $response['products']['items'][0]['small_image']['url']); + self::assertTrue($this->checkImageExists($response['products']['items'][0]['small_image']['url'])); + self::assertEquals('Image Alt Text', $response['products']['items'][0]['small_image']['label']); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_with_image.php + */ + public function testProductWithThumbnail() + { + $productSku = 'simple'; + $query = <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) { + items { + thumbnail { + url + label + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + + self::assertContains('magento_image.jpg', $response['products']['items'][0]['thumbnail']['url']); + self::assertTrue($this->checkImageExists($response['products']['items'][0]['thumbnail']['url'])); + self::assertEquals('Image Alt Text', $response['products']['items'][0]['thumbnail']['label']); + } + + /** + * @param string $url + * @return bool + */ + private function checkImageExists(string $url): bool + { + $connection = curl_init($url); + curl_setopt($connection, CURLOPT_HEADER, true); + curl_setopt($connection, CURLOPT_NOBODY, true); + curl_setopt($connection, CURLOPT_RETURNTRANSFER, 1); + curl_exec($connection); + $responseStatus = curl_getinfo($connection, CURLINFO_HTTP_CODE); + + return $responseStatus === 200 ? true : false; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductInMultipleStoresTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductInMultipleStoresTest.php index 2ef081ebcfa9..d17b434f39d9 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductInMultipleStoresTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductInMultipleStoresTest.php @@ -10,10 +10,13 @@ use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\GraphQlAbstract; +/** + * Class ProductInMultipleStoresTest + */ class ProductInMultipleStoresTest extends GraphQlAbstract { - /** + * Test a product from a specific and a default store * * @magentoApiDataFixture Magento/Store/_files/second_store.php * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php @@ -94,7 +97,7 @@ public function testProductFromSpecificAndDefaultStore() $nonExistingStoreCode = "non_existent_store"; $headerMapInvalidStoreCode = ['Store' => $nonExistingStoreCode]; $this->expectException(\Exception::class); - $this->expectExceptionMessage('Store code non_existent_store does not exist'); + $this->expectExceptionMessage('Requested store is not found'); $this->graphQlQuery($query, [], '', $headerMapInvalidStoreCode); } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php index f1a79490b3dc..99de6088b19a 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -381,84 +381,11 @@ public function testSearchWithFilterWithPageSizeEqualTotalCount() } QUERY; $this->expectException(\Exception::class); - $this->expectExceptionMessage('GraphQL response contains errors: currentPage value 1 specified is greater ' . - 'than the number of pages available.'); + $this->expectExceptionMessage('GraphQL response contains errors: currentPage value 2 specified is greater ' . + 'than the 1 page(s) available'); $this->graphQlQuery($query); } - /** - * The query returns a total_count of 2 records; setting the pageSize = 1 and currentPage2 - * Expected result is to get the second product on the list on the second page - * - * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php - */ - public function testSearchWithFilterPageSizeLessThanCurrentPage() - { - - $query - = <<<QUERY -{ - products( - search : "simple" - filter: - { - special_price:{neq:"null"} - price:{lt:"60"} - or: - { - sku:{like:"%simple%"} - name:{like:"%configurable%"} - } - weight:{eq:"1"} - } - pageSize:1 - currentPage:2 - sort: - { - price:DESC - } - ) - { - items - { - sku - price { - minimalPrice { - amount { - value - currency - } - } - } - name - ... on PhysicalProductInterface { - weight - } - type_id - attribute_set_id - } - total_count - page_info - { - page_size - } - } -} -QUERY; - /** - * @var ProductRepositoryInterface $productRepository - */ - $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); - // when pagSize =1 and currentPage = 2, it should have simple2 on first page and simple1 on 2nd page - // since sorting is done on price in the DESC order - $product = $productRepository->get('simple1'); - $filteredProducts = [$product]; - - $response = $this->graphQlQuery($query); - $this->assertEquals(1, $response['products']['total_count']); - $this->assertProductItems($filteredProducts, $response); - } - /** * Requesting for items that match a specific SKU or NAME within a certain price range sorted by Price in ASC order * @@ -549,12 +476,11 @@ public function testQueryProductsInCurrentPageSortedByPriceASC() } /** - * Verify the items in the second page is correct after sorting their name in ASC order + * Verify the items is correct after sorting their name in ASC order * * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products_2.php - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testFilterProductsInNextPageSortedByNameASC() + public function testQueryProductsSortedByNameASC() { $query = <<<QUERY @@ -562,44 +488,27 @@ public function testFilterProductsInNextPageSortedByNameASC() products( filter: { - price:{gt: "5", lt: "50"} - or: - { - sku:{eq:"simple1"} - name:{like:"configurable%"} - } + sku:{in:["simple2", "simple1"]} } - pageSize:4 + pageSize:1 currentPage:2 sort: { - name:ASC + name:ASC } ) { items { sku - price { - minimalPrice { - amount { - value - currency - } - } - } name - type_id - ... on PhysicalProductInterface { - weight - } - attribute_set_id - } - total_count - page_info - { + } + total_count + page_info + { page_size - } + current_page + } } } QUERY; @@ -607,13 +516,15 @@ public function testFilterProductsInNextPageSortedByNameASC() * @var ProductRepositoryInterface $productRepository */ $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); - $product = $productRepository->get('simple1'); - $filteredProducts = [$product]; + $product = $productRepository->get('simple2'); $response = $this->graphQlQuery($query); - $this->assertEquals(1, $response['products']['total_count']); - $this->assertProductItems($filteredProducts, $response); - $this->assertEquals(4, $response['products']['page_info']['page_size']); + $this->assertEquals(2, $response['products']['total_count']); + $this->assertEquals(['page_size' => 1, 'current_page' => 2], $response['products']['page_info']); + $this->assertEquals( + [['sku' => $product->getSku(), 'name' => $product->getName()]], + $response['products']['items'] + ); } /** @@ -1132,8 +1043,8 @@ public function testQueryPageOutOfBoundException() QUERY; $this->expectException(\Exception::class); - $this->expectExceptionMessage('GraphQL response contains errors: currentPage value 1 specified is greater ' . - 'than the number of pages available.'); + $this->expectExceptionMessage('GraphQL response contains errors: currentPage value 2 specified is greater ' . + 'than the 1 page(s) available.'); $this->graphQlQuery($query); } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductTextAttributesTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductTextAttributesTest.php new file mode 100644 index 000000000000..999e1cc7fca3 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductTextAttributesTest.php @@ -0,0 +1,142 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Catalog; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +class ProductTextAttributesTest extends GraphQlAbstract +{ + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + protected function setUp() + { + $this->productRepository = Bootstrap::getObjectManager()::getInstance()->get(ProductRepositoryInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testProductTextAttributes() + { + $productSku = 'simple'; + + $query = <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) + { + items { + sku + description { + html + } + short_description { + html + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + + $this->assertEquals( + $productSku, + $response['products']['items'][0]['sku'] + ); + $this->assertEquals( + 'Short description', + $response['products']['items'][0]['short_description']['html'] + ); + $this->assertEquals( + 'Description with <b>html tag</b>', + $response['products']['items'][0]['description']['html'] + ); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + */ + public function testProductWithoutFilledTextAttributes() + { + $productSku = 'virtual-product'; + + $query = <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) + { + items { + sku + description { + html + } + short_description { + html + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + + $this->assertEquals( + $productSku, + $response['products']['items'][0]['sku'] + ); + $this->assertEquals( + '', + $response['products']['items'][0]['short_description']['html'] + ); + $this->assertEquals( + '', + $response['products']['items'][0]['description']['html'] + ); + } + + /** + * Test for checking that product fields with directives allowed are rendered correctly + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @magentoApiDataFixture Magento/Cms/_files/block.php + */ + public function testHtmlDirectivesRendering() + { + $productSku = 'simple'; + $cmsBlockId = 'fixture_block'; + $assertionCmsBlockText = 'Fixture Block Title'; + + $product = $this->productRepository->get($productSku, false, null, true); + $product->setDescription('Test: {{block id="' . $cmsBlockId . '"}}'); + $product->setShortDescription('Test: {{block id="' . $cmsBlockId . '"}}'); + $this->productRepository->save($product); + + $query = <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) { + items { + description { + html + } + short_description { + html + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + + self::assertContains($assertionCmsBlockText, $response['products']['items'][0]['description']['html']); + self::assertNotContains('{{block id', $response['products']['items'][0]['description']['html']); + self::assertContains($assertionCmsBlockText, $response['products']['items'][0]['short_description']['html']); + self::assertNotContains('{{block id', $response['products']['items'][0]['short_description']['html']); + } +} 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 34cebde64d03..e517b22ad09d 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php @@ -44,7 +44,6 @@ public function testQueryAllFieldsSimpleProduct() attribute_set_id country_of_manufacture created_at - description gift_message_available id categories { @@ -53,8 +52,7 @@ public function testQueryAllFieldsSimpleProduct() available_sort_by level } - image - image_label + image { url, label } meta_description meta_keyword meta_title @@ -92,6 +90,7 @@ public function testQueryAllFieldsSimpleProduct() title required sort_order + option_id ... on CustomizableFieldOption { product_sku field_option: value { @@ -138,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 { @@ -203,16 +222,13 @@ public function testQueryAllFieldsSimpleProduct() position sku } - short_description sku - small_image - small_image_label + small_image{ url, label } + thumbnail { url, label } special_from_date special_price special_to_date - swatch_image - thumbnail - thumbnail_label + swatch_image tier_price tier_prices { @@ -285,7 +301,6 @@ public function testQueryAllFieldsSimpleProduct() */ public function testQueryMediaGalleryEntryFieldsSimpleProduct() { - $this->markTestSkipped("Skipped until ticket MAGETWO-90021 is resolved."); $productSku = 'simple'; $query = <<<QUERY @@ -300,11 +315,9 @@ public function testQueryMediaGalleryEntryFieldsSimpleProduct() } country_of_manufacture created_at - description gift_message_available id - image - image_label + image {url, label} meta_description meta_keyword meta_title @@ -342,6 +355,7 @@ public function testQueryMediaGalleryEntryFieldsSimpleProduct() title required sort_order + option_id ... on CustomizableFieldOption { product_sku field_option: value { @@ -451,16 +465,13 @@ public function testQueryMediaGalleryEntryFieldsSimpleProduct() position sku } - short_description sku - small_image - small_image_label + small_image { url, label } special_from_date special_price special_to_date swatch_image - thumbnail - thumbnail_label + thumbnail { url, label } tier_price tier_prices { @@ -484,7 +495,7 @@ public function testQueryMediaGalleryEntryFieldsSimpleProduct() QUERY; $response = $this->graphQlQuery($query); - + /** * @var ProductRepositoryInterface $productRepository */ @@ -745,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; } @@ -760,11 +771,12 @@ private function assertOptions($product, $actualResponse) $assertionMap = [ ['response_field' => 'sort_order', 'expected_value' => $option->getSortOrder()], ['response_field' => 'title', 'expected_value' => $option->getTitle()], - ['response_field' => 'required', 'expected_value' => $option->getIsRequire()] + ['response_field' => 'required', 'expected_value' => $option->getIsRequire()], + ['response_field' => 'option_id', 'expected_value' => $option->getOptionId()] ]; 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()); @@ -784,7 +796,7 @@ private function assertOptions($product, $actualResponse) ['response_field' => 'product_sku', 'expected_value' => $option->getProductSku()], ] ); - $valueKeyName = ""; + if ($option->getType() === 'file') { $valueKeyName = 'file_option'; $valueAssertionMap = [ @@ -915,11 +927,9 @@ private function assertEavAttributes($product, $actualResponse) { $eavAttributes = [ 'url_key', - 'description', 'meta_description', 'meta_keyword', 'meta_title', - 'short_description', 'country_of_manufacture', 'gift_message_available', 'news_from_date', diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/UrlRewritesTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/UrlRewritesTest.php new file mode 100644 index 000000000000..43796d780646 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/UrlRewritesTest.php @@ -0,0 +1,153 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Catalog; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\UrlRewrite\Model\UrlFinderInterface; +use Magento\UrlRewrite\Service\V1\Data\UrlRewrite as UrlRewriteDTO; + +/** + * Test of getting URL rewrites data from products + */ +class UrlRewritesTest extends GraphQlAbstract +{ + /** + * + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function testProductWithNoCategoriesAssigned() + { + $productSku = 'virtual-product'; + $query + = <<<QUERY +{ + products (filter: {sku: {eq: "{$productSku}"}}) { + items { + name, + sku, + description { + html + } + url_rewrites { + url, + parameters { + name, + value + } + } + } + } +} +QUERY; + + $response = $this->graphQlQuery($query); + + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); + $product = $productRepository->get('virtual-product', false, null, true); + + $urlFinder = ObjectManager::getInstance()->get(UrlFinderInterface::class); + + $rewritesCollection = $urlFinder->findAllByData([UrlRewriteDTO::ENTITY_ID => $product->getId()]); + + /* There should be only one rewrite */ + /** @var UrlRewriteDTO $urlRewrite */ + $urlRewrite = current($rewritesCollection); + + $this->assertArrayHasKey('url_rewrites', $response['products']['items'][0]); + $this->assertCount(1, $response['products']['items'][0]['url_rewrites']); + + $this->assertResponseFields( + $response['products']['items'][0]['url_rewrites'][0], + [ + "url" => $urlRewrite->getRequestPath(), + "parameters" => $this->getUrlParameters($urlRewrite->getTargetPath()) + ] + ); + } + + /** + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function testProductWithOneCategoryAssigned() + { + $productSku = 'simple'; + $query + = <<<QUERY +{ + products (filter: {sku: {eq: "{$productSku}"}}) { + items { + name, + sku, + description { + html + } + url_rewrites { + url, + parameters { + name, + value + } + } + } + } +} +QUERY; + + $response = $this->graphQlQuery($query); + + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); + $product = $productRepository->get('simple', false, null, true); + + $urlFinder = ObjectManager::getInstance()->get(UrlFinderInterface::class); + + $rewritesCollection = $urlFinder->findAllByData([UrlRewriteDTO::ENTITY_ID => $product->getId()]); + $rewritesCount = count($rewritesCollection); + + $this->assertArrayHasKey('url_rewrites', $response['products']['items'][0]); + $this->assertCount($rewritesCount, $response['products']['items'][0]['url_rewrites']); + + for ($i = 0; $i < $rewritesCount; $i++) { + $urlRewrite = $rewritesCollection[$i]; + $this->assertResponseFields( + $response['products']['items'][0]['url_rewrites'][$i], + [ + "url" => $urlRewrite->getRequestPath(), + "parameters" => $this->getUrlParameters($urlRewrite->getTargetPath()) + ] + ); + } + } + + /** + * Parses target path and extracts parameters + * + * @param string $targetPath + * @return array + */ + private function getUrlParameters(string $targetPath): array + { + $urlParameters = []; + $targetPathParts = explode('/', trim($targetPath, '/')); + + for ($i = 3; ($i < sizeof($targetPathParts) - 1); $i += 2) { + $urlParameters[] = [ + 'name' => $targetPathParts[$i], + 'value' => $targetPathParts[$i + 1] + ]; + } + + return $urlParameters; + } +} 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 000000000000..6f27693eeb4a --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/AddProductToCartTest.php @@ -0,0 +1,134 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\CatalogInventory; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Add simple product to cart testcases related to inventory + */ +class AddProductToCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/products.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + * @expectedException \Exception + * @expectedExceptionMessage The requested qty is not available + */ + public function testAddProductIfQuantityIsNotAvailable() + { + $sku = 'simple'; + $quantity = 200; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $query = $this->getQuery($maskedQuoteId, $sku, $quantity); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/products.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + * @magentoConfigFixture default cataloginventory/item_options/max_sale_qty 5 + * @expectedException \Exception + * @expectedExceptionMessage The most you may purchase is 5. + */ + public function testAddMoreProductsThatAllowed() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/167'); + + $sku = 'custom-design-simple-product'; + $quantity = 7; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $query = $this->getQuery($maskedQuoteId, $sku, $quantity); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/products.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + * @expectedException \Exception + * @expectedExceptionMessage Please enter a number greater than 0 in this field. + */ + public function testAddSimpleProductToCartWithNegativeQuantity() + { + $sku = 'simple'; + $quantity = -2; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $query = $this->getQuery($maskedQuoteId, $sku, $quantity); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + */ + public function testAddProductIfQuantityIsDecimal() + { + $sku = 'simple_product'; + $quantity = 0.2; + + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $sku, $quantity); + + $this->expectExceptionMessage( + "Could not add the product with SKU {$sku} to the shopping cart: The fewest you may purchase is 1" + ); + $this->graphQlMutation($query); + } + + /** + * @param string $maskedQuoteId + * @param string $sku + * @param float $quantity + * @return string + */ + private function getQuery(string $maskedQuoteId, string $sku, float $quantity) : string + { + return <<<QUERY +mutation { + addSimpleProductsToCart( + input: { + cart_id: "{$maskedQuoteId}", + cartItems: [ + { + data: { + quantity: $quantity + sku: "$sku" + } + } + ] + } + ) { + cart { + items { + quantity + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/ProductOnlyXLeftInStockTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/ProductOnlyXLeftInStockTest.php new file mode 100644 index 000000000000..3969c758f12d --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/ProductOnlyXLeftInStockTest.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\CatalogInventory; + +use Magento\TestFramework\TestCase\GraphQlAbstract; + +class ProductOnlyXLeftInStockTest extends GraphQlAbstract +{ + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_all_fields.php + */ + public function testQueryProductOnlyXLeftInStockDisabled() + { + $productSku = 'simple'; + + $query = <<<QUERY + { + products(filter: {sku: {eq: "{$productSku}"}}) + { + items { + only_x_left_in_stock + } + } + } +QUERY; + + $response = $this->graphQlQuery($query); + + $this->assertArrayHasKey(0, $response['products']['items']); + $this->assertArrayHasKey('only_x_left_in_stock', $response['products']['items'][0]); + $this->assertNull($response['products']['items'][0]['only_x_left_in_stock']); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_all_fields.php + * @magentoConfigFixture default_store cataloginventory/options/stock_threshold_qty 120 + */ + public function testQueryProductOnlyXLeftInStockEnabled() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/167'); + $productSku = 'simple'; + + $query = <<<QUERY + { + products(filter: {sku: {eq: "{$productSku}"}}) + { + items { + only_x_left_in_stock + } + } + } +QUERY; + $response = $this->graphQlQuery($query); + + $this->assertArrayHasKey(0, $response['products']['items']); + $this->assertArrayHasKey('only_x_left_in_stock', $response['products']['items'][0]); + $this->assertEquals(100, $response['products']['items'][0]['only_x_left_in_stock']); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/ProductStockStatusTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/ProductStockStatusTest.php new file mode 100644 index 000000000000..c5571a4a338c --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/ProductStockStatusTest.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\CatalogInventory; + +use Magento\CatalogInventory\Api\Data\StockStatusInterface; +use Magento\CatalogInventory\Api\StockRegistryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +class ProductStockStatusTest extends GraphQlAbstract +{ + /** + * @var StockRegistryInterface + */ + private $stockRegistry; + + protected function setUp() + { + $this->stockRegistry = Bootstrap::getObjectManager()->create(StockRegistryInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_all_fields.php + */ + public function testQueryProductStockStatusInStock() + { + $productSku = 'simple'; + + $query = <<<QUERY + { + products(filter: {sku: {eq: "{$productSku}"}}) + { + items { + stock_status + } + } + } +QUERY; + + $response = $this->graphQlQuery($query); + + $this->assertArrayHasKey(0, $response['products']['items']); + $this->assertArrayHasKey('stock_status', $response['products']['items'][0]); + $this->assertEquals('IN_STOCK', $response['products']['items'][0]['stock_status']); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_all_fields.php + * @magentoConfigFixture default_store cataloginventory/options/show_out_of_stock 1 + */ + public function testQueryProductStockStatusOutOfStock() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/167'); + $productSku = 'simple'; + + $query = <<<QUERY + { + products(filter: {sku: {eq: "{$productSku}"}}) + { + items { + stock_status + } + } + } +QUERY; + + $stockItem = $this->stockRegistry->getStockItemBySku($productSku); + $stockItem->setQty(0); + $this->stockRegistry->updateStockItemBySku($productSku, $stockItem); + + $response = $this->graphQlQuery($query); + + $this->assertArrayHasKey(0, $response['products']['items']); + $this->assertArrayHasKey('stock_status', $response['products']['items'][0]); + $this->assertEquals('OUT_OF_STOCK', $response['products']['items'][0]['stock_status']); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/UpdateCartItemsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/UpdateCartItemsTest.php new file mode 100644 index 000000000000..3b238f864163 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogInventory/UpdateCartItemsTest.php @@ -0,0 +1,86 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\CatalogInventory; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\GraphQl\Quote\GetQuoteItemIdByReservedQuoteIdAndSku; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for updating/removing shopping cart items + */ +class UpdateCartItemsTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var GetQuoteItemIdByReservedQuoteIdAndSku + */ + private $getQuoteItemIdByReservedQuoteIdAndSku; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->getQuoteItemIdByReservedQuoteIdAndSku = $objectManager->get( + GetQuoteItemIdByReservedQuoteIdAndSku::class + ); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testUpdateCartItemDecimalQuantity() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $itemId = $this->getQuoteItemIdByReservedQuoteIdAndSku->execute('test_quote', 'simple_product'); + + $quantity = 0.5; + $this->expectExceptionMessage( + "Could not update the product with SKU simple_product: The fewest you may purchase is 1" + ); + $query = $this->getQuery($maskedQuoteId, $itemId, $quantity); + $this->graphQlMutation($query); + } + + /** + * @param string $maskedQuoteId + * @param int $itemId + * @param float $quantity + * @return string + */ + private function getQuery(string $maskedQuoteId, int $itemId, float $quantity): string + { + return <<<QUERY +mutation { + updateCartItems(input: { + cart_id: "{$maskedQuoteId}" + cart_items:[ + { + cart_item_id: {$itemId} + quantity: {$quantity} + } + ] + }) { + cart { + items { + id + quantity + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Cms/CmsBlockTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Cms/CmsBlockTest.php new file mode 100644 index 000000000000..57f526b1cb2f --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Cms/CmsBlockTest.php @@ -0,0 +1,170 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Cms; + +use Magento\Cms\Api\BlockRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQl\ResponseContainsErrorsException; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Widget\Model\Template\FilterEmulate; + +class CmsBlockTest extends GraphQlAbstract +{ + /** + * @var BlockRepositoryInterface + */ + private $blockRepository; + + /** + * @var FilterEmulate + */ + private $filterEmulate; + + protected function setUp() + { + $this->blockRepository = Bootstrap::getObjectManager()->get(BlockRepositoryInterface::class); + $this->filterEmulate = Bootstrap::getObjectManager()->get(FilterEmulate::class); + } + + /** + * Verify the fields of CMS Block selected by identifiers + * + * @magentoApiDataFixture Magento/Cms/_files/blocks.php + */ + public function testGetCmsBlock() + { + $cmsBlock = $this->blockRepository->getById('enabled_block'); + $cmsBlockData = $cmsBlock->getData(); + $renderedContent = $this->filterEmulate->setUseSessionInUrl(false)->filter($cmsBlock->getContent()); + + $query = + <<<QUERY +{ + cmsBlocks(identifiers: "enabled_block") { + items { + identifier + title + content + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + + self::assertArrayHasKey('cmsBlocks', $response); + self::assertArrayHasKey('items', $response['cmsBlocks']); + + self::assertEquals($cmsBlockData['identifier'], $response['cmsBlocks']['items'][0]['identifier']); + self::assertEquals($cmsBlockData['title'], $response['cmsBlocks']['items'][0]['title']); + self::assertEquals($renderedContent, $response['cmsBlocks']['items'][0]['content']); + } + + /** + * Verify the message when CMS Block is disabled + * + * @expectedException \Exception + * @expectedExceptionMessage The CMS block with the "disabled_block" ID doesn't exist + * + * @magentoApiDataFixture Magento/Cms/_files/blocks.php + */ + public function testGetDisabledCmsBlock() + { + $query = + <<<QUERY +{ + cmsBlocks(identifiers: "disabled_block") { + items { + identifier + title + content + } + } +} +QUERY; + $this->graphQlQuery($query); + } + + /** + * Verify the message when identifiers were not specified + * + * @expectedException \Exception + * @expectedExceptionMessage "identifiers" of CMS blocks should be specified + */ + public function testGetCmsBlocksWithoutIdentifiers() + { + $query = + <<<QUERY +{ + cmsBlocks(identifiers: []) { + items { + identifier + title + content + } + } +} +QUERY; + $this->graphQlQuery($query); + } + + /** + * Verify the message when CMS Block with such identifiers does not exist + * + * @expectedException \Exception + * @expectedExceptionMessage The CMS block with the "nonexistent_id" ID doesn't exist. + */ + public function testGetCmsBlockByNonExistentIdentifier() + { + $query = + <<<QUERY +{ + cmsBlocks(identifiers: "nonexistent_id") { + items { + identifier + title + content + } + } +} +QUERY; + $this->graphQlQuery($query); + } + + /** + * Verify the fields of CMS Block selected by identifiers + * + * @magentoApiDataFixture Magento/Cms/_files/blocks.php + */ + public function testGetEnabledAndDisabledCmsBlockInOneRequest() + { + $query = + <<<QUERY +{ + cmsBlocks(identifiers: ["enabled_block", "disabled_block"]) { + items { + identifier + } + } +} +QUERY; + + try { + $this->graphQlQuery($query); + self::fail('Response should contains errors.'); + } catch (ResponseContainsErrorsException $e) { + $responseData = $e->getResponseData(); + } + + self::assertNotEmpty($responseData); + self::assertEquals('enabled_block', $responseData['data']['cmsBlocks']['items'][0]['identifier']); + self::assertEquals( + 'The CMS block with the "disabled_block" ID doesn\'t exist.', + $responseData['errors'][0]['message'] + ); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Cms/CmsPageTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Cms/CmsPageTest.php new file mode 100644 index 000000000000..86145fafa62f --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Cms/CmsPageTest.php @@ -0,0 +1,133 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Cms; + +use Magento\Cms\Model\GetPageByIdentifier; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +class CmsPageTest extends GraphQlAbstract +{ + /** + * Verify the fields of CMS Page selected by page_id + * + * @magentoApiDataFixture Magento/Cms/_files/pages.php + */ + public function testGetCmsPageById() + { + $cmsPage = ObjectManager::getInstance()->get(GetPageByIdentifier::class)->execute('page100', 0); + $pageId = $cmsPage->getPageId(); + $cmsPageData = $cmsPage->getData(); + $query = + <<<QUERY +{ + cmsPage(id: $pageId) { + url_key + title + content + content_heading + page_layout + meta_title + meta_description + meta_keywords + } +} +QUERY; + + $response = $this->graphQlQuery($query); + $this->assertEquals($cmsPageData['identifier'], $response['cmsPage']['url_key']); + $this->assertEquals($cmsPageData['title'], $response['cmsPage']['title']); + $this->assertEquals($cmsPageData['content'], $response['cmsPage']['content']); + $this->assertEquals($cmsPageData['content_heading'], $response['cmsPage']['content_heading']); + $this->assertEquals($cmsPageData['page_layout'], $response['cmsPage']['page_layout']); + $this->assertEquals($cmsPageData['meta_title'], $response['cmsPage']['meta_title']); + $this->assertEquals($cmsPageData['meta_description'], $response['cmsPage']['meta_description']); + $this->assertEquals($cmsPageData['meta_keywords'], $response['cmsPage']['meta_keywords']); + } + + /** + * Verify the message when page_id is not specified. + */ + public function testGetCmsPageWithoutId() + { + $query = + <<<QUERY +{ + cmsPage { + url_key + title + content + content_heading + page_layout + meta_title + meta_description + meta_keywords + } +} +QUERY; + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Page id should be specified'); + $this->graphQlQuery($query); + } + + /** + * Verify the message when page_id does not exist. + */ + public function testGetCmsPageByNonExistentId() + { + $query = + <<<QUERY +{ + cmsPage(id: 0) { + url_key + title + content + content_heading + page_layout + meta_title + meta_description + meta_keywords + } +} +QUERY; + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('The CMS page with the "0" ID doesn\'t exist.'); + $this->graphQlQuery($query); + } + + /** + * Verify the message when CMS Page selected by page_id is disabled + * + * @magentoApiDataFixture Magento/Cms/_files/noroute.php + */ + public function testGetDisabledCmsPageById() + { + $cmsPageId = ObjectManager::getInstance()->get(GetPageByIdentifier::class)->execute('no-route', 0)->getPageId(); + $query = + <<<QUERY +{ + cmsPage(id: $cmsPageId) { + url_key + title + content + content_heading + page_layout + meta_title + meta_description + meta_keywords + } +} +QUERY; + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('No such entity.'); + $this->graphQlQuery($query); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartTest.php new file mode 100644 index 000000000000..6810f9d44ce9 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartTest.php @@ -0,0 +1,126 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\ConfigurableProduct; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Add configurable product to cart testcases + */ +class AddConfigurableProductToCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/multiple_mixed_products.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddConfigurableProductToCart() + { + $variantSku = 'simple_41'; + $quantity = 2; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $query = $this->getQuery($maskedQuoteId, $variantSku, $quantity); + $response = $this->graphQlMutation($query); + + $cartItems = $response['addConfigurableProductsToCart']['cart']['items']; + self::assertEquals($quantity, $cartItems[0]['quantity']); + 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'; + $quantity = 200; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $query = $this->getQuery($maskedQuoteId, $variantSku, $quantity); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Framework/Search/_files/product_configurable.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + * @expectedException \Exception + * @expectedExceptionMessage Product that you are trying to add is not available. + */ + public function testAddOutOfStockProduct() + { + $variantSku = 'simple_1010'; + $quantity = 1; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $query = $this->getQuery($maskedQuoteId, $variantSku, $quantity); + $this->graphQlMutation($query); + } + + /** + * @param string $maskedQuoteId + * @param string $variantSku + * @param int $quantity + * @return string + */ + private function getQuery(string $maskedQuoteId, string $variantSku, int $quantity): string + { + return <<<QUERY +mutation { + addConfigurableProductsToCart( + input: { + cart_id: "{$maskedQuoteId}" + cartItems: [ + { + variant_sku: "{$variantSku}" + data: { + quantity: {$quantity} + sku: "{$variantSku}" + } + } + ] + } + ) { + cart { + items { + id + quantity + product { + name + sku + } + ... on ConfigurableCartItem { + configurable_options { + option_label + } + } + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductFrontendLabelAttributeTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductFrontendLabelAttributeTest.php new file mode 100644 index 000000000000..32cd3a9a51dc --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductFrontendLabelAttributeTest.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\ConfigurableProduct; + +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Class ConfigurableProductFrontendLabelAttributeTest + * + * @package Magento\GraphQl\ConfigurableProduct + */ +class ConfigurableProductFrontendLabelAttributeTest extends GraphQlAbstract +{ + /** + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable_with_frontend_label_attribute.php + */ + public function testGetFrontendLabelAttribute() + { + $expectLabelValue = 'Default Store View label'; + $productSku = 'configurable'; + + $query = <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) { + items { + name + ... on ConfigurableProduct{ + configurable_options{ + id + label + } + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + + $this->assertArrayHasKey('products', $response); + $this->assertArrayHasKey('items', $response['products']); + $this->assertArrayHasKey(0, $response['products']['items']); + + $product = $response['products']['items'][0]; + $this->assertArrayHasKey('configurable_options', $product); + $this->assertArrayHasKey(0, $product['configurable_options']); + $this->assertArrayHasKey('label', $product['configurable_options'][0]); + + $option = $product['configurable_options'][0]; + $this->assertEquals($expectLabelValue, $option['label']); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductViewTest.php index 735ae7fff646..da5410384627 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductViewTest.php @@ -204,7 +204,6 @@ public function testQueryConfigurableProductLinks() /** * @var ProductRepositoryInterface $productRepository */ - $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); $product = $productRepository->get($productSku, false, null, true); @@ -407,6 +406,7 @@ private function assertConfigurableVariants($actualResponse) $variantArray['product']['price'] ); $configurableOptions = $this->getConfigurableOptions(); + $this->assertEquals(1, count($variantArray['attributes'])); foreach ($variantArray['attributes'] as $attribute) { $hasAssertion = false; foreach ($configurableOptions as $option) { diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ChangeCustomerPasswordTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ChangeCustomerPasswordTest.php new file mode 100644 index 000000000000..ab6a1d4b464d --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ChangeCustomerPasswordTest.php @@ -0,0 +1,143 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer; + +use Magento\Customer\Api\AccountManagementInterface; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Framework\Exception\LocalizedException; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test change customer password + */ +class ChangeCustomerPasswordTest extends GraphQlAbstract +{ + /** + * @var AccountManagementInterface + */ + private $accountManagement; + + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var CustomerRegistry + */ + private $customerRegistry; + + protected function setUp() + { + $this->customerTokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); + $this->accountManagement = Bootstrap::getObjectManager()->get(AccountManagementInterface::class); + $this->customerRegistry = Bootstrap::getObjectManager()->get(CustomerRegistry::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testChangePassword() + { + $customerEmail = 'customer@example.com'; + $oldCustomerPassword = 'password'; + $newCustomerPassword = 'anotherPassword1'; + + $query = $this->getChangePassQuery($oldCustomerPassword, $newCustomerPassword); + $headerMap = $this->getCustomerAuthHeaders($customerEmail, $oldCustomerPassword); + + $response = $this->graphQlMutation($query, [], '', $headerMap); + $this->assertEquals($customerEmail, $response['changeCustomerPassword']['email']); + + try { + // registry contains the old password hash so needs to be reset + $this->customerRegistry->removeByEmail($customerEmail); + $this->accountManagement->authenticate($customerEmail, $newCustomerPassword); + } catch (LocalizedException $e) { + $this->fail('Password was not changed: ' . $e->getMessage()); + } + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage The current customer isn't authorized. + */ + public function testChangePasswordIfUserIsNotAuthorizedTest() + { + $query = $this->getChangePassQuery('currentpassword', 'newpassword'); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testChangeWeakPassword() + { + $customerEmail = 'customer@example.com'; + $oldCustomerPassword = 'password'; + $newCustomerPassword = 'weakpass'; + + $query = $this->getChangePassQuery($oldCustomerPassword, $newCustomerPassword); + $headerMap = $this->getCustomerAuthHeaders($customerEmail, $oldCustomerPassword); + + $this->expectException(\Exception::class); + $this->expectExceptionMessageRegExp('/Minimum of different classes of characters in password is.*/'); + + $this->graphQlMutation($query, [], '', $headerMap); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @expectedException \Exception + * @expectedExceptionMessage Invalid login or password. + */ + public function testChangePasswordIfPasswordIsInvalid() + { + $customerEmail = 'customer@example.com'; + $oldCustomerPassword = 'password'; + $newCustomerPassword = 'anotherPassword1'; + $incorrectPassword = 'password-incorrect'; + + $query = $this->getChangePassQuery($incorrectPassword, $newCustomerPassword); + + $headerMap = $this->getCustomerAuthHeaders($customerEmail, $oldCustomerPassword); + $this->graphQlMutation($query, [], '', $headerMap); + } + + private function getChangePassQuery($currentPassword, $newPassword) + { + $query = <<<QUERY +mutation { + changeCustomerPassword( + currentPassword: "$currentPassword", + newPassword: "$newPassword" + ) { + id + email + firstname + lastname + } +} +QUERY; + + return $query; + } + + /** + * @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/Customer/CreateCustomerAddressTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerAddressTest.php new file mode 100644 index 000000000000..891c74ca3c1e --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerAddressTest.php @@ -0,0 +1,316 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer; + +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Api\Data\AddressInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Integration\Api\CustomerTokenServiceInterface; + +/** + * Create customer address tests + */ +class CreateCustomerAddressTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var AddressRepositoryInterface + */ + private $addressRepository; + + protected function setUp() + { + parent::setUp(); + + $this->customerTokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); + $this->addressRepository = Bootstrap::getObjectManager()->get(AddressRepositoryInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer_without_addresses.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCreateCustomerAddress() + { + $customerId = 1; + $newAddress = [ + 'region' => [ + 'region' => 'Arizona', + 'region_id' => 4, + 'region_code' => 'AZ' + ], + 'country_id' => 'US', + 'street' => ['Line 1 Street', 'Line 2'], + 'company' => 'Company name', + 'telephone' => '123456789', + 'fax' => '123123123', + 'postcode' => '7777', + 'city' => 'City Name', + 'firstname' => 'Adam', + 'lastname' => 'Phillis', + 'middlename' => 'A', + 'prefix' => 'Mr.', + 'suffix' => 'Jr.', + 'vat_id' => '1', + 'default_shipping' => true, + 'default_billing' => false + ]; + + $mutation + = <<<MUTATION +mutation { + createCustomerAddress(input: { + region: { + region: "{$newAddress['region']['region']}" + region_id: {$newAddress['region']['region_id']} + region_code: "{$newAddress['region']['region_code']}" + } + country_id: {$newAddress['country_id']} + street: ["{$newAddress['street'][0]}","{$newAddress['street'][1]}"] + company: "{$newAddress['company']}" + telephone: "{$newAddress['telephone']}" + fax: "{$newAddress['fax']}" + postcode: "{$newAddress['postcode']}" + city: "{$newAddress['city']}" + firstname: "{$newAddress['firstname']}" + lastname: "{$newAddress['lastname']}" + middlename: "{$newAddress['middlename']}" + prefix: "{$newAddress['prefix']}" + suffix: "{$newAddress['suffix']}" + vat_id: "{$newAddress['vat_id']}" + default_shipping: true + default_billing: false + }) { + id + customer_id + region { + region + region_id + region_code + } + country_id + street + company + telephone + fax + postcode + city + firstname + lastname + middlename + prefix + suffix + vat_id + default_shipping + default_billing + } +} +MUTATION; + + $userName = 'customer@example.com'; + $password = 'password'; + + $response = $this->graphQlMutation($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); + $this->assertArrayHasKey('createCustomerAddress', $response); + $this->assertArrayHasKey('customer_id', $response['createCustomerAddress']); + $this->assertEquals($customerId, $response['createCustomerAddress']['customer_id']); + $this->assertArrayHasKey('id', $response['createCustomerAddress']); + + $address = $this->addressRepository->getById($response['createCustomerAddress']['id']); + $this->assertEquals($address->getId(), $response['createCustomerAddress']['id']); + $this->assertCustomerAddressesFields($address, $response['createCustomerAddress']); + $this->assertCustomerAddressesFields($address, $newAddress); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage The current customer isn't authorized. + */ + public function testCreateCustomerAddressIfUserIsNotAuthorized() + { + $mutation + = <<<MUTATION +mutation{ + createCustomerAddress(input: { + prefix: "Mr." + firstname: "John" + middlename: "A" + lastname: "Smith" + telephone: "123456789" + street: ["Line 1", "Line 2"] + city: "Test City" + region: { + region_id: 1 + } + country_id: US + postcode: "9999" + default_shipping: true + default_billing: false + }) { + id + } +} +MUTATION; + $this->graphQlMutation($mutation); + } + + /** + * Verify customers with valid credentials create new address + * with missing required Firstname attribute + * + * @magentoApiDataFixture Magento/Customer/_files/customer_without_addresses.php + * @expectedException \Exception + * @expectedExceptionMessage Required parameters are missing: firstname + */ + public function testCreateCustomerAddressWithMissingAttribute() + { + $mutation + = <<<MUTATION +mutation { + createCustomerAddress(input: { + region: { + region_id: 1 + } + country_id: US + street: ["Line 1 Street","Line 2"] + company: "Company name" + telephone: "123456789" + fax: "123123123" + postcode: "7777" + city: "City Name" + firstname: "" + lastname: "Phillis" + }) { + id + } +} +MUTATION; + + $userName = 'customer@example.com'; + $password = 'password'; + $this->graphQlMutation($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer_without_addresses.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCreateCustomerAddressWithRedundantStreetLine() + { + $newAddress = [ + 'region' => [ + 'region' => 'Arizona', + 'region_id' => 4, + 'region_code' => 'AZ' + ], + 'country_id' => 'US', + 'street' => ['Line 1 Street', 'Line 2', 'Line 3'], + 'company' => 'Company name', + 'telephone' => '123456789', + 'fax' => '123123123', + 'postcode' => '7777', + 'city' => 'City Name', + 'firstname' => 'Adam', + 'lastname' => 'Phillis', + 'middlename' => 'A', + 'prefix' => 'Mr.', + 'suffix' => 'Jr.', + 'vat_id' => '1', + 'default_shipping' => true, + 'default_billing' => false + ]; + + $mutation + = <<<MUTATION +mutation { + createCustomerAddress(input: { + region: { + region: "{$newAddress['region']['region']}" + region_id: {$newAddress['region']['region_id']} + region_code: "{$newAddress['region']['region_code']}" + } + country_id: {$newAddress['country_id']} + street: ["{$newAddress['street'][0]}","{$newAddress['street'][1]}","{$newAddress['street'][2]}"] + company: "{$newAddress['company']}" + telephone: "{$newAddress['telephone']}" + fax: "{$newAddress['fax']}" + postcode: "{$newAddress['postcode']}" + city: "{$newAddress['city']}" + firstname: "{$newAddress['firstname']}" + lastname: "{$newAddress['lastname']}" + middlename: "{$newAddress['middlename']}" + prefix: "{$newAddress['prefix']}" + suffix: "{$newAddress['suffix']}" + vat_id: "{$newAddress['vat_id']}" + default_shipping: true + default_billing: false + }) { + id + } +} +MUTATION; + + $userName = 'customer@example.com'; + $password = 'password'; + + self::expectExceptionMessage('"Street Address" cannot contain more than 2 lines.'); + $this->graphQlMutation($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); + } + + /** + * Verify the fields for Customer address + * + * @param AddressInterface $address + * @param array $actualResponse + */ + private function assertCustomerAddressesFields(AddressInterface $address, array $actualResponse): void + { + /** @var $addresses */ + $assertionMap = [ + ['response_field' => 'country_id', 'expected_value' => $address->getCountryId()], + ['response_field' => 'street', 'expected_value' => $address->getStreet()], + ['response_field' => 'company', 'expected_value' => $address->getCompany()], + ['response_field' => 'telephone', 'expected_value' => $address->getTelephone()], + ['response_field' => 'fax', 'expected_value' => $address->getFax()], + ['response_field' => 'postcode', 'expected_value' => $address->getPostcode()], + ['response_field' => 'city', 'expected_value' => $address->getCity()], + ['response_field' => 'firstname', 'expected_value' => $address->getFirstname()], + ['response_field' => 'lastname', 'expected_value' => $address->getLastname()], + ['response_field' => 'middlename', 'expected_value' => $address->getMiddlename()], + ['response_field' => 'prefix', 'expected_value' => $address->getPrefix()], + ['response_field' => 'suffix', 'expected_value' => $address->getSuffix()], + ['response_field' => 'vat_id', 'expected_value' => $address->getVatId()], + ['response_field' => 'default_shipping', 'expected_value' => (bool)$address->isDefaultShipping()], + ['response_field' => 'default_billing', 'expected_value' => (bool)$address->isDefaultBilling()], + ]; + $this->assertResponseFields($actualResponse, $assertionMap); + $this->assertTrue(is_array([$actualResponse['region']]), "region field must be of an array type."); + $assertionRegionMap = [ + ['response_field' => 'region', 'expected_value' => $address->getRegion()->getRegion()], + ['response_field' => 'region_code', 'expected_value' => $address->getRegion()->getRegionCode()], + ['response_field' => 'region_id', 'expected_value' => $address->getRegion()->getRegionId()] + ]; + $this->assertResponseFields($actualResponse['region'], $assertionRegionMap); + } + + /** + * @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/Customer/CreateCustomerTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerTest.php new file mode 100644 index 000000000000..fc51f57a83a7 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerTest.php @@ -0,0 +1,260 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for create customer functionallity + */ +class CreateCustomerTest extends GraphQlAbstract +{ + /** + * @var Registry + */ + private $registry; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + protected function setUp() + { + parent::setUp(); + + $this->registry = Bootstrap::getObjectManager()->get(Registry::class); + $this->customerRepository = Bootstrap::getObjectManager()->get(CustomerRepositoryInterface::class); + } + + /** + * @throws \Exception + */ + public function testCreateCustomerAccountWithPassword() + { + $newFirstname = 'Richard'; + $newLastname = 'Rowe'; + $currentPassword = 'test123#'; + $newEmail = 'new_customer@example.com'; + + $query = <<<QUERY +mutation { + createCustomer( + input: { + firstname: "{$newFirstname}" + lastname: "{$newLastname}" + email: "{$newEmail}" + password: "{$currentPassword}" + is_subscribed: true + } + ) { + customer { + id + firstname + lastname + email + is_subscribed + } + } +} +QUERY; + $response = $this->graphQlMutation($query); + + $this->assertEquals($newFirstname, $response['createCustomer']['customer']['firstname']); + $this->assertEquals($newLastname, $response['createCustomer']['customer']['lastname']); + $this->assertEquals($newEmail, $response['createCustomer']['customer']['email']); + $this->assertEquals(true, $response['createCustomer']['customer']['is_subscribed']); + } + + /** + * @throws \Exception + */ + public function testCreateCustomerAccountWithoutPassword() + { + $newFirstname = 'Richard'; + $newLastname = 'Rowe'; + $newEmail = 'new_customer@example.com'; + + $query = <<<QUERY +mutation { + createCustomer( + input: { + firstname: "{$newFirstname}" + lastname: "{$newLastname}" + email: "{$newEmail}" + is_subscribed: true + } + ) { + customer { + id + firstname + lastname + email + is_subscribed + } + } +} +QUERY; + $response = $this->graphQlMutation($query); + + $this->assertEquals($newFirstname, $response['createCustomer']['customer']['firstname']); + $this->assertEquals($newLastname, $response['createCustomer']['customer']['lastname']); + $this->assertEquals($newEmail, $response['createCustomer']['customer']['email']); + $this->assertEquals(true, $response['createCustomer']['customer']['is_subscribed']); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage "input" value should be specified + */ + public function testCreateCustomerIfInputDataIsEmpty() + { + $query = <<<QUERY +mutation { + createCustomer( + input: { + + } + ) { + customer { + id + firstname + lastname + email + is_subscribed + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage The customer email is missing. Enter and try again. + */ + public function testCreateCustomerIfEmailMissed() + { + $newFirstname = 'Richard'; + $newLastname = 'Rowe'; + $currentPassword = 'test123#'; + + $query = <<<QUERY +mutation { + createCustomer( + input: { + firstname: "{$newFirstname}" + lastname: "{$newLastname}" + password: "{$currentPassword}" + is_subscribed: true + } + ) { + customer { + id + firstname + lastname + email + is_subscribed + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage "Email" is not a valid email address. + */ + public function testCreateCustomerIfEmailIsNotValid() + { + $newFirstname = 'Richard'; + $newLastname = 'Rowe'; + $currentPassword = 'test123#'; + $newEmail = 'email'; + + $query = <<<QUERY +mutation { + createCustomer( + input: { + firstname: "{$newFirstname}" + lastname: "{$newLastname}" + email: "{$newEmail}" + password: "{$currentPassword}" + is_subscribed: true + } + ) { + customer { + id + firstname + lastname + email + is_subscribed + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Field "test123" is not defined by type CustomerInput. + */ + public function testCreateCustomerIfPassedAttributeDosNotExistsInCustomerInput() + { + $newFirstname = 'Richard'; + $newLastname = 'Rowe'; + $currentPassword = 'test123#'; + $newEmail = 'new_customer@example.com'; + + $query = <<<QUERY +mutation { + createCustomer( + input: { + firstname: "{$newFirstname}" + lastname: "{$newLastname}" + test123: "123test123" + email: "{$newEmail}" + password: "{$currentPassword}" + is_subscribed: true + } + ) { + customer { + id + firstname + lastname + email + is_subscribed + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + public function tearDown() + { + $newEmail = 'new_customer@example.com'; + try { + $customer = $this->customerRepository->get($newEmail); + } catch (\Exception $exception) { + return; + } + + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', true); + $this->customerRepository->delete($customer); + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', false); + parent::tearDown(); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CustomerAuthenticationTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CustomerAuthenticationTest.php deleted file mode 100644 index 88ce7e91d94b..000000000000 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CustomerAuthenticationTest.php +++ /dev/null @@ -1,167 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\GraphQl\Customer; - -use Magento\Customer\Api\CustomerRepositoryInterface; -use Magento\Customer\Api\Data\AddressInterface; -use Magento\Customer\Api\Data\CustomerInterface; -use Magento\TestFramework\ObjectManager; -use Magento\TestFramework\TestCase\GraphQlAbstract; -use Magento\Integration\Api\CustomerTokenServiceInterface; - -class CustomerAuthenticationTest extends GraphQlAbstract -{ - /** - * Verify customers with valid credentials with a customer bearer token - * - * @magentoApiDataFixture Magento/Customer/_files/customer.php - * @magentoApiDataFixture Magento/Customer/_files/customer_two_addresses.php - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testRegisteredCustomerWithValidCredentials() - { - $query - = <<<QUERY -{ - customer - { - created_at - group_id - prefix - firstname - middlename - lastname - suffix - email - default_billing - default_shipping - id - addresses{ - id - customer_id - region_id - country_id - telephone - postcode - city - firstname - lastname - } - } -} -QUERY; - - $userName = 'customer@example.com'; - $password = 'password'; - /** @var CustomerTokenServiceInterface $customerTokenService */ - $customerTokenService = ObjectManager::getInstance() - ->get(\Magento\Integration\Api\CustomerTokenServiceInterface::class); - $customerToken = $customerTokenService->createCustomerAccessToken($userName, $password); - $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; - /** @var CustomerRepositoryInterface $customerRepository */ - $customerRepository = ObjectManager::getInstance()->get(CustomerRepositoryInterface::class); - $customer = $customerRepository->get('customer@example.com'); - - $response = $this->graphQlQuery($query, [], '', $headerMap); - $this->assertArrayHasKey('customer', $response); - $this->assertArrayHasKey('addresses', $response['customer']); - $this->assertTrue( - is_array([$response['customer']['addresses']]), - " Addresses field must be of an array type." - ); - $this->assertCustomerFields($customer, $response['customer']); - $this->assertCustomerAddressesFields($customer, $response); - } - - /** - * Verify customer with valid credentials but without the bearer token - * - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testCustomerWithValidCredentialsWithoutToken() - { - $query - = <<<QUERY -{ - customer - { - created_at - group_id - prefix - firstname - middlename - lastname - suffix - email - default_billing - default_shipping - id - } -} -QUERY; - - $this->expectException(\Exception::class); - $this->expectExceptionMessage('GraphQL response contains errors: Current customer' . ' ' . - 'does not have access to the resource "customer"'); - $this->graphQlQuery($query); - } - - /** - * Verify the all the whitelisted fields for a Customer Object - * - * @param CustomerInterface $customer - * @param $actualResponse - */ - public function assertCustomerFields($customer, $actualResponse) - { - // ['customer_object_field_name', 'expected_value'] - $assertionMap = [ - ['response_field' => 'id', 'expected_value' => $customer->getId()], - ['response_field' => 'created_at', 'expected_value' => $customer->getCreatedAt()], - ['response_field' => 'group_id', 'expected_value' => $customer->getGroupId()], - ['response_field' => 'prefix', 'expected_value' => $customer->getPrefix()], - ['response_field' => 'firstname', 'expected_value' => $customer->getFirstname()], - ['response_field' => 'middlename', 'expected_value' => $customer->getMiddlename()], - ['response_field' => 'lastname', 'expected_value' => $customer->getLastname()], - ['response_field' => 'suffix', 'expected_value' => $customer->getSuffix()], - ['response_field' => 'email', 'expected_value' => $customer->getEmail()], - ['response_field' => 'default_shipping', 'expected_value' => (bool)$customer->getDefaultShipping()], - ['response_field' => 'default_billing', 'expected_value' => (bool)$customer->getDefaultBilling()], - ['response_field' => 'id', 'expected_value' => $customer->getId()] - ]; - - $this->assertResponseFields($actualResponse, $assertionMap); - } - - /** - * Verify the fields for CustomerAddress object - * - * @param CustomerInterface $customer - * @param array $actualResponse - */ - public function assertCustomerAddressesFields($customer, $actualResponse) - { - /** @var AddressInterface $addresses */ - $addresses = $customer->getAddresses(); - foreach ($addresses as $addressKey => $addressValue) { - $this->assertNotEmpty($addressValue); - $assertionMap = [ - ['response_field' => 'id', 'expected_value' => $addresses[$addressKey]->getId()], - ['response_field' => 'customer_id', 'expected_value' => $addresses[$addressKey]->getCustomerId()], - ['response_field' => 'region_id', 'expected_value' => $addresses[$addressKey]->getRegionId()], - ['response_field' => 'country_id', 'expected_value' => $addresses[$addressKey]->getCountryId()], - ['response_field' => 'telephone', 'expected_value' => $addresses[$addressKey]->getTelephone()], - ['response_field' => 'postcode', 'expected_value' => $addresses[$addressKey]->getPostcode()], - ['response_field' => 'city', 'expected_value' => $addresses[$addressKey]->getCity()], - ['response_field' => 'firstname', 'expected_value' => $addresses[$addressKey]->getFirstname()], - ['response_field' => 'lastname', 'expected_value' => $addresses[$addressKey]->getLastname()] - ]; - $this->assertResponseFields($actualResponse['customer']['addresses'][$addressKey], $assertionMap); - } - } -} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/DeleteCustomerAddressTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/DeleteCustomerAddressTest.php new file mode 100644 index 000000000000..bdfd428a78c2 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/DeleteCustomerAddressTest.php @@ -0,0 +1,163 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Integration\Api\CustomerTokenServiceInterface; + +/** + * Delete customer address tests + */ +class DeleteCustomerAddressTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var AddressRepositoryInterface + */ + private $addressRepository; + + protected function setUp() + { + parent::setUp(); + + $this->customerTokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); + $this->customerRepository = Bootstrap::getObjectManager()->get(CustomerRepositoryInterface::class); + $this->addressRepository = Bootstrap::getObjectManager()->get(AddressRepositoryInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Customer/_files/customer_two_addresses.php + */ + public function testDeleteCustomerAddress() + { + $userName = 'customer@example.com'; + $password = 'password'; + $addressId = 2; + + $mutation + = <<<MUTATION +mutation { + deleteCustomerAddress(id: {$addressId}) +} +MUTATION; + $response = $this->graphQlMutation($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); + $this->assertArrayHasKey('deleteCustomerAddress', $response); + $this->assertEquals(true, $response['deleteCustomerAddress']); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage The current customer isn't authorized. + */ + public function testDeleteCustomerAddressIfUserIsNotAuthorized() + { + $addressId = 1; + $mutation + = <<<MUTATION +mutation { + deleteCustomerAddress(id: {$addressId}) +} +MUTATION; + $this->graphQlMutation($mutation); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Customer/_files/customer_two_addresses.php + * + * @expectedException \Exception + * @expectedExceptionMessage Customer Address 2 is set as default shipping address and can not be deleted + */ + public function testDeleteDefaultShippingCustomerAddress() + { + $userName = 'customer@example.com'; + $password = 'password'; + $addressId = 2; + + $address = $this->addressRepository->getById($addressId); + $address->setIsDefaultShipping(true); + $this->addressRepository->save($address); + + $mutation + = <<<MUTATION +mutation { + deleteCustomerAddress(id: {$addressId}) +} +MUTATION; + $this->graphQlMutation($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Customer/_files/customer_two_addresses.php + * + * @expectedException \Exception + * @expectedExceptionMessage Customer Address 2 is set as default billing address and can not be deleted + */ + public function testDeleteDefaultBillingCustomerAddress() + { + $userName = 'customer@example.com'; + $password = 'password'; + $addressId = 2; + + $address = $this->addressRepository->getById($addressId); + $address->setIsDefaultBilling(true); + $this->addressRepository->save($address); + + $mutation + = <<<MUTATION +mutation { + deleteCustomerAddress(id: {$addressId}) +} +MUTATION; + $this->graphQlMutation($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * + * @expectedException \Exception + * @expectedExceptionMessage Could not find a address with ID "9999" + */ + public function testDeleteNonExistCustomerAddress() + { + $userName = 'customer@example.com'; + $password = 'password'; + $mutation + = <<<MUTATION +mutation { + deleteCustomerAddress(id: 9999) +} +MUTATION; + $this->graphQlMutation($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); + } + + /** + * @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/Customer/GenerateCustomerTokenTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GenerateCustomerTokenTest.php new file mode 100644 index 000000000000..88eaeaa8f9dd --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GenerateCustomerTokenTest.php @@ -0,0 +1,71 @@ +<?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; +use PHPUnit\Framework\TestResult; + +/** + * Class GenerateCustomerTokenTest + * @package Magento\GraphQl\Customer + */ +class GenerateCustomerTokenTest extends GraphQlAbstract +{ + /** + * Verify customer token with valid credentials + * + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testGenerateCustomerValidToken() + { + $userName = 'customer@example.com'; + $password = 'password'; + + $mutation + = <<<MUTATION +mutation { + generateCustomerToken( + email: "{$userName}" + password: "{$password}" + ) { + token + } +} +MUTATION; + + $response = $this->graphQlMutation($mutation); + $this->assertArrayHasKey('generateCustomerToken', $response); + $this->assertInternalType('array', $response['generateCustomerToken']); + } + + /** + * Verify customer with invalid credentials + */ + public function testGenerateCustomerTokenWithInvalidCredentials() + { + $userName = 'customer@example.com'; + $password = 'bad-password'; + + $mutation + = <<<MUTATION +mutation { + generateCustomerToken( + email: "{$userName}" + password: "{$password}" + ) { + token + } +} +MUTATION; + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('GraphQL response contains errors: The account sign-in' . ' ' . + 'was incorrect or your account is disabled temporarily. Please wait and try again later.'); + $this->graphQlMutation($mutation); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GetAddressesTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GetAddressesTest.php new file mode 100644 index 000000000000..53eb80335ff2 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GetAddressesTest.php @@ -0,0 +1,93 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\AddressInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Integration\Api\CustomerTokenServiceInterface; + +class GetAddressesTest extends GraphQlAbstract +{ + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Customer/_files/customer_two_addresses.php + */ + public function testGetCustomerWithAddresses() + { + $query + = <<<QUERY +{ + customer { + id + addresses { + id + customer_id + region_id + country_id + telephone + postcode + city + firstname + lastname + } + } +} +QUERY; + + $userName = 'customer@example.com'; + $password = 'password'; + /** @var CustomerTokenServiceInterface $customerTokenService */ + $customerTokenService = ObjectManager::getInstance() + ->get(\Magento\Integration\Api\CustomerTokenServiceInterface::class); + $customerToken = $customerTokenService->createCustomerAccessToken($userName, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + /** @var CustomerRepositoryInterface $customerRepository */ + $customerRepository = ObjectManager::getInstance()->get(CustomerRepositoryInterface::class); + $customer = $customerRepository->get('customer@example.com'); + + $response = $this->graphQlQuery($query, [], '', $headerMap); + $this->assertArrayHasKey('customer', $response); + $this->assertArrayHasKey('addresses', $response['customer']); + $this->assertTrue( + is_array([$response['customer']['addresses']]), + " Addresses field must be of an array type." + ); + self::assertEquals($customer->getId(), $response['customer']['id']); + $this->assertCustomerAddressesFields($customer, $response); + } + + /** + * Verify the fields for CustomerAddress object + * + * @param CustomerInterface $customer + * @param array $actualResponse + */ + public function assertCustomerAddressesFields($customer, $actualResponse) + { + /** @var AddressInterface $addresses */ + $addresses = $customer->getAddresses(); + foreach ($addresses as $addressKey => $addressValue) { + $this->assertNotEmpty($addressValue); + $assertionMap = [ + ['response_field' => 'id', 'expected_value' => $addresses[$addressKey]->getId()], + ['response_field' => 'customer_id', 'expected_value' => $addresses[$addressKey]->getCustomerId()], + ['response_field' => 'region_id', 'expected_value' => $addresses[$addressKey]->getRegionId()], + ['response_field' => 'country_id', 'expected_value' => $addresses[$addressKey]->getCountryId()], + ['response_field' => 'telephone', 'expected_value' => $addresses[$addressKey]->getTelephone()], + ['response_field' => 'postcode', 'expected_value' => $addresses[$addressKey]->getPostcode()], + ['response_field' => 'city', 'expected_value' => $addresses[$addressKey]->getCity()], + ['response_field' => 'firstname', 'expected_value' => $addresses[$addressKey]->getFirstname()], + ['response_field' => 'lastname', 'expected_value' => $addresses[$addressKey]->getLastname()] + ]; + $this->assertResponseFields($actualResponse['customer']['addresses'][$addressKey], $assertionMap); + } + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GetCustomerTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GetCustomerTest.php new file mode 100644 index 000000000000..928a263e8531 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GetCustomerTest.php @@ -0,0 +1,129 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer; + +use Magento\Customer\Model\CustomerAuthUpdate; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +class GetCustomerTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var CustomerRegistry + */ + private $customerRegistry; + + /** + * @var CustomerAuthUpdate + */ + private $customerAuthUpdate; + + protected function setUp() + { + parent::setUp(); + + $this->customerTokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); + $this->customerRegistry = Bootstrap::getObjectManager()->get(CustomerRegistry::class); + $this->customerAuthUpdate = Bootstrap::getObjectManager()->get(CustomerAuthUpdate::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testGetCustomer() + { + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + + $query = <<<QUERY +query { + customer { + firstname + lastname + email + } +} +QUERY; + $response = $this->graphQlQuery($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + + $this->assertEquals('John', $response['customer']['firstname']); + $this->assertEquals('Smith', $response['customer']['lastname']); + $this->assertEquals($currentEmail, $response['customer']['email']); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage The current customer isn't authorized. + */ + public function testGetCustomerIfUserIsNotAuthorized() + { + $query = <<<QUERY +query { + customer { + firstname + lastname + email + } +} +QUERY; + $this->graphQlQuery($query); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @expectedException \Exception + * @expectedExceptionMessage The account is locked. + */ + public function testGetCustomerIfAccountIsLocked() + { + $this->lockCustomer(1); + + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + + $query = <<<QUERY +query { + customer { + firstname + lastname + email + } +} +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]; + } + + /** + * @param int $customerId + * @return void + */ + private function lockCustomer(int $customerId): void + { + $customerSecure = $this->customerRegistry->retrieveSecureData($customerId); + $customerSecure->setLockExpires('2030-12-31 00:00:00'); + $this->customerAuthUpdate->saveAuth($customerId); + } +} 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 000000000000..37693fbba7fe --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/IsEmailAvailableTest.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer; + +use Magento\TestFramework\TestCase\GraphQlAbstract; + +class IsEmailAvailableTest extends GraphQlAbstract +{ + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testEmailNotAvailable() + { + $query = + <<<QUERY +{ + isEmailAvailable(email: "customer@example.com") { + is_email_available + } +} +QUERY; + $response = $this->graphQlQuery($query); + + self::assertArrayHasKey('isEmailAvailable', $response); + self::assertArrayHasKey('is_email_available', $response['isEmailAvailable']); + self::assertFalse($response['isEmailAvailable']['is_email_available']); + } + + public function testEmailAvailable() + { + $query = + <<<QUERY +{ + isEmailAvailable(email: "customer@example.com") { + is_email_available + } +} +QUERY; + $response = $this->graphQlQuery($query); + + self::assertArrayHasKey('isEmailAvailable', $response); + self::assertArrayHasKey('is_email_available', $response['isEmailAvailable']); + self::assertTrue($response['isEmailAvailable']['is_email_available']); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/RevokeCustomerTokenTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/RevokeCustomerTokenTest.php new file mode 100644 index 000000000000..fc0c02bae550 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/RevokeCustomerTokenTest.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer; + +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for revoke customer token mutation + */ +class RevokeCustomerTokenTest extends GraphQlAbstract +{ + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testRevokeCustomerTokenValidCredentials() + { + $query = <<<QUERY + mutation { + revokeCustomerToken { + result + } + } +QUERY; + + $userName = 'customer@example.com'; + $password = 'password'; + /** @var CustomerTokenServiceInterface $customerTokenService */ + $customerTokenService = ObjectManager::getInstance()->get(CustomerTokenServiceInterface::class); + $customerToken = $customerTokenService->createCustomerAccessToken($userName, $password); + + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + $response = $this->graphQlMutation($query, [], '', $headerMap); + $this->assertTrue($response['revokeCustomerToken']['result']); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage The current customer isn't authorized. + */ + public function testRevokeCustomerTokenForGuestCustomer() + { + $query = <<<QUERY + mutation { + revokeCustomerToken { + result + } + } +QUERY; + $this->graphQlMutation($query, [], ''); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/SubscriptionStatusTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/SubscriptionStatusTest.php new file mode 100644 index 000000000000..2b54c97cd1e9 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/SubscriptionStatusTest.php @@ -0,0 +1,142 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer; + +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\Newsletter\Model\SubscriberFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Tests for subscription status + */ +class SubscriptionStatusTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var SubscriberFactory + */ + private $subscriberFactory; + + protected function setUp() + { + parent::setUp(); + + $this->customerTokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); + $this->subscriberFactory = Bootstrap::getObjectManager()->get(SubscriberFactory::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testGetSubscriptionStatusTest() + { + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + + $query = <<<QUERY +query { + customer { + is_subscribed + } +} +QUERY; + $response = $this->graphQlQuery($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + $this->assertFalse($response['customer']['is_subscribed']); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage The current customer isn't authorized. + */ + public function testGetSubscriptionStatusIfUserIsNotAuthorizedTest() + { + $query = <<<QUERY +query { + customer { + is_subscribed + } +} +QUERY; + $this->graphQlQuery($query); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testChangeSubscriptionStatusTest() + { + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + + $query = <<<QUERY +mutation { + updateCustomer( + input: { + is_subscribed: true + } + ) { + customer { + is_subscribed + } + } +} +QUERY; + $response = $this->graphQlMutation( + $query, + [], + '', + $this->getCustomerAuthHeaders($currentEmail, $currentPassword) + ); + $this->assertTrue($response['updateCustomer']['customer']['is_subscribed']); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage The current customer isn't authorized. + */ + public function testChangeSubscriptionStatuIfUserIsNotAuthorizedTest() + { + $query = <<<QUERY +mutation { + updateCustomer( + input: { + is_subscribed: true + } + ) { + customer { + is_subscribed + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * @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]; + } + + protected function tearDown() + { + parent::tearDown(); + + $this->subscriberFactory->create()->loadByCustomerId(1)->delete(); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerAddressTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerAddressTest.php new file mode 100644 index 000000000000..e7a7eda2897b --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerAddressTest.php @@ -0,0 +1,242 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Api\Data\AddressInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Integration\Api\CustomerTokenServiceInterface; + +/** + * Update customer address tests + */ +class UpdateCustomerAddressTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var AddressRepositoryInterface + */ + private $addressRepository; + + protected function setUp() + { + parent::setUp(); + + $this->customerTokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); + $this->customerRepository = Bootstrap::getObjectManager()->get(CustomerRepositoryInterface::class); + $this->addressRepository = Bootstrap::getObjectManager()->get(AddressRepositoryInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Customer/_files/customer_address.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testUpdateCustomerAddress() + { + $userName = 'customer@example.com'; + $password = 'password'; + $customerId = 1; + $addressId = 1; + + $updateAddress = [ + 'region' => [ + 'region' => 'Alaska', + 'region_id' => 2, + 'region_code' => 'AK' + ], + 'country_id' => 'US', + 'street' => ['Line 1 Street', 'Line 2'], + 'company' => 'Company Name', + 'telephone' => '123456789', + 'fax' => '123123123', + 'postcode' => '7777', + 'city' => 'City Name', + 'firstname' => 'Adam', + 'lastname' => 'Phillis', + 'middlename' => 'A', + 'prefix' => 'Mr.', + 'suffix' => 'Jr.', + 'vat_id' => '1', + 'default_shipping' => true, + 'default_billing' => true + ]; + $defaultShippingText = $updateAddress['default_shipping'] ? "true": "false"; + $defaultBillingText = $updateAddress['default_billing'] ? "true": "false"; + + $mutation + = <<<MUTATION +mutation { + updateCustomerAddress(id: {$addressId}, input: { + region: { + region: "{$updateAddress['region']['region']}" + region_id: {$updateAddress['region']['region_id']} + region_code: "{$updateAddress['region']['region_code']}" + } + country_id: {$updateAddress['country_id']} + street: ["{$updateAddress['street'][0]}","{$updateAddress['street'][1]}"] + company: "{$updateAddress['company']}" + telephone: "{$updateAddress['telephone']}" + fax: "{$updateAddress['fax']}" + postcode: "{$updateAddress['postcode']}" + city: "{$updateAddress['city']}" + firstname: "{$updateAddress['firstname']}" + lastname: "{$updateAddress['lastname']}" + middlename: "{$updateAddress['middlename']}" + prefix: "{$updateAddress['prefix']}" + suffix: "{$updateAddress['suffix']}" + vat_id: "{$updateAddress['vat_id']}" + default_shipping: {$defaultShippingText} + default_billing: {$defaultBillingText} + }) { + id + customer_id + region { + region + region_id + region_code + } + country_id + street + company + telephone + fax + postcode + city + firstname + lastname + middlename + prefix + suffix + vat_id + default_shipping + default_billing + } +} +MUTATION; + + $response = $this->graphQlMutation($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); + $this->assertArrayHasKey('updateCustomerAddress', $response); + $this->assertArrayHasKey('customer_id', $response['updateCustomerAddress']); + $this->assertEquals($customerId, $response['updateCustomerAddress']['customer_id']); + $this->assertArrayHasKey('id', $response['updateCustomerAddress']); + + $address = $this->addressRepository->getById($addressId); + $this->assertEquals($address->getId(), $response['updateCustomerAddress']['id']); + $this->assertCustomerAddressesFields($address, $response['updateCustomerAddress']); + $this->assertCustomerAddressesFields($address, $updateAddress); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage The current customer isn't authorized. + */ + public function testUpdateCustomerAddressIfUserIsNotAuthorized() + { + $addressId = 1; + $mutation + = <<<MUTATION +mutation { + updateCustomerAddress(id:{$addressId}, input: { + city: "New City" + postcode: "5555" + }) { + id + } +} +MUTATION; + $this->graphQlMutation($mutation); + } + + /** + * Verify customers with credentials update address + * with missing required Firstname attribute + * + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Customer/_files/customer_address.php + * @expectedException \Exception + * @expectedExceptionMessage Required parameters are missing: firstname + */ + public function testUpdateCustomerAddressWithMissingAttribute() + { + $userName = 'customer@example.com'; + $password = 'password'; + $addressId = 1; + + $mutation + = <<<MUTATION +mutation { + updateCustomerAddress(id: {$addressId}, input: { + firstname: "" + lastname: "Phillis" + }) { + id + } +} +MUTATION; + $this->graphQlMutation($mutation, [], '', $this->getCustomerAuthHeaders($userName, $password)); + } + + /** + * Verify the fields for Customer address + * + * @param AddressInterface $address + * @param array $actualResponse + */ + private function assertCustomerAddressesFields(AddressInterface $address, $actualResponse): void + { + /** @var $addresses */ + $assertionMap = [ + ['response_field' => 'country_id', 'expected_value' => $address->getCountryId()], + ['response_field' => 'street', 'expected_value' => $address->getStreet()], + ['response_field' => 'company', 'expected_value' => $address->getCompany()], + ['response_field' => 'telephone', 'expected_value' => $address->getTelephone()], + ['response_field' => 'fax', 'expected_value' => $address->getFax()], + ['response_field' => 'postcode', 'expected_value' => $address->getPostcode()], + ['response_field' => 'city', 'expected_value' => $address->getCity()], + ['response_field' => 'firstname', 'expected_value' => $address->getFirstname()], + ['response_field' => 'lastname', 'expected_value' => $address->getLastname()], + ['response_field' => 'middlename', 'expected_value' => $address->getMiddlename()], + ['response_field' => 'prefix', 'expected_value' => $address->getPrefix()], + ['response_field' => 'suffix', 'expected_value' => $address->getSuffix()], + ['response_field' => 'vat_id', 'expected_value' => $address->getVatId()], + ['response_field' => 'default_shipping', 'expected_value' => (bool)$address->isDefaultShipping()], + ['response_field' => 'default_billing', 'expected_value' => (bool)$address->isDefaultBilling()], + ]; + $this->assertResponseFields($actualResponse, $assertionMap); + $this->assertTrue(is_array([$actualResponse['region']]), "region field must be of an array type."); + $assertionRegionMap = [ + ['response_field' => 'region', 'expected_value' => $address->getRegion()->getRegion()], + ['response_field' => 'region_code', 'expected_value' => $address->getRegion()->getRegionCode()], + ['response_field' => 'region_id', 'expected_value' => $address->getRegion()->getRegionId()] + ]; + $this->assertResponseFields($actualResponse['region'], $assertionRegionMap); + } + + /** + * @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/Customer/UpdateCustomerTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php new file mode 100644 index 000000000000..08933f47191b --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php @@ -0,0 +1,295 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer; + +use Magento\Customer\Model\CustomerAuthUpdate; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Tests for update customer + */ +class UpdateCustomerTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var CustomerRegistry + */ + private $customerRegistry; + + /** + * @var CustomerAuthUpdate + */ + private $customerAuthUpdate; + + protected function setUp() + { + parent::setUp(); + + $this->customerTokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); + $this->customerRegistry = Bootstrap::getObjectManager()->get(CustomerRegistry::class); + $this->customerAuthUpdate = Bootstrap::getObjectManager()->get(CustomerAuthUpdate::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + 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->graphQlMutation( + $query, + [], + '', + $this->getCustomerAuthHeaders($currentEmail, $currentPassword) + ); + + $this->assertEquals($newPrefix, $response['updateCustomer']['customer']['prefix']); + $this->assertEquals($newFirstname, $response['updateCustomer']['customer']['firstname']); + $this->assertEquals($newMiddlename, $response['updateCustomer']['customer']['middlename']); + $this->assertEquals($newLastname, $response['updateCustomer']['customer']['lastname']); + $this->assertEquals($newSuffix, $response['updateCustomer']['customer']['suffix']); + $this->assertEquals($newDob, $response['updateCustomer']['customer']['dob']); + $this->assertEquals($newTaxVat, $response['updateCustomer']['customer']['taxvat']); + $this->assertEquals($newEmail, $response['updateCustomer']['customer']['email']); + $this->assertEquals($newGender, $response['updateCustomer']['customer']['gender']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @expectedException \Exception + * @expectedExceptionMessage "input" value should be specified + */ + public function testUpdateCustomerIfInputDataIsEmpty() + { + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + + $query = <<<QUERY +mutation { + updateCustomer( + input: { + + } + ) { + customer { + firstname + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage The current customer isn't authorized. + */ + public function testUpdateCustomerIfUserIsNotAuthorized() + { + $newFirstname = 'Richard'; + + $query = <<<QUERY +mutation { + updateCustomer( + input: { + firstname: "{$newFirstname}" + } + ) { + customer { + firstname + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @expectedException \Exception + * @expectedExceptionMessage The account is locked. + */ + public function testUpdateCustomerIfAccountIsLocked() + { + $this->lockCustomer(1); + + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $newFirstname = 'Richard'; + + $query = <<<QUERY +mutation { + updateCustomer( + input: { + firstname: "{$newFirstname}" + } + ) { + customer { + firstname + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @expectedException \Exception + * @expectedExceptionMessage Provide the current "password" to change "email". + */ + public function testUpdateEmailIfPasswordIsMissed() + { + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $newEmail = 'customer_updated@example.com'; + + $query = <<<QUERY +mutation { + updateCustomer( + input: { + email: "{$newEmail}" + } + ) { + customer { + firstname + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @expectedException \Exception + * @expectedExceptionMessage Invalid login or password. + */ + public function testUpdateEmailIfPasswordIsInvalid() + { + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $invalidPassword = 'invalid_password'; + $newEmail = 'customer_updated@example.com'; + + $query = <<<QUERY +mutation { + updateCustomer( + input: { + email: "{$newEmail}" + password: "{$invalidPassword}" + } + ) { + customer { + firstname + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/two_customers.php + * @expectedException \Exception + * @expectedExceptionMessage A customer with the same email address already exists in an associated website. + */ + public function testUpdateEmailIfEmailAlreadyExists() + { + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $existedEmail = 'customer_two@example.com'; + + $query = <<<QUERY +mutation { + updateCustomer( + input: { + email: "{$existedEmail}" + password: "{$currentPassword}" + } + ) { + customer { + firstname + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + } + + /** + * @param string $email + * @param string $password + * @return array + */ + private function getCustomerAuthHeaders(string $email, string $password): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($email, $password); + return ['Authorization' => 'Bearer ' . $customerToken]; + } + + /** + * @param int $customerId + * @return void + */ + private function lockCustomer(int $customerId): void + { + $customerSecure = $this->customerRegistry->retrieveSecureData($customerId); + $customerSecure->setLockExpires('2030-12-31 00:00:00'); + $this->customerAuthUpdate->saveAuth($customerId); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CountriesTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CountriesTest.php new file mode 100644 index 000000000000..c42ce4c46fa2 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CountriesTest.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Directory; + +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test the GraphQL endpoint's Coutries query + */ +class CountriesTest extends GraphQlAbstract +{ + public function testGetCountries() + { + $query = <<<QUERY +query { + countries { + id + two_letter_abbreviation + three_letter_abbreviation + full_name_locale + full_name_english + available_regions { + id + code + name + } + } +} +QUERY; + + $result = $this->graphQlQuery($query); + $this->assertArrayHasKey('countries', $result); + $this->assertArrayHasKey('id', $result['countries'][0]); + $this->assertArrayHasKey('two_letter_abbreviation', $result['countries'][0]); + $this->assertArrayHasKey('three_letter_abbreviation', $result['countries'][0]); + $this->assertArrayHasKey('full_name_locale', $result['countries'][0]); + $this->assertArrayHasKey('full_name_english', $result['countries'][0]); + $this->assertArrayHasKey('available_regions', $result['countries'][0]); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CountryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CountryTest.php new file mode 100644 index 000000000000..dda5ef342247 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CountryTest.php @@ -0,0 +1,74 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Directory; + +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test the GraphQL endpoint's Coutries query + */ +class CountryTest extends GraphQlAbstract +{ + public function testGetCountry() + { + $query = <<<QUERY +query { + country(id: "US") { + id + two_letter_abbreviation + three_letter_abbreviation + full_name_locale + full_name_english + available_regions { + id + code + name + } + } +} +QUERY; + + $result = $this->graphQlQuery($query); + $this->assertArrayHasKey('country', $result); + $this->assertArrayHasKey('id', $result['country']); + $this->assertArrayHasKey('two_letter_abbreviation', $result['country']); + $this->assertArrayHasKey('three_letter_abbreviation', $result['country']); + $this->assertArrayHasKey('full_name_locale', $result['country']); + $this->assertArrayHasKey('full_name_english', $result['country']); + $this->assertArrayHasKey('available_regions', $result['country']); + $this->assertArrayHasKey('id', $result['country']['available_regions'][0]); + $this->assertArrayHasKey('code', $result['country']['available_regions'][0]); + $this->assertArrayHasKey('name', $result['country']['available_regions'][0]); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage GraphQL response contains errors: The country isn't available. + */ + public function testGetCountryNotFoundException() + { + $query = <<<QUERY +query { + country(id: "BLAH") { + id + two_letter_abbreviation + three_letter_abbreviation + full_name_locale + full_name_english + available_regions { + id + code + name + } + } +} +QUERY; + + $this->graphQlQuery($query); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CurrencyTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CurrencyTest.php new file mode 100644 index 000000000000..ad5d71cb0860 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Directory/CurrencyTest.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Directory; + +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test the GraphQL endpoint's Currency query + */ +class CurrencyTest extends GraphQlAbstract +{ + public function testGetCurrency() + { + $query = <<<QUERY +query { + currency { + base_currency_code + base_currency_symbol + default_display_currency_code + default_display_currency_symbol + available_currency_codes + exchange_rates { + currency_to + rate + } + } +} +QUERY; + + $result = $this->graphQlQuery($query); + $this->assertArrayHasKey('currency', $result); + $this->assertArrayHasKey('base_currency_code', $result['currency']); + $this->assertArrayHasKey('base_currency_symbol', $result['currency']); + $this->assertArrayHasKey('default_display_currency_code', $result['currency']); + $this->assertArrayHasKey('default_display_currency_symbol', $result['currency']); + $this->assertArrayHasKey('available_currency_codes', $result['currency']); + $this->assertArrayHasKey('exchange_rates', $result['currency']); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Framework/QueryComplexityLimiterTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Framework/QueryComplexityLimiterTest.php new file mode 100644 index 000000000000..e784061d5562 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Framework/QueryComplexityLimiterTest.php @@ -0,0 +1,466 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Framework; + +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Tests query complexity limiter and depth limiter. + * Actual for production mode only + */ +class QueryComplexityLimiterTest extends GraphQlAbstract +{ + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testQueryComplexityIsLimited() + { + $query + = <<<QUERY +{ + category(id: 2) { + products { + items { + name + categories { + id + position + level + url_key + url_path + product_count + breadcrumbs { + category_id + category_name + category_url_key + } + products { + items { + media_gallery_entries { + file + } + name + special_from_date + special_to_date + new_to_date + new_from_date + tier_price + manufacturer + thumbnail { + url + label + } + sku + image { + url + label + } + canonical_url + updated_at + created_at + categories { + id + position + level + url_key + url_path + product_count + breadcrumbs { + category_id + category_name + category_url_key + } + products { + items { + name + special_from_date + special_to_date + new_to_date + thumbnail { + url + label + } + new_from_date + tier_price + manufacturer + sku + image { + url + label + } + canonical_url + updated_at + created_at + media_gallery_entries { + position + id + types + } + categories { + id + position + level + url_key + url_path + product_count + breadcrumbs { + category_id + category_name + category_url_key + } + products { + items { + name + special_from_date + special_to_date + new_to_date + new_from_date + tier_price + manufacturer + thumbnail { + url + label + } + sku + image { + url + label + } + canonical_url + updated_at + created_at + categories { + id + position + level + url_key + url_path + product_count + breadcrumbs { + category_id + category_name + category_url_key + } + products { + items { + name + special_from_date + special_to_date + new_to_date + new_from_date + tier_price + manufacturer + sku + image { + url + label + } + canonical_url + updated_at + created_at + categories { + id + position + level + url_key + url_path + product_count + breadcrumbs { + category_id + category_name + category_url_key + } + products { + items { + name + special_from_date + special_to_date + price { + minimalPrice { + amount { + value + currency + } + } + maximalPrice { + amount { + value + currency + } + } + regularPrice { + amount { + value + currency + } + } + } + tier_price + special_price + tier_prices { + customer_group_id + qty + percentage_value + website_id + } + tier_prices { + customer_group_id + qty + percentage_value + website_id + } + tier_prices { + customer_group_id + qty + percentage_value + website_id + } + tier_prices { + customer_group_id + qty + percentage_value + website_id + } + tier_prices { + customer_group_id + qty + percentage_value + website_id + } + tier_prices { + customer_group_id + qty + percentage_value + website_id + } + tier_prices { + customer_group_id + qty + percentage_value + website_id + } + tier_prices { + customer_group_id + qty + percentage_value + website_id + } + tier_prices { + customer_group_id + qty + percentage_value + website_id + } + tier_prices { + customer_group_id + qty + percentage_value + website_id + } + tier_prices { + customer_group_id + qty + percentage_value + website_id + } + tier_prices { + customer_group_id + qty + percentage_value + website_id + } + tier_prices { + customer_group_id + qty + percentage_value + website_id + } + tier_prices { + customer_group_id + qty + percentage_value + website_id + } + tier_prices { + customer_group_id + qty + percentage_value + website_id + } + tier_prices { + customer_group_id + qty + percentage_value + website_id + } + tier_prices { + customer_group_id + qty + percentage_value + website_id + } + tier_prices { + customer_group_id + qty + percentage_value + website_id + } + tier_prices { + customer_group_id + qty + percentage_value + website_id + } + tier_prices { + customer_group_id + qty + percentage_value + website_id + } + new_to_date + new_from_date + tier_price + manufacturer + sku + image { + url + label + } + thumbnail { + url + label + } + canonical_url + updated_at + created_at + categories { + id + position + position + position + position + position + position + position + position + position + position + position + position + position + position + position + position + position + position + position + level + url_key + url_path + product_count + default_sort_by + breadcrumbs { + category_id + category_name + category_url_key + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } +} +QUERY; + + self::expectExceptionMessageRegExp('/Max query complexity should be 300 but got 302/'); + //Use POST request because request uri is too large for some servers + $this->graphQlMutation($query); + } + + /** + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testQueryDepthIsLimited() + { + $query + = <<<QUERY +{ + category(id: 2) { + products { + items { + name + categories { + products { + items { + media_gallery_entries { + file + } + categories { + products { + items { + categories { + products { + items { + categories { + products { + items { + categories { + products { + items { + categories { + products { + items { + categories { + products { + items { + name + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } +} +QUERY; + self::expectExceptionMessageRegExp('/Max query depth should be 20 but got 23/'); + $this->graphQlQuery($query); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/IntrospectionQueryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/IntrospectionQueryTest.php index 85b4c62c4194..60acb3a7a4d4 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/IntrospectionQueryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/IntrospectionQueryTest.php @@ -12,10 +12,10 @@ class IntrospectionQueryTest extends GraphQlAbstract { /** - * Tests that Introspection is disabled when not in developer mode + * Tests that Introspection is allowed by default * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testIntrospectionQueryWithFieldArgs() + public function testIntrospectionQuery() { $query = <<<QUERY @@ -54,11 +54,6 @@ public function testIntrospectionQueryWithFieldArgs() } QUERY; - $this->expectException(\Exception::class); - $this->expectExceptionMessage( - 'GraphQL response contains errors: GraphQL introspection is not allowed, but ' . - 'the query contained __schema or __type' - ); - $this->graphQlQuery($query); + $this->assertArrayHasKey('__schema', $this->graphQlQuery($query)); } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/CacheTagTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/CacheTagTest.php new file mode 100644 index 000000000000..23bcd342ec99 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/CacheTagTest.php @@ -0,0 +1,202 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\PageCache; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test the caching works properly for products and categories + */ +class CacheTagTest extends GraphQlAbstract +{ + /** + * @inheritdoc + */ + protected function setUp() + { + $this->markTestSkipped( + 'This test will stay skipped until DEVOPS-4924 is resolved' + ); + } + + /** + * Test if Magento cache tags and debug headers for products are generated properly + * + * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php + */ + public function testCacheTagsAndCacheDebugHeaderForProducts() + { + $productSku='simple2'; + $query + = <<<QUERY + { + products(filter: {sku: {eq: "{$productSku}"}}) + { + items { + id + name + sku + } + } + } +QUERY; + + // Cache-debug should be a MISS when product is queried for first time + $responseMiss = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseMiss['headers']); + $this->assertEquals('MISS', $responseMiss['headers']['X-Magento-Cache-Debug']); + + // Cache-debug should be a HIT for the second round + $responseHit = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseHit['headers']); + $this->assertEquals('HIT', $responseHit['headers']['X-Magento-Cache-Debug']); + + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); + /** @var Product $product */ + $product = $productRepository->get($productSku, false, null, true); + $product->setPrice(15); + $productRepository->save($product); + // Cache invalidation happens and cache-debug header value is a MISS after product update + $responseMiss = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseMiss['headers']); + $this->assertEquals('MISS', $responseMiss['headers']['X-Magento-Cache-Debug']); + $this->assertArrayHasKey('X-Magento-Tags', $responseMiss['headers']); + $expectedCacheTags = ['cat_p','cat_p_' . $product->getId(),'FPC']; + $actualCacheTags = explode(',', $responseMiss['headers']['X-Magento-Tags']); + foreach ($expectedCacheTags as $expectedCacheTag) { + $this->assertContains($expectedCacheTag, $actualCacheTags); + } + } + + /** + * Test if X-Magento-Tags for categories are generated properly + * + * Also tests the use case for cache invalidation + * + * @magentoApiDataFixture Magento/Catalog/_files/product_in_multiple_categories.php + */ + public function testCacheTagForCategoriesWithProduct() + { + $firstProductSku = 'simple333'; + $secondProductSku = 'simple444'; + $categoryId ='4'; + + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); + /** @var Product $firstProduct */ + $firstProduct = $productRepository->get($firstProductSku, false, null, true); + /** @var Product $secondProduct */ + $secondProduct = $productRepository->get($secondProductSku, false, null, true); + + $categoryQueryVariables =[ + 'id' => $categoryId, + 'pageSize'=> 10, + 'currentPage' => 1 + ]; + + $product1Query = $this->getProductQuery($firstProductSku); + $product2Query =$this->getProductQuery($secondProductSku); + $categoryQuery = $this->getCategoryQuery(); + + // cache-debug header value should be a MISS when category is loaded first time + $responseMiss = $this->graphQlQueryWithResponseHeaders($categoryQuery, $categoryQueryVariables); + $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseMiss['headers']); + $this->assertEquals('MISS', $responseMiss['headers']['X-Magento-Cache-Debug']); + $this->assertArrayHasKey('X-Magento-Tags', $responseMiss['headers']); + $actualCacheTags = explode(',', $responseMiss['headers']['X-Magento-Tags']); + $expectedCacheTags = + [ + 'cat_c', + 'cat_c_' . $categoryId, + 'cat_p', + 'cat_p_' . $firstProduct->getId(), + 'cat_p_' . $secondProduct->getId(), + 'FPC' + ]; + $this->assertEquals($expectedCacheTags, $actualCacheTags); + + // Cache-debug header should be a MISS for product 1 on first request + $responseFirstProduct = $this->graphQlQueryWithResponseHeaders($product1Query); + $this->assertEquals('MISS', $responseFirstProduct['headers']['X-Magento-Cache-Debug']); + // Cache-debug header should be a MISS for product 2 during first load + $responseSecondProduct = $this->graphQlQueryWithResponseHeaders($product2Query); + $this->assertEquals('MISS', $responseSecondProduct['headers']['X-Magento-Cache-Debug']); + + $firstProduct->setPrice(20); + $productRepository->save($firstProduct); + // cache-debug header value should be MISS after updating product1 and reloading the Category + $responseMissCategory = $this->graphQlQueryWithResponseHeaders($categoryQuery, $categoryQueryVariables); + $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseMissCategory['headers']); + $this->assertEquals('MISS', $responseMissCategory['headers']['X-Magento-Cache-Debug']); + + // cache-debug should be a MISS for product 1 after it is updated - cache invalidation + $responseMissFirstProduct = $this->graphQlQueryWithResponseHeaders($product1Query); + $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseMissFirstProduct['headers']); + $this->assertEquals('MISS', $responseMissFirstProduct['headers']['X-Magento-Cache-Debug']); + // Cache-debug header should be a HIT for product 2 + $responseHitSecondProduct = $this->graphQlQueryWithResponseHeaders($product2Query); + $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseHitSecondProduct['headers']); + $this->assertEquals('HIT', $responseHitSecondProduct['headers']['X-Magento-Cache-Debug']); + } + + /** + * Get Product query + * + * @param string $productSku + * @return string + */ + private function getProductQuery(string $productSku): string + { + $productQuery = <<<QUERY + { + products(filter: {sku: {eq: "{$productSku}"}}) + { + items { + id + name + sku + } + } + } +QUERY; + return $productQuery; + } + + /** + * Get category query + * + * @return string + */ + private function getCategoryQuery(): string + { + $categoryQueryString = <<<QUERY +query GetCategoryQuery(\$id: Int!, \$pageSize: Int!, \$currentPage: Int!) { + category(id: \$id) { + id + description + name + product_count + products(pageSize: \$pageSize, currentPage: \$currentPage) { + items { + id + name + url_key + } + total_count + } + } + } +QUERY; + + return $categoryQueryString; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/Cms/BlockCacheTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/Cms/BlockCacheTest.php new file mode 100644 index 000000000000..5182ff791f57 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/Cms/BlockCacheTest.php @@ -0,0 +1,160 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\PageCache\Cms; + +use Magento\Cms\Model\Block; +use Magento\Cms\Model\BlockRepository; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test the caching works properly for CMS Blocks + */ +class BlockCacheTest extends GraphQlAbstract +{ + /** + * @inheritdoc + */ + protected function setUp() + { + $this->markTestSkipped( + 'This test will stay skipped until DEVOPS-4924 is resolved' + ); + } + + /** + * Test that X-Magento-Tags are correct + * + * @magentoApiDataFixture Magento/Cms/_files/block.php + */ + public function testCacheTagsHaveExpectedValue() + { + $blockIdentifier = 'fixture_block'; + $blockRepository = Bootstrap::getObjectManager()->get(BlockRepository::class); + $block = $blockRepository->getById($blockIdentifier); + $blockId = $block->getId(); + $query = $this->getBlockQuery([$blockIdentifier]); + + //cache-debug should be a MISS on first request + $response = $this->graphQlQueryWithResponseHeaders($query); + + $this->assertArrayHasKey('X-Magento-Tags', $response['headers']); + $actualTags = explode(',', $response['headers']['X-Magento-Tags']); + $expectedTags = ["cms_b", "cms_b_{$blockId}", "cms_b_{$blockIdentifier}", "FPC"]; + $this->assertEquals($expectedTags, $actualTags); + } + + /** + * Test the second request for the same block will return a cached result + * + * @magentoApiDataFixture Magento/Cms/_files/block.php + */ + public function testCacheIsUsedOnSecondRequest() + { + $blockIdentifier = 'fixture_block'; + $query = $this->getBlockQuery([$blockIdentifier]); + + //cache-debug should be a MISS on first request + $responseMiss = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseMiss['headers']); + $this->assertEquals('MISS', $responseMiss['headers']['X-Magento-Cache-Debug']); + + //cache-debug should be a HIT on second request + $responseHit = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseHit['headers']); + $this->assertEquals('HIT', $responseHit['headers']['X-Magento-Cache-Debug']); + //cached data should be correct + $this->assertNotEmpty($responseHit['body']); + $this->assertArrayNotHasKey('errors', $responseHit['body']); + $blocks = $responseHit['body']['cmsBlocks']['items']; + $this->assertEquals($blockIdentifier, $blocks[0]['identifier']); + $this->assertEquals('CMS Block Title', $blocks[0]['title']); + } + + /** + * Test that cache is invalidated when block is updated + * + * @magentoApiDataFixture Magento/Cms/_files/blocks.php + * @magentoApiDataFixture Magento/Cms/_files/block.php + */ + public function testCacheIsInvalidatedOnBlockUpdate() + { + $fixtureBlockIdentifier = 'fixture_block'; + $enabledBlockIdentifier = 'enabled_block'; + $fixtureBlockQuery = $this->getBlockQuery([$fixtureBlockIdentifier]); + $enabledBlockQuery = $this->getBlockQuery([$enabledBlockIdentifier]); + + //cache-debug should be a MISS on first request + $fixtureBlockMiss = $this->graphQlQueryWithResponseHeaders($fixtureBlockQuery); + $this->assertEquals('MISS', $fixtureBlockMiss['headers']['X-Magento-Cache-Debug']); + $enabledBlockMiss = $this->graphQlQueryWithResponseHeaders($enabledBlockQuery); + $this->assertEquals('MISS', $enabledBlockMiss['headers']['X-Magento-Cache-Debug']); + + //cache-debug should be a HIT on second request + $fixtureBlockHit = $this->graphQlQueryWithResponseHeaders($fixtureBlockQuery); + $this->assertEquals('HIT', $fixtureBlockHit['headers']['X-Magento-Cache-Debug']); + $enabledBlockHit = $this->graphQlQueryWithResponseHeaders($enabledBlockQuery); + $this->assertEquals('HIT', $enabledBlockHit['headers']['X-Magento-Cache-Debug']); + + $newBlockContent = 'New block content!!!'; + $this->updateBlockContent($fixtureBlockIdentifier, $newBlockContent); + + //cache-debug should be a MISS after update the block + $fixtureBlockMiss = $this->graphQlQueryWithResponseHeaders($fixtureBlockQuery); + $this->assertEquals('MISS', $fixtureBlockMiss['headers']['X-Magento-Cache-Debug']); + $enabledBlockHit = $this->graphQlQueryWithResponseHeaders($enabledBlockQuery); + $this->assertEquals('HIT', $enabledBlockHit['headers']['X-Magento-Cache-Debug']); + //updated block data should be correct + $this->assertNotEmpty($fixtureBlockMiss['body']); + $blocks = $fixtureBlockMiss['body']['cmsBlocks']['items']; + $this->assertArrayNotHasKey('errors', $fixtureBlockMiss['body']); + $this->assertEquals($fixtureBlockIdentifier, $blocks[0]['identifier']); + $this->assertEquals('CMS Block Title', $blocks[0]['title']); + $this->assertEquals($newBlockContent, $blocks[0]['content']); + } + + /** + * Update the content of a CMS block + * + * @param $identifier + * @param $newContent + * @return Block + */ + private function updateBlockContent($identifier, $newContent): Block + { + $blockRepository = Bootstrap::getObjectManager()->get(BlockRepository::class); + $block = $blockRepository->getById($identifier); + $block->setContent($newContent); + $blockRepository->save($block); + + return $block; + } + + /** + * Get cmsBlocks query + * + * @param array $identifiers + * @return string + */ + private function getBlockQuery(array $identifiers): string + { + $identifiersString = implode(',', $identifiers); + $query = <<<QUERY + { + cmsBlocks(identifiers: ["$identifiersString"]) { + items { + title + identifier + content + } + } + } +QUERY; + return $query; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/Cms/PageCacheTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/Cms/PageCacheTest.php new file mode 100644 index 000000000000..34dc9eef4c33 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/Cms/PageCacheTest.php @@ -0,0 +1,153 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\PageCache\Cms; + +use Magento\Cms\Model\GetPageByIdentifier; +use Magento\Cms\Model\PageRepository; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test the caching works properly for CMS Pages + */ +class PageCacheTest extends GraphQlAbstract +{ + /** + * @var GetPageByIdentifier + */ + private $pageByIdentifier; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->markTestSkipped( + 'This test will stay skipped until DEVOPS-4924 is resolved' + ); + $this->pageByIdentifier = Bootstrap::getObjectManager()->get(GetPageByIdentifier::class); + } + + /** + * Test that X-Magento-Tags are correct + * + * @magentoApiDataFixture Magento/Cms/_files/pages.php + */ + public function testCacheTagsHaveExpectedValue() + { + $pageIdentifier = 'page100'; + $page = $this->pageByIdentifier->execute($pageIdentifier, 0); + $pageId = (int) $page->getId(); + + $query = $this->getPageQuery($pageId); + + //cache-debug should be a MISS on first request + $response = $this->graphQlQueryWithResponseHeaders($query); + + $this->assertArrayHasKey('X-Magento-Tags', $response['headers']); + $actualTags = explode(',', $response['headers']['X-Magento-Tags']); + $expectedTags = ["cms_p", "cms_p_{$pageId}", "FPC"]; + $this->assertEquals($expectedTags, $actualTags); + } + + /** + * Test the second request for the same page will return a cached result + * + * @magentoApiDataFixture Magento/Cms/_files/pages.php + */ + public function testCacheIsUsedOnSecondRequest() + { + $pageIdentifier = 'page100'; + $page = $this->pageByIdentifier->execute($pageIdentifier, 0); + $pageId = (int) $page->getId(); + + $query = $this->getPageQuery($pageId); + + //cache-debug should be a MISS on first request + $responseMiss = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseMiss['headers']); + $this->assertEquals('MISS', $responseMiss['headers']['X-Magento-Cache-Debug']); + + //cache-debug should be a HIT on second request + $responseHit = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseHit['headers']); + $this->assertEquals('HIT', $responseHit['headers']['X-Magento-Cache-Debug']); + //cached data should be correct + $this->assertNotEmpty($responseHit['body']); + $this->assertArrayNotHasKey('errors', $responseHit['body']); + $pageData = $responseHit['body']['cmsPage']; + $this->assertEquals('Cms Page 100', $pageData['title']); + } + + /** + * Test that cache is invalidated when page is updated + * + * @magentoApiDataFixture Magento/Cms/_files/pages.php + */ + public function testCacheIsInvalidatedOnPageUpdate() + { + $page100Identifier = 'page100'; + $page100 = $this->pageByIdentifier->execute($page100Identifier, 0); + $page100Id = (int) $page100->getId(); + $pageBlankIdentifier = 'page_design_blank'; + $pageBlank = $this->pageByIdentifier->execute($pageBlankIdentifier, 0); + $pageBlankId = (int) $pageBlank->getId(); + + $page100Query = $this->getPageQuery($page100Id); + $pageBlankQuery = $this->getPageQuery($pageBlankId); + + //cache-debug should be a MISS on first request + $page100Miss = $this->graphQlQueryWithResponseHeaders($page100Query); + $this->assertEquals('MISS', $page100Miss['headers']['X-Magento-Cache-Debug']); + $pageBlankMiss = $this->graphQlQueryWithResponseHeaders($pageBlankQuery); + $this->assertEquals('MISS', $pageBlankMiss['headers']['X-Magento-Cache-Debug']); + + //cache-debug should be a HIT on second request + $page100Hit = $this->graphQlQueryWithResponseHeaders($page100Query); + $this->assertEquals('HIT', $page100Hit['headers']['X-Magento-Cache-Debug']); + $pageBlankHit = $this->graphQlQueryWithResponseHeaders($pageBlankQuery); + $this->assertEquals('HIT', $pageBlankHit['headers']['X-Magento-Cache-Debug']); + + $pageRepository = Bootstrap::getObjectManager()->get(PageRepository::class); + $newPageContent = 'New page content for blank page.'; + $pageBlank->setContent($newPageContent); + $pageRepository->save($pageBlank); + + //cache-debug should be a MISS after updating the page + $pageBlankMiss = $this->graphQlQueryWithResponseHeaders($pageBlankQuery); + $this->assertEquals('MISS', $pageBlankMiss['headers']['X-Magento-Cache-Debug']); + $page100Hit = $this->graphQlQueryWithResponseHeaders($page100Query); + $this->assertEquals('HIT', $page100Hit['headers']['X-Magento-Cache-Debug']); + //updated page data should be correct + $this->assertNotEmpty($pageBlankMiss['body']); + $pageData = $pageBlankMiss['body']['cmsPage']; + $this->assertArrayNotHasKey('errors', $pageBlankMiss['body']); + $this->assertEquals('Cms Page Design Blank', $pageData['title']); + $this->assertEquals($newPageContent, $pageData['content']); + } + + /** + * Get page query + * + * @param int $pageId + * @return string + */ + private function getPageQuery(int $pageId): string + { + $query = <<<QUERY +{ + cmsPage(id: $pageId) { + title + url_key + content + } +} +QUERY; + return $query; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/ProductInMultipleStoresCacheTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/ProductInMultipleStoresCacheTest.php new file mode 100644 index 000000000000..cf4cebdfe8e4 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/ProductInMultipleStoresCacheTest.php @@ -0,0 +1,322 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\PageCache; + +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * This test considers FPC caching but it can't check the headers since we don't guarantee that test if in dev mode. + * + * @magentoAppIsolation enabled + */ +class ProductInMultipleStoresCacheTest extends GraphQlAbstract +{ + /** + * @inheritdoc + */ + protected function setUp() + { + /** @var \Magento\Store\Model\Store $store */ + $store = ObjectManager::getInstance()->get(\Magento\Store\Model\Store::class); + $storeCodeFromFixture = 'fixture_second_store'; + + /** @var \Magento\Config\Model\ResourceModel\Config $configResource */ + $configResource = ObjectManager::getInstance()->get(\Magento\Config\Model\ResourceModel\Config::class); + /** @var \Magento\Config\App\Config\Type\System $config */ + $config = ObjectManager::getInstance()->get(\Magento\Config\App\Config\Type\System::class); + + $configResource->saveConfig( + \Magento\Directory\Model\Currency::XML_PATH_CURRENCY_DEFAULT, + 'EUR', + \Magento\Store\Model\ScopeInterface::SCOPE_WEBSITES, + $store->load($storeCodeFromFixture)->getWebsiteId() + ); + + // allow USD & EUR currency + $configResource->saveConfig( + \Magento\Directory\Model\Currency::XML_PATH_CURRENCY_ALLOW, + 'EUR,USD', + \Magento\Store\Model\ScopeInterface::SCOPE_WEBSITES, + $store->load($storeCodeFromFixture)->getWebsiteId() + ); + + // allow USD & EUR currency + $configResource->saveConfig( + \Magento\Directory\Model\Currency::XML_PATH_CURRENCY_ALLOW, + 'EUR,USD' + ); + + // configuration cache clean is required to reload currency setting + $config->clean(); + } + + /** + * @inheritdoc + */ + protected function tearDown() + { + /** @var \Magento\Config\App\Config\Type\System $config */ + $config = ObjectManager::getInstance()->get(\Magento\Config\App\Config\Type\System::class); + /** @var \Magento\Config\Model\ResourceModel\Config $configResource */ + $configResource = ObjectManager::getInstance()->get(\Magento\Config\Model\ResourceModel\Config::class); + + // restore allow USD currency + $configResource->saveConfig( + \Magento\Directory\Model\Currency::XML_PATH_CURRENCY_ALLOW, + 'USD' + ); + + // configuration cache clean is required to reload currency setting + $config->clean(); + parent::tearDown(); + } + + /** + * Test a non existing or non existing currency + * + * @magentoApiDataFixture Magento/Store/_files/second_website_with_second_currency.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testProductFromSpecificAndDefaultStoreWithMultiCurrencyNonExisting() + { + $productSku = 'simple'; + + $query = <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) + { + items { + attribute_set_id + created_at + id + name + price { + minimalPrice { + amount { + value + currency + } + } + } + sku + type_id + updated_at + ... on PhysicalProductInterface { + weight + } + } + } +} +QUERY; + + //test non existing currency + $headerMap = ['Store' => 'default', 'Content-Currency' => 'someNonExistentCurrency']; + $this->expectExceptionMessage('GraphQL response contains errors: Please correct the target currency'); + $this->graphQlQuery($query, [], '', $headerMap); + } + + /** + * Test a non existing or non allowed currency + * + * @magentoApiDataFixture Magento/Store/_files/second_website_with_second_currency.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testProductFromSpecificAndDefaultStoreWithMultiCurrencyNotAllowed() + { + $productSku = 'simple'; + + $query = <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) + { + items { + attribute_set_id + created_at + id + name + price { + minimalPrice { + amount { + value + currency + } + } + } + sku + type_id + updated_at + ... on PhysicalProductInterface { + weight + } + } + } +} +QUERY; + + $storeCodeFromFixture = 'fixture_second_store'; + + //test not allowed existing currency + $headerMap = ['Store' => $storeCodeFromFixture, 'Content-Currency' => 'CAD']; + $this->expectExceptionMessage( + 'GraphQL response contains errors: Please correct the target currency' + ); + $this->graphQlQuery($query, [], '', $headerMap); + } + + /** + * Test a product from a custom and default store, with cache with repeating queries asserting different results. + * + * @magentoApiDataFixture Magento/Store/_files/second_website_with_second_currency.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testProductFromSpecificAndDefaultStoreWithMultiCurrency() + { + $productSku = 'simple'; + + $query = <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) + { + items { + attribute_set_id + created_at + id + name + price { + minimalPrice { + amount { + value + currency + } + } + } + sku + type_id + updated_at + ... on PhysicalProductInterface { + weight + } + } + } +} +QUERY; + + /** @var \Magento\Store\Model\Store $store */ + $store = ObjectManager::getInstance()->get(\Magento\Store\Model\Store::class); + $storeCodeFromFixture = 'fixture_second_store'; + $storeId = $store->load($storeCodeFromFixture)->getStoreId(); + + /** @var \Magento\Catalog\Model\Product $product */ + $product = ObjectManager::getInstance()->get(\Magento\Catalog\Model\Product::class); + $product->load($product->getIdBySku($productSku)); + + $website = ObjectManager::getInstance()->get(\Magento\Store\Model\Website::class); + /** @var $website \Magento\Store\Model\Website */ + $website->load('test', 'code'); + $product->setWebsiteIds([1, $website->getId()]); + + // change product name for custom store + $productNameInFixtureStore = 'Product\'s Name in Fixture Store'; + $product->setName($productNameInFixtureStore)->setStoreId($storeId)->save(); + + // test store header only, query is cached at this point in EUR + $headerMap = ['Store' => $storeCodeFromFixture]; + $response = $this->graphQlQuery($query, [], '', $headerMap); + $this->assertEquals( + $productNameInFixtureStore, + $response['products']['items'][0]['name'], + 'Product name in fixture store is invalid.' + ); + $this->assertEquals( + 'EUR', + $response['products']['items'][0]['price']['minimalPrice']['amount']['currency'], + 'Currency code EUR in fixture ' . $storeCodeFromFixture . ' is unexpected' + ); + + // test cached store + currency header in Euros + $headerMap = ['Store' => $storeCodeFromFixture, 'Content-Currency' => 'EUR']; + $response = $this->graphQlQuery($query, [], '', $headerMap); + $this->assertEquals( + $productNameInFixtureStore, + $response['products']['items'][0]['name'], + 'Product name in fixture ' . $storeCodeFromFixture . ' is invalid.' + ); + $this->assertEquals( + 'EUR', + $response['products']['items'][0]['price']['minimalPrice']['amount']['currency'], + 'Currency code EUR in fixture ' . $storeCodeFromFixture . ' is unexpected' + ); + + // test non cached store + currency header in USD + $headerMap = ['Store' => $storeCodeFromFixture, 'Content-Currency' => 'USD']; + $response = $this->graphQlQuery($query, [], '', $headerMap); + $this->assertEquals( + $productNameInFixtureStore, + $response['products']['items'][0]['name'], + 'Product name in fixture store is invalid.' + ); + $this->assertEquals( + 'USD', + $response['products']['items'][0]['price']['minimalPrice']['amount']['currency'], + 'Currency code USD in fixture ' . $storeCodeFromFixture . ' is unexpected' + ); + + // test non cached store + currency header in USD not cached + $headerMap = ['Store' => 'default', 'Content-Currency' => 'USD']; + $response = $this->graphQlQuery($query, [], '', $headerMap); + $this->assertEquals( + 'Simple Product', + $response['products']['items'][0]['name'], + 'Product name in fixture store is invalid.' + ); + $this->assertEquals( + 'USD', + $response['products']['items'][0]['price']['minimalPrice']['amount']['currency'], + 'Currency code USD in fixture store default is unexpected' + ); + + // test non cached store + currency header in USD not cached + $headerMap = ['Store' => 'default', 'Content-Currency' => 'EUR']; + $response = $this->graphQlQuery($query, [], '', $headerMap); + $this->assertEquals( + 'Simple Product', + $response['products']['items'][0]['name'], + 'Product name in fixture store is invalid.' + ); + $this->assertEquals( + 'EUR', + $response['products']['items'][0]['price']['minimalPrice']['amount']['currency'], + 'Currency code EUR in fixture store default is unexpected' + ); + + // test non cached store + currency header in USD cached + $headerMap = ['Store' => 'default']; + $response = $this->graphQlQuery($query, [], '', $headerMap); + $this->assertEquals( + 'Simple Product', + $response['products']['items'][0]['name'], + 'Product name in fixture store is invalid.' + ); + $this->assertEquals( + 'USD', + $response['products']['items'][0]['price']['minimalPrice']['amount']['currency'], + 'Currency code USD in fixture store default is unexpected' + ); + + // test cached response store + currency header with non existing currency, and no valid response, no cache + $headerMap = ['Store' => $storeCodeFromFixture, 'Content-Currency' => 'SOMECURRENCY']; + $this->expectExceptionMessage( + 'GraphQL response contains errors: Please correct the target currency' + ); + $this->graphQlQuery($query, [], '', $headerMap); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/Quote/Guest/CartCacheTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/Quote/Guest/CartCacheTest.php new file mode 100644 index 000000000000..e09ee8bc969a --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/Quote/Guest/CartCacheTest.php @@ -0,0 +1,129 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\PageCache\Quote\Guest; + +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test cart queries are not cached + * + * @magentoApiDataFixture Magento/Catalog/_files/products.php + */ +class CartCacheTest extends GraphQlAbstract +{ + /** + * @inheritdoc + */ + protected function setUp() + { + $this->markTestSkipped( + 'This test will stay skipped until DEVOPS-4924 is resolved' + ); + } + + public function testCartIsNotCached() + { + $qty = 2; + $sku = 'simple'; + $cartId = $this->createEmptyCart(); + $this->addSimpleProductToCart($cartId, $qty, $sku); + + $getCartQuery = $this->getCartQuery($cartId); + $responseMiss = $this->graphQlQueryWithResponseHeaders($getCartQuery); + $this->assertArrayHasKey('cart', $responseMiss['body']); + $this->assertArrayHasKey('items', $responseMiss['body']['cart']); + $this->assertEquals('MISS', $responseMiss['headers']['X-Magento-Cache-Debug']); + + /** Cache debug header value is still a MISS for any subsequent request */ + $responseMissNext = $this->graphQlQueryWithResponseHeaders($getCartQuery); + $this->assertEquals('MISS', $responseMissNext['headers']['X-Magento-Cache-Debug']); + } + + /** + * Create a guest cart which generates a maskedQuoteId + * + * @return string + */ + private function createEmptyCart(): string + { + $query = + <<<QUERY + mutation + { + createEmptyCart + } +QUERY; + + $response = $this->graphQlMutation($query); + $maskedQuoteId = $response['createEmptyCart']; + return $maskedQuoteId; + } + + /** + * Add simple product to the cart using the maskedQuoteId + * + * @param string $maskedCartId + * @param int $qty + * @param string $sku + */ + private function addSimpleProductToCart(string $maskedCartId, int $qty, string $sku): void + { + $addProductToCartQuery = + <<<QUERY + mutation { + addSimpleProductsToCart( + input: { + cart_id: "{$maskedCartId}" + cartItems: [ + { + data: { + qty: $qty + sku: "$sku" + } + } + ] + } + ) { + cart { + items { + qty + product { + sku + } + } + } + } + } +QUERY; + $response = $this->graphQlMutation($addProductToCartQuery); + self::assertArrayHasKey('cart', $response['addSimpleProductsToCart']); + } + + /** + * Get cart query string + * + * @param string $maskedQuoteId + * @return string + */ + private function getCartQuery(string $maskedQuoteId): string + { + return <<<QUERY +{ + cart(cart_id: "{$maskedQuoteId}") { + items { + id + qty + product { + sku + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductWithCustomOptionsToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductWithCustomOptionsToCartTest.php new file mode 100644 index 000000000000..63546298304b --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductWithCustomOptionsToCartTest.php @@ -0,0 +1,184 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote; + +use Magento\Catalog\Api\ProductCustomOptionRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Add simple product with custom options to cart testcases + */ +class AddSimpleProductWithCustomOptionsToCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var ProductCustomOptionRepositoryInterface + */ + private $productCustomOptionsRepository; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->productCustomOptionsRepository = $objectManager->get(ProductCustomOptionRepositoryInterface::class); + } + + /** + * Test adding a simple product to the shopping cart with all supported + * customizable options assigned + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_options.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddSimpleProductWithOptions() + { + $sku = 'simple'; + $quantity = 1; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $customOptionsValues = $this->getCustomOptionsValuesForQuery($sku); + + /* Generate customizable options fragment for GraphQl request */ + $queryCustomizableOptions = preg_replace('/"([^"]+)"\s*:\s*/', '$1:', json_encode($customOptionsValues)); + + $query = <<<QUERY +mutation { + addSimpleProductsToCart( + input: { + cart_id: "{$maskedQuoteId}", + cartItems: [ + { + data: { + quantity: $quantity + sku: "$sku" + }, + customizable_options: $queryCustomizableOptions + } + ] + } + ) { + cart { + items { + ... on SimpleCartItem { + customizable_options { + label + values { + value + } + } + } + } + } + } +} +QUERY; + + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('items', $response['addSimpleProductsToCart']['cart']); + self::assertCount(1, $response['addSimpleProductsToCart']['cart']); + + $customizableOptionsOutput = $response['addSimpleProductsToCart']['cart']['items'][0]['customizable_options']; + $assignedOptionsCount = count($customOptionsValues); + for ($counter = 0; $counter < $assignedOptionsCount; $counter++) { + self::assertEquals( + $customOptionsValues[$counter]['value'], + $customizableOptionsOutput[$counter]['values'][0]['value'] + ); + } + } + + /** + * Test adding a simple product with empty values for required options + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_options.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddSimpleProductWithNoRequiredOptionsSet() + { + $sku = 'simple'; + $quantity = 1; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $query = <<<QUERY +mutation { + addSimpleProductsToCart( + input: { + cart_id: "{$maskedQuoteId}", + cartItems: [ + { + data: { + quantity: $quantity + sku: "$sku" + } + } + ] + } + ) { + cart { + items { + ... on SimpleCartItem { + customizable_options { + label + values { + value + } + } + } + } + } + } +} +QUERY; + + self::expectExceptionMessage( + 'The product\'s required option(s) weren\'t entered. Make sure the options are entered and try again.' + ); + + $this->graphQlMutation($query); + } + + /** + * Generate an array with test values for customizable options + * based on the option type + * + * @param string $sku + * @return array + */ + private function getCustomOptionsValuesForQuery(string $sku): array + { + $customOptions = $this->productCustomOptionsRepository->getList($sku); + $customOptionsValues = []; + + foreach ($customOptions as $customOption) { + $optionType = $customOption->getType(); + if ($optionType == 'field' || $optionType == 'area') { + $customOptionsValues[] = [ + 'id' => (int) $customOption->getOptionId(), + 'value' => 'test' + ]; + } elseif ($optionType == 'drop_down') { + $optionSelectValues = $customOption->getValues(); + $customOptionsValues[] = [ + 'id' => (int) $customOption->getOptionId(), + 'value' => reset($optionSelectValues)->getOptionTypeId() + ]; + } + } + + return $customOptionsValues; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddVirtualProductWithCustomOptionsToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddVirtualProductWithCustomOptionsToCartTest.php new file mode 100644 index 000000000000..94ac11ad8e0b --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddVirtualProductWithCustomOptionsToCartTest.php @@ -0,0 +1,184 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote; + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Catalog\Api\ProductCustomOptionRepositoryInterface; + +/** + * Add virtual product with custom options to cart testcases + */ +class AddVirtualProductWithCustomOptionsToCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var ProductCustomOptionRepositoryInterface + */ + private $productCustomOptionsRepository; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->productCustomOptionsRepository = $objectManager->get(ProductCustomOptionRepositoryInterface::class); + } + + /** + * Test adding a virtual product to the shopping cart with all supported + * customizable options assigned + * + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual_with_options.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddVirtualProductWithOptions() + { + $sku = 'virtual'; + $quantity = 1; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $customOptionsValues = $this->getCustomOptionsValuesForQuery($sku); + + /* Generate customizable options fragment for GraphQl request */ + $queryCustomizableOptions = preg_replace('/"([^"]+)"\s*:\s*/', '$1:', json_encode($customOptionsValues)); + + $query = <<<QUERY +mutation { + addVirtualProductsToCart( + input: { + cart_id: "{$maskedQuoteId}", + cartItems: [ + { + data: { + quantity: $quantity + sku: "$sku" + }, + customizable_options: $queryCustomizableOptions + } + ] + } + ) { + cart { + items { + ... on VirtualCartItem { + customizable_options { + label + values { + value + } + } + } + } + } + } +} +QUERY; + + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('items', $response['addVirtualProductsToCart']['cart']); + self::assertCount(1, $response['addVirtualProductsToCart']['cart']); + + $customizableOptionsOutput = $response['addVirtualProductsToCart']['cart']['items'][0]['customizable_options']; + $assignedOptionsCount = count($customOptionsValues); + for ($counter = 0; $counter < $assignedOptionsCount; $counter++) { + self::assertEquals( + $customOptionsValues[$counter]['value'], + $customizableOptionsOutput[$counter]['values'][0]['value'] + ); + } + } + + /** + * Test adding a virtual product with empty values for required options + * + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual_with_options.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddVirtualProductWithNoRequiredOptionsSet() + { + $sku = 'virtual'; + $quantity = 1; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $query = <<<QUERY +mutation { + addVirtualProductsToCart( + input: { + cart_id: "{$maskedQuoteId}", + cartItems: [ + { + data: { + quantity: $quantity + sku: "$sku" + } + } + ] + } + ) { + cart { + items { + ... on VirtualCartItem { + customizable_options { + label + values { + value + } + } + } + } + } + } +} +QUERY; + + self::expectExceptionMessage( + 'The product\'s required option(s) weren\'t entered. Make sure the options are entered and try again.' + ); + + $this->graphQlMutation($query); + } + + /** + * Generate an array with test values for customizable options + * based on the option type + * + * @param string $sku + * @return array + */ + private function getCustomOptionsValuesForQuery(string $sku): array + { + $customOptions = $this->productCustomOptionsRepository->getList($sku); + $customOptionsValues = []; + + foreach ($customOptions as $customOption) { + $optionType = $customOption->getType(); + if ($optionType == 'field' || $optionType == 'area') { + $customOptionsValues[] = [ + 'id' => (int) $customOption->getOptionId(), + 'value' => 'test' + ]; + } elseif ($optionType == 'drop_down') { + $optionSelectValues = $customOption->getValues(); + $customOptionsValues[] = [ + 'id' => (int) $customOption->getOptionId(), + 'value' => reset($optionSelectValues)->getOptionTypeId() + ]; + } + } + + return $customOptionsValues; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/AddSimpleProductToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/AddSimpleProductToCartTest.php new file mode 100644 index 000000000000..be22d860df12 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/AddSimpleProductToCartTest.php @@ -0,0 +1,179 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\Framework\Exception\AuthenticationException; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test adding simple product to Cart + */ +class AddSimpleProductToCartTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + */ + public function testAddSimpleProductToCart() + { + $sku = 'simple_product'; + $quantity = 2; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $sku, $quantity); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('cart', $response['addSimpleProductsToCart']); + self::assertEquals($quantity, $response['addSimpleProductsToCart']['cart']['items'][0]['quantity']); + self::assertEquals($sku, $response['addSimpleProductsToCart']['cart']['items'][0]['product']['sku']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testAddProductToNonExistentCart() + { + $sku = 'simple_product'; + $quantity = 2; + $maskedQuoteId = 'non_existent_masked_id'; + + $query = $this->getQuery($maskedQuoteId, $sku, $quantity); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * + * @expectedException \Exception + * @expectedExceptionMessage Could not find a product with SKU "simple_product" + */ + public function testNonExistentProductToCart() + { + $sku = 'simple_product'; + $qty = 2; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $sku, $qty); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + */ + public function testAddSimpleProductToGuestCart() + { + $sku = 'simple_product'; + $qty = 2; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $sku, $qty); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + */ + public function testAddSimpleProductToAnotherCustomerCart() + { + $sku = 'simple_product'; + $qty = 2; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $sku, $qty); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + + $this->graphQlMutation($query, [], '', $this->getHeaderMap('customer2@search.example.com')); + } + + /** + * @param string $maskedQuoteId + * @param string $sku + * @param float $quantity + * @return string + */ + private function getQuery(string $maskedQuoteId, string $sku, float $quantity): string + { + return <<<QUERY +mutation { + addSimpleProductsToCart(input: { + cart_id: "{$maskedQuoteId}", + cartItems: [ + { + data: { + quantity: {$quantity} + sku: "{$sku}" + } + } + ] + }) { + cart { + items { + id + quantity + product { + sku + } + } + } + } +} +QUERY; + } + + /** + * Retrieve customer authorization headers + * + * @param string $username + * @param string $password + * @return array + * @throws AuthenticationException + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/AddVirtualProductToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/AddVirtualProductToCartTest.php new file mode 100644 index 000000000000..1269b9d8e7b3 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/AddVirtualProductToCartTest.php @@ -0,0 +1,179 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\Framework\Exception\AuthenticationException; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test adding virtual product to Cart + */ +class AddVirtualProductToCartTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/virtual_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + */ + public function testAddVirtualProductToCart() + { + $sku = 'virtual_product'; + $quantity = 2; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $sku, $quantity); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('cart', $response['addVirtualProductsToCart']); + self::assertEquals($quantity, $response['addVirtualProductsToCart']['cart']['items'][0]['quantity']); + self::assertEquals($sku, $response['addVirtualProductsToCart']['cart']['items'][0]['product']['sku']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/virtual_product.php + * + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testAddVirtualToNonExistentCart() + { + $sku = 'virtual_product'; + $qty = 2; + $nonExistentMaskedQuoteId = 'non_existent_masked_id'; + + $query = $this->getQuery($nonExistentMaskedQuoteId, $sku, $qty); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * + * @expectedException \Exception + * @expectedExceptionMessage Could not find a product with SKU "virtual_product" + */ + public function testNonExistentProductToCart() + { + $sku = 'virtual_product'; + $qty = 2; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $sku, $qty); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/virtual_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + */ + public function testAddVirtualProductToGuestCart() + { + $sku = 'virtual_product'; + $qty = 2; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $sku, $qty); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/virtual_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + */ + public function testAddVirtualProductToAnotherCustomerCart() + { + $sku = 'virtual_product'; + $qty = 2; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $sku, $qty); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + + $this->graphQlMutation($query, [], '', $this->getHeaderMap('customer2@search.example.com')); + } + + /** + * @param string $maskedQuoteId + * @param string $sku + * @param float $quantity + * @return string + */ + private function getQuery(string $maskedQuoteId, string $sku, float $quantity): string + { + return <<<QUERY +mutation { + addVirtualProductsToCart(input: { + cart_id: "{$maskedQuoteId}", + cartItems: [ + { + data: { + quantity: {$quantity} + sku: "{$sku}" + } + } + ] + }) { + cart { + items { + id + quantity + product { + sku + } + } + } + } +} +QUERY; + } + + /** + * Retrieve customer authorization headers + * + * @param string $username + * @param string $password + * @return array + * @throws AuthenticationException + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/ApplyCouponToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/ApplyCouponToCartTest.php new file mode 100644 index 000000000000..5a2221a18429 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/ApplyCouponToCartTest.php @@ -0,0 +1,283 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\Framework\Exception\AuthenticationException; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test Apply Coupon to Cart functionality for customer + */ +class ApplyCouponToCartTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/discount_10percent_generalusers.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testApplyCouponToCart() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('applyCouponToCart', $response); + self::assertEquals($couponCode, $response['applyCouponToCart']['cart']['applied_coupon']['code']); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/discount_10percent_generalusers.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @expectedException \Exception + * @expectedExceptionMessage A coupon is already applied to the cart. Please remove it to apply another + */ + public function testApplyCouponTwice() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey("applyCouponToCart", $response); + self::assertEquals($couponCode, $response['applyCouponToCart']['cart']['applied_coupon']['code']); + + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/discount_10percent_generalusers.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @expectedException \Exception + * @expectedExceptionMessage Cart does not contain products. + */ + public function testApplyCouponToCartWithoutItems() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + * @expectedException \Exception + */ + public function testApplyCouponToGuestCart() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + + self::expectExceptionMessage('The current user cannot perform operations on cart "' . $maskedQuoteId . '"'); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Checkout/_files/discount_10percent_generalusers.php + * @magentoApiDataFixture Magento/Customer/_files/two_customers.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @expectedException \Exception + */ + public function testApplyCouponToAnotherCustomerCart() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + + self::expectExceptionMessage('The current user cannot perform operations on cart "' . $maskedQuoteId . '"'); + $this->graphQlMutation($query, [], '', $this->getHeaderMap('customer_two@example.com')); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @expectedException \Exception + * @expectedExceptionMessage The coupon code isn't valid. Verify the code and try again. + */ + public function testApplyNonExistentCouponToCart() + { + $couponCode = 'non_existent_coupon_code'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + * @expectedException \Exception + */ + public function testApplyCouponToNonExistentCart() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = 'non_existent_masked_id'; + $query = $this->getQuery($maskedQuoteId, $couponCode); + + self::expectExceptionMessage('Could not find a cart with ID "' . $maskedQuoteId . '"'); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/discount_10percent_generalusers.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/make_coupon_expired.php + * @expectedException \Exception + * @expectedExceptionMessage The coupon code isn't valid. Verify the code and try again. + */ + public function testApplyExpiredCoupon() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/574'); + $couponCode = '2?ds5!2d'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * Products in cart don't fit to the coupon + * + * @magentoApiDataFixture Magento/Checkout/_files/discount_10percent_generalusers.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/restrict_coupon_usage_for_simple_product.php + * @expectedException \Exception + * @expectedExceptionMessage The coupon code isn't valid. Verify the code and try again. + */ + public function testApplyCouponWhichIsNotApplicable() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @param string $input + * @param string $message + * @dataProvider dataProviderUpdateWithMissedRequiredParameters + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @expectedException \Exception + */ + public function testApplyCouponWithMissedRequiredParameters(string $input, string $message) + { + $query = <<<QUERY +mutation { + applyCouponToCart(input: {{$input}}) { + cart { + applied_coupon { + code + } + } + } +} +QUERY; + + $this->expectExceptionMessage($message); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @return array + */ + public function dataProviderUpdateWithMissedRequiredParameters(): array + { + return [ + 'missed_cart_id' => [ + 'coupon_code: "test"', + 'Required parameter "cart_id" is missing' + ], + 'missed_coupon_code' => [ + 'cart_id: "test"', + 'Required parameter "coupon_code" is missing' + ], + ]; + } + + /** + * Retrieve customer authorization headers + * + * @param string $username + * @param string $password + * @return array + * @throws AuthenticationException + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } + + /** + * @param string $maskedQuoteId + * @param string $couponCode + * @return string + */ + private function getQuery(string $maskedQuoteId, string $couponCode): string + { + return <<<QUERY +mutation { + applyCouponToCart(input: {cart_id: "$maskedQuoteId", coupon_code: "$couponCode"}) { + cart { + applied_coupon { + code + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CartTotalsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CartTotalsTest.php new file mode 100644 index 000000000000..bb8acfce629f --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CartTotalsTest.php @@ -0,0 +1,208 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test getting cart totals for registered customer + */ +class CartTotalsTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/apply_tax_for_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + */ + public function testGetCartTotalsWithTaxApplied() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('prices', $response['cart']); + $pricesResponse = $response['cart']['prices']; + self::assertEquals(21.5, $pricesResponse['grand_total']['value']); + self::assertEquals(21.5, $pricesResponse['subtotal_including_tax']['value']); + self::assertEquals(20, $pricesResponse['subtotal_excluding_tax']['value']); + self::assertEquals(20, $pricesResponse['subtotal_with_discount_excluding_tax']['value']); + + $appliedTaxesResponse = $pricesResponse['applied_taxes']; + + self::assertEquals('US-TEST-*-Rate-1', $appliedTaxesResponse[0]['label']); + self::assertEquals(1.5, $appliedTaxesResponse[0]['amount']['value']); + self::assertEquals('USD', $appliedTaxesResponse[0]['amount']['currency']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + */ + public function testGetTotalsWithNoTaxApplied() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + + $pricesResponse = $response['cart']['prices']; + self::assertEquals(20, $pricesResponse['grand_total']['value']); + self::assertEquals(20, $pricesResponse['subtotal_including_tax']['value']); + self::assertEquals(20, $pricesResponse['subtotal_excluding_tax']['value']); + self::assertEquals(20, $pricesResponse['subtotal_with_discount_excluding_tax']['value']); + self::assertEmpty($pricesResponse['applied_taxes']); + } + + /** + * The totals calculation is based on quote address. + * But the totals should be calculated even if no address is set + * + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testGetCartTotalsWithNoAddressSet() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + + $pricesResponse = $response['cart']['prices']; + self::assertEquals(20, $pricesResponse['grand_total']['value']); + self::assertEquals(20, $pricesResponse['subtotal_including_tax']['value']); + self::assertEquals(20, $pricesResponse['subtotal_excluding_tax']['value']); + self::assertEquals(20, $pricesResponse['subtotal_with_discount_excluding_tax']['value']); + self::assertEmpty($pricesResponse['applied_taxes']); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/apply_tax_for_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + */ + public function testGetTotalsFromGuestCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/apply_tax_for_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + */ + public function testGetTotalsFromAnotherCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlQuery($query, [], '', $this->getHeaderMap('customer3@search.example.com')); + } + + /** + * Generates GraphQl query for retrieving cart totals + * + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +{ + cart(cart_id: "$maskedQuoteId") { + prices { + grand_total { + value, + currency + } + subtotal_including_tax { + value + currency + } + subtotal_excluding_tax { + value + currency + } + subtotal_with_discount_excluding_tax { + value + currency + } + applied_taxes { + label + amount { + value + currency + } + } + } + } +} +QUERY; + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CheckoutEndToEndTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CheckoutEndToEndTest.php new file mode 100644 index 000000000000..92974f2491f7 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CheckoutEndToEndTest.php @@ -0,0 +1,524 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Registry; +use Magento\Quote\Model\ResourceModel\Quote\CollectionFactory as QuoteCollectionFactory; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\ResourceModel\Order\CollectionFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * End to checkout tests for customer + */ +class CheckoutEndToEndTest extends GraphQlAbstract +{ + /** + * @var Registry + */ + private $registry; + + /** + * @var QuoteCollectionFactory + */ + private $quoteCollectionFactory; + + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteIdMaskFactory + */ + private $quoteIdMaskFactory; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var CollectionFactory + */ + private $orderCollectionFactory; + + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @var array + */ + private $headers = []; + + protected function setUp() + { + parent::setUp(); + + $objectManager = Bootstrap::getObjectManager(); + $this->registry = $objectManager->get(Registry::class); + $this->quoteCollectionFactory = $objectManager->get(QuoteCollectionFactory::class); + $this->quoteResource = $objectManager->get(QuoteResource::class); + $this->quoteIdMaskFactory = $objectManager->get(QuoteIdMaskFactory::class); + $this->customerRepository = Bootstrap::getObjectManager()->get(CustomerRepositoryInterface::class); + $this->orderCollectionFactory = $objectManager->get(CollectionFactory::class); + $this->orderRepository = $objectManager->get(OrderRepositoryInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_attribute.php + */ + public function testCheckoutWorkflow() + { + $quantity = 2; + + $this->createCustomer(); + $token = $this->loginCustomer(); + $this->headers = ['Authorization' => 'Bearer ' . $token]; + + $sku = $this->findProduct(); + $cartId = $this->createEmptyCart(); + $this->addProductToCart($cartId, $quantity, $sku); + + $this->setBillingAddress($cartId); + $shippingMethod = $this->setShippingAddress($cartId); + + $paymentMethod = $this->setShippingMethod($cartId, $shippingMethod); + $this->setPaymentMethod($cartId, $paymentMethod); + + $orderId = $this->placeOrder($cartId); + $this->checkOrderInHistory($orderId); + } + + /** + * @return void + */ + private function createCustomer(): void + { + $query = <<<QUERY +mutation { + createCustomer( + input: { + firstname: "endto" + lastname: "endtester" + email: "customer@example.com" + password: "123123Qa" + } + ) { + customer { + id + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * @return string + */ + private function loginCustomer(): string + { + $query = <<<QUERY +mutation { + generateCustomerToken( + email: "customer@example.com" + password: "123123Qa" + ) { + token + } +} +QUERY; + $response = $this->graphQlMutation($query); + self::assertArrayHasKey('generateCustomerToken', $response); + self::assertArrayHasKey('token', $response['generateCustomerToken']); + self::assertNotEmpty($response['generateCustomerToken']['token']); + + return $response['generateCustomerToken']['token']; + } + + /** + * @return string + */ + private function findProduct(): string + { + $query = <<<QUERY +{ + products ( + filter: { + sku: { + like:"simple%" + } + } + pageSize: 1 + currentPage: 1 + ) { + items { + sku + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + self::assertArrayHasKey('products', $response); + self::assertArrayHasKey('items', $response['products']); + self::assertCount(1, $response['products']['items']); + + $product = current($response['products']['items']); + self::assertArrayHasKey('sku', $product); + self::assertNotEmpty($product['sku']); + + return $product['sku']; + } + + /** + * @return string + */ + private function createEmptyCart(): string + { + $query = <<<QUERY +mutation { + createEmptyCart +} +QUERY; + $response = $this->graphQlMutation($query, [], '', $this->headers); + self::assertArrayHasKey('createEmptyCart', $response); + self::assertNotEmpty($response['createEmptyCart']); + + return $response['createEmptyCart']; + } + + /** + * @param string $cartId + * @param float $qty + * @param string $sku + * @return void + */ + private function addProductToCart(string $cartId, float $qty, string $sku): void + { + $query = <<<QUERY +mutation { + addSimpleProductsToCart( + input: { + cart_id: "{$cartId}" + cartItems: [ + { + data: { + quantity: {$qty} + sku: "{$sku}" + } + } + ] + } + ) { + cart { + items { + quantity + product { + sku + } + } + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->headers); + } + + /** + * @param string $cartId + * @param array $auth + * @return array + */ + private function setBillingAddress(string $cartId): void + { + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "{$cartId}" + billing_address: { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + postcode: "887766" + telephone: "88776655" + region: "TX" + country_code: "US" + save_in_address_book: false + } + } + } + ) { + cart { + billing_address { + __typename + } + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->headers); + } + + /** + * @param string $cartId + * @return array + */ + private function setShippingAddress(string $cartId): array + { + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$cartId" + shipping_addresses: [ + { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "TX" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + ] + } + ) { + cart { + shipping_addresses { + available_shipping_methods { + carrier_code + method_code + amount + } + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query, [], '', $this->headers); + self::assertArrayHasKey('setShippingAddressesOnCart', $response); + self::assertArrayHasKey('cart', $response['setShippingAddressesOnCart']); + self::assertArrayHasKey('shipping_addresses', $response['setShippingAddressesOnCart']['cart']); + self::assertCount(1, $response['setShippingAddressesOnCart']['cart']['shipping_addresses']); + + $shippingAddress = current($response['setShippingAddressesOnCart']['cart']['shipping_addresses']); + self::assertArrayHasKey('available_shipping_methods', $shippingAddress); + self::assertCount(1, $shippingAddress['available_shipping_methods']); + + $availableShippingMethod = current($shippingAddress['available_shipping_methods']); + self::assertArrayHasKey('carrier_code', $availableShippingMethod); + self::assertNotEmpty($availableShippingMethod['carrier_code']); + + self::assertArrayHasKey('method_code', $availableShippingMethod); + self::assertNotEmpty($availableShippingMethod['method_code']); + + self::assertArrayHasKey('amount', $availableShippingMethod); + self::assertNotEmpty($availableShippingMethod['amount']); + + return $availableShippingMethod; + } + + /** + * @param string $cartId + * @param array $method + * @return array + */ + private function setShippingMethod(string $cartId, array $method): array + { + $query = <<<QUERY +mutation { + setShippingMethodsOnCart(input: { + cart_id: "{$cartId}", + shipping_methods: [ + { + carrier_code: "{$method['carrier_code']}" + method_code: "{$method['method_code']}" + } + ] + }) { + cart { + available_payment_methods { + code + title + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query, [], '', $this->headers); + self::assertArrayHasKey('setShippingMethodsOnCart', $response); + self::assertArrayHasKey('cart', $response['setShippingMethodsOnCart']); + self::assertArrayHasKey('available_payment_methods', $response['setShippingMethodsOnCart']['cart']); + self::assertCount(1, $response['setShippingMethodsOnCart']['cart']['available_payment_methods']); + + $availablePaymentMethod = current($response['setShippingMethodsOnCart']['cart']['available_payment_methods']); + self::assertArrayHasKey('code', $availablePaymentMethod); + self::assertNotEmpty($availablePaymentMethod['code']); + self::assertArrayHasKey('title', $availablePaymentMethod); + self::assertNotEmpty($availablePaymentMethod['title']); + + return $availablePaymentMethod; + } + + /** + * @param string $cartId + * @param array $method + * @return void + */ + private function setPaymentMethod(string $cartId, array $method): void + { + $query = <<<QUERY +mutation { + setPaymentMethodOnCart( + input: { + cart_id: "{$cartId}" + payment_method: { + code: "{$method['code']}" + } + } + ) { + cart { + selected_payment_method { + code + } + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->headers); + } + + /** + * @param string $cartId + * @return string + */ + private function placeOrder(string $cartId): string + { + $query = <<<QUERY +mutation { + placeOrder( + input: { + cart_id: "{$cartId}" + } + ) { + order { + order_id + } + } +} +QUERY; + $response = $this->graphQlMutation($query, [], '', $this->headers); + self::assertArrayHasKey('placeOrder', $response); + self::assertArrayHasKey('order', $response['placeOrder']); + self::assertArrayHasKey('order_id', $response['placeOrder']['order']); + self::assertNotEmpty($response['placeOrder']['order']['order_id']); + + return $response['placeOrder']['order']['order_id']; + } + + /** + * @param string $orderId + * @return void + */ + private function checkOrderInHistory(string $orderId): void + { + $query = <<<QUERY +{ + customerOrders { + items { + increment_id + grand_total + } + } +} +QUERY; + $response = $this->graphQlQuery($query, [], '', $this->headers); + self::assertArrayHasKey('customerOrders', $response); + self::assertArrayHasKey('items', $response['customerOrders']); + self::assertCount(1, $response['customerOrders']['items']); + + $order = current($response['customerOrders']['items']); + self::assertArrayHasKey('increment_id', $order); + self::assertEquals($orderId, $order['increment_id']); + + self::assertArrayHasKey('grand_total', $order); + } + + public function tearDown() + { + $this->deleteCustomer(); + $this->deleteQuote(); + $this->deleteOrder(); + parent::tearDown(); + } + + /** + * @return void + */ + private function deleteCustomer(): void + { + $email = 'customer@example.com'; + try { + $customer = $this->customerRepository->get($email); + } catch (\Exception $exception) { + return; + } + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', true); + $this->customerRepository->delete($customer); + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', false); + } + + /** + * @return void + */ + private function deleteQuote(): void + { + $quoteCollection = $this->quoteCollectionFactory->create(); + foreach ($quoteCollection as $quote) { + $this->quoteResource->delete($quote); + + $quoteIdMask = $this->quoteIdMaskFactory->create(); + $quoteIdMask->setQuoteId($quote->getId()) + ->delete(); + } + } + + /** + * @return void + */ + private function deleteOrder() + { + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', true); + + $orderCollection = $this->orderCollectionFactory->create(); + foreach ($orderCollection as $order) { + $this->orderRepository->delete($order); + } + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', false); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CreateEmptyCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CreateEmptyCartTest.php new file mode 100644 index 000000000000..fbd0cf19740d --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CreateEmptyCartTest.php @@ -0,0 +1,196 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\Quote\Model\ResourceModel\Quote\CollectionFactory as QuoteCollectionFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Quote\Api\GuestCartRepositoryInterface; + +/** + * Test for empty cart creation mutation for customer + */ +class CreateEmptyCartTest extends GraphQlAbstract +{ + /** + * @var GuestCartRepositoryInterface + */ + private $guestCartRepository; + + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var QuoteCollectionFactory + */ + private $quoteCollectionFactory; + + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteIdMaskFactory + */ + private $quoteIdMaskFactory; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->quoteCollectionFactory = $objectManager->get(QuoteCollectionFactory::class); + $this->guestCartRepository = $objectManager->get(GuestCartRepositoryInterface::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->quoteResource = $objectManager->get(QuoteResource::class); + $this->quoteIdMaskFactory = $objectManager->get(QuoteIdMaskFactory::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testCreateEmptyCart() + { + $query = $this->getQuery(); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMapWithCustomerToken()); + + self::assertArrayHasKey('createEmptyCart', $response); + self::assertNotEmpty($response['createEmptyCart']); + + $guestCart = $this->guestCartRepository->get($response['createEmptyCart']); + + self::assertNotNull($guestCart->getId()); + self::assertEquals(1, $guestCart->getCustomer()->getId()); + self::assertEquals('default', $guestCart->getStore()->getCode()); + } + + /** + * @magentoApiDataFixture Magento/Store/_files/second_store.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testCreateEmptyCartWithNotDefaultStore() + { + $query = $this->getQuery(); + + $headerMap = $this->getHeaderMapWithCustomerToken(); + $headerMap['Store'] = 'fixture_second_store'; + $response = $this->graphQlMutation($query, [], '', $headerMap); + + self::assertArrayHasKey('createEmptyCart', $response); + self::assertNotEmpty($response['createEmptyCart']); + + /* guestCartRepository is used for registered customer to get the cart hash */ + $guestCart = $this->guestCartRepository->get($response['createEmptyCart']); + + self::assertNotNull($guestCart->getId()); + self::assertEquals(1, $guestCart->getCustomer()->getId()); + self::assertEquals('fixture_second_store', $guestCart->getStore()->getCode()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testCreateEmptyCartWithPredefinedCartId() + { + $predefinedCartId = '572cda51902b5b517c0e1a2b2fd004b4'; + + $query = <<<QUERY +mutation { + createEmptyCart (input: {cart_id: "{$predefinedCartId}"}) +} +QUERY; + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMapWithCustomerToken()); + + self::assertArrayHasKey('createEmptyCart', $response); + self::assertEquals($predefinedCartId, $response['createEmptyCart']); + + $guestCart = $this->guestCartRepository->get($response['createEmptyCart']); + self::assertNotNull($guestCart->getId()); + self::assertEquals(1, $guestCart->getCustomer()->getId()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * + * @expectedException \Exception + * @expectedExceptionMessage Cart with ID "572cda51902b5b517c0e1a2b2fd004b4" already exists. + */ + public function testCreateEmptyCartIfPredefinedCartIdAlreadyExists() + { + $predefinedCartId = '572cda51902b5b517c0e1a2b2fd004b4'; + + $query = <<<QUERY +mutation { + createEmptyCart (input: {cart_id: "{$predefinedCartId}"}) +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getHeaderMapWithCustomerToken()); + $this->graphQlMutation($query, [], '', $this->getHeaderMapWithCustomerToken()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * + * @expectedException \Exception + * @expectedExceptionMessage Cart ID length should to be 32 symbols. + */ + public function testCreateEmptyCartWithWrongPredefinedCartId() + { + $predefinedCartId = '572'; + + $query = <<<QUERY +mutation { + createEmptyCart (input: {cart_id: "{$predefinedCartId}"}) +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getHeaderMapWithCustomerToken()); + } + + /** + * @return string + */ + private function getQuery(): string + { + return <<<QUERY +mutation { + createEmptyCart +} +QUERY; + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMapWithCustomerToken( + string $username = 'customer@example.com', + string $password = 'password' + ): array { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } + + public function tearDown() + { + $quoteCollection = $this->quoteCollectionFactory->create(); + foreach ($quoteCollection as $quote) { + $this->quoteResource->delete($quote); + + $quoteIdMask = $this->quoteIdMaskFactory->create(); + $quoteIdMask->setQuoteId($quote->getId()) + ->delete(); + } + parent::tearDown(); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetAvailablePaymentMethodsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetAvailablePaymentMethodsTest.php new file mode 100644 index 000000000000..673d49630266 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetAvailablePaymentMethodsTest.php @@ -0,0 +1,160 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for get available payment methods + */ +class GetAvailablePaymentMethodsTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testGetAvailablePaymentMethods() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('available_payment_methods', $response['cart']); + + self::assertEquals('checkmo', $response['cart']['available_payment_methods'][0]['code']); + self::assertEquals('Check / Money order', $response['cart']['available_payment_methods'][0]['title']); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testGetAvailablePaymentMethodsFromGuestCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testGetAvailablePaymentMethodsFromAnotherCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlQuery($query, [], '', $this->getHeaderMap('customer3@search.example.com')); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/disable_all_active_payment_methods.php + */ + public function testGetAvailablePaymentMethodsIfPaymentsAreNotPresent() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('available_payment_methods', $response['cart']); + self::assertEmpty($response['cart']['available_payment_methods']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testGetAvailablePaymentMethodsOfNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $query = $this->getQuery($maskedQuoteId); + + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +{ + cart(cart_id: "$maskedQuoteId") { + available_payment_methods { + code + title + } + } +} +QUERY; + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetAvailableShippingMethodsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetAvailableShippingMethodsTest.php new file mode 100644 index 000000000000..2b647f61c4c6 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetAvailableShippingMethodsTest.php @@ -0,0 +1,188 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for get available shipping methods + */ +class GetAvailableShippingMethodsTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * Test case: get available shipping methods from current customer quote + * + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testGetAvailableShippingMethods() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $response = $this->graphQlQuery($this->getQuery($maskedQuoteId), [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('shipping_addresses', $response['cart']); + self::assertCount(1, $response['cart']['shipping_addresses']); + self::assertArrayHasKey('available_shipping_methods', $response['cart']['shipping_addresses'][0]); + self::assertCount(1, $response['cart']['shipping_addresses'][0]['available_shipping_methods']); + + $expectedAddressData = [ + 'amount' => 10, + 'base_amount' => 10, + 'carrier_code' => 'flatrate', + 'carrier_title' => 'Flat Rate', + 'error_message' => '', + 'method_code' => 'flatrate', + 'method_title' => 'Fixed', + 'price_incl_tax' => 10, + 'price_excl_tax' => 10, + ]; + self::assertEquals( + $expectedAddressData, + $response['cart']['shipping_addresses'][0]['available_shipping_methods'][0] + ); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testGetAvailableShippingMethodsFromGuestCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * Test case: get available shipping methods from quote of another customer + * + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testGetAvailableShippingMethodsFromAnotherCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlQuery($query, [], '', $this->getHeaderMap('customer3@search.example.com')); + } + + /** + * Test case: get available shipping methods when all shipping methods are disabled + * + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/disable_offline_shipping_methods.php + */ + public function testGetAvailableShippingMethodsIfShippingMethodsAreNotPresent() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $response = $this->graphQlQuery($this->getQuery($maskedQuoteId), [], '', $this->getHeaderMap()); + + self::assertEmpty($response['cart']['shipping_addresses'][0]['available_shipping_methods']); + } + + /** + * Test case: get available shipping methods from non-existent cart + * + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testGetAvailableShippingMethodsOfNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $query = $this->getQuery($maskedQuoteId); + + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +query { + cart (cart_id: "{$maskedQuoteId}") { + shipping_addresses { + available_shipping_methods { + amount + base_amount + carrier_code + carrier_title + error_message + method_code + method_title + price_excl_tax + price_incl_tax + } + } + } +} +QUERY; + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCartEmailTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCartEmailTest.php new file mode 100644 index 000000000000..951fe08db5e3 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCartEmailTest.php @@ -0,0 +1,125 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for getting email from cart + */ +class GetCartEmailTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + */ + public function testGetCartEmail() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + + $this->assertArrayHasKey('cart', $response); + $this->assertArrayHasKey('email', $response['cart']); + $this->assertEquals('customer@example.com', $response['cart']['email']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testGetCartEmailFromNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $query = $this->getQuery($maskedQuoteId); + + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/set_guest_email.php + */ + public function testGetEmailFromGuestCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"{$maskedQuoteId}\"" + ); + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + */ + public function testGetEmailFromAnotherCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"{$maskedQuoteId}\"" + ); + $this->graphQlMutation($query, [], '', $this->getHeaderMap('customer3@search.example.com')); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +{ + cart(cart_id:"$maskedQuoteId") { + email + } +} +QUERY; + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCartTest.php new file mode 100644 index 000000000000..99e1c0bbd157 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCartTest.php @@ -0,0 +1,211 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for getting cart information + */ +class GetCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_virtual_product.php + */ + public function testGetCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('items', $response['cart']); + self::assertCount(2, $response['cart']['items']); + + self::assertNotEmpty($response['cart']['items'][0]['id']); + self::assertEquals(2, $response['cart']['items'][0]['quantity']); + self::assertEquals('simple_product', $response['cart']['items'][0]['product']['sku']); + + self::assertNotEmpty($response['cart']['items'][1]['id']); + self::assertEquals(2, $response['cart']['items'][1]['quantity']); + self::assertEquals('virtual-product', $response['cart']['items'][1]['product']['sku']); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + */ + public function testGetGuestCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"{$maskedQuoteId}\"" + ); + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + */ + public function testGetAnotherCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"{$maskedQuoteId}\"" + ); + $this->graphQlQuery($query, [], '', $this->getHeaderMap('customer2@search.example.com')); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testGetNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $query = $this->getQuery($maskedQuoteId); + + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/make_cart_inactive.php + * + * @expectedException \Exception + * @expectedExceptionMessage Current user does not have an active cart. + */ + public function testGetInactiveCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/active_quote_customer_not_default_store.php + */ + public function testGetCartWithNotDefaultStore() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1_not_default_store'); + $query = $this->getQuery($maskedQuoteId); + + $headerMap = $this->getHeaderMap(); + $headerMap['Store'] = 'fixture_second_store'; + + $response = $this->graphQlQuery($query, [], '', $headerMap); + + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('items', $response['cart']); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + * @magentoApiDataFixture Magento/Store/_files/second_store.php + * + * @expectedException \Exception + * @expectedExceptionMessage Wrong store code specified for cart + */ + public function testGetCartWithWrongStore() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + $query = $this->getQuery($maskedQuoteId); + + $headerMap = $this->getHeaderMap(); + $headerMap['Store'] = 'fixture_second_store'; + + $this->graphQlQuery($query, [], '', $headerMap); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/active_quote_customer_not_default_store.php + * + * @expectedException \Exception + * @expectedExceptionMessage Requested store is not found + */ + public function testGetCartWithNotExistingStore() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1_not_default_store'); + $query = $this->getQuery($maskedQuoteId); + + $headerMap = $this->getHeaderMap(); + $headerMap['Store'] = 'not_existing_store'; + + $this->graphQlQuery($query, [], '', $headerMap); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +{ + cart(cart_id: "{$maskedQuoteId}") { + items { + id + quantity + product { + sku + } + } + } +} +QUERY; + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSelectedShippingMethodTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSelectedShippingMethodTest.php new file mode 100644 index 000000000000..5575830ea51c --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSelectedShippingMethodTest.php @@ -0,0 +1,215 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for get selected shipping method + */ +class GetSelectedShippingMethodTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testGetSelectedShippingMethod() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('shipping_addresses', $response['cart']); + self::assertCount(1, $response['cart']['shipping_addresses']); + + $shippingAddress = current($response['cart']['shipping_addresses']); + self::assertArrayHasKey('selected_shipping_method', $shippingAddress); + + self::assertArrayHasKey('carrier_code', $shippingAddress['selected_shipping_method']); + self::assertEquals('flatrate', $shippingAddress['selected_shipping_method']['carrier_code']); + + self::assertArrayHasKey('method_code', $shippingAddress['selected_shipping_method']); + self::assertEquals('flatrate', $shippingAddress['selected_shipping_method']['method_code']); + + self::assertArrayHasKey('carrier_title', $shippingAddress['selected_shipping_method']); + self::assertEquals('Flat Rate', $shippingAddress['selected_shipping_method']['carrier_title']); + + self::assertArrayHasKey('method_title', $shippingAddress['selected_shipping_method']); + self::assertEquals('Fixed', $shippingAddress['selected_shipping_method']['method_title']); + + self::assertArrayHasKey('amount', $shippingAddress['selected_shipping_method']); + $amount = $shippingAddress['selected_shipping_method']['amount']; + + self::assertArrayHasKey('value', $amount); + self::assertEquals(10, $amount['value']); + self::assertArrayHasKey('currency', $amount); + self::assertEquals('USD', $amount['currency']); + + self::assertArrayHasKey('base_amount', $shippingAddress['selected_shipping_method']); + $baseAmount = $shippingAddress['selected_shipping_method']['base_amount']; + + self::assertArrayHasKey('value', $baseAmount); + self::assertEquals(10, $baseAmount['value']); + self::assertArrayHasKey('currency', $baseAmount); + self::assertEquals('USD', $baseAmount['currency']); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testGetSelectedShippingMethodFromGuestCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testGetSelectedShippingMethodFromAnotherCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlQuery($query, [], '', $this->getHeaderMap('customer3@search.example.com')); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testGetGetSelectedShippingMethodIfShippingMethodIsNotSet() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('shipping_addresses', $response['cart']); + self::assertCount(1, $response['cart']['shipping_addresses']); + + $shippingAddress = current($response['cart']['shipping_addresses']); + self::assertArrayHasKey('selected_shipping_method', $shippingAddress); + + self::assertNull($shippingAddress['selected_shipping_method']['carrier_code']); + self::assertNull($shippingAddress['selected_shipping_method']['method_code']); + self::assertNull($shippingAddress['selected_shipping_method']['carrier_title']); + self::assertNull($shippingAddress['selected_shipping_method']['method_title']); + self::assertNull($shippingAddress['selected_shipping_method']['amount']); + self::assertNull($shippingAddress['selected_shipping_method']['base_amount']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testGetGetSelectedShippingMethodOfNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $query = $this->getQuery($maskedQuoteId); + + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +{ + cart(cart_id: "$maskedQuoteId") { + shipping_addresses { + selected_shipping_method { + carrier_code + method_code + carrier_title + method_title + amount { + value + currency + } + base_amount { + value + currency + } + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSpecifiedBillingAddressTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSpecifiedBillingAddressTest.php new file mode 100644 index 000000000000..1ff5ddbde54e --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSpecifiedBillingAddressTest.php @@ -0,0 +1,219 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for get specified billing address + */ +class GetSpecifiedBillingAddressTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + */ + public function testGeSpecifiedBillingAddress() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('billing_address', $response['cart']); + + $expectedBillingAddressData = [ + 'firstname' => 'John', + 'lastname' => 'Smith', + 'company' => 'CompanyName', + 'street' => [ + 'Green str, 67' + ], + 'city' => 'CityM', + 'region' => [ + 'code' => 'AL', + 'label' => 'Alabama', + ], + 'postcode' => '75477', + 'country' => [ + 'code' => 'US', + 'label' => 'US', + ], + 'telephone' => '3468676', + '__typename' => 'BillingCartAddress', + 'customer_notes' => null, + ]; + self::assertEquals($expectedBillingAddressData, $response['cart']['billing_address']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testGeSpecifiedBillingAddressIfBillingAddressIsNotSet() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('billing_address', $response['cart']); + + $expectedBillingAddressData = [ + 'firstname' => null, + 'lastname' => null, + 'company' => null, + 'street' => [ + '' + ], + 'city' => null, + 'region' => [ + 'code' => null, + 'label' => null, + ], + 'postcode' => null, + 'country' => [ + 'code' => null, + 'label' => null, + ], + 'telephone' => null, + '__typename' => 'BillingCartAddress', + 'customer_notes' => null, + ]; + self::assertEquals($expectedBillingAddressData, $response['cart']['billing_address']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testGeSpecifiedBillingAddressOfNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $query = $this->getQuery($maskedQuoteId); + + $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + */ + public function testGeSpecifiedBillingAddressFromAnotherGuestCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlQuery($this->getQuery($maskedQuoteId), [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + */ + public function testGeSpecifiedBillingAddressFromAnotherCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlQuery( + $this->getQuery($maskedQuoteId), + [], + '', + $this->getHeaderMap('customer2@search.example.com') + ); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +{ + cart(cart_id: "$maskedQuoteId") { + billing_address { + firstname + lastname + company + street + city + region + { + code + label + } + postcode + country + { + code + label + } + telephone + __typename + customer_notes + } + } +} +QUERY; + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/PlaceOrderTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/PlaceOrderTest.php new file mode 100644 index 000000000000..47d0d661fb33 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/PlaceOrderTest.php @@ -0,0 +1,295 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\Framework\Registry; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\ResourceModel\Order\CollectionFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for placing an order for customer + */ +class PlaceOrderTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var CollectionFactory + */ + private $orderCollectionFactory; + + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @var Registry + */ + private $registry; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->orderCollectionFactory = $objectManager->get(CollectionFactory::class); + $this->orderRepository = $objectManager->get(OrderRepositoryInterface::class); + $this->registry = Bootstrap::getObjectManager()->get(Registry::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_payment_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_checkmo_payment_method.php + */ + public function testPlaceOrder() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('placeOrder', $response); + self::assertArrayHasKey('order_id', $response['placeOrder']['order']); + self::assertEquals($reservedOrderId, $response['placeOrder']['order']['order_id']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + */ + public function testPlaceOrderWithNoItemsInCart() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessage( + 'Unable to place order: A server error stopped your order from being placed. ' . + 'Please try to place your order again' + ); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testPlaceOrderWithNoShippingAddress() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessage( + 'Unable to place order: Some addresses can\'t be used due to the configurations for specific countries' + ); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testPlaceOrderWithNoShippingMethod() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessage( + 'Unable to place order: The shipping method is missing. Select the shipping method and try again' + ); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testPlaceOrderWithNoBillingAddress() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessageRegExp( + '/Unable to place order: Please check the billing address information*/' + ); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testPlaceOrderWithNoPaymentMethod() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessage('Unable to place order: Enter a valid payment method and try again'); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/set_simple_product_out_of_stock.php + */ + public function testPlaceOrderWithOutOfStockProduct() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessage('Unable to place order: Some of the products are out of stock'); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_payment_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_checkmo_payment_method.php + */ + public function testPlaceOrderOfGuestCart() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessageRegExp('/The current user cannot perform operations on cart*/'); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_payment_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_checkmo_payment_method.php + */ + public function testPlaceOrderOfAnotherCustomerCart() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessageRegExp('/The current user cannot perform operations on cart*/'); + $this->graphQlMutation($query, [], '', $this->getHeaderMap('customer3@search.example.com')); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +mutation { + placeOrder(input: {cart_id: "{$maskedQuoteId}"}) { + order { + order_id + } + } +} +QUERY; + } + + /** + * @param string $username + * @param string $password + * @return array + * @throws \Magento\Framework\Exception\AuthenticationException + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } + + /** + * @inheritdoc + */ + public function tearDown() + { + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', true); + + $orderCollection = $this->orderCollectionFactory->create(); + foreach ($orderCollection as $order) { + $this->orderRepository->delete($order); + } + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', false); + + parent::tearDown(); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/RemoveCouponFromCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/RemoveCouponFromCartTest.php new file mode 100644 index 000000000000..ce1c85417b16 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/RemoveCouponFromCartTest.php @@ -0,0 +1,169 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Check removing of the coupon from customer cart + */ +class RemoveCouponFromCartTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/Checkout/_files/discount_10percent_generalusers.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/apply_coupon.php + */ + public function testRemoveCouponFromCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('removeCouponFromCart', $response); + self::assertNull($response['removeCouponFromCart']['cart']['applied_coupon']['code']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testRemoveCouponFromNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $query = $this->getQuery($maskedQuoteId); + + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @expectedException \Exception + * @expectedExceptionMessage Cart does not contain products + */ + public function testRemoveCouponFromEmptyCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testRemoveCouponFromCartIfCouponWasNotSet() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('removeCouponFromCart', $response); + self::assertNull($response['removeCouponFromCart']['cart']['applied_coupon']['code']); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/apply_coupon.php + */ + public function testRemoveCouponFromGuestCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessage('The current user cannot perform operations on cart "' . $maskedQuoteId . '"'); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/Checkout/_files/discount_10percent_generalusers.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/apply_coupon.php + */ + public function testRemoveCouponFromAnotherCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessage('The current user cannot perform operations on cart "' . $maskedQuoteId . '"'); + $this->graphQlMutation($query, [], '', $this->getHeaderMap('customer3@search.example.com')); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +mutation { + removeCouponFromCart(input: {cart_id: "$maskedQuoteId"}) { + cart { + applied_coupon { + code + } + } + } +} + +QUERY; + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/RemoveItemFromCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/RemoveItemFromCartTest.php new file mode 100644 index 000000000000..5f953d43355f --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/RemoveItemFromCartTest.php @@ -0,0 +1,238 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\GraphQl\Quote\GetQuoteItemIdByReservedQuoteIdAndSku; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for removeItemFromCartTest mutation + */ +class RemoveItemFromCartTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var GetQuoteItemIdByReservedQuoteIdAndSku + */ + private $getQuoteItemIdByReservedQuoteIdAndSku; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->getQuoteItemIdByReservedQuoteIdAndSku = $objectManager->get( + GetQuoteItemIdByReservedQuoteIdAndSku::class + ); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testRemoveItemFromCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $itemId = $this->getQuoteItemIdByReservedQuoteIdAndSku->execute('test_quote', 'simple_product'); + + $query = $this->getQuery($maskedQuoteId, $itemId); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + $this->assertArrayHasKey('removeItemFromCart', $response); + $this->assertArrayHasKey('cart', $response['removeItemFromCart']); + $this->assertCount(0, $response['removeItemFromCart']['cart']['items']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testRemoveItemFromNonExistentCart() + { + $query = $this->getQuery('non_existent_masked_id', 1); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testRemoveNonExistentItem() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $notExistentItemId = 999; + + $this->expectExceptionMessage("Cart doesn't contain the {$notExistentItemId} item."); + + $query = $this->getQuery($maskedQuoteId, $notExistentItemId); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @param string $input + * @param string $message + * @dataProvider dataProviderUpdateWithMissedRequiredParameters + */ + public function testUpdateWithMissedItemRequiredParameters(string $input, string $message) + { + $query = <<<QUERY +mutation { + removeItemFromCart( + input: { + {$input} + } + ) { + cart { + items { + quantity + } + } + } +} +QUERY; + $this->expectExceptionMessage($message); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @return array + */ + public function dataProviderUpdateWithMissedRequiredParameters(): array + { + return [ + 'missed_cart_id' => [ + 'cart_item_id: 1', + 'Required parameter "cart_id" is missing.' + ], + 'missed_cart_item_id' => [ + 'cart_id: "test"', + 'Required parameter "cart_item_id" is missing.' + ], + ]; + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_virtual_product_and_address.php + */ + public function testRemoveItemIfItemIsNotBelongToCart() + { + $firstQuoteMaskedId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $secondQuoteItemId = $this->getQuoteItemIdByReservedQuoteIdAndSku->execute( + 'test_order_with_virtual_product', + 'virtual-product' + ); + + $this->expectExceptionMessage("Cart doesn't contain the {$secondQuoteItemId} item."); + + $query = $this->getQuery($firstQuoteMaskedId, $secondQuoteItemId); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testRemoveItemFromGuestCart() + { + $guestQuoteMaskedId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $guestQuoteItemId = $this->getQuoteItemIdByReservedQuoteIdAndSku->execute('test_quote', 'simple_product'); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$guestQuoteMaskedId\"" + ); + + $query = $this->getQuery($guestQuoteMaskedId, $guestQuoteItemId); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testRemoveItemFromAnotherCustomerCart() + { + $anotherCustomerQuoteMaskedId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $anotherCustomerQuoteItemId = $this->getQuoteItemIdByReservedQuoteIdAndSku->execute( + 'test_quote', + 'simple_product' + ); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$anotherCustomerQuoteMaskedId\"" + ); + + $query = $this->getQuery($anotherCustomerQuoteMaskedId, $anotherCustomerQuoteItemId); + $this->graphQlMutation($query, [], '', $this->getHeaderMap('customer2@search.example.com')); + } + + /** + * @param string $maskedQuoteId + * @param int $itemId + * @return string + */ + private function getQuery(string $maskedQuoteId, int $itemId): string + { + return <<<QUERY +mutation { + removeItemFromCart( + input: { + cart_id: "{$maskedQuoteId}" + cart_item_id: {$itemId} + } + ) { + cart { + items { + quantity + } + } + } +} +QUERY; + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetBillingAddressOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetBillingAddressOnCartTest.php new file mode 100644 index 000000000000..fc73d88be1f8 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetBillingAddressOnCartTest.php @@ -0,0 +1,628 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for set billing address on cart mutation + */ +class SetBillingAddressOnCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedId; + + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->quoteResource = $objectManager->get(QuoteResource::class); + $this->quoteFactory = $objectManager->get(QuoteFactory::class); + $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetNewBillingAddress() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + } + ) { + cart { + billing_address { + firstname + lastname + company + street + city + postcode + telephone + country { + code + label + } + __typename + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('cart', $response['setBillingAddressOnCart']); + $cartResponse = $response['setBillingAddressOnCart']['cart']; + self::assertArrayHasKey('billing_address', $cartResponse); + $billingAddressResponse = $cartResponse['billing_address']; + $this->assertNewAddressFields($billingAddressResponse); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetNewBillingAddressWithUseForShippingParameter() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + use_for_shipping: true + } + } + ) { + cart { + billing_address { + firstname + lastname + company + street + city + postcode + telephone + country { + code + label + } + __typename + } + shipping_addresses { + firstname + lastname + company + street + city + postcode + telephone + country { + code + label + } + __typename + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('cart', $response['setBillingAddressOnCart']); + $cartResponse = $response['setBillingAddressOnCart']['cart']; + self::assertArrayHasKey('billing_address', $cartResponse); + $billingAddressResponse = $cartResponse['billing_address']; + self::assertArrayHasKey('shipping_addresses', $cartResponse); + $shippingAddressResponse = current($cartResponse['shipping_addresses']); + $this->assertNewAddressFields($billingAddressResponse); + $this->assertNewAddressFields($shippingAddressResponse, 'ShippingCartAddress'); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Customer/_files/customer_two_addresses.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetBillingAddressFromAddressBook() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + customer_address_id: 1 + } + } + ) { + cart { + billing_address { + firstname + lastname + company + street + city + postcode + telephone + country { + code + label + } + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('cart', $response['setBillingAddressOnCart']); + $cartResponse = $response['setBillingAddressOnCart']['cart']; + self::assertArrayHasKey('billing_address', $cartResponse); + $billingAddressResponse = $cartResponse['billing_address']; + $this->assertSavedBillingAddressFields($billingAddressResponse); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * + * @expectedException \Exception + * @expectedExceptionMessage Could not find a address with ID "100" + */ + public function testSetNotExistedBillingAddressFromAddressBook() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + customer_address_id: 100 + } + } + ) { + cart { + billing_address { + city + } + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Customer/_files/customer_two_addresses.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetNewBillingAddressAndFromAddressBookAtSameTime() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + customer_address_id: 1 + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + } + ) { + cart { + billing_address { + city + } + } + } +} +QUERY; + + self::expectExceptionMessage( + 'The billing address cannot contain "customer_address_id" and "address" at the same time.' + ); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Customer/_files/customer_address.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetBillingAddressToGuestCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + customer_address_id: 1 + } + } + ) { + cart { + billing_address { + city + } + } + } +} +QUERY; + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"{$maskedQuoteId}\"" + ); + + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/Customer/_files/customer_address.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + */ + public function testSetBillingAddressToAnotherCustomerCart() + { + $maskedQuoteId = $this->assignQuoteToCustomer('test_order_with_simple_product_without_address', 2); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + customer_address_id: 1 + } + } + ) { + cart { + billing_address { + city + } + } + } +} +QUERY; + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"{$maskedQuoteId}\"" + ); + + $this->graphQlMutation($query, [], '', $this->getHeaderMap('customer@search.example.com')); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/Customer/_files/customer_address.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * + * @expectedException \Exception + * @expectedExceptionMessage Current customer does not have permission to address with ID "1" + */ + public function testSetBillingAddressIfCustomerIsNotOwnerOfAddress() + { + $maskedQuoteId = $this->assignQuoteToCustomer('test_order_with_simple_product_without_address', 2); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + customer_address_id: 1 + } + } + ) { + cart { + billing_address { + city + } + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getHeaderMap('customer2@search.example.com')); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Customer/_files/customer_address.php + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testSetBillingAddressOnNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + customer_address_id: 1 + } + } + ) { + cart { + billing_address { + city + } + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * + * @dataProvider dataProviderSetWithoutRequiredParameters + * @param string $input + * @param string $message + * @throws \Exception + */ + public function testSetBillingAddressWithoutRequiredParameters(string $input, string $message) + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $input = str_replace('cart_id_value', $maskedQuoteId, $input); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + {$input} + } + ) { + cart { + billing_address { + city + } + } + } +} +QUERY; + + $this->expectExceptionMessage($message); + $this->graphQlMutation($query); + } + + /** + * @return array + */ + public function dataProviderSetWithoutRequiredParameters(): array + { + return [ + 'missed_billing_address' => [ + 'cart_id: "cart_id_value"', + 'Field SetBillingAddressOnCartInput.billing_address of required type BillingAddressInput!' + . ' was not provided.', + ], + 'missed_cart_id' => [ + 'billing_address: {}', + 'Required parameter "cart_id" is missing' + ] + ]; + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetNewBillingAddressWithRedundantStreetLine() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2", "test street 3"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + } + ) { + cart { + billing_address { + firstname + } + } + } +} +QUERY; + self::expectExceptionMessage('"Street Address" cannot contain more than 2 lines.'); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * Verify the all the whitelisted fields for a New Address Object + * + * @param array $addressResponse + * @param string $addressType + */ + private function assertNewAddressFields(array $addressResponse, string $addressType = 'BillingCartAddress'): 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' => '__typename', 'expected_value' => $addressType] + ]; + + $this->assertResponseFields($addressResponse, $assertionMap); + } + + /** + * Verify the all the whitelisted fields for a Address Object + * + * @param array $billingAddressResponse + */ + private function assertSavedBillingAddressFields(array $billingAddressResponse): void + { + $assertionMap = [ + ['response_field' => 'firstname', 'expected_value' => 'John'], + ['response_field' => 'lastname', 'expected_value' => 'Smith'], + ['response_field' => 'company', 'expected_value' => 'CompanyName'], + ['response_field' => 'street', 'expected_value' => [0 => 'Green str, 67']], + ['response_field' => 'city', 'expected_value' => 'CityM'], + ['response_field' => 'postcode', 'expected_value' => '75477'], + ['response_field' => 'telephone', 'expected_value' => '3468676'], + ['response_field' => 'country', 'expected_value' => ['code' => 'US', 'label' => 'US']], + ]; + + $this->assertResponseFields($billingAddressResponse, $assertionMap); + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } + + /** + * @param string $reservedOrderId + * @param int $customerId + * @return string + */ + private function assignQuoteToCustomer( + string $reservedOrderId = 'test_order_with_simple_product_without_address', + int $customerId = 1 + ): string { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reservedOrderId, 'reserved_order_id'); + $quote->setCustomerId($customerId); + $this->quoteResource->save($quote); + return $this->quoteIdToMaskedId->execute((int)$quote->getId()); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetGuestEmailOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetGuestEmailOnCartTest.php new file mode 100644 index 000000000000..a4a84c2f8c74 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetGuestEmailOnCartTest.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for setGuestEmailOnCart mutation + */ +class SetGuestEmailOnCartTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * + * @expectedException \Exception + * @expectedExceptionMessage The request is not allowed for logged in customers + */ + public function testSetGuestEmailOnCartForLoggedInCustomer() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $email = 'some@user.com'; + + $query = $this->getQuery($maskedQuoteId, $email); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * + * @expectedException \Exception + * @expectedExceptionMessage The request is not allowed for logged in customers + */ + public function testSetGuestEmailOnGuestCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $email = 'some@user.com'; + + $query = $this->getQuery($maskedQuoteId, $email); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * Returns GraphQl mutation query for setting email address for a guest + * + * @param string $maskedQuoteId + * @param string $email + * @return string + */ + private function getQuery(string $maskedQuoteId, string $email): string + { + return <<<QUERY +mutation { + setGuestEmailOnCart(input: { + cart_id: "$maskedQuoteId" + email: "$email" + }) { + cart { + email + } + } +} +QUERY; + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetOfflinePaymentMethodsOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetOfflinePaymentMethodsOnCartTest.php new file mode 100644 index 000000000000..0fe7aec2d634 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetOfflinePaymentMethodsOnCartTest.php @@ -0,0 +1,177 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\OfflinePayments\Model\Banktransfer; +use Magento\OfflinePayments\Model\Cashondelivery; +use Magento\OfflinePayments\Model\Checkmo; +use Magento\OfflinePayments\Model\Purchaseorder; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for setting offline payment methods on cart + */ +class SetOfflinePaymentMethodsOnCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_payment_methods.php + * + * @param string $methodCode + * @param string $methodTitle + * @dataProvider offlinePaymentMethodDataProvider + */ + public function testSetOfflinePaymentMethod(string $methodCode, string $methodTitle) + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('setPaymentMethodOnCart', $response); + self::assertArrayHasKey('cart', $response['setPaymentMethodOnCart']); + self::assertArrayHasKey('selected_payment_method', $response['setPaymentMethodOnCart']['cart']); + + $selectedPaymentMethod = $response['setPaymentMethodOnCart']['cart']['selected_payment_method']; + self::assertArrayHasKey('code', $selectedPaymentMethod); + self::assertEquals($methodCode, $selectedPaymentMethod['code']); + + self::assertArrayHasKey('title', $selectedPaymentMethod); + self::assertEquals($methodTitle, $selectedPaymentMethod['title']); + } + + /** + * @return array + */ + public function offlinePaymentMethodDataProvider(): array + { + return [ + 'check_mo' => [Checkmo::PAYMENT_METHOD_CHECKMO_CODE, 'Check / Money order'], + 'bank_transfer' => [Banktransfer::PAYMENT_METHOD_BANKTRANSFER_CODE, 'Bank Transfer Payment'], + 'cash_on_delivery' => [Cashondelivery::PAYMENT_METHOD_CASHONDELIVERY_CODE, 'Cash On Delivery'], + ]; + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_payment_methods.php + */ + public function testSetPurchaseOrderPaymentMethod() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $methodTitle = 'Purchase Order'; + $methodCode = Purchaseorder::PAYMENT_METHOD_PURCHASEORDER_CODE; + $poNumber = 'abc123'; + + $query = <<<QUERY +mutation { + setPaymentMethodOnCart(input: { + cart_id: "{$maskedQuoteId}", + payment_method: { + code: "{$methodCode}" + purchase_order_number: "{$poNumber}" + } + }) { + cart { + selected_payment_method { + code + title + purchase_order_number + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('setPaymentMethodOnCart', $response); + self::assertArrayHasKey('cart', $response['setPaymentMethodOnCart']); + self::assertArrayHasKey('selected_payment_method', $response['setPaymentMethodOnCart']['cart']); + + $selectedPaymentMethod = $response['setPaymentMethodOnCart']['cart']['selected_payment_method']; + self::assertArrayHasKey('code', $selectedPaymentMethod); + self::assertEquals($methodCode, $selectedPaymentMethod['code']); + + self::assertArrayHasKey('title', $selectedPaymentMethod); + self::assertEquals($methodTitle, $selectedPaymentMethod['title']); + + self::assertArrayHasKey('purchase_order_number', $selectedPaymentMethod); + self::assertEquals($poNumber, $selectedPaymentMethod['purchase_order_number']); + } + + /** + * @param string $maskedQuoteId + * @param string $methodCode + * @return string + */ + private function getQuery( + string $maskedQuoteId, + string $methodCode + ) : string { + return <<<QUERY +mutation { + setPaymentMethodOnCart(input: { + cart_id: "{$maskedQuoteId}", + payment_method: { + code: "{$methodCode}" + } + }) { + cart { + selected_payment_method { + code + title + } + } + } +} +QUERY; + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetOfflineShippingMethodsOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetOfflineShippingMethodsOnCartTest.php new file mode 100644 index 000000000000..c6c4a18bce4a --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetOfflineShippingMethodsOnCartTest.php @@ -0,0 +1,190 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for setting offline shipping methods on cart + */ +class SetOfflineShippingMethodsOnCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @magentoApiDataFixture Magento/OfflineShipping/_files/tablerates_weight.php + * + * @param string $carrierCode + * @param string $methodCode + * @param string $carrierTitle + * @param string $methodTitle + * @param array $amount + * @param array $baseAmount + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @dataProvider offlineShippingMethodDataProvider + */ + public function testSetOfflineShippingMethod( + string $carrierCode, + string $methodCode, + string $carrierTitle, + string $methodTitle, + array $amount, + array $baseAmount + ) { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery( + $maskedQuoteId, + $methodCode, + $carrierCode + ); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('setShippingMethodsOnCart', $response); + self::assertArrayHasKey('cart', $response['setShippingMethodsOnCart']); + self::assertArrayHasKey('shipping_addresses', $response['setShippingMethodsOnCart']['cart']); + self::assertCount(1, $response['setShippingMethodsOnCart']['cart']['shipping_addresses']); + + $shippingAddress = current($response['setShippingMethodsOnCart']['cart']['shipping_addresses']); + self::assertArrayHasKey('selected_shipping_method', $shippingAddress); + + self::assertArrayHasKey('carrier_code', $shippingAddress['selected_shipping_method']); + self::assertEquals($carrierCode, $shippingAddress['selected_shipping_method']['carrier_code']); + + self::assertArrayHasKey('method_code', $shippingAddress['selected_shipping_method']); + self::assertEquals($methodCode, $shippingAddress['selected_shipping_method']['method_code']); + + self::assertArrayHasKey('carrier_title', $shippingAddress['selected_shipping_method']); + self::assertEquals($carrierTitle, $shippingAddress['selected_shipping_method']['carrier_title']); + + self::assertArrayHasKey('method_title', $shippingAddress['selected_shipping_method']); + self::assertEquals($methodTitle, $shippingAddress['selected_shipping_method']['method_title']); + + self::assertArrayHasKey('amount', $shippingAddress['selected_shipping_method']); + self::assertEquals($amount, $shippingAddress['selected_shipping_method']['amount']); + + self::assertArrayHasKey('base_amount', $shippingAddress['selected_shipping_method']); + self::assertEquals($baseAmount, $shippingAddress['selected_shipping_method']['base_amount']); + } + + /** + * @return array + */ + public function offlineShippingMethodDataProvider(): array + { + return [ + 'flatrate_flatrate' => [ + 'flatrate', + 'flatrate', + 'Flat Rate', + 'Fixed', + ['value' => 10, 'currency' => 'USD'], + ['value' => 10, 'currency' => 'USD'], + ], + 'tablerate_bestway' => [ + 'tablerate', + 'bestway', + 'Best Way', + 'Table Rate', + ['value' => 10, 'currency' => 'USD'], + ['value' => 10, 'currency' => 'USD'], + ], + 'freeshipping_freeshipping' => [ + 'freeshipping', + 'freeshipping', + 'Free Shipping', + 'Free', + ['value' => 0, 'currency' => 'USD'], + ['value' => 0, 'currency' => 'USD'], + ], + ]; + } + + /** + * @param string $maskedQuoteId + * @param string $shippingMethodCode + * @param string $shippingCarrierCode + * @return string + */ + private function getQuery( + string $maskedQuoteId, + string $shippingMethodCode, + string $shippingCarrierCode + ): string { + return <<<QUERY +mutation { + setShippingMethodsOnCart(input: + { + cart_id: "$maskedQuoteId", + shipping_methods: [{ + carrier_code: "$shippingCarrierCode" + method_code: "$shippingMethodCode" + }] + }) { + cart { + shipping_addresses { + selected_shipping_method { + carrier_code + method_code + carrier_title + method_title + amount { + value + currency + } + base_amount { + value + currency + } + } + } + } + } +} +QUERY; + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodOnCartTest.php new file mode 100644 index 000000000000..593bb8c9fcb7 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodOnCartTest.php @@ -0,0 +1,310 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Exception; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\OfflinePayments\Model\Cashondelivery; +use Magento\OfflinePayments\Model\Checkmo; +use Magento\OfflinePayments\Model\Purchaseorder; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for setting payment methods on cart by customer + */ +class SetPaymentMethodOnCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testSetPaymentOnCartWithSimpleProduct() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('setPaymentMethodOnCart', $response); + self::assertArrayHasKey('cart', $response['setPaymentMethodOnCart']); + self::assertArrayHasKey('selected_payment_method', $response['setPaymentMethodOnCart']['cart']); + self::assertEquals($methodCode, $response['setPaymentMethodOnCart']['cart']['selected_payment_method']['code']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * + * @expectedException Exception + * @expectedExceptionMessage The shipping address is missing. Set the address and try again. + */ + public function testSetPaymentOnCartWithSimpleProductAndWithoutAddress() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_virtual_product.php + */ + public function testSetPaymentOnCartWithVirtualProduct() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('setPaymentMethodOnCart', $response); + self::assertArrayHasKey('cart', $response['setPaymentMethodOnCart']); + self::assertArrayHasKey('selected_payment_method', $response['setPaymentMethodOnCart']['cart']); + self::assertEquals($methodCode, $response['setPaymentMethodOnCart']['cart']['selected_payment_method']['code']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * + * @expectedException Exception + * @expectedExceptionMessage The requested Payment Method is not available. + */ + public function testSetNonExistentPaymentMethod() + { + $methodCode = 'noway'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * + * @expectedException Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testSetPaymentOnNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + + $query = $this->getQuery($maskedQuoteId, $methodCode); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetPaymentMethodToGuestCart() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetPaymentMethodToAnotherCustomerCart() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlMutation($query, [], '', $this->getHeaderMap('customer2@search.example.com')); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * + * @param string $input + * @param string $message + * @throws Exception + * @dataProvider dataProviderSetPaymentMethodWithoutRequiredParameters + */ + public function testSetPaymentMethodWithoutRequiredParameters(string $input, string $message) + { + $query = <<<QUERY +mutation { + setPaymentMethodOnCart( + input: { + {$input} + } + ) { + cart { + items { + quantity + } + } + } +} +QUERY; + $this->expectExceptionMessage($message); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @expectedException Exception + * @expectedExceptionMessage The requested Payment Method is not available. + */ + public function testSetDisabledPaymentOnCart() + { + $methodCode = Purchaseorder::PAYMENT_METHOD_PURCHASEORDER_CODE; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @return array + */ + public function dataProviderSetPaymentMethodWithoutRequiredParameters(): array + { + return [ + 'missed_cart_id' => [ + 'payment_method: {code: "' . Checkmo::PAYMENT_METHOD_CHECKMO_CODE . '"}', + 'Required parameter "cart_id" is missing.' + ], + 'missed_payment_method' => [ + 'cart_id: "test"', + 'Required parameter "code" for "payment_method" is missing.' + ], + 'missed_payment_method_code' => [ + 'cart_id: "test", payment_method: {code: ""}', + 'Required parameter "code" for "payment_method" is missing.' + ], + ]; + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_payment_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_checkmo_payment_method.php + */ + public function testReSetPayment() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $methodCode = Cashondelivery::PAYMENT_METHOD_CASHONDELIVERY_CODE; + $query = $this->getQuery($maskedQuoteId, $methodCode); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('setPaymentMethodOnCart', $response); + self::assertArrayHasKey('cart', $response['setPaymentMethodOnCart']); + self::assertArrayHasKey('selected_payment_method', $response['setPaymentMethodOnCart']['cart']); + self::assertArrayHasKey('code', $response['setPaymentMethodOnCart']['cart']['selected_payment_method']); + self::assertEquals($methodCode, $response['setPaymentMethodOnCart']['cart']['selected_payment_method']['code']); + } + + /** + * @param string $maskedQuoteId + * @param string $methodCode + * @return string + */ + private function getQuery( + string $maskedQuoteId, + string $methodCode + ) : string { + return <<<QUERY +mutation { + setPaymentMethodOnCart(input: { + cart_id: "$maskedQuoteId" + payment_method: { + code: "$methodCode" + } + }) { + cart { + selected_payment_method { + code + } + } + } +} +QUERY; + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPurchaseOrderPaymentMethodOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPurchaseOrderPaymentMethodOnCartTest.php new file mode 100644 index 000000000000..bff66ece59b9 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPurchaseOrderPaymentMethodOnCartTest.php @@ -0,0 +1,169 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Exception; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\OfflinePayments\Model\Purchaseorder; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for setting Purchase Order payment method on cart by customer + */ +class SetPurchaseOrderPaymentMethodOnCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_payment_methods.php + */ + public function testSetPurchaseOrderPaymentMethodOnCartWithSimpleProduct() + { + $methodCode = Purchaseorder::PAYMENT_METHOD_PURCHASEORDER_CODE; + $purchaseOrderNumber = '123456'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setPaymentMethodOnCart(input: { + cart_id: "$maskedQuoteId" + payment_method: { + code: "$methodCode" + purchase_order_number: "$purchaseOrderNumber" + } + }) { + cart { + selected_payment_method { + code + purchase_order_number + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('setPaymentMethodOnCart', $response); + self::assertArrayHasKey('cart', $response['setPaymentMethodOnCart']); + self::assertArrayHasKey('selected_payment_method', $response['setPaymentMethodOnCart']['cart']); + self::assertEquals($methodCode, $response['setPaymentMethodOnCart']['cart']['selected_payment_method']['code']); + self::assertEquals( + $purchaseOrderNumber, + $response['setPaymentMethodOnCart']['cart']['selected_payment_method']['purchase_order_number'] + ); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_payment_methods.php + * + * @expectedException Exception + * @expectedExceptionMessage Purchase order number is a required field. + */ + public function testSetPurchaseOrderPaymentMethodOnCartWithoutPurchaseOrderNumber() + { + $methodCode = Purchaseorder::PAYMENT_METHOD_PURCHASEORDER_CODE; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setPaymentMethodOnCart(input: { + cart_id: "$maskedQuoteId" + payment_method: { + code: "$methodCode" + } + }) { + cart { + selected_payment_method { + code + } + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * + * @expectedException Exception + * @expectedExceptionMessage The requested Payment Method is not available. + */ + public function testSetDisabledPurchaseOrderPaymentMethodOnCart() + { + $methodCode = Purchaseorder::PAYMENT_METHOD_PURCHASEORDER_CODE; + $purchaseOrderNumber = '123456'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setPaymentMethodOnCart(input: { + cart_id: "$maskedQuoteId" + payment_method: { + code: "$methodCode" + purchase_order_number: "$purchaseOrderNumber" + } + }) { + cart { + selected_payment_method { + code + purchase_order_number + } + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingAddressOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingAddressOnCartTest.php new file mode 100644 index 000000000000..5d7a073d2d6d --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingAddressOnCartTest.php @@ -0,0 +1,604 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for set shipping addresses on cart mutation + */ +class SetShippingAddressOnCartTest extends GraphQlAbstract +{ + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedId; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->quoteResource = $objectManager->get(QuoteResource::class); + $this->quoteFactory = $objectManager->get(QuoteFactory::class); + $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetNewShippingAddressOnCartWithSimpleProduct() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + ] + } + ) { + cart { + shipping_addresses { + firstname + lastname + company + street + city + postcode + telephone + country { + label + code + } + __typename + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('cart', $response['setShippingAddressesOnCart']); + $cartResponse = $response['setShippingAddressesOnCart']['cart']; + self::assertArrayHasKey('shipping_addresses', $cartResponse); + $shippingAddressResponse = current($cartResponse['shipping_addresses']); + $this->assertNewShippingAddressFields($shippingAddressResponse); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_virtual_product.php + * + * @expectedException \Exception + * @expectedExceptionMessage The Cart includes virtual product(s) only, so a shipping address is not used. + */ + public function testSetNewShippingAddressOnCartWithVirtualProduct() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + ] + } + ) { + cart { + shipping_addresses { + city + } + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Customer/_files/customer_two_addresses.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetShippingAddressFromAddressBook() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + customer_address_id: 1 + } + ] + } + ) { + cart { + shipping_addresses { + firstname + lastname + company + street + city + postcode + telephone + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('cart', $response['setShippingAddressesOnCart']); + $cartResponse = $response['setShippingAddressesOnCart']['cart']; + self::assertArrayHasKey('shipping_addresses', $cartResponse); + $shippingAddressResponse = current($cartResponse['shipping_addresses']); + $this->assertSavedShippingAddressFields($shippingAddressResponse); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * + * @expectedException \Exception + * @expectedExceptionMessage Could not find a address with ID "100" + */ + public function testSetNonExistentShippingAddressFromAddressBook() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + customer_address_id: 100 + } + ] + } + ) { + cart { + shipping_addresses { + city + } + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Customer/_files/customer_two_addresses.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetNewShippingAddressAndFromAddressBookAtSameTime() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + customer_address_id: 1, + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + ] + } + ) { + cart { + shipping_addresses { + city + } + } + } +} +QUERY; + self::expectExceptionMessage( + 'The shipping address cannot contain "customer_address_id" and "address" at the same time.' + ); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/Customer/_files/customer_address.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * + * @expectedException \Exception + * @expectedExceptionMessage Current customer does not have permission to address with ID "1" + */ + public function testSetShippingAddressIfCustomerIsNotOwnerOfAddress() + { + $maskedQuoteId = $this->assignQuoteToCustomer('test_order_with_simple_product_without_address', 2); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + customer_address_id: 1 + } + ] + } + ) { + cart { + shipping_addresses { + postcode + } + } + } +} +QUERY; + + $this->graphQlMutation($query, [], '', $this->getHeaderMap('customer2@search.example.com')); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/Customer/_files/customer_address.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * + * @expectedException \Exception + */ + public function testSetShippingAddressToAnotherCustomerCart() + { + $maskedQuoteId = $this->assignQuoteToCustomer('test_order_with_simple_product_without_address', 1); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + customer_address_id: 1 + } + ] + } + ) { + cart { + shipping_addresses { + postcode + } + } + } +} +QUERY; + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlMutation($query, [], '', $this->getHeaderMap('customer2@search.example.com')); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * + * @dataProvider dataProviderUpdateWithMissedRequiredParameters + * @param string $input + * @param string $message + * @throws \Exception + */ + public function testSetNewShippingAddressWithMissedRequiredParameters(string $input, string $message) + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "{$maskedQuoteId}" + shipping_addresses: [ + { + {$input} + } + ] + } + ) { + cart { + shipping_addresses { + city + } + } + } +} +QUERY; + $this->expectExceptionMessage($message); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @return array + */ + public function dataProviderUpdateWithMissedRequiredParameters(): array + { + return [ + 'shipping_addresses' => [ + '', + 'The shipping address must contain either "customer_address_id" or "address".', + ], + 'missed_city' => [ + 'address: { save_in_address_book: false }', + 'Field CartAddressInput.city of required type String! was not provided' + ] + ]; + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * + * @expectedException \Exception + * @expectedExceptionMessage You cannot specify multiple shipping addresses. + */ + public function testSetMultipleNewShippingAddresses() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + }, + { + address: { + firstname: "test firstname 2" + lastname: "test lastname 2" + company: "test company 2" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + ] + } + ) { + cart { + shipping_addresses { + city + } + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetNewShippingAddressOnCartWithRedundantStreetLine() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2", "test street 3"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + ] + } + ) { + cart { + shipping_addresses { + firstname + } + } + } +} +QUERY; + + self::expectExceptionMessage('"Street Address" cannot contain more than 2 lines.'); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * Verify the all the whitelisted fields for a New Address Object + * + * @param array $shippingAddressResponse + */ + private function assertNewShippingAddressFields(array $shippingAddressResponse): void + { + $assertionMap = [ + ['response_field' => 'firstname', 'expected_value' => 'test firstname'], + ['response_field' => 'lastname', 'expected_value' => 'test lastname'], + ['response_field' => 'company', 'expected_value' => 'test company'], + ['response_field' => 'street', 'expected_value' => [0 => 'test street 1', 1 => 'test street 2']], + ['response_field' => 'city', 'expected_value' => 'test city'], + ['response_field' => 'postcode', 'expected_value' => '887766'], + ['response_field' => 'telephone', 'expected_value' => '88776655'], + ['response_field' => 'country', 'expected_value' => ['code' => 'US', 'label' => 'US']], + ['response_field' => '__typename', 'expected_value' => 'ShippingCartAddress'] + ]; + + $this->assertResponseFields($shippingAddressResponse, $assertionMap); + } + + /** + * Verify the all the whitelisted fields for a Address Object + * + * @param array $shippingAddressResponse + */ + private function assertSavedShippingAddressFields(array $shippingAddressResponse): void + { + $assertionMap = [ + ['response_field' => 'firstname', 'expected_value' => 'John'], + ['response_field' => 'lastname', 'expected_value' => 'Smith'], + ['response_field' => 'company', 'expected_value' => 'CompanyName'], + ['response_field' => 'street', 'expected_value' => [0 => 'Green str, 67']], + ['response_field' => 'city', 'expected_value' => 'CityM'], + ['response_field' => 'postcode', 'expected_value' => '75477'], + ['response_field' => 'telephone', 'expected_value' => '3468676'] + ]; + + $this->assertResponseFields($shippingAddressResponse, $assertionMap); + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } + + /** + * @param string $reservedOrderId + * @param int $customerId + * @return string + */ + private function assignQuoteToCustomer( + string $reservedOrderId = 'test_order_with_simple_product_without_address', + int $customerId = 1 + ): string { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reservedOrderId, 'reserved_order_id'); + $quote->setCustomerId($customerId); + $this->quoteResource->save($quote); + return $this->quoteIdToMaskedId->execute((int)$quote->getId()); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingMethodsOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingMethodsOnCartTest.php new file mode 100644 index 000000000000..9ddedbfc4654 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingMethodsOnCartTest.php @@ -0,0 +1,423 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Customer; + +use Exception; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for setting shipping methods on cart for customer + */ +class SetShippingMethodsOnCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testSetShippingMethodOnCartWithSimpleProduct() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $carrierCode = 'flatrate'; + $methodCode = 'flatrate'; + + $query = $this->getQuery( + $maskedQuoteId, + $methodCode, + $carrierCode + ); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('setShippingMethodsOnCart', $response); + self::assertArrayHasKey('cart', $response['setShippingMethodsOnCart']); + self::assertArrayHasKey('shipping_addresses', $response['setShippingMethodsOnCart']['cart']); + self::assertCount(1, $response['setShippingMethodsOnCart']['cart']['shipping_addresses']); + + $shippingAddress = current($response['setShippingMethodsOnCart']['cart']['shipping_addresses']); + self::assertArrayHasKey('selected_shipping_method', $shippingAddress); + + self::assertArrayHasKey('carrier_code', $shippingAddress['selected_shipping_method']); + self::assertEquals('flatrate', $shippingAddress['selected_shipping_method']['carrier_code']); + + self::assertArrayHasKey('method_code', $shippingAddress['selected_shipping_method']); + self::assertEquals('flatrate', $shippingAddress['selected_shipping_method']['method_code']); + + self::assertArrayHasKey('carrier_title', $shippingAddress['selected_shipping_method']); + self::assertEquals('Flat Rate', $shippingAddress['selected_shipping_method']['carrier_title']); + + self::assertArrayHasKey('method_title', $shippingAddress['selected_shipping_method']); + self::assertEquals('Fixed', $shippingAddress['selected_shipping_method']['method_title']); + + self::assertArrayHasKey('amount', $shippingAddress['selected_shipping_method']); + $amount = $shippingAddress['selected_shipping_method']['amount']; + + self::assertArrayHasKey('value', $amount); + self::assertEquals(10, $amount['value']); + self::assertArrayHasKey('currency', $amount); + self::assertEquals('USD', $amount['currency']); + + self::assertArrayHasKey('base_amount', $shippingAddress['selected_shipping_method']); + $baseAmount = $shippingAddress['selected_shipping_method']['base_amount']; + + self::assertArrayHasKey('value', $baseAmount); + self::assertEquals(10, $baseAmount['value']); + self::assertArrayHasKey('currency', $baseAmount); + self::assertEquals('USD', $baseAmount['currency']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testReSetShippingMethod() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $carrierCode = 'freeshipping'; + $methodCode = 'freeshipping'; + + $query = $this->getQuery( + $maskedQuoteId, + $methodCode, + $carrierCode + ); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('setShippingMethodsOnCart', $response); + self::assertArrayHasKey('cart', $response['setShippingMethodsOnCart']); + self::assertArrayHasKey('shipping_addresses', $response['setShippingMethodsOnCart']['cart']); + self::assertCount(1, $response['setShippingMethodsOnCart']['cart']['shipping_addresses']); + + $shippingAddress = current($response['setShippingMethodsOnCart']['cart']['shipping_addresses']); + self::assertArrayHasKey('selected_shipping_method', $shippingAddress); + + self::assertArrayHasKey('carrier_code', $shippingAddress['selected_shipping_method']); + self::assertEquals($carrierCode, $shippingAddress['selected_shipping_method']['carrier_code']); + + self::assertArrayHasKey('method_code', $shippingAddress['selected_shipping_method']); + self::assertEquals($methodCode, $shippingAddress['selected_shipping_method']['method_code']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * + * @param string $input + * @param string $message + * @dataProvider dataProviderSetShippingMethodWithWrongParameters + * @throws Exception + */ + public function testSetShippingMethodWithWrongParameters(string $input, string $message) + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $input = str_replace('cart_id_value', $maskedQuoteId, $input); + + $query = <<<QUERY +mutation { + setShippingMethodsOnCart(input: { + {$input} + }) { + cart { + shipping_addresses { + selected_shipping_method { + carrier_code + } + } + } + } +} +QUERY; + $this->expectExceptionMessage($message); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function dataProviderSetShippingMethodWithWrongParameters(): array + { + return [ + 'missed_cart_id' => [ + 'shipping_methods: [{ + carrier_code: "flatrate" + method_code: "flatrate" + }]', + 'Required parameter "cart_id" is missing' + ], + 'missed_shipping_methods' => [ + 'cart_id: "cart_id_value"', + 'Required parameter "shipping_methods" is missing' + ], + 'shipping_methods_are_empty' => [ + 'cart_id: "cart_id_value" shipping_methods: []', + 'Required parameter "shipping_methods" is missing' + ], + 'missed_carrier_code' => [ + 'cart_id: "cart_id_value", shipping_methods: [{ + method_code: "flatrate" + }]', + 'Field ShippingMethodInput.carrier_code of required type String! was not provided.' + ], + 'empty_carrier_code' => [ + 'cart_id: "cart_id_value", shipping_methods: [{ + carrier_code: "" + method_code: "flatrate" + }]', + 'Required parameter "carrier_code" is missing.' + ], + 'non_existent_carrier_code' => [ + 'cart_id: "cart_id_value", shipping_methods: [{ + carrier_code: "wrong-carrier-code" + method_code: "flatrate" + }]', + 'Carrier with such method not found: wrong-carrier-code, flatrate' + ], + 'missed_method_code' => [ + 'cart_id: "cart_id_value", shipping_methods: [{ + carrier_code: "flatrate" + }]', + 'Required parameter "method_code" is missing.' + ], + 'empty_method_code' => [ + 'cart_id: "cart_id_value", shipping_methods: [{ + carrier_code: "flatrate" + method_code: "" + }]', + 'Required parameter "method_code" is missing.' + ], + 'non_existent_method_code' => [ + 'cart_id: "cart_id_value", shipping_methods: [{ + carrier_code: "flatrate" + method_code: "wrong-carrier-code" + }]', + 'Carrier with such method not found: flatrate, wrong-carrier-code' + ], + 'non_existent_shopping_cart' => [ + 'cart_id: "non_existent_masked_id", shipping_methods: [{ + carrier_code: "flatrate" + method_code: "flatrate" + }]', + 'Could not find a cart with ID "non_existent_masked_id"' + ], + 'disabled_shipping_method' => [ + 'cart_id: "cart_id_value", shipping_methods: [{ + carrier_code: "freeshipping" + method_code: "freeshipping" + }]', + 'Carrier with such method not found: freeshipping, freeshipping' + ] + ]; + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @expectedException Exception + * @expectedExceptionMessage You cannot specify multiple shipping methods. + */ + public function testSetMultipleShippingMethods() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingMethodsOnCart(input: { + cart_id: "{$maskedQuoteId}", + shipping_methods: [ + { + carrier_code: "flatrate" + method_code: "flatrate" + } + { + carrier_code: "flatrate" + method_code: "flatrate" + } + ] + }) { + cart { + shipping_addresses { + selected_shipping_method { + carrier_code + } + } + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * + * @expectedException Exception + */ + public function testSetShippingMethodToGuestCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $carrierCode = 'flatrate'; + $methodCode = 'flatrate'; + $query = $this->getQuery( + $maskedQuoteId, + $methodCode, + $carrierCode + ); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * + * @expectedException Exception + */ + public function testSetShippingMethodToAnotherCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $carrierCode = 'flatrate'; + $methodCode = 'flatrate'; + $query = $this->getQuery( + $maskedQuoteId, + $methodCode, + $carrierCode + ); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlMutation($query, [], '', $this->getHeaderMap('customer2@search.example.com')); + } + + /** + * @param string $maskedQuoteId + * @param string $shippingMethodCode + * @param string $shippingCarrierCode + * @return string + */ + private function getQuery( + string $maskedQuoteId, + string $shippingMethodCode, + string $shippingCarrierCode + ): string { + return <<<QUERY +mutation { + setShippingMethodsOnCart(input: + { + cart_id: "$maskedQuoteId", + shipping_methods: [{ + carrier_code: "$shippingCarrierCode" + method_code: "$shippingMethodCode" + }] + }) { + cart { + shipping_addresses { + selected_shipping_method { + carrier_code + method_code + carrier_title + method_title + amount { + value + currency + } + base_amount { + value + currency + } + } + } + } + } +} +QUERY; + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * + * @expectedException Exception + * @expectedExceptionMessage The shipping method can't be set for an empty cart. Add an item to cart and try again. + */ + public function testSetShippingMethodOnAnEmptyCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $carrierCode = 'flatrate'; + $methodCode = 'flatrate'; + + $query = $this->getQuery( + $maskedQuoteId, + $methodCode, + $carrierCode + ); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/UpdateCartItemsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/UpdateCartItemsTest.php new file mode 100644 index 000000000000..bc88bd1ddb43 --- /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 testUpdateCartItemQuantity() + { + $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(); + $quantity = 2; + + $query = $this->getQuery($maskedQuoteId, $itemId, $quantity); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + $this->assertArrayHasKey('updateCartItems', $response); + $this->assertArrayHasKey('cart', $response['updateCartItems']); + + $responseCart = $response['updateCartItems']['cart']; + $item = current($responseCart['items']); + + $this->assertEquals($itemId, $item['id']); + $this->assertEquals($quantity, $item['quantity']); + } + + /** + * @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(); + $quantity = 0; + + $query = $this->getQuery($maskedQuoteId, $itemId, $quantity); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + $this->assertArrayHasKey('updateCartItems', $response); + $this->assertArrayHasKey('cart', $response['updateCartItems']); + + $responseCart = $response['updateCartItems']['cart']; + $this->assertCount(0, $responseCart['items']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testUpdateItemInNonExistentCart() + { + $query = $this->getQuery('non_existent_masked_id', 1, 2); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php + */ + public function testUpdateNonExistentItem() + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, 'test_order_1', 'reserved_order_id'); + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$quote->getId()); + $notExistentItemId = 999; + + $this->expectExceptionMessage("Could not find cart item with id: {$notExistentItemId}."); + + $query = $this->getQuery($maskedQuoteId, $notExistentItemId, 2); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_virtual_product_saved.php + */ + public function testUpdateItemIfItemIsNotBelongToCart() + { + $firstQuote = $this->quoteFactory->create(); + $this->quoteResource->load($firstQuote, 'test_order_1', 'reserved_order_id'); + $firstQuoteMaskedId = $this->quoteIdToMaskedId->execute((int)$firstQuote->getId()); + + $secondQuote = $this->quoteFactory->create(); + $this->quoteResource->load( + $secondQuote, + 'test_order_with_virtual_product_without_address', + 'reserved_order_id' + ); + $secondQuote->setCustomerId(1); + $this->quoteResource->save($secondQuote); + $secondQuoteItemId = (int)$secondQuote + ->getItemByProduct($this->productRepository->get('virtual-product')) + ->getId(); + + $this->expectExceptionMessage("Could not find cart item with id: {$secondQuoteItemId}."); + + $query = $this->getQuery($firstQuoteMaskedId, $secondQuoteItemId, 2); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_virtual_product_saved.php + */ + public function testUpdateItemInGuestCart() + { + $guestQuote = $this->quoteFactory->create(); + $this->quoteResource->load( + $guestQuote, + 'test_order_with_virtual_product_without_address', + 'reserved_order_id' + ); + $guestQuoteMaskedId = $this->quoteIdToMaskedId->execute((int)$guestQuote->getId()); + $guestQuoteItemId = (int)$guestQuote + ->getItemByProduct($this->productRepository->get('virtual-product')) + ->getId(); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$guestQuoteMaskedId\"" + ); + + $query = $this->getQuery($guestQuoteMaskedId, $guestQuoteItemId, 2); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_virtual_product_saved.php + */ + public function testUpdateItemInAnotherCustomerCart() + { + $anotherCustomerQuote = $this->quoteFactory->create(); + $this->quoteResource->load( + $anotherCustomerQuote, + 'test_order_with_virtual_product_without_address', + 'reserved_order_id' + ); + $anotherCustomerQuote->setCustomerId(2); + $this->quoteResource->save($anotherCustomerQuote); + + $anotherCustomerQuoteMaskedId = $this->quoteIdToMaskedId->execute((int)$anotherCustomerQuote->getId()); + $anotherCustomerQuoteItemId = (int)$anotherCustomerQuote + ->getItemByProduct($this->productRepository->get('virtual-product')) + ->getId(); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$anotherCustomerQuoteMaskedId\"" + ); + + $query = $this->getQuery($anotherCustomerQuoteMaskedId, $anotherCustomerQuoteItemId, 2); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @expectedException \Exception + * @expectedExceptionMessage Required parameter "cart_id" is missing. + */ + public function testUpdateWithMissedCartItemId() + { + $query = <<<QUERY +mutation { + updateCartItems(input: { + cart_items: [ + { + cart_item_id: 1 + quantity: 2 + } + ] + }) { + cart { + items { + id + quantity + } + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @param string $input + * @param string $message + * @dataProvider dataProviderUpdateWithMissedRequiredParameters + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php + */ + public function testUpdateWithMissedItemRequiredParameters(string $input, string $message) + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, 'test_order_1', 'reserved_order_id'); + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$quote->getId()); + + $query = <<<QUERY +mutation { + updateCartItems(input: { + cart_id: "{$maskedQuoteId}" + {$input} + }) { + cart { + items { + id + quantity + } + } + } +} +QUERY; + $this->expectExceptionMessage($message); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * @return array + */ + public function dataProviderUpdateWithMissedRequiredParameters(): array + { + return [ + 'missed_cart_items' => [ + '', + 'Required parameter "cart_items" is missing.' + ], + 'missed_cart_item_id' => [ + 'cart_items: [{ quantity: 2 }]', + 'Required parameter "cart_item_id" for "cart_items" is missing.' + ], + 'missed_cart_item_qty' => [ + 'cart_items: [{ cart_item_id: 1 }]', + 'Field CartItemUpdateInput.quantity of required type Float! was not provided.' + ], + ]; + } + + /** + * @param string $maskedQuoteId + * @param int $itemId + * @param float $quantity + * @return string + */ + private function getQuery(string $maskedQuoteId, int $itemId, float $quantity): string + { + return <<<QUERY +mutation { + updateCartItems(input: { + cart_id: "{$maskedQuoteId}" + cart_items:[ + { + cart_item_id: {$itemId} + quantity: {$quantity} + } + ] + }) { + cart { + items { + id + quantity + } + } + } +} +QUERY; + } + + /** + * @param string $username + * @param string $password + * @return array + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + return $headerMap; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetMaskedQuoteIdByReservedOrderId.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetMaskedQuoteIdByReservedOrderId.php new file mode 100644 index 000000000000..9bb9bef9bdb0 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetMaskedQuoteIdByReservedOrderId.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote; + +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; +use Magento\Quote\Model\QuoteFactory; + +/** + * Get masked quote id by reserved order id + */ +class GetMaskedQuoteIdByReservedOrderId +{ + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedId; + + /** + * @param QuoteFactory $quoteFactory + * @param QuoteResource $quoteResource + * @param QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedId + */ + public function __construct( + QuoteFactory $quoteFactory, + QuoteResource $quoteResource, + QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedId + ) { + $this->quoteFactory = $quoteFactory; + $this->quoteResource = $quoteResource; + $this->quoteIdToMaskedId = $quoteIdToMaskedId; + } + + /** + * Get masked quote id by reserved order id + * + * @param string $reservedOrderId + * @return string + * @throws NoSuchEntityException + */ + public function execute(string $reservedOrderId): string + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reservedOrderId, 'reserved_order_id'); + + return $this->quoteIdToMaskedId->execute((int)$quote->getId()); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetQuoteItemIdByReservedQuoteIdAndSku.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetQuoteItemIdByReservedQuoteIdAndSku.php new file mode 100644 index 000000000000..6f027babc0e2 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetQuoteItemIdByReservedQuoteIdAndSku.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\Quote\Model\QuoteFactory; + +/** + * Get quote item id by reserved order id and product sku + */ +class GetQuoteItemIdByReservedQuoteIdAndSku +{ + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @param QuoteFactory $quoteFactory + * @param QuoteResource $quoteResource + * @param ProductRepositoryInterface $productRepository + */ + public function __construct( + QuoteFactory $quoteFactory, + QuoteResource $quoteResource, + ProductRepositoryInterface $productRepository + ) { + $this->quoteFactory = $quoteFactory; + $this->quoteResource = $quoteResource; + $this->productRepository = $productRepository; + } + + /** + * Get quote item id by reserved order id and product sku + * + * @param string $reservedOrderId + * @param string $sku + * @return int + * @throws NoSuchEntityException + */ + public function execute(string $reservedOrderId, string $sku): int + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reservedOrderId, 'reserved_order_id'); + $product = $this->productRepository->get($sku); + + return (int)$quote->getItemByProduct($product)->getId(); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetQuoteShippingAddressIdByReservedQuoteId.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetQuoteShippingAddressIdByReservedQuoteId.php new file mode 100644 index 000000000000..a56949b6f563 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetQuoteShippingAddressIdByReservedQuoteId.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote; + +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\Quote\Model\QuoteFactory; + +/** + * Get quote shipping address id by reserved order id + */ +class GetQuoteShippingAddressIdByReservedQuoteId +{ + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @param QuoteFactory $quoteFactory + * @param QuoteResource $quoteResource + */ + public function __construct( + QuoteFactory $quoteFactory, + QuoteResource $quoteResource + ) { + $this->quoteFactory = $quoteFactory; + $this->quoteResource = $quoteResource; + } + + /** + * Get quote shipping address id by reserved order id + * + * @param string $reservedOrderId + * @return int + */ + public function execute(string $reservedOrderId): int + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reservedOrderId, 'reserved_order_id'); + + return (int)$quote->getShippingAddress()->getId(); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/AddSimpleProductToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/AddSimpleProductToCartTest.php new file mode 100644 index 000000000000..8b20320fc2f0 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/AddSimpleProductToCartTest.php @@ -0,0 +1,137 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Add simple product to cart testcases + */ +class AddSimpleProductToCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + */ + public function testAddSimpleProductToCart() + { + $sku = 'simple_product'; + $quantity = 2; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $sku, $quantity); + $response = $this->graphQlMutation($query); + self::assertArrayHasKey('cart', $response['addSimpleProductsToCart']); + + self::assertEquals($quantity, $response['addSimpleProductsToCart']['cart']['items'][0]['quantity']); + self::assertEquals($sku, $response['addSimpleProductsToCart']['cart']['items'][0]['product']['sku']); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testAddProductToNonExistentCart() + { + $sku = 'simple_product'; + $quantity = 1; + $maskedQuoteId = 'non_existent_masked_id'; + + $query = $this->getQuery($maskedQuoteId, $sku, $quantity); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * + * @expectedException \Exception + * @expectedExceptionMessage Could not find a product with SKU "simple_product" + */ + public function testNonExistentProductToCart() + { + $sku = 'simple_product'; + $quantity = 1; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $sku, $quantity); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + */ + public function testAddSimpleProductToCustomerCart() + { + $sku = 'simple_product'; + $quantity = 2; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $sku, $quantity); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + + $this->graphQlMutation($query); + } + + /** + * @param string $maskedQuoteId + * @param string $sku + * @param float $quantity + * @return string + */ + private function getQuery(string $maskedQuoteId, string $sku, float $quantity): string + { + return <<<QUERY +mutation { + addSimpleProductsToCart( + input: { + cart_id: "{$maskedQuoteId}" + cartItems: [ + { + data: { + quantity: $quantity + sku: "$sku" + } + } + ] + } + ) { + cart { + items { + quantity + product { + sku + } + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/AddVirtualProductToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/AddVirtualProductToCartTest.php new file mode 100644 index 000000000000..cb429e7b9cb9 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/AddVirtualProductToCartTest.php @@ -0,0 +1,138 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Add virtual product to cart testcases + */ +class AddVirtualProductToCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/virtual_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + */ + public function testAddVirtualProductToCart() + { + $sku = 'virtual_product'; + $quantity = 2; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $sku, $quantity); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('cart', $response['addVirtualProductsToCart']); + self::assertEquals($quantity, $response['addVirtualProductsToCart']['cart']['items'][0]['quantity']); + self::assertEquals($sku, $response['addVirtualProductsToCart']['cart']['items'][0]['product']['sku']); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/virtual_product.php + * + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testAddVirtualToNonExistentCart() + { + $sku = 'virtual_product'; + $quantity = 1; + $maskedQuoteId = 'non_existent_masked_id'; + + $query = $this->getQuery($maskedQuoteId, $sku, $quantity); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * + * @expectedException \Exception + * @expectedExceptionMessage Could not find a product with SKU "virtual_product" + */ + public function testNonExistentProductToCart() + { + $sku = 'virtual_product'; + $quantity = 1; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $sku, $quantity); + $this->graphQlMutation($query); + } + + /** + * _security + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/virtual_product.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + */ + public function testAddVirtualProductToCustomerCart() + { + $sku = 'virtual_product'; + $quantity = 2; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $sku, $quantity); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + + $this->graphQlMutation($query); + } + + /** + * @param string $maskedQuoteId + * @param string $sku + * @param float $quantity + * @return string + */ + private function getQuery(string $maskedQuoteId, string $sku, float $quantity): string + { + return <<<QUERY +mutation { + addVirtualProductsToCart( + input: { + cart_id: "{$maskedQuoteId}" + cartItems: [ + { + data: { + quantity: {$quantity} + sku: "{$sku}" + } + } + ] + } + ) { + cart { + items { + quantity + product { + sku + } + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/ApplyCouponToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/ApplyCouponToCartTest.php new file mode 100644 index 000000000000..affe36ea8617 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/ApplyCouponToCartTest.php @@ -0,0 +1,232 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test Apply Coupon to Cart functionality for guest + */ +class ApplyCouponToCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + */ + public function testApplyCouponToCart() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('applyCouponToCart', $response); + self::assertEquals($couponCode, $response['applyCouponToCart']['cart']['applied_coupon']['code']); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + * @expectedException \Exception + * @expectedExceptionMessage A coupon is already applied to the cart. Please remove it to apply another + */ + public function testApplyCouponTwice() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey("applyCouponToCart", $response); + self::assertEquals($couponCode, $response['applyCouponToCart']['cart']['applied_coupon']['code']); + + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + * @expectedException \Exception + * @expectedExceptionMessage Cart does not contain products. + */ + public function testApplyCouponToCartWithoutItems() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + + $this->graphQlMutation($query); + } + + /** + * _security + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @expectedException \Exception + */ + public function testApplyCouponToCustomerCart() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + + self::expectExceptionMessage('The current user cannot perform operations on cart "' . $maskedQuoteId . '"'); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @expectedException \Exception + * @expectedExceptionMessage The coupon code isn't valid. Verify the code and try again. + */ + public function testApplyNonExistentCouponToCart() + { + $couponCode = 'non_existent_coupon_code'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + * @expectedException \Exception + */ + public function testApplyCouponToNonExistentCart() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = 'non_existent_masked_id'; + $query = $this->getQuery($maskedQuoteId, $couponCode); + + self::expectExceptionMessage('Could not find a cart with ID "' . $maskedQuoteId . '"'); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/make_coupon_expired.php + * @expectedException \Exception + * @expectedExceptionMessage The coupon code isn't valid. Verify the code and try again. + */ + public function testApplyExpiredCoupon() + { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/574'); + $couponCode = '2?ds5!2d'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + + $this->graphQlMutation($query); + } + + /** + * Products in cart don't fit to the coupon + * + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/restrict_coupon_usage_for_simple_product.php + * @expectedException \Exception + * @expectedExceptionMessage The coupon code isn't valid. Verify the code and try again. + */ + public function testApplyCouponWhichIsNotApplicable() + { + $couponCode = '2?ds5!2d'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId, $couponCode); + + $this->graphQlMutation($query); + } + + /** + * @param string $input + * @param string $message + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @dataProvider dataProviderUpdateWithMissedRequiredParameters + * @expectedException \Exception + */ + public function testApplyCouponWithMissedRequiredParameters(string $input, string $message) + { + $query = <<<QUERY +mutation { + applyCouponToCart(input: {{$input}}) { + cart { + applied_coupon { + code + } + } + } +} +QUERY; + + $this->expectExceptionMessage($message); + $this->graphQlMutation($query); + } + + /** + * @return array + */ + public function dataProviderUpdateWithMissedRequiredParameters(): array + { + return [ + 'missed_cart_id' => [ + 'coupon_code: "test"', + 'Required parameter "cart_id" is missing' + ], + 'missed_coupon_code' => [ + 'cart_id: "test"', + 'Required parameter "coupon_code" is missing' + ], + ]; + } + + /** + * @param string $maskedQuoteId + * @param string $couponCode + * @return string + */ + private function getQuery(string $maskedQuoteId, string $couponCode): string + { + return <<<QUERY +mutation { + applyCouponToCart(input: {cart_id: "$maskedQuoteId", coupon_code: "$couponCode"}) { + cart { + applied_coupon { + code + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CartTotalsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CartTotalsTest.php new file mode 100644 index 000000000000..ee2d6a2b31de --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CartTotalsTest.php @@ -0,0 +1,168 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test getting cart totals for guest + */ +class CartTotalsTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/apply_tax_for_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + */ + public function testGetCartTotalsWithTaxApplied() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query); + + self::assertArrayHasKey('prices', $response['cart']); + $pricesResponse = $response['cart']['prices']; + self::assertEquals(21.5, $pricesResponse['grand_total']['value']); + self::assertEquals(21.5, $pricesResponse['subtotal_including_tax']['value']); + self::assertEquals(20, $pricesResponse['subtotal_excluding_tax']['value']); + self::assertEquals(20, $pricesResponse['subtotal_with_discount_excluding_tax']['value']); + + $appliedTaxesResponse = $pricesResponse['applied_taxes']; + + self::assertEquals('US-TEST-*-Rate-1', $appliedTaxesResponse[0]['label']); + self::assertEquals(1.5, $appliedTaxesResponse[0]['amount']['value']); + self::assertEquals('USD', $appliedTaxesResponse[0]['amount']['currency']); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + */ + public function testGetTotalsWithNoTaxApplied() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query); + + $pricesResponse = $response['cart']['prices']; + self::assertEquals(20, $pricesResponse['grand_total']['value']); + self::assertEquals(20, $pricesResponse['subtotal_including_tax']['value']); + self::assertEquals(20, $pricesResponse['subtotal_excluding_tax']['value']); + self::assertEquals(20, $pricesResponse['subtotal_with_discount_excluding_tax']['value']); + self::assertEmpty($pricesResponse['applied_taxes']); + } + + /** + * The totals calculation is based on quote address. + * But the totals should be calculated even if no address is set + * + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @group recent + */ + public function testGetCartTotalsWithNoAddressSet() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query); + + $pricesResponse = $response['cart']['prices']; + self::assertEquals(20, $pricesResponse['grand_total']['value']); + self::assertEquals(20, $pricesResponse['subtotal_including_tax']['value']); + self::assertEquals(20, $pricesResponse['subtotal_excluding_tax']['value']); + self::assertEquals(20, $pricesResponse['subtotal_with_discount_excluding_tax']['value']); + self::assertEmpty($pricesResponse['applied_taxes']); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/apply_tax_for_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + */ + public function testGetSelectedShippingMethodFromCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlQuery($query); + } + + /** + * Generates GraphQl query for retrieving cart totals + * + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +{ + cart(cart_id: "$maskedQuoteId") { + prices { + grand_total { + value, + currency + } + subtotal_including_tax { + value + currency + } + subtotal_excluding_tax { + value + currency + } + subtotal_with_discount_excluding_tax { + value + currency + } + applied_taxes { + label + amount { + value + currency + } + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CheckoutEndToEndTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CheckoutEndToEndTest.php new file mode 100644 index 000000000000..bddee2151f5c --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CheckoutEndToEndTest.php @@ -0,0 +1,435 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\Framework\Registry; +use Magento\Quote\Model\ResourceModel\Quote\CollectionFactory as QuoteCollectionFactory; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\ResourceModel\Order\CollectionFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * End to checkout tests for guest + */ +class CheckoutEndToEndTest extends GraphQlAbstract +{ + /** + * @var Registry + */ + private $registry; + + /** + * @var QuoteCollectionFactory + */ + private $quoteCollectionFactory; + + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteIdMaskFactory + */ + private $quoteIdMaskFactory; + + /** + * @var CollectionFactory + */ + private $orderCollectionFactory; + + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + protected function setUp() + { + parent::setUp(); + + $objectManager = Bootstrap::getObjectManager(); + $this->registry = $objectManager->get(Registry::class); + $this->quoteCollectionFactory = $objectManager->get(QuoteCollectionFactory::class); + $this->quoteResource = $objectManager->get(QuoteResource::class); + $this->quoteIdMaskFactory = $objectManager->get(QuoteIdMaskFactory::class); + $this->orderCollectionFactory = $objectManager->get(CollectionFactory::class); + $this->orderRepository = $objectManager->get(OrderRepositoryInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_attribute.php + */ + public function testCheckoutWorkflow() + { + $quantity = 2; + + $sku = $this->findProduct(); + $cartId = $this->createEmptyCart(); + $this->setGuestEmailOnCart($cartId); + $this->addProductToCart($cartId, $quantity, $sku); + + $this->setBillingAddress($cartId); + $shippingMethod = $this->setShippingAddress($cartId); + + $paymentMethod = $this->setShippingMethod($cartId, $shippingMethod); + $this->setPaymentMethod($cartId, $paymentMethod); + + $this->placeOrder($cartId); + } + + /** + * @return string + */ + private function findProduct(): string + { + $query = <<<QUERY +{ + products ( + filter: { + sku: { + like:"simple%" + } + } + pageSize: 1 + currentPage: 1 + ) { + items { + sku + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + self::assertArrayHasKey('products', $response); + self::assertArrayHasKey('items', $response['products']); + self::assertCount(1, $response['products']['items']); + + $product = current($response['products']['items']); + self::assertArrayHasKey('sku', $product); + self::assertNotEmpty($product['sku']); + + return $product['sku']; + } + + /** + * @return string + */ + private function createEmptyCart(): string + { + $query = <<<QUERY +mutation { + createEmptyCart +} +QUERY; + $response = $this->graphQlMutation($query); + self::assertArrayHasKey('createEmptyCart', $response); + self::assertNotEmpty($response['createEmptyCart']); + + return $response['createEmptyCart']; + } + + /** + * @param string $cartId + * @return void + */ + private function setGuestEmailOnCart(string $cartId): void + { + $query = <<<QUERY +mutation { + setGuestEmailOnCart( + input: { + cart_id: "{$cartId}" + email: "customer@example.com" + } + ) { + cart { + email + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * @param string $cartId + * @param float $quantity + * @param string $sku + * @return void + */ + private function addProductToCart(string $cartId, float $quantity, string $sku): void + { + $query = <<<QUERY +mutation { + addSimpleProductsToCart( + input: { + cart_id: "{$cartId}" + cartItems: [ + { + data: { + quantity: {$quantity} + sku: "{$sku}" + } + } + ] + } + ) { + cart { + items { + quantity + product { + sku + } + } + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * @param string $cartId + * @param array $auth + * @return array + */ + private function setBillingAddress(string $cartId): void + { + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "{$cartId}" + billing_address: { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + postcode: "887766" + telephone: "88776655" + region: "TX" + country_code: "US" + save_in_address_book: false + } + } + } + ) { + cart { + billing_address { + __typename + } + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * @param string $cartId + * @return array + */ + private function setShippingAddress(string $cartId): array + { + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$cartId" + shipping_addresses: [ + { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "TX" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + ] + } + ) { + cart { + shipping_addresses { + available_shipping_methods { + carrier_code + method_code + amount + } + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query); + self::assertArrayHasKey('setShippingAddressesOnCart', $response); + self::assertArrayHasKey('cart', $response['setShippingAddressesOnCart']); + self::assertArrayHasKey('shipping_addresses', $response['setShippingAddressesOnCart']['cart']); + self::assertCount(1, $response['setShippingAddressesOnCart']['cart']['shipping_addresses']); + + $shippingAddress = current($response['setShippingAddressesOnCart']['cart']['shipping_addresses']); + self::assertArrayHasKey('available_shipping_methods', $shippingAddress); + self::assertCount(1, $shippingAddress['available_shipping_methods']); + + $availableShippingMethod = current($shippingAddress['available_shipping_methods']); + self::assertArrayHasKey('carrier_code', $availableShippingMethod); + self::assertNotEmpty($availableShippingMethod['carrier_code']); + + self::assertArrayHasKey('method_code', $availableShippingMethod); + self::assertNotEmpty($availableShippingMethod['method_code']); + + self::assertArrayHasKey('amount', $availableShippingMethod); + self::assertNotEmpty($availableShippingMethod['amount']); + + return $availableShippingMethod; + } + + /** + * @param string $cartId + * @param array $method + * @return array + */ + private function setShippingMethod(string $cartId, array $method): array + { + $query = <<<QUERY +mutation { + setShippingMethodsOnCart(input: { + cart_id: "{$cartId}", + shipping_methods: [ + { + carrier_code: "{$method['carrier_code']}" + method_code: "{$method['method_code']}" + } + ] + }) { + cart { + available_payment_methods { + code + title + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query); + self::assertArrayHasKey('setShippingMethodsOnCart', $response); + self::assertArrayHasKey('cart', $response['setShippingMethodsOnCart']); + self::assertArrayHasKey('available_payment_methods', $response['setShippingMethodsOnCart']['cart']); + self::assertCount(1, $response['setShippingMethodsOnCart']['cart']['available_payment_methods']); + + $availablePaymentMethod = current($response['setShippingMethodsOnCart']['cart']['available_payment_methods']); + self::assertArrayHasKey('code', $availablePaymentMethod); + self::assertNotEmpty($availablePaymentMethod['code']); + self::assertArrayHasKey('title', $availablePaymentMethod); + self::assertNotEmpty($availablePaymentMethod['title']); + + return $availablePaymentMethod; + } + + /** + * @param string $cartId + * @param array $method + * @return void + */ + private function setPaymentMethod(string $cartId, array $method): void + { + $query = <<<QUERY +mutation { + setPaymentMethodOnCart( + input: { + cart_id: "{$cartId}" + payment_method: { + code: "{$method['code']}" + } + } + ) { + cart { + selected_payment_method { + code + } + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * @param string $cartId + * @return void + */ + private function placeOrder(string $cartId): void + { + $query = <<<QUERY +mutation { + placeOrder( + input: { + cart_id: "{$cartId}" + } + ) { + order { + order_id + } + } +} +QUERY; + $response = $this->graphQlMutation($query); + self::assertArrayHasKey('placeOrder', $response); + self::assertArrayHasKey('order', $response['placeOrder']); + self::assertArrayHasKey('order_id', $response['placeOrder']['order']); + self::assertNotEmpty($response['placeOrder']['order']['order_id']); + } + + public function tearDown() + { + $this->deleteQuote(); + $this->deleteOrder(); + parent::tearDown(); + } + + /** + * @return void + */ + private function deleteQuote(): void + { + $quoteCollection = $this->quoteCollectionFactory->create(); + foreach ($quoteCollection as $quote) { + $this->quoteResource->delete($quote); + + $quoteIdMask = $this->quoteIdMaskFactory->create(); + $quoteIdMask->setQuoteId($quote->getId()) + ->delete(); + } + } + + /** + * @return void + */ + private function deleteOrder() + { + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', true); + + $orderCollection = $this->orderCollectionFactory->create(); + foreach ($orderCollection as $order) { + $this->orderRepository->delete($order); + } + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', false); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CreateEmptyCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CreateEmptyCartTest.php new file mode 100644 index 000000000000..6ed91d21f0ae --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CreateEmptyCartTest.php @@ -0,0 +1,169 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\Quote\Model\ResourceModel\Quote\CollectionFactory as QuoteCollectionFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Quote\Api\GuestCartRepositoryInterface; + +/** + * Test for empty cart creation mutation + */ +class CreateEmptyCartTest extends GraphQlAbstract +{ + /** + * @var GuestCartRepositoryInterface + */ + private $guestCartRepository; + + /** + * @var QuoteCollectionFactory + */ + private $quoteCollectionFactory; + + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteIdMaskFactory + */ + private $quoteIdMaskFactory; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->guestCartRepository = $objectManager->get(GuestCartRepositoryInterface::class); + $this->quoteCollectionFactory = $objectManager->get(QuoteCollectionFactory::class); + $this->quoteResource = $objectManager->get(QuoteResource::class); + $this->quoteIdMaskFactory = $objectManager->get(QuoteIdMaskFactory::class); + } + + public function testCreateEmptyCart() + { + $query = $this->getQuery(); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('createEmptyCart', $response); + self::assertNotEmpty($response['createEmptyCart']); + + $guestCart = $this->guestCartRepository->get($response['createEmptyCart']); + + self::assertNotNull($guestCart->getId()); + self::assertNull($guestCart->getCustomer()->getId()); + self::assertEquals('default', $guestCart->getStore()->getCode()); + } + + /** + * @magentoApiDataFixture Magento/Store/_files/second_store.php + */ + public function testCreateEmptyCartWithNotDefaultStore() + { + $query = $this->getQuery(); + $headerMap = ['Store' => 'fixture_second_store']; + $response = $this->graphQlMutation($query, [], '', $headerMap); + + self::assertArrayHasKey('createEmptyCart', $response); + self::assertNotEmpty($response['createEmptyCart']); + + $guestCart = $this->guestCartRepository->get($response['createEmptyCart']); + $this->maskedQuoteId = $response['createEmptyCart']; + + self::assertNotNull($guestCart->getId()); + self::assertNull($guestCart->getCustomer()->getId()); + self::assertSame('fixture_second_store', $guestCart->getStore()->getCode()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testCreateEmptyCartWithPredefinedCartId() + { + $predefinedCartId = '572cda51902b5b517c0e1a2b2fd004b4'; + + $query = <<<QUERY +mutation { + createEmptyCart (input: {cart_id: "{$predefinedCartId}"}) +} +QUERY; + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('createEmptyCart', $response); + self::assertEquals($predefinedCartId, $response['createEmptyCart']); + + $guestCart = $this->guestCartRepository->get($response['createEmptyCart']); + self::assertNotNull($guestCart->getId()); + self::assertNull($guestCart->getCustomer()->getId()); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * + * @expectedException \Exception + * @expectedExceptionMessage Cart with ID "572cda51902b5b517c0e1a2b2fd004b4" already exists. + */ + public function testCreateEmptyCartIfPredefinedCartIdAlreadyExists() + { + $predefinedCartId = '572cda51902b5b517c0e1a2b2fd004b4'; + + $query = <<<QUERY +mutation { + createEmptyCart (input: {cart_id: "{$predefinedCartId}"}) +} +QUERY; + $this->graphQlMutation($query); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * + * @expectedException \Exception + * @expectedExceptionMessage Cart ID length should to be 32 symbols. + */ + public function testCreateEmptyCartWithWrongPredefinedCartId() + { + $predefinedCartId = '572'; + + $query = <<<QUERY +mutation { + createEmptyCart (input: {cart_id: "{$predefinedCartId}"}) +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * @return string + */ + private function getQuery(): string + { + return <<<QUERY +mutation { + createEmptyCart +} +QUERY; + } + + public function tearDown() + { + $quoteCollection = $this->quoteCollectionFactory->create(); + foreach ($quoteCollection as $quote) { + $this->quoteResource->delete($quote); + + $quoteIdMask = $this->quoteIdMaskFactory->create(); + $quoteIdMask->setQuoteId($quote->getId()) + ->delete(); + } + parent::tearDown(); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetAvailablePaymentMethodsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetAvailablePaymentMethodsTest.php new file mode 100644 index 000000000000..af1f72fe7162 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetAvailablePaymentMethodsTest.php @@ -0,0 +1,117 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for getting cart information + */ +class GetAvailablePaymentMethodsTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testGetAvailablePaymentMethods() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query); + + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('available_payment_methods', $response['cart']); + + self::assertEquals('checkmo', $response['cart']['available_payment_methods'][0]['code']); + self::assertEquals('Check / Money order', $response['cart']['available_payment_methods'][0]['title']); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testGetAvailablePaymentMethodsFromCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlQuery($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/disable_all_active_payment_methods.php + */ + public function testGetAvailablePaymentMethodsIfPaymentsAreNotPresent() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query); + + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('available_payment_methods', $response['cart']); + self::assertEmpty($response['cart']['available_payment_methods']); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testGetAvailablePaymentMethodsOfNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $query = $this->getQuery($maskedQuoteId); + $this->graphQlQuery($query); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +{ + cart(cart_id: "$maskedQuoteId") { + available_payment_methods { + code + title + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetAvailableShippingMethodsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetAvailableShippingMethodsTest.php new file mode 100644 index 000000000000..a8113657eff6 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetAvailableShippingMethodsTest.php @@ -0,0 +1,144 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for get available shipping methods + */ +class GetAvailableShippingMethodsTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * Test case: get available shipping methods from current customer quote + * + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testGetAvailableShippingMethods() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $response = $this->graphQlQuery($this->getQuery($maskedQuoteId)); + + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('shipping_addresses', $response['cart']); + self::assertCount(1, $response['cart']['shipping_addresses']); + self::assertArrayHasKey('available_shipping_methods', $response['cart']['shipping_addresses'][0]); + self::assertCount(1, $response['cart']['shipping_addresses'][0]['available_shipping_methods']); + + $expectedAddressData = [ + 'amount' => 10, + 'base_amount' => 10, + 'carrier_code' => 'flatrate', + 'carrier_title' => 'Flat Rate', + 'error_message' => '', + 'method_code' => 'flatrate', + 'method_title' => 'Fixed', + 'price_incl_tax' => 10, + 'price_excl_tax' => 10, + ]; + self::assertEquals( + $expectedAddressData, + $response['cart']['shipping_addresses'][0]['available_shipping_methods'][0] + ); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testGetAvailableShippingMethodsFromCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlQuery($this->getQuery($maskedQuoteId)); + } + + /** + * Test case: get available shipping methods when all shipping methods are disabled + * + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/disable_offline_shipping_methods.php + */ + public function testGetAvailableShippingMethodsIfShippingMethodsAreNotPresent() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $response = $this->graphQlQuery($this->getQuery($maskedQuoteId)); + + self::assertEmpty($response['cart']['shipping_addresses'][0]['available_shipping_methods']); + } + + /** + * Test case: get available shipping methods from non-existent cart + * + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testGetAvailableShippingMethodsOfNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $query = $this->getQuery($maskedQuoteId); + + $this->graphQlQuery($query); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +query { + cart (cart_id: "{$maskedQuoteId}") { + shipping_addresses { + available_shipping_methods { + amount + base_amount + carrier_code + carrier_title + error_message + method_code + method_title + price_excl_tax + price_incl_tax + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetCartEmailTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetCartEmailTest.php new file mode 100644 index 000000000000..8c6ecd075049 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetCartEmailTest.php @@ -0,0 +1,88 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for getting email from cart + */ +class GetCartEmailTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/set_guest_email.php + */ + public function testGetCartEmail() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query); + + $this->assertArrayHasKey('cart', $response); + $this->assertArrayHasKey('email', $response['cart']); + $this->assertEquals('guest@example.com', $response['cart']['email']); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testGetCartEmailFromNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $query = $this->getQuery($maskedQuoteId); + + $this->graphQlQuery($query); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + */ + public function testGetCartEmailFromCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"{$maskedQuoteId}\"" + ); + $this->graphQlQuery($query); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +{ + cart(cart_id:"$maskedQuoteId") { + email + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetCartTest.php new file mode 100644 index 000000000000..b35d689af7d2 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetCartTest.php @@ -0,0 +1,168 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for getting cart information + */ +class GetCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_virtual_product.php + */ + public function testGetCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $response = $this->graphQlQuery($query); + + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('items', $response['cart']); + self::assertCount(2, $response['cart']['items']); + + self::assertNotEmpty($response['cart']['items'][0]['id']); + self::assertEquals(2, $response['cart']['items'][0]['quantity']); + self::assertEquals('simple_product', $response['cart']['items'][0]['product']['sku']); + + self::assertNotEmpty($response['cart']['items'][1]['id']); + self::assertEquals(2, $response['cart']['items'][1]['quantity']); + self::assertEquals('virtual-product', $response['cart']['items'][1]['product']['sku']); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + */ + public function testGetCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"{$maskedQuoteId}\"" + ); + $this->graphQlQuery($query); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testGetNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $query = $this->getQuery($maskedQuoteId); + + $this->graphQlQuery($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/make_cart_inactive.php + * + * @expectedException \Exception + * @expectedExceptionMessage Current user does not have an active cart. + */ + public function testGetInactiveCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->graphQlQuery($query); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/active_quote_guest_not_default_store.php + */ + public function testGetCartWithNotDefaultStore() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1_not_default_store_guest'); + $query = $this->getQuery($maskedQuoteId); + + $headerMap = ['Store' => 'fixture_second_store']; + $response = $this->graphQlQuery($query, [], '', $headerMap); + + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('items', $response['cart']); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + * @magentoApiDataFixture Magento/Store/_files/second_store.php + * + * @expectedException \Exception + * @expectedExceptionMessage Wrong store code specified for cart + */ + public function testGetCartWithWrongStore() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + $query = $this->getQuery($maskedQuoteId); + + $headerMap = ['Store' => 'fixture_second_store']; + $this->graphQlQuery($query, [], '', $headerMap); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote_guest_not_default_store.php + * + * @expectedException \Exception + * @expectedExceptionMessage Requested store is not found + */ + public function testGetCartWithNotExistingStore() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1_not_default_store_guest'); + + $headerMap['Store'] = 'not_existing_store'; + $query = $this->getQuery($maskedQuoteId); + + $this->graphQlQuery($query, [], '', $headerMap); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +{ + cart(cart_id: "{$maskedQuoteId}") { + items { + id + quantity + product { + sku + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetSelectedShippingMethodTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetSelectedShippingMethodTest.php new file mode 100644 index 000000000000..bd684a950b59 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetSelectedShippingMethodTest.php @@ -0,0 +1,171 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for get selected shipping method + */ +class GetSelectedShippingMethodTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testGetSelectedShippingMethod() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlQuery($query); + + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('shipping_addresses', $response['cart']); + self::assertCount(1, $response['cart']['shipping_addresses']); + + $shippingAddress = current($response['cart']['shipping_addresses']); + self::assertArrayHasKey('selected_shipping_method', $shippingAddress); + + self::assertArrayHasKey('carrier_code', $shippingAddress['selected_shipping_method']); + self::assertEquals('flatrate', $shippingAddress['selected_shipping_method']['carrier_code']); + + self::assertArrayHasKey('method_code', $shippingAddress['selected_shipping_method']); + self::assertEquals('flatrate', $shippingAddress['selected_shipping_method']['method_code']); + + self::assertArrayHasKey('carrier_title', $shippingAddress['selected_shipping_method']); + self::assertEquals('Flat Rate', $shippingAddress['selected_shipping_method']['carrier_title']); + + self::assertArrayHasKey('method_title', $shippingAddress['selected_shipping_method']); + self::assertEquals('Fixed', $shippingAddress['selected_shipping_method']['method_title']); + + self::assertArrayHasKey('amount', $shippingAddress['selected_shipping_method']); + $amount = $shippingAddress['selected_shipping_method']['amount']; + + self::assertArrayHasKey('value', $amount); + self::assertEquals(10, $amount['value']); + self::assertArrayHasKey('currency', $amount); + self::assertEquals('USD', $amount['currency']); + + self::assertArrayHasKey('base_amount', $shippingAddress['selected_shipping_method']); + $baseAmount = $shippingAddress['selected_shipping_method']['base_amount']; + + self::assertArrayHasKey('value', $baseAmount); + self::assertEquals(10, $baseAmount['value']); + self::assertArrayHasKey('currency', $baseAmount); + self::assertEquals('USD', $baseAmount['currency']); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testGetSelectedShippingMethodFromCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlQuery($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testGetGetSelectedShippingMethodIfShippingMethodIsNotSet() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $response = $this->graphQlQuery($query); + + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('shipping_addresses', $response['cart']); + self::assertCount(1, $response['cart']['shipping_addresses']); + + $shippingAddress = current($response['cart']['shipping_addresses']); + self::assertArrayHasKey('selected_shipping_method', $shippingAddress); + + self::assertNull($shippingAddress['selected_shipping_method']['carrier_code']); + self::assertNull($shippingAddress['selected_shipping_method']['method_code']); + self::assertNull($shippingAddress['selected_shipping_method']['carrier_title']); + self::assertNull($shippingAddress['selected_shipping_method']['method_title']); + self::assertNull($shippingAddress['selected_shipping_method']['amount']); + self::assertNull($shippingAddress['selected_shipping_method']['base_amount']); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testGetSelectedShippingMethodOfNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $query = $this->getQuery($maskedQuoteId); + $this->graphQlQuery($query); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +{ + cart(cart_id: "$maskedQuoteId") { + shipping_addresses { + selected_shipping_method { + carrier_code + method_code + carrier_title + method_title + amount { + value + currency + } + base_amount { + value + currency + } + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetSpecifiedBillingAddressTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetSpecifiedBillingAddressTest.php new file mode 100644 index 000000000000..48bff7322689 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetSpecifiedBillingAddressTest.php @@ -0,0 +1,170 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for get specified billing address + */ +class GetSpecifiedBillingAddressTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + */ + public function testGeSpecifiedBillingAddress() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $response = $this->graphQlQuery($query); + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('billing_address', $response['cart']); + + $expectedBillingAddressData = [ + 'firstname' => 'John', + 'lastname' => 'Smith', + 'company' => 'CompanyName', + 'street' => [ + 'Green str, 67' + ], + 'city' => 'CityM', + 'region' => [ + 'code' => 'AL', + 'label' => 'Alabama', + ], + 'postcode' => '75477', + 'country' => [ + 'code' => 'US', + 'label' => 'US', + ], + 'telephone' => '3468676', + '__typename' => 'BillingCartAddress', + ]; + self::assertEquals($expectedBillingAddressData, $response['cart']['billing_address']); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testGeSpecifiedBillingAddressIfBillingAddressIsNotSet() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $response = $this->graphQlQuery($query); + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('billing_address', $response['cart']); + + $expectedBillingAddressData = [ + 'firstname' => null, + 'lastname' => null, + 'company' => null, + 'street' => [ + '' + ], + 'city' => null, + 'region' => [ + 'code' => null, + 'label' => null, + ], + 'postcode' => null, + 'country' => [ + 'code' => null, + 'label' => null, + ], + 'telephone' => null, + '__typename' => 'BillingCartAddress', + ]; + self::assertEquals($expectedBillingAddressData, $response['cart']['billing_address']); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testGetBillingAddressOfNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $query = $this->getQuery($maskedQuoteId); + $this->graphQlQuery($query); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + */ + public function testGetBillingAddressFromAnotherCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlQuery($query); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +{ + cart(cart_id: "$maskedQuoteId") { + billing_address { + firstname + lastname + company + street + city + region + { + code + label + } + postcode + country + { + code + label + } + telephone + __typename + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/PlaceOrderTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/PlaceOrderTest.php new file mode 100644 index 000000000000..30ad69eada29 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/PlaceOrderTest.php @@ -0,0 +1,273 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\Framework\Registry; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\ResourceModel\Order\CollectionFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for placing an order for guest + */ +class PlaceOrderTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var CollectionFactory + */ + private $orderCollectionFactory; + + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @var Registry + */ + private $registry; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->orderCollectionFactory = $objectManager->get(CollectionFactory::class); + $this->orderRepository = $objectManager->get(OrderRepositoryInterface::class); + $this->registry = Bootstrap::getObjectManager()->get(Registry::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_payment_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/set_guest_email.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_checkmo_payment_method.php + */ + public function testPlaceOrder() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('placeOrder', $response); + self::assertArrayHasKey('order_id', $response['placeOrder']['order']); + self::assertEquals($reservedOrderId, $response['placeOrder']['order']['order_id']); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_payment_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_checkmo_payment_method.php + */ + public function testPlaceOrderWithNoEmail() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessage("Guest email for cart is missing. Please enter"); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/set_guest_email.php + */ + public function testPlaceOrderWithNoItemsInCart() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessage( + 'Unable to place order: A server error stopped your order from being placed. ' . + 'Please try to place your order again' + ); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/set_guest_email.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testPlaceOrderWithNoShippingAddress() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessage( + 'Unable to place order: Some addresses can\'t be used due to the configurations for specific countries' + ); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/set_guest_email.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testPlaceOrderWithNoShippingMethod() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessage( + 'Unable to place order: The shipping method is missing. Select the shipping method and try again' + ); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/set_guest_email.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testPlaceOrderWithNoBillingAddress() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessageRegExp( + '/Unable to place order: Please check the billing address information*/' + ); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/set_guest_email.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testPlaceOrderWithNoPaymentMethod() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessage('Unable to place order: Enter a valid payment method and try again'); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/set_guest_email.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/set_simple_product_out_of_stock.php + */ + public function testPlaceOrderWithOutOfStockProduct() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessage('Unable to place order: Some of the products are out of stock'); + $this->graphQlMutation($query); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/three_customers.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_payment_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_checkmo_payment_method.php + */ + public function testPlaceOrderOfCustomerCart() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessageRegExp('/The current user cannot perform operations on cart*/'); + $this->graphQlMutation($query); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +mutation { + placeOrder(input: {cart_id: "{$maskedQuoteId}"}) { + order { + order_id + } + } +} +QUERY; + } + + /** + * @inheritdoc + */ + public function tearDown() + { + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', true); + + $orderCollection = $this->orderCollectionFactory->create(); + foreach ($orderCollection as $order) { + $this->orderRepository->delete($order); + } + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', false); + + parent::tearDown(); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/RemoveCouponFromCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/RemoveCouponFromCartTest.php new file mode 100644 index 000000000000..5adb6ce65db6 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/RemoveCouponFromCartTest.php @@ -0,0 +1,128 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Check removing of the coupon from guest cart + */ +class RemoveCouponFromCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/SalesRule/_files/coupon_code_with_wildcard.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/apply_coupon.php + */ + public function testRemoveCouponFromCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('removeCouponFromCart', $response); + self::assertNull($response['removeCouponFromCart']['cart']['applied_coupon']['code']); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testRemoveCouponFromNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $query = $this->getQuery($maskedQuoteId); + + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @expectedException \Exception + * @expectedExceptionMessage Cart does not contain products + */ + public function testRemoveCouponFromEmptyCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testRemoveCouponFromCartIfCouponWasNotSet() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('removeCouponFromCart', $response); + self::assertNull($response['removeCouponFromCart']['cart']['applied_coupon']['code']); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/Checkout/_files/discount_10percent_generalusers.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/apply_coupon.php + */ + public function testRemoveCouponFromCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $query = $this->getQuery($maskedQuoteId); + + self::expectExceptionMessage('The current user cannot perform operations on cart "' . $maskedQuoteId . '"'); + $this->graphQlMutation($query); + } + + /** + * @param string $maskedQuoteId + * @return string + */ + private function getQuery(string $maskedQuoteId): string + { + return <<<QUERY +mutation { + removeCouponFromCart(input: {cart_id: "{$maskedQuoteId}"}) { + cart { + applied_coupon { + code + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/RemoveItemFromCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/RemoveItemFromCartTest.php new file mode 100644 index 000000000000..77a057e52f68 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/RemoveItemFromCartTest.php @@ -0,0 +1,189 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\GraphQl\Quote\GetQuoteItemIdByReservedQuoteIdAndSku; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for removeItemFromCartTest mutation + */ +class RemoveItemFromCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var GetQuoteItemIdByReservedQuoteIdAndSku + */ + private $getQuoteItemIdByReservedQuoteIdAndSku; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->getQuoteItemIdByReservedQuoteIdAndSku = $objectManager->get( + GetQuoteItemIdByReservedQuoteIdAndSku::class + ); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testRemoveItemFromCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $itemId = $this->getQuoteItemIdByReservedQuoteIdAndSku->execute('test_quote', 'simple_product'); + + $query = $this->getQuery($maskedQuoteId, $itemId); + $response = $this->graphQlMutation($query); + + $this->assertArrayHasKey('removeItemFromCart', $response); + $this->assertArrayHasKey('cart', $response['removeItemFromCart']); + $this->assertCount(0, $response['removeItemFromCart']['cart']['items']); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testRemoveItemFromNonExistentCart() + { + $query = $this->getQuery('non_existent_masked_id', 1); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testRemoveNonExistentItem() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $notExistentItemId = 999; + + $this->expectExceptionMessage("Cart doesn't contain the {$notExistentItemId} item."); + + $query = $this->getQuery($maskedQuoteId, $notExistentItemId); + $this->graphQlMutation($query); + } + + /** + * @param string $input + * @param string $message + * @dataProvider dataProviderUpdateWithMissedRequiredParameters + */ + public function testUpdateWithMissedItemRequiredParameters(string $input, string $message) + { + $query = <<<QUERY +mutation { + removeItemFromCart( + input: { + {$input} + } + ) { + cart { + items { + quantity + } + } + } +} +QUERY; + $this->expectExceptionMessage($message); + $this->graphQlMutation($query); + } + + /** + * @return array + */ + public function dataProviderUpdateWithMissedRequiredParameters(): array + { + return [ + 'missed_cart_id' => [ + 'cart_item_id: 1', + 'Required parameter "cart_id" is missing.' + ], + 'missed_cart_item_id' => [ + 'cart_id: "test"', + 'Required parameter "cart_item_id" is missing.' + ], + ]; + } + + /** + * _security + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_virtual_product_saved.php + */ + public function testRemoveItemIfItemIsNotBelongToCart() + { + $firstQuoteMaskedId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $secondQuoteItemId = $this->getQuoteItemIdByReservedQuoteIdAndSku->execute( + 'test_order_with_virtual_product_without_address', + 'virtual-product' + ); + + $this->expectExceptionMessage("Cart doesn't contain the {$secondQuoteItemId} item."); + + $query = $this->getQuery($firstQuoteMaskedId, $secondQuoteItemId); + $this->graphQlMutation($query); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testRemoveItemFromCustomerCart() + { + $customerQuoteMaskedId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $customerQuoteItemId = $this->getQuoteItemIdByReservedQuoteIdAndSku->execute('test_quote', 'simple_product'); + + $this->expectExceptionMessage("The current user cannot perform operations on cart \"$customerQuoteMaskedId\""); + + $query = $this->getQuery($customerQuoteMaskedId, $customerQuoteItemId); + $this->graphQlMutation($query); + } + + /** + * @param string $maskedQuoteId + * @param int $itemId + * @return string + */ + private function getQuery(string $maskedQuoteId, int $itemId): string + { + return <<<QUERY +mutation { + removeItemFromCart( + input: { + cart_id: "{$maskedQuoteId}" + cart_item_id: {$itemId} + } + ) { + cart { + items { + quantity + } + } + } +} +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 000000000000..d8d95040eea4 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetBillingAddressOnCartTest.php @@ -0,0 +1,399 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for set billing address on cart mutation + */ +class SetBillingAddressOnCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetNewBillingAddress() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + } + ) { + cart { + billing_address { + firstname + lastname + company + street + city + postcode + telephone + country { + code + label + } + __typename + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('cart', $response['setBillingAddressOnCart']); + $cartResponse = $response['setBillingAddressOnCart']['cart']; + self::assertArrayHasKey('billing_address', $cartResponse); + $billingAddressResponse = $cartResponse['billing_address']; + $this->assertNewAddressFields($billingAddressResponse); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetNewBillingAddressWithUseForShippingParameter() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + use_for_shipping: true + } + } + ) { + cart { + billing_address { + firstname + lastname + company + street + city + postcode + telephone + country { + code + label + } + __typename + } + shipping_addresses { + firstname + lastname + company + street + city + postcode + telephone + country { + code + label + } + __typename + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('cart', $response['setBillingAddressOnCart']); + $cartResponse = $response['setBillingAddressOnCart']['cart']; + self::assertArrayHasKey('billing_address', $cartResponse); + $billingAddressResponse = $cartResponse['billing_address']; + self::assertArrayHasKey('shipping_addresses', $cartResponse); + $shippingAddressResponse = current($cartResponse['shipping_addresses']); + $this->assertNewAddressFields($billingAddressResponse); + $this->assertNewAddressFields($shippingAddressResponse, 'ShippingCartAddress'); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + */ + public function testSetBillingAddressToCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + } + ) { + cart { + billing_address { + city + } + } + } +} +QUERY; + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlMutation($query); + } + + /** + * _security + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * + * @expectedException \Exception + * @expectedExceptionMessage The current customer isn't authorized. + */ + public function testSetBillingAddressFromAddressBook() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + customer_address_id: 1 + } + } + ) { + cart { + billing_address { + city + } + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testSetBillingAddressOnNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + } + ) { + cart { + billing_address { + city + } + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * + * @dataProvider dataProviderSetWithoutRequiredParameters + * @param string $input + * @param string $message + * @throws \Exception + */ + public function testSetBillingAddressWithoutRequiredParameters(string $input, string $message) + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $input = str_replace('cart_id_value', $maskedQuoteId, $input); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + {$input} + } + ) { + cart { + billing_address { + city + } + } + } +} +QUERY; + $this->expectExceptionMessage($message); + $this->graphQlMutation($query); + } + + /** + * @return array + */ + public function dataProviderSetWithoutRequiredParameters(): array + { + return [ + 'missed_billing_address' => [ + 'cart_id: "cart_id_value"', + 'Field SetBillingAddressOnCartInput.billing_address of required type BillingAddressInput!' + . ' was not provided.', + ], + 'missed_cart_id' => [ + 'billing_address: {}', + 'Required parameter "cart_id" is missing' + ] + ]; + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetNewBillingAddressRedundantStreetLine() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "$maskedQuoteId" + billing_address: { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2", "test street 3"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + } + ) { + cart { + billing_address { + firstname + } + } + } +} +QUERY; + + self::expectExceptionMessage('"Street Address" cannot contain more than 2 lines.'); + $this->graphQlMutation($query); + } + + /** + * Verify the all the whitelisted fields for a New Address Object + * + * @param array $addressResponse + * @param string $addressType + */ + private function assertNewAddressFields(array $addressResponse, string $addressType = 'BillingCartAddress'): 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' => '__typename', 'expected_value' => $addressType] + ]; + + $this->assertResponseFields($addressResponse, $assertionMap); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetGuestEmailOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetGuestEmailOnCartTest.php new file mode 100644 index 000000000000..b877dccdeba3 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetGuestEmailOnCartTest.php @@ -0,0 +1,141 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for setGuestEmailOnCart mutation + */ +class SetGuestEmailOnCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + */ + public function testSetGuestEmailOnCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $email = 'some@user.com'; + + $query = $this->getQuery($maskedQuoteId, $email); + $response = $this->graphQlMutation($query); + + $this->assertArrayHasKey('setGuestEmailOnCart', $response); + $this->assertArrayHasKey('cart', $response['setGuestEmailOnCart']); + $this->assertEquals($email, $response['setGuestEmailOnCart']['cart']['email']); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + */ + public function testSetGuestEmailOnCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $email = 'some@user.com'; + + $query = $this->getQuery($maskedQuoteId, $email); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * + * @dataProvider incorrectEmailDataProvider + * @param string $email + * @param string $exceptionMessage + */ + public function testSetGuestEmailOnCartWithIncorrectEmail( + string $email, + string $exceptionMessage + ) { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $email); + $this->expectExceptionMessage($exceptionMessage); + $this->graphQlMutation($query); + } + + /** + * @return array + */ + public function incorrectEmailDataProvider(): array + { + return [ + 'wrong_email' => ['some', 'Invalid email format'], + 'no_email' => ['', 'Required parameter "email" is missing'], + ]; + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testSetGuestEmailOnNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $email = 'some@user.com'; + + $query = $this->getQuery($maskedQuoteId, $email); + $this->graphQlMutation($query); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Required parameter "cart_id" is missing + */ + public function testSetGuestEmailWithEmptyCartId() + { + $maskedQuoteId = ''; + $email = 'some@user.com'; + + $query = $this->getQuery($maskedQuoteId, $email); + $this->graphQlMutation($query); + } + + /** + * Returns GraphQl mutation query for setting email address for a guest + * + * @param string $maskedQuoteId + * @param string $email + * @return string + */ + private function getQuery(string $maskedQuoteId, string $email): string + { + return <<<QUERY +mutation { + setGuestEmailOnCart(input: { + cart_id: "$maskedQuoteId" + email: "$email" + }) { + cart { + email + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetOfflinePaymentMethodsOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetOfflinePaymentMethodsOnCartTest.php new file mode 100644 index 000000000000..7ec3148ea85a --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetOfflinePaymentMethodsOnCartTest.php @@ -0,0 +1,157 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\OfflinePayments\Model\Banktransfer; +use Magento\OfflinePayments\Model\Cashondelivery; +use Magento\OfflinePayments\Model\Checkmo; +use Magento\OfflinePayments\Model\Purchaseorder; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for setting offline payment methods on cart + */ +class SetOfflinePaymentMethodsOnCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_payment_methods.php + * + * @param string $methodCode + * @param string $methodTitle + * @dataProvider offlinePaymentMethodDataProvider + */ + public function testSetOfflinePaymentMethod(string $methodCode, string $methodTitle) + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('setPaymentMethodOnCart', $response); + self::assertArrayHasKey('cart', $response['setPaymentMethodOnCart']); + self::assertArrayHasKey('selected_payment_method', $response['setPaymentMethodOnCart']['cart']); + + $selectedPaymentMethod = $response['setPaymentMethodOnCart']['cart']['selected_payment_method']; + self::assertArrayHasKey('code', $selectedPaymentMethod); + self::assertEquals($methodCode, $selectedPaymentMethod['code']); + + self::assertArrayHasKey('title', $selectedPaymentMethod); + self::assertEquals($methodTitle, $selectedPaymentMethod['title']); + } + + /** + * @return array + */ + public function offlinePaymentMethodDataProvider(): array + { + return [ + 'check_mo' => [Checkmo::PAYMENT_METHOD_CHECKMO_CODE, 'Check / Money order'], + 'bank_transfer' => [Banktransfer::PAYMENT_METHOD_BANKTRANSFER_CODE, 'Bank Transfer Payment'], + 'cash_on_delivery' => [Cashondelivery::PAYMENT_METHOD_CASHONDELIVERY_CODE, 'Cash On Delivery'], + ]; + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_payment_methods.php + */ + public function testSetPurchaseOrderPaymentMethod() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $methodCode = Purchaseorder::PAYMENT_METHOD_PURCHASEORDER_CODE; + $methodTitle = 'Purchase Order'; + $poNumber = 'abc123'; + + $query = <<<QUERY +mutation { + setPaymentMethodOnCart(input: { + cart_id: "{$maskedQuoteId}", + payment_method: { + code: "{$methodCode}" + purchase_order_number: "{$poNumber}" + } + }) { + cart { + selected_payment_method { + code + title + purchase_order_number + } + } + } +} +QUERY; + + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('setPaymentMethodOnCart', $response); + self::assertArrayHasKey('cart', $response['setPaymentMethodOnCart']); + self::assertArrayHasKey('selected_payment_method', $response['setPaymentMethodOnCart']['cart']); + + $selectedPaymentMethod = $response['setPaymentMethodOnCart']['cart']['selected_payment_method']; + self::assertArrayHasKey('code', $selectedPaymentMethod); + self::assertEquals($methodCode, $selectedPaymentMethod['code']); + + self::assertArrayHasKey('title', $selectedPaymentMethod); + self::assertEquals($methodTitle, $selectedPaymentMethod['title']); + + self::assertArrayHasKey('purchase_order_number', $selectedPaymentMethod); + self::assertEquals($poNumber, $selectedPaymentMethod['purchase_order_number']); + } + + /** + * @param string $maskedQuoteId + * @param string $methodCode + * @return string + */ + private function getQuery( + string $maskedQuoteId, + string $methodCode + ) : string { + return <<<QUERY +mutation { + setPaymentMethodOnCart(input: { + cart_id: "{$maskedQuoteId}", + payment_method: { + code: "{$methodCode}" + } + }) { + cart { + selected_payment_method { + code + title + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetOfflineShippingMethodsOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetOfflineShippingMethodsOnCartTest.php new file mode 100644 index 000000000000..ff2c5f2e6b52 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetOfflineShippingMethodsOnCartTest.php @@ -0,0 +1,170 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for setting offline shipping methods on cart + */ +class SetOfflineShippingMethodsOnCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @magentoApiDataFixture Magento/OfflineShipping/_files/tablerates_weight.php + * + * @param string $carrierCode + * @param string $methodCode + * @param string $carrierTitle + * @param string $methodTitle + * @param array $amount + * @param array $baseAmount + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @dataProvider offlineShippingMethodDataProvider + */ + public function testSetOfflineShippingMethod( + string $carrierCode, + string $methodCode, + string $carrierTitle, + string $methodTitle, + array $amount, + array $baseAmount + ) { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery( + $maskedQuoteId, + $methodCode, + $carrierCode + ); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('setShippingMethodsOnCart', $response); + self::assertArrayHasKey('cart', $response['setShippingMethodsOnCart']); + self::assertArrayHasKey('shipping_addresses', $response['setShippingMethodsOnCart']['cart']); + self::assertCount(1, $response['setShippingMethodsOnCart']['cart']['shipping_addresses']); + + $shippingAddress = current($response['setShippingMethodsOnCart']['cart']['shipping_addresses']); + self::assertArrayHasKey('selected_shipping_method', $shippingAddress); + + self::assertArrayHasKey('carrier_code', $shippingAddress['selected_shipping_method']); + self::assertEquals($carrierCode, $shippingAddress['selected_shipping_method']['carrier_code']); + + self::assertArrayHasKey('method_code', $shippingAddress['selected_shipping_method']); + self::assertEquals($methodCode, $shippingAddress['selected_shipping_method']['method_code']); + + self::assertArrayHasKey('carrier_title', $shippingAddress['selected_shipping_method']); + self::assertEquals($carrierTitle, $shippingAddress['selected_shipping_method']['carrier_title']); + + self::assertArrayHasKey('method_title', $shippingAddress['selected_shipping_method']); + self::assertEquals($methodTitle, $shippingAddress['selected_shipping_method']['method_title']); + + self::assertArrayHasKey('amount', $shippingAddress['selected_shipping_method']); + self::assertEquals($amount, $shippingAddress['selected_shipping_method']['amount']); + + self::assertArrayHasKey('base_amount', $shippingAddress['selected_shipping_method']); + self::assertEquals($baseAmount, $shippingAddress['selected_shipping_method']['base_amount']); + } + + /** + * @return array + */ + public function offlineShippingMethodDataProvider(): array + { + return [ + 'flatrate_flatrate' => [ + 'flatrate', + 'flatrate', + 'Flat Rate', + 'Fixed', + ['value' => 10, 'currency' => 'USD'], + ['value' => 10, 'currency' => 'USD'], + ], + 'tablerate_bestway' => [ + 'tablerate', + 'bestway', + 'Best Way', + 'Table Rate', + ['value' => 10, 'currency' => 'USD'], + ['value' => 10, 'currency' => 'USD'], + ], + 'freeshipping_freeshipping' => [ + 'freeshipping', + 'freeshipping', + 'Free Shipping', + 'Free', + ['value' => 0, 'currency' => 'USD'], + ['value' => 0, 'currency' => 'USD'], + ], + ]; + } + + /** + * @param string $maskedQuoteId + * @param string $shippingMethodCode + * @param string $shippingCarrierCode + * @return string + */ + private function getQuery( + string $maskedQuoteId, + string $shippingMethodCode, + string $shippingCarrierCode + ): string { + return <<<QUERY +mutation { + setShippingMethodsOnCart(input: + { + cart_id: "$maskedQuoteId", + shipping_methods: [{ + carrier_code: "$shippingCarrierCode" + method_code: "$shippingMethodCode" + }] + }) { + cart { + shipping_addresses { + selected_shipping_method { + carrier_code + method_code + carrier_title + method_title + amount { + value + currency + } + base_amount { + value + currency + } + } + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodOnCartTest.php new file mode 100644 index 000000000000..1b2ceecd213a --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodOnCartTest.php @@ -0,0 +1,264 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Exception; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\OfflinePayments\Model\Cashondelivery; +use Magento\OfflinePayments\Model\Checkmo; +use Magento\OfflinePayments\Model\Purchaseorder; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for setting payment methods on cart by guest + */ +class SetPaymentMethodOnCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testSetPaymentOnCartWithSimpleProduct() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('setPaymentMethodOnCart', $response); + self::assertArrayHasKey('cart', $response['setPaymentMethodOnCart']); + self::assertArrayHasKey('selected_payment_method', $response['setPaymentMethodOnCart']['cart']); + self::assertEquals($methodCode, $response['setPaymentMethodOnCart']['cart']['selected_payment_method']['code']); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * + * @expectedException Exception + * @expectedExceptionMessage The shipping address is missing. Set the address and try again. + */ + public function testSetPaymentOnCartWithSimpleProductAndWithoutAddress() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_virtual_product.php + */ + public function testSetPaymentOnCartWithVirtualProduct() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('setPaymentMethodOnCart', $response); + self::assertArrayHasKey('cart', $response['setPaymentMethodOnCart']); + self::assertArrayHasKey('selected_payment_method', $response['setPaymentMethodOnCart']['cart']); + self::assertEquals($methodCode, $response['setPaymentMethodOnCart']['cart']['selected_payment_method']['code']); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * + * @expectedException Exception + * @expectedExceptionMessage The requested Payment Method is not available. + */ + public function testSetNonExistentPaymentMethod() + { + $methodCode = 'noway'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + $this->graphQlMutation($query); + } + + /** + * @expectedException Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testSetPaymentOnNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + + $query = $this->getQuery($maskedQuoteId, $methodCode); + $this->graphQlMutation($query); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testSetPaymentMethodToCustomerCart() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * + * @param string $input + * @param string $message + * @dataProvider dataProviderSetPaymentMethodWithoutRequiredParameters + * @throws Exception + */ + public function testSetPaymentMethodWithoutRequiredParameters(string $input, string $message) + { + $query = <<<QUERY +mutation { + setPaymentMethodOnCart( + input: { + {$input} + } + ) { + cart { + items { + quantity + } + } + } +} +QUERY; + $this->expectExceptionMessage($message); + $this->graphQlMutation($query); + } + + /** + * @return array + */ + public function dataProviderSetPaymentMethodWithoutRequiredParameters(): array + { + return [ + 'missed_cart_id' => [ + 'payment_method: {code: "' . Checkmo::PAYMENT_METHOD_CHECKMO_CODE . '"}', + 'Required parameter "cart_id" is missing.' + ], + 'missed_payment_method' => [ + 'cart_id: "test"', + 'Required parameter "code" for "payment_method" is missing.' + ], + 'missed_payment_method_code' => [ + 'cart_id: "test", payment_method: {code: ""}', + 'Required parameter "code" for "payment_method" is missing.' + ], + ]; + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_payment_methods.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_checkmo_payment_method.php + */ + public function testReSetPayment() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $methodCode = Cashondelivery::PAYMENT_METHOD_CASHONDELIVERY_CODE; + $query = $this->getQuery($maskedQuoteId, $methodCode); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('setPaymentMethodOnCart', $response); + self::assertArrayHasKey('cart', $response['setPaymentMethodOnCart']); + self::assertArrayHasKey('selected_payment_method', $response['setPaymentMethodOnCart']['cart']); + self::assertArrayHasKey('code', $response['setPaymentMethodOnCart']['cart']['selected_payment_method']); + self::assertEquals($methodCode, $response['setPaymentMethodOnCart']['cart']['selected_payment_method']['code']); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @expectedException Exception + * @expectedExceptionMessage The requested Payment Method is not available. + */ + public function testSetDisabledPaymentOnCart() + { + $methodCode = Purchaseorder::PAYMENT_METHOD_PURCHASEORDER_CODE; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + $this->graphQlMutation($query); + } + + /** + * @param string $maskedQuoteId + * @param string $methodCode + * @return string + */ + private function getQuery( + string $maskedQuoteId, + string $methodCode + ) : string { + return <<<QUERY +mutation { + setPaymentMethodOnCart(input: { + cart_id: "{$maskedQuoteId}", + payment_method: { + code: "{$methodCode}" + } + }) { + cart { + selected_payment_method { + code + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPurchaseOrderPaymentMethodOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPurchaseOrderPaymentMethodOnCartTest.php new file mode 100644 index 000000000000..9dd0382a5dbe --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPurchaseOrderPaymentMethodOnCartTest.php @@ -0,0 +1,147 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Exception; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\OfflinePayments\Model\Purchaseorder; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for setting Purchase Order payment method on cart by guest + */ +class SetPurchaseOrderPaymentMethodOnCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_payment_methods.php + */ + public function testSetPurchaseOrderPaymentMethodOnCartWithSimpleProduct() + { + $methodCode = Purchaseorder::PAYMENT_METHOD_PURCHASEORDER_CODE; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $purchaseOrderNumber = '123456'; + + $query = <<<QUERY +mutation { + setPaymentMethodOnCart(input: { + cart_id: "$maskedQuoteId" + payment_method: { + code: "$methodCode" + purchase_order_number: "$purchaseOrderNumber" + } + }) { + cart { + selected_payment_method { + code + purchase_order_number + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('setPaymentMethodOnCart', $response); + self::assertArrayHasKey('cart', $response['setPaymentMethodOnCart']); + self::assertArrayHasKey('selected_payment_method', $response['setPaymentMethodOnCart']['cart']); + self::assertEquals($methodCode, $response['setPaymentMethodOnCart']['cart']['selected_payment_method']['code']); + self::assertEquals( + $purchaseOrderNumber, + $response['setPaymentMethodOnCart']['cart']['selected_payment_method']['purchase_order_number'] + ); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_payment_methods.php + * + * @expectedException Exception + * @expectedExceptionMessage Purchase order number is a required field. + */ + public function testSetPurchaseOrderPaymentMethodOnCartWithoutPurchaseOrderNumber() + { + $methodCode = Purchaseorder::PAYMENT_METHOD_PURCHASEORDER_CODE; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setPaymentMethodOnCart(input: { + cart_id: "$maskedQuoteId" + payment_method: { + code: "$methodCode" + } + }) { + cart { + selected_payment_method { + code + } + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * + * @expectedException Exception + * @expectedExceptionMessage The requested Payment Method is not available. + */ + public function testSetDisabledPurchaseOrderPaymentMethodOnCart() + { + $methodCode = Purchaseorder::PAYMENT_METHOD_PURCHASEORDER_CODE; + $purchaseOrderNumber = '123456'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setPaymentMethodOnCart(input: { + cart_id: "$maskedQuoteId" + payment_method: { + code: "$methodCode" + purchase_order_number: "$purchaseOrderNumber" + } + }) { + cart { + selected_payment_method { + code + purchase_order_number + } + } + } +} +QUERY; + $this->graphQlMutation($query); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressOnCartTest.php new file mode 100644 index 000000000000..c58098c6c254 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressOnCartTest.php @@ -0,0 +1,391 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for set shipping addresses on cart mutation + */ +class SetShippingAddressOnCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetNewShippingAddressOnCartWithSimpleProduct() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + ] + } + ) { + cart { + shipping_addresses { + firstname + lastname + company + street + city + postcode + telephone + country { + code + label + } + __typename + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('cart', $response['setShippingAddressesOnCart']); + $cartResponse = $response['setShippingAddressesOnCart']['cart']; + self::assertArrayHasKey('shipping_addresses', $cartResponse); + $shippingAddressResponse = current($cartResponse['shipping_addresses']); + $this->assertNewShippingAddressFields($shippingAddressResponse); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_virtual_product.php + * + * @expectedException \Exception + * @expectedExceptionMessage The Cart includes virtual product(s) only, so a shipping address is not used. + */ + public function testSetNewShippingAddressOnCartWithVirtualProduct() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + ] + } + ) { + cart { + shipping_addresses { + city + } + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * _security + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * + * @expectedException \Exception + * @expectedExceptionMessage The current customer isn't authorized. + */ + public function testSetShippingAddressFromAddressBook() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + customer_address_id: 1 + } + ] + } + ) { + cart { + shipping_addresses { + city + } + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * + * @expectedException \Exception + */ + public function testSetShippingAddressToCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + customer_address_id: 1 + } + ] + } + ) { + cart { + shipping_addresses { + postcode + } + } + } +} +QUERY; + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * + * @dataProvider dataProviderUpdateWithMissedRequiredParameters + * @param string $input + * @param string $message + * @throws \Exception + */ + public function testSetNewShippingAddressWithMissedRequiredParameters(string $input, string $message) + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "{$maskedQuoteId}" + shipping_addresses: [ + { + {$input} + } + ] + } + ) { + cart { + shipping_addresses { + city + } + } + } +} +QUERY; + $this->expectExceptionMessage($message); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetNewShippingAddressOnCartWithRedundantStreetLine() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2", "test street 3"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + ] + } + ) { + cart { + shipping_addresses { + firstname + } + } + } +} +QUERY; + self::expectExceptionMessage('"Street Address" cannot contain more than 2 lines.'); + $this->graphQlMutation($query); + } + + /** + * @return array + */ + public function dataProviderUpdateWithMissedRequiredParameters(): array + { + return [ + 'shipping_addresses' => [ + '', + 'The shipping address must contain either "customer_address_id" or "address".', + ], + 'missed_city' => [ + 'address: { save_in_address_book: false }', + 'Field CartAddressInput.city of required type String! was not provided' + ] + ]; + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * + * @expectedException \Exception + * @expectedExceptionMessage You cannot specify multiple shipping addresses. + */ + public function testSetMultipleNewShippingAddresses() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + address: { + firstname: "test firstname" + lastname: "test lastname" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + }, + { + address: { + firstname: "test firstname 2" + lastname: "test lastname 2" + company: "test company 2" + street: ["test street 1", "test street 2"] + city: "test city" + region: "test region" + postcode: "887766" + country_code: "US" + telephone: "88776655" + save_in_address_book: false + } + } + ] + } + ) { + cart { + shipping_addresses { + city + } + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * Verify the all the whitelisted fields for a New Address Object + * + * @param array $shippingAddressResponse + */ + private function assertNewShippingAddressFields(array $shippingAddressResponse): void + { + $assertionMap = [ + ['response_field' => 'firstname', 'expected_value' => 'test firstname'], + ['response_field' => 'lastname', 'expected_value' => 'test lastname'], + ['response_field' => 'company', 'expected_value' => 'test company'], + ['response_field' => 'street', 'expected_value' => [0 => 'test street 1', 1 => 'test street 2']], + ['response_field' => 'city', 'expected_value' => 'test city'], + ['response_field' => 'postcode', 'expected_value' => '887766'], + ['response_field' => 'telephone', 'expected_value' => '88776655'], + ['response_field' => 'country', 'expected_value' => ['code' => 'US', 'label' => 'US']], + ['response_field' => '__typename', 'expected_value' => 'ShippingCartAddress'] + ]; + + $this->assertResponseFields($shippingAddressResponse, $assertionMap); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingMethodsOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingMethodsOnCartTest.php new file mode 100644 index 000000000000..0c2bf1453b54 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingMethodsOnCartTest.php @@ -0,0 +1,400 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote\Guest; + +use Exception; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for setting shipping methods on cart for guest + */ +class SetShippingMethodsOnCartTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + */ + public function testSetShippingMethodOnCartWithSimpleProduct() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $carrierCode = 'flatrate'; + $methodCode = 'flatrate'; + + $query = $this->getQuery( + $maskedQuoteId, + $methodCode, + $carrierCode + ); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('setShippingMethodsOnCart', $response); + self::assertArrayHasKey('cart', $response['setShippingMethodsOnCart']); + self::assertArrayHasKey('shipping_addresses', $response['setShippingMethodsOnCart']['cart']); + self::assertCount(1, $response['setShippingMethodsOnCart']['cart']['shipping_addresses']); + + $shippingAddress = current($response['setShippingMethodsOnCart']['cart']['shipping_addresses']); + self::assertArrayHasKey('selected_shipping_method', $shippingAddress); + + self::assertArrayHasKey('carrier_code', $shippingAddress['selected_shipping_method']); + self::assertEquals('flatrate', $shippingAddress['selected_shipping_method']['carrier_code']); + + self::assertArrayHasKey('method_code', $shippingAddress['selected_shipping_method']); + self::assertEquals('flatrate', $shippingAddress['selected_shipping_method']['method_code']); + + self::assertArrayHasKey('carrier_title', $shippingAddress['selected_shipping_method']); + self::assertEquals('Flat Rate', $shippingAddress['selected_shipping_method']['carrier_title']); + + self::assertArrayHasKey('method_title', $shippingAddress['selected_shipping_method']); + self::assertEquals('Fixed', $shippingAddress['selected_shipping_method']['method_title']); + + self::assertArrayHasKey('amount', $shippingAddress['selected_shipping_method']); + $amount = $shippingAddress['selected_shipping_method']['amount']; + + self::assertArrayHasKey('value', $amount); + self::assertEquals(10, $amount['value']); + self::assertArrayHasKey('currency', $amount); + self::assertEquals('USD', $amount['currency']); + + self::assertArrayHasKey('base_amount', $shippingAddress['selected_shipping_method']); + $baseAmount = $shippingAddress['selected_shipping_method']['base_amount']; + + self::assertArrayHasKey('value', $baseAmount); + self::assertEquals(10, $baseAmount['value']); + self::assertArrayHasKey('currency', $baseAmount); + self::assertEquals('USD', $baseAmount['currency']); + } + + /** + * Shipping address for quote will be created automatically BUT with NULL values (considered that address + * is not set) + * + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_virtual_product.php + * + * @expectedException Exception + * @expectedExceptionMessage The shipping address is missing. Set the address and try again. + */ + public function testSetShippingMethodOnCartWithSimpleProductAndWithoutAddress() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $carrierCode = 'flatrate'; + $methodCode = 'flatrate'; + + $query = $this->getQuery( + $maskedQuoteId, + $methodCode, + $carrierCode + ); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testReSetShippingMethod() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $carrierCode = 'freeshipping'; + $methodCode = 'freeshipping'; + + $query = $this->getQuery( + $maskedQuoteId, + $methodCode, + $carrierCode + ); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('setShippingMethodsOnCart', $response); + self::assertArrayHasKey('cart', $response['setShippingMethodsOnCart']); + self::assertArrayHasKey('shipping_addresses', $response['setShippingMethodsOnCart']['cart']); + self::assertCount(1, $response['setShippingMethodsOnCart']['cart']['shipping_addresses']); + + $shippingAddress = current($response['setShippingMethodsOnCart']['cart']['shipping_addresses']); + self::assertArrayHasKey('selected_shipping_method', $shippingAddress); + + self::assertArrayHasKey('carrier_code', $shippingAddress['selected_shipping_method']); + self::assertEquals($carrierCode, $shippingAddress['selected_shipping_method']['carrier_code']); + + self::assertArrayHasKey('method_code', $shippingAddress['selected_shipping_method']); + self::assertEquals($methodCode, $shippingAddress['selected_shipping_method']['method_code']); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * + * @param string $input + * @param string $message + * @dataProvider dataProviderSetShippingMethodWithWrongParameters + * @throws Exception + */ + public function testSetShippingMethodWithWrongParameters(string $input, string $message) + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $input = str_replace('cart_id_value', $maskedQuoteId, $input); + + $query = <<<QUERY +mutation { + setShippingMethodsOnCart(input: { + {$input} + }) { + cart { + shipping_addresses { + selected_shipping_method { + carrier_code + } + } + } + } +} +QUERY; + $this->expectExceptionMessage($message); + $this->graphQlMutation($query); + } + + /** + * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function dataProviderSetShippingMethodWithWrongParameters(): array + { + return [ + 'missed_cart_id' => [ + 'shipping_methods: [{ + carrier_code: "flatrate" + method_code: "flatrate" + }]', + 'Required parameter "cart_id" is missing' + ], + 'missed_shipping_methods' => [ + 'cart_id: "cart_id_value"', + 'Required parameter "shipping_methods" is missing' + ], + 'shipping_methods_are_empty' => [ + 'cart_id: "cart_id_value" shipping_methods: []', + 'Required parameter "shipping_methods" is missing' + ], + 'missed_carrier_code' => [ + 'cart_id: "cart_id_value", shipping_methods: [{ + method_code: "flatrate" + }]', + 'Field ShippingMethodInput.carrier_code of required type String! was not provided.' + ], + 'empty_carrier_code' => [ + 'cart_id: "cart_id_value", shipping_methods: [{ + carrier_code: "" + method_code: "flatrate" + }]', + 'Required parameter "carrier_code" is missing.' + ], + 'non_existent_carrier_code' => [ + 'cart_id: "cart_id_value", shipping_methods: [{ + carrier_code: "wrong-carrier-code" + method_code: "flatrate" + }]', + 'Carrier with such method not found: wrong-carrier-code, flatrate' + ], + 'missed_method_code' => [ + 'cart_id: "cart_id_value", shipping_methods: [{ + carrier_code: "flatrate" + }]', + 'Required parameter "method_code" is missing.' + ], + 'empty_method_code' => [ + 'cart_id: "cart_id_value", shipping_methods: [{ + carrier_code: "flatrate" + method_code: "" + }]', + 'Required parameter "method_code" is missing.' + ], + 'non_existent_method_code' => [ + 'cart_id: "cart_id_value", shipping_methods: [{ + carrier_code: "flatrate" + method_code: "wrong-carrier-code" + }]', + 'Carrier with such method not found: flatrate, wrong-carrier-code' + ], + 'non_existent_shopping_cart' => [ + 'cart_id: "non_existent_masked_id", shipping_methods: [{ + carrier_code: "flatrate" + method_code: "flatrate" + }]', + 'Could not find a cart with ID "non_existent_masked_id"' + ], + 'disabled_shipping_method' => [ + 'cart_id: "cart_id_value", shipping_methods: [{ + carrier_code: "freeshipping" + method_code: "freeshipping" + }]', + 'Carrier with such method not found: freeshipping, freeshipping' + ], + ]; + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @expectedException Exception + * @expectedExceptionMessage You cannot specify multiple shipping methods. + */ + public function testSetMultipleShippingMethods() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingMethodsOnCart(input: { + cart_id: "{$maskedQuoteId}", + shipping_methods: [ + { + carrier_code: "flatrate" + method_code: "flatrate" + } + { + carrier_code: "flatrate" + method_code: "flatrate" + } + ] + }) { + cart { + shipping_addresses { + selected_shipping_method { + carrier_code + } + } + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * _security + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * + * @expectedException Exception + */ + public function testSetShippingMethodToCustomerCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $carrierCode = 'flatrate'; + $methodCode = 'flatrate'; + $query = $this->getQuery( + $maskedQuoteId, + $methodCode, + $carrierCode + ); + + $this->expectExceptionMessage( + "The current user cannot perform operations on cart \"$maskedQuoteId\"" + ); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/quote_with_address.php + * + * @expectedException Exception + * @expectedExceptionMessage The shipping method can't be set for an empty cart. Add an item to cart and try again. + */ + public function testSetShippingMethodOnAnEmptyCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + $carrierCode = 'flatrate'; + $methodCode = 'flatrate'; + + $query = $this->getQuery( + $maskedQuoteId, + $methodCode, + $carrierCode + ); + $this->graphQlMutation($query); + } + + /** + * @param string $maskedQuoteId + * @param string $shippingMethodCode + * @param string $shippingCarrierCode + * @return string + */ + private function getQuery( + string $maskedQuoteId, + string $shippingMethodCode, + string $shippingCarrierCode + ): string { + return <<<QUERY +mutation { + setShippingMethodsOnCart(input: + { + cart_id: "$maskedQuoteId", + shipping_methods: [{ + carrier_code: "$shippingCarrierCode" + method_code: "$shippingMethodCode" + }] + }) { + cart { + shipping_addresses { + selected_shipping_method { + carrier_code + method_code + carrier_title + method_title + amount { + value + currency + } + base_amount { + value + currency + } + } + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/UpdateCartItemsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/UpdateCartItemsTest.php new file mode 100644 index 000000000000..1e1fb0a17699 --- /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 testUpdateCartItemQuantity() + { + $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(); + $quantity = 2; + + $query = $this->getQuery($maskedQuoteId, $itemId, $quantity); + $response = $this->graphQlMutation($query); + + $this->assertArrayHasKey('updateCartItems', $response); + $this->assertArrayHasKey('cart', $response['updateCartItems']); + + $responseCart = $response['updateCartItems']['cart']; + $item = current($responseCart['items']); + + $this->assertEquals($itemId, $item['id']); + $this->assertEquals($quantity, $item['quantity']); + } + + /** + * @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(); + $quantity = 0; + + $query = $this->getQuery($maskedQuoteId, $itemId, $quantity); + $response = $this->graphQlMutation($query); + + $this->assertArrayHasKey('updateCartItems', $response); + $this->assertArrayHasKey('cart', $response['updateCartItems']); + + $responseCart = $response['updateCartItems']['cart']; + $this->assertCount(0, $responseCart['items']); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testUpdateItemInNonExistentCart() + { + $query = $this->getQuery('non_existent_masked_id', 1, 2); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + */ + public function testUpdateNonExistentItem() + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, 'test_order_with_simple_product_without_address', 'reserved_order_id'); + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$quote->getId()); + $notExistentItemId = 999; + + $this->expectExceptionMessage("Could not find cart item with id: {$notExistentItemId}."); + + $query = $this->getQuery($maskedQuoteId, $notExistentItemId, 2); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_virtual_product_saved.php + */ + public function testUpdateItemIfItemIsNotBelongToCart() + { + $firstQuote = $this->quoteFactory->create(); + $this->quoteResource->load($firstQuote, 'test_order_with_simple_product_without_address', 'reserved_order_id'); + $firstQuoteMaskedId = $this->quoteIdToMaskedId->execute((int)$firstQuote->getId()); + + $secondQuote = $this->quoteFactory->create(); + $this->quoteResource->load( + $secondQuote, + 'test_order_with_virtual_product_without_address', + 'reserved_order_id' + ); + $secondQuoteItemId = (int)$secondQuote + ->getItemByProduct($this->productRepository->get('virtual-product')) + ->getId(); + + $this->expectExceptionMessage("Could not find cart item with id: {$secondQuoteItemId}."); + + $query = $this->getQuery($firstQuoteMaskedId, $secondQuoteItemId, 2); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php + */ + public function testUpdateItemFromCustomerCart() + { + $customerQuote = $this->quoteFactory->create(); + $this->quoteResource->load($customerQuote, 'test_order_1', 'reserved_order_id'); + $customerQuoteMaskedId = $this->quoteIdToMaskedId->execute((int)$customerQuote->getId()); + $customerQuoteItemId = (int)$customerQuote->getItemByProduct($this->productRepository->get('simple'))->getId(); + + $this->expectExceptionMessage("The current user cannot perform operations on cart \"$customerQuoteMaskedId\""); + + $query = $this->getQuery($customerQuoteMaskedId, $customerQuoteItemId, 2); + $this->graphQlMutation($query); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Required parameter "cart_id" is missing. + */ + public function testUpdateWithMissedCartItemId() + { + $query = <<<QUERY +mutation { + updateCartItems(input: { + cart_items: [ + { + cart_item_id: 1 + quantity: 2 + } + ] + }) { + cart { + items { + id + quantity + } + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * @param string $input + * @param string $message + * @dataProvider dataProviderUpdateWithMissedRequiredParameters + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + */ + public function testUpdateWithMissedItemRequiredParameters(string $input, string $message) + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, 'test_order_with_simple_product_without_address', 'reserved_order_id'); + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$quote->getId()); + + $query = <<<QUERY +mutation { + updateCartItems(input: { + cart_id: "{$maskedQuoteId}" + {$input} + }) { + cart { + items { + id + quantity + } + } + } +} +QUERY; + $this->expectExceptionMessage($message); + $this->graphQlMutation($query); + } + + /** + * @return array + */ + public function dataProviderUpdateWithMissedRequiredParameters(): array + { + return [ + 'missed_cart_items' => [ + '', + 'Required parameter "cart_items" is missing.' + ], + 'missed_cart_item_id' => [ + 'cart_items: [{ quantity: 2 }]', + 'Required parameter "cart_item_id" for "cart_items" is missing.' + ], + 'missed_cart_item_qty' => [ + 'cart_items: [{ cart_item_id: 1 }]', + 'Field CartItemUpdateInput.quantity of required type Float! was not provided.' + ], + ]; + } + + /** + * @param string $maskedQuoteId + * @param int $itemId + * @param float $quantity + * @return string + */ + private function getQuery(string $maskedQuoteId, int $itemId, float $quantity): string + { + return <<<QUERY +mutation { + updateCartItems(input: { + cart_id: "{$maskedQuoteId}" + cart_items: [ + { + cart_item_id: {$itemId} + quantity: {$quantity} + } + ] + }) { + cart { + items { + id + quantity + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/OrdersTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/OrdersTest.php new file mode 100644 index 000000000000..9c969befa328 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/OrdersTest.php @@ -0,0 +1,115 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Sales; + +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Class OrdersTest + */ +class OrdersTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + protected function setUp() + { + parent::setUp(); + $this->customerTokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Sales/_files/orders_with_customer.php + */ + public function testOrdersQuery() + { + $query = + <<<QUERY +query { + customerOrders { + items { + id + increment_id + created_at + grand_total + status + } + } +} +QUERY; + + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlQuery($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + + $expectedData = [ + [ + 'increment_id' => '100000002', + 'status' => 'processing', + 'grand_total' => 120.00 + ], + [ + 'increment_id' => '100000003', + 'status' => 'processing', + 'grand_total' => 130.00 + ], + [ + 'increment_id' => '100000004', + 'status' => 'closed', + 'grand_total' => 140.00 + ], + [ + 'increment_id' => '100000005', + 'status' => 'complete', + 'grand_total' => 150.00 + ], + [ + 'increment_id' => '100000006', + 'status' => 'complete', + 'grand_total' => 160.00 + ] + ]; + + $actualData = $response['customerOrders']['items']; + + foreach ($expectedData as $key => $data) { + $this->assertEquals( + $data['increment_id'], + $actualData[$key]['increment_id'], + "increment_id is different than the expected for order - " . $data['increment_id'] + ); + $this->assertEquals( + $data['grand_total'], + $actualData[$key]['grand_total'], + "grand_total is different than the expected for order - " . $data['increment_id'] + ); + $this->assertEquals( + $data['status'], + $actualData[$key]['status'], + "status is different than the expected for order - " . $data['increment_id'] + ); + } + } + + /** + * @param string $email + * @param string $password + * @return array + * @throws \Magento\Framework\Exception\AuthenticationException + */ + private function getCustomerAuthHeaders(string $email, string $password): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($email, $password); + return ['Authorization' => 'Bearer ' . $customerToken]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/SendFriend/SendFriendTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/SendFriend/SendFriendTest.php new file mode 100644 index 000000000000..ae6faae7650b --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/SendFriend/SendFriendTest.php @@ -0,0 +1,361 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\SendFriend; + +use Magento\SendFriend\Model\SendFriend; +use Magento\SendFriend\Model\SendFriendFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Tests for send email to friend + */ +class SendFriendTest extends GraphQlAbstract +{ + + /** + * @var SendFriendFactory + */ + private $sendFriendFactory; + + protected function setUp() + { + $this->sendFriendFactory = Bootstrap::getObjectManager()->get(SendFriendFactory::class); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testSendFriend() + { + $query = + <<<QUERY +mutation { + sendEmailToFriend( + input: { + product_id: 1 + sender: { + name: "Name" + email: "e@mail.com" + message: "Lorem Ipsum" + } + recipients: [ + { + name: "Recipient Name 1" + email:"recipient1@mail.com" + }, + { + name: "Recipient Name 2" + email:"recipient2@mail.com" + } + ] + } + ) { + sender { + name + email + message + } + recipients { + name + email + } + } +} +QUERY; + + $response = $this->graphQlMutation($query); + self::assertEquals('Name', $response['sendEmailToFriend']['sender']['name']); + self::assertEquals('e@mail.com', $response['sendEmailToFriend']['sender']['email']); + self::assertEquals('Lorem Ipsum', $response['sendEmailToFriend']['sender']['message']); + self::assertEquals('Recipient Name 1', $response['sendEmailToFriend']['recipients'][0]['name']); + self::assertEquals('recipient1@mail.com', $response['sendEmailToFriend']['recipients'][0]['email']); + self::assertEquals('Recipient Name 2', $response['sendEmailToFriend']['recipients'][1]['name']); + self::assertEquals('recipient2@mail.com', $response['sendEmailToFriend']['recipients'][1]['email']); + } + + public function testSendWithoutExistProduct() + { + $query = + <<<QUERY +mutation { + sendEmailToFriend( + input: { + product_id: 2018 + sender: { + name: "Name" + email: "e@mail.com" + message: "Lorem Ipsum" + } + recipients: [ + { + name: "Recipient Name 1" + email:"recipient1@mail.com" + }, + { + name: "Recipient Name 2" + email:"recipient2@mail.com" + } + ] + } + ) { + sender { + name + email + message + } + recipients { + name + email + } + } +} +QUERY; + $this->expectException(\Exception::class); + $this->expectExceptionMessage( + 'The product that was requested doesn\'t exist. Verify the product and try again.' + ); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testMaxSendEmailToFriend() + { + /** @var SendFriend $sendFriend */ + $sendFriend = $this->sendFriendFactory->create(); + + $query = + <<<QUERY +mutation { + sendEmailToFriend( + input: { + product_id: 1 + sender: { + name: "Name" + email: "e@mail.com" + message: "Lorem Ipsum" + } + recipients: [ + { + name: "Recipient Name 1" + email:"recipient1@mail.com" + }, + { + name: "Recipient Name 1" + email:"recipient1@mail.com" + }, + { + name: "Recipient Name 1" + email:"recipient1@mail.com" + }, + { + name: "Recipient Name 1" + email:"recipient1@mail.com" + }, + { + name: "Recipient Name 1" + email:"recipient1@mail.com" + }, + { + name: "Recipient Name 1" + email:"recipient1@mail.com" + } + ] + } + ) { + sender { + name + email + message + } + recipients { + name + email + } + } +} +QUERY; + $this->expectException(\Exception::class); + $this->expectExceptionMessage("No more than {$sendFriend->getMaxRecipients()} emails can be sent at a time."); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @dataProvider sendFriendsErrorsDataProvider + * @param string $input + * @param string $errorMessage + */ + public function testErrors(string $input, string $errorMessage) + { + $query = + <<<QUERY +mutation { + sendEmailToFriend( + input: { + $input + } + ) { + sender { + name + email + message + } + recipients { + name + email + } + } +} +QUERY; + $this->expectException(\Exception::class); + $this->expectExceptionMessage($errorMessage); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * TODO: use magentoApiConfigFixture (to be merged https://github.com/magento/graphql-ce/pull/351) + * @magentoApiDataFixture Magento/SendFriend/Fixtures/sendfriend_configuration.php + */ + public function testLimitMessagesPerHour() + { + + /** @var SendFriend $sendFriend */ + $sendFriend = $this->sendFriendFactory->create(); + + $query = + <<<QUERY +mutation { + sendEmailToFriend( + input: { + product_id: 1 + sender: { + name: "Name" + email: "e@mail.com" + message: "Lorem Ipsum" + } + recipients: [ + { + name: "Recipient Name 1" + email:"recipient1@mail.com" + }, + { + name: "Recipient Name 2" + email:"recipient2@mail.com" + } + + ] + } + ) { + sender { + name + email + message + } + recipients { + name + email + } + } +} +QUERY; + $this->expectException(\Exception::class); + $this->expectExceptionMessage( + "You can't send messages more than {$sendFriend->getMaxSendsToFriend()} times an hour." + ); + + $maxSendToFriends = $sendFriend->getMaxSendsToFriend(); + for ($i = 0; $i <= $maxSendToFriends + 1; $i++) { + $this->graphQlMutation($query); + } + } + + /** + * @return array + */ + public function sendFriendsErrorsDataProvider() + { + return [ + [ + 'product_id: 1 + sender: { + name: "Name" + email: "e@mail.com" + message: "Lorem Ipsum" + } + recipients: [ + { + name: "" + email:"recipient1@mail.com" + }, + { + name: "" + email:"recipient2@mail.com" + } + ]', 'Please provide Name for all of recipients.' + ], + [ + 'product_id: 1 + sender: { + name: "Name" + email: "e@mail.com" + message: "Lorem Ipsum" + } + recipients: [ + { + name: "Recipient Name 1" + email:"" + }, + { + name: "Recipient Name 2" + email:"" + } + ]', 'Please provide Email for all of recipients.' + ], + [ + 'product_id: 1 + sender: { + name: "" + email: "e@mail.com" + message: "Lorem Ipsum" + } + recipients: [ + { + name: "Recipient Name 1" + email:"recipient1@mail.com" + }, + { + name: "Recipient Name 2" + email:"recipient2@mail.com" + } + ]', 'Please provide Name of sender.' + ], + [ + 'product_id: 1 + sender: { + name: "Name" + email: "e@mail.com" + message: "" + } + recipients: [ + { + name: "Recipient Name 1" + email:"recipient1@mail.com" + }, + { + name: "Recipient Name 2" + email:"recipient2@mail.com" + } + ]', 'Please provide Message.' + ] + ]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigResolverTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigResolverTest.php index 4657a1e763ae..a536022e3163 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigResolverTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigResolverTest.php @@ -25,6 +25,7 @@ class StoreConfigResolverTest extends GraphQlAbstract protected function setUp() { + $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/167'); $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/TestModule/GraphQlMutationTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/TestModule/GraphQlMutationTest.php index b6e1a61f0357..c85f63c08370 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/TestModule/GraphQlMutationTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/TestModule/GraphQlMutationTest.php @@ -28,10 +28,31 @@ public function testMutation() } MUTATION; - $response = $this->graphQlQuery($query); + $response = $this->graphQlMutation($query); $this->assertArrayHasKey('testItem', $response); $testItem = $response['testItem']; $this->assertArrayHasKey('integer_list', $testItem); $this->assertEquals([4, 5, 6], $testItem['integer_list']); } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Mutation requests allowed only for POST requests + */ + public function testMutationIsNotAllowedViaGetRequest() + { + $id = 3; + + $query = <<<MUTATION +mutation { + testItem(id: {$id}) { + item_id, + name, + integer_list + } +} +MUTATION; + + $this->graphQlQuery($query, [], '', []); + } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/TestModule/GraphQlQueryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/TestModule/GraphQlQueryTest.php index d59e255daa10..2db06e383758 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/TestModule/GraphQlQueryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/TestModule/GraphQlQueryTest.php @@ -58,4 +58,46 @@ public function testQueryTestModuleExtensionAttribute() $this->assertArrayHasKey('integer_list', $testItem); $this->assertEquals([3, 4, 5], $testItem['integer_list']); } + + public function testQueryViaGetRequestReturnsResults() + { + $id = 1; + + $query = <<<QUERY +{ + testItem(id: {$id}) + { + item_id + name + } +} +QUERY; + + $response = $this->graphQlQuery($query, [], '', []); + + $this->assertArrayHasKey('testItem', $response); + } + + public function testQueryViaGetRequestWithVariablesReturnsResults() + { + $id = 1; + + $query = <<<QUERY +query getTestItem(\$id: Int!) +{ + testItem(id: \$id) + { + item_id + name + } +} +QUERY; + $variables = [ + "id" => $id + ]; + + $response = $this->graphQlQuery($query, $variables, '', []); + + $this->assertArrayHasKey('testItem', $response); + } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Ups/SetUpsShippingMethodsOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Ups/SetUpsShippingMethodsOnCartTest.php new file mode 100644 index 000000000000..c50ae0a70380 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Ups/SetUpsShippingMethodsOnCartTest.php @@ -0,0 +1,243 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Ups; + +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for setting "UPS" shipping method on cart. Current class covers the next UPS shipping methods: + * + * | Code | Label + * -------------------------------------- + * | 1DM | Next Day Air Early AM + * | 1DA | Next Day Air + * | 2DA | 2nd Day Air + * | 3DS | 3 Day Select + * | GND | Ground + * | STD | Canada Standard + * | XPR | Worldwide Express + * | WXS | Worldwide Express Saver + * | XDM | Worldwide Express Plus + * | XPD | Worldwide Expedited + * + * Current class does not cover these UPS shipping methods (depends on address and sandbox settings) + * + * | Code | Label + * -------------------------------------- + * | 1DML | Next Day Air Early AM Letter + * | 1DAL | Next Day Air Letter + * | 1DAPI | Next Day Air Intra (Puerto Rico) + * | 1DP | Next Day Air Saver + * | 1DPL | Next Day Air Saver Letter + * | 2DM | 2nd Day Air AM + * | 2DML | 2nd Day Air AM Letter + * | 2DAL | 2nd Day Air Letter + * | GNDCOM | Ground Commercial + * | GNDRES | Ground Residential + * | XPRL | Worldwide Express Letter + * | XDML | Worldwide Express Plus Letter + */ +class SetUpsShippingMethodsOnCartTest extends GraphQlAbstract +{ + /** + * Defines carrier title for "UPS" shipping method + */ + const CARRIER_TITLE = 'United Parcel Service'; + + /** + * Defines carrier code for "UPS" shipping method + */ + const CARRIER_CODE = 'ups'; + + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Ups/_files/enable_ups_shipping_method.php + * + * @dataProvider dataProviderShippingMethods + * @param string $methodCode + * @param string $methodTitle + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function testSetUpsShippingMethod(string $methodCode, string $methodTitle) + { + $quoteReservedId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($quoteReservedId); + + $query = $this->getQuery($maskedQuoteId, self::CARRIER_CODE, $methodCode); + $response = $this->sendRequestWithToken($query); + + self::assertArrayHasKey('setShippingMethodsOnCart', $response); + self::assertArrayHasKey('cart', $response['setShippingMethodsOnCart']); + self::assertArrayHasKey('shipping_addresses', $response['setShippingMethodsOnCart']['cart']); + self::assertCount(1, $response['setShippingMethodsOnCart']['cart']['shipping_addresses']); + + $shippingAddress = current($response['setShippingMethodsOnCart']['cart']['shipping_addresses']); + self::assertArrayHasKey('selected_shipping_method', $shippingAddress); + + self::assertArrayHasKey('carrier_code', $shippingAddress['selected_shipping_method']); + self::assertEquals(self::CARRIER_CODE, $shippingAddress['selected_shipping_method']['carrier_code']); + + self::assertArrayHasKey('method_code', $shippingAddress['selected_shipping_method']); + self::assertEquals($methodCode, $shippingAddress['selected_shipping_method']['method_code']); + + self::assertArrayHasKey('carrier_title', $shippingAddress['selected_shipping_method']); + self::assertEquals(self::CARRIER_TITLE, $shippingAddress['selected_shipping_method']['carrier_title']); + + self::assertArrayHasKey('method_title', $shippingAddress['selected_shipping_method']); + } + + /** + * @return array + */ + public function dataProviderShippingMethods(): array + { + return [ + 'Next Day Air Early AM' => ['1DM', 'Next Day Air Early AM'], + 'Next Day Air' => ['1DA', 'Next Day Air'], + '2nd Day Air' => ['2DA', '2nd Day Air'], + '3 Day Select' => ['3DS', '3 Day Select'], + 'Ground' => ['GND', 'Ground'], + ]; + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_canada_address.php + * @magentoApiDataFixture Magento/GraphQl/Ups/_files/enable_ups_shipping_method.php + * + * @dataProvider dataProviderShippingMethodsBasedOnCanadaAddress + * @param string $methodCode + * @param string $methodTitle + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function testSetUpsShippingMethodBasedOnCanadaAddress(string $methodCode, string $methodTitle) + { + $quoteReservedId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($quoteReservedId); + + $query = $this->getQuery($maskedQuoteId, self::CARRIER_CODE, $methodCode); + $response = $this->sendRequestWithToken($query); + + self::assertArrayHasKey('setShippingMethodsOnCart', $response); + self::assertArrayHasKey('cart', $response['setShippingMethodsOnCart']); + self::assertArrayHasKey('shipping_addresses', $response['setShippingMethodsOnCart']['cart']); + self::assertCount(1, $response['setShippingMethodsOnCart']['cart']['shipping_addresses']); + + $shippingAddress = current($response['setShippingMethodsOnCart']['cart']['shipping_addresses']); + self::assertArrayHasKey('selected_shipping_method', $shippingAddress); + + self::assertArrayHasKey('carrier_code', $shippingAddress['selected_shipping_method']); + self::assertEquals(self::CARRIER_CODE, $shippingAddress['selected_shipping_method']['carrier_code']); + + self::assertArrayHasKey('method_code', $shippingAddress['selected_shipping_method']); + self::assertEquals($methodCode, $shippingAddress['selected_shipping_method']['method_code']); + + self::assertArrayHasKey('carrier_title', $shippingAddress['selected_shipping_method']); + self::assertEquals(self::CARRIER_TITLE, $shippingAddress['selected_shipping_method']['carrier_title']); + + self::assertArrayHasKey('method_title', $shippingAddress['selected_shipping_method']); + } + + /** + * @return array + */ + public function dataProviderShippingMethodsBasedOnCanadaAddress(): array + { + return [ + 'Canada Standard' => ['STD', 'Canada Standard'], + 'Worldwide Express' => ['XPR', 'Worldwide Express'], + 'Worldwide Express Saver' => ['WXS', 'Worldwide Express Saver'], + 'Worldwide Express Plus' => ['XDM', 'Worldwide Express Plus'], + 'Worldwide Expedited' => ['XPD', 'Worldwide Expedited'], + ]; + } + + /** + * Generates query for setting the specified shipping method on cart + * + * @param string $maskedQuoteId + * @param string $carrierCode + * @param string $methodCode + * @return string + */ + private function getQuery( + string $maskedQuoteId, + string $carrierCode, + string $methodCode + ): string { + return <<<QUERY +mutation { + setShippingMethodsOnCart(input: { + cart_id: "$maskedQuoteId" + shipping_methods: [ + { + carrier_code: "$carrierCode" + method_code: "$methodCode" + } + ] + }) { + cart { + shipping_addresses { + selected_shipping_method { + carrier_code + method_code + carrier_title + method_title + } + } + } + } +} +QUERY; + } + + /** + * Sends a GraphQL request with using a bearer token + * + * @param string $query + * @return array + * @throws \Magento\Framework\Exception\AuthenticationException + */ + private function sendRequestWithToken(string $query): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken('customer@example.com', 'password'); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + + return $this->graphQlMutation($query, [], '', $headerMap); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/UrlRewrite/UrlResolverTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/UrlRewrite/UrlResolverTest.php index 0bd24ee7bc88..370121a1dad7 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/UrlRewrite/UrlResolverTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/UrlRewrite/UrlResolverTest.php @@ -12,6 +12,9 @@ use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\GraphQlAbstract; use Magento\UrlRewrite\Model\UrlFinderInterface; +use Magento\Cms\Helper\Page as PageHelper; +use Magento\Store\Model\ScopeInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; /** * Test the GraphQL endpoint's URLResolver query to verify canonical URL's are correctly returned. @@ -28,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 */ @@ -57,7 +60,7 @@ public function testProductUrlResolver() urlResolver(url:"{$urlPath}") { id - canonical_url + relative_url type } } @@ -65,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 */ @@ -101,7 +104,7 @@ public function testProductUrlWithCanonicalUrlInput() urlResolver(url:"{$canonicalPath}") { id - canonical_url + relative_url type } } @@ -109,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']); } @@ -144,7 +147,7 @@ public function testCategoryUrlResolver() urlResolver(url:"{$urlPath2}") { id - canonical_url + relative_url type } } @@ -152,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']); } @@ -180,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']); } @@ -223,7 +226,7 @@ public function testProductUrlRewriteResolver() urlResolver(url:"{$urlPath}") { id - canonical_url + relative_url type } } @@ -231,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']); } @@ -263,7 +266,7 @@ public function testInvalidUrlResolverInput() urlResolver(url:"{$urlPath}") { id - canonical_url + relative_url type } } @@ -304,7 +307,7 @@ public function testCategoryUrlWithLeadingSlash() urlResolver(url:"/{$urlPath}") { id - canonical_url + relative_url type } } @@ -312,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']); } @@ -321,22 +324,35 @@ public function testCategoryUrlWithLeadingSlash() */ public function testResolveSlash() { + /** @var \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfigInterface */ + $scopeConfigInterface = $this->objectManager->get(ScopeConfigInterface::class); + $homePageIdentifier = $scopeConfigInterface->getValue( + PageHelper::XML_PATH_HOME_PAGE, + ScopeInterface::SCOPE_STORE + ); + /** @var \Magento\Cms\Model\Page $page */ + $page = $this->objectManager->get(\Magento\Cms\Model\Page::class); + $page->load($homePageIdentifier); + $homePageId = $page->getId(); + /** @var \Magento\CmsUrlRewrite\Model\CmsPageUrlPathGenerator $urlPathGenerator */ + $urlPathGenerator = $this->objectManager->get(\Magento\CmsUrlRewrite\Model\CmsPageUrlPathGenerator::class); + /** @param \Magento\Cms\Api\Data\PageInterface $page */ + $targetPath = $urlPathGenerator->getCanonicalUrlPath($page); $query = <<<QUERY { urlResolver(url:"/") { id - canonical_url + relative_url type } } QUERY; $response = $this->graphQlQuery($query); - $this->assertArrayHasKey('urlResolver', $response); - $this->assertEquals(2, $response['urlResolver']['id']); - $this->assertEquals('cms/page/view/page_id/2', $response['urlResolver']['canonical_url']); + $this->assertEquals($homePageId, $response['urlResolver']['id']); + $this->assertEquals($targetPath, $response['urlResolver']['relative_url']); $this->assertEquals('CMS_PAGE', $response['urlResolver']['type']); } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/VariablesSupportQueryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/VariablesSupportQueryTest.php new file mode 100644 index 000000000000..7448b165fc23 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/VariablesSupportQueryTest.php @@ -0,0 +1,81 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl; + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Catalog\Api\ProductRepositoryInterface; + +class VariablesSupportQueryTest extends GraphQlAbstract +{ + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + protected function setUp() + { + $this->productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/products_list.php + */ + public function testQueryObjectVariablesSupport() + { + $productSku = 'simple-249'; + $minPrice = 153; + + $query + = <<<'QUERY' +query GetProductsQuery($pageSize: Int, $filterInput: ProductFilterInput, $priceSort: SortEnum) { + products( + pageSize: $pageSize + filter: $filterInput + sort: {price: $priceSort} + ) { + items { + sku + price { + minimalPrice { + amount { + value + currency + } + } + } + } + } +} +QUERY; + + $variables = [ + 'pageSize' => 1, + 'priceSort' => 'ASC', + 'filterInput' => [ + 'min_price' => [ + 'gt' => 150, + ], + ], + ]; + + $response = $this->graphQlQuery($query, $variables); + /** @var \Magento\Catalog\Model\Product $product */ + $product = $this->productRepository->get($productSku, false, null, true); + + self::assertArrayHasKey('products', $response); + self::assertArrayHasKey('items', $response['products']); + self::assertEquals(1, count($response['products']['items'])); + self::assertArrayHasKey(0, $response['products']['items']); + self::assertEquals($product->getSku(), $response['products']['items'][0]['sku']); + self::assertEquals( + $minPrice, + $response['products']['items'][0]['price']['minimalPrice']['amount']['value'] + ); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Vault/CustomerPaymentTokensTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Vault/CustomerPaymentTokensTest.php new file mode 100644 index 000000000000..45c82906d255 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Vault/CustomerPaymentTokensTest.php @@ -0,0 +1,214 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Vault; + +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Vault\Model\PaymentTokenManagement; +use Magento\Vault\Model\ResourceModel\PaymentToken as TokenResource; +use Magento\Vault\Model\ResourceModel\PaymentToken\CollectionFactory; + +/** + * Tests for customer payment tokens + */ +class CustomerPaymentTokensTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var PaymentTokenManagement + */ + private $paymentTokenManagement; + + /** + * @var CollectionFactory + */ + private $tokenCollectionFactory; + + /** + * @var TokenResource + */ + private $tokenResource; + + protected function setUp() + { + parent::setUp(); + + $this->customerTokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); + $this->paymentTokenManagement = Bootstrap::getObjectManager()->get(PaymentTokenManagement::class); + $this->tokenResource = Bootstrap::getObjectManager()->get(TokenResource::class); + $this->tokenCollectionFactory = Bootstrap::getObjectManager()->get(CollectionFactory::class); + } + + protected function tearDown() + { + parent::tearDown(); + + $collection = $this->tokenCollectionFactory->create(); + $collection->addFieldToFilter('customer_id', ['eq' => 1]); + + foreach ($collection->getItems() as $token) { + // Using the resource directly to delete. Deleting from the repository only makes token inactive + $this->tokenResource->delete($token); + } + } + + /** + * @magentoApiDataFixture Magento/Vault/_files/payment_tokens.php + */ + public function testGetCustomerPaymentTokens() + { + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + + $query = <<<QUERY +query { + customerPaymentTokens { + items { + public_hash + details + payment_method_code + type + } + } +} +QUERY; + $response = $this->graphQlQuery($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + + $this->assertEquals(2, count($response['customerPaymentTokens']['items'])); + $this->assertArrayHasKey('public_hash', $response['customerPaymentTokens']['items'][0]); + $this->assertArrayHasKey('details', $response['customerPaymentTokens']['items'][0]); + $this->assertArrayHasKey('payment_method_code', $response['customerPaymentTokens']['items'][0]); + $this->assertArrayHasKey('type', $response['customerPaymentTokens']['items'][0]); + // Validate gateway token is NOT returned + $this->assertArrayNotHasKey('gateway_token', $response['customerPaymentTokens']['items'][0]); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage GraphQL response contains errors: The current customer isn't authorized. + */ + public function testGetCustomerPaymentTokensIfUserIsNotAuthorized() + { + $query = <<<QUERY +query { + customerPaymentTokens { + items { + public_hash + details + payment_method_code + type + } + } +} +QUERY; + $this->graphQlQuery($query); + } + + /** + * @magentoApiDataFixture Magento/Vault/_files/payment_tokens.php + */ + public function testDeletePaymentToken() + { + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $tokens = $this->paymentTokenManagement->getVisibleAvailableTokens(1); + $token = current($tokens); + $publicHash = $token->getPublicHash(); + + $query = <<<QUERY +mutation { + deletePaymentToken( + public_hash: "$publicHash" + ) { + result + customerPaymentTokens { + items { + public_hash + details + payment_method_code + type + } + } + } +} +QUERY; + $response = $this->graphQlMutation( + $query, + [], + '', + $this->getCustomerAuthHeaders($currentEmail, $currentPassword) + ); + + $this->assertTrue($response['deletePaymentToken']['result']); + $this->assertEquals(1, count($response['deletePaymentToken']['customerPaymentTokens']['items'])); + + $token = $response['deletePaymentToken']['customerPaymentTokens']['items'][0]; + $this->assertArrayHasKey('public_hash', $token); + $this->assertArrayHasKey('details', $token); + $this->assertArrayHasKey('payment_method_code', $token); + $this->assertArrayHasKey('type', $token); + // Validate gateway token is NOT returned + $this->assertArrayNotHasKey('gateway_token', $token); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage GraphQL response contains errors: The current customer isn't authorized. + */ + public function testDeletePaymentTokenIfUserIsNotAuthorized() + { + $query = <<<QUERY +mutation { + deletePaymentToken( + public_hash: "ksdfk392ks" + ) { + result + } +} +QUERY; + $this->graphQlMutation($query, [], ''); + } + + /** + * @magentoApiDataFixture Magento/Vault/_files/payment_tokens.php + * @expectedException \Exception + * @expectedExceptionMessage GraphQL response contains errors: Could not find a token using public hash: ksdfk392ks + */ + public function testDeletePaymentTokenInvalidPublicHash() + { + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + + $query = <<<QUERY +mutation { + deletePaymentToken( + public_hash: "ksdfk392ks" + ) { + result + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + } + + /** + * @param string $email + * @param string $password + * @return array + */ + private function getCustomerAuthHeaders(string $email, string $password): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($email, $password); + return ['Authorization' => 'Bearer ' . $customerToken]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/WishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/WishlistTest.php new file mode 100644 index 000000000000..4aac5d944593 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/WishlistTest.php @@ -0,0 +1,137 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Wishlist; + +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Wishlist\Model\Item; +use Magento\Wishlist\Model\ResourceModel\Wishlist as WishlistResourceModel; +use Magento\Wishlist\Model\WishlistFactory; + +class WishlistTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var WishlistFactory + */ + private $wishlistFactory; + + /** + * @var WishlistResourceModel + */ + private $wishlistResource; + + protected function setUp() + { + $this->customerTokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); + $this->wishlistFactory = Bootstrap::getObjectManager()->get(WishlistFactory::class); + $this->wishlistResource = Bootstrap::getObjectManager()->get(WishlistResourceModel::class); + } + + /** + * @magentoApiDataFixture Magento/Wishlist/_files/wishlist.php + */ + public function testGetCustomerWishlist(): void + { + /** @var \Magento\Wishlist\Model\Wishlist $wishlist */ + $wishlist = $this->wishlistFactory->create(); + $this->wishlistResource->load($wishlist, 1, 'customer_id'); + + /** @var Item $wishlistItem */ + $wishlistItem = $wishlist->getItemCollection()->getFirstItem(); + $wishlistItemProduct = $wishlistItem->getProduct(); + $query = + <<<QUERY +{ + wishlist { + items_count + name + sharing_code + updated_at + items { + id + qty + description + added_at + product { + sku + name + } + } + } +} +QUERY; + + $response = $this->graphQlQuery( + $query, + [], + '', + $this->getCustomerAuthHeaders('customer@example.com', 'password') + ); + + $this->assertEquals($wishlist->getItemsCount(), $response['wishlist']['items_count']); + $this->assertEquals($wishlist->getName(), $response['wishlist']['name']); + $this->assertEquals($wishlist->getSharingCode(), $response['wishlist']['sharing_code']); + $this->assertEquals($wishlist->getUpdatedAt(), $response['wishlist']['updated_at']); + + $this->assertEquals($wishlistItem->getId(), $response['wishlist']['items'][0]['id']); + $this->assertEquals($wishlistItem->getData('qty'), $response['wishlist']['items'][0]['qty']); + $this->assertEquals($wishlistItem->getDescription(), $response['wishlist']['items'][0]['description']); + $this->assertEquals($wishlistItem->getAddedAt(), $response['wishlist']['items'][0]['added_at']); + + $this->assertEquals($wishlistItemProduct->getSku(), $response['wishlist']['items'][0]['product']['sku']); + $this->assertEquals($wishlistItemProduct->getName(), $response['wishlist']['items'][0]['product']['name']); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage The current user cannot perform operations on wishlist + */ + public function testGetGuestWishlist() + { + $query = + <<<QUERY +{ + wishlist { + items_count + name + sharing_code + updated_at + items { + id + qty + description + added_at + product { + sku + name + } + } + } +} +QUERY; + $this->graphQlQuery($query); + } + + /** + * @param string $email + * @param string $password + * @return array + * @throws \Magento\Framework\Exception\AuthenticationException + */ + private function getCustomerAuthHeaders(string $email, string $password): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($email, $password); + return ['Authorization' => 'Bearer ' . $customerToken]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GroupedProduct/Api/CartItemRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/GroupedProduct/Api/CartItemRepositoryTest.php new file mode 100644 index 000000000000..602493481449 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GroupedProduct/Api/CartItemRepositoryTest.php @@ -0,0 +1,68 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\GroupedProduct\Api; + +use Magento\Catalog\Model\CustomOptions\CustomOptionProcessor; +use Magento\Framework\Webapi\Rest\Request; +use Magento\Quote\Model\Quote; +use Magento\TestFramework\TestCase\WebapiAbstract; + +class CartItemRepositoryTest extends WebapiAbstract +{ + const SERVICE_VERSION = 'V1'; + const SERVICE_NAME = 'quoteCartItemRepositoryV1'; + const RESOURCE_PATH = '/V1/carts/'; + + /** + * @var \Magento\TestFramework\ObjectManager + */ + protected $objectManager; + + protected function setUp() + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_address_saved.php + * @magentoApiDataFixture Magento/GroupedProduct/_files/product_grouped_with_simple_out_of_stock.php + */ + public function testAddGroupedProductToCartThatHasAnOutOfStockItemInTheGroup() + { + $this->_markTestAsRestOnly(); + + /** @var \Magento\Catalog\Model\Product $product */ + $product = $this->objectManager->create(\Magento\Catalog\Model\Product::class)->load('100000003'); + $productSku = $product->getSku(); + /** @var Quote $quote */ + $quote = $this->objectManager->create(Quote::class); + $quote->load('test_order_1', 'reserved_order_id'); + $cartId = $quote->getId(); + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH . $cartId . '/items', + 'httpMethod' => Request::HTTP_METHOD_POST, + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'Save', + ], + ]; + + $requestData = [ + 'cartItem' => [ + 'sku' => $productSku, + 'qty' => 1, + 'quote_id' => $cartId, + ], + ]; + $this->_webApiCall($serviceInfo, $requestData); + $this->assertTrue($quote->hasProductId('100000001')); + $this->assertFalse($quote->hasProductId('100000002')); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartTotalRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartTotalRepositoryTest.php index 609ae1cfe094..a3ded4f5f125 100644 --- a/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartTotalRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartTotalRepositoryTest.php @@ -12,6 +12,8 @@ use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\WebapiAbstract; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Address; class CartTotalRepositoryTest extends WebapiAbstract { @@ -54,36 +56,11 @@ public function testGetTotals() /** @var \Magento\Quote\Model\Quote\Address $shippingAddress */ $shippingAddress = $quote->getShippingAddress(); - $data = [ - Totals::KEY_GRAND_TOTAL => $quote->getGrandTotal(), - Totals::KEY_BASE_GRAND_TOTAL => $quote->getBaseGrandTotal(), - Totals::KEY_SUBTOTAL => $quote->getSubtotal(), - Totals::KEY_BASE_SUBTOTAL => $quote->getBaseSubtotal(), - Totals::KEY_DISCOUNT_AMOUNT => $shippingAddress->getDiscountAmount(), - Totals::KEY_BASE_DISCOUNT_AMOUNT => $shippingAddress->getBaseDiscountAmount(), - Totals::KEY_SUBTOTAL_WITH_DISCOUNT => $quote->getSubtotalWithDiscount(), - Totals::KEY_BASE_SUBTOTAL_WITH_DISCOUNT => $quote->getBaseSubtotalWithDiscount(), - Totals::KEY_SHIPPING_AMOUNT => $shippingAddress->getShippingAmount(), - Totals::KEY_BASE_SHIPPING_AMOUNT => $shippingAddress->getBaseShippingAmount(), - Totals::KEY_SHIPPING_DISCOUNT_AMOUNT => $shippingAddress->getShippingDiscountAmount(), - Totals::KEY_BASE_SHIPPING_DISCOUNT_AMOUNT => $shippingAddress->getBaseShippingDiscountAmount(), - Totals::KEY_TAX_AMOUNT => $shippingAddress->getTaxAmount(), - Totals::KEY_BASE_TAX_AMOUNT => $shippingAddress->getBaseTaxAmount(), - Totals::KEY_SHIPPING_TAX_AMOUNT => $shippingAddress->getShippingTaxAmount(), - Totals::KEY_BASE_SHIPPING_TAX_AMOUNT => $shippingAddress->getBaseShippingTaxAmount(), - Totals::KEY_SUBTOTAL_INCL_TAX => $shippingAddress->getSubtotalInclTax(), - Totals::KEY_BASE_SUBTOTAL_INCL_TAX => $shippingAddress->getBaseSubtotalTotalInclTax(), - Totals::KEY_SHIPPING_INCL_TAX => $shippingAddress->getShippingInclTax(), - Totals::KEY_BASE_SHIPPING_INCL_TAX => $shippingAddress->getBaseShippingInclTax(), - Totals::KEY_BASE_CURRENCY_CODE => $quote->getBaseCurrencyCode(), - Totals::KEY_QUOTE_CURRENCY_CODE => $quote->getQuoteCurrencyCode(), - Totals::KEY_ITEMS_QTY => $quote->getItemsQty(), - Totals::KEY_ITEMS => [$this->getQuoteItemTotalsData($quote)], - ]; + $data = $this->getData($quote, $shippingAddress); + $data = $this->formatTotalsData($data); $requestData = ['cartId' => $cartId]; - $data = $this->formatTotalsData($data); $actual = $this->_webApiCall($this->getServiceInfoForTotalsService($cartId), $requestData); unset($actual['items'][0]['options']); unset($actual['weee_tax_applied_amount']); @@ -162,22 +139,22 @@ protected function getQuoteItemTotalsData(\Magento\Quote\Model\Quote $quote) $item = array_shift($items); return [ ItemTotals::KEY_ITEM_ID => $item->getItemId(), - ItemTotals::KEY_PRICE => intval($item->getPrice()), - ItemTotals::KEY_BASE_PRICE => intval($item->getBasePrice()), + ItemTotals::KEY_PRICE => (int)$item->getPrice(), + ItemTotals::KEY_BASE_PRICE => (int)$item->getBasePrice(), ItemTotals::KEY_QTY => $item->getQty(), - ItemTotals::KEY_ROW_TOTAL => intval($item->getRowTotal()), - ItemTotals::KEY_BASE_ROW_TOTAL => intval($item->getBaseRowTotal()), - ItemTotals::KEY_ROW_TOTAL_WITH_DISCOUNT => intval($item->getRowTotalWithDiscount()), - ItemTotals::KEY_TAX_AMOUNT => intval($item->getTaxAmount()), - ItemTotals::KEY_BASE_TAX_AMOUNT => intval($item->getBaseTaxAmount()), - ItemTotals::KEY_TAX_PERCENT => intval($item->getTaxPercent()), - ItemTotals::KEY_DISCOUNT_AMOUNT => intval($item->getDiscountAmount()), - ItemTotals::KEY_BASE_DISCOUNT_AMOUNT => intval($item->getBaseDiscountAmount()), - ItemTotals::KEY_DISCOUNT_PERCENT => intval($item->getDiscountPercent()), - ItemTotals::KEY_PRICE_INCL_TAX => intval($item->getPriceInclTax()), - ItemTotals::KEY_BASE_PRICE_INCL_TAX => intval($item->getBasePriceInclTax()), - ItemTotals::KEY_ROW_TOTAL_INCL_TAX => intval($item->getRowTotalInclTax()), - ItemTotals::KEY_BASE_ROW_TOTAL_INCL_TAX => intval($item->getBaseRowTotalInclTax()), + ItemTotals::KEY_ROW_TOTAL => (int)$item->getRowTotal(), + ItemTotals::KEY_BASE_ROW_TOTAL => (int)$item->getBaseRowTotal(), + ItemTotals::KEY_ROW_TOTAL_WITH_DISCOUNT => (int)$item->getRowTotalWithDiscount(), + ItemTotals::KEY_TAX_AMOUNT => (int)$item->getTaxAmount(), + ItemTotals::KEY_BASE_TAX_AMOUNT => (int)$item->getBaseTaxAmount(), + ItemTotals::KEY_TAX_PERCENT => (int)$item->getTaxPercent(), + ItemTotals::KEY_DISCOUNT_AMOUNT => (int)$item->getDiscountAmount(), + ItemTotals::KEY_BASE_DISCOUNT_AMOUNT => (int)$item->getBaseDiscountAmount(), + ItemTotals::KEY_DISCOUNT_PERCENT => (int)$item->getDiscountPercent(), + ItemTotals::KEY_PRICE_INCL_TAX => (int)$item->getPriceInclTax(), + ItemTotals::KEY_BASE_PRICE_INCL_TAX => (int)$item->getBasePriceInclTax(), + ItemTotals::KEY_ROW_TOTAL_INCL_TAX => (int)$item->getRowTotalInclTax(), + ItemTotals::KEY_BASE_ROW_TOTAL_INCL_TAX => (int)$item->getBaseRowTotalInclTax(), ItemTotals::KEY_WEEE_TAX_APPLIED_AMOUNT => $item->getWeeeTaxAppliedAmount(), ItemTotals::KEY_WEEE_TAX_APPLIED => $item->getWeeeTaxApplied(), ItemTotals::KEY_NAME => $item->getName(), @@ -213,7 +190,32 @@ public function testGetMyTotals() /** @var \Magento\Quote\Model\Quote\Address $shippingAddress */ $shippingAddress = $quote->getShippingAddress(); - $data = [ + $data = $this->getData($quote, $shippingAddress); + $data = $this->formatTotalsData($data); + + $actual = $this->_webApiCall($serviceInfo); + unset($actual['items'][0]['options']); + unset($actual['weee_tax_applied_amount']); + + /** TODO: cover total segments with separate test */ + unset($actual['total_segments']); + if (array_key_exists('extension_attributes', $actual)) { + unset($actual['extension_attributes']); + } + $this->assertEquals($data, $actual); + } + + /** + * Get expected data. + * + * @param Quote $quote + * @param Address $shippingAddress + * + * @return array + */ + private function getData(Quote $quote, Address $shippingAddress) : array + { + return [ Totals::KEY_GRAND_TOTAL => $quote->getGrandTotal(), Totals::KEY_BASE_GRAND_TOTAL => $quote->getBaseGrandTotal(), Totals::KEY_SUBTOTAL => $quote->getSubtotal(), @@ -239,17 +241,5 @@ public function testGetMyTotals() Totals::KEY_ITEMS_QTY => $quote->getItemsQty(), Totals::KEY_ITEMS => [$this->getQuoteItemTotalsData($quote)], ]; - - $data = $this->formatTotalsData($data); - $actual = $this->_webApiCall($serviceInfo); - unset($actual['items'][0]['options']); - unset($actual['weee_tax_applied_amount']); - - /** TODO: cover total segments with separate test */ - unset($actual['total_segments']); - if (array_key_exists('extension_attributes', $actual)) { - unset($actual['extension_attributes']); - } - $this->assertEquals($data, $actual); } } diff --git a/dev/tests/api-functional/testsuite/Magento/Quote/Api/CouponManagementTest.php b/dev/tests/api-functional/testsuite/Magento/Quote/Api/CouponManagementTest.php index 1aee493d8e0c..1fb8fc43b0db 100644 --- a/dev/tests/api-functional/testsuite/Magento/Quote/Api/CouponManagementTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Quote/Api/CouponManagementTest.php @@ -9,6 +9,9 @@ use Magento\TestFramework\TestCase\WebapiAbstract; +/** + * Coupon management service tests + */ class CouponManagementTest extends WebapiAbstract { const SERVICE_VERSION = 'V1'; @@ -93,7 +96,7 @@ public function testSetCouponThrowsExceptionIfCouponDoesNotExist() $serviceInfo = [ 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . $cartId . '/coupons/' . $couponCode, + 'resourcePath' => self::RESOURCE_PATH . $cartId . '/coupons/' . urlencode($couponCode), 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, ], 'soap' => [ @@ -129,7 +132,7 @@ public function testSetCouponSuccess() $couponCode = $salesRule->getPrimaryCoupon()->getCode(); $serviceInfo = [ 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . $cartId . '/coupons/' . $couponCode, + 'resourcePath' => self::RESOURCE_PATH . $cartId . '/coupons/' . urlencode($couponCode), 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, ], 'soap' => [ @@ -232,7 +235,7 @@ public function testSetMyCouponThrowsExceptionIfCouponDoesNotExist() $serviceInfo = [ 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . 'mine/coupons/' . $couponCode, + 'resourcePath' => self::RESOURCE_PATH . 'mine/coupons/' . urlencode($couponCode), 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, 'token' => $token, ], @@ -280,7 +283,7 @@ public function testSetMyCouponSuccess() $serviceInfo = [ 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . 'mine/coupons/' . $couponCode, + 'resourcePath' => self::RESOURCE_PATH . 'mine/coupons/' . urlencode($couponCode), 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, 'token' => $token, ], diff --git a/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartTotalRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartTotalRepositoryTest.php index 7ad0e62f29dc..28195cca679f 100644 --- a/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartTotalRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartTotalRepositoryTest.php @@ -63,14 +63,14 @@ public function testGetTotals() $shippingAddress = $quote->getShippingAddress(); $data = [ - Totals::KEY_BASE_GRAND_TOTAL => $quote->getBaseGrandTotal(), Totals::KEY_GRAND_TOTAL => $quote->getGrandTotal(), - Totals::KEY_BASE_SUBTOTAL => $quote->getBaseSubtotal(), + Totals::KEY_BASE_GRAND_TOTAL => $quote->getBaseGrandTotal(), Totals::KEY_SUBTOTAL => $quote->getSubtotal(), - Totals::KEY_BASE_SUBTOTAL_WITH_DISCOUNT => $quote->getBaseSubtotalWithDiscount(), - Totals::KEY_SUBTOTAL_WITH_DISCOUNT => $quote->getSubtotalWithDiscount(), + Totals::KEY_BASE_SUBTOTAL => $quote->getBaseSubtotal(), Totals::KEY_DISCOUNT_AMOUNT => $shippingAddress->getDiscountAmount(), Totals::KEY_BASE_DISCOUNT_AMOUNT => $shippingAddress->getBaseDiscountAmount(), + Totals::KEY_SUBTOTAL_WITH_DISCOUNT => $quote->getSubtotalWithDiscount(), + Totals::KEY_BASE_SUBTOTAL_WITH_DISCOUNT => $quote->getBaseSubtotalWithDiscount(), Totals::KEY_SHIPPING_AMOUNT => $shippingAddress->getShippingAmount(), Totals::KEY_BASE_SHIPPING_AMOUNT => $shippingAddress->getBaseShippingAmount(), Totals::KEY_SHIPPING_DISCOUNT_AMOUNT => $shippingAddress->getShippingDiscountAmount(), @@ -94,6 +94,7 @@ public function testGetTotals() $data = $this->formatTotalsData($data); $actual = $this->_webApiCall($this->getServiceInfoForTotalsService($cartId), $requestData); + $actual = $this->formatTotalsData($actual); unset($actual['items'][0]['options']); unset($actual['weee_tax_applied_amount']); diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderGetTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderGetTest.php index 7adde3c11ac6..db96728e206b 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderGetTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderGetTest.php @@ -3,8 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Service\V1; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Webapi\Rest\Request; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Model\Order; +use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\WebapiAbstract; class OrderGetTest extends WebapiAbstract @@ -18,19 +25,24 @@ class OrderGetTest extends WebapiAbstract const ORDER_INCREMENT_ID = '100000001'; /** - * @var \Magento\Framework\ObjectManagerInterface + * @var ObjectManagerInterface */ - protected $objectManager; + private $objectManager; - protected function setUp() + /** + * @inheritdoc + */ + protected function setUp(): void { - $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->objectManager = Bootstrap::getObjectManager(); } /** + * Checks order attributes. + * * @magentoApiDataFixture Magento/Sales/_files/order.php */ - public function testOrderGet() + public function testOrderGet(): void { $expectedOrderData = [ 'base_subtotal' => '100.0000', @@ -67,36 +79,21 @@ public function testOrderGet() 'region' => 'CA' ]; - /** @var \Magento\Sales\Model\Order $order */ - $order = $this->objectManager->create(\Magento\Sales\Model\Order::class); - $order->loadByIncrementId(self::ORDER_INCREMENT_ID); - - $serviceInfo = [ - 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . '/' . $order->getId(), - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, - ], - 'soap' => [ - 'service' => self::SERVICE_READ_NAME, - 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => self::SERVICE_READ_NAME . 'get', - ], - ]; - $result = $this->_webApiCall($serviceInfo, ['id' => $order->getId()]); + $result = $this->makeServiceCall(self::ORDER_INCREMENT_ID); foreach ($expectedOrderData as $field => $value) { - $this->assertArrayHasKey($field, $result); - $this->assertEquals($value, $result[$field]); + self::assertArrayHasKey($field, $result); + self::assertEquals($value, $result[$field]); } - $this->assertArrayHasKey('payment', $result); + self::assertArrayHasKey('payment', $result); foreach ($expectedPayments as $field => $value) { - $this->assertEquals($value, $result['payment'][$field]); + self::assertEquals($value, $result['payment'][$field]); } - $this->assertArrayHasKey('billing_address', $result); + self::assertArrayHasKey('billing_address', $result); foreach ($expectedBillingAddressNotEmpty as $field) { - $this->assertArrayHasKey($field, $result['billing_address']); + self::assertArrayHasKey($field, $result['billing_address']); } self::assertArrayHasKey('extension_attributes', $result); @@ -112,7 +109,112 @@ public function testOrderGet() //check that nullable fields were marked as optional and were not sent foreach ($result as $value) { - $this->assertNotNull($value); + self::assertNotNull($value); + } + } + + /** + * Checks order extension attributes. + * + * @magentoApiDataFixture Magento/Sales/_files/order_with_tax.php + */ + public function testOrderGetExtensionAttributes(): void + { + $expectedTax = [ + 'code' => 'US-NY-*-Rate 1', + 'type' => 'shipping' + ]; + + $result = $this->makeServiceCall(self::ORDER_INCREMENT_ID); + + $appliedTaxes = $result['extension_attributes']['applied_taxes']; + self::assertEquals($expectedTax['code'], $appliedTaxes[0]['code']); + $appliedTaxes = $result['extension_attributes']['item_applied_taxes']; + self::assertEquals($expectedTax['type'], $appliedTaxes[0]['type']); + self::assertNotEmpty($appliedTaxes[0]['applied_taxes']); + self::assertEquals(true, $result['extension_attributes']['converting_from_quote']); + self::assertArrayHasKey('payment_additional_info', $result['extension_attributes']); + self::assertNotEmpty($result['extension_attributes']['payment_additional_info']); + } + + /** + * Checks if the order contains product option attributes. + * + * @magentoApiDataFixture Magento/Sales/_files/order_with_bundle.php + */ + public function testGetOrderWithProductOption(): void + { + $expected = [ + 'extension_attributes' => [ + 'bundle_options' => [ + [ + 'option_id' => 1, + 'option_selections' => [1], + 'option_qty' => 1 + ] + ] + ] + ]; + $result = $this->makeServiceCall(self::ORDER_INCREMENT_ID); + + $bundleProduct = $this->getBundleProduct($result['items']); + self::assertNotEmpty($bundleProduct, '"Bundle Product" should not be empty.'); + self::assertNotEmpty($bundleProduct['product_option'], '"Product Option" should not be empty.'); + self::assertEquals($expected, $bundleProduct['product_option']); + } + + /** + * Gets order by increment ID. + * + * @param string $incrementId + * @return OrderInterface + */ + private function getOrder(string $incrementId): OrderInterface + { + /** @var Order $order */ + $order = $this->objectManager->create(Order::class); + $order->loadByIncrementId($incrementId); + + return $order; + } + + /** + * Makes service call. + * + * @param string $incrementId + * @return array + */ + private function makeServiceCall(string $incrementId): array + { + $order = $this->getOrder($incrementId); + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH . '/' . $order->getId(), + 'httpMethod' => Request::HTTP_METHOD_GET, + ], + 'soap' => [ + 'service' => self::SERVICE_READ_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_READ_NAME . 'get', + ], + ]; + return $this->_webApiCall($serviceInfo, ['id' => $order->getId()]); + } + + /** + * Gets a bundle product from the result. + * + * @param array $items + * @return array + */ + private function getBundleProduct(array $items): array + { + foreach ($items as $item) { + if ($item['product_type'] == 'bundle') { + return $item; + } } + + return []; } } diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderInvoiceCreateTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderInvoiceCreateTest.php index 49e05832f2ef..f6194db6d8eb 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderInvoiceCreateTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderInvoiceCreateTest.php @@ -90,4 +90,96 @@ public function testInvoiceCreate() 'Failed asserting that Order status was changed' ); } + + /** + * Tests that MAGETWO-95346 was fixed for bundled products + * + * @expectedException \Exception + * @codingStandardsIgnoreStart + * @expectedExceptionMessageRegExp /Invoice Document Validation Error\(s\):(?:\n|\\n)The invoice can't be created without products. Add products and try again./ + * @codingStandardsIgnoreEnd + * @magentoApiDataFixture Magento/Sales/_files/order_with_bundle.php + */ + public function testOrderWithBundleInvoicedWithInvalidQuantitiesReturnsError() + { + /** @var \Magento\Sales\Model\Order $existingOrder */ + $existingOrder = $this->objectManager->create(\Magento\Sales\Model\Order::class) + ->loadByIncrementId('100000001'); + + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/order/' . $existingOrder->getId() . '/invoice', + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + ], + 'soap' => [ + 'service' => self::SERVICE_READ_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_READ_NAME . 'execute', + ], + ]; + + $requestData = [ + 'orderId' => $existingOrder->getId(), + 'notify' => true, + 'appendComment' => true, + 'items' => [ + [ + 'order_item_id' => -1, + 'qty' => 1 + ] + ], + 'comment' => [ + 'comment' => 'Test offline', + 'isVisibleOnFront' => 1, + ], + ]; + + $this->_webApiCall($serviceInfo, $requestData); + } + + /** + * Tests that MAGETWO-95346 was fixed for configurable products + * + * @expectedException \Exception + * @codingStandardsIgnoreStart + * @expectedExceptionMessageRegExp /Invoice Document Validation Error\(s\):(?:\n|\\n)The invoice can't be created without products. Add products and try again./ + * @codingStandardsIgnoreEnd + * @magentoApiDataFixture Magento/Sales/_files/order_configurable_product.php + */ + public function testOrderWithConfigurableProductInvoicedWithInvalidQuantitiesReturnsError() + { + /** @var \Magento\Sales\Model\Order $existingOrder */ + $existingOrder = $this->objectManager->create(\Magento\Sales\Model\Order::class) + ->loadByIncrementId('100000001'); + + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/order/' . $existingOrder->getId() . '/invoice', + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + ], + 'soap' => [ + 'service' => self::SERVICE_READ_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_READ_NAME . 'execute', + ], + ]; + + $requestData = [ + 'orderId' => $existingOrder->getId(), + 'notify' => true, + 'appendComment' => true, + 'items' => [ + [ + 'order_item_id' => -1, + 'qty' => 1 + ] + ], + 'comment' => [ + 'comment' => 'Test offline', + 'isVisibleOnFront' => 1, + ], + ]; + + $this->_webApiCall($serviceInfo, $requestData); + } } diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderItemGetTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderItemGetTest.php index 3ab93f9aecb9..9ba648c73276 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderItemGetTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderItemGetTest.php @@ -77,4 +77,36 @@ protected function assertOrderItem(\Magento\Sales\Model\Order\Item $orderItem, a $this->assertEquals($orderItem->getBasePrice(), $response['base_price']); $this->assertEquals($orderItem->getRowTotal(), $response['row_total']); } + + /** + * @magentoApiDataFixture Magento/Sales/_files/order_with_discount.php + */ + public function testGetOrderWithDiscount() + { + /** @var \Magento\Sales\Model\Order $order */ + $order = $this->objectManager->create(\Magento\Sales\Model\Order::class); + $order->loadByIncrementId(self::ORDER_INCREMENT_ID); + /** @var \Magento\Sales\Model\Order\Item $orderItem */ + $orderItem = current($order->getItems()); + + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH . '/' . $orderItem->getId(), + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'get', + ], + ]; + + $response = $this->_webApiCall($serviceInfo, ['id' => $orderItem->getId()]); + + $this->assertTrue(is_array($response)); + $this->assertEquals(8.00, $response['row_total']); + $this->assertEquals(8.00, $response['base_row_total']); + $this->assertEquals(9.00, $response['row_total_incl_tax']); + $this->assertEquals(9.00, $response['base_row_total_incl_tax']); + } } diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderListTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderListTest.php index 0500f3185829..506f82eab7ae 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderListTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderListTest.php @@ -33,6 +33,73 @@ protected function setUp() * @magentoApiDataFixture Magento/Sales/_files/order_list.php */ public function testOrderList() + { + $searchData = $this->getSearchData(); + + $requestData = ['searchCriteria' => $searchData]; + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH . '?' . http_build_query($requestData), + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + ], + 'soap' => [ + 'service' => self::SERVICE_READ_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_READ_NAME . 'getList', + ], + ]; + + $result = $this->_webApiCall($serviceInfo, $requestData); + $this->assertArrayHasKey('items', $result); + $this->assertCount(2, $result['items']); + $this->assertArrayHasKey('search_criteria', $result); + $this->assertEquals($searchData, $result['search_criteria']); + $this->assertEquals('100000002', $result['items'][0]['increment_id']); + $this->assertEquals('100000001', $result['items'][1]['increment_id']); + } + + /** + * @magentoApiDataFixture Magento/Sales/_files/order_list_with_tax.php + */ + public function testOrderListExtensionAttributes() + { + $searchData = $this->getSearchData(); + + $requestData = ['searchCriteria' => $searchData]; + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH . '?' . http_build_query($requestData), + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + ], + 'soap' => [ + 'service' => self::SERVICE_READ_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_READ_NAME . 'getList', + ], + ]; + + $result = $this->_webApiCall($serviceInfo, $requestData); + + $expectedTax = [ + 'code' => 'US-NY-*-Rate 1', + 'type' => 'shipping' + ]; + $appliedTaxes = $result['items'][0]['extension_attributes']['applied_taxes']; + $this->assertEquals($expectedTax['code'], $appliedTaxes[0]['code']); + $appliedTaxes = $result['items'][0]['extension_attributes']['item_applied_taxes']; + $this->assertEquals($expectedTax['type'], $appliedTaxes[0]['type']); + $this->assertNotEmpty($appliedTaxes[0]['applied_taxes']); + $this->assertEquals(true, $result['items'][0]['extension_attributes']['converting_from_quote']); + $this->assertArrayHasKey('payment_additional_info', $result['items'][0]['extension_attributes']); + $this->assertNotEmpty($result['items'][0]['extension_attributes']['payment_additional_info']); + } + + /** + * Get search data for request. + * + * @return array + */ + private function getSearchData() : array { /** @var \Magento\Framework\Api\SortOrderBuilder $sortOrderBuilder */ $sortOrderBuilder = $this->objectManager->get( @@ -70,25 +137,6 @@ public function testOrderList() $searchCriteriaBuilder->addSortOrder($sortOrder); $searchData = $searchCriteriaBuilder->create()->__toArray(); - $requestData = ['searchCriteria' => $searchData]; - $serviceInfo = [ - 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . '?' . http_build_query($requestData), - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, - ], - 'soap' => [ - 'service' => self::SERVICE_READ_NAME, - 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => self::SERVICE_READ_NAME . 'getList', - ], - ]; - - $result = $this->_webApiCall($serviceInfo, $requestData); - $this->assertArrayHasKey('items', $result); - $this->assertCount(2, $result['items']); - $this->assertArrayHasKey('search_criteria', $result); - $this->assertEquals($searchData, $result['search_criteria']); - $this->assertEquals('100000002', $result['items'][0]['increment_id']); - $this->assertEquals('100000001', $result['items'][1]['increment_id']); + return $searchData; } } diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderStatusHistoryAddTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderStatusHistoryAddTest.php index c5ecead00ce2..9e3bd4ca4847 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderStatusHistoryAddTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderStatusHistoryAddTest.php @@ -11,6 +11,7 @@ /** * Class OrderCommentAddTest + * * @package Magento\Sales\Service\V1 */ class OrderStatusHistoryAddTest extends WebapiAbstract @@ -48,7 +49,7 @@ public function testOrderCommentAdd() OrderStatusHistoryInterface::CREATED_AT => null, OrderStatusHistoryInterface::PARENT_ID => $order->getId(), OrderStatusHistoryInterface::ENTITY_NAME => null, - OrderStatusHistoryInterface::STATUS => null, + OrderStatusHistoryInterface::STATUS => $order->getStatus(), OrderStatusHistoryInterface::IS_VISIBLE_ON_FRONT => 1, ]; @@ -69,25 +70,27 @@ public function testOrderCommentAdd() //Verification $comments = $order->load($order->getId())->getAllStatusHistory(); + $comment = reset($comments); - $commentData = reset($comments); - foreach ($commentData as $key => $value) { - $this->assertEquals( - $commentData[OrderStatusHistoryInterface::COMMENT], - $statusHistoryComment->getComment() - ); - $this->assertEquals( - $commentData[OrderStatusHistoryInterface::PARENT_ID], - $statusHistoryComment->getParentId() - ); - $this->assertEquals( - $commentData[OrderStatusHistoryInterface::IS_CUSTOMER_NOTIFIED], - $statusHistoryComment->getIsCustomerNotified() - ); - $this->assertEquals( - $commentData[OrderStatusHistoryInterface::IS_VISIBLE_ON_FRONT], - $statusHistoryComment->getIsVisibleOnFront() - ); - } + $this->assertEquals( + $commentData[OrderStatusHistoryInterface::COMMENT], + $comment->getComment() + ); + $this->assertEquals( + $commentData[OrderStatusHistoryInterface::PARENT_ID], + $comment->getParentId() + ); + $this->assertEquals( + $commentData[OrderStatusHistoryInterface::IS_CUSTOMER_NOTIFIED], + $comment->getIsCustomerNotified() + ); + $this->assertEquals( + $commentData[OrderStatusHistoryInterface::IS_VISIBLE_ON_FRONT], + $comment->getIsVisibleOnFront() + ); + $this->assertEquals( + $commentData[OrderStatusHistoryInterface::STATUS], + $comment->getStatus() + ); } } diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/RefundOrderTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/RefundOrderTest.php index 12cbe1ca0e5e..92942d7acc6f 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/RefundOrderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/RefundOrderTest.php @@ -3,8 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Sales\Service\V1; +use Magento\Sales\Model\Order; + /** * API test for creation of Creditmemo for certain Order. */ @@ -86,10 +89,10 @@ public function testShortRequest() 'Failed asserting that proper shipping amount of the Order was refunded' ); - $this->assertNotEquals( - $existingOrder->getStatus(), + $this->assertEquals( + Order::STATE_COMPLETE, $updatedOrder->getStatus(), - 'Failed asserting that order status was changed' + 'Failed asserting that order status has not changed' ); } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { $this->fail('Failed asserting that Creditmemo was created'); @@ -199,6 +202,73 @@ public function testFullRequest() } } + /** + * Test order will keep same(custom) status after partial refund, if state has not been changed. + * + * @magentoApiDataFixture Magento/Sales/_files/order_with_invoice_and_custom_status.php + */ + public function testOrderStatusPartialRefund() + { + /** @var \Magento\Sales\Model\Order $existingOrder */ + $existingOrder = $this->objectManager->create(\Magento\Sales\Model\Order::class) + ->loadByIncrementId('100000001'); + + $items = $this->getOrderItems($existingOrder); + $items[0]['qty'] -= 1; + $result = $this->_webApiCall( + $this->getServiceData($existingOrder), + [ + 'orderId' => $existingOrder->getEntityId(), + 'items' => $items, + ] + ); + + $this->assertNotEmpty( + $result, + 'Failed asserting that the received response is correct' + ); + + /** @var \Magento\Sales\Model\Order $updatedOrder */ + $updatedOrder = $this->objectManager->create(\Magento\Sales\Model\Order::class) + ->loadByIncrementId($existingOrder->getIncrementId()); + + $this->assertSame('custom_processing', $updatedOrder->getStatus()); + $this->assertSame('processing', $updatedOrder->getState()); + } + + /** + * Test order will change custom status after total refund, when state has been changed. + * + * @magentoApiDataFixture Magento/Sales/_files/order_with_invoice_and_custom_status.php + */ + public function testOrderStatusTotalRefund() + { + /** @var \Magento\Sales\Model\Order $existingOrder */ + $existingOrder = $this->objectManager->create(\Magento\Sales\Model\Order::class) + ->loadByIncrementId('100000001'); + + $items = $this->getOrderItems($existingOrder); + $result = $this->_webApiCall( + $this->getServiceData($existingOrder), + [ + 'orderId' => $existingOrder->getEntityId(), + 'items' => $items, + ] + ); + + $this->assertNotEmpty( + $result, + 'Failed asserting that the received response is correct' + ); + + /** @var \Magento\Sales\Model\Order $updatedOrder */ + $updatedOrder = $this->objectManager->create(\Magento\Sales\Model\Order::class) + ->loadByIncrementId($existingOrder->getIncrementId()); + + $this->assertSame('complete', $updatedOrder->getStatus()); + $this->assertSame('complete', $updatedOrder->getState()); + } + /** * Prepares and returns info for API service. * diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipOrderTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipOrderTest.php index 32e33e8ed9bf..e2a156cc6fbf 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipOrderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipOrderTest.php @@ -37,6 +37,7 @@ protected function setUp() */ public function testConfigurableShipOrder() { + $this->markTestIncomplete('https://github.com/magento-engcom/msi/issues/1335'); $productsQuantity = 1; /** @var \Magento\Sales\Model\Order $existingOrder */ @@ -131,6 +132,39 @@ public function testShipOrder() ); } + /** + * Tests that not providing a tracking number produces the correct error. See MAGETWO-95429 + * @expectedException \Exception + * @codingStandardsIgnoreStart + * @expectedExceptionMessageRegExp /Shipment Document Validation Error\(s\):(?:\n|\\n)Please enter a tracking number./ + * @codingStandardsIgnoreEnd + * @magentoApiDataFixture Magento/Sales/_files/order_new.php + */ + public function testShipOrderWithoutTrackingNumberReturnsError() + { + $this->_markTestAsRestOnly('SOAP requires an tracking number to be provided so this case is not possible.'); + + /** @var \Magento\Sales\Model\Order $existingOrder */ + $existingOrder = $this->objectManager->create(\Magento\Sales\Model\Order::class) + ->loadByIncrementId('100000001'); + + $requestData = [ + 'orderId' => $existingOrder->getId(), + 'comment' => [ + 'comment' => 'Test Comment', + 'is_visible_on_front' => 1, + ], + 'tracks' => [ + [ + 'title' => 'Simple shipment track', + 'carrier_code' => 'UPS' + ] + ] + ]; + + $this->_webApiCall($this->getServiceInfo($existingOrder), $requestData); + } + /** * @magentoApiDataFixture Magento/Bundle/_files/order_with_bundle_shipped_separately.php */ diff --git a/dev/tests/api-functional/testsuite/Magento/Search/Api/SearchTest.php b/dev/tests/api-functional/testsuite/Magento/Search/Api/SearchTest.php new file mode 100644 index 000000000000..f6167a06c643 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/Search/Api/SearchTest.php @@ -0,0 +1,110 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Search\Api; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Webapi\Rest\Request; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\WebapiAbstract; + +class SearchTest extends WebapiAbstract +{ + const SERVICE_VERSION = 'V1'; + const SERVICE_NAME = 'searchV1'; + const RESOURCE_PATH = '/V1/search/'; + + /** + * @var ProductInterface + */ + private $product; + + protected function setUp() + { + $productSku = 'simple'; + + $objectManager = Bootstrap::getObjectManager(); + $productRepository = $objectManager->create(ProductRepositoryInterface::class); + $this->product = $productRepository->get($productSku); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testExistingProductSearch() + { + $productName = $this->product->getName(); + + $searchCriteria = $this->buildSearchCriteria($productName); + $serviceInfo = $this->buildServiceInfo($searchCriteria); + + $response = $this->_webApiCall($serviceInfo, $searchCriteria); + + self::assertArrayHasKey('search_criteria', $response); + self::assertArrayHasKey('items', $response); + self::assertGreaterThan(0, count($response['items'])); + self::assertGreaterThan(0, $response['items'][0]['id']); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testNonExistentProductSearch() + { + $searchCriteria = $this->buildSearchCriteria('nonExistentProduct'); + $serviceInfo = $this->buildServiceInfo($searchCriteria); + + $response = $this->_webApiCall($serviceInfo, $searchCriteria); + + self::assertArrayHasKey('search_criteria', $response); + self::assertArrayHasKey('items', $response); + self::assertEquals(0, count($response['items'])); + } + + /** + * @param string $productName + * @return array + */ + private function buildSearchCriteria(string $productName): array + { + return [ + 'searchCriteria' => [ + 'request_name' => 'quick_search_container', + 'filter_groups' => [ + [ + 'filters' => [ + [ + 'field' => 'search_term', + 'value' => $productName, + ] + ] + ] + ] + ] + ]; + } + + /** + * @param array $searchCriteria + * @return array + */ + private function buildServiceInfo(array $searchCriteria): array + { + return [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH . '?' . http_build_query($searchCriteria), + 'httpMethod' => Request::HTTP_METHOD_GET + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'Search' + ] + ]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/Webapi/JoinDirectivesTest.php b/dev/tests/api-functional/testsuite/Magento/Webapi/JoinDirectivesTest.php index 5f967758e21b..8beb14e81be7 100644 --- a/dev/tests/api-functional/testsuite/Magento/Webapi/JoinDirectivesTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Webapi/JoinDirectivesTest.php @@ -6,12 +6,14 @@ namespace Magento\Webapi; +use Magento\Framework\Api\FilterBuilder; use Magento\Framework\Api\SearchCriteriaBuilder; -use Magento\Framework\Api\SortOrderBuilder; use Magento\Framework\Api\SortOrder; -use Magento\Framework\Api\SearchCriteria; -use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\SortOrderBuilder; +/** + * Test join directives. + */ class JoinDirectivesTest extends \Magento\TestFramework\TestCase\WebapiAbstract { /** @@ -44,7 +46,8 @@ protected function setUp() } /** - * Rollback rules + * Rollback rules. + * * @magentoApiDataFixture Magento/SalesRule/_files/rules_rollback.php * @magentoApiDataFixture Magento/Sales/_files/quote.php */ @@ -124,6 +127,49 @@ public function testAutoGeneratedGetList() $this->assertEquals($expectedExtensionAttributes['email'], $testAttribute['email']); } + /** + * Test get list of orders with extension attributes. + * + * @magentoApiDataFixture Magento/Sales/_files/order.php + */ + public function testGetOrdertList() + { + $filter = $this->filterBuilder + ->setField('increment_id') + ->setValue('100000001') + ->setConditionType('eq') + ->create(); + $this->searchBuilder->addFilters([$filter]); + $searchData = $this->searchBuilder->create()->__toArray(); + + $requestData = ['searchCriteria' => $searchData]; + + $restResourcePath = '/V1/orders/'; + $soapService = 'salesOrderRepositoryV1'; + $expectedExtensionAttributes = $this->getExpectedExtensionAttributes(); + + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => $restResourcePath . '?' . http_build_query($requestData), + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + ], + 'soap' => [ + 'service' => $soapService, + 'operation' => $soapService . 'GetList', + ], + ]; + $searchResult = $this->_webApiCall($serviceInfo, $requestData); + + $this->assertArrayHasKey('items', $searchResult); + $itemData = array_pop($searchResult['items']); + $this->assertArrayHasKey('extension_attributes', $itemData); + $this->assertArrayHasKey('order_api_test_attribute', $itemData['extension_attributes']); + $testAttribute = $itemData['extension_attributes']['order_api_test_attribute']; + $this->assertEquals($expectedExtensionAttributes['firstname'], $testAttribute['first_name']); + $this->assertEquals($expectedExtensionAttributes['lastname'], $testAttribute['last_name']); + $this->assertEquals($expectedExtensionAttributes['email'], $testAttribute['email']); + } + /** * Retrieve the admin user's information. * diff --git a/dev/tests/functional/lib/Magento/Mtf/App/State/State1.php b/dev/tests/functional/lib/Magento/Mtf/App/State/State1.php index 60abe18f5a7b..fc54e73ff1ac 100644 --- a/dev/tests/functional/lib/Magento/Mtf/App/State/State1.php +++ b/dev/tests/functional/lib/Magento/Mtf/App/State/State1.php @@ -7,6 +7,7 @@ namespace Magento\Mtf\App\State; use Magento\Mtf\ObjectManager; +use Magento\Mtf\Util\Command\Cli; use Magento\Mtf\Util\Protocol\CurlInterface; use Magento\Mtf\Util\Protocol\CurlTransport; @@ -27,7 +28,7 @@ class State1 extends AbstractState * * @var string */ - protected $config ='admin_session_lifetime_1_hour, wysiwyg_disabled, admin_account_sharing_enable, log_to_file'; + protected $config ='admin_session_lifetime_1_hour, wysiwyg_disabled, admin_account_sharing_enable'; /** * HTTP CURL Adapter. @@ -55,6 +56,7 @@ public function __construct( * Apply set up configuration profile. * * @return void + * @throws \Exception */ public function apply() { @@ -67,6 +69,10 @@ public function apply() ['configData' => $this->config] )->run(); } + + /** @var Cli $cli */ + $cli = $this->objectManager->create(Cli::class); + $cli->execute('setup:config:set', ['--enable-debug-logging=true']); } /** diff --git a/dev/tests/functional/lib/Magento/Mtf/Client/Element/ConditionsElement.php b/dev/tests/functional/lib/Magento/Mtf/Client/Element/ConditionsElement.php index d1fd35130241..03492f7ae1a9 100644 --- a/dev/tests/functional/lib/Magento/Mtf/Client/Element/ConditionsElement.php +++ b/dev/tests/functional/lib/Magento/Mtf/Client/Element/ConditionsElement.php @@ -6,9 +6,9 @@ namespace Magento\Mtf\Client\Element; -use Magento\Mtf\ObjectManager; -use Magento\Mtf\Client\Locator; use Magento\Mtf\Client\ElementInterface; +use Magento\Mtf\Client\Locator; +use Magento\Mtf\ObjectManager; /** * Typified element class for conditions. @@ -135,6 +135,13 @@ class ConditionsElement extends SimpleElement */ protected $chooserGridLocator = 'div[id*=chooser]'; + /** + * Datepicker xpath. + * + * @var string + */ + private $datepicker = './/*[contains(@class,"ui-datepicker-trigger")]'; + /** * Key of last find param. * @@ -189,10 +196,14 @@ class ConditionsElement extends SimpleElement protected $exception; /** - * Set value to conditions. + * Condition option text selector. * - * @param string $value - * @return void + * @var string + */ + private $conditionOptionTextSelector = '//option[normalize-space(text())="%s"]'; + + /** + * @inheritdoc */ public function setValue($value) { @@ -261,7 +272,7 @@ protected function addSingleCondition($condition, ElementInterface $context) $this->addCondition($condition['type'], $context); $createdCondition = $context->find($this->created, Locator::SELECTOR_XPATH); $this->waitForCondition($createdCondition); - $this->fillCondition($condition['rules'], $createdCondition); + $this->fillCondition($condition['rules'], $createdCondition, $condition['type']); } /** @@ -278,10 +289,16 @@ protected function addCondition($type, ElementInterface $context) $count = 0; do { - $newCondition->find($this->addNew, Locator::SELECTOR_XPATH)->click(); - try { - $newCondition->find($this->typeNew, Locator::SELECTOR_XPATH, 'select')->setValue($type); + $specificType = $newCondition->find( + sprintf($this->conditionOptionTextSelector, $type), + Locator::SELECTOR_XPATH + )->isPresent(); + $newCondition->find($this->addNew, Locator::SELECTOR_XPATH)->click(); + $condition = $specificType + ? $newCondition->find($this->typeNew, Locator::SELECTOR_XPATH, 'selectcondition') + : $newCondition->find($this->typeNew, Locator::SELECTOR_XPATH, 'select'); + $condition->setValue($type); $isSetType = true; } catch (\PHPUnit_Extensions_Selenium2TestCase_WebDriverException $e) { $isSetType = false; @@ -302,13 +319,14 @@ protected function addCondition($type, ElementInterface $context) * * @param array $rules * @param ElementInterface $element + * @param string|null $type * @return void * @throws \Exception * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ - protected function fillCondition(array $rules, ElementInterface $element) + protected function fillCondition(array $rules, ElementInterface $element, $type = null) { $this->resetKeyParam(); foreach ($rules as $rule) { @@ -329,7 +347,7 @@ protected function fillCondition(array $rules, ElementInterface $element) if ($this->fillGrid($rule, $param)) { $isSet = true; - } elseif ($this->fillSelect($rule, $param)) { + } elseif ($this->fillSelect($rule, $param, $type)) { $isSet = true; } elseif ($this->fillText($rule, $param)) { $isSet = true; @@ -386,11 +404,15 @@ protected function fillGrid($rule, ElementInterface $param) * * @param string $rule * @param ElementInterface $param + * @param string|null $type * @return bool */ - protected function fillSelect($rule, ElementInterface $param) + protected function fillSelect($rule, ElementInterface $param, $type = null) { - $value = $param->find('select', Locator::SELECTOR_TAG_NAME, 'select'); + //Avoid confusion between regions like: "Baja California" and "California". + $value = strpos($type, 'State/Province') === false + ? $param->find('select', Locator::SELECTOR_TAG_NAME, 'select') + : $param->find('select', Locator::SELECTOR_TAG_NAME, 'selectstate'); if ($value->isVisible()) { $value->setValue($rule); $this->click(); @@ -411,7 +433,16 @@ protected function fillText($rule, ElementInterface $param) { $value = $param->find('input', Locator::SELECTOR_TAG_NAME); if ($value->isVisible()) { - $value->setValue($rule); + if (!$value->getAttribute('readonly')) { + $value->setValue($rule); + } else { + $datepicker = $param->find( + $this->datepicker, + Locator::SELECTOR_XPATH, + DatepickerElement::class + ); + $datepicker->setValue($rule); + } $apply = $param->find('.//*[@class="rule-param-apply"]', Locator::SELECTOR_XPATH); if ($apply->isVisible()) { diff --git a/dev/tests/functional/lib/Magento/Mtf/Client/Element/DatepickerElement.php b/dev/tests/functional/lib/Magento/Mtf/Client/Element/DatepickerElement.php index a0e350cb3da4..eb277c2cc43d 100644 --- a/dev/tests/functional/lib/Magento/Mtf/Client/Element/DatepickerElement.php +++ b/dev/tests/functional/lib/Magento/Mtf/Client/Element/DatepickerElement.php @@ -66,13 +66,16 @@ public function setValue($value) $date = $this->parseDate($value); $date[1] = ltrim($date[1], '0'); $this->click(); - $this->find($this->datePickerButton, Locator::SELECTOR_XPATH)->click(); $datapicker = $this->find($this->datePickerBlock, Locator::SELECTOR_XPATH); + $datepickerClose = $datapicker->find($this->datePickerButtonClose, Locator::SELECTOR_XPATH); + if (!$datepickerClose->isVisible()) { + $this->find($this->datePickerButton, Locator::SELECTOR_XPATH)->click(); + } $datapicker->find($this->datePickerYear, Locator::SELECTOR_XPATH, 'select')->setValue($date[2]); $datapicker->find($this->datePickerMonth, Locator::SELECTOR_XPATH, 'select')->setValue($date[0]); $datapicker->find(sprintf($this->datePickerCalendar, $date[1]), Locator::SELECTOR_XPATH)->click(); if ($datapicker->isVisible()) { - $datapicker->find($this->datePickerButtonClose, Locator::SELECTOR_XPATH)->click(); + $datepickerClose->click(); } } diff --git a/dev/tests/functional/lib/Magento/Mtf/Client/Element/SelectconditionElement.php b/dev/tests/functional/lib/Magento/Mtf/Client/Element/SelectconditionElement.php new file mode 100644 index 000000000000..15a799eac518 --- /dev/null +++ b/dev/tests/functional/lib/Magento/Mtf/Client/Element/SelectconditionElement.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Mtf\Client\Element; + +/** + * @inheritdoc + */ +class SelectconditionElement extends SelectElement +{ + /** + * @inheritdoc + */ + protected $optionByValue = './/option[normalize-space(.)=%s]'; +} diff --git a/dev/tests/functional/lib/Magento/Mtf/Client/Element/SelectstateElement.php b/dev/tests/functional/lib/Magento/Mtf/Client/Element/SelectstateElement.php new file mode 100644 index 000000000000..a21353f46c1c --- /dev/null +++ b/dev/tests/functional/lib/Magento/Mtf/Client/Element/SelectstateElement.php @@ -0,0 +1,19 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Mtf\Client\Element; + +/** + * @inheritdoc + */ +class SelectstateElement extends SelectElement +{ + /** + * @inheritdoc + */ + protected $optionByValue = './/option[normalize-space(.)=%s]'; +} diff --git a/dev/tests/functional/lib/Magento/Mtf/Constraint/AbstractAssertForm.php b/dev/tests/functional/lib/Magento/Mtf/Constraint/AbstractAssertForm.php index eb04450d5261..61e5b7649303 100644 --- a/dev/tests/functional/lib/Magento/Mtf/Constraint/AbstractAssertForm.php +++ b/dev/tests/functional/lib/Magento/Mtf/Constraint/AbstractAssertForm.php @@ -53,7 +53,7 @@ protected function verifyData(array $fixtureData, array $formData, $isStrict = f } $formValue = isset($formData[$key]) ? $formData[$key] : null; if (is_numeric($formValue)) { - $formValue = floatval($formValue); + $formValue = (float)$formValue; } if (null === $formValue) { @@ -118,6 +118,7 @@ protected function sortData(array $data) /** * Sort multidimensional array by paths. + * * Pattern path: key/subKey::sortKey. * Example: * $data = [ @@ -150,7 +151,6 @@ protected function sortData(array $data) * * @param array $data * @param string $path - * @param string $path * @return array * @throws \Exception * diff --git a/dev/tests/functional/lib/Magento/Mtf/EntryPoint/EntryPoint.php b/dev/tests/functional/lib/Magento/Mtf/EntryPoint/EntryPoint.php index 745f97b9b43f..836cc486cb0f 100644 --- a/dev/tests/functional/lib/Magento/Mtf/EntryPoint/EntryPoint.php +++ b/dev/tests/functional/lib/Magento/Mtf/EntryPoint/EntryPoint.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Mtf\EntryPoint; @@ -10,8 +11,8 @@ /** * Class EntryPoint - * Application entry point, used to bootstrap and run application * + * Application entry point, used to bootstrap and run application */ class EntryPoint { @@ -36,7 +37,6 @@ class EntryPoint * @param string $rootDir * @param array $parameters * @param ObjectManager $objectManager - * @SuppressWarnings(PHPMD.ExitExpression) */ public function __construct( $rootDir, @@ -51,7 +51,7 @@ public function __construct( /** * Run a Mtf application * - * @param $applicationName + * @param string $applicationName * @param array $arguments * @return mixed * @throws \DomainException diff --git a/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Export.php b/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Export.php index f2ab1501dc2b..1dac1f213920 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 1c05fbaebf62..d7336b51a18e 100644 --- a/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Export/Reader.php +++ b/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Export/Reader.php @@ -73,8 +73,8 @@ private function getFiles() $this->transport->write($this->prepareUrl(), [], CurlInterface::GET); $serializedFiles = $this->transport->read(); $this->transport->close(); - - return unserialize($serializedFiles); + // phpcs:ignore Magento2.Security.InsecureFunction + return unserialize($serializedFiles, ['allowed_classes' => false]); } /** diff --git a/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Log.php b/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Log.php index 8b41924fe0a9..f4e55682857a 100644 --- a/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Log.php +++ b/dev/tests/functional/lib/Magento/Mtf/Util/Command/File/Log.php @@ -45,7 +45,7 @@ public function getFileContent($name) $curl->write($this->prepareUrl($name), [], CurlTransport::GET); $data = $curl->read(); $curl->close(); - + // phpcs:ignore Magento2.Security.InsecureFunction return unserialize($data); } diff --git a/dev/tests/functional/lib/Magento/Mtf/Util/Generate/Fixture/SchemaXml.php b/dev/tests/functional/lib/Magento/Mtf/Util/Generate/Fixture/SchemaXml.php index aa85299deea4..6d1d5b6f4b34 100644 --- a/dev/tests/functional/lib/Magento/Mtf/Util/Generate/Fixture/SchemaXml.php +++ b/dev/tests/functional/lib/Magento/Mtf/Util/Generate/Fixture/SchemaXml.php @@ -145,7 +145,7 @@ protected function generateFixtureXml(array $config) foreach ($fields as $fieldName => $fieldValue) { $field = $this->dom->createElement('field'); $field->setAttribute('name', $fieldName); - $field->setAttribute('is_required', intval($fieldValue['is_required'])); + $field->setAttribute('is_required', (int)$fieldValue['is_required']); $fixture->appendChild($field); } 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 bb82715e4b40..b1c552370835 100644 --- a/dev/tests/functional/lib/Magento/Mtf/Util/Protocol/CurlTransport/BackendDecorator.php +++ b/dev/tests/functional/lib/Magento/Mtf/Util/Protocol/CurlTransport/BackendDecorator.php @@ -77,6 +77,7 @@ protected function authorize() $this->transport->write($url, $data, CurlInterface::POST); $response = $this->read(); if (strpos($response, 'login-form') !== false) { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception( 'Admin user cannot be logged in by curl handler!' ); @@ -111,6 +112,7 @@ public function write($url, $params = [], $method = CurlInterface::POST, $header if ($this->formKey) { $params['form_key'] = $this->formKey; } else { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception(sprintf('Form key is absent! Url: "%s" Response: "%s"', $url, $this->response)); } $this->transport->write($url, http_build_query($params), $method, $headers); diff --git a/dev/tests/functional/tests/app/Magento/AdminNotification/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/AdminNotification/Test/TestCase/NavigateMenuTest.xml index f2f56eb74f70..99bd9c6d9d22 100644 --- a/dev/tests/functional/tests/app/Magento/AdminNotification/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/AdminNotification/Test/TestCase/NavigateMenuTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">System > Notifications</data> <data name="pageTitle" xsi:type="string">Notifications</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/Constraint/AssertExportAdvancedPricing.php b/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/Constraint/AssertExportAdvancedPricing.php index 565d0f432bda..c92563c1ca5b 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 df8cd6f354c2..c2c684c89d06 100644 --- a/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/TestCase/ExportAdvancedPricingTest.php +++ b/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/TestCase/ExportAdvancedPricingTest.php @@ -11,6 +11,7 @@ use Magento\Mtf\TestCase\Injectable; use Magento\Mtf\TestStep\TestStepFactory; use Magento\Store\Test\Fixture\Website; +use Magento\Mtf\Util\Command\Cli\Cron; /** * Preconditions: @@ -65,16 +66,23 @@ class ExportAdvancedPricingTest extends Injectable private $catalogProductIndex; /** - * Prepare test data. + * Cron command * - * @param CatalogProductIndex $catalogProductIndex + * @var Cron + */ + private $cron; + + /** + * Run cron before tests running + * + * @param Cron $cron * @return void */ public function __prepare( - CatalogProductIndex $catalogProductIndex + Cron $cron ) { - $catalogProductIndex->open(); - $catalogProductIndex->getProductGrid()->massaction([], 'Delete', true, 'Select All'); + $cron->run(); + $cron->run(); } /** @@ -84,18 +92,21 @@ public function __prepare( * @param FixtureFactory $fixtureFactory * @param AdminExportIndex $adminExportIndex * @param CatalogProductIndex $catalogProductIndexPage + * @param Cron $cron * @return void */ public function __inject( TestStepFactory $stepFactory, FixtureFactory $fixtureFactory, AdminExportIndex $adminExportIndex, - CatalogProductIndex $catalogProductIndexPage + CatalogProductIndex $catalogProductIndexPage, + Cron $cron ) { $this->stepFactory = $stepFactory; $this->fixtureFactory = $fixtureFactory; $this->adminExportIndex = $adminExportIndex; $this->catalogProductIndex = $catalogProductIndexPage; + $this->cron = $cron; } /** @@ -130,9 +141,13 @@ public function test( $website->persist(); $this->setupCurrencyForCustomWebsite($website, $currencyCustomWebsite); } + $this->cron->run(); + $this->cron->run(); $products = $this->prepareProducts($products, $website); + $this->cron->run(); + $this->cron->run(); $this->adminExportIndex->open(); - + $this->adminExportIndex->getExportedGrid()->deleteAllExportedFiles(); $exportData = $this->fixtureFactory->createByCode( 'exportData', [ @@ -150,7 +165,8 @@ public function test( if (!empty($advancedPricingAttributes)) { $products = [$products[0]]; } - + $this->cron->run(); + $this->cron->run(); return [ 'products' => $products ]; @@ -191,6 +207,9 @@ private function setupCurrencyForCustomWebsite($website, $currencyDataset) */ public function prepareProducts(array $products, Website $website = null) { + $this->catalogProductIndex->open(); + $this->catalogProductIndex->getProductGrid()->massaction([], 'Delete', true, 'Select All'); + if (empty($products)) { return null; } diff --git a/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/TestCase/ExportAdvancedPricingTest.xml b/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/TestCase/ExportAdvancedPricingTest.xml index 9f19ff4cb00a..07646c2aceda 100644 --- a/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/TestCase/ExportAdvancedPricingTest.xml +++ b/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/TestCase/ExportAdvancedPricingTest.xml @@ -9,6 +9,7 @@ <testCase name="Magento\AdvancedPricingImportExport\Test\TestCase\ExportAdvancedPricingTest" summary="Export with advanced pricing entity type option"> <variation name="ExportAdvancedPricingTestVariation1" summary="Trying export product data with advanced pricing option but without created products" ticketId="MAGETWO-46147"> <data name="exportData" xsi:type="string">csv_with_advanced_pricing</data> + <constraint name="Magento\ImportExport\Test\Constraint\AssertExportSubmittedMessage"/> <constraint name="Magento\ImportExport\Test\Constraint\AssertExportNoDataErrorMessage"/> </variation> <variation name="ExportAdvancedPricingTestVariation2" summary="Trying export product data with advanced pricing option" ticketId="MAGETWO-46120"> diff --git a/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/TestCase/ImportDataNegativeTest.xml b/dev/tests/functional/tests/app/Magento/AdvancedPricingImportExport/Test/TestCase/ImportDataNegativeTest.xml index 65b4d6e973bb..db992e662d81 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 12203222534c..e728a8761639 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 a975d19ef887..89c9d9168921 100644 --- a/dev/tests/functional/tests/app/Magento/Analytics/Test/TestCase/AdvancedReportingButtonTest.xml +++ b/dev/tests/functional/tests/app/Magento/Analytics/Test/TestCase/AdvancedReportingButtonTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Analytics\Test\TestCase\AdvancedReportingButtonTest" summary="Navigate through Advanced Reporting button on dashboard to Sign Up page" ticketId="MAGETWO-63715"> <variation name="AdvancedReportingButtonTest"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="advancedReportingLink" xsi:type="string">https://advancedreporting.rjmetrics.com/report</data> <constraint name="Magento\Analytics\Test\Constraint\AssertAdvancedReportingPage" /> </variation> diff --git a/dev/tests/functional/tests/app/Magento/Analytics/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Analytics/Test/TestCase/NavigateMenuTest.xml index 9c19f80e91d3..8f7b07c8c14c 100644 --- a/dev/tests/functional/tests/app/Magento/Analytics/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Analytics/Test/TestCase/NavigateMenuTest.xml @@ -8,12 +8,15 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest" summary="Navigate to menu chapter"> <variation name="NavigateMenuTestBIEssentials" summary="Navigate through BI Essentials admin menu to Sign Up page" ticketId="MAGETWO-63700"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> + <data name="issue" xsi:type="string">MAGETWO-97261: Magento\Backend\Test\TestCase\NavigateMenuTest fails on Jenkins</data> <data name="menuItem" xsi:type="string">Reports > BI Essentials</data> <data name="waitMenuItemNotVisible" xsi:type="boolean">false</data> <data name="businessIntelligenceLink" xsi:type="string">https://dashboard.rjmetrics.com/v2/magento/signup</data> <constraint name="Magento\Analytics\Test\Constraint\AssertBIEssentialsLink" /> </variation> <variation name="NavigateMenuTestAdvancedReporting" summary="Navigate through Advanced Reporting admin menu to BI Reports page" ticketId="MAGETWO-65748"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > Advanced Reporting</data> <data name="waitMenuItemNotVisible" xsi:type="boolean">false</data> <data name="advancedReportingLink" xsi:type="string">https://advancedreporting.rjmetrics.com/report</data> diff --git a/dev/tests/functional/tests/app/Magento/Backend/Test/Handler/Ui/LoginUser.php b/dev/tests/functional/tests/app/Magento/Backend/Test/Handler/Ui/LoginUser.php index 3738f51ef6c6..01d8401b22fe 100644 --- a/dev/tests/functional/tests/app/Magento/Backend/Test/Handler/Ui/LoginUser.php +++ b/dev/tests/functional/tests/app/Magento/Backend/Test/Handler/Ui/LoginUser.php @@ -6,6 +6,7 @@ namespace Magento\Backend\Test\Handler\Ui; +use Magento\Backend\Test\Page\AdminAuthLogin; use Magento\Mtf\Factory\Factory; use Magento\Mtf\Fixture\FixtureInterface; use Magento\Mtf\Handler\Ui; @@ -29,10 +30,15 @@ public function persist(FixtureInterface $fixture = null) $fixture = Factory::getFixtureFactory()->getMagentoBackendAdminSuperAdmin(); } + /** @var AdminAuthLogin $loginPage */ $loginPage = Factory::getPageFactory()->getAdminAuthLogin(); $loginForm = $loginPage->getLoginBlock(); - $adminHeaderPanel = $loginPage->getHeaderBlock(); + if (!$loginForm->isVisible() && !$adminHeaderPanel->isVisible()) { + //We are currently not in the admin area. + $loginPage->open(); + } + if (!$adminHeaderPanel || !$adminHeaderPanel->isVisible()) { $loginPage->open(); if ($adminHeaderPanel->isVisible()) { diff --git a/dev/tests/functional/tests/app/Magento/Backend/Test/Page/Adminhtml/Dashboard.xml b/dev/tests/functional/tests/app/Magento/Backend/Test/Page/Adminhtml/Dashboard.xml index adae65a1d06d..799f9e30fd97 100644 --- a/dev/tests/functional/tests/app/Magento/Backend/Test/Page/Adminhtml/Dashboard.xml +++ b/dev/tests/functional/tests/app/Magento/Backend/Test/Page/Adminhtml/Dashboard.xml @@ -17,5 +17,6 @@ <block name="accessDeniedBlock" class="Magento\Backend\Test\Block\Denied" locator="#anchor-content" strategy="css selector" /> <block name="systemMessageDialog" class="Magento\AdminNotification\Test\Block\System\Messages" locator='.ui-popup-message .modal-inner-wrap' strategy="css selector" /> <block name="applicationVersion" class="Magento\Backend\Test\Block\Version" locator="body" strategy="css selector" /> + <block name="modalMessage" class="Magento\Ui\Test\Block\Adminhtml\Modal" locator=".modal-popup>.modal-inner-wrap" strategy="css selector" /> </page> </config> diff --git a/dev/tests/functional/tests/app/Magento/Backend/Test/Repository/ConfigData.xml b/dev/tests/functional/tests/app/Magento/Backend/Test/Repository/ConfigData.xml index 1792ddb5abdc..9985e962b04e 100644 --- a/dev/tests/functional/tests/app/Magento/Backend/Test/Repository/ConfigData.xml +++ b/dev/tests/functional/tests/app/Magento/Backend/Test/Repository/ConfigData.xml @@ -174,15 +174,6 @@ <item name="value" xsi:type="number">0</item> </field> </dataset> - - <dataset name="log_to_file"> - <field name="dev/debug/debug_logging" xsi:type="array"> - <item name="scope" xsi:type="string">default</item> - <item name="scope_id" xsi:type="number">0</item> - <item name="label" xsi:type="string">Yes</item> - <item name="value" xsi:type="number">1</item> - </field> - </dataset> <dataset name="minify_js_files"> <field name="dev/js/minify_files" xsi:type="array"> diff --git a/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/GlobalSearchEntityTest.xml b/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/GlobalSearchEntityTest.xml index dca6e1d15024..6d9c50b4317c 100644 --- a/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/GlobalSearchEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/GlobalSearchEntityTest.xml @@ -8,31 +8,26 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\GlobalSearchEntityTest" summary="Global Search Backend " ticketId="MAGETWO-28457"> <variation name="GlobalSearchEntityTestVariation1"> - <data name="tag" xsi:type="string">stable:no</data> <data name="description" xsi:type="string">search shows admin preview</data> <data name="search/data/query" xsi:type="string">Some search term</data> <constraint name="Magento\Backend\Test\Constraint\AssertGlobalSearchPreview" /> </variation> <variation name="GlobalSearchEntityTestVariation2"> - <data name="tag" xsi:type="string">stable:no</data> <data name="description" xsi:type="string">search with 2 sign return no results</data> <data name="search/data/query" xsi:type="string">:)</data> <constraint name="Magento\Backend\Test\Constraint\AssertGlobalSearchNoRecordsFound" /> </variation> <variation name="GlobalSearchEntityTestVariation3"> - <data name="tag" xsi:type="string">stable:no</data> <data name="description" xsi:type="string">search product by sku</data> <data name="search/data/query" xsi:type="string">orderInjectable::default::product::sku</data> <constraint name="Magento\Backend\Test\Constraint\AssertGlobalSearchProductName" /> </variation> <variation name="GlobalSearchEntityTestVariation4"> - <data name="tag" xsi:type="string">stable:no</data> <data name="description" xsi:type="string">search existed customer</data> <data name="search/data/query" xsi:type="string">customer::johndoe_unique::lastname</data> <constraint name="Magento\Backend\Test\Constraint\AssertGlobalSearchCustomerName" /> </variation> <variation name="GlobalSearchEntityTestVariation5"> - <data name="tag" xsi:type="string">stable:no</data> <data name="description" xsi:type="string">search order (by order id)</data> <data name="search/data/query" xsi:type="string">orderInjectable::default::id</data> <constraint name="Magento\Backend\Test\Constraint\AssertGlobalSearchOrderId" /> diff --git a/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/LoginAfterJSMinificationTest.php b/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/LoginAfterJSMinificationTest.php index 1c3d018af077..4a6202f815b9 100644 --- a/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/LoginAfterJSMinificationTest.php +++ b/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/LoginAfterJSMinificationTest.php @@ -9,6 +9,7 @@ use Magento\Backend\Test\Page\Adminhtml\Dashboard; use Magento\Mtf\Util\Command\Cli\DeployMode; use Magento\Mtf\TestStep\TestStepFactory; +use Magento\User\Test\TestStep\LoginUserOnBackendStep; /** * Verify visibility of form elements on Configuration page. @@ -53,9 +54,11 @@ public function __inject( } /** - * Admin login test after JS minification is turned on in production mode + * Admin login test after JS minification is turned on in production mode. + * * @param DeployMode $cli * @param null $configData + * * @return void */ public function test( @@ -64,15 +67,26 @@ public function test( ) { $this->configData = $configData; - //Pre-conditions + //Pre-conditions $cli->setDeployModeToDeveloper(); - $this->objectManager->create( + $this->stepFactory->create( \Magento\Config\Test\TestStep\SetupConfigurationStep::class, ['configData' => $this->configData] )->run(); // Steps $cli->setDeployModeToProduction(); - $this->adminDashboardPage->open(); + $this->stepFactory->create(LoginUserOnBackendStep::class)->run(); + } + + /** + * @inheritdoc + */ + protected function tearDown() + { + $this->stepFactory->create( + \Magento\Config\Test\TestStep\SetupConfigurationStep::class, + ['configData' => $this->configData] + )->cleanup(); } } diff --git a/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/NavigateMenuTest.xml index 67842f62d7c9..afdf70704a98 100644 --- a/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Backend/Test/TestCase/NavigateMenuTest.xml @@ -8,26 +8,31 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest" summary="Navigate through admin menu" ticketId="MAGETWO-34874"> <variation name="NavigateMenuTest2"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Dashboard</data> <data name="pageTitle" xsi:type="string">Dashboard</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest3"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Content > Schedule</data> <data name="pageTitle" xsi:type="string">Store Design Schedule</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest4"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Stores > All Stores</data> <data name="pageTitle" xsi:type="string">Stores</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest5"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Stores > Configuration</data> <data name="pageTitle" xsi:type="string">Configuration</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest6"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">System > Cache Management</data> <data name="pageTitle" xsi:type="string">Cache Management</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> diff --git a/dev/tests/functional/tests/app/Magento/Backup/Test/Repository/ConfigData.xml b/dev/tests/functional/tests/app/Magento/Backup/Test/Repository/ConfigData.xml new file mode 100644 index 000000000000..c8b19aa2bd32 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Backup/Test/Repository/ConfigData.xml @@ -0,0 +1,24 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/Magento/Mtf/Repository/etc/repository.xsd"> + <repository class="Magento\Config\Test\Repository\ConfigData"> + <dataset name="enable_backups_functionality"> + <field name="system/backup/functionality_enabled" xsi:type="array"> + <item name="label" xsi:type="string">Yes</item> + <item name="value" xsi:type="number">1</item> + </field> + </dataset> + <dataset name="enable_backups_functionality_rollback"> + <field name="web/url/use_store" xsi:type="array"> + <item name="label" xsi:type="string">No</item> + <item name="value" xsi:type="number">0</item> + </field> + </dataset> + </repository> +</config> diff --git a/dev/tests/functional/tests/app/Magento/Backup/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Backup/Test/TestCase/NavigateMenuTest.xml deleted file mode 100644 index 0c024f0e3f5a..000000000000 --- a/dev/tests/functional/tests/app/Magento/Backup/Test/TestCase/NavigateMenuTest.xml +++ /dev/null @@ -1,16 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - --> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> - <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> - <variation name="NavigateMenuTest7"> - <data name="menuItem" xsi:type="string">System > Backups</data> - <data name="pageTitle" xsi:type="string">Backups</data> - <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> - </variation> - </testCase> -</config> diff --git a/dev/tests/functional/tests/app/Magento/Bundle/Test/Block/Adminhtml/Product/Composite/Configure.xml b/dev/tests/functional/tests/app/Magento/Bundle/Test/Block/Adminhtml/Product/Composite/Configure.xml index e4c0dce0c5b1..18aedfa97f9e 100644 --- a/dev/tests/functional/tests/app/Magento/Bundle/Test/Block/Adminhtml/Product/Composite/Configure.xml +++ b/dev/tests/functional/tests/app/Magento/Bundle/Test/Block/Adminhtml/Product/Composite/Configure.xml @@ -7,7 +7,6 @@ --> <mapping strict="0"> <fields> - <qty /> <checkbox> <selector>div[contains(@class,"field choice") and label[contains(.,"%product_name%")]]//input</selector> <strategy>xpath</strategy> diff --git a/dev/tests/functional/tests/app/Magento/Bundle/Test/Constraint/AssertBundleProductOnConfigureCartPage.php b/dev/tests/functional/tests/app/Magento/Bundle/Test/Constraint/AssertBundleProductOnConfigureCartPage.php index efa75981db7b..b97a954a981b 100644 --- a/dev/tests/functional/tests/app/Magento/Bundle/Test/Constraint/AssertBundleProductOnConfigureCartPage.php +++ b/dev/tests/functional/tests/app/Magento/Bundle/Test/Constraint/AssertBundleProductOnConfigureCartPage.php @@ -8,6 +8,7 @@ use Magento\Bundle\Test\Fixture\BundleProduct; use Magento\Catalog\Test\Page\Product\CatalogProductView; +use Magento\Checkout\Test\Constraint\Utils\CartPageLoadTrait; use Magento\Checkout\Test\Fixture\Cart; use Magento\Checkout\Test\Page\CheckoutCart; use Magento\Mtf\Constraint\AbstractAssertForm; @@ -17,6 +18,8 @@ */ class AssertBundleProductOnConfigureCartPage extends AbstractAssertForm { + use CartPageLoadTrait; + /** * Check bundle product options correctly displayed on cart configuration page. * @@ -28,6 +31,8 @@ class AssertBundleProductOnConfigureCartPage extends AbstractAssertForm public function processAssert(CheckoutCart $checkoutCart, Cart $cart, CatalogProductView $catalogProductView) { $checkoutCart->open(); + $this->waitForCartPageLoaded($checkoutCart); + $sourceProducts = $cart->getDataFieldConfig('items')['source']; $products = $sourceProducts->getProducts(); foreach ($cart->getItems() as $key => $item) { diff --git a/dev/tests/functional/tests/app/Magento/Bundle/Test/Fixture/Cart/Item.php b/dev/tests/functional/tests/app/Magento/Bundle/Test/Fixture/Cart/Item.php index 9b12c467e577..4d6d06ac6e62 100644 --- a/dev/tests/functional/tests/app/Magento/Bundle/Test/Fixture/Cart/Item.php +++ b/dev/tests/functional/tests/app/Magento/Bundle/Test/Fixture/Cart/Item.php @@ -46,7 +46,7 @@ public function getData($key = null) $optionData = [ 'title' => $checkoutOption['title'], 'value' => "{$qty} x {$value} {$price}", - 'sku' => "{$qty} x {$value}" + 'sku' => "{$value}" ]; $checkoutBundleOptions[$checkoutOptionKey] = $optionData; diff --git a/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/DeleteProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/DeleteProductEntityTest.xml index 45e255649d2c..157135117fbe 100644 --- a/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/DeleteProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/DeleteProductEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Product\DeleteProductEntityTest"> <variation name="DeleteProductEntityTestVariation4"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="products" xsi:type="string">bundleProduct::bundle_dynamic_product</data> <data name="isRequired" xsi:type="string">Yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSuccessDeleteMessage" /> @@ -15,6 +16,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductIsNotDisplayingOnFrontend" /> </variation> <variation name="DeleteProductEntityTestVariation5"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="products" xsi:type="string">bundleProduct::bundle_fixed_product</data> <data name="isRequired" xsi:type="string">No</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSuccessDeleteMessage" /> diff --git a/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/ValidateOrderOfProductTypeTest.xml b/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/ValidateOrderOfProductTypeTest.xml index 7077f367795e..cb32742a0ce6 100644 --- a/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/ValidateOrderOfProductTypeTest.xml +++ b/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/ValidateOrderOfProductTypeTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Product\ValidateOrderOfProductTypeTest"> <variation name="ValidateOrderOfProductTypeTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menu/4" xsi:type="string">Bundle Product</data> </variation> </testCase> diff --git a/dev/tests/functional/tests/app/Magento/BundleImportExport/Test/TestCase/ExportProductsTest.xml b/dev/tests/functional/tests/app/Magento/BundleImportExport/Test/TestCase/ExportProductsTest.xml index 3ad8cff31eaf..bfbe233b9dc1 100644 --- a/dev/tests/functional/tests/app/Magento/BundleImportExport/Test/TestCase/ExportProductsTest.xml +++ b/dev/tests/functional/tests/app/Magento/BundleImportExport/Test/TestCase/ExportProductsTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\CatalogImportExport\Test\TestCase\ExportProductsTest" summary="Export products"> <variation name="ExportProductsTestVariation4" summary="Export bundle products" ticketId="MAGETWO-30602"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="exportData" xsi:type="string">default</data> <data name="products/0" xsi:type="array"> <item name="fixture" xsi:type="string">bundleProduct</item> diff --git a/dev/tests/functional/tests/app/Magento/Captcha/Test/Constraint/AssertCaptchaFieldOnContactUsForm.php b/dev/tests/functional/tests/app/Magento/Captcha/Test/Constraint/AssertCaptchaFieldOnContactUsForm.php index 4883d7819c28..b04039713945 100644 --- a/dev/tests/functional/tests/app/Magento/Captcha/Test/Constraint/AssertCaptchaFieldOnContactUsForm.php +++ b/dev/tests/functional/tests/app/Magento/Captcha/Test/Constraint/AssertCaptchaFieldOnContactUsForm.php @@ -6,7 +6,7 @@ namespace Magento\Captcha\Test\Constraint; -use Magento\Contact\Test\Page\ContactIndex; +use Magento\Captcha\Test\Page\ContactIndexCaptcha as ContactIndex; use Magento\Mtf\Constraint\AbstractConstraint; /** diff --git a/dev/tests/functional/tests/app/Magento/Captcha/Test/Page/ContactIndex.xml b/dev/tests/functional/tests/app/Magento/Captcha/Test/Page/ContactIndex.xml index 060fc5f346fd..742eabb61f37 100644 --- a/dev/tests/functional/tests/app/Magento/Captcha/Test/Page/ContactIndex.xml +++ b/dev/tests/functional/tests/app/Magento/Captcha/Test/Page/ContactIndex.xml @@ -6,7 +6,7 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/pages.xsd"> - <page name="ContactIndex" mca="contact/index/index" module="Magento_Contact"> + <page name="ContactIndexCaptcha" mca="contact/index/index" module="Magento_Captcha"> <block name="contactUs" class="Magento\Captcha\Test\Block\Form\ContactUs" locator="#contact-form" strategy="css selector" /> </page> </config> diff --git a/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaEditCustomerTest.xml b/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaEditCustomerTest.xml index 12b9808adb9e..0c0e06d63b6c 100644 --- a/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaEditCustomerTest.xml +++ b/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaEditCustomerTest.xml @@ -14,6 +14,7 @@ <data name="attempts" xsi:type="number">3</data> <data name="captcha" xsi:type="string">111</data> <data name="configData" xsi:type="string">captcha_storefront_user_edit_failures_number, customer_max_login_failures_number</data> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <constraint name="Magento\Security\Test\Constraint\AssertCustomerIsLocked" /> <constraint name="Magento\Customer\Test\Constraint\AssertCustomerIsLockedOnBackend" /> </variation> diff --git a/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnAdminLoginTest.xml b/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnAdminLoginTest.xml index 186439bb9f15..9242bfbef237 100644 --- a/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnAdminLoginTest.xml +++ b/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnAdminLoginTest.xml @@ -12,6 +12,7 @@ <data name="customAdmin/data/captcha" xsi:type="string">111</data> <data name="pageTitle" xsi:type="string">Dashboard</data> <data name="configData" xsi:type="string">captcha_backend_login</data> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> </testCase> diff --git a/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnContactUsTest.php b/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnContactUsTest.php index d8c9bf1f719d..0de71c3a416c 100644 --- a/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnContactUsTest.php +++ b/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnContactUsTest.php @@ -8,7 +8,7 @@ use Magento\Captcha\Test\Constraint\AssertCaptchaFieldOnContactUsForm; use Magento\Contact\Test\Fixture\Comment; -use Magento\Contact\Test\Page\ContactIndex; +use Magento\Captcha\Test\Page\ContactIndexCaptcha as ContactIndex; use Magento\Mtf\TestCase\Injectable; use Magento\Mtf\TestStep\TestStepFactory; diff --git a/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnContactUsTest.xml b/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnContactUsTest.xml index a88cd98e3c31..1a25afeabc5d 100644 --- a/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnContactUsTest.xml +++ b/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnContactUsTest.xml @@ -12,6 +12,7 @@ <data name="comment/data/captcha" xsi:type="string">111</data> <data name="comment/data/customer/dataset" xsi:type="string">default</data> <data name="configData" xsi:type="string">captcha_storefront_contact_us</data> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <constraint name="Magento\Contact\Test\Constraint\AssertContactUsSuccessMessage" /> </variation> </testCase> diff --git a/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnStoreFrontLoginTest.xml b/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnStoreFrontLoginTest.xml index 8e4327db5edd..8068b2cbc050 100644 --- a/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnStoreFrontLoginTest.xml +++ b/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnStoreFrontLoginTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Captcha\Test\TestCase\CaptchaOnStoreFrontLoginTest" summary="Check CAPTCHA on StoreFront Login Page" ticketId="MAGETWO-43639"> <variation name="CaptchaOnStoreFrontLoginTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="customer/dataset" xsi:type="string">default</data> <data name="captcha" xsi:type="string">111</data> <data name="configData" xsi:type="string">captcha_storefront_login</data> diff --git a/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnStoreFrontRegisterTest.xml b/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnStoreFrontRegisterTest.xml index 8e83c189efc2..b0ce6dfa561a 100644 --- a/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnStoreFrontRegisterTest.xml +++ b/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnStoreFrontRegisterTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Captcha\Test\TestCase\CaptchaOnStoreFrontRegisterTest" summary="Check CAPTCHA on StoreFront Register Page" ticketId="MAGETWO-43602"> <variation name="CaptchaOnStoreFrontRegisterTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="customer/dataset" xsi:type="string">register_customer</data> <data name="customer/data/captcha" xsi:type="string">111</data> <data name="configData" xsi:type="string">captcha_storefront_register</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Category/Tree.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Category/Tree.php index 2035e7e83200..30a323eebb73 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Category/Tree.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Category/Tree.php @@ -187,6 +187,8 @@ public function assignCategory($parentCategoryName, $childCategoryName) */ public function expandAllCategories() { + $this->getTemplateBlock()->waitLoader(); $this->_rootElement->find($this->expandAll)->click(); + $this->getTemplateBlock()->waitLoader(); } } diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Edit/Section/Options.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Edit/Section/Options.php index 2ac4fb81ae60..d591f3b44462 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Edit/Section/Options.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Adminhtml/Product/Edit/Section/Options.php @@ -403,4 +403,21 @@ public function getFileOptionElements() { return $this->_rootElement->getElements($this->hintMessage); } + + /** + * @inheritdoc + */ + protected function _fill(array $fields, SimpleElement $element = null) + { + $context = ($element === null) ? $this->_rootElement : $element; + foreach ($fields as $name => $field) { + $element = $this->getElement($context, $field); + if (!$element->isDisabled()) { + $element->getContext()->hover(); + $element->setValue($field['value']); + } else { + throw new \Exception("Unable to set value to field '$name' as it's disabled."); + } + } + } } diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/ProductList/TopToolbar.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/ProductList/TopToolbar.php index 090042140a54..48769126d88e 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/ProductList/TopToolbar.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/ProductList/TopToolbar.php @@ -48,7 +48,7 @@ public function getSelectSortType() public function getSortType() { $content = $this->_rootElement->find($this->sorter)->getText(); - return explode("\n", $content); + return array_values(array_filter(array_map('trim', explode("\n", $content)))); } /** diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/View/CustomOptions.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/View/CustomOptions.php index 9f05a4ade8a3..4e8e0f97d70d 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/View/CustomOptions.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/View/CustomOptions.php @@ -128,7 +128,8 @@ class CustomOptions extends Form * * @var string */ - private $validationErrorMessage = '//div[@class="mage-error"][contains(text(), "required field")]'; + private $validationErrorMessage = '//div[@class="mage-error"][contains(text(), "required field")' . + 'and not(contains(@style,\'display\'))]'; /** * Get product options @@ -148,6 +149,7 @@ public function getOptions(FixtureInterface $product) foreach ($dataOptions as $option) { $title = $option['title']; if (!isset($listCustomOptions[$title])) { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception("Can't find option: \"{$title}\""); } @@ -231,7 +233,7 @@ protected function getFieldData(SimpleElement $option) return [ 'options' => [ [ - 'price' => floatval($price), + 'price' => (float)$price, 'max_characters' => $maxCharacters, ], ] @@ -262,7 +264,7 @@ protected function getFileData(SimpleElement $option) return [ 'options' => [ [ - 'price' => floatval($price), + 'price' => (float)$price, 'file_extension' => $this->getOptionNotice($option, 1), 'image_size_x' => preg_replace('/[^0-9]/', '', $this->getOptionNotice($option, 2)), 'image_size_y' => preg_replace('/[^0-9]/', '', $this->getOptionNotice($option, 3)), @@ -344,7 +346,7 @@ protected function getDateData(SimpleElement $option) return [ 'options' => [ [ - 'price' => floatval($price), + 'price' => (float)$price, ], ] ]; diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Search.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Search.php index 7ca5bfd2be14..a34b97b4ce22 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Search.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Search.php @@ -10,7 +10,6 @@ use Magento\Mtf\Client\Locator; /** - * Class Search * Block for "Search" section */ class Search extends Block @@ -77,6 +76,7 @@ public function search($keyword, $length = null) $keyword = substr($keyword, 0, $length); } $this->fillSearch($keyword); + $this->waitForElementEnabled($this->searchButton); $this->_rootElement->find($this->searchButton)->click(); } @@ -157,4 +157,24 @@ public function clickSuggestedText($text) $searchAutocomplete = sprintf($this->searchAutocomplete, $text); $this->_rootElement->find($searchAutocomplete, Locator::SELECTOR_XPATH)->click(); } + + /** + * Wait for element is enabled. + * + * @param string $selector + * @param string $strategy + * @return bool|null + */ + public function waitForElementEnabled($selector, $strategy = Locator::SELECTOR_CSS) + { + $browser = $this->browser; + + return $browser->waitUntil( + function () use ($browser, $selector, $strategy) { + $element = $browser->find($selector, $strategy); + + return !$element->isDisabled() ? true : null; + } + ); + } } diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertProductDuplicateForm.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertProductDuplicateForm.php index f65aa33ff93e..d5cf5d918817 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertProductDuplicateForm.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Constraint/AssertProductDuplicateForm.php @@ -116,7 +116,7 @@ function (&$item, $key, $formattingOptions) { protected function prepareUrlKey($urlKey) { preg_match("~\d+$~", $urlKey, $matches); - $key = intval($matches[0]) + 1; + $key = (int)$matches[0] + 1; return str_replace($matches[0], $key, $urlKey); } diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductAttribute/Curl.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductAttribute/Curl.php index d78095f05fe4..3d644a2a3227 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductAttribute/Curl.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductAttribute/Curl.php @@ -91,15 +91,18 @@ public function persist(FixtureInterface $fixture = null) $data['frontend_label'] = [0 => $data['frontend_label']]; if (isset($data['options'])) { + $optionsData = []; foreach ($data['options'] as $key => $values) { + $optionRowData = []; $index = 'option_' . $key; if ($values['is_default'] == 'Yes') { - $data['default'][] = $index; + $optionRowData['default'][] = $index; } - $data['option']['value'][$index] = [$values['admin'], $values['view']]; - $data['option']['order'][$index] = $key; + $optionRowData['option']['value'][$index] = [$values['admin'], $values['view']]; + $optionRowData['option']['order'][$index] = $key; + $optionsData[] = $optionRowData; } - unset($data['options']); + $data['options'] = $optionsData; } $data = $this->changeStructureOfTheData($data); @@ -134,11 +137,39 @@ public function persist(FixtureInterface $fixture = null) } /** + * Additional data handling. + * * @param array $data * @return array */ - protected function changeStructureOfTheData(array $data) + protected function changeStructureOfTheData(array $data): array { + if (!isset($data['options'])) { + return $data; + } + + $serializedOptions = $this->getSerializeOptions($data['options']); + if ($serializedOptions) { + $data['serialized_options'] = $serializedOptions; + unset($data['options']); + } + return $data; } + + /** + * Provides serialized product attribute options. + * + * @param array $data + * @return string + */ + protected function getSerializeOptions(array $data): string + { + $options = []; + foreach ($data as $optionRowData) { + $options[] = http_build_query($optionRowData); + } + + return json_encode($options); + } } diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductSimple/Curl.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductSimple/Curl.php index c75112fee860..2a23903a697b 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductSimple/Curl.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Handler/CatalogProductSimple/Curl.php @@ -411,7 +411,7 @@ protected function prepareQuantityAndStockStatus() : ['is_in_stock' => 'In Stock']; if (!isset($quantityAndStockStatus['is_in_stock'])) { - $qty = isset($quantityAndStockStatus['qty']) ? intval($quantityAndStockStatus['qty']) : null; + $qty = isset($quantityAndStockStatus['qty']) ? (int)$quantityAndStockStatus['qty'] : null; $quantityAndStockStatus['is_in_stock'] = 0 === $qty ? 'Out of Stock' : 'In Stock'; } diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml index 721b0ff57007..e90ca6bf7868 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/CatalogProductSimple.xml @@ -41,7 +41,7 @@ <field name="attribute_set_id" xsi:type="array"> <item name="dataset" xsi:type="string">default</item> </field> - <field name="name" xsi:type="string">Product \'!@#$%^&*()+:;\\|}{][?=-~` %isolation%</field> + <field name="name" xsi:type="string">Product \'!@#$%^&*()+:;\\|}{][?=~` %isolation%</field> <field name="sku" xsi:type="string">sku_simple_product_%isolation%</field> <field name="is_virtual" xsi:type="string">No</field> <field name="product_has_weight" xsi:type="string">This item has weight</field> 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 d529f7486598..014d685cfdb7 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/Category.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Repository/Category.xml @@ -179,5 +179,31 @@ <item name="dataset" xsi:type="string">catalogProductSimple::default</item> </field> </dataset> + <dataset name="default_subcategory_with_anchored_parent_with_product"> + <field name="name" xsi:type="string">DefaultSubcategory%isolation%</field> + <field name="url_key" xsi:type="string">default-subcategory-%isolation%</field> + <field name="parent_id" xsi:type="array"> + <item name="dataset" xsi:type="string">default_anchored_category_with_product</item> + </field> + <field name="is_active" xsi:type="string">Yes</field> + <field name="include_in_menu" xsi:type="string">Yes</field> + <field name="is_anchor" xsi:type="string">Yes</field> + <field name="category_products" xsi:type="array"> + <item name="dataset" xsi:type="string">catalogProductSimple::default</item> + </field> + </dataset> + <dataset name="default_anchored_category_with_product"> + <field name="name" xsi:type="string">Category%isolation%</field> + <field name="url_key" xsi:type="string">category%isolation%</field> + <field name="is_active" xsi:type="string">Yes</field> + <field name="include_in_menu" xsi:type="string">Yes</field> + <field name="is_anchor" xsi:type="string">Yes</field> + <field name="parent_id" xsi:type="array"> + <item name="dataset" xsi:type="string">default_category</item> + </field> + <field name="category_products" xsi:type="array"> + <item name="dataset" xsi:type="string">catalogProductSimple::product_5_dollar</item> + </field> + </dataset> </repository> </config> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/AdvancedMoveCategoryEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/AdvancedMoveCategoryEntityTest.xml index 96a9d91a8e5f..e92edf4a143b 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/AdvancedMoveCategoryEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/AdvancedMoveCategoryEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Category\AdvancedMoveCategoryEntityTest" summary="Move category from one to another" ticketId="MAGETWO-27319"> <variation name="AdvancedMoveCategoryEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="childCategory/dataset" xsi:type="string">three_nested_categories</data> <data name="parentCategory/dataset" xsi:type="string">default</data> <data name="moveLevel" xsi:type="number">1</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/CreateCategoryEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/CreateCategoryEntityTest.xml index a840b81a5d20..69093b8adb8d 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/CreateCategoryEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/CreateCategoryEntityTest.xml @@ -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\CreateCategoryEntityTest" summary="Create Category from Category Page" ticketId="MAGETWO-23411"> <variation name="CreateCategoryEntityTestVariation1_RootCategory_RequiredFields"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Create root category with required fields only</data> <data name="addCategory" xsi:type="string">addRootCategory</data> <data name="category/data/is_active" xsi:type="string">Yes</data> @@ -16,7 +17,6 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryForm" /> </variation> <variation name="CreateCategoryEntityTestVariation2_RootCategory_AllFields"> - <data name="issue" xsi:type="string">MAGETWO-60635: [CE][Categories] Design update dates are incorrect after save</data> <data name="description" xsi:type="string">Create root category with all fields</data> <data name="addCategory" xsi:type="string">addRootCategory</data> <data name="category/data/is_active" xsi:type="string">Yes</data> @@ -57,7 +57,6 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryPage" /> </variation> <variation name="CreateCategoryEntityTestVariation4_Subcategory_AllFields"> - <data name="issue" xsi:type="string">MAGETWO-60635: [CE][Categories] Design update dates are incorrect after save</data> <data name="description" xsi:type="string">Create not anchor subcategory specifying all fields</data> <data name="addCategory" xsi:type="string">addSubcategory</data> <data name="category/data/parent_id/dataset" xsi:type="string">default_category</data> @@ -95,6 +94,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryForAssignedProducts" /> </variation> <variation name="CreateCategoryEntityTestVariation5_Anchor_MostOfFields"> + <data name="tag" xsi:type="string">test_type:acceptance_test</data> <data name="description" xsi:type="string">Create anchor subcategory with all fields</data> <data name="addCategory" xsi:type="string">addSubcategory</data> <data name="category/data/parent_id/dataset" xsi:type="string">default_category</data> @@ -144,7 +144,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryPage" /> </variation> <variation name="CreateCategoryEntityTestVariation8_WithProducts" summary="Assign Products at the Category Level" ticketId="MAGETWO-16351"> - <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, stable:no</data> + <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test</data> <data name="addCategory" xsi:type="string">addSubcategory</data> <data name="category/data/parent_id/dataset" xsi:type="string">default_category</data> <data name="category/data/is_active" xsi:type="string">Yes</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/DeleteCategoryEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/DeleteCategoryEntityTest.xml index 6951194308bc..77ed04d40b77 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/DeleteCategoryEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/DeleteCategoryEntityTest.xml @@ -8,11 +8,13 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Category\DeleteCategoryEntityTest" summary="Delete Category" ticketId="MAGETWO-23303"> <variation name="DeleteCategoryEntityTestVariation1_RootCategory" summary="Can delete a root category not assigned to any store"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="category/dataset" xsi:type="string">root_category</data> <constraint name="Magento\Catalog\Test\Constraint\AssertCategorySuccessDeleteMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryAbsenceOnBackend" /> </variation> <variation name="DeleteCategoryEntityTestVariation2_Subcategory" summary="Can delete a subcategory"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="category/dataset" xsi:type="string">root_subcategory</data> <constraint name="Magento\Catalog\Test\Constraint\AssertCategorySuccessDeleteMessage" /> <constraint name="Magento\UrlRewrite\Test\Constraint\AssertUrlRewriteCategoryNotInGrid" /> @@ -20,6 +22,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryAbsenceOnFrontend" /> </variation> <variation name="DeleteCategoryEntityTestVariation3_RootCategory_AssignedToStore" summary="Cannot delete root category assigned to some store"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="category/dataset" xsi:type="string">default_category</data> <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryCannotBeDeleted" /> </variation> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/MoveCategoryEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/MoveCategoryEntityTest.xml index 446011902c09..b4fd843ca800 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/MoveCategoryEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/MoveCategoryEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Category\MoveCategoryEntityTest" summary="Move category from one to another" ticketId="MAGETWO-27319"> <variation name="MoveCategoryEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="childCategory/dataset" xsi:type="string">three_nested_categories</data> <data name="parentCategory/dataset" xsi:type="string">default</data> <data name="nestingLevel" xsi:type="string">3</data> @@ -15,8 +16,8 @@ <constraint name="Magento\UrlRewrite\Test\Constraint\AssertUrlRewriteCategoryInGrid" /> </variation> <variation name="MoveCategoryEntityTestVariation2" summary="Move default subcategory with anchored parent to default subcategory" ticketId="MAGETWO-21202"> - <data name="issue" xsi:type="string">MAGETWO-65147: Category is not present in Layered navigation block when anchor is on</data> - <data name="childCategory/dataset" xsi:type="string">default_subcategory_with_anchored_parent</data> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> + <data name="childCategory/dataset" xsi:type="string">default_subcategory_with_anchored_parent_with_product</data> <data name="parentCategory/dataset" xsi:type="string">default</data> <data name="moveLevel" xsi:type="number">2</data> <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryMovedMessage" /> @@ -25,9 +26,9 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryBreadcrumbs" /> </variation> <variation name="MoveCategoryEntityTestVariation3" summary="Move default anchored subcategory with anchored parent to default subcategory" ticketId="MAGETWO-21202"> - <data name="issue" xsi:type="string">MAGETWO-65147: Category is not present in Layered navigation block when anchor is on</data> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="childCategory/dataset" xsi:type="string">default_subcategory_with_anchored_parent</data> - <data name="childCategory/data/parent_id/dataset" xsi:type="string">default_anchor_subcategory_with_anchored_parent</data> + <data name="childCategory/data/parent_id/dataset" xsi:type="string">default_subcategory_with_anchored_parent_with_product</data> <data name="parentCategory/dataset" xsi:type="string">default_category</data> <data name="moveLevel" xsi:type="number">2</data> <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryMovedMessage" /> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/SubcategoryNotIncludeInNavigationMenuTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/SubcategoryNotIncludeInNavigationMenuTest.xml index 94d99dd6b7b2..53a7debffa43 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/SubcategoryNotIncludeInNavigationMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/SubcategoryNotIncludeInNavigationMenuTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Category\MyTest" summary="Test child categories should not include in menu" ticketId="MAGETWO-72238"> <variation name="CategoryIncludeInNavigationMenuAndSubcategoryNotIncludeInNavigationMenu" summary="Active category and check that category is visible on navigation menu and subcategory is not visible on navigation menu"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialCategory/dataset" xsi:type="string">two_nested_categories</data> <data name="nestingLevel" xsi:type="number">2</data> <data name="category/data/is_active" xsi:type="string">Yes</data> @@ -16,6 +17,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertSubCategoryNotInNavigationMenu" /> </variation> <variation name="CategoryAndSubcategotyNotIncludeInNavigationMenu1" summary="Turn off include_in_menu category and check that category and subcategory are not visible on navigation menu"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialCategory/dataset" xsi:type="string">two_nested_categories</data> <data name="nestingLevel" xsi:type="number">2</data> <data name="category/data/is_active" xsi:type="string">Yes</data> @@ -24,6 +26,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertSubCategoryNotInNavigationMenu" /> </variation> <variation name="InactiveCategoryAndSubcategotyNotIncludeInNavigationMenu" summary="Inactive category and check that category and subcategory are not visible on navigation menu"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialCategory/dataset" xsi:type="string">two_nested_categories</data> <data name="nestingLevel" xsi:type="number">2</data> <data name="category/data/is_active" xsi:type="string">No</data> @@ -32,6 +35,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertSubCategoryNotInNavigationMenu" /> </variation> <variation name="CategoryAndSubcategotyNotIncludeInNavigationMenu2" summary="Turn off include_in_menu category, inactive category and check that category and subcategory are not visible on navigation menu"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialCategory/dataset" xsi:type="string">two_nested_categories</data> <data name="nestingLevel" xsi:type="number">2</data> <data name="category/data/is_active" xsi:type="string">No</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateCategoryEntityFlatDataTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateCategoryEntityFlatDataTest.xml index 5f14f579a427..99f4b6718feb 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateCategoryEntityFlatDataTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateCategoryEntityFlatDataTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Category\UpdateCategoryEntityFlatDataTest" summary="Update Category if Use Category Flat (Cron is ON, 'Update on Save' Mode)" ticketId="MAGETWO-20169"> <variation name="UpdateCategoryEntityFlatDataTestVariation1" summary="Update Category with custom name and description"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialCategory/dataset" xsi:type="string">default</data> <data name="category/data/name" xsi:type="string">Name%isolation%</data> <data name="category/data/description" xsi:type="string">Category Description Updated</data> @@ -23,6 +24,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryPage" /> </variation> <variation name="UpdateCategoryEntityFlatDataTestVariation2" summary="Include category to navigation menu"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialCategory/dataset" xsi:type="string">default</data> <data name="initialCategory/data/include_in_menu" xsi:type="string">No</data> <data name="category/data/include_in_menu" xsi:type="string">Yes</data> @@ -37,6 +39,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryInNavigationMenu" /> </variation> <variation name="UpdateCategoryEntityFlatDataTestVariation3" summary="Update category and assert assigned products"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialCategory/dataset" xsi:type="string">default</data> <data name="category/data/category_products/dataset" xsi:type="string">catalogProductSimple::default</data> <data name="indexers/0" xsi:type="string">Category Flat Data</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateCategoryEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateCategoryEntityTest.xml index 76d5a532271e..1cda62997e18 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateCategoryEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateCategoryEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Category\UpdateCategoryEntityTest" summary="Update Category" ticketId="MAGETWO-23290"> <variation name="UpdateCategoryEntityTestVariation1_Name_Description_UrlKey_MetaTitle_ExcludeFromMenu"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="category/data/parent_id/dataset" xsi:type="string">default_category</data> <data name="category/data/name" xsi:type="string">Name%isolation%</data> <data name="category/data/include_in_menu" xsi:type="string">No</data> @@ -22,6 +23,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryPage" /> </variation> <variation name="UpdateCategoryEntityTestVariation2_SortProductsBy_DefaultProductSorting_AddProduct"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="category/data/parent_id/dataset" xsi:type="string">default_category</data> <data name="category/data/available_product_listing_config" xsi:type="string">Yes</data> <data name="category/data/default_product_listing_config" xsi:type="string">No</data> @@ -36,6 +38,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryForAssignedProducts" /> </variation> <variation name="UpdateCategoryEntityTestVariation3_MakeInactive"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="category/data/parent_id/dataset" xsi:type="string">default_category</data> <data name="category/data/is_active" xsi:type="string">No</data> <data name="category/data/name" xsi:type="string">Name%isolation%</data> @@ -44,6 +47,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryIsNotActive" /> </variation> <variation name="UpdateCategoryEntityTestVariation4_ChangeCategoryNameOnStoreView" summary="Update Category with custom Store View."> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="category/data/store_id/dataset" xsi:type="string">custom</data> <data name="category/data/use_default_name" xsi:type="string">No</data> <data name="category/data/name" xsi:type="string">Category %isolation%</data> @@ -51,6 +55,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryWithCustomStoreOnFrontend" /> </variation> <variation name="UpdateCategoryEntityTestVariation5_ChangeCategoryUrlOnStoreView" summary="Update URL Key with custom Store View." ticketId="MAGETWO-16471"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="category/data/parent_id/dataset" xsi:type="string">default_category</data> <data name="category/data/store_id/dataset" xsi:type="string">custom</data> <data name="category/data/use_default_url_key" xsi:type="string">No</data> @@ -59,6 +64,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryOnCustomStore" /> </variation> <variation name="UpdateCategoryEntityTestVariation6_CheckCategoryDefaultUrlOnStoreView" summary="Check default URL Key on the custom Store View." ticketId="MAGETWO-64337"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialCategory/dataset" xsi:type="string">default_with_custom_url</data> <data name="category/data/parent_id/dataset" xsi:type="string">default_category</data> <data name="category/data/store_id/dataset" xsi:type="string">custom</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateInactiveCategoryEntityFlatDataTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateInactiveCategoryEntityFlatDataTest.xml index fd2c9afaea16..0c7d88cc920b 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateInactiveCategoryEntityFlatDataTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateInactiveCategoryEntityFlatDataTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Category\UpdateInactiveCategoryEntityFlatDataTest" summary="Update Category if Use Category Flat (Cron is ON, 'Update on Save' Mode)" ticketId="MAGETWO-20169"> <variation name="UpdateInactiveCategoryEntityFlatDataTestVariation1" summary="Inactive category and check that category is absent on frontend"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialCategory/dataset" xsi:type="string">default</data> <data name="category/data/is_active" xsi:type="string">No</data> <data name="indexers/0" xsi:type="string">Category Flat Data</data> @@ -22,6 +23,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryAbsenceOnFrontend" /> </variation> <variation name="UpdateInactiveCategoryEntityFlatDataTestVariation2" summary="Inactive category and check that category is not active on frontend"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialCategory/dataset" xsi:type="string">default</data> <data name="initialCategory/data/is_active" xsi:type="string">No</data> <data name="category/data/is_active" xsi:type="string">No</data> @@ -36,6 +38,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertCategoryIsNotActive" /> </variation> <variation name="UpdateInactiveCategoryEntityFlatDataTestVariation3" summary="Exclude category from navigation menu"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialCategory/dataset" xsi:type="string">default</data> <data name="category/data/include_in_menu" xsi:type="string">No</data> <data name="indexers/0" xsi:type="string">Category Flat Data</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateTopCategoryEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateTopCategoryEntityTest.xml index 9126b619bbfd..e8a5fd355da7 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateTopCategoryEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Category/UpdateTopCategoryEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Category\UpdateTopCategoryEntityTest" summary="Update top category url" ticketId="MAGETWO-27327"> <variation name="UpdateCategoryEntityTestVariation1" summary="Update top category url and do not create redirect"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialCategory/dataset" xsi:type="string">three_nested_categories</data> <data name="nestingLevel" xsi:type="number">3</data> <data name="category/data/url_key" xsi:type="string">cat1-rewrite%isolation%</data> @@ -17,6 +18,7 @@ <constraint name="Magento\UrlRewrite\Test\Constraint\AssertUrlRewritesCategoriesInGrid" /> </variation> <variation name="UpdateCategoryEntityTestVariation2" summary="Update top category url and create redirect"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialCategory/dataset" xsi:type="string">three_nested_categories</data> <data name="nestingLevel" xsi:type="number">3</data> <data name="category/data/url_key" xsi:type="string">cat1-rewrite%isolation%</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/NavigateMenuTest.xml index 260095048431..08bff9f70708 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/NavigateMenuTest.xml @@ -8,21 +8,25 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest9"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Catalog > Products</data> <data name="pageTitle" xsi:type="string">Products</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest10"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Catalog > Categories</data> <data name="pageTitle" xsi:type="string">Default Category (ID: 2)</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest11"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Stores > Product</data> <data name="pageTitle" xsi:type="string">Product Attributes</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest12"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Stores > Attribute Set</data> <data name="pageTitle" xsi:type="string">Attribute Sets</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/AddCompareProductsTest.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/AddCompareProductsTest.php index c700dbc362cb..c40387aba460 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/AddCompareProductsTest.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/AddCompareProductsTest.php @@ -76,7 +76,7 @@ public function tearDown() { $this->cmsIndex->open(); $this->cmsIndex->getLinksBlock()->openLink("Compare Products"); - for ($i = 1; $i <= count($this->products); $i++) { + for ($i = 1, $count = count($this->products); $i <= $count; $i++) { $this->catalogProductCompare->getCompareProductsBlock()->removeProduct(); } } 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 874fc3f67036..b1f093162fa4 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/AddToCartCrossSellTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/AddToCartCrossSellTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Product\AddToCartCrossSellTest" summary="Promote Products as Cross-Sells" ticketId="MAGETWO-12390"> <variation name="AddToCartCrossSellTestVariation1" method="test"> - <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test</data> + <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, mftf_migrated:yes</data> <data name="products" xsi:type="string">simple1::catalogProductSimple::product_with_category,simple2::catalogProductSimple::product_with_category,config1::configurableProduct::two_options_with_fixed_price</data> <data name="promotedProducts" xsi:type="string">simple1:simple2,config1;config1:simple2</data> <data name="navigateProductsOrder" xsi:type="string">simple1,config1,simple2</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateFlatCatalogProduct.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateFlatCatalogProduct.php deleted file mode 100644 index cb5ad93ee429..000000000000 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateFlatCatalogProduct.php +++ /dev/null @@ -1,146 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Catalog\Test\TestCase\Product; - -use Magento\Config\Test\TestStep\SetupConfigurationStep; -use Magento\Catalog\Test\Page\Category\CatalogCategoryView; -use Magento\Mtf\Fixture\FixtureFactory; -use Magento\Mtf\TestCase\Injectable; -use Magento\Catalog\Test\Fixture\Category; - -/** - * Preconditions: - * 1. Use Flat Catalog Category & Use Flat Catalog Product are enabled (Store/Configuration/Catalog/Catalog/Storefront) - * - * Steps: - * 1. Assign 16 or more products to the same category (e.g. 20 products) - * 2. Go to Storefront and navigate to this category - * 3. Click on Page 2 or any further page - * 4. Go back to page 1 and change № of products per page from 9 to any number (e.g 12) - * 5. Click on Page 2 or any further page - * 5. Perform assertions. - * - * @ZephyrId MAGETWO-67570 - */ -class CreateFlatCatalogProduct extends Injectable -{ - /* tags */ - const MVP = 'yes'; - /* end tags */ - - /** - * Configuration data - * - * @var string - */ - private $configData; - - /** - * Factory for Fixtures - * - * @var FixtureFactory - */ - private $fixtureFactory; - - /** - * Category fixture - * - * @var Category - */ - private $category; - - /** - * CatalogCategoryView page - * - * @var CatalogCategoryView - */ - private $catalogCategoryView; - - /** - * Prepare data - * - * @param Category $category - * @return array - */ - public function __prepare(Category $category) - { - $category->persist(); - return [ - 'category' => $category - ]; - } - - /** - * Injection data - * - * @param Category $category - * @param FixtureFactory $fixtureFactory - * @param CatalogCategoryView $catalogCategoryView - * @return void - */ - public function __inject( - Category $category, - FixtureFactory $fixtureFactory, - CatalogCategoryView $catalogCategoryView - ) { - $this->category = $category; - $this->fixtureFactory = $fixtureFactory; - $this->catalogCategoryView = $catalogCategoryView; - } - - /** - * Run create flat catalog product - * - * @param string $configData - * @param string $productsCount - * @return array - */ - public function test($configData, $productsCount) - { - $this->objectManager->create(SetupConfigurationStep::class, ['configData' => $this->configData])->run(); - $this->createBulkOfProducts($productsCount); - $this->configData = $configData; - return ['category' => $this->category, 'catalogCategoryView' => $this->catalogCategoryView]; - } - - /** - * Clear data after test - * - * @return void - */ - public function tearDown() - { - $this->objectManager->create( - SetupConfigurationStep::class, - ['configData' => $this->configData, 'rollback' => true] - )->run(); - } - - /** - * Create products for tests - * - * @param $productsCount - * @return void - */ - private function createBulkOfProducts($productsCount) - { - for ($counter = 1; $counter <= $productsCount; $counter++) { - $product = $this->fixtureFactory->createByCode( - 'catalogProductSimple', - [ - 'dataset' => 'default', - 'data' => [ - 'category_ids' => [ - 'category' => $this->category - ] - ] - ] - ); - $product->persist(); - } - } -} diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateFlatCatalogProduct.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateFlatCatalogProduct.xml deleted file mode 100644 index 17d362f35ec5..000000000000 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateFlatCatalogProduct.xml +++ /dev/null @@ -1,16 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - --> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> - <testCase name="Magento\Catalog\Test\TestCase\Product\CreateFlatCatalogProduct" summary="Create flat catalog Product" ticketId="MAGETWO-67570"> - <variation name="CheckPaginationInStorefront" ticketId="MAGETWO-67570"> - <data name="configData" xsi:type="string">category_flat,product_flat</data> - <data name="productsCount" xsi:type="number">19</data> - <constraint name="Magento\Catalog\Test\Constraint\AssertPaginationCorrectOnStoreFront" /> - </variation> - </testCase> -</config> \ No newline at end of file diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateFlatCatalogProductTest.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateFlatCatalogProductTest.php new file mode 100644 index 000000000000..8f11f31a6dff --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateFlatCatalogProductTest.php @@ -0,0 +1,146 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Test\TestCase\Product; + +use Magento\Config\Test\TestStep\SetupConfigurationStep; +use Magento\Catalog\Test\Page\Category\CatalogCategoryView; +use Magento\Mtf\Fixture\FixtureFactory; +use Magento\Mtf\TestCase\Injectable; +use Magento\Catalog\Test\Fixture\Category; + +/** + * Preconditions: + * 1. Use Flat Catalog Category & Use Flat Catalog Product are enabled (Store/Configuration/Catalog/Catalog/Storefront) + * + * Steps: + * 1. Assign 16 or more products to the same category (e.g. 20 products) + * 2. Go to Storefront and navigate to this category + * 3. Click on Page 2 or any further page + * 4. Go back to page 1 and change № of products per page from 9 to any number (e.g 12) + * 5. Click on Page 2 or any further page + * 5. Perform assertions. + * + * @ZephyrId MAGETWO-67570 + */ +class CreateFlatCatalogProductTest extends Injectable +{ + /* tags */ + const MVP = 'yes'; + /* end tags */ + + /** + * Configuration data + * + * @var string + */ + private $configData; + + /** + * Factory for Fixtures + * + * @var FixtureFactory + */ + private $fixtureFactory; + + /** + * Category fixture + * + * @var Category + */ + private $category; + + /** + * CatalogCategoryView page + * + * @var CatalogCategoryView + */ + private $catalogCategoryView; + + /** + * Prepare data + * + * @param Category $category + * @return array + */ + public function __prepare(Category $category) + { + $category->persist(); + return [ + 'category' => $category + ]; + } + + /** + * Injection data + * + * @param Category $category + * @param FixtureFactory $fixtureFactory + * @param CatalogCategoryView $catalogCategoryView + * @return void + */ + public function __inject( + Category $category, + FixtureFactory $fixtureFactory, + CatalogCategoryView $catalogCategoryView + ) { + $this->category = $category; + $this->fixtureFactory = $fixtureFactory; + $this->catalogCategoryView = $catalogCategoryView; + } + + /** + * Run create flat catalog product + * + * @param string $configData + * @param string $productsCount + * @return array + */ + public function test($configData, $productsCount) + { + $this->objectManager->create(SetupConfigurationStep::class, ['configData' => $this->configData])->run(); + $this->createBulkOfProducts($productsCount); + $this->configData = $configData; + return ['category' => $this->category, 'catalogCategoryView' => $this->catalogCategoryView]; + } + + /** + * Clear data after test + * + * @return void + */ + public function tearDown() + { + $this->objectManager->create( + SetupConfigurationStep::class, + ['configData' => $this->configData, 'rollback' => true] + )->run(); + } + + /** + * Create products for tests + * + * @param $productsCount + * @return void + */ + private function createBulkOfProducts($productsCount) + { + for ($counter = 1; $counter <= $productsCount; $counter++) { + $product = $this->fixtureFactory->createByCode( + 'catalogProductSimple', + [ + 'dataset' => 'default', + 'data' => [ + 'category_ids' => [ + 'category' => $this->category + ] + ] + ] + ); + $product->persist(); + } + } +} diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateFlatCatalogProductTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateFlatCatalogProductTest.xml new file mode 100644 index 000000000000..45161e1471f6 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateFlatCatalogProductTest.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + --> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> + <testCase name="Magento\Catalog\Test\TestCase\Product\CreateFlatCatalogProductTest" summary="Create flat catalog Product" ticketId="MAGETWO-67570"> + <variation name="CheckPaginationInStorefront" ticketId="MAGETWO-67570"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> + <data name="configData" xsi:type="string">category_flat,product_flat</data> + <data name="productsCount" xsi:type="number">19</data> + <constraint name="Magento\Catalog\Test\Constraint\AssertPaginationCorrectOnStoreFront" /> + </variation> + </testCase> +</config> \ No newline at end of file diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityByAttributeMaskSkuTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityByAttributeMaskSkuTest.xml index aecdbc5362fb..bdea332a3af0 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityByAttributeMaskSkuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityByAttributeMaskSkuTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Product\CreateSimpleProductEntityByAttributeMaskSkuTest" summary="Create Simple Product with attribute sku mask" ticketId="MAGETWO-59861"> <variation name="CreateSimpleProductEntityByAttributeMaskSkuTest1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="configData" xsi:type="string">attribute_product_mask_sku</data> <data name="description" xsi:type="string">Create product with country of manufacture attribute sku mask</data> <data name="product/data/url_key" xsi:type="string">simple-product-%isolation%</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityPartOneTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityPartOneTest.xml index 4a3d80b0b609..f97735304baa 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityPartOneTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityPartOneTest.xml @@ -25,6 +25,8 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductInventoryMaxAllowedQty" /> </variation> <variation name="CreateSimpleProductEntityTestVariation13"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> + <data name="issue" xsi:type="string">MAGETWO-48850: Filtering Category Products using scope selector</data> <data name="description" xsi:type="string">Create simple product and check search by sku</data> <data name="product/data/url_key" xsi:type="string">simple-product-%isolation%</data> <data name="product/data/name" xsi:type="string">Simple Product %isolation%</data> @@ -42,6 +44,8 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage" /> </variation> <variation name="CreateSimpleProductEntityTestVariation14"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> + <data name="issue" xsi:type="string">MC-6220: Add products to wishlist from Category page with multiple wishlist enabled</data> <data name="description" xsi:type="string">Create simple product and check visibility in category</data> <data name="product/data/url_key" xsi:type="string">simple-product-%isolation%</data> <data name="product/data/name" xsi:type="string">Simple Product %isolation%</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityPartTwoTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityPartTwoTest.xml index a40c715bb3ac..d6b75e5c3a6e 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityPartTwoTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityPartTwoTest.xml @@ -8,6 +8,8 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Product\CreateSimpleProductEntityPartTwoTest" summary="Create Simple Product" ticketId="MAGETWO-23414"> <variation name="CreateSimpleProductEntityTestVariation23" summary="Create Simple Product with Creating New Category (Required Fields Only)" ticketId="MAGETWO-27293"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> + <data name="issue" xsi:type="string">MC-234: Admin should be able to create category from the product page</data> <data name="product/data/category_ids/new_category" xsi:type="string">yes</data> <data name="product/data/category_ids/dataset" xsi:type="string">default_subcategory_without_url_key</data> <data name="product/data/website_ids/0/dataset" xsi:type="string">default</data> @@ -23,6 +25,8 @@ <constraint name="Magento\UrlRewrite\Test\Constraint\AssertUrlRewriteProductInGrid" /> </variation> <variation name="CreateSimpleProductEntityTestVariation24" summary="Create Simple Product and Assigning It to Category" ticketId="MAGETWO-12514"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> + <data name="issue" xsi:type="string">MAGETWO-23414: Create Simple Product</data> <data name="product/data/url_key" xsi:type="string">simple-product-%isolation%</data> <data name="product/data/name" xsi:type="string">Simple Product %isolation%</data> <data name="product/data/sku" xsi:type="string">simple_sku_%isolation%</data> @@ -79,6 +83,8 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> </variation> <variation name="CreateSimpleProductEntityTestVariation28" summary="Create product with tier price for not logged in customer"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> + <data name="issue" xsi:type="string">MAGETWO-68921: Apply Tier Price to a product</data> <data name="product/data/url_key" xsi:type="string">simple-product-%isolation%</data> <data name="product/data/name" xsi:type="string">Simple Product %isolation%</data> <data name="product/data/sku" xsi:type="string">simple_sku_%isolation%</data> @@ -118,6 +124,8 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductInCart" /> </variation> <variation name="CreateSimpleProductEntityWithTierPriceTestVariation1" summary="Create Simple Product with fixed tier price."> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> + <data name="issue" xsi:type="string">MAGETWO-68921: Apply Tier Price to a product</data> <data name="product/data/url_key" xsi:type="string">simple-product-%isolation%</data> <data name="product/data/name" xsi:type="string">Simple Product %isolation%</data> <data name="product/data/sku" xsi:type="string">simple_sku_%isolation%</data> @@ -127,6 +135,8 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductTierPriceInCart" /> </variation> <variation name="CreateSimpleProductEntityWithTierPriceTestVariation2" summary="Create Simple Product with percentage tier price."> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> + <data name="issue" xsi:type="string">MAGETWO-68921: Apply Tier Price to a product</data> <data name="product/data/url_key" xsi:type="string">simple-product-%isolation%</data> <data name="product/data/name" xsi:type="string">Simple Product %isolation%</data> <data name="product/data/sku" xsi:type="string">simple_sku_%isolation%</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.php index 3ae531ac46c1..4d866f716d70 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.php @@ -79,6 +79,7 @@ public function testCreate( $flushCache = false, $configData = null ) { + $this->configData = $configData; $this->flushCache = $flushCache; diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.xml index 8732157e5c65..840dc0b0812b 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateSimpleProductEntityTest.xml @@ -182,6 +182,8 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage" /> </variation> <variation name="CreateSimpleProductEntityTestVariation10" summary="Create in stock product and check threshold" ticketId="MAGETWO-43345"> + <data name="issue" xsi:type="string">https://github.com/magento-engcom/msi/issues/1620</data> + <data name="tag" xsi:type="string">to_maintain:yes</data> <data name="configData" xsi:type="string">inventory_threshold_5</data> <data name="product/data/url_key" xsi:type="string">simple-product-%isolation%</data> <data name="product/data/name" xsi:type="string">Simple Product %isolation%</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateVirtualProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateVirtualProductEntityTest.xml index 1449e7df0ce6..a9c78117d7b6 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateVirtualProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/CreateVirtualProductEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Product\CreateVirtualProductEntityTest" summary="Create Virtual Product" ticketId="MAGETWO-23417"> <variation name="CreateVirtualProductEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Create product with required fields</data> <data name="product/data/url_key" xsi:type="string">virtual-product-%isolation%</data> <data name="product/data/name" xsi:type="string">VirtualProduct %isolation%</data> @@ -17,7 +18,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> </variation> <variation name="CreateVirtualProductEntityTestVariation2" summary="Create product with tier price"> - <data name="tag" xsi:type="string">test_type:extended_acceptance_test</data> + <data name="tag" xsi:type="string">test_type:extended_acceptance_test, mftf_migrated:yes</data> <data name="product/data/url_key" xsi:type="string">virtual-product-%isolation%</data> <data name="product/data/name" xsi:type="string">VirtualProduct %isolation%</data> <data name="product/data/sku" xsi:type="string">virtual_sku_%isolation%</data> @@ -51,6 +52,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductSearchableBySku" /> </variation> <variation name="CreateVirtualProductEntityTestVariation4"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Create product with tier price for "General" group</data> <data name="product/data/url_key" xsi:type="string">virtual-product-%isolation%</data> <data name="product/data/name" xsi:type="string">VirtualProduct %isolation%</data> @@ -71,6 +73,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductTierPriceOnProductPageWithCustomer" /> </variation> <variation name="CreateVirtualProductEntityTestVariation5"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Create product with custom options suite and import options</data> <data name="product/data/url_key" xsi:type="string">virtual-product-%isolation%</data> <data name="product/data/name" xsi:type="string">VirtualProduct %isolation%</data> @@ -86,6 +89,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductCustomOptionsOnProductPage" /> </variation> <variation name="CreateVirtualProductEntityTestVariation6"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Create product without manage stock</data> <data name="product/data/url_key" xsi:type="string">virtual-product-%isolation%</data> <data name="product/data/name" xsi:type="string">VirtualProduct %isolation%</data> @@ -102,6 +106,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductInStock" /> </variation> <variation name="CreateVirtualProductEntityTestVariation7"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Create product out of stock with tier price</data> <data name="product/data/url_key" xsi:type="string">virtual-product-%isolation%</data> <data name="product/data/name" xsi:type="string">VirtualProduct %isolation%</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/DeleteProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/DeleteProductEntityTest.xml index fc566e855c0f..2d91f4a7024e 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/DeleteProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/DeleteProductEntityTest.xml @@ -10,12 +10,13 @@ <variation name="DeleteProductEntityTestVariation1"> <data name="products" xsi:type="string">catalogProductSimple::default</data> <data name="isRequired" xsi:type="string">Yes</data> - <data name="tag" xsi:type="string">to_maintain:yes</data> + <data name="tag" xsi:type="string">to_maintain:yes, mftf_migrated:yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSuccessDeleteMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductNotInGrid" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductIsNotDisplayingOnFrontend" /> </variation> <variation name="DeleteProductEntityTestVariation2"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="products" xsi:type="string">catalogProductVirtual::default</data> <data name="isRequired" xsi:type="string">Yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSuccessDeleteMessage" /> @@ -23,6 +24,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductIsNotDisplayingOnFrontend" /> </variation> <variation name="DeleteProductEntityTestVariation3"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="products" xsi:type="string">catalogProductSimple::with_one_custom_option</data> <data name="isRequired" xsi:type="string">Yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSuccessDeleteMessage" /> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/DuplicateProductEntityTest.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/DuplicateProductEntityTest.php index 0954727ce132..69d09bdd47f1 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/DuplicateProductEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/DuplicateProductEntityTest.php @@ -90,6 +90,8 @@ public function __prepare( */ public function test($productType) { + $this->markTestIncomplete('https://github.com/magento-engcom/msi/issues/666'); + // Precondition $product = $this->createProduct($productType); 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 33b578672d48..d005c05a9cba 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ManageProductsStockTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ManageProductsStockTest.xml @@ -15,6 +15,7 @@ <constraint name="Magento\Checkout\Test\Constraint\AssertProductQtyInShoppingCart" /> </variation> <variation name="ManageProductsStockTestVariation2" summary="Checking that Out of Stock products are not visible in category" ticketId="MAGETWO-13645"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/dataset" xsi:type="string">product_with_category</data> <data name="product/data/stock_data/manage_stock" xsi:type="string">Yes</data> <data name="product/data/quantity_and_stock_status/qty" xsi:type="string">5</data> @@ -33,6 +34,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductOutOfStock" /> </variation> <variation name="ManageProductsStockTestVariation3" summary="Add In Stock product to cart" ticketId="MAGETWO-13645"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/dataset" xsi:type="string">product_with_category</data> <data name="product/data/stock_data/manage_stock" xsi:type="string">Yes</data> <data name="product/data/quantity_and_stock_status/qty" xsi:type="string">5</data> @@ -51,6 +53,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage" /> </variation> <variation name="ManageProductsStockTestVariation4" summary="Enable displaying of out of stock products in category" ticketId="MAGETWO-13645"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/dataset" xsi:type="string">product_with_category</data> <data name="product/data/stock_data/manage_stock" xsi:type="string">Yes</data> <data name="product/data/quantity_and_stock_status/qty" xsi:type="string">5</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/MassProductUpdateStatusTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/MassProductUpdateStatusTest.xml index fa82fd90268f..6936315a1281 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/MassProductUpdateStatusTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/MassProductUpdateStatusTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Product\MassProductUpdateStatusTest" summary="Update status of Products Using Mass Actions" ticketId="MAGETWO-60847"> <variation name="MassProductStatusUpdateTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialProducts" xsi:type="array"> <item name ="0" xsi:type="string">catalogProductSimple::simple_10_dollar</item> <item name ="1" xsi:type="string">catalogProductSimple::simple_10_dollar</item> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/MassProductUpdateTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/MassProductUpdateTest.xml index 6f3803d832c6..d2fe51ecd810 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/MassProductUpdateTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/MassProductUpdateTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Product\MassProductUpdateTest" summary="Edit Products Using Mass Actions" ticketId="MAGETWO-21128"> <variation name="MassProductPriceUpdateTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="configData" xsi:type="string">product_flat</data> <data name="initialProducts/0" xsi:type="string">catalogProductSimple::simple_10_dollar</data> <data name="initialProducts/1" xsi:type="string">catalogProductSimple::simple_10_dollar</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/NavigateUpSellProductsTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/NavigateUpSellProductsTest.xml index cf52597cfc52..0db197ba3b38 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/NavigateUpSellProductsTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/NavigateUpSellProductsTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Product\NavigateUpSellProductsTest" summary="Promote Products as Up-Sells" ticketId="MAGETWO-12391"> <variation name="NavigateUpSellProductsTestVariation1" method="test"> - <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, stable:no</data> + <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, stable:no, mftf_migrated:yes</data> <data name="products" xsi:type="string">simple1::catalogProductSimple::product_with_category,simple2::catalogProductSimple::product_with_category,config1::configurableProduct::two_options_with_fixed_price</data> <data name="promotedProducts" xsi:type="string">simple1:simple2,config1;config1:simple2</data> <data name="navigateProductsOrder" xsi:type="string">simple1,config1,simple2</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnCreationTest.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnCreationTest.php index 5e871bf6d97a..707a62a44605 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnCreationTest.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnCreationTest.php @@ -10,6 +10,7 @@ use Magento\Catalog\Test\Page\Adminhtml\CatalogProductNew; use Magento\Mtf\Fixture\FixtureFactory; use Magento\Mtf\TestCase\Injectable; +use Magento\Downloadable\Test\Block\Adminhtml\Catalog\Product\Edit\Section\Downloadable; /** * Test Creation for ProductTypeSwitchingOnCreation @@ -75,18 +76,49 @@ public function __inject( * * @param string $createProduct * @param string $product + * @param string $actionName * @return array */ - public function test($createProduct, $product) + public function test(string $createProduct, string $product, string $actionName = null): array { // Steps list($fixture, $dataset) = explode('::', $product); $product = $this->fixtureFactory->createByCode($fixture, ['dataset' => $dataset]); $this->catalogProductIndex->open(); $this->catalogProductIndex->getGridPageActionBlock()->addProduct($createProduct); + if ($actionName) { + $this->performAction($actionName); + } $this->catalogProductNew->getProductForm()->fill($product); $this->catalogProductNew->getFormPageActions()->save($product); return ['product' => $product]; } + + /** + * Perform action. + * + * @param string $actionName + * @return void + */ + private function performAction(string $actionName): void + { + if (method_exists(__CLASS__, $actionName)) { + $this->$actionName(); + } + } + + /** + * Clear downloadable product data. + * + * @return void + */ + private function clearDownloadableData(): void + { + $this->catalogProductNew->getProductForm()->openSection('downloadable_information'); + /** @var Downloadable $downloadableInfoTab */ + $downloadableInfoTab = $this->catalogProductNew->getProductForm()->getSection('downloadable_information'); + $downloadableInfoTab->getDownloadableBlock('Links')->clearDownloadableData(); + $downloadableInfoTab->setIsDownloadable('No'); + } } 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 f45fbc96a738..a563d369f95c 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,20 @@ <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> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> @@ -41,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" /> @@ -70,13 +75,15 @@ <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> <variation name="ProductTypeSwitchingOnCreationTestVariation9"> <data name="createProduct" xsi:type="string">downloadable</data> <data name="product" xsi:type="string">configurableProduct::not_virtual_for_type_switching</data> - <data name="tag" xsi:type="string">to_maintain:yes</data> + <data name="actionName" xsi:type="string">clearDownloadableData</data> + <data name="issue" xsi:type="string">MSI-1624</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> <constraint name="Magento\ConfigurableProduct\Test\Constraint\AssertChildProductsInGrid" /> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.php index 43741393e796..90cd6bdb7632 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.php +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.php @@ -143,5 +143,6 @@ protected function clearDownloadableData() /** @var Downloadable $downloadableInfoTab */ $downloadableInfoTab = $this->catalogProductEdit->getProductForm()->getSection('downloadable_information'); $downloadableInfoTab->getDownloadableBlock('Links')->clearDownloadableData(); + $downloadableInfoTab->setIsDownloadable('No'); } } diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.xml index f3df374a8bac..5fa1cfe5e591 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ProductTypeSwitchingOnUpdateTest.xml @@ -11,7 +11,6 @@ <data name="productOrigin" xsi:type="string">catalogProductSimple::default</data> <data name="product" xsi:type="string">configurableProduct::default</data> <data name="actionName" xsi:type="string">-</data> - <data name="tag" xsi:type="string">to_maintain:yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> <constraint name="Magento\ConfigurableProduct\Test\Constraint\AssertChildProductsInGrid" /> @@ -21,7 +20,6 @@ <constraint name="Magento\ConfigurableProduct\Test\Constraint\AssertChildProductIsNotDisplayedSeparately" /> </variation> <variation name="ProductTypeSwitchingOnUpdateTestVariation2"> - <data name="tag" xsi:type="string">to_maintain:yes</data> <data name="productOrigin" xsi:type="string">catalogProductSimple::default</data> <data name="product" xsi:type="string">catalogProductVirtual::default</data> <data name="actionName" xsi:type="string">-</data> @@ -29,7 +27,6 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> </variation> <variation name="ProductTypeSwitchingOnUpdateTestVariation3"> - <data name="tag" xsi:type="string">stable:no</data> <data name="productOrigin" xsi:type="string">configurableProduct::default</data> <data name="product" xsi:type="string">catalogProductSimple::product_without_category</data> <data name="actionName" xsi:type="string">deleteVariations</data> @@ -40,12 +37,10 @@ <data name="productOrigin" xsi:type="string">configurableProduct::default</data> <data name="product" xsi:type="string">catalogProductVirtual::required_fields</data> <data name="actionName" xsi:type="string">deleteVariations</data> - <data name="tag" xsi:type="string">to_maintain:yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> </variation> <variation name="ProductTypeSwitchingOnUpdateTestVariation5"> - <data name="tag" xsi:type="string">to_maintain:yes</data> <data name="productOrigin" xsi:type="string">catalogProductVirtual::default</data> <data name="product" xsi:type="string">catalogProductSimple::default</data> <data name="actionName" xsi:type="string">-</data> @@ -56,7 +51,6 @@ <data name="productOrigin" xsi:type="string">catalogProductVirtual::default</data> <data name="product" xsi:type="string">configurableProduct::not_virtual_for_type_switching</data> <data name="actionName" xsi:type="string">-</data> - <data name="tag" xsi:type="string">to_maintain:yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> <constraint name="Magento\ConfigurableProduct\Test\Constraint\AssertChildProductsInGrid" /> @@ -69,7 +63,6 @@ <data name="productOrigin" xsi:type="string">catalogProductVirtual::default</data> <data name="product" xsi:type="string">downloadableProduct::default</data> <data name="actionName" xsi:type="string">-</data> - <data name="tag" xsi:type="string">to_maintain:yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableProductForm" /> @@ -81,15 +74,13 @@ <data name="productOrigin" xsi:type="string">downloadableProduct::default</data> <data name="product" xsi:type="string">catalogProductSimple::default</data> <data name="actionName" xsi:type="string">-</data> - <data name="tag" xsi:type="string">to_maintain:yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> </variation> <variation name="ProductTypeSwitchingOnUpdateTestVariation9"> <data name="productOrigin" xsi:type="string">downloadableProduct::default</data> <data name="product" xsi:type="string">configurableProduct::not_virtual_for_type_switching</data> - <data name="actionName" xsi:type="string">-</data> - <data name="tag" xsi:type="string">to_maintain:yes</data> + <data name="actionName" xsi:type="string">clearDownloadableData</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> <constraint name="Magento\ConfigurableProduct\Test\Constraint\AssertChildProductsInGrid" /> @@ -99,7 +90,6 @@ <constraint name="Magento\ConfigurableProduct\Test\Constraint\AssertChildProductIsNotDisplayedSeparately" /> </variation> <variation name="ProductTypeSwitchingOnUpdateTestVariation10"> - <data name="tag" xsi:type="string">stable:no</data> <data name="productOrigin" xsi:type="string">downloadableProduct::default</data> <data name="product" xsi:type="string">catalogProductVirtual::default</data> <data name="actionName" xsi:type="string">clearDownloadableData</data> @@ -110,7 +100,6 @@ <data name="productOrigin" xsi:type="string">catalogProductSimple::default</data> <data name="product" xsi:type="string">downloadableProduct::default</data> <data name="actionName" xsi:type="string">-</data> - <data name="tag" xsi:type="string">to_maintain:yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductInGrid" /> <constraint name="Magento\Downloadable\Test\Constraint\AssertDownloadableProductForm" /> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateSimpleProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateSimpleProductEntityTest.xml index 2a46abdc2fd1..ce99a61c33ba 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateSimpleProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateSimpleProductEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Product\UpdateSimpleProductEntityTest" summary="Update Simple Product" ticketId="MAGETWO-23544"> <variation name="UpdateSimpleProductEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Update visibility to Catalog, Search</data> <data name="initialProduct/dataset" xsi:type="string">product_with_category</data> <data name="product/data/name" xsi:type="string">Test simple product %isolation%</data> @@ -24,6 +25,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage" /> </variation> <variation name="UpdateSimpleProductEntityTestVariation2"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Update visibility to Not Visible Individually</data> <data name="initialProduct/dataset" xsi:type="string">product_with_category</data> <data name="product/data/name" xsi:type="string">Test simple product %isolation%</data> @@ -38,6 +40,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductIsNotDisplayingOnFrontend" /> </variation> <variation name="UpdateSimpleProductEntityTestVariation3"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Update visibility to Catalog</data> <data name="initialProduct/dataset" xsi:type="string">product_with_category</data> <data name="product/data/name" xsi:type="string">Test simple product %isolation%</data> @@ -55,6 +58,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage" /> </variation> <variation name="UpdateSimpleProductEntityTestVariation4"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Update visibility to Search</data> <data name="initialProduct/dataset" xsi:type="string">product_with_category</data> <data name="product/data/name" xsi:type="string">Test simple product %isolation%</data> @@ -72,6 +76,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductSearchableBySku" /> </variation> <variation name="UpdateSimpleProductEntityTestVariation5"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Update stock to Out of Stock</data> <data name="initialProduct/dataset" xsi:type="string">product_with_category</data> <data name="product/data/name" xsi:type="string">Test simple product %isolation%</data> @@ -89,6 +94,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductSearchableBySku" /> </variation> <variation name="UpdateSimpleProductEntityTestVariation6"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Update product status to offline</data> <data name="initialProduct/dataset" xsi:type="string">product_with_category</data> <data name="product/data/name" xsi:type="string">Test simple product %isolation%</data> @@ -103,6 +109,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductIsNotDisplayingOnFrontend" /> </variation> <variation name="UpdateSimpleProductEntityTestVariation7"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Update category</data> <data name="initialProduct/dataset" xsi:type="string">product_with_category</data> <data name="product/data/category_ids/dataset" xsi:type="string">default</data> @@ -118,6 +125,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductVisibleInCategory" /> </variation> <variation name="UpdateSimpleProductEntityTestVariation8" summary="Edit Simple Product" ticketId="MAGETWO-12428"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialProduct/dataset" xsi:type="string">product_with_category</data> <data name="product/data/category_ids/dataset" xsi:type="string">default</data> <data name="product/data/name" xsi:type="string">Test simple product %isolation%</data> @@ -128,13 +136,14 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage" /> </variation> <variation name="UpdateSimpleProductEntityTestVariation9" summary="Unassign Products from the Category" ticketId="MAGETWO-12417"> - <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test</data> + <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, mftf_migrated:yes</data> <data name="initialProduct/dataset" xsi:type="string">product_with_category</data> <data name="product/data/category_ids/dataset" xsi:type="string"> -</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSaveMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductNotVisibleInCategory" /> </variation> <variation name="EditSimpleProductTestVariation10" summary="Edit product with enabled flat" ticketId="MAGETWO-21125"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="configData" xsi:type="string">product_flat</data> <data name="initialProduct/dataset" xsi:type="string">simple_10_dollar</data> <data name="product/data/name" xsi:type="string">Simple Product %isolation%</data> @@ -146,6 +155,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductInCategory" /> </variation> <variation name="EditSimpleProductTestVariation11" summary="Update simple product with custom option"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialProduct/dataset" xsi:type="string">product_with_category</data> <data name="product/data/name" xsi:type="string">Test simple product %isolation%</data> <data name="product/data/sku" xsi:type="string">test_simple_product_%isolation%</data> @@ -160,6 +170,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductInCart" /> </variation> <variation name="UpdateSimpleProductEntityTestVariation12" summary="Verify data overriding on Store View level" ticketId="MAGETWO-50640"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialProduct/dataset" xsi:type="string">product_with_category</data> <data name="store/dataset" xsi:type="string">custom</data> <data name="product/data/use_default_name" xsi:type="string">No</data> @@ -168,7 +179,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductNameOnDifferentStoreViews" /> </variation> <variation name="UpdateSimpleProductEntityTestVariation13" summary="Price overriding on Store View level" ticketId="MAGETWO-58861"> - <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test</data> + <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, mftf_migrated:yes</data> <data name="configData" xsi:type="string">price_scope_website</data> <data name="initialProduct/dataset" xsi:type="string">product_with_category</data> <data name="store/dataset" xsi:type="string">custom</data> @@ -178,6 +189,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductPriceOnDifferentStoreViews" /> </variation> <variation name="UpdateSimpleProductEntityTestVariation14" summary="An error appears on open tier price with locale formatting" ticketId="MAGETWO-62076"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialProduct/dataset" xsi:type="string">simple_with_hight_tier_price</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductFormattingTierPrice" /> </variation> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateVirtualProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateVirtualProductEntityTest.xml index daa19a8fe6fe..e2715bb6b4b8 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateVirtualProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/UpdateVirtualProductEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Product\UpdateVirtualProductEntityTest" summary="Update Virtual Product" ticketId="MAGETWO-26204"> <variation name="UpdateVirtualProductEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/data/name" xsi:type="string">VirtualProduct %isolation%</data> <data name="product/data/sku" xsi:type="string">virtual_sku_%isolation%</data> <data name="product/data/price/value" xsi:type="string">99.99</data> @@ -28,6 +29,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductSearchableBySku" /> </variation> <variation name="UpdateVirtualProductEntityTestVariation2"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/data/name" xsi:type="string">virtual_product_%isolation%</data> <data name="product/data/sku" xsi:type="string">virtual_sku_%isolation%</data> <data name="product/data/price/value" xsi:type="string">120.00</data> @@ -47,6 +49,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductSearchableBySku" /> </variation> <variation name="UpdateVirtualProductEntityTestVariation3"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/data/name" xsi:type="string">VirtualProduct %isolation%</data> <data name="product/data/sku" xsi:type="string">virtual_sku_%isolation%</data> <data name="product/data/price/value" xsi:type="string">185.00</data> @@ -67,6 +70,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductSearchableBySku" /> </variation> <variation name="UpdateVirtualProductEntityTestVariation4"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/data/name" xsi:type="string">virtual_product_%isolation%</data> <data name="product/data/sku" xsi:type="string">virtual_sku_%isolation%</data> <data name="product/data/price/value" xsi:type="string">99.99</data> @@ -82,6 +86,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductSearchableBySku" /> </variation> <variation name="UpdateVirtualProductEntityTestVariation5"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/data/name" xsi:type="string">VirtualProduct %isolation%</data> <data name="product/data/sku" xsi:type="string">virtual_sku_%isolation%</data> <data name="product/data/price/value" xsi:type="string">5.00</data> @@ -97,6 +102,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductSearchableBySku" /> </variation> <variation name="UpdateVirtualProductEntityTestVariation6"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/data/name" xsi:type="string">virtual_product_%isolation%</data> <data name="product/data/sku" xsi:type="string">virtual_sku_%isolation%</data> <data name="product/data/price/value" xsi:type="string">145.00</data> @@ -116,6 +122,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductSearchableBySku" /> </variation> <variation name="UpdateVirtualProductEntityTestVariation7"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/data/name" xsi:type="string">VirtualProduct %isolation%</data> <data name="product/data/sku" xsi:type="string">virtual_sku_%isolation%</data> <data name="product/data/price/value" xsi:type="string">99.99</data> @@ -133,6 +140,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductSearchableBySku" /> </variation> <variation name="UpdateVirtualProductEntityTestVariation8"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/data/name" xsi:type="string">virtual_product_%isolation%</data> <data name="product/data/sku" xsi:type="string">virtual_sku_%isolation%</data> <data name="product/data/price/value" xsi:type="string">5.00</data> @@ -149,6 +157,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductSearchableBySku" /> </variation> <variation name="UpdateVirtualProductEntityTestVariation9"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/data/name" xsi:type="string">VirtualProduct %isolation%</data> <data name="product/data/sku" xsi:type="string">virtual_sku_%isolation%</data> <data name="product/data/price/value" xsi:type="string">120.00</data> @@ -168,6 +177,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductSearchableBySku" /> </variation> <variation name="UpdateVirtualProductEntityTestVariation10"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/data/name" xsi:type="string">VirtualProduct %isolation%</data> <data name="product/data/sku" xsi:type="string">virtual_sku_%isolation%</data> <data name="product/data/price/value" xsi:type="string">99.99</data> @@ -183,6 +193,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductSearchableBySku" /> </variation> <variation name="UpdateVirtualProductEntityTestVariation11"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/data/name" xsi:type="string">VirtualProduct %isolation%</data> <data name="product/data/sku" xsi:type="string">virtual_sku_%isolation%</data> <data name="product/data/price/value" xsi:type="string">99.99</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ValidateOrderOfProductTypeTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/Product/ValidateOrderOfProductTypeTest.xml index 0d1a34ab52f9..ec776128fdfe 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 86bacd925ba0..13e05b1d122c 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 73fbf556d099..e15eab57cca0 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 aae9ec6039f5..49725d08b63e 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/CreateProductAttributeEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/CreateProductAttributeEntityTest.xml @@ -8,7 +8,6 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\ProductAttribute\CreateProductAttributeEntityTest" summary="Create Product Attribute" ticketId="MAGETWO-24767"> <variation name="CreateProductAttributeEntityTestVariation1"> - <data name="tag" xsi:type="string">to_maintain:yes</data> <data name="attributeSet/dataset" xsi:type="string">custom_attribute_set</data> <data name="productAttribute/data/frontend_label" xsi:type="string">Text_Field_Admin_%isolation%</data> <data name="productAttribute/data/frontend_input" xsi:type="string">Text Field</data> @@ -20,6 +19,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertAddedProductAttributeOnProductForm" /> </variation> <variation name="CreateProductAttributeEntityTestVariation2" summary="Create custom text attribute product field" ticketId="MAGETWO-17475"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="attributeSet/dataset" xsi:type="string">custom_attribute_set</data> <data name="productAttribute/data/frontend_label" xsi:type="string">Text_Field_Admin_%isolation%</data> <data name="productAttribute/data/frontend_input" xsi:type="string">Text Area</data> @@ -59,7 +59,6 @@ <data name="productAttribute/data/used_in_product_listing" xsi:type="string">Yes</data> <data name="productAttribute/data/is_used_for_promo_rules" xsi:type="string">Yes</data> <data name="productAttribute/data/used_for_sort_by" xsi:type="string">Yes</data> - <data name="tag" xsi:type="string">to_maintain:yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductAttributeInGrid" /> <constraint name="Magento\Catalog\Test\Constraint\AssertAttributeForm" /> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchProductByAttribute" /> @@ -68,7 +67,6 @@ <constraint name="Magento\CatalogRule\Test\Constraint\AssertProductAttributeIsUsedPromoRules" /> </variation> <variation name="CreateProductAttributeEntityTestVariation4"> - <data name="tag" xsi:type="string">to_maintain:yes</data> <data name="attributeSet/dataset" xsi:type="string">custom_attribute_set</data> <data name="productAttribute/data/frontend_label" xsi:type="string">Yes/No_Admin_%isolation%</data> <data name="productAttribute/data/frontend_input" xsi:type="string">Yes/No</data> @@ -85,6 +83,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertAddedProductAttributeOnProductForm" /> </variation> <variation name="CreateProductAttributeEntityTestVariation5" summary="Create custom multiple select attribute product field" ticketId="MAGETWO-14862"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="attributeSet/dataset" xsi:type="string">custom_attribute_set</data> <data name="productAttribute/data/frontend_label" xsi:type="string">Multiple_Select_Admin_%isolation%</data> <data name="productAttribute/data/frontend_input" xsi:type="string">Multiple Select</data> @@ -101,7 +100,6 @@ <data name="productAttribute/data/is_html_allowed_on_front" xsi:type="string">Yes</data> <data name="productAttribute/data/is_visible_on_front" xsi:type="string">Yes</data> <data name="productAttribute/data/used_in_product_listing" xsi:type="string">Yes</data> - <data name="tag" xsi:type="string">to_maintain:yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductAttributeInGrid" /> <constraint name="Magento\Catalog\Test\Constraint\AssertAttributeForm" /> <constraint name="Magento\Catalog\Test\Constraint\AssertAddedProductAttributeOnProductForm" /> @@ -115,7 +113,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertAttributeOptionsOnProductForm" /> </variation> <variation name="CreateProductAttributeEntityTestVariation6" summary="Create custom dropdown attribute product field" ticketId="MAGETWO-17475, MAGETWO-14862"> - <data name="tag" xsi:type="string">test_type:extended_acceptance_test</data> + <data name="tag" xsi:type="string">test_type:extended_acceptance_test, mftf_migrated:yes</data> <data name="attributeSet/dataset" xsi:type="string">custom_attribute_set</data> <data name="productAttribute/data/frontend_label" xsi:type="string">Dropdown_Admin_%isolation%</data> <data name="productAttribute/data/frontend_input" xsi:type="string">Dropdown</data> @@ -154,6 +152,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertAttributeOptionsOnProductForm" /> </variation> <variation name="CreateProductAttributeEntityTestVariation7" summary="Create custom price attribute product field" ticketId="MAGETWO-17475"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="attributeSet/dataset" xsi:type="string">custom_attribute_set</data> <data name="productAttribute/data/frontend_label" xsi:type="string">Price_Admin_%isolation%</data> <data name="productAttribute/data/frontend_input" xsi:type="string">Price</data> @@ -176,7 +175,6 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductAttributeIsFilterableInSearch" /> </variation> <variation name="CreateProductAttributeEntityTestVariation8"> - <data name="tag" xsi:type="string">to_maintain:yes</data> <data name="attributeSet/dataset" xsi:type="string">custom_attribute_set</data> <data name="productAttribute/data/frontend_label" xsi:type="string">Fixed_Product_Tax_Admin_%isolation%</data> <data name="productAttribute/data/frontend_input" xsi:type="string">Fixed Product Tax</data> @@ -193,7 +191,6 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertAddedProductAttributeOnProductForm" /> </variation> <variation name="CreateProductAttributeEntityTestVariation9"> - <data name="tag" xsi:type="string">to_maintain:yes</data> <data name="attributeSet/dataset" xsi:type="string">custom_attribute_set</data> <data name="productAttribute/data/frontend_label" xsi:type="string">Text_Field_Admin_%isolation%</data> <data name="productAttribute/data/frontend_input" xsi:type="string">Text Field</data> @@ -209,6 +206,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductAttributeIsUnique" /> </variation> <variation name="CreateProductAttributeEntityTestVariation10" summary="Create custom dropdown attribute with single quotation in option"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="attributeSet/dataset" xsi:type="string">custom_attribute_set</data> <data name="productAttribute/data/frontend_label" xsi:type="string">Dropdown_Admin_%isolation%</data> <data name="productAttribute/data/frontend_input" xsi:type="string">Dropdown</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteAssignedToTemplateProductAttributeTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteAssignedToTemplateProductAttributeTest.xml index b12f4f1ad7d9..d674535b54ab 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteAssignedToTemplateProductAttributeTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteAssignedToTemplateProductAttributeTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\ProductAttribute\DeleteAssignedToTemplateProductAttributeTest" summary="Delete Assigned to Template Product Attribute" ticketId="MAGETWO-26011"> <variation name="DeleteAssignedToTemplateProductAttributeTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="attributeSet/dataset" xsi:type="string">custom_attribute_set</data> <data name="attributeSet/data/assigned_attributes/dataset" xsi:type="string">attribute_type_dropdown</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductAttributeSuccessDeleteMessage" /> @@ -16,6 +17,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductAttributeAbsenceInUnassignedAttributes" /> </variation> <variation name="DeleteAssignedToTemplateProductAttributeTestVariation2"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="attributeSet/dataset" xsi:type="string">default</data> <data name="attributeSet/data/assigned_attributes/dataset" xsi:type="string">attribute_type_text_field</data> <data name="assertProduct/data/name" xsi:type="string">Product name</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteAttributeSetTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteAttributeSetTest.xml index 361f2acb1d8a..a83fce14d938 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteAttributeSetTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteAttributeSetTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\ProductAttribute\DeleteAttributeSetTest" summary="Delete Attribute Set (Attribute Set)" ticketId="MAGETWO-25473"> <variation name="DeleteAttributeSetTestVariation1"> - <data name="tag" xsi:type="string">stable:no</data> + <data name="tag" xsi:type="string">stable:no, mftf_migrated:yes</data> <data name="attributeSet/dataset" xsi:type="string">custom_attribute_set</data> <data name="attributeSet/data/assigned_attributes/dataset" xsi:type="string">default</data> <data name="product/dataset" xsi:type="string">default</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteProductAttributeEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteProductAttributeEntityTest.xml index 6cca4b3f3685..0dd47942c8fe 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteProductAttributeEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteProductAttributeEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\ProductAttribute\DeleteProductAttributeEntityTest" summary="Delete Product Attribute" ticketId="MAGETWO-24998"> <variation name="DeleteProductAttributeEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="attribute/dataset" xsi:type="string">attribute_type_text_field</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductAttributeSuccessDeleteMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductAttributeAbsenceInGrid" /> @@ -15,7 +16,6 @@ <constraint name="Magento\ImportExport\Test\Constraint\AssertProductAttributeAbsenceForExport" /> </variation> <variation name="DeleteProductAttributeEntityTestVariation2"> - <data name="tag" xsi:type="string">stable:no</data> <data name="attribute/dataset" xsi:type="string">attribute_type_dropdown</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductAttributeSuccessDeleteMessage" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductAttributeAbsenceInGrid" /> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteSystemProductAttributeTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteSystemProductAttributeTest.xml index 7763b0fb534f..a9a85d247207 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 df396c22312b..e46174de8818 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteUsedInConfigurableProductAttributeTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/DeleteUsedInConfigurableProductAttributeTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\ProductAttribute\DeleteUsedInConfigurableProductAttributeTest" summary="Delete Used in Configurable Product Attribute" ticketId="MAGETWO-26652"> <variation name="DeleteUsedInConfigurableProductAttributeTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/dataset" xsi:type="string">one_variation</data> <constraint name="Magento\Catalog\Test\Constraint\AssertUsedSuperAttributeImpossibleDeleteMessages" /> <constraint name="Magento\Catalog\Test\Constraint\AssertProductAttributeInGrid" /> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/UpdateAttributeSetTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/UpdateAttributeSetTest.xml index 18b9a199cac0..18dc5c983ad7 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/UpdateAttributeSetTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/UpdateAttributeSetTest.xml @@ -8,7 +8,6 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\ProductAttribute\UpdateAttributeSetTest" summary="Update Attribute Set" ticketId="MAGETWO-26251"> <variation name="UpdateAttributeSetTestVariation1"> - <data name="tag" xsi:type="string">stable:no</data> <data name="attributeSet/data/attribute_set_name" xsi:type="string">AttributeSetEdit1%isolation%</data> <data name="attributeSet/data/group" xsi:type="string">Custom-group%isolation%</data> <data name="attributeSetOriginal/dataset" xsi:type="string">custom_attribute_set</data> diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/UpdateProductAttributeEntityTest.xml b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/UpdateProductAttributeEntityTest.xml index b051d50b4acb..d6420ff431ce 100644 --- a/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/UpdateProductAttributeEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/TestCase/ProductAttribute/UpdateProductAttributeEntityTest.xml @@ -8,7 +8,6 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\ProductAttribute\UpdateProductAttributeEntityTest" summary="Update Product Attribute" ticketId="MAGETWO-23459"> <variation name="UpdateProductAttributeEntityTestVariation1"> - <data name="tag" xsi:type="string">to_maintain:yes</data> <data name="attributeSet/dataset" xsi:type="string">custom_attribute_set</data> <data name="productAttributeOriginal/dataset" xsi:type="string">attribute_type_text_field</data> <data name="attribute/data/frontend_label" xsi:type="string">Text_Field_%isolation%</data> @@ -29,7 +28,6 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertAddedProductAttributeOnProductForm" /> </variation> <variation name="UpdateProductAttributeEntityTestVariation2"> - <data name="tag" xsi:type="string">to_maintain:yes</data> <data name="attributeSet/dataset" xsi:type="string">custom_attribute_set</data> <data name="productAttributeOriginal/dataset" xsi:type="string">attribute_type_dropdown</data> <data name="attribute/data/frontend_label" xsi:type="string">Dropdown_%isolation%</data> @@ -55,6 +53,8 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertAddedProductAttributeOnProductForm" /> </variation> <variation name="UpdateProductAttributeEntityTestVariation3"> + <data name="issue" xsi:type="string">MAGETWO-46494: [FT] UpdateProductAttributeEntityTestVariation3 does not actually create an attribute to check</data> + <data name="tag" xsi:type="string">to_maintain:yes</data> <data name="attributeSet/dataset" xsi:type="string">custom_attribute_set</data> <data name="productAttributeOriginal/dataset" xsi:type="string">tax_class_id</data> <data name="attribute/data/is_searchable" xsi:type="string">Yes</data> @@ -62,11 +62,11 @@ <data name="cacheTags" xsi:type="array"> <item name="0" xsi:type="string">FPC</item> </data> - <data name="tag" xsi:type="string">to_maintain:yes</data> <constraint name="Magento\PageCache\Test\Constraint\AssertCacheIsRefreshableAndInvalidated" /> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchProductByAttribute" /> </variation> <variation name="UpdateProductAttributeEntityTestVariation4" summary="Create product attribute of type Dropdown and check its visibility on frontend in Advanced Search form" ticketId="MAGETWO-12941"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="attributeSet/dataset" xsi:type="string">custom_attribute_set</data> <data name="productAttributeOriginal/dataset" xsi:type="string">attribute_type_dropdown</data> <data name="attribute/data/frontend_input" xsi:type="string">Dropdown</data> @@ -76,6 +76,7 @@ <constraint name="Magento\CatalogSearch\Test\Constraint\AssertAdvancedSearchAttributeIsAbsent" /> </variation> <variation name="UpdateProductAttributeEntityTestVariation5" summary="Create product attribute of type Multiple Select and check its visibility on frontend in Advanced Search form" ticketId="MAGETWO-12941"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="attributeSet/dataset" xsi:type="string">custom_attribute_set</data> <data name="productAttributeOriginal/dataset" xsi:type="string">attribute_type_multiple_select</data> <data name="attribute/data/frontend_label" xsi:type="string">Dropdown_%isolation%</data> diff --git a/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ExportProductsTest.php b/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ExportProductsTest.php index 9e246939595c..b5cd056fb99a 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ExportProductsTest.php +++ b/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ExportProductsTest.php @@ -11,6 +11,7 @@ use Magento\Mtf\Util\Command\File\Export; use Magento\Mtf\Fixture\FixtureFactory; use Magento\Mtf\TestCase\Injectable; +use Magento\Mtf\Util\Command\Cli\Cron; /** * Preconditions: @@ -50,22 +51,32 @@ class ExportProductsTest extends Injectable */ private $assertExportProduct; + /** + * Cron command + * + * @var Cron + */ + private $cron; + /** * Inject data. * * @param FixtureFactory $fixtureFactory * @param AdminExportIndex $adminExportIndex * @param AssertExportProduct $assertExportProduct + * @param Cron $cron * @return void */ public function __inject( FixtureFactory $fixtureFactory, AdminExportIndex $adminExportIndex, - AssertExportProduct $assertExportProduct + AssertExportProduct $assertExportProduct, + Cron $cron ) { $this->fixtureFactory = $fixtureFactory; $this->adminExportIndex = $adminExportIndex; $this->assertExportProduct = $assertExportProduct; + $this->cron = $cron; } /** @@ -83,14 +94,18 @@ public function test( array $exportedFields, array $products ) { + $this->cron->run(); + $this->cron->run(); $products = $this->prepareProducts($products); $this->adminExportIndex->open(); + $this->adminExportIndex->getExportedGrid()->deleteAllExportedFiles(); $exportData = $this->fixtureFactory->createByCode('exportData', ['dataset' => $exportData]); $exportData->persist(); $this->adminExportIndex->getExportForm()->fill($exportData); $this->adminExportIndex->getFilterExport()->clickContinue(); - + $this->cron->run(); + $this->cron->run(); $this->assertExportProduct->processAssert($export, $exportedFields, $products); } diff --git a/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ExportProductsTest.xml b/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ExportProductsTest.xml index 40f535cd225a..be22eab8ac71 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ExportProductsTest.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ExportProductsTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\CatalogImportExport\Test\TestCase\ExportProductsTest" summary="Export products"> <variation name="ExportProductsTestVariation1" summary="Export simple product and configured products with assigned images" ticketId="MAGETWO-46112"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="exportData" xsi:type="string">default</data> <data name="products/0" xsi:type="array"> <item name="fixture" xsi:type="string">catalogProductSimple</item> @@ -27,6 +28,7 @@ </data> </variation> <variation name="ExportProductsTestVariation2" summary="Export simple and configured products with custom options" ticketId="MAGETWO-46113, MAGETWO-46109"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="exportData" xsi:type="string">default</data> <data name="products/0" xsi:type="array"> <item name="fixture" xsi:type="string">catalogProductSimple</item> @@ -43,6 +45,7 @@ <constraint name="Magento\CatalogImportExport\Test\Constraint\AssertExportProductDate" /> </variation> <variation name="ExportProductsTestVariation3" summary="Export simple product with custom attribute" ticketId="MAGETWO-46121"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="exportData" xsi:type="string">default</data> <data name="products/0" xsi:type="array"> <item name="fixture" xsi:type="string">catalogProductSimple</item> @@ -58,6 +61,7 @@ </data> </variation> <variation name="ExportProductsTestVariation5" summary="Export simple product assigned to Main Website and configurable product assigned to Custom Website" ticketId="MAGETWO-46114"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="exportData" xsi:type="string">default</data> <data name="products/0" xsi:type="array"> <item name="fixture" xsi:type="string">catalogProductSimple</item> diff --git a/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ImportProductsTest.xml b/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ImportProductsTest.xml index edb0aad954fb..77e5e2b91d93 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ImportProductsTest.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogImportExport/Test/TestCase/ImportProductsTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\CatalogImportExport\Test\TestCase\ImportProductsTest" summary="Import products"> <variation name="ImportProductVariation1" ticketId="MAGETWO-47724" summary="Import Products with Add/Update Behavior"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="import/data" xsi:type="array"> <item name="entity" xsi:type="string">Products</item> <item name="behavior" xsi:type="string">Add/Update</item> @@ -38,6 +39,7 @@ <constraint name="Magento\CatalogImportExport\Test\Constraint\AssertProductsInGrid" /> </variation> <variation name="ImportProductVariation2" ticketId="MAGETWO-47719" summary="Import Products assigned to different websites with Replace Behavior"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="import/data/entity" xsi:type="string">Products</data> <data name="import/data/behavior" xsi:type="string">Replace</data> <data name="import/data/validation_strategy" xsi:type="string">Stop on Error</data> diff --git a/dev/tests/functional/tests/app/Magento/CatalogRule/Test/Constraint/AssertCatalogPriceRuleForm.php b/dev/tests/functional/tests/app/Magento/CatalogRule/Test/Constraint/AssertCatalogPriceRuleForm.php index 7db32337b995..17739f5524e1 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogRule/Test/Constraint/AssertCatalogPriceRuleForm.php +++ b/dev/tests/functional/tests/app/Magento/CatalogRule/Test/Constraint/AssertCatalogPriceRuleForm.php @@ -42,10 +42,10 @@ public function processAssert( $fixtureData = $catalogPriceRule->getData(); //convert discount_amount to float to compare if (isset($formData['discount_amount'])) { - $formData['discount_amount'] = floatval($formData['discount_amount']); + $formData['discount_amount'] = (float)$formData['discount_amount']; } if (isset($fixtureData['discount_amount'])) { - $fixtureData['discount_amount'] = floatval($fixtureData['discount_amount']); + $fixtureData['discount_amount'] = (float)$fixtureData['discount_amount']; } $diff = $this->verifyData($formData, $fixtureData); \PHPUnit\Framework\Assert::assertTrue( diff --git a/dev/tests/functional/tests/app/Magento/CatalogRule/Test/TestCase/ApplyCatalogPriceRulesTest.xml b/dev/tests/functional/tests/app/Magento/CatalogRule/Test/TestCase/ApplyCatalogPriceRulesTest.xml index 1fad2f282fa5..719e9bb95694 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogRule/Test/TestCase/ApplyCatalogPriceRulesTest.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogRule/Test/TestCase/ApplyCatalogPriceRulesTest.xml @@ -106,6 +106,8 @@ <data name="productPrice/0/special" xsi:type="string">5</data> <data name="productPrice/0/sub_total" xsi:type="string">5</data> <data name="productPrice/0/regular" xsi:type="string">10</data> + <data name="shipping/shipping_service" xsi:type="string">Flat Rate</data> + <data name="shipping/shipping_method" xsi:type="string">Fixed</data> <constraint name="Magento\CatalogRule\Test\Constraint\AssertCatalogPriceRuleNotAppliedCatalogPage" /> <constraint name="Magento\CatalogRule\Test\Constraint\AssertCatalogPriceRuleNotAppliedProductPage" /> <constraint name="Magento\CatalogRule\Test\Constraint\AssertCatalogPriceRuleNotAppliedShoppingCart" /> diff --git a/dev/tests/functional/tests/app/Magento/CatalogRule/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/CatalogRule/Test/TestCase/NavigateMenuTest.xml index 659d76eabccb..4a965d570894 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogRule/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogRule/Test/TestCase/NavigateMenuTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest14"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Marketing > Catalog Price Rule</data> <data name="pageTitle" xsi:type="string">Catalog Price Rule</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/Fixture/CatalogSearchQuery/QueryText.php b/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/Fixture/CatalogSearchQuery/QueryText.php index e2193b799c3b..11a8693723f2 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/Fixture/CatalogSearchQuery/QueryText.php +++ b/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/Fixture/CatalogSearchQuery/QueryText.php @@ -61,7 +61,7 @@ private function createProducts(FixtureFactory $fixtureFactory, $productsData) $searchValue = isset($productData[2]) ? $productData[2] : $productData[1]; if ($this->data === null) { if ($product->hasData($searchValue)) { - $getProperty = 'get' . str_replace(' ', '', ucwords(str_replace('_', ' ', $searchValue))); + $getProperty = 'get' . str_replace('_', '', ucwords($searchValue, '_')); $this->data = $product->$getProperty(); } else { $this->data = $searchValue; diff --git a/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/CreateSearchTermEntityTest.xml b/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/CreateSearchTermEntityTest.xml index 0437e0a5e999..8c465544a328 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/CreateSearchTermEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/CreateSearchTermEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\CatalogSearch\Test\TestCase\CreateSearchTermEntityTest" summary="Create Search Term" ticketId="MAGETWO-26165"> <variation name="CreateSearchTermEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="searchTerm/data/query_text/value" xsi:type="string">catalogProductSimple::sku</data> <data name="searchTerm/data/store_id" xsi:type="string">Main Website/Main Website Store/Default Store View</data> <data name="searchTerm/data/redirect" xsi:type="string">http://example.com/</data> diff --git a/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/DeleteSearchTermEntityTest.xml b/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/DeleteSearchTermEntityTest.xml index a9cc0dfd34f9..8fdd7ef71552 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/DeleteSearchTermEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/DeleteSearchTermEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\CatalogSearch\Test\TestCase\DeleteSearchTermEntityTest" summary="Delete Search Term" ticketId="MAGETWO-26491"> <variation name="DeleteSearchTermEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="searchTerm/dataset" xsi:type="string">default</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertSearchTermSuccessDeleteMessage" /> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertSearchTermNotInGrid" /> diff --git a/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/MassDeleteSearchTermEntityTest.xml b/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/MassDeleteSearchTermEntityTest.xml index 3bf4e521c4a0..3ef2b65c0224 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/MassDeleteSearchTermEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/MassDeleteSearchTermEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\CatalogSearch\Test\TestCase\MassDeleteSearchTermEntityTest" summary="Mass Delete Search Term" ticketId="MAGETWO-26599"> <variation name="MassDeleteSearchTermEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="searchTerms" xsi:type="string">catalogSearchQuery::default,catalogSearchQuery::default,catalogSearchQuery::default</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertSearchTermSuccessMassDeleteMessage" /> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertSearchTermMassActionsNotInGrid" /> diff --git a/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/NavigateMenuTest.xml index 493d427dd0ac..d9bb0f65e704 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/NavigateMenuTest.xml @@ -8,11 +8,13 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest15"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Marketing > Search Terms</data> <data name="pageTitle" xsi:type="string">Search Terms</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest16"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > Search Terms</data> <data name="pageTitle" xsi:type="string">Search Terms Report</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/SearchEntityResultsTest.xml b/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/SearchEntityResultsTest.xml index 9a6a66091d42..42dd7b6c96e2 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/SearchEntityResultsTest.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogSearch/Test/TestCase/SearchEntityResultsTest.xml @@ -8,66 +8,76 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\CatalogSearch\Test\TestCase\SearchEntityResultsTest" summary="Use Quick Search to Find Product" ticketId="MAGETWO-25095"> <variation name="SearchEntityResultsTestVariation1" summary="Use Quick Search to Find the Product" ticketId="MAGETWO-12420"> - <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test</data> + <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, mftf_migrated:yes</data> <data name="catalogSearch/data/query_text/value" xsi:type="string">catalogProductSimple::default::sku</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertProductCanBeOpenedFromSearchResult" /> </variation> <variation name="SearchEntityResultsTestVariation2" summary="Search simple product and add to cart" ticketId="MAGETWO-43235"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="catalogSearch/data/query_text/value" xsi:type="string">catalogProductSimple::default::simple</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertProductAddedToCartFromSearchResults" /> </variation> <variation name="SearchEntityResultsTestVariation3" summary="Search virtual product and add to cart" ticketId="MAGETWO-43235"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="catalogSearch/data/query_text/value" xsi:type="string">catalogProductVirtual::default::virtual</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertProductAddedToCartFromSearchResults" /> </variation> <variation name="SearchEntityResultsTestVariation4" summary="Search configurable product and add to cart" ticketId="MAGETWO-43235"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="catalogSearch/data/query_text/value" xsi:type="string">configurableProduct::default::configurable</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertProductAddedToCartFromSearchResults" /> </variation> <variation name="SearchEntityResultsTestVariation5" summary="Search downloadable product and add to cart" ticketId="MAGETWO-43235"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="catalogSearch/data/query_text/value" xsi:type="string">downloadableProduct::default::downloadable</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertProductAddedToCartFromSearchResults" /> </variation> <variation name="SearchEntityResultsTestVariation6" summary="Search grouped product and add to cart" ticketId="MAGETWO-43235"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="catalogSearch/data/query_text/value" xsi:type="string">groupedProduct::withSimpleProducts_without_qty::grouped</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertProductAddedToCartFromSearchResults" /> </variation> <variation name="SearchEntityResultsTestVariation7" summary="Search bundle dynamic product and add to cart" ticketId="MAGETWO-43235"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="catalogSearch/data/query_text/value" xsi:type="string">bundleProduct::bundle_dynamic_product::bundle</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertProductAddedToCartFromSearchResults" /> </variation> <variation name="SearchEntityResultsTestVariation8" summary="Search fixed product"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="catalogSearch/data/query_text/value" xsi:type="string">bundleProduct::bundle_fixed_product::bundle</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertCatalogSearchResult" /> </variation> <variation name="SearchEntityResultsTestVariation9"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="catalogSearch/data/query_text/value" xsi:type="string">catalogProductSimple::default::name</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertProductCanBeOpenedFromSearchResult" /> </variation> <variation name="SearchEntityResultsTestVariation10"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="catalogSearch/data/query_text/value" xsi:type="string">catalogProductSimple::product_with_special_symbols_in_name::name</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertProductCanBeOpenedFromSearchResult" /> </variation> <variation name="SearchEntityResultsTestVariation11"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="catalogSearch/data/query_text/search_query" xsi:type="string">TryToFindMeAndI'llFindYOU</data> <data name="catalogSearch/data/query_text/value" xsi:type="string">catalogProductSimple::default</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertCatalogSearchNoResultMessage" /> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertCatalogSearchNoResult" /> </variation> <variation name="SearchEntityResultsTestVariation12" summary="Search for simple product name using 2 symbols query length" ticketId="MAGETWO-36542"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="catalogSearch/data/query_text/value" xsi:type="string">catalogProductSimple::default::name</data> <data name="queryLength" xsi:type="string">2</data> - <constraint name="Magento\CatalogSearch\Test\Constraint\AssertCatalogSearchNoResultMessage" /> - <constraint name="Magento\CatalogSearch\Test\Constraint\AssertCatalogSearchNoResult" /> + <constraint name="Magento\CatalogSearch\Test\Constraint\AssertProductCanBeOpenedFromSearchResult" /> </variation> <variation name="SearchEntityResultsTestVariation13" summary="Search for simple product name using 3 symbols query length" ticketId="MAGETWO-36542"> - <data name="issue" xsi:type="string">MAGETWO-65509: [FT] Magento\CatalogSearch\Test\TestCase\SearchEntityResultsTest fails on Jenkins</data> <data name="tag" xsi:type="string">stable:no</data> <data name="catalogSearch/data/query_text/value" xsi:type="string">catalogProductSimple::default::name</data> <data name="queryLength" xsi:type="string">3</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertProductCanBeOpenedFromSearchResult" /> </variation> <variation name="SearchEntityResultsTestVariation14" summary="Search for simple product name using 128 symbols query length" ticketId="MAGETWO-36542"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="catalogSearch/data/query_text/value" xsi:type="string">catalogProductSimple::product_with_long_name::name</data> <data name="queryLength" xsi:type="string">128</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertProductCanBeOpenedFromSearchResult" /> @@ -82,6 +92,7 @@ <variation name="SearchEntityResultsTestVariation16" summary="Search for two simple products for text in attributes with same search weight and check their sort order" ticketId="MAGETWO-64501"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="catalogSearch/data/query_text/search_query" xsi:type="string">alaska</data> <data name="catalogSearch/data/query_text/value" xsi:type="array"> <item name="product_2" xsi:type="string">catalogProductSimple::search_weight_term_twice_weight_1</item> @@ -92,6 +103,7 @@ <variation name="SearchEntityResultsTestVariation17" summary="Search for two simple products for text in attributes with different search weight and check their sort order" ticketId="MAGETWO-64502"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="catalogSearch/data/query_text/search_query" xsi:type="string">alaska</data> <data name="catalogSearch/data/query_text/value" xsi:type="array"> <item name="product_1" xsi:type="string">catalogProductSimple::search_weight_term_once_weight_5</item> @@ -100,6 +112,7 @@ <constraint name="Magento\CatalogSearch\Test\Constraint\AssertCatalogSearchResultOrder" /> </variation> <variation name="SearchEntityResultsTestVariation18" summary="Search Configurable Product with Enabled and Disabled Children." ticketId="MAGETWO-69181"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="catalogSearch/data/query_text/value" xsi:type="string">configurableProduct::one_simple_product_not_visible_individually::name</data> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertCatalogSearchResult" /> <constraint name="Magento\CatalogSearch\Test\Constraint\AssertConfigurableWithDisabledOptionCatalogSearchNoResult" /> diff --git a/dev/tests/functional/tests/app/Magento/CatalogUrlRewrite/Test/TestCase/CreateDuplicateUrlCategoryEntityTest.xml b/dev/tests/functional/tests/app/Magento/CatalogUrlRewrite/Test/TestCase/CreateDuplicateUrlCategoryEntityTest.xml index 398054f1f0ed..8b15da5ecd2e 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogUrlRewrite/Test/TestCase/CreateDuplicateUrlCategoryEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogUrlRewrite/Test/TestCase/CreateDuplicateUrlCategoryEntityTest.xml @@ -14,7 +14,7 @@ <data name="category/data/include_in_menu" xsi:type="string">Yes</data> <data name="category/data/name" xsi:type="string">Subcategory%isolation%</data> <data name="category/data/url_key" xsi:type="string">subcategory-%isolation%</data> - <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, severity:S1</data> + <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, severity:S1, mftf_migrated:yes</data> <constraint name="Magento\CatalogUrlRewrite\Test\Constraint\AssertCategoryUrlDuplicateErrorMessage" /> </variation> </testCase> diff --git a/dev/tests/functional/tests/app/Magento/CatalogUrlRewrite/Test/TestCase/CreateDuplicateUrlProductEntity.xml b/dev/tests/functional/tests/app/Magento/CatalogUrlRewrite/Test/TestCase/CreateDuplicateUrlProductEntity.xml index 1116821f756a..8110ed1ed00b 100644 --- a/dev/tests/functional/tests/app/Magento/CatalogUrlRewrite/Test/TestCase/CreateDuplicateUrlProductEntity.xml +++ b/dev/tests/functional/tests/app/Magento/CatalogUrlRewrite/Test/TestCase/CreateDuplicateUrlProductEntity.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\CatalogUrlRewrite\Test\TestCase\CreateDuplicateUrlProductEntity" summary="Create Simple Product" ticketId="MAGETWO-69427"> <variation name="CreateDuplicateUrlProductEntityTestVariation1" summary="Create Duplicate Url Product"> - <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, severity:S1</data> + <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, severity:S1, mftf_migrated:yes</data> <data name="product/data/url_key" xsi:type="string">simple-product-%isolation%</data> <data name="product/data/name" xsi:type="string">Simple Product %isolation%</data> <data name="product/data/sku" xsi:type="string">simple_sku_%isolation%</data> diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart.php index b0e996355642..bbe6fe293ab5 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart.php @@ -106,6 +106,13 @@ class Cart extends Block */ protected $cartItemClass = \Magento\Checkout\Test\Block\Cart\CartItem::class; + /** + * Locator for page with ajax loading state. + * + * @var string + */ + private $ajaxLoading = 'body.ajax-loading'; + /** * Wait for PayPal page is loaded. * @@ -273,4 +280,14 @@ public function waitForCheckoutButton() { $this->waitForElementVisible($this->inContextPaypalCheckoutButton); } + + /** + * Wait loading. + * + * @return void + */ + public function waitForLoader() + { + $this->waitForElementNotVisible($this->ajaxLoading); + } } diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Shipping.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Shipping.php index 10299486a08c..3d293700db8c 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Shipping.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Shipping.php @@ -72,6 +72,13 @@ class Shipping extends Form */ protected $commonShippingPriceSelector = '.totals.shipping .price'; + /** + * Estimate shipping and tax form locator. + * + * @var string + */ + private $estimateShippingForm = '#shipping-zip-form'; + /** * Open estimate shipping and tax form. * @@ -250,4 +257,51 @@ public function waitForCommonShippingPriceBlock() { $this->waitForElementVisible($this->commonShippingPriceSelector, Locator::SELECTOR_CSS); } + + /** + * Wait until estimation form to appear. + * + * @return void + */ + public function waitForEstimateShippingAndTaxForm() + { + $browser = $this->browser; + $selector = $this->estimateShippingForm; + + $browser->waitUntil( + function () use ($browser, $selector) { + $element = $browser->find($selector); + return $element->isPresent() ? true : null; + } + ); + } + + /** + * Wait for shipping method form. + * + * @return void + */ + public function waitForShippingMethodForm() + { + $browser = $this->browser; + $selector = $this->shippingMethodForm; + + $browser->waitUntil( + function () use ($browser, $selector) { + $element = $browser->find($selector); + return $element->isPresent() ? true : null; + } + ); + } + + /** + * Wait for summary block to be loaded. + * + * @return void + */ + public function waitForSummaryBlock() + { + $this->waitForEstimateShippingAndTaxForm(); + $this->waitForShippingMethodForm(); + } } diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php index fda29a2fe78f..1d3950091d06 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Cart/Totals.php @@ -117,6 +117,7 @@ public function getGrandTotal() */ public function getGrandTotalIncludingTax() { + $this->waitForGrandTotal(); $priceElement = $this->_rootElement->find($this->grandTotalInclTax, Locator::SELECTOR_CSS); return $priceElement->isVisible() ? $this->escapeCurrency($priceElement->getText()) : null; } @@ -264,4 +265,15 @@ public function waitForShippingPriceBlock() { $this->waitForElementVisible($this->shippingPriceBlockSelector, Locator::SELECTOR_CSS); } + + /** + * Wait for "Grand Total" row to appear. + * + * @return void + */ + public function waitForGrandTotal() + { + $this->waitForUpdatedTotals(); + $this->waitForElementVisible($this->grandTotal); + } } diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Shipping.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Shipping.php index 0bf9b37c75e1..1013404f42df 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Shipping.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Shipping.php @@ -102,7 +102,7 @@ class Shipping extends Form * * @var string */ - private $emailError = '#customer-email-error'; + private $emailError = '#checkout-customer-email-error'; /** * Get email error. diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Shipping.xml b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Shipping.xml index 71115e402880..0973b968cba9 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Shipping.xml +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Block/Onepage/Shipping.xml @@ -8,7 +8,7 @@ <mapping strict="0"> <fields> <email> - <selector>#customer-email</selector> + <selector>#checkout-customer-email</selector> </email> <firstname /> <lastname /> diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertCartIsEmpty.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertCartIsEmpty.php index c2839651b582..cf05079b0a07 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertCartIsEmpty.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertCartIsEmpty.php @@ -3,10 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Checkout\Test\Constraint; -use Magento\Checkout\Test\Fixture\Cart; use Magento\Checkout\Test\Page\CheckoutCart; use Magento\Mtf\Client\BrowserInterface; use Magento\Mtf\Constraint\AbstractConstraint; @@ -30,8 +30,10 @@ class AssertCartIsEmpty extends AbstractConstraint * @param BrowserInterface $browser * @return void */ - public function processAssert(CheckoutCart $checkoutCart, BrowserInterface $browser) - { + public function processAssert( + CheckoutCart $checkoutCart, + BrowserInterface $browser + ): void { $checkoutCart->open(); $cartEmptyBlock = $checkoutCart->getCartEmptyBlock(); @@ -42,10 +44,12 @@ public function processAssert(CheckoutCart $checkoutCart, BrowserInterface $brow ); $cartEmptyBlock->clickLinkToMainPage(); - \PHPUnit\Framework\Assert::assertEquals( + $this->assertUrlEqual( $_ENV['app_frontend_url'], $browser->getUrl(), - 'Wrong link to main page on empty cart page.' + true, + 'Wrong link to main page on empty cart page: expected - ' . $_ENV['app_frontend_url'] + . ', actual - ' . $browser->getUrl() ); } @@ -58,4 +62,31 @@ public function toString() { return 'Shopping Cart is empty.'; } + + /** + * Asserts that two urls are equal + * + * @param string $expectedUrl + * @param string $actualUrl + * @param bool $ignoreScheme + * @param string $message + * @return void + */ + private function assertUrlEqual( + string $expectedUrl, + string $actualUrl, + bool $ignoreScheme = false, + string $message = '' + ): void { + $urlArray1 = parse_url($expectedUrl); + $urlArray2 = parse_url($actualUrl); + if ($ignoreScheme) { + unset($urlArray1['scheme']); + unset($urlArray2['scheme']); + } + \PHPUnit\Framework\Assert::assertTrue( + $urlArray1 === $urlArray2, + $message + ); + } } diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertCartItemsOptions.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertCartItemsOptions.php index 144e41dca0d9..b0ed9d358f93 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertCartItemsOptions.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertCartItemsOptions.php @@ -7,6 +7,7 @@ namespace Magento\Checkout\Test\Constraint; use Magento\Catalog\Test\Fixture\CatalogProductSimple; +use Magento\Checkout\Test\Constraint\Utils\CartPageLoadTrait; use Magento\Checkout\Test\Fixture\Cart; use Magento\Checkout\Test\Fixture\Cart\Items; use Magento\Checkout\Test\Page\CheckoutCart; @@ -21,6 +22,8 @@ */ class AssertCartItemsOptions extends AbstractAssertForm { + use CartPageLoadTrait; + /** * Error message for verify options * @@ -44,6 +47,8 @@ class AssertCartItemsOptions extends AbstractAssertForm public function processAssert(CheckoutCart $checkoutCart, Cart $cart) { $checkoutCart->open(); + $this->waitForCartPageLoaded($checkoutCart); + /** @var Items $sourceProducts */ $sourceProducts = $cart->getDataFieldConfig('items')['source']; $products = $sourceProducts->getProducts(); diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertGrandTotalInShoppingCart.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertGrandTotalInShoppingCart.php index 9bedda350d06..432bbd4f2146 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertGrandTotalInShoppingCart.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertGrandTotalInShoppingCart.php @@ -6,6 +6,7 @@ namespace Magento\Checkout\Test\Constraint; +use Magento\Checkout\Test\Constraint\Utils\CartPageLoadTrait; use Magento\Checkout\Test\Fixture\Cart; use Magento\Checkout\Test\Page\CheckoutCart; use Magento\Mtf\Constraint\AbstractConstraint; @@ -16,6 +17,8 @@ */ class AssertGrandTotalInShoppingCart extends AbstractConstraint { + use CartPageLoadTrait; + /** * Assert that grand total is equal to expected * @@ -28,6 +31,7 @@ public function processAssert(CheckoutCart $checkoutCart, Cart $cart, $requireRe { if ($requireReload) { $checkoutCart->open(); + $this->waitForCartPageLoaded($checkoutCart); $checkoutCart->getTotalsBlock()->waitForUpdatedTotals(); } diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertPriceInShoppingCart.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertPriceInShoppingCart.php index 42c79c1280e3..88d4a3e8d35b 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertPriceInShoppingCart.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertPriceInShoppingCart.php @@ -7,6 +7,7 @@ namespace Magento\Checkout\Test\Constraint; use Magento\Catalog\Test\Fixture\CatalogProductSimple; +use Magento\Checkout\Test\Constraint\Utils\CartPageLoadTrait; use Magento\Checkout\Test\Fixture\Cart; use Magento\Checkout\Test\Fixture\Cart\Items; use Magento\Checkout\Test\Page\CheckoutCart; @@ -19,6 +20,8 @@ */ class AssertPriceInShoppingCart extends AbstractAssertForm { + use CartPageLoadTrait; + /** * Assert that price in the shopping cart equals to expected price from data set * @@ -29,6 +32,8 @@ class AssertPriceInShoppingCart extends AbstractAssertForm public function processAssert(CheckoutCart $checkoutCart, Cart $cart) { $checkoutCart->open(); + $this->waitForCartPageLoaded($checkoutCart); + /** @var Items $sourceProducts */ $sourceProducts = $cart->getDataFieldConfig('items')['source']; $products = $sourceProducts->getProducts(); diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertProductQtyInShoppingCart.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertProductQtyInShoppingCart.php index 40eb41e12724..b80b4c85227c 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertProductQtyInShoppingCart.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertProductQtyInShoppingCart.php @@ -7,6 +7,7 @@ namespace Magento\Checkout\Test\Constraint; use Magento\Catalog\Test\Fixture\CatalogProductSimple; +use Magento\Checkout\Test\Constraint\Utils\CartPageLoadTrait; use Magento\Checkout\Test\Fixture\Cart; use Magento\Checkout\Test\Fixture\Cart\Items; use Magento\Checkout\Test\Page\CheckoutCart; @@ -19,6 +20,8 @@ */ class AssertProductQtyInShoppingCart extends AbstractAssertForm { + use CartPageLoadTrait; + /** * Assert that quantity in the shopping cart is equals to expected quantity from data set * @@ -29,6 +32,8 @@ class AssertProductQtyInShoppingCart extends AbstractAssertForm public function processAssert(CheckoutCart $checkoutCart, Cart $cart) { $checkoutCart->open(); + $this->waitForCartPageLoaded($checkoutCart); + /** @var Items $sourceProducts */ $sourceProducts = $cart->getDataFieldConfig('items')['source']; $products = $sourceProducts->getProducts(); diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertSubtotalInShoppingCart.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertSubtotalInShoppingCart.php index 2ee9caa251a7..5e743e735d42 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertSubtotalInShoppingCart.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/AssertSubtotalInShoppingCart.php @@ -7,6 +7,7 @@ namespace Magento\Checkout\Test\Constraint; use Magento\Catalog\Test\Fixture\CatalogProductSimple; +use Magento\Checkout\Test\Constraint\Utils\CartPageLoadTrait; use Magento\Checkout\Test\Fixture\Cart; use Magento\Checkout\Test\Fixture\Cart\Items; use Magento\Checkout\Test\Page\CheckoutCart; @@ -19,6 +20,8 @@ */ class AssertSubtotalInShoppingCart extends AbstractAssertForm { + use CartPageLoadTrait; + /** * Assert that subtotal total in the shopping cart is equals to expected total from data set * @@ -31,6 +34,7 @@ public function processAssert(CheckoutCart $checkoutCart, Cart $cart, $requireRe { if ($requireReload) { $checkoutCart->open(); + $this->waitForCartPageLoaded($checkoutCart); } /** @var Items $sourceProducts */ diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/Utils/CartPageLoadTrait.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/Utils/CartPageLoadTrait.php new file mode 100644 index 000000000000..fa349554fa13 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/Constraint/Utils/CartPageLoadTrait.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Checkout\Test\Constraint\Utils; + +use Magento\Checkout\Test\Page\CheckoutCart; + +/** + * Check if cart page is fully loaded. + */ +trait CartPageLoadTrait +{ + /** + * @param CheckoutCart $checkoutCart + * @return void + */ + public function waitForCartPageLoaded(CheckoutCart $checkoutCart) : void + { + $checkoutCart->getCartBlock()->waitForLoader(); + if (!$checkoutCart->getCartBlock()->cartIsEmpty()) { + $checkoutCart->getShippingBlock()->waitForSummaryBlock(); + $checkoutCart->getTotalsBlock()->waitForGrandTotal(); + } + } +} diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/AddProductsToShoppingCartEntityTest.xml b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/AddProductsToShoppingCartEntityTest.xml index 8d054c023087..c0df4b16b92e 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/AddProductsToShoppingCartEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/AddProductsToShoppingCartEntityTest.xml @@ -22,7 +22,6 @@ <constraint name="Magento\Checkout\Test\Constraint\AssertSubtotalInMiniShoppingCart" /> </variation> <variation name="AddProductsToShoppingCartEntityTestVariation2"> - <data name="tag" xsi:type="string">to_maintain:yes, severity:S2</data> <data name="productsData/0" xsi:type="string">bundleProduct::bundle_fixed_product</data> <data name="cart/data/grand_total" xsi:type="string">761</data> <data name="cart/data/subtotal" xsi:type="string">756</data> @@ -35,7 +34,6 @@ <constraint name="Magento\Checkout\Test\Constraint\AssertSubtotalInMiniShoppingCart" /> </variation> <variation name="AddProductsToShoppingCartEntityTestVariation3"> - <data name="tag" xsi:type="string">to_maintain:yes, severity:S0</data> <data name="productsData/0" xsi:type="string">catalogProductSimple::with_two_custom_option</data> <data name="cart/data/grand_total" xsi:type="string">345</data> <data name="cart/data/subtotal" xsi:type="string">340</data> @@ -48,7 +46,6 @@ <constraint name="Magento\Checkout\Test\Constraint\AssertSubtotalInMiniShoppingCart" /> </variation> <variation name="AddProductsToShoppingCartEntityTestVariation4"> - <data name="tag" xsi:type="string">to_maintain:yes, severity:S1</data> <data name="productsData/0" xsi:type="string">catalogProductVirtual::product_50_dollar</data> <data name="cart/data/grand_total" xsi:type="string">50</data> <data name="cart/data/subtotal" xsi:type="string">50</data> @@ -100,8 +97,7 @@ <constraint name="Magento\Checkout\Test\Constraint\AssertSubtotalInMiniShoppingCart" /> </variation> <variation name="AddProductsToShoppingCartEntityTestVariation8" summary="Enable https in backend and add products of different types to cart" ticketId="MAGETWO-42677"> - <data name="tag" xsi:type="string">to_maintain:yes, severity:S0</data> - <data name="issue" xsi:type="string">Errors on configuration step. Skipped.</data> + <data name="tag" xsi:type="string">severity:S0</data> <data name="productsData/0" xsi:type="string">catalogProductSimple::with_two_custom_option</data> <data name="productsData/1" xsi:type="string">catalogProductVirtual::product_50_dollar</data> <data name="productsData/2" xsi:type="string">downloadableProduct::with_two_separately_links</data> diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/EditShippingAddressOnePageCheckoutTest.xml b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/EditShippingAddressOnePageCheckoutTest.xml index 3c88d9193db2..64ed05904469 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/EditShippingAddressOnePageCheckoutTest.xml +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/EditShippingAddressOnePageCheckoutTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Checkout\Test\TestCase\EditShippingAddressOnePageCheckoutTest" summary="Customer can place order with new addresses that was edited during checkout with several conditions" ticketId="MAGETWO-67837"> <variation name="EditShippingAddressOnePageCheckoutTestVariation1"> - <data name="tag" xsi:type="string">severity:S1</data> + <data name="tag" xsi:type="string">severity:S1, mftf_migrated:yes</data> <data name="customer/dataset" xsi:type="string">johndoe_with_addresses</data> <data name="shippingAddress/dataset" xsi:type="string">UK_address_without_email</data> <data name="editShippingAddress/dataset" xsi:type="string">empty_UK_address_without_email</data> diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/OnePageCheckoutJsValidationTest.xml b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/OnePageCheckoutJsValidationTest.xml index 6635c1edbe78..75603d12cbe3 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/OnePageCheckoutJsValidationTest.xml +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/OnePageCheckoutJsValidationTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Checkout\Test\TestCase\OnePageCheckoutJsValidationTest" summary="JS validation verification for Checkout flow" ticketId="MAGETWO-59697"> <variation name="OnePageCheckoutJsValidationTestVariation1" summary="JS validation is not applied for empty required checkout fields if customer did not fill them"> + <data name="issue" xsi:type="string">MAGETWO-97990: [MTF] OnePageCheckoutJsValidationTestVariation1_0 randomly fails on jenkins</data> <data name="tag" xsi:type="string">severity:S2</data> <data name="products/0" xsi:type="string">catalogProductSimple::default</data> <data name="checkoutMethod" xsi:type="string">guest</data> diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/OnePageCheckoutOfflinePaymentMethodsTest.xml b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/OnePageCheckoutOfflinePaymentMethodsTest.xml index 387e6418ee57..361c5031f331 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/OnePageCheckoutOfflinePaymentMethodsTest.xml +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/OnePageCheckoutOfflinePaymentMethodsTest.xml @@ -22,7 +22,6 @@ <constraint name="Magento\Shipping\Test\Constraint\AssertShipmentSuccessCreateMessage" /> </variation> <variation name="OnePageCheckoutUsingRegisterLink" summary="Customer is redirected to checkout on login if guest is disabled, flow with registration new Customer" ticketId="MAGETWO-49917"> - <data name="issue" xsi:type="string">MAGETWO-59816: Redirect works improperly in a browser incognito mode</data> <data name="tag" xsi:type="string">severity:S1</data> <data name="products/0" xsi:type="string">catalogProductSimple::default</data> <data name="customer/dataset" xsi:type="string">register_customer</data> @@ -57,7 +56,7 @@ <constraint name="Magento\Sales\Test\Constraint\AssertOrderGrandTotal" /> </variation> <variation name="OnePageCheckoutTestVariation2" summary="US customer during checkout using coupon for all customer groups"> - <data name="tag" xsi:type="string">stable:no, severity:S0</data> + <data name="tag" xsi:type="string">severity:S0</data> <data name="products/0" xsi:type="string">catalogProductSimple::default</data> <data name="salesRule" xsi:type="string">active_sales_rule_for_all_groups</data> <data name="customer/dataset" xsi:type="string">default</data> @@ -79,7 +78,7 @@ <constraint name="Magento\Sales\Test\Constraint\AssertOrderGrandTotal" /> </variation> <variation name="OnePageCheckoutTestVariation3" summary="Checkout as UK guest with simple product" ticketId="MAGETWO-42603, MAGETWO-43282, MAGETWO-43318"> - <data name="tag" xsi:type="string">severity:S1, stable:no</data> + <data name="tag" xsi:type="string">severity:S1</data> <data name="products/0" xsi:type="string">catalogProductSimple::product_with_qty_25</data> <data name="expectedQty/0" xsi:type="string">0</data> <data name="expectedStockStatus/0" xsi:type="string">out of stock</data> @@ -92,7 +91,7 @@ <item name="grandTotal" xsi:type="string">375.00</item> </data> <data name="payment/method" xsi:type="string">banktransfer</data> - <data name="status" xsi:type="string">Precessing</data> + <data name="status" xsi:type="string">Processing</data> <data name="orderButtonsAvailable" xsi:type="string">Back, Send Email, Cancel, Hold, Invoice, Edit</data> <data name="configData" xsi:type="string">banktransfer_specificcountry_gb, can_subtract_and_can_back_in_stock</data> <constraint name="Magento\Shipping\Test\Constraint\AssertShipmentSuccessCreateMessage" /> @@ -102,7 +101,6 @@ <constraint name="Magento\Sales\Test\Constraint\AssertOrderStatusIsCorrect" /> <constraint name="Magento\Sales\Test\Constraint\AssertOrderButtonsAvailable" /> <constraint name="Magento\Sales\Test\Constraint\AssertOrderGrandTotal" /> - <data name="issue" xsi:type="string">MAGETWO-66737: Magento\Checkout\Test\TestCase\OnePageCheckoutTest with OnePageCheckoutTestVariation3 and 4 is not stable</data> </variation> <variation name="OnePageCheckoutTestVariation4" summary="One Page Checkout Products with Special Prices" ticketId="MAGETWO-12429"> <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, severity:S0</data> @@ -125,7 +123,6 @@ <constraint name="Magento\Shipping\Test\Constraint\AssertShipmentSuccessCreateMessage" /> <constraint name="Magento\Checkout\Test\Constraint\AssertMinicartEmpty" /> <constraint name="Magento\Sales\Test\Constraint\AssertOrderGrandTotal"/> - <data name="issue" xsi:type="string">MAGETWO-66737: Magento\Checkout\Test\TestCase\OnePageCheckoutTest with OnePageCheckoutTestVariation3 and 4 is not stable</data> </variation> <variation name="OnePageCheckoutTestVariation5" summary="Guest Checkout using Check/Money Order and Free Shipping with Prices/Taxes Verifications" ticketId="MAGETWO-12412"> <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, severity:S0</data> @@ -211,7 +208,7 @@ <constraint name="Magento\Sales\Test\Constraint\AssertOrderGrandTotal" /> </variation> <variation name="OnePageCheckoutTestVariation9" summary="One Page Checkout Products with different shipping/billing address and Tier Prices" ticketId="MAGETWO-42604"> - <data name="tag" xsi:type="string">stable:no, severity:S1</data> + <data name="tag" xsi:type="string">severity:S1</data> <data name="products/0" xsi:type="string">catalogProductSimple::simple_with_tier_price_and_order_qty_3</data> <data name="customer/dataset" xsi:type="string">default</data> <data name="checkoutMethod" xsi:type="string">login</data> diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/OnePageCheckoutTest.xml b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/OnePageCheckoutTest.xml index 5c05d4a84000..daf9aaae5058 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/OnePageCheckoutTest.xml +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/OnePageCheckoutTest.xml @@ -7,7 +7,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\OnePageCheckoutTest" summary="OnePageCheckout within Offline Payment Methods" ticketId="MAGETWO-27485"> - <variation name="OnePageCheckoutUsingSingInLink" summary="Login during checkout using 'Sign In' link" ticketId="MAGETWO-42547"> + <variation name="OnePageCheckoutUsingSignInLink" summary="Login during checkout using 'Sign In' link" ticketId="MAGETWO-42547"> <data name="tag" xsi:type="string">severity:S1</data> <data name="products/0" xsi:type="string">catalogProductSimple::default</data> <data name="customer/dataset" xsi:type="string">customer_UK_US_addresses</data> @@ -15,13 +15,11 @@ <data name="shippingAddressCustomer" xsi:type="array"> <item name="added" xsi:type="number">1</item> </data> - <data name="billingAddressCustomer" xsi:type="array"> - <item name="added" xsi:type="number">1</item> - </data> <data name="prices" xsi:type="array"> <item name="grandTotal" xsi:type="string">565.00</item> </data> <data name="shipping/shipping_service" xsi:type="string">Flat Rate</data> + <data name="editBillingInformation" xsi:type="boolean">false</data> <data name="shipping/shipping_method" xsi:type="string">Fixed</data> <data name="payment/method" xsi:type="string">checkmo</data> <data name="configData" xsi:type="string">checkmo</data> @@ -43,16 +41,13 @@ <item name="grandTotal" xsi:type="string">565.00</item> </data> <data name="shipping/shipping_service" xsi:type="string">Flat Rate</data> + <data name="editBillingInformation" xsi:type="boolean">false</data> <data name="shipping/shipping_method" xsi:type="string">Fixed</data> <data name="payment/method" xsi:type="string">checkmo</data> <data name="configData" xsi:type="string">checkmo</data> <constraint name="Magento\Checkout\Test\Constraint\AssertOrderSuccessPlacedMessage" /> <constraint name="Magento\Sales\Test\Constraint\AssertOrderGrandTotal" /> <constraint name="Magento\Sales\Test\Constraint\AssertOrderAddresses" /> - <!-- MAGETWO-94169 --> - <data name="tag" xsi:type="string">stable:no</data> - <data name="issue" xsi:type="string">MAGETWO-94169: [MTF] - OnePageCheckoutUsingNonDefaultAddress_0 fails on 2.3-develop</data> - <!-- MAGETWO-94169 --> </variation> <variation name="OnePageCheckoutUsingNewAddress" summary="Checkout as Customer using New address" ticketId="MAGETWO-42601"> <data name="tag" xsi:type="string">severity:S1</data> @@ -65,7 +60,7 @@ <data name="shippingAddress/dataset" xsi:type="string">UK_address_without_email</data> <data name="shipping/shipping_service" xsi:type="string">Flat Rate</data> <data name="shipping/shipping_method" xsi:type="string">Fixed</data> - <data name="billingCheckboxState" xsi:type="string">Yes</data> + <data name="editBillingInformation" xsi:type="boolean">false</data> <data name="billingAddress/dataset" xsi:type="string">US_address_1_without_email</data> <data name="payment/method" xsi:type="string">checkmo</data> <data name="configData" xsi:type="string">checkmo</data> diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/ShoppingCartPerCustomerTest.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/ShoppingCartPerCustomerTest.php index bdd54ce3559f..7365195d0cd4 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/ShoppingCartPerCustomerTest.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/ShoppingCartPerCustomerTest.php @@ -95,7 +95,7 @@ public function test( $customers = []; $cartFixtures = []; - for ($i = 0; $i < count($checkoutData); $i++) { + for ($i = 0, $count = count($checkoutData); $i < $count; $i++) { $customers[$i] = $this->fixtureFactory->createByCode('customer', ['dataset' => $customerDataset]); $customers[$i]->persist(); diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateProductFromMiniShoppingCartEntityTest.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateProductFromMiniShoppingCartEntityTest.php index 5935a68c43c2..af267cfa30ec 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateProductFromMiniShoppingCartEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateProductFromMiniShoppingCartEntityTest.php @@ -115,6 +115,7 @@ public function test( } else { $miniShoppingCart->getCartItem($newProduct)->clickEditItem(); $this->catalogProductView->getViewBlock()->addToCart($newProduct); + $this->catalogProductView->getMessagesBlock()->waitSuccessMessage(); } // Prepare data for asserts: $cart['data']['items'] = ['products' => [$newProduct]]; diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateProductFromMiniShoppingCartEntityTest.xml b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateProductFromMiniShoppingCartEntityTest.xml index 3177f0fbccc2..4b99de09f2a7 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateProductFromMiniShoppingCartEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateProductFromMiniShoppingCartEntityTest.xml @@ -8,7 +8,8 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Checkout\Test\TestCase\UpdateProductFromMiniShoppingCartEntityTest" summary="Update Product from Mini Shopping Cart" ticketId="MAGETWO-29812"> <variation name="UpdateProductFromMiniShoppingCartEntityTestVariation1" summary="Update Product Qty on Mini Shopping Cart" ticketId=" MAGETWO-35536"> - <data name="tag" xsi:type="string">test_type:extended_acceptance_test, severity:S0</data> + <data name="issue" xsi:type="string">https://github.com/magento-engcom/msi/issues/1624</data> + <data name="tag" xsi:type="string">test_type:extended_acceptance_test, severity:S0, mftf_migrated:yes</data> <data name="originalProduct/0" xsi:type="string">catalogProductSimple::default</data> <data name="checkoutData/dataset" xsi:type="string">simple_order_qty_2</data> <data name="use_minicart_to_edit_qty" xsi:type="boolean">true</data> @@ -25,7 +26,7 @@ <constraint name="Magento\Checkout\Test\Constraint\AssertCustomerIsRedirectedToCheckoutFromCart" /> </variation> <variation name="UpdateProductFromMiniShoppingCartEntityTestVariation2" summary="Update Configurable and verify previous product was updated to new one in shopping cart and mini shopping cart"> - <data name="tag" xsi:type="string">test_type:extended_acceptance_test, to_maintain:yes, severity:S0</data> + <data name="tag" xsi:type="string">test_type:extended_acceptance_test, severity:S0</data> <data name="originalProduct/0" xsi:type="string">configurableProduct::default</data> <data name="checkoutData/dataset" xsi:type="string">configurable_update_mini_shopping_cart</data> <constraint name="Magento\Checkout\Test\Constraint\AssertCartItemsOptions" /> @@ -34,7 +35,7 @@ <constraint name="Magento\Checkout\Test\Constraint\AssertProductOptionsAbsentInShoppingCart" /> </variation> <variation name="UpdateProductFromMiniShoppingCartEntityTestVariation3" summary="Update Bundle and verify previous product was updated to new one in shopping cart and mini shopping cart"> - <data name="tag" xsi:type="string">test_type:extended_acceptance_test, to_maintain:yes, severity:S0</data> + <data name="tag" xsi:type="string">test_type:extended_acceptance_test, severity:S0</data> <data name="originalProduct/0" xsi:type="string">bundleProduct::bundle_fixed_product</data> <data name="checkoutData/dataset" xsi:type="string">bundle_update_mini_shopping_cart</data> <constraint name="Magento\Checkout\Test\Constraint\AssertCartItemsOptions" /> @@ -43,7 +44,7 @@ <constraint name="Magento\Checkout\Test\Constraint\AssertProductOptionsAbsentInShoppingCart" /> </variation> <variation name="UpdateProductFromMiniShoppingCartEntityTestVariation4" summary="Update Downloadable and check previous link was updated to new one in shopping cart and mini shopping cart"> - <data name="tag" xsi:type="string">test_type:extended_acceptance_test, to_maintain:yes, severity:S1</data> + <data name="tag" xsi:type="string">test_type:extended_acceptance_test, severity:S1</data> <data name="originalProduct/0" xsi:type="string">downloadableProduct::with_two_separately_links</data> <data name="checkoutData/dataset" xsi:type="string">downloadable_update_mini_shopping_cart</data> <constraint name="Magento\Checkout\Test\Constraint\AssertCartItemsOptions" /> @@ -52,7 +53,7 @@ <constraint name="Magento\Checkout\Test\Constraint\AssertProductOptionsAbsentInShoppingCart" /> </variation> <variation name="UpdateProductFromMiniShoppingCartEntityTestVariation5" summary="Update Virtual product in mini shopping cart"> - <data name="tag" xsi:type="string">test_type:extended_acceptance_test, to_maintain:yes, severity:S1</data> + <data name="tag" xsi:type="string">test_type:extended_acceptance_test, severity:S1</data> <data name="originalProduct/0" xsi:type="string">catalogProductVirtual::default</data> <data name="checkoutData/dataset" xsi:type="string">virtual_update_mini_shopping_cart</data> <constraint name="Magento\Checkout\Test\Constraint\AssertProductDataInMiniShoppingCart" /> diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateShoppingCartTest.xml b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateShoppingCartTest.xml index e0ea721a51f1..5caa3ba9b924 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateShoppingCartTest.xml +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/UpdateShoppingCartTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Checkout\Test\TestCase\UpdateShoppingCartTest" summary="Update Shopping Cart" ticketId="MAGETWO-25081"> <variation name="UpdateShoppingCartTestVariation1"> - <data name="tag" xsi:type="string">severity:S0</data> + <data name="tag" xsi:type="string">severity:S0,mftf_migrated:yes</data> <data name="product/dataset" xsi:type="string">default</data> <data name="product/data/price/value" xsi:type="string">100</data> <data name="product/data/checkout_data/qty" xsi:type="string">3</data> @@ -20,7 +20,7 @@ <constraint name="Magento\Checkout\Test\Constraint\AssertSubtotalInShoppingCart" /> </variation> <variation name="UpdateShoppingCartTestVariation2"> - <data name="tag" xsi:type="string">severity:S0</data> + <data name="tag" xsi:type="string">severity:S0,mftf_migrated:yes</data> <data name="product/dataset" xsi:type="string">with_two_custom_option</data> <data name="product/data/price/value" xsi:type="string">50</data> <data name="product/data/checkout_data/qty" xsi:type="string">11</data> diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/ValidateEmailOnCheckoutTest.xml b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/ValidateEmailOnCheckoutTest.xml index 90c42505585a..8290d825593a 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/ValidateEmailOnCheckoutTest.xml +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestCase/ValidateEmailOnCheckoutTest.xml @@ -10,17 +10,20 @@ <variation name="ValidateEmailOnCheckoutTestVariation1"> <data name="customer/data/email" xsi:type="string">johndoe</data> <data name="customer/data/firstname" xsi:type="string">John</data> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <constraint name="Magento\Checkout\Test\Constraint\AssertEmailErrorValidationMessage" /> <constraint name="Magento\Checkout\Test\Constraint\AssertEmailToolTips" /> </variation> <variation name="ValidateEmailOnCheckoutTestVariation2"> <data name="customer/data/email" xsi:type="string">johndoe#example.com</data> <data name="customer/data/firstname" xsi:type="string">John</data> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <constraint name="Magento\Checkout\Test\Constraint\AssertEmailErrorValidationMessage" /> </variation> <variation name="ValidateEmailOnCheckoutTestVariation3"> <data name="customer/data/email" xsi:type="string">johndoe@example.c</data> <data name="customer/data/firstname" xsi:type="string">John</data> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <constraint name="Magento\Checkout\Test\Constraint\AssertEmailErrorValidationMessage" /> </variation> </testCase> diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestStep/FillBillingInformationStep.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestStep/FillBillingInformationStep.php index aa7eba634145..52b296c2e01f 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestStep/FillBillingInformationStep.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestStep/FillBillingInformationStep.php @@ -126,11 +126,15 @@ public function run() if ($this->billingCheckboxState) { $this->assertBillingAddressCheckbox->processAssert($this->checkoutOnepage, $this->billingCheckboxState); } - if ($this->billingCheckboxState === 'Yes' && !$this->editBillingInformation) { - return [ - 'billingAddress' => $this->shippingAddress - ]; + + if (!$this->editBillingInformation) { + $billingAddress = $this->billingCheckboxState === 'Yes' + ? $this->shippingAddress + : $this->getDefaultBillingAddress(); + + return ['billingAddress' => $billingAddress]; } + if ($this->billingAddress) { $selectedPaymentMethod = $this->checkoutOnepage->getPaymentBlock()->getSelectedPaymentMethodBlock(); if ($this->shippingAddress) { @@ -139,9 +143,11 @@ public function run() $selectedPaymentMethod->getBillingBlock()->fillBilling($this->billingAddress); $billingAddress = $this->billingAddress; } + if (isset($this->billingAddressCustomer['added'])) { $addressIndex = $this->billingAddressCustomer['added']; - $billingAddress = $this->customer->getDataFieldConfig('address')['source']->getAddresses()[$addressIndex]; + $billingAddress = $this->customer->getDataFieldConfig('address')['source'] + ->getAddresses()[$addressIndex]; $address = $this->objectManager->create( \Magento\Customer\Test\Block\Address\Renderer::class, ['address' => $billingAddress, 'type' => 'html_for_select_element'] @@ -156,4 +162,25 @@ public function run() 'billingAddress' => $billingAddress ]; } + + /** + * Get default billing address + * + * @return Address|null + */ + private function getDefaultBillingAddress() + { + $addresses = $this->customer->hasData('address') + ? $this->customer->getDataFieldConfig('address')['source']->getAddresses() + : []; + $defaultAddress = null; + foreach ($addresses as $address) { + if ($address->getDefaultBilling() === 'Yes') { + $defaultAddress = $address; + break; + } + } + + return $defaultAddress; + } } diff --git a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestStep/SelectCheckoutMethodStep.php b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestStep/SelectCheckoutMethodStep.php index d951d84bab78..f79cf8d7eb7f 100644 --- a/dev/tests/functional/tests/app/Magento/Checkout/Test/TestStep/SelectCheckoutMethodStep.php +++ b/dev/tests/functional/tests/app/Magento/Checkout/Test/TestStep/SelectCheckoutMethodStep.php @@ -59,6 +59,13 @@ class SelectCheckoutMethodStep implements TestStepInterface */ private $customerAccountCreatePage; + /** + * Proceed to checkout from minicart step + * + * @var proceedToCheckoutFromMiniShoppingCartStep + */ + private $proceedToCheckoutFromMiniShoppingCartStep; + /** * @constructor * @param CheckoutOnepage $checkoutOnepage @@ -66,6 +73,7 @@ class SelectCheckoutMethodStep implements TestStepInterface * @param Customer $customer * @param LogoutCustomerOnFrontendStep $logoutCustomerOnFrontend * @param ClickProceedToCheckoutStep $clickProceedToCheckoutStep + * @param ProceedToCheckoutFromMiniShoppingCartStep $proceedToCheckoutFromMiniShoppingCartStep * @param string $checkoutMethod */ public function __construct( @@ -74,6 +82,7 @@ public function __construct( Customer $customer, LogoutCustomerOnFrontendStep $logoutCustomerOnFrontend, ClickProceedToCheckoutStep $clickProceedToCheckoutStep, + ProceedToCheckoutFromMiniShoppingCartStep $proceedToCheckoutFromMiniShoppingCartStep, $checkoutMethod ) { $this->checkoutOnepage = $checkoutOnepage; @@ -82,6 +91,7 @@ public function __construct( $this->logoutCustomerOnFrontend = $logoutCustomerOnFrontend; $this->clickProceedToCheckoutStep = $clickProceedToCheckoutStep; $this->checkoutMethod = $checkoutMethod; + $this->proceedToCheckoutFromMiniShoppingCartStep = $proceedToCheckoutFromMiniShoppingCartStep; } /** @@ -129,6 +139,7 @@ private function processRegister() if ($this->checkoutMethod === 'register_before_checkout') { $this->checkoutOnepage->getAuthenticationPopupBlock()->createAccount(); $this->customerAccountCreatePage->getRegisterForm()->registerCustomer($this->customer); + $this->proceedToCheckoutFromMiniShoppingCartStep->run(); } } diff --git a/dev/tests/functional/tests/app/Magento/CheckoutAgreements/Test/Page/MultishippingCheckoutOverview.xml b/dev/tests/functional/tests/app/Magento/CheckoutAgreements/Test/Page/MultishippingCheckoutOverview.xml index d304d305a726..a266b09278dd 100644 --- a/dev/tests/functional/tests/app/Magento/CheckoutAgreements/Test/Page/MultishippingCheckoutOverview.xml +++ b/dev/tests/functional/tests/app/Magento/CheckoutAgreements/Test/Page/MultishippingCheckoutOverview.xml @@ -7,6 +7,6 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/pages.xsd"> <page name="MultishippingCheckoutOverview" mca="multishipping/checkout/overview" module="Magento_Checkout"> - <block name="agreementReview" class="Magento\CheckoutAgreements\Test\Block\Multishipping\MultishippingAgreementReview" locator="#checkout-agreements" strategy="css selector"/> + <block name="agreementReview" class="Magento\CheckoutAgreements\Test\Block\Multishipping\MultishippingAgreementReview" locator=".checkout-agreements" strategy="css selector"/> </page> </config> diff --git a/dev/tests/functional/tests/app/Magento/CheckoutAgreements/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/CheckoutAgreements/Test/TestCase/NavigateMenuTest.xml index 18cbf32ded5c..3458e2944a9e 100644 --- a/dev/tests/functional/tests/app/Magento/CheckoutAgreements/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/CheckoutAgreements/Test/TestCase/NavigateMenuTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest17"> - <data name="tag" xsi:type="string">severity:S2</data> + <data name="tag" xsi:type="string">severity:S2, mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Stores > Terms and Conditions</data> <data name="pageTitle" xsi:type="string">Terms and Conditions</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/Cms/Test/Block/Adminhtml/Page/Edit/PageForm.php b/dev/tests/functional/tests/app/Magento/Cms/Test/Block/Adminhtml/Page/Edit/PageForm.php index 0b7c31a09228..c08ea7aa9e29 100644 --- a/dev/tests/functional/tests/app/Magento/Cms/Test/Block/Adminhtml/Page/Edit/PageForm.php +++ b/dev/tests/functional/tests/app/Magento/Cms/Test/Block/Adminhtml/Page/Edit/PageForm.php @@ -87,7 +87,7 @@ public function openTab($tabName) if ($tabHeader->isVisible() && strpos($tabHeader->getAttribute('class'), '_show') === false) { $tabHeader->hover(); $tabHeader->click(); - }; + } return $this; } diff --git a/dev/tests/functional/tests/app/Magento/Cms/Test/Page/CmsPage.xml b/dev/tests/functional/tests/app/Magento/Cms/Test/Page/CmsPage.xml index 22d29eae1656..1a7adcd531c5 100644 --- a/dev/tests/functional/tests/app/Magento/Cms/Test/Page/CmsPage.xml +++ b/dev/tests/functional/tests/app/Magento/Cms/Test/Page/CmsPage.xml @@ -7,6 +7,6 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/pages.xsd"> <page name="CmsPage" mca="cms/page" module="Magento_Cms"> - <block name="cmsPageBlock" class="Magento\Cms\Test\Block\Page" locator=".page-main" strategy="css selector" /> + <block name="cmsPageBlock" class="Magento\Cms\Test\Block\Page" locator="#maincontent" strategy="css selector" /> </page> </config> diff --git a/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/CreateCmsPageEntityMultipleStoreViewsTest.xml b/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/CreateCmsPageEntityMultipleStoreViewsTest.xml index 72a76dacc329..06fe76c5efd0 100644 --- a/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/CreateCmsPageEntityMultipleStoreViewsTest.xml +++ b/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/CreateCmsPageEntityMultipleStoreViewsTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Cms\Test\TestCase\CreateCmsPageEntityMultipleStoreViewsTest" summary="Page cache for different CMS pages on multiple store views" ticketId="MAGETWO-52467"> <variation name="CreateCmsPageEntityMultipleStoreViewsTestVariation1"> + <data name="issue" xsi:type="string">MC-13801: Test "Page cache for different CMS pages on multiple store views" fails on Jenkins</data> <data name="cmsPages/0/is_active" xsi:type="string">Yes</data> <data name="cmsPages/0/title" xsi:type="string">NewCmsPage</data> <data name="cmsPages/0/store_id/dataset" xsi:type="string">default</data> diff --git a/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/CreateCustomUrlRewriteEntityTest.xml b/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/CreateCustomUrlRewriteEntityTest.xml index 3682fce4267c..8adbc1ae46c7 100644 --- a/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/CreateCustomUrlRewriteEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/CreateCustomUrlRewriteEntityTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\UrlRewrite\Test\TestCase\CreateCustomUrlRewriteEntityTest" summary="Create Custom URL Rewrites" ticketId="MAGETWO-25474"> <variation name="CreateCustomUrlRewriteEntityTestVariation3"> - <data name="tag" xsi:type="string">severity:S1</data> + <data name="tag" xsi:type="string">severity:S1, mftf_migrated:yes</data> <data name="urlRewrite/data/entity_type" xsi:type="string">Custom</data> <data name="urlRewrite/data/store_id" xsi:type="string">Main Website/Main Website Store/Default Store View</data> <data name="urlRewrite/data/target_path/entity" xsi:type="string">cms/page/view/page_id/%cmsPage::default%</data> @@ -19,7 +19,7 @@ <constraint name="Magento\UrlRewrite\Test\Constraint\AssertUrlRewriteCustomRedirect" /> </variation> <variation name="CreateCustomUrlRewriteEntityTestVariation4"> - <data name="tag" xsi:type="string">severity:S1</data> + <data name="tag" xsi:type="string">severity:S1, mftf_migrated:yes</data> <data name="urlRewrite/data/entity_type" xsi:type="string">Custom</data> <data name="urlRewrite/data/store_id" xsi:type="string">Main Website/Main Website Store/Default Store View</data> <data name="urlRewrite/data/target_path/entity" xsi:type="string">cms/page/view/page_id/%cmsPage::default%</data> diff --git a/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/NavigateMenuTest.xml index f9f0a11c0a47..cff5f7f2a562 100644 --- a/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Cms/Test/TestCase/NavigateMenuTest.xml @@ -8,13 +8,13 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest18"> - <data name="tag" xsi:type="string">severity:S2</data> + <data name="tag" xsi:type="string">severity:S2, mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Content > Pages</data> <data name="pageTitle" xsi:type="string">Pages</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest19"> - <data name="tag" xsi:type="string">severity:S2</data> + <data name="tag" xsi:type="string">severity:S2, mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Content > Blocks</data> <data name="pageTitle" xsi:type="string">Blocks</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableImportExport/Test/TestCase/ExportProductsTest.xml b/dev/tests/functional/tests/app/Magento/ConfigurableImportExport/Test/TestCase/ExportProductsTest.xml index c157f5c58d40..0a2ce7ab7f18 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableImportExport/Test/TestCase/ExportProductsTest.xml +++ b/dev/tests/functional/tests/app/Magento/ConfigurableImportExport/Test/TestCase/ExportProductsTest.xml @@ -7,7 +7,8 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\CatalogImportExport\Test\TestCase\ExportProductsTest" summary="Export products"> - <variation name="ExportProductsTestVariation1" summary="Export simple product and configured products with assigned images" ticketId="MAGETWO-46112"> + <variation name="ExportProductsTestVariation7" summary="Export simple product and configured products with assigned images" ticketId="MAGETWO-46112"> + <data name="exportData" xsi:type="string">default</data> <data name="products/1" xsi:type="array"> <item name="fixture" xsi:type="string">configurableProduct</item> <item name="dataset" xsi:type="string">product_with_size</item> @@ -18,19 +19,48 @@ </item> </item> </data> + <data name="exportedFields" xsi:type="array"> + <item name="0" xsi:type="string">sku</item> + <item name="1" xsi:type="string">name</item> + <item name="2" xsi:type="string">weight</item> + <item name="3" xsi:type="string">visibility</item> + <item name="4" xsi:type="string">price</item> + <item name="5" xsi:type="string">url_key</item> + <item name="6" xsi:type="string">additional_images</item> + </data> </variation> - <variation name="ExportProductsTestVariation2" summary="Export simple and configured products with custom options" ticketId="MAGETWO-46113, MAGETWO-46109"> + <variation name="ExportProductsTestVariation8" summary="Export simple and configured products with custom options" ticketId="MAGETWO-46113, MAGETWO-46109"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> + <data name="exportData" xsi:type="string">default</data> <data name="products/1" xsi:type="array"> <item name="fixture" xsi:type="string">configurableProduct</item> <item name="dataset" xsi:type="string">first_product_with_custom_options_and_option_key_1</item> </data> + <data name="exportedFields" xsi:type="array"> + <item name="0" xsi:type="string">sku</item> + <item name="1" xsi:type="string">name</item> + <item name="2" xsi:type="string">weight</item> + <item name="3" xsi:type="string">visibility</item> + <item name="4" xsi:type="string">price</item> + <item name="5" xsi:type="string">url_key</item> + </data> </variation> - <variation name="ExportProductsTestVariation5" summary="Export simple product assigned to Main Website and configurable product assigned to Custom Website" ticketId="MAGETWO-46114"> + <variation name="ExportProductsTestVariation9" summary="Export simple product assigned to Main Website and configurable product assigned to Custom Website" ticketId="MAGETWO-46114"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> + <data name="exportData" xsi:type="string">default</data> <data name="products/1" xsi:type="array"> <item name="fixture" xsi:type="string">configurableProduct</item> <item name="dataset" xsi:type="string">default</item> <item name="store" xsi:type="string">custom_store</item> </data> + <data name="exportedFields" xsi:type="array"> + <item name="0" xsi:type="string">sku</item> + <item name="1" xsi:type="string">name</item> + <item name="2" xsi:type="string">weight</item> + <item name="3" xsi:type="string">visibility</item> + <item name="4" xsi:type="string">price</item> + <item name="5" xsi:type="string">url_key</item> + </data> </variation> </testCase> </config> diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Block/Adminhtml/Product/Composite/Configure.xml b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Block/Adminhtml/Product/Composite/Configure.xml index a66753c2adf2..d89fb3ddf88a 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Block/Adminhtml/Product/Composite/Configure.xml +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Block/Adminhtml/Product/Composite/Configure.xml @@ -7,9 +7,8 @@ --> <mapping strict="0"> <fields> - <qty /> <attribute> - <selector>//div[@class="product-options"]//label[.="%s"]//following-sibling::*//select</selector> + <selector>//div[contains(@class, "product-options")]//div//label[.="%s"]//following-sibling::*//select</selector> <strategy>xpath</strategy> <input>select</input> </attribute> diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Constraint/AssertConfigurableProductDuplicateForm.php b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Constraint/AssertConfigurableProductDuplicateForm.php index 02cb304325c2..c50f0e338c20 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Constraint/AssertConfigurableProductDuplicateForm.php +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Constraint/AssertConfigurableProductDuplicateForm.php @@ -70,7 +70,7 @@ protected function prepareFixtureData(array $data, array $sortFields = []) protected function prepareUrlKey($urlKey) { preg_match("~\d+$~", $urlKey, $matches); - $key = intval($matches[0]) + 1; + $key = (int)$matches[0] + 1; return str_replace($matches[0], $key, $urlKey); } 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 e1b6fcf47e56..f831173ba0ae 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 435d5aad4635..64f9141fba96 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/DeleteChildConfigurableProductTest.xml +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/DeleteChildConfigurableProductTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\ConfigurableProduct\Test\TestCase\DeleteChildConfigurableProductTest" summary="Configurable Product is not available on frontend after child products are deleted" ticketId="MAGETWO-70346"> <variation name="DeleteChildConfigurableProductTestVariation1" summary="Verify that variation's SKU based on parent SKU"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product/data/url_key" xsi:type="string">configurable-product-%isolation%</data> <data name="product/data/configurable_attributes_data/dataset" xsi:type="string">two_new_options_with_empty_sku</data> <data name="product/data/name" xsi:type="string">Configurable Product %isolation%</data> diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/DeleteProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/DeleteProductEntityTest.xml index 25f31b23fa66..68dc1ecbe787 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/DeleteProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/DeleteProductEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Product\DeleteProductEntityTest"> <variation name="DeleteProductEntityTestVariation9"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="products" xsi:type="string">configurableProduct::default</data> <data name="isRequired" xsi:type="string">Yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSuccessDeleteMessage" /> @@ -15,6 +16,7 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductIsNotDisplayingOnFrontend" /> </variation> <variation name="DeleteProductEntityTestVariation10"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="products" xsi:type="string">configurableProduct::with_one_option</data> <data name="isRequired" xsi:type="string">Yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSuccessDeleteMessage" /> diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/UpdateConfigurableProductEntityTest.php b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/UpdateConfigurableProductEntityTest.php index c7d66781738b..bb88bc854f75 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/UpdateConfigurableProductEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/UpdateConfigurableProductEntityTest.php @@ -32,7 +32,6 @@ class UpdateConfigurableProductEntityTest extends Scenario { /* tags */ const MVP = 'yes'; - const TO_MAINTAIN = 'yes'; /* end tags */ /** diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/ValidateOrderOfProductTypeTest.xml b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/ValidateOrderOfProductTypeTest.xml index a62bcf92ebf3..5af854cae543 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 6d22cea4689a..d576e760179e 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 082b93ba62e0..9108b44a0e85 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/VerifyConfigurableProductLayeredNavigationTest.xml +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/VerifyConfigurableProductLayeredNavigationTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\ConfigurableProduct\Test\TestCase\VerifyConfigurableProductLayeredNavigationTest" summary="Verify OOS option configurable product in Layered Navigation on storefront"> <variation name="VerifyConfigurableProductLayeredNavigationTestVariation1" summary="Verify the out of stock configurable attribute option doesn't show in Layered navigation" ticketId="MAGETWO-89745"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="product" xsi:type="string">configurableProduct::product_with_3_sizes</data> <data name="productUpdate/childProductUpdate" xsi:type="array"> <item name="data" xsi:type="array"> diff --git a/dev/tests/functional/tests/app/Magento/CurrencySymbol/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/CurrencySymbol/Test/TestCase/NavigateMenuTest.xml index 0a061eb4be6c..fc031a77ff53 100644 --- a/dev/tests/functional/tests/app/Magento/CurrencySymbol/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/CurrencySymbol/Test/TestCase/NavigateMenuTest.xml @@ -8,11 +8,13 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest20"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Stores > Currency Rates</data> <data name="pageTitle" xsi:type="string">Currency Rates</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest21"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Stores > Currency Symbols</data> <data name="pageTitle" xsi:type="string">Currency Symbols</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Account/AddressesAdditional.php b/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Account/AddressesAdditional.php index cba358549c3d..13ef742d0627 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Account/AddressesAdditional.php +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Account/AddressesAdditional.php @@ -20,14 +20,14 @@ class AddressesAdditional extends Block * * @var string */ - protected $addressSelector = '//li[address[contains(.,"%s")]]'; + protected $addressSelector = '//tbody//tr[contains(.,"%s")]'; /** * Selector for addresses block * * @var string */ - protected $addressesSelector = '//li[address]'; + protected $addressesSelector = '.additional-addresses'; /** * Selector for delete link @@ -74,16 +74,19 @@ public function deleteAdditionalAddress(Address $address) */ public function isAdditionalAddressExists($address) { - $additionalAddressExists = false; - - $addresses = $this->_rootElement->getElements($this->addressesSelector, Locator::SELECTOR_XPATH); - foreach ($addresses as $addressBlock) { - if (strpos($addressBlock->getText(), $address) === 0) { - $additionalAddressExists = $addressBlock->isVisible(); + $addressExists = true; + foreach (explode("\n", $address) as $addressItem) { + $addressElement = $this->_rootElement->find( + sprintf($this->addressSelector, $addressItem), + Locator::SELECTOR_XPATH + ); + if (!$addressElement->isVisible()) { + $addressExists = false; break; } } - return $additionalAddressExists; + + return $addressExists; } /** diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Address/Renderer.php b/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Address/Renderer.php index f9a3989d8f57..8d8a0cfe5ea1 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Address/Renderer.php +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Address/Renderer.php @@ -58,6 +58,11 @@ protected function getPattern() . "{{lastname}}{{depend}} {{suffix}}{{/depend}}\n{{/depend}}{{street}}\n" . "{{city}}, {{{$region}}} {{postcode}}\n{{country_id}}\n{{depend}}{{telephone}}{{/depend}}"; break; + case "html_without_company_separated_names": + $outputPattern = "{{depend}}{{prefix}}\n{{/depend}}{{firstname}}\n{{depend}}{{middlename}}\n{{/depend}}" + . "{{lastname}}{{depend}}\n{{suffix}}{{/depend}}\n{{/depend}}{{street}}\n" + . "{{city}}\n{{{$region}}}\n{{postcode}}\n{{country_id}}\n{{depend}}{{telephone}}{{/depend}}"; + break; case "html_for_select_element": $outputPattern = "{{depend}}{{prefix}} {{/depend}}{{firstname}} {{depend}}{{middlename}} {{/depend}}" . "{{lastname}}{{depend}} {{suffix}}{{/depend}}, {{/depend}}{{street}}, " diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Adminhtml/Edit/CustomerForm.php b/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Adminhtml/Edit/CustomerForm.php index 83d86669aa8e..f456635882ed 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Adminhtml/Edit/CustomerForm.php +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Adminhtml/Edit/CustomerForm.php @@ -65,6 +65,7 @@ class CustomerForm extends FormTabs * @param FixtureInterface $customer * @param FixtureInterface|FixtureInterface[]|null $address * @return $this + * @throws \Exception */ public function fillCustomer(FixtureInterface $customer, $address = null) { @@ -76,12 +77,29 @@ public function fillCustomer(FixtureInterface $customer, $address = null) } if (null !== $address) { $this->openTab('addresses'); - $this->getTab('addresses')->fillAddresses($address); + $this->fillCustomerAddress($address); } return $this; } + /** + * Fill customer address by provided in parameter data + * + * @param FixtureInterface|FixtureInterface[] $address + * @return $this + * @throws \Exception + */ + public function fillCustomerAddress($address) + { + $addressesTab = $this->getTab('addresses'); + $this->openTab('addresses'); + $addressesTab->waitForAddressesGrid(); + $addressesTab->fillAddresses($address); + + return $this; + } + /** * Update Customer forms on tabs by customer, addresses data. * @@ -98,13 +116,16 @@ public function updateCustomer(FixtureInterface $customer, $address = null, Addr if ($isHasData) { parent::fill($customer); } + $addressesTab = $this->getTab('addresses'); if ($addressToDelete !== null) { $this->openTab('addresses'); - $this->getTab('addresses')->deleteCustomerAddress($addressToDelete); + $addressesTab->waitForAddressesGrid(); + $addressesTab->deleteCustomerAddress($addressToDelete); } if ($address !== null) { $this->openTab('addresses'); - $this->getTab('addresses')->updateAddresses($address); + $addressesTab->waitForAddressesGrid(); + $addressesTab->updateAddresses($address); } return $this; @@ -124,6 +145,9 @@ public function getDataCustomer(FixtureInterface $customer, $address = null) $data = ['customer' => $customer->hasData() ? parent::getData($customer) : parent::getData()]; if (null !== $address) { $this->openTab('addresses'); + $this->waitForElementNotVisible($this->tabReadiness); + $this->waitForm(); + $this->getTab('addresses')->waitForAddressesGrid(); $data['addresses'] = $this->getTab('addresses')->getDataAddresses($address); } @@ -148,8 +172,10 @@ protected function waitForm() */ public function openTab($tabName) { + $this->waitForElementNotVisible($this->tabReadiness); parent::openTab($tabName); $this->waitForElementNotVisible($this->tabReadiness); + $this->waitForm(); return $this; } @@ -161,13 +187,10 @@ public function openTab($tabName) */ public function getJsErrors() { - $tabs = ['account_information', 'addresses']; $jsErrors = []; - foreach ($tabs as $tabName) { - $tab = $this->getTab($tabName); - $this->openTab($tabName); - $jsErrors = array_merge($jsErrors, $tab->getJsErrors()); - } + $tab = $this->getTab('account_information'); + $this->openTab('account_information'); + $jsErrors = array_merge($jsErrors, $tab->getJsErrors()); return $jsErrors; } diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Adminhtml/Edit/Tab/Addresses.php b/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Adminhtml/Edit/Tab/Addresses.php index 86df3ea91210..99a79a8a4a85 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Adminhtml/Edit/Tab/Addresses.php +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Adminhtml/Edit/Tab/Addresses.php @@ -29,7 +29,7 @@ class Addresses extends Tab * * @var string */ - protected $addNewAddress = '.address-list-actions .add'; + protected $addNewAddress = '.add-new-address-button'; /** * Selector for address block. @@ -48,25 +48,32 @@ class Addresses extends Tab protected $deleteAddress = '.action-delete'; /** - * Accept button selector. + * Open customer address. * * @var string */ - private $confirmModal = '.confirm._show[data-role=modal]'; + protected $customerAddress = '//*[contains(@class, "address-list-item")][%d]'; /** - * Open customer address. + * Magento loader. * * @var string */ - protected $customerAddress = '//*[contains(@class, "address-list-item")][%d]'; + protected $loader = '//ancestor::body/div[@data-role="loader"]'; /** - * Magento loader. + * Customer address modal window. * * @var string */ - protected $loader = '//ancestor::body/div[@data-role="loader"]'; + private $customerAddressModalForm = '.customer_form_areas_address_address_customer_address_update_modal'; + + /** + * Customer addresses list grid. + * + * @var string + */ + private $customerAddressesGrid = '.customer_form_areas_address_address_customer_address_listing'; /** * Object Manager. @@ -107,9 +114,11 @@ public function __construct( public function fillAddresses($address) { $addresses = is_array($address) ? $address : [$address]; + $customerAddressForm = $this->getCustomerAddressModalForm(); foreach ($addresses as $address) { $this->addNewAddress(); - $this->setFieldsData($address->getData(), $this->_rootElement); + $customerAddressForm->fillAddressData($address); + $customerAddressForm->saveAddress(); } return $this; @@ -136,8 +145,9 @@ public function updateAddresses($address) if (!$this->isVisibleCustomerAddress($addressNumber)) { $this->addNewAddress(); + } else { + $this->openCustomerAddress($addressNumber); } - $this->openCustomerAddress($addressNumber); $defaultAddress = ['default_billing' => 'No', 'default_shipping' => 'No']; $addressData = $address->getData(); @@ -146,9 +156,10 @@ public function updateAddresses($address) $defaultAddress[$key] = $value; } } - $this->_fill($this->dataMapping($defaultAddress)); - - $this->setFieldsData(array_diff($addressData, $defaultAddress), $this->_rootElement); + $customerAddressForm = $this->getCustomerAddressModalForm(); + $customerAddressForm->setFieldsData($this->dataMapping($defaultAddress)); + $customerAddressForm->setFieldsData(array_diff($addressData, $defaultAddress)); + $customerAddressForm->saveAddress(); } return $this; @@ -168,6 +179,10 @@ public function getDataAddresses($address = null) foreach ($addresses as $addressNumber => $address) { $hasData = (null !== $address) && $address->hasData(); + $customerAddressesGrid = $this->getCustomerAddressesGrid(); + if ($hasData) { + $customerAddressesGrid->search($address->getData()); + } $isVisibleCustomerAddress = $this->isVisibleCustomerAddress($addressNumber); if ($hasData && !$isVisibleCustomerAddress) { @@ -177,14 +192,36 @@ public function getDataAddresses($address = null) if (!$hasData && !$isVisibleCustomerAddress) { $data[$addressNumber] = []; } else { - $this->openCustomerAddress($addressNumber); - $data[$addressNumber] = $this->getData($address, $this->_rootElement); + $customerAddressesGrid->openFirstRow(); + $data[$addressNumber] = $this->getCustomerAddressModalForm() + ->getData($address, $this->browser->find($this->customerAddressModalForm)); + $this->getCustomerAddressModalForm()->clickCancelButton(); } } return $data; } + /** + * Get data from Customer addresses. + * + * @param FixtureInterface|FixtureInterface[]|null $address + * @return array|null + * @throws \Exception + */ + public function getAddressFromFirstRow($address = null) + { + $customerAddressesGrid = $this->getCustomerAddressesGrid(); + $customerAddressesGrid->resetFilter(); + $customerAddressesGrid->openFirstRow(); + if ($this->getCustomerAddressModalForm()->isVisible()) { + $address = $this->getCustomerAddressModalForm() + ->getData($address, $this->browser->find($this->customerAddressModalForm)); + } + + return $address; + } + /** * Get data to fields on tab. * @@ -206,6 +243,7 @@ public function getFieldsData($fields = null, SimpleElement $element = null) protected function addNewAddress() { $this->_rootElement->find($this->addNewAddress)->click(); + $this->waitForElementVisible($this->customerAddressModalForm); } /** @@ -216,31 +254,24 @@ protected function addNewAddress() */ protected function openCustomerAddress($addressNumber) { - $addressTab = $this->_rootElement->find( - sprintf($this->customerAddress, $addressNumber), - Locator::SELECTOR_XPATH - ); - - if (!$addressTab->isVisible()) { + $customerAddressesGrid = $this->getCustomerAddressesGrid(); + if (!$customerAddressesGrid->getFirstRow()->isVisible()) { throw new \Exception("Can't open customer address #{$addressNumber}"); } - $addressTab->click(); + $customerAddressesGrid->openFirstRow(); } /** * Check is visible customer address. * - * @param int $addressNumber * @return bool */ - protected function isVisibleCustomerAddress($addressNumber) + protected function isVisibleCustomerAddress() { - $addressTab = $this->_rootElement->find( - sprintf($this->customerAddress, $addressNumber), - Locator::SELECTOR_XPATH - ); + $customerAddressesGrid = $this->getCustomerAddressesGrid(); + $customerAddressesGrid->isFirstRowVisible(); - return $addressTab->isVisible(); + return $customerAddressesGrid->isFirstRowVisible(); } /** @@ -279,24 +310,43 @@ public function getCountriesList($addressNumber) */ public function deleteCustomerAddress(Address $addressToDelete) { - $addressRenderer = $this->objectManager->create( - \Magento\Customer\Test\Block\Address\Renderer::class, - ['address' => $addressToDelete, 'type' => 'html'] - ); - $addressToDelete = $addressRenderer->render(); + $customerAddressesGrid = $this->getCustomerAddressesGrid(); + $customerAddressesGrid->deleteCustomerAddress($addressToDelete->getData()); - $dataList = explode("\n", $addressToDelete); - $dataList = implode("') and contains(.,'", $dataList); + return $this; + } - $this->_rootElement - ->find(sprintf($this->addressSelector, $dataList), Locator::SELECTOR_XPATH) - ->find($this->deleteAddress)->click(); + /** + * Get new/update customer address modal form. + * + * @return \Magento\Customer\Test\Block\Adminhtml\Edit\Tab\Addresses\AddressForm + */ + public function getCustomerAddressModalForm() + { + return $this->blockFactory->create( + \Magento\Customer\Test\Block\Adminhtml\Edit\Tab\Addresses\AddressForm::class, + ['element' => $this->browser->find($this->customerAddressModalForm)] + ); + } - $element = $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' => $element]); - $modal->acceptAlert(); + /** + * Get customer addresses grid. + * + * @return \Magento\Customer\Test\Block\Adminhtml\Edit\Tab\Addresses\AddressesGrid + */ + public function getCustomerAddressesGrid() + { + return $this->blockFactory->create( + \Magento\Customer\Test\Block\Adminhtml\Edit\Tab\Addresses\AddressesGrid::class, + ['element' => $this->browser->find($this->customerAddressesGrid)] + ); + } - return $this; + /** + * Wait for addresses grid rendering + */ + public function waitForAddressesGrid() + { + $this->waitForElementVisible($this->customerAddressesGrid); } } diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Adminhtml/Edit/Tab/Addresses.xml b/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Adminhtml/Edit/Tab/Addresses.xml deleted file mode 100644 index 8e16226adcc3..000000000000 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Adminhtml/Edit/Tab/Addresses.xml +++ /dev/null @@ -1,81 +0,0 @@ -<?xml version="1.0" ?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<mapping strict="0"> - <fields> - <prefix> - <selector>.address-item-edit:not([style="display: none;"]) [name$="[prefix]"]</selector> - <strategy>css selector</strategy> - </prefix> - <firstname> - <selector>.address-item-edit:not([style="display: none;"]) [name$="[firstname]"]</selector> - <strategy>css selector</strategy> - </firstname> - <middlename> - <selector>.address-item-edit:not([style="display: none;"]) [name$="[middlename]"]</selector> - <strategy>css selector</strategy> - </middlename> - <lastname> - <selector>.address-item-edit:not([style="display: none;"]) [name$="[lastname]"]</selector> - <strategy>css selector</strategy> - </lastname> - <suffix> - <selector>.address-item-edit:not([style="display: none;"]) [name$="[suffix]"]</selector> - <strategy>css selector</strategy> - </suffix> - <company> - <selector>.address-item-edit:not([style="display: none;"]) [name$="[company]"]</selector> - <strategy>css selector</strategy> - </company> - <street> - <selector>.address-item-edit:not([style="display: none;"]) [name$="[street][0]"]</selector> - <strategy>css selector</strategy> - </street> - <city> - <selector>.address-item-edit:not([style="display: none;"]) [name$="[city]"]</selector> - <strategy>css selector</strategy> - </city> - <country_id> - <input>select</input> - <selector>.address-item-edit:not([style="display: none;"]) [name$="[country_id]"]</selector> - <strategy>css selector</strategy> - </country_id> - <region_id> - <input>select</input> - <selector>.address-item-edit:not([style="display: none;"]) [name$="[region_id]"]</selector> - <strategy>css selector</strategy> - </region_id> - <region> - <selector>.address-item-edit:not([style="display: none;"]) [name$="[region]"]</selector> - <strategy>css selector</strategy> - </region> - <postcode> - <selector>.address-item-edit:not([style="display: none;"]) [name$="[postcode]"]</selector> - <strategy>css selector</strategy> - </postcode> - <telephone> - <selector>.address-item-edit:not([style="display: none;"]) [name$="[telephone]"]</selector> - <strategy>css selector</strategy> - </telephone> - <fax> - <selector>.address-item-edit:not([style="display: none;"]) [name$="[fax]"]</selector> - <strategy>css selector</strategy> - </fax> - <vat_id> - <selector>.address-item-edit:not([style="display: none;"]) [name$="[vat_id]"]</selector> - <strategy>css selector</strategy> - </vat_id> - <default_billing> - <selector>.ui-state-active [name$="[default_billing]"]</selector> - <input>checkbox</input> - </default_billing> - <default_shipping> - <selector>.ui-state-active [name$="[default_shipping]"]</selector> - <input>checkbox</input> - </default_shipping> - </fields> -</mapping> diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Adminhtml/Edit/Tab/Addresses/AddressForm.php b/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Adminhtml/Edit/Tab/Addresses/AddressForm.php new file mode 100644 index 000000000000..029836d21433 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Adminhtml/Edit/Tab/Addresses/AddressForm.php @@ -0,0 +1,126 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Test\Block\Adminhtml\Edit\Tab\Addresses; + +use Magento\Mtf\Client\Locator; +use Magento\Mtf\Block\Form; + +/** + * Create/Edit customer address. + */ +class AddressForm extends Form +{ + /** + * Save address button + * + * @var string + */ + protected $saveAddressButton = '#save'; + + /** + * cancel button + * + * @var string + */ + protected $cancelButton = '#cancel'; + + /** + * Loader mask + * + * @var string + */ + private $loader = '.popup-loading'; + + /** + * Field with Mage error. + * + * @var string + */ + private $mageErrorField = '//fieldset/*[contains(@class,"field ")][.//*[contains(@class,"error")]]'; + + /** + * Fields label with mage error. + * + * @var string + */ + private $mageErrorLabel = './/*[contains(@class,"label")]'; + + /** + * Mage error text. + * + * @var string + */ + private $mageErrorText = './/label[contains(@class,"error")]'; + + /** + * Fill address form by provided data + * + * @param \Magento\Mtf\Fixture\FixtureInterface $address + * @return void + * @throws \Exception + */ + public function fillAddressData(\Magento\Mtf\Fixture\FixtureInterface $address) + { + $this->waitForElementNotVisible($this->loader); + $this->setFieldsData($address->getData(), $this->_rootElement); + } + + /** + * Fill data into fields in the container. + * + * @param array $fields + * @param \Magento\Mtf\Client\Element\SimpleElement|null $contextElement + * @return void + * @throws \Exception + */ + public function setFieldsData(array $fields, \Magento\Mtf\Client\Element\SimpleElement $contextElement = null): void + { + $data = $this->dataMapping($fields); + $this->_fill($data, $contextElement); + } + + /** + * Save customer address + * + * @return void + */ + public function saveAddress(): void + { + $this->_rootElement->find($this->saveAddressButton)->click(); + $this->waitForElementNotVisible($this->loader); + } + + /** + * Close create/update address modal + * + * @return void + */ + public function clickCancelButton(): void + { + $this->_rootElement->find($this->cancelButton)->click(); + } + + /** + * Get array of label => js error text. + * + * @return array + */ + public function getJsErrors(): array + { + $data = []; + $elements = $this->_rootElement->getElements($this->mageErrorField, Locator::SELECTOR_XPATH); + foreach ($elements as $element) { + $error = $element->find($this->mageErrorText, Locator::SELECTOR_XPATH); + if ($error->isVisible()) { + $label = $element->find($this->mageErrorLabel, Locator::SELECTOR_XPATH)->getText(); + $data[$label] = $error->getText(); + } + } + return $data; + } +} diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Adminhtml/Edit/Tab/Addresses/AddressForm.xml b/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Adminhtml/Edit/Tab/Addresses/AddressForm.xml new file mode 100644 index 000000000000..c56c6479dcb3 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Adminhtml/Edit/Tab/Addresses/AddressForm.xml @@ -0,0 +1,81 @@ +<?xml version="1.0" ?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<mapping strict="0"> + <fields> + <prefix> + <selector>.admin__fieldset:not([style="display: none;"]) [name="prefix"]</selector> + <strategy>css selector</strategy> + </prefix> + <firstname> + <selector>.admin__fieldset:not([style="display: none;"]) [name="firstname"]</selector> + <strategy>css selector</strategy> + </firstname> + <middlename> + <selector>.admin__fieldset:not([style="display: none;"]) [name="middlename"]</selector> + <strategy>css selector</strategy> + </middlename> + <lastname> + <selector>.admin__fieldset:not([style="display: none;"]) [name="lastname"]</selector> + <strategy>css selector</strategy> + </lastname> + <suffix> + <selector>.admin__fieldset:not([style="display: none;"]) [name="suffix"]</selector> + <strategy>css selector</strategy> + </suffix> + <company> + <selector>.admin__fieldset:not([style="display: none;"]) [name="company"]</selector> + <strategy>css selector</strategy> + </company> + <street> + <selector>.admin__fieldset:not([style="display: none;"]) [name="street[0]"]</selector> + <strategy>css selector</strategy> + </street> + <city> + <selector>.admin__fieldset:not([style="display: none;"]) [name="city"]</selector> + <strategy>css selector</strategy> + </city> + <country_id> + <input>select</input> + <selector>.admin__fieldset:not([style="display: none;"]) [name="country_id"]</selector> + <strategy>css selector</strategy> + </country_id> + <region_id> + <input>select</input> + <selector>.admin__fieldset:not([style="display: none;"]) [name="region_id"]</selector> + <strategy>css selector</strategy> + </region_id> + <region> + <selector>.admin__fieldset:not([style="display: none;"]) [name="region"]</selector> + <strategy>css selector</strategy> + </region> + <postcode> + <selector>.admin__fieldset:not([style="display: none;"]) [name="postcode"]</selector> + <strategy>css selector</strategy> + </postcode> + <telephone> + <selector>.admin__fieldset:not([style="display: none;"]) [name="telephone"]</selector> + <strategy>css selector</strategy> + </telephone> + <fax> + <selector>.admin__fieldset:not([style="display: none;"]) [name="fax"]</selector> + <strategy>css selector</strategy> + </fax> + <vat_id> + <selector>.admin__fieldset:not([style="display: none;"]) [name="vat_id"]</selector> + <strategy>css selector</strategy> + </vat_id> + <default_billing> + <selector>.admin__fieldset:not([style="display: none;"]) input[name='default_billing']</selector> + <input>switcher</input> + </default_billing> + <default_shipping> + <selector>.admin__fieldset:not([style="display: none;"]) input[name='default_shipping']</selector> + <input>switcher</input> + </default_shipping> + </fields> +</mapping> diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Adminhtml/Edit/Tab/Addresses/AddressesGrid.php b/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Adminhtml/Edit/Tab/Addresses/AddressesGrid.php new file mode 100644 index 000000000000..ef1fb09c8f83 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/Block/Adminhtml/Edit/Tab/Addresses/AddressesGrid.php @@ -0,0 +1,174 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Test\Block\Adminhtml\Edit\Tab\Addresses; + +use Magento\Ui\Test\Block\Adminhtml\DataGrid; + +/** + * Class AddressesGrid + * Backend customer addresses grid + * + */ +class AddressesGrid extends DataGrid +{ + /** + * Locator value for link in action column + * + * @var string + */ + protected $editLink = '//tr[@class="data-row"][1]//a[@data-action="item-edit"]'; + + /** + * First row selector + * + * @var string + */ + protected $firstRowSelector = '//tr[@class="data-row"][1]'; + + /** + * Customer address grid loader. + * + * @var string + */ + protected $loader = '.customer_form_areas_address_address_customer_address_listing [data-role="spinner"]'; + + /** + * Filters array mapping + * + * @var array + */ + protected $filters = [ + 'firstname' => [ + 'selector' => '.admin__data-grid-filters input[name*=firstname]', + ], + 'lastname' => [ + 'selector' => '.admin__data-grid-filters input[name*=lastname]', + ], + 'street' => [ + 'selector' => '.admin__data-grid-filters input[name*=street]', + ], + 'city' => [ + 'selector' => '.admin__data-grid-filters input[name*=city]', + ], + 'region_id' => [ + 'selector' => '.admin__data-grid-filters input[name*=region]', + ], + 'postcode' => [ + 'selector' => '.admin__data-grid-filters input[name*=postcode]', + ], + 'telephone' => [ + 'selector' => '.admin__data-grid-filters input[name*=telephone]', + ], + 'country_id' => [ + 'selector' => '.admin__data-grid-filters select[name*=country]', + 'input' => 'select', + ], + + ]; + + /** + * Select action toggle. + * + * @var string + */ + private $selectAction = '.action-select'; + + /** + * Delete action toggle. + * + * @var string + */ + private $deleteAddress = '[data-action="item-delete"]'; + + /** + * Locator value for "Edit" link inside action column. + * + * @var string + */ + private $editAddress = '[data-action="item-edit"]'; + + /** + * Customer address modal window. + * + * @var string + */ + private $customerAddressModalForm = '.customer_form_areas_address_address_customer_address_update_modal'; + + /** + * Search customer address by filter. + * + * @param array $filter + * @return void + */ + public function search(array $filter): void + { + parent::search(array_intersect_key($filter, $this->filters)); + } + + /** + * Delete customer address by filter + * + * @param array $filter + * @return void + * @throws \Exception + */ + public function deleteCustomerAddress(array $filter): void + { + $this->search($filter); + $rowItem = $this->getRow([$filter['firstname']]); + if ($rowItem->isVisible()) { + $this->deleteRowItemAddress($rowItem); + } else { + throw new \Exception("Searched item was not found by filter\n" . print_r($filter, true)); + } + } + + /** + * @param \Magento\Mtf\Client\Element\SimpleElement $rowItem + * @return void + */ + public function deleteRowItemAddress(\Magento\Mtf\Client\Element\SimpleElement $rowItem): void + { + $rowItem->find($this->selectAction)->click(); + $rowItem->find($this->deleteAddress)->click(); + $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(); + $this->waitLoader(); + } + + /** + * Open first row from the addresses grid + * + * @return void + */ + public function openFirstRow(): void + { + $firstRow = $this->getFirstRow(); + if ($firstRow->isVisible()) { + $firstRow->find($this->selectAction)->click(); + $firstRow->find($this->editAddress)->click(); + $this->waitForElementVisible($this->customerAddressModalForm); + $this->waitLoader(); + } + } + + /** + * Get first row from the grid + * + * @return \Magento\Mtf\Client\Element\SimpleElement + */ + public function getFirstRow(): \Magento\Mtf\Client\Element\SimpleElement + { + return $this->_rootElement->find($this->firstRowSelector, \Magento\Mtf\Client\Locator::SELECTOR_XPATH); + } +} diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/Constraint/AssertAdditionalAddressCreatedFrontend.php b/dev/tests/functional/tests/app/Magento/Customer/Test/Constraint/AssertAdditionalAddressCreatedFrontend.php index abfee067a73d..4d086cf06053 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/Constraint/AssertAdditionalAddressCreatedFrontend.php +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/Constraint/AssertAdditionalAddressCreatedFrontend.php @@ -28,7 +28,7 @@ public function processAssert(CustomerAccountIndex $customerAccountIndex, Addres $customerAccountIndex->getAccountMenuBlock()->openMenuItem('Address Book'); $addressRenderer = $this->objectManager->create( \Magento\Customer\Test\Block\Address\Renderer::class, - ['address' => $shippingAddress, 'type' => 'html'] + ['address' => $shippingAddress, 'type' => 'html_without_company_separated_names'] )->render(); $isAddressExists = $customerAccountIndex->getAdditionalAddressBlock() ->isAdditionalAddressExists($addressRenderer); diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/Constraint/AssertCustomerAddressBackendRequiredFields.php b/dev/tests/functional/tests/app/Magento/Customer/Test/Constraint/AssertCustomerAddressBackendRequiredFields.php new file mode 100644 index 000000000000..46bb8ce72abb --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/Constraint/AssertCustomerAddressBackendRequiredFields.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Test\Constraint; + +use Magento\Customer\Test\Page\Adminhtml\CustomerIndexNew; +use Magento\Mtf\Constraint\AbstractConstraint; + +/** + * Assert required fields on customer address form. + */ +class AssertCustomerAddressBackendRequiredFields extends AbstractConstraint +{ + /** + * Expected message. + */ + const REQUIRE_MESSAGE = 'This is a required field.'; + + /** + * Assert required fields on customer address form. + * @param CustomerIndexNew $customerNewPage + * @param array $expectedRequiredFields + * @return void + * @throws \Exception + */ + public function processAssert(CustomerIndexNew $customerNewPage, array $expectedRequiredFields): void + { + $actualRequiredFields = $customerNewPage->getCustomerForm()->getTab('addresses') + ->getCustomerAddressModalForm()->getJsErrors(); + foreach ($expectedRequiredFields as $field) { + \PHPUnit\Framework\Assert::assertTrue( + isset($actualRequiredFields[$field]), + "Field '$field' is not highlighted with an JS error." + ); + \PHPUnit\Framework\Assert::assertEquals( + self::REQUIRE_MESSAGE, + $actualRequiredFields[$field], + "Field '$field' is not highlighted as required." + ); + } + } + + /** + * Return string representation of object. + * + * @return string + */ + public function toString(): string + { + return 'All required fields on customer form are highlighted.'; + } +} 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 9765b9d9340a..f94d99b07ec6 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.php b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/CreateCustomerBackendEntityTest.php index f1a8b17ae285..ccc6da81038f 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/CreateCustomerBackendEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/CreateCustomerBackendEntityTest.php @@ -112,15 +112,15 @@ public function __inject( * Create customer on backend. * * @param Customer $customer - * @param string $customerAction - * @param Address $address + * @param null $customerAction + * @param Address|null $address * @param array $steps * @param array $beforeActionCallback - * @return void + * @throws \Exception */ public function test( Customer $customer, - $customerAction, + $customerAction = null, Address $address = null, array $steps = [], array $beforeActionCallback = [] @@ -135,7 +135,12 @@ public function test( $this->pageCustomerIndex->open(); $this->pageCustomerIndex->getPageActionsBlock()->addNew(); - $this->pageCustomerIndexNew->getCustomerForm()->fillCustomer($customer, $address); + $this->pageCustomerIndexNew->getCustomerForm()->fillCustomer($customer); + if (null !== $address) { + $this->pageCustomerIndexNew->getPageActionsBlock()->saveAndContinue(); + $this->pageCustomerIndexNew->getMessagesBlock()->waitSuccessMessage(); + $this->pageCustomerIndexNew->getCustomerForm()->fillCustomerAddress($address); + } $this->address = $address; $this->customer = $customer; @@ -144,8 +149,9 @@ public function test( call_user_func([$this, $methodName]); } } - - $this->pageCustomerIndexNew->getPageActionsBlock()->$customerAction(); + if (null !== $customerAction) { + $this->pageCustomerIndexNew->getPageActionsBlock()->$customerAction(); + } } /** 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 502ac2495357..ff87a1df60fe 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> @@ -115,8 +120,8 @@ </data> <constraint name="Magento\Customer\Test\Constraint\AssertCustomerBackendRequiredFields" /> </variation> - <variation name="CreateCustomerBackendEntityTestVariation9" summary="Verify required fields on Addresses tab."> - <data name="customerAction" xsi:type="string">save</data> + <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> @@ -131,7 +136,7 @@ <item name="5" xsi:type="string">Zip/Postal Code</item> <item name="6" xsi:type="string">Phone Number</item> </data> - <constraint name="Magento\Customer\Test\Constraint\AssertCustomerBackendRequiredFields" /> + <constraint name="Magento\Customer\Test\Constraint\AssertCustomerAddressBackendRequiredFields" /> </variation> <variation name="CreateCustomerBackendEntityTestVariation10" summary="Create customer with 2 websites and with different allowed countries."> <data name="tag" xsi:type="string">to_maintain:yes</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 77b63fe39d4a..3807360e89ec 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/DeleteCustomerBackendEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/DeleteCustomerBackendEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Customer\Test\TestCase\DeleteCustomerBackendEntityTest" summary="Delete Customer Backend Entity" ticketId="MAGETWO-24764"> <variation name="DeleteCustomerBackendEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="customer/dataset" xsi:type="string">default</data> <constraint name="Magento\Customer\Test\Constraint\AssertCustomerSuccessDeleteMessage" /> <constraint name="Magento\Customer\Test\Constraint\AssertCustomerNotInGrid" /> diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/DeleteCustomerGroupEntityTest.xml b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/DeleteCustomerGroupEntityTest.xml index cffbbca8ad5c..4251a1c5ee9c 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/DeleteCustomerGroupEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/DeleteCustomerGroupEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Customer\Test\TestCase\DeleteCustomerGroupEntityTest" summary="Delete Customer Group" ticketId="MAGETWO-25243"> <variation name="DeleteCustomerGroupEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="customer/dataset" xsi:type="string">customer_with_new_customer_group</data> <data name="customer/data/group_id/dataset" xsi:type="string">default</data> <data name="defaultCustomerGroup/dataset" xsi:type="string">General</data> diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/ForgotPasswordOnFrontendTest.xml b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/ForgotPasswordOnFrontendTest.xml index 0d168451c6d3..7bf916157c5d 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 2dea52c1b7fc..bf84d2be43a1 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 4086a8585c8a..c868d3332a5a 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/MassAssignCustomerGroupTest.xml +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/MassAssignCustomerGroupTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Customer\Test\TestCase\MassAssignCustomerGroupTest" summary="Mass Assign Customer's Group to Customers" ticketId="MAGETWO-27892"> <variation name="MassAssignCustomerGroupTestVariation1" summary="Customer is created and mass action for changing customer group to created group is applied"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="customers" xsi:type="array"> <item name="0" xsi:type="string">default</item> </data> @@ -16,6 +17,7 @@ <constraint name="Magento\Customer\Test\Constraint\AssertCustomerGroupInGrid" /> </variation> <variation name="MassAssignCustomerGroupTestVariation2" summary="Two customers are created and mass actions for changing customer group to 'Retail' is applied" ticketId="MAGETWO-19456"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="customers" xsi:type="array"> <item name="0" xsi:type="string">default</item> <item name="1" xsi:type="string">customer_US</item> diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/NavigateMenuTest.xml index 404e62dcad64..a4a3aa34f9f1 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/NavigateMenuTest.xml @@ -8,16 +8,19 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest22"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Customers > All Customers</data> <data name="pageTitle" xsi:type="string">Customers</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest23"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Customers > Now Online</data> <data name="pageTitle" xsi:type="string">Customers Now Online</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest24"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Customers > Customer Groups</data> <data name="pageTitle" xsi:type="string">Customer Groups</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/PasswordAutocompleteOffTest.xml b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/PasswordAutocompleteOffTest.xml index b4188ebd98ae..7f267c1f171f 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 c1cf53358311..86c8e56d232d 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 24f68f686fdc..095746cfdef5 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 2cbb9a0315e1..b96d688b0c4b 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/UpdateCustomerFrontendEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/UpdateCustomerFrontendEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Customer\Test\TestCase\UpdateCustomerFrontendEntityTest" summary="Update Customer on Frontend" ticketId="MAGETWO-25925"> <variation name="UpdateCustomerFrontendEntityTestVariation1" summary="No XSS injection on update customer information add address" ticketId="MAGETWO-47189"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="customer/data/firstname" xsi:type="string">Patrick</title></head><svg/onload=alert('XSS')></data> <data name="customer/data/lastname" xsi:type="string"><script>alert('Last name')</script></data> <data name="customer/data/current_password" xsi:type="string">123123^q</data> @@ -25,6 +26,7 @@ <constraint name="Magento\Customer\Test\Constraint\AssertCustomerForm" /> </variation> <variation name="UpdateCustomerFrontendEntityTestVariation2" summary="Update customer information and add UK address, assert customer name and address on address book tab"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="customer/data/firstname" xsi:type="string">Jonny %isolation%</data> <data name="customer/data/lastname" xsi:type="string">Doe %isolation%</data> <data name="customer/data/email" xsi:type="string">jonny%isolation%@example.com</data> @@ -44,6 +46,7 @@ <constraint name="Magento\Customer\Test\Constraint\AssertCustomerNameFrontend" /> </variation> <variation name="UpdateCustomerFrontendEntityTestVariation3" summary="Update customer information and add France address"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="customer/data/firstname" xsi:type="string">Jean %isolation%</data> <data name="customer/data/lastname" xsi:type="string">Reno %isolation%</data> <data name="customer/data/email" xsi:type="string">jean%isolation%@example.com</data> diff --git a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/VerifyDisabledCustomerGroupFieldTest.xml b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/VerifyDisabledCustomerGroupFieldTest.xml index 70a912a3b5ff..e88e5161e474 100644 --- a/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/VerifyDisabledCustomerGroupFieldTest.xml +++ b/dev/tests/functional/tests/app/Magento/Customer/Test/TestCase/VerifyDisabledCustomerGroupFieldTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Customer\Test\TestCase\VerifyDisabledCustomerGroupFieldTest" summary="Check that field is disabled in system Customer Group" ticketId="MAGETWO-52481"> <variation name="VerifyDisabledCustomerGroupField1" summary="Checks that customer_group_code field is disabled in NOT LOGGED IN Customer Group"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="customerGroup/dataset" xsi:type="string">NOT_LOGGED_IN</data> <data name="disabledFields" xsi:type="array"> <item name="0" xsi:type="string">customer_group_code</item> diff --git a/dev/tests/functional/tests/app/Magento/CustomerImportExport/Test/Constraint/AssertImportCustomerAddresses.php b/dev/tests/functional/tests/app/Magento/CustomerImportExport/Test/Constraint/AssertImportCustomerAddresses.php index 18b2d44a22ff..f73609b3ba2c 100644 --- a/dev/tests/functional/tests/app/Magento/CustomerImportExport/Test/Constraint/AssertImportCustomerAddresses.php +++ b/dev/tests/functional/tests/app/Magento/CustomerImportExport/Test/Constraint/AssertImportCustomerAddresses.php @@ -116,8 +116,7 @@ private function getPrepareAddresses() foreach ($customers as $customer) { $this->customerIndexEdit->open(['id' => $customer->getId()]); $customerForm->openTab('addresses'); - $addresses = $customerForm->getTab('addresses')->getDataAddresses($addressTemplate); - $address = array_shift($addresses); + $address = $customerForm->getTab('addresses')->getAddressFromFirstRow($addressTemplate); if (!empty($address)) { $resultAddressesArray[] = $address; } 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 1f046f5111df..17dfb4fb8cda 100644 --- a/dev/tests/functional/tests/app/Magento/CustomerImportExport/Test/TestCase/ExportCustomerAddressesTest.php +++ b/dev/tests/functional/tests/app/Magento/CustomerImportExport/Test/TestCase/ExportCustomerAddressesTest.php @@ -10,6 +10,7 @@ use Magento\ImportExport\Test\Page\Adminhtml\AdminExportIndex; use Magento\Mtf\Fixture\FixtureFactory; use Magento\Mtf\TestCase\Injectable; +use Magento\Mtf\Util\Command\Cli\Cron; /** * Preconditions: @@ -42,19 +43,29 @@ class ExportCustomerAddressesTest extends Injectable */ private $adminExportIndex; + /** + * Cron command + * + * @var Cron + */ + private $cron; + /** * Inject pages. * * @param FixtureFactory $fixtureFactory * @param AdminExportIndex $adminExportIndex + * @param Cron $cron * @return void */ public function __inject( FixtureFactory $fixtureFactory, - AdminExportIndex $adminExportIndex + AdminExportIndex $adminExportIndex, + Cron $cron ) { $this->fixtureFactory = $fixtureFactory; $this->adminExportIndex = $adminExportIndex; + $this->cron = $cron; } /** @@ -68,12 +79,16 @@ public function test( ExportData $exportData, Customer $customer ) { + $this->cron->run(); + $this->cron->run(); $customer->persist(); $this->adminExportIndex->open(); + $this->adminExportIndex->getExportedGrid()->deleteAllExportedFiles(); $exportData->persist(); $this->adminExportIndex->getExportForm()->fill($exportData); $this->adminExportIndex->getFilterExport()->clickContinue(); - + $this->cron->run(); + $this->cron->run(); return [ 'customer' => $customer ]; diff --git a/dev/tests/functional/tests/app/Magento/Downloadable/Test/Block/Adminhtml/Catalog/Product/Edit/Section/Downloadable.php b/dev/tests/functional/tests/app/Magento/Downloadable/Test/Block/Adminhtml/Catalog/Product/Edit/Section/Downloadable.php index ebc19eec9ad5..d78a2d94e763 100644 --- a/dev/tests/functional/tests/app/Magento/Downloadable/Test/Block/Adminhtml/Catalog/Product/Edit/Section/Downloadable.php +++ b/dev/tests/functional/tests/app/Magento/Downloadable/Test/Block/Adminhtml/Catalog/Product/Edit/Section/Downloadable.php @@ -104,4 +104,21 @@ public function setFieldsData(array $fields, SimpleElement $element = null) return $this; } + + /** + * Set "Is this downloadable Product?" value. + * + * @param string $downloadable + * @param SimpleElement|null $element + * @return void + */ + public function setIsDownloadable(string $downloadable = 'Yes', SimpleElement $element = null): void + { + $context = $element ?: $this->_rootElement; + $isDownloadable = $context->find($this->isDownloadableProduct); + $value = 'Yes' == $downloadable ? '1' : '0'; + if ($isDownloadable->isVisible() && $isDownloadable->getAttribute('value') != $value) { + $isDownloadable->click(); + } + } } diff --git a/dev/tests/functional/tests/app/Magento/Downloadable/Test/Block/Adminhtml/Product/Composite/Configure.xml b/dev/tests/functional/tests/app/Magento/Downloadable/Test/Block/Adminhtml/Product/Composite/Configure.xml index 7a7a6d2124cb..2f721f05f5ee 100644 --- a/dev/tests/functional/tests/app/Magento/Downloadable/Test/Block/Adminhtml/Product/Composite/Configure.xml +++ b/dev/tests/functional/tests/app/Magento/Downloadable/Test/Block/Adminhtml/Product/Composite/Configure.xml @@ -7,7 +7,6 @@ --> <mapping strict="0"> <fields> - <qty /> <link> <selector>//*[@id="downloadable-links-list"]/*[contains(.,"%link_name%")]//input</selector> <strategy>xpath</strategy> diff --git a/dev/tests/functional/tests/app/Magento/Downloadable/Test/Constraint/AssertDownloadableDuplicateForm.php b/dev/tests/functional/tests/app/Magento/Downloadable/Test/Constraint/AssertDownloadableDuplicateForm.php index e5d97e1511e7..2033189214e1 100644 --- a/dev/tests/functional/tests/app/Magento/Downloadable/Test/Constraint/AssertDownloadableDuplicateForm.php +++ b/dev/tests/functional/tests/app/Magento/Downloadable/Test/Constraint/AssertDownloadableDuplicateForm.php @@ -32,11 +32,7 @@ protected function sortDownloadableArray(array $fields) usort( $fields, function ($row1, $row2) { - if ($row1['sort_order'] == $row2['sort_order']) { - return 0; - } - - return ($row1['sort_order'] < $row2['sort_order']) ? -1 : 1; + return $row1['sort_order'] <=> $row2['sort_order']; } ); diff --git a/dev/tests/functional/tests/app/Magento/Downloadable/Test/Handler/DownloadableProduct/Webapi.php b/dev/tests/functional/tests/app/Magento/Downloadable/Test/Handler/DownloadableProduct/Webapi.php index 434c78e55c69..d14d6754b12e 100644 --- a/dev/tests/functional/tests/app/Magento/Downloadable/Test/Handler/DownloadableProduct/Webapi.php +++ b/dev/tests/functional/tests/app/Magento/Downloadable/Test/Handler/DownloadableProduct/Webapi.php @@ -148,7 +148,7 @@ protected function prepareLinkData(array $link) 'title' => $link['title'], 'sort_order' => isset($link['sort_order']) ? $link['sort_order'] : 0, 'is_shareable' => $link['is_shareable'], - 'price' => floatval($link['price']), + 'price' => (float)$link['price'], 'number_of_downloads' => isset($link['number_of_downloads']) ? $link['number_of_downloads'] : 0, 'link_type' => $link['type'], 'link_url' => isset($link['link_url']) ? $link['link_url'] : null, diff --git a/dev/tests/functional/tests/app/Magento/Downloadable/Test/TestCase/DeleteProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/Downloadable/Test/TestCase/DeleteProductEntityTest.xml index e2f86d82363c..ffcafbe68723 100644 --- a/dev/tests/functional/tests/app/Magento/Downloadable/Test/TestCase/DeleteProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Downloadable/Test/TestCase/DeleteProductEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Product\DeleteProductEntityTest"> <variation name="DeleteProductEntityTestVariation7" firstConstraint="Magento\Catalog\Test\Constraint\AssertProductSuccessDeleteMessage" method="test"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="products" xsi:type="string">downloadableProduct::default</data> <data name="isRequired" xsi:type="string">Yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSuccessDeleteMessage" /> diff --git a/dev/tests/functional/tests/app/Magento/Downloadable/Test/TestCase/ValidateOrderOfProductTypeTest.xml b/dev/tests/functional/tests/app/Magento/Downloadable/Test/TestCase/ValidateOrderOfProductTypeTest.xml index ccb74e1f79dc..e807ec913427 100644 --- a/dev/tests/functional/tests/app/Magento/Downloadable/Test/TestCase/ValidateOrderOfProductTypeTest.xml +++ b/dev/tests/functional/tests/app/Magento/Downloadable/Test/TestCase/ValidateOrderOfProductTypeTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Product\ValidateOrderOfProductTypeTest"> <variation name="ValidateOrderOfProductTypeTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menu/5" xsi:type="string">Downloadable Product</data> </variation> </testCase> diff --git a/dev/tests/functional/tests/app/Magento/Email/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Email/Test/TestCase/NavigateMenuTest.xml index f6dcdd5b65d0..4e98019c2531 100644 --- a/dev/tests/functional/tests/app/Magento/Email/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Email/Test/TestCase/NavigateMenuTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest29"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Marketing > Email Templates</data> <data name="pageTitle" xsi:type="string">Email Templates</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/GroupedImportExport/Test/TestCase/ExportProductsTest.xml b/dev/tests/functional/tests/app/Magento/GroupedImportExport/Test/TestCase/ExportProductsTest.xml index cffcdbf45a6d..a110dc6a89f8 100644 --- a/dev/tests/functional/tests/app/Magento/GroupedImportExport/Test/TestCase/ExportProductsTest.xml +++ b/dev/tests/functional/tests/app/Magento/GroupedImportExport/Test/TestCase/ExportProductsTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\CatalogImportExport\Test\TestCase\ExportProductsTest" summary="Export products"> <variation name="ExportProductsTestVariation6" summary="Export grouped product with special price" ticketId="MAGETWO-46116"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="exportData" xsi:type="string">default</data> <data name="products/0" xsi:type="array"> <item name="fixture" xsi:type="string">groupedProduct</item> diff --git a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Block/Adminhtml/Product/Grouped/AssociatedProducts/Search/Grid.php b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Block/Adminhtml/Product/Grouped/AssociatedProducts/Search/Grid.php index 45481d6ee075..6c19291222a9 100644 --- a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Block/Adminhtml/Product/Grouped/AssociatedProducts/Search/Grid.php +++ b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Block/Adminhtml/Product/Grouped/AssociatedProducts/Search/Grid.php @@ -20,6 +20,13 @@ class Grid extends DataGrid */ protected $addProducts = '.action-primary[data-role="action"]'; + /** + * Grid selector. + * + * @var string + */ + private $gridSelector = '[data-role="grid-wrapper"]'; + /** * Filters array mapping * @@ -40,4 +47,59 @@ public function addProducts() { $this->_rootElement->find($this->addProducts)->click(); } + + /** + * @inheritdoc + */ + public function searchAndSelect(array $filter) + { + $this->waitGridVisible(); + $this->waitLoader(); + parent::searchAndSelect($filter); + } + + /** + * @inheritdoc + */ + protected function waitLoader() + { + parent::waitLoader(); + $this->waitGridLoaderInvisible(); + } + + /** + * Wait for grid to appear. + * + * @return void + */ + private function waitGridVisible() + { + $browser = $this->_rootElement; + $selector = $this->gridSelector; + + return $browser->waitUntil( + function () use ($browser, $selector) { + $element = $browser->find($selector); + return $element->isVisible() ? true : null; + } + ); + } + + /** + * Wait for grid spinner disappear. + * + * @return void + */ + private function waitGridLoaderInvisible() + { + $browser = $this->_rootElement; + $selector = $this->loader; + + return $browser->waitUntil( + function () use ($browser, $selector) { + $element = $browser->find($selector); + return $element->isVisible() === false ? true : null; + } + ); + } } diff --git a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Block/Catalog/Product/View.php b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Block/Catalog/Product/View.php index 5627a9d887bc..c47df8c5463e 100644 --- a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Block/Catalog/Product/View.php +++ b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Block/Catalog/Product/View.php @@ -27,14 +27,15 @@ class View extends ParentView * * @var string */ - protected $formatTierPrice = "//tbody[%row-number%]//ul[contains(@class,'tier')]//*[@class='item'][%line-number%]"; + protected $formatTierPrice = + "//tr[@class='row-tier-price'][%row-number%]//ul[contains(@class,'tier')]//*[@class='item'][%line-number%]"; /** * This member holds the class name of the special price block. * * @var string */ - protected $formatSpecialPrice = '//tbody[%row-number%]//*[contains(@class,"price-box")]'; + protected $formatSpecialPrice = '//tbody//tr[%row-number%]//*[contains(@class,"price-box")]'; /** * Get grouped product block diff --git a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Fixture/GroupedProduct.xml b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Fixture/GroupedProduct.xml index dce1358a1ecf..59c00683e3b1 100644 --- a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Fixture/GroupedProduct.xml +++ b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/Fixture/GroupedProduct.xml @@ -58,7 +58,7 @@ <field name="sku" is_required="1" group="product-details" /> <field name="small_image" is_required="0" /> <field name="small_image_label" is_required="0" /> - <field name="status" is_required="0" /> + <field name="status" is_required="0" group="product-details" /> <field name="thumbnail" is_required="0" /> <field name="thumbnail_label" is_required="0" /> <field name="updated_at" is_required="1" /> @@ -66,7 +66,7 @@ <field name="upsell_tgtr_position_limit" is_required="0" /> <field name="url_key" is_required="0" group="search-engine-optimization" /> <field name="url_path" is_required="0" /> - <field name="visibility" is_required="0" /> + <field name="visibility" is_required="0" group="product-details" /> <field name="id" /> <field name="type_id" /> <field name="attribute_set_id" group="product-details" source="Magento\Catalog\Test\Fixture\Product\AttributeSetId" /> diff --git a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/TestCase/CreateGroupedProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/TestCase/CreateGroupedProductEntityTest.xml index 38ef02ff4944..f397c1b99e3b 100644 --- a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/TestCase/CreateGroupedProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/TestCase/CreateGroupedProductEntityTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\GroupedProduct\Test\TestCase\CreateGroupedProductEntityTest" summary="Create Grouped Product" ticketId="MAGETWO-24877"> <variation name="CreateGroupedProductEntityTestVariation1" summary="Create Grouped Product and Assign It to the Category" ticketId="MAGETWO-13610"> - <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test, stable:no</data> + <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test</data> <data name="product/data/url_key" xsi:type="string">test-grouped-product-%isolation%</data> <data name="product/data/name" xsi:type="string">GroupedProduct %isolation%</data> <data name="product/data/sku" xsi:type="string">GroupedProduct_sku%isolation%</data> @@ -57,7 +57,6 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductOutOfStock" /> </variation> <variation name="CreateGroupedProductEntityTestVariation5" summary="Create with no required products"> - <data name="tag" xsi:type="string">stable:no</data> <data name="product/data/url_key" xsi:type="string">test-grouped-product-%isolation%</data> <data name="product/data/name" xsi:type="string">GroupedProduct %isolation%</data> <data name="product/data/sku" xsi:type="string">GroupedProduct_sku%isolation%</data> @@ -110,7 +109,6 @@ <constraint name="Magento\Catalog\Test\Constraint\AssertProductPage" /> </variation> <variation name="CreateGroupedProductEntityTestVariation10" summary="Create Grouped Product and Assign it on Custom Website"> - <data name="tag" xsi:type="string">stable:no</data> <data name="product/data/url_key" xsi:type="string">test-grouped-product-%isolation%</data> <data name="product/data/name" xsi:type="string">GroupedProduct %isolation%</data> <data name="product/data/sku" xsi:type="string">GroupedProduct_sku%isolation%</data> diff --git a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/TestCase/DeleteProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/TestCase/DeleteProductEntityTest.xml index cf0c8c814167..e62e5ad73958 100644 --- a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/TestCase/DeleteProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/TestCase/DeleteProductEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Catalog\Test\TestCase\Product\DeleteProductEntityTest"> <variation name="DeleteProductEntityTestVariation8" firstConstraint="Magento\Catalog\Test\Constraint\AssertProductSuccessDeleteMessage" method="test"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="products" xsi:type="string">groupedProduct::default</data> <data name="isRequired" xsi:type="string">Yes</data> <constraint name="Magento\Catalog\Test\Constraint\AssertProductSuccessDeleteMessage" /> diff --git a/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/TestCase/ValidateOrderOfProductTypeTest.xml b/dev/tests/functional/tests/app/Magento/GroupedProduct/Test/TestCase/ValidateOrderOfProductTypeTest.xml index 5a5cfdd9adc4..ac57cdeb9205 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 000000000000..60a313a9c01b --- /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 000000000000..4a781c787eb0 --- /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 25e4d4b39174..c52f8c6613fb 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 000000000000..59b1c7570c3d --- /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/Constraint/AssertImportSuccessMessage.php b/dev/tests/functional/tests/app/Magento/ImportExport/Test/Constraint/AssertImportSuccessMessage.php index ca75e3b203f6..a5408426514f 100644 --- a/dev/tests/functional/tests/app/Magento/ImportExport/Test/Constraint/AssertImportSuccessMessage.php +++ b/dev/tests/functional/tests/app/Magento/ImportExport/Test/Constraint/AssertImportSuccessMessage.php @@ -28,7 +28,7 @@ class AssertImportSuccessMessage extends AbstractConstraint public function processAssert(AdminImportIndex $adminImportIndex) { $validationMessage = $adminImportIndex->getMessagesBlock()->getImportResultMessage(); - \PHPUnit\Framework\Assert::assertEquals( + \PHPUnit\Framework\Assert::assertStringEndsWith( self::SUCCESS_MESSAGE, $validationMessage, 'Wrong validation result is displayed.' diff --git a/dev/tests/functional/tests/app/Magento/ImportExport/Test/Fixture/Import/File.php b/dev/tests/functional/tests/app/Magento/ImportExport/Test/Fixture/Import/File.php index 17cfdc31720c..89f51931f8dc 100644 --- a/dev/tests/functional/tests/app/Magento/ImportExport/Test/Fixture/Import/File.php +++ b/dev/tests/functional/tests/app/Magento/ImportExport/Test/Fixture/Import/File.php @@ -316,7 +316,7 @@ private function prepareCsv($csvContent) $explodedArray = $compiledData['explodedArray']; } else { $explodedArray[$i] = str_replace('"', '', $explodedArray[$i]); - }; + } } $data = array_diff($explodedArray, ['%to_delete%']); $this->csv[] = $data; 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 51afed208731..e70a5fc29820 100644 --- a/dev/tests/functional/tests/app/Magento/ImportExport/Test/Page/Adminhtml/AdminExportIndex.xml +++ b/dev/tests/functional/tests/app/Magento/ImportExport/Test/Page/Adminhtml/AdminExportIndex.xml @@ -9,6 +9,9 @@ <page name="AdminExportIndex" area="Adminhtml" mca="admin/export/index" module="Magento_ImportExport"> <block name="filterExport" class="Magento\ImportExport\Test\Block\Adminhtml\Export\Filter" locator="#export_filter_container" strategy="css selector" /> <block name="exportForm" class="Magento\ImportExport\Test\Block\Adminhtml\Export\Edit\Form" locator="#container" strategy="css selector" /> + <block name="exportedGrid" class="Magento\ImportExport\Test\Block\Adminhtml\Export\ExportedGrid" locator="#container" strategy="css selector" /> <block name="messagesBlock" class="Magento\Backend\Test\Block\Messages" locator="#messages" strategy="css selector" /> + <block name="systemMessageDialog" class="Magento\AdminNotification\Test\Block\System\Messages" locator='.ui-popup-message .modal-inner-wrap' strategy="css selector" /> + <block name="notificationsArea" class="Magento\ImportExport\Test\Block\Adminhtml\Export\NotificationsArea" locator='//ul[contains(@data-mark-as-read-url,"notification")]' strategy="xpath" /> </page> </config> diff --git a/dev/tests/functional/tests/app/Magento/ImportExport/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/ImportExport/Test/TestCase/NavigateMenuTest.xml index 6ce4d01d177d..d396a364a3f4 100644 --- a/dev/tests/functional/tests/app/Magento/ImportExport/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/ImportExport/Test/TestCase/NavigateMenuTest.xml @@ -8,11 +8,13 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest35"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">System > Import</data> <data name="pageTitle" xsi:type="string">Import</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest36"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">System > Export</data> <data name="pageTitle" xsi:type="string">Export</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/Indexer/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Indexer/Test/TestCase/NavigateMenuTest.xml index 883e9bde47bf..16ae092e62ca 100644 --- a/dev/tests/functional/tests/app/Magento/Indexer/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Indexer/Test/TestCase/NavigateMenuTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest37"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">System > Index Management</data> <data name="pageTitle" xsi:type="string">Index Management</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/Install/Test/Page/DevdocsInstall.xml b/dev/tests/functional/tests/app/Magento/Install/Test/Page/DevdocsInstall.xml index f63e7282719d..15e0b8a27d24 100644 --- a/dev/tests/functional/tests/app/Magento/Install/Test/Page/DevdocsInstall.xml +++ b/dev/tests/functional/tests/app/Magento/Install/Test/Page/DevdocsInstall.xml @@ -6,7 +6,7 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/pages.xsd"> - <page name="DevdocsInstall" mca="http://devdocs.magento.com/guides/v2.0/install-gde/install/web/install-web.html" module="Magento_Install"> + <page name="DevdocsInstall" mca="https://devdocs.magento.com/guides/v2.0/install-gde/install/web/install-web.html" module="Magento_Install"> <block name="devdocsBlock" class="Magento\Install\Test\Block\Devdocs" locator="body" strategy="css selector"/> </page> </config> diff --git a/dev/tests/functional/tests/app/Magento/Install/Test/TestCase/InstallTest.xml b/dev/tests/functional/tests/app/Magento/Install/Test/TestCase/InstallTest.xml index 2b407e3d3920..86906d4c0940 100644 --- a/dev/tests/functional/tests/app/Magento/Install/Test/TestCase/InstallTest.xml +++ b/dev/tests/functional/tests/app/Magento/Install/Test/TestCase/InstallTest.xml @@ -17,7 +17,7 @@ <variation name="InstallTestVariation2" firstConstraint="Magento\Install\Test\Constraint\AssertSuccessInstall" summary="Install with custom encryption key and changed currency and locale"> <data name="user/dataset" xsi:type="string">default</data> <data name="install/keyOwn" xsi:type="string">I want to use my own encryption key</data> - <data name="install/keyValue" xsi:type="string">123123qa</data> + <data name="install/keyValue" xsi:type="string">I_want_to_use_my_own__encryption</data> <data name="install/storeLanguage" xsi:type="string">German (Germany)</data> <data name="install/storeCurrency" xsi:type="string">Euro (EUR)</data> <data name="currencySymbol" xsi:type="string">€</data> diff --git a/dev/tests/functional/tests/app/Magento/Integration/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Integration/Test/TestCase/NavigateMenuTest.xml index 22def36751f5..265790ed4b76 100644 --- a/dev/tests/functional/tests/app/Magento/Integration/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Integration/Test/TestCase/NavigateMenuTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest38"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">System > Integrations</data> <data name="pageTitle" xsi:type="string">Integrations</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/LayeredNavigation/Test/Block/Navigation.php b/dev/tests/functional/tests/app/Magento/LayeredNavigation/Test/Block/Navigation.php index 3c36fe82b130..97c43d7c4e2c 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/Multishipping/Test/TestStep/FillShippingInformationStep.php b/dev/tests/functional/tests/app/Magento/Multishipping/Test/TestStep/FillShippingInformationStep.php index 67c1826f40a2..2c41ce3d8b4e 100644 --- a/dev/tests/functional/tests/app/Magento/Multishipping/Test/TestStep/FillShippingInformationStep.php +++ b/dev/tests/functional/tests/app/Magento/Multishipping/Test/TestStep/FillShippingInformationStep.php @@ -58,7 +58,7 @@ public function __construct( public function run() { $shippingMethods = []; - for ($i = 0; $i < count($this->customer->getAddress()); $i++) { + for ($i = 0, $count = count($this->customer->getAddress()); $i < $count; $i++) { $shippingMethods[] = $this->shippingMethod; } $this->shippingInformation->getShippingBlock()->selectShippingMethod($shippingMethods); diff --git a/dev/tests/functional/tests/app/Magento/Newsletter/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Newsletter/Test/TestCase/NavigateMenuTest.xml index 13843a50868e..b1a6b3e0c438 100644 --- a/dev/tests/functional/tests/app/Magento/Newsletter/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Newsletter/Test/TestCase/NavigateMenuTest.xml @@ -8,21 +8,25 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest46"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Marketing > Newsletter Template</data> <data name="pageTitle" xsi:type="string">Newsletter Templates</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest47"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Marketing > Newsletter Queue</data> <data name="pageTitle" xsi:type="string">Newsletter Queue</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest48"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Marketing > Newsletter Subscribers</data> <data name="pageTitle" xsi:type="string">Newsletter Subscribers</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest49"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > Newsletter Problem Reports</data> <data name="pageTitle" xsi:type="string">Newsletter Problems Report</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/PageCache/Test/TestCase/FlushStaticFilesCacheButtonVisibilityTest.xml b/dev/tests/functional/tests/app/Magento/PageCache/Test/TestCase/FlushStaticFilesCacheButtonVisibilityTest.xml index cbdce5905719..bc529729f121 100644 --- a/dev/tests/functional/tests/app/Magento/PageCache/Test/TestCase/FlushStaticFilesCacheButtonVisibilityTest.xml +++ b/dev/tests/functional/tests/app/Magento/PageCache/Test/TestCase/FlushStaticFilesCacheButtonVisibilityTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\PageCache\Test\TestCase\FlushStaticFilesCacheButtonVisibilityTest" summary="Flush Static Files Cache button visibility" ticketId="MAGETWO-39934"> <variation name="FlushStaticFilesCacheButtonVisibilityTest"> - <data name="tag" xsi:type="string">severity:S3</data> + <data name="tag" xsi:type="string">severity:S3, mftf_migrated:yes</data> <constraint name="Magento\PageCache\Test\Constraint\AssertFlushStaticFilesCacheButtonVisibility" /> </variation> </testCase> diff --git a/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/NavigateMenuTest.xml index d7d031f559f8..114235e75524 100644 --- a/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/NavigateMenuTest.xml @@ -8,13 +8,13 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest" summary="Navigate through admin menu" ticketId="MAGETWO-34874"> <variation name="NavigateMenuTest50"> - <data name="tag" xsi:type="string">severity:S0</data> + <data name="tag" xsi:type="string">severity:S0, mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > PayPal Settlement</data> <data name="pageTitle" xsi:type="string">PayPal Settlement Reports</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest51"> - <data name="tag" xsi:type="string">severity:S0</data> + <data name="tag" xsi:type="string">severity:S0, mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Sales > Billing Agreements</data> <data name="pageTitle" xsi:type="string">Billing Agreements</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/Reports/Test/Constraint/AssertSalesReportIntervalResult.php b/dev/tests/functional/tests/app/Magento/Reports/Test/Constraint/AssertSalesReportIntervalResult.php index 745450aa2c02..1236a86e160b 100644 --- a/dev/tests/functional/tests/app/Magento/Reports/Test/Constraint/AssertSalesReportIntervalResult.php +++ b/dev/tests/functional/tests/app/Magento/Reports/Test/Constraint/AssertSalesReportIntervalResult.php @@ -52,7 +52,7 @@ protected function prepareSalesResult($salesResult) { $data = []; foreach ($salesResult as $key => $result) { - $data[$key] = floatval($result); + $data[$key] = (float)$result; } return $data; diff --git a/dev/tests/functional/tests/app/Magento/Reports/Test/Constraint/AssertSalesReportTotalResult.php b/dev/tests/functional/tests/app/Magento/Reports/Test/Constraint/AssertSalesReportTotalResult.php index 5f435e482290..423ca6dafbdd 100644 --- a/dev/tests/functional/tests/app/Magento/Reports/Test/Constraint/AssertSalesReportTotalResult.php +++ b/dev/tests/functional/tests/app/Magento/Reports/Test/Constraint/AssertSalesReportTotalResult.php @@ -52,7 +52,7 @@ protected function prepareSalesResult($salesResult) { $data = []; foreach ($salesResult as $key => $result) { - $data[$key] = floatval($result); + $data[$key] = (float)$result; } return $data; diff --git a/dev/tests/functional/tests/app/Magento/Reports/Test/TestCase/AbandonedCartsReportEntityTest.php b/dev/tests/functional/tests/app/Magento/Reports/Test/TestCase/AbandonedCartsReportEntityTest.php index 72a3b2b5f68d..610387d9a39e 100644 --- a/dev/tests/functional/tests/app/Magento/Reports/Test/TestCase/AbandonedCartsReportEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/Reports/Test/TestCase/AbandonedCartsReportEntityTest.php @@ -34,7 +34,7 @@ class AbandonedCartsReportEntityTest extends Injectable { /* tags */ const MVP = 'no'; - const STABLE = 'no'; + const STABLE = 'yes'; /* end tags */ /** diff --git a/dev/tests/functional/tests/app/Magento/Reports/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Reports/Test/TestCase/NavigateMenuTest.xml index 08cce1ffe7d2..4e3cd1824767 100644 --- a/dev/tests/functional/tests/app/Magento/Reports/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Reports/Test/TestCase/NavigateMenuTest.xml @@ -8,76 +8,91 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest" summary="Navigate through Reports admin menu"> <variation name="NavigateMenuTest55"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > Products in Cart</data> <data name="pageTitle" xsi:type="string">Products in Carts</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest56"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > Abandoned Carts</data> <data name="pageTitle" xsi:type="string">Abandoned Carts</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest57"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > Orders</data> <data name="pageTitle" xsi:type="string">Orders Report</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest58"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > Tax</data> <data name="pageTitle" xsi:type="string">Tax Report</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest59"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > Invoiced</data> <data name="pageTitle" xsi:type="string">Invoice Report</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest60"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > Coupons</data> <data name="pageTitle" xsi:type="string">Coupons Report</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest61"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > Order Total</data> <data name="pageTitle" xsi:type="string">Order Total Report</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest62"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > Order Count</data> <data name="pageTitle" xsi:type="string">Order Count Report</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest63"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > New</data> <data name="pageTitle" xsi:type="string">New Accounts Report</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest64"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > Views</data> <data name="pageTitle" xsi:type="string">Product Views Report</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest65"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > Bestsellers</data> <data name="pageTitle" xsi:type="string">Bestsellers Report</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest66"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > Low Stock</data> <data name="pageTitle" xsi:type="string">Low Stock Report</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest67"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > Ordered</data> <data name="pageTitle" xsi:type="string">Ordered Products Report</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest68"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > Downloads</data> <data name="pageTitle" xsi:type="string">Downloads Report</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> </variation> <variation name="NavigateMenuTest69"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > Refresh Statistics</data> <data name="pageTitle" xsi:type="string">Refresh Statistics</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> diff --git a/dev/tests/functional/tests/app/Magento/Reports/Test/TestCase/ProductsInCartReportEntityTest.php b/dev/tests/functional/tests/app/Magento/Reports/Test/TestCase/ProductsInCartReportEntityTest.php index 30e790f978c4..1a4cb787bf1c 100644 --- a/dev/tests/functional/tests/app/Magento/Reports/Test/TestCase/ProductsInCartReportEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/Reports/Test/TestCase/ProductsInCartReportEntityTest.php @@ -102,10 +102,12 @@ public function test( $productUrl = $_ENV['app_frontend_url'] . $product->getUrlKey() . '.html'; $browser->open($productUrl); $this->catalogProductView->getViewBlock()->addToCart($product); + $this->catalogProductView->getMessagesBlock()->waitSuccessMessage(); if ($isGuest) { $this->objectManager->create(\Magento\Customer\Test\TestStep\LogoutCustomerOnFrontendStep::class)->run(); $browser->open($productUrl); $this->catalogProductView->getViewBlock()->addToCart($product); + $this->catalogProductView->getMessagesBlock()->waitSuccessMessage(); } } diff --git a/dev/tests/functional/tests/app/Magento/Review/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Review/Test/TestCase/NavigateMenuTest.xml index 8445a21604cd..334497cc2f77 100644 --- a/dev/tests/functional/tests/app/Magento/Review/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Review/Test/TestCase/NavigateMenuTest.xml @@ -8,21 +8,25 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest70"> - <data name="menuItem" xsi:type="string">Marketing > Reviews</data> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> + <data name="menuItem" xsi:type="string">Marketing > All Reviews</data> <data name="pageTitle" xsi:type="string">Reviews</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest71"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > By Customers</data> <data name="pageTitle" xsi:type="string">Customer Reviews Report</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest72"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Reports > By Products</data> <data name="pageTitle" xsi:type="string">Product Reviews Report</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest73"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Stores > Rating</data> <data name="pageTitle" xsi:type="string">Ratings</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Create.php b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Create.php index 5b9ae99a2e86..f460cd91167d 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Create.php +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Create.php @@ -323,7 +323,6 @@ public function fillShippingAddress(FixtureInterface $shippingAddress) */ public function selectShippingMethod(array $shippingMethod) { - $this->_rootElement->find($this->orderMethodsSelector)->click(); $this->getShippingMethodBlock()->selectShippingMethod($shippingMethod); $this->getTemplateBlock()->waitLoader(); } diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Create/Billing/Method.php b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Create/Billing/Method.php index dee1336c14e4..e5c414c4807d 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Create/Billing/Method.php +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Create/Billing/Method.php @@ -22,6 +22,13 @@ class Method extends Block */ private $paymentMethod = '#p_method_%s'; + /** + * Get available payment methods link. + * + * @var string + */ + private $billingMethodsLink = '#order-billing_method_summary a'; + /** * Purchase order number selector. * @@ -59,6 +66,13 @@ class Method extends Block */ public function selectPaymentMethod(array $payment, CreditCard $creditCard = null) { + $this->waitForElementNotVisible($this->loader); + $billingMethodsLink = $this->_rootElement->find($this->billingMethodsLink); + if ($billingMethodsLink->isPresent()) { + $billingMethodsLink->click(); + $this->waitForElementNotVisible($this->loader); + } + $paymentMethod = $payment['method']; $paymentInput = $this->_rootElement->find(sprintf($this->paymentMethod, $paymentMethod)); if ($paymentInput->isVisible()) { diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Create/Items.php b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Create/Items.php index daa58a3447c0..61c48d62630f 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Create/Items.php +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Create/Items.php @@ -73,6 +73,7 @@ function () use ($element, $selector) { return $addProductsButton->isVisible() ? true : null; } ); + $this->getTemplateBlock()->waitLoader(); $this->_rootElement->find($this->addProducts, Locator::SELECTOR_XPATH)->click(); $this->getTemplateBlock()->waitLoader(); } diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Create/Shipping/Method.php b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Create/Shipping/Method.php index 2ee6269c39d4..b5acaf01f548 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Create/Shipping/Method.php +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/Block/Adminhtml/Order/Create/Shipping/Method.php @@ -44,13 +44,12 @@ class Method extends Block public function selectShippingMethod(array $shippingMethod) { $this->waitFormLoading(); - $this->_rootElement->waitUntil( - function () { - return $this->_rootElement->find($this->shippingMethodsLink)->isVisible() ? true : null; - } - ); + $shippingMethodsLink = $this->_rootElement->find($this->shippingMethodsLink); + if ($shippingMethodsLink->isPresent()) { + $shippingMethodsLink->click(); + $this->waitFormLoading(); + } - $this->_rootElement->find($this->shippingMethodsLink)->click(); $selector = sprintf( $this->shippingMethod, $shippingMethod['shipping_service'], @@ -67,7 +66,6 @@ function () { */ private function waitFormLoading() { - $this->_rootElement->click(); $this->browser->waitUntil( function () { return $this->browser->find($this->waitElement)->isVisible() ? null : true; diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/Constraint/AssertOrderCancelMassActionFailMessage.php b/dev/tests/functional/tests/app/Magento/Sales/Test/Constraint/AssertOrderCancelMassActionFailMessage.php index fffb0746d496..2c87d8539f0d 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/Constraint/AssertOrderCancelMassActionFailMessage.php +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/Constraint/AssertOrderCancelMassActionFailMessage.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Sales\Test\Constraint; diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/Constraint/AssertOrderStatusIsCorrect.php b/dev/tests/functional/tests/app/Magento/Sales/Test/Constraint/AssertOrderStatusIsCorrect.php index c3e8558df7fc..fc854bd8c50a 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/Constraint/AssertOrderStatusIsCorrect.php +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/Constraint/AssertOrderStatusIsCorrect.php @@ -39,8 +39,8 @@ public function processAssert( /** @var \Magento\Sales\Test\Block\Adminhtml\Order\View\Tab\Info $infoTab */ $infoTab = $salesOrderView->getOrderForm()->openTab('info')->getTab('info'); \PHPUnit\Framework\Assert::assertEquals( - $infoTab->getOrderStatus(), - $orderStatus + $orderStatus, + $infoTab->getOrderStatus() ); } diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/Constraint/AssertProductsQtyAfterOrderCancel.php b/dev/tests/functional/tests/app/Magento/Sales/Test/Constraint/AssertProductsQtyAfterOrderCancel.php index 28259c8f6d93..24027cacd9e4 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/Constraint/AssertProductsQtyAfterOrderCancel.php +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/Constraint/AssertProductsQtyAfterOrderCancel.php @@ -52,7 +52,7 @@ public function processAssert( AssertProductForm $assertProductForm, AssertConfigurableProductForm $assertConfigurableProductForm ) { - for ($i = 0; $i < count($order->getEntityId()['products']); $i++) { + for ($i = 0, $count = count($order->getEntityId()['products']); $i < $count; $i++) { $product = $order->getEntityId()['products'][$i]; $productData = $product->getData(); if ($product instanceof BundleProduct) { diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/Handler/OrderInjectable/Webapi.php b/dev/tests/functional/tests/app/Magento/Sales/Test/Handler/OrderInjectable/Webapi.php index 073fb0967144..972dc0f510b3 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/Handler/OrderInjectable/Webapi.php +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/Handler/OrderInjectable/Webapi.php @@ -200,7 +200,7 @@ protected function setBillingAddress(OrderInjectable $order) $this->webapiTransport->close(); if (!is_numeric($response)) { $this->eventManager->dispatchEvent(['webapi_failed'], [$response]); - throw new \Exception("Could not set billing addresss to quote!"); + throw new \Exception("Could not set billing address to quote!"); } } diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CancelCreatedOrderTest.xml b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CancelCreatedOrderTest.xml index 4d997d528c04..fdb396bbbd05 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CancelCreatedOrderTest.xml +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CancelCreatedOrderTest.xml @@ -18,7 +18,6 @@ <constraint name="Magento\Sales\Test\Constraint\AssertOrderInOrdersGridOnFrontend" /> </variation> <variation name="CancelCreatedOrderTestVariationWithZeroSubtotalCheckout" summary="Cancel order with zero subtotal checkout payment method and check status on storefront"> - <data name="tag" xsi:type="string">stable:no</data> <data name="order/dataset" xsi:type="string">default</data> <data name="order/data/payment_auth_expiration/method" xsi:type="string">free</data> <data name="order/data/shipping_method" xsi:type="string">freeshipping_freeshipping</data> @@ -48,6 +47,7 @@ <variation name="CancelCreatedOrderTestVariationWithPurchaseOrderPaymentMethod" summary="Cancel order with purchase order payment method and check status on storefront"> <data name="order/dataset" xsi:type="string">default</data> <data name="order/data/payment_auth_expiration/method" xsi:type="string">purchaseorder</data> + <data name="order/data/payment_auth_expiration/po_number" xsi:type="string">po_number</data> <data name="status" xsi:type="string">Canceled</data> <data name="configData" xsi:type="string">purchaseorder</data> <constraint name="Magento\Sales\Test\Constraint\AssertOrderCancelSuccessMessage" /> diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateCreditMemoEntityTest.xml b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateCreditMemoEntityTest.xml index 9a781be52f08..309f035a1c24 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateCreditMemoEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateCreditMemoEntityTest.xml @@ -104,6 +104,7 @@ </data> <data name="order/dataset" xsi:type="string">default</data> <data name="order/data/payment_auth_expiration/method" xsi:type="string">purchaseorder</data> + <data name="order/data/payment_auth_expiration/po_number" xsi:type="string">po_number</data> <data name="order/data/price/dataset" xsi:type="string">full_refund_with_zero_shipping_refund</data> <data name="configData" xsi:type="string">purchaseorder</data> <constraint name="Magento\Sales\Test\Constraint\AssertRefundSuccessCreateMessage" /> diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateCustomOrderStatusEntityTest.xml b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateCustomOrderStatusEntityTest.xml index 38ce04fa56d8..e05d0fea6b12 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateCustomOrderStatusEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateCustomOrderStatusEntityTest.xml @@ -8,17 +8,20 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Sales\Test\TestCase\CreateCustomOrderStatusEntityTest" summary="Create Custom Order Status Entity" ticketId="MAGETWO-23412"> <variation name="CreateCustomOrderStatusEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="orderStatus/data/status" xsi:type="string">order_status%isolation%</data> <data name="orderStatus/data/label" xsi:type="string">orderLabel%isolation%</data> <constraint name="Magento\Sales\Test\Constraint\AssertOrderStatusSuccessCreateMessage" /> <constraint name="Magento\Sales\Test\Constraint\AssertOrderStatusInGrid" /> </variation> <variation name="CreateCustomOrderStatusEntityTestVariation2"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="orderStatus/data/status" xsi:type="string">pending</data> <data name="orderStatus/data/label" xsi:type="string">orderLabel%isolation%</data> <constraint name="Magento\Sales\Test\Constraint\AssertOrderStatusDuplicateStatus" /> </variation> <variation name="CreateCustomOrderStatusEntityTestVariation3"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="orderStatus/data/status" xsi:type="string">order_status%isolation%</data> <data name="orderStatus/data/label" xsi:type="string">Suspected Fraud</data> <constraint name="Magento\Sales\Test\Constraint\AssertOrderStatusSuccessCreateMessage" /> diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateInvoiceEntityTest.xml b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateInvoiceEntityTest.xml index 5896c2b8eb61..3d967fdea299 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateInvoiceEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateInvoiceEntityTest.xml @@ -88,6 +88,7 @@ <data name="order/data/entity_id/products" xsi:type="string">catalogProductSimple::product_100_dollar</data> <data name="order/data/total_qty_ordered/0" xsi:type="string">-</data> <data name="order/data/payment_auth_expiration/method" xsi:type="string">purchaseorder</data> + <data name="order/data/payment_auth_expiration/po_number" xsi:type="string">po_number</data> <data name="order/data/invoice" xsi:type="array"> <item name="0" xsi:type="array"> <item name="items_data" xsi:type="array"> diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateOrderBackendPartOneTest.xml b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateOrderBackendPartOneTest.xml index 0a5958b506a4..880c66483d5f 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateOrderBackendPartOneTest.xml +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/CreateOrderBackendPartOneTest.xml @@ -69,7 +69,6 @@ <constraint name="Magento\Sales\Test\Constraint\AssertOrderByDateInOrdersGrid" /> </variation> <variation name="CreateOrderBackendTestVariation4"> - <data name="tag" xsi:type="string">to_maintain:yes</data> <data name="description" xsi:type="string">Create order with virtual product for registered UK customer using Bank Transfer payment method</data> <data name="products/0" xsi:type="string">catalogProductVirtual::default</data> <data name="customer/dataset" xsi:type="string">default</data> diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/GridSortingTest.xml b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/GridSortingTest.xml index 6ae9d19a898b..28894ed6cc15 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/GridSortingTest.xml +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/GridSortingTest.xml @@ -9,7 +9,6 @@ <testCase name="Magento\Ui\Test\TestCase\GridSortingTest" summary="Grid UI Component Sorting" ticketId="MAGETWO-41328"> <variation name="SalesOrderGridSorting"> <data name="tag" xsi:type="string">severity:S2</data> - <data name="tag" xsi:type="string">to_maintain:yes</data> <data name="description" xsi:type="string">Verify sales order grid storting</data> <data name="steps" xsi:type="array"> <item name="0" xsi:type="string">-</item> diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MassOrdersUpdateTest.xml b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MassOrdersUpdateTest.xml index d4b9856c5e31..1f75b07c8ca1 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MassOrdersUpdateTest.xml +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MassOrdersUpdateTest.xml @@ -8,7 +8,6 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Sales\Test\TestCase\MassOrdersUpdateTest" summary="Mass Update Orders" ticketId="MAGETWO-27897"> <variation name="MassOrdersUpdateTestVariation1"> - <data name="tag" xsi:type="string">stable:no</data> <data name="description" xsi:type="string">cancel orders in status Pending and Processing</data> <data name="steps" xsi:type="string">-</data> <data name="action" xsi:type="string">Cancel</data> @@ -27,7 +26,7 @@ <constraint name="Magento\Sales\Test\Constraint\AssertOrdersInOrdersGrid" /> </variation> <variation name="MassOrdersUpdateTestVariation3"> - <data name="description" xsi:type="string">try to cancel orders in status Pending, Closed</data> + <data name="description" xsi:type="string">try to cancel orders in status Processing, Closed</data> <data name="steps" xsi:type="string">invoice|invoice, credit memo</data> <data name="action" xsi:type="string">Cancel</data> <data name="ordersCount" xsi:type="string">2</data> @@ -45,7 +44,6 @@ <constraint name="Magento\Sales\Test\Constraint\AssertOrdersInOrdersGrid" /> </variation> <variation name="MassOrdersUpdateTestVariation5"> - <data name="tag" xsi:type="string">stable:no</data> <data name="description" xsi:type="string">Try to put order in status Complete on Hold</data> <data name="steps" xsi:type="string">invoice, shipment</data> <data name="action" xsi:type="string">Hold</data> @@ -55,7 +53,7 @@ <constraint name="Magento\Sales\Test\Constraint\AssertOrdersInOrdersGrid" /> </variation> <variation name="MassOrdersUpdateTestVariation6"> - <data name="description" xsi:type="string">Release order in statuse On Hold</data> + <data name="description" xsi:type="string">Release order in status On Hold</data> <data name="steps" xsi:type="string">on hold</data> <data name="action" xsi:type="string">Unhold</data> <data name="ordersCount" xsi:type="string">1</data> diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MoveLastOrderedProductsOnOrderPageTest.xml b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MoveLastOrderedProductsOnOrderPageTest.xml index 6f568df8f21c..8bb4ef56361f 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MoveLastOrderedProductsOnOrderPageTest.xml +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MoveLastOrderedProductsOnOrderPageTest.xml @@ -8,13 +8,11 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Sales\Test\TestCase\MoveLastOrderedProductsOnOrderPageTest" summary="Add Products to Order from Last Ordered Products Section" ticketId="MAGETWO-27640"> <variation name="MoveLastOrderedProductsOnOrderPageTestVariation1"> - <data name="tag" xsi:type="string">stable:no</data> <data name="order/dataset" xsi:type="string">default</data> <data name="order/data/entity_id/products" xsi:type="string">catalogProductSimple::default</data> <constraint name="Magento\Sales\Test\Constraint\AssertProductInItemsOrderedGrid" /> </variation> <variation name="MoveLastOrderedProductsOnOrderPageTestVariation2"> - <data name="issue" xsi:type="string">MAGETWO-58762: Customer grid does not open in MoveLastOrderedProductsOnOrderPageTestVariation2 on Jenkins</data> <data name="order/dataset" xsi:type="string">default</data> <data name="order/data/entity_id/products" xsi:type="string">configurableProduct::configurable_with_qty_1</data> <constraint name="Magento\Sales\Test\Constraint\AssertProductInItemsOrderedGrid" /> diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MoveShoppingCartProductsOnOrderPageTest.php b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MoveShoppingCartProductsOnOrderPageTest.php index 2b0bd7178cda..91b4f711700b 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MoveShoppingCartProductsOnOrderPageTest.php +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/MoveShoppingCartProductsOnOrderPageTest.php @@ -144,6 +144,7 @@ public function test(Customer $customer, $product) )->run(); $this->browser->open($_ENV['app_frontend_url'] . $product->getUrlKey() . '.html'); $this->catalogProductView->getViewBlock()->addToCart($product); + $this->catalogProductView->getMessagesBlock()->waitSuccessMessage(); //Steps $this->customerIndex->open(); diff --git a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/NavigateMenuTest.xml index 316ba33f19fd..5cc673f4b4fa 100644 --- a/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Sales/Test/TestCase/NavigateMenuTest.xml @@ -8,31 +8,37 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest77"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Sales > Orders</data> <data name="pageTitle" xsi:type="string">Orders</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest78"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Sales > Invoices</data> <data name="pageTitle" xsi:type="string">Invoices</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest79"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Sales > Shipments</data> <data name="pageTitle" xsi:type="string">Shipments</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest80"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Sales > Credit Memos</data> <data name="pageTitle" xsi:type="string">Credit Memos</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest81"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Sales > Transactions</data> <data name="pageTitle" xsi:type="string">Transactions</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest82"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Stores > Order Status</data> <data name="pageTitle" xsi:type="string">Order Status</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/PromoQuoteForm.php b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/PromoQuoteForm.php index 2627f99d4c8c..54cec6cf279f 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/PromoQuoteForm.php +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Block/Adminhtml/Promo/Quote/Edit/PromoQuoteForm.php @@ -28,6 +28,13 @@ class PromoQuoteForm extends FormSections */ protected $waitForSelectorVisible = false; + /** + * Selector of name element on the form. + * + * @var string + */ + private $nameElementSelector = 'input[name=name]'; + /** * Fill form with sections. * @@ -38,6 +45,8 @@ class PromoQuoteForm extends FormSections */ public function fill(FixtureInterface $fixture, SimpleElement $element = null, array $replace = null) { + $this->waitForElementNotVisible($this->waitForSelector); + $this->waitForElementVisible($this->nameElementSelector); $sections = $this->getFixtureFieldsByContainers($fixture); if ($replace) { $sections = $this->prepareData($sections, $replace); diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Repository/SalesRule.xml b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Repository/SalesRule.xml index 521d7d68ac4a..5cb5b4db7276 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Repository/SalesRule.xml +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Repository/SalesRule.xml @@ -271,7 +271,7 @@ <field name="coupon_type" xsi:type="string">No Coupon</field> <field name="sort_order" xsi:type="string">1</field> <field name="is_rss" xsi:type="string">Yes</field> - <field name="conditions_serialized" xsi:type="string">[Total Items Quantity|equals or greater than|3]{Product attribute combination|FOUND|ALL|:[[Category|is|2]]}</field> + <field name="conditions_serialized" xsi:type="string">[Total Items Quantity|equals or greater than|3]</field> <field name="simple_action" xsi:type="string">Percent of product price discount</field> <field name="discount_amount" xsi:type="string">25</field> <field name="apply_to_shipping" xsi:type="string">No</field> diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/ApplySeveralSalesRuleEntityTest.xml b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/ApplySeveralSalesRuleEntityTest.xml index e160fef60954..3dfe4cf11855 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/ApplySeveralSalesRuleEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/ApplySeveralSalesRuleEntityTest.xml @@ -31,7 +31,6 @@ <constraint name="Magento\SalesRule\Test\Constraint\AssertCartPriceRuleConditionIsApplied" /> </variation> <variation name="ApplySeveralSalesRuleEntityTestVariation3" summary="Rules with different priority, both are applied"> - <data name="tag" xsi:type="string">stable:no</data> <data name="salesRules/rule1" xsi:type="string">active_sales_rule_product_attribute</data> <data name="salesRules/rule2" xsi:type="string">active_sales_total_items</data> <data name="cartPrice/sub_total" xsi:type="string">250.00</data> @@ -44,7 +43,6 @@ <constraint name="Magento\SalesRule\Test\Constraint\AssertCartPriceRuleConditionIsApplied" /> </variation> <variation name="ApplySeveralSalesRuleEntityTestVariation4" summary="Rules with different priority, none are applied"> - <data name="tag" xsi:type="string">to_maintain:yes</data> <data name="salesRules/rule1" xsi:type="string">active_sales_rule_row_total</data> <data name="salesRules/rule2" xsi:type="string">active_sales_total_items</data> <data name="productForSalesRule1/dataset" xsi:type="string">simple_for_salesrule_1</data> diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml index 586ad2acee20..4995c1feb048 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml @@ -75,7 +75,6 @@ <data name="salesRule/data/coupon_code" xsi:type="string">Lorem ipsum dolor sit amet, consectetur adipiscing elit - %isolation%</data> <data name="salesRule/data/simple_action" xsi:type="string">Fixed amount discount for whole cart</data> <data name="salesRule/data/discount_amount" xsi:type="string">60</data> - <data name="salesRule/data/apply_to_shipping" xsi:type="string">No</data> <data name="salesRule/data/simple_free_shipping" xsi:type="string">No</data> <data name="salesRule/data/store_labels/0" xsi:type="string">Coupon code+Fixed amount discount for whole cart</data> <data name="productForSalesRule1/dataset" xsi:type="string">simple_for_salesrule_1</data> diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/NavigateMenuTest.xml index 0a100eabcff4..1eeaeaaa483c 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/NavigateMenuTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest83"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Marketing > Cart Price Rules</data> <data name="pageTitle" xsi:type="string">Cart Price Rules</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/Search/Test/TestCase/AdvancedSearchWithAttributeTest.xml b/dev/tests/functional/tests/app/Magento/Search/Test/TestCase/AdvancedSearchWithAttributeTest.xml index 13c7051d0c1b..733b110ec549 100644 --- a/dev/tests/functional/tests/app/Magento/Search/Test/TestCase/AdvancedSearchWithAttributeTest.xml +++ b/dev/tests/functional/tests/app/Magento/Search/Test/TestCase/AdvancedSearchWithAttributeTest.xml @@ -8,8 +8,6 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Search\Test\TestCase\AdvancedSearchWithAttributeTest" summary="Use Advanced Search by Decimal indexable attribute if Edit/Add Attribute" ticketId="MAGETWO-25931"> <variation name="AdvancedSearchWithWeightAttributeTestVariation1"> - <data name="issue" xsi:type="string">MAGETWO-65408: [FT] Magento\Search\Test\TestCase\AdvancedSearchWithAttributeTest fails on Jenkins</data> - <data name="tag" xsi:type="string">to_maintain:yes</data> <data name="productDropDownList/0" xsi:type="string">configurable</data> <data name="productDropDownList/1" xsi:type="string">simple</data> <data name="productDropDownList/2" xsi:type="string">bundle</data> diff --git a/dev/tests/functional/tests/app/Magento/Security/Test/TestCase/LockAdminUserWhenCreatingNewUserTest.xml b/dev/tests/functional/tests/app/Magento/Security/Test/TestCase/LockAdminUserWhenCreatingNewUserTest.xml index c4cfe3f7f274..3bb370a15e97 100644 --- a/dev/tests/functional/tests/app/Magento/Security/Test/TestCase/LockAdminUserWhenCreatingNewUserTest.xml +++ b/dev/tests/functional/tests/app/Magento/Security/Test/TestCase/LockAdminUserWhenCreatingNewUserTest.xml @@ -9,7 +9,7 @@ <testCase name="Magento\Security\Test\TestCase\LockAdminUserWhenCreatingNewUserTest" summary="Lock admin user after entering incorrect password while creating new User"> <variation name="LockAdminUserWhenCreatingNewUserTestVariation1"> <data name="configData" xsi:type="string">user_lockout_failures</data> - <data name="tag" xsi:type="string">severity:S2</data> + <data name="tag" xsi:type="string">severity:S2,mftf_migrated:yes</data> <data name="customAdmin/dataset" xsi:type="string">custom_admin_with_default_role</data> <data name="user/data/username" xsi:type="string">AdminUser%isolation%</data> <data name="user/data/firstname" xsi:type="string">FirstName%isolation%</data> diff --git a/dev/tests/functional/tests/app/Magento/Security/Test/TestCase/ResetCustomerPasswordFailedTest.xml b/dev/tests/functional/tests/app/Magento/Security/Test/TestCase/ResetCustomerPasswordFailedTest.xml index 0b5b8a059cbb..b4fbe7bb9292 100644 --- a/dev/tests/functional/tests/app/Magento/Security/Test/TestCase/ResetCustomerPasswordFailedTest.xml +++ b/dev/tests/functional/tests/app/Magento/Security/Test/TestCase/ResetCustomerPasswordFailedTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Security\Test\TestCase\ResetCustomerPasswordFailedTest" summary="Reset customer password."> <variation name="ResetPasswordTestVariation"> - <data name="tag" xsi:type="string">severity:S1</data> + <data name="tag" xsi:type="string">severity:S1,mftf_migrated:yes</data> <data name="customer/dataset" xsi:type="string">customer_US</data> <data name="attempts" xsi:type="string">2</data> <data name="configData" xsi:type="string">captcha_storefront_disable</data> diff --git a/dev/tests/functional/tests/app/Magento/Security/Test/TestCase/ResetUserPasswordFailedTest.xml b/dev/tests/functional/tests/app/Magento/Security/Test/TestCase/ResetUserPasswordFailedTest.xml index 55f1b6950436..f43469358aa9 100644 --- a/dev/tests/functional/tests/app/Magento/Security/Test/TestCase/ResetUserPasswordFailedTest.xml +++ b/dev/tests/functional/tests/app/Magento/Security/Test/TestCase/ResetUserPasswordFailedTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Security\Test\TestCase\ResetUserPasswordFailedTest" summary="Prevent Locked Admin User to Log In into the Backend"> <variation name="ResetUserPasswordTestVariation1"> - <data name="tag" xsi:type="string">severity:S1</data> + <data name="tag" xsi:type="string">severity:S1,mftf_migrated:yes</data> <data name="customAdmin/dataset" xsi:type="string">custom_admin_with_default_role</data> <data name="attempts" xsi:type="string">2</data> <constraint name="Magento\Security\Test\Constraint\AssertUserPasswordResetFailed" /> diff --git a/dev/tests/functional/tests/app/Magento/Sitemap/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Sitemap/Test/TestCase/NavigateMenuTest.xml index cbef8fd52fd8..0f9514ffc7a0 100644 --- a/dev/tests/functional/tests/app/Magento/Sitemap/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Sitemap/Test/TestCase/NavigateMenuTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest85"> - <data name="tag" xsi:type="string">severity:S2</data> + <data name="tag" xsi:type="string">severity:S2, mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Marketing > Site Map</data> <data name="pageTitle" xsi:type="string">Site Map</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/Handler/StoreGroup/Curl.php b/dev/tests/functional/tests/app/Magento/Store/Test/Handler/StoreGroup/Curl.php index 10ebd2b6d5f6..0c1017410961 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/Handler/StoreGroup/Curl.php +++ b/dev/tests/functional/tests/app/Magento/Store/Test/Handler/StoreGroup/Curl.php @@ -65,7 +65,7 @@ protected function getStoreGroupIdByGroupName($storeName) throw new \Exception('Cannot find store group id'); } - return intval($matches[1]); + return (int)$matches[1]; } /** diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/Handler/Website/Curl.php b/dev/tests/functional/tests/app/Magento/Store/Test/Handler/Website/Curl.php index ab0b1f209680..0b738c5e159c 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/Handler/Website/Curl.php +++ b/dev/tests/functional/tests/app/Magento/Store/Test/Handler/Website/Curl.php @@ -119,7 +119,7 @@ protected function getWebSiteIdByWebsiteName($websiteName) throw new \Exception('Cannot find website id.'); } - return intval($matches[1]); + return (int)$matches[1]; } /** diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/AccessAdminWithStoreCodeInUrlTest.xml b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/AccessAdminWithStoreCodeInUrlTest.xml index a4abfc5daf29..4d3677076d30 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/AccessAdminWithStoreCodeInUrlTest.xml +++ b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/AccessAdminWithStoreCodeInUrlTest.xml @@ -11,6 +11,7 @@ <data name="configData" xsi:type="string">add_store_code_to_urls</data> <data name="user/dataset" xsi:type="string">default</data> <data name="storeCode" xsi:type="string">default</data> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <constraint name="Magento\User\Test\Constraint\AssertUserSuccessLogin" /> <constraint name="Magento\Store\Test\Constraint\AssertStoreCodeInUrl" /> </variation> diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/CreateStoreGroupEntityTest.xml b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/CreateStoreGroupEntityTest.xml index 8cdeb48bb1c9..dcfe22eb0d5f 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/CreateStoreGroupEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/CreateStoreGroupEntityTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Store\Test\TestCase\CreateStoreGroupEntityTest" summary="Create Store Group" ticketId="MAGETWO-27345"> <variation name="CreateStoreGroupEntityTestVariation1"> - <data name="tag" xsi:type="string">severity:S1</data> + <data name="tag" xsi:type="string">severity:S1, mftf_migrated:yes</data> <data name="storeGroup/data/website_id/dataset" xsi:type="string">main_website</data> <data name="storeGroup/data/name" xsi:type="string">store_name_%isolation%</data> <data name="storeGroup/data/code" xsi:type="string">store_code_%isolation%</data> @@ -18,7 +18,7 @@ <constraint name="Magento\Store\Test\Constraint\AssertStoreGroupOnStoreViewForm" /> </variation> <variation name="CreateStoreGroupEntityTestVariation2"> - <data name="tag" xsi:type="string">severity:S1</data> + <data name="tag" xsi:type="string">severity:S1, mftf_migrated:yes</data> <data name="storeGroup/data/website_id/dataset" xsi:type="string">custom_website</data> <data name="storeGroup/data/name" xsi:type="string">store_name_%isolation%</data> <data name="storeGroup/data/code" xsi:type="string">store_code_%isolation%</data> @@ -29,6 +29,7 @@ <constraint name="Magento\Store\Test\Constraint\AssertStoreGroupOnStoreViewForm" /> </variation> <variation name="CreateStoreGroupEntityTestVariation3" summary="Check the absence of delete button" ticketId="MAGETWO-17475"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="storeGroup/dataset" xsi:type="string">custom_new_group</data> <constraint name="Magento\Store\Test\Constraint\AssertStoreGroupSuccessSaveMessage" /> <constraint name="Magento\Store\Test\Constraint\AssertStoreGroupForm" /> diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/CreateWebsiteEntityTest.xml b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/CreateWebsiteEntityTest.xml index 5a547f69280e..e35ef853d1b6 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/CreateWebsiteEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/CreateWebsiteEntityTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Store\Test\TestCase\CreateWebsiteEntityTest" summary="Create Website" ticketId="MAGETWO-27665"> <variation name="CreateWebsiteEntityTestVariation1"> - <data name="tag" xsi:type="string">severity:S1</data> + <data name="tag" xsi:type="string">severity:S1, mftf_migrated:yes</data> <data name="website/data/name" xsi:type="string">website_%isolation%</data> <data name="website/data/code" xsi:type="string">code_%isolation%</data> <constraint name="Magento\Store\Test\Constraint\AssertWebsiteSuccessSaveMessage" /> diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteStoreEntityTest.php b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteStoreEntityTest.php index e867ebe39978..c1fe9ca45972 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteStoreEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteStoreEntityTest.php @@ -12,6 +12,7 @@ use Magento\Backup\Test\Page\Adminhtml\BackupIndex; use Magento\Store\Test\Fixture\Store; use Magento\Mtf\TestCase\Injectable; +use Magento\Config\Test\TestStep\SetupConfigurationStep; /** * Test Creation for DeleteStoreEntity @@ -100,7 +101,15 @@ public function test(Store $store, $createBackup) { // Preconditions: $store->persist(); - $this->backupIndex->open()->getBackupGrid()->massaction([], 'Delete', true, 'Select All'); + /** @var SetupConfigurationStep $enableBackupsStep */ + $enableBackupsStep = $this->objectManager->create( + SetupConfigurationStep::class, + ['configData' => 'enable_backups_functionality'] + ); + $enableBackupsStep->run(); + $this->backupIndex->open() + ->getBackupGrid() + ->massaction([], 'Delete', true, 'Select All'); // Steps: $this->storeIndex->open(); diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteStoreEntityTest.xml b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteStoreEntityTest.xml index 306a9fd2024a..cd37c555fdb1 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteStoreEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteStoreEntityTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Store\Test\TestCase\DeleteStoreEntityTest" summary="Delete Store View" ticketId="MAGETWO-27942"> <variation name="DeleteStoreEntityTestVariation1"> - <data name="tag" xsi:type="string">severity:S2</data> + <data name="tag" xsi:type="string">severity:S2, mftf_migrated:yes</data> <data name="store/dataset" xsi:type="string">custom</data> <data name="createBackup" xsi:type="string">Yes</data> <constraint name="Magento\Store\Test\Constraint\AssertStoreSuccessDeleteAndBackupMessages" /> diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteStoreGroupEntityTest.php b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteStoreGroupEntityTest.php index cd37576443cd..c332b83a22de 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteStoreGroupEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteStoreGroupEntityTest.php @@ -12,6 +12,7 @@ use Magento\Backup\Test\Page\Adminhtml\BackupIndex; use Magento\Store\Test\Fixture\StoreGroup; use Magento\Mtf\TestCase\Injectable; +use Magento\Config\Test\TestStep\SetupConfigurationStep; /** * Delete StoreGroup (Store Management) @@ -101,7 +102,15 @@ public function test(StoreGroup $storeGroup, $createBackup) { //Preconditions $storeGroup->persist(); - $this->backupIndex->open()->getBackupGrid()->massaction([], 'Delete', true, 'Select All'); + /** @var SetupConfigurationStep $enableBackupsStep */ + $enableBackupsStep = $this->objectManager->create( + SetupConfigurationStep::class, + ['configData' => 'enable_backups_functionality'] + ); + $enableBackupsStep->run(); + $this->backupIndex->open() + ->getBackupGrid() + ->massaction([], 'Delete', true, 'Select All'); //Steps $this->storeIndex->open(); 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 02fcf3b19fbd..865530862853 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, stable:no</data> + <data name="tag" xsi:type="string">severity:S3, mftf_migrated:yes</data> <data name="storeGroup/dataset" xsi:type="string">custom</data> <data name="createBackup" xsi:type="string">Yes</data> <constraint name="Magento\Store\Test\Constraint\AssertStoreGroupSuccessDeleteAndBackupMessages" /> diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteWebsiteEntityTest.php b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteWebsiteEntityTest.php index 2431cb3e065d..22259a30b153 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteWebsiteEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/DeleteWebsiteEntityTest.php @@ -12,6 +12,7 @@ use Magento\Backup\Test\Page\Adminhtml\BackupIndex; use Magento\Store\Test\Fixture\Website; use Magento\Mtf\TestCase\Injectable; +use Magento\Config\Test\TestStep\SetupConfigurationStep; /** * Delete Website (Store Management) @@ -102,7 +103,15 @@ public function test(Website $website, $createBackup) { //Preconditions $website->persist(); - $this->backupIndex->open()->getBackupGrid()->massaction([], 'Delete', true, 'Select All'); + /** @var SetupConfigurationStep $enableBackupsStep */ + $enableBackupsStep = $this->objectManager->create( + SetupConfigurationStep::class, + ['configData' => 'enable_backups_functionality'] + ); + $enableBackupsStep->run(); + $this->backupIndex->open() + ->getBackupGrid() + ->massaction([], 'Delete', true, 'Select All'); //Steps $this->storeIndex->open(); diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/MoveStoreToOtherGroupSameWebsiteTest.xml b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/MoveStoreToOtherGroupSameWebsiteTest.xml index 8da208bc3f0f..ab702dfdd4b7 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/MoveStoreToOtherGroupSameWebsiteTest.xml +++ b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/MoveStoreToOtherGroupSameWebsiteTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Store\Test\TestCase\MoveStoreToOtherGroupSameWebsiteTest" summary="Move Store View" ticketId="MAGETWO-58361"> <variation name="MoveStoreTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="storeInitialA/dataset" xsi:type="string">custom_group_custom_store</data> <data name="storeInitialB/dataset" xsi:type="string">custom_group_custom_store</data> <constraint name="Magento\Store\Test\Constraint\AssertStoreSuccessSaveMessage" /> diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/UpdateStoreEntityTest.xml b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/UpdateStoreEntityTest.xml index 4a73986673c6..605335238620 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/UpdateStoreEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/UpdateStoreEntityTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Store\Test\TestCase\UpdateStoreEntityTest" summary="Update Store View" ticketId="MAGETWO-27786"> <variation name="UpdateStoreEntityTestVariation1"> - <data name="tag" xsi:type="string">severity:S2</data> + <data name="tag" xsi:type="string">severity:S2, mftf_migrated:yes</data> <data name="storeInitial/dataset" xsi:type="string">custom</data> <data name="store/data/group_id/dataset" xsi:type="string">default</data> <data name="store/data/name" xsi:type="string">storename_updated%isolation%</data> @@ -21,7 +21,7 @@ <constraint name="Magento\Store\Test\Constraint\AssertStoreFrontend" /> </variation> <variation name="UpdateStoreEntityTestVariation2"> - <data name="tag" xsi:type="string">severity:S1</data> + <data name="tag" xsi:type="string">severity:S1, mftf_migrated:yes</data> <data name="storeInitial/dataset" xsi:type="string">default</data> <data name="store/data/name" xsi:type="string">storename_updated%isolation%</data> <constraint name="Magento\Store\Test\Constraint\AssertStoreSuccessSaveMessage" /> diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/UpdateStoreGroupEntityTest.xml b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/UpdateStoreGroupEntityTest.xml index 593d9c365a92..9ff67a9ba9fe 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/UpdateStoreGroupEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/UpdateStoreGroupEntityTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Store\Test\TestCase\UpdateStoreGroupEntityTest" summary="Update Store Group" ticketId="MAGETWO-27568"> <variation name="UpdateStoreGroupEntityTestVariation1"> - <data name="tag" xsi:type="string">severity:S2</data> + <data name="tag" xsi:type="string">severity:S2, mftf_migrated:yes</data> <data name="storeGroupOrigin/dataset" xsi:type="string">custom</data> <data name="storeGroup/data/website_id/dataset" xsi:type="string">main_website</data> <data name="storeGroup/data/name" xsi:type="string">store_name_updated_%isolation%</data> @@ -20,7 +20,7 @@ <constraint name="Magento\Store\Test\Constraint\AssertStoreGroupOnStoreViewForm" /> </variation> <variation name="UpdateStoreGroupEntityTestVariation2"> - <data name="tag" xsi:type="string">severity:S2</data> + <data name="tag" xsi:type="string">severity:S2, mftf_migrated:yes</data> <data name="storeGroupOrigin/dataset" xsi:type="string">custom</data> <data name="storeGroup/data/website_id/dataset" xsi:type="string">custom_website</data> <data name="storeGroup/data/name" xsi:type="string">store_name_updated_%isolation%</data> diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/UpdateWebsiteEntityTest.xml b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/UpdateWebsiteEntityTest.xml index ac857ad035f4..5db0e7f8baad 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/UpdateWebsiteEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Store/Test/TestCase/UpdateWebsiteEntityTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Store\Test\TestCase\UpdateWebsiteEntityTest" summary="Update Website" ticketId="MAGETWO-27690"> <variation name="UpdateWebsiteEntityTestVariation1"> - <data name="tag" xsi:type="string">severity:S2</data> + <data name="tag" xsi:type="string">severity:S2, mftf_migrated:yes</data> <data name="websiteOrigin/dataset" xsi:type="string">custom_website</data> <data name="website/data/name" xsi:type="string">website_upd%isolation%</data> <data name="website/data/code" xsi:type="string">code_upd%isolation%</data> diff --git a/dev/tests/functional/tests/app/Magento/Store/Test/TestStep/DeleteWebsitesEntityStep.php b/dev/tests/functional/tests/app/Magento/Store/Test/TestStep/DeleteWebsitesEntityStep.php index 155ed21064bc..03486f337d74 100644 --- a/dev/tests/functional/tests/app/Magento/Store/Test/TestStep/DeleteWebsitesEntityStep.php +++ b/dev/tests/functional/tests/app/Magento/Store/Test/TestStep/DeleteWebsitesEntityStep.php @@ -10,10 +10,12 @@ use Magento\Backend\Test\Page\Adminhtml\DeleteWebsite; use Magento\Backend\Test\Page\Adminhtml\StoreIndex; use Magento\Backup\Test\Page\Adminhtml\BackupIndex; +use Magento\Config\Test\TestStep\SetupConfigurationStep; use Magento\Store\Test\Fixture\Store; use Magento\Mtf\TestStep\TestStepInterface; use Magento\Mtf\Fixture\FixtureFactory; use Magento\Mtf\Fixture\FixtureInterface; +use Magento\Mtf\TestStep\TestStepFactory; /** * Test Step for DeleteWebsitesEntity. @@ -60,6 +62,11 @@ class DeleteWebsitesEntityStep implements TestStepInterface */ private $createBackup; + /** + * @var TestStepFactory + */ + private $stepFactory; + /** * Prepare pages for test. * @@ -69,6 +76,7 @@ class DeleteWebsitesEntityStep implements TestStepInterface * @param DeleteWebsite $deleteWebsite * @param FixtureFactory $fixtureFactory * @param FixtureInterface $item + * @param TestStepFactory $testStepFactory * @param string $createBackup */ public function __construct( @@ -78,6 +86,7 @@ public function __construct( DeleteWebsite $deleteWebsite, FixtureFactory $fixtureFactory, FixtureInterface $item, + TestStepFactory $testStepFactory, $createBackup = 'No' ) { $this->storeIndex = $storeIndex; @@ -87,6 +96,7 @@ public function __construct( $this->item = $item; $this->createBackup = $createBackup; $this->fixtureFactory = $fixtureFactory; + $this->stepFactory = $testStepFactory; } /** @@ -96,6 +106,12 @@ public function __construct( */ public function run() { + /** @var SetupConfigurationStep $enableBackupsStep */ + $enableBackupsStep = $this->stepFactory->create( + SetupConfigurationStep::class, + ['configData' => 'enable_backups_functionality'] + ); + $enableBackupsStep->run(); $this->backupIndex->open()->getBackupGrid()->massaction([], 'Delete', true, 'Select All'); $this->storeIndex->open(); $websiteNames = $this->item->getWebsiteIds(); diff --git a/dev/tests/functional/tests/app/Magento/Swatches/Test/Handler/SwatchProductAttribute/Curl.php b/dev/tests/functional/tests/app/Magento/Swatches/Test/Handler/SwatchProductAttribute/Curl.php index 083fa246c96e..f4adb9dec125 100644 --- a/dev/tests/functional/tests/app/Magento/Swatches/Test/Handler/SwatchProductAttribute/Curl.php +++ b/dev/tests/functional/tests/app/Magento/Swatches/Test/Handler/SwatchProductAttribute/Curl.php @@ -29,21 +29,32 @@ public function __construct(DataInterface $configuration, EventManagerInterface ]; } + /** + * @inheritdoc + */ + protected function changeStructureOfTheData(array $data): array + { + return parent::changeStructureOfTheData($data); + } + /** * Re-map options from default options structure to swatches structure, * as swatches was initially created with name convention differ from other attributes. * - * @param array $data - * @return array + * @inheritdoc */ - protected function changeStructureOfTheData(array $data) + protected function getSerializeOptions(array $data): string { - $data = parent::changeStructureOfTheData($data); - $data['optiontext'] = $data['option']; - $data['swatchtext'] = [ - 'value' => $data['option']['value'] - ]; - unset($data['option']); - return $data; + $options = []; + foreach ($data as $optionRowData) { + $optionRowData['optiontext'] = $optionRowData['option']; + $optionRowData['swatchtext'] = [ + 'value' => $optionRowData['option']['value'] + ]; + unset($optionRowData['option']); + $options[] = http_build_query($optionRowData); + } + + return json_encode($options); } } diff --git a/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/CreateTaxRateEntityTest.php b/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/CreateTaxRateEntityTest.php index aa74de5afe5f..8b4f233f3550 100644 --- a/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/CreateTaxRateEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/CreateTaxRateEntityTest.php @@ -28,6 +28,7 @@ class CreateTaxRateEntityTest extends Injectable { /* tags */ const MVP = 'yes'; + const MFTF_MIGRATED = 'yes'; /* end tags */ /** diff --git a/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/CreateTaxRateEntityTest.xml b/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/CreateTaxRateEntityTest.xml index 62e91f39fcce..b4b76fd62cc3 100644 --- a/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/CreateTaxRateEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/CreateTaxRateEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Tax\Test\TestCase\CreateTaxRateEntityTest" summary="Create Tax Rate" ticketId="MAGETWO-23286"> <variation name="CreateTaxRateEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="taxRate/data/code" xsi:type="string">TaxIdentifier%isolation%</data> <data name="taxRate/data/zip_is_range" xsi:type="string">No</data> <data name="taxRate/data/zip_from" xsi:type="string">-</data> @@ -21,6 +22,7 @@ <constraint name="Magento\Tax\Test\Constraint\AssertTaxRateInTaxRule" /> </variation> <variation name="CreateTaxRateEntityTestVariation2"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="taxRate/data/code" xsi:type="string">TaxIdentifier%isolation%</data> <data name="taxRate/data/zip_is_range" xsi:type="string">Yes</data> <data name="taxRate/data/zip_from" xsi:type="string">90001</data> @@ -35,6 +37,7 @@ <constraint name="Magento\Tax\Test\Constraint\AssertTaxRateInTaxRule" /> </variation> <variation name="CreateTaxRateEntityTestVariation3"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="taxRate/data/code" xsi:type="string">TaxIdentifier%isolation%</data> <data name="taxRate/data/zip_is_range" xsi:type="string">No</data> <data name="taxRate/data/zip_from" xsi:type="string">-</data> @@ -48,6 +51,7 @@ <constraint name="Magento\Tax\Test\Constraint\AssertTaxRateInTaxRule" /> </variation> <variation name="CreateTaxRateEntityTestVariation4"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="taxRate/data/code" xsi:type="string">TaxIdentifier%isolation%</data> <data name="taxRate/data/zip_is_range" xsi:type="string">Yes</data> <data name="taxRate/data/zip_from" xsi:type="string">1</data> @@ -60,6 +64,7 @@ <constraint name="Magento\Tax\Test\Constraint\AssertTaxRateInTaxRule" /> </variation> <variation name="CreateTaxRateEntityTestVariation5"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="taxRate/data/code" xsi:type="string">TaxIdentifier%isolation%</data> <data name="taxRate/data/zip_is_range" xsi:type="string">No</data> <data name="taxRate/data/zip_from" xsi:type="string">-</data> diff --git a/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/CreateTaxRuleEntityTest.xml b/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/CreateTaxRuleEntityTest.xml index fe4ea7a0fb07..cf4e54adac0c 100644 --- a/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/CreateTaxRuleEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/CreateTaxRuleEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Tax\Test\TestCase\CreateTaxRuleEntityTest" summary="Create Tax Rule " ticketId="MAGETWO-20913"> <variation name="CreateTaxRuleEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="taxRule/data/code" xsi:type="string">TaxIdentifier%isolation%</data> <data name="taxRule/data/tax_rate/dataset/rate_0" xsi:type="string">US-CA-Rate_1</data> <data name="taxRule/data/tax_customer_class/dataset/class_0" xsi:type="string">-</data> @@ -19,6 +20,7 @@ <constraint name="Magento\Tax\Test\Constraint\AssertTaxRuleForm" /> </variation> <variation name="CreateTaxRuleEntityTestVariation2"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="tag" xsi:type="string">stable:no</data> <data name="taxRule/data/code" xsi:type="string">TaxIdentifier%isolation%</data> <data name="taxRule/data/tax_rate/dataset/rate_0" xsi:type="string">US-CA-Rate_1</data> @@ -34,6 +36,7 @@ <constraint name="Magento\Tax\Test\Constraint\AssertTaxRuleForm" /> </variation> <variation name="CreateTaxRuleEntityTestVariation3"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="description" xsi:type="string">Creating tax rule with new tax classes and tax rate</data> <data name="taxRule/data/code" xsi:type="string">TaxIdentifier%isolation%</data> <data name="taxRule/data/tax_rate/dataset/rate_0" xsi:type="string">default</data> @@ -48,6 +51,7 @@ <constraint name="Magento\Tax\Test\Constraint\AssertTaxRuleForm" /> </variation> <variation name="CreateTaxRuleEntityTestVariation4"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="taxRule/data/code" xsi:type="string">TaxIdentifier%isolation%</data> <data name="taxRule/data/tax_rate/dataset/rate_0" xsi:type="string">withZipRange</data> <data name="taxRule/data/tax_rate/dataset/rate_1" xsi:type="string">US-CA-Rate_1</data> @@ -61,6 +65,7 @@ <constraint name="Magento\Tax\Test\Constraint\AssertTaxRuleForm" /> </variation> <variation name="CreateTaxRuleEntityTestVariation5" summary="Create Tax Rule with New and Existing Tax Rate, Customer Tax Class, Product Tax Class" ticketId="MAGETWO-12438"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="tag" xsi:type="string">test_type:acceptance_test, test_type:extended_acceptance_test</data> <data name="taxRule/data/code" xsi:type="string">TaxIdentifier%isolation%</data> <data name="taxRule/data/tax_rate/dataset/rate_0" xsi:type="string">US-CA-*-Rate 1</data> diff --git a/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/DeleteTaxRateEntityTest.xml b/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/DeleteTaxRateEntityTest.xml index 5d80acd8a003..40270d84199c 100644 --- a/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/DeleteTaxRateEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/DeleteTaxRateEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Tax\Test\TestCase\DeleteTaxRateEntityTest" summary="Delete Tax Rate" ticketId="MAGETWO-23295"> <variation name="DeleteTaxRateEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="taxRate/dataset" xsi:type="string">default</data> <constraint name="Magento\Tax\Test\Constraint\AssertTaxRateSuccessDeleteMessage" /> <constraint name="Magento\Tax\Test\Constraint\AssertTaxRateNotInGrid" /> diff --git a/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/DeleteTaxRuleEntityTest.xml b/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/DeleteTaxRuleEntityTest.xml index dc5e121db3d1..96bfec0121eb 100644 --- a/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/DeleteTaxRuleEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/DeleteTaxRuleEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Tax\Test\TestCase\DeleteTaxRuleEntityTest" summary="Delete Tax Rule" ticketId="MAGETWO-20924"> <variation name="DeleteTaxRuleEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="taxRule/dataset" xsi:type="string">tax_rule_with_custom_tax_classes</data> <data name="address/data/country_id" xsi:type="string">United States</data> <data name="address/data/region_id" xsi:type="string">California</data> diff --git a/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/NavigateMenuTest.xml index ee225f21462e..ecf202ddae3a 100644 --- a/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/NavigateMenuTest.xml @@ -8,16 +8,19 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest87"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Stores > Tax Rules</data> <data name="pageTitle" xsi:type="string">Tax Rules</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest88"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Stores > Tax Zones and Rates</data> <data name="pageTitle" xsi:type="string">Tax Zones and Rates</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest89"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">System > Import/Export Tax Rates</data> <data name="pageTitle" xsi:type="string">Import and Export Tax Rates</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/TaxWithCrossBorderTest.php b/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/TaxWithCrossBorderTest.php index 0f163933d260..40d3401a207a 100644 --- a/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/TaxWithCrossBorderTest.php +++ b/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/TaxWithCrossBorderTest.php @@ -37,7 +37,6 @@ class TaxWithCrossBorderTest extends Injectable { /* tags */ const MVP = 'yes'; - const STABLE = 'no'; /* end tags */ /** diff --git a/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/UpdateTaxRateEntityTest.php b/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/UpdateTaxRateEntityTest.php index 1e66ad61b6c8..10f41e68b659 100644 --- a/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/UpdateTaxRateEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/UpdateTaxRateEntityTest.php @@ -33,6 +33,7 @@ class UpdateTaxRateEntityTest extends Injectable { /* tags */ const MVP = 'yes'; + const MFTF_MIGRATED = 'yes'; /* end tags */ /** diff --git a/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/UpdateTaxRateEntityTest.xml b/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/UpdateTaxRateEntityTest.xml index f8d9a00958b9..1fef659a3deb 100644 --- a/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/UpdateTaxRateEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Tax/Test/TestCase/UpdateTaxRateEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Tax\Test\TestCase\UpdateTaxRateEntityTest" summary="Edit Tax Rate" ticketId="MAGETWO-23299"> <variation name="UpdateTaxRateEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialTaxRate/dataset" xsi:type="string">default</data> <data name="taxRate/data/code" xsi:type="string">TaxIdentifier%isolation%</data> <data name="taxRate/data/zip_is_range" xsi:type="string">No</data> @@ -20,6 +21,7 @@ <constraint name="Magento\Tax\Test\Constraint\AssertTaxRateForm" /> </variation> <variation name="UpdateTaxRateEntityTestVariation2"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialTaxRate/dataset" xsi:type="string">default</data> <data name="taxRate/data/code" xsi:type="string">TaxIdentifier%isolation%</data> <data name="taxRate/data/zip_is_range" xsi:type="string">Yes</data> @@ -33,6 +35,7 @@ <constraint name="Magento\Tax\Test\Constraint\AssertTaxRateForm" /> </variation> <variation name="UpdateTaxRateEntityTestVariation3"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialTaxRate/dataset" xsi:type="string">default</data> <data name="taxRate/data/code" xsi:type="string">TaxIdentifier%isolation%</data> <data name="taxRate/data/zip_is_range" xsi:type="string">No</data> @@ -44,6 +47,7 @@ <constraint name="Magento\Tax\Test\Constraint\AssertTaxRateForm" /> </variation> <variation name="UpdateTaxRateEntityTestVariation4"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialTaxRate/dataset" xsi:type="string">withZipRange</data> <data name="taxRate/data/code" xsi:type="string">TaxIdentifier%isolation%</data> <data name="taxRate/data/zip_is_range" xsi:type="string">No</data> @@ -56,6 +60,7 @@ <constraint name="Magento\Tax\Test\Constraint\AssertTaxRateForm" /> </variation> <variation name="UpdateTaxRateEntityTestVariation5"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialTaxRate/dataset" xsi:type="string">withZipRange</data> <data name="taxRate/data/code" xsi:type="string">TaxIdentifier%isolation%</data> <data name="taxRate/data/zip_is_range" xsi:type="string">Yes</data> @@ -67,6 +72,7 @@ <constraint name="Magento\Tax\Test\Constraint\AssertTaxRateForm" /> </variation> <variation name="UpdateTaxRateEntityTestVariation6"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialTaxRate/dataset" xsi:type="string">withZipRange</data> <data name="taxRate/data/code" xsi:type="string">TaxIdentifier%isolation%</data> <data name="taxRate/data/zip_is_range" xsi:type="string">No</data> diff --git a/dev/tests/functional/tests/app/Magento/Theme/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Theme/Test/TestCase/NavigateMenuTest.xml index 71964fc92649..ee3865912230 100644 --- a/dev/tests/functional/tests/app/Magento/Theme/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Theme/Test/TestCase/NavigateMenuTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest90"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Content > Themes</data> <data name="pageTitle" xsi:type="string">Themes</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/Ui/Test/Block/Adminhtml/DataGrid.php b/dev/tests/functional/tests/app/Magento/Ui/Test/Block/Adminhtml/DataGrid.php index 235b0d096533..56ca47331fa1 100644 --- a/dev/tests/functional/tests/app/Magento/Ui/Test/Block/Adminhtml/DataGrid.php +++ b/dev/tests/functional/tests/app/Magento/Ui/Test/Block/Adminhtml/DataGrid.php @@ -162,6 +162,11 @@ class DataGrid extends Grid */ protected $currentPage = ".//*[@data-ui-id='current-page-input'][not(ancestor::*[@class='sticky-header'])]"; + /** + * Top page element to implement a scrolling in case of grid element not visible. + */ + private $topElementToScroll = 'header.page-header'; + /** * Clear all applied Filters. * @@ -181,7 +186,7 @@ public function resetFilter() * * @return void */ - protected function waitFilterToLoad() + public function waitFilterToLoad() { $this->getTemplateBlock()->waitLoader(); $browser = $this->_rootElement; @@ -368,6 +373,10 @@ public function selectItems(array $items, $isSortable = true) $this->sortGridByField('ID'); } foreach ($items as $item) { + //Scroll to the top of the page in case current page input is not visible. + if (!$this->_rootElement->find($this->currentPage, Locator::SELECTOR_XPATH)->isVisible()) { + $this->browser->find($this->topElementToScroll)->hover(); + } $this->_rootElement->find($this->currentPage, Locator::SELECTOR_XPATH)->setValue(''); $this->waitLoader(); $selectItem = $this->getRow($item)->find($this->selectItem); diff --git a/dev/tests/functional/tests/app/Magento/Ui/Test/TestCase/GridSortingTest.php b/dev/tests/functional/tests/app/Magento/Ui/Test/TestCase/GridSortingTest.php index af4ccfdac9d3..0574fc8dc55f 100644 --- a/dev/tests/functional/tests/app/Magento/Ui/Test/TestCase/GridSortingTest.php +++ b/dev/tests/functional/tests/app/Magento/Ui/Test/TestCase/GridSortingTest.php @@ -89,6 +89,7 @@ public function test( $page->open(); /** @var DataGrid $gridBlock */ $gridBlock = $page->$gridRetriever(); + $gridBlock->waitFilterToLoad(); $gridBlock->resetFilter(); $sortingResults = []; diff --git a/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/CategoryUrlRewriteTest.php b/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/CategoryUrlRewriteTest.php index 8147818135ed..a2767f76cfec 100644 --- a/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/CategoryUrlRewriteTest.php +++ b/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/CategoryUrlRewriteTest.php @@ -85,6 +85,7 @@ public function test(Store $storeView, Category $childCategory, Category $parent $this->catalogCategoryEdit->getFormPageActions()->selectStoreView($storeView->getName()); $this->catalogCategoryEdit->getEditForm()->fill($categoryUpdates); $this->catalogCategoryEdit->getFormPageActions()->save(); + $this->catalogCategoryEdit->getFormPageActions()->selectStoreView('All Store Views'); $this->catalogCategoryIndex->getTreeCategories()->assignCategory( $parentCategory->getName(), $childCategory->getName() 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 730eb285d6d0..1f888c3a2a8b 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 890b1d55a647..1293c6e89b69 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 159663ad391e..d6c37b851aa1 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 1917ec928bfa..0cf2fd9c0d4e 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 98e3f9e38382..8e737f193cf9 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 90f5edec2f46..8e0e8ebde99f 100644 --- a/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/DeleteCustomUrlRewriteEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/DeleteCustomUrlRewriteEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\UrlRewrite\Test\TestCase\DeleteCustomUrlRewriteEntityTest" summary="Delete Custom URL Rewrites" ticketId="MAGETWO-26337"> <variation name="DeleteCustomUrlRewriteEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="urlRewrite/dataset" xsi:type="string">default</data> <constraint name="Magento\UrlRewrite\Test\Constraint\AssertUrlRewriteDeletedMessage" /> <constraint name="Magento\UrlRewrite\Test\Constraint\AssertUrlRewriteNotInGrid" /> diff --git a/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/NavigateMenuTest.xml index cdf1d9e3617d..30d9e57602af 100644 --- a/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/NavigateMenuTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest91"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Marketing > URL Rewrites</data> <data name="pageTitle" xsi:type="string">URL Rewrites</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/UpdateCategoryUrlRewriteEntityTest.xml b/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/UpdateCategoryUrlRewriteEntityTest.xml index 4a88ad01b5a7..b2be1a205212 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 3671cd767ece..2405216c1220 100644 --- a/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/UpdateCustomUrlRewriteEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/UpdateCustomUrlRewriteEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\UrlRewrite\Test\TestCase\UpdateCustomUrlRewriteEntityTest" summary="Update Custom URL Rewrites" ticketId="MAGETWO-25784"> <variation name="UpdateCustomUrlRewriteEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="initialRewrite/dataset" xsi:type="string">default</data> <data name="urlRewrite/data/store_id" xsi:type="string">Main Website/Main Website Store/Default Store View</data> <data name="urlRewrite/data/request_path" xsi:type="string">wishlist/%isolation%</data> @@ -19,7 +20,7 @@ <constraint name="Magento\UrlRewrite\Test\Constraint\AssertUrlRewriteSuccessOutsideRedirect" /> </variation> <variation name="UpdateCustomUrlRewriteEntityTestVariation2"> - <data name="tag" xsi:type="string">test_type:extended_acceptance_test</data> + <data name="tag" xsi:type="string">test_type:extended_acceptance_test, mftf_migrated:yes</data> <data name="initialRewrite/dataset" xsi:type="string">custom_rewrite_wishlist</data> <data name="urlRewrite/data/store_id" xsi:type="string">Main Website/Main Website Store/Default Store View</data> <data name="urlRewrite/data/request_path" xsi:type="string">wishlist/%isolation%</data> diff --git a/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/UpdateProductUrlRewriteEntityTest.xml b/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/UpdateProductUrlRewriteEntityTest.xml index 60de554d594d..8f12930aa417 100644 --- a/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/UpdateProductUrlRewriteEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/UrlRewrite/Test/TestCase/UpdateProductUrlRewriteEntityTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\UrlRewrite\Test\TestCase\UpdateProductUrlRewriteEntityTest" summary="Update Product URL Rewrites" ticketId="MAGETWO-24819"> <variation name="UpdateProductUrlRewriteEntityTestVariation1"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="urlRewrite/data/target_path/entity" xsi:type="string">product/%catalogProductSimple::product_100_dollar%</data> <data name="urlRewrite/data/store_id" xsi:type="string">Main Website/Main Website Store/Default Store View</data> <data name="urlRewrite/data/request_path" xsi:type="string">test_%isolation%.html</data> diff --git a/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertAccessTokensErrorRevokeMessage.php b/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertAccessTokensErrorRevokeMessage.php deleted file mode 100644 index b1a64c7c7e71..000000000000 --- a/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertAccessTokensErrorRevokeMessage.php +++ /dev/null @@ -1,46 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\User\Test\Constraint; - -use Magento\User\Test\Page\Adminhtml\UserEdit; -use Magento\Mtf\Constraint\AbstractConstraint; - -/** - * Class AssertAccessTokensErrorRevokeMessage - * Assert that error message appears after click on 'Force Sing-In' button for user without tokens. - */ -class AssertAccessTokensErrorRevokeMessage extends AbstractConstraint -{ - /** - * User revoke tokens error message. - */ - const ERROR_MESSAGE = 'This user has no tokens.'; - - /** - * Assert that error message appears after click on 'Force Sing-In' button for user without tokens. - * - * @param UserEdit $userEdit - * @return void - */ - public function processAssert(UserEdit $userEdit) - { - \PHPUnit\Framework\Assert::assertEquals( - self::ERROR_MESSAGE, - $userEdit->getMessagesBlock()->getErrorMessage() - ); - } - - /** - * Return string representation of object - * - * @return string - */ - public function toString() - { - return self::ERROR_MESSAGE . ' error message is present on UserEdit page.'; - } -} diff --git a/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertAccessTokensSuccessfullyRevoked.php b/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertAccessTokensSuccessfullyRevoked.php new file mode 100644 index 000000000000..b2e52f6a15a1 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertAccessTokensSuccessfullyRevoked.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\User\Test\Constraint; + +use Magento\User\Test\Page\Adminhtml\UserEdit; +use Magento\Mtf\Constraint\AbstractConstraint; + +/** + * Assert that success message appears after click on 'Force Sing-In' button. + */ +class AssertAccessTokensSuccessfullyRevoked extends AbstractConstraint +{ + /** + * User revoke tokens success message. + */ + const SUCCESS_MESSAGE = 'You have revoked the user\'s tokens.'; + + /** + * Assert that success message appears after click on 'Force Sing-In' button. + * + * @param UserEdit $userEdit + * @return void + */ + public function processAssert(UserEdit $userEdit): void + { + \PHPUnit\Framework\Assert::assertEquals( + self::SUCCESS_MESSAGE, + $userEdit->getMessagesBlock()->getSuccessMessage() + ); + } + + /** + * Return string representation of object + * + * @return string + */ + public function toString() + { + return self::SUCCESS_MESSAGE . ' message is present on UserEdit page.'; + } +} diff --git a/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertUserRoleRestrictedAccess.php b/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertUserRoleRestrictedAccess.php index f7c56ae1b965..ecfbc8d35388 100644 --- a/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertUserRoleRestrictedAccess.php +++ b/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertUserRoleRestrictedAccess.php @@ -10,6 +10,7 @@ use Magento\Mtf\Client\BrowserInterface; use Magento\Mtf\Constraint\AbstractConstraint; use Magento\User\Test\Fixture\User; +use Magento\User\Test\TestStep\LoginUserOnBackendWithErrorStep; /** * Asserts that user has only related permissions. @@ -18,6 +19,8 @@ class AssertUserRoleRestrictedAccess extends AbstractConstraint { const DENIED_ACCESS = 'Sorry, you need permissions to view this content.'; + protected $loginStep = 'Magento\User\Test\TestStep\LoginUserOnBackendStep'; + /** * Asserts that user has only related permissions. * @@ -36,7 +39,7 @@ public function processAssert( $denyUrl ) { $this->objectManager->create( - \Magento\User\Test\TestStep\LoginUserOnBackendStep::class, + $this->loginStep, ['user' => $user] )->run(); diff --git a/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertUserRoleRestrictedAccessWithError.php b/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertUserRoleRestrictedAccessWithError.php new file mode 100644 index 000000000000..b001893abb4c --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertUserRoleRestrictedAccessWithError.php @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\User\Test\Constraint; + +/** + * @inheritdoc + */ +class AssertUserRoleRestrictedAccessWithError extends AssertUserRoleRestrictedAccess +{ + protected $loginStep = 'Magento\User\Test\TestStep\LoginUserOnBackendWithErrorStep'; +} diff --git a/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertUserSuccessLogin.php b/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertUserSuccessLogin.php index c0c04628f744..c4645e6e8916 100644 --- a/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertUserSuccessLogin.php +++ b/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertUserSuccessLogin.php @@ -7,14 +7,20 @@ namespace Magento\User\Test\Constraint; use Magento\Backend\Test\Page\Adminhtml\Dashboard; -use Magento\User\Test\Fixture\User; use Magento\Mtf\Constraint\AbstractConstraint; +use Magento\User\Test\Fixture\User; +use Magento\User\Test\TestStep\LoginUserOnBackendStep; /** * Verify whether customer has logged in to the Backend. */ class AssertUserSuccessLogin extends AbstractConstraint { + /** + * @var string + */ + protected $loginStep = LoginUserOnBackendStep::class; + /** * Verify whether customer has logged in to the Backend. * @@ -25,7 +31,7 @@ class AssertUserSuccessLogin extends AbstractConstraint public function processAssert(User $user, Dashboard $dashboard) { $this->objectManager->create( - \Magento\User\Test\TestStep\LoginUserOnBackendStep::class, + $this->loginStep, ['user' => $user] )->run(); \PHPUnit\Framework\Assert::assertTrue( diff --git a/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertUserSuccessLoginWithError.php b/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertUserSuccessLoginWithError.php new file mode 100644 index 000000000000..9fed1f4df857 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/User/Test/Constraint/AssertUserSuccessLoginWithError.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\User\Test\Constraint; + +use Magento\User\Test\TestStep\LoginUserOnBackendWithErrorStep; + +/** + * Verify whether customer has logged in to the Backend with error alert. + */ +class AssertUserSuccessLoginWithError extends AssertUserSuccessLogin +{ + /** + * @var string + */ + protected $loginStep = LoginUserOnBackendWithErrorStep::class; +} diff --git a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/NavigateMenuTest.xml index d196bebca0e9..4572738b6cb4 100644 --- a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/NavigateMenuTest.xml @@ -8,21 +8,25 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest52"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">System > Locked Users</data> <data name="pageTitle" xsi:type="string">Locked Users</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest53"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">System > Manage Encryption Key</data> <data name="pageTitle" xsi:type="string">Encryption Key</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest92"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">System > All Users</data> <data name="pageTitle" xsi:type="string">Users</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> </variation> <variation name="NavigateMenuTest93"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">System > User Roles</data> <data name="pageTitle" xsi:type="string">Roles</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/RevokeAllAccessTokensForAdminWithoutTokensTest.xml b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/RevokeAllAccessTokensForAdminWithoutTokensTest.xml index e5fcba9b72c2..afdb72d8c561 100644 --- a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/RevokeAllAccessTokensForAdminWithoutTokensTest.xml +++ b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/RevokeAllAccessTokensForAdminWithoutTokensTest.xml @@ -9,7 +9,7 @@ <testCase name="Magento\User\Test\TestCase\RevokeAllAccessTokensForAdminWithoutTokensTest" summary="Revoke All Access Tokens for Admin without Tokens" ticketId="MAGETWO-29675"> <variation name="RevokeAllAccessTokensForAdminWithoutTokensTestVariation1"> <data name="user/dataset" xsi:type="string">custom_admin</data> - <constraint name="Magento\User\Test\Constraint\AssertAccessTokensErrorRevokeMessage" /> + <constraint name="Magento\User\Test\Constraint\AssertAccessTokensSuccessfullyRevoked" /> </variation> </testCase> </config> diff --git a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserEntityTest.xml b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserEntityTest.xml index f7de667cf17a..a89d1ede8011 100644 --- a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserEntityTest.xml @@ -32,7 +32,7 @@ <constraint name="Magento\User\Test\Constraint\AssertUserInGrid" /> <constraint name="Magento\User\Test\Constraint\AssertUserSuccessLogOut" /> <constraint name="Magento\User\Test\Constraint\AssertUserSuccessLogin" /> - <constraint name="Magento\User\Test\Constraint\AssertUserRoleRestrictedAccess" /> + <constraint name="Magento\User\Test\Constraint\AssertUserRoleRestrictedAccessWithError" /> </variation> </testCase> </config> diff --git a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserRoleEntityTest.php b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserRoleEntityTest.php index cc1d0fc980fb..58450abc7163 100644 --- a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserRoleEntityTest.php +++ b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserRoleEntityTest.php @@ -121,6 +121,11 @@ public function testUpdateAdminUserRolesEntity( */ public function tearDown() { + sleep(3); + $modalMessage = $this->dashboard->getModalMessage(); + if ($modalMessage->isVisible()) { + $modalMessage->acceptAlert(); + } $this->dashboard->getAdminPanelHeader()->logOut(); } } diff --git a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserRoleEntityTest.xml b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserRoleEntityTest.xml index 1f1530b08b28..db6a13d0f355 100644 --- a/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserRoleEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/User/Test/TestCase/UpdateAdminUserRoleEntityTest.xml @@ -17,7 +17,6 @@ <constraint name="Magento\User\Test\Constraint\AssertUserSuccessLogin" /> </variation> <variation name="UpdateAdminUserRoleEntityTestVariation2"> - <data name="issue" xsi:type="string">MAGETWO-65658: [FT] User with restricted access can't log in to Admin in UpdateAdminUserEntityTest</data> <data name="user/dataset" xsi:type="string">default</data> <data name="role/data/resource_access" xsi:type="string">Custom</data> <data name="role/data/roles_resources" xsi:type="string">Sales</data> @@ -30,8 +29,8 @@ <constraint name="Magento\User\Test\Constraint\AssertRoleSuccessSaveMessage" /> <constraint name="Magento\User\Test\Constraint\AssertRoleInGrid" /> <constraint name="Magento\User\Test\Constraint\AssertUserSuccessLogOut" /> - <constraint name="Magento\User\Test\Constraint\AssertUserSuccessLogin" /> - <constraint name="Magento\User\Test\Constraint\AssertUserRoleRestrictedAccess" /> + <constraint name="Magento\User\Test\Constraint\AssertUserSuccessLoginWithError"/> + <constraint name="Magento\User\Test\Constraint\AssertUserRoleRestrictedAccessWithError" /> </variation> <variation name="UpdateAdminUserRoleEntityTestVariation3"> <data name="user/dataset" xsi:type="string">custom_admin_with_default_role</data> diff --git a/dev/tests/functional/tests/app/Magento/User/Test/TestStep/CloseErrorAlertStep.php b/dev/tests/functional/tests/app/Magento/User/Test/TestStep/CloseErrorAlertStep.php new file mode 100644 index 000000000000..51d48058c8ae --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/User/Test/TestStep/CloseErrorAlertStep.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\User\Test\TestStep; + +use Magento\Backend\Test\Page\Adminhtml\Dashboard; +use Magento\Mtf\Client\BrowserInterface; +use Magento\Mtf\TestStep\TestStepInterface; + +/** + * Close access error modal message. + */ +class CloseErrorAlertStep implements TestStepInterface +{ + /** + * @var Dashboard + */ + private $dashboard; + + /** + * @var BrowserInterface + */ + private $browser; + + /** + * @param Dashboard $dashboard + * @param BrowserInterface $browser + */ + public function __construct( + Dashboard $dashboard, + BrowserInterface $browser + ) { + $this->dashboard = $dashboard; + $this->browser = $browser; + } + + /** + * @inheritdoc + */ + public function run() + { + $modalMessage = $this->dashboard->getModalMessage(); + try { + $this->browser->waitUntil( + function () use ($modalMessage) { + return $modalMessage->isVisible() ? true : null; + } + ); + $modalMessage->acceptAlert(); + } catch (\PHPUnit_Extensions_Selenium2TestCase_WebDriverException $e) { + //There is no modal to accept. + } + } +} diff --git a/dev/tests/functional/tests/app/Magento/User/Test/TestStep/LoginUserOnBackendStep.php b/dev/tests/functional/tests/app/Magento/User/Test/TestStep/LoginUserOnBackendStep.php index 4f7e6deed7a8..c244e27d4289 100644 --- a/dev/tests/functional/tests/app/Magento/User/Test/TestStep/LoginUserOnBackendStep.php +++ b/dev/tests/functional/tests/app/Magento/User/Test/TestStep/LoginUserOnBackendStep.php @@ -50,7 +50,7 @@ class LoginUserOnBackendStep implements TestStepInterface * * @var BrowserInterface */ - private $browser; + protected $browser; /** * Array of error messages on admin login form. @@ -108,8 +108,6 @@ public function run() } } } - - $this->dashboard->getSystemMessageDialog()->closePopup(); } /** diff --git a/dev/tests/functional/tests/app/Magento/User/Test/TestStep/LoginUserOnBackendWithErrorStep.php b/dev/tests/functional/tests/app/Magento/User/Test/TestStep/LoginUserOnBackendWithErrorStep.php new file mode 100644 index 000000000000..094f90d0a5d7 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/User/Test/TestStep/LoginUserOnBackendWithErrorStep.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\User\Test\TestStep; + +use Magento\Backend\Test\Page\AdminAuthLogin; +use Magento\Backend\Test\Page\Adminhtml\Dashboard; +use Magento\Mtf\Client\BrowserInterface; +use Magento\User\Test\Fixture\User; + +/** + * Login user on backend with access error. + */ +class LoginUserOnBackendWithErrorStep extends LoginUserOnBackendStep +{ + /** + * @var CloseErrorAlertStep + */ + private $closeErrorAlertStep; + + /** + * @param LogoutUserOnBackendStep $logoutUserOnBackendStep + * @param AdminAuthLogin $adminAuth + * @param User $user + * @param Dashboard $dashboard + * @param BrowserInterface $browser + */ + public function __construct( + LogoutUserOnBackendStep $logoutUserOnBackendStep, + AdminAuthLogin $adminAuth, + User $user, + Dashboard $dashboard, + BrowserInterface $browser, + CloseErrorAlertStep $closeErrorAlertStep + ) { + parent::__construct($logoutUserOnBackendStep, $adminAuth, $user, $dashboard, $browser); + $this->closeErrorAlertStep = $closeErrorAlertStep; + } + + /** + * Run step flow. + * + * @return void + */ + public function run() + { + parent::run(); + $this->closeErrorAlertStep->run(); + } +} diff --git a/dev/tests/functional/tests/app/Magento/User/Test/TestStep/LogoutUserOnBackendStep.php b/dev/tests/functional/tests/app/Magento/User/Test/TestStep/LogoutUserOnBackendStep.php index 70a4080a0b4d..7f366312bba2 100644 --- a/dev/tests/functional/tests/app/Magento/User/Test/TestStep/LogoutUserOnBackendStep.php +++ b/dev/tests/functional/tests/app/Magento/User/Test/TestStep/LogoutUserOnBackendStep.php @@ -48,7 +48,6 @@ public function __construct(AdminAuthLogin $adminAuth, Dashboard $dashboard) public function run() { $this->adminAuth->open(); - $this->dashboard->getSystemMessageDialog()->closePopup(); $this->dashboard->getAdminPanelHeader()->logOut(); } } diff --git a/dev/tests/functional/tests/app/Magento/User/Test/TestStep/LogoutUserOnBackendWithErrorStep.php b/dev/tests/functional/tests/app/Magento/User/Test/TestStep/LogoutUserOnBackendWithErrorStep.php new file mode 100644 index 000000000000..ce49e86afc06 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/User/Test/TestStep/LogoutUserOnBackendWithErrorStep.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\User\Test\TestStep; + +use Magento\Backend\Test\Page\AdminAuthLogin; +use Magento\Backend\Test\Page\Adminhtml\Dashboard; + +/** + * Logout user on backend with access error. + */ +class LogoutUserOnBackendWithErrorStep extends LogoutUserOnBackendStep +{ + /** + * @var CloseErrorAlertStep + */ + private $closeErrorAlertStep; + + public function __construct( + AdminAuthLogin $adminAuth, + Dashboard $dashboard, + CloseErrorAlertStep $closeErrorAlertStep + ) { + parent::__construct($adminAuth, $dashboard); + $this->closeErrorAlertStep = $closeErrorAlertStep; + } + + /** + * @inheritdoc + */ + public function run() + { + $this->adminAuth->open(); + $this->closeErrorAlertStep->run(); + $this->dashboard->getAdminPanelHeader()->logOut(); + } +} diff --git a/dev/tests/functional/tests/app/Magento/User/Test/etc/di.xml b/dev/tests/functional/tests/app/Magento/User/Test/etc/di.xml new file mode 100644 index 000000000000..1298bd56a8fb --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/User/Test/etc/di.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" ?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\User\Test\TestStep\LoginUserOnBackendWithErrorStep"> + <arguments> + <argument name="logoutUserOnBackendStep" xsi:type="object">\Magento\User\Test\TestStep\LogoutUserOnBackendWithErrorStep</argument> + </arguments> + </type> +</config> diff --git a/dev/tests/functional/tests/app/Magento/Variable/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Variable/Test/TestCase/NavigateMenuTest.xml index 3fceddf1a807..0a9c74cd92bc 100644 --- a/dev/tests/functional/tests/app/Magento/Variable/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Variable/Test/TestCase/NavigateMenuTest.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest94"> + <data name="tag" xsi:type="string">mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">System > Custom Variables</data> <data name="pageTitle" xsi:type="string">Custom Variables</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable"/> diff --git a/dev/tests/functional/tests/app/Magento/Widget/Test/Handler/Widget/Curl.php b/dev/tests/functional/tests/app/Magento/Widget/Test/Handler/Widget/Curl.php index 1a024eefe162..13c16c888fbb 100644 --- a/dev/tests/functional/tests/app/Magento/Widget/Test/Handler/Widget/Curl.php +++ b/dev/tests/functional/tests/app/Magento/Widget/Test/Handler/Widget/Curl.php @@ -172,7 +172,7 @@ protected function prepareWidgetInstance(array $data) $widgetInstances = []; foreach ($data['widget_instance'] as $key => $widgetInstance) { $pageGroup = $widgetInstance['page_group']; - $method = 'prepare' . str_replace(' ', '', ucwords(str_replace('_', ' ', $pageGroup))) . 'Group'; + $method = 'prepare' . str_replace('_', '', ucwords($pageGroup, '_')) . 'Group'; if (!method_exists(__CLASS__, $method)) { throw new \Exception('Method for prepare page group "' . $method . '" is not exist.'); } diff --git a/dev/tests/functional/tests/app/Magento/Widget/Test/TestCase/NavigateMenuTest.xml b/dev/tests/functional/tests/app/Magento/Widget/Test/TestCase/NavigateMenuTest.xml index 6b3215dd30d1..6a2a533c59df 100644 --- a/dev/tests/functional/tests/app/Magento/Widget/Test/TestCase/NavigateMenuTest.xml +++ b/dev/tests/functional/tests/app/Magento/Widget/Test/TestCase/NavigateMenuTest.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> <testCase name="Magento\Backend\Test\TestCase\NavigateMenuTest"> <variation name="NavigateMenuTest96"> - <data name="tag" xsi:type="string">severity:S2</data> + <data name="tag" xsi:type="string">severity:S2, mftf_migrated:yes</data> <data name="menuItem" xsi:type="string">Content > Widgets</data> <data name="pageTitle" xsi:type="string">Widgets</data> <constraint name="Magento\Backend\Test\Constraint\AssertBackendPageIsAvailable" /> diff --git a/dev/tests/functional/tests/app/Magento/Wishlist/Test/Block/Customer/Wishlist/Items/TopToolbar.php b/dev/tests/functional/tests/app/Magento/Wishlist/Test/Block/Customer/Wishlist/Items/TopToolbar.php new file mode 100644 index 000000000000..9d2d0fee46b5 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/Wishlist/Test/Block/Customer/Wishlist/Items/TopToolbar.php @@ -0,0 +1,98 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Test\Block\Customer\Wishlist\Items; + +use Magento\Mtf\Block\Block; +use Magento\Mtf\Client\Locator; + +/** + * Pager block for wishlist items page. + */ +class TopToolbar extends Block +{ + /** + * Selector next active element + * + * @var string + */ + private $nextPageSelector = '.item.current + .item a'; + + /** + * Selector first element + * + * @var string + */ + private $firstPageSelector = '.item>.page'; + + /** + * Selector option element + * + * @var string + */ + private $optionSelector = './/option'; + + /** + * Go to the next page + * + * @return bool + */ + public function nextPage() + { + $nextPageItem = $this->_rootElement->find($this->nextPageSelector); + if ($nextPageItem->isVisible()) { + $nextPageItem->click(); + return true; + } + return false; + } + + /** + * Go to the first page + * + * @return bool + */ + public function firstPage() + { + $firstPageItem = $this->_rootElement->find($this->firstPageSelector); + if ($firstPageItem->isVisible()) { + $firstPageItem->click(); + return true; + } + return false; + } + + /** + * Set value for limiter element by index + * + * @param int $index + * @return $this + */ + public function setLimiterValueByIndex($index) + { + $options = $this->_rootElement->getElements($this->optionSelector, Locator::SELECTOR_XPATH); + if (isset($options[$index])) { + $options[$index]->click(); + } + return $this; + } + + /** + * Get value for limiter element by index + * + * @param int $index + * @return int|null + */ + public function getLimitedValueByIndex($index) + { + $options = $this->_rootElement->getElements($this->optionSelector, Locator::SELECTOR_XPATH); + if (isset($options[$index])) { + return $options[$index]->getValue(); + } + return null; + } +} diff --git a/dev/tests/functional/tests/app/Magento/Wishlist/Test/Constraint/AssertProductIsPresentInWishlist.php b/dev/tests/functional/tests/app/Magento/Wishlist/Test/Constraint/AssertProductIsPresentInWishlist.php index ae994f84c47f..058c764be16a 100644 --- a/dev/tests/functional/tests/app/Magento/Wishlist/Test/Constraint/AssertProductIsPresentInWishlist.php +++ b/dev/tests/functional/tests/app/Magento/Wishlist/Test/Constraint/AssertProductIsPresentInWishlist.php @@ -36,6 +36,13 @@ public function processAssert( $cmsIndex->getLinksBlock()->openLink('My Account'); $customerAccountIndex->getAccountMenuBlock()->openMenuItem('My Wish List'); + $isProductVisible = $wishlistIndex->getWishlistBlock()->getProductItemsBlock()->getItemProduct($product) + ->isVisible(); + while (!$isProductVisible && $wishlistIndex->getTopToolbar()->nextPage()) { + $isProductVisible = $wishlistIndex->getWishlistBlock()->getProductItemsBlock()->getItemProduct($product) + ->isVisible(); + } + \PHPUnit\Framework\Assert::assertTrue( $wishlistIndex->getWishlistBlock()->getProductItemsBlock()->getItemProduct($product)->isVisible(), $product->getName() . ' is not visible on Wish List page.' diff --git a/dev/tests/functional/tests/app/Magento/Wishlist/Test/Constraint/AssertProductRegularPriceOnStorefront.php b/dev/tests/functional/tests/app/Magento/Wishlist/Test/Constraint/AssertProductRegularPriceOnStorefront.php index 68e30e13558c..dc71939d4790 100644 --- a/dev/tests/functional/tests/app/Magento/Wishlist/Test/Constraint/AssertProductRegularPriceOnStorefront.php +++ b/dev/tests/functional/tests/app/Magento/Wishlist/Test/Constraint/AssertProductRegularPriceOnStorefront.php @@ -44,44 +44,40 @@ public function processAssert( $cmsIndex->getLinksBlock()->openLink('My Account'); $customerAccountIndex->getAccountMenuBlock()->openMenuItem('My Wish List'); - $productRegularPrice = 0; - if ($product instanceof GroupedProduct) { - $associatedProducts = $product->getAssociated(); + $isProductVisible = $wishlistIndex->getWishlistBlock() + ->getProductItemsBlock() + ->getItemProduct($product) + ->isVisible(); + while (!$isProductVisible && $wishlistIndex->getTopToolbar()->nextPage()) { + $isProductVisible = $wishlistIndex->getWishlistBlock() + ->getProductItemsBlock() + ->getItemProduct($product) + ->isVisible(); + } - /** @var \Magento\Catalog\Test\Fixture\CatalogProductSimple $associatedProduct */ - foreach ($associatedProducts['products'] as $key => $associatedProduct) { - $qty = $associatedProducts['assigned_products'][$key]['qty']; - $price = $associatedProduct->getPrice(); - $productRegularPrice += $qty * $price; - } + if ($product instanceof GroupedProduct) { + $productRegularPrice = $this->getGroupedProductRegularPrice($product); } elseif ($product instanceof BundleProduct) { - $bundleSelection = (array)$product->getBundleSelections(); - foreach ($bundleSelection['products'] as $bundleOption) { - $regularBundleProductPrice = 0; - /** @var \Magento\Catalog\Test\Fixture\CatalogProductSimple $bundleProduct */ - foreach ($bundleOption as $bundleProduct) { - $checkoutData = $bundleProduct->getCheckoutData(); - $bundleProductPrice = $checkoutData['qty'] * $checkoutData['cartItem']['price']; - if (0 === $regularBundleProductPrice) { - $regularBundleProductPrice = $bundleProductPrice; - } else { - $regularBundleProductPrice = max([$bundleProductPrice, $regularBundleProductPrice]); - } - } - $productRegularPrice += $regularBundleProductPrice; - } + $productRegularPrice = $this->getBundleProductRegularPrice($product); } else { - $productRegularPrice = (float)$product->getPrice(); + $productRegularPrice = (float) $product->getPrice(); } - $productItem = $wishlistIndex->getWishlistBlock()->getProductItemsBlock()->getItemProduct($product); - $wishListProductRegularPrice = (float)$productItem->getRegularPrice(); + $productItem = $wishlistIndex->getWishlistBlock() + ->getProductItemsBlock() + ->getItemProduct($product); - \PHPUnit\Framework\Assert::assertEquals( - $this->regularPriceLabel, - $productItem->getPriceLabel(), - 'Wrong product regular price is displayed.' - ); + $wishListProductRegularPrice = $product instanceof BundleProduct + ? (float)$productItem->getPrice() + : (float)$productItem->getRegularPrice(); + + if (!$product instanceof BundleProduct) { + \PHPUnit\Framework\Assert::assertEquals( + $this->regularPriceLabel, + $productItem->getPriceLabel(), + 'Wrong product regular price is displayed.' + ); + } \PHPUnit\Framework\Assert::assertNotEmpty( $wishListProductRegularPrice, @@ -95,6 +91,52 @@ public function processAssert( ); } + /** + * Retrieve grouped product regular price + * + * @param GroupedProduct $product + * @return float + */ + private function getGroupedProductRegularPrice(GroupedProduct $product) + { + $productRegularPrice = 0; + $associatedProducts = $product->getAssociated(); + /** @var \Magento\Catalog\Test\Fixture\CatalogProductSimple $associatedProduct */ + foreach ($associatedProducts['products'] as $key => $associatedProduct) { + $qty = $associatedProducts['assigned_products'][$key]['qty']; + $price = $associatedProduct->getPrice(); + $productRegularPrice += $qty * $price; + } + return $productRegularPrice; + } + + /** + * Retrieve bundle product regular price + * + * @param BundleProduct $product + * @return float + */ + private function getBundleProductRegularPrice(BundleProduct $product) + { + $productRegularPrice = 0; + $bundleSelection = (array) $product->getBundleSelections(); + foreach ($bundleSelection['products'] as $bundleOption) { + $regularBundleProductPrice = 0; + /** @var \Magento\Catalog\Test\Fixture\CatalogProductSimple $bundleProduct */ + foreach ($bundleOption as $bundleProduct) { + $checkoutData = $bundleProduct->getCheckoutData(); + $bundleProductPrice = $checkoutData['qty'] * $checkoutData['cartItem']['price']; + if (0 === $regularBundleProductPrice) { + $regularBundleProductPrice = $bundleProductPrice; + } else { + $regularBundleProductPrice = max([$bundleProductPrice, $regularBundleProductPrice]); + } + } + $productRegularPrice += $regularBundleProductPrice; + } + return $productRegularPrice; + } + /** * Returns a string representation of the object. * diff --git a/dev/tests/functional/tests/app/Magento/Wishlist/Test/Page/WishlistIndex.xml b/dev/tests/functional/tests/app/Magento/Wishlist/Test/Page/WishlistIndex.xml index 141bc8c5898c..4e67c8d4e1dd 100644 --- a/dev/tests/functional/tests/app/Magento/Wishlist/Test/Page/WishlistIndex.xml +++ b/dev/tests/functional/tests/app/Magento/Wishlist/Test/Page/WishlistIndex.xml @@ -9,5 +9,6 @@ <page name="WishlistIndex" mca="wishlist/index/index" module="Magento_Wishlist"> <block name="messagesBlock" class="Magento\Backend\Test\Block\Messages" locator=".messages" strategy="css selector"/> <block name="wishlistBlock" class="Magento\Wishlist\Test\Block\Customer\Wishlist" locator="#wishlist-view-form" strategy="css selector"/> + <block name="topToolbar" class="Magento\Wishlist\Test\Block\Customer\Wishlist\Items\TopToolbar" locator=".//*[contains(@class,'wishlist-toolbar')][2]" strategy="xpath"/> </page> </config> diff --git a/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/AddProductToWishlistEntityTest.xml b/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/AddProductToWishlistEntityTest.xml index 06b1a6078d5c..e5fa4b6fc11e 100644 --- a/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/AddProductToWishlistEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/AddProductToWishlistEntityTest.xml @@ -108,7 +108,7 @@ </variation> <variation name="AddProductToWishlistEntityTestVariation14" ticketId="MAGETWO-90131"> <data name="product" xsi:type="array"> - <item name="0" xsi:type="string">bundleProduct::with_special_price_and_custom_options</item> + <item name="0" xsi:type="string">bundleProduct::default_with_one_simple_product</item> </data> <constraint name="Magento\Wishlist\Test\Constraint\AssertAddProductToWishlistSuccessMessage"/> <constraint name="Magento\Wishlist\Test\Constraint\AssertProductIsPresentInWishlist"/> diff --git a/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/ShareWishlistEntityTest.xml b/dev/tests/functional/tests/app/Magento/Wishlist/Test/TestCase/ShareWishlistEntityTest.xml index ec01f57202b2..cbf5ba392844 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/testsuites/Magento/Mtf/TestSuite/InjectableTests/basic_green.xml b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/basic_green.xml index 7eea71a955c4..17578fcfe85b 100644 --- a/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/basic_green.xml +++ b/dev/tests/functional/testsuites/Magento/Mtf/TestSuite/InjectableTests/basic_green.xml @@ -11,6 +11,7 @@ <deny> <tag group="stable" value="no" /> <tag group="to_maintain" value="yes" /> + <tag group="mftf_migrated" value="yes" /> </deny> </rule> <rule scope="testsuite"> @@ -23,6 +24,7 @@ <deny> <tag group="test_type" value="3rd_party_test, 3rd_party_test_single_flow" /> <tag group="stable" value="no" /> + <tag group="mftf_migrated" value="yes" /> <tag group="to_maintain" value="yes" /> </deny> </rule> diff --git a/dev/tests/functional/utils/command.php b/dev/tests/functional/utils/command.php index 8eaf82475a4e..99025dd1cffc 100644 --- a/dev/tests/functional/utils/command.php +++ b/dev/tests/functional/utils/command.php @@ -4,14 +4,19 @@ * See COPYING.txt for license details. */ +// phpcs:ignore Magento2.Security.IncludeFile require_once __DIR__ . '/../../../../app/bootstrap.php'; use Symfony\Component\Console\Input\StringInput; use Symfony\Component\Console\Output\NullOutput; +// phpcs:ignore Magento2.Security.Superglobal if (isset($_GET['command'])) { + // phpcs:ignore Magento2.Security.Superglobal $command = urldecode($_GET['command']); + // phpcs:ignore Magento2.Security.Superglobal $magentoObjectManagerFactory = \Magento\Framework\App\Bootstrap::createObjectManagerFactory(BP, $_SERVER); + // phpcs:ignore Magento2.Security.Superglobal $magentoObjectManager = $magentoObjectManagerFactory->create($_SERVER); $cli = $magentoObjectManager->create(\Magento\Framework\Console\Cli::class); $input = new StringInput($command); diff --git a/dev/tests/functional/utils/deleteMagentoGeneratedCode.php b/dev/tests/functional/utils/deleteMagentoGeneratedCode.php index 99aa9af06e92..17260bd1da63 100644 --- a/dev/tests/functional/utils/deleteMagentoGeneratedCode.php +++ b/dev/tests/functional/utils/deleteMagentoGeneratedCode.php @@ -4,4 +4,5 @@ * See COPYING.txt for license details. */ +// phpcs:ignore Magento2.Security.InsecureFunction exec('rm -rf ../../../../generated/*'); diff --git a/dev/tests/functional/utils/export.php b/dev/tests/functional/utils/export.php index 343dcc557c83..fa50bc729d0f 100644 --- a/dev/tests/functional/utils/export.php +++ b/dev/tests/functional/utils/export.php @@ -4,12 +4,16 @@ * See COPYING.txt for license details. */ +// phpcs:ignore Magento2.Security.Superglobal if (!isset($_GET['template'])) { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \InvalidArgumentException('Argument "template" must be set.'); } -$varDir = '../../../../var/'; +$varDir = '../../../../var/export/'; +// phpcs:ignore Magento2.Security.Superglobal $template = urldecode($_GET['template']); +// phpcs:ignore Magento2.Functions.DiscouragedFunction $fileList = scandir($varDir, SCANDIR_SORT_NONE); $files = []; @@ -17,11 +21,14 @@ if (preg_match("`$template`", $fileName) === 1) { $filePath = $varDir . $fileName; $files[] = [ + // phpcs:ignore Magento2.Functions.DiscouragedFunction 'content' => file_get_contents($filePath), 'name' => $fileName, + // phpcs:ignore Magento2.Functions.DiscouragedFunction 'date' => filectime($filePath), ]; } } +// phpcs:ignore Magento2.Security.LanguageConstruct, Magento2.Security.InsecureFunction echo serialize($files); diff --git a/dev/tests/functional/utils/locales.php b/dev/tests/functional/utils/locales.php index 827b8b1b8944..11e1e2b70fa5 100644 --- a/dev/tests/functional/utils/locales.php +++ b/dev/tests/functional/utils/locales.php @@ -4,14 +4,19 @@ * See COPYING.txt for license details. */ +// phpcs:ignore Magento2.Security.Superglobal if (isset($_GET['type']) && $_GET['type'] == 'deployed') { + // phpcs:ignore Magento2.Security.Superglobal $themePath = isset($_GET['theme_path']) ? $_GET['theme_path'] : 'adminhtml/Magento/backend'; $directory = __DIR__ . '/../../../../pub/static/' . $themePath; + // phpcs:ignore Magento2.Functions.DiscouragedFunction $locales = array_diff(scandir($directory), ['..', '.']); } else { + // phpcs:ignore Magento2.Security.IncludeFile require_once __DIR__ . DIRECTORY_SEPARATOR . 'bootstrap.php'; $localeConfig = $magentoObjectManager->create(\Magento\Framework\Locale\Config::class); $locales = $localeConfig->getAllowedLocales(); } +// phpcs:ignore Magento2.Security.LanguageConstruct echo implode('|', $locales); diff --git a/dev/tests/functional/utils/log.php b/dev/tests/functional/utils/log.php index b6c19458b10b..30783ae8e1d2 100644 --- a/dev/tests/functional/utils/log.php +++ b/dev/tests/functional/utils/log.php @@ -4,11 +4,20 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); +// phpcs:ignore Magento2.Security.Superglobal if (!isset($_GET['name'])) { - throw new \InvalidArgumentException('The name of log file is required for getting logs.'); + // phpcs:ignore Magento2.Exceptions.DirectThrow + throw new \InvalidArgumentException( + 'The name of log file is required for getting logs.' + ); } +// phpcs:ignore Magento2.Security.Superglobal $name = urldecode($_GET['name']); -$file = file_get_contents('../../../../var/log/' . $name); +if (preg_match('/\.\.(\\\|\/)/', $name)) { + throw new \InvalidArgumentException('Invalid log file name'); +} -echo serialize($file); +// phpcs:ignore Magento2.Security.InsecureFunction, Magento2.Functions.DiscouragedFunction, Magento2.Security.LanguageConstruct +echo serialize(file_get_contents('../../../../var/log' .'/' .$name)); diff --git a/dev/tests/functional/utils/pathChecker.php b/dev/tests/functional/utils/pathChecker.php index 11f8229bce56..217cf90af0a5 100644 --- a/dev/tests/functional/utils/pathChecker.php +++ b/dev/tests/functional/utils/pathChecker.php @@ -4,14 +4,19 @@ * See COPYING.txt for license details. */ +// phpcs:ignore Magento2.Security.Superglobal if (isset($_GET['path'])) { + // phpcs:ignore Magento2.Security.Superglobal $path = urldecode($_GET['path']); - + // phpcs:ignore Magento2.Functions.DiscouragedFunction if (file_exists('../../../../' . $path)) { + // phpcs:ignore Magento2.Security.LanguageConstruct echo 'path exists: true'; } else { + // phpcs:ignore Magento2.Security.LanguageConstruct echo 'path exists: false'; } } else { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \InvalidArgumentException("GET parameter 'path' is not set."); } diff --git a/dev/tests/functional/utils/website.php b/dev/tests/functional/utils/website.php index 625f5c6b483f..720b4962aedd 100644 --- a/dev/tests/functional/utils/website.php +++ b/dev/tests/functional/utils/website.php @@ -4,13 +4,17 @@ * See COPYING.txt for license details. */ +// phpcs:ignore Magento2.Security.Superglobal if (!isset($_GET['website_code'])) { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception("website_code GET parameter is not set."); } +// phpcs:ignore Magento2.Security.Superglobal $websiteCode = urldecode($_GET['website_code']); $rootDir = '../../../../'; $websiteDir = $rootDir . 'websites/' . $websiteCode . '/'; +// phpcs:ignore Magento2.Functions.DiscouragedFunction $contents = file_get_contents($rootDir . 'index.php'); $websiteParam = <<<EOD @@ -25,8 +29,10 @@ $contents = preg_replace($pattern, $replacement, $contents); $old = umask(0); +// phpcs:ignore Magento2.Functions.DiscouragedFunction mkdir($websiteDir, 0760, true); umask($old); - +// phpcs:ignore Magento2.Functions.DiscouragedFunction copy($rootDir . '.htaccess', $websiteDir . '.htaccess'); +// phpcs:ignore Magento2.Functions.DiscouragedFunction file_put_contents($websiteDir . 'index.php', $contents); diff --git a/dev/tests/integration/_files/Magento/TestModuleMysqlMq/Model/DataObject.php b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/Model/DataObject.php new file mode 100644 index 000000000000..ad3033cf7eea --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/Model/DataObject.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\TestModuleMysqlMq\Model; + +class DataObject extends \Magento\Framework\Api\AbstractExtensibleObject +{ + /** + * @return string + */ + public function getName() + { + return $this->_get('name'); + } + + /** + * @param string $name + * @return $this + */ + public function setName($name) + { + return $this->setData('name', $name); + } + + /** + * @return int|null + */ + public function getEntityId() + { + return $this->_get('entity_id'); + } + + /** + * @param int $entityId + * @return $this + */ + public function setEntityId($entityId) + { + return $this->setData('entity_id', $entityId); + } + + /** + * @return string + */ + public function getOutputPath() + { + return $this->_get('outputPath'); + } + + /** + * @param string $path + * @return $this + */ + public function setOutputPath($path) + { + return $this->setData('outputPath', $path); + } +} diff --git a/dev/tests/integration/_files/Magento/TestModuleMysqlMq/Model/DataObjectRepository.php b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/Model/DataObjectRepository.php new file mode 100644 index 000000000000..942298e49972 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/Model/DataObjectRepository.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\TestModuleMysqlMq\Model; + +class DataObjectRepository +{ + /** + * @param DataObject $dataObject + * @param string $requiredParam + * @param int|null $optionalParam + * @return null + */ + public function delayedOperation( + \Magento\TestModuleMysqlMq\Model\DataObject $dataObject, + $requiredParam, + $optionalParam = null + ) { + $output = "Processed '{$dataObject->getEntityId()}'; " + . "Required param '{$requiredParam}'; Optional param '{$optionalParam}'\n"; + file_put_contents($dataObject->getOutputPath(), $output); + + return null; + } +} diff --git a/dev/tests/integration/_files/Magento/TestModuleMysqlMq/Model/Processor.php b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/Model/Processor.php new file mode 100644 index 000000000000..fb6fd4c5c280 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/Model/Processor.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\TestModuleMysqlMq\Model; + +/** + * Test message processor is used by \Magento\MysqlMq\Model\PublisherConsumerTest + */ +class Processor +{ + /** + * @param \Magento\TestModuleMysqlMq\Model\DataObject $message + */ + public function processMessage($message) + { + file_put_contents( + $message->getOutputPath(), + "Processed {$message->getEntityId()}" . PHP_EOL, + FILE_APPEND + ); + } + + /** + * @param \Magento\TestModuleMysqlMq\Model\DataObject $message + */ + public function processObjectCreated($message) + { + file_put_contents( + $message->getOutputPath(), + "Processed object created {$message->getEntityId()}" . PHP_EOL, + FILE_APPEND + ); + } + + /** + * @param \Magento\TestModuleMysqlMq\Model\DataObject $message + */ + public function processCustomObjectCreated($message) + { + file_put_contents( + $message->getOutputPath(), + "Processed custom object created {$message->getEntityId()}" . PHP_EOL, + FILE_APPEND + ); + } + + /** + * @param \Magento\TestModuleMysqlMq\Model\DataObject $message + */ + public function processObjectUpdated($message) + { + file_put_contents( + $message->getOutputPath(), + "Processed object updated {$message->getEntityId()}" . PHP_EOL, + FILE_APPEND + ); + } + + /** + * @param \Magento\TestModuleMysqlMq\Model\DataObject $message + */ + public function processMessageWithException($message) + { + file_put_contents($message->getOutputPath(), "Exception processing {$message->getEntityId()}"); + throw new \LogicException( + "Exception during message processing happened. Entity: {{$message->getEntityId()}}" + ); + } +} diff --git a/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/communication.xml b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/communication.xml new file mode 100644 index 000000000000..4d6269dbb792 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/communication.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Communication/etc/communication.xsd"> + <topic name="demo.exception" request="Magento\TestModuleMysqlMq\Model\DataObject"/> + <topic name="test.schema.defined.by.method" schema="Magento\TestModuleMysqlMq\Model\DataObjectRepository::delayedOperation" is_synchronous="false"/> + <topic name="demo.object.created" request="Magento\TestModuleMysqlMq\Model\DataObject"/> + <topic name="demo.object.updated" request="Magento\TestModuleMysqlMq\Model\DataObject"/> + <topic name="demo.object.custom.created" request="Magento\TestModuleMysqlMq\Model\DataObject"/> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/module.xml b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/module.xml new file mode 100644 index 000000000000..8b6ea0f44ce9 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/module.xml @@ -0,0 +1,11 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_TestModuleMysqlMq" active="true"> + </module> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/queue.xml b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/queue.xml new file mode 100644 index 000000000000..362237c0c5e6 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/queue.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/queue.xsd"> + <broker topic="demo.exception" type="db" exchange="magento"> + <queue consumer="demoConsumerWithException" name="queue-exception" handler="Magento\TestModuleMysqlMq\Model\Processor::processMessageWithException"/> + </broker> + <broker topic="test.schema.defined.by.method" type="db" exchange="magento"> + <queue consumer="delayedOperationConsumer" name="demo-queue-6" handler="Magento\TestModuleMysqlMq\Model\DataObjectRepository::delayedOperation"/> + </broker> + <broker topic="demo.object.created" type="db" exchange="magento"> + <queue consumer="demoConsumerQueueOne" name="queue-created" handler="\Magento\TestModuleMysqlMq\Model\Processor::processObjectCreated"/> + </broker> + <broker topic="demo.object.updated" exchange="magento" type="db"> + <queue consumer="demoConsumerQueueTwo" name="queue-updated" handler="\Magento\TestModuleMysqlMq\Model\Processor::processObjectUpdated"/> + </broker> + <broker topic="demo.object.custom.created" exchange="magento" type="db"> + <queue consumer="demoConsumerQueueThree" name="queue-custom-created" handler="\Magento\TestModuleMysqlMq\Model\Processor::processCustomObjectCreated"/> + </broker> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/queue_consumer.xml b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/queue_consumer.xml new file mode 100644 index 000000000000..bb495a123a05 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/queue_consumer.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/consumer.xsd"> + <consumer name="demoConsumerQueueOne" queue="queue-created" connection="db" handler="Magento\TestModuleMysqlMq\Model\Processor::processObjectCreated"/> + <consumer name="demoConsumerQueueTwo" queue="queue-updated" connection="db" handler="Magento\TestModuleMysqlMq\Model\Processor::processObjectUpdated"/> + <consumer name="demoConsumerQueueThree" queue="queue-custom-created" connection="db" handler="Magento\TestModuleMysqlMq\Model\Processor::processCustomObjectCreated"/> + <consumer name="demoConsumerWithException" queue="queue-exception" connection="db" handler="Magento\TestModuleMysqlMq\Model\Processor::processMessageWithException"/> + <consumer name="delayedOperationConsumer" queue="demo-queue-6" connection="db" handler="Magento\TestModuleMysqlMq\Model\DataObjectRepository::delayedOperation"/> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/queue_publisher.xml b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/queue_publisher.xml new file mode 100644 index 000000000000..a665e10ef5f1 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/queue_publisher.xml @@ -0,0 +1,24 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/publisher.xsd"> + <publisher topic="demo.exception"> + <connection name="db" exchange="magento"/> + </publisher> + <publisher topic="test.schema.defined.by.method"> + <connection name="db" exchange="magento"/> + </publisher> + <publisher topic="demo.object.created"> + <connection name="db" exchange="magento"/> + </publisher> + <publisher topic="demo.object.updated"> + <connection name="db" exchange="magento"/> + </publisher> + <publisher topic="demo.object.custom.created"> + <connection name="db" exchange="magento"/> + </publisher> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/queue_topology.xml b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/queue_topology.xml new file mode 100644 index 000000000000..2df5485ee344 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/etc/queue_topology.xml @@ -0,0 +1,17 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/topology.xsd"> + + <exchange name="magento" type="topic" connection="db"> + <binding id="demo.exception.consumer" topic="demo.exception" destination="queue-exception" destinationType="queue"/> + <binding id="test.schema.defined.by.method" topic="test.schema.defined.by.method" destination="demo-queue-6" destinationType="queue"/> + <binding id="demo.object.created" topic="demo.object.created" destination="queue-created" destinationType="queue"/> + <binding id="demo.object.updated" topic="demo.object.updated" destination="queue-updated" destinationType="queue"/> + <binding id="demo.object.all" topic="demo.object.*" destination="queue-custom-created" destinationType="queue"/> + </exchange> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleMysqlMq/registration.php b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/registration.php new file mode 100644 index 000000000000..4250e95bd7cc --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleMysqlMq/registration.php @@ -0,0 +1,12 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Framework\Component\ComponentRegistrar; + +$registrar = new ComponentRegistrar(); +if ($registrar->getPath(ComponentRegistrar::MODULE, 'Magento_TestModuleMysqlMq') === null) { + ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_TestModuleMysqlMq', __DIR__); +} diff --git a/dev/tests/integration/etc/di/preferences/ce.php b/dev/tests/integration/etc/di/preferences/ce.php index b871fe190591..3065b1712640 100644 --- a/dev/tests/integration/etc/di/preferences/ce.php +++ b/dev/tests/integration/etc/di/preferences/ce.php @@ -27,4 +27,7 @@ \Magento\Framework\App\Config\ScopeConfigInterface::class => \Magento\TestFramework\App\Config::class, \Magento\Framework\App\ResourceConnection\ConfigInterface::class => \Magento\Framework\App\ResourceConnection\Config::class, + \Magento\Framework\Lock\Backend\Cache::class => + \Magento\TestFramework\Lock\Backend\DummyLocker::class, + \Magento\Framework\Session\SessionStartChecker::class => \Magento\TestFramework\Session\SessionStartChecker::class, ]; diff --git a/dev/tests/integration/framework/Magento/TestFramework/Annotation/DataFixture.php b/dev/tests/integration/framework/Magento/TestFramework/Annotation/DataFixture.php index dc525a46428c..ddebbf37b16d 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Annotation/DataFixture.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Annotation/DataFixture.php @@ -4,13 +4,14 @@ * See COPYING.txt for license details. */ -/** - * Implementation of the @magentoDataFixture DocBlock annotation - */ namespace Magento\TestFramework\Annotation; +use Magento\Framework\Component\ComponentRegistrar; use PHPUnit\Framework\Exception; +/** + * Implementation of the @magentoDataFixture DocBlock annotation. + */ class DataFixture { /** @@ -126,6 +127,8 @@ protected function _getFixtures(\PHPUnit\Framework\TestCase $test, $scope = null $fixtureMethod = [get_class($test), $fixture]; if (is_callable($fixtureMethod)) { $result[] = $fixtureMethod; + } elseif ($this->isModuleAnnotation($fixture)) { + $result[] = $this->getModulePath($fixture); } else { $result[] = $this->_fixtureBaseDir . '/' . $fixture; } @@ -135,6 +138,44 @@ protected function _getFixtures(\PHPUnit\Framework\TestCase $test, $scope = null } /** + * Check is the Annotation like Magento_InventoryApi::Test/_files/products.php + * + * @param string $fixture + * @return bool + */ + private function isModuleAnnotation(string $fixture) + { + return (strpos($fixture, '::') !== false); + } + + /** + * Resolve the Fixture + * + * @param string $fixture + * @return string + * @throws \Magento\Framework\Exception\LocalizedException + * @SuppressWarnings(PHPMD.StaticAccess) + */ + private function getModulePath(string $fixture) + { + [$moduleName, $fixtureFile] = explode('::', $fixture, 2); + + $modulePath = (new ComponentRegistrar())->getPath(ComponentRegistrar::MODULE, $moduleName); + + if ($modulePath === null) { + throw new \Magento\Framework\Exception\LocalizedException( + new \Magento\Framework\Phrase('Can\'t find registered Module with name %1 .', [$moduleName]) + ); + } + + return $modulePath . '/' . ltrim($fixtureFile, '/'); + } + + /** + * Get method annotations. + * + * Overwrites class-defined annotations. + * * @param \PHPUnit\Framework\TestCase $test * @return array */ diff --git a/dev/tests/integration/framework/Magento/TestFramework/Annotation/DataFixtureBeforeTransaction.php b/dev/tests/integration/framework/Magento/TestFramework/Annotation/DataFixtureBeforeTransaction.php index db7f57362d80..ba23ecd13b6a 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Annotation/DataFixtureBeforeTransaction.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Annotation/DataFixtureBeforeTransaction.php @@ -4,13 +4,14 @@ * See COPYING.txt for license details. */ -/** - * Implementation of the @magentoDataFixture DocBlock annotation - */ namespace Magento\TestFramework\Annotation; +use Magento\Framework\Component\ComponentRegistrar; use PHPUnit\Framework\Exception; +/** + * Implementation of the @magentoDataFixtureBeforeTransaction DocBlock annotation + */ class DataFixtureBeforeTransaction { /** @@ -93,6 +94,8 @@ protected function _getFixtures(\PHPUnit\Framework\TestCase $test, $scope = null $fixtureMethod = [get_class($test), $fixture]; if (is_callable($fixtureMethod)) { $result[] = $fixtureMethod; + } elseif ($this->isModuleAnnotation($fixture)) { + $result[] = $this->getModulePath($fixture); } else { $result[] = $this->_fixtureBaseDir . '/' . $fixture; } @@ -102,6 +105,42 @@ protected function _getFixtures(\PHPUnit\Framework\TestCase $test, $scope = null } /** + * Check is the Annotation like Magento_InventoryApi::Test/_files/products.php + * + * @param string $fixture + * @return bool + */ + private function isModuleAnnotation(string $fixture) + { + return (strpos($fixture, '::') !== false); + } + + /** + * Resolve the Fixture + * + * @param string $fixture + * @return string + * @throws \Magento\Framework\Exception\LocalizedException + * @SuppressWarnings(PHPMD.StaticAccess) + */ + private function getModulePath(string $fixture) + { + [$moduleName, $fixtureFile] = explode('::', $fixture, 2); + + $modulePath = (new ComponentRegistrar())->getPath(ComponentRegistrar::MODULE, $moduleName); + + if ($modulePath === null) { + throw new \Magento\Framework\Exception\LocalizedException( + new \Magento\Framework\Phrase('Can\'t find registered Module with name %1 .', [$moduleName]) + ); + } + + return $modulePath . '/' . ltrim($fixtureFile, '/'); + } + + /** + * Get annotations for test. + * * @param \PHPUnit\Framework\TestCase $test * @return array */ diff --git a/dev/tests/integration/framework/Magento/TestFramework/Application.php b/dev/tests/integration/framework/Magento/TestFramework/Application.php index 4fec36633c9b..1bfc928f2916 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Application.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Application.php @@ -498,6 +498,7 @@ public function install($cleanup) $this->_ensureDirExists($this->_initParams[$dirs][DirectoryList::VAR_DIR][DirectoryList::PATH]); $this->copyAppConfigFiles(); + $this->copyGlobalConfigFile(); $installParams = $this->getInstallCliParams(); @@ -557,6 +558,17 @@ private function copyAppConfigFiles() } } } + + /** + * Copies global configuration file from the tests folder (see TESTS_GLOBAL_CONFIG_FILE) + * + * @return void + */ + private function copyGlobalConfigFile() + { + $targetFile = $this->_configDir . '/config.local.php'; + copy($this->globalConfigFile, $targetFile); + } /** * Gets a list of CLI params for installation diff --git a/dev/tests/integration/framework/Magento/TestFramework/Helper/Amqp.php b/dev/tests/integration/framework/Magento/TestFramework/Helper/Amqp.php index 992b980d6a80..aa0c790eeac8 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Helper/Amqp.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Helper/Amqp.php @@ -13,26 +13,62 @@ */ class Amqp { + const CONFIG_PATH_HOST = 'queue/amqp/host'; + const CONFIG_PATH_USER = 'queue/amqp/user'; + const CONFIG_PATH_PASSWORD = 'queue/amqp/password'; + const DEFAULT_MANAGEMENT_PORT = '15672'; + /** * @var Curl */ private $curl; + /** + * @var \Magento\Framework\App\DeploymentConfig + */ + private $deploymentConfig; + /** * RabbitMQ API host * * @var string */ - private $host = 'http://localhost:15672/api/'; + private $host; /** * Initialize dependencies. + * @param \Magento\Framework\App\DeploymentConfig $deploymentConfig */ - public function __construct() - { + public function __construct( + \Magento\Framework\App\DeploymentConfig $deploymentConfig = null + ) { + $this->deploymentConfig = $deploymentConfig ?? \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Framework\App\DeploymentConfig::class); $this->curl = new Curl(); - $this->curl->setCredentials('guest', 'guest'); + $this->curl->setCredentials( + $this->deploymentConfig->get(self::CONFIG_PATH_USER), + $this->deploymentConfig->get(self::CONFIG_PATH_PASSWORD) + ); $this->curl->addHeader('content-type', 'application/json'); + $this->host = sprintf( + 'http://%s:%s/api/', + $this->deploymentConfig->get(self::CONFIG_PATH_HOST), + defined('RABBITMQ_MANAGEMENT_PORT') ? RABBITMQ_MANAGEMENT_PORT : self::DEFAULT_MANAGEMENT_PORT + ); + } + + /** + * Check that the RabbitMQ instance has the management plugin installed and the api is available. + * + * @return bool + */ + public function isAvailable(): bool + { + $this->curl->get($this->host . 'overview'); + $data = $this->curl->getBody(); + $data = json_decode($data, true); + + return isset($data['management_version']); } /** @@ -55,6 +91,7 @@ public function getExchanges() /** * Get declared exchange bindings. * + * @param string $name * @return array */ public function getExchangeBindings($name) @@ -82,6 +119,8 @@ public function getConnections() } /** + * Clear Queue + * * @param string $name * @param int $numMessages * @return string @@ -101,7 +140,7 @@ public function clearQueue(string $name, int $numMessages = 50) /** * Delete connection * - * @param $name + * @param string $name * @return string $data */ public function deleteConnection($name) diff --git a/dev/tests/integration/framework/Magento/TestFramework/Lock/Backend/DummyLocker.php b/dev/tests/integration/framework/Magento/TestFramework/Lock/Backend/DummyLocker.php new file mode 100644 index 000000000000..41125493643e --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Lock/Backend/DummyLocker.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Lock\Backend; + +use Magento\Framework\Lock\LockManagerInterface; + +/** + * Dummy locker for the integration framework. + */ +class DummyLocker implements LockManagerInterface +{ + /** + * @inheritdoc + */ + public function lock(string $name, int $timeout = -1): bool + { + return true; + } + + /** + * @inheritdoc + */ + public function unlock(string $name): bool + { + return true; + } + + /** + * @inheritdoc + */ + public function isLocked(string $name): bool + { + return false; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/MessageQueue/PublisherConsumerController.php b/dev/tests/integration/framework/Magento/TestFramework/MessageQueue/PublisherConsumerController.php index 14847ae50662..32240e68ae73 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/MessageQueue/PublisherConsumerController.php +++ b/dev/tests/integration/framework/Magento/TestFramework/MessageQueue/PublisherConsumerController.php @@ -12,6 +12,9 @@ use Magento\Framework\OsInfo; use Magento\TestFramework\Helper\Amqp; +/** + * Publisher Consumer Controller + */ class PublisherConsumerController { /** @@ -49,6 +52,16 @@ class PublisherConsumerController */ private $amqpHelper; + /** + * PublisherConsumerController constructor. + * @param PublisherInterface $publisher + * @param OsInfo $osInfo + * @param Amqp $amqpHelper + * @param string $logFilePath + * @param array $consumers + * @param array $appInitParams + * @param null|int $maxMessages + */ public function __construct( PublisherInterface $publisher, OsInfo $osInfo, @@ -75,27 +88,16 @@ public function __construct( */ public function initialize() { - if ($this->osInfo->isWindows()) { - throw new EnvironmentPreconditionException( - "This test relies on *nix shell and should be skipped in Windows environment." - ); - } + $this->validateEnvironmentPreconditions(); + $connections = $this->amqpHelper->getConnections(); foreach (array_keys($connections) as $connectionName) { $this->amqpHelper->deleteConnection($connectionName); } $this->amqpHelper->clearQueue("async.operations.all"); - foreach ($this->consumers as $consumer) { - foreach ($this->getConsumerProcessIds($consumer) as $consumerProcessId) { - exec("kill {$consumerProcessId}"); - } - } - foreach ($this->consumers as $consumer) { - if (!$this->getConsumerProcessIds($consumer)) { - exec("{$this->getConsumerStartCommand($consumer, true)} > /dev/null &"); - } - sleep(5); - } + + $this->stopConsumers(); + $this->startConsumers(); if (file_exists($this->logFilePath)) { // try to remove before failing the test @@ -108,6 +110,27 @@ public function initialize() } } + /** + * Validate environment preconditions + * + * @throws EnvironmentPreconditionException + * @throws PreconditionFailedException + */ + private function validateEnvironmentPreconditions() + { + if ($this->osInfo->isWindows()) { + throw new EnvironmentPreconditionException( + "This test relies on *nix shell and should be skipped in Windows environment." + ); + } + + if (!$this->amqpHelper->isAvailable()) { + throw new PreconditionFailedException( + 'This test relies on RabbitMQ Management Plugin.' + ); + } + } + /** * Stop Consumers */ @@ -121,6 +144,8 @@ public function stopConsumers() } /** + * Get Consumers ProcessIds + * * @return array */ public function getConsumersProcessIds() @@ -133,6 +158,8 @@ public function getConsumersProcessIds() } /** + * Get Consumer ProcessIds + * * @param string $consumer * @return string[] */ @@ -167,8 +194,10 @@ private function getConsumerStartCommand($consumer, $withEnvVariables = false) } /** + * Wait for asynchronous result + * * @param callable $condition - * @param $params + * @param array $params * @throws PreconditionFailedException */ public function waitForAsynchronousResult(callable $condition, $params) @@ -185,10 +214,27 @@ public function waitForAsynchronousResult(callable $condition, $params) } /** + * Get publisher + * * @return PublisherInterface */ public function getPublisher() { return $this->publisher; } + + /** + * Start consumers + * + * @return void + */ + public function startConsumers(): void + { + foreach ($this->consumers as $consumer) { + if (!$this->getConsumerProcessIds($consumer)) { + exec("{$this->getConsumerStartCommand($consumer, true)} > /dev/null &"); + } + sleep(5); + } + } } diff --git a/dev/tests/integration/framework/Magento/TestFramework/Session/SessionStartChecker.php b/dev/tests/integration/framework/Magento/TestFramework/Session/SessionStartChecker.php new file mode 100644 index 000000000000..136b0565a729 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Session/SessionStartChecker.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\TestFramework\Session; + +/** + * Class to check if session can be started or not. Dummy for integration tests. + */ +class SessionStartChecker extends \Magento\Framework\Session\SessionStartChecker +{ + /** + * Can session be started or not. + * + * @return bool + */ + public function check() : bool + { + return true; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php b/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php index 543bac2c6b5b..7a387bd41eec 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php +++ b/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php @@ -6,9 +6,9 @@ namespace Magento\TestFramework\TestCase; /** - * A parent class for backend controllers - contains directives for admin user creation and authentication + * A parent class for backend controllers - contains directives for admin user creation and authentication. + * * @SuppressWarnings(PHPMD.NumberOfChildren) - * @SuppressWarnings(PHPMD.numberOfChildren) */ abstract class AbstractBackendController extends \Magento\TestFramework\TestCase\AbstractController { @@ -36,6 +36,16 @@ abstract class AbstractBackendController extends \Magento\TestFramework\TestCase */ protected $uri = null; + /** + * @var string|null + */ + protected $httpMethod; + + /** + * @inheritDoc + * + * @throws \Magento\Framework\Exception\AuthenticationException + */ protected function setUp() { parent::setUp(); @@ -62,6 +72,9 @@ protected function _getAdminCredentials() ]; } + /** + * @inheritDoc + */ protected function tearDown() { $this->_auth->getAuthStorage()->destroy(['send_expire_cookie' => false]); @@ -86,21 +99,33 @@ public function assertSessionMessages( parent::assertSessionMessages($constraint, $messageType, $messageManagerClass); } + /** + * Test ACL configuration for action working. + */ public function testAclHasAccess() { if ($this->uri === null) { $this->markTestIncomplete('AclHasAccess test is not complete'); } + if ($this->httpMethod) { + $this->getRequest()->setMethod($this->httpMethod); + } $this->dispatch($this->uri); $this->assertNotSame(403, $this->getResponse()->getHttpResponseCode()); $this->assertNotSame(404, $this->getResponse()->getHttpResponseCode()); } + /** + * Test ACL actually denying access. + */ public function testAclNoAccess() { - if ($this->resource === null) { + if ($this->resource === null || $this->uri === null) { $this->markTestIncomplete('Acl test is not complete'); } + if ($this->httpMethod) { + $this->getRequest()->setMethod($this->httpMethod); + } $this->_objectManager->get(\Magento\Framework\Acl\Builder::class) ->getAcl() ->deny(null, $this->resource); diff --git a/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractController.php b/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractController.php index 1bf6d471fc40..feb9eca0793a 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractController.php +++ b/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractController.php @@ -10,10 +10,12 @@ namespace Magento\TestFramework\TestCase; use Magento\Framework\Data\Form\FormKey; +use Magento\Framework\Message\MessageInterface; use Magento\Framework\Stdlib\CookieManagerInterface; use Magento\Framework\View\Element\Message\InterpretationStrategyInterface; use Magento\Theme\Controller\Result\MessagePlugin; use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\App\Response\Http as HttpResponse; /** * @SuppressWarnings(PHPMD.NumberOfChildren) @@ -70,6 +72,9 @@ protected function setUp() $this->_objectManager->removeSharedInstance(\Magento\Framework\App\RequestInterface::class); } + /** + * @inheritDoc + */ protected function tearDown() { $this->_request = null; @@ -114,7 +119,7 @@ public function dispatch($uri) /** * Request getter * - * @return \Magento\Framework\App\RequestInterface + * @return \Magento\Framework\App\RequestInterface|HttpRequest */ public function getRequest() { @@ -127,7 +132,7 @@ public function getRequest() /** * Response getter * - * @return \Magento\Framework\App\ResponseInterface + * @return \Magento\Framework\App\ResponseInterface|HttpResponse */ public function getResponse() { @@ -212,13 +217,21 @@ public function assertSessionMessages( $messageManagerClass = \Magento\Framework\Message\Manager::class ) { $this->_assertSessionErrors = false; - + /** @var MessageInterface[]|string[] $messageObjects */ $messages = $this->getMessages($messageType, $messageManagerClass); + /** @var string[] $messages */ + $messagesFiltered = array_map( + function ($message) { + /** @var MessageInterface|string $message */ + return ($message instanceof MessageInterface) ? $message->toString() : $message; + }, + $messages + ); $this->assertThat( - $messages, + $messagesFiltered, $constraint, - 'Session messages do not meet expectations ' . var_export($messages, true) + 'Session messages do not meet expectations ' . var_export($messagesFiltered, true) ); } diff --git a/dev/tests/integration/framework/tests/unit/phpunit.xml.dist b/dev/tests/integration/framework/tests/unit/phpunit.xml.dist index b52ca37987cb..1a93397caaa4 100644 --- a/dev/tests/integration/framework/tests/unit/phpunit.xml.dist +++ b/dev/tests/integration/framework/tests/unit/phpunit.xml.dist @@ -21,4 +21,32 @@ <php> <ini name="date.timezone" value="America/Los_Angeles"/> </php> + <listeners> + <listener class="Yandex\Allure\Adapter\AllureAdapter"> + <arguments> + <string>var/allure-results</string> <!-- XML files output directory --> + <boolean>true</boolean> <!-- Whether to delete previous results on rerun --> + <array> <!-- A list of custom annotations to ignore (optional) --> + <element key="magentoAdminConfigFixture"> + <string>magentoAdminConfigFixture</string> + </element> + <element key="magentoAppIsolation"> + <string>magentoAppIsolation</string> + </element> + <element key="magentoComponentsDir"> + <string>magentoComponentsDir</string> + </element> + <element key="magentoConfigFixture"> + <string>magentoConfigFixture</string> + </element> + <element key="@magentoDbIsolation"> + <string>magentoDataFixture</string> + </element> + <element key="magentoDbIsolation"> + <string>magentoDbIsolation</string> + </element> + </array> + </arguments> + </listener> + </listeners> </phpunit> diff --git a/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/Annotation/DataFixtureTest.php b/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/Annotation/DataFixtureTest.php index 22240ad8e1fe..00af4419e114 100644 --- a/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/Annotation/DataFixtureTest.php +++ b/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/Annotation/DataFixtureTest.php @@ -5,6 +5,8 @@ */ namespace Magento\Test\Annotation; +use Magento\Framework\Component\ComponentRegistrar; + /** * Test class for \Magento\TestFramework\Annotation\DataFixture. * @@ -178,4 +180,16 @@ public function testRollbackTransactionRevertFixtureFile() ); $this->_object->rollbackTransaction(); } + + /** + * @magentoDataFixture Foo_DataFixtureDummy::Test/Integration/foo.php + * @SuppressWarnings(PHPMD.StaticAccess) + */ + public function testModuleDataFixture() + { + ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Foo_DataFixtureDummy', __DIR__); + $this->_object->expects($this->once())->method('_applyOneFixture') + ->with(__DIR__ . '/Test/Integration/foo.php'); + $this->_object->startTransaction($this); + } } diff --git a/dev/tests/integration/phpunit.xml.dist b/dev/tests/integration/phpunit.xml.dist index dedc144c5679..815abde6ac26 100644 --- a/dev/tests/integration/phpunit.xml.dist +++ b/dev/tests/integration/phpunit.xml.dist @@ -72,10 +72,59 @@ <!-- Connection parameters for MongoDB library tests --> <!--<const name="MONGODB_CONNECTION_STRING" value="mongodb://localhost:27017"/>--> <!--<const name="MONGODB_DATABASE_NAME" value="magento_integration_tests"/>--> + <!-- Connection parameters for RabbitMQ tests --> + <!--<const name="RABBITMQ_MANAGEMENT_PORT" value="15672"/>--> </php> <!-- Test listeners --> <listeners> <listener class="Magento\TestFramework\Event\PhpUnit"/> <listener class="Magento\TestFramework\ErrorLog\Listener"/> + <listener class="Yandex\Allure\Adapter\AllureAdapter"> + <arguments> + <string>var/allure-results</string> <!-- XML files output directory --> + <boolean>true</boolean> <!-- Whether to delete previous results on rerun --> + <array> <!-- A list of custom annotations to ignore (optional) --> + <element key="codingStandardsIgnoreStart"> + <string>codingStandardsIgnoreStart</string> + </element> + <element key="codingStandardsIgnoreEnd"> + <string>codingStandardsIgnoreEnd</string> + </element> + <element key="expectedExceptionMessageRegExp"> + <string>expectedExceptionMessageRegExp</string> + </element> + <element key="magentoAdminConfigFixture"> + <string>magentoAdminConfigFixture</string> + </element> + <element key="magentoAppArea"> + <string>magentoAppArea</string> + </element> + <element key="magentoAppIsolation"> + <string>magentoAppIsolation</string> + </element> + <element key="magentoCache"> + <string>magentoCache</string> + </element> + <element key="magentoComponentsDir"> + <string>magentoComponentsDir</string> + </element> + <element key="magentoConfigFixture"> + <string>magentoConfigFixture</string> + </element> + <element key="magentoDataFixture"> + <string>magentoDataFixture</string> + </element> + <element key="magentoDataFixtureBeforeTransaction"> + <string>magentoDataFixtureBeforeTransaction</string> + </element> + <element key="magentoDbIsolation"> + <string>magentoDbIsolation</string> + </element> + <element key="magentoIndexerDimensionMode"> + <string>magentoIndexerDimensionMode</string> + </element> + </array> + </arguments> + </listener> </listeners> </phpunit> diff --git a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Model/MassScheduleTest.php b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Model/MassScheduleTest.php index 305c3550269d..c0cc1763b265 100644 --- a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Model/MassScheduleTest.php +++ b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Model/MassScheduleTest.php @@ -128,7 +128,10 @@ public function sendBulk($products) } $this->clearProducts(); - $result = $this->massSchedule->publishMass('async.V1.products.POST', $products); + $result = $this->massSchedule->publishMass( + 'async.magento.catalog.api.productrepositoryinterface.save.post', + $products + ); //assert bulk accepted with no errors $this->assertFalse($result->isErrors()); @@ -206,7 +209,7 @@ public function testScheduleMassOneEntityFailure($products) $expectedErrorMessage = "Data item corresponding to \"product\" " . "must be specified in the message with topic " . - "\"async.V1.products.POST\"."; + "\"async.magento.catalog.api.productrepositoryinterface.save.post\"."; $this->assertEquals( $expectedErrorMessage, $reasonException->getMessage() diff --git a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/_files/bulk.php b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/_files/bulk.php index c6c58d47e5af..9e215667903d 100644 --- a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/_files/bulk.php +++ b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/_files/bulk.php @@ -18,30 +18,35 @@ 'not_started' => [ 'uuid' => 'bulk-uuid-1', 'user_id' => 1, + 'user_type' => \Magento\Authorization\Model\UserContextInterface::USER_TYPE_ADMIN, 'description' => 'Bulk Description', 'operation_count' => 1, ], 'in_progress_success' => [ 'uuid' => 'bulk-uuid-2', 'user_id' => 1, + 'user_type' => \Magento\Authorization\Model\UserContextInterface::USER_TYPE_ADMIN, 'description' => 'Bulk Description', 'operation_count' => 3, ], 'in_progress_failed' => [ 'uuid' => 'bulk-uuid-3', 'user_id' => 1, + 'user_type' => \Magento\Authorization\Model\UserContextInterface::USER_TYPE_ADMIN, 'description' => 'Bulk Description', 'operation_count' => 2, ], 'finish_success' => [ 'uuid' => 'bulk-uuid-4', 'user_id' => 1, + 'user_type' => \Magento\Authorization\Model\UserContextInterface::USER_TYPE_ADMIN, 'description' => 'Bulk Description', 'operation_count' => 1, ], 'finish_failed' => [ 'uuid' => 'bulk-uuid-5', 'user_id' => 1, + 'user_type' => \Magento\Authorization\Model\UserContextInterface::USER_TYPE_ADMIN, 'description' => 'Bulk Description', 'operation_count' => 2, ], @@ -90,8 +95,8 @@ ], ]; -$bulkQuery = "INSERT INTO {$bulkTable} (`uuid`, `user_id`, `description`, `operation_count`)" - . " VALUES (:uuid, :user_id, :description, :operation_count);"; +$bulkQuery = "INSERT INTO {$bulkTable} (`uuid`, `user_id`, `user_type`, `description`, `operation_count`)" + . " VALUES (:uuid, :user_id, :user_type, :description, :operation_count);"; foreach ($bulks as $bulk) { $connection->query($bulkQuery, $bulk); } diff --git a/dev/tests/integration/testsuite/Magento/Authorizenet/Controller/Directpost/Payment/BackendResponseTest.php b/dev/tests/integration/testsuite/Magento/Authorizenet/Controller/Directpost/Payment/BackendResponseTest.php index 28d4395e7e41..7ab55dc7fd92 100644 --- a/dev/tests/integration/testsuite/Magento/Authorizenet/Controller/Directpost/Payment/BackendResponseTest.php +++ b/dev/tests/integration/testsuite/Magento/Authorizenet/Controller/Directpost/Payment/BackendResponseTest.php @@ -23,7 +23,7 @@ public function testUnauthorizedRequest() $data = [ 'x_response_code' => 1, 'x_response_reason_code' => 1, - 'x_invoice_num' => 1, + 'x_invoice_num' => '1', 'x_amount' => 16, 'x_trans_id' => '32iiw5ve', 'x_card_type' => 'American Express', @@ -48,7 +48,7 @@ public function testSuccess() $data = [ 'x_response_code' => 1, 'x_response_reason_code' => 1, - 'x_invoice_num' => 1, + 'x_invoice_num' => '1', 'x_amount' => 16, 'x_trans_id' => '32iiw5ve', 'x_card_type' => 'American Express', diff --git a/dev/tests/integration/testsuite/Magento/Authorizenet/Model/DirectpostTest.php b/dev/tests/integration/testsuite/Magento/Authorizenet/Model/DirectpostTest.php index 71105cd844c2..ba4c4efd78f1 100644 --- a/dev/tests/integration/testsuite/Magento/Authorizenet/Model/DirectpostTest.php +++ b/dev/tests/integration/testsuite/Magento/Authorizenet/Model/DirectpostTest.php @@ -19,6 +19,8 @@ /** * Class contains tests for Direct Post integration + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class DirectpostTest extends \PHPUnit\Framework\TestCase { @@ -141,7 +143,7 @@ public function fdsFilterActionDataProvider() [ 'filter_action' => 'report', 'order_id' => '100000004', - 'expected_order_state' => Order::STATE_PROCESSING + 'expected_order_state' => Order::STATE_COMPLETE ], ]; } diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/full_order_with_capture.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/full_order_with_capture.php new file mode 100644 index 000000000000..4ef5a4dd14c0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/full_order_with_capture.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\AuthorizenetAcceptjs\Gateway\Config; +use Magento\Sales\Model\Order\Payment; +use Magento\Sales\Model\OrderRepository; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Sales\Api\TransactionRepositoryInterface; +use Magento\Sales\Model\Order\Payment\Transaction; +use Magento\Sales\Model\Order\Payment\Transaction\BuilderInterface as TransactionBuilder; + +$order = include __DIR__ . '/../_files/full_order.php'; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var Payment $payment */ +$payment = $order->getPayment(); +$payment->setMethod(Config::METHOD); +$payment->setAuthorizationTransaction(false); +$payment->setParentTransactionId(4321); + + +/** @var OrderRepository $orderRepo */ +$orderRepo = $objectManager->get(OrderRepository::class); +$orderRepo->save($order); + +/** @var TransactionBuilder $transactionBuilder */ +$transactionBuilder = $objectManager->create(TransactionBuilder::class); +$transactionAuthorize = $transactionBuilder->setPayment($payment) + ->setOrder($order) + ->setTransactionId(1234) + ->build(Transaction::TYPE_AUTH); +$transactionCapture = $transactionBuilder->setPayment($payment) + ->setOrder($order) + ->setTransactionId(4321) + ->build(Transaction::TYPE_CAPTURE); + +$transactionRepository = $objectManager->create(TransactionRepositoryInterface::class); +$transactionRepository->save($transactionAuthorize); +$transactionRepository->save($transactionCapture); diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/full_order_with_capture_rollback.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/full_order_with_capture_rollback.php new file mode 100644 index 000000000000..1a2cb2532fe5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/full_order_with_capture_rollback.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\TestFramework\ObjectManager; + +$objectManager = ObjectManager::getInstance(); + +/** @var SearchCriteriaBuilder $searchCriteriaBuilder */ +$searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); +$searchCriteria = $searchCriteriaBuilder->addFilter('increment_id', '100000001') + ->create(); + +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->get(OrderRepositoryInterface::class); +$items = $orderRepository->getList($searchCriteria) + ->getItems(); + +foreach ($items as $item) { + $orderRepository->delete($item); +} + +require __DIR__ . '/../../../Magento/Catalog/_files/product_simple_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/order_auth_only.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/order_auth_only.php new file mode 100644 index 000000000000..b1d0521c9c61 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/order_auth_only.php @@ -0,0 +1,74 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Address; +use Magento\Sales\Model\Order\Item as OrderItem; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use Magento\Sales\Api\TransactionRepositoryInterface; +use Magento\Sales\Model\Order\Payment\Transaction; +use Magento\Sales\Model\Order\Payment\Transaction\BuilderInterface as TransactionBuilder; + +/** @var ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +$addressData = include __DIR__ . '/../../Sales/_files/address_data.php'; +require __DIR__ . '/../../../Magento/Catalog/_files/product_simple.php'; + +$billingAddress = $objectManager->create(Address::class, ['data' => $addressData]); +$billingAddress->setAddressType('billing'); + +$shippingAddress = clone $billingAddress; +$shippingAddress->setId(null) + ->setAddressType('shipping'); + +/** @var OrderItem $orderItem */ +$orderItem = $objectManager->create(OrderItem::class); +$orderItem->setProductId($product->getId()) + ->setQtyOrdered(2) + ->setBasePrice($product->getPrice()) + ->setPrice($product->getPrice()) + ->setRowTotal($product->getPrice()) + ->setProductType('simple'); + +require __DIR__ . '/payment.php'; + +$order = $objectManager->create(Order::class); +$order->setIncrementId('100000002') + ->setSubtotal($product->getPrice() * 2) + ->setBaseSubtotal($product->getPrice() * 2) + ->setCustomerEmail('admin@example.com') + ->setCustomerIsGuest(true) + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->setStoreId( + $objectManager->get(StoreManagerInterface::class)->getStore() + ->getId() + ) + ->addItem($orderItem) + ->setPayment($payment); + +$payment->setParentTransactionId(1234); + +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->get(OrderRepositoryInterface::class); +$orderRepository->save($order); + +/** @var TransactionBuilder $transactionBuilder */ +$transactionBuilder = $objectManager->create(TransactionBuilder::class); +$transactionAuthorize = $transactionBuilder->setPayment($payment) + ->setOrder($order) + ->setTransactionId(1234) + ->build(Transaction::TYPE_AUTH); + +$transactionAuthorize->setAdditionalInformation('real_transaction_id', '1234'); + +$transactionRepository = $objectManager->create(TransactionRepositoryInterface::class); +$transactionRepository->save($transactionAuthorize); diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/order_auth_only_rollback.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/order_auth_only_rollback.php new file mode 100644 index 000000000000..5a65a1fc0d0c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/order_auth_only_rollback.php @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +require __DIR__ . '/order_captured_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/order_captured.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/order_captured.php new file mode 100644 index 000000000000..9bfc863df7de --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/order_captured.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Address; +use Magento\Sales\Model\Order\Item as OrderItem; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use Magento\Sales\Api\TransactionRepositoryInterface; +use Magento\Sales\Model\Order\Payment\Transaction; +use Magento\Sales\Model\Order\Payment\Transaction\BuilderInterface as TransactionBuilder; + +/** @var ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +$addressData = include __DIR__ . '/../../Sales/_files/address_data.php'; +require __DIR__ . '/../../../Magento/Catalog/_files/product_simple.php'; + +$billingAddress = $objectManager->create(Address::class, ['data' => $addressData]); +$billingAddress->setAddressType('billing'); + +$shippingAddress = clone $billingAddress; +$shippingAddress->setId(null) + ->setAddressType('shipping'); + +/** @var OrderItem $orderItem */ +$orderItem = $objectManager->create(OrderItem::class); +$orderItem->setProductId($product->getId()) + ->setQtyOrdered(2) + ->setBasePrice($product->getPrice()) + ->setPrice($product->getPrice()) + ->setRowTotal($product->getPrice()) + ->setProductType('simple'); + +require __DIR__ . '/payment.php'; + +$order = $objectManager->create(Order::class); +$order->setIncrementId('100000002') + ->setSubtotal($product->getPrice() * 2) + ->setBaseSubtotal($product->getPrice() * 2) + ->setCustomerEmail('admin@example.com') + ->setCustomerIsGuest(true) + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->setStoreId( + $objectManager->get(StoreManagerInterface::class)->getStore() + ->getId() + ) + ->addItem($orderItem) + ->setPayment($payment); + +$payment->setParentTransactionId(4321); + +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->get(OrderRepositoryInterface::class); +$orderRepository->save($order); + +/** @var TransactionBuilder $transactionBuilder */ +$transactionBuilder = $objectManager->create(TransactionBuilder::class); +$transactionAuthorize = $transactionBuilder->setPayment($payment) + ->setOrder($order) + ->setTransactionId(1234) + ->build(Transaction::TYPE_AUTH); +$transactionCapture = $transactionBuilder->setPayment($payment) + ->setOrder($order) + ->setTransactionId(4321) + ->build(Transaction::TYPE_CAPTURE); + +$transactionRepository = $objectManager->create(TransactionRepositoryInterface::class); +$transactionRepository->save($transactionAuthorize); +$transactionRepository->save($transactionCapture); diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/order_captured_rollback.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/order_captured_rollback.php new file mode 100644 index 000000000000..a2da0b639e98 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/order_captured_rollback.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\TestFramework\ObjectManager; + +$objectManager = ObjectManager::getInstance(); + +/** @var SearchCriteriaBuilder $searchCriteriaBuilder */ +$searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); +$searchCriteria = $searchCriteriaBuilder->addFilter('increment_id', '100000002') + ->create(); + +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->get(OrderRepositoryInterface::class); +$items = $orderRepository->getList($searchCriteria) + ->getItems(); + +foreach ($items as $item) { + $orderRepository->delete($item); +} + +require __DIR__ . '/../../../Magento/Catalog/_files/product_simple_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/payment.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/payment.php new file mode 100644 index 000000000000..5b15e356a7d8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Fixture/payment.php @@ -0,0 +1,19 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\AuthorizenetAcceptjs\Gateway\Config; +use Magento\Sales\Model\Order\Payment; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; + +/** @var ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +/** @var Payment $payment */ +$payment = $objectManager->create(Payment::class); +$payment->setMethod(Config::METHOD); +$payment->setAuthorizationTransaction(true); diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/AbstractTest.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/AbstractTest.php new file mode 100644 index 000000000000..f1458a19012f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/AbstractTest.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway; + +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\Area; +use Magento\Framework\HTTP\ZendClient; +use Magento\Framework\HTTP\ZendClientFactory; +use PHPUnit\Framework\MockObject\Builder\InvocationMocker; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use Magento\Payment\Gateway\Data\PaymentDataObjectFactory; +use Magento\Quote\Model\Quote\PaymentFactory; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order; +use Zend_Http_Response; + +abstract class AbstractTest extends TestCase +{ + /** + * @var ObjectManager + */ + protected $objectManager; + + /** + * @var ZendClient|MockObject|InvocationMocker + */ + protected $clientMock; + + /** + * @var PaymentFactory + */ + protected $paymentFactory; + + /** + * @var Zend_Http_Response + */ + protected $responseMock; + + /** + * @throws \Magento\Framework\Exception\LocalizedException + */ + protected function setUp() + { + $bootstrap = Bootstrap::getInstance(); + $bootstrap->loadArea(Area::AREA_FRONTEND); + $this->objectManager = Bootstrap::getObjectManager(); + $this->clientMock = $this->createMock(ZendClient::class); + $this->responseMock = $this->createMock(Zend_Http_Response::class); + $this->clientMock->method('request') + ->willReturn($this->responseMock); + $this->clientMock->method('setUri') + ->with('https://apitest.authorize.net/xml/v1/request.api'); + $clientFactoryMock = $this->createMock(ZendClientFactory::class); + $clientFactoryMock->method('create') + ->willReturn($this->clientMock); + /** @var PaymentDataObjectFactory $paymentFactory */ + $this->paymentFactory = $this->objectManager->get(PaymentDataObjectFactory::class); + $this->objectManager->addSharedInstance($clientFactoryMock, ZendClientFactory::class); + } + + protected function tearDown() + { + $this->objectManager->removeSharedInstance(ZendClientFactory::class); + parent::tearDown(); + } + + protected function getOrderWithIncrementId(string $incrementId): Order + { + /** @var OrderRepositoryInterface $orderRepository */ + $orderRepository = $this->objectManager->get(OrderRepositoryInterface::class); + $searchCriteria = $this->objectManager->get(SearchCriteriaBuilder::class) + ->addFilter('increment_id', $incrementId) + ->create(); + /** @var Order $order */ + $order = current( + $orderRepository->getList($searchCriteria) + ->getItems() + ); + + return $order; + } +} diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/AcceptFdsCommandTest.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/AcceptFdsCommandTest.php new file mode 100644 index 000000000000..394d9de6684c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/AcceptFdsCommandTest.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Command; + +use Magento\AuthorizenetAcceptjs\Gateway\AbstractTest; +use Magento\Payment\Gateway\Command\CommandPoolInterface; +use Magento\Sales\Model\Order\Payment; + +class AcceptFdsCommandTest extends AbstractTest +{ + /** + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/environment sandbox + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/login someusername + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_key somepassword + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_signature_key abc + * @magentoDataFixture Magento/AuthorizenetAcceptjs/Fixture/order_auth_only.php + */ + public function testAcceptFdsCommand() + { + /** @var CommandPoolInterface $commandPool */ + $commandPool = $this->objectManager->get('AuthorizenetAcceptjsCommandPool'); + $command = $commandPool->get('accept_fds'); + + $order = $this->getOrderWithIncrementId('100000002'); + $payment = $order->getPayment(); + $paymentDO = $this->paymentFactory->create($payment); + + $expectedRequest = include __DIR__ . '/../../_files/expected_request/accept_fds.php'; + $response = include __DIR__ . '/../../_files/response/generic_success.php'; + + $this->clientMock->expects($this->once()) + ->method('setRawData') + ->with(json_encode($expectedRequest), 'application/json'); + + $this->responseMock->expects($this->once()) + ->method('getBody') + ->willReturn(json_encode($response)); + + $command->execute([ + 'payment' => $paymentDO + ]); + } +} diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/AuthorizeCommandTest.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/AuthorizeCommandTest.php new file mode 100644 index 000000000000..9affd80be060 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/AuthorizeCommandTest.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Command; + +use Magento\AuthorizenetAcceptjs\Gateway\AbstractTest; +use Magento\Payment\Gateway\Command\CommandPoolInterface; +use Magento\Sales\Model\Order\Payment; +use Magento\Sales\Model\Order\Payment\Transaction; + +class AuthorizeCommandTest extends AbstractTest +{ + /** + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/environment sandbox + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/login someusername + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_key somepassword + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_signature_key abc + */ + public function testAuthorizeCommand() + { + /** @var CommandPoolInterface $commandPool */ + $commandPool = $this->objectManager->get('AuthorizenetAcceptjsCommandPool'); + $command = $commandPool->get('authorize'); + + $order = include __DIR__ . '/../../_files/full_order.php'; + $payment = $order->getPayment(); + + $paymentDO = $this->paymentFactory->create($payment); + + $expectedRequest = include __DIR__ . '/../../_files/expected_request/authorize.php'; + $response = include __DIR__ . '/../../_files/response/authorize.php'; + + $this->clientMock->method('setRawData') + ->with(json_encode($expectedRequest), 'application/json'); + + $this->responseMock->method('getBody') + ->willReturn(json_encode($response)); + + $command->execute([ + 'payment' => $paymentDO, + 'amount' => 100.00 + ]); + + /** @var Payment $payment */ + $rawDetails = [ + 'authCode' => 'abc123', + 'avsResultCode' => 'Y', + 'cvvResultCode' => 'P', + 'cavvResultCode' => '2', + 'accountType' => 'Visa', + ]; + $this->assertSame('1111', $payment->getCcLast4()); + $this->assertSame('Y', $payment->getCcAvsStatus()); + $this->assertFalse($payment->getData('is_transaction_closed')); + + $transactionDetails = $payment->getTransactionAdditionalInfo(); + foreach ($rawDetails as $key => $value) { + $this->assertSame($value, $payment->getAdditionalInformation($key)); + $this->assertSame($value, $transactionDetails[Transaction::RAW_DETAILS][$key]); + } + + $this->assertSame('123456', $payment->getTransactionId()); + $this->assertSame('123456', $transactionDetails['real_transaction_id']); + } +} diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/CancelCommandTest.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/CancelCommandTest.php new file mode 100644 index 000000000000..aa606a50ae67 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/CancelCommandTest.php @@ -0,0 +1,62 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Command; + +use Magento\AuthorizenetAcceptjs\Gateway\AbstractTest; +use Magento\Payment\Gateway\Command\CommandPoolInterface; +use Magento\Sales\Model\Order\Payment; + +class CancelCommandTest extends AbstractTest +{ + /** + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/environment sandbox + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/login someusername + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_key somepassword + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_signature_key abc + * @magentoDataFixture Magento/AuthorizenetAcceptjs/Fixture/order_auth_only.php + * @dataProvider aliasesProvider + */ + public function testCancelCommand(string $commandName) + { + /** @var CommandPoolInterface $commandPool */ + $commandPool = $this->objectManager->get('AuthorizenetAcceptjsCommandPool'); + $command = $commandPool->get($commandName); + + $order = $this->getOrderWithIncrementId('100000002'); + $payment = $order->getPayment(); + $paymentDO = $this->paymentFactory->create($payment); + + $expectedRequest = include __DIR__ . '/../../_files/expected_request/void.php'; + $response = include __DIR__ . '/../../_files/response/void.php'; + + $this->clientMock->method('setRawData') + ->with(json_encode($expectedRequest), 'application/json'); + + $this->responseMock->method('getBody') + ->willReturn(json_encode($response)); + + $command->execute([ + 'payment' => $paymentDO + ]); + + /** @var Payment $payment */ + + $this->assertTrue($payment->getIsTransactionClosed()); + $this->assertTrue($payment->getShouldCloseParentTransaction()); + $this->assertArrayNotHasKey('real_transaction_id', $payment->getTransactionAdditionalInfo()); + } + + public function aliasesProvider() + { + return [ + ['cancel'], + ['deny_payment'] + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/FetchTransactionInfoCommandTest.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/FetchTransactionInfoCommandTest.php new file mode 100644 index 000000000000..1651dfc7db3d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/FetchTransactionInfoCommandTest.php @@ -0,0 +1,184 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Command; + +use Magento\AuthorizenetAcceptjs\Gateway\AbstractTest; +use Magento\Payment\Gateway\Command\CommandPoolInterface; +use Magento\Sales\Model\Order\Payment; + +class FetchTransactionInfoCommandTest extends AbstractTest +{ + /** + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/environment sandbox + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/login someusername + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_key somepassword + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_signature_key abc + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/transactionSyncKeys transId,transactionType + * @magentoDataFixture Magento/AuthorizenetAcceptjs/Fixture/order_auth_only.php + */ + public function testTransactionApproved() + { + /** @var CommandPoolInterface $commandPool */ + $commandPool = $this->objectManager->get('AuthorizenetAcceptjsCommandPool'); + $command = $commandPool->get('fetch_transaction_information'); + + $order = $this->getOrderWithIncrementId('100000002'); + $payment = $order->getPayment(); + $paymentDO = $this->paymentFactory->create($payment); + + $expectedRequest = include __DIR__ . '/../../_files/expected_request/transaction_details_authorized.php'; + $response = include __DIR__ . '/../../_files/response/transaction_details_authorized.php'; + + $this->clientMock->method('setRawData') + ->with(json_encode($expectedRequest), 'application/json'); + + $this->responseMock->method('getBody') + ->willReturn(json_encode($response)); + + $result = $command->execute([ + 'payment' => $paymentDO + ]); + + $expected = [ + 'transId' => '1234', + 'transactionType' => 'authOnlyTransaction' + ]; + $this->assertSame($expected, $result); + + /** @var Payment $payment */ + $this->assertTrue($payment->getIsTransactionApproved()); + $this->assertFalse($payment->getIsTransactionDenied()); + } + + /** + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/environment sandbox + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/login someusername + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_key somepassword + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_signature_key abc + * * @magentoConfigFixture default_store payment/authorizenet_acceptjs/transactionSyncKeys transId,transactionType + * @magentoDataFixture Magento/AuthorizenetAcceptjs/Fixture/order_auth_only.php + */ + public function testTransactionVoided() + { + /** @var CommandPoolInterface $commandPool */ + $commandPool = $this->objectManager->get('AuthorizenetAcceptjsCommandPool'); + $command = $commandPool->get('fetch_transaction_information'); + + $order = $this->getOrderWithIncrementId('100000002'); + $payment = $order->getPayment(); + $paymentDO = $this->paymentFactory->create($payment); + + $expectedRequest = include __DIR__ . '/../../_files/expected_request/transaction_details_authorized.php'; + $response = include __DIR__ . '/../../_files/response/transaction_details_voided.php'; + + $this->clientMock->method('setRawData') + ->with(json_encode($expectedRequest), 'application/json'); + + $this->responseMock->method('getBody') + ->willReturn(json_encode($response)); + + $result = $command->execute([ + 'payment' => $paymentDO + ]); + + $expected = [ + 'transId' => '1234', + 'transactionType' => 'authOnlyTransaction' + ]; + $this->assertSame($expected, $result); + + /** @var Payment $payment */ + $this->assertFalse($payment->getIsTransactionApproved()); + $this->assertTrue($payment->getIsTransactionDenied()); + } + + /** + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/environment sandbox + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/login someusername + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_key somepassword + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_signature_key abc + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/transactionSyncKeys transId,transactionType + * @magentoDataFixture Magento/AuthorizenetAcceptjs/Fixture/order_auth_only.php + */ + public function testTransactionDenied() + { + /** @var CommandPoolInterface $commandPool */ + $commandPool = $this->objectManager->get('AuthorizenetAcceptjsCommandPool'); + $command = $commandPool->get('fetch_transaction_information'); + + $order = $this->getOrderWithIncrementId('100000002'); + $payment = $order->getPayment(); + $paymentDO = $this->paymentFactory->create($payment); + + $expectedRequest = include __DIR__ . '/../../_files/expected_request/transaction_details_authorized.php'; + $response = include __DIR__ . '/../../_files/response/transaction_details_voided.php'; + + $this->clientMock->method('setRawData') + ->with(json_encode($expectedRequest), 'application/json'); + + $this->responseMock->method('getBody') + ->willReturn(json_encode($response)); + + $result = $command->execute([ + 'payment' => $paymentDO + ]); + + $expected = [ + 'transId' => '1234', + 'transactionType' => 'authOnlyTransaction' + ]; + $this->assertSame($expected, $result); + + /** @var Payment $payment */ + $this->assertFalse($payment->getIsTransactionApproved()); + $this->assertTrue($payment->getIsTransactionDenied()); + } + + /** + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/environment sandbox + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/login someusername + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_key somepassword + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_signature_key abc + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/transactionSyncKeys transId,transactionType + * @magentoDataFixture Magento/AuthorizenetAcceptjs/Fixture/order_auth_only.php + */ + public function testTransactionPending() + { + /** @var CommandPoolInterface $commandPool */ + $commandPool = $this->objectManager->get('AuthorizenetAcceptjsCommandPool'); + $command = $commandPool->get('fetch_transaction_information'); + + $order = $this->getOrderWithIncrementId('100000002'); + $payment = $order->getPayment(); + $paymentDO = $this->paymentFactory->create($payment); + + $expectedRequest = include __DIR__ . '/../../_files/expected_request/transaction_details_authorized.php'; + $response = include __DIR__ . '/../../_files/response/transaction_details_fds_pending.php'; + + $this->clientMock->method('setRawData') + ->with(json_encode($expectedRequest), 'application/json'); + + $this->responseMock->method('getBody') + ->willReturn(json_encode($response)); + + $result = $command->execute([ + 'payment' => $paymentDO + ]); + + $expected = [ + 'transId' => '1234', + 'transactionType' => 'authOnlyTransaction' + ]; + $this->assertSame($expected, $result); + + /** @var Payment $payment */ + $this->assertNull($payment->getIsTransactionApproved()); + $this->assertNull($payment->getIsTransactionDenied()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/RefundSettledCommandTest.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/RefundSettledCommandTest.php new file mode 100644 index 000000000000..6e06d749f390 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/RefundSettledCommandTest.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Command; + +use Magento\AuthorizenetAcceptjs\Gateway\AbstractTest; +use Magento\Payment\Gateway\Command\CommandPoolInterface; +use Magento\Sales\Model\Order\Payment; + +class RefundSettledCommandTest extends AbstractTest +{ + /** + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/environment sandbox + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/login someusername + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_key somepassword + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_signature_key abc + * @magentoDataFixture Magento/AuthorizenetAcceptjs/Fixture/full_order_with_capture.php + */ + public function testRefundSettledCommand() + { + /** @var CommandPoolInterface $commandPool */ + $commandPool = $this->objectManager->get('AuthorizenetAcceptjsCommandPool'); + $command = $commandPool->get('refund_settled'); + + $order = $this->getOrderWithIncrementId('100000001'); + $payment = $order->getPayment(); + + $paymentDO = $this->paymentFactory->create($payment); + + $expectedRequest = include __DIR__ . '/../../_files/expected_request/refund.php'; + $response = include __DIR__ . '/../../_files/response/refund.php'; + + $this->clientMock->method('setRawData') + ->with(json_encode($expectedRequest), 'application/json'); + + $this->responseMock->method('getBody') + ->willReturn(json_encode($response)); + + $command->execute([ + 'payment' => $paymentDO, + 'amount' => 100.00 + ]); + + /** @var Payment $payment */ + $this->assertTrue($payment->getIsTransactionClosed()); + $this->assertSame('5678', $payment->getTransactionId()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/SaleCommandTest.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/SaleCommandTest.php new file mode 100644 index 000000000000..7ae03d36cb75 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/SaleCommandTest.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Command; + +use Magento\AuthorizenetAcceptjs\Gateway\AbstractTest; +use Magento\Payment\Gateway\Command\CommandPoolInterface; +use Magento\Sales\Model\Order\Payment; +use Magento\Sales\Model\Order\Payment\Transaction; + +class SaleCommandTest extends AbstractTest +{ + /** + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/environment sandbox + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/login someusername + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_key somepassword + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_signature_key abc + */ + public function testSaleCommand() + { + /** @var CommandPoolInterface $commandPool */ + $commandPool = $this->objectManager->get('AuthorizenetAcceptjsCommandPool'); + $command = $commandPool->get('sale'); + + $order = include __DIR__ . '/../../_files/full_order.php'; + $payment = $order->getPayment(); + + $paymentDO = $this->paymentFactory->create($payment); + + $expectedRequest = include __DIR__ . '/../../_files/expected_request/sale.php'; + $response = include __DIR__ . '/../../_files/response/sale.php'; + + $this->clientMock->method('setRawData') + ->with(json_encode($expectedRequest), 'application/json'); + + $this->responseMock->method('getBody') + ->willReturn(json_encode($response)); + + $command->execute([ + 'payment' => $paymentDO, + 'amount' => 100.00 + ]); + + /** @var Payment $payment */ + $rawDetails = [ + 'authCode' => 'abc123', + 'avsResultCode' => 'Y', + 'cvvResultCode' => 'P', + 'cavvResultCode' => '2', + 'accountType' => 'Visa', + ]; + $this->assertSame('1111', $payment->getCcLast4()); + $this->assertSame('Y', $payment->getCcAvsStatus()); + + $transactionDetails = $payment->getTransactionAdditionalInfo(); + foreach ($rawDetails as $key => $value) { + $this->assertSame($value, $payment->getAdditionalInformation($key)); + $this->assertSame($value, $transactionDetails[Transaction::RAW_DETAILS][$key]); + } + + $this->assertSame('123456', $payment->getTransactionId()); + $this->assertSame('123456', $transactionDetails['real_transaction_id']); + $this->assertTrue($payment->getShouldCloseParentTransaction()); + $this->assertFalse($payment->getData('is_transaction_closed')); + } +} diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/SettleCommandTest.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/SettleCommandTest.php new file mode 100644 index 000000000000..bb0a259b165b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/SettleCommandTest.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Command; + +use Magento\AuthorizenetAcceptjs\Gateway\AbstractTest; +use Magento\Payment\Gateway\Command\CommandPoolInterface; +use Magento\Sales\Model\Order\Payment; + +class SettleCommandTest extends AbstractTest +{ + /** + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/environment sandbox + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/login someusername + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_key somepassword + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_signature_key abc + * @magentoDataFixture Magento/AuthorizenetAcceptjs/Fixture/order_auth_only.php + */ + public function testRefundSettledCommand() + { + /** @var CommandPoolInterface $commandPool */ + $commandPool = $this->objectManager->get('AuthorizenetAcceptjsCommandPool'); + $command = $commandPool->get('settle'); + + $order = $this->getOrderWithIncrementId('100000002'); + $payment = $order->getPayment(); + + $paymentDO = $this->paymentFactory->create($payment); + + $expectedRequest = include __DIR__ . '/../../_files/expected_request/settle.php'; + $response = include __DIR__ . '/../../_files/response/settle.php'; + + $this->clientMock->method('setRawData') + ->with(json_encode($expectedRequest), 'application/json'); + + $this->responseMock->method('getBody') + ->willReturn(json_encode($response)); + + $command->execute([ + 'payment' => $paymentDO, + 'amount' => 100.00 + ]); + + /** @var Payment $payment */ + $this->assertTrue($payment->getShouldCloseParentTransaction()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/TransactionDetailsCommandTest.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/TransactionDetailsCommandTest.php new file mode 100644 index 000000000000..d81cffc413b5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/TransactionDetailsCommandTest.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Command; + +use Magento\AuthorizenetAcceptjs\Gateway\AbstractTest; +use Magento\Payment\Gateway\Command\CommandPoolInterface; + +class TransactionDetailsCommandTest extends AbstractTest +{ + /** + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/environment sandbox + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/login someusername + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_key somepassword + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_signature_key abc + * @magentoDataFixture Magento/AuthorizenetAcceptjs/Fixture/order_captured.php + */ + public function testTransactionDetails() + { + /** @var CommandPoolInterface $commandPool */ + $commandPool = $this->objectManager->get('AuthorizenetAcceptjsCommandPool'); + $command = $commandPool->get('get_transaction_details'); + + $order = $this->getOrderWithIncrementId('100000002'); + $payment = $order->getPayment(); + $paymentDO = $this->paymentFactory->create($payment); + + $expectedRequest = include __DIR__ . '/../../_files/expected_request/transaction_details.php'; + $response = include __DIR__ . '/../../_files/response/transaction_details_settled_capture.php'; + + $this->clientMock->method('setRawData') + ->with(json_encode($expectedRequest), 'application/json'); + + $this->responseMock->method('getBody') + ->willReturn(json_encode($response)); + + $result = $command->execute([ + 'payment' => $paymentDO + ]); + + $resultData = $result->get(); + + $this->assertEquals($response, $resultData); + } +} diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/VoidCommandTest.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/VoidCommandTest.php new file mode 100644 index 000000000000..f74f8542bfdc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/Command/VoidCommandTest.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway\Command; + +use Magento\AuthorizenetAcceptjs\Gateway\AbstractTest; +use Magento\Payment\Gateway\Command\CommandPoolInterface; +use Magento\Sales\Model\Order\Payment; + +class VoidCommandTest extends AbstractTest +{ + /** + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/environment sandbox + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/login someusername + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_key somepassword + * @magentoConfigFixture default_store payment/authorizenet_acceptjs/trans_signature_key abc + * @magentoDataFixture Magento/AuthorizenetAcceptjs/Fixture/order_auth_only.php + */ + public function testVoidCommand() + { + /** @var CommandPoolInterface $commandPool */ + $commandPool = $this->objectManager->get('AuthorizenetAcceptjsCommandPool'); + $command = $commandPool->get('void'); + + $order = $this->getOrderWithIncrementId('100000002'); + $payment = $order->getPayment(); + $paymentDO = $this->paymentFactory->create($payment); + + $expectedRequest = include __DIR__ . '/../../_files/expected_request/void.php'; + $response = include __DIR__ . '/../../_files/response/void.php'; + + $this->clientMock->method('setRawData') + ->with(json_encode($expectedRequest), 'application/json'); + + $this->responseMock->method('getBody') + ->willReturn(json_encode($response)); + + $command->execute([ + 'payment' => $paymentDO + ]); + + /** @var Payment $payment */ + + $this->assertTrue($payment->getIsTransactionClosed()); + $this->assertTrue($payment->getShouldCloseParentTransaction()); + $this->assertEquals('1234', $payment->getTransactionAdditionalInfo()['real_transaction_id']); + } +} diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/ConfigTest.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/ConfigTest.php new file mode 100644 index 000000000000..a37f92727424 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/Gateway/ConfigTest.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\AuthorizenetAcceptjs\Gateway; + +use Magento\Framework\Config\Data; +use Magento\Payment\Model\Method\Adapter; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use PHPUnit\Framework\TestCase; + +class ConfigTest extends TestCase +{ + /** + * @var ObjectManager + */ + private $objectManager; + + protected function setUp() + { + $this->objectManager = Bootstrap::getObjectManager(); + } + + public function testVerifyConfiguration() + { + /** @var Adapter $paymentAdapter */ + $paymentAdapter = $this->objectManager->get('AuthorizenetAcceptjsFacade'); + + $this->assertEquals('authorizenet_acceptjs', $paymentAdapter->getCode()); + $this->assertTrue($paymentAdapter->canAuthorize()); + $this->assertTrue($paymentAdapter->canCapture()); + $this->assertFalse($paymentAdapter->canCapturePartial()); + $this->assertTrue($paymentAdapter->canRefund()); + $this->assertTrue($paymentAdapter->canUseCheckout()); + $this->assertTrue($paymentAdapter->canVoid()); + $this->assertTrue($paymentAdapter->canUseInternal()); + $this->assertTrue($paymentAdapter->canEdit()); + $this->assertTrue($paymentAdapter->canFetchTransactionInfo()); + + /** @var Data $configReader */ + $configReader = $this->objectManager->get('Magento\Payment\Model\Config\Data'); + $value = $configReader->get('methods/authorizenet_acceptjs/allow_multiple_address'); + + $this->assertSame('0', $value); + } +} diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/accept_fds.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/accept_fds.php new file mode 100644 index 000000000000..d843de1c2cac --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/accept_fds.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'updateHeldTransactionRequest' => [ + 'merchantAuthentication' => [ + 'name' => 'someusername', + 'transactionKey' => 'somepassword' + ], + 'heldTransactionRequest' => [ + 'action' => 'approve', + 'refTransId' => '1234', + ] + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/authorize.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/authorize.php new file mode 100644 index 000000000000..16debdb2ef82 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/authorize.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'createTransactionRequest' => [ + 'merchantAuthentication' =>[ + 'name' => 'someusername', + 'transactionKey' => 'somepassword', + ], + 'transactionRequest' => [ + 'transactionType' => 'authOnlyTransaction', + 'amount' => '100.00', + 'payment' => [ + 'opaqueData' => [ + 'dataDescriptor' => 'mydescriptor', + 'dataValue' => 'myvalue', + ], + ], + 'solution' => [ + 'id' => 'AAA102993', + ], + 'order' => [ + 'invoiceNumber' => '100000001', + ], + 'poNumber' => null, + 'customer' => [ + 'id' => 1, + 'email' => 'admin@example.com', + ], + 'billTo' => [ + 'firstName' => 'firstname', + 'lastName' => 'lastname', + 'company' => '', + 'address' => 'street', + 'city' => 'Los Angeles', + 'state' => 'CA', + 'zip' => '11111', + 'country' => 'US', + ], + 'shipTo' => [ + 'firstName' => 'John', + 'lastName' => 'Doe', + 'company' => '', + 'address' => '6161 West Centinela Avenue', + 'city' => 'Los Angeles', + 'state' => 'CA', + 'zip' => '11111', + 'country' => 'US', + ], + 'customerIP' => '127.0.0.1', + 'userFields' => [ + 'userField' => [ + [ + 'name' => 'transactionType', + 'value' => 'authOnlyTransaction', + ], + ], + ], + ], + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/refund.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/refund.php new file mode 100644 index 000000000000..5ed331d076f6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/refund.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'createTransactionRequest' => [ + 'merchantAuthentication' =>[ + 'name' => 'someusername', + 'transactionKey' => 'somepassword', + ], + 'transactionRequest' => [ + 'transactionType' => 'refundTransaction', + 'amount' => '100.00', + 'payment' => [ + 'creditCard' => [ + 'cardNumber' => '1111', + 'expirationDate' => 'XXXX' + ] + ], + 'refTransId' => '4321', + 'order' => [ + 'invoiceNumber' => '100000001', + ], + 'poNumber' => null, + 'customer' => [ + 'id' => '1', + 'email' => 'admin@example.com', + ], + 'billTo' => [ + 'firstName' => 'firstname', + 'lastName' => 'lastname', + 'company' => '', + 'address' => 'street', + 'city' => 'Los Angeles', + 'state' => 'CA', + 'zip' => '11111', + 'country' => 'US', + ], + 'shipTo' => [ + 'firstName' => 'John', + 'lastName' => 'Doe', + 'company' => '', + 'address' => '6161 West Centinela Avenue', + 'city' => 'Los Angeles', + 'state' => 'CA', + 'zip' => '11111', + 'country' => 'US', + ], + 'customerIP' => '127.0.0.1' + ], + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/sale.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/sale.php new file mode 100644 index 000000000000..4514acbcb664 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/sale.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'createTransactionRequest' => [ + 'merchantAuthentication' =>[ + 'name' => 'someusername', + 'transactionKey' => 'somepassword', + ], + 'transactionRequest' => [ + 'transactionType' => 'authCaptureTransaction', + 'amount' => '100.00', + 'payment' => [ + 'opaqueData' => [ + 'dataDescriptor' => 'mydescriptor', + 'dataValue' => 'myvalue', + ], + ], + 'solution' => [ + 'id' => 'AAA102993', + ], + 'order' => [ + 'invoiceNumber' => '100000001', + ], + 'poNumber' => null, + 'customer' => [ + 'id' => 1, + 'email' => 'admin@example.com', + ], + 'billTo' => [ + 'firstName' => 'firstname', + 'lastName' => 'lastname', + 'company' => '', + 'address' => 'street', + 'city' => 'Los Angeles', + 'state' => 'CA', + 'zip' => '11111', + 'country' => 'US', + ], + 'shipTo' => [ + 'firstName' => 'John', + 'lastName' => 'Doe', + 'company' => '', + 'address' => '6161 West Centinela Avenue', + 'city' => 'Los Angeles', + 'state' => 'CA', + 'zip' => '11111', + 'country' => 'US', + ], + 'customerIP' => '127.0.0.1', + 'userFields' => [ + 'userField' => [ + [ + 'name' => 'transactionType', + 'value' => 'authCaptureTransaction', + ], + ], + ], + ], + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/settle.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/settle.php new file mode 100644 index 000000000000..b4fa88cc1e5a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/settle.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'createTransactionRequest' => [ + 'merchantAuthentication' =>[ + 'name' => 'someusername', + 'transactionKey' => 'somepassword', + ], + 'transactionRequest' => [ + 'transactionType' => 'priorAuthCaptureTransaction', + 'refTransId' => '1234', + 'userFields' => [ + 'userField' => [ + [ + 'name' => 'transactionType', + 'value' => 'priorAuthCaptureTransaction', + ], + ], + ], + ], + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/transaction_details.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/transaction_details.php new file mode 100644 index 000000000000..110333866766 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/transaction_details.php @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'getTransactionDetailsRequest' => [ + 'merchantAuthentication' => [ + 'name' => 'someusername', + 'transactionKey' => 'somepassword' + ], + 'transId' => '4321' + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/transaction_details_authorized.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/transaction_details_authorized.php new file mode 100644 index 000000000000..c3ffdedba685 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/transaction_details_authorized.php @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'getTransactionDetailsRequest' => [ + 'merchantAuthentication' => [ + 'name' => 'someusername', + 'transactionKey' => 'somepassword' + ], + 'transId' => '1234' + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/void.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/void.php new file mode 100644 index 000000000000..a1d3dade74ff --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/expected_request/void.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'createTransactionRequest' => [ + 'merchantAuthentication' => [ + 'name' => 'someusername', + 'transactionKey' => 'somepassword', + ], + 'transactionRequest' =>[ + 'transactionType' => 'voidTransaction', + 'refTransId' => '1234', + ], + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/full_order.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/full_order.php new file mode 100644 index 000000000000..cac7c38971ae --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/full_order.php @@ -0,0 +1,125 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Payment; +use Magento\Sales\Model\Order\Address; +use Magento\Sales\Model\Order\Item; +use Magento\TestFramework\Helper\Bootstrap; + +$addressData = include __DIR__ . '/../../../Magento/Sales/_files/address_data.php'; +require __DIR__ . '/../../../Magento/Customer/_files/customer.php'; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var $product Product */ +$product = $objectManager->create(Product::class); +$product->isObjectNew(true); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setId(1) + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Simple Product') + ->setSku('simple') + ->setPrice(10) + ->setWeight(1) + ->setShortDescription('Short description') + ->setTaxClassId(0) + ->setDescription('Description with <b>html tag</b>') + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData([ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1, + ])->setCanSaveCustomOptions(true) + ->setHasOptions(false); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$productRepository->save($product); + + +$billingAddress = $objectManager->create(Address::class, ['data' => $addressData]); +$billingAddress->setAddressType('billing'); + +$shippingAddress = clone $billingAddress; +$shippingAddress->setId(null) + ->setAddressType('shipping') + ->setStreet(['6161 West Centinela Avenue']) + ->setFirstname('John') + ->setLastname('Doe') + ->setShippingMethod('flatrate_flatrate'); + +$payment = $objectManager->create(Payment::class); +$payment->setAdditionalInformation('ccLast4', '1111'); +$payment->setAdditionalInformation('opaqueDataDescriptor', 'mydescriptor'); +$payment->setAdditionalInformation('opaqueDataValue', 'myvalue'); + +/** @var Item $orderItem */ +$orderItem1 = $objectManager->create(Item::class); +$orderItem1->setProductId($product->getId()) + ->setSku($product->getSku()) + ->setName($product->getName()) + ->setQtyOrdered(1) + ->setBasePrice($product->getPrice()) + ->setPrice($product->getPrice()) + ->setRowTotal($product->getPrice()) + ->setProductType($product->getTypeId()); + +/** @var Item $orderItem */ +$orderItem2 = $objectManager->create(Item::class); +$orderItem2->setProductId($product->getId()) + ->setSku('simple2') + ->setName('Simple product') + ->setPrice(100) + ->setQtyOrdered(2) + ->setBasePrice($product->getPrice()) + ->setPrice($product->getPrice()) + ->setRowTotal($product->getPrice()) + ->setProductType($product->getTypeId()); + +$orderAmount = 100; +$customerEmail = $billingAddress->getEmail(); + +/** @var Order $order */ +$order = $objectManager->create(Order::class); +$order->setIncrementId('100000001') + ->setState(Order::STATE_PROCESSING) + ->setStatus(Order::STATE_PROCESSING) + ->setCustomerId($customer->getId()) + ->setCustomerIsGuest(false) + ->setRemoteIp('127.0.0.1') + ->setCreatedAt(date('Y-m-d 00:00:55')) + ->setOrderCurrencyCode('USD') + ->setBaseCurrencyCode('USD') + ->setSubtotal($orderAmount) + ->setGrandTotal($orderAmount) + ->setBaseSubtotal($orderAmount) + ->setBaseGrandTotal($orderAmount) + ->setCustomerEmail($customerEmail) + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->setShippingDescription('Flat Rate - Fixed') + ->setShippingAmount(10) + ->setStoreId(1) + ->addItem($orderItem1) + ->addItem($orderItem2) + ->setQuoteId(1) + ->setPayment($payment); + +return $order; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/authorize.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/authorize.php new file mode 100644 index 000000000000..f80495137ca2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/authorize.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'transactionResponse' => [ + 'responseCode' => '1', + 'authCode' => 'abc123', + 'avsResultCode' => 'Y', + 'cvvResultCode' => 'P', + 'cavvResultCode' => '2', + 'transId' => '123456', + 'refTransID' => '', + 'transHash' => 'foobar', + 'testRequest' => '0', + 'accountNumber' => 'XXXX1111', + 'accountType' => 'Visa', + 'messages' => [ + [ + 'code' => '1', + 'description' => 'This transaction has been approved.' + ] + ], + 'userFields' => [ + [ + 'name' => 'transactionType', + 'value' => 'authOnlyTransaction' + ] + ], + 'transHashSha2' => 'CD1E57FB1B5C876FDBD536CB16F8BBBA687580EDD78DD881C7F14DC4467C32BF6C' + . '808620FBD59E5977DF19460B98CCFC0DA0D90755992C0D611CABB8E2BA52B0', + 'SupplementalDataQualificationIndicator' => 0 + ], + 'messages' => [ + 'resultCode' => 'Ok', + 'message' => [ + [ + 'code' => 'I00001', + 'text' => 'Successful.' + ] + ] + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/generic_success.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/generic_success.php new file mode 100644 index 000000000000..ea7662e31937 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/generic_success.php @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'messages' => [ + 'resultCode' => 'Ok', + 'message' => [ + 'code' => 'I00001', + 'text' => 'Successful' + ] + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/refund.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/refund.php new file mode 100644 index 000000000000..536f51d659ad --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/refund.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'transactionResponse' => [ + 'responseCode' => '1', + 'authCode' => '', + 'avsResultCode' => 'P', + 'cvvResultCode' => '', + 'cavvResultCode' => '', + 'transId' => '5678', + 'refTransID' => '4321', + 'testRequest' => '0', + 'accountNumber' => 'XXXX1111', + 'accountType' => 'Visa', + 'messages' => [ + [ + 'code' => '1', + 'description' => 'This transaction has been approved.' + ] + ], + 'transHashSha2' => '78BD31BA5BCDF3C3FA3C8373D8DF80EF07FC7E02C3545FCF18A408E2F76ED4F20D' + . 'FF007221374B576FDD1BFD953B3E5CF37249CEC4C135EEF975F7B478D8452C', + 'SupplementalDataQualificationIndicator' => 0 + ], + 'messages' => [ + 'resultCode' => 'Ok', + 'message' => [ + [ + 'code' => 'I00001', + 'text' => 'Successful.' + ] + ] + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/sale.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/sale.php new file mode 100644 index 000000000000..74a80110adec --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/sale.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'transactionResponse' => [ + 'responseCode' => '1', + 'authCode' => 'abc123', + 'avsResultCode' => 'Y', + 'cvvResultCode' => 'P', + 'cavvResultCode' => '2', + 'transId' => '123456', + 'refTransID' => '', + 'transHash' => 'foobar', + 'testRequest' => '0', + 'accountNumber' => 'XXXX1111', + 'accountType' => 'Visa', + 'messages' => [ + [ + 'code' => '1', + 'description' => 'This transaction has been approved.' + ] + ], + 'userFields' => [ + [ + 'name' => 'transactionType', + 'value' => 'authCaptureTransaction' + ] + ], + 'transHashSha2' => 'CD1E57FB1B5C876FDBD536CB16F8BBBA687580EDD78DD881C7F14DC4467C32BF6C' + . '808620FBD59E5977DF19460B98CCFC0DA0D90755992C0D611CABB8E2BA52B0', + 'SupplementalDataQualificationIndicator' => 0 + ], + 'messages' => [ + 'resultCode' => 'Ok', + 'message' => [ + [ + 'code' => 'I00001', + 'text' => 'Successful.' + ] + ] + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/settle.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/settle.php new file mode 100644 index 000000000000..5e54c3019874 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/settle.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'transactionResponse' => [ + 'responseCode' => '1', + 'authCode' => '', + 'avsResultCode' => 'P', + 'cvvResultCode' => '', + 'cavvResultCode' => '', + 'transId' => '1234', + 'refTransID' => '1234', + 'testRequest' => '0', + 'accountNumber' => 'XXXX1111', + 'accountType' => 'Visa', + 'messages' => [ + [ + 'code' => '1', + 'description' => 'This transaction has been approved.' + ] + ], + 'transHashSha2' => '1B22AB4E4DF750CF2E0D1944BB6903537C145545C7313C87B6FD4A6384' + . '709EA2609CE9A9788C128F2F2EAEEE474F6010418904648C6D000BE3AF7BCD98A5AD8F', + 'SupplementalDataQualificationIndicator' => 0 + ], + 'messages' => [ + 'resultCode' => 'Ok', + 'message' => [ + [ + 'code' => 'I00001', + 'text' => 'Successful.' + ] + ] + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/transaction_details_authorized.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/transaction_details_authorized.php new file mode 100644 index 000000000000..80fd24a5c601 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/transaction_details_authorized.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'messages' => [ + 'resultCode' => 'Ok', + 'message' => [ + 'code' => 'I00001', + 'text' => 'Successful' + ] + ], + 'transaction' => [ + 'transId' => '1234', + 'transactionType' => 'authOnlyTransaction', + 'transactionStatus' => 'authorizedPendingCapture' + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/transaction_details_declined.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/transaction_details_declined.php new file mode 100644 index 000000000000..24c9353e4088 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/transaction_details_declined.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'messages' => [ + 'resultCode' => 'Ok', + 'message' => [ + 'code' => 'I00001', + 'text' => 'Successful' + ] + ], + 'transaction' => [ + 'transId' => '1234', + 'transactionType' => 'authOnlyTransaction', + 'transactionStatus' => 'declined' + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/transaction_details_fds_pending.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/transaction_details_fds_pending.php new file mode 100644 index 000000000000..de045f30ab22 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/transaction_details_fds_pending.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'messages' => [ + 'resultCode' => 'Ok', + 'message' => [ + 'code' => 'I00001', + 'text' => 'Successful' + ] + ], + 'transaction' => [ + 'transId' => '1234', + 'transactionType' => 'authOnlyTransaction', + 'transactionStatus' => 'FDSPendingReview' + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/transaction_details_settled_capture.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/transaction_details_settled_capture.php new file mode 100644 index 000000000000..5df2f03a943a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/transaction_details_settled_capture.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'messages' => [ + 'resultCode' => 'Ok', + 'message' => [ + 'code' => 'I00001', + 'text' => 'Successful' + ] + ], + 'transaction' => [ + 'transId' => '4321', + 'transactionType' => 'captureOnlyTransaction', + 'transactionStatus' => 'settledSuccessfully' + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/transaction_details_voided.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/transaction_details_voided.php new file mode 100644 index 000000000000..7ee735cd8cf3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/transaction_details_voided.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'messages' => [ + 'resultCode' => 'Ok', + 'message' => [ + 'code' => 'I00001', + 'text' => 'Successful' + ] + ], + 'transaction' => [ + 'transId' => '1234', + 'transactionType' => 'authOnlyTransaction', + 'transactionStatus' => 'void' + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/void.php b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/void.php new file mode 100644 index 000000000000..eb71de4dd966 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AuthorizenetAcceptjs/_files/response/void.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +return [ + 'messages' => [ + 'resultCode' => 'Ok', + 'message' => [ + 'code' => 'I00001', + 'text' => 'Successful' + ] + ], + 'transactionResponse' => [ + 'responseCode' => '1', + 'messages' => [ + 'message' => [ + [ + 'code' => 1 + ] + ] + ], + 'transHashSha2' => '1B22AB4E4DF750CF2E0D1944BB6903537C145545C7313C87B6FD4A6384709E' + . 'A2609CE9A9788C128F2F2EAEEE474F6010418904648C6D000BE3AF7BCD98A5AD8F', + 'transId' => '1234' + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/Backend/Block/Dashboard/GraphTest.php b/dev/tests/integration/testsuite/Magento/Backend/Block/Dashboard/GraphTest.php index 17863cd70958..497deb2c9911 100644 --- a/dev/tests/integration/testsuite/Magento/Backend/Block/Dashboard/GraphTest.php +++ b/dev/tests/integration/testsuite/Magento/Backend/Block/Dashboard/GraphTest.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Backend\Block\Dashboard; /** @@ -27,6 +29,6 @@ protected function setUp() public function testGetChartUrl() { - $this->assertStringStartsWith('http://chart.apis.google.com/chart', $this->_block->getChartUrl()); + $this->assertStringStartsWith('https://image-charts.com/chart', $this->_block->getChartUrl()); } } diff --git a/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/Grid/MassactionTest.php b/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/Grid/MassactionTest.php index 8aeee9cf1249..e11c5ce5d9cf 100644 --- a/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/Grid/MassactionTest.php +++ b/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/Grid/MassactionTest.php @@ -87,41 +87,6 @@ public function testMassactionDefaultValues() $this->assertFalse($blockEmpty->isAvailable()); } - public function testGetJavaScript() - { - $this->loadLayout(); - - $javascript = $this->_block->getJavaScript(); - - $expectedItemFirst = '#"option_id1":{"label":"Option One",' . - '"url":"http:\\\/\\\/localhost\\\/index\.php\\\/(?:key\\\/([\w\d]+)\\\/)?",' . - '"complete":"Test","id":"option_id1"}#'; - $this->assertRegExp($expectedItemFirst, $javascript); - - $expectedItemSecond = '#"option_id2":{"label":"Option Two",' . - '"url":"http:\\\/\\\/localhost\\\/index\.php\\\/(?:key\\\/([\w\d]+)\\\/)?",' . - '"confirm":"Are you sure\?","id":"option_id2"}#'; - $this->assertRegExp($expectedItemSecond, $javascript); - } - - public function testGetJavaScriptWithAddedItem() - { - $this->loadLayout(); - - $input = [ - 'id' => 'option_id3', - 'label' => 'Option Three', - 'url' => '*/*/option3', - 'block_name' => 'admin.test.grid.massaction.option3', - ]; - $expected = '#"option_id3":{"id":"option_id3","label":"Option Three",' . - '"url":"http:\\\/\\\/localhost\\\/index\.php\\\/(?:key\\\/([\w\d]+)\\\/)?",' . - '"block_name":"admin.test.grid.massaction.option3"}#'; - - $this->_block->addItem($input['id'], $input); - $this->assertRegExp($expected, $this->_block->getJavaScript()); - } - /** * @param string $mageMode * @param int $expectedCount @@ -213,21 +178,4 @@ public function getItemsDataProvider() ] ]; } - - public function testGridContainsMassactionColumn() - { - $this->loadLayout(); - $this->_layout->getBlock('admin.test.grid')->toHtml(); - - $gridMassactionColumn = $this->_layout->getBlock('admin.test.grid') - ->getColumnSet() - ->getChildBlock('massaction'); - - $this->assertNotNull($gridMassactionColumn, 'Massaction column does not exist in the grid column set'); - $this->assertInstanceOf( - \Magento\Backend\Block\Widget\Grid\Column::class, - $gridMassactionColumn, - 'Massaction column is not an instance of \Magento\Backend\Block\Widget\Column' - ); - } } diff --git a/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/Dashboard/ProductsViewedTest.php b/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/Dashboard/ProductsViewedTest.php index 595a33344c7e..bd4dd0c8daf0 100644 --- a/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/Dashboard/ProductsViewedTest.php +++ b/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/Dashboard/ProductsViewedTest.php @@ -4,8 +4,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Backend\Controller\Adminhtml\Dashboard; +/** + * Test product viewed backend controller. + */ class ProductsViewedTest extends \Magento\TestFramework\TestCase\AbstractBackendController { /** @@ -14,6 +18,7 @@ class ProductsViewedTest extends \Magento\TestFramework\TestCase\AbstractBackend */ public function testExecute() { + $this->getRequest()->setMethod("POST"); $this->dispatch('backend/admin/dashboard/productsViewed/'); $this->assertEquals(200, $this->getResponse()->getHttpResponseCode()); diff --git a/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/DashboardTest.php b/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/DashboardTest.php index 07af21505f18..0eb98379b457 100644 --- a/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/DashboardTest.php +++ b/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/DashboardTest.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Backend\Controller\Adminhtml; /** @@ -19,8 +21,15 @@ public function testAjaxBlockAction() $this->assertContains('dashboard-diagram', $actual); } + /** + * Tests tunnelAction + * + * @throws \Exception + * @return void + */ public function testTunnelAction() { + // phpcs:disable Magento2.Functions.DiscouragedFunction $testUrl = \Magento\Backend\Block\Dashboard\Graph::API_URL . '?cht=p3&chd=t:60,40&chs=250x100&chl=Hello|World'; $handle = curl_init(); curl_setopt($handle, CURLOPT_URL, $testUrl); @@ -34,6 +43,7 @@ public function testTunnelAction() curl_close($handle); throw $e; } + // phpcs:enable $gaData = [ 'cht' => 'lc', diff --git a/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/IndexTest.php b/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/IndexTest.php index 219fde6e3707..d5a48b960811 100644 --- a/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/IndexTest.php +++ b/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/IndexTest.php @@ -5,6 +5,8 @@ */ namespace Magento\Backend\Controller\Adminhtml; +use Magento\Framework\App\Request\Http as HttpRequest; + /** * @magentoAppArea adminhtml * @magentoDbIsolation enabled @@ -45,6 +47,7 @@ public function testLoggedIndexAction() public function testGlobalSearchAction() { $this->getRequest()->setParam('isAjax', 'true'); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue('query', 'dummy'); $this->dispatch('backend/admin/index/globalSearch'); diff --git a/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/UrlRewriteTest.php b/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/UrlRewriteTest.php index 1185ae9727e9..0d48fc8b0f59 100644 --- a/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/UrlRewriteTest.php +++ b/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/UrlRewriteTest.php @@ -5,6 +5,8 @@ */ namespace Magento\Backend\Controller\Adminhtml; +use Magento\Framework\App\Request\Http as HttpRequest; + /** * @magentoAppArea adminhtml */ @@ -20,6 +22,7 @@ public function testSaveActionCmsPage() $page = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Cms\Model\Page::class); $page->load('page_design_blank', 'identifier'); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue( [ 'description' => 'Some URL rewrite description', diff --git a/dev/tests/integration/testsuite/Magento/Backend/Model/Auth/SessionTest.php b/dev/tests/integration/testsuite/Magento/Backend/Model/Auth/SessionTest.php index 5ca2bf1f7317..f1e7a1073760 100644 --- a/dev/tests/integration/testsuite/Magento/Backend/Model/Auth/SessionTest.php +++ b/dev/tests/integration/testsuite/Magento/Backend/Model/Auth/SessionTest.php @@ -3,8 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Backend\Model\Auth; +use Magento\TestFramework\Bootstrap as TestHelper; +use Magento\TestFramework\Helper\Bootstrap; + /** * @magentoAppArea adminhtml * @magentoAppIsolation enabled @@ -18,10 +22,15 @@ class SessionTest extends \PHPUnit\Framework\TestCase private $auth; /** - * @var \Magento\Backend\Model\Auth\Session + * @var Session */ private $authSession; + /** + * @var SessionFactory + */ + private $authSessionFactory; + /** * @var \Magento\Framework\ObjectManagerInterface */ @@ -30,11 +39,12 @@ class SessionTest extends \PHPUnit\Framework\TestCase protected function setUp() { parent::setUp(); - $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->objectManager = Bootstrap::getObjectManager(); $this->objectManager->get(\Magento\Framework\Config\ScopeInterface::class) ->setCurrentScope(\Magento\Backend\App\Area\FrontNameResolver::AREA_CODE); $this->auth = $this->objectManager->create(\Magento\Backend\Model\Auth::class); - $this->authSession = $this->objectManager->create(\Magento\Backend\Model\Auth\Session::class); + $this->authSession = $this->objectManager->create(Session::class); + $this->authSessionFactory = $this->objectManager->get(SessionFactory::class); $this->auth->setAuthStorage($this->authSession); $this->auth->logout(); } @@ -52,8 +62,8 @@ public function testIsLoggedIn($loggedIn) { if ($loggedIn) { $this->auth->login( - \Magento\TestFramework\Bootstrap::ADMIN_NAME, - \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD + TestHelper::ADMIN_NAME, + TestHelper::ADMIN_PASSWORD ); } $this->assertEquals($loggedIn, $this->authSession->isLoggedIn()); @@ -63,4 +73,23 @@ public function loginDataProvider() { return [[false], [true]]; } + + /** + * Check that persisting user data is working. + */ + public function testStorage() + { + $this->auth->login(TestHelper::ADMIN_NAME, TestHelper::ADMIN_PASSWORD); + $user = $this->authSession->getUser(); + $acl = $this->authSession->getAcl(); + /** @var Session $session */ + $session = $this->authSessionFactory->create(); + $persistedUser = $session->getUser(); + $persistedAcl = $session->getAcl(); + + $this->assertEquals($user->getData(), $persistedUser->getData()); + $this->assertEquals($user->getAclRole(), $persistedUser->getAclRole()); + $this->assertEquals($acl->getRoles(), $persistedAcl->getRoles()); + $this->assertEquals($acl->getResources(), $persistedAcl->getResources()); + } } diff --git a/dev/tests/integration/testsuite/Magento/Backend/Model/Locale/ResolverTest.php b/dev/tests/integration/testsuite/Magento/Backend/Model/Locale/ResolverTest.php index d1252be2c4b5..88662a65c742 100644 --- a/dev/tests/integration/testsuite/Magento/Backend/Model/Locale/ResolverTest.php +++ b/dev/tests/integration/testsuite/Magento/Backend/Model/Locale/ResolverTest.php @@ -3,9 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Backend\Model\Locale; use Magento\Framework\Locale\Resolver; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\User\Model\User; /** * @magentoAppArea adminhtml @@ -20,7 +23,7 @@ class ResolverTest extends \PHPUnit\Framework\TestCase protected function setUp() { parent::setUp(); - $this->_model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + $this->_model = Bootstrap::getObjectManager()->create( \Magento\Backend\Model\Locale\Resolver::class ); } @@ -38,12 +41,12 @@ public function testSetLocaleWithDefaultLocale() */ public function testSetLocaleWithBaseInterfaceLocale() { - $user = new \Magento\Framework\DataObject(); - $session = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + $user = Bootstrap::getObjectManager()->create(User::class); + $session = Bootstrap::getObjectManager()->get( \Magento\Backend\Model\Auth\Session::class ); $session->setUser($user); - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + Bootstrap::getObjectManager()->get( \Magento\Backend\Model\Auth\Session::class )->getUser()->setInterfaceLocale( 'fr_FR' @@ -56,7 +59,7 @@ public function testSetLocaleWithBaseInterfaceLocale() */ public function testSetLocaleWithSessionLocale() { - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + Bootstrap::getObjectManager()->get( \Magento\Backend\Model\Session::class )->setSessionLocale( 'es_ES' @@ -69,7 +72,7 @@ public function testSetLocaleWithSessionLocale() */ public function testSetLocaleWithRequestLocale() { - $request = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + $request = Bootstrap::getObjectManager() ->get(\Magento\Framework\App\RequestInterface::class); $request->setPostValue(['locale' => 'de_DE']); $this->_checkSetLocale('de_DE'); diff --git a/dev/tests/integration/testsuite/Magento/Braintree/Controller/Adminhtml/Order/PaymentReviewTest.php b/dev/tests/integration/testsuite/Magento/Braintree/Controller/Adminhtml/Order/PaymentReviewTest.php index 04bac807637d..bb7b04f53dd6 100644 --- a/dev/tests/integration/testsuite/Magento/Braintree/Controller/Adminhtml/Order/PaymentReviewTest.php +++ b/dev/tests/integration/testsuite/Magento/Braintree/Controller/Adminhtml/Order/PaymentReviewTest.php @@ -70,8 +70,8 @@ public function testExecuteAccept() ); $order = $this->orderRepository->get($orderId); - static::assertEquals(Order::STATE_PROCESSING, $order->getState()); - static::assertEquals(Order::STATE_PROCESSING, $order->getStatus()); + static::assertEquals(Order::STATE_COMPLETE, $order->getState()); + static::assertEquals(Order::STATE_COMPLETE, $order->getStatus()); } /** diff --git a/dev/tests/integration/testsuite/Magento/Braintree/Controller/Paypal/PlaceOrderTest.php b/dev/tests/integration/testsuite/Magento/Braintree/Controller/Paypal/PlaceOrderTest.php new file mode 100644 index 000000000000..4f2b0fd67840 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Braintree/Controller/Paypal/PlaceOrderTest.php @@ -0,0 +1,173 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Braintree\Controller\Paypal; + +use Braintree\Result\Successful; +use Braintree\Transaction; +use Magento\Braintree\Model\Adapter\BraintreeAdapter; +use Magento\Braintree\Model\Adapter\BraintreeAdapterFactory; +use Magento\Checkout\Model\Session; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Message\MessageInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\TestFramework\TestCase\AbstractController; +use PHPUnit\Framework\MockObject\MockObject; + +/** + * PlaceOrderTest + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class PlaceOrderTest extends AbstractController +{ + /** + * @var Session|MockObject + */ + private $session; + + /** + * @var BraintreeAdapter|MockObject + */ + private $adapter; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + + $this->session = $this->getMockBuilder(Session::class) + ->disableOriginalConstructor() + ->setMethods(['getQuote', 'setLastOrderStatus', 'unsLastBillingAgreementReferenceId']) + ->getMock(); + + $adapterFactory = $this->getMockBuilder(BraintreeAdapterFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->adapter = $this->getMockBuilder(BraintreeAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + $adapterFactory->method('create') + ->willReturn($this->adapter); + + $this->_objectManager->addSharedInstance($this->session, Session::class); + $this->_objectManager->addSharedInstance($adapterFactory, BraintreeAdapterFactory::class); + } + + /** + * @inheritdoc + */ + protected function tearDown() + { + $this->_objectManager->removeSharedInstance(Session::class); + $this->_objectManager->removeSharedInstance(BraintreeAdapterFactory::class); + parent::tearDown(); + } + + /** + * Tests a negative scenario for a place order flow when exception throws after placing an order. + * + * @magentoDataFixture Magento/Braintree/Fixtures/paypal_quote.php + */ + public function testExecuteWithFailedOrder() + { + $reservedOrderId = 'test01'; + $quote = $this->getQuote($reservedOrderId); + + $this->session->method('getQuote') + ->willReturn($quote); + + $this->adapter->method('sale') + ->willReturn($this->getTransactionStub('authorized')); + $this->adapter->method('void') + ->willReturn($this->getTransactionStub('voided')); + + // emulates an error after placing the order + $this->session->method('setLastOrderStatus') + ->willThrowException(new \Exception('Test Exception')); + + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->dispatch('braintree/paypal/placeOrder'); + + self::assertRedirect(self::stringContains('checkout/cart')); + self::assertSessionMessages( + self::equalTo(['The order #' . $reservedOrderId . ' cannot be processed.']), + MessageInterface::TYPE_ERROR + ); + + $order = $this->getOrder($reservedOrderId); + self::assertEquals('canceled', $order->getState()); + } + + /** + * Gets quote by reserved order ID. + * + * @param string $reservedOrderId + * @return CartInterface + */ + private function getQuote(string $reservedOrderId): CartInterface + { + $searchCriteria = $this->_objectManager->get(SearchCriteriaBuilder::class) + ->addFilter('reserved_order_id', $reservedOrderId) + ->create(); + + /** @var CartRepositoryInterface $quoteRepository */ + $quoteRepository = $this->_objectManager->get(CartRepositoryInterface::class); + $items = $quoteRepository->getList($searchCriteria) + ->getItems(); + + return array_pop($items); + } + + /** + * Gets order by increment ID. + * + * @param string $incrementId + * @return OrderInterface + */ + private function getOrder(string $incrementId): OrderInterface + { + $searchCriteria = $this->_objectManager->get(SearchCriteriaBuilder::class) + ->addFilter('increment_id', $incrementId) + ->create(); + + /** @var OrderRepositoryInterface $repository */ + $repository = $this->_objectManager->get(OrderRepositoryInterface::class); + $items = $repository->getList($searchCriteria) + ->getItems(); + + return array_pop($items); + } + + /** + * Creates stub for Braintree Transaction. + * + * @param string $status + * @return Successful + */ + private function getTransactionStub(string $status): Successful + { + $transaction = $this->getMockBuilder(Transaction::class) + ->disableOriginalConstructor() + ->getMock(); + $transaction->status = $status; + $transaction->paypal = [ + 'paymentId' => 'pay-001', + 'payerEmail' => 'test@test.com' + ]; + $response = new Successful(); + $response->success = true; + $response->transaction = $transaction; + + return $response; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Braintree/Controller/Paypal/ReviewTest.php b/dev/tests/integration/testsuite/Magento/Braintree/Controller/Paypal/ReviewTest.php new file mode 100644 index 000000000000..fc79048f15f4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Braintree/Controller/Paypal/ReviewTest.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Braintree\Controller\Paypal; + +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\TestFramework\TestCase\AbstractController; + +/** + * ReviewTest + */ +class ReviewTest extends AbstractController +{ + /** + * @var Review + */ + private $controller; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + + $this->controller = $this->_objectManager->create(Review::class); + } + + /** + * Test controller implements correct interfaces + * + */ + public function testInterfaceImplementation() + { + $this->assertInstanceOf(HttpGetActionInterface::class, $this->controller); + $this->assertInstanceOf(HttpPostActionInterface::class, $this->controller); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/assign_items_per_address.php b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/assign_items_per_address.php new file mode 100644 index 000000000000..91cea7dc9660 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/assign_items_per_address.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Api\CartRepositoryInterface; + +$store = $storeManager->getStore(); +$quote->setReservedOrderId('multishipping_quote_id_braintree') + ->setStoreId($store->getId()) + ->setCustomerEmail('customer001@test.com'); + +/** @var CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->get(CartRepositoryInterface::class); +$quote->collectTotals(); +$quoteRepository->save($quote); + +$items = $quote->getAllItems(); +$addressList = $quote->getAllShippingAddresses(); + +foreach ($addressList as $key => $address) { + $item = $items[$key]; + // set correct quantity per shipping address + $item->setQty(1); + $address->setTotalQty(1); + $address->addItem($item); +} + +// assign virtual product to the billing address +$billingAddress = $quote->getBillingAddress(); +$virtualItem = $items[sizeof($items) - 1]; +$billingAddress->setTotalQty(1); +$billingAddress->addItem($virtualItem); + +// need to recollect totals +$quote->setTotalsCollectedFlag(false); +$quote->collectTotals(); +$quoteRepository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/payment_braintree.php b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/payment_braintree.php new file mode 100644 index 000000000000..3e1db90f1f2c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/payment_braintree.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Model\Quote\Payment; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use Magento\Quote\Api\Data\PaymentInterface; +use Magento\Braintree\Model\Ui\ConfigProvider; + +/** + * @var Magento\Quote\Model\Quote $quote + */ + +if (empty($quote)) { + throw new \Exception('$quote should be defined in the parent fixture'); +} + +/** @var ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +/** @var PaymentInterface $payment */ +$payment = $objectManager->create(Payment::class); +$payment->setMethod(ConfigProvider::CODE); +$quote->setPayment($payment); diff --git a/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/payment_braintree_paypal.php b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/payment_braintree_paypal.php new file mode 100644 index 000000000000..e4bba222078b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/payment_braintree_paypal.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Model\Quote\Payment; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use Magento\Quote\Api\Data\PaymentInterface; +use Magento\Braintree\Model\Ui\PayPal\ConfigProvider; + +/** + * @var Magento\Quote\Model\Quote $quote + */ + +if (empty($quote)) { + throw new \Exception('$quote should be defined in the parent fixture'); +} + +/** @var ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +/** @var PaymentInterface $payment */ +$payment = $objectManager->create(Payment::class); +$payment->setMethod(ConfigProvider::PAYPAL_CODE); +$quote->setPayment($payment); diff --git a/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/paypal_quote.php b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/paypal_quote.php new file mode 100644 index 000000000000..e4c64d0e33d8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/paypal_quote.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\Braintree\Model\Ui\PayPal\ConfigProvider; +use Magento\Quote\Api\CartRepositoryInterface; + +require __DIR__ . '/../_files/paypal_vault_token.php'; +require __DIR__ . '/../../Sales/_files/quote_with_customer.php'; + +$quote->getShippingAddress() + ->setShippingMethod('flatrate_flatrate') + ->setCollectShippingRates(true); +$quote->getPayment() + ->setMethod(ConfigProvider::PAYPAL_VAULT_CODE) + ->setAdditionalInformation( + [ + 'customer_id' => $quote->getCustomerId(), + 'public_hash' => $paymentToken->getPublicHash() + ] + ); + +$quote->collectTotals(); + +/** @var CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->get(CartRepositoryInterface::class); +$quoteRepository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/paypal_quote_rollback.php b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/paypal_quote_rollback.php new file mode 100644 index 000000000000..5d0fa8cca85d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/paypal_quote_rollback.php @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +require __DIR__ . '/../../Sales/_files/quote_with_customer_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/quote_with_split_items_braintree.php b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/quote_with_split_items_braintree.php new file mode 100644 index 000000000000..1c56e611dd6d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/quote_with_split_items_braintree.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Model\Quote; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; + +/** @var ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); + +/** @var Quote $quote */ +$quote = $objectManager->create(Quote::class); + +require __DIR__ . '/../../../Magento/Multishipping/Fixtures/shipping_address_list.php'; +require __DIR__ . '/../../../Magento/Multishipping/Fixtures/billing_address.php'; +require __DIR__ . '/payment_braintree.php'; +require __DIR__ . '/../../../Magento/Multishipping/Fixtures/items.php'; +require __DIR__ . '/assign_items_per_address.php'; diff --git a/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/quote_with_split_items_braintree_paypal.php b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/quote_with_split_items_braintree_paypal.php new file mode 100644 index 000000000000..4bd8e926abb7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Braintree/Fixtures/quote_with_split_items_braintree_paypal.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Model\Quote; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; + +/** @var ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); + +/** @var Quote $quote */ +$quote = $objectManager->create(Quote::class); + +require __DIR__ . '/../../../Magento/Multishipping/Fixtures/shipping_address_list.php'; +require __DIR__ . '/../../../Magento/Multishipping/Fixtures/billing_address.php'; +require __DIR__ . '/payment_braintree_paypal.php'; +require __DIR__ . '/../../../Magento/Multishipping/Fixtures/items.php'; +require __DIR__ . '/assign_items_per_address.php'; diff --git a/dev/tests/integration/testsuite/Magento/Braintree/Model/MultishippingTest.php b/dev/tests/integration/testsuite/Magento/Braintree/Model/MultishippingTest.php new file mode 100644 index 000000000000..91bc0388d855 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Braintree/Model/MultishippingTest.php @@ -0,0 +1,254 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Braintree\Model; + +use Braintree\Result\Successful; +use Braintree\Transaction; +use Magento\Braintree\Gateway\Command\GetPaymentNonceCommand; +use Magento\Braintree\Model\Adapter\BraintreeAdapter; +use Magento\Braintree\Model\Adapter\BraintreeAdapterFactory; +use Magento\Checkout\Model\Session as CheckoutSession; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Multishipping\Model\Checkout\Type\Multishipping; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\Quote; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order\Email\Sender\OrderSender; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use \PHPUnit_Framework_MockObject_MockObject as MockObject; +use Magento\Payment\Gateway\Command\ResultInterface as CommandResultInterface; + +/** + * Tests Magento\Multishipping\Model\Checkout\Type\Multishipping with Braintree and BraintreePayPal payments. + * + * @magentoAppArea frontend + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class MultishippingTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var BraintreeAdapter|MockObject + */ + private $adapter; + + /** + * @var Multishipping + */ + private $model; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = Bootstrap::getObjectManager(); + + $orderSender = $this->getMockBuilder(OrderSender::class) + ->disableOriginalConstructor() + ->getMock(); + + $adapterFactory = $this->getMockBuilder(BraintreeAdapterFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->adapter = $this->getMockBuilder(BraintreeAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + $adapterFactory->method('create') + ->willReturn($this->adapter); + + $this->objectManager->addSharedInstance($adapterFactory, BraintreeAdapterFactory::class); + $this->objectManager->addSharedInstance($this->getPaymentNonceMock(), GetPaymentNonceCommand::class); + + $this->model = $this->objectManager->create( + Multishipping::class, + ['orderSender' => $orderSender] + ); + } + + /** + * Checks a case when multiple orders are created successfully using Braintree payment method. + * + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Braintree/Fixtures/quote_with_split_items_braintree.php + * @magentoConfigFixture current_store payment/braintree/active 1 + * @return void + */ + public function testCreateOrdersWithBraintree() + { + $this->adapter->method('sale') + ->willReturn( + $this->getTransactionStub() + ); + $this->createOrders(); + } + + /** + * Checks a case when multiple orders are created successfully using Braintree PayPal payment method. + * + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Braintree/Fixtures/quote_with_split_items_braintree_paypal.php + * @magentoConfigFixture current_store payment/braintree_paypal/active 1 + * @return void + */ + public function testCreateOrdersWithBraintreePaypal() + { + $this->adapter->method('sale') + ->willReturn( + $this->getTransactionPaypalStub() + ); + $this->createOrders(); + } + + /** + * Creates orders for multishipping checkout flow. + * + * @return void + */ + private function createOrders() + { + $expectedPlacedOrdersNumber = 3; + $quote = $this->getQuote('multishipping_quote_id_braintree'); + + /** @var CheckoutSession $session */ + $session = $this->objectManager->get(CheckoutSession::class); + $session->replaceQuote($quote); + + $this->model->createOrders(); + + $orderList = $this->getOrderList((int)$quote->getId()); + self::assertCount( + $expectedPlacedOrdersNumber, + $orderList, + 'Total successfully placed orders number mismatch' + ); + } + + /** + * Creates stub for Braintree capture Transaction. + * + * @return Successful + */ + private function getTransactionStub(): Successful + { + $transaction = $this->getMockBuilder(Transaction::class) + ->disableOriginalConstructor() + ->getMock(); + $transaction->status = 'submitted_for_settlement'; + $transaction->creditCard = [ + 'last4' => '1111', + 'cardType' => 'Visa', + 'expirationMonth' => '12', + 'expirationYear' => '2021' + ]; + + $creditCardDetails = new \stdClass(); + $creditCardDetails->token = '4fdg'; + $creditCardDetails->expirationMonth = '12'; + $creditCardDetails->expirationYear = '2021'; + $creditCardDetails->cardType = 'Visa'; + $creditCardDetails->last4 = '1111'; + $creditCardDetails->expirationDate = '12/2021'; + $transaction->creditCardDetails = $creditCardDetails; + + $response = new Successful(); + $response->success = true; + $response->transaction = $transaction; + + return $response; + } + + /** + * Creates stub for BraintreePaypal capture Transaction. + * + * @return Successful + */ + private function getTransactionPaypalStub(): Successful + { + $transaction = $this->getMockBuilder(Transaction::class) + ->disableOriginalConstructor() + ->getMock(); + $transaction->status = 'submitted_for_settlement'; + $transaction->paypal = [ + 'token' => 'fchxqx', + 'payerEmail' => 'payer@example.com', + 'paymentId' => 'PAY-33ac47a28e7f54791f6cda45', + ]; + $paypalDetails = new \stdClass(); + $paypalDetails->token = 'fchxqx'; + $paypalDetails->payerEmail = 'payer@example.com'; + $paypalDetails->paymentId = '33ac47a28e7f54791f6cda45'; + $transaction->paypalDetails = $paypalDetails; + + $response = new Successful(); + $response->success = true; + $response->transaction = $transaction; + + return $response; + } + + /** + * Retrieves quote by reserved order id. + * + * @param string $reservedOrderId + * @return Quote + */ + private function getQuote(string $reservedOrderId): Quote + { + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter('reserved_order_id', $reservedOrderId) + ->create(); + + /** @var CartRepositoryInterface $quoteRepository */ + $quoteRepository = $this->objectManager->get(CartRepositoryInterface::class); + $items = $quoteRepository->getList($searchCriteria)->getItems(); + + return array_pop($items); + } + + /** + * Get list of orders by quote id. + * + * @param int $quoteId + * @return array + */ + private function getOrderList(int $quoteId): array + { + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter('quote_id', $quoteId) + ->create(); + + /** @var OrderRepositoryInterface $orderRepository */ + $orderRepository = $this->objectManager->get(OrderRepositoryInterface::class); + return $orderRepository->getList($searchCriteria)->getItems(); + } + + /** + * Returns GetPaymentNonceCommand command mock. + * + * @return MockObject + */ + private function getPaymentNonceMock(): MockObject + { + $commandResult = $this->createMock(CommandResultInterface::class); + $commandResult->method('get') + ->willReturn(['paymentMethodNonce' => 'testNonce']); + $paymentNonce = $this->createMock(GetPaymentNonceCommand::class); + $paymentNonce->method('execute') + ->willReturn($commandResult); + + return $paymentNonce; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/Plugin/Frontend/ProductTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/Plugin/Frontend/ProductTest.php new file mode 100644 index 000000000000..91dcd5f3e8d5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/Plugin/Frontend/ProductTest.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Bundle\Model\Plugin\Frontend; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Interception\PluginList; +use PHPUnit\Framework\TestCase; + +/** + * Test bundle fronted product plugin adds children products ids to bundle product identities. + */ +class ProductTest extends TestCase +{ + /** + * Check, product plugin is registered for storefront. + * + * @magentoAppArea frontend + * @return void + */ + public function testProductIsRegistered(): void + { + $pluginInfo = Bootstrap::getObjectManager()->get(PluginList::class) + ->get(\Magento\Catalog\Model\Product::class, []); + $this->assertSame(Product::class, $pluginInfo['bundle']['instance']); + } + + /** + * Check plugin will add children ids to bundle product identities on storefront. + * + * @magentoDataFixture Magento/Bundle/_files/product.php + * @magentoAppArea frontend + * @return void + */ + public function testGetIdentitiesForBundleProductOnStorefront(): void + { + $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + $bundleProduct = $productRepository->get('bundle-product'); + $simpleProduct = $productRepository->get('simple'); + $expectedIdentities = [ + 'cat_p_' . $bundleProduct->getId(), + 'cat_p', + 'cat_p_' . $simpleProduct->getId(), + + ]; + $this->assertEquals($expectedIdentities, $bundleProduct->getIdentities()); + } + + /** + * Check plugin won't add children ids to bundle product identities in admin area. + * + * @magentoDataFixture Magento/Bundle/_files/product.php + * @magentoAppArea adminhtml + * @return void + */ + public function testGetIdentitiesForBundleProductInAdminArea(): void + { + $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + $bundleProduct = $productRepository->get('bundle-product'); + $expectedIdentities = [ + 'cat_p_' . $bundleProduct->getId(), + ]; + $this->assertEquals($expectedIdentities, $bundleProduct->getIdentities()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/FixedBundlePriceCalculatorWithDimensionTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/FixedBundlePriceCalculatorWithDimensionTest.php index b97bd9f82266..e9cb2f2d6c9d 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/FixedBundlePriceCalculatorWithDimensionTest.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/FixedBundlePriceCalculatorWithDimensionTest.php @@ -13,7 +13,6 @@ * @magentoDbIsolation disabled * @magentoIndexerDimensionMode catalog_product_price website_and_customer_group * @group indexer_dimension - * @magentoAppArea frontend */ class FixedBundlePriceCalculatorWithDimensionTest extends BundlePriceAbstract { diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/FixedBundleWithSpecialPriceCalculatorTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/FixedBundleWithSpecialPriceCalculatorTest.php index 1fcc205ddc33..d02f84e1641a 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/FixedBundleWithSpecialPriceCalculatorTest.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/FixedBundleWithSpecialPriceCalculatorTest.php @@ -23,6 +23,9 @@ class FixedBundleWithSpecialPriceCalculatorTest extends BundlePriceAbstract */ public function testPriceForFixedBundle(array $strategyModifiers, array $expectedResults) { + if (empty($strategyModifiers)) { + $this->markTestSkipped('Unskip after fixing https://github.com/magento-engcom/msi/issues/1398'); + } $this->prepareFixture($strategyModifiers, 'bundle_product'); $bundleProduct = $this->productRepository->get('bundle_product', false, null, true); diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/FixedBundleWithTierPriceCalculatorTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/FixedBundleWithTierPriceCalculatorTest.php index 3285b1e6450c..743795246217 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/FixedBundleWithTierPriceCalculatorTest.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/FixedBundleWithTierPriceCalculatorTest.php @@ -38,6 +38,10 @@ public function testPriceForFixedBundle(array $strategyModifiers, array $expecte $this->prepareFixture($strategyModifiers, 'bundle_product'); $bundleProduct = $this->productRepository->get('bundle_product', false, null, true); + if (empty($bundleProduct->getOptions())) { + $this->markTestSkipped('Unskip after fixing https://github.com/magento-engcom/msi/issues/1398'); + } + /** @var \Magento\Framework\Pricing\PriceInfo\Base $priceInfo */ $priceInfo = $bundleProduct->getPriceInfo(); $priceCode = \Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE; diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/PriceTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/PriceTest.php index 2a68ff48e5f9..4a5757aae313 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/PriceTest.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/PriceTest.php @@ -6,7 +6,7 @@ namespace Magento\Bundle\Model\Product; /** - * @magentoDataFixture Magento/Bundle/_files/product_with_tier_pricing.php + * Class to test bundle prices */ class PriceTest extends \PHPUnit\Framework\TestCase { @@ -22,6 +22,9 @@ protected function setUp() ); } + /** + * @magentoDataFixture Magento/Bundle/_files/product_with_tier_pricing.php + */ public function testGetTierPrice() { /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ @@ -37,4 +40,50 @@ public function testGetTierPrice() $this->assertEquals(20.0, $this->_model->getTierPrice(4, $product)); $this->assertEquals(30.0, $this->_model->getTierPrice(5, $product)); } + + /** + * Test calculation final price for bundle product with tire price in simple product + * + * @param float $bundleQty + * @param float $selectionQty + * @param float $finalPrice + * @magentoDataFixture Magento/Bundle/_files/product_with_simple_tier_pricing.php + * @dataProvider getSelectionFinalTotalPriceWithSimpleTierPriceDataProvider + */ + public function testGetSelectionFinalTotalPriceWithSimpleTierPrice( + float $bundleQty, + float $selectionQty, + float $finalPrice + ) { + /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ + $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + $bundleProduct = $productRepository->get('bundle-product'); + $simpleProduct = $productRepository->get('simple'); + $simpleProduct->setCustomerGroupId(\Magento\Customer\Model\Group::CUST_GROUP_ALL); + + $this->assertEquals( + $finalPrice, + $this->_model->getSelectionFinalTotalPrice( + $bundleProduct, + $simpleProduct, + $bundleQty, + $selectionQty, + false + ), + 'Tier price calculation for Simple product is wrong' + ); + } + + /** + * @return array + */ + public function getSelectionFinalTotalPriceWithSimpleTierPriceDataProvider(): array + { + return [ + [1, 1, 10], + [2, 1, 8], + [5, 1, 5], + ]; + } } diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_simple_tier_pricing.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_simple_tier_pricing.php new file mode 100644 index 000000000000..30f097848070 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_simple_tier_pricing.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +require __DIR__ . '/../../../Magento/Catalog/_files/product_simple.php'; + +/** @var $productFactory Magento\Catalog\Model\ProductFactory */ +$productFactory = $objectManager->create(\Magento\Catalog\Model\ProductFactory::class); +/** @var $bundleProduct \Magento\Catalog\Model\Product */ +$bundleProduct = $productFactory->create(); +$bundleProduct->setTypeId('bundle') + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([1]) + ->setPriceType(\Magento\Bundle\Model\Product\Price::PRICE_TYPE_DYNAMIC) + ->setPriceView(1) + ->setName('Bundle Product') + ->setSku('bundle-product') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData([ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1, + ]) + ->setBundleOptionsData( + [ + [ + 'title' => 'Bundle Product Items', + 'default_title' => 'Bundle Product Items', + 'type' => 'checkbox', + 'required' => 1, + 'delete' => '', + ], + ] + ) + ->setBundleSelectionsData( + [[['product_id' => $product->getId(), 'selection_qty' => 1, 'delete' => '']]] + ); +$productRepository->save($bundleProduct); diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_simple_tier_pricing_rollback.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_simple_tier_pricing_rollback.php new file mode 100644 index 000000000000..aa661c7412d4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_simple_tier_pricing_rollback.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +require __DIR__ . '/../../../Magento/Catalog/_files/product_simple_rollback.php'; + +/** @var \Magento\Framework\Registry $registry */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +try { + $product = $productRepository->get('bundle-product'); + $productRepository->delete($product); +} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { + //Product already removed +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/BundleTest.php b/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/BundleTest.php index e15f8d47a7bf..864bdaa2a133 100644 --- a/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/BundleTest.php +++ b/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/BundleTest.php @@ -9,7 +9,10 @@ class BundleTest extends AbstractProductExportImportTestCase { - public function exportImportDataProvider() + /** + * @return array + */ + public function exportImportDataProvider(): array { return [ // @todo uncomment after MAGETWO-49677 resolved @@ -45,17 +48,13 @@ public function exportImportDataProvider() ]; } - public function importReplaceDataProvider() - { - return $this->exportImportDataProvider(); - } - /** - * @param \Magento\Catalog\Model\Product $expectedProduct - * @param \Magento\Catalog\Model\Product $actualProduct + * @inheritdoc */ - protected function assertEqualsSpecificAttributes($expectedProduct, $actualProduct) - { + protected function assertEqualsSpecificAttributes( + \Magento\Catalog\Model\Product $expectedProduct, + \Magento\Catalog\Model\Product $actualProduct + ): void { $expectedBundleProductOptions = $expectedProduct->getExtensionAttributes()->getBundleProductOptions(); $actualBundleProductOptions = $actualProduct->getExtensionAttributes()->getBundleProductOptions(); diff --git a/dev/tests/integration/testsuite/Magento/Captcha/Observer/CaseCheckOnFrontendUnsuccessfulMessageWhenCaptchaFailedTest.php b/dev/tests/integration/testsuite/Magento/Captcha/Observer/CaseCheckOnFrontendUnsuccessfulMessageWhenCaptchaFailedTest.php new file mode 100644 index 000000000000..8355d81fdf5d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Captcha/Observer/CaseCheckOnFrontendUnsuccessfulMessageWhenCaptchaFailedTest.php @@ -0,0 +1,118 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Captcha\Observer; + +use Magento\Framework\Data\Form\FormKey; +use Magento\Framework\Message\MessageInterface; +use Magento\TestFramework\Request; +use Magento\TestFramework\TestCase\AbstractController; + +/** + * Test captcha observer behavior + * + * @magentoAppArea frontend + */ +class CaseCheckOnFrontendUnsuccessfulMessageWhenCaptchaFailedTest extends AbstractController +{ + /** + * Test incorrect captcha on customer login page + * + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * @magentoConfigFixture default_store customer/captcha/enable 1 + * @magentoConfigFixture default_store customer/captcha/forms user_login + * @magentoConfigFixture default_store customer/captcha/mode always + */ + public function testLoginCheckUnsuccessfulMessageWhenCaptchaFailed() + { + /** @var FormKey $formKey */ + $formKey = $this->_objectManager->get(FormKey::class); + $post = [ + 'login' => [ + 'username' => 'dummy@dummy.com', + 'password' => 'dummy_password1', + ], + 'captcha' => ['user_login' => 'wrong_captcha'], + 'form_key' => $formKey->getFormKey(), + ]; + + $this->prepareRequestData($post); + + $this->dispatch('customer/account/loginPost'); + + $this->assertRedirect($this->stringContains('customer/account/login')); + $this->assertSessionMessages( + $this->equalTo(['Incorrect CAPTCHA']), + MessageInterface::TYPE_ERROR + ); + } + + /** + * Test incorrect captcha on customer forgot password page + * + * @codingStandardsIgnoreStart + * @magentoConfigFixture current_store customer/password/limit_password_reset_requests_method 0 + * @magentoConfigFixture default_store customer/captcha/enable 1 + * @magentoConfigFixture default_store customer/captcha/forms user_forgotpassword + * @magentoConfigFixture default_store customer/captcha/mode always + */ + public function testForgotPasswordCheckUnsuccessfulMessageWhenCaptchaFailed() + { + $post = ['email' => 'dummy@dummy.com']; + $this->prepareRequestData($post); + + $this->dispatch('customer/account/forgotPasswordPost'); + + $this->assertRedirect($this->stringContains('customer/account/forgotpassword')); + $this->assertSessionMessages( + $this->equalTo(['Incorrect CAPTCHA']), + MessageInterface::TYPE_ERROR + ); + } + + /** + * Test incorrect captcha on customer create account page + * + * @codingStandardsIgnoreStart + * @magentoConfigFixture current_store customer/password/limit_password_reset_requests_method 0 + * @magentoConfigFixture default_store customer/captcha/enable 1 + * @magentoConfigFixture default_store customer/captcha/forms user_create + * @magentoConfigFixture default_store customer/captcha/mode always + */ + public function testCreateAccountCheckUnsuccessfulMessageWhenCaptchaFailed() + { + /** @var FormKey $formKey */ + $formKey = $this->_objectManager->get(FormKey::class); + $post = [ + 'firstname' => 'Firstname', + 'lastname' => 'Lastname', + 'email' => 'dummy@dummy.com', + 'password' => 'TestPassword123', + 'password_confirmation' => 'TestPassword123', + 'captcha' => ['user_create' => 'wrong_captcha'], + 'form_key' => $formKey->getFormKey(), + ]; + $this->prepareRequestData($post); + + $this->dispatch('customer/account/createPost'); + + $this->assertRedirect($this->stringContains('customer/account/create')); + $this->assertSessionMessages( + $this->equalTo(['Incorrect CAPTCHA']), + MessageInterface::TYPE_ERROR + ); + } + + /** + * @param array $postData + * @return void + */ + private function prepareRequestData($postData) + { + $this->getRequest()->setMethod(Request::METHOD_POST); + $this->getRequest()->setPostValue($postData); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php index bdc135f2b489..800d41f3c786 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php @@ -3,25 +3,140 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery; +use Magento\Catalog\Model\Product; +use Magento\Framework\App\Request\DataPersistorInterface; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + /** * @magentoAppArea adminhtml */ class ContentTest extends \PHPUnit\Framework\TestCase { - public function testGetUploader() + /** + * Test subject. + * + * @var Content + */ + private $block; + + /** + * @var Registry + */ + private $registry; + + /** + * @var DataPersistorInterface + */ + private $dataPersistor; + + /** + * @inheritdoc + */ + protected function setUp() { - /** @var $layout \Magento\Framework\View\Layout */ + $gallery = Bootstrap::getObjectManager()->get(Gallery::class); $layout = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( \Magento\Framework\View\LayoutInterface::class ); - /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery\Content */ - $block = $layout->createBlock( - \Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery\Content::class, + $this->block = $layout->createBlock( + Content::class, 'block' ); + $this->block->setElement($gallery); + $this->registry = Bootstrap::getObjectManager()->get(Registry::class); + $this->dataPersistor = Bootstrap::getObjectManager()->get(DataPersistorInterface::class); + } + + public function testGetUploader() + { + $this->assertInstanceOf(\Magento\Backend\Block\Media\Uploader::class, $this->block->getUploader()); + } - $this->assertInstanceOf(\Magento\Backend\Block\Media\Uploader::class, $block->getUploader()); + /** + * Test get images json using registry or data persistor. + * + * @dataProvider getImagesAndImageTypesDataProvider + * @magentoDataFixture Magento/Catalog/_files/product_with_image.php + * @magentoAppIsolation enabled + * @param bool $isProductNew + * @return void + */ + public function testGetImagesJson(bool $isProductNew) + { + $this->prepareProduct($isProductNew); + $imagesJson = $this->block->getImagesJson(); + $images = json_decode($imagesJson); + $image = array_shift($images); + $this->assertRegExp('/\/m\/a\/magento_image/', $image->file); + $this->assertSame('image', $image->media_type); + $this->assertSame('Image Alt Text', $image->label); + $this->assertSame('Image Alt Text', $image->label_default); + $this->assertRegExp('/\/pub\/media\/catalog\/product\/m\/a\/magento_image/', $image->url); + } + + /** + * Test get image types json using registry or data persistor. + * + * @dataProvider getImagesAndImageTypesDataProvider + * @magentoDataFixture Magento/Catalog/_files/product_with_image.php + * @magentoAppIsolation enabled + * @param bool $isProductNew + * @return void + */ + public function testGetImageTypes(bool $isProductNew) + { + $this->prepareProduct($isProductNew); + $imageTypes = $this->block->getImageTypes(); + foreach ($imageTypes as $type => $image) { + $this->assertSame($type, $image['code']); + $type !== 'swatch_image' + ? $this->assertRegExp('/\/m\/a\/magento_image/', $image['value']) + : $this->assertNull($image['value']); + $this->assertSame('[STORE VIEW]', $image['scope']->getText()); + $this->assertSame(sprintf('product[%s]', $type), $image['name']); + } + } + + /** + * Provide test data for testGetImagesJson() and tesGetImageTypes(). + * + * @return array + */ + public function getImagesAndImageTypesDataProvider() + { + return [ + [ + 'isProductNew' => true, + ], + [ + 'isProductNew' => false, + ], + ]; + } + + /** + * Prepare product, and set it to registry and data persistor. + * + * @param bool $isProductNew + * @return void + */ + private function prepareProduct(bool $isProductNew) + { + $product = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class)->get('simple'); + if ($isProductNew) { + $newProduct = Bootstrap::getObjectManager()->create(Product::class); + $this->registry->register('current_product', $newProduct); + $productData['product'] = $product->getData(); + $dataPersistor = Bootstrap::getObjectManager()->get(DataPersistorInterface::class); + $dataPersistor->set('catalog_product', $productData); + } else { + $this->registry->register('current_product', $product); + } } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php index 11f49464bbd2..1001d58ee8a6 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php @@ -5,11 +5,36 @@ */ namespace Magento\Catalog\Controller\Adminhtml; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Store\Model\Store; +use Magento\Catalog\Model\ResourceModel\Product; + /** * @magentoAppArea adminhtml */ class CategoryTest extends \Magento\TestFramework\TestCase\AbstractBackendController { + /** + * @var \Magento\Catalog\Model\ResourceModel\Product + */ + protected $productResource; + + /** + * @inheritDoc + * + * @throws \Magento\Framework\Exception\AuthenticationException + */ + protected function setUp() + { + parent::setUp(); + + /** @var Product $productResource */ + $this->productResource = Bootstrap::getObjectManager()->get( + Product::class + ); + } + /** * @magentoDataFixture Magento/Store/_files/core_fixturestore.php * @magentoDbIsolation enabled @@ -27,6 +52,7 @@ public function testSaveAction($inputData, $defaultAttributes, $attributesSaved $store->load('fixturestore', 'code'); $storeId = $store->getId(); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($inputData); $this->getRequest()->setParam('store', $storeId); $this->getRequest()->setParam('id', 2); @@ -75,6 +101,7 @@ public function testSaveAction($inputData, $defaultAttributes, $attributesSaved */ public function testSaveActionFromProductCreationPage($postData) { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($postData); $this->dispatch('backend/catalog/category/save'); @@ -123,6 +150,9 @@ public static function categoryCreatedFromProductCreationPageDataProvider() return [[$postData], [$postData + ['return_session_messages_only' => 1]]]; } + /** + * Test SuggestCategories finds any categories. + */ public function testSuggestCategoriesActionDefaultCategoryFound() { $this->getRequest()->setParam('label_part', 'Default'); @@ -133,6 +163,9 @@ public function testSuggestCategoriesActionDefaultCategoryFound() ); } + /** + * Test SuggestCategories properly processes search by label. + */ public function testSuggestCategoriesActionNoSuggestions() { $this->getRequest()->setParam('label_part', strrev('Default')); @@ -322,8 +355,12 @@ public function saveActionDataProvider() ]; } + /** + * Test validation. + */ public function testSaveActionCategoryWithDangerRequest() { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue( [ 'general' => [ @@ -374,7 +411,8 @@ public function testMoveAction($parentId, $childId, $childUrlKey, $grandChildId, } $this->getRequest() ->setPostValue('id', $grandChildId) - ->setPostValue('pid', $parentId); + ->setPostValue('pid', $parentId) + ->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/catalog/category/move'); $jsonResponse = json_decode($this->getResponse()->getBody()); $this->assertNotNull($jsonResponse); @@ -388,9 +426,133 @@ public function moveActionDataProvider() { return [ [400, 401, 'first_url_key', 402, 'second_url_key', false], - [400, 401, 'duplicated_url_key', 402, 'duplicated_url_key', true], + [400, 401, 'duplicated_url_key', 402, 'duplicated_url_key', false], [0, 401, 'first_url_key', 402, 'second_url_key', true], [400, 401, 'first_url_key', 0, 'second_url_key', true], ]; } + + /** + * @magentoDataFixture Magento/Catalog/_files/products_in_different_stores.php + * @magentoDbIsolation disabled + * @dataProvider saveActionWithDifferentWebsitesDataProvider + * + * @param array $postData + */ + public function testSaveCategoryWithProductPosition(array $postData) + { + /** @var $store \Magento\Store\Model\Store */ + $store = Bootstrap::getObjectManager()->create(Store::class); + $store->load('fixturestore', 'code'); + $storeId = $store->getId(); + $oldCategoryProductsCount = $this->getCategoryProductsCount(); + $this->getRequest()->setParam('store', $storeId); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setParam('id', 96377); + $this->getRequest()->setPostValue($postData); + $this->dispatch('backend/catalog/category/save'); + $newCategoryProductsCount = $this->getCategoryProductsCount(); + $this->assertEquals( + $oldCategoryProductsCount, + $newCategoryProductsCount, + 'After changing product position number of records from catalog_category_product has changed' + ); + } + + /** + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @return array + */ + public function saveActionWithDifferentWebsitesDataProvider() + { + return [ + 'default_values' => [ + [ + 'store_id' => '1', + 'entity_id' => '96377', + 'attribute_set_id' => '4', + 'parent_id' => '2', + 'created_at' => '2018-11-29 08:28:37', + 'updated_at' => '2018-11-29 08:57:43', + 'path' => '1/2/96377', + 'level' => '2', + 'children_count' => '0', + 'row_id' => '96377', + 'name' => 'Category 1', + 'display_mode' => 'PRODUCTS', + 'url_key' => 'category-1', + 'url_path' => 'category-1', + 'automatic_sorting' => '0', + 'is_active' => '1', + 'is_anchor' => '1', + 'include_in_menu' => '1', + 'custom_use_parent_settings' => '0', + 'custom_apply_to_products' => '0', + 'path_ids' => [ + 0 => '1', + 1 => '2', + 2 => '96377' + ], + 'use_config' => [ + 'available_sort_by' => 'true', + 'default_sort_by' => 'true', + 'filter_price_range' => 'true' + ], + 'id' => '', + 'parent' => '0', + 'use_default' => [ + 'name' => '1', + 'url_key' => '1', + 'meta_title' => '1', + 'is_active' => '1', + 'include_in_menu' => '1', + 'custom_use_parent_settings' => '1', + 'custom_apply_to_products' => '1', + 'description' => '1', + 'landing_page' => '1', + 'display_mode' => '1', + 'custom_design' => '1', + 'page_layout' => '1', + 'meta_keywords' => '1', + 'meta_description' => '1', + 'custom_layout_update' => '1', + 'image' => '1' + ], + 'filter_price_range' => false, + 'meta_title' => false, + 'url_key_create_redirect' => 'category-1', + 'description' => false, + 'landing_page' => false, + 'default_sort_by' => 'position', + 'available_sort_by' => false, + 'custom_design' => false, + 'page_layout' => false, + 'meta_keywords' => false, + 'meta_description' => false, + 'custom_layout_update' => false, + 'position_cache_key' => '5c069248346ac', + 'is_smart_category' => '0', + 'smart_category_rules' => false, + 'sort_order' => '0', + 'vm_category_products' => '{"1":1,"3":0}' + ] + ] + ]; + } + + /** + * Get items count from catalog_category_product + * + * @return int + */ + private function getCategoryProductsCount(): int + { + $oldCategoryProducts = $this->productResource->getConnection()->select()->from( + $this->productResource->getTable('catalog_category_product'), + 'product_id' + ); + return count( + $this->productResource->getConnection()->fetchAll($oldCategoryProducts) + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php index cea49d940cb6..3ec8c806dcbb 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php @@ -5,11 +5,49 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product\Action; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Model\ProductRepository; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\MessageQueue\PublisherConsumerController; + /** * @magentoAppArea adminhtml */ class AttributeTest extends \Magento\TestFramework\TestCase\AbstractBackendController { + /** @var PublisherConsumerController */ + private $publisherConsumerController; + private $consumers = ['product_action_attribute.update']; + + protected function setUp() + { + $this->publisherConsumerController = Bootstrap::getObjectManager()->create(PublisherConsumerController::class, [ + 'consumers' => $this->consumers, + 'logFilePath' => TESTS_TEMP_DIR . "/MessageQueueTestLog.txt", + 'maxMessages' => null, + 'appInitParams' => Bootstrap::getInstance()->getAppInitParams() + ]); + + try { + $this->publisherConsumerController->startConsumers(); + } catch (\Magento\TestFramework\MessageQueue\EnvironmentPreconditionException $e) { + $this->markTestSkipped($e->getMessage()); + } catch (\Magento\TestFramework\MessageQueue\PreconditionFailedException $e) { + $this->fail( + $e->getMessage() + ); + } + + parent::setUp(); + } + + protected function tearDown() + { + $this->publisherConsumerController->stopConsumers(); + parent::tearDown(); + } + /** * @covers \Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute\Save::execute * @@ -18,11 +56,12 @@ class AttributeTest extends \Magento\TestFramework\TestCase\AbstractBackendContr */ public function testSaveActionRedirectsSuccessfully() { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $objectManager = Bootstrap::getObjectManager(); /** @var $session \Magento\Backend\Model\Session */ $session = $objectManager->get(\Magento\Backend\Model\Session::class); $session->setProductIds([1]); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/catalog/product_action_attribute/save/store/0'); @@ -56,30 +95,46 @@ public function testSaveActionRedirectsSuccessfully() */ public function testSaveActionChangeVisibility($attributes) { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\ProductRepository::class + $objectManager = Bootstrap::getObjectManager(); + /** @var ProductRepository $repository */ + $repository = Bootstrap::getObjectManager()->create( + ProductRepository::class ); $product = $repository->get('simple'); $product->setOrigData(); - $product->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_NOT_VISIBLE); + $product->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE); $product->save(); /** @var $session \Magento\Backend\Model\Session */ $session = $objectManager->get(\Magento\Backend\Model\Session::class); $session->setProductIds([$product->getId()]); $this->getRequest()->setParam('attributes', $attributes); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/catalog/product_action_attribute/save/store/0'); + /** @var \Magento\Catalog\Model\Category $category */ - $categoryFactory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + $categoryFactory = Bootstrap::getObjectManager()->get( \Magento\Catalog\Model\CategoryFactory::class ); /** @var \Magento\Catalog\Block\Product\ListProduct $listProduct */ - $listProduct = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + $listProduct = Bootstrap::getObjectManager()->get( \Magento\Catalog\Block\Product\ListProduct::class ); + $this->publisherConsumerController->waitForAsynchronousResult( + function () use ($repository) { + sleep(3); + return $repository->get( + 'simple', + false, + null, + true + )->getVisibility() != Visibility::VISIBILITY_NOT_VISIBLE; + }, + [] + ); + $category = $categoryFactory->create()->load(2); $layer = $listProduct->getLayer(); $layer->setCurrentCategory($category); @@ -89,6 +144,8 @@ public function testSaveActionChangeVisibility($attributes) } /** + * @param array $attributes Request parameter. + * * @covers \Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute\Validate::execute * * @dataProvider validateActionDataProvider @@ -99,7 +156,7 @@ public function testSaveActionChangeVisibility($attributes) */ public function testValidateActionWithMassUpdate($attributes) { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $objectManager = Bootstrap::getObjectManager(); /** @var $session \Magento\Backend\Model\Session */ $session = $objectManager->get(\Magento\Backend\Model\Session::class); @@ -150,8 +207,8 @@ public function validateActionDataProvider() public function saveActionVisibilityAttrDataProvider() { return [ - ['arguments' => ['visibility' => \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH]], - ['arguments' => ['visibility' => \Magento\Catalog\Model\Product\Visibility::VISIBILITY_IN_CATALOG]] + ['arguments' => ['visibility' => Visibility::VISIBILITY_BOTH]], + ['arguments' => ['visibility' => Visibility::VISIBILITY_IN_CATALOG]] ]; } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php index 4261873cc8e6..e1d3e960593a 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php @@ -6,10 +6,13 @@ namespace Magento\Catalog\Controller\Adminhtml\Product; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\App\Request\Http as HttpRequest; /** * @magentoAppArea adminhtml * @magentoDbIsolation enabled + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class AttributeTest extends \Magento\TestFramework\TestCase\AbstractBackendController { @@ -25,6 +28,7 @@ public function testWrongFrontendInput() 'frontend_input' => 'some_input', ] ); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($postData); $this->dispatch('backend/catalog/product_attribute/save'); $this->assertEquals(302, $this->getResponse()->getHttpResponseCode()); @@ -50,6 +54,7 @@ public function testWithPopup() 'popup' => 'true', 'new_attribute_set_name' => 'new_attribute_set', ]; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($postData); $this->dispatch('backend/catalog/product_attribute/save'); $this->assertEquals(302, $this->getResponse()->getHttpResponseCode()); @@ -71,6 +76,7 @@ public function testWithExceptionWhenSaveAttribute() { $postData = $this->_getAttributeData() + ['attribute_id' => 0, 'frontend_input' => 'boolean']; $this->getRequest()->setPostValue($postData); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/catalog/product_attribute/save'); $this->assertEquals(302, $this->getResponse()->getHttpResponseCode()); $this->assertContains( @@ -89,6 +95,7 @@ public function testWrongAttributeId() { $postData = $this->_getAttributeData() + ['attribute_id' => 100500]; $this->getRequest()->setPostValue($postData); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/catalog/product_attribute/save'); $this->assertEquals(302, $this->getResponse()->getHttpResponseCode()); $this->assertContains( @@ -113,6 +120,7 @@ public function testAttributeWithoutId() 'set' => 4, 'frontend_input' => 'boolean', ]; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($postData); $this->dispatch('backend/catalog/product_attribute/save'); $this->assertEquals(302, $this->getResponse()->getHttpResponseCode()); @@ -135,6 +143,7 @@ public function testWrongAttributeCode() { $postData = $this->_getAttributeData() + ['attribute_code' => '_()&&&?']; $this->getRequest()->setPostValue($postData); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/catalog/product_attribute/save'); $this->assertEquals(302, $this->getResponse()->getHttpResponseCode()); $this->assertContains( @@ -147,8 +156,8 @@ public function testWrongAttributeCode() /** @var \Magento\Framework\Message\Error $message */ $message = $messages->getItemsByType('error')[0]; $this->assertEquals( - 'Attribute code "_()&&&?" is invalid. Please use only letters (a-z),' - . ' numbers (0-9) or underscore(_) in this field, first character should be a letter.', + 'Attribute code "_()&&&?" is invalid. Please use only letters (a-z or A-Z),' + . ' numbers (0-9) or underscore (_) in this field, and the first character should be a letter.', $message->getText() ); } @@ -159,6 +168,7 @@ public function testWrongAttributeCode() public function testAttributeWithoutEntityTypeId() { $postData = $this->_getAttributeData() + ['attribute_id' => '2', 'new_attribute_set_name' => ' ']; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($postData); $this->dispatch('backend/catalog/product_attribute/save'); $this->assertEquals(302, $this->getResponse()->getHttpResponseCode()); @@ -174,6 +184,7 @@ public function testAttributeWithoutEntityTypeId() public function testSaveActionApplyToDataSystemAttribute() { $postData = $this->_getAttributeData() + ['attribute_id' => '2']; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($postData); $this->dispatch('backend/catalog/product_attribute/save'); $model = $this->_objectManager->create(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); @@ -187,6 +198,7 @@ public function testSaveActionApplyToDataSystemAttribute() public function testSaveActionApplyToDataUserDefinedAttribute() { $postData = $this->_getAttributeData() + ['attribute_id' => '1']; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($postData); $this->dispatch('backend/catalog/product_attribute/save'); /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $model */ @@ -202,6 +214,7 @@ public function testSaveActionApplyToData() { $postData = $this->_getAttributeData() + ['attribute_id' => '3']; unset($postData['apply_to']); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($postData); $this->dispatch('backend/catalog/product_attribute/save'); $model = $this->_objectManager->create(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); @@ -221,6 +234,7 @@ public function testSaveActionCleanAttributeLabelCache() $this->assertEquals('predefined string translation', $this->_translate('string to translate')); $string->saveTranslate('string to translate', 'new string translation'); $postData = $this->_getAttributeData() + ['attribute_id' => 1]; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($postData); $this->dispatch('backend/catalog/product_attribute/save'); $this->assertEquals('new string translation', $this->_translate('string to translate')); @@ -284,15 +298,18 @@ public function testLargeOptionsDataSet() $optionsData = []; $expectedOptionsLabels = []; for ($i = 0; $i < $optionsCount; $i++) { - $order = $i + 1; - $expectedOptionLabelOnStoreView = "value_{$i}_store_1"; + $expectedOptionLabelOnStoreView = 'value_' . $i . '_store_1'; $expectedOptionsLabels[$i+1] = $expectedOptionLabelOnStoreView; - $optionsData []= "option[order][option_{$i}]={$order}"; - $optionsData []= "option[value][option_{$i}][0]=value_{$i}_admin"; - $optionsData []= "option[value][option_{$i}][1]={$expectedOptionLabelOnStoreView}"; - $optionsData []= "option[delete][option_{$i}="; + $optionId = 'option_' . $i; + $optionRowData = []; + $optionRowData['option']['order'][$optionId] = $i + 1; + $optionRowData['option']['value'][$optionId][0] = 'value_' . $i . '_admin'; + $optionRowData['option']['value'][$optionId][1] = $expectedOptionLabelOnStoreView; + $optionRowData['option']['delete'][$optionId] = ''; + $optionsData[] = http_build_query($optionRowData); } $attributeData['serialized_options'] = json_encode($optionsData); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($attributeData); $this->dispatch('backend/catalog/product_attribute/save'); $entityTypeId = $this->_objectManager->create( @@ -376,7 +393,7 @@ protected function _getAttributeData() 'used_in_product_listing' => '1', 'used_for_sort_by' => '0', 'apply_to' => ['simple'], - 'frontend_label' => [\Magento\Store\Model\Store::DEFAULT_STORE_ID => 'string to translate'] + 'frontend_label' => [\Magento\Store\Model\Store::DEFAULT_STORE_ID => 'string to translate'], ]; } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Set/DeleteTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Set/DeleteTest.php index 7c5d4ea48a23..7e034b8b3cb7 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Set/DeleteTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Set/DeleteTest.php @@ -6,6 +6,7 @@ namespace Magento\Catalog\Controller\Adminhtml\Product\Set; use Magento\Framework\Message\MessageInterface; +use Magento\Framework\App\Request\Http as HttpRequest; class DeleteTest extends \Magento\TestFramework\TestCase\AbstractBackendController { @@ -15,7 +16,7 @@ class DeleteTest extends \Magento\TestFramework\TestCase\AbstractBackendControll public function testDeleteById() { $attributeSet = $this->getAttributeSetByName('empty_attribute_set'); - $this->getRequest()->setParam('id', $attributeSet->getId()); + $this->getRequest()->setParam('id', $attributeSet->getId())->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/catalog/product_set/delete/'); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Set/SaveTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Set/SaveTest.php index 5b711b2ea741..8ccd426424a2 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Set/SaveTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Set/SaveTest.php @@ -9,6 +9,7 @@ use Magento\Eav\Api\Data\AttributeSetInterface; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Request\Http as HttpRequest; class SaveTest extends \Magento\TestFramework\TestCase\AbstractBackendController { @@ -20,6 +21,7 @@ public function testAlreadyExistsExceptionProcessingWhenGroupCodeIsDuplicated() $attributeSet = $this->getAttributeSetByName('attribute_set_test'); $this->assertNotEmpty($attributeSet, 'Attribute set with name "attribute_set_test" is missed'); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue('data', json_encode([ 'attribute_set_name' => 'attribute_set_test', 'groups' => [ diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php index 3e67095edcea..acec996d0c40 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php @@ -5,23 +5,35 @@ */ namespace Magento\Catalog\Controller\Adminhtml; +use Magento\Framework\App\Request\DataPersistorInterface; +use Magento\Framework\Message\Manager; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Message\MessageInterface; + /** * @magentoAppArea adminhtml */ class ProductTest extends \Magento\TestFramework\TestCase\AbstractBackendController { + /** + * Test calling save with invalid product's ID. + */ public function testSaveActionWithDangerRequest() { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue(['product' => ['entity_id' => 15]]); $this->dispatch('backend/catalog/product/save'); $this->assertSessionMessages( $this->equalTo(['The product was unable to be saved. Please try again.']), - \Magento\Framework\Message\MessageInterface::TYPE_ERROR + MessageInterface::TYPE_ERROR ); $this->assertRedirect($this->stringContains('/backend/catalog/product/new')); } /** + * Test saving existing product and specifying that we want redirect to new product form. + * * @magentoDataFixture Magento/Catalog/_files/product_simple.php */ public function testSaveActionAndNew() @@ -29,15 +41,19 @@ public function testSaveActionAndNew() $this->getRequest()->setPostValue(['back' => 'new']); $repository = $this->_objectManager->create(\Magento\Catalog\Model\ProductRepository::class); $product = $repository->get('simple'); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/catalog/product/save/id/' . $product->getEntityId()); $this->assertRedirect($this->stringStartsWith('http://localhost/index.php/backend/catalog/product/new/')); $this->assertSessionMessages( $this->contains('You saved the product.'), - \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS + MessageInterface::TYPE_SUCCESS ); } /** + * Test saving existing product and specifying that + * we want redirect to new product form with saved product's data applied. + * * @magentoDataFixture Magento/Catalog/_files/product_simple.php */ public function testSaveActionAndDuplicate() @@ -45,6 +61,7 @@ public function testSaveActionAndDuplicate() $this->getRequest()->setPostValue(['back' => 'duplicate']); $repository = $this->_objectManager->create(\Magento\Catalog\Model\ProductRepository::class); $product = $repository->get('simple'); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/catalog/product/save/id/' . $product->getEntityId()); $this->assertRedirect($this->stringStartsWith('http://localhost/index.php/backend/catalog/product/edit/')); $this->assertRedirect( @@ -56,14 +73,17 @@ public function testSaveActionAndDuplicate() ); $this->assertSessionMessages( $this->contains('You saved the product.'), - \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS + MessageInterface::TYPE_SUCCESS ); $this->assertSessionMessages( $this->contains('You duplicated the product.'), - \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS + MessageInterface::TYPE_SUCCESS ); } + /** + * Testing Add Product button showing. + */ public function testIndexAction() { $this->dispatch('backend/catalog/product'); @@ -104,6 +124,8 @@ public function testIndexAction() } /** + * Testing existing product edit page. + * * @magentoDataFixture Magento/Catalog/_files/product_simple.php */ public function testEditAction() @@ -140,4 +162,197 @@ public function testEditAction() '"Save & Duplicate" button isn\'t present on Edit Product page' ); } + + /** + * Test create product with already existing url key. + * + * @dataProvider saveActionWithAlreadyExistingUrlKeyDataProvider + * @magentoDataFixture Magento/Catalog/_files/product_image.php + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDbIsolation disabled + * @param array $postData + * @return void + */ + public function testSaveActionWithAlreadyExistingUrlKey(array $postData) + { + $this->getRequest()->setPostValue($postData); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->dispatch('backend/catalog/product/save'); + /** @var Manager $messageManager */ + $messageManager = $this->_objectManager->get(Manager::class); + $messages = $messageManager->getMessages(); + $errors = $messages->getItemsByType('error'); + $this->assertNotEmpty($errors); + $message = array_shift($errors); + $this->assertSame('URL key for specified store already exists.', $message->getText()); + $this->assertRedirect($this->stringContains('/backend/catalog/product/new')); + /** @var DataPersistorInterface $dataPersistor */ + $dataPersistor = $this->_objectManager->get(DataPersistorInterface::class); + $productData = $dataPersistor->get('catalog_product')['product']; + $image = array_shift($productData['media_gallery']['images']); + $this->assertStringEndsNotWith('.tmp', $image['file']); + $this->assertStringEndsNotWith('.tmp', $productData['image']); + $this->assertStringEndsNotWith('.tmp', $productData['small_image']); + $this->assertStringEndsNotWith('.tmp', $productData['thumbnail']); + $this->assertStringEndsNotWith('.tmp', $productData['swatch_image']); + } + + /** + * Provide test data for testSaveActionWithAlreadyExistingUrlKey(). + * + * @return array + */ + public function saveActionWithAlreadyExistingUrlKeyDataProvider() + { + return [ + [ + 'post_data' => [ + 'product' => + [ + 'attribute_set_id' => '4', + 'status' => '1', + 'name' => 's2', + 'url_key' => 'simple-product', + 'quantity_and_stock_status' => + [ + 'qty' => '10', + 'is_in_stock' => '1', + ], + 'website_ids' => + [ + 1 => '1', + ], + 'sku' => 's2', + 'price' => '3', + 'tax_class_id' => '2', + 'product_has_weight' => '0', + 'visibility' => '4', + 'media_gallery' => + [ + 'images' => + [ + 'h17hftqohrd' => + [ + 'position' => '1', + 'media_type' => 'image', + 'video_provider' => '', + 'file' => '/m/a//magento_image.jpg.tmp', + 'value_id' => '', + 'label' => '', + 'disabled' => '0', + 'removed' => '', + 'role' => '', + ], + ], + ], + 'image' => '/m/a//magento_image.jpg.tmp', + 'small_image' => '/m/a//magento_image.jpg.tmp', + 'thumbnail' => '/m/a//magento_image.jpg.tmp', + 'swatch_image' => '/m/a//magento_image.jpg.tmp', + ], + ] + ] + ]; + } + + /** + * Test product save with selected tier price + * + * @dataProvider saveActionTierPriceDataProvider + * @param array $postData + * @param array $tierPrice + * @magentoDataFixture Magento/Catalog/_files/product_has_tier_price_show_as_low_as.php + * @magentoConfigFixture current_store catalog/price/scope 1 + */ + public function testSaveActionTierPrice(array $postData, array $tierPrice) + { + $postData['product'] = $this->getProductData($tierPrice); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue($postData); + $this->dispatch('backend/catalog/product/save/id/' . $postData['id']); + $this->assertSessionMessages( + $this->contains('You saved the product.'), + MessageInterface::TYPE_SUCCESS + ); + } + + /** + * Provide test data for testSaveActionWithAlreadyExistingUrlKey(). + * + * @return array + */ + public function saveActionTierPriceDataProvider() + { + return [ + [ + 'post_data' => [ + 'id' => '1', + 'type' => 'simple', + 'store' => '0', + 'set' => '4', + 'back' => 'edit', + 'product' => [], + 'is_downloadable' => '0', + 'affect_configurable_product_attributes' => '1', + 'new_variation_attribute_set_id' => '4', + 'use_default' => [ + 'gift_message_available' => '0', + 'gift_wrapping_available' => '0' + ], + 'configurable_matrix_serialized' => '[]', + 'associated_product_ids_serialized' => '[]' + ], + 'tier_price_for_request' => [ + [ + 'price_id' => '1', + 'website_id' => '0', + 'cust_group' => '32000', + 'price' => '111.00', + 'price_qty' => '100', + 'website_price' => '111.0000', + 'initialize' => 'true', + 'record_id' => '1', + 'value_type' => 'fixed' + ], + [ + 'price_id' => '2', + 'website_id' => '1', + 'cust_group' => '32000', + 'price' => '222.00', + 'price_qty' => '200', + 'website_price' => '111.0000', + 'initialize' => 'true', + 'record_id' => '2', + 'value_type' => 'fixed' + ], + [ + 'price_id' => '3', + 'website_id' => '1', + 'cust_group' => '32000', + 'price' => '333.00', + 'price_qty' => '300', + 'website_price' => '111.0000', + 'initialize' => 'true', + 'record_id' => '3', + 'value_type' => 'fixed' + ] + ] + ] + ]; + } + + /** + * Return product data for test without entity_id for further save + * + * @param array $tierPrice + * @return array + */ + private function getProductData(array $tierPrice) + { + $productRepositoryInterface = $this->_objectManager->get(ProductRepositoryInterface::class); + $product = $productRepositoryInterface->get('tier_prices')->getData(); + $product['tier_price'] = $tierPrice; + unset($product['entity_id']); + return $product; + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Product/CompareTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Product/CompareTest.php index ec2e97f94245..1cb2caace4fa 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Product/CompareTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Product/CompareTest.php @@ -7,6 +7,7 @@ namespace Magento\Catalog\Controller\Product; use Magento\Framework\Message\MessageInterface; +use Magento\Framework\App\Request\Http as HttpRequest; /** * @magentoDataFixture Magento/Catalog/controllers/_files/products.php @@ -23,7 +24,7 @@ class CompareTest extends \Magento\TestFramework\TestCase\AbstractController protected $productRepository; /** - * Test setup + * @inheritDoc */ protected function setUp() { @@ -36,6 +37,8 @@ protected function setUp() } /** + * Test adding product to compare list. + * * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function testAddAction() @@ -45,6 +48,7 @@ public function testAddAction() /** @var \Magento\Framework\Data\Form\FormKey $formKey */ $formKey = $objectManager->get(\Magento\Framework\Data\Form\FormKey::class); $product = $this->productRepository->get('simple_product_1'); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch( sprintf( 'catalog/product_compare/add/product/%s/form_key/%s?nocookie=1', @@ -69,6 +73,8 @@ public function testAddAction() } /** + * Test comparing a product. + * * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function testIndexActionAddProducts() @@ -83,12 +89,15 @@ public function testIndexActionAddProducts() } /** + * Test removing a product from compare list. + * * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function testRemoveAction() { $this->_requireVisitorWithTwoProducts(); $product = $this->productRepository->get('simple_product_2'); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('catalog/product_compare/remove/product/' . $product->getEntityId()); $this->assertSessionMessages( @@ -102,12 +111,15 @@ public function testRemoveAction() } /** + * Test removing a product from compare list of a registered customer. + * * @throws \Magento\Framework\Exception\NoSuchEntityException */ public function testRemoveActionWithSession() { $this->_requireCustomerWithTwoProducts(); $product = $this->productRepository->get('simple_product_1'); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('catalog/product_compare/remove/product/' . $product->getEntityId()); $secondProduct = $this->productRepository->get('simple_product_2'); @@ -122,7 +134,7 @@ public function testRemoveActionWithSession() } /** - * testIndexActionDisplay + * Test getting a list of compared product. */ public function testIndexActionDisplay() { @@ -151,12 +163,13 @@ public function testIndexActionDisplay() } /** - * testClearAction + * Test clearing a list of compared products. */ public function testClearAction() { $this->_requireVisitorWithTwoProducts(); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('catalog/product_compare/clear'); $this->assertSessionMessages( @@ -170,12 +183,15 @@ public function testClearAction() } /** + * Test escaping a session message. + * * @magentoDataFixture Magento/Catalog/_files/product_simple_xss.php */ public function testRemoveActionProductNameXss() { $this->_prepareCompareListWithProductNameXss(); $product = $this->productRepository->get('product-with-xss'); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('catalog/product_compare/remove/product/' . $product->getEntityId() . '?nocookie=1'); $this->assertSessionMessages( @@ -187,6 +203,9 @@ public function testRemoveActionProductNameXss() } /** + * Preparing compare list. + * + * @throws \Magento\Framework\Exception\LocalizedException * @throws \Magento\Framework\Exception\NoSuchEntityException */ protected function _prepareCompareListWithProductNameXss() @@ -212,7 +231,9 @@ protected function _prepareCompareListWithProductNameXss() } /** - * _requireVisitorWithNoProducts + * Preparing compare list. + * + * @throws \Magento\Framework\Exception\LocalizedException */ protected function _requireVisitorWithNoProducts() { @@ -234,6 +255,9 @@ protected function _requireVisitorWithNoProducts() } /** + * Preparing compare list. + * + * @throws \Magento\Framework\Exception\LocalizedException * @throws \Magento\Framework\Exception\NoSuchEntityException */ protected function _requireVisitorWithTwoProducts() @@ -269,6 +293,9 @@ protected function _requireVisitorWithTwoProducts() } /** + * Preparing a compare list. + * + * @throws \Magento\Framework\Exception\LocalizedException * @throws \Magento\Framework\Exception\NoSuchEntityException */ protected function _requireCustomerWithTwoProducts() diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Category/Link/SaveHandlerTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Category/Link/SaveHandlerTest.php new file mode 100644 index 000000000000..5b24c4e22191 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Category/Link/SaveHandlerTest.php @@ -0,0 +1,141 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Category\Link; + +use Magento\Catalog\Api\Data\CategoryLinkInterfaceFactory; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Save handler test + * + * @magentoDataFixture Magento/Catalog/_files/categories_no_products.php + * @magentoDataFixture Magento/Catalog/_files/second_product_simple.php + */ +class SaveHandlerTest extends TestCase +{ + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var string + */ + private $productLinkField; + + /** + * @var CategoryLinkInterfaceFactory + */ + private $categoryLinkFactory; + + /** + * @var SaveHandler + */ + private $saveHandler; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->productRepository = $objectManager->create(ProductRepositoryInterface::class); + $metadataPool = $objectManager->create(MetadataPool::class); + $this->productLinkField = $metadataPool->getMetadata(ProductInterface::class) + ->getLinkField(); + $this->categoryLinkFactory = $objectManager->create(CategoryLinkInterfaceFactory::class); + $this->saveHandler = $objectManager->create(SaveHandler::class); + } + + /** + * Execute test + * + * @return void + */ + public function testExecute(): void + { + $product = $this->productRepository->get('simple2'); + $product->setCategoryIds([3, 4, 6]); + $this->productRepository->save($product); + $categoryPositions = [ + 3 => [ + 'category_id' => 3, + 'position' => 0, + ], + 4 => [ + 'category_id' => 4, + 'position' => 0, + ], + 6 => [ + 'category_id' => 6, + 'position' => 0, + ], + ]; + + $categoryLinks = $product->getExtensionAttributes()->getCategoryLinks(); + $this->assertEmpty($categoryLinks); + + $categoryLinks = []; + $categoryPositions[4]['position'] = 1; + $categoryPositions[6]['position'] = 1; + foreach ($categoryPositions as $categoryPosition) { + $categoryLink = $this->categoryLinkFactory->create() + ->setCategoryId($categoryPosition['category_id']) + ->setPosition($categoryPosition['position']); + $categoryLinks[] = $categoryLink; + } + $categoryLinks = $this->updateCategoryLinks($product, $categoryLinks); + $this->assertPositions($categoryPositions, $categoryLinks); + + $categoryPositions[4]['position'] = 2; + $categoryLink = $this->categoryLinkFactory->create() + ->setCategoryId(4) + ->setPosition($categoryPositions[4]['position']); + $categoryLinks = $this->updateCategoryLinks($product, [$categoryLink]); + $this->assertPositions($categoryPositions, $categoryLinks); + } + + /** + * Update category links + * + * @param ProductInterface $product + * @param \Magento\Catalog\Api\Data\CategoryLinkInterface[] $categoryLinks + * @return \Magento\Catalog\Api\Data\CategoryLinkInterface[] + */ + private function updateCategoryLinks(ProductInterface $product, array $categoryLinks): array + { + $product->getExtensionAttributes()->setCategoryLinks($categoryLinks); + $arguments = [$this->productLinkField => $product->getData($this->productLinkField)]; + $this->saveHandler->execute($product, $arguments); + $product = $this->productRepository->get($product->getSku(), false, null, true); + $categoryLinks = $product->getExtensionAttributes() + ->getCategoryLinks(); + + return $categoryLinks; + } + + /** + * Assert positions + * + * @param array $categoryPositions + * @param array $categoryLinks + * @return void + */ + private function assertPositions(array $categoryPositions, array $categoryLinks): void + { + foreach ($categoryLinks as $categoryLink) { + $categoryPosition = $categoryPositions[$categoryLink->getCategoryId()]; + $this->assertEquals($categoryPosition['category_id'], $categoryLink->getCategoryId()); + $this->assertEquals($categoryPosition['position'], $categoryLink->getPosition()); + } + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/DesignTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/DesignTest.php index 33f26302394f..38960ab66399 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/DesignTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/DesignTest.php @@ -32,8 +32,12 @@ public function testApplyCustomDesign($theme) $design = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( \Magento\Framework\View\DesignInterface::class ); + $translate = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + \Magento\Framework\TranslateInterface::class + ); $this->assertEquals('package', $design->getDesignTheme()->getPackageCode()); $this->assertEquals('theme', $design->getDesignTheme()->getThemeCode()); + $this->assertEquals('themepackage/theme', $translate->getTheme()); } /** diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ImageUploaderTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ImageUploaderTest.php index 50b40cf4f2ef..04baef55863f 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ImageUploaderTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ImageUploaderTest.php @@ -50,6 +50,7 @@ protected function setUp() 'baseTmpPath' => $this->mediaDirectory->getRelativePath('tmp'), 'basePath' => __DIR__, 'allowedExtensions' => ['jpg', 'jpeg', 'gif', 'png'], + 'allowedMimeTypes' => ['image/jpg', 'image/jpeg', 'image/gif', 'image/png'] ] ); } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/CustomFlatAttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/CustomFlatAttributeTest.php new file mode 100644 index 000000000000..049e7c8d2011 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/CustomFlatAttributeTest.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Indexer\Product\Flat\Action; + +use Magento\TestFramework\Indexer\TestCase as IndexerTestCase; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Indexer\Product\Flat\Processor; +use Magento\Catalog\Model\ResourceModel\Product\Flat; +use Magento\Framework\Api\SearchCriteriaBuilder; + +/** + * Custom Flat Attribute Test + */ +class CustomFlatAttributeTest extends IndexerTestCase +{ + /** + * @var Processor + */ + private $processor; + + /** + * @var \Magento\Framework\ObjectManagerInterface + */ + private $objectManager; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->processor = $this->objectManager->get(Processor::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + } + + /** + * Tests that custom product attribute will appear in flat table and can be updated in it. + * + * @magentoDbIsolation disabled + * @magentoAppArea frontend + * @magentoConfigFixture current_store catalog/frontend/flat_catalog_product 1 + * @magentoDataFixture Magento/Catalog/_files/product_simple_with_custom_attribute_in_flat.php + * + * @return void + * @throws \Magento\Framework\Exception\CouldNotSaveException + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Magento\Framework\Exception\StateException + */ + public function testProductUpdateCustomAttribute(): void + { + $product = $this->productRepository->get('simple_with_custom_flat_attribute'); + $product->setCustomAttribute('flat_attribute', 'changed flat attribute'); + $this->productRepository->save($product); + + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $this->objectManager->create(SearchCriteriaBuilder::class); + /** @var \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria */ + $searchCriteria = $searchCriteriaBuilder->addFilter('sku', 'simple_with_custom_flat_attribute') + ->create(); + + $items = $this->productRepository->getList($searchCriteria) + ->getItems(); + $product = reset($items); + $resourceModel = $product->getResourceCollection() + ->getEntity(); + + self::assertInstanceOf( + Flat::class, + $resourceModel, + 'Product should be received from flat resource' + ); + + self::assertEquals( + 'changed flat attribute', + $product->getFlatAttribute(), + 'Product flat attribute should be able to change.' + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/RowTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/RowTest.php index 15c90891878a..a32b476fe75f 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/RowTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/RowTest.php @@ -3,87 +3,110 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Model\Indexer\Product\Flat\Action; +use Magento\TestFramework\Indexer\TestCase as IndexerTestCase; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Indexer\Product\Flat\Processor; +use Magento\Catalog\Block\Product\ListProduct; +use Magento\Catalog\Api\CategoryRepositoryInterface; + /** * Class RowTest */ -class RowTest extends \Magento\TestFramework\Indexer\TestCase +class RowTest extends IndexerTestCase { /** - * @var \Magento\Catalog\Model\Product + * @var Processor */ - protected $_product; + private $processor; /** - * @var \Magento\Catalog\Model\Category + * @var \Magento\Framework\ObjectManagerInterface */ - protected $_category; + private $objectManager; /** - * @var \Magento\Catalog\Model\Indexer\Product\Flat\State + * @var ProductRepositoryInterface */ - protected $_state; + private $productRepository; /** - * @var \Magento\Catalog\Model\Indexer\Product\Flat\Processor + * @var CategoryRepositoryInterface */ - protected $_processor; + private $categoryRepository; - protected function setUp() + /** + * @inheritdoc + */ + protected function setUp(): void { - $this->_product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); - $this->_category = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Category::class - ); - $this->_processor = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Indexer\Product\Flat\Processor::class - ); + $this->objectManager = Bootstrap::getObjectManager(); + $this->processor = $this->objectManager->get(Processor::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->categoryRepository = $this->objectManager->get(CategoryRepositoryInterface::class); } /** + * Tests product update + * * @magentoDbIsolation disabled * @magentoDataFixture Magento/Catalog/_files/row_fixture.php * @magentoConfigFixture current_store catalog/frontend/flat_catalog_product 1 * @magentoAppArea frontend + * + * @return void + * @throws \Magento\Framework\Exception\CouldNotSaveException + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Magento\Framework\Exception\StateException */ - public function testProductUpdate() + public function testProductUpdate(): void { - $categoryFactory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Catalog\Model\CategoryFactory::class); - $listProduct = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Catalog\Block\Product\ListProduct::class); + /** @var ListProduct $listProduct */ + $listProduct = $this->objectManager->create(ListProduct::class); - $this->_processor->getIndexer()->setScheduled(false); - $this->assertFalse( - $this->_processor->getIndexer()->isScheduled(), + $this->processor->getIndexer() + ->setScheduled(false); + $isScheduled = $this->processor->getIndexer() + ->isScheduled(); + self::assertFalse( + $isScheduled, 'Indexer is in scheduled mode when turned to update on save mode' ); - $this->_product->load(1); - $this->_product->setName('Updated Product'); - $this->_product->save(); + $this->processor->reindexAll(); - $this->_processor->reindexAll(); - - $category = $categoryFactory->create()->load(9); + $product = $this->productRepository->get('simple'); + $product->setName('Updated Product'); + $this->productRepository->save($product); + + /** @var \Magento\Catalog\Api\Data\CategoryInterface $category */ + $category = $this->categoryRepository->get(9); + /** @var \Magento\Catalog\Model\Layer $layer */ $layer = $listProduct->getLayer(); $layer->setCurrentCategory($category); /** @var \Magento\Catalog\Model\ResourceModel\Product\Collection $productCollection */ $productCollection = $layer->getProductCollection(); - $this->assertTrue( + self::assertTrue( $productCollection->isEnabledFlat(), 'Product collection is not using flat resource when flat is on' ); - $this->assertEquals(2, $productCollection->count(), 'Product collection items count must be exactly 2'); + self::assertEquals( + 2, + $productCollection->count(), + 'Product collection items count must be exactly 2' + ); foreach ($productCollection as $product) { /** @var $product \Magento\Catalog\Model\Product */ - if ($product->getId() == 1) { - $this->assertEquals( + if ($product->getSku() === 'simple') { + self::assertEquals( 'Updated Product', $product->getName(), 'Product name from flat does not match with updated name' diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Price/SimpleWithOptionsTierPriceWithDimensionTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Price/SimpleWithOptionsTierPriceWithDimensionTest.php index 691ae78de71a..80af901788dd 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Price/SimpleWithOptionsTierPriceWithDimensionTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Price/SimpleWithOptionsTierPriceWithDimensionTest.php @@ -13,7 +13,7 @@ use Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory; use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; use Magento\Catalog\Pricing\Price\TierPrice; -use Magento\Customer\Model\Group; +use Magento\Customer\Model\Group as CustomerGroup; /** * @group indexer_dimension @@ -56,7 +56,7 @@ public function testTierPrice() $tierPrice = $this->objectManager->create(ProductTierPriceInterfaceFactory::class) ->create(); - $tierPrice->setCustomerGroupId(Group::CUST_GROUP_ALL); + $tierPrice->setCustomerGroupId(CustomerGroup::CUST_GROUP_ALL); $tierPrice->setQty(1.00); $tierPrice->setValue($tierPriceValue); $tierPriceManagement = $this->objectManager->create(ScopedProductTierPriceManagementInterface::class); 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 5721982ab8dc..d4926e78040d 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/Layer/Filter/Price/AlgorithmBaseTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Layer/Filter/Price/AlgorithmBaseTest.php index 4b69bcd4615d..e3a948d6c63d 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Layer/Filter/Price/AlgorithmBaseTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Layer/Filter/Price/AlgorithmBaseTest.php @@ -112,7 +112,7 @@ public function testPricesSegmentation($categoryId, array $entityIds, array $int $items = $model->calculateSeparators($interval); $this->assertEquals(array_keys($intervalItems), array_keys($items)); - for ($i = 0; $i < count($intervalItems); ++$i) { + for ($i = 0, $count = count($intervalItems); $i < $count; ++$i) { $this->assertInternalType('array', $items[$i]); $this->assertEquals($intervalItems[$i]['from'], $items[$i]['from']); $this->assertEquals($intervalItems[$i]['to'], $items[$i]['to']); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Backend/TierpriceTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Backend/TierpriceTest.php index 1a0439fccacb..973d290d6146 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Backend/TierpriceTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Backend/TierpriceTest.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Model\Product\Attribute\Backend; use Magento\Catalog\Api\Data\ProductInterface; @@ -11,6 +13,7 @@ * Test class for \Magento\Catalog\Model\Product\Attribute\Backend\Tierprice. * * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class TierpriceTest extends \PHPUnit\Framework\TestCase { @@ -24,6 +27,11 @@ class TierpriceTest extends \PHPUnit\Framework\TestCase */ protected $productRepository; + /** + * @var \Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory + */ + private $tierPriceFactory; + /** * @var \Magento\Catalog\Model\Product\Attribute\Backend\Tierprice */ @@ -40,6 +48,9 @@ protected function setUp() $this->metadataPool = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( \Magento\Framework\EntityManager\MetadataPool::class ); + $this->tierPriceFactory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory::class); + $this->_model->setAttribute( \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( \Magento\Eav\Model\Config::class @@ -57,27 +68,49 @@ public function testValidate() [ ['website_id' => 0, 'cust_group' => 1, 'price_qty' => 2, 'price' => 8], ['website_id' => 0, 'cust_group' => 1, 'price_qty' => 5, 'price' => 5], + ['website_id' => 0, 'cust_group' => 1, 'price_qty' => 5.6, 'price' => 4], ] ); $this->assertTrue($this->_model->validate($product)); } /** + * Test that duplicated tier price values issues exception during validation. + * + * @dataProvider validateDuplicateDataProvider * @expectedException \Magento\Framework\Exception\LocalizedException */ - public function testValidateDuplicate() + public function testValidateDuplicate(array $tierPricesData) { $product = new \Magento\Framework\DataObject(); - $product->setTierPrice( - [ - ['website_id' => 0, 'cust_group' => 1, 'price_qty' => 2, 'price' => 8], - ['website_id' => 0, 'cust_group' => 1, 'price_qty' => 2, 'price' => 8], - ] - ); + $product->setTierPrice($tierPricesData); $this->_model->validate($product); } + /** + * testValidateDuplicate data provider. + * + * @return array + */ + public function validateDuplicateDataProvider(): array + { + return [ + [ + [ + ['website_id' => 0, 'cust_group' => 1, 'price_qty' => 2, 'price' => 8], + ['website_id' => 0, 'cust_group' => 1, 'price_qty' => 2, 'price' => 8], + ], + ], + [ + [ + ['website_id' => 0, 'cust_group' => 1, 'price_qty' => 2.2, 'price' => 8], + ['website_id' => 0, 'cust_group' => 1, 'price_qty' => 2.2, 'price' => 8], + ], + ], + ]; + } + /** * @expectedException \Magento\Framework\Exception\LocalizedException */ @@ -86,9 +119,9 @@ public function testValidateDuplicateWebsite() $product = new \Magento\Framework\DataObject(); $product->setTierPrice( [ - ['website_id' => 0, 'cust_group' => 1, 'price_qty' => 2, 'price' => 8], - ['website_id' => 0, 'cust_group' => 1, 'price_qty' => 5, 'price' => 5], - ['website_id' => 1, 'cust_group' => 1, 'price_qty' => 5, 'price' => 5], + ['website_id' => 0, 'cust_group' => 1, 'price_qty' => 2.2, 'price' => 8], + ['website_id' => 0, 'cust_group' => 1, 'price_qty' => 5.3, 'price' => 5], + ['website_id' => 1, 'cust_group' => 1, 'price_qty' => 5.3, 'price' => 5], ] ); @@ -116,12 +149,17 @@ public function testPreparePriceData() ['website_id' => 0, 'cust_group' => 1, 'price_qty' => 2, 'price' => 8], ['website_id' => 0, 'cust_group' => 1, 'price_qty' => 5, 'price' => 5], ['website_id' => 1, 'cust_group' => 1, 'price_qty' => 5, 'price' => 5], + ['website_id' => 1, 'cust_group' => 1, 'price_qty' => 5.3, 'price' => 4], + ['website_id' => 1, 'cust_group' => 1, 'price_qty' => 5.4, 'price' => 3], + ['website_id' => 1, 'cust_group' => 1, 'price_qty' => '5.40', 'price' => 2], ]; $newData = $this->_model->preparePriceData($data, \Magento\Catalog\Model\Product\Type::TYPE_SIMPLE, 1); - $this->assertEquals(2, count($newData)); + $this->assertEquals(4, count($newData)); $this->assertArrayHasKey('1-2', $newData); $this->assertArrayHasKey('1-5', $newData); + $this->assertArrayHasKey('1-5.3', $newData); + $this->assertArrayHasKey('1-5.4', $newData); } public function testAfterLoad() @@ -137,87 +175,163 @@ public function testAfterLoad() $this->_model->afterLoad($product); $price = $product->getTierPrice(); $this->assertNotEmpty($price); - $this->assertEquals(4, count($price)); + $this->assertEquals(5, count($price)); } /** - * @magentoAppArea adminhtml - * @param array $tierPrice - * @param bool $isChanged - * @param int $tierPriceCtr - * @dataProvider afterSaveDataProvider + * @dataProvider saveExistingProductDataProvider + * @param array $tierPricesData + * @param int $tierPriceCount + * @throws \Magento\Framework\Exception\CouldNotSaveException + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Magento\Framework\Exception\StateException */ - public function testAfterSave($tierPrice, $isChanged, $tierPriceCtr) + public function testSaveExistingProduct(array $tierPricesData, int $tierPriceCount): void { /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); - $product->load($this->productRepository->get('simple')->getId()); - $product->unlockAttributes(); - // Added tier price - $product->setTierPrice($tierPrice); - - $this->_model->afterSave($product); - $this->assertEquals($isChanged, $product->getData('tier_price_changed')); - - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); - $fixtureProduct = $this->productRepository->get('simple'); - $product->setId($fixtureProduct->getId()); - $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); - $product->setData($linkField, $fixtureProduct->getData($linkField)); - $this->_model->afterLoad($product); - $this->assertEquals($tierPriceCtr, count($product->getTierPrice())); + $product = $this->productRepository->get('simple', true); + $tierPrices = []; + foreach ($tierPricesData as $tierPrice) { + $tierPrices[] = $this->tierPriceFactory->create([ + 'data' => $tierPrice + ]); + } + $product->setTierPrices($tierPrices); + $product = $this->productRepository->save($product); + $this->assertEquals($tierPriceCount, count($product->getTierPrice())); $this->assertEquals(0, $product->getData('tier_price_changed')); } - public function afterSaveDataProvider() + /** + * @return array + */ + public function saveExistingProductDataProvider(): array { return [ 'same' => [ [ - ['website_id' => 0, 'cust_group' => 32000, 'price_qty' => 2, 'price' => 8], - ['website_id' => 0, 'cust_group' => 32000, 'price_qty' => 5, 'price' => 5], - ['website_id' => 0, 'cust_group' => 0, 'price_qty' => 3, 'price' => 5], - ['website_id' => 0, 'cust_group' => 0, 'price_qty' => 10, 'percentage_value' => 50], + ['website_id' => 0, 'customer_group_id' => 32000, 'qty' => 2, 'value' => 8], + ['website_id' => 0, 'customer_group_id' => 32000, 'qty' => 5, 'value' => 5], + ['website_id' => 0, 'customer_group_id' => 0, 'qty' => 3, 'value' => 5], + ['website_id' => 0, 'customer_group_id' => 0, 'qty' => 3.2, 'value' => 6], + [ + 'website_id' => 0, + 'customer_group_id' => 0, + 'qty' => 10, + 'extension_attributes' => new \Magento\Framework\DataObject(['percentage_value' => 50]) + ], ], - 0, - 4, + 5, ], 'update one' => [ [ - ['website_id' => 0, 'cust_group' => 32000, 'price_qty' => 2, 'price' => 8], - ['website_id' => 0, 'cust_group' => 32000, 'price_qty' => 5, 'price' => 5], - ['website_id' => 0, 'cust_group' => 0, 'price_qty' => 3, 'price' => 5], - ['website_id' => 0, 'cust_group' => 0, 'price_qty' => 10, 'percentage_value' => 10], + ['website_id' => 0, 'customer_group_id' => 32000, 'qty' => 2, 'value' => 8], + ['website_id' => 0, 'customer_group_id' => 32000, 'qty' => 5, 'value' => 5], + ['website_id' => 0, 'customer_group_id' => 0, 'qty' => 3, 'value' => 5], + ['website_id' => 0, 'customer_group_id' => 0, 'qty' => '3.2', 'value' => 6], + [ + 'website_id' => 0, + 'customer_group_id' => 0, + 'qty' => 10, + 'extension_attributes' => new \Magento\Framework\DataObject(['percentage_value' => 10]) + ], ], - 1, - 4, + 5, ], 'delete one' => [ [ - ['website_id' => 0, 'cust_group' => 32000, 'price_qty' => 2, 'price' => 8, 'delete' => true], - ['website_id' => 0, 'cust_group' => 32000, 'price_qty' => 5, 'price' => 5], - ['website_id' => 0, 'cust_group' => 0, 'price_qty' => 3, 'price' => 5], - ['website_id' => 0, 'cust_group' => 0, 'price_qty' => 10, 'percentage_value' => 50], + ['website_id' => 0, 'customer_group_id' => 32000, 'qty' => 5, 'value' => 5], + ['website_id' => 0, 'customer_group_id' => 0, 'qty' => 3, 'value' => 5], + ['website_id' => 0, 'customer_group_id' => 0, 'qty' => '3.2', 'value' => 6], + [ + 'website_id' => 0, + 'customer_group_id' => 0, + 'qty' => 10, + 'extension_attributes' => new \Magento\Framework\DataObject(['percentage_value' => 50]) + ], ], - 1, - 3, + 4, ], 'add one' => [ [ - ['website_id' => 0, 'cust_group' => 32000, 'price_qty' => 2, 'price' => 8], - ['website_id' => 0, 'cust_group' => 32000, 'price_qty' => 5, 'price' => 5], - ['website_id' => 0, 'cust_group' => 32000, 'price_qty' => 20, 'percentage_value' => 90], - ['website_id' => 0, 'cust_group' => 0, 'price_qty' => 3, 'price' => 5], - ['website_id' => 0, 'cust_group' => 0, 'price_qty' => 10, 'percentage_value' => 50], + ['website_id' => 0, 'customer_group_id' => 32000, 'qty' => 2, 'value' => 8], + ['website_id' => 0, 'customer_group_id' => 32000, 'qty' => 5, 'value' => 5], + ['website_id' => 0, 'customer_group_id' => 0, 'qty' => 3, 'value' => 5], + ['website_id' => 0, 'customer_group_id' => 0, 'qty' => 3.2, 'value' => 6], + [ + 'website_id' => 0, + 'customer_group_id' => 32000, + 'qty' => 20, + 'extension_attributes' => new \Magento\Framework\DataObject(['percentage_value' => 90]) + ], + [ + 'website_id' => 0, + 'customer_group_id' => 0, + 'qty' => 10, + 'extension_attributes' => new \Magento\Framework\DataObject(['percentage_value' => 50]) + ], ], - 1, - 5, + 6, + ], + 'delete all' => [[], 0,], + ]; + } + + /** + * @dataProvider saveNewProductDataProvider + * @param array $tierPricesData + * @param int $tierPriceCount + * @throws \Magento\Framework\Exception\CouldNotSaveException + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\StateException + */ + public function testSaveNewProduct(array $tierPricesData, int $tierPriceCount): void + { + /** @var $product \Magento\Catalog\Model\Product */ + $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Model\Product::class); + $product->isObjectNew(true); + $product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setAttributeSetId(4) + ->setName('Simple Product New') + ->setSku('simple product new') + ->setPrice(10); + $tierPrices = []; + foreach ($tierPricesData as $tierPrice) { + $tierPrices[] = $this->tierPriceFactory->create([ + 'data' => $tierPrice, + ]); + } + $product->setTierPrices($tierPrices); + $product = $this->productRepository->save($product); + $this->assertEquals($tierPriceCount, count($product->getTierPrice())); + $this->assertEquals(0, $product->getData('tier_price_changed')); + } + + /** + * @return array + */ + public function saveNewProductDataProvider(): array + { + return [ + [ + [ + ['website_id' => 0, 'customer_group_id' => 32000, 'qty' => 2, 'value' => 8], + ['website_id' => 0, 'customer_group_id' => 32000, 'qty' => 5, 'value' => 5], + ['website_id' => 0, 'customer_group_id' => 0, 'qty' => 3, 'value' => 5], + ['website_id' => 0, 'customer_group_id' => 0, 'qty' => '3.2', 'value' => 4], + ['website_id' => 0, 'customer_group_id' => 0, 'qty' => '3.3', 'value' => 3], + [ + 'website_id' => 0, + 'customer_group_id' => 0, + 'qty' => 10, + 'extension_attributes' => new \Magento\Framework\DataObject(['percentage_value' => 50]) + ], + ], + 6, ], - 'delete all' => [[], 1, 0,], ]; } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFileTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFileTest.php index e7a2d17586a9..37ef60208861 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFileTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFileTest.php @@ -6,6 +6,8 @@ namespace Magento\Catalog\Model\Product\Option\Type\File; +use Magento\Framework\Math\Random; + /** * @magentoDataFixture Magento/Catalog/_files/validate_image.php * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -48,11 +50,18 @@ protected function setUp() $fileSize = $this->objectManager->create(\Magento\Framework\File\Size::class); $this->maxFileSize = $fileSize->getMaxFileSize(); $this->maxFileSizeInMb = $fileSize->getMaxFileSizeInMb(); + $random = $this->getMockBuilder(Random::class) + ->disableOriginalConstructor() + ->getMock(); + $random->expects($this->any()) + ->method('getRandomString') + ->willReturn('RandomString'); $this->model = $this->objectManager->create( - \Magento\Catalog\Model\Product\Option\Type\File\ValidatorFile::class, + ValidatorFile::class, [ - 'httpFactory' => $this->httpFactoryMock + 'httpFactory' => $this->httpFactoryMock, + 'random' => $random, ] ); } @@ -350,8 +359,8 @@ protected function expectedValidate() return [ 'type' => 'image/jpeg', 'title' => 'test.jpg', - 'quote_path' => 'custom_options/quote/t/e/a071b9ffc8fda6df1652c05a4c61bf8a.jpg', - 'order_path' => 'custom_options/order/t/e/a071b9ffc8fda6df1652c05a4c61bf8a.jpg', + 'quote_path' => 'custom_options/quote/t/e/RandomString', + 'order_path' => 'custom_options/order/t/e/RandomString', 'size' => '3046', 'width' => 136, 'height' => 131, 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 280e83a86332..fe39de2729ea 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Type/PriceWithDimensionTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Type/PriceWithDimensionTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + declare(strict_types=1); namespace Magento\Catalog\Model\Product\Type; @@ -108,7 +109,7 @@ public function testGetFinalPrice() } /** - * Get formated price + * Get formatted price */ public function testGetFormatedPrice() { diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductExternalTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductExternalTest.php index 8370e514dc2f..a1923e63972e 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductExternalTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductExternalTest.php @@ -69,7 +69,7 @@ public function testGetCategoryId() { $this->assertFalse($this->_model->getCategoryId()); $category = new \Magento\Framework\DataObject(['id' => 5]); - + $this->_model->setCategoryIds([5]); $this->objectManager->get(\Magento\Framework\Registry::class)->register('current_category', $category); try { $this->assertEquals(5, $this->_model->getCategoryId()); @@ -83,6 +83,7 @@ public function testGetCategoryId() public function testGetCategory() { $this->assertEmpty($this->_model->getCategory()); + $this->_model->setCategoryIds([3]); $this->objectManager->get(\Magento\Framework\Registry::class) ->register('current_category', new \Magento\Framework\DataObject(['id' => 3])); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductRepositoryTest.php index 39752460a1cd..d4016b2bfa8d 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductRepositoryTest.php @@ -90,4 +90,52 @@ public function skuDataProvider(): array ['sku' => 'simple '], ]; } + + /** + * Test save product with gallery image + * + * @magentoDataFixture Magento/Catalog/_files/product_simple_with_image.php + * + * @throws \Magento\Framework\Exception\CouldNotSaveException + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Exception\StateException + */ + public function testSaveProductWithGalleryImage(): void + { + /** @var $mediaConfig \Magento\Catalog\Model\Product\Media\Config */ + $mediaConfig = Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Model\Product\Media\Config::class); + + /** @var $mediaDirectory \Magento\Framework\Filesystem\Directory\WriteInterface */ + $mediaDirectory = Bootstrap::getObjectManager() + ->get(\Magento\Framework\Filesystem::class) + ->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::MEDIA); + + $product = Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); + $product->load(1); + + $path = $mediaConfig->getBaseMediaPath() . '/magento_image.jpg'; + $absolutePath = $mediaDirectory->getAbsolutePath() . $path; + $product->addImageToMediaGallery($absolutePath, [ + 'image', + 'small_image', + ], false, false); + + /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ + $productRepository = Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + $productRepository->save($product); + + $gallery = $product->getData('media_gallery'); + $this->assertArrayHasKey('images', $gallery); + $images = array_values($gallery['images']); + + $this->assertNotEmpty($gallery); + $this->assertTrue(isset($images[0]['file'])); + $this->assertStringStartsWith('/m/a/magento_image', $images[0]['file']); + $this->assertArrayHasKey('media_type', $images[0]); + $this->assertEquals('image', $images[0]['media_type']); + $this->assertStringStartsWith('/m/a/magento_image', $product->getData('image')); + $this->assertStringStartsWith('/m/a/magento_image', $product->getData('small_image')); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php index 82d39bbb7066..c34120404a95 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php @@ -581,8 +581,11 @@ public function testGetOptions() '4-2-radio' => 40000.00 ]; foreach ($options as $option) { + if (!$option->getValues()) { + continue; + } foreach ($option->getValues() as $value) { - $this->assertEquals($expectedValue[$value->getSku()], floatval($value->getPrice())); + $this->assertEquals($expectedValue[$value->getSku()], (float)$value->getPrice()); } } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/CollectionTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/CollectionTest.php index a5e55e181cba..904af7f33408 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/CollectionTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/CollectionTest.php @@ -187,4 +187,15 @@ public function testJoinTable() self::assertContains($expected, str_replace(PHP_EOL, '', $sql)); } + + /** + * @magentoDataFixture Magento/Catalog/Model/ResourceModel/_files/few_simple_products.php + * @magentoDbIsolation enabled + */ + public function testAddAttributeToFilterAffectsGetSize(): void + { + $this->assertEquals(10, $this->collection->getSize()); + $this->collection->addAttributeToFilter('sku', 'Product1'); + $this->assertEquals(1, $this->collection->getSize()); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/SourceTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/SourceTest.php index 4280e9dc8915..5cf6d00fe77e 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/SourceTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/SourceTest.php @@ -8,6 +8,7 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\TestFramework\Helper\Bootstrap; +use Magento\Catalog\_files\MultiselectSourceMock; /** * Class SourceTest @@ -157,4 +158,50 @@ public function testReindexMultiselectAttribute() $result = $connection->fetchAll($select); $this->assertCount(3, $result); } + + /** + * @magentoDataFixture Magento/Catalog/_files/products_with_multiselect_attribute_with_source_model.php + * @magentoDbIsolation disabled + */ + public function testReindexMultiselectAttributeWithSourceModel() + { + $objectManager = Bootstrap::getObjectManager(); + + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $objectManager->create(ProductRepositoryInterface::class); + + /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attr **/ + $attr = $objectManager->get(\Magento\Eav\Model\Config::class) + ->getAttribute('catalog_product', 'multiselect_attr_with_source'); + + /** @var $sourceModel MultiselectSourceMock */ + $sourceModel = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + MultiselectSourceMock::class + ); + $options = $sourceModel->getAllOptions(); + $product1Id = $options[0]['value'] * 10; + $product2Id = $options[1]['value'] * 10; + + /** @var \Magento\Catalog\Model\Product $product1 **/ + $product1 = $productRepository->getById($product1Id); + $product1->setSpecialFromDate(date('Y-m-d H:i:s')); + $product1->setNewsFromDate(date('Y-m-d H:i:s')); + $productRepository->save($product1); + + /** @var \Magento\Catalog\Model\Product $product2 **/ + $product2 = $productRepository->getById($product2Id); + $product1->setSpecialFromDate(date('Y-m-d H:i:s')); + $product1->setNewsFromDate(date('Y-m-d H:i:s')); + $productRepository->save($product2); + + $this->_eavIndexerProcessor->reindexAll(); + $connection = $this->productResource->getConnection(); + $select = $connection->select() + ->from($this->productResource->getTable('catalog_product_index_eav')) + ->where('entity_id in (?)', [$product1Id, $product2Id]) + ->where('attribute_id = ?', $attr->getId()); + + $result = $connection->fetchAll($select); + $this->assertCount(3, $result); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/ProductTest.php index 7954e2c36227..476f01eb277d 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/ProductTest.php @@ -12,6 +12,11 @@ class ProductTest extends TestCase { + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + /** * @var Product */ @@ -29,7 +34,8 @@ protected function setUp() { $this->objectManager = Bootstrap::getObjectManager(); - $this->model = $this->objectManager->get(Product::class); + $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + $this->model = $this->objectManager->create(Product::class); } /** @@ -42,11 +48,29 @@ public function testGetAttributeRawValue() $sku = 'simple'; $attribute = 'name'; - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); - $product = $productRepository->get($sku); - + $product = $this->productRepository->get($sku); $actual = $this->model->getAttributeRawValue($product->getId(), $attribute, null); self::assertEquals($product->getName(), $actual); } + + /** + * @magentoAppArea adminhtml + * @magentoDataFixture Magento/Catalog/_files/product_special_price.php + * @magentoAppIsolation enabled + * @magentoConfigFixture default_store catalog/price/scope 1 + */ + public function testUpdateStoreSpecificSpecialPrice() + { + /** @var \Magento\Catalog\Model\Product $product */ + $product = $this->productRepository->get('simple', true, 1); + $this->assertEquals(5.99, $product->getSpecialPrice()); + + $product->setSpecialPrice(''); + $this->model->save($product); + $product = $this->productRepository->get('simple', false, 1, true); + $this->assertEmpty($product->getSpecialPrice()); + + $product = $this->productRepository->get('simple', false, 0, true); + $this->assertEquals(5.99, $product->getSpecialPrice()); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/_files/few_simple_products.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/_files/few_simple_products.php new file mode 100644 index 000000000000..d8b24855ed1f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/_files/few_simple_products.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Catalog\Model\ProductFactory; +use Magento\Catalog\Api\ProductRepositoryInterface; + +/** @var Magento\Framework\ObjectManagerInterface $objcetManager */ +$objectManager = Bootstrap::getObjectManager(); + +/** @var ProductFactory $productFactory */ +$productFactory = $objectManager->create(ProductFactory::class); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); + +// Create 10 products (with change this variable, don't forget to change the same in rollback) +$productsAmount = 10; + +for ($i = 1; $i <= $productsAmount; $i++) { + $productArray = [ + 'data' => [ + 'name' => "Product{$i}", + 'sku' => "Product{$i}", + 'price' => 100, + 'attribute_set_id' => 4, + 'website_ids' => [1] + ] + ]; + + $productRepository->save($productFactory->create($productArray)); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/_files/few_simple_products_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/_files/few_simple_products_rollback.php new file mode 100644 index 000000000000..afe9483f63e1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/_files/few_simple_products_rollback.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager() + ->get(ProductRepositoryInterface::class); + +/** + * Delete 10 products + */ +$productsAmount = 10; + +try { + for ($i = 1; $i <= $productsAmount; $i++) { + /** @var \Magento\Catalog\Api\Data\ProductInterface $product */ + $product = $productRepository->get("Product{$i}", false, null, true); + $productRepository->delete($product); + } +} catch (NoSuchEntityException $e) { +} 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 000000000000..c09d68a66ee8 --- /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/MultiselectSourceMock.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/MultiselectSourceMock.php new file mode 100644 index 000000000000..b9325622d890 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/MultiselectSourceMock.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Catalog\_files; + +use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource; + +/** + * Creates mock source for multiselect attributes + */ +class MultiselectSourceMock extends AbstractSource +{ + + public function getAllOptions() + { + return [ + ['value' => 1, 'label' => 'Option 1'], + ['value' => 2, 'label' => 'Option 2'], + ['value' => 3, 'label' => 'Option 3'], + ['value' => 4, 'label' => 'Option 4 "!@#$%^&*'], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_with_product.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_with_product.php index 95f277c7124b..8712bbb1f86c 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_with_product.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_set_with_product.php @@ -7,5 +7,12 @@ require __DIR__ . '/../../Eav/_files/empty_attribute_set.php'; require __DIR__ . '/../../Catalog/_files/product_simple.php'; -$product->setAttributeSetId($attributeSet->getId()); -$product->save(); +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); +try { + $product = $productRepository->get('simple', true, null, true); + $product->setAttributeSetId($attributeSet->getId()); + $productRepository->save($product); +} catch (\Magento\Framework\Exception\NoSuchEntityException $e) { +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php index a903274793c3..a5ab96193246 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php @@ -178,7 +178,7 @@ ->setParentId(3) ->setPath('1/2/3/13') ->setLevel(3) - ->setDescription('Ololo') + ->setDescription('Its a description of Test Category 1.2') ->setAvailableSortBy('name') ->setDefaultSortBy('name') ->setIsActive(true) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_duplicates.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_duplicates.php index d3825675b724..46d5bfea6459 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_duplicates.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_duplicates.php @@ -15,7 +15,7 @@ )->setParentId( 2 )->setPath( - '1/2' + '1/2/444' )->setLevel( '2' )->setDefaultSortBy( diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_duplicates_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_duplicates_rollback.php new file mode 100644 index 000000000000..ceaad3a00329 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_duplicates_rollback.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Exception\NoSuchEntityException; + +/** @var \Magento\Framework\ObjectManagerInterface $objectManager */ +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var \Magento\Framework\Registry $registry */ +$registry = $objectManager->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Model\ResourceModel\Category\Collection $collection */ +$categoryCollection = $objectManager->create(\Magento\Catalog\Model\ResourceModel\Category\Collection::class); +$categoryCollection + ->addAttributeToFilter('level', 2) + ->load() + ->delete(); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); +try { + $product = $productRepository->get('simple3', false, null, true); + $productRepository->delete($product); +} catch (NoSuchEntityException $e) { +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/multiselect_attribute_with_source_model.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/multiselect_attribute_with_source_model.php new file mode 100644 index 000000000000..6dcba90bf44f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/multiselect_attribute_with_source_model.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +/* Create attribute */ +/** @var $installer \Magento\Catalog\Setup\CategorySetup */ +$installer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Setup\CategorySetup::class +); +/** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ +$attribute = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class +); +$entityType = $installer->getEntityTypeId('catalog_product'); +if (!$attribute->loadByCode($entityType, 'multiselect_attr_with_source')->getAttributeId()) { + $attribute->setData( + [ + 'attribute_code' => 'multiselect_attr_with_source', + 'entity_type_id' => $entityType, + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'multiselect', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 0, + 'is_visible_in_advanced_search' => 0, + 'is_comparable' => 0, + 'is_filterable' => 1, + 'is_filterable_in_search' => 0, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 0, + 'used_in_product_listing' => 0, + 'used_for_sort_by' => 0, + 'frontend_label' => ['Multiselect Attribute with Source Model'], + 'backend_type' => 'varchar', + 'backend_model' => \Magento\Eav\Model\Entity\Attribute\Backend\ArrayBackend::class, + 'source_model' => \Magento\Catalog\_files\MultiselectSourceMock::class + ] + ); + $attribute->save(); + + /* Assign attribute to attribute set */ + $installer->addAttributeToGroup('catalog_product', 'Default', 'General', $attribute->getId()); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/multiselect_attribute_with_source_model_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/multiselect_attribute_with_source_model_rollback.php new file mode 100644 index 000000000000..9859e1ab4c78 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/multiselect_attribute_with_source_model_rollback.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +/* Delete attribute with multiselect_attribute code */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get('Magento\Framework\Registry'); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +/** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ +$attribute = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + 'Magento\Catalog\Model\ResourceModel\Eav\Attribute' +); +$attribute->load('multiselect_attr_with_source', 'attribute_code'); +$attribute->delete(); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple.php index 1c8a4e64cdfd..514c6563622c 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple.php @@ -5,6 +5,7 @@ */ use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory; +use Magento\Catalog\Api\Data\ProductExtensionInterfaceFactory; \Magento\TestFramework\Helper\Bootstrap::getInstance()->reinitialize(); @@ -19,10 +20,15 @@ $tierPriceFactory = $objectManager->get(\Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory::class); /** @var $tpExtensionAttributes */ $tpExtensionAttributesFactory = $objectManager->get(ProductTierPriceExtensionFactory::class); +/** @var $productExtensionAttributes */ +$productExtensionAttributesFactory = $objectManager->get(ProductExtensionInterfaceFactory::class); $adminWebsite = $objectManager->get(\Magento\Store\Api\WebsiteRepositoryInterface::class)->get('admin'); $tierPriceExtensionAttributes1 = $tpExtensionAttributesFactory->create() ->setWebsiteId($adminWebsite->getId()); +$productExtensionAttributesWebsiteIds = $productExtensionAttributesFactory->create( + ['website_ids' => $adminWebsite->getId()] +); $tierPrices[] = $tierPriceFactory->create( [ @@ -54,6 +60,16 @@ ] )->setExtensionAttributes($tierPriceExtensionAttributes1); +$tierPrices[] = $tierPriceFactory->create( + [ + 'data' => [ + 'customer_group_id' => \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID, + 'qty' => 3.2, + 'value' => 6, + ] + ] +)->setExtensionAttributes($tierPriceExtensionAttributes1); + $tierPriceExtensionAttributes2 = $tpExtensionAttributesFactory->create() ->setWebsiteId($adminWebsite->getId()) ->setPercentageValue(50); @@ -82,6 +98,7 @@ ->setTaxClassId(0) ->setTierPrices($tierPrices) ->setDescription('Description with <b>html tag</b>') + ->setExtensionAttributes($productExtensionAttributesWebsiteIds) ->setMetaTitle('meta title') ->setMetaKeyword('meta keyword') ->setMetaDescription('meta description') diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_attribute_in_flat.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_attribute_in_flat.php new file mode 100644 index 000000000000..2b1b271a8bb3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_attribute_in_flat.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\ProductFactory; +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Model\Indexer\Product\Flat\Processor; +use Magento\Catalog\Setup\CategorySetup; +use Magento\Eav\Model\Entity; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Type; + +/** @var \Magento\TestFramework\ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var ProductFactory $productFactory */ +$productFactory = $objectManager->get(ProductFactory::class); +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); + + +/** @var $installer CategorySetup */ +$installer = $objectManager->create(CategorySetup::class); +$entityModel = $objectManager->create(Entity::class); +$attributeSetId = $installer->getAttributeSetId(Product::ENTITY, 'Default'); +$entityTypeId = $entityModel->setType(Product::ENTITY) + ->getTypeId(); +$groupId = $installer->getDefaultAttributeGroupId($entityTypeId, $attributeSetId); + +/** @var ProductAttributeInterface $attribute */ +$attribute = $objectManager->create(ProductAttributeInterface::class); + +$attribute->setAttributeCode('flat_attribute') + ->setEntityTypeId($entityTypeId) + ->setIsVisible(true) + ->setFrontendInput('text') + ->setIsFilterable(1) + ->setIsUserDefined(1) + ->setUsedInProductListing(1) + ->setBackendType('varchar') + ->setIsUsedInGrid(1) + ->setIsVisibleInGrid(1) + ->setIsFilterableInGrid(1) + ->setFrontendLabel('nobody cares') + ->setAttributeGroupId($groupId) + ->setAttributeSetId(4); + +$attributeRepository->save($attribute); + +/** @var Processor $processor */ +$processor = $objectManager->create(Processor::class); +$scheduled = $processor->getIndexer() + ->isScheduled(); +$processor->reindexAll(); + +$product = $productFactory->create() + ->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId(4) + ->setName('Simple With Attribute That Used In Flat') + ->setSku('simple_with_custom_flat_attribute') + ->setPrice(100) + ->setVisibility(1) + ->setStockData( + [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_in_stock' => 1, + ] + ) + ->setStatus(1); +$product->setCustomAttribute('flat_attribute', 'flat attribute value'); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_attribute_in_flat_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_attribute_in_flat_rollback.php new file mode 100644 index 000000000000..c1892d504ecc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_attribute_in_flat_rollback.php @@ -0,0 +1,41 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; + +/** @var Magento\Framework\ObjectManagerInterface $objectManager */ +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +try { + /** @var \Magento\Catalog\Api\Data\ProductInterface $product */ + $product = $productRepository->get('simple_with_custom_flat_attribute'); + $productRepository->delete($product); +} catch (NoSuchEntityException $e) { +} + +try { + /** @var \Magento\Catalog\Api\Data\ProductAttributeInterface $attribute */ + $attribute = $attributeRepository->get('flat_attribute'); + $attributeRepository->delete($attribute); +} catch (NoSuchEntityException $e) { +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_options.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_options.php index 059b784978a2..61b549f7729d 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_options.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_options.php @@ -86,7 +86,18 @@ 'sku' => '4-2-radio', ], ] - ] + ], + [ + 'previous_group' => 'text', + 'title' => 'Test Field', + 'type' => 'field', + 'is_require' => 1, + 'sort_order' => 0, + 'price' => 1, + 'price_type' => 'fixed', + 'sku' => '1-text', + 'max_characters' => 100, + ], ]; $options = []; 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 bd5dc541a15a..5facba07d58f 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_full_option_set.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_full_option_set.php @@ -187,6 +187,50 @@ 'price_type' => 'percent', 'sku' => 'sku2', 'max_characters' => 20, + ], + [ + 'title' => 'multiple option', + 'type' => 'multiple', + 'is_require' => true, + 'sort_order' => 7, + 'values' => [ + [ + 'title' => 'multiple option 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'multiple option 1 sku', + 'sort_order' => 1, + ], + [ + 'title' => 'multiple option 2', + 'price' => 20, + 'price_type' => 'fixed', + 'sku' => 'multiple option 2 sku', + 'sort_order' => 2, + ], + ], + ], + [ + 'title' => 'checkbox option', + 'type' => 'checkbox', + 'is_require' => true, + 'sort_order' => 6, + 'values' => [ + [ + 'title' => 'checkbox option 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'checkbox option 1 sku', + 'sort_order' => 1, + ], + [ + 'title' => 'checkbox option 2', + 'price' => 20, + 'price_type' => 'fixed', + 'sku' => 'checkbox option 2 sku', + 'sort_order' => 2, + ], + ], ] ]; diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_image.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_image.php new file mode 100644 index 000000000000..252f99c97b78 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_image.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Api\Data\ProductExtensionInterfaceFactory; +use Magento\Framework\App\Filesystem\DirectoryList; + +\Magento\TestFramework\Helper\Bootstrap::getInstance()->reinitialize(); + +/** @var \Magento\TestFramework\ObjectManager $objectManager */ +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->isObjectNew(true); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setId(1) + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Simple Product') + ->setSku('simple') + ->setPrice(10) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED); + +/** @var $mediaConfig \Magento\Catalog\Model\Product\Media\Config */ +$mediaConfig = $objectManager->get(\Magento\Catalog\Model\Product\Media\Config::class); + +/** @var $mediaDirectory \Magento\Framework\Filesystem\Directory\WriteInterface */ +$mediaDirectory = $objectManager->get(\Magento\Framework\Filesystem::class) + ->getDirectoryWrite(DirectoryList::MEDIA); + +$targetDirPath = $mediaConfig->getBaseMediaPath(); +$targetTmpDirPath = $mediaConfig->getBaseTmpMediaPath(); + +$mediaDirectory->create($targetDirPath); +$mediaDirectory->create($targetTmpDirPath); + +$dist = $mediaDirectory->getAbsolutePath($mediaConfig->getBaseMediaPath() . DIRECTORY_SEPARATOR . 'magento_image.jpg'); +copy(__DIR__ . '/magento_image.jpg', $dist); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_options.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_options.php new file mode 100644 index 000000000000..a401db8eb2bf --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_options.php @@ -0,0 +1,107 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); + +$product->setTypeId( + 'simple' +)->setAttributeSetId( + 4 +)->setWebsiteIds( + [1] +)->setName( + 'Virtual Product With Custom Options' +)->setSku( + 'simple' +)->setPrice( + 10 +)->setMetaTitle( + 'meta title' +)->setMetaKeyword( + 'meta keyword' +)->setMetaDescription( + 'meta description' +)->setVisibility( + \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH +)->setStatus( + \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED +)->setCanSaveCustomOptions( + true +)->setStockData( + [ + 'qty' => 100, + 'is_in_stock' => 1, + 'manage_stock' => 1, + ] +)->setHasOptions(true); + +$options = [ + [ + 'title' => 'test_option_code_1', + 'type' => 'field', + 'is_require' => true, + 'sort_order' => 1, + 'price' => -10.0, + 'price_type' => 'fixed', + 'sku' => 'sku1', + 'max_characters' => 10, + ], + [ + 'title' => 'area option', + 'type' => 'area', + 'is_require' => true, + 'sort_order' => 2, + 'price' => 20.0, + 'price_type' => 'percent', + 'sku' => 'sku2', + 'max_characters' => 20 + ], + [ + 'title' => 'drop_down option', + 'type' => 'drop_down', + 'is_require' => false, + 'sort_order' => 4, + 'values' => [ + [ + 'title' => 'drop_down option 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'drop_down option 1 sku', + 'sort_order' => 1, + ], + [ + 'title' => 'drop_down option 2', + 'price' => 20, + 'price_type' => 'fixed', + 'sku' => 'drop_down option 2 sku', + 'sort_order' => 2, + ], + ], + ] +]; + +$customOptions = []; + +/** @var \Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory $customOptionFactory */ +$customOptionFactory = $objectManager->get(\Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory::class); + +foreach ($options as $option) { + /** @var \Magento\Catalog\Api\Data\ProductCustomOptionInterface $customOption */ + $customOption = $customOptionFactory->create(['data' => $option]); + $customOption->setProductSku($product->getSku()); + + $customOptions[] = $customOption; +} + +$product->setOptions($customOptions); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepositoryFactory */ +$productRepository = $objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_options_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_options_rollback.php new file mode 100644 index 000000000000..8863da1cd278 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_options_rollback.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +/** @var \Magento\Framework\Registry $registry */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ProductRepository::class +); +try { + $product = $repository->get('simple', false, null, true); + $repository->delete($product); +} catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + //Entity already deleted +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_wrong_url_key.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_wrong_url_key.php new file mode 100644 index 000000000000..5012bc947211 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_wrong_url_key.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Simple Product') + ->setSku('simple1') + ->setPrice(10) + ->setDescription('Description with <b>html tag</b>') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setCategoryIds([2]) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) + ->setUrlKey('cuvee-merlot-cabernet-igp-pays-d-oc-frankrijk') + ->save(); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Simple Product 2') + ->setSku('simple2') + ->setPrice(10) + ->setDescription('Description with <b>html tag</b>') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setCategoryIds([2]) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) + ->setUrlKey('normal-url') + ->save(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_wrong_url_key_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_wrong_url_key_rollback.php new file mode 100644 index 000000000000..778fa7b15df3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_wrong_url_key_rollback.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Exception\NoSuchEntityException; + +\Magento\TestFramework\Helper\Bootstrap::getInstance()->getInstance()->reinitialize(); + +/** @var \Magento\Framework\Registry $registry */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); +try { + $product = $productRepository->get('simple1', false, null, true); + $productRepository->delete($product); + $product = $productRepository->get('simple2', false, null, true); + $productRepository->delete($product); +} catch (NoSuchEntityException $e) { +} +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_text_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_text_attribute_rollback.php new file mode 100644 index 000000000000..a9ab0e11312b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_text_attribute_rollback.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/* Delete attribute with text_attribute code */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ +$attribute = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class +); +$attribute->load('text_attribute', 'attribute_code'); +$attribute->delete(); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_virtual.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_virtual.php index 38e8c404dc00..838ae2b9a2aa 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_virtual.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_virtual.php @@ -3,9 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); -/** @var $product \Magento\Catalog\Model\Product */ -$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Catalog\Model\ResourceModel\Product as ProductResource; + +$productFactory = Bootstrap::getObjectManager()->get(ProductInterfaceFactory::class); +$product = $productFactory->create(); $product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_VIRTUAL) ->setId(21) ->setAttributeSetId(4) @@ -22,4 +27,7 @@ 'is_in_stock' => 1, 'manage_stock' => 1, ] - )->save(); + ); +/** @var ProductResource $productResource */ +$productResource = Bootstrap::getObjectManager()->get(ProductResource::class); +$productResource->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_virtual_out_of_stock.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_virtual_out_of_stock.php new file mode 100644 index 000000000000..cff2e83ed002 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_virtual_out_of_stock.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** @var $product \Magento\Catalog\Model\Product */ +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_VIRTUAL) + ->setId(31) + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Virtual Product Out') + ->setSku('virtual-product-out') + ->setPrice(10) + ->setTaxClassId(0) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData(['is_in_stock' => 0]) + ->save(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_virtual_out_of_stock_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_virtual_out_of_stock_rollback.php new file mode 100644 index 000000000000..8055ca4a2556 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_virtual_out_of_stock_rollback.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** @var \Magento\Framework\Registry $registry */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +try { + $product = $productRepository->get('virtual-product-out', 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/Catalog/_files/product_virtual_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_virtual_rollback.php index 479661f26bc5..f5568ced2c96 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_virtual_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_virtual_rollback.php @@ -3,22 +3,27 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); -/** @var \Magento\Framework\Registry $registry */ -$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Exception\StateException; +use Magento\TestFramework\Helper\Bootstrap; + +$registry = Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); $registry->unregister('isSecureArea'); $registry->register('isSecureArea', true); -/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ -$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); +$productRepository = Bootstrap::getObjectManager() + ->get(ProductRepositoryInterface::class); try { $product = $productRepository->get('virtual-product', false, null, true); $productRepository->delete($product); -} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { +} catch (NoSuchEntityException $exception) { //Product already removed +} catch (StateException $exception) { } $registry->unregister('isSecureArea'); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_virtual_with_options.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_virtual_with_options.php new file mode 100644 index 000000000000..c1f981cefa64 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_virtual_with_options.php @@ -0,0 +1,107 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); + +$product->setTypeId( + 'virtual' +)->setAttributeSetId( + 4 +)->setWebsiteIds( + [1] +)->setName( + 'Virtual Product With Custom Options' +)->setSku( + 'virtual' +)->setPrice( + 10 +)->setMetaTitle( + 'meta title' +)->setMetaKeyword( + 'meta keyword' +)->setMetaDescription( + 'meta description' +)->setVisibility( + \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH +)->setStatus( + \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED +)->setCanSaveCustomOptions( + true +)->setStockData( + [ + 'qty' => 100, + 'is_in_stock' => 1, + 'manage_stock' => 1, + ] +)->setHasOptions(true); + +$options = [ + [ + 'title' => 'test_option_code_1', + 'type' => 'field', + 'is_require' => true, + 'sort_order' => 1, + 'price' => -10.0, + 'price_type' => 'fixed', + 'sku' => 'sku1', + 'max_characters' => 10, + ], + [ + 'title' => 'area option', + 'type' => 'area', + 'is_require' => true, + 'sort_order' => 2, + 'price' => 20.0, + 'price_type' => 'percent', + 'sku' => 'sku2', + 'max_characters' => 20 + ], + [ + 'title' => 'drop_down option', + 'type' => 'drop_down', + 'is_require' => false, + 'sort_order' => 4, + 'values' => [ + [ + 'title' => 'drop_down option 1', + 'price' => 10, + 'price_type' => 'fixed', + 'sku' => 'drop_down option 1 sku', + 'sort_order' => 1, + ], + [ + 'title' => 'drop_down option 2', + 'price' => 20, + 'price_type' => 'fixed', + 'sku' => 'drop_down option 2 sku', + 'sort_order' => 2, + ], + ], + ] +]; + +$customOptions = []; + +/** @var \Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory $customOptionFactory */ +$customOptionFactory = $objectManager->get(\Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory::class); + +foreach ($options as $option) { + /** @var \Magento\Catalog\Api\Data\ProductCustomOptionInterface $customOption */ + $customOption = $customOptionFactory->create(['data' => $option]); + $customOption->setProductSku($product->getSku()); + + $customOptions[] = $customOption; +} + +$product->setOptions($customOptions); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepositoryFactory */ +$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_virtual_with_options_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_virtual_with_options_rollback.php new file mode 100644 index 000000000000..f46cdc13d326 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_virtual_with_options_rollback.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +/** @var \Magento\Framework\Registry $registry */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ProductRepository::class +); +try { + $product = $repository->get('virtual', false, null, true); + $repository->delete($product); +} catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + //Entity already deleted +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_two_websites_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_two_websites_rollback.php index 141920afbdc2..987ee96ce6b0 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_two_websites_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_two_websites_rollback.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -use Magento\Catalog\Api\Data\ProductInterface; - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); /** @var \Magento\Framework\Registry $registry */ @@ -15,16 +13,18 @@ $registry->register("isSecureArea", true); /** @var Magento\Store\Model\Website $website */ -$website = $objectManager->create(\Magento\Store\Model\Website::class); -$website->load('second_website'); -$website->delete(); +$website = $objectManager->get(Magento\Store\Model\Website::class); +$website->load('second_website', 'code'); +if ($website->getId()) { + $website->delete(); +} /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ $productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); try { $firstProduct = $productRepository->get('unique-simple-azaza'); - $firstProduct->delete(); + $productRepository->delete($firstProduct); } catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { //Product already removed } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_in_different_stores.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_in_different_stores.php new file mode 100644 index 000000000000..6102738bd6be --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_in_different_stores.php @@ -0,0 +1,122 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\Indexer\IndexerRegistry; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Api\ProductRepositoryInterface; + +require __DIR__ . '/../../Store/_files/core_fixturestore.php'; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var Magento\Store\Model\Website $website */ +$website = $objectManager->get(Magento\Store\Model\Website::class); + +$website->setData( + [ + 'code' => 'second_website', + 'name' => 'Test Website', + ] +); + +$website->save(); + +$objectManager->get(\Magento\Store\Model\StoreManagerInterface::class)->reinitStores(); + +/** @var IndexerRegistry $indexerRegistry */ +$indexerRegistry = $objectManager->create(IndexerRegistry::class); +$indexer = $indexerRegistry->get('catalogsearch_fulltext'); + +$indexer->reindexAll(); + +$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category->isObjectNew(true); +$category->setId(96377) + ->setName('Category 1') + ->setParentId(2) + ->setPath('1/2/96377') + ->setLevel(2) + ->setAvailableSortBy('name') + ->setDefaultSortBy('name') + ->setIsActive(true) + ->setPosition(1) + ->save(); + +/** @var $product Product */ +$product = $objectManager->create(Product::class); +$product->isObjectNew(true); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Simple Product') + ->setSku('simple_1') + ->setPrice(10) + ->setWeight(1) + ->setShortDescription("Short description") + ->setTaxClassId(0) + ->setDescription('Description with <b>html tag</b>') + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setCategoryIds([96377]); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$productRepository->save($product); + +/** @var $product Product */ +$product = $objectManager->create(Product::class); +$product->isObjectNew(true); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId(4) + ->setWebsiteIds([$website->getId()]) + ->setName('Simple Product 2') + ->setSku('simple_2') + ->setPrice(10) + ->setWeight(1) + ->setShortDescription("Short description") + ->setTaxClassId(0) + ->setDescription('Description with <b>html tag</b>') + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setCategoryIds([96377]); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$productRepository->save($product); + +/** @var $product Product */ +$product = $objectManager->create(Product::class); +$product->isObjectNew(true); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId(4) + ->setWebsiteIds([1, $website->getId()]) + ->setName('Simple Product 3') + ->setSku('simple_3') + ->setPrice(10) + ->setWeight(1) + ->setShortDescription("Short description") + ->setTaxClassId(0) + ->setDescription('Description with <b>html tag</b>') + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setCategoryIds([96377]); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_in_different_stores_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_in_different_stores_rollback.php new file mode 100644 index 000000000000..9b957b75eb2a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_in_different_stores_rollback.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +require __DIR__ . '/../../Store/_files/core_fixturestore_rollback.php'; + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +/** @var \Magento\Framework\Registry $registry */ +$registry = $objectManager->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +//Remove category +/** @var $category \Magento\Catalog\Model\Category */ +$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category->load(96377); +if ($category->getId()) { + $category->delete(); +} + +$productSkuList = ['simple_1', 'simple_2', 'simple_3']; +foreach ($productSkuList as $sku) { + try { + $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + $product = $productRepository->get($sku, true); + if ($product->getId()) { + $productRepository->delete($product); + } + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + //Product already removed + } +} + +/** @var Magento\Store\Model\Website $website */ +$website = $objectManager->get(Magento\Store\Model\Website::class); +$website->load('second_website', 'code'); +if ($website->getId()) { + $website->delete(); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_new.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_new.php index 7d6e2e6f9780..2cd0dd2c7756 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_new.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_new.php @@ -15,8 +15,8 @@ ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) ->setWebsiteIds([1]) ->setStockData(['qty' => 100, 'is_in_stock' => 1, 'manage_stock' => 1]) - ->setNewsFromDate(date('Y-m-d', strtotime('-2 day'))) - ->setNewsToDate(date('Y-m-d', strtotime('+2 day'))) + ->setNewsFromDate(date('Y-m-d H:i:s', strtotime('-2 day'))) + ->setNewsToDate(date('Y-m-d H:i:s', strtotime('+2 day'))) ->setDescription('description') ->setShortDescription('short desc') ->save(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_dropdown_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_dropdown_attribute.php new file mode 100644 index 000000000000..1dca8c383cc7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_dropdown_attribute.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +/** + * Create multiselect attribute + */ +require __DIR__ . '/dropdown_attribute.php'; + +/** Create products with attribute option of dropdown type */ + +/** @var $installer \Magento\Catalog\Setup\CategorySetup */ +$installer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Setup\CategorySetup::class +); + +/** @var $options \Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection */ +$options = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection::class +); +$options->setAttributeFilter($attribute->getId()); +$optionIds = $options->getAllIds(); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setId($optionIds[0] * 10) + ->setAttributeSetId($installer->getAttributeSetId('catalog_product', 'Default')) + ->setWebsiteIds([1]) + ->setName('With Option 1') + ->setSku('simple_op_1') + ->setPrice(10) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setDropdownAttribute($optionIds[0]) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) + ->save(); + +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setId($optionIds[1] * 10) + ->setAttributeSetId($installer->getAttributeSetId('catalog_product', 'Default')) + ->setWebsiteIds([1]) + ->setName('With Option 2') + ->setSku('simple_op_2') + ->setPrice(10) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setDropdownAttribute($optionIds[1]) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) + ->save(); + +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setId($optionIds[2] * 10) + ->setAttributeSetId($installer->getAttributeSetId('catalog_product', 'Default')) + ->setWebsiteIds([1]) + ->setName('With Option 3') + ->setSku('simple_op_3') + ->setPrice(10) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setDropdownAttribute($optionIds[2]) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) + ->save(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_dropdown_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_dropdown_attribute_rollback.php new file mode 100644 index 000000000000..07570894b87b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_dropdown_attribute_rollback.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +/** + * Remove all products as strategy of isolation process + */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get('Magento\Framework\Registry'); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var $productCollection \Magento\Catalog\Model\ResourceModel\Product */ +$productCollection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create('Magento\Catalog\Model\Product') + ->getCollection(); + +foreach ($productCollection as $product) { + $product->delete(); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_multiselect_attribute_with_source_model.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_multiselect_attribute_with_source_model.php new file mode 100644 index 000000000000..a1e81fd3b40a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_multiselect_attribute_with_source_model.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\_files\MultiselectSourceMock; + +/** + * Create multiselect attribute + */ +require __DIR__ . '/multiselect_attribute_with_source_model.php'; +require __DIR__ . '/../../Checkout/_files/ValidatorFileMock.php'; + +/** Create product with options and multiselect attribute */ + +/** @var $installer \Magento\Catalog\Setup\CategorySetup */ +$installer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Setup\CategorySetup::class +); + +/** @var $sourceModel MultiselectSourceMock */ +$sourceModel = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + MultiselectSourceMock::class +); +$options = $sourceModel->getAllOptions(); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setId($options[0]['value'] * 10) + ->setAttributeSetId($installer->getAttributeSetId('catalog_product', 'Default')) + ->setWebsiteIds([1]) + ->setName('With Multiselect Source Model 1') + ->setSku('simple_mssm_1') + ->setPrice(10) + ->setDescription('Hello " &" Bring the water bottle when you can!') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setMultiselectAttrWithSource([$options[0]['value']]) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) + ->save(); + +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setId($options[1]['value'] * 10) + ->setAttributeSetId($installer->getAttributeSetId('catalog_product', 'Default')) + ->setWebsiteIds([1]) + ->setName('With Multiselect Source Model 2') + ->setSku('simple_mssm_2') + ->setPrice(10) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setMultiselectAttrWithSource([$options[1]['value'], $options[2]['value'], $options[3]['value']]) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) + ->save(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_multiselect_attribute_with_source_model_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_multiselect_attribute_with_source_model_rollback.php new file mode 100644 index 000000000000..a8121a7913e5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_multiselect_attribute_with_source_model_rollback.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +require __DIR__ . '/multiselect_attribute_with_source_model_rollback.php'; + +use Magento\Framework\Indexer\IndexerRegistry; + +/** + * Remove all products as strategy of isolation process + */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get('Magento\Framework\Registry'); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var $productCollection \Magento\Catalog\Model\ResourceModel\Product */ +$productCollection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create('Magento\Catalog\Model\Product') + ->getCollection(); + +foreach ($productCollection as $product) { + $product->delete(); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +\Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(IndexerRegistry::class) + ->get(Magento\CatalogInventory\Model\Indexer\Stock\Processor::INDEXER_ID) + ->reindexAll(); 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 000000000000..1870eaba566d --- /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 000000000000..fba12f19fdca --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/quantity_and_stock_status_attribute_used_in_grid_rollback.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +$eavSetupFactory = $objectManager->create(\Magento\Eav\Setup\EavSetupFactory::class); +/** @var \Magento\Eav\Setup\EavSetup $eavSetup */ +$eavSetup = $eavSetupFactory->create(); +$eavSetup->updateAttribute( + \Magento\Catalog\Model\Product::ENTITY, + 'quantity_and_stock_status', + [ + 'is_used_in_grid' => 0, + ] +); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/text_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/text_attribute_rollback.php deleted file mode 100644 index cbc0476efd1b..000000000000 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/text_attribute_rollback.php +++ /dev/null @@ -1,18 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -/* Delete attribute with text_attribute code */ -$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get('Magento\Framework\Registry'); -$registry->unregister('isSecureArea'); -$registry->register('isSecureArea', true); -/** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ -$attribute = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - 'Magento\Catalog\Model\ResourceModel\Eav\Attribute' -); -$attribute->load('text_attribute', 'attribute_code'); -$attribute->delete(); - -$registry->unregister('isSecureArea'); -$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/AbstractProductExportImportTestCase.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/AbstractProductExportImportTestCase.php index b562879b319d..d3a2e4c53f24 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/AbstractProductExportImportTestCase.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/AbstractProductExportImportTestCase.php @@ -55,6 +55,16 @@ abstract class AbstractProductExportImportTestCase extends \PHPUnit\Framework\Te 'is_salable', // stock indexation is not performed during import ]; + /** + * @var array + */ + private static $attributesToRefresh = [ + 'tax_class_id', + ]; + + /** + * @inheritdoc + */ protected function setUp() { $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); @@ -65,12 +75,17 @@ protected function setUp() \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType::$commonAttributesCache = []; } + /** + * @inheritdoc + */ protected function tearDown() { - $this->executeRollbackFixtures($this->fixtures); + $this->executeFixtures($this->fixtures, true); } /** + * Run import/export tests. + * * @magentoAppArea adminhtml * @magentoDbIsolation disabled * @magentoAppIsolation enabled @@ -78,36 +93,60 @@ protected function tearDown() * @param array $fixtures * @param string[] $skus * @param string[] $skippedAttributes + * @return void * @dataProvider exportImportDataProvider */ - public function testExport($fixtures, $skus, $skippedAttributes = []) + public function testImportExport(array $fixtures, array $skus, array $skippedAttributes = []): void { $this->fixtures = $fixtures; - $this->executeFixtures($fixtures, $skus); + $this->executeFixtures($fixtures); $this->modifyData($skus); $skippedAttributes = array_merge(self::$skippedAttributes, $skippedAttributes); - $this->executeExportTest($skus, $skippedAttributes); + $csvFile = $this->executeExportTest($skus, $skippedAttributes); + + $this->executeImportReplaceTest($skus, $skippedAttributes, false, $csvFile); + $this->executeImportReplaceTest($skus, $skippedAttributes, true, $csvFile); + $this->executeImportDeleteTest($skus, $csvFile); } - abstract public function exportImportDataProvider(); + /** + * Provide data for import/export. + * + * @return array + */ + abstract public function exportImportDataProvider(): array; /** + * Modify data. + * * @param array $skus + * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - protected function modifyData($skus) + protected function modifyData(array $skus): void { } /** + * Prepare product. + * * @param \Magento\Catalog\Model\Product $product + * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function prepareProduct($product) + public function prepareProduct(\Magento\Catalog\Model\Product $product): void { } - protected function executeExportTest($skus, $skippedAttributes) + /** + * Execute export test. + * + * @param array $skus + * @param array $skippedAttributes + * @return string + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + protected function executeExportTest(array $skus, array $skippedAttributes): string { $index = 0; $ids = []; @@ -140,10 +179,23 @@ protected function executeExportTest($skus, $skippedAttributes) $this->assertEqualsSpecificAttributes($origProducts[$index], $newProduct); } + + return $csvfile; } - private function assertEqualsOtherThanSkippedAttributes($expected, $actual, $skippedAttributes) - { + /** + * Assert data equals (ignore skipped attributes). + * + * @param array $expected + * @param array $actual + * @param array $skippedAttributes + * @return void + */ + private function assertEqualsOtherThanSkippedAttributes( + array $expected, + array $actual, + array $skippedAttributes + ): void { foreach ($expected as $key => $value) { if (is_object($value) || in_array($key, $skippedAttributes)) { continue; @@ -158,134 +210,93 @@ private function assertEqualsOtherThanSkippedAttributes($expected, $actual, $ski } /** - * @magentoAppArea adminhtml - * @magentoDbIsolation disabled - * @magentoAppIsolation enabled + * Execute import test with delete behavior. * - * @param array $fixtures - * @param string[] $skus - * @dataProvider exportImportDataProvider - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @param array $skus + * @param string|null $csvFile + * @return void */ - public function testImportDelete($fixtures, $skus, $skippedAttributes = []) - { - $this->fixtures = $fixtures; - $this->executeFixtures($fixtures, $skus); - $this->modifyData($skus); - $this->executeImportDeleteTest($skus); - } - - protected function executeImportDeleteTest($skus) + protected function executeImportDeleteTest(array $skus, string $csvFile = null): void { - $csvfile = $this->exportProducts(); - $this->importProducts($csvfile, \Magento\ImportExport\Model\Import::BEHAVIOR_DELETE); - /** @var \Magento\Catalog\Model\Product $product */ - $product = $this->objectManager->create(\Magento\Catalog\Model\Product::class); + $csvFile = $csvFile ?? $this->exportProducts(); + $this->importProducts($csvFile, \Magento\ImportExport\Model\Import::BEHAVIOR_DELETE); foreach ($skus as $sku) { $productId = $this->productResource->getIdBySku($sku); - $product->load($productId); - $this->assertNull($product->getId()); + $this->assertFalse($productId); } } /** - * Execute fixtures + * Execute fixtures. * - * @param array $skus * @param array $fixtures + * @param bool $rollback * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - protected function executeFixtures($fixtures, $skus = []) + protected function executeFixtures(array $fixtures, bool $rollback = false) { foreach ($fixtures as $fixture) { - $fixturePath = $this->fileSystem->getDirectoryRead(DirectoryList::ROOT) - ->getAbsolutePath('/dev/tests/integration/testsuite/' . $fixture); + $fixturePath = $this->resolveFixturePath($fixture, $rollback); include $fixturePath; } } /** - * Execute rollback fixtures + * Resolve fixture path. * - * @param array $fixtures - * @return void + * @param string $fixture + * @param bool $rollback + * @return string */ - private function executeRollbackFixtures($fixtures) + private function resolveFixturePath(string $fixture, bool $rollback = false) { - foreach ($fixtures as $fixture) { - $fixturePath = $this->fileSystem->getDirectoryRead(DirectoryList::ROOT) - ->getAbsolutePath('/dev/tests/integration/testsuite/' . $fixture); + $fixturePath = $this->fileSystem->getDirectoryRead(DirectoryList::ROOT) + ->getAbsolutePath('/dev/tests/integration/testsuite/' . $fixture); + if ($rollback) { $fileInfo = pathinfo($fixturePath); $extension = ''; if (isset($fileInfo['extension'])) { $extension = '.' . $fileInfo['extension']; } - $rollbackfixturePath = $fileInfo['dirname'] . '/' . $fileInfo['filename'] . '_rollback' . $extension; - if (file_exists($rollbackfixturePath)) { - include $rollbackfixturePath; - } + $fixturePath = $fileInfo['dirname'] . '/' . $fileInfo['filename'] . '_rollback' . $extension; } + + return $fixturePath; } /** + * Assert that specific attributes equal. + * * @param \Magento\Catalog\Model\Product $expectedProduct * @param \Magento\Catalog\Model\Product $actualProduct * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - protected function assertEqualsSpecificAttributes($expectedProduct, $actualProduct) - { + protected function assertEqualsSpecificAttributes( + \Magento\Catalog\Model\Product $expectedProduct, + \Magento\Catalog\Model\Product $actualProduct + ): void { // check custom options } /** - * @magentoAppArea adminhtml - * @magentoDbIsolation disabled - * @magentoAppIsolation enabled + * Execute import test with replace behavior. * - * @param array $fixtures - * @param string[] $skus - * @param string[] $skippedAttributes - * @dataProvider importReplaceDataProvider - */ - public function testImportReplace($fixtures, $skus, $skippedAttributes = []) - { - $this->fixtures = $fixtures; - $this->executeFixtures($fixtures, $skus); - $this->modifyData($skus); - $skippedAttributes = array_merge(self::$skippedAttributes, $skippedAttributes); - $this->executeImportReplaceTest($skus, $skippedAttributes); - } - - /** - * @magentoAppArea adminhtml - * @magentoDbIsolation disabled - * @magentoAppIsolation enabled - * - * @param array $fixtures - * @param string[] $skus - * @param string[] $skippedAttributes - * @dataProvider importReplaceDataProvider - */ - public function testImportReplaceWithPagination($fixtures, $skus, $skippedAttributes = []) - { - $this->fixtures = $fixtures; - $this->executeFixtures($fixtures, $skus); - $this->modifyData($skus); - $skippedAttributes = array_merge(self::$skippedAttributes, $skippedAttributes); - $this->executeImportReplaceTest($skus, $skippedAttributes, true); - } - - /** * @param string[] $skus * @param string[] $skippedAttributes * @param bool $usePagination - * + * @param string|null $csvfile + * @return void + * @throws \Magento\Framework\Exception\NoSuchEntityException * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - protected function executeImportReplaceTest($skus, $skippedAttributes, $usePagination = false) - { + protected function executeImportReplaceTest( + $skus, + $skippedAttributes, + $usePagination = false, + string $csvfile = null + ) { $replacedAttributes = [ 'row_id', 'entity_id', @@ -293,6 +304,7 @@ protected function executeImportReplaceTest($skus, $skippedAttributes, $usePagin 'media_gallery' ]; $skippedAttributes = array_merge($replacedAttributes, $skippedAttributes); + $this->cleanAttributesCache(); $index = 0; $ids = []; @@ -316,15 +328,15 @@ protected function executeImportReplaceTest($skus, $skippedAttributes, $usePagin $itemsPerPageProperty->setValue($exportProduct, 1); } - $csvfile = $this->exportProducts($exportProduct); + $csvfile = $csvfile ?? $this->exportProducts($exportProduct); $this->importProducts($csvfile, \Magento\ImportExport\Model\Import::BEHAVIOR_REPLACE); while ($index > 0) { $index--; $newProduct = $productRepository->get($skus[$index], false, Store::DEFAULT_STORE_ID, true); // check original product is deleted - $origProduct = $this->objectManager->create(\Magento\Catalog\Model\Product::class)->load($ids[$index]); - $this->assertNull($origProduct->getId()); + $productId = $this->productResource->getIdBySku($ids[$index]); + $this->assertFalse($productId); // check new product data // @todo uncomment or remove after MAGETWO-49806 resolved @@ -342,7 +354,7 @@ protected function executeImportReplaceTest($skus, $skippedAttributes, $usePagin array_filter($origProductData[$attribute]) : $origProductData[$attribute]; if (!empty($expected)) { - $actual = isset($newProductData[$attribute]) ? $newProductData[$attribute] : null; + $actual = $newProductData[$attribute] ?? null; $actual = is_array($actual) ? array_filter($actual) : $actual; $this->assertNotEquals($expected, $actual, $attribute . ' is expected to be changed'); } @@ -352,7 +364,7 @@ protected function executeImportReplaceTest($skus, $skippedAttributes, $usePagin } /** - * Export products in the system + * Export products in the system. * * @param \Magento\CatalogImportExport\Model\Export\Product|null $exportProduct * @return string Return exported file name @@ -371,17 +383,18 @@ private function exportProducts(\Magento\CatalogImportExport\Model\Export\Produc ) ); $this->assertNotEmpty($exportProduct->export()); + return $csvfile; } /** - * Import products from the given file + * Import products from the given file. * * @param string $csvfile * @param string $behavior * @return void */ - private function importProducts($csvfile, $behavior) + private function importProducts(string $csvfile, string $behavior): void { /** @var \Magento\CatalogImportExport\Model\Import\Product $importModel */ $importModel = $this->objectManager->create( @@ -437,15 +450,33 @@ private function importProducts($csvfile, $behavior) } /** + * Extract error message. + * * @param \Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError[] $errors * @return string */ - private function extractErrorMessage($errors) + private function extractErrorMessage(array $errors): string { $errorMessage = ''; foreach ($errors as $error) { $errorMessage = "\n" . $error->getErrorMessage(); } + return $errorMessage; } + + /** + * Clean import attribute cache. + * + * @return void + */ + private function cleanAttributesCache(): void + { + foreach (self::$attributesToRefresh as $attributeCode) { + $attributeId = Import\Product\Type\AbstractType::$attributeCodeToId[$attributeCode] ?? null; + if ($attributeId !== null) { + unset(Import\Product\Type\AbstractType::$commonAttributesCache[$attributeId]); + } + } + } } diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php index 70e0cf0e1e74..c212d4c0d971 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php @@ -70,7 +70,7 @@ protected function setUp() /** * @magentoDataFixture Magento/CatalogImportExport/_files/product_export_data.php - * @magentoDbIsolationEnabled + * @magentoDbIsolation enabled */ public function testExport() { @@ -95,7 +95,7 @@ public function testExport() /** * @magentoDataFixture Magento/CatalogImportExport/_files/product_export_data_special_chars.php - * @magentoDbIsolationEnabled + * @magentoDbIsolation enabled */ public function testExportSpecialChars() { @@ -111,7 +111,7 @@ public function testExportSpecialChars() /** * @magentoDataFixture Magento/CatalogImportExport/_files/product_export_with_product_links_data.php - * @magentoDbIsolationEnabled + * @magentoDbIsolation enabled */ public function testExportWithProductLinks() { diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php index c5e704c2434b..67446960e15d 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php @@ -34,6 +34,7 @@ * @magentoDataFixtureBeforeTransaction Magento/Catalog/_files/enable_catalog_product_reindex_schedule.php * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * phpcs:disable Generic.PHP.NoSilencedErrors, Generic.Metrics.NestingLevel, Magento2.Functions.StaticFunction */ class ProductTest extends \Magento\TestFramework\Indexer\TestCase { @@ -431,7 +432,7 @@ public function getBehaviorDataProvider(): array 'Append behavior with new product' => [ 'importFile' => 'product_with_custom_options_new.csv', 'sku' => 'simple_new', - 'expectedOptionsQty' => 4, + 'expectedOptionsQty' => 5, ], ]; } @@ -567,6 +568,7 @@ public function testSaveDatetimeAttribute() */ protected function getExpectedOptionsData(string $pathToFile, string $storeCode = ''): array { + // phpcs:disable Magento2.Functions.DiscouragedFunction $productData = $this->csvToArray(file_get_contents($pathToFile)); $expectedOptionId = 0; $expectedOptions = []; @@ -762,7 +764,6 @@ protected function getOptionValues(\Magento\Catalog\Model\Product\Option $option /** * Test that product import with images works properly * - * @magentoDataIsolation enabled * @magentoDataFixture mediaImportImageFixture * @magentoAppIsolation enabled * @SuppressWarnings(PHPMD.ExcessiveMethodLength) @@ -813,7 +814,6 @@ public function testSaveMediaImage() /** * Test that errors occurred during importing images are logged. * - * @magentoDataIsolation enabled * @magentoAppIsolation enabled * @magentoDataFixture mediaImportImageFixture * @magentoDataFixture mediaImportImageFixtureError @@ -958,6 +958,7 @@ public function testInvalidSkuLink() $errors = $this->_model->setParameters( [ 'behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND, + Import::FIELD_NAME_VALIDATION_STRATEGY => null, 'entity' => 'catalog_product' ] )->setSource( @@ -1292,12 +1293,10 @@ public function categoryTestDataProvider() * @magentoAppArea adminhtml * @magentoDbIsolation disabled * @magentoAppIsolation enabled - * @magentoDataFixture Magento/Catalog/_files/category_duplicates.php + * @magentoDataFixture Magento/CatalogImportExport/_files/update_category_duplicates.php */ public function testProductDuplicateCategories() { - $this->markTestSkipped('Due to MAGETWO-48956'); - $csvFixture = 'products_duplicate_category.csv'; // import data from CSV file $pathToFile = __DIR__ . '/_files/' . $csvFixture; @@ -1322,19 +1321,6 @@ public function testProductDuplicateCategories() $this->assertTrue($errors->getErrorsCount() === 0); - /** @var \Magento\Catalog\Model\Category $category */ - $category = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Category::class - ); - - $category->load(444); - - $this->assertTrue($category !== null); - - $category->setName( - 'Category 2-updated' - )->save(); - $this->_model->importData(); $errorProcessor = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( @@ -1566,6 +1552,68 @@ public function testExistingProductWithUrlKeys() } } + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple_with_wrong_url_key.php + * @magentoDbIsolation disabled + * @magentoAppIsolation enabled + */ + public function testAddUpdateProductWithInvalidUrlKeys() : void + { + $products = [ + 'simple1' => 'cuvee-merlot-cabernet-igp-pays-d-oc-frankrijk', + 'simple2' => 'normal-url', + 'simple3' => 'some-wrong-url' + ]; + $filesystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Framework\Filesystem::class); + $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); + $source = $this->objectManager->create( + \Magento\ImportExport\Model\Import\Source\Csv::class, + [ + 'file' => __DIR__ . '/_files/products_to_import_with_invalid_url_keys.csv', + 'directory' => $directory + ] + ); + + $errors = $this->_model->setParameters( + ['behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_ADD_UPDATE, 'entity' => 'catalog_product'] + )->setSource( + $source + )->validateData(); + + $this->assertTrue($errors->getErrorsCount() == 0); + $this->_model->importData(); + + $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Api\ProductRepositoryInterface::class + ); + foreach ($products as $productSku => $productUrlKey) { + $this->assertEquals($productUrlKey, $productRepository->get($productSku)->getUrlKey()); + } + } + + /** + * Make sure the non existing image in the csv file won't erase the qty key of the existing products. + * + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + */ + public function testImportWithNonExistingImage() + { + $products = [ + 'simple_new' => 100, + ]; + + $this->importFile('products_to_import_with_non_existing_image.csv'); + + $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + foreach ($products as $productSku => $productQty) { + $product = $productRepository->get($productSku); + $stockItem = $product->getExtensionAttributes()->getStockItem(); + $this->assertEquals($productQty, $stockItem->getQty()); + } + } + /** * @magentoDataFixture Magento/Catalog/_files/product_simple_with_url_key.php * @magentoDbIsolation disabled @@ -1603,6 +1651,45 @@ public function testImportWithoutUrlKeys() } } + /** + * Make sure the absence of a url_key column in the csv file won't erase the url key of the existing products. + * To reach the goal we need to not send the name column, as the url key is generated from it. + * + * @magentoDataFixture Magento/Catalog/_files/product_simple_with_url_key.php + * @magentoDbIsolation disabled + * @magentoAppIsolation enabled + */ + public function testImportWithoutUrlKeysAndName() + { + $products = [ + 'simple1' => 'url-key', + 'simple2' => 'url-key2', + ]; + $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); + $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); + $source = $this->objectManager->create( + \Magento\ImportExport\Model\Import\Source\Csv::class, + [ + 'file' => __DIR__ . '/_files/products_to_import_without_url_keys_and_name.csv', + 'directory' => $directory + ] + ); + + $errors = $this->_model->setParameters( + ['behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND, 'entity' => 'catalog_product'] + ) + ->setSource($source) + ->validateData(); + + $this->assertTrue($errors->getErrorsCount() == 0); + $this->_model->importData(); + + $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + foreach ($products as $productSku => $productUrlKey) { + $this->assertEquals($productUrlKey, $productRepository->get($productSku)->getUrlKey()); + } + } + /** * @magentoAppIsolation enabled */ @@ -1718,6 +1805,7 @@ function (ProductInterface $item) { if ($product->getId()) { $productRepository->delete($product); } + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { //Product already removed } @@ -2147,7 +2235,6 @@ public function testImportWithDifferentSkuCase() /** * Test that product import with images for non-default store works properly. * - * @magentoDataIsolation enabled * @magentoDataFixture mediaImportImageFixture * @magentoAppIsolation enabled */ @@ -2210,6 +2297,20 @@ public function testImportWithBackordersEnabled(): void $this->assertFalse($product->getDataByKey('quantity_and_stock_status')['is_in_stock']); } + /** + * Test that imported product stock status with stock quantity > 0 and backorders functionality disabled + * can be set to 'out of stock'. + * + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + */ + public function testImportWithBackordersDisabled(): void + { + $this->importFile('products_to_import_with_backorders_disabled_and_not_0_qty.csv'); + $product = $this->getProductBySku('simple_new'); + $this->assertFalse($product->getDataByKey('quantity_and_stock_status')['is_in_stock']); + } + /** * Import file by providing import filename in parameters. * diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/product_with_custom_options_new.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/product_with_custom_options_new.csv index 7fe8832cd580..f276a96cd1d3 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/product_with_custom_options_new.csv +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/product_with_custom_options_new.csv @@ -1,2 +1,2 @@ sku,store_view_code,attribute_set_code,product_type,categories,product_websites,name,description,short_description,weight,product_online,tax_class_name,visibility,price,special_price,special_price_from_date,special_price_to_date,url_key,meta_title,meta_keywords,meta_description,base_image,base_image_label,small_image,small_image_label,thumbnail_image,thumbnail_image_label,created_at,updated_at,new_from_date,new_to_date,display_product_options_in,map_price,msrp_price,map_enabled,gift_message_available,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_display_actual_price_type,country_of_manufacture,additional_attributes,qty,out_of_stock_qty,use_config_min_qty,is_qty_decimal,allow_backorders,use_config_backorders,min_cart_qty,use_config_min_sale_qty,max_cart_qty,use_config_max_sale_qty,is_in_stock,notify_on_stock_below,use_config_notify_stock_qty,manage_stock,use_config_manage_stock,use_config_qty_increments,qty_increments,use_config_enable_qty_inc,enable_qty_increments,is_decimal_divided,website_id,related_skus,crosssell_skus,upsell_skus,additional_images,additional_image_labels,hide_from_product_page,custom_options,bundle_price_type,bundle_sku_type,bundle_price_view,bundle_weight_type,bundle_values,associated_skus -simple_new,,Default,simple,,base,"New Product",,,,1,"Taxable Goods","Catalog, Search",10.0000,,,,new-product,"New Product","New Product","New Product ",,,,,,,"2015-10-20 07:05:38","2015-10-20 07:05:38",,,"Block after Info Column",,,,,,,,,,,,,"has_options=1,quantity_and_stock_status=In Stock,required_options=1",100.0000,0.0000,1,0,0,1,1.0000,1,10000.0000,1,1,1.0000,1,1,0,1,1.0000,0,0,0,1,,,,,,,"name=New Radio,type=radio,required=1,price=3.0000,price_type=fixed,sku=4-1-radio,option_title=Option 1|name=New Radio,type=radio,required=1,price=3.0000,price_type=fixed,sku=4-2-radio,option_title=Option 2|name=New Select,type=drop_down,required=1,price=3.0000,price_type=fixed,sku=3-1-select,option_title=Option 1|name=New Select,type=drop_down,required=1,price=3.0000,price_type=fixed,sku=3-2-select,option_title=Option2|name=Test Date and Time Title,type=date_time,required=1,price=2.0000,price_type=fixed,sku=2-date|name=Test Field Title,type=field,required=1,price=0.0000,price_type=fixed,sku=1-text,max_characters=10",,,,,, +simple_new,,Default,simple,,base,"New Product",,,,1,"Taxable Goods","Catalog, Search",10.0000,,,,new-product,"New Product","New Product","New Product ",,,,,,,"2015-10-20 07:05:38","2015-10-20 07:05:38",,,"Block after Info Column",,,,,,,,,,,,,"has_options=1,quantity_and_stock_status=In Stock,required_options=1",100.0000,0.0000,1,0,0,1,1.0000,1,10000.0000,1,1,1.0000,1,1,0,1,1.0000,0,0,0,1,,,,,,,"name=New Radio,type=radio,required=1,price=3.0000,price_type=fixed,sku=4-1-radio,option_title=Option 1|name=New Radio,type=radio,required=1,price=3.0000,price_type=fixed,sku=4-2-radio,option_title=Option 2|name=New Select,type=drop_down,required=1,price=3.0000,price_type=fixed,sku=3-1-select,option_title=Option 1|name=New Select,type=drop_down,required=1,price=3.0000,price_type=fixed,sku=3-2-select,option_title=Option2|name=Test Date and Time Title,type=date_time,required=1,price=2.0000,price_type=fixed,sku=2-date|name=Test Field Title,type=field,required=1,price=0.0000,price_type=fixed,sku=1-text,max_characters=10|name=New Select With Zero Price,type=drop_down,required=1,price=0,price_type=fixed,sku=3-1-select,option_title=Option 1",,,,,, diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_backorders_disabled_and_not_0_qty.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_backorders_disabled_and_not_0_qty.csv new file mode 100644 index 000000000000..b22427a8af12 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_backorders_disabled_and_not_0_qty.csv @@ -0,0 +1,2 @@ +sku,store_view_code,attribute_set_code,product_type,categories,product_websites,name,description,short_description,weight,product_online,tax_class_name,visibility,price,special_price,special_price_from_date,special_price_to_date,url_key,meta_title,meta_keywords,meta_description,base_image,base_image_label,small_image,small_image_label,thumbnail_image,thumbnail_image_label,created_at,updated_at,new_from_date,new_to_date,display_product_options_in,map_price,msrp_price,map_enabled,gift_message_available,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_display_actual_price_type,country_of_manufacture,additional_attributes,qty,out_of_stock_qty,use_config_min_qty,is_qty_decimal,allow_backorders,use_config_backorders,min_cart_qty,use_config_min_sale_qty,max_cart_qty,use_config_max_sale_qty,is_in_stock,notify_on_stock_below,use_config_notify_stock_qty,manage_stock,use_config_manage_stock,use_config_qty_increments,qty_increments,use_config_enable_qty_inc,enable_qty_increments,is_decimal_divided,website_id,related_skus,crosssell_skus,upsell_skus,additional_images,additional_image_labels,hide_from_product_page,custom_options,bundle_price_type,bundle_sku_type,bundle_price_view,bundle_weight_type,bundle_values,associated_skus +simple_new,,Default,simple,,base,New Product,,,,1,Taxable Goods,"Catalog, Search",10,,,,new-product,New Product,New Product,New Product ,,,,,,,10/20/2015 7:05,10/20/2015 7:05,,,Block after Info Column,,,,,,,,,,,,,"has_options=1,quantity_and_stock_status=In Stock,required_options=1",100,0,1,0,0,0,1,1,10000,1,0,1,1,1,0,1,1,0,0,0,1,,,,,,,,,,,,, diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_invalid_url_keys.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_invalid_url_keys.csv new file mode 100644 index 000000000000..25bb95ab9e0f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_invalid_url_keys.csv @@ -0,0 +1,4 @@ +sku,product_type,store_view_code,name,price,attribute_set_code,url_key +simple1,simple,,"simple 1",25,Default,cuvée merlot-cabernet igp pays d'oc frankrijk +simple2,simple,,"simple 2",34,Default,normal-url +simple3,simple,,"simple 3",58,Default,some!wrong'url diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_non_existing_image.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_non_existing_image.csv new file mode 100644 index 000000000000..8122433a8c9e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_non_existing_image.csv @@ -0,0 +1,2 @@ +sku,store_view_code,attribute_set_code,product_type,categories,product_websites,name,description,short_description,weight,product_online,tax_class_name,visibility,price,special_price,special_price_from_date,special_price_to_date,url_key,meta_title,meta_keywords,meta_description,base_image,base_image_label,small_image,small_image_label,thumbnail_image,thumbnail_image_label,swatch_image,swatch_image_label1,created_at,updated_at,new_from_date,new_to_date,display_product_options_in,map_price,msrp_price,map_enabled,gift_message_available,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_display_actual_price_type,country_of_manufacture,additional_attributes,qty,out_of_stock_qty,use_config_min_qty,is_qty_decimal,allow_backorders,use_config_backorders,min_cart_qty,use_config_min_sale_qty,max_cart_qty,use_config_max_sale_qty,is_in_stock,notify_on_stock_below,use_config_notify_stock_qty,manage_stock,use_config_manage_stock,use_config_qty_increments,qty_increments,use_config_enable_qty_inc,enable_qty_increments,is_decimal_divided,website_id,related_skus,crosssell_skus,upsell_skus,additional_images,additional_image_labels,hide_from_product_page,custom_options,bundle_price_type,bundle_sku_type,bundle_price_view,bundle_weight_type,bundle_values,associated_skus +simple_new,,Default,simple,,base,New Product,,,,1,Taxable Goods,"Catalog, Search",10,,,,new-product,New Product,New Product,New Product ,/no/exists/image/magento_image.jpg,Image Label,magento_small_image.jpg,Small Image Label,magento_thumbnail.jpg,Thumbnail Label,magento_image.jpg,Image Label,10/20/15 07:05,10/20/15 07:05,,,Block after Info Column,,,,,,,,,,,,,"has_options=1,quantity_and_stock_status=In Stock,required_options=1",100,0,1,0,0,1,1,1,10000,1,1,1,1,1,0,1,1,0,0,0,1,,,,"magento_additional_image_one.jpg, magento_additional_image_two.jpg","Additional Image Label One,Additional Image Label Two",,,,,,,, diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_without_url_keys_and_name.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_without_url_keys_and_name.csv new file mode 100644 index 000000000000..8ea6ab92a029 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_without_url_keys_and_name.csv @@ -0,0 +1,3 @@ +sku,price +simple1,25 +simple2,34 diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/ProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/ProductTest.php index 186c6b8a92bb..c39acbc33872 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/ProductTest.php @@ -13,7 +13,7 @@ class ProductTest extends AbstractProductExportImportTestCase /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function exportImportDataProvider() + public function exportImportDataProvider(): array { return [ 'product_export_data' => [ @@ -136,11 +136,6 @@ public function exportImportDataProvider() ]; } - public function importReplaceDataProvider() - { - return $this->exportImportDataProvider(); - } - /** * Fixing https://github.com/magento-engcom/import-export-improvements/issues/50 means that during import images * can now get renamed for this we need to skip the attribute checking and instead check that the images contain @@ -150,8 +145,10 @@ public function importReplaceDataProvider() * @param \Magento\Catalog\Model\Product $expectedProduct * @param \Magento\Catalog\Model\Product $actualProduct */ - protected function assertEqualsSpecificAttributes($expectedProduct, $actualProduct) - { + protected function assertEqualsSpecificAttributes( + \Magento\Catalog\Model\Product $expectedProduct, + \Magento\Catalog\Model\Product $actualProduct + ): void { if (!empty($actualProduct->getImage()) && !empty($expectedProduct->getImage()) ) { diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data.php index 89fb9952cc6f..477494626b9f 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data.php @@ -70,7 +70,7 @@ )->setPrice( 10 )->addData( - ['text_attribute' => '!@#$%^&*()_+1234567890-=|\\:;"\'<,>.?/'] + ['text_attribute' => '!@#$%^&*()_+1234567890-=|\\:;"\'<,>.?/›ƒª'] )->setTierPrice( [0 => ['website_id' => 0, 'cust_group' => 0, 'price_qty' => 3, 'price' => 8]] )->setVisibility( 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 168073bc6ab7..c57c7c3fd6a9 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_rollback.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_rollback.php @@ -5,10 +5,10 @@ */ /** Delete all products */ -require dirname(dirname(__DIR__)) . '/Catalog/_files/products_with_multiselect_attribute_rollback.php'; +include dirname(dirname(__DIR__)) . '/Catalog/_files/products_with_multiselect_attribute_rollback.php'; /** Delete text attribute */ -require dirname(dirname(__DIR__)) . '/Catalog/_files/text_attribute_rollback.php'; +include dirname(dirname(__DIR__)) . '/Catalog/_files/product_text_attribute_rollback.php'; -require dirname(dirname(__DIR__)) . '/Store/_files/second_store_rollback.php'; +include dirname(dirname(__DIR__)) . '/Store/_files/second_store_rollback.php'; -require dirname(dirname(__DIR__)) . '/Catalog/_files/category_rollback.php'; +include dirname(dirname(__DIR__)) . '/Catalog/_files/category_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars.php index a2c2c1815a8a..591dccf229f8 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars.php @@ -25,7 +25,7 @@ ->setName('New Product') ->setSku('simple "1"') ->setPrice(10) - ->addData(['text_attribute' => '!@#$%^&*()_+1234567890-=|\\:;"\'<,>.?/']) + ->addData(['text_attribute' => '!@#$%^&*()_+1234567890-=|\\:;"\'<,>.?/›ƒª']) ->setTierPrice([0 => ['website_id' => 0, 'cust_group' => 0, 'price_qty' => 3, 'price' => 8]]) ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) 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 168073bc6ab7..c57c7c3fd6a9 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars_rollback.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/product_export_data_special_chars_rollback.php @@ -5,10 +5,10 @@ */ /** Delete all products */ -require dirname(dirname(__DIR__)) . '/Catalog/_files/products_with_multiselect_attribute_rollback.php'; +include dirname(dirname(__DIR__)) . '/Catalog/_files/products_with_multiselect_attribute_rollback.php'; /** Delete text attribute */ -require dirname(dirname(__DIR__)) . '/Catalog/_files/text_attribute_rollback.php'; +include dirname(dirname(__DIR__)) . '/Catalog/_files/product_text_attribute_rollback.php'; -require dirname(dirname(__DIR__)) . '/Store/_files/second_store_rollback.php'; +include dirname(dirname(__DIR__)) . '/Store/_files/second_store_rollback.php'; -require dirname(dirname(__DIR__)) . '/Catalog/_files/category_rollback.php'; +include dirname(dirname(__DIR__)) . '/Catalog/_files/category_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/update_category_duplicates.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/update_category_duplicates.php new file mode 100644 index 000000000000..53d30e834a07 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/update_category_duplicates.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +require dirname(dirname(__DIR__)) . '/Catalog/_files/category_duplicates.php'; + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var \Magento\Catalog\Model\Category $category */ +$categoryModel = $objectManager->create(\Magento\Catalog\Model\Category::class); +$categoryModel->setStoreId(\Magento\Store\Model\Store::DEFAULT_STORE_ID); + +$categoryModel->load(444) + ->setName('Category 2-updated') + ->save(); diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/update_category_duplicates_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/update_category_duplicates_rollback.php new file mode 100644 index 000000000000..ca0f379a03dc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/update_category_duplicates_rollback.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +require dirname(dirname(__DIR__)) . '/Catalog/_files/category_duplicates_rollback.php'; + +//Delete products created in CSV import +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); +$csvProductSkus = ['simple1', 'simple2', 'simple3']; +foreach ($csvProductSkus as $sku) { + try { + $product = $productRepository->get($sku, false, null, true); + $productRepository->delete($product); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + } +} diff --git a/dev/tests/integration/testsuite/Magento/CatalogInventory/Model/Quote/Item/QuantityValidatorTest.php b/dev/tests/integration/testsuite/Magento/CatalogInventory/Model/Quote/Item/QuantityValidatorTest.php index a849e412675e..6105aba9201e 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogInventory/Model/Quote/Item/QuantityValidatorTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogInventory/Model/Quote/Item/QuantityValidatorTest.php @@ -5,12 +5,20 @@ */ namespace Magento\CatalogInventory\Model\Quote\Item; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\CatalogInventory\Api\Data\StockItemInterface; +use Magento\CatalogInventory\Model\Stock\StockItemRepository; +use Magento\CatalogInventory\Model\StockState; +use Magento\CatalogInventory\Observer\QuantityValidatorObserver; +use Magento\Eav\Model\Config; +use Magento\Framework\Exception\CouldNotSaveException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Model\Quote; use Magento\TestFramework\Helper\Bootstrap; use Magento\CatalogInventory\Model\Quote\Item\QuantityValidator\Initializer\Option; use Magento\Framework\Event\Observer; -use Magento\CatalogInventory\Model\StockState; -use Magento\CatalogInventory\Model\Quote\Item\QuantityValidator; -use Magento\CatalogInventory\Observer\QuantityValidatorObserver; use Magento\Framework\Event; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Framework\DataObject; @@ -96,7 +104,7 @@ public function testQuoteWithOptions() /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ $productRepository = $this->objectManager->create(ProductRepositoryInterface::class); - /** @var $product \Magento\Catalog\Model\Product */ + /** @var $product Product */ $product = $productRepository->get('bundle-product'); $resultMock = $this->createMock(DataObject::class); $this->stockState->expects($this->any())->method('checkQtyIncrements')->willReturn($resultMock); @@ -120,7 +128,7 @@ public function testQuoteWithOptionsWithErrors() $session = $this->objectManager->create(Session::class); /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ $productRepository = $this->objectManager->create(ProductRepositoryInterface::class); - /** @var $product \Magento\Catalog\Model\Product */ + /** @var $product Product */ $product = $productRepository->get('bundle-product'); /* @var $quoteItem \Magento\Quote\Model\Quote\Item */ $quoteItem = $this->_getQuoteItemIdByProductId($session->getQuote(), $product->getId()); @@ -135,7 +143,7 @@ public function testQuoteWithOptionsWithErrors() $resultMock->expects($this->any())->method('getHasError')->willReturn(true); $this->setMockStockStateResultToQuoteItemOptions($quoteItem, $resultMock); $this->observer->execute($this->observerMock); - $this->assertCount(2, $quoteItem->getErrorInfos(), 'Expected 2 errors in QuoteItem'); + $this->assertCount(1, $quoteItem->getErrorInfos(), 'Expected 1 error in QuoteItem'); } /** @@ -158,10 +166,101 @@ private function setMockStockStateResultToQuoteItemOptions($quoteItem, $resultMo $this->fail('Test failed since Quote Item does not have Qty options.'); } + /** + * Tests quantity verifications for configurable product. + * + * @param int $quantity - quantity of configurable option. + * @param string $errorMessageRegexp - expected error message regexp. + * @return void + * @throws CouldNotSaveException + * @throws LocalizedException + * @dataProvider quantityDataProvider + * @magentoDataFixture Magento/CatalogInventory/_files/configurable_options_advanced_inventory.php + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + */ + public function testConfigurableWithOptions(int $quantity, string $errorMessageRegexp): void + { + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + /** @var Product $product */ + $product = $productRepository->get('configurable'); + $product->setStatus(Status::STATUS_ENABLED) + ->setData('is_salable', true); + $productRepository->save($product); + + /** @var StockItemRepository $stockItemRepository */ + $stockItemRepository = $this->objectManager->create(StockItemRepository::class); + + /** @var StockItemInterface $stockItem */ + $stockItem = $stockItemRepository->get($product->getExtensionAttributes() + ->getStockItem() + ->getItemId()); + $stockItem->setIsInStock(true) + ->setQty(1000); + $stockItemRepository->save($stockItem); + + /** @var Config $eavConfig */ + $eavConfig = $this->objectManager->get(Config::class); + /** @var $attribute */ + $attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); + + $request = $this->objectManager->create(DataObject::class); + $request->setData( + [ + 'product_id' => $product->getId(), + 'selected_configurable_option' => 1, + 'super_attribute' => [ + $attribute->getAttributeId() => $attribute->getOptions()[1]->getValue() + ], + 'qty' => $quantity + ] + ); + + try { + /** @var Quote $cart */ + $cart = $this->objectManager->create(CartInterface::class); + $result = $cart->addProduct($product, $request); + + if (empty($errorMessageRegexp)) { + self::assertEquals('Configurable Product', $result->getName()); + } + } catch (LocalizedException $e) { + self::assertEquals(1, preg_match($errorMessageRegexp, $e->getMessage())); + } + } + + /** + * Provides request quantity for configurable option + * and corresponding error message. + * + * @return array + */ + public function quantityDataProvider(): array + { + $qtyRegexp = '/You can buy (this product|Configurable OptionOption 1) only in quantities of 500 at a time/'; + + return [ + [ + 'quantity' => 1, + 'error_regexp' => '/The fewest you may purchase is 500/' + ], + [ + 'quantity' => 501, + 'error_regexp' => $qtyRegexp + ], + [ + 'quantity' => 1000, + 'error_regexp' => '' + ], + + ]; + } + /** * Gets \Magento\Quote\Model\Quote\Item from \Magento\Quote\Model\Quote by product id * - * @param \Magento\Quote\Model\Quote $quote + * @param Quote $quote * @param int $productId * @return \Magento\Quote\Model\Quote\Item */ diff --git a/dev/tests/integration/testsuite/Magento/CatalogInventory/_files/configurable_options_advanced_inventory.php b/dev/tests/integration/testsuite/Magento/CatalogInventory/_files/configurable_options_advanced_inventory.php new file mode 100644 index 000000000000..901a1d334448 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogInventory/_files/configurable_options_advanced_inventory.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\CatalogInventory\Api\Data\StockItemInterface; +use Magento\CatalogInventory\Api\StockItemRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; + +require __DIR__ . '/../../../Magento/ConfigurableProduct/_files/product_configurable.php'; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var StockItemRepositoryInterface $stockItemRepository */ +$stockItemRepository = $objectManager->get(StockItemRepositoryInterface::class); + +/** @var ProductInterface $product */ +$product = $productRepository->get('simple_10'); + +/** @var StockItemInterface $stockItem */ +$stockItem = $product->getExtensionAttributes()->getStockItem(); +$stockItem->setIsInStock(true) + ->setQty(10000) + ->setUseConfigMinSaleQty(false) + ->setMinSaleQty(500) + ->setUseConfigEnableQtyInc(false) + ->setEnableQtyIncrements(true) + ->setUseConfigQtyIncrements(false) + ->setQtyIncrements(500); + +$stockItemRepository->save($stockItem); diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/BatchIndexTest.php b/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/BatchIndexTest.php index b71b466714e8..11556dcfb7e7 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/BatchIndexTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/BatchIndexTest.php @@ -102,17 +102,20 @@ protected function prepareProducts($price) ->setUrlKey(null) ->setSku(uniqid($this->product->getSku() . '-')) ->setName(uniqid($this->product->getName() . '-')) - ->setWebsiteIds([1]); - $productSecond->save(); - $productSecond->setPrice($price)->save(); + ->setWebsiteIds([1]) + ->save(); + $productSecond->setPrice($price); + $this->productRepository->save($productSecond); $productThird = clone $this->product; $productThird->setId(null) ->setUrlKey(null) - ->setSku(uniqid($this->product->getSku() . '-')) - ->setName(uniqid($this->product->getName() . '-')) + ->setSku(uniqid($this->product->getSku() . '--')) + ->setName(uniqid($this->product->getName() . '--')) ->setWebsiteIds([1]) ->save(); - $productThird->setPrice($price)->save(); + $productThird->setPrice($price); + $this->productRepository->save($productThird); + return [ $productSecond->getEntityId(), $productThird->getEntityId(), diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php index 59ad91ae7b07..56c5db5572a3 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/FullTest.php @@ -14,6 +14,9 @@ use Magento\Catalog\Model\Product; use Magento\TestFramework\Helper\Bootstrap; +/** + * Class for testing fulltext index rebuild + */ class FullTest extends \PHPUnit\Framework\TestCase { /** @@ -21,6 +24,9 @@ class FullTest extends \PHPUnit\Framework\TestCase */ protected $actionFull; + /** + * @inheritdoc + */ protected function setUp() { $this->actionFull = Bootstrap::getObjectManager()->create( @@ -29,6 +35,8 @@ protected function setUp() } /** + * Testing fulltext index rebuild + * * @magentoDataFixture Magento/CatalogSearch/_files/products_for_index.php * @magentoDataFixture Magento/CatalogSearch/_files/product_configurable_not_available.php * @magentoDataFixture Magento/Framework/Search/_files/product_configurable.php @@ -39,7 +47,6 @@ public function testGetIndexData() $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); $allowedStatuses = Bootstrap::getObjectManager()->get(Status::class)->getVisibleStatusIds(); $allowedVisibility = Bootstrap::getObjectManager()->get(Engine::class)->getAllowedVisibility(); - $result = iterator_to_array($this->actionFull->rebuildStoreIndex(Store::DISTRO_STORE_ID)); $this->assertNotEmpty($result); @@ -58,7 +65,10 @@ public function testGetIndexData() } /** + * Prepare and return expected index data + * * @return array + * @throws \Magento\Framework\Exception\NoSuchEntityException */ private function getExpectedIndexData() { @@ -68,32 +78,48 @@ private function getExpectedIndexData() $nameId = $attributeRepository->get(ProductInterface::NAME)->getAttributeId(); /** @see dev/tests/integration/testsuite/Magento/Framework/Search/_files/configurable_attribute.php */ $configurableId = $attributeRepository->get('test_configurable')->getAttributeId(); + $statusId = $attributeRepository->get(ProductInterface::STATUS)->getAttributeId(); + $taxClassId = $attributeRepository + ->get(\Magento\Customer\Api\Data\GroupInterface::TAX_CLASS_ID) + ->getAttributeId(); return [ 'configurable' => [ $skuId => 'configurable', $configurableId => 'Option 1 | Option 2', $nameId => 'Configurable Product | Configurable OptionOption 1 | Configurable OptionOption 2', + $taxClassId => 'Taxable Goods | Taxable Goods | Taxable Goods', + $statusId => 'Enabled | Enabled | Enabled' ], 'index_enabled' => [ $skuId => 'index_enabled', $nameId => 'index enabled', + $taxClassId => 'Taxable Goods', + $statusId => 'Enabled' ], 'index_visible_search' => [ $skuId => 'index_visible_search', $nameId => 'index visible search', + $taxClassId => 'Taxable Goods', + $statusId => 'Enabled' ], 'index_visible_category' => [ $skuId => 'index_visible_category', $nameId => 'index visible category', + $taxClassId => 'Taxable Goods', + $statusId => 'Enabled' ], 'index_visible_both' => [ $skuId => 'index_visible_both', $nameId => 'index visible both', + $taxClassId => 'Taxable Goods', + $statusId => 'Enabled' ] ]; } /** + * Testing fulltext index rebuild with configurations + * * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php */ public function testRebuildStoreIndexConfigurable() @@ -114,6 +140,8 @@ public function testRebuildStoreIndexConfigurable() } /** + * Get product Id by its SKU + * * @param string $sku * @return int */ diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Search/FilterMapper/CustomAttributeFilterTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Search/FilterMapper/CustomAttributeFilterTest.php index 7d0f9148cd70..d92539396de5 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Search/FilterMapper/CustomAttributeFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Search/FilterMapper/CustomAttributeFilterTest.php @@ -155,7 +155,6 @@ private function getSqlForOneAttributeSearch() $joinConditions = [ '`some_index`.`entity_id` = `field1_filter`.`entity_id`', - '`some_index`.`source_id` = `field1_filter`.`source_id`', sprintf('`field1_filter`.`attribute_id` = %s', $firstAttribute->getId()), sprintf('`field1_filter`.`store_id` = %s', (int) $this->storeManager->getStore()->getId()) ]; @@ -182,14 +181,12 @@ private function getSqlForTwoAttributeSearch() $joinConditions1 = [ '`some_index`.`entity_id` = `field1_filter`.`entity_id`', - '`some_index`.`source_id` = `field1_filter`.`source_id`', sprintf('`field1_filter`.`attribute_id` = %s', $firstAttribute->getId()), sprintf('`field1_filter`.`store_id` = %s', (int) $this->storeManager->getStore()->getId()) ]; $joinConditions2 = [ '`some_index`.`entity_id` = `field2_filter`.`entity_id`', - '`some_index`.`source_id` = `field2_filter`.`source_id`', sprintf('`field2_filter`.`attribute_id` = %s', $secondAttribute->getId()), sprintf('`field2_filter`.`store_id` = %s', (int) $this->storeManager->getStore()->getId()) ]; diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Observer/ProcessUrlRewriteOnChangeVisibilityObserverTest.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Observer/ProcessUrlRewriteOnChangeVisibilityObserverTest.php new file mode 100644 index 000000000000..d3f0e9fa2a5a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Observer/ProcessUrlRewriteOnChangeVisibilityObserverTest.php @@ -0,0 +1,195 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\CatalogUrlRewrite\Observer; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Framework\Event\ManagerInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException; +use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; + +/** + * @magentoAppArea adminhtml + * @magentoDbIsolation disabled + */ +class ProcessUrlRewriteOnChangeVisibilityObserverTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\ObjectManagerInterface + */ + private $objectManager; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var ManagerInterface + */ + private $eventManager; + + /** + * Set up + */ + protected function setUp() + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + $this->eventManager = $this->objectManager->create(ManagerInterface::class); + } + + /** + * @magentoDataFixture Magento/CatalogUrlRewrite/_files/product_rewrite_multistore.php + * @magentoAppIsolation enabled + */ + public function testMakeProductInvisibleViaMassAction() + { + /** @var \Magento\Catalog\Model\Product $product*/ + $product = $this->productRepository->get('product1'); + + /** @var StoreManagerInterface $storeManager */ + $storeManager = $this->objectManager->get(StoreManagerInterface::class); + $storeManager->setCurrentStore(0); + + $testStore = $storeManager->getStore('test'); + $productFilter = [ + UrlRewrite::ENTITY_TYPE => 'product', + ]; + + $expected = [ + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => '1', + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore->getId(), + ] + ]; + + $actual = $this->getActualResults($productFilter); + foreach ($expected as $row) { + $this->assertContains($row, $actual); + } + + $this->eventManager->dispatch( + 'catalog_product_attribute_update_before', + [ + 'attributes_data' => [ ProductInterface::VISIBILITY => Visibility::VISIBILITY_NOT_VISIBLE ], + 'product_ids' => [$product->getId()] + ] + ); + + $actual = $this->getActualResults($productFilter); + $this->assertCount(0, $actual); + } + + /** + * @magentoDataFixture Magento/CatalogUrlRewrite/_files/product_invisible_multistore.php + * @magentoAppIsolation enabled + */ + public function testMakeProductVisibleViaMassAction() + { + /** @var \Magento\Catalog\Model\Product $product*/ + $product = $this->productRepository->get('product1'); + + /** @var StoreManagerInterface $storeManager */ + $storeManager = $this->objectManager->get(StoreManagerInterface::class); + $storeManager->setCurrentStore(0); + + $testStore = $storeManager->getStore('test'); + $productFilter = [ + UrlRewrite::ENTITY_TYPE => 'product', + ]; + + $actual = $this->getActualResults($productFilter); + $this->assertCount(0, $actual); + + $this->eventManager->dispatch( + 'catalog_product_attribute_update_before', + [ + 'attributes_data' => [ ProductInterface::VISIBILITY => Visibility::VISIBILITY_BOTH ], + 'product_ids' => [$product->getId()] + ] + ); + + $expected = [ + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => '1', + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore->getId(), + ] + ]; + + $actual = $this->getActualResults($productFilter); + foreach ($expected as $row) { + $this->assertContains($row, $actual); + } + } + + /** + * @magentoDataFixture Magento/CatalogUrlRewrite/_files/products_invisible.php + * @magentoAppIsolation enabled + */ + public function testErrorOnDuplicatedUrlKey() + { + $skus = ['product1', 'product2']; + foreach ($skus as $sku) { + /** @var \Magento\Catalog\Model\Product $product */ + $productIds[] = $this->productRepository->get($sku)->getId(); + } + $this->expectException(UrlAlreadyExistsException::class); + $this->expectExceptionMessage('Can not change the visibility of the product with SKU equals "product2". ' + . 'URL key "product-1" for specified store already exists.'); + + $this->eventManager->dispatch( + 'catalog_product_attribute_update_before', + [ + 'attributes_data' => [ ProductInterface::VISIBILITY => Visibility::VISIBILITY_BOTH ], + 'product_ids' => $productIds + ] + ); + } + + /** + * @param array $filter + * @return array + */ + private function getActualResults(array $filter) + { + /** @var \Magento\UrlRewrite\Model\UrlFinderInterface $urlFinder */ + $urlFinder = $this->objectManager->get(\Magento\UrlRewrite\Model\UrlFinderInterface::class); + $actualResults = []; + foreach ($urlFinder->findAllByData($filter) as $url) { + $actualResults[] = [ + 'request_path' => $url->getRequestPath(), + 'target_path' => $url->getTargetPath(), + 'is_auto_generated' => (int)$url->getIsAutogenerated(), + 'redirect_type' => $url->getRedirectType(), + 'store_id' => $url->getStoreId() + ]; + } + return $actualResults; + } +} diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserverTest.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserverTest.php index 9de0588356c4..c72a58197b1f 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserverTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserverTest.php @@ -90,6 +90,7 @@ public function testUrlKeyHasChangedInGlobalContext() $product->setData('save_rewrites_history', true); $product->setUrlKey('new-url'); + $product->setUrlPath('new-path'); $product->save(); $expected = [ @@ -152,6 +153,7 @@ public function testUrlKeyHasChangedInStoreviewContextWithPermanentRedirection() $product->setData('save_rewrites_history', true); $product->setUrlKey('new-url'); + $product->setUrlPath('new-path'); $product->save(); $expected = [ @@ -207,6 +209,7 @@ public function testUrlKeyHasChangedInStoreviewContextWithoutPermanentRedirectio $product->setData('save_rewrites_history', false); $product->setUrlKey('new-url'); + $product->setUrlPath('new-path'); $product->save(); $expected = [ diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/product_invisible_multistore.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/product_invisible_multistore.php new file mode 100644 index 000000000000..d50b29383b1e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/product_invisible_multistore.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Setup\CategorySetup; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +\Magento\TestFramework\Helper\Bootstrap::getInstance() + ->loadArea(\Magento\Backend\App\Area\FrontNameResolver::AREA_CODE); + +require __DIR__ . '/../../Store/_files/store.php'; + +/** @var $installer CategorySetup */ +$objectManager = Bootstrap::getObjectManager(); +$installer = $objectManager->create(CategorySetup::class); +$storeManager = $objectManager->get(StoreManagerInterface::class); +$storeManager->setCurrentStore(0); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setAttributeSetId($installer->getAttributeSetId('catalog_product', 'Default')) + ->setStoreId(0) + ->setWebsiteIds([1]) + ->setName('Product1') + ->setSku('product1') + ->setPrice(10) + ->setWeight(18) + ->setStockData(['use_config_manage_stock' => 0]) + ->setUrlKey('product-1') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/product_invisible_multistore_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/product_invisible_multistore_rollback.php new file mode 100644 index 000000000000..bcf399cb5e55 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/product_invisible_multistore_rollback.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\Framework\Exception\NoSuchEntityException; + +\Magento\TestFramework\Helper\Bootstrap::getInstance()->getInstance()->reinitialize(); + +/** @var \Magento\Framework\Registry $registry */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); +try { + $product = $productRepository->get('product1', true); + if ($product->getId()) { + $productRepository->delete($product); + } +} catch (NoSuchEntityException $e) { +} +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +require __DIR__ . '/../../Store/_files/store_rollback.php'; +require __DIR__ . '/../../Store/_files/second_store_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/products_invisible.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/products_invisible.php new file mode 100644 index 000000000000..c72d9c8284db --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/products_invisible.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Setup\CategorySetup; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +\Magento\TestFramework\Helper\Bootstrap::getInstance() + ->loadArea(\Magento\Backend\App\Area\FrontNameResolver::AREA_CODE); + +/** @var $installer CategorySetup */ +$objectManager = Bootstrap::getObjectManager(); +$installer = $objectManager->create(CategorySetup::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); + +$skus = ['product1', 'product2']; +foreach ($skus as $sku) { + /** @var $product \Magento\Catalog\Model\Product */ + $product = $objectManager->create(\Magento\Catalog\Model\Product::class); + $product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setAttributeSetId($installer->getAttributeSetId('catalog_product', 'Default')) + ->setStoreId(0) + ->setWebsiteIds([1]) + ->setName('Product1') + ->setSku($sku) + ->setPrice(10) + ->setWeight(18) + ->setStockData(['use_config_manage_stock' => 0]) + ->setUrlKey('product-1') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED); + $productRepository->save($product); +} diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/products_invisible_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/products_invisible_rollback.php new file mode 100644 index 000000000000..d3d17542aa6a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/products_invisible_rollback.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\Framework\Exception\NoSuchEntityException; + +\Magento\TestFramework\Helper\Bootstrap::getInstance()->getInstance()->reinitialize(); + +/** @var \Magento\Framework\Registry $registry */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +$skus = ['product1', 'product2']; +foreach ($skus as $sku) { + try { + $product = $productRepository->get($sku, true); + if ($product->getId()) { + $productRepository->delete($product); + } + } catch (NoSuchEntityException $e) { + } +} +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +require __DIR__ . '/../../Store/_files/store_rollback.php'; +require __DIR__ . '/../../Store/_files/second_store_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/CatalogWidget/Block/Product/ProductListTest.php b/dev/tests/integration/testsuite/Magento/CatalogWidget/Block/Product/ProductListTest.php index a8572df0361d..2695948e7831 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogWidget/Block/Product/ProductListTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogWidget/Block/Product/ProductListTest.php @@ -78,4 +78,98 @@ public function testCreateCollection() "Product collection was not filtered according to the widget condition." ); } + + /** + * Test product list widget can process condition with dropdown type of attribute + * + * @magentoDbIsolation disabled + * @magentoDataFixture Magento/Catalog/_files/products_with_dropdown_attribute.php + */ + public function testCreateCollectionWithDropdownAttribute() + { + /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ + $attribute = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class + ); + $attribute->load('dropdown_attribute', 'attribute_code'); + $dropdownAttributeOptionIds = []; + foreach ($attribute->getOptions() as $option) { + if ($option->getValue()) { + $dropdownAttributeOptionIds[] = $option->getValue(); + } + } + $encodedConditions = '^[`1`:^[`type`:`Magento||CatalogWidget||Model||Rule||Condition||Combine`,' . + '`aggregator`:`any`,`value`:`1`,`new_child`:``^],`1--1`:^[`type`:`Magento||CatalogWidget||Model||Rule|' . + '|Condition||Product`,`attribute`:`dropdown_attribute`,`operator`:`==`,`value`:`' + . $dropdownAttributeOptionIds[0] . '`^],`1--2`:^[`type`:`Magento||CatalogWidget||Model||Rule|' . + '|Condition||Product`,`attribute`:`dropdown_attribute`,`operator`:`==`,`value`:`' + . $dropdownAttributeOptionIds[1] . '`^]^]'; + $this->block->setData('conditions_encoded', $encodedConditions); + $this->performAssertions(2); + $attribute->setUsedInProductListing(0); + $attribute->save(); + $this->performAssertions(2); + $attribute->setIsGlobal(1); + $attribute->save(); + $this->performAssertions(2); + } + + /** + * Check product collection includes correct amount of products. + * + * @param int $count + * @return void + */ + private function performAssertions(int $count) + { + // Load products collection filtered using specified conditions and perform assertions. + $productCollection = $this->block->createCollection(); + $productCollection->load(); + $this->assertEquals( + $count, + $productCollection->count(), + "Product collection was not filtered according to the widget condition." + ); + } + + /** + * Check that collection returns correct result if use not contains operator for string attribute + * + * @magentoDbIsolation disabled + * @magentoDataFixture Magento/Catalog/_files/product_simple_xss.php + * @magentoDataFixture Magento/Catalog/_files/product_virtual.php + * @dataProvider createCollectionForSkuDataProvider + * @param string $encodedConditions + * @param string $sku + * @return void + */ + public function testCreateCollectionForSku($encodedConditions, $sku) + { + $this->block->setData('conditions_encoded', $encodedConditions); + $productCollection = $this->block->createCollection(); + $productCollection->load(); + $this->assertEquals( + 1, + $productCollection->count(), + "Product collection was not filtered according to the widget condition." + ); + $this->assertEquals($sku, $productCollection->getFirstItem()->getSku()); + } + + /** + * @return array + */ + public function createCollectionForSkuDataProvider() + { + return [ + 'contains' => ['^[`1`:^[`type`:`Magento||CatalogWidget||Model||Rule||Condition||Combine`,' + . '`aggregator`:`all`,`value`:`1`,`new_child`:``^],' + . '`1--1`:^[`type`:`Magento||CatalogWidget||Model||Rule||Condition||Product`,' + . '`attribute`:`sku`,`operator`:`^[^]`,`value`:`virtual`^]^]' , 'virtual-product'], + 'not contains' => ['^[`1`:^[`type`:`Magento||CatalogWidget||Model||Rule||Condition||Combine`,' + . '`aggregator`:`all`,`value`:`1`,`new_child`:``^],' + . '`1--1`:^[`type`:`Magento||CatalogWidget||Model||Rule||Condition||Product`,' + . '`attribute`:`sku`,`operator`:`!^[^]`,`value`:`virtual`^]^]', 'product-with-xss'] + ]; + } } diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Api/GuestShippingInformationManagementTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Api/GuestShippingInformationManagementTest.php new file mode 100644 index 000000000000..50b1256c0f12 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/Api/GuestShippingInformationManagementTest.php @@ -0,0 +1,125 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Api; + +use Magento\Checkout\Api\Data\ShippingInformationInterface; +use Magento\Checkout\Api\Data\ShippingInformationInterfaceFactory; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\ShippingAssignmentInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\QuoteIdMaskFactory; + +/** + * Test GuestShippingInformationManagement API. + */ +class GuestShippingInformationManagementTest extends TestCase +{ + /** + * @var GuestShippingInformationManagementInterface + */ + private $management; + + /** + * @var CartRepositoryInterface + */ + private $cartRepo; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepo; + + /** + * @var ShippingInformationInterfaceFactory + */ + private $shippingFactory; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteria; + + /** + * @var QuoteIdMaskFactory + */ + private $maskFactory; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->management = $objectManager->get(GuestShippingInformationManagementInterface::class); + $this->cartRepo = $objectManager->get(CartRepositoryInterface::class); + $this->customerRepo = $objectManager->get(CustomerRepositoryInterface::class); + $this->shippingFactory = $objectManager->get(ShippingInformationInterfaceFactory::class); + $this->searchCriteria = $objectManager->get(SearchCriteriaBuilder::class); + $this->maskFactory = $objectManager->get(QuoteIdMaskFactory::class); + } + + /** + * Test using another address for quote. + * + * @param bool $swapShipping Whether to swap shipping or billing addresses. + * @return void + * + * @magentoDataFixture Magento/Sales/_files/quote.php + * @magentoDataFixture Magento/Customer/_files/customer_with_addresses.php + * @dataProvider getAddressesVariation + * @expectedException \Magento\Framework\Exception\InputException + * @expectedExceptionMessage The shipping information was unable to be saved. Verify the input data and try again. + */ + public function testDifferentAddresses(bool $swapShipping) + { + $carts = $this->cartRepo->getList( + $this->searchCriteria->addFilter('reserved_order_id', 'test01')->create() + )->getItems(); + $cart = array_pop($carts); + $otherCustomer = $this->customerRepo->get('customer_with_addresses@test.com'); + $otherAddresses = $otherCustomer->getAddresses(); + $otherAddress = array_pop($otherAddresses); + + //Setting invalid IDs. + /** @var ShippingAssignmentInterface $shippingAssignment */ + $shippingAssignment = $cart->getExtensionAttributes()->getShippingAssignments()[0]; + $shippingAddress = $shippingAssignment->getShipping()->getAddress(); + $billingAddress = $cart->getBillingAddress(); + if ($swapShipping) { + $address = $shippingAddress; + } else { + $address = $billingAddress; + } + $address->setCustomerAddressId($otherAddress->getId()); + $address->setCustomerId($otherCustomer->getId()); + $address->setId(null); + /** @var ShippingInformationInterface $shippingInformation */ + $shippingInformation = $this->shippingFactory->create(); + $shippingInformation->setBillingAddress($billingAddress); + $shippingInformation->setShippingAddress($shippingAddress); + $shippingInformation->setShippingMethodCode('flatrate'); + /** @var QuoteIdMask $idMask */ + $idMask = $this->maskFactory->create(); + $idMask->load($cart->getId(), 'quote_id'); + $this->management->saveAddressInformation($idMask->getMaskedId(), $shippingInformation); + } + + /** + * Different variations for addresses test. + * + * @return array + */ + public function getAddressesVariation(): array + { + return [ + 'Shipping address swap' => [true], + 'Billing address swap' => [false] + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Api/ShippingInformationManagementTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Api/ShippingInformationManagementTest.php new file mode 100644 index 000000000000..7440fb7fd3d9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/Api/ShippingInformationManagementTest.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Api; + +use Magento\Checkout\Api\Data\ShippingInformationInterface; +use Magento\Checkout\Api\Data\ShippingInformationInterfaceFactory; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\ShippingAssignmentInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test ShippingInformationManagement API. + */ +class ShippingInformationManagementTest extends TestCase +{ + /** + * @var ShippingInformationManagementInterface + */ + private $management; + + /** + * @var CartRepositoryInterface + */ + private $cartRepo; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepo; + + /** + * @var ShippingInformationInterfaceFactory + */ + private $shippingFactory; + + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->management = $objectManager->get(ShippingInformationManagementInterface::class); + $this->cartRepo = $objectManager->get(CartRepositoryInterface::class); + $this->customerRepo = $objectManager->get(CustomerRepositoryInterface::class); + $this->shippingFactory = $objectManager->get(ShippingInformationInterfaceFactory::class); + } + + /** + * Test using another address for quote. + * + * @param bool $swapShipping Whether to swap shipping or billing addresses. + * @return void + * + * @magentoDataFixture Magento/Sales/_files/quote_with_customer.php + * @magentoDataFixture Magento/Customer/_files/customer_with_addresses.php + * @dataProvider getAddressesVariation + * @expectedException \Magento\Framework\Exception\InputException + * @expectedExceptionMessage The shipping information was unable to be saved. Verify the input data and try again. + */ + public function testDifferentAddresses(bool $swapShipping) + { + $cart = $this->cartRepo->getForCustomer(1); + $otherCustomer = $this->customerRepo->get('customer_with_addresses@test.com'); + $otherAddresses = $otherCustomer->getAddresses(); + $otherAddress = array_pop($otherAddresses); + + //Setting invalid IDs. + /** @var ShippingAssignmentInterface $shippingAssignment */ + $shippingAssignment = $cart->getExtensionAttributes()->getShippingAssignments()[0]; + $shippingAddress = $shippingAssignment->getShipping()->getAddress(); + $billingAddress = $cart->getBillingAddress(); + if ($swapShipping) { + $address = $shippingAddress; + } else { + $address = $billingAddress; + } + $address->setCustomerAddressId($otherAddress->getId()); + $address->setCustomerId($otherCustomer->getId()); + $address->setId(null); + /** @var ShippingInformationInterface $shippingInformation */ + $shippingInformation = $this->shippingFactory->create(); + $shippingInformation->setBillingAddress($billingAddress); + $shippingInformation->setShippingAddress($shippingAddress); + $shippingInformation->setShippingMethodCode('flatrate'); + $this->management->saveAddressInformation($cart->getId(), $shippingInformation); + } + + /** + * Different variations for addresses test. + * + * @return array + */ + public function getAddressesVariation(): array + { + return [ + 'Shipping address swap' => [true], + 'Billing address swap' => [false] + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/Index/CouponPostTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/Index/CouponPostTest.php index 07141843793e..2ba798e4811a 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/Index/CouponPostTest.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/Index/CouponPostTest.php @@ -6,6 +6,8 @@ namespace Magento\Checkout\Controller\Cart\Index; +use Magento\Framework\App\Request\Http as HttpRequest; + /** * @magentoDbIsolation enabled */ @@ -26,6 +28,7 @@ public function testExecute() 'remove' => 0, 'coupon_code' => 'test' ]; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($inputData); $this->dispatch( 'checkout/cart/couponPost/' @@ -36,4 +39,35 @@ public function testExecute() \Magento\Framework\Message\MessageInterface::TYPE_ERROR ); } + + /** + * Testing by adding a valid coupon to cart + * + * @magentoDataFixture Magento/Checkout/_files/quote_with_virtual_product_and_address.php + * @magentoDataFixture Magento/Usps/Fixtures/cart_rule_coupon_free_shipping.php + * @return void + */ + public function testAddingValidCoupon(): void + { + /** @var $session \Magento\Checkout\Model\Session */ + $session = $this->_objectManager->create(\Magento\Checkout\Model\Session::class); + $quote = $session->getQuote(); + $quote->setData('trigger_recollect', 1)->setTotalsCollectedFlag(true); + + $couponCode = 'IMPHBR852R61'; + $inputData = [ + 'remove' => 0, + 'coupon_code' => $couponCode + ]; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue($inputData); + $this->dispatch( + 'checkout/cart/couponPost/' + ); + + $this->assertSessionMessages( + $this->equalTo(['You used coupon code "' . $couponCode . '".']), + \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Controller/CartTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Controller/CartTest.php index 52156040b280..fc85cc384a3d 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/Controller/CartTest.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/Controller/CartTest.php @@ -11,13 +11,18 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Checkout\Model\Session; +use Magento\Checkout\Model\Session as CheckoutSession; use Magento\Customer\Model\ResourceModel\CustomerRepository; use Magento\Framework\Data\Form\FormKey; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Quote\Model\Quote; +use Magento\Quote\Api\CartRepositoryInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Request; use Magento\Customer\Model\Session as CustomerSession; use Magento\Sales\Model\ResourceModel\Order\Collection as OrderCollection; use Magento\Sales\Model\ResourceModel\Order\Item\Collection as OrderItemCollection; +use Magento\Framework\App\Request\Http as HttpRequest; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -25,6 +30,28 @@ */ class CartTest extends \Magento\TestFramework\TestCase\AbstractController { + /** @var CheckoutSession */ + private $checkoutSession; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + $this->checkoutSession = $this->_objectManager->get(CheckoutSession::class); + $this->_objectManager->addSharedInstance($this->checkoutSession, CheckoutSession::class); + } + + /** + * @inheritdoc + */ + protected function tearDown() + { + $this->_objectManager->removeSharedInstance(CheckoutSession::class); + parent::tearDown(); + } + /** * Test for \Magento\Checkout\Controller\Cart::configureAction() with simple product * @@ -32,8 +59,8 @@ class CartTest extends \Magento\TestFramework\TestCase\AbstractController */ public function testConfigureActionWithSimpleProduct() { - /** @var $session \Magento\Checkout\Model\Session */ - $session = $this->_objectManager->create(\Magento\Checkout\Model\Session::class); + /** @var $session CheckoutSession */ + $session = $this->_objectManager->create(CheckoutSession::class); /** @var ProductRepositoryInterface $productRepository */ $productRepository = $this->_objectManager->create(ProductRepositoryInterface::class); @@ -63,19 +90,20 @@ public function testConfigureActionWithSimpleProduct() /** * Test for \Magento\Checkout\Controller\Cart::configureAction() with simple product and custom option * - * @magentoDataFixture Magento/Checkout/_files/quote_with_simple_product_and_custom_option.php + * @magentoDataFixture Magento/Checkout/_files/cart_with_simple_product_and_custom_options.php */ public function testConfigureActionWithSimpleProductAndCustomOption() { - /** @var $session \Magento\Checkout\Model\Session */ - $session = $this->_objectManager->create(\Magento\Checkout\Model\Session::class); + /** @var Quote $quote */ + $quote = $this->getQuote('test_order_item_with_custom_options'); + $this->checkoutSession->setQuoteId($quote->getId()); /** @var ProductRepositoryInterface $productRepository */ $productRepository = $this->_objectManager->create(ProductRepositoryInterface::class); /** @var $product \Magento\Catalog\Model\Product */ - $product = $productRepository->get('simple'); + $product = $productRepository->get('simple_with_custom_options'); - $quoteItem = $this->_getQuoteItemIdByProductId($session->getQuote(), $product->getId()); + $quoteItem = $this->_getQuoteItemIdByProductId($quote, $product->getId()); $this->assertNotNull($quoteItem, 'Cannot get quote item for simple product with custom option'); $this->dispatch( @@ -112,8 +140,8 @@ public function testConfigureActionWithSimpleProductAndCustomOption() */ public function testConfigureActionWithBundleProduct() { - /** @var $session \Magento\Checkout\Model\Session */ - $session = $this->_objectManager->create(\Magento\Checkout\Model\Session::class); + /** @var $session CheckoutSession */ + $session = $this->_objectManager->create(CheckoutSession::class); /** @var ProductRepositoryInterface $productRepository */ $productRepository = $this->_objectManager->create(ProductRepositoryInterface::class); @@ -147,8 +175,8 @@ public function testConfigureActionWithBundleProduct() */ public function testConfigureActionWithDownloadableProduct() { - /** @var $session \Magento\Checkout\Model\Session */ - $session = $this->_objectManager->create(\Magento\Checkout\Model\Session::class); + /** @var $session CheckoutSession */ + $session = $this->_objectManager->create(CheckoutSession::class); /** @var ProductRepositoryInterface $productRepository */ $productRepository = $this->_objectManager->create(ProductRepositoryInterface::class); @@ -201,8 +229,8 @@ public function testUpdatePostAction() $productId = $product->getId(); $originalQuantity = 1; $updatedQuantity = 2; - /** @var $checkoutSession \Magento\Checkout\Model\Session */ - $checkoutSession = $this->_objectManager->create(\Magento\Checkout\Model\Session::class); + /** @var $checkoutSession CheckoutSession */ + $checkoutSession = $this->_objectManager->create(CheckoutSession::class); $quoteItem = $this->_getQuoteItemIdByProductId($checkoutSession->getQuote(), $productId); /** @var FormKey $formKey */ @@ -212,6 +240,7 @@ public function testUpdatePostAction() 'update_cart_action' => 'update_qty', 'form_key' => $formKey->getFormKey(), ]; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($postData); /** @var $customerSession \Magento\Customer\Model\Session */ $customerSession = $this->_objectManager->create(\Magento\Customer\Model\Session::class); @@ -235,11 +264,32 @@ public function testUpdatePostAction() $this->assertEquals($updatedQuantity, $quoteItem->getQty(), "Invalid quote item quantity"); } + /** + * Gets quote by reserved order id. + * + * @param string $reservedOrderId + * @return Quote + */ + private function getQuote($reservedOrderId) + { + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $this->_objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter('reserved_order_id', $reservedOrderId) + ->create(); + + /** @var CartRepositoryInterface $quoteRepository */ + $quoteRepository = $this->_objectManager->get(CartRepositoryInterface::class); + $items = $quoteRepository->getList($searchCriteria)->getItems(); + + return array_pop($items); + } + /** * Gets \Magento\Quote\Model\Quote\Item from \Magento\Quote\Model\Quote by product id * * @param \Magento\Quote\Model\Quote $quote - * @param int $productId + * @param string|int $productId + * * @return \Magento\Quote\Model\Quote\Item|null */ private function _getQuoteItemIdByProductId($quote, $productId) @@ -274,6 +324,7 @@ public function testAddToCartSimpleProduct($area, $expectedPrice) 'isAjax' => 1 ]; \Magento\TestFramework\Helper\Bootstrap::getInstance()->loadArea($area); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($postData); $quote = $this->_objectManager->create(\Magento\Checkout\Model\Cart::class); @@ -301,6 +352,77 @@ public function addAddProductDataProvider() ]; } + /** + * Test for \Magento\Checkout\Controller\Cart\Add::execute() with simple product and activated redirect to cart + * + * @magentoDataFixture Magento/Catalog/_files/products.php + * @magentoConfigFixture current_store checkout/cart/redirect_to_cart 1 + * @magentoAppIsolation enabled + */ + public function testMessageAtAddToCartWithRedirect() + { + $formKey = $this->_objectManager->get(FormKey::class); + $postData = [ + 'qty' => '1', + 'product' => '1', + 'custom_price' => 1, + 'form_key' => $formKey->getFormKey(), + 'isAjax' => 1 + ]; + \Magento\TestFramework\Helper\Bootstrap::getInstance()->loadArea('frontend'); + $this->getRequest()->setPostValue($postData); + $this->getRequest()->setMethod('POST'); + + $this->dispatch('checkout/cart/add'); + + $this->assertEquals( + '{"backUrl":"http:\/\/localhost\/index.php\/checkout\/cart\/"}', + $this->getResponse()->getBody() + ); + + $this->assertSessionMessages( + $this->contains( + 'You added Simple Product to your shopping cart.' + ), + \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS + ); + } + + /** + * Test for \Magento\Checkout\Controller\Cart\Add::execute() with simple product and deactivated redirect to cart + * + * @magentoDataFixture Magento/Catalog/_files/products.php + * @magentoConfigFixture current_store checkout/cart/redirect_to_cart 0 + * @magentoAppIsolation enabled + */ + public function testMessageAtAddToCartWithoutRedirect() + { + $formKey = $this->_objectManager->get(FormKey::class); + $postData = [ + 'qty' => '1', + 'product' => '1', + 'custom_price' => 1, + 'form_key' => $formKey->getFormKey(), + 'isAjax' => 1 + ]; + \Magento\TestFramework\Helper\Bootstrap::getInstance()->loadArea('frontend'); + $this->getRequest()->setPostValue($postData); + $this->getRequest()->setMethod('POST'); + + $this->dispatch('checkout/cart/add'); + + $this->assertFalse($this->getResponse()->isRedirect()); + $this->assertEquals('[]', $this->getResponse()->getBody()); + + $this->assertSessionMessages( + $this->contains( + "\n" . 'You added Simple Product to your ' . + '<a href="http://localhost/index.php/checkout/cart/">shopping cart</a>.' + ), + \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS + ); + } + /** * @covers \Magento\Checkout\Controller\Cart\Addgroup::execute() * diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Model/SessionTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Model/SessionTest.php index 57cbaef54e58..af572c556bb0 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/Model/SessionTest.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/Model/SessionTest.php @@ -5,19 +5,50 @@ */ namespace Magento\Checkout\Model; +use Magento\Catalog\Api\Data\ProductTierPriceInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartInterface; use Magento\TestFramework\Helper\Bootstrap; +/** + * Class SessionTest + */ class SessionTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Checkout\Model\Session + * @var \Magento\Framework\ObjectManagerInterface + */ + private $objectManager; + + /** + * @var CustomerRepositoryInterface */ - protected $_checkoutSession; + private $customerRepository; + /** + * @var CustomerSession + */ + private $customerSession; + + /** + * @var Session + */ + private $checkoutSession; + + /** + * @return void + */ protected function setUp() { - $this->_checkoutSession = Bootstrap::getObjectManager()->create(\Magento\Checkout\Model\Session::class); - parent::setUp(); + $this->objectManager = Bootstrap::getObjectManager(); + $this->customerRepository = $this->objectManager->create(CustomerRepositoryInterface::class); + $this->customerSession = $this->objectManager->get(CustomerSession::class); + $this->checkoutSession = $this->objectManager->create(Session::class); } /** @@ -29,15 +60,11 @@ protected function setUp() */ public function testGetQuoteNotInitializedCustomerSet() { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - - /** @var \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository */ - $customerRepository = $objectManager->create(\Magento\Customer\Api\CustomerRepositoryInterface::class); - $customer = $customerRepository->getById(1); - $this->_checkoutSession->setCustomerData($customer); + $customer = $this->customerRepository->getById(1); + $this->checkoutSession->setCustomerData($customer); /** Execute SUT */ - $quote = $this->_checkoutSession->getQuote(); + $quote = $this->checkoutSession->getQuote(); $this->_validateCustomerDataInQuote($quote); } @@ -51,17 +78,11 @@ public function testGetQuoteNotInitializedCustomerSet() */ public function testGetQuoteNotInitializedCustomerLoggedIn() { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - - /** @var \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository */ - $customerRepository = $objectManager->create(\Magento\Customer\Api\CustomerRepositoryInterface::class); - $customer = $customerRepository->getById(1); - /** @var \Magento\Customer\Model\Session $customerSession */ - $customerSession = Bootstrap::getObjectManager()->get(\Magento\Customer\Model\Session::class); - $customerSession->setCustomerDataObject($customer); + $customer = $this->customerRepository->getById(1); + $this->customerSession->setCustomerDataObject($customer); /** Execute SUT */ - $quote = $this->_checkoutSession->getQuote(); + $quote = $this->checkoutSession->getQuote(); $this->_validateCustomerDataInQuote($quote); } @@ -80,29 +101,86 @@ public function testGetQuoteNotInitializedCustomerLoggedIn() */ public function testLoadCustomerQuoteCustomerWithoutQuote() { - $quote = $this->_checkoutSession->getQuote(); + $quote = $this->checkoutSession->getQuote(); $this->assertEmpty($quote->getCustomerId(), 'Precondition failed: Customer data must not be set to quote'); $this->assertEmpty($quote->getCustomerEmail(), 'Precondition failed: Customer data must not be set to quote'); - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - - /** @var \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository */ - $customerRepository = $objectManager->create(\Magento\Customer\Api\CustomerRepositoryInterface::class); - $customer = $customerRepository->getById(1); - /** @var \Magento\Customer\Model\Session $customerSession */ - $customerSession = Bootstrap::getObjectManager()->get(\Magento\Customer\Model\Session::class); - $customerSession->setCustomerDataObject($customer); + $customer = $this->customerRepository->getById(1); + $this->customerSession->setCustomerDataObject($customer); /** Ensure that customer data is still unavailable before SUT invocation */ - $quote = $this->_checkoutSession->getQuote(); + $quote = $this->checkoutSession->getQuote(); $this->assertEmpty($quote->getCustomerEmail(), 'Precondition failed: Customer data must not be set to quote'); /** Execute SUT */ - $this->_checkoutSession->loadCustomerQuote(); - $quote = $this->_checkoutSession->getQuote(); + $this->checkoutSession->loadCustomerQuote(); + $quote = $this->checkoutSession->getQuote(); $this->_validateCustomerDataInQuote($quote); } + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Sales/_files/quote.php + */ + public function testGetQuoteWithProductWithTierPrice() + { + $reservedOrderId = 'test01'; + $customerGroupId = 1; + $tierPriceQty = 1; + $tierPriceValue = 9; + + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get('simple'); + $tierPrice = $this->objectManager->create(ProductTierPriceInterface::class) + ->setCustomerGroupId($customerGroupId) + ->setQty($tierPriceQty) + ->setValue($tierPriceValue); + $product->setTierPrices([$tierPrice]); + $productRepository->save($product); + + $quote = $this->getQuote($reservedOrderId); + $this->checkoutSession->setQuoteId($quote->getId()); + + $quote = $this->checkoutSession->getQuote(); + $item = $quote->getItems()[0]; + /** @var \Magento\Catalog\Model\Product $quoteProduct */ + $quoteProduct = $item->getProduct(); + $this->assertEquals(10, $quoteProduct->getTierPrice($tierPriceQty)); + + $customer = $this->customerRepository->getById(1); + $this->customerSession->setCustomerDataAsLoggedIn($customer); + + $quote = $this->checkoutSession->getQuote(); + $item = $quote->getItems()[0]; + /** @var \Magento\Catalog\Model\Product $quoteProduct */ + $quoteProduct = $item->getProduct(); + $this->assertEquals($tierPriceValue, $quoteProduct->getTierPrice(1)); + } + + /** + * Returns quote by reserved order id. + * + * @param string $reservedOrderId + * @return CartInterface + */ + private function getQuote(string $reservedOrderId): CartInterface + { + $filterBuilder = $this->objectManager->create(FilterBuilder::class); + $filter = $filterBuilder->setField('reserved_order_id') + ->setConditionType('=') + ->setValue($reservedOrderId) + ->create(); + $searchCriteriaBuilder = $this->objectManager->create(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilters([$filter]) + ->create(); + $quoteRepository = $this->objectManager->get(CartRepositoryInterface::class); + $searchResult = $quoteRepository->getList($searchCriteria); + /** @var CartInterface[] $items */ + $items = $searchResult->getItems(); + + return \array_values($items)[0]; + } + /** * Ensure that quote has customer data specified in customer fixture. * diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Plugin/Model/Quote/ResetQuoteAddressesTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Plugin/Model/Quote/ResetQuoteAddressesTest.php index e533dbfa0c87..60ccdb88676a 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/Plugin/Model/Quote/ResetQuoteAddressesTest.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/Plugin/Model/Quote/ResetQuoteAddressesTest.php @@ -22,6 +22,7 @@ class ResetQuoteAddressesTest extends \PHPUnit\Framework\TestCase /** * @magentoDataFixture Magento/Checkout/_files/quote_with_virtual_product_and_address.php * + * @magentoAppArea frontend * @return void */ public function testAfterRemoveItem(): void @@ -52,9 +53,15 @@ public function testAfterRemoveItem(): void $cart = Bootstrap::getObjectManager()->create(Cart::class); $activeQuote = $cart->getQuote(); + // Dummy data is still persisted here. This is sufficient to check that it is removed + $activeQuote->getExtensionAttributes()->setShippingAssignments(['test']); + $cart->removeItem($activeQuote->getAllVisibleItems()[0]->getId()); $cart->save(); + // Check that the shipping assignments were also unset + $this->assertEmpty($activeQuote->getExtensionAttributes()->getShippingAssignments()); + /** @var Quote $quote */ $quote = Bootstrap::getObjectManager()->create(Quote::class); $quote->load('test_order_with_virtual_product', 'reserved_order_id'); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote.php index 2d948ebeb012..52437ef828af 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote.php @@ -9,3 +9,11 @@ ->setIsMultiShipping(false) ->setReservedOrderId('test_order_1') ->save(); + +/** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ +$quoteIdMask = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Quote\Model\QuoteIdMaskFactory::class) + ->create(); +$quoteIdMask->setQuoteId($quote->getId()); +$quoteIdMask->setDataChanges(true); +$quoteIdMask->save(); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote_customer_not_default_store.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote_customer_not_default_store.php new file mode 100644 index 000000000000..6448a570424e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote_customer_not_default_store.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +include 'testsuite/Magento/Store/_files/second_store.php'; +include 'testsuite/Magento/Customer/_files/customer.php'; + +$quote = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Quote\Model\Quote::class); +$quote->setStoreId($store->getId()) + ->setIsActive(true) + ->setIsMultiShipping(false) + ->setReservedOrderId('test_order_1_not_default_store') + ->setCustomerId($customer->getId()) + ->save(); + +/** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ +$quoteIdMask = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Quote\Model\QuoteIdMaskFactory::class) + ->create(); +$quoteIdMask->setQuoteId($quote->getId()); +$quoteIdMask->setDataChanges(true); +$quoteIdMask->save(); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote_customer_not_default_store_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote_customer_not_default_store_rollback.php new file mode 100644 index 000000000000..e3e1513cb614 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote_customer_not_default_store_rollback.php @@ -0,0 +1,8 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +$quote = $objectManager->create(\Magento\Quote\Model\Quote::class); +$quote->load('test_order_1_not_default_store', 'reserved_order_id')->delete(); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote_guest_not_default_store.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote_guest_not_default_store.php new file mode 100644 index 000000000000..bbd3d5efbe8c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote_guest_not_default_store.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +include 'testsuite/Magento/Store/_files/second_store.php'; + +$quote = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Quote\Model\Quote::class); +$quote->setStoreId($store->getId()) + ->setIsActive(true) + ->setIsMultiShipping(false) + ->setReservedOrderId('test_order_1_not_default_store_guest') + ->save(); + +/** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ +$quoteIdMask = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Quote\Model\QuoteIdMaskFactory::class) + ->create(); +$quoteIdMask->setQuoteId($quote->getId()); +$quoteIdMask->setDataChanges(true); +$quoteIdMask->save(); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote_guest_not_default_store_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote_guest_not_default_store_rollback.php new file mode 100644 index 000000000000..f511133280e7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/active_quote_guest_not_default_store_rollback.php @@ -0,0 +1,8 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +$quote = $objectManager->create(\Magento\Quote\Model\Quote::class); +$quote->load('test_order_1_not_default_store_guest', 'reserved_order_id')->delete(); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/cart_with_simple_product_and_custom_options.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/cart_with_simple_product_and_custom_options.php new file mode 100644 index 000000000000..22367979adca --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/cart_with_simple_product_and_custom_options.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\Data\ProductCustomOptionInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Option; +use Magento\Checkout\Model\Session; +use Magento\Framework\DataObject; +use Magento\Quote\Model\Quote; +use Magento\TestFramework\Helper\Bootstrap; + +require __DIR__ . '/../../../Magento/Catalog/_files/product_simple_with_custom_options.php'; + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager() + ->create(ProductRepositoryInterface::class); +$product = $productRepository->get('simple_with_custom_options'); + +$options = []; +/** @var $option Option */ +foreach ($product->getOptions() as $option) { + switch ($option->getGroupByType()) { + case ProductCustomOptionInterface::OPTION_GROUP_SELECT: + $value = key($option->getValues()); + break; + default: + $value = 'test'; + break; + } + $options[$option->getId()] = $value; +} + +$requestInfo = new DataObject(['qty' => 1, 'options' => $options]); + +/** @var $cart \Magento\Checkout\Model\Cart */ +$quote = Bootstrap::getObjectManager()->create(Quote::class); +$quote->setReservedOrderId('test_order_item_with_custom_options'); +$quote->addProduct($product, $requestInfo); +$quote->save(); + +/** @var $objectManager \Magento\TestFramework\ObjectManager */ +$objectManager = Bootstrap::getObjectManager(); +$objectManager->removeSharedInstance(Session::class); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/discount_10percent_generalusers.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/discount_10percent_generalusers.php index 507f6b755bcd..e66227a60e8f 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/_files/discount_10percent_generalusers.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/discount_10percent_generalusers.php @@ -21,7 +21,7 @@ ], 'customer_group_ids' => [1], 'coupon_type' => \Magento\SalesRule\Model\Rule::COUPON_TYPE_SPECIFIC, - 'coupon_code' => uniqid(), + 'coupon_code' => '2?ds5!2d', 'simple_action' => \Magento\SalesRule\Model\Rule::BY_PERCENT_ACTION, 'discount_amount' => 10, 'discount_step' => 1 diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_shipping_method.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_shipping_method.php index 3c54fe16db7d..61779da29c65 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_shipping_method.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_shipping_method.php @@ -11,10 +11,18 @@ require 'quote_with_address_saved.php'; +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +$rate = $objectManager->get(\Magento\Quote\Model\Quote\Address\Rate::class); + $quote->load('test_order_1', 'reserved_order_id'); $shippingAddress = $quote->getShippingAddress(); $shippingAddress->setShippingMethod('flatrate_flatrate') ->setShippingDescription('Flat Rate - Fixed') - ->setShippingAmount(10.0) - ->setBaseShippingAmount(12.0) ->save(); + +$rate->setPrice(0) + ->setAddressId($shippingAddress->getId()) + ->save(); +$shippingAddress->setBaseShippingAmount($rate->getPrice()); +$shippingAddress->setShippingAmount($rate->getPrice()); +$rate->delete(); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_simple_product_saved_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_simple_product_saved_rollback.php index 105a981ccfc8..39b758447221 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_simple_product_saved_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_simple_product_saved_rollback.php @@ -14,3 +14,5 @@ /** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ $quoteIdMask = $objectManager->create(\Magento\Quote\Model\QuoteIdMask::class); $quoteIdMask->delete($quote->getId()); + +require 'simple_product_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_virtual_product_and_address_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_virtual_product_and_address_rollback.php index 2b906bdc022f..402ad030ed85 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_virtual_product_and_address_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_virtual_product_and_address_rollback.php @@ -13,3 +13,7 @@ /** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ $quoteIdMask = $objectManager->create(\Magento\Quote\Model\QuoteIdMask::class); $quoteIdMask->delete($quote->getId()); + +require __DIR__ . '/../../Customer/_files/customer_rollback.php'; +require __DIR__ . '/../../Customer/_files/customer_address_rollback.php'; +require __DIR__ . '/../../Catalog/_files/product_virtual_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_virtual_product_saved.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_virtual_product_saved.php index 835b2ab81285..833e5a57ac34 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_virtual_product_saved.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_virtual_product_saved.php @@ -13,6 +13,7 @@ ->setIsMultiShipping(false) ->setReservedOrderId('test_order_with_virtual_product_without_address') ->setEmail('store@example.com') + ->setCustomerEmail('store@example.com') ->addProduct( $product->load($product->getId()), 1 diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_virtual_product_saved_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_virtual_product_saved_rollback.php index b3224bb52744..afcb7f56f8d1 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_virtual_product_saved_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_virtual_product_saved_rollback.php @@ -14,3 +14,5 @@ /** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ $quoteIdMask = $objectManager->create(\Magento\Quote\Model\QuoteIdMask::class); $quoteIdMask->delete($quote->getId()); + +require __DIR__ . '/../../../Magento/Catalog/_files/product_virtual_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/simple_product_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/simple_product_rollback.php new file mode 100644 index 000000000000..d8744873af00 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/simple_product_rollback.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +/** @var \Magento\Framework\ObjectManagerInterface $objectManager */ +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var \Magento\Framework\Registry $registry */ +$registry = $objectManager->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +try { + $product = $productRepository->get('simple', false, null, true); + $productRepository->delete($product); +} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { + //Product already removed +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/Model/ResourceModel/Grid/CollectionTest.php b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/Model/ResourceModel/Grid/CollectionTest.php new file mode 100644 index 000000000000..bc098cf1bd0e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/Model/ResourceModel/Grid/CollectionTest.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CheckoutAgreements\Model\ResourceModel\Grid; + +use Magento\CheckoutAgreements\Model\ResourceModel\Agreement\Grid\Collection; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Check data in collection + */ +class CollectionTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Collection; + */ + private $collection; + + /** + * @inheritdoc + */ + public function setUp() + { + $this->collection = Bootstrap::getObjectManager() + ->create(Collection::class); + } + + /** + * Check that collection is filterable by store + * + * @magentoDataFixture Magento/CheckoutAgreements/_files/multi_agreements_active_with_text.php + */ + public function testAddStoresToFilter(): void + { + $collectionSize = $this->collection->addStoreFilter(1) + ->load(false, false) + ->getSize(); + $this->assertEquals(2, $collectionSize); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Cms/Block/Widget/BlockTest.php b/dev/tests/integration/testsuite/Magento/Cms/Block/Widget/BlockTest.php index 7ee1f569180a..266fd66bc3db 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Block/Widget/BlockTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Block/Widget/BlockTest.php @@ -31,5 +31,6 @@ public function testToHtml() $this->assertContains('<a href="http://example.com/', $result); $this->assertContains('<p>Config value: "http://example.com/".</p>', $result); $this->assertContains('<p>Custom variable: "HTML Value".</p>', $result); + $this->assertSame($cmsBlock->getIdentities(), $block->getIdentities()); } } diff --git a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFilesTest.php b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFilesTest.php index fa49a7b1f57e..1fc07d32c77b 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFilesTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFilesTest.php @@ -64,6 +64,10 @@ protected function setUp() $filePath = $this->fullDirectoryPath . DIRECTORY_SEPARATOR . $this->fileName; $fixtureDir = realpath(__DIR__ . '/../../../../../Catalog/_files'); copy($fixtureDir . '/' . $this->fileName, $filePath); + $path = $this->fullDirectoryPath . '/.htaccess'; + if (!$this->mediaDirectory->isFile($path)) { + $this->mediaDirectory->writeFile($path, "Order deny,allow\nDeny from all"); + } $this->model = $this->objectManager->get(\Magento\Cms\Controller\Adminhtml\Wysiwyg\Images\DeleteFiles::class); } @@ -87,6 +91,26 @@ public function testExecute() ); } + /** + * Check that htaccess file couldn't be removed via + * \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images\DeleteFiles::execute method + * + * @return void + */ + public function testDeleteHtaccess() + { + $this->model->getRequest()->setMethod('POST') + ->setPostValue('files', [$this->imagesHelper->idEncode('.htaccess')]); + $this->model->getStorage()->getSession()->setCurrentPath($this->fullDirectoryPath); + $this->model->execute(); + + $this->assertTrue( + $this->mediaDirectory->isExist( + $this->mediaDirectory->getRelativePath($this->fullDirectoryPath . '/' . '.htaccess') + ) + ); + } + /** * Execute method with traversal file path to check that there is no ability to remove file which is not * under media directory. diff --git a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolderTest.php b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolderTest.php index a1a29706756b..c574869a83ca 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolderTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolderTest.php @@ -66,6 +66,7 @@ public function testExecute() $this->mediaDirectory->getRelativePath($this->fullDirectoryPath . $directoryName) ); $this->model->getRequest()->setParams(['node' => $this->imagesHelper->idEncode($directoryName)]); + $this->model->getRequest()->setMethod('POST'); $this->model->execute(); $this->assertFalse( diff --git a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/UploadTest.php b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/UploadTest.php index bab14a8663ea..00f56e570041 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/UploadTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/UploadTest.php @@ -4,9 +4,14 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Cms\Controller\Adminhtml\Wysiwyg\Images; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Controller\Result\Json as JsonResponse; +use Magento\Framework\App\Response\HttpFactory as ResponseFactory; +use Magento\Framework\App\Response\Http as Response; /** * Test for \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images\Upload class. @@ -43,6 +48,11 @@ class UploadTest extends \PHPUnit\Framework\TestCase */ private $objectManager; + /** + * @var HttpFactory + */ + private $responseFactory; + /** * @inheritdoc */ @@ -56,6 +66,7 @@ protected function setUp() $this->mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); $this->fullDirectoryPath = $imagesHelper->getStorageRoot() . DIRECTORY_SEPARATOR . $directoryName; $this->mediaDirectory->create($this->mediaDirectory->getRelativePath($this->fullDirectoryPath)); + $this->responseFactory = $this->objectManager->get(ResponseFactory::class); $this->model = $this->objectManager->get(\Magento\Cms\Controller\Adminhtml\Wysiwyg\Images\Upload::class); $fixtureDir = realpath(__DIR__ . '/../../../../../Catalog/_files'); $tmpFile = $this->filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath() . $this->fileName; @@ -81,8 +92,13 @@ protected function setUp() public function testExecute() { $this->model->getRequest()->setParams(['type' => 'image/png']); + $this->model->getRequest()->setMethod('POST'); $this->model->getStorage()->getSession()->setCurrentPath($this->fullDirectoryPath); - $this->model->execute(); + /** @var JsonResponse $jsonResponse */ + $jsonResponse = $this->model->execute(); + /** @var Response $response */ + $jsonResponse->renderResult($response = $this->responseFactory->create()); + $data = json_decode($response->getBody(), true); $this->assertTrue( $this->mediaDirectory->isExist( @@ -91,6 +107,12 @@ public function testExecute() ) ) ); + //Asserting that response contains only data needed by clients. + $keys = ['name', 'type', 'error', 'size', 'file']; + sort($keys); + $dataKeys = array_keys($data); + sort($dataKeys); + $this->assertEquals($keys, $dataKeys); } /** diff --git a/dev/tests/integration/testsuite/Magento/Cms/Model/BlockTest.php b/dev/tests/integration/testsuite/Magento/Cms/Model/BlockTest.php index 16331ccde245..3925975a0f70 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Model/BlockTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Model/BlockTest.php @@ -7,6 +7,7 @@ use Magento\Cms\Model\ResourceModel\Block; use Magento\Cms\Model\BlockFactory; +use Magento\Framework\App\ResourceConnection; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Stdlib\DateTime\DateTime; use Magento\Framework\Stdlib\DateTime\Timezone; @@ -82,23 +83,26 @@ public function testGetByIdentifier(array $blockData) */ public function testUpdateTime(array $blockData) { + /** + * @var $db \Magento\Framework\DB\Adapter\AdapterInterface + */ + $db = $this->objectManager->get(\Magento\Framework\App\ResourceConnection::class) + ->getConnection(ResourceConnection::DEFAULT_CONNECTION); # Prepare and save the temporary block - $beforeTimestamp = $this->objectManager->get(DateTime::class)->timestamp(); $tempBlock = $this->blockFactory->create(); $tempBlock->setData($blockData); + $beforeTimestamp = $db->fetchCol('SELECT UNIX_TIMESTAMP()')[0]; $this->blockResource->save($tempBlock); + $afterTimestamp = $db->fetchCol('SELECT UNIX_TIMESTAMP()')[0]; # Load previously created block and compare identifiers $storeId = reset($blockData['stores']); $block = $this->blockIdentifier->execute($blockData['identifier'], $storeId); - $afterTimestamp = $this->objectManager->get(DateTime::class)->timestamp(); $blockTimestamp = strtotime($block->getUpdateTime()); /* - * This test used to fail due to a race condition @see MAGETWO-87353 - * The DB time would be one second older than the check time. The new check allows the DB time - * to be between the test start time and right before the assertion. + * These checks prevent a race condition MAGETWO-87353 */ $this->assertGreaterThanOrEqual($beforeTimestamp, $blockTimestamp); $this->assertLessThanOrEqual($afterTimestamp, $blockTimestamp); diff --git a/dev/tests/integration/testsuite/Magento/Cms/Model/PageTest.php b/dev/tests/integration/testsuite/Magento/Cms/Model/PageTest.php index 42697e1b66b7..83e7c678ffc5 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Model/PageTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Model/PageTest.php @@ -6,6 +6,7 @@ namespace Magento\Cms\Model; use Magento\Cms\Api\PageRepositoryInterface; +use Magento\Framework\App\ResourceConnection; use Magento\Framework\Stdlib\DateTime\DateTime; /** @@ -79,13 +80,27 @@ public function testGenerateIdentifierFromTitle($data, $expectedIdentifier) public function testUpdateTime() { $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + + /** + * @var $db \Magento\Framework\DB\Adapter\AdapterInterface + */ + $db = $objectManager->get(\Magento\Framework\App\ResourceConnection::class) + ->getConnection(ResourceConnection::DEFAULT_CONNECTION); + /** @var \Magento\Cms\Model\Page $page */ $page = $objectManager->create(\Magento\Cms\Model\Page::class); $page->setData(['title' => 'Test', 'stores' => [1]]); + $beforeTimestamp = $db->fetchCol('SELECT UNIX_TIMESTAMP()')[0]; $page->save(); + $afterTimestamp = $db->fetchCol('SELECT UNIX_TIMESTAMP()')[0]; $page = $objectManager->get(PageRepositoryInterface::class)->getById($page->getId()); - $date = $objectManager->get(DateTime::class)->date(); - $this->assertEquals($date, $page->getUpdateTime()); + $pageTimestamp = strtotime($page->getUpdateTime()); + + /* + * These checks prevent a race condition MAGETWO-95534 + */ + $this->assertGreaterThanOrEqual($beforeTimestamp, $pageTimestamp); + $this->assertLessThanOrEqual($afterTimestamp, $pageTimestamp); } public function generateIdentifierFromTitleDataProvider() : array diff --git a/dev/tests/integration/testsuite/Magento/Cms/_files/block_rollback.php b/dev/tests/integration/testsuite/Magento/Cms/_files/block_rollback.php new file mode 100644 index 000000000000..9214dd543bd3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Cms/_files/block_rollback.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Cms\Api\BlockRepositoryInterface; +use Magento\Cms\Api\Data\BlockInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var BlockRepositoryInterface $blockRepository */ +$blockRepository = $objectManager->get(BlockRepositoryInterface::class); + +/** @var SearchCriteriaBuilder $searchCriteriaBuilder */ +$searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); +$searchCriteria = $searchCriteriaBuilder->addFilter(BlockInterface::IDENTIFIER, 'fixture_block') + ->create(); +$result = $blockRepository->getList($searchCriteria); + +/** + * Tests which are wrapped with MySQL transaction clear all data by transaction rollback. + * In that case there is "if" which checks that "fixture_block" still exists in database. + */ +foreach ($result->getItems() as $item) { + $blockRepository->delete($item); +} diff --git a/dev/tests/integration/testsuite/Magento/Cms/_files/blocks.php b/dev/tests/integration/testsuite/Magento/Cms/_files/blocks.php new file mode 100644 index 000000000000..bbdc697645a4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Cms/_files/blocks.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Cms\Api\BlockRepositoryInterface; +use Magento\Cms\Api\Data\BlockInterface; +use Magento\Cms\Api\Data\BlockInterfaceFactory; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var BlockRepositoryInterface $blockRepository */ +$blockRepository = Bootstrap::getObjectManager()->get(BlockRepositoryInterface::class); +/** @var BlockInterfaceFactory $blockFactory */ +$blockFactory = Bootstrap::getObjectManager()->get(BlockInterfaceFactory::class); +$storeId = Bootstrap::getObjectManager()->get(StoreManagerInterface::class)->getStore()->getId(); + +/** @var BlockInterface $block */ +$block = $blockFactory->create([ + 'data' => [ + BlockInterface::IDENTIFIER => 'enabled_block', + BlockInterface::TITLE => 'Enabled CMS Block Title', + BlockInterface::CONTENT => ' + <h1>Enabled Block</h1> + <a href="{{store url=""}}">store url</a> + <p>Config value: "{{config path="web/unsecure/base_url"}}".</p> + <p>Custom variable: "{{customvar code="variable_code"}}".</p> + ', + BlockInterface::IS_ACTIVE => 1, + 'store_id' => [$storeId], + ] +]); +$blockRepository->save($block); + +/** @var BlockInterface $block */ +$block = $blockFactory->create([ + 'data' => [ + BlockInterface::IDENTIFIER => 'disabled_block', + BlockInterface::TITLE => 'Disabled CMS Block Title', + BlockInterface::CONTENT => ' + <h1>Disabled Block</h1> + <a href="{{store url=""}}">store url</a> + <p>Config value: "{{config path="web/unsecure/base_url"}}".</p> + <p>Custom variable: "{{customvar code="variable_code"}}".</p> + ', + BlockInterface::IS_ACTIVE => 0, + 'store_id' => [$storeId], + ] +]); +$blockRepository->save($block); diff --git a/dev/tests/integration/testsuite/Magento/Cms/_files/blocks_rollback.php b/dev/tests/integration/testsuite/Magento/Cms/_files/blocks_rollback.php new file mode 100644 index 000000000000..a7a2a51524cf --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Cms/_files/blocks_rollback.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Cms\Api\BlockRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var BlockRepositoryInterface $blockRepository */ +$blockRepository = Bootstrap::getObjectManager()->get(BlockRepositoryInterface::class); + +foreach (['enabled_block', 'disabled_block'] as $blockId) { + try { + $blockRepository->deleteById($blockId); + } catch (NoSuchEntityException $e) { + /** + * Tests which are wrapped with MySQL transaction clear all data by transaction rollback. + */ + } +} diff --git a/dev/tests/integration/testsuite/Magento/Cms/_files/pages.php b/dev/tests/integration/testsuite/Magento/Cms/_files/pages.php index a0b7b99a877a..b2742ecd380f 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/_files/pages.php +++ b/dev/tests/integration/testsuite/Magento/Cms/_files/pages.php @@ -11,6 +11,10 @@ ->setStores([0]) ->setIsActive(1) ->setContent('<h1>Cms Page 100 Title</h1>') + ->setContentHeading('<h2>Cms Page 100 Title</h2>') + ->setMetaTitle('Cms Meta title for page100') + ->setMetaKeywords('Cms Meta Keywords for page100') + ->setMetaDescription('Cms Meta Description for page100') ->setPageLayout('1column') ->save(); @@ -20,6 +24,10 @@ ->setStores([0]) ->setIsActive(1) ->setContent('<h1>Cms Page Design Blank Title</h1>') + ->setContentHeading('<h2>Cms Page Blank Title</h2>') + ->setMetaTitle('Cms Meta title for Blank page') + ->setMetaKeywords('Cms Meta Keywords for Blank page') + ->setMetaDescription('Cms Meta Description for Blank page') ->setPageLayout('1column') ->setCustomTheme('Magento/blank') ->save(); diff --git a/dev/tests/integration/testsuite/Magento/Cms/_files/pages_rollback.php b/dev/tests/integration/testsuite/Magento/Cms/_files/pages_rollback.php new file mode 100644 index 000000000000..038b36db32a3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Cms/_files/pages_rollback.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Api\PageRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var PageRepositoryInterface $pageRepository */ +$pageRepository = $objectManager->get(PageRepositoryInterface::class); + +/** @var SearchCriteriaBuilder $searchCriteriaBuilder */ +$searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); +$searchCriteria = $searchCriteriaBuilder->addFilter(PageInterface::IDENTIFIER, ['page100', 'page_design_blank'], 'in') + ->create(); +$result = $pageRepository->getList($searchCriteria); + +/** + * Tests which are wrapped with MySQL transaction clear all data by transaction rollback. + * In that case there is "if" which checks that "page100" and "page_design_blank" still exists in database. + */ +foreach ($result->getItems() as $item) { + $pageRepository->delete($item); +} 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 f68f3cf37b07..e59672f1b5e1 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; @@ -28,7 +30,7 @@ /** * Tests the different flows of config:set command. * - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @magentoDbIsolation enabled */ @@ -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'], ]; } @@ -292,8 +313,7 @@ public function runExtendedDataProvider() * @param string $scope * @param $scopeCode string|null * @dataProvider configSetValidationErrorDataProvider - * - * @magentoDbIsolation enabled + * @magentoDbIsolation disabled */ public function testConfigSetValidationError( $path, @@ -307,6 +327,7 @@ public function testConfigSetValidationError( /** * Data provider for testConfigSetValidationError + * * @return array */ public function configSetValidationErrorDataProvider() @@ -399,7 +420,6 @@ public function testConfigSetCurrency() * Saving values with successful validation * * @dataProvider configSetValidDataProvider - * * @magentoDbIsolation enabled */ public function testConfigSetValid() diff --git a/dev/tests/integration/testsuite/Magento/Config/Controller/Adminhtml/System/ConfigTest.php b/dev/tests/integration/testsuite/Magento/Config/Controller/Adminhtml/System/ConfigTest.php index 2ad15fbea6c4..24b68e804cd5 100644 --- a/dev/tests/integration/testsuite/Magento/Config/Controller/Adminhtml/System/ConfigTest.php +++ b/dev/tests/integration/testsuite/Magento/Config/Controller/Adminhtml/System/ConfigTest.php @@ -7,12 +7,16 @@ namespace Magento\Config\Controller\Adminhtml\System; use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Request\Http as HttpRequest; /** * @magentoAppArea adminhtml */ class ConfigTest extends \Magento\TestFramework\TestCase\AbstractBackendController { + /** + * Test Configuration page existing. + */ public function testEditAction() { $this->dispatch('backend/admin/system_config/edit'); @@ -20,6 +24,8 @@ public function testEditAction() } /** + * Test redirect after changing base URL. + * * @magentoAppIsolation enabled * @magentoDbIsolation enabled */ @@ -43,6 +49,8 @@ public function testChangeBaseUrl() )->setParam( 'section', 'web' + )->setMethod( + HttpRequest::METHOD_POST ); $this->dispatch('backend/admin/system_config/save'); @@ -60,7 +68,9 @@ public function testChangeBaseUrl() } /** - * Reset test framework default base url + * Reset test framework default base url. + * + * @param string $defaultHost */ protected function resetBaseUrl($defaultHost) { diff --git a/dev/tests/integration/testsuite/Magento/Config/Model/Config/Structure/Reader/ConverterStub.php b/dev/tests/integration/testsuite/Magento/Config/Model/Config/Structure/Reader/ConverterStub.php new file mode 100644 index 000000000000..7493d31f02b3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Config/Model/Config/Structure/Reader/ConverterStub.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Config\Model\Config\Structure\Reader; + +use Magento\Config\Model\Config\Structure\Converter; + +/** + * Class ConverterStub used for ReaderTest. + */ +class ConverterStub extends Converter +{ + /** + * Convert dom document wrapper. + * + * @param \DOMDocument $document + * @return array|null + */ + public function getArrayData(\DOMDocument $document) + { + return $this->_convertDOMDocument($document); + } + + /** + * Convert dom document. + * + * @param \DOMNode $source + * @return array + */ + public function convert($source) + { + return $this->_convertDOMDocument($source); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Config/Model/Config/Structure/Reader/ReaderStub.php b/dev/tests/integration/testsuite/Magento/Config/Model/Config/Structure/Reader/ReaderStub.php new file mode 100644 index 000000000000..866ff91678ec --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Config/Model/Config/Structure/Reader/ReaderStub.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Config\Model\Config\Structure\Reader; + +use Magento\Config\Model\Config\Structure\Reader; + +/** + * Class ReaderStub used for testing protected Reader::_readFiles() method. + */ +class ReaderStub extends Reader +{ + /** + * Wrapper for protected Reader::_readFiles() method. + * + * @param array $fileList + * @return array + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function readFiles(array $fileList) + { + return $this->_readFiles($fileList); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Config/Model/Config/Structure/Reader/ReaderTest.php b/dev/tests/integration/testsuite/Magento/Config/Model/Config/Structure/Reader/ReaderTest.php new file mode 100644 index 000000000000..eef8e68458d9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Config/Model/Config/Structure/Reader/ReaderTest.php @@ -0,0 +1,146 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Config\Model\Config\Structure\Reader; + +use Magento\Config\Model\Config\SchemaLocator; +use Magento\Framework\App\Utility\Files; +use Magento\Framework\Config\Dom; +use Magento\Framework\Config\FileResolverInterface; +use Magento\Framework\Config\ValidationStateInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\TemplateEngine\Xhtml\CompilerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Class ReaderTest check Magento\Config\Model\Config\Structure\Reader::_readFiles() method. + */ +class ReaderTest extends \PHPUnit\Framework\TestCase +{ + /** + * Test config location. + * + * @string + */ + const CONFIG = '/dev/tests/integration/testsuite/Magento/Config/Model/Config/Structure/Reader/_files/'; + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var Files + */ + private $fileUtility; + + /** + * @var ValidationStateInterface + */ + private $validationStateMock; + + /** + * @var \Magento\Framework\Config\SchemaLocatorInterface + */ + private $schemaLocatorMock; + + /** + * @var FileResolverInterface + */ + private $fileResolverMock; + + /** + * @var ReaderStub + */ + private $reader; + + /** + * @var ConverterStub + */ + private $converter; + + /** + * @var CompilerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $compiler; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->fileUtility = Files::init(); + + $this->validationStateMock = $this->getMockBuilder(ValidationStateInterface::class) + ->setMethods(['isValidationRequired']) + ->getMockForAbstractClass(); + $this->schemaLocatorMock = $this->getMockBuilder(SchemaLocator::class) + ->disableOriginalConstructor() + ->setMethods(['getPerFileSchema']) + ->getMock(); + $this->fileResolverMock = $this->getMockBuilder(FileResolverInterface::class) + ->getMockForAbstractClass(); + + $this->validationStateMock->expects($this->atLeastOnce()) + ->method('isValidationRequired') + ->willReturn(false); + $this->schemaLocatorMock->expects($this->atLeastOnce()) + ->method('getPerFileSchema') + ->willReturn(false); + + $this->converter = $this->objectManager->create(ConverterStub::class); + + //Isolate test from actual configuration, and leave only sample data. + $this->compiler = $this->getMockBuilder(CompilerInterface::class) + ->disableOriginalConstructor() + ->setMethods(['compile']) + ->getMockForAbstractClass(); + + $this->reader = $this->objectManager->create( + ReaderStub::class, + [ + 'fileResolver' => $this->fileResolverMock, + 'converter' => $this->converter, + 'schemaLocator' => $this->schemaLocatorMock, + 'validationState' => $this->validationStateMock, + 'fileName' => 'no_existing_file.xml', + 'compiler' => $this->compiler, + 'domDocumentClass' => Dom::class + ] + ); + } + + /** + * The test checks the file structure after processing the nodes responsible for inserting content. + * + * @return void + */ + public function testXmlConvertedConfigurationAndCompereStructure() + { + $actual = $this->reader->readFiles(['actual' => $this->getContent()]); + + $document = new \DOMDocument(); + $document->loadXML($this->getContent()); + + $expected = $this->converter->getArrayData($document); + + $this->assertEquals($expected, $actual); + } + + /** + * Get config sample data for test. + * + * @return string + */ + protected function getContent() + { + $files = $this->fileUtility->getFiles([BP . static::CONFIG], 'config.xml'); + + return file_get_contents(reset($files)); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/_files/actual/config.xml b/dev/tests/integration/testsuite/Magento/Config/Model/Config/Structure/Reader/_files/config.xml similarity index 100% rename from dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/_files/actual/config.xml rename to dev/tests/integration/testsuite/Magento/Config/Model/Config/Structure/Reader/_files/config.xml diff --git a/dev/tests/integration/testsuite/Magento/Config/_files/system.xml b/dev/tests/integration/testsuite/Magento/Config/_files/system.xml new file mode 100644 index 000000000000..f0063a3c0bf7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Config/_files/system.xml @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="general"> + <group id="group"> + <group id="subgroup"> + <field id="field" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Label</label> + </field> + </group> + </group> + </section> + </system> +</config> diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/ConfigurableTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/ConfigurableTest.php index 5184a3756331..338daa56450d 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/ConfigurableTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/ConfigurableTest.php @@ -9,7 +9,10 @@ class ConfigurableTest extends AbstractProductExportImportTestCase { - public function exportImportDataProvider() + /** + * @return array + */ + public function exportImportDataProvider(): array { return [ 'configurable-product' => [ @@ -34,11 +37,12 @@ public function exportImportDataProvider() } /** - * @param \Magento\Catalog\Model\Product $expectedProduct - * @param \Magento\Catalog\Model\Product $actualProduct + * @inheritdoc */ - protected function assertEqualsSpecificAttributes($expectedProduct, $actualProduct) - { + protected function assertEqualsSpecificAttributes( + \Magento\Catalog\Model\Product $expectedProduct, + \Magento\Catalog\Model\Product $actualProduct + ): void { /** @var \Magento\ConfigurableProduct\Model\Product\Type\Configurable $productType */ $productType = $expectedProduct->getTypeInstance(); $expectedAssociatedProducts = $productType->getUsedProductCollection($expectedProduct); @@ -95,12 +99,16 @@ protected function assertEqualsSpecificAttributes($expectedProduct, $actualProdu } } - public function importReplaceDataProvider() - { - $data = $this->exportImportDataProvider(); - foreach ($data as $key => $value) { - $data[$key][2] = array_merge($value[2], ['_cache_instance_product_set_attributes']); - } - return $data; + /** + * @inheritdoc + */ + protected function executeImportReplaceTest( + $skus, + $skippedAttributes, + $usePagination = false, + string $csvfile = null + ) { + $skippedAttributes = array_merge($skippedAttributes, ['_cache_instance_product_set_attributes']); + parent::executeImportReplaceTest($skus, $skippedAttributes, $usePagination, $csvfile); } } diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Controller/Adminhtml/ProductTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Controller/Adminhtml/ProductTest.php index 4254a6ce9c71..b71507ae43f9 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Controller/Adminhtml/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Controller/Adminhtml/ProductTest.php @@ -9,6 +9,7 @@ use Magento\Framework\Registry; use Magento\TestFramework\ObjectManager; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\App\Request\Http as HttpRequest; /** * @magentoAppArea adminhtml @@ -23,6 +24,7 @@ public function testSaveActionAssociatedProductIds() { $associatedProductIds = ['3', '14', '15', '92']; $associatedProductIdsJSON = json_encode($associatedProductIds); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue( [ 'id' => 1, diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Controller/CartTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Controller/CartTest.php index 4e0b74ba0f90..0d93d3ad4f4a 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Controller/CartTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Controller/CartTest.php @@ -9,6 +9,8 @@ */ namespace Magento\ConfigurableProduct\Controller; +use Magento\Framework\App\Request\Http as HttpRequest; + class CartTest extends \Magento\TestFramework\TestCase\AbstractController { /** @@ -85,6 +87,7 @@ public function testExecuteForConfigurableLastOption() 'remove' => 0, 'coupon_code' => 'test' ]; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($inputData); $this->dispatch( 'checkout/cart/couponPost/' diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Plugin/Frontend/ProductIdentitiesExtenderTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Plugin/Frontend/ProductIdentitiesExtenderTest.php new file mode 100644 index 000000000000..1fffd701c509 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Plugin/Frontend/ProductIdentitiesExtenderTest.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Model\Plugin\Frontend; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Interception\PluginList; +use PHPUnit\Framework\TestCase; + +/** + * Test configurable fronted product plugin will add children products ids to configurable product identities. + */ +class ProductIdentitiesExtenderTest extends TestCase +{ + /** + * Check, product identities extender plugin is registered for storefront. + * + * @magentoAppArea frontend + * @return void + */ + public function testIdentitiesExtenderIsRegistered(): void + { + $pluginInfo = Bootstrap::getObjectManager()->get(PluginList::class) + ->get(\Magento\Catalog\Model\Product::class, []); + $this->assertSame(ProductIdentitiesExtender::class, $pluginInfo['product_identities_extender']['instance']); + } + + /** + * Check plugin will add children ids to configurable product identities on storefront. + * + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @magentoAppArea frontend + * @return void + */ + public function testGetIdentitiesForConfigurableProductOnStorefront(): void + { + $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + $configurableProduct = $productRepository->get('configurable'); + $simpleProduct1 = $productRepository->get('simple_10'); + $simpleProduct2 = $productRepository->get('simple_20'); + $expectedIdentities = [ + 'cat_p_' . $configurableProduct->getId(), + 'cat_p', + 'cat_p_' . $simpleProduct1->getId(), + 'cat_p_' . $simpleProduct2->getId(), + + ]; + $this->assertEquals($expectedIdentities, $configurableProduct->getIdentities()); + } + + /** + * Check plugin won't add children ids to configurable product identities in admin area. + * + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @magentoAppArea adminhtml + * @return void + */ + public function testGetIdentitiesForConfigurableProductInAdminArea(): void + { + $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + $configurableProduct = $productRepository->get('configurable'); + $expectedIdentities = [ + 'cat_p_' . $configurableProduct->getId(), + ]; + $this->assertEquals($expectedIdentities, $configurableProduct->getIdentities()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/ConfigurableTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/ConfigurableTest.php index 01a7ae6087e0..78fa4733a256 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/ConfigurableTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/ConfigurableTest.php @@ -4,6 +4,8 @@ * See COPYING.txt for license details. */ +// @codingStandardsIgnoreFile + namespace Magento\ConfigurableProduct\Model\Product\Type; use Magento\Catalog\Api\Data\ProductInterface; @@ -128,6 +130,10 @@ public function testGetUsedProductAttributes() $this->assertEquals($testConfigurable->getData(), $attributes[$attributeId]->getData()); } + /** + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + */ public function testGetConfigurableAttributes() { $collection = $this->model->getConfigurableAttributes($this->product); @@ -151,6 +157,19 @@ public function testGetConfigurableAttributes() } } + /** + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable_custom.php + */ + public function testGetConfigurableAttributesWithSourceModel() + { + $collection = $this->model->getConfigurableAttributes($this->product); + /** @var \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute $configurableAttribute */ + $configurableAttribute = $collection->getFirstItem(); + $attribute = $this->_getAttributeByCode('test_configurable_with_sm'); + $this->assertSameSize($attribute->getSource()->getAllOptions(), $configurableAttribute->getOptions()); + } + /** * @magentoAppIsolation enabled * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php @@ -319,8 +338,7 @@ public function testGetSelectedAttributesInfo() $attribute = reset($attributes); $optionValueId = $attribute['values'][0]['value_index']; - $product->addCustomOption( - 'attributes', + $product->addCustomOption('attributes', $serializer->serialize([$attribute['attribute_id'] => $optionValueId]) ); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php index 66091c108f0b..32eddb28151a 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php @@ -12,11 +12,18 @@ use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; +use Magento\CatalogInventory\Model\Stock; +use Magento\CatalogInventory\Api\StockItemRepositoryInterface; +use PHPUnit\Framework\TestCase; +use Magento\Catalog\Api\Data\ProductInterface; /** + * Configurable test + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @magentoAppArea adminhtml */ -class ConfigurableTest extends \PHPUnit\Framework\TestCase +class ConfigurableTest extends TestCase { /** * @var StoreManagerInterface @@ -28,26 +35,36 @@ class ConfigurableTest extends \PHPUnit\Framework\TestCase */ private $productRepository; - protected function setUp() + /** + * @var StockItemRepositoryInterface + */ + private $stockRepository; + + /** + * @inheritdoc + */ + protected function setUp(): void { $this->storeManager = Bootstrap::getObjectManager()->get(StoreManagerInterface::class); $this->productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + $this->stockRepository = Bootstrap::getObjectManager()->get(StockItemRepositoryInterface::class); } /** + * Test get product final price if one of child is disabled + * * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php * @magentoDbIsolation disabled + * + * @return void + * @throws \Magento\Framework\Exception\CouldNotSaveException + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Magento\Framework\Exception\StateException */ - public function testGetProductFinalPriceIfOneOfChildIsDisabled() + public function testGetProductFinalPriceIfOneOfChildIsDisabled(): void { - /** @var Collection $collection */ - $collection = Bootstrap::getObjectManager()->get(CollectionFactory::class) - ->create(); - $configurableProduct = $collection - ->addIdFilter([1]) - ->addMinimalPrice() - ->load() - ->getFirstItem(); + $configurableProduct = $this->getConfigurableProductFromCollection(); $this->assertEquals(10, $configurableProduct->getMinimalPrice()); $childProduct = $this->productRepository->getById(10, false, null, true); @@ -58,34 +75,28 @@ public function testGetProductFinalPriceIfOneOfChildIsDisabled() $this->productRepository->save($childProduct); $this->storeManager->setCurrentStore($currentStoreId); - /** @var Collection $collection */ - $collection = Bootstrap::getObjectManager()->get(CollectionFactory::class) - ->create(); - $configurableProduct = $collection - ->addIdFilter([1]) - ->addMinimalPrice() - ->load() - ->getFirstItem(); + $configurableProduct = $this->getConfigurableProductFromCollection(); $this->assertEquals(20, $configurableProduct->getMinimalPrice()); } /** + * Test get product final price if one of child is disabled per store + * * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php * @magentoDbIsolation disabled + * + * @return void + * @throws \Magento\Framework\Exception\CouldNotSaveException + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Magento\Framework\Exception\StateException */ - public function testGetProductFinalPriceIfOneOfChildIsDisabledPerStore() + public function testGetProductFinalPriceIfOneOfChildIsDisabledPerStore(): void { - /** @var Collection $collection */ - $collection = Bootstrap::getObjectManager()->get(CollectionFactory::class) - ->create(); - $configurableProduct = $collection - ->addIdFilter([1]) - ->addMinimalPrice() - ->load() - ->getFirstItem(); + $configurableProduct = $this->getConfigurableProductFromCollection(); $this->assertEquals(10, $configurableProduct->getMinimalPrice()); - $childProduct = $this->productRepository->getById(10, false, null, true); + $childProduct = $this->productRepository->get('simple_10', false, null, true); $childProduct->setStatus(Status::STATUS_DISABLED); // update in default store scope @@ -95,14 +106,53 @@ public function testGetProductFinalPriceIfOneOfChildIsDisabledPerStore() $this->productRepository->save($childProduct); $this->storeManager->setCurrentStore($currentStoreId); + $configurableProduct = $this->getConfigurableProductFromCollection(); + $this->assertEquals(20, $configurableProduct->getMinimalPrice()); + } + + /** + * Test get product minimal price if one child is out of stock + * + * @magentoConfigFixture current_store cataloginventory/options/show_out_of_stock 1 + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @magentoDbIsolation disabled + * + * @return void + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function testGetProductMinimalPriceIfOneOfChildIsOutOfStock(): void + { + $configurableProduct = $this->getConfigurableProductFromCollection(); + $this->assertEquals(10, $configurableProduct->getMinimalPrice()); + + $childProduct = $this->productRepository->getById(10, false, null, true); + $stockItem = $childProduct->getExtensionAttributes()->getStockItem(); + $stockItem->setIsInStock(Stock::STOCK_OUT_OF_STOCK); + $this->stockRepository->save($stockItem); + + $configurableProduct = $this->getConfigurableProductFromCollection(); + $this->assertEquals(20, $configurableProduct->getMinimalPrice()); + } + + /** + * Retrieve configurable product. + * Returns Configurable product that was created by Magento/ConfigurableProduct/_files/product_configurable.php + * fixture + * + * @return ProductInterface + */ + private function getConfigurableProductFromCollection(): ProductInterface + { /** @var Collection $collection */ $collection = Bootstrap::getObjectManager()->get(CollectionFactory::class) ->create(); + /** @var ProductInterface $configurableProduct */ $configurableProduct = $collection ->addIdFilter([1]) ->addMinimalPrice() ->load() ->getFirstItem(); - $this->assertEquals(20, $configurableProduct->getMinimalPrice()); + + return $configurableProduct; } } 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 234f0aae6a3e..676433c0a1e6 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProductsTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProductsTest.php @@ -5,7 +5,19 @@ */ namespace Magento\ConfigurableProduct\Ui\DataProvider\Product\Form\Modifier\Data; -class AssociatedProductsTest extends \PHPUnit\Framework\TestCase +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Ui\Component\Filters\FilterModifier; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\ConfigurableProduct\Ui\DataProvider\Product\Form\Modifier\ConfigurablePanel; +use Magento\Framework\App\RequestInterface; +use PHPUnit\Framework\TestCase; + +/** + * AssociatedProductsTest + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class AssociatedProductsTest extends TestCase { /** * @var \Magento\Framework\ObjectManagerInterface $objectManager @@ -17,7 +29,10 @@ class AssociatedProductsTest extends \PHPUnit\Framework\TestCase */ private $registry; - public function setUp() + /** + * @inheritdoc + */ + public function setUp(): void { $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); $this->registry = $this->objectManager->get(\Magento\Framework\Registry::class); @@ -64,6 +79,53 @@ public function testGetProductMatrix($interfaceLocale) } } + /** + * Tests configurable product won't appear in product listing. + * + * Tests configurable product won't appear in configurable associated product listing if its options attribute + * is not filterable in grid. + * + * @return void + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @magentoAppArea adminhtml + */ + public function testAddManuallyConfigurationsWithNotFilterableInGridAttribute(): void + { + /** @var RequestInterface $request */ + $request = $this->objectManager->get(RequestInterface::class); + $request->setParams([ + FilterModifier::FILTER_MODIFIER => [ + 'test_configurable' => [ + 'condition_type' => 'notnull', + ], + ], + 'attributes_codes' => [ + 'test_configurable' + ], + ]); + $context = $this->objectManager->create(ContextInterface::class, ['request' => $request]); + /** @var UiComponentFactory $uiComponentFactory */ + $uiComponentFactory = $this->objectManager->get(UiComponentFactory::class); + $uiComponent = $uiComponentFactory->create( + ConfigurablePanel::ASSOCIATED_PRODUCT_LISTING, + null, + ['context' => $context] + ); + + foreach ($uiComponent->getChildComponents() as $childUiComponent) { + $childUiComponent->prepare(); + } + + $dataSource = $uiComponent->getDataSourceData(); + $skus = array_column($dataSource['data']['items'], 'sku'); + + $this->assertNotContains( + 'configurable', + $skus, + 'Only products with specified attribute should be in product list' + ); + } + /** * @return array */ diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_with_source_model.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_with_source_model.php new file mode 100644 index 000000000000..1f30d5385083 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_with_source_model.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +$eavSetup = $objectManager->create(\Magento\Eav\Setup\EavSetup::class); +$eavSetup->addAttribute( + \Magento\Catalog\Model\Product::ENTITY, + 'test_configurable_with_sm', + [ + 'group' => 'General', + 'type' => 'varchar', + 'backend' => '', + 'frontend' => '', + 'label' => 'Test configurable with source model', + 'input' => 'select', + 'source' => \Magento\Catalog\Model\Category\Attribute\Source\Mode::class, + 'global' => \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_GLOBAL, + 'visible' => true, + 'required' => false, + 'user_defined' => true, + 'default' => '', + 'searchable' => false, + 'filterable' => false, + 'comparable' => false, + 'visible_on_front' => false, + 'used_in_product_listing' => true, + 'unique' => false, + 'apply_to' => '' + ] +); + +$eavConfig = $objectManager->get(\Magento\Eav\Model\Config::class); +$eavConfig->clear(); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_with_source_model_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_with_source_model_rollback.php new file mode 100644 index 000000000000..8b6861516a86 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_with_source_model_rollback.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +$registry = $objectManager->get(\Magento\Framework\Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$eavConfig = $objectManager->get(\Magento\Eav\Model\Config::class); +$attribute = $eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, 'test_configurable_with_sm'); +if ($attribute instanceof \Magento\Eav\Model\Entity\Attribute\AbstractAttribute && $attribute->getId()) { + $attribute->delete(); +} +$eavConfig->clear(); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_custom.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_custom.php new file mode 100644 index 000000000000..d0bdf8f2e0bd --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_custom.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +require __DIR__ . '/configurable_attribute_with_source_model.php'; + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +$installer = $objectManager->create(\Magento\Catalog\Setup\CategorySetup::class); + +$eavConfig = $objectManager->get(\Magento\Eav\Model\Config::class); +$attribute = $eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, 'test_configurable_with_sm'); +$options = $attribute->getOptions(); + +$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); +$attributeValues = []; +$associatedProductIds = []; +$attributeSetId = $installer->getAttributeSetId(\Magento\Catalog\Model\Product::ENTITY, 'Default'); + +foreach ($options as $key => $option) { + $productId = $key + 10; + + $product = $objectManager->create(\Magento\Catalog\Model\Product::class); + $product->setTypeId(Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setId($productId) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Option ' . $option->getLabel()) + ->setSku('simple_' . $productId) + ->setPrice($productId) + ->setTestConfigurableWithSm($option->getValue()) + ->setVisibility(Magento\Catalog\Model\Product\Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); + $product = $productRepository->save($product); + + $stockItem = $objectManager->create(\Magento\CatalogInventory\Model\Stock\Item::class); + $stockItem->load($productId, 'product_id'); + if (!$stockItem->getProductId()) { + $stockItem->setProductId($productId); + } + $stockItem->setUseConfigManageStock(1); + $stockItem->setQty(1000); + $stockItem->setIsQtyDecimal(0); + $stockItem->setIsInStock(1); + $stockItem->save(); + + $attributeValues[] = [ + 'label' => 'test', + 'attribute_id' => $attribute->getId(), + 'value_index' => $option->getValue(), + ]; + $associatedProductIds[] = $product->getId(); +} + +$optionsFactory = $objectManager->create(\Magento\ConfigurableProduct\Helper\Product\Options\Factory::class); +$configurableAttributesData = [ + [ + 'attribute_id' => $attribute->getId(), + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getStoreLabel(), + 'position' => '0', + 'values' => $attributeValues, + ], +]; +$configurableOptions = $optionsFactory->create($configurableAttributesData); + +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$extensionConfigurableAttributes = $product->getExtensionAttributes(); +$extensionConfigurableAttributes->setConfigurableProductOptions($configurableOptions); +$extensionConfigurableAttributes->setConfigurableProductLinks($associatedProductIds); +$product->setExtensionAttributes($extensionConfigurableAttributes); + +$product->setTypeId(\Magento\ConfigurableProduct\Model\Product\Type\Configurable::TYPE_CODE) + ->setId(1) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Product') + ->setSku('configurable') + ->setVisibility(Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'is_in_stock' => 1]); +$productRepository->save($product); + +$categoryLinkManagement = $objectManager->create(\Magento\Catalog\Api\CategoryLinkManagementInterface::class); +$categoryLinkManagement->assignProductToCategories( + $product->getSku(), + [2] +); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_custom_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_custom_rollback.php new file mode 100644 index 000000000000..eb3db259d4cb --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_custom_rollback.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var \Magento\Framework\Registry $registry */ +$registry = $objectManager->get(\Magento\Framework\Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$productRepository = $objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); +foreach (['simple_10', 'simple_11', 'simple_12', 'configurable'] as $sku) { + try { + $product = $productRepository->get($sku, true); + + $stockStatus = $objectManager->create(\Magento\CatalogInventory\Model\Stock\Status::class); + $stockStatus->load($product->getEntityId(), 'product_id'); + $stockStatus->delete(); + + if ($product->getId()) { + $productRepository->delete($product); + } + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + //Product already removed + } +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +require __DIR__ . '/configurable_attribute_with_source_model_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_frontend_label_attribute.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_frontend_label_attribute.php new file mode 100644 index 000000000000..69607ffb445b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_frontend_label_attribute.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Eav\Model\Entity\Attribute\FrontendLabel; +use Magento\TestFramework\Helper\Bootstrap; + +require __DIR__ . '/configurable_products.php'; + +// Add frontend label to created attribute: +$frontendLabelAttribute = Bootstrap::getObjectManager()->get(FrontendLabel::class); +$frontendLabelAttribute->setStoreId(1); +$frontendLabelAttribute->setLabel('Default Store View label'); + +$frontendLabels = $attribute->getFrontendLabels(); +$frontendLabels[] = $frontendLabelAttribute; + +$attribute->setFrontendLabels($frontendLabels); +$attributeRepository->save($attribute); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_frontend_label_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_frontend_label_attribute_rollback.php new file mode 100644 index 000000000000..616bd44666ef --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_frontend_label_attribute_rollback.php @@ -0,0 +1,8 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +require __DIR__ . '/configurable_products_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Contact/Controller/IndexTest.php b/dev/tests/integration/testsuite/Magento/Contact/Controller/IndexTest.php index 2f89cb72b0cd..c1255ae9b1aa 100644 --- a/dev/tests/integration/testsuite/Magento/Contact/Controller/IndexTest.php +++ b/dev/tests/integration/testsuite/Magento/Contact/Controller/IndexTest.php @@ -6,7 +6,7 @@ namespace Magento\Contact\Controller; -use Zend\Http\Request; +use Magento\Framework\App\Request\Http as HttpRequest; /** * Contact index controller test @@ -14,7 +14,7 @@ class IndexTest extends \Magento\TestFramework\TestCase\AbstractController { /** - * testPostAction + * Test contacting. */ public function testPostAction() { @@ -24,8 +24,7 @@ public function testPostAction() 'email' => 'user@example.com', 'hideit' => '', ]; - $this->getRequest()->setPostValue($params); - $this->getRequest()->setMethod(Request::METHOD_POST); + $this->getRequest()->setPostValue($params)->setMethod(HttpRequest::METHOD_POST); $this->dispatch('contact/index/post'); $this->assertRedirect($this->stringContains('contact/index')); @@ -38,14 +37,16 @@ public function testPostAction() } /** + * Test validation. + * + * @param array $params For Request. + * @param string $expectedMessage Expected response. + * * @dataProvider dataInvalidPostAction - * @param array $params - * @param string $expectedMessage */ public function testInvalidPostAction($params, $expectedMessage) { - $this->getRequest()->setPostValue($params); - $this->getRequest()->setMethod(Request::METHOD_POST); + $this->getRequest()->setPostValue($params)->setMethod(HttpRequest::METHOD_POST); $this->dispatch('contact/index/post'); $this->assertRedirect($this->stringContains('contact/index')); diff --git a/dev/tests/integration/testsuite/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRatesTest.php b/dev/tests/integration/testsuite/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRatesTest.php index c9f2ffad6764..fefd1a7b250c 100644 --- a/dev/tests/integration/testsuite/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRatesTest.php +++ b/dev/tests/integration/testsuite/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRatesTest.php @@ -5,6 +5,8 @@ */ namespace Magento\CurrencySymbol\Controller\Adminhtml\System\Currency; +use Magento\Framework\App\Request\Http as HttpRequest; + class SaveRatesTest extends \Magento\TestFramework\TestCase\AbstractBackendController { @@ -43,6 +45,7 @@ public function testSaveAction() $rate = 1.0000; $request = $this->getRequest(); + $request->setMethod(HttpRequest::METHOD_POST); $request->setPostValue( 'rate', [ @@ -75,6 +78,7 @@ public function testSaveWithWarningAction() $rate = '0'; $request = $this->getRequest(); + $request->setMethod(HttpRequest::METHOD_POST); $request->setPostValue( 'rate', [ diff --git a/dev/tests/integration/testsuite/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/SaveTest.php b/dev/tests/integration/testsuite/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/SaveTest.php index 5217c3576a51..2929f137be89 100644 --- a/dev/tests/integration/testsuite/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/SaveTest.php +++ b/dev/tests/integration/testsuite/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/SaveTest.php @@ -5,10 +5,16 @@ */ namespace Magento\CurrencySymbol\Controller\Adminhtml\System\Currencysymbol; +use Magento\Framework\App\Request\Http as HttpRequest; + class SaveTest extends \Magento\TestFramework\TestCase\AbstractBackendController { /** - * Test save action + * Test save action. + * + * @param string $currencyCode + * @param string $inputCurrencySymbol + * @param string $outputCurrencySymbol * * @magentoConfigFixture currency/options/allow USD * @magentoDbIsolation enabled @@ -31,6 +37,7 @@ public function testSaveAction($currencyCode, $inputCurrencySymbol, $outputCurre $currencyCode => $inputCurrencySymbol, ] ); + $request->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/admin/system_currencysymbol/save'); $this->assertRedirect(); diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Address/BookTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Address/BookTest.php index 4c31fb740c57..1ef7d54c5aa7 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Block/Address/BookTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Address/BookTest.php @@ -65,6 +65,7 @@ public function testGetAddressEditUrl() * @magentoDataFixture Magento/Customer/_files/customer_two_addresses.php * @magentoDataFixture Magento/Customer/_files/customer_no_address.php * @dataProvider hasPrimaryAddressDataProvider + * @magentoAppIsolation enabled */ public function testHasPrimaryAddress($customerId, $expected) { @@ -82,6 +83,7 @@ public function hasPrimaryAddressDataProvider() /** * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Customer/_files/customer_two_addresses.php + * @magentoAppIsolation enabled */ public function testGetAdditionalAddresses() { @@ -98,6 +100,7 @@ public function testGetAdditionalAddresses() /** * @magentoDataFixture Magento/Customer/_files/customer_no_address.php * @dataProvider getAdditionalAddressesDataProvider + * @magentoAppIsolation enabled */ public function testGetAdditionalAddressesNegative($customerId, $expected) { @@ -115,6 +118,7 @@ public function getAdditionalAddressesDataProvider() /** * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Customer/_files/customer_address.php + * @magentoAppIsolation enabled */ public function testGetAddressHtml() { @@ -134,6 +138,7 @@ public function testGetAddressHtmlWithoutAddress() /** * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoAppIsolation enabled */ public function testGetCustomer() { @@ -158,6 +163,7 @@ public function testGetCustomerMissingCustomer() * @magentoDataFixture Magento/Customer/_files/customer_two_addresses.php * @magentoDataFixture Magento/Customer/_files/customer_no_address.php * @dataProvider getDefaultBillingDataProvider + * @magentoAppIsolation enabled */ public function testGetDefaultBilling($customerId, $expected) { @@ -175,6 +181,7 @@ public function getDefaultBillingDataProvider() * @magentoDataFixture Magento/Customer/_files/customer_two_addresses.php * @magentoDataFixture Magento/Customer/_files/customer_no_address.php * @dataProvider getDefaultShippingDataProvider + * @magentoAppIsolation enabled */ public function testGetDefaultShipping($customerId, $expected) { diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Address/GridTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Address/GridTest.php new file mode 100644 index 000000000000..ac11c6c08bd6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Address/GridTest.php @@ -0,0 +1,141 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Customer\Block\Address; + +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Integration tests for the \Magento\Customer\Block\Address\Grid class + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class GridTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\View\LayoutInterface + */ + private $layout; + + /** + * @var \Magento\Customer\Helper\Session\CurrentCustomer + */ + protected $currentCustomer; + + protected function setUp() + { + /** @var \PHPUnit_Framework_MockObject_MockObject $blockMock */ + $blockMock = $this->getMockBuilder( + \Magento\Framework\View\Element\BlockInterface::class + )->disableOriginalConstructor()->setMethods( + ['setTitle', 'toHtml'] + )->getMock(); + $blockMock->expects($this->any())->method('setTitle'); + + $this->currentCustomer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Customer\Helper\Session\CurrentCustomer::class); + $this->layout = Bootstrap::getObjectManager()->get(\Magento\Framework\View\LayoutInterface::class); + $this->layout->setBlock('head', $blockMock); + } + + protected function tearDown() + { + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + /** @var \Magento\Customer\Model\CustomerRegistry $customerRegistry */ + $customerRegistry = $objectManager->get(\Magento\Customer\Model\CustomerRegistry::class); + // Cleanup customer from registry + $customerRegistry->remove(1); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoAppIsolation enabled + */ + public function testGetAddressEditUrl() + { + $gridBlock = $this->createBlockForCustomer(1); + + $this->assertEquals( + 'http://localhost/index.php/customer/address/edit/id/1/', + $gridBlock->getAddressEditUrl(1) + ); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Customer/_files/customer_two_addresses.php + * @magentoAppIsolation enabled + */ + public function testGetAdditionalAddresses() + { + $gridBlock = $this->createBlockForCustomer(1); + $this->assertNotNull($gridBlock->getAdditionalAddresses()); + $this->assertCount(1, $gridBlock->getAdditionalAddresses()); + $this->assertInstanceOf( + \Magento\Customer\Api\Data\AddressInterface::class, + $gridBlock->getAdditionalAddresses()[0] + ); + $this->assertEquals(2, $gridBlock->getAdditionalAddresses()[0]->getId()); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer_no_address.php + * @dataProvider getAdditionalAddressesDataProvider + * @magentoAppIsolation enabled + */ + public function testGetAdditionalAddressesNegative($customerId, $expected) + { + $gridBlock = $this->createBlockForCustomer($customerId); + $this->currentCustomer->setCustomerId($customerId); + $this->assertEquals($expected, $gridBlock->getAdditionalAddresses()); + } + + public function getAdditionalAddressesDataProvider() + { + return ['5' => [5, []]]; + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer_no_address.php + * @magentoAppIsolation enabled + */ + public function testGetAddressHtmlWithoutAddress() + { + $gridBlock = $this->createBlockForCustomer(5); + $this->assertEquals('', $gridBlock->getAddressHtml(null)); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoAppIsolation enabled + */ + public function testGetCustomer() + { + $gridBlock = $this->createBlockForCustomer(1); + /** @var CustomerRepositoryInterface $customerRepository */ + $customerRepository = Bootstrap::getObjectManager()->get( + \Magento\Customer\Api\CustomerRepositoryInterface::class + ); + $customer = $customerRepository->getById(1); + $object = $gridBlock->getCustomer(); + $this->assertEquals($customer, $object); + } + + /** + * Create address book block for customer + * + * @param int $customerId + * @return \Magento\Framework\View\Element\BlockInterface + */ + private function createBlockForCustomer($customerId) + { + $this->currentCustomer->setCustomerId($customerId); + return $this->layout->createBlock( + \Magento\Customer\Block\Address\Grid::class, + '', + ['currentCustomer' => $this->currentCustomer] + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/CartTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/CartTest.php index 3bb10baff657..3ade17d90fe9 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/CartTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/CartTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Customer\Block\Adminhtml\Edit\Tab\View; use Magento\Customer\Controller\RegistryConstants; @@ -100,15 +101,4 @@ public function testToHtmlCartItem() $this->assertContains('$10.00', $html); $this->assertContains('catalog/product/edit/id/1', $html); } - - /** - * Verify that the customer has a single item in his cart. - * - * @magentoDataFixture Magento/Customer/_files/customer.php - * @magentoDataFixture Magento/Customer/_files/quote.php - */ - public function testGetCollection() - { - $this->assertEquals(1, $this->block->getCollection()->getSize()); - } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/PersonalInfoTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/PersonalInfoTest.php index 8d82ad94dfab..161734f47bfd 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/PersonalInfoTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/PersonalInfoTest.php @@ -110,9 +110,9 @@ public function testGetCustomer() \Magento\Customer\Api\Data\CustomerInterface::class ); foreach ($expectedCustomerData as $property => $value) { - $expectedValue = is_numeric($value) ? intval($value) : $value; + $expectedValue = is_numeric($value) ? (int)$value : $value; $actualValue = isset($actualCustomerData[$property]) ? $actualCustomerData[$property] : null; - $actualValue = is_numeric($actualValue) ? intval($actualValue) : $actualValue; + $actualValue = is_numeric($actualValue) ? (int)$actualValue : $actualValue; $this->assertEquals($expectedValue, $actualValue); } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Widget/GenderTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Widget/GenderTest.php index 2cc2c2a426d1..3883f3f88ee5 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Block/Widget/GenderTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Widget/GenderTest.php @@ -15,6 +15,9 @@ class GenderTest extends \PHPUnit\Framework\TestCase /** @var Gender */ protected $_block; + /** @var \Magento\Customer\Model\Attribute */ + private $_model; + /** * Test initialization and set up. Create the Gender block. * @return void @@ -28,6 +31,8 @@ protected function setUp() )->createBlock( \Magento\Customer\Block\Widget\Gender::class ); + $this->_model = $objectManager->create(\Magento\Customer\Model\Attribute::class); + $this->_model->loadByCode('customer', 'gender'); } /** @@ -49,7 +54,8 @@ public function testGetGenderOptions() public function testToHtml() { $html = $this->_block->toHtml(); - $this->assertContains('<span>Gender</span>', $html); + $attributeLabel = $this->_model->getStoreLabel(); + $this->assertContains('<span>' . $attributeLabel . '</span>', $html); $this->assertContains('<option value="1">Male</option>', $html); $this->assertContains('<option value="2">Female</option>', $html); $this->assertContains('<option value="3">Not Specified</option>', $html); diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Widget/TaxvatTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Widget/TaxvatTest.php index 3650a7e95a36..3bc9fea5db38 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Block/Widget/TaxvatTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Widget/TaxvatTest.php @@ -22,7 +22,13 @@ public function testToHtml() \Magento\Customer\Block\Widget\Taxvat::class ); - $this->assertContains('title="Tax/VAT number"', $block->toHtml()); + $model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Customer\Model\Attribute::class + ); + $model->loadByCode('customer', 'taxvat'); + $attributeLabel = $model->getStoreLabel(); + + $this->assertContains('title="' . $block->escapeHtmlAttr($attributeLabel) . '"', $block->toHtml()); $this->assertNotContains('required', $block->toHtml()); } @@ -38,13 +44,14 @@ public function testToHtmlRequired() ); $model->loadByCode('customer', 'taxvat')->setIsRequired(true); $model->save(); + $attributeLabel = $model->getStoreLabel(); /** @var \Magento\Customer\Block\Widget\Taxvat $block */ $block = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( \Magento\Customer\Block\Widget\Taxvat::class ); - $this->assertContains('title="Tax/VAT number"', $block->toHtml()); + $this->assertContains('title="' . $block->escapeHtmlAttr($attributeLabel) . '"', $block->toHtml()); $this->assertContains('required', $block->toHtml()); } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php index 186d9c8f87dc..10b632c00247 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php @@ -17,17 +17,35 @@ use Magento\Framework\App\Http; use Magento\Framework\Data\Form\FormKey; use Magento\Framework\Message\MessageInterface; -use Magento\Store\Model\ScopeInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Request; use Magento\TestFramework\Response; use Zend\Stdlib\Parameters; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\TestFramework\Mail\Template\TransportBuilderMock; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\Stdlib\CookieManagerInterface; +use Magento\Theme\Controller\Result\MessagePlugin; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class AccountTest extends \Magento\TestFramework\TestCase\AbstractController { + /** + * @var TransportBuilderMock + */ + private $transportBuilderMock; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + $this->transportBuilderMock = $this->_objectManager->get(TransportBuilderMock::class); + } + /** * Login the user * @@ -56,7 +74,36 @@ public function testIndexAction() } /** - * @return void + * @magentoDataFixture Magento/Customer/_files/customer_no_password.php + */ + public function testLoginWithIncorrectPassword() + { + $expectedMessage = 'The account sign-in was incorrect or your account is disabled temporarily. ' + . 'Please wait and try again later.'; + $this->getRequest() + ->setMethod('POST') + ->setPostValue( + [ + 'login' => [ + 'username' => 'customer@example.com', + 'password' => '123123q' + ] + ] + ); + + $this->dispatch('customer/account/loginPost'); + $this->assertRedirect($this->stringContains('customer/account/login')); + $this->assertSessionMessages( + $this->equalTo( + [ + $expectedMessage + ] + ) + ); + } + + /** + * Test sign up form displaying. */ public function testCreateAction() { @@ -97,19 +144,13 @@ public function testForgotPasswordEmailMessageWithSpecialCharacters() { $email = 'customer@example.com'; - $this->getRequest() - ->setPostValue([ - 'email' => $email, - ]); + $this->getRequest()->setPostValue(['email' => $email]); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('customer/account/forgotPasswordPost'); $this->assertRedirect($this->stringContains('customer/account/')); - /** @var \Magento\TestFramework\Mail\Template\TransportBuilderMock $transportBuilder */ - $transportBuilder = $this->_objectManager->get( - \Magento\TestFramework\Mail\Template\TransportBuilderMock::class - ); - $subject = $transportBuilder->getSentMessage()->getSubject(); + $subject = $this->transportBuilderMock->getSentMessage()->getSubject(); $this->assertContains( 'Test special\' characters', $subject @@ -131,7 +172,6 @@ public function testCreatepasswordActionWithDirectLink() $customer->save(); $this->getRequest()->setParam('token', $token); - $this->getRequest()->setParam('id', $customer->getId()); $this->dispatch('customer/account/createPassword'); @@ -139,12 +179,13 @@ public function testCreatepasswordActionWithDirectLink() $this->assertEquals(302, $response->getHttpResponseCode()); $text = $response->getBody(); $this->assertFalse((bool)preg_match('/' . $token . '/m', $text)); - $this->assertRedirect($this->stringContains('customer/account/createpassword')); + $this->assertRedirect( + $this->stringContains('customer/account/createpassword') + ); - /** @var \Magento\Customer\Model\Session $customer */ - $session = Bootstrap::getObjectManager()->get(\Magento\Customer\Model\Session::class); + /** @var Session $customer */ + $session = Bootstrap::getObjectManager()->get(Session::class); $this->assertEquals($token, $session->getRpToken()); - $this->assertEquals($customer->getId(), $session->getRpCustomerId()); $this->assertNotContains($token, $response->getHeader('Location')->getFieldValue()); } @@ -232,26 +273,10 @@ public function testNoFormKeyCreatePostAction() /** * @magentoDbIsolation enabled * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Customer/_files/customer_confirmation_config_disable.php */ public function testNoConfirmCreatePostAction() { - /** @var \Magento\Framework\App\Config\MutableScopeConfigInterface $mutableScopeConfig */ - $mutableScopeConfig = Bootstrap::getObjectManager() - ->get(\Magento\Framework\App\Config\MutableScopeConfigInterface::class); - - $scopeValue = $mutableScopeConfig->getValue( - 'customer/create_account/confirm', - ScopeInterface::SCOPE_WEBSITES, - null - ); - - $mutableScopeConfig->setValue( - 'customer/create_account/confirm', - 0, - ScopeInterface::SCOPE_WEBSITES, - null - ); - $this->fillRequestWithAccountDataAndFormKey('test1@email.com'); $this->dispatch('customer/account/createPost'); $this->assertRedirect($this->stringEndsWith('customer/account/')); @@ -259,38 +284,15 @@ public function testNoConfirmCreatePostAction() $this->equalTo(['Thank you for registering with Main Website Store.']), MessageInterface::TYPE_SUCCESS ); - - $mutableScopeConfig->setValue( - 'customer/create_account/confirm', - $scopeValue, - ScopeInterface::SCOPE_WEBSITES, - null - ); } /** * @magentoDbIsolation enabled * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Customer/_files/customer_confirmation_config_enable.php */ public function testWithConfirmCreatePostAction() { - /** @var \Magento\Framework\App\Config\MutableScopeConfigInterface $mutableScopeConfig */ - $mutableScopeConfig = Bootstrap::getObjectManager() - ->get(\Magento\Framework\App\Config\MutableScopeConfigInterface::class); - - $scopeValue = $mutableScopeConfig->getValue( - 'customer/create_account/confirm', - ScopeInterface::SCOPE_WEBSITES, - null - ); - - $mutableScopeConfig->setValue( - 'customer/create_account/confirm', - 1, - ScopeInterface::SCOPE_WEBSITES, - null - ); - $this->fillRequestWithAccountDataAndFormKey('test2@email.com'); $this->dispatch('customer/account/createPost'); $this->assertRedirect($this->stringContains('customer/account/index/')); @@ -302,13 +304,6 @@ public function testWithConfirmCreatePostAction() ]), MessageInterface::TYPE_SUCCESS ); - - $mutableScopeConfig->setValue( - 'customer/create_account/confirm', - $scopeValue, - ScopeInterface::SCOPE_WEBSITES, - null - ); } /** @@ -377,10 +372,8 @@ public function testForgotPasswordPostAction() { $email = 'customer@example.com'; - $this->getRequest() - ->setPostValue([ - 'email' => $email, - ]); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue(['email' => $email]); $this->dispatch('customer/account/forgotPasswordPost'); $this->assertRedirect($this->stringContains('customer/account/')); @@ -400,6 +393,7 @@ public function testForgotPasswordPostAction() */ public function testForgotPasswordPostWithBadEmailAction() { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest() ->setPostValue([ 'email' => 'bad@email', @@ -421,6 +415,7 @@ public function testResetPasswordPostNoTokenAction() $this->getRequest() ->setParam('id', 1) ->setParam('token', '8ed8677e6c79e68b94e61658bd756ea5') + ->setMethod('POST') ->setPostValue([ 'password' => 'new-password', 'password_confirmation' => 'new-password', @@ -443,6 +438,7 @@ public function testResetPasswordPostAction() $this->getRequest() ->setQueryValue('id', 1) ->setQueryValue('token', '8ed8677e6c79e68b94e61658bd756ea5') + ->setMethod('POST') ->setPostValue([ 'password' => 'new-Password1', 'password_confirmation' => 'new-Password1', @@ -608,7 +604,7 @@ public function testMissingDataEditPostAction() $this->assertRedirect($this->stringContains('customer/account/edit/')); $this->assertSessionMessages( - $this->equalTo(['"Email" is not a valid email address.']), + $this->equalTo(['"Email" is not a valid email address.']), MessageInterface::TYPE_ERROR ); } @@ -639,7 +635,7 @@ public function testWrongPasswordEditPostAction() $this->assertRedirect($this->stringContains('customer/account/edit/')); // Not sure if its the most secure message. Not changing the behavior for now in the new AccountManagement APIs. $this->assertSessionMessages( - $this->equalTo(["The password doesn't match this account. Verify the password and try again."]), + $this->equalTo(["The password doesn't match this account. Verify the password and try again."]), MessageInterface::TYPE_ERROR ); } @@ -667,7 +663,7 @@ public function testWrongConfirmationEditPostAction() $this->assertRedirect($this->stringContains('customer/account/edit/')); $this->assertSessionMessages( - $this->equalTo(['Password confirmation doesn\'t match entered password.']), + $this->equalTo(['Password confirmation doesn't match entered password.']), MessageInterface::TYPE_ERROR ); } @@ -701,6 +697,46 @@ public function testLoginPostRedirect($redirectDashboard, string $redirectUrl) $this->assertTrue($this->_objectManager->get(Session::class)->isLoggedIn()); } + /** + * Test that confirmation email address displays special characters correctly. + * + * @magentoDbIsolation enabled + * @magentoDataFixture Magento/Customer/_files/customer_confirmation_email_address_with_special_chars.php + * + * @return void + */ + public function testConfirmationEmailWithSpecialCharacters(): void + { + $email = 'customer+confirmation@example.com'; + $this->dispatch('customer/account/confirmation/email/customer%2Bconfirmation%40email.com'); + $this->getRequest()->setPostValue('email', $email); + $this->dispatch('customer/account/confirmation/email/customer%2Bconfirmation%40email.com'); + + $this->assertRedirect($this->stringContains('customer/account/index')); + $this->assertSessionMessages( + $this->equalTo(['Please check your email for confirmation key.']), + MessageInterface::TYPE_SUCCESS + ); + + /** @var $message \Magento\Framework\Mail\Message */ + $message = $this->transportBuilderMock->getSentMessage(); + $rawMessage = $message->getRawMessage(); + + $this->assertContains('To: ' . $email, $rawMessage); + + $content = $message->getBody()->getPartContent(0); + $confirmationUrl = $this->getConfirmationUrlFromMessageContent($content); + $this->setRequestInfo($confirmationUrl, 'confirm'); + $this->clearCookieMessagesList(); + $this->dispatch($confirmationUrl); + + $this->assertRedirect($this->stringContains('customer/account/index')); + $this->assertSessionMessages( + $this->equalTo(['Thank you for registering with Main Website Store.']), + MessageInterface::TYPE_SUCCESS + ); + } + /** * Data provider for testLoginPostRedirect. * @@ -715,6 +751,21 @@ public function loginPostRedirectDataProvider() ]; } + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Customer/_files/customer_address.php + * @magentoAppArea frontend + */ + public function testCheckVisitorModel() + { + /** @var \Magento\Customer\Model\Visitor $visitor */ + $visitor = $this->_objectManager->get(\Magento\Customer\Model\Visitor::class); + $this->login(1); + $this->assertNull($visitor->getId()); + $this->dispatch('customer/account/index'); + $this->assertNotNull($visitor->getId()); + } + /** * @param string $email * @return void @@ -818,4 +869,53 @@ private function assertResponseRedirect(Response $response, string $redirectUrl) $this->assertTrue($response->isRedirect()); $this->assertSame($redirectUrl, $response->getHeader('Location')->getUri()); } + + /** + * Add new request info (request uri, path info, action name). + * + * @param string $uri + * @param string $actionName + * @return void + */ + private function setRequestInfo(string $uri, string $actionName): void + { + $this->getRequest() + ->setRequestUri($uri) + ->setPathInfo() + ->setActionName($actionName); + } + + /** + * Clear cookie messages list. + * + * @return void + */ + private function clearCookieMessagesList(): void + { + $cookieManager = $this->_objectManager->get(CookieManagerInterface::class); + $jsonSerializer = $this->_objectManager->get(Json::class); + $cookieManager->setPublicCookie( + MessagePlugin::MESSAGES_COOKIES_NAME, + $jsonSerializer->serialize([]) + ); + } + + /** + * Get confirmation URL from message content. + * + * @param string $content + * @return string + */ + private function getConfirmationUrlFromMessageContent(string $content): string + { + $confirmationUrl = ''; + + if (preg_match('<a\s*href="(?<url>.*?)".*>', $content, $matches)) { + $confirmationUrl = $matches['url']; + $confirmationUrl = str_replace('http://localhost/index.php/', '', $confirmationUrl); + $confirmationUrl = html_entity_decode($confirmationUrl); + } + + return $confirmationUrl; + } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/AddressTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/AddressTest.php index 32a622d4aa65..92e104496fed 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/AddressTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/AddressTest.php @@ -9,6 +9,7 @@ use Magento\Customer\Model\CustomerRegistry; use Magento\Framework\Data\Form\FormKey; use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Request\Http as HttpRequest; class AddressTest extends \Magento\TestFramework\TestCase\AbstractController { @@ -19,7 +20,7 @@ class AddressTest extends \Magento\TestFramework\TestCase\AbstractController private $formKey; /** - * {@inheritDoc} + * @inheritDoc */ protected function setUp() { @@ -168,7 +169,7 @@ public function testFailedFormPostAction() public function testDeleteAction() { $this->getRequest()->setParam('id', 1); - $this->getRequest()->setParam('form_key', $this->formKey->getFormKey()); + $this->getRequest()->setParam('form_key', $this->formKey->getFormKey())->setMethod(HttpRequest::METHOD_POST); // we are overwriting the address coming from the fixture $this->dispatch('customer/address/delete'); @@ -186,7 +187,7 @@ public function testDeleteAction() public function testWrongAddressDeleteAction() { $this->getRequest()->setParam('id', 555); - $this->getRequest()->setParam('form_key', $this->formKey->getFormKey()); + $this->getRequest()->setParam('form_key', $this->formKey->getFormKey())->setMethod(HttpRequest::METHOD_POST); // we are overwriting the address coming from the fixture $this->dispatch('customer/address/delete'); diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Address/SaveTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Address/SaveTest.php new file mode 100644 index 000000000000..cbfa794f94dc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Address/SaveTest.php @@ -0,0 +1,195 @@ +<?php +declare(strict_types=1); +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Customer\Controller\Adminhtml\Address; + +use Magento\Customer\Api\AccountManagementInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Request\Http as HttpRequest; + +/** + * @magentoAppArea adminhtml + */ +class SaveTest extends \Magento\TestFramework\TestCase\AbstractBackendController +{ + /** @var CustomerRepositoryInterface */ + private $customerRepository; + + /** @var AccountManagementInterface */ + private $accountManagement; + + /** @var \Magento\TestFramework\ObjectManager */ + private $objectManager; + + /** @var \Magento\Customer\Controller\Adminhtml\Address\Save */ + private $customerAddress; + + /** + * @inheritDoc + */ + protected function setUp() + { + parent::setUp(); + $this->customerRepository = Bootstrap::getObjectManager()->get( + \Magento\Customer\Api\CustomerRepositoryInterface::class + ); + $this->accountManagement = Bootstrap::getObjectManager()->get( + \Magento\Customer\Api\AccountManagementInterface::class + ); + $this->objectManager = Bootstrap::getObjectManager(); + $this->customerAddress = $this->objectManager->get(\Magento\Customer\Controller\Adminhtml\Address\Save::class); + } + + /** + * @inheritDoc + */ + protected function tearDown() + { + /** + * Unset customer data + */ + Bootstrap::getObjectManager()->get(\Magento\Backend\Model\Session::class)->setCustomerData(null); + + /** + * Unset messages + */ + Bootstrap::getObjectManager()->get(\Magento\Backend\Model\Session::class)->getMessages(true); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer_no_address.php + * + * Check that customer id set and addresses saved + */ + public function testSaveActionWithValidAddressData() + { + $customer = $this->customerRepository->get('customer5@example.com'); + $customerId = $customer->getId(); + $post = [ + 'parent_id' => $customerId, + 'firstname' => 'test firstname', + 'lastname' => 'test lastname', + 'street' => ['test street'], + 'city' => 'test city', + 'region_id' => 10, + 'country_id' => 'US', + 'postcode' => '01001', + 'telephone' => '+7000000001', + ]; + $this->getRequest()->setPostValue($post)->setMethod(HttpRequest::METHOD_POST); + + $this->objectManager->get(\Magento\Backend\Model\Session::class)->setCustomerFormData($post); + + $this->customerAddress->execute(); + + $this->assertSessionMessages($this->isEmpty(), \Magento\Framework\Message\MessageInterface::TYPE_ERROR); + + /** Check that customer data were cleaned after it was saved successfully*/ + $this->assertEmpty($this->objectManager->get(\Magento\Backend\Model\Session::class)->getCustomerData()); + + $customer = $this->customerRepository->getById($customerId); + + $this->assertEquals('Firstname', $customer->getFirstname()); + $addresses = $customer->getAddresses(); + $this->assertCount(1, $addresses); + $this->assertNull($this->accountManagement->getDefaultBillingAddress($customerId)); + $this->assertNull($this->accountManagement->getDefaultShippingAddress($customerId)); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer_no_address.php + * + * Check that customer id set and addresses saved + */ + public function testSaveActionWithDefaultShippingAndBilling() + { + $customer = $this->customerRepository->get('customer5@example.com'); + $customerId = $customer->getId(); + $post = [ + 'parent_id' => $customerId, + 'firstname' => 'test firstname', + 'lastname' => 'test lastname', + 'street' => ['test street'], + 'city' => 'test city', + 'region_id' => 10, + 'country_id' => 'US', + 'postcode' => '01001', + 'telephone' => '+7000000001', + 'default_billing' => true, + 'default_shipping' => true + ]; + $this->getRequest()->setPostValue($post)->setMethod(HttpRequest::METHOD_POST); + + $this->objectManager->get(\Magento\Backend\Model\Session::class)->setCustomerFormData($post); + + $this->customerAddress->execute(); + /** + * Check that errors was generated and set to session + */ + $this->assertSessionMessages($this->isEmpty(), \Magento\Framework\Message\MessageInterface::TYPE_ERROR); + + /** + * Check that customer data were cleaned after it was saved successfully + */ + $this->assertEmpty($this->objectManager->get(\Magento\Backend\Model\Session::class)->getCustomerData()); + + /** + * Remove stored customer from registry + */ + $this->objectManager->get(\Magento\Customer\Model\CustomerRegistry::class)->remove($customerId); + $customer = $this->customerRepository->get('customer5@example.com'); + $this->assertEquals('Firstname', $customer->getFirstname()); + $addresses = $customer->getAddresses(); + $this->assertCount(1, $addresses); + + $this->assertNotNull($this->accountManagement->getDefaultBillingAddress($customerId)); + $this->assertNotNull($this->accountManagement->getDefaultShippingAddress($customerId)); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer_sample.php + * + * Check that customer id set and addresses saved + */ + public function testSaveActionWithExistingAdresses() + { + $customer = $this->customerRepository->get('customer@example.com'); + $customerId = $customer->getId(); + $post = [ + 'parent_id' => $customerId, + 'firstname' => 'test firstname', + 'lastname' => 'test lastname', + 'street' => ['test street'], + 'city' => 'test city', + 'region_id' => 10, + 'country_id' => 'US', + 'postcode' => '01001', + 'telephone' => '+7000000001', + ]; + $this->getRequest()->setPostValue($post)->setMethod(HttpRequest::METHOD_POST); + + $this->objectManager->get(\Magento\Backend\Model\Session::class)->setCustomerFormData($post); + + $this->customerAddress->execute(); + /** + * Check that errors was generated and set to session + */ + $this->assertSessionMessages($this->isEmpty(), \Magento\Framework\Message\MessageInterface::TYPE_ERROR); + + /** + * Check that customer data were cleaned after it was saved successfully + */ + $this->assertEmpty($this->objectManager->get(\Magento\Backend\Model\Session::class)->getCustomerData()); + + $customer = $this->customerRepository->getById($customerId); + + $this->assertEquals('test firstname', $customer->getFirstname()); + $addresses = $customer->getAddresses(); + $this->assertCount(4, $addresses); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/GroupTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/GroupTest.php index 80b11a920e3f..db1cc4995e67 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/GroupTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/GroupTest.php @@ -7,6 +7,7 @@ use Magento\Framework\Message\MessageInterface; use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Request\Http as HttpRequest; /** * @magentoAppArea adminhtml @@ -25,6 +26,11 @@ class GroupTest extends \Magento\TestFramework\TestCase\AbstractBackendControlle /** @var \Magento\Customer\Api\GroupRepositoryInterface */ private $groupRepository; + /** + * @inheritDoc + * + * @throws \Magento\Framework\Exception\AuthenticationException + */ public function setUp() { parent::setUp(); @@ -33,12 +39,18 @@ public function setUp() $this->groupRepository = $objectManager->get(\Magento\Customer\Api\GroupRepositoryInterface::class); } + /** + * @inheritDoc + */ public function tearDown() { parent::tearDown(); //$this->session->unsCustomerGroupData(); } + /** + * Test new group form. + */ public function testNewActionNoCustomerGroupDataInSession() { $this->dispatch('backend/customer/group/new'); @@ -49,6 +61,9 @@ public function testNewActionNoCustomerGroupDataInSession() $this->assertContains($expected, $responseBody); } + /** + * Test form filling with data in session. + */ public function testNewActionWithCustomerGroupDataInSession() { /** @var \Magento\Customer\Api\Data\GroupInterfaceFactory $customerGroupFactory */ @@ -76,21 +91,27 @@ public function testNewActionWithCustomerGroupDataInSession() } /** + * Test calling delete without an ID. + * * @magentoDataFixture Magento/Customer/_files/customer_group.php */ public function testDeleteActionNoGroupId() { + $this->getRequest()->setMethod(\Magento\Framework\App\Request\Http::METHOD_POST); $this->dispatch('backend/customer/group/delete'); $this->assertRedirect($this->stringStartsWith(self::BASE_CONTROLLER_URL)); } /** + * Test deleting a group. + * * @magentoDataFixture Magento/Customer/_files/customer_group.php */ public function testDeleteActionExistingGroup() { $groupId = $this->findGroupIdWithCode(self::CUSTOMER_GROUP_CODE); $this->getRequest()->setParam('id', $groupId); + $this->getRequest()->setMethod(\Magento\Framework\App\Request\Http::METHOD_POST); $this->dispatch('backend/customer/group/delete'); /** @@ -104,11 +125,14 @@ public function testDeleteActionExistingGroup() } /** + * Tet deleting with wrong ID. + * * @magentoDataFixture Magento/Customer/_files/customer_group.php */ public function testDeleteActionNonExistingGroupId() { $this->getRequest()->setParam('id', 10000); + $this->getRequest()->setMethod(\Magento\Framework\App\Request\Http::METHOD_POST); $this->dispatch('backend/customer/group/delete'); /** @@ -122,6 +146,8 @@ public function testDeleteActionNonExistingGroupId() } /** + * Test saving a valid group. + * * @magentoDataFixture Magento/Customer/_files/customer_group.php */ public function testSaveActionExistingGroup() @@ -130,6 +156,7 @@ public function testSaveActionExistingGroup() $this->getRequest()->setParam('tax_class', self::TAX_CLASS_ID); $this->getRequest()->setParam('id', $groupId); $this->getRequest()->setParam('code', self::CUSTOMER_GROUP_CODE); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/group/save'); @@ -161,9 +188,13 @@ public function testSaveActionExistingGroup() ); } + /** + * Test saving an invalid group. + */ public function testSaveActionCreateNewGroupWithoutCode() { $this->getRequest()->setParam('tax_class', self::TAX_CLASS_ID); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/group/save'); @@ -173,19 +204,26 @@ public function testSaveActionCreateNewGroupWithoutCode() ); } + /** + * Test saving an empty group. + */ public function testSaveActionForwardNewCreateNewGroup() { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/group/save'); $responseBody = $this->getResponse()->getBody(); $this->assertRegExp('/<h1 class\="page-title">\s*New Customer Group\s*<\/h1>/', $responseBody); } /** + * Test saving an existing group. + * * @magentoDataFixture Magento/Customer/_files/customer_group.php */ public function testSaveActionForwardNewEditExistingGroup() { $groupId = $this->findGroupIdWithCode(self::CUSTOMER_GROUP_CODE); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setParam('id', $groupId); $this->dispatch('backend/customer/group/save'); @@ -193,10 +231,14 @@ public function testSaveActionForwardNewEditExistingGroup() $this->assertRegExp('/<h1 class\="page-title">\s*' . self::CUSTOMER_GROUP_CODE . '\s*<\/h1>/', $responseBody); } + /** + * Test using an invalid ID. + */ public function testSaveActionNonExistingGroupId() { $this->getRequest()->setParam('id', 10000); $this->getRequest()->setParam('tax_class', self::TAX_CLASS_ID); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/group/save'); @@ -211,6 +253,8 @@ public function testSaveActionNonExistingGroupId() } /** + * Test using existing code. + * * @magentoDataFixture Magento/Customer/_files/customer_group.php */ public function testSaveActionNewGroupWithExistingGroupCode() @@ -220,6 +264,7 @@ public function testSaveActionNewGroupWithExistingGroupCode() $this->getRequest()->setParam('tax_class', self::TAX_CLASS_ID); $this->getRequest()->setParam('code', self::CUSTOMER_GROUP_CODE); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/group/save'); @@ -232,6 +277,8 @@ public function testSaveActionNewGroupWithExistingGroupCode() } /** + * Test saving an invalid group. + * * @magentoDataFixture Magento/Customer/_files/customer_group.php */ public function testSaveActionNewGroupWithoutGroupCode() @@ -240,6 +287,7 @@ public function testSaveActionNewGroupWithoutGroupCode() $originalCode = $this->groupRepository->getById($groupId)->getCode(); $this->getRequest()->setParam('tax_class', self::TAX_CLASS_ID); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/group/save'); diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroupTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroupTest.php index 8ee12fa1aa8d..761064ed61fc 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroupTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroupTest.php @@ -9,6 +9,7 @@ use Magento\Backend\Model\Session; use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\App\Request\Http as HttpRequest; use Magento\Customer\Api\Data\CustomerInterface; use Magento\Framework\Message\MessageInterface; use Magento\TestFramework\Helper\Bootstrap; @@ -31,12 +32,20 @@ class MassAssignGroupTest extends AbstractBackendController */ protected $customerRepository; + /** + * @inheritDoc + * + * @throws \Magento\Framework\Exception\AuthenticationException + */ protected function setUp() { parent::setUp(); $this->customerRepository = Bootstrap::getObjectManager()->get(CustomerRepositoryInterface::class); } + /** + * @inheritDoc + */ protected function tearDown() { /** @@ -69,7 +78,8 @@ public function testMassAssignGroupAction() 'selected' => [$customer->getId()] ]; - $this->getRequest()->setParams($params); + $this->getRequest()->setParams($params) + ->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/index/massAssignGroup'); $this->assertSessionMessages( self::equalTo(['A total of 1 record(s) were updated.']), @@ -103,7 +113,8 @@ public function testLargeGroupMassAssignGroupAction() 'selected' => $ids, ]; - $this->getRequest()->setParams($params); + $this->getRequest()->setParams($params) + ->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/index/massAssignGroup'); $this->assertSessionMessages( self::equalTo(['A total of 5 record(s) were updated.']), @@ -124,11 +135,9 @@ public function testLargeGroupMassAssignGroupAction() */ public function testMassAssignGroupActionNoCustomerIds() { - $params = [ - 'group' => 0, - 'namespace' => 'customer_listing', + $params = ['group'=> 0,'namespace'=> 'customer_listing', ]; - $this->getRequest()->setParams($params); + $this->getRequest()->setParams($params)->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/index/massAssignGroup'); $this->assertSessionMessages( $this->equalTo(['An item needs to be selected. Select and try again.']), diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassDeleteTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassDeleteTest.php index 940f7c84325f..eea94bb3f986 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassDeleteTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/MassDeleteTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\Constraint\Constraint; use Magento\Framework\Message\MessageInterface; use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Request\Http as HttpRequest; use Magento\TestFramework\TestCase\AbstractBackendController; /** @@ -32,12 +33,20 @@ class MassDeleteTest extends AbstractBackendController */ private $baseControllerUrl = 'http://localhost/index.php/backend/customer/index/index'; + /** + * @inheritDoc + * + * @throws \Magento\Framework\Exception\AuthenticationException + */ protected function setUp() { parent::setUp(); $this->customerRepository = Bootstrap::getObjectManager()->get(CustomerRepositoryInterface::class); } + /** + * @inheritDoc + */ protected function tearDown() { /** @@ -106,7 +115,7 @@ private function massDeleteAssertions($ids, Constraint $constraint, $messageType 'namespace' => 'customer_listing', ]; - $this->getRequest()->setParams($requestData); + $this->getRequest()->setParams($requestData)->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/index/massDelete'); $this->assertSessionMessages( $constraint, diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/ResetPasswordTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/ResetPasswordTest.php index b5ca783d68cf..789978d8180d 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/ResetPasswordTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/ResetPasswordTest.php @@ -5,6 +5,8 @@ */ namespace Magento\Customer\Controller\Adminhtml\Index; +use Magento\Framework\App\Request\Http as HttpRequest; + /** * ResetPassword controller test. * @@ -32,7 +34,7 @@ public function testResetPasswordSuccess() $this->passwordResetRequestEventCreate( \Magento\Security\Model\PasswordResetRequestEvent::CUSTOMER_PASSWORD_RESET_REQUEST ); - $this->getRequest()->setPostValue(['customer_id' => '1']); + $this->getRequest()->setPostValue(['customer_id' => '1'])->setMethod(HttpRequest::METHOD_GET); $this->dispatch('backend/customer/index/resetPassword'); $this->assertSessionMessages( $this->equalTo(['The customer will receive an email with a link to reset password.']), @@ -41,6 +43,23 @@ public function testResetPasswordSuccess() $this->assertRedirect($this->stringStartsWith($this->baseControllerUrl . 'edit')); } + /** + * Checks reset password functionality cannot be performed with POST request + * + * @magentoConfigFixture current_store customer/password/limit_password_reset_requests_method 0 + * @magentoConfigFixture current_store customer/password/min_time_between_password_reset_requests 0 + * @magentoDataFixture Magento/Customer/_files/customer.php + */ + public function testResetPasswordWithPost() + { + $this->passwordResetRequestEventCreate( + \Magento\Security\Model\PasswordResetRequestEvent::CUSTOMER_PASSWORD_RESET_REQUEST + ); + $this->getRequest()->setPostValue(['customer_id' => '1'])->setMethod(HttpRequest::METHOD_POST); + $this->dispatch('backend/customer/index/resetPassword'); + $this->assertEquals('noroute', $this->getRequest()->getControllerName()); + } + /** * Checks reset password functionality with default restrictive min time between * password reset requests and customer reset request event. @@ -55,7 +74,7 @@ public function testResetPasswordMinTimeError() $this->passwordResetRequestEventCreate( \Magento\Security\Model\PasswordResetRequestEvent::CUSTOMER_PASSWORD_RESET_REQUEST ); - $this->getRequest()->setPostValue(['customer_id' => '1']); + $this->getRequest()->setPostValue(['customer_id' => '1'])->setMethod(HttpRequest::METHOD_GET); $this->dispatch('backend/customer/index/resetPassword'); $this->assertSessionMessages( $this->equalTo(['The customer will receive an email with a link to reset password.']), @@ -78,7 +97,7 @@ public function testResetPasswordLimitError() $this->passwordResetRequestEventCreate( \Magento\Security\Model\PasswordResetRequestEvent::CUSTOMER_PASSWORD_RESET_REQUEST ); - $this->getRequest()->setPostValue(['customer_id' => '1']); + $this->getRequest()->setPostValue(['customer_id' => '1'])->setMethod(HttpRequest::METHOD_GET); $this->dispatch('backend/customer/index/resetPassword'); $this->assertSessionMessages( $this->equalTo(['The customer will receive an email with a link to reset password.']), @@ -103,7 +122,7 @@ public function testResetPasswordWithSecurityViolationException() $this->passwordResetRequestEventCreate( \Magento\Security\Model\PasswordResetRequestEvent::ADMIN_PASSWORD_RESET_REQUEST ); - $this->getRequest()->setPostValue(['customer_id' => '1']); + $this->getRequest()->setPostValue(['customer_id' => '1'])->setMethod(HttpRequest::METHOD_GET); $this->dispatch('backend/customer/index/resetPassword'); $this->assertSessionMessages( $this->equalTo(['The customer will receive an email with a link to reset password.']), 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 6448816c9345..1b7f2c1f7efd 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/IndexTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/IndexTest.php @@ -12,6 +12,7 @@ use Magento\Customer\Controller\RegistryConstants; use Magento\Customer\Model\EmailNotification; use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Request\Http as HttpRequest; /** * @magentoAppArea adminhtml @@ -45,7 +46,7 @@ class IndexTest extends \Magento\TestFramework\TestCase\AbstractBackendControlle protected $objectManager; /** - * {@inheritDoc} + * @inheritDoc */ protected function setUp() { @@ -71,7 +72,7 @@ protected function setUp() } /** - * {@inheritDoc} + * @inheritDoc */ protected function tearDown() { @@ -91,7 +92,7 @@ protected function tearDown() */ public function testSaveActionWithEmptyPostData() { - $this->getRequest()->setPostValue([]); + $this->getRequest()->setPostValue([])->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/index/save'); $this->assertRedirect($this->stringStartsWith($this->_baseControllerUrl)); } @@ -102,7 +103,7 @@ public function testSaveActionWithEmptyPostData() public function testSaveActionWithInvalidFormData() { $post = ['account' => ['middlename' => 'test middlename', 'group_id' => 1]]; - $this->getRequest()->setPostValue($post); + $this->getRequest()->setPostValue($post)->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/index/save'); /** * Check that errors was generated and set to session @@ -111,236 +112,16 @@ public function testSaveActionWithInvalidFormData() $this->logicalNot($this->isEmpty()), \Magento\Framework\Message\MessageInterface::TYPE_ERROR ); + /** @var \Magento\Backend\Model\Session $session */ + $session = $this->objectManager->get(\Magento\Backend\Model\Session::class); /** * Check that customer data were set to session */ - $this->assertEquals( - $post, - $this->objectManager->get(\Magento\Backend\Model\Session::class)->getCustomerFormData() - ); - $this->assertRedirect($this->stringStartsWith($this->_baseControllerUrl . 'new')); - } - - /** - * @magentoDbIsolation enabled - */ - public function testSaveActionWithInvalidCustomerAddressData() - { - $post = [ - 'customer' => [ - 'middlename' => 'test middlename', - 'group_id' => 1, - 'website_id' => 0, - 'firstname' => 'test firstname', - 'lastname' => 'test lastname', - 'email' => 'example@domain.com', - 'default_billing' => '_item1', - ], - 'address' => ['_item1' => []], - ]; - $this->getRequest()->setPostValue($post); - $this->dispatch('backend/customer/index/save'); - /** - * Check that errors was generated and set to session - */ - $this->assertSessionMessages( - $this->logicalNot($this->isEmpty()), - \Magento\Framework\Message\MessageInterface::TYPE_ERROR - ); - /** - * Check that customer data were set to session - */ - $this->assertEquals( - $post, - $this->objectManager->get(\Magento\Backend\Model\Session::class)->getCustomerFormData() - ); + $this->assertNotEmpty($session->getCustomerFormData()); + $this->assertArraySubset($post, $session->getCustomerFormData()); $this->assertRedirect($this->stringStartsWith($this->_baseControllerUrl . 'new')); } - /** - * @magentoDbIsolation enabled - */ - public function testSaveActionWithValidCustomerDataAndValidAddressData() - { - $post = [ - 'customer' => [ - 'middlename' => 'test middlename', - 'group_id' => 1, - 'website_id' => 0, - 'firstname' => 'test firstname', - 'lastname' => 'test lastname', - 'email' => 'example@domain.com', - 'default_billing' => '_item1', - 'password' => 'password', - ], - 'address' => [ - '_item1' => [ - 'firstname' => 'test firstname', - 'lastname' => 'test lastname', - 'street' => ['test street'], - 'city' => 'test city', - 'region_id' => 10, - 'country_id' => 'US', - 'postcode' => '01001', - 'telephone' => '+7000000001', - 'default_billing' => 'true', - ], - ], - ]; - $this->getRequest()->setPostValue($post); - $this->getRequest()->setParam('back', '1'); - - // Emulate setting customer data to session in editAction - $this->objectManager->get(\Magento\Backend\Model\Session::class)->setCustomerFormData($post); - - $this->dispatch('backend/customer/index/save'); - /** - * Check that errors was generated and set to session - */ - $this->assertSessionMessages($this->isEmpty(), \Magento\Framework\Message\MessageInterface::TYPE_ERROR); - - /** - * Check that customer data were cleaned after it was saved successfully - */ - $this->assertEmpty($this->objectManager->get(\Magento\Backend\Model\Session::class)->getCustomerData()); - - /** - * Check that success message is set - */ - $this->assertSessionMessages( - $this->logicalNot($this->isEmpty()), - \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS - ); - - /** - * Check that customer id set and addresses saved - */ - $registry = $this->objectManager->get(\Magento\Framework\Registry::class); - $customerId = $registry->registry(RegistryConstants::CURRENT_CUSTOMER_ID); - $customer = $this->customerRepository->getById($customerId); - $this->assertEquals('test firstname', $customer->getFirstname()); - $addresses = $customer->getAddresses(); - $this->assertEquals(1, count($addresses)); - $this->assertNotEquals(0, $this->accountManagement->getDefaultBillingAddress($customerId)); - $this->assertNull($this->accountManagement->getDefaultShippingAddress($customerId)); - - $urlPatternParts = [ - $this->_baseControllerUrl . 'edit', - 'id/' . $customerId, - 'back/1', - ]; - $urlPattern = '/^' . str_replace('/', '\/', implode('(/.*/)|/', $urlPatternParts)) . '/'; - - $this->assertRedirect( - $this->matchesRegularExpression($urlPattern) - ); - - /** @var \Magento\Newsletter\Model\Subscriber $subscriber */ - $subscriber = $this->objectManager->get(\Magento\Newsletter\Model\SubscriberFactory::class)->create(); - $this->assertEmpty($subscriber->getId()); - $subscriber->loadByCustomerId($customerId); - $this->assertEmpty($subscriber->getId()); - } - - /** - * @magentoDataFixture Magento/Customer/_files/customer_sample.php - */ - public function testSaveActionExistingCustomerAndExistingAddressData() - { - $post = [ - 'customer' => [ - 'entity_id' => '1', - 'middlename' => 'test middlename', - 'group_id' => 1, - 'website_id' => 1, - 'firstname' => 'test firstname', - 'lastname' => 'test lastname', - 'email' => 'customer@example.com', - 'new_password' => 'auto', - 'sendemail_store_id' => '1', - 'sendemail' => '1', - 'created_at' => '2000-01-01 00:00:00', - 'default_shipping' => '_item1', - 'default_billing' => 1, - ], - 'address' => [ - '1' => [ - 'firstname' => 'update firstname', - 'lastname' => 'update lastname', - 'street' => ['update street'], - 'city' => 'update city', - 'region_id' => 10, - 'country_id' => 'US', - 'postcode' => '01001', - 'telephone' => '+7000000001', - 'default_billing' => 'true', - ], - '_item1' => [ - 'firstname' => 'new firstname', - 'lastname' => 'new lastname', - 'street' => ['new street'], - 'city' => 'new city', - 'region_id' => 10, - 'country_id' => 'US', - 'postcode' => '01001', - 'telephone' => '+7000000001', - 'default_shipping' => 'true', - ], - '_template_' => [ - 'firstname' => '', - 'lastname' => '', - 'street' => [], - 'city' => '', - 'region_id' => 10, - 'country_id' => 'US', - 'postcode' => '', - 'telephone' => '', - ], - ], - 'subscription' => '', - ]; - $this->getRequest()->setPostValue($post); - $this->getRequest()->setParam('id', 1); - $this->dispatch('backend/customer/index/save'); - - /** Check that success message is set */ - $this->assertSessionMessages( - $this->equalTo(['You saved the customer.']), - \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS - ); - - /** Check that customer id set and addresses saved */ - $registry = $this->objectManager->get(\Magento\Framework\Registry::class); - $customerId = $registry->registry(RegistryConstants::CURRENT_CUSTOMER_ID); - $customer = $this->customerRepository->getById($customerId); - $this->assertEquals('test firstname', $customer->getFirstname()); - - /** - * Addresses should be removed by - * \Magento\Customer\Model\ResourceModel\Customer::_saveAddresses during _afterSave - * addressOne - updated - * addressTwo - removed - * addressThree - removed - * _item1 - new address - */ - $addresses = $customer->getAddresses(); - $this->assertEquals(2, count($addresses)); - $updatedAddress = $this->addressRepository->getById(1); - $this->assertEquals('update firstname', $updatedAddress->getFirstname()); - $this->assertTrue($updatedAddress->isDefaultBilling()); - $this->assertEquals($updatedAddress->getId(), $customer->getDefaultBilling()); - $newAddress = $this->accountManagement->getDefaultShippingAddress($customerId); - $this->assertEquals('new firstname', $newAddress->getFirstname()); - - /** @var \Magento\Newsletter\Model\Subscriber $subscriber */ - $subscriber = $this->objectManager->get(\Magento\Newsletter\Model\SubscriberFactory::class)->create(); - $this->assertEmpty($subscriber->getId()); - $subscriber->loadByCustomerId($customerId); - $this->assertNotEmpty($subscriber->getId()); - $this->assertEquals(1, $subscriber->getStatus()); - $this->assertRedirect($this->stringStartsWith($this->_baseControllerUrl . 'index/key/')); - } - /** * @magentoDataFixture Magento/Newsletter/_files/subscribers.php */ @@ -365,7 +146,7 @@ public function testSaveActionExistingCustomerUnsubscribeNewsletter() ], 'subscription' => '0' ]; - $this->getRequest()->setPostValue($post); + $this->getRequest()->setPostValue($post)->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setParam('id', 1); $this->dispatch('backend/customer/index/save'); @@ -426,7 +207,7 @@ public function testSaveActionExistingCustomerChangeEmail() 'default_billing' => 1, ] ]; - $this->getRequest()->setPostValue($post); + $this->getRequest()->setPostValue($post)->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setParam('id', 1); $this->dispatch('backend/customer/index/save'); @@ -473,7 +254,7 @@ public function testInlineEditChangeEmail() ] ]; $this->getRequest()->setParam('ajax', true)->setParam('isAjax', true); - $this->getRequest()->setPostValue($post); + $this->getRequest()->setPostValue($post)->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setParam('id', 1); $this->dispatch('backend/customer/index/inlineEdit'); @@ -499,7 +280,7 @@ public function testSaveActionCoreException() 'password' => 'password', ], ]; - $this->getRequest()->setPostValue($post); + $this->getRequest()->setPostValue($post)->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/index/save'); /* * Check that error message is set @@ -508,13 +289,58 @@ public function testSaveActionCoreException() $this->equalTo(['A customer with the same email address already exists in an associated website.']), \Magento\Framework\Message\MessageInterface::TYPE_ERROR ); - $this->assertEquals( + $this->assertArraySubset( $post, Bootstrap::getObjectManager()->get(\Magento\Backend\Model\Session::class)->getCustomerFormData() ); $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 */ @@ -529,9 +355,9 @@ public function testEditAction() } /** - * Tests new action + * Test new customer form page. */ - public function testNewAction(): void + public function testNewAction() { $this->dispatch('backend/customer/index/edit'); $body = $this->getResponse()->getBody(); @@ -543,7 +369,7 @@ public function testNewAction(): void /** * Test the editing of a new customer that has not been saved but the page has been reloaded */ - public function testNewActionWithCustomerData() + public function te1stNewActionWithCustomerData() { $customerData = [ 'customer_id' => 0, @@ -666,7 +492,7 @@ public function testValidateCustomerWithAddressSuccess() /** * set customer data */ - $this->getRequest()->setParams($customerData); + $this->getRequest()->setParams($customerData)->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/customer/index/validate'); $body = $this->getResponse()->getBody(); @@ -678,65 +504,13 @@ public function testValidateCustomerWithAddressSuccess() $this->assertEquals('{"error":0}', $body); } - /** - * @magentoDataFixture Magento/Customer/_files/customer.php - * @magentoDataFixture Magento/Customer/_files/customer_address.php - */ - public function testValidateCustomerWithAddressFailure() - { - $customerData = [ - 'customer' => [ - 'entity_id' => '1', - 'middlename' => 'new middlename', - 'group_id' => 1, - 'website_id' => 1, - 'firstname' => '', - 'lastname' => '', - 'email' => '*', - 'default_shipping' => '_item1', - 'new_password' => 'auto', - 'sendemail_store_id' => '1', - 'sendemail' => '1', - ], - 'address' => [ - '1' => [ - 'firstname' => '', - 'lastname' => '', - 'street' => ['update street'], - 'city' => 'update city', - 'postcode' => '01001', - 'telephone' => '', - ], - '_template_' => [ - 'lastname' => '', - 'street' => [], - 'city' => '', - 'country_id' => 'US', - 'postcode' => '', - 'telephone' => '', - ], - ], - ]; - /** - * set customer data - */ - $this->getRequest()->setPostValue($customerData); - $this->dispatch('backend/customer/index/validate'); - $body = $this->getResponse()->getBody(); - - $this->assertContains('{"error":true,"messages":', $body); - $this->assertContains('\"First Name\" is a required value', $body); - $this->assertContains('\"Last Name\" is a required value.', $body); - $this->assertContains('\"Country\" is a required value.', $body); - $this->assertContains('\"Phone Number\" is a required value.', $body); - } - /** * @magentoDbIsolation enabled */ public function testResetPasswordActionNoCustomerId() { // No customer ID in post, will just get redirected to base + $this->getRequest()->setMethod(HttpRequest::METHOD_GET); $this->dispatch('backend/customer/index/resetPassword'); $this->assertRedirect($this->stringStartsWith($this->_baseControllerUrl)); } @@ -747,6 +521,7 @@ public function testResetPasswordActionNoCustomerId() public function testResetPasswordActionBadCustomerId() { // Bad customer ID in post, will just get redirected to base + $this->getRequest()->setMethod(HttpRequest::METHOD_GET); $this->getRequest()->setPostValue(['customer_id' => '789']); $this->dispatch('backend/customer/index/resetPassword'); $this->assertRedirect($this->stringStartsWith($this->_baseControllerUrl)); @@ -758,6 +533,7 @@ public function testResetPasswordActionBadCustomerId() public function testResetPasswordActionSuccess() { $this->getRequest()->setPostValue(['customer_id' => '1']); + $this->getRequest()->setMethod(HttpRequest::METHOD_GET); $this->dispatch('backend/customer/index/resetPassword'); $this->assertSessionMessages( $this->equalTo(['The customer will receive an email with a link to reset password.']), diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Section/LoadTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Section/LoadTest.php index 3e086a89f814..3db22b837985 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/Section/LoadTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Section/LoadTest.php @@ -13,7 +13,9 @@ public function testLoadInvalidSection() $expected = [ 'message' => 'The "section<invalid" section source isn't supported.', ]; - $this->dispatch('/customer/section/load/?sections=section<invalid&update_section_id=false&_=147066166394'); + $this->dispatch( + '/customer/section/load/?sections=section<invalid&force_new_section_timestamp=false&_=147066166394' + ); self::assertEquals(json_encode($expected), $this->getResponse()->getBody()); } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/SendTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/SendTest.php new file mode 100644 index 000000000000..54dbdf25dd64 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/SendTest.php @@ -0,0 +1,93 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Customer\Controller; + +use Magento\TestFramework\TestCase\AbstractController; +use Magento\Customer\Api\AccountManagementInterface; +use Magento\Framework\Data\Form\FormKey; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Customer\Model\Session; +use Psr\Log\LoggerInterface; + +class SendTest extends AbstractController +{ + /** @var AccountManagementInterface */ + private $accountManagement; + + /** @var FormKey */ + private $formKey; + + /** + * @throws \Magento\Framework\Exception\LocalizedException + */ + protected function setUp() + { + parent::setUp(); + $logger = $this->createMock(LoggerInterface::class); + $session = Bootstrap::getObjectManager()->create( + Session::class, + [$logger] + ); + $this->accountManagement = Bootstrap::getObjectManager()->create(AccountManagementInterface::class); + $this->formKey = Bootstrap::getObjectManager()->create(FormKey::class); + $customer = $this->accountManagement->authenticate('customer@example.com', 'password'); + $session->setCustomerDataAsLoggedIn($customer); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + */ + public function testExecutePost() + { + $this->getRequest() + ->setMethod('POST') + ->setPostValue( + [ + 'form_key' => $this->formKey->getFormKey(), + 'emails' => 'example1@gmail.com, example2@gmail.com, example3@gmail.com' + ] + ); + + $this->dispatch('wishlist/index/send'); + $this->assertRedirect($this->stringContains('wishlist/index/index')); + $this->assertSessionMessages( + $this->equalTo(['Your wish list has been shared.']), + \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS + ); + } + /** + * @magentoAppIsolation enabled + * @magentoConfigFixture default_store customer/captcha/enable 1 + * @magentoConfigFixture default_store customer/captcha/failed_attempts_login 0 + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoConfigFixture default_store customer/captcha/forms user_forgotpassword,user_login,share_wishlist_form + * + */ + public function testCaptchaFailed() + { + $this->getRequest() + ->setMethod('POST') + ->setPostValue( + [ + 'form_key' => $this->formKey->getFormKey(), + 'emails' => 'example1@gmail.com, example2@gmail.com, example3@gmail.com', + 'captcha' => [ + 'share_wishlist_form' => 'wrong_captcha_word' + ] + ] + ); + + $this->dispatch('wishlist/index/send'); + $this->assertRedirect($this->stringContains('wishlist/index/share')); + $this->assertSessionMessages( + $this->equalTo(['Incorrect CAPTCHA']), + \Magento\Framework\Message\MessageInterface::TYPE_ERROR + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Fixtures/address_data.php b/dev/tests/integration/testsuite/Magento/Customer/Fixtures/address_data.php new file mode 100644 index 000000000000..62b7fab906c5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Fixtures/address_data.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +return [ + [ + 'telephone' => 3468676, + 'postcode' => 90230, + 'country_id' => 'US', + 'city' => 'Culver City', + 'street' => 'Green str, 67', + 'lastname' => 'Smith', + 'firstname' => 'John', + 'region_id' => 12, + ], + [ + 'telephone' => 845454465, + 'postcode' => 10178, + 'country_id' => 'DE', + 'city' => 'Berlin', + 'street' => ['Tunnel Alexanderpl'], + 'lastname' => 'Smith', + 'firstname' => 'John', + ] +]; diff --git a/dev/tests/integration/testsuite/Magento/Customer/Fixtures/customer_2_addresses.php b/dev/tests/integration/testsuite/Magento/Customer/Fixtures/customer_2_addresses.php new file mode 100644 index 000000000000..24a99638c64f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Fixtures/customer_2_addresses.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Api\Data\AddressInterface; +use Magento\TestFramework\Helper\Bootstrap; + +require __DIR__ . '/../../Customer/_files/customer.php'; + +$objectManager = Bootstrap::getObjectManager(); + +$addressData = include __DIR__ . '/address_data.php'; + +/** @var AddressRepositoryInterface $repository */ +$repository = $objectManager->get(AddressRepositoryInterface::class); +foreach ($addressData as $data) { + /** @var AddressInterface $address */ + $address = $objectManager->create(AddressInterface::class, ['data' => $data]); + $address->setCustomerId($customer->getId()); + $repository->save($address); +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Fixtures/customer_2_addresses_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/Fixtures/customer_2_addresses_rollback.php new file mode 100644 index 000000000000..9e29bd77e2bc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Fixtures/customer_2_addresses_rollback.php @@ -0,0 +1,8 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +require __DIR__ . '/../../Customer/_files/customer_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Customer/Fixtures/customer_sec_website.php b/dev/tests/integration/testsuite/Magento/Customer/Fixtures/customer_sec_website.php new file mode 100644 index 000000000000..781bcec1670f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Fixtures/customer_sec_website.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +require __DIR__ . '/../../Store/_files/websites_different_countries.php'; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var CustomerInterface $customer */ +$customer = $objectManager->create(CustomerInterface::class); +$customer->setWebsiteId($websiteId) + ->setEmail('customer.web@example.com') + ->setGroupId(1) + ->setStoreId($store->getId()) + ->setFirstname('John') + ->setLastname('Doe') + ->setGender(1); + +/** @var $repository CustomerRepositoryInterface */ +$repository = $objectManager->get(CustomerRepositoryInterface::class); +$customer = $repository->save($customer); diff --git a/dev/tests/integration/testsuite/Magento/Customer/Fixtures/customer_sec_website_2_addresses.php b/dev/tests/integration/testsuite/Magento/Customer/Fixtures/customer_sec_website_2_addresses.php new file mode 100644 index 000000000000..5dafc0dcae3e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Fixtures/customer_sec_website_2_addresses.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Api\Data\AddressInterface; +use Magento\TestFramework\Helper\Bootstrap; + +require __DIR__ . '/customer_sec_website.php'; + +$objectManager = Bootstrap::getObjectManager(); + +$addressData = include __DIR__ . '/address_data.php'; + +/** @var AddressRepositoryInterface $repository */ +$repository = $objectManager->get(AddressRepositoryInterface::class); +foreach ($addressData as $data) { + /** @var AddressInterface $address */ + $address = $objectManager->create(AddressInterface::class, ['data' => $data]); + $address->setCustomerId($customer->getId()); + $repository->save($address); +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Fixtures/customer_sec_website_2_addresses_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/Fixtures/customer_sec_website_2_addresses_rollback.php new file mode 100644 index 000000000000..77f1b5d07366 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Fixtures/customer_sec_website_2_addresses_rollback.php @@ -0,0 +1,8 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +require __DIR__ . '/customer_sec_website_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Customer/Fixtures/customer_sec_website_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/Fixtures/customer_sec_website_rollback.php new file mode 100644 index 000000000000..a5aab4132278 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Fixtures/customer_sec_website_rollback.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Registry; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +$registry = $objectManager->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +$website = $websiteRepository->get('test'); + +/** @var $repository CustomerRepositoryInterface */ +$repository = $objectManager->get(CustomerRepositoryInterface::class); +$customer = $repository->get('customer.web@example.com', $website->getId()); +$repository->delete($customer); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +require __DIR__ . '/../../Store/_files/websites_different_countries_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagementTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagementTest.php index f5b6fdc93d32..4810b6c28cae 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagementTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagementTest.php @@ -357,6 +357,29 @@ public function testValidateResetPasswordLinkTokenNull() } } + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + */ + public function testValidateResetPasswordLinkTokenWithoutId() + { + $token = 'randomStr123'; + $this->setResetPasswordData($token, 'Y-m-d H:i:s'); + $this->assertTrue( + $this->accountManagement->validateResetPasswordLinkToken(null, $token) + ); + } + /** + * @magentoDataFixture Magento/Customer/_files/two_customers.php + * @expectedException \Magento\Framework\Exception\State\ExpiredException + */ + public function testValidateResetPasswordLinkTokenAmbiguous() + { + $token = 'randomStr123'; + $this->setResetPasswordData($token, 'Y-m-d H:i:s', 1); + $this->setResetPasswordData($token, 'Y-m-d H:i:s', 2); + $this->accountManagement->validateResetPasswordLinkToken(null, $token); + } + /** * @magentoAppArea frontend * @magentoDataFixture Magento/Customer/_files/customer.php @@ -512,6 +535,31 @@ public function testResetPasswordTokenInvalidUserEmail() } } + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + */ + public function testResetPasswordWithoutEmail() + { + $resetToken = 'lsdj579slkj5987slkj595lkj'; + $password = 'new_Password123'; + $this->setResetPasswordData($resetToken, 'Y-m-d H:i:s'); + $this->assertTrue( + $this->accountManagement->resetPassword(null, $resetToken, $password) + ); + } + /** + * @magentoDataFixture Magento/Customer/_files/two_customers.php + * @expectedException \Magento\Framework\Exception\State\ExpiredException + */ + public function testResetPasswordAmbiguousToken() + { + $resetToken = 'lsdj579slkj5987slkj595lkj'; + $password = 'new_Password123'; + $this->setResetPasswordData($resetToken, 'Y-m-d H:i:s', 1); + $this->setResetPasswordData($resetToken, 'Y-m-d H:i:s', 2); + $this->accountManagement->resetPassword(null, $resetToken, $password); + } + /** * @magentoAppArea frontend * @magentoAppIsolation enabled @@ -960,10 +1008,14 @@ public function testGetDefaultAddressesForNonExistentAddress() * * @param $resetToken * @param $date + * @param int $customerIdFromFixture Which customer to use. + * @throws \Exception */ - protected function setResetPasswordData($resetToken, $date) - { - $customerIdFromFixture = 1; + protected function setResetPasswordData( + $resetToken, + $date, + int $customerIdFromFixture = 1 + ) { /** @var \Magento\Customer\Model\Customer $customerModel */ $customerModel = $this->objectManager->create(\Magento\Customer\Model\Customer::class); $customerModel->load($customerIdFromFixture); diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/AddressTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/AddressTest.php index 017532fb392b..b6e8cba82ada 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/AddressTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/AddressTest.php @@ -67,4 +67,28 @@ public function testUpdateDataOverrideExistingData() $this->assertEquals('CompanyZ', $updatedAddressData->getCompany()); $this->assertEquals('99999', $updatedAddressData->getPostcode()); } + + /** + * @magentoDataFixture Magento/Customer/_files/customer_sample.php + */ + public function testUpdateDataForExistingCustomer() + { + /** @var \Magento\Customer\Model\CustomerRegistry $customerRegistry */ + $customerRegistry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(CustomerRegistry::class); + /** @var \Magento\Customer\Model\Data\Address $addressData */ + $updatedAddressData = $this->addressFactory->create() + ->setId(1) + ->setCustomerId($customerRegistry->retrieveByEmail('customer@example.com')->getId()) + ->setCity('CityZ') + ->setCompany('CompanyZ') + ->setPostcode('99999'); + $updatedAddressData = $this->addressModel->updateData($updatedAddressData)->getDataModel(); + + $this->assertEquals(1, $updatedAddressData->getId()); + $this->assertEquals('CityZ', $updatedAddressData->getCity()); + $this->assertEquals('CompanyZ', $updatedAddressData->getCompany()); + $this->assertEquals('99999', $updatedAddressData->getPostcode()); + $this->assertEquals(true, $updatedAddressData->isDefaultBilling()); + $this->assertEquals(true, $updatedAddressData->isDefaultShipping()); + } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/CustomerMetadataTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/CustomerMetadataTest.php index 336b43866170..a5c69bcd3239 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/CustomerMetadataTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/CustomerMetadataTest.php @@ -51,16 +51,23 @@ protected function setUp() public function testGetCustomAttributesMetadata() { - $customAttributesMetadata = $this->service->getCustomAttributesMetadata(); - $this->assertCount(0, $customAttributesMetadata, "Invalid number of attributes returned."); + $customAttributesMetadataQty = count($this->service->getCustomAttributesMetadata()) ; // Verify the consistency of getCustomerAttributeMetadata() function from the 2nd call of the same service - $customAttributesMetadata1 = $this->service->getCustomAttributesMetadata(); - $this->assertCount(0, $customAttributesMetadata1, "Invalid number of attributes returned."); + $customAttributesMetadata1Qty = count($this->service->getCustomAttributesMetadata()); + $this->assertEquals( + $customAttributesMetadataQty, + $customAttributesMetadata1Qty, + "Invalid number of attributes returned." + ); // Verify the consistency of getCustomAttributesMetadata() function from the 2nd service - $customAttributesMetadata2 = $this->serviceTwo->getCustomAttributesMetadata(); - $this->assertCount(0, $customAttributesMetadata2, "Invalid number of attributes returned."); + $customAttributesMetadata2Qty = count($this->serviceTwo->getCustomAttributesMetadata()); + $this->assertEquals( + $customAttributesMetadataQty, + $customAttributesMetadata2Qty, + "Invalid number of attributes returned." + ); } public function testGetNestedOptionsCustomerAttributesMetadata() @@ -232,10 +239,10 @@ public function testGetCustomerAttributeMetadata() $this->assertNotEmpty($attributes); // remove odd extension attributes - $allAtrributes = $expectAttrsWithVals; - $allAtrributes['created_at'] = $attributes['created_at']; - $allAtrributes['updated_at'] = $attributes['updated_at']; - $attributes = array_intersect_key($attributes, $allAtrributes); + $allAttributes = $expectAttrsWithVals; + $allAttributes['created_at'] = $attributes['created_at']; + $allAttributes['updated_at'] = $attributes['updated_at']; + $attributes = array_intersect_key($attributes, $allAttributes); foreach ($attributes as $attributeCode => $attributeValue) { $this->assertNotNull($attributeCode); diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/AddressRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/AddressRepositoryTest.php index 4257b5a92850..381c580f55e6 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/AddressRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/AddressRepositoryTest.php @@ -7,12 +7,17 @@ namespace Magento\Customer\Model\ResourceModel; use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Api\Data\RegionInterfaceFactory; use Magento\Framework\Api\SortOrder; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Store\Api\Data\WebsiteInterface; +use Magento\Store\Api\WebsiteRepositoryInterface; /** - * Integration test for \Magento\Customer\Model\ResourceModel\AddressRepository + * Class with integration tests for AddressRepository. * * @SuppressWarnings(PHPMD.TooManyMethods) * @SuppressWarnings(PHPMD.ExcessivePublicCount) @@ -24,37 +29,40 @@ class AddressRepositoryTest extends \PHPUnit\Framework\TestCase private $repository; /** @var \Magento\Framework\ObjectManagerInterface */ - private $_objectManager; + private $objectManager; /** @var \Magento\Customer\Model\Data\Address[] */ - private $_expectedAddresses; + private $expectedAddresses; /** @var \Magento\Customer\Api\Data\AddressInterfaceFactory */ - private $_addressFactory; + private $addressFactory; /** @var \Magento\Framework\Api\DataObjectHelper */ - protected $dataObjectHelper; + private $dataObjectHelper; + /** + * Set up. + */ protected function setUp() { - $this->_objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); /* @var \Magento\Framework\Config\CacheInterface $cache */ - $cache = $this->_objectManager->create(\Magento\Framework\Config\CacheInterface::class); + $cache = $this->objectManager->create(\Magento\Framework\Config\CacheInterface::class); $cache->remove('extension_attributes_config'); - $this->repository = $this->_objectManager->create(\Magento\Customer\Api\AddressRepositoryInterface::class); - $this->_addressFactory = $this->_objectManager->create( + $this->repository = $this->objectManager->create(\Magento\Customer\Api\AddressRepositoryInterface::class); + $this->addressFactory = $this->objectManager->create( \Magento\Customer\Api\Data\AddressInterfaceFactory::class ); - $this->dataObjectHelper = $this->_objectManager->create(\Magento\Framework\Api\DataObjectHelper::class); + $this->dataObjectHelper = $this->objectManager->create(\Magento\Framework\Api\DataObjectHelper::class); - $regionFactory = $this->_objectManager->create(\Magento\Customer\Api\Data\RegionInterfaceFactory::class); + $regionFactory = $this->objectManager->get(RegionInterfaceFactory::class); $region = $regionFactory->create() ->setRegionCode('AL') ->setRegion('Alabama') ->setRegionId(1); - $address = $this->_addressFactory->create() + $address = $this->addressFactory->create() ->setId('1') ->setCountryId('US') ->setCustomerId('1') @@ -67,7 +75,7 @@ protected function setUp() ->setFirstname('John') ->setLastname('Smith') ->setCompany('CompanyName'); - $address2 = $this->_addressFactory->create() + $address2 = $this->addressFactory->create() ->setId('2') ->setCountryId('US') ->setCustomerId('1') @@ -80,9 +88,12 @@ protected function setUp() ->setFirstname('John') ->setLastname('Smith'); - $this->_expectedAddresses = [$address, $address2]; + $this->expectedAddresses = [$address, $address2]; } + /** + * Tear down. + */ protected function tearDown() { $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); @@ -92,6 +103,8 @@ protected function tearDown() } /** + * Test for save address changes. + * * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Customer/_files/customer_address.php * @magentoDataFixture Magento/Customer/_files/customer_two_addresses.php @@ -109,10 +122,12 @@ public function testSaveAddressChanges() $this->assertEquals(2, $proposedAddress->getId()); $savedAddress = $this->repository->getById(2); - $this->assertNotEquals($this->_expectedAddresses[1]->getTelephone(), $savedAddress->getTelephone()); + $this->assertNotEquals($this->expectedAddresses[1]->getTelephone(), $savedAddress->getTelephone()); } /** + * Test for method save address with new id. + * * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Customer/_files/customer_address.php * @magentoDataFixture Magento/Customer/_files/customer_two_addresses.php @@ -127,6 +142,8 @@ public function testSaveAddressesIdSetButNotAlreadyExisting() } /** + * Test for method get address by id. + * * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Customer/_files/customer_address.php * @magentoDataFixture Magento/Customer/_files/customer_two_addresses.php @@ -136,10 +153,12 @@ public function testGetAddressById() { $addressId = 2; $address = $this->repository->getById($addressId); - $this->assertEquals($this->_expectedAddresses[1], $address); + $this->assertEquals($this->expectedAddresses[1], $address); } /** + * Test for method get address by id with incorrect id. + * * @magentoDataFixture Magento/Customer/_files/customer.php * @expectedException \Magento\Framework\Exception\NoSuchEntityException * @expectedExceptionMessage No such entity with addressId = 12345 @@ -150,6 +169,8 @@ public function testGetAddressByIdBadAddressId() } /** + * Test for method save new address. + * * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Customer/_files/customer_address.php * @magentoAppIsolation enabled @@ -163,20 +184,21 @@ public function testSaveNewAddress() $savedAddress = $this->repository->getById($returnedAddress->getId()); - $expectedNewAddress = $this->_expectedAddresses[1]; + $expectedNewAddress = $this->expectedAddresses[1]; $expectedNewAddress->setId($savedAddress->getId()); - $expectedNewAddress->setRegion($this->_expectedAddresses[1]->getRegion()); + $expectedNewAddress->setRegion($this->expectedAddresses[1]->getRegion()); $this->assertEquals($expectedNewAddress->getExtensionAttributes(), $savedAddress->getExtensionAttributes()); $this->assertEquals( $expectedNewAddress->getRegion()->getExtensionAttributes(), $savedAddress->getRegion()->getExtensionAttributes() ); - $this->assertEquals($expectedNewAddress, $savedAddress); } /** + * Test for method saaveNewAddress with new attributes. + * * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Customer/_files/customer_address.php * @magentoAppIsolation enabled @@ -202,6 +224,8 @@ public function testSaveNewAddressWithAttributes() } /** + * Test for saving address with invalid address. + * * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Customer/_files/customer_address.php * @magentoAppIsolation enabled @@ -225,6 +249,11 @@ public function testSaveNewInvalidAddress() } } + /** + * Test for saving address without existing customer. + * + * @return void + */ public function testSaveAddressesCustomerIdNotExist() { $proposedAddress = $this->_createSecondAddress()->setCustomerId(4200); @@ -236,6 +265,11 @@ public function testSaveAddressesCustomerIdNotExist() } } + /** + * Test for saving addresses with invalid customer id. + * + * @return void + */ public function testSaveAddressesCustomerIdInvalid() { $proposedAddress = $this->_createSecondAddress()->setCustomerId('this_is_not_a_valid_id'); @@ -248,6 +282,8 @@ public function testSaveAddressesCustomerIdInvalid() } /** + * Test for delete method. + * * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Customer/_files/customer_address.php */ @@ -271,6 +307,8 @@ public function testDeleteAddress() } /** + * Test for deleteAddressById. + * * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Customer/_files/customer_address.php */ @@ -294,6 +332,8 @@ public function testDeleteAddressById() } /** + * Test delete address from customer with incorrect address id. + * * @magentoDataFixture Magento/Customer/_files/customer.php */ public function testDeleteAddressFromCustomerBadAddressId() @@ -307,10 +347,13 @@ public function testDeleteAddressFromCustomerBadAddressId() } /** + * Test for searching addressed. + * * @param \Magento\Framework\Api\Filter[] $filters * @param \Magento\Framework\Api\Filter[] $filterGroup * @param \Magento\Framework\Api\SortOrder[] $filterOrders * @param array $expectedResult array of expected results indexed by ID + * @param int $currentPage current page for search criteria * * @dataProvider searchAddressDataProvider * @@ -318,10 +361,10 @@ public function testDeleteAddressFromCustomerBadAddressId() * @magentoDataFixture Magento/Customer/_files/customer_two_addresses.php * @magentoAppIsolation enabled */ - public function testSearchAddresses($filters, $filterGroup, $filterOrders, $expectedResult) + public function testSearchAddresses($filters, $filterGroup, $filterOrders, $expectedResult, $currentPage) { /** @var \Magento\Framework\Api\SearchCriteriaBuilder $searchBuilder */ - $searchBuilder = $this->_objectManager->create(\Magento\Framework\Api\SearchCriteriaBuilder::class); + $searchBuilder = $this->objectManager->create(\Magento\Framework\Api\SearchCriteriaBuilder::class); foreach ($filters as $filter) { $searchBuilder->addFilters([$filter]); } @@ -335,7 +378,7 @@ public function testSearchAddresses($filters, $filterGroup, $filterOrders, $expe } $searchBuilder->setPageSize(1); - $searchBuilder->setCurrentPage(2); + $searchBuilder->setCurrentPage($currentPage); $searchCriteria = $searchBuilder->create(); $searchResults = $this->repository->getList($searchCriteria); @@ -353,6 +396,11 @@ public function testSearchAddresses($filters, $filterGroup, $filterOrders, $expe $this->assertEquals($expectedResult[$expectedResultIndex]['firstname'], $items[0]->getFirstname()); } + /** + * Data provider for searchAddresses. + * + * @return array + */ public function searchAddressDataProvider() { /** @@ -373,6 +421,7 @@ public function searchAddressDataProvider() [ ['id' => 1, 'city' => 'CityM', 'postcode' => 75477, 'firstname' => 'John'], ], + 1 ], 'Address with city CityM' => [ [$filterBuilder->setField('city')->setValue('CityM')->create()], @@ -381,6 +430,7 @@ public function searchAddressDataProvider() [ ['id' => 1, 'city' => 'CityM', 'postcode' => 75477, 'firstname' => 'John'], ], + 1 ], 'Addresses with firstname John sorted by firstname desc, city asc' => [ [$filterBuilder->setField('firstname')->setValue('John')->create()], @@ -393,6 +443,7 @@ public function searchAddressDataProvider() ['id' => 1, 'city' => 'CityM', 'postcode' => 75477, 'firstname' => 'John'], ['id' => 2, 'city' => 'CityX', 'postcode' => 47676, 'firstname' => 'John'], ], + 2 ], 'Addresses with postcode of either 75477 or 47676 sorted by city desc' => [ [], @@ -407,6 +458,7 @@ public function searchAddressDataProvider() ['id' => 2, 'city' => 'CityX', 'postcode' => 47676, 'firstname' => 'John'], ['id' => 1, 'city' => 'CityM', 'postcode' => 75477, 'firstname' => 'John'], ], + 2 ], 'Addresses with postcode greater than 0 sorted by firstname asc, postcode desc' => [ [$filterBuilder->setField('postcode')->setValue('0')->setConditionType('gt')->create()], @@ -419,10 +471,41 @@ public function searchAddressDataProvider() ['id' => 2, 'city' => 'CityX', 'postcode' => 47676, 'firstname' => 'John'], ['id' => 1, 'city' => 'CityM', 'postcode' => 75477, 'firstname' => 'John'], ], + 2 ], ]; } + /** + * Test for save addresses with restricted countries. + * + * @magentoDataFixture Magento/Customer/Fixtures/customer_sec_website.php + */ + public function testSaveAddressWithRestrictedCountries() + { + $website = $this->getWebsite('test'); + $customer = $this->getCustomer('customer.web@example.com', (int)$website->getId()); + $regionFactory = $this->objectManager->get(RegionInterfaceFactory::class); + $region = $regionFactory->create() + ->setRegionCode('CA') + ->setRegion('California') + ->setRegionId(12); + $addressData = [ + 'customer_id' => $customer->getId(), + 'firstname' => 'John', + 'lastname' => 'Doe', + 'street' => ['6161 Main Street'], + 'city' => 'Culver City', + 'country_id' => 'US', + 'region' => $region, + 'postcode' => 90230, + 'telephone' => '555655431' + ]; + $address = $this->addressFactory->create(['data' => $addressData]); + $saved = $this->repository->save($address); + self::assertNotEmpty($saved->getId()); + } + /** * Helper function that returns an Address Data Object that matches the data from customer_address fixture * @@ -430,14 +513,14 @@ public function searchAddressDataProvider() */ private function _createFirstAddress() { - $address = $this->_addressFactory->create(); + $address = $this->addressFactory->create(); $this->dataObjectHelper->mergeDataObjects( \Magento\Customer\Api\Data\AddressInterface::class, $address, - $this->_expectedAddresses[0] + $this->expectedAddresses[0] ); $address->setId(null); - $address->setRegion($this->_expectedAddresses[0]->getRegion()); + $address->setRegion($this->expectedAddresses[0]->getRegion()); return $address; } @@ -448,14 +531,44 @@ private function _createFirstAddress() */ private function _createSecondAddress() { - $address = $this->_addressFactory->create(); + $address = $this->addressFactory->create(); $this->dataObjectHelper->mergeDataObjects( \Magento\Customer\Api\Data\AddressInterface::class, $address, - $this->_expectedAddresses[1] + $this->expectedAddresses[1] ); $address->setId(null); - $address->setRegion($this->_expectedAddresses[1]->getRegion()); + $address->setRegion($this->expectedAddresses[1]->getRegion()); return $address; } + + /** + * Gets customer entity. + * + * @param string $email + * @param int $websiteId + * @return CustomerInterface + * @throws NoSuchEntityException + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function getCustomer(string $email, int $websiteId): CustomerInterface + { + /** @var CustomerRepositoryInterface $repository */ + $repository = $this->objectManager->get(CustomerRepositoryInterface::class); + return $repository->get($email, $websiteId); + } + + /** + * Gets website entity. + * + * @param string $code + * @return WebsiteInterface + * @throws NoSuchEntityException + */ + private function getWebsite(string $code): WebsiteInterface + { + /** @var WebsiteRepositoryInterface $repository */ + $repository = $this->objectManager->get(WebsiteRepositoryInterface::class); + return $repository->get($code); + } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/CustomerRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/CustomerRepositoryTest.php index 1af093bea06c..75dfcf7e8f2f 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/CustomerRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/CustomerRepositoryTest.php @@ -8,8 +8,24 @@ use Magento\Customer\Api\AccountManagementInterface; use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterfaceFactory; +use Magento\Customer\Api\Data\AddressInterfaceFactory; +use Magento\Customer\Api\Data\RegionInterfaceFactory; +use Magento\Framework\Api\ExtensibleDataObjectConverter; +use Magento\Framework\Api\DataObjectHelper; +use Magento\Framework\Encryption\EncryptorInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\CustomerRegistry; use Magento\Framework\Api\SortOrder; +use Magento\Framework\Config\CacheInterface; +use Magento\Framework\ObjectManagerInterface; use Magento\TestFramework\Helper\Bootstrap; +use Magento\Customer\Api\Data\AddressInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\SortOrderBuilder; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Customer\Model\Customer; /** * Checks Customer insert, update, search with repository @@ -24,60 +40,65 @@ class CustomerRepositoryTest extends \PHPUnit\Framework\TestCase /** @var CustomerRepositoryInterface */ private $customerRepository; - /** @var \Magento\Framework\ObjectManagerInterface */ + /** @var ObjectManagerInterface */ private $objectManager; - /** @var \Magento\Customer\Api\Data\CustomerInterfaceFactory */ + /** @var CustomerInterfaceFactory */ private $customerFactory; - /** @var \Magento\Customer\Api\Data\AddressInterfaceFactory */ + /** @var AddressInterfaceFactory */ private $addressFactory; - /** @var \Magento\Customer\Api\Data\RegionInterfaceFactory */ + /** @var RegionInterfaceFactory */ private $regionFactory; - /** @var \Magento\Framework\Api\ExtensibleDataObjectConverter */ + /** @var ExtensibleDataObjectConverter */ private $converter; - /** @var \Magento\Framework\Api\DataObjectHelper */ + /** @var DataObjectHelper */ protected $dataObjectHelper; - /** @var \Magento\Framework\Encryption\EncryptorInterface */ + /** @var EncryptorInterface */ protected $encryptor; - /** @var \Magento\Customer\Model\CustomerRegistry */ + /** @var CustomerRegistry */ protected $customerRegistry; + /** + * @inheritdoc + */ protected function setUp() { $this->objectManager = Bootstrap::getObjectManager(); - $this->customerRepository = - $this->objectManager->create(\Magento\Customer\Api\CustomerRepositoryInterface::class); - $this->customerFactory = - $this->objectManager->create(\Magento\Customer\Api\Data\CustomerInterfaceFactory::class); - $this->addressFactory = $this->objectManager->create(\Magento\Customer\Api\Data\AddressInterfaceFactory::class); - $this->regionFactory = $this->objectManager->create(\Magento\Customer\Api\Data\RegionInterfaceFactory::class); - $this->accountManagement = - $this->objectManager->create(\Magento\Customer\Api\AccountManagementInterface::class); - $this->converter = $this->objectManager->create(\Magento\Framework\Api\ExtensibleDataObjectConverter::class); - $this->dataObjectHelper = $this->objectManager->create(\Magento\Framework\Api\DataObjectHelper::class); - $this->encryptor = $this->objectManager->create(\Magento\Framework\Encryption\EncryptorInterface::class); - $this->customerRegistry = $this->objectManager->create(\Magento\Customer\Model\CustomerRegistry::class); - - /** @var \Magento\Framework\Config\CacheInterface $cache */ - $cache = $this->objectManager->create(\Magento\Framework\Config\CacheInterface::class); + $this->customerRepository = $this->objectManager->create(CustomerRepositoryInterface::class); + $this->customerFactory = $this->objectManager->create(CustomerInterfaceFactory::class); + $this->addressFactory = $this->objectManager->create(AddressInterfaceFactory::class); + $this->regionFactory = $this->objectManager->create(RegionInterfaceFactory::class); + $this->accountManagement = $this->objectManager->create(AccountManagementInterface::class); + $this->converter = $this->objectManager->create(ExtensibleDataObjectConverter::class); + $this->dataObjectHelper = $this->objectManager->create(DataObjectHelper::class); + $this->encryptor = $this->objectManager->create(EncryptorInterface::class); + $this->customerRegistry = $this->objectManager->create(CustomerRegistry::class); + + /** @var CacheInterface $cache */ + $cache = $this->objectManager->create(CacheInterface::class); $cache->remove('extension_attributes_config'); } + /** + * @inheritdoc + */ protected function tearDown() { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $objectManager = Bootstrap::getObjectManager(); /** @var \Magento\Customer\Model\CustomerRegistry $customerRegistry */ - $customerRegistry = $objectManager->get(\Magento\Customer\Model\CustomerRegistry::class); + $customerRegistry = $objectManager->get(CustomerRegistry::class); $customerRegistry->remove(1); } /** + * Check if first name update was successful + * * @magentoDbIsolation enabled */ public function testCreateCustomerNewThenUpdateFirstName() @@ -99,7 +120,7 @@ public function testCreateCustomerNewThenUpdateFirstName() $newCustomerFirstname = 'New First Name'; $updatedCustomer = $this->customerFactory->create(); $this->dataObjectHelper->mergeDataObjects( - \Magento\Customer\Api\Data\CustomerInterface::class, + CustomerInterface::class, $updatedCustomer, $customer ); @@ -112,6 +133,8 @@ public function testCreateCustomerNewThenUpdateFirstName() } /** + * Test create new customer + * * @magentoDbIsolation enabled */ public function testCreateNewCustomer() @@ -139,6 +162,8 @@ public function testCreateNewCustomer() } /** + * Test update customer + * * @dataProvider updateCustomerDataProvider * @magentoAppArea frontend * @magentoDataFixture Magento/Customer/_files/customer.php @@ -168,7 +193,7 @@ public function testUpdateCustomer($defaultBilling, $defaultShipping) $this->dataObjectHelper->populateWithArray( $customerDetails, $customerData, - \Magento\Customer\Api\Data\CustomerInterface::class + CustomerInterface::class ); $this->customerRepository->save($customerDetails, $newPasswordHash); $customerAfter = $this->customerRepository->getById($existingCustomerId); @@ -187,12 +212,12 @@ public function testUpdateCustomer($defaultBilling, $defaultShipping) $attributesBefore = $this->converter->toFlatArray( $customerBefore, [], - \Magento\Customer\Api\Data\CustomerInterface::class + CustomerInterface::class ); $attributesAfter = $this->converter->toFlatArray( $customerAfter, [], - \Magento\Customer\Api\Data\CustomerInterface::class + CustomerInterface::class ); // ignore 'updated_at' unset($attributesBefore['updated_at']); @@ -215,6 +240,8 @@ public function testUpdateCustomer($defaultBilling, $defaultShipping) } /** + * Test update customer address + * * @magentoAppArea frontend * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Customer/_files/customer_two_addresses.php @@ -233,14 +260,14 @@ public function testUpdateCustomerAddress() $this->dataObjectHelper->populateWithArray( $newAddressDataObject, $newAddress, - \Magento\Customer\Api\Data\AddressInterface::class + AddressInterface::class ); $newAddressDataObject->setRegion($addresses[0]->getRegion()); $newCustomerEntity = $this->customerFactory->create(); $this->dataObjectHelper->populateWithArray( $newCustomerEntity, $customerDetails, - \Magento\Customer\Api\Data\CustomerInterface::class + CustomerInterface::class ); $newCustomerEntity->setId($customerId) ->setAddresses([$newAddressDataObject, $addresses[1]]); @@ -256,6 +283,8 @@ public function testUpdateCustomerAddress() } /** + * Test preserve all addresses after customer update + * * @magentoAppArea frontend * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Customer/_files/customer_two_addresses.php @@ -269,7 +298,7 @@ public function testUpdateCustomerPreserveAllAddresses() $this->dataObjectHelper->populateWithArray( $newCustomerEntity, $customerDetails, - \Magento\Customer\Api\Data\CustomerInterface::class + CustomerInterface::class ); $newCustomerEntity->setId($customer->getId()) ->setAddresses(null); @@ -281,6 +310,8 @@ public function testUpdateCustomerPreserveAllAddresses() } /** + * Test update delete all addresses with empty arrays + * * @magentoAppArea frontend * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Customer/_files/customer_two_addresses.php @@ -294,7 +325,7 @@ public function testUpdateCustomerDeleteAllAddressesWithEmptyArray() $this->dataObjectHelper->populateWithArray( $newCustomerEntity, $customerDetails, - \Magento\Customer\Api\Data\CustomerInterface::class + CustomerInterface::class ); $newCustomerEntity->setId($customer->getId()) ->setAddresses([]); @@ -306,6 +337,51 @@ public function testUpdateCustomerDeleteAllAddressesWithEmptyArray() } /** + * Test customer update with new address + * + * @magentoAppArea frontend + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Customer/_files/customer_two_addresses.php + */ + public function testUpdateCustomerWithNewAddress() + { + $customerId = 1; + $customer = $this->customerRepository->getById($customerId); + $customerDetails = $customer->__toArray(); + unset($customerDetails['default_billing']); + unset($customerDetails['default_shipping']); + + $beforeSaveCustomer = $this->customerFactory->create(); + $this->dataObjectHelper->populateWithArray( + $beforeSaveCustomer, + $customerDetails, + CustomerInterface::class + ); + + $addresses = $customer->getAddresses(); + $beforeSaveAddress = $addresses[0]->__toArray(); + unset($beforeSaveAddress['id']); + $newAddressDataObject = $this->addressFactory->create(); + $this->dataObjectHelper->populateWithArray( + $newAddressDataObject, + $beforeSaveAddress, + AddressInterface::class + ); + + $beforeSaveCustomer->setAddresses([$newAddressDataObject]); + $this->customerRepository->save($beforeSaveCustomer); + + $newCustomer = $this->customerRepository->getById($customerId); + $newCustomerAddresses = $newCustomer->getAddresses(); + $addressId = $newCustomerAddresses[0]->getId(); + + $this->assertEquals($newCustomer->getDefaultBilling(), $addressId, "Default billing invalid value"); + $this->assertEquals($newCustomer->getDefaultShipping(), $addressId, "Default shipping invalid value"); + } + + /** + * Test search customers + * * @param \Magento\Framework\Api\Filter[] $filters * @param \Magento\Framework\Api\Filter[] $filterGroup * @param array $expectedResult array of expected results indexed by ID @@ -317,9 +393,8 @@ public function testUpdateCustomerDeleteAllAddressesWithEmptyArray() */ public function testSearchCustomers($filters, $filterGroup, $expectedResult) { - /** @var \Magento\Framework\Api\SearchCriteriaBuilder $searchBuilder */ - $searchBuilder = Bootstrap::getObjectManager() - ->create(\Magento\Framework\Api\SearchCriteriaBuilder::class); + /** @var SearchCriteriaBuilder $searchBuilder */ + $searchBuilder = Bootstrap::getObjectManager()->create(SearchCriteriaBuilder::class); foreach ($filters as $filter) { $searchBuilder->addFilters([$filter]); } @@ -346,19 +421,19 @@ public function testSearchCustomers($filters, $filterGroup, $expectedResult) */ public function testSearchCustomersOrder() { - /** @var \Magento\Framework\Api\SearchCriteriaBuilder $searchBuilder */ + /** @var SearchCriteriaBuilder $searchBuilder */ $objectManager = Bootstrap::getObjectManager(); - $searchBuilder = $objectManager->create(\Magento\Framework\Api\SearchCriteriaBuilder::class); + $searchBuilder = $objectManager->create(SearchCriteriaBuilder::class); // Filter for 'firstname' like 'First' - $filterBuilder = $objectManager->create(\Magento\Framework\Api\FilterBuilder::class); + $filterBuilder = $objectManager->create(FilterBuilder::class); $firstnameFilter = $filterBuilder->setField('firstname') ->setConditionType('like') ->setValue('First%') ->create(); $searchBuilder->addFilters([$firstnameFilter]); // Search ascending order - $sortOrderBuilder = $objectManager->create(\Magento\Framework\Api\SortOrderBuilder::class); + $sortOrderBuilder = $objectManager->create(SortOrderBuilder::class); $sortOrder = $sortOrderBuilder ->setField('lastname') ->setDirection(SortOrder::SORT_ASC) @@ -383,6 +458,8 @@ public function testSearchCustomersOrder() } /** + * Test delete + * * @magentoAppArea adminhtml * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoAppIsolation enabled @@ -393,12 +470,14 @@ public function testDelete() $customer = $this->customerRepository->get($fixtureCustomerEmail); $this->customerRepository->delete($customer); /** Ensure that customer was deleted */ - $this->expectException(\Magento\Framework\Exception\NoSuchEntityException::class); + $this->expectException(NoSuchEntityException::class); $this->expectExceptionMessage('No such entity with email = customer@example.com, websiteId = 1'); $this->customerRepository->get($fixtureCustomerEmail); } /** + * Test delete by id + * * @magentoAppArea adminhtml * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoAppIsolation enabled @@ -409,7 +488,7 @@ public function testDeleteById() $fixtureCustomerId = 1; $this->customerRepository->deleteById($fixtureCustomerId); /** Ensure that customer was deleted */ - $this->expectException(\Magento\Framework\Exception\NoSuchEntityException::class); + $this->expectException(NoSuchEntityException::class); $this->expectExceptionMessage('No such entity with email = customer@example.com, websiteId = 1'); $this->customerRepository->get($fixtureCustomerEmail); } @@ -433,9 +512,14 @@ public function updateCustomerDataProvider() ]; } + /** + * Search customer data provider + * + * @return array + */ public function searchCustomersDataProvider() { - $builder = Bootstrap::getObjectManager()->create(\Magento\Framework\Api\FilterBuilder::class); + $builder = Bootstrap::getObjectManager()->create(FilterBuilder::class); return [ 'Customer with specific email' => [ [$builder->setField('email')->setValue('customer@search.example.com')->create()], @@ -485,9 +569,9 @@ protected function expectedDefaultShippingsInCustomerModelAttributes( $defaultShipping ) { /** - * @var \Magento\Customer\Model\Customer $customer + * @var Customer $customer */ - $customer = $this->objectManager->create(\Magento\Customer\Model\Customer::class); + $customer = $this->objectManager->create(Customer::class); /** @var \Magento\Customer\Model\Customer $customer */ $customer->load($customerId); $this->assertEquals( @@ -503,6 +587,8 @@ protected function expectedDefaultShippingsInCustomerModelAttributes( } /** + * Test update default shipping and default billing address + * * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDbIsolation enabled */ @@ -530,13 +616,13 @@ public function testUpdateDefaultShippingAndDefaultBillingTest() $this->assertEquals( $savedCustomer->getDefaultBilling(), $oldDefaultBilling, - 'Default billing shoud not be overridden' + 'Default billing should not be overridden' ); $this->assertEquals( $savedCustomer->getDefaultShipping(), $oldDefaultShipping, - 'Default shipping shoud not be overridden' + 'Default shipping should not be overridden' ); } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_disable.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_disable.php new file mode 100644 index 000000000000..7d4e451db514 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_disable.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\Store\Model\ScopeInterface; +use Magento\Framework\App\Config\MutableScopeConfigInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +$mutableScopeConfig = $objectManager->create(MutableScopeConfigInterface::class); + +$mutableScopeConfig->setValue( + 'customer/create_account/confirm', + 0, + ScopeInterface::SCOPE_WEBSITES, + null +); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_disable_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_disable_rollback.php new file mode 100644 index 000000000000..36743b4a20e9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_disable_rollback.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\Config\Model\ResourceModel\Config; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var Registry $registry */ +$registry = Bootstrap::getObjectManager()->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var Config $config */ +$config = Bootstrap::getObjectManager()->create(Config::class); +$config->deleteConfig('customer/create_account/confirm'); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_enable.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_enable.php new file mode 100644 index 000000000000..c8deb7ec2a53 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_enable.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\Store\Model\ScopeInterface; +use Magento\Framework\App\Config\MutableScopeConfigInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +$mutableScopeConfig = $objectManager->create(MutableScopeConfigInterface::class); + +$mutableScopeConfig->setValue( + 'customer/create_account/confirm', + 1, + ScopeInterface::SCOPE_WEBSITES, + null +); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_enable_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_enable_rollback.php new file mode 100644 index 000000000000..36743b4a20e9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_config_enable_rollback.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\Config\Model\ResourceModel\Config; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var Registry $registry */ +$registry = Bootstrap::getObjectManager()->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var Config $config */ +$config = Bootstrap::getObjectManager()->create(Config::class); +$config->deleteConfig('customer/create_account/confirm'); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_address_with_special_chars.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_address_with_special_chars.php new file mode 100644 index 000000000000..c4f046bac57a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_address_with_special_chars.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\Customer; +use Magento\TestFramework\Helper\Bootstrap; + +include __DIR__ . '/customer_confirmation_config_enable.php'; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var Customer $customer */ +$customer = $objectManager->create(Customer::class); +/** @var CustomerRepositoryInterface $customerRepository */ +$customerRepository = $objectManager->create(CustomerRepositoryInterface::class); +/** @var CustomerInterface $customerInterface */ +$customerInterface = $objectManager->create(CustomerInterface::class); + +$customerInterface->setWebsiteId(1) + ->setEmail('customer+confirmation@example.com') + ->setConfirmation($customer->getRandomConfirmationKey()) + ->setGroupId(1) + ->setStoreId(1) + ->setFirstname('John') + ->setLastname('Smith') + ->setDefaultBilling(1) + ->setDefaultShipping(1) + ->setTaxvat('12') + ->setGender(0); + +$customerRepository->save($customerInterface, 'password'); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_address_with_special_chars_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_address_with_special_chars_rollback.php new file mode 100644 index 000000000000..7a0ebf74ed8a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_address_with_special_chars_rollback.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +include __DIR__ . '/customer_confirmation_config_enable_rollback.php'; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var Registry $registry */ +$registry = Bootstrap::getObjectManager()->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var CustomerRepositoryInterface $customerRepository */ +$customerRepository = Bootstrap::getObjectManager()->create(CustomerRepositoryInterface::class); + +try { + $customer = $customerRepository->get('customer+confirmation@example.com'); + $customerRepository->delete($customer); +} catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + // Customer with the specified email does not exist +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_no_password.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_no_password.php new file mode 100644 index 000000000000..0340f0f841ca --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_no_password.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +use Magento\Customer\Model\CustomerRegistry; + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +/** @var $repository \Magento\Customer\Api\CustomerRepositoryInterface */ +$repository = $objectManager->create(\Magento\Customer\Api\CustomerRepositoryInterface::class); +$customer = $objectManager->create(\Magento\Customer\Model\Customer::class); +/** @var CustomerRegistry $customerRegistry */ +$customerRegistry = $objectManager->get(CustomerRegistry::class); +/** @var Magento\Customer\Model\Customer $customer */ +$customer->setWebsiteId(1) + ->setId(1) + ->setEmail('customer@example.com') + ->setGroupId(1) + ->setStoreId(1) + ->setIsActive(1) + ->setPrefix('Mr.') + ->setFirstname('John') + ->setMiddlename('A') + ->setLastname('Smith') + ->setSuffix('Esq.') + ->setDefaultBilling(1) + ->setDefaultShipping(1) + ->setTaxvat('12') + ->setGender(0); + +$customer->isObjectNew(true); +$customer->save(); +$customerRegistry->remove($customer->getId()); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_sample.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_sample.php index e12eec293f2a..1af648987055 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_sample.php +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_sample.php @@ -19,6 +19,7 @@ 'lastname' => 'test lastname', 'email' => 'customer@example.com', 'default_billing' => 1, + 'default_shipping' => 1, 'password' => '123123q', 'attribute_set_id' => 1, ]; diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_two_addresses_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_two_addresses_rollback.php new file mode 100644 index 000000000000..414d31ac9d0a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_two_addresses_rollback.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var Registry $registry */ +$registry = Bootstrap::getObjectManager()->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var AddressRepositoryInterface $addressRepository */ +$addressRepository = Bootstrap::getObjectManager()->get(AddressRepositoryInterface::class); + +foreach ([1, 2] as $addressId) { + try { + $addressRepository->deleteById($addressId); + } catch (NoSuchEntityException $e) { + /** + * Tests which are wrapped with MySQL transaction clear all data by transaction rollback. + */ + } +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_with_addresses.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_with_addresses.php new file mode 100644 index 000000000000..60b570b9d13d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_with_addresses.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\Customer\Model\CustomerRegistry; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Model\Customer; +use Magento\Customer\Model\Address; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Model\AddressRegistry; + +$objectManager = Bootstrap::getObjectManager(); +//Creating customer +/** @var $repository CustomerRepositoryInterface */ +$repository = $objectManager->create(CustomerRepositoryInterface::class); +/** @var Customer $customer */ +$customer = $objectManager->create(Customer::class); +/** @var CustomerRegistry $customerRegistry */ +$customerRegistry = $objectManager->get(CustomerRegistry::class); +$customer->setWebsiteId(1) + ->setEmail('customer_with_addresses@test.com') + ->setPassword('password') + ->setGroupId(1) + ->setStoreId(1) + ->setIsActive(1) + ->setPrefix('Mr.') + ->setFirstname('John') + ->setMiddlename('A') + ->setLastname('Smith') + ->setSuffix('Esq.') + ->setDefaultBilling(1) + ->setDefaultShipping(1) + ->setTaxvat('12') + ->setGender(0); + +$customer->isObjectNew(true); +$customer->save(); +$customerRegistry->remove($customer->getId()); + +//Creating address +/** @var Address $customerAddress */ +$customerAddress = $objectManager->create(Address::class); +$customerAddress->isObjectNew(true); +$customerAddress->setData( + [ + 'attribute_set_id' => 2, + 'telephone' => 3468676, + 'postcode' => 75477, + 'country_id' => 'US', + 'city' => 'CityM', + 'company' => 'CompanyName', + 'street' => 'CustomerAddress1', + 'lastname' => 'Smith', + 'firstname' => 'John', + 'parent_id' => $customer->getId(), + 'region_id' => 1, + ] +); +$customerAddress->save(); +/** @var AddressRepositoryInterface $addressRepository */ +$addressRepository = $objectManager->get(AddressRepositoryInterface::class); +$customerAddress = $addressRepository->getById($customerAddress->getId()); +$customerAddress->setCustomerId($customer->getId()); +$customerAddress->isDefaultBilling(true); +$customerAddress->setIsDefaultShipping(true); +$customerAddress = $addressRepository->save($customerAddress); +$customerRegistry->remove($customerAddress->getCustomerId()); +/** @var AddressRegistry $addressRegistry */ +$addressRegistry = $objectManager->get(AddressRegistry::class); +$addressRegistry->remove($customerAddress->getId()); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_with_addresses_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_with_addresses_rollback.php new file mode 100644 index 000000000000..e0c62bffc70d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_with_addresses_rollback.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +/** @var CustomerRepositoryInterface $customerRepo */ +$customerRepo = $objectManager->get(CustomerRepositoryInterface::class); +try { + $customer = $customerRepo->get('customer_with_addresses@test.com'); + /** @var AddressRepositoryInterface $addressRepo */ + $addressRepo = $objectManager->get(AddressRepositoryInterface::class); + foreach ($customer->getAddresses() as $address) { + $addressRepo->delete($address); + } + $customerRepo->delete($customer); +} catch (NoSuchEntityException $exception) { + //Already deleted +} +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_without_addresses.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_without_addresses.php new file mode 100644 index 000000000000..0948e4617361 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_without_addresses.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Customer\Model\Customer; +use Magento\Customer\Model\CustomerRegistry; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var CustomerRegistry $customerRegistry */ +$customerRegistry = Bootstrap::getObjectManager()->get(CustomerRegistry::class); +/** @var Customer $customer */ +$customer = Bootstrap::getObjectManager()->get(Customer::class); + +$customer->setWebsiteId(1) + ->setId(1) + ->setEmail('customer@example.com') + ->setPassword('password') + ->setGroupId(1) + ->setStoreId(1) + ->setIsActive(1) + ->setPrefix('Mr.') + ->setFirstname('John') + ->setMiddlename('A') + ->setLastname('Smith') + ->setSuffix('Esq.') + ->setGender(0) + ->save(); +$customerRegistry->remove($customer->getId()); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_without_addresses_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_without_addresses_rollback.php new file mode 100644 index 000000000000..d5f9308ee77b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_without_addresses_rollback.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var CustomerRegistry $customerRegistry */ +$customerRegistry = Bootstrap::getObjectManager()->get(CustomerRegistry::class); +/** @var CustomerRepositoryInterface $customerRepository */ +$customerRepository = Bootstrap::getObjectManager()->get(CustomerRepositoryInterface::class); + +/** @var Registry $registry */ +$registry = Bootstrap::getObjectManager()->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +try { + $customerRepository->deleteById(1); +} catch (NoSuchEntityException $e) { + /** + * Tests which are wrapped with MySQL transaction clear all data by transaction rollback. + */ +} +$customerRegistry->remove(1); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/three_customers_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/three_customers_rollback.php new file mode 100644 index 000000000000..8e13c1c25162 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/three_customers_rollback.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Integration\Model\Oauth\Token\RequestThrottler; + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var \Magento\Framework\Registry $registry */ +$registry = $objectManager->get(\Magento\Framework\Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$customersToRemove = [ + 'customer@search.example.com', + 'customer2@search.example.com', + 'customer3@search.example.com', +]; + +/** + * @var Magento\Customer\Api\CustomerRepositoryInterface + */ +$customerRepository = $objectManager->create(\Magento\Customer\Api\CustomerRepositoryInterface::class); +/** + * @var RequestThrottler $throttler + */ +$throttler = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(RequestThrottler::class); +foreach ($customersToRemove as $customerEmail) { + try { + $customer = $customerRepository->get($customerEmail); + $customerRepository->delete($customer); + } catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { + /** + * Tests which are wrapped with MySQL transaction clear all data by transaction rollback. + */ + continue; + } + + /* Unlock account if it was locked for tokens retrieval */ + $throttler->resetAuthenticationFailuresCount($customerEmail, RequestThrottler::USER_TYPE_CUSTOMER); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/two_customers_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/two_customers_rollback.php new file mode 100644 index 000000000000..5beb9e654de6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/two_customers_rollback.php @@ -0,0 +1,32 @@ +<?php +/** + * Fixture for Customer List method. + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +require 'customer_rollback.php'; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var \Magento\Framework\Registry $registry */ +$registry = Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var CustomerRepositoryInterface $customerRepository */ +$customerRepository = Bootstrap::getObjectManager()->get(CustomerRepositoryInterface::class); +try { + $customerRepository->deleteById(2); +} catch (NoSuchEntityException $e) { + /** + * Tests which are wrapped with MySQL transaction clear all data by transaction rollback. + */ +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Deploy/_files/theme.php b/dev/tests/integration/testsuite/Magento/Deploy/_files/theme.php index ffeca0f16094..bc190a8519ae 100644 --- a/dev/tests/integration/testsuite/Magento/Deploy/_files/theme.php +++ b/dev/tests/integration/testsuite/Magento/Deploy/_files/theme.php @@ -24,7 +24,7 @@ function rcopy($src, $destination) // If source is not a directory stop processing if (!is_dir($src)) { return false; - }; + } // If the destination directory does not exist create it // If the destination directory could not be created stop processing diff --git a/dev/tests/integration/testsuite/Magento/Developer/Model/Logger/Handler/DebugTest.php b/dev/tests/integration/testsuite/Magento/Developer/Model/Logger/Handler/DebugTest.php index 3bef48d8801f..f7a47017f8b1 100644 --- a/dev/tests/integration/testsuite/Magento/Developer/Model/Logger/Handler/DebugTest.php +++ b/dev/tests/integration/testsuite/Magento/Developer/Model/Logger/Handler/DebugTest.php @@ -5,34 +5,20 @@ */ namespace Magento\Developer\Model\Logger\Handler; -use Magento\Config\Console\Command\ConfigSetCommand; -use Magento\Framework\App\Config; -use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Config\Setup\ConfigOptionsList; +use Magento\Framework\App\DeploymentConfig; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\State; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Framework\Logger\Monolog; +use Magento\Framework\Shell; +use Magento\Setup\Mvc\Bootstrap\InitParamListener; use Magento\TestFramework\Helper\Bootstrap; -use Magento\Deploy\Model\Mode; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; +use Magento\TestFramework\ObjectManager; /** - * Preconditions - * - Developer mode enabled - * - Log file isn't exists - * - 'Log to file' setting are enabled - * - * Test steps - * - Enable production mode without compilation - * - Try to log message into log file - * - Assert that log file isn't exists - * - Assert that 'Log to file' setting are disabled - * - * - Enable 'Log to file' setting - * - Try to log message into debug file - * - Assert that log file is exists - * - Assert that log file contain logged message + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class DebugTest extends \PHPUnit\Framework\TestCase { @@ -42,127 +28,212 @@ class DebugTest extends \PHPUnit\Framework\TestCase private $logger; /** - * @var Mode + * @var WriteInterface */ - private $mode; + private $etcDirectory; /** - * @var InputInterface|\PHPUnit_Framework_MockObject_MockObject + * @var ObjectManager */ - private $inputMock; + private $objectManager; /** - * @var OutputInterface|\PHPUnit_Framework_MockObject_MockObject + * @var Shell */ - private $outputMock; + private $shell; /** - * @var ConfigSetCommand + * @var DeploymentConfig */ - private $configSetCommand; + private $deploymentConfig; /** - * @var WriteInterface + * @var string */ - private $etcDirectory; + private $debugLogPath = ''; + + /** + * @var string + */ + private static $backupFile = 'env.base.php'; + + /** + * @var string + */ + private static $configFile = 'env.php'; /** - * @var Config + * @var Debug */ - private $appConfig; + private $debugHandler; + /** + * @inheritdoc + * @throws \Magento\Framework\Exception\FileSystemException + * @throws \Exception + */ public function setUp() { + $this->objectManager = Bootstrap::getObjectManager(); + $this->shell = $this->objectManager->get(Shell::class); + $this->logger = $this->objectManager->get(Monolog::class); + $this->deploymentConfig = $this->objectManager->get(DeploymentConfig::class); + /** @var Filesystem $filesystem */ - $filesystem = Bootstrap::getObjectManager()->create(Filesystem::class); + $filesystem = $this->objectManager->create(Filesystem::class); $this->etcDirectory = $filesystem->getDirectoryWrite(DirectoryList::CONFIG); - $this->etcDirectory->copyFile('env.php', 'env.base.php'); - - $this->inputMock = $this->getMockBuilder(InputInterface::class) - ->getMockForAbstractClass(); - $this->outputMock = $this->getMockBuilder(OutputInterface::class) - ->getMockForAbstractClass(); - $this->logger = Bootstrap::getObjectManager()->get(Monolog::class); - $this->mode = Bootstrap::getObjectManager()->create( - Mode::class, - [ - 'input' => $this->inputMock, - 'output' => $this->outputMock - ] - ); - $this->configSetCommand = Bootstrap::getObjectManager()->create(ConfigSetCommand::class); - $this->appConfig = Bootstrap::getObjectManager()->create(Config::class); - - // Preconditions - $this->mode->enableDeveloperMode(); - $this->enableDebugging(); - if (file_exists($this->getDebuggerLogPath())) { - unlink($this->getDebuggerLogPath()); - } + $this->etcDirectory->copyFile(self::$configFile, self::$backupFile); } + /** + * @inheritdoc + * @throws \Magento\Framework\Exception\FileSystemException + */ public function tearDown() { - $this->etcDirectory->delete('env.php'); - $this->etcDirectory->renameFile('env.base.php', 'env.php'); + $this->reinitDeploymentConfig(); + $this->etcDirectory->delete(self::$backupFile); } - private function enableDebugging() + /** + * @param bool $flag + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function enableDebugging(bool $flag) { - $this->inputMock = $this->getMockBuilder(InputInterface::class) - ->getMockForAbstractClass(); - $this->outputMock = $this->getMockBuilder(OutputInterface::class) - ->getMockForAbstractClass(); - $this->inputMock->expects($this->exactly(4)) - ->method('getOption') - ->withConsecutive( - [ConfigSetCommand::OPTION_LOCK_ENV], - [ConfigSetCommand::OPTION_LOCK_CONFIG], - [ConfigSetCommand::OPTION_SCOPE], - [ConfigSetCommand::OPTION_SCOPE_CODE] - ) - ->willReturnOnConsecutiveCalls( - true, - false, - ScopeConfigInterface::SCOPE_TYPE_DEFAULT, - null - ); - $this->inputMock->expects($this->exactly(2)) - ->method('getArgument') - ->withConsecutive([ConfigSetCommand::ARG_PATH], [ConfigSetCommand::ARG_VALUE]) - ->willReturnOnConsecutiveCalls('dev/debug/debug_logging', 1); - $this->outputMock->expects($this->once()) - ->method('writeln') - ->with('<info>Value was saved in app/etc/env.php and locked.</info>'); - $this->assertFalse((bool)$this->configSetCommand->run($this->inputMock, $this->outputMock)); + $this->shell->execute( + PHP_BINARY . ' -f %s setup:config:set -n --%s=%s --%s=%s', + [ + BP . '/bin/magento', + ConfigOptionsList::INPUT_KEY_DEBUG_LOGGING, + (int)$flag, + InitParamListener::BOOTSTRAP_PARAM, + urldecode(http_build_query(Bootstrap::getInstance()->getAppInitParams())), + ] + ); + $this->deploymentConfig->resetData(); + $this->assertSame((int)$flag, $this->deploymentConfig->get(ConfigOptionsList::CONFIG_PATH_DEBUG_LOGGING)); } + /** + * @throws \Magento\Framework\Exception\LocalizedException + */ public function testDebugInProductionMode() { $message = 'test message'; + $this->reinitDebugHandler(State::MODE_PRODUCTION); - $this->mode->enableProductionModeMinimal(); + $this->removeDebugLog(); $this->logger->debug($message); $this->assertFileNotExists($this->getDebuggerLogPath()); - $this->assertFalse((bool)$this->appConfig->getValue('dev/debug/debug_logging')); + $this->assertNull($this->deploymentConfig->get(ConfigOptionsList::CONFIG_PATH_DEBUG_LOGGING)); - $this->enableDebugging(); - $this->logger->debug($message); + $this->checkCommonFlow($message); + $this->reinitDeploymentConfig(); + } + + /** + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function testDebugInDeveloperMode() + { + $message = 'test message'; + $this->reinitDebugHandler(State::MODE_DEVELOPER); + $this->removeDebugLog(); + $this->logger->debug($message); $this->assertFileExists($this->getDebuggerLogPath()); $this->assertContains($message, file_get_contents($this->getDebuggerLogPath())); + $this->assertNull($this->deploymentConfig->get(ConfigOptionsList::CONFIG_PATH_DEBUG_LOGGING)); + + $this->checkCommonFlow($message); + $this->reinitDeploymentConfig(); } /** - * @return bool|string + * @return string */ private function getDebuggerLogPath() { - foreach ($this->logger->getHandlers() as $handler) { - if ($handler instanceof Debug) { - return $handler->getUrl(); + if (!$this->debugLogPath) { + foreach ($this->logger->getHandlers() as $handler) { + if ($handler instanceof Debug) { + $this->debugLogPath = $handler->getUrl(); + } } } - return false; + + return $this->debugLogPath; + } + + /** + * @throws \Magento\Framework\Exception\FileSystemException + */ + private function reinitDeploymentConfig() + { + $this->etcDirectory->delete(self::$configFile); + $this->etcDirectory->copyFile(self::$backupFile, self::$configFile); + } + + /** + * @param string $instanceMode + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function reinitDebugHandler(string $instanceMode) + { + $this->debugHandler = $this->objectManager->create( + Debug::class, + [ + 'filePath' => Bootstrap::getInstance()->getAppTempDir(), + 'state' => $this->objectManager->create( + State::class, + [ + 'mode' => $instanceMode, + ] + ), + ] + ); + $this->logger->setHandlers( + [ + $this->debugHandler, + ] + ); + } + + /** + * @return void + */ + private function detachLogger() + { + $this->debugHandler->close(); + } + + /** + * @return void + */ + private function removeDebugLog() + { + $this->detachLogger(); + if (file_exists($this->getDebuggerLogPath())) { + unlink($this->getDebuggerLogPath()); + } + } + + /** + * @param string $message + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function checkCommonFlow(string $message) + { + $this->enableDebugging(true); + $this->removeDebugLog(); + $this->logger->debug($message); + $this->assertFileExists($this->getDebuggerLogPath()); + $this->assertContains($message, file_get_contents($this->getDebuggerLogPath())); + + $this->enableDebugging(false); + $this->removeDebugLog(); + $this->logger->debug($message); + $this->assertFileNotExists($this->getDebuggerLogPath()); } } diff --git a/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php b/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php new file mode 100644 index 000000000000..8874d880a4dd --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php @@ -0,0 +1,241 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Dhl\Model; + +use Magento\Framework\HTTP\ZendClient; +use Magento\Framework\HTTP\ZendClientFactory; +use Magento\Framework\Simplexml\Element; +use Magento\Shipping\Model\Tracking\Result\Error; +use Magento\Shipping\Model\Tracking\Result\Status; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + +class CarrierTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Dhl\Model\Carrier + */ + private $dhlCarrier; + + /** + * @var ZendClient|MockObject + */ + private $httpClientMock; + + /** + * @var \Zend_Http_Response|MockObject + */ + private $httpResponseMock; + + protected function setUp() + { + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->dhlCarrier = $objectManager->create( + \Magento\Dhl\Model\Carrier::class, + ['httpClientFactory' => $this->getHttpClientFactory()] + ); + } + + /** + * @magentoConfigFixture default_store carriers/dhl/id CustomerSiteID + * @magentoConfigFixture default_store carriers/dhl/password CustomerPassword + * @param string[] $trackingNumbers + * @param string $responseXml + * @param $expectedTrackingData + * @param string $expectedRequestXml + * @dataProvider getTrackingDataProvider + */ + public function testGetTracking( + $trackingNumbers, + string $responseXml, + $expectedTrackingData, + string $expectedRequestXml = '' + ) { + $this->httpResponseMock->method('getBody') + ->willReturn($responseXml); + $trackingResult = $this->dhlCarrier->getTracking($trackingNumbers); + $this->assertTrackingResult($expectedTrackingData, $trackingResult->getAllTrackings()); + if ($expectedRequestXml !== '') { + $method = new \ReflectionMethod($this->httpClientMock, '_prepareBody'); + $method->setAccessible(true); + $requestXml = $method->invoke($this->httpClientMock); + $this->assertRequest($expectedRequestXml, $requestXml); + } + } + + /** + * Get tracking data provider + * @return array + */ + public function getTrackingDataProvider() : array + { + $expectedMultiAWBRequestXml = file_get_contents(__DIR__ . '/../_files/TrackingRequest_MultipleAWB.xml'); + $multiAWBResponseXml = file_get_contents(__DIR__ . '/../_files/TrackingResponse_MultipleAWB.xml'); + $expectedSingleAWBRequestXml = file_get_contents(__DIR__ . '/../_files/TrackingRequest_SingleAWB.xml'); + $singleAWBResponseXml = file_get_contents(__DIR__ . '/../_files/TrackingResponse_SingleAWB.xml'); + $singleNoDataResponseXml = file_get_contents(__DIR__ . '/../_files/SingleknownTrackResponse-no-data-found.xml'); + $failedResponseXml = file_get_contents(__DIR__ . '/../_files/Track-res-XML-Parse-Err.xml'); + $expectedTrackingDataA = [ + 'carrier' => 'dhl', + 'carrier_title' => 'DHL', + 'tracking' => 4781584780, + 'service' => 'DOCUMENT', + 'progressdetail' => [ + [ + 'activity' => 'SD Shipment information received', + 'deliverydate' => '2017-12-25', + 'deliverytime' => '14:38:00', + 'deliverylocation' => 'BEIJING-CHN [PEK]' + ] + ], + 'weight' => '0.5 K', + ]; + $expectedTrackingDataB = [ + 'carrier' => 'dhl', + 'carrier_title' => 'DHL', + 'tracking' => 4781585060, + 'service' => 'NOT RESTRICTED FOR TRANSPORT,', + 'progressdetail' => [ + [ + 'activity' => 'SD Shipment information received', + 'deliverydate' => '2017-12-24', + 'deliverytime' => '13:35:00', + 'deliverylocation' => 'HONG KONG-HKG [HKG]' + ] + ], + 'weight' => '2.0 K', + ]; + $expectedTrackingDataC = [ + 'carrier' => 'dhl', + 'carrier_title' => 'DHL', + 'tracking' => 5702254250, + 'service' => 'CD', + 'progressdetail' => [ + [ + 'activity' => 'SD Shipment information received', + 'deliverydate' => '2017-12-24', + 'deliverytime' => '04:12:00', + 'deliverylocation' => 'BIRMINGHAM-GBR [BHX]' + ] + ], + 'weight' => '0.12 K', + ]; + $expectedTrackingDataD = [ + 'carrier' => 'dhl', + 'carrier_title' => 'DHL', + 'tracking' => 4781585060, + 'error_message' => __('Unable to retrieve tracking') + ]; + $expectedTrackingDataE = [ + 'carrier' => 'dhl', + 'carrier_title' => 'DHL', + 'tracking' => 111, + 'error_message' => __( + 'Error #%1 : %2', + '111', + ' Error Parsing incoming request XML + Error: The content of element type + "ShipperReference" must match + "(ReferenceID,ReferenceType?)". at line + 16, column 22' + ) + ]; + return [ + 'multi-AWB' => [ + ['4781584780', '4781585060', '5702254250'], + $multiAWBResponseXml, + [$expectedTrackingDataA, $expectedTrackingDataB, $expectedTrackingDataC], + $expectedMultiAWBRequestXml + ], + 'single-AWB' => [ + ['4781585060'], + $singleAWBResponseXml, + [$expectedTrackingDataB], + $expectedSingleAWBRequestXml + ], + 'single-AWB-no-data' => [ + ['4781585061'], + $singleNoDataResponseXml, + [$expectedTrackingDataD] + ], + 'failed-response' => [ + ['4781585060-failed'], + $failedResponseXml, + [$expectedTrackingDataE] + ] + ]; + } + + /** + * Get mocked Http Client Factory + * + * @return MockObject + */ + private function getHttpClientFactory(): MockObject + { + $this->httpResponseMock = $this->getMockBuilder(\Zend_Http_Response::class) + ->disableOriginalConstructor() + ->getMock(); + $this->httpClientMock = $this->getMockBuilder(ZendClient::class) + ->disableOriginalConstructor() + ->setMethods(['request']) + ->getMock(); + $this->httpClientMock->method('request') + ->willReturn($this->httpResponseMock); + /** @var ZendClientFactory|MockObject $httpClientFactoryMock */ + $httpClientFactoryMock = $this->getMockBuilder(ZendClientFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $httpClientFactoryMock->method('create') + ->willReturn($this->httpClientMock); + + return $httpClientFactoryMock; + } + + /** + * Assert request + * + * @param string $expectedRequestXml + * @param string $requestXml + */ + private function assertRequest(string $expectedRequestXml, string $requestXml): void + { + $expectedRequestElement = new Element($expectedRequestXml); + $requestElement = new Element($requestXml); + $requestMessageTime = $requestElement->Request->ServiceHeader->MessageTime->__toString(); + $this->assertEquals( + 1, + preg_match("/\d{4}\-\d{2}\-\d{2}T\d{2}\:\d{2}\:\d{2}\+\d{2}\:\d{2}/", $requestMessageTime) + ); + $expectedRequestElement->Request->ServiceHeader->MessageTime = $requestMessageTime; + $messageReference = $requestElement->Request->ServiceHeader->MessageReference->__toString(); + $this->assertStringStartsWith('MAGE_TRCK_', $messageReference); + $this->assertGreaterThanOrEqual(28, strlen($messageReference)); + $this->assertLessThanOrEqual(32, strlen($messageReference)); + $requestElement->Request->ServiceHeader->MessageReference = 'MAGE_TRCK_28TO32_Char_CHECKED'; + $this->assertXmlStringEqualsXmlString($expectedRequestElement->asXML(), $requestElement->asXML()); + } + + /** + * Assert tracking + * + * @param array|null $expectedTrackingData + * @param Status[]|null $trackingResults + * @return void + */ + private function assertTrackingResult($expectedTrackingData, $trackingResults): void + { + if (null === $expectedTrackingData) { + $this->assertNull($trackingResults); + } else { + $ctr = 0; + foreach ($trackingResults as $trackingResult) { + $this->assertEquals($expectedTrackingData[$ctr++], $trackingResult->getData()); + } + } + } +} diff --git a/dev/tests/integration/testsuite/Magento/Dhl/_files/SingleknownTrackResponse-no-data-found.xml b/dev/tests/integration/testsuite/Magento/Dhl/_files/SingleknownTrackResponse-no-data-found.xml new file mode 100755 index 000000000000..9887cecbd2d4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Dhl/_files/SingleknownTrackResponse-no-data-found.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<req:TrackingResponse xmlns:req="http://www.dhl.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.dhl.com TrackingResponse.xsd"> + <Response> + <ServiceHeader> + <MessageTime>2018-02-27T12:59:34+01:00</MessageTime> + <MessageReference>1234567890123456789012345678</MessageReference> + <SiteID>CustomerSiteID</SiteID> + </ServiceHeader> + </Response> + <AWBInfo> + <AWBNumber>4781585060</AWBNumber> + <Status> + <ActionStatus>No Shipments Found</ActionStatus> + <Condition> + <ConditionCode>209</ConditionCode> + <ConditionData>No Shipments Found for AWBNumber 6017300993</ConditionData> + </Condition> + </Status> + </AWBInfo> + <LanguageCode>String</LanguageCode> +</req:TrackingResponse> +<!-- ServiceInvocationId:20180227125934_5793_74fbd9e1-a8b0-4f6a-a326-26aae979e5f0 --> diff --git a/dev/tests/integration/testsuite/Magento/Dhl/_files/Track-res-XML-Parse-Err.xml b/dev/tests/integration/testsuite/Magento/Dhl/_files/Track-res-XML-Parse-Err.xml new file mode 100755 index 000000000000..c2abd68d3c4a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Dhl/_files/Track-res-XML-Parse-Err.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<req:ShipmentTrackingErrorResponse xmlns:req="http://www.dhl.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.dhl.com track-err-res.xsd"> + <Response> + <ServiceHeader> + <MessageTime>2018-02-27T12:55:05+01:00</MessageTime> + </ServiceHeader> + <Status> + <ActionStatus>Failure</ActionStatus> + <Condition> + <ConditionCode>111</ConditionCode> + <ConditionData> Error Parsing incoming request XML + Error: The content of element type + "ShipperReference" must match + "(ReferenceID,ReferenceType?)". at line + 16, column 22</ConditionData> + </Condition> + </Status> + </Response> +</req:ShipmentTrackingErrorResponse> +<!-- ServiceInvocationId:20180227125505_5793_2008671c-9292-4790-87b6-b02ccdf913db --> diff --git a/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingRequest_MultipleAWB.xml b/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingRequest_MultipleAWB.xml new file mode 100755 index 000000000000..c0a18fcc4e2f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingRequest_MultipleAWB.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<req:KnownTrackingRequest xmlns:req="http://www.dhl.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.dhl.com TrackingRequestKnown-1.0.xsd" schemaVersion="1.0"> + <Request> + <ServiceHeader> + <MessageTime>2002-06-25T11:28:56-08:00</MessageTime> + <MessageReference>MAGE_TRCK_28TO32_Char_CHECKED</MessageReference> + <SiteID>CustomerSiteID</SiteID> + <Password>CustomerPassword</Password> + </ServiceHeader> + </Request> + <LanguageCode>en</LanguageCode> + <AWBNumber>4781584780</AWBNumber> + <AWBNumber>4781585060</AWBNumber> + <AWBNumber>5702254250</AWBNumber> + <LevelOfDetails>ALL_CHECK_POINTS</LevelOfDetails> +</req:KnownTrackingRequest> + + diff --git a/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingRequest_SingleAWB.xml b/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingRequest_SingleAWB.xml new file mode 100755 index 000000000000..dac69a0d68c5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingRequest_SingleAWB.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<req:KnownTrackingRequest xmlns:req="http://www.dhl.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.dhl.com TrackingRequestKnown-1.0.xsd" schemaVersion="1.0"> + <Request> + <ServiceHeader> + <MessageTime>2002-06-25T11:28:56-08:00</MessageTime> + <MessageReference>MAGE_TRCK_28TO32_Char_CHECKED</MessageReference> + <SiteID>CustomerSiteID</SiteID> + <Password>CustomerPassword</Password> + </ServiceHeader> + </Request> + <LanguageCode>en</LanguageCode> + <AWBNumber>4781585060</AWBNumber> + <LevelOfDetails>ALL_CHECK_POINTS</LevelOfDetails> +</req:KnownTrackingRequest> \ No newline at end of file diff --git a/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingResponse_MultipleAWB.xml b/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingResponse_MultipleAWB.xml new file mode 100755 index 000000000000..369236d80c61 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingResponse_MultipleAWB.xml @@ -0,0 +1,174 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<req:TrackingResponse xmlns:req="http://www.dhl.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.dhl.com TrackingResponse.xsd"> + <Response> + <ServiceHeader> + <MessageTime>2018-02-27T12:43:44+01:00</MessageTime> + <MessageReference>1234567890123456789012345678</MessageReference> + <SiteID>CustomerSiteID</SiteID> + </ServiceHeader> + </Response> + <AWBInfo> + <AWBNumber>4781584780</AWBNumber> + <Status> + <ActionStatus>success</ActionStatus> + </Status> + <ShipmentInfo> + <OriginServiceArea> + <ServiceAreaCode>PEK</ServiceAreaCode> + <Description>BEIJING-CHN</Description> + </OriginServiceArea> + <DestinationServiceArea> + <ServiceAreaCode>PHL</ServiceAreaCode> + <Description>WEST PHILADELPHIA,PA-USA</Description> + </DestinationServiceArea> + <ShipperName>THE EXP HIGH SCH ATT TO BNU</ShipperName> + <ShipperAccountNumber>123456789</ShipperAccountNumber> + <ConsigneeName>HAVEFORD COLLEGE</ConsigneeName> + <ShipmentDate>2017-12-25T14:38:00</ShipmentDate> + <Pieces>1</Pieces> + <Weight>0.5</Weight> + <WeightUnit>K</WeightUnit> + <GlobalProductCode>D</GlobalProductCode> + <ShipmentDesc>DOCUMENT</ShipmentDesc> + <DlvyNotificationFlag>Y</DlvyNotificationFlag> + <Shipper> + <City>BEIJING</City> + <PostalCode>100032</PostalCode> + <CountryCode>CN</CountryCode> + </Shipper> + <Consignee> + <City>HAVERFORD</City> + <DivisionCode>PA</DivisionCode> + <PostalCode>19041</PostalCode> + <CountryCode>US</CountryCode> + </Consignee> + <ShipperReference> + <ReferenceID>2469</ReferenceID> + </ShipperReference> + <ShipmentEvent> + <Date>2017-12-25</Date> + <Time>14:38:00</Time> + <ServiceEvent> + <EventCode>SD</EventCode> + <Description>Shipment information received</Description> + </ServiceEvent> + <Signatory/> + <ServiceArea> + <ServiceAreaCode>PEK</ServiceAreaCode> + <Description>BEIJING-CHN</Description> + </ServiceArea> + </ShipmentEvent> + </ShipmentInfo> + </AWBInfo> + <AWBInfo> + <AWBNumber>4781585060</AWBNumber> + <Status> + <ActionStatus>success</ActionStatus> + </Status> + <ShipmentInfo> + <OriginServiceArea> + <ServiceAreaCode>HKG</ServiceAreaCode> + <Description>HONG KONG-HKG</Description> + </OriginServiceArea> + <DestinationServiceArea> + <ServiceAreaCode>HKG</ServiceAreaCode> + <Description>HONG KONG-HKG</Description> + </DestinationServiceArea> + <ShipperName>NET-A-PORTER</ShipperName> + <ShipperAccountNumber>123456789</ShipperAccountNumber> + <ConsigneeName>NICOLE LI</ConsigneeName> + <ShipmentDate>2017-12-24T13:35:00</ShipmentDate> + <Pieces>1</Pieces> + <Weight>2.0</Weight> + <WeightUnit>K</WeightUnit> + <GlobalProductCode>N</GlobalProductCode> + <ShipmentDesc>NOT RESTRICTED FOR TRANSPORT,</ShipmentDesc> + <DlvyNotificationFlag>Y</DlvyNotificationFlag> + <Shipper> + <City>HONG KONG</City> + <CountryCode>HK</CountryCode> + </Shipper> + <Consignee> + <City>HONG KONG</City> + <DivisionCode>CH</DivisionCode> + <CountryCode>HK</CountryCode> + </Consignee> + <ShipperReference> + <ReferenceID>1060571</ReferenceID> + </ShipperReference> + <ShipmentEvent> + <Date>2017-12-24</Date> + <Time>13:35:00</Time> + <ServiceEvent> + <EventCode>SD</EventCode> + <Description>Shipment information received</Description> + </ServiceEvent> + <Signatory/> + <ServiceArea> + <ServiceAreaCode>HKG</ServiceAreaCode> + <Description>HONG KONG-HKG</Description> + </ServiceArea> + </ShipmentEvent> + </ShipmentInfo> + </AWBInfo> + <AWBInfo> + <AWBNumber>5702254250</AWBNumber> + <Status> + <ActionStatus>success</ActionStatus> + </Status> + <ShipmentInfo> + <OriginServiceArea> + <ServiceAreaCode>BHX</ServiceAreaCode> + <Description>BIRMINGHAM-GBR</Description> + </OriginServiceArea> + <DestinationServiceArea> + <ServiceAreaCode>AOI</ServiceAreaCode> + <Description>ANCONA-ITA</Description> + </DestinationServiceArea> + <ShipperName>AMAZON EU SARL</ShipperName> + <ShipperAccountNumber>123456789</ShipperAccountNumber> + <ConsigneeName>MATTEO LOMBO</ConsigneeName> + <ShipmentDate>2017-12-24T04:12:00</ShipmentDate> + <Pieces>1</Pieces> + <Weight>0.12</Weight> + <WeightUnit>K</WeightUnit> + <GlobalProductCode>U</GlobalProductCode> + <ShipmentDesc>CD</ShipmentDesc> + <DlvyNotificationFlag>Y</DlvyNotificationFlag> + <Shipper> + <City>PETERBOROUGH</City> + <PostalCode>PE2 9EN</PostalCode> + <CountryCode>GB</CountryCode> + </Shipper> + <Consignee> + <City>ORTONA</City> + <PostalCode>66026</PostalCode> + <CountryCode>IT</CountryCode> + </Consignee> + <ShipperReference> + <ReferenceID>DGWYDy4xN_1</ReferenceID> + </ShipperReference> + <ShipmentEvent> + <Date>2017-12-24</Date> + <Time>04:12:00</Time> + <ServiceEvent> + <EventCode>SD</EventCode> + <Description>Shipment information received</Description> + </ServiceEvent> + <Signatory/> + <ServiceArea> + <ServiceAreaCode>BHX</ServiceAreaCode> + <Description>BIRMINGHAM-GBR</Description> + </ServiceArea> + </ShipmentEvent> + </ShipmentInfo> + </AWBInfo> + <LanguageCode>en</LanguageCode> +</req:TrackingResponse> +<!-- ServiceInvocationId:20180227124344_5793_23bed3d9-e792-4955-8055-9472b1b41929 --> diff --git a/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingResponse_SingleAWB.xml b/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingResponse_SingleAWB.xml new file mode 100755 index 000000000000..ef303eaab64f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Dhl/_files/TrackingResponse_SingleAWB.xml @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<req:TrackingResponse xmlns:req="http://www.dhl.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.dhl.com TrackingResponse.xsd"> + <Response> + <ServiceHeader> + <MessageTime>2018-02-27T12:27:42+01:00</MessageTime> + <MessageReference>1234567890123456789012345678</MessageReference> + <SiteID>CustomerSiteID</SiteID> + </ServiceHeader> + </Response> + <AWBInfo> + <AWBNumber>4781585060</AWBNumber> + <Status> + <ActionStatus>success</ActionStatus> + </Status> + <ShipmentInfo> + <OriginServiceArea> + <ServiceAreaCode>HKG</ServiceAreaCode> + <Description>HONG KONG-HKG</Description> + </OriginServiceArea> + <DestinationServiceArea> + <ServiceAreaCode>HKG</ServiceAreaCode> + <Description>HONG KONG-HKG</Description> + </DestinationServiceArea> + <ShipperName>NET-A-PORTER</ShipperName> + <ShipperAccountNumber>123456789</ShipperAccountNumber> + <ConsigneeName>NICOLE LI</ConsigneeName> + <ShipmentDate>2017-12-24T13:35:00</ShipmentDate> + <Pieces>1</Pieces> + <Weight>2.0</Weight> + <WeightUnit>K</WeightUnit> + <GlobalProductCode>N</GlobalProductCode> + <ShipmentDesc>NOT RESTRICTED FOR TRANSPORT,</ShipmentDesc> + <DlvyNotificationFlag>Y</DlvyNotificationFlag> + <Shipper> + <City>HONG KONG</City> + <CountryCode>HK</CountryCode> + </Shipper> + <Consignee> + <City>HONG KONG</City> + <DivisionCode>CH</DivisionCode> + <CountryCode>HK</CountryCode> + </Consignee> + <ShipperReference> + <ReferenceID>1060571</ReferenceID> + </ShipperReference> + <ShipmentEvent> + <Date>2017-12-24</Date> + <Time>13:35:00</Time> + <ServiceEvent> + <EventCode>SD</EventCode> + <Description>Shipment information received</Description> + </ServiceEvent> + <Signatory/> + <ServiceArea> + <ServiceAreaCode>HKG</ServiceAreaCode> + <Description>HONG KONG-HKG</Description> + </ServiceArea> + </ShipmentEvent> + </ShipmentInfo> + </AWBInfo> + <LanguageCode>en</LanguageCode> +</req:TrackingResponse> +<!-- ServiceInvocationId:20180227122741_5793_e0f8c40e-5245-4737-ab31-323030366721 --> diff --git a/dev/tests/integration/testsuite/Magento/Directory/Model/CurrencyConfigTest.php b/dev/tests/integration/testsuite/Magento/Directory/Model/CurrencyConfigTest.php index b620d9097b4b..10f2749ddace 100644 --- a/dev/tests/integration/testsuite/Magento/Directory/Model/CurrencyConfigTest.php +++ b/dev/tests/integration/testsuite/Magento/Directory/Model/CurrencyConfigTest.php @@ -56,7 +56,7 @@ protected function setUp() } /** - * Test get currency config for admin and storefront areas. + * Test get currency config for admin, crontab and storefront areas. * * @dataProvider getConfigCurrenciesDataProvider * @magentoDataFixture Magento/Store/_files/store.php @@ -77,7 +77,7 @@ public function testGetConfigCurrencies(string $areaCode, array $expected) $storeManager = Bootstrap::getObjectManager()->get(StoreManagerInterface::class); $storeManager->setCurrentStore($store->getId()); - if ($areaCode === Area::AREA_ADMINHTML) { + if (in_array($areaCode, [Area::AREA_ADMINHTML, Area::AREA_CRONTAB])) { self::assertEquals($expected['allowed'], $this->currency->getConfigAllowCurrencies()); self::assertEquals($expected['base'], $this->currency->getConfigBaseCurrencies()); self::assertEquals($expected['default'], $this->currency->getConfigDefaultCurrencies()); @@ -118,6 +118,14 @@ public function getConfigCurrenciesDataProvider() 'default' => ['BDT', 'USD'], ], ], + [ + 'areaCode' => Area::AREA_CRONTAB, + 'expected' => [ + 'allowed' => ['BDT', 'BNS', 'BTD', 'EUR', 'USD'], + 'base' => ['BDT', 'USD'], + 'default' => ['BDT', 'USD'], + ], + ], [ 'areaCode' => Area::AREA_FRONTEND, 'expected' => [ diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/LinksTest.php b/dev/tests/integration/testsuite/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/LinksTest.php index c743fcec1dd8..b07a6506c1b7 100644 --- a/dev/tests/integration/testsuite/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/LinksTest.php +++ b/dev/tests/integration/testsuite/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/LinksTest.php @@ -5,6 +5,13 @@ */ namespace Magento\Downloadable\Block\Adminhtml\Catalog\Product\Edit\Tab\Downloadable; +/** + * Class LinksTest + * + * @package Magento\Downloadable\Block\Adminhtml\Catalog\Product\Edit\Tab\Downloadable + * @deprecated + * @see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Links + */ class LinksTest extends \PHPUnit\Framework\TestCase { /** diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/SamplesTest.php b/dev/tests/integration/testsuite/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/SamplesTest.php index 28d368035832..3f3b3bd62195 100644 --- a/dev/tests/integration/testsuite/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/SamplesTest.php +++ b/dev/tests/integration/testsuite/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/SamplesTest.php @@ -5,6 +5,13 @@ */ namespace Magento\Downloadable\Block\Adminhtml\Catalog\Product\Edit\Tab\Downloadable; +/** + * Class SamplesTest + * + * @package Magento\Downloadable\Block\Adminhtml\Catalog\Product\Edit\Tab\Downloadable + * @deprecated + * @see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Samples + */ class SamplesTest extends \PHPUnit\Framework\TestCase { public function testGetUploadButtonsHtml() diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/Controller/Adminhtml/Downloadable/FileTest.php b/dev/tests/integration/testsuite/Magento/Downloadable/Controller/Adminhtml/Downloadable/FileTest.php index 3f13ce7f41ed..60c2a41fae49 100644 --- a/dev/tests/integration/testsuite/Magento/Downloadable/Controller/Adminhtml/Downloadable/FileTest.php +++ b/dev/tests/integration/testsuite/Magento/Downloadable/Controller/Adminhtml/Downloadable/FileTest.php @@ -37,6 +37,7 @@ public function testUploadAction() ], ]; + $this->getRequest()->setMethod('POST'); $this->dispatch('backend/admin/downloadable_file/upload/type/samples'); $body = $this->getResponse()->getBody(); $result = Bootstrap::getObjectManager()->get(Json::class)->unserialize($body); @@ -64,6 +65,7 @@ public function testUploadProhibitedExtensions($fileName) ], ]; + $this->getRequest()->setMethod('POST'); $this->dispatch('backend/admin/downloadable_file/upload/type/samples'); $body = $this->getResponse()->getBody(); $result = Bootstrap::getObjectManager()->get(Json::class)->unserialize($body); diff --git a/dev/tests/integration/testsuite/Magento/DownloadableImportExport/Model/DownloadableTest.php b/dev/tests/integration/testsuite/Magento/DownloadableImportExport/Model/DownloadableTest.php index c80cd13a1683..d0e4471e2ea6 100644 --- a/dev/tests/integration/testsuite/Magento/DownloadableImportExport/Model/DownloadableTest.php +++ b/dev/tests/integration/testsuite/Magento/DownloadableImportExport/Model/DownloadableTest.php @@ -9,7 +9,10 @@ class DownloadableTest extends AbstractProductExportImportTestCase { - public function exportImportDataProvider() + /** + * @return array + */ + public function exportImportDataProvider(): array { return [ 'downloadable-product' => [ @@ -31,79 +34,32 @@ public function exportImportDataProvider() ]; } - public function importReplaceDataProvider() - { - return $this->exportImportDataProvider(); - } - - /** - * @param array $fixtures - * @param string[] $skus - * @param string[] $skippedAttributes - * @dataProvider exportImportDataProvider - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - * @todo remove after MAGETWO-38240 resolved - */ - public function testExport($fixtures, $skus, $skippedAttributes = [], $rollbackFixtures = []) - { - $this->markTestSkipped('Uncomment after MAGETWO-38240 resolved'); - } - - /** - * @param array $fixtures - * @param string[] $skus - * @dataProvider exportImportDataProvider - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * - * @todo remove after MAGETWO-38240 resolved - */ - public function testImportDelete($fixtures, $skus, $skippedAttributes = [], $rollbackFixtures = []) - { - $this->markTestSkipped('Uncomment after MAGETWO-38240 resolved'); - } - /** - * @magentoAppArea adminhtml - * @magentoDbIsolation enabled - * @magentoAppIsolation enabled - * - * @param array $fixtures - * @param string[] $skus - * @param string[] $skippedAttributes - * @dataProvider importReplaceDataProvider - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * Run import/export tests. * - * @todo remove after MAGETWO-38240 resolved - */ - public function testImportReplace($fixtures, $skus, $skippedAttributes = [], $rollbackFixtures = []) - { - $this->markTestSkipped('Uncomment after MAGETWO-38240 resolved'); - } - - /** * @magentoAppArea adminhtml - * @magentoDbIsolation enabled + * @magentoDbIsolation disabled * @magentoAppIsolation enabled * * @param array $fixtures * @param string[] $skus * @param string[] $skippedAttributes - * @dataProvider importReplaceDataProvider - * + * @return void + * @dataProvider exportImportDataProvider * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function testImportReplaceWithPagination($fixtures, $skus, $skippedAttributes = []) + public function testImportExport(array $fixtures, array $skus, array $skippedAttributes = []): void { $this->markTestSkipped('Uncomment after MAGETWO-38240 resolved'); } /** - * @param \Magento\Catalog\Model\Product $expectedProduct - * @param \Magento\Catalog\Model\Product $actualProduct + * @inheritdoc */ - protected function assertEqualsSpecificAttributes($expectedProduct, $actualProduct) - { + protected function assertEqualsSpecificAttributes( + \Magento\Catalog\Model\Product $expectedProduct, + \Magento\Catalog\Model\Product $actualProduct + ): void { $expectedProductLinks = $expectedProduct->getExtensionAttributes()->getDownloadableProductLinks(); $expectedProductSamples = $expectedProduct->getExtensionAttributes()->getDownloadableProductSamples(); diff --git a/dev/tests/integration/testsuite/Magento/Eav/Model/Entity/Attribute/Frontend/DefaultFrontendTest.php b/dev/tests/integration/testsuite/Magento/Eav/Model/Entity/Attribute/Frontend/DefaultFrontendTest.php index 5e8239586d76..9961101dac7c 100644 --- a/dev/tests/integration/testsuite/Magento/Eav/Model/Entity/Attribute/Frontend/DefaultFrontendTest.php +++ b/dev/tests/integration/testsuite/Magento/Eav/Model/Entity/Attribute/Frontend/DefaultFrontendTest.php @@ -9,7 +9,7 @@ use Magento\TestFramework\Helper\CacheCleaner; use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; use Magento\Framework\App\CacheInterface; -use Magento\Store\Api\StoreResolverInterface; +use Magento\Store\Model\StoreManagerInterface; use Magento\Framework\Serialize\Serializer\Json as Serializer; use Magento\Eav\Model\Entity\Attribute; @@ -44,9 +44,9 @@ class DefaultFrontendTest extends \PHPUnit\Framework\TestCase private $cache; /** - * @var StoreResolverInterface + * @var StoreManagerInterface */ - private $storeResolver; + private $storeManager; /** * @var Serializer @@ -60,7 +60,7 @@ protected function setUp() $this->defaultFrontend = $this->objectManager->get(DefaultFrontend::class); $this->cache = $this->objectManager->get(CacheInterface::class); - $this->storeResolver = $this->objectManager->get(StoreResolverInterface::class); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); $this->serializer = $this->objectManager->get(Serializer::class); $this->attribute = $this->objectManager->get(Attribute::class); @@ -99,6 +99,6 @@ private function getCacheKey() { return 'attribute-navigation-option-' . $this->defaultFrontend->getAttribute()->getAttributeCode() . '-' . - $this->storeResolver->getCurrentStoreId(); + $this->storeManager->getStore()->getId(); } } diff --git a/dev/tests/integration/testsuite/Magento/Eav/Setup/EavSetupTest.php b/dev/tests/integration/testsuite/Magento/Eav/Setup/EavSetupTest.php index 5d7a72c65597..a5843f20ad98 100644 --- a/dev/tests/integration/testsuite/Magento/Eav/Setup/EavSetupTest.php +++ b/dev/tests/integration/testsuite/Magento/Eav/Setup/EavSetupTest.php @@ -55,7 +55,7 @@ public function addAttributeDataProvider() { return [ ['eav_setup_test'], - ['_59_characters_59_characters_59_characters_59_characters_59'], + ['characters_59_characters_59_characters_59_characters_59_59_'], ]; } @@ -90,6 +90,33 @@ public function addAttributeThrowExceptionDataProvider() ]; } + /** + * Verify that add attribute throw exception if attribute_code is not valid. + * + * @param string|null $attributeCode + * + * @dataProvider addInvalidAttributeThrowExceptionDataProvider + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Please use only letters (a-z or A-Z), numbers (0-9) or underscore (_) in this field, + */ + public function testAddInvalidAttributeThrowException($attributeCode) + { + $attributeData = $this->getAttributeData(); + $this->eavSetup->addAttribute(\Magento\Catalog\Model\Product::ENTITY, $attributeCode, $attributeData); + } + /** + * Data provider for testAddInvalidAttributeThrowException(). + * + * @return array + */ + public function addInvalidAttributeThrowExceptionDataProvider() + { + return [ + ['1first_character_is_not_letter'], + ['attribute.with.dots'], + ]; + } + /** * Get simple attribute data. */ diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/Controller/Adminhtml/Category/SaveTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/Controller/Adminhtml/Category/SaveTest.php new file mode 100644 index 000000000000..fcd8226aec50 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/Controller/Adminhtml/Category/SaveTest.php @@ -0,0 +1,170 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Controller\Adminhtml\Category; + +use Magento\Catalog\Api\CategoryListInterface; +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Indexer\Category\Product as CategoryIndexer; +use Magento\CatalogSearch\Model\Indexer\Fulltext as FulltextIndexer; +use Magento\Elasticsearch\Model\Config; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Indexer\IndexerInterface; +use Magento\Framework\Indexer\IndexerRegistry; +use Magento\Framework\Message\MessageInterface; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * @magentoAppArea adminhtml + * @magentoDbIsolation disabled + */ +class SaveTest extends AbstractBackendController +{ + private $indexerSchedule = []; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + + $config = $this->getMockBuilder(Config::class) + ->disableOriginalConstructor() + ->getMock(); + $config->method('isElasticsearchEnabled') + ->willReturn(true); + $this->_objectManager->addSharedInstance($config, Config::class); + + $this->changeIndexerSchedule(FulltextIndexer::INDEXER_ID, true); + $this->changeIndexerSchedule(CategoryIndexer::INDEXER_ID, true); + } + + /** + * @inheritdoc + */ + protected function tearDown() + { + $this->_objectManager->removeSharedInstance(Config::class); + $this->changeIndexerSchedule(FulltextIndexer::INDEXER_ID, $this->indexerSchedule[FulltextIndexer::INDEXER_ID]); + $this->changeIndexerSchedule(CategoryIndexer::INDEXER_ID, $this->indexerSchedule[CategoryIndexer::INDEXER_ID]); + + parent::tearDown(); + } + + /** + * Checks a case when indexers are invalidated if products for category were changed. + * + * @magentoDataFixture Magento/Catalog/_files/category_product.php + * @magentoDataFixture Magento/Catalog/_files/multiple_products.php + */ + public function testExecute() + { + $fulltextIndexer = $this->getIndexer(FulltextIndexer::INDEXER_ID); + self::assertTrue($fulltextIndexer->isValid(), 'Fulltext indexer should be valid.'); + $categoryIndexer = $this->getIndexer(CategoryIndexer::INDEXER_ID); + self::assertTrue($categoryIndexer->isValid(), 'Category indexer should be valid.'); + + $category = $this->getCategory('Category 1'); + $productIdList = $this->getProductIdList(['simple1', 'simple2', 'simple3']); + $inputData = [ + 'category_products' => json_encode(array_fill_keys($productIdList, [0, 1, 2])), + 'entity_id' => $category->getId(), + 'default_sort_by' => 'position' + ]; + + $this->getRequest()->setPostValue($inputData); + $this->getRequest()->setMethod('POST'); + $this->dispatch('backend/catalog/category/save'); + $this->assertSessionMessages( + self::equalTo(['You saved the category.']), + MessageInterface::TYPE_SUCCESS + ); + + $fulltextIndexer = $this->getIndexer(FulltextIndexer::INDEXER_ID); + self::assertTrue($fulltextIndexer->isInvalid(), 'Fulltext indexer should be invalidated.'); + $categoryIndexer = $this->getIndexer(CategoryIndexer::INDEXER_ID); + self::assertTrue($categoryIndexer->isInvalid(), 'Category indexer should be invalidated.'); + } + + /** + * Gets indexer from registry by ID. + * + * @param string $indexerId + * @return IndexerInterface + */ + private function getIndexer(string $indexerId): IndexerInterface + { + /** @var IndexerRegistry $indexerRegistry */ + $indexerRegistry = $this->_objectManager->get(IndexerRegistry::class); + return $indexerRegistry->get($indexerId); + } + + /** + * Changes the scheduled state of indexer. + * + * @param string $indexerId + * @param bool $isScheduled + * @return void + */ + private function changeIndexerSchedule(string $indexerId, bool $isScheduled): void + { + $indexer = $this->getIndexer($indexerId); + if (!isset($this->indexerSchedule[$indexerId])) { + $this->indexerSchedule[$indexerId] = $indexer->isScheduled(); + $indexer->reindexAll(); + } + $indexer->setScheduled($isScheduled); + } + + /** + * Gets category by name. + * + * @param string $name + * @return CategoryInterface + */ + private function getCategory(string $name): CategoryInterface + { + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $this->_objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter('name', $name) + ->create(); + /** @var CategoryListInterface $repository */ + $repository = $this->_objectManager->get(CategoryListInterface::class); + $items = $repository->getList($searchCriteria) + ->getItems(); + + return array_pop($items); + } + + /** + * Gets list of product ID by SKU. + * + * @param array $skuList + * @return array + */ + private function getProductIdList(array $skuList): array + { + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $this->_objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter('sku', $skuList, 'in') + ->create(); + + /** @var ProductRepositoryInterface $repository */ + $repository = $this->_objectManager->get(ProductRepositoryInterface::class); + $items = $repository->getList($searchCriteria) + ->getItems(); + + $idList = array_map(function (ProductInterface $item) { + return $item->getId(); + }, $items); + + return $idList; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/AdapterTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/AdapterTest.php new file mode 100644 index 000000000000..a52c5bb9e21b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/AdapterTest.php @@ -0,0 +1,107 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Elasticsearch5\SearchAdapter; + +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Class AdapterTest + */ +class AdapterTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Adapter + */ + private $adapter; + + /** + * @var \Magento\Elasticsearch\Model\Client\Elasticsearch|\PHPUnit\Framework\MockObject\MockObject + */ + private $clientMock; + + /** + * @var \Magento\Framework\Search\Request\Builder + */ + private $requestBuilder; + + /** + * @var \Psr\Log\LoggerInterface|\PHPUnit\Framework\MockObject\MockObject + */ + private $loggerMock; + + /** + * @return void + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $contentManager = $this->getMockBuilder(\Magento\Elasticsearch\SearchAdapter\ConnectionManager::class) + ->disableOriginalConstructor() + ->getMock(); + $this->clientMock = $this->getMockBuilder(\Magento\Elasticsearch6\Model\Client\Elasticsearch::class) + ->disableOriginalConstructor() + ->getMock(); + $contentManager + ->expects($this->any()) + ->method('getConnection') + ->willReturn($this->clientMock); + /** @var \Magento\Framework\Search\Request\Config\Converter $converter */ + $converter = $objectManager->create(\Magento\Framework\Search\Request\Config\Converter::class); + + $document = new \DOMDocument(); + $document->load($this->getRequestConfigPath()); + $requestConfig = $converter->convert($document); + + /** @var \Magento\Framework\Search\Request\Config $config */ + $config = $objectManager->create(\Magento\Framework\Search\Request\Config::class); + $config->merge($requestConfig); + + $this->requestBuilder = $objectManager->create( + \Magento\Framework\Search\Request\Builder::class, + ['config' => $config] + ); + $this->loggerMock = $this->getMockForAbstractClass(\Psr\Log\LoggerInterface::class); + + $this->adapter = $objectManager->create( + \Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Adapter::class, + [ + 'connectionManager' => $contentManager, + 'logger' => $this->loggerMock + ] + ); + } + + /** + * @magentoAppIsolation enabled + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 + * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest + * @return void + */ + public function testQuery() + { + $this->requestBuilder->bind('fulltext_search_query', 'socks'); + $this->requestBuilder->setRequestName('one_match'); + $queryRequest = $this->requestBuilder->create(); + $exception = new \Exception('Test Message'); + $this->loggerMock->expects($this->once())->method('critical')->with($exception); + $this->clientMock->expects($this->once())->method('query')->willThrowException($exception); + $actualResponse = $this->adapter->query($queryRequest); + $this->assertEmpty($actualResponse->getAggregations()->getBuckets()); + $this->assertEquals(0, $actualResponse->count()); + } + + /** + * Get request config path + * + * @return string + */ + private function getRequestConfigPath() + { + return __DIR__ . '/../../_files/requests.xml'; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Client/ElasticsearchTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Client/ElasticsearchTest.php index 61add5f7d0ea..3eea2497daa1 100644 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Client/ElasticsearchTest.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Client/ElasticsearchTest.php @@ -10,7 +10,7 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\Store\Model\StoreManagerInterface; use Magento\Elasticsearch\SearchAdapter\ConnectionManager; -use Magento\Elasticsearch\Model\Client\Elasticsearch as ElasticsearchClient; +use Magento\Elasticsearch6\Model\Client\Elasticsearch as ElasticsearchClient; use Magento\Elasticsearch\Model\Config; use Magento\Elasticsearch\SearchAdapter\SearchIndexNameResolver; @@ -95,7 +95,7 @@ private function search($text) } /** - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix composite_product_search */ public function testSearchConfigurableProductBySimpleProductName() @@ -104,7 +104,7 @@ public function testSearchConfigurableProductBySimpleProductName() } /** - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix composite_product_search */ public function testSearchConfigurableProductBySimpleProductAttributeMultiselect() @@ -113,7 +113,7 @@ public function testSearchConfigurableProductBySimpleProductAttributeMultiselect } /** - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix composite_product_search */ public function testSearchConfigurableProductBySimpleProductAttributeSelect() @@ -122,7 +122,7 @@ public function testSearchConfigurableProductBySimpleProductAttributeSelect() } /** - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix composite_product_search */ public function testSearchConfigurableProductBySimpleProductAttributeShortDescription() diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/IndexHandlerTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/IndexHandlerTest.php index 014aaf7679bc..77533e83b719 100755 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/IndexHandlerTest.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/IndexHandlerTest.php @@ -13,7 +13,7 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\Store\Model\StoreManagerInterface; use Magento\Elasticsearch\SearchAdapter\ConnectionManager; -use Magento\Elasticsearch\Model\Client\Elasticsearch as ElasticsearchClient; +use Magento\Elasticsearch6\Model\Client\Elasticsearch as ElasticsearchClient; use Magento\Elasticsearch\Model\Config; use Magento\Elasticsearch\SearchAdapter\SearchIndexNameResolver; use Magento\Indexer\Model\Indexer; @@ -87,7 +87,7 @@ protected function setUp() } /** - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix indexerhandlertest * @return void */ @@ -106,7 +106,7 @@ public function testReindexAll(): void /** * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix indexerhandlertest * @return void */ @@ -131,7 +131,7 @@ public function testReindexRowAfterEdit(): void } /** - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix indexerhandlertest * @return void */ @@ -170,7 +170,7 @@ public function testReindexRowAfterMassAction(): void } /** - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix indexerhandlertest * @magentoAppArea adminhtml * @return void @@ -192,7 +192,7 @@ public function testReindexRowAfterDelete(): void /** * @magentoDbIsolation enabled * @magentoAppArea adminhtml - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix indexerhandlertest * @magentoDataFixture Magento/Elasticsearch/_files/configurable_products.php * @return void diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/ReindexAllTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/ReindexAllTest.php index d40ce9e8a070..7d4aa8e005e4 100644 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/ReindexAllTest.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/ReindexAllTest.php @@ -68,7 +68,7 @@ protected function setUp() /** * Test search of all products after full reindex * - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix indexerhandlertest_configurable * @magentoDataFixture Magento/ConfigurableProduct/_files/configurable_products.php */ @@ -82,7 +82,7 @@ public function testSearchAll() /** * Test search of specific product after full reindex * - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix indexerhandlertest_configurable * @magentoDataFixture Magento/ConfigurableProduct/_files/configurable_products.php */ diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/SearchAdapter/AdapterTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/SearchAdapter/AdapterTest.php index 05b9ed3bd657..a3da32e0d6c4 100644 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/SearchAdapter/AdapterTest.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/SearchAdapter/AdapterTest.php @@ -5,8 +5,6 @@ */ namespace Magento\Elasticsearch\SearchAdapter; -use Magento\Elasticsearch\Model\Config; - /** * Class AdapterTest * @@ -26,7 +24,7 @@ class AdapterTest extends \Magento\Framework\Search\Adapter\Mysql\AdapterTest /** * @var string */ - protected $searchEngine = Config::ENGINE_NAME; + protected $searchEngine = 'elasticsearch6'; /** * Get request config path @@ -43,12 +41,12 @@ protected function getRequestConfigPath() */ protected function createAdapter() { - return $this->objectManager->create(\Magento\Elasticsearch\SearchAdapter\Adapter::class); + return $this->objectManager->create(\Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Adapter::class); } /** * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testMatchQuery() @@ -58,7 +56,7 @@ public function testMatchQuery() /** * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testMatchOrderedQuery() @@ -70,7 +68,7 @@ public function testMatchOrderedQuery() /** * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testAggregationsQuery() @@ -80,7 +78,7 @@ public function testAggregationsQuery() /** * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testMatchQueryFilters() @@ -92,7 +90,7 @@ public function testMatchQueryFilters() * Range filter test with all fields filled * * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testRangeFilterWithAllFields() @@ -104,7 +102,7 @@ public function testRangeFilterWithAllFields() * Range filter test with all fields filled * * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testRangeFilterWithoutFromField() @@ -116,7 +114,7 @@ public function testRangeFilterWithoutFromField() * Range filter test with all fields filled * * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testRangeFilterWithoutToField() @@ -128,7 +126,7 @@ public function testRangeFilterWithoutToField() * Term filter test * * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testTermFilter() @@ -140,7 +138,7 @@ public function testTermFilter() * Term filter test * * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testTermFilterArray() @@ -152,7 +150,7 @@ public function testTermFilterArray() * Term filter test * * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testWildcardFilter() @@ -164,7 +162,7 @@ public function testWildcardFilter() * Request limits test * * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testSearchLimit() @@ -176,7 +174,7 @@ public function testSearchLimit() * Bool filter test * * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testBoolFilter() @@ -188,7 +186,7 @@ public function testBoolFilter() * Test bool filter with nested negative bool filter * * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testBoolFilterWithNestedNegativeBoolFilter() @@ -200,7 +198,7 @@ public function testBoolFilterWithNestedNegativeBoolFilter() * Test range inside nested negative bool filter * * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testBoolFilterWithNestedRangeInNegativeBoolFilter() @@ -213,7 +211,7 @@ public function testBoolFilterWithNestedRangeInNegativeBoolFilter() * * @dataProvider elasticSearchAdvancedSearchDataProvider * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest * @param string $nameQuery * @param string $descriptionQuery @@ -259,7 +257,7 @@ public function elasticSearchAdvancedSearchDataProvider() /** * @magentoAppIsolation enabled * @magentoDataFixture Magento/Framework/Search/_files/filterable_attribute.php - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testCustomFilterableAttribute() @@ -274,7 +272,7 @@ public function testCustomFilterableAttribute() * * @magentoAppIsolation enabled * @magentoDataFixture Magento/Framework/Search/_files/filterable_attributes.php - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest * @dataProvider filterByAttributeValuesDataProvider * @param string $requestName @@ -294,7 +292,7 @@ public function testFilterByAttributeValues($requestName, $additionalData) * @param $rangeFilter * @param $expectedRecordsCount * @magentoDataFixture Magento/Framework/Search/_files/date_attribute.php - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest * @magentoAppIsolation enabled * @dataProvider dateDataProvider @@ -309,7 +307,7 @@ public function testAdvancedSearchDateField($rangeFilter, $expectedRecordsCount) /** * @magentoDataFixture Magento/Framework/Search/_files/product_configurable.php * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testAdvancedSearchCompositeProductWithOutOfStockOption() @@ -320,7 +318,7 @@ public function testAdvancedSearchCompositeProductWithOutOfStockOption() /** * @magentoDataFixture Magento/Framework/Search/_files/product_configurable_with_disabled_child.php * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testAdvancedSearchCompositeProductWithDisabledChild() @@ -333,7 +331,7 @@ public function testAdvancedSearchCompositeProductWithDisabledChild() /** * @magentoDataFixture Magento/Framework/Search/_files/search_weight_products.php * @magentoAppIsolation enabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix adaptertest */ public function testSearchQueryBoost() @@ -356,4 +354,31 @@ private function reindexAll() $indexer->load('catalogsearch_fulltext'); $indexer->reindexAll(); } + + /** + * Date data provider + * + * @return array + */ + public function dateDataProvider() + { + return [ + [['from' => '1999-12-31T00:00:00Z', 'to' => '2000-01-01T00:00:00Z'], 1], + [['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/Email/Model/Template/FilterTest.php b/dev/tests/integration/testsuite/Magento/Email/Model/Template/FilterTest.php index a68b0942ec09..dd55dcc8b47c 100644 --- a/dev/tests/integration/testsuite/Magento/Email/Model/Template/FilterTest.php +++ b/dev/tests/integration/testsuite/Magento/Email/Model/Template/FilterTest.php @@ -130,22 +130,22 @@ public function layoutDirectiveDataProvider() 'area parameter - omitted' => [ 'adminhtml', 'handle="email_template_test_handle"', - '<b>Email content for frontend/Magento/default theme</b>', + '<strong>Email content for frontend/Magento/default theme</strong>', ], 'area parameter - frontend' => [ 'adminhtml', 'handle="email_template_test_handle" area="frontend"', - '<b>Email content for frontend/Magento/default theme</b>', + '<strong>Email content for frontend/Magento/default theme</strong>', ], 'area parameter - backend' => [ 'frontend', 'handle="email_template_test_handle" area="adminhtml"', - '<b>Email content for adminhtml/Magento/default theme</b>', + '<strong>Email content for adminhtml/Magento/default theme</strong>', ], 'custom parameter' => [ 'frontend', 'handle="email_template_test_handle" template="Magento_Email::sample_email_content_custom.phtml"', - '<b>Custom Email content for frontend/Magento/default theme</b>', + '<strong>Custom Email content for frontend/Magento/default theme</strong>', ], ]; return $result; diff --git a/dev/tests/integration/testsuite/Magento/Email/Model/TemplateTest.php b/dev/tests/integration/testsuite/Magento/Email/Model/TemplateTest.php index a83de07443e9..2d12eefc286c 100644 --- a/dev/tests/integration/testsuite/Magento/Email/Model/TemplateTest.php +++ b/dev/tests/integration/testsuite/Magento/Email/Model/TemplateTest.php @@ -315,25 +315,25 @@ public function templateDirectiveDataProvider() Area::AREA_FRONTEND, TemplateTypesInterface::TYPE_HTML, '{{template config_path="customer/create_account/email_template"}}', - '<b>customer_create_account_email_template template from Vendor/custom_theme</b>', + '<strong>customer_create_account_email_template template from Vendor/custom_theme</strong>', ], 'Template from parent theme - frontend' => [ Area::AREA_FRONTEND, TemplateTypesInterface::TYPE_HTML, '{{template config_path="customer/create_account/email_confirmation_template"}}', - '<b>customer_create_account_email_confirmation_template template from Vendor/default</b>', + '<strong>customer_create_account_email_confirmation_template template from Vendor/default</strong', ], 'Template from grandparent theme - frontend' => [ Area::AREA_FRONTEND, TemplateTypesInterface::TYPE_HTML, '{{template config_path="customer/create_account/email_confirmed_template"}}', - '<b>customer_create_account_email_confirmed_template template from Magento/default</b>', + '<strong>customer_create_account_email_confirmed_template template from Magento/default</strong', ], 'Template from grandparent theme - adminhtml' => [ BackendFrontNameResolver::AREA_CODE, TemplateTypesInterface::TYPE_HTML, '{{template config_path="catalog/productalert_cron/error_email_template"}}', - '<b>catalog_productalert_cron_error_email_template template from Magento/default</b>', + '<strong>catalog_productalert_cron_error_email_template template from Magento/default</strong', null, null, true, @@ -703,8 +703,8 @@ public function testGetVariablesOptionArrayInGroup() $testTemplateVariables = '{"var data.name":"Sender Name","var data.email":"Sender Email"}'; $this->model->setOrigTemplateVariables($testTemplateVariables); $variablesOptionArray = $this->model->getVariablesOptionArray(true); - $this->assertEquals('Template Variables', $variablesOptionArray['label']->getText()); - $this->assertEquals($this->model->getVariablesOptionArray(), $variablesOptionArray['value']); + $this->assertEquals('Template Variables', $variablesOptionArray[0]['label']->getText()); + $this->assertEquals($this->model->getVariablesOptionArray(), $variablesOptionArray[0]['value']); } /** diff --git a/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/adminhtml/Magento/default/Magento_Email/templates/sample_email_content.phtml b/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/adminhtml/Magento/default/Magento_Email/templates/sample_email_content.phtml index d53468a38376..bb0073cedee9 100644 --- a/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/adminhtml/Magento/default/Magento_Email/templates/sample_email_content.phtml +++ b/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/adminhtml/Magento/default/Magento_Email/templates/sample_email_content.phtml @@ -4,4 +4,4 @@ * See COPYING.txt for license details. */ ?> -<b>Email content for adminhtml/Magento/default theme</b> +<strong>Email content for adminhtml/Magento/default theme</strong> diff --git a/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/adminhtml/Magento/default/Magento_ProductAlert/email/cron_error.html b/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/adminhtml/Magento/default/Magento_ProductAlert/email/cron_error.html index f13e54edf93a..d65f9d4c4087 100644 --- a/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/adminhtml/Magento/default/Magento_ProductAlert/email/cron_error.html +++ b/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/adminhtml/Magento/default/Magento_ProductAlert/email/cron_error.html @@ -4,4 +4,4 @@ * See COPYING.txt for license details. */ --> -<b>catalog_productalert_cron_error_email_template template from Magento/default</b> +<strong>catalog_productalert_cron_error_email_template template from Magento/default</strong> diff --git a/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Magento/default/Magento_Customer/email/account_new_confirmed.html b/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Magento/default/Magento_Customer/email/account_new_confirmed.html index f687fc041db1..ffc4d8893fe9 100644 --- a/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Magento/default/Magento_Customer/email/account_new_confirmed.html +++ b/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Magento/default/Magento_Customer/email/account_new_confirmed.html @@ -4,4 +4,4 @@ * See COPYING.txt for license details. */ --> -<b>customer_create_account_email_confirmed_template template from Magento/default</b> +<strong>customer_create_account_email_confirmed_template template from Magento/default</strong> diff --git a/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Magento/default/Magento_Email/templates/sample_email_content.phtml b/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Magento/default/Magento_Email/templates/sample_email_content.phtml index acbdf16d474d..9c973818272c 100644 --- a/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Magento/default/Magento_Email/templates/sample_email_content.phtml +++ b/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Magento/default/Magento_Email/templates/sample_email_content.phtml @@ -4,4 +4,4 @@ * See COPYING.txt for license details. */ ?> -<b>Email content for frontend/Magento/default theme</b> +<strong>Email content for frontend/Magento/default theme</strong> diff --git a/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Magento/default/Magento_Email/templates/sample_email_content_custom.phtml b/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Magento/default/Magento_Email/templates/sample_email_content_custom.phtml index 1730bf904bb3..4ed5685ee010 100644 --- a/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Magento/default/Magento_Email/templates/sample_email_content_custom.phtml +++ b/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Magento/default/Magento_Email/templates/sample_email_content_custom.phtml @@ -4,4 +4,4 @@ * See COPYING.txt for license details. */ ?> -<b>Custom Email content for frontend/Magento/default theme</b> +<strong>Custom Email content for frontend/Magento/default theme</strong> diff --git a/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Vendor/custom_theme/Magento_Customer/email/account_new.html b/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Vendor/custom_theme/Magento_Customer/email/account_new.html index 7e8f9bd1b12b..46257060f828 100644 --- a/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Vendor/custom_theme/Magento_Customer/email/account_new.html +++ b/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Vendor/custom_theme/Magento_Customer/email/account_new.html @@ -4,4 +4,4 @@ * See COPYING.txt for license details. */ --> -<b>customer_create_account_email_template template from Vendor/custom_theme</b> +<strong>customer_create_account_email_template template from Vendor/custom_theme</strong> diff --git a/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Vendor/default/Magento_Customer/email/account_new_confirmation.html b/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Vendor/default/Magento_Customer/email/account_new_confirmation.html index c5801b6557a6..9c52c5a1b38c 100644 --- a/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Vendor/default/Magento_Customer/email/account_new_confirmation.html +++ b/dev/tests/integration/testsuite/Magento/Email/Model/_files/design/frontend/Vendor/default/Magento_Customer/email/account_new_confirmation.html @@ -4,4 +4,4 @@ * See COPYING.txt for license details. */ --> -<b>customer_create_account_email_confirmation_template template from Vendor/default</b> +<strong>customer_create_account_email_confirmation_template template from Vendor/default</strong> diff --git a/dev/tests/integration/testsuite/Magento/EncryptionKey/Controller/Adminhtml/Crypt/Key/SaveTest.php b/dev/tests/integration/testsuite/Magento/EncryptionKey/Controller/Adminhtml/Crypt/Key/SaveTest.php index 22e8a5911f08..822c3c031886 100644 --- a/dev/tests/integration/testsuite/Magento/EncryptionKey/Controller/Adminhtml/Crypt/Key/SaveTest.php +++ b/dev/tests/integration/testsuite/Magento/EncryptionKey/Controller/Adminhtml/Crypt/Key/SaveTest.php @@ -72,7 +72,7 @@ public function testSaveActionWithInvalidKey() $this->assertRedirect(); $this->assertSessionMessages( - $this->contains('The encryption key format is invalid.'), + $this->contains('Encryption key must be 32 character string without any white space.'), \Magento\Framework\Message\MessageInterface::TYPE_ERROR ); } diff --git a/dev/tests/integration/testsuite/Magento/EncryptionKey/Setup/Patch/Data/SodiumChachaPatchTest.php b/dev/tests/integration/testsuite/Magento/EncryptionKey/Setup/Patch/Data/SodiumChachaPatchTest.php index 3a47692bdb93..a563641a4f87 100644 --- a/dev/tests/integration/testsuite/Magento/EncryptionKey/Setup/Patch/Data/SodiumChachaPatchTest.php +++ b/dev/tests/integration/testsuite/Magento/EncryptionKey/Setup/Patch/Data/SodiumChachaPatchTest.php @@ -41,6 +41,9 @@ public function testChangeEncryptionKey() $structureMock->expects($this->once()) ->method('getFieldPathsByAttribute') ->will($this->returnValue([$testPath])); + $structureMock->expects($this->once()) + ->method('getFieldPaths') + ->willReturn([]); /** @var \Magento\Config\Model\ResourceModel\Config $configModel */ $configModel = $this->objectManager->create(\Magento\Config\Model\ResourceModel\Config::class); diff --git a/dev/tests/integration/testsuite/Magento/Framework/App/FrontControllerTest.php b/dev/tests/integration/testsuite/Magento/Framework/App/FrontControllerTest.php index 431c70543081..89240f4ab324 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/App/FrontControllerTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/App/FrontControllerTest.php @@ -5,11 +5,21 @@ */ namespace Magento\Framework\App; +use Magento\Framework\App\Request\InvalidRequestException; +use Magento\Framework\App\Request\ValidatorInterface; +use Magento\Framework\Exception\NotFoundException; +use Magento\Framework\Phrase; +use PHPUnit\Framework\TestCase; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Controller\ResultInterface; +use Magento\TestFramework\Helper\Bootstrap; + /** * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @magentoAppArea frontend */ -class FrontControllerTest extends \PHPUnit\Framework\TestCase +class FrontControllerTest extends TestCase { /** * @var \Magento\Framework\ObjectManagerInterface @@ -17,28 +27,94 @@ class FrontControllerTest extends \PHPUnit\Framework\TestCase protected $_objectManager; /** - * @var \Magento\Framework\App\FrontController + * @var FrontController */ protected $_model; + /** + * @var ValidatorInterface + */ + private $fakeRequestValidator; + + /** + * @return ValidatorInterface + * + * @SuppressWarnings(PHPMD.UnusedLocalVariable) + */ + private function createRequestValidator(): ValidatorInterface + { + if (!$this->fakeRequestValidator) { + $this->fakeRequestValidator = new class implements ValidatorInterface { + /** + * @var bool + */ + public $valid; + + /** + * @inheritDoc + */ + public function validate( + RequestInterface $request, + ActionInterface $action + ): void { + if (!$this->valid) { + throw new InvalidRequestException(new NotFoundException(new Phrase('Not found'))); + } + } + }; + } + + return $this->fakeRequestValidator; + } + + /** + * @inheritDoc + */ protected function setUp() { - $this->_objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $this->_model = $this->_objectManager->create(\Magento\Framework\App\FrontController::class); + $this->_objectManager = Bootstrap::getObjectManager(); + $this->_model = $this->_objectManager->create( + FrontController::class, + ['requestValidator' => $this->createRequestValidator()] + ); } + /** + * Test dispatching an empty action. + */ public function testDispatch() { - if (!\Magento\TestFramework\Helper\Bootstrap::canTestHeaders()) { + if (!Bootstrap::canTestHeaders()) { + $this->markTestSkipped('Can\'t test dispatch process without sending headers'); + } + $this->fakeRequestValidator->valid = true; + $_SERVER['HTTP_HOST'] = 'localhost'; + $this->_objectManager->get(State::class)->setAreaCode('frontend'); + $request = $this->_objectManager->get(HttpRequest::class); + /* empty action */ + $request->setRequestUri('core/index/index'); + $this->assertInstanceOf( + ResultInterface::class, + $this->_model->dispatch($request) + ); + } + + /** + * Test request validator invalidating given request. + */ + public function testInvalidRequest() + { + if (!Bootstrap::canTestHeaders()) { $this->markTestSkipped('Can\'t test dispatch process without sending headers'); } + $this->fakeRequestValidator->valid = false; $_SERVER['HTTP_HOST'] = 'localhost'; - $this->_objectManager->get(\Magento\Framework\App\State::class)->setAreaCode('frontend'); - $request = $this->_objectManager->get(\Magento\Framework\App\Request\Http::class); + $this->_objectManager->get(State::class)->setAreaCode('frontend'); + $request = $this->_objectManager->get(HttpRequest::class); /* empty action */ $request->setRequestUri('core/index/index'); $this->assertInstanceOf( - \Magento\Framework\Controller\ResultInterface::class, + ResultInterface::class, $this->_model->dispatch($request) ); } diff --git a/dev/tests/integration/testsuite/Magento/Framework/App/ObjectManager/ConfigWriter/FilesystemTest.php b/dev/tests/integration/testsuite/Magento/Framework/App/ObjectManager/ConfigWriter/FilesystemTest.php new file mode 100644 index 000000000000..843be22c57d6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/App/ObjectManager/ConfigWriter/FilesystemTest.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\App\ObjectManager\ConfigWriter; + +class FilesystemTest extends \PHPUnit\Framework\TestCase +{ + const CACHE_KEY = 'filesystemtest'; + + /** + * @var \Magento\Framework\App\ObjectManager\ConfigWriter\Filesystem + */ + private $configWriter; + + /** + * @var \Magento\Framework\App\ObjectManager\ConfigLoader + */ + private $configReader; + + protected function setUp() + { + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->configWriter = $objectManager->create( + \Magento\Framework\App\ObjectManager\ConfigWriter\Filesystem::class + ); + $this->configReader = $objectManager->create( + \Magento\Framework\App\ObjectManager\ConfigLoader\Compiled::class + ); + } + + public function testWrite() + { + $sampleData = [ + 'classA' => true, + 'classB' => false, + ]; + + $this->configWriter->write(self::CACHE_KEY, $sampleData); + $this->assertEquals($sampleData, $this->configReader->load(self::CACHE_KEY)); + } + + public function testOverwrite() + { + $this->configWriter->write(self::CACHE_KEY, ['hello' => 'world']); + + $sampleData = [ + 'classC' => false, + 'classD' => true, + ]; + + $this->configWriter->write(self::CACHE_KEY, $sampleData); + $this->assertEquals($sampleData, $this->configReader->load(self::CACHE_KEY)); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/App/Request/HttpMethodValidatorTest.php b/dev/tests/integration/testsuite/Magento/Framework/App/Request/HttpMethodValidatorTest.php new file mode 100644 index 000000000000..74aa5b8bfed0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/App/Request/HttpMethodValidatorTest.php @@ -0,0 +1,110 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\App\Request; + +use Magento\Framework\App\ActionInterface; +use Magento\Framework\App\RequestInterface; +use PHPUnit\Framework\TestCase; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Request\Http as HttpRequest; + +class HttpMethodValidatorTest extends TestCase +{ + /** + * @var HttpMethodValidator + */ + private $validator; + + /** + * @var HttpRequest + */ + private $request; + + /** + * @var HttpMethodMap + */ + private $map; + + /** + * @inheritDoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->validator = $objectManager->get(HttpMethodValidator::class); + $this->request = $objectManager->get(RequestInterface::class); + if (!$this->request instanceof HttpRequest) { + throw new \RuntimeException('We need HTTP request'); + } + $this->map = $objectManager->get(HttpMethodMap::class); + } + + /** + * @return array + */ + private function getMap(): array + { + $map = $this->map->getMap(); + if (count($map) < 2) { + throw new \RuntimeException( + 'We need at least 2 HTTP methods allowed' + ); + } + + $sorted = []; + foreach ($map as $method => $interface) { + $sorted[] = ['method' => $method, 'interface' => $interface]; + } + + return $sorted; + } + + /** + * Test positive case. + * + * @throws InvalidRequestException + */ + public function testAllowed() + { + $map = $this->getMap(); + + $action1 = $this->getMockForAbstractClass($map[0]['interface']); + $this->request->setMethod($map[0]['method']); + $this->validator->validate($this->request, $action1); + + $action2 = $this->getMockForAbstractClass(ActionInterface::class); + $this->validator->validate($this->request, $action2); + } + + /** + * Test negative case. + * + * @expectedException \Magento\Framework\App\Request\InvalidRequestException + */ + public function testNotAllowedMethod() + { + $this->request->setMethod('method' .rand(0, 1000)); + $action = $this->getMockForAbstractClass(ActionInterface::class); + + $this->validator->validate($this->request, $action); + } + + /** + * @expectedException \Magento\Framework\App\Request\InvalidRequestException + */ + public function testRestrictedMethod() + { + $map = $this->getMap(); + + $this->request->setMethod($map[1]['method']); + $action = $this->getMockForAbstractClass($map[0]['interface']); + + $this->validator->validate($this->request, $action); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Backup/DbTest.php b/dev/tests/integration/testsuite/Magento/Framework/Backup/DbTest.php index 9d149eceb454..3ee68b1012c4 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Backup/DbTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Backup/DbTest.php @@ -34,6 +34,7 @@ public static function setUpBeforeClass() /** * Test db backup includes triggers. * + * @magentoConfigFixture default/system/backup/functionality_enabled 1 * @magentoDataFixture Magento/Framework/Backup/_files/trigger.php * @magentoDbIsolation disabled */ diff --git a/dev/tests/integration/testsuite/Magento/Framework/Code/File/Validator/NotProtectedExtensionTest.php b/dev/tests/integration/testsuite/Magento/Framework/Code/File/Validator/NotProtectedExtensionTest.php new file mode 100644 index 000000000000..622f17cbf9ce --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Code/File/Validator/NotProtectedExtensionTest.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Code\File\Validator; + +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Class NotProtectedExtension + */ +class NotProtectedExtensionTest extends \PHPUnit\Framework\TestCase +{ + /** + * Test that phpt, pht is invalid extension type + * @dataProvider isValidDataProvider + */ + public function testIsValid($extension) + { + $objectManager = Bootstrap::getObjectManager(); + /** @var \Magento\MediaStorage\Model\File\Validator\NotProtectedExtension $model */ + $model = $objectManager->create(\Magento\MediaStorage\Model\File\Validator\NotProtectedExtension::class); + $this->assertFalse($model->isValid($extension)); + } + + public function isValidDataProvider() + { + return [ + ['phpt'], + ['pht'] + ]; + } +} 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 000000000000..0e1b51b3ae27 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Code/Generator/AutoloaderTest.php @@ -0,0 +1,85 @@ +<?php declare(strict_types=1); +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Framework\Code\Generator; + +use Magento\Framework\Code\Generator; +use Magento\Framework\Logger\Monolog as MagentoMonologLogger; +use Magento\TestFramework\ObjectManager; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +use Psr\Log\LoggerInterface; + +class AutoloaderTest extends TestCase +{ + /** + * This method exists to fix the wrong return type hint on \Magento\Framework\App\ObjectManager::getInstance. + * This way the IDE knows it's dealing with an instance of \Magento\TestFramework\ObjectManager and + * not \Magento\Framework\App\ObjectManager. The former has the method addSharedInstance, the latter does not. + * + * @return ObjectManager|\Magento\Framework\App\ObjectManager + * @SuppressWarnings(PHPMD.StaticAccess) + */ + private function getTestFrameworkObjectManager() + { + return ObjectManager::getInstance(); + } + + /** + * @before + */ + public function setupLoggerTestDouble(): void + { + $loggerTestDouble = $this->createMock(LoggerInterface::class); + $this->getTestFrameworkObjectManager()->addSharedInstance($loggerTestDouble, MagentoMonologLogger::class); + } + + /** + * @after + */ + public function removeLoggerTestDouble(): void + { + $this->getTestFrameworkObjectManager()->removeSharedInstance(MagentoMonologLogger::class); + } + + /** + * @param \RuntimeException $testException + * @return Generator|MockObject + */ + private function createExceptionThrowingGeneratorTestDouble(\RuntimeException $testException) + { + /** @var Generator|MockObject $generatorStub */ + $generatorStub = $this->createMock(Generator::class); + $generatorStub->method('generateClass')->willThrowException($testException); + + return $generatorStub; + } + + public function testLogsExceptionDuringGeneration(): void + { + $exceptionMessage = 'Test exception thrown during generation'; + $testException = new \RuntimeException($exceptionMessage); + + $loggerMock = ObjectManager::getInstance()->get(LoggerInterface::class); + $loggerMock->expects($this->once())->method('debug')->with($exceptionMessage, ['exception' => $testException]); + + $autoloader = new Autoloader($this->createExceptionThrowingGeneratorTestDouble($testException)); + $this->assertNull($autoloader->load(NonExistingClassName::class)); + } + + public function testFiltersDuplicateExceptionMessages(): void + { + $exceptionMessage = 'Test exception thrown during generation'; + $testException = new \RuntimeException($exceptionMessage); + + $loggerMock = ObjectManager::getInstance()->get(LoggerInterface::class); + $loggerMock->expects($this->once())->method('debug')->with($exceptionMessage, ['exception' => $testException]); + + $autoloader = new Autoloader($this->createExceptionThrowingGeneratorTestDouble($testException)); + $autoloader->load(OneNonExistingClassName::class); + $autoloader->load(AnotherNonExistingClassName::class); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest/ParentClassWithNamespace.php b/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest/ParentClassWithNamespace.php index e42a79aa5259..01e63126e5cd 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest/ParentClassWithNamespace.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest/ParentClassWithNamespace.php @@ -7,6 +7,9 @@ use Zend\Code\Generator\DocBlockGenerator; +/** + * phpcs:ignoreFile + */ class ParentClassWithNamespace { /** @@ -78,9 +81,6 @@ public static function publicParentStatic() { } - /** - * @SuppressWarnings(PHPMD.FinalImplementation) Suppressed as is a fixture but not a real code - */ final public function publicParentFinal() { } diff --git a/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest/SourceClassWithNamespace.php b/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest/SourceClassWithNamespace.php index 62a91b913fbf..b9fc351ff64e 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest/SourceClassWithNamespace.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest/SourceClassWithNamespace.php @@ -7,6 +7,9 @@ use Zend\Code\Generator\ClassGenerator; +/** + * phpcs:ignoreFile + */ class SourceClassWithNamespace extends ParentClassWithNamespace { /** @@ -96,22 +99,30 @@ private function _privateChildMethod( ) { } + /** + * Test method + */ public function publicChildWithoutParameters() { } + /** + * Test method + */ public static function publicChildStatic() { } /** - * @SuppressWarnings(PHPMD.FinalImplementation) Suppressed as is a fixture but not a real code + * Test method */ final public function publicChildFinal() { } /** + * Test method + * * @param mixed $arg1 * @param string $arg2 * @param int|null $arg3 @@ -130,6 +141,8 @@ public function public71( } /** + * Test method + * * @param \DateTime|null $arg1 * @param mixed $arg2 * @@ -140,4 +153,16 @@ public function public71( public function public71Another(?\DateTime $arg1, $arg2 = false): ?string { } + + /** + * Test method + * + * @param bool $arg + * @return SourceClassWithNamespace + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function publicWithSelf($arg = false): self + { + } } diff --git a/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNamespaceInterceptor.php.sample b/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNamespaceInterceptor.php.sample index 2a240bb685f1..83191f2d1b09 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNamespaceInterceptor.php.sample +++ b/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNamespaceInterceptor.php.sample @@ -80,6 +80,19 @@ class Interceptor extends \Magento\Framework\Code\GeneratorTest\SourceClassWithN } } + /** + * {@inheritdoc} + */ + public function publicWithSelf($arg = false) : \Magento\Framework\Code\GeneratorTest\SourceClassWithNamespace + { + $pluginInfo = $this->pluginList->getNext($this->subjectType, 'publicWithSelf'); + if (!$pluginInfo) { + return parent::publicWithSelf($arg); + } else { + return $this->___callPlugins('publicWithSelf', func_get_args(), $pluginInfo); + } + } + /** * {@inheritdoc} */ diff --git a/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNamespaceProxy.php.sample b/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNamespaceProxy.php.sample index 0aa086c0ab84..359854f2d481 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNamespaceProxy.php.sample +++ b/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNamespaceProxy.php.sample @@ -130,6 +130,14 @@ class Proxy extends \Magento\Framework\Code\GeneratorTest\SourceClassWithNamespa return $this->_getSubject()->public71Another($arg1, $arg2); } + /** + * {@inheritdoc} + */ + public function publicWithSelf($arg = false) : \Magento\Framework\Code\GeneratorTest\SourceClassWithNamespace + { + return $this->_getSubject()->publicWithSelf($arg); + } + /** * {@inheritdoc} */ diff --git a/dev/tests/integration/testsuite/Magento/Framework/Composer/RemoveTest.php b/dev/tests/integration/testsuite/Magento/Framework/Composer/RemoveTest.php index bfe8264d7cce..211b28617cbe 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Composer/RemoveTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Composer/RemoveTest.php @@ -24,7 +24,7 @@ public function testRemove() [ 'command' => 'remove', 'packages' => ['magento/package-a', 'magento/package-b'], - '--no-update' => true, + '--no-update-with-dependencies' => true, ] ); $composerAppFactory->expects($this->once()) diff --git a/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/Pdo/MysqlTest.php b/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/Pdo/MysqlTest.php index cf3b9f05cbe0..403c45dde71a 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/Pdo/MysqlTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/Pdo/MysqlTest.php @@ -8,6 +8,7 @@ use Magento\Framework\App\ResourceConnection; use Magento\TestFramework\Helper\CacheCleaner; use Magento\Framework\DB\Ddl\Table; +use Magento\TestFramework\Helper\Bootstrap; class MysqlTest extends \PHPUnit\Framework\TestCase { @@ -19,7 +20,7 @@ class MysqlTest extends \PHPUnit\Framework\TestCase protected function setUp() { set_error_handler(null); - $this->resourceConnection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + $this->resourceConnection = Bootstrap::getObjectManager() ->get(ResourceConnection::class); CacheCleaner::cleanAll(); } @@ -40,7 +41,6 @@ public function testWaitTimeout() $this->markTestSkipped('This test is for \Magento\Framework\DB\Adapter\Pdo\Mysql'); } try { - $defaultWaitTimeout = $this->getWaitTimeout(); $minWaitTimeout = 1; $this->setWaitTimeout($minWaitTimeout); $this->assertEquals($minWaitTimeout, $this->getWaitTimeout(), 'Wait timeout was not changed'); @@ -49,17 +49,8 @@ public function testWaitTimeout() sleep($minWaitTimeout + 1); $result = $this->executeQuery('SELECT 1'); $this->assertInstanceOf(\Magento\Framework\DB\Statement\Pdo\Mysql::class, $result); - // Restore wait_timeout - $this->setWaitTimeout($defaultWaitTimeout); - $this->assertEquals( - $defaultWaitTimeout, - $this->getWaitTimeout(), - 'Default wait timeout was not restored' - ); - } catch (\Exception $e) { - // Reset connection on failure to restore global variables + } finally { $this->getDbAdapter()->closeConnection(); - throw $e; } } @@ -87,30 +78,14 @@ private function setWaitTimeout($waitTimeout) /** * Execute SQL query and return result statement instance * - * @param string $sql - * @return \Zend_Db_Statement_Interface - * @throws \Exception + * @param $sql + * @return void|\Zend_Db_Statement_Pdo + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Zend_Db_Adapter_Exception */ private function executeQuery($sql) { - /** - * Suppress PDO warnings to work around the bug https://bugs.php.net/bug.php?id=63812 - */ - $phpErrorReporting = error_reporting(); - /** @var $pdoConnection \PDO */ - $pdoConnection = $this->getDbAdapter()->getConnection(); - $pdoWarningsEnabled = $pdoConnection->getAttribute(\PDO::ATTR_ERRMODE) & \PDO::ERRMODE_WARNING; - if (!$pdoWarningsEnabled) { - error_reporting($phpErrorReporting & ~E_WARNING); - } - try { - $result = $this->getDbAdapter()->query($sql); - error_reporting($phpErrorReporting); - } catch (\Exception $e) { - error_reporting($phpErrorReporting); - throw $e; - } - return $result; + return $this->getDbAdapter()->query($sql); } /** diff --git a/dev/tests/integration/testsuite/Magento/Framework/Data/Form/Element/DateTest.php b/dev/tests/integration/testsuite/Magento/Framework/Data/Form/Element/DateTest.php index a934372bfd90..9980f40239f8 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Data/Form/Element/DateTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Data/Form/Element/DateTest.php @@ -4,41 +4,51 @@ * See COPYING.txt for license details. */ +namespace Magento\Framework\Data\Form\Element; + +use Magento\Framework\Data\Form\ElementFactory; +use Magento\TestFramework\Helper\Bootstrap; + /** * Tests for \Magento\Framework\Data\Form\Element\Date */ -namespace Magento\Framework\Data\Form\Element; - class DateTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Framework\Data\Form\ElementFactory + * @var ElementFactory */ - protected $_elementFactory; + private $elementFactory; /** - * SetUp method + * @inheritdoc */ protected function setUp() { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $this->_elementFactory = $objectManager->create(\Magento\Framework\Data\Form\ElementFactory::class); + $objectManager = Bootstrap::getObjectManager(); + $this->elementFactory = $objectManager->create(ElementFactory::class); } /** + * Test get value + * + * @param array $data + * @param string $expect + * @return void * @dataProvider getValueDataProvider */ - public function testGetValue(array $data, $expect) + public function testGetValue(array $data, string $expect): void { - /** @var $date \Magento\Framework\Data\Form\Element\Date */ - $date = $this->_elementFactory->create(\Magento\Framework\Data\Form\Element\Date::class, $data); + /** @var $date Date */ + $date = $this->elementFactory->create(Date::class, $data); $this->assertEquals($expect, $date->getValue()); } /** + * Get value test data provider + * * @return array */ - public function getValueDataProvider() + public function getValueDataProvider(): array { $testTimestamp = strtotime('2014-05-18 12:08:16'); $date = new \DateTime('@' . $testTimestamp); @@ -56,15 +66,22 @@ public function getValueDataProvider() 'time_format' => 'h:mm a', 'value' => $testTimestamp, ], - $date->format('g:i A') + $date->format('g:i A'), ], [ [ 'date_format' => 'MM/d/yy', 'value' => $testTimestamp, ], - $date->format('m/j/y') - ] + $date->format('m/j/y'), + ], + [ + [ + 'date_format' => 'd-MM-Y', + 'value' => $date->format('d-m-Y'), + ], + $date->format('d-m-Y'), + ], ]; } } diff --git a/dev/tests/integration/testsuite/Magento/Framework/Encryption/EncryptorTest.php b/dev/tests/integration/testsuite/Magento/Framework/Encryption/EncryptorTest.php index 2ba9109df86e..88c567da7529 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Encryption/EncryptorTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Encryption/EncryptorTest.php @@ -10,18 +10,64 @@ class EncryptorTest extends \PHPUnit\Framework\TestCase /** * @var \Magento\Framework\Encryption\Encryptor */ - protected $_model; + private $encryptor; protected function setUp() { - $this->_model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + $this->encryptor = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( \Magento\Framework\Encryption\Encryptor::class ); } public function testEncryptDecrypt() { - $this->assertEquals('', $this->_model->decrypt($this->_model->encrypt(''))); - $this->assertEquals('test', $this->_model->decrypt($this->_model->encrypt('test'))); + $this->assertEquals('', $this->encryptor->decrypt($this->encryptor->encrypt(''))); + $this->assertEquals('test', $this->encryptor->decrypt($this->encryptor->encrypt('test'))); + } + + /** + * @param string $key + * @dataProvider validEncryptionKeyDataProvider + */ + public function testValidateKey($key) + { + $this->encryptor->validateKey($key); + } + + public function validEncryptionKeyDataProvider() + { + return [ + '32 numbers' => ['12345678901234567890123456789012'], + '32 characters' => ['aBcdeFghIJKLMNOPQRSTUvwxYzabcdef'], + '32 special characters' => ['!@#$%^&*()_+~`:;"<>,.?/|*&^%$#@!'], + '32 combination' =>['1234eFghI1234567^&*(890123456789'], + ]; + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Encryption key must be 32 character string without any white space. + * + * @param string $key + * @dataProvider invalidEncryptionKeyDataProvider + */ + public function testValidateKeyInvalid($key) + { + $this->encryptor->validateKey($key); + } + + public function invalidEncryptionKeyDataProvider() + { + return [ + 'empty string' => [''], + 'leading space' => [' 1234567890123456789012345678901'], + 'tailing space' => ['1234567890123456789012345678901 '], + 'space in the middle' => ['12345678901 23456789012345678901'], + 'tab in the middle' => ['12345678901 23456789012345678'], + 'return in the middle' => ['12345678901 + 23456789012345678901'], + '31 characters' => ['1234567890123456789012345678901'], + '33 characters' => ['123456789012345678901234567890123'], + ]; } } diff --git a/dev/tests/integration/testsuite/Magento/Framework/Filesystem/Driver/FileTest.php b/dev/tests/integration/testsuite/Magento/Framework/Filesystem/Driver/FileTest.php index 26401c782efc..5f53e6216550 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Filesystem/Driver/FileTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Filesystem/Driver/FileTest.php @@ -7,22 +7,27 @@ */ namespace Magento\Framework\Filesystem\Driver; -use Magento\Framework\Filesystem\DriverInterface; +use Magento\Framework\Exception\FileSystemException; class FileTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Framework\Filesystem\Driver\File + * @var File */ - protected $driver; + private $driver; /** - * @var string + * @var String */ - protected $absolutePath; + private $absolutePath; /** - * get relative path for test + * @var String + */ + private $generatedPath; + + /** + * Returns relative path for the test. * * @param $relativePath * @return string @@ -33,16 +38,26 @@ protected function getTestPath($relativePath) } /** - * Set up + * @inheritdoc */ public function setUp() { - $this->driver = new \Magento\Framework\Filesystem\Driver\File(); + $this->driver = new File(); $this->absolutePath = dirname(__DIR__) . '/_files/'; + $this->generatedPath = $this->getTestPath('generated'); + $this->removeGeneratedDirectory(); + } + + /** + * @inheritdoc + */ + protected function tearDown() + { + $this->removeGeneratedDirectory(); } /** - * test read recursively read + * Tests directory recursive read. */ public function testReadDirectoryRecursively() { @@ -60,7 +75,7 @@ public function testReadDirectoryRecursively() } /** - * test exception + * Tests directory reading exception. * * @expectedException \Magento\Framework\Exception\FileSystemException */ @@ -69,6 +84,11 @@ public function testReadDirectoryRecursivelyFailure() $this->driver->readDirectoryRecursively($this->getTestPath('not-existing-directory')); } + /** + * Tests of directory creating. + * + * @throws FileSystemException + */ public function testCreateDirectory() { $generatedPath = $this->getTestPath('generated/roo/bar/baz/foo'); @@ -80,4 +100,39 @@ public function testCreateDirectory() $this->assertTrue($this->driver->createDirectory($generatedPath)); $this->assertTrue(is_dir($generatedPath)); } + + /** + * Tests creation and removing of symlinks. + * + * @throws FileSystemException + * @return void + */ + public function testSymlinks(): void + { + $sourceDirectory = $this->generatedPath . '/source'; + $destinationDirectory = $this->generatedPath . '/destination'; + + $this->driver->createDirectory($sourceDirectory); + $this->driver->createDirectory($destinationDirectory); + + $linkName = $destinationDirectory . '/link'; + + self::assertTrue($this->driver->isWritable($destinationDirectory)); + self::assertTrue($this->driver->symlink($sourceDirectory, $linkName)); + self::assertTrue($this->driver->isExists($linkName)); + self::assertTrue($this->driver->deleteDirectory($linkName)); + } + + /** + * Remove generated directories. + * + * @throws FileSystemException + * @return void + */ + private function removeGeneratedDirectory(): void + { + if (is_dir($this->generatedPath)) { + $this->driver->deleteDirectory($this->generatedPath); + } + } } diff --git a/dev/tests/integration/testsuite/Magento/Framework/GraphQl/Config/GraphQlReaderTest.php b/dev/tests/integration/testsuite/Magento/Framework/GraphQl/Config/GraphQlReaderTest.php index 8583dcf3e4cd..af8d1c7af134 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/GraphQl/Config/GraphQlReaderTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/GraphQl/Config/GraphQlReaderTest.php @@ -8,6 +8,7 @@ namespace Magento\Framework\GraphQl\Config; use Magento\Framework\App\Cache; +use Magento\Framework\App\Request\Http; use Magento\Framework\GraphQl\Config; use Magento\Framework\GraphQl\Schema\SchemaGenerator; use Magento\Framework\ObjectManagerInterface; @@ -34,6 +35,9 @@ class GraphQlReaderTest extends \PHPUnit\Framework\TestCase /** @var SerializerInterface */ private $jsonSerializer; + /** + * @inheritdoc + */ protected function setUp() { $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); @@ -175,12 +179,14 @@ enumValues(includeDeprecated: true) { 'operationName' => 'IntrospectionQuery' ]; /** @var Http $request */ - $request = $this->objectManager->get(\Magento\Framework\App\Request\Http::class); + $request = $this->objectManager->get(Http::class); $request->setPathInfo('/graphql'); + $request->setMethod('POST'); $request->setContent(json_encode($postData)); $headers = $this->objectManager->create(\Zend\Http\Headers::class) ->addHeaders(['Content-Type' => 'application/json']); $request->setHeaders($headers); + $response = $this->graphQlController->dispatch($request); $output = $this->jsonSerializer->unserialize($response->getContent()); $expectedOutput = require __DIR__ . '/../_files/schema_response_sdl_description.php'; @@ -191,6 +197,16 @@ enumValues(includeDeprecated: true) { $mergedSchemaResponseFields = array_merge($schemaResponseFieldsFirstHalf, $schemaResponseFieldsSecondHalf); foreach ($expectedOutput as $searchTerm) { + $sortFields = ['inputFields', 'fields']; + foreach ($sortFields as $sortField) { + isset($searchTerm[$sortField]) && is_array($searchTerm[$sortField]) + ? usort($searchTerm[$sortField], function ($a, $b) { + $cmpField = 'name'; + return isset($a[$cmpField]) && isset($b[$cmpField]) + ? strcmp($a[$cmpField], $b[$cmpField]) : 0; + }) : null; + } + $this->assertTrue( (in_array($searchTerm, $mergedSchemaResponseFields)), 'Missing type in the response' diff --git a/dev/tests/integration/testsuite/Magento/Framework/GraphQl/GraphQlConfigTest.php b/dev/tests/integration/testsuite/Magento/Framework/GraphQl/GraphQlConfigTest.php index ef4612ea357e..5668c1bef668 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/GraphQl/GraphQlConfigTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/GraphQl/GraphQlConfigTest.php @@ -7,11 +7,8 @@ namespace Magento\Framework\GraphQl; -use Magento\Framework\App\Bootstrap; use Magento\Framework\App\Cache; -use Magento\Framework\Config\FileResolverInterface; use Magento\Framework\GraphQl\Config\Config; -use Magento\Framework\GraphQl\Config\ConfigElementInterface; use Magento\Framework\GraphQl\Config\Data\Argument; use Magento\Framework\GraphQl\Config\Data\Enum; use Magento\Framework\GraphQl\Config\Data\Field; @@ -19,9 +16,11 @@ use Magento\Framework\GraphQl\Config\Data\Type; use Magento\Framework\GraphQl\Config\Element\EnumValue; use Magento\Framework\GraphQl\Config\Element\InterfaceType; -use Magento\Framework\GraphQl\Config\Element\TypeFactory; use Magento\Framework\ObjectManagerInterface; +/** + * Test of schema configuration reading and parsing + */ class GraphQlConfigTest extends \PHPUnit\Framework\TestCase { /** @var \Magento\Framework\GraphQl\Config */ @@ -76,7 +75,12 @@ public function testGraphQlTypeAndFieldConfigStructure() ['response_field' => 'required', 'expected_value' => $queryFields[$fieldKey]->isRequired()], ['response_field' => 'isList', 'expected_value' => $queryFields[$fieldKey]->isList()], ['response_field' => 'resolver', 'expected_value' => $queryFields[$fieldKey]->getResolver()], - ['response_field' => 'description', 'expected_value' => $queryFields[$fieldKey]->getDescription()] + ['response_field' => 'description', 'expected_value' => $queryFields[$fieldKey]->getDescription()], + [ + 'response_field' => 'cache', + 'expected_value' => $queryFields[$fieldKey]->getCache(), + 'optional' => true + ] ]; $this->assertResponseFields($expectedOutputArray['Query']['fields'][$fieldKey], $fieldAssertionMap); /** @var \Magento\Framework\GraphQl\Config\Element\Argument $queryFieldArguments */ @@ -212,12 +216,15 @@ private function assertResponseFields($actualResponse, $assertionMap) $expectedValue, "Value of '{$responseField}' field must not be NULL" ); - $this->assertEquals( - $expectedValue, - $actualResponse[$responseField], - "Value of '{$responseField}' field in response does not match expected value: " - . var_export($expectedValue, true) - ); + $optionalField = isset($assertionData['optional']) ? $assertionData['optional'] : false; + if (!$optionalField || isset($actualResponse[$responseField])) { + $this->assertEquals( + $expectedValue, + $actualResponse[$responseField], + "Value of '{$responseField}' field in response does not match expected value: " + . var_export($expectedValue, true) + ); + } } } diff --git a/dev/tests/integration/testsuite/Magento/Framework/GraphQl/_files/query_array_output.php b/dev/tests/integration/testsuite/Magento/Framework/GraphQl/_files/query_array_output.php index 9ebd0160240a..c37632fe3e21 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/GraphQl/_files/query_array_output.php +++ b/dev/tests/integration/testsuite/Magento/Framework/GraphQl/_files/query_array_output.php @@ -31,8 +31,13 @@ ], 'required' => false, 'isList' => false, - 'resolver' => 'Magento\\EavGraphQl\\Model\\Resolver\\CustomAttributeMetadata', - 'description' => 'Returns the attribute type, given an attribute code and entity type' + 'resolver' => Magento\EavGraphQl\Model\Resolver\CustomAttributeMetadata::class, + 'description' => 'Returns the attribute type, given an attribute code and entity type', + 'cache' => [ + 'cacheTag' => 'cat_test', + 'cacheIdentity' => + Magento\EavGraphQl\Model\Resolver\CustomAttributeMetadata::class + ] ], 'products' => [ 'name' => 'products', @@ -95,7 +100,7 @@ ], 'required' => false, 'isList' => false, - 'resolver' => 'Magento\\CatalogGraphQl\\Model\\Resolver\\Products', + 'resolver' => Magento\CatalogGraphQl\Model\Resolver\Products::class, 'description' => 'comment for products fields' ] ] @@ -274,7 +279,7 @@ ] ], - 'typeResolver' => 'Magento\\CatalogGraphQl\\Model\\ProductLinkTypeResolverComposite', + 'typeResolver' => Magento\CatalogGraphQl\Model\ProductLinkTypeResolverComposite::class, 'description' => 'description for ProductLinksInterface' ] ]; diff --git a/dev/tests/integration/testsuite/Magento/Framework/GraphQl/_files/schemaC.graphqls b/dev/tests/integration/testsuite/Magento/Framework/GraphQl/_files/schemaC.graphqls index 84609293f31a..92682468c42b 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/GraphQl/_files/schemaC.graphqls +++ b/dev/tests/integration/testsuite/Magento/Framework/GraphQl/_files/schemaC.graphqls @@ -1,6 +1,6 @@ type Query { customAttributeMetadata(attributes: [AttributeInput!]!): CustomAttributeMetadata @resolver(class: "Magento\\EavGraphQl\\Model\\Resolver\\CustomAttributeMetadata") - @doc(description: "Returns the attribute type, given an attribute code and entity type") + @doc(description: "Returns the attribute type, given an attribute code and entity type") @cache(cacheTag: "cat_test", cacheIdentity: "Magento\\EavGraphQl\\Model\\Resolver\\CustomAttributeMetadata") } type CustomAttributeMetadata { diff --git a/dev/tests/integration/testsuite/Magento/Framework/Interception/AbstractPlugin.php b/dev/tests/integration/testsuite/Magento/Framework/Interception/AbstractPlugin.php index a85e5e7c8948..281a038a5a9a 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Interception/AbstractPlugin.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Interception/AbstractPlugin.php @@ -7,25 +7,35 @@ /** * Class GeneralTest + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ abstract class AbstractPlugin extends \PHPUnit\Framework\TestCase { /** + * Config reader + * * @var \PHPUnit_Framework_MockObject_MockObject */ protected $_configReader; /** + * Object Manager + * * @var \Magento\Framework\ObjectManagerInterface */ protected $_objectManager; /** + * Applicartion Object Manager + * * @var \Magento\Framework\ObjectManagerInterface */ private $applicationObjectManager; + /** + * Set up + */ public function setUp() { if (!$this->_objectManager) { @@ -36,11 +46,19 @@ public function setUp() \Magento\Framework\App\ObjectManager::setInstance($this->_objectManager); } + /** + * Tear down + */ public function tearDown() { \Magento\Framework\App\ObjectManager::setInstance($this->applicationObjectManager); } + /** + * Set up Interception Config + * + * @param array $pluginConfig + */ public function setUpInterceptionConfig($pluginConfig) { $config = new \Magento\Framework\Interception\ObjectManager\Config\Developer(); @@ -59,7 +77,8 @@ public function setUpInterceptionConfig($pluginConfig) $areaList->expects($this->any())->method('getCodes')->will($this->returnValue([])); $configScope = new \Magento\Framework\Config\Scope($areaList, 'global'); $cache = $this->createMock(\Magento\Framework\Config\CacheInterface::class); - $cache->expects($this->any())->method('load')->will($this->returnValue(false)); + $cacheManager = $this->createMock(\Magento\Framework\Interception\Config\CacheManager::class); + $cacheManager->method('load')->willReturn(null); $definitions = new \Magento\Framework\ObjectManager\Definition\Runtime(); $relations = new \Magento\Framework\ObjectManager\Relations\Runtime(); $interceptionConfig = new Config\Config( @@ -68,7 +87,10 @@ public function setUpInterceptionConfig($pluginConfig) $cache, $relations, $config, - $definitions + $definitions, + 'interception', + null, + $cacheManager ); $interceptionDefinitions = new Definition\Runtime(); $json = new \Magento\Framework\Serialize\Serializer\Json(); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Interception/Config/CacheManagerTest.php b/dev/tests/integration/testsuite/Magento/Framework/Interception/Config/CacheManagerTest.php new file mode 100644 index 000000000000..75737f014727 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Interception/Config/CacheManagerTest.php @@ -0,0 +1,135 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Interception\Config; + +use Magento\Framework\App\Filesystem\DirectoryList; + +class CacheManagerTest extends \PHPUnit\Framework\TestCase +{ + const CACHE_ID = 'interceptiontest'; + + /** + * @var \Magento\Framework\ObjectManagerInterface + */ + private $objectManager; + + /** + * @var \Magento\Framework\Serialize\SerializerInterface + */ + private $serializer; + + /** + * @var \Magento\Framework\Cache\FrontendInterface + */ + private $cache; + + /** + * @var \Magento\Framework\App\ObjectManager\ConfigWriterInterface + */ + private $configWriter; + + protected function setUp() + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + + $this->serializer = $this->objectManager->get(\Magento\Framework\Serialize\SerializerInterface::class); + $this->cache = $this->objectManager->get(\Magento\Framework\App\CacheInterface::class); + $this->configWriter = + $this->objectManager->get(\Magento\Framework\App\ObjectManager\ConfigWriter\Filesystem::class); + + $this->initializeMetadataDirectory(); + } + + /** + * Delete compiled file if it was created and clear cache data + */ + protected function tearDown() + { + $compiledPath = \Magento\Framework\App\ObjectManager\ConfigLoader\Compiled::getFilePath(self::CACHE_ID); + if (file_exists($compiledPath)) { + unlink($compiledPath); + } + + $this->cache->remove(self::CACHE_ID); + } + + /** + * Test load interception cache from generated/metadata + * @dataProvider interceptionCompiledConfigDataProvider + * @param array $testConfig + */ + public function testInstantiateFromCompiled(array $testConfig) + { + $this->configWriter->write(self::CACHE_ID, $testConfig); + $config = $this->getConfig(); + + $this->assertEquals($testConfig, $config->load(self::CACHE_ID)); + } + + /** + * Test load interception cache from backend cache + * @dataProvider interceptionCacheConfigDataProvider + * @param array $testConfig + */ + public function testInstantiateFromCache(array $testConfig) + { + $this->cache->save($this->serializer->serialize($testConfig), self::CACHE_ID); + $config = $this->getConfig(); + + $this->assertEquals($testConfig, $config->load(self::CACHE_ID)); + } + + public function interceptionCompiledConfigDataProvider() + { + return [ + [['classA' => true, 'classB' => false]], + [['classA' => false, 'classB' => true]], + ]; + } + + public function interceptionCacheConfigDataProvider() + { + return [ + [['classC' => true, 'classD' => false]], + [['classC' => false, 'classD' => true]], + ]; + } + + /** + * Ensure generated/metadata exists + */ + private function initializeMetadataDirectory() + { + $diPath = DirectoryList::getDefaultConfig()[DirectoryList::GENERATED_METADATA][DirectoryList::PATH]; + $fullPath = BP . DIRECTORY_SEPARATOR . $diPath; + if (!file_exists($fullPath)) { + mkdir($fullPath); + } + } + + /** + * Create instance of Config class with specific cacheId. This is done to prevent our test + * from altering the interception config that may have been generated during application + * installation. Inject a new instance of the compileLoaded to bypass it's caching. + * + * @return \Magento\Framework\Interception\Config\CacheManager + */ + private function getConfig() + { + return $this->objectManager->create( + \Magento\Framework\Interception\Config\CacheManager::class, + [ + 'cacheId' => self::CACHE_ID, + 'compiledLoader' => $this->objectManager->create( + \Magento\Framework\App\ObjectManager\ConfigLoader\Compiled::class + ), + ] + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Interception/Fixture/Intercepted.php b/dev/tests/integration/testsuite/Magento/Framework/Interception/Fixture/Intercepted.php index 0a75aba5c9c4..a35596cf0f90 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Interception/Fixture/Intercepted.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Interception/Fixture/Intercepted.php @@ -8,7 +8,7 @@ namespace Magento\Framework\Interception\Fixture; /** - * @codingStandardsIgnoreStart + * phpcs:ignoreFile */ class Intercepted extends InterceptedParent implements InterceptedInterface { @@ -49,7 +49,6 @@ public function D($param1) /** * @SuppressWarnings(PHPMD.ShortMethodName) - * @SuppressWarnings(PHPMD.FinalImplementation) Suppressed as is a fixture but not a real code */ final public function E($param1) { diff --git a/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/DatabaseTest.php b/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/DatabaseTest.php new file mode 100644 index 000000000000..f2a77db40bad --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/DatabaseTest.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Lock\Backend; + +/** + * \Magento\Framework\Lock\Backend\Database test case + */ +class DatabaseTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\Lock\Backend\Database + */ + private $model; + + /** + * @var \Magento\Framework\ObjectManagerInterface + */ + private $objectManager; + + protected function setUp() + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->model = $this->objectManager->create(\Magento\Framework\Lock\Backend\Database::class); + } + + public function testLockAndUnlock() + { + $name = 'test_lock'; + + $this->assertFalse($this->model->isLocked($name)); + + $this->assertTrue($this->model->lock($name)); + $this->assertTrue($this->model->isLocked($name)); + + $this->assertTrue($this->model->unlock($name)); + $this->assertFalse($this->model->isLocked($name)); + } + + public function testUnlockWithoutExistingLock() + { + $name = 'test_lock'; + + $this->assertFalse($this->model->isLocked($name)); + $this->assertFalse($this->model->unlock($name)); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/FileLockTest.php b/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/FileLockTest.php new file mode 100644 index 000000000000..e64b3c505acf --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/FileLockTest.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Lock\Backend; + +/** + * \Magento\Framework\Lock\Backend\File test case + */ +class FileLockTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \Magento\Framework\Lock\Backend\FileLock + */ + private $model; + + /** + * @var \Magento\Framework\ObjectManagerInterface + */ + private $objectManager; + + protected function setUp() + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->model = $this->objectManager->create( + \Magento\Framework\Lock\Backend\FileLock::class, + ['path' => '/tmp'] + ); + } + + public function testLockAndUnlock() + { + $name = 'test_lock'; + + $this->assertFalse($this->model->isLocked($name)); + + $this->assertTrue($this->model->lock($name)); + $this->assertTrue($this->model->isLocked($name)); + $this->assertFalse($this->model->lock($name, 2)); + + $this->assertTrue($this->model->unlock($name)); + $this->assertFalse($this->model->isLocked($name)); + } + + public function testUnlockWithoutExistingLock() + { + $name = 'test_lock'; + + $this->assertFalse($this->model->isLocked($name)); + $this->assertFalse($this->model->unlock($name)); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/ZookeeperTest.php b/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/ZookeeperTest.php new file mode 100644 index 000000000000..8d0caad5d55e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/ZookeeperTest.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Lock\Backend; + +use Magento\Framework\Lock\Backend\Zookeeper as ZookeeperLock; +use Magento\Framework\Lock\LockBackendFactory; +use Magento\Framework\Config\File\ConfigFilePool; +use Magento\Framework\App\DeploymentConfig\FileReader; +use Magento\Framework\Stdlib\ArrayManager; + +/** + * \Magento\Framework\Lock\Backend\Zookeeper test case + */ +class ZookeeperTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var FileReader + */ + private $configReader; + + /** + * @var \Magento\Framework\ObjectManagerInterface + */ + private $objectManager; + + /** + * @var LockBackendFactory + */ + private $lockBackendFactory; + + /** + * @var ArrayManager + */ + private $arrayManager; + + /** + * @var ZookeeperLock + */ + private $model; + + /** + * @inheritdoc + */ + protected function setUp() + { + if (!extension_loaded('zookeeper')) { + $this->markTestSkipped('php extension Zookeeper is not installed.'); + } + + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->configReader = $this->objectManager->get(FileReader::class); + $this->lockBackendFactory = $this->objectManager->create(LockBackendFactory::class); + $this->arrayManager = $this->objectManager->create(ArrayManager::class); + $config = $this->configReader->load(ConfigFilePool::APP_ENV); + + if ($this->arrayManager->get('lock/provider', $config) !== 'zookeeper') { + $this->markTestSkipped('Zookeeper is not configured during installation.'); + } + + $this->model = $this->lockBackendFactory->create(); + $this->assertInstanceOf(ZookeeperLock::class, $this->model); + } + + public function testLockAndUnlock() + { + $name = 'test_lock'; + + $this->assertFalse($this->model->isLocked($name)); + + $this->assertTrue($this->model->lock($name)); + $this->assertTrue($this->model->isLocked($name)); + $this->assertFalse($this->model->lock($name, 2)); + + $this->assertTrue($this->model->unlock($name)); + $this->assertFalse($this->model->isLocked($name)); + } + + public function testUnlockWithoutExistingLock() + { + $name = 'test_lock'; + + $this->assertFalse($this->model->isLocked($name)); + $this->assertFalse($this->model->unlock($name)); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/TopologyTest.php b/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/TopologyTest.php index 189d189d32c9..c2521c27a0c7 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/TopologyTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/TopologyTest.php @@ -5,6 +5,9 @@ */ namespace Magento\Framework\MessageQueue; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\MessageQueue\PreconditionFailedException; + /** * @see dev/tests/integration/_files/Magento/TestModuleMessageQueueConfiguration * @see dev/tests/integration/_files/Magento/TestModuleMessageQueueConfigOverride @@ -25,7 +28,12 @@ class TopologyTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->helper = new \Magento\TestFramework\Helper\Amqp(); + $this->helper = Bootstrap::getObjectManager()->create(\Magento\TestFramework\Helper\Amqp::class); + + if (!$this->helper->isAvailable()) { + $this->fail('This test relies on RabbitMQ Management Plugin.'); + } + $this->declaredExchanges = $this->helper->getExchanges(); } @@ -39,6 +47,7 @@ public function testTopologyInstallation(array $expectedConfig, array $bindingCo $name = $expectedConfig['name']; $this->assertArrayHasKey($name, $this->declaredExchanges); unset($this->declaredExchanges[$name]['message_stats']); + unset($this->declaredExchanges[$name]['user_who_performed_action']); $this->assertEquals( $expectedConfig, $this->declaredExchanges[$name], diff --git a/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/_files/communication.xml b/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/_files/communication.xml index 0fc50f0432b9..1cc5d6cd3b71 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/_files/communication.xml +++ b/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/_files/communication.xml @@ -7,6 +7,6 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Communication/etc/communication.xsd"> <topic name="topic.broker.test" request="string" response="string"> - <handler name="topicBrokerHandler" type="Magento\MysqlMq\Model\Processor" method="processMessage"/> + <handler name="topicBrokerHandler" type="Magento\TestModuleMysqlMq\Model\Processor" method="processMessage"/> </topic> </config> diff --git a/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/_files/valid_expected_queue.php b/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/_files/valid_expected_queue.php index b5f5145c32c7..9a813b4424ea 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/_files/valid_expected_queue.php +++ b/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/_files/valid_expected_queue.php @@ -40,7 +40,7 @@ "name" => "publisher5.topic", "schema" => [ "schema_type" => "object", - "schema_value" => '\\' . \Magento\MysqlMq\Model\DataObject::class + "schema_value" => '\\' . \Magento\TestModuleMysqlMq\Model\DataObject::class ], "response_schema" => [ "schema_type" => "object", @@ -58,7 +58,7 @@ "handlers" => [ "topic.broker.test" => [ "0" => [ - "type" => \Magento\MysqlMq\Model\Processor::class, + "type" => \Magento\TestModuleMysqlMq\Model\Processor::class, "method" => "processMessage" ] ] diff --git a/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/_files/valid_queue_input.php b/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/_files/valid_queue_input.php index fdd4a7d3007a..ed6e13cfe9fa 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/_files/valid_queue_input.php +++ b/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/_files/valid_queue_input.php @@ -23,11 +23,11 @@ "name" => "publisher5.topic", "schema" => [ "schema_type" => "object", - "schema_value" => "Magento\\MysqlMq\\Model\\DataObject" + "schema_value" => \Magento\TestModuleMysqlMq\Model\DataObject::class ], "response_schema" => [ "schema_type" => "object", - "schema_value" => "Magento\\Customer\\Api\\Data\\CustomerInterface" + "schema_value" => \Magento\Customer\Api\Data\CustomerInterface::class ], "publisher" => "test-publisher-5" ] diff --git a/dev/tests/integration/testsuite/Magento/Framework/Model/ResourceModel/Db/Collection/AbstractTest.php b/dev/tests/integration/testsuite/Magento/Framework/Model/ResourceModel/Db/Collection/AbstractTest.php index 419a7d04b778..2ee0953dcdbf 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Model/ResourceModel/Db/Collection/AbstractTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Model/ResourceModel/Db/Collection/AbstractTest.php @@ -72,4 +72,42 @@ public function testGetAllIdsWithBind() $this->_model->addBindParam('code', 'admin'); $this->assertEquals(['0'], $this->_model->getAllIds()); } + + /** + * Check add field to select doesn't remove expression field from select. + * + * @return void + */ + public function testAddExpressionFieldToSelectWithAdditionalFields() + { + $expectedColumns = ['code', 'test_field']; + $actualColumns = []; + + $testExpression = new \Zend_Db_Expr('(sort_order + group_id)'); + $this->_model->addExpressionFieldToSelect('test_field', $testExpression, ['sort_order', 'group_id']); + $this->_model->addFieldToSelect('code', 'code'); + $columns = $this->_model->getSelect()->getPart(\Magento\Framework\DB\Select::COLUMNS); + foreach ($columns as $columnEntry) { + $actualColumns[] = $columnEntry[2]; + } + + $this->assertEquals($expectedColumns, $actualColumns); + } + + /** + * Check add expression field doesn't remove all fields from select. + * + * @return void + */ + public function testAddExpressionFieldToSelectWithoutAdditionalFields() + { + $expectedColumns = ['*', 'test_field']; + + $testExpression = new \Zend_Db_Expr('(sort_order + group_id)'); + $this->_model->addExpressionFieldToSelect('test_field', $testExpression, ['sort_order', 'group_id']); + $columns = $this->_model->getSelect()->getPart(\Magento\Framework\DB\Select::COLUMNS); + $actualColumns = [$columns[0][1], $columns[1][2]]; + + $this->assertEquals($expectedColumns, $actualColumns); + } } 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 b09af48b5f94..f4f3337a253c 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes.php @@ -20,6 +20,10 @@ CategorySetup::class, ['resourceName' => 'catalog_setup'] ); +$productEntityTypeId = $installer->getEntityTypeId( + \Magento\Catalog\Api\Data\ProductAttributeInterface::ENTITY_TYPE_CODE +); + $selectOptions = []; $selectAttributes = []; foreach (range(1, 2) as $index) { @@ -30,7 +34,7 @@ $selectAttribute->setData( [ 'attribute_code' => 'select_attribute_' . $index, - 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), + 'entity_type_id' => $productEntityTypeId, 'is_global' => 1, 'is_user_defined' => 1, 'frontend_input' => 'select', @@ -56,7 +60,8 @@ ); $selectAttribute->save(); /* Assign attribute to attribute set */ - $installer->addAttributeToGroup('catalog_product', 'Default', 'General', $selectAttribute->getId()); + $installer->addAttributeToGroup($productEntityTypeId, 'Default', 'General', $selectAttribute->getId()); + /** @var $selectOptions Collection */ $selectOption = Bootstrap::getObjectManager()->create( Collection::class @@ -65,6 +70,26 @@ $selectAttributes[$index] = $selectAttribute; $selectOptions[$index] = $selectOption; } + +$dateAttribute = Bootstrap::getObjectManager()->create(Attribute::class); +$dateAttribute->setData( + [ + 'attribute_code' => 'date_attribute', + 'entity_type_id' => $productEntityTypeId, + 'is_global' => 1, + 'is_filterable' => 1, + 'backend_type' => 'datetime', + 'frontend_input' => 'date', + 'frontend_label' => 'Test Date', + 'is_searchable' => 1, + 'is_filterable_in_search' => 1, + ] +); +$dateAttribute->save(); +/* Assign attribute to attribute set */ +$installer->addAttributeToGroup($productEntityTypeId, 'Default', 'General', $dateAttribute->getId()); + +$productAttributeSetId = $installer->getAttributeSetId($productEntityTypeId, 'Default'); /* Create simple products per each first attribute option */ foreach ($selectOptions[1] as $option) { /** @var $product Product */ @@ -74,7 +99,7 @@ $product->setTypeId( Type::TYPE_SIMPLE )->setAttributeSetId( - $installer->getAttributeSetId('catalog_product', 'Default') + $productAttributeSetId )->setWebsiteIds( [1] )->setName( @@ -92,6 +117,7 @@ )->setStockData( ['use_config_manage_stock' => 1, 'qty' => 5, 'is_in_stock' => 1] )->save(); + Bootstrap::getObjectManager()->get( Action::class )->updateAttributes( @@ -99,6 +125,7 @@ [ $selectAttributes[1]->getAttributeCode() => $option->getId(), $selectAttributes[2]->getAttributeCode() => $selectOptions[2]->getLastItem()->getId(), + $dateAttribute->getAttributeCode() => '10/30/2000', ], $product->getStoreId() ); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes_rollback.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes_rollback.php index 18a5372d06d9..fd413726b263 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/filterable_attributes_rollback.php @@ -13,6 +13,7 @@ $registry = Bootstrap::getObjectManager()->get(Registry::class); $registry->unregister('isSecureArea'); $registry->register('isSecureArea', true); + /** @var $productCollection \Magento\Catalog\Model\ResourceModel\Product\Collection */ $productCollection = Bootstrap::getObjectManager() ->create(Product::class) @@ -20,17 +21,26 @@ foreach ($productCollection as $product) { $product->delete(); } + /** @var $attribute Attribute */ $attribute = Bootstrap::getObjectManager()->create( Attribute::class ); /** @var $installer CategorySetup */ $installer = Bootstrap::getObjectManager()->create(CategorySetup::class); +$productEntityTypeId = $installer->getEntityTypeId( + \Magento\Catalog\Api\Data\ProductAttributeInterface::ENTITY_TYPE_CODE +); foreach (range(1, 2) as $index) { - $attribute->loadByCode($installer->getEntityTypeId('catalog_product'), 'select_attribute_' . $index); + $attribute->loadByCode($productEntityTypeId, 'select_attribute_' . $index); if ($attribute->getId()) { $attribute->delete(); } } +$attribute->loadByCode($productEntityTypeId, 'date_attribute'); +if ($attribute->getId()) { + $attribute->delete(); +} + $registry->unregister('isSecureArea'); $registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/search_request_merged.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/search_request_merged.php index 8586f47a0f7f..0aaa3f4e15bd 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/search_request_merged.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/search_request_merged.php @@ -35,6 +35,7 @@ 'match_query' => [ 'value' => '$match_term_override$', 'name' => 'match_query', + 'boost' => '1', 'match' => [ 0 => [ 'field' => 'match_field', @@ -50,6 +51,7 @@ ], 'must_query' => [ 'name' => 'must_query', + 'boost' => '1', 'filterReference' => [ 0 => [ 'clause' => 'must', @@ -60,6 +62,7 @@ ], 'should_query' => [ 'name' => 'should_query', + 'boost' => '1', 'filterReference' => [ 0 => [ 'clause' => 'should', @@ -70,6 +73,7 @@ ], 'not_query' => [ 'name' => 'not_query', + 'boost' => '1', 'filterReference' => [ 0 => [ 'clause' => 'not', @@ -80,6 +84,7 @@ ], 'match_query_2' => [ 'value' => '$match_term_override$', + 'boost' => '1', 'name' => 'match_query_2', 'match' => [ 0 => [ @@ -163,6 +168,7 @@ 'queries' => [ 'filter_query' => [ 'name' => 'filter_query', + 'boost' => '1', 'filterReference' => [ 0 => [ @@ -230,6 +236,7 @@ 'new_match_query' => [ 'value' => '$match_term$', 'name' => 'new_match_query', + 'boost' => '1', 'match' => [ 0 => [ diff --git a/dev/tests/integration/testsuite/Magento/Framework/Session/SidResolverTest.php b/dev/tests/integration/testsuite/Magento/Framework/Session/SidResolverTest.php index e92842252840..5e70eb491b50 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Session/SidResolverTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Session/SidResolverTest.php @@ -114,7 +114,7 @@ public function testGetSid($sid, $useFrontedSid, $isOwnOriginUrl, $testSid) $this->scopeConfig->expects( $this->any() )->method( - 'getValue' + 'isSetFlag' )->with( \Magento\Framework\Session\SidResolver::XML_PATH_USE_FRONTEND_SID, \Magento\Store\Model\ScopeInterface::SCOPE_STORE @@ -195,7 +195,7 @@ public function testSetGetUseSessionInUrl($configValue) $this->scopeConfig->expects( $this->any() )->method( - 'getValue' + 'isSetFlag' )->with( \Magento\Framework\Session\SidResolver::XML_PATH_USE_FRONTEND_SID, \Magento\Store\Model\ScopeInterface::SCOPE_STORE diff --git a/dev/tests/integration/testsuite/Magento/GiftMessage/Observer/SalesEventQuoteMergeTest.php b/dev/tests/integration/testsuite/Magento/GiftMessage/Observer/SalesEventQuoteMergeTest.php new file mode 100644 index 000000000000..0902c35568ee --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GiftMessage/Observer/SalesEventQuoteMergeTest.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GiftMessage\Observer; + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\Event\ManagerInterface; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\Quote; + +class SalesEventQuoteMergeTest extends \PHPUnit\Framework\TestCase +{ + /** + * @magentoAppArea frontend + */ + public function testQuoteMerge() + { + $giftMessageId = 6; + $objectManager = Bootstrap::getObjectManager(); + $eventManager = $objectManager->get(ManagerInterface::class); + /** @var Quote $sourceQuote */ + $sourceQuote = $objectManager->create(QuoteFactory::class)->create(); + $targetQuote = clone($sourceQuote); + $sourceQuote->setGiftMessageId($giftMessageId); + + $eventManager->dispatch( + 'sales_quote_merge_after', + [ + 'quote' => $targetQuote, + 'source' => $sourceQuote + ] + ); + + self::assertEquals($giftMessageId, $targetQuote->getGiftMessageId()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/GiftMessage/_files/quote_with_item_message_rollback.php b/dev/tests/integration/testsuite/Magento/GiftMessage/_files/quote_with_item_message_rollback.php index 22307b4d8395..cd17183be7f7 100644 --- a/dev/tests/integration/testsuite/Magento/GiftMessage/_files/quote_with_item_message_rollback.php +++ b/dev/tests/integration/testsuite/Magento/GiftMessage/_files/quote_with_item_message_rollback.php @@ -20,7 +20,7 @@ if ($product->getId()) { $product->delete(); } -}; +} $quote->delete(); $registry->unregister('isSecureArea'); $registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/apply_tax_for_simple_product.php b/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/apply_tax_for_simple_product.php new file mode 100644 index 000000000000..9968704517ec --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/apply_tax_for_simple_product.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Tax\Model\ClassModel as TaxClassModel; +use Magento\Tax\Model\ResourceModel\TaxClass\CollectionFactory as TaxClassCollectionFactory; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$product = $productRepository->get('simple_product'); + +/** @var TaxClassCollectionFactory $taxClassCollectionFactory */ +$taxClassCollectionFactory = $objectManager->get(TaxClassCollectionFactory::class); +$taxClassCollection = $taxClassCollectionFactory->create(); + +/** @var TaxClassModel $taxClass */ +$taxClassCollection->addFieldToFilter('class_type', TaxClassModel::TAX_CLASS_TYPE_PRODUCT); +$taxClass = $taxClassCollection->getFirstItem(); + +$product->setCustomAttribute('tax_class_id', $taxClass->getClassId()); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/set_simple_product_out_of_stock.php b/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/set_simple_product_out_of_stock.php new file mode 100644 index 000000000000..f465f482275c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/set_simple_product_out_of_stock.php @@ -0,0 +1,19 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); + +$product = $productRepository->get('simple_product'); +$extensionAttributes = $product->getExtensionAttributes(); +$stockItem = $extensionAttributes->getStockItem(); +$stockItem->setIsInStock(false); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/simple_product.php b/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/simple_product.php new file mode 100644 index 000000000000..732c18d4d734 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/simple_product.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Framework\Api\DataObjectHelper; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductInterfaceFactory $productFactory */ +$productFactory = $objectManager->get(ProductInterfaceFactory::class); +/** @var DataObjectHelper $dataObjectHelper */ +$dataObjectHelper = Bootstrap::getObjectManager()->get(DataObjectHelper::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); + +$product = $productFactory->create(); +$productData = [ + ProductInterface::TYPE_ID => Type::TYPE_SIMPLE, + ProductInterface::ATTRIBUTE_SET_ID => 4, + ProductInterface::SKU => 'simple_product', + ProductInterface::NAME => 'Simple Product', + ProductInterface::PRICE => 10, + ProductInterface::VISIBILITY => Visibility::VISIBILITY_BOTH, + ProductInterface::STATUS => Status::STATUS_ENABLED, +]; +$dataObjectHelper->populateWithArray($product, $productData, ProductInterface::class); +/** Out of interface */ +$product + ->setWebsiteIds([1]) + ->setStockData([ + 'qty' => 85.5, + 'is_in_stock' => true, + 'manage_stock' => true, + 'is_qty_decimal' => true + ]); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/simple_product_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/simple_product_rollback.php new file mode 100644 index 000000000000..9a54f663c9c1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/simple_product_rollback.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); + +$currentArea = $registry->registry('isSecureArea'); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +try { + $productRepository->deleteById('simple_product'); +} catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + /** + * Tests which are wrapped with MySQL transaction clear all data by transaction rollback. + */ +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', $currentArea); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/virtual_product.php b/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/virtual_product.php new file mode 100644 index 000000000000..e4472464e17a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/virtual_product.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Framework\Api\DataObjectHelper; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductInterfaceFactory $productFactory */ +$productFactory = $objectManager->get(ProductInterfaceFactory::class); +/** @var DataObjectHelper $dataObjectHelper */ +$dataObjectHelper = Bootstrap::getObjectManager()->get(DataObjectHelper::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); + +$product = $productFactory->create(); +$productData = [ + ProductInterface::TYPE_ID => Type::TYPE_VIRTUAL, + ProductInterface::ATTRIBUTE_SET_ID => 4, + ProductInterface::SKU => 'virtual_product', + ProductInterface::NAME => 'Virtual Product', + ProductInterface::PRICE => 10, + ProductInterface::VISIBILITY => Visibility::VISIBILITY_BOTH, + ProductInterface::STATUS => Status::STATUS_ENABLED, +]; +$dataObjectHelper->populateWithArray($product, $productData, ProductInterface::class); +/** Out of interface */ +$product + ->setWebsiteIds([1]) + ->setStockData([ + 'qty' => 85.5, + 'is_in_stock' => true, + 'manage_stock' => true, + 'is_qty_decimal' => true + ]); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/virtual_product_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/virtual_product_rollback.php new file mode 100644 index 000000000000..f8d329f57462 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/virtual_product_rollback.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); + +$currentArea = $registry->registry('isSecureArea'); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +try { + $productRepository->deleteById('virtual_product'); +} catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + /** + * Tests which are wrapped with MySQL transaction clear all data by transaction rollback. + */ +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', $currentArea); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Controller/GraphQlControllerTest.php b/dev/tests/integration/testsuite/Magento/GraphQl/Controller/GraphQlControllerTest.php index 16a15cfcd2e2..d0d746812ec4 100644 --- a/dev/tests/integration/testsuite/Magento/GraphQl/Controller/GraphQlControllerTest.php +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Controller/GraphQlControllerTest.php @@ -38,6 +38,9 @@ class GraphQlControllerTest extends \Magento\TestFramework\Indexer\TestCase /** @var MetadataPool */ private $metadataPool; + /** @var Http */ + private $request; + public static function setUpBeforeClass() { $db = Bootstrap::getInstance()->getBootstrap() @@ -57,6 +60,7 @@ protected function setUp() : void $this->graphql = $this->objectManager->get(\Magento\GraphQl\Controller\GraphQl::class); $this->jsonSerializer = $this->objectManager->get(SerializerInterface::class); $this->metadataPool = $this->objectManager->get(MetadataPool::class); + $this->request = $this->objectManager->get(Http::class); } /** @@ -86,27 +90,120 @@ public function testDispatch() : void } QUERY; $postData = [ - 'query' => $query, - 'variables' => null, + 'query' => $query, + 'variables' => null, 'operationName' => null ]; - /** @var Http $request */ - $request = $this->objectManager->get(\Magento\Framework\App\Request\Http::class); - $request->setPathInfo('/graphql'); - $request->setContent(json_encode($postData)); + + $this->request->setPathInfo('/graphql'); + $this->request->setMethod('POST'); + $this->request->setContent(json_encode($postData)); $headers = $this->objectManager->create(\Zend\Http\Headers::class) ->addHeaders(['Content-Type' => 'application/json']); - $request->setHeaders($headers); - $response = $this->graphql->dispatch($request); + $this->request->setHeaders($headers); + $response = $this->graphql->dispatch($this->request); $output = $this->jsonSerializer->unserialize($response->getContent()); $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); $this->assertArrayNotHasKey('errors', $output, 'Response has errors'); - $this->assertTrue(!empty($output['data']['products']['items']), 'Products array has items'); - $this->assertTrue(!empty($output['data']['products']['items'][0]), 'Products array has items'); - $this->assertEquals($output['data']['products']['items'][0]['id'], $product->getData($linkField)); - $this->assertEquals($output['data']['products']['items'][0]['sku'], $product->getSku()); - $this->assertEquals($output['data']['products']['items'][0]['name'], $product->getName()); + $this->assertNotEmpty($output['data']['products']['items'], 'Products array has items'); + $this->assertNotEmpty($output['data']['products']['items'][0], 'Products array has items'); + $this->assertEquals($product->getData($linkField), $output['data']['products']['items'][0]['id']); + $this->assertEquals($product->getSku(), $output['data']['products']['items'][0]['sku']); + $this->assertEquals($product->getName(), $output['data']['products']['items'][0]['name']); + } + + /** + * Test request is dispatched and response generated when using GET request with query string + * + * @return void + */ + public function testDispatchWithGet() : void + { + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + + /** @var ProductInterface $product */ + $product = $productRepository->get('simple1'); + + $query + = <<<QUERY + { + products(filter: {sku: {eq: "simple1"}}) + { + items { + id + name + sku + } + } + } +QUERY; + + $this->request->setPathInfo('/graphql'); + $this->request->setMethod('GET'); + $this->request->setQueryValue('query', $query); + $response = $this->graphql->dispatch($this->request); + $output = $this->jsonSerializer->unserialize($response->getContent()); + $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); + + $this->assertArrayNotHasKey('errors', $output, 'Response has errors'); + $this->assertNotEmpty($output['data']['products']['items'], 'Products array has items'); + $this->assertNotEmpty($output['data']['products']['items'][0], 'Products array has items'); + $this->assertEquals($product->getData($linkField), $output['data']['products']['items'][0]['id']); + $this->assertEquals($product->getSku(), $output['data']['products']['items'][0]['sku']); + $this->assertEquals($product->getName(), $output['data']['products']['items'][0]['name']); + } + + /** Test request is dispatched and response generated when using GET request with parameterized query string + * + * @return void + */ + public function testDispatchGetWithParameterizedVariables() : void + { + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + + /** @var ProductInterface $product */ + $product = $productRepository->get('simple1'); + $query = <<<QUERY +query GetProducts(\$filterInput:ProductFilterInput){ + products( + filter:\$filterInput + ){ + items{ + id + name + sku + } + } +} +QUERY; + + $variables = [ + 'filterInput' => [ + 'sku' => ['eq' => 'simple1'] + ] + ]; + $queryParams = [ + 'query' => $query, + 'variables' => json_encode($variables), + 'operationName' => 'GetProducts' + ]; + + $this->request->setPathInfo('/graphql'); + $this->request->setMethod('GET'); + $this->request->setParams($queryParams); + $response = $this->graphql->dispatch($this->request); + $output = $this->jsonSerializer->unserialize($response->getContent()); + $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); + + $this->assertArrayNotHasKey('errors', $output, 'Response has errors'); + $this->assertNotEmpty($output['data']['products']['items'], 'Products array has items'); + $this->assertNotEmpty($output['data']['products']['items'][0], 'Products array has items'); + $this->assertEquals($product->getData($linkField), $output['data']['products']['items'][0]['id']); + $this->assertEquals($product->getSku(), $output['data']['products']['items'][0]['sku']); + $this->assertEquals($product->getName(), $output['data']['products']['items'][0]['name']); } /** @@ -116,7 +213,6 @@ public function testDispatch() : void */ public function testError() : void { - $this->markTestSkipped('Causes failiure with php unit and php 7.2'); $query = <<<QUERY { @@ -137,25 +233,25 @@ public function testError() : void QUERY; $postData = [ - 'query' => $query, - 'variables' => null, + 'query' => $query, + 'variables' => null, 'operationName' => null ]; - /** @var Http $request */ - $request = $this->objectManager->get(\Magento\Framework\App\Request\Http::class); - $request->setPathInfo('/graphql'); - $request->setContent(json_encode($postData)); + + $this->request->setPathInfo('/graphql'); + $this->request->setMethod('POST'); + $this->request->setContent(json_encode($postData)); $headers = $this->objectManager->create(\Zend\Http\Headers::class) ->addHeaders(['Content-Type' => 'application/json']); - $request->setHeaders($headers); - $response = $this->graphql->dispatch($request); + $this->request->setHeaders($headers); + $response = $this->graphql->dispatch($this->request); $outputResponse = $this->jsonSerializer->unserialize($response->getContent()); if (isset($outputResponse['errors'][0])) { if (is_array($outputResponse['errors'][0])) { foreach ($outputResponse['errors'] as $error) { $this->assertEquals( - $error['category'], - \Magento\Framework\GraphQl\Exception\GraphQlInputException::EXCEPTION_CATEGORY + \Magento\Framework\GraphQl\Exception\GraphQlInputException::EXCEPTION_CATEGORY, + $error['category'] ); if (isset($error['message'])) { $this->assertEquals($error['message'], 'Invalid entity_type specified: invalid'); @@ -169,12 +265,4 @@ public function testError() : void } } } - - /** - * teardown - */ - public function tearDown() - { - parent::tearDown(); - } } diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/add_simple_product.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/add_simple_product.php new file mode 100644 index 000000000000..f62b463d9400 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/add_simple_product.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); +/** @var CartRepositoryInterface $cartRepository */ +$cartRepository = Bootstrap::getObjectManager()->get(CartRepositoryInterface::class); + +$product = $productRepository->get('simple_product'); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); +$quote->addProduct($product, 2); +$cartRepository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/add_virtual_product.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/add_virtual_product.php new file mode 100644 index 000000000000..f597d0bffa7c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/add_virtual_product.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); +/** @var CartRepositoryInterface $cartRepository */ +$cartRepository = Bootstrap::getObjectManager()->get(CartRepositoryInterface::class); + +$product = $productRepository->get('virtual-product'); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); +$quote->addProduct($product, 2); +$cartRepository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/apply_coupon.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/apply_coupon.php new file mode 100644 index 000000000000..c70efa9a12a5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/apply_coupon.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Api\CouponManagementInterface; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var CouponManagementInterface $couponManagement */ +$couponManagement = Bootstrap::getObjectManager()->get(CouponManagementInterface::class); +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); +$couponManagement->set($quote->getId(), '2?ds5!2d'); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/apply_coupon_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/apply_coupon_rollback.php new file mode 100644 index 000000000000..5431c25b7df5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/apply_coupon_rollback.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Api\CouponManagementInterface; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var CouponManagementInterface $couponManagement */ +$couponManagement = Bootstrap::getObjectManager()->get(CouponManagementInterface::class); +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); +$couponManagement->remove($quote->getId()); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/customer/create_empty_cart.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/customer/create_empty_cart.php new file mode 100644 index 000000000000..86174a7753f4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/customer/create_empty_cart.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Api\CartManagementInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var CartManagementInterface $cartManagement */ +$cartManagement = Bootstrap::getObjectManager()->get(CartManagementInterface::class); +/** @var CartRepositoryInterface $cartRepository */ +$cartRepository = Bootstrap::getObjectManager()->get(CartRepositoryInterface::class); +/** @var QuoteIdMaskFactory $quoteIdMaskFactory */ +$quoteIdMaskFactory = Bootstrap::getObjectManager()->get(QuoteIdMaskFactory::class); + +$cartId = $cartManagement->createEmptyCartForCustomer(1); +$cart = $cartRepository->get($cartId); +$cart->setReservedOrderId('test_quote'); +$cartRepository->save($cart); + +/** @var QuoteIdMask $quoteIdMask */ +$quoteIdMask = $quoteIdMaskFactory->create(); +$quoteIdMask->setQuoteId($cartId) + ->save(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/customer/create_empty_cart_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/customer/create_empty_cart_rollback.php new file mode 100644 index 000000000000..0d5d5f067e35 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/customer/create_empty_cart_rollback.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); +/** @var QuoteIdMaskFactory $quoteIdMaskFactory */ +$quoteIdMaskFactory = Bootstrap::getObjectManager()->get(QuoteIdMaskFactory::class); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); +$quoteResource->delete($quote); + +/** @var QuoteIdMask $quoteIdMask */ +$quoteIdMask = $quoteIdMaskFactory->create(); +$quoteIdMask->setQuoteId($quote->getId()) + ->delete(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/disable_all_active_payment_methods.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/disable_all_active_payment_methods.php new file mode 100644 index 000000000000..8e6d4b8f74b8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/disable_all_active_payment_methods.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +// TODO: Should be removed in scope of https://github.com/magento/graphql-ce/issues/167 +declare(strict_types=1); + +use Magento\Config\Model\Config; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\Store; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +$paymentMethodList = $objectManager->get(\Magento\Payment\Api\PaymentMethodListInterface::class); +$rollbackConfigKey = 'test/payment/disabled_payment_methods'; +$configData = []; +$disabledPaymentMethods = []; + +// Get all active Payment Methods +foreach ($paymentMethodList->getActiveList(Store::DEFAULT_STORE_ID) as $paymentMethod) { + $configData['payment/' . $paymentMethod->getCode() . '/active'] = 0; + $disabledPaymentMethods[] = $paymentMethod->getCode(); +} +// Remember all manually disabled Payment Methods for rollback +$configData[$rollbackConfigKey] = implode(',', $disabledPaymentMethods); + +/** @var Config $defConfig */ +$defConfig = $objectManager->create(Config::class); +$defConfig->setScope(ScopeConfigInterface::SCOPE_TYPE_DEFAULT); + +foreach ($configData as $key => $value) { + $defConfig->setDataByPath($key, $value); + $defConfig->save(); +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/disable_all_active_payment_methods_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/disable_all_active_payment_methods_rollback.php new file mode 100644 index 000000000000..092826d1fd3f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/disable_all_active_payment_methods_rollback.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +// TODO: Should be removed in scope of https://github.com/magento/graphql-ce/issues/167 +declare(strict_types=1); + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +$rollbackConfigKey = 'test/payment/disabled_payment_methods'; + +$configWriter = $objectManager->create(WriterInterface::class); +$rollbackConfigValue = $objectManager->get(\Magento\Store\Model\StoreManagerInterface::class) + ->getStore(\Magento\Store\Model\Store::DEFAULT_STORE_ID) + ->getConfig($rollbackConfigKey); + +$disabledPaymentMethods = []; +if (!empty($rollbackConfigValue)) { + $disabledPaymentMethods = explode(',', $rollbackConfigValue); +} + +if (count($disabledPaymentMethods)) { + foreach ($disabledPaymentMethods as $keyToRemove) { + $configWriter->delete(sprintf('payment/%s/active', $keyToRemove)); + } +} +$configWriter->delete($rollbackConfigKey); + +$scopeConfig = $objectManager->get(ScopeConfigInterface::class); +$scopeConfig->clean(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/disable_offline_shipping_methods.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/disable_offline_shipping_methods.php new file mode 100644 index 000000000000..c5d0468dbfac --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/disable_offline_shipping_methods.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +// TODO: Should be removed in scope of https://github.com/magento/graphql-ce/issues/167 +declare(strict_types=1); + +use Magento\Framework\App\Config\Storage\Writer; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Config\ScopeConfigInterface; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Writer $configWriter */ +$configWriter = $objectManager->get(WriterInterface::class); + +$configWriter->save('carriers/flatrate/active', 0); +$configWriter->save('carriers/tablerate/active', 0); +$configWriter->save('carriers/freeshipping/active', 0); + +$scopeConfig = $objectManager->get(ScopeConfigInterface::class); +$scopeConfig->clean(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/disable_offline_shipping_methods_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/disable_offline_shipping_methods_rollback.php new file mode 100644 index 000000000000..384ffbdf51f3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/disable_offline_shipping_methods_rollback.php @@ -0,0 +1,19 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +// TODO: Should be removed in scope of https://github.com/magento/graphql-ce/issues/167 +declare(strict_types=1); + +use Magento\Framework\App\Config\Storage\Writer; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Writer $configWriter */ +$configWriter = $objectManager->create(WriterInterface::class); + +$configWriter->delete('carriers/flatrate/active'); +$configWriter->delete('carriers/tablerate/active'); +$configWriter->delete('carriers/freeshipping/active'); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_payment_methods.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_payment_methods.php new file mode 100644 index 000000000000..9c15589ba82e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_payment_methods.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +// TODO: Should be removed in scope of https://github.com/magento/graphql-ce/issues/167 +declare(strict_types=1); + +use Magento\Framework\App\Config\Storage\Writer; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Config\ScopeConfigInterface; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Writer $configWriter */ +$configWriter = $objectManager->get(WriterInterface::class); + +$configWriter->save('payment/banktransfer/active', 1); +$configWriter->save('payment/cashondelivery/active', 1); +$configWriter->save('payment/checkmo/active', 1); +$configWriter->save('payment/purchaseorder/active', 1); + +$scopeConfig = $objectManager->get(ScopeConfigInterface::class); +$scopeConfig->clean(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_payment_methods_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_payment_methods_rollback.php new file mode 100644 index 000000000000..61b7ed9737ff --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_payment_methods_rollback.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +// TODO: Should be removed in scope of https://github.com/magento/graphql-ce/issues/167 +declare(strict_types=1); + +use Magento\Framework\App\Config\Storage\Writer; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Writer $configWriter */ +$configWriter = $objectManager->create(WriterInterface::class); + +$configWriter->delete('payment/banktransfer/active'); +$configWriter->delete('payment/cashondelivery/active'); +$configWriter->delete('payment/checkmo/active'); +$configWriter->delete('payment/purchaseorder/active'); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php new file mode 100644 index 000000000000..ebc41da9b1b3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +// TODO: Should be removed in scope of https://github.com/magento/graphql-ce/issues/167 +declare(strict_types=1); + +use Magento\Framework\App\Config\Storage\Writer; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Config\ScopeConfigInterface; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Writer $configWriter */ +$configWriter = $objectManager->get(WriterInterface::class); + +$configWriter->save('carriers/flatrate/active', 1); +$configWriter->save('carriers/tablerate/active', 1); +$configWriter->save('carriers/freeshipping/active', 1); + +$scopeConfig = $objectManager->get(ScopeConfigInterface::class); +$scopeConfig->clean(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_shipping_methods_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_shipping_methods_rollback.php new file mode 100644 index 000000000000..384ffbdf51f3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/enable_offline_shipping_methods_rollback.php @@ -0,0 +1,19 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +// TODO: Should be removed in scope of https://github.com/magento/graphql-ce/issues/167 +declare(strict_types=1); + +use Magento\Framework\App\Config\Storage\Writer; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Writer $configWriter */ +$configWriter = $objectManager->create(WriterInterface::class); + +$configWriter->delete('carriers/flatrate/active'); +$configWriter->delete('carriers/tablerate/active'); +$configWriter->delete('carriers/freeshipping/active'); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/create_empty_cart.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/create_empty_cart.php new file mode 100644 index 000000000000..6a9ed898c316 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/create_empty_cart.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\GuestCartManagementInterface; +use Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var GuestCartManagementInterface $guestCartManagement */ +$guestCartManagement = Bootstrap::getObjectManager()->get(GuestCartManagementInterface::class); +/** @var CartRepositoryInterface $cartRepository */ +$cartRepository = Bootstrap::getObjectManager()->get(CartRepositoryInterface::class); +/** @var MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId */ +$maskedQuoteIdToQuoteId = Bootstrap::getObjectManager()->get(MaskedQuoteIdToQuoteIdInterface::class); + +$cartHash = $guestCartManagement->createEmptyCart(); +$cartId = $maskedQuoteIdToQuoteId->execute($cartHash); +$cart = $cartRepository->get($cartId); +$cart->setReservedOrderId('test_quote'); +$cartRepository->save($cart); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/create_empty_cart_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/create_empty_cart_rollback.php new file mode 100644 index 000000000000..0d5d5f067e35 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/create_empty_cart_rollback.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); +/** @var QuoteIdMaskFactory $quoteIdMaskFactory */ +$quoteIdMaskFactory = Bootstrap::getObjectManager()->get(QuoteIdMaskFactory::class); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); +$quoteResource->delete($quote); + +/** @var QuoteIdMask $quoteIdMask */ +$quoteIdMask = $quoteIdMaskFactory->create(); +$quoteIdMask->setQuoteId($quote->getId()) + ->delete(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/quote_with_address.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/quote_with_address.php new file mode 100644 index 000000000000..60d2f1c49d24 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/quote_with_address.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\AddressInterfaceFactory; +use Magento\Quote\Api\GuestCartManagementInterface; +use Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface; +use Magento\Quote\Model\ShippingAddressManagementInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\Api\DataObjectHelper; + +/** @var GuestCartManagementInterface $guestCartManagement */ +$guestCartManagement = Bootstrap::getObjectManager()->get(GuestCartManagementInterface::class); +/** @var CartRepositoryInterface $cartRepository */ +$cartRepository = Bootstrap::getObjectManager()->get(CartRepositoryInterface::class); +/** @var MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId */ +$maskedQuoteIdToQuoteId = Bootstrap::getObjectManager()->get(MaskedQuoteIdToQuoteIdInterface::class); +/** @var AddressInterfaceFactory $quoteAddressFactory */ +$quoteAddressFactory = Bootstrap::getObjectManager()->get(AddressInterfaceFactory::class); +/** @var DataObjectHelper $dataObjectHelper */ +$dataObjectHelper = Bootstrap::getObjectManager()->get(DataObjectHelper::class); +/** @var ShippingAddressManagementInterface $shippingAddressManagement */ +$shippingAddressManagement = Bootstrap::getObjectManager()->get(ShippingAddressManagementInterface::class); + +$cartHash = $guestCartManagement->createEmptyCart(); +$cartId = $maskedQuoteIdToQuoteId->execute($cartHash); +$cart = $cartRepository->get($cartId); +$cart->setReservedOrderId('guest_quote_with_address'); +$cartRepository->save($cart); + +$quoteAddressData = [ + AddressInterface::KEY_TELEPHONE => 4435555, + AddressInterface::KEY_POSTCODE => 78717, + AddressInterface::KEY_COUNTRY_ID => 'US', + AddressInterface::KEY_CITY => 'CityA', + AddressInterface::KEY_COMPANY => 'CompanyName', + AddressInterface::KEY_STREET => 'Andora str, 121', + AddressInterface::KEY_LASTNAME => 'Smith', + AddressInterface::KEY_FIRSTNAME => 'John', + AddressInterface::KEY_REGION_ID => 1, +]; +$quoteAddress = $quoteAddressFactory->create(); +$dataObjectHelper->populateWithArray($quoteAddress, $quoteAddressData, AddressInterfaceFactory::class); +$shippingAddressManagement->assign($cartId, $quoteAddress); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/quote_with_address_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/quote_with_address_rollback.php new file mode 100644 index 000000000000..d9f894abf45b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/quote_with_address_rollback.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); +/** @var QuoteIdMaskFactory $quoteIdMaskFactory */ +$quoteIdMaskFactory = Bootstrap::getObjectManager()->get(QuoteIdMaskFactory::class); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'guest_quote_with_address', 'reserved_order_id'); +$quoteResource->delete($quote); + +/** @var QuoteIdMask $quoteIdMask */ +$quoteIdMask = $quoteIdMaskFactory->create(); +$quoteIdMask->setQuoteId($quote->getId()) + ->delete(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/set_guest_email.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/set_guest_email.php new file mode 100644 index 000000000000..c8084b255239 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/guest/set_guest_email.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var CartRepositoryInterface $cartRepository */ +$cartRepository = Bootstrap::getObjectManager()->get(CartRepositoryInterface::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); + +$quote->setCustomerEmail('guest@example.com'); +$cartRepository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/make_cart_inactive.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/make_cart_inactive.php new file mode 100644 index 000000000000..b5704f82879b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/make_cart_inactive.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); +/** @var CartRepositoryInterface $cartRepository */ +$cartRepository = Bootstrap::getObjectManager()->get(CartRepositoryInterface::class); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); +$quote->setIsActive(false); +$cartRepository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/make_coupon_expired.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/make_coupon_expired.php new file mode 100644 index 000000000000..5316b184ecd1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/make_coupon_expired.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\SalesRule\Model\CouponFactory; +use Magento\SalesRule\Model\Spi\CouponResourceInterface; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var CouponResourceInterface $couponResource */ +$couponResource = Bootstrap::getObjectManager()->get(CouponResourceInterface::class); +/** @var CouponFactory $couponFactory */ +$couponFactory = Bootstrap::getObjectManager()->get(CouponFactory::class); + +$coupon = $couponFactory->create(); +$coupon->loadByCode('2?ds5!2d'); +$yesterday = new \DateTime(); +$yesterday->add(\DateInterval::createFromDateString('-1 day')); +$coupon->setExpirationDate($yesterday->format('Y-m-d')); +$couponResource->save($coupon); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/make_coupon_expired_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/make_coupon_expired_rollback.php new file mode 100644 index 000000000000..32c3d78bafd0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/make_coupon_expired_rollback.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\SalesRule\Model\CouponFactory; +use Magento\SalesRule\Model\Spi\CouponResourceInterface; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var CouponResourceInterface $couponResource */ +$couponResource = Bootstrap::getObjectManager()->get(CouponResourceInterface::class); +/** @var CouponFactory $couponFactory */ +$couponFactory = Bootstrap::getObjectManager()->get(CouponFactory::class); + +$coupon = $couponFactory->create(); +$coupon->loadByCode('2?ds5!2d'); + +if ($coupon->getId()) { + $coupon->setExpirationDate(null); + $couponResource->save($coupon); +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/restrict_coupon_usage_for_simple_product.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/restrict_coupon_usage_for_simple_product.php new file mode 100644 index 000000000000..e58c6b21d8d2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/restrict_coupon_usage_for_simple_product.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\SalesRule\Api\Data\ConditionInterface; +use Magento\SalesRule\Api\Data\ConditionInterfaceFactory; +use Magento\SalesRule\Api\RuleRepositoryInterface; +use Magento\SalesRule\Model\CouponFactory; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var CouponFactory $couponFactory */ +$couponFactory = Bootstrap::getObjectManager()->get(CouponFactory::class); +/** @var ConditionInterfaceFactory $conditionFactory */ +$conditionFactory = Bootstrap::getObjectManager()->get(ConditionInterfaceFactory::class); +/** @var RuleRepositoryInterface $ruleRepository */ +$ruleRepository = Bootstrap::getObjectManager()->get(RuleRepositoryInterface::class); + +$couponCode = '2?ds5!2d'; +$sku = 'simple_product'; + +$coupon = $couponFactory->create(); +$coupon->loadByCode($couponCode); +$ruleId = $coupon->getRuleId(); +$salesRule = $ruleRepository->getById($ruleId); + +/** @var ConditionInterface $conditionProductSku */ +$conditionProductSku = $conditionFactory->create(); +$conditionProductSku->setConditionType(\Magento\SalesRule\Model\Rule\Condition\Product::class); +$conditionProductSku->setAttributeName('sku'); +$conditionProductSku->setValue('1'); +$conditionProductSku->setOperator('!='); +$conditionProductSku->setValue($sku); + +/** @var ConditionInterface $conditionProductFound */ +$conditionProductFound = $conditionFactory->create(); +$conditionProductFound->setConditionType(\Magento\SalesRule\Model\Rule\Condition\Product\Found::class); +$conditionProductFound->setValue('1'); +$conditionProductFound->setAggregatorType('all'); +$conditionProductFound->setConditions([$conditionProductSku]); + +/** @var ConditionInterface $conditionCombine */ +$conditionCombine = $conditionFactory->create(); +$conditionCombine->setConditionType(\Magento\SalesRule\Model\Rule\Condition\Combine::class); +$conditionCombine->setValue('1'); +$conditionCombine->setAggregatorType('all'); +$conditionCombine->setConditions([$conditionProductFound]); + +$salesRule->setCondition($conditionCombine); +$ruleRepository->save($salesRule); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/restrict_coupon_usage_for_simple_product_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/restrict_coupon_usage_for_simple_product_rollback.php new file mode 100644 index 000000000000..86ab253f1d3c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/restrict_coupon_usage_for_simple_product_rollback.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\SalesRule\Api\Data\ConditionInterface; +use Magento\SalesRule\Api\Data\ConditionInterfaceFactory; +use Magento\SalesRule\Api\RuleRepositoryInterface; +use Magento\SalesRule\Model\CouponFactory; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var CouponFactory $couponFactory */ +$couponFactory = Bootstrap::getObjectManager()->get(CouponFactory::class); +/** @var ConditionInterfaceFactory $conditionFactory */ +$conditionFactory = Bootstrap::getObjectManager()->get(ConditionInterfaceFactory::class); +/** @var RuleRepositoryInterface $ruleRepository */ +$ruleRepository = Bootstrap::getObjectManager()->get(RuleRepositoryInterface::class); + +$couponCode = '2?ds5!2d'; +$sku = 'simple_product'; + +$coupon = $couponFactory->create(); +$coupon->loadByCode($couponCode); + +if ($coupon->getId()) { + $ruleId = $coupon->getRuleId(); + $salesRule = $ruleRepository->getById($ruleId); + + /** @var ConditionInterface $conditionCombine */ + $conditionCombine = $conditionFactory->create(); + $conditionCombine->setConditions([]); + + $salesRule->setCondition($conditionCombine); + $ruleRepository->save($salesRule); +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_checkmo_payment_method.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_checkmo_payment_method.php new file mode 100644 index 000000000000..a973a982b115 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_checkmo_payment_method.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\OfflinePayments\Model\Checkmo; +use Magento\Quote\Api\Data\PaymentInterface; +use Magento\Quote\Api\Data\PaymentInterfaceFactory; +use Magento\Quote\Api\PaymentMethodManagementInterface; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); +/** @var PaymentInterfaceFactory $paymentFactory */ +$paymentFactory = Bootstrap::getObjectManager()->get(PaymentInterfaceFactory::class); +/** @var PaymentMethodManagementInterface $paymentMethodManagement */ +$paymentMethodManagement = Bootstrap::getObjectManager()->get(PaymentMethodManagementInterface::class); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); + +$payment = $paymentFactory->create([ + 'data' => [ + PaymentInterface::KEY_METHOD => Checkmo::PAYMENT_METHOD_CHECKMO_CODE, + ] +]); +$paymentMethodManagement->set($quote->getId(), $payment); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php new file mode 100644 index 000000000000..1e7c10d251be --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Checkout\Api\Data\ShippingInformationInterface; +use Magento\Checkout\Api\Data\ShippingInformationInterfaceFactory; +use Magento\Checkout\Api\ShippingInformationManagementInterface; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); +/** @var ShippingInformationInterfaceFactory $shippingInformationFactory */ +$shippingInformationFactory = Bootstrap::getObjectManager()->get(ShippingInformationInterfaceFactory::class); +/** @var ShippingInformationManagementInterface $shippingInformationManagement */ +$shippingInformationManagement = Bootstrap::getObjectManager()->get(ShippingInformationManagementInterface::class); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); +$quoteAddress = $quote->getShippingAddress(); + +/** @var ShippingInformationInterface $shippingInformation */ +$shippingInformation = $shippingInformationFactory->create([ + 'data' => [ + ShippingInformationInterface::SHIPPING_ADDRESS => $quoteAddress, + ShippingInformationInterface::SHIPPING_CARRIER_CODE => 'flatrate', + ShippingInformationInterface::SHIPPING_METHOD_CODE => 'flatrate', + ], +]); +$shippingInformationManagement->saveAddressInformation($quote->getId(), $shippingInformation); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_new_billing_address.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_new_billing_address.php new file mode 100644 index 000000000000..2e15ef218d0c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_new_billing_address.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Api\DataObjectHelper; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\AddressInterfaceFactory; +use Magento\Quote\Api\BillingAddressManagementInterface; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; + +use Magento\TestFramework\Helper\Bootstrap; + +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); +/** @var AddressInterfaceFactory $quoteAddressFactory */ +$quoteAddressFactory = Bootstrap::getObjectManager()->get(AddressInterfaceFactory::class); +/** @var DataObjectHelper $dataObjectHelper */ +$dataObjectHelper = Bootstrap::getObjectManager()->get(DataObjectHelper::class); +/** @var BillingAddressManagementInterface $billingAddressManagement */ +$billingAddressManagement = Bootstrap::getObjectManager()->get(BillingAddressManagementInterface::class); + +$quoteAddressData = [ + AddressInterface::KEY_TELEPHONE => 3468676, + AddressInterface::KEY_POSTCODE => 75477, + AddressInterface::KEY_COUNTRY_ID => 'US', + AddressInterface::KEY_CITY => 'CityM', + AddressInterface::KEY_COMPANY => 'CompanyName', + AddressInterface::KEY_STREET => 'Green str, 67', + AddressInterface::KEY_LASTNAME => 'Smith', + AddressInterface::KEY_FIRSTNAME => 'John', + AddressInterface::KEY_REGION_ID => 1, +]; +$quoteAddress = $quoteAddressFactory->create(); +$dataObjectHelper->populateWithArray($quoteAddress, $quoteAddressData, AddressInterfaceFactory::class); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); +$billingAddressManagement->assign($quote->getId(), $quoteAddress); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_new_shipping_address.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_new_shipping_address.php new file mode 100644 index 000000000000..54f4d8d0c6e7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_new_shipping_address.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Api\DataObjectHelper; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\AddressInterfaceFactory; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\Quote\Model\ShippingAddressManagementInterface; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); +/** @var AddressInterfaceFactory $quoteAddressFactory */ +$quoteAddressFactory = Bootstrap::getObjectManager()->get(AddressInterfaceFactory::class); +/** @var DataObjectHelper $dataObjectHelper */ +$dataObjectHelper = Bootstrap::getObjectManager()->get(DataObjectHelper::class); +/** @var ShippingAddressManagementInterface $shippingAddressManagement */ +$shippingAddressManagement = Bootstrap::getObjectManager()->get(ShippingAddressManagementInterface::class); + +$quoteAddressData = [ + AddressInterface::KEY_TELEPHONE => 3468676, + AddressInterface::KEY_POSTCODE => '75477', + AddressInterface::KEY_COUNTRY_ID => 'US', + AddressInterface::KEY_CITY => 'CityM', + AddressInterface::KEY_COMPANY => 'CompanyName', + AddressInterface::KEY_STREET => 'Green str, 67', + AddressInterface::KEY_LASTNAME => 'Smith', + AddressInterface::KEY_FIRSTNAME => 'John', + AddressInterface::KEY_REGION_ID => 1, +]; +$quoteAddress = $quoteAddressFactory->create(); +$dataObjectHelper->populateWithArray($quoteAddress, $quoteAddressData, AddressInterfaceFactory::class); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); +$shippingAddressManagement->assign($quote->getId(), $quoteAddress); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_new_shipping_canada_address.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_new_shipping_canada_address.php new file mode 100644 index 000000000000..8e60dc904bd4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_new_shipping_canada_address.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Api\DataObjectHelper; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\AddressInterfaceFactory; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\Quote\Model\ShippingAddressManagementInterface; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); +/** @var AddressInterfaceFactory $quoteAddressFactory */ +$quoteAddressFactory = Bootstrap::getObjectManager()->get(AddressInterfaceFactory::class); +/** @var DataObjectHelper $dataObjectHelper */ +$dataObjectHelper = Bootstrap::getObjectManager()->get(DataObjectHelper::class); +/** @var ShippingAddressManagementInterface $shippingAddressManagement */ +$shippingAddressManagement = Bootstrap::getObjectManager()->get(ShippingAddressManagementInterface::class); + +$quoteAddressData = [ + AddressInterface::KEY_TELEPHONE => 3468676, + AddressInterface::KEY_POSTCODE => 'M4L 1V3', + AddressInterface::KEY_COUNTRY_ID => 'CA', + AddressInterface::KEY_CITY => 'Toronto', + AddressInterface::KEY_COMPANY => 'CompanyName', + AddressInterface::KEY_STREET => '500 Kingston Rd', + AddressInterface::KEY_LASTNAME => 'Smith', + AddressInterface::KEY_FIRSTNAME => 'John', + AddressInterface::KEY_REGION_CODE => 'ON', +]; +$quoteAddress = $quoteAddressFactory->create(); +$dataObjectHelper->populateWithArray($quoteAddress, $quoteAddressData, AddressInterfaceFactory::class); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); +$shippingAddressManagement->assign($quote->getId(), $quoteAddress); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php new file mode 100644 index 000000000000..aca55bd8414f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Tax\Api\Data\TaxRateInterface; +use Magento\Tax\Api\Data\TaxRuleInterface; +use Magento\Tax\Api\TaxRateRepositoryInterface; +use Magento\Tax\Api\TaxRuleRepositoryInterface; +use Magento\Tax\Model\Calculation\Rate; +use Magento\Tax\Model\Calculation\RateFactory; +use Magento\Tax\Model\Calculation\RateRepository; +use Magento\Tax\Model\Calculation\Rule; +use Magento\Tax\Model\Calculation\RuleFactory; +use Magento\Tax\Model\TaxRuleRepository; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\Api\DataObjectHelper; + +$objectManager = Bootstrap::getObjectManager(); +/** @var DataObjectHelper $dataObjectHelper */ +$dataObjectHelper = Bootstrap::getObjectManager()->get(DataObjectHelper::class); +/** @var RateFactory $rateFactory */ +$rateFactory = $objectManager->get(RateFactory::class); +/** @var RuleFactory $ruleFactory */ +$ruleFactory = $objectManager->get(RuleFactory::class); +/** @var RateRepository $rateRepository */ +$rateRepository = $objectManager->get(TaxRateRepositoryInterface::class); +/** @var TaxRuleRepository $ruleRepository */ +$ruleRepository = $objectManager->get(TaxRuleRepositoryInterface::class); +/** @var Rate $rate */ +$rate = $rateFactory->create(); +$rateData = [ + Rate::KEY_COUNTRY_ID => 'US', + Rate::KEY_REGION_ID => '1', + Rate::KEY_POSTCODE => '*', + Rate::KEY_CODE => 'US-TEST-*-Rate-1', + Rate::KEY_PERCENTAGE_RATE => '7.5', +]; +$dataObjectHelper->populateWithArray($rate, $rateData, TaxRateInterface::class); +$rateRepository->save($rate); + +$rule = $ruleFactory->create(); +$ruleData = [ + Rule::KEY_CODE=> 'GraphQl Test Rule', + Rule::KEY_PRIORITY => '0', + Rule::KEY_POSITION => '0', + Rule::KEY_CUSTOMER_TAX_CLASS_IDS => [3], + Rule::KEY_PRODUCT_TAX_CLASS_IDS => [2], + Rule::KEY_TAX_RATE_IDS => [$rate->getId()], +]; +$dataObjectHelper->populateWithArray($rule, $ruleData, TaxRuleInterface::class); +$ruleRepository->save($rule); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_1_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_1_rollback.php new file mode 100644 index 000000000000..aba1960624ed --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_1_rollback.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Tax\Api\TaxRateRepositoryInterface; +use Magento\Tax\Api\TaxRuleRepositoryInterface; +use Magento\Tax\Model\Calculation\Rate; +use Magento\Tax\Model\Calculation\RateFactory; +use Magento\Tax\Model\Calculation\RateRepository; +use Magento\Tax\Model\Calculation\Rule; +use Magento\Tax\Model\Calculation\RuleFactory; +use Magento\Tax\Model\TaxRuleRepository; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Tax\Model\ResourceModel\Calculation\Rate as RateResource; +use Magento\Tax\Model\ResourceModel\Calculation\Rule as RuleResource; + +$objectManager = Bootstrap::getObjectManager(); +/** @var RateFactory $rateFactory */ +$rateFactory = $objectManager->get(RateFactory::class); +/** @var RuleFactory $ruleFactory */ +$ruleFactory = $objectManager->get(RuleFactory::class); +/** @var RateRepository $rateRepository */ +$rateRepository = $objectManager->get(TaxRateRepositoryInterface::class); +/** @var TaxRuleRepository $ruleRepository */ +$ruleRepository = $objectManager->get(TaxRuleRepositoryInterface::class); +/** @var RateResource $rateResource */ +$rateResource = $objectManager->get(RateResource::class); +/** @var RuleResource $ruleResource */ +$ruleResource = $objectManager->get(RuleResource::class); + +$rate = $rateFactory->create(); +$rateResource->load($rate, 'US-TEST-*-Rate-1', Rate::KEY_CODE); +$rule = $ruleFactory->create(); +$ruleResource->load($rule, 'GraphQl Test Rule', Rule::KEY_CODE); +$ruleRepository->delete($rule); +$rateRepository->delete($rate); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Ups/_files/enable_ups_shipping_method.php b/dev/tests/integration/testsuite/Magento/GraphQl/Ups/_files/enable_ups_shipping_method.php new file mode 100644 index 000000000000..42931db75a43 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Ups/_files/enable_ups_shipping_method.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +// TODO: Should be removed in scope of https://github.com/magento/graphql-ce/issues/167 +declare(strict_types=1); + +use Magento\Framework\App\Config\Storage\Writer; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Config\ScopeConfigInterface; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Writer $configWriter */ +$configWriter = $objectManager->get(WriterInterface::class); + +$configWriter->save('carriers/ups/active', 1); +$configWriter->save('carriers/ups/type', "UPS"); + +$scopeConfig = $objectManager->get(ScopeConfigInterface::class); +$scopeConfig->clean(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Ups/_files/enable_ups_shipping_method_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Ups/_files/enable_ups_shipping_method_rollback.php new file mode 100644 index 000000000000..cf6dc08dd91a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Ups/_files/enable_ups_shipping_method_rollback.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +// TODO: Should be removed in scope of https://github.com/magento/graphql-ce/issues/167 +declare(strict_types=1); + +use Magento\Framework\App\Config\Storage\Writer; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Writer $configWriter */ +$configWriter = $objectManager->create(WriterInterface::class); + +$configWriter->delete('carriers/ups/active'); +$configWriter->delete('carriers/ups/type'); diff --git a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/AbstractGraphqlCacheTest.php b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/AbstractGraphqlCacheTest.php new file mode 100644 index 000000000000..4cc46a8e745e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/AbstractGraphqlCacheTest.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlCache\Controller; + +use PHPUnit\Framework\TestCase; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Abstract test class for Graphql cache tests + */ +abstract class AbstractGraphqlCacheTest extends TestCase +{ + /** + * @var ObjectManager + */ + protected $objectManager; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->usePageCachePlugin(); + } + + /** + * Enable full page cache plugin + */ + protected function usePageCachePlugin(): void + { + /** @var $registry \Magento\Framework\Registry */ + $registry = $this->objectManager->get(\Magento\Framework\Registry::class); + $registry->register('use_page_cache_plugin', true, true); + } +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoriesWithProductsCacheTest.php b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoriesWithProductsCacheTest.php new file mode 100644 index 000000000000..62cda28a4493 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoriesWithProductsCacheTest.php @@ -0,0 +1,103 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlCache\Controller\Catalog; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\App\Request\Http; +use Magento\GraphQl\Controller\GraphQl; +use Magento\GraphQlCache\Controller\AbstractGraphqlCacheTest; + +/** + * Tests cache debug headers and cache tag validation for a category with product query + * + * @magentoAppArea graphql + * @magentoCache full_page enabled + * @magentoDbIsolation disabled + */ +class CategoriesWithProductsCacheTest extends AbstractGraphqlCacheTest +{ + /** + * @var GraphQl + */ + private $graphqlController; + + /** + * @var Http + */ + private $request; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->graphqlController = $this->objectManager->get(\Magento\GraphQl\Controller\GraphQl::class); + $this->request = $this->objectManager->create(Http::class); + } + /** + * Test cache tags and debug header for category with products querying for products and category + * + * @magentoDataFixture Magento/Catalog/_files/category_product.php + */ + public function testToCheckRequestCacheTagsForCategoryWithProducts(): void + { + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + /** @var ProductInterface $product */ + $product = $productRepository->get('simple333'); + $categoryId ='333'; + $query + = <<<QUERY +query GetCategoryWithProducts(\$id: Int!, \$pageSize: Int!, \$currentPage: Int!) { + category(id: \$id) { + id + description + name + product_count + products( + pageSize: \$pageSize, + currentPage: \$currentPage) { + items { + id + name + attribute_set_id + url_key + sku + type_id + updated_at + url_key + url_path + } + total_count + } + } + } +QUERY; + $variables =[ + 'id' => $categoryId, + 'pageSize'=> 10, + 'currentPage' => 1 + ]; + $queryParams = [ + 'query' => $query, + 'variables' => json_encode($variables), + 'operationName' => 'GetCategoryWithProducts' + ]; + + $this->request->setPathInfo('/graphql'); + $this->request->setMethod('GET'); + $this->request->setParams($queryParams); + $response = $this->graphqlController->dispatch($this->request); + $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $expectedCacheTags = ['cat_c','cat_c_' . $categoryId,'cat_p','cat_p_' . $product->getId(),'FPC']; + $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); + $this->assertEquals($expectedCacheTags, $actualCacheTags); + } +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryCacheTest.php b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryCacheTest.php new file mode 100644 index 000000000000..96f6685233f2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryCacheTest.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlCache\Controller\Catalog; + +use Magento\Framework\App\Request\Http; +use Magento\GraphQl\Controller\GraphQl; +use Magento\GraphQlCache\Controller\AbstractGraphqlCacheTest; + +/** + * Tests cache debug headers and cache tag validation for a simple category query + * + * @magentoAppArea graphql + * @magentoCache full_page enabled + * @magentoDbIsolation disabled + */ +class CategoryCacheTest extends AbstractGraphqlCacheTest +{ + /** + * @var GraphQl + */ + private $graphqlController; + + /** + * @var Http + */ + private $request; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->graphqlController = $this->objectManager->get(\Magento\GraphQl\Controller\GraphQl::class); + $this->request = $this->objectManager->create(Http::class); + } + /** + * Test cache tags and debug header for category and querying only for category + * + * @magentoDataFixture Magento/Catalog/_files/category_product.php + */ + public function testToCheckRequestCacheTagsForForCategory(): void + { + $categoryId ='333'; + $query + = <<<QUERY + { + category(id: $categoryId) { + id + name + url_key + description + product_count + } + } +QUERY; + $this->request->setPathInfo('/graphql'); + $this->request->setMethod('GET'); + $this->request->setQueryValue('query', $query); + $response = $this->graphqlController->dispatch($this->request); + $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); + $expectedCacheTags = ['cat_c','cat_c_' . $categoryId,'FPC']; + $this->assertEquals($expectedCacheTags, $actualCacheTags); + } +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/DeepNestedCategoriesAndProductsTest.php b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/DeepNestedCategoriesAndProductsTest.php new file mode 100644 index 000000000000..7f992a0843f7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/DeepNestedCategoriesAndProductsTest.php @@ -0,0 +1,128 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlCache\Controller\Catalog; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Framework\App\Request\Http; +use Magento\GraphQlCache\Controller\AbstractGraphqlCacheTest; + +/** + * Tests cache debug headers and cache tag validation for a deep nested category and product query + * + * @magentoAppArea graphql + * @magentoDbIsolation disabled + */ +class DeepNestedCategoriesAndProductsTest extends AbstractGraphqlCacheTest +{ + /** @var \Magento\GraphQl\Controller\GraphQl */ + private $graphql; + + /** @var Http */ + private $request; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->graphql = $this->objectManager->get(\Magento\GraphQl\Controller\GraphQl::class); + $this->request = $this->objectManager->get(Http::class); + } + + /** + * Test cache tags and debug header for deep nested queries involving category and products + * + * @magentoCache all enabled + * @magentoDataFixture Magento/Catalog/_files/product_in_multiple_categories.php + * + */ + public function testDispatchForCacheHeadersOnDeepNestedQueries(): void + { + $baseCategoryId ='333'; + $query + = <<<QUERY + { + category(id: $baseCategoryId) { + products { + items { + attribute_set_id + country_of_manufacture + created_at + description { + html + } + gift_message_available + id + categories { + name + url_path + available_sort_by + level + products { + items { + name + id + } + } + } + } + } + } +} +QUERY; + /** @var CategoryRepositoryInterface $categoryRepository */ + $categoryRepository = $this->objectManager->get(CategoryRepositoryInterface::class); + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + + $resolvedCategoryIds = []; + $category = $categoryRepository->get($baseCategoryId); + + $productIdsFromCategory = $category->getProductCollection()->getAllIds(); + foreach ($productIdsFromCategory as $productId) { + $resolvedCategoryIds = array_merge( + $resolvedCategoryIds, + $productRepository->getById($productId)->getCategoryIds() + ); + } + + $resolvedCategoryIds = array_merge($resolvedCategoryIds, [$baseCategoryId]); + foreach ($resolvedCategoryIds as $categoryId) { + $category = $categoryRepository->get($categoryId); + $productIdsFromCategory= array_merge( + $productIdsFromCategory, + $category->getProductCollection()->getAllIds() + ); + } + + $uniqueProductIds = array_unique($productIdsFromCategory); + $uniqueCategoryIds = array_unique($resolvedCategoryIds); + $expectedCacheTags = ['cat_c', 'cat_p', 'FPC']; + foreach ($uniqueProductIds as $uniqueProductId) { + $expectedCacheTags = array_merge($expectedCacheTags, ['cat_p_'.$uniqueProductId]); + } + foreach ($uniqueCategoryIds as $uniqueCategoryId) { + $expectedCacheTags = array_merge($expectedCacheTags, ['cat_c_'.$uniqueCategoryId]); + } + + $this->request->setPathInfo('/graphql'); + $this->request->setMethod('GET'); + $this->request->setQueryValue('query', $query); + $response = $this->graphql->dispatch($this->request); + $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); + $this->assertEmpty( + array_merge( + array_diff($expectedCacheTags, $actualCacheTags), + array_diff($actualCacheTags, $expectedCacheTags) + ) + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/ProductsCacheTest.php b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/ProductsCacheTest.php new file mode 100644 index 000000000000..78534176a352 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/ProductsCacheTest.php @@ -0,0 +1,115 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlCache\Controller\Catalog; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\App\Request\Http; +use Magento\GraphQl\Controller\GraphQl; +use Magento\GraphQlCache\Controller\AbstractGraphqlCacheTest; + +/** + * Tests cache debug headers and cache tag validation for a simple product query + * + * @magentoAppArea graphql + * @magentoCache full_page enabled + * @magentoDbIsolation disabled + */ +class ProductsCacheTest extends AbstractGraphqlCacheTest +{ + /** + * @var GraphQl + */ + private $graphqlController; + + /** + * @var Http + */ + private $request; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->graphqlController = $this->objectManager->get(\Magento\GraphQl\Controller\GraphQl::class); + $this->request = $this->objectManager->create(Http::class); + } + + /** + * Test request is dispatched and response is checked for debug headers and cache tags + * + * @magentoDataFixture Magento/Catalog/_files/product_simple_with_url_key.php + */ + public function testToCheckRequestCacheTagsForProducts(): void + { + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + + /** @var ProductInterface $product */ + $product = $productRepository->get('simple1'); + + $query + = <<<QUERY + { + products(filter: {sku: {eq: "simple1"}}) + { + items { + id + name + sku + description { + html + } + } + } + } +QUERY; + + $this->request->setPathInfo('/graphql'); + $this->request->setMethod('GET'); + $this->request->setQueryValue('query', $query); + $response = $this->graphqlController->dispatch($this->request); + $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); + $expectedCacheTags = ['cat_p', 'cat_p_' . $product->getId(), 'FPC']; + $this->assertEquals($expectedCacheTags, $actualCacheTags); + } + + /** + * Test request is checked for debug headers and no cache tags for not existing product + */ + public function testToCheckRequestNoTagsForProducts(): void + { + $query + = <<<QUERY + { + products(filter: {sku: {eq: "simple10"}}) + { + items { + id + name + sku + description { + html + } + } + } + } + +QUERY; + $this->request->setPathInfo('/graphql'); + $this->request->setMethod('GET'); + $this->request->setQueryValue('query', $query); + $response = $this->graphqlController->dispatch($this->request); + $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); + $expectedCacheTags = ['FPC']; + $this->assertEquals($expectedCacheTags, $actualCacheTags); + } +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Cms/BlockCacheTest.php b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Cms/BlockCacheTest.php new file mode 100644 index 000000000000..160f5f9109f3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Cms/BlockCacheTest.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlCache\Controller\Cms; + +use Magento\Cms\Model\BlockRepository; +use Magento\Framework\App\Request\Http; +use Magento\GraphQl\Controller\GraphQl; +use Magento\GraphQlCache\Controller\AbstractGraphqlCacheTest; + +/** + * Test caching works for CMS blocks + * + * @magentoAppArea graphql + * @magentoCache full_page enabled + * @magentoDbIsolation disabled + */ +class BlockCacheTest extends AbstractGraphqlCacheTest +{ + /** + * @var GraphQl + */ + private $graphqlController; + + /** + * @var Http + */ + private $request; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->graphqlController = $this->objectManager->get(\Magento\GraphQl\Controller\GraphQl::class); + $this->request = $this->objectManager->create(Http::class); + } + + /** + * Test that the correct cache tags get added to request for cmsBlocks + * + * @magentoDataFixture Magento/Cms/_files/block.php + */ + public function testCmsBlocksRequestHasCorrectTags(): void + { + $blockIdentifier = 'fixture_block'; + $blockRepository = $this->objectManager->get(BlockRepository::class); + $block = $blockRepository->getById($blockIdentifier); + + $query + = <<<QUERY + { + cmsBlocks(identifiers: ["$blockIdentifier"]) { + items { + title + identifier + content + } + } +} +QUERY; + + $this->request->setPathInfo('/graphql'); + $this->request->setMethod('GET'); + $this->request->setQueryValue('query', $query); + $response = $this->graphqlController->dispatch($this->request); + $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $expectedCacheTags = ['cms_b', 'cms_b_' . $block->getId(), 'cms_b_' . $block->getIdentifier(), 'FPC']; + $rawActualCacheTags = $response->getHeader('X-Magento-Tags')->getFieldValue(); + $actualCacheTags = explode(',', $rawActualCacheTags); + $this->assertEquals($expectedCacheTags, $actualCacheTags); + } +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Cms/CmsPageCacheTest.php b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Cms/CmsPageCacheTest.php new file mode 100644 index 000000000000..8d4bbfc0f2b1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Cms/CmsPageCacheTest.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlCache\Controller\Cms; + +use Magento\Cms\Model\GetPageByIdentifier; +use Magento\Framework\App\Request\Http; +use Magento\GraphQl\Controller\GraphQl; +use Magento\GraphQlCache\Controller\AbstractGraphqlCacheTest; + +/** + * Test caching works for CMS page + * + * @magentoAppArea graphql + * @magentoCache full_page enabled + * @magentoDbIsolation disabled + * + */ +class CmsPageCacheTest extends AbstractGraphqlCacheTest +{ + /** + * @var GraphQl + */ + private $graphqlController; + + /** + * @var Http + */ + private $request; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->graphqlController = $this->objectManager->get(\Magento\GraphQl\Controller\GraphQl::class); + $this->request = $this->objectManager->create(Http::class); + } + + /** + * Test that the correct cache tags get added to request for cmsPage query + * + * @magentoDataFixture Magento/Cms/_files/pages.php + */ + public function testToCheckCmsPageRequestCacheTags(): void + { + $cmsPage = $this->objectManager->get(GetPageByIdentifier::class)->execute('page100', 0); + $pageId = $cmsPage->getId(); + + $query = + <<<QUERY + { + cmsPage(id: $pageId) { + url_key + title + content + content_heading + page_layout + meta_title + meta_description + meta_keywords + } + } +QUERY; + + $this->request->setPathInfo('/graphql'); + $this->request->setMethod('GET'); + $this->request->setQueryValue('query', $query); + $response = $this->graphqlController->dispatch($this->request); + $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $requestedCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); + $expectedCacheTags = ['cms_p', 'cms_p_' .$pageId , 'FPC']; + $this->assertEquals($expectedCacheTags, $requestedCacheTags); + } +} diff --git a/dev/tests/integration/testsuite/Magento/GroupedImportExport/Model/GroupedTest.php b/dev/tests/integration/testsuite/Magento/GroupedImportExport/Model/GroupedTest.php index 67817b068ff0..afd515757ae4 100644 --- a/dev/tests/integration/testsuite/Magento/GroupedImportExport/Model/GroupedTest.php +++ b/dev/tests/integration/testsuite/Magento/GroupedImportExport/Model/GroupedTest.php @@ -9,7 +9,10 @@ class GroupedTest extends AbstractProductExportImportTestCase { - public function exportImportDataProvider() + /** + * @return array + */ + public function exportImportDataProvider(): array { return [ 'grouped-product' => [ @@ -23,17 +26,13 @@ public function exportImportDataProvider() ]; } - public function importReplaceDataProvider() - { - return $this->exportImportDataProvider(); - } - /** - * @param \Magento\Catalog\Model\Product $expectedProduct - * @param \Magento\Catalog\Model\Product $actualProduct + * @inheritdoc */ - protected function assertEqualsSpecificAttributes($expectedProduct, $actualProduct) - { + protected function assertEqualsSpecificAttributes( + \Magento\Catalog\Model\Product $expectedProduct, + \Magento\Catalog\Model\Product $actualProduct + ): void { $expectedAssociatedProducts = $expectedProduct->getTypeInstance()->getAssociatedProducts($expectedProduct); $actualAssociatedProducts = $actualProduct->getTypeInstance()->getAssociatedProducts($actualProduct); diff --git a/dev/tests/integration/testsuite/Magento/GroupedProduct/Model/Product/Type/GroupedTest.php b/dev/tests/integration/testsuite/Magento/GroupedProduct/Model/Product/Type/GroupedTest.php index dcf4565873ea..ed283d196e69 100644 --- a/dev/tests/integration/testsuite/Magento/GroupedProduct/Model/Product/Type/GroupedTest.php +++ b/dev/tests/integration/testsuite/Magento/GroupedProduct/Model/Product/Type/GroupedTest.php @@ -5,8 +5,19 @@ */ namespace Magento\GroupedProduct\Model\Product\Type; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\CatalogInventory\Model\Configuration; +use Magento\Framework\App\Config\ReinitableConfigInterface; +use Magento\Framework\App\Config\Value; + class GroupedTest extends \PHPUnit\Framework\TestCase { + /** + * @var ReinitableConfigInterface + */ + private $reinitableConfig; + /** * @var \Magento\Framework\ObjectManagerInterface */ @@ -20,16 +31,21 @@ class GroupedTest extends \PHPUnit\Framework\TestCase protected function setUp() { $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $this->_productType = $this->objectManager->get(\Magento\Catalog\Model\Product\Type::class); + $this->reinitableConfig = $this->objectManager->get(ReinitableConfigInterface::class); + } + + protected function tearDown() + { + $this->dropConfigValue(Configuration::XML_PATH_SHOW_OUT_OF_STOCK); } public function testFactory() { $product = new \Magento\Framework\DataObject(); - $product->setTypeId(\Magento\GroupedProduct\Model\Product\Type\Grouped::TYPE_CODE); + $product->setTypeId(Grouped::TYPE_CODE); $type = $this->_productType->factory($product); - $this->assertInstanceOf(\Magento\GroupedProduct\Model\Product\Type\Grouped::class, $type); + $this->assertInstanceOf(Grouped::class, $type); } /** @@ -38,12 +54,12 @@ public function testFactory() */ public function testGetAssociatedProducts() { - $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + $productRepository = $this->objectManager->create(ProductRepositoryInterface::class); - /** @var \Magento\Catalog\Model\Product $product */ + /** @var Product $product */ $product = $productRepository->get('grouped-product'); $type = $product->getTypeInstance(); - $this->assertInstanceOf(\Magento\GroupedProduct\Model\Product\Type\Grouped::class, $type); + $this->assertInstanceOf(Grouped::class, $type); $associatedProducts = $type->getAssociatedProducts($product); $this->assertCount(2, $associatedProducts); @@ -53,7 +69,7 @@ public function testGetAssociatedProducts() } /** - * @param \Magento\Catalog\Model\Product $product + * @param Product $product */ private function assertProductInfo($product) { @@ -92,25 +108,25 @@ public function testPrepareProduct() \Magento\Framework\DataObject::class, ['data' => ['value' => ['qty' => 2]]] ); - /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ - $productRepository = $this->objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); $product = $productRepository->get('grouped-product'); - /** @var \Magento\GroupedProduct\Model\Product\Type\Grouped $type */ - $type = $this->objectManager->get(\Magento\GroupedProduct\Model\Product\Type\Grouped::class); + /** @var Grouped $type */ + $type = $this->objectManager->get(Grouped::class); $processModes = [ - \Magento\GroupedProduct\Model\Product\Type\Grouped::PROCESS_MODE_FULL, - \Magento\GroupedProduct\Model\Product\Type\Grouped::PROCESS_MODE_LITE + Grouped::PROCESS_MODE_FULL, + Grouped::PROCESS_MODE_LITE ]; $expectedData = [ - \Magento\GroupedProduct\Model\Product\Type\Grouped::PROCESS_MODE_FULL => [ + Grouped::PROCESS_MODE_FULL => [ 1 => '{"super_product_config":{"product_type":"grouped","product_id":"' . $product->getId() . '"}}', 21 => '{"super_product_config":{"product_type":"grouped","product_id":"' . $product->getId() . '"}}', ], - \Magento\GroupedProduct\Model\Product\Type\Grouped::PROCESS_MODE_LITE => [ + Grouped::PROCESS_MODE_LITE => [ $product->getId() => '{"value":{"qty":2}}', ] ]; @@ -127,4 +143,152 @@ public function testPrepareProduct() } } } + + /** + * Test adding grouped product to cart when one of subproducts is out of stock. + * + * @magentoDataFixture Magento/GroupedProduct/_files/product_grouped_with_out_of_stock.php + * @magentoAppArea frontend + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + * @dataProvider outOfStockSubProductDataProvider + * @param bool $outOfStockShown + * @param array $data + * @param array $expected + */ + public function testOutOfStockSubProduct(bool $outOfStockShown, array $data, array $expected) + { + $this->changeConfigValue(Configuration::XML_PATH_SHOW_OUT_OF_STOCK, $outOfStockShown); + $buyRequest = new \Magento\Framework\DataObject($data); + $productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + /** @var Product $product */ + $product = $productRepository->get('grouped-product'); + /** @var Grouped $groupedProduct */ + $groupedProduct = $this->objectManager->get(Grouped::class); + $actual = $groupedProduct->prepareForCartAdvanced($buyRequest, $product, Grouped::PROCESS_MODE_FULL); + self::assertEquals( + count($expected), + count($actual) + ); + /** @var Product $product */ + foreach ($actual as $product) { + $sku = $product->getSku(); + self::assertEquals( + $expected[$sku], + $product->getCartQty(), + "Failed asserting that Product Cart Quantity matches expected" + ); + } + } + + /** + * Data provider for testOutOfStockSubProduct. + * + * @return array + */ + public function outOfStockSubProductDataProvider() + { + return [ + 'Out of stock product are shown #1' => [ + true, + [ + 'product' => 3, + 'qty' => 1, + 'super_group' => [ + 1 => 4, + 21 => 5, + ], + ], + [ + 'virtual-product' => 5, + 'simple' => 4 + ], + ], + 'Out of stock product are shown #2' => [ + true, + [ + 'product' => 3, + 'qty' => 1, + 'super_group' => [ + 1 => 0, + ], + ], + [ + 'virtual-product' => 2.5, // This is a default quantity. + ], + ], + 'Out of stock product are hidden #1' => [ + false, + [ + 'product' => 3, + 'qty' => 1, + 'super_group' => [ + 1 => 4, + 21 => 5, + ], + ], + [ + 'virtual-product' => 5, + 'simple' => 4, + ], + ], + 'Out of stock product are hidden #2' => [ + false, + [ + 'product' => 3, + 'qty' => 1, + 'super_group' => [ + 1 => 0, + ], + ], + [ + 'virtual-product' => 2.5, // This is a default quantity. + ], + ], + ]; + } + + /** + * Write config value to database. + * + * @param string $path + * @param string $value + * @param string $scope + * @param int $scopeId + */ + private function changeConfigValue(string $path, string $value, string $scope = 'default', int $scopeId = 0) + { + $configValue = $this->objectManager->create(Value::class); + $configValue->setPath($path) + ->setValue($value) + ->setScope($scope) + ->setScopeId($scopeId) + ->save(); + $this->reinitConfig(); + } + + /** + * Delete config value from database. + * + * @param string $path + */ + private function dropConfigValue(string $path) + { + $configValue = $this->objectManager->create(Value::class); + try { + $configValue->load($path, 'path'); + $configValue->delete(); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + // do nothing + } + $this->reinitConfig(); + } + + /** + * Reinit config. + */ + private function reinitConfig() + { + $this->reinitableConfig->reinit(); + } } diff --git a/dev/tests/integration/testsuite/Magento/GroupedProduct/_files/product_grouped_with_out_of_stock.php b/dev/tests/integration/testsuite/Magento/GroupedProduct/_files/product_grouped_with_out_of_stock.php new file mode 100644 index 000000000000..369ce7d490ee --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GroupedProduct/_files/product_grouped_with_out_of_stock.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +require realpath(__DIR__ . '/../../') . '/Catalog/_files/product_associated.php'; +require realpath(__DIR__ . '/../../') . '/Catalog/_files/product_virtual_in_stock.php'; +require realpath(__DIR__ . '/../../') . '/Catalog/_files/product_virtual_out_of_stock.php'; + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +$productRepository = $objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->isObjectNew(true); +$product->setTypeId( + \Magento\GroupedProduct\Model\Product\Type\Grouped::TYPE_CODE +)->setAttributeSetId( + 4 +)->setWebsiteIds( + [1] +)->setName( + 'Grouped Product' +)->setSku( + 'grouped-product' +)->setPrice( + 100 +)->setTaxClassId( + 0 +)->setVisibility( + \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH +)->setStatus( + \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED +); + +$newLinks = []; +$productLinkFactory = $objectManager->get(\Magento\Catalog\Api\Data\ProductLinkInterfaceFactory::class); + +/** @var \Magento\Catalog\Api\Data\ProductLinkInterface $productLink */ +$productLink = $productLinkFactory->create(); +$linkedProduct = $productRepository->getById(1); +$productLink->setSku($product->getSku()) + ->setLinkType('associated') + ->setLinkedProductSku($linkedProduct->getSku()) + ->setLinkedProductType($linkedProduct->getTypeId()) + ->setPosition(1) + ->getExtensionAttributes() + ->setQty(1); +$newLinks[] = $productLink; + +$subProductsSkus = ['virtual-product', 'virtual-product-out']; +foreach ($subProductsSkus as $sku) { + /** @var \Magento\Catalog\Api\Data\ProductLinkInterface $productLink */ + $productLink = $productLinkFactory->create(); + $linkedProduct = $productRepository->get($sku); + $productLink->setSku($product->getSku()) + ->setLinkType('associated') + ->setLinkedProductSku($sku) + ->setLinkedProductType($linkedProduct->getTypeId()) + ->getExtensionAttributes() + ->setQty(2.5); + $newLinks[] = $productLink; +} +$product->setProductLinks($newLinks); +$product->save(); + +/** @var \Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement */ +$categoryLinkManagement = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Api\CategoryLinkManagementInterface::class); + +$categoryLinkManagement->assignProductToCategories( + $product->getSku(), + [2] +); diff --git a/dev/tests/integration/testsuite/Magento/GroupedProduct/_files/product_grouped_with_out_of_stock_rollback.php b/dev/tests/integration/testsuite/Magento/GroupedProduct/_files/product_grouped_with_out_of_stock_rollback.php new file mode 100644 index 000000000000..26a7487077e4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GroupedProduct/_files/product_grouped_with_out_of_stock_rollback.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +require 'product_grouped_rollback.php'; +require realpath(__DIR__ . '/../../') . '/Catalog/_files/product_virtual_out_of_stock_rollback.php'; +require realpath(__DIR__ . '/../../') . '/Catalog/_files/product_virtual_in_stock_rollback.php'; +require realpath(__DIR__ . '/../../') . '/Catalog/_files/product_associated_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/GroupedProduct/_files/product_grouped_with_simple_out_of_stock.php b/dev/tests/integration/testsuite/Magento/GroupedProduct/_files/product_grouped_with_simple_out_of_stock.php new file mode 100644 index 000000000000..6ef9b5cd5b0a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GroupedProduct/_files/product_grouped_with_simple_out_of_stock.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\GroupedProduct\Model\Product\Type\Grouped; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager() + ->get(ProductRepositoryInterface::class); + +$productLinkFactory = Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Api\Data\ProductLinkInterfaceFactory::class); +$productConfigs = [ + [ + 'id' => '100000001', + 'stock_config' => ['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1] + ], + [ + 'id' => '100000002', + 'stock_config' => ['use_config_manage_stock' => 1, 'qty' => 0, 'is_qty_decimal' => 0, 'is_in_stock' => 0] + ] +]; + +foreach ($productConfigs as $productConfig) { + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create(Product::class); + $product->setTypeId(Type::TYPE_SIMPLE) + ->setId($productConfig['id']) + ->setWebsiteIds([1]) + ->setAttributeSetId(4) + ->setName('Simple ' . $productConfig['id']) + ->setSku('simple_' . $productConfig['id']) + ->setPrice(100) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData($productConfig['stock_config']); + + $linkedProducts[] = $productRepository->save($product); +} + +/** @var $product Product */ +$product = Bootstrap::getObjectManager()->create(Product::class); + +$product->setTypeId(Grouped::TYPE_CODE) + ->setId('100000003') + ->setWebsiteIds([1]) + ->setAttributeSetId(4) + ->setName('Grouped Product') + ->setSku('grouped') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'is_in_stock' => 1]); + +foreach ($linkedProducts as $linkedProduct) { + /** @var \Magento\Catalog\Api\Data\ProductLinkInterface $productLink */ + $productLink = $productLinkFactory->create(); + $productLink->setSku($product->getSku()) + ->setLinkType('associated') + ->setLinkedProductSku($linkedProduct->getSku()) + ->setLinkedProductType($linkedProduct->getTypeId()) + ->getExtensionAttributes() + ->setQty(1); + $newLinks[] = $productLink; +} + +$product->setProductLinks($newLinks); + +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/GroupedProduct/_files/product_grouped_with_simple_out_of_stock_rollback.php b/dev/tests/integration/testsuite/Magento/GroupedProduct/_files/product_grouped_with_simple_out_of_stock_rollback.php new file mode 100644 index 000000000000..b81c008cf1ab --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GroupedProduct/_files/product_grouped_with_simple_out_of_stock_rollback.php @@ -0,0 +1,34 @@ +<?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); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$skuList = ['simple_100000001', 'simple_100000002', 'grouped']; +foreach ($skuList as $sku) { + try { + $product = $productRepository->get($sku, false, null, true); + + $stockStatus = $objectManager->create(\Magento\CatalogInventory\Model\Stock\Status::class); + $stockStatus->load($product->getData('entity_id'), 'product_id'); + $stockStatus->delete(); + + $productRepository->delete($product); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + //Product already removed + } +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Import/ValidateTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Import/ValidateTest.php index 552a23a0c1fd..a3cf42b48489 100644 --- a/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Import/ValidateTest.php +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Import/ValidateTest.php @@ -3,9 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\ImportExport\Controller\Adminhtml\Import; use Magento\Framework\Filesystem\DirectoryList; +use Magento\Framework\HTTP\Adapter\FileTransferFactory; use Magento\ImportExport\Model\Import; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; @@ -17,12 +19,15 @@ class ValidateTest extends \Magento\TestFramework\TestCase\AbstractBackendContro /** * @dataProvider validationDataProvider * @param string $fileName + * @param string $mimeType * @param string $message * @param string $delimiter + * @throws \Magento\Framework\Exception\FileSystemException * @backupGlobals enabled * @magentoDbIsolation enabled + * @SuppressWarnings(PHPMD.Superglobals) */ - public function testValidationReturn($fileName, $message, $delimiter) + public function testValidationReturn(string $fileName, string $mimeType, string $message, string $delimiter): void { $validationStrategy = ProcessingErrorAggregatorInterface::VALIDATION_STRATEGY_STOP_ON_ERROR; @@ -50,7 +55,7 @@ public function testValidationReturn($fileName, $message, $delimiter) $_FILES = [ 'import_file' => [ 'name' => $fileName, - 'type' => 'text/csv', + 'type' => $mimeType, 'tmp_name' => $target, 'error' => 0, 'size' => filesize($target) @@ -59,10 +64,7 @@ public function testValidationReturn($fileName, $message, $delimiter) $this->_objectManager->configure( [ - 'preferences' => [ - \Magento\Framework\HTTP\Adapter\FileTransferFactory::class => - \Magento\ImportExport\Controller\Adminhtml\Import\HttpFactoryMock::class - ] + 'preferences' => [FileTransferFactory::class => HttpFactoryMock::class] ] ); @@ -79,29 +81,39 @@ public function testValidationReturn($fileName, $message, $delimiter) /** * @return array */ - public function validationDataProvider() + public function validationDataProvider(): array { return [ [ 'file_name' => 'catalog_product.csv', + 'mime-type' => 'text/csv', 'message' => 'File is valid', 'delimiter' => ',', ], [ 'file_name' => 'test.txt', + 'mime-type' => 'text/csv', 'message' => '\'txt\' file extension is not supported', 'delimiter' => ',', ], [ 'file_name' => 'incorrect_catalog_product_comma.csv', + 'mime-type' => 'text/csv', 'message' => 'Download full report', 'delimiter' => ',', ], [ 'file_name' => 'incorrect_catalog_product_semicolon.csv', + 'mime-type' => 'text/csv', 'message' => 'Download full report', 'delimiter' => ';', ], + [ + 'file_name' => 'catalog_product.zip', + 'mime-type' => 'application/zip', + 'message' => 'File is valid', + 'delimiter' => ',', + ], ]; } } diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Import/_files/catalog_product.zip b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Import/_files/catalog_product.zip new file mode 100644 index 000000000000..812beae22b78 Binary files /dev/null and b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Import/_files/catalog_product.zip differ diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Model/ImportTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Model/ImportTest.php index 5ba955430021..93fa04806d57 100644 --- a/dev/tests/integration/testsuite/Magento/ImportExport/Model/ImportTest.php +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Model/ImportTest.php @@ -5,6 +5,7 @@ */ namespace Magento\ImportExport\Model; +use Magento\Framework\Phrase; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; /** @@ -33,9 +34,7 @@ class ImportTest extends \PHPUnit\Framework\TestCase 'catalog_product' => [ 'token' => \Magento\ImportExport\Model\Source\Import\Behavior\Basic::class, 'code' => 'basic_behavior', - 'notes' => [ - \Magento\ImportExport\Model\Import::BEHAVIOR_REPLACE => "Note: Product IDs will be regenerated." - ], + 'notes' => [], ], 'customer_composite' => [ 'token' => \Magento\ImportExport\Model\Source\Import\Behavior\Basic::class, @@ -70,7 +69,7 @@ protected function setUp() \Magento\ImportExport\Model\Import\Config::class ); $this->_model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\ImportExport\Model\Import::class, + Import::class, ['importConfig' => $this->_importConfig] ); } @@ -108,8 +107,8 @@ public function testValidateSource() $validationStrategy = ProcessingErrorAggregatorInterface::VALIDATION_STRATEGY_STOP_ON_ERROR; $this->_model->setEntity('catalog_product'); - $this->_model->setData(\Magento\ImportExport\Model\Import::FIELD_NAME_VALIDATION_STRATEGY, $validationStrategy); - $this->_model->setData(\Magento\ImportExport\Model\Import::FIELD_NAME_ALLOWED_ERROR_COUNT, 0); + $this->_model->setData(Import::FIELD_NAME_VALIDATION_STRATEGY, $validationStrategy); + $this->_model->setData(Import::FIELD_NAME_ALLOWED_ERROR_COUNT, 0); /** @var \Magento\ImportExport\Model\Import\AbstractSource|\PHPUnit_Framework_MockObject_MockObject $source */ $source = $this->getMockForAbstractClass( @@ -159,6 +158,8 @@ public function testGetEntityEntityIsNotSet() */ public function testGetEntityBehaviors() { + $this->prepareProductNotes(); + $importModel = $this->_model; $actualBehaviors = $importModel->getEntityBehaviors(); @@ -200,4 +201,24 @@ public function testGetUniqueEntityBehaviors() $this->assertEquals($behaviorClass, $actualBehaviors[$behaviorCode]); } } + + /** + * Add Catalog Product Notes to expected results. + * + * @return void + * @ SuppressWarnings(PHPMD.) + */ + private function prepareProductNotes(): void + { + $this->_entityBehaviors['catalog_product']['notes'] = + [ + Import::BEHAVIOR_APPEND => new Phrase('New product data is added to the existing product data for' + . ' the existing entries in the database. All fields except sku can be updated.'), + Import::BEHAVIOR_REPLACE => new Phrase('The existing product data is replaced with new data.' + . ' <b>Exercise caution when replacing data because the existing product data will be completely' + . ' cleared and all references in the system will be lost.</b>'), + Import::BEHAVIOR_DELETE => new Phrase('Any entities in the import data that already exist in the' + . ' database are deleted from the database.'), + ]; + } } diff --git a/dev/tests/integration/testsuite/Magento/Integration/Block/Adminhtml/System/Config/OauthSectionTest.php b/dev/tests/integration/testsuite/Magento/Integration/Block/Adminhtml/System/Config/OauthSectionTest.php new file mode 100644 index 000000000000..ac5d8005180b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Integration/Block/Adminhtml/System/Config/OauthSectionTest.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + + +namespace Magento\Integration\Block\Adminhtml\System\Config; + +class OauthSectionTest extends \Magento\TestFramework\TestCase\AbstractBackendController +{ + /** + * Checks that OAuth Section in the system config is loaded + */ + public function testOAuthSection() + { + $this->dispatch('backend/admin/system_config/edit/section/oauth/'); + $body = $this->getResponse()->getBody(); + $this->assertContains('id="oauth_access_token_lifetime-head"', $body); + $this->assertContains('id="oauth_cleanup-head"', $body); + $this->assertContains('id="oauth_consumer-head"', $body); + $this->assertContains('id="oauth_authentication_lock-head"', $body); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Integration/Controller/Adminhtml/IntegrationTest.php b/dev/tests/integration/testsuite/Magento/Integration/Controller/Adminhtml/IntegrationTest.php index 8011873577dc..4da0c12c6087 100644 --- a/dev/tests/integration/testsuite/Magento/Integration/Controller/Adminhtml/IntegrationTest.php +++ b/dev/tests/integration/testsuite/Magento/Integration/Controller/Adminhtml/IntegrationTest.php @@ -7,6 +7,7 @@ namespace Magento\Integration\Controller\Adminhtml; use Magento\TestFramework\Bootstrap; +use Magento\Framework\App\Request\Http as HttpRequest; /** * \Magento\Integration\Controller\Adminhtml\Integration @@ -20,6 +21,9 @@ class IntegrationTest extends \Magento\TestFramework\TestCase\AbstractBackendCon /** @var \Magento\Integration\Model\Integration */ private $_integration; + /** + * @inheritDoc + */ protected function setUp() { parent::setUp(); @@ -29,6 +33,9 @@ protected function setUp() $this->_integration = $integration->load('Fixture Integration', 'name'); } + /** + * Test view page. + */ public function testIndexAction() { $this->dispatch('backend/admin/integration/index'); @@ -44,6 +51,9 @@ public function testIndexAction() ); } + /** + * Test creation form. + */ public function testNewAction() { $this->dispatch('backend/admin/integration/new'); @@ -61,6 +71,9 @@ public function testNewAction() ); } + /** + * Test update form. + */ public function testEditAction() { $integrationId = $this->_integration->getId(); @@ -88,12 +101,16 @@ public function testEditAction() ); } + /** + * Test saving. + */ public function testSaveActionUpdateIntegration() { $integrationId = $this->_integration->getId(); $integrationName = $this->_integration->getName(); $this->getRequest()->setParam('id', $integrationId); $url = 'http://magento.ll/endpoint_url'; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue( [ 'name' => $integrationName, @@ -111,10 +128,14 @@ public function testSaveActionUpdateIntegration() $this->assertRedirect($this->stringContains('backend/admin/integration/index/')); } + /** + * Test saving. + */ public function testSaveActionNewIntegration() { $url = 'http://magento.ll/endpoint_url'; $integrationName = md5(rand()); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue( [ 'name' => $integrationName, diff --git a/dev/tests/integration/testsuite/Magento/Integration/Model/Config/Consolidated/_files/integration.php b/dev/tests/integration/testsuite/Magento/Integration/Model/Config/Consolidated/_files/integration.php index cc2841f1acf2..d72a1359dfa5 100644 --- a/dev/tests/integration/testsuite/Magento/Integration/Model/Config/Consolidated/_files/integration.php +++ b/dev/tests/integration/testsuite/Magento/Integration/Model/Config/Consolidated/_files/integration.php @@ -9,6 +9,7 @@ 'endpoint_url' => 'http://example.com/endpoint1', 'identity_link_url' => 'http://www.example.com/identity1', 'resource' => [ + 'Magento_Backend::admin', 'Magento_Customer::customer', 'Magento_Customer::manage', 'Magento_Sales::sales', @@ -26,6 +27,7 @@ 'endpoint_url' => 'http://example.com/integration2', 'identity_link_url' => 'http://www.example.com/identity2', 'resource' => [ + 'Magento_Backend::admin', 'Magento_Sales::sales', 'Magento_Sales::sales_operation', 'Magento_Sales::sales_order', @@ -40,6 +42,7 @@ 'TestIntegration3' => [ 'email' => 'test-integration3@example.com', 'resource' => [ + 'Magento_Backend::admin', 'Magento_Sales::sales', 'Magento_Sales::sales_operation', 'Magento_Sales::sales_order', diff --git a/dev/tests/integration/testsuite/Magento/Integration/Model/ConfigBasedIntegrationManagerTest.php b/dev/tests/integration/testsuite/Magento/Integration/Model/ConfigBasedIntegrationManagerTest.php new file mode 100644 index 000000000000..ee14b3ba53dd --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Integration/Model/ConfigBasedIntegrationManagerTest.php @@ -0,0 +1,126 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Integration\Model; + +/** + * Test class for \Magento\Integration\Model\ConfigBasedIntegrationManager.php. + */ +class ConfigBasedIntegrationManagerTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $consolidatedMock; + + /** + * @var \Magento\Integration\Model\ConfigBasedIntegrationManager + */ + protected $integrationManager; + + /** + * @var \Magento\Integration\Api\IntegrationServiceInterface + */ + protected $integrationService; + + /** + * @var \Magento\TestFramework\ObjectManager + */ + protected $objectManager; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->consolidatedMock = $this->createMock(\Magento\Integration\Model\ConsolidatedConfig::class); + $this->objectManager->addSharedInstance( + $this->consolidatedMock, + \Magento\Integration\Model\ConsolidatedConfig::class + ); + $this->integrationManager = $this->objectManager->create( + \Magento\Integration\Model\ConfigBasedIntegrationManager::class, + [] + ); + $this->integrationService = $this->objectManager->create( + \Magento\Integration\Api\IntegrationServiceInterface::class, + [] + ); + } + + /** + * @inheritdoc + */ + protected function tearDown() + { + $this->objectManager->removeSharedInstance(\Magento\Integration\Model\ConsolidatedConfig::class); + parent::tearDown(); + } + + /** + * @magentoDbIsolation enabled + */ + public function testProcessConfigBasedIntegrations() + { + $newIntegrations = require __DIR__ . '/Config/Consolidated/_files/integration.php'; + $this->consolidatedMock + ->expects($this->any()) + ->method('getIntegrations') + ->willReturn($newIntegrations); + + // Check that the integrations do not exist already + foreach ($newIntegrations as $integrationName => $integrationData) { + $integration = $this->integrationService->findByName($integrationName); + $this->assertEquals(null, $integration->getId(), 'Integration already exists'); + } + + // Create new integrations + $this->assertEquals( + $newIntegrations, + $this->integrationManager->processConfigBasedIntegrations($newIntegrations), + 'Error processing config based integrations.' + ); + $createdIntegrations = []; + + // Check that the integrations are new with "inactive" status + foreach ($newIntegrations as $integrationName => $integrationData) { + $integration = $this->integrationService->findByName($integrationName); + $this->assertNotEmpty($integration->getId(), 'Integration was not created'); + $this->assertEquals( + $integration::STATUS_INACTIVE, + $integration->getStatus(), + 'Integration is not created with "inactive" status' + ); + $createdIntegrations[$integrationName] = $integration; + } + + // Rerun integration creation with the same data (data has not changed) + $this->assertEquals( + $newIntegrations, + $this->integrationManager->processConfigBasedIntegrations($newIntegrations), + 'Error processing config based integrations.' + ); + + // Check that the integrations are not recreated when data has not actually changed + foreach ($newIntegrations as $integrationName => $integrationData) { + $integration = $this->integrationService->findByName($integrationName); + $this->assertEquals( + $createdIntegrations[$integrationName]->getId(), + $integration->getId(), + 'Integration ID has changed' + ); + $this->assertEquals( + $createdIntegrations[$integrationName]->getStatus(), + $integration->getStatus(), + 'Integration status has changed' + ); + } + } +} diff --git a/dev/tests/integration/testsuite/Magento/MessageQueue/Model/Cron/ConsumersRunnerTest.php b/dev/tests/integration/testsuite/Magento/MessageQueue/Model/Cron/ConsumersRunnerTest.php index 4acba63d98e6..2eda32e894d3 100644 --- a/dev/tests/integration/testsuite/Magento/MessageQueue/Model/Cron/ConsumersRunnerTest.php +++ b/dev/tests/integration/testsuite/Magento/MessageQueue/Model/Cron/ConsumersRunnerTest.php @@ -109,19 +109,6 @@ protected function setUp() }); } - /** - * Checks that pid files are created - * - * @return void - */ - public function testCheckThatPidFilesWasCreated() - { - $this->consumersRunner->run(); - foreach ($this->consumerConfig->getConsumers() as $consumer) { - $this->waitConsumerPidFile($consumer->getName()); - } - } - /** * Tests running of specific consumer and his re-running when it is working * @@ -185,23 +172,6 @@ public function testCronJobDisabled() } } - /** - * @param string $consumerName - * @return void - */ - private function waitConsumerPidFile($consumerName) - { - $pidFileFullPath = $this->getPidFileFullPath($consumerName); - $i = 0; - do { - sleep(1); - } while (!file_exists($pidFileFullPath) && ($i++ < 60)); - - if (!file_exists($pidFileFullPath)) { - $this->fail($consumerName . ' pid file does not exist.'); - } - } - /** * @return array */ diff --git a/dev/tests/integration/testsuite/Magento/MysqlMq/Model/DataObject.php b/dev/tests/integration/testsuite/Magento/MysqlMq/Model/DataObject.php deleted file mode 100644 index 31843be00a54..000000000000 --- a/dev/tests/integration/testsuite/Magento/MysqlMq/Model/DataObject.php +++ /dev/null @@ -1,43 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\MysqlMq\Model; - -class DataObject extends \Magento\Framework\Api\AbstractExtensibleObject -{ - /** - * @return string - */ - public function getName() - { - return $this->_get('name'); - } - - /** - * @param string $name - * @return $this - */ - public function setName($name) - { - return $this->setData('name', $name); - } - - /** - * @return int|null - */ - public function getEntityId() - { - return $this->_get('entity_id'); - } - - /** - * @param int $entityId - * @return $this - */ - public function setEntityId($entityId) - { - return $this->setData('entity_id', $entityId); - } -} diff --git a/dev/tests/integration/testsuite/Magento/MysqlMq/Model/DataObjectRepository.php b/dev/tests/integration/testsuite/Magento/MysqlMq/Model/DataObjectRepository.php deleted file mode 100644 index 879872315ec5..000000000000 --- a/dev/tests/integration/testsuite/Magento/MysqlMq/Model/DataObjectRepository.php +++ /dev/null @@ -1,25 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\MysqlMq\Model; - -class DataObjectRepository -{ - /** - * @param DataObject $dataObject - * @param string $requiredParam - * @param int|null $optionalParam - * @return bool - */ - public function delayedOperation( - \Magento\MysqlMq\Model\DataObject $dataObject, - $requiredParam, - $optionalParam = null - ) { - echo "Processed '{$dataObject->getEntityId()}'; " - . "Required param '{$requiredParam}'; Optional param '{$optionalParam}'\n"; - return true; - } -} diff --git a/dev/tests/integration/testsuite/Magento/MysqlMq/Model/Processor.php b/dev/tests/integration/testsuite/Magento/MysqlMq/Model/Processor.php deleted file mode 100644 index 3b2a76104a2c..000000000000 --- a/dev/tests/integration/testsuite/Magento/MysqlMq/Model/Processor.php +++ /dev/null @@ -1,28 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\MysqlMq\Model; - -/** - * Test message processor is used by \Magento\MysqlMq\Model\PublisherTest - */ -class Processor -{ - /** - * @param \Magento\MysqlMq\Model\DataObject $message - */ - public function processMessage($message) - { - echo "Processed {$message->getEntityId()}\n"; - } - - /** - * @param \Magento\MysqlMq\Model\DataObject $message - */ - public function processMessageWithException($message) - { - throw new \LogicException("Exception during message processing happened. Entity: {{$message->getEntityId()}}"); - } -} diff --git a/dev/tests/integration/testsuite/Magento/MysqlMq/Model/PublisherConsumerTest.php b/dev/tests/integration/testsuite/Magento/MysqlMq/Model/PublisherConsumerTest.php index f03d03d3a25f..f911165bd27f 100644 --- a/dev/tests/integration/testsuite/Magento/MysqlMq/Model/PublisherConsumerTest.php +++ b/dev/tests/integration/testsuite/Magento/MysqlMq/Model/PublisherConsumerTest.php @@ -3,87 +3,45 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\MysqlMq\Model; -use Magento\Framework\MessageQueue\PublisherInterface; +use Magento\Framework\MessageQueue\UseCase\QueueTestCaseAbstract; +use Magento\MysqlMq\Model\ResourceModel\MessageCollection; +use Magento\MysqlMq\Model\ResourceModel\MessageStatusCollection; /** * Test for MySQL publisher class. * * @magentoDbIsolation disabled */ -class PublisherConsumerTest extends \PHPUnit\Framework\TestCase +class PublisherConsumerTest extends QueueTestCaseAbstract { const MAX_NUMBER_OF_TRIALS = 3; /** - * @var \Magento\Framework\MessageQueue\PublisherInterface + * @var string[] */ - protected $publisher; - - /** - * @var \Magento\Framework\ObjectManagerInterface - */ - protected $objectManager; - - protected function setUp() - { - $this->markTestIncomplete('Should be converted to queue config v2.'); - $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - - $configPath = __DIR__ . '/../etc/queue.xml'; - $fileResolverMock = $this->createMock(\Magento\Framework\Config\FileResolverInterface::class); - $fileResolverMock->expects($this->any()) - ->method('get') - ->willReturn([$configPath => file_get_contents(($configPath))]); - - /** @var \Magento\Framework\MessageQueue\Config\Reader\Xml $xmlReader */ - $xmlReader = $this->objectManager->create( - \Magento\Framework\MessageQueue\Config\Reader\Xml::class, - ['fileResolver' => $fileResolverMock] - ); - - $newData = $xmlReader->read(); - - /** @var \Magento\Framework\MessageQueue\Config\Data $configData */ - $configData = $this->objectManager->get(\Magento\Framework\MessageQueue\Config\Data::class); - $configData->reset(); - $configData->merge($newData); - - $this->publisher = $this->objectManager->create(\Magento\Framework\MessageQueue\PublisherInterface::class); - } - - protected function tearDown() - { - $this->markTestIncomplete('Should be converted to queue config v2.'); - $this->consumeMessages('demoConsumerQueueOne', PHP_INT_MAX); - $this->consumeMessages('demoConsumerQueueTwo', PHP_INT_MAX); - $this->consumeMessages('demoConsumerQueueThree', PHP_INT_MAX); - $this->consumeMessages('demoConsumerQueueFour', PHP_INT_MAX); - $this->consumeMessages('demoConsumerQueueFive', PHP_INT_MAX); - $this->consumeMessages('demoConsumerQueueOneWithException', PHP_INT_MAX); - - $objectManagerConfiguration = [\Magento\Framework\MessageQueue\Config\Reader\Xml::class => [ - 'arguments' => [ - 'fileResolver' => ['instance' => \Magento\Framework\Config\FileResolverInterface::class], - ], - ], - ]; - $this->objectManager->configure($objectManagerConfiguration); - /** @var \Magento\Framework\MessageQueue\Config\Data $queueConfig */ - $queueConfig = $this->objectManager->get(\Magento\Framework\MessageQueue\Config\Data::class); - $queueConfig->reset(); - } + protected $consumers = [ + 'demoConsumerQueueOne', + 'demoConsumerQueueTwo', + 'demoConsumerQueueThree', + 'delayedOperationConsumer', + 'demoConsumerWithException' + ]; /** * @magentoDataFixture Magento/MysqlMq/_files/queues.php */ public function testPublishConsumeFlow() { - /** @var \Magento\MysqlMq\Model\DataObjectFactory $objectFactory */ - $objectFactory = $this->objectManager->create(\Magento\MysqlMq\Model\DataObjectFactory::class); - /** @var \Magento\MysqlMq\Model\DataObject $object */ + /** @var \Magento\TestModuleMysqlMq\Model\DataObjectFactory $objectFactory */ + $objectFactory = $this->objectManager->create(\Magento\TestModuleMysqlMq\Model\DataObjectFactory::class); + /** @var \Magento\TestModuleMysqlMq\Model\DataObject $object */ $object = $objectFactory->create(); + $object->setOutputPath($this->logFilePath); + file_put_contents($this->logFilePath, ''); for ($i = 0; $i < 10; $i++) { $object->setName('Object name ' . $i)->setEntityId($i); $this->publisher->publish('demo.object.created', $object); @@ -96,105 +54,87 @@ public function testPublishConsumeFlow() $object->setName('Object name ' . $i)->setEntityId($i); $this->publisher->publish('demo.object.custom.created', $object); } - - $outputPattern = '/(Processed \d+\s)/'; - /** There are total of 10 messages in the first queue, total expected consumption is 7, 3 then 0 */ - $this->consumeMessages('demoConsumerQueueOne', 7, 7, $outputPattern); - /** Consumer all messages which left in this queue */ - $this->consumeMessages('demoConsumerQueueOne', PHP_INT_MAX, 3, $outputPattern); - $this->consumeMessages('demoConsumerQueueOne', 7, 0, $outputPattern); - - /** Verify that messages were added correctly to second queue for update and create topics */ - $this->consumeMessages('demoConsumerQueueTwo', 20, 15, $outputPattern); - - /** Verify that messages were NOT added to fourth queue */ - $this->consumeMessages('demoConsumerQueueFour', 11, 0, $outputPattern); - - /** Verify that messages were added correctly by '*' pattern in bind config to third queue */ - $this->consumeMessages('demoConsumerQueueThree', 20, 15, $outputPattern); - - /** Verify that messages were added correctly by '#' pattern in bind config to fifth queue */ - $this->consumeMessages('demoConsumerQueueFive', 20, 18, $outputPattern); + $this->waitForAsynchronousResult(18, $this->logFilePath); + + //Check lines in file + $createdPattern = '/Processed object created \d+/'; + $updatedPattern = '/Processed object updated \d+/'; + $customCreatedPattern = '/Processed custom object created \d+/'; + $logFileContents = file_get_contents($this->logFilePath); + + preg_match_all($createdPattern, $logFileContents, $createdMatches); + $this->assertEquals(10, count($createdMatches[0])); + preg_match_all($updatedPattern, $logFileContents, $updatedMatches); + $this->assertEquals(5, count($updatedMatches[0])); + preg_match_all($customCreatedPattern, $logFileContents, $customCreatedMatches); + $this->assertEquals(3, count($customCreatedMatches[0])); } /** * @magentoDataFixture Magento/MysqlMq/_files/queues.php */ - public function testPublishAndConsumeWithFailedJobs() + public function testPublishAndConsumeSchemaDefinedByMethod() { - /** @var \Magento\MysqlMq\Model\DataObjectFactory $objectFactory */ - $objectFactory = $this->objectManager->create(\Magento\MysqlMq\Model\DataObjectFactory::class); - /** @var \Magento\MysqlMq\Model\DataObject $object */ - /** Try consume messages for MAX_NUMBER_OF_TRIALS and then consumer them without exception */ + $topic = 'test.schema.defined.by.method'; + /** @var \Magento\TestModuleMysqlMq\Model\DataObjectFactory $objectFactory */ + $objectFactory = $this->objectManager->create(\Magento\TestModuleMysqlMq\Model\DataObjectFactory::class); + /** @var \Magento\TestModuleMysqlMq\Model\DataObject $object */ $object = $objectFactory->create(); - for ($i = 0; $i < 5; $i++) { - $object->setName('Object name ' . $i)->setEntityId($i); - $this->publisher->publish('demo.object.created', $object); - } - $outputPattern = '/(Processed \d+\s)/'; - for ($i = 0; $i < self::MAX_NUMBER_OF_TRIALS; $i++) { - $this->consumeMessages('demoConsumerQueueOneWithException', PHP_INT_MAX, 0, $outputPattern); - } - $this->consumeMessages('demoConsumerQueueOne', PHP_INT_MAX, 0, $outputPattern); + $id = 33; + $object->setName('Object name ' . $id)->setEntityId($id); + $object->setOutputPath($this->logFilePath); + $requiredStringParam = 'Required value'; + $optionalIntParam = 44; + $this->publisher->publish($topic, [$object, $requiredStringParam, $optionalIntParam]); - /** Try consume messages for MAX_NUMBER_OF_TRIALS+1 and then consumer them without exception */ - for ($i = 0; $i < 5; $i++) { - $object->setName('Object name ' . $i)->setEntityId($i); - $this->publisher->publish('demo.object.created', $object); - } - /** Try consume messages for MAX_NUMBER_OF_TRIALS and then consumer them without exception */ - for ($i = 0; $i < self::MAX_NUMBER_OF_TRIALS + 1; $i++) { - $this->consumeMessages('demoConsumerQueueOneWithException', PHP_INT_MAX, 0, $outputPattern); - } - /** Make sure that messages are not accessible anymore after number of trials is exceeded */ - $this->consumeMessages('demoConsumerQueueOne', PHP_INT_MAX, 0, $outputPattern); + $expectedOutput = "Processed '{$object->getEntityId()}'; " + . "Required param '{$requiredStringParam}'; Optional param '{$optionalIntParam}'"; + + $this->waitForAsynchronousResult(1, $this->logFilePath); + + $this->assertEquals($expectedOutput, trim(file_get_contents($this->logFilePath))); } /** * @magentoDataFixture Magento/MysqlMq/_files/queues.php */ - public function testPublishAndConsumeSchemaDefinedByMethod() + public function testConsumeWithException() { - /** @var \Magento\MysqlMq\Model\DataObjectFactory $objectFactory */ - $objectFactory = $this->objectManager->create(\Magento\MysqlMq\Model\DataObjectFactory::class); - /** @var \Magento\MysqlMq\Model\DataObject $object */ + $topic = 'demo.exception'; + /** @var \Magento\TestModuleMysqlMq\Model\DataObjectFactory $objectFactory */ + $objectFactory = $this->objectManager->create(\Magento\TestModuleMysqlMq\Model\DataObjectFactory::class); + /** @var \Magento\TestModuleMysqlMq\Model\DataObject $object */ $object = $objectFactory->create(); - $id = 33; + $id = 99; + $object->setName('Object name ' . $id)->setEntityId($id); - $requiredStringParam = 'Required value'; - $optionalIntParam = 44; - $this->publisher->publish('test.schema.defined.by.method', [$object, $requiredStringParam, $optionalIntParam]); - $outputPattern = "/Processed '{$object->getEntityId()}'; " - . "Required param '{$requiredStringParam}'; Optional param '{$optionalIntParam}'/"; - $this->consumeMessages('delayedOperationConsumer', PHP_INT_MAX, 1, $outputPattern); + $object->setOutputPath($this->logFilePath); + $this->publisher->publish($topic, $object); + $expectedOutput = "Exception processing {$id}"; + $this->waitForAsynchronousResult(1, $this->logFilePath); + $message = $this->getTopicLatestMessage($topic); + $this->assertEquals($expectedOutput, trim(file_get_contents($this->logFilePath))); + $this->assertEquals(QueueManagement::MESSAGE_STATUS_ERROR, $message->getStatus()); } /** - * Make sure that consumers consume correct number of messages. - * - * @param string $consumerName - * @param int|null $messagesToProcess - * @param int|null $expectedNumberOfProcessedMessages - * @param string|null $outputPattern + * @param string $topic + * @return Message */ - protected function consumeMessages( - $consumerName, - $messagesToProcess, - $expectedNumberOfProcessedMessages = null, - $outputPattern = null - ) { - /** @var \Magento\Framework\MessageQueue\ConsumerFactory $consumerFactory */ - $consumerFactory = $this->objectManager->create(\Magento\Framework\MessageQueue\ConsumerFactory::class); - $consumer = $consumerFactory->get($consumerName); - ob_start(); - $consumer->process($messagesToProcess); - $consumersOutput = ob_get_contents(); - ob_end_clean(); - if ($outputPattern) { - $this->assertEquals( - $expectedNumberOfProcessedMessages, - preg_match_all($outputPattern, $consumersOutput) - ); - } + private function getTopicLatestMessage(string $topic) : Message + { + // Assert message status is error + $messageCollection = $this->objectManager->create(MessageCollection::class); + $messageStatusCollection = $this->objectManager->create(MessageStatusCollection::class); + + $messageCollection->addFilter('topic_name', $topic); + $messageCollection->join( + ['status' => $messageStatusCollection->getMainTable()], + "status.message_id = main_table.id" + ); + $messageCollection->addOrder('updated_at', MessageCollection::SORT_ORDER_DESC); + + $message = $messageCollection->getFirstItem(); + return $message; } } diff --git a/dev/tests/integration/testsuite/Magento/MysqlMq/Model/QueueManagementTest.php b/dev/tests/integration/testsuite/Magento/MysqlMq/Model/QueueManagementTest.php index 197df2923329..56dd77d3da17 100644 --- a/dev/tests/integration/testsuite/Magento/MysqlMq/Model/QueueManagementTest.php +++ b/dev/tests/integration/testsuite/Magento/MysqlMq/Model/QueueManagementTest.php @@ -5,8 +5,6 @@ */ namespace Magento\MysqlMq\Model; -use Magento\MysqlMq\Model\QueueManagement; - /** * Test for Queue Management class. */ diff --git a/dev/tests/integration/testsuite/Magento/MysqlMq/etc/queue.xml b/dev/tests/integration/testsuite/Magento/MysqlMq/etc/queue.xml deleted file mode 100644 index fd618d504df0..000000000000 --- a/dev/tests/integration/testsuite/Magento/MysqlMq/etc/queue.xml +++ /dev/null @@ -1,67 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/queue.xsd"> - <publisher name="demo-publisher-1" connection="db" exchange="magento"/> - <publisher name="demo-publisher-2" connection="db" exchange="magento"/> - - <publisher name="test-publisher-1" connection="amqp" exchange="magento"/> - <publisher name="test-publisher-3" connection="amqp" exchange="test-exchange-1"/> - - <topic name="demo.object.created" schema="Magento\MysqlMq\Model\DataObject" publisher="demo-publisher-1"/> - <topic name="demo.object.updated" schema="Magento\MysqlMq\Model\DataObject" publisher="demo-publisher-2"/> - <topic name="demo.object.custom.created" schema="Magento\MysqlMq\Model\DataObject" publisher="demo-publisher-2"/> - - <topic name="test.schema.defined.by.method" schema="Magento\MysqlMq\Model\DataObjectRepository::delayedOperation" publisher="demo-publisher-2"/> - - <topic name="customer.created" schema="Magento\Customer\Api\Data\CustomerInterface" publisher="test-publisher-1"/> - <topic name="customer.created.one" schema="Magento\Customer\Api\Data\CustomerInterface" publisher="test-publisher-1"/> - <topic name="customer.created.one.two" schema="Magento\Customer\Api\Data\CustomerInterface" publisher="test-publisher-1"/> - <topic name="customer.created.two" schema="Magento\Customer\Api\Data\CustomerInterface" publisher="test-publisher-1"/> - <topic name="customer.updated" schema="Magento\Customer\Api\Data\CustomerInterface" publisher="demo-publisher-2"/> - <topic name="customer.deleted" schema="Magento\Customer\Api\Data\CustomerInterface" publisher="demo-publisher-2"/> - <topic name="cart.created" schema="Magento\Quote\Api\Data\CartInterface" publisher="test-publisher-3"/> - <topic name="cart.created.one" schema="Magento\Quote\Api\Data\CartInterface" publisher="test-publisher-3"/> - - <consumer name="demoConsumerQueueOne" queue="demo-queue-1" connection="db" class="\Magento\MysqlMq\Model\Processor" method="processMessage"/> - <consumer name="demoConsumerQueueOneWithException" queue="demo-queue-1" connection="db" class="\Magento\MysqlMq\Model\Processor" method="processMessageWithException"/> - <consumer name="demoConsumerQueueTwo" queue="demo-queue-2" connection="db" class="\Magento\MysqlMq\Model\Processor" method="processMessage"/> - <consumer name="demoConsumerQueueThree" queue="demo-queue-3" connection="db" class="\Magento\MysqlMq\Model\Processor" method="processMessage"/> - <consumer name="demoConsumerQueueFour" queue="demo-queue-4" connection="db" class="\Magento\MysqlMq\Model\Processor" method="processMessage"/> - <consumer name="demoConsumerQueueFive" queue="demo-queue-5" connection="db" class="\Magento\MysqlMq\Model\Processor" method="processMessage"/> - - <consumer name="customerCreatedListener" queue="test-queue-1" connection="amqp" class="Magento\MysqlMq\Model\Processor" method="processMessage"/> - <consumer name="customerDeletedListener" queue="test-queue-2" connection="db" class="Magento\MysqlMq\Model\Processor" method="processMessage" max_messages="98765"/> - <consumer name="cartCreatedListener" queue="test-queue-3" connection="amqp" class="Magento\MysqlMq\Model\Processor" method="processMessage"/> - - <consumer name="delayedOperationConsumer" queue="demo-queue-6" connection="db" class="Magento\MysqlMq\Model\DataObjectRepository" method="delayedOperation"/> - - <bind queue="demo-queue-1" exchange="magento" topic="demo.object.created"/> - <bind queue="demo-queue-2" exchange="magento" topic="demo.object.created"/> - <bind queue="demo-queue-2" exchange="magento" topic="demo.object.updated"/> - <bind queue="demo-queue-3" exchange="magento" topic="demo.object.*"/> - <bind queue="demo-queue-5" exchange="magento" topic="demo.object.#"/> - - <bind queue="demo-queue-6" exchange="magento" topic="test.schema.defined.by.method"/> - - <bind queue="test-queue-1" exchange="magento" topic="customer.created"/> - <bind queue="test-queue-1" exchange="magento" topic="customer.created.one"/> - <bind queue="test-queue-1" exchange="magento" topic="customer.created.one.two"/> - <bind queue="test-queue-1" exchange="magento" topic="customer.created.two"/> - <bind queue="test-queue-1" exchange="magento" topic="customer.updated"/> - <bind queue="test-queue-1" exchange="test-exchange-1" topic="cart.created"/> - <bind queue="test-queue-2" exchange="magento" topic="customer.created"/> - <bind queue="test-queue-2" exchange="magento" topic="customer.deleted"/> - <bind queue="test-queue-3" exchange="magento" topic="cart.created"/> - <bind queue="test-queue-3" exchange="magento" topic="cart.created.one"/> - <bind queue="test-queue-3" exchange="test-exchange-1" topic="cart.created"/> - <bind queue="test-queue-4" exchange="magento" topic="customer.*"/> - <bind queue="test-queue-5" exchange="magento" topic="customer.#"/> - <bind queue="test-queue-7" exchange="magento" topic="*.created.*"/> - <bind queue="test-queue-8" exchange="magento" topic="*.created.#"/> - <bind queue="test-queue-9" exchange="magento" topic="#"/> -</config> diff --git a/dev/tests/integration/testsuite/Magento/NewRelicReporting/Model/Module/CollectTest.php b/dev/tests/integration/testsuite/Magento/NewRelicReporting/Model/Module/CollectTest.php new file mode 100644 index 000000000000..5e5051163cc1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/NewRelicReporting/Model/Module/CollectTest.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\NewRelicReporting\Model\Module; + +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/*** + * Class CollectTest + */ +class CollectTest extends TestCase +{ + /** + * @var Collect + */ + private $collect; + + /** + * @inheritDoc + */ + protected function setUp() + { + $this->collect = Bootstrap::getObjectManager()->create(Collect::class); + } + + /** + * @return void + */ + public function testReport() + { + $this->collect->getModuleData(); + $moduleData = $this->collect->getModuleData(); + $this->assertEmpty($moduleData['changes']); + } +} diff --git a/dev/tests/integration/testsuite/Magento/NewRelicReporting/Plugin/SeparateAppsTest.php b/dev/tests/integration/testsuite/Magento/NewRelicReporting/Plugin/SeparateAppsTest.php new file mode 100644 index 000000000000..9271e0894227 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/NewRelicReporting/Plugin/SeparateAppsTest.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\NewRelicReporting\Plugin; + +use Magento\Framework\App\State; +use Magento\NewRelicReporting\Model\NewRelicWrapper; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Class SeparateAppsTest + */ +class SeparateAppsTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = Bootstrap::getObjectManager(); + } + + /** + * @magentoConfigFixture default/newrelicreporting/general/enable 1 + * @magentoConfigFixture default/newrelicreporting/general/app_name beverly_hills + * @magentoConfigFixture default/newrelicreporting/general/separate_apps 1 + */ + public function testAppNameIsSetWhenConfiguredCorrectly() + { + $newRelicWrapper = $this->getMockBuilder(NewRelicWrapper::class) + ->setMethods(['setAppName']) + ->getMock(); + + $this->objectManager->configure([NewRelicWrapper::class => ['shared' => true]]); + $this->objectManager->addSharedInstance($newRelicWrapper, NewRelicWrapper::class); + + $newRelicWrapper->expects($this->once()) + ->method('setAppName') + ->with($this->equalTo('beverly_hills;beverly_hills_90210')); + + $state = $this->objectManager->get(State::class); + + $state->setAreaCode('90210'); + } + + /** + * @magentoConfigFixture default/newrelicreporting/general/enable 1 + * @magentoConfigFixture default/newrelicreporting/general/app_name beverly_hills + * @magentoConfigFixture default/newrelicreporting/general/separate_apps 0 + */ + public function testAppNameIsNotSetWhenDisabled() + { + $newRelicWrapper = $this->getMockBuilder(NewRelicWrapper::class) + ->setMethods(['setAppName']) + ->getMock(); + + $this->objectManager->configure([NewRelicWrapper::class => ['shared' => true]]); + $this->objectManager->addSharedInstance($newRelicWrapper, NewRelicWrapper::class); + + $newRelicWrapper->expects($this->never())->method('setAppName'); + + $state = $this->objectManager->get(State::class); + + $state->setAreaCode('90210'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Newsletter/Block/Adminhtml/Subscriber/GridTest.php b/dev/tests/integration/testsuite/Magento/Newsletter/Block/Adminhtml/Subscriber/GridTest.php new file mode 100644 index 000000000000..48d3356525f4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Newsletter/Block/Adminhtml/Subscriber/GridTest.php @@ -0,0 +1,105 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Newsletter\Block\Adminhtml\Subscriber; + +/** + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + * + * @see \Magento\Newsletter\Block\Adminhtml\Subscriber\Grid + */ +class GridTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var null|\Magento\Framework\ObjectManagerInterface + */ + private $objectManager = null; + /** + * @var null|\Magento\Framework\View\LayoutInterface + */ + private $layout = null; + + /** + * Set up layout. + */ + protected function setUp() + { + parent::setUp(); + + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + + $this->layout = $this->objectManager->create(\Magento\Framework\View\LayoutInterface::class); + $this->layout->getUpdate()->load('newsletter_subscriber_grid'); + $this->layout->generateXml(); + $this->layout->generateElements(); + } + + /** + * Check if mass action block exists. + */ + public function testMassActionBlockExists() + { + $this->assertNotFalse( + $this->getMassActionBlock(), + 'Mass action block does not exist in the grid, or it name was changed.' + ); + } + + /** + * Check if mass action id field is correct. + */ + public function testMassActionFieldIdIsCorrect() + { + $this->assertEquals( + 'subscriber_id', + $this->getMassActionBlock()->getMassactionIdField(), + 'Mass action id field is incorrect.' + ); + } + + /** + * Check if function returns correct result. + * + * @magentoDataFixture Magento/Newsletter/_files/subscribers.php + */ + public function testMassActionBlockContainsCorrectIdList() + { + $this->assertEquals( + implode(',', $this->getAllSubscriberIdList()), + $this->getMassActionBlock()->getGridIdsJson(), + 'Function returns incorrect result.' + ); + } + + /** + * Retrieve mass action block. + * + * @return bool|\Magento\Backend\Block\Widget\Grid\Massaction + */ + private function getMassActionBlock() + { + return $this->layout->getBlock('adminhtml.newslettrer.subscriber.grid.massaction'); + } + + /** + * Retrieve list of id of all subscribers. + * + * @return array + */ + private function getAllSubscriberIdList() + { + /** @var \Magento\Framework\App\ResourceConnection $resourceConnection */ + $resourceConnection = $this->objectManager->get(\Magento\Framework\App\ResourceConnection::class); + $select = $resourceConnection->getConnection() + ->select() + ->from($resourceConnection->getTableName('newsletter_subscriber')) + ->columns(['subscriber_id' => 'subscriber_id']); + + return $resourceConnection->getConnection()->fetchCol($select); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Newsletter/Controller/Adminhtml/NewsletterQueueTest.php b/dev/tests/integration/testsuite/Magento/Newsletter/Controller/Adminhtml/NewsletterQueueTest.php index 5c1a11756c1b..7a0ac030d120 100644 --- a/dev/tests/integration/testsuite/Magento/Newsletter/Controller/Adminhtml/NewsletterQueueTest.php +++ b/dev/tests/integration/testsuite/Magento/Newsletter/Controller/Adminhtml/NewsletterQueueTest.php @@ -5,6 +5,8 @@ */ namespace Magento\Newsletter\Controller\Adminhtml; +use Magento\Framework\App\Request\Http as HttpRequest; + /** * @magentoAppArea adminhtml */ @@ -15,6 +17,9 @@ class NewsletterQueueTest extends \Magento\TestFramework\TestCase\AbstractBacken */ protected $_model; + /** + * @inheritDoc + */ protected function setUp() { parent::setUp(); @@ -23,6 +28,9 @@ protected function setUp() ); } + /** + * @inheritDoc + */ protected function tearDown() { /** @@ -47,6 +55,7 @@ public function testSaveActionQueueTemplateAndVerifySuccessMessage() 'subject' => 'test subject', 'text' => 'newsletter text', ]; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($postForQueue); // Loading by code, since ID will vary. template_code is not actually used to load anywhere else. diff --git a/dev/tests/integration/testsuite/Magento/Newsletter/Controller/Adminhtml/NewsletterTemplateTest.php b/dev/tests/integration/testsuite/Magento/Newsletter/Controller/Adminhtml/NewsletterTemplateTest.php index 50e89d92e434..ae57703f9e8e 100644 --- a/dev/tests/integration/testsuite/Magento/Newsletter/Controller/Adminhtml/NewsletterTemplateTest.php +++ b/dev/tests/integration/testsuite/Magento/Newsletter/Controller/Adminhtml/NewsletterTemplateTest.php @@ -20,6 +20,9 @@ class NewsletterTemplateTest extends \Magento\TestFramework\TestCase\AbstractBac */ protected $model; + /** + * @inheritDoc + */ protected function setUp() { parent::setUp(); @@ -39,6 +42,9 @@ protected function setUp() ); } + /** + * @inheritDoc + */ protected function tearDown() { /** @@ -138,19 +144,6 @@ public function testSaveActionTemplateWithGetAndVerifyRedirect() $this->getRequest()->setMethod(\Zend\Http\Request::METHOD_GET)->setParam('id', $this->model->getId()); $this->dispatch('backend/newsletter/template/save'); - /** - * Check that errors was generated and set to session - */ - $this->assertSessionMessages($this->isEmpty(), \Magento\Framework\Message\MessageInterface::TYPE_ERROR); - - /** - * Check that correct redirect performed. - */ - $backendUrlModel = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Backend\Model\UrlInterface::class - ); - $backendUrlModel->turnOffSecretKey(); - $url = $backendUrlModel->getUrl('newsletter'); - $this->assertRedirect($this->stringStartsWith($url)); + $this->assertEquals(404, $this->getResponse()->getStatusCode()); } } diff --git a/dev/tests/integration/testsuite/Magento/Newsletter/Controller/ManageTest.php b/dev/tests/integration/testsuite/Magento/Newsletter/Controller/ManageTest.php index 5a094eb05c77..175c1c7c6c66 100644 --- a/dev/tests/integration/testsuite/Magento/Newsletter/Controller/ManageTest.php +++ b/dev/tests/integration/testsuite/Magento/Newsletter/Controller/ManageTest.php @@ -40,7 +40,7 @@ protected function setUp() protected function tearDown() { $this->customerSession->setCustomerId(null); - $this->coreSession->unsData('_form_key'); + $this->coreSession->unsetData('_form_key'); } /** diff --git a/dev/tests/integration/testsuite/Magento/Newsletter/Controller/SubscriberTest.php b/dev/tests/integration/testsuite/Magento/Newsletter/Controller/SubscriberTest.php index 5347881f5e7d..9dbf5c4d2a2a 100644 --- a/dev/tests/integration/testsuite/Magento/Newsletter/Controller/SubscriberTest.php +++ b/dev/tests/integration/testsuite/Magento/Newsletter/Controller/SubscriberTest.php @@ -57,7 +57,7 @@ public function testNewActionUsedEmail() $this->dispatch('newsletter/subscriber/new'); $this->assertSessionMessages($this->equalTo([ - 'There was a problem with the subscription: This email address is already assigned to another user.', + 'Thank you for your subscription.', ])); $this->assertRedirect($this->anything()); } diff --git a/dev/tests/integration/testsuite/Magento/Newsletter/Model/Plugin/PluginTest.php b/dev/tests/integration/testsuite/Magento/Newsletter/Model/Plugin/PluginTest.php index ab38dcf158c5..dbc666e49b6c 100644 --- a/dev/tests/integration/testsuite/Magento/Newsletter/Model/Plugin/PluginTest.php +++ b/dev/tests/integration/testsuite/Magento/Newsletter/Model/Plugin/PluginTest.php @@ -63,14 +63,14 @@ public function testCustomerCreated() ->setFirstname('Firstname') ->setLastname('Lastname') ->setEmail('customer_two@example.com'); - $this->customerRepository->save( + $createdCustomer = $this->customerRepository->save( $customerDataObject, $this->accountManagement->getPasswordHash('password') ); $subscriber->loadByEmail('customer_two@example.com'); $this->assertTrue($subscriber->isSubscribed()); - $this->assertEquals(0, (int)$subscriber->getCustomerId()); + $this->assertEquals((int)$createdCustomer->getId(), (int)$subscriber->getCustomerId()); } /** @@ -167,4 +167,42 @@ private function verifySubscriptionNotExist($email) $this->assertEquals(0, (int)$subscriber->getId()); return $subscriber; } + + /** + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ + public function testCustomerWithZeroStoreIdIsSubscribed() + { + $objectManager = Bootstrap::getObjectManager(); + + $currentStore = $objectManager->get( + \Magento\Store\Model\StoreManagerInterface::class + )->getStore()->getId(); + + $subscriber = $objectManager->create(\Magento\Newsletter\Model\Subscriber::class); + /** @var \Magento\Newsletter\Model\Subscriber $subscriber */ + $subscriber->setStoreId($currentStore) + ->setCustomerId(0) + ->setSubscriberEmail('customer@example.com') + ->setSubscriberStatus(\Magento\Newsletter\Model\Subscriber::STATUS_SUBSCRIBED) + ->save(); + + /** @var \Magento\Customer\Api\Data\CustomerInterfaceFactory $customerFactory */ + $customerFactory = $objectManager->get(\Magento\Customer\Api\Data\CustomerInterfaceFactory::class); + $customerDataObject = $customerFactory->create() + ->setFirstname('Firstname') + ->setLastname('Lastname') + ->setStoreId(0) + ->setEmail('customer@example.com'); + /** @var \Magento\Customer\Api\Data\CustomerInterface $customer */ + $customer = $this->accountManagement->createAccount($customerDataObject); + + $this->customerRepository->save($customer); + + $subscriber->loadByEmail('customer@example.com'); + + $this->assertEquals($customer->getId(), (int)$subscriber->getCustomerId()); + $this->assertEquals($currentStore, (int)$subscriber->getStoreId()); + } } diff --git a/dev/tests/integration/testsuite/Magento/Newsletter/Model/ResourceModel/SubscriberTest.php b/dev/tests/integration/testsuite/Magento/Newsletter/Model/ResourceModel/SubscriberTest.php index b6f9a22ff527..356cedde5777 100644 --- a/dev/tests/integration/testsuite/Magento/Newsletter/Model/ResourceModel/SubscriberTest.php +++ b/dev/tests/integration/testsuite/Magento/Newsletter/Model/ResourceModel/SubscriberTest.php @@ -40,13 +40,15 @@ public function testLoadByCustomerDataWithCustomerId() * @magentoDataFixture Magento/Newsletter/_files/subscribers.php * @magentoDataFixture Magento/Customer/_files/two_customers.php */ - public function testTryLoadByCustomerDataWithoutCustomerId() + public function testLoadByCustomerDataWithoutCustomerId() { /** @var \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository */ $customerRepository = Bootstrap::getObjectManager() ->create(\Magento\Customer\Api\CustomerRepositoryInterface::class); $customerData = $customerRepository->getById(2); $result = $this->_resourceModel->loadByCustomerData($customerData); - $this->assertEmpty($result); + + $this->assertEquals(0, $result['customer_id']); + $this->assertEquals('customer_two@example.com', $result['subscriber_email']); } } diff --git a/dev/tests/integration/testsuite/Magento/OfflineShipping/_files/tablerates_second_website.php b/dev/tests/integration/testsuite/Magento/OfflineShipping/_files/tablerates_second_website.php new file mode 100644 index 000000000000..52551e6dc96d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/OfflineShipping/_files/tablerates_second_website.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\App\ResourceConnection; +use Magento\OfflineShipping\Model\ResourceModel\Carrier\Tablerate; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +$website = $websiteRepository->get('test'); + +/** @var ResourceConnection $resource */ +$resource = $objectManager->get(ResourceConnection::class); +$connection = $resource->getConnection(); +$resourceModel = $objectManager->create(Tablerate::class); +$entityTable = $resourceModel->getTable('shipping_tablerate'); +$data = + [ + 'website_id' => $website->getId(), + 'dest_country_id' => 'US', + 'dest_region_id' => 0, + 'dest_zip' => '*', + 'condition_name' => 'package_qty', + 'condition_value' => 1, + 'price' => 20, + 'cost' => 20 + ]; +$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_second_website_rollback.php b/dev/tests/integration/testsuite/Magento/OfflineShipping/_files/tablerates_second_website_rollback.php new file mode 100644 index 000000000000..9606b0eb605f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/OfflineShipping/_files/tablerates_second_website_rollback.php @@ -0,0 +1,13 @@ +<?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'); +$connection->query("DELETE FROM {$entityTable};"); 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 000000000000..40e01a81ac80 --- /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 000000000000..cb6e9353b897 --- /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 000000000000..dc2447e8b4c1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance/PageCacheStateTest.php @@ -0,0 +1,69 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\PageCache\Observer\SwitchPageCacheOnMaintenance; + +use PHPUnit\Framework\TestCase; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Page Cache state test. + */ +class PageCacheStateTest extends TestCase +{ + /** + * @var PageCacheState + */ + private $pageCacheStateStorage; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->pageCacheStateStorage = $objectManager->get(PageCacheState::class); + } + + /** + * Tests save state. + * + * @param bool $state + * @return void + * @dataProvider saveStateProvider + */ + public function testSave(bool $state): void + { + $this->pageCacheStateStorage->save($state); + $this->assertEquals($state, $this->pageCacheStateStorage->isEnabled()); + } + + /** + * Tests flush state. + * + * @return void + */ + public function testFlush(): void + { + $this->pageCacheStateStorage->save(true); + $this->assertTrue($this->pageCacheStateStorage->isEnabled()); + $this->pageCacheStateStorage->flush(); + $this->assertFalse($this->pageCacheStateStorage->isEnabled()); + } + + /** + * Save state provider. + * + * @return array + */ + public function saveStateProvider(): array + { + return [[true], [false]]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Controller/ExpressTest.php b/dev/tests/integration/testsuite/Magento/Paypal/Controller/ExpressTest.php index 3d30f8366598..157999224d7b 100644 --- a/dev/tests/integration/testsuite/Magento/Paypal/Controller/ExpressTest.php +++ b/dev/tests/integration/testsuite/Magento/Paypal/Controller/ExpressTest.php @@ -140,12 +140,11 @@ public function testStartActionCustomerToQuote() /** * Test return action with configurable product. + * + * @magentoDataFixture Magento/Paypal/_files/quote_express_configurable.php */ public function testReturnAction() { - // Skipped due to MAGETWO-87333 - //@magentoDataFixture Magento/Paypal/_files/quote_express_configurable.php - $this->markTestSkipped('MAGETWO-87333'); $quote = $this->_objectManager->create(Quote::class); $quote->load('test_cart_with_configurable', 'reserved_order_id'); diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Controller/Payflow/SilentPostTest.php b/dev/tests/integration/testsuite/Magento/Paypal/Controller/Payflow/SilentPostTest.php index 2bebb5bd95bb..81a9587d36f4 100644 --- a/dev/tests/integration/testsuite/Magento/Paypal/Controller/Payflow/SilentPostTest.php +++ b/dev/tests/integration/testsuite/Magento/Paypal/Controller/Payflow/SilentPostTest.php @@ -93,7 +93,7 @@ public function testSuccessfulNotification($resultCode, $orderState, $orderStatu public function responseCodeDataProvider() { return [ - [Payflowlink::RESPONSE_CODE_APPROVED, Order::STATE_PROCESSING, Order::STATE_PROCESSING], + [Payflowlink::RESPONSE_CODE_APPROVED, Order::STATE_COMPLETE, Order::STATE_COMPLETE], [Payflowlink::RESPONSE_CODE_FRAUDSERVICE_FILTER, Order::STATE_PAYMENT_REVIEW, Order::STATUS_FRAUD], ]; } diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Controller/Transparent/ResponseTest.php b/dev/tests/integration/testsuite/Magento/Paypal/Controller/Transparent/ResponseTest.php new file mode 100644 index 000000000000..17464b6d6586 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Paypal/Controller/Transparent/ResponseTest.php @@ -0,0 +1,134 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Paypal\Controller\Transparent; + +use Magento\Checkout\Model\Session; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Intl\DateTimeFactory; +use Magento\Framework\Session\Generic as GenericSession; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Api\PaymentMethodManagementInterface; + +/** + * Tests PayPal transparent response controller. + */ +class ResponseTest extends \Magento\TestFramework\TestCase\AbstractController +{ + /** + * Tests setting credit card expiration month and year to payment from PayPal response. + * + * @param string $currentDateTime + * @param string $paypalExpDate + * @param string $expectedCcMonth + * @param string $expectedCcYear + * @throws NoSuchEntityException + * + * @magentoConfigFixture current_store payment/payflowpro/active 1 + * @magentoDataFixture Magento/Sales/_files/quote.php + * @dataProvider paymentCcExpirationDateDataProvider + */ + public function testPaymentCcExpirationDate( + string $currentDateTime, + string $paypalExpDate, + string $expectedCcMonth, + string $expectedCcYear + ) { + $reservedOrderId = 'test01'; + $postData = [ + 'EXPDATE' => $paypalExpDate, + 'AMT' => '0.00', + 'RESPMSG' => 'Verified', + 'CVV2MATCH' => 'Y', + 'PNREF' => 'A10AAD866C87', + 'SECURETOKEN' => '3HYEHfG06skydAdBXbpIl8QJZ', + 'AVSDATA' => 'YNY', + 'RESULT' => '0', + 'IAVS' => 'N', + 'AVSADDR' => 'Y', + 'SECURETOKENID' => 'yqanLisRZbI0HAG8q3SbbKbhiwjNZAGf', + ]; + + $quote = $this->getQuote($reservedOrderId); + $this->getRequest()->setPostValue($postData); + + /** @var Session $checkoutSession */ + $checkoutSession = $this->_objectManager->get(GenericSession::class); + $checkoutSession->setQuoteId($quote->getId()); + $this->setCurrentDateTime($currentDateTime); + + $this->dispatch('paypal/transparent/response'); + + /** @var PaymentMethodManagementInterface $paymentManagment */ + $paymentManagment = $this->_objectManager->get(PaymentMethodManagementInterface::class); + $payment = $paymentManagment->get($quote->getId()); + + $this->assertEquals($expectedCcMonth, $payment->getCcExpMonth()); + $this->assertEquals($expectedCcYear, $payment->getCcExpYear()); + } + + /** + * @return array + */ + public function paymentCcExpirationDateDataProvider(): array + { + return [ + 'Expiration year in current century' => [ + 'currentDateTime' => '2019-07-05 00:00:00', + 'paypalExpDate' => '0321', + 'expectedCcMonth' => 3, + 'expectedCcYear' => 2021 + ], + 'Expiration year in next century' => [ + 'currentDateTime' => '2099-01-01 00:00:00', + 'paypalExpDate' => '1002', + 'expectedCcMonth' => 10, + 'expectedCcYear' => 2102 + ] + ]; + } + + /** + * Sets current date and time. + * + * @param string $date + */ + private function setCurrentDateTime(string $dateTime): void + { + $dateTime = new \DateTime($dateTime, new \DateTimeZone('UTC')); + $dateTimeFactory = $this->getMockBuilder(DateTimeFactory::class) + ->disableOriginalConstructor() + ->getMock(); + + $dateTimeFactory->method('create') + ->willReturn($dateTime); + + $this->_objectManager->addSharedInstance($dateTimeFactory, DateTimeFactory::class); + } + + /** + * Gets quote by reserved order ID. + * + * @param string $reservedOrderId + * @return CartInterface + */ + private function getQuote(string $reservedOrderId): CartInterface + { + $searchCriteria = $this->_objectManager->get(SearchCriteriaBuilder::class) + ->addFilter('reserved_order_id', $reservedOrderId) + ->create(); + + /** @var CartRepositoryInterface $quoteRepository */ + $quoteRepository = $this->_objectManager->get(CartRepositoryInterface::class); + $items = $quoteRepository->getList($searchCriteria) + ->getItems(); + + return array_pop($items); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/ConverterStub.php b/dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/ConverterStub.php deleted file mode 100644 index 223ef35c0dcd..000000000000 --- a/dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/ConverterStub.php +++ /dev/null @@ -1,32 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Paypal\Model\Config\Structure\Reader; - -/** - * Class ConverterStub - */ -class ConverterStub extends \Magento\Config\Model\Config\Structure\Converter -{ - /** - * @param \DOMDocument $document - * @return array|null - */ - public function getArrayData(\DOMDocument $document) - { - return $this->_convertDOMDocument($document); - } - - /** - * Convert dom document - * - * @param \DOMNode $source - * @return array - */ - public function convert($source) - { - return $this->_convertDOMDocument($source); - } -} diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/ReaderStub.php b/dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/ReaderStub.php deleted file mode 100644 index ed1366ad737f..000000000000 --- a/dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/ReaderStub.php +++ /dev/null @@ -1,22 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Paypal\Model\Config\Structure\Reader; - -/** - * Class ReaderStub - */ -class ReaderStub extends \Magento\Config\Model\Config\Structure\Reader -{ - /** - * @param array $fileList - * @return array - * @throws \Magento\Framework\Exception\LocalizedException - */ - public function readFiles(array $fileList) - { - return $this->_readFiles($fileList); - } -} diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/ReaderTest.php b/dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/ReaderTest.php deleted file mode 100644 index 6b966a045c98..000000000000 --- a/dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/ReaderTest.php +++ /dev/null @@ -1,135 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Paypal\Model\Config\Structure\Reader; - -use Magento\Framework\ObjectManagerInterface; - -/** - * Class ReaderTest - */ -class ReaderTest extends \PHPUnit\Framework\TestCase -{ - const EXPECTED = '/dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/_files/expected'; - - const ACTUAL = '/dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/_files/actual'; - - /** - * @var ObjectManagerInterface - */ - protected $objectManager; - - /** - * @var \Magento\Framework\App\Utility\Files - */ - protected $fileUtility; - - /** - * @var \Magento\Framework\Config\ValidationStateInterface - */ - protected $validationStateMock; - - /** - * @var \Magento\Framework\Config\SchemaLocatorInterface - */ - protected $schemaLocatorMock; - - /** - * @var \Magento\Framework\Config\FileResolverInterface - */ - protected $fileResolverMock; - - /** - * @var \Magento\Paypal\Model\Config\Structure\Reader\ReaderStub - */ - protected $reader; - - /** - * @var \Magento\Paypal\Model\Config\Structure\Reader\ConverterStub - */ - protected $converter; - - /** - * Set up - * - * @return void - */ - protected function setUp() - { - $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $this->fileUtility = \Magento\Framework\App\Utility\Files::init(); - - $this->validationStateMock = $this->getMockBuilder(\Magento\Framework\Config\ValidationStateInterface::class) - ->setMethods(['isValidationRequired']) - ->getMockForAbstractClass(); - $this->schemaLocatorMock = $this->getMockBuilder(\Magento\Config\Model\Config\SchemaLocator::class) - ->disableOriginalConstructor() - ->setMethods(['getPerFileSchema']) - ->getMock(); - $this->fileResolverMock = $this->getMockBuilder(\Magento\Framework\Config\FileResolverInterface::class) - ->getMockForAbstractClass(); - - $this->validationStateMock->expects($this->atLeastOnce()) - ->method('isValidationRequired') - ->willReturn(false); - $this->schemaLocatorMock->expects($this->atLeastOnce()) - ->method('getPerFileSchema') - ->willReturn(false); - - /** @var \Magento\Paypal\Model\Config\Structure\Reader\ConverterStub $converter */ - $this->converter = $this->objectManager->create( - \Magento\Paypal\Model\Config\Structure\Reader\ConverterStub::class - ); - - $this->reader = $this->objectManager->create( - \Magento\Paypal\Model\Config\Structure\Reader\ReaderStub::class, - [ - 'fileResolver' => $this->fileResolverMock, - 'converter' => $this->converter, - 'schemaLocator' => $this->schemaLocatorMock, - 'validationState' => $this->validationStateMock, - 'fileName' => 'no_existing_file.xml', - 'domDocumentClass' => \Magento\Framework\Config\Dom::class - ] - ); - } - - /** - * The test checks the file structure after processing the nodes responsible for inserting content - * - * @return void - */ - public function testXmlConvertedConfigurationAndCompereStructure() - { - $actual = $this->reader->readFiles(['actual' => $this->getActualContent()]); - - $document = new \DOMDocument(); - $document->loadXML($this->getExpectedContent()); - - $expected = $this->converter->getArrayData($document); - - $this->assertEquals($expected, $actual); - } - - /** - * @return string - */ - protected function getActualContent() - { - $files = $this->fileUtility->getFiles([BP . static::ACTUAL], 'config.xml'); - - return file_get_contents(reset($files)); - } - - /** - * @return string - */ - protected function getExpectedContent() - { - $files = $this->fileUtility->getFiles([BP . static::EXPECTED], 'config.xml'); - - return file_get_contents(reset($files)); - } -} diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/_files/expected/config.xml b/dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/_files/expected/config.xml deleted file mode 100644 index 2bd346a6e8f7..000000000000 --- a/dev/tests/integration/testsuite/Magento/Paypal/Model/Config/Structure/Reader/_files/expected/config.xml +++ /dev/null @@ -1,2369 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> - <system> - <section id="payment"> - <group id="account" translate="label" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="0"> - <label>Merchant Location</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <field id="merchant_country" type="select" translate="label comment" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="0"> - <label>Merchant Country</label> - <comment>If not specified, Default Country from General Config will be used</comment> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Country</frontend_model> - <source_model>Magento\Paypal\Model\System\Config\Source\MerchantCountry</source_model> - <backend_model>Magento\Paypal\Model\System\Config\Backend\MerchantCountry</backend_model> - <config_path>paypal/general/merchant_country</config_path> - </field> - </group> - <group id="recommended_solutions" translate="label" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Recommended Solutions:</label> - <fieldset_css>paypal-top-section paypal-recommended-header</fieldset_css> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - </group> - <group id="other_paypal_payment_solutions" translate="label" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Other PayPal Payment Solutions:</label> - <fieldset_css>paypal-top-section paypal-other-header</fieldset_css> - <frontend_model>\Magento\Config\Block\System\Config\Form\Fieldset</frontend_model> - </group> - <group id="other_payment_methods" translate="label" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Other Payment Methods:</label> - <fieldset_css>paypal-top-section payments-other-header</fieldset_css> - <frontend_model>\Magento\Config\Block\System\Config\Form\Fieldset</frontend_model> - </group> - </section> - <section id="payment_all_paypal" showInDefault="0" showInWebsite="0" showInStore="0"> - <group id="paypal_payflowpro" translate="label comment" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="10"> - <label>Payflow Pro</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Payment</frontend_model> - <fieldset_css>paypal-other-section</fieldset_css> - <comment><![CDATA[Connect your merchant account with a fully customizable gateway that lets customers pay without leaving your site. (<u>Includes Express Checkout</u>)]]></comment> - <attribute type="activity_path">payment/payflowpro/active</attribute> - <attribute type="paypal_ec_separate">1</attribute> - <group id="configuration_details" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="4"> - <comment>http://docs.magento.com/m2/ce/user_guide/payment/paypal-payflow-pro.html</comment> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Hint</frontend_model> - </group> - <group id="paypal_payflow_required" translate="label" showInDefault="1" showInWebsite="1" sortOrder="10"> - <label>Required PayPal Settings</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <group id="paypal_payflow_api_settings" translate="label" showInDefault="1" showInWebsite="1" sortOrder="10"> - <label>Payflow Pro</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <field id="partner" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1"> - <label>Partner</label> - <config_path>payment/payflowpro/partner</config_path> - <attribute type="shared">1</attribute> - </field> - <field id="user" translate="label" type="obscure" sortOrder="30" showInDefault="1" showInWebsite="1"> - <label>User</label> - <config_path>payment/payflowpro/user</config_path> - <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> - <attribute type="shared">1</attribute> - </field> - <field id="vendor" translate="label" type="text" sortOrder="40" showInDefault="1" showInWebsite="1"> - <label>Vendor</label> - <config_path>payment/payflowpro/vendor</config_path> - <attribute type="shared">1</attribute> - </field> - <field id="pwd" translate="label" type="obscure" sortOrder="50" showInDefault="1" showInWebsite="1"> - <label>Password</label> - <config_path>payment/payflowpro/pwd</config_path> - <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> - <attribute type="shared">1</attribute> - </field> - <field id="sandbox_flag" translate="label" type="select" sortOrder="60" showInDefault="1" showInWebsite="1"> - <label>Test Mode</label> - <config_path>payment/payflowpro/sandbox_flag</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="use_proxy" translate="label" type="select" sortOrder="70" showInDefault="1" showInWebsite="1"> - <label>Use Proxy</label> - <config_path>payment/payflowpro/use_proxy</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="proxy_host" translate="label" type="text" sortOrder="80" showInDefault="1" showInWebsite="1"> - <label>Proxy Host</label> - <config_path>payment/payflowpro/proxy_host</config_path> - <depends> - <field id="use_proxy">1</field> - </depends> - <attribute type="shared">1</attribute> - </field> - <field id="proxy_port" translate="label" type="text" sortOrder="90" showInDefault="1" showInWebsite="1"> - <label>Proxy Port</label> - <config_path>payment/payflowpro/proxy_port</config_path> - <depends> - <field id="use_proxy">1</field> - </depends> - <attribute type="shared">1</attribute> - </field> - </group> - <field id="enable_paypal_payflow" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1"> - <label>Enable this Solution</label> - <config_path>payment/payflowpro/active</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Enable\Payment</frontend_model> - <requires> - <group id="paypal_payflow_api_settings"/> - </requires> - </field> - <field id="payflowpro_cc_vault_active" translate="label" type="select" sortOrder="22" showInDefault="1" showInWebsite="1" showInStore="0"> - <label>Vault Enabled</label> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <config_path>payment/payflowpro_cc_vault/active</config_path> - <attribute type="shared">1</attribute> - <requires> - <group id="paypal_payflow_api_settings"/> - </requires> - </field> - </group> - <group id="settings_paypal_payflow" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="20"> - <label>Basic Settings - PayPal Payflow Pro</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <field id="title" translate="label comment" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Title</label> - <comment>It is recommended to set this value to "Debit or Credit Card" per store views.</comment> - <config_path>payment/payflowpro/title</config_path> - <attribute type="shared">1</attribute> - </field> - <field id="payflowpro_cc_vault_title" translate="label" type="text" sortOrder="15" showInDefault="1" showInWebsite="1" showInStore="0"> - <label>Vault Title</label> - <config_path>payment/payflowpro_cc_vault/title</config_path> - </field> - <field id="sort_order" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Sort Order</label> - <config_path>payment/payflowpro/sort_order</config_path> - <frontend_class>validate-number</frontend_class> - <attribute type="shared">1</attribute> - </field> - <field id="payment_action" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1"> - <label>Payment Action</label> - <config_path>payment/payflowpro/payment_action</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\PaymentActions</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="heading_cc" translate="label" sortOrder="40" showInDefault="1" showInWebsite="1"> - <label>Credit Card Settings</label> - <frontend_model>Magento\Config\Block\System\Config\Form\Field\Heading</frontend_model> - <attribute type="shared">1</attribute> - </field> - <field id="cctypes" translate="label comment" type="multiselect" sortOrder="50" showInDefault="1" showInWebsite="1"> - <label>Allowed Credit Card Types</label> - <comment> - <![CDATA[Supporting of American Express cards require additional agreement. Learn more at <a href="http://www.paypal.com/amexupdate">http://www.paypal.com/amexupdate</a>.]]> - </comment> - <config_path>payment/payflowpro/cctypes</config_path> - <source_model>Magento\Paypal\Model\Config::getPayflowproCcTypesAsOptionArray</source_model> - <attribute type="shared">1</attribute> - </field> - <group id="settings_paypal_payflow_advanced" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="60"> - <label>Advanced Settings</label> - <fieldset_css>config-advanced</fieldset_css> - <field id="allowspecific" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1"> - <label>Payment Applicable From</label> - <config_path>payment/payflowpro/allowspecific</config_path> - <source_model>Magento\Payment\Model\Config\Source\Allspecificcountries</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="specificcountry" translate="label" type="multiselect" sortOrder="20" showInDefault="1" showInWebsite="1"> - <label>Countries Payment Applicable From</label> - <config_path>payment/payflowpro/specificcountry</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BuyerCountry</source_model> - <depends> - <field id="allowspecific">1</field> - </depends> - <attribute type="shared">1</attribute> - </field> - <field id="debug" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1"> - <label>Debug Mode</label> - <config_path>payment/payflowpro/debug</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="verify_peer" translate="label" type="select" sortOrder="35" showInDefault="1" showInWebsite="1"> - <label>Enable SSL verification</label> - <config_path>payment/payflowpro/verify_peer</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="useccv" translate="label" type="select" sortOrder="40" showInDefault="1" showInWebsite="1"> - <label>Require CVV Entry</label> - <config_path>payment/payflowpro/useccv</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <group id="paypal_payflow_avs_check" translate="label" showInDefault="1" showInWebsite="1" sortOrder="80"> - <label>CVV and AVS Settings</label> - <field id="heading_avs_settings" translate="label" sortOrder="0" showInDefault="1" showInWebsite="1"> - <label>Reject Transaction if:</label> - <frontend_model>Magento\Config\Block\System\Config\Form\Field\Heading</frontend_model> - <attribute type="shared">1</attribute> - </field> - <field id="avs_street" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1"> - <label>AVS Street Does Not Match</label> - <config_path>payment/payflowpro/avs_street</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="avs_zip" translate="label" type="select" sortOrder="2" showInDefault="1" showInWebsite="1"> - <label>AVS Zip Does Not Match</label> - <config_path>payment/payflowpro/avs_zip</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="avs_international" translate="label" type="select" sortOrder="3" showInDefault="1" showInWebsite="1"> - <label>Card Issuer Is Outside The United States</label> - <config_path>payment/payflowpro/avs_international</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="avs_security_code" translate="label" type="select" sortOrder="4" showInDefault="1" showInWebsite="1"> - <label>Card Security Code Does Not Match</label> - <config_path>payment/payflowpro/avs_security_code</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">0</attribute> - </field> - </group> - <group id="paypal_payflow_settlement_report" translate="label" showInDefault="1" showInWebsite="1" sortOrder="90"> - <label>Settlement Report Settings</label> - <field id="heading_sftp" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/heading_sftp"/> - <field id="settlement_reports_ftp_login" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_login"/> - <field id="settlement_reports_ftp_password" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_password"/> - <field id="settlement_reports_ftp_sandbox" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_sandbox"/> - <field id="settlement_reports_ftp_ip" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_ip"/> - <field id="settlement_reports_ftp_path" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_path"/> - <field id="heading_schedule" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/heading_schedule"/> - <field id="settlement_reports_active" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_active"/> - <field id="settlement_reports_schedule" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_schedule"/> - <field id="settlement_reports_time" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_time"/> - </group> - </group> - </group> - </group> - <group id="payflow_link" translate="label comment" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="20"> - <label>Payflow Link</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Payment</frontend_model> - <fieldset_css>paypal-other-section</fieldset_css> - <comment><![CDATA[Connect your merchant account with a PCI-compliant gateway that lets customers pay without leaving your site. (<u>Includes Express Checkout</u>)]]></comment> - <attribute type="activity_path">payment/payflow_link/active</attribute> - <group id="configuration_details" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="4"> - <comment>http://docs.magento.com/m2/ce/user_guide/payment/paypal-payflow-link.html</comment> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Hint</frontend_model> - </group> - <group id="payflow_link_required" translate="label" showInDefault="1" showInWebsite="1" sortOrder="10"> - <label>Required PayPal Settings</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <group id="payflow_link_payflow_link" translate="label" showInDefault="1" showInWebsite="1" sortOrder="10"> - <label>Payflow Link and Express Checkout</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <field id="business_account" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_required_express_checkout/business_account" translate="label" sortOrder="5"> - <frontend_class>not-required</frontend_class> - <label>Email Associated with PayPal Merchant Account (Optional)</label> - <attribute type="shared">1</attribute> - </field> - <field id="partner" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1"> - <label>Partner</label> - <config_path>payment/payflow_link/partner</config_path> - <attribute type="shared">1</attribute> - </field> - <field id="vendor" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1"> - <label>Vendor</label> - <config_path>payment/payflow_link/vendor</config_path> - <attribute type="shared">1</attribute> - </field> - <field id="user" translate="label comment" type="obscure" sortOrder="30" showInDefault="1" showInWebsite="1"> - <label>User</label> - <comment>If you do not have multiple users set up on your account, please re-enter your Vendor/Merchant Login here.</comment> - <config_path>payment/payflow_link/user</config_path> - <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> - <attribute type="shared">1</attribute> - </field> - <field id="pwd" translate="label" type="obscure" sortOrder="40" showInDefault="1" showInWebsite="1"> - <label>Password</label> - <config_path>payment/payflow_link/pwd</config_path> - <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> - <attribute type="shared">1</attribute> - </field> - <field id="sandbox_flag" translate="label" type="select" sortOrder="50" showInDefault="1" showInWebsite="1"> - <label>Test Mode</label> - <config_path>payment/payflow_link/sandbox_flag</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="use_proxy" translate="label" type="select" sortOrder="60" showInDefault="1" showInWebsite="1"> - <label>Use Proxy</label> - <config_path>payment/payflow_link/use_proxy</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="proxy_host" translate="label" type="text" sortOrder="70" showInDefault="1" showInWebsite="1"> - <label>Proxy Host</label> - <config_path>payment/payflow_link/proxy_host</config_path> - <depends> - <field id="use_proxy">1</field> - </depends> - <attribute type="shared">1</attribute> - </field> - <field id="proxy_port" translate="label" type="text" sortOrder="80" showInDefault="1" showInWebsite="1"> - <label>Proxy Port</label> - <config_path>payment/payflow_link/proxy_port</config_path> - <depends> - <field id="use_proxy">1</field> - </depends> - <attribute type="shared">1</attribute> - </field> - <field id="payflowlink_info" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="90"> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Payflowlink\Info</frontend_model> - <attribute type="shared">1</attribute> - </field> - </group> - - - - <field id="enable_payflow_link" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1"> - <label>Enable Payflow Link</label> - <config_path>payment/payflow_link/active</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Enable\Payment</frontend_model> - <requires> - <group id="payflow_link_payflow_link"/> - </requires> - </field> - <field id="enable_express_checkout_basic" translate="label" type="select" sortOrder="40"> - <label>Enable Express Checkout</label> - <config_path>payment/payflow_express/active</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Enable\Express</frontend_model> - </field> - <field id="enable_express_checkout" extends="payment_all_paypal/payflow_link/payflow_link_required/enable_express_checkout_basic" showInDefault="1" showInWebsite="1"> - <requires> - <field id="enable_payflow_link"/> - </requires> - </field> - <field id="enable_express_checkout_bml" extends="payment_all_paypal/express_checkout/express_checkout_required/enable_express_checkout_bml" sortOrder="41"> - <comment><![CDATA[Payflow Link lets you give customers access to financing through PayPal Credit® - at no additional cost to you. - You get paid up front, even though customers have more time to pay. A pre-integrated payment button lets customers pay quickly with PayPal Credit®. - <a href="https://www.paypal.com/webapps/mpp/promotional-financing" target="_blank">Learn More</a>]]> - </comment> - <config_path>payment/payflow_express_bml/active</config_path> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Enable\Bml</frontend_model> - <requires> - <field id="enable_express_checkout"/> - </requires> - </field> - <field id="express_checkout_bml_sort_order" sortOrder="50" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_bml_sort_order"> - <config_path>payment/payflow_express_bml/sort_order</config_path> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Depends\BmlSortOrder</frontend_model> - <depends> - <field id="enable_express_checkout_bml">1</field> - </depends> - </field> - - - - <group id="payflow_link_advertise_bml" translate="label comment" showInDefault="1" showInWebsite="1" sortOrder="60"> - <label>Advertise PayPal Credit</label> - <comment> - <![CDATA[<a href="https://financing.paypal.com/ppfinportal/content/whyUseFinancing" target="_blank">Why Advertise Financing?</a><br/> - <strong>Give your sales a boost when you advertise financing.</strong><br/>PayPal helps turn browsers into buyers with financing - from PayPal Credit®. Your customers have more time to pay, while you get paid up front – at no additional cost to you. - Use PayPal’s free banner ads that let you advertise PayPal Credit® financing as a payment option when your customers check out with PayPal. - The PayPal Advertising Program has been shown to generate additional purchases as well as increase consumer's average purchase sizes by 15% - or more. <a href="https://financing.paypal.com/ppfinportal/content/forrester" target="_blank">See Details</a>.]]> - </comment> - <field id="bml_publisher_id" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/bml_publisher_id" /> - <field id="bml_wizard" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/bml_wizard" /> - <group id="payflow_link_settings_bml_homepage" translate="label" showInWebsite="1" sortOrder="20" showInDefault="1" showInStore="1"> - <label>Home Page</label> - <field id="bml_homepage_display" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_homepage/bml_homepage_display"/> - <field id="payflow_link_bml_homepage_position" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_homepage/bml_homepage_position"/> - <field id="payflow_link_bml_homepage_size1" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="30"> - <label>Size</label> - <config_path>payment/paypal_express_bml/homepage_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeHPH</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="payflow_link_bml_homepage_position">0</field> - </depends> - </field> - <field id="payflow_link_bml_homepage_size2" extends="payment_all_paypal/payflow_link/payflow_link_required/payflow_link_advertise_bml/payflow_link_settings_bml_homepage/payflow_link_bml_homepage_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeHPS</source_model> - <depends> - <field id="payflow_link_bml_homepage_position">1</field> - </depends> - </field> - </group> - <group id="payflow_link_settings_bml_categorypage" translate="label" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Catalog Category Page</label> - <field id="bml_categorypage_display" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_categorypage/bml_categorypage_display"/> - <field id="payflow_link_bml_categorypage_position" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_categorypage/bml_categorypage_position" /> - <field id="payflow_link_bml_categorypage_size1" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="40"> - <label>Size</label> - <config_path>payment/paypal_express_bml/categorypage_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCCPC</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="payflow_link_bml_categorypage_position">0</field> - </depends> - </field> - <field id="payflow_link_bml_categorypage_size2" extends="payment_all_paypal/payflow_link/payflow_link_required/payflow_link_advertise_bml/payflow_link_settings_bml_categorypage/payflow_link_bml_categorypage_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCCPS</source_model> - <depends> - <field id="payflow_link_bml_categorypage_position">1</field> - </depends> - </field> - </group> - <group id="payflow_link_settings_bml_productpage" translate="label" showInDefault="1" showInStore="1" showInWebsite="1" sortOrder="50"> - <label>Catalog Product Page</label> - <field id="bml_productpage_display" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_productpage/bml_productpage_display" /> - <field id="payflow_link_bml_productpage_position" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_productpage/bml_productpage_position" /> - <field id="payflow_link_bml_productpage_size1" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="30"> - <label>Size</label> - <config_path>payment/paypal_express_bml/productpage_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCPPC</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="payflow_link_bml_productpage_position">0</field> - </depends> - </field> - <field id="payflow_link_bml_productpage_size2" extends="payment_all_paypal/payflow_link/payflow_link_required/payflow_link_advertise_bml/payflow_link_settings_bml_productpage/payflow_link_bml_productpage_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCPPN</source_model> - <depends> - <field id="payflow_link_bml_productpage_position">1</field> - </depends> - </field> - </group> - <group id="payflow_link_settings_bml_checkout" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="60"> - <label>Checkout Cart Page</label> - <field id="bml_checkout_display" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_checkout/bml_checkout_display" /> - <field id="payflow_link_bml_checkout_position" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_checkout/bml_checkout_position" /> - <field id="payflow_link_bml_checkout_size1" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1"> - <label>Size</label> - <config_path>payment/paypal_express_bml/checkout_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCheckoutC</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="payflow_link_bml_checkout_position">0</field> - </depends> - </field> - <field id="payflow_link_bml_checkout_size2" extends="payment_all_paypal/payflow_link/payflow_link_required/payflow_link_advertise_bml/payflow_link_settings_bml_checkout/payflow_link_bml_checkout_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCheckoutN</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="payflow_link_bml_checkout_position">1</field> - </depends> - </field> - </group> - </group> - </group> - <group id="settings_payflow_link" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="20"> - <label>Basic Settings - PayPal Payflow Link</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <field id="title" translate="label comment" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Title</label> - <comment>It is recommended to set this value to "Debit or Credit Card" per store views.</comment> - <config_path>payment/payflow_link/title</config_path> - <attribute type="shared">1</attribute> - </field> - <field id="sort_order" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Sort Order</label> - <config_path>payment/payflow_link/sort_order</config_path> - <frontend_class>validate-number</frontend_class> - <attribute type="shared">1</attribute> - </field> - <field id="payment_action" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1"> - <label>Payment Action</label> - <config_path>payment/payflow_link/payment_action</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\PaymentActions</source_model> - <attribute type="shared">1</attribute> - </field> - <group id="settings_payflow_link_advanced" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="40"> - <label>Advanced Settings</label> - <fieldset_css>config-advanced</fieldset_css> - <field id="allowspecific" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1"> - <label>Payment Applicable From</label> - <config_path>payment/payflow_link/allowspecific</config_path> - <source_model>Magento\Payment\Model\Config\Source\Allspecificcountries</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="specificcountry" translate="label" type="multiselect" sortOrder="20" showInDefault="1" showInWebsite="1"> - <label>Countries Payment Applicable From</label> - <config_path>payment/payflow_link/specificcountry</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BuyerCountry</source_model> - <depends> - <field id="allowspecific">1</field> - </depends> - <attribute type="shared">1</attribute> - </field> - <field id="debug" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1"> - <label>Debug Mode</label> - <config_path>payment/payflow_link/debug</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="verify_peer" translate="label" type="select" sortOrder="35" showInDefault="1" showInWebsite="1"> - <label>Enable SSL verification</label> - <config_path>payment/payflow_link/verify_peer</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="csc_editable" translate="label" type="select" sortOrder="40" showInDefault="1" showInWebsite="1"> - <label>CVV Entry is Editable</label> - <config_path>payment/payflow_link/csc_editable</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="csc_required" translate="label" type="select" sortOrder="50" showInDefault="1" showInWebsite="1"> - <label>Require CVV Entry</label> - <config_path>payment/payflow_link/csc_required</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <depends> - <field id="csc_editable">1</field> - </depends> - <attribute type="shared">1</attribute> - </field> - <field id="email_confirmation" translate="label" type="select" sortOrder="60" showInDefault="1" showInWebsite="1"> - <label>Send Email Confirmation</label> - <config_path>payment/payflow_link/email_confirmation</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="url_method" translate="label" type="select" sortOrder="70" showInDefault="1" showInWebsite="1"> - <label>URL method for Cancel URL and Return URL</label> - <config_path>payment/payflow_link/url_method</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\UrlMethod</source_model> - <attribute type="shared">1</attribute> - </field> - <group id="payflow_link_settlement_report" translate="label" showInDefault="1" showInWebsite="1" sortOrder="80"> - <label>Settlement Report Settings</label> - <field id="heading_sftp" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/heading_sftp"/> - <field id="settlement_reports_ftp_login" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_login"/> - <field id="settlement_reports_ftp_password" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_password"/> - <field id="settlement_reports_ftp_sandbox" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_sandbox"/> - <field id="settlement_reports_ftp_ip" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_ip"/> - <field id="settlement_reports_ftp_path" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_path"/> - <field id="heading_schedule" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/heading_schedule"/> - <field id="settlement_reports_active" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_active"/> - <field id="settlement_reports_schedule" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_schedule"/> - <field id="settlement_reports_time" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_time"/> - </group> - <group id="payflow_link_frontend" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="90"> - <label>Frontend Experience Settings</label> - <field id="logo" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/logo"/> - <field id="paypal_pages" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_pages"/> - <field id="page_style" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/page_style"/> - <field id="paypal_hdrimg" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_hdrimg"/> - <field id="paypal_hdrbackcolor" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_hdrbackcolor"/> - <field id="paypal_hdrbordercolor" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_hdrbordercolor"/> - <field id="paypal_payflowcolor" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_payflowcolor"/> - </group> - </group> - </group> - <group id="settings_payflow_link_express_checkout" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="30"> - <label>Basic Settings - PayPal Express Checkout</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <field id="title" extends="payment_us/paypal_payment_gateways/paypal_payflowpro_with_express_checkout/paypal_payflow_express_checkout/title" /> - <field id="sort_order" extends="payment_us/paypal_payment_gateways/paypal_payflowpro_with_express_checkout/paypal_payflow_express_checkout/sort_order" /> - <field id="payment_action" extends="payment_us/paypal_payment_gateways/paypal_payflowpro_with_express_checkout/paypal_payflow_express_checkout/payment_action" /> - <field id="visible_on_product" extends="payment_us/paypal_payment_gateways/paypal_payflowpro_with_express_checkout/paypal_payflow_express_checkout/visible_on_product" /> - <group id="settings_payflow_link_express_checkout_advanced" extends="payment_us/paypal_payment_gateways/paypal_payflowpro_with_express_checkout/paypal_payflow_express_checkout/paypal_payflow_express_checkout_advanced"/> - </group> - </group> - <group id="express_checkout" translate="label comment" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="60"> - <label>Express Checkout</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Payment</frontend_model> - <fieldset_css>paypal-other-section</fieldset_css> - <comment>Add PayPal as an additional payment method to your checkout page.</comment> - <attribute type="activity_path">payment/paypal_express/active</attribute> - <group id="configuration_details" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="4"> - <comment>http://docs.magento.com/m2/ce/user_guide/payment/paypal-express-checkout.html</comment> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Hint</frontend_model> - </group> - <group id="express_checkout_required" translate="label" showInDefault="1" showInWebsite="1" sortOrder="10"> - <label>Required PayPal Settings</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <group id="express_checkout_required_express_checkout" translate="label" showInDefault="1" showInWebsite="1" sortOrder="10"> - <label>Express Checkout</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <field id="business_account" translate="label comment tooltip" showInDefault="1" showInWebsite="1" sortOrder="5"> - <label>Email Associated with PayPal Merchant Account (Optional)</label> - <frontend_class>not-required</frontend_class> - <comment> - <![CDATA[<a href="http://www.magentocommerce.com/paypal">Start accepting payments via PayPal!</a>]]> - </comment> - <tooltip>Don't have a PayPal account? Simply enter your email address.</tooltip> - <config_path>paypal/general/business_account</config_path> - <validate>validate-email</validate> - <attribute type="shared">1</attribute> - </field> - <field id="api_authentication" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1"> - <label>API Authentication Methods</label> - <config_path>paypal/wpp/api_authentication</config_path> - <source_model>Magento\Paypal\Model\Config::getApiAuthenticationMethods</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="api_username" translate="label" type="obscure" sortOrder="30" showInDefault="1" showInWebsite="1"> - <label>API Username</label> - <config_path>paypal/wpp/api_username</config_path> - <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> - <attribute type="shared">1</attribute> - </field> - <field id="api_password" translate="label" type="obscure" sortOrder="40" showInDefault="1" showInWebsite="1"> - <label>API Password</label> - <config_path>paypal/wpp/api_password</config_path> - <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> - <attribute type="shared">1</attribute> - </field> - <field id="api_signature" translate="label" type="obscure" sortOrder="50" showInDefault="1" showInWebsite="1"> - <label>API Signature</label> - <config_path>paypal/wpp/api_signature</config_path> - <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> - <attribute type="shared">1</attribute> - <depends> - <field id="api_authentication">0</field> - </depends> - </field> - <field id="api_cert" translate="label" type="file" sortOrder="60" showInDefault="1" showInWebsite="1"> - <label>API Certificate</label> - <config_path>paypal/wpp/api_cert</config_path> - <backend_model>Magento\Paypal\Model\System\Config\Backend\Cert</backend_model> - <attribute type="shared">1</attribute> - <depends> - <field id="api_authentication">1</field> - </depends> - </field> - <field id="api_wizard" translate="button_label attribute sandbox_button_label" sortOrder="70" showInDefault="1" showInWebsite="1"> - <attribute type="button_label">Get Credentials from PayPal</attribute> - <attribute type="button_url"> - <![CDATA[https://www.paypal.com/webapps/merchantboarding/webflow/externalpartnerflow]]> - </attribute> - - <attribute type="sandbox_button_label">Sandbox Credentials</attribute> - <attribute type="sandbox_button_url"> - <![CDATA[https://www.sandbox.paypal.com/webapps/merchantboarding/webflow/externalpartnerflow]]> - </attribute> - - <!-- partnerId --> - <attribute type="partner_id">NB9WWHYEMVUMS</attribute> - <!-- partnerLogoUrl --> - <attribute type="partner_logo_url">Magento_Backend/web/images/logo-magento.png</attribute> - <!-- receiveCredentials --> - <attribute type="receive_credentials">FALSE</attribute> - <!-- showPermissions --> - <attribute type="show_permissions">FALSE</attribute> - <!-- displayMode --> - <attribute type="display_mode">embedded</attribute> - <!-- productIntentID --> - <attribute type="product_intent_id">pp_express</attribute> - - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\ApiWizard</frontend_model> - <attribute type="shared">1</attribute> - </field> - <field id="sandbox_flag" translate="label" type="select" sortOrder="80" showInDefault="1" showInWebsite="1"> - <label>Sandbox Mode</label> - <config_path>paypal/wpp/sandbox_flag</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="use_proxy" translate="label" type="select" sortOrder="90" showInDefault="1" showInWebsite="1"> - <label>API Uses Proxy</label> - <config_path>paypal/wpp/use_proxy</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="proxy_host" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1"> - <label>Proxy Host</label> - <config_path>paypal/wpp/proxy_host</config_path> - <attribute type="shared">1</attribute> - <depends> - <field id="use_proxy">1</field> - </depends> - </field> - <field id="proxy_port" translate="label" type="text" sortOrder="110" showInDefault="1" showInWebsite="1"> - <label>Proxy Port</label> - <config_path>paypal/wpp/proxy_port</config_path> - <attribute type="shared">1</attribute> - <depends> - <field id="use_proxy">1</field> - </depends> - </field> - </group> - <field id="enable_express_checkout" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1"> - <label>Enable this Solution</label> - <config_path>payment/paypal_express/active</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Enable\Payment</frontend_model> - <requires> - <group id="express_checkout_required_express_checkout"/> - </requires> - </field> - <field id="enable_in_context_checkout" translate="label comment" type="select" sortOrder="21" showInDefault="1" showInWebsite="1"> - <label>Enable In-Context Checkout Experience</label> - <comment> - <![CDATA[See PayPal Feature Support details and list of supported regions - <a href="https://developer.paypal.com/docs/classic/express-checkout/in-context/" target="_blank">here</a>.]]> - </comment> - <config_path>payment/paypal_express/in_context</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Enable\InContextApi</frontend_model> - <requires> - <field id="enable_express_checkout"/> - </requires> - </field> - <field id="merchant_id" translate="label" type="text" sortOrder="22" showInDefault="1" showInWebsite="1"> - <label>Merchant Account ID</label> - <tooltip>You can look up your merchant ID by logging into https://www.paypal.com/. Click the profile icon on the top right side of the page and then select Profile and settings in the Business Profile menu. (If you do not see the profile icon at the top of the page, click Profile, which appears in the top menu when the My Account tab is selected.) Click My business info on the left, and the Merchant account ID is displayed in the list of profile items on the right.</tooltip> - <config_path>payment/paypal_express/merchant_id</config_path> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Depends\MerchantId</frontend_model> - <depends> - <field id="enable_in_context_checkout">1</field> - </depends> - <validate>required-entry</validate> - </field> - <field id="enable_express_checkout_bml" translate="label comment" type="select" sortOrder="23" showInDefault="1" showInWebsite="1"> - <label>Enable PayPal Credit</label> - <comment><![CDATA[PayPal Express Checkout lets you give customers access to financing through PayPal Credit® - at no additional cost to you. - You get paid up front, even though customers have more time to pay. A pre-integrated payment button lets customers pay quickly with PayPal Credit®. - <a href="https://www.paypal.com/webapps/mpp/promotional-financing" target="_blank">Learn More</a>]]> - </comment> - <config_path>payment/paypal_express_bml/active</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Enable\BmlApi</frontend_model> - <requires> - <field id="enable_express_checkout"/> - </requires> - </field> - <field id="express_checkout_bml_sort_order" translate="label" type="text" sortOrder="25" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Sort Order PayPal Credit</label> - <config_path>payment/paypal_express_bml/sort_order</config_path> - <frontend_class>validate-number</frontend_class> - <attribute type="shared">1</attribute> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Depends\BmlApiSortOrder</frontend_model> - <depends> - <field id="enable_express_checkout_bml">1</field> - </depends> - </field> - <group id="advertise_bml" translate="label comment" showInDefault="1" showInWebsite="1" sortOrder="30"> - <label>Advertise PayPal Credit</label> - <comment> - <![CDATA[<a href="https://financing.paypal.com/ppfinportal/content/whyUseFinancing" target="_blank">Why Advertise Financing?</a><br/> - <strong>Give your sales a boost when you advertise financing.</strong><br/>PayPal helps turn browsers into buyers with financing - from PayPal Credit®. Your customers have more time to pay, while you get paid up front – at no additional cost to you. - Use PayPal’s free banner ads that let you advertise PayPal Credit® financing as a payment option when your customers check out with PayPal. - The PayPal Advertising Program has been shown to generate additional purchases as well as increase consumer's average purchase sizes by 15% - or more. <a href="https://financing.paypal.com/ppfinportal/content/forrester" target="_blank">See Details</a>.]]> - </comment> - <field id="bml_publisher_id" translate="label comment tooltip" showInDefault="1" showInWebsite="1" sortOrder="10"> - <label>Publisher ID</label> - <comment><![CDATA[Required to display a banner]]></comment> - <config_path>payment/paypal_express_bml/publisher_id</config_path> - <attribute type="shared">1</attribute> - </field> - <field id="bml_wizard" translate="button_label" sortOrder="15" showInDefault="1" showInWebsite="1"> - <button_label>Get Publisher ID from PayPal</button_label> - <button_url><![CDATA[https://financing.paypal.com/ppfinportal/cart/index?dcp=4eff8563b9cc505e0b9afaff3256705081553c79]]></button_url> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\BmlApiWizard</frontend_model> - </field> - <group id="settings_bml_homepage" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="20"> - <label>Home Page</label> - <field id="bml_homepage_display" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="10"> - <label>Display</label> - <config_path>payment/paypal_express_bml/homepage_display</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="bml_homepage_position" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="20"> - <label>Position</label> - <config_path>payment/paypal_express_bml/homepage_position</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlPosition::getBmlPositionsHP</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="bml_homepage_size1" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="30"> - <label>Size</label> - <config_path>payment/paypal_express_bml/homepage_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeHPH</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="bml_homepage_position">0</field> - </depends> - </field> - <field id="bml_homepage_size2" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_homepage/bml_homepage_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeHPS</source_model> - <depends> - <field id="bml_homepage_position">1</field> - </depends> - </field> - </group> - <group id="settings_bml_categorypage" translate="label" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Catalog Category Page</label> - <field id="bml_categorypage_display" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="10"> - <label>Display</label> - <config_path>payment/paypal_express_bml/categorypage_display</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="bml_categorypage_position" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="20"> - <label>Position</label> - <config_path>payment/paypal_express_bml/categorypage_position</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlPosition::getBmlPositionsCCP</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="bml_categorypage_size1" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="30"> - <label>Size</label> - <config_path>payment/paypal_express_bml/categorypage_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCCPC</source_model> - <attribute type="shared">1</attribute> - <depends><field id="bml_categorypage_position">0</field></depends> - </field> - <field id="bml_categorypage_size2" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_categorypage/bml_categorypage_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCCPS</source_model> - <depends><field id="bml_categorypage_position">1</field></depends> - </field> - </group> - <group id="settings_bml_productpage" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="40"> - <label>Catalog Product Page</label> - <field id="bml_productpage_display" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="10"> - <label>Display</label> - <config_path>payment/paypal_express_bml/productpage_display</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="bml_productpage_position" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="20"> - <label>Position</label> - <config_path>payment/paypal_express_bml/productpage_position</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlPosition::getBmlPositionsCPP</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="bml_productpage_size1" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="30"> - <label>Size</label> - <config_path>payment/paypal_express_bml/productpage_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCPPC</source_model> - <attribute type="shared">1</attribute> - <depends><field id="bml_productpage_position">0</field></depends> - </field> - <field id="bml_productpage_size2" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_productpage/bml_productpage_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCPPN</source_model> - <depends><field id="bml_productpage_position">1</field></depends> - </field> - </group> - <group id="settings_bml_checkout" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="50"> - <label>Checkout Cart Page</label> - <field id="bml_checkout_display" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="10"> - <label>Display</label> - <config_path>payment/paypal_express_bml/checkout_display</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="bml_checkout_position" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="20"> - <label>Position</label> - <config_path>payment/paypal_express_bml/checkout_position</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlPosition::getBmlPositionsCheckout</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="bml_checkout_size1" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="30"> - <label>Size</label> - <config_path>payment/paypal_express_bml/checkout_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCheckoutC</source_model> - <attribute type="shared">1</attribute> - <depends><field id="bml_checkout_position">0</field></depends> - </field> - <field id="bml_checkout_size2" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_checkout/bml_checkout_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCheckoutN</source_model> - <attribute type="shared">1</attribute> - <depends><field id="bml_checkout_position">1</field></depends> - </field> - </group> - </group> - </group> - <group id="settings_ec" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="20"> - <label>Basic Settings - PayPal Express Checkout</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <field id="title" translate="label comment" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Title</label> - <comment>It is recommended to set this value to "PayPal" per store views.</comment> - <config_path>payment/paypal_express/title</config_path> - <attribute type="shared">1</attribute> - </field> - <field id="sort_order" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Sort Order</label> - <config_path>payment/paypal_express/sort_order</config_path> - <frontend_class>validate-number</frontend_class> - <attribute type="shared">1</attribute> - </field> - <field id="payment_action" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1"> - <label>Payment Action</label> - <config_path>payment/paypal_express/payment_action</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\PaymentActions\Express</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="visible_on_product" translate="label" type="select" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Display on Product Details Page</label> - <config_path>payment/paypal_express/visible_on_product</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="authorization_honor_period" translate="label comment" type="text" sortOrder="50" showInDefault="1" showInWebsite="1"> - <label>Authorization Honor Period (days)</label> - <comment>Specifies what the Authorization Honor Period is on the merchant’s PayPal account. It must mirror the setting in PayPal.</comment> - <config_path>payment/paypal_express/authorization_honor_period</config_path> - <attribute type="shared">1</attribute> - <depends> - <field id="payment_action">Order</field> - </depends> - </field> - <field id="order_valid_period" translate="label comment" type="text" sortOrder="60" showInDefault="1" showInWebsite="1"> - <label>Order Valid Period (days)</label> - <comment>Specifies what the Order Valid Period is on the merchant’s PayPal account. It must mirror the setting in PayPal.</comment> - <config_path>payment/paypal_express/order_valid_period</config_path> - <attribute type="shared">1</attribute> - <depends> - <field id="payment_action">Order</field> - </depends> - </field> - <field id="child_authorization_number" translate="label comment" type="text" sortOrder="70" showInDefault="1" showInWebsite="1"> - <label>Number of Child Authorizations</label> - <comment>The default number of child authorizations in your PayPal account is 1. To do multiple authorizations please contact PayPal to request an increase.</comment> - <config_path>payment/paypal_express/child_authorization_number</config_path> - <attribute type="shared">1</attribute> - <depends> - <field id="payment_action">Order</field> - </depends> - </field> - <group id="settings_ec_advanced" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="80"> - <label>Advanced Settings</label> - <fieldset_css>config-advanced</fieldset_css> - <field id="visible_on_cart" translate="label comment" type="select" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Display on Shopping Cart</label> - <config_path>payment/paypal_express/visible_on_cart</config_path> - <comment>Also affects mini-shopping cart.</comment> - <source_model>Magento\Paypal\Model\System\Config\Source\Yesnoshortcut</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="allowspecific" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1"> - <label>Payment Applicable From</label> - <config_path>payment/paypal_express/allowspecific</config_path> - <source_model>Magento\Payment\Model\Config\Source\Allspecificcountries</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="specificcountry" translate="label" type="multiselect" sortOrder="20" showInDefault="1" showInWebsite="1"> - <label>Countries Payment Applicable From</label> - <config_path>payment/paypal_express/specificcountry</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BuyerCountry</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="allowspecific">1</field> - </depends> - </field> - <field id="debug" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1"> - <label>Debug Mode</label> - <config_path>payment/paypal_express/debug</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="verify_peer" translate="label" type="select" sortOrder="35" showInDefault="1" showInWebsite="1"> - <label>Enable SSL verification</label> - <config_path>payment/paypal_express/verify_peer</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="line_items_enabled" translate="label" type="select" sortOrder="40" showInDefault="1" showInWebsite="1"> - <label>Transfer Cart Line Items</label> - <config_path>payment/paypal_express/line_items_enabled</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="transfer_shipping_options" translate="label tooltip comment" type="select" sortOrder="50" showInDefault="1" showInWebsite="1"> - <label>Transfer Shipping Options</label> - <config_path>payment/paypal_express/transfer_shipping_options</config_path> - <tooltip>If this option is enabled, customer can change shipping address and shipping method on PayPal website. In live mode works via HTTPS protocol only.</tooltip> - <comment>Notice that PayPal can handle up to 10 shipping options. That is why Magento will transfer only first 10 cheapest shipping options if there are more than 10 available.</comment> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="line_items_enabled">1</field> - </depends> - </field> - <field id="button_flavor" translate="label" type="select" sortOrder="60" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Shortcut Buttons Flavor</label> - <config_path>paypal/wpp/button_flavor</config_path> - <source_model>Magento\Paypal\Model\Config::getExpressCheckoutButtonFlavors</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="solution_type" translate="label comment" type="select" sortOrder="70" showInDefault="1" showInWebsite="1"> - <label>Enable PayPal Guest Checkout</label> - <comment>Ability for buyer to purchase without PayPal account.</comment> - <config_path>payment/paypal_express/solution_type</config_path> - <source_model>Magento\Paypal\Model\Config::getExpressCheckoutSolutionTypes</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="require_billing_address" translate="label comment" type="select" sortOrder="80" showInDefault="1" showInWebsite="1"> - <label>Require Customer's Billing Address</label> - <comment>This feature needs be enabled first for the merchant account through PayPal technical support.</comment> - <config_path>payment/paypal_express/require_billing_address</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\RequireBillingAddress</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="allow_ba_signup" translate="label comment tooltip" type="select" sortOrder="90" showInDefault="1" showInWebsite="1"> - <label>Billing Agreement Signup</label> - <comment>Whether to create a billing agreement, if there are no active billing agreements available.</comment> - <tooltip> - <![CDATA[Merchants need to apply to PayPal for enabling billing agreements feature. Do not enable this option until PayPal confirms that billing agreements are enabled for your merchant account.]]> - </tooltip> - <config_path>payment/paypal_express/allow_ba_signup</config_path> - <source_model>Magento\Paypal\Model\Config::getExpressCheckoutBASignupOptions</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="skip_order_review_step" translate="label" type="select" sortOrder="95" showInDefault="1" showInWebsite="1"> - <label>Skip Order Review Step</label> - <config_path>payment/paypal_express/skip_order_review_step</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <group id="express_checkout_billing_agreement" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="100"> - <label>PayPal Billing Agreement Settings</label> - <field id="active" translate="label comment" type="select" sortOrder="10" showInDefault="1" showInWebsite="1"> - <label>Enabled</label> - <comment> - <![CDATA[Will appear as a payment option only for customers who have at least one active billing agreement.]]> - </comment> - <config_path>payment/paypal_billing_agreement/active</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="title" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Title</label> - <config_path>payment/paypal_billing_agreement/title</config_path> - <attribute type="shared">1</attribute> - </field> - <field id="sort_order" translate="label" type="text" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Sort Order</label> - <config_path>payment/paypal_billing_agreement/sort_order</config_path> - <frontend_class>validate-number</frontend_class> - <attribute type="shared">1</attribute> - </field> - <field id="payment_action" translate="label" type="select" sortOrder="40" showInDefault="1" showInWebsite="1"> - <label>Payment Action</label> - <config_path>payment/paypal_billing_agreement/payment_action</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\PaymentActions</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="allowspecific" translate="label" type="select" sortOrder="50" showInDefault="1" showInWebsite="1"> - <label>Payment Applicable From</label> - <config_path>payment/paypal_billing_agreement/allowspecific</config_path> - <source_model>Magento\Payment\Model\Config\Source\Allspecificcountries</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="specificcountry" translate="label" type="multiselect" sortOrder="60" showInDefault="1" showInWebsite="1"> - <label>Countries Payment Applicable From</label> - <config_path>payment/paypal_billing_agreement/specificcountry</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BuyerCountry</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="allowspecific">1</field> - </depends> - </field> - <field id="debug" translate="label" type="select" sortOrder="70" showInDefault="1" showInWebsite="1"> - <label>Debug Mode</label> - <config_path>payment/paypal_billing_agreement/debug</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="verify_peer" translate="label" type="select" sortOrder="75" showInDefault="1" showInWebsite="1"> - <label>Enable SSL verification</label> - <config_path>payment/paypal_billing_agreement/verify_peer</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="line_items_enabled" translate="label" type="select" sortOrder="80" showInDefault="1" showInWebsite="1"> - <label>Transfer Cart Line Items</label> - <config_path>payment/paypal_billing_agreement/line_items_enabled</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="allow_billing_agreement_wizard" translate="label" type="select" sortOrder="90" showInDefault="1" showInWebsite="1"> - <label>Allow in Billing Agreement Wizard</label> - <config_path>payment/paypal_billing_agreement/allow_billing_agreement_wizard</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - </group> - <group id="express_checkout_settlement_report" translate="label" showInDefault="1" showInWebsite="1" sortOrder="110"> - <label>Settlement Report Settings</label> - <field id="heading_sftp" translate="label" sortOrder="10" showInDefault="1" showInWebsite="1"> - <label>SFTP Credentials</label> - <frontend_model>Magento\Config\Block\System\Config\Form\Field\Heading</frontend_model> - <attribute type="shared">1</attribute> - </field> - <field id="settlement_reports_ftp_login" translate="label" type="obscure" sortOrder="20" showInDefault="1" showInWebsite="1"> - <label>Login</label> - <config_path>paypal/fetch_reports/ftp_login</config_path> - <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> - <attribute type="shared">1</attribute> - </field> - <field id="settlement_reports_ftp_password" translate="label" type="obscure" sortOrder="30" showInDefault="1" showInWebsite="1"> - <label>Password</label> - <config_path>paypal/fetch_reports/ftp_password</config_path> - <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> - <attribute type="shared">1</attribute> - </field> - <field id="settlement_reports_ftp_sandbox" translate="label" type="select" sortOrder="40" showInDefault="1" showInWebsite="1"> - <label>Sandbox Mode</label> - <config_path>paypal/fetch_reports/ftp_sandbox</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="settlement_reports_ftp_ip" translate="label comment tooltip" type="text" sortOrder="50" showInDefault="1" showInWebsite="1"> - <label>Custom Endpoint Hostname or IP-Address</label> - <comment>By default it is "reports.paypal.com".</comment> - <tooltip>Use colon to specify port. For example: "test.example.com:5224".</tooltip> - <config_path>paypal/fetch_reports/ftp_ip</config_path> - <attribute type="shared">1</attribute> - <depends> - <field id="settlement_reports_ftp_sandbox">0</field> - </depends> - </field> - <field id="settlement_reports_ftp_path" translate="label comeent" type="text" sortOrder="60" showInDefault="1" showInWebsite="1"> - <label>Custom Path</label> - <comment>By default it is "/ppreports/outgoing".</comment> - <config_path>paypal/fetch_reports/ftp_path</config_path> - <attribute type="shared">1</attribute> - <depends> - <field id="settlement_reports_ftp_sandbox">0</field> - </depends> - </field> - <field id="heading_schedule" translate="label" sortOrder="70" showInDefault="1" showInWebsite="1"> - <label>Scheduled Fetching</label> - <frontend_model>Magento\Config\Block\System\Config\Form\Field\Heading</frontend_model> - <attribute type="shared">1</attribute> - </field> - <field id="settlement_reports_active" translate="label" type="select" sortOrder="80" showInDefault="1" showInWebsite="1"> - <label>Enable Automatic Fetching</label> - <config_path>paypal/fetch_reports/active</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="settlement_reports_schedule" translate="label comment" type="select" sortOrder="90" showInDefault="1"> - <label>Schedule</label> - <comment>PayPal retains reports for 45 days.</comment> - <config_path>paypal/fetch_reports/schedule</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\FetchingSchedule</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="settlement_reports_time" translate="label" type="time" sortOrder="100" showInDefault="1"> - <label>Time of Day</label> - <config_path>paypal/fetch_reports/time</config_path> - <attribute type="shared">1</attribute> - </field> - </group> - <group id="express_checkout_frontend" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="120"> - <label>Frontend Experience Settings</label> - <field id="logo" translate="label comment" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>PayPal Product Logo</label> - <comment>Displays on catalog pages and homepage.</comment> - <config_path>paypal/style/logo</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\Logo</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="paypal_pages" translate="label" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>PayPal Merchant Pages Style</label> - <frontend_model>Magento\Config\Block\System\Config\Form\Field\Heading</frontend_model> - <attribute type="shared">1</attribute> - </field> - <field id="page_style" translate="label tooltip" type="text" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Page Style</label> - <config_path>paypal/style/page_style</config_path> - <tooltip> - <![CDATA[Allowable values: "paypal", "primary" (default), your_custom_value (a custom payment page style from your merchant account profile).]]> - </tooltip> - <attribute type="shared">1</attribute> - </field> - <field id="paypal_hdrimg" translate="label tooltip" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Header Image URL</label> - <config_path>paypal/style/paypal_hdrimg</config_path> - <tooltip> - <![CDATA[The image at the top left of the checkout page. Max size is 750x90-pixel. <strong style="color:red">https</strong> is highly encouraged.]]> - </tooltip> - <attribute type="shared">1</attribute> - </field> - <field id="paypal_hdrbackcolor" translate="label tooltip" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Header Background Color</label> - <config_path>paypal/style/paypal_hdrbackcolor</config_path> - <tooltip> - <![CDATA[The background color for the header of the checkout page. Case-insensitive six-character HTML hexadecimal color code in ASCII.]]> - </tooltip> - <attribute type="shared">1</attribute> - </field> - <field id="paypal_hdrbordercolor" translate="label tooltip" sortOrder="60" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Header Border Color</label> - <config_path>paypal/style/paypal_hdrbordercolor</config_path> - <tooltip>2-pixel perimeter around the header space.</tooltip> - <attribute type="shared">1</attribute> - </field> - <field id="paypal_payflowcolor" translate="label tooltip" sortOrder="70" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Page Background Color</label> - <config_path>paypal/style/paypal_payflowcolor</config_path> - <tooltip> - <![CDATA[The background color for the checkout page around the header and payment form.]]> - </tooltip> - <attribute type="shared">1</attribute> - </field> - </group> - </group> - </group> - </group> - <group id="payments_pro_hosted_solution" translate="label comment" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="20"> - <label>Website Payments Pro Hosted Solution</label> - <fieldset_css>paypal-other-section</fieldset_css> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Payment</frontend_model> - <attribute type="activity_path">payment/hosted_pro/active</attribute> - <comment><![CDATA[Accept payments with a PCI compliant checkout that keeps customers on your site. (<u>Includes Express Checkout</u>)]]></comment> - <attribute type="paypal_ec_separate">1</attribute> - <group id="configuration_details" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="4"> - <comment>http://docs.magento.com/m2/ce/user_guide/payment/paypal-payments-pro.html</comment> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Hint</frontend_model> - </group> - <group id="pphs_required_settings" translate="label" showInDefault="1" showInWebsite="1" sortOrder="10"> - <label>Required PayPal Settings</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <group id="pphs_required_settings_pphs" translate="label" showInDefault="1" showInWebsite="1" sortOrder="10"> - <label>Payments Pro Hosted Solution</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <field id="business_account" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_required_express_checkout/business_account"/> - <field id="api_authentication" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_required_express_checkout/api_authentication"/> - <field id="api_username" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_required_express_checkout/api_username" /> - <field id="api_password" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_required_express_checkout/api_password" /> - <field id="api_signature" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_required_express_checkout/api_signature" /> - <field id="api_cert" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_required_express_checkout/api_cert" /> - <field id="api_wizard" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_required_express_checkout/api_wizard" /> - <field id="sandbox_flag" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_required_express_checkout/sandbox_flag" /> - <field id="use_proxy" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_required_express_checkout/use_proxy" /> - <field id="proxy_host" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_required_express_checkout/proxy_host" /> - <field id="proxy_port" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_required_express_checkout/proxy_port" /> - </group> - <field id="pphs_enable" type="select" translate="label" sortOrder="20" showInDefault="1" showInWebsite="1"> - <label>Enable this Solution</label> - <config_path>payment/hosted_pro/active</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Enable\Payment</frontend_model> - <requires> - <group id="pphs_required_settings_pphs"/> - </requires> - <frontend_class>paypal-enabler paypal-ec-separate</frontend_class> - </field> - - <field id="enable_express_checkout_bml" extends="payment_all_paypal/express_checkout/express_checkout_required/enable_express_checkout_bml" sortOrder="21"> - <comment><![CDATA[Payments Pro Hosted Solution lets you give customers access to financing through PayPal Credit® - at no additional cost to you. - You get paid up front, even though customers have more time to pay. A pre-integrated payment button lets customers pay quickly with PayPal Credit®. - <a href="https://www.paypal.com/webapps/mpp/promotional-financing" target="_blank">Learn More</a>]]> - </comment> - <requires> - <field id="pphs_enable"/> - </requires> - </field> - <group id="pphs_advertise_bml" translate="label comment" showInDefault="1" showInWebsite="1" sortOrder="22"> - <label>Advertise PayPal Credit</label> - <comment> - <![CDATA[<a href="https://financing.paypal.com/ppfinportal/content/whyUseFinancing" target="_blank">Why Advertise Financing?</a><br/> - <strong>Give your sales a boost when you advertise financing.</strong><br/>PayPal helps turn browsers into buyers with financing - from PayPal Credit®. Your customers have more time to pay, while you get paid up front – at no additional cost to you. - Use PayPal’s free banner ads that let you advertise PayPal Credit® financing as a payment option when your customers check out with PayPal. - The PayPal Advertising Program has been shown to generate additional purchases as well as increase consumer's average purchase sizes by 15% - or more. <a href="https://financing.paypal.com/ppfinportal/content/forrester" target="_blank">See Details</a>.]]> - </comment> - <field id="bml_publisher_id" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/bml_publisher_id" /> - <field id="bml_wizard" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/bml_wizard" /> - <group id="pphs_settings_bml_homepage" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="20"> - <label>Home Page</label> - <field id="bml_homepage_display" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_homepage/bml_homepage_display"/> - <field id="pphs_bml_homepage_position" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_homepage/bml_homepage_position"/> - <field id="pphs_bml_homepage_size1" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="30"> - <label>Size</label> - <config_path>payment/paypal_express_bml/homepage_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeHPH</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="pphs_bml_homepage_position">0</field> - </depends> - </field> - <field id="pphs_bml_homepage_size2" extends="payment_all_paypal/payments_pro_hosted_solution/pphs_required_settings/pphs_advertise_bml/pphs_settings_bml_homepage/pphs_bml_homepage_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeHPS</source_model> - <depends> - <field id="pphs_bml_homepage_position">1</field> - </depends> - </field> - </group> - <group id="pphs_settings_bml_categorypage" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="30"> - <label>Catalog Category Page</label> - <field id="bml_categorypage_display" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_categorypage/bml_categorypage_display"/> - <field id="pphs_bml_categorypage_position" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_categorypage/bml_categorypage_position" /> - <field id="pphs_bml_categorypage_size1" translate="label" showInDefault="1" showInWebsite="1" sortOrder="30" type="select"> - <label>Size</label> - <config_path>payment/paypal_express_bml/categorypage_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCCPC</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="pphs_bml_categorypage_position">0</field> - </depends> - </field> - <field id="pphs_bml_categorypage_size2" extends="payment_all_paypal/payments_pro_hosted_solution/pphs_required_settings/pphs_advertise_bml/pphs_settings_bml_categorypage/pphs_bml_categorypage_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCCPS</source_model> - <depends> - <field id="pphs_bml_categorypage_position">1</field> - </depends> - </field> - </group> - <group id="pphs_settings_bml_productpage" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="40"> - <label>Catalog Product Page</label> - <field id="bml_productpage_display" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_productpage/bml_productpage_display" /> - <field id="pphs_bml_productpage_position" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_productpage/bml_productpage_position" /> - <field id="pphs_bml_productpage_size1" translate="label" type="select" showInWebsite="1" showInDefault="1" sortOrder="30"> - <label>Size</label> - <config_path>payment/paypal_express_bml/productpage_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCPPC</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="pphs_bml_productpage_position">0</field> - </depends> - </field> - <field id="pphs_bml_productpage_size2" extends="payment_all_paypal/payments_pro_hosted_solution/pphs_required_settings/pphs_advertise_bml/pphs_settings_bml_productpage/pphs_bml_productpage_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCPPN</source_model> - <depends> - <field id="pphs_bml_productpage_position">1</field> - </depends> - </field> - </group> - <group id="pphs_settings_bml_checkout" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="50"> - <label>Checkout Cart Page</label> - <field id="bml_checkout_display" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_checkout/bml_checkout_display" /> - <field id="pphs_bml_checkout_position" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_checkout/bml_checkout_position" /> - <field id="pphs_bml_checkout_size1" translate="label" type="select" showInWebsite="1" showInDefault="1" sortOrder="30"> - <label>Size</label> - <config_path>payment/paypal_express_bml/checkout_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCheckoutC</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="pphs_bml_checkout_position">0</field> - </depends> - </field> - <field id="pphs_bml_checkout_size2" extends="payment_all_paypal/payments_pro_hosted_solution/pphs_required_settings/pphs_advertise_bml/pphs_settings_bml_checkout/pphs_bml_checkout_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCheckoutN</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="pphs_bml_checkout_position">1</field> - </depends> - </field> - </group> - </group> - </group> - <group id="pphs_settings" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="20"> - <label>Basic Settings - PayPal Payments Pro Hosted Solution</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <field id="title" type="text" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="10" translate="label comment"> - <label>Title</label> - <comment>It is recommended to set this value to "PayPal" per store views.</comment> - <config_path>payment/hosted_pro/title</config_path> - </field> - <field id="sort_order" type="text" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="20" translate="label"> - <label>Sort Order</label> - <config_path>payment/hosted_pro/sort_order</config_path> - <frontend_class>validate-number</frontend_class> - </field> - <field id="payment_action" type="select" showInDefault="1" showInWebsite="1" sortOrder="30" translate="label"> - <label>Payment Action</label> - <config_path>payment/hosted_pro/payment_action</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\PaymentActions</source_model> - </field> - <field id="display_ec" type="select" showInDefault="1" showInWebsite="1" sortOrder="40" translate="label"> - <label>Display Express Checkout in the Payment Information step</label> - <config_path>payment/hosted_pro/display_ec</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - </field> - <group id="pphs_settings_advanced" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="50"> - <label>Advanced Settings</label> - <fieldset_css>config-advanced</fieldset_css> - <field id="allowspecific" type="select" showInDefault="1" showInWebsite="1" sortOrder="10" translate="label"> - <label>Payment Applicable From</label> - <config_path>payment/hosted_pro/allowspecific</config_path> - <source_model>Magento\Payment\Model\Config\Source\Allspecificcountries</source_model> - </field> - <field id="specificcountry" type="multiselect" showInDefault="1" showInWebsite="1" sortOrder="20" translate="label"> - <label>Countries Payment Applicable From</label> - <config_path>payment/hosted_pro/specificcountry</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BuyerCountry</source_model> - <depends> - <field id="allowspecific">1</field> - </depends> - </field> - <field id="debug" type="select" showInDefault="1" showInWebsite="1" sortOrder="30" translate="label"> - <label>Debug Mode</label> - <config_path>payment/hosted_pro/debug</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - </field> - <field id="verify_peer" type="select" showInDefault="1" showInWebsite="1" sortOrder="35" translate="label"> - <label>Enable SSL verification</label> - <config_path>payment/hosted_pro/verify_peer</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - </field> - <group id="pphs_settlement_report" showInDefault="1" showInWebsite="1" sortOrder="50" translate="label"> - <label>Settlement Report Settings</label> - <field id="heading_sftp" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/heading_sftp"/> - <field id="settlement_reports_ftp_login" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_login" /> - <field id="settlement_reports_ftp_password" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_password" /> - <field id="settlement_reports_ftp_sandbox" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_sandbox" /> - <field id="settlement_reports_ftp_ip" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_ip" /> - <field id="settlement_reports_ftp_path" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_path" /> - <field id="heading_schedule" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/heading_schedule" /> - <field id="settlement_reports_active" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_active" /> - <field id="settlement_reports_schedule" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_schedule" /> - <field id="settlement_reports_time" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_time" /> - </group> - </group> - </group> - </group> - <group id="payments_pro_hosted_solution_without_bml" extends="payments_pro_hosted_solution"> - <group id="pphs_required_settings"> - <field id="enable_express_checkout_bml" showInDefault="0" showInWebsite="0"/> - <field id="express_checkout_bml_sort_order" showInDefault="0" showInWebsite="0"/> - <group id="pphs_advertise_bml" showInDefault="0" showInWebsite="0"/> - </group> - </group> - </section> - <section id="payment_us" extends="payment" showInDefault="0" showInWebsite="0" showInStore="0"> - <group id="paypal_group_all_in_one" translate="label comment" sortOrder="7" showInDefault="1" showInWebsite="1" showInStore="1"> - <label><![CDATA[PayPal All-in-One Payment Solutions <i>Accept and process credit cards and PayPal payments.</i>]]></label> - <fieldset_css>complex paypal-other-section paypal-all-in-one-section</fieldset_css> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <comment>Choose a secure bundled payment solution for your business.</comment> - <attribute type="displayIn">other_paypal_payment_solutions</attribute> - <group id="payflow_advanced" translate="label comment" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="30"> - <label>Payments Advanced</label> - <fieldset_css>paypal-other-section</fieldset_css> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Payment</frontend_model> - <comment><![CDATA[Accept payments with a PCI-compliant checkout that keeps customers on your site. (<u>Includes Express Checkout</u>)]]></comment> - <attribute type="activity_path">payment/payflow_advanced/active</attribute> - <group id="configuration_details" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="4"> - <comment>http://docs.magento.com/m2/ce/user_guide/payment/paypal-payments-advanced.html</comment> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Hint</frontend_model> - </group> - <group id="required_settings" translate="label" showInDefault="1" showInWebsite="1" sortOrder="10"> - <label>Required PayPal Settings</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <group id="payments_advanced" translate="label" showInDefault="1" showInWebsite="1" sortOrder="10"> - <label>Payments Advanced and Express Checkout</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <field id="business_account" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_required_express_checkout/business_account"> - <label>Email Associated with PayPal Merchant Account (Optional)</label> - </field> - <field id="partner" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1"> - <label>Partner</label> - <config_path>payment/payflow_advanced/partner</config_path> - <attribute type="shared">1</attribute> - </field> - <field id="vendor" translate="label" type="text" sortOrder="30" showInDefault="1" showInWebsite="1"> - <label>Vendor</label> - <config_path>payment/payflow_advanced/vendor</config_path> - <attribute type="shared">1</attribute> - </field> - <field id="user" translate="label comment tooltip" type="obscure" sortOrder="40" showInDefault="1" showInWebsite="1"> - <label>User</label> - <comment>PayPal recommends that you set up an additional User on your account at manager.paypal.com</comment> - <tooltip>PayPal recommends you set up an additional User on your account at manager.paypal.com, instead of entering your admin username and password here. This will enhance your security and prevent service interruptions if you later change your password. If you do not want to set up an additional User, you can re-enter your Merchant Login here.</tooltip> - <config_path>payment/payflow_advanced/user</config_path> - <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> - <attribute type="shared">1</attribute> - </field> - <field id="pwd" translate="label" type="obscure" sortOrder="50" showInDefault="1" showInWebsite="1"> - <label>Password</label> - <config_path>payment/payflow_advanced/pwd</config_path> - <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> - <attribute type="shared">1</attribute> - </field> - <field id="sandbox_flag" translate="label" type="select" sortOrder="60" showInDefault="1" showInWebsite="1"> - <label>Test Mode</label> - <config_path>payment/payflow_advanced/sandbox_flag</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="use_proxy" translate="label" type="select" sortOrder="70" showInDefault="1" showInWebsite="1"> - <label>Use Proxy</label> - <config_path>payment/payflow_advanced/use_proxy</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="proxy_host" translate="label" type="text" sortOrder="80" showInDefault="1" showInWebsite="1"> - <label>Proxy Host</label> - <config_path>payment/payflow_advanced/proxy_host</config_path> - <depends> - <field id="use_proxy">1</field> - </depends> - <attribute type="shared">1</attribute> - </field> - <field id="proxy_port" translate="label" type="text" sortOrder="90" showInDefault="1" showInWebsite="1"> - <label>Proxy Port</label> - <config_path>payment/payflow_advanced/proxy_port</config_path> - <depends> - <field id="use_proxy">1</field> - </depends> - <attribute type="shared">1</attribute> - </field> - <field id="payflow_advanced_info" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="100"> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Payflowlink\Advanced</frontend_model> - <attribute type="shared">1</attribute> - </field> - </group> - <field id="enable_payflow_advanced" translate="label comment" type="select" sortOrder="41" showInDefault="1" showInWebsite="1"> - <label>Enable this Solution</label> - <config_path>payment/payflow_advanced/active</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Enable\Payment</frontend_model> - <requires> - <group id="payments_advanced"/> - </requires> - </field> - <field id="enable_express_checkout" extends="payment_all_paypal/payflow_link/payflow_link_required/enable_express_checkout_basic" showInDefault="1" showInWebsite="1"> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Hidden</frontend_model> - <requires> - <field id="enable_payflow_advanced"/> - </requires> - </field> - <field id="enable_express_checkout_bml" sortOrder="42" extends="payment_all_paypal/express_checkout/express_checkout_required/enable_express_checkout_bml"> - <comment><![CDATA[PayPal Express Checkout Payflow Edition lets you give customers access to financing through PayPal Credit® - at no additional cost to you. - You get paid up front, even though customers have more time to pay. A pre-integrated payment button lets customers pay quickly with PayPal Credit®. - <a href="https://www.paypal.com/webapps/mpp/promotional-financing" target="_blank">Learn More</a>]]> - </comment> - <config_path>payment/payflow_express_bml/active</config_path> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Enable\Bml</frontend_model> - <requires> - <field id="enable_payflow_advanced"/> - </requires> - </field> - <field id="express_checkout_bml_sort_order" sortOrder="50" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_bml_sort_order"> - <config_path>payment/payflow_express_bml/sort_order</config_path> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Depends\BmlSortOrder</frontend_model> - <depends> - <field id="enable_express_checkout_bml">1</field> - </depends> - </field> - <group id="advanced_advertise_bml" showInDefault="1" showInWebsite="1" sortOrder="60" translate="label comment"> - <label>Advertise PayPal Credit</label> - <comment> - <![CDATA[<a href="https://financing.paypal.com/ppfinportal/content/whyUseFinancing" target="_blank">Why Advertise Financing?</a><br/> - <strong>Give your sales a boost when you advertise financing.</strong><br/>PayPal helps turn browsers into buyers with financing - from PayPal Credit®. Your customers have more time to pay, while you get paid up front – at no additional cost to you. - Use PayPal’s free banner ads that let you advertise PayPal Credit® financing as a payment option when your customers check out with PayPal. - The PayPal Advertising Program has been shown to generate additional purchases as well as increase consumer's average purchase sizes by 15% - or more. <a href="https://financing.paypal.com/ppfinportal/content/forrester" target="_blank">See Details</a>.]]> - </comment> - <field id="bml_publisher_id" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/bml_publisher_id" /> - <field id="bml_wizard" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/bml_wizard" /> - <group id="advanced_settings_bml_homepage" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="20" translate="label"> - <label>Home Page</label> - <field id="bml_homepage_display" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_homepage/bml_homepage_display"/> - <field id="advanced_bml_homepage_position" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_homepage/bml_homepage_position"/> - <field id="advanced_bml_homepage_size1" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1"> - <label>Size</label> - <config_path>payment/paypal_express_bml/homepage_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeHPH</source_model> - <depends> - <field id="advanced_bml_homepage_position">0</field> - </depends> - </field> - <field id="advanced_bml_homepage_size2" extends="payment_us/paypal_group_all_in_one/payflow_advanced/required_settings/advanced_advertise_bml/advanced_settings_bml_homepage/advanced_bml_homepage_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeHPS</source_model> - <depends> - <field id="advanced_bml_homepage_position">1</field> - </depends> - </field> - </group> - <group id="advanced_settings_bml_categorypage" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="30" translate="label"> - <label>Catalog Category Page</label> - <field id="bml_categorypage_display" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_categorypage/bml_categorypage_display"/> - <field id="advanced_bml_categorypage_position" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_categorypage/bml_categorypage_position"/> - <field id="advanced_bml_categorypage_size1" type="select" showInDefault="1" showInWebsite="1" sortOrder="30" translate="label"> - <label>Size</label> - <config_path>payment/paypal_express_bml/categorypage_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCCPC</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="advanced_bml_categorypage_position">0</field> - </depends> - </field> - <field id="advanced_bml_categorypage_size2" extends="payment_us/paypal_group_all_in_one/payflow_advanced/required_settings/advanced_advertise_bml/advanced_settings_bml_categorypage/advanced_bml_categorypage_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCCPS</source_model> - <depends> - <field id="advanced_bml_categorypage_position">1</field> - </depends> - </field> - </group> - <group id="advanced_settings_bml_productpage" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="40" translate="label"> - <label>Catalog Product Page</label> - <field id="bml_productpage_display" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_productpage/bml_productpage_display" /> - <field id="advanced_bml_productpage_position" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_productpage/bml_productpage_position" /> - <field id="advanced_bml_productpage_size1" type="select" sortOrder="30" showInDefault="1" showInWebsite="1" translate="label"> - <label>Size</label> - <config_path>payment/paypal_express_bml/productpage_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCPPC</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="advanced_bml_productpage_position">0</field> - </depends> - </field> - <field id="advanced_bml_productpage_size2" extends="payment_us/paypal_group_all_in_one/payflow_advanced/required_settings/advanced_advertise_bml/advanced_settings_bml_productpage/advanced_bml_productpage_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCPPN</source_model> - <depends> - <field id="advanced_bml_productpage_position">1</field> - </depends> - </field> - - </group> - <group id="advanced_settings_bml_checkout" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="50" translate="label"> - <label>Checkout Cart Page</label> - <field id="bml_checkout_display" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_checkout/bml_checkout_display"/> - <field id="advanced_bml_checkout_position" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_checkout/bml_checkout_position"/> - <field id="advanced_bml_checkout_size1" type="select" showInDefault="1" showInWebsite="1" sortOrder="30" translate="label"> - <label>Size</label> - <config_path>payment/paypal_express_bml/checkout_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCheckoutC</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="advanced_bml_checkout_position">0</field> - </depends> - </field> - <field id="advanced_bml_checkout_size2" extends="payment_us/paypal_group_all_in_one/payflow_advanced/required_settings/advanced_advertise_bml/advanced_settings_bml_checkout/advanced_bml_checkout_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCheckoutN</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="advanced_bml_checkout_position">1</field> - </depends> - </field> - </group> - </group> - </group> - <group id="settings_payments_advanced" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="20"> - <label>Basic Settings - PayPal Payments Advanced</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <field id="title" translate="label comment" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Title</label> - <comment>It is recommended to set this value to "Debit or Credit Card" per store views.</comment> - <config_path>payment/payflow_advanced/title</config_path> - <attribute type="shared">1</attribute> - </field> - <field id="sort_order" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Sort Order</label> - <config_path>payment/payflow_advanced/sort_order</config_path> - <frontend_class>validate-number</frontend_class> - <attribute type="shared">1</attribute> - </field> - <field id="payment_action" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1"> - <label>Payment Action</label> - <config_path>payment/payflow_advanced/payment_action</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\PaymentActions</source_model> - <attribute type="shared">1</attribute> - </field> - <group id="settings_payments_advanced_advanced" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="40"> - <label>Advanced Settings</label> - <fieldset_css>config-advanced</fieldset_css> - <field id="allowspecific" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1"> - <label>Payment Applicable From</label> - <config_path>payment/payflow_advanced/allowspecific</config_path> - <source_model>Magento\Payment\Model\Config\Source\Allspecificcountries</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="specificcountry" translate="label" type="multiselect" sortOrder="25" showInDefault="1" showInWebsite="1"> - <label>Countries Payment Applicable From</label> - <config_path>payment/payflow_advanced/specificcountry</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BuyerCountry</source_model> - <depends> - <field id="allowspecific">1</field> - </depends> - <attribute type="shared">1</attribute> - </field> - <field id="debug" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1"> - <label>Debug Mode</label> - <config_path>payment/payflow_advanced/debug</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="verify_peer" translate="label" type="select" sortOrder="35" showInDefault="1" showInWebsite="1"> - <label>Enable SSL verification</label> - <config_path>payment/payflow_advanced/verify_peer</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="csc_editable" translate="label" type="select" sortOrder="40" showInDefault="1" showInWebsite="1"> - <label>CVV Entry is Editable</label> - <config_path>payment/payflow_advanced/csc_editable</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="csc_required" translate="label" type="select" sortOrder="50" showInDefault="1" showInWebsite="1"> - <label>Require CVV Entry</label> - <config_path>payment/payflow_advanced/csc_required</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <depends> - <field id="csc_editable">1</field> - </depends> - <attribute type="shared">1</attribute> - </field> - <field id="email_confirmation" translate="label" type="select" sortOrder="60" showInDefault="1" showInWebsite="1"> - <label>Send Email Confirmation</label> - <config_path>payment/payflow_advanced/email_confirmation</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="url_method" translate="label" type="select" sortOrder="70" showInDefault="1" showInWebsite="1"> - <label>URL method for Cancel URL and Return URL</label> - <config_path>payment/payflow_advanced/url_method</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\UrlMethod</source_model> - <attribute type="shared">1</attribute> - </field> - <group id="settlement_report" translate="label" showInDefault="1" showInWebsite="1" sortOrder="80"> - <label>Settlement Report Settings</label> - <field id="heading_sftp" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/heading_sftp"/> - <field id="settlement_reports_ftp_login" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_login"/> - <field id="settlement_reports_ftp_password" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_password"/> - <field id="settlement_reports_ftp_sandbox" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_sandbox"/> - <field id="settlement_reports_ftp_ip" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_ip"/> - <field id="settlement_reports_ftp_path" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_ftp_path"/> - <field id="heading_schedule" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/heading_schedule"/> - <field id="settlement_reports_active" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_active"/> - <field id="settlement_reports_schedule" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_schedule"/> - <field id="settlement_reports_time" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_settlement_report/settlement_reports_time"/> - </group> - <group id="frontend" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="90"> - <label>Frontend Experience Settings</label> - <field id="logo" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/logo"/> - <field id="paypal_pages" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_pages"/> - <field id="page_style" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/page_style"/> - <field id="paypal_hdrimg" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_hdrimg"/> - <field id="paypal_hdrbackcolor" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_hdrbackcolor"/> - <field id="paypal_hdrbordercolor" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_hdrbordercolor"/> - <field id="paypal_payflowcolor" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_payflowcolor"/> - </group> - </group> - </group> - <group id="settings_express_checkout" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="30"> - <label>Basic Settings - PayPal Express Checkout</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <field id="title" extends="payment_us/paypal_payment_gateways/paypal_payflowpro_with_express_checkout/paypal_payflow_express_checkout/title" /> - <field id="sort_order" extends="payment_us/paypal_payment_gateways/paypal_payflowpro_with_express_checkout/paypal_payflow_express_checkout/sort_order" /> - <field id="payment_action" extends="payment_us/paypal_payment_gateways/paypal_payflowpro_with_express_checkout/paypal_payflow_express_checkout/payment_action" /> - <field id="visible_on_product" extends="payment_us/paypal_payment_gateways/paypal_payflowpro_with_express_checkout/paypal_payflow_express_checkout/visible_on_product" /> - <group id="settings_express_checkout_advanced" extends="payment_us/paypal_payment_gateways/paypal_payflowpro_with_express_checkout/paypal_payflow_express_checkout/paypal_payflow_express_checkout_advanced"/> - </group> - </group> - <group id="wpp_usuk" translate="label" sortOrder="40" extends="payment_us/paypal_payment_gateways/paypal_payflowpro_with_express_checkout"> - <label>Payments Pro</label> - <attribute type="activity_path">payment/paypal_payment_pro/active</attribute> - <group id="configuration_details"> - <comment>http://docs.magento.com/m2/ce/user_guide/payment/paypal-payments-pro.html</comment> - </group> - <group id="paypal_payflow_required" translate="label" showInDefault="1" showInWebsite="1" sortOrder="10"> - <field id="enable_paypal_payflow"> - <attribute type="shared">0</attribute> - <config_path>payment/paypal_payment_pro/active</config_path> - </field> - <group id="paypal_payflow_api_settings" translate="label"> - <label>Payments Pro and Express Checkout</label> - </group> - </group> - <group id="settings_paypal_payflow" translate="label"> - <label>Basic Settings - PayPal Payments Pro</label> - </group> - </group> - <group id="wps_express" extends="payment_all_paypal/express_checkout"> - <label>Payments Standard</label> - <comment>Accept credit card and PayPal payments securely.</comment> - <attribute type="activity_path">payment/wps_express/active</attribute> - <group id="configuration_details"> - <comment>http://docs.magento.com/m2/ce/user_guide/payment/paypal-payments-standard.html</comment> - </group> - <group id="express_checkout_required"> - <field id="enable_in_context_checkout" showInDefault="0" showInWebsite="0"/> - <field id="merchant_id" showInDefault="0" showInWebsite="0"/> - <group id="express_checkout_required_express_checkout"> - <label>Payments Standard</label> - </group> - <field id="enable_express_checkout"> - <config_path>payment/wps_express/active</config_path> - </field> - <field id="enable_express_checkout_bml"> - <config_path>payment/wps_express_bml/active</config_path> - </field> - </group> - <group id="settings_ec"> - <label>Basic Settings - PayPal Website Payments Standard</label> - </group> - </group> - </group> - <group id="paypal_payment_gateways" translate="label" sortOrder="8" showInDefault="1" showInWebsite="1" showInStore="1"> - <label><![CDATA[PayPal Payment Gateways <i>Process payments using your own internet merchant account.</i>]]></label> - <fieldset_css>complex paypal-other-section paypal-gateways-section</fieldset_css> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <attribute type="displayIn">other_paypal_payment_solutions</attribute> - <group id="paypal_payflowpro_with_express_checkout" translate="label comment" extends="payment_all_paypal/paypal_payflowpro"> - <label>Payflow Pro</label> - <attribute type="paypal_ec_separate">0</attribute> - <group id="paypal_payflow_required" translate="label" showInDefault="1" showInWebsite="1" sortOrder="10"> - <label>Required PayPal Settings</label> - <field id="enable_paypal_payflow"> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Enable\Payment</frontend_model> - </field> - <group id="paypal_payflow_api_settings" translate="label"> - <label>Payflow Pro and Express Checkout</label> - <field id="business_account" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_required_express_checkout/business_account" translate="label" sortOrder="10"> - <frontend_class>not-required</frontend_class> - <label>Email Associated with PayPal Merchant Account (Optional)</label> - <attribute type="shared">1</attribute> - </field> - </group> - <field id="enable_express_checkout" extends="payment_all_paypal/payflow_link/payflow_link_required/enable_express_checkout_basic" showInDefault="1" showInWebsite="1"> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Hidden</frontend_model> - <requires> - <field id="enable_paypal_payflow"/> - </requires> - </field> - <field id="enable_in_context_checkout" showInDefault="0" showInWebsite="0"/> - <field id="merchant_id" showInDefault="0" showInWebsite="0"/> - <field id="enable_express_checkout_bml_payflow" translate="label" type="select" sortOrder="21" showInWebsite="1" showInDefault="1"> - <label>Enable PayPal Credit</label> - <comment><![CDATA[PayPal Express Checkout Payflow Edition lets you give customers access to financing through PayPal Credit® - at no additional cost to you. - You get paid up front, even though customers have more time to pay. A pre-integrated payment button lets customers pay quickly with PayPal Credit®. - <a href="https://www.paypal.com/webapps/mpp/promotional-financing" target="_blank">Learn More</a>]]> - </comment> - <config_path>payment/payflow_express_bml/active</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Enable\Bml</frontend_model> - <requires> - <field id="enable_paypal_payflow"/> - </requires> - </field> - <field id="express_checkout_bml_sort_order" sortOrder="30" extends="payment_all_paypal/express_checkout/express_checkout_required/express_checkout_bml_sort_order"> - <config_path>payment/payflow_express_bml/sort_order</config_path> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Depends\BmlSortOrder</frontend_model> - <depends> - <field id="enable_express_checkout_bml_payflow">1</field> - </depends> - </field> - <group id="paypal_payflow_advertise_bml" translate="label comment" showInDefault="1" showInWebsite="1" sortOrder="40"> - <label>Advertise PayPal Credit</label> - <comment> - <![CDATA[<a href="https://financing.paypal.com/ppfinportal/content/whyUseFinancing" target="_blank">Why Advertise Financing?</a><br/> - <strong>Give your sales a boost when you advertise financing.</strong><br/>PayPal helps turn browsers into buyers with financing - from PayPal Credit®. Your customers have more time to pay, while you get paid up front – at no additional cost to you. - Use PayPal’s free banner ads that let you advertise PayPal Credit® financing as a payment option when your customers check out with PayPal. - The PayPal Advertising Program has been shown to generate additional purchases as well as increase consumer's average purchase sizes by 15% - or more. <a href="https://financing.paypal.com/ppfinportal/content/forrester" target="_blank">See Details</a>.]]> - </comment> - <field id="bml_publisher_id" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/bml_publisher_id" /> - <field id="bml_wizard" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/bml_wizard" /> - <group id="paypal_payflow_settings_bml_homepage" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="20"> - <label>Home Page</label> - <field id="bml_homepage_display" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_homepage/bml_homepage_display"/> - <field id="paypal_payflow_bml_homepage_position" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_homepage/bml_homepage_position"/> - <field id="paypal_payflow_bml_homepage_size1" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="30"> - <label>Size</label> - <config_path>payment/paypal_express_bml/homepage_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeHPH</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="paypal_payflow_bml_homepage_position">0</field> - </depends> - </field> - <field id="paypal_payflow_bml_homepage_size2" extends="payment_us/paypal_payment_gateways/paypal_payflowpro_with_express_checkout/paypal_payflow_required/paypal_payflow_advertise_bml/paypal_payflow_settings_bml_homepage/paypal_payflow_bml_homepage_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeHPS</source_model> - <depends> - <field id="paypal_payflow_bml_homepage_position">1</field> - </depends> - </field> - </group> - <group id="paypal_payflow_settings_bml_categorypage" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="30"> - <label>Catalog Category Page</label> - <field id="bml_categorypage_display" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_categorypage/bml_categorypage_display"/> - <field id="paypal_payflow_bml_categorypage_position" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_categorypage/bml_categorypage_position" /> - <field id="paypal_payflow_bml_categorypage_size1" translate="label" sortOrder="30" showInWebsite="1" showInDefault="1" type="select"> - <label>Size</label> - <config_path>payment/paypal_express_bml/categorypage_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCCPC</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="paypal_payflow_bml_categorypage_position">0</field> - </depends> - </field> - <field id="paypal_payflow_bml_categorypage_size2" extends="payment_us/paypal_payment_gateways/paypal_payflowpro_with_express_checkout/paypal_payflow_required/paypal_payflow_advertise_bml/paypal_payflow_settings_bml_categorypage/paypal_payflow_bml_categorypage_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCCPS</source_model> - <depends> - <field id="paypal_payflow_bml_categorypage_position">1</field> - </depends> - </field> - </group> - <group id="paypal_payflow_settings_bml_productpage" translate="label" showInDefault="1" showInStore="1" showInWebsite="1" sortOrder="40"> - <label>Catalog Product Page</label> - <field id="bml_productpage_display" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_productpage/bml_productpage_display" /> - <field id="paypal_payflow_bml_productpage_position" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_productpage/bml_productpage_position" /> - <field id="paypal_payflow_bml_productpage_size1" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="30"> - <label>Size</label> - <config_path>payment/paypal_express_bml/productpage_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCPPC</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="paypal_payflow_bml_productpage_position">0</field> - </depends> - </field> - <field id="paypal_payflow_bml_productpage_size2" extends="payment_us/paypal_payment_gateways/paypal_payflowpro_with_express_checkout/paypal_payflow_required/paypal_payflow_advertise_bml/paypal_payflow_settings_bml_productpage/paypal_payflow_bml_productpage_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCPPN</source_model> - <depends> - <field id="paypal_payflow_bml_productpage_position">1</field> - </depends> - </field> - </group> - <group id="paypal_payflow_settings_bml_checkout" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="50"> - <label>Checkout Cart Page</label> - <field id="bml_checkout_display" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_checkout/bml_checkout_display" /> - <field id="paypal_payflow_bml_checkout_position" translate="label" extends="payment_all_paypal/express_checkout/express_checkout_required/advertise_bml/settings_bml_checkout/bml_checkout_position" /> - <field id="paypal_payflow_bml_checkout_size1" translate="label" type="select" showInDefault="1" showInWebsite="1" sortOrder="30"> - <label>Size</label> - <config_path>payment/paypal_express_bml/checkout_size</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCheckoutC</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="paypal_payflow_bml_checkout_position">0</field> - </depends> - </field> - <field id="paypal_payflow_bml_checkout_size2" extends="payment_us/paypal_payment_gateways/paypal_payflowpro_with_express_checkout/paypal_payflow_required/paypal_payflow_advertise_bml/paypal_payflow_settings_bml_checkout/paypal_payflow_bml_checkout_size1"> - <source_model>Magento\Paypal\Model\System\Config\Source\BmlSize::getBmlSizeCheckoutN</source_model> - <attribute type="shared">1</attribute> - <depends> - <field id="paypal_payflow_bml_checkout_position">1</field> - </depends> - </field> - </group> - </group> - </group> - <group id="settings_paypal_payflow" translate="label"> - <group id="settings_paypal_payflow_advanced" translate="label"> - <group id="paypal_payflow_frontend" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="100"> - <label>Frontend Experience Settings</label> - <field id="logo" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/logo"/> - <field id="paypal_pages" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_pages"/> - <field id="page_style" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/page_style"/> - <field id="paypal_hdrimg" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_hdrimg"/> - <field id="paypal_hdrbackcolor" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_hdrbackcolor"/> - <field id="paypal_hdrbordercolor" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_hdrbordercolor"/> - <field id="paypal_payflowcolor" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_payflowcolor"/> - </group> - </group> - </group> - <group id="paypal_payflow_express_checkout" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="30"> - <label>Basic Settings - PayPal Express Checkout</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <field id="title" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Title</label> - <config_path>payment/payflow_express/title</config_path> - <attribute type="shared">1</attribute> - </field> - <field id="sort_order" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Sort Order</label> - <config_path>payment/payflow_express/sort_order</config_path> - <frontend_class>validate-number</frontend_class> - <attribute type="shared">1</attribute> - </field> - <field id="payment_action" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1"> - <label>Payment Action</label> - <config_path>payment/payflow_express/payment_action</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\PaymentActions</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="visible_on_product" translate="label" type="select" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Display on Product Details Page</label> - <config_path>payment/payflow_express/visible_on_product</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <group id="paypal_payflow_express_checkout_advanced" translate="label" showInDefault="1" showInWebsite="1" sortOrder="60"> - <label>Advanced Settings</label> - <fieldset_css>config-advanced</fieldset_css> - <field id="visible_on_cart" translate="label comment" type="select" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Display on Shopping Cart</label> - <comment>Also affects mini-shopping cart.</comment> - <config_path>payment/payflow_express/visible_on_cart</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\Yesnoshortcut</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="allowspecific" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1"> - <label>Payment Applicable From</label> - <config_path>payment/payflow_express/allowspecific</config_path> - <source_model>Magento\Payment\Model\Config\Source\Allspecificcountries</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="specificcountry" translate="label" type="multiselect" sortOrder="20" showInDefault="1" showInWebsite="1"> - <label>Countries Payment Applicable From</label> - <config_path>payment/payflow_express/specificcountry</config_path> - <source_model>Magento\Paypal\Model\System\Config\Source\BuyerCountry</source_model> - <depends> - <field id="allowspecific">1</field> - </depends> - <attribute type="shared">1</attribute> - </field> - <field id="debug" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1"> - <label>Debug Mode</label> - <config_path>payment/payflow_express/debug</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="verify_peer" translate="label" type="select" sortOrder="35" showInDefault="1" showInWebsite="1"> - <label>Enable SSL verification</label> - <config_path>payment/payflow_express/verify_peer</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="line_items_enabled" translate="label" type="select" sortOrder="40" showInDefault="1" showInWebsite="1"> - <label>Transfer Cart Line Items</label> - <config_path>payment/payflow_express/line_items_enabled</config_path> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <attribute type="shared">1</attribute> - </field> - <field id="skip_order_review_step" sortOrder="50" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/skip_order_review_step"/> - </group> - </group> - </group> - <group id="payflow_link_us" extends="payment_all_paypal/payflow_link"/> - </group> - <group id="paypal_alternative_payment_methods" sortOrder="5" showInDefault="0" showInWebsite="0" showInStore="0"> - <group id="express_checkout_us" translate="label comment" extends="payment_all_paypal/express_checkout" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>PayPal Express Checkout</label> - <fieldset_css>complex paypal-express-section</fieldset_css> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Payment</frontend_model> - <comment>Add another payment method to your existing solution or as a stand-alone option.</comment> - <help_url>https://merchant.paypal.com/cgi-bin/marketingweb?cmd=_render-content</help_url> - <attribute type="shared">0</attribute> - <attribute type="activity_path">payment/paypal_express/active</attribute> - <attribute type="activity_path">payment/payflow_express/active</attribute> - <attribute type="displayIn">recommended_solutions</attribute> - </group> - </group> - </section> - <section id="payment_gb" extends="payment" showInDefault="0" showInWebsite="0" showInStore="0"> - <group id="paypal_group_all_in_one" translate="label comment" sortOrder="7" showInDefault="1" showInWebsite="1" showInStore="1"> - <label><![CDATA[PayPal All-in-One Payment Solutions  <i>Accept and process credit cards and PayPal payments.</i>]]></label> - <fieldset_css>complex paypal-other-section paypal-all-in-one-section</fieldset_css> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <comment>Choose a secure bundled payment solution for your business.</comment> - <help_url>https://www.paypal-marketing.com/emarketing/partner/na/merchantlineup/home.page#mainTab=checkoutlineup&subTab=newlineup</help_url> - <attribute type="displayIn">other_paypal_payment_solutions</attribute> - <group id="wpp_usuk" translate="comment" sortOrder="20"> - <fieldset_css>pp-general-uk</fieldset_css> - <demo_link>http://www.youtube.com/watch?v=LBe-TW87eGI&list=PLF18B1094ABCD7CE8&index=1&feature=plpp_video</demo_link> - <comment>Accept payments with a completely customizable checkout page.</comment> - <group id="wpp_required_settings"> - <field id="enable_express_checkout_bml" showInDefault="0" showInWebsite="0"/> - <field id="express_checkout_bml_sort_order" showInDefault="0" showInWebsite="0"/> - <group id="wpp_advertise_bml" showInDefault="0" showInWebsite="0"/> - </group> - </group> - <group id="payments_pro_hosted_solution_with_express_checkout" translate="label comment" extends="payment_all_paypal/payments_pro_hosted_solution_without_bml" sortOrder="30"> - <label>Website Payments Pro Hosted Solution</label> - <attribute type="paypal_ec_separate">0</attribute> - <group id="pphs_required_settings"> - <group id="pphs_required_settings_pphs" translate="label"> - <label>Website Payments Pro Hosted Solution and Express Checkout</label> - </group> - <field id="pphs_enable"> - <requires> - <group id="pphs_required_settings_pphs"/> - </requires> - <frontend_class>paypal-enabler</frontend_class> - </field> - </group> - <group id="pphs_settings" translate="label"> - <label>Basic Settings - PayPal Website Payments Pro Hosted Solution</label> - <group id="pphs_settings_advanced"> - <group id="pphs_billing_agreement" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="40"> - <label>PayPal Billing Agreement Settings</label> - <field id="active" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_billing_agreement/active" /> - <field id="title" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_billing_agreement/title" /> - <field id="sort_order" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_billing_agreement/sort_order" /> - <field id="payment_action" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_billing_agreement/payment_action" /> - <field id="allowspecific" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_billing_agreement/allowspecific" /> - <field id="specificcountry" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_billing_agreement/specificcountry" /> - <field id="debug" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_billing_agreement/debug" /> - <field id="verify_peer" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_billing_agreement/verify_peer" /> - <field id="line_items_enabled" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_billing_agreement/line_items_enabled" /> - <field id="allow_billing_agreement_wizard" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_billing_agreement/allow_billing_agreement_wizard" /> - </group> - <group id="pphs_frontend" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="60"> - <label>Frontend Experience Settings</label> - <field id="logo" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/logo" /> - <field id="paypal_pages" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_pages" /> - <field id="page_style" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/page_style" /> - <field id="paypal_hdrimg" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_hdrimg" /> - <field id="paypal_hdrbackcolor" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_hdrbackcolor" /> - <field id="paypal_hdrbordercolor" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_hdrbordercolor" /> - <field id="paypal_payflowcolor" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/express_checkout_frontend/paypal_payflowcolor" /> - </group> - </group> - </group> - <group id="pphs_settings_express_checkout" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="30"> - <label>Basic Settings - PayPal Express Checkout</label> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <field id="title" extends="payment_all_paypal/express_checkout/settings_ec/title" /> - <field id="sort_order" extends="payment_all_paypal/express_checkout/settings_ec/sort_order" /> - <field id="payment_action" extends="payment_all_paypal/express_checkout/settings_ec/payment_action" /> - <field id="visible_on_product" extends="payment_all_paypal/express_checkout/settings_ec/visible_on_product" /> - <field id="authorization_honor_period" extends="payment_all_paypal/express_checkout/settings_ec/authorization_honor_period" /> - <field id="order_valid_period" extends="payment_all_paypal/express_checkout/settings_ec/order_valid_period" /> - <field id="child_authorization_number" extends="payment_all_paypal/express_checkout/settings_ec/child_authorization_number" /> - <group id="pphs_settings_express_checkout_advanced" translate="label" showInDefault="1" showInWebsite="1" showInStore="1" sortOrder="80"> - <label>Advanced Settings</label> - <fieldset_css>config-advanced</fieldset_css> - <field id="visible_on_cart" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/visible_on_cart" /> - <field id="allowspecific" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/allowspecific" /> - <field id="specificcountry" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/specificcountry" /> - <field id="debug" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/debug" /> - <field id="verify_peer" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/verify_peer" /> - <field id="line_items_enabled" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/line_items_enabled" /> - <field id="transfer_shipping_options" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/transfer_shipping_options" /> - <field id="button_flavor" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/button_flavor" /> - <field id="solution_type" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/solution_type" /> - <field id="require_billing_address" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/require_billing_address" /> - <field id="allow_ba_signup" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/allow_ba_signup" /> - <field id="skip_order_review_step" extends="payment_all_paypal/express_checkout/settings_ec/settings_ec_advanced/skip_order_review_step" /> - </group> - </group> - </group> - <group id="wps_express" extends="payment_all_paypal/express_checkout" sortOrder="50"> - <label>Website Payments Standard</label> - <comment>Accept credit card and PayPal payments securely.</comment> - <attribute type="activity_path">payment/wps_express/active</attribute> - <group id="configuration_details"> - <comment>http://docs.magento.com/m2/ce/user_guide/payment/paypal-payments-standard.html</comment> - </group> - <group id="express_checkout_required"> - <group id="express_checkout_required_express_checkout"> - <label>Website Payments Standard</label> - </group> - <field id="enable_express_checkout"> - <config_path>payment/wps_express/active</config_path> - </field> - <field id="enable_in_context_checkout" showInDefault="0" showInWebsite="0"/> - <field id="merchant_id" showInDefault="0" showInWebsite="0"/> - <field id="express_checkout_bml_sort_order" showInDefault="0" showInWebsite="0"/> - <field id="enable_express_checkout_bml" showInDefault="0" showInWebsite="0"/> - <group id="advertise_bml" showInDefault="0" showInWebsite="0"/> - </group> - <group id="settings_ec"> - <label>Basic Settings - PayPal Website Payments Standard</label> - </group> - </group> - </group> - <group id="paypal_alternative_payment_methods" sortOrder="5" showInDefault="0" showInWebsite="0" showInStore="0"> - <group id="express_checkout_gb" translate="label comment" extends="payment_all_paypal/express_checkout" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>PayPal Express Checkout</label> - <fieldset_css>complex paypal-express-section</fieldset_css> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Payment</frontend_model> - <comment>Add another payment method to your existing solution or as a stand-alone option.</comment> - <help_url>https://merchant.paypal.com/cgi-bin/marketingweb?cmd=_render-content</help_url> - <attribute type="displayIn">recommended_solutions</attribute> - <group id="express_checkout_required"> - <field id="express_checkout_bml_sort_order" showInDefault="0" showInWebsite="0"/> - <field id="enable_express_checkout_bml" showInDefault="0" showInWebsite="0"/> - <group id="advertise_bml" showInDefault="0" showInWebsite="0"/> - </group> - </group> - </group> - </section> - <section id="payment_de" extends="payment" showInDefault="0" showInWebsite="0" showInStore="0"> - <group id="paypal_payment_solutions" showInDefault="0" showInWebsite="0" showInStore="0" sortOrder="5"> - <group id="express_checkout_de" translate="label comment" extends="payment_all_paypal/express_checkout" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>PayPal Express Checkout</label> - <fieldset_css>complex paypal-express-section</fieldset_css> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Payment</frontend_model> - <comment>Add another payment method to your existing solution or as a stand-alone option.</comment> - <help_url>https://www.paypal-marketing.com/emarketing/partner/na/merchantlineup/home.page#mainTab=checkoutlineup&subTab=newlineup</help_url> - <attribute type="displayIn">recommended_solutions</attribute> - <group id="express_checkout_required"> - <field id="enable_express_checkout_bml" showInDefault="0" showInWebsite="0"/> - <field id="express_checkout_bml_sort_order" showInDefault="0" showInWebsite="0"/> - <group id="advertise_bml" showInDefault="0" showInWebsite="0"/> - </group> - <group id="settings_ec"> - <group id="settings_ec_advanced"> - <field id="solution_type" showInDefault="0" showInWebsite="0"/> - </group> - </group> - </group> - </group> - </section> - <section id="payment_other" extends="payment" showInDefault="0" showInWebsite="0" showInStore="0"> - <group id="express_checkout_other" translate="label comment" sortOrder="5" extends="payment_all_paypal/express_checkout" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>PayPal Express Checkout</label> - <fieldset_css>complex paypal-express-section</fieldset_css> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Payment</frontend_model> - <comment>Add another payment method to your existing solution or as a stand-alone option.</comment> - <help_url>https://www.paypal-marketing.com/emarketing/partner/na/merchantlineup/home.page#mainTab=checkoutlineup&subTab=newlineup</help_url> - <attribute type="displayIn">recommended_solutions</attribute> - <group id="express_checkout_required"> - <field id="enable_express_checkout_bml" showInDefault="0" showInWebsite="0"/> - <field id="express_checkout_bml_sort_order" showInDefault="0" showInWebsite="0"/> - <group id="advertise_bml" showInDefault="0" showInWebsite="0"/> - </group> - </group> - <group id="paypal_group_all_in_one" translate="label comment" sortOrder="7" showInDefault="1" showInWebsite="1" showInStore="1"> - <label><![CDATA[PayPal All-in-One Payment Solutions  <i>Accept and process credit cards and PayPal payments.</i>]]></label> - <fieldset_css>complex paypal-other-section paypal-all-in-one-section</fieldset_css> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <comment>Choose a secure bundled payment solution for your business.</comment> - <help_url>https://www.paypal-marketing.com/emarketing/partner/na/merchantlineup/home.page#mainTab=checkoutlineup&subTab=newlineup</help_url> - <attribute type="displayIn">other_paypal_payment_solutions</attribute> - <group id="wps_other" extends="payment_all_paypal/express_checkout" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Website Payments Standard</label> - <fieldset_css>complex</fieldset_css> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Payment</frontend_model> - <comment>Accept credit card and PayPal payments securely.</comment> - <attribute type="activity_path">payment/wps_express/active</attribute> - <group id="configuration_details"> - <comment>http://docs.magento.com/m2/ce/user_guide/payment/paypal-payments-standard.html</comment> - </group> - <group id="express_checkout_required"> - <group id="express_checkout_required_express_checkout"> - <label>Website Payments Standard</label> - </group> - <field id="enable_in_context_checkout" showInDefault="0" showInWebsite="0"/> - <field id="merchant_id" showInDefault="0" showInWebsite="0"/> - <field id="enable_express_checkout"> - <config_path>payment/wps_express/active</config_path> - </field> - <field id="enable_express_checkout_bml" showInDefault="0" showInWebsite="0"/> - <field id="express_checkout_bml_sort_order" showInDefault="0" showInWebsite="0"/> - <group id="advertise_bml" showInDefault="0" showInWebsite="0"/> - </group> - <group id="settings_ec"> - <label>Basic Settings - PayPal Website Payments Standard</label> - </group> - </group> - </group> - <group id="paypal_payment_gateways" translate="label comment" sortOrder="8" showInDefault="0" showInWebsite="0" showInStore="0"> - <label><![CDATA[PayPal Payment Gateways <i>Process payments using your own internet merchant account.</i>]]></label> - <fieldset_css>complex paypal-other-section paypal-gateways-section</fieldset_css> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <comment>Process payments using your own internet merchant account.</comment> - <help_url>https://merchant.paypal.com/cgi-bin/marketingweb?cmd=_render-content</help_url> - <attribute type="displayIn">other_paypal_payment_solutions</attribute> - </group> - </section> - <section id="payment_ca" extends="payment_other"> - <group id="express_checkout_other"> - <attribute type="activity_path">payment/paypal_express/active</attribute> - <attribute type="activity_path">payment/payflow_express/active</attribute> - </group> - <group id="paypal_group_all_in_one"> - <group id="wps_other" sortOrder="20"/> - </group> - <group id="paypal_payment_gateways" showInDefault="1" showInWebsite="1" showInStore="1"> - <fieldset_css>complex paypal-other-section paypal-gateways-section</fieldset_css> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Expanded</frontend_model> - <label><![CDATA[PayPal Payment Gateways <i>Process payments using your own internet merchant account.</i>]]></label> - <group id="wpp_ca" translate="label" extends="payment_all_paypal/paypal_payflowpro" sortOrder="30"> - <label>Website Payments Pro</label> - <attribute type="activity_path">payment/paypal_payment_pro/active</attribute> - <group id="configuration_details"> - <comment>http://docs.magento.com/m2/ce/user_guide/payment/paypal-payments-pro.html</comment> - </group> - <group id="paypal_payflow_required" translate="label" showInDefault="1" showInWebsite="1" sortOrder="10"> - <group id="paypal_payflow_api_settings"> - <label>Payments Pro</label> - </group> - <field id="enable_in_context_checkout" showInDefault="0" showInWebsite="0"/> - <field id="merchant_id" showInDefault="0" showInWebsite="0"/> - <field id="enable_paypal_payflow"> - <frontend_class>paypal-enabler paypal-ec-pe</frontend_class> - <attribute type="shared">0</attribute> - <config_path>payment/paypal_payment_pro/active</config_path> - <frontend_model>Magento\Paypal\Block\Adminhtml\System\Config\Field\Enable\Payment</frontend_model> - </field> - </group> - <group id="settings_paypal_payflow"> - <label>Basic Settings - PayPal Payments Pro</label> - </group> - </group> - <group id="paypal_payflowpro_ca" extends="payment_all_paypal/paypal_payflowpro" sortOrder="40"/> - <group id="payflow_link_ca" extends="payment_all_paypal/payflow_link" sortOrder="50"> - <group id="payflow_link_required"> - <field id="enable_express_checkout_bml" showInDefault="0" showInWebsite="0"/> - <field id="express_checkout_bml_sort_order" showInDefault="0" showInWebsite="0"/> - <group id="payflow_link_advertise_bml" showInDefault="0" showInWebsite="0"/> - </group> - </group> - </group> - </section> - <section id="payment_au" extends="payment_other"> - <group id="express_checkout_other"/> - <group id="paypal_group_all_in_one"> - <group id="wps_other" sortOrder="12"/> - <group id="payments_pro_hosted_solution_au" extends="payment_all_paypal/payments_pro_hosted_solution_without_bml" sortOrder="10"/> - </group> - <group id="paypal_payment_gateways" showInDefault="1" showInWebsite="1" showInStore="1"> - <group id="paypal_payflowpro_au" extends="payment_all_paypal/paypal_payflowpro" sortOrder="20"/> - </group> - </section> - <section id="payment_jp" extends="payment_other"> - <group id="express_checkout_other"/> - <group id="paypal_group_all_in_one"> - <group id="wps_other" sortOrder="12"/> - <group id="payments_pro_hosted_solution_jp" extends="payment_all_paypal/payments_pro_hosted_solution_without_bml" sortOrder="10"> - <label>Website Payments Plus</label> - </group> - </group> - </section> - <section id="payment_fr" extends="payment_other"> - <group id="express_checkout_other"/> - <group id="paypal_group_all_in_one"> - <group id="wps_other" sortOrder="12"/> - <group id="payments_pro_hosted_solution_fr" extends="payment_all_paypal/payments_pro_hosted_solution_without_bml" sortOrder="10"> - <label>Integral Evolution</label> - </group> - </group> - </section> - <section id="payment_it" extends="payment_other"> - <group id="express_checkout_other"/> - <group id="paypal_group_all_in_one"> - <group id="wps_other" sortOrder="12"/> - <group id="payments_pro_hosted_solution_it" extends="payment_all_paypal/payments_pro_hosted_solution_without_bml" sortOrder="10"> - <label>Pro</label> - </group> - </group> - </section> - <section id="payment_es" extends="payment_other"> - <group id="express_checkout_other"/> - <group id="paypal_group_all_in_one"> - <group id="wps_other" sortOrder="12"/> - <group id="payments_pro_hosted_solution_es" extends="payment_all_paypal/payments_pro_hosted_solution_without_bml" sortOrder="10"> - <label>Pasarela integral</label> - </group> - </group> - </section> - <section id="payment_hk" extends="payment_other"> - <group id="express_checkout_other"/> - <group id="paypal_group_all_in_one"> - <group id="wps_other" sortOrder="12"/> - <group id="payments_pro_hosted_solution_hk" extends="payment_all_paypal/payments_pro_hosted_solution_without_bml" sortOrder="10"/> - </group> - </section> - <section id="payment_nz" extends="payment_other"> - <group id="express_checkout_other"/> - <group id="paypal_group_all_in_one"> - <group id="wps_other"/> - </group> - <group id="paypal_payment_gateways" showInDefault="1" showInWebsite="1" showInStore="1"> - <group id="paypal_payflowpro_nz" extends="payment_all_paypal/paypal_payflowpro"/> - </group> - </section> - </system> -</config> diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php b/dev/tests/integration/testsuite/Magento/Paypal/Model/Express/CheckoutTest.php index bd641dab26c0..3f7f8719fd58 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()) @@ -293,6 +293,7 @@ public function testReturnFromPaypal() /** * The case when handling address data from Paypal button. * System's address fields are replacing from export Paypal data. + * Billing and Shipping address are the same * * @magentoDataFixture Magento/Paypal/_files/quote_payment_express_with_customer.php * @magentoAppIsolation enabled @@ -301,24 +302,75 @@ public function testReturnFromPaypal() public function testReturnFromPaypalButton() { $quote = $this->getFixtureQuote(); + $quote->getShippingAddress()->setShippingMethod(''); + $this->prepareCheckoutModel($quote); $quote->getPayment()->setAdditionalInformation(Checkout::PAYMENT_INFO_BUTTON, 1); $this->checkoutModel->returnFromPaypal('token'); $shippingAddress = $quote->getShippingAddress(); - $prefix = ''; + $billingAddress = $quote->getBillingAddress(); + $exportedShippingData = $this->getExportedData()['shipping']; + + $this->assertEquals([$exportedShippingData['street']], $shippingAddress->getStreet()); + $this->assertEquals($exportedShippingData['firstname'], $shippingAddress->getFirstname()); + $this->assertEquals($exportedShippingData['city'], $shippingAddress->getCity()); + $this->assertEquals($exportedShippingData['telephone'], $shippingAddress->getTelephone()); + $this->assertEquals($exportedShippingData['email'], $shippingAddress->getEmail()); - $this->assertEquals([$prefix . $this->getExportedData()['street']], $shippingAddress->getStreet()); - $this->assertEquals($prefix . $this->getExportedData()['firstname'], $shippingAddress->getFirstname()); - $this->assertEquals($prefix . $this->getExportedData()['city'], $shippingAddress->getCity()); - $this->assertEquals($prefix . $this->getExportedData()['telephone'], $shippingAddress->getTelephone()); - $this->assertEquals($prefix . $this->getExportedData()['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()); + $this->assertEquals($exportedShippingData['telephone'], $billingAddress->getTelephone()); + $this->assertEquals($exportedShippingData['email'], $billingAddress->getEmail()); + } + + /** + * The case when handling address data from Paypal button. + * System's address fields are replacing from export Paypal data. + * Billing and Shipping address are different + * + * @magentoDataFixture Magento/Paypal/_files/quote_payment_express_with_customer.php + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + */ + public function testReturnFromPaypalButtonWithReturnBillingAddress() + { + $quote = $this->getFixtureQuote(); + $this->paypalConfig->expects($this->exactly(2)) + ->method('getValue') + ->with('requireBillingAddress') + ->willReturn(1); + $this->prepareCheckoutModel($quote); + $quote->getPayment()->setAdditionalInformation(Checkout::PAYMENT_INFO_BUTTON, 1); + + $this->checkoutModel->returnFromPaypal('token'); + + $shippingAddress = $quote->getShippingAddress(); + $billingAddress = $quote->getBillingAddress(); + $exportedBillingData = $this->getExportedData()['billing']; + $exportedShippingData = $this->getExportedData()['shipping']; + + $this->assertEquals([$exportedShippingData['street']], $shippingAddress->getStreet()); + $this->assertEquals($exportedShippingData['firstname'], $shippingAddress->getFirstname()); + $this->assertEquals($exportedShippingData['city'], $shippingAddress->getCity()); + $this->assertEquals($exportedShippingData['telephone'], $shippingAddress->getTelephone()); + $this->assertEquals($exportedShippingData['email'], $shippingAddress->getEmail()); + + $this->assertEquals([$exportedBillingData['street']], $billingAddress->getStreet()); + $this->assertEquals($exportedBillingData['firstname'], $billingAddress->getFirstname()); + $this->assertEquals($exportedBillingData['city'], $billingAddress->getCity()); + $this->assertEquals($exportedBillingData['telephone'], $billingAddress->getTelephone()); + $this->assertEquals($exportedBillingData['email'], $billingAddress->getEmail()); } /** * The case when handling address data from the checkout. * System's address fields are not replacing from export PayPal data. + * Billing and Shipping address are the same * * @magentoDataFixture Magento/Paypal/_files/quote_payment_express_with_customer.php * @magentoAppIsolation enabled @@ -326,20 +378,67 @@ public function testReturnFromPaypalButton() */ public function testReturnFromPaypalIfCheckout() { + $prefix = 'exported'; $quote = $this->getFixtureQuote(); - $this->prepareCheckoutModel($quote); + $this->prepareCheckoutModel($quote, $prefix); $quote->getPayment()->setAdditionalInformation(Checkout::PAYMENT_INFO_BUTTON, 0); + $originalShippingAddress = $quote->getShippingAddress(); + $originalBillingAddress = $quote->getBillingAddress(); + $this->checkoutModel->returnFromPaypal('token'); $shippingAddress = $quote->getShippingAddress(); + $billingAddress = $quote->getBillingAddress(); + + $this->assertEquals($originalShippingAddress->getStreet(), $shippingAddress->getStreet()); + $this->assertEquals($originalShippingAddress->getFirstname(), $shippingAddress->getFirstname()); + $this->assertEquals($originalShippingAddress->getCity(), $shippingAddress->getCity()); + $this->assertEquals($originalShippingAddress->getTelephone(), $shippingAddress->getTelephone()); + $this->assertEquals($originalBillingAddress->getStreet(), $billingAddress->getStreet()); + $this->assertEquals($originalBillingAddress->getFirstname(), $billingAddress->getFirstname()); + $this->assertEquals($originalBillingAddress->getCity(), $billingAddress->getCity()); + $this->assertEquals($originalBillingAddress->getTelephone(), $billingAddress->getTelephone()); + } + + /** + * The case when handling address data from the checkout. + * System's address fields are replacing billing address from export PayPal data. + * Billing and Shipping address are different + * + * @magentoDataFixture Magento/Paypal/_files/quote_payment_express_with_customer.php + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + */ + public function testReturnFromPaypalIfCheckoutWithReturnBillingAddress() + { $prefix = 'exported'; + $quote = $this->getFixtureQuote(); + $this->paypalConfig->expects($this->exactly(2)) + ->method('getValue') + ->with('requireBillingAddress') + ->willReturn(1); + $this->prepareCheckoutModel($quote, $prefix); + $quote->getPayment()->setAdditionalInformation(Checkout::PAYMENT_INFO_BUTTON, 0); + + $originalShippingAddress = $quote->getShippingAddress(); + + $this->checkoutModel->returnFromPaypal('token'); - $this->assertNotEquals([$prefix . $this->getExportedData()['street']], $shippingAddress->getStreet()); - $this->assertNotEquals($prefix . $this->getExportedData()['firstname'], $shippingAddress->getFirstname()); - $this->assertNotEquals($prefix . $this->getExportedData()['city'], $shippingAddress->getCity()); - $this->assertNotEquals($prefix . $this->getExportedData()['telephone'], $shippingAddress->getTelephone()); + $shippingAddress = $quote->getShippingAddress(); + $billingAddress = $quote->getBillingAddress(); + $exportedBillingData = $this->getExportedData()['billing']; + + $this->assertEquals($originalShippingAddress->getStreet(), $shippingAddress->getStreet()); + $this->assertEquals($originalShippingAddress->getFirstname(), $shippingAddress->getFirstname()); + $this->assertEquals($originalShippingAddress->getCity(), $shippingAddress->getCity()); + $this->assertEquals($originalShippingAddress->getTelephone(), $shippingAddress->getTelephone()); + + $this->assertEquals([$prefix . $exportedBillingData['street']], $billingAddress->getStreet()); + $this->assertEquals($prefix . $exportedBillingData['firstname'], $billingAddress->getFirstname()); + $this->assertEquals($prefix . $exportedBillingData['city'], $billingAddress->getCity()); + $this->assertEquals($prefix . $exportedBillingData['telephone'], $billingAddress->getTelephone()); } /** @@ -363,7 +462,7 @@ public function testReturnFromPayPalForCustomerWithEmptyAddresses(): void $billingAddress = $quote->getBillingAddress(); - $this->performQuoteAddressAssertions($billingAddress, $this->getExportedData()); + $this->performQuoteAddressAssertions($billingAddress, $this->getExportedData()['billing']); } /** @@ -437,7 +536,7 @@ private function performQuoteAddressAssertions(Address $address, array $expected * * @param Quote $quote */ - private function prepareCheckoutModel(Quote $quote) + private function prepareCheckoutModel(Quote $quote, $prefix = '') { $this->checkoutModel = $this->objectManager->create( Checkout::class, @@ -448,14 +547,17 @@ private function prepareCheckoutModel(Quote $quote) ] ); - $exportedBillingAddress = $this->getExportedAddressFixture($this->getExportedData()); + $exportedBillingAddress = $this->getExportedAddressFixture($this->getExportedData()['billing'], $prefix); $this->api->method('getExportedBillingAddress') ->willReturn($exportedBillingAddress); - $exportedShippingAddress = $this->getExportedAddressFixture($this->getExportedData()); + $exportedShippingAddress = $this->getExportedAddressFixture($this->getExportedData()['shipping'], $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()); } @@ -468,16 +570,30 @@ private function prepareCheckoutModel(Quote $quote) private function getExportedData(): array { return [ - 'email' => 'customer@example.com', - 'firstname' => 'John', - 'lastname' => 'Doe', - 'country' => 'US', - 'region' => 'Colorado', - 'region_id' => '13', - 'city' => 'Denver', - 'street' => '66 Pearl St', - 'postcode' => '80203', - 'telephone' => '555-555-555', + 'shipping' => [ + 'email' => 'customer@example.com', + 'firstname' => 'John', + 'lastname' => 'Doe', + 'country' => 'US', + 'region' => 'Colorado', + 'region_id' => '13', + 'city' => 'Denver', + 'street' => '66 Pearl St', + 'postcode' => '80203', + 'telephone' => '555-555-555', + ], + 'billing' => [ + 'email' => 'customer@example.com', + 'firstname' => 'Jane', + 'lastname' => 'Doe', + 'country' => 'US', + 'region' => 'Texas', + 'region_id' => '13', + 'city' => 'Austin', + 'street' => '1100 Congress Ave', + 'postcode' => '78701', + 'telephone' => '555-555-555' + ] ]; } diff --git a/dev/tests/integration/testsuite/Magento/Persistent/Block/Header/AdditionalTest.php b/dev/tests/integration/testsuite/Magento/Persistent/Block/Header/AdditionalTest.php index fba5354b691d..2e447e7854a7 100644 --- a/dev/tests/integration/testsuite/Magento/Persistent/Block/Header/AdditionalTest.php +++ b/dev/tests/integration/testsuite/Magento/Persistent/Block/Header/AdditionalTest.php @@ -56,8 +56,8 @@ public function testToHtml() $this->_customerSession->loginById(1); $translation = __('Not you?'); - $this->assertStringMatchesFormat( - '%A<span>%A<a%Ahref="' . $this->_block->getHref() . '"%A>' . $translation . '</a>%A</span>%A', + $this->assertContains( + '<a href="' . $this->_block->getHref() . '">' . $translation . '</a>', $this->_block->toHtml() ); $this->_customerSession->logout(); diff --git a/dev/tests/integration/testsuite/Magento/Persistent/Model/ObserverTest.php b/dev/tests/integration/testsuite/Magento/Persistent/Model/ObserverTest.php index ecf2cd77a13f..d0c253fc7a64 100644 --- a/dev/tests/integration/testsuite/Magento/Persistent/Model/ObserverTest.php +++ b/dev/tests/integration/testsuite/Magento/Persistent/Model/ObserverTest.php @@ -9,7 +9,6 @@ use Magento\Customer\Model\Context; /** - * @magentoDataFixture Magento/Persistent/_files/persistent.php * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ObserverTest extends \PHPUnit\Framework\TestCase @@ -29,11 +28,6 @@ class ObserverTest extends \PHPUnit\Framework\TestCase */ protected $customerRepository; - /** - * @var \Magento\Persistent\Helper\Session - */ - protected $_persistentSessionHelper; - /** * @var \Magento\Framework\ObjectManagerInterface */ @@ -44,11 +38,6 @@ class ObserverTest extends \PHPUnit\Framework\TestCase */ protected $_observer; - /** - * @var \Magento\Customer\Model\Session - */ - protected $_customerSession; - /** * @var \Magento\Checkout\Model\Session | \PHPUnit_Framework_MockObject_MockObject */ @@ -58,8 +47,6 @@ public function setUp() { $this->_objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $this->_customerSession = $this->_objectManager->get(\Magento\Customer\Model\Session::class); - $this->_customerViewHelper = $this->_objectManager->create( \Magento\Customer\Helper\View::class ); @@ -75,8 +62,6 @@ public function setUp() \Magento\Checkout\Model\Session::class )->disableOriginalConstructor()->setMethods([])->getMock(); - $this->_persistentSessionHelper = $this->_objectManager->create(\Magento\Persistent\Helper\Session::class); - $this->_observer = $this->_objectManager->create( \Magento\Persistent\Model\Observer::class, [ @@ -89,16 +74,11 @@ public function setUp() } /** - * @magentoConfigFixture current_store persistent/options/enabled 1 - * @magentoConfigFixture current_store persistent/options/remember_enabled 1 - * @magentoConfigFixture current_store persistent/options/remember_default 1 * @magentoAppArea frontend * @magentoAppIsolation enabled */ public function testEmulateWelcomeBlock() { - $this->_customerSession->loginById(1); - $httpContext = new \Magento\Framework\App\Http\Context(); $httpContext->setValue(Context::CONTEXT_AUTH, 1, 1); $block = $this->_objectManager->create( @@ -108,15 +88,7 @@ public function testEmulateWelcomeBlock() ] ); $this->_observer->emulateWelcomeBlock($block); - $customerName = $this->_escaper->escapeHtml( - $this->_customerViewHelper->getCustomerName( - $this->customerRepository->getById( - $this->_persistentSessionHelper->getSession()->getCustomerId() - ) - ) - ); - $translation = __('Welcome, %1!', $customerName)->__toString(); - $this->assertStringMatchesFormat('%A' . $translation . '%A', $block->getWelcome()->__toString()); - $this->_customerSession->logout(); + + $this->assertEquals(' ', $block->getWelcome()); } } diff --git a/dev/tests/integration/testsuite/Magento/Quote/Fixtures/quote_sec_website.php b/dev/tests/integration/testsuite/Magento/Quote/Fixtures/quote_sec_website.php new file mode 100644 index 000000000000..e0dfba50fcd5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Quote/Fixtures/quote_sec_website.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Address; +use Magento\Store\Api\Data\StoreInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; + +/** + * @var CustomerInterface $customer + * @var StoreInterface $store + * @var ProductInterface $product + */ +require __DIR__ . '/../../Customer/Fixtures/customer_sec_website.php'; +require __DIR__ . '/simple_product.php'; + +/** @var ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +$addressData = include __DIR__ . '/../../Customer/Fixtures/address_data.php'; +/** @var Address $shippingAddress */ +$shippingAddress = $objectManager->create(Address::class, ['data' => $addressData[0]]); +$shippingAddress->setAddressType('shipping'); + +$billingAddress = clone $shippingAddress; +$billingAddress->setId(null) + ->setAddressType('billing'); + +/** @var Quote $quote */ +$quote = $objectManager->create( + Quote::class, + [ + 'data' => [ + 'customer_id' => $customer->getId(), + 'store_id' => $store->getId(), + 'reserved_order_id' => '0000032134', + 'is_active' => true, + 'is_multishipping' => false + ] + ] +); +$quote->setShippingAddress($shippingAddress) + ->setBillingAddress($billingAddress) + ->addProduct($product); + +$quote->getPayment() + ->setMethod('checkmo'); +$quote->getShippingAddress() + ->setShippingMethod('flatrate_flatrate') + ->setCollectShippingRates(true); +$quote->collectTotals(); + +/** @var CartRepositoryInterface $repository */ +$repository = $objectManager->get(CartRepositoryInterface::class); +$repository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/Quote/Fixtures/quote_sec_website_rollback.php b/dev/tests/integration/testsuite/Magento/Quote/Fixtures/quote_sec_website_rollback.php new file mode 100644 index 000000000000..2244a1f22b1b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Quote/Fixtures/quote_sec_website_rollback.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Registry; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; + +/** @var ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->get(CartRepositoryInterface::class); +/** @var SearchCriteriaBuilder $searchCriteriaBuilder */ +$searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); +$searchCriteria = $searchCriteriaBuilder->addFilter('reserved_order_id', '0000032134') + ->create(); +$items = $quoteRepository->getList($searchCriteria) + ->getItems(); +foreach ($items as $item) { + $quoteRepository->delete($item); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +require __DIR__ . '/simple_product_rollback.php'; +require __DIR__ . '/../../Customer/Fixtures/customer_sec_website_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Quote/Fixtures/simple_product.php b/dev/tests/integration/testsuite/Magento/Quote/Fixtures/simple_product.php new file mode 100644 index 000000000000..49cb0501fe28 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Quote/Fixtures/simple_product.php @@ -0,0 +1,39 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Visibility; +use Magento\CatalogInventory\Api\Data\StockItemInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; + +/** @var ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +/** @var Product $product */ +$product = $objectManager->create(Product::class); +$product->setTypeId('simple') + ->setAttributeSetId(4) + ->setName('Simple Product') + ->setSku('simple002') + ->setPrice(10) + ->setQty(100) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED); + +/** @var StockItemInterface $stockItem */ +$stockItem = $objectManager->create(StockItemInterface::class); +$stockItem->setQty(100) + ->setIsInStock(true); +$extensionAttributes = $product->getExtensionAttributes(); +$extensionAttributes->setStockItem($stockItem); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$product = $productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Quote/Fixtures/simple_product_rollback.php b/dev/tests/integration/testsuite/Magento/Quote/Fixtures/simple_product_rollback.php new file mode 100644 index 000000000000..331615020ed7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Quote/Fixtures/simple_product_rollback.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; + +/** @var ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var SearchCriteriaBuilder $searchCriteriaBuilder */ +$searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); +$searchCriteria = $searchCriteriaBuilder->addFilter('sku', 'simple002') + ->create(); +$items = $productRepository->getList($searchCriteria) + ->getItems(); +foreach ($items as $product) { + $productRepository->delete($product); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/MaskedQuoteIdToQuoteIdTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/MaskedQuoteIdToQuoteIdTest.php new file mode 100644 index 000000000000..8d4a2ab4899a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/MaskedQuoteIdToQuoteIdTest.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model; + +use Magento\TestFramework\Helper\Bootstrap as BootstrapHelper; +use Magento\Quote\Api\GuestCartManagementInterface; + +class MaskedQuoteIdToQuoteIdTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var MaskedQuoteIdToQuoteIdInterface + */ + private $maskedQuoteIdToQuoteId; + + /** + * @var GuestCartManagementInterface + */ + private $guestCartManagement; + + protected function setUp() + { + $objectManager = BootstrapHelper::getObjectManager(); + $this->maskedQuoteIdToQuoteId = $objectManager->create(MaskedQuoteIdToQuoteIdInterface::class); + $this->guestCartManagement = $objectManager->create(GuestCartManagementInterface::class); + } + + public function testMaskedIdToQuoteId() + { + $maskedQuoteId = $this->guestCartManagement->createEmptyCart(); + $quoteId = $this->maskedQuoteIdToQuoteId->execute($maskedQuoteId); + + self::assertGreaterThan(0, $quoteId); + } + + public function testMaskedQuoteIdToQuoteIdForNonExistentQuote() + { + self::expectException(\Magento\Framework\Exception\NoSuchEntityException::class); + + $this->maskedQuoteIdToQuoteId->execute('test'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/Quote/Item/UpdaterTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/Quote/Item/UpdaterTest.php new file mode 100644 index 000000000000..a375b3c665f2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/Quote/Item/UpdaterTest.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\Quote\Item; + +use Magento\Quote\Api\CartItemRepositoryInterface; +use Magento\Quote\Api\Data\CartItemInterface; +use Magento\Quote\Model\Quote; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Tests \Magento\Quote\Model\Quote\Item\Updater + */ +class UpdaterTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Updater + */ + private $updater; + + /** + * @var \Magento\Framework\ObjectManagerInterface + */ + private $objectManager; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->updater = $this->objectManager->create(Updater::class); + } + + /** + * @magentoDataFixture Magento/Sales/_files/quote_with_custom_price.php + * @return void + */ + public function testUpdate(): void + { + /** @var CartItemRepositoryInterface $quoteItemRepository */ + $quoteItemRepository = $this->objectManager->create(CartItemRepositoryInterface::class); + /** @var Quote $quote */ + $quote = $this->objectManager->create(Quote::class); + $quoteId = $quote->load('test01', 'reserved_order_id')->getId(); + /** @var CartItemInterface[] $quoteItems */ + $quoteItems = $quoteItemRepository->getList($quoteId); + /** @var CartItemInterface $actualQuoteItem */ + $actualQuoteItem = array_pop($quoteItems); + $this->assertInstanceOf(CartItemInterface::class, $actualQuoteItem); + + $this->updater->update($actualQuoteItem, ['qty' => 1]); + + $this->assertNull( + $actualQuoteItem->getCustomPrice(), + 'Item custom price has to be null' + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteIdToMaskedQuoteIdTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteIdToMaskedQuoteIdTest.php new file mode 100644 index 000000000000..9301b76104d1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteIdToMaskedQuoteIdTest.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model; + +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\TestFramework\Helper\Bootstrap as BootstrapHelper; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; + +class QuoteIdToMaskedQuoteIdTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedQuoteId; + + protected function setUp() + { + $objectManager = BootstrapHelper::getObjectManager(); + $this->quoteIdToMaskedQuoteId = $objectManager->create(QuoteIdToMaskedQuoteIdInterface::class); + $this->quoteFactory = $objectManager->create(QuoteFactory::class); + $this->quoteResource = $objectManager->create(QuoteResource::class); + } + + /** + * @magentoDataFixture Magento/Sales/_files/quote.php + */ + public function testMaskedQuoteId() + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, 'test01', 'reserved_order_id'); + $maskedQuoteId = $this->quoteIdToMaskedQuoteId->execute((int) $quote->getId()); + + self::assertNotEmpty($maskedQuoteId); + } + + public function testMaskedQuoteIdWithNonExistentQuoteId() + { + self::expectException(NoSuchEntityException::class); + + $this->quoteIdToMaskedQuoteId->execute(0); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteManagementTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteManagementTest.php index 0b0071beb513..356117f2b3dc 100644 --- a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteManagementTest.php +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteManagementTest.php @@ -3,10 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Quote\Model; +use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product\Type; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Quote\Api\CartManagementInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Sales\Api\OrderRepositoryInterface; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; /** * Class for testing QuoteManagement model @@ -14,79 +22,120 @@ class QuoteManagementTest extends \PHPUnit\Framework\TestCase { /** - * Create order with product that has child items + * @var ObjectManager + */ + private $objectManager; + + /** + * @var CartManagementInterface + */ + private $cartManagement; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = Bootstrap::getObjectManager(); + + $this->cartManagement = $this->objectManager->create(CartManagementInterface::class); + } + + /** + * Creates order with product that has child items. * * @magentoAppIsolation enabled * @magentoDataFixture Magento/Sales/_files/quote_with_bundle.php */ public function testSubmit() { - /** - * Preconditions: - * Load quote with Bundle product that has at least two child products - */ - $objectManager = Bootstrap::getObjectManager(); - /** @var \Magento\Quote\Model\Quote $quote */ - $quote = $objectManager->create(\Magento\Quote\Model\Quote::class); - $quote->load('test01', 'reserved_order_id'); - - /** Execute SUT */ - /** @var \Magento\Quote\Api\CartManagementInterface $model */ - $cartManagement = $objectManager->create(\Magento\Quote\Api\CartManagementInterface::class); - /** @var \Magento\Sales\Api\OrderRepositoryInterface $orderRepository */ - $orderRepository = $objectManager->create(\Magento\Sales\Api\OrderRepositoryInterface::class); - $orderId = $cartManagement->placeOrder($quote->getId()); + $quote = $this->getQuote('test01'); + $orderId = $this->cartManagement->placeOrder($quote->getId()); + + /** @var OrderRepositoryInterface $orderRepository */ + $orderRepository = $this->objectManager->create(OrderRepositoryInterface::class); $order = $orderRepository->get($orderId); - /** Check if SUT caused expected effects */ $orderItems = $order->getItems(); - $this->assertCount(3, $orderItems); + self::assertCount(3, $orderItems); foreach ($orderItems as $orderItem) { if ($orderItem->getProductType() == Type::TYPE_SIMPLE) { - $this->assertNotEmpty($orderItem->getParentItem(), 'Parent is not set for child product'); - $this->assertNotEmpty($orderItem->getParentItemId(), 'Parent is not set for child product'); + self::assertNotEmpty($orderItem->getParentItem(), 'Parent is not set for child product'); + self::assertNotEmpty($orderItem->getParentItemId(), 'Parent is not set for child product'); } } } /** - * Create order with product that has child items and one of them was deleted + * Tries to create order with product that has child items and one of them was deleted. * * @magentoAppArea adminhtml * @magentoAppIsolation enabled * @magentoDataFixture Magento/Sales/_files/quote_with_bundle.php + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Some of the products below do not have all the required options. */ public function testSubmitWithDeletedItem() { - /** - * Preconditions: - * Load quote with Bundle product that have at least to child products - */ - $objectManager = Bootstrap::getObjectManager(); - /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ - $productRepository = $objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); $product = $productRepository->get('simple-2'); $productRepository->delete($product); - /** @var \Magento\Quote\Model\Quote $quote */ - $quote = $objectManager->create(\Magento\Quote\Model\Quote::class); - $quote->load('test01', 'reserved_order_id'); - - /** Execute SUT */ - /** @var \Magento\Quote\Api\CartManagementInterface $model */ - $cartManagement = $objectManager->create(\Magento\Quote\Api\CartManagementInterface::class); - /** @var \Magento\Sales\Api\OrderRepositoryInterface $orderRepository */ - $orderRepository = $objectManager->create(\Magento\Sales\Api\OrderRepositoryInterface::class); - $orderId = $cartManagement->placeOrder($quote->getId()); - $order = $orderRepository->get($orderId); + $quote = $this->getQuote('test01'); - /** Check if SUT caused expected effects */ - $orderItems = $order->getItems(); - $this->assertCount(2, $orderItems); - foreach ($orderItems as $orderItem) { - if ($orderItem->getProductType() == Type::TYPE_SIMPLE) { - $this->assertNotEmpty($orderItem->getParentItem(), 'Parent is not set for child product'); - $this->assertNotEmpty($orderItem->getParentItemId(), 'Parent is not set for child product'); - } - } + $this->cartManagement->placeOrder($quote->getId()); + } + + /** + * Tries to create order with item of stock during checkout. + * + * @magentoDataFixture Magento/Sales/_files/quote.php + * @expectedException \Magento\Framework\Exception\LocalizedException + * @expectedExceptionMessage Some of the products are out of stock. + * @magentoDbIsolation enabled + */ + public function testSubmitWithItemOutOfStock() + { + $this->makeProductOutOfStock('simple'); + $quote = $this->getQuote('test01'); + $this->cartManagement->placeOrder($quote->getId()); + } + + /** + * Gets quote by reserved order ID. + * + * @param string $reservedOrderId + * @return Quote + */ + private function getQuote(string $reservedOrderId): Quote + { + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter('reserved_order_id', $reservedOrderId) + ->create(); + + /** @var CartRepositoryInterface $quoteRepository */ + $quoteRepository = $this->objectManager->get(CartRepositoryInterface::class); + $items = $quoteRepository->getList($searchCriteria) + ->getItems(); + + return array_pop($items); + } + + /** + * Makes provided product as out of stock. + * + * @param string $sku + * @return void + */ + private function makeProductOutOfStock(string $sku) + { + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($sku); + $extensionAttributes = $product->getExtensionAttributes(); + $stockItem = $extensionAttributes->getStockItem(); + $stockItem->setIsInStock(false); + $productRepository->save($product); } } diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php index 6ea25c8f337d..15f555a67e72 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()); } /** @@ -362,7 +365,8 @@ public function testAddProductUpdateItem(): void $this->assertEquals(1, $quote->getItemsQty()); $this->expectException(\Magento\Framework\Exception\LocalizedException::class); - $this->expectExceptionMessage('The requested qty is not available'); + // TODO: fix test or implementation as described in https://github.com/magento-engcom/msi/issues/1037 +// $this->expectExceptionMessage('The requested qty is not available'); $updateParams['qty'] = $productStockQty + 1; $quote->updateItem($updateParams['id'], $updateParams); } diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteValidatorTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteValidatorTest.php index 922ee35a9df5..34b709509e4d 100644 --- a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteValidatorTest.php +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteValidatorTest.php @@ -7,6 +7,8 @@ namespace Magento\Quote\Model; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Quote\Api\CartRepositoryInterface; use Magento\Quote\Api\Data\AddressInterface; use Magento\Quote\Model\Quote\Address\Rate; use Magento\TestFramework\Helper\Bootstrap; @@ -55,7 +57,7 @@ public function testValidateBeforeSubmitCountryIsNotAllowed() )->setValue( 'general/country/allow', 'US', - \Magento\Store\Model\ScopeInterface::SCOPE_WEBSITES + \Magento\Store\Model\ScopeInterface::SCOPE_STORE ); $quote = $this->getQuote(); $quote->getShippingAddress()->setCountryId('AF'); @@ -132,6 +134,19 @@ public function testValidateBeforeSubmitWithMinimumOrderAmount() $this->quoteValidator->validateBeforeSubmit($quote); } + /** + * Checks a case when the default website has country restrictions and the quote created + * for the another website with different country restrictions. + * + * @magentoDataFixture Magento/Quote/Fixtures/quote_sec_website.php + * @magentoDbIsolation disabled + */ + public function testValidateBeforeSubmit() + { + $quote = $this->getQuoteById('0000032134'); + $this->quoteValidator->validateBeforeSubmit($quote); + } + /** * @return Quote */ @@ -185,4 +200,25 @@ private function getQuote(): Quote return $quote; } + + /** + * Gets quote entity by reserved order id. + * + * @param string $reservedOrderId + * @return Quote + */ + private function getQuoteById(string $reservedOrderId): Quote + { + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = Bootstrap::getObjectManager()->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter('reserved_order_id', $reservedOrderId) + ->create(); + + /** @var CartRepositoryInterface $repository */ + $repository = Bootstrap::getObjectManager()->get(CartRepositoryInterface::class); + $items = $repository->getList($searchCriteria) + ->getItems(); + + return array_pop($items); + } } diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/ShippingMethodManagementTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/ShippingMethodManagementTest.php index e2c25e76ceaa..8db7b65d0142 100644 --- a/dev/tests/integration/testsuite/Magento/Quote/Model/ShippingMethodManagementTest.php +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/ShippingMethodManagementTest.php @@ -28,6 +28,57 @@ public function testRateAppliedToShipping(): void $this->assertEquals(0, $customerQuote->getBaseGrandTotal()); } + /** + * @magentoConfigFixture current_store carriers/tablerate/active 1 + * @magentoConfigFixture current_store carriers/flatrate/active 0 + * @magentoConfigFixture current_store carriers/freeshipping/active 0 + * @magentoConfigFixture current_store carriers/tablerate/condition_name package_qty + * @magentoDataFixture Magento/SalesRule/_files/cart_rule_free_shipping_by_cart.php + * @magentoDataFixture Magento/Sales/_files/quote.php + * @magentoDataFixture Magento/OfflineShipping/_files/tablerates.php + * @return void + */ + public function testTableRateFreeShipping() + { + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + /** @var \Magento\Quote\Model\Quote $quote */ + $quote = $objectManager->get(\Magento\Quote\Model\Quote::class); + $quote->load('test01', 'reserved_order_id'); + $cartId = $quote->getId(); + if (!$cartId) { + $this->fail('quote fixture failed'); + } + /** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ + $quoteIdMask = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Quote\Model\QuoteIdMaskFactory::class) + ->create(); + $quoteIdMask->load($cartId, 'quote_id'); + //Use masked cart Id + $cartId = $quoteIdMask->getMaskedId(); + $data = [ + 'data' => [ + 'country_id' => "US", + 'postcode' => null, + 'region' => null, + 'region_id' => null + ] + ]; + /** @var \Magento\Quote\Api\Data\EstimateAddressInterface $address */ + $address = $objectManager->create(\Magento\Quote\Api\Data\EstimateAddressInterface::class, $data); + /** @var \Magento\Quote\Api\GuestShippingMethodManagementInterface $shippingEstimation */ + $shippingEstimation = $objectManager->get(\Magento\Quote\Api\GuestShippingMethodManagementInterface::class); + $result = $shippingEstimation->estimateByAddress($cartId, $address); + $this->assertNotEmpty($result); + $expectedResult = [ + 'method_code' => 'bestway', + 'amount' => 0 + ]; + foreach ($result as $rate) { + $this->assertEquals($expectedResult['amount'], $rate->getAmount()); + $this->assertEquals($expectedResult['method_code'], $rate->getMethodCode()); + } + } + /** * @magentoConfigFixture current_store carriers/tablerate/active 1 * @magentoConfigFixture current_store carriers/tablerate/condition_name package_qty @@ -51,6 +102,7 @@ public function testEstimateByAddressWithCartPriceRuleByItem() */ public function testEstimateByAddressWithCartPriceRuleByShipment() { + $this->markTestSkipped('According to MAGETWO-69940 it is an incorrect behavior'); // Rule applied to entire shipment should not overwrite flat or table rate shipping prices // Only rules applied to specific items should modify those prices (MAGETWO-63844) $this->executeTestFlow(5, 10); diff --git a/dev/tests/integration/testsuite/Magento/Reports/Block/Adminhtml/GridTest.php b/dev/tests/integration/testsuite/Magento/Reports/Block/Adminhtml/GridTest.php index bc9d0f895941..7398d9c2392b 100644 --- a/dev/tests/integration/testsuite/Magento/Reports/Block/Adminhtml/GridTest.php +++ b/dev/tests/integration/testsuite/Magento/Reports/Block/Adminhtml/GridTest.php @@ -5,18 +5,84 @@ */ namespace Magento\Reports\Block\Adminhtml; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\Locale\ResolverInterface; +use Magento\Framework\App\RequestInterface; +use Magento\Reports\Model\ResourceModel\Product\Sold\Collection\Initial; + /** * Test class for \Magento\Reports\Block\Adminhtml\Grid * @magentoAppArea adminhtml */ class GridTest extends \PHPUnit\Framework\TestCase { - public function testGetDateFormat() + /** + * @var $block \Magento\Reports\Block\Adminhtml\Grid + */ + private $block; + + /** + * @inheritDoc + */ + protected function setUp() { - /** @var $block \Magento\Reports\Block\Adminhtml\Grid */ - $block = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + $this->block = Bootstrap::getObjectManager()->get( \Magento\Reports\Block\Adminhtml\Grid::class ); - $this->assertNotEmpty($block->getDateFormat()); + } + + public function testGetDateFormat() + { + $this->assertNotEmpty($this->block->getDateFormat()); + } + + /** + * Test apply filtering to collection + * + * @param string $from + * @param string $to + * @param string $period + * @param string $locale + * @param int $expected + * @dataProvider getSalesRepresentativeIdDataProvider + */ + public function testGetPreparedCollection($from, $to, $period, $locale, $expected) + { + $encodedFilter = base64_encode('report_from='. $from . '&report_to=' . $to . '&report_period=' . $period); + + $this->block->setVarNameFilter('filtername'); + /** @var $request RequestInterface */ + $request = Bootstrap::getObjectManager()->get(RequestInterface::class); + $request->setParams(['filtername' => $encodedFilter]); + $request->setParams(['locale' => $locale]); + + /** @var $localeResolver ResolverInterface */ + $localeResolver = Bootstrap::getObjectManager()->get(ResolverInterface::class); + $localeResolver->setLocale(); + + /** @var $initialCollection Initial */ + $initialCollection = Bootstrap::getObjectManager()->create( + Initial::class + ); + $this->block->setData(['dataSource' => $initialCollection]); + + /** @var $collection Initial */ + $collection = $this->block->getPreparedCollection(); + $items = $collection->getItems(); + $this->assertCount($expected, $items); + } + + /** + * Data provider for testGetPreparedCollection method. + * + * @return array + */ + public function getSalesRepresentativeIdDataProvider() + { + return [ + 'Data for US locale' => ['08/15/2018', '08/20/2018', 'day', 'en_US', 6], + 'Data for Australian locale' => ['15/08/2018', '31/08/2018', 'day', 'en_AU', 17], + 'Data for French locale' => ['20.08.2018', '30.08.2018', 'day', 'fr_FR', 11], + ]; } } diff --git a/dev/tests/integration/testsuite/Magento/Reports/Block/Adminhtml/Shopcart/Abandoned/GridTest.php b/dev/tests/integration/testsuite/Magento/Reports/Block/Adminhtml/Shopcart/Abandoned/GridTest.php index b8446839c8c9..0af32b32c7d4 100644 --- a/dev/tests/integration/testsuite/Magento/Reports/Block/Adminhtml/Shopcart/Abandoned/GridTest.php +++ b/dev/tests/integration/testsuite/Magento/Reports/Block/Adminhtml/Shopcart/Abandoned/GridTest.php @@ -3,10 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Reports\Block\Adminhtml\Shopcart\Abandoned; use Magento\Quote\Model\Quote; use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\View\LayoutInterface; /** * Test class for \Magento\Reports\Block\Adminhtml\Shopcart\Abandoned\Grid @@ -20,12 +23,12 @@ class GridTest extends \Magento\Reports\Block\Adminhtml\Shopcart\GridTestAbstrac /** * @return void */ - public function testGridContent() + public function testGridContent(): void { - /** @var \Magento\Framework\View\LayoutInterface $layout */ - $layout = Bootstrap::getObjectManager()->get(\Magento\Framework\View\LayoutInterface::class); + /** @var LayoutInterface $layout */ + $layout = Bootstrap::getObjectManager()->get(LayoutInterface::class); /** @var Grid $grid */ - $grid = $layout->createBlock(\Magento\Reports\Block\Adminhtml\Shopcart\Abandoned\Grid::class); + $grid = $layout->createBlock(Grid::class); $grid->getRequest()->setParams(['filter' => base64_encode(urlencode('email=customer@example.com'))]); $result = $grid->getPreparedCollection(); @@ -35,4 +38,17 @@ public function testGridContent() $this->assertEquals('customer@example.com', $quote->getCustomerEmail()); $this->assertEquals(10.00, $quote->getSubtotal()); } + + /** + * @return void + */ + public function testPageSizeIsSetToNullWhenExportCsvFile(): void + { + /** @var LayoutInterface $layout */ + $layout = Bootstrap::getObjectManager()->get(LayoutInterface::class); + /** @var Grid $grid */ + $grid = $layout->createBlock(Grid::class); + $grid->getCsvFile(); + $this->assertNull($grid->getCollection()->getPageSize()); + } } diff --git a/dev/tests/integration/testsuite/Magento/Review/Controller/CaseCheckAddingProductReviewTest.php b/dev/tests/integration/testsuite/Magento/Review/Controller/CaseCheckAddingProductReviewTest.php new file mode 100644 index 000000000000..49e980ed5360 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Review/Controller/CaseCheckAddingProductReviewTest.php @@ -0,0 +1,120 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Review\Controller; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Data\Form\FormKey; +use Magento\Framework\Message\MessageInterface; +use Magento\TestFramework\Request; +use Magento\TestFramework\TestCase\AbstractController; + +/** + * Test review product controller behavior + * + * @magentoAppArea frontend + */ +class CaseCheckAddingProductReviewTest extends AbstractController +{ + /** + * Test adding a review for allowed guests with incomplete data by a not logged in user + * + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Review/_files/config.php + * @magentoDataFixture Magento/Catalog/_files/products.php + */ + public function testAttemptForGuestToAddReviewsWithIncompleteData() + { + $product = $this->getProduct(); + /** @var FormKey $formKey */ + $formKey = $this->_objectManager->get(FormKey::class); + $post = [ + 'nickname' => 'Test nick', + 'title' => 'Summary', + 'form_key' => $formKey->getFormKey(), + ]; + $this->prepareRequestData($post); + $this->dispatch('review/product/post/id/' . $product->getId()); + $this->assertSessionMessages( + $this->equalTo(['Please enter a review.']), + MessageInterface::TYPE_ERROR + ); + } + + /** + * Test adding a review for not allowed guests by a guest + * + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Review/_files/disable_config.php + * @magentoDataFixture Magento/Catalog/_files/products.php + */ + public function testAttemptForGuestToAddReview() + { + $product = $this->getProduct(); + /** @var FormKey $formKey */ + $formKey = $this->_objectManager->get(FormKey::class); + $post = [ + 'nickname' => 'Test nick', + 'title' => 'Summary', + 'detail' => 'Test Details', + 'form_key' => $formKey->getFormKey(), + ]; + + $this->prepareRequestData($post); + $this->dispatch('review/product/post/id/' . $product->getId()); + + $this->assertRedirect($this->stringContains('customer/account/login')); + } + + /** + * Test successfully adding a product review by a guest + * + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Review/_files/config.php + * @magentoDataFixture Magento/Catalog/_files/products.php + */ + public function testSuccessfullyAddingProductReviewForGuest() + { + $product = $this->getProduct(); + /** @var FormKey $formKey */ + $formKey = $this->_objectManager->get(FormKey::class); + $post = [ + 'nickname' => 'Test nick', + 'title' => 'Summary', + 'detail' => 'Test Details', + 'form_key' => $formKey->getFormKey(), + ]; + + $this->prepareRequestData($post); + $this->dispatch('review/product/post/id/' . $product->getId()); + + $this->assertSessionMessages( + $this->equalTo(['You submitted your review for moderation.']), + MessageInterface::TYPE_SUCCESS + ); + } + + /** + * @return ProductInterface + */ + private function getProduct() + { + return $this->_objectManager->get(ProductRepositoryInterface::class)->get('custom-design-simple-product'); + } + + /** + * @param array $postData + * @return void + */ + private function prepareRequestData($postData) + { + $this->getRequest()->setMethod(Request::METHOD_POST); + $this->getRequest()->setPostValue($postData); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Review/Model/ResourceModel/Review/Product/CollectionTest.php b/dev/tests/integration/testsuite/Magento/Review/Model/ResourceModel/Review/Product/CollectionTest.php index a7f859b23dfa..559c62e42dec 100644 --- a/dev/tests/integration/testsuite/Magento/Review/Model/ResourceModel/Review/Product/CollectionTest.php +++ b/dev/tests/integration/testsuite/Magento/Review/Model/ResourceModel/Review/Product/CollectionTest.php @@ -3,20 +3,88 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Review\Model\ResourceModel\Review\Product; +/** + * Tests some functionality of the Product Review collection + */ class CollectionTest extends \PHPUnit\Framework\TestCase { /** + * @param string $status + * @param int $expectedCount + * @param string $sortAttribute + * @param string $dir + * @param callable $assertion + * @dataProvider sortOrderAssertionsDataProvider * @magentoDataFixture Magento/Review/_files/different_reviews.php */ - public function testGetResultingIds() - { + public function testGetResultingIds( + ?string $status, + int $expectedCount, + string $sortAttribute, + string $dir, + callable $assertion + ) { + /** + * @var $collection \Magento\Review\Model\ResourceModel\Review\Product\Collection + */ $collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( \Magento\Review\Model\ResourceModel\Review\Product\Collection::class ); - $collection->addStatusFilter(\Magento\Review\Model\Review::STATUS_APPROVED); + if ($status) { + $collection->addStatusFilter($status); + } + $collection->setOrder($sortAttribute, $dir); $actual = $collection->getResultingIds(); - $this->assertCount(2, $actual); + $this->assertCount($expectedCount, $actual); + $assertion($actual); + } + + /** + * @return array + */ + public function sortOrderAssertionsDataProvider() :array + { + return [ + [ + \Magento\Review\Model\Review::STATUS_APPROVED, + 2, + 'rt.review_id', + 'DESC', + function (array $actual) :void { + $this->assertLessThan($actual[0], $actual[1]); + } + ], + [ + \Magento\Review\Model\Review::STATUS_APPROVED, + 2, + 'rt.review_id', + 'ASC', + function (array $actual) :void { + $this->assertLessThan($actual[1], $actual[0]); + } + ], + [ + \Magento\Review\Model\Review::STATUS_APPROVED, + 2, + 'rt.created_at', + 'ASC', + function (array $actual) :void { + $this->assertLessThan($actual[1], $actual[0]); + } + ], + [ + null, + 3, + 'rt.review_id', + 'ASC', + function (array $actual) :void { + $this->assertLessThan($actual[1], $actual[0]); + } + ] + ]; } } diff --git a/dev/tests/integration/testsuite/Magento/Review/_files/disable_config.php b/dev/tests/integration/testsuite/Magento/Review/_files/disable_config.php new file mode 100644 index 000000000000..ee21150bd612 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Review/_files/disable_config.php @@ -0,0 +1,15 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** @var Value $config */ +use Magento\Framework\App\Config\Value; + +$config = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(Value::class); +$config->setPath('catalog/review/allow_guest'); +$config->setScope('default'); +$config->setScopeId(0); +$config->setValue(0); +$config->save(); diff --git a/dev/tests/integration/testsuite/Magento/Rss/Controller/Feed/IndexTest.php b/dev/tests/integration/testsuite/Magento/Rss/Controller/Feed/IndexTest.php new file mode 100644 index 000000000000..9a611b8f2b9e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Rss/Controller/Feed/IndexTest.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Rss\Controller\Feed; + +class IndexTest extends \Magento\TestFramework\TestCase\AbstractBackendController +{ + /** + * @var \Magento\Rss\Model\UrlBuilder + */ + private $urlBuilder; + + /** + * @var \Magento\Customer\Api\CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var \Magento\Wishlist\Model\Wishlist + */ + private $wishlist; + + /** + * @var + */ + private $customerSession; + + protected function setUp() + { + parent::setUp(); + $this->urlBuilder = $this->_objectManager->get(\Magento\Rss\Model\UrlBuilder::class); + $this->customerRepository = $this->_objectManager->get( + \Magento\Customer\Api\CustomerRepositoryInterface::class + ); + $this->wishlist = $this->_objectManager->get(\Magento\Wishlist\Model\Wishlist::class); + $this->customerSession = $this->_objectManager->get(\Magento\Customer\Model\Session::class); + } + + /** + * Check Rss response. + * + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Wishlist/_files/two_wishlists_for_two_diff_customers.php + * @magentoConfigFixture current_store rss/wishlist/active 1 + * @magentoConfigFixture current_store rss/config/active 1 + */ + public function testRssResponse() + { + $firstCustomerId = 1; + $this->customerSession->setCustomerId($firstCustomerId); + $customer = $this->customerRepository->getById($firstCustomerId); + $customerEmail = $customer->getEmail(); + $wishlistId = $this->wishlist->loadByCustomerId($firstCustomerId)->getId(); + $this->dispatch($this->getLink($firstCustomerId, $customerEmail, $wishlistId)); + $body = $this->getResponse()->getBody(); + $this->assertContains('<title>John Smith\'s Wishlist', $body); + } + + /** + * Check Rss with incorrect wishlist id. + * + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Wishlist/_files/two_wishlists_for_two_diff_customers.php + * @magentoConfigFixture current_store rss/wishlist/active 1 + * @magentoConfigFixture current_store rss/config/active 1 + */ + public function testRssResponseWithIncorrectWishlistId() + { + $firstCustomerId = 1; + $secondCustomerId = 2; + $this->customerSession->setCustomerId($firstCustomerId); + $customer = $this->customerRepository->getById($firstCustomerId); + $customerEmail = $customer->getEmail(); + $wishlistId = $this->wishlist->loadByCustomerId($secondCustomerId, true)->getId(); + $this->dispatch($this->getLink($firstCustomerId, $customerEmail, $wishlistId)); + $body = $this->getResponse()->getBody(); + $this->assertContains('404 Not Found', $body); + } + + private function getLink($customerId, $customerEmail, $wishlistId) + { + + return 'rss/feed/index/type/wishlist/data/' + . base64_encode($customerId . ',' . $customerEmail) + . '/wishlist_id/' . $wishlistId; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Form/AccountTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Form/AccountTest.php index 999522a49e00..b75501911be6 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Form/AccountTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Form/AccountTest.php @@ -5,104 +5,166 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - declare(strict_types=1); namespace Magento\Sales\Block\Adminhtml\Order\Create\Form; use Magento\Backend\Model\Session\Quote as SessionQuote; +use Magento\Customer\Api\Data\AttributeMetadataInterface; use Magento\Customer\Api\Data\AttributeMetadataInterfaceFactory; +use Magento\Customer\Model\Data\Option; use Magento\Customer\Model\Metadata\Form; use Magento\Customer\Model\Metadata\FormFactory; use Magento\Framework\View\LayoutInterface; use Magento\Quote\Model\Quote; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use PHPUnit\Framework\MockObject\MockObject; /** + * Class for test Account + * * @magentoAppArea adminhtml + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class AccountTest extends \PHPUnit\Framework\TestCase { - /** @var Account */ + /** + * @var Account + */ private $accountBlock; /** - * @var Bootstrap + * @var ObjectManager */ private $objectManager; /** - * @magentoDataFixture Magento/Sales/_files/quote.php + * @var SessionQuote|MockObject + */ + private $session; + + /** + * @inheritdoc */ protected function setUp() { $this->objectManager = Bootstrap::getObjectManager(); - $quote = $this->objectManager->create(Quote::class)->load(1); - $sessionQuoteMock = $this->getMockBuilder( - SessionQuote::class - )->disableOriginalConstructor()->setMethods( - ['getCustomerId', 'getStore', 'getStoreId', 'getQuote'] - )->getMock(); - $sessionQuoteMock->expects($this->any())->method('getCustomerId')->will($this->returnValue(1)); - $sessionQuoteMock->expects($this->any())->method('getQuote')->will($this->returnValue($quote)); - /** @var LayoutInterface $layout */ - $layout = $this->objectManager->get(LayoutInterface::class); - $this->accountBlock = $layout->createBlock( - Account::class, - 'address_block' . rand(), - ['sessionQuote' => $sessionQuoteMock] - ); parent::setUp(); } /** + * Test for get form with existing customer + * * @magentoDataFixture Magento/Customer/_files/customer.php */ - public function testGetForm() + public function testGetFormWithCustomer() { + $customerGroup = 2; + $quote = $this->objectManager->create(Quote::class); + + $this->session = $this->getMockBuilder(SessionQuote::class) + ->disableOriginalConstructor() + ->setMethods(['getCustomerId','getQuote']) + ->getMock(); + $this->session->method('getQuote') + ->willReturn($quote); + $this->session->method('getCustomerId') + ->willReturn(1); + + /** @var LayoutInterface $layout */ + $layout = $this->objectManager->get(LayoutInterface::class); + $this->accountBlock = $layout->createBlock( + Account::class, + 'address_block' . rand(), + ['sessionQuote' => $this->session] + ); + + $fixtureCustomerId = 1; + /** @var \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository */ + $customerRepository = $this->objectManager->get(\Magento\Customer\Api\CustomerRepositoryInterface::class); + /** @var \Magento\Customer\Api\Data\CustomerInterface $customer */ + $customer = $customerRepository->getById($fixtureCustomerId); + $customer->setGroupId($customerGroup); + $customerRepository->save($customer); + $expectedFields = ['group_id', 'email']; $form = $this->accountBlock->getForm(); - $this->assertEquals(1, $form->getElements()->count(), "Form has invalid number of fieldsets"); + self::assertEquals(1, $form->getElements()->count(), "Form has invalid number of fieldsets"); $fieldset = $form->getElements()[0]; + $content = $form->toHtml(); - $this->assertEquals(count($expectedFields), $fieldset->getElements()->count()); + self::assertEquals(count($expectedFields), $fieldset->getElements()->count()); foreach ($fieldset->getElements() as $element) { - $this->assertTrue( + self::assertTrue( in_array($element->getId(), $expectedFields), sprintf('Unexpected field "%s" in form.', $element->getId()) ); } + + self::assertContains( + '', + $content, + 'The Customer Group specified for the chosen customer should be selected.' + ); + + self::assertContains( + 'value="'.$customer->getEmail().'"', + $content, + 'The Customer Email specified for the chosen customer should be input ' + ); } /** * Tests a case when user defined custom attribute has default value. * - * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Store/_files/core_second_third_fixturestore.php + * @magentoConfigFixture current_store customer/create_account/default_group 2 + * @magentoConfigFixture secondstore_store customer/create_account/default_group 3 */ public function testGetFormWithUserDefinedAttribute() { + /** @var \Magento\Store\Model\StoreManagerInterface $storeManager */ + $storeManager = Bootstrap::getObjectManager()->get(\Magento\Store\Model\StoreManagerInterface::class); + $secondStore = $storeManager->getStore('secondstore'); + + $quoteSession = $this->objectManager->get(SessionQuote::class); + $quoteSession->setStoreId($secondStore->getId()); + $formFactory = $this->getFormFactoryMock(); $this->objectManager->addSharedInstance($formFactory, FormFactory::class); /** @var LayoutInterface $layout */ $layout = $this->objectManager->get(LayoutInterface::class); - $accountBlock = $layout->createBlock(Account::class, 'address_block' . rand()); + $accountBlock = $layout->createBlock( + Account::class, + 'address_block' . rand() + ); $form = $accountBlock->getForm(); $form->setUseContainer(true); + $content = $form->toHtml(); - $this->assertContains( + self::assertContains( '', - $form->toHtml(), - 'Default value for user defined custom attribute should be selected' + $content, + 'Default value for user defined custom attribute should be selected.' + ); + + self::assertContains( + '', + $content, + 'The Customer Group specified for the chosen store should be selected.' ); } /** - * @return \PHPUnit_Framework_MockObject_MockObject + * Creates a mock for Form object. + * + * @return MockObject */ - private function getFormFactoryMock(): \PHPUnit_Framework_MockObject_MockObject + private function getFormFactoryMock(): MockObject { /** @var AttributeMetadataInterfaceFactory $attributeMetadataFactory */ $attributeMetadataFactory = $this->objectManager->create(AttributeMetadataInterfaceFactory::class); @@ -113,11 +175,12 @@ private function getFormFactoryMock(): \PHPUnit_Framework_MockObject_MockObject ->setDefaultValue('1') ->setFrontendLabel('Yes/No'); + /** @var Form|MockObject $form */ $form = $this->getMockBuilder(Form::class) ->disableOriginalConstructor() ->getMock(); $form->method('getUserAttributes')->willReturn([$booleanAttribute]); - $form->method('getSystemAttributes')->willReturn([]); + $form->method('getSystemAttributes')->willReturn([$this->createCustomerGroupAttribute()]); $formFactory = $this->getMockBuilder(FormFactory::class) ->disableOriginalConstructor() @@ -126,4 +189,33 @@ private function getFormFactoryMock(): \PHPUnit_Framework_MockObject_MockObject return $formFactory; } + + /** + * Creates a customer group attribute object. + * + * @return AttributeMetadataInterface + */ + private function createCustomerGroupAttribute(): AttributeMetadataInterface + { + /** @var Option $option1 */ + $option1 = $this->objectManager->create(Option::class); + $option1->setValue(2); + $option1->setLabel('Wholesale'); + + /** @var Option $option2 */ + $option2 = $this->objectManager->create(Option::class); + $option2->setValue(3); + $option2->setLabel('Retailer'); + + /** @var AttributeMetadataInterfaceFactory $attributeMetadataFactory */ + $attributeMetadataFactory = $this->objectManager->create(AttributeMetadataInterfaceFactory::class); + $attribute = $attributeMetadataFactory->create() + ->setAttributeCode('group_id') + ->setBackendType('static') + ->setFrontendInput('select') + ->setOptions([$option1, $option2]) + ->setIsRequired(true); + + return $attribute; + } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Form/AddressTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Form/AddressTest.php index 18b54e77124b..f5a22ec19ccf 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Form/AddressTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Form/AddressTest.php @@ -3,159 +3,167 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Block\Adminhtml\Order\Create\Form; +use Magento\Backend\Model\Session\Quote as QuoteSession; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\AddressInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Directory\Model\ResourceModel\Country\Collection; +use Magento\Framework\Data\Form\Element\AbstractElement; +use Magento\Framework\Data\Form\Element\Fieldset; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Api\Data\WebsiteInterface; +use Magento\Store\Api\StoreRepositoryInterface; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Xpath; +use Magento\TestFramework\ObjectManager; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + /** - * Test class for \Magento\Sales\Block\Adminhtml\Order\Create\Form\Address - * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @magentoAppArea adminhtml */ class AddressTest extends \PHPUnit\Framework\TestCase { - /** @var \Magento\Framework\ObjectManagerInterface */ - protected $_objectManager; - - /** @var \Magento\Sales\Block\Adminhtml\Order\Create\Form\Address */ - protected $_addressBlock; + /** + * @var ObjectManager + */ + private $objectManager; - /** @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Customer\Api\AddressRepositoryInterface */ - protected $addressRepository; + /** + * @var Address + */ + private $block; /** - * @return int + * @var QuoteSession|MockObject */ - private function getNumberOfCountryOptions() - { - /** @var \Magento\Directory\Model\ResourceModel\Country\Collection $countryCollection */ - $countryCollection = $this->_objectManager->create( - \Magento\Directory\Model\ResourceModel\Country\Collection::class - ); - return count($countryCollection->toOptionArray()); - } + private $quoteSession; + /** + * @inheritdoc + */ protected function setUp() { - $this->_objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $this->addressRepository = $this->getMockForAbstractClass( - \Magento\Customer\Api\AddressRepositoryInterface::class, - [], - '', - false, - true, - true, - ['getList'] - ); - /** @var \Magento\Framework\View\LayoutInterface $layout */ - $layout = $this->_objectManager->get(\Magento\Framework\View\LayoutInterface::class); - $sessionQuoteMock = $this->getMockBuilder(\Magento\Backend\Model\Session\Quote::class) - ->disableOriginalConstructor()->setMethods(['getCustomerId', 'getStore', 'getStoreId', 'getQuote']) + $this->objectManager = Bootstrap::getObjectManager(); + + $this->quoteSession = $this->getMockBuilder(QuoteSession::class) + ->disableOriginalConstructor() + ->setMethods(['getCustomerId', 'getStore', 'getStoreId', 'getQuote']) ->getMock(); - $sessionQuoteMock->expects($this->any())->method('getCustomerId')->will($this->returnValue(1)); - $this->_addressBlock = $layout->createBlock( - \Magento\Sales\Block\Adminhtml\Order\Create\Form\Address::class, - 'address_block' . rand(), - ['addressService' => $this->addressRepository, 'sessionQuote' => $sessionQuoteMock] + $this->block = $this->objectManager->create( + Address::class, + ['sessionQuote' => $this->quoteSession] ); - parent::setUp(); } + /** + * Checks address collection. + * + * @magentoDataFixture Magento/Customer/Fixtures/customer_2_addresses.php + */ public function testGetAddressCollection() { - $addressData = $this->_getAddresses(); - $searchResult = $this->getMockForAbstractClass( - \Magento\Customer\Api\Data\AddressSearchResultsInterface::class, - [], - '', - false, - true, - true, - ['getItems'] - ); - $searchResult->expects($this->any()) - ->method('getItems') - ->will($this->returnValue($addressData)); - $this->addressRepository->expects($this->any()) - ->method('getList') - ->will($this->returnValue($searchResult)); - $this->assertEquals($addressData, $this->_addressBlock->getAddressCollection()); + $website = $this->getWebsite('base'); + $customer = $this->getCustomer('customer@example.com', (int)$website->getId()); + $addresses = $customer->getAddresses(); + $this->quoteSession->method('getCustomerId') + ->willReturn($customer->getId()); + + $actual = $this->block->getAddressCollection(); + self::assertNotEmpty($actual); + self::assertEquals($addresses, $actual); } + /** + * Checks address collection output encoded to json. + * + * @magentoDataFixture Magento/Customer/Fixtures/customer_sec_website_2_addresses.php + */ public function testGetAddressCollectionJson() { - $addressData = $this->_getAddresses(); - $searchResult = $this->getMockForAbstractClass( - \Magento\Customer\Api\Data\AddressSearchResultsInterface::class, - [], - '', - false, - true, - true, - ['getItems'] - ); - $searchResult->expects($this->any()) - ->method('getItems') - ->will($this->returnValue($addressData)); - $this->addressRepository->expects($this->any()) - ->method('getList') - ->will($this->returnValue($searchResult)); - $expectedOutput = '[ - { - "firstname": false, - "lastname": false, - "company": false, - "street": "", - "city": false, - "country_id": "US", - "region": false, - "region_id": false, - "postcode": false, - "telephone": false, - "vat_id": false - }, - { - "firstname": "FirstName1", - "lastname": "LastName1", - "company": false, - "street": "Street1", - "city": false, - "country_id": false, - "region": false, - "region_id": false, - "postcode": false, - "telephone": false, - "vat_id": false - }, - { - "firstname": "FirstName2", - "lastname": "LastName2", - "company": false, - "street": "Street2", - "city": false, - "country_id": false, - "region": false, - "region_id": false, - "postcode": false, - "telephone": false, - "vat_id": false - } - ]'; - $expectedOutput = str_replace([' ', "\n", "\r"], '', $expectedOutput); - $expectedOutput = str_replace(': ', ':', $expectedOutput); - - $this->assertEquals($expectedOutput, $this->_addressBlock->getAddressCollectionJson()); + $website = $this->getWebsite('test'); + $customer = $this->getCustomer('customer.web@example.com', (int)$website->getId()); + + $store = $this->getStore('fixture_second_store'); + $this->quoteSession->method('getStore') + ->willReturn($store); + $this->quoteSession->method('getCustomerId') + ->willReturn($customer->getId()); + $addresses = $customer->getAddresses(); + $expected = [ + 0 => [ + 'firstname' => false, + 'lastname' => false, + 'company' => false, + 'street' => '', + 'city' => false, + 'country_id' => 'US', + 'region' => false, + 'region_id' => false, + 'postcode' => false, + 'telephone' => false, + 'vat_id' => false, + ], + $addresses[0]->getId() => [ + 'firstname' => 'John', + 'lastname' => 'Smith', + 'company' => false, + 'street' => 'Green str, 67', + 'city' => 'Culver City', + 'country_id' => 'US', + 'region' => 'California', + 'region_id' => 12, + 'postcode' => '90230', + 'telephone' => '3468676', + 'vat_id' => false, + ], + $addresses[1]->getId() => [ + 'telephone' => '845454465', + 'postcode' => '10178', + 'country_id' => 'DE', + 'city' => 'Berlin', + 'street' => 'Tunnel Alexanderpl', + 'firstname' => 'John', + 'lastname' => 'Smith', + 'company' => false, + 'region' => false, + 'region_id' => 0, + 'vat_id' => false, + ] + ]; + + $actual = json_decode($this->block->getAddressCollectionJson(), true); + self::assertEquals($expected, $actual); } + /** + * Checks one line address formatting + */ public function testGetAddressAsString() { - $address = $this->_getAddresses()[0]; - $expectedResult = "FirstName1 LastName1, Street1, , , "; - $this->assertEquals($expectedResult, $this->_addressBlock->getAddressAsString($address)); + $data = [ + 'firstname' => 'John', + 'lastname' => 'Smith', + 'company' => 'Test Company', + 'street' => 'Green str, 67', + 'city' => 'Culver City', + 'country_id' => 'US', + 'region' => 'California', + 'region_id' => 12, + 'postcode' => '90230', + 'telephone' => '3468676', + ]; + $address = $this->objectManager->create(AddressInterface::class, ['data' => $data]); + $expected = 'John Smith, Green str, 67, Culver City, California 90230, United States'; + self::assertEquals($expected, $this->block->getAddressAsString($address)); } - /** - * Test \Magento\Sales\Block\Adminhtml\Order\Create\Form\Address::_prepareForm() indirectly. - */ public function testGetForm() { $expectedFields = [ @@ -175,18 +183,21 @@ public function testGetForm() 'fax', 'vat_id', ]; - $form = $this->_addressBlock->getForm(); - $this->assertEquals(1, $form->getElements()->count(), "Form has invalid number of fieldsets"); - /** @var \Magento\Framework\Data\Form\Element\Fieldset $fieldset */ + + $form = $this->block->getForm(); + self::assertEquals(1, $form->getElements()->count(), 'Form has invalid number of fieldsets'); + + /** @var Fieldset $fieldset */ $fieldset = $form->getElements()[0]; - $this->assertEquals( + self::assertEquals( count($expectedFields), $fieldset->getElements()->count(), - "Form has invalid number of fields" + 'Form has invalid number of fields' ); - /** @var \Magento\Framework\Data\Form\Element\AbstractElement $element */ + + /** @var AbstractElement $element */ foreach ($fieldset->getElements() as $element) { - $this->assertTrue( + self::assertTrue( in_array($element->getId(), $expectedFields), sprintf('Unexpected field "%s" in form.', $element->getId()) ); @@ -194,32 +205,61 @@ public function testGetForm() /** @var \Magento\Framework\Data\Form\Element\Select $countryIdField */ $countryIdField = $fieldset->getElements()->searchById('country_id'); - $this->assertEquals( - $this->getNumberOfCountryOptions(), - \Magento\TestFramework\Helper\Xpath::getElementsCountForXpath( - '//option', - $countryIdField->getElementHtml() - ) - ); + $actual = Xpath::getElementsCountForXpath('//option', $countryIdField->getElementHtml()); + self::assertEquals($this->getNumberOfCountryOptions(), $actual); + } + + /** + * Gets customer entity. + * + * @param string $email + * @param int $websiteId + * @return CustomerInterface + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function getCustomer(string $email, int $websiteId): CustomerInterface + { + /** @var CustomerRepositoryInterface $repository */ + $repository = $this->objectManager->get(CustomerRepositoryInterface::class); + return $repository->get($email, $websiteId); } /** - * @return \Magento\Customer\Api\Data\AddressInterface[] + * Gets website by code. + * + * @param string $code + * @return WebsiteInterface + * @throws \Magento\Framework\Exception\NoSuchEntityException */ - protected function _getAddresses() + private function getWebsite(string $code): WebsiteInterface { - /** @var \Magento\Customer\Api\Data\AddressInterfaceFactory $addressFactory */ - $addressFactory = $this->_objectManager->create(\Magento\Customer\Api\Data\AddressInterfaceFactory::class); - $addressData[] = $addressFactory->create() - ->setId(1) - ->setStreet(['Street1']) - ->setFirstname('FirstName1') - ->setLastname('LastName1'); - $addressData[] = $addressFactory->create() - ->setId(2) - ->setStreet(['Street2']) - ->setFirstname('FirstName2') - ->setLastname('LastName2'); - return $addressData; + /** @var WebsiteRepositoryInterface $repository */ + $repository = $this->objectManager->get(WebsiteRepositoryInterface::class); + return $repository->get($code); + } + + /** + * Gets store by code. + * + * @param string $code + * @return StoreInterface + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function getStore(string $code): StoreInterface + { + /** @var StoreRepositoryInterface $repository */ + $repository = $this->objectManager->get(StoreRepositoryInterface::class); + return $repository->get($code); + } + + /** + * @return int + */ + private function getNumberOfCountryOptions() + { + /** @var Collection $countryCollection */ + $countryCollection = $this->objectManager->create(Collection::class); + return count($countryCollection->toOptionArray()); } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/TotalsTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/TotalsTest.php new file mode 100644 index 000000000000..1125fc173071 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/TotalsTest.php @@ -0,0 +1,59 @@ +layout = $this->_objectManager->get(LayoutInterface::class); + $this->block = $this->layout->createBlock(Totals::class, 'totals_block'); + $this->orderFactory = $this->_objectManager->get(OrderFactory::class); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order_with_free_shipping_by_coupon.php + */ + public function testShowShippingCoupon() + { + /** @var Order $order */ + $order = $this->orderFactory->create(); + $order->loadByIncrementId('100000001'); + + $this->block->setOrder($order); + $this->block->toHtml(); + + $shippingTotal = $this->block->getTotal('shipping'); + $this->assertNotFalse($shippingTotal, 'Shipping method is absent on the total\'s block.'); + $this->assertContains( + '1234567890', + $shippingTotal->getLabel(), + 'Coupon code is absent in the shipping method label name.' + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/SaveTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/SaveTest.php index b9573c99b449..f863edd04925 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/SaveTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/SaveTest.php @@ -8,21 +8,62 @@ use Magento\Backend\Model\Session\Quote; use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\Request\Http; +use Magento\Framework\Data\Form\FormKey; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Message\MessageInterface; use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Model\OrderRepository; use Magento\Sales\Model\Service\OrderService; +use Magento\TestFramework\Mail\Template\TransportBuilderMock; use Magento\TestFramework\TestCase\AbstractBackendController; +use PHPUnit\Framework\Constraint\StringContains; use PHPUnit_Framework_MockObject_MockObject as MockObject; +/** + * Class test backend order save. + * + * @magentoAppArea adminhtml + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class SaveTest extends AbstractBackendController { + /** + * @var TransportBuilderMock + */ + private $transportBuilder; + + /** + * @var FormKey + */ + private $formKey; + + /** + * @var string + */ + protected $resource = 'Magento_Sales::create'; + + /** + * @var string + */ + protected $uri = 'backend/sales/order_create/save'; + + /** + * @inheritdoc + */ + protected function setUp() + { + parent::setUp(); + $this->transportBuilder = $this->_objectManager->get(TransportBuilderMock::class); + $this->formKey = $this->_objectManager->get(FormKey::class); + } + /** * Checks a case when order creation is failed on payment method processing but new customer already created * in the database and after new controller dispatching the customer should be already loaded in session * to prevent invalid validation. * - * @magentoAppArea adminhtml * @magentoDataFixture Magento/Sales/_files/quote_with_new_customer.php */ public function testExecuteWithPaymentOperation() @@ -35,9 +76,10 @@ public function testExecuteWithPaymentOperation() $email = 'john.doe001@test.com'; $data = [ 'account' => [ - 'email' => $email + 'email' => $email, ] ]; + $this->getRequest()->setMethod(Http::METHOD_POST); $this->getRequest()->setPostValue(['order' => $data]); /** @var OrderService|MockObject $orderService */ @@ -64,13 +106,52 @@ public function testExecuteWithPaymentOperation() $this->_objectManager->removeSharedInstance(OrderService::class); } + /** + * @magentoDbIsolation enabled + * @magentoDataFixture Magento/Sales/_files/guest_quote_with_addresses.php + * + * @return void + */ + public function testSendEmailOnOrderSave(): void + { + $this->prepareRequest(['send_confirmation' => true]); + $this->dispatch('backend/sales/order_create/save'); + $this->assertSessionMessages( + $this->equalTo([(string)__('You created the order.')]), + MessageInterface::TYPE_SUCCESS + ); + + $this->assertRedirect($this->stringContains('sales/order/view/')); + + $orderId = $this->getOrderId(); + if ($orderId === false) { + $this->fail('Order is not created.'); + } + $order = $this->getOrder($orderId); + + $message = $this->transportBuilder->getSentMessage(); + $subject = __('Your %1 order confirmation', $order->getStore()->getFrontendName())->render(); + $assert = $this->logicalAnd( + new StringContains($order->getBillingAddress()->getName()), + new StringContains( + 'Thank you for your order from ' . $order->getStore()->getFrontendName() + ), + new StringContains( + "Your Order #{$order->getIncrementId()}" + ) + ); + + $this->assertEquals($message->getSubject(), $subject); + $this->assertThat($message->getRawMessage(), $assert); + } + /** * Gets quote by reserved order id. * * @param string $reservedOrderId * @return \Magento\Quote\Api\Data\CartInterface */ - private function getQuote($reservedOrderId) + private function getQuote(string $reservedOrderId): \Magento\Quote\Api\Data\CartInterface { /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ $searchCriteriaBuilder = $this->_objectManager->get(SearchCriteriaBuilder::class); @@ -80,6 +161,81 @@ private function getQuote($reservedOrderId) /** @var CartRepositoryInterface $quoteRepository */ $quoteRepository = $this->_objectManager->get(CartRepositoryInterface::class); $items = $quoteRepository->getList($searchCriteria)->getItems(); + return array_pop($items); } + + /** + * @inheritdoc + * @magentoDbIsolation enabled + * @magentoDataFixture Magento/Sales/_files/guest_quote_with_addresses.php + */ + public function testAclHasAccess() + { + $this->prepareRequest(); + + parent::testAclHasAccess(); + } + + /** + * @inheritdoc + * @magentoDbIsolation enabled + * @magentoDataFixture Magento/Sales/_files/guest_quote_with_addresses.php + */ + public function testAclNoAccess() + { + $this->prepareRequest(); + + parent::testAclNoAccess(); + } + + /** + * @param int $orderId + * @return OrderInterface + */ + private function getOrder(int $orderId): OrderInterface + { + return $this->_objectManager->get(OrderRepository::class)->get($orderId); + } + + /** + * @param array $params + * @return void + */ + private function prepareRequest(array $params = []): void + { + $quote = $this->getQuote('guest_quote'); + $session = $this->_objectManager->get(Quote::class); + $session->setQuoteId($quote->getId()); + $session->setCustomerId(0); + + $email = 'john.doe001@test.com'; + $data = [ + 'account' => [ + 'email' => $email, + ], + ]; + + $data = array_replace_recursive($data, $params); + + $this->getRequest() + ->setMethod('POST') + ->setParams(['form_key' => $this->formKey->getFormKey()]) + ->setPostValue(['order' => $data]); + } + + /** + * @return string|bool + */ + protected function getOrderId() + { + $currentUrl = $this->getResponse()->getHeader('Location'); + $orderId = false; + + if (preg_match('/order_id\/(?\d+)/', $currentUrl, $matches)) { + $orderId = $matches['order_id'] ?? ''; + } + + return $orderId; + } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/CreateTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/CreateTest.php index e071dde26a26..a07616474a41 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/CreateTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/CreateTest.php @@ -6,12 +6,26 @@ namespace Magento\Sales\Controller\Adminhtml\Order; use Magento\Customer\Api\CustomerRepositoryInterface; -use Magento\Backend\Model\Session\Quote; +use Magento\Backend\Model\Session\Quote as SessionQuote; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\Config\MutableScopeConfigInterface; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Quote\Model\Quote; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Api\Data\WebsiteInterface; +use Magento\Store\Api\StoreRepositoryInterface; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\Store\Model\ScopeInterface; /** * @magentoAppArea adminhtml * @magentoDbIsolation enabled + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyPublicMethods) */ class CreateTest extends \Magento\TestFramework\TestCase\AbstractBackendController { @@ -20,6 +34,11 @@ class CreateTest extends \Magento\TestFramework\TestCase\AbstractBackendControll */ protected $productRepository; + /** + * @inheritDoc + * + * @throws \Magento\Framework\Exception\AuthenticationException + */ protected function setUp() { parent::setUp(); @@ -27,8 +46,12 @@ protected function setUp() ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); } + /** + * Test LoadBlock being dispatched. + */ public function testLoadBlockAction() { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setParam('block', ','); $this->getRequest()->setParam('json', 1); $this->dispatch('backend/sales/order_create/loadBlock'); @@ -46,6 +69,7 @@ public function testLoadBlockActionData() )->addProducts( [$product->getId() => ['qty' => 1]] ); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setParam('block', 'data'); $this->getRequest()->setParam('json', 1); $this->dispatch('backend/sales/order_create/loadBlock'); @@ -57,10 +81,65 @@ public function testLoadBlockActionData() } /** + * Tests that shipping method 'Table rates' shows rates according to selected website. + * + * @magentoAppArea adminhtml + * @magentoDataFixture Magento/Quote/Fixtures/quote_sec_website.php + * @magentoDataFixture Magento/OfflineShipping/_files/tablerates_second_website.php + * @magentoDbIsolation disabled + */ + public function testLoadBlockShippingMethod() + { + $store = $this->getStore('fixture_second_store'); + + /** @var MutableScopeConfigInterface $mutableScopeConfig */ + $mutableScopeConfig = $this->_objectManager->get(MutableScopeConfigInterface::class); + $mutableScopeConfig->setValue( + 'carriers/tablerate/active', + 1, + ScopeInterface::SCOPE_STORE, + $store->getCode() + ); + $mutableScopeConfig->setValue( + 'carriers/tablerate/condition_name', + 'package_qty', + ScopeInterface::SCOPE_STORE, + $store->getCode() + ); + + $website = $this->getWebsite('test'); + $customer = $this->getCustomer('customer.web@example.com', (int)$website->getId()); + $quote = $this->getQuoteById('0000032134'); + $session = $this->_objectManager->get(SessionQuote::class); + $session->setQuoteId($quote->getId()); + + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue( + [ + 'customer_id' => $customer->getId(), + 'collect_shipping_rates' => 1, + 'store_id' => $store->getId(), + 'json' => true + ] + ); + $this->dispatch('backend/sales/order_create/loadBlock/block/shipping_method'); + $body = $this->getResponse()->getBody(); + $expectedTableRatePrice = '$20.00<\/span>'; + + $this->assertContains($expectedTableRatePrice, $body, ''); + } + + /** + * Tests LoadBlock actions. + * + * @param string $block Block name. + * @param string $expected Contains HTML. + * * @dataProvider loadBlockActionsDataProvider */ public function testLoadBlockActions($block, $expected) { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setParam('block', $block); $this->getRequest()->setParam('json', 1); $this->dispatch('backend/sales/order_create/loadBlock'); @@ -68,6 +147,9 @@ public function testLoadBlockActions($block, $expected) $this->assertContains($expected, $html); } + /** + * @return array + */ public function loadBlockActionsDataProvider() { return [ @@ -80,6 +162,8 @@ public function loadBlockActionsDataProvider() } /** + * Tests action items. + * * @magentoDataFixture Magento/Catalog/_files/product_simple.php */ public function testLoadBlockActionItems() @@ -90,6 +174,7 @@ public function testLoadBlockActionItems() )->addProducts( [$product->getId() => ['qty' => 1]] ); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setParam('block', 'items'); $this->getRequest()->setParam('json', 1); $this->dispatch('backend/sales/order_create/loadBlock'); @@ -153,6 +238,8 @@ public function testIndexAction() } /** + * Tests ACL. + * * @param string $actionName * @param boolean $reordered * @param string $expectedResult @@ -162,7 +249,7 @@ public function testIndexAction() */ public function testGetAclResource($actionName, $reordered, $expectedResult) { - $this->_objectManager->get(Quote::class)->setReordered($reordered); + $this->_objectManager->get(SessionQuote::class)->setReordered($reordered); $orderController = $this->_objectManager->get( \Magento\Sales\Controller\Adminhtml\Order\Stub\OrderCreateStub::class ); @@ -215,6 +302,11 @@ public function testConfigureProductToAddAction() $this->assertContains(sprintf('"productId":"%s"', $product->getEntityId()), $body); } + /** + * Test not allowing to save. + * + * @throws \Magento\Framework\Exception\LocalizedException + */ public function testDeniedSaveAction() { $this->_objectManager->configure( @@ -230,6 +322,7 @@ public function testDeniedSaveAction() \Magento\TestFramework\Helper\Bootstrap::getInstance() ->loadArea('adminhtml'); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/sales/order_create/save'); $this->assertEquals('403', $this->getResponse()->getHttpResponseCode()); } @@ -251,7 +344,7 @@ public function testSyncBetweenQuoteAddresses() $quoteRepository = $this->_objectManager->get(CartRepositoryInterface::class); $quote = $quoteRepository->getActiveForCustomer($customer->getId()); - $session = $this->_objectManager->get(Quote::class); + $session = $this->_objectManager->get(SessionQuote::class); $session->setQuoteId($quote->getId()); $data = [ @@ -263,6 +356,7 @@ public function testSyncBetweenQuoteAddresses() 'region' => 'Kyivska', 'region_id' => 1 ]; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue( [ 'order' => ['billing_address' => $data], @@ -286,4 +380,69 @@ public function testSyncBetweenQuoteAddresses() self::assertEquals($data['city'], $shippingAddress->getCity()); self::assertEquals($data['street'], $shippingAddress->getStreet()); } + + /** + * Gets quote entity by reserved order id. + * + * @param string $reservedOrderId + * @return Quote + */ + private function getQuoteById(string $reservedOrderId): Quote + { + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $this->_objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter('reserved_order_id', $reservedOrderId) + ->create(); + + /** @var CartRepositoryInterface $repository */ + $repository = $this->_objectManager->get(CartRepositoryInterface::class); + $items = $repository->getList($searchCriteria) + ->getItems(); + + return array_pop($items); + } + + /** + * Gets website entity. + * + * @param string $code + * @return WebsiteInterface + * @throws NoSuchEntityException + */ + private function getWebsite(string $code): WebsiteInterface + { + /** @var WebsiteRepositoryInterface $repository */ + $repository = $this->_objectManager->get(WebsiteRepositoryInterface::class); + return $repository->get($code); + } + + /** + * Gets customer entity. + * + * @param string $email + * @param int $websiteId + * @return CustomerInterface + * @throws NoSuchEntityException + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function getCustomer(string $email, int $websiteId): CustomerInterface + { + /** @var CustomerRepositoryInterface $repository */ + $repository = $this->_objectManager->get(CustomerRepositoryInterface::class); + return $repository->get($email, $websiteId); + } + + /** + * Gets store by code. + * + * @param string $code + * @return StoreInterface + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function getStore(string $code): StoreInterface + { + /** @var StoreRepositoryInterface $repository */ + $repository = $this->_objectManager->get(StoreRepositoryInterface::class); + return $repository->get($code); + } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/AbstractCreditmemoControllerTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/AbstractCreditmemoControllerTest.php new file mode 100644 index 000000000000..2a7731715021 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/AbstractCreditmemoControllerTest.php @@ -0,0 +1,92 @@ +transportBuilder = $this->_objectManager->get(TransportBuilderMock::class); + $this->orderRepository = $this->_objectManager->get(OrderRepository::class); + $this->formKey = $this->_objectManager->get(FormKey::class); + } + + /** + * @param string $incrementalId + * @return OrderInterface|null + */ + protected function getOrder(string $incrementalId) + { + /** @var SearchCriteria $searchCriteria */ + $searchCriteria = $this->_objectManager->create(SearchCriteriaBuilder::class) + ->addFilter(OrderInterface::INCREMENT_ID, $incrementalId) + ->create(); + + $orders = $this->orderRepository->getList($searchCriteria)->getItems(); + /** @var OrderInterface|null $order */ + $order = reset($orders); + + return $order; + } + + /** + * @param OrderInterface $order + * @return CreditmemoInterface + */ + protected function getCreditMemo(OrderInterface $order): CreditmemoInterface + { + /** @var \Magento\Sales\Model\ResourceModel\Order\Creditmemo\Collection $creditMemoCollection */ + $creditMemoCollection = $this->_objectManager->create( + \Magento\Sales\Model\ResourceModel\Order\Creditmemo\CollectionFactory::class + )->create(); + + /** @var CreditmemoInterface $creditMemo */ + $creditMemo = $creditMemoCollection + ->setOrderFilter($order) + ->setPageSize(1) + ->getFirstItem(); + + return $creditMemo; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/AddCommentTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/AddCommentTest.php new file mode 100644 index 000000000000..2f23da8b3db8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/AddCommentTest.php @@ -0,0 +1,102 @@ +prepareRequest( + [ + 'comment' => ['comment' => $comment, 'is_customer_notified' => true], + ] + ); + $this->dispatch('backend/sales/order_creditmemo/addComment'); + $html = $this->getResponse()->getBody(); + $this->assertContains($comment, $html); + + $message = $this->transportBuilder->getSentMessage(); + $subject =__('Update to your %1 credit memo', $order->getStore()->getFrontendName())->render(); + $messageConstraint = $this->logicalAnd( + new StringContains($order->getBillingAddress()->getName()), + new RegularExpression( + sprintf( + "/Your order #%s has been updated with a status of.*%s/", + $order->getIncrementId(), + $order->getFrontendStatusLabel() + ) + ), + new StringContains($comment) + ); + + $this->assertEquals($message->getSubject(), $subject); + $this->assertThat($message->getRawMessage(), $messageConstraint); + } + + /** + * @inheritdoc + */ + public function testAclHasAccess() + { + $this->prepareRequest(['comment' => ['comment' => 'Comment']]); + + parent::testAclHasAccess(); + } + + /** + * @inheritdoc + */ + public function testAclNoAccess() + { + $this->prepareRequest(['comment' => ['comment' => 'Comment']]); + + parent::testAclNoAccess(); + } + + /** + * @param array $params + * @return \Magento\Sales\Api\Data\OrderInterface|null + */ + private function prepareRequest(array $params = []) + { + $order = $this->getOrder('100000001'); + $creditmemo = $this->getCreditMemo($order); + + $this->getRequest()->setMethod('POST'); + $this->getRequest()->setParams( + [ + 'id' => $creditmemo->getEntityId(), + 'form_key' => $this->formKey->getFormKey(), + ] + ); + + $data = $params ?? []; + $this->getRequest()->setPostValue($data); + + return $order; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/SaveTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/SaveTest.php new file mode 100644 index 000000000000..f589a0f5a1c7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/SaveTest.php @@ -0,0 +1,189 @@ +prepareRequest(['creditmemo' => ['send_email' => true]]); + $this->dispatch('backend/sales/order_creditmemo/save'); + + $this->assertSessionMessages( + $this->equalTo([(string)__('You created the credit memo.')]), + \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS + ); + $this->assertRedirect($this->stringContains('sales/order/view/order_id/' . $order->getEntityId())); + + $creditMemo = $this->getCreditMemo($order); + $message = $this->transportBuilder->getSentMessage(); + $subject = __('Credit memo for your %1 order', $order->getStore()->getFrontendName())->render(); + $messageConstraint = $this->logicalAnd( + new StringContains($order->getBillingAddress()->getName()), + new StringContains( + 'Thank you for your order from ' . $creditMemo->getStore()->getFrontendName() + ), + new StringContains( + "Your Credit Memo #{$creditMemo->getIncrementId()} for Order #{$order->getIncrementId()}" + ) + ); + + $this->assertEquals($message->getSubject(), $subject); + $this->assertThat($message->getRawMessage(), $messageConstraint); + } + + /** + * Test order will keep same(custom) status after partial refund, if state has not been changed. + * + * @magentoDataFixture Magento/Sales/_files/order_with_invoice_and_custom_status.php + */ + public function testOrderStatusPartialRefund() + { + /** @var Order $existingOrder */ + $existingOrder = $this->_objectManager->create(Order::class)->loadByIncrementId('100000001'); + $items = $this->getOrderItems($existingOrder, 1); + $requestParams = [ + 'creditmemo' => [ + 'items' => $items, + 'do_offline' => '1', + 'comment_text' => '', + 'shipping_amount' => '0', + 'adjustment_positive' => '0', + 'adjustment_negative' => '0', + ], + 'order_id' => $existingOrder->getId(), + ]; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setParams($requestParams); + $this->dispatch('backend/sales/order_creditmemo/save'); + + /** @var Order $updatedOrder */ + $updatedOrder = $this->_objectManager->create(Order::class) + ->loadByIncrementId($existingOrder->getIncrementId()); + + $this->assertSame('custom_processing', $updatedOrder->getStatus()); + $this->assertSame('processing', $updatedOrder->getState()); + } + + /** + * Test order will change custom status after total refund, when state has been changed. + * + * @magentoDataFixture Magento/Sales/_files/order_with_invoice_and_custom_status.php + */ + public function testOrderStatusTotalRefund() + { + /** @var Order $existingOrder */ + $existingOrder = $this->_objectManager->create(Order::class)->loadByIncrementId('100000001'); + $requestParams = [ + 'creditmemo' => [ + 'items' => $this->getOrderItems($existingOrder), + 'do_offline' => '1', + 'comment_text' => '', + 'shipping_amount' => '0', + 'adjustment_positive' => '0', + 'adjustment_negative' => '0', + ], + 'order_id' => $existingOrder->getId(), + ]; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setParams($requestParams); + $this->dispatch('backend/sales/order_creditmemo/save'); + + /** @var Order $updatedOrder */ + $updatedOrder = $this->_objectManager->create(Order::class) + ->loadByIncrementId($existingOrder->getIncrementId()); + + $this->assertSame('complete', $updatedOrder->getStatus()); + $this->assertSame('complete', $updatedOrder->getState()); + } + + /** + * Gets all items of given Order in proper format. + * + * @param Order $order + * @param int $subQty + * @return array + */ + private function getOrderItems(Order $order, int $subQty = 0) + { + $items = []; + /** @var OrderItemInterface $item */ + foreach ($order->getAllItems() as $item) { + $items[$item->getItemId()] = [ + 'qty' => $item->getQtyOrdered() - $subQty, + ]; + } + + return $items; + } + + /** + * @inheritdoc + */ + public function testAclHasAccess() + { + $this->prepareRequest(); + + parent::testAclHasAccess(); + } + + /** + * @inheritdoc + */ + public function testAclNoAccess() + { + $this->prepareRequest(); + + parent::testAclNoAccess(); + } + + /** + * @param array $params + * @return \Magento\Sales\Api\Data\OrderInterface|null + */ + private function prepareRequest(array $params = []) + { + $order = $this->getOrder('100000001'); + $this->getRequest()->setMethod('POST'); + $this->getRequest()->setParams( + [ + 'order_id' => $order->getEntityId(), + 'form_key' => $this->formKey->getFormKey(), + ] + ); + + $data = ['creditmemo' => ['do_offline' => true]]; + $data = array_replace_recursive($data, $params); + + $this->getRequest()->setPostValue($data); + + return $order; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/CreditmemoTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/CreditmemoTest.php index 2cbd9b863ce0..2de06558ab66 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/CreditmemoTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/CreditmemoTest.php @@ -16,6 +16,7 @@ class CreditmemoTest extends \Magento\TestFramework\TestCase\AbstractBackendCont */ public function testAddCommentAction() { + $this->markTestIncomplete('https://github.com/magento-engcom/msi/issues/393'); $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); /** @var \Magento\CatalogInventory\Api\StockIndexInterface $stockIndex */ $stockIndex = $objectManager->get(\Magento\CatalogInventory\Api\StockIndexInterface::class); diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/EmailTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/EmailTest.php new file mode 100644 index 000000000000..4d19106ad8e5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/EmailTest.php @@ -0,0 +1,136 @@ +orderRepository = $this->_objectManager->get(OrderRepository::class); + $this->transportBuilder = $this->_objectManager->get(TransportBuilderMock::class); + } + + /** + * @return void + */ + public function testSendOrderEmail(): void + { + $order = $this->prepareRequest(); + $this->dispatch('backend/sales/order/email'); + + $this->assertSessionMessages( + $this->equalTo([(string)__('You sent the order email.')]), + \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS + ); + + $redirectUrl = 'sales/order/view/order_id/' . $order->getEntityId(); + $this->assertRedirect($this->stringContains($redirectUrl)); + + $message = $this->transportBuilder->getSentMessage(); + $subject = __('Your %1 order confirmation', $order->getStore()->getFrontendName())->render(); + $assert = $this->logicalAnd( + new StringContains($order->getBillingAddress()->getName()), + new StringContains( + 'Thank you for your order from ' . $order->getStore()->getFrontendName() + ), + new StringContains( + "Your Order #{$order->getIncrementId()}" + ) + ); + + $this->assertEquals($message->getSubject(), $subject); + $this->assertThat($message->getRawMessage(), $assert); + } + + /** + * @inheritdoc + */ + public function testAclHasAccess() + { + $this->prepareRequest(); + + parent::testAclHasAccess(); + } + + /** + * @inheritdoc + */ + public function testAclNoAccess() + { + $this->prepareRequest(); + + parent::testAclNoAccess(); + } + + /** + * @param string $incrementalId + * @return OrderInterface|null + */ + private function getOrder(string $incrementalId) + { + /** @var SearchCriteria $searchCriteria */ + $searchCriteria = $this->_objectManager->create(SearchCriteriaBuilder::class) + ->addFilter(OrderInterface::INCREMENT_ID, $incrementalId) + ->create(); + + $orders = $this->orderRepository->getList($searchCriteria)->getItems(); + /** @var OrderInterface|null $order */ + $order = reset($orders); + + return $order; + } + + /** + * @return OrderInterface|null + */ + private function prepareRequest() + { + $order = $this->getOrder('100000001'); + $this->getRequest()->setParams(['order_id' => $order->getEntityId()]); + + return $order; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AbstractInvoiceControllerTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AbstractInvoiceControllerTest.php new file mode 100644 index 000000000000..3ba54418b6c2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AbstractInvoiceControllerTest.php @@ -0,0 +1,92 @@ +transportBuilder = $this->_objectManager->get(TransportBuilderMock::class); + $this->orderRepository = $this->_objectManager->get(OrderRepository::class); + $this->formKey = $this->_objectManager->get(FormKey::class); + } + + /** + * @param string $incrementalId + * @return OrderInterface|null + */ + protected function getOrder(string $incrementalId) + { + /** @var SearchCriteria $searchCriteria */ + $searchCriteria = $this->_objectManager->create(SearchCriteriaBuilder::class) + ->addFilter(OrderInterface::INCREMENT_ID, $incrementalId) + ->create(); + + $orders = $this->orderRepository->getList($searchCriteria)->getItems(); + /** @var OrderInterface $order */ + $order = reset($orders); + + return $order; + } + + /** + * @param OrderInterface $order + * @return InvoiceInterface + */ + protected function getInvoiceByOrder(OrderInterface $order): InvoiceInterface + { + /** @var \Magento\Sales\Model\ResourceModel\Order\Invoice\Collection $invoiceCollection */ + $invoiceCollection = $this->_objectManager->create( + \Magento\Sales\Model\ResourceModel\Order\Invoice\CollectionFactory::class + )->create(); + + /** @var InvoiceInterface $invoice */ + $invoice = $invoiceCollection + ->setOrderFilter($order) + ->setPageSize(1) + ->getFirstItem(); + + return $invoice; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AddCommentTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AddCommentTest.php new file mode 100644 index 000000000000..81e1dd7afc49 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AddCommentTest.php @@ -0,0 +1,103 @@ +prepareRequest( + [ + 'comment' => ['comment' => $comment, 'is_customer_notified' => true], + ] + ); + $this->dispatch('backend/sales/order_invoice/addComment'); + + $html = $this->getResponse()->getBody(); + $this->assertContains($comment, $html); + + $message = $this->transportBuilder->getSentMessage(); + $subject = __('Update to your %1 invoice', $order->getStore()->getFrontendName())->render(); + $messageConstraint = $this->logicalAnd( + new StringContains($order->getBillingAddress()->getName()), + new RegularExpression( + sprintf( + "/Your order #%s has been updated with a status of.*%s/", + $order->getIncrementId(), + $order->getFrontendStatusLabel() + ) + ), + new StringContains($comment) + ); + + $this->assertEquals($message->getSubject(), $subject); + $this->assertThat($message->getRawMessage(), $messageConstraint); + } + + /** + * @inheritdoc + */ + public function testAclHasAccess() + { + $this->prepareRequest(['comment' => ['comment' => 'Comment']]); + + parent::testAclHasAccess(); + } + + /** + * @inheritdoc + */ + public function testAclNoAccess() + { + $this->prepareRequest(['comment' => ['comment' => 'Comment']]); + + parent::testAclNoAccess(); + } + + /** + * @param array $params + * @return \Magento\Sales\Api\Data\OrderInterface|null + */ + private function prepareRequest(array $params = []) + { + $order = $this->getOrder('100000001'); + $invoice = $this->getInvoiceByOrder($order); + + $this->getRequest()->setMethod('POST'); + $this->getRequest()->setParams( + [ + 'id' => $invoice->getEntityId(), + 'form_key' => $this->formKey->getFormKey(), + ] + ); + + $data = $params ?? []; + $this->getRequest()->setPostValue($data); + + return $order; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/EmailTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/EmailTest.php new file mode 100644 index 000000000000..85223528ec82 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/EmailTest.php @@ -0,0 +1,88 @@ +getOrder('100000001'); + $invoice = $this->getInvoiceByOrder($order); + + $this->getRequest()->setParams(['invoice_id' => $invoice->getEntityId()]); + $this->dispatch('backend/sales/order_invoice/email'); + + $this->assertSessionMessages( + $this->equalTo([(string)__('You sent the message.')]), + \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS + ); + + $redirectUrl = sprintf( + 'sales/invoice/view/order_id/%s/invoice_id/%s', + $order->getEntityId(), + $invoice->getEntityId() + ); + $this->assertRedirect($this->stringContains($redirectUrl)); + + $message = $this->transportBuilder->getSentMessage(); + $subject = __('Invoice for your %1 order', $order->getStore()->getFrontendName())->render(); + $messageConstraint = $this->logicalAnd( + new StringContains($invoice->getBillingAddress()->getName()), + new StringContains( + 'Thank you for your order from ' . $invoice->getStore()->getFrontendName() + ), + new StringContains( + "Your Invoice #{$invoice->getIncrementId()} for Order #{$order->getIncrementId()}" + ) + ); + + $this->assertEquals($message->getSubject(), $subject); + $this->assertThat($message->getRawMessage(), $messageConstraint); + } + + /** + * @inheritdoc + */ + public function testAclHasAccess() + { + $order = $this->getOrder('100000001'); + $invoice = $this->getInvoiceByOrder($order); + $this->uri .= '/invoice_id/' . $invoice->getEntityId(); + + parent::testAclHasAccess(); + } + + /** + * @inheritdoc + */ + public function testAclNoAccess() + { + $order = $this->getOrder('100000001'); + $invoice = $this->getInvoiceByOrder($order); + $this->uri .= '/invoice_id/' . $invoice->getEntityId(); + + parent::testAclNoAccess(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/SaveTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/SaveTest.php new file mode 100644 index 000000000000..68074e38d9a3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/SaveTest.php @@ -0,0 +1,97 @@ +prepareRequest(['invoice' => ['send_email' => true]]); + $this->dispatch('backend/sales/order_invoice/save'); + + $this->assertSessionMessages( + $this->equalTo([(string)__('The invoice has been created.')]), + \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS + ); + $this->assertRedirect($this->stringContains('sales/order/view/order_id/' . $order->getEntityId())); + + $invoice = $this->getInvoiceByOrder($order); + $message = $this->transportBuilder->getSentMessage(); + $subject = __('Invoice for your %1 order', $order->getStore()->getFrontendName())->render(); + $messageConstraint = $this->logicalAnd( + new StringContains($invoice->getBillingAddress()->getName()), + new StringContains( + 'Thank you for your order from ' . $invoice->getStore()->getFrontendName() + ), + new StringContains( + "Your Invoice #{$invoice->getIncrementId()} for Order #{$order->getIncrementId()}" + ) + ); + + $this->assertEquals($message->getSubject(), $subject); + $this->assertThat($message->getRawMessage(), $messageConstraint); + } + + /** + * @inheritdoc + */ + public function testAclHasAccess() + { + $this->prepareRequest(); + + parent::testAclHasAccess(); + } + + /** + * @inheritdoc + */ + public function testAclNoAccess() + { + $this->prepareRequest(); + + parent::testAclNoAccess(); + } + + /** + * @param array $params + * @return \Magento\Sales\Api\Data\OrderInterface|null + */ + private function prepareRequest(array $params = []) + { + $order = $this->getOrder('100000001'); + $this->getRequest()->setMethod('POST'); + $this->getRequest()->setParams( + [ + 'order_id' => $order->getEntityId(), + 'form_key' => $this->formKey->getFormKey(), + ] + ); + + $data = $params ?? []; + $this->getRequest()->setPostValue($data); + + return $order; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Guest/FormTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Guest/FormTest.php new file mode 100644 index 000000000000..1067a474e19a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Guest/FormTest.php @@ -0,0 +1,109 @@ +prepareRequestData(); + $this->dispatch('sales/guest/view/'); + $content = $this->getResponse()->getBody(); + $this->assertContains('Order # 100000001', $content); + } + + /** + * View order as logged in customer + * + * @magentoDataFixture Magento/Sales/_files/order.php + * @magentoDataFixture Magento/Customer/_files/customer.php + */ + public function testViewOrderAsLoggedIn() + { + $this->login(1); + $this->dispatch('sales/guest/view/'); + $this->assertRedirect($this->stringContains('sales/order/history/')); + } + + /** + * Test attempting to open the Returns form as logged in customer + * + * @magentoDataFixture Magento/Sales/_files/order.php + * @magentoDataFixture Magento/Customer/_files/customer.php + */ + public function testAttemptToOpenTheFormAsLoggedIn() + { + $this->login(1); + $this->dispatch('sales/guest/form/'); + $this->assertRedirect($this->stringContains('customer/account')); + } + + /** + * Test Return Order for guest with incorrect data + */ + public function testViewOrderAsGuestWithIncorrectData() + { + $this->prepareRequestData(true); + $this->dispatch('sales/guest/view/'); + $this->assertSessionMessages( + $this->equalTo(['You entered incorrect data. Please try again.']), + MessageInterface::TYPE_ERROR + ); + } + + /** + * Login the user + * + * @param string $customerId Customer to mark as logged in for the session + * @return void + */ + protected function login($customerId) + { + /** @var Session $session */ + $session = $this->_objectManager->get(Session::class); + $session->loginById($customerId); + } + + /** + * @param bool $invalidData + * @return void + */ + private function prepareRequestData($invalidData = false) + { + $orderId = 100000001; + $email = $invalidData ? 'wrong@example.com' : 'customer@null.com'; + + /** @var FormKey $formKey */ + $formKey = $this->_objectManager->get(FormKey::class); + $post = [ + 'oar_order_id' => $orderId, + 'oar_billing_lastname' => 'lastname', + 'oar_type' => 'email', + 'oar_email' => $email, + 'oar_zip' => '', + 'form_key' => $formKey->getFormKey(), + ]; + + $this->getRequest()->setMethod(Request::METHOD_POST); + $this->getRequest()->setPostValue($post); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/CustomerData/LastOrderedItemsTest.php b/dev/tests/integration/testsuite/Magento/Sales/CustomerData/LastOrderedItemsTest.php new file mode 100644 index 000000000000..7800d2363b36 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/CustomerData/LastOrderedItemsTest.php @@ -0,0 +1,48 @@ +objectManager = Bootstrap::getObjectManager(); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order_with_customer_and_multiple_order_items.php + */ + public function testDefaultFormatterIsAppliedWhenBasicIntegration() + { + /** @var Session $customerSession */ + $customerSession = $this->objectManager->get(Session::class); + $customerSession->loginById(1); + + /** @var LastOrderedItems $customerDataSectionSource */ + $customerDataSectionSource = $this->objectManager->get(LastOrderedItems::class); + $data = $customerDataSectionSource->getSectionData(); + $this->assertEquals( + LastOrderedItems::SIDEBAR_ORDER_LIMIT, + count($data['items']), + 'Section items count should not be greater then ' . LastOrderedItems::SIDEBAR_ORDER_LIMIT + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/Order/CreateTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/CreateTest.php new file mode 100644 index 000000000000..1035ce159231 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/CreateTest.php @@ -0,0 +1,100 @@ +objectManager = Bootstrap::getObjectManager(); + $this->transportBuilder = $this->objectManager->get(TransportBuilderMock::class); + $this->quoteIdMaskFactory = $this->objectManager->get(QuoteIdMaskFactory::class); + $this->formKey = $this->objectManager->get(FormKey::class); + } + + /** + * @magentoDataFixture Magento/Sales/_files/guest_quote_with_addresses.php + * @return void + */ + public function testSendEmailOnOrderPlace(): void + { + /** @var Quote $quote */ + $quote = $this->objectManager->create(Quote::class); + $quote->load('guest_quote', 'reserved_order_id'); + + $checkoutSession = $this->objectManager->get(CheckoutSession::class); + $checkoutSession->setQuoteId($quote->getId()); + + /** @var QuoteIdMask $quoteIdMask */ + $quoteIdMask = $this->quoteIdMaskFactory->create(); + $quoteIdMask->load($quote->getId(), 'quote_id'); + $cartId = $quoteIdMask->getMaskedId(); + + /** @var GuestCartManagementInterface $cartManagement */ + $cartManagement = $this->objectManager->get(GuestCartManagementInterface::class); + $orderId = $cartManagement->placeOrder($cartId); + $order = $this->objectManager->get(OrderRepository::class)->get($orderId); + + $message = $this->transportBuilder->getSentMessage(); + $subject = __('Your %1 order confirmation', $order->getStore()->getFrontendName())->render(); + $assert = $this->logicalAnd( + new StringContains($order->getBillingAddress()->getName()), + new StringContains( + 'Thank you for your order from ' . $order->getStore()->getFrontendName() + ), + new StringContains( + "Your Order #{$order->getIncrementId()}" + ) + ); + + $this->assertEquals($message->getSubject(), $subject); + $this->assertThat($message->getRawMessage(), $assert); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/Order/ShipmentTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/ShipmentTest.php index 0679fc6ffe6c..1d04a79ae3f8 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Model/Order/ShipmentTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/ShipmentTest.php @@ -90,10 +90,11 @@ public function testAddTrack() $items[$item->getId()] = $item->getQtyOrdered(); } /** @var \Magento\Sales\Model\Order\Shipment $shipment */ - $shipment = $this->objectManager->get(ShipmentFactory::class)->create($order, $items); + $shipment = $this->objectManager->get(ShipmentFactory::class) + ->create($order, $items); $shipment->addTrack($track); - $shipment->save(); - $saved = $this->shipmentRepository->save($shipment); + $this->shipmentRepository->save($shipment); + $saved = $this->shipmentRepository->get((int)$shipment->getEntityId()); self::assertNotEmpty($saved->getTracks()); } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/ResourceModel/Order/Grid/CollectionTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/ResourceModel/Order/Grid/CollectionTest.php new file mode 100644 index 000000000000..1649706a51f6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/ResourceModel/Order/Grid/CollectionTest.php @@ -0,0 +1,45 @@ +get(Collection::class); + $tableDescription = $gridCollection->getConnection() + ->describeTable($gridCollection->getMainTable()); + + $mapper = new \ReflectionMethod( + Collection::class, + '_getMapper' + ); + $mapper->setAccessible(true); + $map = $mapper->invoke($gridCollection); + + self::assertInternalType('array', $map); + self::assertArrayHasKey('fields', $map); + self::assertInternalType('array', $map['fields']); + self::assertCount(count($tableDescription), $map['fields']); + + foreach ($map['fields'] as $mappedName) { + self::assertContains('main_table.', $mappedName); + } + } +} 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 000000000000..e35c480e44c6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/Service/InvoiceServiceTest.php @@ -0,0 +1,92 @@ +invoiceService = Bootstrap::getObjectManager()->create(InvoiceService::class); + } + + /** + * @param int $invoiceQty + * @magentoDataFixture Magento/Sales/_files/order_configurable_product.php + * @return void + * @dataProvider prepareInvoiceConfigurableProductDataProvider + */ + public function testPrepareInvoiceConfigurableProduct(int $invoiceQty): void + { + /** @var OrderInterface $order */ + $order = Bootstrap::getObjectManager()->create(Order::class)->load('100000001', 'increment_id'); + $orderItems = $order->getItems(); + foreach ($orderItems as $orderItem) { + if ($orderItem->getParentItemId()) { + $parentItemId = $orderItem->getParentItemId(); + } + } + $invoice = $this->invoiceService->prepareInvoice($order, [$parentItemId => $invoiceQty]); + $invoiceItems = $invoice->getItems(); + foreach ($invoiceItems as $invoiceItem) { + $this->assertEquals($invoiceQty, $invoiceItem->getQty()); + } + } + + public function prepareInvoiceConfigurableProductDataProvider() + { + return [ + 'full invoice' => [2], + 'partial invoice' => [1] + ]; + } + + /** + * @param int $invoiceQty + * @magentoDataFixture Magento/Sales/_files/order.php + * @return void + * @dataProvider prepareInvoiceSimpleProductDataProvider + */ + public function testPrepareInvoiceSimpleProduct(int $invoiceQty): void + { + /** @var OrderInterface $order */ + $order = Bootstrap::getObjectManager()->create(Order::class)->load('100000001', 'increment_id'); + $orderItems = $order->getItems(); + $invoiceQtys = []; + foreach ($orderItems as $orderItem) { + $invoiceQtys[$orderItem->getItemId()] = $invoiceQty; + } + $invoice = $this->invoiceService->prepareInvoice($order, $invoiceQtys); + $invoiceItems = $invoice->getItems(); + foreach ($invoiceItems as $invoiceItem) { + $this->assertEquals($invoiceQty, $invoiceItem->getQty()); + } + } + + public function prepareInvoiceSimpleProductDataProvider() + { + return [ + 'full invoice' => [2], + 'partial invoice' => [1] + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/guest_quote_with_addresses.php b/dev/tests/integration/testsuite/Magento/Sales/_files/guest_quote_with_addresses.php new file mode 100644 index 000000000000..1d03170d5b1e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/guest_quote_with_addresses.php @@ -0,0 +1,71 @@ +loadArea(\Magento\Framework\App\Area::AREA_FRONTEND); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); + +/** @var \Magento\Catalog\Model\Product $product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId('simple') + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setName('Simple Product') + ->setSku('simple-product-guest-quote') + ->setPrice(10) + ->setTaxClassId(0) + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData( + [ + 'qty' => 100, + 'is_in_stock' => 1, + ] + )->save(); +$product = $productRepository->get('simple-product-guest-quote'); + +$addressData = reset($addresses); + +$billingAddress = $objectManager->create( + \Magento\Quote\Model\Quote\Address::class, + ['data' => $addressData] +); +$billingAddress->setAddressType('billing'); + +$shippingAddress = clone $billingAddress; +$shippingAddress->setId(null)->setAddressType('shipping'); + +$store = $objectManager->get(\Magento\Store\Model\StoreManagerInterface::class)->getStore(); + +/** @var \Magento\Quote\Model\Quote $quote */ +$quote = $objectManager->create(\Magento\Quote\Model\Quote::class); +$quote->setCustomerIsGuest(true) + ->setStoreId($store->getId()) + ->setReservedOrderId('guest_quote') + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->addProduct($product); +$quote->getPayment()->setMethod('checkmo'); +$quote->getShippingAddress()->setShippingMethod('flatrate_flatrate')->setCollectShippingRates(true); +$quote->collectTotals(); + +$quoteRepository = $objectManager->create(\Magento\Quote\Api\CartRepositoryInterface::class); +$quoteRepository->save($quote); + +/** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ +$quoteIdMask = $objectManager->create(\Magento\Quote\Model\QuoteIdMaskFactory::class)->create(); +$quoteIdMask->setQuoteId($quote->getId()); +$quoteIdMask->setDataChanges(true); +$quoteIdMask->save(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/guest_quote_with_addresses_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/guest_quote_with_addresses_rollback.php new file mode 100644 index 000000000000..e8992aec3c92 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/guest_quote_with_addresses_rollback.php @@ -0,0 +1,43 @@ +get(\Magento\Framework\Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var $quote \Magento\Quote\Model\Quote */ +$quote = $objectManager->create(\Magento\Quote\Model\Quote::class); +/** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ +$quoteIdMask = $objectManager->create(\Magento\Quote\Model\QuoteIdMask::class); + +$quote->load('guest_quote', 'reserved_order_id'); + +$quoteId = $quote->getId(); +if (null !== $quoteId) { + $quote->delete(); + $quoteIdMask->delete($quoteId); +} + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +try { + $product = $productRepository->get('simple-product-guest-quote', false, null, true); + $productRepository->delete($product); +} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { + //Product already removed +} + +/** @var \Magento\CatalogInventory\Model\StockRegistryStorage $stockRegistryStorage */ +$stockRegistryStorage = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\CatalogInventory\Model\StockRegistryStorage::class); +$stockRegistryStorage->clean(); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order.php index a1c5f8277762..6b9cf3bc613c 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/_files/order.php +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order.php @@ -44,7 +44,8 @@ ->setBasePrice($product->getPrice()) ->setPrice($product->getPrice()) ->setRowTotal($product->getPrice()) - ->setProductType('simple'); + ->setProductType('simple') + ->setName($product->getName()); /** @var Order $order */ $order = $objectManager->create(Order::class); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_list.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_list.php index 43e98419798a..99122d72df4b 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/_files/order_list.php +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_list.php @@ -5,7 +5,11 @@ */ use Magento\Sales\Model\Order; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order\Address as OrderAddress; +use Magento\Sales\Model\Order\Payment; +// phpcs:ignore Magento2.Security.IncludeFile require 'order.php'; /** @var Order $order */ /** @var Order\Payment $payment */ @@ -21,8 +25,7 @@ 'subtotal' => 120.00, 'base_grand_total' => 120.00, 'store_id' => 1, - 'website_id' => 1, - 'payment' => $payment + 'website_id' => 1 ], [ 'increment_id' => '100000003', @@ -32,8 +35,7 @@ 'base_grand_total' => 140.00, 'subtotal' => 140.00, 'store_id' => 0, - 'website_id' => 0, - 'payment' => $payment + 'website_id' => 0 ], [ 'increment_id' => '100000004', @@ -43,21 +45,49 @@ 'base_grand_total' => 140.00, 'subtotal' => 140.00, 'store_id' => 1, - 'website_id' => 1, - 'payment' => $payment + 'website_id' => 1 ], ]; +$orderList = []; +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->create(OrderRepositoryInterface::class); /** @var array $orderData */ foreach ($orders as $orderData) { /** @var $order \Magento\Sales\Model\Order */ $order = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( \Magento\Sales\Model\Order::class ); + + // Reset addresses + /** @var Order\Address $billingAddress */ + $billingAddress = $objectManager->create(OrderAddress::class, ['data' => $addressData]); + $billingAddress->setAddressType('billing'); + + $shippingAddress = clone $billingAddress; + $shippingAddress->setId(null)->setAddressType('shipping'); + + /** @var Payment $payment */ + $payment = $objectManager->create(Payment::class); + $payment->setMethod('checkmo') + ->setAdditionalInformation('last_trans_id', '11122') + ->setAdditionalInformation( + 'metadata', + [ + 'type' => 'free', + 'fraudulent' => false, + ] + ); + $order ->setData($orderData) ->addItem($orderItem) + ->setCustomerIsGuest(true) + ->setCustomerEmail('customer@null.com') ->setBillingAddress($billingAddress) - ->setBillingAddress($shippingAddress) - ->save(); + ->setShippingAddress($shippingAddress) + ->setPayment($payment); + + $orderRepository->save($order); + $orderList[] = $order; } diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_list_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_list_rollback.php index aae4a557ba1e..6e24cee501f5 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/_files/order_list_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_list_rollback.php @@ -4,6 +4,4 @@ * See COPYING.txt for license details. */ -use Magento\Sales\Model\Order; - require 'default_rollback.php'; diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_list_with_tax.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_list_with_tax.php new file mode 100644 index 000000000000..48f6ccfead29 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_list_with_tax.php @@ -0,0 +1,55 @@ +create(TaxFactory::class); + + /** @var \Magento\Tax\Model\Sales\Order\Tax $tax */ + $tax = $taxFactory->create(); + $tax->setOrderId($order->getId()) + ->setCode('US-NY-*-Rate 1') + ->setTitle('US-NY-*-Rate 1') + ->setPercent(8.37) + ->setAmount($amount) + ->setBaseAmount($amount) + ->setBaseRealAmount($amount); + $tax->save(); + + $salesOrderFactory = $objectManager->create(ItemFactory::class); + + /** @var \Magento\Sales\Model\Order\Tax\Item $salesOrderItem */ + $salesOrderItem = $salesOrderFactory->create(); + $salesOrderItem->setOrderId($order->getId()) + ->setStoreId($objectManager->get(StoreManagerInterface::class)->getStore()->getId()) + ->setProductOptions([]); + $salesCollection = $objectManager->create(\Magento\Sales\Model\ResourceModel\Order\Item::class); + $salesCollection->save($salesOrderItem); + + /** @var \Magento\Sales\Model\Order\Tax\Item $salesOrderItem */ + $salesOrderTaxItem = $salesOrderFactory->create(); + $salesOrderTaxItem->setTaxId($tax->getId()) + ->setTaxPercent(8.37) + ->setTaxAmount($amount) + ->setBaseAmount($amount) + ->setRealAmount($amount) + ->setRealBaseAmount($amount) + ->setAppliedTaxes([$tax]) + ->setTaxableItemType('shipping') + ->setItemId($salesOrderItem->getId()); + + $taxItemCollection = $objectManager->create(\Magento\Sales\Model\ResourceModel\Order\Tax\Item::class); + $taxItemCollection->save($salesOrderTaxItem); +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_list_with_tax_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_list_with_tax_rollback.php new file mode 100644 index 000000000000..dd52deab825c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_list_with_tax_rollback.php @@ -0,0 +1,8 @@ + 2, + OrderItemInterface::BASE_PRICE => 100, + OrderItemInterface::ORDER_ID => $order->getId(), + OrderItemInterface::QTY_ORDERED => 2, + OrderItemInterface::QTY_INVOICED => 2, + OrderItemInterface::PRICE => 100, + OrderItemInterface::ROW_TOTAL => 102, + OrderItemInterface::PRODUCT_TYPE => 'bundle', + 'product_options' => [ + 'product_calculations' => 0, + 'info_buyRequest' => [ + 'bundle_option' => [1 => 1], + 'bundle_option_qty' => 1, + ] + ], + 'children' => [ + [ + OrderItemInterface::PRODUCT_ID => 13, + OrderItemInterface::ORDER_ID => $order->getId(), + OrderItemInterface::QTY_ORDERED => 10, + OrderItemInterface::QTY_INVOICED => 10, + OrderItemInterface::BASE_PRICE => 90, + OrderItemInterface::PRICE => 90, + OrderItemInterface::ROW_TOTAL => 92, + OrderItemInterface::PRODUCT_TYPE => 'simple', + 'product_options' => [ + 'bundle_selection_attributes' => [ + 'qty' => 2, + ], + ], + ], + ], + ], +]; + +if (!function_exists('saveOrderItems')) { + /** + * Save Order Items. + * + * @param array $orderItems + * @param Item|null $parentOrderItem [optional] + * @return void + */ + function saveOrderItems(array $orderItems, Order $order, $parentOrderItem = null) + { + $objectManager = ObjectManager::getInstance(); + + foreach ($orderItems as $orderItemData) { + /** @var Item $orderItem */ + $orderItem = $objectManager->create(Item::class); + if (null !== $parentOrderItem) { + $orderItemData['parent_item'] = $parentOrderItem; + } + $orderItem->setData($orderItemData); + $order->addItem($orderItem); + + if (isset($orderItemData['children'])) { + saveOrderItems($orderItemData['children'], $order, $orderItem); + } + } + } +} + +saveOrderItems($orderItems, $order); +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->get(OrderRepositoryInterface::class); +$order = $orderRepository->save($order); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_bundle_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_bundle_rollback.php new file mode 100644 index 000000000000..dd52deab825c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_bundle_rollback.php @@ -0,0 +1,8 @@ +setCustomerId($customerIdFromFixture)->setCustomerIsGuest(false)->save(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_discount.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_discount.php new file mode 100644 index 000000000000..29a7aa4d9033 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_discount.php @@ -0,0 +1,73 @@ +create(OrderAddress::class, ['data' => $addressData]); +$billingAddress->setAddressType('billing'); + +$shippingAddress = clone $billingAddress; +$shippingAddress->setId(null)->setAddressType('shipping'); + +/** @var Payment $payment */ +$payment = $objectManager->create(Payment::class); +$payment->setMethod('checkmo') + ->setAdditionalInformation('last_trans_id', '11122') + ->setAdditionalInformation( + 'metadata', + [ + 'type' => 'free', + 'fraudulent' => false, + ] + ); + +/** @var OrderItem $orderItem */ +$orderItem = $objectManager->create(OrderItem::class); +$orderItem->setProductId($product->getId()) + ->setQtyOrdered(2) + ->setBasePrice($product->getPrice()) + ->setPrice($product->getPrice()) + ->setRowTotal($product->getPrice()) + ->setProductType('simple') + ->setDiscountAmount(2) + ->setBaseRowTotal($product->getPrice()) + ->setBaseDiscountAmount(2) + ->setTaxAmount(1) + ->setBaseTaxAmount(1); + +/** @var Order $order */ +$order = $objectManager->create(Order::class); +$order->setIncrementId('100000001') + ->setState(Order::STATE_PROCESSING) + ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_PROCESSING)) + ->setSubtotal(100) + ->setGrandTotal(100) + ->setBaseSubtotal(100) + ->setBaseGrandTotal(100) + ->setCustomerIsGuest(true) + ->setCustomerEmail('customer@null.com') + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->setStoreId($objectManager->get(StoreManagerInterface::class)->getStore()->getId()) + ->addItem($orderItem) + ->setPayment($payment); + +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->create(OrderRepositoryInterface::class); +$orderRepository->save($order); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_discount_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_discount_rollback.php new file mode 100644 index 000000000000..1fb4b4636ab2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_discount_rollback.php @@ -0,0 +1,8 @@ +create(OrderItem::class); +$orderItem->setProductId($product->getId()) + ->setQtyOrdered(2) + ->setBasePrice($product->getPrice()) + ->setPrice($product->getPrice()) + ->setRowTotal($product->getPrice()) + ->setProductType('simple') + ->setName($product->getName()) + ->setFreeShipping('1'); + +/** @var Order $order */ +$order->setShippingDescription('Flat Rate - Fixed') + ->setShippingAmount(0) + ->setCouponCode('1234567890') + ->setDiscountDescription('1234567890') + ->addItem($orderItem); + +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->create(OrderRepositoryInterface::class); +$orderRepository->save($order); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_free_shipping_by_coupon_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_free_shipping_by_coupon_rollback.php new file mode 100644 index 000000000000..1fb4b4636ab2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_free_shipping_by_coupon_rollback.php @@ -0,0 +1,8 @@ +create(Status::class); +$data = [ + 'status' => 'custom_processing', + 'label' => 'Custom Processing Status', +]; +$orderStatus->setData($data)->setStatus('custom_processing'); +$orderStatus->save(); +$orderStatus->assignState('processing'); + +$order->setStatus('custom_processing'); +$order->save(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_invoice_and_custom_status_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_invoice_and_custom_status_rollback.php new file mode 100644 index 000000000000..274cb3c74395 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_invoice_and_custom_status_rollback.php @@ -0,0 +1,17 @@ +create(Status::class); +$orderStatus->load('custom_processing', 'status'); +$orderStatus->delete(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_multiple_items.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_multiple_items.php new file mode 100644 index 000000000000..ae0523d34fd2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_multiple_items.php @@ -0,0 +1,82 @@ + $product->getId(), + 'base_price' => 123, + 'order_id' => $order->getId(), + 'price' => 123, + 'row_total' => 126, + 'product_type' => 'simple' +]; + +require __DIR__ . '/../../../Magento/Catalog/_files/product_simple_duplicated.php'; +$orderItems[] = [ + 'product_id' => $product->getId(), + 'base_price' => 123, + 'order_id' => $order->getId(), + 'price' => 123, + 'row_total' => 126, + 'product_type' => 'simple' +]; + +require __DIR__ . '/../../../Magento/Catalog/_files/product_simple_with_full_option_set.php'; +$orderItems[] = [ + 'product_id' => $product->getId(), + 'base_price' => 123, + 'order_id' => $order->getId(), + 'price' => 123, + 'row_total' => 126, + 'product_type' => 'simple' +]; + +require __DIR__ . '/../../../Magento/Catalog/_files/product_simple_with_url_key.php'; +$orderItems[] = [ + 'product_id' => $product->getId(), + 'base_price' => 123, + 'order_id' => $order->getId(), + 'price' => 123, + 'row_total' => 126, + 'product_type' => 'simple' +]; + +require __DIR__ . '/../../../Magento/Catalog/_files/product_simple_with_all_fields.php'; +$orderItems[] = [ + 'product_id' => $product->getId(), + 'base_price' => 123, + 'order_id' => $order->getId(), + 'price' => 123, + 'row_total' => 126, + 'product_type' => 'simple' +]; + +require __DIR__ . '/../../../Magento/Catalog/_files/product_simple_with_custom_attribute.php'; +$orderItems[] = [ + 'product_id' => $product->getId(), + 'base_price' => 123, + 'order_id' => $order->getId(), + 'price' => 123, + 'row_total' => 126, + 'product_type' => 'simple' +]; + +/** @var array $orderItemData */ +foreach ($orderItems as $orderItemData) { + /** @var $orderItem \Magento\Sales\Model\Order\Item */ + $orderItem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Sales\Model\Order\Item::class + ); + $orderItem + ->setData($orderItemData) + ->save(); +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_shipping_and_invoice.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_shipping_and_invoice.php index 4c892904b3c3..61d8be98bdd2 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_shipping_and_invoice.php +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_shipping_and_invoice.php @@ -44,5 +44,6 @@ $items[$orderItem->getId()] = $orderItem->getQtyOrdered(); } $shipment = $objectManager->get(ShipmentFactory::class)->create($order, $items); +$shipment->register(); $transaction->addObject($invoice)->addObject($shipment)->addObject($order)->save(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_tax.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_tax.php new file mode 100644 index 000000000000..0c7dc522f575 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_tax.php @@ -0,0 +1,52 @@ +create(TaxFactory::class); + +/** @var \Magento\Tax\Model\Sales\Order\Tax $tax */ +$tax = $taxFactory->create(); +$tax->setOrderId($order->getId()) + ->setCode('US-NY-*-Rate 1') + ->setTitle('US-NY-*-Rate 1') + ->setPercent(8.37) + ->setAmount($amount) + ->setBaseAmount($amount) + ->setBaseRealAmount($amount); +$tax->save(); + +$salesOrderFactory = $objectManager->create(ItemFactory::class); + +/** @var \Magento\Sales\Model\Order\Tax\Item $salesOrderItem */ +$salesOrderItem = $salesOrderFactory->create(); +$salesOrderItem->setOrderId($order->getId()) + ->setStoreId($objectManager->get(StoreManagerInterface::class)->getStore()->getId()) + ->setProductOptions([]); +$salesCollection = $objectManager->create(\Magento\Sales\Model\ResourceModel\Order\Item::class); +$salesCollection->save($salesOrderItem); + +/** @var \Magento\Sales\Model\Order\Tax\Item $salesOrderTaxItem */ +$salesOrderTaxItem = $salesOrderFactory->create(); +$salesOrderTaxItem->setTaxId($tax->getId()) + ->setTaxPercent(8.37) + ->setTaxAmount($amount) + ->setBaseAmount($amount) + ->setRealAmount($amount) + ->setRealBaseAmount($amount) + ->setAppliedTaxes([$tax]) + ->setTaxableItemType('shipping') + ->setItemId($salesOrderItem->getId()); + +$taxItemCollection = $objectManager->create(\Magento\Sales\Model\ResourceModel\Order\Tax\Item::class); +$taxItemCollection->save($salesOrderTaxItem); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_tax_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_tax_rollback.php new file mode 100644 index 000000000000..dd52deab825c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_tax_rollback.php @@ -0,0 +1,8 @@ + '100000002', + 'state' => \Magento\Sales\Model\Order::STATE_NEW, + 'status' => 'processing', + 'grand_total' => 120.00, + 'subtotal' => 120.00, + 'base_grand_total' => 120.00, + 'store_id' => 1, + 'website_id' => 1, + 'payment' => $payment + ], + [ + 'increment_id' => '100000003', + 'state' => \Magento\Sales\Model\Order::STATE_PROCESSING, + 'status' => 'processing', + 'grand_total' => 130.00, + 'base_grand_total' => 130.00, + 'subtotal' => 130.00, + 'total_paid' => 130.00, + 'store_id' => 0, + 'website_id' => 0, + 'payment' => $payment + ], + [ + 'increment_id' => '100000004', + 'state' => \Magento\Sales\Model\Order::STATE_PROCESSING, + 'status' => 'closed', + 'grand_total' => 140.00, + 'base_grand_total' => 140.00, + 'subtotal' => 140.00, + 'store_id' => 1, + 'website_id' => 1, + 'payment' => $payment + ], + [ + 'increment_id' => '100000005', + 'state' => \Magento\Sales\Model\Order::STATE_COMPLETE, + 'status' => 'complete', + 'grand_total' => 150.00, + 'base_grand_total' => 150.00, + 'subtotal' => 150.00, + 'total_paid' => 150.00, + 'store_id' => 1, + 'website_id' => 1, + 'payment' => $payment + ], + [ + 'increment_id' => '100000006', + 'state' => \Magento\Sales\Model\Order::STATE_COMPLETE, + 'status' => 'complete', + 'grand_total' => 160.00, + 'base_grand_total' => 160.00, + 'subtotal' => 160.00, + 'total_paid' => 160.00, + 'store_id' => 1, + 'website_id' => 1, + 'payment' => $payment + ], +]; + +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->create(OrderRepositoryInterface::class); +/** @var array $orderData */ +foreach ($orders as $orderData) { + /** @var $order \Magento\Sales\Model\Order */ + $order = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Sales\Model\Order::class + ); + + // Reset addresses + /** @var Order\Address $billingAddress */ + $billingAddress = $objectManager->create(OrderAddress::class, ['data' => $addressData]); + $billingAddress->setAddressType('billing'); + + $shippingAddress = clone $billingAddress; + $shippingAddress->setId(null)->setAddressType('shipping'); + + /** @var Order\Item $orderItem */ + $orderItem = $objectManager->create(Order\Item::class); + $orderItem->setProductId($product->getId()) + ->setQtyOrdered(2) + ->setBasePrice($product->getPrice()) + ->setPrice($product->getPrice()) + ->setRowTotal($product->getPrice()) + ->setProductType('simple'); + + $order + ->setData($orderData) + ->addItem($orderItem) + ->setCustomerIsGuest(false) + ->setCustomerId(1) + ->setCustomerEmail('customer@example.com') + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress); + + $orderRepository->save($order); +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/orders_with_customer_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/orders_with_customer_rollback.php new file mode 100644 index 000000000000..1fb4b4636ab2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/orders_with_customer_rollback.php @@ -0,0 +1,8 @@ +create( + \Magento\Framework\DataObject::class, + [ + 'data' => [ + 'qty' => 1, + 'custom_price' => 12, + ], + ] +); +/** @var \Magento\Quote\Model\Quote $items */ +$items = $quote->getItemsCollection()->getItems(); +$quoteItem = reset($items); +$quote->updateItem($quoteItem->getId(), $buyRequest)->save(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/quote_with_custom_price_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/quote_with_custom_price_rollback.php new file mode 100644 index 000000000000..2bcbb636b97a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/quote_with_custom_price_rollback.php @@ -0,0 +1,8 @@ +markTestSkipped('MAGETWO-87436'); - // Load the quote that contains a child of a configurable product /** @var \Magento\Quote\Model\Quote $quote */ $quote = $this->objectManager->create(\Magento\Quote\Model\Quote::class) diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupon_cart_fixed_discount_rollback.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupon_cart_fixed_discount_rollback.php new file mode 100644 index 000000000000..33a8b4285d8d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupon_cart_fixed_discount_rollback.php @@ -0,0 +1,17 @@ +get(\Magento\Framework\Registry::class); + +/** @var Magento\SalesRule\Model\Rule $rule */ +$rule = $registry->registry('cart_rule_fixed_discount_coupon'); +if ($rule) { + $rule->delete(); +} diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupon_code_with_wildcard_rollback.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupon_code_with_wildcard_rollback.php index 776c30221035..c9613c371bbe 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupon_code_with_wildcard_rollback.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupon_code_with_wildcard_rollback.php @@ -14,8 +14,19 @@ $objectManager = Bootstrap::getObjectManager(); +/** @var SearchCriteriaBuilder $searchCriteriaBuilder */ +$searchCriteriaBuilder = Bootstrap::getObjectManager()->get(SearchCriteriaBuilder::class); +$searchCriteria = $searchCriteriaBuilder->addFilter('name', '5$ fixed discount on whole cart') + ->create(); + +/** @var RuleRepositoryInterface $ruleRepository */ +$ruleRepository = Bootstrap::getObjectManager()->get(RuleRepositoryInterface::class); +$items = $ruleRepository->getList($searchCriteria) + ->getItems(); + +$salesRule = array_pop($items); + /** @var Rule $salesRule */ -$salesRule = getSalesRule('5$ fixed discount on whole cart'); if ($salesRule !== null) { /** @var RuleRepositoryInterface $ruleRepository */ $ruleRepository = $objectManager->get(RuleRepositoryInterface::class); @@ -29,18 +40,3 @@ $couponRepository = $objectManager->get(CouponRepositoryInterface::class); $couponRepository->deleteById($coupon->getCouponId()); } - -function getSalesRule(string $name) -{ - /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ - $searchCriteriaBuilder = Bootstrap::getObjectManager()->get(SearchCriteriaBuilder::class); - $searchCriteria = $searchCriteriaBuilder->addFilter('name', $name) - ->create(); - - /** @var RuleRepositoryInterface $ruleRepository */ - $ruleRepository = Bootstrap::getObjectManager()->get(RuleRepositoryInterface::class); - $items = $ruleRepository->getList($searchCriteria) - ->getItems(); - - return array_pop($items); -} diff --git a/dev/tests/integration/testsuite/Magento/Search/Model/SynonymReaderTest.php b/dev/tests/integration/testsuite/Magento/Search/Model/SynonymReaderTest.php index b9ba89ba5314..2d0020ba2268 100644 --- a/dev/tests/integration/testsuite/Magento/Search/Model/SynonymReaderTest.php +++ b/dev/tests/integration/testsuite/Magento/Search/Model/SynonymReaderTest.php @@ -48,7 +48,22 @@ public static function loadByPhraseDataProvider() ['synonyms' => 'queen,monarch', 'store_id' => 1, 'website_id' => 0], ['synonyms' => 'british,english', 'store_id' => 1, 'website_id' => 0] ] - ] + ], + [ + 'query_value', [] + ], + [ + 'query_value+', [] + ], + [ + 'query_value-', [] + ], + [ + 'query_@value', [] + ], + [ + 'query_value+@', [] + ], ]; } diff --git a/dev/tests/integration/testsuite/Magento/SendFriend/Controller/Product/CustomerSendmailTest.php b/dev/tests/integration/testsuite/Magento/SendFriend/Controller/Product/CustomerSendmailTest.php new file mode 100644 index 000000000000..8794dfdff8fd --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SendFriend/Controller/Product/CustomerSendmailTest.php @@ -0,0 +1,171 @@ +accountManagement = $this->_objectManager->create(AccountManagementInterface::class); + $this->formKey = $this->_objectManager->create(FormKey::class); + $logger = $this->createMock(LoggerInterface::class); + $this->session = $this->_objectManager->create( + Session::class, + [$logger] + ); + $this->captchaHelper = $this->_objectManager->create(CaptchaHelper::class); + $customer = $this->accountManagement->authenticate('customer@example.com', 'password'); + $this->session->setCustomerDataAsLoggedIn($customer); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testExecute() + { + $this->getRequest() + ->setMethod('POST') + ->setPostValue( + [ + 'form_key' => $this->formKey->getFormKey(), + 'sender' => [ + 'name' => 'customer', + 'email' => 'customer@example.com', + 'message' => 'example message' + ], + 'id' => 1, + 'recipients' => [ + 'name' => ['John'], + 'email' => ['example1@gmail.com'] + ] + + ] + ); + + $this->dispatch('sendfriend/product/sendmail'); + $this->assertSessionMessages( + $this->equalTo(['The link to a friend was sent.']), + MessageInterface::TYPE_SUCCESS + ); + } + + /** + * @magentoConfigFixture default_store customer/captcha/enable 1 + * @magentoConfigFixture default_store customer/captcha/failed_attempts_login 0 + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoConfigFixture default_store customer/captcha/forms product_sendtofriend_form + */ + public function testWithCaptchaFailed() + { + $this->getRequest() + ->setMethod('POST') + ->setPostValue( + [ + 'form_key' => $this->formKey->getFormKey(), + 'sender' => [ + 'name' => 'customer', + 'email' => 'customer@example.com', + 'message' => 'example message' + ], + 'id' => 1, + 'captcha' => [ + 'product_sendtofriend_form' => 'test' + ], + 'recipients' => [ + 'name' => ['John'], + 'email' => ['example1@gmail.com'] + ] + + ] + ); + + $this->dispatch('sendfriend/product/sendmail'); + $this->assertSessionMessages( + $this->equalTo(['Incorrect CAPTCHA']), + MessageInterface::TYPE_ERROR + ); + } + + /** + * @magentoConfigFixture default_store customer/captcha/enable 1 + * @magentoConfigFixture default_store customer/captcha/failed_attempts_login 0 + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoConfigFixture default_store customer/captcha/forms product_sendtofriend_form + * + */ + public function testWithCaptchaSuccess() + { + /** @var DefaultModel $captchaModel */ + $captchaModel = $this->captchaHelper->getCaptcha('product_sendtofriend_form'); + $captchaModel->generate(); + $word = $captchaModel->getWord(); + $this->getRequest() + ->setMethod('POST') + ->setPostValue( + [ + 'form_key' => $this->formKey->getFormKey(), + 'sender' => [ + 'name' => 'customer', + 'email' => 'customer@example.com', + 'message' => 'example message' + ], + 'id' => 1, + 'captcha' => [ + 'product_sendtofriend_form' => $word + ], + 'recipients' => [ + 'name' => ['John'], + 'email' => ['example1@gmail.com'] + ] + ] + ); + + $this->dispatch('sendfriend/product/sendmail'); + $this->assertSessionMessages( + $this->equalTo(['The link to a friend was sent.']), + MessageInterface::TYPE_SUCCESS + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/SendFriend/Controller/SendmailTest.php b/dev/tests/integration/testsuite/Magento/SendFriend/Controller/SendmailTest.php new file mode 100644 index 000000000000..a075398e9cdb --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SendFriend/Controller/SendmailTest.php @@ -0,0 +1,143 @@ +getProduct(); + $this->login(1); + $this->prepareRequestData(); + + $this->dispatch('sendfriend/product/sendmail/id/' . $product->getId()); + $this->assertSessionMessages( + $this->equalTo(['The link to a friend was sent.']), + MessageInterface::TYPE_SUCCESS + ); + } + + /** + * Share the product to friend as guest customer + * + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * @magentoConfigFixture default_store sendfriend/email/enabled 1 + * @magentoConfigFixture default_store sendfriend/email/allow_guest 1 + * @magentoDataFixture Magento/Catalog/_files/products.php + */ + public function testSendActionAsGuest() + { + $product = $this->getProduct(); + $this->prepareRequestData(); + + $this->dispatch('sendfriend/product/sendmail/id/' . $product->getId()); + $this->assertSessionMessages( + $this->equalTo(['The link to a friend was sent.']), + MessageInterface::TYPE_SUCCESS + ); + } + + /** + * Share the product to friend as guest customer with invalid post data + * + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * @magentoConfigFixture default_store sendfriend/email/enabled 1 + * @magentoConfigFixture default_store sendfriend/email/allow_guest 1 + * @magentoDataFixture Magento/Catalog/_files/products.php + */ + public function testSendActionAsGuestWithInvalidData() + { + $product = $this->getProduct(); + $this->prepareRequestData(true); + + $this->dispatch('sendfriend/product/sendmail/id/' . $product->getId()); + $this->assertSessionMessages( + $this->equalTo(['Invalid Sender Email']), + MessageInterface::TYPE_ERROR + ); + } + + /** + * @return ProductInterface + */ + private function getProduct() + { + return $this->_objectManager->get(ProductRepositoryInterface::class)->get('custom-design-simple-product'); + } + + /** + * Login the user + * + * @param string $customerId Customer to mark as logged in for the session + * @return void + */ + protected function login($customerId) + { + /** @var Session $session */ + $session = Bootstrap::getObjectManager() + ->get(Session::class); + $session->loginById($customerId); + } + + /** + * @param bool $invalidData + * @return void + */ + private function prepareRequestData($invalidData = false) + { + /** @var FormKey $formKey */ + $formKey = $this->_objectManager->get(FormKey::class); + $post = [ + 'sender' => [ + 'name' => 'Test', + 'email' => 'test@example.com', + 'message' => 'Message', + ], + 'recipients' => [ + 'name' => [ + 'Recipient 1', + 'Recipient 2' + ], + 'email' => [ + 'r1@example.com', + 'r2@example.com' + ] + ], + 'form_key' => $formKey->getFormKey(), + ]; + if ($invalidData) { + unset($post['sender']['email']); + } + + $this->getRequest()->setMethod(Request::METHOD_POST); + $this->getRequest()->setPostValue($post); + } +} diff --git a/dev/tests/integration/testsuite/Magento/SendFriend/Fixtures/process_config_data.php b/dev/tests/integration/testsuite/Magento/SendFriend/Fixtures/process_config_data.php new file mode 100644 index 000000000000..2c672378fb83 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SendFriend/Fixtures/process_config_data.php @@ -0,0 +1,22 @@ + $value) { + $config->setDataByPath($key, $value); + $config->save(); + } +}; + +$deleteConfigData = function (WriterInterface $writer, array $configData, string $scope, int $scopeId) { + foreach ($configData as $path) { + $writer->delete($path, $scope, $scopeId); + } +}; diff --git a/dev/tests/integration/testsuite/Magento/SendFriend/Fixtures/sendfriend_configuration.php b/dev/tests/integration/testsuite/Magento/SendFriend/Fixtures/sendfriend_configuration.php new file mode 100644 index 000000000000..229d6eddb496 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SendFriend/Fixtures/sendfriend_configuration.php @@ -0,0 +1,24 @@ + 1, + 'sendfriend/email/check_by' => 1, + +]; +$objectManager = Bootstrap::getObjectManager(); +$defConfig = $objectManager->create(Config::class); +$defConfig->setScope(ScopeConfigInterface::SCOPE_TYPE_DEFAULT); +$processConfigData($defConfig, $configData); diff --git a/dev/tests/integration/testsuite/Magento/SendFriend/Fixtures/sendfriend_configuration_rollback.php b/dev/tests/integration/testsuite/Magento/SendFriend/Fixtures/sendfriend_configuration_rollback.php new file mode 100644 index 000000000000..9265e032bbc4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SendFriend/Fixtures/sendfriend_configuration_rollback.php @@ -0,0 +1,22 @@ +get(WriterInterface::class); +$deleteConfigData($configWriter, $configData, ScopeConfigInterface::SCOPE_TYPE_DEFAULT, 0); diff --git a/dev/tests/integration/testsuite/Magento/SendFriend/_files/disable_allow_guest_config.php b/dev/tests/integration/testsuite/Magento/SendFriend/_files/disable_allow_guest_config.php new file mode 100644 index 000000000000..202a39613248 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SendFriend/_files/disable_allow_guest_config.php @@ -0,0 +1,24 @@ +create(Value::class); +$config->setPath('sendfriend/email/enabled'); +$config->setScope('default'); +$config->setScopeId(0); +$config->setValue(1); +$config->save(); + +/** @var Value $config */ +$config = Bootstrap::getObjectManager()->create(Value::class); +$config->setPath('sendfriend/email/allow_guest'); +$config->setScope('default'); +$config->setScopeId(0); +$config->setValue(0); +$config->save(); diff --git a/dev/tests/integration/testsuite/Magento/SendFriend/_files/product_simple_rollback.php b/dev/tests/integration/testsuite/Magento/SendFriend/_files/product_simple_rollback.php new file mode 100644 index 000000000000..ed98732fc870 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SendFriend/_files/product_simple_rollback.php @@ -0,0 +1,26 @@ +get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +try { + $product = $productRepository->get('simple', false, null, true); + $productRepository->delete($product); +} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { + //Product already removed +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Setup/Declaration/WhitelistDeclarationTest.php b/dev/tests/integration/testsuite/Magento/Setup/Declaration/WhitelistDeclarationTest.php index 6b92e687284d..04f25f231c93 100644 --- a/dev/tests/integration/testsuite/Magento/Setup/Declaration/WhitelistDeclarationTest.php +++ b/dev/tests/integration/testsuite/Magento/Setup/Declaration/WhitelistDeclarationTest.php @@ -47,7 +47,7 @@ public function setUp() /** * Checks that all declared table elements also declared into whitelist declaration. * - * @appIsolation + * @magentoAppIsolation enabled * @throws \Exception */ public function testConstraintsAndIndexesAreWhitelisted() diff --git a/dev/tests/integration/testsuite/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AbstractShipmentControllerTest.php b/dev/tests/integration/testsuite/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AbstractShipmentControllerTest.php new file mode 100644 index 000000000000..0a1926d58624 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AbstractShipmentControllerTest.php @@ -0,0 +1,92 @@ +transportBuilder = $this->_objectManager->get(TransportBuilderMock::class); + $this->orderRepository = $this->_objectManager->get(OrderRepository::class); + $this->formKey = $this->_objectManager->get(FormKey::class); + } + + /** + * @param string $incrementalId + * @return OrderInterface|null + */ + protected function getOrder(string $incrementalId) + { + /** @var SearchCriteria $searchCriteria */ + $searchCriteria = $this->_objectManager->create(SearchCriteriaBuilder::class) + ->addFilter(OrderInterface::INCREMENT_ID, $incrementalId) + ->create(); + + $orders = $this->orderRepository->getList($searchCriteria)->getItems(); + /** @var OrderInterface|null $order */ + $order = reset($orders); + + return $order; + } + + /** + * @param OrderInterface $order + * @return ShipmentInterface + */ + protected function getShipment(OrderInterface $order): ShipmentInterface + { + /** @var \Magento\Sales\Model\ResourceModel\Order\Shipment\Collection $shipmentCollection */ + $shipmentCollection = $this->_objectManager->create( + \Magento\Sales\Model\ResourceModel\Order\Shipment\CollectionFactory::class + )->create(); + + /** @var ShipmentInterface $shipment */ + $shipment = $shipmentCollection + ->setOrderFilter($order) + ->setPageSize(1) + ->getFirstItem(); + + return $shipment; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AddCommentTest.php b/dev/tests/integration/testsuite/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AddCommentTest.php new file mode 100644 index 000000000000..25a44bab6299 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AddCommentTest.php @@ -0,0 +1,102 @@ +prepareRequest( + [ + 'comment' => ['comment' => $comment, 'is_customer_notified' => true], + ] + ); + $this->dispatch('backend/admin/order_shipment/addComment'); + $html = $this->getResponse()->getBody(); + $this->assertContains($comment, $html); + + $message = $this->transportBuilder->getSentMessage(); + $subject =__('Update to your %1 shipment', $order->getStore()->getFrontendName())->render(); + $messageConstraint = $this->logicalAnd( + new StringContains($order->getBillingAddress()->getName()), + new RegularExpression( + sprintf( + "/Your order #%s has been updated with a status of.*%s/", + $order->getIncrementId(), + $order->getFrontendStatusLabel() + ) + ), + new StringContains($comment) + ); + + $this->assertEquals($message->getSubject(), $subject); + $this->assertThat($message->getRawMessage(), $messageConstraint); + } + + /** + * @inheritdoc + */ + public function testAclHasAccess() + { + $this->prepareRequest(['comment', ['comment' => 'Comment']]); + + parent::testAclHasAccess(); + } + + /** + * @inheritdoc + */ + public function testAclNoAccess() + { + $this->prepareRequest(['comment', ['comment' => 'Comment']]); + + parent::testAclNoAccess(); + } + + /** + * @param array $params + * @return \Magento\Sales\Api\Data\OrderInterface|null + */ + private function prepareRequest(array $params = []) + { + $order = $this->getOrder('100000001'); + $shipment = $this->getShipment($order); + + $this->getRequest()->setMethod('POST'); + $this->getRequest()->setParams( + [ + 'id' => $shipment->getEntityId(), + 'form_key' => $this->formKey->getFormKey(), + ] + ); + + $data = $params ?? []; + $this->getRequest()->setPostValue($data); + + return $order; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Shipping/Controller/Adminhtml/Order/Shipment/SaveTest.php b/dev/tests/integration/testsuite/Magento/Shipping/Controller/Adminhtml/Order/Shipment/SaveTest.php new file mode 100644 index 000000000000..27b5bb02d4b2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Shipping/Controller/Adminhtml/Order/Shipment/SaveTest.php @@ -0,0 +1,97 @@ +prepareRequest(['shipment' => ['send_email' => true]]); + $this->dispatch('backend/admin/order_shipment/save'); + + $this->assertSessionMessages( + $this->equalTo([(string)__('The shipment has been created.')]), + \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS + ); + $this->assertRedirect($this->stringContains('sales/order/view/order_id/' . $order->getEntityId())); + + $shipment = $this->getShipment($order); + $message = $this->transportBuilder->getSentMessage(); + $subject = __('Your %1 order has shipped', $order->getStore()->getFrontendName())->render(); + $messageConstraint = $this->logicalAnd( + new StringContains($order->getBillingAddress()->getName()), + new StringContains( + 'Thank you for your order from ' . $shipment->getStore()->getFrontendName() + ), + new StringContains( + "Your Shipment #{$shipment->getIncrementId()} for Order #{$order->getIncrementId()}" + ) + ); + + $this->assertEquals($message->getSubject(), $subject); + $this->assertThat($message->getRawMessage(), $messageConstraint); + } + + /** + * @inheritdoc + */ + public function testAclHasAccess() + { + $this->prepareRequest(); + + parent::testAclHasAccess(); + } + + /** + * @inheritdoc + */ + public function testAclNoAccess() + { + $this->prepareRequest(); + + parent::testAclNoAccess(); + } + + /** + * @param array $params + * @return \Magento\Sales\Api\Data\OrderInterface|null + */ + private function prepareRequest(array $params = []) + { + $order = $this->getOrder('100000001'); + $this->getRequest()->setMethod('POST'); + $this->getRequest()->setParams( + [ + 'order_id' => $order->getEntityId(), + 'form_key' => $this->formKey->getFormKey(), + ] + ); + + $data = $params ?? []; + $this->getRequest()->setPostValue($data); + + return $order; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Store/App/FrontController/Plugin/RequestPreprocessorTest.php b/dev/tests/integration/testsuite/Magento/Store/App/FrontController/Plugin/RequestPreprocessorTest.php index ebf302c16bd6..0e158821f180 100644 --- a/dev/tests/integration/testsuite/Magento/Store/App/FrontController/Plugin/RequestPreprocessorTest.php +++ b/dev/tests/integration/testsuite/Magento/Store/App/FrontController/Plugin/RequestPreprocessorTest.php @@ -56,7 +56,7 @@ public function testHttpsPassSecureLoginPost() $this->prepareRequest(true); $this->dispatch('customer/account/loginPost/'); $redirectUrl = str_replace('http://', 'https://', $this->baseUrl) . - 'index.php/customer/account/'; + 'customer/account/'; $this->assertResponseRedirect($this->getResponse(), $redirectUrl); $this->assertTrue($this->_objectManager->get(Session::class)->isLoggedIn()); $this->setFrontendCompletelySecureRollback(); diff --git a/dev/tests/integration/testsuite/Magento/Store/App/Request/PathInfoProcessorTest.php b/dev/tests/integration/testsuite/Magento/Store/App/Request/PathInfoProcessorTest.php index 90ceaa4fcc5a..e0a1321092a6 100644 --- a/dev/tests/integration/testsuite/Magento/Store/App/Request/PathInfoProcessorTest.php +++ b/dev/tests/integration/testsuite/Magento/Store/App/Request/PathInfoProcessorTest.php @@ -14,7 +14,7 @@ class PathInfoProcessorTest extends \PHPUnit\Framework\TestCase /** * @var \Magento\Store\App\Request\PathInfoProcessor */ - protected $pathProcessor; + private $pathProcessor; protected function setUp() { @@ -31,7 +31,11 @@ public function testProcessNotValidStoreCode($pathInfo) { /** @var \Magento\Framework\App\RequestInterface $request */ $request = Bootstrap::getObjectManager()->create(\Magento\Framework\App\RequestInterface::class); - $this->assertEquals($pathInfo, $this->pathProcessor->process($request, $pathInfo)); + /** @var \Magento\Framework\App\Config\ReinitableConfigInterface $config */ + $config = Bootstrap::getObjectManager()->get(\Magento\Framework\App\Config\ReinitableConfigInterface::class); + $config->setValue(Store::XML_PATH_STORE_IN_URL, true); + $info = $this->pathProcessor->process($request, $pathInfo); + $this->assertEquals($pathInfo, $info); } public function notValidStoreCodeDataProvider() @@ -46,7 +50,7 @@ public function notValidStoreCodeDataProvider() * @covers \Magento\Store\App\Request\PathInfoProcessor::process * @magentoDataFixture Magento/Store/_files/core_fixturestore.php */ - public function testProcessValidStoreCodeCase1() + public function testProcessValidStoreDisabledStoreUrl() { /** @var \Magento\Store\Model\Store $store */ $store = Bootstrap::getObjectManager()->get(\Magento\Store\Model\Store::class); @@ -57,6 +61,7 @@ public function testProcessValidStoreCodeCase1() /** @var \Magento\Framework\App\Config\ReinitableConfigInterface $config */ $config = Bootstrap::getObjectManager()->get(\Magento\Framework\App\Config\ReinitableConfigInterface::class); + $config->setValue(Store::XML_PATH_STORE_IN_URL, true); $config->setValue(Store::XML_PATH_STORE_IN_URL, false, ScopeInterface::SCOPE_STORE, $store->getCode()); $pathInfo = sprintf('/%s/m/c/a', $store->getCode()); $this->assertEquals($pathInfo, $this->pathProcessor->process($request, $pathInfo)); @@ -66,7 +71,7 @@ public function testProcessValidStoreCodeCase1() * @covers \Magento\Store\App\Request\PathInfoProcessor::process * @magentoDataFixture Magento/Store/_files/core_fixturestore.php */ - public function testProcessValidStoreCodeCase2() + public function testProcessValidStoreCodeCaseProcessStoreName() { /** @var \Magento\Store\Model\Store $store */ $store = Bootstrap::getObjectManager()->get(\Magento\Store\Model\Store::class); @@ -77,6 +82,7 @@ public function testProcessValidStoreCodeCase2() /** @var \Magento\Framework\App\Config\ReinitableConfigInterface $config */ $config = Bootstrap::getObjectManager()->get(\Magento\Framework\App\Config\ReinitableConfigInterface::class); + $config->setValue(Store::XML_PATH_STORE_IN_URL, true); $config->setValue(Store::XML_PATH_STORE_IN_URL, true, ScopeInterface::SCOPE_STORE, $store->getCode()); $pathInfo = sprintf('/%s/m/c/a', $store->getCode()); $this->assertEquals('/m/c/a', $this->pathProcessor->process($request, $pathInfo)); @@ -86,7 +92,7 @@ public function testProcessValidStoreCodeCase2() * @covers \Magento\Store\App\Request\PathInfoProcessor::process * @magentoDataFixture Magento/Store/_files/core_fixturestore.php */ - public function testProcessValidStoreCodeCase3() + public function testProcessValidStoreCodeWhenStoreIsDirectFrontNameWithFrontName() { /** @var \Magento\Store\Model\Store $store */ $store = Bootstrap::getObjectManager()->get(\Magento\Store\Model\Store::class); @@ -100,9 +106,77 @@ public function testProcessValidStoreCodeCase3() /** @var \Magento\Framework\App\Config\ReinitableConfigInterface $config */ $config = Bootstrap::getObjectManager()->get(\Magento\Framework\App\Config\ReinitableConfigInterface::class); + $config->setValue(Store::XML_PATH_STORE_IN_URL, true); $config->setValue(Store::XML_PATH_STORE_IN_URL, true, ScopeInterface::SCOPE_STORE, $store->getCode()); $pathInfo = sprintf('/%s/m/c/a', $store->getCode()); $this->assertEquals($pathInfo, $this->pathProcessor->process($request, $pathInfo)); - $this->assertEquals('noroute', $request->getActionName()); + $this->assertEquals(\Magento\Framework\App\Router\Base::NO_ROUTE, $request->getActionName()); + } + + /** + * @covers \Magento\Store\App\Request\PathInfoProcessor::process + * @magentoDataFixture Magento/Store/_files/core_fixturestore.php + */ + public function testProcessValidStoreCodeWhenStoreCodeInUrlIsDisabledWithFrontName() + { + /** @var \Magento\Store\Model\Store $store */ + $store = Bootstrap::getObjectManager()->get(\Magento\Store\Model\Store::class); + $store->load('fixturestore', 'code'); + + /** @var \Magento\Framework\App\RequestInterface $request */ + $request = Bootstrap::getObjectManager()->create( + \Magento\Framework\App\RequestInterface::class, + ['directFrontNames' => ['someFrontName' => true]] + ); + + /** @var \Magento\Framework\App\Config\ReinitableConfigInterface $config */ + $config = Bootstrap::getObjectManager()->get(\Magento\Framework\App\Config\ReinitableConfigInterface::class); + $config->setValue(Store::XML_PATH_STORE_IN_URL, true); + $config->setValue(Store::XML_PATH_STORE_IN_URL, false, ScopeInterface::SCOPE_STORE, $store->getCode()); + $pathInfo = sprintf('/%s/m/c/a', $store->getCode()); + $this->assertEquals($pathInfo, $this->pathProcessor->process($request, $pathInfo)); + } + + /** + * @covers \Magento\Store\App\Request\PathInfoProcessor::process + * @magentoDataFixture Magento/Store/_files/core_fixturestore.php + */ + public function testProcessValidStoreCodeWhenStoreCodeisAdmin() + { + /** @var \Magento\Store\Model\Store $store */ + $store = Bootstrap::getObjectManager()->get(\Magento\Store\Model\Store::class); + $store->load('fixturestore', 'code'); + + /** @var \Magento\Framework\App\RequestInterface $request */ + $request = Bootstrap::getObjectManager()->create( + \Magento\Framework\App\RequestInterface::class, + ['directFrontNames' => ['someFrontName' => true]] + ); + + /** @var \Magento\Framework\App\Config\ReinitableConfigInterface $config */ + $config = Bootstrap::getObjectManager()->get(\Magento\Framework\App\Config\ReinitableConfigInterface::class); + $config->setValue(Store::XML_PATH_STORE_IN_URL, true); + $config->setValue(Store::XML_PATH_STORE_IN_URL, false, ScopeInterface::SCOPE_STORE, $store->getCode()); + $pathInfo = sprintf('/%s/m/c/a', 'admin'); + $this->assertEquals($pathInfo, $this->pathProcessor->process($request, $pathInfo)); + } + + /** + * @covers \Magento\Store\App\Request\PathInfoProcessor::process + */ + public function testProcessValidStoreCodeWhenUrlConfigIsDisabled() + { + /** @var \Magento\Framework\App\RequestInterface $request */ + $request = Bootstrap::getObjectManager()->create( + \Magento\Framework\App\RequestInterface::class, + ['directFrontNames' => ['someFrontName' => true]] + ); + + /** @var \Magento\Framework\App\Config\ReinitableConfigInterface $config */ + $config = Bootstrap::getObjectManager()->get(\Magento\Framework\App\Config\ReinitableConfigInterface::class); + $config->setValue(Store::XML_PATH_STORE_IN_URL, false); + $pathInfo = sprintf('/%s/m/c/a', 'whatever'); + $this->assertEquals($pathInfo, $this->pathProcessor->process($request, $pathInfo)); + $this->assertEquals(null, $request->getActionName()); } } diff --git a/dev/tests/integration/testsuite/Magento/Store/Model/StoreTest.php b/dev/tests/integration/testsuite/Magento/Store/Model/StoreTest.php index 9690199eeb8b..00de5544d8fb 100644 --- a/dev/tests/integration/testsuite/Magento/Store/Model/StoreTest.php +++ b/dev/tests/integration/testsuite/Magento/Store/Model/StoreTest.php @@ -6,13 +6,16 @@ namespace Magento\Store\Model; +use Magento\Catalog\Model\ProductRepository; use Magento\Framework\App\Bootstrap; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\UrlInterface; +use Magento\Store\Api\StoreRepositoryInterface; use Zend\Stdlib\Parameters; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * phpcs:disable Magento2.Security.Superglobal */ class StoreTest extends \PHPUnit\Framework\TestCase { @@ -198,7 +201,7 @@ public function testGetBaseUrlInPub() */ public function testGetBaseUrlForCustomEntryPoint($type, $useCustomEntryPoint, $useStoreCode, $expected) { - /* config operations require store to be loaded */ + /* config operations require store to be loaded */ $this->model->load('default'); \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->get(\Magento\Framework\App\Config\MutableScopeConfigInterface::class) @@ -210,6 +213,10 @@ public function testGetBaseUrlForCustomEntryPoint($type, $useCustomEntryPoint, $ // emulate custom entry point $_SERVER['SCRIPT_FILENAME'] = 'custom_entry.php'; + $request = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Framework\App\RequestInterface::class); + $request->setServer(new Parameters($_SERVER)); + if ($useCustomEntryPoint) { $property = new \ReflectionProperty($this->model, '_isCustomEntryPoint'); $property->setAccessible(true); @@ -267,12 +274,89 @@ public function testIsCanDelete() $this->assertFalse($this->model->isCanDelete()); } + /** + * @magentoDataFixture Magento/Store/_files/core_second_third_fixturestore.php + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDbIsolation disabled + */ public function testGetCurrentUrl() { + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $objectManager->get(\Magento\Framework\App\Config\MutableScopeConfigInterface::class) + ->setValue('web/url/use_store', true, ScopeInterface::SCOPE_STORE, 'secondstore'); + $this->model->load('admin'); - $this->model->expects($this->any())->method('getUrl')->will($this->returnValue('http://localhost/index.php')); + $this->model + ->expects($this->any())->method('getUrl') + ->will($this->returnValue('http://localhost/index.php')); $this->assertStringEndsWith('default', $this->model->getCurrentUrl()); $this->assertStringEndsNotWith('default', $this->model->getCurrentUrl(false)); + + /** @var \Magento\Store\Model\Store $secondStore */ + $secondStore = $objectManager->get(StoreRepositoryInterface::class)->get('secondstore'); + + /** @var \Magento\Catalog\Model\ProductRepository $productRepository */ + $productRepository = $objectManager->create(ProductRepository::class); + $product = $productRepository->get('simple'); + $product->setStoreId($secondStore->getId()); + $url = $product->getUrlInStore(); + + $this->assertEquals( + $secondStore->getBaseUrl() . 'catalog/product/view/id/1/s/simple-product/', + $url + ); + $this->assertEquals( + $secondStore->getBaseUrl() . '?___from_store=default', + $secondStore->getCurrentUrl() + ); + $this->assertEquals( + $secondStore->getBaseUrl(), + $secondStore->getCurrentUrl(false) + ); + } + + /** + * @magentoDataFixture Magento/Store/_files/second_store.php + * @magentoDataFixture Magento/Catalog/_files/category_product.php + * @magentoDbIsolation disabled + */ + public function testGetCurrentUrlWithUseStoreInUrlFalse() + { + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $objectManager->get(\Magento\Framework\App\Config\ReinitableConfigInterface::class) + ->setValue('web/url/use_store', false, ScopeInterface::SCOPE_STORE, 'default'); + + /** @var \Magento\Store\Model\Store $secondStore */ + $secondStore = $objectManager->get(StoreRepositoryInterface::class)->get('fixture_second_store'); + + /** @var \Magento\Catalog\Model\ProductRepository $productRepository */ + $productRepository = $objectManager->create(ProductRepository::class); + $product = $productRepository->get('simple333'); + + $product->setStoreId($secondStore->getId()); + $url = $product->getUrlInStore(); + + /** @var \Magento\Catalog\Model\CategoryRepository $categoryRepository */ + $categoryRepository = $objectManager->get(\Magento\Catalog\Model\CategoryRepository::class); + $category = $categoryRepository->get(333, $secondStore->getStoreId()); + + $this->assertEquals( + $secondStore->getBaseUrl() . 'catalog/category/view/s/category-1/id/333/', + $category->getUrl() + ); + $this->assertEquals( + $secondStore->getBaseUrl() . + 'catalog/product/view/id/333/s/simple-product-three/?___store=fixture_second_store', + $url + ); + $this->assertEquals( + $secondStore->getBaseUrl() . '?___store=fixture_second_store&___from_store=default', + $secondStore->getCurrentUrl() + ); + $this->assertEquals( + $secondStore->getBaseUrl() . '?___store=fixture_second_store', + $secondStore->getCurrentUrl(false) + ); } /** @@ -325,7 +409,7 @@ public function testSaveValidation($badStoreData) /** * @return array */ - public static function saveValidationDataProvider() + public function saveValidationDataProvider() { return [ 'empty store name' => [['name' => '']], diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/store.php b/dev/tests/integration/testsuite/Magento/Store/_files/store.php index 94ea5a9364b8..7e30978af48b 100644 --- a/dev/tests/integration/testsuite/Magento/Store/_files/store.php +++ b/dev/tests/integration/testsuite/Magento/Store/_files/store.php @@ -22,7 +22,18 @@ $store->save(); } else { if ($store->getId()) { + /** @var \Magento\TestFramework\Helper\Bootstrap $registry */ + $registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + \Magento\Framework\Registry::class + ); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + $store->delete(); + + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + $store = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\Store::class); $store->setData( [ diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/websites_different_countries.php b/dev/tests/integration/testsuite/Magento/Store/_files/websites_different_countries.php index 13a43d0a710a..970b1619f021 100644 --- a/dev/tests/integration/testsuite/Magento/Store/_files/websites_different_countries.php +++ b/dev/tests/integration/testsuite/Magento/Store/_files/websites_different_countries.php @@ -10,6 +10,7 @@ use Magento\Store\Model\Store; use Magento\CatalogSearch\Model\Indexer\Fulltext as FulltextIndex; use Magento\Framework\App\Config\ReinitableConfigInterface; +use Magento\Store\Model\Group; $objectManager = Bootstrap::getObjectManager(); //Creating second website with a store. @@ -21,12 +22,23 @@ $website->setData([ 'code' => 'test', 'name' => 'Test Website', - 'default_group_id' => '1', 'is_default' => '0', ]); $website->save(); } +/** + * @var Group $storeGroup + */ +$storeGroup = $objectManager->create(Group::class); +$storeGroup->setCode('some_group') + ->setName('custom store group') + ->setWebsite($website); +$storeGroup->save($storeGroup); + +$website->setDefaultGroupId($storeGroup->getId()); +$website->save($website); + $websiteId = $website->getId(); $store = $objectManager->create(Store::class); $store->load('fixture_second_store', 'code'); @@ -56,7 +68,7 @@ //Allowed countries for second website $configResource->saveConfig( 'general/country/allow', - 'ES', + 'ES,US,UK,DE', \Magento\Store\Model\ScopeInterface::SCOPE_WEBSITES, $websiteId ); diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/websites_different_countries_rollback.php b/dev/tests/integration/testsuite/Magento/Store/_files/websites_different_countries_rollback.php index 0e4de1595bbc..97f7db97aa27 100644 --- a/dev/tests/integration/testsuite/Magento/Store/_files/websites_different_countries_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Store/_files/websites_different_countries_rollback.php @@ -13,6 +13,11 @@ use Magento\Framework\App\Config\ReinitableConfigInterface; $objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + //Deleting second website's store. $store = $objectManager->create(Store::class); if ($store->load('fixture_second_store', 'code')->getId()) { @@ -20,10 +25,6 @@ } //Deleting the second website. -/** @var Registry $registry */ -$registry = $objectManager->get(Registry::class); -$registry->unregister('isSecureArea'); -$registry->register('isSecureArea', true); $configResource = $objectManager->get(\Magento\Config\Model\ResourceModel\Config::class); //Restoring allowed countries. diff --git a/dev/tests/integration/testsuite/Magento/Swatches/Controller/Adminhtml/Product/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Swatches/Controller/Adminhtml/Product/AttributeTest.php index 969d9530ae54..f806674d2970 100644 --- a/dev/tests/integration/testsuite/Magento/Swatches/Controller/Adminhtml/Product/AttributeTest.php +++ b/dev/tests/integration/testsuite/Magento/Swatches/Controller/Adminhtml/Product/AttributeTest.php @@ -7,6 +7,8 @@ namespace Magento\Swatches\Controller\Adminhtml\Product; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Data\Form\FormKey; use Magento\Framework\Exception\LocalizedException; /** @@ -17,6 +19,21 @@ */ class AttributeTest extends \Magento\TestFramework\TestCase\AbstractBackendController { + /** + * @var FormKey + */ + private $formKey; + + /** + * @inheritDoc + */ + protected function setUp() + { + parent::setUp(); + + $this->formKey = $this->_objectManager->get(FormKey::class); + } + /** * Generate random hex color. * @@ -38,22 +55,26 @@ private function getSwatchVisualDataSet(int $optionsCount) : array $optionsData = []; $expectedOptionsLabels = []; for ($i = 0; $i < $optionsCount; $i++) { - $order = $i + 1; - $expectedOptionLabelOnStoreView = "value_{$i}_store_1"; + $expectedOptionLabelOnStoreView = 'value_' . $i .'_store_1'; $expectedOptionsLabels[$i+1] = $expectedOptionLabelOnStoreView; - $optionsData []= "optionvisual[order][option_{$i}]={$order}"; - $optionsData []= "defaultvisual[]=option_{$i}"; - $optionsData []= "swatchvisual[value][option_{$i}]={$this->getRandomColor()}"; - $optionsData []= "optionvisual[value][option_{$i}][0]=value_{$i}_admin"; - $optionsData []= "optionvisual[value][option_{$i}][1]={$expectedOptionLabelOnStoreView}"; - $optionsData []= "optionvisual[delete][option_{$i}]="; + $optionId = 'option_' .$i; + $optionRowData = []; + $optionRowData['optionvisual']['order'][$optionId] = $i + 1; + $optionRowData['defaultvisual'][] = $optionId; + $optionRowData['swatchvisual']['value'][$optionId] = $this->getRandomColor(); + $optionRowData['optionvisual']['value'][$optionId][0] = 'value_' . $i .'_admin'; + $optionRowData['optionvisual']['value'][$optionId][1] = $expectedOptionLabelOnStoreView; + $optionRowData['optionvisual']['delete'][$optionId] = ''; + $optionsData[] = http_build_query($optionRowData); } - $optionsData []= "visual_swatch_validation="; - $optionsData []= "visual_swatch_validation_unique="; return [ 'attribute_data' => array_merge_recursive( [ - 'serialized_swatch_values' => json_encode($optionsData), + 'serialized_options' => json_encode($optionsData), + ], + [ + 'visual_swatch_validation' => '', + 'visual_swatch_validation_unique' => '', ], $this->getAttributePreset(), [ @@ -76,22 +97,26 @@ private function getSwatchTextDataSet(int $optionsCount) : array $optionsData = []; $expectedOptionsLabels = []; for ($i = 0; $i < $optionsCount; $i++) { - $order = $i + 1; - $expectedOptionLabelOnStoreView = "value_{$i}_store_1"; + $expectedOptionLabelOnStoreView = 'value_' . $i . '_store_1'; $expectedOptionsLabels[$i+1] = $expectedOptionLabelOnStoreView; - $optionsData []= "optiontext[order][option_{$i}]={$order}"; - $optionsData []= "defaulttext[]=option_{$i}"; - $optionsData []= "swatchtext[value][option_{$i}]=x{$i}"; - $optionsData []= "optiontext[value][option_{$i}][0]=value_{$i}_admin"; - $optionsData []= "optiontext[value][option_{$i}][1]={$expectedOptionLabelOnStoreView}"; - $optionsData []= "optiontext[delete][option_{$i}]="; + $optionId = 'option_' . $i; + $optionRowData = []; + $optionRowData['optiontext']['order'][$optionId] = $i + 1; + $optionRowData['defaulttext'][] = $optionId; + $optionRowData['swatchtext']['value'][$optionId] = 'x' . $i ; + $optionRowData['optiontext']['value'][$optionId][0] = 'value_' . $i . '_admin'; + $optionRowData['optiontext']['value'][$optionId][1]= $expectedOptionLabelOnStoreView; + $optionRowData['optiontext']['delete'][$optionId]=''; + $optionsData[] = http_build_query($optionRowData); } - $optionsData []= "text_swatch_validation="; - $optionsData []= "text_swatch_validation_unique="; return [ 'attribute_data' => array_merge_recursive( [ - 'serialized_swatch_values' => json_encode($optionsData), + 'serialized_options' => json_encode($optionsData), + ], + [ + 'text_swatch_validation' => '', + 'text_swatch_validation_unique' => '', ], $this->getAttributePreset(), [ @@ -111,7 +136,6 @@ private function getSwatchTextDataSet(int $optionsCount) : array private function getAttributePreset() : array { return [ - 'serialized_options' => '[]', 'form_key' => 'XxtpPYjm2YPYUlAt', 'frontend_label' => [ 0 => 'asdasd', @@ -175,7 +199,9 @@ public function testLargeOptionsDataSet( int $expectedOptionsCount, array $expectedLabels ) : void { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($attributeData); + $this->getRequest()->setPostValue('form_key', $this->formKey->getFormKey()); $this->dispatch('backend/catalog/product_attribute/save'); $entityTypeId = $this->_objectManager->create( \Magento\Eav\Model\Entity::class diff --git a/dev/tests/integration/testsuite/Magento/Swatches/Observer/AddSwatchAttributeTypeObserverTest.php b/dev/tests/integration/testsuite/Magento/Swatches/Observer/AddSwatchAttributeTypeObserverTest.php new file mode 100644 index 000000000000..15d10c2ddffd --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Swatches/Observer/AddSwatchAttributeTypeObserverTest.php @@ -0,0 +1,48 @@ +get(ManagerInterface::class); + $response = new DataObject(); + $response->setTypes([]); + + $eventManager->dispatch( + 'adminhtml_product_attribute_types', + ['response' => $response] + ); + + $responseTypes = $response->getTypes(); + + self::assertGreaterThan(0, count($responseTypes)); + + /* Iterate through values since other types (not swatches) might be added by observers */ + $responseTypeValues = []; + foreach ($responseTypes as $responseType) { + $responseTypeValues[] = $responseType['value']; + } + + self::assertTrue(in_array(Swatch::SWATCH_TYPE_VISUAL_ATTRIBUTE_FRONTEND_INPUT, $responseTypeValues)); + self::assertTrue(in_array(Swatch::SWATCH_TYPE_TEXTUAL_ATTRIBUTE_FRONTEND_INPUT, $responseTypeValues)); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Tax/Model/Sales/Total/Quote/SetupUtil.php b/dev/tests/integration/testsuite/Magento/Tax/Model/Sales/Total/Quote/SetupUtil.php index bd505fd4db03..985019b687ce 100644 --- a/dev/tests/integration/testsuite/Magento/Tax/Model/Sales/Total/Quote/SetupUtil.php +++ b/dev/tests/integration/testsuite/Magento/Tax/Model/Sales/Total/Quote/SetupUtil.php @@ -7,10 +7,19 @@ namespace Magento\Tax\Model\Sales\Total\Quote; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Quote\Model\Quote; use Magento\Tax\Model\Config; use Magento\Tax\Model\Calculation; +use Magento\Quote\Model\Quote\Item\Updater; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\Api\Filter; +use Magento\Framework\Api\Search\FilterGroup; +use Magento\Framework\Api\SearchCriteriaInterface; /** + * Setup utility for quote + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class SetupUtil @@ -594,7 +603,7 @@ protected function createCartRule($ruleDataOverride) * * @param array $quoteData * @param \Magento\Customer\Api\Data\CustomerInterface $customer - * @return \Magento\Quote\Model\Quote + * @return Quote */ protected function createQuote($quoteData, $customer) { @@ -619,8 +628,8 @@ protected function createQuote($quoteData, $customer) $quoteBillingAddress = $this->objectManager->create(\Magento\Quote\Model\Quote\Address::class); $quoteBillingAddress->importCustomerAddressData($addressService->getById($billingAddress->getId())); - /** @var \Magento\Quote\Model\Quote $quote */ - $quote = $this->objectManager->create(\Magento\Quote\Model\Quote::class); + /** @var Quote $quote */ + $quote = $this->objectManager->create(Quote::class); $quote->setStoreId(1) ->setIsActive(true) ->setIsMultiShipping(false) @@ -634,7 +643,7 @@ protected function createQuote($quoteData, $customer) /** * Add products to quote * - * @param \Magento\Quote\Model\Quote $quote + * @param Quote $quote * @param array $itemsData * @return $this */ @@ -657,7 +666,8 @@ protected function addProductToQuote($quote, $itemsData) * Create a quote based on given data * * @param array $quoteData - * @return \Magento\Quote\Model\Quote + * + * @return Quote */ public function setupQuote($quoteData) { @@ -666,7 +676,9 @@ public function setupQuote($quoteData) $quote = $this->createQuote($quoteData, $customer); $this->addProductToQuote($quote, $quoteData['items']); - + if (isset($quoteData['update_items'])) { + $this->updateItems($quote, $quoteData['update_items']); + } //Set shipping amount if (isset($quoteData['shipping_method'])) { $quote->getShippingAddress()->setShippingMethod($quoteData['shipping_method']); @@ -683,4 +695,33 @@ public function setupQuote($quoteData) return $quote; } + + /** + * Update quote items + * + * @param Quote $quote + * @param array $items + * + * @return void + */ + private function updateItems(Quote $quote, array $items): void + { + $updater = $this->objectManager->get(Updater::class); + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $filter = $this->objectManager->create(Filter::class); + $filter->setField('sku')->setValue(array_keys($items)); + $filterGroup = $this->objectManager->create(FilterGroup::class); + $filterGroup->setFilters([$filter]); + $searchCriteria = $this->objectManager->create(SearchCriteriaInterface::class); + $searchCriteria->setFilterGroups([$filterGroup]); + $products = $productRepository->getList($searchCriteria)->getItems(); + /** @var ProductInterface $product */ + foreach ($products as $product) { + $quoteItem = $quote->getItemByProduct($product); + $updater->update( + $quoteItem, + $items[$product->getSku()] + ); + } + } } diff --git a/dev/tests/integration/testsuite/Magento/Tax/_files/scenarios/including_tax_with_custom_price.php b/dev/tests/integration/testsuite/Magento/Tax/_files/scenarios/including_tax_with_custom_price.php new file mode 100644 index 000000000000..290c133f455f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Tax/_files/scenarios/including_tax_with_custom_price.php @@ -0,0 +1,93 @@ + [ + SetupUtil::CONFIG_OVERRIDES => [ + Config::CONFIG_XML_PATH_PRICE_INCLUDES_TAX => 1, + Config::CONFIG_XML_PATH_APPLY_ON => 0, + ], + SetupUtil::TAX_RATE_OVERRIDES => [ + SetupUtil::TAX_RATE_TX => 8.25, + SetupUtil::TAX_STORE_RATE => 8.25, + ], + SetupUtil::TAX_RULE_OVERRIDES => [ + ], + ], + 'quote_data' => [ + 'billing_address' => [ + 'region_id' => SetupUtil::REGION_TX, + ], + 'shipping_address' => [ + 'region_id' => SetupUtil::REGION_TX, + ], + 'items' => [ + [ + 'sku' => 'simple1', + 'price' => 16.24, + 'qty' => 1, + ], + ], + 'update_items' => [ + 'simple1' => [ + 'custom_price' => 14, + 'qty' => 1, + ], + ], + ], + 'expected_results' => [ + 'address_data' => [ + 'subtotal' => 12.93, + 'base_subtotal' => 12.93, + 'subtotal_incl_tax' => 14, + 'base_subtotal_incl_tax' => 14, + 'tax_amount' => 1.07, + 'base_tax_amount' => 1.07, + 'shipping_amount' => 0, + 'base_shipping_amount' => 0, + 'shipping_incl_tax' => 0, + 'base_shipping_incl_tax' => 0, + 'shipping_taxable' => 0, + 'base_shipping_taxable' => 0, + 'shipping_tax_amount' => 0, + 'base_shipping_tax_amount' => 0, + 'discount_amount' => 0, + 'base_discount_amount' => 0, + 'discount_tax_compensation_amount' => 0, + 'base_discount_tax_compensation_amount' => 0, + 'shipping_discount_tax_compensation_amount' => 0, + 'base_shipping_discount_tax_compensation_amount' => 0, + 'grand_total' => 14, + 'base_grand_total' => 14, + ], + 'items_data' => [ + 'simple1' => [ + 'row_total' => 12.93, + 'base_row_total' => 12.93, + 'tax_percent' => 8.25, + 'price' => 12.93, + 'custom_price' => 12.93, + 'original_custom_price' => 14, + 'base_price' => 12.93, + 'price_incl_tax' => 14, + 'base_price_incl_tax' => 14, + 'row_total_incl_tax' => 14, + 'base_row_total_incl_tax' => 14, + 'tax_amount' => 1.07, + 'base_tax_amount' => 1.07, + 'discount_amount' => 0, + 'base_discount_amount' => 0, + 'discount_percent' => 0, + 'discount_tax_compensation_amount' => 0, + 'base_discount_tax_compensation_amount' => 0, + ], + ], + ], +]; diff --git a/dev/tests/integration/testsuite/Magento/Tax/_files/tax_calculation_data_aggregated.php b/dev/tests/integration/testsuite/Magento/Tax/_files/tax_calculation_data_aggregated.php index f22b48a25968..3c56b1bf815a 100644 --- a/dev/tests/integration/testsuite/Magento/Tax/_files/tax_calculation_data_aggregated.php +++ b/dev/tests/integration/testsuite/Magento/Tax/_files/tax_calculation_data_aggregated.php @@ -3,14 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); /** - * Global array that holds test scenarios data + * Global array that holds test scenarios data. * * @var array */ $taxCalculationData = []; - +//phpcs:disable Magento2.Security.IncludeFile require_once __DIR__ . '/scenarios/excluding_tax_apply_tax_after_discount.php'; require_once __DIR__ . '/scenarios/excluding_tax_apply_tax_after_discount_discount_tax.php'; require_once __DIR__ . '/scenarios/excluding_tax_apply_tax_before_discount.php'; @@ -31,3 +32,4 @@ require_once __DIR__ . '/scenarios/multi_tax_rule_two_row_calculate_subtotal_yes_row.php'; require_once __DIR__ . '/scenarios/multi_tax_rule_two_row_calculate_subtotal_yes_total.php'; require_once __DIR__ . '/scenarios/including_tax_apply_tax_after_discount.php'; +require_once __DIR__ . '/scenarios/including_tax_with_custom_price.php'; diff --git a/dev/tests/integration/testsuite/Magento/Test/Integrity/Modular/LayoutFilesTest.php b/dev/tests/integration/testsuite/Magento/Test/Integrity/Modular/LayoutFilesTest.php index a11cd4b73b5c..32a58e285d76 100644 --- a/dev/tests/integration/testsuite/Magento/Test/Integrity/Modular/LayoutFilesTest.php +++ b/dev/tests/integration/testsuite/Magento/Test/Integrity/Modular/LayoutFilesTest.php @@ -77,9 +77,8 @@ public function layoutArgumentsDataProvider() */ protected function isSkippedArgument(array $argumentData) { - // Do not take into account argument name and parameters - unset($argumentData['name']); - unset($argumentData['param']); + // Do not take into account argument name, shared and parameters + unset($argumentData['name'], $argumentData['param'], $argumentData['shared']); $isUpdater = isset($argumentData['updater']); unset($argumentData['updater']); diff --git a/dev/tests/integration/testsuite/Magento/Test/Integrity/StaticFilesTest.php b/dev/tests/integration/testsuite/Magento/Test/Integrity/StaticFilesTest.php index 2afb6e90667d..eee166ab341a 100644 --- a/dev/tests/integration/testsuite/Magento/Test/Integrity/StaticFilesTest.php +++ b/dev/tests/integration/testsuite/Magento/Test/Integrity/StaticFilesTest.php @@ -76,7 +76,7 @@ protected function setUp() /** * Scan references to files from other static files and assert they are correct * - * The CSS or LESS files may refer to other resources using @import or url() notation + * The CSS or LESS files may refer to other resources using `import` or url() notation * We want to check integrity of all these references * Note that the references may have syntax specific to the Magento preprocessing subsystem * diff --git a/dev/tests/integration/testsuite/Magento/Theme/Controller/Adminhtml/System/Design/Config/SaveTest.php b/dev/tests/integration/testsuite/Magento/Theme/Controller/Adminhtml/System/Design/Config/SaveTest.php index 95fcf24894c8..cb684c021688 100644 --- a/dev/tests/integration/testsuite/Magento/Theme/Controller/Adminhtml/System/Design/Config/SaveTest.php +++ b/dev/tests/integration/testsuite/Magento/Theme/Controller/Adminhtml/System/Design/Config/SaveTest.php @@ -6,6 +6,7 @@ namespace Magento\Theme\Controller\Adminhtml\System\Design\Config; +use Magento\Framework\App\Request\Http; use Magento\Framework\Data\Form\FormKey; use Magento\TestFramework\TestCase\AbstractBackendController; @@ -29,6 +30,9 @@ class SaveTest extends AbstractBackendController */ protected $uri = 'backend/theme/design_config/save'; + /** + * @inheritdoc + */ protected function setUp() { parent::setUp(); @@ -36,6 +40,7 @@ protected function setUp() $this->formKey = $this->_objectManager->get( FormKey::class ); + $this->httpMethod = Http::METHOD_POST; } /** @@ -107,12 +112,11 @@ private function getRequestParams() ]; } + /** + * @inheritDoc + */ public function testAclHasAccess() { - $this->getRequest()->setMethod( - \Zend\Http\Request::METHOD_POST - ); - $this->getRequest()->setParams( [ 'form_key' => $this->formKey->getFormKey() diff --git a/dev/tests/integration/testsuite/Magento/Ui/Controller/Adminhtml/Index/Renderer/HandleTest.php b/dev/tests/integration/testsuite/Magento/Ui/Controller/Adminhtml/Index/Renderer/HandleTest.php new file mode 100644 index 000000000000..dbe721a76e2c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Ui/Controller/Adminhtml/Index/Renderer/HandleTest.php @@ -0,0 +1,52 @@ +configure([ + 'preferences' => [ + AuthorizationInterface::class => \Magento\Ui\Model\AuthorizationMock::class + ] + ]); + $this->getRequest()->setParam('handle', 'customer_index_index'); + $this->getRequest()->setParam('namespace', 'customer_listing'); + $this->getRequest()->setParam('sorting%5Bfield%5D', 'entity_id'); + $this->getRequest()->setParam('paging%5BpageSize%5D', 20); + $this->getRequest()->setParam('isAjax', 1); + $this->dispatch('backend/mui/index/render_handle'); + $output = $this->getResponse()->getBody(); + $this->assertEmpty($output, 'The acl restriction wasn\'t applied properly'); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + */ + public function testExecuteWhenUserHasPermission() + { + $this->getRequest()->setParam('handle', 'customer_index_index'); + $this->getRequest()->setParam('namespace', 'customer_listing'); + $this->getRequest()->setParam('sorting%5Bfield%5D', 'entity_id'); + $this->getRequest()->setParam('paging%5BpageSize%5D', 20); + $this->getRequest()->setParam('isAjax', 1); + $this->dispatch('backend/mui/index/render_handle'); + $output = $this->getResponse()->getBody(); + $this->assertNotEmpty($output, 'The acl restriction wasn\'t applied properly'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Ui/Model/AuthorizationMock.php b/dev/tests/integration/testsuite/Magento/Ui/Model/AuthorizationMock.php new file mode 100644 index 000000000000..76f472f30eb1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Ui/Model/AuthorizationMock.php @@ -0,0 +1,27 @@ +objectManager->create(\Magento\Store\Api\StoreRepositoryInterface::class); $toStore = $storeRepository->get($toStoreCode); - $redirectUrl = $expectedUrl = "http://localhost/page-c"; + $redirectUrl = "http://localhost/index.php/page-c/"; + $expectedUrl = "http://localhost/index.php/page-c-on-2nd-store"; $this->assertEquals($expectedUrl, $this->storeSwitcher->switch($fromStore, $toStore, $redirectUrl)); } diff --git a/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/UrlFinderInterfaceTest.php b/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/UrlFinderInterfaceTest.php new file mode 100644 index 000000000000..b6055f14e79d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/UrlFinderInterfaceTest.php @@ -0,0 +1,71 @@ +urlFinder = Bootstrap::getObjectManager()->create(UrlFinderInterface::class); + } + + /** + * @dataProvider findOneDataProvider + * @param string $requestPath + * @param string $targetPath + * @param int $redirectType + */ + public function testFindOneByData(string $requestPath, string $targetPath, int $redirectType) + { + $data = [ + UrlRewrite::REQUEST_PATH => $requestPath, + ]; + $urlRewrite = $this->urlFinder->findOneByData($data); + $this->assertEquals($targetPath, $urlRewrite->getTargetPath()); + $this->assertEquals($redirectType, $urlRewrite->getRedirectType()); + } + + /** + * @return array + */ + public function findOneDataProvider(): array + { + return [ + ['string', 'test_page1', 0], + ['string/', 'string', 301], + ['string_permanent', 'test_page1', 301], + ['string_permanent/', 'test_page1', 301], + ['string_temporary', 'test_page1', 302], + ['string_temporary/', 'test_page1', 302], + ['строка', 'test_page1', 0], + ['строка/', 'строка', 301], + [urlencode('строка'), 'test_page2', 0], + [urlencode('строка') . '/', urlencode('строка'), 301], + ['другая_строка', 'test_page1', 302], + ['другая_строка/', 'test_page1', 302], + [urlencode('другая_строка'), 'test_page1', 302], + [urlencode('другая_строка') . '/', 'test_page1', 302], + ['السلسلة', 'test_page1', 0], + [urlencode('السلسلة'), 'test_page1', 0], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrites.php b/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrites.php new file mode 100644 index 000000000000..9edc6507308e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrites.php @@ -0,0 +1,42 @@ +create(\Magento\UrlRewrite\Model\ResourceModel\UrlRewrite::class); +foreach ($rewritesData as $rewriteData) { + list ($requestPath, $targetPath, $redirectType) = $rewriteData; + $rewrite = $objectManager->create(\Magento\UrlRewrite\Model\UrlRewrite::class); + $rewrite->setEntityType('custom') + ->setRequestPath($requestPath) + ->setTargetPath($targetPath) + ->setRedirectType($redirectType); + $rewriteResource->save($rewrite); +} diff --git a/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrites_rollback.php b/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrites_rollback.php new file mode 100644 index 000000000000..a98f947d614e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrites_rollback.php @@ -0,0 +1,20 @@ +get(\Magento\Framework\Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$urlRewriteCollection = $objectManager->create(\Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollection::class); +$collection = $urlRewriteCollection + ->addFieldToFilter('target_path', ['test_page1', 'test_page2']) + ->load() + ->walk('delete'); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/AuthTest.php b/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/AuthTest.php index 1a726c4e2c17..5f4964d042da 100644 --- a/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/AuthTest.php +++ b/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/AuthTest.php @@ -5,6 +5,9 @@ */ namespace Magento\User\Controller\Adminhtml; +use Magento\TestFramework\Mail\Template\TransportBuilderMock; +use Magento\TestFramework\Helper\Bootstrap; + /** * Test class for \Magento\User\Controller\Adminhtml\Auth * @@ -35,7 +38,7 @@ public function testForgotpasswordAction() $this->dispatch('backend/admin/auth/forgotpassword'); $this->assertRedirect( $this->equalTo( - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + Bootstrap::getObjectManager()->get( \Magento\Backend\Helper\Data::class )->getHomePageUrl() ) @@ -51,22 +54,25 @@ public function testForgotpasswordAction() */ public function testEmailSendForgotPasswordAction() { - $transportBuilderMock = $this->prepareEmailMock( - 1, - 'admin_emails_forgot_email_template', - 'general' + /** @var TransportBuilderMock $transportMock */ + $transportMock = Bootstrap::getObjectManager()->get( + TransportBuilderMock::class ); - $this->addMockToClass($transportBuilderMock, \Magento\User\Model\User::class); - $this->getRequest()->setPostValue('email', 'adminUser@example.com'); $this->dispatch('backend/admin/auth/forgotpassword'); $this->assertRedirect( $this->equalTo( - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + Bootstrap::getObjectManager()->get( \Magento\Backend\Helper\Data::class )->getHomePageUrl() ) ); + $message = $transportMock->getSentMessage(); + $this->assertNotEmpty($message); + $this->assertEquals( + __('Password Reset Confirmation for %1', ['John Doe'])->render(), + $message->getSubject() + ); } /** @@ -79,13 +85,13 @@ public function testEmailSendForgotPasswordAction() public function testResetPasswordAction() { /** @var $user \Magento\User\Model\User */ - $user = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + $user = Bootstrap::getObjectManager()->create( \Magento\User\Model\User::class )->loadByUsername( 'dummy_username' ); $this->assertNotEmpty($user->getId(), 'Broken fixture'); - $resetPasswordToken = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + $resetPasswordToken = Bootstrap::getObjectManager()->get( \Magento\User\Helper\Data::class )->generateResetPasswordLinkToken(); $user->changeResetPasswordLinkToken($resetPasswordToken); @@ -123,7 +129,7 @@ public function testResetPasswordActionWithDummyToken() */ public function testResetPasswordPostAction($password, $passwordConfirmation, $isPasswordChanged) { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $objectManager = Bootstrap::getObjectManager(); /** @var $user \Magento\User\Model\User */ $user = $objectManager->create(\Magento\User\Model\User::class); @@ -203,7 +209,7 @@ public function testResetPasswordPostActionWithDummyToken() \Magento\Framework\Message\MessageInterface::TYPE_ERROR ); - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $objectManager = Bootstrap::getObjectManager(); /** @var \Magento\Backend\Helper\Data $backendHelper */ $backendHelper = $objectManager->get(\Magento\Backend\Helper\Data::class); @@ -218,7 +224,7 @@ public function testResetPasswordPostActionWithDummyToken() */ public function testResetPasswordPostActionWithInvalidPassword() { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $objectManager = Bootstrap::getObjectManager(); $user = $objectManager->create(\Magento\User\Model\User::class); $user->loadByUsername('dummy_username'); diff --git a/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/User/InvalidateTokenTest.php b/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/User/InvalidateTokenTest.php index 672cbd7a586e..937a26fdf0a8 100644 --- a/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/User/InvalidateTokenTest.php +++ b/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/User/InvalidateTokenTest.php @@ -89,10 +89,6 @@ public function testInvalidateTokenNoTokens() // invalidate token $this->getRequest()->setParam('user_id', $adminUserId); $this->dispatch('backend/admin/user/invalidateToken'); - $this->assertSessionMessages( - $this->equalTo(['This user has no tokens.']), - MessageInterface::TYPE_ERROR - ); } public function testInvalidateTokenNoUser() @@ -110,9 +106,5 @@ public function testInvalidateTokenInvalidUser() // invalidate token $this->getRequest()->setParam('user_id', $adminUserId); $this->dispatch('backend/admin/user/invalidateToken'); - $this->assertSessionMessages( - $this->equalTo(['This user has no tokens.']), - MessageInterface::TYPE_ERROR - ); } } diff --git a/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/UserTest.php b/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/UserTest.php index 995ec5f6bed8..9cf8fc43951d 100644 --- a/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/UserTest.php +++ b/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/UserTest.php @@ -6,6 +6,7 @@ namespace Magento\User\Controller\Adminhtml; +use Magento\Framework\App\Request\Http as HttpRequest; use Magento\TestFramework\Bootstrap; /** @@ -35,6 +36,7 @@ public function testIndexAction() */ public function testSaveActionNoData() { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->dispatch('backend/admin/user/save'); $this->assertRedirect($this->stringContains('backend/admin/user/index/')); } @@ -55,6 +57,7 @@ public function testSaveActionWrongId() $userId = $user->getId(); $this->assertNotEmpty($userId, 'Broken fixture'); $user->delete(); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue('user_id', $userId); $this->dispatch('backend/admin/user/save'); $this->assertSessionMessages( @@ -72,6 +75,7 @@ public function testSaveActionWrongId() public function testSaveActionMissingCurrentAdminPassword() { $fixture = uniqid(); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue( [ 'username' => $fixture, @@ -99,6 +103,7 @@ public function testSaveActionMissingCurrentAdminPassword() public function testSaveAction() { $fixture = uniqid(); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue( [ 'username' => $fixture, @@ -126,6 +131,7 @@ public function testSaveAction() */ public function testSaveActionDuplicateUser() { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue( [ 'username' => 'adminUser', @@ -147,13 +153,17 @@ public function testSaveActionDuplicateUser() } /** - * Verify password change properly updates fields when the request is valid + * Verify password change properly updates fields when the request is valid. + * + * @param array $postData + * @param bool $isPasswordCorrect * * @magentoDbIsolation enabled * @dataProvider saveActionPasswordChangeDataProvider */ public function testSaveActionPasswordChange($postData, $isPasswordCorrect) { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); $this->getRequest()->setPostValue($postData); $this->dispatch('backend/admin/user/save'); 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 2af563b35a39..f23a8fcd1bcf 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/Webapi/Controller/PathProcessorTest.php b/dev/tests/integration/testsuite/Magento/Webapi/Controller/PathProcessorTest.php index 932ad03d691e..14fbc2ffc67d 100644 --- a/dev/tests/integration/testsuite/Magento/Webapi/Controller/PathProcessorTest.php +++ b/dev/tests/integration/testsuite/Magento/Webapi/Controller/PathProcessorTest.php @@ -8,6 +8,9 @@ use Magento\Store\Model\Store; +/** + * Test for Magento\Webapi\Controller\PathProcessor class. + */ class PathProcessorTest extends \PHPUnit\Framework\TestCase { /** @@ -15,6 +18,11 @@ class PathProcessorTest extends \PHPUnit\Framework\TestCase */ protected $storeManager; + /** + * @var \Magento\Framework\Locale\ResolverInterface::class + */ + private $localeResolver; + /** * @var \Magento\Webapi\Controller\PathProcessor */ @@ -25,6 +33,7 @@ protected function setUp() $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); $this->storeManager = $objectManager->get(\Magento\Store\Model\StoreManagerInterface::class); $this->storeManager->reinitStores(); + $this->localeResolver = $objectManager->get(\Magento\Framework\Locale\ResolverInterface::class); $this->pathProcessor = $objectManager->get(\Magento\Webapi\Controller\PathProcessor::class); } @@ -59,4 +68,20 @@ public function testProcessWithoutStoreCode() $this->assertEquals($path, $result); $this->assertEquals('default', $this->storeManager->getStore()->getCode()); } + + /** + * @magentoDataFixture Magento/Store/_files/core_fixturestore.php + * @magentoConfigFixture default_store general/locale/code en_US + * @magentoConfigFixture fixturestore_store general/locale/code de_DE + */ + public function testProcessWithValidStoreCodeApplyLocale() + { + $locale = 'de_DE'; + $storeCode = 'fixturestore'; + $basePath = "rest/{$storeCode}"; + $path = $basePath . '/V1/customerAccounts/createCustomer'; + $this->pathProcessor->process($path); + $this->assertEquals($locale, $this->localeResolver->getLocale()); + $this->assertNotEquals('en_US', $this->localeResolver->getLocale()); + } } diff --git a/dev/tests/integration/testsuite/Magento/Widget/Controller/Adminhtml/Widget/InstanceTest.php b/dev/tests/integration/testsuite/Magento/Widget/Controller/Adminhtml/Widget/InstanceTest.php index 5a07bdcfca35..cc6f2793fcfe 100644 --- a/dev/tests/integration/testsuite/Magento/Widget/Controller/Adminhtml/Widget/InstanceTest.php +++ b/dev/tests/integration/testsuite/Magento/Widget/Controller/Adminhtml/Widget/InstanceTest.php @@ -38,6 +38,12 @@ public function testEditAction() public function testBlocksAction() { + \Magento\TestFramework\Helper\Bootstrap::getInstance() + ->loadArea(\Magento\Framework\App\Area::AREA_FRONTEND); + $theme = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + \Magento\Framework\View\DesignInterface::class + )->setDefaultDesignTheme()->getDesignTheme(); + $this->getRequest()->setParam('theme_id', $theme->getId()); $this->dispatch('backend/admin/widget_instance/blocks'); $this->assertStringStartsWith('' + + '' + ), + productIdResolver; + + beforeAll(function (done) { + + injector.mock(mocks); + injector.require( + [ + 'Magento_Catalog/js/product/view/product-ids-resolver' + ], function (resolver) { + productIdResolver = resolver; + done(); + } + ); + }); + + describe('Magento_Catalog/js/product/view/product-ids-resolver', function () { + var dataProvider = [ + { + ids: [], + expected: ['1'] + }, + { + ids: ['2', '3'], + expected: ['2', '3', '1'] + }, + { + ids: ['3', '1', '5'], + expected: ['3', '1', '5'] + } + ]; + + dataProvider.forEach(function (data) { + it('resolved product id\'s', function () { + mocks['Magento_Catalog/js/product/view/product-ids'](data.ids); + expect(productIdResolver(form)).toEqual(data.expected); + }); + }); + }); +}); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/model/cart/estimate-service.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/model/cart/estimate-service.test.js index 3d8325a3ecd5..ce9c98c9d256 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/model/cart/estimate-service.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/model/cart/estimate-service.test.js @@ -150,5 +150,12 @@ define([ }); expect(mocks['Magento_Checkout/js/model/cart/totals-processor/default'].estimateTotals).toHaveBeenCalled(); }); + + it('test subscribe when cart data was changed', function () { + mocks['Magento_Customer/js/customer-data'].get('cart')({ + dataId: 2 + }); + expect(mocks['Magento_Checkout/js/model/cart/totals-processor/default'].estimateTotals).toHaveBeenCalled(); + }); }); }); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/model/error-processor.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/model/error-processor.test.js new file mode 100644 index 000000000000..772250eb7c66 --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/model/error-processor.test.js @@ -0,0 +1,72 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/*eslint max-nested-callbacks: 0*/ +define([ + 'squire' +], function (Squire) { + 'use strict'; + + describe('Magento_Checkout/js/model/error-processor', function () { + var injector = new Squire(), + mocks = { + 'mage/url': { + /** Method stub. */ + build: jasmine.createSpy() + }, + 'Magento_Ui/js/model/messageList': jasmine.createSpy('globalList') + }, + model; + + beforeEach(function (done) { + injector.mock(mocks); + injector.require([ + 'Magento_Checkout/js/model/error-processor' + ], function (processor) { + model = processor; + + done(); + }); + }); + + describe('Check process method', function () { + it('check on success response with valid response data', function () { + var messageObject = { + message: 'Valid error message!' + }, + messageContainer = jasmine.createSpyObj('globalMessageList', ['addErrorMessage']); + + model.process({ + status: 200, + responseText: JSON.stringify(messageObject) + }, messageContainer); + expect(messageContainer.addErrorMessage).toHaveBeenCalledWith(messageObject); + }); + + it('check on success response with invalid response data', function () { + var messageContainer = jasmine.createSpyObj('globalMessageList', ['addErrorMessage']); + + model.process({ + status: 200, + responseText: '' + }, messageContainer); + expect(messageContainer.addErrorMessage) + .toHaveBeenCalledWith('Something went wrong with your request. Please try again later.'); + }); + + it('check on failed status', function () { + var messageContainer = jasmine.createSpyObj('globalMessageList', ['addErrorMessage']); + + spyOn(window.location, 'replace').and.callFake(function () {}); + model.process({ + status: 401, + responseText: '' + }, messageContainer); + expect(mocks['mage/url'].build) + .toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/sidebar.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/sidebar.test.js index 8b4fae5a6946..7c21a8aae2f2 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/sidebar.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/sidebar.test.js @@ -26,15 +26,18 @@ define([ 'items': [ { 'item_id': 1, - 'product_sku': 'bundle' + 'product_sku': 'bundle', + 'product_id': '1' }, { 'item_id': 5, - 'product_sku': 'simple' + 'product_sku': 'simple', + 'product_id': '5' }, { 'item_id': 7, - 'product_sku': 'configurable' + 'product_sku': 'configurable', + 'product_id': '7' } ] }, @@ -63,7 +66,9 @@ define([ sidebar._removeItemAfter(elem); expect(mocks['Magento_Customer/js/customer-data'].get).toHaveBeenCalledWith('cart'); - expect(jQuery('body').trigger).toHaveBeenCalledWith('ajax:removeFromCart', 'simple'); + expect(jQuery('body').trigger).toHaveBeenCalledWith('ajax:removeFromCart', { + 'productIds': ['5'] + }); }); it('Cart item doesn\'t exists', function () { @@ -91,7 +96,7 @@ define([ sidebar._updateItemQtyAfter(elem); expect(mocks['Magento_Customer/js/customer-data'].get).toHaveBeenCalledWith('cart'); - expect(jQuery('body').trigger).toHaveBeenCalledWith('ajax:updateCartItemQty', 'simple'); + expect(jQuery('body').trigger).toHaveBeenCalledWith('ajax:updateCartItemQty'); expect(sidebar._hideItemButton).toHaveBeenCalledWith(elem); }); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/view/shipping.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/view/shipping.test.js index ff420c483047..9b9b1ce5b161 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/view/shipping.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/view/shipping.test.js @@ -12,7 +12,7 @@ require.config({ } }); -define(['squire', 'ko', 'jquery', 'jquery/validate'], function (Squire, ko, $) { +define(['squire', 'ko', 'jquery', 'uiRegistry', 'jquery/validate'], function (Squire, ko, $, registry) { 'use strict'; var injector = new Squire(), @@ -28,7 +28,17 @@ define(['squire', 'ko', 'jquery', 'jquery/validate'], function (Squire, ko, $) { 'Magento_Checkout/js/model/address-converter': jasmine.createSpy(), 'Magento_Checkout/js/model/quote': { isVirtual: jasmine.createSpy(), - shippingMethod: ko.observable() + shippingMethod: ko.observable(), + + /** + * Stub + */ + shippingAddress: function () { + + return { + 'countryId': 'AD' + }; + } }, 'Magento_Checkout/js/action/create-shipping-address': jasmine.createSpy().and.returnValue( jasmine.createSpyObj('newShippingAddress', ['getKey']) @@ -52,7 +62,7 @@ define(['squire', 'ko', 'jquery', 'jquery/validate'], function (Squire, ko, $) { 'checkoutData', ['setSelectedShippingAddress', 'setNewCustomerShippingAddress', 'setSelectedShippingRate'] ), - 'uiRegistry': jasmine.createSpy(), + 'Magento_Ui/js/lib/registry/registry': registry, 'Magento_Checkout/js/model/shipping-rate-service': jasmine.createSpy() }, obj; @@ -63,6 +73,7 @@ define(['squire', 'ko', 'jquery', 'jquery/validate'], function (Squire, ko, $) { obj = new Constr({ provider: 'provName', name: '', + parentName: 'test', index: '', popUpForm: { options: { @@ -156,12 +167,26 @@ define(['squire', 'ko', 'jquery', 'jquery/validate'], function (Squire, ko, $) { describe('"setShippingInformation" method', function () { it('Check method call.', function () { + spyOn(obj, 'validateShippingInformation').and.returnValue(false); expect(obj.setShippingInformation()).toBeUndefined(); }); }); describe('"validateShippingInformation" method', function () { it('Check method call on negative cases.', function () { + var country = { + 'indexedOptions': { + 'AD': + { + label: 'Andorra', + labeltitle: 'Andorra', + value: 'AD' + } + } + }; + + registry.set('test.shippingAddress.shipping-address-fieldset.country_id', country); + registry.set('checkout.errors', {}); obj.source = { get: jasmine.createSpy().and.returnValue(true), set: jasmine.createSpy(), diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/variations.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/variations.test.js index ed2e29f4baf1..6b68978380ea 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/variations.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/variations.test.js @@ -46,8 +46,8 @@ define([ variation.serializeData(); - expect(variation.source.data['configurable-matrix']).toBeUndefined(); - expect(variation.source.data['associated_product_ids']).toBeUndefined(); + expect(variation.source.data['configurable-matrix']).toEqual(matrix); + expect(variation.source.data['associated_product_ids']).toEqual(ids); expect(variation.source.data['configurable-matrix-serialized']).toEqual(resultMatrix); expect(variation.source.data['associated_product_ids_serialized']).toEqual(resultIds); }); @@ -112,10 +112,69 @@ define([ variation.source.data['associated_product_ids_serialized'] = JSON.stringify(['some old data']); variation.serializeData(); - expect(variation.source.data['configurable-matrix']).toBeUndefined(); - expect(variation.source.data['associated_product_ids']).toBeUndefined(); + expect(variation.source.data['configurable-matrix']).toEqual(matrix); + expect(variation.source.data['associated_product_ids']).toEqual(ids); expect(variation.source.data['configurable-matrix-serialized']).toEqual(resultMatrix); expect(variation.source.data['associated_product_ids_serialized']).toEqual(resultIds); }); + + it('checks that "unserializeData" unserializes data', function () { + var matrixString = '[{"name":"Small Product","attributes":"Size: small","price":5.5},' + + '{"name":"Medium Product","attributes":"Size: medium","price":10.99},' + + '{"name":"Large Product","attributes":"Size: large","price":25}]', + idString = '[100, 101, 102]', + resultMatrix = JSON.parse(matrixString), + resultIds = JSON.parse(idString); + + variation.source.data['configurable-matrix-serialized'] = matrixString; + variation.source.data['associated_product_ids_serialized'] = idString; + + variation.unserializeData(); + + expect(variation.source.data['configurable-matrix-serialized']).toBeUndefined(); + expect(variation.source.data['associated_product_ids_serialized']).toBeUndefined(); + expect(variation.source.data['configurable-matrix']).toEqual(resultMatrix); + expect(variation.source.data['associated_product_ids']).toEqual(resultIds); + }); + + it('checks that "serializeData" and "unserializeData" give proper result', function () { + var matrix = [ + { + name: 'Small Product', + attributes: 'Size: small', + price: 5.50 + }, + { + name: 'Medium Product', + attributes: 'Size: medium', + price: 10.99 + }, + { + name: 'Large Product', + attributes: 'Size: large', + price: 25 + } + ], + ids = [1, 2, 3], + resultMatrix = JSON.stringify(matrix), + resultIds = JSON.stringify(ids); + + variation.source.data['configurable-matrix'] = matrix; + variation.source.data['associated_product_ids'] = ids; + + variation.serializeData(); + + expect(variation.source.data['configurable-matrix']).toEqual(matrix); + expect(variation.source.data['associated_product_ids']).toEqual(ids); + expect(variation.source.data['configurable-matrix-serialized']).toEqual(resultMatrix); + expect(variation.source.data['associated_product_ids_serialized']).toEqual(resultIds); + + variation.unserializeData(); + + expect(variation.source.data['configurable-matrix']).toEqual(matrix); + expect(variation.source.data['associated_product_ids']).toEqual(ids); + expect(variation.source.data['configurable-matrix-serialized']).toBeUndefined(); + expect(variation.source.data['associated_product_ids_serialized']).toBeUndefined(); + }); }); }); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Customer/adminhtml/js/view/form/components/form.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Customer/adminhtml/js/view/form/components/form.test.js new file mode 100644 index 000000000000..2ef8736b4f0b --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Customer/adminhtml/js/view/form/components/form.test.js @@ -0,0 +1,71 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'underscore', + 'uiRegistry', + 'Magento_Customer/js/form/components/form', + 'jquery' +], function (_, registry, Constr, $) { + 'use strict'; + + describe('Magento_Customer/js/form/components/form', function () { + + var obj, + originaljQueryAjax; + + beforeEach(function () { + originaljQueryAjax = $.ajax; + obj = new Constr({ + provider: 'provName', + name: '', + index: '' + }); + }); + + afterEach(function () { + $.ajax = originaljQueryAjax; + }); + + registry.set('provName', { + /** Stub */ + on: function () {}, + + /** Stub */ + get: function () {}, + + /** Stub */ + set: function () {} + }); + + describe('"deleteAddress" method', function () { + it('Check for defined ', function () { + expect(obj.hasOwnProperty('deleteAddress')).toBeDefined(); + }); + it('Check method type', function () { + var type = typeof obj.deleteAddress; + + expect(type).toEqual('function'); + }); + it('Check returned value if method called without arguments', function () { + expect(obj.deleteAddress()).toBeUndefined(); + }); + it('Check returned value type if method called without arguments', function () { + var type = typeof obj.deleteAddress(); + + expect(type).toEqual('undefined'); + }); + it('Should call not call ajax if arguments are empty', function () { + $.ajax = jasmine.createSpy(); + + spyOn(obj, 'deleteAddress'); + + expect(obj.deleteAddress()).toBeUndefined(); + + expect($.ajax).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Paypal/frontend/js/view/payment/method-renderer/in-context/checkout-express.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Paypal/frontend/js/view/payment/method-renderer/in-context/checkout-express.test.js index 395dd96822ea..c2a20c8339c7 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Paypal/frontend/js/view/payment/method-renderer/in-context/checkout-express.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Paypal/frontend/js/view/payment/method-renderer/in-context/checkout-express.test.js @@ -25,17 +25,7 @@ define([ 'Magento_Checkout/js/model/payment/additional-validators': { validate: jasmine.createSpy().and.returnValue(true) }, - 'Magento_Ui/js/model/messageList': { - addErrorMessage: jasmine.createSpy(), - addSuccessMessage: jasmine.createSpy() - }, - 'paypalInContextExpressCheckout': { - checkout: { - initXO: jasmine.createSpy(), - closeFlow: jasmine.createSpy(), - startFlow: jasmine.createSpy() - } - }, + 'Magento_Paypal/js/in-context/express-checkout-smart-buttons': jasmine.createSpy(), 'Magento_Customer/js/customer-data': { invalidate: jasmine.createSpy() } @@ -43,6 +33,15 @@ define([ obj; beforeAll(function (done) { + window.checkoutConfig = { + quoteData: { + entityId: 1 + }, + formKey: 'formKey' + }; + window.customerData = { + id: 1 + }; injector.mock(mocks); injector.require( ['Magento_Paypal/js/view/payment/method-renderer/in-context/checkout-express'], @@ -51,9 +50,11 @@ define([ provider: 'provName', name: 'test', index: 'test', - selectPaymentMethod: jasmine.createSpy() + item: { + method: 'payflow_express_bml' + }, + clientConfig: {} }); - done(); }); }); @@ -65,77 +66,11 @@ define([ } catch (e) {} }); - describe('"click" method checks', function () { - it('check success request', function () { - mocks['Magento_Paypal/js/action/set-payment-method'].and.callFake(function () { - var promise = $.Deferred(); - - promise.resolve(); - - return promise; - }); - spyOn(jQuery, 'get').and.callFake(function () { - var promise = $.Deferred(); - - promise.resolve({ - url: 'url' - }); - - return promise; - }); - - obj.clientConfig.click(new Event('event')); - - expect(mocks.paypalInContextExpressCheckout.checkout.initXO).toHaveBeenCalled(); - expect(mocks.paypalInContextExpressCheckout.checkout.startFlow).toHaveBeenCalledWith('url'); - expect(mocks.paypalInContextExpressCheckout.checkout.closeFlow).not.toHaveBeenCalled(); - expect(mocks['Magento_Customer/js/customer-data'].invalidate).toHaveBeenCalled(); - }); - - it('check request with error message', function () { - mocks['Magento_Paypal/js/action/set-payment-method'].and.callFake(function () { - var promise = $.Deferred(); - - promise.resolve(); - - return promise; - }); - spyOn(jQuery, 'get').and.callFake(function () { - var promise = $.Deferred(); - - promise.resolve({ - message: { - text: 'Text', - type: 'error' - } - }); - - return promise; - }); - - obj.clientConfig.click(new Event('event')); - - expect(mocks['Magento_Ui/js/model/messageList'].addErrorMessage).toHaveBeenCalledWith({ - message: 'Text' - }); - expect(mocks.paypalInContextExpressCheckout.checkout.initXO).toHaveBeenCalled(); - expect(mocks.paypalInContextExpressCheckout.checkout.closeFlow).toHaveBeenCalled(); - expect(mocks['Magento_Customer/js/customer-data'].invalidate).toHaveBeenCalled(); - }); - - it('check on fail request', function () { - mocks['Magento_Paypal/js/action/set-payment-method'].and.callFake(function () { - var promise = $.Deferred(); - - promise.reject(); - - return promise; - }); - spyOn(jQuery, 'get'); - - obj.clientConfig.click(new Event('event')); + describe('check smart button initialization', function () { + it('express-checkout-smart-buttons is initialized', function () { - expect(jQuery.get).not.toHaveBeenCalled(); + obj.renderPayPalButtons(); + expect(mocks['Magento_Paypal/js/in-context/express-checkout-smart-buttons']).toHaveBeenCalled(); }); }); }); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Paypal/frontend/js/view/payment/method-renderer/paypal-express-abstract.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Paypal/frontend/js/view/payment/method-renderer/paypal-express-abstract.test.js index f05338ad1e7d..7bc9a2a0113a 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Paypal/frontend/js/view/payment/method-renderer/paypal-express-abstract.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Paypal/frontend/js/view/payment/method-renderer/paypal-express-abstract.test.js @@ -24,12 +24,28 @@ define([ return true; }).and.callThrough(), mocks = { + 'Magento_Checkout/js/model/checkout-data-resolver': { + + /** Stub */ + applyBillingAddress: function () { + return true; + }, + + /** Stub */ + resolveBillingAddress: function () { + return true; + } + }, 'Magento_Checkout/js/model/quote': { billingAddress: ko.observable(), shippingAddress: ko.observable(), paymentMethod: ko.observable(), - totals: ko.observable({}) + totals: ko.observable({}), + /** Stub */ + isVirtual: function () { + return false; + } }, 'Magento_Checkout/js/action/set-payment-information': setPaymentMock, 'Magento_Checkout/js/model/payment/additional-validators': { @@ -61,7 +77,7 @@ define([ name: 'test', index: 'test', item: { - method: 'paypal_express_bml' + method: 'payflow_express_bml' } }); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/adminhtml/js/form/insert.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/adminhtml/js/form/insert.test.js new file mode 100644 index 000000000000..dbe76f0dc269 --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/adminhtml/js/form/insert.test.js @@ -0,0 +1,45 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/* eslint-disable max-nested-callbacks */ +define([ + 'jquery', + 'Magento_Ui/js/form/components/insert' +], function ($, Insert) { + 'use strict'; + + describe('Magento_Ui/js/form/components/insert', function () { + var obj, params; + + beforeEach(function () { + params = { + isRendered: false, + autoRender: false + }; + obj = new Insert(params); + }); + + describe('"onRender" method', function () { + it('Check method call with not JSON response', function () { + var data = ''; + + obj.onRender(data); + + expect(obj.content()).toBe(data); + expect(obj.isRendered).toBeTruthy(); + expect(obj.startRender).toBeFalsy(); + }); + + it('Check method call with ajaxExpired JSON', function () { + var data = '{"ajaxExpired": 1, "ajaxRedirect": "#test"}'; + + obj.onRender(data); + + expect(obj.content()).toBe(''); + expect(window.location.href).toContain('#test'); + }); + }); + }); +}); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/dynamic-rows/dynamic-rows.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/dynamic-rows/dynamic-rows.test.js index 2e238eb99302..fc60fbb0bdcc 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/dynamic-rows/dynamic-rows.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/dynamic-rows/dynamic-rows.test.js @@ -151,12 +151,14 @@ define([ } }], result = [{ + defaultLabelVisible: true, label: 'Label 2', name: 'Name 2', required: false, columnsHeaderClasses: '', sortOrder: 5 }, { + defaultLabelVisible: true, label: 'Label 1', name: 'Name 1', required: false, 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 057b22a75298..b36a075c9a77 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/js/jasmine/tests/app/code/Magento/Ui/base/js/lib/validation/rules.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/lib/validation/rules.test.js index 0703374aaa9d..692c843d18e9 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/lib/validation/rules.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/lib/validation/rules.test.js @@ -15,7 +15,7 @@ define([ var value = '', params = [1,3]; - expect(rules['range-words'].handler(value, params)).toBe(false); + expect(rules['range-words'].handler(value, params)).toBe(true); }); it('Check on redundant words', function () { diff --git a/dev/tests/js/jasmine/tests/lib/mage/collapsible.test.js b/dev/tests/js/jasmine/tests/lib/mage/collapsible.test.js index d6c95d2887ec..1f34e48f8ae9 100644 --- a/dev/tests/js/jasmine/tests/lib/mage/collapsible.test.js +++ b/dev/tests/js/jasmine/tests/lib/mage/collapsible.test.js @@ -26,7 +26,8 @@ define([ describe('Test enable, disable, activate and deactivate methods', function () { var group = $('
'), - content = $('
').appendTo(group); + content = $('
').appendTo(group), + emptyGroup = $('
'); $('
').prependTo(group); @@ -65,6 +66,15 @@ define([ group.collapsible('destroy'); expect(group.is(':mage-collapsible')).toBeFalsy(); }); + + it('check activate method on empty group', function () { + emptyGroup.collapsible(); + expect(emptyGroup.is(':mage-collapsible')).toBeTruthy(); + + expect(function () { + emptyGroup.collapsible('activate'); + }).not.toThrow(); + }); }); it('check if the widget gets expanded/collapsed when the title is clicked', function () { diff --git a/dev/tests/js/jasmine/tests/lib/mage/validation.test.js b/dev/tests/js/jasmine/tests/lib/mage/validation.test.js index 1e1203d22a1e..ca9ec877013f 100644 --- a/dev/tests/js/jasmine/tests/lib/mage/validation.test.js +++ b/dev/tests/js/jasmine/tests/lib/mage/validation.test.js @@ -1142,4 +1142,24 @@ define([ )).toEqual(true); }); }); + + describe('Testing validate-forbidden-extensions', function () { + it('validate-forbidden-extensions', function () { + var el1 = $('').get(0); + + expect($.validator.methods['validate-forbidden-extensions'] + .call($.validator.prototype, 'php', el1, null)).toEqual(false); + expect($.validator.methods['validate-forbidden-extensions'] + .call($.validator.prototype, 'php,phtml', el1, null)).toEqual(false); + expect($.validator.methods['validate-forbidden-extensions'] + .call($.validator.prototype, 'html', el1, null)).toEqual(true); + expect($.validator.methods['validate-forbidden-extensions'] + .call($.validator.prototype, 'html,png', el1, null)).toEqual(true); + expect($.validator.methods['validate-forbidden-extensions'] + .call($.validator.prototype, 'php,html', el1, null)).toEqual(false); + expect($.validator.methods['validate-forbidden-extensions'] + .call($.validator.prototype, 'html,php', el1, null)).toEqual(false); + }); + }); }); diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/etc/db_schema.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/etc/db_schema.xml index 5b8a4c8f37c9..ae6c98e4627d 100644 --- a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/etc/db_schema.xml +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/etc/db_schema.xml @@ -21,7 +21,7 @@ - + @@ -30,7 +30,7 @@ nullable="true"/> - + @@ -53,15 +53,15 @@ - + - - + diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/foreign_key_interpreter_result.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/foreign_key_interpreter_result.php index 76fcf6b2fad1..e3068c1bd762 100644 --- a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/foreign_key_interpreter_result.php +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/foreign_key_interpreter_result.php @@ -33,7 +33,7 @@ 'constraint' => [ 'TEST_TABLE_TINYINT_REFERENCE_TABLE_TINYINT_REF' => [ 'type' => 'foreign', - 'name' => 'TEST_TABLE_TINYINT_REFERENCE_TABLE_TINYINT_REF', + 'referenceId' => 'TEST_TABLE_TINYINT_REFERENCE_TABLE_TINYINT_REF', 'column' => 'tinyint', 'table' => 'test_table', 'referenceTable' => 'reference_table', diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/valid_xml_revision_1.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/valid_xml_revision_1.php index cd42a1e8fc36..a064d096f6d3 100644 --- a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/valid_xml_revision_1.php +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/valid_xml_revision_1.php @@ -93,7 +93,7 @@ 'tinyint_ref' => 'tinyint_ref', ], 'type' => 'primary', - 'name' => 'tinyint_primary', + 'referenceId' => 'tinyint_primary', ], ], 'name' => 'reference_table', @@ -125,7 +125,7 @@ 'int_auto_increment_with_nullable' => 'int_auto_increment_with_nullable', ], 'type' => 'unique', - 'name' => 'AUTO_INCREMENT_TEST_INT_AUTO_INCREMENT_WITH_NULLABLE', + 'referenceId' => 'AUTO_INCREMENT_TEST_INT_AUTO_INCREMENT_WITH_NULLABLE', ], ], 'name' => 'auto_increment_test', @@ -225,17 +225,17 @@ ], ], 'constraint' => [ - 'TEST_TABLE_SMALLINT_BIGINT' => [ + 'TEST_TABLE_UNIQUE' => [ 'column' => [ 'smallint' => 'smallint', 'bigint' => 'bigint', ], 'type' => 'unique', - 'name' => 'TEST_TABLE_SMALLINT_BIGINT', + 'referenceId' => 'TEST_TABLE_UNIQUE', ], - 'TEST_TABLE_TINYINT_REFERENCE_TABLE_TINYINT_REF' => [ + 'TEST_TABLE_TINYINT_REFERENCE' => [ 'type' => 'foreign', - 'name' => 'TEST_TABLE_TINYINT_REFERENCE_TABLE_TINYINT_REF', + 'referenceId' => 'TEST_TABLE_TINYINT_REFERENCE', 'column' => 'tinyint', 'table' => 'test_table', 'referenceTable' => 'reference_table', @@ -244,12 +244,12 @@ ], ], 'index' => [ - 'TEST_TABLE_TINYINT_BIGINT' => [ + 'TEST_TABLE_INDEX' => [ 'column' => [ 'tinyint' => 'tinyint', 'bigint' => 'bigint', ], - 'name' => 'TEST_TABLE_TINYINT_BIGINT', + 'referenceId' => 'TEST_TABLE_INDEX', 'indexType' => 'btree', ], ], diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/base_update/db_schema.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/base_update/db_schema.xml index 7384c4ed7baa..af66686e77b4 100644 --- a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/base_update/db_schema.xml +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/base_update/db_schema.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> - +
@@ -22,14 +22,14 @@ - + - - + diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/before_rollback/db_schema.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/before_rollback/db_schema.xml index 4d3d51b9c023..8c42f9efd129 100644 --- a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/before_rollback/db_schema.xml +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/before_rollback/db_schema.xml @@ -20,7 +20,7 @@ - +
diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/column_modifications/db_schema.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/column_modifications/db_schema.xml index 0adcd5c52f96..4f9a5ab6d5a9 100644 --- a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/column_modifications/db_schema.xml +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/column_modifications/db_schema.xml @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -52,15 +52,15 @@ - + - - + diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/column_removals/db_schema.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/column_removals/db_schema.xml index 4b78f436eab3..a3c4fe010fd3 100644 --- a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/column_removals/db_schema.xml +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/column_removals/db_schema.xml @@ -20,14 +20,14 @@ - + - +
@@ -50,15 +50,15 @@ - + - - + diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/constraint_modifications/db_schema.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/constraint_modifications/db_schema.xml index 68f2810e8eed..93e6e13eb36f 100644 --- a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/constraint_modifications/db_schema.xml +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/constraint_modifications/db_schema.xml @@ -22,11 +22,11 @@ - + - + @@ -35,7 +35,7 @@ nullable="true"/> - + @@ -60,23 +60,23 @@ - + - + - - - - + diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/drop_table/db_schema.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/drop_table/db_schema.xml index 0a0131e50064..8a73d9e22554 100644 --- a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/drop_table/db_schema.xml +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/drop_table/db_schema.xml @@ -12,7 +12,7 @@ nullable="true"/> - + diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/foreign_key_interpreter/db_schema.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/foreign_key_interpreter/db_schema.xml index e4a5ca05d02f..479c4c87f614 100644 --- a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/foreign_key_interpreter/db_schema.xml +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/foreign_key_interpreter/db_schema.xml @@ -12,7 +12,7 @@ -
diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/index_to_disable/db_schema.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/index_to_disable/db_schema.xml new file mode 100644 index 000000000000..e10803af248c --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/index_to_disable/db_schema.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/old_diff/db_schema.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/old_diff/db_schema.xml index 6b92440ca92f..1347a1ba8e3b 100644 --- a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/old_diff/db_schema.xml +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/old_diff/db_schema.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> - +
@@ -22,14 +22,14 @@ - + - - + diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/whitelist_upgrade/db_schema.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/whitelist_upgrade/db_schema.xml new file mode 100644 index 000000000000..90eaf91b1074 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/whitelist_upgrade/db_schema.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/without_setup_version/module.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/without_setup_version/module.xml new file mode 100644 index 000000000000..ed831eb6e935 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/revisions/without_setup_version/module.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule3/etc/db_schema.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule3/etc/db_schema.xml index b6ff634e6ef1..9bd6c438455b 100644 --- a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule3/etc/db_schema.xml +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule3/etc/db_schema.xml @@ -11,7 +11,7 @@ - + @@ -21,10 +21,10 @@ - - + diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule3/revisions/drop_table_with_external_dependency/db_schema.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule3/revisions/drop_table_with_external_dependency/db_schema.xml new file mode 100644 index 000000000000..c22c41b7a5c0 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule3/revisions/drop_table_with_external_dependency/db_schema.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule3/revisions/without_setup_version/module.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule3/revisions/without_setup_version/module.xml new file mode 100644 index 000000000000..27e21dc3fe89 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule3/revisions/without_setup_version/module.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule4/etc/db_schema.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule4/etc/db_schema.xml index 4520cd9e4d40..8082157524ad 100644 --- a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule4/etc/db_schema.xml +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule4/etc/db_schema.xml @@ -11,7 +11,7 @@ - + diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule4/revisions/remove_title_column/db_schema.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule4/revisions/remove_title_column/db_schema.xml index 9d868c016882..0eeff4174870 100644 --- a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule4/revisions/remove_title_column/db_schema.xml +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule4/revisions/remove_title_column/db_schema.xml @@ -10,7 +10,7 @@
- + diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule4/revisions/restore_title_column/db_schema.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule4/revisions/restore_title_column/db_schema.xml index 4520cd9e4d40..8082157524ad 100644 --- a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule4/revisions/restore_title_column/db_schema.xml +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule4/revisions/restore_title_column/db_schema.xml @@ -11,7 +11,7 @@ - + diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule5/etc/db_schema.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule5/etc/db_schema.xml index b6ff634e6ef1..9bd6c438455b 100644 --- a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule5/etc/db_schema.xml +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule5/etc/db_schema.xml @@ -11,7 +11,7 @@ - +
@@ -21,10 +21,10 @@ - - + diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule6/etc/db_schema.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule6/etc/db_schema.xml index 9263004c6e7f..d6be9376cb4d 100644 --- a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule6/etc/db_schema.xml +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule6/etc/db_schema.xml @@ -21,7 +21,7 @@ - + @@ -30,7 +30,7 @@ nullable="true"/> - + @@ -53,15 +53,15 @@ - + - - + diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/disable_index_by_external_module/db_schema.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/disable_index_by_external_module/db_schema.xml new file mode 100644 index 000000000000..f8860731aa80 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/disable_index_by_external_module/db_schema.xml @@ -0,0 +1,13 @@ + + + + + +
+
diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/disable_index_by_external_module/db_schema_whitelist.json b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/disable_index_by_external_module/db_schema_whitelist.json new file mode 100644 index 000000000000..a65bb6aab1c4 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/disable_index_by_external_module/db_schema_whitelist.json @@ -0,0 +1,7 @@ +{ + "test_table": { + "index": { + "TEST_TABLE_VARCHAR": true + } + } +} diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/disable_index_by_external_module/module.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/disable_index_by_external_module/module.xml new file mode 100644 index 000000000000..a0b9bbe483bc --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/disable_index_by_external_module/module.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/disabling_tables/db_schema.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/disabling_tables/db_schema.xml new file mode 100644 index 000000000000..5ea5816b1df8 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/disabling_tables/db_schema.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + +
+ + + + + + +
+
diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/incosistence_reference_definition/db_schema.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/incosistence_reference_definition/db_schema.xml index e39f0a04bcf5..1b7d0dcb93ce 100644 --- a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/incosistence_reference_definition/db_schema.xml +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/incosistence_reference_definition/db_schema.xml @@ -9,13 +9,13 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> - +
-
diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/invalid_primary_key/db_schema.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/invalid_primary_key/db_schema.xml index 4ecee6228641..b42d74aea06a 100644 --- a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/invalid_primary_key/db_schema.xml +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/invalid_primary_key/db_schema.xml @@ -11,7 +11,7 @@ - + diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/setup_install_with_converting/InstallSchema.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/setup_install_with_converting/InstallSchema.php new file mode 100644 index 000000000000..c1eaff264df0 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/setup_install_with_converting/InstallSchema.php @@ -0,0 +1,334 @@ +startSetup(); + + $this->createTables($setup); + + $setup->endSetup(); + } + + /** + * Create tables. + * + * @param SchemaSetupInterface $installer + * @throws \Zend_Db_Exception + */ + private function createTables(SchemaSetupInterface $installer) + { + $mainTableName = $installer->getTable(self::MAIN_TABLE); + $this->dropTableIfExists($installer, $mainTableName); + $mainTable = $installer->getConnection()->newTable($mainTableName); + $mainTable->setComment('Main Test Table for Module8'); + $this->addColumnsToMainTable($mainTable); + $this->addIndexesToMainTable($mainTable); + $installer->getConnection()->createTable($mainTable); + + $secondTableName = $installer->getTable(self::SECOND_TABLE); + $this->dropTableIfExists($installer, $secondTableName); + $secondTable = $installer->getConnection()->newTable($secondTableName); + $secondTable->setComment('Second Test Table for Module8'); + $this->addColumnsToSecondTable($secondTable); + $this->addIndexesToSecondTable($secondTable); + $this->addConstraintsToSecondTable($secondTable); + $installer->getConnection()->createTable($secondTable); + + $this->createSimpleTable($installer, self::TEMP_TABLE); + } + + /** + * Drop existing tables. + * + * @param SchemaSetupInterface $installer + * @param string $table + */ + private function dropTableIfExists($installer, $table) + { + $connection = $installer->getConnection(); + if ($connection->isTableExists($installer->getTable($table))) { + $connection->dropTable( + $installer->getTable($table) + ); + } + } + + /** + * Add tables to main table. + * + * @param Table $table + * @throws \Zend_Db_Exception + */ + private function addColumnsToMainTable($table) + { + $table + ->addColumn( + 'module8_email_contact_id', + Table::TYPE_INTEGER, + 10, + [ + 'primary' => true, + 'identity' => true, + 'unsigned' => true, + 'nullable' => false + ], + 'Email Contact ID' + )->addColumn( + 'module8_contact_group_id', + Table::TYPE_INTEGER, + 10, + [ + 'unsigned' => true, + 'nullable' => false + ], + 'Contact Group ID' + )->addColumn( + 'module8_is_guest', + Table::TYPE_SMALLINT, + null, + [ + 'unsigned' => true, + 'nullable' => true + ], + 'Is Guest' + )->addColumn( + 'module8_contact_id', + Table::TYPE_TEXT, + 15, + [ + 'unsigned' => true, + 'nullable' => true + ], + 'Contact ID' + )->addColumn( + 'module8_content', + Table::TYPE_TEXT, + 15, + [ + 'nullable' => false, + ], + 'Content' + ); + } + + /** + * Add indexes to main table. + * + * @param Table $table + * @throws \Zend_Db_Exception + */ + private function addIndexesToMainTable($table) + { + $table + ->addIndex( + 'MODULE8_INSTALL_INDEX_1', + ['module8_email_contact_id'] + )->addIndex( + 'MODULE8_INSTALL_UNIQUE_INDEX_2', + ['module8_email_contact_id', 'module8_is_guest'], + ['type' => AdapterInterface::INDEX_TYPE_UNIQUE] + )->addIndex( + 'MODULE8_INSTALL_INDEX_3', + ['module8_is_guest'] + )->addIndex( + 'MODULE8_INSTALL_INDEX_4', + ['module8_contact_id'] + )->addIndex( + 'MODULE8_INSTALL_INDEX_TEMP', + ['module8_content'] + )->addIndex( + 'MODULE8_INSTALL_UNIQUE_INDEX_TEMP', + ['module8_contact_group_id'], + ['type' => AdapterInterface::INDEX_TYPE_UNIQUE] + ); + } + + /** + * Add tables to second table. + * + * @param Table $table + * @throws \Zend_Db_Exception + */ + private function addColumnsToSecondTable($table) + { + $table + ->addColumn( + 'module8_entity_id', + Table::TYPE_INTEGER, + 10, + [ + 'primary' => true, + 'identity' => true, + 'unsigned' => true, + 'nullable' => false + ], + 'Entity ID' + )->addColumn( + 'module8_contact_id', + Table::TYPE_INTEGER, + null, + [], + 'Contact ID' + )->addColumn( + 'module8_address', + Table::TYPE_TEXT, + 15, + [ + 'nullable' => false, + ], + 'Address' + )->addColumn( + 'module8_counter_with_multiline_comment', + Table::TYPE_SMALLINT, + null, + [ + 'unsigned' => true, + 'nullable' => true, + 'default' => 0 + ], + 'Empty + Counter + Multiline + Comment' + )->addColumn( + 'module8_second_address', + Table::TYPE_TEXT, + 15, + [ + 'unsigned' => true, + 'nullable' => true + ], + 'Second Address' + )->addColumn( + 'module8_temp_column', + Table::TYPE_TEXT, + 15, + [ + 'unsigned' => true, + 'nullable' => true + ], + 'Temp column for remove' + ); + } + + /** + * Add indexes to second table. + * + * @param Table $table + * @throws \Zend_Db_Exception + */ + private function addIndexesToSecondTable($table) + { + $table + ->addIndex( + 'MODULE8_INSTALL_SECOND_TABLE_INDEX_1', + ['module8_entity_id'] + )->addIndex( + 'MODULE8_INSTALL_SECOND_TABLE_INDEX_2', + ['module8_address'] + )->addIndex( + 'MODULE8_INSTALL_SECOND_TABLE_INDEX_3_TEMP', + ['module8_second_address'] + ); + } + + /** + * Add constraints to second table. + * + * @param Table $table + * @throws \Zend_Db_Exception + */ + private function addConstraintsToSecondTable($table) + { + $table + ->addForeignKey( + 'MODULE8_INSTALL_FK_ENTITY_ID_TEST_MAIN_TABLE_EMAIL_CONTACT_ID', + 'module8_entity_id', + self::MAIN_TABLE, + 'module8_email_contact_id' + )->addForeignKey( + 'MODULE8_INSTALL_FK_ADDRESS_TEST_MAIN_TABLE_CONTACT_ID', + 'module8_address', + self::MAIN_TABLE, + 'module8_contact_id' + )->addForeignKey( + 'MODULE8_INSTALL_FK_ADDRESS_TEST_MAIN_TABLE_MODULE8_CONTENT_TEMP', + 'module8_address', + self::MAIN_TABLE, + 'module8_content' + ); + } + + /** + * Create a simple table. + * + * @param SchemaSetupInterface $setup + * @param $tableName + * @throws \Zend_Db_Exception + */ + private function createSimpleTable(SchemaSetupInterface $setup, $tableName): void + { + $table = $setup->getConnection()->newTable($tableName); + $table + ->addColumn( + 'module8_entity_id', + Table::TYPE_INTEGER, + null, + [ + 'primary' => true, + 'identity' => true, + 'nullable' => false, + 'unsigned' => true, + ], + 'Entity ID' + )->addColumn( + 'module8_counter', + Table::TYPE_INTEGER, + null, + [ + 'unsigned' => true, + 'nullable' => true, + 'default' => 100 + ], + 'Counter' + ); + $setup->getConnection()->createTable($table); + } +} diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/setup_install_with_converting/UpgradeSchema.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/setup_install_with_converting/UpgradeSchema.php new file mode 100644 index 000000000000..2dc8667a75dc --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/setup_install_with_converting/UpgradeSchema.php @@ -0,0 +1,242 @@ +startSetup(); + + if (version_compare($context->getVersion(), '1.0.1', '<')) { + $tableName = $setup->getTable(self::UPDATE_TABLE); + $table = $setup->getConnection()->newTable($tableName); + $table->setComment('Update Test Table for Module8'); + + $this->addColumns($setup, $table); + $this->addIndexes($table); + $this->addConstraints($table); + $setup->getConnection()->createTable($table); + + $this->createSimpleTable($setup, $setup->getTable(self::TEMP_TABLE)); + } + + if (version_compare($context->getVersion(), '1.0.2', '<')) { + $connection = $setup->getConnection(); + $connection + ->dropTable( + InstallSchema::TEMP_TABLE + ); + $connection + ->dropColumn( + InstallSchema::SECOND_TABLE, + 'module8_temp_column' + ); + $connection + ->dropForeignKey( + InstallSchema::SECOND_TABLE, + 'MODULE8_INSTALL_FK_ADDRESS_TEST_MAIN_TABLE_MODULE8_CONTENT_TEMP' + ); + $connection + ->dropIndex( + InstallSchema::MAIN_TABLE, + 'MODULE8_INSTALL_INDEX_TEMP' + ); + $connection + ->dropIndex( + InstallSchema::MAIN_TABLE, + 'MODULE8_INSTALL_UNIQUE_INDEX_TEMP' + ); + } + + $setup->endSetup(); + } + + /** + * Create columns for tables. + * + * @param SchemaSetupInterface $setup + * @param Table $table + * @throws \Zend_Db_Exception + */ + private function addColumns(SchemaSetupInterface $setup, Table $table): void + { + $table + ->addColumn( + 'module8_entity_id', + Table::TYPE_INTEGER, + 10, + [ + 'primary' => true, + 'unsigned' => true, + 'nullable' => false + ], + 'Entity ID' + )->addColumn( + 'module8_entity_row_id', + Table::TYPE_INTEGER, + null, + [ + 'unsigned' => true, + 'nullable' => false + ] + )->addColumn( + 'module8_is_guest', + Table::TYPE_SMALLINT, + null, + [ + 'unsigned' => true, + 'nullable' => true + ], + 'Is Guest' + )->addColumn( + 'module8_guest_browser_id', + Table::TYPE_SMALLINT, + null, + [ + 'unsigned' => true, + 'nullable' => true + ], + 'Guest Browser ID' + )->addColumn( + 'module8_column_for_remove', + Table::TYPE_SMALLINT, + null, + [ + 'unsigned' => true, + 'nullable' => true + ], + 'For remove' + ); + + $setup->getConnection()->addColumn( + InstallSchema::MAIN_TABLE, + 'module8_update_column', + [ + 'type' => Table::TYPE_INTEGER, + 'nullable' => false, + 'comment' => 'Module_8 Update Column', + ] + ); + } + + /** + * Add indexes. + * + * @param Table $table + * @throws \Zend_Db_Exception + */ + private function addIndexes(Table $table): void + { + $table + ->addIndex( + 'MODULE8_UPDATE_IS_GUEST_INDEX', + [ + 'module8_is_guest' + ] + )->addIndex( + 'MODULE8_UPDATE_UNIQUE_INDEX_TEMP', + [ + 'module8_entity_id', + 'module8_is_guest', + + ], + ['type' => AdapterInterface::INDEX_TYPE_UNIQUE] + )->addIndex( + 'MODULE8_UPDATE_TEMP_INDEX', + [ + 'module8_column_for_remove', + 'module8_guest_browser_id' + ] + ); + } + + /** + * Add constraints. + * + * @param Table $table + * @throws \Zend_Db_Exception + */ + private function addConstraints(Table $table): void + { + $table + ->addForeignKey( + 'MODULE8_UPDATE_FK_MODULE8_IS_GUEST', + 'module8_is_guest', + InstallSchema::MAIN_TABLE, + 'module8_is_guest', + Table::ACTION_CASCADE + )->addForeignKey( + 'MODULE8_UPDATE_FK_TEMP', + 'module8_column_for_remove', + InstallSchema::MAIN_TABLE, + 'module8_is_guest', + Table::ACTION_CASCADE + ); + } + + /** + * Create a simple table. + * + * @param SchemaSetupInterface $setup + * @param $tableName + * @throws \Zend_Db_Exception + */ + private function createSimpleTable(SchemaSetupInterface $setup, $tableName): void + { + $table = $setup->getConnection()->newTable($tableName); + $table + ->addColumn( + 'module8_entity_id', + Table::TYPE_INTEGER, + null, + [ + 'primary' => true, + 'identity' => true, + 'nullable' => false, + 'unsigned' => true, + ], + 'Entity ID' + )->addColumn( + 'module8_counter', + Table::TYPE_INTEGER, + null, + [ + 'unsigned' => true, + 'nullable' => true, + 'default' => 100 + ], + 'Counter' + ); + $setup->getConnection()->createTable($table); + } +} diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/setup_install_with_converting/module.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/setup_install_with_converting/module.xml new file mode 100644 index 000000000000..b6a57fcb4763 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/setup_install_with_converting/module.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/unpatterned_fk_name/db_schema.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/unpatterned_fk_name/db_schema.xml new file mode 100644 index 000000000000..eebeb154adf4 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/unpatterned_fk_name/db_schema.xml @@ -0,0 +1,32 @@ + + + + + + + + +
+ + + + + +
+ + + + + + + +
+
diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/whitelist_upgrade/db_schema.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/whitelist_upgrade/db_schema.xml new file mode 100644 index 000000000000..05ce3318ef73 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule8/revisions/whitelist_upgrade/db_schema.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + +
+ + +
+
diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/etc/module.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/etc/module.xml new file mode 100644 index 000000000000..e472f951b08e --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/etc/module.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/fixture/declarative_installer/disabling_tables.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/fixture/declarative_installer/disabling_tables.php new file mode 100644 index 000000000000..633185390ae8 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/fixture/declarative_installer/disabling_tables.php @@ -0,0 +1,14 @@ + 'CREATE TABLE `auto_increment_test` ( + `int_auto_increment_with_nullable` int(12) unsigned NOT NULL AUTO_INCREMENT, + `int_disabled_auto_increment` smallint(12) unsigned DEFAULT \'0\', + UNIQUE KEY `AUTO_INCREMENT_TEST_INT_AUTO_INCREMENT_WITH_NULLABLE` (`int_auto_increment_with_nullable`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8' +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/registration.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/registration.php new file mode 100644 index 000000000000..f021af556bb7 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/registration.php @@ -0,0 +1,13 @@ +getPath(ComponentRegistrar::MODULE, 'Magento_TestSetupDeclarationModule9') === null) { + ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_TestSetupDeclarationModule9', __DIR__); +} diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/disabling_tables/db_schema.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/disabling_tables/db_schema.xml new file mode 100644 index 000000000000..c22c41b7a5c0 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/disabling_tables/db_schema.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/setup_install_with_converting/InstallSchema.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/setup_install_with_converting/InstallSchema.php new file mode 100644 index 000000000000..d78a8e25230b --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/setup_install_with_converting/InstallSchema.php @@ -0,0 +1,134 @@ +startSetup(); + + $this->createTables($setup); + + $setup->endSetup(); + } + + /** + * Create tables. + * + * @param SchemaSetupInterface $installer + * @throws \Zend_Db_Exception + */ + private function createTables(SchemaSetupInterface $installer) + { + $mainTableName = $installer->getTable(self::MAIN_TABLE); + $this->dropTableIfExists($installer, $mainTableName); + $mainTable = $installer->getConnection()->newTable($mainTableName); + $mainTable->setComment('Main Test Table for Module9'); + $this->addColumnsToMainTable($mainTable); + $this->addIndexesToMainTable($mainTable); + $installer->getConnection()->createTable($mainTable); + } + + /** + * Drop existing tables. + * + * @param SchemaSetupInterface $installer + * @param string $table + */ + private function dropTableIfExists($installer, $table) + { + $connection = $installer->getConnection(); + if ($connection->isTableExists($installer->getTable($table))) { + $connection->dropTable( + $installer->getTable($table) + ); + } + } + + /** + * Add tables to main table. + * + * @param Table $table + * @throws \Zend_Db_Exception + */ + private function addColumnsToMainTable($table) + { + $table + ->addColumn( + 'module9_email_contact_id', + Table::TYPE_INTEGER, + 10, + [ + 'primary' => true, + 'identity' => true, + 'unsigned' => true, + 'nullable' => false + ], + 'Entity ID' + )->addColumn( + 'module9_is_guest', + Table::TYPE_SMALLINT, + null, + [ + 'unsigned' => true, + 'nullable' => true + ], + 'Is Guest' + )->addColumn( + 'module9_guest_id', + Table::TYPE_INTEGER, + null, + [ + 'unsigned' => true + ], + 'Guest ID' + ) + ->addColumn( + 'module9_created_at', + Table::TYPE_DATE, + null, + [], + 'Created At' + ); + } + + /** + * Add indexes to main table. + * + * @param Table $table + * @throws \Zend_Db_Exception + */ + private function addIndexesToMainTable($table) + { + $table + ->addIndex( + 'MODULE9_INSTALL_UNIQUE_INDEX_1', + ['module9_email_contact_id', 'module9_guest_id'], + ['type' => AdapterInterface::INDEX_TYPE_UNIQUE] + ); + } +} diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/setup_install_with_converting/UpgradeSchema.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/setup_install_with_converting/UpgradeSchema.php new file mode 100644 index 000000000000..0a4934969996 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/setup_install_with_converting/UpgradeSchema.php @@ -0,0 +1,173 @@ +startSetup(); + + if (version_compare($context->getVersion(), '2.0.0', '<')) { + $this->addColumns($setup); + $this->addIndexes($setup); + $this->addConstraints($setup); + $this->removeColumns($setup); + $this->removeIndexes($setup); + $this->removeConstraints($setup); + $this->removeTables($setup); + $replicaTable = $setup->getConnection() + ->createTableByDdl(Module8InstallSchema::SECOND_TABLE, self::REPLICA_TABLE); + $setup->getConnection()->createTable($replicaTable); + } + + $setup->endSetup(); + } + + /** + * Create columns for tables. + * + * @param SchemaSetupInterface $setup + */ + private function addColumns(SchemaSetupInterface $setup): void + { + $setup->getConnection()->addColumn( + InstallSchema::MAIN_TABLE, + 'module9_update_column', + [ + 'type' => Table::TYPE_INTEGER, + 'nullable' => false, + 'comment' => 'Module_9 Update Column', + ] + ); + + $setup->getConnection()->addColumn( + Module8InstallSchema::MAIN_TABLE, + 'module9_update_column', + [ + 'type' => Table::TYPE_INTEGER, + 'nullable' => false, + 'comment' => 'Module_9 Update Column', + ] + ); + } + + /** + * Add indexes. + * + * @param SchemaSetupInterface $setup + */ + private function addIndexes(SchemaSetupInterface $setup): void + { + $setup->getConnection() + ->addIndex( + Module8UpgradeSchema::UPDATE_TABLE, + 'MODULE9_UPDATE_MODULE8_GUEST_BROWSER_ID', + [ + 'module8_guest_browser_id' + ] + ); + } + + /** + * Add constraints. + * + * @param SchemaSetupInterface $setup + */ + private function addConstraints(SchemaSetupInterface $setup): void + { + $setup->getConnection() + ->addForeignKey( + 'MODULE9_UPDATE_FK_MODULE9_IS_GUEST', + InstallSchema::MAIN_TABLE, + 'module9_is_guest', + Module8InstallSchema::MAIN_TABLE, + 'module8_is_guest', + Table::ACTION_CASCADE + ); + } + + /** + * Remove columns. + * + * @param SchemaSetupInterface $setup + */ + private function removeColumns(SchemaSetupInterface $setup): void + { + $setup->getConnection() + ->dropColumn( + Module8UpgradeSchema::UPDATE_TABLE, + 'module8_column_for_remove' + ); + } + + /** + * Remove indexes. + * + * @param SchemaSetupInterface $setup + */ + private function removeIndexes(SchemaSetupInterface $setup): void + { + $connection = $setup->getConnection(); + $connection + ->dropIndex( + Module8InstallSchema::SECOND_TABLE, + 'MODULE8_INSTALL_SECOND_TABLE_INDEX_3_TEMP' + ); + } + + /** + * Remove constraints. + * + * @param SchemaSetupInterface $setup + */ + private function removeConstraints(SchemaSetupInterface $setup): void + { + $setup->getConnection() + ->dropForeignKey( + Module8InstallSchema::SECOND_TABLE, + 'MODULE8_INSTALL_FK_ADDRESS_TEST_MAIN_TABLE_CONTACT_ID' + )->dropIndex( + Module8UpgradeSchema::UPDATE_TABLE, + 'MODULE8_UPDATE_UNIQUE_INDEX_TEMP' + ); + } + + /** + * Remove tables. + * + * @param SchemaSetupInterface $setup + */ + private function removeTables(SchemaSetupInterface $setup): void + { + $setup->getConnection() + ->dropTable( + Module8UpgradeSchema::TEMP_TABLE + ); + } +} diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/setup_install_with_converting/module.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/setup_install_with_converting/module.xml new file mode 100644 index 000000000000..a553b82d7814 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/revisions/setup_install_with_converting/module.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/dev/tests/setup-integration/etc/install-config-mysql.php.dist b/dev/tests/setup-integration/etc/install-config-mysql.php.dist index c5757b001106..d355100c446b 100644 --- a/dev/tests/setup-integration/etc/install-config-mysql.php.dist +++ b/dev/tests/setup-integration/etc/install-config-mysql.php.dist @@ -17,8 +17,8 @@ return [ 'admin-email' => \Magento\TestFramework\Bootstrap::ADMIN_EMAIL, 'admin-firstname' => \Magento\TestFramework\Bootstrap::ADMIN_FIRSTNAME, 'admin-lastname' => \Magento\TestFramework\Bootstrap::ADMIN_LASTNAME, - 'enable_modules' => 'Magento_TestSetupModule2,Magento_TestSetupModule1,Magento_Backend', - 'disable_modules' => 'all' + 'enable-modules' => 'Magento_TestSetupModule2,Magento_TestSetupModule1,Magento_Backend', + 'disable-modules' => 'all' ], 'checkout' => [ 'host' => '{{db-host}}', diff --git a/dev/tests/setup-integration/framework/tests/unit/phpunit.xml.dist b/dev/tests/setup-integration/framework/tests/unit/phpunit.xml.dist index b52ca37987cb..1a93397caaa4 100644 --- a/dev/tests/setup-integration/framework/tests/unit/phpunit.xml.dist +++ b/dev/tests/setup-integration/framework/tests/unit/phpunit.xml.dist @@ -21,4 +21,32 @@ + + + + var/allure-results + true + + + magentoAdminConfigFixture + + + magentoAppIsolation + + + magentoComponentsDir + + + magentoConfigFixture + + + magentoDataFixture + + + magentoDbIsolation + + + + + diff --git a/dev/tests/setup-integration/phpunit.xml.dist b/dev/tests/setup-integration/phpunit.xml.dist index 7dd8609bdcad..620ebbb5b8f0 100644 --- a/dev/tests/setup-integration/phpunit.xml.dist +++ b/dev/tests/setup-integration/phpunit.xml.dist @@ -43,5 +43,61 @@ + + + var/allure-results + true + + + codingStandardsIgnoreStart + + + codingStandardsIgnoreEnd + + + expectedExceptionMessageRegExp + + + magentoAdminConfigFixture + + + magentoAppArea + + + magentoAppIsolation + + + magentoCache + + + magentoComponentsDir + + + magentoConfigFixture + + + magentoDataFixture + + + magentoDataFixtureBeforeTransaction + + + magentoDbIsolation + + + magentoIndexerDimensionMode + + + moduleName + + + dataProviderFromFile + + + magentoSchemaFixture + + + + diff --git a/dev/tests/setup-integration/testsuite/Magento/Developer/Console/Command/SetupInstallTest.php b/dev/tests/setup-integration/testsuite/Magento/Developer/Console/Command/SetupInstallTest.php new file mode 100644 index 000000000000..cf137233ead0 --- /dev/null +++ b/dev/tests/setup-integration/testsuite/Magento/Developer/Console/Command/SetupInstallTest.php @@ -0,0 +1,126 @@ +cliCommand = $objectManager->get(CliCommand::class); + $this->moduleManager = $objectManager->get(TestModuleManager::class); + $this->componentRegistrar = $objectManager->create( + ComponentRegistrar::class + ); + } + + /** + * @moduleName Magento_TestSetupDeclarationModule8 + * @moduleName Magento_TestSetupDeclarationModule9 + * @throws \Exception + */ + public function testInstallWithConverting() + { + $modules = [ + 'Magento_TestSetupDeclarationModule8', + 'Magento_TestSetupDeclarationModule9', + ]; + + foreach ($modules as $moduleName) { + $this->moduleManager->updateRevision( + $moduleName, + 'setup_install_with_converting', + 'InstallSchema.php', + 'Setup' + ); + $this->moduleManager->updateRevision( + $moduleName, + 'setup_install_with_converting', + 'UpgradeSchema.php', + 'Setup' + ); + + $this->moduleManager->updateRevision( + $moduleName, + 'setup_install_with_converting', + 'module.xml', + 'etc' + ); + } + + $this->cliCommand->install($modules, ['convert-old-scripts' => true]); + + foreach ($modules as $moduleName) { + $modulePath = $this->componentRegistrar->getPath('module', $moduleName); + $schemaFileName = $modulePath + . DIRECTORY_SEPARATOR + . \Magento\Framework\Module\Dir::MODULE_ETC_DIR + . DIRECTORY_SEPARATOR + . 'db_schema.xml'; + $generatedSchema = $this->getSchemaDocument($schemaFileName); + + $expectedSchemaFileName = dirname(__DIR__, 2) + . DIRECTORY_SEPARATOR + . implode( + DIRECTORY_SEPARATOR, + [ + '_files', + 'SetupInstall', + str_replace('Magento_', '', $moduleName), + 'db_schema.xml' + ] + ); + $expectedSchema = $this->getSchemaDocument($expectedSchemaFileName); + + $this->assertEquals($expectedSchema->saveXML(), $generatedSchema->saveXML()); + } + } + + /** + * Convert file content in the DOM document. + * + * @param $schemaFileName + * @return \DOMDocument + */ + private function getSchemaDocument($schemaFileName): \DOMDocument + { + $schemaDocument = new \DOMDocument(); + $schemaDocument->preserveWhiteSpace = false; + $schemaDocument->formatOutput = true; + $schemaDocument->loadXML(file_get_contents($schemaFileName)); + + return $schemaDocument; + } +} diff --git a/dev/tests/setup-integration/testsuite/Magento/Developer/Console/Command/SetupUpgradeTest.php b/dev/tests/setup-integration/testsuite/Magento/Developer/Console/Command/SetupUpgradeTest.php new file mode 100644 index 000000000000..932662b58f3a --- /dev/null +++ b/dev/tests/setup-integration/testsuite/Magento/Developer/Console/Command/SetupUpgradeTest.php @@ -0,0 +1,173 @@ +cliCommand = $objectManager->get(CliCommand::class); + $this->moduleManager = $objectManager->get(TestModuleManager::class); + $this->componentRegistrar = $objectManager->create( + ComponentRegistrar::class + ); + } + + /** + * @moduleName Magento_TestSetupDeclarationModule8 + * @moduleName Magento_TestSetupDeclarationModule9 + * @throws \Exception + */ + public function testUpgradeWithConverting() + { + $modules = [ + 'Magento_TestSetupDeclarationModule8', + 'Magento_TestSetupDeclarationModule9', + ]; + + foreach ($modules as $moduleName) { + $this->moduleManager->updateRevision( + $moduleName, + 'setup_install_with_converting', + 'InstallSchema.php', + 'Setup' + ); + } + + $this->cliCommand->install($modules, ['convert-old-scripts' => true]); + foreach ($modules as $moduleName) { + $this->assertInstallScriptChanges($moduleName); + } + + foreach ($modules as $moduleName) { + $this->moduleManager->updateRevision( + $moduleName, + 'setup_install_with_converting', + 'UpgradeSchema.php', + 'Setup' + ); + + $this->moduleManager->updateRevision( + $moduleName, + 'setup_install_with_converting', + 'module.xml', + 'etc' + ); + } + + $this->cliCommand->upgrade(['convert-old-scripts' => true]); + + foreach ($modules as $moduleName) { + $this->assertUpgradeScriptChanges($moduleName); + } + } + + /** + * Convert file content in the DOM document. + * + * @param string $schemaFileName + * @return \DOMDocument + */ + private function getSchemaDocument(string $schemaFileName): \DOMDocument + { + $schemaDocument = new \DOMDocument(); + $schemaDocument->preserveWhiteSpace = false; + $schemaDocument->formatOutput = true; + $schemaDocument->loadXML(file_get_contents($schemaFileName)); + + return $schemaDocument; + } + + /** + * @param string $moduleName + */ + private function assertInstallScriptChanges(string $moduleName): void + { + $generatedSchema = $this->getGeneratedSchema($moduleName); + $expectedSchema = $this->getSchemaDocument($this->getSchemaFixturePath($moduleName, 'install')); + + $this->assertEquals($expectedSchema->saveXML(), $generatedSchema->saveXML()); + } + + /** + * @param string $moduleName + */ + private function assertUpgradeScriptChanges(string $moduleName): void + { + $generatedSchema = $this->getGeneratedSchema($moduleName); + $expectedSchema = $this->getSchemaDocument($this->getSchemaFixturePath($moduleName, 'upgrade')); + + $this->assertEquals($expectedSchema->saveXML(), $generatedSchema->saveXML()); + } + + /** + * @param string $moduleName + * @return \DOMDocument + */ + private function getGeneratedSchema(string $moduleName): \DOMDocument + { + $modulePath = $this->componentRegistrar->getPath('module', $moduleName); + $schemaFileName = $modulePath + . DIRECTORY_SEPARATOR + . \Magento\Framework\Module\Dir::MODULE_ETC_DIR + . DIRECTORY_SEPARATOR + . 'db_schema.xml'; + + return $this->getSchemaDocument($schemaFileName); + } + + /** + * @param string $moduleName + * @param string $suffix + * @return string + */ + private function getSchemaFixturePath(string $moduleName, string $suffix): string + { + $schemaFixturePath = dirname(__DIR__, 2) + . DIRECTORY_SEPARATOR + . implode( + DIRECTORY_SEPARATOR, + [ + '_files', + 'SetupUpgrade', + str_replace('Magento_', '', $moduleName), + 'db_schema_' . $suffix . '.xml' + ] + ); + + return $schemaFixturePath; + } +} diff --git a/dev/tests/setup-integration/testsuite/Magento/Developer/Console/Command/TablesWhitelistGenerateCommandTest.php b/dev/tests/setup-integration/testsuite/Magento/Developer/Console/Command/TablesWhitelistGenerateCommandTest.php index 77f2d741fc6b..434c99cb9876 100644 --- a/dev/tests/setup-integration/testsuite/Magento/Developer/Console/Command/TablesWhitelistGenerateCommandTest.php +++ b/dev/tests/setup-integration/testsuite/Magento/Developer/Console/Command/TablesWhitelistGenerateCommandTest.php @@ -11,6 +11,7 @@ use Magento\TestFramework\TestCase\SetupTestCase; use Magento\Framework\Console\Cli; use Magento\TestFramework\Deploy\CliCommand; +use Magento\TestFramework\Deploy\TestModuleManager; /** * The purpose of this test is to verify the declaration:generate:whitelist command. @@ -42,6 +43,11 @@ class TablesWhitelistGenerateCommandTest extends SetupTestCase */ private $cliCommand; + /** + * @var TestModuleManager + */ + private $moduleManager; + /** * {@inheritdoc} */ @@ -56,20 +62,43 @@ public function setUp() ); $this->cliCommand = $this->objectManager->get(CliCommand::class); $this->tester = new CommandTester($this->command); + $this->moduleManager = $this->objectManager->get(TestModuleManager::class); } /** - * Execute generate command for whitelist on module Magento_TestSetupDeclarationModule1. - * - * @param array $expectedWhitelistContent + * Execute generate command for whitelist. * * @moduleName Magento_TestSetupDeclarationModule1 - * @dataProvider contentsDataProvider + * @moduleName Magento_TestSetupDeclarationModule8 + * @throws \Exception + */ + public function testExecute() + { + $modules = [ + 'Magento_TestSetupDeclarationModule1', + 'Magento_TestSetupDeclarationModule8', + ]; + + $this->cliCommand->install($modules); + foreach ($modules as $moduleName) { + $this->moduleManager->updateRevision( + $moduleName, + 'whitelist_upgrade', + 'db_schema.xml', + 'etc' + ); + } + + foreach ($modules as $moduleName) { + $this->checkWhitelistFile($moduleName); + } + } + + /** + * @param string $moduleName */ - public function testExecute(array $expectedWhitelistContent) + private function checkWhitelistFile(string $moduleName) { - $moduleName = 'Magento_TestSetupDeclarationModule1'; - $this->cliCommand->install([$moduleName]); $modulePath = $this->componentRegistrar->getPath('module', $moduleName); $whiteListFileName = $modulePath . DIRECTORY_SEPARATOR @@ -79,124 +108,25 @@ public function testExecute(array $expectedWhitelistContent) //run bin/magento declaration:generate:whitelist Magento_TestSetupDeclarationModule1 command. $this->tester->execute(['--module-name' => $moduleName], ['interactive' => false]); - $this->assertSame(Cli::RETURN_SUCCESS, $this->tester->getStatusCode()); $this->assertFileExists($whiteListFileName); $this->assertContains('', $this->tester->getDisplay()); - $whitelistContent = json_decode(file_get_contents($whiteListFileName), true); - $this->assertEquals($expectedWhitelistContent, $whitelistContent); - } - - /** - * Data provider for whitelist contents. - * - * @return array - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function contentsDataProvider(): array - { - return [ - [ - 'content' => [ - 'reference_table' => - [ - 'column' => - [ - 'tinyint_ref' => true, - 'tinyint_without_padding' => true, - 'bigint_without_padding' => true, - 'integer_without_padding' => true, - 'smallint_with_big_padding' => true, - 'smallint_without_default' => true, - 'int_without_unsigned' => true, - 'int_unsigned' => true, - 'bigint_default_nullable' => true, - 'bigint_not_default_not_nullable' => true, - 'smallint_without_padding' => true, - ], - 'constraint' => - [ - 'tinyint_primary' => true, - ], - ], - 'auto_increment_test' => - [ - 'column' => - [ - 'int_auto_increment_with_nullable' => true, - 'int_disabled_auto_increment' => true, - ], - 'constraint' => - [ - 'AUTO_INCREMENT_TEST_INT_AUTO_INCREMENT_WITH_NULLABLE' => true, - ], - ], - 'test_table' => - [ - 'column' => - [ - 'smallint' => true, - 'tinyint' => true, - 'bigint' => true, - 'float' => true, - 'double' => true, - 'decimal' => true, - 'date' => true, - 'timestamp' => true, - 'datetime' => true, - 'longtext' => true, - 'mediumtext' => true, - 'varchar' => true, - 'mediumblob' => true, - 'blob' => true, - 'boolean' => true, - 'varbinary_rename' => true, - ], - 'index' => - [ - 'TEST_TABLE_TINYINT_BIGINT' => true, - ], - 'constraint' => - [ - 'TEST_TABLE_SMALLINT_BIGINT' => true, - 'TEST_TABLE_TINYINT_REFERENCE_TABLE_TINYINT_REF' => true, - ], - ], - 'store' => - [ - 'column' => - [ - 'store_owner_id' => true, - 'store_owner' => true, - ], - 'constraint' => - [ - 'STORE_STORE_OWNER_ID_STORE_OWNER_OWNER_ID' => true, - ], - ], - 'store_owner' => - [ - 'column' => - [ - 'owner_id' => true, - 'label' => true, - ], - 'constraint' => - [ - '' => true, - ], - ], - 'some_table' => - [ - 'column' => - [ - 'some_column' => true, - ], - ], - ], - ], - ]; + $whitelistFileContent = file_get_contents($whiteListFileName); + $expectedWhitelistContent = file_get_contents( + dirname(__DIR__, 2) + . DIRECTORY_SEPARATOR + . implode( + DIRECTORY_SEPARATOR, + [ + '_files', + 'WhitelistGenerate', + str_replace('Magento_', '', $moduleName), + 'db_schema_whitelist.json' + ] + ) + ); + $this->assertEquals($expectedWhitelistContent, $whitelistFileContent); } } diff --git a/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupInstall/TestSetupDeclarationModule8/db_schema.xml b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupInstall/TestSetupDeclarationModule8/db_schema.xml new file mode 100644 index 000000000000..cdc71980bf50 --- /dev/null +++ b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupInstall/TestSetupDeclarationModule8/db_schema.xml @@ -0,0 +1,114 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + +
+
diff --git a/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupInstall/TestSetupDeclarationModule9/db_schema.xml b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupInstall/TestSetupDeclarationModule9/db_schema.xml new file mode 100644 index 000000000000..3ded03c9e79f --- /dev/null +++ b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupInstall/TestSetupDeclarationModule9/db_schema.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + + +
+ + + +
+ +
+ + + + + + + + + + + + + + + +
+
diff --git a/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule8/db_schema_install.xml b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule8/db_schema_install.xml new file mode 100644 index 000000000000..2da9901cf962 --- /dev/null +++ b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule8/db_schema_install.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + +
+
diff --git a/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule8/db_schema_upgrade.xml b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule8/db_schema_upgrade.xml new file mode 100644 index 000000000000..6deed3105f29 --- /dev/null +++ b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule8/db_schema_upgrade.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + +
+
diff --git a/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule9/db_schema_install.xml b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule9/db_schema_install.xml new file mode 100644 index 000000000000..2ac2cc607f0d --- /dev/null +++ b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule9/db_schema_install.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + +
+
diff --git a/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule9/db_schema_upgrade.xml b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule9/db_schema_upgrade.xml new file mode 100644 index 000000000000..b522224ca07b --- /dev/null +++ b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/SetupUpgrade/TestSetupDeclarationModule9/db_schema_upgrade.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + + +
+ + + +
+ +
+ + + + + + + + + + + + + + + + + + + + +
+
diff --git a/dev/tests/setup-integration/testsuite/Magento/Developer/_files/WhitelistGenerate/TestSetupDeclarationModule1/db_schema_whitelist.json b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/WhitelistGenerate/TestSetupDeclarationModule1/db_schema_whitelist.json new file mode 100644 index 000000000000..55005b82ab8b --- /dev/null +++ b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/WhitelistGenerate/TestSetupDeclarationModule1/db_schema_whitelist.json @@ -0,0 +1,80 @@ +{ + "reference_table": { + "column": { + "tinyint_ref": true, + "tinyint_without_padding": true, + "bigint_without_padding": true, + "integer_without_padding": true, + "smallint_with_big_padding": true, + "smallint_without_default": true, + "int_without_unsigned": true, + "int_unsigned": true, + "bigint_default_nullable": true, + "bigint_not_default_not_nullable": true, + "smallint_without_padding": true + }, + "constraint": { + "tinyint_primary": true, + "PRIMARY": true + } + }, + "auto_increment_test": { + "column": { + "int_auto_increment_with_nullable": true, + "int_disabled_auto_increment": true + }, + "constraint": { + "AUTO_INCREMENT_TEST_INT_AUTO_INCREMENT_WITH_NULLABLE": true + } + }, + "test_table": { + "column": { + "smallint": true, + "tinyint": true, + "bigint": true, + "float": true, + "double": true, + "decimal": true, + "date": true, + "timestamp": true, + "datetime": true, + "longtext": true, + "mediumtext": true, + "varchar": true, + "mediumblob": true, + "blob": true, + "boolean": true, + "varbinary_rename": true + }, + "index": { + "TEST_TABLE_TINYINT_BIGINT": true + }, + "constraint": { + "TEST_TABLE_SMALLINT_BIGINT": true, + "TEST_TABLE_TINYINT_REFERENCE_TABLE_TINYINT_REF": true + } + }, + "store": { + "column": { + "store_owner_id": true, + "store_owner": true + }, + "constraint": { + "STORE_STORE_OWNER_ID_STORE_OWNER_OWNER_ID": true + } + }, + "store_owner": { + "column": { + "owner_id": true, + "label": true + }, + "constraint": { + "": true + } + }, + "some_table": { + "column": { + "some_column": true + } + } +} \ No newline at end of file diff --git a/dev/tests/setup-integration/testsuite/Magento/Developer/_files/WhitelistGenerate/TestSetupDeclarationModule8/db_schema_whitelist.json b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/WhitelistGenerate/TestSetupDeclarationModule8/db_schema_whitelist.json new file mode 100644 index 000000000000..b4209edfe471 --- /dev/null +++ b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/WhitelistGenerate/TestSetupDeclarationModule8/db_schema_whitelist.json @@ -0,0 +1,20 @@ +{ + "reference_test": { + "column": { + "entity_id": true, + "product_id": true + }, + "index": { + "REFERENCE_TEST_ENTITY_ID": true + }, + "constraint": { + "REFERENCE_TEST_ENTITY_ID_PRODUCT_ID": true, + "REFERENCE_TEST_ENTITY_ID_TEST_TABLE_SMALLINT": true + } + }, + "auto_increment_test": { + "column": { + "int_counter": true + } + } +} \ No newline at end of file diff --git a/dev/tests/setup-integration/testsuite/Magento/Setup/DeclarativeInstallerTest.php b/dev/tests/setup-integration/testsuite/Magento/Setup/DeclarativeInstallerTest.php index 34d432e566fe..e8698965de00 100644 --- a/dev/tests/setup-integration/testsuite/Magento/Setup/DeclarativeInstallerTest.php +++ b/dev/tests/setup-integration/testsuite/Magento/Setup/DeclarativeInstallerTest.php @@ -29,7 +29,7 @@ class DeclarativeInstallerTest extends SetupTestCase /** * @var CliCommand */ - private $cliCommad; + private $cliCommand; /** * @var SchemaDiff @@ -51,11 +51,14 @@ class DeclarativeInstallerTest extends SetupTestCase */ private $describeTable; + /** + * @inheritdoc + */ public function setUp() { $objectManager = Bootstrap::getObjectManager(); $this->moduleManager = $objectManager->get(TestModuleManager::class); - $this->cliCommad = $objectManager->get(CliCommand::class); + $this->cliCommand = $objectManager->get(CliCommand::class); $this->describeTable = $objectManager->get(DescribeTable::class); $this->schemaDiff = $objectManager->get(SchemaDiff::class); $this->schemaConfig = $objectManager->get(SchemaConfigInterface::class); @@ -68,7 +71,7 @@ public function setUp() */ public function testInstallation() { - $this->cliCommad->install( + $this->cliCommand->install( ['Magento_TestSetupDeclarationModule1'] ); @@ -79,17 +82,36 @@ public function testInstallation() //Second time installation should not find anything as we do not change anything self::assertNull($diff->getAll()); + $this->compareStructures(); + } + + /** + * Compare structure of DB and declared structure. + */ + private function compareStructures() + { $shardData = $this->describeTable->describeShard(Sharding::DEFAULT_CONNECTION); - self::assertEquals($this->getTrimmedData(), $shardData); + foreach ($this->getTrimmedData() as $tableName => $sql) { + $this->assertArrayHasKey($tableName, $shardData); + /** + * MySQL 8.0 and above does not provide information about the ON DELETE instruction + * if ON DELETE NO ACTION + */ + if (preg_match('#ON DELETE\s+NO ACTION#i', $shardData[$tableName] === 1)) { + preg_replace('#ON DELETE\s+NO ACTION#i', '', $sql); + self::assertEquals($sql, $shardData[$tableName]); + } + } } /** * @moduleName Magento_TestSetupDeclarationModule1 * @dataProviderFromFile Magento/TestSetupDeclarationModule1/fixture/declarative_installer/column_modification.php + * @throws \Exception */ public function testInstallationWithColumnsModification() { - $this->cliCommad->install( + $this->cliCommand->install( ['Magento_TestSetupDeclarationModule1'] ); @@ -101,7 +123,7 @@ public function testInstallationWithColumnsModification() 'etc' ); - $this->cliCommad->install( + $this->cliCommand->install( ['Magento_TestSetupDeclarationModule1'] ); @@ -110,8 +132,7 @@ public function testInstallationWithColumnsModification() $this->schemaConfig->getDbConfig() ); self::assertNull($diff->getAll()); - $shardData = $this->describeTable->describeShard(Sharding::DEFAULT_CONNECTION); - self::assertEquals($this->getTrimmedData(), $shardData); + $this->compareStructures(); } /** @@ -140,14 +161,15 @@ private function updateDbSchemaRevision($revisionName) /** * @moduleName Magento_TestSetupDeclarationModule1 * @dataProviderFromFile Magento/TestSetupDeclarationModule1/fixture/declarative_installer/column_removal.php + * @throws \Exception */ public function testInstallationWithColumnsRemoval() { - $this->cliCommad->install( + $this->cliCommand->install( ['Magento_TestSetupDeclarationModule1'] ); $this->updateDbSchemaRevision('column_removals'); - $this->cliCommad->install( + $this->cliCommand->install( ['Magento_TestSetupDeclarationModule1'] ); @@ -156,8 +178,7 @@ public function testInstallationWithColumnsRemoval() $this->schemaConfig->getDbConfig() ); self::assertNull($diff->getAll()); - $shardData = $this->describeTable->describeShard(Sharding::DEFAULT_CONNECTION); - self::assertEquals($this->getTrimmedData(), $shardData); + $this->compareStructures(); } /** @@ -179,14 +200,15 @@ private function getTrimmedData() /** * @moduleName Magento_TestSetupDeclarationModule1 * @dataProviderFromFile Magento/TestSetupDeclarationModule1/fixture/declarative_installer/constraint_modification.php + * @throws \Exception */ public function testInstallationWithConstraintsModification() { - $this->cliCommad->install( + $this->cliCommand->install( ['Magento_TestSetupDeclarationModule1'] ); $this->updateDbSchemaRevision('constraint_modifications'); - $this->cliCommad->upgrade(); + $this->cliCommand->upgrade(); $diff = $this->schemaDiff->diff( $this->schemaConfig->getDeclarationConfig(), @@ -200,10 +222,11 @@ public function testInstallationWithConstraintsModification() /** * @moduleName Magento_TestSetupDeclarationModule1 * @dataProviderFromFile Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_removal.php + * @throws \Exception */ public function testInstallationWithDroppingTables() { - $this->cliCommad->install( + $this->cliCommand->install( ['Magento_TestSetupDeclarationModule1'] ); @@ -215,7 +238,7 @@ public function testInstallationWithDroppingTables() 'etc' ); - $this->cliCommad->upgrade(); + $this->cliCommand->upgrade(); $diff = $this->schemaDiff->diff( $this->schemaConfig->getDeclarationConfig(), @@ -226,9 +249,49 @@ public function testInstallationWithDroppingTables() self::assertEquals($this->getData(), $shardData); } + /** + * @moduleName Magento_TestSetupDeclarationModule1 + * @moduleName Magento_TestSetupDeclarationModule3 + */ + public function testInstallationWithDroppingTablesFromSecondaryModule() + { + $modules = [ + 'Magento_TestSetupDeclarationModule1', + 'Magento_TestSetupDeclarationModule3', + ]; + + $this->moduleManager->updateRevision( + 'Magento_TestSetupDeclarationModule3', + 'drop_table_with_external_dependency', + 'db_schema.xml', + 'etc' + ); + + foreach ($modules as $moduleName) { + $this->moduleManager->updateRevision( + $moduleName, + 'without_setup_version', + 'module.xml', + 'etc' + ); + } + + try { + $this->cliCommand->install($modules); + } catch (\Exception $e) { + $installException = $e->getPrevious(); + self::assertSame(1, $installException->getCode()); + self::assertContains( + 'The reference table named "reference_table" is disabled', + $installException->getMessage() + ); + } + } + /** * @moduleName Magento_TestSetupDeclarationModule1 * @dataProviderFromFile Magento/TestSetupDeclarationModule1/fixture/declarative_installer/rollback.php + * @throws \Exception */ public function testInstallWithCodeBaseRollback() { @@ -239,7 +302,7 @@ public function testInstallWithCodeBaseRollback() 'db_schema.xml', 'etc' ); - $this->cliCommad->install( + $this->cliCommand->install( ['Magento_TestSetupDeclarationModule1'] ); $beforeRollback = $this->describeTable->describeShard('default'); @@ -252,7 +315,7 @@ public function testInstallWithCodeBaseRollback() 'etc' ); - $this->cliCommad->upgrade(); + $this->cliCommand->upgrade(); $afterRollback = $this->describeTable->describeShard('default'); self::assertEquals($this->getData()['after'], $afterRollback); } @@ -260,6 +323,7 @@ public function testInstallWithCodeBaseRollback() /** * @moduleName Magento_TestSetupDeclarationModule1 * @dataProviderFromFile Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_rename.php + * @throws \Exception */ public function testTableRename() { @@ -271,7 +335,7 @@ public function testTableRename() 'db_schema.xml', 'etc' ); - $this->cliCommad->install( + $this->cliCommand->install( ['Magento_TestSetupDeclarationModule1'] ); $before = $this->describeTable->describeShard('default'); @@ -289,11 +353,109 @@ public function testTableRename() 'etc' ); - $this->cliCommad->upgrade(); + $this->cliCommand->upgrade(); $after = $this->describeTable->describeShard('default'); self::assertEquals($this->getData()['after'], $after['some_table_renamed']); $select = $adapter->select() ->from($this->resourceConnection->getTableName('some_table_renamed')); self::assertEquals([$dataToMigrate], $adapter->fetchAll($select)); } + + /** + * @moduleName Magento_TestSetupDeclarationModule8 + * @throws \Exception + */ + public function testForeignKeyReferenceId() + { + $this->cliCommand->install( + ['Magento_TestSetupDeclarationModule8'] + ); + $this->moduleManager->updateRevision( + 'Magento_TestSetupDeclarationModule8', + 'unpatterned_fk_name', + 'db_schema.xml', + 'etc' + ); + $this->cliCommand->upgrade(); + $tableStatements = $this->describeTable->describeShard('default'); + $tableSql = $tableStatements['dependent']; + $this->assertRegExp('/CONSTRAINT\s`DEPENDENT_PAGE_ID_ON_TEST_TABLE_PAGE_ID`/', $tableSql); + $this->assertRegExp('/CONSTRAINT\s`DEPENDENT_SCOPE_ID_ON_TEST_SCOPE_TABLE_SCOPE_ID`/', $tableSql); + } + + /** + * @moduleName Magento_TestSetupDeclarationModule1 + * @moduleName Magento_TestSetupDeclarationModule8 + * @throws \Exception + */ + public function testDisableIndexByExternalModule() + { + $this->cliCommand->install( + ['Magento_TestSetupDeclarationModule1', 'Magento_TestSetupDeclarationModule8'] + ); + $this->moduleManager->updateRevision( + 'Magento_TestSetupDeclarationModule1', + 'index_to_disable', + 'db_schema.xml', + 'etc' + ); + $this->moduleManager->updateRevision( + 'Magento_TestSetupDeclarationModule8', + 'disable_index_by_external_module', + 'db_schema.xml', + 'etc' + ); + $this->moduleManager->updateRevision( + 'Magento_TestSetupDeclarationModule8', + 'disable_index_by_external_module', + 'db_schema_whitelist.json', + 'etc' + ); + $this->moduleManager->updateRevision( + 'Magento_TestSetupDeclarationModule8', + 'disable_index_by_external_module', + 'module.xml', + 'etc' + ); + $this->cliCommand->upgrade(); + $tableStatements = $this->describeTable->describeShard('default'); + $tableSql = $tableStatements['test_table']; + $this->assertNotRegExp( + '/KEY\s+`TEST_TABLE_VARCHAR`\s+\(`varchar`\)/', + $tableSql, + 'Index is not being disabled by external module' + ); + } + + /** + * @moduleName Magento_TestSetupDeclarationModule8 + * @moduleName Magento_TestSetupDeclarationModule9 + * @dataProviderFromFile Magento/TestSetupDeclarationModule9/fixture/declarative_installer/disabling_tables.php + * @throws \Exception + */ + public function testInstallationWithDisablingTables() + { + $modules = [ + 'Magento_TestSetupDeclarationModule8', + 'Magento_TestSetupDeclarationModule9', + ]; + + foreach ($modules as $moduleName) { + $this->moduleManager->updateRevision( + $moduleName, + 'disabling_tables', + 'db_schema.xml', + 'etc' + ); + } + $this->cliCommand->install($modules); + + $diff = $this->schemaDiff->diff( + $this->schemaConfig->getDeclarationConfig(), + $this->schemaConfig->getDbConfig() + ); + self::assertNull($diff->getAll()); + $shardData = $this->describeTable->describeShard(Sharding::DEFAULT_CONNECTION); + self::assertEquals($this->getData(), $shardData); + } } diff --git a/dev/tests/static/framework/Magento/CodeMessDetector/Rule/Design/AllPurposeAction.php b/dev/tests/static/framework/Magento/CodeMessDetector/Rule/Design/AllPurposeAction.php new file mode 100644 index 000000000000..fb5938be6132 --- /dev/null +++ b/dev/tests/static/framework/Magento/CodeMessDetector/Rule/Design/AllPurposeAction.php @@ -0,0 +1,50 @@ +getFullQualifiedName(), true); + } catch (\Throwable $exception) { + //Couldn't load a class. + return; + } + + if (in_array(ActionInterface::class, $impl, true)) { + $methodsDefined = false; + foreach ($impl as $i) { + if (preg_match('/\\\Http[a-z]+ActionInterface$/i', $i)) { + $methodsDefined = true; + break; + } + } + if (!$methodsDefined) { + $this->addViolation($node, [$node->getFullQualifiedName()]); + } + } + } +} diff --git a/dev/tests/static/framework/Magento/CodeMessDetector/Rule/Design/CookieAndSessionMisuse.php b/dev/tests/static/framework/Magento/CodeMessDetector/Rule/Design/CookieAndSessionMisuse.php new file mode 100644 index 000000000000..707c3442d405 --- /dev/null +++ b/dev/tests/static/framework/Magento/CodeMessDetector/Rule/Design/CookieAndSessionMisuse.php @@ -0,0 +1,199 @@ +isSubclassOf(\Magento\Framework\App\ActionInterface::class); + } + + /** + * Is given class a block? + * + * @param \ReflectionClass $class + * @return bool + */ + private function isBlock(\ReflectionClass $class): bool + { + return $class->isSubclassOf(\Magento\Framework\View\Element\BlockInterface::class); + } + + /** + * Is given class an HTML UI data provider? + * + * @param \ReflectionClass $class + * @return bool + */ + private function isUiDataProvider(\ReflectionClass $class): bool + { + return $class->isSubclassOf( + \Magento\Framework\View\Element\UiComponent\DataProvider\DataProviderInterface::class + ); + } + + /** + * Is given class a Layout Processor? + * + * @param \ReflectionClass $class + * @return bool + */ + private function isLayoutProcessor(\ReflectionClass $class): bool + { + return $class->isSubclassOf( + \Magento\Checkout\Block\Checkout\LayoutProcessorInterface::class + ); + } + + /** + * Is given class an HTML UI Document? + * + * @param \ReflectionClass $class + * @return bool + */ + private function isUiDocument(\ReflectionClass $class): bool + { + return $class->isSubclassOf(\Magento\Framework\View\Element\UiComponent\DataProvider\Document::class) + || $class->getName() === \Magento\Framework\View\Element\UiComponent\DataProvider\Document::class; + } + + /** + * Is given class a plugin for controllers? + * + * @param \ReflectionClass $class + * @return bool + */ + private function isControllerPlugin(\ReflectionClass $class): bool + { + foreach ($class->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { + if (preg_match('/^(after|around|before).+/i', $method->getName())) { + try { + $argument = $method->getParameters()[0]->getClass(); + } catch (\Throwable $exception) { + //Non-existing class (autogenerated perhaps) or doesn't have an argument. + continue; + } + if ($argument) { + $isAction = $argument->isSubclassOf(\Magento\Framework\App\ActionInterface::class) + || $argument->getName() === \Magento\Framework\App\ActionInterface::class; + if ($isAction) { + return true; + } + } + } + } + + return false; + } + + /** + * Is given class a plugin for blocks? + * + * @param \ReflectionClass $class + * @return bool + */ + private function isBlockPlugin(\ReflectionClass $class): bool + { + foreach ($class->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { + if (preg_match('/^(after|around|before).+/i', $method->getName())) { + try { + $argument = $method->getParameters()[0]->getClass(); + } catch (\Throwable $exception) { + //Non-existing class (autogenerated perhaps) or doesn't have an argument. + continue; + } + if ($argument) { + $isBlock = $argument->isSubclassOf(\Magento\Framework\View\Element\BlockInterface::class) + || $argument->getName() === \Magento\Framework\View\Element\BlockInterface::class; + if ($isBlock) { + return true; + } + } + } + } + + return false; + } + + /** + * Whether given class depends on classes to pay attention to. + * + * @param \ReflectionClass $class + * @return bool + */ + private function doesUseRestrictedClasses(\ReflectionClass $class): bool + { + $constructor = $class->getConstructor(); + if ($constructor) { + foreach ($constructor->getParameters() as $argument) { + try { + if ($class = $argument->getClass()) { + if ($class->isSubclassOf(\Magento\Framework\Session\SessionManagerInterface::class) + || $class->getName() === \Magento\Framework\Session\SessionManagerInterface::class + || $class->isSubclassOf(\Magento\Framework\Stdlib\Cookie\CookieReaderInterface::class) + || $class->getName() === \Magento\Framework\Stdlib\Cookie\CookieReaderInterface::class + ) { + return true; + } + } + } catch (\ReflectionException $exception) { + //Failed to load the argument's class information + continue; + } + } + } + + return false; + } + + /** + * @inheritdoc + * + * @param ClassNode|ASTClass $node + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + public function apply(AbstractNode $node) + { + try { + $class = new \ReflectionClass($node->getFullQualifiedName()); + } catch (\Throwable $exception) { + //Failed to load class, nothing we can do + return; + } + + if ($this->doesUseRestrictedClasses($class)) { + if (!$this->isController($class) + && !$this->isBlock($class) + && !$this->isUiDataProvider($class) + && !$this->isUiDocument($class) + && !$this->isControllerPlugin($class) + && !$this->isBlockPlugin($class) + && !$this->isLayoutProcessor($class) + ) { + $this->addViolation($node, [$node->getFullQualifiedName()]); + } + } + } +} diff --git a/dev/tests/static/framework/Magento/CodeMessDetector/Rule/Design/FinalImplementation.php b/dev/tests/static/framework/Magento/CodeMessDetector/Rule/Design/FinalImplementation.php deleted file mode 100644 index 22c008b05806..000000000000 --- a/dev/tests/static/framework/Magento/CodeMessDetector/Rule/Design/FinalImplementation.php +++ /dev/null @@ -1,29 +0,0 @@ -isFinal()) { - $this->addViolation($node, [$node->getType(), $node->getFullQualifiedName()]); - } - } -} diff --git a/dev/tests/static/framework/Magento/CodeMessDetector/Rule/Design/SerializationAware.php b/dev/tests/static/framework/Magento/CodeMessDetector/Rule/Design/SerializationAware.php new file mode 100644 index 000000000000..e38fba8558ba --- /dev/null +++ b/dev/tests/static/framework/Magento/CodeMessDetector/Rule/Design/SerializationAware.php @@ -0,0 +1,34 @@ +getName() === '__wakeup' || $method->getName() === '__sleep') { + $this->addViolation($method, [$method->getName(), $method->getParent()->getFullQualifiedName()]); + } + } +} diff --git a/dev/tests/static/framework/Magento/CodeMessDetector/Test/Unit/Rule/Design/AllPurposeActionTest.php b/dev/tests/static/framework/Magento/CodeMessDetector/Test/Unit/Rule/Design/AllPurposeActionTest.php new file mode 100644 index 000000000000..408853141e53 --- /dev/null +++ b/dev/tests/static/framework/Magento/CodeMessDetector/Test/Unit/Rule/Design/AllPurposeActionTest.php @@ -0,0 +1,134 @@ +createNodeMock($fakeAction); + $rule = new AllPurposeAction(); + $this->expectsRuleViolation($rule, $violates); + $rule->apply($node); + } + + /** + * @return array + */ + public function getCases(): array + { + return [ + [ + new class implements ActionInterface, HttpGetActionInterface { + /** + * @inheritDoc + */ + public function execute() + { + return null; + } + }, + false + ], + [ + new class implements ActionInterface { + /** + * @inheritDoc + */ + public function execute() + { + return null; + } + }, + true + ], + [ + new class implements HttpGetActionInterface { + /** + * @inheritDoc + */ + public function execute() + { + return null; + } + }, + false + ], + [ + new class { + + }, + false + ] + ]; + } + + /** + * @param object $dynamicObject + * + * @return ClassNode|MockObject + */ + private function createNodeMock($dynamicObject): MockObject + { + $node = $this->getMockBuilder(ClassNode::class) + ->disableOriginalConstructor() + ->disableProxyingToOriginalMethods() + ->setMethods([ + // disable name lookup from AST artifact + 'getNamespaceName', + 'getParentName', + 'getName', + 'getFullQualifiedName', + ]) + ->getMock(); + $node->expects($this->any()) + ->method('getFullQualifiedName') + ->willReturn(get_class($dynamicObject)); + + return $node; + } + + /** + * @param AllPurposeAction $rule + * @param bool $expects + * @return InvocationMocker + */ + private function expectsRuleViolation( + AllPurposeAction $rule, + bool $expects + ): InvocationMocker { + /** @var Report|MockObject $report */ + $report = $this->getMockBuilder(Report::class)->getMock(); + if ($expects) { + $violationExpectation = $this->atLeastOnce(); + } else { + $violationExpectation = $this->never(); + } + $invokation = $report->expects($violationExpectation) + ->method('addRuleViolation'); + $rule->setReport($report); + + return $invokation; + } +} diff --git a/dev/tests/static/framework/Magento/CodeMessDetector/Test/Unit/Rule/Design/FinalImplementationTest.php b/dev/tests/static/framework/Magento/CodeMessDetector/Test/Unit/Rule/Design/FinalImplementationTest.php deleted file mode 100644 index ca944b5c60e1..000000000000 --- a/dev/tests/static/framework/Magento/CodeMessDetector/Test/Unit/Rule/Design/FinalImplementationTest.php +++ /dev/null @@ -1,128 +0,0 @@ -createFinalizableNodeMock($nodeType); - $finalizableNode->method('isFinal')->willReturn(false); - - $rule = new FinalImplementation(); - $this->expectsRuleViolation($rule, $this->never()); - $rule->apply($finalizableNode); - } - - /** - * @param string $nodeType - * - * @dataProvider finalizableNodeTypesProvider - */ - public function testRuleAppliesToFinalFinalizable($nodeType) - { - $finalizableNode = $this->createFinalizableNodeMock($nodeType); - $finalizableNode->method('isFinal')->willReturn(true); - - $rule = new FinalImplementation(); - $this->expectsRuleViolation($rule, $this->once()); - $rule->apply($finalizableNode); - } - - /** - * @param string $nodeType - * - * @dataProvider finalizableNodeTypesProvider - */ - public function testRuleVerifiesFinalizableNodes($nodeType) - { - $finalizableNode = $this->createFinalizableNodeMock($nodeType); - - $finalizableNode->expects($this->atLeastOnce()) - ->method('isFinal'); - - $rule = new FinalImplementation(); - $rule->apply($finalizableNode); - } - - /** - * @expectedException BadMethodCallException - */ - public function testRuleFailsOnNotFinalizableNodes() - { - $someNode = $this->getMockBuilder(AbstractNode::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - - $rule = new FinalImplementation(); - $rule->apply($someNode); - } - - /** - * "final" keyword may be applied only to classes and methods - * - * @return array - */ - public function finalizableNodeTypesProvider() - { - return [ - [ClassNode::class], - [MethodNode::class], - ]; - } - - /** - * If node is finalizable it has "isFinal" magic PHP method - * - * @param string $nodeType - * @return ClassNode|MethodNode|MockObject - */ - private function createFinalizableNodeMock($nodeType) - { - $finalizableNode = $this->getMockBuilder($nodeType) - ->disableOriginalConstructor() - ->disableProxyingToOriginalMethods() - ->setMethods([ - 'isFinal', - // disable name lookup from AST artifact - 'getNamespaceName', - 'getParentName', - 'getName', - ]) - ->getMock(); - return $finalizableNode; - } - - /** - * @param FinalImplementation $rule - * @param InvokedRecorder $violationExpectation - * @return InvocationMocker - */ - private function expectsRuleViolation(FinalImplementation $rule, InvokedRecorder $violationExpectation) - { - $report = $this->getMockBuilder(Report::class)->getMock(); - $invokation = $report->expects($violationExpectation)->method('addRuleViolation'); - $rule->setReport($report); - return $invokation; - } -} diff --git a/dev/tests/static/framework/Magento/CodeMessDetector/resources/rulesets/design.xml b/dev/tests/static/framework/Magento/CodeMessDetector/resources/rulesets/design.xml index 61d4f7b3be81..5f2461812bab 100644 --- a/dev/tests/static/framework/Magento/CodeMessDetector/resources/rulesets/design.xml +++ b/dev/tests/static/framework/Magento/CodeMessDetector/resources/rulesets/design.xml @@ -10,25 +10,81 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0 http://pmd.sf.net/ruleset_xml_schema.xsd" xsi:noNamespaceSchemaLocation="http://pmd.sf.net/ruleset_xml_schema.xsd"> - + ActionInterface +to restrict incoming requests by methods. ]]> - 1 + 2 + + + + + + + 2 + + + session = $session; + } + + public function place(OrderInterface $order) + { + //Will not be present if processing a WebAPI request + $currentOrder = $this->session->get('current_order'); + ... + } +} + ]]> + + + + + + + 2 + + + diff --git a/dev/tests/static/framework/Magento/Sniffs/Annotation/AnnotationFormatValidator.php b/dev/tests/static/framework/Magento/Sniffs/Annotation/AnnotationFormatValidator.php new file mode 100644 index 000000000000..33df6e8ae54f --- /dev/null +++ b/dev/tests/static/framework/Magento/Sniffs/Annotation/AnnotationFormatValidator.php @@ -0,0 +1,374 @@ +getTokens(); + $shortPtrEnd = $shortPtr; + for ($i = ($shortPtr + 1); $i < $commentEndPtr; $i++) { + if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) { + if ($tokens[$i]['line'] === $tokens[$shortPtrEnd]['line'] + 1) { + $shortPtrEnd = $i; + } else { + break; + } + } + } + return $shortPtrEnd; + } + + /** + * Validates whether the short description has multi lines in description + * + * @param File $phpcsFile + * @param int $shortPtr + * @param int $commentEndPtr + */ + private function validateMultiLinesInShortDescription( + File $phpcsFile, + int $shortPtr, + int $commentEndPtr + ) : void { + $tokens = $phpcsFile->getTokens(); + $shortPtrEnd = $this->getShortDescriptionEndPosition( + $phpcsFile, + (int) $shortPtr, + $commentEndPtr + ); + $shortPtrEndContent = $tokens[$shortPtrEnd]['content']; + if (preg_match('/^[a-z]/', $shortPtrEndContent) + && $shortPtrEnd != $shortPtr + && !preg_match('/\bSee\b/', $shortPtrEndContent) + && $tokens[$shortPtr]['line']+1 === $tokens[$shortPtrEnd]['line'] + && $tokens[$shortPtrEnd]['code'] !== T_DOC_COMMENT_TAG + ) { + $error = 'Short description should not be in multi lines'; + $phpcsFile->addFixableError($error, $shortPtrEnd+1, 'MethodAnnotation'); + } + } + + /** + * Validates whether the spacing between short and long descriptions + * + * @param File $phpcsFile + * @param int $shortPtr + * @param int $commentEndPtr + * @param array $emptyTypeTokens + */ + private function validateSpacingBetweenShortAndLongDescriptions( + File $phpcsFile, + int $shortPtr, + int $commentEndPtr, + array $emptyTypeTokens + ) : void { + $tokens = $phpcsFile->getTokens(); + $shortPtrEnd = $this->getShortDescriptionEndPosition( + $phpcsFile, + (int) $shortPtr, + $commentEndPtr + ); + $shortPtrEndContent = $tokens[$shortPtrEnd]['content']; + if (preg_match('/^[A-Z]/', $shortPtrEndContent) + && !preg_match('/\bSee\b/', $shortPtrEndContent) + && $tokens[$shortPtr]['line']+1 === $tokens[$shortPtrEnd]['line'] + && $tokens[$shortPtrEnd]['code'] !== T_DOC_COMMENT_TAG + ) { + $error = 'There must be exactly one blank line between lines'; + $phpcsFile->addFixableError($error, $shortPtrEnd + 1, 'MethodAnnotation'); + } + if ($shortPtrEnd != $shortPtr) { + $this->validateLongDescriptionFormat($phpcsFile, $shortPtrEnd, $commentEndPtr, $emptyTypeTokens); + } else { + $this->validateLongDescriptionFormat($phpcsFile, $shortPtr, $commentEndPtr, $emptyTypeTokens); + } + } + + /** + * Validates short description format + * + * @param File $phpcsFile + * @param int $shortPtr + * @param int $stackPtr + * @param int $commentEndPtr + * @param array $emptyTypeTokens + */ + private function validateShortDescriptionFormat( + File $phpcsFile, + int $shortPtr, + int $stackPtr, + int $commentEndPtr, + array $emptyTypeTokens + ) : void { + $tokens = $phpcsFile->getTokens(); + if ($tokens[$shortPtr]['line'] !== $tokens[$stackPtr]['line'] + 1) { + $error = 'No blank lines are allowed before short description'; + $phpcsFile->addFixableError($error, $shortPtr, 'MethodAnnotation'); + } + if (strtolower($tokens[$shortPtr]['content']) === '{@inheritdoc}') { + $error = 'If the @inheritdoc not inline it shouldn’t have braces'; + $phpcsFile->addFixableError($error, $shortPtr, 'MethodAnnotation'); + } + $shortPtrContent = $tokens[$shortPtr]['content']; + if (preg_match('/^\p{Ll}/u', $shortPtrContent) === 1) { + $error = 'Short description must start with a capital letter'; + $phpcsFile->addFixableError($error, $shortPtr, 'MethodAnnotation'); + } + $this->validateNoExtraNewLineBeforeShortDescription( + $phpcsFile, + $stackPtr, + $commentEndPtr, + $emptyTypeTokens + ); + $this->validateSpacingBetweenShortAndLongDescriptions( + $phpcsFile, + $shortPtr, + $commentEndPtr, + $emptyTypeTokens + ); + $this->validateMultiLinesInShortDescription( + $phpcsFile, + $shortPtr, + $commentEndPtr + ); + } + + /** + * Validates long description format + * + * @param File $phpcsFile + * @param int $shortPtrEnd + * @param int $commentEndPtr + * @param array $emptyTypeTokens + */ + private function validateLongDescriptionFormat( + File $phpcsFile, + int $shortPtrEnd, + int $commentEndPtr, + array $emptyTypeTokens + ) : void { + $tokens = $phpcsFile->getTokens(); + $longPtr = $phpcsFile->findNext($emptyTypeTokens, $shortPtrEnd + 1, $commentEndPtr - 1, true); + if (strtolower($tokens[$longPtr]['content']) === '@inheritdoc') { + $error = '@inheritdoc imports only short description, annotation must have long description'; + $phpcsFile->addFixableError($error, $longPtr, 'MethodAnnotation'); + } + if ($longPtr !== false && $tokens[$longPtr]['code'] === T_DOC_COMMENT_STRING) { + if ($tokens[$longPtr]['line'] !== $tokens[$shortPtrEnd]['line'] + 2) { + $error = 'There must be exactly one blank line between descriptions'; + $phpcsFile->addFixableError($error, $longPtr, 'MethodAnnotation'); + } + if (preg_match('/^\p{Ll}/u', $tokens[$longPtr]['content']) === 1) { + $error = 'Long description must start with a capital letter'; + $phpcsFile->addFixableError($error, $longPtr, 'MethodAnnotation'); + } + } + } + + /** + * Validates tags spacing format + * + * @param File $phpcsFile + * @param int $commentStartPtr + * @param array $emptyTypeTokens + */ + public function validateTagsSpacingFormat(File $phpcsFile, int $commentStartPtr, array $emptyTypeTokens) : void + { + $tokens = $phpcsFile->getTokens(); + if (isset($tokens[$commentStartPtr]['comment_tags'][0])) { + $firstTagPtr = $tokens[$commentStartPtr]['comment_tags'][0]; + $commentTagPtrContent = $tokens[$firstTagPtr]['content']; + $prevPtr = $phpcsFile->findPrevious($emptyTypeTokens, $firstTagPtr - 1, $commentStartPtr, true); + if ($tokens[$firstTagPtr]['line'] !== $tokens[$prevPtr]['line'] + 2 + && strtolower($commentTagPtrContent) !== '@inheritdoc' + ) { + $error = 'There must be exactly one blank line before tags'; + $phpcsFile->addFixableError($error, $firstTagPtr, 'MethodAnnotation'); + } + } + } + + /** + * Validates tag grouping format + * + * @param File $phpcsFile + * @param int $commentStartPtr + */ + public function validateTagGroupingFormat(File $phpcsFile, int $commentStartPtr) : void + { + $tokens = $phpcsFile->getTokens(); + $tagGroups = []; + $groupId = 0; + $paramGroupId = null; + foreach ($tokens[$commentStartPtr]['comment_tags'] as $position => $tag) { + if ($position > 0) { + $prevPtr = $phpcsFile->findPrevious( + T_DOC_COMMENT_STRING, + $tag - 1, + $tokens[$commentStartPtr]['comment_tags'][$position - 1] + ); + if ($prevPtr === false) { + $prevPtr = $tokens[$commentStartPtr]['comment_tags'][$position - 1]; + } + + if ($tokens[$prevPtr]['line'] !== $tokens[$tag]['line'] - 1) { + $groupId++; + } + } + + if (strtolower($tokens[$tag]['content']) === '@param') { + if ($paramGroupId !== null + && $paramGroupId !== $groupId) { + $error = 'Parameter tags must be grouped together'; + $phpcsFile->addFixableError($error, $tag, 'MethodAnnotation'); + } + if ($paramGroupId === null) { + $paramGroupId = $groupId; + } + } + $tagGroups[$groupId][] = $tag; + } + } + + /** + * Validates tag aligning format + * + * @param File $phpcsFile + * @param int $commentStartPtr + */ + public function validateTagAligningFormat(File $phpcsFile, int $commentStartPtr) : void + { + $tokens = $phpcsFile->getTokens(); + $noAlignmentPositions = []; + $actualPositions = []; + $stackPtr = null; + foreach ($tokens[$commentStartPtr]['comment_tags'] as $tag) { + $content = $tokens[$tag]['content']; + if (preg_match('/^@/', $content) && ($tokens[$tag]['line'] === $tokens[$tag + 2]['line'])) { + $noAlignmentPositions[] = $tokens[$tag + 1]['column'] + 1; + $actualPositions[] = $tokens[$tag + 2]['column']; + $stackPtr = $stackPtr ?? $tag; + } + } + + if (!$this->allTagsAligned($actualPositions) + && !$this->noneTagsAligned($actualPositions, $noAlignmentPositions)) { + $phpcsFile->addFixableError( + 'Tags visual alignment must be consistent', + $stackPtr, + 'MethodArguments' + ); + } + } + + /** + * Check whether all docblock params are aligned. + * + * @param array $actualPositions + * @return bool + */ + private function allTagsAligned(array $actualPositions) + { + return count(array_unique($actualPositions)) === 1; + } + + /** + * Check whether all docblock params are not aligned. + * + * @param array $actualPositions + * @param array $noAlignmentPositions + * @return bool + */ + private function noneTagsAligned(array $actualPositions, array $noAlignmentPositions) + { + return $actualPositions === $noAlignmentPositions; + } + + /** + * Validates extra newline before short description + * + * @param File $phpcsFile + * @param int $commentStartPtr + * @param int $commentEndPtr + * @param array $emptyTypeTokens + */ + private function validateNoExtraNewLineBeforeShortDescription( + File $phpcsFile, + int $commentStartPtr, + int $commentEndPtr, + array $emptyTypeTokens + ) : void { + $tokens = $phpcsFile->getTokens(); + $prevPtr = $phpcsFile->findPrevious($emptyTypeTokens, $commentEndPtr - 1, $commentStartPtr, true); + if ($tokens[$prevPtr]['line'] < ($tokens[$commentEndPtr]['line'] - 1)) { + $error = 'Additional blank lines found at end of the annotation block'; + $phpcsFile->addFixableError($error, $commentEndPtr, 'MethodAnnotation'); + } + } + + /** + * Validates structure description format + * + * @param File $phpcsFile + * @param int $commentStartPtr + * @param int $shortPtr + * @param int $commentEndPtr + * @param array $emptyTypeTokens + */ + public function validateDescriptionFormatStructure( + File $phpcsFile, + int $commentStartPtr, + int $shortPtr, + int $commentEndPtr, + array $emptyTypeTokens + ) : void { + $tokens = $phpcsFile->getTokens(); + if (isset($tokens[$commentStartPtr]['comment_tags'][0]) + ) { + $commentTagPtr = $tokens[$commentStartPtr]['comment_tags'][0]; + $commentTagPtrContent = $tokens[$commentTagPtr]['content']; + if ($tokens[$shortPtr]['code'] !== T_DOC_COMMENT_STRING + && strtolower($commentTagPtrContent) !== '@inheritdoc' + ) { + $error = 'Missing short description'; + $phpcsFile->addFixableError($error, $commentStartPtr, 'MethodAnnotation'); + } else { + $this->validateShortDescriptionFormat( + $phpcsFile, + (int) $shortPtr, + (int)$commentStartPtr, + (int)$commentEndPtr, + $emptyTypeTokens + ); + } + } else { + $this->validateShortDescriptionFormat( + $phpcsFile, + (int) $shortPtr, + $commentStartPtr, + $commentEndPtr, + $emptyTypeTokens + ); + } + } +} diff --git a/dev/tests/static/framework/Magento/Sniffs/Annotation/ClassAnnotationStructureSniff.php b/dev/tests/static/framework/Magento/Sniffs/Annotation/ClassAnnotationStructureSniff.php new file mode 100644 index 000000000000..c37f0b500fe3 --- /dev/null +++ b/dev/tests/static/framework/Magento/Sniffs/Annotation/ClassAnnotationStructureSniff.php @@ -0,0 +1,121 @@ +annotationFormatValidator = new AnnotationFormatValidator(); + } + + /** + * Validates whether annotation block exists for interface, abstract or final classes + * + * @param File $phpcsFile + * @param int $previousCommentClosePtr + * @param int $stackPtr + */ + private function validateInterfaceOrAbstractOrFinalClassAnnotationBlockExists( + File $phpcsFile, + int $previousCommentClosePtr, + int $stackPtr + ) : void { + $tokens = $phpcsFile->getTokens(); + if ($tokens[$stackPtr]['type'] === 'T_CLASS') { + if ($tokens[$stackPtr - 2]['type'] === 'T_ABSTRACT' && + $tokens[$stackPtr - 4]['content'] != $tokens[$previousCommentClosePtr]['content'] + ) { + $error = 'Interface or abstract class is missing annotation block'; + $phpcsFile->addFixableError($error, $stackPtr, 'ClassAnnotation'); + } + if ($tokens[$stackPtr - 2]['type'] === 'T_FINAL' && + $tokens[$stackPtr - 4]['content'] != $tokens[$previousCommentClosePtr]['content'] + ) { + $error = 'Final class is missing annotation block'; + $phpcsFile->addFixableError($error, $stackPtr, 'ClassAnnotation'); + } + } + } + + /** + * Validates whether annotation block exists + * + * @param File $phpcsFile + * @param int $previousCommentClosePtr + * @param int $stackPtr + */ + private function validateAnnotationBlockExists(File $phpcsFile, int $previousCommentClosePtr, int $stackPtr) : void + { + $tokens = $phpcsFile->getTokens(); + $this->validateInterfaceOrAbstractOrFinalClassAnnotationBlockExists( + $phpcsFile, + $previousCommentClosePtr, + $stackPtr + ); + if ($tokens[$stackPtr - 2]['content'] != 'class' && $tokens[$stackPtr - 2]['content'] != 'abstract' + && $tokens[$stackPtr - 2]['content'] != 'final' + && $tokens[$stackPtr - 2]['content'] !== $tokens[$previousCommentClosePtr]['content'] + ) { + $error = 'Class is missing annotation block'; + $phpcsFile->addFixableError($error, $stackPtr, 'ClassAnnotation'); + } + } + + /** + * @inheritdoc + */ + public function process(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + $previousCommentClosePtr = $phpcsFile->findPrevious(T_DOC_COMMENT_CLOSE_TAG, $stackPtr - 1, 0); + $this->validateAnnotationBlockExists($phpcsFile, (int)$previousCommentClosePtr, (int)$stackPtr); + $commentStartPtr = (int)$phpcsFile->findPrevious(T_DOC_COMMENT_OPEN_TAG, $stackPtr - 1, 0); + $commentCloserPtr = $tokens[$commentStartPtr]['comment_closer']; + $emptyTypeTokens = [ + T_DOC_COMMENT_WHITESPACE, + T_DOC_COMMENT_STAR + ]; + $shortPtr = $phpcsFile->findNext($emptyTypeTokens, $commentStartPtr +1, $commentCloserPtr, true); + if ($shortPtr === false) { + $error = 'Annotation block is empty'; + $phpcsFile->addError($error, $commentStartPtr, 'MethodAnnotation'); + } else { + $this->annotationFormatValidator->validateDescriptionFormatStructure( + $phpcsFile, + (int)$commentStartPtr, + (int) $shortPtr, + $previousCommentClosePtr, + $emptyTypeTokens + ); + } + } +} diff --git a/dev/tests/static/framework/Magento/Sniffs/Annotation/MethodAnnotationStructureSniff.php b/dev/tests/static/framework/Magento/Sniffs/Annotation/MethodAnnotationStructureSniff.php new file mode 100644 index 000000000000..f05ae64a170d --- /dev/null +++ b/dev/tests/static/framework/Magento/Sniffs/Annotation/MethodAnnotationStructureSniff.php @@ -0,0 +1,81 @@ +annotationFormatValidator = new AnnotationFormatValidator(); + } + + /** + * @inheritdoc + */ + public function register() + { + return [ + T_FUNCTION + ]; + } + + /** + * @inheritdoc + */ + public function process(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + $commentStartPtr = $phpcsFile->findPrevious(T_DOC_COMMENT_OPEN_TAG, ($stackPtr), 0); + $commentEndPtr = $phpcsFile->findPrevious(T_DOC_COMMENT_CLOSE_TAG, ($stackPtr), 0); + $commentCloserPtr = $tokens[$commentStartPtr]['comment_closer']; + $functionPtrContent = $tokens[$stackPtr+2]['content'] ; + if (preg_match('/(?i)__construct/', $functionPtrContent)) { + return; + } + $emptyTypeTokens = [ + T_DOC_COMMENT_WHITESPACE, + T_DOC_COMMENT_STAR + ]; + $shortPtr = $phpcsFile->findNext($emptyTypeTokens, $commentStartPtr + 1, $commentCloserPtr, true); + if ($shortPtr === false) { + $error = 'Annotation block is empty'; + $phpcsFile->addError($error, $commentStartPtr, 'MethodAnnotation'); + } else { + $this->annotationFormatValidator->validateDescriptionFormatStructure( + $phpcsFile, + $commentStartPtr, + (int) $shortPtr, + $commentEndPtr, + $emptyTypeTokens + ); + if (empty($tokens[$commentStartPtr]['comment_tags'])) { + return; + } + $this->annotationFormatValidator->validateTagsSpacingFormat( + $phpcsFile, + $commentStartPtr, + $emptyTypeTokens + ); + $this->annotationFormatValidator->validateTagGroupingFormat($phpcsFile, $commentStartPtr); + $this->annotationFormatValidator->validateTagAligningFormat($phpcsFile, $commentStartPtr); + } + } +} diff --git a/dev/tests/static/framework/Magento/Sniffs/Annotation/MethodArgumentsSniff.php b/dev/tests/static/framework/Magento/Sniffs/Annotation/MethodArgumentsSniff.php index 879334d8c553..50efca9b1ed2 100644 --- a/dev/tests/static/framework/Magento/Sniffs/Annotation/MethodArgumentsSniff.php +++ b/dev/tests/static/framework/Magento/Sniffs/Annotation/MethodArgumentsSniff.php @@ -8,8 +8,8 @@ namespace Magento\Sniffs\Annotation; -use PHP_CodeSniffer\Sniffs\Sniff; use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Sniffs\Sniff; /** * Sniff to validate method arguments annotations @@ -42,7 +42,7 @@ class MethodArgumentsSniff implements Sniff /** * @inheritdoc */ - public function register() : array + public function register(): array { return [ T_FUNCTION @@ -55,7 +55,7 @@ public function register() : array * @param string $type * @return bool */ - private function isTokenBeforeClosingCommentTagValid(string $type) : bool + private function isTokenBeforeClosingCommentTagValid(string $type): bool { return in_array($type, $this->validTokensBeforeClosingCommentTag); } @@ -68,7 +68,7 @@ private function isTokenBeforeClosingCommentTagValid(string $type) : bool * @param int $stackPtr * @return bool */ - private function validateCommentBlockExists(File $phpcsFile, int $previousCommentClosePtr, int $stackPtr) : bool + private function validateCommentBlockExists(File $phpcsFile, int $previousCommentClosePtr, int $stackPtr): bool { $tokens = $phpcsFile->getTokens(); for ($tempPtr = $previousCommentClosePtr + 1; $tempPtr < $stackPtr; $tempPtr++) { @@ -85,7 +85,7 @@ private function validateCommentBlockExists(File $phpcsFile, int $previousCommen * @param string $type * @return bool */ - private function isInvalidType(string $type) : bool + private function isInvalidType(string $type): bool { return in_array(strtolower($type), $this->invalidTypes); } @@ -98,7 +98,7 @@ private function isInvalidType(string $type) : bool * @param int $closedParenthesisPtr * @return array */ - private function getMethodArguments(File $phpcsFile, int $openParenthesisPtr, int $closedParenthesisPtr) : array + private function getMethodArguments(File $phpcsFile, int $openParenthesisPtr, int $closedParenthesisPtr): array { $tokens = $phpcsFile->getTokens(); $methodArguments = []; @@ -121,10 +121,11 @@ private function getMethodArguments(File $phpcsFile, int $openParenthesisPtr, in * @param array $paramDefinitions * @return array */ - private function getMethodParameters(array $paramDefinitions) : array + private function getMethodParameters(array $paramDefinitions): array { $paramName = []; - for ($i = 0; $i < count($paramDefinitions); $i++) { + $paramCount = count($paramDefinitions); + for ($i = 0; $i < $paramCount; $i++) { if (isset($paramDefinitions[$i]['paramName'])) { $paramName[] = $paramDefinitions[$i]['paramName']; } @@ -143,7 +144,7 @@ private function validateInheritdocAnnotationWithoutBracesExists( File $phpcsFile, int $previousCommentOpenPtr, int $previousCommentClosePtr - ) : bool { + ): bool { return $this->validateInheritdocAnnotationExists( $phpcsFile, $previousCommentOpenPtr, @@ -163,7 +164,7 @@ private function validateInheritdocAnnotationWithBracesExists( File $phpcsFile, int $previousCommentOpenPtr, int $previousCommentClosePtr - ) : bool { + ): bool { return $this->validateInheritdocAnnotationExists( $phpcsFile, $previousCommentOpenPtr, @@ -186,7 +187,7 @@ private function validateInheritdocAnnotationExists( int $previousCommentOpenPtr, int $previousCommentClosePtr, string $inheritdocAnnotation - ) : bool { + ): bool { $tokens = $phpcsFile->getTokens(); for ($ptr = $previousCommentOpenPtr; $ptr < $previousCommentClosePtr; $ptr++) { if (strtolower($tokens[$ptr]['content']) === $inheritdocAnnotation) { @@ -213,7 +214,7 @@ private function validateParameterAnnotationForArgumentExists( int $previousCommentOpenPtr, int $previousCommentClosePtr, int $stackPtr - ) : void { + ): void { if ($argumentsCount > 0 && $parametersCount === 0) { $inheritdocAnnotationWithoutBracesExists = $this->validateInheritdocAnnotationWithoutBracesExists( $phpcsFile, @@ -256,7 +257,7 @@ private function validateCommentBlockDoesnotHaveExtraParameterAnnotation( int $argumentsCount, int $parametersCount, int $stackPtr - ) : void { + ): void { if ($argumentsCount < $parametersCount && $argumentsCount > 0) { $phpcsFile->addFixableError( 'Extra @param found in method annotation', @@ -287,10 +288,10 @@ private function validateArgumentNameInParameterAnnotationExists( File $phpcsFile, array $methodArguments, array $paramDefinitions - ) : void { + ): void { $parameterNames = $this->getMethodParameters($paramDefinitions); if (!in_array($methodArguments[$ptr], $parameterNames)) { - $error = $methodArguments[$ptr]. ' parameter is missing in method annotation'; + $error = $methodArguments[$ptr] . ' parameter is missing in method annotation'; $phpcsFile->addFixableError($error, $stackPtr, 'MethodArguments'); } } @@ -310,7 +311,7 @@ private function validateParameterPresentInMethodSignature( array $methodArguments, File $phpcsFile, array $paramPointers - ) : void { + ): void { if (!in_array($paramDefinitionsArguments, $methodArguments)) { $phpcsFile->addFixableError( $paramDefinitionsArguments . ' parameter is missing in method arguments signature', @@ -333,7 +334,7 @@ private function validateParameterOrderIsCorrect( array $methodArguments, File $phpcsFile, array $paramPointers - ) : void { + ): void { $parameterNames = $this->getMethodParameters($paramDefinitions); $paramDefinitionsCount = count($paramDefinitions); for ($ptr = 0; $ptr < $paramDefinitionsCount; $ptr++) { @@ -342,7 +343,7 @@ private function validateParameterOrderIsCorrect( ) { if ($methodArguments[$ptr] != $parameterNames[$ptr]) { $phpcsFile->addFixableError( - $methodArguments[$ptr].' parameter is not in order', + $methodArguments[$ptr] . ' parameter is not in order', $paramPointers[$ptr], 'MethodArguments' ); @@ -366,15 +367,16 @@ private function validateDuplicateAnnotationDoesnotExists( array $paramPointers, File $phpcsFile, array $methodArguments - ) : void { + ): void { $argumentsCount = count($methodArguments); $parametersCount = count($paramPointers); if ($argumentsCount <= $parametersCount && $argumentsCount > 0) { $duplicateParameters = []; - for ($i = 0; $i < sizeof($paramDefinitions); $i++) { + $paramCount = count($paramDefinitions); + for ($i = 0; $i < $paramCount; $i++) { if (isset($paramDefinitions[$i]['paramName'])) { $parameterContent = $paramDefinitions[$i]['paramName']; - for ($j = $i + 1; $j < count($paramDefinitions); $j++) { + for ($j = $i + 1; $j < $paramCount; $j++) { if (isset($paramDefinitions[$j]['paramName']) && $parameterContent === $paramDefinitions[$j]['paramName'] ) { @@ -408,7 +410,7 @@ private function validateParameterAnnotationFormatIsCorrect( array $methodArguments, array $paramDefinitions, array $paramPointers - ) : void { + ): void { switch (count($paramDefinitions)) { case 0: $phpcsFile->addFixableError( @@ -429,7 +431,7 @@ private function validateParameterAnnotationFormatIsCorrect( case 2: if ($this->isInvalidType($paramDefinitions[0])) { $phpcsFile->addFixableError( - $paramDefinitions[0].' is not a valid PHP type', + $paramDefinitions[0] . ' is not a valid PHP type', $paramPointers[$ptr], 'MethodArguments' ); @@ -451,7 +453,7 @@ private function validateParameterAnnotationFormatIsCorrect( ); if ($this->isInvalidType($paramDefinitions[0])) { $phpcsFile->addFixableError( - $paramDefinitions[0].' is not a valid PHP type', + $paramDefinitions[0] . ' is not a valid PHP type', $paramPointers[$ptr], 'MethodArguments' ); @@ -480,7 +482,7 @@ private function validateMethodParameterAnnotations( array $methodArguments, int $previousCommentOpenPtr, int $previousCommentClosePtr - ) : void { + ): void { $argumentCount = count($methodArguments); $paramCount = count($paramPointers); $this->validateParameterAnnotationForArgumentExists( @@ -510,8 +512,14 @@ private function validateMethodParameterAnnotations( $phpcsFile, $paramPointers ); - for ($ptr = 0; $ptr < count($methodArguments); $ptr++) { - $tokens = $phpcsFile->getTokens(); + $this->validateFormattingConsistency( + $paramDefinitions, + $methodArguments, + $phpcsFile, + $paramPointers + ); + $tokens = $phpcsFile->getTokens(); + for ($ptr = 0; $ptr < $argumentCount; $ptr++) { if (isset($paramPointers[$ptr])) { $this->validateArgumentNameInParameterAnnotationExists( $stackPtr, @@ -520,7 +528,7 @@ private function validateMethodParameterAnnotations( $methodArguments, $paramDefinitions ); - $paramContent = $tokens[$paramPointers[$ptr]+2]['content']; + $paramContent = $tokens[$paramPointers[$ptr] + 2]['content']; $paramContentExplode = explode(' ', $paramContent); $this->validateParameterAnnotationFormatIsCorrect( $ptr, @@ -540,36 +548,40 @@ public function process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); $numTokens = count($tokens); - $previousCommentOpenPtr = $phpcsFile->findPrevious(T_DOC_COMMENT_OPEN_TAG, $stackPtr-1, 0); - $previousCommentClosePtr = $phpcsFile->findPrevious(T_DOC_COMMENT_CLOSE_TAG, $stackPtr-1, 0); + $previousCommentOpenPtr = $phpcsFile->findPrevious(T_DOC_COMMENT_OPEN_TAG, $stackPtr - 1, 0); + $previousCommentClosePtr = $phpcsFile->findPrevious(T_DOC_COMMENT_CLOSE_TAG, $stackPtr - 1, 0); if (!$this->validateCommentBlockExists($phpcsFile, $previousCommentClosePtr, $stackPtr)) { $phpcsFile->addError('Comment block is missing', $stackPtr, 'MethodArguments'); return; } - $openParenthesisPtr = $phpcsFile->findNext(T_OPEN_PARENTHESIS, $stackPtr+1, $numTokens); - $closedParenthesisPtr = $phpcsFile->findNext(T_CLOSE_PARENTHESIS, $stackPtr+1, $numTokens); + $openParenthesisPtr = $phpcsFile->findNext(T_OPEN_PARENTHESIS, $stackPtr + 1, $numTokens); + $closedParenthesisPtr = $phpcsFile->findNext(T_CLOSE_PARENTHESIS, $stackPtr + 1, $numTokens); $methodArguments = $this->getMethodArguments($phpcsFile, $openParenthesisPtr, $closedParenthesisPtr); $paramPointers = $paramDefinitions = []; for ($tempPtr = $previousCommentOpenPtr; $tempPtr < $previousCommentClosePtr; $tempPtr++) { if (strtolower($tokens[$tempPtr]['content']) === '@param') { $paramPointers[] = $tempPtr; - $paramAnnotationParts = explode(' ', $tokens[$tempPtr+2]['content']); + $content = preg_replace('/\s+/', ' ', $tokens[$tempPtr + 2]['content'], 2); + $paramAnnotationParts = explode(' ', $content, 3); if (count($paramAnnotationParts) === 1) { if ((preg_match('/^\$.*/', $paramAnnotationParts[0]))) { $paramDefinitions[] = [ 'type' => null, - 'paramName' => rtrim(ltrim($tokens[$tempPtr+2]['content'], '&'), ',') + 'paramName' => rtrim(ltrim($tokens[$tempPtr + 2]['content'], '&'), ','), + 'comment' => null ]; } else { $paramDefinitions[] = [ - 'type' => $tokens[$tempPtr+2]['content'], - 'paramName' => null + 'type' => $tokens[$tempPtr + 2]['content'], + 'paramName' => null, + 'comment' => null ]; } } else { $paramDefinitions[] = [ 'type' => $paramAnnotationParts[0], - 'paramName' => rtrim(ltrim($paramAnnotationParts[1], '&'), ',') + 'paramName' => rtrim(ltrim($paramAnnotationParts[1], '&'), ','), + 'comment' => $paramAnnotationParts[2] ?? null, ]; } } @@ -584,4 +596,81 @@ public function process(File $phpcsFile, $stackPtr) $previousCommentClosePtr ); } + + /** + * Validates function params format consistency. + * + * @param array $paramDefinitions + * @param array $methodArguments + * @param File $phpcsFile + * @param array $paramPointers + * + * @see https://devdocs.magento.com/guides/v2.3/coding-standards/docblock-standard-general.html#format-consistency + */ + private function validateFormattingConsistency( + array $paramDefinitions, + array $methodArguments, + File $phpcsFile, + array $paramPointers + ): void { + $argumentPositions = []; + $commentPositions = []; + $tokens = $phpcsFile->getTokens(); + $argumentCount = count($methodArguments); + for ($ptr = 0; $ptr < $argumentCount; $ptr++) { + if (isset($paramPointers[$ptr])) { + $paramContent = $tokens[$paramPointers[$ptr] + 2]['content']; + $paramDefinition = $paramDefinitions[$ptr]; + $argumentPositions[] = strpos($paramContent, $paramDefinition['paramName']); + $commentPositions[] = $paramDefinition['comment'] + ? strpos($paramContent, $paramDefinition['comment']) : null; + } + } + if (!$this->allParamsAligned($argumentPositions, $commentPositions) + && !$this->noneParamsAligned($argumentPositions, $commentPositions, $paramDefinitions)) { + $phpcsFile->addFixableError( + 'Visual alignment must be consistent', + $paramPointers[0], + 'MethodArguments' + ); + } + } + + /** + * Check all params are aligned. + * + * @param array $argumentPositions + * @param array $commentPositions + * @return bool + */ + private function allParamsAligned(array $argumentPositions, array $commentPositions): bool + { + return count(array_unique($argumentPositions)) === 1 + && count(array_unique(array_filter($commentPositions))) <= 1; + } + + /** + * Check none of params are aligned. + * + * @param array $argumentPositions + * @param array $commentPositions + * @param array $paramDefinitions + * @return bool + */ + private function noneParamsAligned(array $argumentPositions, array $commentPositions, array $paramDefinitions): bool + { + $flag = true; + foreach ($argumentPositions as $index => $argumentPosition) { + $commentPosition = $commentPositions[$index]; + $type = $paramDefinitions[$index]['type']; + $paramName = $paramDefinitions[$index]['paramName']; + if (($argumentPosition !== strlen($type) + 1) || + (isset($commentPosition) && ($commentPosition !== $argumentPosition + strlen($paramName) + 1))) { + $flag = false; + break; + } + } + + return $flag; + } } diff --git a/dev/tests/static/framework/Magento/Sniffs/Arrays/ShortArraySyntaxSniff.php b/dev/tests/static/framework/Magento/Sniffs/Arrays/ShortArraySyntaxSniff.php deleted file mode 100644 index 9013f12b6b46..000000000000 --- a/dev/tests/static/framework/Magento/Sniffs/Arrays/ShortArraySyntaxSniff.php +++ /dev/null @@ -1,32 +0,0 @@ -addError( - 'Short array syntax must be used; expected "[]" but found "array()"', - $stackPtr, - 'ShortArraySyntax' - ); - } -} diff --git a/dev/tests/static/framework/Magento/Sniffs/EchoTags/ShortEchoSyntaxSniff.php b/dev/tests/static/framework/Magento/Sniffs/EchoTags/ShortEchoSyntaxSniff.php deleted file mode 100644 index 694905cb37ad..000000000000 --- a/dev/tests/static/framework/Magento/Sniffs/EchoTags/ShortEchoSyntaxSniff.php +++ /dev/null @@ -1,44 +0,0 @@ -getTokens(); - $openTag = $tokens[$stackPtr]; - - // HHVM Will classify findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); - if ($tokens[$nextToken]['code'] == T_ECHO) { - $phpcsFile->addError( - 'Short echo tag syntax must be used; expected "previousLineContent) !== 0; - $this->previousLineContent = $lineContent; - if (! $currentLineMatch && !$previousLineMatch) { - parent::checkLineLength($phpcsFile, $stackPtr, $lineContent); - } - } -} diff --git a/dev/tests/static/framework/Magento/Sniffs/Functions/OutputBufferingSniff.php b/dev/tests/static/framework/Magento/Sniffs/Functions/OutputBufferingSniff.php deleted file mode 100644 index d2bf2de418c2..000000000000 --- a/dev/tests/static/framework/Magento/Sniffs/Functions/OutputBufferingSniff.php +++ /dev/null @@ -1,31 +0,0 @@ - null]; - - /** - * @inheritdoc - */ - protected function addError($phpcsFile, $stackPtr, $function, $pattern = null) - { - $data = [$function]; - $error = 'The usage of %s() is forbidden'; - $type = 'Found'; - - if ($this->error === true) { - $phpcsFile->addError($error, $stackPtr, $type, $data); - } else { - $phpcsFile->addWarning($error, $stackPtr, $type, $data); - } - } -} diff --git a/dev/tests/static/framework/Magento/Sniffs/LanguageConstructs/LanguageConstructsSniff.php b/dev/tests/static/framework/Magento/Sniffs/LanguageConstructs/LanguageConstructsSniff.php deleted file mode 100644 index a3a49adf62a6..000000000000 --- a/dev/tests/static/framework/Magento/Sniffs/LanguageConstructs/LanguageConstructsSniff.php +++ /dev/null @@ -1,77 +0,0 @@ -getTokens(); - if ($tokens[$stackPtr]['code'] === T_BACKTICK) { - if ($phpcsFile->findNext(T_BACKTICK, $stackPtr + 1)) { - return; - } - $phpcsFile->addError($this->errorMessageBacktick, $stackPtr, $this->backtickCode); - return; - } - $phpcsFile->addError($this->errorMessage, $stackPtr, $this->directOutput, [$tokens[$stackPtr]['content']]); - } -} diff --git a/dev/tests/static/framework/Magento/Sniffs/Less/AvoidIdSniff.php b/dev/tests/static/framework/Magento/Sniffs/Less/AvoidIdSniff.php index f9cec6404dc6..4f94e335e036 100644 --- a/dev/tests/static/framework/Magento/Sniffs/Less/AvoidIdSniff.php +++ b/dev/tests/static/framework/Magento/Sniffs/Less/AvoidIdSniff.php @@ -13,8 +13,7 @@ * * Ensure that id selector is not used * - * @link http://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#types - * + * @link https://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#types */ class AvoidIdSniff implements Sniff { @@ -26,15 +25,36 @@ class AvoidIdSniff implements Sniff public $supportedTokenizers = [TokenizerSymbolsInterface::TOKENIZER_CSS]; /** + * Tokens that can appear in a selector + * * @var array */ - private $symbolsBeforeId = [ - TokenizerSymbolsInterface::INDENT_SPACES, - TokenizerSymbolsInterface::NEW_LINE, + private $selectorTokens = [ + T_HASH, + T_WHITESPACE, + T_STRING_CONCAT, + T_OPEN_PARENTHESIS, + T_CLOSE_PARENTHESIS, + T_OPEN_SQUARE_BRACKET, + T_CLOSE_SQUARE_BRACKET, + T_DOUBLE_QUOTED_STRING, + T_CONSTANT_ENCAPSED_STRING, + T_DOUBLE_COLON, + T_COLON, + T_EQUAL, + T_MUL_EQUAL, + T_OR_EQUAL, + T_STRING, + T_NONE, + T_DOLLAR, + T_GREATER_THAN, + T_PLUS, + T_NS_SEPARATOR, + T_LNUMBER, ]; /** - * {@inheritdoc} + * @inheritdoc */ public function register() { @@ -42,15 +62,38 @@ public function register() } /** - * {@inheritdoc} + * @inheritdoc + * + * Will flag any selector that looks like the following: + * #foo[bar], + * #foo[bar=bash], + * #foo[bar~=bash], + * #foo[bar$=bash], + * #foo[bar*=bash], + * #foo[bar|=bash], + * #foo[bar='bash'], + * #foo:hover, + * #foo:nth-last-of-type(n), + * #foo::before, + * #foo + div, + * #foo > div, + * #foo ~ div, + * #foo\3Abar ~ div, + * #foo\:bar ~ div, + * #foo.bar .baz, + * div#foo { + * blah: 'abc'; + * } */ public function process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); - if (T_WHITESPACE === $tokens[$stackPtr - 1]['code'] - && in_array($tokens[$stackPtr - 1]['content'], $this->symbolsBeforeId) - ) { + // Find the next non-selector token + $nextToken = $phpcsFile->findNext($this->selectorTokens, $stackPtr + 1, null, true); + + // Anything except a { or a , means this is not a selector + if ($nextToken !== false && in_array($tokens[$nextToken]['code'], [T_OPEN_CURLY_BRACKET, T_COMMA])) { $phpcsFile->addError('Id selector is used', $stackPtr, 'IdSelectorUsage'); } } diff --git a/dev/tests/static/framework/Magento/Sniffs/Less/BracesFormattingSniff.php b/dev/tests/static/framework/Magento/Sniffs/Less/BracesFormattingSniff.php index 435eea118af6..61e2e69eeb86 100644 --- a/dev/tests/static/framework/Magento/Sniffs/Less/BracesFormattingSniff.php +++ b/dev/tests/static/framework/Magento/Sniffs/Less/BracesFormattingSniff.php @@ -15,7 +15,7 @@ * Ensure there is a single blank line after the closing brace of a class definition * * @see Squiz_Sniffs_CSS_ClassDefinitionClosingBraceSpaceSniff - * @link http://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#braces + * @link https://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#braces */ class BracesFormattingSniff implements Sniff { @@ -27,7 +27,7 @@ class BracesFormattingSniff implements Sniff public $supportedTokenizers = [TokenizerSymbolsInterface::TOKENIZER_CSS]; /** - * {@inheritdoc} + * @inheritdoc */ public function register() { @@ -35,7 +35,7 @@ public function register() } /** - * {@inheritdoc} + * @inheritdoc */ public function process(File $phpcsFile, $stackPtr) { diff --git a/dev/tests/static/framework/Magento/Sniffs/Less/ClassNamingSniff.php b/dev/tests/static/framework/Magento/Sniffs/Less/ClassNamingSniff.php index 798c13ff2699..8c55edc13afc 100644 --- a/dev/tests/static/framework/Magento/Sniffs/Less/ClassNamingSniff.php +++ b/dev/tests/static/framework/Magento/Sniffs/Less/ClassNamingSniff.php @@ -17,8 +17,7 @@ * - start with a letter (except helper classes); * - words should be separated with dash '-'; * - * @link http://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#standard-classes - * + * @link https://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#standard-classes */ class ClassNamingSniff implements Sniff { @@ -35,7 +34,7 @@ class ClassNamingSniff implements Sniff public $supportedTokenizers = [TokenizerSymbolsInterface::TOKENIZER_CSS]; /** - * {@inheritdoc} + * @inheritdoc */ public function register() { @@ -43,7 +42,7 @@ public function register() } /** - * {@inheritdoc} + * @inheritdoc */ public function process(File $phpcsFile, $stackPtr) { diff --git a/dev/tests/static/framework/Magento/Sniffs/Less/ColonSpacingSniff.php b/dev/tests/static/framework/Magento/Sniffs/Less/ColonSpacingSniff.php index 39753544cf24..b5f61a643273 100644 --- a/dev/tests/static/framework/Magento/Sniffs/Less/ColonSpacingSniff.php +++ b/dev/tests/static/framework/Magento/Sniffs/Less/ColonSpacingSniff.php @@ -14,8 +14,7 @@ * * Ensure that single quotes are used * - * @link http://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#properties-colon-indents - * + * @link https://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#properties-colon-indents */ class ColonSpacingSniff implements Sniff { @@ -27,7 +26,7 @@ class ColonSpacingSniff implements Sniff public $supportedTokenizers = [TokenizerSymbolsInterface::TOKENIZER_CSS]; /** - * {@inheritdoc} + * @inheritdoc */ public function register() { @@ -35,7 +34,7 @@ public function register() } /** - * {@inheritdoc} + * @inheritdoc */ public function process(File $phpcsFile, $stackPtr) { diff --git a/dev/tests/static/framework/Magento/Sniffs/Less/ColourDefinitionSniff.php b/dev/tests/static/framework/Magento/Sniffs/Less/ColourDefinitionSniff.php index 83f2e8f5d94d..83e3da698f08 100644 --- a/dev/tests/static/framework/Magento/Sniffs/Less/ColourDefinitionSniff.php +++ b/dev/tests/static/framework/Magento/Sniffs/Less/ColourDefinitionSniff.php @@ -13,8 +13,7 @@ * * Ensure that hexadecimal values are used for variables not for properties * - * @link http://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#hexadecimal-notation - * + * @link https://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#hexadecimal-notation */ class ColourDefinitionSniff implements Sniff { @@ -26,7 +25,7 @@ class ColourDefinitionSniff implements Sniff public $supportedTokenizers = [TokenizerSymbolsInterface::TOKENIZER_CSS]; /** - * {@inheritdoc} + * @inheritdoc */ public function register() { @@ -34,7 +33,7 @@ public function register() } /** - * {@inheritdoc} + * @inheritdoc */ public function process(File $phpcsFile, $stackPtr) { diff --git a/dev/tests/static/framework/Magento/Sniffs/Less/CombinatorIndentationSniff.php b/dev/tests/static/framework/Magento/Sniffs/Less/CombinatorIndentationSniff.php index d27c853ab780..a8ce4cfe547a 100644 --- a/dev/tests/static/framework/Magento/Sniffs/Less/CombinatorIndentationSniff.php +++ b/dev/tests/static/framework/Magento/Sniffs/Less/CombinatorIndentationSniff.php @@ -13,8 +13,7 @@ * * Ensure that spaces are used before and after combinators * - * @link http://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#combinator-indents - * + * @link https://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#combinator-indents */ class CombinatorIndentationSniff implements Sniff { @@ -26,7 +25,7 @@ class CombinatorIndentationSniff implements Sniff public $supportedTokenizers = [TokenizerSymbolsInterface::TOKENIZER_CSS]; /** - * {@inheritdoc} + * @inheritdoc */ public function register() { @@ -34,7 +33,7 @@ public function register() } /** - * {@inheritdoc} + * @inheritdoc */ public function process(File $phpcsFile, $stackPtr) { diff --git a/dev/tests/static/framework/Magento/Sniffs/Less/CommentLevelsSniff.php b/dev/tests/static/framework/Magento/Sniffs/Less/CommentLevelsSniff.php index abf15dd3748d..41c970d4cb49 100644 --- a/dev/tests/static/framework/Magento/Sniffs/Less/CommentLevelsSniff.php +++ b/dev/tests/static/framework/Magento/Sniffs/Less/CommentLevelsSniff.php @@ -15,8 +15,7 @@ * First, second and third level comments should have two spaces after "//". * Inline comments should have one space after "//". * - * @link http://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#comments - * + * @link https://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#comments */ class CommentLevelsSniff implements Sniff { @@ -42,7 +41,7 @@ class CommentLevelsSniff implements Sniff public $supportedTokenizers = [TokenizerSymbolsInterface::TOKENIZER_CSS]; /** - * {@inheritdoc} + * @inheritdoc */ public function register() { @@ -50,7 +49,7 @@ public function register() } /** - * {@inheritdoc} + * @inheritdoc */ public function process(File $phpcsFile, $stackPtr) { diff --git a/dev/tests/static/framework/Magento/Sniffs/Less/ImportantPropertySniff.php b/dev/tests/static/framework/Magento/Sniffs/Less/ImportantPropertySniff.php index 54bd8b70d5c5..db03440699f4 100644 --- a/dev/tests/static/framework/Magento/Sniffs/Less/ImportantPropertySniff.php +++ b/dev/tests/static/framework/Magento/Sniffs/Less/ImportantPropertySniff.php @@ -13,8 +13,7 @@ * * Ensure that single quotes are used * - * @link http://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#important-property - * + * @link https://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#important-property */ class ImportantPropertySniff implements Sniff { @@ -26,7 +25,7 @@ class ImportantPropertySniff implements Sniff public $supportedTokenizers = [TokenizerSymbolsInterface::TOKENIZER_CSS]; /** - * {@inheritdoc} + * @inheritdoc */ public function register() { @@ -34,7 +33,7 @@ public function register() } /** - * {@inheritdoc} + * @inheritdoc */ public function process(File $phpcsFile, $stackPtr) { diff --git a/dev/tests/static/framework/Magento/Sniffs/Less/IndentationSniff.php b/dev/tests/static/framework/Magento/Sniffs/Less/IndentationSniff.php index 3ff268acf0c1..f0f994131e63 100644 --- a/dev/tests/static/framework/Magento/Sniffs/Less/IndentationSniff.php +++ b/dev/tests/static/framework/Magento/Sniffs/Less/IndentationSniff.php @@ -14,7 +14,7 @@ * Ensures styles are indented 4 spaces. * * @see Squiz_Sniffs_CSS_IndentationSniff - * @link http://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#indentation + * @link https://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#indentation */ class IndentationSniff implements Sniff { @@ -46,7 +46,7 @@ class IndentationSniff implements Sniff private $styleCodesToSkip = [T_ASPERAND, T_COLON, T_OPEN_PARENTHESIS, T_CLOSE_PARENTHESIS]; /** - * {@inheritdoc} + * @inheritdoc */ public function register() { @@ -54,7 +54,7 @@ public function register() } /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ diff --git a/dev/tests/static/framework/Magento/Sniffs/Less/PropertiesLineBreakSniff.php b/dev/tests/static/framework/Magento/Sniffs/Less/PropertiesLineBreakSniff.php index 3f33d0b2c69d..e9bd1f7942ed 100644 --- a/dev/tests/static/framework/Magento/Sniffs/Less/PropertiesLineBreakSniff.php +++ b/dev/tests/static/framework/Magento/Sniffs/Less/PropertiesLineBreakSniff.php @@ -13,8 +13,7 @@ * * Start each property declaration in a new line * - * @link http://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#properties-line-break - * + * @link https://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#properties-line-break */ class PropertiesLineBreakSniff implements Sniff { @@ -26,7 +25,7 @@ class PropertiesLineBreakSniff implements Sniff public $supportedTokenizers = [TokenizerSymbolsInterface::TOKENIZER_CSS]; /** - * {@inheritdoc} + * @inheritdoc */ public function register() { @@ -34,7 +33,7 @@ public function register() } /** - * {@inheritdoc} + * @inheritdoc */ public function process(File $phpcsFile, $stackPtr) { diff --git a/dev/tests/static/framework/Magento/Sniffs/Less/PropertiesSortingSniff.php b/dev/tests/static/framework/Magento/Sniffs/Less/PropertiesSortingSniff.php index e59b5da37d0e..a39974f2b386 100644 --- a/dev/tests/static/framework/Magento/Sniffs/Less/PropertiesSortingSniff.php +++ b/dev/tests/static/framework/Magento/Sniffs/Less/PropertiesSortingSniff.php @@ -13,8 +13,7 @@ * * Ensure that properties are sorted alphabetically * - * @link http://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#sorting - * + * @link https://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#sorting */ class PropertiesSortingSniff implements Sniff { @@ -43,7 +42,7 @@ class PropertiesSortingSniff implements Sniff ]; /** - * {@inheritdoc} + * @inheritdoc */ public function register() { @@ -57,7 +56,7 @@ public function register() } /** - * {@inheritdoc} + * @inheritdoc */ public function process(File $phpcsFile, $stackPtr) { diff --git a/dev/tests/static/framework/Magento/Sniffs/Less/QuotesSniff.php b/dev/tests/static/framework/Magento/Sniffs/Less/QuotesSniff.php index 92ea5b420658..8127d2677041 100644 --- a/dev/tests/static/framework/Magento/Sniffs/Less/QuotesSniff.php +++ b/dev/tests/static/framework/Magento/Sniffs/Less/QuotesSniff.php @@ -13,8 +13,7 @@ * * Ensure that single quotes are used * - * @link http://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#quotes - * + * @link https://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#quotes */ class QuotesSniff implements Sniff { @@ -26,7 +25,7 @@ class QuotesSniff implements Sniff public $supportedTokenizers = [TokenizerSymbolsInterface::TOKENIZER_CSS]; /** - * {@inheritdoc} + * @inheritdoc */ public function register() { @@ -34,7 +33,7 @@ public function register() } /** - * {@inheritdoc} + * @inheritdoc */ public function process(File $phpcsFile, $stackPtr) { diff --git a/dev/tests/static/framework/Magento/Sniffs/Less/SelectorDelimiterSniff.php b/dev/tests/static/framework/Magento/Sniffs/Less/SelectorDelimiterSniff.php index 466078666981..99b91b63d88f 100644 --- a/dev/tests/static/framework/Magento/Sniffs/Less/SelectorDelimiterSniff.php +++ b/dev/tests/static/framework/Magento/Sniffs/Less/SelectorDelimiterSniff.php @@ -14,8 +14,7 @@ * Ensure that a line break exists after each selector delimiter. * No spaces should be before or after delimiters. * - * @link http://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#selector-delimiters - * + * @link https://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#selector-delimiters */ class SelectorDelimiterSniff implements Sniff { @@ -27,7 +26,7 @@ class SelectorDelimiterSniff implements Sniff public $supportedTokenizers = [TokenizerSymbolsInterface::TOKENIZER_CSS]; /** - * {@inheritdoc} + * @inheritdoc */ public function register() { @@ -35,7 +34,7 @@ public function register() } /** - * {@inheritdoc} + * @inheritdoc */ public function process(File $phpcsFile, $stackPtr) { @@ -50,6 +49,8 @@ public function process(File $phpcsFile, $stackPtr) } /** + * Parenthesis validation. + * * @param File $phpcsFile * @param int $stackPtr * @param array $tokens diff --git a/dev/tests/static/framework/Magento/Sniffs/Less/SemicolonSpacingSniff.php b/dev/tests/static/framework/Magento/Sniffs/Less/SemicolonSpacingSniff.php index c1cd6cde3340..de30d9cdbf49 100644 --- a/dev/tests/static/framework/Magento/Sniffs/Less/SemicolonSpacingSniff.php +++ b/dev/tests/static/framework/Magento/Sniffs/Less/SemicolonSpacingSniff.php @@ -13,8 +13,7 @@ * * Property should have a semicolon at the end of line * - * @link http://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#end-of-the-property-line - * + * @link https://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#end-of-the-property-line */ class SemicolonSpacingSniff implements Sniff { @@ -44,7 +43,7 @@ class SemicolonSpacingSniff implements Sniff private $styleCodesToSkip = [T_ASPERAND, T_COLON, T_OPEN_PARENTHESIS, T_CLOSE_PARENTHESIS]; /** - * {@inheritdoc} + * @inheritdoc */ public function register() { @@ -52,7 +51,7 @@ public function register() } /** - * {@inheritdoc} + * @inheritdoc */ public function process(File $phpcsFile, $stackPtr) { @@ -72,6 +71,8 @@ public function process(File $phpcsFile, $stackPtr) } /** + * Semicolon validation. + * * @param File $phpcsFile * @param int $stackPtr * @param array $tokens @@ -90,6 +91,8 @@ private function validateSemicolon(File $phpcsFile, $stackPtr, array $tokens, $s } /** + * Spaces validation. + * * @param File $phpcsFile * @param int $stackPtr * @param array $tokens diff --git a/dev/tests/static/framework/Magento/Sniffs/Less/TokenizerSymbolsInterface.php b/dev/tests/static/framework/Magento/Sniffs/Less/TokenizerSymbolsInterface.php index 9da92913b402..db6a2fe11692 100644 --- a/dev/tests/static/framework/Magento/Sniffs/Less/TokenizerSymbolsInterface.php +++ b/dev/tests/static/framework/Magento/Sniffs/Less/TokenizerSymbolsInterface.php @@ -7,7 +7,6 @@ /** * Interface TokenizerSymbolsInterface - * */ interface TokenizerSymbolsInterface { diff --git a/dev/tests/static/framework/Magento/Sniffs/Less/TypeSelectorConcatenationSniff.php b/dev/tests/static/framework/Magento/Sniffs/Less/TypeSelectorConcatenationSniff.php index 88a275baec2a..16ebb71c7281 100644 --- a/dev/tests/static/framework/Magento/Sniffs/Less/TypeSelectorConcatenationSniff.php +++ b/dev/tests/static/framework/Magento/Sniffs/Less/TypeSelectorConcatenationSniff.php @@ -13,8 +13,7 @@ * * Ensure that selector in one line, concatenation is not used * - * @link http://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#formatting-1 - * + * @link https://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#formatting-1 */ class TypeSelectorConcatenationSniff implements Sniff { @@ -34,7 +33,7 @@ class TypeSelectorConcatenationSniff implements Sniff ]; /** - * {@inheritdoc} + * @inheritdoc */ public function register() { @@ -42,7 +41,7 @@ public function register() } /** - * {@inheritdoc} + * @inheritdoc */ public function process(File $phpcsFile, $stackPtr) { diff --git a/dev/tests/static/framework/Magento/Sniffs/Less/TypeSelectorsSniff.php b/dev/tests/static/framework/Magento/Sniffs/Less/TypeSelectorsSniff.php index 3983cf54e05f..3cd0f5d1679b 100644 --- a/dev/tests/static/framework/Magento/Sniffs/Less/TypeSelectorsSniff.php +++ b/dev/tests/static/framework/Magento/Sniffs/Less/TypeSelectorsSniff.php @@ -18,8 +18,7 @@ * - Type selectors must be lowercase * - Write selector in one line, do not use concatenation * - * @link http://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#selectors-naming - * + * @link https://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#selectors-naming */ class TypeSelectorsSniff implements Sniff { @@ -56,7 +55,7 @@ class TypeSelectorsSniff implements Sniff public $supportedTokenizers = [TokenizerSymbolsInterface::TOKENIZER_CSS]; /** - * {@inheritdoc} + * @inheritdoc */ public function register() { @@ -64,7 +63,7 @@ public function register() } /** - * {@inheritdoc} + * @inheritdoc */ public function process(File $phpcsFile, $stackPtr) { diff --git a/dev/tests/static/framework/Magento/Sniffs/Less/VariablesSniff.php b/dev/tests/static/framework/Magento/Sniffs/Less/VariablesSniff.php index 8a559da88e6f..433c256c5bda 100644 --- a/dev/tests/static/framework/Magento/Sniffs/Less/VariablesSniff.php +++ b/dev/tests/static/framework/Magento/Sniffs/Less/VariablesSniff.php @@ -16,9 +16,8 @@ * they should be located in the module file, in the beginning of the general comment. * - All variable names must be lowercase * - * @link http://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#local-variables - * @link http://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#naming - * + * @link https://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#local-variables + * @link https://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#naming */ class VariablesSniff implements Sniff { @@ -30,7 +29,7 @@ class VariablesSniff implements Sniff public $supportedTokenizers = [TokenizerSymbolsInterface::TOKENIZER_CSS]; /** - * {@inheritdoc} + * @inheritdoc */ public function register() { @@ -38,7 +37,7 @@ public function register() } /** - * {@inheritdoc} + * @inheritdoc */ public function process(File $phpcsFile, $stackPtr) { diff --git a/dev/tests/static/framework/Magento/Sniffs/Less/ZeroUnitsSniff.php b/dev/tests/static/framework/Magento/Sniffs/Less/ZeroUnitsSniff.php index 1b4fb53c4501..a19a0a8eb701 100644 --- a/dev/tests/static/framework/Magento/Sniffs/Less/ZeroUnitsSniff.php +++ b/dev/tests/static/framework/Magento/Sniffs/Less/ZeroUnitsSniff.php @@ -14,9 +14,8 @@ * Ensure that units for 0 is not specified * Omit leading "0"s in values, use dot instead * - * @link http://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#and-units - * @link http://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#floating-values - * + * @link https://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#and-units + * @link https://devdocs.magento.com/guides/v2.0/coding-standards/code-standard-less.html#floating-values */ class ZeroUnitsSniff implements Sniff { @@ -43,7 +42,7 @@ class ZeroUnitsSniff implements Sniff public $supportedTokenizers = [TokenizerSymbolsInterface::TOKENIZER_CSS]; /** - * {@inheritdoc} + * @inheritdoc */ public function register() { @@ -51,7 +50,7 @@ public function register() } /** - * {@inheritdoc} + * @inheritdoc */ public function process(File $phpcsFile, $stackPtr) { diff --git a/dev/tests/static/framework/Magento/Sniffs/LiteralNamespaces/LiteralNamespacesSniff.php b/dev/tests/static/framework/Magento/Sniffs/LiteralNamespaces/LiteralNamespacesSniff.php deleted file mode 100644 index b6af3f37cf11..000000000000 --- a/dev/tests/static/framework/Magento/Sniffs/LiteralNamespaces/LiteralNamespacesSniff.php +++ /dev/null @@ -1,75 +0,0 @@ -getTokens(); - if ($sourceFile->findPrevious(T_STRING_CONCAT, $stackPtr, $stackPtr - 3) || - $sourceFile->findNext(T_STRING_CONCAT, $stackPtr, $stackPtr + 3) - ) { - return; - } - - $content = trim($tokens[$stackPtr]['content'], "\"'"); - // replace double slashes from class name for avoiding problems with class autoload - if (strpos($content, '\\') !== false) { - $content = preg_replace('|\\\{2,}|', '\\', $content); - } - - if (preg_match($this->literalNamespacePattern, $content) === 1 && $this->classExists($content)) { - $sourceFile->addError( - "Use ::class notation instead.", - $stackPtr, - 'LiteralClassUsage' - ); - } - } - - /** - * @param string $className - * @return bool - */ - private function classExists($className) - { - if (!isset($this->classNames[$className])) { - $this->classNames[$className] = class_exists($className) || interface_exists($className); - } - return $this->classNames[$className]; - } -} diff --git a/dev/tests/static/framework/Magento/Sniffs/MicroOptimizations/IsNullSniff.php b/dev/tests/static/framework/Magento/Sniffs/MicroOptimizations/IsNullSniff.php deleted file mode 100644 index 928fc3a0defd..000000000000 --- a/dev/tests/static/framework/Magento/Sniffs/MicroOptimizations/IsNullSniff.php +++ /dev/null @@ -1,40 +0,0 @@ -getTokens(); - if ($tokens[$stackPtr]['content'] === $this->blacklist) { - $sourceFile->addError( - "is_null must be avoided. Use strict comparison instead.", - $stackPtr, - 'IsNullUsage' - ); - } - } -} diff --git a/dev/tests/static/framework/Magento/Sniffs/NamingConventions/InterfaceNameSniff.php b/dev/tests/static/framework/Magento/Sniffs/NamingConventions/InterfaceNameSniff.php deleted file mode 100644 index 1618beb665d4..000000000000 --- a/dev/tests/static/framework/Magento/Sniffs/NamingConventions/InterfaceNameSniff.php +++ /dev/null @@ -1,46 +0,0 @@ -getTokens(); - $declarationLine = $tokens[$stackPtr]['line']; - $suffixLength = strlen(self::INTERFACE_SUFFIX); - // Find first T_STRING after 'interface' keyword in the line and verify it - while ($tokens[$stackPtr]['line'] == $declarationLine) { - if ($tokens[$stackPtr]['type'] == 'T_STRING') { - if (substr($tokens[$stackPtr]['content'], 0 - $suffixLength) != self::INTERFACE_SUFFIX) { - $sourceFile->addError( - 'Interface should have name that ends with "Interface" suffix.', - $stackPtr, - 'WrongInterfaceName' - ); - } - break; - } - $stackPtr++; - } - } -} diff --git a/dev/tests/static/framework/Magento/Sniffs/NamingConventions/ReservedWordsSniff.php b/dev/tests/static/framework/Magento/Sniffs/NamingConventions/ReservedWordsSniff.php deleted file mode 100644 index e3cfdf532438..000000000000 --- a/dev/tests/static/framework/Magento/Sniffs/NamingConventions/ReservedWordsSniff.php +++ /dev/null @@ -1,113 +0,0 @@ - '7', - 'float' => '7', - 'bool' => '7', - 'string' => '7', - 'true' => '7', - 'false' => '7', - 'null' => '7', - 'void' => '7.1', - 'iterable' => '7.1', - 'resource' => '7', - 'object' => '7', - 'mixed' => '7', - 'numeric' => '7', - ]; - - /** - * {@inheritdoc} - */ - public function register() - { - return [T_CLASS, T_INTERFACE, T_TRAIT, T_NAMESPACE]; - } - - /** - * Check all namespace parts - * - * @param File $sourceFile - * @param int $stackPtr - * @return void - */ - protected function validateNamespace(File $sourceFile, $stackPtr) - { - $stackPtr += 2; - $tokens = $sourceFile->getTokens(); - while ($stackPtr < $sourceFile->numTokens && $tokens[$stackPtr]['code'] !== T_SEMICOLON) { - if ($tokens[$stackPtr]['code'] === T_WHITESPACE || $tokens[$stackPtr]['code'] === T_NS_SEPARATOR) { - $stackPtr++; //skip "namespace" and whitespace - continue; - } - $namespacePart = $tokens[$stackPtr]['content']; - if (isset($this->reservedWords[strtolower($namespacePart)])) { - $sourceFile->addError( - 'Cannot use "%s" in namespace as it is reserved since PHP %s', - $stackPtr, - 'Namespace', - [$namespacePart, $this->reservedWords[strtolower($namespacePart)]] - ); - } - $stackPtr++; - } - } - - /** - * Check class name not having reserved words - * - * @param File $sourceFile - * @param int $stackPtr - * @return void - */ - protected function validateClass(File $sourceFile, $stackPtr) - { - $tokens = $sourceFile->getTokens(); - $stackPtr += 2; //skip "class" and whitespace - $className = strtolower($tokens[$stackPtr]['content']); - if (isset($this->reservedWords[$className])) { - $sourceFile->addError( - 'Cannot use "%s" as class name as it is reserved since PHP %s', - $stackPtr, - 'Class', - [$className, $this->reservedWords[$className]] - ); - } - } - - /** - * {@inheritdoc} - */ - public function process(File $sourceFile, $stackPtr) - { - $tokens = $sourceFile->getTokens(); - switch ($tokens[$stackPtr]['code']) { - case T_CLASS: - case T_INTERFACE: - case T_TRAIT: - $this->validateClass($sourceFile, $stackPtr); - break; - case T_NAMESPACE: - $this->validateNamespace($sourceFile, $stackPtr); - break; - } - } -} diff --git a/dev/tests/static/framework/Magento/Sniffs/Security/ExecutableRegExSniff.php b/dev/tests/static/framework/Magento/Sniffs/Security/ExecutableRegExSniff.php deleted file mode 100644 index 0482f574f28c..000000000000 --- a/dev/tests/static/framework/Magento/Sniffs/Security/ExecutableRegExSniff.php +++ /dev/null @@ -1,87 +0,0 @@ -getTokens(); - if ($tokens[$stackPtr]['content'] !== $this->function) { - return; - } - $prevToken = $phpcsFile->findPrevious(T_WHITESPACE, $stackPtr - 1, null, true); - if (in_array($tokens[$prevToken]['code'], $this->ignoreTokens)) { - return; - } - $nextToken = $phpcsFile->findNext([T_WHITESPACE, T_OPEN_PARENTHESIS], $stackPtr + 1, null, true); - if (in_array($tokens[$nextToken]['code'], Tokens::$stringTokens) - && preg_match('/[#\/|~\}\)][imsxADSUXJu]*e[imsxADSUXJu]*.$/', $tokens[$nextToken]['content']) - ) { - $phpcsFile->addError( - $this->errorMessage, - $stackPtr, - $this->errorCode, - [$tokens[$stackPtr]['content']] - ); - } - } -} diff --git a/dev/tests/static/framework/Magento/Sniffs/Strings/StringPositionSniff.php b/dev/tests/static/framework/Magento/Sniffs/Strings/StringPositionSniff.php deleted file mode 100644 index d98a55a6f3ee..000000000000 --- a/dev/tests/static/framework/Magento/Sniffs/Strings/StringPositionSniff.php +++ /dev/null @@ -1,168 +0,0 @@ -tokens = $phpcsFile->getTokens(); - $this->file = $phpcsFile; - $this->leftLimit = $open = $this->tokens[$stackPtr]['parenthesis_opener']; - $this->rightLimit = $close = $this->tokens[$stackPtr]['parenthesis_closer']; - for ($i = ($open + 1); $i < $close; $i++) { - if (($this->tokens[$i]['code'] === T_STRING && in_array($this->tokens[$i]['content'], $this->functions)) - && (!$this->findIdentical($i - 1, $this->findFunctionParenthesisCloser($i) + 1)) - ) { - $foundFunctionName = $this->tokens[$i]['content']; - $phpcsFile->addError($this->errorMessage, $i, $this->errorCode, [$foundFunctionName]); - } - } - } - - /** - * Recursively finds identical operators in current scope. - * - * @param int $leftCurrentPosition - * @param int $rightCurrentPosition - * @return bool - */ - protected function findIdentical($leftCurrentPosition, $rightCurrentPosition) - { - $leftBound = $this->file->findPrevious($this->leftRangeTokens, $leftCurrentPosition, $this->leftLimit - 1); - $rightBound = $this->file->findNext($this->rightRangeTokens, $rightCurrentPosition, $this->rightLimit + 1); - $leftToken = $this->tokens[$leftBound]; - $rightToken = $this->tokens[$rightBound]; - if ($leftToken['code'] === T_OPEN_PARENTHESIS && $rightToken['code'] === T_CLOSE_PARENTHESIS) { - return $this->findIdentical($leftBound - 1, $rightBound + 1); - } else { - return ( - in_array($leftToken['code'], $this->identical) || in_array($rightToken['code'], $this->identical) - ) ?: false; - } - } - - /** - * Finds the position of close parenthesis of detected function. - * - * @param int $currentPosition - * @return mixed - */ - protected function findFunctionParenthesisCloser($currentPosition) - { - $nextOpenParenthesis = $this->file->findNext(T_OPEN_PARENTHESIS, $currentPosition, $this->rightLimit); - return $nextOpenParenthesis ? $this->tokens[$nextOpenParenthesis]['parenthesis_closer'] : false; - } -} diff --git a/dev/tests/static/framework/Magento/Sniffs/Translation/ConstantUsageSniff.php b/dev/tests/static/framework/Magento/Sniffs/Translation/ConstantUsageSniff.php deleted file mode 100644 index b1e358e40482..000000000000 --- a/dev/tests/static/framework/Magento/Sniffs/Translation/ConstantUsageSniff.php +++ /dev/null @@ -1,95 +0,0 @@ -getTokens(); - - // Make sure this is the first open tag - $previousOpenTag = $phpcsFile->findPrevious(T_OPEN_TAG, ($stackPtr - 1)); - if ($previousOpenTag !== false) { - return; - } - - $tokenCount = 0; - $currentLineContent = ''; - $currentLine = 1; - - for (; $tokenCount < $phpcsFile->numTokens; $tokenCount++) { - if ($tokens[$tokenCount]['line'] === $currentLine) { - $currentLineContent .= $tokens[$tokenCount]['content']; - } else { - $this->checkIfFirstArgumentConstant($phpcsFile, ($tokenCount - 1), $currentLineContent); - $currentLineContent = $tokens[$tokenCount]['content']; - $currentLine++; - } - } - - $this->checkIfFirstArgumentConstant($phpcsFile, ($tokenCount - 1), $currentLineContent); - } - - /** - * Checks if first argument of \Magento\Framework\Phrase or translation function is a constant - * - * @param File $phpcsFile - * @param int $stackPtr - * @param string $lineContent - * @return void - */ - private function checkIfFirstArgumentConstant( - File $phpcsFile, - $stackPtr, - $lineContent - ) { - $previousLineRegexp = '/(__|Phrase)\($/im'; - $currentLineRegexp = '/(__|Phrase)\(.+\)/'; - $currentLineMatch = preg_match($currentLineRegexp, $lineContent) !== 0; - $previousLineMatch = preg_match($previousLineRegexp, $this->previousLineContent) !== 0; - $this->previousLineContent = $lineContent; - $error = 'Constants are not allowed as the first argument of translation function, use string literal instead'; - $constantRegexp = '[^\$\'"]+::[A-Z_0-9]+.*'; - if ($currentLineMatch) { - $variableRegexp = "/(__|Phrase)\({$constantRegexp}\)/"; - if (preg_match($variableRegexp, $lineContent) !== 0) { - $phpcsFile->addError($error, $stackPtr, 'VariableTranslation'); - } - } elseif ($previousLineMatch) { - $variableRegexp = "/^{$constantRegexp}/"; - if (preg_match($variableRegexp, $lineContent) !== 0) { - $phpcsFile->addError($error, $stackPtr, 'VariableTranslation'); - } - } - } -} diff --git a/dev/tests/static/framework/Magento/Sniffs/Variables/GlobalVariablesSniff.php b/dev/tests/static/framework/Magento/Sniffs/Variables/GlobalVariablesSniff.php deleted file mode 100644 index 2c4ce8f94af4..000000000000 --- a/dev/tests/static/framework/Magento/Sniffs/Variables/GlobalVariablesSniff.php +++ /dev/null @@ -1,40 +0,0 @@ -getTokens(); - if (preg_match('/^\$[_A-Z0-9]+$/', $tokens[$stackPtr]['content'])) { - $phpcsFile->addError( - 'Usage of global variables is not allowed: ' . $tokens[$stackPtr]['content'], - $stackPtr, - 'ERROR' - ); - return; - } - } -} diff --git a/dev/tests/static/framework/Magento/Sniffs/Whitespace/EmptyLineMissedSniff.php b/dev/tests/static/framework/Magento/Sniffs/Whitespace/EmptyLineMissedSniff.php deleted file mode 100644 index de3cfc50bbb5..000000000000 --- a/dev/tests/static/framework/Magento/Sniffs/Whitespace/EmptyLineMissedSniff.php +++ /dev/null @@ -1,66 +0,0 @@ -getTokens(); - if ($this->doCheck($phpcsFile, $stackPtr, $tokens)) { - $previous = $phpcsFile->findPrevious(T_WHITESPACE, $stackPtr - 1, null, true); - if ($tokens[$stackPtr]['line'] - $tokens[$previous]['line'] < 2) { - $error = 'Empty line missed'; - $phpcsFile->addError($error, $stackPtr, '', null); - } - } - } - - /** - * @param File $phpcsFile - * @param int $stackPtr - * @param array $tokens - * @return bool - */ - private function doCheck(File $phpcsFile, $stackPtr, $tokens) - { - $result = false; - if ($phpcsFile->hasCondition($stackPtr, T_CLASS) || $phpcsFile->hasCondition($stackPtr, T_INTERFACE)) { - $result = true; - } - - if ($phpcsFile->hasCondition($stackPtr, T_FUNCTION)) { - $result = false; - } - $previous = $phpcsFile->findPrevious(T_WHITESPACE, $stackPtr - 1, null, true); - if ($tokens[$previous]['type'] === 'T_OPEN_CURLY_BRACKET') { - $result = false; - } - - if (strpos($tokens[$stackPtr]['content'], '/**') === false) { - $result = false; - } - - return $result; - } -} diff --git a/dev/tests/static/framework/Magento/Sniffs/Whitespace/MultipleEmptyLinesSniff.php b/dev/tests/static/framework/Magento/Sniffs/Whitespace/MultipleEmptyLinesSniff.php deleted file mode 100644 index f276426efad6..000000000000 --- a/dev/tests/static/framework/Magento/Sniffs/Whitespace/MultipleEmptyLinesSniff.php +++ /dev/null @@ -1,49 +0,0 @@ -getTokens(); - if ($phpcsFile->hasCondition($stackPtr, T_FUNCTION) - || $phpcsFile->hasCondition($stackPtr, T_CLASS) - || $phpcsFile->hasCondition($stackPtr, T_INTERFACE) - ) { - if ($tokens[($stackPtr - 1)]['line'] < $tokens[$stackPtr]['line'] - && $tokens[($stackPtr - 2)]['line'] === $tokens[($stackPtr - 1)]['line'] - ) { - // This is an empty line and the line before this one is not - // empty, so this could be the start of a multiple empty line block - $next = $phpcsFile->findNext(T_WHITESPACE, $stackPtr, null, true); - $lines = $tokens[$next]['line'] - $tokens[$stackPtr]['line']; - if ($lines > 1) { - $error = 'Code must not contain multiple empty lines in a row; found %s empty lines'; - $data = [$lines]; - $phpcsFile->addError($error, $stackPtr, 'MultipleEmptyLines', $data); - } - } - } - } -} diff --git a/dev/tests/static/framework/Magento/TestFramework/CodingStandard/Tool/CodeSniffer.php b/dev/tests/static/framework/Magento/TestFramework/CodingStandard/Tool/CodeSniffer.php index 225be94958d0..2cd1d0969c4b 100644 --- a/dev/tests/static/framework/Magento/TestFramework/CodingStandard/Tool/CodeSniffer.php +++ b/dev/tests/static/framework/Magento/TestFramework/CodingStandard/Tool/CodeSniffer.php @@ -54,6 +54,7 @@ class CodeSniffer implements ToolInterface, ExtensionInterface public function __construct($rulesetDir, $reportFile, Wrapper $wrapper) { $this->rulesetDir = $rulesetDir; + // phpcs:ignore Magento2.Functions.DiscouragedFunction if (!file_exists($rulesetDir) && file_exists($fullPath = realpath(__DIR__ . '/../../../../' . $rulesetDir))) { $this->rulesetDir = $fullPath; } @@ -62,7 +63,7 @@ public function __construct($rulesetDir, $reportFile, Wrapper $wrapper) } /** - * {@inheritdoc} + * @inheritdoc */ public function setExtensions(array $extensions) { @@ -80,7 +81,7 @@ public function canRun() } /** - * {@inheritdoc} + * @inheritdoc */ public function run(array $whiteList) { @@ -97,10 +98,10 @@ public function run(array $whiteList) $settings['files'] = $whiteList; $settings['standards'] = [$this->rulesetDir]; $settings['extensions'] = $this->extensions; - $settings['warningSeverity'] = 0; $settings['reports']['full'] = $this->reportFile; $this->wrapper->setSettings($settings); + // phpcs:ignore Magento2.Functions.DiscouragedFunction ob_start(); $result = $this->wrapper->runPHPCS(); ob_end_clean(); diff --git a/dev/tests/static/framework/Magento/TestFramework/CodingStandard/Tool/CodeSniffer/Wrapper.php b/dev/tests/static/framework/Magento/TestFramework/CodingStandard/Tool/CodeSniffer/Wrapper.php index cb186f7cb80a..e11b85a6c20f 100644 --- a/dev/tests/static/framework/Magento/TestFramework/CodingStandard/Tool/CodeSniffer/Wrapper.php +++ b/dev/tests/static/framework/Magento/TestFramework/CodingStandard/Tool/CodeSniffer/Wrapper.php @@ -12,6 +12,9 @@ use PHP_CodeSniffer\Config; use PHP_CodeSniffer\Runner; +/** + * PHP Code Sniffer wrapper class + */ class Wrapper extends Runner { /** @@ -33,10 +36,21 @@ public function version() return $version; } + /** + * Initialize PHPCS runner and modifies the configuration settings + * + * @throws \PHP_CodeSniffer\Exceptions\DeepExitException + */ public function init() { $this->config->extensions = $this->settings['extensions']; unset($this->settings['extensions']); + + $settings = $this->config->getSettings(); + unset($settings['files']); + + $this->config->setSettings($settings); + $this->config->setSettings(array_replace_recursive( $this->config->getSettings(), $this->settings @@ -45,6 +59,8 @@ public function init() } /** + * Sets the settings + * * @param array $settings */ public function setSettings($settings) diff --git a/dev/tests/static/framework/Magento/TestFramework/Dependency/AnalyticsConfigRule.php b/dev/tests/static/framework/Magento/TestFramework/Dependency/AnalyticsConfigRule.php new file mode 100644 index 000000000000..b1a6da5e4382 --- /dev/null +++ b/dev/tests/static/framework/Magento/TestFramework/Dependency/AnalyticsConfigRule.php @@ -0,0 +1,43 @@ +]*class=[\'"]([^\'"]+)[\'"]#i', $contents, $matches)) { + $classes = array_pop($matches); + foreach ($classes as $class) { + $classParts = explode('\\', $class); + $module = implode('\\', array_slice($classParts, 0, 2)); + if (strtolower($currentModule) !== strtolower($module)) { + $dependenciesInfo[] = [ + 'module' => $module, + 'type' => RuleInterface::TYPE_HARD, + 'source' => $file, + ]; + } + } + } + + return $dependenciesInfo; + } +} diff --git a/dev/tests/static/framework/Magento/TestFramework/Dependency/DeclarativeSchemaRule.php b/dev/tests/static/framework/Magento/TestFramework/Dependency/DeclarativeSchemaRule.php deleted file mode 100644 index 72e4fff7e516..000000000000 --- a/dev/tests/static/framework/Magento/TestFramework/Dependency/DeclarativeSchemaRule.php +++ /dev/null @@ -1,101 +0,0 @@ -_moduleTableMap = $tables; - } - - /** - * Gets external dependencies information for current module by analyzing db_schema.xml files contents. - * - * @param string $currentModule - * @param string $fileType - * @param string $file - * @param string $contents - * @return array - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) - */ - public function getDependencyInfo($currentModule, $fileType, $file, &$contents) - { - if ('db_schema' != $fileType || !preg_match('#.*/db_schema\.xml$#', $file)) { - return []; - } - - $dependenciesInfo = []; - $unKnowTables = []; - - $dom = new \DOMDocument(); - $dom->loadXML($contents); - $tables = $dom->getElementsByTagName('table'); - $constraints = $dom->getElementsByTagName('constraint'); - - $tableNames = []; - $foreignKeyTables = []; - $foreignKeyReferenceTables = []; - - /** @var \DOMElement $table */ - foreach ($tables as $table) { - $tableNames[] = $table->getAttribute('name'); - } - - /** @var \DOMElement $constraint */ - foreach ($constraints as $constraint) { - $xsiType = $constraint->getAttribute('xsi:type'); - if (strtolower($xsiType) == 'foreign' && $constraint->getAttribute('disabled') !== '1') { - $foreignKeyTables[] = $constraint->getAttribute('table'); - $foreignKeyReferenceTables[] = $constraint->getAttribute('referenceTable'); - } - } - - $tableNames = array_unique(array_merge($tableNames, $foreignKeyReferenceTables, $foreignKeyTables)); - - /** @var string $table */ - foreach ($tableNames as $table) { - if (!isset($this->_moduleTableMap[$table])) { - $unKnowTables[$file][$table] = $table; - continue; - } - if (strtolower($currentModule) !== strtolower($this->_moduleTableMap[$table])) { - $dependenciesInfo[] = [ - 'module' => $this->_moduleTableMap[$table], - 'type' => RuleInterface::TYPE_HARD, - 'source' => $table, - ]; - } - } - - foreach ($unKnowTables as $tables) { - foreach ($tables as $table) { - $dependenciesInfo[] = ['module' => 'Unknown', 'source' => $table]; - } - } - return $dependenciesInfo; - } -} diff --git a/dev/tests/static/framework/Magento/TestFramework/Dependency/DiRule.php b/dev/tests/static/framework/Magento/TestFramework/Dependency/DiRule.php index cdaa49e8d37f..b3ee0a288030 100644 --- a/dev/tests/static/framework/Magento/TestFramework/Dependency/DiRule.php +++ b/dev/tests/static/framework/Magento/TestFramework/Dependency/DiRule.php @@ -7,11 +7,13 @@ */ namespace Magento\TestFramework\Dependency; -use DOMDocument; -use DOMXPath; +use Magento\Framework\App\Utility\Classes; use Magento\Framework\App\Utility\Files; use Magento\TestFramework\Dependency\VirtualType\VirtualTypeMapper; +/** + * Class provide dependency rule for di.xml config files. + */ class DiRule implements RuleInterface { /** @@ -33,6 +35,8 @@ public function __construct(VirtualTypeMapper $mapper) } /** + * Get class name pattern. + * * @return string * @throws \Exception */ @@ -73,6 +77,7 @@ private function getPattern() */ public function getDependencyInfo($currentModule, $fileType, $file, &$contents) { + //phpcs:ignore Magento2.Functions.DiscouragedFunction if (pathinfo($file, PATHINFO_BASENAME) !== 'di.xml') { return []; } @@ -99,12 +104,14 @@ public function getDependencyInfo($currentModule, $fileType, $file, &$contents) } /** + * Fetch all possible dependencies. + * * @param string $contents * @return array */ private function fetchPossibleDependencies($contents) { - $doc = new DOMDocument(); + $doc = new \DOMDocument(); $doc->loadXML($contents); return [ RuleInterface::TYPE_SOFT => $this->getSoftDependencies($doc), @@ -113,16 +120,22 @@ private function fetchPossibleDependencies($contents) } /** - * @param DOMDocument $doc + * Collect soft dependencies. + * + * @param \DOMDocument $doc * @return array */ - private function getSoftDependencies(DOMDocument $doc) + private function getSoftDependencies(\DOMDocument $doc) { $result = []; foreach (self::$tagNameMap as $tagName => $attributeNames) { $nodes = $doc->getElementsByTagName($tagName); /** @var \DOMElement $node */ foreach ($nodes as $node) { + if ($tagName === 'virtualType' && !$node->getAttribute('type')) { + $result[] = Classes::resolveVirtualType($node->getAttribute('name')); + continue; + } foreach ($attributeNames as $attributeName) { $result[] = $node->getAttribute($attributeName); } @@ -133,13 +146,15 @@ private function getSoftDependencies(DOMDocument $doc) } /** - * @param DOMDocument $doc + * Collect hard dependencies. + * + * @param \DOMDocument $doc * @return array */ - private function getHardDependencies(DOMDocument $doc) + private function getHardDependencies(\DOMDocument $doc) { $result = []; - $xpath = new DOMXPath($doc); + $xpath = new \DOMXPath($doc); $textNodes = $xpath->query('//*[@xsi:type="object"]'); /** @var \DOMElement $node */ foreach ($textNodes as $node) { diff --git a/dev/tests/static/framework/Magento/TestFramework/Utility/XssOutputValidator.php b/dev/tests/static/framework/Magento/TestFramework/Utility/XssOutputValidator.php deleted file mode 100644 index 516f169227a9..000000000000 --- a/dev/tests/static/framework/Magento/TestFramework/Utility/XssOutputValidator.php +++ /dev/null @@ -1,441 +0,0 @@ -getXssUnsafeBlocks($fileContent); - - $lines = []; - foreach ($xssUnsafeBlocks as $block) { - $lines = array_merge($lines, $this->findBlockLineNumbers($block, $fileContent)); - } - - if (count($lines)) { - $lines = array_unique($lines); - sort($lines); - return implode(',', $lines); - } - - return ''; - } - - /** - * Find block line numbers - * - * @param string $block - * @param string $content - * @return array - */ - private function findBlockLineNumbers($block, $content) - { - $results = []; - $pos = strpos($content, $block, 0); - while ($pos !== false) { - $contentBeforeString = substr($content, 0, $pos); - if ($this->isNotEscapeMarkedBlock($contentBeforeString) - && $this->isNotInCommentBlock($contentBeforeString) - ) { - $results[] = count(explode(PHP_EOL, $contentBeforeString)); - } - $pos = strpos($content, $block, $pos + 1); - } - - return $results; - } - - /** - * Get XSS unsafe output blocks - * - * @param string $fileContent - * @return array - */ - public function getXssUnsafeBlocks($fileContent) - { - $results = []; - - $fileContent = $this->replacePhpQuoteWithPlaceholders($fileContent); - $fileContent = $this->replacePhpCommentsWithPlaceholders($fileContent); - - $this->addOriginReplacement('\'\'', "'-*=single=*-'"); - $this->addOriginReplacement('""', '"-*=double=*-"'); - - if (preg_match_all('/<[?](php|=)(.*?)[?]>/sm', $fileContent, $phpBlockMatches)) { - foreach ($phpBlockMatches[2] as $index => $phpBlock) { - $phpCommands = explode(';', $phpBlock); - if ($phpBlockMatches[1][$index] == 'php') { - $echoCommands = preg_grep('#( |^|/\*.*?\*/)echo[\s(]+.*#sm', $phpCommands); - } else { - $echoCommands[] = $phpBlockMatches[0][$index]; - } - $results = array_merge( - $results, - $this->getEchoUnsafeCommands($echoCommands) - ); - } - } - - $this->clearOriginReplacements(); - $results = array_unique($results); - - return $results; - } - - /** - * @param array $echoCommands - * @return array - */ - private function getEchoUnsafeCommands(array $echoCommands) - { - $results = []; - foreach ($echoCommands as $echoCommand) { - if ($this->isNotEscapeMarkedCommand($echoCommand)) { - $echoCommand = preg_replace('/^(.*?)echo/sim', 'echo', $echoCommand); - $preparedEchoCommand = $this->prepareEchoCommand($echoCommand); - $isEscapeFunctionArgument = preg_match( - '/->(' . implode('|', $this->escapeFunctions) . ')\(.*?\)$/sim', - $preparedEchoCommand - ); - $xssUnsafeCommands = array_filter( - $isEscapeFunctionArgument ? [$preparedEchoCommand] : explode('.', $preparedEchoCommand), - [$this, 'isXssUnsafeCommand'] - ); - if (count($xssUnsafeCommands)) { - $results[] = str_replace( - $this->getReplacements(), - $this->getOrigins(), - $echoCommand - ); - } - } - } - - return $results; - } - - /** - * @param string $command - * @return string - */ - private function prepareEchoCommand($command) - { - $command = preg_replace('/<[?]=(.*?)[?]>/sim', '\1', $command); - return trim(ltrim(explode(';', $command)[0], 'echo')); - } - - /** - * @param string $contentBeforeString - * @return bool - */ - private function isNotEscapeMarkedBlock($contentBeforeString) - { - return !preg_match( - '%(' . self::ESCAPE_NOT_VERIFIED_PATTERN . '|' . self::ESCAPED_PATTERN . ')$%sim', - trim($contentBeforeString) - ); - } - - /** - * @param string $contentBeforeString - * @return bool - */ - private function isNotInCommentBlock($contentBeforeString) - { - $contentBeforeString = explode('(' . implode('|', $this->escapeFunctions) . '|.*html.*)\(/simU', - $this->getLastMethod($command) - ): - return false; - case preg_match('/^\((int|bool|float)\)/sim', $command): - return false; - case preg_match('/^count\(/sim', $command): - return false; - case preg_match("/^'.*'$/sim", $command): - return false; - case preg_match('/^".*?"$/sim', $command, $matches): - return $this->isContainPhpVariables($this->getOrigin($matches[0])); - default: - return true; - } - } - - /** - * @param string $command - * @return string - */ - private function getLastMethod($command) - { - if (preg_match_all( - '/->.*?\(.*?\)/sim', - $this->clearMethodBracketContent($command), - $matches - )) { - $command = end($matches[0]); - $command = substr($command, 0, strpos($command, '(') + 1); - } - - return $command; - } - - /** - * @param string $command - * @return string - */ - private function clearMethodBracketContent($command) - { - $bracketInterval = []; - $bracketOpenPos = []; - $command = str_split($command); - foreach ($command as $index => $character) { - if ($character == '(') { - array_push($bracketOpenPos, $index); - } - if (count($bracketOpenPos)) { - if ($character == ')') { - $lastOpenPos = array_pop($bracketOpenPos); - if (count($bracketOpenPos) == 0) { - $bracketInterval[] = [$lastOpenPos, $index]; - } - } - } - } - foreach ($bracketInterval as $interval) { - for ($i = $interval[0] + 1; $i < $interval[1]; $i++) { - unset($command[$i]); - } - } - $command = implode('', $command); - - return $command; - } - - /** - * @param string $content - * @return int - */ - private function isContainPhpVariables($content) - { - return preg_match('/[^\\\\]\$[a-z_\x7f-\xff]/sim', $content); - } - - /** - * @param string $fileContent - * @return string - */ - private function replacePhpQuoteWithPlaceholders($fileContent) - { - $origins = []; - $replacements = []; - if (preg_match_all('/<[?](php|=)(.*?)[?]>/sm', $fileContent, $phpBlockMatches)) { - foreach ($phpBlockMatches[2] as $phpBlock) { - $phpBlockQuoteReplaced = preg_replace( - ['/([^\\\\])\'\'/si', '/([^\\\\])""/si'], - ["\1'-*=single=*-'", '\1"-*=double=*-"'], - $phpBlock - ); - - $this->addQuoteOriginsReplacements( - $phpBlockQuoteReplaced, - [ - '/([^\\\\])([\'])(.*?)([^\\\\])([\'])/sim' - ] - ); - $this->addQuoteOriginsReplacements( - $phpBlockQuoteReplaced, - [ - '/([^\\\\])(["])(.*?)([^\\\\])(["])/sim', - ] - ); - - $origins[] = $phpBlock; - $replacements[] = str_replace( - $this->getOrigins(), - $this->getReplacements(), - $phpBlockQuoteReplaced - ); - } - } - - return str_replace($origins, $replacements, $fileContent); - } - - /** - * @param string $fileContent - * @return string - */ - private function replacePhpCommentsWithPlaceholders($fileContent) - { - $origins= []; - $replacements = []; - if (preg_match_all('%/\*.*?\*/%simu', $fileContent, $docCommentMatches, PREG_SET_ORDER)) { - foreach ($docCommentMatches as $docCommentMatch) { - if ($this->isNotEscapeMarkedCommand($docCommentMatch[0]) - && !$this->issetOrigin($docCommentMatch[0])) { - $origin = $docCommentMatch[0]; - $replacement = '-*!' . count($this->getOrigins()) . '!*-'; - $origins[] = $origin; - $replacements[] = $replacement; - $this->addOriginReplacement( - $origin, - $replacement - ); - } - } - } - - return str_replace($origins, $replacements, $fileContent); - } - - /** - * Add replacements for expressions in single and double quotes - * - * @param string $phpBlock - * @param array $patterns - * @return void - */ - private function addQuoteOriginsReplacements($phpBlock, array $patterns) - { - foreach ($patterns as $pattern) { - if (preg_match_all($pattern, $phpBlock, $quoteMatches, PREG_SET_ORDER)) { - foreach ($quoteMatches as $quoteMatch) { - $origin = $quoteMatch[2] . $quoteMatch[3] . $quoteMatch[4] . $quoteMatch[5]; - if (!$this->issetOrigin($origin)) { - $this->addOriginReplacement( - $origin, - $quoteMatch[2] . '-*=' . count($this->getOrigins()) . '=*-' . $quoteMatch[5] - ); - } - } - } - } - } - - /** - * @param string $origin - * @param string $replacement - * @return void - */ - private function addOriginReplacement($origin, $replacement) - { - $this->origins[$replacement] = $origin; - $this->replacements[$replacement] = $replacement; - } - - /** - * Clear origins and replacements - * - * @return void - */ - private function clearOriginReplacements() - { - $this->origins = []; - $this->replacements = []; - } - - /** - * @return array - */ - private function getOrigins() - { - return $this->origins; - } - - /** - * @param string $key - * @return string|null - */ - private function getOrigin($key) - { - return array_key_exists($key, $this->origins) ? $this->origins[$key] : null; - } - - /** - * @param string $origin - * @return bool - */ - private function issetOrigin($origin) - { - return in_array($origin, $this->origins); - } - - /** - * @return array - */ - private function getReplacements() - { - return $this->replacements; - } -} diff --git a/dev/tests/static/framework/Magento/ruleset.xml b/dev/tests/static/framework/Magento/ruleset.xml index f3e4893ba752..70d1810d1eb2 100644 --- a/dev/tests/static/framework/Magento/ruleset.xml +++ b/dev/tests/static/framework/Magento/ruleset.xml @@ -7,62 +7,12 @@ --> Custom Magento coding standard. + - - - - - - - - - - */app/code/*\.(?!phtml) - - + + */_files/* + */Test/* + *Test.php - - - * - - - */(app/code|vendor|setup/src|lib/internal/Magento)/* - */lib/internal/Magento/Framework/Image/Adapter/Gd2.php - */lib/internal/Magento/Framework/View/Result/Page.php - */lib/internal/Magento/Framework/View/TemplateEngine/Php.php - */Test/Unit/* - - - */(app/code|vendor|setup/src)/* - */setup/src/Magento/Setup/Controller/WebConfiguration.php - */setup/src/Magento/Setup/Mvc/Bootstrap/InitParamListener.php - */app/code/Magento/Eav/Model/Attribute/Data/File.php - */app/code/Magento/Config/Model/Config/Reader/Source/Deployed/SettingChecker.php - */app/code/Magento/Backend/App/Area/FrontNameResolver.php - */app/code/Magento/Indexer/Console/Command/AbstractIndexerCommand.php - */app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFile.php - */app/code/Magento/Cron/Console/Command/CronCommand.php - */app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php - */app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate.php - */app/code/Magento/Store/Model/Store.php - */app/code/Magento/Config/Model/Config/Processor/EnvironmentPlaceholder.php - */app/code/Magento/Config/Model/Config/Backend/Email/Logo.php - */app/code/Magento/Config/Model/Config/Backend/File/RequestData.php - */app/code/Magento/Config/App/Config/Source/EnvironmentConfigSource.php - */app/code/Magento/Theme/Controller/Adminhtml/Design/Config/FileUploader/Save.php - */app/code/Magento/Customer/Model/Metadata/Form/File.php - */app/code/Magento/Customer/Model/FileUploader.php - */app/code/Magento/Customer/Controller/Adminhtml/File/Address/Upload.php - */app/code/Magento/Customer/Controller/Adminhtml/File/Customer/Upload.php - */Test/Unit/* - - - - - - - - - diff --git a/dev/tests/static/framework/bootstrap.php b/dev/tests/static/framework/bootstrap.php index 1fe48e5a36ce..5e3336ebc28d 100644 --- a/dev/tests/static/framework/bootstrap.php +++ b/dev/tests/static/framework/bootstrap.php @@ -14,6 +14,10 @@ require __DIR__ . '/autoload.php'; +if (!defined('TESTS_TEMP_DIR')) { + define('TESTS_TEMP_DIR', __DIR__ . DIRECTORY_SEPARATOR . 'tmp'); +} + setCustomErrorHandler(); $componentRegistrar = new ComponentRegistrar(); diff --git a/dev/tests/static/framework/tests/unit/phpunit.xml.dist b/dev/tests/static/framework/tests/unit/phpunit.xml.dist index 57c13a6b3931..aca48f6f8d1d 100644 --- a/dev/tests/static/framework/tests/unit/phpunit.xml.dist +++ b/dev/tests/static/framework/tests/unit/phpunit.xml.dist @@ -20,4 +20,32 @@ + + + + var/allure-results + true + + + magentoAdminConfigFixture + + + magentoAppIsolation + + + magentoComponentsDir + + + magentoConfigFixture + + + magentoDataFixture + + + magentoDbIsolation + + + + + diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Annotation/ClassAnnotationStructureSniffTest.php b/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Annotation/ClassAnnotationStructureSniffTest.php new file mode 100644 index 000000000000..b12cb1fbfcbd --- /dev/null +++ b/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Annotation/ClassAnnotationStructureSniffTest.php @@ -0,0 +1,85 @@ +copyFile( + __DIR__ . DIRECTORY_SEPARATOR . '_files'. DIRECTORY_SEPARATOR, + TESTS_TEMP_DIR + ); + $codeSniffer = new \Magento\TestFramework\CodingStandard\Tool\CodeSniffer( + 'Magento', + $reportFile, + new \Magento\TestFramework\CodingStandard\Tool\CodeSniffer\Wrapper() + ); + $result = $codeSniffer->run( + [TESTS_TEMP_DIR . $fileUnderTest] + ); + $actual = file_get_contents($reportFile); + $expected = file_get_contents( + TESTS_TEMP_DIR . $expectedReportFile + ); + unlink($reportFile); + $this->assertEquals(2, $result); + $this->assertEquals($expected, $actual); + } +} diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Annotation/MethodAnnotationStructureSniffTest.php b/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Annotation/MethodAnnotationStructureSniffTest.php new file mode 100644 index 000000000000..b56239f4df8a --- /dev/null +++ b/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Annotation/MethodAnnotationStructureSniffTest.php @@ -0,0 +1,75 @@ +copyFile( + __DIR__ . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR, + TESTS_TEMP_DIR + ); + $codeSniffer = new \Magento\TestFramework\CodingStandard\Tool\CodeSniffer( + 'Magento', + $reportFile, + new \Magento\TestFramework\CodingStandard\Tool\CodeSniffer\Wrapper() + ); + $result = $codeSniffer->run( + [TESTS_TEMP_DIR . $fileUnderTest] + ); + $actual = file_get_contents($reportFile); + $expected = file_get_contents( + TESTS_TEMP_DIR . $expectedReportFile + ); + unlink($reportFile); + $this->assertEquals(2, $result); + $this->assertEquals($expected, $actual); + } +} diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Annotation/_files/AbstractClassAnnotationFixture.php b/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Annotation/_files/AbstractClassAnnotationFixture.php new file mode 100644 index 000000000000..43973b3083e5 --- /dev/null +++ b/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Annotation/_files/AbstractClassAnnotationFixture.php @@ -0,0 +1,13 @@ +productVisibility = $productVisibility; + } + + /** + * block description + * + * {@inheritdoc} + * + * @param \Magento\Catalog\Model\ResourceModel\Product\Collection $collection + * @return void + */ + public function construct(AbstractDb $collection) + { + /** @var */ + $collection->setVisibility($this->productVisibility->getVisibleInCatalogIds()); + } + + /** + * Move category + * + * + * @param int $parentId new parent category id + * + * @return $this + * @throws \Magento\Framework\Exception\LocalizedException|\Exception + */ + public function move($parentId) + { + /** + * Validate new parent category id. (category model is used for backward + * compatibility in event params) + */ + try { + $this->categoryRepository->get($parentId, $this->getStoreId()); + } catch (NoSuchEntityException $e) { + throw new \Magento\Framework\Exception\LocalizedException( + __('Sorry, but we can\'t find the new parent category you selected.'), + $e + ); + } + return true; + } + + /** + * Block for short description + * + * This a long description {@inheritdoc} consists more lines as part of the long description + * on multi line. + * + * @param int $store + * + * + */ + public function getProductListDefaultSortBy26032($store) + { + return $store; + } + + /** + * + * + * + */ + public function getProductListDefaultSortBy2632() + { + } + + /** + * Block for short description + * + * This a long description {@inheritdoc} consists more lines as part of the long description + * on multi line. + * + * @param int $store + * + * + * + */ + public function getProductListDefaultSortBy2002($store) + { + return $store; + } + + /** + * + * block for short description + * + * @param int $store + * @return int + */ + public function getProductListDefaultSortBy3002($store) + { + return $store; + } + + /** + * Block for short description + * + * @see consists more lines as part of the long description + * on multi line. + * + * @param string $store + * @param string $foo + */ + public function getProductListDefaultSortBy12($store, $foo) + { + return $store === $foo; + } + + /** + * Block for short description + * + * {@inheritdoc} + * + * @param string $store + * @param string $foo + */ + public function getProductListDefaultSort2($store, $foo) + { + return $store === $foo; + } + + /** + * Block for short description + * + * a long description {@inheritdoc} consists more lines as part of the long description + * on multi line. + * + * @param string $store + * @param string $foo + */ + public function getProductListDefault($store, $foo) + { + return $store === $foo; + } + + /** + * Retrieve custom options + * + * @param ProductOptionInterface $productOption + * + * @return array + */ + protected function getCustomOptions(ProductOptionInterface $productOption) + { + if ($productOption + && $productOption->getExtensionAttributes() + && $productOption->getExtensionAttributes()->getCustomOptions() + ) { + return $productOption->getExtensionAttributes() + ->getCustomOptions(); + } + return []; + } + + /** + * This is the summary for a DocBlock. + * + * This is the description for a DocBlock. This text may contain + * multiple lines and even some _markdown_. + * * Markdown style lists function too + * * Just try this out once + * The section after the description contains the tags; which provide + * structured meta-data concerning the given element. + * + * @param int $example This is an example function/method parameter description. + * @param string $example2 This is a second example. + * + */ + public function getProductListDefaultSortBy2($example, $example2) + { + return $example === $example2; + } + + /** + * Returns the content of the tokens from the specified start position in + * the token stack for the specified length. + * + * @param int $start + * @param int $length + * + * @return string The token contents. + */ + public function getProductListDefaultSortBy($start, $length) + { + return $start === $length; + } + + /** + * Some text about this step/method returns the content of the tokens the token stack for the specified length + * + * @param string $name + * @param string $folder + * + * @see this file + * @When I create a file called :name in :folder + */ + public function getProductListDefaultSortBy222($name, $folder) + { + return $name === $folder; + } + + public function setExtensionAs(\Magento\Catalog\Api\Data\CategoryExtensionInterface $extensionAttributes) + { + return $this->_setExtensionAttributes($extensionAttributes); + } + + /** + * + * short description + * @param \Magento\Catalog\Api\Data\CategoryExtensionInterface $extensionAttributes + * @return mixed + */ + public function setEn(\Magento\Catalog\Api\Data\CategoryExtensionInterface $extensionAttributes) + { + return $this->_setExtensionAttributes($extensionAttributes); + } + + /** + * @param \Magento\Catalog\Api\Data\CategoryExtensionInterface $extensionAttributes + * @return mixed + */ + public function setExtenw(\Magento\Catalog\Api\Data\CategoryExtensionInterface $extensionAttributes) + { + return $this->_setExtensionAttributes($extensionAttributes); + } + + /** + * + * Short description + * @param \Magento\Catalog\Api\Data\CategoryExtensionInterface $extensionAttributes + * @return mixed + */ + public function setExff(\Magento\Catalog\Api\Data\CategoryExtensionInterface $extensionAttributes) + { + return $this->_setExtensionAttributes($extensionAttributes); + } + + /** + * {@inheritdoc} + * + * @param int $start + * @param int $length + * + * @return string The token contents. + */ + public function getProductSortBy($start, $length) + { + return $start === $length; + } +} diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Annotation/_files/abstract_class_annotation_errors.txt b/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Annotation/_files/abstract_class_annotation_errors.txt new file mode 100644 index 000000000000..23698cfb72e2 --- /dev/null +++ b/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Annotation/_files/abstract_class_annotation_errors.txt @@ -0,0 +1,10 @@ +FILE: ...o/Sniffs/Annotation/_fixtures/AbstractClassAnnotationFixture.php +---------------------------------------------------------------------- +FOUND 1 ERROR AFFECTING 1 LINE +---------------------------------------------------------------------- + 11 | ERROR | [x] Interface or abstract class is missing annotation + | | block +---------------------------------------------------------------------- +PHPCBF CAN FIX THE 1 MARKED SNIFF VIOLATIONS AUTOMATICALLY\n + + diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Annotation/_files/class_annotation_errors.txt b/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Annotation/_files/class_annotation_errors.txt new file mode 100644 index 000000000000..aca272113160 --- /dev/null +++ b/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Annotation/_files/class_annotation_errors.txt @@ -0,0 +1,11 @@ + +FILE: ...Magento/Sniffs/Annotation/_fixtures/class_annotation_fixture.php +---------------------------------------------------------------------- +FOUND 1 ERROR AFFECTING 1 LINE +---------------------------------------------------------------------- + 11 | ERROR | [x] Class is missing annotation block +---------------------------------------------------------------------- +PHPCBF CAN FIX THE 1 MARKED SNIFF VIOLATIONS AUTOMATICALLY +---------------------------------------------------------------------- + + diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Annotation/_files/class_annotation_noshortdescription_errors.txt b/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Annotation/_files/class_annotation_noshortdescription_errors.txt new file mode 100644 index 000000000000..ecc702c56893 --- /dev/null +++ b/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Annotation/_files/class_annotation_noshortdescription_errors.txt @@ -0,0 +1,14 @@ +'\n +FILE: ...nnotation/_fixtures/ClassAnnotationNoShortDescriptionFixture.php\n +----------------------------------------------------------------------\n +FOUND 1 ERROR AFFECTING 1 LINE\n +----------------------------------------------------------------------\n + 11 | ERROR | [x] Missing short description\n +----------------------------------------------------------------------\n +PHPCBF CAN FIX THE 1 MARKED SNIFF VIOLATIONS AUTOMATICALLY\n +----------------------------------------------------------------------\n +\n +\n +' + + diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Annotation/_files/class_annotation_nospacingbetweenLines_errors.txt b/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Annotation/_files/class_annotation_nospacingbetweenLines_errors.txt new file mode 100644 index 000000000000..01c145ad6c29 --- /dev/null +++ b/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Annotation/_files/class_annotation_nospacingbetweenLines_errors.txt @@ -0,0 +1,12 @@ +'\n +FILE: ...tation/_fixtures/ClassAnnotationNoSpacingBetweenLinesFixture.php\n +----------------------------------------------------------------------\n +FOUND 1 ERROR AFFECTING 1 LINE\n +----------------------------------------------------------------------\n + 13 | ERROR | [x] There must be exactly one blank line between lines\n +----------------------------------------------------------------------\n +PHPCBF CAN FIX THE 1 MARKED SNIFF VIOLATIONS AUTOMATICALLY\n +----------------------------------------------------------------------\n +\n +\n +' \ No newline at end of file diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Annotation/_files/method_annotation_errors.txt b/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Annotation/_files/method_annotation_errors.txt new file mode 100644 index 000000000000..65f75f6564a6 --- /dev/null +++ b/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Annotation/_files/method_annotation_errors.txt @@ -0,0 +1,55 @@ +FILE: .../Magento/Sniffs/Annotation/_fixtures/MethodAnnotationFixture.php\n +----------------------------------------------------------------------\n +FOUND 29 ERRORS AFFECTING 26 LINES +---------------------------------------------------------------------- + 18 | ERROR | [x] No blank lines are allowed before short + | | description + 27 | ERROR | [x] {@inheritdoc} imports only short description, + | | annotation must have long description + 29 | ERROR | [x] Additional blank lines found at end of the + | | annotation block + 35 | ERROR | [x] {@inheritdoc} imports only short description, + | | annotation must have long description + 44 | ERROR | [x] No blank lines are allowed before short + | | description + 44 | ERROR | [x] {@inheritdoc} imports only short description, + | | annotation must have long description + 46 | ERROR | [x] Additional blank lines found at end of the + | | annotation block + 54 | ERROR | [x] There must be exactly one blank line before tags + 62 | ERROR | [x] Short description must start with a capital letter + 64 | ERROR | [x] {@inheritdoc} imports only short description, + | | annotation must have long description + 79 | ERROR | [x] There must be exactly one blank line before tags + 110 | ERROR | [x] Additional blank lines found at end of the + | | annotation block + 116 | ERROR | [ ] Annotation block is empty + 135 | ERROR | [x] Additional blank lines found at end of the + | | annotation block + 143 | ERROR | [x] No blank lines are allowed before short + | | description + 143 | ERROR | [x] Short description must start with a capital letter + 170 | ERROR | [x] {@inheritdoc} imports only short description, + | | annotation must have long description + 183 | ERROR | [x] Long description must start with a capital letter + 226 | ERROR | [x] Additional blank lines found at end of the + | | annotation block + 234 | ERROR | [x] Short description should not be in multi lines + 260 | ERROR | [ ] Comment block is missing + 267 | ERROR | [x] No blank lines are allowed before short + | | description + 267 | ERROR | [x] Short description must start with a capital letter + 268 | ERROR | [x] There must be exactly one blank line before tags + 276 | ERROR | [x] Missing short description + 277 | ERROR | [x] There must be exactly one blank line before tags + 287 | ERROR | [x] No blank lines are allowed before short + | | description + 288 | ERROR | [x] There must be exactly one blank line before tags + 297 | ERROR | [x] {@inheritdoc} imports only short description, + | | annotation must have long description +---------------------------------------------------------------------- +PHPCBF CAN FIX THE 27 MARKED SNIFF VIOLATIONS AUTOMATICALLY +---------------------------------------------------------------------- +\n +\n +' \ No newline at end of file diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/EchoTags/ShortEchoSyntaxSniffTest.php b/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/EchoTags/ShortEchoSyntaxSniffTest.php deleted file mode 100644 index 0601c3eb2669..000000000000 --- a/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/EchoTags/ShortEchoSyntaxSniffTest.php +++ /dev/null @@ -1,82 +0,0 @@ -fileMock = $this->getMock(\PHP_CodeSniffer_File::class, [], [], '', false); - $this->shortEchoUsageSniff = new ShortEchoSyntaxSniff(); - } - - /** - * @param string $file - * @param int $incorrectUsages - * @dataProvider processDataProvider - */ - public function testEchoTagSniff($file, $stackPtr, $incorrectUsages) - { - $fileContent = file_get_contents(__DIR__ . '/_files/' . $file); - $tokens = $this->tokenizeString($fileContent); - - $this->fileMock->expects($this->any()) - ->method('findNext') - ->with([T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], $stackPtr + 1, null, true) - ->willReturn($stackPtr + 1); - - $this->fileMock->expects($this->once()) - ->method('getTokens') - ->willReturn($tokens); - - $this->fileMock->expects($this->exactly($incorrectUsages)) - ->method('addError') - ->with('Short echo tag syntax must be used; expected "shortEchoUsageSniff->process($this->fileMock, $stackPtr); - } - - /** - * Get tokens for a string - * - * @param string $fileContent - * @return array - */ - private function tokenizeString($fileContent) - { - $tokens = token_get_all($fileContent); - $snifferTokens = []; - for ($i = 0; $i < count($tokens); $i++) { - $code = is_array($tokens[$i]) ? $tokens[$i][0] : $tokens[$i]; - $content = is_array($tokens[$i]) ? $tokens[$i][1] : $tokens[$i]; - $snifferTokens[$i]['code'] = $code; - $snifferTokens[$i]['content'] = $content; - } - return $snifferTokens; - } - - /** - * @return array - */ - public function processDataProvider() - { - return [ - ['incorrect_echotag.txt', 1, 1], - ['correct_noecho.txt', 1, 0], - ['correct_echotag.txt', 1, 0] - ]; - } -} diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/EchoTags/_files/correct_echotag.txt b/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/EchoTags/_files/correct_echotag.txt deleted file mode 100644 index fd8027e1e23e..000000000000 --- a/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/EchoTags/_files/correct_echotag.txt +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - - diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/EchoTags/_files/correct_noecho.txt b/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/EchoTags/_files/correct_noecho.txt deleted file mode 100644 index 5b003e1e5cfb..000000000000 --- a/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/EchoTags/_files/correct_noecho.txt +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - - diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/EchoTags/_files/incorrect_echotag.txt b/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/EchoTags/_files/incorrect_echotag.txt deleted file mode 100644 index 84d72334bf85..000000000000 --- a/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/EchoTags/_files/incorrect_echotag.txt +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - - diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Less/AvoidIdSniffTest.php b/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Less/AvoidIdSniffTest.php new file mode 100644 index 000000000000..ad9e8edd4d1b --- /dev/null +++ b/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Less/AvoidIdSniffTest.php @@ -0,0 +1,55 @@ +setExtensions([LessWrapper::LESS_FILE_EXTENSION]); + $result = $codeSniffer->run( + [__DIR__ . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . $fileUnderTest] + ); + // Remove the absolute path to the file from the output + $actual = preg_replace('/^.+\n/', '', ltrim(file_get_contents($reportFile))); + $expected = file_get_contents( + __DIR__ . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . $expectedReportFile + ); + unlink($reportFile); + $this->assertEquals(1, $result); + $this->assertEquals($expected, $actual); + } +} diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Less/_files/avoid-ids-errors.txt b/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Less/_files/avoid-ids-errors.txt new file mode 100644 index 000000000000..ebc01c4e0852 --- /dev/null +++ b/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Less/_files/avoid-ids-errors.txt @@ -0,0 +1,26 @@ +--------------------------------------------------------------------------------------------------------------------------------- +FOUND 20 ERRORS AFFECTING 20 LINES +--------------------------------------------------------------------------------------------------------------------------------- + 6 | ERROR | Id selector is used + 7 | ERROR | Id selector is used + 8 | ERROR | Id selector is used + 9 | ERROR | Id selector is used + 10 | ERROR | Id selector is used + 11 | ERROR | Id selector is used + 12 | ERROR | Id selector is used + 13 | ERROR | Id selector is used + 14 | ERROR | Id selector is used + 15 | ERROR | Id selector is used + 16 | ERROR | Id selector is used + 17 | ERROR | Id selector is used + 18 | ERROR | Id selector is used + 19 | ERROR | Id selector is used + 20 | ERROR | Id selector is used + 21 | ERROR | Id selector is used + 22 | ERROR | Id selector is used + 26 | ERROR | Id selector is used + 27 | ERROR | Id selector is used + 28 | ERROR | Id selector is used +--------------------------------------------------------------------------------------------------------------------------------- + + diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Less/_files/avoid-ids.less b/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Less/_files/avoid-ids.less new file mode 100644 index 000000000000..dd37677b5b53 --- /dev/null +++ b/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Less/_files/avoid-ids.less @@ -0,0 +1,39 @@ +// /** +// * Copyright © Magento, Inc. All rights reserved. +// * See COPYING.txt for license details. +// */ + +#foo[bar], +#foo[bar=bash], +#foo[bar~=bash], +#foo[bar$=bash], +#foo[bar*=bash], +#foo[bar|=bash], +#foo[bar='bash'], +#foo:hover, +#foo:nth-last-of-type(n), +#foo::before, +#foo + div, +#foo > div, +#foo ~ div, +#foo\3Abar ~ div, +#foo\:bar ~ div, +#foo.bar .baz, +div#foo { + blah: 'abc'; +} + +.my #foo, +#foo .blah, +.my #foo .blah { + some: 'stuff'; +} +.blah { + #bar .baz(); + .foo #bar .baz(); + #bar .baz(); + + #bar .baz; + .foo #bar .baz; + #bar .baz; +} diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Translation/ConstantUsageSniffTest.php b/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Translation/ConstantUsageSniffTest.php deleted file mode 100644 index 39ad80850dd3..000000000000 --- a/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Translation/ConstantUsageSniffTest.php +++ /dev/null @@ -1,98 +0,0 @@ -fileMock = $this->createMock(File::class); - $this->constantUsageSniff = new ConstantUsageSniff(); - } - - /** - * @param string $file - * @param int $numIncorrectUsages - * @dataProvider processDataProvider - */ - public function testProcessIncorrectArguments($file, $numIncorrectUsages) - { - $stackPtr = 10; - $fileContent = file_get_contents(__DIR__ . '/_files/' . $file); - $tokens = $this->tokenizeString($fileContent); - $this->fileMock->expects($this->once()) - ->method('findPrevious') - ->with( - T_OPEN_TAG, - $stackPtr - 1 - ) - ->willReturn(false); - $this->fileMock->expects($this->once()) - ->method('getTokens') - ->willReturn($tokens); - $this->fileMock->numTokens = count($tokens); - $this->fileMock->expects($this->exactly($numIncorrectUsages)) - ->method('addError') - ->with( - 'Constants are not allowed as the first argument of translation function, use string literal instead', - $this->anything(), - 'VariableTranslation' - ); - $this->constantUsageSniff->process($this->fileMock, $stackPtr); - } - - /** - * Get tokens for a string - * - * @param string $fileContent - * @return array - */ - private function tokenizeString($fileContent) - { - $lineNumber = 1; - $tokens = token_get_all($fileContent); - $snifferTokens = []; - for ($i = 0; $i < count($tokens); $i++) { - $content = is_array($tokens[$i]) ? $tokens[$i][1] : $tokens[$i]; - $snifferTokens[$i]['line'] = $lineNumber; - $snifferTokens[$i]['content'] = $content; - $trimmedContent = trim($content, ' '); - if ($trimmedContent == PHP_EOL || $trimmedContent == PHP_EOL . PHP_EOL) { - $lineNumber++; - } - } - return $snifferTokens; - } - - /** - * @return array - */ - public function processDataProvider() - { - return [ - [ - 'incorrect_arguments.txt', - 9 - ], - [ - 'correct_arguments.txt', - 0 - ] - ]; - } -} diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Translation/_files/correct_arguments.txt b/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Translation/_files/correct_arguments.txt deleted file mode 100644 index 8446fe75cf11..000000000000 --- a/dev/tests/static/framework/tests/unit/testsuite/Magento/Sniffs/Translation/_files/correct_arguments.txt +++ /dev/null @@ -1,29 +0,0 @@ - $whiteList, 'standards' => [self::RULE_SET], 'extensions' => $extensions, - 'warningSeverity' => 0, 'reports' => ['full' => self::REPORT_FILE], ]; diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/DeclarativeSchemaRuleTest.php b/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/DeclarativeSchemaRuleTest.php deleted file mode 100644 index 269eff8087a9..000000000000 --- a/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/DeclarativeSchemaRuleTest.php +++ /dev/null @@ -1,98 +0,0 @@ -model = new DeclarativeSchemaRule(['some_table' => 'SomeModule']); - } - - /** - * @param string $module - * @param string $file - * @param string $contents - * @param array $expected - * @dataProvider getDependencyInfoDataProvider - */ - public function testGetDependencyInfo($module, $file, $contents, array $expected) - { - $actualDependencies = $this->model->getDependencyInfo($module, 'db_schema', $file, $contents); - $this->assertEquals( - $expected, - $actualDependencies - ); - } - - public function getDependencyInfoDataProvider() - { - return [ - ['any', 'non-db-schema-file.php', 'any', []], - [ - 'any', - '/app/Magento/Module/etc/db_schema.xml', - '
', - [['module' => 'Unknown', 'source' => 'unknown_table']] - ], - [ - 'SomeModule', - '/app/some/path/etc/db_schema.xml', - '
', - [] - ], - [ - 'any', - '/app/some/path/etc/db_schema.xml', - '
', - [ - [ - 'module' => 'SomeModule', - 'type' => \Magento\TestFramework\Dependency\RuleInterface::TYPE_HARD, - 'source' => 'some_table', - ] - ] - ], - [ - 'any', - '/app/some/path/etc/db_schema.xml', - ' - - -
-
', - [ - [ - 'module' => 'SomeModule', - 'type' => \Magento\TestFramework\Dependency\RuleInterface::TYPE_HARD, - 'source' => 'some_table', - ], - [ - 'module' => 'Unknown', - 'source' => 'ref_table', - ] - ] - ] - ]; - } -} diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Utility/XssOutputValidatorTest.php b/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Utility/XssOutputValidatorTest.php deleted file mode 100644 index ed9d8115cca4..000000000000 --- a/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Utility/XssOutputValidatorTest.php +++ /dev/null @@ -1,33 +0,0 @@ -getLinesWithXssSensitiveOutput($file); - static::assertEquals($expectedResults, $lines); - } - - /** - * @return array - */ - public function getLinesWithXssSensitiveOutputDataProvider() - { - $fixturePath = __DIR__ . '/_files/'; - return [ - 'xss_safe' => [$fixturePath . 'xss_safe.phtml', ''], - 'xss_unsafe' => [$fixturePath . 'xss_unsafe.phtml', '9,10,11,12,13,14,15,16,18,22,23'], - ]; - } -} diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Utility/_files/xss_safe.phtml b/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Utility/_files/xss_safe.phtml deleted file mode 100644 index 50d2503514d1..000000000000 --- a/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Utility/_files/xss_safe.phtml +++ /dev/null @@ -1,48 +0,0 @@ - - -getSomeData() ?> -getSomeData() ?> - - - -escapeCss('value') ?> -echo $var; -getHtmlId("some value") ?> -getIdHtml("some value") ?> -getIdHtml("some value") ?> - - - - - - - -methodHtml() ?> - -methodHtml() . - (bool)$var . - $block->escapeUrl("some value"); -?> -escapeHtml($data['parentSymbol']) . '\'' ?> -getExtendedElement($switchAttributeCode)->toHtml() ?> -escapeHtml($_filter->getFilter()->getClearLinkText()) ?> - - - tags from the code. */ -/* foreach ($block->getColumns() as $_column): ?> - getProperty() ?> /> - - -escapeHtmlAttr($block->getParamValue('title_' . $store['value'])) ?> diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Utility/_files/xss_unsafe.phtml b/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Utility/_files/xss_unsafe.phtml deleted file mode 100644 index bf3aec540b81..000000000000 --- a/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Utility/_files/xss_unsafe.phtml +++ /dev/null @@ -1,29 +0,0 @@ - -getSomeData() ?> -getTitle() ?> -getSomeMethod($block->getHtmlId()) ?> - -escapeUrl($var) . $var . 'value' ?> - - - - -methodHtml() . - $var . - $block->getSomeData(); -?> -escapeHtml($data['parentSymbol']) . '\'' ?> -escapeHtml($data['parentSymbol']) . "\"" ?> - - tags from the code. */ -/* foreach ($block->getColumns() as $_column): ?> - getProperty() ?> /> - diff --git a/dev/tests/static/get_github_changes.php b/dev/tests/static/get_github_changes.php index dba235c184e5..a619f951dac8 100644 --- a/dev/tests/static/get_github_changes.php +++ b/dev/tests/static/get_github_changes.php @@ -147,6 +147,8 @@ function getRepo($options, $mainline) } /** + * Combine list of changed files based on comparison between forks. + * * @param string $mainline * @param GitRepo $repo * @param string $branchName @@ -158,6 +160,8 @@ function retrieveChangesAcrossForks($mainline, GitRepo $repo, $branchName) } /** + * Combine list of new files based on comparison between forks. + * * @param string $mainline * @param GitRepo $repo * @param string $branchName @@ -172,7 +176,7 @@ function retrieveNewFilesAcrossForks($mainline, GitRepo $repo, $branchName) * Deletes temporary "base" repo * * @param GitRepo $repo - * @param string $repo + * @param string $mainline */ function cleanup($repo, $mainline) { @@ -392,6 +396,7 @@ private function buildChangedFilesMask(int $changesType): array /** * Find one of the allowed modification mask returned by git diff. + * * Example of change record: "A path/to/added_file" * * @param string $changeRecord @@ -427,7 +432,7 @@ private function getFileChangeDetails(string $fileName, string $remoteAlias, str 'diff HEAD %s/%s -- %s', $remoteAlias, $remoteBranch, - $this->workTree . '/' . $fileName + $fileName ) ); diff --git a/dev/tests/static/phpunit-all.xml.dist b/dev/tests/static/phpunit-all.xml.dist index 3020889e6066..7f067d9290f3 100644 --- a/dev/tests/static/phpunit-all.xml.dist +++ b/dev/tests/static/phpunit-all.xml.dist @@ -23,4 +23,12 @@ + + + + var/allure-results + true + + + diff --git a/dev/tests/static/phpunit.xml.dist b/dev/tests/static/phpunit.xml.dist index ec581ec992f0..6c02ec138256 100644 --- a/dev/tests/static/phpunit.xml.dist +++ b/dev/tests/static/phpunit.xml.dist @@ -24,9 +24,6 @@ testsuite/Magento/Test/Integrity - - testsuite/Magento/Test/Php/XssPhtmlTemplateTest.php - @@ -35,4 +32,12 @@ + + + + var/allure-results + true + + + diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/ClassesTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/ClassesTest.php index 6d627574a3a1..b5a4e41b6327 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/ClassesTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/ClassesTest.php @@ -194,7 +194,7 @@ private function assertClassesExist(array $classes, string $path): void foreach ($classes as $class) { $class = trim($class, '\\'); try { - if (strrchr($class, '\\') === false and !Classes::isVirtual($class)) { + if (strrchr($class, '\\') === false && !Classes::isVirtual($class)) { $badUsages[] = $class; continue; } else { diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/DeclarativeDependencyTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/DeclarativeDependencyTest.php new file mode 100644 index 000000000000..e090338f90c6 --- /dev/null +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/DeclarativeDependencyTest.php @@ -0,0 +1,146 @@ +readJsonFile($root . '/composer.json', true); + if (preg_match('/magento\/project-*/', $rootJson['name']) == 1) { + // The Dependency test is skipped for vendor/magento build + self::markTestSkipped( + 'MAGETWO-43654: The build is running from vendor/magento. DependencyTest is skipped.' + ); + } + $this->dependencyProvider = new DeclarativeSchemaDependencyProvider(); + } + + /** + * @throws \Exception + */ + public function testUndeclaredDependencies() + { + $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this); + $invoker( + /** + * Check undeclared modules dependencies for specified file + * + * @param string $fileType + * @param string $file + */ + function ($file) { + $componentRegistrar = new ComponentRegistrar(); + $foundModuleName = ''; + foreach ($componentRegistrar->getPaths(ComponentRegistrar::MODULE) as $moduleName => $moduleDir) { + if (strpos($file, $moduleDir . '/') !== false) { + $foundModuleName = str_replace('_', '\\', $moduleName); + break; + } + } + if (empty($foundModuleName)) { + return; + } + + $undeclaredDependency = $this->dependencyProvider->getUndeclaredModuleDependencies($foundModuleName); + + $result = []; + foreach ($undeclaredDependency as $name => $modules) { + $modules = array_unique($modules); + $result[] = $this->getErrorMessage($name) . "\n" . implode("\t\n", $modules) . "\n"; + } + if (!empty($result)) { + $this->fail( + 'Module ' . $moduleName . ' has undeclared dependencies: ' . "\n" . implode("\t\n", $result) + ); + } + }, + $this->prepareFiles(Files::init()->getDbSchemaFiles()) + ); + } + + /** + * Convert file list to data provider structure. + * + * @param string[] $files + * @return array + */ + private function prepareFiles(array $files): array + { + $result = []; + foreach ($files as $relativePath => $file) { + $absolutePath = reset($file); + $result[$relativePath] = [$absolutePath]; + } + return $result; + } + + /** + * Retrieve error message for dependency. + * + * @param string $id + * @return string + */ + private function getErrorMessage(string $id): string + { + $decodedId = $this->dependencyProvider->decodeDependencyId($id); + $entityType = $decodedId['entityType']; + if ($entityType === DeclarativeSchemaDependencyProvider::SCHEMA_ENTITY_TABLE) { + $message = sprintf( + 'Table %s has undeclared dependency on one of the following modules:', + $decodedId['tableName'] + ); + } else { + $message = sprintf( + '%s %s from %s table has undeclared dependency on one of the following modules:', + ucfirst($entityType), + $decodedId['entityName'], + $decodedId['tableName'] + ); + } + + return $message; + } + + /** + * Read data from json file. + * + * @param string $file + * @return mixed + * @throws \Exception + */ + private function readJsonFile(string $file, bool $asArray = false) + { + $decodedJson = json_decode(file_get_contents($file), $asArray); + if (null == $decodedJson) { + //phpcs:ignore Magento2.Exceptions.DirectThrow + throw new \Exception("Invalid Json: $file"); + } + + return $decodedJson; + } +} diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/DeclarativeSchemaDependencyProvider.php b/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/DeclarativeSchemaDependencyProvider.php new file mode 100644 index 000000000000..965bc6184144 --- /dev/null +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/DeclarativeSchemaDependencyProvider.php @@ -0,0 +1,758 @@ +initDeclaredDependencies(); + $dependencies = $this->getDependenciesFromFiles($this->getSchemaFileNameByModuleName($moduleName)); + $dependencies = $this->filterSelfDependency($moduleName, $dependencies); + $declared = $this->getDeclaredDependencies($moduleName, self::TYPE_HARD, self::MAP_TYPE_DECLARED); + + $existingDeclared = []; + foreach ($dependencies as $dependency) { + $checkResult = array_intersect($declared, $dependency); + if ($checkResult) { + $existingDeclared = array_merge($existingDeclared, array_values($checkResult)); + } + } + + return array_unique($existingDeclared); + } + + /** + * Provide undeclared dependencies between modules based on the declarative schema configuration. + * + * [ + * $dependencyId => [$module1, $module2, $module3 ...], + * ... + * ] + * + * @param string $moduleName + * @return array + * @throws \Exception + */ + public function getUndeclaredModuleDependencies(string $moduleName): array + { + $this->initDeclaredDependencies(); + $dependencies = $this->getDependenciesFromFiles($this->getSchemaFileNameByModuleName($moduleName)); + $dependencies = $this->filterSelfDependency($moduleName, $dependencies); + return $this->collectDependencies($moduleName, $dependencies); + } + + /** + * Provide schema file name by module name. + * + * @param string $module + * @return string + * @throws \Exception + */ + private function getSchemaFileNameByModuleName(string $module): string + { + if (empty($this->moduleSchemaFileMapping)) { + $componentRegistrar = new ComponentRegistrar(); + foreach (array_values(Files::init()->getDbSchemaFiles()) as $filePath) { + $filePath = reset($filePath); + foreach ($componentRegistrar->getPaths(ComponentRegistrar::MODULE) as $moduleName => $moduleDir) { + if (strpos($filePath, $moduleDir . '/') !== false) { + $foundModuleName = str_replace('_', '\\', $moduleName); + $this->moduleSchemaFileMapping[$foundModuleName] = $filePath; + break; + } + } + } + } + + return $this->moduleSchemaFileMapping[$module] ?? ''; + } + + /** + * Initialise map of dependencies. + * + * @throws \Exception + */ + private function initDeclaredDependencies() + { + if (empty($this->mapDependencies)) { + $jsonFiles = Files::init()->getComposerFiles(ComponentRegistrar::MODULE, false); + foreach ($jsonFiles as $file) { + $json = new \Magento\Framework\Config\Composer\Package($this->readJsonFile($file)); + $moduleName = $this->convertModuleName($json->get('name')); + $require = array_keys((array)$json->get('require')); + $this->presetDependencies($moduleName, $require, self::TYPE_HARD); + } + } + } + + /** + * Read data from json file. + * + * @param string $file + * @return mixed + * @throws \Exception + */ + private function readJsonFile(string $file, bool $asArray = false) + { + $decodedJson = json_decode(file_get_contents($file), $asArray); + if (null == $decodedJson) { + //phpcs:ignore Magento2.Exceptions.DirectThrow + throw new \Exception("Invalid Json: $file"); + } + + return $decodedJson; + } + + /** + * Remove self dependencies. + * + * @param string $moduleName + * @param array $dependencies + * @return array + */ + private function filterSelfDependency(string $moduleName, array $dependencies):array + { + foreach ($dependencies as $id => $modules) { + $decodedId = $this->decodeDependencyId($id); + $entityType = $decodedId['entityType']; + if ($entityType === self::SCHEMA_ENTITY_TABLE || $entityType === "column") { + if (array_search($moduleName, $modules) !== false) { + unset($dependencies[$id]); + } + } else { + $dependencies[$id] = $this->filterComplexDependency($moduleName, $modules); + } + } + + return array_filter($dependencies); + } + + /** + * Remove already declared dependencies. + * + * @param string $moduleName + * @param array $modules + * @return array + */ + private function filterComplexDependency(string $moduleName, array $modules): array + { + $resultDependencies = []; + if (!is_array(reset($modules))) { + if (array_search($moduleName, $modules) === false) { + $resultDependencies = $modules; + } + } else { + foreach ($modules as $dependencySet) { + if (array_search($moduleName, $dependencySet) === false) { + $resultDependencies = array_merge( + $resultDependencies, + $dependencySet + ); + } + } + } + + return array_values(array_unique($resultDependencies)); + } + + /** + * Retrieve declarative schema declaration. + * + * @return array + * @throws \Exception + */ + private function getDeclarativeSchema(): array + { + if ($this->dbSchemaDeclaration) { + return $this->dbSchemaDeclaration; + } + + $entityTypes = [self::SCHEMA_ENTITY_COLUMN, self::SCHEMA_ENTITY_CONSTRAINT, self::SCHEMA_ENTITY_INDEX]; + $declaration = []; + foreach (Files::init()->getDbSchemaFiles() as $filePath) { + $filePath = reset($filePath); + preg_match('#app/code/(\w+/\w+)#', $filePath, $result); + $moduleName = str_replace('/', '\\', $result[1]); + $moduleDeclaration = $this->getDbSchemaDeclaration($filePath); + + foreach ($moduleDeclaration[self::SCHEMA_ENTITY_TABLE] as $tableName => $tableDeclaration) { + if (!isset($tableDeclaration['modules'])) { + $tableDeclaration['modules'] = []; + } + array_push($tableDeclaration['modules'], $moduleName); + $moduleDeclaration = array_replace_recursive( + $moduleDeclaration, + [self::SCHEMA_ENTITY_TABLE => + [ + $tableName => $tableDeclaration, + ] + ] + ); + foreach ($entityTypes as $entityType) { + if (!isset($tableDeclaration[$entityType])) { + continue; + } + $moduleDeclaration = array_replace_recursive( + $moduleDeclaration, + [self::SCHEMA_ENTITY_TABLE => + [ + $tableName => + $this->addModuleAssigment($tableDeclaration, $entityType, $moduleName) + ] + ] + ); + } + } + $declaration = array_merge_recursive($declaration, $moduleDeclaration); + } + $this->dbSchemaDeclaration = $declaration; + + return $this->dbSchemaDeclaration; + } + + /** + * Get declared dependencies. + * + * @param string $tableName + * @param string $entityType + * @param null|string $entityName + * @return array + * @throws \Exception + */ + private function resolveEntityDependencies(string $tableName, string $entityType, ?string $entityName = null): array + { + switch ($entityType) { + case self::SCHEMA_ENTITY_COLUMN: + case self::SCHEMA_ENTITY_CONSTRAINT: + case self::SCHEMA_ENTITY_INDEX: + return $this->getDeclarativeSchema() + [self::SCHEMA_ENTITY_TABLE][$tableName][$entityType][$entityName]['modules']; + case self::SCHEMA_ENTITY_TABLE: + return $this->getDeclarativeSchema()[self::SCHEMA_ENTITY_TABLE][$tableName]['modules']; + default: + return []; + } + } + + /** + * @param string $filePath + * @return array + */ + private function getDbSchemaDeclaration(string $filePath): array + { + $dom = new \DOMDocument(); + $dom->loadXML(file_get_contents($filePath)); + return (new Converter())->convert($dom); + } + + /** + * Add dependency on the current module. + * + * @param array $tableDeclaration + * @param string $entityType + * @param string $moduleName + * @return array + */ + private function addModuleAssigment( + array $tableDeclaration, + string $entityType, + string $moduleName + ): array { + $declarationWithAssigment = []; + foreach ($tableDeclaration[$entityType] as $entityName => $entityDeclaration) { + if (!isset($entityDeclaration['modules'])) { + $entityDeclaration['modules'] = []; + } + if (!$this->isEntityDisabled($entityDeclaration)) { + array_push($entityDeclaration['modules'], $moduleName); + } + + $declarationWithAssigment[$entityType][$entityName] = $entityDeclaration; + } + + return $declarationWithAssigment; + } + + /** + * Retrieve dependencies from files. + * + * @param string $file + * @return string[] + * @throws \Exception + */ + private function getDependenciesFromFiles($file) + { + if (!$file) { + return []; + } + + $moduleDbSchema = $this->getDbSchemaDeclaration($file); + $dependencies = array_merge_recursive( + $this->getDisabledDependencies($moduleDbSchema), + $this->getConstraintDependencies($moduleDbSchema), + $this->getIndexDependencies($moduleDbSchema) + ); + return $dependencies; + } + + /** + * Retrieve dependencies for disabled entities. + * + * @param array $moduleDeclaration + * @return array + * @throws \Exception + */ + private function getDisabledDependencies(array $moduleDeclaration): array + { + $disabledDependencies = []; + $entityTypes = [self::SCHEMA_ENTITY_COLUMN, self::SCHEMA_ENTITY_CONSTRAINT, self::SCHEMA_ENTITY_INDEX]; + foreach ($moduleDeclaration[self::SCHEMA_ENTITY_TABLE] as $tableName => $tableDeclaration) { + foreach ($entityTypes as $entityType) { + if (!isset($tableDeclaration[$entityType])) { + continue; + } + foreach ($tableDeclaration[$entityType] as $entityName => $entityDeclaration) { + if ($this->isEntityDisabled($entityDeclaration)) { + $dependencyIdentifier = $this->getDependencyId($tableName, $entityType, $entityName); + $disabledDependencies[$dependencyIdentifier] = + $this->resolveEntityDependencies($tableName, $entityType, $entityName); + } + } + } + if ($this->isEntityDisabled($tableDeclaration)) { + $disabledDependencies[$this->getDependencyId($tableName)] = + $this->resolveEntityDependencies($tableName, self::SCHEMA_ENTITY_TABLE); + } + } + + return $disabledDependencies; + } + + /** + * Retrieve dependencies for foreign entities. + * + * @param array $constraintDeclaration + * @return array + * @throws \Exception + */ + private function getFKDependencies(array $constraintDeclaration): array + { + $referenceDependencyIdentifier = + $this->getDependencyId( + $constraintDeclaration['referenceTable'], + self::SCHEMA_ENTITY_CONSTRAINT, + $constraintDeclaration['referenceId'] + ); + $dependencyIdentifier = + $this->getDependencyId( + $constraintDeclaration[self::SCHEMA_ENTITY_TABLE], + self::SCHEMA_ENTITY_CONSTRAINT, + $constraintDeclaration['referenceId'] + ); + + $constraintDependencies = []; + $constraintDependencies[$referenceDependencyIdentifier] = + $this->resolveEntityDependencies( + $constraintDeclaration['referenceTable'], + self::SCHEMA_ENTITY_COLUMN, + $constraintDeclaration['referenceColumn'] + ); + $constraintDependencies[$dependencyIdentifier] = + $this->resolveEntityDependencies( + $constraintDeclaration[self::SCHEMA_ENTITY_TABLE], + self::SCHEMA_ENTITY_COLUMN, + $constraintDeclaration[self::SCHEMA_ENTITY_COLUMN] + ); + + return $constraintDependencies; + } + + /** + * Retrieve dependencies for constraint entities. + * + * @param array $moduleDeclaration + * @return array + * @throws \Exception + */ + private function getConstraintDependencies(array $moduleDeclaration): array + { + $constraintDependencies = []; + foreach ($moduleDeclaration[self::SCHEMA_ENTITY_TABLE] as $tableName => $tableDeclaration) { + if (empty($tableDeclaration[self::SCHEMA_ENTITY_CONSTRAINT])) { + continue; + } + foreach ($tableDeclaration[self::SCHEMA_ENTITY_CONSTRAINT] as $constraintName => $constraintDeclaration) { + if ($this->isEntityDisabled($constraintDeclaration)) { + continue; + } + $dependencyIdentifier = + $this->getDependencyId($tableName, self::SCHEMA_ENTITY_CONSTRAINT, $constraintName); + switch ($constraintDeclaration['type']) { + case 'foreign': + $constraintDependencies = array_merge( + $constraintDependencies, + $this->getFKDependencies($constraintDeclaration) + ); + break; + case 'primary': + case 'unique': + $constraintDependencies[$dependencyIdentifier] = $this->getComplexDependency( + $tableName, + $constraintDeclaration + ); + } + } + } + return $constraintDependencies; + } + + /** + * Calculate complex dependency. + * + * @param string $tableName + * @param array $entityDeclaration + * @return array + * @throws \Exception + */ + private function getComplexDependency(string $tableName, array $entityDeclaration): array + { + $complexDependency = []; + if (empty($entityDeclaration[self::SCHEMA_ENTITY_COLUMN])) { + return $complexDependency; + } + + if (!is_array($entityDeclaration[self::SCHEMA_ENTITY_COLUMN])) { + $entityDeclaration[self::SCHEMA_ENTITY_COLUMN] = [$entityDeclaration[self::SCHEMA_ENTITY_COLUMN]]; + } + + foreach (array_keys($entityDeclaration[self::SCHEMA_ENTITY_COLUMN]) as $columnName) { + $complexDependency[] = + $this->resolveEntityDependencies($tableName, self::SCHEMA_ENTITY_COLUMN, $columnName); + } + + return array_values($complexDependency); + } + + /** + * Retrieve dependencies for index entities. + * + * @param array $moduleDeclaration + * @return array + * @throws \Exception + */ + private function getIndexDependencies(array $moduleDeclaration): array + { + $indexDependencies = []; + foreach ($moduleDeclaration[self::SCHEMA_ENTITY_TABLE] as $tableName => $tableDeclaration) { + if (empty($tableDeclaration[self::SCHEMA_ENTITY_INDEX])) { + continue; + } + foreach ($tableDeclaration[self::SCHEMA_ENTITY_INDEX] as $indexName => $indexDeclaration) { + if ($this->isEntityDisabled($indexDeclaration)) { + continue; + } + $dependencyIdentifier = + $this->getDependencyId($tableName, self::SCHEMA_ENTITY_INDEX, $indexName); + $indexDependencies[$dependencyIdentifier] = + $this->getComplexDependency($tableName, $indexDeclaration); + } + } + + return $indexDependencies; + } + + /** + * Check status of the entity declaration. + * + * @param array $entityDeclaration + * @return bool + */ + private function isEntityDisabled(array $entityDeclaration): bool + { + return isset($entityDeclaration['disabled']) && $entityDeclaration['disabled'] == true; + } + + /** + * Retrieve dependency id. + * + * @param string $tableName + * @param string $entityType + * @param null|string $entityName + * @return string + */ + private function getDependencyId( + string $tableName, + string $entityType = self::SCHEMA_ENTITY_TABLE, + ?string $entityName = null + ) { + return implode('___', [$tableName, $entityType, $entityName ?: $tableName]); + } + + /** + * Retrieve dependency parameters from dependency id. + * + * @param string $id + * @return array + */ + public static function decodeDependencyId(string $id): array + { + $decodedValues = explode('___', $id); + $result = [ + 'tableName' => $decodedValues[0], + 'entityType' => $decodedValues[1], + 'entityName' => $decodedValues[2], + ]; + return $result; + } + + /** + * Collect module dependencies. + * + * @param string $currentModuleName + * @param array $dependencies + * @return array + */ + private function collectDependencies($currentModuleName, $dependencies = []) + { + if (empty($dependencies)) { + return []; + } + foreach ($dependencies as $dependencyName => $dependency) { + $this->collectDependency($dependencyName, $dependency, $currentModuleName); + } + + return $this->getDeclaredDependencies($currentModuleName, self::TYPE_HARD, self::MAP_TYPE_FOUND); + } + + /** + * Collect a module dependency. + * + * @param string $dependencyName + * @param array $dependency + * @param string $currentModule + */ + private function collectDependency( + string $dependencyName, + array $dependency, + string $currentModule + ) { + $declared = $this->getDeclaredDependencies($currentModule, self::TYPE_HARD, self::MAP_TYPE_DECLARED); + $checkResult = array_intersect($declared, $dependency); + + if (empty($checkResult)) { + $this->addDependencies( + $currentModule, + self::TYPE_HARD, + self::MAP_TYPE_FOUND, + [ + $dependencyName => $dependency, + ] + ); + } + } + + /** + * Add dependencies to dependency list. + * + * @param string $moduleName + * @param array $packageNames + * @param string $type + * + * @return void + * @throws \Exception + */ + private function presetDependencies( + string $moduleName, + array $packageNames, + string $type + ): void { + $packageNames = array_filter($packageNames, function ($packageName) { + return $this->getModuleName($packageName) || + 0 === strpos($packageName, 'magento/') && 'magento/magento-composer-installer' != $packageName; + }); + + foreach ($packageNames as $packageName) { + $this->addDependencies( + $moduleName, + $type, + self::MAP_TYPE_DECLARED, + [$this->convertModuleName($packageName)] + ); + } + } + + /** + * Returns package name on module name mapping. + * + * @return array + * @throws \Exception + */ + private function getPackageModuleMapping(): array + { + if (!$this->packageModuleMapping) { + $jsonFiles = Files::init()->getComposerFiles(ComponentRegistrar::MODULE, false); + + $packageModuleMapping = []; + foreach ($jsonFiles as $file) { + $moduleXml = simplexml_load_file(dirname($file) . '/etc/module.xml'); + $moduleName = str_replace('_', '\\', (string)$moduleXml->module->attributes()->name); + $composerJson = $this->readJsonFile($file); + $packageName = $composerJson->name; + $packageModuleMapping[$packageName] = $moduleName; + } + + $this->packageModuleMapping = $packageModuleMapping; + } + + return $this->packageModuleMapping; + } + + /** + * Retrieve Magento style module name. + * + * @param string $packageName + * @return null|string + * @throws \Exception + */ + private function getModuleName(string $packageName): ?string + { + return $this->getPackageModuleMapping()[$packageName] ?? null; + } + + /** + * Retrieve array of dependency items. + * + * @param $module + * @param $type + * @param $mapType + * @return array + */ + private function getDeclaredDependencies(string $module, string $type, string $mapType) + { + return $this->mapDependencies[$module][$type][$mapType] ?? []; + } + + /** + * Add dependency map items. + * + * @param $module + * @param $type + * @param $mapType + * @param $dependencies + */ + protected function addDependencies(string $module, string $type, string $mapType, array $dependencies) + { + $this->mapDependencies[$module][$type][$mapType] = array_merge_recursive( + $this->getDeclaredDependencies($module, $type, $mapType), + $dependencies + ); + } + + /** + * Converts a composer json component name into the Magento Module form. + * + * @param string $jsonName The name of a composer json component or dependency e.g. 'magento/module-theme' + * @return string The corresponding Magento Module e.g. 'Magento\Theme' + * @throws \Exception + */ + private function convertModuleName(string $jsonName): string + { + $moduleName = $this->getModuleName($jsonName); + if ($moduleName) { + return $moduleName; + } + + if (strpos($jsonName, 'magento/magento') !== false + || strpos($jsonName, 'magento/framework') !== false + ) { + $moduleName = str_replace('/', "\t", $jsonName); + $moduleName = str_replace('framework-', "Framework\t", $moduleName); + $moduleName = str_replace('-', ' ', $moduleName); + $moduleName = ucwords($moduleName); + $moduleName = str_replace("\t", '\\', $moduleName); + $moduleName = str_replace(' ', '', $moduleName); + } else { + $moduleName = $jsonName; + } + + return $moduleName; + } +} diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php index a4113abed803..9c0380260293 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php @@ -10,12 +10,13 @@ use Magento\Framework\App\Utility\Files; use Magento\Framework\Component\ComponentRegistrar; +use Magento\Test\Integrity\Dependency\DeclarativeSchemaDependencyProvider; use Magento\TestFramework\Dependency\DbRule; -use Magento\TestFramework\Dependency\DeclarativeSchemaRule; use Magento\TestFramework\Dependency\DiRule; use Magento\TestFramework\Dependency\LayoutRule; use Magento\TestFramework\Dependency\PhpRule; use Magento\TestFramework\Dependency\ReportsConfigRule; +use Magento\TestFramework\Dependency\AnalyticsConfigRule; use Magento\TestFramework\Dependency\VirtualType\VirtualTypeMapper; /** @@ -25,19 +26,28 @@ class DependencyTest extends \PHPUnit\Framework\TestCase { /** - * Types of dependencies between modules + * Soft dependency between modules */ const TYPE_SOFT = 'soft'; + /** + * Hard dependency between modules + */ const TYPE_HARD = 'hard'; /** - * Types of dependencies map arrays + * The identifier of dependency for mapping. */ const MAP_TYPE_DECLARED = 'declared'; + /** + * The identifier of dependency for mapping. + */ const MAP_TYPE_FOUND = 'found'; + /** + * The identifier of dependency for mapping. + */ const MAP_TYPE_REDUNDANT = 'redundant'; /** @@ -57,7 +67,7 @@ class DependencyTest extends \PHPUnit\Framework\TestCase protected static $_listConfigXml = []; /** - * List of db_schema.xml files by modules + * List of routes.xml files by modules * * Format: array( * '{Module_Name}' => '{Filename}' @@ -65,10 +75,10 @@ class DependencyTest extends \PHPUnit\Framework\TestCase * * @var array */ - protected static $_listDbSchemaXml = []; + protected static $_listRoutesXml = []; /** - * List of routes.xml files by modules + * List of analytics.xml * * Format: array( * '{Module_Name}' => '{Filename}' @@ -76,7 +86,7 @@ class DependencyTest extends \PHPUnit\Framework\TestCase * * @var array */ - protected static $_listRoutesXml = []; + protected static $_listAnalyticsXml = []; /** * List of routers @@ -174,8 +184,8 @@ public static function setUpBeforeClass() self::$_namespaces = implode('|', Files::init()->getNamespaces()); self::_prepareListConfigXml(); - self::_prepareListDbSchemaXml(); self::_prepareListRoutesXml(); + self::_prepareListAnalyticsXml(); self::_prepareMapRouters(); self::_prepareMapLayoutBlocks(); @@ -211,6 +221,7 @@ protected static function _initThemes() $defaultThemes = []; foreach (self::$_listConfigXml as $file) { $config = simplexml_load_file($file); + //phpcs:ignore Generic.PHP.NoSilencedErrors $nodes = @($config->xpath("/config/*/design/theme/full_name") ?: []); foreach ($nodes as $node) { $defaultThemes[] = (string)$node; @@ -224,13 +235,13 @@ protected static function _initThemes() */ protected static function _initRules() { - $replaceFilePattern = str_replace('\\', '/', realpath(__DIR__)) . '/_files/dependency_test/*.php'; + $replaceFilePattern = str_replace('\\', '/', realpath(__DIR__)) . '/_files/dependency_test/tables_*.php'; $dbRuleTables = []; foreach (glob($replaceFilePattern) as $fileName) { + //phpcs:ignore Generic.PHP.NoSilencedErrors $dbRuleTables = array_merge($dbRuleTables, @include $fileName); } self::$_rulesInstances = [ - new DeclarativeSchemaRule($dbRuleTables), new PhpRule(self::$_mapRouters, self::$_mapLayoutBlocks), new DbRule($dbRuleTables), new LayoutRule( @@ -240,6 +251,7 @@ protected static function _initRules() ), new DiRule(new VirtualTypeMapper()), new ReportsConfigRule($dbRuleTables), + new AnalyticsConfigRule(), ]; } @@ -261,7 +273,6 @@ protected function _getCleanedFileContents($fileType, $file) break; case 'layout': case 'config': - case 'db_schema': //Removing xml comments $contents = preg_replace('~\~s', '', $contents); break; @@ -285,6 +296,9 @@ function ($matches) use ($contents, &$contentsWithoutHtml) { return $contents; } + /** + * @inheritdoc + */ public function testUndeclared() { $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this); @@ -327,12 +341,12 @@ function ($fileType, $file) { $result = []; foreach ($undeclaredDependency as $type => $modules) { $modules = array_unique($modules); - if (!count($modules)) { + if (empty($modules)) { continue; } $result[] = sprintf("%s [%s]", $type, implode(', ', $modules)); } - if (count($result)) { + if (!empty($result)) { $this->fail('Module ' . $module . ' has undeclared dependencies: ' . implode(', ', $result)); } }, @@ -378,7 +392,7 @@ protected function getDependenciesFromFiles($module, $fileType, $file, $contents */ protected function _collectDependencies($currentModuleName, $dependencies = []) { - if (!count($dependencies)) { + if (empty($dependencies)) { return []; } $undeclared = []; @@ -416,18 +430,35 @@ private function collectDependency($dependency, $currentModule, &$undeclared) /** * Collect redundant dependencies + * * @SuppressWarnings(PHPMD.NPathComplexity) * @test * @depends testUndeclared + * @throws \Exception */ public function collectRedundant() { + $schemaDependencyProvider = new DeclarativeSchemaDependencyProvider(); + + /** TODO: Remove this temporary solution after MC-5806 is closed */ + $filePattern = __DIR__ . '/_files/dependency_test/undetected_dependencies_*.php'; + $undetectedDependencies = []; + foreach (glob($filePattern) as $fileName) { + $undetectedDependencies = array_merge($undetectedDependencies, require $fileName); + } + foreach (array_keys(self::$mapDependencies) as $module) { $declared = $this->_getDependencies($module, self::TYPE_HARD, self::MAP_TYPE_DECLARED); $found = array_merge( $this->_getDependencies($module, self::TYPE_HARD, self::MAP_TYPE_FOUND), - $this->_getDependencies($module, self::TYPE_SOFT, self::MAP_TYPE_FOUND) + $this->_getDependencies($module, self::TYPE_SOFT, self::MAP_TYPE_FOUND), + $schemaDependencyProvider->getDeclaredExistingModuleDependencies($module) ); + /** TODO: Remove this temporary solution after MC-5806 is closed */ + if (!empty($undetectedDependencies[$module])) { + $found = array_merge($found, $undetectedDependencies[$module]); + } + $found['Magento\Framework'] = 'Magento\Framework'; $this->_setDependencies($module, self::TYPE_HARD, self::MAP_TYPE_REDUNDANT, array_diff($declared, $found)); } @@ -444,7 +475,7 @@ public function testRedundant() foreach (array_keys(self::$mapDependencies) as $module) { $result = []; $redundant = $this->_getDependencies($module, self::TYPE_HARD, self::MAP_TYPE_REDUNDANT); - if (count($redundant)) { + if (!empty($redundant)) { $result[] = sprintf( "\r\nModule %s: %s [%s]", $module, @@ -453,11 +484,11 @@ public function testRedundant() ); } - if (count($result)) { + if (!empty($result)) { $output[] = implode(', ', $result); } } - if (count($output)) { + if (!empty($output)) { $this->fail("Redundant dependencies found!\r\n" . implode(' ', $output)); } } @@ -487,6 +518,7 @@ protected function _prepareFiles($fileType, $files, $skip = null) * Return all files * * @return array + * @throws \Exception */ public function getAllFiles() { @@ -508,12 +540,6 @@ public function getAllFiles() $this->_prepareFiles('config', Files::init()->getConfigFiles()) ); - // Get all configuration files - $files = array_merge( - $files, - $this->_prepareFiles('db_schema', Files::init()->getDbSchemaFiles()) - ); - //Get all layout updates files $files = array_merge( $files, @@ -544,29 +570,29 @@ protected static function _prepareListConfigXml() } /** - * Prepare list of db_schema.xml files (by modules) + * Prepare list of routes.xml files (by modules) */ - protected static function _prepareListDbSchemaXml() + protected static function _prepareListRoutesXml() { - $files = Files::init()->getDbSchemaFiles('db_schema.xml', [], false); + $files = Files::init()->getConfigFiles('*/routes.xml', [], false); foreach ($files as $file) { if (preg_match('/(?[A-Z][a-z]+)[_\/\\\\](?[A-Z][a-zA-Z]+)/', $file, $matches)) { $module = $matches['namespace'] . '\\' . $matches['module']; - self::$_listDbSchemaXml[$module] = $file; + self::$_listRoutesXml[$module][] = $file; } } } /** - * Prepare list of routes.xml files (by modules) + * Prepare list of analytics.xml files */ - protected static function _prepareListRoutesXml() + protected static function _prepareListAnalyticsXml() { - $files = Files::init()->getConfigFiles('*/routes.xml', [], false); + $files = Files::init()->getDbSchemaFiles('analytics.xml', [], false); foreach ($files as $file) { if (preg_match('/(?[A-Z][a-z]+)[_\/\\\\](?[A-Z][a-zA-Z]+)/', $file, $matches)) { $module = $matches['namespace'] . '\\' . $matches['module']; - self::$_listRoutesXml[$module][] = $file; + self::$_listAnalyticsXml[$module] = $file; } } } @@ -630,7 +656,7 @@ protected static function _prepareMapLayoutBlocks() $area = 'default'; if (preg_match('/[\/](?adminhtml|frontend)[\/]/', $file, $matches)) { $area = $matches['area']; - self::$_mapLayoutBlocks[$area] = @(self::$_mapLayoutBlocks[$area] ?: []); + self::$_mapLayoutBlocks[$area] = self::$_mapLayoutBlocks[$area] ?? []; } if (preg_match('/(?[A-Z][a-z]+)[_\/\\\\](?[A-Z][a-zA-Z]+)/', $file, $matches)) { $module = $matches['namespace'] . '\\' . $matches['module']; @@ -640,7 +666,7 @@ protected static function _prepareMapLayoutBlocks() $attributes = $element->attributes(); $block = (string)$attributes->name; if (!empty($block)) { - self::$_mapLayoutBlocks[$area][$block] = @(self::$_mapLayoutBlocks[$area][$block] ?: []); + self::$_mapLayoutBlocks[$area][$block] = self::$_mapLayoutBlocks[$area][$block] ?? []; self::$_mapLayoutBlocks[$area][$block][$module] = $module; } } @@ -658,7 +684,7 @@ protected static function _prepareMapLayoutHandles() $area = 'default'; if (preg_match('/\/(?adminhtml|frontend)\//', $file, $matches)) { $area = $matches['area']; - self::$_mapLayoutHandles[$area] = @(self::$_mapLayoutHandles[$area] ?: []); + self::$_mapLayoutHandles[$area] = self::$_mapLayoutHandles[$area] ?? []; } if (preg_match('/app\/code\/(?[A-Z][a-z]+)[_\/\\\\](?[A-Z][a-zA-Z]+)/', $file, $matches) ) { @@ -667,7 +693,7 @@ protected static function _prepareMapLayoutHandles() foreach ((array)$xml->xpath('/layout/child::*') as $element) { /** @var \SimpleXMLElement $element */ $handle = $element->getName(); - self::$_mapLayoutHandles[$area][$handle] = @(self::$_mapLayoutHandles[$area][$handle] ?: []); + self::$_mapLayoutHandles[$area][$handle] = self::$_mapLayoutHandles[$area][$handle] ?? []; self::$_mapLayoutHandles[$area][$handle][$module] = $module; } } @@ -727,6 +753,7 @@ protected static function _initDependencies() $contents = file_get_contents($file); $decodedJson = json_decode($contents); if (null == $decodedJson) { + //phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception("Invalid Json: $file"); } $json = new \Magento\Framework\Config\Composer\Package(json_decode($contents)); @@ -815,6 +842,7 @@ private static function getPackageModuleMapping(): array $contents = file_get_contents($file); $composerJson = json_decode($contents); if (null == $composerJson) { + //phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception("Invalid Json: $file"); } $moduleXml = simplexml_load_file(dirname($file) . '/etc/module.xml'); diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/Library/DependencyTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/Library/DependencyTest.php index 81088522bccb..a4baacbe4d16 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/Library/DependencyTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Library/DependencyTest.php @@ -96,35 +96,6 @@ function ($file) { ); } - public function testAppCodeUsage() - { - $files = Files::init(); - $componentRegistrar = new ComponentRegistrar(); - $libPaths = $componentRegistrar->getPaths(ComponentRegistrar::LIBRARY); - $invoker = new AggregateInvoker($this); - $invoker( - function ($file) use ($libPaths) { - $content = file_get_contents($file); - foreach ($libPaths as $libPath) { - if (strpos($file, $libPath) === 0) { - $this->assertSame( - 0, - preg_match('~(?|function\\s)__\\s*\\(~iS', $content), - 'Function __() is defined outside of the library and must not be used there. ' . - 'Replacement suggestion: new \\Magento\\Framework\\Phrase()' - ); - } - } - }, - $files->getPhpFiles( - Files::INCLUDE_PUB_CODE | - Files::INCLUDE_LIBS | - Files::AS_DATA_SET | - Files::INCLUDE_NON_CLASSES - ) - ); - } - /** * @inheritdoc */ diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Backend/ControllerAclTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Backend/ControllerAclTest.php index f204019988b7..46bdd3dd666d 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Backend/ControllerAclTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Backend/ControllerAclTest.php @@ -97,7 +97,7 @@ public function testAcl() $inheritanceMessage = "Backend controller $className have to inherit " . AbstractAction::class; $errorMessages[] = $inheritanceMessage; continue; - }; + } $isAclRedefinedInTheChildClass = $this->isConstantOverwritten($controllerClass) || $this->isMethodOverwritten($controllerClass); diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Backend/_files/controller_acl_test_whitelist_ce.txt b/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Backend/_files/controller_acl_test_whitelist_ce.txt index a819654b35bd..1119824f217b 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Backend/_files/controller_acl_test_whitelist_ce.txt +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Backend/_files/controller_acl_test_whitelist_ce.txt @@ -32,3 +32,4 @@ Magento\Inventory\Controller\Adminhtml\Source\SourceCarrierDataProcessor Magento\Inventory\Controller\Adminhtml\Source\SourceHydrator Magento\Inventory\Controller\Adminhtml\Stock\StockSaveProcessor Magento\Inventory\Controller\Adminhtml\Stock\StockSourceLinkProcessor +Magento\InventoryCatalogAdminUi\Controller\Adminhtml\Bulk\BulkPageProcessor 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 4c0fed148aea..f0177c449a3f 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Framework/Search/RequestConfigTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Framework/Search/RequestConfigTest.php @@ -102,8 +102,7 @@ public function testFileSchemaUsingInvalidXml($expectedErrors = null) Element 'metric', attribute 'type': [facet 'enumeration'] " . "The value 'sumasdasd' is not an element of the set {'sum', 'count', 'min', 'max', 'avg'}. Element 'metric', attribute 'type': 'sumasdasd' is not a valid value of the local atomic type. -Element 'bucket': Missing child element(s). Expected is one of ( metrics, ranges ). -Element 'request': Missing child element(s). Expected is ( from )." +Element 'bucket': Missing child element(s). Expected is one of ( metrics, ranges )." ) ); parent::testFileSchemaUsingInvalidXml($expectedErrors); diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/_files/dependency_test/tables_ce.php b/dev/tests/static/testsuite/Magento/Test/Integrity/_files/dependency_test/tables_ce.php index 530f55504d00..3fb53be2ec40 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/_files/dependency_test/tables_ce.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/_files/dependency_test/tables_ce.php @@ -96,7 +96,7 @@ 'catalogrule_website' => 'Magento\CatalogRule', 'catalogsearch_fulltext' => 'Magento\CatalogSearch', 'catalogsearch_result' => 'Magento\CatalogSearch', - 'search_query' => 'Magento\CatalogSearch', + 'search_query' => 'Magento\Search', 'checkout_agreement' => 'Magento\Checkout', 'checkout_agreement_store' => 'Magento\Checkout', 'cms_block' => 'Magento\Cms', diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/_files/dependency_test/undetected_dependencies_ce.php b/dev/tests/static/testsuite/Magento/Test/Integrity/_files/dependency_test/undetected_dependencies_ce.php new file mode 100644 index 000000000000..407f57ee5125 --- /dev/null +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/_files/dependency_test/undetected_dependencies_ce.php @@ -0,0 +1,10 @@ + ["Magento\CatalogSearch"] +]; diff --git a/dev/tests/static/testsuite/Magento/Test/Legacy/ObsoleteCodeTest.php b/dev/tests/static/testsuite/Magento/Test/Legacy/ObsoleteCodeTest.php index 2a6079d619d4..fe15c06bdea4 100644 --- a/dev/tests/static/testsuite/Magento/Test/Legacy/ObsoleteCodeTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Legacy/ObsoleteCodeTest.php @@ -501,8 +501,8 @@ private function _checkConstantWithClasspath($constant, $class, $replacement, $c { $classPathParts = explode('\\', $class); $classPartialPath = ''; - for ($i = count($classPathParts) - 1; $i >= 0; $i--) { - if ($i === (count($classPathParts) - 1)) { + for ($count = count($classPathParts), $i = $count - 1; $i >= 0; $i--) { + if ($i === ($count - 1)) { $classPartialPath = $classPathParts[$i] . $classPartialPath; } else { $classPartialPath = $classPathParts[$i] . '\\' . $classPartialPath; diff --git a/dev/tests/static/testsuite/Magento/Test/Legacy/UnsecureFunctionsUsageTest.php b/dev/tests/static/testsuite/Magento/Test/Legacy/UnsecureFunctionsUsageTest.php index 6edc46090d54..2ce9934c6c1a 100644 --- a/dev/tests/static/testsuite/Magento/Test/Legacy/UnsecureFunctionsUsageTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Legacy/UnsecureFunctionsUsageTest.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Test\Legacy; use Magento\Framework\App\Utility\Files; @@ -42,7 +44,6 @@ class UnsecureFunctionsUsageTest extends \PHPUnit\Framework\TestCase */ public static function setUpBeforeClass() { - self::loadData(self::$phpUnsecureFunctions, 'unsecure_php_functions*.php'); self::loadData(self::$jsUnsecureFunctions, 'unsecure_js_functions*.php'); } diff --git a/dev/tests/static/testsuite/Magento/Test/Legacy/_files/copyright/blacklist.php b/dev/tests/static/testsuite/Magento/Test/Legacy/_files/copyright/blacklist.php index 4ff5d0013892..242e4ebb22a5 100644 --- a/dev/tests/static/testsuite/Magento/Test/Legacy/_files/copyright/blacklist.php +++ b/dev/tests/static/testsuite/Magento/Test/Legacy/_files/copyright/blacklist.php @@ -8,5 +8,6 @@ '/pub\/opt\/magento\/var/', '/COPYING\.txt/', '/setup\/src\/Zend\/Mvc\/Controller\/LazyControllerAbstractFactory\.php/', - '/app\/code\/(?!Magento)[^\/]*/' + '/app\/code\/(?!Magento)[^\/]*/', + '#dev/tests/setup-integration/testsuite/Magento/Developer/_files/\S*\.xml$#', ]; diff --git a/dev/tests/static/testsuite/Magento/Test/Legacy/_files/obsolete_constants.php b/dev/tests/static/testsuite/Magento/Test/Legacy/_files/obsolete_constants.php index cbf499c8dad3..5bcc712be6f7 100644 --- a/dev/tests/static/testsuite/Magento/Test/Legacy/_files/obsolete_constants.php +++ b/dev/tests/static/testsuite/Magento/Test/Legacy/_files/obsolete_constants.php @@ -712,7 +712,6 @@ 'Magento\Sales\Block\Reorder\Sidebar', '\Magento\Sales\CustomerData\LastOrderedItems::SIDEBAR_ORDER_LIMIT', ], - ['ENTITY', 'Magento\Framework\App\Config\ValueInterface'], ['XML_PATH_ALLOW_CURRENCIES_INSTALLED', 'Magento\Framework\Locale\CurrencyInterface'], [ 'DEFAULT_CURRENCY', diff --git a/dev/tests/static/testsuite/Magento/Test/Legacy/_files/security/unsecure_php_functions.php b/dev/tests/static/testsuite/Magento/Test/Legacy/_files/security/unsecure_php_functions.php deleted file mode 100644 index 1c23f8d8ccf8..000000000000 --- a/dev/tests/static/testsuite/Magento/Test/Legacy/_files/security/unsecure_php_functions.php +++ /dev/null @@ -1,77 +0,0 @@ - will be suggested to be used instead. - * Use to specify files and directories that are allowed to use function. - * - * Format: [ - * => [ - * 'replacement' => , - * 'exclude' => [ - * , - * , - * ] - * ] - */ -return [ - 'unserialize' => [ - 'replacement' => '\Magento\Framework\Serialize\SerializerInterface::unserialize', - 'exclude' => [ - ['type' => 'library', 'name' => 'magento/framework', 'path' => 'DB/Adapter/Pdo/Mysql.php'], - ['type' => 'library', 'name' => 'magento/framework', 'path' => 'Serialize/Serializer/Serialize.php'], - ] - ], - 'serialize' => [ - 'replacement' => '\Magento\Framework\Serialize\SerializerInterface::serialize', - 'exclude' => [ - ['type' => 'library', 'name' => 'magento/framework', 'path' => 'DB/Adapter/Pdo/Mysql.php'], - ['type' => 'library', 'name' => 'magento/framework', 'path' => 'Serialize/Serializer/Serialize.php'], - ] - ], - 'eval' => [ - 'replacement' => '', - 'exclude' => [] - ], - 'md5' => [ - 'replacement' => '', - 'exclude' => [ - /* - * Usage of md5 in MessageQueue key generation algorithm - * added to exclude list to avoid backward incompatible changes - */ - [ - 'type' => 'library', - 'name' => 'magento/framework', - 'path' => 'MessageQueue/Rpc/Publisher.php', - ], - [ - 'type' => 'library', - 'name' => 'magento/framework', - 'path' => 'MessageQueue/MessageController.php', - ], - [ - 'type' => 'library', - 'name' => 'magento/framework', - 'path' => 'MessageQueue/Publisher.php', - ], - [ - 'type' => 'module', - 'name' => 'Magento_AsynchronousOperations', - 'path' => 'Model/ResourceModel/System/Message/Collection/Synchronized/Plugin.php' - ] - ] - ], - 'srand' => [ - 'replacement' => '', - 'exclude' => [] - ], - 'mt_srand' => [ - 'replacement' => '', - 'exclude' => [] - ], -]; diff --git a/dev/tests/static/testsuite/Magento/Test/Legacy/_files/words_ce.xml b/dev/tests/static/testsuite/Magento/Test/Legacy/_files/words_ce.xml index 4790ce24fd87..92e7b15efed2 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 @@ -53,7 +53,7 @@ overriden - composer.lock + dev/composer.lock app/code/Magento/Customer/view/frontend/web/js/zxcvbn.js @@ -69,5 +69,9 @@ dev/build/publication/sanity/ce.xml + + app/design/adminhtml/Magento/backend/Magento_Rma/web/css/source/_module.less + rma + diff --git a/dev/tests/static/testsuite/Magento/Test/Less/_files/blacklist/old.txt b/dev/tests/static/testsuite/Magento/Test/Less/_files/blacklist/old.txt index db5c1cab0665..4c00371faede 100644 --- a/dev/tests/static/testsuite/Magento/Test/Less/_files/blacklist/old.txt +++ b/dev/tests/static/testsuite/Magento/Test/Less/_files/blacklist/old.txt @@ -4,6 +4,7 @@ app/design/adminhtml/Magento/backend/Magento_ConfigurableProduct/web/css/source/ app/design/adminhtml/Magento/backend/Magento_Developer/web/css/source/_module-old.less app/design/adminhtml/Magento/backend/Magento_Enterprise/web/css/source/_module-old.less app/design/adminhtml/Magento/backend/Magento_Msrp/web/css/source/_module-old.less +app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/_order.less app/design/adminhtml/Magento/backend/Magento_Tax/web/css/source/_module-old.less app/design/adminhtml/Magento/backend/Magento_Theme/web/css/source/_module-old.less app/design/adminhtml/Magento/backend/Magento_Ui/web/css/source/_module-old.less @@ -22,4 +23,4 @@ app/design/adminhtml/Magento/backend/web/mui/clearless/_settings.less app/design/adminhtml/Magento/backend/web/mui/clearless/_sprites.less app/design/adminhtml/Magento/backend/web/mui/styles/_abstract.less app/design/adminhtml/Magento/backend/web/mui/styles/_table.less -app/design/adminhtml/Magento/backend/web/mui/styles/_vars.less +app/design/adminhtml/Magento/backend/web/mui/styles/_vars.less \ No newline at end of file diff --git a/dev/tests/static/testsuite/Magento/Test/Php/LiveCodeTest.php b/dev/tests/static/testsuite/Magento/Test/Php/LiveCodeTest.php index 21ca0a495dd1..76c0d047bcbb 100644 --- a/dev/tests/static/testsuite/Magento/Test/Php/LiveCodeTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Php/LiveCodeTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Test\Php; @@ -116,8 +117,10 @@ private static function getChangedFilesList($changedFilesBaseDir) 'changed_files*', function () { // if no list files, probably, this is the dev environment + // phpcs:disable Generic.PHP.NoSilencedErrors,Magento2.Security.InsecureFunction @exec('git diff --name-only', $changedFiles); @exec('git diff --cached --name-only', $addedFiles); + // phpcs:enable $changedFiles = array_unique(array_merge($changedFiles, $addedFiles)); return $changedFiles; } @@ -137,6 +140,7 @@ private static function getAddedFilesList($changedFilesBaseDir) 'changed_files*.added.*', function () { // if no list files, probably, this is the dev environment + // phpcs:ignore Generic.PHP.NoSilencedErrors,Magento2.Security.InsecureFunction @exec('git diff --cached --name-only', $addedFiles); return $addedFiles; } @@ -158,7 +162,7 @@ private static function getFilesFromListFile($listsBaseDir, $listFilePattern, $n $globFilesListPattern = ($listsBaseDir ?: self::getChangedFilesBaseDir()) . '/_files/' . $listFilePattern; $listFiles = glob($globFilesListPattern); - if (count($listFiles)) { + if (!empty($listFiles)) { foreach ($listFiles as $listFile) { $filesDefinedInList = array_merge( $filesDefinedInList, diff --git a/dev/tests/static/testsuite/Magento/Test/Php/XssPhtmlTemplateTest.php b/dev/tests/static/testsuite/Magento/Test/Php/XssPhtmlTemplateTest.php deleted file mode 100644 index fac14af5ecab..000000000000 --- a/dev/tests/static/testsuite/Magento/Test/Php/XssPhtmlTemplateTest.php +++ /dev/null @@ -1,89 +0,0 @@ -{suffix}Html{postfix}() ). - * Data is ready for the HTML output. Test is green. - * 3. AbstractBlock methods escapeHtml, escapeUrl, escapeQuote, escapeXssInUrl are allowed. Test is green. - * 4. Type casting and php function count() are allowed - * (e.g. echo (int)$var, echo (float)$var, echo (bool)$var, echo count($var)). Test is green. - * 5. Output in single quotes (e.g. echo 'some text'). Test is green. - * 6. Output in double quotes without variables (e.g. echo "some text"). Test is green. - * 7. Other of p.1-6. Output is not escaped. Test is red. - * - * @param string $file - */ - function ($file) use ($xssOutputValidator) { - $lines = $xssOutputValidator->getLinesWithXssSensitiveOutput($file); - $this->assertEmpty( - $lines, - "Potentially XSS vulnerability. " . - "Please verify that output is escaped at lines " . $lines - ); - }, - Files::init()->getPhtmlFiles() - ); - } - - /** - * @return void - */ - public function testAbsenceOfEscapeNotVerifiedAnnotationInRefinedModules() - { - $componentRegistrar = new ComponentRegistrar(); - $exemptModules = []; - foreach (array_diff(scandir(__DIR__ . '/_files/whitelist/exempt_modules'), ['..', '.']) as $file) { - $exemptModules = array_merge( - $exemptModules, - include(__DIR__ . '/_files/whitelist/exempt_modules/' . $file) - ); - } - - $result = ""; - foreach ($componentRegistrar->getPaths(ComponentRegistrar::MODULE) as $moduleName => $modulePath) { - if (in_array($moduleName, $exemptModules)) { - continue; - } - foreach (Files::init()->getFiles([$modulePath], '*.phtml') as $file) { - $fileContents = file_get_contents($file); - $pattern = "/\\/* @escapeNotVerified \\*\\/ echo (?!__).+/"; - $instances = preg_grep($pattern, explode("\n", $fileContents)); - if (!empty($instances)) { - foreach (array_keys($instances) as $line) { - $result .= $file . ':' . ($line + 1) . "\n"; - } - } - } - } - $this->assertEmpty( - $result, - "@escapeNotVerified annotation detected.\n" . - "Please use the correct escape strategy and remove annotation at:\n" . $result - ); - } -} diff --git a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt index fd8c09907e0e..055a3faf70be 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 @@ -202,3 +202,16 @@ setup/performance-toolkit/aggregate-report Magento/MessageQueue/Setup Magento/Elasticsearch/Elasticsearch5 Test/_files +Magento/InventoryCatalogAdminUi/Controller/Adminhtml +Magento/InventoryConfigurableProductIndexer/Indexer +Magento/InventoryGroupedProductIndexer/Indexer +Magento/Customer/Model/FileUploaderDataResolver.php +Magento/Customer/Model/Customer/DataProvider.php +Magento/InventoryShippingAdminUi/Ui/DataProvider +Magento/Elasticsearch6/Model/Client +Magento/CatalogSearch/Model/ResourceModel/Fulltext +Magento/Elasticsearch/Model/Layer/Search +Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver +Magento/Elasticsearch6/Model/Client +Magento/Config/App/Config/Type +Magento/InventoryReservationCli/Test/Integration diff --git a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpmd/ruleset.xml b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpmd/ruleset.xml index 76f76e2b23c5..e65a9a089da9 100644 --- a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpmd/ruleset.xml +++ b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpmd/ruleset.xml @@ -29,9 +29,6 @@ - - - @@ -46,6 +43,8 @@ - + + + diff --git a/dev/tests/static/tmp/.gitignore b/dev/tests/static/tmp/.gitignore new file mode 100644 index 000000000000..a68d087bfe51 --- /dev/null +++ b/dev/tests/static/tmp/.gitignore @@ -0,0 +1,2 @@ +/* +!/.gitignore diff --git a/dev/tests/unit/framework/bootstrap.php b/dev/tests/unit/framework/bootstrap.php index 68dba4e2ce00..06aee53dafb1 100644 --- a/dev/tests/unit/framework/bootstrap.php +++ b/dev/tests/unit/framework/bootstrap.php @@ -11,7 +11,6 @@ } require_once __DIR__ . '/autoload.php'; -require BP . '/app/functions.php'; setCustomErrorHandler(); diff --git a/dev/tests/unit/phpunit.xml.dist b/dev/tests/unit/phpunit.xml.dist index d35f7fc1819f..94500ff7bdc8 100644 --- a/dev/tests/unit/phpunit.xml.dist +++ b/dev/tests/unit/phpunit.xml.dist @@ -12,8 +12,10 @@ beStrictAboutTestsThatDoNotTestAnything="false" bootstrap="./framework/bootstrap.php" > - + ../../../app/code/*/*/Test/Unit + + ../../../lib/internal/*/*/Test/Unit ../../../lib/internal/*/*/*/Test/Unit ../../../setup/src/*/*/Test/Unit @@ -40,6 +42,26 @@ + + + var/allure-results + true + + + codingStandardsIgnoreStart + + + codingStandardsIgnoreEnd + + + cover + + + expectedExceptionMessageRegExp + + + + diff --git a/dev/tools/UpgradeScripts/pre_composer_update_2.3.php b/dev/tools/UpgradeScripts/pre_composer_update_2.3.php index 6fe629e717b9..04e64ba45ba0 100644 --- a/dev/tools/UpgradeScripts/pre_composer_update_2.3.php +++ b/dev/tools/UpgradeScripts/pre_composer_update_2.3.php @@ -148,7 +148,7 @@ } else { foreach ($composerData['repositories'] as $label => $repo) { - if (strpos(strtolower($label), 'mage') !== false || strpos($repo['url'], '.mage') !== false) { + if (is_string($label) && strpos(strtolower($label), 'mage') !== false || strpos($repo['url'], '.mage') !== false) { $mageUrls[] = $repo['url']; } } diff --git a/dev/tools/grunt/configs/watch.js b/dev/tools/grunt/configs/watch.js index 356d0b4b7b2d..c85ecb00f780 100644 --- a/dev/tools/grunt/configs/watch.js +++ b/dev/tools/grunt/configs/watch.js @@ -11,11 +11,8 @@ var combo = require('./combo'), var themeOptions = {}; -_.each(themes, function(theme, name) { +_.each(themes, function (theme, name) { themeOptions[name] = { - 'options': { - livereload: true - }, 'files': [ '<%= combo.autopath(\''+name+'\', path.pub) %>/**/*.less' ], diff --git a/dev/travis/before_install.sh b/dev/travis/before_install.sh index c9302f3b6672..845d70e4e79f 100755 --- a/dev/travis/before_install.sh +++ b/dev/travis/before_install.sh @@ -34,7 +34,7 @@ if [ $TEST_SUITE == "js" ]; then yarn global add grunt-cli fi -if [ $TEST_SUITE = "functional" ]; then +if [ $TEST_SUITE = "functional" ] || [ $TEST_SUITE = "graphql-api-functional" ]; then # Install apache sudo apt-get update sudo apt-get install apache2 libapache2-mod-fastcgi diff --git a/dev/travis/before_script.sh b/dev/travis/before_script.sh index 1dccc310c7a2..5d091efbb30a 100755 --- a/dev/travis/before_script.sh +++ b/dev/travis/before_script.sh @@ -13,9 +13,9 @@ case $TEST_SUITE in test_set_list=$(find testsuite/* -maxdepth 1 -mindepth 1 -type d | sort) test_set_count=$(printf "$test_set_list" | wc -l) - test_set_size[1]=$(printf "%.0f" $(echo "$test_set_count*0.17" | bc)) #17% - test_set_size[2]=$(printf "%.0f" $(echo "$test_set_count*0.27" | bc)) #27% - test_set_size[3]=$((test_set_count-test_set_size[1]-test_set_size[2])) #56% + test_set_size[1]=$(printf "%.0f" $(echo "$test_set_count*0.13" | bc)) #13% + test_set_size[2]=$(printf "%.0f" $(echo "$test_set_count*0.30" | bc)) #30% + test_set_size[3]=$((test_set_count-test_set_size[1]-test_set_size[2])) #55% echo "Total = ${test_set_count}; Batch #1 = ${test_set_size[1]}; Batch #2 = ${test_set_size[2]}; Batch #3 = ${test_set_size[3]};"; echo "==> preparing integration testsuite on index $INTEGRATION_INDEX with set size of ${test_set_size[$INTEGRATION_INDEX]}" @@ -72,7 +72,7 @@ case $TEST_SUITE in --base-path="$TRAVIS_BUILD_DIR" \ --repo='https://github.com/magento/magento2.git' \ --branch="$TRAVIS_BRANCH" - cat "$changed_files_ce" | sed 's/^/ + including /' + sed 's/^/ + including /' "$changed_files_ce" cd ../../.. ;; @@ -134,4 +134,35 @@ case $TEST_SUITE in cd ../../.. ;; + + graphql-api-functional) + echo "Installing Magento" + mysql -uroot -e 'CREATE DATABASE magento2;' + php bin/magento setup:install -q \ + --language="en_US" \ + --timezone="UTC" \ + --currency="USD" \ + --base-url="http://${MAGENTO_HOST_NAME}/" \ + --admin-firstname="John" \ + --admin-lastname="Doe" \ + --backend-frontname="backend" \ + --admin-email="admin@example.com" \ + --admin-user="admin" \ + --use-rewrites=1 \ + --admin-use-security-key=0 \ + --admin-password="123123q" + + echo "Prepare api-functional tests for running" + cd dev/tests/api-functional + cp -r _files/Magento/TestModuleGraphQl* ../../../app/code/Magento # Deploy and enable test modules before running tests + + cp ./phpunit_graphql.xml.dist ./phpunit.xml + sed -e "s?magento.url?${MAGENTO_HOST_NAME}?g" --in-place ./phpunit.xml + + cd ../../.. + php bin/magento setup:upgrade + + echo "Enabling production mode" + php bin/magento deploy:mode:set production + ;; esac diff --git a/lib/internal/LinLibertineFont/ChangeLog.txt b/lib/internal/LinLibertineFont/ChangeLog.txt index 8dc2c56567a4..83b8792e71ed 100644 --- a/lib/internal/LinLibertineFont/ChangeLog.txt +++ b/lib/internal/LinLibertineFont/ChangeLog.txt @@ -952,7 +952,7 @@ Changes to version 0.5.8 regular(|) & italic(/) (20040315) Changes to version 0.5.7 regular(|) & italic(/) (20040315) -N is now 66pt wider -^ {Ascicircum} is now better -- {exclamdown} is now availible +- {exclamdown} is now available - {currency} has been added | "-" hyphen is the same as softhyphen. length is now 510pt -bars have been made diff --git a/lib/internal/Magento/Framework/Acl/Loader/ResourceLoader.php b/lib/internal/Magento/Framework/Acl/Loader/ResourceLoader.php index 8a4ad39d7857..af781e19d045 100644 --- a/lib/internal/Magento/Framework/Acl/Loader/ResourceLoader.php +++ b/lib/internal/Magento/Framework/Acl/Loader/ResourceLoader.php @@ -12,12 +12,15 @@ use Magento\Framework\Acl\AclResource\ProviderInterface; use Magento\Framework\Acl\AclResourceFactory; +/** + * ACL Loader + */ class ResourceLoader implements \Magento\Framework\Acl\LoaderInterface { /** * Acl resource config * - * @var ProviderInterface $resourceProvider + * @var ProviderInterface */ protected $_resourceProvider; diff --git a/lib/internal/Magento/Framework/Amqp/Config.php b/lib/internal/Magento/Framework/Amqp/Config.php index 8fb827d9eb0d..684c5cd38b1e 100644 --- a/lib/internal/Magento/Framework/Amqp/Config.php +++ b/lib/internal/Magento/Framework/Amqp/Config.php @@ -131,10 +131,12 @@ public function __destruct() public function getValue($key) { $this->load(); - return isset($this->data[$key]) ? $this->data[$key] : null; + return $this->data[$key] ?? null; } /** + * Create amqp connection + * * @return AbstractConnection */ private function createConnection(): AbstractConnection diff --git a/lib/internal/Magento/Framework/Amqp/Queue.php b/lib/internal/Magento/Framework/Amqp/Queue.php index edd57339b842..ff5bae5017fb 100644 --- a/lib/internal/Magento/Framework/Amqp/Queue.php +++ b/lib/internal/Magento/Framework/Amqp/Queue.php @@ -37,7 +37,7 @@ class Queue implements QueueInterface private $envelopeFactory; /** - * @var LoggerInterface $logger + * @var LoggerInterface */ private $logger; @@ -63,7 +63,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc * @since 100.0.0 */ public function dequeue() @@ -98,7 +98,7 @@ public function dequeue() } /** - * {@inheritdoc} + * @inheritdoc * @since 100.0.0 */ public function acknowledge(EnvelopeInterface $envelope) @@ -119,7 +119,7 @@ public function acknowledge(EnvelopeInterface $envelope) } /** - * {@inheritdoc} + * @inheritdoc * @since 100.0.0 */ public function subscribe($callback) @@ -153,7 +153,7 @@ public function subscribe($callback) } /** - * (@inheritdoc) + * @inheritdoc * @since 100.0.0 */ public function reject(EnvelopeInterface $envelope, $requeue = true, $rejectionMessage = null) @@ -172,7 +172,7 @@ public function reject(EnvelopeInterface $envelope, $requeue = true, $rejectionM } /** - * (@inheritdoc) + * @inheritdoc * @since 100.0.0 */ public function push(EnvelopeInterface $envelope) diff --git a/lib/internal/Magento/Framework/Api/SearchCriteria/CollectionProcessor/JoinProcessor.php b/lib/internal/Magento/Framework/Api/SearchCriteria/CollectionProcessor/JoinProcessor.php index 207325042c73..29029ab6efac 100644 --- a/lib/internal/Magento/Framework/Api/SearchCriteria/CollectionProcessor/JoinProcessor.php +++ b/lib/internal/Magento/Framework/Api/SearchCriteria/CollectionProcessor/JoinProcessor.php @@ -10,6 +10,9 @@ use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Data\Collection\AbstractDb; +/** + * Search criteria join processor + */ class JoinProcessor implements CollectionProcessorInterface { /** @@ -28,7 +31,7 @@ class JoinProcessor implements CollectionProcessorInterface private $appliedFields = []; /** - * @param CustomJoinInterface[] $customFilters + * @param CustomJoinInterface[] $customJoins * @param array $fieldMapping */ public function __construct( diff --git a/lib/internal/Magento/Framework/Api/SimpleDataObjectConverter.php b/lib/internal/Magento/Framework/Api/SimpleDataObjectConverter.php index a1f5acb4e60e..4dbf4680f898 100644 --- a/lib/internal/Magento/Framework/Api/SimpleDataObjectConverter.php +++ b/lib/internal/Magento/Framework/Api/SimpleDataObjectConverter.php @@ -8,6 +8,9 @@ use Magento\Framework\Convert\ConvertArray; use Magento\Framework\Reflection\DataObjectProcessor; +/** + * Data object converter. + */ class SimpleDataObjectConverter { /** @@ -55,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; @@ -145,7 +148,7 @@ protected function _unpackAssociativeArray($data) */ public static function snakeCaseToUpperCamelCase($input) { - return str_replace(' ', '', ucwords(str_replace('_', ' ', $input))); + return str_replace('_', '', ucwords($input, '_')); } /** @@ -156,14 +159,13 @@ public static function snakeCaseToUpperCamelCase($input) */ public static function snakeCaseToCamelCase($input) { - return lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $input)))); + return lcfirst(self::snakeCaseToUpperCamelCase($input)); } /** * Convert a CamelCase string read from method into field key in snake_case * - * e.g. DefaultShipping => default_shipping - * Postcode => postcode + * For example [DefaultShipping => default_shipping, Postcode => postcode] * * @param string $name * @return string diff --git a/lib/internal/Magento/Framework/Api/Test/Unit/ExtensionAttribute/Config/SchemaLocatorTest.php b/lib/internal/Magento/Framework/Api/Test/Unit/ExtensionAttribute/Config/SchemaLocatorTest.php index d874f155b16f..972b165d8f31 100644 --- a/lib/internal/Magento/Framework/Api/Test/Unit/ExtensionAttribute/Config/SchemaLocatorTest.php +++ b/lib/internal/Magento/Framework/Api/Test/Unit/ExtensionAttribute/Config/SchemaLocatorTest.php @@ -15,7 +15,7 @@ class SchemaLocatorTest extends \PHPUnit\Framework\TestCase */ protected $model; - /** @var \Magento\Framework\Config\Dom\UrnResolver $urnResolverMock */ + /** @var \Magento\Framework\Config\Dom\UrnResolver */ protected $urnResolver; protected function setUp() diff --git a/lib/internal/Magento/Framework/App/Action/Forward.php b/lib/internal/Magento/Framework/App/Action/Forward.php index 7d6f956545b4..c81bc48ace4d 100644 --- a/lib/internal/Magento/Framework/App/Action/Forward.php +++ b/lib/internal/Magento/Framework/App/Action/Forward.php @@ -10,6 +10,11 @@ use Magento\Framework\App\RequestInterface; use Magento\Framework\App\ResponseInterface; +/** + * Forward request further. + * + * @SuppressWarnings(PHPMD.AllPurposeAction) + */ class Forward extends AbstractAction { /** diff --git a/lib/internal/Magento/Framework/App/Action/HttpConnectActionInterface.php b/lib/internal/Magento/Framework/App/Action/HttpConnectActionInterface.php new file mode 100644 index 000000000000..426fe584bade --- /dev/null +++ b/lib/internal/Magento/Framework/App/Action/HttpConnectActionInterface.php @@ -0,0 +1,19 @@ +_objectManager = \Magento\Framework\App\ObjectManager::getInstance(); } diff --git a/lib/internal/Magento/Framework/App/Bootstrap.php b/lib/internal/Magento/Framework/App/Bootstrap.php index 904c41ab9ec3..717b810cffd2 100644 --- a/lib/internal/Magento/Framework/App/Bootstrap.php +++ b/lib/internal/Magento/Framework/App/Bootstrap.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Framework\App; @@ -245,6 +246,8 @@ public function createApplication($type, $arguments = []) * * @param \Magento\Framework\AppInterface $application * @return void + * + * phpcs:disable Magento2.Exceptions,Squiz.Commenting.FunctionCommentThrowTag */ public function run(AppInterface $application) { @@ -267,13 +270,15 @@ public function run(AppInterface $application) } catch (\Exception $e) { $this->terminate($e); } - } + } // phpcs:enable /** * Asserts maintenance mode * * @return void * @throws \Exception + * + * phpcs:disable Magento2.Exceptions */ protected function assertMaintenance() { @@ -299,7 +304,7 @@ protected function assertMaintenance() $this->errorCode = self::ERR_MAINTENANCE; throw new \Exception('Unable to proceed: the maintenance mode must be enabled first. '); } - } + } // phpcs:enable /** * Asserts whether application is installed @@ -316,10 +321,12 @@ protected function assertInstalled() $isInstalled = $this->isInstalled(); if (!$isInstalled && $isExpected) { $this->errorCode = self::ERR_IS_INSTALLED; + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception('Error: Application is not installed yet. '); } if ($isInstalled && !$isExpected) { $this->errorCode = self::ERR_IS_INSTALLED; + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception('Error: Application is already installed. '); } } @@ -413,10 +420,12 @@ public function isDeveloperMode() * * @param \Exception $e * @return void - * @SuppressWarnings(PHPMD.ExitExpression) + * + * phpcs:disable Magento2.Security.LanguageConstruct, Squiz.Commenting.FunctionCommentThrowTag */ protected function terminate(\Exception $e) { + if ($this->isDeveloperMode()) { echo $e; } else { @@ -433,4 +442,5 @@ protected function terminate(\Exception $e) } exit(1); } + // phpcs:enable } diff --git a/lib/internal/Magento/Framework/App/Cache/State.php b/lib/internal/Magento/Framework/App/Cache/State.php index 08c1fca66ea1..9d268ac2d1bb 100644 --- a/lib/internal/Magento/Framework/App/Cache/State.php +++ b/lib/internal/Magento/Framework/App/Cache/State.php @@ -11,6 +11,9 @@ use Magento\Framework\App\DeploymentConfig\Writer; use Magento\Framework\Config\File\ConfigFilePool; +/** + * Cache State + */ class State implements StateInterface { /** @@ -74,7 +77,7 @@ public function __construct(DeploymentConfig $config, Writer $writer, $banAll = public function isEnabled($cacheType) { $this->load(); - return isset($this->statuses[$cacheType]) ? (bool)$this->statuses[$cacheType] : false; + return (bool)($this->statuses[$cacheType] ?? false); } /** diff --git a/lib/internal/Magento/Framework/App/Config/MetadataConfigTypeProcessor.php b/lib/internal/Magento/Framework/App/Config/MetadataConfigTypeProcessor.php index 6eb5fd7e9d3b..bc23032903d2 100644 --- a/lib/internal/Magento/Framework/App/Config/MetadataConfigTypeProcessor.php +++ b/lib/internal/Magento/Framework/App/Config/MetadataConfigTypeProcessor.php @@ -11,6 +11,9 @@ use Magento\Framework\App\Config\Spi\PostProcessorInterface; use Magento\Framework\App\ObjectManager; +/** + * Post-process config values using their backend models. + */ class MetadataConfigTypeProcessor implements PostProcessorInterface { /** @@ -114,9 +117,14 @@ private function processScopeData( $scopeCode = null ) { foreach ($this->_metadata as $path => $metadata) { - $configPath = $this->configPathResolver->resolve($path, $scope, $scopeCode); - if (!empty($this->configSource->get($configPath))) { - continue; + try { + $configPath = $this->configPathResolver->resolve($path, $scope, $scopeCode); + if (!empty($this->configSource->get($configPath))) { + continue; + } + } catch (\Throwable $exception) { + //Failed to load scopes or config source, perhaps config data received is outdated. + return $data; } /** @var \Magento\Framework\App\Config\Data\ProcessorInterface $processor */ $processor = $this->_processorFactory->get($metadata['backendModel']); diff --git a/lib/internal/Magento/Framework/App/Config/ScopeCodeResolver.php b/lib/internal/Magento/Framework/App/Config/ScopeCodeResolver.php index 321997afdba9..681af3594469 100644 --- a/lib/internal/Magento/Framework/App/Config/ScopeCodeResolver.php +++ b/lib/internal/Magento/Framework/App/Config/ScopeCodeResolver.php @@ -5,6 +5,7 @@ */ namespace Magento\Framework\App\Config; +use Magento\Framework\App\ScopeInterface; use Magento\Framework\App\ScopeResolverPool; /** @@ -34,7 +35,7 @@ public function __construct(ScopeResolverPool $scopeResolverPool) * Resolve scope code * * @param string $scopeType - * @param string $scopeCode + * @param string|null $scopeCode * @return string */ public function resolve($scopeType, $scopeCode) @@ -42,20 +43,24 @@ public function resolve($scopeType, $scopeCode) if (isset($this->resolvedScopeCodes[$scopeType][$scopeCode])) { return $this->resolvedScopeCodes[$scopeType][$scopeCode]; } - if (($scopeCode === null || is_numeric($scopeCode)) - && $scopeType !== ScopeConfigInterface::SCOPE_TYPE_DEFAULT - ) { + + if ($scopeType !== ScopeConfigInterface::SCOPE_TYPE_DEFAULT) { $scopeResolver = $this->scopeResolverPool->get($scopeType); $resolverScopeCode = $scopeResolver->getScope($scopeCode); } else { $resolverScopeCode = $scopeCode; } - if ($resolverScopeCode instanceof \Magento\Framework\App\ScopeInterface) { + if ($resolverScopeCode instanceof ScopeInterface) { $resolverScopeCode = $resolverScopeCode->getCode(); } + if ($scopeCode === null) { + $scopeCode = $resolverScopeCode; + } + $this->resolvedScopeCodes[$scopeType][$scopeCode] = $resolverScopeCode; + return $resolverScopeCode; } diff --git a/lib/internal/Magento/Framework/App/Config/Value.php b/lib/internal/Magento/Framework/App/Config/Value.php index c85b484d51ce..6fde4dded469 100644 --- a/lib/internal/Magento/Framework/App/Config/Value.php +++ b/lib/internal/Magento/Framework/App/Config/Value.php @@ -8,6 +8,10 @@ /** * Config data model * + * This model is temporarily marked as API since {@see \Magento\Framework\App\Config\ValueInterface} doesn't fit + * developers' needs of extensibility. In 2.4 we are going to introduce a new interface which should cover all needs + * and deprecate the mentioned together with the model + * * @method string getScope() * @method \Magento\Framework\App\Config\ValueInterface setScope(string $value) * @method int getScopeId() @@ -17,6 +21,8 @@ * @method string getValue() * @method \Magento\Framework\App\Config\ValueInterface setValue(string $value) * + * @api + * * @SuppressWarnings(PHPMD.NumberOfChildren) */ class Value extends \Magento\Framework\Model\AbstractModel implements \Magento\Framework\App\Config\ValueInterface @@ -107,7 +113,7 @@ public function getFieldsetDataValue($key) } /** - * {@inheritdoc} + * Processing object after save data * * {@inheritdoc}. In addition, it sets status 'invalidate' for config caches * @@ -123,7 +129,7 @@ public function afterSave() } /** - * {@inheritdoc} + * Processing object after delete data * * {@inheritdoc}. In addition, it sets status 'invalidate' for config caches * diff --git a/lib/internal/Magento/Framework/App/Config/ValueInterface.php b/lib/internal/Magento/Framework/App/Config/ValueInterface.php index 1e0747acc36f..0aa600b84dcc 100644 --- a/lib/internal/Magento/Framework/App/Config/ValueInterface.php +++ b/lib/internal/Magento/Framework/App/Config/ValueInterface.php @@ -10,16 +10,22 @@ /** * Interface \Magento\Framework\App\Config\ValueInterface * + * This interface cannot be marked as API since doesn't fit developers' needs of extensibility. In 2.4 we are going + * to introduce a new interface which should cover all needs and deprecate the this one with the model + * {@see \Magento\Framework\App\Config\Value} */ interface ValueInterface { /** * Table name + * + * @deprecated since it is not used */ const ENTITY = 'config_data'; /** * Check if config data value was changed + * * @todo this method should be make as protected * @return bool */ diff --git a/lib/internal/Magento/Framework/App/Console/Response.php b/lib/internal/Magento/Framework/App/Console/Response.php index 6255aaa3d87a..853c3d5ca269 100644 --- a/lib/internal/Magento/Framework/App/Console/Response.php +++ b/lib/internal/Magento/Framework/App/Console/Response.php @@ -3,10 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Framework\App\Console; /** - * @SuppressWarnings(PHPMD.ExitExpression) + * HTTP response implementation. */ class Response implements \Magento\Framework\App\ResponseInterface { @@ -53,9 +55,11 @@ class Response implements \Magento\Framework\App\ResponseInterface public function sendResponse() { if (!empty($this->body)) { + // phpcs:ignore Magento2.Security.LanguageConstruct.DirectOutput echo $this->body; } if ($this->terminateOnSend) { + // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage exit($this->code); } return $this->code; diff --git a/lib/internal/Magento/Framework/App/DeploymentConfig.php b/lib/internal/Magento/Framework/App/DeploymentConfig.php index 615c295675ad..40b03b068d6a 100644 --- a/lib/internal/Magento/Framework/App/DeploymentConfig.php +++ b/lib/internal/Magento/Framework/App/DeploymentConfig.php @@ -70,6 +70,11 @@ public function get($key = null, $defaultValue = null) if ($key === null) { return $this->flatData; } + + if (array_key_exists($key, $this->flatData) && $this->flatData[$key] === null) { + return ''; + } + return $this->flatData[$key] ?? $defaultValue; } @@ -146,6 +151,8 @@ private function load() } /** + * Array keys conversion + * * Convert associative array of arbitrary depth to a flat associative array with concatenated key path as keys * each level of array is accessible by path key * diff --git a/lib/internal/Magento/Framework/App/DocRootLocator.php b/lib/internal/Magento/Framework/App/DocRootLocator.php index 6fb35c42f133..d73baf8e4e74 100644 --- a/lib/internal/Magento/Framework/App/DocRootLocator.php +++ b/lib/internal/Magento/Framework/App/DocRootLocator.php @@ -3,10 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Framework\App; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\ReadFactory; /** @@ -20,18 +22,26 @@ class DocRootLocator private $request; /** + * @deprecated * @var ReadFactory */ private $readFactory; + /** + * @var Filesystem + */ + private $filesystem; + /** * @param RequestInterface $request * @param ReadFactory $readFactory + * @param Filesystem|null $filesystem */ - public function __construct(RequestInterface $request, ReadFactory $readFactory) + public function __construct(RequestInterface $request, ReadFactory $readFactory, Filesystem $filesystem = null) { $this->request = $request; $this->readFactory = $readFactory; + $this->filesystem = $filesystem ?: ObjectManager::getInstance()->get(Filesystem::class); } /** @@ -42,7 +52,8 @@ public function __construct(RequestInterface $request, ReadFactory $readFactory) public function isPub() { $rootBasePath = $this->request->getServer('DOCUMENT_ROOT'); - $readDirectory = $this->readFactory->create(DirectoryList::ROOT); - return (substr($rootBasePath, -strlen('/pub')) === '/pub') && !$readDirectory->isExist($rootBasePath . 'setup'); + $readDirectory = $this->filesystem->getDirectoryRead(DirectoryList::ROOT); + + return (substr($rootBasePath, -\strlen('/pub')) === '/pub') && ! $readDirectory->isExist('setup'); } } diff --git a/lib/internal/Magento/Framework/App/FrontController.php b/lib/internal/Magento/Framework/App/FrontController.php index b00cb3ed6090..ba23e010e3e3 100644 --- a/lib/internal/Magento/Framework/App/FrontController.php +++ b/lib/internal/Magento/Framework/App/FrontController.php @@ -14,6 +14,7 @@ use Magento\Framework\Message\ManagerInterface as MessageManager; use Magento\Framework\App\Action\AbstractAction; use Psr\Log\LoggerInterface; +use Magento\Framework\App\Request\Http as HttpRequest; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -45,6 +46,11 @@ class FrontController implements FrontControllerInterface */ private $logger; + /** + * @var bool + */ + private $validatedRequest = false; + /** * @param RouterListInterface $routerList * @param ResponseInterface $response @@ -72,13 +78,14 @@ public function __construct( /** * Perform action and generate response * - * @param RequestInterface $request + * @param RequestInterface|HttpRequest $request * @return ResponseInterface|ResultInterface * @throws \LogicException */ public function dispatch(RequestInterface $request) { \Magento\Framework\Profiler::start('routers_match'); + $this->validatedRequest = false; $routingCycleCounter = 0; $result = null; while (!$request->isDispatched() && $routingCycleCounter++ < 100) { @@ -109,44 +116,56 @@ public function dispatch(RequestInterface $request) } /** - * @param RequestInterface $request + * @param HttpRequest $request * @param ActionInterface $actionInstance * @throws NotFoundException * * @return ResponseInterface|ResultInterface */ private function processRequest( - RequestInterface $request, + HttpRequest $request, ActionInterface $actionInstance ) { $request->setDispatched(true); $this->response->setNoCacheHeaders(); - //Validating request. - try { - $this->requestValidator->validate( - $request, - $actionInstance - ); + $result = null; + + //Validating a request only once. + if (!$this->validatedRequest) { + try { + $this->requestValidator->validate( + $request, + $actionInstance + ); + } catch (InvalidRequestException $exception) { + //Validation failed - processing validation results. + $this->logger->debug( + 'Request validation failed for action "' + .get_class($actionInstance) .'"' + ); + $result = $exception->getReplaceResult(); + if ($messages = $exception->getMessages()) { + foreach ($messages as $message) { + $this->messages->addErrorMessage($message); + } + } + } + $this->validatedRequest = true; + } + //Validation did not produce a result to replace the action's. + if (!$result) { if ($actionInstance instanceof AbstractAction) { $result = $actionInstance->dispatch($request); } else { $result = $actionInstance->execute(); } - } catch (InvalidRequestException $exception) { - //Validation failed - processing validation results. - $this->logger->debug( - 'Request validation failed for action "' - .get_class($actionInstance) .'"' - ); - $result = $exception->getReplaceResult(); - if ($messages = $exception->getMessages()) { - foreach ($messages as $message) { - $this->messages->addErrorMessage($message); - } - } } + //handling redirect to 404 + if ($result instanceof NotFoundException) { + throw $result; + } return $result; } } diff --git a/lib/internal/Magento/Framework/App/FrontControllerInterface.php b/lib/internal/Magento/Framework/App/FrontControllerInterface.php index a552d88e68f5..afd3091097d1 100644 --- a/lib/internal/Magento/Framework/App/FrontControllerInterface.php +++ b/lib/internal/Magento/Framework/App/FrontControllerInterface.php @@ -8,7 +8,7 @@ /** * Application front controller responsible for dispatching application requests. * Front controller contains logic common for all actions. - * Evary application area has own front controller + * Every application area has own front controller. * * @api */ diff --git a/lib/internal/Magento/Framework/App/Http.php b/lib/internal/Magento/Framework/App/Http.php index 3c6dee49f97b..ca3976da1df5 100644 --- a/lib/internal/Magento/Framework/App/Http.php +++ b/lib/internal/Magento/Framework/App/Http.php @@ -3,16 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Framework\App; use Magento\Framework\App\Filesystem\DirectoryList; -use Magento\Framework\ObjectManager\ConfigLoaderInterface; use Magento\Framework\App\Request\Http as RequestHttp; use Magento\Framework\App\Response\Http as ResponseHttp; use Magento\Framework\App\Response\HttpInterface; use Magento\Framework\Controller\ResultInterface; +use Magento\Framework\Debug; use Magento\Framework\Event; use Magento\Framework\Filesystem; +use Magento\Framework\ObjectManager\ConfigLoaderInterface; /** * HTTP web application. Called from webroot index.php to serve web requests. @@ -79,7 +81,7 @@ class Http implements \Magento\Framework\AppInterface * @param ResponseHttp $response * @param ConfigLoaderInterface $configLoader * @param State $state - * @param Filesystem $filesystem, + * @param Filesystem $filesystem * @param \Magento\Framework\Registry $registry */ public function __construct( @@ -142,6 +144,9 @@ public function launch() } else { throw new \InvalidArgumentException('Invalid return type'); } + if ($this->_request->isHead() && $this->_response->getHttpResponseCode() == 200) { + $this->handleHeadRequest(); + } // This event gives possibility to launch something before sending output (allow cookie setting) $eventParams = ['request' => $this->_request, 'response' => $this->_response]; $this->_eventManager->dispatch('controller_front_send_response_before', $eventParams); @@ -149,7 +154,23 @@ public function launch() } /** - * {@inheritdoc} + * Handle HEAD requests by adding the Content-Length header and removing the body from the response. + * + * @return void + */ + private function handleHeadRequest() + { + // It is possible that some PHP installations have overloaded strlen to use mb_strlen instead. + // This means strlen might return the actual number of characters in a non-ascii string instead + // of the number of bytes. Use mb_strlen explicitly with a single byte character encoding to ensure + // that the content length is calculated in bytes. + $contentLength = mb_strlen($this->_response->getContent(), '8bit'); + $this->_response->clearBody(); + $this->_response->setHeader('Content-Length', $contentLength); + } + + /** + * @inheritdoc */ public function catchException(Bootstrap $bootstrap, \Exception $exception) { @@ -198,6 +219,7 @@ private function buildContentFromException(\Exception $exception) { /** @var \Exception[] $exceptions */ $exceptions = []; + do { $exceptions[] = $exception; } while ($exception = $exception->getPrevious()); @@ -214,7 +236,12 @@ private function buildContentFromException(\Exception $exception) $index, get_class($exception), $exception->getMessage(), - $exception->getTraceAsString() + Debug::trace( + $exception->getTrace(), + true, + true, + (bool)getenv('MAGE_DEBUG_SHOW_ARGS') + ) ); } @@ -241,7 +268,7 @@ private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception) . "because the Magento setup directory cannot be accessed. \n" . 'You can install Magento using either the command line or you must restore access ' . 'to the following directory: ' . $setupInfo->getDir($projectRoot) . "\n"; - + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception($newMessage, 0, $exception); } } @@ -250,13 +277,14 @@ private function redirectToSetup(Bootstrap $bootstrap, \Exception $exception) * Handler for bootstrap errors * * @param Bootstrap $bootstrap - * @param \Exception &$exception + * @param \Exception $exception * @return bool */ private function handleBootstrapErrors(Bootstrap $bootstrap, \Exception &$exception) { $bootstrapCode = $bootstrap->getErrorCode(); if (Bootstrap::ERR_MAINTENANCE == $bootstrapCode) { + // phpcs:ignore Magento2.Security.IncludeFile require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/503.php'); return true; } @@ -297,6 +325,7 @@ private function handleInitException(\Exception $exception) { if ($exception instanceof \Magento\Framework\Exception\State\InitException) { $this->getLogger()->critical($exception); + // phpcs:ignore Magento2.Security.IncludeFile require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/404.php'); return true; } @@ -312,7 +341,15 @@ private function handleInitException(\Exception $exception) */ private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception) { - $reportData = [$exception->getMessage(), $exception->getTraceAsString()]; + $reportData = [ + $exception->getMessage(), + Debug::trace( + $exception->getTrace(), + true, + true, + (bool)getenv('MAGE_DEBUG_SHOW_ARGS') + ) + ]; $params = $bootstrap->getParams(); if (isset($params['REQUEST_URI'])) { $reportData['url'] = $params['REQUEST_URI']; @@ -320,6 +357,7 @@ private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception if (isset($params['SCRIPT_NAME'])) { $reportData['script_name'] = $params['SCRIPT_NAME']; } + // phpcs:ignore Magento2.Security.IncludeFile require $this->_filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/report.php'); return true; } diff --git a/lib/internal/Magento/Framework/App/Language/Dictionary.php b/lib/internal/Magento/Framework/App/Language/Dictionary.php index 294490a665cb..d9a5ccb00d89 100644 --- a/lib/internal/Magento/Framework/App/Language/Dictionary.php +++ b/lib/internal/Magento/Framework/App/Language/Dictionary.php @@ -111,7 +111,9 @@ public function getDictionary($languageCode) /** @var Config $languageConfig */ $languageConfig = $packInfo['language']; $dictionary = $this->readPackCsv($languageConfig->getVendor(), $languageConfig->getPackage()); - $result = array_merge($result, $dictionary); + foreach ($dictionary as $key => $value) { + $result[$key] = $value; + } } return $result; } diff --git a/lib/internal/Magento/Framework/App/MaintenanceMode.php b/lib/internal/Magento/Framework/App/MaintenanceMode.php index 4e4328cb72ae..e813522a0151 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/ObjectManager/ConfigWriter/Filesystem.php b/lib/internal/Magento/Framework/App/ObjectManager/ConfigWriter/Filesystem.php new file mode 100644 index 000000000000..2b1cdd2a9c10 --- /dev/null +++ b/lib/internal/Magento/Framework/App/ObjectManager/ConfigWriter/Filesystem.php @@ -0,0 +1,61 @@ +directoryList = $directoryList; + } + + /** + * Writes config in storage + * + * @param string $key + * @param array $config + * @return void + */ + public function write(string $key, array $config) + { + $this->initialize(); + $configuration = sprintf('directoryList->getPath(DirectoryList::GENERATED_METADATA) . '/' . $key . '.php', + $configuration + ); + } + + /** + * Initializes writer + * + * @return void + */ + private function initialize() + { + if (!file_exists($this->directoryList->getPath(DirectoryList::GENERATED_METADATA))) { + mkdir($this->directoryList->getPath(DirectoryList::GENERATED_METADATA)); + } + } +} diff --git a/lib/internal/Magento/Framework/App/ObjectManager/ConfigWriterInterface.php b/lib/internal/Magento/Framework/App/ObjectManager/ConfigWriterInterface.php new file mode 100644 index 000000000000..cccbb0ae30b9 --- /dev/null +++ b/lib/internal/Magento/Framework/App/ObjectManager/ConfigWriterInterface.php @@ -0,0 +1,24 @@ +cache = $cache; $this->identifier = $identifier; $this->request = $request; - - if ($context) { - $this->context = $context; - } else { - $this->context = \Magento\Framework\App\ObjectManager::getInstance()->get( - \Magento\Framework\App\Http\Context::class - ); - } - if ($contextFactory) { - $this->contextFactory = $contextFactory; - } else { - $this->contextFactory = \Magento\Framework\App\ObjectManager::getInstance()->get( - \Magento\Framework\App\Http\ContextFactory::class - ); - } - if ($httpFactory) { - $this->httpFactory = $httpFactory; - } else { - $this->httpFactory = \Magento\Framework\App\ObjectManager::getInstance()->get( - \Magento\Framework\App\Response\HttpFactory::class - ); - } - if ($serializer) { - $this->serializer = $serializer; - } else { - $this->serializer = \Magento\Framework\App\ObjectManager::getInstance()->get( - \Magento\Framework\Serialize\SerializerInterface::class - ); - } + $this->context = $context ?? ObjectManager::getInstance()->get(\Magento\Framework\App\Http\Context::class); + $this->contextFactory = $contextFactory ?? ObjectManager::getInstance()->get( + \Magento\Framework\App\Http\ContextFactory::class + ); + $this->httpFactory = $httpFactory ?? ObjectManager::getInstance()->get( + \Magento\Framework\App\Response\HttpFactory::class + ); + $this->serializer = $serializer ?? ObjectManager::getInstance()->get( + \Magento\Framework\Serialize\SerializerInterface::class + ); + $this->state = $state ?? ObjectManager::getInstance()->get(AppState::class); + $this->fullPageCache = $fullPageCache ?? ObjectManager::getInstance()->get( + \Magento\PageCache\Model\Cache\Type::class + ); } /** @@ -112,7 +109,7 @@ public function __construct( public function load() { if ($this->request->isGet() || $this->request->isHead()) { - $responseData = $this->getCache()->load($this->identifier->getValue()); + $responseData = $this->fullPageCache->load($this->identifier->getValue()); if (!$responseData) { return false; } @@ -144,12 +141,14 @@ public function process(\Magento\Framework\App\Response\Http $response) $tags = $tagsHeader ? explode(',', $tagsHeader->getFieldValue()) : []; $response->clearHeader('Set-Cookie'); - $response->clearHeader('X-Magento-Tags'); + if ($this->state->getMode() != AppState::MODE_DEVELOPER) { + $response->clearHeader('X-Magento-Tags'); + } if (!headers_sent()) { header_remove('Set-Cookie'); } - $this->getCache()->save( + $this->fullPageCache->save( $this->serializer->serialize($this->getPreparedData($response)), $this->identifier->getValue(), $tags, @@ -203,19 +202,4 @@ private function buildResponse($responseData) return $response; } - - /** - * TODO: Workaround to support backwards compatibility, will rework to use Dependency Injection in MAGETWO-49547 - * - * @return \Magento\PageCache\Model\Cache\Type - */ - private function getCache() - { - if (!$this->fullPageCache) { - $this->fullPageCache = \Magento\Framework\App\ObjectManager::getInstance()->get( - \Magento\PageCache\Model\Cache\Type::class - ); - } - return $this->fullPageCache; - } } diff --git a/lib/internal/Magento/Framework/App/Request/CompositeValidator.php b/lib/internal/Magento/Framework/App/Request/CompositeValidator.php new file mode 100644 index 000000000000..2b5205fddc9c --- /dev/null +++ b/lib/internal/Magento/Framework/App/Request/CompositeValidator.php @@ -0,0 +1,43 @@ +validators = $validators; + } + + /** + * @inheritDoc + */ + public function validate( + RequestInterface $request, + ActionInterface $action + ): void { + foreach ($this->validators as $validator) { + $validator->validate($request, $action); + } + } +} diff --git a/lib/internal/Magento/Framework/App/Request/DataPersistor.php b/lib/internal/Magento/Framework/App/Request/DataPersistor.php index bb3740aceb01..f0b8819621a1 100644 --- a/lib/internal/Magento/Framework/App/Request/DataPersistor.php +++ b/lib/internal/Magento/Framework/App/Request/DataPersistor.php @@ -5,8 +5,12 @@ */ namespace Magento\Framework\App\Request; +use Magento\Framework\Api\SimpleDataObjectConverter; use Magento\Framework\Session\SessionManagerInterface; +/** + * Persist data to session. + */ class DataPersistor implements DataPersistorInterface { /** @@ -32,7 +36,7 @@ public function __construct( */ public function set($key, $data) { - $method = 'set' . ucfirst($key) . 'Data'; + $method = 'set' . SimpleDataObjectConverter::snakeCaseToUpperCamelCase($key) . 'Data'; call_user_func_array([$this->session, $method], [$data]); } @@ -44,7 +48,7 @@ public function set($key, $data) */ public function get($key) { - $method = 'get' . ucfirst($key) . 'Data'; + $method = 'get' . SimpleDataObjectConverter::snakeCaseToUpperCamelCase($key) . 'Data'; return call_user_func_array([$this->session, $method], []); } @@ -56,7 +60,7 @@ public function get($key) */ public function clear($key) { - $method = 'uns' . ucfirst($key) . 'Data'; + $method = 'uns' . SimpleDataObjectConverter::snakeCaseToUpperCamelCase($key) . 'Data'; call_user_func_array([$this->session, $method], []); } } diff --git a/lib/internal/Magento/Framework/App/Request/Http.php b/lib/internal/Magento/Framework/App/Request/Http.php index 03a7200fbe20..4e709ed27695 100644 --- a/lib/internal/Magento/Framework/App/Request/Http.php +++ b/lib/internal/Magento/Framework/App/Request/Http.php @@ -95,6 +95,11 @@ class Http extends Request implements RequestContentInterface, RequestSafetyInte */ private $distroBaseUrl; + /** + * @var PathInfo + */ + private $pathInfoService; + /** * @param CookieReaderInterface $cookieReader * @param StringUtils $converter @@ -103,6 +108,7 @@ class Http extends Request implements RequestContentInterface, RequestSafetyInte * @param ObjectManagerInterface $objectManager * @param \Zend\Uri\UriInterface|string|null $uri * @param array $directFrontNames + * @param PathInfo|null $pathInfoService */ public function __construct( CookieReaderInterface $cookieReader, @@ -111,94 +117,71 @@ public function __construct( PathInfoProcessorInterface $pathInfoProcessor, ObjectManagerInterface $objectManager, $uri = null, - $directFrontNames = [] + $directFrontNames = [], + PathInfo $pathInfoService = null ) { parent::__construct($cookieReader, $converter, $uri); $this->routeConfig = $routeConfig; $this->pathInfoProcessor = $pathInfoProcessor; $this->objectManager = $objectManager; $this->directFrontNames = $directFrontNames; + $this->pathInfoService = $pathInfoService ?: \Magento\Framework\App\ObjectManager::getInstance()->get( + PathInfo::class + ); } /** - * Returns ORIGINAL_PATH_INFO. - * This value is calculated instead of reading PATH_INFO - * directly from $_SERVER due to cross-platform differences. + * Return the ORIGINAL_PATH_INFO. + * This value is calculated and processed from $_SERVER due to cross-platform differences. + * instead of reading PATH_INFO * * @return string */ public function getOriginalPathInfo() { if (empty($this->originalPathInfo)) { - $this->setPathInfo(); + $originalPathInfoFromRequest = $this->pathInfoService->getPathInfo( + $this->getRequestUri(), + $this->getBaseUrl() + ); + $this->originalPathInfo = (string)$this->pathInfoProcessor->process($this, $originalPathInfoFromRequest); + $this->requestString = $this->originalPathInfo + . $this->pathInfoService->getQueryString($this->getRequestUri()); } return $this->originalPathInfo; } /** - * Set the PATH_INFO string - * Set the ORIGINAL_PATH_INFO string - * - * @param string|null $pathInfo - * @return $this - */ - public function setPathInfo($pathInfo = null) - { - if ($pathInfo === null) { - $requestUri = $this->getRequestUri(); - if ('/' === $requestUri) { - return $this; - } - - $requestUri = $this->removeRepeatedSlashes($requestUri); - $parsedRequestUri = explode('?', $requestUri, 2); - $queryString = !isset($parsedRequestUri[1]) ? '' : '?' . $parsedRequestUri[1]; - $baseUrl = $this->getBaseUrl(); - $pathInfo = (string)substr($parsedRequestUri[0], (int)strlen($baseUrl)); - - if ($this->isNoRouteUri($baseUrl, $pathInfo)) { - $pathInfo = 'noroute'; - } - $pathInfo = $this->pathInfoProcessor->process($this, $pathInfo); - $this->originalPathInfo = (string)$pathInfo; - $this->requestString = $pathInfo . $queryString; - } - $this->pathInfo = (string)$pathInfo; - return $this; - } - - /** - * Remove repeated slashes from the start of the path. + * Return the path info * - * @param string $pathInfo * @return string */ - private function removeRepeatedSlashes($pathInfo) + public function getPathInfo() { - $firstChar = (string)substr($pathInfo, 0, 1); - if ($firstChar == '/') { - $pathInfo = '/' . ltrim($pathInfo, '/'); + if (empty($this->pathInfo)) { + $this->pathInfo = $this->getOriginalPathInfo(); } - - return $pathInfo; + return $this->pathInfo; } /** - * Check is URI should be marked as no route, helps route to 404 URI like `index.phpadmin`. + * Set the PATH_INFO string. + * + * Set the ORIGINAL_PATH_INFO string. * - * @param string $baseUrl - * @param string $pathInfo - * @return bool + * @param string|null $pathInfo + * @return $this */ - private function isNoRouteUri($baseUrl, $pathInfo) + public function setPathInfo($pathInfo = null) { - $firstChar = (string)substr($pathInfo, 0, 1); - return $baseUrl !== '' && !in_array($firstChar, ['/', '']); + $this->pathInfo = (string)$pathInfo; + return $this; } /** - * Check if code declared as direct access frontend name - * this mean what this url can be used without store code + * Check if code declared as direct access frontend name. + * + * This means what this url can be used without store code. * * @param string $code * @return bool @@ -284,8 +267,7 @@ public function getControllerModule() } /** - * Collect properties changed by _forward in protected storage - * before _forward was called first time. + * Collect properties changed by _forward in protected storage before _forward was called first time. * * @return $this */ @@ -427,6 +409,8 @@ public function getFullActionName($delimiter = '_') } /** + * Sleep + * * @return array */ public function __sleep() @@ -435,7 +419,7 @@ public function __sleep() } /** - * {@inheritdoc} + * @inheritdoc */ public function isSafeMethod() { diff --git a/lib/internal/Magento/Framework/App/Request/HttpMethodMap.php b/lib/internal/Magento/Framework/App/Request/HttpMethodMap.php new file mode 100644 index 000000000000..4e7216954b0d --- /dev/null +++ b/lib/internal/Magento/Framework/App/Request/HttpMethodMap.php @@ -0,0 +1,68 @@ +map = $this->processMap($map); + } + + /** + * Filter given map. + * + * @param array $map + * @throws \InvalidArgumentException + * + * @return string[] + */ + private function processMap(array $map): array + { + $filtered = []; + foreach ($map as $method => $interface) { + $interface = trim(preg_replace('/^\\\+/', '', $interface)); + if (!(interface_exists($interface) || class_exists($interface))) { + throw new \InvalidArgumentException( + "Interface '$interface' does not exist" + ); + } + if (!$method) { + throw new \InvalidArgumentException('Invalid method given'); + } + + $filtered[$method] = $interface; + } + + return $filtered; + } + + /** + * Where keys are methods' names and values are interfaces' names. + * + * @return string[] + * + * @see \Zend\Http\Request Has list of methods as METHOD_* constants. + */ + public function getMap(): array + { + return $this->map; + } +} diff --git a/lib/internal/Magento/Framework/App/Request/HttpMethodValidator.php b/lib/internal/Magento/Framework/App/Request/HttpMethodValidator.php new file mode 100644 index 000000000000..d3eb514caad1 --- /dev/null +++ b/lib/internal/Magento/Framework/App/Request/HttpMethodValidator.php @@ -0,0 +1,96 @@ +map = $map; + $this->log = $logger; + } + + /** + * Create exception when invalid HTTP method used. + * + * @param Http $request + * @param ActionInterface $action + * @throws InvalidRequestException + * + * @return void + */ + private function throwException( + Http $request, + ActionInterface $action + ): void { + $uri = $request->getRequestUri(); + $method = $request->getMethod(); + if ($action instanceof InterceptorInterface) { + $actionClass = get_parent_class($action); + } else { + $actionClass = get_class($action); + } + $this->log->debug( + "URI '$uri'' cannot be accessed with $method method ($actionClass)" + ); + + throw new InvalidRequestException( + new NotFoundException(new Phrase('Page not found.')) + ); + } + + /** + * @inheritDoc + */ + public function validate( + RequestInterface $request, + ActionInterface $action + ): void { + if ($request instanceof Http) { + $method = $request->getMethod(); + $map = $this->map->getMap(); + //If we don't have an interface for the HTTP method or + //the action has HTTP method limitations and doesn't allow the + //received one then the request is invalid. + if (!array_key_exists($method, $map) + || (array_intersect($map, class_implements($action, true)) + && !$action instanceof $map[$method] + ) + ) { + $this->throwException($request, $action); + } + } + } +} diff --git a/lib/internal/Magento/Framework/App/Request/InvalidRequestException.php b/lib/internal/Magento/Framework/App/Request/InvalidRequestException.php index 3d408b005068..f15ce494e9bb 100644 --- a/lib/internal/Magento/Framework/App/Request/InvalidRequestException.php +++ b/lib/internal/Magento/Framework/App/Request/InvalidRequestException.php @@ -10,6 +10,7 @@ use Magento\Framework\App\ResponseInterface; use Magento\Framework\Controller\ResultInterface; +use Magento\Framework\Exception\NotFoundException; use Magento\Framework\Exception\RuntimeException; use Magento\Framework\Phrase; @@ -29,8 +30,9 @@ class InvalidRequestException extends RuntimeException private $messages; /** - * @param ResponseInterface|ResultInterface $replaceResult Use this result - * instead of calling action instance. + * @param ResponseInterface|ResultInterface|NotFoundException $replaceResult + * Use this result instead of calling an action instance, + * if NotFoundException is given the the default 404 mechanism will be triggered. * @param Phrase[]|null $messages Messages to show to client * as error messages. */ @@ -43,7 +45,7 @@ public function __construct($replaceResult, ?array $messages = null) } /** - * @return ResponseInterface|ResultInterface + * @return ResponseInterface|ResultInterface|NotFoundException */ public function getReplaceResult() { diff --git a/lib/internal/Magento/Framework/App/Request/PathInfo.php b/lib/internal/Magento/Framework/App/Request/PathInfo.php new file mode 100644 index 000000000000..76a12d6aefe9 --- /dev/null +++ b/lib/internal/Magento/Framework/App/Request/PathInfo.php @@ -0,0 +1,80 @@ +removeRepeatedSlashes($requestUri); + $parsedRequestUri = explode('?', $requestUri, 2); + $pathInfo = (string)substr(current($parsedRequestUri), (int)strlen($baseUrl)); + + if ($this->isNoRouteUri($baseUrl, $pathInfo)) { + $pathInfo = \Magento\Framework\App\Router\Base::NO_ROUTE; + } + return $pathInfo; + } + + /** + * Get query string using from the request URI + * + * @param string $requestUri + * @return string + */ + public function getQueryString(string $requestUri) : string + { + $requestUri = $this->removeRepeatedSlashes($requestUri); + $parsedRequestUri = explode('?', $requestUri, 2); + $queryString = !isset($parsedRequestUri[1]) ? '' : '?' . $parsedRequestUri[1]; + return $queryString; + } + + /** + * Remove repeated slashes from the start of the path. + * + * @param string $pathInfo + * @return string + */ + private function removeRepeatedSlashes($pathInfo) : string + { + $firstChar = (string)substr($pathInfo, 0, 1); + if ($firstChar == '/') { + $pathInfo = '/' . ltrim($pathInfo, '/'); + } + + return $pathInfo; + } + + /** + * Check is URI should be marked as no route, helps route to 404 URI like `index.phpadmin`. + * + * @param string $baseUrl + * @param string $pathInfo + * @return bool + */ + private function isNoRouteUri($baseUrl, $pathInfo) : bool + { + $firstChar = (string)substr($pathInfo, 0, 1); + return $baseUrl !== '' && !in_array($firstChar, ['/', '']); + } +} diff --git a/lib/internal/Magento/Framework/App/ResourceConnection.php b/lib/internal/Magento/Framework/App/ResourceConnection.php index 148053fa44e0..b543cc970f64 100644 --- a/lib/internal/Magento/Framework/App/ResourceConnection.php +++ b/lib/internal/Magento/Framework/App/ResourceConnection.php @@ -11,7 +11,9 @@ /** * Application provides ability to configure multiple connections to persistent storage. + * * This class provides access to all these connections. + * * @api */ class ResourceConnection @@ -53,7 +55,7 @@ class ResourceConnection protected $connectionFactory; /** - * @var DeploymentConfig $deploymentConfig + * @var DeploymentConfig */ private $deploymentConfig; diff --git a/lib/internal/Magento/Framework/App/Response/Http.php b/lib/internal/Magento/Framework/App/Response/Http.php index 62ff94e7043f..a80d9cbdd668 100644 --- a/lib/internal/Magento/Framework/App/Response/Http.php +++ b/lib/internal/Magento/Framework/App/Response/Http.php @@ -1,10 +1,9 @@ cookieManager = $objectManager->create(\Magento\Framework\Stdlib\CookieManagerInterface::class); $this->cookieMetadataFactory = $objectManager->get( diff --git a/lib/internal/Magento/Framework/App/Response/Http/FileFactory.php b/lib/internal/Magento/Framework/App/Response/Http/FileFactory.php index 19a89681a2d5..d599f91ca8ca 100644 --- a/lib/internal/Magento/Framework/App/Response/Http/FileFactory.php +++ b/lib/internal/Magento/Framework/App/Response/Http/FileFactory.php @@ -1,13 +1,17 @@ isFile($file)) { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception((string)new \Magento\Framework\Phrase('File not found')); } $contentLength = $dir->stat($file)['size']; @@ -86,6 +90,7 @@ public function create( if ($isFile) { $stream = $dir->openFile($file, 'r'); while (!$stream->eof()) { + // phpcs:ignore Magento2.Security.LanguageConstruct.DirectOutput echo $stream->read(1024); } } else { @@ -93,6 +98,7 @@ public function create( $file = $fileName; $stream = $dir->openFile($fileName, 'r'); while (!$stream->eof()) { + // phpcs:ignore Magento2.Security.LanguageConstruct.DirectOutput echo $stream->read(1024); } } diff --git a/lib/internal/Magento/Framework/App/Route/ConfigInterface/Proxy.php b/lib/internal/Magento/Framework/App/Route/ConfigInterface/Proxy.php index fd37590bb778..5e79315238f7 100644 --- a/lib/internal/Magento/Framework/App/Route/ConfigInterface/Proxy.php +++ b/lib/internal/Magento/Framework/App/Route/ConfigInterface/Proxy.php @@ -60,10 +60,17 @@ public function __construct( } /** + * Remove links to other objects. + * * @return array + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + return ['_subject', '_isShared']; } @@ -71,9 +78,14 @@ public function __sleep() * Retrieve ObjectManager from global scope * * @return void + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + $this->_objectManager = \Magento\Framework\App\ObjectManager::getInstance(); } diff --git a/lib/internal/Magento/Framework/App/Router/Base.php b/lib/internal/Magento/Framework/App/Router/Base.php index ed9def8e1cf5..fcce821858eb 100644 --- a/lib/internal/Magento/Framework/App/Router/Base.php +++ b/lib/internal/Magento/Framework/App/Router/Base.php @@ -5,14 +5,23 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Framework\App\Router; /** + * Base router implementation. + * * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Base implements \Magento\Framework\App\RouterInterface { + /** + * No route constant used for request + */ + const NO_ROUTE = 'noroute'; + /** * @var \Magento\Framework\App\ActionFactory */ @@ -303,7 +312,7 @@ protected function matchAction(\Magento\Framework\App\RequestInterface $request, if ($actionInstance === null) { return null; } - $action = 'noroute'; + $action = self::NO_ROUTE; } // set values only after all the checks are done @@ -333,12 +342,12 @@ public function getActionClassName($module, $actionPath) /** * Check that request uses https protocol if it should. + * * Function redirects user to correct URL if needed. * * @param \Magento\Framework\App\RequestInterface $request * @param string $path * @return void - * @SuppressWarnings(PHPMD.ExitExpression) */ protected function _checkShouldBeSecure(\Magento\Framework\App\RequestInterface $request, $path = '') { @@ -353,6 +362,7 @@ protected function _checkShouldBeSecure(\Magento\Framework\App\RequestInterface } $this->_responseFactory->create()->setRedirect($url)->sendResponse(); + // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage exit; } } diff --git a/lib/internal/Magento/Framework/App/RouterList.php b/lib/internal/Magento/Framework/App/RouterList.php index ae14f1ad3d2f..75ea8766c960 100644 --- a/lib/internal/Magento/Framework/App/RouterList.php +++ b/lib/internal/Magento/Framework/App/RouterList.php @@ -119,14 +119,6 @@ public function rewind() */ protected function compareRoutersSortOrder($routerDataFirst, $routerDataSecond) { - if ((int)$routerDataFirst['sortOrder'] == (int)$routerDataSecond['sortOrder']) { - return 0; - } - - if ((int)$routerDataFirst['sortOrder'] < (int)$routerDataSecond['sortOrder']) { - return -1; - } else { - return 1; - } + return (int)$routerDataFirst['sortOrder'] <=> (int)$routerDataSecond['sortOrder']; } } diff --git a/lib/internal/Magento/Framework/App/ScopeDefault.php b/lib/internal/Magento/Framework/App/ScopeDefault.php index 2ea62387145b..e62d19f9ffbb 100644 --- a/lib/internal/Magento/Framework/App/ScopeDefault.php +++ b/lib/internal/Magento/Framework/App/ScopeDefault.php @@ -17,7 +17,7 @@ class ScopeDefault implements ScopeInterface */ public function getCode() { - return 'default'; + return ''; } /** @@ -27,7 +27,7 @@ public function getCode() */ public function getId() { - return 1; + return 0; } /** diff --git a/lib/internal/Magento/Framework/App/StaticResource.php b/lib/internal/Magento/Framework/App/StaticResource.php index 575074fdb58a..86b2b15d3c44 100644 --- a/lib/internal/Magento/Framework/App/StaticResource.php +++ b/lib/internal/Magento/Framework/App/StaticResource.php @@ -10,6 +10,7 @@ use Magento\Framework\Filesystem; use Magento\Framework\Config\ConfigOptionsListConstants; use Psr\Log\LoggerInterface; +use Magento\Framework\Debug; /** * Entry point for retrieving static resources like JS, CSS, images by requested public path @@ -138,7 +139,7 @@ public function launch() } /** - * {@inheritdoc} + * @inheritdoc */ public function catchException(Bootstrap $bootstrap, \Exception $exception) { @@ -146,7 +147,15 @@ public function catchException(Bootstrap $bootstrap, \Exception $exception) if ($bootstrap->isDeveloperMode()) { $this->response->setHttpResponseCode(404); $this->response->setHeader('Content-Type', 'text/plain'); - $this->response->setBody($exception->getMessage() . "\n" . $exception->getTraceAsString()); + $this->response->setBody( + $exception->getMessage() . "\n" . + Debug::trace( + $exception->getTrace(), + true, + true, + (bool)getenv('MAGE_DEBUG_SHOW_ARGS') + ) + ); $this->response->sendResponse(); } else { require $this->getFilesystem()->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/404.php'); diff --git a/lib/internal/Magento/Framework/App/Test/Unit/DocRootLocatorTest.php b/lib/internal/Magento/Framework/App/Test/Unit/DocRootLocatorTest.php index 23afbbc73d2b..ef4152ba2e49 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/DocRootLocatorTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/DocRootLocatorTest.php @@ -8,6 +8,9 @@ use Magento\Framework\App\DocRootLocator; +/** + * Test for Magento\Framework\App\DocRootLocator class. + */ class DocRootLocatorTest extends \PHPUnit\Framework\TestCase { /** @@ -21,11 +24,15 @@ public function testIsPub($path, $isExist, $result) { $request = $this->createMock(\Magento\Framework\App\Request\Http::class); $request->expects($this->once())->method('getServer')->willReturn($path); + + $readFactory = $this->createMock(\Magento\Framework\Filesystem\Directory\ReadFactory::class); + $reader = $this->createMock(\Magento\Framework\Filesystem\Directory\Read::class); + $filesystem = $this->createMock(\Magento\Framework\Filesystem::class); + $filesystem->expects($this->once())->method('getDirectoryRead')->willReturn($reader); $reader->expects($this->any())->method('isExist')->willReturn($isExist); - $readFactory = $this->createMock(\Magento\Framework\Filesystem\Directory\ReadFactory::class); - $readFactory->expects($this->once())->method('create')->willReturn($reader); - $model = new DocRootLocator($request, $readFactory); + + $model = new DocRootLocator($request, $readFactory, $filesystem); $this->assertSame($result, $model->isPub()); } diff --git a/lib/internal/Magento/Framework/App/Test/Unit/ErrorHandlerTest.php b/lib/internal/Magento/Framework/App/Test/Unit/ErrorHandlerTest.php index 4b904cc2b55b..bba4d67ddd10 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/ErrorHandlerTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/ErrorHandlerTest.php @@ -56,9 +56,9 @@ public function testHandlerException($errorNo, $errorPhrase) $errorFile = 'test_file'; $errorLine = 'test_error_line'; - $exceptedExceptionMessage = sprintf('%s: %s in %s on line %s', $errorPhrase, $errorStr, $errorFile, $errorLine); + $expectedExceptionMessage = sprintf('%s: %s in %s on line %s', $errorPhrase, $errorStr, $errorFile, $errorLine); $this->expectException('Exception'); - $this->expectExceptionMessage($exceptedExceptionMessage); + $this->expectExceptionMessage($expectedExceptionMessage); $this->object->handler($errorNo, $errorStr, $errorFile, $errorLine); } diff --git a/lib/internal/Magento/Framework/App/Test/Unit/HttpTest.php b/lib/internal/Magento/Framework/App/Test/Unit/HttpTest.php index a299e04e152c..dbb315e88a52 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/HttpTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/HttpTest.php @@ -92,7 +92,7 @@ protected function setUp() 'pathInfoProcessor' => $pathInfoProcessorMock, 'objectManager' => $objectManagerMock ]) - ->setMethods(['getFrontName']) + ->setMethods(['getFrontName', 'isHead']) ->getMock(); $this->areaListMock = $this->getMockBuilder(\Magento\Framework\App\AreaList::class) ->disableOriginalConstructor() @@ -135,12 +135,17 @@ private function setUpLaunch() { $frontName = 'frontName'; $areaCode = 'areaCode'; - $this->requestMock->expects($this->once())->method('getFrontName')->will($this->returnValue($frontName)); + $this->requestMock->expects($this->once()) + ->method('getFrontName') + ->willReturn($frontName); $this->areaListMock->expects($this->once()) ->method('getCodeByFrontName') - ->with($frontName)->will($this->returnValue($areaCode)); + ->with($frontName) + ->willReturn($areaCode); $this->configLoaderMock->expects($this->once()) - ->method('load')->with($areaCode)->will($this->returnValue([])); + ->method('load') + ->with($areaCode) + ->willReturn([]); $this->objectManagerMock->expects($this->once())->method('configure')->with([]); $this->objectManagerMock->expects($this->once()) ->method('get') @@ -149,12 +154,15 @@ private function setUpLaunch() $this->frontControllerMock->expects($this->once()) ->method('dispatch') ->with($this->requestMock) - ->will($this->returnValue($this->responseMock)); + ->willReturn($this->responseMock); } public function testLaunchSuccess() { $this->setUpLaunch(); + $this->requestMock->expects($this->once()) + ->method('isHead') + ->willReturn(false); $this->eventManagerMock->expects($this->once()) ->method('dispatch') ->with( @@ -171,33 +179,101 @@ public function testLaunchSuccess() public function testLaunchException() { $this->setUpLaunch(); - $this->frontControllerMock->expects($this->once())->method('dispatch')->with($this->requestMock)->will( - $this->returnCallback( - function () { - throw new \Exception('Message'); - } - ) - ); + $this->frontControllerMock->expects($this->once()) + ->method('dispatch') + ->with($this->requestMock) + ->willThrowException( + new \Exception('Message') + ); $this->http->launch(); } + /** + * Test that HEAD requests lead to an empty body and a Content-Length header matching the original body size. + * @dataProvider dataProviderForTestLaunchHeadRequest + * @param string $body + * @param int $expectedLength + */ + public function testLaunchHeadRequest($body, $expectedLength) + { + $this->setUpLaunch(); + $this->requestMock->expects($this->once()) + ->method('isHead') + ->willReturn(true); + $this->responseMock->expects($this->once()) + ->method('getHttpResponseCode') + ->willReturn(200); + $this->responseMock->expects($this->once()) + ->method('getContent') + ->willReturn($body); + $this->responseMock->expects($this->once()) + ->method('clearBody') + ->willReturn($this->responseMock); + $this->responseMock->expects($this->once()) + ->method('setHeader') + ->with('Content-Length', $expectedLength) + ->willReturn($this->responseMock); + $this->eventManagerMock->expects($this->once()) + ->method('dispatch') + ->with( + 'controller_front_send_response_before', + ['request' => $this->requestMock, 'response' => $this->responseMock] + ); + $this->assertSame($this->responseMock, $this->http->launch()); + } + + /** + * Different test content for responseMock with their expected lengths in bytes. + * @return array + */ + public function dataProviderForTestLaunchHeadRequest(): array + { + return [ + [ + "Test", // Ascii text + 43 // Expected Content-Length + ], + [ + "部落格", // Multi-byte characters + 48 // Expected Content-Length + ], + [ + "\0", // Null byte + 40 // Expected Content-Length + ], + [ + "خرید", // LTR text + 47 // Expected Content-Length + ] + ]; + } + public function testHandleDeveloperModeNotInstalled() { $dir = $this->getMockForAbstractClass(\Magento\Framework\Filesystem\Directory\ReadInterface::class); - $dir->expects($this->once())->method('getAbsolutePath')->willReturn(__DIR__); + $dir->expects($this->once()) + ->method('getAbsolutePath') + ->willReturn(__DIR__); $this->filesystemMock->expects($this->once()) ->method('getDirectoryRead') ->with(DirectoryList::ROOT) ->willReturn($dir); - $this->responseMock->expects($this->once())->method('setRedirect')->with('/_files/'); - $this->responseMock->expects($this->once())->method('sendHeaders'); + $this->responseMock->expects($this->once()) + ->method('setRedirect') + ->with('/_files/'); + $this->responseMock->expects($this->once()) + ->method('sendHeaders'); $bootstrap = $this->getBootstrapNotInstalled(); - $bootstrap->expects($this->once())->method('getParams')->willReturn([ - 'SCRIPT_NAME' => '/index.php', - 'DOCUMENT_ROOT' => __DIR__, - 'SCRIPT_FILENAME' => __DIR__ . '/index.php', - SetupInfo::PARAM_NOT_INSTALLED_URL_PATH => '_files', - ]); + $bootstrap->expects($this->once()) + ->method('getParams') + ->willReturn( + [ + 'SCRIPT_NAME' => '/index.php', + 'DOCUMENT_ROOT' => __DIR__, + 'SCRIPT_FILENAME' => __DIR__ . '/index.php', + SetupInfo::PARAM_NOT_INSTALLED_URL_PATH => '_files', + ] + ); $this->assertTrue($this->http->catchException($bootstrap, new \Exception('Test Message'))); } @@ -206,24 +282,37 @@ public function testHandleDeveloperMode() $this->filesystemMock->expects($this->once()) ->method('getDirectoryRead') ->will($this->throwException(new \Exception('strange error'))); - $this->responseMock->expects($this->once())->method('setHttpResponseCode')->with(500); - $this->responseMock->expects($this->once())->method('setHeader')->with('Content-Type', 'text/plain'); + $this->responseMock->expects($this->once()) + ->method('setHttpResponseCode') + ->with(500); + $this->responseMock->expects($this->once()) + ->method('setHeader') + ->with('Content-Type', 'text/plain'); $constraint = new \PHPUnit\Framework\Constraint\StringStartsWith('1 exception(s):'); - $this->responseMock->expects($this->once())->method('setBody')->with($constraint); - $this->responseMock->expects($this->once())->method('sendResponse'); + $this->responseMock->expects($this->once()) + ->method('setBody') + ->with($constraint); + $this->responseMock->expects($this->once()) + ->method('sendResponse'); $bootstrap = $this->getBootstrapNotInstalled(); - $bootstrap->expects($this->once())->method('getParams')->willReturn( - ['DOCUMENT_ROOT' => 'something', 'SCRIPT_FILENAME' => 'something/else'] - ); + $bootstrap->expects($this->once()) + ->method('getParams') + ->willReturn( + ['DOCUMENT_ROOT' => 'something', 'SCRIPT_FILENAME' => 'something/else'] + ); $this->assertTrue($this->http->catchException($bootstrap, new \Exception('Test'))); } public function testCatchExceptionSessionException() { - $this->responseMock->expects($this->once())->method('setRedirect'); - $this->responseMock->expects($this->once())->method('sendHeaders'); + $this->responseMock->expects($this->once()) + ->method('setRedirect'); + $this->responseMock->expects($this->once()) + ->method('sendHeaders'); $bootstrap = $this->createMock(\Magento\Framework\App\Bootstrap::class); - $bootstrap->expects($this->once())->method('isDeveloperMode')->willReturn(false); + $bootstrap->expects($this->once()) + ->method('isDeveloperMode') + ->willReturn(false); $this->assertTrue($this->http->catchException( $bootstrap, new \Magento\Framework\Exception\SessionException(new \Magento\Framework\Phrase('Test')) @@ -238,8 +327,12 @@ public function testCatchExceptionSessionException() private function getBootstrapNotInstalled() { $bootstrap = $this->createMock(\Magento\Framework\App\Bootstrap::class); - $bootstrap->expects($this->once())->method('isDeveloperMode')->willReturn(true); - $bootstrap->expects($this->once())->method('getErrorCode')->willReturn(Bootstrap::ERR_IS_INSTALLED); + $bootstrap->expects($this->once()) + ->method('isDeveloperMode') + ->willReturn(true); + $bootstrap->expects($this->once()) + ->method('getErrorCode') + ->willReturn(Bootstrap::ERR_IS_INSTALLED); return $bootstrap; } } diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Language/DictionaryTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Language/DictionaryTest.php index 472fff4f4f28..748337443d7a 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/Language/DictionaryTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/Language/DictionaryTest.php @@ -52,7 +52,7 @@ public function testDictionaryGetter() } $file = $this->getMockForAbstractClass(\Magento\Framework\Filesystem\File\ReadInterface::class); - for ($i = 0; $i < count($data); $i++) { + for ($i = 0, $count = count($data); $i < $count; $i++) { $file->expects($this->at($i))->method('readCsv')->will($this->returnValue($data[$i])); } $file->expects($this->at($i))->method('readCsv')->will($this->returnValue(false)); diff --git a/lib/internal/Magento/Framework/App/Test/Unit/MaintenanceModeTest.php b/lib/internal/Magento/Framework/App/Test/Unit/MaintenanceModeTest.php index 5d1c22a38af4..5970d2561660 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/MaintenanceModeTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/MaintenanceModeTest.php @@ -6,9 +6,17 @@ namespace Magento\Framework\App\Test\Unit; -use \Magento\Framework\App\MaintenanceMode; +use Magento\Framework\App\MaintenanceMode; +use Magento\Framework\Event\Manager; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\Filesystem; +use PHPUnit\Framework\TestCase; -class MaintenanceModeTest extends \PHPUnit\Framework\TestCase +/** + * MaintenanceMode Test + */ +class MaintenanceModeTest extends TestCase { /** * @var MaintenanceMode @@ -16,141 +24,213 @@ class MaintenanceModeTest extends \PHPUnit\Framework\TestCase protected $model; /** - * @var \Magento\Framework\Filesystem\Directory\WriteInterface | \PHPUnit_Framework_MockObject_MockObject + * @var WriteInterface|\PHPUnit\Framework\MockObject\MockObject */ protected $flagDir; + /** + * @var Manager|\PHPUnit\Framework\MockObject\MockObject + */ + private $eventManager; + + /** + * @inheritdoc + */ protected function setup() { - $this->flagDir = $this->getMockForAbstractClass(\Magento\Framework\Filesystem\Directory\WriteInterface::class); - $filesystem = $this->createMock(\Magento\Framework\Filesystem::class); - $filesystem->expects($this->any()) - ->method('getDirectoryWrite') - ->will($this->returnValue($this->flagDir)); + $this->flagDir = $this->getMockForAbstractClass(WriteInterface::class); + $filesystem = $this->createMock(Filesystem::class); + $filesystem->method('getDirectoryWrite') + ->willReturn($this->flagDir); + $this->eventManager = $this->createMock(Manager::class); - $this->model = new MaintenanceMode($filesystem); + $objectManager = new ObjectManager($this); + $this->model = $objectManager->getObject(MaintenanceMode::class, [ + 'filesystem' => $filesystem, + 'eventManager' => $this->eventManager, + ]); } + /** + * Is On initial test + * + * @return void + */ public function testIsOnInitial() { - $this->flagDir->expects($this->once())->method('isExist') + $this->flagDir->expects($this->once()) + ->method('isExist') ->with(MaintenanceMode::FLAG_FILENAME) - ->will($this->returnValue(false)); + ->willReturn(false); $this->assertFalse($this->model->isOn()); } + /** + * Is On without ip test + * + * @return void + */ public function testisOnWithoutIP() { $mapisExist = [ [MaintenanceMode::FLAG_FILENAME, true], [MaintenanceMode::IP_FILENAME, false], ]; - $this->flagDir->expects($this->exactly(2))->method('isExist') - ->will(($this->returnValueMap($mapisExist))); + $this->flagDir->expects($this->exactly(2)) + ->method('isExist') + ->willReturnMap($mapisExist); $this->assertTrue($this->model->isOn()); } + /** + * Is On with IP test + * + * @return void + */ public function testisOnWithIP() { $mapisExist = [ [MaintenanceMode::FLAG_FILENAME, true], [MaintenanceMode::IP_FILENAME, true], ]; - $this->flagDir->expects($this->exactly(2))->method('isExist') - ->will(($this->returnValueMap($mapisExist))); + $this->flagDir->expects($this->exactly(2)) + ->method('isExist') + ->willReturnMap($mapisExist); $this->assertFalse($this->model->isOn()); } + /** + * Is On with IP but no Maintenance files test + * + * @return void + */ public function testisOnWithIPNoMaintenance() { - $this->flagDir->expects($this->once())->method('isExist') + $this->flagDir->expects($this->once()) + ->method('isExist') ->with(MaintenanceMode::FLAG_FILENAME) ->willReturn(false); $this->assertFalse($this->model->isOn()); } + /** + * Maintenance Mode On test + * + * Tests common scenario with Full Page Cache is set to On + * + * @return void + */ public function testMaintenanceModeOn() { - $this->flagDir->expects($this->at(0))->method('isExist')->with(MaintenanceMode::FLAG_FILENAME) - ->will($this->returnValue(false)); - $this->flagDir->expects($this->at(1))->method('touch')->will($this->returnValue(true)); - $this->flagDir->expects($this->at(2))->method('isExist')->with(MaintenanceMode::FLAG_FILENAME) - ->will($this->returnValue(true)); - $this->flagDir->expects($this->at(3))->method('isExist')->with(MaintenanceMode::IP_FILENAME) - ->will($this->returnValue(false)); + $this->eventManager->expects($this->once()) + ->method('dispatch') + ->with('maintenance_mode_changed', ['isOn' => true]); - $this->assertFalse($this->model->isOn()); - $this->assertTrue($this->model->set(true)); - $this->assertTrue($this->model->isOn()); + $this->flagDir->expects($this->once()) + ->method('touch') + ->with(MaintenanceMode::FLAG_FILENAME); + + $this->model->set(true); } + /** + * Maintenance Mode Off test + * + * Tests common scenario when before Maintenance Mode Full Page Cache was setted to on + * + * @return void + */ public function testMaintenanceModeOff() { - $this->flagDir->expects($this->at(0))->method('isExist')->with(MaintenanceMode::FLAG_FILENAME) - ->will($this->returnValue(true)); - $this->flagDir->expects($this->at(1))->method('delete')->with(MaintenanceMode::FLAG_FILENAME) - ->will($this->returnValue(false)); - $this->flagDir->expects($this->at(2))->method('isExist')->with(MaintenanceMode::FLAG_FILENAME) - ->will($this->returnValue(false)); - - $this->assertFalse($this->model->set(false)); - $this->assertFalse($this->model->isOn()); + $this->eventManager->expects($this->once()) + ->method('dispatch') + ->with('maintenance_mode_changed', ['isOn' => false]); + + $this->flagDir->method('isExist') + ->with(MaintenanceMode::FLAG_FILENAME) + ->willReturn(true); + + $this->flagDir->expects($this->once()) + ->method('delete') + ->with(MaintenanceMode::FLAG_FILENAME); + + $this->model->set(false); } + /** + * Set empty addresses test + * + * @return void + */ public function testSetAddresses() { $mapisExist = [ [MaintenanceMode::FLAG_FILENAME, true], [MaintenanceMode::IP_FILENAME, true], ]; - $this->flagDir->expects($this->any())->method('isExist')->will($this->returnValueMap($mapisExist)); - $this->flagDir->expects($this->any())->method('writeFile') + $this->flagDir->method('isExist') + ->willReturnMap($mapisExist); + $this->flagDir->method('writeFile') ->with(MaintenanceMode::IP_FILENAME) - ->will($this->returnValue(true)); + ->willReturn(true); - $this->flagDir->expects($this->any())->method('readFile') + $this->flagDir->method('readFile') ->with(MaintenanceMode::IP_FILENAME) - ->will($this->returnValue('')); + ->willReturn(''); $this->model->setAddresses(''); $this->assertEquals([''], $this->model->getAddressInfo()); } + /** + * Set single address test + * + * @return void + */ public function testSetSingleAddresses() { $mapisExist = [ [MaintenanceMode::FLAG_FILENAME, true], [MaintenanceMode::IP_FILENAME, true], ]; - $this->flagDir->expects($this->any())->method('isExist')->will($this->returnValueMap($mapisExist)); - $this->flagDir->expects($this->any())->method('delete')->will($this->returnValueMap($mapisExist)); + $this->flagDir->method('isExist') + ->willReturnMap($mapisExist); + $this->flagDir->method('delete') + ->willReturnMap($mapisExist); - $this->flagDir->expects($this->any())->method('writeFile') - ->will($this->returnValue(10)); + $this->flagDir->method('writeFile') + ->willReturn(10); - $this->flagDir->expects($this->any())->method('readFile') + $this->flagDir->method('readFile') ->with(MaintenanceMode::IP_FILENAME) - ->will($this->returnValue('address1')); + ->willReturn('address1'); $this->model->setAddresses('address1'); $this->assertEquals(['address1'], $this->model->getAddressInfo()); } + /** + * Is On when multiple addresses test was setted + * + * @return void + */ public function testOnSetMultipleAddresses() { $mapisExist = [ [MaintenanceMode::FLAG_FILENAME, true], [MaintenanceMode::IP_FILENAME, true], ]; - $this->flagDir->expects($this->any())->method('isExist')->will($this->returnValueMap($mapisExist)); - $this->flagDir->expects($this->any())->method('delete')->will($this->returnValueMap($mapisExist)); + $this->flagDir->method('isExist') + ->willReturnMap($mapisExist); + $this->flagDir->method('delete') + ->willReturnMap($mapisExist); - $this->flagDir->expects($this->any())->method('writeFile') - ->will($this->returnValue(10)); + $this->flagDir->method('writeFile') + ->willReturn(10); - $this->flagDir->expects($this->any())->method('readFile') + $this->flagDir->method('readFile') ->with(MaintenanceMode::IP_FILENAME) - ->will($this->returnValue('address1,10.50.60.123')); + ->willReturn('address1,10.50.60.123'); $expectedArray = ['address1', '10.50.60.123']; $this->model->setAddresses('address1,10.50.60.123'); @@ -159,18 +239,25 @@ public function testOnSetMultipleAddresses() $this->assertTrue($this->model->isOn('address3')); } + /** + * Is Off when multiple addresses test was setted + * + * @return void + */ public function testOffSetMultipleAddresses() { $mapisExist = [ [MaintenanceMode::FLAG_FILENAME, false], [MaintenanceMode::IP_FILENAME, true], ]; - $this->flagDir->expects($this->any())->method('isExist')->will($this->returnValueMap($mapisExist)); - $this->flagDir->expects($this->any())->method('delete')->will($this->returnValueMap($mapisExist)); + $this->flagDir->method('isExist') + ->willReturnMap($mapisExist); + $this->flagDir->method('delete') + ->willReturnMap($mapisExist); - $this->flagDir->expects($this->any())->method('readFile') + $this->flagDir->method('readFile') ->with(MaintenanceMode::IP_FILENAME) - ->will($this->returnValue('address1,10.50.60.123')); + ->willReturn('address1,10.50.60.123'); $expectedArray = ['address1', '10.50.60.123']; $this->model->setAddresses('address1,10.50.60.123'); diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Request/HttpMethodMapTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Request/HttpMethodMapTest.php new file mode 100644 index 000000000000..6215da3ae901 --- /dev/null +++ b/lib/internal/Magento/Framework/App/Test/Unit/Request/HttpMethodMapTest.php @@ -0,0 +1,49 @@ + '\\Throwable', 'method2' => 'DateTime'] + ); + $this->assertEquals( + ['method1' => \Throwable::class, 'method2' => \DateTime::class], + $map->getMap() + ); + } + + /** + * Test validation of interface names. + * + * @expectedException \InvalidArgumentException + */ + public function testExisting() + { + new HttpMethodMap(['method1' => 'NonExistingClass']); + } + + /** + * Test validation of method names. + * + * @expectedException \InvalidArgumentException + */ + public function testMethod() + { + new HttpMethodMap([\Throwable::class]); + } +} diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Request/HttpTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Request/HttpTest.php index 62445d4244fb..81805e2df969 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/Request/HttpTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/Request/HttpTest.php @@ -10,32 +10,41 @@ use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Request\Http; +/** + * @SuppressWarnings(PHPMD.TooManyMethods) + * @SuppressWarnings(PHPMD.TooManyPublicMethods) + */ class HttpTest extends \PHPUnit\Framework\TestCase { /** * @var \Magento\Framework\App\Request\Http */ - protected $_model; + private $model; /** * @var \Magento\Framework\App\Route\ConfigInterface\Proxy | \PHPUnit_Framework_MockObject_MockObject */ - protected $_routerListMock; + private $routerListMock; /** * @var \Magento\Framework\App\Request\PathInfoProcessorInterface | \PHPUnit_Framework_MockObject_MockObject */ - protected $_infoProcessorMock; + private $infoProcessorMock; + + /** + * @var \Magento\Framework\App\Request\PathInfo + */ + private $pathInfo; /** * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager | \PHPUnit_Framework_MockObject_MockObject */ - protected $objectManagerMock; + private $objectManagerMock; /** * @var \Magento\Framework\Stdlib\StringUtils | \PHPUnit_Framework_MockObject_MockObject */ - protected $converterMock; + private $converterMock; /** * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager @@ -49,12 +58,12 @@ class HttpTest extends \PHPUnit\Framework\TestCase protected function setUp() { - $this->_routerListMock = $this->createPartialMock( + $this->routerListMock = $this->createPartialMock( \Magento\Framework\App\Route\ConfigInterface\Proxy::class, ['getRouteFrontName', 'getRouteByFrontName', '__wakeup'] ); - $this->_infoProcessorMock = $this->createMock(\Magento\Framework\App\Request\PathInfoProcessorInterface::class); - $this->_infoProcessorMock->expects($this->any())->method('process')->will($this->returnArgument(1)); + $this->infoProcessorMock = $this->createMock(\Magento\Framework\App\Request\PathInfoProcessorInterface::class); + $this->infoProcessorMock->expects($this->any())->method('process')->will($this->returnArgument(1)); $this->objectManagerMock = $this->createMock(\Magento\Framework\ObjectManagerInterface::class); $this->converterMock = $this->getMockBuilder(\Magento\Framework\Stdlib\StringUtils::class) ->disableOriginalConstructor() @@ -66,6 +75,7 @@ protected function setUp() $this->serverArray = $_SERVER; $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->pathInfo = $this->objectManager->getObject(\Magento\Framework\App\Request\PathInfo::class); } public function tearDown() @@ -81,8 +91,9 @@ private function getModel($uri = null, $appConfigMock = true) $model = $this->objectManager->getObject( \Magento\Framework\App\Request\Http::class, [ - 'routeConfig' => $this->_routerListMock, - 'pathInfoProcessor' => $this->_infoProcessorMock, + 'routeConfig' => $this->routerListMock, + 'pathInfoProcessor' => $this->infoProcessorMock, + 'pathInfoService' => $this->pathInfo, 'objectManager' => $this->objectManagerMock, 'converter' => $this->converterMock, 'uri' => $uri, @@ -100,91 +111,91 @@ private function getModel($uri = null, $appConfigMock = true) public function testGetOriginalPathInfoWithTestUri() { $uri = 'http://test.com/value?key=value'; - $this->_model = $this->getModel($uri); - $this->assertEquals('/value', $this->_model->getOriginalPathInfo()); + $this->model = $this->getModel($uri); + $this->assertEquals('/value', $this->model->getOriginalPathInfo()); } public function testGetOriginalPathInfoWithEmptyUri() { - $this->_model = $this->getModel(); - $this->assertEmpty($this->_model->getOriginalPathInfo()); + $this->model = $this->getModel(); + $this->assertEmpty($this->model->getOriginalPathInfo()); } public function testGetBasePathWithPath() { - $this->_model = $this->getModel(); - $this->_model->setBasePath('http:\/test.com\one/two'); - $this->assertEquals('http://test.com/one/two', $this->_model->getBasePath()); + $this->model = $this->getModel(); + $this->model->setBasePath('http:\/test.com\one/two'); + $this->assertEquals('http://test.com/one/two', $this->model->getBasePath()); } public function testGetBasePathWithoutPath() { - $this->_model = $this->getModel(); - $this->_model->setBasePath(null); - $this->assertEquals('/', $this->_model->getBasePath()); + $this->model = $this->getModel(); + $this->model->setBasePath(null); + $this->assertEquals('/', $this->model->getBasePath()); } public function testSetRouteNameWithRouter() { $router = $this->createMock(\Magento\Framework\App\Route\ConfigInterface::class); - $this->_routerListMock->expects($this->any())->method('getRouteFrontName')->will($this->returnValue($router)); - $this->_model = $this->getModel(); - $this->_model->setRouteName('RouterName'); - $this->assertEquals('RouterName', $this->_model->getRouteName()); + $this->routerListMock->expects($this->any())->method('getRouteFrontName')->will($this->returnValue($router)); + $this->model = $this->getModel(); + $this->model->setRouteName('RouterName'); + $this->assertEquals('RouterName', $this->model->getRouteName()); } public function testSetRouteNameWithNullRouterValue() { - $this->_model = $this->getModel(); - $this->_routerListMock->expects($this->once())->method('getRouteFrontName')->will($this->returnValue(null)); - $this->_model->setRouteName('RouterName'); + $this->model = $this->getModel(); + $this->routerListMock->expects($this->once())->method('getRouteFrontName')->will($this->returnValue(null)); + $this->model->setRouteName('RouterName'); } public function testGetFrontName() { $uri = 'http://test.com/one/two'; - $this->_model = $this->getModel($uri); - $this->assertEquals('one', $this->_model->getFrontName()); + $this->model = $this->getModel($uri); + $this->assertEquals('one', $this->model->getFrontName()); } public function testGetRouteNameWithNullValueRouteName() { - $this->_model = $this->getModel(); - $this->_model->setRouteName('RouteName'); - $this->assertEquals('RouteName', $this->_model->getRouteName()); + $this->model = $this->getModel(); + $this->model->setRouteName('RouteName'); + $this->assertEquals('RouteName', $this->model->getRouteName()); } public function testGetRouteName() { - $this->_model = $this->getModel(); + $this->model = $this->getModel(); $expected = 'RouteName'; - $this->_model->setRouteName($expected); - $this->assertEquals($expected, $this->_model->getRouteName()); + $this->model->setRouteName($expected); + $this->assertEquals($expected, $this->model->getRouteName()); } public function testGetFullActionName() { - $this->_model = $this->getModel(); + $this->model = $this->getModel(); /* empty request */ - $this->assertEquals('__', $this->_model->getFullActionName()); - $this->_model->setRouteName('test')->setControllerName('controller')->setActionName('action'); - $this->assertEquals('test/controller/action', $this->_model->getFullActionName('/')); + $this->assertEquals('__', $this->model->getFullActionName()); + $this->model->setRouteName('test')->setControllerName('controller')->setActionName('action'); + $this->assertEquals('test/controller/action', $this->model->getFullActionName('/')); } public function testInitForward() { - $expected = $this->_initForward(); - $this->assertEquals($expected, $this->_model->getBeforeForwardInfo()); + $expected = $this->initForward(); + $this->assertEquals($expected, $this->model->getBeforeForwardInfo()); } public function testGetBeforeForwardInfo() { - $beforeForwardInfo = $this->_initForward(); - $this->assertNull($this->_model->getBeforeForwardInfo('not_existing_forward_info_key')); + $beforeForwardInfo = $this->initForward(); + $this->assertNull($this->model->getBeforeForwardInfo('not_existing_forward_info_key')); foreach (array_keys($beforeForwardInfo) as $key) { - $this->assertEquals($beforeForwardInfo[$key], $this->_model->getBeforeForwardInfo($key)); + $this->assertEquals($beforeForwardInfo[$key], $this->model->getBeforeForwardInfo($key)); } - $this->assertEquals($beforeForwardInfo, $this->_model->getBeforeForwardInfo()); + $this->assertEquals($beforeForwardInfo, $this->model->getBeforeForwardInfo()); } /** @@ -192,9 +203,9 @@ public function testGetBeforeForwardInfo() * * @return array Contents of $_beforeForwardInfo */ - protected function _initForward() + private function initForward() { - $this->_model = $this->getModel(); + $this->model = $this->getModel(); $beforeForwardInfo = [ 'params' => ['one' => '111', 'two' => '222'], 'action_name' => 'ActionName', @@ -202,49 +213,49 @@ protected function _initForward() 'module_name' => 'ModuleName', 'route_name' => 'RouteName' ]; - $this->_model->setParams($beforeForwardInfo['params']); - $this->_model->setActionName($beforeForwardInfo['action_name']); - $this->_model->setControllerName($beforeForwardInfo['controller_name']); - $this->_model->setModuleName($beforeForwardInfo['module_name']); - $this->_model->setRouteName($beforeForwardInfo['route_name']); - $this->_model->initForward(); + $this->model->setParams($beforeForwardInfo['params']); + $this->model->setActionName($beforeForwardInfo['action_name']); + $this->model->setControllerName($beforeForwardInfo['controller_name']); + $this->model->setModuleName($beforeForwardInfo['module_name']); + $this->model->setRouteName($beforeForwardInfo['route_name']); + $this->model->initForward(); return $beforeForwardInfo; } public function testIsAjax() { - $this->_model = $this->getModel(); + $this->model = $this->getModel(); - $this->assertFalse($this->_model->isAjax()); + $this->assertFalse($this->model->isAjax()); - $this->_model->clearParams(); - $this->_model->setParam('ajax', 1); - $this->assertTrue($this->_model->isAjax()); + $this->model->clearParams(); + $this->model->setParam('ajax', 1); + $this->assertTrue($this->model->isAjax()); - $this->_model->clearParams(); - $this->_model->setParam('isAjax', 1); - $this->assertTrue($this->_model->isAjax()); + $this->model->clearParams(); + $this->model->setParam('isAjax', 1); + $this->assertTrue($this->model->isAjax()); - $this->_model->clearParams(); - $this->_model->getHeaders()->addHeaderLine('X-Requested-With', 'XMLHttpRequest'); - $this->assertTrue($this->_model->isAjax()); + $this->model->clearParams(); + $this->model->getHeaders()->addHeaderLine('X-Requested-With', 'XMLHttpRequest'); + $this->assertTrue($this->model->isAjax()); - $this->_model->getHeaders()->clearHeaders(); - $this->_model->getHeaders()->addHeaderLine('X-Requested-With', 'NotXMLHttpRequest'); - $this->assertFalse($this->_model->isAjax()); + $this->model->getHeaders()->clearHeaders(); + $this->model->getHeaders()->addHeaderLine('X-Requested-With', 'NotXMLHttpRequest'); + $this->assertFalse($this->model->isAjax()); } /** - * @param $serverVariables array - * @param $expectedResult string + * @param array $serverVariables + * @param string $expectedResult * @dataProvider serverVariablesProvider */ public function testGetDistroBaseUrl($serverVariables, $expectedResult) { $originalServerValue = $_SERVER; $_SERVER = $serverVariables; - $this->_model = $this->getModel(); - $this->assertEquals($expectedResult, $this->_model->getDistroBaseUrl()); + $this->model = $this->getModel(); + $this->assertEquals($expectedResult, $this->model->getDistroBaseUrl()); $_SERVER = $originalServerValue; } @@ -338,7 +349,7 @@ public function serverVariablesProvider() */ public function testIsSecure($isSecure, $serverHttps, $headerOffloadKey, $headerOffloadValue, $configCall) { - $this->_model = $this->getModel(null, false); + $this->model = $this->getModel(null, false); $configOffloadHeader = 'Header-From-Proxy'; $configMock = $this->getMockBuilder(\Magento\Framework\App\Config::class) ->disableOriginalConstructor() @@ -351,37 +362,37 @@ public function testIsSecure($isSecure, $serverHttps, $headerOffloadKey, $header ScopeConfigInterface::SCOPE_TYPE_DEFAULT )->willReturn($configOffloadHeader); - $this->objectManager->setBackwardCompatibleProperty($this->_model, 'appConfig', $configMock); - $this->objectManager->setBackwardCompatibleProperty($this->_model, 'sslOffloadHeader', null); + $this->objectManager->setBackwardCompatibleProperty($this->model, 'appConfig', $configMock); + $this->objectManager->setBackwardCompatibleProperty($this->model, 'sslOffloadHeader', null); - $this->_model->getServer()->set($headerOffloadKey, $headerOffloadValue); - $this->_model->getServer()->set('HTTPS', $serverHttps); + $this->model->getServer()->set($headerOffloadKey, $headerOffloadValue); + $this->model->getServer()->set('HTTPS', $serverHttps); - $this->assertSame($isSecure, $this->_model->isSecure()); + $this->assertSame($isSecure, $this->model->isSecure()); } /** * @dataProvider httpSafeMethodProvider * @backupGlobals enabled - * @param string $method value of $_SERVER['REQUEST_METHOD'] + * @param string $httpMethod value of $_SERVER['REQUEST_METHOD'] */ public function testIsSafeMethodTrue($httpMethod) { - $this->_model = $this->getModel(); + $this->model = $this->getModel(); $_SERVER['REQUEST_METHOD'] = $httpMethod; - $this->assertEquals(true, $this->_model->isSafeMethod()); + $this->assertEquals(true, $this->model->isSafeMethod()); } /** * @dataProvider httpNotSafeMethodProvider * @backupGlobals enabled - * @param string $method value of $_SERVER['REQUEST_METHOD'] + * @param string $httpMethod value of $_SERVER['REQUEST_METHOD'] */ public function testIsSafeMethodFalse($httpMethod) { - $this->_model = $this->getModel(); + $this->model = $this->getModel(); $_SERVER['REQUEST_METHOD'] = $httpMethod; - $this->assertEquals(false, $this->_model->isSafeMethod()); + $this->assertEquals(false, $this->model->isSafeMethod()); } /** @@ -449,12 +460,25 @@ public function isSecureDataProvider() * @param string $basePath$ * @param string $expected */ - public function testSetPathInfo($requestUri, $basePath, $expected) + public function testGetPathInfo($requestUri, $basePath, $expected) + { + $this->model = $this->getModel($requestUri); + $this->model->setBaseUrl($basePath); + $this->assertEquals($expected, $this->model->getPathInfo()); + $this->assertEquals($expected, $this->model->getOriginalPathInfo()); + } + + public function testSetPathInfo() { - $this->_model = $this->getModel($requestUri); - $this->_model->setBaseUrl($basePath); - $this->_model->setPathInfo(); - $this->assertEquals($expected, $this->_model->getPathInfo()); + $requestUri = 'http://svr.com//module/route/mypage/myproduct?param1=1'; + $basePath = '/module/route/'; + $this->model = $this->getModel($requestUri); + $this->model->setBaseUrl($basePath); + $expected = '/mypage/myproduct'; + $this->assertEquals($expected, $this->model->getOriginalPathInfo()); + $this->model->setPathInfo('http://svr.com/something/route?param1=1'); + $this->assertEquals('http://svr.com/something/route?param1=1', $this->model->getPathInfo()); + $this->assertEquals($expected, $this->model->getOriginalPathInfo()); } /** diff --git a/lib/internal/Magento/Framework/App/Test/Unit/ResourceConnection/Config/SchemaLocatorTest.php b/lib/internal/Magento/Framework/App/Test/Unit/ResourceConnection/Config/SchemaLocatorTest.php index 8a5c071e8c4e..921fcf2e61e6 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/ResourceConnection/Config/SchemaLocatorTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/ResourceConnection/Config/SchemaLocatorTest.php @@ -13,7 +13,7 @@ class SchemaLocatorTest extends \PHPUnit\Framework\TestCase */ protected $model; - /** @var \Magento\Framework\Config\Dom\UrnResolver $urnResolverMock */ + /** @var \Magento\Framework\Config\Dom\UrnResolver */ protected $urnResolver; protected function setUp() diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Response/Http/FileFactoryTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Response/Http/FileFactoryTest.php index 11e305f806ba..7ad2f21c89d7 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/Response/Http/FileFactoryTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/Response/Http/FileFactoryTest.php @@ -67,7 +67,7 @@ public function testCreateIfContentDoesntHaveRequiredKeys() /** * @expectedException \Exception - * @exceptedExceptionMessage File not found + * @expectedExceptionMessage File not found */ public function testCreateIfFileNotExist() { diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Response/HttpTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Response/HttpTest.php index efb35b7321c3..9be68b379900 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/Response/HttpTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/Response/HttpTest.php @@ -290,45 +290,6 @@ public function testRepresentJson() $this->assertEquals('json_string', $this->model->getBody('default')); } - /** - * - * @expectedException \RuntimeException - * @expectedExceptionMessage ObjectManager isn't initialized - */ - public function testWakeUpWithException() - { - /* ensure that the test preconditions are met */ - $objectManagerClass = new \ReflectionClass(\Magento\Framework\App\ObjectManager::class); - $instanceProperty = $objectManagerClass->getProperty('_instance'); - $instanceProperty->setAccessible(true); - $instanceProperty->setValue(null); - - $this->model->__wakeup(); - $this->assertNull($this->cookieMetadataFactoryMock); - $this->assertNull($this->cookieManagerMock); - } - - /** - * Test for the magic method __wakeup - * - * @covers \Magento\Framework\App\Response\Http::__wakeup - */ - public function testWakeUpWith() - { - $objectManagerMock = $this->createMock(\Magento\Framework\App\ObjectManager::class); - $objectManagerMock->expects($this->once()) - ->method('create') - ->with(\Magento\Framework\Stdlib\CookieManagerInterface::class) - ->will($this->returnValue($this->cookieManagerMock)); - $objectManagerMock->expects($this->at(1)) - ->method('get') - ->with(\Magento\Framework\Stdlib\Cookie\CookieMetadataFactory::class) - ->will($this->returnValue($this->cookieMetadataFactoryMock)); - - \Magento\Framework\App\ObjectManager::setInstance($objectManagerMock); - $this->model->__wakeup(); - } - public function testSetXFrameOptions() { $value = 'DENY'; diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Route/Config/SchemaLocatorTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Route/Config/SchemaLocatorTest.php index 143de5ee47fa..61938b455295 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/Route/Config/SchemaLocatorTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/Route/Config/SchemaLocatorTest.php @@ -12,10 +12,10 @@ class SchemaLocatorTest extends \PHPUnit\Framework\TestCase */ protected $config; - /** @var \Magento\Framework\Config\Dom\UrnResolver $urnResolverMock */ + /** @var \Magento\Framework\Config\Dom\UrnResolver */ protected $urnResolver; - /** @var \Magento\Framework\Config\Dom\UrnResolver $urnResolverMock */ + /** @var \Magento\Framework\Config\Dom\UrnResolver */ protected $urnResolverMock; protected function setUp() diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Router/BaseTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Router/BaseTest.php index 3d44073a24b8..94a7330c322b 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/Router/BaseTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/Router/BaseTest.php @@ -7,6 +7,9 @@ */ namespace Magento\Framework\App\Test\Unit\Router; +/** + * Base router unit test. + */ class BaseTest extends \Magento\Framework\TestFramework\Unit\BaseTestCase { /** @@ -83,11 +86,13 @@ public function testMatch() $actionClassName = \Magento\Framework\App\Action\Action::class; $moduleName = 'module name'; $moduleList = [$moduleName]; + $paramList = $moduleFrontName . '/' . $actionPath . '/' . $actionName . '/key/val/key2/val2/'; // Stubs $this->requestMock->expects($this->any())->method('getModuleName')->willReturn($moduleFrontName); $this->requestMock->expects($this->any())->method('getControllerName')->willReturn($actionPath); $this->requestMock->expects($this->any())->method('getActionName')->willReturn($actionName); + $this->requestMock->expects($this->any())->method('getPathInfo')->willReturn($paramList); $this->routeConfigMock->expects($this->any())->method('getModulesByFrontName')->willReturn($moduleList); $this->appStateMock->expects($this->any())->method('isInstalled')->willReturn(true); $this->actionListMock->expects($this->any())->method('get')->willReturn($actionClassName); @@ -140,6 +145,7 @@ public function testMatchUseDefaultPath() $actionClassName = \Magento\Framework\App\Action\Action::class; $moduleName = 'module name'; $moduleList = [$moduleName]; + $paramList = $moduleFrontName . '/' . $actionPath . '/' . $actionName . '/key/val/key2/val2/'; // Stubs $defaultReturnMap = [ @@ -147,6 +153,7 @@ public function testMatchUseDefaultPath() ['controller', $actionPath], ['action', $actionName], ]; + $this->requestMock->expects($this->any())->method('getPathInfo')->willReturn($paramList); $this->defaultPathMock->expects($this->any())->method('getPart')->willReturnMap($defaultReturnMap); $this->routeConfigMock->expects($this->any())->method('getModulesByFrontName')->willReturn($moduleList); $this->appStateMock->expects($this->any())->method('isInstalled')->willReturn(false); @@ -171,9 +178,11 @@ public function testMatchEmptyModuleList() $actionName = 'action name'; $actionClassName = \Magento\Framework\App\Action\Action::class; $emptyModuleList = []; + $paramList = $moduleFrontName . '/' . $actionPath . '/' . $actionName . '/key/val/key2/val2/'; // Stubs $this->requestMock->expects($this->any())->method('getModuleName')->willReturn($moduleFrontName); + $this->requestMock->expects($this->any())->method('getPathInfo')->willReturn($paramList); $this->routeConfigMock->expects($this->any())->method('getModulesByFrontName')->willReturn($emptyModuleList); $this->requestMock->expects($this->any())->method('getControllerName')->willReturn($actionPath); $this->requestMock->expects($this->any())->method('getActionName')->willReturn($actionName); @@ -195,9 +204,11 @@ public function testMatchEmptyActionInstance() $actionClassName = \Magento\Framework\App\Action\Action::class; $moduleName = 'module name'; $moduleList = [$moduleName]; + $paramList = $moduleFrontName . '/' . $actionPath . '/' . $actionName . '/key/val/key2/val2/'; // Stubs $this->requestMock->expects($this->any())->method('getModuleName')->willReturn($moduleFrontName); + $this->requestMock->expects($this->any())->method('getPathInfo')->willReturn($paramList); $this->routeConfigMock->expects($this->any())->method('getModulesByFrontName')->willReturn($moduleList); $this->requestMock->expects($this->any())->method('getControllerName')->willReturn($actionPath); $this->requestMock->expects($this->any())->method('getActionName')->willReturn($actionName); diff --git a/lib/internal/Magento/Framework/App/Test/Unit/ViewTest.php b/lib/internal/Magento/Framework/App/Test/Unit/ViewTest.php index 807a70ecd759..f39a91161c1f 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/ViewTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/ViewTest.php @@ -122,7 +122,7 @@ public function testGetLayout() /** * @expectedException \RuntimeException - * @exceptedExceptionMessage 'Layout must be loaded only once.' + * @expectedExceptionMessage Layout must be loaded only once. */ public function testLoadLayoutWhenLayoutAlreadyLoaded() { diff --git a/lib/internal/Magento/Framework/App/Utility/Classes.php b/lib/internal/Magento/Framework/App/Utility/Classes.php index 7cd36f0e37fc..3518264e1ab3 100644 --- a/lib/internal/Magento/Framework/App/Utility/Classes.php +++ b/lib/internal/Magento/Framework/App/Utility/Classes.php @@ -9,6 +9,9 @@ use Magento\Framework\Component\ComponentRegistrar; +/** + * Utility for class names processing + */ class Classes { /** @@ -236,7 +239,7 @@ public static function getVirtualClasses() /** * Check if instance is virtual type * - * @param $className string + * @param string $className * @return bool */ public static function isVirtual($className) diff --git a/lib/internal/Magento/Framework/Archive.php b/lib/internal/Magento/Framework/Archive.php index d43c976431b5..3b706f8e97d0 100644 --- a/lib/internal/Magento/Framework/Archive.php +++ b/lib/internal/Magento/Framework/Archive.php @@ -96,14 +96,14 @@ public function pack($source, $destination = 'packed.tgz', $skipRoot = false) { $archivers = $this->_getArchivers($destination); $interimSource = ''; - for ($i = 0; $i < count($archivers); $i++) { - if ($i == count($archivers) - 1) { + for ($i = 0, $count = count($archivers); $i < $count; $i++) { + if ($i == $count - 1) { $packed = $destination; } else { $packed = dirname($destination) . '/~tmp-' . microtime(true) . $archivers[$i] . '.' . $archivers[$i]; } $source = $this->_getArchiver($archivers[$i])->pack($source, $packed, $skipRoot); - if ($interimSource && $i < count($archivers)) { + if ($interimSource && $i < $count) { unlink($interimSource); } $interimSource = $source; diff --git a/lib/internal/Magento/Framework/Archive/Tar.php b/lib/internal/Magento/Framework/Archive/Tar.php index 7fe1255e5b85..a858b2411515 100644 --- a/lib/internal/Magento/Framework/Archive/Tar.php +++ b/lib/internal/Magento/Framework/Archive/Tar.php @@ -4,15 +4,15 @@ * See COPYING.txt for license details. */ +namespace Magento\Framework\Archive; + +use Magento\Framework\Archive\Helper\File; + /** * Class to work with tar archives * * @author Magento Core Team */ -namespace Magento\Framework\Archive; - -use Magento\Framework\Archive\Helper\File; - class Tar extends \Magento\Framework\Archive\AbstractArchive implements \Magento\Framework\Archive\ArchiveInterface { /** @@ -259,10 +259,7 @@ protected function _createTar($skipRoot = false, $finalize = false) ); } - array_shift($dirFiles); - /* remove './'*/ - array_shift($dirFiles); - /* remove '../'*/ + $dirFiles = array_diff($dirFiles, ['..', '.']); foreach ($dirFiles as $item) { $this->_setCurrentFile($file . $item)->_createTar(); diff --git a/lib/internal/Magento/Framework/Archive/Zip.php b/lib/internal/Magento/Framework/Archive/Zip.php index f33ad8700f05..c41f8b28ce34 100644 --- a/lib/internal/Magento/Framework/Archive/Zip.php +++ b/lib/internal/Magento/Framework/Archive/Zip.php @@ -4,13 +4,11 @@ * See COPYING.txt for license details. */ -/** - * Class to work with zip archives - * - * @author Magento Core Team - */ namespace Magento\Framework\Archive; +/** + * Zip compressed file archive. + */ class Zip extends AbstractArchive implements ArchiveInterface { /** @@ -54,11 +52,34 @@ public function pack($source, $destination) public function unpack($source, $destination) { $zip = new \ZipArchive(); - $zip->open($source); - $filename = $zip->getNameIndex(0); - $zip->extractTo(dirname($destination), $filename); - rename(dirname($destination).'/'.$filename, $destination); - $zip->close(); + if ($zip->open($source) === true) { + $filename = $this->filterRelativePaths($zip->getNameIndex(0) ?: ''); + if ($filename) { + $zip->extractTo(dirname($destination), $filename); + rename(dirname($destination).'/'.$filename, $destination); + } else { + $destination = ''; + } + $zip->close(); + } else { + $destination = ''; + } + return $destination; } + + /** + * Filter file names with relative paths. + * + * @param string $path + * @return string + */ + private function filterRelativePaths(string $path): string + { + if ($path && preg_match('#^\s*(../)|(/../)#i', $path)) { + $path = ''; + } + + return $path; + } } diff --git a/lib/internal/Magento/Framework/Backup/BackupInterface.php b/lib/internal/Magento/Framework/Backup/BackupInterface.php index 3d054bdbd1a9..16aada9689c1 100644 --- a/lib/internal/Magento/Framework/Backup/BackupInterface.php +++ b/lib/internal/Magento/Framework/Backup/BackupInterface.php @@ -13,6 +13,8 @@ /** * @api + * + * @deprecated Backups should be done using other means. */ interface BackupInterface { diff --git a/lib/internal/Magento/Framework/Backup/Db/BackupDbInterface.php b/lib/internal/Magento/Framework/Backup/Db/BackupDbInterface.php index 13e3c562bb52..a019ccac06b6 100644 --- a/lib/internal/Magento/Framework/Backup/Db/BackupDbInterface.php +++ b/lib/internal/Magento/Framework/Backup/Db/BackupDbInterface.php @@ -7,6 +7,8 @@ /** * @api + * + * @deprecated Backups should be done using other means. */ interface BackupDbInterface { diff --git a/lib/internal/Magento/Framework/Backup/Db/BackupInterface.php b/lib/internal/Magento/Framework/Backup/Db/BackupInterface.php index f7459f629cb4..ae5879290eb2 100644 --- a/lib/internal/Magento/Framework/Backup/Db/BackupInterface.php +++ b/lib/internal/Magento/Framework/Backup/Db/BackupInterface.php @@ -7,6 +7,8 @@ /** * @api + * + * @deprecated Backups should be done using other means. */ interface BackupInterface { diff --git a/lib/internal/Magento/Framework/Backup/Filesystem/Rollback/Fs.php b/lib/internal/Magento/Framework/Backup/Filesystem/Rollback/Fs.php index 14531deda45b..4820b83ceb7a 100644 --- a/lib/internal/Magento/Framework/Backup/Filesystem/Rollback/Fs.php +++ b/lib/internal/Magento/Framework/Backup/Filesystem/Rollback/Fs.php @@ -75,7 +75,7 @@ public function run() /** * we need these fake initializations because all magento's files in filesystem will be deleted and autoloader - * wont be able to load classes that we need for unpacking + * won't be able to load classes that we need for unpacking */ new Tar(); new Gz(); @@ -94,6 +94,8 @@ public function run() } /** + * Get file system helper instance + * * @return Helper * @deprecated 100.2.0 */ diff --git a/lib/internal/Magento/Framework/Cache/Backend/Database.php b/lib/internal/Magento/Framework/Cache/Backend/Database.php index 291078383014..231a8584cc8a 100644 --- a/lib/internal/Magento/Framework/Cache/Backend/Database.php +++ b/lib/internal/Magento/Framework/Cache/Backend/Database.php @@ -27,11 +27,11 @@ * ) ENGINE=InnoDB DEFAULT CHARSET=utf8; */ -/** - * Database cache backend - */ namespace Magento\Framework\Cache\Backend; +/** + * Database cache backend. + */ class Database extends \Zend_Cache_Backend implements \Zend_Cache_Backend_ExtendedInterface { /** @@ -139,7 +139,7 @@ protected function _getTagsTable() * * Note : return value is always "string" (unserialization is done by the core not by the backend) * - * @param string $id Cache id + * @param string $id Cache id * @param boolean $doNotTestCacheValidity If set to true, the cache validity won't be tested * @return string|false cached datas */ @@ -432,7 +432,7 @@ public function touch($id, $extraLifetime) return $this->_getConnection()->update( $this->_getDataTable(), ['expire_time' => new \Zend_Db_Expr('expire_time+' . $extraLifetime)], - ['id=?' => $id, 'expire_time = 0 OR expire_time>' => time()] + ['id=?' => $id, 'expire_time = 0 OR expire_time>?' => time()] ); } else { return true; diff --git a/lib/internal/Magento/Framework/Cache/Backend/Memcached.php b/lib/internal/Magento/Framework/Cache/Backend/Memcached.php index c5e7a7e9e7c0..0621c63acbd8 100644 --- a/lib/internal/Magento/Framework/Cache/Backend/Memcached.php +++ b/lib/internal/Magento/Framework/Cache/Backend/Memcached.php @@ -5,6 +5,9 @@ */ namespace Magento\Framework\Cache\Backend; +/** + * Memcached cache model + */ class Memcached extends \Zend_Cache_Backend_Memcached implements \Zend_Cache_Backend_ExtendedInterface { /** @@ -46,7 +49,7 @@ public function __construct(array $options = []) * Returns ID of a specific chunk on the basis of data's ID * * @param string $id Main data's ID - * @param int $index Particular chunk number to return ID for + * @param int $index Particular chunk number to return ID for * @return string */ protected function _getChunkId($id, $index) @@ -58,7 +61,7 @@ protected function _getChunkId($id, $index) * Remove saved chunks in case something gone wrong (e.g. some chunk from the chain can not be found) * * @param string $id ID of data's info cell - * @param int $chunks Number of chunks to remove (basically, the number after '{splitted}|') + * @param int $chunks Number of chunks to remove (basically, the number after '{splitted}|') * @return null */ protected function _cleanTheMess($id, $chunks) @@ -84,7 +87,7 @@ public function save($data, $id, $tags = [], $specificLifetime = false) if (is_string($data) && strlen($data) > $this->_options['slab_size']) { $dataChunks = str_split($data, $this->_options['slab_size']); - for ($i = 0, $cnt = count($dataChunks); $i < $cnt; $i++) { + for ($i = 0, $count = count($dataChunks); $i < $count; $i++) { $chunkId = $this->_getChunkId($id, $i); if (!parent::save($dataChunks[$i], $chunkId, $tags, $specificLifetime)) { @@ -103,7 +106,7 @@ public function save($data, $id, $tags = [], $specificLifetime = false) * Load data from memcached, glue from several chunks if it was splitted upon save. * * @param string $id @see \Zend_Cache_Backend_Memcached::load() - * @param bool $doNotTestCacheValidity @see \Zend_Cache_Backend_Memcached::load() + * @param bool $doNotTestCacheValidity @see \Zend_Cache_Backend_Memcached::load() * @return bool|false|string */ public function load($id, $doNotTestCacheValidity = false) diff --git a/lib/internal/Magento/Framework/Cache/InvalidateLogger.php b/lib/internal/Magento/Framework/Cache/InvalidateLogger.php index 10886f911e29..08f9930a81b2 100644 --- a/lib/internal/Magento/Framework/Cache/InvalidateLogger.php +++ b/lib/internal/Magento/Framework/Cache/InvalidateLogger.php @@ -10,6 +10,9 @@ use Magento\Framework\App\Request\Http as HttpRequest; use Psr\Log\LoggerInterface as Logger; +/** + * Invalidate logger cache. + */ class InvalidateLogger { /** @@ -34,6 +37,7 @@ public function __construct(HttpRequest $request, Logger $logger) /** * Logger invalidate cache + * * @param mixed $invalidateInfo * @return void */ @@ -44,6 +48,7 @@ public function execute($invalidateInfo) /** * Make extra data to logger message + * * @param mixed $invalidateInfo * @return array */ @@ -65,4 +70,16 @@ public function critical($message, $params) { $this->logger->critical($message, $this->makeParams($params)); } + + /** + * Log warning + * + * @param string $message + * @param mixed $params + * @return void + */ + public function warning($message, $params) + { + $this->logger->warning($message, $this->makeParams($params)); + } } diff --git a/lib/internal/Magento/Framework/Cache/LockGuardedCacheLoader.php b/lib/internal/Magento/Framework/Cache/LockGuardedCacheLoader.php new file mode 100644 index 000000000000..216d8e9a0a01 --- /dev/null +++ b/lib/internal/Magento/Framework/Cache/LockGuardedCacheLoader.php @@ -0,0 +1,116 @@ +locker = $locker; + $this->lockTimeout = $lockTimeout; + $this->delayTimeout = $delayTimeout; + } + + /** + * Load data. + * + * @param string $lockName + * @param callable $dataLoader + * @param callable $dataCollector + * @param callable $dataSaver + * @return mixed + */ + public function lockedLoadData( + string $lockName, + callable $dataLoader, + callable $dataCollector, + callable $dataSaver + ) { + $cachedData = $dataLoader(); //optimistic read + + while ($cachedData === false && $this->locker->isLocked($lockName)) { + usleep($this->delayTimeout * 1000); + $cachedData = $dataLoader(); + } + + while ($cachedData === false) { + try { + if ($this->locker->lock($lockName, $this->lockTimeout / 1000)) { + $data = $dataCollector(); + $dataSaver($data); + $cachedData = $data; + } + } finally { + $this->locker->unlock($lockName); + } + + if ($cachedData === false) { + usleep($this->delayTimeout * 1000); + $cachedData = $dataLoader(); + } + } + + return $cachedData; + } + + /** + * Clean data. + * + * @param string $lockName + * @param callable $dataCleaner + * @return void + */ + public function lockedCleanData(string $lockName, callable $dataCleaner) + { + while ($this->locker->isLocked($lockName)) { + usleep($this->delayTimeout * 1000); + } + try { + if ($this->locker->lock($lockName, $this->lockTimeout / 1000)) { + $dataCleaner(); + } + } finally { + $this->locker->unlock($lockName); + } + } +} diff --git a/lib/internal/Magento/Framework/Cache/Test/Unit/Backend/Decorator/DecoratorAbstractTest.php b/lib/internal/Magento/Framework/Cache/Test/Unit/Backend/Decorator/DecoratorAbstractTest.php index ba75b7eed52c..03ce339e90ba 100644 --- a/lib/internal/Magento/Framework/Cache/Test/Unit/Backend/Decorator/DecoratorAbstractTest.php +++ b/lib/internal/Magento/Framework/Cache/Test/Unit/Backend/Decorator/DecoratorAbstractTest.php @@ -54,7 +54,7 @@ public function testConstructor() } /** - * @param array options + * @param array $options * @expectedException \Zend_Cache_Exception * @dataProvider constructorExceptionDataProvider */ diff --git a/lib/internal/Magento/Framework/Code/Generator.php b/lib/internal/Magento/Framework/Code/Generator.php index 4dec7d1a2814..b46c8c681bb5 100644 --- a/lib/internal/Magento/Framework/Code/Generator.php +++ b/lib/internal/Magento/Framework/Code/Generator.php @@ -8,11 +8,15 @@ use Magento\Framework\Code\Generator\DefinedClasses; use Magento\Framework\Code\Generator\EntityAbstract; use Magento\Framework\Code\Generator\Io; +use Magento\Framework\ObjectManager\ConfigInterface; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Phrase; use Magento\Framework\Filesystem\Driver\File; use Psr\Log\LoggerInterface; +/** + * Class code generator. + */ class Generator { const GENERATION_SUCCESS = 'success'; @@ -232,7 +236,21 @@ protected function shouldSkipGeneration($resultEntityType, $sourceClassName, $re { if (!$resultEntityType || !$sourceClassName) { return self::GENERATION_ERROR; - } elseif ($this->definedClasses->isClassLoadableFromDisk($resultClass)) { + } + + /** @var ConfigInterface $omConfig */ + $omConfig = $this->objectManager->get(ConfigInterface::class); + $virtualTypes = $omConfig->getVirtualTypes(); + + /** + * Do not try to autogenerate virtual types + * For example virtual types with names overlapping autogenerated suffixes + */ + if (isset($virtualTypes[$resultClass])) { + return self::GENERATION_SKIP; + } + + if ($this->definedClasses->isClassLoadableFromDisk($resultClass)) { $generatedFileName = $this->_ioObject->generateResultFileName($resultClass); /** * Must handle two edge cases: a competing process has generated the class and written it to disc already, @@ -244,9 +262,12 @@ protected function shouldSkipGeneration($resultEntityType, $sourceClassName, $re $this->_ioObject->includeFile($generatedFileName); } return self::GENERATION_SKIP; - } elseif (!isset($this->_generatedEntities[$resultEntityType])) { + } + + if (!isset($this->_generatedEntities[$resultEntityType])) { throw new \InvalidArgumentException('Unknown generation entity.'); } + return false; } } diff --git a/lib/internal/Magento/Framework/Code/Generator/Autoloader.php b/lib/internal/Magento/Framework/Code/Generator/Autoloader.php index c21400839360..35c138147e9d 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 c27a896b65f0..993235054e49 100644 --- a/lib/internal/Magento/Framework/Code/NameBuilder.php +++ b/lib/internal/Magento/Framework/Code/NameBuilder.php @@ -1,12 +1,15 @@ definedClassesMock = $this->createMock(DefinedClasses::class); @@ -65,6 +88,12 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); $this->loggerMock = $this->getMockForAbstractClass(LoggerInterface::class); + $this->objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->objectManagerConfigMock = $this->getMockBuilder(ConfigInterface::class) + ->disableOriginalConstructor() + ->getMock(); $this->model = new Generator( $this->ioObjectMock, @@ -78,7 +107,7 @@ protected function setUp() ); } - public function testGetGeneratedEntities() + public function testGetGeneratedEntities(): void { $this->model = new Generator( $this->ioObjectMock, @@ -91,22 +120,58 @@ public function testGetGeneratedEntities() /** * @param string $className * @param string $entityType - * @expectedException \RuntimeException + * @expectedException RuntimeException * @dataProvider generateValidClassDataProvider */ - public function testGenerateClass($className, $entityType) + public function testGenerateClass($className, $entityType): void { - $objectManagerMock = $this->createMock(ObjectManagerInterface::class); $fullClassName = $className . $entityType; + $entityGeneratorMock = $this->getMockBuilder(EntityAbstract::class) ->disableOriginalConstructor() ->getMock(); - $objectManagerMock->expects($this->once())->method('create')->willReturn($entityGeneratorMock); - $this->model->setObjectManager($objectManagerMock); - $this->model->generateClass($fullClassName); + $this->objectManagerMock + ->expects($this->once()) + ->method('create') + ->willReturn($entityGeneratorMock); + + $this->objectManagerConfigMock + ->expects($this->once()) + ->method('getVirtualTypes') + ->willReturn([]); + $this->objectManagerMock + ->expects($this->once()) + ->method('get') + ->with(ConfigInterface::class) + ->willReturn($this->objectManagerConfigMock); + $this->model->setObjectManager($this->objectManagerMock); + + $this->assertSame( + Generator::GENERATION_SUCCESS, + $this->model->generateClass($fullClassName) + ); } - public function testGenerateClassWithWrongName() + public function testShouldNotGenerateVirtualType(): void + { + $this->objectManagerConfigMock + ->expects($this->once()) + ->method('getVirtualTypes') + ->willReturn([GeneratedClassFactory::class => GeneratedClassFactory::class]); + $this->objectManagerMock + ->expects($this->once()) + ->method('get') + ->with(ConfigInterface::class) + ->willReturn($this->objectManagerConfigMock); + $this->model->setObjectManager($this->objectManagerMock); + + $this->assertSame( + Generator::GENERATION_SKIP, + $this->model->generateClass(GeneratedClassFactory::class) + ); + } + + public function testGenerateClassWithWrongName(): void { $this->assertEquals( Generator::GENERATION_ERROR, @@ -115,25 +180,42 @@ public function testGenerateClassWithWrongName() } /** - * @expectedException \RuntimeException + * @expectedException RuntimeException */ - public function testGenerateClassWhenClassIsNotGenerationSuccess() + public function testGenerateClassWhenClassIsNotGenerationSuccess(): void { $expectedEntities = array_values($this->expectedEntities); $resultClassName = self::SOURCE_CLASS . ucfirst(array_shift($expectedEntities)); - $objectManagerMock = $this->createMock(ObjectManagerInterface::class); + $entityGeneratorMock = $this->getMockBuilder(EntityAbstract::class) ->disableOriginalConstructor() ->getMock(); - $objectManagerMock->expects($this->once())->method('create')->willReturn($entityGeneratorMock); - $this->model->setObjectManager($objectManagerMock); - $this->model->generateClass($resultClassName); + $this->objectManagerMock + ->expects($this->once()) + ->method('create') + ->willReturn($entityGeneratorMock); + + $this->objectManagerConfigMock + ->expects($this->once()) + ->method('getVirtualTypes') + ->willReturn([]); + $this->objectManagerMock + ->expects($this->once()) + ->method('get') + ->with(ConfigInterface::class) + ->willReturn($this->objectManagerConfigMock); + $this->model->setObjectManager($this->objectManagerMock); + + $this->assertSame( + Generator::GENERATION_SUCCESS, + $this->model->generateClass($resultClassName) + ); } /** * @inheritdoc */ - public function testGenerateClassWithErrors() + public function testGenerateClassWithErrors(): void { $expectedEntities = array_values($this->expectedEntities); $resultClassName = self::SOURCE_CLASS . ucfirst(array_shift($expectedEntities)); @@ -148,17 +230,15 @@ public function testGenerateClassWithErrors() . 'directory permission is set to write --- the requested class did not generate properly, then ' . 'you must add the generated class object to the signature of the related construct method, only.'; $FinalErrorMessage = implode(PHP_EOL, $errorMessages) . "\n" . $mainErrorMessage; - $this->expectException(\RuntimeException::class); + $this->expectException(RuntimeException::class); $this->expectExceptionMessage($FinalErrorMessage); - /** @var ObjectManagerInterface|Mock $objectManagerMock */ - $objectManagerMock = $this->createMock(ObjectManagerInterface::class); /** @var EntityAbstract|Mock $entityGeneratorMock */ $entityGeneratorMock = $this->getMockBuilder(EntityAbstract::class) ->disableOriginalConstructor() ->getMock(); - $objectManagerMock->expects($this->once()) + $this->objectManagerMock->expects($this->once()) ->method('create') ->willReturn($entityGeneratorMock); $entityGeneratorMock->expects($this->once()) @@ -177,26 +257,62 @@ public function testGenerateClassWithErrors() $this->loggerMock->expects($this->once()) ->method('critical') ->with($FinalErrorMessage); - $this->model->setObjectManager($objectManagerMock); - $this->model->generateClass($resultClassName); + + $this->objectManagerConfigMock + ->expects($this->once()) + ->method('getVirtualTypes') + ->willReturn([]); + $this->objectManagerMock + ->expects($this->once()) + ->method('get') + ->with(ConfigInterface::class) + ->willReturn($this->objectManagerConfigMock); + $this->model->setObjectManager($this->objectManagerMock); + + $this->assertSame( + Generator::GENERATION_SUCCESS, + $this->model->generateClass($resultClassName) + ); } /** * @dataProvider trueFalseDataProvider + * @param $fileExists */ - public function testGenerateClassWithExistName($fileExists) + public function testGenerateClassWithExistName($fileExists): void { $this->definedClassesMock->expects($this->any()) ->method('isClassLoadableFromDisk') ->willReturn(true); $resultClassFileName = '/Magento/Path/To/Class.php'; - $this->ioObjectMock->expects($this->once())->method('generateResultFileName')->willReturn($resultClassFileName); - $this->ioObjectMock->expects($this->once())->method('fileExists')->willReturn($fileExists); + + $this->objectManagerConfigMock + ->expects($this->once()) + ->method('getVirtualTypes') + ->willReturn([]); + $this->objectManagerMock + ->expects($this->once()) + ->method('get') + ->with(ConfigInterface::class) + ->willReturn($this->objectManagerConfigMock); + $this->model->setObjectManager($this->objectManagerMock); + + $this->ioObjectMock + ->expects($this->once()) + ->method('generateResultFileName') + ->willReturn($resultClassFileName); + $this->ioObjectMock + ->expects($this->once()) + ->method('fileExists') + ->willReturn($fileExists); + $includeFileInvokeCount = $fileExists ? 1 : 0; - $this->ioObjectMock->expects($this->exactly($includeFileInvokeCount))->method('includeFile'); + $this->ioObjectMock + ->expects($this->exactly($includeFileInvokeCount)) + ->method('includeFile'); - $this->assertEquals( + $this->assertSame( Generator::GENERATION_SKIP, $this->model->generateClass(GeneratedClassFactory::class) ); @@ -205,7 +321,7 @@ public function testGenerateClassWithExistName($fileExists) /** * @return array */ - public function trueFalseDataProvider() + public function trueFalseDataProvider(): array { return [[true], [false]]; } @@ -215,7 +331,7 @@ public function trueFalseDataProvider() * * @return array */ - public function generateValidClassDataProvider() + public function generateValidClassDataProvider(): array { $data = []; foreach ($this->expectedEntities as $generatedEntity) { diff --git a/lib/internal/Magento/Framework/Communication/Config/CompositeReader.php b/lib/internal/Magento/Framework/Communication/Config/CompositeReader.php index f3af69142691..2ca64bf14d33 100644 --- a/lib/internal/Magento/Framework/Communication/Config/CompositeReader.php +++ b/lib/internal/Magento/Framework/Communication/Config/CompositeReader.php @@ -27,12 +27,10 @@ public function __construct(array $readers) usort( $readers, function ($firstItem, $secondItem) { - if (!isset($firstItem['sortOrder']) || !isset($secondItem['sortOrder']) - || $firstItem['sortOrder'] == $secondItem['sortOrder'] - ) { + if (!isset($firstItem['sortOrder']) || !isset($secondItem['sortOrder'])) { return 0; } - return $firstItem['sortOrder'] < $secondItem['sortOrder'] ? -1 : 1; + return $firstItem['sortOrder'] <=> $secondItem['sortOrder']; } ); $this->readers = []; diff --git a/lib/internal/Magento/Framework/Communication/Config/Reader/XmlReader/Converter.php b/lib/internal/Magento/Framework/Communication/Config/Reader/XmlReader/Converter.php index 8fcbd74c884d..b79ba49a24dd 100644 --- a/lib/internal/Magento/Framework/Communication/Config/Reader/XmlReader/Converter.php +++ b/lib/internal/Magento/Framework/Communication/Config/Reader/XmlReader/Converter.php @@ -124,20 +124,22 @@ protected function extractTopics($config) $requestSchema, $responseSchema ); + $isSynchronous = $this->extractTopicIsSynchronous($topicNode); if ($serviceMethod) { $output[$topicName] = $this->reflectionGenerator->generateTopicConfigForServiceMethod( $topicName, $serviceMethod[ConfigParser::TYPE_NAME], $serviceMethod[ConfigParser::METHOD_NAME], - $handlers + $handlers, + $isSynchronous ); } elseif ($requestSchema && $responseSchema) { $output[$topicName] = [ Config::TOPIC_NAME => $topicName, - Config::TOPIC_IS_SYNCHRONOUS => true, + Config::TOPIC_IS_SYNCHRONOUS => $isSynchronous, Config::TOPIC_REQUEST => $requestSchema, Config::TOPIC_REQUEST_TYPE => Config::TOPIC_REQUEST_TYPE_CLASS, - Config::TOPIC_RESPONSE => $responseSchema, + Config::TOPIC_RESPONSE => ($isSynchronous) ? $responseSchema: null, Config::TOPIC_HANDLERS => $handlers ]; } elseif ($requestSchema) { @@ -258,4 +260,20 @@ protected function parseServiceMethod($serviceMethod, $topicName) ); return $parsedServiceMethod; } + + /** + * Extract is_synchronous topic value. + * + * @param \DOMNode $topicNode + * @return bool + */ + private function extractTopicIsSynchronous($topicNode): bool + { + $attributeName = Config::TOPIC_IS_SYNCHRONOUS; + $topicAttributes = $topicNode->attributes; + if (!$topicAttributes->getNamedItem($attributeName)) { + return true; + } + return $this->booleanUtils->toBoolean($topicAttributes->getNamedItem($attributeName)->nodeValue); + } } diff --git a/lib/internal/Magento/Framework/Communication/Config/ReflectionGenerator.php b/lib/internal/Magento/Framework/Communication/Config/ReflectionGenerator.php index d1bc62464f21..7ef84f1c43b1 100644 --- a/lib/internal/Magento/Framework/Communication/Config/ReflectionGenerator.php +++ b/lib/internal/Magento/Framework/Communication/Config/ReflectionGenerator.php @@ -42,7 +42,10 @@ public function extractMethodMetadata($className, $methodName) $result = [ Config::SCHEMA_METHOD_PARAMS => [], Config::SCHEMA_METHOD_RETURN_TYPE => $this->methodsMap->getMethodReturnType($className, $methodName), - Config::SCHEMA_METHOD_HANDLER => [Config::HANDLER_TYPE => $className, Config::HANDLER_METHOD => $methodName] + Config::SCHEMA_METHOD_HANDLER => [ + Config::HANDLER_TYPE => $className, + Config::HANDLER_METHOD => $methodName + ] ]; $paramsMeta = $this->methodsMap->getMethodParams($className, $methodName); foreach ($paramsMeta as $paramPosition => $paramMeta) { @@ -63,16 +66,27 @@ public function extractMethodMetadata($className, $methodName) * @param string $serviceType * @param string $serviceMethod * @param array|null $handlers + * @param bool|null $isSynchronous * @return array */ - public function generateTopicConfigForServiceMethod($topicName, $serviceType, $serviceMethod, $handlers = []) - { + public function generateTopicConfigForServiceMethod( + $topicName, + $serviceType, + $serviceMethod, + $handlers = [], + $isSynchronous = null + ) { $methodMetadata = $this->extractMethodMetadata($serviceType, $serviceMethod); $returnType = $methodMetadata[Config::SCHEMA_METHOD_RETURN_TYPE]; $returnType = ($returnType != 'void' && $returnType != 'null') ? $returnType : null; + if (!isset($isSynchronous)) { + $isSynchronous = $returnType ? true : false; + } else { + $returnType = ($isSynchronous) ? $returnType : null; + } return [ Config::TOPIC_NAME => $topicName, - Config::TOPIC_IS_SYNCHRONOUS => $returnType ? true : false, + Config::TOPIC_IS_SYNCHRONOUS => $isSynchronous, Config::TOPIC_REQUEST => $methodMetadata[Config::SCHEMA_METHOD_PARAMS], Config::TOPIC_REQUEST_TYPE => Config::TOPIC_REQUEST_TYPE_METHOD, Config::TOPIC_RESPONSE => $returnType, @@ -85,7 +99,8 @@ public function generateTopicConfigForServiceMethod($topicName, $serviceType, $s * Generate topic name based on service type and method name. * * Perform the following conversion: - * \Magento\Customer\Api\RepositoryInterface + getById => magento.customer.api.repositoryInterface.getById + * \Magento\Customer\Api\RepositoryInterface + getById => + * magento.customer.api.repositoryInterface.getById * * @param string $typeName * @param string $methodName diff --git a/lib/internal/Magento/Framework/Communication/Config/Validator.php b/lib/internal/Magento/Framework/Communication/Config/Validator.php index 5e2687c3b08c..76ef1b85b63e 100644 --- a/lib/internal/Magento/Framework/Communication/Config/Validator.php +++ b/lib/internal/Magento/Framework/Communication/Config/Validator.php @@ -38,6 +38,8 @@ public function __construct( } /** + * Validate response schema definition for topic + * * @param string $responseSchema * @param string $topicName * @return void @@ -46,6 +48,12 @@ public function validateResponseSchemaType($responseSchema, $topicName) { try { $this->validateType($responseSchema); + } catch (\InvalidArgumentException $e) { + throw new \LogicException( + 'Response schema definition has service class with wrong annotated methods', + $e->getCode(), + $e + ); } catch (\Exception $e) { throw new \LogicException( sprintf( @@ -59,6 +67,8 @@ public function validateResponseSchemaType($responseSchema, $topicName) } /** + * Validate request schema definition for topic + * * @param string $requestSchema * @param string $topicName * @return void @@ -67,6 +77,12 @@ public function validateRequestSchemaType($requestSchema, $topicName) { try { $this->validateType($requestSchema); + } catch (\InvalidArgumentException $e) { + throw new \LogicException( + 'Request schema definition has service class with wrong annotated methods', + $e->getCode(), + $e + ); } catch (\Exception $e) { throw new \LogicException( sprintf( @@ -80,6 +96,8 @@ public function validateRequestSchemaType($requestSchema, $topicName) } /** + * Validate service method specified in the definition of handler + * * @param string $serviceName * @param string $methodName * @param string $handlerName @@ -109,6 +127,7 @@ public function validateResponseHandlersType($serviceName, $methodName, $handler * @param string $typeName * @return $this * @throws \Exception In case when type is invalid + * @throws \InvalidArgumentException if methods don't have annotation */ protected function validateType($typeName) { diff --git a/lib/internal/Magento/Framework/Communication/etc/communication.xsd b/lib/internal/Magento/Framework/Communication/etc/communication.xsd index 12ee56371ce7..678d89f30c53 100644 --- a/lib/internal/Magento/Framework/Communication/etc/communication.xsd +++ b/lib/internal/Magento/Framework/Communication/etc/communication.xsd @@ -40,6 +40,7 @@ + diff --git a/lib/internal/Magento/Framework/Composer/ComposerJsonFinder.php b/lib/internal/Magento/Framework/Composer/ComposerJsonFinder.php index 259fcd323b38..2a641a41377a 100644 --- a/lib/internal/Magento/Framework/Composer/ComposerJsonFinder.php +++ b/lib/internal/Magento/Framework/Composer/ComposerJsonFinder.php @@ -13,7 +13,7 @@ class ComposerJsonFinder { /** - * @var DirectoryList $directoryList + * @var DirectoryList */ private $directoryList; diff --git a/lib/internal/Magento/Framework/Composer/Remove.php b/lib/internal/Magento/Framework/Composer/Remove.php index b7a9a20333d8..b7cea7769a17 100644 --- a/lib/internal/Magento/Framework/Composer/Remove.php +++ b/lib/internal/Magento/Framework/Composer/Remove.php @@ -46,7 +46,7 @@ public function remove(array $packages) [ 'command' => 'remove', 'packages' => $packages, - '--no-update' => true, + '--no-update-with-dependencies' => true, ] ); } diff --git a/lib/internal/Magento/Framework/Config/ConfigOptionsListConstants.php b/lib/internal/Magento/Framework/Config/ConfigOptionsListConstants.php index 92f0302d93ba..6bdb74ef7b89 100644 --- a/lib/internal/Magento/Framework/Config/ConfigOptionsListConstants.php +++ b/lib/internal/Magento/Framework/Config/ConfigOptionsListConstants.php @@ -47,7 +47,7 @@ class ConfigOptionsListConstants const CONFIG_PATH_SCD_ON_DEMAND_IN_PRODUCTION = 'static_content_on_demand_in_production'; /** - * Paramater for forcing HTML minification even if file is already minified. + * Parameter for forcing HTML minification even if file is already minified. */ const CONFIG_PATH_FORCE_HTML_MINIFICATION = 'force_html_minification'; diff --git a/lib/internal/Magento/Framework/Config/Dom.php b/lib/internal/Magento/Framework/Config/Dom.php index 1d995bab007e..5c97c996634d 100644 --- a/lib/internal/Magento/Framework/Config/Dom.php +++ b/lib/internal/Magento/Framework/Config/Dom.php @@ -190,9 +190,20 @@ protected function _mergeNode(\DOMElement $node, $parentPath) /* override node value */ if ($this->_isTextNode($node)) { /* skip the case when the matched node has children, otherwise they get overridden */ - if (!$matchedNode->hasChildNodes() || $this->_isTextNode($matchedNode)) { + if (!$matchedNode->hasChildNodes() + || $this->_isTextNode($matchedNode) + || $this->isCdataNode($matchedNode) + ) { $matchedNode->nodeValue = $node->childNodes->item(0)->nodeValue; } + } elseif ($this->isCdataNode($node) && $this->_isTextNode($matchedNode)) { + /* Replace text node with CDATA section */ + if ($this->findCdataSection($node)) { + $matchedNode->nodeValue = $this->findCdataSection($node)->nodeValue; + } + } elseif ($this->isCdataNode($node) && $this->isCdataNode($matchedNode)) { + /* Replace CDATA with new one */ + $this->replaceCdataNode($matchedNode, $node); } else { /* recursive merge for all child nodes */ foreach ($node->childNodes as $childNode) { @@ -220,6 +231,56 @@ protected function _isTextNode($node) return $node->childNodes->length == 1 && $node->childNodes->item(0) instanceof \DOMText; } + /** + * Check if the node content is CDATA (probably surrounded with text nodes) or just text node + * + * @param \DOMNode $node + * @return bool + */ + private function isCdataNode($node) + { + // If every child node of current is NOT \DOMElement + // It is arbitrary combination of text nodes and CDATA sections. + foreach ($node->childNodes as $childNode) { + if ($childNode instanceof \DOMElement) { + return false; + } + } + + return true; + } + + /** + * Finds CDATA section from given node children + * + * @param \DOMNode $node + * @return \DOMCdataSection|null + */ + private function findCdataSection($node) + { + foreach ($node->childNodes as $childNode) { + if ($childNode instanceof \DOMCdataSection) { + return $childNode; + } + } + } + + /** + * Replaces CDATA section in $oldNode with $newNode's + * + * @param \DOMNode $oldNode + * @param \DOMNode $newNode + */ + private function replaceCdataNode($oldNode, $newNode) + { + $oldCdata = $this->findCdataSection($oldNode); + $newCdata = $this->findCdataSection($newNode); + + if ($oldCdata && $newCdata) { + $oldCdata->nodeValue = $newCdata->nodeValue; + } + } + /** * Merges attributes of the merge node to the base node * @@ -318,7 +379,7 @@ public static function validateDomDocument( libxml_set_external_entity_loader([self::$urnResolver, 'registerEntityLoader']); $errors = []; try { - $result = $dom->schemaValidate($schema); + $result = $dom->schemaValidate($schema, LIBXML_SCHEMA_CREATE); if (!$result) { $errors = self::getXmlErrors($errorFormat); } diff --git a/lib/internal/Magento/Framework/Config/Reader.php b/lib/internal/Magento/Framework/Config/Reader.php index c7ec5f327436..9ace14ad9d8d 100644 --- a/lib/internal/Magento/Framework/Config/Reader.php +++ b/lib/internal/Magento/Framework/Config/Reader.php @@ -63,10 +63,7 @@ function ($item) { uasort( $array, function ($firstItem, $nexItem) { - if ((int)$firstItem['sortOrder'] == (int)$nexItem['sortOrder']) { - return 0; - } - return (int)$firstItem['sortOrder'] < (int)$nexItem['sortOrder'] ? -1 : 1; + return (int)$firstItem['sortOrder'] <=> (int)$nexItem['sortOrder']; } ); diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/DomTest.php b/lib/internal/Magento/Framework/Config/Test/Unit/DomTest.php index 5c8f66683877..73968ac1ed89 100644 --- a/lib/internal/Magento/Framework/Config/Test/Unit/DomTest.php +++ b/lib/internal/Magento/Framework/Config/Test/Unit/DomTest.php @@ -5,6 +5,9 @@ */ namespace Magento\Framework\Config\Test\Unit; +/** + * Test for \Magento\Framework\Config\Dom class. + */ class DomTest extends \PHPUnit\Framework\TestCase { /** @@ -62,6 +65,37 @@ public function mergeDataProvider() ['override_node.xml', 'override_node_new.xml', [], null, 'override_node_merged.xml'], ['override_node_new.xml', 'override_node.xml', [], null, 'override_node_merged.xml'], ['text_node.xml', 'text_node_new.xml', [], null, 'text_node_merged.xml'], + 'text node replaced with cdata' => [ + 'text_node_cdata.xml', + 'text_node_cdata_new.xml', + [], + null, + 'text_node_cdata_merged.xml' + ], + 'cdata' => ['cdata.xml', 'cdata_new.xml', [], null, 'cdata_merged.xml'], + 'cdata with html' => ['cdata_html.xml', 'cdata_html_new.xml', [], null, 'cdata_html_merged.xml'], + 'cdata replaced with text node' => [ + 'cdata_text.xml', + 'cdata_text_new.xml', + [], + null, + 'cdata_text_merged.xml' + ], + 'big cdata' => ['big_cdata.xml', 'big_cdata_new.xml', [], null, 'big_cdata_merged.xml'], + 'big cdata with attribute' => [ + 'big_cdata_attribute.xml', + 'big_cdata_attribute_new.xml', + [], + null, + 'big_cdata_attribute_merged.xml' + ], + 'big cdata replaced with text' => [ + 'big_cdata_text.xml', + 'big_cdata_text_new.xml', + [], + null, + 'big_cdata_text_merged.xml' + ], [ 'recursive.xml', 'recursive_new.xml', @@ -135,6 +169,48 @@ public function validateDataProvider() ]; } + /** + * @param string $xml + * @param string $expectedValue + * @dataProvider validateWithDefaultValueDataProvider + */ + public function testValidateWithDefaultValue($xml, $expectedValue) + { + if (!function_exists('libxml_set_external_entity_loader')) { + $this->markTestSkipped('Skipped on HHVM. Will be fixed in MAGETWO-45033'); + } + + $actualErrors = []; + + $dom = new \Magento\Framework\Config\Dom($xml, $this->validationStateMock); + $dom->validate(__DIR__ . '/_files/sample.xsd', $actualErrors); + + $actualValue = $dom->getDom() + ->getElementsByTagName('root')->item(0) + ->getElementsByTagName('node')->item(0) + ->getAttribute('attribute_with_default_value'); + + $this->assertEmpty($actualErrors); + $this->assertEquals($expectedValue, $actualValue); + } + + /** + * @return array + */ + public function validateWithDefaultValueDataProvider() + { + return [ + 'default_value' => [ + '', + 'default_value' + ], + 'custom_value' => [ + '', + 'non_default_value' + ], + ]; + } + public function testValidateCustomErrorFormat() { $xml = ''; diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/ThemeTest.php b/lib/internal/Magento/Framework/Config/Test/Unit/ThemeTest.php index 17fa89068362..4966a45c7b47 100644 --- a/lib/internal/Magento/Framework/Config/Test/Unit/ThemeTest.php +++ b/lib/internal/Magento/Framework/Config/Test/Unit/ThemeTest.php @@ -7,10 +7,10 @@ class ThemeTest extends \PHPUnit\Framework\TestCase { - /** @var \Magento\Framework\Config\Dom\UrnResolver $urnResolverMock */ + /** @var \Magento\Framework\Config\Dom\UrnResolver */ protected $urnResolver; - /** @var \Magento\Framework\Config\Dom\UrnResolver $urnResolverMock */ + /** @var \Magento\Framework\Config\Dom\UrnResolver */ protected $urnResolverMock; protected function setUp() diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata.xml new file mode 100644 index 000000000000..69eb0035958e --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_attribute.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_attribute.xml new file mode 100644 index 000000000000..12a9389e3d23 --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_attribute.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_attribute_merged.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_attribute_merged.xml new file mode 100644 index 000000000000..6e95d843e34b --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_attribute_merged.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_attribute_new.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_attribute_new.xml new file mode 100644 index 000000000000..b905781a9fe5 --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_attribute_new.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_merged.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_merged.xml new file mode 100644 index 000000000000..b905781a9fe5 --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_merged.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_new.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_new.xml new file mode 100644 index 000000000000..b905781a9fe5 --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_new.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_text.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_text.xml new file mode 100644 index 000000000000..69eb0035958e --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_text.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_text_merged.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_text_merged.xml new file mode 100644 index 000000000000..3e37e67ffcf3 --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_text_merged.xml @@ -0,0 +1,12 @@ + + + + + Some Other Phrase + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_text_new.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_text_new.xml new file mode 100644 index 000000000000..3e37e67ffcf3 --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/big_cdata_text_new.xml @@ -0,0 +1,12 @@ + + + + + Some Other Phrase + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata.xml new file mode 100644 index 000000000000..f65a21e12239 --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_html.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_html.xml new file mode 100644 index 000000000000..15294f46445e --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_html.xml @@ -0,0 +1,12 @@ + + + + + Phrase]]> + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_html_merged.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_html_merged.xml new file mode 100644 index 000000000000..709d921f737e --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_html_merged.xml @@ -0,0 +1,12 @@ + + + + + Other
Phrase]]>
+
+
diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_html_new.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_html_new.xml new file mode 100644 index 000000000000..709d921f737e --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_html_new.xml @@ -0,0 +1,12 @@ + + + + + Other
Phrase]]>
+
+
diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_merged.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_merged.xml new file mode 100644 index 000000000000..e6d2d809d7f7 --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_merged.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_new.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_new.xml new file mode 100644 index 000000000000..e6d2d809d7f7 --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_new.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_text.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_text.xml new file mode 100644 index 000000000000..f65a21e12239 --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_text.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_text_merged.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_text_merged.xml new file mode 100644 index 000000000000..3e37e67ffcf3 --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_text_merged.xml @@ -0,0 +1,12 @@ + + + + + Some Other Phrase + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_text_new.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_text_new.xml new file mode 100644 index 000000000000..3e37e67ffcf3 --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/cdata_text_new.xml @@ -0,0 +1,12 @@ + + + + + Some Other Phrase + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/text_node_cdata.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/text_node_cdata.xml new file mode 100644 index 000000000000..6807872aa3d3 --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/text_node_cdata.xml @@ -0,0 +1,12 @@ + + + + + Some Phrase + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/text_node_cdata_merged.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/text_node_cdata_merged.xml new file mode 100644 index 000000000000..b905781a9fe5 --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/text_node_cdata_merged.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/text_node_cdata_new.xml b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/text_node_cdata_new.xml new file mode 100644 index 000000000000..b905781a9fe5 --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/dom/text_node_cdata_new.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/_files/sample.xsd b/lib/internal/Magento/Framework/Config/Test/Unit/_files/sample.xsd index 1f635b7081e0..701a2eb18c2a 100644 --- a/lib/internal/Magento/Framework/Config/Test/Unit/_files/sample.xsd +++ b/lib/internal/Magento/Framework/Config/Test/Unit/_files/sample.xsd @@ -21,6 +21,7 @@ +
diff --git a/lib/internal/Magento/Framework/Console/Cli.php b/lib/internal/Magento/Framework/Console/Cli.php index dabb080cf7ed..fac588f1fbc1 100644 --- a/lib/internal/Magento/Framework/Console/Cli.php +++ b/lib/internal/Magento/Framework/Console/Cli.php @@ -3,13 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Framework\Console; use Magento\Framework\App\Bootstrap; use Magento\Framework\App\DeploymentConfig; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ProductMetadata; -use Magento\Framework\App\State; use Magento\Framework\Composer\ComposerJsonFinder; use Magento\Framework\Console\Exception\GenerationDirectoryAccessException; use Magento\Framework\Filesystem\Driver\File; @@ -23,9 +24,10 @@ /** * Magento 2 CLI Application. + * * This is the hood for all command line tools supported by Magento. * - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Cli extends Console\Application @@ -62,11 +64,11 @@ class Cli extends Console\Application /** * @param string $name the application name * @param string $version the application version - * @SuppressWarnings(PHPMD.ExitExpression) */ public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN') { try { + // phpcs:ignore Magento2.Security.IncludeFile $configuration = require BP . '/setup/config/application.config.php'; $bootstrapApplication = new Application(); $application = $bootstrapApplication->bootstrap($configuration); @@ -74,13 +76,12 @@ public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN') $this->assertCompilerPreparation(); $this->initObjectManager(); - $this->assertGenerationPermissions(); } catch (\Exception $exception) { $output = new \Symfony\Component\Console\Output\ConsoleOutput(); $output->writeln( '' . $exception->getMessage() . '' ); - + // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage exit(static::RETURN_FAILURE); } @@ -95,7 +96,7 @@ public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN') } /** - * {@inheritdoc} + * @inheritdoc * * @throws \Exception The exception in case of unexpected error */ @@ -111,7 +112,7 @@ public function doRun(Console\Input\InputInterface $input, Console\Output\Output } /** - * {@inheritdoc} + * @inheritdoc */ protected function getDefaultCommands() { @@ -173,33 +174,6 @@ private function initObjectManager() $omProvider->setObjectManager($this->objectManager); } - /** - * Checks whether generation directory is read-only. - * Depends on the current mode: - * production - application will proceed - * default - application will be terminated - * developer - application will be terminated - * - * @return void - * @throws GenerationDirectoryAccessException If generation directory is read-only in developer mode - */ - private function assertGenerationPermissions() - { - /** @var GenerationDirectoryAccess $generationDirectoryAccess */ - $generationDirectoryAccess = $this->objectManager->create( - GenerationDirectoryAccess::class, - ['serviceManager' => $this->serviceManager] - ); - /** @var State $state */ - $state = $this->objectManager->get(State::class); - - if ($state->getMode() !== State::MODE_PRODUCTION - && !$generationDirectoryAccess->check() - ) { - throw new GenerationDirectoryAccessException(); - } - } - /** * Checks whether compiler is being prepared. * @@ -246,8 +220,7 @@ protected function getVendorCommands($objectManager) } /** - * Provides updated configuration in - * accordance to document root settings. + * Provides updated configuration in accordance to document root settings. * * @param array $config * @return array diff --git a/lib/internal/Magento/Framework/Crontab/CrontabManager.php b/lib/internal/Magento/Framework/Crontab/CrontabManager.php index 6049b7fba6d4..da81540faf47 100644 --- a/lib/internal/Magento/Framework/Crontab/CrontabManager.php +++ b/lib/internal/Magento/Framework/Crontab/CrontabManager.php @@ -4,6 +4,7 @@ * See COPYING.txt for license details. */ + namespace Magento\Framework\Crontab; use Magento\Framework\App\Filesystem\DirectoryList; @@ -40,31 +41,35 @@ public function __construct( } /** + * Build tasks block start text. + * * @return string */ private function getTasksBlockStart() { $tasksBlockStart = self::TASKS_BLOCK_START; if (defined('BP')) { - $tasksBlockStart .= ' ' . md5(BP); + $tasksBlockStart .= ' ' . hash("sha256", BP); } return $tasksBlockStart; } /** + * Build tasks block end text. + * * @return string */ private function getTasksBlockEnd() { $tasksBlockEnd = self::TASKS_BLOCK_END; if (defined('BP')) { - $tasksBlockEnd .= ' ' . md5(BP); + $tasksBlockEnd .= ' ' . hash("sha256", BP); } return $tasksBlockEnd; } /** - * {@inheritdoc} + * @inheritdoc */ public function getTasks() { @@ -82,7 +87,7 @@ public function getTasks() } /** - * {@inheritdoc} + * @inheritdoc */ public function saveTasks(array $tasks) { @@ -118,8 +123,7 @@ public function saveTasks(array $tasks) } /** - * {@inheritdoc} - * @throws LocalizedException + * @inheritdoc */ public function removeTasks() { @@ -182,7 +186,7 @@ private function cleanMagentoSection($content) private function getCrontabContent() { try { - $content = (string)$this->shell->execute('crontab -l'); + $content = (string)$this->shell->execute('crontab -l 2>/dev/null'); } catch (LocalizedException $e) { return ''; } @@ -203,6 +207,7 @@ private function save($content) try { $this->shell->execute('echo "' . $content . '" | crontab -'); + // phpcs:disable Magento2.Exceptions.ThrowCatch } catch (LocalizedException $e) { throw new LocalizedException( new Phrase('Error during saving of crontab: %1', [$e->getPrevious()->getMessage()]), diff --git a/lib/internal/Magento/Framework/Crontab/Test/Unit/CrontabManagerTest.php b/lib/internal/Magento/Framework/Crontab/Test/Unit/CrontabManagerTest.php index b160eb0a7f95..f6c863d9d9fa 100644 --- a/lib/internal/Magento/Framework/Crontab/Test/Unit/CrontabManagerTest.php +++ b/lib/internal/Magento/Framework/Crontab/Test/Unit/CrontabManagerTest.php @@ -4,6 +4,7 @@ * See COPYING.txt for license details. */ + namespace Magento\Framework\Crontab\Test\Unit; use Magento\Framework\Crontab\CrontabManager; @@ -16,6 +17,9 @@ use Magento\Framework\Filesystem\Directory\ReadInterface; use Magento\Framework\Filesystem\DriverPool; +/** + * Tests crontab manager functionality. + */ class CrontabManagerTest extends \PHPUnit\Framework\TestCase { /** @@ -58,7 +62,7 @@ public function testGetTasksNoCrontab() $this->shellMock->expects($this->once()) ->method('execute') - ->with('crontab -l', []) + ->with('crontab -l 2>/dev/null', []) ->willThrowException($localizedException); $this->assertEquals([], $this->crontabManager->getTasks()); @@ -74,7 +78,7 @@ public function testGetTasks($content, $tasks) { $this->shellMock->expects($this->once()) ->method('execute') - ->with('crontab -l', []) + ->with('crontab -l 2>/dev/null', []) ->willReturn($content); $this->assertEquals($tasks, $this->crontabManager->getTasks()); @@ -88,17 +92,17 @@ public function getTasksDataProvider() return [ [ 'content' => '* * * * * /bin/php /var/www/cron.php' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . md5(BP) . PHP_EOL + . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . hash("sha256", BP) . PHP_EOL . '* * * * * /bin/php /var/www/magento/bin/magento cron:run' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . md5(BP) . PHP_EOL, + . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . hash("sha256", BP) . PHP_EOL, 'tasks' => ['* * * * * /bin/php /var/www/magento/bin/magento cron:run'], ], [ 'content' => '* * * * * /bin/php /var/www/cron.php' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . md5(BP) . PHP_EOL + . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . hash("sha256", BP) . PHP_EOL . '* * * * * /bin/php /var/www/magento/bin/magento cron:run' . PHP_EOL . '* * * * * /bin/php /var/www/magento/bin/magento setup:cron:run' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . md5(BP) . PHP_EOL, + . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . hash("sha256", BP) . PHP_EOL, 'tasks' => [ '* * * * * /bin/php /var/www/magento/bin/magento cron:run', '* * * * * /bin/php /var/www/magento/bin/magento setup:cron:run', @@ -127,7 +131,7 @@ public function testRemoveTasksWithException() $this->shellMock->expects($this->at(0)) ->method('execute') - ->with('crontab -l', []) + ->with('crontab -l 2>/dev/null', []) ->willReturn(''); $this->shellMock->expects($this->at(1)) @@ -148,7 +152,7 @@ public function testRemoveTasks($contentBefore, $contentAfter) { $this->shellMock->expects($this->at(0)) ->method('execute') - ->with('crontab -l', []) + ->with('crontab -l 2>/dev/null', []) ->willReturn($contentBefore); $this->shellMock->expects($this->at(1)) @@ -166,17 +170,17 @@ public function removeTasksDataProvider() return [ [ 'contentBefore' => '* * * * * /bin/php /var/www/cron.php' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . md5(BP) . PHP_EOL + . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . hash("sha256", BP) . PHP_EOL . '* * * * * /bin/php /var/www/magento/bin/magento cron:run' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . md5(BP) . PHP_EOL, + . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . hash("sha256", BP) . PHP_EOL, 'contentAfter' => '* * * * * /bin/php /var/www/cron.php' . PHP_EOL ], [ 'contentBefore' => '* * * * * /bin/php /var/www/cron.php' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . md5(BP) . PHP_EOL + . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . hash("sha256", BP) . PHP_EOL . '* * * * * /bin/php /var/www/magento/bin/magento cron:run' . PHP_EOL . '* * * * * /bin/php /var/www/magento/bin/magento setup:cron:run' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . md5(BP) . PHP_EOL, + . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . hash("sha256", BP) . PHP_EOL, 'contentAfter' => '* * * * * /bin/php /var/www/cron.php' . PHP_EOL ], [ @@ -276,7 +280,7 @@ public function testSaveTasks($tasks, $content, $contentToSave) $this->shellMock->expects($this->at(0)) ->method('execute') - ->with('crontab -l', []) + ->with('crontab -l 2>/dev/null', []) ->willReturn($content); $this->shellMock->expects($this->at(1)) @@ -292,9 +296,9 @@ public function testSaveTasks($tasks, $content, $contentToSave) public function saveTasksDataProvider() { $content = '* * * * * /bin/php /var/www/cron.php' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . md5(BP) . PHP_EOL + . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . hash("sha256", BP) . PHP_EOL . '* * * * * /bin/php /var/www/magento/bin/magento cron:run' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . md5(BP) . PHP_EOL; + . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . hash("sha256", BP) . PHP_EOL; return [ [ @@ -303,9 +307,9 @@ public function saveTasksDataProvider() ], 'content' => $content, 'contentToSave' => '* * * * * /bin/php /var/www/cron.php' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . md5(BP) . PHP_EOL + . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . hash("sha256", BP) . PHP_EOL . '* * * * * ' . PHP_BINARY . ' run.php' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . md5(BP) . PHP_EOL, + . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . hash("sha256", BP) . PHP_EOL, ], [ 'tasks' => [ @@ -313,9 +317,9 @@ public function saveTasksDataProvider() ], 'content' => $content, 'contentToSave' => '* * * * * /bin/php /var/www/cron.php' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . md5(BP) . PHP_EOL + . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . hash("sha256", BP) . PHP_EOL . '1 2 3 4 5 ' . PHP_BINARY . ' run.php' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . md5(BP) . PHP_EOL, + . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . hash("sha256", BP) . PHP_EOL, ], [ 'tasks' => [ @@ -323,10 +327,10 @@ public function saveTasksDataProvider() ], 'content' => $content, 'contentToSave' => '* * * * * /bin/php /var/www/cron.php' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . md5(BP) . PHP_EOL + . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . hash("sha256", BP) . PHP_EOL . '* * * * * ' . PHP_BINARY . ' /var/www/magento2/run.php >>' . ' /var/www/magento2/var/log/cron.log' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . md5(BP) . PHP_EOL, + . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . hash("sha256", BP) . PHP_EOL, ], [ 'tasks' => [ @@ -334,10 +338,10 @@ public function saveTasksDataProvider() ], 'content' => $content, 'contentToSave' => '* * * * * /bin/php /var/www/cron.php' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . md5(BP) . PHP_EOL + . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . hash("sha256", BP) . PHP_EOL . '* * * * * ' . PHP_BINARY . ' /var/www/magento2/run.php' . ' %% cron:run | grep -v \"Ran \'jobs\' by schedule\"' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . md5(BP) . PHP_EOL, + . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . hash("sha256", BP) . PHP_EOL, ], [ 'tasks' => [ @@ -345,10 +349,10 @@ public function saveTasksDataProvider() ], 'content' => '* * * * * /bin/php /var/www/cron.php', 'contentToSave' => '* * * * * /bin/php /var/www/cron.php' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . md5(BP) . PHP_EOL + . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . hash("sha256", BP) . PHP_EOL . '* * * * * ' . PHP_BINARY . ' /var/www/magento2/run.php' . ' %% cron:run | grep -v \"Ran \'jobs\' by schedule\"' . PHP_EOL - . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . md5(BP) . PHP_EOL, + . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . hash("sha256", BP) . PHP_EOL, ], ]; } diff --git a/lib/internal/Magento/Framework/Css/Test/Unit/PreProcessor/File/Collector/AggregatedTest.php b/lib/internal/Magento/Framework/Css/Test/Unit/PreProcessor/File/Collector/AggregatedTest.php index c1c2a4f4a1d1..cdf7a1ec3734 100644 --- a/lib/internal/Magento/Framework/Css/Test/Unit/PreProcessor/File/Collector/AggregatedTest.php +++ b/lib/internal/Magento/Framework/Css/Test/Unit/PreProcessor/File/Collector/AggregatedTest.php @@ -98,9 +98,9 @@ public function testGetFilesEmpty() * * @dataProvider getFilesDataProvider * - * @param $libraryFiles array Files in lib directory - * @param $baseFiles array Files in base directory - * @param $themeFiles array Files in theme + * @param array $libraryFiles Files in lib directory + * @param array $baseFiles Files in base directory + * @param array $themeFiles Files in theme * * * @return void */ diff --git a/lib/internal/Magento/Framework/Css/Test/Unit/PreProcessor/File/Collector/LibraryTest.php b/lib/internal/Magento/Framework/Css/Test/Unit/PreProcessor/File/Collector/LibraryTest.php index 962734feea18..6080652a4ad3 100644 --- a/lib/internal/Magento/Framework/Css/Test/Unit/PreProcessor/File/Collector/LibraryTest.php +++ b/lib/internal/Magento/Framework/Css/Test/Unit/PreProcessor/File/Collector/LibraryTest.php @@ -129,8 +129,8 @@ public function testGetFilesEmpty() * * @dataProvider getFilesDataProvider * - * @param $libraryFiles array Files in lib directory - * @param $themeFiles array Files in theme + * @param array $libraryFiles Files in lib directory + * @param array $themeFiles Files in theme * * * @return void */ diff --git a/lib/internal/Magento/Framework/DB/Adapter/AdapterInterface.php b/lib/internal/Magento/Framework/DB/Adapter/AdapterInterface.php index 5c9bc9c2fb2d..f654fd263f60 100644 --- a/lib/internal/Magento/Framework/DB/Adapter/AdapterInterface.php +++ b/lib/internal/Magento/Framework/DB/Adapter/AdapterInterface.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Framework\DB\Adapter; use Magento\Framework\DB\Ddl\Table; @@ -365,6 +366,7 @@ public function getIndexList($tableName, $schemaName = null); /** * Add new Foreign Key to table + * * If Foreign Key with same name is exist - it will be deleted * * @param string $fkName @@ -373,7 +375,6 @@ public function getIndexList($tableName, $schemaName = null); * @param string $refTableName * @param string $refColumnName * @param string $onDelete - * @param string $onUpdate * @param boolean $purge trying remove invalid data * @param string $schemaName * @param string $refSchemaName @@ -484,6 +485,7 @@ public function insert($table, array $bind); /** * Inserts a table row with specified data + * * Special for Zero values to identity column * * @param string $table @@ -502,9 +504,9 @@ public function insertForce($table, array $bind); * If the $where parameter is an array of multiple clauses, they will be joined by AND, with each clause wrapped in * parenthesis. If you wish to use an OR, you must give a single clause that is an instance of {@see Zend_Db_Expr} * - * @param mixed $table The table to update. - * @param array $bind Column-value pairs. - * @param mixed $where UPDATE WHERE clause(s). + * @param mixed $table The table to update. + * @param array $bind Column-value pairs. + * @param mixed $where UPDATE WHERE clause(s). * @return int The number of affected rows. */ public function update($table, array $bind, $where = ''); @@ -512,8 +514,8 @@ public function update($table, array $bind, $where = ''); /** * Deletes table rows based on a WHERE clause. * - * @param mixed $table The table to update. - * @param mixed $where DELETE WHERE clause(s). + * @param mixed $table The table to update. + * @param mixed $where DELETE WHERE clause(s). * @return int The number of affected rows. */ public function delete($table, $where = ''); @@ -521,31 +523,33 @@ public function delete($table, $where = ''); /** * Prepares and executes an SQL statement with bound data. * - * @param mixed $sql The SQL statement with placeholders. + * @param mixed $sql The SQL statement with placeholders. * May be a string or \Magento\Framework\DB\Select. - * @param mixed $bind An array of data or data itself to bind to the placeholders. + * @param mixed $bind An array of data or data itself to bind to the placeholders. * @return \Zend_Db_Statement_Interface */ public function query($sql, $bind = []); /** * Fetches all SQL result rows as a sequential array. + * * Uses the current fetchMode for the adapter. * - * @param string|\Magento\Framework\DB\Select $sql An SQL SELECT statement. - * @param mixed $bind Data to bind into SELECT placeholders. - * @param mixed $fetchMode Override current fetch mode. + * @param string|\Magento\Framework\DB\Select $sql An SQL SELECT statement. + * @param mixed $bind Data to bind into SELECT placeholders. + * @param mixed $fetchMode Override current fetch mode. * @return array */ public function fetchAll($sql, $bind = [], $fetchMode = null); /** * Fetches the first row of the SQL result. + * * Uses the current fetchMode for the adapter. * * @param string|\Magento\Framework\DB\Select $sql An SQL SELECT statement. * @param mixed $bind Data to bind into SELECT placeholders. - * @param mixed $fetchMode Override current fetch mode. + * @param mixed $fetchMode Override current fetch mode. * @return array */ public function fetchRow($sql, $bind = [], $fetchMode = null); @@ -622,9 +626,9 @@ public function quote($value, $type = null); * // $safe = "WHERE date < '2005-01-02'" * * - * @param string $text The text with a placeholder. - * @param mixed $value The value to quote. - * @param string $type OPTIONAL SQL datatype + * @param string $text The text with a placeholder. + * @param mixed $value The value to quote. + * @param string $type OPTIONAL SQL datatype * @param integer $count OPTIONAL count of placeholders to replace * @return string An SQL-safe quoted value placed into the original text. */ @@ -633,7 +637,7 @@ public function quoteInto($text, $value, $type = null, $count = null); /** * Quotes an identifier. * - * Accepts a string representing a qualified indentifier. For Example: + * Accepts a string representing a qualified identifier. For Example: * * $adapter->quoteIdentifier('myschema.mytable') * @@ -721,7 +725,8 @@ public function disallowDdlCache(); /** * Reset cached DDL data from cache - * if table name is null - reset all cached DDL data + * + * If table name is null - reset all cached DDL data * * @param string $tableName * @param string $schemaName OPTIONAL @@ -741,6 +746,7 @@ public function saveDdlCache($tableCacheKey, $ddlType, $data); /** * Load DDL data from cache + * * Return false if cache does not exists * * @param string $tableCacheKey the table cache key @@ -784,6 +790,7 @@ public function prepareSqlCondition($fieldName, $condition); /** * Prepare value for save in column + * * Return converted to column data type value * * @param array $column the column describe array @@ -813,6 +820,7 @@ public function getIfNullSql($expression, $value = 0); /** * Generate fragment of SQL, that combine together (concatenate) the results from data array + * * All arguments in data must be quoted * * @param array $data @@ -823,6 +831,7 @@ public function getConcatSql(array $data, $separator = null); /** * Generate fragment of SQL that returns length of character string + * * The string argument must be quoted * * @param string $string @@ -931,6 +940,7 @@ public function getDateExtractSql($date, $unit); /** * Retrieve valid table name + * * Check table name length and allowed symbols * * @param string $tableName @@ -950,6 +960,7 @@ public function getTriggerName($tableName, $time, $event); /** * Retrieve valid index name + * * Check index name length and allowed symbols * * @param string $tableName @@ -961,6 +972,7 @@ public function getIndexName($tableName, $fields, $indexType = ''); /** * Retrieve valid foreign key name + * * Check foreign key name length and allowed symbols * * @param string $priTableName @@ -1047,6 +1059,7 @@ public function supportStraightJoin(); /** * Adds order by random to select object + * * Possible using integer field for optimization * * @param \Magento\Framework\DB\Select $select @@ -1074,6 +1087,7 @@ public function getPrimaryKeyName($tableName, $schemaName = null); /** * Converts fetched blob into raw binary PHP data. + * * Some DB drivers return blobs as hex-coded strings, so we need to process them. * * @param mixed $value @@ -1114,6 +1128,8 @@ public function dropTrigger($triggerName, $schemaName = null); public function getTables($likeCondition = null); /** + * Generates case SQL fragment + * * Generate fragment of SQL, that check value against multiple condition cases * and return different result depends on them * diff --git a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php index 441da10253a1..edec7e135ae9 100644 --- a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php +++ b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php @@ -515,6 +515,7 @@ protected function _checkDdlTransaction($sql) /** * Special handling for PDO query(). + * * All bind parameter names must begin with ':'. * * @param string|\Magento\Framework\DB\Select $sql The SQL statement with placeholders. @@ -595,6 +596,7 @@ protected function _query($sql, $bind = []) /** * Special handling for PDO query(). + * * All bind parameter names must begin with ':'. * * @param string|\Magento\Framework\DB\Select $sql The SQL statement with placeholders. @@ -615,9 +617,10 @@ public function query($sql, $bind = []) } /** + * Allows multiple queries + * * Allows multiple queries -- to safeguard against SQL injection, USE CAUTION and verify that input * cannot be tampered with. - * * Special handling for PDO query(). * All bind parameter names must begin with ':'. * @@ -1278,6 +1281,7 @@ public function getForeignKeysTree() /** * Modify tables, used for upgrade process + * * Change columns definitions, reset foreign keys, change tables comments and engines. * * The value of each array element is an associative array @@ -1469,9 +1473,9 @@ public function select() * * Method revrited for handle empty arrays in value param * - * @param string $text The text with a placeholder. - * @param mixed $value The value to quote. - * @param string $type OPTIONAL SQL datatype + * @param string $text The text with a placeholder. + * @param mixed $value The value to quote. + * @param string $type OPTIONAL SQL datatype * @param integer $count OPTIONAL count of placeholders to replace * @return string An SQL-safe quoted value placed into the orignal text. */ @@ -1514,6 +1518,7 @@ protected function _getCacheId($tableKey, $ddlType) /** * Load DDL data from cache + * * Return false if cache does not exists * * @param string $tableCacheKey the table cache key @@ -1568,7 +1573,8 @@ public function saveDdlCache($tableCacheKey, $ddlType, $data) /** * Reset cached DDL data from cache - * if table name is null - reset all cached DDL data + * + * If table name is null - reset all cached DDL data * * @param string $tableName * @param string $schemaName OPTIONAL @@ -1605,6 +1611,7 @@ public function resetDdlCache($tableName = null, $schemaName = null) /** * Disallow DDL caching + * * @return $this */ public function disallowDdlCache() @@ -1615,6 +1622,7 @@ public function disallowDdlCache() /** * Allow DDL caching + * * @return $this */ public function allowDdlCache() @@ -1675,9 +1683,10 @@ public function describeTable($tableName, $schemaName = null) /** * Format described column to definition, ready to be added to ddl table. + * * Return array with keys: name, type, length, options, comment * - * @param array $columnData + * @param array $columnData * @return array * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) @@ -1892,6 +1901,7 @@ public function changeTableComment($tableName, $comment, $schemaName = null) /** * Inserts a table row with specified data + * * Special for Zero values to identity column * * @param string $table @@ -2144,7 +2154,6 @@ public function createTable(Table $table) */ public function createTemporaryTable(\Magento\Framework\DB\Ddl\Table $table) { - $columns = $table->getColumns(); $sqlFragment = array_merge( $this->_getColumnsDefinition($table), $this->_getIndexesDefinition($table), @@ -2782,6 +2791,7 @@ public function dropIndex($tableName, $keyName, $schemaName = null) /** * Add new Foreign Key to table + * * If Foreign Key with same name is exist - it will be deleted * * @param string $fkName @@ -2904,6 +2914,7 @@ public function endSetup() * - array("gteq" => $greaterOrEqualValue) * - array("lteq" => $lessOrEqualValue) * - array("finset" => $valueInSet) + * - array("nfinset" => $valueNotInSet) * - array("regexp" => $regularExpression) * - array("seq" => $stringValue) * - array("sneq" => $stringValue) @@ -2933,6 +2944,7 @@ public function prepareSqlCondition($fieldName, $condition) 'gteq' => "{{fieldName}} >= ?", 'lteq' => "{{fieldName}} <= ?", 'finset' => "FIND_IN_SET(?, {{fieldName}})", + 'nfinset' => "NOT FIND_IN_SET(?, {{fieldName}})", 'regexp' => "{{fieldName}} REGEXP ?", 'from' => "{{fieldName}} >= ?", 'to' => "{{fieldName}} <= ?", @@ -2954,7 +2966,7 @@ public function prepareSqlCondition($fieldName, $condition) if (isset($condition['to'])) { $query .= empty($query) ? '' : ' AND '; $to = $this->_prepareSqlDateCondition($condition, 'to'); - $query = $this->_prepareQuotedSqlCondition($query . $conditionKeyMap['to'], $to, $fieldName); + $query = $query . $this->_prepareQuotedSqlCondition($conditionKeyMap['to'], $to, $fieldName); } } elseif (array_key_exists($key, $conditionKeyMap)) { $value = $condition[$key]; @@ -3016,6 +3028,7 @@ protected function _transformStringSqlCondition($conditionKey, $value) /** * Prepare value for save in column + * * Return converted to column data type value * * @param array $column the column describe array @@ -3139,6 +3152,8 @@ public function getIfNullSql($expression, $value = 0) } /** + * Generates case SQL fragment + * * Generate fragment of SQL, that check value against multiple condition cases * and return different result depends on them * @@ -3163,6 +3178,7 @@ public function getCaseSql($valueName, $casesResults, $defaultValue = null) /** * Generate fragment of SQL, that combine together (concatenate) the results from data array + * * All arguments in data must be quoted * * @param string[] $data @@ -3177,6 +3193,7 @@ public function getConcatSql(array $data, $separator = null) /** * Generate fragment of SQL that returns length of character string + * * The string argument must be quoted * * @param string $string @@ -3188,6 +3205,8 @@ public function getLengthSql($string) } /** + * Generate least SQL fragment + * * Generate fragment of SQL, that compare with two or more arguments, and returns the smallest * (minimum-valued) argument * All arguments in data must be quoted @@ -3201,6 +3220,8 @@ public function getLeastSql(array $data) } /** + * Generate greatest SQL fragment + * * Generate fragment of SQL, that compare with two or more arguments, and returns the largest * (maximum-valued) argument * All arguments in data must be quoted @@ -3371,6 +3392,7 @@ public function getTriggerName($tableName, $time, $event) /** * Retrieve valid index name + * * Check index name length and allowed symbols * * @param string $tableName @@ -3400,6 +3422,7 @@ public function getIndexName($tableName, $fields, $indexType = '') /** * Retrieve valid foreign key name + * * Check foreign key name length and allowed symbols * * @param string $priTableName @@ -3668,6 +3691,7 @@ public function supportStraightJoin() /** * Adds order by random to select object + * * Possible using integer field for optimization * * @param Select $select @@ -3845,6 +3869,7 @@ public function getPrimaryKeyName($tableName, $schemaName = null) /** * Parse text size + * * Returns max allowed size if value great it * * @param string|int $size @@ -3857,13 +3882,13 @@ protected function _parseTextSize($size) switch ($last) { case 'k': - $size = intval($size) * 1024; + $size = (int)$size * 1024; break; case 'm': - $size = intval($size) * 1024 * 1024; + $size = (int)$size * 1024 * 1024; break; case 'g': - $size = intval($size) * 1024 * 1024 * 1024; + $size = (int)$size * 1024 * 1024 * 1024; break; } @@ -3874,11 +3899,12 @@ protected function _parseTextSize($size) return Table::MAX_TEXT_SIZE; } - return intval($size); + return (int)$size; } /** * Converts fetched blob into raw binary PHP data. + * * The MySQL drivers do it nice, no processing required. * * @param mixed $value diff --git a/lib/internal/Magento/Framework/DB/Helper.php b/lib/internal/Magento/Framework/DB/Helper.php index 4cb8304cdedf..4d4eac62978c 100644 --- a/lib/internal/Magento/Framework/DB/Helper.php +++ b/lib/internal/Magento/Framework/DB/Helper.php @@ -8,6 +8,9 @@ namespace Magento\Framework\DB; +/** + * DataBase Helper + */ class Helper extends \Magento\Framework\DB\Helper\AbstractHelper { /** @@ -52,7 +55,7 @@ protected function _prepareOrder(\Magento\Framework\DB\Select $select, $autoRese * Field can be with 'dot' delimiter. * * @param string $field - * @param bool $reverse OPTIONAL + * @param bool $reverse OPTIONAL * @return string */ protected function _truncateAliasName($field, $reverse = false) @@ -143,6 +146,7 @@ protected function _prepareHaving(\Magento\Framework\DB\Select $select, $autoRes } /** + * Assemble limit * * @param string $query * @param int $limitCount @@ -153,12 +157,12 @@ protected function _prepareHaving(\Magento\Framework\DB\Select $select, $autoRes protected function _assembleLimit($query, $limitCount, $limitOffset, $columnList = []) { if ($limitCount !== null) { - $limitCount = intval($limitCount); + $limitCount = (int)$limitCount; if ($limitCount <= 0) { //throw new \Exception("LIMIT argument count={$limitCount} is not valid"); } - $limitOffset = intval($limitOffset); + $limitOffset = (int)$limitOffset; if ($limitOffset < 0) { //throw new \Exception("LIMIT argument offset={$limitOffset} is not valid"); } diff --git a/lib/internal/Magento/Framework/DB/Query.php b/lib/internal/Magento/Framework/DB/Query.php index 04ac2006b5e1..36aab54df392 100644 --- a/lib/internal/Magento/Framework/DB/Query.php +++ b/lib/internal/Magento/Framework/DB/Query.php @@ -142,7 +142,7 @@ public function getSize() $sql = $this->getSelectCountSql(); $this->totalRecords = $this->getConnection()->fetchOne($sql, $this->bindParams); } - return intval($this->totalRecords); + return (int)$this->totalRecords; } /** diff --git a/lib/internal/Magento/Framework/DB/Query/BatchIterator.php b/lib/internal/Magento/Framework/DB/Query/BatchIterator.php index 7b4406fd9adb..6485973ec359 100644 --- a/lib/internal/Magento/Framework/DB/Query/BatchIterator.php +++ b/lib/internal/Magento/Framework/DB/Query/BatchIterator.php @@ -88,6 +88,8 @@ public function __construct( } /** + * Returns current select + * * @return Select */ public function current() @@ -101,6 +103,8 @@ public function current() } /** + * Returns next select + * * @return Select */ public function next() @@ -121,6 +125,8 @@ public function next() } /** + * Returns key + * * @return int */ public function key() @@ -129,6 +135,8 @@ public function key() } /** + * Returns is valid + * * @return bool */ public function valid() @@ -137,6 +145,8 @@ public function valid() } /** + * Rewind + * * @return void */ public function rewind() @@ -165,7 +175,7 @@ private function calculateBatchSize(Select $select) ); $row = $this->connection->fetchRow($wrapperSelect); $this->minValue = $row['max']; - return intval($row['cnt']); + return (int)$row['cnt']; } /** diff --git a/lib/internal/Magento/Framework/DB/Query/BatchRangeIterator.php b/lib/internal/Magento/Framework/DB/Query/BatchRangeIterator.php index 5176c85cfa70..e2ef4ae530a9 100644 --- a/lib/internal/Magento/Framework/DB/Query/BatchRangeIterator.php +++ b/lib/internal/Magento/Framework/DB/Query/BatchRangeIterator.php @@ -201,7 +201,7 @@ private function initSelectObject() ); $row = $this->connection->fetchRow($wrapperSelect); - $this->totalItemCount = intval($row['cnt']); + $this->totalItemCount = (int)$row['cnt']; } $rangeField = is_array($this->rangeField) ? $this->rangeField : [$this->rangeField]; diff --git a/lib/internal/Magento/Framework/DB/Select.php b/lib/internal/Magento/Framework/DB/Select.php index 4d178b81af6d..f33aaea7d0e6 100644 --- a/lib/internal/Magento/Framework/DB/Select.php +++ b/lib/internal/Magento/Framework/DB/Select.php @@ -400,7 +400,7 @@ public function useStraightJoin($flag = true) /** * Render STRAIGHT_JOIN clause * - * @param string $sql SQL query + * @param string $sql SQL query * @return string */ protected function _renderStraightjoin($sql) @@ -452,7 +452,7 @@ public function orderRand($field = null) /** * Render FOR UPDATE clause * - * @param string $sql SQL query + * @param string $sql SQL query * @return string */ protected function _renderForupdate($sql) @@ -467,9 +467,9 @@ protected function _renderForupdate($sql) /** * Add EXISTS clause * - * @param Select $select - * @param string $joinCondition - * @param bool $isExists + * @param Select $select + * @param string $joinCondition + * @param bool $isExists * @return $this */ public function exists($select, $joinCondition, $isExists = true) @@ -509,11 +509,18 @@ public function assemble() } /** + * Remove links to other objects. + * * @return string[] * @since 100.0.11 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + $properties = array_keys(get_object_vars($this)); $properties = array_diff( $properties, @@ -530,9 +537,14 @@ public function __sleep() * * @return void * @since 100.0.11 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); $this->_adapter = $objectManager->get(ResourceConnection::class)->getConnection(); $this->selectRenderer = $objectManager->get(\Magento\Framework\DB\Select\SelectRenderer::class); diff --git a/lib/internal/Magento/Framework/DB/Select/RendererProxy.php b/lib/internal/Magento/Framework/DB/Select/RendererProxy.php index 3626f6a07fa1..dc69b96b7905 100644 --- a/lib/internal/Magento/Framework/DB/Select/RendererProxy.php +++ b/lib/internal/Magento/Framework/DB/Select/RendererProxy.php @@ -56,10 +56,17 @@ public function __construct( } /** + * Remove links to other objects. + * * @return array + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + return ['_subject', '_isShared']; } @@ -67,9 +74,14 @@ public function __sleep() * Retrieve ObjectManager from global scope * * @return void + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + $this->_objectManager = \Magento\Framework\App\ObjectManager::getInstance(); } @@ -99,7 +111,7 @@ protected function _getSubject() } /** - * {@inheritdoc} + * @inheritdoc */ public function render(\Magento\Framework\DB\Select $select, $sql = '') { diff --git a/lib/internal/Magento/Framework/DB/Sql/LimitExpression.php b/lib/internal/Magento/Framework/DB/Sql/LimitExpression.php index 9e875e485c67..3c5cb65e898f 100644 --- a/lib/internal/Magento/Framework/DB/Sql/LimitExpression.php +++ b/lib/internal/Magento/Framework/DB/Sql/LimitExpression.php @@ -41,19 +41,19 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function __toString() { $sql = $this->sql; - $count = intval($this->count); + $count = (int)$this->count; if ($count <= 0) { /** @see Zend_Db_Adapter_Exception */ #require_once 'Zend/Db/Adapter/Exception.php'; throw new \Zend_Db_Adapter_Exception("LIMIT argument count=$count is not valid"); } - $offset = intval($this->offset); + $offset = (int)$this->offset; if ($offset < 0) { /** @see Zend_Db_Adapter_Exception */ #require_once 'Zend/Db/Adapter/Exception.php'; diff --git a/lib/internal/Magento/Framework/DB/Sql/UnionExpression.php b/lib/internal/Magento/Framework/DB/Sql/UnionExpression.php index 3ce78177d875..f1d093b7deaf 100644 --- a/lib/internal/Magento/Framework/DB/Sql/UnionExpression.php +++ b/lib/internal/Magento/Framework/DB/Sql/UnionExpression.php @@ -22,18 +22,25 @@ class UnionExpression extends Expression */ protected $type; + /** + * @var string + */ + protected $pattern; + /** * @param Select[] $parts - * @param string $type + * @param string $type (optional) + * @param string $pattern (optional) */ - public function __construct(array $parts, $type = Select::SQL_UNION) + public function __construct(array $parts, $type = Select::SQL_UNION, $pattern = '') { $this->parts = $parts; $this->type = $type; + $this->pattern = $pattern; } /** - * @return string + * @inheritdoc */ public function __toString() { @@ -45,6 +52,10 @@ public function __toString() $parts[] = $part; } } - return implode($parts, $this->type); + $sql = implode($parts, $this->type); + if ($this->pattern) { + return sprintf($this->pattern, $sql); + } + return $sql; } } diff --git a/lib/internal/Magento/Framework/DB/Statement/Pdo/Mysql.php b/lib/internal/Magento/Framework/DB/Statement/Pdo/Mysql.php index 7b8314a76f32..d24bc5fef6ef 100644 --- a/lib/internal/Magento/Framework/DB/Statement/Pdo/Mysql.php +++ b/lib/internal/Magento/Framework/DB/Statement/Pdo/Mysql.php @@ -3,21 +3,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +namespace Magento\Framework\DB\Statement\Pdo; + +use Magento\Framework\DB\Statement\Parameter; /** * Mysql DB Statement * * @author Magento Core Team */ -namespace Magento\Framework\DB\Statement\Pdo; - -use Magento\Framework\DB\Statement\Parameter; - class Mysql extends \Zend_Db_Statement_Pdo { + /** - * Executes statement with binding values to it. - * Allows transferring specific options to DB driver. + * Executes statement with binding values to it. Allows transferring specific options to DB driver. * * @param array $params Array of values to bind to parameter placeholders. * @return bool @@ -61,11 +60,9 @@ public function _executeWithBinding(array $params) $statement->bindParam($paramName, $bindValues[$name], $dataType, $length, $driverOptions); } - try { + return $this->tryExecute(function () use ($statement) { return $statement->execute(); - } catch (\PDOException $e) { - throw new \Zend_Db_Statement_Exception($e->getMessage(), (int)$e->getCode(), $e); - } + }); } /** @@ -90,7 +87,29 @@ public function _execute(array $params = null) if ($specialExecute) { return $this->_executeWithBinding($params); } else { - return parent::_execute($params); + return $this->tryExecute(function () use ($params) { + return $params !== null ? $this->_stmt->execute($params) : $this->_stmt->execute(); + }); + } + } + + /** + * Executes query and avoid warnings. + * + * @param callable $callback + * @return bool + * @throws \Zend_Db_Statement_Exception + */ + private function tryExecute($callback) + { + $previousLevel = error_reporting(\E_ERROR); // disable warnings for PDO bugs #63812, #74401 + try { + return $callback(); + } catch (\PDOException $e) { + $message = sprintf('%s, query was: %s', $e->getMessage(), $this->_stmt->queryString); + throw new \Zend_Db_Statement_Exception($message, (int)$e->getCode(), $e); + } finally { + error_reporting($previousLevel); } } } diff --git a/lib/internal/Magento/Framework/DB/Test/Unit/DB/Statement/MysqlTest.php b/lib/internal/Magento/Framework/DB/Test/Unit/DB/Statement/MysqlTest.php new file mode 100644 index 000000000000..714dfe6bb105 --- /dev/null +++ b/lib/internal/Magento/Framework/DB/Test/Unit/DB/Statement/MysqlTest.php @@ -0,0 +1,154 @@ +adapterMock = $this->getMockForAbstractClass( + \Zend_Db_Adapter_Abstract::class, + [], + '', + false, + true, + true, + ['getConnection', 'getProfiler'] + ); + $this->pdoMock = $this->createMock(\PDO::class); + $this->adapterMock->expects($this->once()) + ->method('getConnection') + ->willReturn($this->pdoMock); + $this->zendDbProfilerMock = $this->createMock(\Zend_Db_Profiler::class); + $this->adapterMock->expects($this->once()) + ->method('getProfiler') + ->willReturn($this->zendDbProfilerMock); + $this->pdoStatementMock = $this->createMock(\PDOStatement::class); + } + + public function testExecuteWithoutParams() + { + $query = 'SET @a=1;'; + $this->pdoMock->expects($this->once()) + ->method('prepare') + ->with($query) + ->willReturn($this->pdoStatementMock); + $this->pdoStatementMock->expects($this->once()) + ->method('execute'); + (new Mysql($this->adapterMock, $query))->_execute(); + } + + public function testExecuteWhenThrowPDOException() + { + $this->expectException(\Zend_Db_Statement_Exception::class); + $this->expectExceptionMessage('test message, query was:'); + $errorReporting = error_reporting(); + $query = 'SET @a=1;'; + $this->pdoMock->expects($this->once()) + ->method('prepare') + ->with($query) + ->willReturn($this->pdoStatementMock); + $this->pdoStatementMock->expects($this->once()) + ->method('execute') + ->willThrowException(new \PDOException('test message')); + + $this->assertEquals($errorReporting, error_reporting(), 'Error report level was\'t restored'); + + (new Mysql($this->adapterMock, $query))->_execute(); + } + + public function testExecuteWhenParamsAsPrimitives() + { + $params = [':param1' => 'value1', ':param2' => 'value2']; + $query = 'UPDATE `some_table1` SET `col1`=\'val1\' WHERE `param1`=\':param1\' AND `param2`=\':param2\';'; + $this->pdoMock->expects($this->once()) + ->method('prepare') + ->with($query) + ->willReturn($this->pdoStatementMock); + $this->pdoStatementMock->expects($this->never()) + ->method('bindParam'); + $this->pdoStatementMock->expects($this->once()) + ->method('execute') + ->with($params); + + (new Mysql($this->adapterMock, $query))->_execute($params); + } + + public function testExecuteWhenParamsAsParameterObject() + { + $param1 = $this->createMock(Parameter::class); + $param1Value = 'SomeValue'; + $param1DataType = 'dataType'; + $param1Length = '9'; + $param1DriverOptions = 'some driver options'; + $param1->expects($this->once()) + ->method('getIsBlob') + ->willReturn(false); + $param1->expects($this->once()) + ->method('getDataType') + ->willReturn($param1DataType); + $param1->expects($this->once()) + ->method('getLength') + ->willReturn($param1Length); + $param1->expects($this->once()) + ->method('getDriverOptions') + ->willReturn($param1DriverOptions); + $param1->expects($this->once()) + ->method('getValue') + ->willReturn($param1Value); + $params = [ + ':param1' => $param1, + ':param2' => 'value2', + ]; + $query = 'UPDATE `some_table1` SET `col1`=\'val1\' WHERE `param1`=\':param1\' AND `param2`=\':param2\';'; + $this->pdoMock->expects($this->once()) + ->method('prepare') + ->with($query) + ->willReturn($this->pdoStatementMock); + $this->pdoStatementMock->expects($this->exactly(2)) + ->method('bindParam') + ->withConsecutive( + [':param1', $param1Value, $param1DataType, $param1Length, $param1DriverOptions], + [':param2', 'value2', \PDO::PARAM_STR, null, null] + ); + $this->pdoStatementMock->expects($this->once()) + ->method('execute'); + + (new Mysql($this->adapterMock, $query))->_execute($params); + } +} diff --git a/lib/internal/Magento/Framework/DB/Tree.php b/lib/internal/Magento/Framework/DB/Tree.php index 9890c6ef0d24..1aeaf122131f 100644 --- a/lib/internal/Magento/Framework/DB/Tree.php +++ b/lib/internal/Magento/Framework/DB/Tree.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Framework\DB; @@ -15,6 +16,7 @@ * Magento Library * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * phpcs:ignoreFile * * @deprecated Not used anymore. */ @@ -300,7 +302,6 @@ public function getNodeInfo($nodeId) * @param string|int $nodeId * @param array $data * @return false|string - * @SuppressWarnings(PHPMD.ExitExpression) * * @deprecated Not used anymore. */ @@ -477,7 +478,6 @@ public function removeNode($nodeId) * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * @SuppressWarnings(PHPMD.ExitExpression) * * @deprecated Not used anymore. */ @@ -814,7 +814,6 @@ public function moveNode($eId, $pId, $aId = 0) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @SuppressWarnings(PHPMD.UnusedLocalVariable) - * @SuppressWarnings(PHPMD.ExitExpression) * * @deprecated Not used anymore. */ @@ -1041,7 +1040,6 @@ protected function _addExtTablesToSelect(Select &$select) * @param int $startLevel * @param int $endLevel * @return NodeSet - * @SuppressWarnings(PHPMD.ExitExpression) * * @deprecated Not used anymore. */ diff --git a/lib/internal/Magento/Framework/Data/Argument/Interpreter/ArrayType.php b/lib/internal/Magento/Framework/Data/Argument/Interpreter/ArrayType.php index b6426a8d2638..9a881ccf515a 100644 --- a/lib/internal/Magento/Framework/Data/Argument/Interpreter/ArrayType.php +++ b/lib/internal/Magento/Framework/Data/Argument/Interpreter/ArrayType.php @@ -28,7 +28,7 @@ public function __construct(InterpreterInterface $itemInterpreter) } /** - * {@inheritdoc} + * @inheritdoc * @return array * @throws \InvalidArgumentException */ @@ -90,11 +90,11 @@ private function compareItems($firstItemKey, $secondItemKey, $indexedItems) $firstValue = 0; $secondValue = 0; if (isset($firstItem['sortOrder'])) { - $firstValue = intval($firstItem['sortOrder']); + $firstValue = (int)$firstItem['sortOrder']; } if (isset($secondItem['sortOrder'])) { - $secondValue = intval($secondItem['sortOrder']); + $secondValue = (int)$secondItem['sortOrder']; } if ($firstValue == $secondValue) { diff --git a/lib/internal/Magento/Framework/Data/Collection.php b/lib/internal/Magento/Framework/Data/Collection.php index 71ec8c1aa837..2f3aaad98dfe 100644 --- a/lib/internal/Magento/Framework/Data/Collection.php +++ b/lib/internal/Magento/Framework/Data/Collection.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Framework\Data; use Magento\Framework\Data\Collection\EntityFactoryInterface; @@ -285,7 +286,7 @@ public function getSize() if ($this->_totalRecords === null) { $this->_totalRecords = count($this->getItems()); } - return intval($this->_totalRecords); + return (int)$this->_totalRecords; } /** @@ -391,7 +392,7 @@ public function getItemByColumnValue($column, $value) /** * Adding item to item array * - * @param \Magento\Framework\DataObject $item + * @param \Magento\Framework\DataObject $item * @return $this * @throws \Exception */ @@ -401,6 +402,7 @@ public function addItem(\Magento\Framework\DataObject $item) if ($itemId !== null) { if (isset($this->_items[$itemId])) { + //phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception( 'Item (' . get_class($item) . ') with the same ID "' . $item->getId() . '" already exists.' ); @@ -452,7 +454,7 @@ public function getAllIds() /** * Remove item from collection by item key * - * @param mixed $key + * @param mixed $key * @return $this */ public function removeItemByKey($key) @@ -483,12 +485,12 @@ public function clear() { $this->_setIsLoaded(false); $this->_items = []; + $this->_totalRecords = null; return $this; } /** - * Walk through the collection and run model method or external callback - * with optional arguments + * Walk through the collection and run model method or external callback with optional arguments * * Returns array with results of callback for each item * @@ -540,8 +542,8 @@ public function each($objMethod, $args = []) /** * Setting data for all collection items * - * @param mixed $key - * @param mixed $value + * @param mixed $key + * @param mixed $value * @return $this */ public function setDataToAll($key, $value = null) @@ -561,7 +563,7 @@ public function setDataToAll($key, $value = null) /** * Set current page * - * @param int $page + * @param int $page * @return $this */ public function setCurPage($page) @@ -573,7 +575,7 @@ public function setCurPage($page) /** * Set collection page size * - * @param int $size + * @param int $size * @return $this */ public function setPageSize($size) @@ -585,8 +587,8 @@ public function setPageSize($size) /** * Set select order * - * @param string $field - * @param string $direction + * @param string $field + * @param string $direction * @return $this */ public function setOrder($field, $direction = self::SORT_ORDER_DESC) @@ -598,7 +600,7 @@ public function setOrder($field, $direction = self::SORT_ORDER_DESC) /** * Set collection item class name * - * @param string $className + * @param string $className * @return $this * @throws \InvalidArgumentException */ @@ -743,7 +745,7 @@ public function toArray($arrRequiredFields = []) /** * Convert items array to array for select options * - * return items array + * Return items array * array( * $index => array( * 'value' => mixed @@ -772,6 +774,8 @@ protected function _toOptionArray($valueField = 'id', $labelField = 'name', $add } /** + * Returns option array + * * @return array */ public function toOptionArray() @@ -780,6 +784,8 @@ public function toOptionArray() } /** + * Returns options hash + * * @return array */ public function toOptionHash() @@ -790,12 +796,12 @@ public function toOptionHash() /** * Convert items array to hash for select options * - * return items hash + * Return items hash * array($value => $label) * - * @param string $valueField - * @param string $labelField - * @return array + * @param string $valueField + * @param string $labelField + * @return array */ protected function _toOptionHash($valueField = 'id', $labelField = 'name') { @@ -809,8 +815,8 @@ protected function _toOptionHash($valueField = 'id', $labelField = 'name') /** * Retrieve item by id * - * @param mixed $idValue - * @return \Magento\Framework\DataObject + * @param mixed $idValue + * @return \Magento\Framework\DataObject */ public function getItemById($idValue) { @@ -879,11 +885,18 @@ public function hasFlag($flag) } /** + * Sleep handler + * * @return string[] * @since 100.0.11 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + $properties = array_keys(get_object_vars($this)); $properties = array_diff( $properties, @@ -899,9 +912,14 @@ public function __sleep() * * @return void * @since 100.0.11 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); $this->_entityFactory = $objectManager->get(EntityFactoryInterface::class); } diff --git a/lib/internal/Magento/Framework/Data/Collection/AbstractDb.php b/lib/internal/Magento/Framework/Data/Collection/AbstractDb.php index 63ba6824e5ab..1b28e367dcc3 100644 --- a/lib/internal/Magento/Framework/Data/Collection/AbstractDb.php +++ b/lib/internal/Magento/Framework/Data/Collection/AbstractDb.php @@ -219,7 +219,7 @@ public function getSize() $sql = $this->getSelectCountSql(); $this->_totalRecords = $this->getConnection()->fetchOne($sql, $this->_bindParams); } - return intval($this->_totalRecords); + return (int)$this->_totalRecords; } /** @@ -276,7 +276,7 @@ public function setOrder($field, $direction = self::SORT_ORDER_DESC) } /** - * self::setOrder() alias + * Sets order and direction. * * @param string $field * @param string $direction @@ -365,6 +365,7 @@ protected function _renderFilters() /** * Hook for operations before rendering filters + * * @return void */ protected function _renderFiltersBefore() @@ -602,6 +603,7 @@ protected function beforeAddLoadedItem(\Magento\Framework\DataObject $item) } /** + * Returns an items collection. * Returns a collection item that corresponds to the fetched row * and moves the internal data pointer ahead * @@ -888,9 +890,14 @@ private function getMainTableAlias() /** * @inheritdoc * @since 100.0.11 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + return array_diff( parent::__sleep(), ['_fetchStrategy', '_logger', '_conn', 'extensionAttributesJoinProcessor'] @@ -900,9 +907,14 @@ public function __sleep() /** * @inheritdoc * @since 100.0.11 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + parent::__wakeup(); $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); $this->_logger = $objectManager->get(Logger::class); diff --git a/lib/internal/Magento/Framework/Data/Collection/Filesystem.php b/lib/internal/Magento/Framework/Data/Collection/Filesystem.php index 330ff4e975e8..b2bd352ea279 100644 --- a/lib/internal/Magento/Framework/Data/Collection/Filesystem.php +++ b/lib/internal/Magento/Framework/Data/Collection/Filesystem.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Framework\Data\Collection; use Magento\Framework\Data\Collection; @@ -27,63 +29,63 @@ class Filesystem extends \Magento\Framework\Data\Collection { /** - * Target directory + * Target directory. * * @var string */ protected $_targetDirs = []; /** - * Whether to collect files + * Whether to collect files. * * @var bool */ protected $_collectFiles = true; /** - * Whether to collect directories before files + * Whether to collect directories before files. * * @var bool */ protected $_dirsFirst = true; /** - * Whether to collect recursively + * Whether to collect recursively. * * @var bool */ protected $_collectRecursively = true; /** - * Whether to collect dirs + * Whether to collect dirs. * * @var bool */ protected $_collectDirs = false; /** - * \Directory names regex pre-filter + * \Directory names regex pre-filter. * * @var string */ protected $_allowedDirsMask = '/^[a-z0-9\.\-\_]+$/i'; /** - * Filenames regex pre-filter + * Filenames regex pre-filter. * * @var string */ protected $_allowedFilesMask = '/^[a-z0-9\.\-\_]+\.[a-z0-9]+$/i'; /** - * Disallowed filenames regex pre-filter match for better versatility + * Disallowed filenames regex pre-filter match for better versatility. * * @var string */ protected $_disallowedFilesMask = ''; /** - * Filter rendering helper variable + * Filter rendering helper variable. * * @var int * @see Collection::$_filter @@ -92,7 +94,7 @@ class Filesystem extends \Magento\Framework\Data\Collection private $_filterIncrement = 0; /** - * Filter rendering helper variable + * Filter rendering helper variable. * * @var array * @see Collection::$_filter @@ -101,7 +103,7 @@ class Filesystem extends \Magento\Framework\Data\Collection private $_filterBrackets = []; /** - * Filter rendering helper variable + * Filter rendering helper variable. * * @var string * @see Collection::$_filter @@ -110,22 +112,21 @@ class Filesystem extends \Magento\Framework\Data\Collection private $_filterEvalRendered = ''; /** - * Collecting items helper variable + * Collecting items helper variable. * * @var array */ protected $_collectedDirs = []; /** - * Collecting items helper variable + * Collecting items helper variable. * * @var array */ protected $_collectedFiles = []; /** - * Allowed dirs mask setter - * Set empty to not filter + * Allowed dirs mask setter. Set empty to not filter. * * @param string $regex * @return $this @@ -137,8 +138,7 @@ public function setDirsFilter($regex) } /** - * Allowed files mask setter - * Set empty to not filter + * Allowed files mask setter. Set empty to not filter. * * @param string $regex * @return $this @@ -150,8 +150,7 @@ public function setFilesFilter($regex) } /** - * Disallowed files mask setter - * Set empty value to not use this filter + * Disallowed files mask setter. Set empty value to not use this filter. * * @param string $regex * @return $this @@ -163,7 +162,7 @@ public function setDisallowedFilesFilter($regex) } /** - * Set whether to collect dirs + * Set whether to collect dirs. * * @param bool $value * @return $this @@ -175,7 +174,7 @@ public function setCollectDirs($value) } /** - * Set whether to collect files + * Set whether to collect files. * * @param bool $value * @return $this @@ -187,7 +186,7 @@ public function setCollectFiles($value) } /** - * Set whether to collect recursively + * Set whether to collect recursively. * * @param bool $value * @return $this @@ -199,7 +198,7 @@ public function setCollectRecursively($value) } /** - * Target directory setter. Adds directory to be scanned + * Target directory setter. Adds directory to be scanned. * * @param string $value * @return $this @@ -209,6 +208,7 @@ public function addTargetDir($value) { $value = (string)$value; if (!is_dir($value)) { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception('Unable to set target directory.'); } $this->_targetDirs[$value] = $value; @@ -216,8 +216,7 @@ public function addTargetDir($value) } /** - * Set whether to collect directories before files - * Works *before* sorting. + * Set whether to collect directories before files. Works *before* sorting. * * @param bool $value * @return $this @@ -229,7 +228,7 @@ public function setDirsFirst($value) } /** - * Get files from specified directory recursively (if needed) + * Get files from specified directory recursively (if needed). * * @param string|array $dir * @return void @@ -281,7 +280,7 @@ protected function _collectRecursive($dir) } /** - * Lauch data collecting + * Launch data collecting. * * @param bool $printQuery * @param bool $logQuery @@ -295,6 +294,7 @@ public function loadData($printQuery = false, $logQuery = false) return $this; } if (empty($this->_targetDirs)) { + // phpcs:disable Magento2.Exceptions.DirectThrow throw new \Exception('Please specify at least one target directory.'); } @@ -364,25 +364,22 @@ private function _generateAndFilterAndSort($attributeName) } /** - * Callback for sorting items - * Currently supports only sorting by one column + * Callback for sorting items. Currently supports only sorting by one column. * * @param array $a * @param array $b - * @return int|void + * @return int */ protected function _usort($a, $b) { foreach ($this->_orders as $key => $direction) { $result = $a[$key] > $b[$key] ? 1 : ($a[$key] < $b[$key] ? -1 : 0); return self::SORT_ORDER_ASC === strtoupper($direction) ? $result : -$result; - break; } } /** - * Set select order - * Currently supports only sorting by one column + * Set select order. Currently supports only sorting by one column. * * @param string $field * @param string $direction @@ -395,7 +392,7 @@ public function setOrder($field, $direction = self::SORT_ORDER_DESC) } /** - * Generate item row basing on the filename + * Generate item row basing on the filename. * * @param string $filename * @return array @@ -433,13 +430,11 @@ public function addCallbackFilter($field, $value, $type, $callback, $isInverted } /** - * The filters renderer and caller - * Applies to each row, renders once. + * The filters renderer and caller. Applies to each row, renders once. * * @param array $row * @return bool * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * @SuppressWarnings(PHPMD.EvalExpression) */ protected function _filterRow($row) { @@ -469,14 +464,14 @@ protected function _filterRow($row) } $result = false; if ($this->_filterEvalRendered) { + // phpcs:ignore Squiz.PHP.Eval eval('$result = ' . $this->_filterEvalRendered . ';'); } return $result; } /** - * Invokes specified callback - * Skips, if there is no filtered key in the row + * Invokes specified callback. Skips, if there is no filtered key in the row. * * @param callback $callback * @param array $callbackParams @@ -493,7 +488,7 @@ protected function _invokeFilter($callback, $callbackParams) } /** - * Fancy field filter + * Fancy field filter. * * @param string $field * @param mixed $cond @@ -626,7 +621,7 @@ public function addFieldToFilter($field, $cond, $type = 'and') } /** - * Prepare a bracket into filters + * Prepare a bracket into filters. * * @param string $bracket * @param bool $isAnd @@ -643,7 +638,7 @@ protected function _addFilterBracket($bracket = '(', $isAnd = true) } /** - * Render condition sign before element, if required + * Render condition sign before element, if required. * * @param int $increment * @param bool $isAnd @@ -666,7 +661,8 @@ protected function _renderConditionBeforeFilterElement($increment, $isAnd) } /** - * Does nothing. Intentionally disabled parent method + * Does nothing. Intentionally disabled parent method. + * * @param string $field * @param string $value * @param string $type @@ -679,7 +675,7 @@ public function addFilter($field, $value, $type = 'and') } /** - * Get all ids of collected items + * Get all ids of collected items. * * @return array */ @@ -689,7 +685,7 @@ public function getAllIds() } /** - * Callback method for 'like' fancy filter + * Callback method for 'like' fancy filter. * * @param string $field * @param mixed $filterValue @@ -700,6 +696,9 @@ public function getAllIds() */ public function filterCallbackLike($field, $filterValue, $row) { + // Forced to do this in order to keep backward compatibility for @api class. + // Strict typing must be added to this method next major release. + $filterValue = (string)$filterValue; $filterValue = trim(stripslashes($filterValue), '\''); $filterValue = trim($filterValue, '%'); $filterValueRegex = '(.*?)' . preg_quote($filterValue, '/') . '(.*?)'; @@ -708,7 +707,7 @@ public function filterCallbackLike($field, $filterValue, $row) } /** - * Callback method for 'eq' fancy filter + * Callback method for 'eq' fancy filter. * * @param string $field * @param mixed $filterValue @@ -723,7 +722,7 @@ public function filterCallbackEq($field, $filterValue, $row) } /** - * Callback method for 'in' fancy filter + * Callback method for 'in' fancy filter. * * @param string $field * @param mixed $filterValue @@ -738,7 +737,7 @@ public function filterCallbackInArray($field, $filterValue, $row) } /** - * Callback method for 'isnull' fancy filter + * Callback method for 'isnull' fancy filter. * * @param string $field * @param mixed $filterValue @@ -754,7 +753,7 @@ public function filterCallbackIsNull($field, $filterValue, $row) } /** - * Callback method for 'moreq' fancy filter + * Callback method for 'moreq' fancy filter. * * @param string $field * @param mixed $filterValue @@ -769,7 +768,7 @@ public function filterCallbackIsMoreThan($field, $filterValue, $row) } /** - * Callback method for 'lteq' fancy filter + * Callback method for 'lteq' fancy filter. * * @param string $field * @param mixed $filterValue diff --git a/lib/internal/Magento/Framework/Data/Form/Element/AbstractElement.php b/lib/internal/Magento/Framework/Data/Form/Element/AbstractElement.php index a8451e43ade2..14f4df7208b0 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/AbstractElement.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/AbstractElement.php @@ -170,7 +170,11 @@ public function setId($id) */ public function getHtmlId() { - return $this->getForm()->getHtmlIdPrefix() . $this->getData('html_id') . $this->getForm()->getHtmlIdSuffix(); + return $this->_escaper->escapeHtml( + $this->getForm()->getHtmlIdPrefix() . + $this->getData('html_id') . + $this->getForm()->getHtmlIdSuffix() + ); } /** @@ -180,7 +184,7 @@ public function getHtmlId() */ public function getName() { - $name = $this->getData('name'); + $name = $this->_escaper->escapeHtml($this->getData('name')); if ($suffix = $this->getForm()->getFieldNameSuffix()) { $name = $this->getForm()->addSuffixToName($name, $suffix); } @@ -201,6 +205,8 @@ public function setType($type) } /** + * Set form. + * * @param AbstractForm $form * @return $this */ @@ -238,6 +244,7 @@ public function getHtmlAttributes() 'onchange', 'disabled', 'readonly', + 'autocomplete', 'tabindex', 'placeholder', 'data-form-part', @@ -326,6 +333,8 @@ public function getRenderer() } /** + * Get Ui Id. + * * @param null|string $suffix * @return string */ @@ -334,7 +343,7 @@ protected function _getUiId($suffix = null) if ($this->_renderer instanceof \Magento\Framework\View\Element\AbstractBlock) { return $this->_renderer->getUiId($this->getType(), $this->getName(), $suffix); } else { - return ' data-ui-id="form-element-' . $this->getName() . ($suffix ?: '') . '"'; + return ' data-ui-id="form-element-' . $this->_escaper->escapeHtml($this->getName()) . ($suffix ?: '') . '"'; } } diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Date.php b/lib/internal/Magento/Framework/Data/Form/Element/Date.php index ed5457edc7f3..6e4e97dbac79 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Date.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Date.php @@ -9,11 +9,15 @@ * * @author Magento Core Team */ + namespace Magento\Framework\Data\Form\Element; use Magento\Framework\Escaper; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +/** + * Date element + */ class Date extends AbstractElement { /** @@ -50,8 +54,20 @@ public function __construct( } /** - * If script executes on x64 system, converts large - * numeric values to timestamp limit + * Check if a string is a date value + * + * @param string $value + * @return bool + */ + private function isDate(string $value): bool + { + $date = date_parse($value); + + return !empty($date['year']) && !empty($date['month']) && !empty($date['day']); + } + + /** + * If script executes on x64 system, converts large numeric values to timestamp limit * * @param int $value * @return int @@ -82,13 +98,14 @@ public function setValue($value) $this->_value = $value; return $this; } - if (preg_match('/^[0-9]+$/', $value)) { - $this->_value = (new \DateTime())->setTimestamp($this->_toTimestamp($value)); - return $this; - } - try { - $this->_value = new \DateTime($value, new \DateTimeZone($this->localeDate->getConfigTimezone())); + if (preg_match('/^[0-9]+$/', $value)) { + $this->_value = (new \DateTime())->setTimestamp($this->_toTimestamp($value)); + } elseif (is_string($value) && $this->isDate($value)) { + $this->_value = new \DateTime($value, new \DateTimeZone($this->localeDate->getConfigTimezone())); + } else { + $this->_value = ''; + } } catch (\Exception $e) { $this->_value = ''; } @@ -97,6 +114,7 @@ public function setValue($value) /** * Get date value as string. + * * Format can be specified, or it will be taken from $this->getFormat() * * @param string $format (compatible with \DateTime) @@ -146,7 +164,7 @@ public function getValueInstance() */ public function getElementHtml() { - $this->addClass('admin__control-text input-text'); + $this->addClass('admin__control-text input-text input-date'); $dateFormat = $this->getDateFormat() ?: $this->getFormat(); $timeFormat = $this->getTimeFormat(); if (empty($dateFormat)) { diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Editor.php b/lib/internal/Magento/Framework/Data/Form/Element/Editor.php index 39a0479f7540..b5f2017501c0 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Editor.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Editor.php @@ -50,6 +50,8 @@ public function __construct( } /** + * Returns buttons translation + * * @return array */ protected function getButtonTranslations() @@ -64,6 +66,8 @@ protected function getButtonTranslations() } /** + * Returns JS config + * * @return bool|string * @throws \InvalidArgumentException */ @@ -80,8 +84,9 @@ protected function getJsonConfig() /** * Fetch config options from plugin. If $key is passed, return only that option key's value + * * @param string $pluginName - * @param null $key + * @param string|null $key * @return mixed all options or single option if $key is passed; null if nonexistent */ public function getPluginConfigOptions($pluginName, $key = null) @@ -101,13 +106,15 @@ public function getPluginConfigOptions($pluginName, $key = null) $pluginOptions = $plugins[$pluginArrIndex]['options']; if ($key !== null) { - return isset($pluginOptions[$key]) ? $pluginOptions[$key] : null; + return $pluginOptions[$key] ?? null; } else { return $pluginOptions; } } /** + * Returns element html + * * @return string * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -200,6 +207,8 @@ public function getElementHtml() } /** + * Returns theme + * * @return mixed */ public function getTheme() @@ -343,6 +352,8 @@ protected function _checkPluginButtonOptions($pluginOptions) } /** + * Convert options + * * Convert options by replacing template constructions ( like {{var_name}} ) * with data from this element object * @@ -389,6 +400,7 @@ protected function _getButtonHtml($data) /** * Wraps Editor HTML into div if 'use_container' config option is set to true + * * If 'no_display' config option is set to true, the div will be invisible * * @param string $html HTML code to wrap @@ -463,6 +475,8 @@ public function isHidden() } /** + * Is Toggle Button Visible + * * @return bool */ protected function isToggleButtonVisible() @@ -528,4 +542,13 @@ protected function getInlineJs($jsSetupObject, $forceLoad) '; return $jsString; } + + /** + * @inheritdoc + */ + public function getHtmlId() + { + $suffix = $this->getConfig('dynamic_id') ? '${ $.wysiwygUniqueSuffix }' : ''; + return parent::getHtmlId() . $suffix; + } } diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Factory.php b/lib/internal/Magento/Framework/Data/Form/Element/Factory.php index 69f9fb18411d..582b0c8cf654 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Factory.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Factory.php @@ -4,13 +4,13 @@ * See COPYING.txt for license details. */ -/** - * @author Magento Core Team - */ namespace Magento\Framework\Data\Form\Element; use Magento\Framework\ObjectManagerInterface; +/** + * Form element Factory + */ class Factory { /** @@ -81,7 +81,7 @@ public function create($elementType, array $config = []) $element = $this->_objectManager->create($className, $config); if (!$element instanceof AbstractElement) { throw new \InvalidArgumentException( - $className . ' doesn\'n extend \Magento\Framework\Data\Form\Element\AbstractElement' + $className . ' doesn\'t extend \Magento\Framework\Data\Form\Element\AbstractElement' ); } return $element; diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Fieldset.php b/lib/internal/Magento/Framework/Data/Form/Element/Fieldset.php index 66b1299b9869..90482ab55fc7 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Fieldset.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Fieldset.php @@ -43,7 +43,8 @@ public function __construct( */ public function getElementHtml() { - $html = '
serialize( + $html = $this->getBeforeElementHtml(); + $html .= '
serialize( ['class'] ) . $this->_getUiId() . '>' . "\n"; if ($this->getLegend()) { diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Label.php b/lib/internal/Magento/Framework/Data/Form/Element/Label.php index 901dcb5289e8..70b7885e7a0d 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Label.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Label.php @@ -4,13 +4,13 @@ * See COPYING.txt for license details. */ -/** - * Data form abstract class - * - * @author Magento Core Team - */ namespace Magento\Framework\Data\Form\Element; +use Magento\Framework\Phrase; + +/** + * Label form element. + */ class Label extends \Magento\Framework\Data\Form\Element\AbstractElement { /** @@ -37,8 +37,13 @@ public function __construct( public function getElementHtml() { $html = $this->getBold() ? '
' : '
'; - $html .= $this->getEscapedValue() . '
'; + if (is_string($this->getValue()) || $this->getValue() instanceof Phrase) { + $html .= $this->getEscapedValue(); + } + + $html .= '
'; $html .= $this->getAfterElementHtml(); + return $html; } } diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Text.php b/lib/internal/Magento/Framework/Data/Form/Element/Text.php index eb157c7279a7..2f001eb10307 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Text.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Text.php @@ -4,15 +4,13 @@ * See COPYING.txt for license details. */ -/** - * Form text element - * - * @author Magento Core Team - */ namespace Magento\Framework\Data\Form\Element; use Magento\Framework\Escaper; +/** + * Form text element + */ class Text extends AbstractElement { /** @@ -65,6 +63,7 @@ public function getHtmlAttributes() 'placeholder', 'data-form-part', 'data-role', + 'data-validation-params', 'data-action' ]; } diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Textarea.php b/lib/internal/Magento/Framework/Data/Form/Element/Textarea.php index 7cd3fb1f7fb9..1970ebeb9544 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Textarea.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Textarea.php @@ -4,15 +4,15 @@ * See COPYING.txt for license details. */ -/** - * Form textarea element - * - * @author Magento Core Team - */ namespace Magento\Framework\Data\Form\Element; use Magento\Framework\Escaper; +/** + * Form textarea element. + * + * @author Magento Core Team + */ class Textarea extends AbstractElement { /** @@ -64,6 +64,7 @@ public function getHtmlAttributes() 'rows', 'cols', 'readonly', + 'maxlength', 'disabled', 'onkeyup', 'tabindex', diff --git a/lib/internal/Magento/Framework/Data/Test/Unit/CollectionTest.php b/lib/internal/Magento/Framework/Data/Test/Unit/CollectionTest.php index 5608959c110f..daadeae2ac0e 100644 --- a/lib/internal/Magento/Framework/Data/Test/Unit/CollectionTest.php +++ b/lib/internal/Magento/Framework/Data/Test/Unit/CollectionTest.php @@ -5,6 +5,9 @@ */ namespace Magento\Framework\Data\Test\Unit; +/** + * Class for Collection test. + */ class CollectionTest extends \PHPUnit\Framework\TestCase { /** @@ -12,6 +15,9 @@ class CollectionTest extends \PHPUnit\Framework\TestCase */ protected $_model; + /** + * Set up. + */ protected function setUp() { $this->_model = new \Magento\Framework\Data\Collection( @@ -19,6 +25,11 @@ protected function setUp() ); } + /** + * Test for method removeAllItems. + * + * @return void + */ public function testRemoveAllItems() { $this->_model->addItem(new \Magento\Framework\DataObject()); @@ -30,6 +41,7 @@ public function testRemoveAllItems() /** * Test loadWithFilter() + * * @return void */ public function testLoadWithFilter() @@ -42,6 +54,8 @@ public function testLoadWithFilter() } /** + * Test for method etItemObjectClass + * * @dataProvider setItemObjectClassDataProvider */ public function testSetItemObjectClass($class) @@ -51,6 +65,8 @@ public function testSetItemObjectClass($class) } /** + * Data provider. + * * @return array */ public function setItemObjectClassDataProvider() @@ -59,6 +75,8 @@ public function setItemObjectClassDataProvider() } /** + * Test for method setItemObjectClass with exception. + * * @expectedException \InvalidArgumentException * @expectedExceptionMessage Incorrect_ClassName does not extend \Magento\Framework\DataObject */ @@ -67,12 +85,22 @@ public function testSetItemObjectClassException() $this->_model->setItemObjectClass('Incorrect_ClassName'); } + /** + * Test for method addFilter. + * + * @return void + */ public function testAddFilter() { $this->_model->addFilter('field1', 'value'); $this->assertEquals('field1', $this->_model->getFilter('field1')->getData('field')); } + /** + * Test for method getFilters. + * + * @return void + */ public function testGetFilters() { $this->_model->addFilter('field1', 'value'); @@ -81,12 +109,22 @@ public function testGetFilters() $this->assertEquals('field2', $this->_model->getFilter(['field1', 'field2'])[1]->getData('field')); } + /** + * Test for method get non existion filters. + * + * @return void + */ public function testGetNonExistingFilters() { $this->assertEmpty($this->_model->getFilter([])); $this->assertEmpty($this->_model->getFilter('non_existing_filter')); } + /** + * Test for lag. + * + * @return void + */ public function testFlag() { $this->_model->setFlag('flag_name', 'flag_value'); @@ -95,12 +133,22 @@ public function testFlag() $this->assertNull($this->_model->getFlag('non_existing_flag')); } + /** + * Test for method getCurPage. + * + * @return void + */ public function testGetCurPage() { - $this->_model->setCurPage(10); + $this->_model->setCurPage(1); $this->assertEquals(1, $this->_model->getCurPage()); } + /** + * Test for method possibleFlowWithItem. + * + * @return void + */ public function testPossibleFlowWithItem() { $firstItemMock = $this->createPartialMock( @@ -168,6 +216,11 @@ public function testPossibleFlowWithItem() $this->assertEquals([], $this->_model->getItems()); } + /** + * Test for method eachCallsMethodOnEachItemWithNoArgs. + * + * @return void + */ public function testEachCallsMethodOnEachItemWithNoArgs() { for ($i = 0; $i < 3; $i++) { @@ -177,7 +230,12 @@ public function testEachCallsMethodOnEachItemWithNoArgs() } $this->_model->each('testCallback'); } - + + /** + * Test for method eachCallsMethodOnEachItemWithArgs. + * + * @return void + */ public function testEachCallsMethodOnEachItemWithArgs() { for ($i = 0; $i < 3; $i++) { @@ -188,6 +246,11 @@ public function testEachCallsMethodOnEachItemWithArgs() $this->_model->each('testCallback', ['a', 'b', 'c']); } + /** + * Test for method callsClosureWithEachItemAndNoArgs. + * + * @return void + */ public function testCallsClosureWithEachItemAndNoArgs() { for ($i = 0; $i < 3; $i++) { @@ -200,6 +263,11 @@ public function testCallsClosureWithEachItemAndNoArgs() }); } + /** + * Test for method callsClosureWithEachItemAndArgs. + * + * @return void + */ public function testCallsClosureWithEachItemAndArgs() { for ($i = 0; $i < 3; $i++) { @@ -212,6 +280,11 @@ public function testCallsClosureWithEachItemAndArgs() }, ['a', 'b', 'c']); } + /** + * Test for method callsCallableArrayWithEachItemNoArgs. + * + * @return void + */ public function testCallsCallableArrayWithEachItemNoArgs() { $mockCallbackObject = $this->getMockBuilder('DummyEachCallbackInstance') @@ -230,6 +303,11 @@ public function testCallsCallableArrayWithEachItemNoArgs() $this->_model->each([$mockCallbackObject, 'testObjCallback']); } + /** + * Test for method callsCallableArrayWithEachItemAndArgs. + * + * @return void + */ public function testCallsCallableArrayWithEachItemAndArgs() { $mockCallbackObject = $this->getMockBuilder('DummyEachCallbackInstance') diff --git a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/AbstractElementTest.php b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/AbstractElementTest.php index e29b1dcf441e..d9dafddc571b 100644 --- a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/AbstractElementTest.php +++ b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/AbstractElementTest.php @@ -33,11 +33,12 @@ class AbstractElementTest extends \PHPUnit\Framework\TestCase protected function setUp() { + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->_factoryMock = $this->createMock(\Magento\Framework\Data\Form\Element\Factory::class); $this->_collectionFactoryMock = $this->createMock(\Magento\Framework\Data\Form\Element\CollectionFactory::class); - $this->_escaperMock = $this->createMock(\Magento\Framework\Escaper::class); + $this->_escaperMock = $objectManager->getObject(\Magento\Framework\Escaper::class); $this->_model = $this->getMockForAbstractClass( \Magento\Framework\Data\Form\Element\AbstractElement::class, @@ -195,6 +196,7 @@ public function testGetHtmlAttributes() 'onchange', 'disabled', 'readonly', + 'autocomplete', 'tabindex', 'placeholder', 'data-form-part', @@ -422,9 +424,6 @@ public function testGetHtmlContainerIdWithFieldContainerIdPrefix() */ public function testAddElementValues(array $initialData, $expectedValue) { - $this->_escaperMock->expects($this->any()) - ->method('escapeHtml') - ->will($this->returnArgument(0)); $this->_model->setValues($initialData['initial_values']); $this->_model->addElementValues($initialData['add_values'], $initialData['overwrite']); diff --git a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/LinkTest.php b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/LinkTest.php index a2a40ee03b04..cf3cd0345e17 100644 --- a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/LinkTest.php +++ b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/LinkTest.php @@ -23,9 +23,10 @@ class LinkTest extends \PHPUnit\Framework\TestCase protected function setUp() { + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $factoryMock = $this->createMock(\Magento\Framework\Data\Form\Element\Factory::class); $collectionFactoryMock = $this->createMock(\Magento\Framework\Data\Form\Element\CollectionFactory::class); - $escaperMock = $this->createMock(\Magento\Framework\Escaper::class); + $escaperMock = $objectManager->getObject(\Magento\Framework\Escaper::class); $this->_link = new \Magento\Framework\Data\Form\Element\Link( $factoryMock, $collectionFactoryMock, diff --git a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/MultiselectTest.php b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/MultiselectTest.php index 47eae73d8cd8..c515e0aca01d 100644 --- a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/MultiselectTest.php +++ b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/MultiselectTest.php @@ -5,6 +5,8 @@ */ namespace Magento\Framework\Data\Test\Unit\Form\Element; +use Magento\Framework\Escaper; + class MultiselectTest extends \PHPUnit\Framework\TestCase { /** @@ -15,7 +17,13 @@ class MultiselectTest extends \PHPUnit\Framework\TestCase protected function setUp() { $testHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->_model = $testHelper->getObject(\Magento\Framework\Data\Form\Element\Editablemultiselect::class); + $escaper = new Escaper(); + $this->_model = $testHelper->getObject( + \Magento\Framework\Data\Form\Element\Editablemultiselect::class, + [ + '_escaper' => $escaper + ] + ); $this->_model->setForm(new \Magento\Framework\DataObject()); } diff --git a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/NoteTest.php b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/NoteTest.php index f77f4a816a1a..ad7d20fdc0ac 100644 --- a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/NoteTest.php +++ b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/NoteTest.php @@ -23,9 +23,10 @@ class NoteTest extends \PHPUnit\Framework\TestCase protected function setUp() { + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $factoryMock = $this->createMock(\Magento\Framework\Data\Form\Element\Factory::class); $collectionFactoryMock = $this->createMock(\Magento\Framework\Data\Form\Element\CollectionFactory::class); - $escaperMock = $this->createMock(\Magento\Framework\Escaper::class); + $escaperMock = $objectManager->getObject(\Magento\Framework\Escaper::class); $this->_model = new \Magento\Framework\Data\Form\Element\Note( $factoryMock, $collectionFactoryMock, diff --git a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/TextareaTest.php b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/TextareaTest.php index e99df6c4c6e6..eec85ca35775 100644 --- a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/TextareaTest.php +++ b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/TextareaTest.php @@ -4,11 +4,11 @@ * See COPYING.txt for license details. */ -/** - * Tests for \Magento\Framework\Data\Form\Element\Textarea - */ namespace Magento\Framework\Data\Test\Unit\Form\Element; +/** + * Tests for \Magento\Framework\Data\Form\Element\Textarea class. + */ class TextareaTest extends \PHPUnit\Framework\TestCase { /** @@ -76,6 +76,7 @@ public function testGetHtmlAttributes() 'rows', 'cols', 'readonly', + 'maxlength', 'disabled', 'onkeyup', 'tabindex', diff --git a/lib/internal/Magento/Framework/Data/Wysiwyg/Normalizer.php b/lib/internal/Magento/Framework/Data/Wysiwyg/Normalizer.php index 62e7500a302a..bbf10e9f574c 100644 --- a/lib/internal/Magento/Framework/Data/Wysiwyg/Normalizer.php +++ b/lib/internal/Magento/Framework/Data/Wysiwyg/Normalizer.php @@ -10,12 +10,13 @@ */ class Normalizer { - const WYSIWYG_RESERVED_CHARACTERS_REPLACEMENT_MAP = [ '{' => '^[', '}' => '^]', '"' => '`', '\\' => '|', + '<' => '^(', + '>' => '^)' ]; /** diff --git a/lib/internal/Magento/Framework/DataObject.php b/lib/internal/Magento/Framework/DataObject.php index 9ff004c53bb9..6ecbca133e22 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 728f72e713d7..6a908ae78a34 100644 --- a/lib/internal/Magento/Framework/DataObject/Copy.php +++ b/lib/internal/Magento/Framework/DataObject/Copy.php @@ -4,11 +4,11 @@ * See COPYING.txt for license details. */ +namespace Magento\Framework\DataObject; + /** * Utility class for copying data sets between objects */ -namespace Magento\Framework\DataObject; - class Copy { /** @@ -116,14 +116,13 @@ protected function dispatchCopyFieldSetEvent($fieldset, $aspect, $source, $targe } /** - * Get data from object|array to object|array containing fields - * from fieldset matching an aspect. + * Get data from object|array to object|array containing fields from fieldset matching an aspect. * * @param string $fieldset * @param string $aspect a field name * @param array|\Magento\Framework\DataObject $source * @param string $root - * @return array $data + * @return array * * @api */ @@ -232,7 +231,7 @@ protected function _setFieldsetFieldValue($target, $targetCode, $value) /** * Access the extension get method * - * @param \Magento\Framework\Api\ExtensibleDataInterface $object + * @param \Magento\Framework\Api\ExtensibleDataInterface $source * @param string $code * * @return mixed @@ -240,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) { @@ -265,7 +264,7 @@ protected function getAttributeValueFromExtensibleDataObject($source, $code) /** * Access the extension set method * - * @param \Magento\Framework\Api\ExtensibleDataInterface $object + * @param \Magento\Framework\Api\ExtensibleDataInterface $target * @param string $code * @param mixed $value * @@ -274,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/DataObject/Copy/Config.php b/lib/internal/Magento/Framework/DataObject/Copy/Config.php index 2ad80321eb1f..fa3ab6e2f7ce 100644 --- a/lib/internal/Magento/Framework/DataObject/Copy/Config.php +++ b/lib/internal/Magento/Framework/DataObject/Copy/Config.php @@ -44,6 +44,6 @@ public function getFieldset($name, $root = 'global') if (empty($fieldsets)) { return null; } - return isset($fieldsets[$name]) ? $fieldsets[$name] : null; + return $fieldsets[$name] ?? null; } } diff --git a/lib/internal/Magento/Framework/DataObject/Copy/Config/Data/Proxy.php b/lib/internal/Magento/Framework/DataObject/Copy/Config/Data/Proxy.php index 880da5db771e..b0f5742afef1 100644 --- a/lib/internal/Magento/Framework/DataObject/Copy/Config/Data/Proxy.php +++ b/lib/internal/Magento/Framework/DataObject/Copy/Config/Data/Proxy.php @@ -57,10 +57,17 @@ public function __construct( } /** + * Remove links to other objects. + * * @return array + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + return ['_subject', '_isShared']; } @@ -68,9 +75,14 @@ public function __sleep() * Retrieve ObjectManager from global scope * * @return void + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + $this->_objectManager = \Magento\Framework\App\ObjectManager::getInstance(); } @@ -100,7 +112,7 @@ protected function _getSubject() } /** - * {@inheritdoc} + * @inheritdoc */ public function merge(array $config) { @@ -108,7 +120,7 @@ public function merge(array $config) } /** - * {@inheritdoc} + * @inheritdoc */ public function get($path = null, $default = null) { @@ -116,7 +128,7 @@ public function get($path = null, $default = null) } /** - * {@inheritdoc} + * @inheritdoc */ public function reset() { diff --git a/lib/internal/Magento/Framework/Encryption/Adapter/Mcrypt.php b/lib/internal/Magento/Framework/Encryption/Adapter/Mcrypt.php index 3660b28657b5..c2e82a6a7f6f 100644 --- a/lib/internal/Magento/Framework/Encryption/Adapter/Mcrypt.php +++ b/lib/internal/Magento/Framework/Encryption/Adapter/Mcrypt.php @@ -144,9 +144,11 @@ public function getHandle() */ public function encrypt(string $data): string { - throw new \Exception( - (string)new \Magento\Framework\Phrase('Mcrypt cannot be used for encryption. Use Sodium instead') - ); + if (strlen($data) == 0) { + return $data; + } + // @codingStandardsIgnoreLine + return @mcrypt_generic($this->getHandle(), $data); } /** diff --git a/lib/internal/Magento/Framework/Encryption/Adapter/SodiumChachaIetf.php b/lib/internal/Magento/Framework/Encryption/Adapter/SodiumChachaIetf.php index 9f9facf98ff8..0c56c2217669 100644 --- a/lib/internal/Magento/Framework/Encryption/Adapter/SodiumChachaIetf.php +++ b/lib/internal/Magento/Framework/Encryption/Adapter/SodiumChachaIetf.php @@ -33,6 +33,7 @@ public function __construct( * * @param string $data * @return string string + * @throws \SodiumException */ public function encrypt(string $data): string { @@ -58,13 +59,17 @@ public function decrypt(string $data): string $nonce = mb_substr($data, 0, SODIUM_CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES, '8bit'); $payload = mb_substr($data, SODIUM_CRYPTO_AEAD_CHACHA20POLY1305_IETF_NPUBBYTES, null, '8bit'); - $plainText = sodium_crypto_aead_chacha20poly1305_ietf_decrypt( - $payload, - $nonce, - $nonce, - $this->key - ); + try { + $plainText = sodium_crypto_aead_chacha20poly1305_ietf_decrypt( + $payload, + $nonce, + $nonce, + $this->key + ); + } catch (\SodiumException $e) { + $plainText = ''; + } - return $plainText; + return $plainText !== false ? $plainText : ''; } } diff --git a/lib/internal/Magento/Framework/Encryption/Encryptor.php b/lib/internal/Magento/Framework/Encryption/Encryptor.php index f7d52d5474ad..791e6d72b951 100644 --- a/lib/internal/Magento/Framework/Encryption/Encryptor.php +++ b/lib/internal/Magento/Framework/Encryption/Encryptor.php @@ -9,6 +9,7 @@ namespace Magento\Framework\Encryption; use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Encryption\Adapter\EncryptionAdapterInterface; use Magento\Framework\Encryption\Helper\Security; use Magento\Framework\Math\Random; @@ -115,20 +116,28 @@ class Encryptor implements EncryptorInterface */ private $random; + /** + * @var KeyValidator + */ + private $keyValidator; + /** * Encryptor constructor. * @param Random $random * @param DeploymentConfig $deploymentConfig + * @param KeyValidator|null $keyValidator */ public function __construct( Random $random, - DeploymentConfig $deploymentConfig + DeploymentConfig $deploymentConfig, + KeyValidator $keyValidator = null ) { $this->random = $random; // load all possible keys $this->keys = preg_split('/\s+/s', trim((string)$deploymentConfig->get(self::PARAM_CRYPT_KEY))); $this->keyVersion = count($this->keys) - 1; + $this->keyValidator = $keyValidator ?: ObjectManager::getInstance()->get(KeyValidator::class); } /** @@ -228,6 +237,8 @@ public function validateHashVersion($hash, $validateCount = false) } /** + * Explode password hash + * * @param string $hash * @return array */ @@ -243,6 +254,8 @@ private function explodePasswordHash($hash) } /** + * Get password hash + * * @return string */ private function getPasswordHash() @@ -251,6 +264,8 @@ private function getPasswordHash() } /** + * Get password salt + * * @return string */ private function getPasswordSalt() @@ -259,11 +274,19 @@ private function getPasswordSalt() } /** + * Get password version + * * @return array */ private function getPasswordVersion() { - return array_map('intval', explode(self::DELIMITER, $this->passwordHashMap[self::PASSWORD_VERSION])); + return array_map( + 'intval', + explode( + self::DELIMITER, + (string)$this->passwordHashMap[self::PASSWORD_VERSION] + ) + ); } /** @@ -281,6 +304,22 @@ public function encrypt($data) ':' . base64_encode($crypt->encrypt($data)); } + /** + * Encrypt data using the fastest available algorithm + * + * @param string $data + * @return string + */ + public function encryptWithFastestAvailableAlgorithm($data) + { + $crypt = $this->getCrypt(); + if (null === $crypt) { + return $data; + } + return $this->keyVersion . + ':' . $this->getCipherVersion() . + ':' . base64_encode($crypt->encrypt($data)); + } /** * Look for key and crypt versions in encrypted data before decrypting * @@ -344,8 +383,12 @@ public function decrypt($data) */ public function validateKey($key) { - if (preg_match('/\s/s', $key)) { - throw new \Exception((string)new \Magento\Framework\Phrase('The encryption key format is invalid.')); + if (!$this->keyValidator->isValid($key)) { + throw new \Exception( + (string)new \Magento\Framework\Phrase( + 'Encryption key must be 32 character string without any white space.' + ) + ); } } @@ -391,7 +434,7 @@ private function getCrypt( string $initVector = null ): ?EncryptionAdapterInterface { if (null === $key && null === $cipherVersion) { - $cipherVersion = self::CIPHER_RIJNDAEL_256; + $cipherVersion = $this->getCipherVersion(); } if (null === $key) { @@ -424,4 +467,18 @@ private function getCrypt( return new Mcrypt($key, $cipher, $mode, $initVector); } + + /** + * Get cipher version + * + * @return int + */ + private function getCipherVersion() + { + if (extension_loaded('sodium')) { + return $this->cipher; + } else { + return self::CIPHER_RIJNDAEL_256; + } + } } diff --git a/lib/internal/Magento/Framework/Encryption/KeyValidator.php b/lib/internal/Magento/Framework/Encryption/KeyValidator.php new file mode 100644 index 000000000000..79d592bec2a1 --- /dev/null +++ b/lib/internal/Magento/Framework/Encryption/KeyValidator.php @@ -0,0 +1,33 @@ +assertEquals($expectedData, $actualData); } - /** - * @expectedException \Exception - */ - public function testEncrypt() - { - $crypt = new \Magento\Framework\Encryption\Adapter\Mcrypt($this->key); - $crypt->encrypt('Hello World!!'); - } - /** * @dataProvider getCipherModeCombinations */ 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 8092ce55b42c..f90cd4eea5a8 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 7917bd9ba83e..8498f9a1a873 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 f3853a123c15..3feb4b412284 100644 --- a/lib/internal/Magento/Framework/Encryption/Test/Unit/EncryptorTest.php +++ b/lib/internal/Magento/Framework/Encryption/Test/Unit/EncryptorTest.php @@ -8,75 +8,92 @@ namespace Magento\Framework\Encryption\Test\Unit; -use Magento\Framework\Encryption\Adapter\Mcrypt; +use Magento\Framework\App\DeploymentConfig; use Magento\Framework\Encryption\Adapter\SodiumChachaIetf; -use Magento\Framework\Encryption\Encryptor; use Magento\Framework\Encryption\Crypt; +use Magento\Framework\Encryption\Encryptor; +use Magento\Framework\Math\Random; +use Magento\Framework\Encryption\KeyValidator; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; class EncryptorTest extends \PHPUnit\Framework\TestCase { - const CRYPT_KEY_1 = 'g9mY9KLrcuAVJfsmVUSRkKFLDdUPVkaZ'; - const CRYPT_KEY_2 = '7wEjmrliuqZQ1NQsndSa8C8WHvddeEbN'; + private const CRYPT_KEY_1 = 'g9mY9KLrcuAVJfsmVUSRkKFLDdUPVkaZ'; + private const CRYPT_KEY_2 = '7wEjmrliuqZQ1NQsndSa8C8WHvddeEbN'; + + /** + * @var Encryptor + */ + private $encryptor; /** - * @var \Magento\Framework\Encryption\Encryptor + * @var Random | \PHPUnit_Framework_MockObject_MockObject */ - protected $_model; + private $randomGeneratorMock; /** - * @var \PHPUnit_Framework_MockObject_MockObject + * @var KeyValidator | \PHPUnit_Framework_MockObject_MockObject */ - protected $_randomGenerator; + private $keyValidatorMock; protected function setUp() { - $this->_randomGenerator = $this->createMock(\Magento\Framework\Math\Random::class); - $deploymentConfigMock = $this->createMock(\Magento\Framework\App\DeploymentConfig::class); + $this->randomGeneratorMock = $this->createMock(Random::class); + /** @var DeploymentConfig | \PHPUnit_Framework_MockObject_MockObject $deploymentConfigMock */ + $deploymentConfigMock = $this->createMock(DeploymentConfig::class); $deploymentConfigMock->expects($this->any()) ->method('get') ->with(Encryptor::PARAM_CRYPT_KEY) - ->will($this->returnValue(self::CRYPT_KEY_1)); - $this->_model = new \Magento\Framework\Encryption\Encryptor($this->_randomGenerator, $deploymentConfigMock); + ->willReturn(self::CRYPT_KEY_1); + $this->keyValidatorMock = $this->createMock(KeyValidator::class); + $this->encryptor = (new ObjectManager($this))->getObject( + Encryptor::class, + [ + 'random' => $this->randomGeneratorMock, + 'deploymentConfig' => $deploymentConfigMock, + 'keyValidator' => $this->keyValidatorMock + ] + ); } - public function testGetHashNoSalt() + public function testGetHashNoSalt(): void { - $this->_randomGenerator->expects($this->never())->method('getRandomString'); + $this->randomGeneratorMock->expects($this->never())->method('getRandomString'); $expected = '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8'; - $actual = $this->_model->getHash('password'); + $actual = $this->encryptor->getHash('password'); $this->assertEquals($expected, $actual); } - public function testGetHashSpecifiedSalt() + public function testGetHashSpecifiedSalt(): void { - $this->_randomGenerator->expects($this->never())->method('getRandomString'); + $this->randomGeneratorMock->expects($this->never())->method('getRandomString'); $expected = '13601bda4ea78e55a07b98866d2be6be0744e3866f13c00c811cab608a28f322:salt:1'; - $actual = $this->_model->getHash('password', 'salt'); + $actual = $this->encryptor->getHash('password', 'salt'); $this->assertEquals($expected, $actual); } - public function testGetHashRandomSaltDefaultLength() + public function testGetHashRandomSaltDefaultLength(): void { $salt = '-----------random_salt----------'; - $this->_randomGenerator + $this->randomGeneratorMock ->expects($this->once()) ->method('getRandomString') ->with(32) - ->will($this->returnValue($salt)); + ->willReturn($salt); $expected = 'a1c7fc88037b70c9be84d3ad12522c7888f647915db78f42eb572008422ba2fa:' . $salt . ':1'; - $actual = $this->_model->getHash('password', true); + $actual = $this->encryptor->getHash('password', true); $this->assertEquals($expected, $actual); } - public function testGetHashRandomSaltSpecifiedLength() + public function testGetHashRandomSaltSpecifiedLength(): void { - $this->_randomGenerator + $this->randomGeneratorMock ->expects($this->once()) ->method('getRandomString') ->with(11) - ->will($this->returnValue('random_salt')); + ->willReturn('random_salt'); $expected = '4c5cab8dd00137d11258f8f87b93fd17bd94c5026fc52d3c5af911dd177a2611:random_salt:1'; - $actual = $this->_model->getHash('password', 11); + $actual = $this->encryptor->getHash('password', 11); $this->assertEquals($expected, $actual); } @@ -87,16 +104,16 @@ public function testGetHashRandomSaltSpecifiedLength() * * @dataProvider validateHashDataProvider */ - public function testValidateHash($password, $hash, $expected) + public function testValidateHash($password, $hash, $expected): void { - $actual = $this->_model->validateHash($password, $hash); + $actual = $this->encryptor->validateHash($password, $hash); $this->assertEquals($expected, $actual); } /** * @return array */ - public function validateHashDataProvider() + public function validateHashDataProvider(): array { return [ ['password', 'hash:salt:1', false], @@ -111,14 +128,14 @@ public function validateHashDataProvider() * @dataProvider encryptWithEmptyKeyDataProvider * @expectedException \SodiumException */ - public function testEncryptWithEmptyKey($key) + public function testEncryptWithEmptyKey($key): void { - $deploymentConfigMock = $this->createMock(\Magento\Framework\App\DeploymentConfig::class); + $deploymentConfigMock = $this->createMock(DeploymentConfig::class); $deploymentConfigMock->expects($this->any()) ->method('get') ->with(Encryptor::PARAM_CRYPT_KEY) - ->will($this->returnValue($key)); - $model = new Encryptor($this->_randomGenerator, $deploymentConfigMock); + ->willReturn($key); + $model = new Encryptor($this->randomGeneratorMock, $deploymentConfigMock); $value = 'arbitrary_string'; $this->assertEquals($value, $model->encrypt($value)); } @@ -126,7 +143,7 @@ public function testEncryptWithEmptyKey($key) /** * @return array */ - public function encryptWithEmptyKeyDataProvider() + public function encryptWithEmptyKeyDataProvider(): array { return [[null], [0], [''], ['0']]; } @@ -136,14 +153,14 @@ public function encryptWithEmptyKeyDataProvider() * * @dataProvider decryptWithEmptyKeyDataProvider */ - public function testDecryptWithEmptyKey($key) + public function testDecryptWithEmptyKey($key): void { - $deploymentConfigMock = $this->createMock(\Magento\Framework\App\DeploymentConfig::class); + $deploymentConfigMock = $this->createMock(DeploymentConfig::class); $deploymentConfigMock->expects($this->any()) ->method('get') ->with(Encryptor::PARAM_CRYPT_KEY) - ->will($this->returnValue($key)); - $model = new Encryptor($this->_randomGenerator, $deploymentConfigMock); + ->willReturn($key); + $model = new Encryptor($this->randomGeneratorMock, $deploymentConfigMock); $value = 'arbitrary_string'; $this->assertEquals('', $model->decrypt($value)); } @@ -151,46 +168,44 @@ public function testDecryptWithEmptyKey($key) /** * @return array */ - public function decryptWithEmptyKeyDataProvider() + public function decryptWithEmptyKeyDataProvider(): array { return [[null], [0], [''], ['0']]; } - public function testEncrypt() + public function testEncrypt(): void { // sample data to encrypt $data = 'Mares eat oats and does eat oats, but little lambs eat ivy.'; - $actual = $this->_model->encrypt($data); + $actual = $this->encryptor->encrypt($data); // Extract the initialization vector and encrypted data - $parts = explode(':', $actual, 3); - list(, , $encryptedData) = $parts; + [, , $encryptedData] = explode(':', $actual, 3); $crypt = new SodiumChachaIetf(self::CRYPT_KEY_1); // Verify decrypted matches original data $this->assertEquals($data, $crypt->decrypt(base64_decode((string)$encryptedData))); } - public function testDecrypt() + public function testDecrypt(): void { $message = 'Mares eat oats and does eat oats, but little lambs eat ivy.'; - $encrypted = $this->_model->encrypt($message); + $encrypted = $this->encryptor->encrypt($message); - $this->assertEquals($message, $this->_model->decrypt($encrypted)); + $this->assertEquals($message, $this->encryptor->decrypt($encrypted)); } - public function testLegacyDecrypt() + public function testLegacyDecrypt(): void { // sample data to encrypt $data = '0:2:z3a4ACpkU35W6pV692U4ueCVQP0m0v0p:' . 'DhEG8/uKGGq92ZusqrGb6X/9+2Ng0QZ9z2UZwljgJbs5/A3LaSnqcK0oI32yjHY49QJi+Z7q1EKu2yVqB8EMpA=='; - $actual = $this->_model->decrypt($data); + $actual = $this->encryptor->decrypt($data); // Extract the initialization vector and encrypted data - $parts = explode(':', $data, 4); - list(, , $iv, $encrypted) = $parts; + [, , $iv, $encrypted] = explode(':', $data, 4); // Decrypt returned data with RIJNDAEL_256 cipher, cbc mode $crypt = new Crypt(self::CRYPT_KEY_1, MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC, $iv); @@ -198,20 +213,20 @@ public function testLegacyDecrypt() $this->assertEquals($encrypted, base64_encode($crypt->encrypt($actual))); } - public function testEncryptDecryptNewKeyAdded() + public function testEncryptDecryptNewKeyAdded(): void { - $deploymentConfigMock = $this->createMock(\Magento\Framework\App\DeploymentConfig::class); + $deploymentConfigMock = $this->createMock(DeploymentConfig::class); $deploymentConfigMock->expects($this->at(0)) ->method('get') ->with(Encryptor::PARAM_CRYPT_KEY) - ->will($this->returnValue(self::CRYPT_KEY_1)); + ->willReturn(self::CRYPT_KEY_1); $deploymentConfigMock->expects($this->at(1)) ->method('get') ->with(Encryptor::PARAM_CRYPT_KEY) - ->will($this->returnValue(self::CRYPT_KEY_1 . "\n" . self::CRYPT_KEY_2)); - $model1 = new Encryptor($this->_randomGenerator, $deploymentConfigMock); + ->willReturn(self::CRYPT_KEY_1 . "\n" . self::CRYPT_KEY_2); + $model1 = new Encryptor($this->randomGeneratorMock, $deploymentConfigMock); // simulate an encryption key is being added - $model2 = new Encryptor($this->_randomGenerator, $deploymentConfigMock); + $model2 = new Encryptor($this->randomGeneratorMock, $deploymentConfigMock); // sample data to encrypt $data = 'Mares eat oats and does eat oats, but little lambs eat ivy.'; @@ -222,23 +237,25 @@ public function testEncryptDecryptNewKeyAdded() $this->assertSame($data, $decryptedData, 'Encryptor failed to decrypt data encrypted by old keys.'); } - public function testValidateKey() + public function testValidateKey(): void { - $this->_model->validateKey(self::CRYPT_KEY_1); + $this->keyValidatorMock->method('isValid')->willReturn(true); + $this->encryptor->validateKey(self::CRYPT_KEY_1); } /** * @expectedException \Exception */ - public function testValidateKeyInvalid() + public function testValidateKeyInvalid(): void { - $this->_model->validateKey('----- '); + $this->keyValidatorMock->method('isValid')->willReturn(false); + $this->encryptor->validateKey('----- '); } /** * @return array */ - public function testUseSpecifiedHashingAlgoDataProvider() + public function useSpecifiedHashingAlgoDataProvider(): array { return [ ['password', 'salt', Encryptor::HASH_VERSION_MD5, @@ -253,16 +270,16 @@ public function testUseSpecifiedHashingAlgoDataProvider() } /** - * @dataProvider testUseSpecifiedHashingAlgoDataProvider + * @dataProvider useSpecifiedHashingAlgoDataProvider * * @param $password * @param $salt * @param $hashAlgo * @param $expected */ - public function testGetHashMustUseSpecifiedHashingAlgo($password, $salt, $hashAlgo, $expected) + public function testGetHashMustUseSpecifiedHashingAlgo($password, $salt, $hashAlgo, $expected): void { - $hash = $this->_model->getHash($password, $salt, $hashAlgo); + $hash = $this->encryptor->getHash($password, $salt, $hashAlgo); $this->assertEquals($expected, $hash); } } diff --git a/lib/internal/Magento/Framework/Encryption/Test/Unit/KeyValidatorTest.php b/lib/internal/Magento/Framework/Encryption/Test/Unit/KeyValidatorTest.php new file mode 100644 index 000000000000..85faa0aa4676 --- /dev/null +++ b/lib/internal/Magento/Framework/Encryption/Test/Unit/KeyValidatorTest.php @@ -0,0 +1,54 @@ +keyValidator = (new ObjectManager($this))->getObject(KeyValidator::class); + } + + /** + * @param $key + * @param bool $expected + * @dataProvider isValidDataProvider + */ + public function testIsValid($key, $expected = true) + { + $this->assertEquals($expected, $this->keyValidator->isValid($key)); + } + + public function isValidDataProvider() : array + { + return [ + '32 numbers' => ['12345678901234567890123456789012'], + '32 characters' => ['aBcdeFghIJKLMNOPQRSTUvwxYzabcdef'], + '32 special characters' => ['!@#$%^&*()_+~`:;"<>,.?/|*&^%$#@!'], + '32 combination' =>['1234eFghI1234567^&*(890123456789'], + 'empty string' => ['', false], + 'leading space' => [' 1234567890123456789012345678901', false], + 'tailing space' => ['1234567890123456789012345678901 ', false], + 'space in the middle' => ['12345678901 23456789012345678901', false], + 'tab in the middle' => ['12345678901 23456789012345678', false], + 'return in the middle' => ['12345678901 + 23456789012345678901', false], + '31 characters' => ['1234567890123456789012345678901', false], + '33 characters' => ['123456789012345678901234567890123', false], + ]; + } +} diff --git a/lib/internal/Magento/Framework/EntityManager/Test/Unit/TypeResolverTest.php b/lib/internal/Magento/Framework/EntityManager/Test/Unit/TypeResolverTest.php index 1ab6074d6a36..9b90e373de85 100644 --- a/lib/internal/Magento/Framework/EntityManager/Test/Unit/TypeResolverTest.php +++ b/lib/internal/Magento/Framework/EntityManager/Test/Unit/TypeResolverTest.php @@ -32,7 +32,7 @@ public function setUp() /** * @param object $dataObject - * @param string $interfaceNames + * @param string $interfaceName * @dataProvider resolveDataProvider */ public function testResolve($dataObject, $interfaceName) diff --git a/lib/internal/Magento/Framework/Escaper.php b/lib/internal/Magento/Framework/Escaper.php index fec64378189e..c4150851ec40 100644 --- a/lib/internal/Magento/Framework/Escaper.php +++ b/lib/internal/Magento/Framework/Escaper.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Framework; /** @@ -32,13 +33,23 @@ class Escaper */ private $allowedAttributes = ['id', 'class', 'href', 'target', 'title', 'style']; + /** + * @var string + */ + private static $xssFiltrationPattern = + '/((javascript(\\\\x3a|:|%3A))|(data(\\\\x3a|:|%3A))|(vbscript:))|' + . '((\\\\x6A\\\\x61\\\\x76\\\\x61\\\\x73\\\\x63\\\\x72\\\\x69\\\\x70\\\\x74(\\\\x3a|:|%3A))|' + . '(\\\\x64\\\\x61\\\\x74\\\\x61(\\\\x3a|:|%3A)))/i'; + /** * @var string[] */ private $escapeAsUrlAttributes = ['href']; /** - * Escape string for HTML context. allowedTags will not be escaped, except the following: script, img, embed, + * Escape string for HTML context. + * + * AllowedTags will not be escaped, except the following: script, img, embed, * iframe, video, source, object, audio * * @param string|array $data @@ -47,6 +58,10 @@ class Escaper */ public function escapeHtml($data, $allowedTags = null) { + if (!is_array($data)) { + $data = (string)$data; + } + if (is_array($data)) { $result = []; foreach ($data as $item) { @@ -54,16 +69,7 @@ public function escapeHtml($data, $allowedTags = null) } } elseif (strlen($data)) { if (is_array($allowedTags) && !empty($allowedTags)) { - $notAllowedTags = array_intersect( - array_map('strtolower', $allowedTags), - $this->notAllowedTags - ); - if (!empty($notAllowedTags)) { - $this->getLogger()->critical( - 'The following tag(s) are not allowed: ' . implode(', ', $notAllowedTags) - ); - $allowedTags = array_diff($allowedTags, $this->notAllowedTags); - } + $allowedTags = $this->filterProhibitedTags($allowedTags); $wrapperElementId = uniqid(); $domDocument = new \DOMDocument('1.0', 'UTF-8'); set_error_handler( @@ -71,6 +77,7 @@ function ($errorNumber, $errorString) { throw new \Exception($errorString, $errorNumber); } ); + $data = $this->prepareUnescapedCharacters($data); $string = mb_convert_encoding($data, 'HTML-ENTITIES', 'UTF-8'); try { $domDocument->loadHTML( @@ -99,6 +106,19 @@ function ($errorNumber, $errorString) { return $result; } + /** + * Used to replace characters, that mb_convert_encoding will not process + * + * @param string $data + * @return string|null + */ + private function prepareUnescapedCharacters(string $data): ?string + { + $patterns = ['/\&/u']; + $replacements = ['&']; + return \preg_replace($patterns, $replacements, $data); + } + /** * Remove not allowed tags * @@ -197,7 +217,7 @@ public function escapeHtmlAttr($string, $escapeSingleQuote = true) if ($escapeSingleQuote) { return $this->getEscaper()->escapeHtmlAttr((string) $string); } - return htmlspecialchars($string, ENT_COMPAT, 'UTF-8', false); + return htmlspecialchars((string)$string, ENT_COMPAT, 'UTF-8', false); } /** @@ -278,14 +298,13 @@ public function escapeJsQuote($data, $quote = '\'') $result[] = $this->escapeJsQuote($item, $quote); } } else { - $result = str_replace($quote, '\\' . $quote, $data); + $result = str_replace($quote, '\\' . $quote, (string)$data); } return $result; } /** * Escape xss in urls - * Remove `javascript:`, `vbscript:`, `data:` words from url * * @param string $data * @return string @@ -293,15 +312,33 @@ public function escapeJsQuote($data, $quote = '\'') */ public function escapeXssInUrl($data) { - $pattern = '/((javascript(\\\\x3a|:|%3A))|(data(\\\\x3a|:|%3A))|(vbscript:))|' - . '((\\\\x6A\\\\x61\\\\x76\\\\x61\\\\x73\\\\x63\\\\x72\\\\x69\\\\x70\\\\x74(\\\\x3a|:|%3A))|' - . '(\\\\x64\\\\x61\\\\x74\\\\x61(\\\\x3a|:|%3A)))/i'; - $result = preg_replace($pattern, ':', $data); - return htmlspecialchars($result, ENT_COMPAT | ENT_HTML5 | ENT_HTML401, 'UTF-8', false); + return htmlspecialchars( + $this->escapeScriptIdentifiers((string)$data), + ENT_COMPAT | ENT_HTML5 | ENT_HTML401, + 'UTF-8', + false + ); + } + + /** + * Remove `javascript:`, `vbscript:`, `data:` words from the string. + * + * @param string $data + * @return string + */ + private function escapeScriptIdentifiers(string $data): string + { + $filteredData = preg_replace(self::$xssFiltrationPattern, ':', $data) ?: ''; + if (preg_match(self::$xssFiltrationPattern, $filteredData)) { + $filteredData = $this->escapeScriptIdentifiers($filteredData); + } + + return $filteredData; } /** * Escape quotes inside html attributes + * * Use $addSlashes = false for escaping js that inside html attribute (onClick, onSubmit etc) * * @param string $data @@ -346,4 +383,27 @@ private function getLogger() } return $this->logger; } + + /** + * Filter prohibited tags. + * + * @param string[] $allowedTags + * @return string[] + */ + private function filterProhibitedTags(array $allowedTags): array + { + $notAllowedTags = array_intersect( + array_map('strtolower', $allowedTags), + $this->notAllowedTags + ); + + if (!empty($notAllowedTags)) { + $this->getLogger()->critical( + 'The following tag(s) are not allowed: ' . implode(', ', $notAllowedTags) + ); + $allowedTags = array_diff($allowedTags, $this->notAllowedTags); + } + + return $allowedTags; + } } diff --git a/lib/internal/Magento/Framework/Event/Invoker/InvokerDefault.php b/lib/internal/Magento/Framework/Event/Invoker/InvokerDefault.php index a7a387b5def8..acd0a6163355 100644 --- a/lib/internal/Magento/Framework/Event/Invoker/InvokerDefault.php +++ b/lib/internal/Magento/Framework/Event/Invoker/InvokerDefault.php @@ -9,7 +9,12 @@ namespace Magento\Framework\Event\Invoker; use Magento\Framework\Event\Observer; +use Psr\Log\LoggerInterface; +use Magento\Framework\App\State; +/** + * Default Invoker. + */ class InvokerDefault implements \Magento\Framework\Event\InvokerInterface { /** @@ -22,20 +27,29 @@ class InvokerDefault implements \Magento\Framework\Event\InvokerInterface /** * Application state * - * @var \Magento\Framework\App\State + * @var State */ protected $_appState; + /** + * @var LoggerInterface + */ + private $logger; + /** * @param \Magento\Framework\Event\ObserverFactory $observerFactory - * @param \Magento\Framework\App\State $appState + * @param State $appState + * @param LoggerInterface $logger */ public function __construct( \Magento\Framework\Event\ObserverFactory $observerFactory, - \Magento\Framework\App\State $appState + State $appState, + LoggerInterface $logger = null ) { $this->_observerFactory = $observerFactory; $this->_appState = $appState; + $this->logger = $logger ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(LoggerInterface::class); } /** @@ -61,6 +75,8 @@ public function dispatch(array $configuration, Observer $observer) } /** + * Execute Observer. + * * @param \Magento\Framework\Event\ObserverInterface $object * @param Observer $observer * @return $this @@ -70,7 +86,7 @@ protected function _callObserverMethod($object, $observer) { if ($object instanceof \Magento\Framework\Event\ObserverInterface) { $object->execute($observer); - } elseif ($this->_appState->getMode() == \Magento\Framework\App\State::MODE_DEVELOPER) { + } elseif ($this->_appState->getMode() == State::MODE_DEVELOPER) { throw new \LogicException( sprintf( 'Observer "%s" must implement interface "%s"', @@ -78,6 +94,12 @@ protected function _callObserverMethod($object, $observer) \Magento\Framework\Event\ObserverInterface::class ) ); + } else { + $this->logger->warning(sprintf( + 'Observer "%s" must implement interface "%s"', + get_class($object), + \Magento\Framework\Event\ObserverInterface::class + )); } return $this; } diff --git a/lib/internal/Magento/Framework/Event/Test/Unit/Config/SchemaLocatorTest.php b/lib/internal/Magento/Framework/Event/Test/Unit/Config/SchemaLocatorTest.php index cd8a16b5c2c4..66651174a87c 100644 --- a/lib/internal/Magento/Framework/Event/Test/Unit/Config/SchemaLocatorTest.php +++ b/lib/internal/Magento/Framework/Event/Test/Unit/Config/SchemaLocatorTest.php @@ -12,10 +12,10 @@ class SchemaLocatorTest extends \PHPUnit\Framework\TestCase */ protected $model; - /** @var \Magento\Framework\Config\Dom\UrnResolver $urnResolverMock */ + /** @var \Magento\Framework\Config\Dom\UrnResolver */ protected $urnResolver; - /** @var \Magento\Framework\Config\Dom\UrnResolver $urnResolverMock */ + /** @var \Magento\Framework\Config\Dom\UrnResolver */ protected $urnResolverMock; protected function setUp() diff --git a/lib/internal/Magento/Framework/Event/Test/Unit/Config/_files/invalidEventsXmlArray.php b/lib/internal/Magento/Framework/Event/Test/Unit/Config/_files/invalidEventsXmlArray.php index 33007b7295bc..e0dc7494cca1 100644 --- a/lib/internal/Magento/Framework/Event/Test/Unit/Config/_files/invalidEventsXmlArray.php +++ b/lib/internal/Magento/Framework/Event/Test/Unit/Config/_files/invalidEventsXmlArray.php @@ -4,10 +4,6 @@ * See COPYING.txt for license details. */ return [ - 'without_event_handle' => [ - '', - ["Element 'config': Missing child element(s). Expected is ( event ).\nLine: 1\n"], - ], 'event_without_required_name_attribute' => [ '', ["Element 'event': Missing child element(s). Expected is ( observer ).\nLine: 1\n"], diff --git a/lib/internal/Magento/Framework/Event/Test/Unit/Invoker/InvokerDefaultTest.php b/lib/internal/Magento/Framework/Event/Test/Unit/Invoker/InvokerDefaultTest.php index 37f650dbef6a..e6ec12382385 100644 --- a/lib/internal/Magento/Framework/Event/Test/Unit/Invoker/InvokerDefaultTest.php +++ b/lib/internal/Magento/Framework/Event/Test/Unit/Invoker/InvokerDefaultTest.php @@ -5,6 +5,9 @@ */ namespace Magento\Framework\Event\Test\Unit\Invoker; +/** + * Test for Magento\Framework\Event\Invoker\InvokerDefault. + */ class InvokerDefaultTest extends \PHPUnit\Framework\TestCase { /** @@ -32,6 +35,11 @@ class InvokerDefaultTest extends \PHPUnit\Framework\TestCase */ protected $_invokerDefault; + /** + * @var |Psr\Log|LoggerInterface + */ + private $loggerMock; + protected function setUp() { $this->_observerFactoryMock = $this->createMock(\Magento\Framework\Event\ObserverFactory::class); @@ -41,10 +49,12 @@ protected function setUp() ['execute'] ); $this->_appStateMock = $this->createMock(\Magento\Framework\App\State::class); + $this->loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class); $this->_invokerDefault = new \Magento\Framework\Event\Invoker\InvokerDefault( $this->_observerFactoryMock, - $this->_appStateMock + $this->_appStateMock, + $this->loggerMock ); } @@ -166,13 +176,15 @@ public function testWrongInterfaceCallWithDisabledDeveloperMode($shared) $this->returnValue($notObserver) ); $this->_appStateMock->expects( - $this->once() + $this->exactly(1) )->method( 'getMode' )->will( $this->returnValue(\Magento\Framework\App\State::MODE_PRODUCTION) ); + $this->loggerMock->expects($this->once())->method('warning'); + $this->_invokerDefault->dispatch( [ 'shared' => $shared, diff --git a/lib/internal/Magento/Framework/Event/etc/events.xsd b/lib/internal/Magento/Framework/Event/etc/events.xsd index d656b7fdb6ed..cac62af35676 100644 --- a/lib/internal/Magento/Framework/Event/etc/events.xsd +++ b/lib/internal/Magento/Framework/Event/etc/events.xsd @@ -9,7 +9,7 @@ - + diff --git a/lib/internal/Magento/Framework/Exception/BulkException.php b/lib/internal/Magento/Framework/Exception/BulkException.php index b2e91da211fe..168e910e0d37 100644 --- a/lib/internal/Magento/Framework/Exception/BulkException.php +++ b/lib/internal/Magento/Framework/Exception/BulkException.php @@ -39,7 +39,9 @@ public function __construct(Phrase $phrase = null, \Exception $cause = null, $co } /** - * @param $data array + * Add data + * + * @param array $data */ public function addData($data) { @@ -47,6 +49,8 @@ public function addData($data) } /** + * Retrieve data + * * @return array */ public function getData() diff --git a/lib/internal/Magento/Framework/Exception/LocalizedException.php b/lib/internal/Magento/Framework/Exception/LocalizedException.php index 0b1d5f1998bf..977c69db77bb 100644 --- a/lib/internal/Magento/Framework/Exception/LocalizedException.php +++ b/lib/internal/Magento/Framework/Exception/LocalizedException.php @@ -11,6 +11,8 @@ use Magento\Framework\Phrase\Renderer\Placeholder; /** + * Localized exception + * * @api */ class LocalizedException extends \Exception @@ -33,7 +35,7 @@ class LocalizedException extends \Exception public function __construct(Phrase $phrase, \Exception $cause = null, $code = 0) { $this->phrase = $phrase; - parent::__construct($phrase->render(), intval($code), $cause); + parent::__construct($phrase->render(), (int)$code, $cause); } /** diff --git a/lib/internal/Magento/Framework/File/Mime.php b/lib/internal/Magento/Framework/File/Mime.php index d42b536f450c..ed370b1beae5 100644 --- a/lib/internal/Magento/Framework/File/Mime.php +++ b/lib/internal/Magento/Framework/File/Mime.php @@ -6,6 +6,9 @@ namespace Magento\Framework\File; +/** + * Utility for mime type retrieval + */ class Mime { /** @@ -61,7 +64,7 @@ class Mime /** * List of mime types that can be defined by file extension. * - * @var array $defineByExtensionList + * @var array */ private $defineByExtensionList = [ 'txt' => 'text/plain', @@ -106,6 +109,7 @@ public function getMimeType($file) /** * Get mime type by the native mime_content_type function. + * * Search for extended mime type if mime_content_type() returned 'application/octet-stream' or 'text/plain' * * @param string $file diff --git a/lib/internal/Magento/Framework/File/Test/Unit/Transfer/Adapter/HttpTest.php b/lib/internal/Magento/Framework/File/Test/Unit/Transfer/Adapter/HttpTest.php index d945791282a2..023c4cc4ddba 100644 --- a/lib/internal/Magento/Framework/File/Test/Unit/Transfer/Adapter/HttpTest.php +++ b/lib/internal/Magento/Framework/File/Test/Unit/Transfer/Adapter/HttpTest.php @@ -3,24 +3,37 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Framework\File\Test\Unit\Transfer\Adapter; -use \Magento\Framework\File\Transfer\Adapter\Http; +use Magento\Framework\File\Transfer\Adapter\Http; +use Magento\Framework\File\Mime; +use Magento\Framework\HTTP\PhpEnvironment\Response; +use Magento\Framework\App\Request\Http as RequestHttp; +use PHPUnit\Framework\MockObject\MockObject; +/** + * Tests http transfer adapter. + */ class HttpTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Framework\HTTP\PhpEnvironment\Response|\PHPUnit_Framework_MockObject_MockObject + * @var RequestHttp|MockObject + */ + private $request; + + /** + * @var Response|MockObject */ private $response; /** - * @var Http|\PHPUnit_Framework_MockObject_MockObject + * @var Http|MockObject */ private $object; /** - * @var \Magento\Framework\File\Mime|\PHPUnit_Framework_MockObject_MockObject + * @var Mime|MockObject */ private $mime; @@ -30,11 +43,15 @@ class HttpTest extends \PHPUnit\Framework\TestCase protected function setUp() { $this->response = $this->createPartialMock( - \Magento\Framework\HTTP\PhpEnvironment\Response::class, + Response::class, ['setHeader', 'sendHeaders', 'setHeaders'] ); - $this->mime = $this->createMock(\Magento\Framework\File\Mime::class); - $this->object = new Http($this->response, $this->mime); + $this->mime = $this->createMock(Mime::class); + $this->request = $this->createPartialMock( + RequestHttp::class, + ['isHead'] + ); + $this->object = new Http($this->response, $this->mime, $this->request); } /** @@ -56,7 +73,10 @@ public function testSend(): void $this->mime->expects($this->once()) ->method('getMimeType') ->with($file) - ->will($this->returnValue($contentType)); + ->willReturn($contentType); + $this->request->expects($this->once()) + ->method('isHead') + ->willReturn(false); $this->expectOutputString(file_get_contents($file)); $this->object->send($file); @@ -82,7 +102,10 @@ public function testSendWithOptions(): void $this->mime->expects($this->once()) ->method('getMimeType') ->with($file) - ->will($this->returnValue($contentType)); + ->willReturn($contentType); + $this->request->expects($this->once()) + ->method('isHead') + ->willReturn(false); $this->expectOutputString(file_get_contents($file)); $this->object->send(['filepath' => $file, 'headers' => $headers]); @@ -106,4 +129,32 @@ public function testSendNoFileExistException(): void { $this->object->send('nonexistent.file'); } + + /** + * @return void + */ + public function testSendHeadRequest(): void + { + $file = __DIR__ . '/../../_files/javascript.js'; + $contentType = 'content/type'; + + $this->response->expects($this->at(0)) + ->method('setHeader') + ->with('Content-length', filesize($file)); + $this->response->expects($this->at(1)) + ->method('setHeader') + ->with('Content-Type', $contentType); + $this->response->expects($this->once()) + ->method('sendHeaders'); + $this->mime->expects($this->once()) + ->method('getMimeType') + ->with($file) + ->willReturn($contentType); + $this->request->expects($this->once()) + ->method('isHead') + ->willReturn(true); + + $this->object->send($file); + $this->assertEquals(false, $this->hasOutput()); + } } diff --git a/lib/internal/Magento/Framework/File/Transfer/Adapter/Http.php b/lib/internal/Magento/Framework/File/Transfer/Adapter/Http.php index aa527866eff5..cd42c8d04b47 100644 --- a/lib/internal/Magento/Framework/File/Transfer/Adapter/Http.php +++ b/lib/internal/Magento/Framework/File/Transfer/Adapter/Http.php @@ -6,28 +6,46 @@ namespace Magento\Framework\File\Transfer\Adapter; +use Magento\Framework\HTTP\PhpEnvironment\Response; +use Magento\Framework\File\Mime; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\App\ObjectManager; +use Zend\Http\Headers; + +/** + * File adapter to send the file to the client. + */ class Http { /** - * @var \Magento\Framework\HTTP\PhpEnvironment\Response + * @var Response */ private $response; /** - * @var \Magento\Framework\File\Mime + * @var Mime */ private $mime; /** - * @param \Magento\Framework\App\Response\Http $response - * @param \Magento\Framework\File\Mime $mime + * @var HttpRequest + */ + private $request; + + /** + * @param Response $response + * @param Mime $mime + * @param HttpRequest|null $request */ public function __construct( - \Magento\Framework\HTTP\PhpEnvironment\Response $response, - \Magento\Framework\File\Mime $mime + Response $response, + Mime $mime, + HttpRequest $request = null ) { $this->response = $response; $this->mime = $mime; + $objectManager = ObjectManager::getInstance(); + $this->request = $request ?: $objectManager->get(HttpRequest::class); } /** @@ -46,18 +64,17 @@ public function send($options = null) throw new \InvalidArgumentException("File '{$filepath}' does not exists."); } - $mimeType = $this->mime->getMimeType($filepath); - if (is_array($options) && isset($options['headers']) && $options['headers'] instanceof \Zend\Http\Headers) { - $this->response->setHeaders($options['headers']); - } - $this->response->setHeader('Content-length', filesize($filepath)); - $this->response->setHeader('Content-Type', $mimeType); + $this->prepareResponse($options, $filepath); - $this->response->sendHeaders(); + if ($this->request->isHead()) { + // Do not send the body on HEAD requests. + return; + } $handle = fopen($filepath, 'r'); if ($handle) { while (($buffer = fgets($handle, 4096)) !== false) { + // phpcs:ignore Magento2.Security.LanguageConstruct.DirectOutput echo $buffer; } if (!feof($handle)) { @@ -88,4 +105,22 @@ private function getFilePath($options): string return $filePath; } + + /** + * Set and send all necessary headers. + * + * @param array $options + * @param string $filepath + */ + private function prepareResponse($options, string $filepath): void + { + $mimeType = $this->mime->getMimeType($filepath); + if (is_array($options) && isset($options['headers']) && $options['headers'] instanceof Headers) { + $this->response->setHeaders($options['headers']); + } + $this->response->setHeader('Content-length', filesize($filepath)); + $this->response->setHeader('Content-Type', $mimeType); + + $this->response->sendHeaders(); + } } diff --git a/lib/internal/Magento/Framework/File/Uploader.php b/lib/internal/Magento/Framework/File/Uploader.php index 33f458d2082e..328a748cfd5d 100644 --- a/lib/internal/Magento/Framework/File/Uploader.php +++ b/lib/internal/Magento/Framework/File/Uploader.php @@ -136,12 +136,16 @@ class Uploader const TMP_NAME_EMPTY = 666; /** - * Max Image Width resolution in pixels. For image resizing on client side + * Maximum Image Width resolution in pixels. For image resizing on client side + * @deprecated + * @see \Magento\Framework\Image\Adapter\UploadConfigInterface::getMaxWidth() */ const MAX_IMAGE_WIDTH = 1920; /** - * Max Image Height resolution in pixels. For image resizing on client side + * Maximum Image Height resolution in pixels. For image resizing on client side + * @deprecated + * @see \Magento\Framework\Image\Adapter\UploadConfigInterface::getMaxHeight() */ const MAX_IMAGE_HEIGHT = 1200; @@ -187,8 +191,7 @@ protected function _afterSave($result) } /** - * Used to save uploaded file into destination folder with - * original or new file name (if specified) + * Used to save uploaded file into destination folder with original or new file name (if specified). * * @param string $destinationFolder * @param string $newFileName @@ -204,20 +207,22 @@ public function save($destinationFolder, $newFileName = null) $this->_result = false; $destinationFile = $destinationFolder; $fileName = isset($newFileName) ? $newFileName : $this->_file['name']; - $fileName = self::getCorrectFileName($fileName); + $fileName = static::getCorrectFileName($fileName); if ($this->_enableFilesDispersion) { $fileName = $this->correctFileNameCase($fileName); $this->setAllowCreateFolders(true); - $this->_dispretionPath = self::getDispersionPath($fileName); + $this->_dispretionPath = static::getDispersionPath($fileName); $destinationFile .= $this->_dispretionPath; $this->_createDestinationFolder($destinationFile); } if ($this->_allowRenameFiles) { - $fileName = self::getNewFileName(self::_addDirSeparator($destinationFile) . $fileName); + $fileName = static::getNewFileName( + static::_addDirSeparator($destinationFile) . $fileName + ); } - $destinationFile = self::_addDirSeparator($destinationFile) . $fileName; + $destinationFile = static::_addDirSeparator($destinationFile) . $fileName; try { $this->_result = $this->_moveFile($this->_file['tmp_name'], $destinationFile); @@ -265,6 +270,8 @@ private function validateDestination($destinationFolder) } /** + * Set access permissions to file. + * * @param string $file * @return void * @@ -541,7 +548,7 @@ private function _setUploadFileId($fileId) preg_match("/^(.*?)\[(.*?)\]$/", $fileId, $file); - if (is_array($file) && count($file) > 0 && count($file[0]) > 0 && count($file[1]) > 0) { + if (is_array($file) && count($file) > 0 && !empty($file[0]) && !empty($file[1])) { array_shift($file); $this->_uploadType = self::MULTIPLE_STYLE; diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/ReadInterface.php b/lib/internal/Magento/Framework/Filesystem/Directory/ReadInterface.php index 5519245ec24b..85d41b693262 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/ReadInterface.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/ReadInterface.php @@ -7,7 +7,7 @@ /** * Interface \Magento\Framework\Filesystem\Directory\ReadInterface - * + * @api */ interface ReadInterface { @@ -89,6 +89,7 @@ public function isDirectory($path = null); * * @param string $path * @return \Magento\Framework\Filesystem\File\ReadInterface + * @throws \Magento\Framework\Exception\FileSystemException */ public function openFile($path); diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/Write.php b/lib/internal/Magento/Framework/Filesystem/Directory/Write.php index 78732d73456f..3c6d2b7321b8 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/Write.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/Write.php @@ -9,6 +9,9 @@ use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Exception\ValidatorException; +/** + * Write Interface implementation + */ class Write extends Read implements WriteInterface { /** @@ -175,6 +178,7 @@ public function createSymlink($path, $destination, WriteInterface $targetDirecto */ public function delete($path = null) { + $exceptionMessages = []; $this->validatePath($path); if (!$this->isExist($path)) { return true; @@ -183,11 +187,59 @@ public function delete($path = null) if ($this->driver->isFile($absolutePath)) { $this->driver->deleteFile($absolutePath); } else { - $this->driver->deleteDirectory($absolutePath); + try { + $this->deleteFilesRecursively($absolutePath); + } catch (FileSystemException $e) { + $exceptionMessages[] = $e->getMessage(); + } + try { + $this->driver->deleteDirectory($absolutePath); + } catch (FileSystemException $e) { + $exceptionMessages[] = $e->getMessage(); + } + + if (!empty($exceptionMessages)) { + throw new FileSystemException( + new \Magento\Framework\Phrase( + \implode(' ', $exceptionMessages) + ) + ); + } } return true; } + /** + * Delete files recursively + * + * Implemented in order to delete as much files as possible and collect all exceptions + * + * @param string $path + * @return void + * @throws FileSystemException + */ + private function deleteFilesRecursively(string $path) + { + $exceptionMessages = []; + $entitiesList = $this->driver->readDirectoryRecursively($path); + foreach ($entitiesList as $entityPath) { + if ($this->driver->isFile($entityPath)) { + try { + $this->driver->deleteFile($entityPath); + } catch (FileSystemException $e) { + $exceptionMessages[] = $e->getMessage(); + } + } + } + if (!empty($exceptionMessages)) { + throw new FileSystemException( + new \Magento\Framework\Phrase( + \implode(' ', $exceptionMessages) + ) + ); + } + } + /** * Change permissions of given path * @@ -245,7 +297,7 @@ public function touch($path, $modificationTime = null) /** * Check if given path is writable * - * @param null $path + * @param string|null $path * @return bool * @throws \Magento\Framework\Exception\FileSystemException * @throws ValidatorException diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/WriteInterface.php b/lib/internal/Magento/Framework/Filesystem/Directory/WriteInterface.php index c72651a78dad..186cbcb81bff 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/WriteInterface.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/WriteInterface.php @@ -7,7 +7,7 @@ /** * Interface \Magento\Framework\Filesystem\Directory\WriteInterface - * + * @api */ interface WriteInterface extends ReadInterface { diff --git a/lib/internal/Magento/Framework/Filesystem/DirectoryList.php b/lib/internal/Magento/Framework/Filesystem/DirectoryList.php index 20874f60791c..b3ab51b94810 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/File.php b/lib/internal/Magento/Framework/Filesystem/Driver/File.php index b54b02bd6de9..59c9775d73a0 100644 --- a/lib/internal/Magento/Framework/Filesystem/Driver/File.php +++ b/lib/internal/Magento/Framework/Filesystem/Driver/File.php @@ -14,6 +14,7 @@ /** * Class File + * * @package Magento\Framework\Filesystem\Driver * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ @@ -399,17 +400,36 @@ public function deleteFile($path) */ public function deleteDirectory($path) { + $exceptionMessages = []; $flags = \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS; $iterator = new \FilesystemIterator($path, $flags); /** @var \FilesystemIterator $entity */ foreach ($iterator as $entity) { - if ($entity->isDir()) { - $this->deleteDirectory($entity->getPathname()); - } else { - $this->deleteFile($entity->getPathname()); + try { + if ($entity->isDir()) { + $this->deleteDirectory($entity->getPathname()); + } else { + $this->deleteFile($entity->getPathname()); + } + } catch (FileSystemException $exception) { + $exceptionMessages[] = $exception->getMessage(); } } - $result = @rmdir($this->getScheme() . $path); + + if (!empty($exceptionMessages)) { + throw new FileSystemException( + new \Magento\Framework\Phrase( + \implode(' ', $exceptionMessages) + ) + ); + } + + $fullPath = $this->getScheme() . $path; + if (is_link($fullPath)) { + $result = @unlink($fullPath); + } else { + $result = @rmdir($fullPath); + } if (!$result) { throw new FileSystemException( new \Magento\Framework\Phrase( @@ -843,6 +863,8 @@ public function fileUnlock($resource) } /** + * Returns an absolute path for the given one. + * * @param string $basePath * @param string $path * @param string|null $scheme @@ -879,7 +901,8 @@ public function getRelativePath($basePath, $path = null) } /** - * Fixes path separator + * Fixes path separator. + * * Utility method. * * @param string $path diff --git a/lib/internal/Magento/Framework/Filesystem/Driver/Http.php b/lib/internal/Magento/Framework/Filesystem/Driver/Http.php index 236585fa6138..f32624f4e751 100644 --- a/lib/internal/Magento/Framework/Filesystem/Driver/Http.php +++ b/lib/internal/Magento/Framework/Filesystem/Driver/Http.php @@ -12,7 +12,6 @@ /** * Class Http - * */ class Http extends File { @@ -28,21 +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]; - if (strpos($status, '200 OK') === false) { - $result = false; - } else { - $result = true; + /* Handling 301 or 302 redirection */ + if (isset($headers[1]) && preg_match('/30[12]/', $status)) { + $status = $headers[1]; } - return $result; + return !(strpos($status, '200 OK') === false); } /** diff --git a/lib/internal/Magento/Framework/Filesystem/File/ReadFactory.php b/lib/internal/Magento/Framework/Filesystem/File/ReadFactory.php index 38b581da752b..5d9badf42073 100644 --- a/lib/internal/Magento/Framework/Filesystem/File/ReadFactory.php +++ b/lib/internal/Magento/Framework/Filesystem/File/ReadFactory.php @@ -8,6 +8,10 @@ use Magento\Framework\Filesystem\DriverInterface; use Magento\Framework\Filesystem\DriverPool; +/** + * Opens a file for reading + * @api + */ class ReadFactory { /** @@ -28,7 +32,7 @@ public function __construct(DriverPool $driverPool) } /** - * Create a readable file + * Create a {@see ReaderInterface} * * @param string $path * @param DriverInterface|string $driver Driver or driver code diff --git a/lib/internal/Magento/Framework/Filesystem/File/WriteFactory.php b/lib/internal/Magento/Framework/Filesystem/File/WriteFactory.php index a45d6a62488f..af2a43ceaedc 100644 --- a/lib/internal/Magento/Framework/Filesystem/File/WriteFactory.php +++ b/lib/internal/Magento/Framework/Filesystem/File/WriteFactory.php @@ -8,6 +8,10 @@ use Magento\Framework\Filesystem\DriverInterface; use Magento\Framework\Filesystem\DriverPool; +/** + * Opens a file for reading and/or writing + * @api + */ class WriteFactory extends ReadFactory { /** @@ -29,12 +33,12 @@ public function __construct(DriverPool $driverPool) } /** - * Create a readable file. + * Create a {@see WriterInterface} * * @param string $path * @param DriverInterface|string $driver Driver or driver code * @param string $mode [optional] - * @return Write + * @return WriteInterface */ public function create($path, $driver, $mode = 'r') { diff --git a/lib/internal/Magento/Framework/Filesystem/Io/Sftp.php b/lib/internal/Magento/Framework/Filesystem/Io/Sftp.php index a04e4fcbb12d..119c89f03348 100644 --- a/lib/internal/Magento/Framework/Filesystem/Io/Sftp.php +++ b/lib/internal/Magento/Framework/Filesystem/Io/Sftp.php @@ -17,7 +17,7 @@ class Sftp extends AbstractIo const SSH2_PORT = 22; /** - * @var \phpseclib\Net\SFTP $_connection + * @var \phpseclib\Net\SFTP */ protected $_connection = null; diff --git a/lib/internal/Magento/Framework/Filter/Template.php b/lib/internal/Magento/Framework/Filter/Template.php index 10b6a17ea57d..d3a8d5334ab9 100644 --- a/lib/internal/Magento/Framework/Filter/Template.php +++ b/lib/internal/Magento/Framework/Filter/Template.php @@ -9,7 +9,12 @@ */ namespace Magento\Framework\Filter; +use Magento\Framework\Model\AbstractExtensibleModel; +use Magento\Framework\Model\AbstractModel; + /** + * Template filter + * * @api */ class Template implements \Zend_Filter_Interface @@ -61,6 +66,18 @@ class Template implements \Zend_Filter_Interface */ protected $string; + /** + * @var string[] + */ + private $restrictedMethods = [ + 'addafterfiltercallback', + 'getresourcecollection', + 'load', + 'save', + 'getcollection', + 'getresource' + ]; + /** * @param \Magento\Framework\Stdlib\StringUtils $string * @param array $variables @@ -228,8 +245,9 @@ protected function afterFilter($value) } /** - * Adds a callback to run after main filtering has happened. Callback must accept a single argument and return - * a string of the processed value. + * Adds a callback to run after main filtering has happened. + * + * Callback must accept a single argument and return a string of the processed value. * * @param callable $afterFilterCallback * @return $this @@ -257,6 +275,8 @@ protected function resetAfterFilterCallbacks() } /** + * Get var directive + * * @param string[] $construction * @return string */ @@ -288,7 +308,7 @@ public function templateDirective($construction) { // Processing of {template config_path=... [...]} statement $templateParameters = $this->getParameters($construction[2]); - if (!isset($templateParameters['config_path']) or !$this->getTemplateProcessor()) { + if (!isset($templateParameters['config_path']) || !$this->getTemplateProcessor()) { // Not specified template or not set include processor $replacedValue = '{Error in template processing}'; } else { @@ -302,6 +322,8 @@ public function templateDirective($construction) } /** + * Get depend directive + * * @param string[] $construction * @return string */ @@ -320,6 +342,8 @@ public function dependDirective($construction) } /** + * If directive + * * @param string[] $construction * @return string */ @@ -358,6 +382,46 @@ protected function getParameters($value) return $params; } + /** + * Validate method call initiated in a template. + * + * Deny calls for methods that may disrupt template processing. + * + * @param object $object + * @param string $method + * @return void + * @throws \InvalidArgumentException + */ + private function validateVariableMethodCall($object, string $method): void + { + if ($object === $this) { + if (in_array(mb_strtolower($method), $this->restrictedMethods)) { + throw new \InvalidArgumentException("Method $method cannot be called from template."); + } + } + } + + /** + * Check allowed methods for data objects. + * + * Deny calls for methods that may disrupt template processing. + * + * @param object $object + * @param string $method + * @return bool + * @throws \InvalidArgumentException + */ + private function isAllowedDataObjectMethod($object, string $method): bool + { + if ($object instanceof AbstractExtensibleModel || $object instanceof AbstractModel) { + if (in_array(mb_strtolower($method), $this->restrictedMethods)) { + throw new \InvalidArgumentException("Method $method cannot be called from template."); + } + } + + return true; + } + /** * Return variable value for var construction * @@ -374,7 +438,7 @@ protected function getVariable($value, $default = '{no_value_defined}') $stackVars = $tokenizer->tokenize(); $result = $default; $last = 0; - for ($i = 0; $i < count($stackVars); $i++) { + for ($i = 0, $count = count($stackVars); $i < $count; $i++) { if ($i == 0 && isset($this->templateVars[$stackVars[$i]['name']])) { // Getting of template value $stackVars[$i]['variable'] = & $this->templateVars[$stackVars[$i]['name']]; @@ -396,21 +460,27 @@ protected function getVariable($value, $default = '{no_value_defined}') || substr($stackVars[$i]['name'], 0, 3) == 'get' ) { $stackVars[$i]['args'] = $this->getStackArgs($stackVars[$i]['args']); - $stackVars[$i]['variable'] = call_user_func_array( - [$stackVars[$i - 1]['variable'], $stackVars[$i]['name']], - $stackVars[$i]['args'] - ); + + if ($this->isAllowedDataObjectMethod($stackVars[$i - 1]['variable'], $stackVars[$i]['name'])) { + $stackVars[$i]['variable'] = call_user_func_array( + [$stackVars[$i - 1]['variable'], $stackVars[$i]['name']], + $stackVars[$i]['args'] + ); + } } } $last = $i; - } elseif (isset($stackVars[$i - 1]['variable']) && $stackVars[$i]['type'] == 'method') { + } elseif (isset($stackVars[$i - 1]['variable']) + && is_object($stackVars[$i - 1]['variable']) + && $stackVars[$i]['type'] == 'method' + ) { // Calling object methods - if (method_exists($stackVars[$i - 1]['variable'], $stackVars[$i]['name'])) { - $stackVars[$i]['args'] = $this->getStackArgs($stackVars[$i]['args']); - $stackVars[$i]['variable'] = call_user_func_array( - [$stackVars[$i - 1]['variable'], $stackVars[$i]['name']], - $stackVars[$i]['args'] - ); + $object = $stackVars[$i - 1]['variable']; + $method = $stackVars[$i]['name']; + if (method_exists($object, $method)) { + $args = $this->getStackArgs($stackVars[$i]['args']); + $this->validateVariableMethodCall($object, $method); + $stackVars[$i]['variable'] = call_user_func_array([$object, $method], $args); } $last = $i; } diff --git a/lib/internal/Magento/Framework/Filter/Template/Tokenizer/Variable.php b/lib/internal/Magento/Framework/Filter/Template/Tokenizer/Variable.php index 67c2d17abe20..574ef9faf74a 100644 --- a/lib/internal/Magento/Framework/Filter/Template/Tokenizer/Variable.php +++ b/lib/internal/Magento/Framework/Filter/Template/Tokenizer/Variable.php @@ -313,6 +313,6 @@ public function getNumber() if (!$this->isNumeric()) { $this->prev(); } - return floatval($value); + return (float)$value; } } diff --git a/lib/internal/Magento/Framework/Filter/Test/Unit/TemplateTest.php b/lib/internal/Magento/Framework/Filter/Test/Unit/TemplateTest.php index 4883dc5fbe33..e4a2dc48d11d 100644 --- a/lib/internal/Magento/Framework/Filter/Test/Unit/TemplateTest.php +++ b/lib/internal/Magento/Framework/Filter/Test/Unit/TemplateTest.php @@ -6,6 +6,8 @@ namespace Magento\Framework\Filter\Test\Unit; +use Magento\Store\Model\Store; + class TemplateTest extends \PHPUnit\Framework\TestCase { /** @@ -13,10 +15,16 @@ class TemplateTest extends \PHPUnit\Framework\TestCase */ private $templateFilter; + /** + * @var Store + */ + private $store; + protected function setUp() { $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->templateFilter = $objectManager->getObject(\Magento\Framework\Filter\Template::class); + $this->store = $objectManager->getObject(Store::class); } public function testFilter() @@ -380,4 +388,55 @@ private function getObjectData() $dataObject->setAllVisibleItems($visibleItems); return $dataObject; } + + /** + * Check that if calling a method of an object fails expected result is returned. + */ + public function testInvalidMethodCall() + { + $this->templateFilter->setVariables(['dateTime' => '\DateTime']); + $this->assertEquals( + '\DateTime', + $this->templateFilter->filter('{{var dateTime.createFromFormat(\'d\',\'1548201468\')}}') + ); + } + + /** + * Test adding callbacks when already filtering. + * + * @expectedException \InvalidArgumentException + */ + public function testInappropriateCallbacks() + { + $this->templateFilter->setVariables(['filter' => $this->templateFilter]); + $this->templateFilter->filter('Test {{var filter.addAfterFilterCallback(\'mb_strtolower\')}}'); + } + + /** + * Test adding callbacks when already filtering. + * + * @expectedException \InvalidArgumentException + * @dataProvider disallowedMethods + */ + public function testDisallowedMethods($method) + { + $this->templateFilter->setVariables(['store' => $this->store]); + $this->templateFilter->filter('{{var store.'.$method.'()}}'); + } + + /** + * Data for testDisallowedMethods method + * + * @return array + */ + public function disallowedMethods() + { + return [ + ['getResourceCollection'], + ['load'], + ['save'], + ['getCollection'], + ['getResource'], + ]; + } } diff --git a/lib/internal/Magento/Framework/GraphQl/Config.php b/lib/internal/Magento/Framework/GraphQl/Config.php index 8d40d7fa6bb4..ec22b742b1d6 100644 --- a/lib/internal/Magento/Framework/GraphQl/Config.php +++ b/lib/internal/Magento/Framework/GraphQl/Config.php @@ -10,6 +10,7 @@ use Magento\Framework\Config\DataInterface; use Magento\Framework\GraphQl\Config\ConfigElementFactoryInterface; use Magento\Framework\GraphQl\Config\ConfigElementInterface; +use Magento\Framework\GraphQl\Query\Fields as QueryFields; /** * Provides access to typing information for a configured GraphQL schema. @@ -26,24 +27,28 @@ class Config implements ConfigInterface */ private $configElementFactory; + /** + * @var QueryFields + */ + private $queryFields; + /** * @param DataInterface $data * @param ConfigElementFactoryInterface $configElementFactory + * @param QueryFields $queryFields */ public function __construct( DataInterface $data, - ConfigElementFactoryInterface $configElementFactory + ConfigElementFactoryInterface $configElementFactory, + QueryFields $queryFields ) { $this->configData = $data; $this->configElementFactory = $configElementFactory; + $this->queryFields = $queryFields; } /** - * Get a data object with data pertaining to a GraphQL type's structural makeup. - * - * @param string $configElementName - * @return ConfigElementInterface - * @throws \LogicException + * @inheritdoc */ public function getConfigElement(string $configElementName) : ConfigElementInterface { @@ -53,22 +58,38 @@ public function getConfigElement(string $configElementName) : ConfigElementInter sprintf('Config element "%s" is not declared in GraphQL schema', $configElementName) ); } + + $fieldsInQuery = $this->queryFields->getFieldsUsedInQuery(); + if (isset($data['fields'])) { + if (!empty($fieldsInQuery)) { + foreach (array_keys($data['fields']) as $fieldName) { + if (!isset($fieldsInQuery[$fieldName])) { + unset($data['fields'][$fieldName]); + } + } + } + + ksort($data['fields']); + } + return $this->configElementFactory->createFromConfigData($data); } /** - * Return all type names declared in a GraphQL schema's configuration. - * - * @return string[] + * @inheritdoc */ - public function getDeclaredTypeNames() : array + public function getDeclaredTypes() : array { $types = []; foreach ($this->configData->get(null) as $item) { - if (isset($item['type']) && $item['type'] == 'graphql_type') { - $types[] = $item['name']; + if (isset($item['type'])) { + $types[] = [ + 'name' => $item['name'], + 'type' => $item['type'], + ]; } } + return $types; } } diff --git a/lib/internal/Magento/Framework/GraphQl/Config/Element/Enum.php b/lib/internal/Magento/Framework/GraphQl/Config/Element/Enum.php index b1210e986b77..994ae489af12 100644 --- a/lib/internal/Magento/Framework/GraphQl/Config/Element/Enum.php +++ b/lib/internal/Magento/Framework/GraphQl/Config/Element/Enum.php @@ -37,7 +37,7 @@ class Enum implements ConfigElementInterface public function __construct( string $name, array $values, - string $description = "" + string $description ) { $this->name = $name; $this->values = $values; diff --git a/lib/internal/Magento/Framework/GraphQl/Config/Element/Field.php b/lib/internal/Magento/Framework/GraphQl/Config/Element/Field.php index 76cfa06f9c11..0fc51e4ecd06 100644 --- a/lib/internal/Magento/Framework/GraphQl/Config/Element/Field.php +++ b/lib/internal/Magento/Framework/GraphQl/Config/Element/Field.php @@ -48,6 +48,11 @@ class Field implements OutputFieldInterface */ private $description; + /** + * @var array + */ + private $cache; + /** * @param string $name * @param string $type @@ -57,6 +62,7 @@ class Field implements OutputFieldInterface * @param string $resolver * @param string $description * @param array $arguments + * @param array $cache */ public function __construct( string $name, @@ -66,7 +72,8 @@ public function __construct( string $itemType = '', string $resolver = '', string $description = '', - array $arguments = [] + array $arguments = [], + array $cache = [] ) { $this->name = $name; $this->type = $isList ? $itemType : $type; @@ -75,6 +82,7 @@ public function __construct( $this->resolver = $resolver; $this->description = $description; $this->arguments = $arguments; + $this->cache = $cache; } /** @@ -146,4 +154,14 @@ public function getDescription() : string { return $this->description; } + + /** + * Return the cache tag for the field. + * + * @return array|null + */ + public function getCache() : array + { + return $this->cache; + } } diff --git a/lib/internal/Magento/Framework/GraphQl/Config/Element/FieldFactory.php b/lib/internal/Magento/Framework/GraphQl/Config/Element/FieldFactory.php index 1a022e92c829..60191b69be47 100644 --- a/lib/internal/Magento/Framework/GraphQl/Config/Element/FieldFactory.php +++ b/lib/internal/Magento/Framework/GraphQl/Config/Element/FieldFactory.php @@ -42,10 +42,11 @@ public function createFromConfigData( array $fieldData, array $arguments = [] ) : Field { - $arraySign = '/^.*(\[\])$/'; + $fieldType = $fieldData['type']; $isList = false; - if (preg_match($arraySign, $fieldData['type'])) { + //check if type ends with [] + if ($fieldType{strlen($fieldType) - 2} == '[' && $fieldType{strlen($fieldType) - 1} == ']') { $isList = true; $fieldData['type'] = str_replace('[]', '', $fieldData['type']); $fieldData['itemType'] = str_replace('[]', '', $fieldData['type']); @@ -61,7 +62,8 @@ public function createFromConfigData( 'itemType' => isset($fieldData['itemType']) ? $fieldData['itemType'] : '', 'resolver' => isset($fieldData['resolver']) ? $fieldData['resolver'] : '', 'description' => isset($fieldData['description']) ? $fieldData['description'] : '', - 'arguments' => $arguments + 'cache' => isset($fieldData['cache']) ? $fieldData['cache'] : [], + 'arguments' => $arguments, ] ); } diff --git a/lib/internal/Magento/Framework/GraphQl/Config/Element/FieldsFactory.php b/lib/internal/Magento/Framework/GraphQl/Config/Element/FieldsFactory.php new file mode 100644 index 000000000000..ca6b67eac3d8 --- /dev/null +++ b/lib/internal/Magento/Framework/GraphQl/Config/Element/FieldsFactory.php @@ -0,0 +1,62 @@ +argumentFactory = $argumentFactory; + $this->fieldFactory = $fieldFactory; + } + + /** + * Create a fields object from a configured array with optional arguments. + * + * Field data must contain name and type. Other values are optional and include required, itemType, description, + * and resolver. Arguments array must be in the format of [$argumentData['name'] => $argumentData]. + * + * @param array $fieldsData + * @return Field[] + */ + public function createFromConfigData( + array $fieldsData + ) : array { + $fields = []; + foreach ($fieldsData as $fieldData) { + $arguments = []; + foreach ($fieldData['arguments'] as $argumentData) { + $arguments[$argumentData['name']] = $this->argumentFactory->createFromConfigData($argumentData); + } + $fields[$fieldData['name']] = $this->fieldFactory->createFromConfigData( + $fieldData, + $arguments + ); + } + return $fields; + } +} diff --git a/lib/internal/Magento/Framework/GraphQl/Config/Element/Input.php b/lib/internal/Magento/Framework/GraphQl/Config/Element/Input.php new file mode 100644 index 000000000000..8e86f701672c --- /dev/null +++ b/lib/internal/Magento/Framework/GraphQl/Config/Element/Input.php @@ -0,0 +1,74 @@ +name = $name; + $this->fields = $fields; + $this->description = $description; + } + + /** + * Get the type name. + * + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * Get a list of fields that make up the possible return or input values of a type. + * + * @return Field[] + */ + public function getFields(): array + { + return $this->fields; + } + + /** + * Get a human-readable description of the type. + * + * @return string + */ + public function getDescription(): string + { + return $this->description; + } +} diff --git a/lib/internal/Magento/Framework/GraphQl/Config/Element/InputFactory.php b/lib/internal/Magento/Framework/GraphQl/Config/Element/InputFactory.php new file mode 100644 index 000000000000..0e7ccb831a5a --- /dev/null +++ b/lib/internal/Magento/Framework/GraphQl/Config/Element/InputFactory.php @@ -0,0 +1,79 @@ +objectManager = $objectManager; + $this->fieldsFactory = $fieldsFactory; + } + + /** + * Instantiate an object representing 'input' GraphQL config element. + * + * @param array $data + * @return ConfigElementInterface + */ + public function createFromConfigData(array $data): ConfigElementInterface + { + $fields = isset($data['fields']) ? $this->fieldsFactory->createFromConfigData($data['fields']) : []; + + return $this->create( + $data, + $fields + ); + } + + /** + * Create input type object based off array of configured GraphQL InputType data. + * + * Type data must contain name and the type's fields. Optional data includes description. + * + * @param array $typeData + * @param array $fields + * @return Input + */ + private function create( + array $typeData, + array $fields + ): Input { + return $this->objectManager->create( + Input::class, + [ + 'name' => $typeData['name'], + 'fields' => $fields, + 'description' => isset($typeData['description']) ? $typeData['description'] : '' + ] + ); + } +} diff --git a/lib/internal/Magento/Framework/GraphQl/Config/Element/InterfaceType.php b/lib/internal/Magento/Framework/GraphQl/Config/Element/InterfaceType.php index 320199c14a6d..73ebd42acfb2 100644 --- a/lib/internal/Magento/Framework/GraphQl/Config/Element/InterfaceType.php +++ b/lib/internal/Magento/Framework/GraphQl/Config/Element/InterfaceType.php @@ -8,7 +8,7 @@ namespace Magento\Framework\GraphQl\Config\Element; /** - * Describes the configured data for a GraphQL interface type. + * Class representing 'interface' GraphQL config element. */ class InterfaceType implements TypeInterface { @@ -42,7 +42,7 @@ public function __construct( string $name, string $typeResolver, array $fields, - string $description = "" + string $description ) { $this->name = $name; $this->fields = $fields; diff --git a/lib/internal/Magento/Framework/GraphQl/Config/Element/Type.php b/lib/internal/Magento/Framework/GraphQl/Config/Element/Type.php index 24ff439db034..20d017cc7106 100644 --- a/lib/internal/Magento/Framework/GraphQl/Config/Element/Type.php +++ b/lib/internal/Magento/Framework/GraphQl/Config/Element/Type.php @@ -8,7 +8,7 @@ namespace Magento\Framework\GraphQl\Config\Element; /** - * Describes all the configured data of an Output or Input type in GraphQL. + * Class representing 'type' GraphQL config element. */ class Type implements TypeInterface { diff --git a/lib/internal/Magento/Framework/GraphQl/Config/Element/TypeFactory.php b/lib/internal/Magento/Framework/GraphQl/Config/Element/TypeFactory.php index c5f3187b0484..5dd477a05089 100644 --- a/lib/internal/Magento/Framework/GraphQl/Config/Element/TypeFactory.php +++ b/lib/internal/Magento/Framework/GraphQl/Config/Element/TypeFactory.php @@ -22,28 +22,20 @@ class TypeFactory implements ConfigElementFactoryInterface private $objectManager; /** - * @var ArgumentFactory + * @var FieldsFactory */ - private $argumentFactory; - - /** - * @var FieldFactory - */ - private $fieldFactory; + private $fieldsFactory; /** * @param ObjectManagerInterface $objectManager - * @param ArgumentFactory $argumentFactory - * @param FieldFactory $fieldFactory + * @param FieldsFactory $fieldsFactory */ public function __construct( ObjectManagerInterface $objectManager, - ArgumentFactory $argumentFactory, - FieldFactory $fieldFactory + FieldsFactory $fieldsFactory ) { $this->objectManager = $objectManager; - $this->argumentFactory = $argumentFactory; - $this->fieldFactory = $fieldFactory; + $this->fieldsFactory = $fieldsFactory; } /** @@ -54,18 +46,8 @@ public function __construct( */ public function createFromConfigData(array $data): ConfigElementInterface { - $fields = []; - $data['fields'] = isset($data['fields']) ? $data['fields'] : []; - foreach ($data['fields'] as $field) { - $arguments = []; - foreach ($field['arguments'] as $argument) { - $arguments[$argument['name']] = $this->argumentFactory->createFromConfigData($argument); - } - $fields[$field['name']] = $this->fieldFactory->createFromConfigData( - $field, - $arguments - ); - } + $fields = isset($data['fields']) ? $this->fieldsFactory->createFromConfigData($data['fields']) : []; + return $this->create( $data, $fields @@ -73,10 +55,10 @@ public function createFromConfigData(array $data): ConfigElementInterface } /** - * Create type object based off array of configured GraphQL Output/InputType data. + * Create type object based off array of configured GraphQL Type data. * * Type data must contain name and the type's fields. Optional data includes 'implements' (i.e. the interfaces - * implemented by the types), and description. An InputType cannot implement an interface. + * implemented by the types), and description. * * @param array $typeData * @param array $fields diff --git a/lib/internal/Magento/Framework/GraphQl/ConfigInterface.php b/lib/internal/Magento/Framework/GraphQl/ConfigInterface.php index c2670967f1db..f7d6cf49e180 100644 --- a/lib/internal/Magento/Framework/GraphQl/ConfigInterface.php +++ b/lib/internal/Magento/Framework/GraphQl/ConfigInterface.php @@ -25,9 +25,11 @@ interface ConfigInterface public function getConfigElement(string $configElementName) : ConfigElementInterface; /** - * Return all type names from a GraphQL schema's configuration. + * Return all type names declared in a GraphQL schema's configuration and their type. * - * @return string[] + * Format is ['name' => 'example value', 'type' = 'example value'] + * + * @return array $types */ - public function getDeclaredTypeNames() : array; + public function getDeclaredTypes() : array; } diff --git a/lib/internal/Magento/Framework/GraphQl/Exception/GraphQlAlreadyExistsException.php b/lib/internal/Magento/Framework/GraphQl/Exception/GraphQlAlreadyExistsException.php new file mode 100644 index 000000000000..8275219e9e55 --- /dev/null +++ b/lib/internal/Magento/Framework/GraphQl/Exception/GraphQlAlreadyExistsException.php @@ -0,0 +1,56 @@ +isSafe = $isSafe; + parent::__construct($phrase, $cause, $code); + } + + /** + * @inheritdoc + */ + public function isClientSafe(): bool + { + return $this->isSafe; + } + + /** + * @inheritdoc + */ + public function getCategory(): string + { + return self::EXCEPTION_CATEGORY; + } +} diff --git a/lib/internal/Magento/Framework/GraphQl/Exception/GraphQlAuthenticationException.php b/lib/internal/Magento/Framework/GraphQl/Exception/GraphQlAuthenticationException.php new file mode 100644 index 000000000000..44c3d07bd186 --- /dev/null +++ b/lib/internal/Magento/Framework/GraphQl/Exception/GraphQlAuthenticationException.php @@ -0,0 +1,56 @@ +isSafe = $isSafe; + parent::__construct($phrase, $cause, $code); + } + + /** + * @inheritdoc + */ + public function isClientSafe(): bool + { + return $this->isSafe; + } + + /** + * @inheritdoc + */ + public function getCategory(): string + { + return self::EXCEPTION_CATEGORY; + } +} diff --git a/lib/internal/Magento/Framework/GraphQl/Exception/GraphQlAuthorizationException.php b/lib/internal/Magento/Framework/GraphQl/Exception/GraphQlAuthorizationException.php index 5b76e0ab9f5a..f1232ebd4d14 100644 --- a/lib/internal/Magento/Framework/GraphQl/Exception/GraphQlAuthorizationException.php +++ b/lib/internal/Magento/Framework/GraphQl/Exception/GraphQlAuthorizationException.php @@ -11,7 +11,7 @@ use Magento\Framework\Exception\AuthorizationException; /** - * Class GraphQlAuthorizationException + * Exception for GraphQL to be thrown when authorization fails */ class GraphQlAuthorizationException extends AuthorizationException implements \GraphQL\Error\ClientAware { @@ -37,7 +37,7 @@ public function __construct(Phrase $phrase, \Exception $cause = null, $code = 0, } /** - * {@inheritDoc} + * @inheritdoc */ public function isClientSafe() : bool { @@ -45,7 +45,7 @@ public function isClientSafe() : bool } /** - * {@inheritDoc} + * @inheritdoc */ public function getCategory() : string { diff --git a/lib/internal/Magento/Framework/GraphQl/Exception/GraphQlInputException.php b/lib/internal/Magento/Framework/GraphQl/Exception/GraphQlInputException.php index 6f97f0626135..429b7c04b747 100644 --- a/lib/internal/Magento/Framework/GraphQl/Exception/GraphQlInputException.php +++ b/lib/internal/Magento/Framework/GraphQl/Exception/GraphQlInputException.php @@ -37,7 +37,7 @@ public function __construct(Phrase $phrase, \Exception $cause = null, $code = 0, } /** - * {@inheritDoc} + * @inheritdoc */ public function isClientSafe() : bool { @@ -45,7 +45,7 @@ public function isClientSafe() : bool } /** - * {@inheritDoc} + * @inheritdoc */ public function getCategory() : string { diff --git a/lib/internal/Magento/Framework/GraphQl/Exception/GraphQlNoSuchEntityException.php b/lib/internal/Magento/Framework/GraphQl/Exception/GraphQlNoSuchEntityException.php index 05c5925e8d1e..2a0b9d3bc2ea 100644 --- a/lib/internal/Magento/Framework/GraphQl/Exception/GraphQlNoSuchEntityException.php +++ b/lib/internal/Magento/Framework/GraphQl/Exception/GraphQlNoSuchEntityException.php @@ -11,7 +11,7 @@ use Magento\Framework\Phrase; /** - * Class GraphQlNoSuchEntityException + * Exception for GraphQL to be thrown when entity does not exists */ class GraphQlNoSuchEntityException extends NoSuchEntityException implements \GraphQL\Error\ClientAware { @@ -37,7 +37,7 @@ public function __construct(Phrase $phrase, \Exception $cause = null, $code = 0, } /** - * {@inheritDoc} + * @inheritdoc */ public function isClientSafe() : bool { @@ -45,7 +45,7 @@ public function isClientSafe() : bool } /** - * {@inheritDoc} + * @inheritdoc */ public function getCategory() : string { diff --git a/lib/internal/Magento/Framework/GraphQl/Query/Fields.php b/lib/internal/Magento/Framework/GraphQl/Query/Fields.php new file mode 100644 index 000000000000..a34c0a9d4218 --- /dev/null +++ b/lib/internal/Magento/Framework/GraphQl/Query/Fields.php @@ -0,0 +1,90 @@ + [ + NodeKind::NAME => function (Node $node) use (&$queryFields) { + $queryFields[$node->value] = $node->value; + } + ] + ] + ); + if (isset($variables)) { + $queryFields = array_merge($queryFields, $this->extractVariables($variables)); + } + } catch (\Exception $e) { + // If a syntax error is encountered do not collect fields + } + if (isset($queryFields['IntrospectionQuery'])) { + // It must be possible to query any fields during introspection query + $queryFields = []; + } + $this->fieldsUsedInQuery = $queryFields; + } + + /** + * Get list of fields used in GraphQL query. + * + * This method is stateful and relies on the query being set with setQuery. + * + * @return string[] + */ + public function getFieldsUsedInQuery() + { + return $this->fieldsUsedInQuery; + } + + /** + * Extract and return list of all used fields in GraphQL query's variables + * + * @param array $variables + * + * @return string[] + */ + private function extractVariables(array $variables): array + { + $fields = []; + foreach ($variables as $key => $value) { + if (is_array($value)) { + $fields = array_merge($fields, $this->extractVariables($value)); + } + $fields[$key] = $key; + } + + return $fields; + } +} diff --git a/lib/internal/Magento/Framework/GraphQl/Query/IntrospectionConfiguration.php b/lib/internal/Magento/Framework/GraphQl/Query/IntrospectionConfiguration.php new file mode 100644 index 000000000000..2fdb3df5f6d7 --- /dev/null +++ b/lib/internal/Magento/Framework/GraphQl/Query/IntrospectionConfiguration.php @@ -0,0 +1,42 @@ +deploymentConfig = $deploymentConfig; + } + + /** + * Check the the environment config to determine if introspection should be disabled. + * + * @return bool + */ + public function isIntrospectionDisabled(): bool + { + return (bool)$this->deploymentConfig->get(self::CONFIG_PATH_DISABLE_INTROSPECTION); + } +} diff --git a/lib/internal/Magento/Framework/GraphQl/Query/QueryComplexityLimiter.php b/lib/internal/Magento/Framework/GraphQl/Query/QueryComplexityLimiter.php new file mode 100644 index 000000000000..2b9ce9b01b5c --- /dev/null +++ b/lib/internal/Magento/Framework/GraphQl/Query/QueryComplexityLimiter.php @@ -0,0 +1,69 @@ +queryDepth = $queryDepth; + $this->queryComplexity = $queryComplexity; + $this->introspectionConfig = $introspectionConfig; + } + + /** + * Sets limits for query complexity + * + * @return void + */ + public function execute(): void + { + DocumentValidator::addRule(new QueryComplexity($this->queryComplexity)); + DocumentValidator::addRule( + new DisableIntrospection((int) $this->introspectionConfig->isIntrospectionDisabled()) + ); + DocumentValidator::addRule(new QueryDepth($this->queryDepth)); + } +} diff --git a/lib/internal/Magento/Framework/GraphQl/Query/QueryProcessor.php b/lib/internal/Magento/Framework/GraphQl/Query/QueryProcessor.php index 94a0e5a1c1a6..0a0dba36ef0e 100644 --- a/lib/internal/Magento/Framework/GraphQl/Query/QueryProcessor.php +++ b/lib/internal/Magento/Framework/GraphQl/Query/QueryProcessor.php @@ -7,9 +7,6 @@ namespace Magento\Framework\GraphQl\Query; -use GraphQL\Validator\DocumentValidator; -use GraphQL\Validator\Rules\DisableIntrospection; -use GraphQL\Validator\Rules\QueryDepth; use Magento\Framework\GraphQl\Exception\ExceptionFormatter; use Magento\Framework\GraphQl\Schema; use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; @@ -24,12 +21,21 @@ class QueryProcessor */ private $exceptionFormatter; + /** + * @var QueryComplexityLimiter + */ + private $queryComplexityLimiter; + /** * @param ExceptionFormatter $exceptionFormatter + * @param QueryComplexityLimiter $queryComplexityLimiter */ - public function __construct(ExceptionFormatter $exceptionFormatter) - { + public function __construct( + ExceptionFormatter $exceptionFormatter, + QueryComplexityLimiter $queryComplexityLimiter + ) { $this->exceptionFormatter = $exceptionFormatter; + $this->queryComplexityLimiter = $queryComplexityLimiter; } /** @@ -50,9 +56,9 @@ public function process( string $operationName = null ) : array { if (!$this->exceptionFormatter->shouldShowDetail()) { - DocumentValidator::addRule(new QueryDepth(10)); - DocumentValidator::addRule(new DisableIntrospection()); + $this->queryComplexityLimiter->execute(); } + $rootValue = null; return \GraphQL\GraphQL::executeQuery( $schema, @@ -63,7 +69,7 @@ public function process( $operationName )->toArray( $this->exceptionFormatter->shouldShowDetail() ? - \GraphQL\Error\Debug::INCLUDE_DEBUG_MESSAGE | \GraphQL\Error\Debug::INCLUDE_TRACE : false + \GraphQL\Error\Debug::INCLUDE_DEBUG_MESSAGE : false ); } } diff --git a/lib/internal/Magento/Framework/GraphQl/Query/Resolver/Argument/FieldEntityAttributesPool.php b/lib/internal/Magento/Framework/GraphQl/Query/Resolver/Argument/FieldEntityAttributesPool.php index e7d14a81b9de..bd9de206ccda 100644 --- a/lib/internal/Magento/Framework/GraphQl/Query/Resolver/Argument/FieldEntityAttributesPool.php +++ b/lib/internal/Magento/Framework/GraphQl/Query/Resolver/Argument/FieldEntityAttributesPool.php @@ -38,7 +38,7 @@ public function getEntityAttributesForEntityFromField(string $fieldName) : array if (isset($this->attributesInstances[$fieldName])) { return $this->attributesInstances[$fieldName]->getEntityAttributes(); } else { - throw new \LogicException(sprintf('There is no attrribute class assigned to field %1', $fieldName)); + throw new \LogicException(sprintf('There is no attribute class assigned to field %1', $fieldName)); } } } diff --git a/lib/internal/Magento/Framework/GraphQl/Query/Resolver/IdentityInterface.php b/lib/internal/Magento/Framework/GraphQl/Query/Resolver/IdentityInterface.php new file mode 100644 index 000000000000..d65e86a37550 --- /dev/null +++ b/lib/internal/Magento/Framework/GraphQl/Query/Resolver/IdentityInterface.php @@ -0,0 +1,20 @@ +schemaFactory = $schemaFactory; - $this->outputMapper = $outputMapper; $this->config = $config; + $this->typeRegistry = $typeRegistry; } /** - * {@inheritdoc} + * @inheritdoc */ public function generate() : Schema { $schema = $this->schemaFactory->create( [ - 'query' => $this->outputMapper->getOutputType('Query'), - 'mutation' => $this->outputMapper->getOutputType('Mutation'), + 'query' => $this->typeRegistry->get('Query'), + 'mutation' => $this->typeRegistry->get('Mutation'), 'typeLoader' => function ($name) { - return $this->outputMapper->getOutputType($name); + return $this->typeRegistry->get($name); }, 'types' => function () { - //all types should be generated only on introspection $typesImplementors = []; - foreach ($this->config->getDeclaredTypeNames() as $name) { - $typesImplementors [] = $this->outputMapper->getOutputType($name); + foreach ($this->config->getDeclaredTypes() as $type) { + $typesImplementors [] = $this->typeRegistry->get($type['name']); } return $typesImplementors; } diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Entity/DefaultMapper.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Entity/DefaultMapper.php index 7e3f2ac6db63..cacc1f9e28c0 100644 --- a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Entity/DefaultMapper.php +++ b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Entity/DefaultMapper.php @@ -26,10 +26,10 @@ public function __construct(array $map = []) } /** - * {@inheritDoc} + * @inheritdoc */ public function getMappedTypes(string $entityName) : array { - return isset($this->map[$entityName]) ? $this->map[$entityName] : []; + return $this->map[$entityName] ?? []; } } diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Enum/DefaultDataMapper.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Enum/DefaultDataMapper.php index f7b39ba64207..f560fcb0de77 100644 --- a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Enum/DefaultDataMapper.php +++ b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Enum/DefaultDataMapper.php @@ -26,10 +26,10 @@ public function __construct(array $map) } /** - * {@inheritDoc} + * @inheritdoc */ public function getMappedEnums(string $enumName) : array { - return isset($this->map[$enumName]) ? $this->map[$enumName] : []; + return $this->map[$enumName] ?? []; } } diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Input/InputFactory.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Input/InputFactory.php deleted file mode 100644 index cbbd97cfdb8c..000000000000 --- a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Input/InputFactory.php +++ /dev/null @@ -1,60 +0,0 @@ -objectManager = $objectManager; - $this->prototypes = $prototypes; - } - - /** - * @param ConfigElementInterface $configElement - * @return InputTypeInterface - */ - public function create(ConfigElementInterface $configElement) : InputTypeInterface - { - if (!isset($this->typeRegistry[$configElement->getName()])) { - $this->typeRegistry[$configElement->getName()] = - $this->objectManager->create( - $this->prototypes[get_class($configElement)], - [ - 'configElement' => $configElement - ] - ); - } - return $this->typeRegistry[$configElement->getName()]; - } -} diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Input/InputMapper.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Input/InputMapper.php index d806c0b3e68a..d1f48dada2cb 100644 --- a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Input/InputMapper.php +++ b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Input/InputMapper.php @@ -9,27 +9,15 @@ use Magento\Framework\GraphQl\Config\Data\WrappedTypeProcessor; use Magento\Framework\GraphQl\Config\Element\Argument; -use Magento\Framework\GraphQl\ConfigInterface; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Schema\Type\ScalarTypes; -use Magento\Framework\GraphQl\Schema\TypeFactory; +use Magento\Framework\GraphQl\Schema\Type\TypeRegistry; +/** + * Prepare argument's metadata for GraphQL schema generation + */ class InputMapper { - /** - * @var InputFactory - */ - private $inputFactory; - - /** - * @var ConfigInterface - */ - private $config; - - /** - * @var TypeFactory - */ - private $typeFactory; - /** * @var ScalarTypes */ @@ -41,24 +29,23 @@ class InputMapper private $wrappedTypeProcessor; /** - * @param InputFactory $inputFactory - * @param ConfigInterface $config - * @param TypeFactory $typeFactory + * @var TypeRegistry + */ + private $typeRegistry; + + /** * @param ScalarTypes $scalarTypes * @param WrappedTypeProcessor $wrappedTypeProcessor + * @param TypeRegistry $typeRegistry */ public function __construct( - InputFactory $inputFactory, - ConfigInterface $config, - TypeFactory $typeFactory, ScalarTypes $scalarTypes, - WrappedTypeProcessor $wrappedTypeProcessor + WrappedTypeProcessor $wrappedTypeProcessor, + TypeRegistry $typeRegistry ) { - $this->inputFactory = $inputFactory; - $this->config = $config; - $this->typeFactory = $typeFactory; $this->scalarTypes = $scalarTypes; $this->wrappedTypeProcessor = $wrappedTypeProcessor; + $this->typeRegistry = $typeRegistry; } /** @@ -66,6 +53,7 @@ public function __construct( * * @param Argument $argument * @return array + * @throws GraphQlInputException */ public function getRepresentation(Argument $argument) : array { @@ -73,8 +61,7 @@ public function getRepresentation(Argument $argument) : array if ($this->scalarTypes->isScalarType($typeName)) { $instance = $this->wrappedTypeProcessor->processScalarWrappedType($argument); } else { - $configElement = $this->config->getConfigElement($typeName); - $instance = $this->inputFactory->create($configElement); + $instance = $this->typeRegistry->get($typeName); $instance = $this->wrappedTypeProcessor->processWrappedType($argument, $instance); } diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Input/InputObjectType.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Input/InputObjectType.php index ae2d07ade2ad..fa0327f79bc6 100644 --- a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Input/InputObjectType.php +++ b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Input/InputObjectType.php @@ -8,21 +8,16 @@ namespace Magento\Framework\GraphQl\Schema\Type\Input; use Magento\Framework\GraphQl\Config\Data\WrappedTypeProcessor; -use Magento\Framework\GraphQl\Config\Element\Type as TypeConfigElement; -use Magento\Framework\GraphQl\ConfigInterface; +use Magento\Framework\GraphQl\Config\Element\Input as InputConfigElement; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Schema\Type\ScalarTypes; -use Magento\Framework\GraphQl\Schema\TypeFactory; +use Magento\Framework\GraphQl\Schema\Type\TypeRegistry; /** * Class InputObjectType */ class InputObjectType extends \Magento\Framework\GraphQl\Schema\Type\InputObjectType { - /** - * @var TypeFactory - */ - private $typeFactory; - /** * @var ScalarTypes */ @@ -34,36 +29,27 @@ class InputObjectType extends \Magento\Framework\GraphQl\Schema\Type\InputObject private $wrappedTypeProcessor; /** - * @var InputFactory + * @var TypeRegistry */ - private $inputFactory; + private $typeRegistry; /** - * @var ConfigInterface - */ - public $graphQlConfig; - - /** - * @param TypeConfigElement $configElement - * @param TypeFactory $typeFactory + * @param InputConfigElement $configElement * @param ScalarTypes $scalarTypes * @param WrappedTypeProcessor $wrappedTypeProcessor - * @param InputFactory $inputFactory - * @param ConfigInterface $graphQlConfig + * @param TypeRegistry $typeRegistry + * @throws GraphQlInputException */ public function __construct( - TypeConfigElement $configElement, - TypeFactory $typeFactory, + InputConfigElement $configElement, ScalarTypes $scalarTypes, WrappedTypeProcessor $wrappedTypeProcessor, - InputFactory $inputFactory, - ConfigInterface $graphQlConfig + TypeRegistry $typeRegistry ) { - $this->typeFactory = $typeFactory; $this->scalarTypes = $scalarTypes; $this->wrappedTypeProcessor = $wrappedTypeProcessor; - $this->inputFactory = $inputFactory; - $this->graphQlConfig = $graphQlConfig; + $this->typeRegistry = $typeRegistry; + $config = [ 'name' => $configElement->getName(), 'description' => $configElement->getDescription() @@ -75,8 +61,7 @@ public function __construct( if ($field->getTypeName() == $configElement->getName()) { $type = $this; } else { - $fieldConfigElement = $this->graphQlConfig->getConfigElement($field->getTypeName()); - $type = $this->inputFactory->create($fieldConfigElement); + $type = $this->typeRegistry->get($field->getTypeName()); } $type = $this->wrappedTypeProcessor->processWrappedType($field, $type); } diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/ElementMapper/Formatter/Fields.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/ElementMapper/Formatter/Fields.php index b54cd4d8ca21..034a5702090d 100644 --- a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/ElementMapper/Formatter/Fields.php +++ b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/ElementMapper/Formatter/Fields.php @@ -16,7 +16,6 @@ use Magento\Framework\GraphQl\Schema\Type\Output\OutputMapper; use Magento\Framework\GraphQl\Schema\Type\OutputTypeInterface; use Magento\Framework\GraphQl\Schema\Type\ScalarTypes; -use Magento\Framework\GraphQl\Schema\TypeFactory; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfoFactory; @@ -40,11 +39,6 @@ class Fields implements FormatterInterface */ private $inputMapper; - /** - * @var TypeFactory - */ - private $typeFactory; - /** * @var ScalarTypes */ @@ -64,7 +58,6 @@ class Fields implements FormatterInterface * @param ObjectManagerInterface $objectManager * @param OutputMapper $outputMapper * @param InputMapper $inputMapper - * @param TypeFactory $typeFactory * @param ScalarTypes $scalarTypes * @param WrappedTypeProcessor $wrappedTypeProcessor * @param ResolveInfoFactory $resolveInfoFactory @@ -73,7 +66,6 @@ public function __construct( ObjectManagerInterface $objectManager, OutputMapper $outputMapper, InputMapper $inputMapper, - TypeFactory $typeFactory, ScalarTypes $scalarTypes, WrappedTypeProcessor $wrappedTypeProcessor, ResolveInfoFactory $resolveInfoFactory @@ -81,14 +73,13 @@ public function __construct( $this->objectManager = $objectManager; $this->outputMapper = $outputMapper; $this->inputMapper = $inputMapper; - $this->typeFactory = $typeFactory; $this->scalarTypes = $scalarTypes; $this->wrappedTypeProcessor = $wrappedTypeProcessor; $this->resolveInfoFactory = $resolveInfoFactory; } /** - * {@inheritDoc} + * @inheritdoc */ public function format(TypeInterface $configElement, OutputTypeInterface $outputType): array { diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/OutputFactory.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/OutputFactory.php deleted file mode 100644 index 81dad11774b0..000000000000 --- a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/OutputFactory.php +++ /dev/null @@ -1,65 +0,0 @@ -objectManager = $objectManager; - $this->prototypes = $prototypes; - } - - /** - * Create output type. - * - * @param ConfigElementInterface $configElement - * @return OutputTypeInterface - */ - public function create(ConfigElementInterface $configElement) : OutputTypeInterface - { - if (!isset($this->typeRegistry[$configElement->getName()])) { - $this->typeRegistry[$configElement->getName()] = - $this->objectManager->create( - $this->prototypes[get_class($configElement)], - [ - 'configElement' => $configElement - ] - ); - } - return $this->typeRegistry[$configElement->getName()]; - } -} diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/OutputMapper.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/OutputMapper.php index b7f4b8a1f60d..046eeb5b1f93 100644 --- a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/OutputMapper.php +++ b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/OutputMapper.php @@ -7,50 +7,28 @@ namespace Magento\Framework\GraphQl\Schema\Type\Output; -use Magento\Framework\GraphQl\ConfigInterface; use Magento\Framework\GraphQl\Schema\Type\OutputTypeInterface; -use Magento\Framework\GraphQl\Schema\TypeFactory; +use Magento\Framework\GraphQl\Schema\Type\TypeRegistry; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\Phrase; /** - * Map type names to their output type/interface classes. + * Map type names to their output type/interface/enum classes. */ class OutputMapper { /** - * @var OutputFactory + * @var TypeRegistry */ - private $outputFactory; + private $typeRegistry; /** - * @var OutputTypeInterface[] - */ - private $outputTypes; - - /** - * @var TypeFactory - */ - private $typeFactory; - - /** - * @var ConfigInterface - */ - private $config; - - /** - * @param OutputFactory $outputFactory - * @param TypeFactory $typeFactory - * @param ConfigInterface $config + * @param TypeRegistry $typeRegistry */ public function __construct( - OutputFactory $outputFactory, - TypeFactory $typeFactory, - ConfigInterface $config + TypeRegistry $typeRegistry ) { - $this->outputFactory = $outputFactory; - $this->config = $config; - $this->typeFactory = $typeFactory; + $this->typeRegistry = $typeRegistry; } /** @@ -62,16 +40,13 @@ public function __construct( */ public function getOutputType($typeName) { - if (!isset($this->outputTypes[$typeName])) { - $configElement = $this->config->getConfigElement($typeName); - $this->outputTypes[$typeName] = $this->outputFactory->create($configElement); - if (!($this->outputTypes[$typeName] instanceof OutputTypeInterface)) { - throw new GraphQlInputException( - new Phrase("Type '{$typeName}' was requested but is not declared in the GraphQL schema.") - ); - } - } + $outputType = $this->typeRegistry->get($typeName); - return $this->outputTypes[$typeName]; + if (!$outputType instanceof OutputTypeInterface) { + throw new GraphQlInputException( + new Phrase("Type '{$typeName}' was requested but is not declared in the GraphQL schema.") + ); + } + return $outputType; } } diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/TypeRegistry.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/TypeRegistry.php new file mode 100644 index 000000000000..cde8b6b3e446 --- /dev/null +++ b/lib/internal/Magento/Framework/GraphQl/Schema/Type/TypeRegistry.php @@ -0,0 +1,95 @@ +objectManager = $objectManager; + $this->config = $config; + $this->configToTypeMap = $configToTypeMap; + } + + /** + * Get GraphQL type object by type name + * + * @param string $typeName + * @return TypeInterface|InputTypeInterface|OutputTypeInterface + * @throws GraphQlInputException + */ + public function get(string $typeName): TypeInterface + { + if (!isset($this->types[$typeName])) { + $configElement = $this->config->getConfigElement($typeName); + + $configElementClass = get_class($configElement); + if (!isset($this->configToTypeMap[$configElementClass])) { + throw new GraphQlInputException( + new Phrase( + "No mapping to Webonyx type is declared for '%1' config element type.", + [$configElementClass] + ) + ); + } + + $this->types[$typeName] = $this->objectManager->create( + $this->configToTypeMap[$configElementClass], + [ + 'configElement' => $configElement, + ] + ); + + if (!($this->types[$typeName] instanceof TypeInterface)) { + throw new GraphQlInputException( + new Phrase("Type '{$typeName}' was requested but is not declared in the GraphQL schema.") + ); + } + } + return $this->types[$typeName]; + } +} diff --git a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/MetaReader/CacheTagReader.php b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/MetaReader/CacheTagReader.php new file mode 100644 index 000000000000..2613b2829e79 --- /dev/null +++ b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/MetaReader/CacheTagReader.php @@ -0,0 +1,50 @@ +name->value == 'cache') { + foreach ($directive->arguments as $directiveArgument) { + if ($directiveArgument->name->value == 'cacheTag') { + $argMap = array_merge( + $argMap, + ["cacheTag" => $directiveArgument->value->value] + ); + } + if ($directiveArgument->name->value == 'cacheable') { + $argMap = array_merge( + $argMap, + ["cacheable" => $directiveArgument->value->value] + ); + } + if ($directiveArgument->name->value == 'cacheIdentity') { + $argMap = array_merge( + $argMap, + ["cacheIdentity" => $directiveArgument->value->value] + ); + } + } + } + } + return $argMap; + } +} diff --git a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/MetaReader/FieldMetaReader.php b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/MetaReader/FieldMetaReader.php index 736a94471100..554d2636cf8c 100644 --- a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/MetaReader/FieldMetaReader.php +++ b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/MetaReader/FieldMetaReader.php @@ -7,8 +7,6 @@ namespace Magento\Framework\GraphQlSchemaStitching\GraphQlReader\MetaReader; -use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\MetaReader\TypeMetaWrapperReader; - /** * Reads fields and possible arguments from a meta field */ @@ -24,14 +22,25 @@ class FieldMetaReader */ private $docReader; + /** + * @var CacheTagReader + */ + private $cacheTagReader; + /** * @param TypeMetaWrapperReader $typeMetaReader * @param DocReader $docReader + * @param CacheTagReader|null $cacheTagReader */ - public function __construct(TypeMetaWrapperReader $typeMetaReader, DocReader $docReader) - { + public function __construct( + TypeMetaWrapperReader $typeMetaReader, + DocReader $docReader, + CacheTagReader $cacheTagReader = null + ) { $this->typeMetaReader = $typeMetaReader; $this->docReader = $docReader; + $this->cacheTagReader = $cacheTagReader ?? \Magento\Framework\App\ObjectManager::getInstance() + ->get(CacheTagReader::class); } /** @@ -63,6 +72,10 @@ public function read(\GraphQL\Type\Definition\FieldDefinition $fieldMeta) : arra $result['description'] = $this->docReader->read($fieldMeta->astNode->directives); } + if ($this->docReader->read($fieldMeta->astNode->directives)) { + $result['cache'] = $this->cacheTagReader->read($fieldMeta->astNode->directives); + } + $arguments = $fieldMeta->args; foreach ($arguments as $argumentMeta) { $argumentName = $argumentMeta->name; diff --git a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/InputObjectType.php b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/InputObjectType.php index 3aea555e67f9..2eda79ce68b0 100644 --- a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/InputObjectType.php +++ b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/InputObjectType.php @@ -10,6 +10,7 @@ use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\TypeMetaReaderInterface; use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\MetaReader\TypeMetaWrapperReader; use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\MetaReader\DocReader; +use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\MetaReader\CacheTagReader; /** * Composite configuration reader to handle the input object type meta @@ -26,18 +27,29 @@ class InputObjectType implements TypeMetaReaderInterface */ private $docReader; + /** + * @var CacheTagReader + */ + private $cacheTagReader; + /** * @param TypeMetaWrapperReader $typeMetaReader * @param DocReader $docReader + * @param CacheTagReader|null $cacheTagReader */ - public function __construct(TypeMetaWrapperReader $typeMetaReader, DocReader $docReader) - { + public function __construct( + TypeMetaWrapperReader $typeMetaReader, + DocReader $docReader, + CacheTagReader $cacheTagReader = null + ) { $this->typeMetaReader = $typeMetaReader; $this->docReader = $docReader; + $this->cacheTagReader = $cacheTagReader ?? \Magento\Framework\App\ObjectManager::getInstance() + ->get(CacheTagReader::class); } /** - * {@inheritdoc} + * @inheritdoc */ public function read(\GraphQL\Type\Definition\Type $typeMeta) : array { @@ -56,6 +68,10 @@ public function read(\GraphQL\Type\Definition\Type $typeMeta) : array if ($this->docReader->read($typeMeta->astNode->directives)) { $result['description'] = $this->docReader->read($typeMeta->astNode->directives); } + + if ($this->docReader->read($typeMeta->astNode->directives)) { + $result['cache'] = $this->cacheTagReader->read($typeMeta->astNode->directives); + } return $result; } else { return []; @@ -63,6 +79,8 @@ public function read(\GraphQL\Type\Definition\Type $typeMeta) : array } /** + * Read the input's meta data + * * @param \GraphQL\Type\Definition\InputObjectField $fieldMeta * @return array */ diff --git a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/InterfaceType.php b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/InterfaceType.php index dd934ffebc2c..7c040cd2e104 100644 --- a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/InterfaceType.php +++ b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/InterfaceType.php @@ -10,6 +10,7 @@ use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\TypeMetaReaderInterface; use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\MetaReader\FieldMetaReader; use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\MetaReader\DocReader; +use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\MetaReader\CacheTagReader; /** * Composite configuration reader to handle the interface object type meta @@ -26,18 +27,29 @@ class InterfaceType implements TypeMetaReaderInterface */ private $docReader; + /** + * @var CacheTagReader + */ + private $cacheTagReader; + /** * @param FieldMetaReader $fieldMetaReader * @param DocReader $docReader + * @param CacheTagReader|null $cacheTagReader */ - public function __construct(FieldMetaReader $fieldMetaReader, DocReader $docReader) - { + public function __construct( + FieldMetaReader $fieldMetaReader, + DocReader $docReader, + CacheTagReader $cacheTagReader = null + ) { $this->fieldMetaReader = $fieldMetaReader; $this->docReader = $docReader; + $this->cacheTagReader = $cacheTagReader ?? \Magento\Framework\App\ObjectManager::getInstance() + ->get(CacheTagReader::class); } /** - * {@inheritdoc} + * @inheritdoc */ public function read(\GraphQL\Type\Definition\Type $typeMeta) : array { @@ -63,6 +75,10 @@ public function read(\GraphQL\Type\Definition\Type $typeMeta) : array $result['description'] = $this->docReader->read($typeMeta->astNode->directives); } + if ($this->docReader->read($typeMeta->astNode->directives)) { + $result['cache'] = $this->cacheTagReader->read($typeMeta->astNode->directives); + } + return $result; } else { return []; diff --git a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/ObjectType.php b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/ObjectType.php index fb015922087b..77a44460f00a 100644 --- a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/ObjectType.php +++ b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/ObjectType.php @@ -11,6 +11,7 @@ use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\MetaReader\FieldMetaReader; use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\MetaReader\DocReader; use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\MetaReader\ImplementsReader; +use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\MetaReader\CacheTagReader; /** * Composite configuration reader to handle the object type meta @@ -33,22 +34,32 @@ class ObjectType implements TypeMetaReaderInterface private $implementsAnnotation; /** + * @var CacheTagReader + */ + private $cacheTagReader; + + /** + * ObjectType constructor. * @param FieldMetaReader $fieldMetaReader * @param DocReader $docReader * @param ImplementsReader $implementsAnnotation + * @param CacheTagReader|null $cacheTagReader */ public function __construct( FieldMetaReader $fieldMetaReader, DocReader $docReader, - ImplementsReader $implementsAnnotation + ImplementsReader $implementsAnnotation, + CacheTagReader $cacheTagReader = null ) { $this->fieldMetaReader = $fieldMetaReader; $this->docReader = $docReader; $this->implementsAnnotation = $implementsAnnotation; + $this->cacheTagReader = $cacheTagReader ?? \Magento\Framework\App\ObjectManager::getInstance() + ->get(CacheTagReader::class); } /** - * {@inheritdoc} + * @inheritdoc */ public function read(\GraphQL\Type\Definition\Type $typeMeta) : array { @@ -77,6 +88,10 @@ public function read(\GraphQL\Type\Definition\Type $typeMeta) : array $result['description'] = $this->docReader->read($typeMeta->astNode->directives); } + if ($this->docReader->read($typeMeta->astNode->directives)) { + $result['cache'] = $this->cacheTagReader->read($typeMeta->astNode->directives); + } + return $result; } else { return []; diff --git a/lib/internal/Magento/Framework/HTTP/Adapter/Curl.php b/lib/internal/Magento/Framework/HTTP/Adapter/Curl.php index 15f09b850520..f2a703a193e2 100644 --- a/lib/internal/Magento/Framework/HTTP/Adapter/Curl.php +++ b/lib/internal/Magento/Framework/HTTP/Adapter/Curl.php @@ -4,13 +4,11 @@ * See COPYING.txt for license details. */ -/** - * HTTP CURL Adapter - * - * @author Magento Core Team - */ namespace Magento\Framework\HTTP\Adapter; +/** + * Curl http adapter + */ class Curl implements \Zend_Http_Client_Adapter_Interface { /** @@ -112,8 +110,8 @@ public function setOptions(array $options = []) /** * Add additional option to cURL * - * @param int $option the CURLOPT_* constants - * @param mixed $value + * @param int $option the CURLOPT_* constants + * @param mixed $value * @return $this */ public function addOption($option, $value) @@ -139,8 +137,8 @@ public function setConfig($config = []) /** * Connect to the remote server * - * @param string $host - * @param int $port + * @param string $host + * @param int $port * @param boolean $secure * @return $this * @SuppressWarnings(PHPMD.UnusedFormalParameter) @@ -180,6 +178,12 @@ public function write($method, $url, $http_ver = '1.1', $headers = [], $body = ' curl_setopt($this->_getResource(), CURLOPT_CUSTOMREQUEST, 'GET'); } + if ($http_ver === \Zend_Http_Client::HTTP_1) { + curl_setopt($this->_getResource(), CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + } elseif ($http_ver === \Zend_Http_Client::HTTP_0) { + curl_setopt($this->_getResource(), CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); + } + if (is_array($headers)) { curl_setopt($this->_getResource(), CURLOPT_HTTPHEADER, $headers); } @@ -273,7 +277,7 @@ public function getInfo($opt = 0) } /** - * curl_multi_* requests support + * Curl_multi_* requests support * * @param array $urls * @param array $options diff --git a/lib/internal/Magento/Framework/HTTP/Client/Curl.php b/lib/internal/Magento/Framework/HTTP/Client/Curl.php index 788439aa4ff0..0ac65a420ddc 100644 --- a/lib/internal/Magento/Framework/HTTP/Client/Curl.php +++ b/lib/internal/Magento/Framework/HTTP/Client/Curl.php @@ -161,6 +161,7 @@ public function removeHeader($name) /** * Authorization: Basic header + * * Login credentials support * * @param string $login username @@ -209,6 +210,7 @@ public function setCookies($cookies) /** * Clear cookies + * * @return void */ public function removeCookies() @@ -293,6 +295,7 @@ public function getCookies() /** * Get cookies array with details * (domain, expire time etc) + * * @return array */ public function getCookiesFull() @@ -327,6 +330,7 @@ public function getCookiesFull() /** * Get response status code + * * @see lib\Magento\Framework\HTTP\Client#getStatus() * * @return int @@ -345,6 +349,7 @@ public function getStatus() * @param string $method * @param string $uri * @param array|string $params - use $params as a string in case of JSON or XML POST request. + * * @return void * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) @@ -410,6 +415,7 @@ protected function makeRequest($method, $uri, $params = []) /** * Throw error exception + * * @param string $string * @return void * @throws \Exception @@ -432,10 +438,10 @@ protected function parseHeaders($ch, $data) { if ($this->_headerCount == 0) { $line = explode(" ", trim($data), 3); - if (count($line) != 3) { + if (count($line) < 2) { $this->doError("Invalid response line returned from server: " . $data); } - $this->_responseStatus = intval($line[1]); + $this->_responseStatus = (int)$line[1]; } else { $name = $value = ''; $out = explode(": ", trim($data), 2); @@ -474,6 +480,7 @@ protected function curlOption($name, $value) /** * Set curl options array directly + * * @param array $arr * @return void */ @@ -484,6 +491,7 @@ protected function curlOptions($arr) /** * Set CURL options overrides array + * * @param array $arr * @return void */ diff --git a/lib/internal/Magento/Framework/HTTP/Client/Socket.php b/lib/internal/Magento/Framework/HTTP/Client/Socket.php index d229baa5dd47..eba182b98b93 100644 --- a/lib/internal/Magento/Framework/HTTP/Client/Socket.php +++ b/lib/internal/Magento/Framework/HTTP/Client/Socket.php @@ -12,9 +12,8 @@ namespace Magento\Framework\HTTP\Client; /** - * @SuppressWarnings(PHPMD.UnusedPrivateField) - */ -/** + * Socket client + * * @SuppressWarnings(PHPMD.UnusedPrivateField) */ class Socket implements \Magento\Framework\HTTP\ClientInterface @@ -134,6 +133,7 @@ public function disconnect() /** * Set headers from hash + * * @param array $headers * @return void */ @@ -167,6 +167,7 @@ public function removeHeader($name) /** * Authorization: Basic header + * * Login credentials support * * @param string $login username @@ -235,8 +236,7 @@ public function get($uri) } /** - * Set host, port from full url - * and return relative url + * Set host, port from full url and return relative url * * @param string $uri ex. http://google.com/index.php?a=b * @return string ex. /index.php?a=b @@ -330,6 +330,7 @@ public function getCookies() /** * Get cookies array with details * (domain, expire time etc) + * * @return array */ public function getCookiesFull() @@ -424,7 +425,7 @@ protected function processResponse() if (count($line) != 3) { return $this->doError("Invalid response line returned from server: " . $responseLine); } - $this->_responseStatus = intval($line[1]); + $this->_responseStatus = (int)$line[1]; $this->processResponseHeaders(); $this->processRedirect(); @@ -444,6 +445,7 @@ protected function processRedirect() /** * Get response status code + * * @see \Magento\Framework\HTTP\Client#getStatus() * * @return int @@ -494,6 +496,7 @@ protected function makeRequest($method, $uri, $params = []) /** * Throw error exception + * * @param string $string * @return void * @throws \Exception @@ -505,6 +508,7 @@ public function doError($string) /** * Convert headers hash to string + * * @param array $append * @return string */ diff --git a/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Request.php b/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Request.php index 4dd358783a50..3ecf360f3689 100644 --- a/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Request.php +++ b/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Request.php @@ -14,7 +14,10 @@ use Zend\Uri\UriInterface; /** + * HTTP Request for current PHP environment. + * * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class Request extends \Zend\Http\PhpEnvironment\Request { @@ -586,6 +589,7 @@ public function setPostValue($name, $value = null) /** * Access values contained in the superglobals as public members + * * Order of precedence: 1. GET, 2. POST, 3. COOKIE, 4. SERVER, 5. ENV * * @see http://msdn.microsoft.com/en-us/library/system.web.httprequest.item.aspx @@ -683,7 +687,7 @@ public function has($key) * * @param string $name Header name to retrieve. * @param mixed|null $default Default value to use when the requested header is missing. - * @return bool|HeaderInterface + * @return bool|string */ public function getHeader($name, $default = false) { @@ -795,6 +799,8 @@ public function getBaseUrl() } /** + * Get flag value for whether the request is forwarded or not. + * * @return bool * @codeCoverageIgnore */ @@ -804,6 +810,8 @@ public function isForwarded() } /** + * Set flag value for whether the request is forwarded or not. + * * @param bool $forwarded * @return $this * @codeCoverageIgnore diff --git a/lib/internal/Magento/Framework/Image.php b/lib/internal/Magento/Framework/Image.php index 5dc1969559fd..b3867c0197b7 100644 --- a/lib/internal/Magento/Framework/Image.php +++ b/lib/internal/Magento/Framework/Image.php @@ -262,7 +262,7 @@ public function instruction() public function setImageBackgroundColor($color) { /** @noinspection PhpUndefinedFieldInspection */ - $this->_adapter->imageBackgroundColor = intval($color); + $this->_adapter->imageBackgroundColor = (int)$color; } /** diff --git a/lib/internal/Magento/Framework/Image/Adapter/Config.php b/lib/internal/Magento/Framework/Image/Adapter/Config.php index ecba84b28bb4..636bbcdcbdb4 100644 --- a/lib/internal/Magento/Framework/Image/Adapter/Config.php +++ b/lib/internal/Magento/Framework/Image/Adapter/Config.php @@ -3,14 +3,31 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Framework\Image\Adapter; -class Config implements \Magento\Framework\Image\Adapter\ConfigInterface +/** + * Image config provider. + */ +class Config implements ConfigInterface, UploadConfigInterface { const XML_PATH_IMAGE_ADAPTER = 'dev/image/default_adapter'; const XML_PATH_IMAGE_ADAPTERS = 'dev/image/adapters'; + /** + * Config path for the maximal image width value + * @deprecated + */ + const XML_PATH_MAX_WIDTH_IMAGE = 'system/upload_configuration/max_width'; + + /** + * Config path for the maximal image height value + * @deprecated + */ + const XML_PATH_MAX_HEIGHT_IMAGE = 'system/upload_configuration/max_height'; + /** * @var \Magento\Framework\App\Config\ScopeConfigInterface */ @@ -43,4 +60,28 @@ public function getAdapters() { return $this->config->getValue(self::XML_PATH_IMAGE_ADAPTERS); } + + /** + * Get Maximum Image Width resolution in pixels. For image resizing on client side. + * + * @return int + * @deprecated + * @see \Magento\Backend\Model\Image\UploadResizeConfigInterface::getMaxHeight() + */ + public function getMaxWidth(): int + { + return (int)$this->config->getValue(self::XML_PATH_MAX_WIDTH_IMAGE); + } + + /** + * Get Maximum Image Height resolution in pixels. For image resizing on client side. + * + * @return int + * @deprecated + * @see \Magento\Backend\Model\Image\UploadResizeConfigInterface::getMaxHeight() + */ + public function getMaxHeight(): int + { + return (int)$this->config->getValue(self::XML_PATH_MAX_HEIGHT_IMAGE); + } } diff --git a/lib/internal/Magento/Framework/Image/Adapter/Gd2.php b/lib/internal/Magento/Framework/Image/Adapter/Gd2.php index ea21faf3f340..cfba1820bec0 100644 --- a/lib/internal/Magento/Framework/Image/Adapter/Gd2.php +++ b/lib/internal/Magento/Framework/Image/Adapter/Gd2.php @@ -5,6 +5,11 @@ */ namespace Magento\Framework\Image\Adapter; +/** + * Gd2 adapter. + * + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + */ class Gd2 extends \Magento\Framework\Image\Adapter\AbstractAdapter { /** @@ -66,6 +71,16 @@ public function open($filename) $this->_getCallback('create', null, sprintf('Unsupported image format. File: %s', $this->_fileName)), $this->_fileName ); + $fileType = $this->getImageType(); + if (in_array($fileType, [IMAGETYPE_PNG, IMAGETYPE_GIF])) { + $this->_keepTransparency = true; + if ($this->_imageHandler) { + $isAlpha = $this->checkAlpha($this->_fileName); + if ($isAlpha) { + $this->_fillBackgroundColor($this->_imageHandler); + } + } + } } /** @@ -112,6 +127,7 @@ protected function _getImageNeedMemorySize($file) /** * Converts memory value (e.g. 64M, 129K) to bytes. + * * Case insensitive value might be used. * * @param string $memoryValue @@ -132,6 +148,7 @@ protected function _convertToByte($memoryValue) /** * Save image to specific path. + * * If some folders of path does not exist they will be created * * @param null|string $destination @@ -188,7 +205,10 @@ public function save($destination = null, $newName = null) } /** + * Render image and return its binary contents. + * * @see \Magento\Framework\Image\Adapter\AbstractAdapter::getImage + * * @return string */ public function getImage() @@ -223,6 +243,7 @@ private function _getCallback($callbackType, $fileType = null, $unsupportedText /** * Fill image with main background color. + * * Returns a color identifier. * * @param resource &$imageResourceTo @@ -425,7 +446,6 @@ public function watermark($imagePath, $positionX = 0, $positionY = 0, $opacity = $col = imagecolorallocate($newWatermark, 255, 255, 255); imagecolortransparent($newWatermark, $col); imagefilledrectangle($newWatermark, 0, 0, $this->getWatermarkWidth(), $this->getWatermarkHeight(), $col); - imagealphablending($newWatermark, true); imagesavealpha($newWatermark, true); imagecopyresampled( $newWatermark, @@ -450,7 +470,6 @@ public function watermark($imagePath, $positionX = 0, $positionY = 0, $opacity = $col = imagecolorallocate($newWatermark, 255, 255, 255); imagecolortransparent($newWatermark, $col); imagefilledrectangle($newWatermark, 0, 0, $this->_imageSrcWidth, $this->_imageSrcHeight, $col); - imagealphablending($newWatermark, true); imagesavealpha($newWatermark, true); imagecopyresampled( $newWatermark, @@ -730,6 +749,7 @@ protected function _createImageFromText($text) /** * Create Image using ttf font + * * Note: This function requires both the GD library and the FreeType library * * @param string $text diff --git a/lib/internal/Magento/Framework/Image/Adapter/UploadConfigInterface.php b/lib/internal/Magento/Framework/Image/Adapter/UploadConfigInterface.php new file mode 100644 index 000000000000..0a2dbefff8ee --- /dev/null +++ b/lib/internal/Magento/Framework/Image/Adapter/UploadConfigInterface.php @@ -0,0 +1,30 @@ +_getMethodParameterInfo($parameter); } - $returnType = $method->getReturnType(); - $returnTypeValue = $returnType - ? ($returnType->allowsNull() ? '?' : '') .$returnType->getName() - : null; + $returnTypeValue = $this->getReturnTypeValue($method->getReturnType()); $methodInfo = [ 'name' => ($method->returnsReference() ? '& ' : '') . $method->getName(), 'parameters' => $parameters, @@ -137,6 +141,8 @@ protected function _getMethodInfo(\ReflectionMethod $method) } /** + * Return parameters list + * * @param array $parameters * @return string */ @@ -175,14 +181,16 @@ protected function _generateCode() } else { $this->_classGenerator->setExtendedClass($typeName); } - $this->_classGenerator->addTrait('\\'. \Magento\Framework\Interception\Interceptor::class); - $interfaces[] = '\\'. \Magento\Framework\Interception\InterceptorInterface::class; + $this->_classGenerator->addTrait('\\' . \Magento\Framework\Interception\Interceptor::class); + $interfaces[] = '\\' . \Magento\Framework\Interception\InterceptorInterface::class; $this->_classGenerator->setImplementedInterfaces($interfaces); return parent::_generateCode(); } /** - * {@inheritdoc} + * Validates data + * + * @return bool */ protected function _validateData() { @@ -205,4 +213,22 @@ protected function _validateData() } return $result; } + + /** + * Returns return type + * + * @param mixed $returnType + * @return null|string + */ + private function getReturnTypeValue($returnType): ?string + { + $returnTypeValue = null; + if ($returnType) { + $returnTypeValue = ($returnType->allowsNull() ? '?' : ''); + $returnTypeValue .= ($returnType->getName() === 'self') + ? $this->getSourceClassName() + : $returnType->getName(); + } + return $returnTypeValue; + } } diff --git a/lib/internal/Magento/Framework/Interception/Config/CacheManager.php b/lib/internal/Magento/Framework/Interception/Config/CacheManager.php new file mode 100644 index 000000000000..fd612370d075 --- /dev/null +++ b/lib/internal/Magento/Framework/Interception/Config/CacheManager.php @@ -0,0 +1,114 @@ +cache = $cache; + $this->serializer = $serializer; + $this->configWriter = $configWriter; + $this->compiledLoader = $compiledLoader; + } + + /** + * Load the interception config from cache + * + * @param string $key + * @return array|null + */ + public function load(string $key): ?array + { + if ($this->isCompiled($key)) { + return $this->compiledLoader->load($key); + } + + $intercepted = $this->cache->load($key); + return $intercepted ? $this->serializer->unserialize($intercepted) : null; + } + + /** + * Save config to cache backend + * + * @param string $key + * @param array $data + */ + public function save(string $key, array $data) + { + $this->cache->save($this->serializer->serialize($data), $key); + } + + /** + * Save config to filesystem + * + * @param string $key + * @param array $data + */ + public function saveCompiled(string $key, array $data) + { + $this->configWriter->write($key, $data); + } + + /** + * Purge interception cache + * + * @param string $key + */ + public function clean(string $key) + { + $this->cache->remove($key); + } + + /** + * Check for the compiled config with the generated metadata + * + * @param string $key + * @return bool + */ + private function isCompiled(string $key): bool + { + return file_exists(\Magento\Framework\App\ObjectManager\ConfigLoader\Compiled::getFilePath($key)); + } +} diff --git a/lib/internal/Magento/Framework/Interception/Config/Config.php b/lib/internal/Magento/Framework/Interception/Config/Config.php index 7c80051537ba..3f16e9275bd0 100644 --- a/lib/internal/Magento/Framework/Interception/Config/Config.php +++ b/lib/internal/Magento/Framework/Interception/Config/Config.php @@ -1,15 +1,17 @@ _omConfig = $omConfig; $this->_relations = $relations; @@ -107,13 +113,13 @@ public function __construct( $this->_cacheId = $cacheId; $this->_reader = $reader; $this->_scopeList = $scopeList; - $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(Serialize::class); - $intercepted = $this->_cache->load($this->_cacheId); - if ($intercepted !== false) { - $this->_intercepted = $this->serializer->unserialize($intercepted); + $this->cacheManager = + $cacheManager ?? \Magento\Framework\App\ObjectManager::getInstance()->get(CacheManager::class); + $intercepted = $this->cacheManager->load($cacheId); + if ($intercepted !== null) { + $this->_intercepted = $intercepted; } else { - $this->initialize($this->_classDefinitions->getClasses()); + $this->initializeUncompiled($this->_classDefinitions->getClasses()); } } @@ -125,24 +131,9 @@ public function __construct( */ public function initialize($classDefinitions = []) { - $this->_cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [$this->_cacheId]); - $config = []; - foreach ($this->_scopeList->getAllScopes() as $scope) { - $config = array_replace_recursive($config, $this->_reader->read($scope)); - } - unset($config['preferences']); - foreach ($config as $typeName => $typeConfig) { - if (!empty($typeConfig['plugins'])) { - $this->_intercepted[ltrim($typeName, '\\')] = true; - } - } - foreach ($config as $typeName => $typeConfig) { - $this->hasPlugins($typeName); - } - foreach ($classDefinitions as $class) { - $this->hasPlugins($class); - } - $this->_cache->save($this->serializer->serialize($this->_intercepted), $this->_cacheId); + $this->generateIntercepted($classDefinitions); + + $this->cacheManager->saveCompiled($this->_cacheId, $this->_intercepted); } /** @@ -179,7 +170,7 @@ protected function _inheritInterception($type) } /** - * {@inheritdoc} + * @inheritdoc */ public function hasPlugins($type) { @@ -188,4 +179,43 @@ public function hasPlugins($type) } return $this->_inheritInterception($type); } + + /** + * Write interception config to cache + * + * @param array $classDefinitions + */ + private function initializeUncompiled($classDefinitions = []) + { + $this->cacheManager->clean($this->_cacheId); + + $this->generateIntercepted($classDefinitions); + + $this->cacheManager->save($this->_cacheId, $this->_intercepted); + } + + /** + * Generate intercepted array to store in compiled metadata or frontend cache + * + * @param array $classDefinitions + */ + private function generateIntercepted($classDefinitions) + { + $config = []; + foreach ($this->_scopeList->getAllScopes() as $scope) { + $config = array_replace_recursive($config, $this->_reader->read($scope)); + } + unset($config['preferences']); + foreach ($config as $typeName => $typeConfig) { + if (!empty($typeConfig['plugins'])) { + $this->_intercepted[ltrim($typeName, '\\')] = true; + } + } + foreach ($config as $typeName => $typeConfig) { + $this->hasPlugins($typeName); + } + foreach ($classDefinitions as $class) { + $this->hasPlugins($class); + } + } } diff --git a/lib/internal/Magento/Framework/Interception/Interceptor.php b/lib/internal/Magento/Framework/Interception/Interceptor.php index 07600c516818..df1b68023422 100644 --- a/lib/internal/Magento/Framework/Interception/Interceptor.php +++ b/lib/internal/Magento/Framework/Interception/Interceptor.php @@ -62,9 +62,14 @@ public function ___callParent($method, array $arguments) * Calls parent class sleep if defined, otherwise provides own implementation * * @return array + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + if (method_exists(get_parent_class($this), '__sleep')) { $properties = parent::__sleep(); } else { @@ -78,9 +83,14 @@ public function __sleep() * Causes Interceptor to be initialized * * @return void + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + if (method_exists(get_parent_class($this), '__wakeup')) { parent::__wakeup(); } diff --git a/lib/internal/Magento/Framework/Interception/PluginList/PluginList.php b/lib/internal/Magento/Framework/Interception/PluginList/PluginList.php index e21841b48bc1..a4f728454a52 100644 --- a/lib/internal/Magento/Framework/Interception/PluginList/PluginList.php +++ b/lib/internal/Magento/Framework/Interception/PluginList/PluginList.php @@ -30,7 +30,7 @@ class PluginList extends Scoped implements InterceptionPluginList * * @var array */ - protected $_inherited; + protected $_inherited = []; /** * Inherited plugin data, preprocessed for read diff --git a/lib/internal/Magento/Framework/Interception/Test/Unit/Config/ConfigTest.php b/lib/internal/Magento/Framework/Interception/Test/Unit/Config/ConfigTest.php index fe8d29bd0d51..61eb2e62091e 100644 --- a/lib/internal/Magento/Framework/Interception/Test/Unit/Config/ConfigTest.php +++ b/lib/internal/Magento/Framework/Interception/Test/Unit/Config/ConfigTest.php @@ -31,11 +31,6 @@ class ConfigTest extends \PHPUnit\Framework\TestCase */ private $readerMock; - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $cacheMock; - /** * @var \PHPUnit_Framework_MockObject_MockObject */ @@ -51,8 +46,10 @@ class ConfigTest extends \PHPUnit\Framework\TestCase */ private $relationsMock; - /** @var SerializerInterface|\PHPUnit_Framework_MockObject_MockObject */ - private $serializerMock; + /** + * @var \Magento\Framework\Interception\Config\CacheManager|\PHPUnit_Framework_MockObject_MockObject + */ + private $cacheManagerMock; /** @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager */ private $objectManagerHelper; @@ -61,7 +58,6 @@ protected function setUp() { $this->readerMock = $this->createMock(\Magento\Framework\ObjectManager\Config\Reader\Dom::class); $this->configScopeMock = $this->createMock(\Magento\Framework\Config\ScopeListInterface::class); - $this->cacheMock = $this->createMock(\Magento\Framework\Cache\FrontendInterface::class); $this->omConfigMock = $this->getMockForAbstractClass( \Magento\Framework\Interception\ObjectManager\ConfigInterface::class ); @@ -69,7 +65,7 @@ protected function setUp() $this->relationsMock = $this->getMockForAbstractClass( \Magento\Framework\ObjectManager\RelationsInterface::class ); - $this->serializerMock = $this->createMock(SerializerInterface::class); + $this->cacheManagerMock = $this->createMock(\Magento\Framework\Interception\Config\CacheManager::class); $this->objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); } @@ -88,9 +84,9 @@ public function testHasPluginsWhenDataIsNotCached($expectedResult, $type, $entit ->method('getAllScopes') ->will($this->returnValue(['global', 'backend', 'frontend'])); // turn cache off - $this->cacheMock->expects($this->any()) + $this->cacheManagerMock->expects($this->any()) ->method('load') - ->will($this->returnValue(false)); + ->will($this->returnValue(null)); $this->omConfigMock->expects($this->any()) ->method('getOriginalInstanceType') ->will($this->returnValueMap( @@ -138,21 +134,15 @@ public function testHasPluginsWhenDataIsNotCached($expectedResult, $type, $entit $this->relationsMock->expects($this->any())->method('has')->will($this->returnValue($expectedResult)); $this->relationsMock->expects($this->any())->method('getParents')->will($this->returnValue($entityParents)); - $this->serializerMock->expects($this->once()) - ->method('serialize'); - - $this->serializerMock->expects($this->never())->method('unserialize'); - $model = $this->objectManagerHelper->getObject( \Magento\Framework\Interception\Config\Config::class, [ 'reader' => $this->readerMock, 'scopeList' => $this->configScopeMock, - 'cache' => $this->cacheMock, + 'cacheManager' => $this->cacheManagerMock, 'relations' => $this->relationsMock, 'omConfig' => $this->omConfigMock, 'classDefinitions' => $this->definitionMock, - 'serializer' => $this->serializerMock ] ); @@ -177,32 +167,24 @@ public function testHasPluginsWhenDataIsCached($expectedResult, $type) 'virtual_custom_item' => true ]; $this->readerMock->expects($this->never())->method('read'); - $this->cacheMock->expects($this->never())->method('save'); - $serializedValue = 'serializedData'; - $this->cacheMock->expects($this->any()) + $this->cacheManagerMock->expects($this->never())->method('save'); + $this->cacheManagerMock->expects($this->any()) ->method('load') ->with($cacheId) - ->will($this->returnValue($serializedValue)); - - $this->serializerMock->expects($this->never())->method('serialize'); - $this->serializerMock->expects($this->once()) - ->method('unserialize') - ->with($serializedValue) - ->willReturn($interceptionData); + ->will($this->returnValue($interceptionData)); $model = $this->objectManagerHelper->getObject( \Magento\Framework\Interception\Config\Config::class, [ 'reader' => $this->readerMock, 'scopeList' => $this->configScopeMock, - 'cache' => $this->cacheMock, + 'cacheManager' => $this->cacheManagerMock, 'relations' => $this->objectManagerHelper->getObject( \Magento\Framework\ObjectManager\Relations\Runtime::class ), 'omConfig' => $this->omConfigMock, 'classDefinitions' => $this->definitionMock, 'cacheId' => $cacheId, - 'serializer' => $this->serializerMock ] ); diff --git a/lib/internal/Magento/Framework/Locale/Config.php b/lib/internal/Magento/Framework/Locale/Config.php index 8767bd515db8..fe685fdf35b7 100644 --- a/lib/internal/Magento/Framework/Locale/Config.php +++ b/lib/internal/Magento/Framework/Locale/Config.php @@ -5,6 +5,9 @@ */ namespace Magento\Framework\Locale; +/** + * Allowed locale and currency configuration. + */ class Config implements \Magento\Framework\Locale\ConfigInterface { /** @@ -90,6 +93,7 @@ class Config implements \Magento\Framework\Locale\ConfigInterface 'sq_AL', /*Albanian (Albania)*/ 'sr_Cyrl_RS', /*Serbian (Serbia)*/ 'sv_SE', /*Swedish (Sweden)*/ + 'sv_FI', /*Swedish (Finland)*/ 'sw_KE', /*Swahili (Kenya)*/ 'th_TH', /*Thai (Thailand)*/ 'tr_TR', /*Turkish (Turkey)*/ diff --git a/lib/internal/Magento/Framework/Locale/Format.php b/lib/internal/Magento/Framework/Locale/Format.php index ca50cdb2440f..adcffe01b910 100644 --- a/lib/internal/Magento/Framework/Locale/Format.php +++ b/lib/internal/Magento/Framework/Locale/Format.php @@ -5,6 +5,9 @@ */ namespace Magento\Framework\Locale; +/** + * Price locale format. + */ class Format implements \Magento\Framework\Locale\FormatInterface { /** @@ -38,7 +41,8 @@ public function __construct( } /** - * Returns the first found number from a string + * Returns the first found number from a string. + * * Parsing depends on given locale (grouping and decimal) * * Examples for input: @@ -100,7 +104,7 @@ public function getPriceFormat($localeCode = null, $currencyCode = null) } $formatter = new \NumberFormatter( - $localeCode . '@currency=' . $currency->getCode(), + $currency->getCode() ? $localeCode . '@currency=' . $currency->getCode() : $localeCode, \NumberFormatter::CURRENCY ); $format = $formatter->getPattern(); diff --git a/lib/internal/Magento/Framework/Locale/Resolver.php b/lib/internal/Magento/Framework/Locale/Resolver.php index b401da8960f0..d058bfd41ab1 100644 --- a/lib/internal/Magento/Framework/Locale/Resolver.php +++ b/lib/internal/Magento/Framework/Locale/Resolver.php @@ -6,7 +6,12 @@ namespace Magento\Framework\Locale; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\App\ObjectManager; +/** + * Manages locale config information. + */ class Resolver implements ResolverInterface { /** @@ -52,26 +57,34 @@ class Resolver implements ResolverInterface */ private $defaultLocalePath; + /** + * @var DeploymentConfig + */ + private $deploymentConfig; + /** * @param ScopeConfigInterface $scopeConfig * @param string $defaultLocalePath * @param string $scopeType * @param mixed $locale + * @param DeploymentConfig|null $deploymentConfig */ public function __construct( ScopeConfigInterface $scopeConfig, $defaultLocalePath, $scopeType, - $locale = null + $locale = null, + DeploymentConfig $deploymentConfig = null ) { $this->scopeConfig = $scopeConfig; $this->defaultLocalePath = $defaultLocalePath; $this->scopeType = $scopeType; + $this->deploymentConfig = $deploymentConfig ?: ObjectManager::getInstance()->create(DeploymentConfig::class); $this->setLocale($locale); } /** - * {@inheritdoc} + * @inheritdoc */ public function getDefaultLocalePath() { @@ -79,7 +92,7 @@ public function getDefaultLocalePath() } /** - * {@inheritdoc} + * @inheritdoc */ public function setDefaultLocale($locale) { @@ -88,12 +101,15 @@ public function setDefaultLocale($locale) } /** - * {@inheritdoc} + * @inheritdoc */ public function getDefaultLocale() { if (!$this->defaultLocale) { - $locale = $this->scopeConfig->getValue($this->getDefaultLocalePath(), $this->scopeType); + $locale = false; + if ($this->deploymentConfig->isAvailable() && $this->deploymentConfig->isDbAvailable()) { + $locale = $this->scopeConfig->getValue($this->getDefaultLocalePath(), $this->scopeType); + } if (!$locale) { $locale = self::DEFAULT_LOCALE; } @@ -103,7 +119,7 @@ public function getDefaultLocale() } /** - * {@inheritdoc} + * @inheritdoc */ public function setLocale($locale = null) { @@ -116,7 +132,7 @@ public function setLocale($locale = null) } /** - * {@inheritdoc} + * @inheritdoc */ public function getLocale() { @@ -127,7 +143,7 @@ public function getLocale() } /** - * {@inheritdoc} + * @inheritdoc */ public function emulate($scopeId) { @@ -147,7 +163,7 @@ public function emulate($scopeId) } /** - * {@inheritdoc} + * @inheritdoc */ public function revert() { diff --git a/lib/internal/Magento/Framework/Locale/Test/Unit/FormatTest.php b/lib/internal/Magento/Framework/Locale/Test/Unit/FormatTest.php index 1141f451c13a..73a029a5a141 100644 --- a/lib/internal/Magento/Framework/Locale/Test/Unit/FormatTest.php +++ b/lib/internal/Magento/Framework/Locale/Test/Unit/FormatTest.php @@ -68,15 +68,17 @@ protected function setUp() /** * @param string $localeCode + * @param string $currencyCode * @param array $expectedResult * @dataProvider getPriceFormatDataProvider */ - public function testGetPriceFormat($localeCode, array $expectedResult): void + public function testGetPriceFormat($localeCode, $currencyCode, array $expectedResult): void { $this->scope->expects($this->once()) ->method('getCurrentCurrency') ->willReturn($this->currency); + $this->currency->method('getCode')->willReturn($currencyCode); $result = $this->formatModel->getPriceFormat($localeCode); $intersection = array_intersect_assoc($result, $expectedResult); $this->assertCount(count($expectedResult), $intersection); @@ -88,18 +90,19 @@ public function testGetPriceFormat($localeCode, array $expectedResult): void */ public function getPriceFormatDataProvider(): array { + $swissGroupSymbol = INTL_ICU_VERSION >= 59.1 ? '’' : '\''; return [ - ['en_US', ['decimalSymbol' => '.', 'groupSymbol' => ',']], - ['de_DE', ['decimalSymbol' => ',', 'groupSymbol' => '.']], - ['de_CH', ['decimalSymbol' => '.', 'groupSymbol' => '\'']], - ['uk_UA', ['decimalSymbol' => ',', 'groupSymbol' => ' ']] + ['en_US', 'USD', ['decimalSymbol' => '.', 'groupSymbol' => ',']], + ['de_DE', 'EUR', ['decimalSymbol' => ',', 'groupSymbol' => '.']], + ['de_CH', 'CHF', ['decimalSymbol' => '.', 'groupSymbol' => $swissGroupSymbol]], + ['uk_UA', 'UAH', ['decimalSymbol' => ',', 'groupSymbol' => ' ']] ]; } /** * - * @param mixed $value - * @param float $expected + * @param mixed $value + * @param float $expected * @dataProvider provideNumbers */ public function testGetNumber($value, $expected): void diff --git a/lib/internal/Magento/Framework/Lock/Backend/Cache.php b/lib/internal/Magento/Framework/Lock/Backend/Cache.php new file mode 100644 index 000000000000..dfe6bbb82835 --- /dev/null +++ b/lib/internal/Magento/Framework/Lock/Backend/Cache.php @@ -0,0 +1,69 @@ +cache = $cache; + } + + /** + * @inheritdoc + */ + public function lock(string $name, int $timeout = -1): bool + { + return $this->cache->save('1', $this->getIdentifier($name), [], $timeout); + } + + /** + * @inheritdoc + */ + public function unlock(string $name): bool + { + return $this->cache->remove($this->getIdentifier($name)); + } + + /** + * @inheritdoc + */ + public function isLocked(string $name): bool + { + return (bool)$this->cache->test($this->getIdentifier($name)); + } + + /** + * Get cache locked identifier based on cache identifier. + * + * @param string $cacheIdentifier + * @return string + */ + private function getIdentifier(string $cacheIdentifier): string + { + return self::LOCK_PREFIX . $cacheIdentifier; + } +} diff --git a/lib/internal/Magento/Framework/Lock/Backend/Database.php b/lib/internal/Magento/Framework/Lock/Backend/Database.php new file mode 100644 index 000000000000..efdba63e7a08 --- /dev/null +++ b/lib/internal/Magento/Framework/Lock/Backend/Database.php @@ -0,0 +1,188 @@ +resource = $resource; + $this->deploymentConfig = $deploymentConfig; + $this->prefix = $prefix; + } + + /** + * Sets a lock for name + * + * @param string $name lock name + * @param int $timeout How long to wait lock acquisition in seconds, negative value means infinite timeout + * @return bool + * @throws InputException + * @throws AlreadyExistsException + * @throws \Zend_Db_Statement_Exception + */ + public function lock(string $name, int $timeout = -1): bool + { + if (!$this->deploymentConfig->isDbAvailable()) { + return true; + }; + $name = $this->addPrefix($name); + + /** + * Before MySQL 5.7.5, only a single simultaneous lock per connection can be acquired. + * This limitation can be removed once MySQL minimum requirement has been raised, + * currently we support MySQL 5.6 way only. + */ + if ($this->currentLock) { + throw new AlreadyExistsException( + new Phrase( + 'Current connection is already holding lock for %1, only single lock allowed', + [$this->currentLock] + ) + ); + } + + $result = (bool)$this->resource->getConnection()->query( + "SELECT GET_LOCK(?, ?);", + [(string)$name, (int)$timeout] + )->fetchColumn(); + + if ($result === true) { + $this->currentLock = $name; + } + + return $result; + } + + /** + * Releases a lock for name + * + * @param string $name lock name + * @return bool + * @throws InputException + * @throws \Zend_Db_Statement_Exception + */ + public function unlock(string $name): bool + { + if (!$this->deploymentConfig->isDbAvailable()) { + return true; + }; + + $name = $this->addPrefix($name); + + $result = (bool)$this->resource->getConnection()->query( + "SELECT RELEASE_LOCK(?);", + [(string)$name] + )->fetchColumn(); + + if ($result === true) { + $this->currentLock = false; + } + + return $result; + } + + /** + * Tests of lock is set for name + * + * @param string $name lock name + * @return bool + * @throws InputException + * @throws \Zend_Db_Statement_Exception + */ + public function isLocked(string $name): bool + { + if (!$this->deploymentConfig->isDbAvailable()) { + return false; + }; + + $name = $this->addPrefix($name); + + return (bool)$this->resource->getConnection()->query( + "SELECT IS_USED_LOCK(?);", + [(string)$name] + )->fetchColumn(); + } + + /** + * Adds prefix and checks for max length of lock name + * + * Limited to 64 characters in MySQL. + * + * @param string $name + * @return string + * @throws InputException + */ + private function addPrefix(string $name): string + { + $name = $this->getPrefix() . '|' . $name; + + if (strlen($name) > 64) { + throw new InputException(new Phrase('Lock name too long: %1...', [substr($name, 0, 64)])); + } + + return $name; + } + + /** + * Get installation specific lock prefix to avoid lock conflicts + * + * @return string lock prefix + */ + private function getPrefix(): string + { + if ($this->prefix === null) { + $this->prefix = (string)$this->deploymentConfig->get( + ConfigOptionsListConstants::CONFIG_PATH_DB_CONNECTION_DEFAULT + . '/' + . ConfigOptionsListConstants::KEY_NAME, + '' + ); + } + + return $this->prefix; + } +} diff --git a/lib/internal/Magento/Framework/Lock/Backend/FileLock.php b/lib/internal/Magento/Framework/Lock/Backend/FileLock.php new file mode 100644 index 000000000000..d168e910a4ab --- /dev/null +++ b/lib/internal/Magento/Framework/Lock/Backend/FileLock.php @@ -0,0 +1,194 @@ +fileDriver = $fileDriver; + $this->path = rtrim($path, '/') . '/'; + + try { + if (!$this->fileDriver->isExists($this->path)) { + $this->fileDriver->createDirectory($this->path); + } + } catch (FileSystemException $exception) { + throw new RuntimeException( + new Phrase('Cannot create the directory for locks: %1', [$this->path]), + $exception + ); + } + } + + /** + * Acquires a lock by name + * + * @param string $name The lock name + * @param int $timeout Timeout in seconds. A negative timeout value means infinite timeout + * @return bool Returns true if the lock is acquired, otherwise returns false + * @throws RuntimeException Throws RuntimeException if cannot acquires the lock because FS problems + */ + public function lock(string $name, int $timeout = -1): bool + { + try { + $lockFile = $this->getLockPath($name); + $fileResource = $this->fileDriver->fileOpen($lockFile, 'w+'); + $skipDeadline = $timeout < 0; + $deadline = microtime(true) + $timeout; + + while (!$this->tryToLock($fileResource)) { + if (!$skipDeadline && $deadline <= microtime(true)) { + $this->fileDriver->fileClose($fileResource); + return false; + } + usleep($this->sleepCycle); + } + } catch (FileSystemException $exception) { + throw new RuntimeException(new Phrase('Cannot acquire a lock.'), $exception); + } + + $this->locks[$lockFile] = $fileResource; + return true; + } + + /** + * Checks if a lock exists by name + * + * @param string $name The lock name + * @return bool Returns true if the lock exists, otherwise returns false + * @throws RuntimeException Throws RuntimeException if cannot check that the lock exists + */ + public function isLocked(string $name): bool + { + $lockFile = $this->getLockPath($name); + $result = false; + + try { + if ($this->fileDriver->isExists($lockFile)) { + $fileResource = $this->fileDriver->fileOpen($lockFile, 'w+'); + if ($this->tryToLock($fileResource)) { + $result = false; + } else { + $result = true; + } + $this->fileDriver->fileClose($fileResource); + } + } catch (FileSystemException $exception) { + throw new RuntimeException(new Phrase('Cannot verify that the lock exists.'), $exception); + } + + return $result; + } + + /** + * Remove the lock by name + * + * @param string $name The lock name + * @return bool If the lock is removed returns true, otherwise returns false + */ + public function unlock(string $name): bool + { + $lockFile = $this->getLockPath($name); + + if (isset($this->locks[$lockFile]) && $this->tryToUnlock($this->locks[$lockFile])) { + unset($this->locks[$lockFile]); + return true; + } + + return false; + } + + /** + * Returns the full path to the lock file by name + * + * @param string $name The lock name + * @return string The path to the lock file + */ + private function getLockPath(string $name): string + { + return $this->path . $name; + } + + /** + * Tries to lock a file resource + * + * @param resource $resource The file resource + * @return bool If the lock is acquired returns true, otherwise returns false + */ + private function tryToLock($resource): bool + { + try { + return $this->fileDriver->fileLock($resource, LOCK_EX | LOCK_NB); + } catch (FileSystemException $exception) { + return false; + } + } + + /** + * Tries to unlock a file resource + * + * @param resource $resource The file resource + * @return bool If the lock is removed returns true, otherwise returns false + */ + private function tryToUnlock($resource): bool + { + try { + return $this->fileDriver->fileLock($resource, LOCK_UN | LOCK_NB); + } catch (FileSystemException $exception) { + return false; + } + } +} diff --git a/lib/internal/Magento/Framework/Lock/Backend/Zookeeper.php b/lib/internal/Magento/Framework/Lock/Backend/Zookeeper.php new file mode 100644 index 000000000000..cbba981ae1b5 --- /dev/null +++ b/lib/internal/Magento/Framework/Lock/Backend/Zookeeper.php @@ -0,0 +1,280 @@ +\Zookeeper::PERM_ALL, 'scheme' => 'world', 'id' => 'anyone']]; + + /** + * The mapping list of the lock name with the full lock path + * + * @var array + */ + private $locks = []; + + /** + * The default path to storage locks + */ + const DEFAULT_PATH = '/magento/locks'; + + /** + * @param string $host The host to connect to Zookeeper + * @param string $path The base path to locks in Zookeeper + * @throws RuntimeException + */ + public function __construct(string $host, string $path = self::DEFAULT_PATH) + { + if (!$path) { + throw new RuntimeException( + new Phrase('The path needs to be a non-empty string.') + ); + } + + if (!$host) { + throw new RuntimeException( + new Phrase('The host needs to be a non-empty string.') + ); + } + + $this->host = $host; + $this->path = rtrim($path, '/') . '/'; + } + + /** + * @inheritdoc + * + * You can see the lock algorithm by the link + * @link https://zookeeper.apache.org/doc/r3.1.2/recipes.html#sc_recipes_Locks + * + * @throws RuntimeException + */ + public function lock(string $name, int $timeout = -1): bool + { + $skipDeadline = $timeout < 0; + $lockPath = $this->getFullPathToLock($name); + $deadline = microtime(true) + $timeout; + + if (!$this->checkAndCreateParentNode($lockPath)) { + throw new RuntimeException(new Phrase('Failed creating the path %1', [$lockPath])); + } + + $lockKey = $this->getProvider() + ->create($lockPath, '1', $this->acl, \Zookeeper::EPHEMERAL | \Zookeeper::SEQUENCE); + + if (!$lockKey) { + throw new RuntimeException(new Phrase('Failed creating lock %1', [$lockPath])); + } + + while ($this->isAnyLock($lockKey, $this->getIndex($lockKey))) { + if (!$skipDeadline && $deadline <= microtime(true)) { + $this->getProvider()->delete($lockKey); + return false; + } + + usleep($this->sleepCycle); + } + + $this->locks[$name] = $lockKey; + + return true; + } + + /** + * @inheritdoc + * + * @throws RuntimeException + */ + public function unlock(string $name): bool + { + if (!isset($this->locks[$name])) { + return false; + } + + return $this->getProvider()->delete($this->locks[$name]); + } + + /** + * @inheritdoc + * + * @throws RuntimeException + */ + public function isLocked(string $name): bool + { + return $this->isAnyLock($this->getFullPathToLock($name)); + } + + /** + * Gets full path to lock by its name + * + * @param string $name + * @return string + */ + private function getFullPathToLock(string $name): string + { + return $this->path . $name . '/' . $this->lockName; + } + + /** + * Initiolizes and returns Zookeeper provider + * + * @return \Zookeeper + * @throws RuntimeException + */ + private function getProvider(): \Zookeeper + { + if (!$this->zookeeper) { + $this->zookeeper = new \Zookeeper($this->host); + } + + $deadline = microtime(true) + $this->connectionTimeout; + while ($this->zookeeper->getState() != \Zookeeper::CONNECTED_STATE) { + if ($deadline <= microtime(true)) { + throw new RuntimeException(new Phrase('Zookeeper connection timed out!')); + } + usleep($this->sleepCycle); + } + + return $this->zookeeper; + } + + /** + * Checks and creates base path recursively + * + * @param string $path + * @return bool + * @throws RuntimeException + */ + private function checkAndCreateParentNode(string $path): bool + { + $path = dirname($path); + if ($this->getProvider()->exists($path)) { + return true; + } + + if (!$this->checkAndCreateParentNode($path)) { + return false; + } + + if ($this->getProvider()->create($path, '1', $this->acl)) { + return true; + } + + return $this->getProvider()->exists($path); + } + + /** + * Gets int increment of lock key + * + * @param string $key + * @return int|null + */ + private function getIndex(string $key) + { + if (!preg_match('/' . $this->lockName . '([0-9]+)$/', $key, $matches)) { + return null; + } + + return intval($matches[1]); + } + + /** + * Checks if there is any sequence node under parent of $fullKey. + * + * At first checks that the $fullKey node is present, if not - returns false. + * If $indexKey is non-null and there is a smaller index than $indexKey then returns true, + * otherwise returns false. + * + * @param string $fullKey The full path without any sequence info + * @param int|null $indexKey The index to compare + * @return bool + * @throws RuntimeException + */ + private function isAnyLock(string $fullKey, int $indexKey = null): bool + { + $parent = dirname($fullKey); + + if (!$this->getProvider()->exists($parent)) { + return false; + } + + $children = $this->getProvider()->getChildren($parent); + + if (null === $indexKey && !empty($children)) { + return true; + } + + foreach ($children as $childKey) { + $childIndex = $this->getIndex($childKey); + + if (null === $childIndex) { + continue; + } + + if ($childIndex < $indexKey) { + return true; + } + } + + return false; + } +} diff --git a/lib/internal/Magento/Framework/Lock/LockBackendFactory.php b/lib/internal/Magento/Framework/Lock/LockBackendFactory.php new file mode 100644 index 000000000000..b142085ef656 --- /dev/null +++ b/lib/internal/Magento/Framework/Lock/LockBackendFactory.php @@ -0,0 +1,111 @@ + DatabaseLock::class, + self::LOCK_ZOOKEEPER => ZookeeperLock::class, + self::LOCK_CACHE => CacheLock::class, + self::LOCK_FILE => FileLock::class, + ]; + + /** + * @param ObjectManagerInterface $objectManager The Object Manager instance + * @param DeploymentConfig $deploymentConfig The Application deployment configuration + */ + public function __construct( + ObjectManagerInterface $objectManager, + DeploymentConfig $deploymentConfig + ) { + $this->objectManager = $objectManager; + $this->deploymentConfig = $deploymentConfig; + } + + /** + * Creates an instance of LockManagerInterface using information from deployment config + * + * @return LockManagerInterface + * @throws RuntimeException + */ + public function create(): LockManagerInterface + { + $provider = $this->deploymentConfig->get('lock/provider', self::LOCK_DB); + $config = $this->deploymentConfig->get('lock/config', []); + + if (!isset($this->lockers[$provider])) { + throw new RuntimeException(new Phrase('Unknown locks provider: %1', [$provider])); + } + + if (self::LOCK_ZOOKEEPER === $provider && !extension_loaded(self::LOCK_ZOOKEEPER)) { + throw new RuntimeException(new Phrase('php extension Zookeeper is not installed.')); + } + + return $this->objectManager->create($this->lockers[$provider], $config); + } +} diff --git a/lib/internal/Magento/Framework/Lock/LockManagerInterface.php b/lib/internal/Magento/Framework/Lock/LockManagerInterface.php new file mode 100644 index 000000000000..76cc8506eb18 --- /dev/null +++ b/lib/internal/Magento/Framework/Lock/LockManagerInterface.php @@ -0,0 +1,44 @@ +factory = $factory; + } + + /** + * @inheritdoc + * + * @throws RuntimeException + */ + public function isLocked(string $name): bool + { + return $this->getLocker()->isLocked($name); + } + + /** + * @inheritdoc + * + * @throws RuntimeException + */ + public function lock(string $name, int $timeout = -1): bool + { + return $this->getLocker()->lock($name, $timeout); + } + + /** + * @inheritdoc + * + * @throws RuntimeException + */ + public function unlock(string $name): bool + { + return $this->getLocker()->unlock($name); + } + + /** + * Gets LockManagerInterface implementation using Factory + * + * @return LockManagerInterface + * @throws RuntimeException + */ + private function getLocker(): LockManagerInterface + { + if (!$this->locker) { + $this->locker = $this->factory->create(); + } + + return $this->locker; + } +} diff --git a/lib/internal/Magento/Framework/Lock/README.md b/lib/internal/Magento/Framework/Lock/README.md new file mode 100644 index 000000000000..cd5ae425b949 --- /dev/null +++ b/lib/internal/Magento/Framework/Lock/README.md @@ -0,0 +1,8 @@ +# Lock library + +Lock library provides mechanism to acquire Magento system-wide lock. Default implementation is based on MySQL locks, where any locks are automatically released on connection close. + +The library provides interface *LockManagerInterface* which provides following methods: +* *lock* - Acquires a named lock +* *unlock* - Releases a named lock +* *isLocked* - Tests if a named lock exists diff --git a/lib/internal/Magento/Framework/Lock/Test/Unit/Backend/DatabaseTest.php b/lib/internal/Magento/Framework/Lock/Test/Unit/Backend/DatabaseTest.php new file mode 100644 index 000000000000..e2a95030bbd1 --- /dev/null +++ b/lib/internal/Magento/Framework/Lock/Test/Unit/Backend/DatabaseTest.php @@ -0,0 +1,182 @@ +connection = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->resource = $this->getMockBuilder(\Magento\Framework\App\ResourceConnection::class) + ->disableOriginalConstructor() + ->getMock(); + $this->statement = $this->getMockBuilder(\Zend_Db_Statement_Interface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->resource->expects($this->any()) + ->method('getConnection') + ->willReturn($this->connection); + + $this->connection->expects($this->any()) + ->method('query') + ->willReturn($this->statement); + + $this->objectManager = new ObjectManager($this); + $this->deploymentConfig = $this->getMockBuilder(DeploymentConfig::class) + ->disableOriginalConstructor() + ->getMock(); + + /** @var Database $database */ + $this->database = $this->objectManager->getObject( + Database::class, + [ + 'resource' => $this->resource, + 'deploymentConfig' => $this->deploymentConfig, + ] + ); + } + + /** + * @throws \Magento\Framework\Exception\AlreadyExistsException + * @throws \Magento\Framework\Exception\InputException + * @throws \Zend_Db_Statement_Exception + */ + public function testLock() + { + $this->deploymentConfig + ->method('isDbAvailable') + ->with() + ->willReturn(true); + $this->statement->expects($this->once()) + ->method('fetchColumn') + ->willReturn(true); + + $this->assertTrue($this->database->lock('testLock')); + } + + /** + * @throws \Magento\Framework\Exception\AlreadyExistsException + * @throws \Magento\Framework\Exception\InputException + * @throws \Zend_Db_Statement_Exception + * @expectedException \Magento\Framework\Exception\InputException + */ + public function testlockWithTooLongName() + { + $this->deploymentConfig + ->method('isDbAvailable') + ->with() + ->willReturn(true); + $this->database->lock('BbXbyf9rIY5xuAVdviQJmh76FyoeeVHTDpcjmcImNtgpO4Hnz4xk76ZGEyYALvrQu'); + } + + /** + * @throws \Magento\Framework\Exception\AlreadyExistsException + * @throws \Magento\Framework\Exception\InputException + * @throws \Zend_Db_Statement_Exception + * @expectedException \Magento\Framework\Exception\AlreadyExistsException + */ + public function testlockWithAlreadyAcquiredLockInSameSession() + { + $this->deploymentConfig + ->method('isDbAvailable') + ->with() + ->willReturn(true); + $this->statement->expects($this->any()) + ->method('fetchColumn') + ->willReturn(true); + + $this->database->lock('testLock'); + $this->database->lock('differentLock'); + } + + /** + * @throws \Magento\Framework\Exception\AlreadyExistsException + * @throws \Magento\Framework\Exception\InputException + * @throws \Zend_Db_Statement_Exception + */ + public function testLockWithUnavailableDeploymentConfig() + { + $this->deploymentConfig + ->expects($this->atLeast(1)) + ->method('isDbAvailable') + ->with() + ->willReturn(false); + $this->assertTrue($this->database->lock('testLock')); + } + + /** + * @throws \Magento\Framework\Exception\InputException + * @throws \Zend_Db_Statement_Exception + */ + public function testUnlockWithUnavailableDeploymentConfig() + { + $this->deploymentConfig + ->expects($this->atLeast(1)) + ->method('isDbAvailable') + ->with() + ->willReturn(false); + $this->assertTrue($this->database->unlock('testLock')); + } + + /** + * @throws \Magento\Framework\Exception\InputException + * @throws \Zend_Db_Statement_Exception + */ + public function testIsLockedWithUnavailableDB() + { + $this->deploymentConfig + ->expects($this->atLeast(1)) + ->method('isDbAvailable') + ->with() + ->willReturn(false); + $this->assertFalse($this->database->isLocked('testLock')); + } +} diff --git a/lib/internal/Magento/Framework/Lock/Test/Unit/Backend/ZookeeperTest.php b/lib/internal/Magento/Framework/Lock/Test/Unit/Backend/ZookeeperTest.php new file mode 100644 index 000000000000..62521b9de308 --- /dev/null +++ b/lib/internal/Magento/Framework/Lock/Test/Unit/Backend/ZookeeperTest.php @@ -0,0 +1,68 @@ +markTestSkipped('Test was skipped because php extension Zookeeper is not installed.'); + } + } + + /** + * @expectedException \Magento\Framework\Exception\RuntimeException + * @expectedExceptionMessage The path needs to be a non-empty string. + * @return void + */ + public function testConstructionWithPathException() + { + $this->zookeeperProvider = new ZookeeperProvider($this->host, ''); + } + + /** + * @expectedException \Magento\Framework\Exception\RuntimeException + * @expectedExceptionMessage The host needs to be a non-empty string. + * @return void + */ + public function testConstructionWithHostException() + { + $this->zookeeperProvider = new ZookeeperProvider('', $this->path); + } + + /** + * @return void + */ + public function testConstructionWithoutException() + { + $this->zookeeperProvider = new ZookeeperProvider($this->host, $this->path); + } +} diff --git a/lib/internal/Magento/Framework/Lock/Test/Unit/LockBackendFactoryTest.php b/lib/internal/Magento/Framework/Lock/Test/Unit/LockBackendFactoryTest.php new file mode 100644 index 000000000000..ebf2f54f3e09 --- /dev/null +++ b/lib/internal/Magento/Framework/Lock/Test/Unit/LockBackendFactoryTest.php @@ -0,0 +1,116 @@ +objectManagerMock = $this->getMockForAbstractClass(ObjectManagerInterface::class); + $this->deploymentConfigMock = $this->createMock(DeploymentConfig::class); + $this->factory = new LockBackendFactory($this->objectManagerMock, $this->deploymentConfigMock); + } + + /** + * @expectedException \Magento\Framework\Exception\RuntimeException + * @expectedExceptionMessage Unknown locks provider: someProvider + */ + public function testCreateWithException() + { + $this->deploymentConfigMock->expects($this->exactly(2)) + ->method('get') + ->withConsecutive(['lock/provider', LockBackendFactory::LOCK_DB], ['lock/config', []]) + ->willReturnOnConsecutiveCalls('someProvider', []); + + $this->factory->create(); + } + + /** + * @param string $lockProvider + * @param string $lockProviderClass + * @param array $config + * @dataProvider createDataProvider + */ + public function testCreate(string $lockProvider, string $lockProviderClass, array $config) + { + $lockManagerMock = $this->getMockForAbstractClass(LockManagerInterface::class); + $this->deploymentConfigMock->expects($this->exactly(2)) + ->method('get') + ->withConsecutive(['lock/provider', LockBackendFactory::LOCK_DB], ['lock/config', []]) + ->willReturnOnConsecutiveCalls($lockProvider, $config); + $this->objectManagerMock->expects($this->once()) + ->method('create') + ->with($lockProviderClass, $config) + ->willReturn($lockManagerMock); + + $this->assertSame($lockManagerMock, $this->factory->create()); + } + + /** + * @return array + */ + public function createDataProvider(): array + { + $data = [ + 'db' => [ + 'lockProvider' => LockBackendFactory::LOCK_DB, + 'lockProviderClass' => DatabaseLock::class, + 'config' => ['prefix' => 'somePrefix'], + ], + 'cache' => [ + 'lockProvider' => LockBackendFactory::LOCK_CACHE, + 'lockProviderClass' => CacheLock::class, + 'config' => [], + ], + 'file' => [ + 'lockProvider' => LockBackendFactory::LOCK_FILE, + 'lockProviderClass' => FileLock::class, + 'config' => ['path' => '/my/path'], + ], + ]; + + if (extension_loaded('zookeeper')) { + $data['zookeeper'] = [ + 'lockProvider' => LockBackendFactory::LOCK_ZOOKEEPER, + 'lockProviderClass' => ZookeeperLock::class, + 'config' => ['host' => 'some host'], + ]; + } + + return $data; + } +} diff --git a/lib/internal/Magento/Framework/Lock/Test/Unit/ProxyTest.php b/lib/internal/Magento/Framework/Lock/Test/Unit/ProxyTest.php new file mode 100644 index 000000000000..c71dad701d71 --- /dev/null +++ b/lib/internal/Magento/Framework/Lock/Test/Unit/ProxyTest.php @@ -0,0 +1,106 @@ +factoryMock = $this->createMock(LockBackendFactory::class); + $this->lockerMock = $this->getMockForAbstractClass(LockManagerInterface::class); + $this->proxy = new Proxy($this->factoryMock); + } + + /** + * @return void + */ + public function testIsLocked() + { + $lockName = 'testLock'; + $this->factoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->lockerMock); + $this->lockerMock->expects($this->exactly(2)) + ->method('isLocked') + ->with($lockName) + ->willReturn(true); + + $this->assertTrue($this->proxy->isLocked($lockName)); + + // Call one more time to check that method Factory::create is called one time + $this->assertTrue($this->proxy->isLocked($lockName)); + } + + /** + * @return void + */ + public function testLock() + { + $lockName = 'testLock'; + $timeout = 123; + $this->factoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->lockerMock); + $this->lockerMock->expects($this->exactly(2)) + ->method('lock') + ->with($lockName, $timeout) + ->willReturn(true); + + $this->assertTrue($this->proxy->lock($lockName, $timeout)); + + // Call one more time to check that method Factory::create is called one time + $this->assertTrue($this->proxy->lock($lockName, $timeout)); + } + + /** + * @return void + */ + public function testUnlock() + { + $lockName = 'testLock'; + $this->factoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->lockerMock); + $this->lockerMock->expects($this->exactly(2)) + ->method('unlock') + ->with($lockName) + ->willReturn(true); + + $this->assertTrue($this->proxy->unlock($lockName)); + + // Call one more time to check that method Factory::create is called one time + $this->assertTrue($this->proxy->unlock($lockName)); + } +} diff --git a/lib/internal/Magento/Framework/Logger/Handler/Base.php b/lib/internal/Magento/Framework/Logger/Handler/Base.php index 46c707a9254c..4c136fd65a55 100644 --- a/lib/internal/Magento/Framework/Logger/Handler/Base.php +++ b/lib/internal/Magento/Framework/Logger/Handler/Base.php @@ -11,6 +11,9 @@ use Monolog\Handler\StreamHandler; use Monolog\Logger; +/** + * Base stream handler + */ class Base extends StreamHandler { /** @@ -32,6 +35,7 @@ class Base extends StreamHandler * @param DriverInterface $filesystem * @param string $filePath * @param string $fileName + * @throws \Exception */ public function __construct( DriverInterface $filesystem, @@ -51,8 +55,9 @@ public function __construct( } /** - * @param string $fileName + * Remove dots from file name * + * @param string $fileName * @return string * @throws \InvalidArgumentException */ @@ -71,11 +76,7 @@ private function sanitizeFileName($fileName) } /** - * {@inheritDoc} - * - * @param $record array - * - * @return void + * @inheritDoc */ public function write(array $record) { diff --git a/lib/internal/Magento/Framework/Logger/Handler/System.php b/lib/internal/Magento/Framework/Logger/Handler/System.php index b30195e97971..c519e320a254 100644 --- a/lib/internal/Magento/Framework/Logger/Handler/System.php +++ b/lib/internal/Magento/Framework/Logger/Handler/System.php @@ -9,6 +9,9 @@ use Magento\Framework\Filesystem\DriverInterface; use Monolog\Logger; +/** + * System stream handler + */ class System extends Base { /** @@ -43,7 +46,7 @@ public function __construct( /** * Writes formatted record through the handler. * - * @param $record array The record metadata + * @param array $record The record metadata * @return void */ public function write(array $record) diff --git a/lib/internal/Magento/Framework/Mail/Message.php b/lib/internal/Magento/Framework/Mail/Message.php index 6f156e42dfdb..71da6e673bf2 100644 --- a/lib/internal/Magento/Framework/Mail/Message.php +++ b/lib/internal/Magento/Framework/Mail/Message.php @@ -8,6 +8,9 @@ use Zend\Mime\Mime; use Zend\Mime\Part; +/** + * Class Message for email transportation + */ class Message implements MailMessageInterface { /** @@ -34,7 +37,7 @@ public function __construct($charset = 'utf-8') } /** - * {@inheritdoc} + * @inheritdoc * * @deprecated * @see \Magento\Framework\Mail\Message::setBodyText @@ -47,7 +50,7 @@ public function setMessageType($type) } /** - * {@inheritdoc} + * @inheritdoc * * @deprecated * @see \Magento\Framework\Mail\Message::setBodyText @@ -63,7 +66,7 @@ public function setBody($body) } /** - * {@inheritdoc} + * @inheritdoc */ public function setSubject($subject) { @@ -72,7 +75,7 @@ public function setSubject($subject) } /** - * {@inheritdoc} + * @inheritdoc */ public function getSubject() { @@ -80,7 +83,7 @@ public function getSubject() } /** - * {@inheritdoc} + * @inheritdoc */ public function getBody() { @@ -88,16 +91,29 @@ public function getBody() } /** - * {@inheritdoc} + * @inheritdoc + * + * @deprecated This function is missing the from name. The + * setFromAddress() function sets both from address and from name. + * @see setFromAddress() */ public function setFrom($fromAddress) { - $this->zendMessage->setFrom($fromAddress); + $this->setFromAddress($fromAddress, null); + return $this; + } + + /** + * @inheritdoc + */ + public function setFromAddress($fromAddress, $fromName = null) + { + $this->zendMessage->setFrom($fromAddress, $fromName); return $this; } /** - * {@inheritdoc} + * @inheritdoc */ public function addTo($toAddress) { @@ -106,7 +122,7 @@ public function addTo($toAddress) } /** - * {@inheritdoc} + * @inheritdoc */ public function addCc($ccAddress) { @@ -115,7 +131,7 @@ public function addCc($ccAddress) } /** - * {@inheritdoc} + * @inheritdoc */ public function addBcc($bccAddress) { @@ -124,7 +140,7 @@ public function addBcc($bccAddress) } /** - * {@inheritdoc} + * @inheritdoc */ public function setReplyTo($replyToAddress) { @@ -133,7 +149,7 @@ public function setReplyTo($replyToAddress) } /** - * {@inheritdoc} + * @inheritdoc */ public function getRawMessage() { @@ -157,7 +173,7 @@ private function createHtmlMimeFromString($htmlBody) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBodyHtml($html) { @@ -166,7 +182,7 @@ public function setBodyHtml($html) } /** - * {@inheritdoc} + * @inheritdoc */ public function setBodyText($text) { diff --git a/lib/internal/Magento/Framework/Mail/Template/TransportBuilder.php b/lib/internal/Magento/Framework/Mail/Template/TransportBuilder.php index eac5d74c6e3d..b9271c0209fd 100644 --- a/lib/internal/Magento/Framework/Mail/Template/TransportBuilder.php +++ b/lib/internal/Magento/Framework/Mail/Template/TransportBuilder.php @@ -17,6 +17,8 @@ use Magento\Framework\Phrase; /** + * TransportBuilder + * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -175,13 +177,31 @@ public function setReplyTo($email, $name = null) /** * Set mail from address * + * @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) { - $result = $this->_senderResolver->resolve($from); - $this->message->setFrom($result['email'], $result['name']); + return $this->setFromByScope($from, null); + } + + /** + * Set mail from address by scopeId + * + * @param string|array $from + * @param string|int $scopeId + * @return $this + * @throws \Magento\Framework\Exception\MailException + */ + public function setFromByScope($from, $scopeId = null) + { + $result = $this->_senderResolver->resolve($from, $scopeId); + $this->message->setFromAddress($result['email'], $result['name']); return $this; } @@ -237,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/Template/TransportBuilderByStore.php b/lib/internal/Magento/Framework/Mail/Template/TransportBuilderByStore.php index 785c93824a57..85b1b181d4f9 100644 --- a/lib/internal/Magento/Framework/Mail/Template/TransportBuilderByStore.php +++ b/lib/internal/Magento/Framework/Mail/Template/TransportBuilderByStore.php @@ -8,6 +8,13 @@ use Magento\Framework\Mail\MessageInterface; +/** + * Class TransportBuilderByStore + * + * @deprecated The ability to set From address based on store is now available + * in the \Magento\Framework\Mail\Template\TransportBuilder class + * @see \Magento\Framework\Mail\Template\TransportBuilder::setFromByStore + */ class TransportBuilderByStore { /** @@ -47,7 +54,7 @@ public function __construct( public function setFromByStore($from, $store) { $result = $this->senderResolver->resolve($from, $store); - $this->message->setFrom($result['email'], $result['name']); + $this->message->setFromAddress($result['email'], $result['name']); return $this; } diff --git a/lib/internal/Magento/Framework/Mail/Test/Unit/Template/TransportBuilderByStoreTest.php b/lib/internal/Magento/Framework/Mail/Test/Unit/Template/TransportBuilderByStoreTest.php index 80df2887a3a9..a28dbcd291ba 100644 --- a/lib/internal/Magento/Framework/Mail/Test/Unit/Template/TransportBuilderByStoreTest.php +++ b/lib/internal/Magento/Framework/Mail/Test/Unit/Template/TransportBuilderByStoreTest.php @@ -55,8 +55,8 @@ public function testSetFromByStore() ->with($sender, $store) ->willReturn($sender); $this->messageMock->expects($this->once()) - ->method('setFrom') - ->with('from@example.com', 'name') + ->method('setFromAddress') + ->with($sender['email'], $sender['name']) ->willReturnSelf(); $this->model->setFromByStore($sender, $store); 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 731db30bada0..5e3309af6497 100644 --- a/lib/internal/Magento/Framework/Mail/Test/Unit/Template/TransportBuilderTest.php +++ b/lib/internal/Magento/Framework/Mail/Test/Unit/Template/TransportBuilderTest.php @@ -120,12 +120,12 @@ public function testGetTransport($templateType, $messageType, $bodyText, $templa ->with($this->equalTo('Email Subject')) ->willReturnSelf(); - $this->messageMock->expects($this->exactly(intval($messageType == MessageInterface::TYPE_TEXT))) + $this->messageMock->expects($this->exactly((int)($messageType == MessageInterface::TYPE_TEXT))) ->method('setBodyText') ->with($this->equalTo($bodyText)) ->willReturnSelf(); - $this->messageMock->expects($this->exactly(intval($messageType == MessageInterface::TYPE_HTML))) + $this->messageMock->expects($this->exactly((int)($messageType == MessageInterface::TYPE_HTML))) ->method('setBodyHtml') ->with($this->equalTo($bodyText)) ->willReturnSelf(); @@ -167,19 +167,20 @@ public function getTransportDataProvider() /** * @return void */ - public function testSetFrom() + public function testSetFromByScope() { $sender = ['email' => 'from@example.com', 'name' => 'name']; + $scopeId = 1; $this->senderResolverMock->expects($this->once()) ->method('resolve') - ->with($sender) + ->with($sender, $scopeId) ->willReturn($sender); $this->messageMock->expects($this->once()) - ->method('setFrom') - ->with('from@example.com', 'name') + ->method('setFromAddress') + ->with($sender['email'], $sender['name']) ->willReturnSelf(); - $this->builder->setFrom($sender); + $this->builder->setFromByScope($sender, $scopeId); } /** diff --git a/lib/internal/Magento/Framework/Math/Random.php b/lib/internal/Magento/Framework/Math/Random.php index 7cb70c9a822c..c2059e1935a8 100644 --- a/lib/internal/Magento/Framework/Math/Random.php +++ b/lib/internal/Magento/Framework/Math/Random.php @@ -5,6 +5,9 @@ */ namespace Magento\Framework\Math; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Phrase; + /** * Random data generator * @@ -24,41 +27,24 @@ class Random /**#@-*/ /** - * Get random string + * Get random string. * * @param int $length * @param null|string $chars + * * @return string - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function getRandomString($length, $chars = null) { $str = ''; if (null === $chars) { - $chars = self::CHARS_LOWERS . self::CHARS_UPPERS . self::CHARS_DIGITS; + $chars = self::CHARS_LOWERS.self::CHARS_UPPERS.self::CHARS_DIGITS; } - if (function_exists('openssl_random_pseudo_bytes')) { - // use openssl lib if it is installed - for ($i = 0, $lc = strlen($chars) - 1; $i < $length; $i++) { - $bytes = openssl_random_pseudo_bytes(PHP_INT_SIZE); - $hex = bin2hex($bytes); // hex() doubles the length of the string - $rand = abs(hexdec($hex) % $lc); // random integer from 0 to $lc - $str .= $chars[$rand]; // random character in $chars - } - } elseif ($fp = @fopen('/dev/urandom', 'rb')) { - // attempt to use /dev/urandom if it exists but openssl isn't available - for ($i = 0, $lc = strlen($chars) - 1; $i < $length; $i++) { - $bytes = @fread($fp, PHP_INT_SIZE); - $hex = bin2hex($bytes); // hex() doubles the length of the string - $rand = abs(hexdec($hex) % $lc); // random integer from 0 to $lc - $str .= $chars[$rand]; // random character in $chars - } - fclose($fp); - } else { - throw new \Magento\Framework\Exception\LocalizedException( - new \Magento\Framework\Phrase("Please make sure you have 'openssl' extension installed") - ); + $charsMaxKey = mb_strlen($chars) - 1; + for ($i = 0; $i < $length; $i++) { + $str .= $chars[self::getRandomNumber(0, $charsMaxKey)]; } return $str; @@ -67,47 +53,33 @@ public function getRandomString($length, $chars = null) /** * Return a random number in the specified range * - * @param $min [optional] - * @param $max [optional] - * @return int A random integer value between min (or 0) and max - * @throws \Magento\Framework\Exception\LocalizedException + * @param int $min + * @param int $max + * @return int A random integer value between min (or 0) and max + * @throws LocalizedException */ public static function getRandomNumber($min = 0, $max = null) { if (null === $max) { $max = mt_getrandmax(); } - $range = $max - $min + 1; - $offset = 0; - if (function_exists('openssl_random_pseudo_bytes')) { - // use openssl lib if it is installed - $bytes = openssl_random_pseudo_bytes(PHP_INT_SIZE); - $hex = bin2hex($bytes); // hex() doubles the length of the string - $offset = abs(hexdec($hex) % $range); // random integer from 0 to $range - } elseif ($fp = @fopen('/dev/urandom', 'rb')) { - // attempt to use /dev/urandom if it exists but openssl isn't available - $bytes = @fread($fp, PHP_INT_SIZE); - $hex = bin2hex($bytes); // hex() doubles the length of the string - $offset = abs(hexdec($hex) % $range); // random integer from 0 to $range - fclose($fp); - } else { - throw new \Magento\Framework\Exception\LocalizedException( - new \Magento\Framework\Phrase("Please make sure you have 'openssl' extension installed") - ); + if ($max < $min) { + throw new LocalizedException(new Phrase('Invalid range given.')); } - return $min + $offset; // random integer from $min to $max + return random_int($min, $max); } /** - * Generate a hash from unique ID + * Generate a hash from unique ID. * * @param string $prefix * @return string + * @throws LocalizedException */ public function getUniqueHash($prefix = '') { - return $prefix . md5(uniqid(microtime() . self::getRandomNumber(), true)); + return $prefix . $this->getRandomString(32); } } diff --git a/lib/internal/Magento/Framework/Message/Manager.php b/lib/internal/Magento/Framework/Message/Manager.php index f31892a938fb..d71e196deea8 100644 --- a/lib/internal/Magento/Framework/Message/Manager.php +++ b/lib/internal/Magento/Framework/Message/Manager.php @@ -8,9 +8,12 @@ use Magento\Framework\Event; use Psr\Log\LoggerInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Debug; /** * Message manager model + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Manager implements ManagerInterface @@ -67,7 +70,7 @@ class Manager implements ManagerInterface * @param Event\ManagerInterface $eventManager * @param LoggerInterface $logger * @param string $defaultGroup - * @param ExceptionMessageFactoryInterface|null exceptionMessageFactory + * @param ExceptionMessageFactoryInterface|null $exceptionMessageFactory */ public function __construct( Session $session, @@ -89,7 +92,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getDefaultGroup() { @@ -110,8 +113,8 @@ protected function prepareGroup($group) /** * @inheritdoc * - * @param string|null $group * @param bool $clear + * @param string|null $group * @return Collection */ public function getMessages($clear = false, $group = null) @@ -226,7 +229,7 @@ public function addUniqueMessages(array $messages, $group = null) $items = $this->getMessages(false, $group)->getItems(); foreach ($messages as $message) { - if ($message instanceof MessageInterface and !in_array($message, $items, false)) { + if ($message instanceof MessageInterface && !in_array($message, $items, false)) { $this->addMessage($message, $group); } } @@ -248,7 +251,12 @@ public function addException(\Exception $exception, $alternativeText = null, $gr 'Exception message: %s%sTrace: %s', $exception->getMessage(), "\n", - $exception->getTraceAsString() + Debug::trace( + $exception->getTrace(), + true, + true, + (bool)getenv('MAGE_DEBUG_SHOW_ARGS') + ) ); $this->logger->critical($message); @@ -286,7 +294,12 @@ public function addExceptionMessage(\Exception $exception, $alternativeText = nu 'Exception message: %s%sTrace: %s', $exception->getMessage(), "\n", - $exception->getTraceAsString() + Debug::trace( + $exception->getTrace(), + true, + true, + (bool)getenv('MAGE_DEBUG_SHOW_ARGS') + ) ); $this->logger->critical($message); diff --git a/lib/internal/Magento/Framework/MessageQueue/CallbackInvoker.php b/lib/internal/Magento/Framework/MessageQueue/CallbackInvoker.php index cb80bc4becae..fe0a84af3ca9 100644 --- a/lib/internal/Magento/Framework/MessageQueue/CallbackInvoker.php +++ b/lib/internal/Magento/Framework/MessageQueue/CallbackInvoker.php @@ -6,11 +6,41 @@ namespace Magento\Framework\MessageQueue; +use Magento\Framework\MessageQueue\PoisonPill\PoisonPillCompareInterface; +use Magento\Framework\MessageQueue\PoisonPill\PoisonPillReadInterface; + /** * Class CallbackInvoker to invoke callbacks for consumer classes */ -class CallbackInvoker +class CallbackInvoker implements CallbackInvokerInterface { + /** + * @var PoisonPillReadInterface $poisonPillRead + */ + private $poisonPillRead; + + /** + * @var int $poisonPillVersion + */ + private $poisonPillVersion; + + /** + * @var PoisonPillCompareInterface + */ + private $poisonPillCompare; + + /** + * @param PoisonPillReadInterface $poisonPillRead + * @param PoisonPillCompareInterface $poisonPillCompare + */ + public function __construct( + PoisonPillReadInterface $poisonPillRead, + PoisonPillCompareInterface $poisonPillCompare + ) { + $this->poisonPillRead = $poisonPillRead; + $this->poisonPillCompare = $poisonPillCompare; + } + /** * Run short running process * @@ -21,10 +51,17 @@ class CallbackInvoker */ public function invoke(QueueInterface $queue, $maxNumberOfMessages, $callback) { + $this->poisonPillVersion = $this->poisonPillRead->getLatestVersion(); for ($i = $maxNumberOfMessages; $i > 0; $i--) { do { $message = $queue->dequeue(); + // phpcs:ignore Magento2.Functions.DiscouragedFunction } while ($message === null && (sleep(1) === 0)); + if (false === $this->poisonPillCompare->isLatestVersion($this->poisonPillVersion)) { + $queue->reject($message); + // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage + exit(0); + } $callback($message); } } diff --git a/lib/internal/Magento/Framework/MessageQueue/CallbackInvokerInterface.php b/lib/internal/Magento/Framework/MessageQueue/CallbackInvokerInterface.php new file mode 100644 index 000000000000..36658f2e4eeb --- /dev/null +++ b/lib/internal/Magento/Framework/MessageQueue/CallbackInvokerInterface.php @@ -0,0 +1,24 @@ +getPublisherConfigByTopic($topicName); - return isset($publisherConfig[ConfigInterface::PUBLISHER_EXCHANGE]) - ? $publisherConfig[ConfigInterface::PUBLISHER_EXCHANGE] - : null; + return $publisherConfig[ConfigInterface::PUBLISHER_EXCHANGE] ?? null; } /** - * {@inheritdoc} + * @inheritdoc */ public function getQueuesByTopic($topic) { @@ -67,7 +65,7 @@ public function getQueuesByTopic($topic) } /** - * {@inheritdoc} + * @inheritdoc */ public function getConnectionByTopic($topic) { @@ -76,13 +74,11 @@ public function getConnectionByTopic($topic) } catch (\Magento\Framework\Exception\LocalizedException $e) { return null; } - return isset($publisherConfig[ConfigInterface::PUBLISHER_CONNECTION]) - ? $publisherConfig[ConfigInterface::PUBLISHER_CONNECTION] - : null; + return $publisherConfig[ConfigInterface::PUBLISHER_CONNECTION] ?? null; } /** - * {@inheritdoc} + * @inheritdoc */ public function getConnectionByConsumer($consumer) { @@ -98,7 +94,7 @@ public function getConnectionByConsumer($consumer) } /** - * {@inheritdoc} + * @inheritdoc */ public function getMessageSchemaType($topic) { @@ -109,7 +105,7 @@ public function getMessageSchemaType($topic) } /** - * {@inheritdoc} + * @inheritdoc */ public function getConsumerNames() { @@ -118,7 +114,7 @@ public function getConsumerNames() } /** - * {@inheritdoc} + * @inheritdoc */ public function getConsumer($name) { @@ -127,7 +123,7 @@ public function getConsumer($name) } /** - * {@inheritdoc} + * @inheritdoc */ public function getBinds() { @@ -135,7 +131,7 @@ public function getBinds() } /** - * {@inheritdoc} + * @inheritdoc */ public function getPublishers() { @@ -143,7 +139,7 @@ public function getPublishers() } /** - * {@inheritdoc} + * @inheritdoc */ public function getConsumers() { @@ -151,7 +147,7 @@ public function getConsumers() } /** - * {@inheritdoc} + * @inheritdoc */ public function getTopic($name) { @@ -159,7 +155,7 @@ public function getTopic($name) } /** - * {@inheritdoc} + * @inheritdoc */ public function getPublisher($name) { @@ -167,7 +163,7 @@ public function getPublisher($name) } /** - * {@inheritdoc} + * @inheritdoc */ public function getResponseQueueName($topicName) { diff --git a/lib/internal/Magento/Framework/MessageQueue/Config/CompositeReader.php b/lib/internal/Magento/Framework/MessageQueue/Config/CompositeReader.php index 370096172865..86e83d678c0b 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Config/CompositeReader.php +++ b/lib/internal/Magento/Framework/MessageQueue/Config/CompositeReader.php @@ -69,17 +69,12 @@ function ($firstItem, $secondItem) { $firstValue = 0; $secondValue = 0; if (isset($firstItem['sortOrder'])) { - $firstValue = intval($firstItem['sortOrder']); + $firstValue = (int)$firstItem['sortOrder']; } - if (isset($secondItem['sortOrder'])) { - $secondValue = intval($secondItem['sortOrder']); - } - - if ($firstValue == $secondValue) { - return 0; + $secondValue = (int)$secondItem['sortOrder']; } - return $firstValue < $secondValue ? -1 : 1; + return $firstValue <=> $secondValue; } ); return $readers; diff --git a/lib/internal/Magento/Framework/MessageQueue/Config/Reader/Xml/CompositeConverter.php b/lib/internal/Magento/Framework/MessageQueue/Config/Reader/Xml/CompositeConverter.php index be375c1ac817..0184f720b3b4 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Config/Reader/Xml/CompositeConverter.php +++ b/lib/internal/Magento/Framework/MessageQueue/Config/Reader/Xml/CompositeConverter.php @@ -43,7 +43,7 @@ public function __construct(array $converters) } /** - * {@inheritdoc} + * @inheritdoc */ public function convert($source) { @@ -68,17 +68,12 @@ function ($firstItem, $secondItem) { $firstValue = 0; $secondValue = 0; if (isset($firstItem['sortOrder'])) { - $firstValue = intval($firstItem['sortOrder']); + $firstValue = (int)$firstItem['sortOrder']; } - if (isset($secondItem['sortOrder'])) { - $secondValue = intval($secondItem['sortOrder']); - } - - if ($firstValue == $secondValue) { - return 0; + $secondValue = (int)$secondItem['sortOrder']; } - return $firstValue < $secondValue ? -1 : 1; + return $firstValue <=> $secondValue; } ); return $converters; diff --git a/lib/internal/Magento/Framework/MessageQueue/Consumer.php b/lib/internal/Magento/Framework/MessageQueue/Consumer.php index 9bdacfbc1ff6..8f65a2d8c5ed 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Consumer.php +++ b/lib/internal/Magento/Framework/MessageQueue/Consumer.php @@ -38,7 +38,7 @@ class Consumer implements ConsumerInterface private $messageEncoder; /** - * @var CallbackInvoker + * @var CallbackInvokerInterface */ private $invoker; @@ -80,7 +80,7 @@ class Consumer implements ConsumerInterface /** * Initialize dependencies. * - * @param CallbackInvoker $invoker + * @param CallbackInvokerInterface $invoker * @param MessageEncoder $messageEncoder * @param ResourceConnection $resource * @param ConsumerConfigurationInterface $configuration @@ -89,7 +89,7 @@ class Consumer implements ConsumerInterface * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( - CallbackInvoker $invoker, + CallbackInvokerInterface $invoker, MessageEncoder $messageEncoder, ResourceConnection $resource, ConsumerConfigurationInterface $configuration, @@ -103,7 +103,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function process($maxNumberOfMessages = null) { diff --git a/lib/internal/Magento/Framework/MessageQueue/Consumer/Config/Env/Reader.php b/lib/internal/Magento/Framework/MessageQueue/Consumer/Config/Env/Reader.php index aa318ba5f19c..a75aa1ea8ead 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Consumer/Config/Env/Reader.php +++ b/lib/internal/Magento/Framework/MessageQueue/Consumer/Config/Env/Reader.php @@ -33,8 +33,6 @@ public function __construct(\Magento\Framework\MessageQueue\Config\Reader\Env $e public function read($scope = null) { $configData = $this->envConfig->read($scope); - return isset($configData[\Magento\Framework\MessageQueue\Config\Reader\Env::ENV_CONSUMERS]) - ? $configData[\Magento\Framework\MessageQueue\Config\Reader\Env::ENV_CONSUMERS] - : []; + return $configData[\Magento\Framework\MessageQueue\Config\Reader\Env::ENV_CONSUMERS] ?? []; } } diff --git a/lib/internal/Magento/Framework/MessageQueue/MessageProcessor.php b/lib/internal/Magento/Framework/MessageQueue/MessageProcessor.php index d71a527c9cbb..ad0201cf56e7 100644 --- a/lib/internal/Magento/Framework/MessageQueue/MessageProcessor.php +++ b/lib/internal/Magento/Framework/MessageQueue/MessageProcessor.php @@ -12,6 +12,11 @@ */ class MessageProcessor implements MessageProcessorInterface { + /** + * Maximum number of transaction retries + */ + const MAX_TRANSACTION_RETRIES = 10; + /** * @var \Magento\Framework\MessageQueue\MessageStatusProcessor */ @@ -22,6 +27,11 @@ class MessageProcessor implements MessageProcessorInterface */ private $resource; + /** + * @var int + */ + private $retryCount = 0; + /** * @param MessageStatusProcessor $messageStatusProcessor * @param ResourceConnection $resource @@ -53,8 +63,19 @@ public function process( } catch (ConnectionLostException $e) { $this->resource->getConnection()->rollBack(); } catch (\Exception $e) { + $retry = false; $this->resource->getConnection()->rollBack(); - $this->messageStatusProcessor->rejectMessages($queue, $messages); + if (strpos($e->getMessage(), 'Error while sending QUERY packet') !== false + && $this->retryCount < self::MAX_TRANSACTION_RETRIES + ) { + $retry = true; + $this->retryCount++; + $this->resource->closeConnection(); + $this->process($queue, $configuration, $messages, $messagesToAcknowledge, $mergedMessages); + } + if (!$retry) { + $this->messageStatusProcessor->rejectMessages($queue, $messages); + } } } diff --git a/lib/internal/Magento/Framework/MessageQueue/PoisonPill/PoisonPillCompare.php b/lib/internal/Magento/Framework/MessageQueue/PoisonPill/PoisonPillCompare.php new file mode 100644 index 000000000000..0b85fbb61472 --- /dev/null +++ b/lib/internal/Magento/Framework/MessageQueue/PoisonPill/PoisonPillCompare.php @@ -0,0 +1,27 @@ +firstVersion; + } +} diff --git a/lib/internal/Magento/Framework/MessageQueue/PoisonPill/PoisonPillPutInterface.php b/lib/internal/Magento/Framework/MessageQueue/PoisonPill/PoisonPillPutInterface.php new file mode 100644 index 000000000000..dbf300101c45 --- /dev/null +++ b/lib/internal/Magento/Framework/MessageQueue/PoisonPill/PoisonPillPutInterface.php @@ -0,0 +1,22 @@ +markTestSkipped('Skipped on HHVM. Will be fixed in MAGETWO-45033'); } $urnResolver = new \Magento\Framework\Config\Dom\UrnResolver(); - $this->_schemaFile = $urnResolver->getRealPath('urn:magento:framework-message-queue:etc/consumer.xsd'); + $this->schemaFile = $urnResolver->getRealPath('urn:magento:framework-message-queue:etc/consumer.xsd'); + $this->schemaQueueFile = $urnResolver->getRealPath('urn:magento:framework-message-queue:etc/queue.xsd'); } /** @@ -29,13 +40,13 @@ protected function setUp() public function testExemplarXml($fixtureXml, array $expectedErrors) { $validationState = $this->createMock(\Magento\Framework\Config\ValidationStateInterface::class); - $validationState->expects($this->any()) + $validationState->expects($this->atLeastOnce()) ->method('isValidationRequired') ->willReturn(true); $messageFormat = '%message%'; $dom = new \Magento\Framework\Config\Dom($fixtureXml, $validationState, [], null, null, $messageFormat); $actualErrors = []; - $actualResult = $dom->validate($this->_schemaFile, $actualErrors); + $actualResult = $dom->validate($this->schemaFile, $actualErrors); $this->assertEquals(empty($expectedErrors), $actualResult, "Validation result is invalid."); $this->assertEquals($expectedErrors, $actualErrors, "Validation errors does not match."); } @@ -125,4 +136,106 @@ public function exemplarXmlDataProvider() ]; // @codingStandardsIgnoreEnd } + + /** + * @param string $fixtureXml + * @param array $expectedErrors + * @dataProvider exemplarQueueXmlDataProvider + */ + public function testExemplarQueueXml($fixtureXml, array $expectedErrors) + { + $validationState = $this->createMock(\Magento\Framework\Config\ValidationStateInterface::class); + $validationState->expects($this->atLeastOnce()) + ->method('isValidationRequired') + ->willReturn(true); + $messageFormat = '%message%'; + $dom = new \Magento\Framework\Config\Dom($fixtureXml, $validationState, [], null, null, $messageFormat); + $actualErrors = []; + $actualResult = $dom->validate($this->schemaQueueFile, $actualErrors); + $this->assertEquals(empty($expectedErrors), $actualResult, "Validation result is invalid."); + $this->assertEquals($expectedErrors, $actualErrors, "Validation errors does not match."); + } + + /** + * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function exemplarQueueXmlDataProvider() + { + // @codingStandardsIgnoreStart + return [ + 'valid' => [ + ' + + + + + ', + [], + ], + 'invalid handler format' => [ + ' + + + + + ', + [ + "Element 'queue', attribute 'handler': [facet 'pattern'] The value 'handlerClass_One1::handlerMethod1' is not accepted by the pattern '[a-zA-Z0-9\\\\]+::[a-zA-Z0-9]+'.", + "Element 'queue', attribute 'handler': 'handlerClass_One1::handlerMethod1' is not a valid value of the atomic type 'handlerType'.", + "Element 'queue', attribute 'handler': [facet 'pattern'] The value 'handlerClassOne2::handler_Method2' is not accepted by the pattern '[a-zA-Z0-9\\\\]+::[a-zA-Z0-9]+'.", + "Element 'queue', attribute 'handler': 'handlerClassOne2::handler_Method2' is not a valid value of the atomic type 'handlerType'.", + ], + ], + 'invalid instance format' => [ + ' + + + + + ', + [ + "Element 'queue', attribute 'consumerInstance': [facet 'pattern'] The value 'consumer_Class1' is not accepted by the pattern '[a-zA-Z0-9\\\\]+'.", + "Element 'queue', attribute 'consumerInstance': 'consumer_Class1' is not a valid value of the atomic type 'instanceType'.", + "Element 'queue', attribute 'consumerInstance': [facet 'pattern'] The value 'consumerClass_2' is not accepted by the pattern '[a-zA-Z0-9\\\\]+'.", + "Element 'queue', attribute 'consumerInstance': 'consumerClass_2' is not a valid value of the atomic type 'instanceType'.", + ], + ], + 'invalid maxMessages format' => [ + ' + + + + + ', + [ + "Element 'queue', attribute 'maxMessages': 'ABC' is not a valid value of the atomic type 'xs:integer'.", + ], + ], + 'unexpected element' => [ + ' + + + + + + ', + [ + "Element 'unexpected': This element is not expected. Expected is ( queue ).", + ], + ], + 'unexpected attribute' => [ + ' + + + + + ', + [ + "Element 'queue', attribute 'unexpected': The attribute 'unexpected' is not allowed.", + ], + ], + ]; + // @codingStandardsIgnoreEnd + } } diff --git a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/ConsumerTest.php b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/ConsumerTest.php index 5eb7cd587aaa..7a3eb3b16bac 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/ConsumerTest.php +++ b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/ConsumerTest.php @@ -6,6 +6,8 @@ namespace Magento\Framework\MessageQueue\Test\Unit; +use Magento\Framework\MessageQueue\PoisonPill\PoisonPillCompareInterface; +use Magento\Framework\MessageQueue\PoisonPill\PoisonPillReadInterface; use Magento\Framework\Phrase; /** @@ -65,6 +67,16 @@ class ConsumerTest extends \PHPUnit\Framework\TestCase */ private $consumer; + /** + * @var PoisonPillReadInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $poisonPillRead; + + /** + * @var PoisonPillCompareInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $poisonPillCompare; + /** * Set up. * @@ -85,8 +97,15 @@ protected function setUp() ->disableOriginalConstructor()->getMock(); $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->poisonPillCompare = $this->getMockBuilder(PoisonPillCompareInterface::class) + ->disableOriginalConstructor()->getMock(); + $this->poisonPillRead = $this->getMockBuilder(PoisonPillReadInterface::class) + ->disableOriginalConstructor()->getMock(); //Hard dependency used because CallbackInvoker invokes closure logic defined inside of Customer class. - $this->callbackInvoker = new \Magento\Framework\MessageQueue\CallbackInvoker(); + $this->callbackInvoker = new \Magento\Framework\MessageQueue\CallbackInvoker( + $this->poisonPillRead, + $this->poisonPillCompare + ); $this->consumer = $objectManager->getObject( \Magento\Framework\MessageQueue\Consumer::class, [ @@ -134,7 +153,8 @@ public function testProcessWithNotFoundException() $numberOfMessages = 1; $consumerName = 'consumer.name'; $exceptionPhrase = new Phrase('Exception successfully thrown'); - + $this->poisonPillRead->expects($this->atLeastOnce())->method('getLatestVersion')->willReturn('version-1'); + $this->poisonPillCompare->expects($this->atLeastOnce())->method('isLatestVersion')->willReturn(true); $queue = $this->getMockBuilder(\Magento\Framework\MessageQueue\QueueInterface::class) ->disableOriginalConstructor()->getMock(); $this->configuration->expects($this->once())->method('getQueue')->willReturn($queue); diff --git a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/MessageProcessorTest.php b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/MessageProcessorTest.php index 5e64d737034c..73841a2a964c 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/MessageProcessorTest.php +++ b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/MessageProcessorTest.php @@ -75,7 +75,7 @@ public function testProcess() ->getMockForAbstractClass(); $configuration->expects($this->atLeastOnce())->method('getHandlers')->willReturn([]); $this->messageStatusProcessor->expects($this->exactly(2))->method('acknowledgeMessages'); - $mergedMessage = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) + $mergedMessage = $this->getMockBuilder(\Magento\Framework\Api\CustomAttributesDataInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); $message = $this->getMockBuilder(\Magento\Framework\MessageQueue\EnvelopeInterface::class) @@ -116,7 +116,7 @@ public function testProcessWithConnectionLostException() $exception = new \Magento\Framework\MessageQueue\ConnectionLostException(__('Exception Message')); $configuration->expects($this->atLeastOnce())->method('getHandlers')->willThrowException($exception); $this->messageStatusProcessor->expects($this->once())->method('acknowledgeMessages'); - $mergedMessage = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) + $mergedMessage = $this->getMockBuilder(\Magento\Framework\Api\CustomAttributesDataInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); $message = $this->getMockBuilder(\Magento\Framework\MessageQueue\EnvelopeInterface::class) @@ -158,7 +158,7 @@ public function testProcessWithException() $configuration->expects($this->atLeastOnce())->method('getHandlers')->willThrowException($exception); $this->messageStatusProcessor->expects($this->once())->method('acknowledgeMessages'); $this->messageStatusProcessor->expects($this->atLeastOnce())->method('rejectMessages'); - $mergedMessage = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductInterface::class) + $mergedMessage = $this->getMockBuilder(\Magento\Framework\Api\CustomAttributesDataInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); $message = $this->getMockBuilder(\Magento\Framework\MessageQueue\EnvelopeInterface::class) diff --git a/lib/internal/Magento/Framework/MessageQueue/etc/queue_base.xsd b/lib/internal/Magento/Framework/MessageQueue/etc/queue_base.xsd index 4b326d637f27..bede197ec34d 100644 --- a/lib/internal/Magento/Framework/MessageQueue/etc/queue_base.xsd +++ b/lib/internal/Magento/Framework/MessageQueue/etc/queue_base.xsd @@ -41,7 +41,7 @@ - + @@ -53,7 +53,7 @@ - + diff --git a/lib/internal/Magento/Framework/Model/AbstractExtensibleModel.php b/lib/internal/Magento/Framework/Model/AbstractExtensibleModel.php index 1cffba2543b0..e1f6c792c9c3 100644 --- a/lib/internal/Magento/Framework/Model/AbstractExtensibleModel.php +++ b/lib/internal/Magento/Framework/Model/AbstractExtensibleModel.php @@ -158,7 +158,7 @@ public function getCustomAttribute($attributeCode) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomAttributes(array $attributes) { @@ -166,7 +166,7 @@ public function setCustomAttributes(array $attributes) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCustomAttribute($attributeCode, $attributeValue) { @@ -182,7 +182,7 @@ public function setCustomAttribute($attributeCode, $attributeValue) } /** - * {@inheritdoc} + * @inheritdoc * * Added custom attributes support. */ @@ -200,7 +200,7 @@ public function setData($key, $value = null) } /** - * {@inheritdoc} + * @inheritdoc * * Unset customAttributesChanged flag */ @@ -359,17 +359,27 @@ private function populateExtensionAttributes(array $extensionAttributesData = [] /** * @inheritdoc + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + return array_diff(parent::__sleep(), ['extensionAttributesFactory', 'customAttributeFactory']); } /** * @inheritdoc + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + parent::__wakeup(); $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); $this->extensionAttributesFactory = $objectManager->get(ExtensionAttributesFactory::class); diff --git a/lib/internal/Magento/Framework/Model/AbstractModel.php b/lib/internal/Magento/Framework/Model/AbstractModel.php index d8a7c96c6391..f5095dbb6e87 100644 --- a/lib/internal/Magento/Framework/Model/AbstractModel.php +++ b/lib/internal/Magento/Framework/Model/AbstractModel.php @@ -42,7 +42,7 @@ abstract class AbstractModel extends \Magento\Framework\DataObject /** * Data changes flag (true after setData|unsetData call) - * @var $_hasDataChange bool + * @var bool */ protected $_hasDataChanges = false; @@ -216,10 +216,17 @@ protected function _init($resourceModel) } /** + * Remove unneeded properties from serialization + * * @return string[] + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + $properties = array_keys(get_object_vars($this)); $properties = array_diff( $properties, @@ -241,9 +248,14 @@ public function __sleep() * Init not serializable fields * * @return void + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); $this->_registry = $objectManager->get(\Magento\Framework\Registry::class); @@ -337,8 +349,8 @@ public function hasDataChanges() * * 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) @@ -616,6 +628,8 @@ protected function _hasModelChanged() } /** + * Check if save is allowed + * * @return bool */ public function isSaveAllowed() @@ -624,6 +638,8 @@ public function isSaveAllowed() } /** + * Set flag property _hasDataChanges + * * @param bool $flag * @return void */ @@ -719,6 +735,7 @@ public function validateBeforeSave() /** * Returns validator, which contains all rules to validate this model. + * * Returns FALSE, if no validation rules exist. * * @return \Zend_Validate_Interface|false @@ -733,6 +750,7 @@ protected function _getValidatorBeforeSave() /** * Creates validator for the model with all validation rules in it. + * * Returns FALSE, if no validation rules exist. * * @return \Zend_Validate_Interface|bool @@ -770,6 +788,7 @@ protected function _getValidationRulesBeforeSave() /** * Get list of cache tags applied to model object. + * * Return false if cache tags are not supported by model * * @return array|false diff --git a/lib/internal/Magento/Framework/Model/ResourceModel/Db/AbstractDb.php b/lib/internal/Magento/Framework/Model/ResourceModel/Db/AbstractDb.php index 1eaed75bcbfd..0cadb10aaafe 100644 --- a/lib/internal/Magento/Framework/Model/ResourceModel/Db/AbstractDb.php +++ b/lib/internal/Magento/Framework/Model/ResourceModel/Db/AbstractDb.php @@ -156,9 +156,14 @@ public function __construct( * Provide variables to serialize * * @return array + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + $properties = array_keys(get_object_vars($this)); $properties = array_diff($properties, ['_resources', '_connections']); return $properties; @@ -168,9 +173,14 @@ public function __sleep() * Restore global dependencies * * @return void + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + $this->_resources = \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Framework\App\ResourceConnection::class); } @@ -219,8 +229,10 @@ protected function _setResource($connections, $tables = null) } /** - * Set main entity table name and primary key field name - * If field name is omitted {table_name}_id will be used + * Main table setter. + * + * Set main entity table name and primary key field name. + * If field name is omitted {table_name}_id will be used. * * @param string $mainTable * @param string|null $idFieldName @@ -253,8 +265,10 @@ public function getIdFieldName() } /** + * Main table getter. + * * Returns main table name - extracted from "module/table" style and - * validated by db adapter + * validated by db adapter. * * @throws LocalizedException * @return string @@ -542,8 +556,7 @@ protected function _prepareDataForSave(\Magento\Framework\Model\AbstractModel $o } /** - * Check that model data fields that can be saved - * has really changed comparing with origData + * Check that model data fields that can be saved has really changed comparing with origData. * * @param \Magento\Framework\Model\AbstractModel $object * @return bool diff --git a/lib/internal/Magento/Framework/Model/ResourceModel/Db/Collection/AbstractCollection.php b/lib/internal/Magento/Framework/Model/ResourceModel/Db/Collection/AbstractCollection.php index b57755ed7eaf..bc2187f47491 100644 --- a/lib/internal/Magento/Framework/Model/ResourceModel/Db/Collection/AbstractCollection.php +++ b/lib/internal/Magento/Framework/Model/ResourceModel/Db/Collection/AbstractCollection.php @@ -45,6 +45,13 @@ abstract class AbstractCollection extends AbstractDb implements SourceProviderIn */ protected $_fieldsToSelect = null; + /** + * Expression fields to select in query. + * + * @var array + */ + private $expressionFieldsToSelect = []; + /** * Fields initial fields to select like id_field * @@ -170,7 +177,7 @@ public function setMainTable($table) } /** - * {@inheritdoc} + * @inheritdoc */ protected function _initSelect() { @@ -205,7 +212,7 @@ protected function _initSelectFields() $columnsToSelect = []; foreach ($columns as $columnEntry) { list($correlationName, $column, $alias) = $columnEntry; - if ($correlationName !== 'main_table') { + if ($correlationName !== 'main_table' || isset($this->expressionFieldsToSelect[$alias])) { // Add joined fields to select if ($column instanceof \Zend_Db_Expr) { $column = $column->__toString(); @@ -347,6 +354,7 @@ public function addExpressionFieldToSelect($alias, $expression, $fields) } $this->getSelect()->columns([$alias => $fullExpression]); + $this->expressionFieldsToSelect[$alias] = $fullExpression; return $this; } @@ -598,9 +606,14 @@ public function save() /** * @inheritdoc * @since 100.0.11 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + return array_diff( parent::__sleep(), ['_resource', '_eventManager'] @@ -610,9 +623,14 @@ public function __sleep() /** * @inheritdoc * @since 100.0.11 + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + parent::__wakeup(); $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); $this->_eventManager = $objectManager->get(\Magento\Framework\Event\ManagerInterface::class); diff --git a/lib/internal/Magento/Framework/Module/ModuleList/Loader.php b/lib/internal/Magento/Framework/Module/ModuleList/Loader.php index bdfb77762b41..72421f793f13 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 0dce507ba26f..1eeb2bafc962 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/Plugin/DbStatusValidator.php b/lib/internal/Magento/Framework/Module/Plugin/DbStatusValidator.php index 30b2a370bcb4..96f9674704e1 100644 --- a/lib/internal/Magento/Framework/Module/Plugin/DbStatusValidator.php +++ b/lib/internal/Magento/Framework/Module/Plugin/DbStatusValidator.php @@ -121,7 +121,7 @@ private function getGroupedDbVersionErrors() (array)$allDbVersionErrors, function ($carry, $item) { if ($item[DbVersionInfo::KEY_CURRENT] === 'none' - || $item[DbVersionInfo::KEY_CURRENT] < $item[DbVersionInfo::KEY_REQUIRED] + || version_compare($item[DbVersionInfo::KEY_CURRENT], $item[DbVersionInfo::KEY_REQUIRED], '<') ) { $carry['version_too_low'][] = $item; } else { diff --git a/lib/internal/Magento/Framework/Module/Setup/Context.php b/lib/internal/Magento/Framework/Module/Setup/Context.php index 81b93ac447cc..4c6240e4c82d 100644 --- a/lib/internal/Magento/Framework/Module/Setup/Context.php +++ b/lib/internal/Magento/Framework/Module/Setup/Context.php @@ -99,6 +99,8 @@ public function __construct( } /** + * Retrieve event manager + * * @return \Magento\Framework\Event\ManagerInterface */ public function getEventManager() @@ -107,7 +109,9 @@ public function getEventManager() } /** - * @return \Psr\Log\LoggerInterface $logger + * Retrieve logger + * + * @return \Psr\Log\LoggerInterface */ public function getLogger() { @@ -115,6 +119,8 @@ public function getLogger() } /** + * Retrieve module list + * * @return \Magento\Framework\Module\ModuleListInterface */ public function getModuleList() @@ -123,6 +129,8 @@ public function getModuleList() } /** + * Retrieve modules reader + * * @return \Magento\Framework\Module\Dir\Reader */ public function getModulesReader() @@ -131,6 +139,8 @@ public function getModulesReader() } /** + * Retrieve resource model + * * @return \Magento\Framework\App\ResourceConnection */ public function getResourceModel() @@ -139,6 +149,8 @@ public function getResourceModel() } /** + * Retrieve migration factory + * * @return \Magento\Framework\Module\Setup\MigrationFactory */ public function getMigrationFactory() @@ -147,6 +159,8 @@ public function getMigrationFactory() } /** + * Retrieve resource + * * @return \Magento\Framework\Module\ResourceInterface */ public function getResource() @@ -155,6 +169,8 @@ public function getResource() } /** + * Retrieve encryptor + * * @return \Magento\Framework\Encryption\EncryptorInterface */ public function getEncryptor() @@ -163,6 +179,8 @@ public function getEncryptor() } /** + * Retrieve filesystem + * * @return \Magento\Framework\Filesystem */ public function getFilesystem() 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 fe613450fd48..a62bb5fa70f9 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/Mview/Config/Data/Proxy.php b/lib/internal/Magento/Framework/Mview/Config/Data/Proxy.php index a83e9507bda0..d67c38020755 100644 --- a/lib/internal/Magento/Framework/Mview/Config/Data/Proxy.php +++ b/lib/internal/Magento/Framework/Mview/Config/Data/Proxy.php @@ -55,10 +55,17 @@ public function __construct( } /** + * Remove links to objects. + * * @return array + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + return ['subject', 'isShared']; } @@ -66,9 +73,14 @@ public function __sleep() * Retrieve ObjectManager from global scope * * @return void + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + $this->objectManager = \Magento\Framework\App\ObjectManager::getInstance(); } @@ -100,7 +112,7 @@ protected function _getSubject() } /** - * {@inheritdoc} + * @inheritdoc */ public function merge(array $config) { @@ -108,7 +120,7 @@ public function merge(array $config) } /** - * {@inheritdoc} + * @inheritdoc */ public function get($path = null, $default = null) { diff --git a/lib/internal/Magento/Framework/Mview/View.php b/lib/internal/Magento/Framework/Mview/View.php index a937c62dfc23..1b32238813f8 100644 --- a/lib/internal/Magento/Framework/Mview/View.php +++ b/lib/internal/Magento/Framework/Mview/View.php @@ -10,6 +10,8 @@ use Magento\Framework\Mview\View\SubscriptionFactory; /** + * Mview + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class View extends \Magento\Framework\DataObject implements ViewInterface @@ -283,7 +285,7 @@ public function update() for ($vsFrom = $lastVersionId; $vsFrom < $currentVersionId; $vsFrom += $versionBatchSize) { // Don't go past the current version for atomicy. $versionTo = min($currentVersionId, $vsFrom + $versionBatchSize); - $ids = $this->getChangelog()->getList($vsFrom, $versionTo); + $ids = array_map('intval', $this->getChangelog()->getList($vsFrom, $versionTo)); // We run the actual indexer in batches. // Chunked AFTER loading to avoid duplicates in separate chunks. diff --git a/lib/internal/Magento/Framework/Mview/View/Subscription.php b/lib/internal/Magento/Framework/Mview/View/Subscription.php index 2f781dcad0ab..67dff1a2cc5d 100644 --- a/lib/internal/Magento/Framework/Mview/View/Subscription.php +++ b/lib/internal/Magento/Framework/Mview/View/Subscription.php @@ -10,6 +10,11 @@ use Magento\Framework\DB\Ddl\Trigger; use Magento\Framework\Mview\View\StateInterface; +/** + * Class Subscription + * + * @package Magento\Framework\Mview\View + */ class Subscription implements SubscriptionInterface { /** @@ -193,33 +198,32 @@ protected function getLinkedViews() */ protected function buildStatement($event, $changelog) { - $columns = []; - if ($this->connection->isTableExists($this->getTableName()) - && $describe = $this->connection->describeTable($this->getTableName()) - ) { - foreach ($describe as $column) { - if (in_array($column['COLUMN_NAME'], $this->ignoredUpdateColumns)) { - continue; - } - $columns[] = sprintf( - 'NEW.%1$s != OLD.%1$s', - $this->connection->quoteIdentifier($column['COLUMN_NAME']) - ); - } - } - switch ($event) { case Trigger::EVENT_INSERT: $trigger = "INSERT IGNORE INTO %s (%s) VALUES (NEW.%s);"; break; case Trigger::EVENT_UPDATE: + $tableName = $this->resource->getTableName($this->getTableName()); $trigger = "INSERT IGNORE INTO %s (%s) VALUES (NEW.%s);"; - if ($columns) { - $trigger = sprintf( - "IF (%s) THEN %s END IF;", - implode(' OR ', $columns), - $trigger - ); + if ($this->connection->isTableExists($tableName) && + $describe = $this->connection->describeTable($tableName) + ) { + $columnNames = array_column($describe, 'COLUMN_NAME'); + $columnNames = array_diff($columnNames, $this->ignoredUpdateColumns); + if ($columnNames) { + $columns = []; + foreach ($columnNames as $columnName) { + $columns[] = sprintf( + 'NEW.%1$s <=> OLD.%1$s', + $this->connection->quoteIdentifier($columnName) + ); + } + $trigger = sprintf( + "IF (%s) THEN %s END IF;", + implode(' OR ', $columns), + $trigger + ); + } } break; case Trigger::EVENT_DELETE: diff --git a/lib/internal/Magento/Framework/Oauth/Oauth.php b/lib/internal/Magento/Framework/Oauth/Oauth.php index 5e48fb5ed30f..919b0e4c86ba 100644 --- a/lib/internal/Magento/Framework/Oauth/Oauth.php +++ b/lib/internal/Magento/Framework/Oauth/Oauth.php @@ -9,6 +9,9 @@ use Magento\Framework\Encryption\Helper\Security; use Magento\Framework\Phrase; +/** + * Authorization service. + */ class Oauth implements OauthInterface { /** @@ -61,7 +64,7 @@ public static function getSupportedSignatureMethods() } /** - * {@inheritdoc} + * @inheritdoc */ public function getRequestToken($params, $requestUrl, $httpMethod = 'POST') { @@ -74,7 +77,7 @@ public function getRequestToken($params, $requestUrl, $httpMethod = 'POST') } /** - * {@inheritdoc} + * @inheritdoc */ public function getAccessToken($params, $requestUrl, $httpMethod = 'POST') { @@ -102,7 +105,7 @@ public function getAccessToken($params, $requestUrl, $httpMethod = 'POST') } /** - * {@inheritdoc} + * @inheritdoc */ public function validateAccessTokenRequest($params, $requestUrl, $httpMethod = 'POST') { @@ -125,7 +128,7 @@ public function validateAccessTokenRequest($params, $requestUrl, $httpMethod = ' } /** - * {@inheritdoc} + * @inheritdoc */ public function validateAccessToken($accessToken) { @@ -133,7 +136,7 @@ public function validateAccessToken($accessToken) } /** - * {@inheritdoc} + * @inheritdoc */ public function buildAuthorizationHeader( $params, @@ -199,7 +202,7 @@ protected function _validateSignature($params, $consumerSecret, $httpMethod, $re ); if (!Security::compareStrings($calculatedSign, $params['oauth_signature'])) { - throw new Exception(new Phrase('The signatire is invalid. Verify and try again.')); + throw new Exception(new Phrase('The signature is invalid. Verify and try again.')); } } diff --git a/lib/internal/Magento/Framework/ObjectManager/Code/Generator/Proxy.php b/lib/internal/Magento/Framework/ObjectManager/Code/Generator/Proxy.php index 96595bc7a073..efa1b4be60ce 100644 --- a/lib/internal/Magento/Framework/ObjectManager/Code/Generator/Proxy.php +++ b/lib/internal/Magento/Framework/ObjectManager/Code/Generator/Proxy.php @@ -5,8 +5,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Framework\ObjectManager\Code\Generator; +/** + * Class Proxy + * + * @package Magento\Framework\ObjectManager\Code\Generator + */ class Proxy extends \Magento\Framework\Code\Generator\EntityAbstract { /** @@ -20,6 +26,8 @@ class Proxy extends \Magento\Framework\Code\Generator\EntityAbstract const NON_INTERCEPTABLE_INTERFACE = \Magento\Framework\ObjectManager\NoninterceptableInterface::class; /** + * Returns default result class name + * * @param string $modelClassName * @return string */ @@ -112,13 +120,16 @@ protected function _getClassMethods() $reflectionClass = new \ReflectionClass($this->getSourceClassName()); $publicMethods = $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC); foreach ($publicMethods as $method) { - if (!($method->isConstructor() || + if (!( + $method->isConstructor() || $method->isFinal() || $method->isStatic() || - $method->isDestructor()) && !in_array( - $method->getName(), - ['__sleep', '__wakeup', '__clone'] - ) + $method->isDestructor() + ) + && !in_array( + $method->getName(), + ['__sleep', '__wakeup', '__clone'] + ) ) { $methods[] = $this->_getMethodInfo($method); } @@ -128,6 +139,8 @@ protected function _getClassMethods() } /** + * Generates code + * * @return string */ protected function _generateCode() @@ -160,10 +173,7 @@ protected function _getMethodInfo(\ReflectionMethod $method) $parameters[] = $this->_getMethodParameterInfo($parameter); } - $returnType = $method->getReturnType(); - $returnTypeValue = $returnType - ? ($returnType->allowsNull() ? '?' : '') .$returnType->getName() - : null; + $returnTypeValue = $this->getReturnTypeValue($method->getReturnType()); $methodInfo = [ 'name' => $method->getName(), 'parameters' => $parameters, @@ -237,11 +247,13 @@ protected function _getMethodBody( } return ($withoutReturn ? '' : 'return ') - .'$this->_getSubject()->' . $methodCall . ';'; + . '$this->_getSubject()->' . $methodCall . ';'; } /** - * {@inheritdoc} + * Validates data + * + * @return bool */ protected function _validateData() { @@ -259,4 +271,22 @@ protected function _validateData() } return $result; } + + /** + * Returns return type + * + * @param mixed $returnType + * @return null|string + */ + private function getReturnTypeValue($returnType): ?string + { + $returnTypeValue = null; + if ($returnType) { + $returnTypeValue = ($returnType->allowsNull() ? '?' : ''); + $returnTypeValue .= ($returnType->getName() === 'self') + ? $this->getSourceClassName() + : $returnType->getName(); + } + return $returnTypeValue; + } } diff --git a/lib/internal/Magento/Framework/ObjectManager/Helper/Composite.php b/lib/internal/Magento/Framework/ObjectManager/Helper/Composite.php index c99170b4112c..b56b6361b36f 100644 --- a/lib/internal/Magento/Framework/ObjectManager/Helper/Composite.php +++ b/lib/internal/Magento/Framework/ObjectManager/Helper/Composite.php @@ -35,15 +35,7 @@ function ($component) { uasort( $declaredComponents, function ($firstComponent, $secondComponent) { - $firstComponentSortOrder = (int)$firstComponent['sortOrder']; - $secondComponentSortOrder = (int)$secondComponent['sortOrder']; - if ($firstComponentSortOrder == $secondComponentSortOrder) { - return 0; - } elseif ($firstComponentSortOrder < $secondComponentSortOrder) { - return -1; - } else { - return 1; - } + return (int)$firstComponent['sortOrder'] <=> (int)$secondComponent['sortOrder']; } ); $declaredComponents = array_values($declaredComponents); diff --git a/lib/internal/Magento/Framework/ObjectManager/Profiler/Log.php b/lib/internal/Magento/Framework/ObjectManager/Profiler/Log.php index 9221dad9090c..c1c1e7c17709 100644 --- a/lib/internal/Magento/Framework/ObjectManager/Profiler/Log.php +++ b/lib/internal/Magento/Framework/ObjectManager/Profiler/Log.php @@ -1,6 +1,5 @@ messages; } + + /** + * @param array $config + */ + public function setConfig(array $config) + { + $this->config = $config; + } + + /** + * @return array + */ + public function getConfig(): array + { + return $this->config; + } } diff --git a/lib/internal/Magento/Framework/ObjectManager/Test/Unit/Code/Generator/_files/SampleProxy.txt b/lib/internal/Magento/Framework/ObjectManager/Test/Unit/Code/Generator/_files/SampleProxy.txt index 7dd126504484..2c56472f323c 100644 --- a/lib/internal/Magento/Framework/ObjectManager/Test/Unit/Code/Generator/_files/SampleProxy.txt +++ b/lib/internal/Magento/Framework/ObjectManager/Test/Unit/Code/Generator/_files/SampleProxy.txt @@ -101,4 +101,20 @@ class Sample_Proxy extends Sample implements \Magento\Framework\ObjectManager\No { return $this->_getSubject()->getMessages(); } + + /** + * {@inheritdoc} + */ + public function setConfig(array $config) + { + return $this->_getSubject()->setConfig($config); + } + + /** + * {@inheritdoc} + */ + public function getConfig() : array + { + return $this->_getSubject()->getConfig(); + } } diff --git a/lib/internal/Magento/Framework/ObjectManager/Test/Unit/Config/SchemaLocatorTest.php b/lib/internal/Magento/Framework/ObjectManager/Test/Unit/Config/SchemaLocatorTest.php index 1a653992e1e3..65018bcd8737 100644 --- a/lib/internal/Magento/Framework/ObjectManager/Test/Unit/Config/SchemaLocatorTest.php +++ b/lib/internal/Magento/Framework/ObjectManager/Test/Unit/Config/SchemaLocatorTest.php @@ -12,10 +12,10 @@ class SchemaLocatorTest extends \PHPUnit\Framework\TestCase */ protected $model; - /** @var \Magento\Framework\Config\Dom\UrnResolver $urnResolverMock */ + /** @var \Magento\Framework\Config\Dom\UrnResolver */ protected $urnResolver; - /** @var \Magento\Framework\Config\Dom\UrnResolver $urnResolverMock */ + /** @var \Magento\Framework\Config\Dom\UrnResolver */ protected $urnResolverMock; protected function setUp() diff --git a/lib/internal/Magento/Framework/Option/ArrayInterface.php b/lib/internal/Magento/Framework/Option/ArrayInterface.php index ecfa71227f9f..054d9059b02f 100644 --- a/lib/internal/Magento/Framework/Option/ArrayInterface.php +++ b/lib/internal/Magento/Framework/Option/ArrayInterface.php @@ -6,7 +6,10 @@ namespace Magento\Framework\Option; /** - * @todo Remove in favor of the ancestor interface + * Array marker interface + * + * @deprecated please use \Magento\Framework\Data\OptionSourceInterface instead. + * @see \Magento\Framework\Data\OptionSourceInterface */ interface ArrayInterface extends \Magento\Framework\Data\OptionSourceInterface { diff --git a/lib/internal/Magento/Framework/Option/ArrayPool.php b/lib/internal/Magento/Framework/Option/ArrayPool.php index 5ac349d99b82..11e1b46ff036 100644 --- a/lib/internal/Magento/Framework/Option/ArrayPool.php +++ b/lib/internal/Magento/Framework/Option/ArrayPool.php @@ -28,13 +28,14 @@ public function __construct(\Magento\Framework\ObjectManagerInterface $objectMan * * @param string $model * @throws \InvalidArgumentException - * @return \Magento\Framework\Option\ArrayInterface + * @return \Magento\Framework\Data\OptionSourceInterface */ public function get($model) { $modelInstance = $this->_objectManager->get($model); - if (false == $modelInstance instanceof \Magento\Framework\Option\ArrayInterface) { - throw new \InvalidArgumentException($model . 'doesn\'t implement \Magento\Framework\Option\ArrayInterface'); + if (false == $modelInstance instanceof \Magento\Framework\Data\OptionSourceInterface) { + throw new \InvalidArgumentException($model + . 'doesn\'t implement \Magento\Framework\Data\OptionSourceInterface'); } return $modelInstance; } diff --git a/lib/internal/Magento/Framework/Phrase/__.php b/lib/internal/Magento/Framework/Phrase/__.php new file mode 100644 index 000000000000..6f3186231e3b --- /dev/null +++ b/lib/internal/Magento/Framework/Phrase/__.php @@ -0,0 +1,23 @@ +methodsMapProcessor = $methodsMapProcessor; $this->typeCaster = $typeCaster; $this->fieldNamer = $fieldNamer; $this->extensionAttributesProcessor = $extensionAttributesProcessor; $this->customAttributesProcessor = $customAttributesProcessor; + $this->processors = $processors; } /** @@ -121,6 +129,27 @@ public function buildOutputDataArray($dataObject, $dataObjectType) $outputData[$key] = $value; } + + $outputData = $this->changeOutputArray($dataObject, $outputData); + + return $outputData; + } + + /** + * Change output array if needed. + * + * @param mixed $dataObject + * @param array $outputData + * @return array + */ + private function changeOutputArray($dataObject, array $outputData): array + { + foreach ($this->processors as $dataObjectClassName => $processor) { + if ($dataObject instanceof $dataObjectClassName) { + $outputData = $processor->execute($dataObject, $outputData); + } + } + return $outputData; } } diff --git a/lib/internal/Magento/Framework/Reflection/MethodsMap.php b/lib/internal/Magento/Framework/Reflection/MethodsMap.php index 944cd8771ee3..6b0ddfbfc212 100644 --- a/lib/internal/Magento/Framework/Reflection/MethodsMap.php +++ b/lib/internal/Magento/Framework/Reflection/MethodsMap.php @@ -94,6 +94,8 @@ public function getMethodReturnType($typeName, $methodName) * 'validatePassword' => 'boolean' * ] * + * @throws \InvalidArgumentException if methods don't have annotation + * @throws \ReflectionException for missing DocBock or invalid reflection class */ public function getMethodsMap($interfaceName) { @@ -148,6 +150,8 @@ public function getMethodParams($serviceClassName, $serviceMethodName) * * @param string $interfaceName * @return array + * @throws \ReflectionException for missing DocBock or invalid reflection class + * @throws \InvalidArgumentException if methods don't have annotation */ private function getMethodMapViaReflection($interfaceName) { diff --git a/lib/internal/Magento/Framework/Reflection/Test/Unit/Fixture/UseClasses/SampleOne.php b/lib/internal/Magento/Framework/Reflection/Test/Unit/Fixture/UseClasses/SampleOne.php new file mode 100644 index 000000000000..6382f4b24707 --- /dev/null +++ b/lib/internal/Magento/Framework/Reflection/Test/Unit/Fixture/UseClasses/SampleOne.php @@ -0,0 +1,13 @@ + 'addData', 'type' => 'array[]'], - ['method name' => 'addObjectList', 'type' => 'TSampleInterface[]'] + ['method name' => 'addObjectList', 'type' => '\\' . TSampleInterface::class . '[]'] ]; } @@ -354,4 +363,182 @@ public function testGetReturnTypeWithoutReturnTag() $methodReflection = $classReflection->getMethod('getName'); $this->typeProcessor->getGetterReturnType($methodReflection); } + + /** + * Simple and complex data provider + * + * @return array + */ + public function simpleAndComplexDataProvider(): array + { + return [ + ['string', true], + ['array', true], + ['int', true], + ['SomeClass', false], + ['\\My\\Namespace\\Model\\Class', false], + ['Some\\Other\\Class', false], + ]; + } + + /** + * Test simple type detection method + * + * @dataProvider simpleAndComplexDataProvider + * @param string $type + * @param bool $expectedValue + */ + public function testIsSimpleType(string $type, bool $expectedValue) + { + self::assertEquals($expectedValue, $this->typeProcessor->isSimpleType($type)); + } + + /** + * Simple and complex data provider + * + * @return array + */ + public function basicClassNameProvider(): array + { + return [ + ['SomeClass[]', 'SomeClass'], + ['\\My\\Namespace\\Model\\Class[]', '\\My\\Namespace\\Model\\Class'], + ['Some\\Other\\Class[]', 'Some\\Other\\Class'], + ['SomeClass', 'SomeClass'], + ['\\My\\Namespace\\Model\\Class', '\\My\\Namespace\\Model\\Class'], + ['Some\\Other\\Class', 'Some\\Other\\Class'], + ]; + } + + /** + * Extract basic class name + * + * @dataProvider basicClassNameProvider + * @param string $type + * @param string $expectedValue + */ + public function testBasicClassName(string $type, string $expectedValue) + { + self::assertEquals($expectedValue, $this->typeProcessor->getBasicClassName($type)); + } + + /** + * Fully qualified class names data provider + * + * @return array + */ + public function isFullyQualifiedClassNamesDataProvider(): array + { + return [ + ['SomeClass', false], + ['\\My\\Namespace\\Model\\Class', true], + ['Some\\Other\\Class', false], + ]; + } + + /** + * Test fully qualified class name detector + * + * @dataProvider isFullyQualifiedClassNamesDataProvider + * @param string $type + * @param bool $expectedValue + */ + public function testIsFullyQualifiedClassName(string $type, bool $expectedValue) + { + self::assertEquals($expectedValue, $this->typeProcessor->isFullyQualifiedClassName($type)); + } + + /** + * Test alias mapping + */ + public function testGetAliasMapping() + { + $sourceClass = new ClassReflection(UseSample::class); + $aliasMap = $this->typeProcessor->getAliasMapping($sourceClass); + + self::assertEquals([ + 'SampleOne' => SampleOne::class, + 'Sample2' => SampleTwo::class, + ], $aliasMap); + } + + /** + * Resolve fully qualified class names data provider + * + * @return array + */ + public function resolveFullyQualifiedClassNamesDataProvider(): array + { + return [ + [UseSample::class, 'string', 'string'], + [UseSample::class, 'string[]', 'string[]'], + + [UseSample::class, 'SampleOne', '\\' . SampleOne::class], + [UseSample::class, 'Sample2', '\\' . SampleTwo::class], + [ + UseSample::class, + '\\Magento\\Framework\\Reflection\\Test\\Unit\\Fixture\\UseClasses\\SampleOne', + '\\' . SampleOne::class + ], + [ + UseSample::class, + '\\Magento\\Framework\\Reflection\\Test\\Unit\\Fixture\\UseClasses\\SampleTwo', + '\\' . SampleTwo::class + ], + [UseSample::class, 'UseClasses\\SampleOne', '\\' . SampleOne::class], + [UseSample::class, 'UseClasses\\SampleTwo', '\\' . SampleTwo::class], + + [UseSample::class, 'SampleOne[]', '\\' . SampleOne::class . '[]'], + [UseSample::class, 'Sample2[]', '\\' . SampleTwo::class . '[]'], + [ + UseSample::class, + '\\Magento\\Framework\\Reflection\\Test\\Unit\\Fixture\\UseClasses\\SampleOne[]', + '\\' . SampleOne::class . '[]' + ], + [ + UseSample::class, + '\\Magento\\Framework\\Reflection\\Test\\Unit\\Fixture\\UseClasses\\SampleTwo[]', + '\\' . SampleTwo::class . '[]' + ], + [UseSample::class, 'UseClasses\\SampleOne[]', '\\' . SampleOne::class . '[]'], + [UseSample::class, 'UseClasses\\SampleTwo[]', '\\' . SampleTwo::class . '[]'], + + [UseSample::class, 'SampleOne\SampleThree', '\\' . SampleThree::class], + [UseSample::class, 'SampleOne\SampleThree[]', '\\' . SampleThree::class . '[]'], + + [UseSample::class, 'Sample2\SampleFour', '\\' . SampleFour::class], + [UseSample::class, 'Sample2\SampleFour[]', '\\' . SampleFour::class . '[]'], + + [UseSample::class, 'Sample2\NotExisting', 'Sample2\NotExisting'], + [UseSample::class, 'Sample2\NotExisting[]', 'Sample2\NotExisting[]'], + + [ + UseSample::class, + '\\Magento\\Framework\\Reflection\\Test\\Unit\\Fixture\\UseClasses\\NotExisting', + '\\Magento\\Framework\\Reflection\\Test\\Unit\\Fixture\\UseClasses\\NotExisting' + ], + [ + UseSample::class, + '\\Magento\\Framework\\Reflection\\Test\\Unit\\Fixture\\UseClasses\\NotExisting[]', + '\\Magento\\Framework\\Reflection\\Test\\Unit\\Fixture\\UseClasses\\NotExisting[]' + ], + ]; + } + + /** + * Resolve fully qualified class names + * + * @dataProvider resolveFullyQualifiedClassNamesDataProvider + * @param string $className + * @param string $type + * @param string $expectedValue + * @throws \ReflectionException + */ + public function testResolveFullyQualifiedClassNames(string $className, string $type, string $expectedValue) + { + $sourceClass = new ClassReflection($className); + $fullyQualified = $this->typeProcessor->resolveFullyQualifiedClassName($sourceClass, $type); + + self::assertEquals($expectedValue, $fullyQualified); + } } diff --git a/lib/internal/Magento/Framework/Reflection/TypeProcessor.php b/lib/internal/Magento/Framework/Reflection/TypeProcessor.php index d7206032c68c..9571fa53547a 100644 --- a/lib/internal/Magento/Framework/Reflection/TypeProcessor.php +++ b/lib/internal/Magento/Framework/Reflection/TypeProcessor.php @@ -512,7 +512,7 @@ public function processSimpleAndAnyType($value, $type) public function getParamType(ParameterReflection $param) { $type = $param->detectType(); - if ($type == 'null') { + if ($type === 'null') { throw new \LogicException(sprintf( '@param annotation is incorrect for the parameter "%s" in the method "%s:%s".' . ' First declared type should not be null. E.g. string|null', @@ -521,14 +521,149 @@ public function getParamType(ParameterReflection $param) $param->getDeclaringFunction()->name )); } - if ($type == 'array') { + if ($type === 'array') { // try to determine class, if it's array of objects $paramDocBlock = $this->getParamDocBlockTag($param); $paramTypes = $paramDocBlock->getTypes(); $paramType = array_shift($paramTypes); + + $paramType = $this->resolveFullyQualifiedClassName($param->getDeclaringClass(), $paramType); + return strpos($paramType, '[]') !== false ? $paramType : "{$paramType}[]"; } - return $type; + + return $this->resolveFullyQualifiedClassName($param->getDeclaringClass(), $type); + } + + /** + * Get alias mapping for source class + * + * @param ClassReflection $sourceClass + * @return array + */ + public function getAliasMapping(ClassReflection $sourceClass): array + { + $sourceFileName = $sourceClass->getDeclaringFile(); + $aliases = []; + foreach ($sourceFileName->getUses() as $use) { + if ($use['as'] !== null) { + $aliases[$use['as']] = $use['use']; + } else { + $pos = strrpos($use['use'], '\\'); + + $aliasName = substr($use['use'], $pos + 1); + $aliases[$aliasName] = $use['use']; + } + } + + return $aliases; + } + + /** + * Return true if the passed type is a simple type + * + * Eg.: + * Return true with; array, string, ... + * Return false with: SomeClassName + * + * @param string $typeName + * @return bool + */ + public function isSimpleType(string $typeName): bool + { + return strtolower($typeName) === $typeName; + } + + /** + * Get basic type for a class name + * + * Eg.: + * SomeClassName[] => SomeClassName + * + * @param string $className + * @return string + */ + public function getBasicClassName(string $className): string + { + $pos = strpos($className, '['); + return ($pos === false) ? $className : substr($className, 0, $pos); + } + + /** + * Return true if it is a FQ class name + * + * Eg.: + * SomeClassName => false + * \My\NameSpace\SomeClassName => true + * + * @param string $className + * @return bool + */ + public function isFullyQualifiedClassName(string $className): bool + { + return strpos($className, '\\') === 0; + } + + /** + * Get aliased class name + * + * @param string $className + * @param string $namespace + * @param array $aliases + * @return string + */ + private function getAliasedClassName(string $className, string $namespace, array $aliases): string + { + $pos = strpos($className, '\\'); + if ($pos === false) { + $namespacePrefix = $className; + $partialClassName = ''; + } else { + $namespacePrefix = substr($className, 0, $pos); + $partialClassName = substr($className, $pos); + } + + if (isset($aliases[$namespacePrefix])) { + return $aliases[$namespacePrefix] . $partialClassName; + } + + return $namespace . '\\' . $className; + } + + /** + * Resolve fully qualified type name in the class alias context + * + * @param ClassReflection $sourceClass + * @param string $typeName + * @return string + */ + public function resolveFullyQualifiedClassName(ClassReflection $sourceClass, string $typeName): string + { + $typeName = trim($typeName); + + // Simple way to understand it is a basic type or a class name + if ($this->isSimpleType($typeName)) { + return $typeName; + } + + $basicTypeName = $this->getBasicClassName($typeName); + + // Already a FQN class name + if ($this->isFullyQualifiedClassName($basicTypeName)) { + return '\\' . substr($typeName, 1); + } + + $isArray = $this->isArrayType($typeName); + $aliases = $this->getAliasMapping($sourceClass); + + $namespace = $sourceClass->getNamespaceName(); + $fqClassName = '\\' . $this->getAliasedClassName($basicTypeName, $namespace, $aliases); + + if (interface_exists($fqClassName) || class_exists($fqClassName)) { + return $fqClassName . ($isArray ? '[]' : ''); + } + + return $typeName; } /** diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Adapter.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Adapter.php index 11bd746eb71e..e1b423d738a2 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Adapter.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Adapter.php @@ -7,6 +7,7 @@ use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Ddl\Table; +use Magento\Framework\DB\Select; use Magento\Framework\Search\Adapter\Mysql\Aggregation\Builder as AggregationBuilder; use Magento\Framework\Search\AdapterInterface; use Magento\Framework\Search\RequestInterface; @@ -14,6 +15,8 @@ /** * MySQL Search Adapter * + * @deprecated + * @see \Magento\ElasticSearch * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Adapter implements AdapterInterface @@ -47,6 +50,16 @@ class Adapter implements AdapterInterface */ private $temporaryStorageFactory; + /** + * Query Select Parts to be skipped when prepare query for count + * + * @var array + */ + private $countSqlSkipParts = [ + \Magento\Framework\DB\Select::LIMIT_COUNT => true, + \Magento\Framework\DB\Select::LIMIT_OFFSET => true, + ]; + /** * @param Mapper $mapper * @param ResponseFactory $responseFactory @@ -84,6 +97,7 @@ public function query(RequestInterface $request) $response = [ 'documents' => $documents, 'aggregations' => $aggregations, + 'total' => $this->getSize($query) ]; return $this->responseFactory->create($response); } @@ -104,10 +118,47 @@ private function getDocuments(Table $table) } /** + * Get connection. + * * @return false|\Magento\Framework\DB\Adapter\AdapterInterface */ private function getConnection() { return $this->resource->getConnection(); } + + /** + * Get rows size + * + * @param Select $query + * @return int + */ + private function getSize(Select $query): int + { + $sql = $this->getSelectCountSql($query); + $parentSelect = $this->getConnection()->select(); + $parentSelect->from(['core_select' => $sql]); + $parentSelect->reset(\Magento\Framework\DB\Select::COLUMNS); + $parentSelect->columns('COUNT(*)'); + $totalRecords = $this->getConnection()->fetchOne($parentSelect); + + return intval($totalRecords); + } + + /** + * Reset limit and offset + * + * @param Select $query + * @return Select + */ + private function getSelectCountSql(Select $query): Select + { + foreach ($this->countSqlSkipParts as $part => $toSkip) { + if ($toSkip) { + $query->reset($part); + } + } + + return $query; + } } diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder.php index 47d991274b9e..4a5802dd44e0 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder.php @@ -15,6 +15,10 @@ use Magento\Framework\Search\RequestInterface; /** + * MySQL search aggregation builder. + * + * @deprecated + * @see \Magento\ElasticSearch * @api */ class Builder @@ -66,6 +70,8 @@ public function __construct( } /** + * Build aggregations. + * * @param RequestInterface $request * @param Table $documentsTable * @param array $documents @@ -77,6 +83,8 @@ public function build(RequestInterface $request, Table $documentsTable, array $d } /** + * Process aggregations. + * * @param RequestInterface $request * @param Table $documentsTable * @param array $documents @@ -102,7 +110,7 @@ private function processAggregations(RequestInterface $request, Table $documents } /** - * Extract document ids + * Extract document ids. * * @param array $documents * @return array @@ -113,7 +121,7 @@ private function extractDocumentIds(array $documents) } /** - * Get document ids + * Get document ids. * * @param Table $documentsTable * @return array @@ -128,7 +136,7 @@ private function getDocumentIds(Table $documentsTable) } /** - * Get Connection + * Get Connection. * * @return AdapterInterface */ diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder/BucketInterface.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder/BucketInterface.php index 90220676e006..4fa2474d2258 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder/BucketInterface.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder/BucketInterface.php @@ -11,12 +11,16 @@ use Magento\Framework\Search\Request\Dimension; /** - * Interface \Magento\Framework\Search\Adapter\Mysql\Aggregation\Builder\BucketInterface + * MySQL search aggregation bucket builder. * + * @deprecated + * @see \Magento\ElasticSearch */ interface BucketInterface { /** + * Build bucket. + * * @param DataProviderInterface $dataProvider * @param Dimension[] $dimensions * @param RequestBucketInterface $bucket diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder/Container.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder/Container.php index cba29ad6287d..844cfc9f8741 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder/Container.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder/Container.php @@ -5,6 +5,12 @@ */ namespace Magento\Framework\Search\Adapter\Mysql\Aggregation\Builder; +/** + * MySQL search aggregation container builder. + * + * @deprecated + * @see \Magento\ElasticSearch + */ class Container { /** @@ -21,6 +27,8 @@ public function __construct(array $buckets) } /** + * Get bucket by type. + * * @param string $bucketType * @return BucketInterface */ diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder/Dynamic.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder/Dynamic.php index 481b2252dbf6..46828ab7a8c7 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder/Dynamic.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder/Dynamic.php @@ -12,6 +12,12 @@ use Magento\Framework\Search\Request\Aggregation\DynamicBucket; use Magento\Framework\Search\Request\BucketInterface as RequestBucketInterface; +/** + * MySQL search dynamic aggregation builder. + * + * @deprecated + * @see \Magento\ElasticSearch + */ class Dynamic implements BucketInterface { /** @@ -37,7 +43,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function build( DataProviderInterface $dataProvider, diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder/Metrics.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder/Metrics.php index 840f94622183..e4cdb04052ef 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder/Metrics.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder/Metrics.php @@ -7,6 +7,12 @@ use Magento\Framework\Search\Request\BucketInterface as RequestBucketInterface; +/** + * MySQL search aggregation metrics builder. + * + * @deprecated + * @see \Magento\ElasticSearch + */ class Metrics { /** diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder/Range.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder/Range.php index 29fc6806b50e..aced57c10013 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder/Range.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder/Range.php @@ -14,6 +14,12 @@ use Magento\Framework\Search\Request\BucketInterface as RequestBucketInterface; use Magento\Framework\Translate\AdapterInterface; +/** + * MySQL search aggregation range builder. + * + * @deprecated + * @see \Magento\ElasticSearch + */ class Range implements BucketInterface { const GREATER_THAN = '>='; @@ -46,7 +52,7 @@ public function __construct(Metrics $metricsBuilder, ResourceConnection $resourc } /** - * {@inheritdoc} + * @inheritdoc */ public function build( DataProviderInterface $dataProvider, @@ -70,6 +76,8 @@ public function build( } /** + * Generate case. + * * @param Select $select * @param AggregationRange[] $ranges * @return Select diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder/Term.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder/Term.php index 78ff7b04fd39..547526be43cc 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder/Term.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder/Term.php @@ -9,6 +9,12 @@ use Magento\Framework\Search\Adapter\Mysql\Aggregation\DataProviderInterface; use Magento\Framework\Search\Request\BucketInterface as RequestBucketInterface; +/** + * MySQL search aggregation term builder. + * + * @deprecated + * @see \Magento\ElasticSearch + */ class Term implements BucketInterface { /** @@ -25,7 +31,7 @@ public function __construct(Metrics $metricsBuilder) } /** - * {@inheritdoc} + * @inheritdoc */ public function build( DataProviderInterface $dataProvider, diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/DataProviderContainer.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/DataProviderContainer.php index 6a817bcbddf8..565b5eeef335 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/DataProviderContainer.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/DataProviderContainer.php @@ -6,6 +6,10 @@ namespace Magento\Framework\Search\Adapter\Mysql\Aggregation; /** + * MySQL search data provider container. + * + * @deprecated + * @see \Magento\ElasticSearch * @api */ class DataProviderContainer @@ -24,6 +28,8 @@ public function __construct(array $dataProviders) } /** + * Get data provider by index name. + * * @param string $indexName * @return DataProviderInterface */ diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/DataProviderInterface.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/DataProviderInterface.php index b18269335bc6..c251265c694d 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/DataProviderInterface.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/DataProviderInterface.php @@ -11,8 +11,10 @@ use Magento\Framework\Search\Request\Dimension; /** - * Interface \Magento\Framework\Search\Adapter\Mysql\Aggregation\DataProviderInterface + * MySQL search data provider. * + * @deprecated + * @see \Magento\ElasticSearch */ interface DataProviderInterface { diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Interval.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Interval.php index ed5640d42274..ba41a535f45c 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Interval.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Interval.php @@ -8,6 +8,12 @@ use Magento\Framework\DB\Select; use Magento\Framework\Search\Dynamic\IntervalInterface; +/** + * MySQL search aggregation interval. + * + * @deprecated + * @see \Magento\ElasticSearch + */ class Interval implements IntervalInterface { /** @@ -41,7 +47,7 @@ private function getValueFiled() } /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ public function load($limit, $offset = null, $lower = null, $upper = null) @@ -64,7 +70,7 @@ public function load($limit, $offset = null, $lower = null, $upper = null) } /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ public function loadPrevious($data, $index, $lower = null) @@ -86,7 +92,7 @@ public function loadPrevious($data, $index, $lower = null) } /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ public function loadNext($data, $rightIndex, $upper = null) @@ -124,6 +130,8 @@ public function loadNext($data, $rightIndex, $upper = null) } /** + * Convert array values to float. + * * @param array $prices * @return array */ diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/AggregationFactory.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/AggregationFactory.php index 4d255705bf7d..756d9edc6fbe 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/AggregationFactory.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/AggregationFactory.php @@ -7,6 +7,9 @@ /** * Aggregation Factory + * + * @deprecated + * @see \Magento\ElasticSearch */ class AggregationFactory { diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/ConditionManager.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/ConditionManager.php index 41d6b14b6a47..e56559563c35 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/ConditionManager.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/ConditionManager.php @@ -9,7 +9,11 @@ use Magento\Framework\DB\Adapter\AdapterInterface; /** + * MySQL search condition manager + * * @api + * @deprecated + * @see \Magento\ElasticSearch */ class ConditionManager { @@ -30,6 +34,8 @@ public function __construct(ResourceConnection $resource) } /** + * Wrap query with parentheses. + * * @param string $query * @return string */ @@ -41,6 +47,8 @@ public function wrapBrackets($query) } /** + * Combine multiple queries. + * * @param string[] $queries * @param string $unionOperator * @return string @@ -54,6 +62,8 @@ public function combineQueries(array $queries, $unionOperator) } /** + * Generate query condition. + * * @param string $field * @param string $operator * @param mixed $value diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/DocumentFactory.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/DocumentFactory.php index 3ca0e94852a5..4e8854fad353 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/DocumentFactory.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/DocumentFactory.php @@ -13,7 +13,10 @@ /** * Document Factory + * * @api + * @deprecated + * @see \Magento\ElasticSearch */ class DocumentFactory { diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Field/Field.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Field/Field.php index 72495e68a26f..0a340e7f76dc 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Field/Field.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Field/Field.php @@ -6,6 +6,11 @@ namespace Magento\Framework\Search\Adapter\Mysql\Field; +/** + * @inheritdoc + * @deprecated + * @see \Magento\ElasticSearch + */ class Field implements FieldInterface { /** @@ -36,6 +41,8 @@ public function __construct($column, $attributeId = null, $type = self::TYPE_FUL } /** + * Get column. + * * @return string */ public function getColumn() @@ -44,6 +51,8 @@ public function getColumn() } /** + * Get attribute ID. + * * @return int|null */ public function getAttributeId() @@ -52,6 +61,8 @@ public function getAttributeId() } /** + * Get type. + * * @return int */ public function getType() diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Field/FieldFactory.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Field/FieldFactory.php index 9a3c55965740..066d1832aefb 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Field/FieldFactory.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Field/FieldFactory.php @@ -7,7 +7,11 @@ namespace Magento\Framework\Search\Adapter\Mysql\Field; /** + * MySQL search field factory. + * * @api + * @deprecated + * @see \Magento\ElasticSearch */ class FieldFactory { diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Field/FieldInterface.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Field/FieldInterface.php index 5f4e0027a850..7dc74f39709f 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Field/FieldInterface.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Field/FieldInterface.php @@ -7,8 +7,10 @@ namespace Magento\Framework\Search\Adapter\Mysql\Field; /** - * Interface \Magento\Framework\Search\Adapter\Mysql\Field\FieldInterface + * MySQL search field. * + * @deprecated + * @see \Magento\ElasticSearch */ interface FieldInterface { @@ -16,19 +18,22 @@ interface FieldInterface const TYPE_FULLTEXT = 2; /** - * Get type of index + * Get type of index. + * * @return int */ public function getType(); /** - * Get ID of attribute + * Get ID of attribute. + * * @return int */ public function getAttributeId(); /** - * Get field name + * Get field nam. + * * @return string */ public function getColumn(); diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Field/Resolver.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Field/Resolver.php index 5146177783ae..908d6ccc5074 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Field/Resolver.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Field/Resolver.php @@ -5,6 +5,9 @@ */ namespace Magento\Framework\Search\Adapter\Mysql\Field; +/** + * @inheritdoc + */ class Resolver implements ResolverInterface { /** @@ -21,7 +24,7 @@ public function __construct(FieldFactory $fieldFactory) } /** - * {@inheritdoc} + * @inheritdoc */ public function resolve(array $fields) { diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Field/ResolverInterface.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Field/ResolverInterface.php index f275c4f1b1c1..5455b91d7302 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Field/ResolverInterface.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Field/ResolverInterface.php @@ -6,13 +6,15 @@ namespace Magento\Framework\Search\Adapter\Mysql\Field; /** - * Interface \Magento\Framework\Search\Adapter\Mysql\Field\ResolverInterface + * MySQL search field resolver. * + * @deprecated + * @see \Magento\ElasticSearch */ interface ResolverInterface { /** - * Resolve field + * Resolve field. * * @param array $fields * @return FieldInterface[] diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Filter/Builder.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Filter/Builder.php index 80074ffc5e2b..ce02bef244fb 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Filter/Builder.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Filter/Builder.php @@ -15,6 +15,7 @@ use Magento\Framework\Search\Request\Query\BoolExpression; /** + * @inheritdoc * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Builder implements BuilderInterface @@ -58,7 +59,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function build(RequestFilterInterface $filter, $conditionType) { @@ -66,6 +67,8 @@ public function build(RequestFilterInterface $filter, $conditionType) } /** + * Process filter. + * * @param RequestFilterInterface $filter * @param bool $isNegation * @return string @@ -87,6 +90,8 @@ private function processFilter(RequestFilterInterface $filter, $isNegation) } /** + * Process boolean filter. + * * @param RequestFilterInterface|\Magento\Framework\Search\Request\Filter\Bool $filter * @param bool $isNegation * @return string @@ -111,6 +116,8 @@ private function processBoolFilter(RequestFilterInterface $filter, $isNegation) } /** + * Build filters. + * * @param \Magento\Framework\Search\Request\FilterInterface[] $filters * @param string $unionOperator * @param bool $isNegation @@ -127,6 +134,8 @@ private function buildFilters(array $filters, $unionOperator, $isNegation) } /** + * Check if condition type is 'negative'. + * * @param string $conditionType * @return bool */ diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Filter/Builder/FilterInterface.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Filter/Builder/FilterInterface.php index 787866a2bca3..6f8b63955a0c 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Filter/Builder/FilterInterface.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Filter/Builder/FilterInterface.php @@ -8,12 +8,16 @@ use Magento\Framework\Search\Request\FilterInterface as RequestFilterInterface; /** - * Interface \Magento\Framework\Search\Adapter\Mysql\Filter\Builder\FilterInterface + * MySQL search filter builder. * + * @deprecated + * @see \Magento\ElasticSearch */ interface FilterInterface { /** + * Build filter. + * * @param RequestFilterInterface $filter * @param bool $isNegation * @return string diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Filter/Builder/Range.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Filter/Builder/Range.php index d4ee9542c593..d14bfdcb548d 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Filter/Builder/Range.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Filter/Builder/Range.php @@ -9,6 +9,12 @@ use Magento\Framework\Search\Request\Filter\Range as RangeFilterRequest; use Magento\Framework\Search\Request\FilterInterface as RequestFilterInterface; +/** + * Range filter builder. + * + * @deprecated + * @see \Magento\ElasticSearch + */ class Range implements FilterInterface { const CONDITION_PART_GREATER_THAN = '>='; @@ -31,7 +37,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function buildFilter( RequestFilterInterface $filter, @@ -48,6 +54,8 @@ public function buildFilter( } /** + * Get left condition filter part. + * * @param RequestFilterInterface|RangeFilterRequest $filter * @param bool $isNegation * @return string @@ -62,6 +70,8 @@ private function getLeftConditionPart(RequestFilterInterface $filter, $isNegatio } /** + * Get right condition filter part. + * * @param RequestFilterInterface|RangeFilterRequest $filter * @param bool $isNegation * @return string @@ -76,6 +86,8 @@ private function getRightConditionPart(RequestFilterInterface $filter, $isNegati } /** + * Get filter part. + * * @param string $field * @param string $operator * @param string $value @@ -89,6 +101,8 @@ private function getPart($field, $operator, $value) } /** + * Get condition union operator. + * * @param bool $isNegation * @return string */ diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Filter/Builder/Term.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Filter/Builder/Term.php index 3ac755de2b8a..c89ef50f3cb3 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Filter/Builder/Term.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Filter/Builder/Term.php @@ -8,6 +8,12 @@ use Magento\Framework\Search\Adapter\Mysql\ConditionManager; use Magento\Framework\Search\Request\FilterInterface as RequestFilterInterface; +/** + * Term filter builder. + * + * @deprecated + * @see \Magento\ElasticSearch + */ class Term implements FilterInterface { const CONDITION_OPERATOR_EQUALS = '='; @@ -30,7 +36,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function buildFilter( RequestFilterInterface $filter, @@ -46,6 +52,8 @@ public function buildFilter( } /** + * Get condition operator. + * * @param string|array $value * @param bool $isNegation * @return string diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Filter/Builder/Wildcard.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Filter/Builder/Wildcard.php index 308d1d564301..9a2776ac20b2 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Filter/Builder/Wildcard.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Filter/Builder/Wildcard.php @@ -7,6 +7,12 @@ use Magento\Framework\Search\Adapter\Mysql\ConditionManager; +/** + * Wildcard filter builder. + * + * @deprecated + * @see \Magento\ElasticSearch + */ class Wildcard implements FilterInterface { const CONDITION_LIKE = 'LIKE'; @@ -27,7 +33,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function buildFilter( \Magento\Framework\Search\Request\FilterInterface $filter, diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Filter/BuilderInterface.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Filter/BuilderInterface.php index ec3b88ad2450..3da989333d66 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Filter/BuilderInterface.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Filter/BuilderInterface.php @@ -8,12 +8,16 @@ use Magento\Framework\Search\Request\FilterInterface as RequestFilterInterface; /** - * Interface \Magento\Framework\Search\Adapter\Mysql\Filter\BuilderInterface + * MySQL search filter builder. * + * @deprecated + * @see \Magento\ElasticSearch */ interface BuilderInterface { /** + * Buil filter. + * * @param RequestFilterInterface $filter * @param string $conditionType * @return string diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Filter/Preprocessor.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Filter/Preprocessor.php index a8bb8e255f2c..32d134cfe8d6 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Filter/Preprocessor.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Filter/Preprocessor.php @@ -8,6 +8,9 @@ use Magento\Framework\Search\Adapter\Mysql\ConditionManager; use Magento\Framework\Search\Request\FilterInterface; +/** + * @inheritdoc + */ class Preprocessor implements PreprocessorInterface { /** @@ -24,7 +27,7 @@ public function __construct(ConditionManager $conditionManager) } /** - * {@inheritdoc} + * @inheritdoc */ public function process(FilterInterface $filter, $isNegation, $query) { diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Filter/PreprocessorInterface.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Filter/PreprocessorInterface.php index 430b775337b7..eb0bbc6f3b56 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Filter/PreprocessorInterface.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Filter/PreprocessorInterface.php @@ -8,12 +8,16 @@ use Magento\Framework\Search\Request\FilterInterface; /** - * Interface \Magento\Framework\Search\Adapter\Mysql\Filter\PreprocessorInterface + * MySQL search filter pre-processor. * + * @deprecated + * @see \Magento\ElasticSearch */ interface PreprocessorInterface { /** + * Process filter. + * * @param FilterInterface $filter * @param bool $isNegation * @param string $query diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/IndexBuilderInterface.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/IndexBuilderInterface.php index 73c20406f35d..fbd35455c229 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/IndexBuilderInterface.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/IndexBuilderInterface.php @@ -10,6 +10,9 @@ /** * Build base Query for Index + * + * @deprecated + * @see \Magento\ElasticSearch */ interface IndexBuilderInterface { diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Mapper.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Mapper.php index fb4ff5d298c1..e97a6690dbcc 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Mapper.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Mapper.php @@ -22,8 +22,11 @@ /** * Mapper class. Maps library request to specific adapter dependent query + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api + * @deprecated + * @see \Magento\ElasticSearch */ class Mapper { @@ -344,6 +347,8 @@ private function processFilterQuery( } /** + * Add match queries to select. + * * @param RequestInterface $request * @param QueryContainer $queryContainer * @param ScoreBuilder $scoreBuilder @@ -380,6 +385,8 @@ private function addDerivedQueries( } /** + * Get connection. + * * @return false|\Magento\Framework\DB\Adapter\AdapterInterface */ private function getConnection() @@ -388,6 +395,8 @@ private function getConnection() } /** + * Add match queries to select. + * * @param RequestInterface $request * @param Select $select * @param IndexBuilderInterface $indexBuilder @@ -424,6 +433,8 @@ private function addMatchQueries( } /** + * Join previous result to select. + * * @param Select $query * @param Table $previousResultTable * @param ScoreBuilder $scoreBuilder diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Query/Builder/Match.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Query/Builder/Match.php index 8e3758817adf..d4c98cda17e1 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Query/Builder/Match.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Query/Builder/Match.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Framework\Search\Adapter\Mysql\Query\Builder; use Magento\Framework\DB\Helper\Mysql\Fulltext; @@ -15,13 +17,20 @@ use Magento\Framework\Search\Adapter\Preprocessor\PreprocessorInterface; /** + * MySQL search query match. + * * @api + * @deprecated + * @see \Magento\ElasticSearch */ class Match implements QueryInterface { + /** + * @var string + */ const SPECIAL_CHARACTERS = '-+~/\\<>\'":*$#@()!,.?`=%&^'; - const MINIMAL_CHARACTER_LENGTH = 3; + const MINIMAL_CHARACTER_LENGTH = 1; /** * @var string[] @@ -69,7 +78,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function build( ScoreBuilder $scoreBuilder, @@ -113,6 +122,8 @@ public function build( } /** + * Prepare query value for build function. + * * @param string $queryValue * @param string $conditionType * @return string diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Query/Builder/QueryInterface.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Query/Builder/QueryInterface.php index a2446264d48d..6796a1f995e4 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Query/Builder/QueryInterface.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Query/Builder/QueryInterface.php @@ -8,12 +8,16 @@ use Magento\Framework\Search\Adapter\Mysql\ScoreBuilder; /** - * Interface \Magento\Framework\Search\Adapter\Mysql\Query\Builder\QueryInterface + * MySQL search query builder. * + * @deprecated + * @see \Magento\ElasticSearch */ interface QueryInterface { /** + * Build query. + * * @param \Magento\Framework\Search\Adapter\Mysql\ScoreBuilder $scoreBuilder * @param \Magento\Framework\DB\Select $select * @param \Magento\Framework\Search\Request\QueryInterface $query diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Query/MatchContainer.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Query/MatchContainer.php index b694cd48a07a..6ed3338c040e 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Query/MatchContainer.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Query/MatchContainer.php @@ -10,6 +10,12 @@ // @codeCoverageIgnore +/** + * MySQL search query match container. + * + * @deprecated + * @see \Magento\ElasticSearch + */ class MatchContainer { /** @@ -34,6 +40,8 @@ public function __construct(QueryInterface $request, $conditionType) } /** + * Get request. + * * @return QueryInterface */ public function getRequest() @@ -42,6 +50,8 @@ public function getRequest() } /** + * Get condition type. + * * @return string */ public function getConditionType() diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Query/MatchContainerFactory.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Query/MatchContainerFactory.php index ba103e060ae7..cb10de7bdcbb 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Query/MatchContainerFactory.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Query/MatchContainerFactory.php @@ -7,6 +7,9 @@ /** * MatchContainer Factory + * + * @deprecated + * @see \Magento\ElasticSearch */ class MatchContainerFactory { diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Query/QueryContainer.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Query/QueryContainer.php index be8507838e44..9161a30f9bc5 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Query/QueryContainer.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Query/QueryContainer.php @@ -9,6 +9,12 @@ use Magento\Framework\DB\Select; use Magento\Framework\Search\Request\QueryInterface as RequestQueryInterface; +/** + * MySQL search query container. + * + * @deprecated + * @see \Magento\ElasticSearch + */ class QueryContainer { const DERIVED_QUERY_PREFIX = 'derived_'; @@ -32,6 +38,8 @@ public function __construct(MatchContainerFactory $matchContainerFactory) } /** + * Add query to select. + * * @param Select $select * @param RequestQueryInterface $query * @param string $conditionType @@ -54,6 +62,8 @@ public function addMatchQuery( } /** + * Get queries. + * * @return MatchContainer[] */ public function getMatchQueries() diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Query/QueryContainerFactory.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Query/QueryContainerFactory.php index 70fefec13dd8..59ee4bb045b2 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Query/QueryContainerFactory.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Query/QueryContainerFactory.php @@ -7,6 +7,9 @@ /** * MatchContainer Factory + * + * @deprecated + * @see \Magento\ElasticSearch */ class QueryContainerFactory { diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/ResponseFactory.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/ResponseFactory.php index 8a269d8d95bc..b41a43883fde 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/ResponseFactory.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/ResponseFactory.php @@ -7,6 +7,9 @@ /** * Response Factory + * + * @deprecated + * @see \Magento\ElasticSearch */ class ResponseFactory { @@ -65,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/Adapter/Mysql/ScoreBuilder.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/ScoreBuilder.php index 190d3a04ed21..1cc417f891c1 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/ScoreBuilder.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/ScoreBuilder.php @@ -9,6 +9,8 @@ * Class for generating sql condition for calculating store manager * * @api + * @deprecated + * @see \Magento\ElasticSearch */ class ScoreBuilder { diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/ScoreBuilderFactory.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/ScoreBuilderFactory.php index 4a7612fcc304..70aa749fd859 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/ScoreBuilderFactory.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/ScoreBuilderFactory.php @@ -7,6 +7,9 @@ /** * ScoreBuilder Factory + * + * @deprecated + * @see \Magento\ElasticSearch */ class ScoreBuilderFactory { diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/TemporaryStorage.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/TemporaryStorage.php index dcb977bfa2ae..60ee2d570606 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/TemporaryStorage.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/TemporaryStorage.php @@ -13,7 +13,11 @@ use Magento\Framework\DB\Select; /** + * MySQL search temporary storage. + * * @api + * @deprecated + * @see \Magento\ElasticSearch */ class TemporaryStorage { @@ -100,6 +104,8 @@ private function populateTemporaryTable(Table $table, $data) } /** + * Store select results in temporary table. + * * @param Select $select * @return Table * @throws \Zend_Db_Exception @@ -112,6 +118,8 @@ public function storeDocumentsFromSelect(Select $select) } /** + * Get connection. + * * @return false|AdapterInterface */ private function getConnection() @@ -120,6 +128,8 @@ private function getConnection() } /** + * Create temporary table for search select results. + * * @return Table * @throws \Zend_Db_Exception */ diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/TemporaryStorageFactory.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/TemporaryStorageFactory.php index 4a282faf317c..208f6b39b9eb 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/TemporaryStorageFactory.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/TemporaryStorageFactory.php @@ -12,6 +12,8 @@ * * @codeCoverageIgnore * @api + * @deprecated + * @see \Magento\ElasticSearch */ class TemporaryStorageFactory { diff --git a/lib/internal/Magento/Framework/Search/Request.php b/lib/internal/Magento/Framework/Search/Request.php index d55b2085e87e..60f333804661 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 34b5d72005e7..9df1ee87eb86 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 a16b3369cdda..74bc65010a93 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 16df80f755c0..2de756e754a2 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 c2328398e53d..90c7056ea254 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 3b8952853260..c6c0d0ab59e1 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 16dfc6fdd1dd..fe228546b55f 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 40fcd493861d..2314252f4609 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 55d26493ca37..fbb56361bfe7 100644 --- a/lib/internal/Magento/Framework/Search/Test/Unit/Adapter/Mysql/AdapterTest.php +++ b/lib/internal/Magento/Framework/Search/Test/Unit/Adapter/Mysql/AdapterTest.php @@ -155,15 +155,22 @@ public function testQuery() 'aggregation2' => [2, 4], ], ], + 'total' => 1 ]; $select = $this->getMockBuilder(\Magento\Framework\DB\Select::class) ->disableOriginalConstructor() ->getMock(); - $this->connectionAdapter->expects($this->once()) + + $this->connectionAdapter->expects($this->exactly(2)) ->method('select') ->willReturn($select); + $this->connectionAdapter->expects($this->once()) + ->method('fetchOne') + ->with($select) + ->willReturn($selectResult['total']); + $table = $this->getMockBuilder(\Magento\Framework\DB\Ddl\Table::class) ->disableOriginalConstructor() ->getMock(); diff --git a/lib/internal/Magento/Framework/Search/Test/Unit/Adapter/Mysql/Filter/BuilderTest.php b/lib/internal/Magento/Framework/Search/Test/Unit/Adapter/Mysql/Filter/BuilderTest.php index 2a7aa8561b27..26e6aba1d33b 100644 --- a/lib/internal/Magento/Framework/Search/Test/Unit/Adapter/Mysql/Filter/BuilderTest.php +++ b/lib/internal/Magento/Framework/Search/Test/Unit/Adapter/Mysql/Filter/BuilderTest.php @@ -29,7 +29,7 @@ class BuilderTest extends \PHPUnit\Framework\TestCase private $preprocessor; /** - * @var ConditionManager|\PHPUnit_Framework_MockObject_MockObject $conditionManager + * @var ConditionManager|\PHPUnit_Framework_MockObject_MockObject */ private $conditionManager; 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 c0699156decd..3d21064b13d4 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 a60a280cb602..a994559f28f0 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 a0b11959a42d..9ffafae9622f 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 a49124748b29..7d277382b698 100644 --- a/lib/internal/Magento/Framework/Search/etc/requests.xsd +++ b/lib/internal/Magento/Framework/Search/etc/requests.xsd @@ -28,8 +28,8 @@ - - + + diff --git a/lib/internal/Magento/Framework/Serialize/README.md b/lib/internal/Magento/Framework/Serialize/README.md index 5af8fb7f71b6..d900f89208a5 100644 --- a/lib/internal/Magento/Framework/Serialize/README.md +++ b/lib/internal/Magento/Framework/Serialize/README.md @@ -3,6 +3,7 @@ **Serialize** library provides interface *SerializerInterface* and multiple implementations: * *Json* - default implementation. Uses PHP native json_encode/json_decode functions; + * *JsonHexTag* - default implementation. Uses PHP native json_encode/json_decode functions with `JSON_HEX_TAG` option enabled; * *Serialize* - less secure than *Json*, but gives higher performance on big arrays. Uses PHP native serialize/unserialize functions, does not unserialize objects on PHP 7. Using *Serialize* implementation directly is discouraged, always use *SerializerInterface*, using *Serialize* implementation may lead to security vulnerabilities. \ No newline at end of file diff --git a/lib/internal/Magento/Framework/Serialize/Serializer/FormData.php b/lib/internal/Magento/Framework/Serialize/Serializer/FormData.php new file mode 100644 index 000000000000..a945822d92f9 --- /dev/null +++ b/lib/internal/Magento/Framework/Serialize/Serializer/FormData.php @@ -0,0 +1,53 @@ +serializer = $serializer; + } + + /** + * Provides form data from the serialized data. + * + * @param string $serializedData + * @return array + * @throws \InvalidArgumentException + */ + public function unserialize(string $serializedData): array + { + $encodedFields = $this->serializer->unserialize($serializedData); + + if (!is_array($encodedFields)) { + throw new \InvalidArgumentException('Unable to unserialize value.'); + } + + $formData = []; + foreach ($encodedFields as $item) { + $decodedFieldData = []; + parse_str($item, $decodedFieldData); + $formData = array_replace_recursive($formData, $decodedFieldData); + } + + return $formData; + } +} diff --git a/lib/internal/Magento/Framework/Serialize/Serializer/Json.php b/lib/internal/Magento/Framework/Serialize/Serializer/Json.php index e352d0c2d712..7ce9756ff243 100644 --- a/lib/internal/Magento/Framework/Serialize/Serializer/Json.php +++ b/lib/internal/Magento/Framework/Serialize/Serializer/Json.php @@ -16,27 +16,27 @@ class Json implements SerializerInterface { /** - * {@inheritDoc} + * @inheritDoc * @since 100.2.0 */ public function serialize($data) { $result = json_encode($data); if (false === $result) { - throw new \InvalidArgumentException('Unable to serialize value.'); + throw new \InvalidArgumentException("Unable to serialize value. Error: " . json_last_error_msg()); } return $result; } /** - * {@inheritDoc} + * @inheritDoc * @since 100.2.0 */ public function unserialize($string) { $result = json_decode($string, true); if (json_last_error() !== JSON_ERROR_NONE) { - throw new \InvalidArgumentException('Unable to unserialize value.'); + throw new \InvalidArgumentException("Unable to unserialize value. Error: " . json_last_error_msg()); } return $result; } diff --git a/lib/internal/Magento/Framework/Serialize/Serializer/JsonHexTag.php b/lib/internal/Magento/Framework/Serialize/Serializer/JsonHexTag.php new file mode 100644 index 000000000000..4a5406ff3fd9 --- /dev/null +++ b/lib/internal/Magento/Framework/Serialize/Serializer/JsonHexTag.php @@ -0,0 +1,35 @@ + are converted to \u003C and \u003E), + * unserialize JSON encoded data + * + * @api + * @since 100.2.0 + */ +class JsonHexTag extends Json implements SerializerInterface +{ + /** + * @inheritDoc + * @since 100.2.0 + */ + public function serialize($data): string + { + $result = json_encode($data, JSON_HEX_TAG); + if (false === $result) { + throw new \InvalidArgumentException('Unable to serialize value.'); + } + return $result; + } +} diff --git a/lib/internal/Magento/Framework/Serialize/Test/Unit/Serializer/JsonHexTagTest.php b/lib/internal/Magento/Framework/Serialize/Test/Unit/Serializer/JsonHexTagTest.php new file mode 100644 index 000000000000..c867dced0fc6 --- /dev/null +++ b/lib/internal/Magento/Framework/Serialize/Test/Unit/Serializer/JsonHexTagTest.php @@ -0,0 +1,118 @@ +json = $objectManager->getObject(JsonHexTag::class); + } + + /** + * @param string|int|float|bool|array|null $value + * @param string $expected + * @dataProvider serializeDataProvider + */ + public function testSerialize($value, $expected) + { + $this->assertEquals( + $expected, + $this->json->serialize($value) + ); + } + + public function serializeDataProvider() + { + $dataObject = new DataObject(['something']); + return [ + ['', '""'], + ['string', '"string"'], + [null, 'null'], + [false, 'false'], + [['a' => 'b', 'd' => 123], '{"a":"b","d":123}'], + [123, '123'], + [10.56, '10.56'], + [$dataObject, '{}'], + ['< >', '"\u003C \u003E"'], + ]; + } + + /** + * @param string $value + * @param string|int|float|bool|array|null $expected + * @dataProvider unserializeDataProvider + */ + public function testUnserialize($value, $expected) + { + $this->assertEquals( + $expected, + $this->json->unserialize($value) + ); + } + + /** + * @return array + */ + public function unserializeDataProvider(): array + { + return [ + ['""', ''], + ['"string"', 'string'], + ['null', null], + ['false', false], + ['{"a":"b","d":123}', ['a' => 'b', 'd' => 123]], + ['123', 123], + ['10.56', 10.56], + ['{}', []], + ['"\u003C \u003E"', '< >'], + ]; + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Unable to serialize value. + */ + public function testSerializeException() + { + $this->json->serialize(STDOUT); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Unable to unserialize value. + * @dataProvider unserializeExceptionDataProvider + */ + public function testUnserializeException($value) + { + $this->json->unserialize($value); + } + + /** + * @return array + */ + public function unserializeExceptionDataProvider(): array + { + return [ + [''], + [false], + [null], + ['{'] + ]; + } +} diff --git a/lib/internal/Magento/Framework/Session/SaveHandler/Redis/Config.php b/lib/internal/Magento/Framework/Session/SaveHandler/Redis/Config.php index 5a4ed1700da5..70b666f8a4e6 100644 --- a/lib/internal/Magento/Framework/Session/SaveHandler/Redis/Config.php +++ b/lib/internal/Magento/Framework/Session/SaveHandler/Redis/Config.php @@ -143,7 +143,7 @@ class Config implements \Cm\RedisSession\Handler\ConfigInterface /** * Deployment config * - * @var DeploymentConfig $deploymentConfig + * @var DeploymentConfig */ private $deploymentConfig; @@ -173,7 +173,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getLogLevel() { @@ -181,7 +181,7 @@ public function getLogLevel() } /** - * {@inheritdoc} + * @inheritdoc */ public function getHost() { @@ -189,7 +189,7 @@ public function getHost() } /** - * {@inheritdoc} + * @inheritdoc */ public function getPort() { @@ -197,7 +197,7 @@ public function getPort() } /** - * {@inheritdoc} + * @inheritdoc */ public function getDatabase() { @@ -205,7 +205,7 @@ public function getDatabase() } /** - * {@inheritdoc} + * @inheritdoc */ public function getPassword() { @@ -213,7 +213,7 @@ public function getPassword() } /** - * {@inheritdoc} + * @inheritdoc */ public function getTimeout() { @@ -221,7 +221,7 @@ public function getTimeout() } /** - * {@inheritdoc} + * @inheritdoc */ public function getPersistentIdentifier() { @@ -229,7 +229,7 @@ public function getPersistentIdentifier() } /** - * {@inheritdoc} + * @inheritdoc */ public function getCompressionThreshold() { @@ -237,7 +237,7 @@ public function getCompressionThreshold() } /** - * {@inheritdoc} + * @inheritdoc */ public function getCompressionLibrary() { @@ -245,7 +245,7 @@ public function getCompressionLibrary() } /** - * {@inheritdoc} + * @inheritdoc */ public function getMaxConcurrency() { @@ -253,7 +253,7 @@ public function getMaxConcurrency() } /** - * {@inheritdoc} + * @inheritdoc */ public function getMaxLifetime() { @@ -261,7 +261,7 @@ public function getMaxLifetime() } /** - * {@inheritdoc} + * @inheritdoc */ public function getMinLifetime() { @@ -269,7 +269,7 @@ public function getMinLifetime() } /** - * {@inheritdoc} + * @inheritdoc */ public function getDisableLocking() { @@ -277,7 +277,7 @@ public function getDisableLocking() } /** - * {@inheritdoc} + * @inheritdoc */ public function getBotLifetime() { @@ -285,7 +285,7 @@ public function getBotLifetime() } /** - * {@inheritdoc} + * @inheritdoc */ public function getBotFirstLifetime() { @@ -293,7 +293,7 @@ public function getBotFirstLifetime() } /** - * {@inheritdoc} + * @inheritdoc */ public function getFirstLifetime() { @@ -301,7 +301,7 @@ public function getFirstLifetime() } /** - * {@inheritdoc} + * @inheritdoc */ public function getBreakAfter() { @@ -309,7 +309,7 @@ public function getBreakAfter() } /** - * {@inheritdoc} + * @inheritdoc */ public function getLifetime() { @@ -320,7 +320,7 @@ public function getLifetime() } /** - * {@inheritdoc} + * @inheritdoc */ public function getSentinelServers() { @@ -328,7 +328,7 @@ public function getSentinelServers() } /** - * {@inheritdoc} + * @inheritdoc */ public function getSentinelMaster() { @@ -336,7 +336,7 @@ public function getSentinelMaster() } /** - * {@inheritdoc} + * @inheritdoc */ public function getSentinelVerifyMaster() { @@ -344,7 +344,7 @@ public function getSentinelVerifyMaster() } /** - * {@inheritdoc} + * @inheritdoc */ public function getSentinelConnectRetries() { @@ -352,7 +352,7 @@ public function getSentinelConnectRetries() } /** - * {@inheritdoc} + * @inheritdoc */ public function getFailAfter() { diff --git a/lib/internal/Magento/Framework/Session/SessionManager.php b/lib/internal/Magento/Framework/Session/SessionManager.php index 662173ad4a09..c7d201676b22 100644 --- a/lib/internal/Magento/Framework/Session/SessionManager.php +++ b/lib/internal/Magento/Framework/Session/SessionManager.php @@ -12,6 +12,7 @@ /** * Session Manager * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class SessionManager implements SessionManagerInterface { @@ -36,7 +37,7 @@ class SessionManager implements SessionManagerInterface /** * Validator * - * @var \Magento\Framework\Session\ValidatorInterface + * @var ValidatorInterface */ protected $validator; @@ -50,28 +51,28 @@ class SessionManager implements SessionManagerInterface /** * SID resolver * - * @var \Magento\Framework\Session\SidResolverInterface + * @var SidResolverInterface */ protected $sidResolver; /** * Session config * - * @var \Magento\Framework\Session\Config\ConfigInterface + * @var Config\ConfigInterface */ protected $sessionConfig; /** * Save handler * - * @var \Magento\Framework\Session\SaveHandlerInterface + * @var SaveHandlerInterface */ protected $saveHandler; /** * Storage * - * @var \Magento\Framework\Session\StorageInterface + * @var StorageInterface */ protected $storage; @@ -92,6 +93,11 @@ class SessionManager implements SessionManagerInterface */ private $appState; + /** + * @var SessionStartChecker + */ + private $sessionStartChecker; + /** * @param \Magento\Framework\App\Request\Http $request * @param SidResolverInterface $sidResolver @@ -102,7 +108,10 @@ class SessionManager implements SessionManagerInterface * @param \Magento\Framework\Stdlib\CookieManagerInterface $cookieManager * @param \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory $cookieMetadataFactory * @param \Magento\Framework\App\State $appState + * @param SessionStartChecker|null $sessionStartChecker * @throws \Magento\Framework\Exception\SessionException + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\App\Request\Http $request, @@ -113,7 +122,8 @@ public function __construct( StorageInterface $storage, \Magento\Framework\Stdlib\CookieManagerInterface $cookieManager, \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory $cookieMetadataFactory, - \Magento\Framework\App\State $appState + \Magento\Framework\App\State $appState, + SessionStartChecker $sessionStartChecker = null ) { $this->request = $request; $this->sidResolver = $sidResolver; @@ -124,11 +134,15 @@ public function __construct( $this->cookieManager = $cookieManager; $this->cookieMetadataFactory = $cookieMetadataFactory; $this->appState = $appState; + $this->sessionStartChecker = $sessionStartChecker ?: \Magento\Framework\App\ObjectManager::getInstance()->get( + SessionStartChecker::class + ); $this->start(); } /** - * This method needs to support sessions with APC enabled + * This method needs to support sessions with APC enabled. + * * @return void */ public function writeClose() @@ -163,36 +177,49 @@ public function __call($method, $args) */ public function start() { - if (!$this->isSessionExists()) { - \Magento\Framework\Profiler::start('session_start'); - - try { - $this->appState->getAreaCode(); - } catch (\Magento\Framework\Exception\LocalizedException $e) { - throw new \Magento\Framework\Exception\SessionException( - new \Magento\Framework\Phrase( - 'Area code not set: Area code must be set before starting a session.' - ), - $e - ); - } + if ($this->sessionStartChecker->check()) { + if (!$this->isSessionExists()) { + \Magento\Framework\Profiler::start('session_start'); + + try { + $this->appState->getAreaCode(); + } catch (\Magento\Framework\Exception\LocalizedException $e) { + throw new \Magento\Framework\Exception\SessionException( + new \Magento\Framework\Phrase( + 'Area code not set: Area code must be set before starting a session.' + ), + $e + ); + } - // Need to apply the config options so they can be ready by session_start - $this->initIniOptions(); - $this->registerSaveHandler(); - $sid = $this->sidResolver->getSid($this); - // potential custom logic for session id (ex. switching between hosts) - $this->setSessionId($sid); - session_start(); - $this->validator->validate($this); - $this->renewCookie($sid); + // Need to apply the config options so they can be ready by session_start + $this->initIniOptions(); + $this->registerSaveHandler(); + if (isset($_SESSION['new_session_id'])) { + // Not fully expired yet. Could be lost cookie by unstable network. + session_commit(); + session_id($_SESSION['new_session_id']); + } + $sid = $this->sidResolver->getSid($this); + // potential custom logic for session id (ex. switching between hosts) + $this->setSessionId($sid); + session_start(); + if (isset($_SESSION['destroyed']) + && $_SESSION['destroyed'] < time() - $this->sessionConfig->getCookieLifetime() + ) { + $this->destroy(['clear_storage' => true]); + } - register_shutdown_function([$this, 'writeClose']); + $this->validator->validate($this); + $this->renewCookie($sid); - $this->_addHost(); - \Magento\Framework\Profiler::stop('session_start'); + register_shutdown_function([$this, 'writeClose']); + + $this->_addHost(); + \Magento\Framework\Profiler::stop('session_start'); + } + $this->storage->init(isset($_SESSION) ? $_SESSION : []); } - $this->storage->init(isset($_SESSION) ? $_SESSION : []); return $this; } @@ -498,7 +525,33 @@ public function regenerateId() return $this; } - $this->isSessionExists() ? session_regenerate_id(true) : session_start(); + if ($this->isSessionExists()) { + // Regenerate the session + session_regenerate_id(); + $newSessionId = session_id(); + $_SESSION['new_session_id'] = $newSessionId; + + // Set destroy timestamp + $_SESSION['destroyed'] = time(); + + // Write and close current session; + session_commit(); + + // Called after destroy() + $oldSession = $_SESSION; + + // Start session with new session ID + session_id($newSessionId); + session_start(); + $_SESSION = $oldSession; + + // New session does not need them + unset($_SESSION['destroyed']); + unset($_SESSION['new_session_id']); + } else { + session_start(); + } + $this->storage->init(isset($_SESSION) ? $_SESSION : []); if ($this->sessionConfig->getUseCookies()) { diff --git a/lib/internal/Magento/Framework/Session/SessionStartChecker.php b/lib/internal/Magento/Framework/Session/SessionStartChecker.php new file mode 100644 index 000000000000..9cc32268d574 --- /dev/null +++ b/lib/internal/Magento/Framework/Session/SessionStartChecker.php @@ -0,0 +1,38 @@ +checkSapi = $checkSapi; + } + + /** + * Can session be started or not. + * + * @return bool + */ + public function check() : bool + { + return !($this->checkSapi && PHP_SAPI === 'cli'); + } +} diff --git a/lib/internal/Magento/Framework/Session/SidResolver.php b/lib/internal/Magento/Framework/Session/SidResolver.php index feb4877028da..1208aeb31eae 100644 --- a/lib/internal/Magento/Framework/Session/SidResolver.php +++ b/lib/internal/Magento/Framework/Session/SidResolver.php @@ -9,6 +9,9 @@ use Magento\Framework\App\State; +/** + * Class SidResolver + */ class SidResolver implements SidResolverInterface { /** @@ -86,8 +89,12 @@ public function __construct( } /** + * Get Sid + * * @param SessionManagerInterface $session + * * @return string|null + * @throws \Magento\Framework\Exception\LocalizedException */ public function getSid(SessionManagerInterface $session) { @@ -169,7 +176,7 @@ public function getUseSessionInUrl() if ($this->_useSessionInUrl === null) { //Using config value by default, can be overridden by using the //setter. - $this->_useSessionInUrl = (bool)$this->scopeConfig->getValue( + $this->_useSessionInUrl = $this->scopeConfig->isSetFlag( self::XML_PATH_USE_FRONTEND_SID, $this->_scopeType ); diff --git a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Config/Converter.php b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Config/Converter.php index ed3f6326cd60..149ad996a5fc 100644 --- a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Config/Converter.php +++ b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Config/Converter.php @@ -5,6 +5,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Framework\Setup\Declaration\Schema\Config; /** @@ -23,7 +25,7 @@ class Converter implements \Magento\Framework\Config\ConverterInterface * @param \DOMDocument $source * @return array */ - public function convert($source) + public function convert($source): array { $output = $this->recursiveConvert($this->getTablesNode($source)); return $output; @@ -37,7 +39,7 @@ public function convert($source) * @param \DOMDocument $element * @return \DOMNodeList */ - private function getTablesNode(\DOMDocument $element) + private function getTablesNode(\DOMDocument $element): \DOMNodeList { return $element->getElementsByTagName('table'); } @@ -48,21 +50,21 @@ private function getTablesNode(\DOMDocument $element) * @param \Traversable $source * @return array */ - private function recursiveConvert(\Traversable $source) + private function recursiveConvert(\Traversable $source): array { $output = []; foreach ($source as $element) { if ($element instanceof \DOMElement) { - $key = $element->getAttribute('name'); + $key = $this->getIdAttributeValue($element); if ($element->hasChildNodes()) { $output[$element->tagName][$key] = array_replace( $this->recursiveConvert($element->childNodes), - $this->interpretateAttributes($element) + $this->interpretAttributes($element) ); - } else if ($this->hasAttributesExceptName($element)) { - $output[$element->tagName][$key] = $this->interpretateAttributes($element); + } elseif ($this->hasAttributesExceptIdAttribute($element)) { + $output[$element->tagName][$key] = $this->interpretAttributes($element); } else { $output[$element->tagName][$key] = $key; } @@ -73,25 +75,48 @@ private function recursiveConvert(\Traversable $source) } /** - * Check whether we have any attributes except name XSI:TYPE is in another namespace. - * Note: name is mandatory attribute. + * Provide the value of the ID attribute for each element. + * + * @param \DOMElement $element + * @return string + */ + private function getIdAttributeValue(\DOMElement $element): string + { + $idAttributeValue = ''; + switch ($element->tagName) { + case ('table'): + case ('column'): + $idAttributeValue = $element->getAttribute('name'); + break; + case ('index'): + case ('constraint'): + $idAttributeValue = $element->getAttribute('referenceId'); + break; + } + + return $idAttributeValue; + } + + /** + * Check whether we have any attributes except ID attribute. * * @param \DOMElement $element * @return bool */ - private function hasAttributesExceptName(\DOMElement $element) + private function hasAttributesExceptIdAttribute(\DOMElement $element) { return $element->hasAttribute('xsi:type') || $element->attributes->length >= 2; } /** * Mix attributes that comes from XML schema with default ones. + * * So if you will not have some attribute in schema - it will be taken from default one. * * @param \DOMElement $domElement - * @return mixed + * @return array */ - private function interpretateAttributes(\DOMElement $domElement) + private function interpretAttributes(\DOMElement $domElement): array { $attributes = $this->getAttributes($domElement); $xsiType = $domElement->getAttribute('xsi:type'); @@ -109,7 +134,7 @@ private function interpretateAttributes(\DOMElement $domElement) * @param \DOMElement $element * @return array */ - private function getAttributes(\DOMElement $element) + private function getAttributes(\DOMElement $element): array { $attributes = []; $attributeNodes = $element->attributes; diff --git a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Config/SchemaLocator.php b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Config/SchemaLocator.php deleted file mode 100644 index 5319c469f045..000000000000 --- a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Config/SchemaLocator.php +++ /dev/null @@ -1,60 +0,0 @@ -_schema = $urnResolver->getRealPath($schemaUrn); - $this->_perFileSchema = $this->_schema; - } - - /** - * Get path to merged config schema. - * - * @return string|null - */ - public function getSchema() - { - return $this->_schema; - } - - /** - * Get path to pre file validation schema. - * - * @return string|null - */ - public function getPerFileSchema() - { - return $this->_perFileSchema; - } -} diff --git a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Db/MySQL/Definition/Columns/Integer.php b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Db/MySQL/Definition/Columns/Integer.php index 07ba03d03310..1d08c5bea4eb 100644 --- a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Db/MySQL/Definition/Columns/Integer.php +++ b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Db/MySQL/Definition/Columns/Integer.php @@ -76,8 +76,8 @@ public function __construct( } /** - * @param \Magento\Framework\Setup\Declaration\Schema\Dto\Columns\Integer $column * @inheritdoc + * @param \Magento\Framework\Setup\Declaration\Schema\Dto\Columns\Integer $column */ public function toDefinition(ElementInterface $column) { @@ -89,7 +89,7 @@ public function toDefinition(ElementInterface $column) $this->unsigned->toDefinition($column), $this->nullable->toDefinition($column), $column->getDefault() !== null ? - sprintf('DEFAULT %s', (string) intval($column->getDefault())) : '', + sprintf('DEFAULT %s', (string) (int)$column->getDefault()) : '', $this->identity->toDefinition($column), $this->comment->toDefinition($column) ); diff --git a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Db/MySQL/Definition/Constraints/ForeignKey.php b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Db/MySQL/Definition/Constraints/ForeignKey.php index 1a9b42355f5e..6d11542bb49d 100644 --- a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Db/MySQL/Definition/Constraints/ForeignKey.php +++ b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Db/MySQL/Definition/Constraints/ForeignKey.php @@ -43,7 +43,6 @@ public function __construct(ResourceConnection $resourceConnection) } /** - * @param Reference $foreignKey * @inheritdoc */ public function toDefinition(ElementInterface $foreignKey) @@ -95,7 +94,7 @@ public function fromDefinition(array $data) 'column' => $match[2], 'referenceTable' => $match[5], 'referenceColumn' => $match[6], - 'onDelete' => isset($match[7]) ? $match[8] : '' + 'onDelete' => isset($match[7]) ? $match[8] : 'NO ACTION' ]; } } diff --git a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Declaration/SchemaBuilder.php b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Declaration/SchemaBuilder.php index 830fcb293a8f..4c65d8a70bed 100644 --- a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Declaration/SchemaBuilder.php +++ b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Declaration/SchemaBuilder.php @@ -3,11 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Framework\Setup\Declaration\Schema\Declaration; -use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\Phrase; -use Magento\Framework\Setup\Declaration\Schema\TableNameResolver; +use Magento\Framework\Setup\Declaration\Schema\Declaration\TableElement\ElementNameResolver; use Magento\Framework\Stdlib\BooleanUtils; use Magento\Framework\Setup\Exception; use Magento\Framework\Setup\Declaration\Schema\Dto\Column; @@ -19,8 +20,7 @@ use Magento\Framework\Setup\Declaration\Schema\Sharding; /** - * This type of builder is responsible for converting ENTIRE data, that comes from XML - * into DTO`s format, with aggregation root: Schema. + * This type of builder is responsible for converting ENTIRE data, that comes from XML into DTO`s format. * * Note: SchemaBuilder can not be used for one structural element, like column or constraint * because it should have references to other DTO objects. @@ -69,9 +69,9 @@ class SchemaBuilder private $resourceConnection; /** - * @var TableNameResolver + * @var ElementNameResolver */ - private $tableNameResolver; + private $elementNameResolver; /** * SchemaBuilder constructor. @@ -81,8 +81,7 @@ class SchemaBuilder * @param Sharding $sharding * @param ValidationComposite $validationComposite * @param \Magento\Framework\App\ResourceConnection $resourceConnection - * @param TableNameResolver $tableNameResolver - * @internal param array $tablesData + * @param ElementNameResolver $elementNameResolver */ public function __construct( ElementFactory $elementFactory, @@ -90,18 +89,19 @@ public function __construct( Sharding $sharding, ValidationComposite $validationComposite, \Magento\Framework\App\ResourceConnection $resourceConnection, - TableNameResolver $tableNameResolver + ElementNameResolver $elementNameResolver ) { $this->sharding = $sharding; $this->elementFactory = $elementFactory; $this->booleanUtils = $booleanUtils; $this->validationComposite = $validationComposite; $this->resourceConnection = $resourceConnection; - $this->tableNameResolver = $tableNameResolver; + $this->elementNameResolver = $elementNameResolver; } /** * Add tables data to builder. + * * Tables data holds tables information: columns, constraints, indexes, attributes. * * @param array $tablesData @@ -140,7 +140,7 @@ private function validate(Schema $schema) * @throws Exception * @return Schema */ - public function build(Schema $schema) + public function build(Schema $schema): Schema { foreach ($this->tablesData as $tableData) { if (!$schema->getTableByName($tableData['name'])) { @@ -161,7 +161,7 @@ public function build(Schema $schema) * @param array $tableData * @return string */ - private function getStructuralElementResource(array $tableData) + private function getStructuralElementResource(array $tableData): string { return isset($tableData['resource']) && $this->sharding->canUseResource($tableData['resource']) ? $tableData['resource'] : 'default'; @@ -173,7 +173,7 @@ private function getStructuralElementResource(array $tableData) * @param array $structuralElementData * @return bool */ - private function isDisabled($structuralElementData) + private function isDisabled(array $structuralElementData): bool { return isset($structuralElementData['disabled']) && $this->booleanUtils->toBoolean($structuralElementData['disabled']); @@ -181,14 +181,15 @@ private function isDisabled($structuralElementData) /** * Instantiate column DTO objects from array. + * * If column was renamed new key will be associated to it. * - * @param array $tableData - * @param string $resource - * @param Table $table + * @param array $tableData + * @param string $resource + * @param Table $table * @return array */ - private function processColumns(array $tableData, $resource, Table $table) + private function processColumns(array $tableData, string $resource, Table $table): array { $columns = []; @@ -208,12 +209,12 @@ private function processColumns(array $tableData, $resource, Table $table) /** * Process generic data that is support by all 3 child types: columns, constraints, indexes. * - * @param array $elementData - * @param Table $table - * @param $resource + * @param array $elementData + * @param string $resource + * @param Table $table * @return array */ - private function processGenericData(array $elementData, $resource, Table $table) + private function processGenericData(array $elementData, string $resource, Table $table): array { $elementData['table'] = $table; $elementData['resource'] = $resource; @@ -223,13 +224,14 @@ private function processGenericData(array $elementData, $resource, Table $table) /** * Process tables and add them to schema. + * * If table already exists - then we need to skip it. * * @param Schema $schema - * @param array $tableData - * @return \Magento\Framework\Setup\Declaration\Schema\Dto\Table + * @param array $tableData + * @return Table */ - private function processTable(Schema $schema, array $tableData) + private function processTable(Schema $schema, array $tableData): Table { if (!$schema->getTableByName($tableData['name'])) { $resource = $this->getStructuralElementResource($tableData); @@ -250,11 +252,13 @@ private function processTable(Schema $schema, array $tableData) } /** + * Provides column by name. + * * @param string $columnName * @param Table $table * @return Column */ - private function getColumnByName(string $columnName, Table $table) + private function getColumnByName(string $columnName, Table $table): Column { $columnCandidate = $table->getColumnByName($columnName); @@ -274,7 +278,7 @@ private function getColumnByName(string $columnName, Table $table) * @param Table $table * @return array */ - private function convertColumnNamesToObjects(array $columnNames, Table $table) + private function convertColumnNamesToObjects(array $columnNames, Table $table): array { $columns = []; @@ -285,44 +289,15 @@ private function convertColumnNamesToObjects(array $columnNames, Table $table) return $columns; } - /** - * Provides the full index name based on the prefix value. - * - * @param string $name - * @param Table $table - * @param array $columns - * @param string $type - * @return string - */ - private function getFullIndexName( - string $name, - Table $table, - array $columns, - string $type = AdapterInterface::INDEX_TYPE_INDEX - ) { - if (AdapterInterface::INDEX_TYPE_PRIMARY === $type) { - return $name; - } - - $tableName = $this->tableNameResolver->getNameOfOriginTable($table->getName()); - - return $this->resourceConnection - ->getIdxName( - $tableName, - $columns, - $type - ); - } - /** * Convert and instantiate index objects. * - * @param array $tableData - * @param $resource - * @param Table $table + * @param array $tableData + * @param string $resource + * @param Table $table * @return Index[] */ - private function processIndexes(array $tableData, $resource, Table $table) + private function processIndexes(array $tableData, string $resource, Table $table): array { if (!isset($tableData['index'])) { return []; @@ -335,20 +310,10 @@ private function processIndexes(array $tableData, $resource, Table $table) continue; } - /** - * Temporary solution. - * @see MAGETWO-91365 - */ - $indexType = AdapterInterface::INDEX_TYPE_INDEX; - if (isset($indexData['indexType']) && $indexData['indexType'] === AdapterInterface::INDEX_TYPE_FULLTEXT) { - $indexType = $indexData['indexType']; - } - - $indexData['name'] = $this->getFullIndexName( - $indexData['name'], + $indexData['name'] = $this->elementNameResolver->getFullIndexName( $table, $indexData['column'], - $indexType + $indexData['indexType'] ?? null ); $indexData = $this->processGenericData($indexData, $resource, $table); $indexData['columns'] = $this->convertColumnNamesToObjects($indexData['column'], $table); @@ -363,12 +328,12 @@ private function processIndexes(array $tableData, $resource, Table $table) * Convert and instantiate constraint objects. * * @param array $tableData - * @param $resource + * @param string $resource * @param Schema $schema * @param Table $table * @return Constraint[] */ - private function processConstraints(array $tableData, $resource, Schema $schema, Table $table) + private function processConstraints(array $tableData, string $resource, Schema $schema, Table $table): array { if (!isset($tableData['constraint'])) { return []; @@ -385,6 +350,13 @@ private function processConstraints(array $tableData, $resource, Schema $schema, if ($constraintData['type'] === 'foreign') { $constraintData['column'] = $this->getColumnByName($constraintData['column'], $table); $referenceTableData = $this->tablesData[$constraintData['referenceTable']]; + + if ($this->isDisabled($referenceTableData)) { + throw new \LogicException( + sprintf('The reference table named "%s" is disabled', $referenceTableData['name']) + ); + } + //If we are referenced to the same table we need to specify it //Get table name from resource connection regarding prefix settings $refTableName = $this->resourceConnection->getTableName($referenceTableData['name']); @@ -408,21 +380,16 @@ private function processConstraints(array $tableData, $resource, Schema $schema, $constraintData['referenceColumn'], $constraintData['referenceTable'] ); - /** - * Calculation of the full name of Foreign Key based on the prefix value. - */ - $constraintData['name'] = $this->resourceConnection - ->getFkName( - $this->tableNameResolver->getNameOfOriginTable($table->getName()), - $constraintData['column']->getName(), - $constraintData['referenceTable']->getName(), - $constraintData['referenceColumn']->getName() - ); + $constraintData['name'] = $this->elementNameResolver->getFullFKName( + $table, + $constraintData['column'], + $constraintData['referenceTable'], + $constraintData['referenceColumn'] + ); $constraint = $this->elementFactory->create($constraintData['type'], $constraintData); $constraints[$constraint->getName()] = $constraint; } else { - $constraintData['name'] = $this->getFullIndexName( - $constraintData['name'], + $constraintData['name'] = $this->elementNameResolver->getFullIndexName( $table, $constraintData['column'], $constraintData['type'] diff --git a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Declaration/TableElement/ElementNameResolver.php b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Declaration/TableElement/ElementNameResolver.php new file mode 100644 index 000000000000..fe7d1a822dae --- /dev/null +++ b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Declaration/TableElement/ElementNameResolver.php @@ -0,0 +1,188 @@ +tableNameResolver = $tableNameResolver; + $this->resourceConnection = $resourceConnection; + } + + /** + * Provide the full index name based on the prefix value. + * + * @param Table $table + * @param string[] $columns + * @param string $type + * @return string + */ + public function getFullIndexName( + Table $table, + array $columns, + ?string $type = AdapterInterface::INDEX_TYPE_INDEX + ): string { + if (AdapterInterface::INDEX_TYPE_PRIMARY === $type) { + return strtoupper(AdapterInterface::INDEX_TYPE_PRIMARY); + } + + /** + * Temporary solution. + * @see MAGETWO-91365 + */ + $isIndexTypeOutOfList = false === array_search( + $type, + [AdapterInterface::INDEX_TYPE_FULLTEXT, AdapterInterface::INDEX_TYPE_UNIQUE] + ); + if ($type && $isIndexTypeOutOfList) { + $type = AdapterInterface::INDEX_TYPE_INDEX; + } + + $tableName = $this->tableNameResolver->getNameOfOriginTable($table->getName()); + + return $this->resourceConnection + ->getIdxName( + $tableName, + $columns, + $type + ); + } + + /** + * Provide the index name without prefix value. + * + * @param string $name + * @param Table $table + * @param string[] $columns + * @param string $type + * @return string + */ + public function getIndexNameWithoutPrefix( + string $name, + Table $table, + array $columns, + ?string $type = AdapterInterface::INDEX_TYPE_INDEX + ): string { + if (AdapterInterface::INDEX_TYPE_PRIMARY === $type) { + return strtoupper(AdapterInterface::INDEX_TYPE_PRIMARY); + } + + $nameWithoutPrefix = $name; + + if ($this->resourceConnection->getTablePrefix()) { + /** + * Temporary solution. + * @see MAGETWO-91365 + */ + $isIndexTypeOutOfList = false === array_search( + $type, + [AdapterInterface::INDEX_TYPE_FULLTEXT, AdapterInterface::INDEX_TYPE_UNIQUE] + ); + if ($type && $isIndexTypeOutOfList) { + $type = AdapterInterface::INDEX_TYPE_INDEX; + } + + $nameWithoutPrefix = $this->resourceConnection + ->getConnection($table->getResource()) + ->getIndexName( + $this->tableNameResolver->getNameOfOriginTable( + $table->getNameWithoutPrefix() + ), + $columns, + $type + ); + } + + return $nameWithoutPrefix; + } + + /** + * Provide the full foreign key name based on the prefix value. + * + * @param Table $table + * @param Column $column + * @param Table $referenceTable + * @param Column $referenceColumn + * @return string + */ + public function getFullFKName( + Table $table, + Column $column, + Table $referenceTable, + Column $referenceColumn + ): string { + $fkName = $this->resourceConnection + ->getFkName( + $this->tableNameResolver->getNameOfOriginTable($table->getName()), + $column->getName(), + $referenceTable->getName(), + $referenceColumn->getName() + ); + + return $fkName; + } + + /** + * Provide the foreign key name without prefix value. + * + * @param string $name + * @param Table $table + * @param Column $column + * @param Table $referenceTable + * @param Column $referenceColumn + * @return string + */ + public function getFKNameWithoutPrefix( + string $name, + Table $table, + Column $column, + Table $referenceTable, + Column $referenceColumn + ): string { + $nameWithoutPrefix = $name; + + if ($this->resourceConnection->getTablePrefix()) { + $nameWithoutPrefix = $this->resourceConnection + ->getConnection($table->getResource()) + ->getForeignKeyName( + $this->tableNameResolver->getNameOfOriginTable( + $table->getNameWithoutPrefix() + ), + $column->getName(), + $referenceTable->getNameWithoutPrefix(), + $referenceColumn->getName() + ); + } + + return $nameWithoutPrefix; + } +} diff --git a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Diff/DiffInterface.php b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Diff/DiffInterface.php index 4b6e3555256f..139e9b902684 100644 --- a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Diff/DiffInterface.php +++ b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Diff/DiffInterface.php @@ -7,9 +7,6 @@ namespace Magento\Framework\Setup\Declaration\Schema\Diff; use Magento\Framework\Setup\Declaration\Schema\Dto\ElementInterface; -use Magento\Framework\Setup\Declaration\Schema\Dto\Schema; -use Magento\Framework\Setup\Declaration\Schema\ElementHistory; -use Magento\Framework\Setup\Declaration\Schema\Request; /** * DiffInterface is type of classes, that holds all information @@ -39,9 +36,9 @@ public function getAll(); /** * Register operation. * - * @param ElementInterface|object $dtoObject - * @param string $operation - * @param ElementInterface $oldDtoObject + * @param ElementInterface|object $dtoObject + * @param string $operation + * @param ElementInterface $oldDtoObject * @return void */ public function register( diff --git a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Dto/Factories/Foreign.php b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Dto/Factories/Foreign.php index 0e1ad0768c4d..040549a5611c 100644 --- a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Dto/Factories/Foreign.php +++ b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Dto/Factories/Foreign.php @@ -7,6 +7,7 @@ use Magento\Framework\App\ResourceConnection; use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Setup\Declaration\Schema\Declaration\TableElement\ElementNameResolver; use Magento\Framework\Setup\Declaration\Schema\TableNameResolver; /** @@ -39,28 +40,36 @@ class Foreign implements FactoryInterface */ private $tableNameResolver; + /** + * @var ElementNameResolver + */ + private $elementNameResolver; + /** * Constructor. * * @param ObjectManagerInterface $objectManager * @param ResourceConnection $resourceConnection * @param TableNameResolver $tableNameResolver + * @param ElementNameResolver $elementNameResolver * @param string $className */ public function __construct( ObjectManagerInterface $objectManager, ResourceConnection $resourceConnection, TableNameResolver $tableNameResolver, + ElementNameResolver $elementNameResolver, $className = \Magento\Framework\Setup\Declaration\Schema\Dto\Constraints\Reference::class ) { $this->objectManager = $objectManager; $this->resourceConnection = $resourceConnection; $this->className = $className; $this->tableNameResolver = $tableNameResolver; + $this->elementNameResolver = $elementNameResolver; } /** - * {@inheritdoc} + * @inheritdoc */ public function create(array $data) { @@ -68,22 +77,13 @@ public function create(array $data) $data['onDelete'] = self::DEFAULT_ON_DELETE; } - $nameWithoutPrefix = $data['name']; - - if ($this->resourceConnection->getTablePrefix()) { - $nameWithoutPrefix = $this->resourceConnection - ->getConnection($data['table']->getResource()) - ->getForeignKeyName( - $this->tableNameResolver->getNameOfOriginTable( - $data['table']->getNameWithoutPrefix() - ), - $data['column']->getName(), - $data['referenceTable']->getNameWithoutPrefix(), - $data['referenceColumn']->getName() - ); - } - - $data['nameWithoutPrefix'] = $nameWithoutPrefix; + $data['nameWithoutPrefix'] = $this->elementNameResolver->getFKNameWithoutPrefix( + $data['name'], + $data['table'], + $data['column'], + $data['referenceTable'], + $data['referenceColumn'] + ); return $this->objectManager->create($this->className, $data); } 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 dd2acd760886..211d3885297b 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 @@ -8,6 +8,7 @@ use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Setup\Declaration\Schema\Declaration\TableElement\ElementNameResolver; use Magento\Framework\Setup\Declaration\Schema\TableNameResolver; /** @@ -18,7 +19,7 @@ class Index implements FactoryInterface /** * Default index type. */ - const DEFAULT_INDEX_TYPE = "BTREE"; + const DEFAULT_INDEX_TYPE = "btree"; /** * @var ObjectManagerInterface @@ -40,28 +41,36 @@ class Index implements FactoryInterface */ private $tableNameResolver; + /** + * @var ElementNameResolver + */ + private $elementNameResolver; + /** * Constructor. * * @param ObjectManagerInterface $objectManager * @param ResourceConnection $resourceConnection * @param TableNameResolver $tableNameResolver + * @param ElementNameResolver $elementNameResolver * @param string $className */ public function __construct( ObjectManagerInterface $objectManager, ResourceConnection $resourceConnection, TableNameResolver $tableNameResolver, + ElementNameResolver $elementNameResolver, $className = \Magento\Framework\Setup\Declaration\Schema\Dto\Index::class ) { $this->objectManager = $objectManager; $this->resourceConnection = $resourceConnection; $this->className = $className; $this->tableNameResolver = $tableNameResolver; + $this->elementNameResolver = $elementNameResolver; } /** - * {@inheritdoc} + * @inheritdoc */ public function create(array $data) { @@ -69,29 +78,12 @@ public function create(array $data) $data['indexType'] = self::DEFAULT_INDEX_TYPE; } - $nameWithoutPrefix = $data['name']; - - if ($this->resourceConnection->getTablePrefix()) { - /** - * Temporary solution. - * @see MAGETWO-91365 - */ - $indexType = AdapterInterface::INDEX_TYPE_INDEX; - if ($data['indexType'] === AdapterInterface::INDEX_TYPE_FULLTEXT) { - $indexType = $data['indexType']; - } - $nameWithoutPrefix = $this->resourceConnection - ->getConnection($data['table']->getResource()) - ->getIndexName( - $this->tableNameResolver->getNameOfOriginTable( - $data['table']->getNameWithoutPrefix() - ), - $data['column'], - $indexType - ); - } - - $data['nameWithoutPrefix'] = $nameWithoutPrefix; + $data['nameWithoutPrefix'] = $this->elementNameResolver->getIndexNameWithoutPrefix( + $data['name'], + $data['table'], + $data['column'], + $data['indexType'] + ); return $this->objectManager->create($this->className, $data); } diff --git a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Dto/Factories/Real.php b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Dto/Factories/Real.php index cdb740ea0a92..e0728b9a34fe 100644 --- a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Dto/Factories/Real.php +++ b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Dto/Factories/Real.php @@ -39,7 +39,7 @@ class Real implements FactoryInterface * Constructor. * * @param ObjectManagerInterface $objectManager - * @param string $className + * @param string $className */ public function __construct( ObjectManagerInterface $objectManager, @@ -50,7 +50,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function create(array $data) { @@ -63,7 +63,7 @@ public function create(array $data) } if (isset($data['default'])) { - $data['default'] = floatval($data['default']); + $data['default'] = (float)$data['default']; } return $this->objectManager->create($this->className, $data); diff --git a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Dto/Factories/Unique.php b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Dto/Factories/Unique.php index 57e5270b0c82..141e4a708320 100644 --- a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Dto/Factories/Unique.php +++ b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Dto/Factories/Unique.php @@ -7,6 +7,7 @@ use Magento\Framework\App\ResourceConnection; use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Setup\Declaration\Schema\Declaration\TableElement\ElementNameResolver; use Magento\Framework\Setup\Declaration\Schema\TableNameResolver; /** @@ -34,46 +35,45 @@ class Unique implements FactoryInterface */ private $tableNameResolver; + /** + * @var ElementNameResolver + */ + private $elementNameResolver; + /** * Constructor. * * @param ObjectManagerInterface $objectManager * @param ResourceConnection $resourceConnection * @param TableNameResolver $tableNameResolver + * @param ElementNameResolver $elementNameResolver * @param string $className */ public function __construct( ObjectManagerInterface $objectManager, ResourceConnection $resourceConnection, TableNameResolver $tableNameResolver, + ElementNameResolver $elementNameResolver, $className = \Magento\Framework\Setup\Declaration\Schema\Dto\Constraints\Internal::class ) { $this->objectManager = $objectManager; $this->resourceConnection = $resourceConnection; $this->className = $className; $this->tableNameResolver = $tableNameResolver; + $this->elementNameResolver = $elementNameResolver; } /** - * {@inheritdoc} + * @inheritdoc */ public function create(array $data) { - $nameWithoutPrefix = $data['name']; - - if ($this->resourceConnection->getTablePrefix()) { - $nameWithoutPrefix = $this->resourceConnection - ->getConnection($data['table']->getResource()) - ->getIndexName( - $this->tableNameResolver->getNameOfOriginTable( - $data['table']->getNameWithoutPrefix() - ), - $data['column'], - $data['type'] - ); - } - - $data['nameWithoutPrefix'] = $nameWithoutPrefix; + $data['nameWithoutPrefix'] = $this->elementNameResolver->getIndexNameWithoutPrefix( + $data['name'], + $data['table'], + $data['column'], + $data['type'] + ); return $this->objectManager->create($this->className, $data); } diff --git a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Dto/Schema.php b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Dto/Schema.php index 3e68b985283c..fbbe188d127a 100644 --- a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Dto/Schema.php +++ b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Dto/Schema.php @@ -63,14 +63,15 @@ public function addTable(Table $table) /** * Retrieve table by it name. + * * Return false if table is not present in schema. * - * @param $name + * @param string $name * @return bool|Table */ public function getTableByName($name) { $name = $this->resourceConnection->getTableName($name); - return isset($this->tables[$name]) ? $this->tables[$name] : false; + return $this->tables[$name] ?? false; } } diff --git a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Dto/Table.php b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Dto/Table.php index 4f020b1a0320..b864a3d927b0 100644 --- a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Dto/Table.php +++ b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Dto/Table.php @@ -87,11 +87,11 @@ class Table extends GenericElement implements * @param string $engine * @param string $charset * @param string $collation + * @param string $onCreate * @param string|null $comment * @param array $columns * @param array $indexes * @param array $constraints - * @param string $onCreate * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -123,6 +123,7 @@ public function __construct( /** * Return different table constraints. + * * It can be constraint like unique key or reference to another table, etc * * @return Constraint[] @@ -133,12 +134,14 @@ public function getConstraints() } /** + * Get constraint by name. + * * @param string $name * @return Constraint | bool */ public function getConstraintByName($name) { - return isset($this->constraints[$name]) ? $this->constraints[$name] : false; + return $this->constraints[$name] ?? false; } /** @@ -160,6 +163,8 @@ public function getReferenceConstraints() } /** + * Returns primary constraint + * * As primary constraint always have one name * and can be only one for table * it name is allocated into it constraint @@ -168,9 +173,7 @@ public function getReferenceConstraints() */ public function getPrimaryConstraint() { - return isset($this->constraints[Internal::PRIMARY_NAME]) ? - $this->constraints[Internal::PRIMARY_NAME] : - false; + return $this->constraints[Internal::PRIMARY_NAME] ?? false; } /** @@ -191,16 +194,19 @@ public function getInternalConstraints() : array } /** + * Get index by name + * * @param string $name * @return Index | bool */ public function getIndexByName($name) { - return isset($this->indexes[$name]) ? $this->indexes[$name] : false; + return $this->indexes[$name] ?? false; } /** * Return all columns. + * * Note, table always must have columns * * @return Column[] @@ -231,6 +237,8 @@ public function getResource() } /** + * Add constraints + * * This is workaround, as any DTO object couldnt be changed after instantiation. * However there is case, when we have 2 tables with constraints in different tables, * that depends to each other table. So we need to setup DTO first and only then pass @@ -280,6 +288,7 @@ public function getColumnByName($nameOrId) /** * Retrieve elements by specific type + * * Allowed types: columns, constraints, indexes... * * @param string $type @@ -295,6 +304,8 @@ public function getElementsByType($type) } /** + * Add indexes + * * This is workaround, as any DTO object couldnt be changed after instantiation. * However there is case, when we depends on column definition we need modify our indexes * @@ -314,6 +325,8 @@ public function getElementType() } /** + * Get engine name + * * @return string */ public function getEngine(): string @@ -356,6 +369,8 @@ public function getCollation() : string } /** + * Get name without prefix + * * @return string */ public function getNameWithoutPrefix(): string @@ -364,6 +379,8 @@ public function getNameWithoutPrefix(): string } /** + * Get comment + * * @return null|string */ public function getComment() diff --git a/lib/internal/Magento/Framework/Setup/Declaration/Schema/FileSystem/XmlReader.php b/lib/internal/Magento/Framework/Setup/Declaration/Schema/FileSystem/XmlReader.php deleted file mode 100644 index bd835c35fd89..000000000000 --- a/lib/internal/Magento/Framework/Setup/Declaration/Schema/FileSystem/XmlReader.php +++ /dev/null @@ -1,63 +0,0 @@ - 'name', - '/schema/table/column' => 'name', - '/schema/table/constraint' => 'name', - '/schema/table/index' => 'name', - '/schema/table/index/column' => 'name', - '/schema/table/constraint/column' => 'name', - ]; - - /** - * XmlReader constructor. - * - * @param FileResolverByModule $fileResolver - * @param \Magento\Framework\Setup\Declaration\Schema\Config\Converter $converter - * @param \Magento\Framework\Setup\Declaration\Schema\Config\SchemaLocator $schemaLocator - * @param \Magento\Framework\Config\ValidationStateInterface $validationState - * @param string $fileName - * @param string $domDocumentClass - * @param string $defaultScope - */ - public function __construct( - FileResolverByModule $fileResolver, - \Magento\Framework\Setup\Declaration\Schema\Config\Converter $converter, - \Magento\Framework\Setup\Declaration\Schema\Config\SchemaLocator $schemaLocator, - \Magento\Framework\Config\ValidationStateInterface $validationState, - $fileName = 'db_schema.xml', - $domDocumentClass = \Magento\Framework\Config\Dom::class, - $defaultScope = 'global' - ) { - parent::__construct( - $fileResolver, - $converter, - $schemaLocator, - $validationState, - $fileName, - $this->idAttributes, - $domDocumentClass, - $defaultScope - ); - } -} diff --git a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Operations/AddColumn.php b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Operations/AddColumn.php index 9a42090ad31d..71a1e2f92dfd 100644 --- a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Operations/AddColumn.php +++ b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Operations/AddColumn.php @@ -110,7 +110,7 @@ private function getTemporaryIndexHistory(Column $column) Index::TYPE, [ 'name' => self::TEMPORARY_KEY, - 'column' => $column->getName(), + 'column' => [$column->getName()], 'columns' => [$column], 'table' => $column->getTable() ] @@ -119,7 +119,7 @@ private function getTemporaryIndexHistory(Column $column) } /** - * {@inheritdoc} + * @inheritdoc */ public function getOperationName() { @@ -127,7 +127,7 @@ public function getOperationName() } /** - * @return bool + * @inheritdoc */ public function isOperationDestructive() { @@ -187,7 +187,7 @@ private function setupTriggersIfExists(Statement $statement, ElementHistory $ele } /** - * {@inheritdoc} + * @inheritdoc */ public function doOperation(ElementHistory $elementHistory) { diff --git a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Operations/DropReference.php b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Operations/DropReference.php index 4a5f651a1081..95fc1c42210b 100644 --- a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Operations/DropReference.php +++ b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Operations/DropReference.php @@ -35,18 +35,15 @@ public function __construct(DropElement $dropElement) } /** - * {@inheritdoc} - * We can drop references and this will not cause any issues. - * - * @return bool + * @inheritdoc */ public function isOperationDestructive() { - return false; + return true; } /** - * {@inheritdoc} + * @inheritdoc */ public function getOperationName() { @@ -54,7 +51,7 @@ public function getOperationName() } /** - * {@inheritdoc} + * @inheritdoc */ public function doOperation(ElementHistory $elementHistory) { diff --git a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Request.php b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Request.php deleted file mode 100644 index 2ffe29cc7696..000000000000 --- a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Request.php +++ /dev/null @@ -1,45 +0,0 @@ -dumpEnable = (bool) $request[static::DUMP_ENABLE_OPTIONS]; - } - } - - /** - * Check whether dump is enabled. - * - * @return boolean - */ - public function isDumpEnabled() - { - return $this->dumpEnable; - } -} diff --git a/lib/internal/Magento/Framework/Setup/Declaration/Schema/RequestFactory.php b/lib/internal/Magento/Framework/Setup/Declaration/Schema/RequestFactory.php deleted file mode 100644 index 8f76815d4dc1..000000000000 --- a/lib/internal/Magento/Framework/Setup/Declaration/Schema/RequestFactory.php +++ /dev/null @@ -1,49 +0,0 @@ -zendDi = $zendDi; - } - - /** - * Create request object with requestOptions params. - * - * @param array $requestOptions - * @return Request - */ - public function create(array $requestOptions = []) - { - return $this->zendDi->newInstance( - self::$instanceName, - ['request' => $requestOptions] - ); - } -} diff --git a/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/constraints/constraint.xsd b/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/constraints/constraint.xsd index 8c264bd95a07..c379452d65d8 100644 --- a/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/constraints/constraint.xsd +++ b/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/constraints/constraint.xsd @@ -9,7 +9,7 @@ - - + + diff --git a/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/index.xsd b/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/index.xsd index ead4fc5604fc..cd08e3f43ad4 100644 --- a/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/index.xsd +++ b/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/index.xsd @@ -19,7 +19,7 @@ - + diff --git a/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/name.xsd b/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/name.xsd index 82db095d6682..dbebdefbda0c 100644 --- a/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/name.xsd +++ b/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/name.xsd @@ -16,4 +16,18 @@ + + + + + + The attribute can contain only [A-Z0-9_]. + + + + + + + + diff --git a/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/schema.xsd b/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/schema.xsd index 66cd19d05128..e3c54413f810 100644 --- a/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/schema.xsd +++ b/lib/internal/Magento/Framework/Setup/Declaration/Schema/etc/schema.xsd @@ -39,9 +39,38 @@ - + + + + Column name be unique for each table + + + + + + + Reference ID should be unique for indexes + + + + + + + Reference ID should be unique for constraints + + + + + + + + Table name should be unique for each module + + + + @@ -55,7 +84,8 @@ - + + diff --git a/lib/internal/Magento/Framework/Setup/OldDbValidator.php b/lib/internal/Magento/Framework/Setup/OldDbValidator.php index 005f7bdd713b..018b010e8fe4 100644 --- a/lib/internal/Magento/Framework/Setup/OldDbValidator.php +++ b/lib/internal/Magento/Framework/Setup/OldDbValidator.php @@ -11,7 +11,9 @@ use Magento\Framework\Module\DbVersionInfo; /** - * Old Validator for database is used in order to support backward compatability of modules that are installed + * Old Validator for database + * + * Used in order to support backward compatibility of modules that are installed * in old way (with Install/Upgrade Schema/Data scripts) */ class OldDbValidator implements UpToDateValidatorInterface @@ -47,7 +49,7 @@ public function getNotUpToDateMessage(): string $requiredVersion = $versionParser->parseConstraints('>' . $error[DbVersionInfo::KEY_REQUIRED]); if ($requiredVersion->matches($currentVersion)) { $codebaseUpdateNeeded = true; - }; + } $messages[] = sprintf( "%20s %10s: %11s -> %-11s", @@ -63,6 +65,8 @@ public function getNotUpToDateMessage(): string } /** + * Is up to date + * * @return bool */ public function isUpToDate(): bool diff --git a/lib/internal/Magento/Framework/Setup/Patch/DependentPatchInterface.php b/lib/internal/Magento/Framework/Setup/Patch/DependentPatchInterface.php index abda94a0e6f8..7573441fa946 100644 --- a/lib/internal/Magento/Framework/Setup/Patch/DependentPatchInterface.php +++ b/lib/internal/Magento/Framework/Setup/Patch/DependentPatchInterface.php @@ -6,7 +6,7 @@ namespace Magento\Framework\Setup\Patch; /** - * Each patch can have dependecies, that should be applied before such patch + * Each patch can have dependencies, that should be applied before such patch * * / Patch2 --- Patch3 * / @@ -20,7 +20,7 @@ interface DependentPatchInterface /** * Get array of patches that have to be executed prior to this. * - * example of implementation: + * Example of implementation: * * [ * \Vendor_Name\Module_Name\Setup\Patch\Patch1::class, diff --git a/lib/internal/Magento/Framework/Setup/SchemaListener.php b/lib/internal/Magento/Framework/Setup/SchemaListener.php index c6407a2569a2..aabd7dedc911 100644 --- a/lib/internal/Magento/Framework/Setup/SchemaListener.php +++ b/lib/internal/Magento/Framework/Setup/SchemaListener.php @@ -12,6 +12,9 @@ /** * Listen for all changes and record them in order to reuse later. + * + * @SuppressWarnings(PHPMD.TooManyPublicMethods) + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ class SchemaListener { @@ -61,7 +64,8 @@ class SchemaListener 'SCALE' => 'scale', 'UNSIGNED' => 'unsigned', 'IDENTITY' => 'identity', - 'PRIMARY' => 'primary' + 'PRIMARY' => 'primary', + 'COMMENT' => 'comment', ]; /** @@ -71,7 +75,6 @@ class SchemaListener 'COLUMN_POSITION', 'COLUMN_TYPE', 'PRIMARY_POSITION', - 'COMMENT' ]; /** @@ -90,8 +93,6 @@ class SchemaListener private $handlers; /** - * Constructor. - * * @param array $definitionMappers * @param array $handlers */ @@ -132,7 +133,9 @@ private function castColumnDefinition($definition, $columnName) $definition = $this->doColumnMapping($definition); $definition['name'] = strtolower($columnName); $definitionType = $definition['type'] === 'int' ? 'integer' : $definition['type']; + $columnComment = $definition['comment'] ?? null; $definition = $this->definitionMappers[$definitionType]->convertToDefinition($definition); + $definition['comment'] = $columnComment; if (isset($definition['default']) && $definition['default'] === false) { $definition['default'] = null; //uniform default values } @@ -214,7 +217,7 @@ private function doColumnMapping(array $definition) * @param string $columnName * @param array $definition * @param string $primaryKeyName - * @param null $onCreate + * @param string|null $onCreate */ public function addColumn($tableName, $columnName, $definition, $primaryKeyName = 'PRIMARY', $onCreate = null) { @@ -448,6 +451,7 @@ private function prepareColumns($tableName, array $tableColumns) * @param array $foreignKeys * @param array $indexes * @param string $tableName + * @param string $engine */ private function prepareConstraintsAndIndexes(array $foreignKeys, array $indexes, $tableName, $engine) { @@ -478,11 +482,16 @@ private function prepareConstraintsAndIndexes(array $foreignKeys, array $indexes * Create table. * * @param Table $table + * @throws \Zend_Db_Exception */ public function createTable(Table $table) { $engine = strtolower($table->getOption('type')); - $this->tables[$this->getModuleName()][strtolower($table->getName())]['engine'] = $engine; + $this->tables[$this->getModuleName()][strtolower($table->getName())] = + [ + 'engine' => $engine, + 'comment' => $table->getComment(), + ]; $this->prepareColumns($table->getName(), $table->getColumns()); $this->prepareConstraintsAndIndexes($table->getForeignKeys(), $table->getIndexes(), $table->getName(), $engine); } @@ -510,7 +519,7 @@ public function toogleIgnore($flag) /** * Drop table. * - * @param $tableName + * @param string $tableName */ public function dropTable($tableName) { diff --git a/lib/internal/Magento/Framework/Setup/SchemaListenerDefinition/TextBlobDefinition.php b/lib/internal/Magento/Framework/Setup/SchemaListenerDefinition/TextBlobDefinition.php index 10e3b70e61be..9e8d63b26cfc 100644 --- a/lib/internal/Magento/Framework/Setup/SchemaListenerDefinition/TextBlobDefinition.php +++ b/lib/internal/Magento/Framework/Setup/SchemaListenerDefinition/TextBlobDefinition.php @@ -15,6 +15,7 @@ class TextBlobDefinition implements DefinitionConverterInterface { /** * Parse text size. + * * Returns max allowed size if value great it. * * @param string|int $size @@ -27,13 +28,13 @@ private function parseTextSize($size) switch ($last) { case 'k': - $size = intval($size) * 1024; + $size = (int)$size * 1024; break; case 'm': - $size = intval($size) * 1024 * 1024; + $size = (int)$size * 1024 * 1024; break; case 'g': - $size = intval($size) * 1024 * 1024 * 1024; + $size = (int)$size * 1024 * 1024 * 1024; break; } @@ -44,7 +45,7 @@ private function parseTextSize($size) return Table::MAX_TEXT_SIZE; } - return intval($size); + return (int)$size; } /** diff --git a/lib/internal/Magento/Framework/Setup/SchemaPersistor.php b/lib/internal/Magento/Framework/Setup/SchemaPersistor.php index f3af56b8ac2c..51f61f1dde13 100644 --- a/lib/internal/Magento/Framework/Setup/SchemaPersistor.php +++ b/lib/internal/Magento/Framework/Setup/SchemaPersistor.php @@ -6,7 +6,7 @@ namespace Magento\Framework\Setup; use Magento\Framework\Component\ComponentRegistrar; -use Magento\Framework\Shell; +use Magento\Framework\Setup\Declaration\Schema\Sharding; /** * Persist listened schema to db_schema.xml file. @@ -24,8 +24,6 @@ class SchemaPersistor private $xmlPersistor; /** - * Constructor. - * * @param ComponentRegistrar $componentRegistrar * @param XmlPersistor $xmlPersistor */ @@ -64,32 +62,88 @@ public function persist(SchemaListener $schemaListener) continue; } $schemaPatch = sprintf('%s/etc/db_schema.xml', $path); - if (file_exists($schemaPatch)) { - $dom = new \SimpleXMLElement(file_get_contents($schemaPatch)); - } else { - $dom = $this->initEmptyDom(); - } + $dom = $this->processTables($schemaPatch, $tablesData); + $this->persistModule($dom, $schemaPatch); + } + } + + /** + * Convert tables data into XML document. + * + * @param string $schemaPatch + * @param array $tablesData + * @return \SimpleXMLElement + */ + private function processTables(string $schemaPatch, array $tablesData): \SimpleXMLElement + { + if (file_exists($schemaPatch)) { + $dom = new \SimpleXMLElement(file_get_contents($schemaPatch)); + } else { + $dom = $this->initEmptyDom(); + } + $defaultAttributesValues = [ + 'resource' => Sharding::DEFAULT_CONNECTION, + ]; - foreach ($tablesData as $tableName => $tableData) { - $tableData = $this->handleDefinition($tableData); + foreach ($tablesData as $tableName => $tableData) { + $tableData = $this->handleDefinition($tableData); + $table = $dom->xpath("//table[@name='" . $tableName . "']"); + if (!$table) { $table = $dom->addChild('table'); $table->addAttribute('name', $tableName); - $table->addAttribute('resource', $tableData['resource']); - if (isset($tableData['engine']) && $tableData['engine'] !== null) { - $table->addAttribute('engine', $tableData['engine']); - } + } else { + $table = reset($table); + } - $this->processColumns($tableData, $table); - $this->processConstraints($tableData, $table); - $this->processIndexes($tableData, $table); + $attributeNames = ['disabled', 'resource', 'engine', 'comment']; + foreach ($attributeNames as $attributeName) { + $this->updateElementAttribute( + $table, + $attributeName, + $tableData, + $defaultAttributesValues[$attributeName] ?? null + ); } - $this->persistModule($dom, $schemaPatch); + $this->processColumns($tableData, $table); + $this->processConstraints($tableData, $table); + $this->processIndexes($tableData, $table); + } + + return $dom; + } + + /** + * Update element attribute value or create new attribute. + * + * @param \SimpleXMLElement $element + * @param string $attributeName + * @param array $elementData + * @param string|null $defaultValue + */ + private function updateElementAttribute( + \SimpleXMLElement $element, + string $attributeName, + array $elementData, + ?string $defaultValue = null + ) { + $attributeValue = $elementData[$attributeName] ?? $defaultValue; + if ($attributeValue !== null) { + if (is_bool($attributeValue)) { + $attributeValue = $this->castBooleanToString($attributeValue); + } + + if ($element->attributes()[$attributeName]) { + $element->attributes()->$attributeName = $attributeValue; + } else { + $element->addAttribute($attributeName, $attributeValue); + } } } /** * If disabled attribute is set to false it remove it at all. + * * Also handle other generic attributes. * * @param array $definition @@ -124,24 +178,30 @@ private function castBooleanToString($boolean) */ private function processColumns(array $tableData, \SimpleXMLElement $table) { - if (isset($tableData['columns'])) { - foreach ($tableData['columns'] as $columnData) { - $columnData = $this->handleDefinition($columnData); - $domColumn = $table->addChild('column'); - $domColumn->addAttribute('xsi:type', $columnData['xsi:type'], 'xsi'); - unset($columnData['xsi:type']); - - foreach ($columnData as $attributeKey => $attributeValue) { - if ($attributeValue === null) { - continue; - } - - if (is_bool($attributeValue)) { - $attributeValue = $this->castBooleanToString($attributeValue); - } + if (!isset($tableData['columns'])) { + return $table; + } - $domColumn->addAttribute($attributeKey, $attributeValue); + foreach ($tableData['columns'] as $columnName => $columnData) { + $columnData = $this->handleDefinition($columnData); + $domColumn = $table->xpath("column[@name='" . $columnName . "']"); + if (!$domColumn) { + $domColumn = $table->addChild('column'); + if (!empty($columnData['xsi:type'])) { + $domColumn->addAttribute('xsi:type', $columnData['xsi:type'], 'xsi'); } + $domColumn->addAttribute('name', $columnName); + } else { + $domColumn = reset($domColumn); + } + + $attributeNames = array_diff(array_keys($columnData), ['name', 'xsi:type']); + foreach ($attributeNames as $attributeName) { + $this->updateElementAttribute( + $domColumn, + $attributeName, + $columnData + ); } } @@ -160,14 +220,29 @@ private function processIndexes(array $tableData, \SimpleXMLElement $table) if (isset($tableData['indexes'])) { foreach ($tableData['indexes'] as $indexName => $indexData) { $indexData = $this->handleDefinition($indexData); - $domIndex = $table->addChild('index'); - $domIndex->addAttribute('name', $indexName); - if (isset($indexData['disabled']) && $indexData['disabled']) { - $domIndex->addAttribute('disabled', true); - } else { - $domIndex->addAttribute('indexType', $indexData['indexType']); + $domIndex = $table->xpath("index[@referenceId='" . $indexName . "']"); + if (!$domIndex) { + $domIndex = $this->getUniqueIndexByName($table, $indexName); + } + + if (!$domIndex) { + $domIndex = $table->addChild('index'); + $domIndex->addAttribute('referenceId', $indexName); + } elseif (is_array($domIndex)) { + $domIndex = reset($domIndex); + } + $attributeNames = array_diff(array_keys($indexData), ['referenceId', 'columns', 'name']); + foreach ($attributeNames as $attributeName) { + $this->updateElementAttribute( + $domIndex, + $attributeName, + $indexData + ); + } + + if (!empty($indexData['columns'])) { foreach ($indexData['columns'] as $column) { $columnXml = $domIndex->addChild('column'); $columnXml->addAttribute('name', $column); @@ -188,37 +263,48 @@ private function processIndexes(array $tableData, \SimpleXMLElement $table) */ private function processConstraints(array $tableData, \SimpleXMLElement $table) { - if (isset($tableData['constraints'])) { - foreach ($tableData['constraints'] as $constraintType => $constraints) { - if ($constraintType === 'foreign') { - foreach ($constraints as $name => $constraintData) { - $constraintData = $this->handleDefinition($constraintData); - $constraintDom = $table->addChild('constraint'); - $constraintDom->addAttribute('xsi:type', $constraintType, 'xsi'); - $constraintDom->addAttribute('name', $name); - - foreach ($constraintData as $attributeKey => $attributeValue) { - $constraintDom->addAttribute($attributeKey, $attributeValue); - } - } + if (!isset($tableData['constraints'])) { + return $table; + } + + foreach ($tableData['constraints'] as $constraintType => $constraints) { + foreach ($constraints as $constraintName => $constraintData) { + $constraintData = $this->handleDefinition($constraintData); + $domConstraint = $table->xpath("constraint[@referenceId='" . $constraintName . "']"); + if (!$domConstraint) { + $domConstraint = $table->addChild('constraint'); + $domConstraint->addAttribute('xsi:type', $constraintType, 'xsi'); + $domConstraint->addAttribute('referenceId', $constraintName); } else { - foreach ($constraints as $name => $constraintData) { - $constraintData = $this->handleDefinition($constraintData); - $constraintDom = $table->addChild('constraint'); - $constraintDom->addAttribute('xsi:type', $constraintType, 'xsi'); - $constraintDom->addAttribute('name', $name); - $constraintData['columns'] = $constraintData['columns'] ?? []; - - if (isset($constraintData['disabled'])) { - $constraintDom->addAttribute('disabled', (bool) $constraintData['disabled']); - } - - foreach ($constraintData['columns'] as $column) { - $columnXml = $constraintDom->addChild('column'); - $columnXml->addAttribute('name', $column); - } + $domConstraint = reset($domConstraint); + } + + $attributeNames = array_diff( + array_keys($constraintData), + ['referenceId', 'xsi:type', 'disabled', 'columns', 'name', 'type'] + ); + foreach ($attributeNames as $attributeName) { + $this->updateElementAttribute( + $domConstraint, + $attributeName, + $constraintData + ); + } + + if (!empty($constraintData['columns'])) { + foreach ($constraintData['columns'] as $column) { + $columnXml = $domConstraint->addChild('column'); + $columnXml->addAttribute('name', $column); } } + + if (!empty($constraintData['disabled'])) { + $this->updateElementAttribute( + $domConstraint, + 'disabled', + $constraintData + ); + } } } @@ -236,4 +322,26 @@ private function persistModule(\SimpleXMLElement $simpleXmlElementDom, $path) { $this->xmlPersistor->persist($simpleXmlElementDom, $path); } + + /** + * Retrieve unique index declaration by name. + * + * @param \SimpleXMLElement $table + * @param string $indexName + * @return \SimpleXMLElement|null + */ + private function getUniqueIndexByName(\SimpleXMLElement $table, string $indexName): ?\SimpleXMLElement + { + $indexElement = null; + $constraint = $table->xpath("constraint[@referenceId='" . $indexName . "']"); + if ($constraint) { + $constraint = reset($constraint); + $type = $constraint->attributes('xsi', true)->type; + if ($type == 'unique') { + $indexElement = $constraint; + } + } + + return $indexElement; + } } diff --git a/lib/internal/Magento/Framework/Setup/Test/Unit/Declaration/Schema/Config/ConverterTest.php b/lib/internal/Magento/Framework/Setup/Test/Unit/Declaration/Schema/Config/ConverterTest.php index 2bb4bbcdb8d0..80b0517f7bfd 100644 --- a/lib/internal/Magento/Framework/Setup/Test/Unit/Declaration/Schema/Config/ConverterTest.php +++ b/lib/internal/Magento/Framework/Setup/Test/Unit/Declaration/Schema/Config/ConverterTest.php @@ -46,7 +46,7 @@ public function testConvert() - +
@@ -74,12 +74,12 @@ public function testConvert() ], ], 'constraint' => [ - 'PRIMARY' => [ + 'PRIMARY_INDEX' => [ 'column' => [ 'id' => 'id', ], 'type' => 'primary', - 'name' => 'PRIMARY', + 'referenceId' => 'PRIMARY_INDEX', ], ], 'name' => 'test_table', diff --git a/lib/internal/Magento/Framework/Setup/Test/Unit/Declaration/Schema/Db/SchemaBuilderTest.php b/lib/internal/Magento/Framework/Setup/Test/Unit/Declaration/Schema/Db/SchemaBuilderTest.php index 15dfcc746407..4e0c12920401 100644 --- a/lib/internal/Magento/Framework/Setup/Test/Unit/Declaration/Schema/Db/SchemaBuilderTest.php +++ b/lib/internal/Magento/Framework/Setup/Test/Unit/Declaration/Schema/Db/SchemaBuilderTest.php @@ -299,9 +299,6 @@ public function testBuild(array $columns, array $references, array $constraints, * @param array $references * @param array $constraints * @param array $indexes - * @expectedException \Exception - * @expectedExceptionMessage - * User Warning: Column unknown_column does not exist for index/constraint FIRST_INDEX in table second_table */ public function testBuildUnknownIndexColumn(array $columns, array $references, array $constraints, array $indexes) { @@ -315,6 +312,10 @@ public function testBuildUnknownIndexColumn(array $columns, array $references, a Schema::class, ['resourceConnection' => $resourceConnectionMock] ); + $this->expectException(\Exception::class); + $this->expectExceptionMessage( + 'User Warning: Column unknown_column does not exist for index/constraint FIRST_INDEX in table second_table.' + ); $this->model->build($schema); } diff --git a/lib/internal/Magento/Framework/Setup/Test/Unit/Declaration/Schema/Dto/Factories/ForeignTest.php b/lib/internal/Magento/Framework/Setup/Test/Unit/Declaration/Schema/Dto/Factories/ForeignTest.php deleted file mode 100644 index ee4331e7bfc5..000000000000 --- a/lib/internal/Magento/Framework/Setup/Test/Unit/Declaration/Schema/Dto/Factories/ForeignTest.php +++ /dev/null @@ -1,187 +0,0 @@ -objectManagerHelper = new ObjectManagerHelper($this); - $this->objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class) - ->disableOriginalConstructor() - ->getMock(); - $this->adapterMock = $this->getMockBuilder(AdapterInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->tableNameResolver = $this->getMockBuilder(TableNameResolver::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->foreignFactory = $this->objectManagerHelper->getObject( - Foreign::class, - [ - 'objectManager' => $this->objectManagerMock, - 'resourceConnection' => $this->resourceConnectionMock, - 'tableNameResolver' => $this->tableNameResolver, - ] - ); - } - - /** - * @param string $prefix - * @dataProvider createDataProvider - */ - public function testCreate(string $prefix) - { - $resource = 'default'; - $tableNameWithoutPrefix = 'table_name'; - $tableName = $prefix . $tableNameWithoutPrefix; - - $columnName = 'entity_id'; - $referenceTableName = 'second_table'; - $referenceColumnName = 'website_id'; - - $foreignKeyNameWithoutPrefix = 'table_name_field_name'; - $foreignKeyName = $prefix . $foreignKeyNameWithoutPrefix; - - $table = $this->objectManagerHelper->getObject( - DataObject::class, - [ - 'data' => [ - 'resource' => $resource, - 'name' => $tableName, - 'name_without_prefix' => $tableNameWithoutPrefix, - ], - ] - ); - - $columnMock = $this->objectManagerHelper->getObject( - DataObject::class, - [ - 'data' => ['name' => $columnName], - ] - ); - - $referenceTableMock = $this->objectManagerHelper->getObject( - DataObject::class, - [ - 'data' => ['name_without_prefix' => $referenceTableName], - ] - ); - - $referenceColumnMock = $this->objectManagerHelper->getObject( - DataObject::class, - [ - 'data' => ['name' => $referenceColumnName], - ] - ); - - $data = [ - 'name' => $foreignKeyName, - 'table' => $table, - 'column' => $columnMock, - 'referenceTable' => $referenceTableMock, - 'referenceColumn' => $referenceColumnMock, - ]; - - $expectedData = array_merge( - $data, - [ - 'onDelete' => Foreign::DEFAULT_ON_DELETE, - 'nameWithoutPrefix' => $foreignKeyNameWithoutPrefix, - ] - ); - - $this->resourceConnectionMock - ->method('getTablePrefix') - ->willReturn($prefix); - - $this->resourceConnectionMock - ->method('getConnection') - ->with($resource) - ->willReturn($this->adapterMock); - - $this->tableNameResolver - ->method('getNameOfOriginTable') - ->with($tableNameWithoutPrefix) - ->willReturn($tableNameWithoutPrefix); - - $this->adapterMock - ->method('getForeignKeyName') - ->with($tableNameWithoutPrefix, $columnName, $referenceTableName, $referenceColumnName) - ->willReturn($foreignKeyNameWithoutPrefix); - - $this->objectManagerMock - ->expects($this->once()) - ->method('create') - ->with(Reference::class, $expectedData); - - $this->foreignFactory->create($data); - } - - /** - * @return array - */ - public function createDataProvider(): array - { - return [ - 'Prefix is defined' => [ - 'pref_', - ], - 'Prefix is not defined' => [ - '', - ], - ]; - } -} diff --git a/lib/internal/Magento/Framework/Setup/Test/Unit/Patch/PatchApplierTest.php b/lib/internal/Magento/Framework/Setup/Test/Unit/Patch/PatchApplierTest.php index f89bdc9e137d..cb40845bcc48 100644 --- a/lib/internal/Magento/Framework/Setup/Test/Unit/Patch/PatchApplierTest.php +++ b/lib/internal/Magento/Framework/Setup/Test/Unit/Patch/PatchApplierTest.php @@ -91,7 +91,7 @@ class PatchApplierTest extends \PHPUnit\Framework\TestCase /** * @var PatchBackwardCompatability |\PHPUnit_Framework_MockObject_MockObject */ - private $patchBacwardCompatability; + private $patchBackwardCompatability; protected function setUp() { @@ -109,7 +109,7 @@ protected function setUp() $this->moduleDataSetupMock->expects($this->any())->method('getConnection')->willReturn($this->connectionMock); $objectManager = new ObjectManager($this); - $this->patchBacwardCompatability = $objectManager->getObject( + $this->patchBackwardCompatability = $objectManager->getObject( PatchBackwardCompatability::class, [ 'moduleResource' => $this->moduleResourceMock @@ -128,7 +128,7 @@ protected function setUp() 'objectManager' => $this->objectManagerMock, 'schemaSetup' => $this->schemaSetupMock, 'moduleDataSetup' => $this->moduleDataSetupMock, - 'patchBackwardCompatability' => $this->patchBacwardCompatability + 'patchBackwardCompatability' => $this->patchBackwardCompatability ] ); require_once __DIR__ . '/../_files/data_patch_classes.php'; diff --git a/lib/internal/Magento/Framework/Setup/Test/Unit/SchemaListenerTest.php b/lib/internal/Magento/Framework/Setup/Test/Unit/SchemaListenerTest.php index 4e34b3aebbf3..cfde80b12ee3 100644 --- a/lib/internal/Magento/Framework/Setup/Test/Unit/SchemaListenerTest.php +++ b/lib/internal/Magento/Framework/Setup/Test/Unit/SchemaListenerTest.php @@ -127,6 +127,7 @@ public function testCreateTable() : void 'default' => 'CURRENT_TIMESTAMP', 'disabled' => false, 'onCreate' => null, + 'comment' => 'Column with type timestamp init update', ], 'integer' => [ @@ -139,6 +140,7 @@ public function testCreateTable() : void 'default' => null, 'disabled' => false, 'onCreate' => null, + 'comment' => 'Integer' ], 'decimal' => [ @@ -151,6 +153,7 @@ public function testCreateTable() : void 'default' => null, 'disabled' => false, 'onCreate' => null, + 'comment' => 'Decimal' ], ], $tables['First_Module']['new_table']['columns'] diff --git a/lib/internal/Magento/Framework/Setup/Test/Unit/SchemaPersistorTest.php b/lib/internal/Magento/Framework/Setup/Test/Unit/SchemaPersistorTest.php index cc88af15a262..f65e6c910dc0 100644 --- a/lib/internal/Magento/Framework/Setup/Test/Unit/SchemaPersistorTest.php +++ b/lib/internal/Magento/Framework/Setup/Test/Unit/SchemaPersistorTest.php @@ -152,13 +152,13 @@ public function schemaListenerTablesDataProvider() : array - - + - + diff --git a/lib/internal/Magento/Framework/Stdlib/DateTime/Timezone.php b/lib/internal/Magento/Framework/Stdlib/DateTime/Timezone.php index 3c2f20fcc186..cf747bfa2b73 100644 --- a/lib/internal/Magento/Framework/Stdlib/DateTime/Timezone.php +++ b/lib/internal/Magento/Framework/Stdlib/DateTime/Timezone.php @@ -85,7 +85,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getDefaultTimezonePath() { @@ -93,7 +93,7 @@ public function getDefaultTimezonePath() } /** - * {@inheritdoc} + * @inheritdoc */ public function getDefaultTimezone() { @@ -101,7 +101,7 @@ public function getDefaultTimezone() } /** - * {@inheritdoc} + * @inheritdoc */ public function getConfigTimezone($scopeType = null, $scopeCode = null) { @@ -113,7 +113,7 @@ public function getConfigTimezone($scopeType = null, $scopeCode = null) } /** - * {@inheritdoc} + * @inheritdoc */ public function getDateFormat($type = \IntlDateFormatter::SHORT) { @@ -125,7 +125,7 @@ public function getDateFormat($type = \IntlDateFormatter::SHORT) } /** - * {@inheritdoc} + * @inheritdoc */ public function getDateFormatWithLongYear() { @@ -137,7 +137,7 @@ public function getDateFormatWithLongYear() } /** - * {@inheritdoc} + * @inheritdoc */ public function getTimeFormat($type = \IntlDateFormatter::SHORT) { @@ -149,7 +149,7 @@ public function getTimeFormat($type = \IntlDateFormatter::SHORT) } /** - * {@inheritdoc} + * @inheritdoc */ public function getDateTimeFormat($type) { @@ -157,7 +157,7 @@ public function getDateTimeFormat($type) } /** - * {@inheritdoc} + * @inheritdoc */ public function date($date = null, $locale = null, $useTimezone = true, $includeTime = true) { @@ -191,7 +191,7 @@ public function date($date = null, $locale = null, $useTimezone = true, $include } /** - * {@inheritdoc} + * @inheritdoc */ public function scopeDate($scope = null, $date = null, $includeTime = false) { @@ -204,7 +204,7 @@ public function scopeDate($scope = null, $date = null, $includeTime = false) } /** - * {@inheritdoc} + * @inheritdoc */ public function formatDate($date = null, $format = \IntlDateFormatter::SHORT, $showTime = false) { @@ -218,7 +218,7 @@ public function formatDate($date = null, $format = \IntlDateFormatter::SHORT, $s } /** - * {@inheritdoc} + * @inheritdoc */ public function scopeTimeStamp($scope = null) { @@ -231,7 +231,7 @@ public function scopeTimeStamp($scope = null) } /** - * {@inheritdoc} + * @inheritdoc */ public function isScopeDateInInterval($scope, $dateFrom = null, $dateTo = null) { @@ -257,13 +257,7 @@ public function isScopeDateInInterval($scope, $dateFrom = null, $dateTo = null) } /** - * @param string|\DateTimeInterface $date - * @param int $dateType - * @param int $timeType - * @param string|null $locale - * @param string|null $timezone - * @param string|null $pattern - * @return string + * @inheritdoc */ public function formatDateTime( $date, @@ -299,13 +293,7 @@ public function formatDateTime( } /** - * Convert date from config timezone to Utc. - * If pass \DateTime object as argument be sure that timezone is the same with config timezone - * - * @param string|\DateTimeInterface $date - * @param string $format - * @throws LocalizedException - * @return string + * @inheritdoc */ public function convertConfigTimeToUtc($date, $format = 'Y-m-d H:i:s') { diff --git a/lib/internal/Magento/Framework/Stdlib/DateTime/Timezone/LocalizedDateToUtcConverter.php b/lib/internal/Magento/Framework/Stdlib/DateTime/Timezone/LocalizedDateToUtcConverter.php new file mode 100644 index 000000000000..420fd6e543e0 --- /dev/null +++ b/lib/internal/Magento/Framework/Stdlib/DateTime/Timezone/LocalizedDateToUtcConverter.php @@ -0,0 +1,74 @@ +timezone = $timezone; + $this->localeResolver = $localeResolver; + } + + /** + * @inheritdoc + */ + public function convertLocalizedDateToUtc($date) + { + $configTimezone = $this->timezone->getConfigTimezone(); + $locale = $this->localeResolver->getLocale(); + + $formatter = new \IntlDateFormatter( + $locale, + \IntlDateFormatter::MEDIUM, + \IntlDateFormatter::MEDIUM, + $configTimezone + ); + + $localTimestamp = $formatter->parse($date); + $gmtTimestamp = $this->timezone->date($localTimestamp)->getTimestamp(); + $formattedUniversalTime = date($this->defaultFormat, $gmtTimestamp); + + $date = new \DateTime($formattedUniversalTime, new \DateTimeZone($configTimezone)); + $date->setTimezone(new \DateTimeZone('UTC')); + + return $date->format($this->defaultFormat); + } +} diff --git a/lib/internal/Magento/Framework/Stdlib/DateTime/Timezone/LocalizedDateToUtcConverterInterface.php b/lib/internal/Magento/Framework/Stdlib/DateTime/Timezone/LocalizedDateToUtcConverterInterface.php new file mode 100644 index 000000000000..d10bd5f2fefb --- /dev/null +++ b/lib/internal/Magento/Framework/Stdlib/DateTime/Timezone/LocalizedDateToUtcConverterInterface.php @@ -0,0 +1,22 @@ +FALSE on error. @@ -56,7 +56,7 @@ public function mkdirRecursive($path, $mode = 0777) $dir = explode("/", $path); $path = ""; $ret = true; - for ($i = 0; $i < count($dir); $i++) { + for ($i = 0, $count = count($dir); $i < $count; $i++) { $path .= "/" . $dir[$i]; if (!@ftp_chdir($this->_conn, $path)) { @ftp_chdir($this->_conn, "/"); @@ -127,7 +127,7 @@ public function validateConnectionString($string) public function connect($string, $timeout = 900) { $params = $this->validateConnectionString($string); - $port = isset($params['port']) ? intval($params['port']) : 21; + $port = isset($params['port']) ? (int)$params['port'] : 21; $this->_conn = ftp_connect($params['host'], $port, $timeout); @@ -147,7 +147,7 @@ public function connect($string, $timeout = 900) } /** - * ftp_fput wrapper + * Wrapper for ftp_fput * * @param string $remoteFile * @param resource $handle @@ -162,7 +162,7 @@ public function fput($remoteFile, $handle, $mode = FTP_BINARY, $startPos = 0) } /** - * ftp_put wrapper + * Wrapper for ftp_put * * @param string $remoteFile * @param string $localFile @@ -188,7 +188,7 @@ public function getcwd() if (empty($data[1])) { return false; } - if (intval($data[0]) != 257) { + if ((int)$data[0] != 257) { return false; } $out = trim($data[1], '"'); @@ -199,7 +199,7 @@ public function getcwd() } /** - * ftp_raw wrapper + * Wrapper for ftp_raw * * @param string $cmd * @return array The server's response as an array of strings. @@ -272,7 +272,7 @@ public function download($remote, $local, $ftpMode = FTP_BINARY) } /** - * ftp_pasv wrapper + * Wrapper for ftp_pasv * * @param bool $pasv * @return bool @@ -296,7 +296,7 @@ public function close() } /** - * ftp_chmod wrapper + * Wrapper for ftp_chmod * * @param int $mode * @param string $remoteFile @@ -309,7 +309,7 @@ public function chmod($mode, $remoteFile) } /** - * ftp_chdir wrapper + * Wrapper for ftp_chdir * * @param string $dir * @return bool @@ -321,7 +321,7 @@ public function chdir($dir) } /** - * ftp_cdup wrapper + * Wrapper for ftp_cdup * * @return bool */ @@ -332,7 +332,7 @@ public function cdup() } /** - * ftp_get wrapper + * Wrapper for ftp_get * * @param string $localFile * @param string $remoteFile @@ -349,7 +349,7 @@ public function get($localFile, $remoteFile, $fileMode = FTP_BINARY, $resumeOffs } /** - * ftp_nlist wrapper + * Wrapper for ftp_nlist * * @param string $dir * @return bool @@ -362,7 +362,7 @@ public function nlist($dir = "/") } /** - * ftp_rawlist wrapper + * Wrapper for ftp_rawlist * * @param string $dir * @param bool $recursive diff --git a/lib/internal/Magento/Framework/Test/Unit/Communication/Config/ValidatorTest.php b/lib/internal/Magento/Framework/Test/Unit/Communication/Config/ValidatorTest.php new file mode 100644 index 000000000000..55410af176af --- /dev/null +++ b/lib/internal/Magento/Framework/Test/Unit/Communication/Config/ValidatorTest.php @@ -0,0 +1,70 @@ +methodsMap = $this->createMock(MethodsMap::class); + + $this->methodsMap->expects(static::any()) + ->method('getMethodsMap') + ->will($this->throwException(new \InvalidArgumentException('message', 333))); + + $this->typeProcessor = $this->createMock(TypeProcessor::class); + $this->typeProcessor->expects(static::any()) + ->method('isTypeSimple') + ->willReturn(false); + + $this->typeProcessor->expects(static::any()) + ->method('isTypeSimple') + ->willReturn(false); + } + + /** + * @expectedException \LogicException + * @expectedExceptionCode 333 + * @expectedExceptionMessage Response schema definition has service class with wrong annotated methods + */ + public function testValidateResponseSchemaType() + { + /** @var Validator $validator */ + $validator = new Validator($this->typeProcessor, $this->methodsMap); + $validator->validateResponseSchemaType('123', '123'); + } + + /** + * @expectedException \LogicException + * @expectedExceptionCode 333 + * @expectedExceptionMessage Request schema definition has service class with wrong annotated methods + */ + public function testValidateRequestSchemaType() + { + /** @var Validator $validator */ + $validator = new Validator($this->typeProcessor, $this->methodsMap); + $validator->validateRequestSchemaType('123', '123'); + } +} diff --git a/lib/internal/Magento/Framework/Test/Unit/Data/CollectionTest.php b/lib/internal/Magento/Framework/Test/Unit/Data/CollectionTest.php index 75f8b1cd0633..913d5a577e62 100644 --- a/lib/internal/Magento/Framework/Test/Unit/Data/CollectionTest.php +++ b/lib/internal/Magento/Framework/Test/Unit/Data/CollectionTest.php @@ -57,6 +57,27 @@ public function testWalk() ); } + /** + * Ensure that getSize works correctly with clear + * + */ + public function testClearTotalRecords() + { + $objOne = new \Magento\Framework\DataObject(['id' => 1, 'name' => 'one']); + $objTwo = new \Magento\Framework\DataObject(['id' => 2, 'name' => 'two']); + $objThree = new \Magento\Framework\DataObject(['id' => 3, 'name' => 'three']); + + /** @noinspection PhpUnhandledExceptionInspection */ + $this->collection->addItem($objOne); + /** @noinspection PhpUnhandledExceptionInspection */ + $this->collection->addItem($objTwo); + /** @noinspection PhpUnhandledExceptionInspection */ + $this->collection->addItem($objThree); + $this->assertEquals(3, $this->collection->getSize()); + $this->collection->clear(); + $this->assertEquals(0, $this->collection->getSize()); + } + /** * Callback function. * diff --git a/lib/internal/Magento/Framework/Test/Unit/EscaperTest.php b/lib/internal/Magento/Framework/Test/Unit/EscaperTest.php index ec05478b90db..e406994b54c1 100644 --- a/lib/internal/Magento/Framework/Test/Unit/EscaperTest.php +++ b/lib/internal/Magento/Framework/Test/Unit/EscaperTest.php @@ -72,9 +72,9 @@ public function testEscapeJsEscapesOwaspRecommendedRanges() // Exceptions to escaping ranges $immune = [',', '.', '_']; for ($chr = 0; $chr < 0xFF; $chr++) { - if ($chr >= 0x30 && $chr <= 0x39 - || $chr >= 0x41 && $chr <= 0x5A - || $chr >= 0x61 && $chr <= 0x7A + if (($chr >= 0x30 && $chr <= 0x39) + || ($chr >= 0x41 && $chr <= 0x5A) + || ($chr >= 0x61 && $chr <= 0x7A) ) { $literal = $this->codepointToUtf8($chr); $this->assertEquals($literal, $this->escaper->escapeJs($literal)); @@ -174,6 +174,11 @@ public function escapeHtmlDataProvider() 'data' => '&<>"\'&<>"' ', 'expected' => '&<>"'&<>"' ' ], + 'text with special characters and allowed tag' => [ + 'data' => '&
"\'&<>"' ', + 'expected' => '&
"'&<>"' ', + 'allowedTags' => ['br'], + ], 'text with multiple allowed tags, includes self closing tag' => [ 'data' => 'some text in tags
', 'expected' => 'some text in tags
', diff --git a/lib/internal/Magento/Framework/Test/Unit/Module/Plugin/DbStatusValidatorTest.php b/lib/internal/Magento/Framework/Test/Unit/Module/Plugin/DbStatusValidatorTest.php index a52c49136d15..5027bfae606a 100644 --- a/lib/internal/Magento/Framework/Test/Unit/Module/Plugin/DbStatusValidatorTest.php +++ b/lib/internal/Magento/Framework/Test/Unit/Module/Plugin/DbStatusValidatorTest.php @@ -157,29 +157,29 @@ public static function beforeDispatchOutOfDateWithErrorsDataProvider() [ DbVersionInfo::KEY_MODULE => 'Magento_Module4', DbVersionInfo::KEY_TYPE => 'data', - DbVersionInfo::KEY_CURRENT => '1.0.1', - DbVersionInfo::KEY_REQUIRED => '1.0.0' + DbVersionInfo::KEY_CURRENT => '1.0.10', + DbVersionInfo::KEY_REQUIRED => '1.0.9' ], ], 'expectedMessage' => "Please update your modules: " . "Run \"composer install\" from the Magento root directory.\n" . "The following modules are outdated:\n" . "Magento_Module3 schema: code version - 1.0.0, database version - 2.0.0\n" - . "Magento_Module4 data: code version - 1.0.0, database version - 1.0.1", + . "Magento_Module4 data: code version - 1.0.9, database version - 1.0.10", ], 'some versions too high, some too low' => [ 'errors' => [ [ - DbVersionInfo::KEY_MODULE => 'Magento_Module1', + DbVersionInfo::KEY_MODULE => 'Magento_Module2', DbVersionInfo::KEY_TYPE => 'schema', - DbVersionInfo::KEY_CURRENT => '2.0.0', - DbVersionInfo::KEY_REQUIRED => '1.0.0' + DbVersionInfo::KEY_CURRENT => '1.9.0', + DbVersionInfo::KEY_REQUIRED => '1.12.0' ], [ - DbVersionInfo::KEY_MODULE => 'Magento_Module2', + DbVersionInfo::KEY_MODULE => 'Magento_Module1', DbVersionInfo::KEY_TYPE => 'schema', - DbVersionInfo::KEY_CURRENT => '1.0.0', - DbVersionInfo::KEY_REQUIRED => '2.0.0' + DbVersionInfo::KEY_CURRENT => '2.0.0', + DbVersionInfo::KEY_REQUIRED => '1.0.0' ], ], 'expectedMessage' => "Please update your modules: " diff --git a/lib/internal/Magento/Framework/Test/Unit/TranslateTest.php b/lib/internal/Magento/Framework/Test/Unit/TranslateTest.php new file mode 100644 index 000000000000..0ec27d6d053c --- /dev/null +++ b/lib/internal/Magento/Framework/Test/Unit/TranslateTest.php @@ -0,0 +1,374 @@ +viewDesign = $this->createMock(\Magento\Framework\View\DesignInterface::class); + $this->cache = $this->createMock(\Magento\Framework\Cache\FrontendInterface::class); + $this->viewFileSystem = $this->createMock(\Magento\Framework\View\FileSystem::class); + $this->moduleList = $this->createMock(\Magento\Framework\Module\ModuleList::class); + $this->modulesReader = $this->createMock(\Magento\Framework\Module\Dir\Reader::class); + $this->scopeResolver = $this->createMock(\Magento\Framework\App\ScopeResolverInterface::class); + $this->resource = $this->createMock(\Magento\Framework\Translate\ResourceInterface::class); + $this->locale = $this->createMock(\Magento\Framework\Locale\ResolverInterface::class); + $this->appState = $this->createMock(\Magento\Framework\App\State::class); + $this->request = $this->getMockForAbstractClass( + \Magento\Framework\App\RequestInterface::class, + [], + '', + false, + false, + true, + ['getParam', 'getControllerModule'] + ); + $this->csvParser = $this->createMock(\Magento\Framework\File\Csv::class); + $this->packDictionary = $this->createMock(\Magento\Framework\App\Language\Dictionary::class); + $this->directory = $this->createMock(\Magento\Framework\Filesystem\Directory\ReadInterface::class); + $filesystem = $this->createMock(\Magento\Framework\Filesystem::class); + $filesystem->expects($this->once())->method('getDirectoryRead')->will($this->returnValue($this->directory)); + $this->fileDriver = $this->createMock(\Magento\Framework\Filesystem\DriverInterface::class); + + $this->translate = new Translate( + $this->viewDesign, + $this->cache, + $this->viewFileSystem, + $this->moduleList, + $this->modulesReader, + $this->scopeResolver, + $this->resource, + $this->locale, + $this->appState, + $filesystem, + $this->request, + $this->csvParser, + $this->packDictionary, + $this->fileDriver + ); + + $serializerMock = $this->createMock(SerializerInterface::class); + $serializerMock->method('serialize') + ->willReturnCallback(function ($data) { + return json_encode($data); + }); + $serializerMock->method('unserialize') + ->willReturnCallback(function ($string) { + return json_decode($string, true); + }); + $objectManager->setBackwardCompatibleProperty( + $this->translate, + 'serializer', + $serializerMock + ); + } + + /** + * @param string $area + * @param bool $forceReload + * @param array $cachedData + * @dataProvider dataProviderLoadDataCachedTranslation + */ + public function testLoadDataCachedTranslation($area, $forceReload, $cachedData): void + { + $this->expectsSetConfig('Magento/luma'); + + $this->cache->expects($this->once()) + ->method('load') + ->will($this->returnValue(json_encode($cachedData))); + + $this->appState->expects($this->exactly($area ? 0 : 1)) + ->method('getAreaCode') + ->will($this->returnValue('frontend')); + + $this->translate->loadData($area, $forceReload); + $this->assertEquals($cachedData, $this->translate->getData()); + } + + /** + * @return array + */ + public function dataProviderLoadDataCachedTranslation(): array + { + $cachedData = ['cached 1' => 'translated 1', 'cached 2' => 'translated 2']; + return [ + ['adminhtml', false, $cachedData], + ['frontend', false, $cachedData], + [null, false, $cachedData], + ]; + } + + /** + * @param string $area + * @param bool $forceReload + * @dataProvider dataProviderForTestLoadData + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + public function testLoadData($area, $forceReload): void + { + $this->expectsSetConfig('Magento/luma'); + + $this->appState->expects($this->exactly($area ? 0 : 1)) + ->method('getAreaCode') + ->will($this->returnValue('frontend')); + + $this->cache->expects($this->exactly($forceReload ? 0 : 1)) + ->method('load') + ->will($this->returnValue(false)); + + $this->directory->expects($this->any())->method('isExist')->will($this->returnValue(true)); + + // _loadModuleTranslation() + $modules = ['some_module', 'other_module', 'another_module', 'current_module']; + $this->request->expects($this->any()) + ->method('getControllerModule') + ->willReturn('current_module'); + $this->moduleList->expects($this->once())->method('getNames')->will($this->returnValue($modules)); + $moduleData = [ + 'module original' => 'module translated', + 'module theme' => 'module-theme original translated', + 'module pack' => 'module-pack original translated', + 'module db' => 'module-db original translated', + ]; + $this->modulesReader->expects($this->any())->method('getModuleDir')->will($this->returnValue('/app/module')); + $themeData = [ + 'theme original' => 'theme translated', + 'module theme' => 'theme translated overwrite', + 'module pack' => 'theme-pack translated overwrite', + 'module db' => 'theme-db translated overwrite', + ]; + $this->csvParser->expects($this->any()) + ->method('getDataPairs') + ->will( + $this->returnValueMap( + [ + ['/app/module/en_US.csv', 0, 1, $moduleData], + ['/app/module/en_GB.csv', 0, 1, $moduleData], + ['/theme.csv', 0, 1, $themeData], + ] + ) + ); + $this->fileDriver->expects($this->any()) + ->method('isExists') + ->will( + $this->returnValueMap( + [ + ['/app/module/en_US.csv', true], + ['/app/module/en_GB.csv', true], + ['/theme.csv', true], + ] + ) + ); + + // _loadPackTranslation + $packData = [ + 'pack original' => 'pack translated', + 'module pack' => 'pack translated overwrite', + 'module db' => 'pack-db translated overwrite', + ]; + $this->packDictionary->expects($this->once())->method('getDictionary')->will($this->returnValue($packData)); + + // _loadThemeTranslation() + $this->viewFileSystem->expects($this->any()) + ->method('getLocaleFileName') + ->will($this->returnValue('/theme.csv')); + + // _loadDbTranslation() + $dbData = [ + 'db original' => 'db translated', + 'module db' => 'db translated overwrite', + ]; + $this->resource->expects($this->any())->method('getTranslationArray')->will($this->returnValue($dbData)); + + $this->cache->expects($this->exactly($forceReload ? 0 : 1))->method('save'); + + $this->translate->loadData($area, $forceReload); + + $expected = [ + 'module original' => 'module translated', + 'module theme' => 'theme translated overwrite', + 'module pack' => 'theme-pack translated overwrite', + 'module db' => 'db translated overwrite', + 'theme original' => 'theme translated', + 'pack original' => 'pack translated', + 'db original' => 'db translated', + ]; + $this->assertEquals($expected, $this->translate->getData()); + } + + /** + * @return array + */ + public function dataProviderForTestLoadData(): array + { + return [ + ['adminhtml', true], + ['adminhtml', false], + ['frontend', true], + ['frontend', false], + [null, true], + [null, false] + ]; + } + + /** + * @param $data + * @param $result + * @dataProvider dataProviderForTestGetData + */ + public function testGetData($data, $result): void + { + $this->cache->expects($this->once()) + ->method('load') + ->will($this->returnValue(json_encode($data))); + $this->expectsSetConfig('themeId'); + $this->translate->loadData('frontend'); + $this->assertEquals($result, $this->translate->getData()); + } + + /** + * @return array + */ + public function dataProviderForTestGetData(): array + { + $data = ['original 1' => 'translated 1', 'original 2' => 'translated 2']; + return [ + [$data, $data], + [null, []] + ]; + } + + public function testGetLocale(): void + { + $this->locale->expects($this->once())->method('getLocale')->will($this->returnValue('en_US')); + $this->assertEquals('en_US', $this->translate->getLocale()); + + $this->locale->expects($this->never())->method('getLocale'); + $this->assertEquals('en_US', $this->translate->getLocale()); + + $this->locale->expects($this->never())->method('getLocale'); + $this->translate->setLocale('en_GB'); + $this->assertEquals('en_GB', $this->translate->getLocale()); + } + + public function testSetLocale(): void + { + $this->translate->setLocale('en_GB'); + $this->locale->expects($this->never())->method('getLocale'); + $this->assertEquals('en_GB', $this->translate->getLocale()); + } + + public function testGetTheme(): void + { + $this->request->expects($this->at(0))->method('getParam')->with('theme')->will($this->returnValue('')); + + $requestTheme = ['theme_title' => 'Theme Title']; + $this->request->expects($this->at(1))->method('getParam')->with('theme') + ->will($this->returnValue($requestTheme)); + + $this->assertEquals('theme', $this->translate->getTheme()); + $this->assertEquals('themeTheme Title', $this->translate->getTheme()); + } + + public function testLoadDataNoTheme(): void + { + $forceReload = true; + $this->expectsSetConfig(null, null); + $this->moduleList->expects($this->once())->method('getNames')->will($this->returnValue([])); + $this->appState->expects($this->once())->method('getAreaCode')->will($this->returnValue('frontend')); + $this->packDictionary->expects($this->once())->method('getDictionary')->will($this->returnValue([])); + $this->resource->expects($this->any())->method('getTranslationArray')->will($this->returnValue([])); + $this->assertEquals($this->translate, $this->translate->loadData(null, $forceReload)); + } + + /** + * Declare calls expectation for setConfig() method + */ + protected function expectsSetConfig($themeId, $localeCode = 'en_US'): void + { + $this->locale->expects($this->any())->method('getLocale')->will($this->returnValue($localeCode)); + $scope = new \Magento\Framework\DataObject(['code' => 'frontendCode', 'id' => 1]); + $scopeAdmin = new \Magento\Framework\DataObject(['code' => 'adminCode', 'id' => 0]); + $this->scopeResolver->expects($this->any()) + ->method('getScope') + ->will( + $this->returnValueMap( + [ + [null, $scope], + ['admin', $scopeAdmin], + ] + ) + ); + $designTheme = $this->getMockBuilder(\Magento\Theme\Model\Theme::class) + ->disableOriginalConstructor() + ->getMock(); + + $designTheme->expects($this->once()) + ->method('getThemePath') + ->willReturn($themeId); + + $this->viewDesign->expects($this->any())->method('getDesignTheme')->will($this->returnValue($designTheme)); + } +} diff --git a/lib/internal/Magento/Framework/Test/Unit/UrlTest.php b/lib/internal/Magento/Framework/Test/Unit/UrlTest.php index 3713978d0f74..046a9d63fc8f 100644 --- a/lib/internal/Magento/Framework/Test/Unit/UrlTest.php +++ b/lib/internal/Magento/Framework/Test/Unit/UrlTest.php @@ -149,8 +149,8 @@ protected function getUrlModel($arguments = []) } /** - * @param $httpHost string - * @param $url string + * @param string $httpHost + * @param string $url * @dataProvider getCurrentUrlProvider */ public function testGetCurrentUrl($httpHost, $url) @@ -435,7 +435,7 @@ public function testGetDirectUrl() } /** - * @param string $url + * @param string $inputUrl * @dataProvider getRebuiltUrlDataProvider */ public function testGetRebuiltUrl($inputUrl, $outputUrl) diff --git a/lib/internal/Magento/Framework/TestFramework/Unit/Autoloader/GeneratedClassesAutoloader.php b/lib/internal/Magento/Framework/TestFramework/Unit/Autoloader/GeneratedClassesAutoloader.php index 03c7d85251ac..2d8348f64a5a 100644 --- a/lib/internal/Magento/Framework/TestFramework/Unit/Autoloader/GeneratedClassesAutoloader.php +++ b/lib/internal/Magento/Framework/TestFramework/Unit/Autoloader/GeneratedClassesAutoloader.php @@ -65,7 +65,7 @@ public function load($className) include $classSourceFile; return true; } - }; + } } return false; diff --git a/lib/internal/Magento/Framework/Translate.php b/lib/internal/Magento/Framework/Translate.php index ffa8e2503106..e992482609a8 100644 --- a/lib/internal/Magento/Framework/Translate.php +++ b/lib/internal/Magento/Framework/Translate.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Framework; @@ -59,7 +60,7 @@ class Translate implements \Magento\Framework\TranslateInterface protected $_viewDesign; /** - * @var \Magento\Framework\Cache\FrontendInterface $cache + * @var \Magento\Framework\Cache\FrontendInterface */ protected $_cache; @@ -277,6 +278,7 @@ protected function getConfig($key) /** * Retrieve name of the current module + * * @return mixed */ protected function getControllerModuleName() @@ -339,16 +341,21 @@ protected function _addData($data) } /** - * Load current theme translation + * Load current theme translation according to fallback * * @return $this */ protected function _loadThemeTranslation() { - $file = $this->_getThemeTranslationFile($this->getLocale()); - if ($file) { - $this->_addData($this->_getFileData($file)); + $themeFiles = $this->getThemeTranslationFilesList($this->getLocale()); + + /** @var string $file */ + foreach ($themeFiles as $file) { + if ($file) { + $this->_addData($this->_getFileData($file)); + } } + return $this; } @@ -389,11 +396,74 @@ protected function _getModuleTranslationFile($moduleName, $locale) return $file; } + /** + * Get theme translation locale file name + * + * @param string|null $locale + * @param array $config + * @return string|null + */ + private function getThemeTranslationFileName(?string $locale, array $config): ?string + { + $fileName = $this->_viewFileSystem->getLocaleFileName( + 'i18n' . '/' . $locale . '.csv', + $config + ); + + return $fileName ? $fileName : null; + } + + /** + * Get parent themes for the current theme in fallback order + * + * @return array + */ + private function getParentThemesList(): array + { + $themes = []; + + $parentTheme = $this->_viewDesign->getDesignTheme()->getParentTheme(); + while ($parentTheme) { + $themes[] = $parentTheme; + $parentTheme = $parentTheme->getParentTheme(); + } + $themes = array_reverse($themes); + + return $themes; + } + + /** + * Retrieve translation files for themes according to fallback + * + * @param string $locale + * + * @return array + */ + private function getThemeTranslationFilesList($locale): array + { + $translationFiles = []; + + /** @var \Magento\Framework\View\Design\ThemeInterface $theme */ + foreach ($this->getParentThemesList() as $theme) { + $config = $this->_config; + $config['theme'] = $theme->getCode(); + $translationFiles[] = $this->getThemeTranslationFileName($locale, $config); + } + + $translationFiles[] = $this->getThemeTranslationFileName($locale, $this->_config); + + return $translationFiles; + } + /** * Retrieve translation file for theme * * @param string $locale * @return string + * + * @deprecated + * + * @see \Magento\Framework\Translate::getThemeTranslationFilesList */ protected function _getThemeTranslationFile($locale) { diff --git a/lib/internal/Magento/Framework/Translate/Inline/ParserInterface.php b/lib/internal/Magento/Framework/Translate/Inline/ParserInterface.php index e8905f71ea73..4c3bfc1a1964 100644 --- a/lib/internal/Magento/Framework/Translate/Inline/ParserInterface.php +++ b/lib/internal/Magento/Framework/Translate/Inline/ParserInterface.php @@ -43,7 +43,7 @@ public function getContent(); /** * Sets the body content that is being parsed passed upon the passed in string. * - * @param $content string + * @param string $content * @return void */ public function setContent($content); diff --git a/lib/internal/Magento/Framework/Translate/Inline/Proxy.php b/lib/internal/Magento/Framework/Translate/Inline/Proxy.php index 370a88d6d9a4..e6d6cc57c2b0 100644 --- a/lib/internal/Magento/Framework/Translate/Inline/Proxy.php +++ b/lib/internal/Magento/Framework/Translate/Inline/Proxy.php @@ -55,10 +55,17 @@ public function __construct( } /** + * Remove links to other objects. + * * @return array + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __sleep() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + return ['subject', 'isShared']; } @@ -66,9 +73,14 @@ public function __sleep() * Retrieve ObjectManager from global scope * * @return void + * + * @SuppressWarnings(PHPMD.SerializationAware) + * @deprecated Do not use PHP serialization. */ public function __wakeup() { + trigger_error('Using PHP serialization is deprecated', E_USER_DEPRECATED); + $this->objectManager = \Magento\Framework\App\ObjectManager::getInstance(); } diff --git a/lib/internal/Magento/Framework/Validator/Factory.php b/lib/internal/Magento/Framework/Validator/Factory.php index f2089c662e95..2a296f7cdcb2 100644 --- a/lib/internal/Magento/Framework/Validator/Factory.php +++ b/lib/internal/Magento/Framework/Validator/Factory.php @@ -6,22 +6,33 @@ namespace Magento\Framework\Validator; +use Magento\Framework\Module\Dir\Reader; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Phrase; +use Magento\Framework\Validator; use Magento\Framework\Cache\FrontendInterface; +/** + * Factory for \Magento\Framework\Validator and \Magento\Framework\Validator\Builder. + */ class Factory { - /** cache key */ + /** + * cache key + * + * @deprecated + */ const CACHE_KEY = __CLASS__; /** - * @var \Magento\Framework\ObjectManagerInterface + * @var ObjectManagerInterface */ protected $_objectManager; /** * Validator config files * - * @var array|null + * @var iterable|null */ protected $_configFiles = null; @@ -31,40 +42,25 @@ class Factory private $isDefaultTranslatorInitialized = false; /** - * @var \Magento\Framework\Module\Dir\Reader + * @var Reader */ private $moduleReader; - /** - * @var FrontendInterface - */ - private $cache; - - /** - * @var \Magento\Framework\Serialize\SerializerInterface - */ - private $serializer; - - /** - * @var \Magento\Framework\Config\FileIteratorFactory - */ - private $fileIteratorFactory; - /** * Initialize dependencies * - * @param \Magento\Framework\ObjectManagerInterface $objectManager - * @param \Magento\Framework\Module\Dir\Reader $moduleReader - * @param FrontendInterface $cache + * @param ObjectManagerInterface $objectManager + * @param Reader $moduleReader + * @param FrontendInterface $cache @deprecated + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( - \Magento\Framework\ObjectManagerInterface $objectManager, - \Magento\Framework\Module\Dir\Reader $moduleReader, + ObjectManagerInterface $objectManager, + Reader $moduleReader, FrontendInterface $cache ) { $this->_objectManager = $objectManager; $this->moduleReader = $moduleReader; - $this->cache = $cache; } /** @@ -75,17 +71,7 @@ public function __construct( protected function _initializeConfigList() { if (!$this->_configFiles) { - $this->_configFiles = $this->cache->load(self::CACHE_KEY); - if (!$this->_configFiles) { - $this->_configFiles = $this->moduleReader->getConfigurationFiles('validation.xml'); - $this->cache->save( - $this->getSerializer()->serialize($this->_configFiles->toArray()), - self::CACHE_KEY - ); - } else { - $filesArray = $this->getSerializer()->unserialize($this->_configFiles); - $this->_configFiles = $this->getFileIteratorFactory()->create(array_keys($filesArray)); - } + $this->_configFiles = $this->moduleReader->getConfigurationFiles('validation.xml'); } } @@ -93,6 +79,7 @@ protected function _initializeConfigList() * Create and set default translator to \Magento\Framework\Validator\AbstractValidator. * * @return void + * @throws \Zend_Translate_Exception */ protected function _initializeDefaultTranslator() { @@ -100,7 +87,7 @@ protected function _initializeDefaultTranslator() // Pass translations to \Magento\Framework\TranslateInterface from validators $translatorCallback = function () { $argc = func_get_args(); - return (string)new \Magento\Framework\Phrase(array_shift($argc), $argc); + return (string)new Phrase(array_shift($argc), $argc); }; /** @var \Magento\Framework\Translate\Adapter $translator */ $translator = $this->_objectManager->create(\Magento\Framework\Translate\Adapter::class); @@ -115,14 +102,15 @@ protected function _initializeDefaultTranslator() * * Will instantiate \Magento\Framework\Validator\Config * - * @return \Magento\Framework\Validator\Config + * @return Config + * @throws \Zend_Translate_Exception */ public function getValidatorConfig() { $this->_initializeConfigList(); $this->_initializeDefaultTranslator(); return $this->_objectManager->create( - \Magento\Framework\Validator\Config::class, + Config::class, ['configFiles' => $this->_configFiles] ); } @@ -133,7 +121,8 @@ public function getValidatorConfig() * @param string $entityName * @param string $groupName * @param array|null $builderConfig - * @return \Magento\Framework\Validator\Builder + * @return Builder + * @throws \Zend_Translate_Exception */ public function createValidatorBuilder($entityName, $groupName, array $builderConfig = null) { @@ -147,43 +136,12 @@ public function createValidatorBuilder($entityName, $groupName, array $builderCo * @param string $entityName * @param string $groupName * @param array|null $builderConfig - * @return \Magento\Framework\Validator + * @return Validator + * @throws \Zend_Translate_Exception */ public function createValidator($entityName, $groupName, array $builderConfig = null) { $this->_initializeDefaultTranslator(); return $this->getValidatorConfig()->createValidator($entityName, $groupName, $builderConfig); } - - /** - * Get serializer - * - * @return \Magento\Framework\Serialize\SerializerInterface - * @deprecated 100.2.0 - */ - private function getSerializer() - { - if ($this->serializer === null) { - $this->serializer = $this->_objectManager->get( - \Magento\Framework\Serialize\SerializerInterface::class - ); - } - return $this->serializer; - } - - /** - * Get file iterator factory - * - * @return \Magento\Framework\Config\FileIteratorFactory - * @deprecated 100.2.0 - */ - private function getFileIteratorFactory() - { - if ($this->fileIteratorFactory === null) { - $this->fileIteratorFactory = $this->_objectManager->get( - \Magento\Framework\Config\FileIteratorFactory::class - ); - } - return $this->fileIteratorFactory; - } } diff --git a/lib/internal/Magento/Framework/Validator/Test/Unit/FactoryTest.php b/lib/internal/Magento/Framework/Validator/Test/Unit/FactoryTest.php index 5511627c6dcc..73a8c95c9a2f 100644 --- a/lib/internal/Magento/Framework/Validator/Test/Unit/FactoryTest.php +++ b/lib/internal/Magento/Framework/Validator/Test/Unit/FactoryTest.php @@ -25,21 +25,6 @@ class FactoryTest extends \PHPUnit\Framework\TestCase */ private $validatorConfigMock; - /** - * @var \Magento\Framework\Cache\FrontendInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $cacheMock; - - /** - * @var \Magento\Framework\Serialize\SerializerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $serializerMock; - - /** - * @var \Magento\Framework\Config\FileIteratorFactory|\PHPUnit_Framework_MockObject_MockObject - */ - private $fileIteratorFactoryMock; - /** * @var \Magento\Framework\Config\FileIterator|\PHPUnit_Framework_MockObject_MockObject */ @@ -55,11 +40,6 @@ class FactoryTest extends \PHPUnit\Framework\TestCase */ private $factory; - /** - * @var string - */ - private $jsonString = '["\/tmp\/moduleOne\/etc\/validation.xml"]'; - /** * @var array */ @@ -99,23 +79,9 @@ protected function setUp() \Magento\Framework\Validator\Factory::class, [ 'objectManager' => $this->objectManagerMock, - 'moduleReader' => $this->readerMock, - 'cache' => $this->cacheMock + 'moduleReader' => $this->readerMock ] ); - - $this->serializerMock = $this->createMock(\Magento\Framework\Serialize\SerializerInterface::class); - $this->fileIteratorFactoryMock = $this->createMock(\Magento\Framework\Config\FileIteratorFactory::class); - $objectManager->setBackwardCompatibleProperty( - $this->factory, - 'serializer', - $this->serializerMock - ); - $objectManager->setBackwardCompatibleProperty( - $this->factory, - 'fileIteratorFactory', - $this->fileIteratorFactoryMock - ); } /** @@ -147,46 +113,6 @@ public function testGetValidatorConfig() ); } - public function testGetValidatorConfigCacheNotExist() - { - $this->cacheMock->expects($this->once()) - ->method('load') - ->willReturn(false); - $this->readerMock->expects($this->once()) - ->method('getConfigurationFiles') - ->willReturn($this->fileIteratorMock); - $this->fileIteratorMock->method('toArray') - ->willReturn($this->data); - $this->cacheMock->expects($this->once()) - ->method('save') - ->with($this->jsonString); - $this->serializerMock->expects($this->once()) - ->method('serialize') - ->with($this->data) - ->willReturn($this->jsonString); - $this->factory->getValidatorConfig(); - $this->factory->getValidatorConfig(); - } - - public function testGetValidatorConfigCacheExist() - { - $this->cacheMock->expects($this->once()) - ->method('load') - ->willReturn($this->jsonString); - $this->readerMock->expects($this->never()) - ->method('getConfigurationFiles'); - $this->cacheMock->expects($this->never()) - ->method('save'); - $this->serializerMock->expects($this->once()) - ->method('unserialize') - ->with($this->jsonString) - ->willReturn($this->data); - $this->fileIteratorFactoryMock->method('create') - ->willReturn($this->fileIteratorMock); - $this->factory->getValidatorConfig(); - $this->factory->getValidatorConfig(); - } - public function testCreateValidatorBuilder() { $this->readerMock->method('getConfigurationFiles') diff --git a/lib/internal/Magento/Framework/View/Asset/Config.php b/lib/internal/Magento/Framework/View/Asset/Config.php index 2a3d0d3dad46..6a0548b3395a 100644 --- a/lib/internal/Magento/Framework/View/Asset/Config.php +++ b/lib/internal/Magento/Framework/View/Asset/Config.php @@ -8,7 +8,6 @@ use Magento\Store\Model\ScopeInterface; use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\Framework\App\State; /** * View asset configuration interface @@ -55,7 +54,7 @@ public function __construct(ScopeConfigInterface $scopeConfig) */ public function isMergeCssFiles() { - return (bool)$this->scopeConfig->isSetFlag( + return $this->scopeConfig->isSetFlag( self::XML_PATH_MERGE_CSS_FILES, ScopeInterface::SCOPE_STORE ); @@ -68,7 +67,7 @@ public function isMergeCssFiles() */ public function isBundlingJsFiles() { - return (bool)$this->scopeConfig->isSetFlag( + return $this->scopeConfig->isSetFlag( self::XML_PATH_JS_BUNDLING, ScopeInterface::SCOPE_STORE ); @@ -81,7 +80,7 @@ public function isBundlingJsFiles() */ public function isMergeJsFiles() { - return (bool)$this->scopeConfig->isSetFlag( + return $this->scopeConfig->isSetFlag( self::XML_PATH_MERGE_JS_FILES, ScopeInterface::SCOPE_STORE ); @@ -94,7 +93,7 @@ public function isMergeJsFiles() */ public function isMinifyHtml() { - return (bool)$this->scopeConfig->isSetFlag( + return $this->scopeConfig->isSetFlag( self::XML_PATH_MINIFICATION_HTML, ScopeInterface::SCOPE_STORE ); diff --git a/lib/internal/Magento/Framework/View/Asset/MergeStrategy/Direct.php b/lib/internal/Magento/Framework/View/Asset/MergeStrategy/Direct.php index 1e8eb74bf541..315c3ee20486 100644 --- a/lib/internal/Magento/Framework/View/Asset/MergeStrategy/Direct.php +++ b/lib/internal/Magento/Framework/View/Asset/MergeStrategy/Direct.php @@ -6,6 +6,8 @@ namespace Magento\Framework\View\Asset\MergeStrategy; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Math\Random; use Magento\Framework\View\Asset; /** @@ -30,38 +32,46 @@ class Direct implements \Magento\Framework\View\Asset\MergeStrategyInterface */ private $cssUrlResolver; + /** + * @var Random + */ + private $mathRandom; + /** * @param \Magento\Framework\Filesystem $filesystem * @param \Magento\Framework\View\Url\CssResolver $cssUrlResolver + * @param Random|null $mathRandom */ public function __construct( \Magento\Framework\Filesystem $filesystem, - \Magento\Framework\View\Url\CssResolver $cssUrlResolver + \Magento\Framework\View\Url\CssResolver $cssUrlResolver, + Random $mathRandom = null ) { $this->filesystem = $filesystem; $this->cssUrlResolver = $cssUrlResolver; + $this->mathRandom = $mathRandom ?: ObjectManager::getInstance()->get(Random::class); } /** - * {@inheritdoc} + * @inheritdoc */ public function merge(array $assetsToMerge, Asset\LocalInterface $resultAsset) { $mergedContent = $this->composeMergedContent($assetsToMerge, $resultAsset); $filePath = $resultAsset->getPath(); + $tmpFilePath = $filePath . $this->mathRandom->getUniqueHash('_'); $staticDir = $this->filesystem->getDirectoryWrite(DirectoryList::STATIC_VIEW); $tmpDir = $this->filesystem->getDirectoryWrite(DirectoryList::TMP); - $tmpDir->writeFile($filePath, $mergedContent); - $tmpDir->renameFile($filePath, $filePath, $staticDir); + $tmpDir->writeFile($tmpFilePath, $mergedContent); + $tmpDir->renameFile($tmpFilePath, $filePath, $staticDir); } /** * Merge files together and modify content if needed * - * @param \Magento\Framework\View\Asset\MergeableInterface[] $assetsToMerge - * @param \Magento\Framework\View\Asset\LocalInterface $resultAsset - * @return string - * @throws \Magento\Framework\Exception\LocalizedException + * @param array $assetsToMerge + * @param Asset\LocalInterface $resultAsset + * @return array|string */ private function composeMergedContent(array $assetsToMerge, Asset\LocalInterface $resultAsset) { diff --git a/lib/internal/Magento/Framework/View/Asset/Minification.php b/lib/internal/Magento/Framework/View/Asset/Minification.php index 596add349dbf..087d57ffa416 100644 --- a/lib/internal/Magento/Framework/View/Asset/Minification.php +++ b/lib/internal/Magento/Framework/View/Asset/Minification.php @@ -64,7 +64,7 @@ public function isEnabled($contentType) if (!isset($this->configCache[self::XML_PATH_MINIFICATION_ENABLED][$contentType])) { $this->configCache[self::XML_PATH_MINIFICATION_ENABLED][$contentType] = $this->appState->getMode() != State::MODE_DEVELOPER && - (bool)$this->scopeConfig->isSetFlag( + $this->scopeConfig->isSetFlag( sprintf(self::XML_PATH_MINIFICATION_ENABLED, $contentType), $this->scope ); @@ -112,6 +112,8 @@ public function removeMinifiedSign($filename) } /** + * Is Minified Filename + * * @param string $filename * @return bool */ @@ -121,6 +123,8 @@ public function isMinifiedFilename($filename) } /** + * Is Excluded + * * @param string $filename * @return boolean */ @@ -135,6 +139,8 @@ public function isExcluded($filename) } /** + * Get Excludes + * * @param string $contentType * @return string[] */ diff --git a/lib/internal/Magento/Framework/View/Asset/Repository.php b/lib/internal/Magento/Framework/View/Asset/Repository.php index 654d80382f4b..19c9ddd1e918 100644 --- a/lib/internal/Magento/Framework/View/Asset/Repository.php +++ b/lib/internal/Magento/Framework/View/Asset/Repository.php @@ -126,6 +126,7 @@ public function __construct( * @return $this * * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) */ public function updateDesignParams(array &$params) { @@ -146,7 +147,12 @@ public function updateDesignParams(array &$params) } if ($theme) { - $params['themeModel'] = $this->getThemeProvider()->getThemeByFullPath($area . '/' . $theme); + if (is_numeric($theme)) { + $params['themeModel'] = $this->getThemeProvider()->getThemeById($theme); + } else { + $params['themeModel'] = $this->getThemeProvider()->getThemeByFullPath($area . '/' . $theme); + } + if (!$params['themeModel']) { throw new \UnexpectedValueException("Could not find theme '$theme' for area '$area'"); } @@ -167,6 +173,8 @@ public function updateDesignParams(array &$params) } /** + * Get theme provider + * * @return ThemeProviderInterface */ private function getThemeProvider() @@ -440,6 +448,8 @@ public static function extractModule($fileId) } /** + * Get repository files map + * * @param string $fileId * @param array $params * @return RepositoryMap diff --git a/lib/internal/Magento/Framework/View/Context.php b/lib/internal/Magento/Framework/View/Context.php index c3f1c3e691c8..508d63d158bd 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/Design/Theme/ThemeList.php b/lib/internal/Magento/Framework/View/Design/Theme/ThemeList.php index 000fba24f082..0d6ed2f98b52 100644 --- a/lib/internal/Magento/Framework/View/Design/Theme/ThemeList.php +++ b/lib/internal/Magento/Framework/View/Design/Theme/ThemeList.php @@ -40,7 +40,7 @@ class ThemeList extends \Magento\Framework\Data\Collection implements ListInterf protected $_itemObjectClass = ThemeInterface::class; /** - * @var \Magento\Framework\Config\ThemeFactory $themeConfigFactory + * @var \Magento\Framework\Config\ThemeFactory */ protected $themeConfigFactory; diff --git a/lib/internal/Magento/Framework/View/Element/AbstractBlock.php b/lib/internal/Magento/Framework/View/Element/AbstractBlock.php index b2f857bf29f4..6c4746d8218e 100644 --- a/lib/internal/Magento/Framework/View/Element/AbstractBlock.php +++ b/lib/internal/Magento/Framework/View/Element/AbstractBlock.php @@ -3,9 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Framework\View\Element; +use Magento\Framework\Cache\LockGuardedCacheLoader; use Magento\Framework\DataObject\IdentityInterface; +use Magento\Framework\App\ObjectManager; /** * Base class for all blocks. @@ -175,14 +178,23 @@ abstract class AbstractBlock extends \Magento\Framework\DataObject implements Bl */ protected $_cache; + /** + * @var LockGuardedCacheLoader + */ + private $lockQuery; + /** * Constructor * * @param \Magento\Framework\View\Element\Context $context * @param array $data + * @param LockGuardedCacheLoader|null $lockQuery */ - public function __construct(\Magento\Framework\View\Element\Context $context, array $data = []) - { + public function __construct( + \Magento\Framework\View\Element\Context $context, + array $data = [], + LockGuardedCacheLoader $lockQuery = null + ) { $this->_request = $context->getRequest(); $this->_layout = $context->getLayout(); $this->_eventManager = $context->getEventManager(); @@ -204,6 +216,8 @@ public function __construct(\Magento\Framework\View\Element\Context $context, ar $this->jsLayout = $data['jsLayout']; unset($data['jsLayout']); } + $this->lockQuery = $lockQuery + ?: ObjectManager::getInstance()->get(LockGuardedCacheLoader::class); parent::__construct($data); $this->_construct(); } @@ -658,19 +672,6 @@ public function toHtml() } $html = $this->_loadCache(); - if ($html === false) { - if ($this->hasData('translate_inline')) { - $this->inlineTranslation->suspend($this->getData('translate_inline')); - } - - $this->_beforeToHtml(); - $html = $this->_toHtml(); - $this->_saveCache($html); - - if ($this->hasData('translate_inline')) { - $this->inlineTranslation->resume(); - } - } $html = $this->_afterToHtml($html); /** @var \Magento\Framework\DataObject */ @@ -952,7 +953,7 @@ public function stripTags($data, $allowableTags = null, $allowHtmlEntities = fal */ public function escapeUrl($string) { - return $this->_escaper->escapeUrl($string); + return $this->_escaper->escapeUrl((string)$string); } /** @@ -1083,23 +1084,54 @@ protected function getCacheLifetime() /** * Load block html from cache storage * - * @return string|false + * @return string */ protected function _loadCache() { + $collectAction = function () { + if ($this->hasData('translate_inline')) { + $this->inlineTranslation->suspend($this->getData('translate_inline')); + } + + $this->_beforeToHtml(); + return $this->_toHtml(); + }; + if ($this->getCacheLifetime() === null || !$this->_cacheState->isEnabled(self::CACHE_GROUP)) { - return false; - } - $cacheKey = $this->getCacheKey(); - $cacheData = $this->_cache->load($cacheKey); - if ($cacheData) { - $cacheData = str_replace( - $this->_getSidPlaceholder($cacheKey), - $this->_sidResolver->getSessionIdQueryParam($this->_session) . '=' . $this->_session->getSessionId(), - $cacheData - ); + $html = $collectAction(); + if ($this->hasData('translate_inline')) { + $this->inlineTranslation->resume(); + } + return $html; } - return $cacheData; + $loadAction = function () { + $cacheKey = $this->getCacheKey(); + $cacheData = $this->_cache->load($cacheKey); + if ($cacheData) { + $cacheData = str_replace( + $this->_getSidPlaceholder($cacheKey), + $this->_sidResolver->getSessionIdQueryParam($this->_session) + . '=' + . $this->_session->getSessionId(), + $cacheData + ); + } + return $cacheData; + }; + + $saveAction = function ($data) { + $this->_saveCache($data); + if ($this->hasData('translate_inline')) { + $this->inlineTranslation->resume(); + } + }; + + return (string)$this->lockQuery->lockedLoadData( + $this->getCacheKey(), + $loadAction, + $collectAction, + $saveAction + ); } /** @@ -1156,6 +1188,7 @@ public function getVar($name, $module = null) /** * Determine if the block scope is private or public. + * * Returns true if scope is private, false otherwise * * @return bool diff --git a/lib/internal/Magento/Framework/View/Element/ComponentVisibilityInterface.php b/lib/internal/Magento/Framework/View/Element/ComponentVisibilityInterface.php new file mode 100644 index 000000000000..29d14553481d --- /dev/null +++ b/lib/internal/Magento/Framework/View/Element/ComponentVisibilityInterface.php @@ -0,0 +1,20 @@ + $this->_request->getModuleName(), - 'controller' => $this->_request->getControllerName(), - 'action' => $this->_request->getActionName(), + (string)$this->_request->getModuleName(), + (string)$this->_request->getControllerName(), + (string)$this->_request->getActionName(), ]; $parts = []; + $pathParts = explode('/', trim($this->_request->getPathInfo(), '/')); foreach ($routeParts as $key => $value) { - if (!empty($value) && $value != $this->_defaultPath->getPart($key)) { + if (isset($pathParts[$key]) && $pathParts[$key] === $value) { $parts[] = $value; } } @@ -104,13 +110,13 @@ protected function _toHtml() if ($this->isCurrent()) { $html = ''; } else { $html = '
  • \n {{heading}}\n
  • \n')}]),angular.module("template/tabs/tabset-titles.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tabset-titles.html","
      \n
    \n")}]),angular.module("template/tabs/tabset.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tabset.html",'\n
    \n \n
    \n
    \n
    \n
    \n
    \n')}]),angular.module("template/timepicker/timepicker.html",[]).run(["$templateCache",function(a){a.put("template/timepicker/timepicker.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
     
    \n \n :\n \n
     
    \n')}]),angular.module("template/typeahead/typeahead-match.html",[]).run(["$templateCache",function(a){a.put("template/typeahead/typeahead-match.html",'')}]),angular.module("template/typeahead/typeahead-popup.html",[]).run(["$templateCache",function(a){a.put("template/typeahead/typeahead-popup.html",'') + return a.replace(b,function(a,b){return(b?c:"")+a.toLowerCase()})}var b={placement:"top",animation:!0,popupDelay:0},c={mouseenter:"mouseleave",click:"click",focus:"blur"},d={};this.options=function(a){angular.extend(d,a)},this.setTriggers=function(a){angular.extend(c,a)},this.$get=["$window","$compile","$timeout","$parse","$document","$position","$interpolate",function(e,f,g,h,i,j,k){return function(e,l,m){function n(a){var b=a||o.trigger||m,d=c[b]||b;return{show:b,hide:d}}var o=angular.extend({},b,d),p=a(e),q=k.startSymbol(),r=k.endSymbol(),s="
    ';return{restrict:"EA",scope:!0,compile:function(){var a=f(s);return function(b,c,d){function f(){b.tt_isOpen?m():k()}function k(){(!y||b.$eval(d[l+"Enable"]))&&(b.tt_popupDelay?v||(v=g(p,b.tt_popupDelay,!1),v.then(function(a){a()})):p()())}function m(){b.$apply(function(){q()})}function p(){return v=null,u&&(g.cancel(u),u=null),b.tt_content?(r(),t.css({top:0,left:0,display:"block"}),w?i.find("body").append(t):c.after(t),z(),b.tt_isOpen=!0,b.$digest(),z):angular.noop}function q(){b.tt_isOpen=!1,g.cancel(v),v=null,b.tt_animation?u||(u=g(s,500)):s()}function r(){t&&s(),t=a(b,function(){}),b.$digest()}function s(){u=null,t&&(t.remove(),t=null)}var t,u,v,w=angular.isDefined(o.appendToBody)?o.appendToBody:!1,x=n(void 0),y=angular.isDefined(d[l+"Enable"]),z=function(){var a=j.positionElements(c,t,b.tt_placement,w);a.top+="px",a.left+="px",t.css(a)};b.tt_isOpen=!1,d.$observe(e,function(a){b.tt_content=a,!a&&b.tt_isOpen&&q()}),d.$observe(l+"Title",function(a){b.tt_title=a}),d.$observe(l+"Placement",function(a){b.tt_placement=angular.isDefined(a)?a:o.placement}),d.$observe(l+"PopupDelay",function(a){var c=parseInt(a,10);b.tt_popupDelay=isNaN(c)?o.popupDelay:c});var A=function(){c.unbind(x.show,k),c.unbind(x.hide,m)};d.$observe(l+"Trigger",function(a){A(),x=n(a),x.show===x.hide?c.bind(x.show,f):(c.bind(x.show,k),c.bind(x.hide,m))});var B=b.$eval(d[l+"Animation"]);b.tt_animation=angular.isDefined(B)?!!B:o.animation,d.$observe(l+"AppendToBody",function(a){w=angular.isDefined(a)?h(a)(b):w}),w&&b.$on("$locationChangeSuccess",function(){b.tt_isOpen&&q()}),b.$on("$destroy",function(){g.cancel(u),g.cancel(v),A(),s()})}}}}}]}).directive("tooltipPopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-popup.html"}}).directive("tooltip",["$tooltip",function(a){return a("tooltip","tooltip","mouseenter")}]).directive("tooltipHtmlUnsafePopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-html-unsafe-popup.html"}}).directive("tooltipHtmlUnsafe",["$tooltip",function(a){return a("tooltipHtmlUnsafe","tooltip","mouseenter")}]),angular.module("ui.bootstrap.popover",["ui.bootstrap.tooltip"]).directive("popoverPopup",function(){return{restrict:"EA",replace:!0,scope:{title:"@",content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/popover/popover.html"}}).directive("popover",["$tooltip",function(a){return a("popover","popover","click")}]),angular.module("ui.bootstrap.progressbar",[]).constant("progressConfig",{animate:!0,max:100}).controller("ProgressController",["$scope","$attrs","progressConfig",function(a,b,c){var d=this,e=angular.isDefined(b.animate)?a.$parent.$eval(b.animate):c.animate;this.bars=[],a.max=angular.isDefined(b.max)?a.$parent.$eval(b.max):c.max,this.addBar=function(b,c){e||c.css({transition:"none"}),this.bars.push(b),b.$watch("value",function(c){b.percent=+(100*c/a.max).toFixed(2)}),b.$on("$destroy",function(){c=null,d.removeBar(b)})},this.removeBar=function(a){this.bars.splice(this.bars.indexOf(a),1)}}]).directive("progress",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",require:"progress",scope:{},templateUrl:"template/progressbar/progress.html"}}).directive("bar",function(){return{restrict:"EA",replace:!0,transclude:!0,require:"^progress",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/bar.html",link:function(a,b,c,d){d.addBar(a,b)}}}).directive("progressbar",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/progressbar.html",link:function(a,b,c,d){d.addBar(a,angular.element(b.children()[0]))}}}),angular.module("ui.bootstrap.rating",[]).constant("ratingConfig",{max:5,stateOn:null,stateOff:null}).controller("RatingController",["$scope","$attrs","ratingConfig",function(a,b,c){var d={$setViewValue:angular.noop};this.init=function(e){d=e,d.$render=this.render,this.stateOn=angular.isDefined(b.stateOn)?a.$parent.$eval(b.stateOn):c.stateOn,this.stateOff=angular.isDefined(b.stateOff)?a.$parent.$eval(b.stateOff):c.stateOff;var f=angular.isDefined(b.ratingStates)?a.$parent.$eval(b.ratingStates):new Array(angular.isDefined(b.max)?a.$parent.$eval(b.max):c.max);a.range=this.buildTemplateObjects(f)},this.buildTemplateObjects=function(a){for(var b=0,c=a.length;c>b;b++)a[b]=angular.extend({index:b},{stateOn:this.stateOn,stateOff:this.stateOff},a[b]);return a},a.rate=function(b){!a.readonly&&b>=0&&b<=a.range.length&&(d.$setViewValue(b),d.$render())},a.enter=function(b){a.readonly||(a.value=b),a.onHover({value:b})},a.reset=function(){a.value=d.$viewValue,a.onLeave()},a.onKeydown=function(b){/(37|38|39|40)/.test(b.which)&&(b.preventDefault(),b.stopPropagation(),a.rate(a.value+(38===b.which||39===b.which?1:-1)))},this.render=function(){a.value=d.$viewValue}}]).directive("rating",function(){return{restrict:"EA",require:["rating","ngModel"],scope:{readonly:"=?",onHover:"&",onLeave:"&"},controller:"RatingController",templateUrl:"template/rating/rating.html",replace:!0,link:function(a,b,c,d){var e=d[0],f=d[1];f&&e.init(f)}}}),angular.module("ui.bootstrap.tabs",[]).controller("TabsetController",["$scope",function(a){var b=this,c=b.tabs=a.tabs=[];b.select=function(a){angular.forEach(c,function(b){b.active&&b!==a&&(b.active=!1,b.onDeselect())}),a.active=!0,a.onSelect()},b.addTab=function(a){c.push(a),1===c.length?a.active=!0:a.active&&b.select(a)},b.removeTab=function(a){var d=c.indexOf(a);if(a.active&&c.length>1){var e=d==c.length-1?d-1:d+1;b.select(c[e])}c.splice(d,1)}}]).directive("tabset",function(){return{restrict:"EA",transclude:!0,replace:!0,scope:{type:"@"},controller:"TabsetController",templateUrl:"template/tabs/tabset.html",link:function(a,b,c){a.vertical=angular.isDefined(c.vertical)?a.$parent.$eval(c.vertical):!1,a.justified=angular.isDefined(c.justified)?a.$parent.$eval(c.justified):!1}}}).directive("tab",["$parse",function(a){return{require:"^tabset",restrict:"EA",replace:!0,templateUrl:"template/tabs/tab.html",transclude:!0,scope:{active:"=?",heading:"@",onSelect:"&select",onDeselect:"&deselect"},controller:function(){},compile:function(b,c,d){return function(b,c,e,f){b.$watch("active",function(a){a&&f.select(b)}),b.disabled=!1,e.disabled&&b.$parent.$watch(a(e.disabled),function(a){b.disabled=!!a}),b.select=function(){b.disabled||(b.active=!0)},f.addTab(b),b.$on("$destroy",function(){f.removeTab(b)}),b.$transcludeFn=d}}}}]).directive("tabHeadingTransclude",[function(){return{restrict:"A",require:"^tab",link:function(a,b){a.$watch("headingElement",function(a){a&&(b.html(""),b.append(a))})}}}]).directive("tabContentTransclude",function(){function a(a){return a.tagName&&(a.hasAttribute("tab-heading")||a.hasAttribute("data-tab-heading")||"tab-heading"===a.tagName.toLowerCase()||"data-tab-heading"===a.tagName.toLowerCase())}return{restrict:"A",require:"^tabset",link:function(b,c,d){var e=b.$eval(d.tabContentTransclude);e.$transcludeFn(e.$parent,function(b){angular.forEach(b,function(b){a(b)?e.headingElement=b:c.append(b)})})}}}),angular.module("ui.bootstrap.timepicker",[]).constant("timepickerConfig",{hourStep:1,minuteStep:1,showMeridian:!0,meridians:null,readonlyInput:!1,mousewheel:!0}).controller("TimepickerController",["$scope","$attrs","$parse","$log","$locale","timepickerConfig",function(a,b,c,d,e,f){function g(){var b=parseInt(a.hours,10),c=a.showMeridian?b>0&&13>b:b>=0&&24>b;return c?(a.showMeridian&&(12===b&&(b=0),a.meridian===p[1]&&(b+=12)),b):void 0}function h(){var b=parseInt(a.minutes,10);return b>=0&&60>b?b:void 0}function i(a){return angular.isDefined(a)&&a.toString().length<2?"0"+a:a}function j(a){k(),o.$setViewValue(new Date(n)),l(a)}function k(){o.$setValidity("time",!0),a.invalidHours=!1,a.invalidMinutes=!1}function l(b){var c=n.getHours(),d=n.getMinutes();a.showMeridian&&(c=0===c||12===c?12:c%12),a.hours="h"===b?c:i(c),a.minutes="m"===b?d:i(d),a.meridian=n.getHours()<12?p[0]:p[1]}function m(a){var b=new Date(n.getTime()+6e4*a);n.setHours(b.getHours(),b.getMinutes()),j()}var n=new Date,o={$setViewValue:angular.noop},p=angular.isDefined(b.meridians)?a.$parent.$eval(b.meridians):f.meridians||e.DATETIME_FORMATS.AMPMS;this.init=function(c,d){o=c,o.$render=this.render;var e=d.eq(0),g=d.eq(1),h=angular.isDefined(b.mousewheel)?a.$parent.$eval(b.mousewheel):f.mousewheel;h&&this.setupMousewheelEvents(e,g),a.readonlyInput=angular.isDefined(b.readonlyInput)?a.$parent.$eval(b.readonlyInput):f.readonlyInput,this.setupInputEvents(e,g)};var q=f.hourStep;b.hourStep&&a.$parent.$watch(c(b.hourStep),function(a){q=parseInt(a,10)});var r=f.minuteStep;b.minuteStep&&a.$parent.$watch(c(b.minuteStep),function(a){r=parseInt(a,10)}),a.showMeridian=f.showMeridian,b.showMeridian&&a.$parent.$watch(c(b.showMeridian),function(b){if(a.showMeridian=!!b,o.$error.time){var c=g(),d=h();angular.isDefined(c)&&angular.isDefined(d)&&(n.setHours(c),j())}else l()}),this.setupMousewheelEvents=function(b,c){var d=function(a){a.originalEvent&&(a=a.originalEvent);var b=a.wheelDelta?a.wheelDelta:-a.deltaY;return a.detail||b>0};b.bind("mousewheel wheel",function(b){a.$apply(d(b)?a.incrementHours():a.decrementHours()),b.preventDefault()}),c.bind("mousewheel wheel",function(b){a.$apply(d(b)?a.incrementMinutes():a.decrementMinutes()),b.preventDefault()})},this.setupInputEvents=function(b,c){if(a.readonlyInput)return a.updateHours=angular.noop,void(a.updateMinutes=angular.noop);var d=function(b,c){o.$setViewValue(null),o.$setValidity("time",!1),angular.isDefined(b)&&(a.invalidHours=b),angular.isDefined(c)&&(a.invalidMinutes=c)};a.updateHours=function(){var a=g();angular.isDefined(a)?(n.setHours(a),j("h")):d(!0)},b.bind("blur",function(){!a.invalidHours&&a.hours<10&&a.$apply(function(){a.hours=i(a.hours)})}),a.updateMinutes=function(){var a=h();angular.isDefined(a)?(n.setMinutes(a),j("m")):d(void 0,!0)},c.bind("blur",function(){!a.invalidMinutes&&a.minutes<10&&a.$apply(function(){a.minutes=i(a.minutes)})})},this.render=function(){var a=o.$modelValue?new Date(o.$modelValue):null;isNaN(a)?(o.$setValidity("time",!1),d.error('Timepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.')):(a&&(n=a),k(),l())},a.incrementHours=function(){m(60*q)},a.decrementHours=function(){m(60*-q)},a.incrementMinutes=function(){m(r)},a.decrementMinutes=function(){m(-r)},a.toggleMeridian=function(){m(720*(n.getHours()<12?1:-1))}}]).directive("timepicker",function(){return{restrict:"EA",require:["timepicker","?^ngModel"],controller:"TimepickerController",replace:!0,scope:{},templateUrl:"template/timepicker/timepicker.html",link:function(a,b,c,d){var e=d[0],f=d[1];f&&e.init(f,b.find("input"))}}}),angular.module("ui.bootstrap.typeahead",["ui.bootstrap.position","ui.bootstrap.bindHtml"]).factory("typeaheadParser",["$parse",function(a){var b=/^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/;return{parse:function(c){var d=c.match(b);if(!d)throw new Error('Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_" but got "'+c+'".');return{itemName:d[3],source:a(d[4]),viewMapper:a(d[2]||d[1]),modelMapper:a(d[1])}}}}]).directive("typeahead",["$compile","$parse","$q","$timeout","$document","$position","typeaheadParser",function(a,b,c,d,e,f,g){var h=[9,13,27,38,40];return{require:"ngModel",link:function(i,j,k,l){var m,n=i.$eval(k.typeaheadMinLength)||1,o=i.$eval(k.typeaheadWaitMs)||0,p=i.$eval(k.typeaheadEditable)!==!1,q=b(k.typeaheadLoading).assign||angular.noop,r=b(k.typeaheadOnSelect),s=k.typeaheadInputFormatter?b(k.typeaheadInputFormatter):void 0,t=k.typeaheadAppendToBody?i.$eval(k.typeaheadAppendToBody):!1,u=b(k.ngModel).assign,v=g.parse(k.typeahead),w=i.$new();i.$on("$destroy",function(){w.$destroy()});var x="typeahead-"+w.$id+"-"+Math.floor(1e4*Math.random());j.attr({"aria-autocomplete":"list","aria-expanded":!1,"aria-owns":x});var y=angular.element("
    ");y.attr({id:x,matches:"matches",active:"activeIdx",select:"select(activeIdx)",query:"query",position:"position"}),angular.isDefined(k.typeaheadTemplateUrl)&&y.attr("template-url",k.typeaheadTemplateUrl);var z=function(){w.matches=[],w.activeIdx=-1,j.attr("aria-expanded",!1)},A=function(a){return x+"-option-"+a};w.$watch("activeIdx",function(a){0>a?j.removeAttr("aria-activedescendant"):j.attr("aria-activedescendant",A(a))});var B=function(a){var b={$viewValue:a};q(i,!0),c.when(v.source(i,b)).then(function(c){var d=a===l.$viewValue;if(d&&m)if(c.length>0){w.activeIdx=0,w.matches.length=0;for(var e=0;e=n?o>0?(C&&d.cancel(C),C=d(function(){B(a)},o)):B(a):(q(i,!1),z()),p?a:a?void l.$setValidity("editable",!1):(l.$setValidity("editable",!0),a)}),l.$formatters.push(function(a){var b,c,d={};return s?(d.$model=a,s(i,d)):(d[v.itemName]=a,b=v.viewMapper(i,d),d[v.itemName]=void 0,c=v.viewMapper(i,d),b!==c?b:a)}),w.select=function(a){var b,c,e={};e[v.itemName]=c=w.matches[a].model,b=v.modelMapper(i,e),u(i,b),l.$setValidity("editable",!0),r(i,{$item:c,$model:b,$label:v.viewMapper(i,e)}),z(),d(function(){j[0].focus()},0,!1)},j.bind("keydown",function(a){0!==w.matches.length&&-1!==h.indexOf(a.which)&&(a.preventDefault(),40===a.which?(w.activeIdx=(w.activeIdx+1)%w.matches.length,w.$digest()):38===a.which?(w.activeIdx=(w.activeIdx?w.activeIdx:w.matches.length)-1,w.$digest()):13===a.which||9===a.which?w.$apply(function(){w.select(w.activeIdx)}):27===a.which&&(a.stopPropagation(),z(),w.$digest()))}),j.bind("blur",function(){m=!1});var D=function(a){j[0]!==a.target&&(z(),w.$digest())};e.bind("click",D),i.$on("$destroy",function(){e.unbind("click",D)});var E=a(y)(w);t?e.find("body").append(E):j.after(E)}}}]).directive("typeaheadPopup",function(){return{restrict:"EA",scope:{matches:"=",query:"=",active:"=",position:"=",select:"&"},replace:!0,templateUrl:"template/typeahead/typeahead-popup.html",link:function(a,b,c){a.templateUrl=c.templateUrl,a.isOpen=function(){return a.matches.length>0},a.isActive=function(b){return a.active==b},a.selectActive=function(b){a.active=b},a.selectMatch=function(b){a.select({activeIdx:b})}}}}).directive("typeaheadMatch",["$http","$templateCache","$compile","$parse",function(a,b,c,d){return{restrict:"EA",scope:{index:"=",match:"=",query:"="},link:function(e,f,g){var h=d(g.templateUrl)(e.$parent)||"template/typeahead/typeahead-match.html";a.get(h,{cache:b}).success(function(a){f.replaceWith(c(a.trim())(e))})}}}]).filter("typeaheadHighlight",function(){function a(a){return a.replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")}return function(b,c){return c?(""+b).replace(new RegExp(a(c),"gi"),"$&"):b}}),angular.module("template/accordion/accordion-group.html",[]).run(["$templateCache",function(a){a.put("template/accordion/accordion-group.html",'
    \n
    \n

    \n {{heading}}\n

    \n
    \n
    \n
    \n
    \n
    ')}]),angular.module("template/accordion/accordion.html",[]).run(["$templateCache",function(a){a.put("template/accordion/accordion.html",'
    ')}]),angular.module("template/alert/alert.html",[]).run(["$templateCache",function(a){a.put("template/alert/alert.html",'\n')}]),angular.module("template/carousel/carousel.html",[]).run(["$templateCache",function(a){a.put("template/carousel/carousel.html",'\n')}]),angular.module("template/carousel/slide.html",[]).run(["$templateCache",function(a){a.put("template/carousel/slide.html","
    \n")}]),angular.module("template/datepicker/datepicker.html",[]).run(["$templateCache",function(a){a.put("template/datepicker/datepicker.html",'
    \n \n \n \n
    ')}]),angular.module("template/datepicker/day.html",[]).run(["$templateCache",function(a){a.put("template/datepicker/day.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
    {{label.abbr}}
    {{ weekNumbers[$index] }}\n \n
    \n')}]),angular.module("template/datepicker/month.html",[]).run(["$templateCache",function(a){a.put("template/datepicker/month.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n')}]),angular.module("template/datepicker/popup.html",[]).run(["$templateCache",function(a){a.put("template/datepicker/popup.html",'\n')}]),angular.module("template/datepicker/year.html",[]).run(["$templateCache",function(a){a.put("template/datepicker/year.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n')}]),angular.module("template/modal/backdrop.html",[]).run(["$templateCache",function(a){a.put("template/modal/backdrop.html",'\n')}]),angular.module("template/modal/window.html",[]).run(["$templateCache",function(a){a.put("template/modal/window.html",'')}]),angular.module("template/pagination/pager.html",[]).run(["$templateCache",function(a){a.put("template/pagination/pager.html",'')}]),angular.module("template/pagination/pagination.html",[]).run(["$templateCache",function(a){a.put("template/pagination/pagination.html",'')}]),angular.module("template/tooltip/tooltip-html-unsafe-popup.html",[]).run(["$templateCache",function(a){a.put("template/tooltip/tooltip-html-unsafe-popup.html",'
    \n
    \n
    \n
    \n')}]),angular.module("template/tooltip/tooltip-popup.html",[]).run(["$templateCache",function(a){a.put("template/tooltip/tooltip-popup.html",'
    \n
    \n
    \n
    \n')}]),angular.module("template/popover/popover.html",[]).run(["$templateCache",function(a){a.put("template/popover/popover.html",'
    \n
    \n\n
    \n

    \n
    \n
    \n
    \n')}]),angular.module("template/progressbar/bar.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/bar.html",'
    ')}]),angular.module("template/progressbar/progress.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/progress.html",'
    ')}]),angular.module("template/progressbar/progressbar.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/progressbar.html",'
    \n
    \n
    ')}]),angular.module("template/rating/rating.html",[]).run(["$templateCache",function(a){a.put("template/rating/rating.html",'\n \n ({{ $index < value ? \'*\' : \' \' }})\n \n')}]),angular.module("template/tabs/tab.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tab.html",'
  • \n {{heading}}\n
  • \n')}]),angular.module("template/tabs/tabset-titles.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tabset-titles.html","
      \n
    \n")}]),angular.module("template/tabs/tabset.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tabset.html",'\n
    \n \n
    \n
    \n
    \n
    \n
    \n')}]),angular.module("template/timepicker/timepicker.html",[]).run(["$templateCache",function(a){a.put("template/timepicker/timepicker.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
     
    \n \n :\n \n
     
    \n')}]),angular.module("template/typeahead/typeahead-match.html",[]).run(["$templateCache",function(a){a.put("template/typeahead/typeahead-match.html",'')}]),angular.module("template/typeahead/typeahead-popup.html",[]).run(["$templateCache",function(a){a.put("template/typeahead/typeahead-popup.html",'') }]); \ No newline at end of file diff --git a/setup/pub/angular-ui-router/angular-ui-router.min.js b/setup/pub/angular-ui-router/angular-ui-router.min.js index f065ecc96068..66568f91192e 100644 --- a/setup/pub/angular-ui-router/angular-ui-router.min.js +++ b/setup/pub/angular-ui-router/angular-ui-router.min.js @@ -1,7 +1,7 @@ /** * State-based routing for AngularJS - * @version v0.2.10 + * @version v0.4.3 * @link http://angular-ui.github.com/ * @license MIT License, http://www.opensource.org/licenses/MIT */ -"undefined"!=typeof module&&"undefined"!=typeof exports&&module.exports===exports&&(module.exports="ui.router"),function(a,b,c){"use strict";function d(a,b){return I(new(I(function(){},{prototype:a})),b)}function e(a){return H(arguments,function(b){b!==a&&H(b,function(b,c){a.hasOwnProperty(c)||(a[c]=b)})}),a}function f(a,b){var c=[];for(var d in a.path){if(a.path[d]!==b.path[d])break;c.push(a.path[d])}return c}function g(a,b){if(Array.prototype.indexOf)return a.indexOf(b,Number(arguments[2])||0);var c=a.length>>>0,d=Number(arguments[2])||0;for(d=0>d?Math.ceil(d):Math.floor(d),0>d&&(d+=c);c>d;d++)if(d in a&&a[d]===b)return d;return-1}function h(a,b,c,d){var e,h=f(c,d),i={},j=[];for(var k in h)if(h[k].params&&h[k].params.length){e=h[k].params;for(var l in e)g(j,e[l])>=0||(j.push(e[l]),i[e[l]]=a[e[l]])}return I({},i,b)}function i(a,b){var c={};return H(a,function(a){var d=b[a];c[a]=null!=d?String(d):null}),c}function j(a,b,c){if(!c){c=[];for(var d in a)c.push(d)}for(var e=0;e "));if(o[c]=d,E(a))m.push(c,[function(){return b.get(a)}],h);else{var e=b.annotate(a);H(e,function(a){a!==c&&g.hasOwnProperty(a)&&k(g[a],a)}),m.push(c,a,e)}n.pop(),o[c]=f}}function l(a){return F(a)&&a.then&&a.$$promises}if(!F(g))throw new Error("'invocables' must be an object");var m=[],n=[],o={};return H(g,k),g=n=o=null,function(d,f,g){function h(){--s||(t||e(r,f.$$values),p.$$values=r,p.$$promises=!0,o.resolve(r))}function k(a){p.$$failure=a,o.reject(a)}function n(c,e,f){function i(a){l.reject(a),k(a)}function j(){if(!C(p.$$failure))try{l.resolve(b.invoke(e,g,r)),l.promise.then(function(a){r[c]=a,h()},i)}catch(a){i(a)}}var l=a.defer(),m=0;H(f,function(a){q.hasOwnProperty(a)&&!d.hasOwnProperty(a)&&(m++,q[a].then(function(b){r[a]=b,--m||j()},i))}),m||j(),q[c]=l.promise}if(l(d)&&g===c&&(g=f,f=d,d=null),d){if(!F(d))throw new Error("'locals' must be an object")}else d=i;if(f){if(!l(f))throw new Error("'parent' must be a promise returned by $resolve.resolve()")}else f=j;var o=a.defer(),p=o.promise,q=p.$$promises={},r=I({},d),s=1+m.length/3,t=!1;if(C(f.$$failure))return k(f.$$failure),p;f.$$values?(t=e(r,f.$$values),h()):(I(q,f.$$promises),f.then(h,k));for(var u=0,v=m.length;v>u;u+=3)d.hasOwnProperty(m[u])?h():n(m[u],m[u+1],m[u+2]);return p}},this.resolve=function(a,b,c,d){return this.study(a)(b,c,d)}}function m(a,b,c){this.fromConfig=function(a,b,c){return C(a.template)?this.fromString(a.template,b):C(a.templateUrl)?this.fromUrl(a.templateUrl,b):C(a.templateProvider)?this.fromProvider(a.templateProvider,b,c):null},this.fromString=function(a,b){return D(a)?a(b):a},this.fromUrl=function(c,d){return D(c)&&(c=c(d)),null==c?null:a.get(c,{cache:b}).then(function(a){return a.data})},this.fromProvider=function(a,b,d){return c.invoke(a,null,d||{params:b})}}function n(a){function b(b){if(!/^\w+(-+\w+)*$/.test(b))throw new Error("Invalid parameter name '"+b+"' in pattern '"+a+"'");if(f[b])throw new Error("Duplicate parameter name '"+b+"' in pattern '"+a+"'");f[b]=!0,j.push(b)}function c(a){return a.replace(/[\\\[\]\^$*+?.()|{}]/g,"\\$&")}var d,e=/([:*])(\w+)|\{(\w+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,f={},g="^",h=0,i=this.segments=[],j=this.params=[];this.source=a;for(var k,l,m;(d=e.exec(a))&&(k=d[2]||d[3],l=d[4]||("*"==d[1]?".*":"[^/]*"),m=a.substring(h,d.index),!(m.indexOf("?")>=0));)g+=c(m)+"("+l+")",b(k),i.push(m),h=e.lastIndex;m=a.substring(h);var n=m.indexOf("?");if(n>=0){var o=this.sourceSearch=m.substring(n);m=m.substring(0,n),this.sourcePath=a.substring(0,h+n),H(o.substring(1).split(/[&?]/),b)}else this.sourcePath=a,this.sourceSearch="";g+=c(m)+"$",i.push(m),this.regexp=new RegExp(g),this.prefix=i[0]}function o(){this.compile=function(a){return new n(a)},this.isMatcher=function(a){return F(a)&&D(a.exec)&&D(a.format)&&D(a.concat)},this.$get=function(){return this}}function p(a){function b(a){var b=/^\^((?:\\[^a-zA-Z0-9]|[^\\\[\]\^$*+?.()|{}]+)*)/.exec(a.source);return null!=b?b[1].replace(/\\(.)/g,"$1"):""}function c(a,b){return a.replace(/\$(\$|\d{1,2})/,function(a,c){return b["$"===c?0:Number(c)]})}function d(a,b,c){if(!c)return!1;var d=a.invoke(b,b,{$match:c});return C(d)?d:!0}var e=[],f=null;this.rule=function(a){if(!D(a))throw new Error("'rule' must be a function");return e.push(a),this},this.otherwise=function(a){if(E(a)){var b=a;a=function(){return b}}else if(!D(a))throw new Error("'rule' must be a function");return f=a,this},this.when=function(e,f){var g,h=E(f);if(E(e)&&(e=a.compile(e)),!h&&!D(f)&&!G(f))throw new Error("invalid 'handler' in when()");var i={matcher:function(b,c){return h&&(g=a.compile(c),c=["$match",function(a){return g.format(a)}]),I(function(a,e){return d(a,c,b.exec(e.path(),e.search()))},{prefix:E(b.prefix)?b.prefix:""})},regex:function(a,e){if(a.global||a.sticky)throw new Error("when() RegExp must not be global or sticky");return h&&(g=e,e=["$match",function(a){return c(g,a)}]),I(function(b,c){return d(b,e,a.exec(c.path()))},{prefix:b(a)})}},j={matcher:a.isMatcher(e),regex:e instanceof RegExp};for(var k in j)if(j[k])return this.rule(i[k](e,f));throw new Error("invalid 'what' in when()")},this.$get=["$location","$rootScope","$injector",function(a,b,c){function d(b){function d(b){var d=b(c,a);return d?(E(d)&&a.replace().url(d),!0):!1}if(!b||!b.defaultPrevented){var g,h=e.length;for(g=0;h>g;g++)if(d(e[g]))return;f&&d(f)}}return b.$on("$locationChangeSuccess",d),{sync:function(){d()}}}]}function q(a,e,f){function g(a){return 0===a.indexOf(".")||0===a.indexOf("^")}function l(a,b){var d=E(a),e=d?a:a.name,f=g(e);if(f){if(!b)throw new Error("No reference point given for path '"+e+"'");for(var h=e.split("."),i=0,j=h.length,k=b;j>i;i++)if(""!==h[i]||0!==i){if("^"!==h[i])break;if(!k.parent)throw new Error("Path '"+e+"' not valid for state '"+b.name+"'");k=k.parent}else k=b;h=h.slice(i).join("."),e=k.name+(k.name&&h?".":"")+h}var l=w[e];return!l||!d&&(d||l!==a&&l.self!==a)?c:l}function m(a,b){x[a]||(x[a]=[]),x[a].push(b)}function n(b){b=d(b,{self:b,resolve:b.resolve||{},toString:function(){return this.name}});var c=b.name;if(!E(c)||c.indexOf("@")>=0)throw new Error("State must have a valid name");if(w.hasOwnProperty(c))throw new Error("State '"+c+"'' is already defined");var e=-1!==c.indexOf(".")?c.substring(0,c.lastIndexOf(".")):E(b.parent)?b.parent:"";if(e&&!w[e])return m(e,b.self);for(var f in z)D(z[f])&&(b[f]=z[f](b,z.$delegates[f]));if(w[c]=b,!b[y]&&b.url&&a.when(b.url,["$match","$stateParams",function(a,c){v.$current.navigable==b&&j(a,c)||v.transitionTo(b,a,{location:!1})}]),x[c])for(var g=0;g-1}function p(a){var b=a.split("."),c=v.$current.name.split(".");if("**"===b[0]&&(c=c.slice(c.indexOf(b[1])),c.unshift("**")),"**"===b[b.length-1]&&(c.splice(c.indexOf(b[b.length-2])+1,Number.MAX_VALUE),c.push("**")),b.length!=c.length)return!1;for(var d=0,e=b.length;e>d;d++)"*"===b[d]&&(c[d]="*");return c.join("")===b.join("")}function q(a,b){return E(a)&&!C(b)?z[a]:D(b)&&E(a)?(z[a]&&!z.$delegates[a]&&(z.$delegates[a]=z[a]),z[a]=b,this):this}function r(a,b){return F(a)?b=a:b.name=a,n(b),this}function s(a,e,g,m,n,q,r,s,x){function z(){r.url()!==M&&(r.url(M),r.replace())}function A(a,c,d,f,h){var i=d?c:k(a.params,c),j={$stateParams:i};h.resolve=n.resolve(a.resolve,j,h.resolve,a);var l=[h.resolve.then(function(a){h.globals=a})];return f&&l.push(f),H(a.views,function(c,d){var e=c.resolve&&c.resolve!==a.resolve?c.resolve:{};e.$template=[function(){return g.load(d,{view:c,locals:j,params:i,notify:!1})||""}],l.push(n.resolve(e,j,h.resolve,a).then(function(f){if(D(c.controllerProvider)||G(c.controllerProvider)){var g=b.extend({},e,j);f.$$controller=m.invoke(c.controllerProvider,null,g)}else f.$$controller=c.controller;f.$$state=a,f.$$controllerAs=c.controllerAs,h[d]=f}))}),e.all(l).then(function(){return h})}var B=e.reject(new Error("transition superseded")),F=e.reject(new Error("transition prevented")),K=e.reject(new Error("transition aborted")),L=e.reject(new Error("transition failed")),M=r.url(),N=x.baseHref();return u.locals={resolve:null,globals:{$stateParams:{}}},v={params:{},current:u.self,$current:u,transition:null},v.reload=function(){v.transitionTo(v.current,q,{reload:!0,inherit:!1,notify:!1})},v.go=function(a,b,c){return this.transitionTo(a,b,I({inherit:!0,relative:v.$current},c))},v.transitionTo=function(b,c,f){c=c||{},f=I({location:!0,inherit:!1,relative:null,notify:!0,reload:!1,$retry:!1},f||{});var g,k=v.$current,n=v.params,o=k.path,p=l(b,f.relative);if(!C(p)){var s={to:b,toParams:c,options:f};if(g=a.$broadcast("$stateNotFound",s,k.self,n),g.defaultPrevented)return z(),K;if(g.retry){if(f.$retry)return z(),L;var w=v.transition=e.when(g.retry);return w.then(function(){return w!==v.transition?B:(s.options.$retry=!0,v.transitionTo(s.to,s.toParams,s.options))},function(){return K}),z(),w}if(b=s.to,c=s.toParams,f=s.options,p=l(b,f.relative),!C(p)){if(f.relative)throw new Error("Could not resolve '"+b+"' from state '"+f.relative+"'");throw new Error("No such state '"+b+"'")}}if(p[y])throw new Error("Cannot transition to abstract state '"+b+"'");f.inherit&&(c=h(q,c||{},v.$current,p)),b=p;var x,D,E=b.path,G=u.locals,H=[];for(x=0,D=E[x];D&&D===o[x]&&j(c,n,D.ownParams)&&!f.reload;x++,D=E[x])G=H[x]=D.locals;if(t(b,k,G,f))return b.self.reloadOnSearch!==!1&&z(),v.transition=null,e.when(v.current);if(c=i(b.params,c||{}),f.notify&&(g=a.$broadcast("$stateChangeStart",b.self,c,k.self,n),g.defaultPrevented))return z(),F;for(var N=e.when(G),O=x;O=x;d--)g=o[d],g.self.onExit&&m.invoke(g.self.onExit,g.self,g.locals.globals),g.locals=null;for(d=x;d1||b.ctrlKey||b.metaKey||b.shiftKey||f.attr("target")||(c(function(){a.go(i.state,j,o)}),b.preventDefault())})}}}function y(a,b,c){return{restrict:"A",controller:["$scope","$element","$attrs",function(d,e,f){function g(){a.$current.self===i&&h()?e.addClass(l):e.removeClass(l)}function h(){return!k||j(k,b)}var i,k,l;l=c(f.uiSrefActive||"",!1)(d),this.$$setStateInfo=function(b,c){i=a.get(b,w(e)),k=c,g()},d.$on("$stateChangeSuccess",g)}]}}function z(a){return function(b){return a.is(b)}}function A(a){return function(b){return a.includes(b)}}function B(a,b){function e(a){this.locals=a.locals.globals,this.params=this.locals.$stateParams}function f(){this.locals=null,this.params=null}function g(c,g){if(null!=g.redirectTo){var h,j=g.redirectTo;if(E(j))h=j;else{if(!D(j))throw new Error("Invalid 'redirectTo' in when()");h=function(a,b){return j(a,b.path(),b.search())}}b.when(c,h)}else a.state(d(g,{parent:null,name:"route:"+encodeURIComponent(c),url:c,onEnter:e,onExit:f}));return i.push(g),this}function h(a,b,d){function e(a){return""!==a.name?a:c}var f={routes:i,params:d,current:c};return b.$on("$stateChangeStart",function(a,c,d,f){b.$broadcast("$routeChangeStart",e(c),e(f))}),b.$on("$stateChangeSuccess",function(a,c,d,g){f.current=e(c),b.$broadcast("$routeChangeSuccess",e(c),e(g)),J(d,f.params)}),b.$on("$stateChangeError",function(a,c,d,f,g,h){b.$broadcast("$routeChangeError",e(c),e(f),h)}),f}var i=[];e.$inject=["$$state"],this.when=g,this.$get=h,h.$inject=["$state","$rootScope","$routeParams"]}var C=b.isDefined,D=b.isFunction,E=b.isString,F=b.isObject,G=b.isArray,H=b.forEach,I=b.extend,J=b.copy;b.module("ui.router.util",["ng"]),b.module("ui.router.router",["ui.router.util"]),b.module("ui.router.state",["ui.router.router","ui.router.util"]),b.module("ui.router",["ui.router.state"]),b.module("ui.router.compat",["ui.router"]),l.$inject=["$q","$injector"],b.module("ui.router.util").service("$resolve",l),m.$inject=["$http","$templateCache","$injector"],b.module("ui.router.util").service("$templateFactory",m),n.prototype.concat=function(a){return new n(this.sourcePath+a+this.sourceSearch)},n.prototype.toString=function(){return this.source},n.prototype.exec=function(a,b){var c=this.regexp.exec(a);if(!c)return null;var d,e=this.params,f=e.length,g=this.segments.length-1,h={};if(g!==c.length-1)throw new Error("Unbalanced capture group in route '"+this.source+"'");for(d=0;g>d;d++)h[e[d]]=c[d+1];for(;f>d;d++)h[e[d]]=b[e[d]];return h},n.prototype.parameters=function(){return this.params},n.prototype.format=function(a){var b=this.segments,c=this.params;if(!a)return b.join("");var d,e,f,g=b.length-1,h=c.length,i=b[0];for(d=0;g>d;d++)f=a[c[d]],null!=f&&(i+=encodeURIComponent(f)),i+=b[d+1];for(;h>d;d++)f=a[c[d]],null!=f&&(i+=(e?"&":"?")+c[d]+"="+encodeURIComponent(f),e=!0);return i},b.module("ui.router.util").provider("$urlMatcherFactory",o),p.$inject=["$urlMatcherFactoryProvider"],b.module("ui.router.router").provider("$urlRouter",p),q.$inject=["$urlRouterProvider","$urlMatcherFactoryProvider","$locationProvider"],b.module("ui.router.state").value("$stateParams",{}).provider("$state",q),r.$inject=[],b.module("ui.router.state").provider("$view",r),b.module("ui.router.state").provider("$uiViewScroll",s),t.$inject=["$state","$injector","$uiViewScroll"],u.$inject=["$compile","$controller","$state"],b.module("ui.router.state").directive("uiView",t),b.module("ui.router.state").directive("uiView",u),x.$inject=["$state","$timeout"],y.$inject=["$state","$stateParams","$interpolate"],b.module("ui.router.state").directive("uiSref",x).directive("uiSrefActive",y),z.$inject=["$state"],A.$inject=["$state"],b.module("ui.router.state").filter("isState",z).filter("includedByState",A),B.$inject=["$stateProvider","$urlRouterProvider"],b.module("ui.router.compat").provider("$route",B).directive("ngView",t)}(window,window.angular); \ No newline at end of file +"undefined"!=typeof module&&"undefined"!=typeof exports&&module.exports===exports&&(module.exports="ui.router"),function(a,b,c){"use strict";function d(a,b){return T(new(T(function(){},{prototype:a})),b)}function e(a){return S(arguments,function(b){b!==a&&S(b,function(b,c){a.hasOwnProperty(c)||(a[c]=b)})}),a}function f(a,b){var c=[];for(var d in a.path){if(a.path[d]!==b.path[d])break;c.push(a.path[d])}return c}function g(a){if(Object.keys)return Object.keys(a);var b=[];return S(a,function(a,c){b.push(c)}),b}function h(a,b){if(Array.prototype.indexOf)return a.indexOf(b,Number(arguments[2])||0);var c=a.length>>>0,d=Number(arguments[2])||0;for(d=d<0?Math.ceil(d):Math.floor(d),d<0&&(d+=c);d=0||(k.push(e[m]),j[e[m]]=a[e[m]]);return T({},j,b)}function j(a,b,c){if(!c){c=[];for(var d in a)c.push(d)}for(var e=0;e "));if(t[c]=d,P(a))r.push(c,[function(){return b.get(a)}],j);else{var e=b.annotate(a);S(e,function(a){a!==c&&i.hasOwnProperty(a)&&n(i[a],a)}),r.push(c,a,e)}s.pop(),t[c]=f}}function o(a){return Q(a)&&a.then&&a.$$promises}if(!Q(i))throw new Error("'invocables' must be an object");var q=g(i||{}),r=[],s=[],t={};return S(i,n),i=s=t=null,function(d,f,g){function h(){--v||(w||e(u,f.$$values),s.$$values=u,s.$$promises=s.$$promises||!0,delete s.$$inheritedValues,n.resolve(u))}function i(a){s.$$failure=a,n.reject(a)}function j(c,e,f){function j(a){l.reject(a),i(a)}function k(){if(!N(s.$$failure))try{l.resolve(b.invoke(e,g,u)),l.promise.then(function(a){u[c]=a,h()},j)}catch(a){j(a)}}var l=a.defer(),m=0;S(f,function(a){t.hasOwnProperty(a)&&!d.hasOwnProperty(a)&&(m++,t[a].then(function(b){u[a]=b,--m||k()},j))}),m||k(),t[c]=p(l.promise)}if(o(d)&&g===c&&(g=f,f=d,d=null),d){if(!Q(d))throw new Error("'locals' must be an object")}else d=k;if(f){if(!o(f))throw new Error("'parent' must be a promise returned by $resolve.resolve()")}else f=l;var n=a.defer(),s=p(n.promise),t=s.$$promises={},u=T({},d),v=1+r.length/3,w=!1;if(p(s),N(f.$$failure))return i(f.$$failure),s;f.$$inheritedValues&&e(u,m(f.$$inheritedValues,q)),T(t,f.$$promises),f.$$values?(w=e(u,m(f.$$values,q)),s.$$inheritedValues=m(f.$$values,q),h()):(f.$$inheritedValues&&(s.$$inheritedValues=m(f.$$inheritedValues,q)),f.then(h,i));for(var x=0,y=r.length;x=0));)s=f(r.id,r.type,r.cfg,"path"),l+=g(r.segment,s.type.pattern.source,s.squash,s.isOptional),n.push(r.segment),m=j.lastIndex;t=a.substring(m);var u=t.indexOf("?");if(u>=0){var v=this.sourceSearch=t.substring(u);if(t=t.substring(0,u),this.sourcePath=a.substring(0,m+u),v.length>0)for(m=0;i=k.exec(v);)r=h(i,!0),s=f(r.id,r.type,r.cfg,"search"),m=j.lastIndex}else this.sourcePath=a,this.sourceSearch="";l+=g(t)+(!1===b.strict?"/?":"")+"$",n.push(t),this.regexp=new RegExp(l,b.caseInsensitive?"i":c),this.prefix=n[0],this.$$paramNames=q}function u(a){T(this,a)}function v(){function a(a){return null!=a?a.toString().replace(/(~|\/)/g,function(a){return{"~":"~~","/":"~2F"}[a]}):a}function e(a){return null!=a?a.toString().replace(/(~~|~2F)/g,function(a){return{"~~":"~","~2F":"/"}[a]}):a}function f(){return{strict:p,caseInsensitive:m}}function i(a){return O(a)||R(a)&&O(a[a.length-1])}function j(){for(;w.length;){var a=w.shift();if(a.pattern)throw new Error("You cannot override a type's .pattern at runtime.");b.extend(r[a.name],l.invoke(a.def))}}function k(a){T(this,a||{})}W=this;var l,m=!1,p=!0,q=!1,r={},s=!0,w=[],x={string:{encode:a,decode:e,is:function(a){return null==a||!N(a)||"string"==typeof a},pattern:/[^\/]*/},int:{encode:a,decode:function(a){return parseInt(a,10)},is:function(a){return a!==c&&null!==a&&this.decode(a.toString())===a},pattern:/-?\d+/},bool:{encode:function(a){return a?1:0},decode:function(a){return 0!==parseInt(a,10)},is:function(a){return!0===a||!1===a},pattern:/0|1/},date:{encode:function(a){return this.is(a)?[a.getFullYear(),("0"+(a.getMonth()+1)).slice(-2),("0"+a.getDate()).slice(-2)].join("-"):c},decode:function(a){if(this.is(a))return a;var b=this.capture.exec(a);return b?new Date(b[1],b[2]-1,b[3]):c},is:function(a){return a instanceof Date&&!isNaN(a.valueOf())},equals:function(a,b){return this.is(a)&&this.is(b)&&a.toISOString()===b.toISOString()},pattern:/[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[1-2][0-9]|3[0-1])/,capture:/([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])/},json:{encode:b.toJson,decode:b.fromJson,is:b.isObject,equals:b.equals,pattern:/[^\/]*/},any:{encode:b.identity,decode:b.identity,equals:b.equals,pattern:/.*/}};v.$$getDefaultValue=function(a){if(!i(a.value))return a.value;if(!l)throw new Error("Injectable functions cannot be called at configuration time");return l.invoke(a.value)},this.caseInsensitive=function(a){return N(a)&&(m=a),m},this.strictMode=function(a){return N(a)&&(p=a),p},this.defaultSquashPolicy=function(a){if(!N(a))return q;if(!0!==a&&!1!==a&&!P(a))throw new Error("Invalid squash policy: "+a+". Valid policies: false, true, arbitrary-string");return q=a,a},this.compile=function(a,b){return new t(a,T(f(),b))},this.isMatcher=function(a){if(!Q(a))return!1;var b=!0;return S(t.prototype,function(c,d){O(c)&&(b=b&&N(a[d])&&O(a[d]))}),b},this.type=function(a,b,c){if(!N(b))return r[a];if(r.hasOwnProperty(a))throw new Error("A type named '"+a+"' has already been defined.");return r[a]=new u(T({name:a},b)),c&&(w.push({name:a,def:c}),s||j()),this},S(x,function(a,b){r[b]=new u(T({name:b},a))}),r=d(r,{}),this.$get=["$injector",function(a){return l=a,s=!1,j(),S(x,function(a,b){r[b]||(r[b]=new u(a))}),this}],this.Param=function(a,d,e,f){function j(a){var b=Q(a)?g(a):[];return-1===h(b,"value")&&-1===h(b,"type")&&-1===h(b,"squash")&&-1===h(b,"array")&&(a={value:a}),a.$$fn=i(a.value)?a.value:function(){return a.value},a}function k(c,d,e){if(c.type&&d)throw new Error("Param '"+a+"' has two type configurations.");return d||(c.type?b.isString(c.type)?r[c.type]:c.type instanceof u?c.type:new u(c.type):"config"===e?r.any:r.string)}function m(){var b={array:"search"===f&&"auto"},c=a.match(/\[\]$/)?{array:!0}:{};return T(b,c,e).array}function p(a,b){var c=a.squash;if(!b||!1===c)return!1;if(!N(c)||null==c)return q;if(!0===c||P(c))return c;throw new Error("Invalid squash policy: '"+c+"'. Valid policies: false, true, or arbitrary string")}function s(a,b,d,e){var f,g,i=[{from:"",to:d||b?c:""},{from:null,to:d||b?c:""}];return f=R(a.replace)?a.replace:[],P(e)&&f.push({from:e,to:c}),g=o(f,function(a){return a.from}),n(i,function(a){return-1===h(g,a.from)}).concat(f)}function t(){if(!l)throw new Error("Injectable functions cannot be called at configuration time");var a=l.invoke(e.$$fn);if(null!==a&&a!==c&&!x.type.is(a))throw new Error("Default value ("+a+") for parameter '"+x.id+"' is not an instance of Type ("+x.type.name+")");return a}function v(a){function b(a){return function(b){return b.from===a}}function c(a){var c=o(n(x.replace,b(a)),function(a){return a.to});return c.length?c[0]:a}return a=c(a),N(a)?x.type.$normalize(a):t()}function w(){return"{Param:"+a+" "+d+" squash: '"+A+"' optional: "+z+"}"}var x=this;e=j(e),d=k(e,d,f);var y=m();d=y?d.$asArray(y,"search"===f):d,"string"!==d.name||y||"path"!==f||e.value!==c||(e.value="");var z=e.value!==c,A=p(e,z),B=s(e,y,z,A);T(this,{id:a,type:d,location:f,array:y,squash:A,replace:B,isOptional:z,value:v,dynamic:c,config:e,toString:w})},k.prototype={$$new:function(){return d(this,T(new k,{$$parent:this}))},$$keys:function(){for(var a=[],b=[],c=this,d=g(k.prototype);c;)b.push(c),c=c.$$parent;return b.reverse(),S(b,function(b){S(g(b),function(b){-1===h(a,b)&&-1===h(d,b)&&a.push(b)})}),a},$$values:function(a){var b={},c=this;return S(c.$$keys(),function(d){b[d]=c[d].value(a&&a[d])}),b},$$equals:function(a,b){var c=!0,d=this;return S(d.$$keys(),function(e){var f=a&&a[e],g=b&&b[e];d[e].type.equals(f,g)||(c=!1)}),c},$$validates:function(a){var d,e,f,g,h,i=this.$$keys();for(d=0;d=0)throw new Error("State must have a valid name");if(A.hasOwnProperty(c))throw new Error("State '"+c+"' is already defined");var e=-1!==c.indexOf(".")?c.substring(0,c.lastIndexOf(".")):P(b.parent)?b.parent:Q(b.parent)&&P(b.parent.name)?b.parent.name:"";if(e&&!A[e])return n(e,b.self);for(var f in D)O(D[f])&&(b[f]=D[f](b,D.$delegates[f]));return A[c]=b,!b[C]&&b.url&&a.when(b.url,["$match","$stateParams",function(a,c){z.$current.navigable==b&&j(a,c)||z.transitionTo(b,a,{inherit:!0,location:!1})}]),q(c),b}function s(a){return a.indexOf("*")>-1}function t(a){for(var b=a.split("."),c=z.$current.name.split("."),d=0,e=b.length;d=G;d--)g=q[d],g.self.onExit&&h.invoke(g.self.onExit,g.self,g.locals.globals),g.locals=null;for(d=G;d2?k.enter(a,null,c).then(d):k.enter(a,null,c,d)},leave:function(a,c){b.version.minor>2?k.leave(a).then(c):k.leave(a,c)}};if(j){var e=j&&j(c,a);return{enter:function(a,b,c){e.enter(a,null,b),c()},leave:function(a,b){e.leave(a),b()}}}return d()}var i=g(),j=i("$animator"),k=i("$animate");return{restrict:"ECA",terminal:!0,priority:400,transclude:"element",compile:function(c,g,i){return function(c,g,j){function k(){if(m&&(m.remove(),m=null),o&&(o.$destroy(),o=null),n){var a=n.data("$uiViewAnim");s.leave(n,function(){a.$$animLeave.resolve(),m=null}),m=n,n=null}}function l(h){var l,m=C(c,j,g,e),t=m&&a.$current&&a.$current.locals[m];if(h||t!==p){l=c.$new(),p=a.$current.locals[m],l.$emit("$viewContentLoading",m);var u=i(l,function(a){var e=f.defer(),h=f.defer(),i={$animEnter:e.promise,$animLeave:h.promise,$$animLeave:h};a.data("$uiViewAnim",i),s.enter(a,g,function(){e.resolve(),o&&o.$emit("$viewContentAnimationEnded"),(b.isDefined(r)&&!r||c.$eval(r))&&d(a)}),k()});n=u,o=l,o.$emit("$viewContentLoaded",m),o.$eval(q)}}var m,n,o,p,q=j.onload||"",r=j.autoscroll,s=h(j,c);g.inheritedData("$uiView");c.$on("$stateChangeSuccess",function(){l(!1)}),l(!0)}}}}function B(a,c,d,e){return{restrict:"ECA",priority:-400,compile:function(f){var g=f.html();return f.empty?f.empty():f[0].innerHTML=null,function(f,h,i){var j=d.$current,k=C(f,i,h,e),l=j&&j.locals[k];if(!l)return h.html(g),void a(h.contents())(f);h.data("$uiView",{name:k,state:l.$$state}),h.html(l.$template?l.$template:g);var m=b.extend({},l);f[l.$$resolveAs]=m;var n=a(h.contents());if(l.$$controller){l.$scope=f,l.$element=h;var o=c(l.$$controller,l);l.$$controllerAs&&(f[l.$$controllerAs]=o,f[l.$$controllerAs][l.$$resolveAs]=m),O(o.$onInit)&&o.$onInit(),h.data("$ngControllerController",o),h.children().data("$ngControllerController",o)}n(f)}}}}function C(a,b,c,d){var e=d(b.uiView||b.name||"")(a),f=c.inheritedData("$uiView");return e.indexOf("@")>=0?e:e+"@"+(f?f.state.name:"")}function D(a,b){var c,d=a.match(/^\s*({[^}]*})\s*$/);if(d&&(a=b+"("+d[1]+")"),!(c=a.replace(/\n/g," ").match(/^([^(]+?)\s*(\((.*)\))?$/))||4!==c.length)throw new Error("Invalid state ref '"+a+"'");return{state:c[1],paramExpr:c[3]||null}}function E(a){var b=a.parent().inheritedData("$uiView");if(b&&b.state&&b.state.name)return b.state}function F(a){var b="[object SVGAnimatedString]"===Object.prototype.toString.call(a.prop("href")),c="FORM"===a[0].nodeName;return{attr:c?"action":b?"xlink:href":"href",isAnchor:"A"===a.prop("tagName").toUpperCase(),clickable:!c}}function G(a,b,c,d,e){return function(f){var g=f.which||f.button,h=e();if(!(g>1||f.ctrlKey||f.metaKey||f.shiftKey||a.attr("target"))){var i=c(function(){b.go(h.state,h.params,h.options)});f.preventDefault();var j=d.isAnchor&&!h.href?1:0;f.preventDefault=function(){j--<=0&&c.cancel(i)}}}}function H(a,b){return{relative:E(a)||b.$current,inherit:!0}}function I(a,c){return{restrict:"A",require:["?^uiSrefActive","?^uiSrefActiveEq"],link:function(d,e,f,g){var h,i=D(f.uiSref,a.current.name),j={state:i.state,href:null,params:null},k=F(e),l=g[1]||g[0],m=null;j.options=T(H(e,a),f.uiSrefOpts?d.$eval(f.uiSrefOpts):{});var n=function(c){c&&(j.params=b.copy(c)),j.href=a.href(i.state,j.params,j.options),m&&m(),l&&(m=l.$$addStateInfo(i.state,j.params)),null!==j.href&&f.$set(k.attr,j.href)};i.paramExpr&&(d.$watch(i.paramExpr,function(a){a!==j.params&&n(a)},!0),j.params=b.copy(d.$eval(i.paramExpr))),n(),k.clickable&&(h=G(e,a,c,k,function(){return j}),e[e.on?"on":"bind"]("click",h),d.$on("$destroy",function(){e[e.off?"off":"unbind"]("click",h)}))}}}function J(a,b){return{restrict:"A",require:["?^uiSrefActive","?^uiSrefActiveEq"],link:function(c,d,e,f){function g(b){m.state=b[0],m.params=b[1],m.options=b[2],m.href=a.href(m.state,m.params,m.options),n&&n(),j&&(n=j.$$addStateInfo(m.state,m.params)),m.href&&e.$set(i.attr,m.href)}var h,i=F(d),j=f[1]||f[0],k=[e.uiState,e.uiStateParams||null,e.uiStateOpts||null],l="["+k.map(function(a){return a||"null"}).join(", ")+"]",m={state:null,params:null,options:null,href:null},n=null;c.$watch(l,g,!0),g(c.$eval(l)),i.clickable&&(h=G(d,a,b,i,function(){return m}),d[d.on?"on":"bind"]("click",h),c.$on("$destroy",function(){d[d.off?"off":"unbind"]("click",h)}))}}}function K(a,b,c){return{restrict:"A",controller:["$scope","$element","$attrs","$timeout",function(b,d,e,f){function g(b,c,e){var f=a.get(b,E(d)),g=h(b,c),i={state:f||{name:b},params:c,hash:g};return p.push(i),q[g]=e,function(){var a=p.indexOf(i);-1!==a&&p.splice(a,1)}}function h(a,c){if(!P(a))throw new Error("state should be a string");return Q(c)?a+V(c):(c=b.$eval(c),Q(c)?a+V(c):a)}function i(){for(var a=0;a0)){var c=g(a,b,o);return i(),c}},b.$on("$stateChangeSuccess",i),i()}]}}function L(a){var b=function(b,c){return a.is(b,c)};return b.$stateful=!0,b}function M(a){var b=function(b,c,d){return a.includes(b,c,d)};return b.$stateful=!0,b}var N=b.isDefined,O=b.isFunction,P=b.isString,Q=b.isObject,R=b.isArray,S=b.forEach,T=b.extend,U=b.copy,V=b.toJson;b.module("ui.router.util",["ng"]),b.module("ui.router.router",["ui.router.util"]),b.module("ui.router.state",["ui.router.router","ui.router.util"]),b.module("ui.router",["ui.router.state"]),b.module("ui.router.compat",["ui.router"]),q.$inject=["$q","$injector"],b.module("ui.router.util").service("$resolve",q),b.module("ui.router.util").provider("$templateFactory",r);var W;t.prototype.concat=function(a,b){var c={caseInsensitive:W.caseInsensitive(),strict:W.strictMode(),squash:W.defaultSquashPolicy()};return new t(this.sourcePath+a+this.sourceSearch,T(c,b),this)},t.prototype.toString=function(){return this.source},t.prototype.exec=function(a,b){function c(a){function b(a){return a.split("").reverse().join("")}function c(a){return a.replace(/\\-/g,"-")}return o(o(b(a).split(/-(?!\\)/),b),c).reverse()}var d=this.regexp.exec(a);if(!d)return null;b=b||{};var e,f,g,h=this.parameters(),i=h.length,j=this.segments.length-1,k={};if(j!==d.length-1)throw new Error("Unbalanced capture group in route '"+this.source+"'");var l,m;for(e=0;e 0; + } /** * @description * * This object provides a utility for producing rich Error messages within - * Angular. It can be called as follows: + * AngularJS. It can be called as follows: * * var exampleMinErr = minErr('example'); * throw exampleMinErr('one', 'This {0} is {1}', foo, bar); @@ -30,140 +80,145 @@ * should all be static strings, not variables or general expressions. * * @param {string} module The namespace to use for the new minErr instance. + * @param {function} ErrorConstructor Custom error constructor to be instantiated when returning + * error from returned function, for cases when a particular type of error is useful. * @returns {function(code:string, template:string, ...templateArgs): Error} minErr instance */ - function minErr(module) { - return function () { + function minErr(module, ErrorConstructor) { + ErrorConstructor = ErrorConstructor || Error; + return function() { var code = arguments[0], - prefix = '[' + (module ? module + ':' : '') + code + '] ', template = arguments[1], - templateArgs = arguments, - stringify = function (obj) { - if (typeof obj === 'function') { - return obj.toString().replace(/ \{[\s\S]*$/, ''); - } else if (typeof obj === 'undefined') { - return 'undefined'; - } else if (typeof obj !== 'string') { - return JSON.stringify(obj); - } - return obj; - }, - message, i; + message = '[' + (module ? module + ':' : '') + code + '] ', + templateArgs = sliceArgs(arguments, 2).map(function(arg) { + return toDebugString(arg, minErrConfig.objectMaxDepth); + }), + paramPrefix, i; - message = prefix + template.replace(/\{\d+\}/g, function (match) { - var index = +match.slice(1, -1), arg; + message += template.replace(/\{\d+\}/g, function(match) { + var index = +match.slice(1, -1); - if (index + 2 < templateArgs.length) { - arg = templateArgs[index + 2]; - if (typeof arg === 'function') { - return arg.toString().replace(/ ?\{[\s\S]*$/, ''); - } else if (typeof arg === 'undefined') { - return 'undefined'; - } else if (typeof arg !== 'string') { - return toJson(arg); - } - return arg; + if (index < templateArgs.length) { + return templateArgs[index]; } + return match; }); - message = message + '\nhttp://errors.angularjs.org/1.2.16/' + - (module ? module + '/' : '') + code; - for (i = 2; i < arguments.length; i++) { - message = message + (i == 2 ? '?' : '&') + 'p' + (i-2) + '=' + - encodeURIComponent(stringify(arguments[i])); + message += '\nhttp://errors.angularjs.org/1.6.9/' + + (module ? module + '/' : '') + code; + + for (i = 0, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') { + message += paramPrefix + 'p' + i + '=' + encodeURIComponent(templateArgs[i]); } - return new Error(message); + return new ErrorConstructor(message); }; } - /* We need to tell jshint what variables are being exported */ - /* global - -angular, - -msie, - -jqLite, - -jQuery, - -slice, - -push, - -toString, - -ngMinErr, - -_angular, - -angularModule, - -nodeName_, - -uid, - - -lowercase, - -uppercase, - -manualLowercase, - -manualUppercase, - -nodeName_, - -isArrayLike, - -forEach, - -sortedKeys, - -forEachSorted, - -reverseParams, - -nextUid, - -setHashKey, - -extend, - -int, - -inherit, - -noop, - -identity, - -valueFn, - -isUndefined, - -isDefined, - -isObject, - -isString, - -isNumber, - -isDate, - -isArray, - -isFunction, - -isRegExp, - -isWindow, - -isScope, - -isFile, - -isBlob, - -isBoolean, - -trim, - -isElement, - -makeMap, - -map, - -size, - -includes, - -indexOf, - -arrayRemove, - -isLeafNode, - -copy, - -shallowCopy, - -equals, - -csp, - -concat, - -sliceArgs, - -bind, - -toJsonReplacer, - -toJson, - -fromJson, - -toBoolean, - -startingTag, - -tryDecodeURIComponent, - -parseKeyValue, - -toKeyValue, - -encodeUriSegment, - -encodeUriQuery, - -angularInit, - -bootstrap, - -snake_case, - -bindJQuery, - -assertArg, - -assertArgFn, - -assertNotHasOwnProperty, - -getter, - -getBlockElements, - -hasOwnProperty, - - */ + /* We need to tell ESLint what variables are being exported */ + /* exported + angular, + msie, + jqLite, + jQuery, + slice, + splice, + push, + toString, + minErrConfig, + errorHandlingConfig, + isValidObjectMaxDepth, + ngMinErr, + angularModule, + uid, + REGEX_STRING_REGEXP, + VALIDITY_STATE_PROPERTY, + + lowercase, + uppercase, + manualLowercase, + manualUppercase, + nodeName_, + isArrayLike, + forEach, + forEachSorted, + reverseParams, + nextUid, + setHashKey, + extend, + toInt, + inherit, + merge, + noop, + identity, + valueFn, + isUndefined, + isDefined, + isObject, + isBlankObject, + isString, + isNumber, + isNumberNaN, + isDate, + isError, + isArray, + isFunction, + isRegExp, + isWindow, + isScope, + isFile, + isFormData, + isBlob, + isBoolean, + isPromiseLike, + trim, + escapeForRegexp, + isElement, + makeMap, + includes, + arrayRemove, + copy, + simpleCompare, + equals, + csp, + jq, + concat, + sliceArgs, + bind, + toJsonReplacer, + toJson, + fromJson, + convertTimezoneToLocal, + timezoneToOffset, + startingTag, + tryDecodeURIComponent, + parseKeyValue, + toKeyValue, + encodeUriSegment, + encodeUriQuery, + angularInit, + bootstrap, + getTestability, + snake_case, + bindJQuery, + assertArg, + assertArgFn, + assertNotHasOwnProperty, + getter, + getBlockNodes, + hasOwnProperty, + createMap, + stringify, + + NODE_TYPE_ELEMENT, + NODE_TYPE_ATTRIBUTE, + NODE_TYPE_TEXT, + NODE_TYPE_COMMENT, + NODE_TYPE_DOCUMENT, + NODE_TYPE_DOCUMENT_FRAGMENT +*/ //////////////////////////////////// @@ -171,91 +226,107 @@ * @ngdoc module * @name ng * @module ng + * @installation * @description * - * # ng (core module) * The ng module is loaded by default when an AngularJS application is started. The module itself * contains the essential components for an AngularJS application to function. The table below * lists a high level breakdown of each of the services/factories, filters, directives and testing * components available within this core module. * - *
    */ + var REGEX_STRING_REGEXP = /^\/(.+)\/([a-z]*)$/; + +// The name of a form control's ValidityState property. +// This is used so that it's possible for internal tests to create mock ValidityStates. + var VALIDITY_STATE_PROPERTY = 'validity'; + + + var hasOwnProperty = Object.prototype.hasOwnProperty; + /** * @ngdoc function * @name angular.lowercase * @module ng - * @function + * @kind function + * + * @deprecated + * sinceVersion="1.5.0" + * removeVersion="1.7.0" + * Use [String.prototype.toLowerCase](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/toLowerCase) instead. * * @description Converts the specified string to lowercase. * @param {string} string String to be converted to lowercase. * @returns {string} Lowercased string. */ - var lowercase = function(string){return isString(string) ? string.toLowerCase() : string;}; - var hasOwnProperty = Object.prototype.hasOwnProperty; + var lowercase = function(string) {return isString(string) ? string.toLowerCase() : string;}; /** * @ngdoc function * @name angular.uppercase * @module ng - * @function + * @kind function + * + * @deprecated + * sinceVersion="1.5.0" + * removeVersion="1.7.0" + * Use [String.prototype.toUpperCase](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/toUpperCase) instead. * * @description Converts the specified string to uppercase. * @param {string} string String to be converted to uppercase. * @returns {string} Uppercased string. */ - var uppercase = function(string){return isString(string) ? string.toUpperCase() : string;}; + var uppercase = function(string) {return isString(string) ? string.toUpperCase() : string;}; var manualLowercase = function(s) { - /* jshint bitwise: false */ + /* eslint-disable no-bitwise */ return isString(s) ? s.replace(/[A-Z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) | 32);}) : s; + /* eslint-enable */ }; var manualUppercase = function(s) { - /* jshint bitwise: false */ + /* eslint-disable no-bitwise */ return isString(s) ? s.replace(/[a-z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) & ~32);}) : s; + /* eslint-enable */ }; // String#toLowerCase and String#toUpperCase don't produce correct results in browsers with Turkish // locale, for this reason we need to detect this case and redefine lowercase/uppercase methods -// with correct but slower alternatives. +// with correct but slower alternatives. See https://github.com/angular/angular.js/issues/11387 if ('i' !== 'I'.toLowerCase()) { lowercase = manualLowercase; uppercase = manualUppercase; } - var /** holds major version number for IE or NaN for real browsers */ - msie, + var + msie, // holds major version number for IE, or NaN if UA is not IE. jqLite, // delay binding since jQuery could be loaded after us. jQuery, // delay binding slice = [].slice, + splice = [].splice, push = [].push, toString = Object.prototype.toString, + getPrototypeOf = Object.getPrototypeOf, ngMinErr = minErr('ng'), - - _angular = window.angular, /** @name angular */ angular = window.angular || (window.angular = {}), angularModule, - nodeName_, - uid = ['0', '0', '0']; + uid = 0; +// Support: IE 9-11 only /** - * IE 11 changed the format of the UserAgent string. - * See http://msdn.microsoft.com/en-us/library/ms537503.aspx + * documentMode is an IE-only property + * http://msdn.microsoft.com/en-us/library/ie/cc196988(v=vs.85).aspx */ - msie = int((/msie (\d+)/.exec(lowercase(navigator.userAgent)) || [])[1]); - if (isNaN(msie)) { - msie = int((/trident\/.*; rv:(\d+)/.exec(lowercase(navigator.userAgent)) || [])[1]); - } + msie = window.document.documentMode; /** @@ -265,39 +336,51 @@ * String ...) */ function isArrayLike(obj) { - if (obj == null || isWindow(obj)) { - return false; - } - var length = obj.length; + // `null`, `undefined` and `window` are not array-like + if (obj == null || isWindow(obj)) return false; - if (obj.nodeType === 1 && length) { - return true; - } + // arrays, strings and jQuery/jqLite objects are array like + // * jqLite is either the jQuery or jqLite constructor function + // * we have to check the existence of jqLite first as this method is called + // via the forEach method when constructing the jqLite object in the first place + if (isArray(obj) || isString(obj) || (jqLite && obj instanceof jqLite)) return true; + + // Support: iOS 8.2 (not reproducible in simulator) + // "length" in obj used to prevent JIT error (gh-11508) + var length = 'length' in Object(obj) && obj.length; + + // NodeList objects (with `item` method) and + // other objects with suitable length characteristics are array-like + return isNumber(length) && + (length >= 0 && ((length - 1) in obj || obj instanceof Array) || typeof obj.item === 'function'); - return isString(obj) || isArray(obj) || length === 0 || - typeof length === 'number' && length > 0 && (length - 1) in obj; } /** * @ngdoc function * @name angular.forEach * @module ng - * @function + * @kind function * * @description * Invokes the `iterator` function once for each item in `obj` collection, which can be either an - * object or an array. The `iterator` function is invoked with `iterator(value, key)`, where `value` - * is the value of an object property or an array element and `key` is the object property key or - * array element index. Specifying a `context` for the function is optional. + * object or an array. The `iterator` function is invoked with `iterator(value, key, obj)`, where `value` + * is the value of an object property or an array element, `key` is the object property key or + * array element index and obj is the `obj` itself. Specifying a `context` for the function is optional. * * It is worth noting that `.forEach` does not iterate over inherited properties because it filters * using the `hasOwnProperty` method. * + * Unlike ES262's + * [Array.prototype.forEach](http://www.ecma-international.org/ecma-262/5.1/#sec-15.4.4.18), + * providing 'undefined' or 'null' values for `obj` will not throw a TypeError, but rather just + * return the value provided. + * ```js var values = {name: 'misko', gender: 'male'}; var log = []; - angular.forEach(values, function(value, key){ + angular.forEach(values, function(value, key) { this.push(key + ': ' + value); }, log); expect(log).toEqual(['name: misko', 'gender: male']); @@ -308,26 +391,42 @@ * @param {Object=} context Object to become context (`this`) for the iterator function. * @returns {Object|Array} Reference to `obj`. */ + function forEach(obj, iterator, context) { - var key; + var key, length; if (obj) { - if (isFunction(obj)){ + if (isFunction(obj)) { for (key in obj) { - // Need to check if hasOwnProperty exists, - // as on IE8 the result of querySelectorAll is an object without a hasOwnProperty function - if (key != 'prototype' && key != 'length' && key != 'name' && (!obj.hasOwnProperty || obj.hasOwnProperty(key))) { - iterator.call(context, obj[key], key); + if (key !== 'prototype' && key !== 'length' && key !== 'name' && obj.hasOwnProperty(key)) { + iterator.call(context, obj[key], key, obj); + } + } + } else if (isArray(obj) || isArrayLike(obj)) { + var isPrimitive = typeof obj !== 'object'; + for (key = 0, length = obj.length; key < length; key++) { + if (isPrimitive || key in obj) { + iterator.call(context, obj[key], key, obj); } } } else if (obj.forEach && obj.forEach !== forEach) { - obj.forEach(iterator, context); - } else if (isArrayLike(obj)) { - for (key = 0; key < obj.length; key++) - iterator.call(context, obj[key], key); - } else { + obj.forEach(iterator, context, obj); + } else if (isBlankObject(obj)) { + // createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty + for (key in obj) { + iterator.call(context, obj[key], key, obj); + } + } else if (typeof obj.hasOwnProperty === 'function') { + // Slow path for objects inheriting Object.prototype, hasOwnProperty check needed for (key in obj) { if (obj.hasOwnProperty(key)) { - iterator.call(context, obj[key], key); + iterator.call(context, obj[key], key, obj); + } + } + } else { + // Slow path for objects which do not have a method `hasOwnProperty` + for (key in obj) { + if (hasOwnProperty.call(obj, key)) { + iterator.call(context, obj[key], key, obj); } } } @@ -335,19 +434,9 @@ return obj; } - function sortedKeys(obj) { - var keys = []; - for (var key in obj) { - if (obj.hasOwnProperty(key)) { - keys.push(key); - } - } - return keys.sort(); - } - function forEachSorted(obj, iterator, context) { - var keys = sortedKeys(obj); - for ( var i = 0; i < keys.length; i++) { + var keys = Object.keys(obj).sort(); + for (var i = 0; i < keys.length; i++) { iterator.call(context, obj[keys[i]], keys[i]); } return keys; @@ -360,37 +449,21 @@ * @returns {function(*, string)} */ function reverseParams(iteratorFn) { - return function(value, key) { iteratorFn(key, value); }; + return function(value, key) {iteratorFn(key, value);}; } /** - * A consistent way of creating unique IDs in angular. The ID is a sequence of alpha numeric - * characters such as '012ABC'. The reason why we are not using simply a number counter is that - * the number string gets longer over time, and it can also overflow, where as the nextId - * will grow much slower, it is a string, and it will never overflow. + * A consistent way of creating unique IDs in angular. * - * @returns {string} an unique alpha-numeric string + * Using simple numbers allows us to generate 28.6 million unique ids per second for 10 years before + * we hit number precision issues in JavaScript. + * + * Math.pow(2,53) / 60 / 60 / 24 / 365 / 10 = 28.6M + * + * @returns {number} an unique alpha-numeric string */ function nextUid() { - var index = uid.length; - var digit; - - while(index) { - index--; - digit = uid[index].charCodeAt(0); - if (digit == 57 /*'9'*/) { - uid[index] = 'A'; - return uid.join(''); - } - if (digit == 90 /*'Z'*/) { - uid[index] = '0'; - } else { - uid[index] = String.fromCharCode(digit + 1); - return uid.join(''); - } - } - uid.unshift('0'); - return uid.join(''); + return ++uid; } @@ -402,54 +475,126 @@ function setHashKey(obj, h) { if (h) { obj.$$hashKey = h; - } - else { + } else { delete obj.$$hashKey; } } + + function baseExtend(dst, objs, deep) { + var h = dst.$$hashKey; + + for (var i = 0, ii = objs.length; i < ii; ++i) { + var obj = objs[i]; + if (!isObject(obj) && !isFunction(obj)) continue; + var keys = Object.keys(obj); + for (var j = 0, jj = keys.length; j < jj; j++) { + var key = keys[j]; + var src = obj[key]; + + if (deep && isObject(src)) { + if (isDate(src)) { + dst[key] = new Date(src.valueOf()); + } else if (isRegExp(src)) { + dst[key] = new RegExp(src); + } else if (src.nodeName) { + dst[key] = src.cloneNode(true); + } else if (isElement(src)) { + dst[key] = src.clone(); + } else { + if (!isObject(dst[key])) dst[key] = isArray(src) ? [] : {}; + baseExtend(dst[key], [src], true); + } + } else { + dst[key] = src; + } + } + } + + setHashKey(dst, h); + return dst; + } + /** * @ngdoc function * @name angular.extend * @module ng - * @function + * @kind function * * @description - * Extends the destination object `dst` by copying all of the properties from the `src` object(s) - * to `dst`. You can specify multiple `src` objects. + * Extends the destination object `dst` by copying own enumerable properties from the `src` object(s) + * to `dst`. You can specify multiple `src` objects. If you want to preserve original objects, you can do so + * by passing an empty object as the target: `var object = angular.extend({}, object1, object2)`. + * + * **Note:** Keep in mind that `angular.extend` does not support recursive merge (deep copy). Use + * {@link angular.merge} for this. * * @param {Object} dst Destination object. * @param {...Object} src Source object(s). * @returns {Object} Reference to `dst`. */ function extend(dst) { - var h = dst.$$hashKey; - forEach(arguments, function(obj){ - if (obj !== dst) { - forEach(obj, function(value, key){ - dst[key] = value; - }); - } - }); + return baseExtend(dst, slice.call(arguments, 1), false); + } - setHashKey(dst,h); - return dst; + + /** + * @ngdoc function + * @name angular.merge + * @module ng + * @kind function + * + * @description + * Deeply extends the destination object `dst` by copying own enumerable properties from the `src` object(s) + * to `dst`. You can specify multiple `src` objects. If you want to preserve original objects, you can do so + * by passing an empty object as the target: `var object = angular.merge({}, object1, object2)`. + * + * Unlike {@link angular.extend extend()}, `merge()` recursively descends into object properties of source + * objects, performing a deep copy. + * + * @deprecated + * sinceVersion="1.6.5" + * This function is deprecated, but will not be removed in the 1.x lifecycle. + * There are edge cases (see {@link angular.merge#known-issues known issues}) that are not + * supported by this function. We suggest + * using [lodash's merge()](https://lodash.com/docs/4.17.4#merge) instead. + * + * @knownIssue + * This is a list of (known) object types that are not handled correctly by this function: + * - [`Blob`](https://developer.mozilla.org/docs/Web/API/Blob) + * - [`MediaStream`](https://developer.mozilla.org/docs/Web/API/MediaStream) + * - [`CanvasGradient`](https://developer.mozilla.org/docs/Web/API/CanvasGradient) + * - AngularJS {@link $rootScope.Scope scopes}; + * + * @param {Object} dst Destination object. + * @param {...Object} src Source object(s). + * @returns {Object} Reference to `dst`. + */ + function merge(dst) { + return baseExtend(dst, slice.call(arguments, 1), true); } - function int(str) { + + + function toInt(str) { return parseInt(str, 10); } + var isNumberNaN = Number.isNaN || function isNumberNaN(num) { + // eslint-disable-next-line no-self-compare + return num !== num; + }; + function inherit(parent, extra) { - return extend(new (extend(function() {}, {prototype:parent}))(), extra); + return extend(Object.create(parent), extra); } /** * @ngdoc function * @name angular.noop * @module ng - * @function + * @kind function * * @description * A function that performs no operations. This function can be useful when writing code in the @@ -469,7 +614,7 @@ * @ngdoc function * @name angular.identity * @module ng - * @function + * @kind function * * @description * A function that returns its first argument. This function is useful when writing code in the @@ -477,21 +622,38 @@ * ```js function transformer(transformationFn, value) { - return (transformationFn || angular.identity)(value); - }; + return (transformationFn || angular.identity)(value); + }; + + // E.g. + function getResult(fn, input) { + return (fn || angular.identity)(input); + }; + + getResult(function(n) { return n * 2; }, 21); // returns 42 + getResult(null, 21); // returns 21 + getResult(undefined, 21); // returns 21 ``` + * + * @param {*} value to be returned. + * @returns {*} the value passed in. */ function identity($) {return $;} identity.$inject = []; - function valueFn(value) {return function() {return value;};} + function valueFn(value) {return function valueRef() {return value;};} + + function hasCustomToString(obj) { + return isFunction(obj.toString) && obj.toString !== toString; + } + /** * @ngdoc function * @name angular.isUndefined * @module ng - * @function + * @kind function * * @description * Determines if a reference is undefined. @@ -499,14 +661,14 @@ * @param {*} value Reference to check. * @returns {boolean} True if `value` is undefined. */ - function isUndefined(value){return typeof value === 'undefined';} + function isUndefined(value) {return typeof value === 'undefined';} /** * @ngdoc function * @name angular.isDefined * @module ng - * @function + * @kind function * * @description * Determines if a reference is defined. @@ -514,14 +676,14 @@ * @param {*} value Reference to check. * @returns {boolean} True if `value` is defined. */ - function isDefined(value){return typeof value !== 'undefined';} + function isDefined(value) {return typeof value !== 'undefined';} /** * @ngdoc function * @name angular.isObject * @module ng - * @function + * @kind function * * @description * Determines if a reference is an `Object`. Unlike `typeof` in JavaScript, `null`s are not @@ -530,14 +692,27 @@ * @param {*} value Reference to check. * @returns {boolean} True if `value` is an `Object` but not `null`. */ - function isObject(value){return value != null && typeof value === 'object';} + function isObject(value) { + // http://jsperf.com/isobject4 + return value !== null && typeof value === 'object'; + } + + + /** + * Determine if a value is an object with a null prototype + * + * @returns {boolean} True if `value` is an `Object` with a null prototype + */ + function isBlankObject(value) { + return value !== null && typeof value === 'object' && !getPrototypeOf(value); + } /** * @ngdoc function * @name angular.isString * @module ng - * @function + * @kind function * * @description * Determines if a reference is a `String`. @@ -545,29 +720,35 @@ * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `String`. */ - function isString(value){return typeof value === 'string';} + function isString(value) {return typeof value === 'string';} /** * @ngdoc function * @name angular.isNumber * @module ng - * @function + * @kind function * * @description * Determines if a reference is a `Number`. * + * This includes the "special" numbers `NaN`, `+Infinity` and `-Infinity`. + * + * If you wish to exclude these then you can use the native + * [`isFinite'](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/isFinite) + * method. + * * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `Number`. */ - function isNumber(value){return typeof value === 'number';} + function isNumber(value) {return typeof value === 'number';} /** * @ngdoc function * @name angular.isDate * @module ng - * @function + * @kind function * * @description * Determines if a value is a date. @@ -575,7 +756,7 @@ * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `Date`. */ - function isDate(value){ + function isDate(value) { return toString.call(value) === '[object Date]'; } @@ -584,24 +765,39 @@ * @ngdoc function * @name angular.isArray * @module ng - * @function + * @kind function * * @description - * Determines if a reference is an `Array`. + * Determines if a reference is an `Array`. Alias of Array.isArray. * * @param {*} value Reference to check. * @returns {boolean} True if `value` is an `Array`. */ - function isArray(value) { - return toString.call(value) === '[object Array]'; - } + var isArray = Array.isArray; + /** + * @description + * Determines if a reference is an `Error`. + * Loosely based on https://www.npmjs.com/package/iserror + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is an `Error`. + */ + function isError(value) { + var tag = toString.call(value); + switch (tag) { + case '[object Error]': return true; + case '[object Exception]': return true; + case '[object DOMException]': return true; + default: return value instanceof Error; + } + } /** * @ngdoc function * @name angular.isFunction * @module ng - * @function + * @kind function * * @description * Determines if a reference is a `Function`. @@ -609,7 +805,7 @@ * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `Function`. */ - function isFunction(value){return typeof value === 'function';} + function isFunction(value) {return typeof value === 'function';} /** @@ -632,7 +828,7 @@ * @returns {boolean} True if `obj` is a window obj. */ function isWindow(obj) { - return obj && obj.document && obj.location && obj.alert && obj.setInterval; + return obj && obj.window === obj; } @@ -646,6 +842,11 @@ } + function isFormData(obj) { + return toString.call(obj) === '[object FormData]'; + } + + function isBlob(obj) { return toString.call(obj) === '[object Blob]'; } @@ -656,26 +857,41 @@ } - var trim = (function() { - // native trim is way faster: http://jsperf.com/angular-trim-test - // but IE doesn't have it... :-( - // TODO: we should move this into IE/ES5 polyfill - if (!String.prototype.trim) { - return function(value) { - return isString(value) ? value.replace(/^\s\s*/, '').replace(/\s\s*$/, '') : value; - }; - } - return function(value) { - return isString(value) ? value.trim() : value; - }; - })(); + function isPromiseLike(obj) { + return obj && isFunction(obj.then); + } + + + var TYPED_ARRAY_REGEXP = /^\[object (?:Uint8|Uint8Clamped|Uint16|Uint32|Int8|Int16|Int32|Float32|Float64)Array]$/; + function isTypedArray(value) { + return value && isNumber(value.length) && TYPED_ARRAY_REGEXP.test(toString.call(value)); + } + + function isArrayBuffer(obj) { + return toString.call(obj) === '[object ArrayBuffer]'; + } + + + var trim = function(value) { + return isString(value) ? value.trim() : value; + }; + +// Copied from: +// http://docs.closure-library.googlecode.com/git/local_closure_goog_string_string.js.source.html#line1021 +// Prereq: s is a string. + var escapeForRegexp = function(s) { + return s + .replace(/([-()[\]{}+?*.$^|,:#=0) + var index = array.indexOf(value); + if (index >= 0) { array.splice(index, 1); - return value; - } - - function isLeafNode (node) { - if (node) { - switch (node.nodeName) { - case "OPTION": - case "PRE": - case "TITLE": - return true; - } } - return false; + return index; } /** * @ngdoc function * @name angular.copy * @module ng - * @function + * @kind function * * @description * Creates a deep copy of `source`, which should be an object or an array. * * * If no destination is supplied, a copy of the object or array is created. - * * If a destination is provided, all of its elements (for array) or properties (for objects) + * * If a destination is provided, all of its elements (for arrays) or properties (for objects) * are deleted and then all elements/properties from the source are copied to it. * * If `source` is not an object or array (inc. `null` and `undefined`), `source` is returned. - * * If `source` is identical to 'destination' an exception will be thrown. + * * If `source` is identical to `destination` an exception will be thrown. + * + *
    + *
    + * Only enumerable properties are taken into account. Non-enumerable properties (both on `source` + * and on `destination`) will be ignored. + *
    * * @param {*} source The source that will be used to make a copy. * Can be any type, including primitives, `null`, and `undefined`. @@ -804,105 +962,198 @@ * @returns {*} The copy or updated `destination`, if `destination` was specified. * * @example - + -
    +
    - Name:
    - Email:
    - Gender: male - female
    +
    +
    + Gender: +
    form = {{user | json}}
    -
    master = {{master | json}}
    +
    leader = {{leader | json}}
    + + + // Module: copyExample + angular. + module('copyExample', []). + controller('ExampleController', ['$scope', function($scope) { + $scope.leader = {}; + + $scope.reset = function() { + // Example with 1 argument + $scope.user = angular.copy($scope.leader); + }; - + $scope.reset(); + }]); */ - function copy(source, destination){ - if (isWindow(source) || isScope(source)) { - throw ngMinErr('cpws', - "Can't copy! Making copies of Window or Scope instances is not supported."); + function copy(source, destination, maxDepth) { + var stackSource = []; + var stackDest = []; + maxDepth = isValidObjectMaxDepth(maxDepth) ? maxDepth : NaN; + + if (destination) { + if (isTypedArray(destination) || isArrayBuffer(destination)) { + throw ngMinErr('cpta', 'Can\'t copy! TypedArray destination cannot be mutated.'); + } + if (source === destination) { + throw ngMinErr('cpi', 'Can\'t copy! Source and destination are identical.'); + } + + // Empty the destination object + if (isArray(destination)) { + destination.length = 0; + } else { + forEach(destination, function(value, key) { + if (key !== '$$hashKey') { + delete destination[key]; + } + }); + } + + stackSource.push(source); + stackDest.push(destination); + return copyRecurse(source, destination, maxDepth); } - if (!destination) { - destination = source; - if (source) { - if (isArray(source)) { - destination = copy(source, []); - } else if (isDate(source)) { - destination = new Date(source.getTime()); - } else if (isRegExp(source)) { - destination = new RegExp(source.source); - } else if (isObject(source)) { - destination = copy(source, {}); - } + return copyElement(source, maxDepth); + + function copyRecurse(source, destination, maxDepth) { + maxDepth--; + if (maxDepth < 0) { + return '...'; } - } else { - if (source === destination) throw ngMinErr('cpi', - "Can't copy! Source and destination are identical."); + var h = destination.$$hashKey; + var key; if (isArray(source)) { - destination.length = 0; - for ( var i = 0; i < source.length; i++) { - destination.push(copy(source[i])); + for (var i = 0, ii = source.length; i < ii; i++) { + destination.push(copyElement(source[i], maxDepth)); + } + } else if (isBlankObject(source)) { + // createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty + for (key in source) { + destination[key] = copyElement(source[key], maxDepth); + } + } else if (source && typeof source.hasOwnProperty === 'function') { + // Slow path, which must rely on hasOwnProperty + for (key in source) { + if (source.hasOwnProperty(key)) { + destination[key] = copyElement(source[key], maxDepth); + } } } else { - var h = destination.$$hashKey; - forEach(destination, function(value, key){ - delete destination[key]; - }); - for ( var key in source) { - destination[key] = copy(source[key]); + // Slowest path --- hasOwnProperty can't be called as a method + for (key in source) { + if (hasOwnProperty.call(source, key)) { + destination[key] = copyElement(source[key], maxDepth); + } } - setHashKey(destination,h); } + setHashKey(destination, h); + return destination; } - return destination; - } - /** - * Create a shallow copy of an object - */ - function shallowCopy(src, dst) { - dst = dst || {}; + function copyElement(source, maxDepth) { + // Simple values + if (!isObject(source)) { + return source; + } + + // Already copied values + var index = stackSource.indexOf(source); + if (index !== -1) { + return stackDest[index]; + } - for(var key in src) { - // shallowCopy is only ever called by $compile nodeLinkFn, which has control over src - // so we don't need to worry about using our custom hasOwnProperty here - if (src.hasOwnProperty(key) && !(key.charAt(0) === '$' && key.charAt(1) === '$')) { - dst[key] = src[key]; + if (isWindow(source) || isScope(source)) { + throw ngMinErr('cpws', + 'Can\'t copy! Making copies of Window or Scope instances is not supported.'); } + + var needsRecurse = false; + var destination = copyType(source); + + if (destination === undefined) { + destination = isArray(source) ? [] : Object.create(getPrototypeOf(source)); + needsRecurse = true; + } + + stackSource.push(source); + stackDest.push(destination); + + return needsRecurse + ? copyRecurse(source, destination, maxDepth) + : destination; } - return dst; + function copyType(source) { + switch (toString.call(source)) { + case '[object Int8Array]': + case '[object Int16Array]': + case '[object Int32Array]': + case '[object Float32Array]': + case '[object Float64Array]': + case '[object Uint8Array]': + case '[object Uint8ClampedArray]': + case '[object Uint16Array]': + case '[object Uint32Array]': + return new source.constructor(copyElement(source.buffer), source.byteOffset, source.length); + + case '[object ArrayBuffer]': + // Support: IE10 + if (!source.slice) { + // If we're in this case we know the environment supports ArrayBuffer + /* eslint-disable no-undef */ + var copied = new ArrayBuffer(source.byteLength); + new Uint8Array(copied).set(new Uint8Array(source)); + /* eslint-enable */ + return copied; + } + return source.slice(0); + + case '[object Boolean]': + case '[object Number]': + case '[object String]': + case '[object Date]': + return new source.constructor(source.valueOf()); + + case '[object RegExp]': + var re = new RegExp(source.source, source.toString().match(/[^/]*$/)[0]); + re.lastIndex = source.lastIndex; + return re; + + case '[object Blob]': + return new source.constructor([source], {type: source.type}); + } + + if (isFunction(source.cloneNode)) { + return source.cloneNode(true); + } + } } +// eslint-disable-next-line no-self-compare + function simpleCompare(a, b) { return a === b || (a !== a && b !== b); } + + /** * @ngdoc function * @name angular.equals * @module ng - * @function + * @kind function * * @description * Determines if two objects or two values are equivalent. Supports value types, regular @@ -914,7 +1165,7 @@ * * Both objects or values are of the same type and all of their properties are equal by * comparing them with `angular.equals`. * * Both values are NaN. (In JavaScript, NaN == NaN => false. But we consider two NaN as equal) - * * Both values represent the same regular expression (In JavasScript, + * * Both values represent the same regular expression (In JavaScript, * /abc/ == /abc/ => false. But we consider two regular expressions as equal when their textual * representation matches). * @@ -926,54 +1177,171 @@ * @param {*} o1 Object or value to compare. * @param {*} o2 Object or value to compare. * @returns {boolean} True if arguments are equal. - */ - function equals(o1, o2) { - if (o1 === o2) return true; - if (o1 === null || o2 === null) return false; - if (o1 !== o1 && o2 !== o2) return true; // NaN === NaN - var t1 = typeof o1, t2 = typeof o2, length, key, keySet; - if (t1 == t2) { - if (t1 == 'object') { - if (isArray(o1)) { - if (!isArray(o2)) return false; - if ((length = o1.length) == o2.length) { - for(key=0; key + +
    +
    +

    User 1

    + Name: + Age: + +

    User 2

    + Name: + Age: + +
    +
    + +
    + User 1:
    {{user1 | json}}
    + User 2:
    {{user2 | json}}
    + Equal:
    {{result}}
    +
    +
    +
    + + angular.module('equalsExample', []).controller('ExampleController', ['$scope', function($scope) { + $scope.user1 = {}; + $scope.user2 = {}; + $scope.compare = function() { + $scope.result = angular.equals($scope.user1, $scope.user2); + }; + }]); + + + */ + function equals(o1, o2) { + if (o1 === o2) return true; + if (o1 === null || o2 === null) return false; + // eslint-disable-next-line no-self-compare + if (o1 !== o1 && o2 !== o2) return true; // NaN === NaN + var t1 = typeof o1, t2 = typeof o2, length, key, keySet; + if (t1 === t2 && t1 === 'object') { + if (isArray(o1)) { + if (!isArray(o2)) return false; + if ((length = o1.length) === o2.length) { + for (key = 0; key < length; key++) { if (!equals(o1[key], o2[key])) return false; - keySet[key] = true; - } - for(key in o2) { - if (!keySet.hasOwnProperty(key) && - key.charAt(0) !== '$' && - o2[key] !== undefined && - !isFunction(o2[key])) return false; } return true; } + } else if (isDate(o1)) { + if (!isDate(o2)) return false; + return simpleCompare(o1.getTime(), o2.getTime()); + } else if (isRegExp(o1)) { + if (!isRegExp(o2)) return false; + return o1.toString() === o2.toString(); + } else { + if (isScope(o1) || isScope(o2) || isWindow(o1) || isWindow(o2) || + isArray(o2) || isDate(o2) || isRegExp(o2)) return false; + keySet = createMap(); + for (key in o1) { + if (key.charAt(0) === '$' || isFunction(o1[key])) continue; + if (!equals(o1[key], o2[key])) return false; + keySet[key] = true; + } + for (key in o2) { + if (!(key in keySet) && + key.charAt(0) !== '$' && + isDefined(o2[key]) && + !isFunction(o2[key])) return false; + } + return true; } } return false; } + var csp = function() { + if (!isDefined(csp.rules)) { - function csp() { - return (document.securityPolicy && document.securityPolicy.isActive) || - (document.querySelector && - !!(document.querySelector('[ng-csp]') || document.querySelector('[data-ng-csp]'))); - } + var ngCspElement = (window.document.querySelector('[ng-csp]') || + window.document.querySelector('[data-ng-csp]')); + + if (ngCspElement) { + var ngCspAttribute = ngCspElement.getAttribute('ng-csp') || + ngCspElement.getAttribute('data-ng-csp'); + csp.rules = { + noUnsafeEval: !ngCspAttribute || (ngCspAttribute.indexOf('no-unsafe-eval') !== -1), + noInlineStyle: !ngCspAttribute || (ngCspAttribute.indexOf('no-inline-style') !== -1) + }; + } else { + csp.rules = { + noUnsafeEval: noUnsafeEval(), + noInlineStyle: false + }; + } + } + + return csp.rules; + + function noUnsafeEval() { + try { + // eslint-disable-next-line no-new, no-new-func + new Function(''); + return false; + } catch (e) { + return true; + } + } + }; + + /** + * @ngdoc directive + * @module ng + * @name ngJq + * + * @element ANY + * @param {string=} ngJq the name of the library available under `window` + * to be used for angular.element + * @description + * Use this directive to force the angular.element library. This should be + * used to force either jqLite by leaving ng-jq blank or setting the name of + * the jquery variable under window (eg. jQuery). + * + * Since AngularJS looks for this directive when it is loaded (doesn't wait for the + * DOMContentLoaded event), it must be placed on an element that comes before the script + * which loads angular. Also, only the first instance of `ng-jq` will be used and all + * others ignored. + * + * @example + * This example shows how to force jqLite using the `ngJq` directive to the `html` tag. + ```html + + + ... + ... + + ``` + * @example + * This example shows how to use a jQuery based library of a different name. + * The library name must be available at the top most 'window'. + ```html + + + ... + ... + + ``` + */ + var jq = function() { + if (isDefined(jq.name_)) return jq.name_; + var el; + var i, ii = ngAttrPrefixes.length, prefix, name; + for (i = 0; i < ii; ++i) { + prefix = ngAttrPrefixes[i]; + el = window.document.querySelector('[' + prefix.replace(':', '\\:') + 'jq]'); + if (el) { + name = el.getAttribute(prefix + 'jq'); + break; + } + } + + return (jq.name_ = name); + }; function concat(array1, array2, index) { return array1.concat(slice.call(array2, index)); @@ -984,12 +1352,11 @@ } - /* jshint -W101 */ /** * @ngdoc function * @name angular.bind * @module ng - * @function + * @kind function * * @description * Returns a function which calls function `fn` bound to `self` (`self` becomes the `this` for @@ -1002,23 +1369,22 @@ * @param {...*} args Optional arguments to be prebound to the `fn` function call. * @returns {function()} Function that wraps the `fn` with all the specified bindings. */ - /* jshint +W101 */ function bind(self, fn) { var curryArgs = arguments.length > 2 ? sliceArgs(arguments, 2) : []; if (isFunction(fn) && !(fn instanceof RegExp)) { return curryArgs.length ? function() { - return arguments.length - ? fn.apply(self, curryArgs.concat(slice.call(arguments, 0))) - : fn.apply(self, curryArgs); - } + return arguments.length + ? fn.apply(self, concat(curryArgs, arguments, 0)) + : fn.apply(self, curryArgs); + } : function() { - return arguments.length - ? fn.apply(self, arguments) - : fn.call(self); - }; + return arguments.length + ? fn.apply(self, arguments) + : fn.call(self); + }; } else { - // in IE, native methods are not functions so they cannot be bound (note: they don't need to be) + // In IE, native methods are not functions so they cannot be bound (note: they don't need to be). return fn; } } @@ -1027,11 +1393,11 @@ function toJsonReplacer(key, value) { var val = value; - if (typeof key === 'string' && key.charAt(0) === '$') { + if (typeof key === 'string' && key.charAt(0) === '$' && key.charAt(1) === '$') { val = undefined; } else if (isWindow(value)) { val = '$WINDOW'; - } else if (value && document === value) { + } else if (value && window.document === value) { val = '$DOCUMENT'; } else if (isScope(value)) { val = '$SCOPE'; @@ -1045,19 +1411,44 @@ * @ngdoc function * @name angular.toJson * @module ng - * @function + * @kind function * * @description - * Serializes input into a JSON-formatted string. Properties with leading $ characters will be - * stripped since angular uses this notation internally. + * Serializes input into a JSON-formatted string. Properties with leading $$ characters will be + * stripped since AngularJS uses this notation internally. * - * @param {Object|Array|Date|string|number} obj Input to be serialized into JSON. - * @param {boolean=} pretty If set to true, the JSON output will contain newlines and whitespace. + * @param {Object|Array|Date|string|number|boolean} obj Input to be serialized into JSON. + * @param {boolean|number} [pretty=2] If set to true, the JSON output will contain newlines and whitespace. + * If set to an integer, the JSON output will contain that many spaces per indentation. * @returns {string|undefined} JSON-ified string representing `obj`. + * @knownIssue + * + * The Safari browser throws a `RangeError` instead of returning `null` when it tries to stringify a `Date` + * object with an invalid date value. The only reliable way to prevent this is to monkeypatch the + * `Date.prototype.toJSON` method as follows: + * + * ``` + * var _DatetoJSON = Date.prototype.toJSON; + * Date.prototype.toJSON = function() { + * try { + * return _DatetoJSON.call(this); + * } catch(e) { + * if (e instanceof RangeError) { + * return null; + * } + * throw e; + * } + * }; + * ``` + * + * See https://github.com/angular/angular.js/pull/14221 for more information. */ function toJson(obj, pretty) { - if (typeof obj === 'undefined') return undefined; - return JSON.stringify(obj, toJsonReplacer, pretty ? ' ' : null); + if (isUndefined(obj)) return undefined; + if (!isNumber(pretty)) { + pretty = pretty ? 2 : null; + } + return JSON.stringify(obj, toJsonReplacer, pretty); } @@ -1065,13 +1456,13 @@ * @ngdoc function * @name angular.fromJson * @module ng - * @function + * @kind function * * @description * Deserializes a JSON string. * * @param {string} json JSON string to deserialize. - * @returns {Object|Array|string|number} Deserialized thingy. + * @returns {Object|Array|string|number} Deserialized JSON string. */ function fromJson(json) { return isString(json) @@ -1080,37 +1471,43 @@ } - function toBoolean(value) { - if (typeof value === 'function') { - value = true; - } else if (value && value.length !== 0) { - var v = lowercase("" + value); - value = !(v == 'f' || v == '0' || v == 'false' || v == 'no' || v == 'n' || v == '[]'); - } else { - value = false; - } - return value; + var ALL_COLONS = /:/g; + function timezoneToOffset(timezone, fallback) { + // Support: IE 9-11 only, Edge 13-15+ + // IE/Edge do not "understand" colon (`:`) in timezone + timezone = timezone.replace(ALL_COLONS, ''); + var requestedTimezoneOffset = Date.parse('Jan 01, 1970 00:00:00 ' + timezone) / 60000; + return isNumberNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset; + } + + + function addDateMinutes(date, minutes) { + date = new Date(date.getTime()); + date.setMinutes(date.getMinutes() + minutes); + return date; } + + function convertTimezoneToLocal(date, timezone, reverse) { + reverse = reverse ? -1 : 1; + var dateTimezoneOffset = date.getTimezoneOffset(); + var timezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset); + return addDateMinutes(date, reverse * (timezoneOffset - dateTimezoneOffset)); + } + + /** * @returns {string} Returns the string representation of the element. */ function startingTag(element) { - element = jqLite(element).clone(); - try { - // turns out IE does not let you set .html() on elements which - // are not allowed to have children. So we just ignore it. - element.empty(); - } catch(e) {} - // As Per DOM Standards - var TEXT_NODE = 3; + element = jqLite(element).clone().empty(); var elemHtml = jqLite('
    ').append(element).html(); try { - return element[0].nodeType === TEXT_NODE ? lowercase(elemHtml) : + return element[0].nodeType === NODE_TYPE_TEXT ? lowercase(elemHtml) : elemHtml. - match(/^(<[^>]+>)/)[1]. - replace(/^<([\w\-]+)/, function(match, nodeName) { return '<' + lowercase(nodeName); }); - } catch(e) { + match(/^(<[^>]+>)/)[1]. + replace(/^<([\w-]+)/, function(match, nodeName) {return '<' + lowercase(nodeName);}); + } catch (e) { return lowercase(elemHtml); } @@ -1130,8 +1527,8 @@ function tryDecodeURIComponent(value) { try { return decodeURIComponent(value); - } catch(e) { - // Ignore any invalid uri component + } catch (e) { + // Ignore any invalid uri component. } } @@ -1141,16 +1538,22 @@ * @returns {Object.} */ function parseKeyValue(/**string*/keyValue) { - var obj = {}, key_value, key; - forEach((keyValue || "").split('&'), function(keyValue){ - if ( keyValue ) { - key_value = keyValue.split('='); - key = tryDecodeURIComponent(key_value[0]); - if ( isDefined(key) ) { - var val = isDefined(key_value[1]) ? tryDecodeURIComponent(key_value[1]) : true; - if (!obj[key]) { + var obj = {}; + forEach((keyValue || '').split('&'), function(keyValue) { + var splitPoint, key, val; + if (keyValue) { + key = keyValue = keyValue.replace(/\+/g,'%20'); + splitPoint = keyValue.indexOf('='); + if (splitPoint !== -1) { + key = keyValue.substring(0, splitPoint); + val = keyValue.substring(splitPoint + 1); + } + key = tryDecodeURIComponent(key); + if (isDefined(key)) { + val = isDefined(val) ? tryDecodeURIComponent(val) : true; + if (!hasOwnProperty.call(obj, key)) { obj[key] = val; - } else if(isArray(obj[key])) { + } else if (isArray(obj[key])) { obj[key].push(val); } else { obj[key] = [obj[key],val]; @@ -1167,11 +1570,11 @@ if (isArray(value)) { forEach(value, function(arrayValue) { parts.push(encodeUriQuery(key, true) + - (arrayValue === true ? '' : '=' + encodeUriQuery(arrayValue, true))); + (arrayValue === true ? '' : '=' + encodeUriQuery(arrayValue, true))); }); } else { parts.push(encodeUriQuery(key, true) + - (value === true ? '' : '=' + encodeUriQuery(value, true))); + (value === true ? '' : '=' + encodeUriQuery(value, true))); } }); return parts.length ? parts.join('&') : ''; @@ -1191,9 +1594,9 @@ */ function encodeUriSegment(val) { return encodeUriQuery(val, true). - replace(/%26/gi, '&'). - replace(/%3D/gi, '='). - replace(/%2B/gi, '+'); + replace(/%26/gi, '&'). + replace(/%3D/gi, '='). + replace(/%2B/gi, '+'); } @@ -1201,7 +1604,7 @@ * This method is intended for encoding *key* or *value* parts of query component. We need a custom * method because encodeURIComponent is too aggressive and encodes stuff that doesn't have to be * encoded per http://tools.ietf.org/html/rfc3986: - * query = *( pchar / "/" / "?" ) + * query = *( pchar / "/" / "?" ) * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" * pct-encoded = "%" HEXDIG HEXDIG @@ -1210,13 +1613,78 @@ */ function encodeUriQuery(val, pctEncodeSpaces) { return encodeURIComponent(val). - replace(/%40/gi, '@'). - replace(/%3A/gi, ':'). - replace(/%24/g, '$'). - replace(/%2C/gi, ','). - replace(/%20/g, (pctEncodeSpaces ? '%20' : '+')); + replace(/%40/gi, '@'). + replace(/%3A/gi, ':'). + replace(/%24/g, '$'). + replace(/%2C/gi, ','). + replace(/%3B/gi, ';'). + replace(/%20/g, (pctEncodeSpaces ? '%20' : '+')); + } + + var ngAttrPrefixes = ['ng-', 'data-ng-', 'ng:', 'x-ng-']; + + function getNgAttribute(element, ngAttr) { + var attr, i, ii = ngAttrPrefixes.length; + for (i = 0; i < ii; ++i) { + attr = ngAttrPrefixes[i] + ngAttr; + if (isString(attr = element.getAttribute(attr))) { + return attr; + } + } + return null; + } + + function allowAutoBootstrap(document) { + var script = document.currentScript; + + if (!script) { + // Support: IE 9-11 only + // IE does not have `document.currentScript` + return true; + } + + // If the `currentScript` property has been clobbered just return false, since this indicates a probable attack + if (!(script instanceof window.HTMLScriptElement || script instanceof window.SVGScriptElement)) { + return false; + } + + var attributes = script.attributes; + var srcs = [attributes.getNamedItem('src'), attributes.getNamedItem('href'), attributes.getNamedItem('xlink:href')]; + + return srcs.every(function(src) { + if (!src) { + return true; + } + if (!src.value) { + return false; + } + + var link = document.createElement('a'); + link.href = src.value; + + if (document.location.origin === link.origin) { + // Same-origin resources are always allowed, even for non-whitelisted schemes. + return true; + } + // Disabled bootstrapping unless angular.js was loaded from a known scheme used on the web. + // This is to prevent angular.js bundled with browser extensions from being used to bypass the + // content security policy in web pages and other browser extensions. + switch (link.protocol) { + case 'http:': + case 'https:': + case 'ftp:': + case 'blob:': + case 'file:': + case 'data:': + return true; + default: + return false; + } + }); } +// Cached as it has to run during loading so that document.currentScript is available. + var isAutoBootstrapAllowed = allowAutoBootstrap(window.document); /** * @ngdoc directive @@ -1226,6 +1694,11 @@ * @element ANY * @param {angular.Module} ngApp an optional application * {@link angular.module module} name to load. + * @param {boolean=} ngStrictDi if this attribute is present on the app element, the injector will be + * created in "strict-di" mode. This means that the application will fail to invoke functions which + * do not use explicit function annotation (and are thus unsuitable for minification), as described + * in {@link guide/di the Dependency Injection guide}, and useful debugging info will assist in + * tracking down the root of these bugs. * * @description * @@ -1233,13 +1706,20 @@ * designates the **root element** of the application and is typically placed near the root element * of the page - e.g. on the `` or `` tags. * - * Only one AngularJS application can be auto-bootstrapped per HTML document. The first `ngApp` - * found in the document will be used to define the root element to auto-bootstrap as an - * application. To run multiple applications in an HTML document you must manually bootstrap them using - * {@link angular.bootstrap} instead. AngularJS applications cannot be nested within each other. + * There are a few things to keep in mind when using `ngApp`: + * - only one AngularJS application can be auto-bootstrapped per HTML document. The first `ngApp` + * found in the document will be used to define the root element to auto-bootstrap as an + * application. To run multiple applications in an HTML document you must manually bootstrap them using + * {@link angular.bootstrap} instead. + * - AngularJS applications cannot be nested within each other. + * - Do not use a directive that uses {@link ng.$compile#transclusion transclusion} on the same element as `ngApp`. + * This includes directives such as {@link ng.ngIf `ngIf`}, {@link ng.ngInclude `ngInclude`} and + * {@link ngRoute.ngView `ngView`}. + * Doing this misplaces the app {@link ng.$rootElement `$rootElement`} and the app's {@link auto.$injector injector}, + * causing animations to stop working and making the injector inaccessible from outside the app. * * You can specify an **AngularJS module** to be used as the root module for the application. This - * module will be loaded into the {@link auto.$injector} when the application is bootstrapped and + * module will be loaded into the {@link auto.$injector} when the application is bootstrapped. It * should contain the application code needed or have dependencies on other modules that will * contain the code. See {@link angular.module} for more information. * @@ -1247,9 +1727,13 @@ * document would not be compiled, the `AppController` would not be instantiated and the `{{ a+b }}` * would not be resolved to `3`. * - * `ngApp` is the easiest, and most common, way to bootstrap an application. + * @example + * + * ### Simple Usage + * + * `ngApp` is the easiest, and most common way to bootstrap an application. * - +
    I can add: {{a}} + {{b}} = {{ a+b }} @@ -1263,48 +1747,118 @@ * + * @example + * + * ### With `ngStrictDi` + * + * Using `ngStrictDi`, you would see something like this: + * + + +
    +
    + I can add: {{a}} + {{b}} = {{ a+b }} + +

    This renders because the controller does not fail to + instantiate, by using explicit annotation style (see + script.js for details) +

    +
    + +
    + Name:
    + Hello, {{name}}! + +

    This renders because the controller does not fail to + instantiate, by using explicit annotation style + (see script.js for details) +

    +
    + +
    + I can add: {{a}} + {{b}} = {{ a+b }} + +

    The controller could not be instantiated, due to relying + on automatic function annotations (which are disabled in + strict mode). As such, the content of this section is not + interpolated, and there should be an error in your web console. +

    +
    +
    +
    + + angular.module('ngAppStrictDemo', []) + // BadController will fail to instantiate, due to relying on automatic function annotation, + // rather than an explicit annotation + .controller('BadController', function($scope) { + $scope.a = 1; + $scope.b = 2; + }) + // Unlike BadController, GoodController1 and GoodController2 will not fail to be instantiated, + // due to using explicit annotations using the array style and $inject property, respectively. + .controller('GoodController1', ['$scope', function($scope) { + $scope.a = 1; + $scope.b = 2; + }]) + .controller('GoodController2', GoodController2); + function GoodController2($scope) { + $scope.name = 'World'; + } + GoodController2.$inject = ['$scope']; + + + div[ng-controller] { + margin-bottom: 1em; + -webkit-border-radius: 4px; + border-radius: 4px; + border: 1px solid; + padding: .5em; + } + div[ng-controller^=Good] { + border-color: #d6e9c6; + background-color: #dff0d8; + color: #3c763d; + } + div[ng-controller^=Bad] { + border-color: #ebccd1; + background-color: #f2dede; + color: #a94442; + margin-bottom: 0; + } + +
    */ function angularInit(element, bootstrap) { - var elements = [element], - appElement, + var appElement, module, - names = ['ng:app', 'ng-app', 'x-ng-app', 'data-ng-app'], - NG_APP_CLASS_REGEXP = /\sng[:\-]app(:\s*([\w\d_]+);?)?\s/; + config = {}; - function append(element) { - element && elements.push(element); - } + // The element `element` has priority over any other element. + forEach(ngAttrPrefixes, function(prefix) { + var name = prefix + 'app'; - forEach(names, function(name) { - names[name] = true; - append(document.getElementById(name)); - name = name.replace(':', '\\:'); - if (element.querySelectorAll) { - forEach(element.querySelectorAll('.' + name), append); - forEach(element.querySelectorAll('.' + name + '\\:'), append); - forEach(element.querySelectorAll('[' + name + ']'), append); + if (!appElement && element.hasAttribute && element.hasAttribute(name)) { + appElement = element; + module = element.getAttribute(name); } }); + forEach(ngAttrPrefixes, function(prefix) { + var name = prefix + 'app'; + var candidate; - forEach(elements, function(element) { - if (!appElement) { - var className = ' ' + element.className + ' '; - var match = NG_APP_CLASS_REGEXP.exec(className); - if (match) { - appElement = element; - module = (match[2] || '').replace(/\s+/g, ','); - } else { - forEach(element.attributes, function(attr) { - if (!appElement && names[attr.name]) { - appElement = element; - module = attr.value; - } - }); - } + if (!appElement && (candidate = element.querySelector('[' + name.replace(':', '\\:') + ']'))) { + appElement = candidate; + module = candidate.getAttribute(name); } }); if (appElement) { - bootstrap(appElement, module ? [module] : []); + if (!isAutoBootstrapAllowed) { + window.console.error('AngularJS: disabling automatic bootstrap. - *
    - * - * - * - * - * - * - * - *
    {{heading}}
    {{fill}}
    + * ```html + * + * + * + *
    + * {{greeting}} *
    - * - * - * var app = angular.module('multi-bootstrap', []) * - * .controller('BrokenTable', function($scope) { - * $scope.headings = ['One', 'Two', 'Three']; - * $scope.fillings = [[1, 2, 3], ['A', 'B', 'C'], [7, 8, 9]]; - * }); - * - * - * it('should only insert one table cell for each item in $scope.fillings', function() { - * expect(element.all(by.css('td')).count()) - * .toBe(9); - * }); - * - * + * + * + * + * + * ``` * - * @param {DOMElement} element DOM element which is the root of angular application. + * @param {DOMElement} element DOM element which is the root of AngularJS application. * @param {Array=} modules an array of modules to load into the application. * Each item in the array should be the name of a predefined module or a (DI annotated) - * function that will be invoked by the injector as a run block. + * function that will be invoked by the injector as a `config` block. * See: {@link angular.module modules} + * @param {Object=} config an object for defining configuration options for the application. The + * following keys are supported: + * + * * `strictDi` - disable automatic function annotation for the application. This is meant to + * assist in finding bugs which break minified code. Defaults to `false`. + * * @returns {auto.$injector} Returns the newly created injector for this app. */ - function bootstrap(element, modules) { + function bootstrap(element, modules, config) { + if (!isObject(config)) config = {}; + var defaultConfig = { + strictDi: false + }; + config = extend(defaultConfig, config); var doBootstrap = function() { element = jqLite(element); if (element.injector()) { - var tag = (element[0] === document) ? 'document' : startingTag(element); - throw ngMinErr('btstrpd', "App Already Bootstrapped with this Element '{0}'", tag); + var tag = (element[0] === window.document) ? 'document' : startingTag(element); + // Encode angle brackets to prevent input from being sanitized to empty string #8683. + throw ngMinErr( + 'btstrpd', + 'App already bootstrapped with this element \'{0}\'', + tag.replace(//,'>')); } modules = modules || []; modules.unshift(['$provide', function($provide) { $provide.value('$rootElement', element); }]); + + if (config.debugInfoEnabled) { + // Pushing so that this overrides `debugInfoEnabled` setting defined in user's `modules`. + modules.push(['$compileProvider', function($compileProvider) { + $compileProvider.debugInfoEnabled(true); + }]); + } + modules.unshift('ng'); - var injector = createInjector(modules); - injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', '$animate', - function(scope, element, compile, injector, animate) { - scope.$apply(function() { - element.data('$injector', injector); - compile(element)(scope); - }); - }] + var injector = createInjector(modules, config.strictDi); + injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', + function bootstrapApply(scope, element, compile, injector) { + scope.$apply(function() { + element.data('$injector', injector); + compile(element)(scope); + }); + }] ); return injector; }; + var NG_ENABLE_DEBUG_INFO = /^NG_ENABLE_DEBUG_INFO!/; var NG_DEFER_BOOTSTRAP = /^NG_DEFER_BOOTSTRAP!/; + if (window && NG_ENABLE_DEBUG_INFO.test(window.name)) { + config.debugInfoEnabled = true; + window.name = window.name.replace(NG_ENABLE_DEBUG_INFO, ''); + } + if (window && !NG_DEFER_BOOTSTRAP.test(window.name)) { return doBootstrap(); } @@ -1399,40 +1981,104 @@ forEach(extraModules, function(module) { modules.push(module); }); - doBootstrap(); + return doBootstrap(); }; + + if (isFunction(angular.resumeDeferredBootstrap)) { + angular.resumeDeferredBootstrap(); + } + } + + /** + * @ngdoc function + * @name angular.reloadWithDebugInfo + * @module ng + * @description + * Use this function to reload the current application with debug information turned on. + * This takes precedence over a call to `$compileProvider.debugInfoEnabled(false)`. + * + * See {@link ng.$compileProvider#debugInfoEnabled} for more. + */ + function reloadWithDebugInfo() { + window.name = 'NG_ENABLE_DEBUG_INFO!' + window.name; + window.location.reload(); + } + + /** + * @name angular.getTestability + * @module ng + * @description + * Get the testability service for the instance of AngularJS on the given + * element. + * @param {DOMElement} element DOM element which is the root of AngularJS application. + */ + function getTestability(rootElement) { + var injector = angular.element(rootElement).injector(); + if (!injector) { + throw ngMinErr('test', + 'no injector found for element argument to getTestability'); + } + return injector.get('$$testability'); } var SNAKE_CASE_REGEXP = /[A-Z]/g; - function snake_case(name, separator){ + function snake_case(name, separator) { separator = separator || '_'; return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) { return (pos ? separator : '') + letter.toLowerCase(); }); } + var bindJQueryFired = false; function bindJQuery() { + var originalCleanData; + + if (bindJQueryFired) { + return; + } + // bind to jQuery if present; - jQuery = window.jQuery; - // reset to jQuery or default to us. - if (jQuery) { + var jqName = jq(); + jQuery = isUndefined(jqName) ? window.jQuery : // use jQuery (if present) + !jqName ? undefined : // use jqLite + window[jqName]; // use jQuery specified by `ngJq` + + // Use jQuery if it exists with proper functionality, otherwise default to us. + // AngularJS 1.2+ requires jQuery 1.7+ for on()/off() support. + // AngularJS 1.3+ technically requires at least jQuery 2.1+ but it may work with older + // versions. It will not work for sure with jQuery <1.7, though. + if (jQuery && jQuery.fn.on) { jqLite = jQuery; extend(jQuery.fn, { scope: JQLitePrototype.scope, isolateScope: JQLitePrototype.isolateScope, - controller: JQLitePrototype.controller, + controller: /** @type {?} */ (JQLitePrototype).controller, injector: JQLitePrototype.injector, inheritedData: JQLitePrototype.inheritedData }); - // Method signature: - // jqLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments) - jqLitePatchJQueryRemove('remove', true, true, false); - jqLitePatchJQueryRemove('empty', false, false, false); - jqLitePatchJQueryRemove('html', false, false, true); + + // All nodes removed from the DOM via various jQuery APIs like .remove() + // are passed through jQuery.cleanData. Monkey-patch this method to fire + // the $destroy event on all removed nodes. + originalCleanData = jQuery.cleanData; + jQuery.cleanData = function(elems) { + var events; + for (var i = 0, elem; (elem = elems[i]) != null; i++) { + events = jQuery._data(elem, 'events'); + if (events && events.$destroy) { + jQuery(elem).triggerHandler('$destroy'); + } + } + originalCleanData(elems); + }; } else { jqLite = JQLite; } + angular.element = jqLite; + + // Prevent double-proxying. + bindJQueryFired = true; } /** @@ -1440,7 +2086,7 @@ */ function assertArg(arg, name, reason) { if (!arg) { - throw ngMinErr('areq', "Argument '{0}' is {1}", (name || '?'), (reason || "required")); + throw ngMinErr('areq', 'Argument \'{0}\' is {1}', (name || '?'), (reason || 'required')); } return arg; } @@ -1451,7 +2097,7 @@ } assertArg(isFunction(arg), name, 'not a function, got ' + - (arg && typeof arg == 'object' ? arg.constructor.name || 'Object' : typeof arg)); + (arg && typeof arg === 'object' ? arg.constructor.name || 'Object' : typeof arg)); return arg; } @@ -1462,7 +2108,7 @@ */ function assertNotHasOwnProperty(name, context) { if (name === 'hasOwnProperty') { - throw ngMinErr('badname', "hasOwnProperty is not a valid {0} name", context); + throw ngMinErr('badname', 'hasOwnProperty is not a valid {0} name', context); } } @@ -1496,34 +2142,77 @@ /** * Return the DOM siblings between the first and last node in the given array. * @param {Array} array like object - * @returns {DOMElement} object containing the elements + * @returns {Array} the inputted object or a jqLite collection containing the nodes */ - function getBlockElements(nodes) { - var startNode = nodes[0], - endNode = nodes[nodes.length - 1]; - if (startNode === endNode) { - return jqLite(startNode); + function getBlockNodes(nodes) { + // TODO(perf): update `nodes` instead of creating a new object? + var node = nodes[0]; + var endNode = nodes[nodes.length - 1]; + var blockNodes; + + for (var i = 1; node !== endNode && (node = node.nextSibling); i++) { + if (blockNodes || nodes[i] !== node) { + if (!blockNodes) { + blockNodes = jqLite(slice.call(nodes, 0, i)); + } + blockNodes.push(node); + } } - var element = startNode; - var elements = [element]; + return blockNodes || nodes; + } + + + /** + * Creates a new object without a prototype. This object is useful for lookup without having to + * guard against prototypically inherited properties via hasOwnProperty. + * + * Related micro-benchmarks: + * - http://jsperf.com/object-create2 + * - http://jsperf.com/proto-map-lookup/2 + * - http://jsperf.com/for-in-vs-object-keys2 + * + * @returns {Object} + */ + function createMap() { + return Object.create(null); + } - do { - element = element.nextSibling; - if (!element) break; - elements.push(element); - } while (element !== endNode); + function stringify(value) { + if (value == null) { // null || undefined + return ''; + } + switch (typeof value) { + case 'string': + break; + case 'number': + value = '' + value; + break; + default: + if (hasCustomToString(value) && !isArray(value) && !isDate(value)) { + value = value.toString(); + } else { + value = toJson(value); + } + } - return jqLite(elements); + return value; } + var NODE_TYPE_ELEMENT = 1; + var NODE_TYPE_ATTRIBUTE = 2; + var NODE_TYPE_TEXT = 3; + var NODE_TYPE_COMMENT = 8; + var NODE_TYPE_DOCUMENT = 9; + var NODE_TYPE_DOCUMENT_FRAGMENT = 11; + /** * @ngdoc type * @name angular.Module * @module ng * @description * - * Interface for configuring angular {@link angular.module modules}. + * Interface for configuring AngularJS {@link angular.module modules}. */ function setupModuleLoader(window) { @@ -1550,18 +2239,18 @@ * @module ng * @description * - * The `angular.module` is a global place for creating, registering and retrieving Angular + * The `angular.module` is a global place for creating, registering and retrieving AngularJS * modules. - * All modules (angular core or 3rd party) that should be available to an application must be + * All modules (AngularJS core or 3rd party) that should be available to an application must be * registered using this mechanism. * - * When passed two or more arguments, a new module is created. If passed only one argument, an - * existing module (the name passed as the first argument to `module`) is retrieved. + * Passing one argument retrieves an existing {@link angular.Module}, + * whereas passing more than one argument creates a new {@link angular.Module} * * * # Module * - * A module is a collection of services, directives, filters, and configuration information. + * A module is a collection of services, directives, controllers, filters, and configuration information. * `angular.module` is used to configure the {@link auto.$injector $injector}. * * ```js @@ -1573,9 +2262,9 @@ * * // configure existing services inside initialization blocks. * myModule.config(['$locationProvider', function($locationProvider) { - * // Configure existing providers - * $locationProvider.hashPrefix('!'); - * }]); + * // Configure existing providers + * $locationProvider.hashPrefix('!'); + * }]); * ``` * * Then you can create an injector and load your modules like this: @@ -1589,13 +2278,16 @@ * {@link angular.bootstrap} to simplify this process for you. * * @param {!string} name The name of the module to create or retrieve. - <<<<<* @param {!Array.=} requires If specified then new module is being created. If - >>>>>* unspecified then the module is being retrieved for further configuration. - * @param {Function} configFn Optional configuration function for the module. Same as + * @param {!Array.=} requires If specified then new module is being created. If + * unspecified then the module is being retrieved for further configuration. + * @param {Function=} configFn Optional configuration function for the module. Same as * {@link angular.Module#config Module#config()}. - * @returns {module} new module with the {@link angular.Module} api. + * @returns {angular.Module} new module with the {@link angular.Module} api. */ return function module(name, requires, configFn) { + + var info = {}; + var assertNotHasOwnProperty = function(name, context) { if (name === 'hasOwnProperty') { throw ngMinErr('badname', 'hasOwnProperty is not a valid {0} name', context); @@ -1608,42 +2300,86 @@ } return ensure(modules, name, function() { if (!requires) { - throw $injectorMinErr('nomod', "Module '{0}' is not available! You either misspelled " + - "the module name or forgot to load it. If registering a module ensure that you " + - "specify the dependencies as the second argument.", name); + throw $injectorMinErr('nomod', 'Module \'{0}\' is not available! You either misspelled ' + + 'the module name or forgot to load it. If registering a module ensure that you ' + + 'specify the dependencies as the second argument.', name); } /** @type {!Array.>} */ var invokeQueue = []; + /** @type {!Array.} */ + var configBlocks = []; + /** @type {!Array.} */ var runBlocks = []; - var config = invokeLater('$injector', 'invoke'); + var config = invokeLater('$injector', 'invoke', 'push', configBlocks); /** @type {angular.Module} */ var moduleInstance = { // Private state _invokeQueue: invokeQueue, + _configBlocks: configBlocks, _runBlocks: runBlocks, /** - * @ngdoc property - * @name angular.Module#requires + * @ngdoc method + * @name angular.Module#info * @module ng - * @returns {Array.} List of module names which must be loaded before this module. + * + * @param {Object=} info Information about the module + * @returns {Object|Module} The current info object for this module if called as a getter, + * or `this` if called as a setter. + * * @description - * Holds the list of modules which the injector will load before the current module is - * loaded. + * Read and write custom information about this module. + * For example you could put the version of the module in here. + * + * ```js + * angular.module('myModule', []).info({ version: '1.0.0' }); + * ``` + * + * The version could then be read back out by accessing the module elsewhere: + * + * ``` + * var version = angular.module('myModule').info().version; + * ``` + * + * You can also retrieve this information during runtime via the + * {@link $injector#modules `$injector.modules`} property: + * + * ```js + * var version = $injector.modules['myModule'].info().version; + * ``` */ - requires: requires, - + info: function(value) { + if (isDefined(value)) { + if (!isObject(value)) throw ngMinErr('aobj', 'Argument \'{0}\' must be an object', 'value'); + info = value; + return this; + } + return info; + }, + + /** + * @ngdoc property + * @name angular.Module#requires + * @module ng + * + * @description + * Holds the list of modules which the injector will load before the current module is + * loaded. + */ + requires: requires, + /** * @ngdoc property * @name angular.Module#name * @module ng - * @returns {string} Name of the module. + * * @description + * Name of the module. */ name: name, @@ -1658,7 +2394,7 @@ * @description * See {@link auto.$provide#provider $provide.provider()}. */ - provider: invokeLater('$provide', 'provider'), + provider: invokeLaterAndSetModuleName('$provide', 'provider'), /** * @ngdoc method @@ -1669,7 +2405,7 @@ * @description * See {@link auto.$provide#factory $provide.factory()}. */ - factory: invokeLater('$provide', 'factory'), + factory: invokeLaterAndSetModuleName('$provide', 'factory'), /** * @ngdoc method @@ -1680,7 +2416,7 @@ * @description * See {@link auto.$provide#service $provide.service()}. */ - service: invokeLater('$provide', 'service'), + service: invokeLaterAndSetModuleName('$provide', 'service'), /** * @ngdoc method @@ -1700,11 +2436,23 @@ * @param {string} name constant name * @param {*} object Constant value. * @description - * Because the constant are fixed, they get applied before other provide methods. + * Because the constants are fixed, they get applied before other provide methods. * See {@link auto.$provide#constant $provide.constant()}. */ constant: invokeLater('$provide', 'constant', 'unshift'), + /** + * @ngdoc method + * @name angular.Module#decorator + * @module ng + * @param {string} name The name of the service to decorate. + * @param {Function} decorFn This function will be invoked when the service needs to be + * instantiated and should return the decorated service instance. + * @description + * See {@link auto.$provide#decorator $provide.decorator()}. + */ + decorator: invokeLaterAndSetModuleName('$provide', 'decorator', configBlocks), + /** * @ngdoc method * @name angular.Module#animation @@ -1718,37 +2466,44 @@ * * * Defines an animation hook that can be later used with - * {@link ngAnimate.$animate $animate} service and directives that use this service. + * {@link $animate $animate} service and directives that use this service. * * ```js * module.animation('.animation-name', function($inject1, $inject2) { - * return { - * eventName : function(element, done) { - * //code to run the animation - * //once complete, then run done() - * return function cancellationFunction(element) { - * //code to cancel the animation - * } - * } - * } - * }) + * return { + * eventName : function(element, done) { + * //code to run the animation + * //once complete, then run done() + * return function cancellationFunction(element) { + * //code to cancel the animation + * } + * } + * } + * }) * ``` * - * See {@link ngAnimate.$animateProvider#register $animateProvider.register()} and + * See {@link ng.$animateProvider#register $animateProvider.register()} and * {@link ngAnimate ngAnimate module} for more information. */ - animation: invokeLater('$animateProvider', 'register'), + animation: invokeLaterAndSetModuleName('$animateProvider', 'register'), /** * @ngdoc method * @name angular.Module#filter * @module ng - * @param {string} name Filter name. + * @param {string} name Filter name - this must be a valid AngularJS expression identifier * @param {Function} filterFactory Factory function for creating new instance of filter. * @description * See {@link ng.$filterProvider#register $filterProvider.register()}. + * + *
    + * **Note:** Filter names must be valid AngularJS {@link expression} identifiers, such as `uppercase` or `orderBy`. + * Names with special characters, such as hyphens and dots, are not allowed. If you wish to namespace + * your filters, then you can use capitalization (`myappSubsectionFilterx`) or underscores + * (`myapp_subsection_filterx`). + *
    */ - filter: invokeLater('$filterProvider', 'register'), + filter: invokeLaterAndSetModuleName('$filterProvider', 'register'), /** * @ngdoc method @@ -1760,7 +2515,7 @@ * @description * See {@link ng.$controllerProvider#register $controllerProvider.register()}. */ - controller: invokeLater('$controllerProvider', 'register'), + controller: invokeLaterAndSetModuleName('$controllerProvider', 'register'), /** * @ngdoc method @@ -1773,7 +2528,20 @@ * @description * See {@link ng.$compileProvider#directive $compileProvider.directive()}. */ - directive: invokeLater('$compileProvider', 'directive'), + directive: invokeLaterAndSetModuleName('$compileProvider', 'directive'), + + /** + * @ngdoc method + * @name angular.Module#component + * @module ng + * @param {string} name Name of the component in camel-case (i.e. myComp which will match as my-comp) + * @param {Object} options Component definition object (a simplified + * {@link ng.$compile#directive-definition-object directive definition object}) + * + * @description + * See {@link ng.$compileProvider#component $compileProvider.component()}. + */ + component: invokeLaterAndSetModuleName('$compileProvider', 'component'), /** * @ngdoc method @@ -1782,7 +2550,15 @@ * @param {Function} configFn Execute this function on module load. Useful for service * configuration. * @description - * Use this method to register work which needs to be performed on module loading. + * Use this method to configure services by injecting their + * {@link angular.Module#provider `providers`}, e.g. for adding routes to the + * {@link ngRoute.$routeProvider $routeProvider}. + * + * Note that you can only inject {@link angular.Module#provider `providers`} and + * {@link angular.Module#constant `constants`} into this function. + * + * For more about how to configure services, see + * {@link providers#provider-recipe Provider Recipe}. */ config: config, @@ -1806,7 +2582,7 @@ config(configFn); } - return moduleInstance; + return moduleInstance; /** * @param {string} provider @@ -1814,9 +2590,24 @@ * @param {String=} insertMethod * @returns {angular.Module} */ - function invokeLater(provider, method, insertMethod) { + function invokeLater(provider, method, insertMethod, queue) { + if (!queue) queue = invokeQueue; return function() { - invokeQueue[insertMethod || 'push']([provider, method, arguments]); + queue[insertMethod || 'push']([provider, method, arguments]); + return moduleInstance; + }; + } + + /** + * @param {string} provider + * @param {string} method + * @returns {angular.Module} + */ + function invokeLaterAndSetModuleName(provider, method, queue) { + if (!queue) queue = invokeQueue; + return function(recipeName, factoryFunction) { + if (factoryFunction && isFunction(factoryFunction)) factoryFunction.$$moduleName = name; + queue.push([provider, method, arguments]); return moduleInstance; }; } @@ -1826,82 +2617,165 @@ } - /* global - angularModule: true, - version: true, - - $LocaleProvider, - $CompileProvider, - - htmlAnchorDirective, - inputDirective, - inputDirective, - formDirective, - scriptDirective, - selectDirective, - styleDirective, - optionDirective, - ngBindDirective, - ngBindHtmlDirective, - ngBindTemplateDirective, - ngClassDirective, - ngClassEvenDirective, - ngClassOddDirective, - ngCspDirective, - ngCloakDirective, - ngControllerDirective, - ngFormDirective, - ngHideDirective, - ngIfDirective, - ngIncludeDirective, - ngIncludeFillContentDirective, - ngInitDirective, - ngNonBindableDirective, - ngPluralizeDirective, - ngRepeatDirective, - ngShowDirective, - ngStyleDirective, - ngSwitchDirective, - ngSwitchWhenDirective, - ngSwitchDefaultDirective, - ngOptionsDirective, - ngTranscludeDirective, - ngModelDirective, - ngListDirective, - ngChangeDirective, - requiredDirective, - requiredDirective, - ngValueDirective, - ngAttributeAliasDirectives, - ngEventDirectives, - - $AnchorScrollProvider, - $AnimateProvider, - $BrowserProvider, - $CacheFactoryProvider, - $ControllerProvider, - $DocumentProvider, - $ExceptionHandlerProvider, - $FilterProvider, - $InterpolateProvider, - $IntervalProvider, - $HttpProvider, - $HttpBackendProvider, - $LocationProvider, - $LogProvider, - $ParseProvider, - $RootScopeProvider, - $QProvider, - $$SanitizeUriProvider, - $SceProvider, - $SceDelegateProvider, - $SnifferProvider, - $TemplateCacheProvider, - $TimeoutProvider, - $$RAFProvider, - $$AsyncCallbackProvider, - $WindowProvider + /* global shallowCopy: true */ + + /** + * Creates a shallow copy of an object, an array or a primitive. + * + * Assumes that there are no proto properties for objects. */ + function shallowCopy(src, dst) { + if (isArray(src)) { + dst = dst || []; + + for (var i = 0, ii = src.length; i < ii; i++) { + dst[i] = src[i]; + } + } else if (isObject(src)) { + dst = dst || {}; + + for (var key in src) { + if (!(key.charAt(0) === '$' && key.charAt(1) === '$')) { + dst[key] = src[key]; + } + } + } + + return dst || src; + } + + /* exported toDebugString */ + + function serializeObject(obj, maxDepth) { + var seen = []; + + // There is no direct way to stringify object until reaching a specific depth + // and a very deep object can cause a performance issue, so we copy the object + // based on this specific depth and then stringify it. + if (isValidObjectMaxDepth(maxDepth)) { + // This file is also included in `angular-loader`, so `copy()` might not always be available in + // the closure. Therefore, it is lazily retrieved as `angular.copy()` when needed. + obj = angular.copy(obj, null, maxDepth); + } + return JSON.stringify(obj, function(key, val) { + val = toJsonReplacer(key, val); + if (isObject(val)) { + + if (seen.indexOf(val) >= 0) return '...'; + + seen.push(val); + } + return val; + }); + } + + function toDebugString(obj, maxDepth) { + if (typeof obj === 'function') { + return obj.toString().replace(/ \{[\s\S]*$/, ''); + } else if (isUndefined(obj)) { + return 'undefined'; + } else if (typeof obj !== 'string') { + return serializeObject(obj, maxDepth); + } + return obj; + } + + /* global angularModule: true, + version: true, + + $CompileProvider, + + htmlAnchorDirective, + inputDirective, + inputDirective, + formDirective, + scriptDirective, + selectDirective, + optionDirective, + ngBindDirective, + ngBindHtmlDirective, + ngBindTemplateDirective, + ngClassDirective, + ngClassEvenDirective, + ngClassOddDirective, + ngCloakDirective, + ngControllerDirective, + ngFormDirective, + ngHideDirective, + ngIfDirective, + ngIncludeDirective, + ngIncludeFillContentDirective, + ngInitDirective, + ngNonBindableDirective, + ngPluralizeDirective, + ngRepeatDirective, + ngShowDirective, + ngStyleDirective, + ngSwitchDirective, + ngSwitchWhenDirective, + ngSwitchDefaultDirective, + ngOptionsDirective, + ngTranscludeDirective, + ngModelDirective, + ngListDirective, + ngChangeDirective, + patternDirective, + patternDirective, + requiredDirective, + requiredDirective, + minlengthDirective, + minlengthDirective, + maxlengthDirective, + maxlengthDirective, + ngValueDirective, + ngModelOptionsDirective, + ngAttributeAliasDirectives, + ngEventDirectives, + + $AnchorScrollProvider, + $AnimateProvider, + $CoreAnimateCssProvider, + $$CoreAnimateJsProvider, + $$CoreAnimateQueueProvider, + $$AnimateRunnerFactoryProvider, + $$AnimateAsyncRunFactoryProvider, + $BrowserProvider, + $CacheFactoryProvider, + $ControllerProvider, + $DateProvider, + $DocumentProvider, + $$IsDocumentHiddenProvider, + $ExceptionHandlerProvider, + $FilterProvider, + $$ForceReflowProvider, + $InterpolateProvider, + $IntervalProvider, + $HttpProvider, + $HttpParamSerializerProvider, + $HttpParamSerializerJQLikeProvider, + $HttpBackendProvider, + $xhrFactoryProvider, + $jsonpCallbacksProvider, + $LocationProvider, + $LogProvider, + $$MapProvider, + $ParseProvider, + $RootScopeProvider, + $QProvider, + $$QProvider, + $$SanitizeUriProvider, + $SceProvider, + $SceDelegateProvider, + $SnifferProvider, + $TemplateCacheProvider, + $TemplateRequestProvider, + $$TestabilityProvider, + $TimeoutProvider, + $$RAFProvider, + $WindowProvider, + $$jqLiteProvider, + $$CookieReaderProvider +*/ /** @@ -1909,8 +2783,9 @@ * @name angular.version * @module ng * @description - * An object that contains information about the current AngularJS version. This object has the - * following properties: + * An object that contains information about the current AngularJS version. + * + * This object has the following properties: * * - `full` – `{string}` – Full version string, such as "0.9.18". * - `major` – `{number}` – Major version number, such as "0". @@ -1919,28 +2794,32 @@ * - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat". */ var version = { - full: '1.2.16', // all of these placeholder strings will be replaced by grunt's - major: 1, // package task - minor: 2, - dot: 16, - codeName: 'badger-enumeration' + // These placeholder strings will be replaced by grunt's `build` task. + // They need to be double- or single-quoted. + full: '1.6.9', + major: 1, + minor: 6, + dot: 9, + codeName: 'fiery-basilisk' }; - function publishExternalAPI(angular){ + function publishExternalAPI(angular) { extend(angular, { + 'errorHandlingConfig': errorHandlingConfig, 'bootstrap': bootstrap, 'copy': copy, 'extend': extend, + 'merge': merge, 'equals': equals, 'element': jqLite, 'forEach': forEach, 'injector': createInjector, - 'noop':noop, - 'bind':bind, + 'noop': noop, + 'bind': bind, 'toJson': toJson, 'fromJson': fromJson, - 'identity':identity, + 'identity': identity, 'isUndefined': isUndefined, 'isDefined': isDefined, 'isString': isString, @@ -1953,17 +2832,17 @@ 'isDate': isDate, 'lowercase': lowercase, 'uppercase': uppercase, - 'callbacks': {counter: 0}, + 'callbacks': {$$counter: 0}, + 'getTestability': getTestability, + 'reloadWithDebugInfo': reloadWithDebugInfo, '$$minErr': minErr, - '$$csp': csp + '$$csp': csp, + '$$encodeUriSegment': encodeUriSegment, + '$$encodeUriQuery': encodeUriQuery, + '$$stringify': stringify }); angularModule = setupModuleLoader(window); - try { - angularModule('ngLocale'); - } catch (e) { - angularModule('ngLocale', []).provider('$locale', $LocaleProvider); - } angularModule('ng', ['ngLocale'], ['$provide', function ngModule($provide) { @@ -1972,88 +2851,120 @@ $$sanitizeUri: $$SanitizeUriProvider }); $provide.provider('$compile', $CompileProvider). - directive({ - a: htmlAnchorDirective, - input: inputDirective, - textarea: inputDirective, - form: formDirective, - script: scriptDirective, - select: selectDirective, - style: styleDirective, - option: optionDirective, - ngBind: ngBindDirective, - ngBindHtml: ngBindHtmlDirective, - ngBindTemplate: ngBindTemplateDirective, - ngClass: ngClassDirective, - ngClassEven: ngClassEvenDirective, - ngClassOdd: ngClassOddDirective, - ngCloak: ngCloakDirective, - ngController: ngControllerDirective, - ngForm: ngFormDirective, - ngHide: ngHideDirective, - ngIf: ngIfDirective, - ngInclude: ngIncludeDirective, - ngInit: ngInitDirective, - ngNonBindable: ngNonBindableDirective, - ngPluralize: ngPluralizeDirective, - ngRepeat: ngRepeatDirective, - ngShow: ngShowDirective, - ngStyle: ngStyleDirective, - ngSwitch: ngSwitchDirective, - ngSwitchWhen: ngSwitchWhenDirective, - ngSwitchDefault: ngSwitchDefaultDirective, - ngOptions: ngOptionsDirective, - ngTransclude: ngTranscludeDirective, - ngModel: ngModelDirective, - ngList: ngListDirective, - ngChange: ngChangeDirective, - required: requiredDirective, - ngRequired: requiredDirective, - ngValue: ngValueDirective - }). - directive({ - ngInclude: ngIncludeFillContentDirective - }). - directive(ngAttributeAliasDirectives). - directive(ngEventDirectives); + directive({ + a: htmlAnchorDirective, + input: inputDirective, + textarea: inputDirective, + form: formDirective, + script: scriptDirective, + select: selectDirective, + option: optionDirective, + ngBind: ngBindDirective, + ngBindHtml: ngBindHtmlDirective, + ngBindTemplate: ngBindTemplateDirective, + ngClass: ngClassDirective, + ngClassEven: ngClassEvenDirective, + ngClassOdd: ngClassOddDirective, + ngCloak: ngCloakDirective, + ngController: ngControllerDirective, + ngForm: ngFormDirective, + ngHide: ngHideDirective, + ngIf: ngIfDirective, + ngInclude: ngIncludeDirective, + ngInit: ngInitDirective, + ngNonBindable: ngNonBindableDirective, + ngPluralize: ngPluralizeDirective, + ngRepeat: ngRepeatDirective, + ngShow: ngShowDirective, + ngStyle: ngStyleDirective, + ngSwitch: ngSwitchDirective, + ngSwitchWhen: ngSwitchWhenDirective, + ngSwitchDefault: ngSwitchDefaultDirective, + ngOptions: ngOptionsDirective, + ngTransclude: ngTranscludeDirective, + ngModel: ngModelDirective, + ngList: ngListDirective, + ngChange: ngChangeDirective, + pattern: patternDirective, + ngPattern: patternDirective, + required: requiredDirective, + ngRequired: requiredDirective, + minlength: minlengthDirective, + ngMinlength: minlengthDirective, + maxlength: maxlengthDirective, + ngMaxlength: maxlengthDirective, + ngValue: ngValueDirective, + ngModelOptions: ngModelOptionsDirective + }). + directive({ + ngInclude: ngIncludeFillContentDirective + }). + directive(ngAttributeAliasDirectives). + directive(ngEventDirectives); $provide.provider({ $anchorScroll: $AnchorScrollProvider, $animate: $AnimateProvider, + $animateCss: $CoreAnimateCssProvider, + $$animateJs: $$CoreAnimateJsProvider, + $$animateQueue: $$CoreAnimateQueueProvider, + $$AnimateRunner: $$AnimateRunnerFactoryProvider, + $$animateAsyncRun: $$AnimateAsyncRunFactoryProvider, $browser: $BrowserProvider, $cacheFactory: $CacheFactoryProvider, $controller: $ControllerProvider, $document: $DocumentProvider, + $$isDocumentHidden: $$IsDocumentHiddenProvider, $exceptionHandler: $ExceptionHandlerProvider, $filter: $FilterProvider, + $$forceReflow: $$ForceReflowProvider, $interpolate: $InterpolateProvider, $interval: $IntervalProvider, $http: $HttpProvider, + $httpParamSerializer: $HttpParamSerializerProvider, + $httpParamSerializerJQLike: $HttpParamSerializerJQLikeProvider, $httpBackend: $HttpBackendProvider, + $xhrFactory: $xhrFactoryProvider, + $jsonpCallbacks: $jsonpCallbacksProvider, $location: $LocationProvider, $log: $LogProvider, $parse: $ParseProvider, $rootScope: $RootScopeProvider, $q: $QProvider, + $$q: $$QProvider, $sce: $SceProvider, $sceDelegate: $SceDelegateProvider, $sniffer: $SnifferProvider, $templateCache: $TemplateCacheProvider, + $templateRequest: $TemplateRequestProvider, + $$testability: $$TestabilityProvider, $timeout: $TimeoutProvider, $window: $WindowProvider, $$rAF: $$RAFProvider, - $$asyncCallback : $$AsyncCallbackProvider + $$jqLite: $$jqLiteProvider, + $$Map: $$MapProvider, + $$cookieReader: $$CookieReaderProvider }); } - ]); + ]) + .info({ angularVersion: '1.6.9' }); } - /* global + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Any commits to this file should be reviewed with security in mind. * + * Changes to this file can potentially create security vulnerabilities. * + * An approval from 2 Core members with history of modifying * + * this file is required. * + * * + * Does the change somehow allow for arbitrary javascript to be executed? * + * Or allows for someone to change the prototype of built-in objects? * + * Or gives undesired access to variables likes document or window? * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -JQLitePrototype, - -addEventListenerFn, - -removeEventListenerFn, - -BOOLEAN_ATTR - */ + /* global + JQLitePrototype: true, + BOOLEAN_ATTR: true, + ALIASED_ATTR: true +*/ ////////////////////////////////// //JQLite @@ -2063,37 +2974,45 @@ * @ngdoc function * @name angular.element * @module ng - * @function + * @kind function * * @description * Wraps a raw DOM element or HTML string as a [jQuery](http://jquery.com) element. * * If jQuery is available, `angular.element` is an alias for the * [jQuery](http://api.jquery.com/jQuery/) function. If jQuery is not available, `angular.element` - * delegates to Angular's built-in subset of jQuery, called "jQuery lite" or "jqLite." + * delegates to AngularJS's built-in subset of jQuery, called "jQuery lite" or **jqLite**. + * + * jqLite is a tiny, API-compatible subset of jQuery that allows + * AngularJS to manipulate the DOM in a cross-browser compatible way. jqLite implements only the most + * commonly needed functionality with the goal of having a very small footprint. * - *
    jqLite is a tiny, API-compatible subset of jQuery that allows - * Angular to manipulate the DOM in a cross-browser compatible way. **jqLite** implements only the most - * commonly needed functionality with the goal of having a very small footprint.
    + * To use `jQuery`, simply ensure it is loaded before the `angular.js` file. You can also use the + * {@link ngJq `ngJq`} directive to specify that jqlite should be used over jQuery, or to use a + * specific version of jQuery if multiple versions exist on the page. * - * To use jQuery, simply load it before `DOMContentLoaded` event fired. + *
    **Note:** All element references in AngularJS are always wrapped with jQuery or + * jqLite (such as the element argument in a directive's compile / link function). They are never raw DOM references.
    * - *
    **Note:** all element references in Angular are always wrapped with jQuery or - * jqLite; they are never raw DOM references.
    + *
    **Note:** Keep in mind that this function will not find elements + * by tag name / CSS selector. For lookups by tag name, try instead `angular.element(document).find(...)` + * or `$document.find()`, or use the standard DOM APIs, e.g. `document.querySelectorAll()`.
    * - * ## Angular's jqLite + * ## AngularJS's jqLite * jqLite provides only the following jQuery methods: * - * - [`addClass()`](http://api.jquery.com/addClass/) + * - [`addClass()`](http://api.jquery.com/addClass/) - Does not support a function as first argument * - [`after()`](http://api.jquery.com/after/) * - [`append()`](http://api.jquery.com/append/) - * - [`attr()`](http://api.jquery.com/attr/) - * - [`bind()`](http://api.jquery.com/bind/) - Does not support namespaces, selectors or eventData + * - [`attr()`](http://api.jquery.com/attr/) - Does not support functions as parameters + * - [`bind()`](http://api.jquery.com/bind/) (_deprecated_, use [`on()`](http://api.jquery.com/on/)) - Does not support namespaces, selectors or eventData * - [`children()`](http://api.jquery.com/children/) - Does not support selectors * - [`clone()`](http://api.jquery.com/clone/) * - [`contents()`](http://api.jquery.com/contents/) - * - [`css()`](http://api.jquery.com/css/) + * - [`css()`](http://api.jquery.com/css/) - Only retrieves inline-styles, does not call `getComputedStyle()`. + * As a setter, does not convert numbers to strings or append 'px', and also does not have automatic property prefixing. * - [`data()`](http://api.jquery.com/data/) + * - [`detach()`](http://api.jquery.com/detach/) * - [`empty()`](http://api.jquery.com/empty/) * - [`eq()`](http://api.jquery.com/eq/) * - [`find()`](http://api.jquery.com/find/) - Limited to lookups by tag name @@ -2101,26 +3020,26 @@ * - [`html()`](http://api.jquery.com/html/) * - [`next()`](http://api.jquery.com/next/) - Does not support selectors * - [`on()`](http://api.jquery.com/on/) - Does not support namespaces, selectors or eventData - * - [`off()`](http://api.jquery.com/off/) - Does not support namespaces or selectors + * - [`off()`](http://api.jquery.com/off/) - Does not support namespaces, selectors or event object as parameter * - [`one()`](http://api.jquery.com/one/) - Does not support namespaces or selectors * - [`parent()`](http://api.jquery.com/parent/) - Does not support selectors * - [`prepend()`](http://api.jquery.com/prepend/) * - [`prop()`](http://api.jquery.com/prop/) - * - [`ready()`](http://api.jquery.com/ready/) + * - [`ready()`](http://api.jquery.com/ready/) (_deprecated_, use `angular.element(callback)` instead of `angular.element(document).ready(callback)`) * - [`remove()`](http://api.jquery.com/remove/) - * - [`removeAttr()`](http://api.jquery.com/removeAttr/) - * - [`removeClass()`](http://api.jquery.com/removeClass/) + * - [`removeAttr()`](http://api.jquery.com/removeAttr/) - Does not support multiple attributes + * - [`removeClass()`](http://api.jquery.com/removeClass/) - Does not support a function as first argument * - [`removeData()`](http://api.jquery.com/removeData/) * - [`replaceWith()`](http://api.jquery.com/replaceWith/) * - [`text()`](http://api.jquery.com/text/) - * - [`toggleClass()`](http://api.jquery.com/toggleClass/) - * - [`triggerHandler()`](http://api.jquery.com/triggerHandler/) - Passes a dummy event object to handlers. - * - [`unbind()`](http://api.jquery.com/unbind/) - Does not support namespaces + * - [`toggleClass()`](http://api.jquery.com/toggleClass/) - Does not support a function as first argument + * - [`triggerHandler()`](http://api.jquery.com/triggerHandler/) - Passes a dummy event object to handlers + * - [`unbind()`](http://api.jquery.com/unbind/) (_deprecated_, use [`off()`](http://api.jquery.com/off/)) - Does not support namespaces or event object as parameter * - [`val()`](http://api.jquery.com/val/) * - [`wrap()`](http://api.jquery.com/wrap/) * * ## jQuery/jqLite Extras - * Angular also provides the following additional methods and events to both jQuery and jqLite: + * AngularJS also provides the following additional methods and events to both jQuery and jqLite: * * ### Events * - `$destroy` - AngularJS intercepts all jqLite/jQuery's DOM destruction apis and fires this event @@ -2134,31 +3053,31 @@ * `'ngModel'`). * - `injector()` - retrieves the injector of the current element or its parent. * - `scope()` - retrieves the {@link ng.$rootScope.Scope scope} of the current - * element or its parent. + * element or its parent. Requires {@link guide/production#disabling-debug-data Debug Data} to + * be enabled. * - `isolateScope()` - retrieves an isolate {@link ng.$rootScope.Scope scope} if one is attached directly to the * current element. This getter should be used only on elements that contain a directive which starts a new isolate * scope. Calling `scope()` on this element always returns the original non-isolate scope. + * Requires {@link guide/production#disabling-debug-data Debug Data} to be enabled. * - `inheritedData()` - same as `data()`, but walks up the DOM until a value is found or the top * parent element is reached. * + * @knownIssue You cannot spy on `angular.element` if you are using Jasmine version 1.x. See + * https://github.com/angular/angular.js/issues/14251 for more information. + * * @param {string|DOMElement} element HTML string or DOMElement to be wrapped into jQuery. * @returns {Object} jQuery object. */ + JQLite.expando = 'ng339'; + var jqCache = JQLite.cache = {}, - jqName = JQLite.expando = 'ng-' + new Date().getTime(), - jqId = 1, - addEventListenerFn = (window.document.addEventListener - ? function(element, type, fn) {element.addEventListener(type, fn, false);} - : function(element, type, fn) {element.attachEvent('on' + type, fn);}), - removeEventListenerFn = (window.document.removeEventListener - ? function(element, type, fn) {element.removeEventListener(type, fn, false); } - : function(element, type, fn) {element.detachEvent('on' + type, fn); }); + jqId = 1; /* - * !!! This is an undocumented "private" function !!! - */ - var jqData = JQLite._data = function(node) { + * !!! This is an undocumented "private" function !!! + */ + JQLite._data = function(node) { //jQuery always returns an object on cache miss return this.cache[node[this.expando]] || {}; }; @@ -2166,70 +3085,37 @@ function jqNextId() { return ++jqId; } - var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g; - var MOZ_HACK_REGEXP = /^moz([A-Z])/; + var DASH_LOWERCASE_REGEXP = /-([a-z])/g; + var MS_HACK_REGEXP = /^-ms-/; + var MOUSE_EVENT_MAP = { mouseleave: 'mouseout', mouseenter: 'mouseover' }; var jqLiteMinErr = minErr('jqLite'); /** - * Converts snake_case to camelCase. - * Also there is special case for Moz prefix starting with upper case letter. + * Converts kebab-case to camelCase. + * There is also a special case for the ms prefix starting with a lowercase letter. * @param name Name to normalize */ - function camelCase(name) { - return name. - replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) { - return offset ? letter.toUpperCase() : letter; - }). - replace(MOZ_HACK_REGEXP, 'Moz$1'); + function cssKebabToCamel(name) { + return kebabToCamel(name.replace(MS_HACK_REGEXP, 'ms-')); } -///////////////////////////////////////////// -// jQuery mutation patch -// -// In conjunction with bindJQuery intercepts all jQuery's DOM destruction apis and fires a -// $destroy event on all DOM nodes being removed. -// -///////////////////////////////////////////// + function fnCamelCaseReplace(all, letter) { + return letter.toUpperCase(); + } - function jqLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments) { - var originalJqFn = jQuery.fn[name]; - originalJqFn = originalJqFn.$original || originalJqFn; - removePatch.$original = originalJqFn; - jQuery.fn[name] = removePatch; - - function removePatch(param) { - // jshint -W040 - var list = filterElems && param ? [this.filter(param)] : [this], - fireEvent = dispatchThis, - set, setIndex, setLength, - element, childIndex, childLength, children; - - if (!getterIfNoArguments || param != null) { - while(list.length) { - set = list.shift(); - for(setIndex = 0, setLength = set.length; setIndex < setLength; setIndex++) { - element = jqLite(set[setIndex]); - if (fireEvent) { - element.triggerHandler('$destroy'); - } else { - fireEvent = !fireEvent; - } - for(childIndex = 0, childLength = (children = element.children()).length; - childIndex < childLength; - childIndex++) { - list.push(jQuery(children[childIndex])); - } - } - } - } - return originalJqFn.apply(this, arguments); - } + /** + * Converts kebab-case to camelCase. + * @param name Name to normalize + */ + function kebabToCamel(name) { + return name + .replace(DASH_LOWERCASE_REGEXP, fnCamelCaseReplace); } - var SINGLE_TAG_REGEXP = /^<(\w+)\s*\/?>(?:<\/\1>|)$/; + var SINGLE_TAG_REGEXP = /^<([\w-]+)\s*\/?>(?:<\/\1>|)$/; var HTML_REGEXP = /<|&#?\w+;/; - var TAG_NAME_REGEXP = /<([\w:]+)/; - var XHTML_TAG_REGEXP = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi; + var TAG_NAME_REGEXP = /<([\w:-]+)/; + var XHTML_TAG_REGEXP = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi; var wrapMap = { 'option': [1, ''], @@ -2238,33 +3124,46 @@ 'col': [2, '', '
    '], 'tr': [2, '', '
    '], 'td': [3, '', '
    '], - '_default': [0, "", ""] + '_default': [0, '', ''] }; wrapMap.optgroup = wrapMap.option; wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; wrapMap.th = wrapMap.td; + function jqLiteIsTextNode(html) { return !HTML_REGEXP.test(html); } + function jqLiteAcceptsData(node) { + // The window object can accept data but has no nodeType + // Otherwise we are only interested in elements (1) and documents (9) + var nodeType = node.nodeType; + return nodeType === NODE_TYPE_ELEMENT || !nodeType || nodeType === NODE_TYPE_DOCUMENT; + } + + function jqLiteHasData(node) { + for (var key in jqCache[node.ng339]) { + return true; + } + return false; + } + function jqLiteBuildFragment(html, context) { - var elem, tmp, tag, wrap, + var tmp, tag, wrap, fragment = context.createDocumentFragment(), - nodes = [], i, j, jj; + nodes = [], i; if (jqLiteIsTextNode(html)) { // Convert non-html into a text node nodes.push(context.createTextNode(html)); } else { - tmp = fragment.appendChild(context.createElement('div')); // Convert html into DOM nodes - tag = (TAG_NAME_REGEXP.exec(html) || ["", ""])[1].toLowerCase(); + tmp = fragment.appendChild(context.createElement('div')); + tag = (TAG_NAME_REGEXP.exec(html) || ['', ''])[1].toLowerCase(); wrap = wrapMap[tag] || wrapMap._default; - tmp.innerHTML = '
     
    ' + - wrap[1] + html.replace(XHTML_TAG_REGEXP, "<$1>") + wrap[2]; - tmp.removeChild(tmp.firstChild); + tmp.innerHTML = wrap[1] + html.replace(XHTML_TAG_REGEXP, '<$1>') + wrap[2]; // Descend through wrappers to the right content i = wrap[0]; @@ -2272,48 +3171,77 @@ tmp = tmp.lastChild; } - for (j=0, jj=tmp.childNodes.length; j 0)) { + element.removeEventListener(type, handle); delete events[type]; - } else { - arrayRemove(events[type] || [], fn); + } + }; + + forEach(type.split(' '), function(type) { + removeHandler(type); + if (MOUSE_EVENT_MAP[type]) { + removeHandler(MOUSE_EVENT_MAP[type]); } }); } } function jqLiteRemoveData(element, name) { - var expandoId = element[jqName], - expandoStore = jqCache[expandoId]; + var expandoId = element.ng339; + var expandoStore = expandoId && jqCache[expandoId]; if (expandoStore) { if (name) { - delete jqCache[expandoId].data[name]; + delete expandoStore.data[name]; return; } if (expandoStore.handle) { - expandoStore.events.$destroy && expandoStore.handle({}, '$destroy'); + if (expandoStore.events.$destroy) { + expandoStore.handle({}, '$destroy'); + } jqLiteOff(element); } delete jqCache[expandoId]; - element[jqName] = undefined; // ie does not allow deletion of attributes on elements. + element.ng339 = undefined; // don't delete DOM expandos. IE and Chrome don't like it } } - function jqLiteExpandoStore(element, key, value) { - var expandoId = element[jqName], - expandoStore = jqCache[expandoId || -1]; - if (isDefined(value)) { - if (!expandoStore) { - element[jqName] = expandoId = jqNextId(); - expandoStore = jqCache[expandoId] = {}; - } - expandoStore[key] = value; - } else { - return expandoStore && expandoStore[key]; + function jqLiteExpandoStore(element, createIfNecessary) { + var expandoId = element.ng339, + expandoStore = expandoId && jqCache[expandoId]; + + if (createIfNecessary && !expandoStore) { + element.ng339 = expandoId = jqNextId(); + expandoStore = jqCache[expandoId] = {events: {}, data: {}, handle: undefined}; } + + return expandoStore; } + function jqLiteData(element, key, value) { - var data = jqLiteExpandoStore(element, 'data'), - isSetter = isDefined(value), - keyDefined = !isSetter && isDefined(key), - isSimpleGetter = keyDefined && !isObject(key); + if (jqLiteAcceptsData(element)) { + var prop; - if (!data && !isSimpleGetter) { - jqLiteExpandoStore(element, 'data', data = {}); - } + var isSimpleSetter = isDefined(value); + var isSimpleGetter = !isSimpleSetter && key && !isObject(key); + var massGetter = !key; + var expandoStore = jqLiteExpandoStore(element, !isSimpleGetter); + var data = expandoStore && expandoStore.data; - if (isSetter) { - data[key] = value; - } else { - if (keyDefined) { - if (isSimpleGetter) { - // don't create data in this case. - return data && data[key]; + if (isSimpleSetter) { // data('key', value) + data[kebabToCamel(key)] = value; + } else { + if (massGetter) { // data() + return data; } else { - extend(data, key); + if (isSimpleGetter) { // data('key') + // don't force creation of expandoStore if it doesn't exist yet + return data && data[kebabToCamel(key)]; + } else { // mass-setter: data({key1: val1, key2: val2}) + for (prop in key) { + data[kebabToCamel(prop)] = key[prop]; + } + } } - } else { - return data; } } } function jqLiteHasClass(element, selector) { if (!element.getAttribute) return false; - return ((" " + (element.getAttribute('class') || '') + " ").replace(/[\n\t]/g, " "). - indexOf( " " + selector + " " ) > -1); + return ((' ' + (element.getAttribute('class') || '') + ' ').replace(/[\n\t]/g, ' '). + indexOf(' ' + selector + ' ') > -1); } function jqLiteRemoveClass(element, cssClasses) { if (cssClasses && element.setAttribute) { + var existingClasses = (' ' + (element.getAttribute('class') || '') + ' ') + .replace(/[\n\t]/g, ' '); + var newClasses = existingClasses; + forEach(cssClasses.split(' '), function(cssClass) { - element.setAttribute('class', trim( - (" " + (element.getAttribute('class') || '') + " ") - .replace(/[\n\t]/g, " ") - .replace(" " + trim(cssClass) + " ", " ")) - ); + cssClass = trim(cssClass); + newClasses = newClasses.replace(' ' + cssClass + ' ', ' '); }); + + if (newClasses !== existingClasses) { + element.setAttribute('class', trim(newClasses)); + } } } function jqLiteAddClass(element, cssClasses) { if (cssClasses && element.setAttribute) { var existingClasses = (' ' + (element.getAttribute('class') || '') + ' ') - .replace(/[\n\t]/g, " "); + .replace(/[\n\t]/g, ' '); + var newClasses = existingClasses; forEach(cssClasses.split(' '), function(cssClass) { cssClass = trim(cssClass); - if (existingClasses.indexOf(' ' + cssClass + ' ') === -1) { - existingClasses += cssClass + ' '; + if (newClasses.indexOf(' ' + cssClass + ' ') === -1) { + newClasses += cssClass + ' '; } }); - element.setAttribute('class', trim(existingClasses)); + if (newClasses !== existingClasses) { + element.setAttribute('class', trim(newClasses)); + } } } + function jqLiteAddNodes(root, elements) { + // THIS CODE IS VERY HOT. Don't make changes without benchmarking. + if (elements) { - elements = (!elements.nodeName && isDefined(elements.length) && !isWindow(elements)) - ? elements - : [ elements ]; - for(var i=0; i < elements.length; i++) { - root.push(elements[i]); + + // if a Node (the most common case) + if (elements.nodeType) { + root[root.length++] = elements; + } else { + var length = elements.length; + + // if an Array or NodeList and not a Window + if (typeof length === 'number' && elements.window !== elements) { + if (length) { + for (var i = 0; i < length; i++) { + root[root.length++] = elements[i]; + } + } + } else { + root[root.length++] = elements; + } } } } + function jqLiteController(element, name) { - return jqLiteInheritedData(element, '$' + (name || 'ngController' ) + 'Controller'); + return jqLiteInheritedData(element, '$' + (name || 'ngController') + 'Controller'); } function jqLiteInheritedData(element, name, value) { - element = jqLite(element); - // if element is the document object work with the html element instead // this makes $(document).scope() possible - if(element[0].nodeType == 9) { - element = element.find('html'); + if (element.nodeType === NODE_TYPE_DOCUMENT) { + element = element.documentElement; } var names = isArray(name) ? name : [name]; - while (element.length) { - var node = element[0]; + while (element) { for (var i = 0, ii = names.length; i < ii; i++) { - if ((value = element.data(names[i])) !== undefined) return value; + if (isDefined(value = jqLite.data(element, names[i]))) return value; } // If dealing with a document fragment node with a host element, and no parent, use the host // element as the parent. This enables directives within a Shadow DOM or polyfilled Shadow DOM // to lookup parent controllers. - element = jqLite(node.parentNode || (node.nodeType === 11 && node.host)); + element = element.parentNode || (element.nodeType === NODE_TYPE_DOCUMENT_FRAGMENT && element.host); } } function jqLiteEmpty(element) { - for (var i = 0, childNodes = element.childNodes; i < childNodes.length; i++) { - jqLiteDealoc(childNodes[i]); - } + jqLiteDealoc(element, true); while (element.firstChild) { element.removeChild(element.firstChild); } } + function jqLiteRemove(element, keepData) { + if (!keepData) jqLiteDealoc(element); + var parent = element.parentNode; + if (parent) parent.removeChild(element); + } + + + function jqLiteDocumentLoaded(action, win) { + win = win || window; + if (win.document.readyState === 'complete') { + // Force the action to be run async for consistent behavior + // from the action's point of view + // i.e. it will definitely not be in a $apply + win.setTimeout(action); + } else { + // No need to unbind this handler as load is only ever called once + jqLite(win).on('load', action); + } + } + + function jqLiteReady(fn) { + function trigger() { + window.document.removeEventListener('DOMContentLoaded', trigger); + window.removeEventListener('load', trigger); + fn(); + } + + // check if document is already loaded + if (window.document.readyState === 'complete') { + window.setTimeout(fn); + } else { + // We can not use jqLite since we are not done loading and jQuery could be loaded later. + + // Works for modern browsers and IE9 + window.document.addEventListener('DOMContentLoaded', trigger); + + // Fallback to window.onload for others + window.addEventListener('load', trigger); + } + } + ////////////////////////////////////////// // Functions which are declared directly. ////////////////////////////////////////// var JQLitePrototype = JQLite.prototype = { - ready: function(fn) { - var fired = false; - - function trigger() { - if (fired) return; - fired = true; - fn(); - } - - // check if document already is loaded - if (document.readyState === 'complete'){ - setTimeout(trigger); - } else { - this.on('DOMContentLoaded', trigger); // works for modern browsers and IE9 - // we can not use jqLite since we are not done loading and jQuery could be loaded later. - // jshint -W064 - JQLite(window).on('load', trigger); // fallback to window.onload for others - // jshint +W064 - } - }, + ready: jqLiteReady, toString: function() { var value = []; - forEach(this, function(e){ value.push('' + e);}); + forEach(this, function(e) { value.push('' + e);}); return '[' + value.join(', ') + ']'; }, @@ -2547,29 +3534,54 @@ }); var BOOLEAN_ELEMENTS = {}; forEach('input,select,option,textarea,button,form,details'.split(','), function(value) { - BOOLEAN_ELEMENTS[uppercase(value)] = true; + BOOLEAN_ELEMENTS[value] = true; }); + var ALIASED_ATTR = { + 'ngMinlength': 'minlength', + 'ngMaxlength': 'maxlength', + 'ngMin': 'min', + 'ngMax': 'max', + 'ngPattern': 'pattern', + 'ngStep': 'step' + }; function getBooleanAttrName(element, name) { // check dom last since we will most likely fail on name var booleanAttr = BOOLEAN_ATTR[name.toLowerCase()]; // booleanAttr is here twice to minimize DOM access - return booleanAttr && BOOLEAN_ELEMENTS[element.nodeName] && booleanAttr; + return booleanAttr && BOOLEAN_ELEMENTS[nodeName_(element)] && booleanAttr; + } + + function getAliasedAttrName(name) { + return ALIASED_ATTR[name]; } + forEach({ + data: jqLiteData, + removeData: jqLiteRemoveData, + hasData: jqLiteHasData, + cleanData: function jqLiteCleanData(nodes) { + for (var i = 0, ii = nodes.length; i < ii; i++) { + jqLiteRemoveData(nodes[i]); + } + } + }, function(fn, name) { + JQLite[name] = fn; + }); + forEach({ data: jqLiteData, inheritedData: jqLiteInheritedData, scope: function(element) { // Can't use jqLiteData here directly so we stay compatible with jQuery! - return jqLite(element).data('$scope') || jqLiteInheritedData(element.parentNode || element, ['$isolateScope', '$scope']); + return jqLite.data(element, '$scope') || jqLiteInheritedData(element.parentNode || element, ['$isolateScope', '$scope']); }, isolateScope: function(element) { // Can't use jqLiteData here directly so we stay compatible with jQuery! - return jqLite(element).data('$isolateScope') || jqLite(element).data('$isolateScopeNoTemplate'); + return jqLite.data(element, '$isolateScope') || jqLite.data(element, '$isolateScopeNoTemplate'); }, controller: jqLiteController, @@ -2578,61 +3590,50 @@ return jqLiteInheritedData(element, '$injector'); }, - removeAttr: function(element,name) { + removeAttr: function(element, name) { element.removeAttribute(name); }, hasClass: jqLiteHasClass, css: function(element, name, value) { - name = camelCase(name); + name = cssKebabToCamel(name); if (isDefined(value)) { element.style[name] = value; } else { - var val; + return element.style[name]; + } + }, - if (msie <= 8) { - // this is some IE specific weirdness that jQuery 1.6.4 does not sure why - val = element.currentStyle && element.currentStyle[name]; - if (val === '') val = 'auto'; - } + attr: function(element, name, value) { + var ret; + var nodeType = element.nodeType; + if (nodeType === NODE_TYPE_TEXT || nodeType === NODE_TYPE_ATTRIBUTE || nodeType === NODE_TYPE_COMMENT || + !element.getAttribute) { + return; + } - val = val || element.style[name]; + var lowercasedName = lowercase(name); + var isBooleanAttr = BOOLEAN_ATTR[lowercasedName]; + + if (isDefined(value)) { + // setter - if (msie <= 8) { - // jquery weirdness :-/ - val = (val === '') ? undefined : val; + if (value === null || (value === false && isBooleanAttr)) { + element.removeAttribute(name); + } else { + element.setAttribute(name, isBooleanAttr ? lowercasedName : value); } + } else { + // getter - return val; - } - }, + ret = element.getAttribute(name); - attr: function(element, name, value){ - var lowercasedName = lowercase(name); - if (BOOLEAN_ATTR[lowercasedName]) { - if (isDefined(value)) { - if (!!value) { - element[name] = true; - element.setAttribute(name, lowercasedName); - } else { - element[name] = false; - element.removeAttribute(lowercasedName); - } - } else { - return (element[name] || - (element.attributes.getNamedItem(name)|| noop).specified) - ? lowercasedName - : undefined; - } - } else if (isDefined(value)) { - element.setAttribute(name, value); - } else if (element.getAttribute) { - // the extra argument "2" is to get the right thing for a.href in IE, see jQuery code - // some elements (e.g. Document) don't have get attribute, so return undefined - var ret = element.getAttribute(name, 2); - // normalize non-existing attributes to undefined (as jQuery) + if (isBooleanAttr && ret !== null) { + ret = lowercasedName; + } + // Normalize non-existing attributes to undefined (as jQuery). return ret === null ? undefined : ret; } }, @@ -2646,36 +3647,28 @@ }, text: (function() { - var NODE_TYPE_TEXT_PROPERTY = []; - if (msie < 9) { - NODE_TYPE_TEXT_PROPERTY[1] = 'innerText'; /** Element **/ - NODE_TYPE_TEXT_PROPERTY[3] = 'nodeValue'; /** Text **/ - } else { - NODE_TYPE_TEXT_PROPERTY[1] = /** Element **/ - NODE_TYPE_TEXT_PROPERTY[3] = 'textContent'; /** Text **/ - } getText.$dv = ''; return getText; function getText(element, value) { - var textProp = NODE_TYPE_TEXT_PROPERTY[element.nodeType]; if (isUndefined(value)) { - return textProp ? element[textProp] : ''; + var nodeType = element.nodeType; + return (nodeType === NODE_TYPE_ELEMENT || nodeType === NODE_TYPE_TEXT) ? element.textContent : ''; } - element[textProp] = value; + element.textContent = value; } })(), val: function(element, value) { if (isUndefined(value)) { - if (nodeName_(element) === 'SELECT' && element.multiple) { + if (element.multiple && nodeName_(element) === 'select') { var result = []; - forEach(element.options, function (option) { + forEach(element.options, function(option) { if (option.selected) { result.push(option.value || option.text); } }); - return result.length === 0 ? null : result; + return result; } return element.value; } @@ -2686,29 +3679,28 @@ if (isUndefined(value)) { return element.innerHTML; } - for (var i = 0, childNodes = element.childNodes; i < childNodes.length; i++) { - jqLiteDealoc(childNodes[i]); - } + jqLiteDealoc(element, true); element.innerHTML = value; }, empty: jqLiteEmpty - }, function(fn, name){ + }, function(fn, name) { /** * Properties: writes return selection, reads return first value */ JQLite.prototype[name] = function(arg1, arg2) { var i, key; + var nodeCount = this.length; // jqLiteHasClass has only two arguments, but is a getter-only fn, so we need to special-case it // in a way that survives minification. // jqLiteEmpty takes no arguments but is a setter. if (fn !== jqLiteEmpty && - (((fn.length == 2 && (fn !== jqLiteHasClass && fn !== jqLiteController)) ? arg1 : arg2) === undefined)) { + (isUndefined((fn.length === 2 && (fn !== jqLiteHasClass && fn !== jqLiteController)) ? arg1 : arg2))) { if (isObject(arg1)) { // we are a write, but the object properties are the key/values - for (i = 0; i < this.length; i++) { + for (i = 0; i < nodeCount; i++) { if (fn === jqLiteData) { // data() takes the whole object in jQuery fn(this[i], arg1); @@ -2722,9 +3714,10 @@ return this; } else { // we are a read, so read the first child. + // TODO: do we still need this? var value = fn.$dv; // Only if we have $dv do we iterate over all, otherwise it is just the first element. - var jj = (value === undefined) ? Math.min(this.length, 1) : this.length; + var jj = (isUndefined(value)) ? Math.min(nodeCount, 1) : nodeCount; for (var j = 0; j < jj; j++) { var nodeValue = fn(this[j], arg1, arg2); value = value ? value + nodeValue : nodeValue; @@ -2733,7 +3726,7 @@ } } else { // we are a write, so apply to all children - for (i = 0; i < this.length; i++) { + for (i = 0; i < nodeCount; i++) { fn(this[i], arg1, arg2); } // return self for chaining @@ -2743,61 +3736,73 @@ }); function createEventHandler(element, events) { - var eventHandler = function (event, type) { - if (!event.preventDefault) { - event.preventDefault = function() { - event.returnValue = false; //ie - }; - } + var eventHandler = function(event, type) { + // jQuery specific api + event.isDefaultPrevented = function() { + return event.defaultPrevented; + }; - if (!event.stopPropagation) { - event.stopPropagation = function() { - event.cancelBubble = true; //ie - }; - } + var eventFns = events[type || event.type]; + var eventFnsLength = eventFns ? eventFns.length : 0; - if (!event.target) { - event.target = event.srcElement || document; - } + if (!eventFnsLength) return; + + if (isUndefined(event.immediatePropagationStopped)) { + var originalStopImmediatePropagation = event.stopImmediatePropagation; + event.stopImmediatePropagation = function() { + event.immediatePropagationStopped = true; - if (isUndefined(event.defaultPrevented)) { - var prevent = event.preventDefault; - event.preventDefault = function() { - event.defaultPrevented = true; - prevent.call(event); + if (event.stopPropagation) { + event.stopPropagation(); + } + + if (originalStopImmediatePropagation) { + originalStopImmediatePropagation.call(event); + } }; - event.defaultPrevented = false; } - event.isDefaultPrevented = function() { - return event.defaultPrevented || event.returnValue === false; + event.isImmediatePropagationStopped = function() { + return event.immediatePropagationStopped === true; }; - // Copy event handlers in case event handlers array is modified during execution. - var eventHandlersCopy = shallowCopy(events[type || event.type] || []); + // Some events have special handlers that wrap the real handler + var handlerWrapper = eventFns.specialHandlerWrapper || defaultHandlerWrapper; - forEach(eventHandlersCopy, function(fn) { - fn.call(element, event); - }); + // Copy event handlers in case event handlers array is modified during execution. + if ((eventFnsLength > 1)) { + eventFns = shallowCopy(eventFns); + } - // Remove monkey-patched methods (IE), - // as they would cause memory leaks in IE8. - if (msie <= 8) { - // IE7/8 does not allow to delete property on native object - event.preventDefault = null; - event.stopPropagation = null; - event.isDefaultPrevented = null; - } else { - // It shouldn't affect normal browsers (native methods are defined on prototype). - delete event.preventDefault; - delete event.stopPropagation; - delete event.isDefaultPrevented; + for (var i = 0; i < eventFnsLength; i++) { + if (!event.isImmediatePropagationStopped()) { + handlerWrapper(element, event, eventFns[i]); + } } }; + + // TODO: this is a hack for angularMocks/clearDataCache that makes it possible to deregister all + // events on `element` eventHandler.elem = element; return eventHandler; } + function defaultHandlerWrapper(element, event, handler) { + handler.call(element, event); + } + + function specialMouseHandlerWrapper(target, event, handler) { + // Refer to jQuery's implementation of mouseenter & mouseleave + // Read about mouseenter and mouseleave: + // http://www.quirksmode.org/js/events_mouse.html#link8 + var related = event.relatedTarget; + // For mousenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if (!related || (related !== target && !jqLiteContains.call(target, related))) { + handler.call(target, event); + } + } + ////////////////////////////////////////// // Functions iterating traversal. // These functions chain results into a single @@ -2806,68 +3811,49 @@ forEach({ removeData: jqLiteRemoveData, - dealoc: jqLiteDealoc, - - on: function onFn(element, type, fn, unsupported){ + on: function jqLiteOn(element, type, fn, unsupported) { if (isDefined(unsupported)) throw jqLiteMinErr('onargs', 'jqLite#on() does not support the `selector` or `eventData` parameters'); - var events = jqLiteExpandoStore(element, 'events'), - handle = jqLiteExpandoStore(element, 'handle'); + // Do not add event handlers to non-elements because they will not be cleaned up. + if (!jqLiteAcceptsData(element)) { + return; + } + + var expandoStore = jqLiteExpandoStore(element, true); + var events = expandoStore.events; + var handle = expandoStore.handle; - if (!events) jqLiteExpandoStore(element, 'events', events = {}); - if (!handle) jqLiteExpandoStore(element, 'handle', handle = createEventHandler(element, events)); + if (!handle) { + handle = expandoStore.handle = createEventHandler(element, events); + } + + // http://jsperf.com/string-indexof-vs-split + var types = type.indexOf(' ') >= 0 ? type.split(' ') : [type]; + var i = types.length; - forEach(type.split(' '), function(type){ + var addHandler = function(type, specialHandlerWrapper, noEventListener) { var eventFns = events[type]; if (!eventFns) { - if (type == 'mouseenter' || type == 'mouseleave') { - var contains = document.body.contains || document.body.compareDocumentPosition ? - function( a, b ) { - // jshint bitwise: false - var adown = a.nodeType === 9 ? a.documentElement : a, - bup = b && b.parentNode; - return a === bup || !!( bup && bup.nodeType === 1 && ( - adown.contains ? - adown.contains( bup ) : - a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 - )); - } : - function( a, b ) { - if ( b ) { - while ( (b = b.parentNode) ) { - if ( b === a ) { - return true; - } - } - } - return false; - }; - - events[type] = []; + eventFns = events[type] = []; + eventFns.specialHandlerWrapper = specialHandlerWrapper; + if (type !== '$destroy' && !noEventListener) { + element.addEventListener(type, handle); + } + } - // Refer to jQuery's implementation of mouseenter & mouseleave - // Read about mouseenter and mouseleave: - // http://www.quirksmode.org/js/events_mouse.html#link8 - var eventmap = { mouseleave : "mouseout", mouseenter : "mouseover"}; + eventFns.push(fn); + }; - onFn(element, eventmap[type], function(event) { - var target = this, related = event.relatedTarget; - // For mousenter/leave call the handler if related is outside the target. - // NB: No relatedTarget if the mouse left/entered the browser window - if ( !related || (related !== target && !contains(target, related)) ){ - handle(event, type); - } - }); - - } else { - addEventListenerFn(element, type, handle); - events[type] = []; - } - eventFns = events[type]; + while (i--) { + type = types[i]; + if (MOUSE_EVENT_MAP[type]) { + addHandler(MOUSE_EVENT_MAP[type], specialMouseHandlerWrapper); + addHandler(type, undefined, true); + } else { + addHandler(type); } - eventFns.push(fn); - }); + } }, off: jqLiteOff, @@ -2888,7 +3874,7 @@ replaceWith: function(element, replaceNode) { var index, parent = element.parentNode; jqLiteDealoc(element); - forEach(new JQLite(replaceNode), function(node){ + forEach(new JQLite(replaceNode), function(node) { if (index) { parent.insertBefore(node, index.nextSibling); } else { @@ -2900,9 +3886,10 @@ children: function(element) { var children = []; - forEach(element.childNodes, function(element){ - if (element.nodeType === 1) + forEach(element.childNodes, function(element) { + if (element.nodeType === NODE_TYPE_ELEMENT) { children.push(element); + } }); return children; }, @@ -2912,43 +3899,48 @@ }, append: function(element, node) { - forEach(new JQLite(node), function(child){ - if (element.nodeType === 1 || element.nodeType === 11) { - element.appendChild(child); - } - }); + var nodeType = element.nodeType; + if (nodeType !== NODE_TYPE_ELEMENT && nodeType !== NODE_TYPE_DOCUMENT_FRAGMENT) return; + + node = new JQLite(node); + + for (var i = 0, ii = node.length; i < ii; i++) { + var child = node[i]; + element.appendChild(child); + } }, prepend: function(element, node) { - if (element.nodeType === 1) { + if (element.nodeType === NODE_TYPE_ELEMENT) { var index = element.firstChild; - forEach(new JQLite(node), function(child){ + forEach(new JQLite(node), function(child) { element.insertBefore(child, index); }); } }, wrap: function(element, wrapNode) { - wrapNode = jqLite(wrapNode)[0]; - var parent = element.parentNode; - if (parent) { - parent.replaceChild(wrapNode, element); - } - wrapNode.appendChild(element); + jqLiteWrapNode(element, jqLite(wrapNode).eq(0).clone()[0]); }, - remove: function(element) { - jqLiteDealoc(element); - var parent = element.parentNode; - if (parent) parent.removeChild(element); + remove: jqLiteRemove, + + detach: function(element) { + jqLiteRemove(element, true); }, after: function(element, newElement) { var index = element, parent = element.parentNode; - forEach(new JQLite(newElement), function(node){ - parent.insertBefore(node, index.nextSibling); - index = node; - }); + + if (parent) { + newElement = new JQLite(newElement); + + for (var i = 0, ii = newElement.length; i < ii; i++) { + var node = newElement[i]; + parent.insertBefore(node, index.nextSibling); + index = node; + } + } }, addClass: jqLiteAddClass, @@ -2956,7 +3948,7 @@ toggleClass: function(element, selector, condition) { if (selector) { - forEach(selector.split(' '), function(className){ + forEach(selector.split(' '), function(className) { var classCondition = condition; if (isUndefined(classCondition)) { classCondition = !jqLiteHasClass(element, className); @@ -2968,20 +3960,11 @@ parent: function(element) { var parent = element.parentNode; - return parent && parent.nodeType !== 11 ? parent : null; + return parent && parent.nodeType !== NODE_TYPE_DOCUMENT_FRAGMENT ? parent : null; }, next: function(element) { - if (element.nextElementSibling) { - return element.nextElementSibling; - } - - // IE8 doesn't have nextElementSibling - var elm = element.nextSibling; - while (elm != null && elm.nodeType !== 1) { - elm = elm.nextSibling; - } - return elm; + return element.nextElementSibling; }, find: function(element, selector) { @@ -2994,27 +3977,50 @@ clone: jqLiteClone, - triggerHandler: function(element, eventName, eventData) { - var eventFns = (jqLiteExpandoStore(element, 'events') || {})[eventName]; + triggerHandler: function(element, event, extraParameters) { + + var dummyEvent, eventFnsCopy, handlerArgs; + var eventName = event.type || event; + var expandoStore = jqLiteExpandoStore(element); + var events = expandoStore && expandoStore.events; + var eventFns = events && events[eventName]; + + if (eventFns) { + // Create a dummy event to pass to the handlers + dummyEvent = { + preventDefault: function() { this.defaultPrevented = true; }, + isDefaultPrevented: function() { return this.defaultPrevented === true; }, + stopImmediatePropagation: function() { this.immediatePropagationStopped = true; }, + isImmediatePropagationStopped: function() { return this.immediatePropagationStopped === true; }, + stopPropagation: noop, + type: eventName, + target: element + }; - eventData = eventData || []; + // If a custom event was provided then extend our dummy event with it + if (event.type) { + dummyEvent = extend(dummyEvent, event); + } - var event = [{ - preventDefault: noop, - stopPropagation: noop - }]; + // Copy event handlers in case event handlers array is modified during execution. + eventFnsCopy = shallowCopy(eventFns); + handlerArgs = extraParameters ? [dummyEvent].concat(extraParameters) : [dummyEvent]; - forEach(eventFns, function(fn) { - fn.apply(element, event.concat(eventData)); - }); + forEach(eventFnsCopy, function(fn) { + if (!dummyEvent.isImmediatePropagationStopped()) { + fn.apply(element, handlerArgs); + } + }); + } } - }, function(fn, name){ + }, function(fn, name) { /** * chaining functions */ JQLite.prototype[name] = function(arg1, arg2, arg3) { var value; - for(var i=0; i < this.length; i++) { + + for (var i = 0, ii = this.length; i < ii; i++) { if (isUndefined(value)) { value = fn(this[i], arg1, arg2, arg3); if (isDefined(value)) { @@ -3027,12 +4033,34 @@ } return isDefined(value) ? value : this; }; - - // bind legacy bind/unbind to on/off - JQLite.prototype.bind = JQLite.prototype.on; - JQLite.prototype.unbind = JQLite.prototype.off; }); +// bind legacy bind/unbind to on/off + JQLite.prototype.bind = JQLite.prototype.on; + JQLite.prototype.unbind = JQLite.prototype.off; + + +// Provider for private $$jqLite service + /** @this */ + function $$jqLiteProvider() { + this.$get = function $$jqLite() { + return extend(JQLite, { + hasClass: function(node, classes) { + if (node.attr) node = node[0]; + return jqLiteHasClass(node, classes); + }, + addClass: function(node, classes) { + if (node.attr) node = node[0]; + return jqLiteAddClass(node, classes); + }, + removeClass: function(node, classes) { + if (node.attr) node = node[0]; + return jqLiteRemoveClass(node, classes); + } + }); + }; + } + /** * Computes a hash of an 'obj'. * Hash of a: @@ -3045,73 +4073,108 @@ * @returns {string} hash string such that the same input will have the same hash string. * The resulting string key is in 'type:hashKey' format. */ - function hashKey(obj) { - var objType = typeof obj, - key; + function hashKey(obj, nextUidFn) { + var key = obj && obj.$$hashKey; - if (objType == 'object' && obj !== null) { - if (typeof (key = obj.$$hashKey) == 'function') { - // must invoke on object to keep the right this + if (key) { + if (typeof key === 'function') { key = obj.$$hashKey(); - } else if (key === undefined) { - key = obj.$$hashKey = nextUid(); } + return key; + } + + var objType = typeof obj; + if (objType === 'function' || (objType === 'object' && obj !== null)) { + key = obj.$$hashKey = objType + ':' + (nextUidFn || nextUid)(); } else { - key = obj; + key = objType + ':' + obj; } - return objType + ':' + key; + return key; } - /** - * HashMap which can use objects as keys - */ - function HashMap(array){ - forEach(array, this.put, this); +// A minimal ES2015 Map implementation. +// Should be bug/feature equivalent to the native implementations of supported browsers +// (for the features required in Angular). +// See https://kangax.github.io/compat-table/es6/#test-Map + var nanKey = Object.create(null); + function NgMapShim() { + this._keys = []; + this._values = []; + this._lastKey = NaN; + this._lastIndex = -1; } - HashMap.prototype = { - /** - * Store key value pair - * @param key key to store can be any type - * @param value value to store can be any type - */ - put: function(key, value) { - this[hashKey(key)] = value; + NgMapShim.prototype = { + _idx: function(key) { + if (key === this._lastKey) { + return this._lastIndex; + } + this._lastKey = key; + this._lastIndex = this._keys.indexOf(key); + return this._lastIndex; + }, + _transformKey: function(key) { + return isNumberNaN(key) ? nanKey : key; }, - - /** - * @param key - * @returns {Object} the value for the key - */ get: function(key) { - return this[hashKey(key)]; + key = this._transformKey(key); + var idx = this._idx(key); + if (idx !== -1) { + return this._values[idx]; + } }, + set: function(key, value) { + key = this._transformKey(key); + var idx = this._idx(key); + if (idx === -1) { + idx = this._lastIndex = this._keys.length; + } + this._keys[idx] = key; + this._values[idx] = value; - /** - * Remove the key/value pair - * @param key - */ - remove: function(key) { - var value = this[key = hashKey(key)]; - delete this[key]; - return value; + // Support: IE11 + // Do not `return this` to simulate the partial IE11 implementation + }, + delete: function(key) { + key = this._transformKey(key); + var idx = this._idx(key); + if (idx === -1) { + return false; + } + this._keys.splice(idx, 1); + this._values.splice(idx, 1); + this._lastKey = NaN; + this._lastIndex = -1; + return true; } }; +// For now, always use `NgMapShim`, even if `window.Map` is available. Some native implementations +// are still buggy (often in subtle ways) and can cause hard-to-debug failures. When native `Map` +// implementations get more stable, we can reconsider switching to `window.Map` (when available). + var NgMap = NgMapShim; + + var $$MapProvider = [/** @this */function() { + this.$get = [function() { + return NgMap; + }]; + }]; + /** * @ngdoc function * @module ng * @name angular.injector - * @function + * @kind function * * @description - * Creates an injector function that can be used for retrieving services as well as for + * Creates an injector object that can be used for retrieving services as well as for * dependency injection (see {@link guide/di dependency injection}). * - * @param {Array.} modules A list of module functions or their aliases. See - * {@link angular.module}. The `ng` module must be explicitly added. - * @returns {function()} Injector function. See {@link auto.$injector $injector}. + * {@link angular.module}. The `ng` module must be explicitly added. + * @param {boolean=} [strictDi=false] Whether the injector should be in strict mode, which + * disallows argument name annotation inference. + * @returns {injector} Injector object. See {@link auto.$injector $injector}. * * @example * Typical usage @@ -3121,15 +4184,15 @@ * * // use the injector to kick off your application * // use the type inference to auto inject arguments, or use implicit injection - * $injector.invoke(function($rootScope, $compile, $document){ - * $compile($document)($rootScope); - * $rootScope.$digest(); - * }); + * $injector.invoke(function($rootScope, $compile, $document) { + * $compile($document)($rootScope); + * $rootScope.$digest(); + * }); * ``` * - * Sometimes you want to get access to the injector of a currently running Angular app - * from outside Angular. Perhaps, you want to inject and compile some markup after the - * application has been bootstrapped. You can do this using extra `injector()` added + * Sometimes you want to get access to the injector of a currently running AngularJS app + * from outside AngularJS. Perhaps, you want to inject and compile some markup after the + * application has been bootstrapped. You can do this using the extra `injector()` added * to JQuery/jqLite elements. See {@link angular.element}. * * *This is fairly rare but could be the case if a third party library is injecting the @@ -3144,9 +4207,9 @@ * $(document.body).append($div); * * angular.element(document).injector().invoke(function($compile) { - * var scope = angular.element($div).scope(); - * $compile($div)(scope); - * }); + * var scope = angular.element($div).scope(); + * $compile($div)(scope); + * }); * ``` */ @@ -3154,30 +4217,58 @@ /** * @ngdoc module * @name auto + * @installation * @description * * Implicit module which gets automatically added to each {@link auto.$injector $injector}. */ - var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m; + var ARROW_ARG = /^([^(]+?)=>/; + var FN_ARGS = /^[^(]*\(\s*([^)]*)\)/m; var FN_ARG_SPLIT = /,/; var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; var $injectorMinErr = minErr('$injector'); - function annotate(fn) { + + function stringifyFn(fn) { + return Function.prototype.toString.call(fn); + } + + function extractArgs(fn) { + var fnText = stringifyFn(fn).replace(STRIP_COMMENTS, ''), + args = fnText.match(ARROW_ARG) || fnText.match(FN_ARGS); + return args; + } + + function anonFn(fn) { + // For anonymous functions, showing at the very least the function signature can help in + // debugging. + var args = extractArgs(fn); + if (args) { + return 'function(' + (args[1] || '').replace(/[\s\r\n]+/, ' ') + ')'; + } + return 'fn'; + } + + function annotate(fn, strictDi, name) { var $inject, - fnText, argDecl, last; - if (typeof fn == 'function') { + if (typeof fn === 'function') { if (!($inject = fn.$inject)) { $inject = []; if (fn.length) { - fnText = fn.toString().replace(STRIP_COMMENTS, ''); - argDecl = fnText.match(FN_ARGS); - forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg){ - arg.replace(FN_ARG, function(all, underscore, name){ + if (strictDi) { + if (!isString(name) || !name) { + name = fn.name || anonFn(fn); + } + throw $injectorMinErr('strictdi', + '{0} is not using explicit annotation and cannot be invoked in strict mode', name); + } + argDecl = extractArgs(fn); + forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg) { + arg.replace(FN_ARG, function(all, underscore, name) { $inject.push(name); }); }); @@ -3199,7 +4290,6 @@ /** * @ngdoc service * @name $injector - * @function * * @description * @@ -3212,12 +4302,12 @@ * ```js * var $injector = angular.injector(); * expect($injector.get('$injector')).toBe($injector); - * expect($injector.invoke(function($injector){ - * return $injector; - * }).toBe($injector); + * expect($injector.invoke(function($injector) { + * return $injector; + * })).toBe($injector); * ``` * - * # Injection Function Annotation + * ## Injection Function Annotation * * JavaScript does not have annotations, and annotations are needed for dependency injection. The * following are all valid ways of annotating function with injection arguments and are equivalent. @@ -3235,19 +4325,43 @@ * $injector.invoke(['serviceA', function(serviceA){}]); * ``` * - * ## Inference + * ### Inference * * In JavaScript calling `toString()` on a function returns the function definition. The definition - * can then be parsed and the function arguments can be extracted. *NOTE:* This does not work with - * minification, and obfuscation tools since these tools change the argument names. + * can then be parsed and the function arguments can be extracted. This method of discovering + * annotations is disallowed when the injector is in strict mode. + * *NOTE:* This does not work with minification, and obfuscation tools since these tools change the + * argument names. * - * ## `$inject` Annotation - * By adding a `$inject` property onto a function the injection parameters can be specified. + * ### `$inject` Annotation + * By adding an `$inject` property onto a function the injection parameters can be specified. * - * ## Inline + * ### Inline * As an array of injection names, where the last item in the array is the function to call. */ + /** + * @ngdoc property + * @name $injector#modules + * @type {Object} + * @description + * A hash containing all the modules that have been loaded into the + * $injector. + * + * You can use this property to find out information about a module via the + * {@link angular.Module#info `myModule.info(...)`} method. + * + * For example: + * + * ``` + * var info = $injector.modules['ngAnimate'].info(); + * ``` + * + * **Do not use this property to attempt to modify the modules after the application + * has been bootstrapped.** + */ + + /** * @ngdoc method * @name $injector#get @@ -3256,6 +4370,7 @@ * Return an instance of the service. * * @param {string} name The name of the instance to retrieve. + * @param {string=} caller An optional string to provide the origin of the function call for error messages. * @return {*} The instance. */ @@ -3266,8 +4381,8 @@ * @description * Invoke the method and supply the method arguments from the `$injector`. * - * @param {!Function} fn The function to invoke. Function parameters are injected according to the - * {@link guide/di $inject Annotation} rules. + * @param {Function|Array.} fn The injectable function to invoke. Function parameters are + * injected according to the {@link guide/di $inject Annotation} rules. * @param {Object=} self The `this` for the invoked method. * @param {Object=} locals Optional object. If preset then any argument names are read from this * object first, before the `$injector` is consulted. @@ -3279,18 +4394,18 @@ * @name $injector#has * * @description - * Allows the user to query if the particular service exist. + * Allows the user to query if the particular service exists. * - * @param {string} Name of the service to query. - * @returns {boolean} returns true if injector has given service. + * @param {string} name Name of the service to query. + * @returns {boolean} `true` if injector has given service. */ /** * @ngdoc method * @name $injector#instantiate * @description - * Create a new instance of JS type. The method takes a constructor function invokes the new - * operator and supplies all of the arguments to the constructor function as specified by the + * Create a new instance of JS type. The method takes a constructor function, invokes the new + * operator, and supplies all of the arguments to the constructor function as specified by the * constructor annotation. * * @param {Function} Type Annotated constructor function. @@ -3309,7 +4424,7 @@ * function is invoked. There are three ways in which the function can be annotated with the needed * dependencies. * - * # Argument names + * #### Argument names * * The simplest form is to extract the dependencies from the arguments of the function. This is done * by converting the function into a string using `toString()` method and extracting the argument @@ -3317,25 +4432,27 @@ * ```js * // Given * function MyController($scope, $route) { - * // ... - * } + * // ... + * } * * // Then * expect(injector.annotate(MyController)).toEqual(['$scope', '$route']); * ``` * + * You can disallow this method by using strict injection mode. + * * This method does not work with code minification / obfuscation. For this reason the following * annotation strategies are supported. * - * # The `$inject` property + * #### The `$inject` property * * If a function has an `$inject` property and its value is an array of strings, then the strings * represent names of services to be injected into the function. * ```js * // Given * var MyController = function(obfuscatedScope, obfuscatedRoute) { - * // ... - * } + * // ... + * } * // Define function dependencies * MyController['$inject'] = ['$scope', '$route']; * @@ -3343,7 +4460,7 @@ * expect(injector.annotate(MyController)).toEqual(['$scope', '$route']); * ``` * - * # The array notation + * #### The array notation * * It is often desirable to inline Injected functions and that's when setting the `$inject` property * is very inconvenient. In these situations using the array notation to specify the dependencies in @@ -3352,20 +4469,20 @@ * ```js * // We wish to write this (not minification / obfuscation safe) * injector.invoke(function($compile, $rootScope) { - * // ... - * }); + * // ... + * }); * * // We are forced to write break inlining * var tmpFn = function(obfuscatedCompile, obfuscatedRootScope) { - * // ... - * }; + * // ... + * }; * tmpFn.$inject = ['$compile', '$rootScope']; * injector.invoke(tmpFn); * * // To better support inline function the inline annotation is supported * injector.invoke(['$compile', '$rootScope', function(obfCompile, obfRootScope) { - * // ... - * }]); + * // ... + * }]); * * // Therefore * expect(injector.annotate( @@ -3376,14 +4493,53 @@ * @param {Function|Array.} fn Function for which dependent service names need to * be retrieved as described above. * + * @param {boolean=} [strictDi=false] Disallow argument name annotation inference. + * * @returns {Array.} The names of the services which the function requires. */ - - + /** + * @ngdoc method + * @name $injector#loadNewModules + * + * @description + * + * **This is a dangerous API, which you use at your own risk!** + * + * Add the specified modules to the current injector. + * + * This method will add each of the injectables to the injector and execute all of the config and run + * blocks for each module passed to the method. + * + * If a module has already been loaded into the injector then it will not be loaded again. + * + * * The application developer is responsible for loading the code containing the modules; and for + * ensuring that lazy scripts are not downloaded and executed more often that desired. + * * Previously compiled HTML will not be affected by newly loaded directives, filters and components. + * * Modules cannot be unloaded. + * + * You can use {@link $injector#modules `$injector.modules`} to check whether a module has been loaded + * into the injector, which may indicate whether the script has been executed already. + * + * @example + * Here is an example of loading a bundle of modules, with a utility method called `getScript`: + * + * ```javascript + * app.factory('loadModule', function($injector) { + * return function loadModule(moduleName, bundleUrl) { + * return getScript(bundleUrl).then(function() { $injector.loadNewModules([moduleName]); }); + * }; + * }) + * ``` + * + * @param {Array=} mods an array of modules to load into the application. + * Each item in the array should be the name of a predefined module or a (DI annotated) + * function that will be invoked by the injector as a `config` block. + * See: {@link angular.module modules} + */ /** - * @ngdoc object + * @ngdoc service * @name $provide * * @description @@ -3392,7 +4548,7 @@ * with the {@link auto.$injector $injector}. Many of these functions are also exposed on * {@link angular.Module}. * - * An Angular **service** is a singleton object created by a **service factory**. These **service + * An AngularJS **service** is a singleton object created by a **service factory**. These **service * factories** are functions which, in turn, are created by a **service provider**. * The **service providers** are constructor functions. When instantiated they must contain a * property called `$get`, which holds the **service factory** function. @@ -3406,18 +4562,20 @@ * these cases the {@link auto.$provide $provide} service has additional helper methods to register * services without specifying a provider. * - * * {@link auto.$provide#provider provider(provider)} - registers a **service provider** with the + * * {@link auto.$provide#provider provider(name, provider)} - registers a **service provider** with the * {@link auto.$injector $injector} - * * {@link auto.$provide#constant constant(obj)} - registers a value/object that can be accessed by + * * {@link auto.$provide#constant constant(name, obj)} - registers a value/object that can be accessed by * providers and services. - * * {@link auto.$provide#value value(obj)} - registers a value/object that can only be accessed by + * * {@link auto.$provide#value value(name, obj)} - registers a value/object that can only be accessed by * services, not providers. - * * {@link auto.$provide#factory factory(fn)} - registers a service **factory function**, `fn`, + * * {@link auto.$provide#factory factory(name, fn)} - registers a service **factory function** * that will be wrapped in a **service provider** object, whose `$get` property will contain the * given factory function. - * * {@link auto.$provide#service service(class)} - registers a **constructor function**, `class` + * * {@link auto.$provide#service service(name, Fn)} - registers a **constructor function** * that will be wrapped in a **service provider** object, whose `$get` property will instantiate * a new object using the given constructor function. + * * {@link auto.$provide#decorator decorator(name, decorFn)} - registers a **decorator function** that + * will be able to modify or replace the implementation of another service. * * See the individual methods for more information and examples. */ @@ -3442,6 +4600,9 @@ * which lets you specify whether the {@link ng.$log $log} service will log debug messages to the * console or not. * + * It is possible to inject other providers into the provider function, + * but the injected provider must have been defined before the one that requires it. + * * @param {string} name The name of the instance. NOTE: the provider will be available under `name + 'Provider'` key. * @param {(Object|function())} provider If the provider is: @@ -3461,60 +4622,60 @@ * ```js * // Define the eventTracker provider * function EventTrackerProvider() { - * var trackingUrl = '/track'; - * - * // A provider method for configuring where the tracked events should been saved - * this.setTrackingUrl = function(url) { - * trackingUrl = url; - * }; - * - * // The service factory function - * this.$get = ['$http', function($http) { - * var trackedEvents = {}; - * return { - * // Call this to track an event - * event: function(event) { - * var count = trackedEvents[event] || 0; - * count += 1; - * trackedEvents[event] = count; - * return count; - * }, - * // Call this to save the tracked events to the trackingUrl - * save: function() { - * $http.post(trackingUrl, trackedEvents); - * } - * }; - * }]; - * } + * var trackingUrl = '/track'; + * + * // A provider method for configuring where the tracked events should been saved + * this.setTrackingUrl = function(url) { + * trackingUrl = url; + * }; + * + * // The service factory function + * this.$get = ['$http', function($http) { + * var trackedEvents = {}; + * return { + * // Call this to track an event + * event: function(event) { + * var count = trackedEvents[event] || 0; + * count += 1; + * trackedEvents[event] = count; + * return count; + * }, + * // Call this to save the tracked events to the trackingUrl + * save: function() { + * $http.post(trackingUrl, trackedEvents); + * } + * }; + * }]; + * } * * describe('eventTracker', function() { - * var postSpy; - * - * beforeEach(module(function($provide) { - * // Register the eventTracker provider - * $provide.provider('eventTracker', EventTrackerProvider); - * })); - * - * beforeEach(module(function(eventTrackerProvider) { - * // Configure eventTracker provider - * eventTrackerProvider.setTrackingUrl('/custom-track'); - * })); - * - * it('tracks events', inject(function(eventTracker) { - * expect(eventTracker.event('login')).toEqual(1); - * expect(eventTracker.event('login')).toEqual(2); - * })); - * - * it('saves to the tracking url', inject(function(eventTracker, $http) { - * postSpy = spyOn($http, 'post'); - * eventTracker.event('login'); - * eventTracker.save(); - * expect(postSpy).toHaveBeenCalled(); - * expect(postSpy.mostRecentCall.args[0]).not.toEqual('/track'); - * expect(postSpy.mostRecentCall.args[0]).toEqual('/custom-track'); - * expect(postSpy.mostRecentCall.args[1]).toEqual({ 'login': 1 }); - * })); - * }); + * var postSpy; + * + * beforeEach(module(function($provide) { + * // Register the eventTracker provider + * $provide.provider('eventTracker', EventTrackerProvider); + * })); + * + * beforeEach(module(function(eventTrackerProvider) { + * // Configure eventTracker provider + * eventTrackerProvider.setTrackingUrl('/custom-track'); + * })); + * + * it('tracks events', inject(function(eventTracker) { + * expect(eventTracker.event('login')).toEqual(1); + * expect(eventTracker.event('login')).toEqual(2); + * })); + * + * it('saves to the tracking url', inject(function(eventTracker, $http) { + * postSpy = spyOn($http, 'post'); + * eventTracker.event('login'); + * eventTracker.save(); + * expect(postSpy).toHaveBeenCalled(); + * expect(postSpy.mostRecentCall.args[0]).not.toEqual('/track'); + * expect(postSpy.mostRecentCall.args[0]).toEqual('/custom-track'); + * expect(postSpy.mostRecentCall.args[1]).toEqual({ 'login': 1 }); + * })); + * }); * ``` */ @@ -3530,24 +4691,24 @@ * configure your service in a provider. * * @param {string} name The name of the instance. - * @param {function()} $getFn The $getFn for the instance creation. Internally this is a short hand - * for `$provide.provider(name, {$get: $getFn})`. + * @param {Function|Array.} $getFn The injectable $getFn for the instance creation. + * Internally this is a short hand for `$provide.provider(name, {$get: $getFn})`. * @returns {Object} registered provider instance * * @example * Here is an example of registering a service * ```js * $provide.factory('ping', ['$http', function($http) { - * return function ping() { - * return $http.send('/ping'); - * }; - * }]); + * return function ping() { + * return $http.send('/ping'); + * }; + * }]); * ``` * You would then inject and use this service like this: * ```js * someModule.controller('Ctrl', ['ping', function(ping) { - * ping(); - * }]); + * ping(); + * }]); * ``` */ @@ -3559,14 +4720,27 @@ * * Register a **service constructor**, which will be invoked with `new` to create the service * instance. - * This is short for registering a service where its provider's `$get` property is the service - * constructor function that will be used to instantiate the service instance. + * This is short for registering a service where its provider's `$get` property is a factory + * function that returns an instance instantiated by the injector from the service constructor + * function. + * + * Internally it looks a bit like this: + * + * ``` + * { + * $get: function() { + * return $injector.instantiate(constructor); + * } + * } + * ``` + * * * You should use {@link auto.$provide#service $provide.service(class)} if you define your service * as a type/class. * * @param {string} name The name of the instance. - * @param {Function} constructor A class (constructor function) that will be instantiated. + * @param {Function|Array.} constructor An injectable class (constructor function) + * that will be instantiated. * @returns {Object} registered provider instance * * @example @@ -3574,21 +4748,21 @@ * {@link auto.$provide#service $provide.service(class)}. * ```js * var Ping = function($http) { - * this.$http = $http; - * }; + * this.$http = $http; + * }; * * Ping.$inject = ['$http']; * * Ping.prototype.send = function() { - * return this.$http.get('/ping'); - * }; + * return this.$http.get('/ping'); + * }; * $provide.service('ping', Ping); * ``` * You would then inject and use this service like this: * ```js * someModule.controller('Ctrl', ['ping', function(ping) { - * ping.send(); - * }]); + * ping.send(); + * }]); * ``` */ @@ -3599,14 +4773,13 @@ * @description * * Register a **value service** with the {@link auto.$injector $injector}, such as a string, a - * number, an array, an object or a function. This is short for registering a service where its + * number, an array, an object or a function. This is short for registering a service where its * provider's `$get` property is a factory function that takes no arguments and returns the **value - * service**. + * service**. That also means it is not possible to inject other services into a value service. * * Value services are similar to constant services, except that they cannot be injected into a * module configuration function (see {@link angular.Module#config}) but they can be overridden by - * an Angular - * {@link auto.$provide#decorator decorator}. + * an AngularJS {@link auto.$provide#decorator decorator}. * * @param {string} name The name of the instance. * @param {*} value The value. @@ -3620,8 +4793,8 @@ * $provide.value('RoleLookup', { admin: 0, writer: 1, reader: 2 }); * * $provide.value('halfOf', function(value) { - * return value / 2; - * }); + * return value / 2; + * }); * ``` */ @@ -3631,10 +4804,13 @@ * @name $provide#constant * @description * - * Register a **constant service**, such as a string, a number, an array, an object or a function, - * with the {@link auto.$injector $injector}. Unlike {@link auto.$provide#value value} it can be + * Register a **constant service** with the {@link auto.$injector $injector}, such as a string, + * a number, an array, an object or a function. Like the {@link auto.$provide#value value}, it is not + * possible to inject other services into a constant. + * + * But unlike {@link auto.$provide#value value}, a constant can be * injected into a module configuration function (see {@link angular.Module#config}) and it cannot - * be overridden by an Angular {@link auto.$provide#decorator decorator}. + * be overridden by an AngularJS {@link auto.$provide#decorator decorator}. * * @param {string} name The name of the constant. * @param {*} value The constant value. @@ -3648,8 +4824,8 @@ * $provide.constant('MY_COLOURS', ['red', 'blue', 'grey']); * * $provide.constant('double', function(value) { - * return value * 2; - * }); + * return value * 2; + * }); * ``` */ @@ -3659,18 +4835,20 @@ * @name $provide#decorator * @description * - * Register a **service decorator** with the {@link auto.$injector $injector}. A service decorator - * intercepts the creation of a service, allowing it to override or modify the behaviour of the - * service. The object returned by the decorator may be the original service, or a new service - * object which replaces or wraps and delegates to the original service. + * Register a **decorator function** with the {@link auto.$injector $injector}. A decorator function + * intercepts the creation of a service, allowing it to override or modify the behavior of the + * service. The return value of the decorator function may be the original service, or a new service + * that replaces (or wraps and delegates to) the original service. + * + * You can find out more about using decorators in the {@link guide/decorators} guide. * * @param {string} name The name of the service to decorate. - * @param {function()} decorator This function will be invoked when the service needs to be - * instantiated and should return the decorated service instance. The function is called using + * @param {Function|Array.} decorator This function will be invoked when the service needs to be + * provided and should return the decorated service instance. The function is called using * the {@link auto.$injector#invoke injector.invoke} method and is therefore fully injectable. * Local injection arguments: * - * * `$delegate` - The original service instance, which can be monkey patched, configured, + * * `$delegate` - The original service instance, which can be replaced, monkey patched, configured, * decorated or delegated to. * * @example @@ -3678,18 +4856,19 @@ * calls to {@link ng.$log#error $log.warn()}. * ```js * $provide.decorator('$log', ['$delegate', function($delegate) { - * $delegate.warn = $delegate.error; - * return $delegate; - * }]); + * $delegate.warn = $delegate.error; + * return $delegate; + * }]); * ``` */ - function createInjector(modulesToLoad) { + function createInjector(modulesToLoad, strictDi) { + strictDi = (strictDi === true); var INSTANTIATING = {}, providerSuffix = 'Provider', path = [], - loadedModules = new HashMap(), + loadedModules = new NgMap(), providerCache = { $provide: { provider: supportObject(provider), @@ -3701,18 +4880,32 @@ } }, providerInjector = (providerCache.$injector = - createInternalInjector(providerCache, function() { - throw $injectorMinErr('unpr', "Unknown provider: {0}", path.join(' <- ')); + createInternalInjector(providerCache, function(serviceName, caller) { + if (angular.isString(caller)) { + path.push(caller); + } + throw $injectorMinErr('unpr', 'Unknown provider: {0}', path.join(' <- ')); })), instanceCache = {}, - instanceInjector = (instanceCache.$injector = - createInternalInjector(instanceCache, function(servicename) { - var provider = providerInjector.get(servicename + providerSuffix); - return instanceInjector.invoke(provider.$get, provider); - })); + protoInstanceInjector = + createInternalInjector(instanceCache, function(serviceName, caller) { + var provider = providerInjector.get(serviceName + providerSuffix, caller); + return instanceInjector.invoke( + provider.$get, provider, undefined, serviceName); + }), + instanceInjector = protoInstanceInjector; + + providerCache['$injector' + providerSuffix] = { $get: valueFn(protoInstanceInjector) }; + instanceInjector.modules = providerInjector.modules = createMap(); + var runBlocks = loadModules(modulesToLoad); + instanceInjector = protoInstanceInjector.get('$injector'); + instanceInjector.strictDi = strictDi; + forEach(runBlocks, function(fn) { if (fn) instanceInjector.invoke(fn); }); + instanceInjector.loadNewModules = function(mods) { + forEach(loadModules(mods), function(fn) { if (fn) instanceInjector.invoke(fn); }); + }; - forEach(loadModules(modulesToLoad), function(fn) { instanceInjector.invoke(fn || noop); }); return instanceInjector; @@ -3736,12 +4929,26 @@ provider_ = providerInjector.instantiate(provider_); } if (!provider_.$get) { - throw $injectorMinErr('pget', "Provider '{0}' must define $get factory method.", name); + throw $injectorMinErr('pget', 'Provider \'{0}\' must define $get factory method.', name); } - return providerCache[name + providerSuffix] = provider_; + return (providerCache[name + providerSuffix] = provider_); + } + + function enforceReturnValue(name, factory) { + return /** @this */ function enforcedReturnValue() { + var result = instanceInjector.invoke(factory, this); + if (isUndefined(result)) { + throw $injectorMinErr('undef', 'Provider \'{0}\' must return a value from $get factory method.', name); + } + return result; + }; } - function factory(name, factoryFn) { return provider(name, { $get: factoryFn }); } + function factory(name, factoryFn, enforce) { + return provider(name, { + $get: enforce !== false ? enforceReturnValue(name, factoryFn) : factoryFn + }); + } function service(name, constructor) { return factory(name, ['$injector', function($injector) { @@ -3749,7 +4956,7 @@ }]); } - function value(name, val) { return factory(name, valueFn(val)); } + function value(name, val) { return factory(name, valueFn(val), false); } function constant(name, value) { assertNotHasOwnProperty(name, 'constant'); @@ -3770,23 +4977,30 @@ //////////////////////////////////// // Module Loading //////////////////////////////////// - function loadModules(modulesToLoad){ - var runBlocks = [], moduleFn, invokeQueue, i, ii; + function loadModules(modulesToLoad) { + assertArg(isUndefined(modulesToLoad) || isArray(modulesToLoad), 'modulesToLoad', 'not an array'); + var runBlocks = [], moduleFn; forEach(modulesToLoad, function(module) { if (loadedModules.get(module)) return; - loadedModules.put(module, true); + loadedModules.set(module, true); + + function runInvokeQueue(queue) { + var i, ii; + for (i = 0, ii = queue.length; i < ii; i++) { + var invokeArgs = queue[i], + provider = providerInjector.get(invokeArgs[0]); + + provider[invokeArgs[1]].apply(provider, invokeArgs[2]); + } + } try { if (isString(module)) { moduleFn = angularModule(module); + instanceInjector.modules[module] = moduleFn; runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks); - - for(invokeQueue = moduleFn._invokeQueue, i = 0, ii = invokeQueue.length; i < ii; i++) { - var invokeArgs = invokeQueue[i], - provider = providerInjector.get(invokeArgs[0]); - - provider[invokeArgs[1]].apply(provider, invokeArgs[2]); - } + runInvokeQueue(moduleFn._invokeQueue); + runInvokeQueue(moduleFn._configBlocks); } else if (isFunction(module)) { runBlocks.push(providerInjector.invoke(module)); } else if (isArray(module)) { @@ -3798,15 +5012,15 @@ if (isArray(module)) { module = module[module.length - 1]; } - if (e.message && e.stack && e.stack.indexOf(e.message) == -1) { + if (e.message && e.stack && e.stack.indexOf(e.message) === -1) { // Safari & FF's stack traces don't contain error.message content // unlike those of Chrome and IE // So if stack doesn't contain message, we create a new string that contains both. // Since error.stack is read-only in Safari, I'm overriding e and not e.stack here. - /* jshint -W022 */ + // eslint-disable-next-line no-ex-assign e = e.message + '\n' + e.stack; } - throw $injectorMinErr('modulerr', "Failed to instantiate module {0} due to:\n{1}", + throw $injectorMinErr('modulerr', 'Failed to instantiate module {0} due to:\n{1}', module, e.stack || e.message || e); } }); @@ -3819,17 +5033,19 @@ function createInternalInjector(cache, factory) { - function getService(serviceName) { + function getService(serviceName, caller) { if (cache.hasOwnProperty(serviceName)) { if (cache[serviceName] === INSTANTIATING) { - throw $injectorMinErr('cdep', 'Circular dependency found: {0}', path.join(' <- ')); + throw $injectorMinErr('cdep', 'Circular dependency found: {0}', + serviceName + ' <- ' + path.join(' <- ')); } return cache[serviceName]; } else { try { path.unshift(serviceName); cache[serviceName] = INSTANTIATING; - return cache[serviceName] = factory(serviceName); + cache[serviceName] = factory(serviceName, caller); + return cache[serviceName]; } catch (err) { if (cache[serviceName] === INSTANTIATING) { delete cache[serviceName]; @@ -3841,52 +5057,76 @@ } } - function invoke(fn, self, locals){ + + function injectionArgs(fn, locals, serviceName) { var args = [], - $inject = annotate(fn), - length, i, - key; + $inject = createInjector.$$annotate(fn, strictDi, serviceName); - for(i = 0, length = $inject.length; i < length; i++) { - key = $inject[i]; + for (var i = 0, length = $inject.length; i < length; i++) { + var key = $inject[i]; if (typeof key !== 'string') { throw $injectorMinErr('itkn', 'Incorrect injection token! Expected service name as string, got {0}', key); } - args.push( - locals && locals.hasOwnProperty(key) - ? locals[key] - : getService(key) - ); + args.push(locals && locals.hasOwnProperty(key) ? locals[key] : + getService(key, serviceName)); + } + return args; + } + + function isClass(func) { + // Support: IE 9-11 only + // IE 9-11 do not support classes and IE9 leaks with the code below. + if (msie || typeof func !== 'function') { + return false; + } + var result = func.$$ngIsClass; + if (!isBoolean(result)) { + // Support: Edge 12-13 only + // See: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/6156135/ + result = func.$$ngIsClass = /^(?:class\b|constructor\()/.test(stringifyFn(func)); } - if (!fn.$inject) { - // this means that we must be an array. - fn = fn[length]; + return result; + } + + function invoke(fn, self, locals, serviceName) { + if (typeof locals === 'string') { + serviceName = locals; + locals = null; + } + + var args = injectionArgs(fn, locals, serviceName); + if (isArray(fn)) { + fn = fn[fn.length - 1]; } - // http://jsperf.com/angularjs-invoke-apply-vs-switch - // #5388 - return fn.apply(self, args); + if (!isClass(fn)) { + // http://jsperf.com/angularjs-invoke-apply-vs-switch + // #5388 + return fn.apply(self, args); + } else { + args.unshift(null); + return new (Function.prototype.bind.apply(fn, args))(); + } } - function instantiate(Type, locals) { - var Constructor = function() {}, - instance, returnedValue; + function instantiate(Type, locals, serviceName) { // Check if Type is annotated and use just the given function at n-1 as parameter // e.g. someModule.factory('greeter', ['$window', function(renamed$window) {}]); - Constructor.prototype = (isArray(Type) ? Type[Type.length - 1] : Type).prototype; - instance = new Constructor(); - returnedValue = invoke(Type, instance, locals); - - return isObject(returnedValue) || isFunction(returnedValue) ? returnedValue : instance; + var ctor = (isArray(Type) ? Type[Type.length - 1] : Type); + var args = injectionArgs(Type, locals, serviceName); + // Empty object at position 0 is ignored for invocation with `new`, but required. + args.unshift(null); + return new (Function.prototype.bind.apply(ctor, args))(); } + return { invoke: invoke, instantiate: instantiate, get: getService, - annotate: annotate, + annotate: createInjector.$$annotate, has: function(name) { return providerCache.hasOwnProperty(name + providerSuffix) || cache.hasOwnProperty(name); } @@ -3894,108 +5134,445 @@ } } + createInjector.$$annotate = annotate; + /** - * @ngdoc service - * @name $anchorScroll - * @kind function - * @requires $window - * @requires $location - * @requires $rootScope + * @ngdoc provider + * @name $anchorScrollProvider + * @this * * @description - * When called, it checks current value of `$location.hash()` and scroll to related element, - * according to rules specified in - * [Html5 spec](http://dev.w3.org/html5/spec/Overview.html#the-indicated-part-of-the-document). - * - * It also watches the `$location.hash()` and scrolls whenever it changes to match any anchor. - * This can be disabled by calling `$anchorScrollProvider.disableAutoScrolling()`. - * - * @example - - -
    - Go to bottom - You're at the bottom! -
    -
    - - function ScrollCtrl($scope, $location, $anchorScroll) { - $scope.gotoBottom = function (){ - // set the location.hash to the id of - // the element you wish to scroll to. - $location.hash('bottom'); - - // call $anchorScroll() - $anchorScroll(); - }; - } - - - #scrollArea { - height: 350px; - overflow: auto; - } - - #bottom { - display: block; - margin-top: 2000px; - } - -
    + * Use `$anchorScrollProvider` to disable automatic scrolling whenever + * {@link ng.$location#hash $location.hash()} changes. */ function $AnchorScrollProvider() { var autoScrollingEnabled = true; + /** + * @ngdoc method + * @name $anchorScrollProvider#disableAutoScrolling + * + * @description + * By default, {@link ng.$anchorScroll $anchorScroll()} will automatically detect changes to + * {@link ng.$location#hash $location.hash()} and scroll to the element matching the new hash.
    + * Use this method to disable automatic scrolling. + * + * If automatic scrolling is disabled, one must explicitly call + * {@link ng.$anchorScroll $anchorScroll()} in order to scroll to the element related to the + * current hash. + */ this.disableAutoScrolling = function() { autoScrollingEnabled = false; }; + /** + * @ngdoc service + * @name $anchorScroll + * @kind function + * @requires $window + * @requires $location + * @requires $rootScope + * + * @description + * When called, it scrolls to the element related to the specified `hash` or (if omitted) to the + * current value of {@link ng.$location#hash $location.hash()}, according to the rules specified + * in the + * [HTML5 spec](http://www.w3.org/html/wg/drafts/html/master/browsers.html#an-indicated-part-of-the-document). + * + * It also watches the {@link ng.$location#hash $location.hash()} and automatically scrolls to + * match any anchor whenever it changes. This can be disabled by calling + * {@link ng.$anchorScrollProvider#disableAutoScrolling $anchorScrollProvider.disableAutoScrolling()}. + * + * Additionally, you can use its {@link ng.$anchorScroll#yOffset yOffset} property to specify a + * vertical scroll-offset (either fixed or dynamic). + * + * @param {string=} hash The hash specifying the element to scroll to. If omitted, the value of + * {@link ng.$location#hash $location.hash()} will be used. + * + * @property {(number|function|jqLite)} yOffset + * If set, specifies a vertical scroll-offset. This is often useful when there are fixed + * positioned elements at the top of the page, such as navbars, headers etc. + * + * `yOffset` can be specified in various ways: + * - **number**: A fixed number of pixels to be used as offset.

    + * - **function**: A getter function called everytime `$anchorScroll()` is executed. Must return + * a number representing the offset (in pixels).

    + * - **jqLite**: A jqLite/jQuery element to be used for specifying the offset. The distance from + * the top of the page to the element's bottom will be used as offset.
    + * **Note**: The element will be taken into account only as long as its `position` is set to + * `fixed`. This option is useful, when dealing with responsive navbars/headers that adjust + * their height and/or positioning according to the viewport's size. + * + *
    + *
    + * In order for `yOffset` to work properly, scrolling should take place on the document's root and + * not some child element. + *
    + * + * @example + + +
    + Go to bottom + You're at the bottom! +
    +
    + + angular.module('anchorScrollExample', []) + .controller('ScrollController', ['$scope', '$location', '$anchorScroll', + function($scope, $location, $anchorScroll) { + $scope.gotoBottom = function() { + // set the location.hash to the id of + // the element you wish to scroll to. + $location.hash('bottom'); + + // call $anchorScroll() + $anchorScroll(); + }; + }]); + + + #scrollArea { + height: 280px; + overflow: auto; + } + + #bottom { + display: block; + margin-top: 2000px; + } + +
    + * + *
    + * The example below illustrates the use of a vertical scroll-offset (specified as a fixed value). + * See {@link ng.$anchorScroll#yOffset $anchorScroll.yOffset} for more details. + * + * @example + + + +
    + Anchor {{x}} of 5 +
    +
    + + angular.module('anchorScrollOffsetExample', []) + .run(['$anchorScroll', function($anchorScroll) { + $anchorScroll.yOffset = 50; // always scroll by 50 extra pixels + }]) + .controller('headerCtrl', ['$anchorScroll', '$location', '$scope', + function($anchorScroll, $location, $scope) { + $scope.gotoAnchor = function(x) { + var newHash = 'anchor' + x; + if ($location.hash() !== newHash) { + // set the $location.hash to `newHash` and + // $anchorScroll will automatically scroll to it + $location.hash('anchor' + x); + } else { + // call $anchorScroll() explicitly, + // since $location.hash hasn't changed + $anchorScroll(); + } + }; + } + ]); + + + body { + padding-top: 50px; + } + + .anchor { + border: 2px dashed DarkOrchid; + padding: 10px 10px 200px 10px; + } + + .fixed-header { + background-color: rgba(0, 0, 0, 0.2); + height: 50px; + position: fixed; + top: 0; left: 0; right: 0; + } + + .fixed-header > a { + display: inline-block; + margin: 5px 15px; + } + +
    + */ this.$get = ['$window', '$location', '$rootScope', function($window, $location, $rootScope) { var document = $window.document; - // helper function to get first anchor from a NodeList - // can't use filter.filter, as it accepts only instances of Array - // and IE can't convert NodeList to an array using [].slice - // TODO(vojta): use filter if we change it to accept lists as well + // Helper function to get first anchor from a NodeList + // (using `Array#some()` instead of `angular#forEach()` since it's more performant + // and working in all supported browsers.) function getFirstAnchor(list) { var result = null; - forEach(list, function(element) { - if (!result && lowercase(element.nodeName) === 'a') result = element; + Array.prototype.some.call(list, function(element) { + if (nodeName_(element) === 'a') { + result = element; + return true; + } }); return result; } - function scroll() { - var hash = $location.hash(), elm; - - // empty hash, scroll to the top of the page - if (!hash) $window.scrollTo(0, 0); + function getYOffset() { - // element with given id - else if ((elm = document.getElementById(hash))) elm.scrollIntoView(); + var offset = scroll.yOffset; - // first anchor with given name :-D - else if ((elm = getFirstAnchor(document.getElementsByName(hash)))) elm.scrollIntoView(); + if (isFunction(offset)) { + offset = offset(); + } else if (isElement(offset)) { + var elem = offset[0]; + var style = $window.getComputedStyle(elem); + if (style.position !== 'fixed') { + offset = 0; + } else { + offset = elem.getBoundingClientRect().bottom; + } + } else if (!isNumber(offset)) { + offset = 0; + } - // no element and hash == 'top', scroll to the top of the page - else if (hash === 'top') $window.scrollTo(0, 0); + return offset; } - // does not scroll when user clicks on anchor link that is currently on - // (no url change, no $location.hash() change), browser native does scroll - if (autoScrollingEnabled) { - $rootScope.$watch(function autoScrollWatch() {return $location.hash();}, - function autoScrollWatchAction() { - $rootScope.$evalAsync(scroll); - }); - } + function scrollTo(elem) { + if (elem) { + elem.scrollIntoView(); - return scroll; + var offset = getYOffset(); + + if (offset) { + // `offset` is the number of pixels we should scroll UP in order to align `elem` properly. + // This is true ONLY if the call to `elem.scrollIntoView()` initially aligns `elem` at the + // top of the viewport. + // + // IF the number of pixels from the top of `elem` to the end of the page's content is less + // than the height of the viewport, then `elem.scrollIntoView()` will align the `elem` some + // way down the page. + // + // This is often the case for elements near the bottom of the page. + // + // In such cases we do not need to scroll the whole `offset` up, just the difference between + // the top of the element and the offset, which is enough to align the top of `elem` at the + // desired position. + var elemTop = elem.getBoundingClientRect().top; + $window.scrollBy(0, elemTop - offset); + } + } else { + $window.scrollTo(0, 0); + } + } + + function scroll(hash) { + // Allow numeric hashes + hash = isString(hash) ? hash : isNumber(hash) ? hash.toString() : $location.hash(); + var elm; + + // empty hash, scroll to the top of the page + if (!hash) scrollTo(null); + + // element with given id + else if ((elm = document.getElementById(hash))) scrollTo(elm); + + // first anchor with given name :-D + else if ((elm = getFirstAnchor(document.getElementsByName(hash)))) scrollTo(elm); + + // no element and hash === 'top', scroll to the top of the page + else if (hash === 'top') scrollTo(null); + } + + // does not scroll when user clicks on anchor link that is currently on + // (no url change, no $location.hash() change), browser native does scroll + if (autoScrollingEnabled) { + $rootScope.$watch(function autoScrollWatch() {return $location.hash();}, + function autoScrollWatchAction(newVal, oldVal) { + // skip the initial scroll if $location.hash is empty + if (newVal === oldVal && newVal === '') return; + + jqLiteDocumentLoaded(function() { + $rootScope.$evalAsync(scroll); + }); + }); + } + + return scroll; }]; } var $animateMinErr = minErr('$animate'); + var ELEMENT_NODE = 1; + var NG_ANIMATE_CLASSNAME = 'ng-animate'; + + function mergeClasses(a,b) { + if (!a && !b) return ''; + if (!a) return b; + if (!b) return a; + if (isArray(a)) a = a.join(' '); + if (isArray(b)) b = b.join(' '); + return a + ' ' + b; + } + + function extractElementNode(element) { + for (var i = 0; i < element.length; i++) { + var elm = element[i]; + if (elm.nodeType === ELEMENT_NODE) { + return elm; + } + } + } + + function splitClasses(classes) { + if (isString(classes)) { + classes = classes.split(' '); + } + + // Use createMap() to prevent class assumptions involving property names in + // Object.prototype + var obj = createMap(); + forEach(classes, function(klass) { + // sometimes the split leaves empty string values + // incase extra spaces were applied to the options + if (klass.length) { + obj[klass] = true; + } + }); + return obj; + } + +// if any other type of options value besides an Object value is +// passed into the $animate.method() animation then this helper code +// will be run which will ignore it. While this patch is not the +// greatest solution to this, a lot of existing plugins depend on +// $animate to either call the callback (< 1.2) or return a promise +// that can be changed. This helper function ensures that the options +// are wiped clean incase a callback function is provided. + function prepareAnimateOptions(options) { + return isObject(options) + ? options + : {}; + } + + var $$CoreAnimateJsProvider = /** @this */ function() { + this.$get = noop; + }; + +// this is prefixed with Core since it conflicts with +// the animateQueueProvider defined in ngAnimate/animateQueue.js + var $$CoreAnimateQueueProvider = /** @this */ function() { + var postDigestQueue = new NgMap(); + var postDigestElements = []; + + this.$get = ['$$AnimateRunner', '$rootScope', + function($$AnimateRunner, $rootScope) { + return { + enabled: noop, + on: noop, + off: noop, + pin: noop, + + push: function(element, event, options, domOperation) { + if (domOperation) { + domOperation(); + } + + options = options || {}; + if (options.from) { + element.css(options.from); + } + if (options.to) { + element.css(options.to); + } + + if (options.addClass || options.removeClass) { + addRemoveClassesPostDigest(element, options.addClass, options.removeClass); + } + + var runner = new $$AnimateRunner(); + + // since there are no animations to run the runner needs to be + // notified that the animation call is complete. + runner.complete(); + return runner; + } + }; + + + function updateData(data, classes, value) { + var changed = false; + if (classes) { + classes = isString(classes) ? classes.split(' ') : + isArray(classes) ? classes : []; + forEach(classes, function(className) { + if (className) { + changed = true; + data[className] = value; + } + }); + } + return changed; + } + + function handleCSSClassChanges() { + forEach(postDigestElements, function(element) { + var data = postDigestQueue.get(element); + if (data) { + var existing = splitClasses(element.attr('class')); + var toAdd = ''; + var toRemove = ''; + forEach(data, function(status, className) { + var hasClass = !!existing[className]; + if (status !== hasClass) { + if (status) { + toAdd += (toAdd.length ? ' ' : '') + className; + } else { + toRemove += (toRemove.length ? ' ' : '') + className; + } + } + }); + + forEach(element, function(elm) { + if (toAdd) { + jqLiteAddClass(elm, toAdd); + } + if (toRemove) { + jqLiteRemoveClass(elm, toRemove); + } + }); + postDigestQueue.delete(element); + } + }); + postDigestElements.length = 0; + } + + + function addRemoveClassesPostDigest(element, add, remove) { + var data = postDigestQueue.get(element) || {}; + + var classesAdded = updateData(data, add, true); + var classesRemoved = updateData(data, remove, false); + + if (classesAdded || classesRemoved) { + + postDigestQueue.set(element, data); + postDigestElements.push(element); + + if (postDigestElements.length === 1) { + $rootScope.$$postDigest(handleCSSClassChanges); + } + } + } + }]; + }; /** * @ngdoc provider @@ -4003,18 +5580,18 @@ * * @description * Default implementation of $animate that doesn't perform any animations, instead just - * synchronously performs DOM - * updates and calls done() callbacks. + * synchronously performs DOM updates and resolves the returned runner promise. * - * In order to enable animations the ngAnimate module has to be loaded. + * In order to enable animations the `ngAnimate` module has to be loaded. * - * To see the functional implementation check out src/ngAnimate/animate.js + * To see the functional implementation check out `src/ngAnimate/animate.js`. */ - var $AnimateProvider = ['$provide', function($provide) { - - - this.$$selectors = {}; + var $AnimateProvider = ['$provide', /** @this */ function($provide) { + var provider = this; + var classNameFilter = null; + var customFilter = null; + this.$$registeredAnimations = Object.create(null); /** * @ngdoc method @@ -4025,36 +5602,91 @@ * animation object which contains callback functions for each event that is expected to be * animated. * - * * `eventFn`: `function(Element, doneFunction)` The element to animate, the `doneFunction` - * must be called once the element animation is complete. If a function is returned then the - * animation service will use this function to cancel the animation whenever a cancel event is - * triggered. + * * `eventFn`: `function(element, ... , doneFunction, options)` + * The element to animate, the `doneFunction` and the options fed into the animation. Depending + * on the type of animation additional arguments will be injected into the animation function. The + * list below explains the function signatures for the different animation methods: * + * - setClass: function(element, addedClasses, removedClasses, doneFunction, options) + * - addClass: function(element, addedClasses, doneFunction, options) + * - removeClass: function(element, removedClasses, doneFunction, options) + * - enter, leave, move: function(element, doneFunction, options) + * - animate: function(element, fromStyles, toStyles, doneFunction, options) + * + * Make sure to trigger the `doneFunction` once the animation is fully complete. * * ```js * return { - * eventFn : function(element, done) { - * //code to run the animation - * //once complete, then run done() - * return function cancellationFunction() { - * //code to cancel the animation - * } - * } - * } + * //enter, leave, move signature + * eventFn : function(element, done, options) { + * //code to run the animation + * //once complete, then run done() + * return function endFunction(wasCancelled) { + * //code to cancel the animation + * } + * } + * } * ``` * - * @param {string} name The name of the animation. + * @param {string} name The name of the animation (this is what the class-based CSS value will be compared to). * @param {Function} factory The factory function that will be executed to return the animation * object. */ this.register = function(name, factory) { + if (name && name.charAt(0) !== '.') { + throw $animateMinErr('notcsel', 'Expecting class selector starting with \'.\' got \'{0}\'.', name); + } + var key = name + '-animation'; - if (name && name.charAt(0) != '.') throw $animateMinErr('notcsel', - "Expecting class selector starting with '.' got '{0}'.", name); - this.$$selectors[name.substr(1)] = key; + provider.$$registeredAnimations[name.substr(1)] = key; $provide.factory(key, factory); }; + /** + * @ngdoc method + * @name $animateProvider#customFilter + * + * @description + * Sets and/or returns the custom filter function that is used to "filter" animations, i.e. + * determine if an animation is allowed or not. When no filter is specified (the default), no + * animation will be blocked. Setting the `customFilter` value will only allow animations for + * which the filter function's return value is truthy. + * + * This allows to easily create arbitrarily complex rules for filtering animations, such as + * allowing specific events only, or enabling animations on specific subtrees of the DOM, etc. + * Filtering animations can also boost performance for low-powered devices, as well as + * applications containing a lot of structural operations. + * + *
    + * **Best Practice:** + * Keep the filtering function as lean as possible, because it will be called for each DOM + * action (e.g. insertion, removal, class change) performed by "animation-aware" directives. + * See {@link guide/animations#which-directives-support-animations- here} for a list of built-in + * directives that support animations. + * Performing computationally expensive or time-consuming operations on each call of the + * filtering function can make your animations sluggish. + *
    + * + * **Note:** If present, `customFilter` will be checked before + * {@link $animateProvider#classNameFilter classNameFilter}. + * + * @param {Function=} filterFn - The filter function which will be used to filter all animations. + * If a falsy value is returned, no animation will be performed. The function will be called + * with the following arguments: + * - **node** `{DOMElement}` - The DOM element to be animated. + * - **event** `{String}` - The name of the animation event (e.g. `enter`, `leave`, `addClass` + * etc). + * - **options** `{Object}` - A collection of options/styles used for the animation. + * @return {Function} The current filter function or `null` if there is none set. + */ + this.customFilter = function(filterFn) { + if (arguments.length === 1) { + customFilter = isFunction(filterFn) ? filterFn : null; + } + + return customFilter; + }; + /** * @ngdoc method * @name $animateProvider#classNameFilter @@ -4062,220 +5694,716 @@ * @description * Sets and/or returns the CSS class regular expression that is checked when performing * an animation. Upon bootstrap the classNameFilter value is not set at all and will - * therefore enable $animate to attempt to perform an animation on any element. - * When setting the classNameFilter value, animations will only be performed on elements + * therefore enable $animate to attempt to perform an animation on any element that is triggered. + * When setting the `classNameFilter` value, animations will only be performed on elements * that successfully match the filter expression. This in turn can boost performance * for low-powered devices as well as applications containing a lot of structural operations. + * + * **Note:** If present, `classNameFilter` will be checked after + * {@link $animateProvider#customFilter customFilter}. If `customFilter` is present and returns + * false, `classNameFilter` will not be checked. + * * @param {RegExp=} expression The className expression which will be checked against all animations * @return {RegExp} The current CSS className expression value. If null then there is no expression value */ this.classNameFilter = function(expression) { - if(arguments.length === 1) { - this.$$classNameFilter = (expression instanceof RegExp) ? expression : null; + if (arguments.length === 1) { + classNameFilter = (expression instanceof RegExp) ? expression : null; + if (classNameFilter) { + var reservedRegex = new RegExp('[(\\s|\\/)]' + NG_ANIMATE_CLASSNAME + '[(\\s|\\/)]'); + if (reservedRegex.test(classNameFilter.toString())) { + classNameFilter = null; + throw $animateMinErr('nongcls', '$animateProvider.classNameFilter(regex) prohibits accepting a regex value which matches/contains the "{0}" CSS class.', NG_ANIMATE_CLASSNAME); + } + } } - return this.$$classNameFilter; + return classNameFilter; }; - this.$get = ['$timeout', '$$asyncCallback', function($timeout, $$asyncCallback) { - - function async(fn) { - fn && $$asyncCallback(fn); + this.$get = ['$$animateQueue', function($$animateQueue) { + function domInsert(element, parentElement, afterElement) { + // if for some reason the previous element was removed + // from the dom sometime before this code runs then let's + // just stick to using the parent element as the anchor + if (afterElement) { + var afterNode = extractElementNode(afterElement); + if (afterNode && !afterNode.parentNode && !afterNode.previousElementSibling) { + afterElement = null; + } + } + if (afterElement) { + afterElement.after(element); + } else { + parentElement.prepend(element); + } } /** - * * @ngdoc service * @name $animate - * @description The $animate service provides rudimentary DOM manipulation functions to - * insert, remove and move elements within the DOM, as well as adding and removing classes. - * This service is the core service used by the ngAnimate $animator service which provides - * high-level animation hooks for CSS and JavaScript. + * @description The $animate service exposes a series of DOM utility methods that provide support + * for animation hooks. The default behavior is the application of DOM operations, however, + * when an animation is detected (and animations are enabled), $animate will do the heavy lifting + * to ensure that animation runs with the triggered DOM operation. + * + * By default $animate doesn't trigger any animations. This is because the `ngAnimate` module isn't + * included and only when it is active then the animation hooks that `$animate` triggers will be + * functional. Once active then all structural `ng-` directives will trigger animations as they perform + * their DOM-related operations (enter, leave and move). Other directives such as `ngClass`, + * `ngShow`, `ngHide` and `ngMessages` also provide support for animations. * - * $animate is available in the AngularJS core, however, the ngAnimate module must be included - * to enable full out animation support. Otherwise, $animate will only perform simple DOM - * manipulation operations. + * It is recommended that the`$animate` service is always used when executing DOM-related procedures within directives. * - * To learn more about enabling animation support, click here to visit the {@link ngAnimate - * ngAnimate module page} as well as the {@link ngAnimate.$animate ngAnimate $animate service - * page}. + * To learn more about enabling animation support, click here to visit the + * {@link ngAnimate ngAnimate module page}. */ return { + // we don't call it directly since non-existant arguments may + // be interpreted as null within the sub enabled function /** * * @ngdoc method - * @name $animate#enter - * @function - * @description Inserts the element into the DOM either after the `after` element or within - * the `parent` element. Once complete, the done() callback will be fired (if provided). - * @param {DOMElement} element the element which will be inserted into the DOM - * @param {DOMElement} parent the parent element which will append the element as - * a child (if the after element is not present) - * @param {DOMElement} after the sibling element which will append the element - * after itself - * @param {Function=} done callback function that will be called after the element has been - * inserted into the DOM + * @name $animate#on + * @kind function + * @description Sets up an event listener to fire whenever the animation event (enter, leave, move, etc...) + * has fired on the given element or among any of its children. Once the listener is fired, the provided callback + * is fired with the following params: + * + * ```js + * $animate.on('enter', container, + * function callback(element, phase) { + * // cool we detected an enter animation within the container + * } + * ); + * ``` + * + * @param {string} event the animation event that will be captured (e.g. enter, leave, move, addClass, removeClass, etc...) + * @param {DOMElement} container the container element that will capture each of the animation events that are fired on itself + * as well as among its children + * @param {Function} callback the callback function that will be fired when the listener is triggered + * + * The arguments present in the callback function are: + * * `element` - The captured DOM element that the animation was fired on. + * * `phase` - The phase of the animation. The two possible phases are **start** (when the animation starts) and **close** (when it ends). */ - enter : function(element, parent, after, done) { - if (after) { - after.after(element); - } else { - if (!parent || !parent[0]) { - parent = after.parent(); - } - parent.append(element); + on: $$animateQueue.on, + + /** + * + * @ngdoc method + * @name $animate#off + * @kind function + * @description Deregisters an event listener based on the event which has been associated with the provided element. This method + * can be used in three different ways depending on the arguments: + * + * ```js + * // remove all the animation event listeners listening for `enter` + * $animate.off('enter'); + * + * // remove listeners for all animation events from the container element + * $animate.off(container); + * + * // remove all the animation event listeners listening for `enter` on the given element and its children + * $animate.off('enter', container); + * + * // remove the event listener function provided by `callback` that is set + * // to listen for `enter` on the given `container` as well as its children + * $animate.off('enter', container, callback); + * ``` + * + * @param {string|DOMElement} event|container the animation event (e.g. enter, leave, move, + * addClass, removeClass, etc...), or the container element. If it is the element, all other + * arguments are ignored. + * @param {DOMElement=} container the container element the event listener was placed on + * @param {Function=} callback the callback function that was registered as the listener + */ + off: $$animateQueue.off, + + /** + * @ngdoc method + * @name $animate#pin + * @kind function + * @description Associates the provided element with a host parent element to allow the element to be animated even if it exists + * outside of the DOM structure of the AngularJS application. By doing so, any animation triggered via `$animate` can be issued on the + * element despite being outside the realm of the application or within another application. Say for example if the application + * was bootstrapped on an element that is somewhere inside of the `` tag, but we wanted to allow for an element to be situated + * as a direct child of `document.body`, then this can be achieved by pinning the element via `$animate.pin(element)`. Keep in mind + * that calling `$animate.pin(element, parentElement)` will not actually insert into the DOM anywhere; it will just create the association. + * + * Note that this feature is only active when the `ngAnimate` module is used. + * + * @param {DOMElement} element the external element that will be pinned + * @param {DOMElement} parentElement the host parent element that will be associated with the external element + */ + pin: $$animateQueue.pin, + + /** + * + * @ngdoc method + * @name $animate#enabled + * @kind function + * @description Used to get and set whether animations are enabled or not on the entire application or on an element and its children. This + * function can be called in four ways: + * + * ```js + * // returns true or false + * $animate.enabled(); + * + * // changes the enabled state for all animations + * $animate.enabled(false); + * $animate.enabled(true); + * + * // returns true or false if animations are enabled for an element + * $animate.enabled(element); + * + * // changes the enabled state for an element and its children + * $animate.enabled(element, true); + * $animate.enabled(element, false); + * ``` + * + * @param {DOMElement=} element the element that will be considered for checking/setting the enabled state + * @param {boolean=} enabled whether or not the animations will be enabled for the element + * + * @return {boolean} whether or not animations are enabled + */ + enabled: $$animateQueue.enabled, + + /** + * @ngdoc method + * @name $animate#cancel + * @kind function + * @description Cancels the provided animation. + * + * @param {Promise} animationPromise The animation promise that is returned when an animation is started. + */ + cancel: function(runner) { + if (runner.end) { + runner.end(); } - async(done); }, /** * * @ngdoc method - * @name $animate#leave - * @function - * @description Removes the element from the DOM. Once complete, the done() callback will be - * fired (if provided). - * @param {DOMElement} element the element which will be removed from the DOM - * @param {Function=} done callback function that will be called after the element has been - * removed from the DOM + * @name $animate#enter + * @kind function + * @description Inserts the element into the DOM either after the `after` element (if provided) or + * as the first child within the `parent` element and then triggers an animation. + * A promise is returned that will be resolved during the next digest once the animation + * has completed. + * + * @param {DOMElement} element the element which will be inserted into the DOM + * @param {DOMElement} parent the parent element which will append the element as + * a child (so long as the after element is not present) + * @param {DOMElement=} after the sibling element after which the element will be appended + * @param {object=} options an optional collection of options/styles that will be applied to the element. + * The object can have the following properties: + * + * - **addClass** - `{string}` - space-separated CSS classes to add to element + * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to` + * - **removeClass** - `{string}` - space-separated CSS classes to remove from element + * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from` + * + * @return {Promise} the animation callback promise */ - leave : function(element, done) { - element.remove(); - async(done); + enter: function(element, parent, after, options) { + parent = parent && jqLite(parent); + after = after && jqLite(after); + parent = parent || after.parent(); + domInsert(element, parent, after); + return $$animateQueue.push(element, 'enter', prepareAnimateOptions(options)); }, /** * * @ngdoc method * @name $animate#move - * @function - * @description Moves the position of the provided element within the DOM to be placed - * either after the `after` element or inside of the `parent` element. Once complete, the - * done() callback will be fired (if provided). + * @kind function + * @description Inserts (moves) the element into its new position in the DOM either after + * the `after` element (if provided) or as the first child within the `parent` element + * and then triggers an animation. A promise is returned that will be resolved + * during the next digest once the animation has completed. + * + * @param {DOMElement} element the element which will be moved into the new DOM position + * @param {DOMElement} parent the parent element which will append the element as + * a child (so long as the after element is not present) + * @param {DOMElement=} after the sibling element after which the element will be appended + * @param {object=} options an optional collection of options/styles that will be applied to the element. + * The object can have the following properties: + * + * - **addClass** - `{string}` - space-separated CSS classes to add to element + * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to` + * - **removeClass** - `{string}` - space-separated CSS classes to remove from element + * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from` * - * @param {DOMElement} element the element which will be moved around within the - * DOM - * @param {DOMElement} parent the parent element where the element will be - * inserted into (if the after element is not present) - * @param {DOMElement} after the sibling element where the element will be - * positioned next to - * @param {Function=} done the callback function (if provided) that will be fired after the - * element has been moved to its new position + * @return {Promise} the animation callback promise */ - move : function(element, parent, after, done) { - // Do not remove element before insert. Removing will cause data associated with the - // element to be dropped. Insert will implicitly do the remove. - this.enter(element, parent, after, done); + move: function(element, parent, after, options) { + parent = parent && jqLite(parent); + after = after && jqLite(after); + parent = parent || after.parent(); + domInsert(element, parent, after); + return $$animateQueue.push(element, 'move', prepareAnimateOptions(options)); }, /** - * * @ngdoc method - * @name $animate#addClass - * @function - * @description Adds the provided className CSS class value to the provided element. Once - * complete, the done() callback will be fired (if provided). - * @param {DOMElement} element the element which will have the className value - * added to it - * @param {string} className the CSS class which will be added to the element - * @param {Function=} done the callback function (if provided) that will be fired after the - * className value has been added to the element + * @name $animate#leave + * @kind function + * @description Triggers an animation and then removes the element from the DOM. + * When the function is called a promise is returned that will be resolved during the next + * digest once the animation has completed. + * + * @param {DOMElement} element the element which will be removed from the DOM + * @param {object=} options an optional collection of options/styles that will be applied to the element. + * The object can have the following properties: + * + * - **addClass** - `{string}` - space-separated CSS classes to add to element + * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to` + * - **removeClass** - `{string}` - space-separated CSS classes to remove from element + * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from` + * + * @return {Promise} the animation callback promise */ - addClass : function(element, className, done) { - className = isString(className) ? - className : - isArray(className) ? className.join(' ') : ''; - forEach(element, function (element) { - jqLiteAddClass(element, className); + leave: function(element, options) { + return $$animateQueue.push(element, 'leave', prepareAnimateOptions(options), function() { + element.remove(); }); - async(done); }, /** + * @ngdoc method + * @name $animate#addClass + * @kind function + * + * @description Triggers an addClass animation surrounding the addition of the provided CSS class(es). Upon + * execution, the addClass operation will only be handled after the next digest and it will not trigger an + * animation if element already contains the CSS class or if the class is removed at a later step. + * Note that class-based animations are treated differently compared to structural animations + * (like enter, move and leave) since the CSS classes may be added/removed at different points + * depending if CSS or JavaScript animations are used. + * + * @param {DOMElement} element the element which the CSS classes will be applied to + * @param {string} className the CSS class(es) that will be added (multiple classes are separated via spaces) + * @param {object=} options an optional collection of options/styles that will be applied to the element. + * The object can have the following properties: + * + * - **addClass** - `{string}` - space-separated CSS classes to add to element + * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to` + * - **removeClass** - `{string}` - space-separated CSS classes to remove from element + * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from` * + * @return {Promise} the animation callback promise + */ + addClass: function(element, className, options) { + options = prepareAnimateOptions(options); + options.addClass = mergeClasses(options.addclass, className); + return $$animateQueue.push(element, 'addClass', options); + }, + + /** * @ngdoc method * @name $animate#removeClass - * @function - * @description Removes the provided className CSS class value from the provided element. - * Once complete, the done() callback will be fired (if provided). - * @param {DOMElement} element the element which will have the className value - * removed from it - * @param {string} className the CSS class which will be removed from the element - * @param {Function=} done the callback function (if provided) that will be fired after the - * className value has been removed from the element + * @kind function + * + * @description Triggers a removeClass animation surrounding the removal of the provided CSS class(es). Upon + * execution, the removeClass operation will only be handled after the next digest and it will not trigger an + * animation if element does not contain the CSS class or if the class is added at a later step. + * Note that class-based animations are treated differently compared to structural animations + * (like enter, move and leave) since the CSS classes may be added/removed at different points + * depending if CSS or JavaScript animations are used. + * + * @param {DOMElement} element the element which the CSS classes will be applied to + * @param {string} className the CSS class(es) that will be removed (multiple classes are separated via spaces) + * @param {object=} options an optional collection of options/styles that will be applied to the element. + * The object can have the following properties: + * + * - **addClass** - `{string}` - space-separated CSS classes to add to element + * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to` + * - **removeClass** - `{string}` - space-separated CSS classes to remove from element + * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from` + * + * @return {Promise} the animation callback promise */ - removeClass : function(element, className, done) { - className = isString(className) ? - className : - isArray(className) ? className.join(' ') : ''; - forEach(element, function (element) { - jqLiteRemoveClass(element, className); - }); - async(done); + removeClass: function(element, className, options) { + options = prepareAnimateOptions(options); + options.removeClass = mergeClasses(options.removeClass, className); + return $$animateQueue.push(element, 'removeClass', options); }, /** - * * @ngdoc method * @name $animate#setClass - * @function - * @description Adds and/or removes the given CSS classes to and from the element. - * Once complete, the done() callback will be fired (if provided). - * @param {DOMElement} element the element which will it's CSS classes changed - * removed from it - * @param {string} add the CSS classes which will be added to the element - * @param {string} remove the CSS class which will be removed from the element - * @param {Function=} done the callback function (if provided) that will be fired after the - * CSS classes have been set on the element + * @kind function + * + * @description Performs both the addition and removal of a CSS classes on an element and (during the process) + * triggers an animation surrounding the class addition/removal. Much like `$animate.addClass` and + * `$animate.removeClass`, `setClass` will only evaluate the classes being added/removed once a digest has + * passed. Note that class-based animations are treated differently compared to structural animations + * (like enter, move and leave) since the CSS classes may be added/removed at different points + * depending if CSS or JavaScript animations are used. + * + * @param {DOMElement} element the element which the CSS classes will be applied to + * @param {string} add the CSS class(es) that will be added (multiple classes are separated via spaces) + * @param {string} remove the CSS class(es) that will be removed (multiple classes are separated via spaces) + * @param {object=} options an optional collection of options/styles that will be applied to the element. + * The object can have the following properties: + * + * - **addClass** - `{string}` - space-separated CSS classes to add to element + * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to` + * - **removeClass** - `{string}` - space-separated CSS classes to remove from element + * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from` + * + * @return {Promise} the animation callback promise */ - setClass : function(element, add, remove, done) { - forEach(element, function (element) { - jqLiteAddClass(element, add); - jqLiteRemoveClass(element, remove); - }); - async(done); + setClass: function(element, add, remove, options) { + options = prepareAnimateOptions(options); + options.addClass = mergeClasses(options.addClass, add); + options.removeClass = mergeClasses(options.removeClass, remove); + return $$animateQueue.push(element, 'setClass', options); }, - enabled : noop + /** + * @ngdoc method + * @name $animate#animate + * @kind function + * + * @description Performs an inline animation on the element which applies the provided to and from CSS styles to the element. + * If any detected CSS transition, keyframe or JavaScript matches the provided className value, then the animation will take + * on the provided styles. For example, if a transition animation is set for the given className, then the provided `from` and + * `to` styles will be applied alongside the given transition. If the CSS style provided in `from` does not have a corresponding + * style in `to`, the style in `from` is applied immediately, and no animation is run. + * If a JavaScript animation is detected then the provided styles will be given in as function parameters into the `animate` + * method (or as part of the `options` parameter): + * + * ```js + * ngModule.animation('.my-inline-animation', function() { + * return { + * animate : function(element, from, to, done, options) { + * //animation + * done(); + * } + * } + * }); + * ``` + * + * @param {DOMElement} element the element which the CSS styles will be applied to + * @param {object} from the from (starting) CSS styles that will be applied to the element and across the animation. + * @param {object} to the to (destination) CSS styles that will be applied to the element and across the animation. + * @param {string=} className an optional CSS class that will be applied to the element for the duration of the animation. If + * this value is left as empty then a CSS class of `ng-inline-animate` will be applied to the element. + * (Note that if no animation is detected then this value will not be applied to the element.) + * @param {object=} options an optional collection of options/styles that will be applied to the element. + * The object can have the following properties: + * + * - **addClass** - `{string}` - space-separated CSS classes to add to element + * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to` + * - **removeClass** - `{string}` - space-separated CSS classes to remove from element + * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from` + * + * @return {Promise} the animation callback promise + */ + animate: function(element, from, to, className, options) { + options = prepareAnimateOptions(options); + options.from = options.from ? extend(options.from, from) : from; + options.to = options.to ? extend(options.to, to) : to; + + className = className || 'ng-inline-animate'; + options.tempClasses = mergeClasses(options.tempClasses, className); + return $$animateQueue.push(element, 'animate', options); + } }; }]; }]; - function $$AsyncCallbackProvider(){ - this.$get = ['$$rAF', '$timeout', function($$rAF, $timeout) { - return $$rAF.supported - ? function(fn) { return $$rAF(fn); } - : function(fn) { - return $timeout(fn, 0, false); + var $$AnimateAsyncRunFactoryProvider = /** @this */ function() { + this.$get = ['$$rAF', function($$rAF) { + var waitQueue = []; + + function waitForTick(fn) { + waitQueue.push(fn); + if (waitQueue.length > 1) return; + $$rAF(function() { + for (var i = 0; i < waitQueue.length; i++) { + waitQueue[i](); + } + waitQueue = []; + }); + } + + return function() { + var passed = false; + waitForTick(function() { + passed = true; + }); + return function(callback) { + if (passed) { + callback(); + } else { + waitForTick(callback); + } + }; }; }]; - } + }; - /** - * ! This is a private undocumented service ! - * - * @name $browser - * @requires $log - * @description - * This object has two goals: - * - * - hide all the global state in the browser caused by the window object - * - abstract away all the browser specific features and inconsistencies - * - * For tests we provide {@link ngMock.$browser mock implementation} of the `$browser` - * service, which can be used for convenient testing of the application without the interaction with - * the real browser apis. - */ - /** - * @param {object} window The global window object. - * @param {object} document jQuery wrapped document. - * @param {function()} XHR XMLHttpRequest constructor. - * @param {object} $log console.log or an object with the same interface. - * @param {object} $sniffer $sniffer service + var $$AnimateRunnerFactoryProvider = /** @this */ function() { + this.$get = ['$q', '$sniffer', '$$animateAsyncRun', '$$isDocumentHidden', '$timeout', + function($q, $sniffer, $$animateAsyncRun, $$isDocumentHidden, $timeout) { + + var INITIAL_STATE = 0; + var DONE_PENDING_STATE = 1; + var DONE_COMPLETE_STATE = 2; + + AnimateRunner.chain = function(chain, callback) { + var index = 0; + + next(); + function next() { + if (index === chain.length) { + callback(true); + return; + } + + chain[index](function(response) { + if (response === false) { + callback(false); + return; + } + index++; + next(); + }); + } + }; + + AnimateRunner.all = function(runners, callback) { + var count = 0; + var status = true; + forEach(runners, function(runner) { + runner.done(onProgress); + }); + + function onProgress(response) { + status = status && response; + if (++count === runners.length) { + callback(status); + } + } + }; + + function AnimateRunner(host) { + this.setHost(host); + + var rafTick = $$animateAsyncRun(); + var timeoutTick = function(fn) { + $timeout(fn, 0, false); + }; + + this._doneCallbacks = []; + this._tick = function(fn) { + if ($$isDocumentHidden()) { + timeoutTick(fn); + } else { + rafTick(fn); + } + }; + this._state = 0; + } + + AnimateRunner.prototype = { + setHost: function(host) { + this.host = host || {}; + }, + + done: function(fn) { + if (this._state === DONE_COMPLETE_STATE) { + fn(); + } else { + this._doneCallbacks.push(fn); + } + }, + + progress: noop, + + getPromise: function() { + if (!this.promise) { + var self = this; + this.promise = $q(function(resolve, reject) { + self.done(function(status) { + if (status === false) { + reject(); + } else { + resolve(); + } + }); + }); + } + return this.promise; + }, + + then: function(resolveHandler, rejectHandler) { + return this.getPromise().then(resolveHandler, rejectHandler); + }, + + 'catch': function(handler) { + return this.getPromise()['catch'](handler); + }, + + 'finally': function(handler) { + return this.getPromise()['finally'](handler); + }, + + pause: function() { + if (this.host.pause) { + this.host.pause(); + } + }, + + resume: function() { + if (this.host.resume) { + this.host.resume(); + } + }, + + end: function() { + if (this.host.end) { + this.host.end(); + } + this._resolve(true); + }, + + cancel: function() { + if (this.host.cancel) { + this.host.cancel(); + } + this._resolve(false); + }, + + complete: function(response) { + var self = this; + if (self._state === INITIAL_STATE) { + self._state = DONE_PENDING_STATE; + self._tick(function() { + self._resolve(response); + }); + } + }, + + _resolve: function(response) { + if (this._state !== DONE_COMPLETE_STATE) { + forEach(this._doneCallbacks, function(fn) { + fn(response); + }); + this._doneCallbacks.length = 0; + this._state = DONE_COMPLETE_STATE; + } + } + }; + + return AnimateRunner; + }]; + }; + + /* exported $CoreAnimateCssProvider */ + + /** + * @ngdoc service + * @name $animateCss + * @kind object + * @this + * + * @description + * This is the core version of `$animateCss`. By default, only when the `ngAnimate` is included, + * then the `$animateCss` service will actually perform animations. + * + * Click here {@link ngAnimate.$animateCss to read the documentation for $animateCss}. + */ + var $CoreAnimateCssProvider = function() { + this.$get = ['$$rAF', '$q', '$$AnimateRunner', function($$rAF, $q, $$AnimateRunner) { + + return function(element, initialOptions) { + // all of the animation functions should create + // a copy of the options data, however, if a + // parent service has already created a copy then + // we should stick to using that + var options = initialOptions || {}; + if (!options.$$prepared) { + options = copy(options); + } + + // there is no point in applying the styles since + // there is no animation that goes on at all in + // this version of $animateCss. + if (options.cleanupStyles) { + options.from = options.to = null; + } + + if (options.from) { + element.css(options.from); + options.from = null; + } + + var closed, runner = new $$AnimateRunner(); + return { + start: run, + end: run + }; + + function run() { + $$rAF(function() { + applyAnimationContents(); + if (!closed) { + runner.complete(); + } + closed = true; + }); + return runner; + } + + function applyAnimationContents() { + if (options.addClass) { + element.addClass(options.addClass); + options.addClass = null; + } + if (options.removeClass) { + element.removeClass(options.removeClass); + options.removeClass = null; + } + if (options.to) { + element.css(options.to); + options.to = null; + } + } + }; + }]; + }; + + /* global stripHash: true */ + + /** + * ! This is a private undocumented service ! + * + * @name $browser + * @requires $log + * @description + * This object has two goals: + * + * - hide all the global state in the browser caused by the window object + * - abstract away all the browser specific features and inconsistencies + * + * For tests we provide {@link ngMock.$browser mock implementation} of the `$browser` + * service, which can be used for convenient testing of the application without the interaction with + * the real browser apis. + */ + /** + * @param {object} window The global window object. + * @param {object} document jQuery wrapped document. + * @param {object} $log window.console or an object with the same interface. + * @param {object} $sniffer $sniffer service */ function Browser(window, document, $log, $sniffer) { var self = this, - rawDocument = document[0], location = window.location, history = window.history, setTimeout = window.setTimeout, @@ -4301,7 +6429,7 @@ } finally { outstandingRequestCount--; if (outstandingRequestCount === 0) { - while(outstandingRequestCallbacks.length) { + while (outstandingRequestCallbacks.length) { try { outstandingRequestCallbacks.pop()(); } catch (e) { @@ -4312,18 +6440,17 @@ } } + function getHash(url) { + var index = url.indexOf('#'); + return index === -1 ? '' : url.substr(index); + } + /** * @private - * Note: this method is used only by scenario runner * TODO(vojta): prefix this method with $$ ? * @param {function()} callback Function that will be called when no outstanding request */ self.notifyWhenNoOutstandingRequests = function(callback) { - // force browser to execute all pollFns - this is needed so that cookies and other pollers fire - // at some deterministic time in respect to the test runner's actions. Leaving things up to the - // regular poller would result in flaky tests. - forEach(pollFns, function(pollFn){ pollFn(); }); - if (outstandingRequestCount === 0) { callback(); } else { @@ -4331,51 +6458,23 @@ } }; - ////////////////////////////////////////////////////////////// - // Poll Watcher API - ////////////////////////////////////////////////////////////// - var pollFns = [], - pollTimeout; - - /** - * @name $browser#addPollFn - * - * @param {function()} fn Poll function to add - * - * @description - * Adds a function to the list of functions that poller periodically executes, - * and starts polling if not started yet. - * - * @returns {function()} the added function - */ - self.addPollFn = function(fn) { - if (isUndefined(pollTimeout)) startPoller(100, setTimeout); - pollFns.push(fn); - return fn; - }; - - /** - * @param {number} interval How often should browser call poll functions (ms) - * @param {function()} setTimeout Reference to a real or fake `setTimeout` function. - * - * @description - * Configures the poller to run in the specified intervals, using the specified - * setTimeout fn and kicks it off. - */ - function startPoller(interval, setTimeout) { - (function check() { - forEach(pollFns, function(pollFn){ pollFn(); }); - pollTimeout = setTimeout(check, interval); - })(); - } - ////////////////////////////////////////////////////////////// // URL API ////////////////////////////////////////////////////////////// - var lastBrowserUrl = location.href, + var cachedState, lastHistoryState, + lastBrowserUrl = location.href, baseElement = document.find('base'), - newLocation = null; + pendingLocation = null, + getCurrentState = !$sniffer.history ? noop : function getCurrentState() { + try { + return history.state; + } catch (e) { + // MSIE can reportedly throw when there is no state (UNCONFIRMED). + } + }; + + cacheState(); /** * @name $browser#url @@ -4394,52 +6493,120 @@ * {@link ng.$location $location service} to change url. * * @param {string} url New url (when used as setter) - * @param {boolean=} replace Should new url replace current history record ? + * @param {boolean=} replace Should new url replace current history record? + * @param {object=} state object to use with pushState/replaceState */ - self.url = function(url, replace) { + self.url = function(url, replace, state) { + // In modern browsers `history.state` is `null` by default; treating it separately + // from `undefined` would cause `$browser.url('/foo')` to change `history.state` + // to undefined via `pushState`. Instead, let's change `undefined` to `null` here. + if (isUndefined(state)) { + state = null; + } + // Android Browser BFCache causes location, history reference to become stale. if (location !== window.location) location = window.location; if (history !== window.history) history = window.history; // setter if (url) { - if (lastBrowserUrl == url) return; + var sameState = lastHistoryState === state; + + // Don't change anything if previous and current URLs and states match. This also prevents + // IE<10 from getting into redirect loop when in LocationHashbangInHtml5Url mode. + // See https://github.com/angular/angular.js/commit/ffb2701 + if (lastBrowserUrl === url && (!$sniffer.history || sameState)) { + return self; + } + var sameBase = lastBrowserUrl && stripHash(lastBrowserUrl) === stripHash(url); lastBrowserUrl = url; - if ($sniffer.history) { - if (replace) history.replaceState(null, '', url); - else { - history.pushState(null, '', url); - // Crazy Opera Bug: http://my.opera.com/community/forums/topic.dml?id=1185462 - baseElement.attr('href', baseElement.attr('href')); - } + lastHistoryState = state; + // Don't use history API if only the hash changed + // due to a bug in IE10/IE11 which leads + // to not firing a `hashchange` nor `popstate` event + // in some cases (see #9143). + if ($sniffer.history && (!sameBase || !sameState)) { + history[replace ? 'replaceState' : 'pushState'](state, '', url); + cacheState(); } else { - newLocation = url; + if (!sameBase) { + pendingLocation = url; + } if (replace) { location.replace(url); - } else { + } else if (!sameBase) { location.href = url; + } else { + location.hash = getHash(url); } + if (location.href !== url) { + pendingLocation = url; + } + } + if (pendingLocation) { + pendingLocation = url; } return self; // getter } else { - // - newLocation is a workaround for an IE7-9 issue with location.replace and location.href - // methods not updating location.href synchronously. + // - pendingLocation is needed as browsers don't allow to read out + // the new location.href if a reload happened or if there is a bug like in iOS 9 (see + // https://openradar.appspot.com/22186109). // - the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172 - return newLocation || location.href.replace(/%27/g,"'"); + return pendingLocation || location.href.replace(/%27/g,'\''); } }; + /** + * @name $browser#state + * + * @description + * This method is a getter. + * + * Return history.state or null if history.state is undefined. + * + * @returns {object} state + */ + self.state = function() { + return cachedState; + }; + var urlChangeListeners = [], urlChangeInit = false; - function fireUrlChange() { - newLocation = null; - if (lastBrowserUrl == self.url()) return; + function cacheStateAndFireUrlChange() { + pendingLocation = null; + fireStateOrUrlChange(); + } + + // This variable should be used *only* inside the cacheState function. + var lastCachedState = null; + function cacheState() { + // This should be the only place in $browser where `history.state` is read. + cachedState = getCurrentState(); + cachedState = isUndefined(cachedState) ? null : cachedState; + + // Prevent callbacks fo fire twice if both hashchange & popstate were fired. + if (equals(cachedState, lastCachedState)) { + cachedState = lastCachedState; + } + + lastCachedState = cachedState; + lastHistoryState = cachedState; + } + + function fireStateOrUrlChange() { + var prevLastHistoryState = lastHistoryState; + cacheState(); + + if (lastBrowserUrl === self.url() && prevLastHistoryState === cachedState) { + return; + } lastBrowserUrl = self.url(); + lastHistoryState = cachedState; forEach(urlChangeListeners, function(listener) { - listener(self.url()); + listener(self.url(), cachedState); }); } @@ -4449,7 +6616,7 @@ * @description * Register callback function that will be called, when url changes. * - * It's only called when the url is changed from outside of angular: + * It's only called when the url is changed from outside of AngularJS: * - user types different url into address bar * - user clicks on history (forward/back) button * - user clicks on a link @@ -4459,7 +6626,7 @@ * The listener gets called with new url as parameter. * * NOTE: this api is intended for use only by the $location service. Please use the - * {@link ng.$location $location service} to monitor url changes in angular apps. + * {@link ng.$location $location service} to monitor url changes in AngularJS apps. * * @param {function(string)} listener Listener function to be called when url changes. * @return {function(string)} Returns the registered listener fn - handy if the fn is anonymous. @@ -4467,16 +6634,14 @@ self.onUrlChange = function(callback) { // TODO(vojta): refactor to use node's syntax for events if (!urlChangeInit) { - // We listen on both (hashchange/popstate) when available, as some browsers (e.g. Opera) - // don't fire popstate when user change the address bar and don't fire hashchange when url + // We listen on both (hashchange/popstate) when available, as some browsers don't + // fire popstate when user changes the address bar and don't fire hashchange when url // changed by push/replaceState // html5 history api - popstate event - if ($sniffer.history) jqLite(window).on('popstate', fireUrlChange); + if ($sniffer.history) jqLite(window).on('popstate', cacheStateAndFireUrlChange); // hashchange event - if ($sniffer.hashchange) jqLite(window).on('hashchange', fireUrlChange); - // polling - else self.addPollFn(fireUrlChange); + jqLite(window).on('hashchange', cacheStateAndFireUrlChange); urlChangeInit = true; } @@ -4485,6 +6650,23 @@ return callback; }; + /** + * @private + * Remove popstate and hashchange handler from window. + * + * NOTE: this api is intended for use only by $rootScope. + */ + self.$$applicationDestroyed = function() { + jqLite(window).off('hashchange popstate', cacheStateAndFireUrlChange); + }; + + /** + * Checks whether the url has changed outside of AngularJS. + * Needs to be exported to be able to check for changes that have been done in sync, + * as hashchange/popstate events fire in async. + */ + self.$$checkUrlChange = fireStateOrUrlChange; + ////////////////////////////////////////////////////////////// // Misc API ////////////////////////////////////////////////////////////// @@ -4500,85 +6682,9 @@ */ self.baseHref = function() { var href = baseElement.attr('href'); - return href ? href.replace(/^(https?\:)?\/\/[^\/]*/, '') : ''; - }; - - ////////////////////////////////////////////////////////////// - // Cookies API - ////////////////////////////////////////////////////////////// - var lastCookies = {}; - var lastCookieString = ''; - var cookiePath = self.baseHref(); - - /** - * @name $browser#cookies - * - * @param {string=} name Cookie name - * @param {string=} value Cookie value - * - * @description - * The cookies method provides a 'private' low level access to browser cookies. - * It is not meant to be used directly, use the $cookie service instead. - * - * The return values vary depending on the arguments that the method was called with as follows: - * - * - cookies() -> hash of all cookies, this is NOT a copy of the internal state, so do not modify - * it - * - cookies(name, value) -> set name to value, if value is undefined delete the cookie - * - cookies(name) -> the same as (name, undefined) == DELETES (no one calls it right now that - * way) - * - * @returns {Object} Hash of all cookies (if called without any parameter) - */ - self.cookies = function(name, value) { - /* global escape: false, unescape: false */ - var cookieLength, cookieArray, cookie, i, index; - - if (name) { - if (value === undefined) { - rawDocument.cookie = escape(name) + "=;path=" + cookiePath + - ";expires=Thu, 01 Jan 1970 00:00:00 GMT"; - } else { - if (isString(value)) { - cookieLength = (rawDocument.cookie = escape(name) + '=' + escape(value) + - ';path=' + cookiePath).length + 1; - - // per http://www.ietf.org/rfc/rfc2109.txt browser must allow at minimum: - // - 300 cookies - // - 20 cookies per unique domain - // - 4096 bytes per cookie - if (cookieLength > 4096) { - $log.warn("Cookie '"+ name + - "' possibly not set or overflowed because it was too large ("+ - cookieLength + " > 4096 bytes)!"); - } - } - } - } else { - if (rawDocument.cookie !== lastCookieString) { - lastCookieString = rawDocument.cookie; - cookieArray = lastCookieString.split("; "); - lastCookies = {}; - - for (i = 0; i < cookieArray.length; i++) { - cookie = cookieArray[i]; - index = cookie.indexOf('='); - if (index > 0) { //ignore nameless cookies - name = unescape(cookie.substring(0, index)); - // the first value that is seen for a cookie is the most - // specific one. values for the same cookie name that - // follow are for less specific paths. - if (lastCookies[name] === undefined) { - lastCookies[name] = unescape(cookie.substring(index + 1)); - } - } - } - } - return lastCookies; - } + return href ? href.replace(/^(https?:)?\/\/[^/]*/, '') : ''; }; - /** * @name $browser#defer * @param {function()} fn A function, who's execution should be deferred. @@ -4627,9 +6733,10 @@ } - function $BrowserProvider(){ + /** @this */ + function $BrowserProvider() { this.$get = ['$window', '$log', '$sniffer', '$document', - function( $window, $log, $sniffer, $document){ + function($window, $log, $sniffer, $document) { return new Browser($window, $document, $log, $sniffer); }]; } @@ -4637,6 +6744,7 @@ /** * @ngdoc service * @name $cacheFactory + * @this * * @description * Factory that constructs {@link $cacheFactory.Cache Cache} objects and gives access to @@ -4673,7 +6781,7 @@ * - `{void}` `destroy()` — Removes references to this cache from $cacheFactory. * * @example - +
    @@ -4701,8 +6809,10 @@ $scope.keys = []; $scope.cache = $cacheFactory('cacheId'); $scope.put = function(key, value) { - $scope.cache.put(key, value); - $scope.keys.push(key); + if (angular.isUndefined($scope.cache.get(key))) { + $scope.keys.push(key); + } + $scope.cache.put(key, angular.isUndefined(value) ? null : value); }; }]); @@ -4720,14 +6830,14 @@ function cacheFactory(cacheId, options) { if (cacheId in caches) { - throw minErr('$cacheFactory')('iid', "CacheId '{0}' is already taken!", cacheId); + throw minErr('$cacheFactory')('iid', 'CacheId \'{0}\' is already taken!', cacheId); } var size = 0, stats = extend({}, options, {id: cacheId}), - data = {}, + data = createMap(), capacity = (options && options.capacity) || Number.MAX_VALUE, - lruHash = {}, + lruHash = createMap(), freshEnd = null, staleEnd = null; @@ -4737,45 +6847,45 @@ * * @description * A cache object used to store and retrieve data, primarily used by - * {@link $http $http} and the {@link ng.directive:script script} directive to cache - * templates and other data. + * {@link $templateRequest $templateRequest} and the {@link ng.directive:script script} + * directive to cache templates and other data. * * ```js * angular.module('superCache') * .factory('superCache', ['$cacheFactory', function($cacheFactory) { - * return $cacheFactory('super-cache'); - * }]); + * return $cacheFactory('super-cache'); + * }]); * ``` * * Example test: * * ```js * it('should behave like a cache', inject(function(superCache) { - * superCache.put('key', 'value'); - * superCache.put('another key', 'another value'); - * - * expect(superCache.info()).toEqual({ - * id: 'super-cache', - * size: 2 - * }); - * - * superCache.remove('another key'); - * expect(superCache.get('another key')).toBeUndefined(); - * - * superCache.removeAll(); - * expect(superCache.info()).toEqual({ - * id: 'super-cache', - * size: 0 - * }); - * })); + * superCache.put('key', 'value'); + * superCache.put('another key', 'another value'); + * + * expect(superCache.info()).toEqual({ + * id: 'super-cache', + * size: 2 + * }); + * + * superCache.remove('another key'); + * expect(superCache.get('another key')).toBeUndefined(); + * + * superCache.removeAll(); + * expect(superCache.info()).toEqual({ + * id: 'super-cache', + * size: 0 + * }); + * })); * ``` */ - return caches[cacheId] = { + return (caches[cacheId] = { /** * @ngdoc method * @name $cacheFactory.Cache#put - * @function + * @kind function * * @description * Inserts a named entry into the {@link $cacheFactory.Cache Cache} object to be @@ -4791,13 +6901,13 @@ * @returns {*} the value stored. */ put: function(key, value) { + if (isUndefined(value)) return; if (capacity < Number.MAX_VALUE) { var lruEntry = lruHash[key] || (lruHash[key] = {key: key}); refresh(lruEntry); } - if (isUndefined(value)) return; if (!(key in data)) size++; data[key] = value; @@ -4811,7 +6921,7 @@ /** * @ngdoc method * @name $cacheFactory.Cache#get - * @function + * @kind function * * @description * Retrieves named data stored in the {@link $cacheFactory.Cache Cache} object. @@ -4835,7 +6945,7 @@ /** * @ngdoc method * @name $cacheFactory.Cache#remove - * @function + * @kind function * * @description * Removes an entry from the {@link $cacheFactory.Cache Cache} object. @@ -4848,13 +6958,15 @@ if (!lruEntry) return; - if (lruEntry == freshEnd) freshEnd = lruEntry.p; - if (lruEntry == staleEnd) staleEnd = lruEntry.n; + if (lruEntry === freshEnd) freshEnd = lruEntry.p; + if (lruEntry === staleEnd) staleEnd = lruEntry.n; link(lruEntry.n,lruEntry.p); delete lruHash[key]; } + if (!(key in data)) return; + delete data[key]; size--; }, @@ -4863,15 +6975,15 @@ /** * @ngdoc method * @name $cacheFactory.Cache#removeAll - * @function + * @kind function * * @description * Clears the cache object of any entries. */ removeAll: function() { - data = {}; + data = createMap(); size = 0; - lruHash = {}; + lruHash = createMap(); freshEnd = staleEnd = null; }, @@ -4879,7 +6991,7 @@ /** * @ngdoc method * @name $cacheFactory.Cache#destroy - * @function + * @kind function * * @description * Destroys the {@link $cacheFactory.Cache Cache} object entirely, @@ -4896,7 +7008,7 @@ /** * @ngdoc method * @name $cacheFactory.Cache#info - * @function + * @kind function * * @description * Retrieve information regarding a particular {@link $cacheFactory.Cache Cache}. @@ -4912,17 +7024,17 @@ info: function() { return extend({}, stats, {size: size}); } - }; + }); /** * makes the `entry` the freshEnd of the LRU linked list */ function refresh(entry) { - if (entry != freshEnd) { + if (entry !== freshEnd) { if (!staleEnd) { staleEnd = entry; - } else if (staleEnd == entry) { + } else if (staleEnd === entry) { staleEnd = entry.n; } @@ -4938,7 +7050,7 @@ * bidirectionally links two entries of the LRU linked list */ function link(nextEntry, prevEntry) { - if (nextEntry != prevEntry) { + if (nextEntry !== prevEntry) { if (nextEntry) nextEntry.p = prevEntry; //p stands for previous, 'prev' didn't minify if (prevEntry) prevEntry.n = nextEntry; //n stands for next, 'next' didn't minify } @@ -4951,7 +7063,7 @@ * @name $cacheFactory#info * * @description - * Get information about all the of the caches that have been created + * Get information about all the caches that have been created * * @returns {Object} - key-value map of `cacheId` to the result of calling `cache#info` */ @@ -4986,11 +7098,15 @@ /** * @ngdoc service * @name $templateCache + * @this * * @description + * `$templateCache` is a {@link $cacheFactory.Cache Cache object} created by the + * {@link ng.$cacheFactory $cacheFactory}. + * * The first time a template is used, it is loaded in the template cache for quick retrieval. You - * can load templates directly into the cache in a `script` tag, or by consuming the - * `$templateCache` service directly. + * can load templates directly into the cache in a `script` tag, by using {@link $templateRequest}, + * or by consuming the `$templateCache` service directly. * * Adding via the `script` tag: * @@ -5001,29 +7117,30 @@ * ``` * * **Note:** the `script` tag containing the template does not need to be included in the `head` of - * the document, but it must be below the `ng-app` definition. + * the document, but it must be a descendent of the {@link ng.$rootElement $rootElement} (e.g. + * element with {@link ngApp} attribute), otherwise the template will be ignored. * - * Adding via the $templateCache service: + * Adding via the `$templateCache` service: * * ```js * var myApp = angular.module('myApp', []); * myApp.run(function($templateCache) { - * $templateCache.put('templateId.html', 'This is the content of the template'); - * }); + * $templateCache.put('templateId.html', 'This is the content of the template'); + * }); * ``` * - * To retrieve the template later, simply use it in your HTML: - * ```html - *
    + * To retrieve the template later, simply use it in your component: + * ```js + * myApp.component('myComponent', { + * templateUrl: 'templateId.html' + * }); * ``` * - * or get it via Javascript: + * or get it via the `$templateCache` service: * ```js * $templateCache.get('templateId.html') * ``` * - * See {@link ng.$cacheFactory $cacheFactory}. - * */ function $TemplateCacheProvider() { this.$get = ['$cacheFactory', function($cacheFactory) { @@ -5031,28 +7148,39 @@ }]; } + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Any commits to this file should be reviewed with security in mind. * + * Changes to this file can potentially create security vulnerabilities. * + * An approval from 2 Core members with history of modifying * + * this file is required. * + * * + * Does the change somehow allow for arbitrary javascript to be executed? * + * Or allows for someone to change the prototype of built-in objects? * + * Or gives undesired access to variables like document or window? * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + /* ! VARIABLE/FUNCTION NAMING CONVENTIONS THAT APPLY TO THIS FILE! - * - * DOM-related variables: - * - * - "node" - DOM Node - * - "element" - DOM Element or Node - * - "$node" or "$element" - jqLite-wrapped node or element - * - * - * Compiler related stuff: - * - * - "linkFn" - linking fn of a single directive - * - "nodeLinkFn" - function that aggregates all linking fns for a particular node - * - "childLinkFn" - function that aggregates all linking fns for child nodes of a particular node - * - "compositeLinkFn" - function that aggregates all linking fns for a compilation root (nodeList) - */ + * + * DOM-related variables: + * + * - "node" - DOM Node + * - "element" - DOM Element or Node + * - "$node" or "$element" - jqLite-wrapped node or element + * + * + * Compiler related stuff: + * + * - "linkFn" - linking fn of a single directive + * - "nodeLinkFn" - function that aggregates all linking fns for a particular node + * - "childLinkFn" - function that aggregates all linking fns for child nodes of a particular node + * - "compositeLinkFn" - function that aggregates all linking fns for a compilation root (nodeList) + */ /** * @ngdoc service * @name $compile - * @function + * @kind function * * @description * Compiles an HTML string or DOM into a template and produces a template function, which @@ -5072,8 +7200,9 @@ * There are many different options for a directive. * * The difference resides in the return value of the factory function. - * You can either return a "Directive Definition Object" (see below) that defines the directive properties, - * or just the `postLink` function (all other properties will have the default values). + * You can either return a {@link $compile#directive-definition-object Directive Definition Object (see below)} + * that defines the directive properties, or just the `postLink` function (all other properties will have + * the default values). * *
    * **Best Practice:** It's recommended to use the "directive definition object" form. @@ -5085,36 +7214,38 @@ * var myModule = angular.module(...); * * myModule.directive('directiveName', function factory(injectables) { - * var directiveDefinitionObject = { - * priority: 0, - * template: '
    ', // or // function(tElement, tAttrs) { ... }, - * // or - * // templateUrl: 'directive.html', // or // function(tElement, tAttrs) { ... }, - * replace: false, - * transclude: false, - * restrict: 'A', - * scope: false, - * controller: function($scope, $element, $attrs, $transclude, otherInjectables) { ... }, - * controllerAs: 'stringAlias', - * require: 'siblingDirectiveName', // or // ['^parentDirectiveName', '?optionalDirectiveName', '?^optionalParent'], - * compile: function compile(tElement, tAttrs, transclude) { - * return { - * pre: function preLink(scope, iElement, iAttrs, controller) { ... }, - * post: function postLink(scope, iElement, iAttrs, controller) { ... } - * } - * // or - * // return function postLink( ... ) { ... } - * }, - * // or - * // link: { - * // pre: function preLink(scope, iElement, iAttrs, controller) { ... }, - * // post: function postLink(scope, iElement, iAttrs, controller) { ... } - * // } - * // or - * // link: function postLink( ... ) { ... } - * }; - * return directiveDefinitionObject; - * }); + * var directiveDefinitionObject = { + * {@link $compile#-priority- priority}: 0, + * {@link $compile#-template- template}: '
    ', // or // function(tElement, tAttrs) { ... }, + * // or + * // {@link $compile#-templateurl- templateUrl}: 'directive.html', // or // function(tElement, tAttrs) { ... }, + * {@link $compile#-transclude- transclude}: false, + * {@link $compile#-restrict- restrict}: 'A', + * {@link $compile#-templatenamespace- templateNamespace}: 'html', + * {@link $compile#-scope- scope}: false, + * {@link $compile#-controller- controller}: function($scope, $element, $attrs, $transclude, otherInjectables) { ... }, + * {@link $compile#-controlleras- controllerAs}: 'stringIdentifier', + * {@link $compile#-bindtocontroller- bindToController}: false, + * {@link $compile#-require- require}: 'siblingDirectiveName', // or // ['^parentDirectiveName', '?optionalDirectiveName', '?^optionalParent'], + * {@link $compile#-multielement- multiElement}: false, + * {@link $compile#-compile- compile}: function compile(tElement, tAttrs, transclude) { + * return { + * {@link $compile#pre-linking-function pre}: function preLink(scope, iElement, iAttrs, controller) { ... }, + * {@link $compile#post-linking-function post}: function postLink(scope, iElement, iAttrs, controller) { ... } + * } + * // or + * // return function postLink( ... ) { ... } + * }, + * // or + * // {@link $compile#-link- link}: { + * // {@link $compile#pre-linking-function pre}: function preLink(scope, iElement, iAttrs, controller) { ... }, + * // {@link $compile#post-linking-function post}: function postLink(scope, iElement, iAttrs, controller) { ... } + * // } + * // or + * // {@link $compile#-link- link}: function postLink( ... ) { ... } + * }; + * return directiveDefinitionObject; + * }); * ``` * *
    @@ -5127,21 +7258,148 @@ * var myModule = angular.module(...); * * myModule.directive('directiveName', function factory(injectables) { - * var directiveDefinitionObject = { - * link: function postLink(scope, iElement, iAttrs) { ... } - * }; - * return directiveDefinitionObject; - * // or - * // return function postLink(scope, iElement, iAttrs) { ... } - * }); + * var directiveDefinitionObject = { + * link: function postLink(scope, iElement, iAttrs) { ... } + * }; + * return directiveDefinitionObject; + * // or + * // return function postLink(scope, iElement, iAttrs) { ... } + * }); * ``` * + * ### Life-cycle hooks + * Directive controllers can provide the following methods that are called by AngularJS at points in the life-cycle of the + * directive: + * * `$onInit()` - Called on each controller after all the controllers on an element have been constructed and + * had their bindings initialized (and before the pre & post linking functions for the directives on + * this element). This is a good place to put initialization code for your controller. + * * `$onChanges(changesObj)` - Called whenever one-way (`<`) or interpolation (`@`) bindings are updated. The + * `changesObj` is a hash whose keys are the names of the bound properties that have changed, and the values are an + * object of the form `{ currentValue, previousValue, isFirstChange() }`. Use this hook to trigger updates within a + * component such as cloning the bound value to prevent accidental mutation of the outer value. Note that this will + * also be called when your bindings are initialized. + * * `$doCheck()` - Called on each turn of the digest cycle. Provides an opportunity to detect and act on + * changes. Any actions that you wish to take in response to the changes that you detect must be + * invoked from this hook; implementing this has no effect on when `$onChanges` is called. For example, this hook + * could be useful if you wish to perform a deep equality check, or to check a Date object, changes to which would not + * be detected by AngularJS's change detector and thus not trigger `$onChanges`. This hook is invoked with no arguments; + * if detecting changes, you must store the previous value(s) for comparison to the current values. + * * `$onDestroy()` - Called on a controller when its containing scope is destroyed. Use this hook for releasing + * external resources, watches and event handlers. Note that components have their `$onDestroy()` hooks called in + * the same order as the `$scope.$broadcast` events are triggered, which is top down. This means that parent + * components will have their `$onDestroy()` hook called before child components. + * * `$postLink()` - Called after this controller's element and its children have been linked. Similar to the post-link + * function this hook can be used to set up DOM event handlers and do direct DOM manipulation. + * Note that child elements that contain `templateUrl` directives will not have been compiled and linked since + * they are waiting for their template to load asynchronously and their own compilation and linking has been + * suspended until that occurs. + * + * #### Comparison with life-cycle hooks in the new Angular + * The new Angular also uses life-cycle hooks for its components. While the AngularJS life-cycle hooks are similar there are + * some differences that you should be aware of, especially when it comes to moving your code from AngularJS to Angular: + * + * * AngularJS hooks are prefixed with `$`, such as `$onInit`. Angular hooks are prefixed with `ng`, such as `ngOnInit`. + * * AngularJS hooks can be defined on the controller prototype or added to the controller inside its constructor. + * In Angular you can only define hooks on the prototype of the Component class. + * * Due to the differences in change-detection, you may get many more calls to `$doCheck` in AngularJS than you would to + * `ngDoCheck` in Angular. + * * Changes to the model inside `$doCheck` will trigger new turns of the digest loop, which will cause the changes to be + * propagated throughout the application. + * Angular does not allow the `ngDoCheck` hook to trigger a change outside of the component. It will either throw an + * error or do nothing depending upon the state of `enableProdMode()`. + * + * #### Life-cycle hook examples + * + * This example shows how you can check for mutations to a Date object even though the identity of the object + * has not changed. + * + * + * + * angular.module('do-check-module', []) + * .component('app', { + * template: + * 'Month: ' + + * 'Date: {{ $ctrl.date }}' + + * '', + * controller: function() { + * this.date = new Date(); + * this.month = this.date.getMonth(); + * this.updateDate = function() { + * this.date.setMonth(this.month); + * }; + * } + * }) + * .component('test', { + * bindings: { date: '<' }, + * template: + * '
    {{ $ctrl.log | json }}
    ', + * controller: function() { + * var previousValue; + * this.log = []; + * this.$doCheck = function() { + * var currentValue = this.date && this.date.valueOf(); + * if (previousValue !== currentValue) { + * this.log.push('doCheck: date mutated: ' + this.date); + * previousValue = currentValue; + * } + * }; + * } + * }); + *
    + * + * + * + *
    * + * This example show how you might use `$doCheck` to trigger changes in your component's inputs even if the + * actual identity of the component doesn't change. (Be aware that cloning and deep equality checks on large + * arrays or objects can have a negative impact on your application performance) * - * ### Directive Definition Object - * - * The directive definition object provides instructions to the {@link ng.$compile - * compiler}. The attributes are: + * + * + *
    + * + * + *
    {{ items }}
    + * + *
    + *
    + * + * angular.module('do-check-module', []) + * .component('test', { + * bindings: { items: '<' }, + * template: + * '
    {{ $ctrl.log | json }}
    ', + * controller: function() { + * this.log = []; + * + * this.$doCheck = function() { + * if (this.items_ref !== this.items) { + * this.log.push('doCheck: items changed'); + * this.items_ref = this.items; + * } + * if (!angular.equals(this.items_clone, this.items)) { + * this.log.push('doCheck: items mutated'); + * this.items_clone = angular.copy(this.items); + * } + * }; + * } + * }); + *
    + *
    + * + * + * ### Directive Definition Object + * + * The directive definition object provides instructions to the {@link ng.$compile + * compiler}. The attributes are: + * + * #### `multiElement` + * When this property is set to true (default is `false`), the HTML compiler will collect DOM nodes between + * nodes with the attributes `directive-name-start` and `directive-name-end`, and group them + * together as the directive elements. It is recommended that this feature be used on directives + * which are not strictly behavioral (such as {@link ngClick}), and which + * do not manipulate or replace child nodes (such as {@link ngInclude}). * * #### `priority` * When there are multiple directives defined on a single DOM element, sometimes it @@ -5154,136 +7412,269 @@ * #### `terminal` * If set to true then the current `priority` will be the last set of directives * which will execute (any directives at the current priority will still execute - * as the order of execution on same `priority` is undefined). + * as the order of execution on same `priority` is undefined). Note that expressions + * and other directives used in the directive's template will also be excluded from execution. * * #### `scope` - * **If set to `true`,** then a new scope will be created for this directive. If multiple directives on the - * same element request a new scope, only one new scope is created. The new scope rule does not - * apply for the root of the template since the root of the template always gets a new scope. + * The scope property can be `false`, `true`, or an object: * - * **If set to `{}` (object hash),** then a new "isolate" scope is created. The 'isolate' scope differs from - * normal scope in that it does not prototypically inherit from the parent scope. This is useful - * when creating reusable components, which should not accidentally read or modify data in the - * parent scope. + * * **`false` (default):** No scope will be created for the directive. The directive will use its + * parent's scope. * - * The 'isolate' scope takes an object hash which defines a set of local scope properties - * derived from the parent scope. These local properties are useful for aliasing values for - * templates. Locals definition is a hash of local scope property to its source: + * * **`true`:** A new child scope that prototypically inherits from its parent will be created for + * the directive's element. If multiple directives on the same element request a new scope, + * only one new scope is created. + * + * * **`{...}` (an object hash):** A new "isolate" scope is created for the directive's template. + * The 'isolate' scope differs from normal scope in that it does not prototypically + * inherit from its parent scope. This is useful when creating reusable components, which should not + * accidentally read or modify data in the parent scope. Note that an isolate scope + * directive without a `template` or `templateUrl` will not apply the isolate scope + * to its children elements. + * + * The 'isolate' scope object hash defines a set of local scope properties derived from attributes on the + * directive's element. These local properties are useful for aliasing values for templates. The keys in + * the object hash map to the name of the property on the isolate scope; the values define how the property + * is bound to the parent scope, via matching attributes on the directive's element: * * * `@` or `@attr` - bind a local scope property to the value of DOM attribute. The result is - * always a string since DOM attributes are strings. If no `attr` name is specified then the - * attribute name is assumed to be the same as the local name. - * Given `` and widget definition - * of `scope: { localName:'@myAttr' }`, then widget scope property `localName` will reflect - * the interpolated value of `hello {{name}}`. As the `name` attribute changes so will the - * `localName` property on the widget scope. The `name` is read from the parent scope (not - * component scope). - * - * * `=` or `=attr` - set up bi-directional binding between a local scope property and the - * parent scope property of name defined via the value of the `attr` attribute. If no `attr` - * name is specified then the attribute name is assumed to be the same as the local name. - * Given `` and widget definition of - * `scope: { localModel:'=myAttr' }`, then widget scope property `localModel` will reflect the + * always a string since DOM attributes are strings. If no `attr` name is specified then the + * attribute name is assumed to be the same as the local name. Given `` and the isolate scope definition `scope: { localName:'@myAttr' }`, + * the directive's scope property `localName` will reflect the interpolated value of `hello + * {{name}}`. As the `name` attribute changes so will the `localName` property on the directive's + * scope. The `name` is read from the parent scope (not the directive's scope). + * + * * `=` or `=attr` - set up a bidirectional binding between a local scope property and an expression + * passed via the attribute `attr`. The expression is evaluated in the context of the parent scope. + * If no `attr` name is specified then the attribute name is assumed to be the same as the local + * name. Given `` and the isolate scope definition `scope: { + * localModel: '=myAttr' }`, the property `localModel` on the directive's scope will reflect the + * value of `parentModel` on the parent scope. Changes to `parentModel` will be reflected in + * `localModel` and vice versa. Optional attributes should be marked as such with a question mark: + * `=?` or `=?attr`. If the binding expression is non-assignable, or if the attribute isn't + * optional and doesn't exist, an exception ({@link error/$compile/nonassign `$compile:nonassign`}) + * will be thrown upon discovering changes to the local value, since it will be impossible to sync + * them back to the parent scope. By default, the {@link ng.$rootScope.Scope#$watch `$watch`} + * method is used for tracking changes, and the equality check is based on object identity. + * However, if an object literal or an array literal is passed as the binding expression, the + * equality check is done by value (using the {@link angular.equals} function). It's also possible + * to watch the evaluated value shallowly with {@link ng.$rootScope.Scope#$watchCollection + * `$watchCollection`}: use `=*` or `=*attr` (`=*?` or `=*?attr` if the attribute is optional). + * + * * `<` or `` and directive definition of + * `scope: { localModel:'` and the isolate scope definition `scope: { + * localFn:'&myAttr' }`, the isolate scope property `localFn` will point to a function wrapper for + * the `count = count + value` expression. Often it's desirable to pass data from the isolated scope + * via an expression to the parent scope. This can be done by passing a map of local variable names + * and values into the expression wrapper fn. For example, if the expression is `increment(amount)` + * then we can specify the amount value by calling the `localFn` as `localFn({amount: 22})`. + * + * In general it's possible to apply more than one directive to one element, but there might be limitations + * depending on the type of scope required by the directives. The following points will help explain these limitations. + * For simplicity only two directives are taken into account, but it is also applicable for several directives: + * + * * **no scope** + **no scope** => Two directives which don't require their own scope will use their parent's scope + * * **child scope** + **no scope** => Both directives will share one single child scope + * * **child scope** + **child scope** => Both directives will share one single child scope + * * **isolated scope** + **no scope** => The isolated directive will use it's own created isolated scope. The other directive will use + * its parent's scope + * * **isolated scope** + **child scope** => **Won't work!** Only one scope can be related to one element. Therefore these directives cannot + * be applied to the same element. + * * **isolated scope** + **isolated scope** => **Won't work!** Only one scope can be related to one element. Therefore these directives + * cannot be applied to the same element. + * + * + * #### `bindToController` + * This property is used to bind scope properties directly to the controller. It can be either + * `true` or an object hash with the same format as the `scope` property. + * + * When an isolate scope is used for a directive (see above), `bindToController: true` will + * allow a component to have its properties bound to the controller, rather than to scope. + * + * After the controller is instantiated, the initial values of the isolate scope bindings will be bound to the controller + * properties. You can access these bindings once they have been initialized by providing a controller method called + * `$onInit`, which is called after all the controllers on an element have been constructed and had their bindings + * initialized. + * + *
    + * **Deprecation warning:** if `$compileProcvider.preAssignBindingsEnabled(true)` was called, bindings for non-ES6 class + * controllers are bound to `this` before the controller constructor is called but this use is now deprecated. Please + * place initialization code that relies upon bindings inside a `$onInit` method on the controller, instead. + *
    * - * * `&` or `&attr` - provides a way to execute an expression in the context of the parent scope. - * If no `attr` name is specified then the attribute name is assumed to be the same as the - * local name. Given `` and widget definition of - * `scope: { localFn:'&myAttr' }`, then isolate scope property `localFn` will point to - * a function wrapper for the `count = count + value` expression. Often it's desirable to - * pass data from the isolated scope via an expression and to the parent scope, this can be - * done by passing a map of local variable names and values into the expression wrapper fn. - * For example, if the expression is `increment(amount)` then we can specify the amount value - * by calling the `localFn` as `localFn({amount: 22})`. + * It is also possible to set `bindToController` to an object hash with the same format as the `scope` property. + * This will set up the scope bindings to the controller directly. Note that `scope` can still be used + * to define which kind of scope is created. By default, no scope is created. Use `scope: {}` to create an isolate + * scope (useful for component directives). * + * If both `bindToController` and `scope` are defined and have object hashes, `bindToController` overrides `scope`. * * * #### `controller` * Controller constructor function. The controller is instantiated before the - * pre-linking phase and it is shared with other directives (see + * pre-linking phase and can be accessed by other directives (see * `require` attribute). This allows the directives to communicate with each other and augment * each other's behavior. The controller is injectable (and supports bracket notation) with the following locals: * * * `$scope` - Current scope associated with the element * * `$element` - Current element * * `$attrs` - Current attributes object for the element - * * `$transclude` - A transclude linking function pre-bound to the correct transclusion scope. - * The scope can be overridden by an optional first argument. - * `function([scope], cloneLinkingFn)`. - * + * * `$transclude` - A transclude linking function pre-bound to the correct transclusion scope: + * `function([scope], cloneLinkingFn, futureParentElement, slotName)`: + * * `scope`: (optional) override the scope. + * * `cloneLinkingFn`: (optional) argument to create clones of the original transcluded content. + * * `futureParentElement` (optional): + * * defines the parent to which the `cloneLinkingFn` will add the cloned elements. + * * default: `$element.parent()` resp. `$element` for `transclude:'element'` resp. `transclude:true`. + * * only needed for transcludes that are allowed to contain non html elements (e.g. SVG elements) + * and when the `cloneLinkingFn` is passed, + * as those elements need to created and cloned in a special way when they are defined outside their + * usual containers (e.g. like ``). + * * See also the `directive.templateNamespace` property. + * * `slotName`: (optional) the name of the slot to transclude. If falsy (e.g. `null`, `undefined` or `''`) + * then the default transclusion is provided. + * The `$transclude` function also has a method on it, `$transclude.isSlotFilled(slotName)`, which returns + * `true` if the specified slot contains content (i.e. one or more DOM nodes). * * #### `require` * Require another directive and inject its controller as the fourth argument to the linking function. The - * `require` takes a string name (or array of strings) of the directive(s) to pass in. If an array is used, the - * injected argument will be an array in corresponding order. If no such directive can be - * found, or if the directive does not have a controller, then an error is raised. The name can be prefixed with: + * `require` property can be a string, an array or an object: + * * a **string** containing the name of the directive to pass to the linking function + * * an **array** containing the names of directives to pass to the linking function. The argument passed to the + * linking function will be an array of controllers in the same order as the names in the `require` property + * * an **object** whose property values are the names of the directives to pass to the linking function. The argument + * passed to the linking function will also be an object with matching keys, whose values will hold the corresponding + * controllers. + * + * If the `require` property is an object and `bindToController` is truthy, then the required controllers are + * bound to the controller using the keys of the `require` property. This binding occurs after all the controllers + * have been constructed but before `$onInit` is called. + * If the name of the required controller is the same as the local name (the key), the name can be + * omitted. For example, `{parentDir: '^^'}` is equivalent to `{parentDir: '^^parentDir'}`. + * See the {@link $compileProvider#component} helper for an example of how this can be used. + * If no such required directive(s) can be found, or if the directive does not have a controller, then an error is + * raised (unless no link function is specified and the required controllers are not being bound to the directive + * controller, in which case error checking is skipped). The name can be prefixed with: * * * (no prefix) - Locate the required controller on the current element. Throw an error if not found. * * `?` - Attempt to locate the required controller or pass `null` to the `link` fn if not found. - * * `^` - Locate the required controller by searching the element's parents. Throw an error if not found. - * * `?^` - Attempt to locate the required controller by searching the element's parents or pass `null` to the - * `link` fn if not found. + * * `^` - Locate the required controller by searching the element and its parents. Throw an error if not found. + * * `^^` - Locate the required controller by searching the element's parents. Throw an error if not found. + * * `?^` - Attempt to locate the required controller by searching the element and its parents or pass + * `null` to the `link` fn if not found. + * * `?^^` - Attempt to locate the required controller by searching the element's parents, or pass + * `null` to the `link` fn if not found. * * * #### `controllerAs` - * Controller alias at the directive scope. An alias for the controller so it - * can be referenced at the directive template. The directive needs to define a scope for this - * configuration to be used. Useful in the case when directive is used as component. + * Identifier name for a reference to the controller in the directive's scope. + * This allows the controller to be referenced from the directive template. This is especially + * useful when a directive is used as component, i.e. with an `isolate` scope. It's also possible + * to use it in a directive without an `isolate` / `new` scope, but you need to be aware that the + * `controllerAs` reference might overwrite a property that already exists on the parent scope. * * * #### `restrict` * String of subset of `EACM` which restricts the directive to a specific directive - * declaration style. If omitted, the default (attributes only) is used. + * declaration style. If omitted, the defaults (elements and attributes) are used. * - * * `E` - Element name: `` + * * `E` - Element name (default): `` * * `A` - Attribute (default): `
    ` * * `C` - Class: `
    ` * * `M` - Comment: `` * * + * #### `templateNamespace` + * String representing the document type used by the markup in the template. + * AngularJS needs this information as those elements need to be created and cloned + * in a special way when they are defined outside their usual containers like `` and ``. + * + * * `html` - All root nodes in the template are HTML. Root nodes may also be + * top-level elements such as `` or ``. + * * `svg` - The root nodes in the template are SVG elements (excluding ``). + * * `math` - The root nodes in the template are MathML elements (excluding ``). + * + * If no `templateNamespace` is specified, then the namespace is considered to be `html`. + * * #### `template` - * replace the current element with the contents of the HTML. The replacement process - * migrates all of the attributes / classes from the old element to the new one. See the - * {@link guide/directive#creating-custom-directives_creating-directives_template-expanding-directive - * Directives Guide} for an example. + * HTML markup that may: + * * Replace the contents of the directive's element (default). + * * Replace the directive's element itself (if `replace` is true - DEPRECATED). + * * Wrap the contents of the directive's element (if `transclude` is true). * - * You can specify `template` as a string representing the template or as a function which takes - * two arguments `tElement` and `tAttrs` (described in the `compile` function api below) and - * returns a string value representing the template. + * Value may be: + * + * * A string. For example `
    {{delete_str}}
    `. + * * A function which takes two arguments `tElement` and `tAttrs` (described in the `compile` + * function api below) and returns a string value. * * * #### `templateUrl` - * Same as `template` but the template is loaded from the specified URL. Because - * the template loading is asynchronous the compilation/linking is suspended until the template - * is loaded. + * This is similar to `template` but the template is loaded from the specified URL, asynchronously. + * + * Because template loading is asynchronous the compiler will suspend compilation of directives on that element + * for later when the template has been resolved. In the meantime it will continue to compile and link + * sibling and parent elements as though this element had not contained any directives. + * + * The compiler does not suspend the entire compilation to wait for templates to be loaded because this + * would result in the whole app "stalling" until all templates are loaded asynchronously - even in the + * case when only one deeply nested directive has `templateUrl`. + * + * Template loading is asynchronous even if the template has been preloaded into the {@link $templateCache} * * You can specify `templateUrl` as a string representing the URL or as a function which takes two * arguments `tElement` and `tAttrs` (described in the `compile` function api below) and returns * a string value representing the url. In either case, the template URL is passed through {@link - * api/ng.$sce#getTrustedResourceUrl $sce.getTrustedResourceUrl}. + * $sce#getTrustedResourceUrl $sce.getTrustedResourceUrl}. * * - * #### `replace` - * specify where the template should be inserted. Defaults to `false`. + * #### `replace` (*DEPRECATED*) * - * * `true` - the template will replace the current element. - * * `false` - the template will replace the contents of the current element. + * `replace` will be removed in next major release - i.e. v2.0). * + * Specifies what the template should replace. Defaults to `false`. * - * #### `transclude` - * compile the content of the element and make it available to the directive. - * Typically used with {@link ng.directive:ngTransclude - * ngTransclude}. The advantage of transclusion is that the linking function receives a - * transclusion function which is pre-bound to the correct scope. In a typical setup the widget - * creates an `isolate` scope, but the transclusion is not a child, but a sibling of the `isolate` - * scope. This makes it possible for the widget to have private state, and the transclusion to - * be bound to the parent (pre-`isolate`) scope. + * * `true` - the template will replace the directive's element. + * * `false` - the template will replace the contents of the directive's element. + * + * The replacement process migrates all of the attributes / classes from the old element to the new + * one. See the {@link guide/directive#template-expanding-directive + * Directives Guide} for an example. + * + * There are very few scenarios where element replacement is required for the application function, + * the main one being reusable custom components that are used within SVG contexts + * (because SVG doesn't work with custom elements in the DOM tree). * - * * `true` - transclude the content of the directive. - * * `'element'` - transclude the whole element including any directives defined at lower priority. + * #### `transclude` + * Extract the contents of the element where the directive appears and make it available to the directive. + * The contents are compiled and provided to the directive as a **transclusion function**. See the + * {@link $compile#transclusion Transclusion} section below. * * * #### `compile` @@ -5293,11 +7684,7 @@ * ``` * * The compile function deals with transforming the template DOM. Since most directives do not do - * template transformation, it is not used often. Examples that require compile functions are - * directives that transform template DOM, such as {@link - * api/ng.directive:ngRepeat ngRepeat}, or load the contents - * asynchronously, such as {@link ngRoute.directive:ngView ngView}. The - * compile function takes the following arguments. + * template transformation, it is not used often. The compile function takes the following arguments: * * * `tElement` - template element - The element where the directive has been declared. It is * safe to do template transformation on the element and child elements only. @@ -5316,7 +7703,7 @@ *
    * **Note:** The compile function cannot handle directives that recursively use themselves in their - * own templates or compile functions. Compiling these directives results in an infinite loop and a + * own templates or compile functions. Compiling these directives results in an infinite loop and * stack overflow errors. * * This can be avoided by manually using $compile in the postLink function to imperatively compile @@ -5324,7 +7711,7 @@ * `templateUrl` declaration or manual compilation inside the compile function. *
    * - *
    + *
    * **Note:** The `transclude` function that is passed to the compile function is deprecated, as it * e.g. does not know about the right outer scope. Please use the transclude function that is passed * to the link function instead. @@ -5361,15 +7748,23 @@ * * `iAttrs` - instance attributes - Normalized list of attributes declared on this element shared * between all directive linking functions. * - * * `controller` - a controller instance - A controller instance if at least one directive on the - * element defines a controller. The controller is shared among all the directives, which allows - * the directives to use the controllers as a communication channel. + * * `controller` - the directive's required controller instance(s) - Instances are shared + * among all directives, which allows the directives to use the controllers as a communication + * channel. The exact value depends on the directive's `require` property: + * * no controller(s) required: the directive's own controller, or `undefined` if it doesn't have one + * * `string`: the controller instance + * * `array`: array of controller instances * - * * `transcludeFn` - A transclude linking function pre-bound to the correct transclusion scope. - * The scope can be overridden by an optional first argument. This is the same as the `$transclude` - * parameter of directive controllers. - * `function([scope], cloneLinkingFn)`. + * If a required controller cannot be found, and it is optional, the instance is `null`, + * otherwise the {@link error:$compile:ctreq Missing Required Controller} error is thrown. * + * Note that you can also require the directive's own controller - it will be made available like + * any other controller. + * + * * `transcludeFn` - A transclude linking function pre-bound to the correct transclusion scope. + * This is the same as the `$transclude` parameter of directive controllers, + * see {@link ng.$compile#-controller- the controller section for details}. + * `function([scope], cloneLinkingFn, futureParentElement)`. * * #### Pre-linking function * @@ -5378,18 +7773,166 @@ * * #### Post-linking function * - * Executed after the child elements are linked. It is safe to do DOM transformation in the post-linking function. + * Executed after the child elements are linked. + * + * Note that child elements that contain `templateUrl` directives will not have been compiled + * and linked since they are waiting for their template to load asynchronously and their own + * compilation and linking has been suspended until that occurs. + * + * It is safe to do DOM transformation in the post-linking function on elements that are not waiting + * for their async templates to be resolved. + * + * + * ### Transclusion + * + * Transclusion is the process of extracting a collection of DOM elements from one part of the DOM and + * copying them to another part of the DOM, while maintaining their connection to the original AngularJS + * scope from where they were taken. + * + * Transclusion is used (often with {@link ngTransclude}) to insert the + * original contents of a directive's element into a specified place in the template of the directive. + * The benefit of transclusion, over simply moving the DOM elements manually, is that the transcluded + * content has access to the properties on the scope from which it was taken, even if the directive + * has isolated scope. + * See the {@link guide/directive#creating-a-directive-that-wraps-other-elements Directives Guide}. + * + * This makes it possible for the widget to have private state for its template, while the transcluded + * content has access to its originating scope. + * + *
    + * **Note:** When testing an element transclude directive you must not place the directive at the root of the + * DOM fragment that is being compiled. See {@link guide/unit-testing#testing-transclusion-directives + * Testing Transclusion Directives}. + *
    + * + * There are three kinds of transclusion depending upon whether you want to transclude just the contents of the + * directive's element, the entire element or multiple parts of the element contents: + * + * * `true` - transclude the content (i.e. the child nodes) of the directive's element. + * * `'element'` - transclude the whole of the directive's element including any directives on this + * element that defined at a lower priority than this directive. When used, the `template` + * property is ignored. + * * **`{...}` (an object hash):** - map elements of the content onto transclusion "slots" in the template. + * + * **Mult-slot transclusion** is declared by providing an object for the `transclude` property. + * + * This object is a map where the keys are the name of the slot to fill and the value is an element selector + * used to match the HTML to the slot. The element selector should be in normalized form (e.g. `myElement`) + * and will match the standard element variants (e.g. `my-element`, `my:element`, `data-my-element`, etc). + * + * For further information check out the guide on {@link guide/directive#matching-directives Matching Directives} + * + * If the element selector is prefixed with a `?` then that slot is optional. + * + * For example, the transclude object `{ slotA: '?myCustomElement' }` maps `` elements to + * the `slotA` slot, which can be accessed via the `$transclude` function or via the {@link ngTransclude} directive. + * + * Slots that are not marked as optional (`?`) will trigger a compile time error if there are no matching elements + * in the transclude content. If you wish to know if an optional slot was filled with content, then you can call + * `$transclude.isSlotFilled(slotName)` on the transclude function passed to the directive's link function and + * injectable into the directive's controller. + * + * + * #### Transclusion Functions + * + * When a directive requests transclusion, the compiler extracts its contents and provides a **transclusion + * function** to the directive's `link` function and `controller`. This transclusion function is a special + * **linking function** that will return the compiled contents linked to a new transclusion scope. + * + *
    + * If you are just using {@link ngTransclude} then you don't need to worry about this function, since + * ngTransclude will deal with it for us. + *
    + * + * If you want to manually control the insertion and removal of the transcluded content in your directive + * then you must use this transclude function. When you call a transclude function it returns a a jqLite/JQuery + * object that contains the compiled DOM, which is linked to the correct transclusion scope. + * + * When you call a transclusion function you can pass in a **clone attach function**. This function accepts + * two parameters, `function(clone, scope) { ... }`, where the `clone` is a fresh compiled copy of your transcluded + * content and the `scope` is the newly created transclusion scope, which the clone will be linked to. + * + *
    + * **Best Practice**: Always provide a `cloneFn` (clone attach function) when you call a transclude function + * since you then get a fresh clone of the original DOM and also have access to the new transclusion scope. + *
    + * + * It is normal practice to attach your transcluded content (`clone`) to the DOM inside your **clone + * attach function**: + * + * ```js + * var transcludedContent, transclusionScope; + * + * $transclude(function(clone, scope) { + * element.append(clone); + * transcludedContent = clone; + * transclusionScope = scope; + * }); + * ``` + * + * Later, if you want to remove the transcluded content from your DOM then you should also destroy the + * associated transclusion scope: + * + * ```js + * transcludedContent.remove(); + * transclusionScope.$destroy(); + * ``` + * + *
    + * **Best Practice**: if you intend to add and remove transcluded content manually in your directive + * (by calling the transclude function to get the DOM and calling `element.remove()` to remove it), + * then you are also responsible for calling `$destroy` on the transclusion scope. + *
    + * + * The built-in DOM manipulation directives, such as {@link ngIf}, {@link ngSwitch} and {@link ngRepeat} + * automatically destroy their transcluded clones as necessary so you do not need to worry about this if + * you are simply using {@link ngTransclude} to inject the transclusion into your directive. + * + * + * #### Transclusion Scopes + * + * When you call a transclude function it returns a DOM fragment that is pre-bound to a **transclusion + * scope**. This scope is special, in that it is a child of the directive's scope (and so gets destroyed + * when the directive's scope gets destroyed) but it inherits the properties of the scope from which it + * was taken. + * + * For example consider a directive that uses transclusion and isolated scope. The DOM hierarchy might look + * like this: + * + * ```html + *
    + *
    + *
    + *
    + *
    + *
    + * ``` + * + * The `$parent` scope hierarchy will look like this: + * + ``` + - $rootScope + - isolate + - transclusion + ``` + * + * but the scopes will inherit prototypically from different scopes to their `$parent`. + * + ``` + - $rootScope + - transclusion + - isolate + ``` + * * - * * ### Attributes * * The {@link ng.$compile.directive.Attributes Attributes} object - passed as a parameter in the * `link()` or `compile()` functions. It has a variety of uses. * - * accessing *Normalized attribute names:* - * Directives like 'ngBind' can be expressed in many ways: 'ng:bind', `data-ng-bind`, or 'x-ng-bind'. - * the attributes object allows for normalized access to - * the attributes. + * * *Accessing normalized attribute names:* Directives like 'ngBind' can be expressed in many ways: + * 'ng:bind', `data-ng-bind`, or 'x-ng-bind'. The attributes object allows for normalized access + * to the attributes. * * * *Directive inter-communication:* All directives share the same instance of the attributes * object which allows the directives to use the attributes object as inter directive @@ -5405,30 +7948,30 @@ * * ```js * function linkingFn(scope, elm, attrs, ctrl) { - * // get the attribute value - * console.log(attrs.ngModel); - * - * // change the attribute - * attrs.$set('ngModel', 'new value'); - * - * // observe changes to interpolated attribute - * attrs.$observe('ngModel', function(value) { - * console.log('ngModel has changed value to ' + value); - * }); - * } + * // get the attribute value + * console.log(attrs.ngModel); + * + * // change the attribute + * attrs.$set('ngModel', 'new value'); + * + * // observe changes to interpolated attribute + * attrs.$observe('ngModel', function(value) { + * console.log('ngModel has changed value to ' + value); + * }); + * } * ``` * - * Below is an example using `$compileProvider`. + * ## Example * *
    * **Note**: Typically directives are registered with `module.directive`. The example below is * to illustrate how `$compile` works. *
    * - + -
    -
    -
    +
    +
    +
    @@ -5470,11 +8012,11 @@ it('should auto compile', function() { var textarea = $('textarea'); var output = $('div[compile]'); - // The initial state reads 'Hello Angular'. - expect(output.getText()).toBe('Hello Angular'); + // The initial state reads 'Hello AngularJS'. + expect(output.getText()).toBe('Hello AngularJS'); textarea.clear(); textarea.sendKeys('{{name}}!'); - expect(output.getText()).toBe('Angular!'); + expect(output.getText()).toBe('AngularJS!'); }); @@ -5482,26 +8024,53 @@ * * * @param {string|DOMElement} element Element or HTML string to compile into a template function. - * @param {function(angular.Scope, cloneAttachFn=)} transclude function available to directives. + * @param {function(angular.Scope, cloneAttachFn=)} transclude function available to directives - DEPRECATED. + * + *
    + * **Note:** Passing a `transclude` function to the $compile function is deprecated, as it + * e.g. will not use the right outer scope. Please pass the transclude function as a + * `parentBoundTranscludeFn` to the link function instead. + *
    + * * @param {number} maxPriority only apply directives lower than given priority (Only effects the * root element(s), not their children) - * @returns {function(scope, cloneAttachFn=)} a link function which is used to bind template + * @returns {function(scope, cloneAttachFn=, options=)} a link function which is used to bind template * (a DOM element/tree) to a scope. Where: * * * `scope` - A {@link ng.$rootScope.Scope Scope} to bind to. * * `cloneAttachFn` - If `cloneAttachFn` is provided, then the link function will clone the * `template` and call the `cloneAttachFn` function allowing the caller to attach the * cloned elements to the DOM document at the appropriate place. The `cloneAttachFn` is - * called as:
    `cloneAttachFn(clonedElement, scope)` where: + * called as:
    `cloneAttachFn(clonedElement, scope)` where: * * * `clonedElement` - is a clone of the original `element` passed into the compiler. * * `scope` - is the current scope with which the linking function is working with. * + * * `options` - An optional object hash with linking options. If `options` is provided, then the following + * keys may be used to control linking behavior: + * + * * `parentBoundTranscludeFn` - the transclude function made available to + * directives; if given, it will be passed through to the link functions of + * directives found in `element` during compilation. + * * `transcludeControllers` - an object hash with keys that map controller names + * to a hash with the key `instance`, which maps to the controller instance; + * if given, it will make the controllers available to directives on the compileNode: + * ``` + * { + * parent: { + * instance: parentControllerInstance + * } + * } + * ``` + * * `futureParentElement` - defines the parent to which the `cloneAttachFn` will add + * the cloned elements; only needed for transcludes that are allowed to contain non html + * elements (e.g. SVG elements). See also the directive.controller property. + * * Calling the linking function returns the element of the template. It is either the original * element passed in, or the clone of the element if the `cloneAttachFn` is provided. * * After linking the view is not updated until after a call to $digest which typically is done by - * Angular automatically. + * AngularJS automatically. * * If you need access to the bound view, there are two ways to do it: * @@ -5519,42 +8088,158 @@ * scope = ....; * * var clonedElement = $compile(templateElement)(scope, function(clonedElement, scope) { - * //attach the clone to DOM document at the right place - * }); + * //attach the clone to DOM document at the right place + * }); * * //now we have reference to the cloned DOM via `clonedElement` * ``` * * * For information on how the compiler works, see the - * {@link guide/compiler Angular HTML Compiler} section of the Developer Guide. + * {@link guide/compiler AngularJS HTML Compiler} section of the Developer Guide. + * + * @knownIssue + * + * ### Double Compilation + * + Double compilation occurs when an already compiled part of the DOM gets + compiled again. This is an undesired effect and can lead to misbehaving directives, performance issues, + and memory leaks. Refer to the Compiler Guide {@link guide/compiler#double-compilation-and-how-to-avoid-it + section on double compilation} for an in-depth explanation and ways to avoid it. + * */ var $compileMinErr = minErr('$compile'); + function UNINITIALIZED_VALUE() {} + var _UNINITIALIZED_VALUE = new UNINITIALIZED_VALUE(); + /** * @ngdoc provider * @name $compileProvider - * @function * * @description */ $CompileProvider.$inject = ['$provide', '$$sanitizeUriProvider']; + /** @this */ function $CompileProvider($provide, $$sanitizeUriProvider) { var hasDirectives = {}, Suffix = 'Directive', - COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\d\w\-_]+)\s+(.*)$/, - CLASS_DIRECTIVE_REGEXP = /(([\d\w\-_]+)(?:\:([^;]+))?;?)/; + COMMENT_DIRECTIVE_REGEXP = /^\s*directive:\s*([\w-]+)\s+(.*)$/, + CLASS_DIRECTIVE_REGEXP = /(([\w-]+)(?::([^;]+))?;?)/, + ALL_OR_NOTHING_ATTRS = makeMap('ngSrc,ngSrcset,src,srcset'), + REQUIRE_PREFIX_REGEXP = /^(?:(\^\^?)?(\?)?(\^\^?)?)?/; // Ref: http://developers.whatwg.org/webappapis.html#event-handler-idl-attributes // The assumption is that future DOM event attribute names will begin with // 'on' and be composed of only English letters. var EVENT_HANDLER_ATTR_REGEXP = /^(on[a-z]+|formaction)$/; + var bindingCache = createMap(); + + function parseIsolateBindings(scope, directiveName, isController) { + var LOCAL_REGEXP = /^\s*([@&<]|=(\*?))(\??)\s*([\w$]*)\s*$/; + + var bindings = createMap(); + + forEach(scope, function(definition, scopeName) { + if (definition in bindingCache) { + bindings[scopeName] = bindingCache[definition]; + return; + } + var match = definition.match(LOCAL_REGEXP); + + if (!match) { + throw $compileMinErr('iscp', + 'Invalid {3} for directive \'{0}\'.' + + ' Definition: {... {1}: \'{2}\' ...}', + directiveName, scopeName, definition, + (isController ? 'controller bindings definition' : + 'isolate scope definition')); + } + + bindings[scopeName] = { + mode: match[1][0], + collection: match[2] === '*', + optional: match[3] === '?', + attrName: match[4] || scopeName + }; + if (match[4]) { + bindingCache[definition] = bindings[scopeName]; + } + }); + + return bindings; + } + + function parseDirectiveBindings(directive, directiveName) { + var bindings = { + isolateScope: null, + bindToController: null + }; + if (isObject(directive.scope)) { + if (directive.bindToController === true) { + bindings.bindToController = parseIsolateBindings(directive.scope, + directiveName, true); + bindings.isolateScope = {}; + } else { + bindings.isolateScope = parseIsolateBindings(directive.scope, + directiveName, false); + } + } + if (isObject(directive.bindToController)) { + bindings.bindToController = + parseIsolateBindings(directive.bindToController, directiveName, true); + } + if (bindings.bindToController && !directive.controller) { + // There is no controller + throw $compileMinErr('noctrl', + 'Cannot bind to controller without directive \'{0}\'s controller.', + directiveName); + } + return bindings; + } + + function assertValidDirectiveName(name) { + var letter = name.charAt(0); + if (!letter || letter !== lowercase(letter)) { + throw $compileMinErr('baddir', 'Directive/Component name \'{0}\' is invalid. The first character must be a lowercase letter', name); + } + if (name !== name.trim()) { + throw $compileMinErr('baddir', + 'Directive/Component name \'{0}\' is invalid. The name should not contain leading or trailing whitespaces', + name); + } + } + + function getDirectiveRequire(directive) { + var require = directive.require || (directive.controller && directive.name); + + if (!isArray(require) && isObject(require)) { + forEach(require, function(value, key) { + var match = value.match(REQUIRE_PREFIX_REGEXP); + var name = value.substring(match[0].length); + if (!name) require[key] = match[0] + key; + }); + } + + return require; + } + + function getDirectiveRestrict(restrict, name) { + if (restrict && !(isString(restrict) && /[EACM]/.test(restrict))) { + throw $compileMinErr('badrestrict', + 'Restrict property \'{0}\' of directive \'{1}\' is invalid', + restrict, + name); + } + + return restrict || 'EA'; + } /** * @ngdoc method * @name $compileProvider#directive - * @function + * @kind function * * @description * Register a new directive with the compiler. @@ -5562,13 +8247,15 @@ * @param {string|Object} name Name of the directive in camel-case (i.e. ngBind which * will match as ng-bind), or an object map of directives where the keys are the * names and the values are the factories. - * @param {Function|Array} directiveFactory An injectable directive factory function. See - * {@link guide/directive} for more info. + * @param {Function|Array} directiveFactory An injectable directive factory function. See the + * {@link guide/directive directive guide} and the {@link $compile compile API} for more info. * @returns {ng.$compileProvider} Self for chaining. */ this.directive = function registerDirective(name, directiveFactory) { + assertArg(name, 'name'); assertNotHasOwnProperty(name, 'directive'); if (isString(name)) { + assertValidDirectiveName(name); assertArg(directiveFactory, 'directiveFactory'); if (!hasDirectives.hasOwnProperty(name)) { hasDirectives[name] = []; @@ -5586,8 +8273,9 @@ directive.priority = directive.priority || 0; directive.index = index; directive.name = directive.name || name; - directive.require = directive.require || (directive.controller && directive.name); - directive.restrict = directive.restrict || 'A'; + directive.require = getDirectiveRequire(directive); + directive.restrict = getDirectiveRestrict(directive.restrict, name); + directive.$$moduleName = directiveFactory.$$moduleName; directives.push(directive); } catch (e) { $exceptionHandler(e); @@ -5603,17 +8291,164 @@ return this; }; + /** + * @ngdoc method + * @name $compileProvider#component + * @module ng + * @param {string|Object} name Name of the component in camelCase (i.e. `myComp` which will match ``), + * or an object map of components where the keys are the names and the values are the component definition objects. + * @param {Object} options Component definition object (a simplified + * {@link ng.$compile#directive-definition-object directive definition object}), + * with the following properties (all optional): + * + * - `controller` – `{(string|function()=}` – controller constructor function that should be + * associated with newly created scope or the name of a {@link ng.$compile#-controller- + * registered controller} if passed as a string. An empty `noop` function by default. + * - `controllerAs` – `{string=}` – identifier name for to reference the controller in the component's scope. + * If present, the controller will be published to scope under the `controllerAs` name. + * If not present, this will default to be `$ctrl`. + * - `template` – `{string=|function()=}` – html template as a string or a function that + * returns an html template as a string which should be used as the contents of this component. + * Empty string by default. + * + * If `template` is a function, then it is {@link auto.$injector#invoke injected} with + * the following locals: + * + * - `$element` - Current element + * - `$attrs` - Current attributes object for the element + * + * - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html + * template that should be used as the contents of this component. + * + * If `templateUrl` is a function, then it is {@link auto.$injector#invoke injected} with + * the following locals: + * + * - `$element` - Current element + * - `$attrs` - Current attributes object for the element + * + * - `bindings` – `{object=}` – defines bindings between DOM attributes and component properties. + * Component properties are always bound to the component controller and not to the scope. + * See {@link ng.$compile#-bindtocontroller- `bindToController`}. + * - `transclude` – `{boolean=}` – whether {@link $compile#transclusion content transclusion} is enabled. + * Disabled by default. + * - `require` - `{Object=}` - requires the controllers of other directives and binds them to + * this component's controller. The object keys specify the property names under which the required + * controllers (object values) will be bound. See {@link ng.$compile#-require- `require`}. + * - `$...` – additional properties to attach to the directive factory function and the controller + * constructor function. (This is used by the component router to annotate) + * + * @returns {ng.$compileProvider} the compile provider itself, for chaining of function calls. + * @description + * Register a **component definition** with the compiler. This is a shorthand for registering a special + * type of directive, which represents a self-contained UI component in your application. Such components + * are always isolated (i.e. `scope: {}`) and are always restricted to elements (i.e. `restrict: 'E'`). + * + * Component definitions are very simple and do not require as much configuration as defining general + * directives. Component definitions usually consist only of a template and a controller backing it. + * + * In order to make the definition easier, components enforce best practices like use of `controllerAs`, + * `bindToController`. They always have **isolate scope** and are restricted to elements. + * + * Here are a few examples of how you would usually define components: + * + * ```js + * var myMod = angular.module(...); + * myMod.component('myComp', { + * template: '
    My name is {{$ctrl.name}}
    ', + * controller: function() { + * this.name = 'shahar'; + * } + * }); + * + * myMod.component('myComp', { + * template: '
    My name is {{$ctrl.name}}
    ', + * bindings: {name: '@'} + * }); + * + * myMod.component('myComp', { + * templateUrl: 'views/my-comp.html', + * controller: 'MyCtrl', + * controllerAs: 'ctrl', + * bindings: {name: '@'} + * }); + * + * ``` + * For more examples, and an in-depth guide, see the {@link guide/component component guide}. + * + *
    + * See also {@link ng.$compileProvider#directive $compileProvider.directive()}. + */ + this.component = function registerComponent(name, options) { + if (!isString(name)) { + forEach(name, reverseParams(bind(this, registerComponent))); + return this; + } + + var controller = options.controller || function() {}; + + function factory($injector) { + function makeInjectable(fn) { + if (isFunction(fn) || isArray(fn)) { + return /** @this */ function(tElement, tAttrs) { + return $injector.invoke(fn, this, {$element: tElement, $attrs: tAttrs}); + }; + } else { + return fn; + } + } + + var template = (!options.template && !options.templateUrl ? '' : options.template); + var ddo = { + controller: controller, + controllerAs: identifierForController(options.controller) || options.controllerAs || '$ctrl', + template: makeInjectable(template), + templateUrl: makeInjectable(options.templateUrl), + transclude: options.transclude, + scope: {}, + bindToController: options.bindings || {}, + restrict: 'E', + require: options.require + }; + + // Copy annotations (starting with $) over to the DDO + forEach(options, function(val, key) { + if (key.charAt(0) === '$') ddo[key] = val; + }); + + return ddo; + } + + // TODO(pete) remove the following `forEach` before we release 1.6.0 + // The component-router@0.2.0 looks for the annotations on the controller constructor + // Nothing in AngularJS looks for annotations on the factory function but we can't remove + // it from 1.5.x yet. + + // Copy any annotation properties (starting with $) over to the factory and controller constructor functions + // These could be used by libraries such as the new component router + forEach(options, function(val, key) { + if (key.charAt(0) === '$') { + factory[key] = val; + // Don't try to copy over annotations to named controller + if (isFunction(controller)) controller[key] = val; + } + }); + + factory.$inject = ['$injector']; + + return this.directive(name, factory); + }; + /** * @ngdoc method * @name $compileProvider#aHrefSanitizationWhitelist - * @function + * @kind function * * @description * Retrieves or overrides the default regular expression that is used for whitelisting of safe * urls during a[href] sanitization. * - * The sanitization is a security measure aimed at prevent XSS attacks via html links. + * The sanitization is a security measure aimed at preventing XSS attacks via html links. * * Any url about to be assigned to a[href] via data-binding is first normalized and turned into * an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist` @@ -5637,7 +8472,7 @@ /** * @ngdoc method * @name $compileProvider#imgSrcSanitizationWhitelist - * @function + * @kind function * * @description * Retrieves or overrides the default regular expression that is used for whitelisting of safe @@ -5663,42 +8498,295 @@ } }; - this.$get = [ - '$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', '$parse', - '$controller', '$rootScope', '$document', '$sce', '$animate', '$$sanitizeUri', - function($injector, $interpolate, $exceptionHandler, $http, $templateCache, $parse, - $controller, $rootScope, $document, $sce, $animate, $$sanitizeUri) { - - var Attributes = function(element, attr) { - this.$$element = element; - this.$attr = attr || {}; - }; + /** + * @ngdoc method + * @name $compileProvider#debugInfoEnabled + * + * @param {boolean=} enabled update the debugInfoEnabled state if provided, otherwise just return the + * current debugInfoEnabled state + * @returns {*} current value if used as getter or itself (chaining) if used as setter + * + * @kind function + * + * @description + * Call this method to enable/disable various debug runtime information in the compiler such as adding + * binding information and a reference to the current scope on to DOM elements. + * If enabled, the compiler will add the following to DOM elements that have been bound to the scope + * * `ng-binding` CSS class + * * `ng-scope` and `ng-isolated-scope` CSS classes + * * `$binding` data property containing an array of the binding expressions + * * Data properties used by the {@link angular.element#methods `scope()`/`isolateScope()` methods} to return + * the element's scope. + * * Placeholder comments will contain information about what directive and binding caused the placeholder. + * E.g. ``. + * + * You may want to disable this in production for a significant performance boost. See + * {@link guide/production#disabling-debug-data Disabling Debug Data} for more. + * + * The default value is true. + */ + var debugInfoEnabled = true; + this.debugInfoEnabled = function(enabled) { + if (isDefined(enabled)) { + debugInfoEnabled = enabled; + return this; + } + return debugInfoEnabled; + }; - Attributes.prototype = { - $normalize: directiveNormalize, + /** + * @ngdoc method + * @name $compileProvider#preAssignBindingsEnabled + * + * @param {boolean=} enabled update the preAssignBindingsEnabled state if provided, otherwise just return the + * current preAssignBindingsEnabled state + * @returns {*} current value if used as getter or itself (chaining) if used as setter + * + * @kind function + * + * @description + * Call this method to enable/disable whether directive controllers are assigned bindings before + * calling the controller's constructor. + * If enabled (true), the compiler assigns the value of each of the bindings to the + * properties of the controller object before the constructor of this object is called. + * + * If disabled (false), the compiler calls the constructor first before assigning bindings. + * + * The default value is false. + * + * @deprecated + * sinceVersion="1.6.0" + * removeVersion="1.7.0" + * + * This method and the option to assign the bindings before calling the controller's constructor + * will be removed in v1.7.0. + */ + var preAssignBindingsEnabled = false; + this.preAssignBindingsEnabled = function(enabled) { + if (isDefined(enabled)) { + preAssignBindingsEnabled = enabled; + return this; + } + return preAssignBindingsEnabled; + }; + /** + * @ngdoc method + * @name $compileProvider#strictComponentBindingsEnabled + * + * @param {boolean=} enabled update the strictComponentBindingsEnabled state if provided, otherwise just return the + * current strictComponentBindingsEnabled state + * @returns {*} current value if used as getter or itself (chaining) if used as setter + * + * @kind function + * + * @description + * Call this method to enable/disable strict component bindings check. If enabled, the compiler will enforce that + * for all bindings of a component that are not set as optional with `?`, an attribute needs to be provided + * on the component's HTML tag. + * + * The default value is false. + */ + var strictComponentBindingsEnabled = false; + this.strictComponentBindingsEnabled = function(enabled) { + if (isDefined(enabled)) { + strictComponentBindingsEnabled = enabled; + return this; + } + return strictComponentBindingsEnabled; + }; - /** - * @ngdoc method - * @name $compile.directive.Attributes#$addClass - * @function - * - * @description - * Adds the CSS class value specified by the classVal parameter to the element. If animations - * are enabled then an animation will be triggered for the class addition. - * - * @param {string} classVal The className value that will be added to the element - */ - $addClass : function(classVal) { - if(classVal && classVal.length > 0) { - $animate.addClass(this.$$element, classVal); + var TTL = 10; + /** + * @ngdoc method + * @name $compileProvider#onChangesTtl + * @description + * + * Sets the number of times `$onChanges` hooks can trigger new changes before giving up and + * assuming that the model is unstable. + * + * The current default is 10 iterations. + * + * In complex applications it's possible that dependencies between `$onChanges` hooks and bindings will result + * in several iterations of calls to these hooks. However if an application needs more than the default 10 + * iterations to stabilize then you should investigate what is causing the model to continuously change during + * the `$onChanges` hook execution. + * + * Increasing the TTL could have performance implications, so you should not change it without proper justification. + * + * @param {number} limit The number of `$onChanges` hook iterations. + * @returns {number|object} the current limit (or `this` if called as a setter for chaining) + */ + this.onChangesTtl = function(value) { + if (arguments.length) { + TTL = value; + return this; + } + return TTL; + }; + + var commentDirectivesEnabledConfig = true; + /** + * @ngdoc method + * @name $compileProvider#commentDirectivesEnabled + * @description + * + * It indicates to the compiler + * whether or not directives on comments should be compiled. + * Defaults to `true`. + * + * Calling this function with false disables the compilation of directives + * on comments for the whole application. + * This results in a compilation performance gain, + * as the compiler doesn't have to check comments when looking for directives. + * This should however only be used if you are sure that no comment directives are used in + * the application (including any 3rd party directives). + * + * @param {boolean} enabled `false` if the compiler may ignore directives on comments + * @returns {boolean|object} the current value (or `this` if called as a setter for chaining) + */ + this.commentDirectivesEnabled = function(value) { + if (arguments.length) { + commentDirectivesEnabledConfig = value; + return this; + } + return commentDirectivesEnabledConfig; + }; + + + var cssClassDirectivesEnabledConfig = true; + /** + * @ngdoc method + * @name $compileProvider#cssClassDirectivesEnabled + * @description + * + * It indicates to the compiler + * whether or not directives on element classes should be compiled. + * Defaults to `true`. + * + * Calling this function with false disables the compilation of directives + * on element classes for the whole application. + * This results in a compilation performance gain, + * as the compiler doesn't have to check element classes when looking for directives. + * This should however only be used if you are sure that no class directives are used in + * the application (including any 3rd party directives). + * + * @param {boolean} enabled `false` if the compiler may ignore directives on element classes + * @returns {boolean|object} the current value (or `this` if called as a setter for chaining) + */ + this.cssClassDirectivesEnabled = function(value) { + if (arguments.length) { + cssClassDirectivesEnabledConfig = value; + return this; + } + return cssClassDirectivesEnabledConfig; + }; + + this.$get = [ + '$injector', '$interpolate', '$exceptionHandler', '$templateRequest', '$parse', + '$controller', '$rootScope', '$sce', '$animate', '$$sanitizeUri', + function($injector, $interpolate, $exceptionHandler, $templateRequest, $parse, + $controller, $rootScope, $sce, $animate, $$sanitizeUri) { + + var SIMPLE_ATTR_NAME = /^\w/; + var specialAttrHolder = window.document.createElement('div'); + + + var commentDirectivesEnabled = commentDirectivesEnabledConfig; + var cssClassDirectivesEnabled = cssClassDirectivesEnabledConfig; + + + var onChangesTtl = TTL; + // The onChanges hooks should all be run together in a single digest + // When changes occur, the call to trigger their hooks will be added to this queue + var onChangesQueue; + + // This function is called in a $$postDigest to trigger all the onChanges hooks in a single digest + function flushOnChangesQueue() { + try { + if (!(--onChangesTtl)) { + // We have hit the TTL limit so reset everything + onChangesQueue = undefined; + throw $compileMinErr('infchng', '{0} $onChanges() iterations reached. Aborting!\n', TTL); + } + // We must run this hook in an apply since the $$postDigest runs outside apply + $rootScope.$apply(function() { + var errors = []; + for (var i = 0, ii = onChangesQueue.length; i < ii; ++i) { + try { + onChangesQueue[i](); + } catch (e) { + errors.push(e); + } + } + // Reset the queue to trigger a new schedule next time there is a change + onChangesQueue = undefined; + if (errors.length) { + throw errors; + } + }); + } finally { + onChangesTtl++; + } + } + + + function Attributes(element, attributesToCopy) { + if (attributesToCopy) { + var keys = Object.keys(attributesToCopy); + var i, l, key; + + for (i = 0, l = keys.length; i < l; i++) { + key = keys[i]; + this[key] = attributesToCopy[key]; + } + } else { + this.$attr = {}; + } + + this.$$element = element; + } + + Attributes.prototype = { + /** + * @ngdoc method + * @name $compile.directive.Attributes#$normalize + * @kind function + * + * @description + * Converts an attribute name (e.g. dash/colon/underscore-delimited string, optionally prefixed with `x-` or + * `data-`) to its normalized, camelCase form. + * + * Also there is special case for Moz prefix starting with upper case letter. + * + * For further information check out the guide on {@link guide/directive#matching-directives Matching Directives} + * + * @param {string} name Name to normalize + */ + $normalize: directiveNormalize, + + + /** + * @ngdoc method + * @name $compile.directive.Attributes#$addClass + * @kind function + * + * @description + * Adds the CSS class value specified by the classVal parameter to the element. If animations + * are enabled then an animation will be triggered for the class addition. + * + * @param {string} classVal The className value that will be added to the element + */ + $addClass: function(classVal) { + if (classVal && classVal.length > 0) { + $animate.addClass(this.$$element, classVal); } }, /** * @ngdoc method * @name $compile.directive.Attributes#$removeClass - * @function + * @kind function * * @description * Removes the CSS class value specified by the classVal parameter from the element. If @@ -5706,8 +8794,8 @@ * * @param {string} classVal The className value that will be removed from the element */ - $removeClass : function(classVal) { - if(classVal && classVal.length > 0) { + $removeClass: function(classVal) { + if (classVal && classVal.length > 0) { $animate.removeClass(this.$$element, classVal); } }, @@ -5715,7 +8803,7 @@ /** * @ngdoc method * @name $compile.directive.Attributes#$updateClass - * @function + * @kind function * * @description * Adds and removes the appropriate CSS class values to the element based on the difference @@ -5724,16 +8812,15 @@ * @param {string} newClasses The current CSS className value * @param {string} oldClasses The former CSS className value */ - $updateClass : function(newClasses, oldClasses) { + $updateClass: function(newClasses, oldClasses) { var toAdd = tokenDifference(newClasses, oldClasses); - var toRemove = tokenDifference(oldClasses, newClasses); + if (toAdd && toAdd.length) { + $animate.addClass(this.$$element, toAdd); + } - if(toAdd.length === 0) { + var toRemove = tokenDifference(oldClasses, newClasses); + if (toRemove && toRemove.length) { $animate.removeClass(this.$$element, toRemove); - } else if(toRemove.length === 0) { - $animate.addClass(this.$$element, toAdd); - } else { - $animate.setClass(this.$$element, toAdd, toRemove); } }, @@ -5751,13 +8838,18 @@ //is set through this function since it may cause $updateClass to //become unstable. - var booleanKey = getBooleanAttrName(this.$$element[0], key), - normalizedVal, + var node = this.$$element[0], + booleanKey = getBooleanAttrName(node, key), + aliasedKey = getAliasedAttrName(key), + observer = key, nodeName; if (booleanKey) { this.$$element.prop(key, value); attrName = booleanKey; + } else if (aliasedKey) { + this[aliasedKey] = value; + observer = aliasedKey; } this[key] = value; @@ -5774,36 +8866,76 @@ nodeName = nodeName_(this.$$element); - // sanitize a[href] and img[src] values - if ((nodeName === 'A' && key === 'href') || - (nodeName === 'IMG' && key === 'src')) { + if ((nodeName === 'a' && (key === 'href' || key === 'xlinkHref')) || + (nodeName === 'img' && key === 'src')) { + // sanitize a[href] and img[src] values this[key] = value = $$sanitizeUri(value, key === 'src'); + } else if (nodeName === 'img' && key === 'srcset' && isDefined(value)) { + // sanitize img[srcset] values + var result = ''; + + // first check if there are spaces because it's not the same pattern + var trimmedSrcset = trim(value); + // ( 999x ,| 999w ,| ,|, ) + var srcPattern = /(\s+\d+x\s*,|\s+\d+w\s*,|\s+,|,\s+)/; + var pattern = /\s/.test(trimmedSrcset) ? srcPattern : /(,)/; + + // split srcset into tuple of uri and descriptor except for the last item + var rawUris = trimmedSrcset.split(pattern); + + // for each tuples + var nbrUrisWith2parts = Math.floor(rawUris.length / 2); + for (var i = 0; i < nbrUrisWith2parts; i++) { + var innerIdx = i * 2; + // sanitize the uri + result += $$sanitizeUri(trim(rawUris[innerIdx]), true); + // add the descriptor + result += (' ' + trim(rawUris[innerIdx + 1])); + } + + // split the last item into uri and descriptor + var lastTuple = trim(rawUris[i * 2]).split(/\s/); + + // sanitize the last uri + result += $$sanitizeUri(trim(lastTuple[0]), true); + + // and add the last descriptor if any + if (lastTuple.length === 2) { + result += (' ' + trim(lastTuple[1])); + } + this[key] = value = result; } if (writeAttr !== false) { - if (value === null || value === undefined) { + if (value === null || isUndefined(value)) { this.$$element.removeAttr(attrName); } else { - this.$$element.attr(attrName, value); + if (SIMPLE_ATTR_NAME.test(attrName)) { + this.$$element.attr(attrName, value); + } else { + setSpecialAttr(this.$$element[0], attrName, value); + } } } // fire observers var $$observers = this.$$observers; - $$observers && forEach($$observers[key], function(fn) { - try { - fn(value); - } catch (e) { - $exceptionHandler(e); - } - }); + if ($$observers) { + forEach($$observers[observer], function(fn) { + try { + fn(value); + } catch (e) { + $exceptionHandler(e); + } + }); + } }, /** * @ngdoc method * @name $compile.directive.Attributes#$observe - * @function + * @kind function * * @description * Observes an interpolated attribute. @@ -5815,34 +8947,95 @@ * @param {string} key Normalized key. (ie ngAttribute) . * @param {function(interpolatedValue)} fn Function that will be called whenever the interpolated value of the attribute changes. - * See the {@link guide/directive#Attributes Directives} guide for more info. - * @returns {function()} the `fn` parameter. + * See the {@link guide/interpolation#how-text-and-attribute-bindings-work Interpolation + * guide} for more info. + * @returns {function()} Returns a deregistration function for this observer. */ $observe: function(key, fn) { var attrs = this, - $$observers = (attrs.$$observers || (attrs.$$observers = {})), + $$observers = (attrs.$$observers || (attrs.$$observers = createMap())), listeners = ($$observers[key] || ($$observers[key] = [])); listeners.push(fn); $rootScope.$evalAsync(function() { - if (!listeners.$$inter) { + if (!listeners.$$inter && attrs.hasOwnProperty(key) && !isUndefined(attrs[key])) { // no one registered attribute interpolation function, so lets call it manually fn(attrs[key]); } }); - return fn; + + return function() { + arrayRemove(listeners, fn); + }; } }; + function setSpecialAttr(element, attrName, value) { + // Attributes names that do not start with letters (such as `(click)`) cannot be set using `setAttribute` + // so we have to jump through some hoops to get such an attribute + // https://github.com/angular/angular.js/pull/13318 + specialAttrHolder.innerHTML = ''; + var attributes = specialAttrHolder.firstChild.attributes; + var attribute = attributes[0]; + // We have to remove the attribute from its container element before we can add it to the destination element + attributes.removeNamedItem(attribute.name); + attribute.value = value; + element.attributes.setNamedItem(attribute); + } + + function safeAddClass($element, className) { + try { + $element.addClass(className); + } catch (e) { + // ignore, since it means that we are trying to set class on + // SVG element, where class name is read-only. + } + } + + var startSymbol = $interpolate.startSymbol(), endSymbol = $interpolate.endSymbol(), - denormalizeTemplate = (startSymbol == '{{' || endSymbol == '}}') + denormalizeTemplate = (startSymbol === '{{' && endSymbol === '}}') ? identity : function denormalizeTemplate(template) { - return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol); - }, + return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol); + }, NG_ATTR_BINDING = /^ngAttr[A-Z]/; + var MULTI_ELEMENT_DIR_RE = /^(.+)Start$/; + + compile.$$addBindingInfo = debugInfoEnabled ? function $$addBindingInfo($element, binding) { + var bindings = $element.data('$binding') || []; + if (isArray(binding)) { + bindings = bindings.concat(binding); + } else { + bindings.push(binding); + } + + $element.data('$binding', bindings); + } : noop; + + compile.$$addBindingClass = debugInfoEnabled ? function $$addBindingClass($element) { + safeAddClass($element, 'ng-binding'); + } : noop; + + compile.$$addScopeInfo = debugInfoEnabled ? function $$addScopeInfo($element, scope, isolated, noTemplate) { + var dataName = isolated ? (noTemplate ? '$isolateScopeNoTemplate' : '$isolateScope') : '$scope'; + $element.data(dataName, scope); + } : noop; + + compile.$$addScopeClass = debugInfoEnabled ? function $$addScopeClass($element, isolated) { + safeAddClass($element, isolated ? 'ng-isolate-scope' : 'ng-scope'); + } : noop; + + compile.$$createComment = function(directiveName, comment) { + var content = ''; + if (debugInfoEnabled) { + content = ' ' + (directiveName || '') + ': '; + if (comment) content += comment + ' '; + } + return window.document.createComment(content); + }; return compile; @@ -5855,50 +9048,84 @@ // modify it. $compileNodes = jqLite($compileNodes); } - // We can not compile top level text elements since text nodes can be merged and we will - // not be able to attach scope data to them, so we will wrap them in - forEach($compileNodes, function(node, index){ - if (node.nodeType == 3 /* text node */ && node.nodeValue.match(/\S+/) /* non-empty */ ) { - $compileNodes[index] = node = jqLite(node).wrap('').parent()[0]; - } - }); var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority, ignoreDirective, previousCompileContext); - safeAddClass($compileNodes, 'ng-scope'); - return function publicLinkFn(scope, cloneConnectFn, transcludeControllers){ + compile.$$addScopeClass($compileNodes); + var namespace = null; + return function publicLinkFn(scope, cloneConnectFn, options) { + if (!$compileNodes) { + throw $compileMinErr('multilink', 'This element has already been linked.'); + } assertArg(scope, 'scope'); - // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart - // and sometimes changes the structure of the DOM. - var $linkNode = cloneConnectFn - ? JQLitePrototype.clone.call($compileNodes) // IMPORTANT!!! - : $compileNodes; - - forEach(transcludeControllers, function(instance, name) { - $linkNode.data('$' + name + 'Controller', instance); - }); - // Attach scope only to non-text nodes. - for(var i = 0, ii = $linkNode.length; i').append($compileNodes).html()) + ); + } else if (cloneConnectFn) { + // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart + // and sometimes changes the structure of the DOM. + $linkNode = JQLitePrototype.clone.call($compileNodes); + } else { + $linkNode = $compileNodes; + } + + if (transcludeControllers) { + for (var controllerName in transcludeControllers) { + $linkNode.data('$' + controllerName + 'Controller', transcludeControllers[controllerName].instance); } } + compile.$$addScopeInfo($linkNode, scope); + if (cloneConnectFn) cloneConnectFn($linkNode, scope); - if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode); + if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode, parentBoundTranscludeFn); + + if (!cloneConnectFn) { + $compileNodes = compositeLinkFn = null; + } return $linkNode; }; } - function safeAddClass($element, className) { - try { - $element.addClass(className); - } catch(e) { - // ignore, since it means that we are trying to set class on - // SVG element, where class name is read-only. + function detectNamespaceForChildElements(parentElement) { + // TODO: Make this detect MathML as well... + var node = parentElement && parentElement[0]; + if (!node) { + return 'html'; + } else { + return nodeName_(node) !== 'foreignobject' && toString.call(node).match(/SVG/) ? 'svg' : 'html'; } } @@ -5920,33 +9147,50 @@ function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, ignoreDirective, previousCompileContext) { var linkFns = [], - attrs, directives, nodeLinkFn, childNodes, childLinkFn, linkFnFound; + // `nodeList` can be either an element's `.childNodes` (live NodeList) + // or a jqLite/jQuery collection or an array + notLiveList = isArray(nodeList) || (nodeList instanceof jqLite), + attrs, directives, nodeLinkFn, childNodes, childLinkFn, linkFnFound, nodeLinkFnFound; + for (var i = 0; i < nodeList.length; i++) { attrs = new Attributes(); - // we must always refer to nodeList[i] since the nodes can be replaced underneath us. + // Support: IE 11 only + // Workaround for #11781 and #14924 + if (msie === 11) { + mergeConsecutiveTextNodes(nodeList, i, notLiveList); + } + + // We must always refer to `nodeList[i]` hereafter, + // since the nodes can be replaced underneath us. directives = collectDirectives(nodeList[i], [], attrs, i === 0 ? maxPriority : undefined, ignoreDirective); nodeLinkFn = (directives.length) ? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement, - null, [], [], previousCompileContext) + null, [], [], previousCompileContext) : null; if (nodeLinkFn && nodeLinkFn.scope) { - safeAddClass(jqLite(nodeList[i]), 'ng-scope'); + compile.$$addScopeClass(attrs.$$element); } childLinkFn = (nodeLinkFn && nodeLinkFn.terminal || - !(childNodes = nodeList[i].childNodes) || - !childNodes.length) + !(childNodes = nodeList[i].childNodes) || + !childNodes.length) ? null : compileNodes(childNodes, - nodeLinkFn ? nodeLinkFn.transclude : transcludeFn); + nodeLinkFn ? ( + (nodeLinkFn.transcludeOnThisElement || !nodeLinkFn.templateOnThisElement) + && nodeLinkFn.transclude) : transcludeFn); + + if (nodeLinkFn || childLinkFn) { + linkFns.push(i, nodeLinkFn, childLinkFn); + linkFnFound = true; + nodeLinkFnFound = nodeLinkFnFound || nodeLinkFn; + } - linkFns.push(nodeLinkFn, childLinkFn); - linkFnFound = linkFnFound || nodeLinkFn || childLinkFn; //use the previous context only for the first element in the virtual group previousCompileContext = null; } @@ -5954,60 +9198,115 @@ // return a linking function if we have found anything, null otherwise return linkFnFound ? compositeLinkFn : null; - function compositeLinkFn(scope, nodeList, $rootElement, boundTranscludeFn) { - var nodeLinkFn, childLinkFn, node, $node, childScope, childTranscludeFn, i, ii, n; + function compositeLinkFn(scope, nodeList, $rootElement, parentBoundTranscludeFn) { + var nodeLinkFn, childLinkFn, node, childScope, i, ii, idx, childBoundTranscludeFn; + var stableNodeList; - // copy nodeList so that linking doesn't break due to live list updates. - var nodeListLength = nodeList.length, + + if (nodeLinkFnFound) { + // copy nodeList so that if a nodeLinkFn removes or adds an element at this DOM level our + // offsets don't get screwed up + var nodeListLength = nodeList.length; stableNodeList = new Array(nodeListLength); - for (i = 0; i < nodeListLength; i++) { - stableNodeList[i] = nodeList[i]; + + // create a sparse array by only copying the elements which have a linkFn + for (i = 0; i < linkFns.length; i += 3) { + idx = linkFns[i]; + stableNodeList[idx] = nodeList[idx]; + } + } else { + stableNodeList = nodeList; } - for(i = 0, n = 0, ii = linkFns.length; i < ii; n++) { - node = stableNodeList[n]; + for (i = 0, ii = linkFns.length; i < ii;) { + node = stableNodeList[linkFns[i++]]; nodeLinkFn = linkFns[i++]; childLinkFn = linkFns[i++]; - $node = jqLite(node); if (nodeLinkFn) { if (nodeLinkFn.scope) { childScope = scope.$new(); - $node.data('$scope', childScope); + compile.$$addScopeInfo(jqLite(node), childScope); } else { childScope = scope; } - childTranscludeFn = nodeLinkFn.transclude; - if (childTranscludeFn || (!boundTranscludeFn && transcludeFn)) { - nodeLinkFn(childLinkFn, childScope, node, $rootElement, - createBoundTranscludeFn(scope, childTranscludeFn || transcludeFn) - ); + + if (nodeLinkFn.transcludeOnThisElement) { + childBoundTranscludeFn = createBoundTranscludeFn( + scope, nodeLinkFn.transclude, parentBoundTranscludeFn); + + } else if (!nodeLinkFn.templateOnThisElement && parentBoundTranscludeFn) { + childBoundTranscludeFn = parentBoundTranscludeFn; + + } else if (!parentBoundTranscludeFn && transcludeFn) { + childBoundTranscludeFn = createBoundTranscludeFn(scope, transcludeFn); + } else { - nodeLinkFn(childLinkFn, childScope, node, $rootElement, boundTranscludeFn); + childBoundTranscludeFn = null; } + + nodeLinkFn(childLinkFn, childScope, node, $rootElement, childBoundTranscludeFn); + } else if (childLinkFn) { - childLinkFn(scope, node.childNodes, undefined, boundTranscludeFn); + childLinkFn(scope, node.childNodes, undefined, parentBoundTranscludeFn); } } } } - function createBoundTranscludeFn(scope, transcludeFn) { - return function boundTranscludeFn(transcludedScope, cloneFn, controllers) { - var scopeCreated = false; + function mergeConsecutiveTextNodes(nodeList, idx, notLiveList) { + var node = nodeList[idx]; + var parent = node.parentNode; + var sibling; + + if (node.nodeType !== NODE_TYPE_TEXT) { + return; + } + + while (true) { + sibling = parent ? node.nextSibling : nodeList[idx + 1]; + if (!sibling || sibling.nodeType !== NODE_TYPE_TEXT) { + break; + } + + node.nodeValue = node.nodeValue + sibling.nodeValue; + + if (sibling.parentNode) { + sibling.parentNode.removeChild(sibling); + } + if (notLiveList && sibling === nodeList[idx + 1]) { + nodeList.splice(idx + 1, 1); + } + } + } + + function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn) { + function boundTranscludeFn(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) { if (!transcludedScope) { - transcludedScope = scope.$new(); + transcludedScope = scope.$new(false, containingScope); transcludedScope.$$transcluded = true; - scopeCreated = true; } - var clone = transcludeFn(transcludedScope, cloneFn, controllers); - if (scopeCreated) { - clone.on('$destroy', bind(transcludedScope, transcludedScope.$destroy)); + return transcludeFn(transcludedScope, cloneFn, { + parentBoundTranscludeFn: previousBoundTranscludeFn, + transcludeControllers: controllers, + futureParentElement: futureParentElement + }); + } + + // We need to attach the transclusion slots onto the `boundTranscludeFn` + // so that they are available inside the `controllersBoundTransclude` function + var boundSlots = boundTranscludeFn.$$slots = createMap(); + for (var slotName in transcludeFn.$$slots) { + if (transcludeFn.$$slots[slotName]) { + boundSlots[slotName] = createBoundTranscludeFn(scope, transcludeFn.$$slots[slotName], previousBoundTranscludeFn); + } else { + boundSlots[slotName] = null; } - return clone; - }; + } + + return boundTranscludeFn; } /** @@ -6024,52 +9323,73 @@ var nodeType = node.nodeType, attrsMap = attrs.$attr, match, + nodeName, className; - switch(nodeType) { - case 1: /* Element */ + switch (nodeType) { + case NODE_TYPE_ELEMENT: /* Element */ + + nodeName = nodeName_(node); + // use the node name: addDirective(directives, - directiveNormalize(nodeName_(node).toLowerCase()), 'E', maxPriority, ignoreDirective); + directiveNormalize(nodeName), 'E', maxPriority, ignoreDirective); // iterate over the attributes - for (var attr, name, nName, ngAttrName, value, nAttrs = node.attributes, + for (var attr, name, nName, ngAttrName, value, isNgAttr, nAttrs = node.attributes, j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) { var attrStartName = false; var attrEndName = false; attr = nAttrs[j]; - if (!msie || msie >= 8 || attr.specified) { - name = attr.name; - // support ngAttr attribute binding - ngAttrName = directiveNormalize(name); - if (NG_ATTR_BINDING.test(ngAttrName)) { - name = snake_case(ngAttrName.substr(6), '-'); - } + name = attr.name; + value = attr.value; + + // support ngAttr attribute binding + ngAttrName = directiveNormalize(name); + isNgAttr = NG_ATTR_BINDING.test(ngAttrName); + if (isNgAttr) { + name = name.replace(PREFIX_REGEXP, '') + .substr(8).replace(/_(.)/g, function(match, letter) { + return letter.toUpperCase(); + }); + } - var directiveNName = ngAttrName.replace(/(Start|End)$/, ''); - if (ngAttrName === directiveNName + 'Start') { - attrStartName = name; - attrEndName = name.substr(0, name.length - 5) + 'end'; - name = name.substr(0, name.length - 6); - } + var multiElementMatch = ngAttrName.match(MULTI_ELEMENT_DIR_RE); + if (multiElementMatch && directiveIsMultiElement(multiElementMatch[1])) { + attrStartName = name; + attrEndName = name.substr(0, name.length - 5) + 'end'; + name = name.substr(0, name.length - 6); + } - nName = directiveNormalize(name.toLowerCase()); - attrsMap[nName] = name; - attrs[nName] = value = trim(attr.value); + nName = directiveNormalize(name.toLowerCase()); + attrsMap[nName] = name; + if (isNgAttr || !attrs.hasOwnProperty(nName)) { + attrs[nName] = value; if (getBooleanAttrName(node, nName)) { attrs[nName] = true; // presence means true } - addAttrInterpolateDirective(node, directives, value, nName); - addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName, - attrEndName); } + addAttrInterpolateDirective(node, directives, value, nName, isNgAttr); + addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName, + attrEndName); + } + + if (nodeName === 'input' && node.getAttribute('type') === 'hidden') { + // Hidden input elements can have strange behaviour when navigating back to the page + // This tells the browser not to try to cache and reinstate previous values + node.setAttribute('autocomplete', 'off'); } // use class as directive + if (!cssClassDirectivesEnabled) break; className = node.className; + if (isObject(className)) { + // Maybe SVGAnimatedString + className = className.animVal; + } if (isString(className) && className !== '') { - while (match = CLASS_DIRECTIVE_REGEXP.exec(className)) { + while ((match = CLASS_DIRECTIVE_REGEXP.exec(className))) { nName = directiveNormalize(match[2]); if (addDirective(directives, nName, 'C', maxPriority, ignoreDirective)) { attrs[nName] = trim(match[3]); @@ -6078,23 +9398,12 @@ } } break; - case 3: /* Text Node */ + case NODE_TYPE_TEXT: /* Text Node */ addTextInterpolateDirective(directives, node.nodeValue); break; - case 8: /* Comment */ - try { - match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue); - if (match) { - nName = directiveNormalize(match[1]); - if (addDirective(directives, nName, 'M', maxPriority, ignoreDirective)) { - attrs[nName] = trim(match[2]); - } - } - } catch (e) { - // turns out that under some circumstances IE9 throws errors when one attempts to read - // comment's node value. - // Just ignore it and continue. (Can't seem to reproduce in test case.) - } + case NODE_TYPE_COMMENT: /* Comment */ + if (!commentDirectivesEnabled) break; + collectCommentDirectives(node, directives, attrs, maxPriority, ignoreDirective); break; } @@ -6102,8 +9411,26 @@ return directives; } + function collectCommentDirectives(node, directives, attrs, maxPriority, ignoreDirective) { + // function created because of performance, try/catch disables + // the optimization of the whole function #14848 + try { + var match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue); + if (match) { + var nName = directiveNormalize(match[1]); + if (addDirective(directives, nName, 'M', maxPriority, ignoreDirective)) { + attrs[nName] = trim(match[2]); + } + } + } catch (e) { + // turns out that under some circumstances IE9 throws errors when one attempts to read + // comment's node value. + // Just ignore it and continue. (Can't seem to reproduce in test case.) + } + } + /** - * Given a node with an directive-start it collects all of the siblings until it finds + * Given a node with a directive-start it collects all of the siblings until it finds * directive-end. * @param node * @param attrStart @@ -6114,14 +9441,13 @@ var nodes = []; var depth = 0; if (attrStart && node.hasAttribute && node.hasAttribute(attrStart)) { - var startNode = node; do { if (!node) { throw $compileMinErr('uterdir', - "Unterminated attribute, found '{0}' but no matching '{1}' found.", + 'Unterminated attribute, found \'{0}\' but no matching \'{1}\' found.', attrStart, attrEnd); } - if (node.nodeType == 1 /** Element **/) { + if (node.nodeType === NODE_TYPE_ELEMENT) { if (node.hasAttribute(attrStart)) depth++; if (node.hasAttribute(attrEnd)) depth--; } @@ -6144,12 +9470,41 @@ * @returns {Function} */ function groupElementsLinkFnWrapper(linkFn, attrStart, attrEnd) { - return function(scope, element, attrs, controllers, transcludeFn) { + return function groupedElementsLink(scope, element, attrs, controllers, transcludeFn) { element = groupScan(element[0], attrStart, attrEnd); return linkFn(scope, element, attrs, controllers, transcludeFn); }; } + /** + * A function generator that is used to support both eager and lazy compilation + * linking function. + * @param eager + * @param $compileNodes + * @param transcludeFn + * @param maxPriority + * @param ignoreDirective + * @param previousCompileContext + * @returns {Function} + */ + function compilationGenerator(eager, $compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext) { + var compiled; + + if (eager) { + return compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext); + } + return /** @this */ function lazyCompilation() { + if (!compiled) { + compiled = compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext); + + // Null out all of these references in order to make them eligible for garbage collection + // since this is a potentially long lived closure + $compileNodes = transcludeFn = previousCompileContext = null; + } + return compiled.apply(this, arguments); + }; + } + /** * Once the directives have been collected, their compile functions are executed. This method * is responsible for inlining directive templates as well as terminating the application @@ -6179,12 +9534,13 @@ previousCompileContext = previousCompileContext || {}; var terminalPriority = -Number.MAX_VALUE, - newScopeDirective, + newScopeDirective = previousCompileContext.newScopeDirective, controllerDirectives = previousCompileContext.controllerDirectives, newIsolateScopeDirective = previousCompileContext.newIsolateScopeDirective, templateDirective = previousCompileContext.templateDirective, nonTlbTranscludeDirective = previousCompileContext.nonTlbTranscludeDirective, hasTranscludeDirective = false, + hasTemplate = false, hasElementTranscludeDirective = previousCompileContext.hasElementTranscludeDirective, $compileNode = templateAttrs.$$element = jqLite(compileNode), directive, @@ -6193,10 +9549,12 @@ replaceDirective = originalReplaceDirective, childTranscludeFn = transcludeFn, linkFn, + didScanForMultipleTransclusion = false, + mightHaveMultipleTransclusionError = false, directiveValue; // executes all directives on the current element - for(var i = 0, ii = directives.length; i < ii; i++) { + for (var i = 0, ii = directives.length; i < ii; i++) { directive = directives[i]; var attrStart = directive.$$start; var attrEnd = directive.$$end; @@ -6211,31 +9569,63 @@ break; // prevent further processing of directives } - if (directiveValue = directive.scope) { - newScopeDirective = newScopeDirective || directive; + directiveValue = directive.scope; + + if (directiveValue) { // skip the check for directives with async templates, we'll check the derived sync // directive when the template arrives if (!directive.templateUrl) { - assertNoDuplicate('new/isolated scope', newIsolateScopeDirective, directive, - $compileNode); if (isObject(directiveValue)) { + // This directive is trying to add an isolated scope. + // Check that there is no scope of any kind already + assertNoDuplicate('new/isolated scope', newIsolateScopeDirective || newScopeDirective, + directive, $compileNode); newIsolateScopeDirective = directive; + } else { + // This directive is trying to add a child scope. + // Check that there is no isolated scope already + assertNoDuplicate('new/isolated scope', newIsolateScopeDirective, directive, + $compileNode); } } + + newScopeDirective = newScopeDirective || directive; } directiveName = directive.name; + // If we encounter a condition that can result in transclusion on the directive, + // then scan ahead in the remaining directives for others that may cause a multiple + // transclusion error to be thrown during the compilation process. If a matching directive + // is found, then we know that when we encounter a transcluded directive, we need to eagerly + // compile the `transclude` function rather than doing it lazily in order to throw + // exceptions at the correct time + if (!didScanForMultipleTransclusion && ((directive.replace && (directive.templateUrl || directive.template)) + || (directive.transclude && !directive.$$tlb))) { + var candidateDirective; + + for (var scanningIndex = i + 1; (candidateDirective = directives[scanningIndex++]);) { + if ((candidateDirective.transclude && !candidateDirective.$$tlb) + || (candidateDirective.replace && (candidateDirective.templateUrl || candidateDirective.template))) { + mightHaveMultipleTransclusionError = true; + break; + } + } + + didScanForMultipleTransclusion = true; + } + if (!directive.templateUrl && directive.controller) { - directiveValue = directive.controller; - controllerDirectives = controllerDirectives || {}; - assertNoDuplicate("'" + directiveName + "' controller", + controllerDirectives = controllerDirectives || createMap(); + assertNoDuplicate('\'' + directiveName + '\' controller', controllerDirectives[directiveName], directive, $compileNode); controllerDirectives[directiveName] = directive; } - if (directiveValue = directive.transclude) { + directiveValue = directive.transclude; + + if (directiveValue) { hasTranscludeDirective = true; // Special case ngIf and ngRepeat so that we don't complain about duplicate transclusion. @@ -6246,17 +9636,27 @@ nonTlbTranscludeDirective = directive; } - if (directiveValue == 'element') { + if (directiveValue === 'element') { hasElementTranscludeDirective = true; terminalPriority = directive.priority; - $template = groupScan(compileNode, attrStart, attrEnd); + $template = $compileNode; $compileNode = templateAttrs.$$element = - jqLite(document.createComment(' ' + directiveName + ': ' + - templateAttrs[directiveName] + ' ')); + jqLite(compile.$$createComment(directiveName, templateAttrs[directiveName])); compileNode = $compileNode[0]; - replaceWith(jqCollection, jqLite(sliceArgs($template)), compileNode); + replaceWith(jqCollection, sliceArgs($template), compileNode); - childTranscludeFn = compile($template, transcludeFn, terminalPriority, + // Support: Chrome < 50 + // https://github.com/angular/angular.js/issues/14041 + + // In the versions of V8 prior to Chrome 50, the document fragment that is created + // in the `replaceWith` function is improperly garbage collected despite still + // being referenced by the `parentNode` property of all of the child nodes. By adding + // a reference to the fragment via a different property, we can avoid that incorrect + // behavior. + // TODO: remove this line after Chrome 50 has been released + $template[0].$$parentNode = $template[0].parentNode; + + childTranscludeFn = compilationGenerator(mightHaveMultipleTransclusionError, $template, transcludeFn, terminalPriority, replaceDirective && replaceDirective.name, { // Don't pass in: // - controllerDirectives - otherwise we'll create duplicates controllers @@ -6268,19 +9668,80 @@ nonTlbTranscludeDirective: nonTlbTranscludeDirective }); } else { - $template = jqLite(jqLiteClone(compileNode)).contents(); - $compileNode.empty(); // clear contents - childTranscludeFn = compile($template, transcludeFn); - } - } - if (directive.template) { - assertNoDuplicate('template', templateDirective, directive, $compileNode); - templateDirective = directive; + var slots = createMap(); - directiveValue = (isFunction(directive.template)) - ? directive.template($compileNode, templateAttrs) - : directive.template; + if (!isObject(directiveValue)) { + $template = jqLite(jqLiteClone(compileNode)).contents(); + } else { + + // We have transclusion slots, + // collect them up, compile them and store their transclusion functions + $template = []; + + var slotMap = createMap(); + var filledSlots = createMap(); + + // Parse the element selectors + forEach(directiveValue, function(elementSelector, slotName) { + // If an element selector starts with a ? then it is optional + var optional = (elementSelector.charAt(0) === '?'); + elementSelector = optional ? elementSelector.substring(1) : elementSelector; + + slotMap[elementSelector] = slotName; + + // We explicitly assign `null` since this implies that a slot was defined but not filled. + // Later when calling boundTransclusion functions with a slot name we only error if the + // slot is `undefined` + slots[slotName] = null; + + // filledSlots contains `true` for all slots that are either optional or have been + // filled. This is used to check that we have not missed any required slots + filledSlots[slotName] = optional; + }); + + // Add the matching elements into their slot + forEach($compileNode.contents(), function(node) { + var slotName = slotMap[directiveNormalize(nodeName_(node))]; + if (slotName) { + filledSlots[slotName] = true; + slots[slotName] = slots[slotName] || []; + slots[slotName].push(node); + } else { + $template.push(node); + } + }); + + // Check for required slots that were not filled + forEach(filledSlots, function(filled, slotName) { + if (!filled) { + throw $compileMinErr('reqslot', 'Required transclusion slot `{0}` was not filled.', slotName); + } + }); + + for (var slotName in slots) { + if (slots[slotName]) { + // Only define a transclusion function if the slot was filled + slots[slotName] = compilationGenerator(mightHaveMultipleTransclusionError, slots[slotName], transcludeFn); + } + } + } + + $compileNode.empty(); // clear contents + childTranscludeFn = compilationGenerator(mightHaveMultipleTransclusionError, $template, transcludeFn, undefined, + undefined, { needsNewScope: directive.$$isolateScope || directive.$$newScope}); + childTranscludeFn.$$slots = slots; + } + } + + if (directive.template) { + hasTemplate = true; + assertNoDuplicate('template', templateDirective, directive, $compileNode); + templateDirective = directive; + + directiveValue = (isFunction(directive.template)) + ? directive.template($compileNode, templateAttrs) + : directive.template; directiveValue = denormalizeTemplate(directiveValue); @@ -6289,13 +9750,13 @@ if (jqLiteIsTextNode(directiveValue)) { $template = []; } else { - $template = jqLite(directiveValue); + $template = removeComments(wrapTemplate(directive.templateNamespace, trim(directiveValue))); } compileNode = $template[0]; - if ($template.length != 1 || compileNode.nodeType !== 1) { + if ($template.length !== 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) { throw $compileMinErr('tplrt', - "Template for directive '{0}' must have exactly one root element. {1}", + 'Template for directive \'{0}\' must have exactly one root element. {1}', directiveName, ''); } @@ -6311,8 +9772,11 @@ var templateDirectives = collectDirectives(compileNode, [], newTemplateAttrs); var unprocessedDirectives = directives.splice(i + 1, directives.length - (i + 1)); - if (newIsolateScopeDirective) { - markDirectivesAsIsolate(templateDirectives); + if (newIsolateScopeDirective || newScopeDirective) { + // The original directive caused the current element to be replaced but this element + // also needs to have a new scope, so we need to tell the template directives + // that they would need to get their scope from further up, if they require transclusion + markDirectiveScope(templateDirectives, newIsolateScopeDirective, newScopeDirective); } directives = directives.concat(templateDirectives).concat(unprocessedDirectives); mergeTemplateAttributes(templateAttrs, newTemplateAttrs); @@ -6324,6 +9788,7 @@ } if (directive.templateUrl) { + hasTemplate = true; assertNoDuplicate('template', templateDirective, directive, $compileNode); templateDirective = directive; @@ -6331,9 +9796,11 @@ replaceDirective = directive; } + // eslint-disable-next-line no-func-assign nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode, - templateAttrs, jqCollection, childTranscludeFn, preLinkFns, postLinkFns, { + templateAttrs, jqCollection, hasTranscludeDirective && childTranscludeFn, preLinkFns, postLinkFns, { controllerDirectives: controllerDirectives, + newScopeDirective: (newScopeDirective !== directive) && newScopeDirective, newIsolateScopeDirective: newIsolateScopeDirective, templateDirective: templateDirective, nonTlbTranscludeDirective: nonTlbTranscludeDirective @@ -6342,10 +9809,11 @@ } else if (directive.compile) { try { linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn); + var context = directive.$$originalDirective || directive; if (isFunction(linkFn)) { - addLinkFns(null, linkFn, attrStart, attrEnd); + addLinkFns(null, bind(context, linkFn), attrStart, attrEnd); } else if (linkFn) { - addLinkFns(linkFn.pre, linkFn.post, attrStart, attrEnd); + addLinkFns(bind(context, linkFn.pre), bind(context, linkFn.post), attrStart, attrEnd); } } catch (e) { $exceptionHandler(e, startingTag($compileNode)); @@ -6360,7 +9828,10 @@ } nodeLinkFn.scope = newScopeDirective && newScopeDirective.scope === true; - nodeLinkFn.transclude = hasTranscludeDirective && childTranscludeFn; + nodeLinkFn.transcludeOnThisElement = hasTranscludeDirective; + nodeLinkFn.templateOnThisElement = hasTemplate; + nodeLinkFn.transclude = childTranscludeFn; + previousCompileContext.hasElementTranscludeDirective = hasElementTranscludeDirective; // might be normal or delayed nodeLinkFn depending on if templateUrl is present @@ -6372,6 +9843,7 @@ if (pre) { if (attrStart) pre = groupElementsLinkFnWrapper(pre, attrStart, attrEnd); pre.require = directive.require; + pre.directiveName = directiveName; if (newIsolateScopeDirective === directive || directive.$$isolateScope) { pre = cloneAndAnnotateFn(pre, {isolateScope: true}); } @@ -6380,6 +9852,7 @@ if (post) { if (attrStart) post = groupElementsLinkFnWrapper(post, attrStart, attrEnd); post.require = directive.require; + post.directiveName = directiveName; if (newIsolateScopeDirective === directive || directive.$$isolateScope) { post = cloneAndAnnotateFn(post, {isolateScope: true}); } @@ -6387,180 +9860,135 @@ } } - - function getControllers(require, $element, elementControllers) { - var value, retrievalMethod = 'data', optional = false; - if (isString(require)) { - while((value = require.charAt(0)) == '^' || value == '?') { - require = require.substr(1); - if (value == '^') { - retrievalMethod = 'inheritedData'; - } - optional = optional || value == '?'; - } - value = null; - - if (elementControllers && retrievalMethod === 'data') { - value = elementControllers[require]; - } - value = value || $element[retrievalMethod]('$' + require + 'Controller'); - - if (!value && !optional) { - throw $compileMinErr('ctreq', - "Controller '{0}', required by directive '{1}', can't be found!", - require, directiveName); - } - return value; - } else if (isArray(require)) { - value = []; - forEach(require, function(require) { - value.push(getControllers(require, $element, elementControllers)); - }); - } - return value; - } - - function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) { - var attrs, $element, i, ii, linkFn, controller, isolateScope, elementControllers = {}, transcludeFn; + var i, ii, linkFn, isolateScope, controllerScope, elementControllers, transcludeFn, $element, + attrs, scopeBindingInfo; if (compileNode === linkNode) { attrs = templateAttrs; + $element = templateAttrs.$$element; } else { - attrs = shallowCopy(templateAttrs, new Attributes(jqLite(linkNode), templateAttrs.$attr)); + $element = jqLite(linkNode); + attrs = new Attributes($element, templateAttrs); } - $element = attrs.$$element; + controllerScope = scope; if (newIsolateScopeDirective) { - var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/; - var $linkNode = jqLite(linkNode); - isolateScope = scope.$new(true); + } else if (newScopeDirective) { + controllerScope = scope.$parent; + } - if (templateDirective && (templateDirective === newIsolateScopeDirective.$$originalDirective)) { - $linkNode.data('$isolateScope', isolateScope) ; - } else { - $linkNode.data('$isolateScopeNoTemplate', isolateScope); - } - - - - safeAddClass($linkNode, 'ng-isolate-scope'); + if (boundTranscludeFn) { + // track `boundTranscludeFn` so it can be unwrapped if `transcludeFn` + // is later passed as `parentBoundTranscludeFn` to `publicLinkFn` + transcludeFn = controllersBoundTransclude; + transcludeFn.$$boundTransclude = boundTranscludeFn; + // expose the slots on the `$transclude` function + transcludeFn.isSlotFilled = function(slotName) { + return !!boundTranscludeFn.$$slots[slotName]; + }; + } - forEach(newIsolateScopeDirective.scope, function(definition, scopeName) { - var match = definition.match(LOCAL_REGEXP) || [], - attrName = match[3] || scopeName, - optional = (match[2] == '?'), - mode = match[1], // @, =, or & - lastValue, - parentGet, parentSet, compare; + if (controllerDirectives) { + elementControllers = setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope, newIsolateScopeDirective); + } - isolateScope.$$isolateBindings[scopeName] = mode + attrName; + if (newIsolateScopeDirective) { + // Initialize isolate scope bindings for new isolate scope directive. + compile.$$addScopeInfo($element, isolateScope, true, !(templateDirective && (templateDirective === newIsolateScopeDirective || + templateDirective === newIsolateScopeDirective.$$originalDirective))); + compile.$$addScopeClass($element, true); + isolateScope.$$isolateBindings = + newIsolateScopeDirective.$$isolateBindings; + scopeBindingInfo = initializeDirectiveBindings(scope, attrs, isolateScope, + isolateScope.$$isolateBindings, + newIsolateScopeDirective); + if (scopeBindingInfo.removeWatches) { + isolateScope.$on('$destroy', scopeBindingInfo.removeWatches); + } + } - switch (mode) { + // Initialize bindToController bindings + for (var name in elementControllers) { + var controllerDirective = controllerDirectives[name]; + var controller = elementControllers[name]; + var bindings = controllerDirective.$$bindings.bindToController; - case '@': - attrs.$observe(attrName, function(value) { - isolateScope[scopeName] = value; - }); - attrs.$$observers[attrName].$$scope = scope; - if( attrs[attrName] ) { - // If the attribute has been provided then we trigger an interpolation to ensure - // the value is there for use in the link fn - isolateScope[scopeName] = $interpolate(attrs[attrName])(scope); - } - break; + if (preAssignBindingsEnabled) { + if (bindings) { + controller.bindingInfo = + initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); + } else { + controller.bindingInfo = {}; + } - case '=': - if (optional && !attrs[attrName]) { - return; - } - parentGet = $parse(attrs[attrName]); - if (parentGet.literal) { - compare = equals; - } else { - compare = function(a,b) { return a === b; }; - } - parentSet = parentGet.assign || function() { - // reset the change, or we will throw this exception on every $digest - lastValue = isolateScope[scopeName] = parentGet(scope); - throw $compileMinErr('nonassign', - "Expression '{0}' used with directive '{1}' is non-assignable!", - attrs[attrName], newIsolateScopeDirective.name); - }; - lastValue = isolateScope[scopeName] = parentGet(scope); - isolateScope.$watch(function parentValueWatch() { - var parentValue = parentGet(scope); - if (!compare(parentValue, isolateScope[scopeName])) { - // we are out of sync and need to copy - if (!compare(parentValue, lastValue)) { - // parent changed and it has precedence - isolateScope[scopeName] = parentValue; - } else { - // if the parent can be assigned then do so - parentSet(scope, parentValue = isolateScope[scopeName]); - } - } - return lastValue = parentValue; - }, null, parentGet.literal); - break; - - case '&': - parentGet = $parse(attrs[attrName]); - isolateScope[scopeName] = function(locals) { - return parentGet(scope, locals); - }; - break; - - default: - throw $compileMinErr('iscp', - "Invalid isolate scope definition for directive '{0}'." + - " Definition: {... {1}: '{2}' ...}", - newIsolateScopeDirective.name, scopeName, definition); + var controllerResult = controller(); + if (controllerResult !== controller.instance) { + // If the controller constructor has a return value, overwrite the instance + // from setupControllers + controller.instance = controllerResult; + $element.data('$' + controllerDirective.name + 'Controller', controllerResult); + if (controller.bindingInfo.removeWatches) { + controller.bindingInfo.removeWatches(); + } + controller.bindingInfo = + initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); } - }); + } else { + controller.instance = controller(); + $element.data('$' + controllerDirective.name + 'Controller', controller.instance); + controller.bindingInfo = + initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); + } } - transcludeFn = boundTranscludeFn && controllersBoundTransclude; - if (controllerDirectives) { - forEach(controllerDirectives, function(directive) { - var locals = { - $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope, - $element: $element, - $attrs: attrs, - $transclude: transcludeFn - }, controllerInstance; - - controller = directive.controller; - if (controller == '@') { - controller = attrs[directive.name]; - } - controllerInstance = $controller(controller, locals); - // For directives with element transclusion the element is a comment, - // but jQuery .data doesn't support attaching data to comment nodes as it's hard to - // clean up (http://bugs.jquery.com/ticket/8335). - // Instead, we save the controllers for the element in a local hash and attach to .data - // later, once we have the actual element. - elementControllers[directive.name] = controllerInstance; - if (!hasElementTranscludeDirective) { - $element.data('$' + directive.name + 'Controller', controllerInstance); - } + // Bind the required controllers to the controller, if `require` is an object and `bindToController` is truthy + forEach(controllerDirectives, function(controllerDirective, name) { + var require = controllerDirective.require; + if (controllerDirective.bindToController && !isArray(require) && isObject(require)) { + extend(elementControllers[name].instance, getControllers(name, require, $element, elementControllers)); + } + }); - if (directive.controllerAs) { - locals.$scope[directive.controllerAs] = controllerInstance; + // Handle the init and destroy lifecycle hooks on all controllers that have them + forEach(elementControllers, function(controller) { + var controllerInstance = controller.instance; + if (isFunction(controllerInstance.$onChanges)) { + try { + controllerInstance.$onChanges(controller.bindingInfo.initialChanges); + } catch (e) { + $exceptionHandler(e); } - }); - } + } + if (isFunction(controllerInstance.$onInit)) { + try { + controllerInstance.$onInit(); + } catch (e) { + $exceptionHandler(e); + } + } + if (isFunction(controllerInstance.$doCheck)) { + controllerScope.$watch(function() { controllerInstance.$doCheck(); }); + controllerInstance.$doCheck(); + } + if (isFunction(controllerInstance.$onDestroy)) { + controllerScope.$on('$destroy', function callOnDestroyHook() { + controllerInstance.$onDestroy(); + }); + } + }); // PRELINKING - for(i = 0, ii = preLinkFns.length; i < ii; i++) { - try { - linkFn = preLinkFns[i]; - linkFn(linkFn.isolateScope ? isolateScope : scope, $element, attrs, - linkFn.require && getControllers(linkFn.require, $element, elementControllers), transcludeFn); - } catch (e) { - $exceptionHandler(e, startingTag($element)); - } + for (i = 0, ii = preLinkFns.length; i < ii; i++) { + linkFn = preLinkFns[i]; + invokeLinkFn(linkFn, + linkFn.isolateScope ? isolateScope : scope, + $element, + attrs, + linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers), + transcludeFn + ); } // RECURSION @@ -6570,25 +9998,38 @@ if (newIsolateScopeDirective && (newIsolateScopeDirective.template || newIsolateScopeDirective.templateUrl === null)) { scopeToChild = isolateScope; } - childLinkFn && childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn); + if (childLinkFn) { + childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn); + } // POSTLINKING - for(i = postLinkFns.length - 1; i >= 0; i--) { - try { - linkFn = postLinkFns[i]; - linkFn(linkFn.isolateScope ? isolateScope : scope, $element, attrs, - linkFn.require && getControllers(linkFn.require, $element, elementControllers), transcludeFn); - } catch (e) { - $exceptionHandler(e, startingTag($element)); - } + for (i = postLinkFns.length - 1; i >= 0; i--) { + linkFn = postLinkFns[i]; + invokeLinkFn(linkFn, + linkFn.isolateScope ? isolateScope : scope, + $element, + attrs, + linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers), + transcludeFn + ); } + // Trigger $postLink lifecycle hooks + forEach(elementControllers, function(controller) { + var controllerInstance = controller.instance; + if (isFunction(controllerInstance.$postLink)) { + controllerInstance.$postLink(); + } + }); + // This is the function that is injected as `$transclude`. - function controllersBoundTransclude(scope, cloneAttachFn) { + // Note: all arguments are optional! + function controllersBoundTransclude(scope, cloneAttachFn, futureParentElement, slotName) { var transcludeControllers; - - // no scope passed - if (arguments.length < 2) { + // No scope passed in: + if (!isScope(scope)) { + slotName = futureParentElement; + futureParentElement = cloneAttachFn; cloneAttachFn = scope; scope = undefined; } @@ -6596,16 +10037,111 @@ if (hasElementTranscludeDirective) { transcludeControllers = elementControllers; } + if (!futureParentElement) { + futureParentElement = hasElementTranscludeDirective ? $element.parent() : $element; + } + if (slotName) { + // slotTranscludeFn can be one of three things: + // * a transclude function - a filled slot + // * `null` - an optional slot that was not filled + // * `undefined` - a slot that was not declared (i.e. invalid) + var slotTranscludeFn = boundTranscludeFn.$$slots[slotName]; + if (slotTranscludeFn) { + return slotTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild); + } else if (isUndefined(slotTranscludeFn)) { + throw $compileMinErr('noslot', + 'No parent directive that requires a transclusion with slot name "{0}". ' + + 'Element: {1}', + slotName, startingTag($element)); + } + } else { + return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild); + } + } + } + } + + function getControllers(directiveName, require, $element, elementControllers) { + var value; - return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers); + if (isString(require)) { + var match = require.match(REQUIRE_PREFIX_REGEXP); + var name = require.substring(match[0].length); + var inheritType = match[1] || match[3]; + var optional = match[2] === '?'; + + //If only parents then start at the parent element + if (inheritType === '^^') { + $element = $element.parent(); + //Otherwise attempt getting the controller from elementControllers in case + //the element is transcluded (and has no data) and to avoid .data if possible + } else { + value = elementControllers && elementControllers[name]; + value = value && value.instance; + } + + if (!value) { + var dataName = '$' + name + 'Controller'; + value = inheritType ? $element.inheritedData(dataName) : $element.data(dataName); + } + + if (!value && !optional) { + throw $compileMinErr('ctreq', + 'Controller \'{0}\', required by directive \'{1}\', can\'t be found!', + name, directiveName); + } + } else if (isArray(require)) { + value = []; + for (var i = 0, ii = require.length; i < ii; i++) { + value[i] = getControllers(directiveName, require[i], $element, elementControllers); + } + } else if (isObject(require)) { + value = {}; + forEach(require, function(controller, property) { + value[property] = getControllers(directiveName, controller, $element, elementControllers); + }); + } + + return value || null; + } + + function setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope, newIsolateScopeDirective) { + var elementControllers = createMap(); + for (var controllerKey in controllerDirectives) { + var directive = controllerDirectives[controllerKey]; + var locals = { + $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope, + $element: $element, + $attrs: attrs, + $transclude: transcludeFn + }; + + var controller = directive.controller; + if (controller === '@') { + controller = attrs[directive.name]; } + + var controllerInstance = $controller(controller, locals, true, directive.controllerAs); + + // For directives with element transclusion the element is a comment. + // In this case .data will not attach any data. + // Instead, we save the controllers for the element in a local hash and attach to .data + // later, once we have the actual element. + elementControllers[directive.name] = controllerInstance; + $element.data('$' + directive.name + 'Controller', controllerInstance.instance); } + return elementControllers; } - function markDirectivesAsIsolate(directives) { - // mark all directives as needing isolate scope. + // Depending upon the context in which a directive finds itself it might need to have a new isolated + // or child scope created. For instance: + // * if the directive has been pulled into a template because another directive with a higher priority + // asked for element transclusion + // * if the directive itself asks for transclusion but it is at the root of a template and the original + // element was replaced. See https://github.com/angular/angular.js/issues/12936 + function markDirectiveScope(directives, isolateScope, newScope) { for (var j = 0, jj = directives.length; j < jj; j++) { - directives[j] = inherit(directives[j], {$$isolateScope: true}); + directives[j] = inherit(directives[j], {$$isolateScope: isolateScope, $$newScope: newScope}); } } @@ -6628,25 +10164,51 @@ if (name === ignoreDirective) return null; var match = null; if (hasDirectives.hasOwnProperty(name)) { - for(var directive, directives = $injector.get(name + Suffix), - i = 0, ii = directives.length; i directive.priority) && - directive.restrict.indexOf(location) != -1) { - if (startAttrName) { - directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName}); + for (var directive, directives = $injector.get(name + Suffix), + i = 0, ii = directives.length; i < ii; i++) { + directive = directives[i]; + if ((isUndefined(maxPriority) || maxPriority > directive.priority) && + directive.restrict.indexOf(location) !== -1) { + if (startAttrName) { + directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName}); + } + if (!directive.$$bindings) { + var bindings = directive.$$bindings = + parseDirectiveBindings(directive, directive.name); + if (isObject(bindings.isolateScope)) { + directive.$$isolateBindings = bindings.isolateScope; } - tDirectives.push(directive); - match = directive; } - } catch(e) { $exceptionHandler(e); } + tDirectives.push(directive); + match = directive; + } } } return match; } + /** + * looks up the directive and returns true if it is a multi-element directive, + * and therefore requires DOM nodes between -start and -end markers to be grouped + * together. + * + * @param {string} name name of the directive to look up. + * @returns true if directive was registered as multi-element. + */ + function directiveIsMultiElement(name) { + if (hasDirectives.hasOwnProperty(name)) { + for (var directive, directives = $injector.get(name + Suffix), + i = 0, ii = directives.length; i < ii; i++) { + directive = directives[i]; + if (directive.multiElement) { + return true; + } + } + } + return false; + } + /** * When the element is replaced with HTML template then the new attributes * on the template need to be merged with the existing attributes in the DOM. @@ -6657,14 +10219,17 @@ */ function mergeTemplateAttributes(dst, src) { var srcAttr = src.$attr, - dstAttr = dst.$attr, - $element = dst.$$element; + dstAttr = dst.$attr; // reapply the old attributes to the new element forEach(dst, function(value, key) { - if (key.charAt(0) != '$') { - if (src[key]) { - value += (key === 'style' ? ';' : ' ') + src[key]; + if (key.charAt(0) !== '$') { + if (src[key] && src[key] !== value) { + if (value.length) { + value += (key === 'style' ? ';' : ' ') + src[key]; + } else { + value = src[key]; + } } dst.$set(key, value, true, srcAttr[key]); } @@ -6672,18 +10237,16 @@ // copy the new attributes on the old attrs object forEach(src, function(value, key) { - if (key == 'class') { - safeAddClass($element, value); - dst['class'] = (dst['class'] ? dst['class'] + ' ' : '') + value; - } else if (key == 'style') { - $element.attr('style', $element.attr('style') + ';' + value); - dst['style'] = (dst['style'] ? dst['style'] + ';' : '') + value; - // `dst` will never contain hasOwnProperty as DOM parser won't let it. - // You will get an "InvalidCharacterError: DOM Exception 5" error if you - // have an attribute like "has-own-property" or "data-has-own-property", etc. - } else if (key.charAt(0) != '$' && !dst.hasOwnProperty(key)) { + // Check if we already set this attribute in the loop above. + // `dst` will never contain hasOwnProperty as DOM parser won't let it. + // You will get an "InvalidCharacterError: DOM Exception 5" error if you + // have an attribute like "has-own-property" or "data-has-own-property", etc. + if (!dst.hasOwnProperty(key) && key.charAt(0) !== '$') { dst[key] = value; - dstAttr[key] = srcAttr[key]; + + if (key !== 'class' && key !== 'style') { + dstAttr[key] = srcAttr[key]; + } } }); } @@ -6696,18 +10259,18 @@ afterTemplateChildLinkFn, beforeTemplateCompileNode = $compileNode[0], origAsyncDirective = directives.shift(), - // The fact that we have to copy and patch the directive seems wrong! - derivedSyncDirective = extend({}, origAsyncDirective, { + derivedSyncDirective = inherit(origAsyncDirective, { templateUrl: null, transclude: null, replace: null, $$originalDirective: origAsyncDirective }), templateUrl = (isFunction(origAsyncDirective.templateUrl)) ? origAsyncDirective.templateUrl($compileNode, tAttrs) - : origAsyncDirective.templateUrl; + : origAsyncDirective.templateUrl, + templateNamespace = origAsyncDirective.templateNamespace; $compileNode.empty(); - $http.get($sce.getTrustedResourceUrl(templateUrl), {cache: $templateCache}). - success(function(content) { + $templateRequest(templateUrl) + .then(function(content) { var compileNode, tempTemplateAttrs, $template, childBoundTranscludeFn; content = denormalizeTemplate(content); @@ -6716,13 +10279,13 @@ if (jqLiteIsTextNode(content)) { $template = []; } else { - $template = jqLite(content); + $template = removeComments(wrapTemplate(templateNamespace, trim(content))); } compileNode = $template[0]; - if ($template.length != 1 || compileNode.nodeType !== 1) { + if ($template.length !== 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) { throw $compileMinErr('tplrt', - "Template for directive '{0}' must have exactly one root element. {1}", + 'Template for directive \'{0}\' must have exactly one root element. {1}', origAsyncDirective.name, templateUrl); } @@ -6731,7 +10294,9 @@ var templateDirectives = collectDirectives(compileNode, [], tempTemplateAttrs); if (isObject(origAsyncDirective.scope)) { - markDirectivesAsIsolate(templateDirectives); + // the original directive that caused the template to be loaded async required + // an isolate scope + markDirectiveScope(templateDirectives, true); } directives = templateDirectives.concat(directives); mergeTemplateAttributes(tAttrs, tempTemplateAttrs); @@ -6746,20 +10311,21 @@ childTranscludeFn, $compileNode, origAsyncDirective, preLinkFns, postLinkFns, previousCompileContext); forEach($rootElement, function(node, i) { - if (node == compileNode) { + if (node === compileNode) { $rootElement[i] = $compileNode[0]; } }); afterTemplateChildLinkFn = compileNodes($compileNode[0].childNodes, childTranscludeFn); - - while(linkQueue.length) { + while (linkQueue.length) { var scope = linkQueue.shift(), beforeTemplateLinkNode = linkQueue.shift(), linkRootElement = linkQueue.shift(), boundTranscludeFn = linkQueue.shift(), linkNode = $compileNode[0]; + if (scope.$$destroyed) continue; + if (beforeTemplateLinkNode !== beforeTemplateCompileNode) { var oldClasses = beforeTemplateLinkNode.className; @@ -6768,14 +10334,13 @@ // it was cloned therefore we have to clone as well. linkNode = jqLiteClone(compileNode); } - replaceWith(linkRootElement, jqLite(beforeTemplateLinkNode), linkNode); // Copy in CSS classes from original node safeAddClass(jqLite(linkNode), oldClasses); } - if (afterTemplateNodeLinkFn.transclude) { - childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude); + if (afterTemplateNodeLinkFn.transcludeOnThisElement) { + childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude, boundTranscludeFn); } else { childBoundTranscludeFn = boundTranscludeFn; } @@ -6783,19 +10348,25 @@ childBoundTranscludeFn); } linkQueue = null; - }). - error(function(response, code, headers, config) { - throw $compileMinErr('tpload', 'Failed to load template: {0}', config.url); - }); + }).catch(function(error) { + if (isError(error)) { + $exceptionHandler(error); + } + }); return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, boundTranscludeFn) { + var childBoundTranscludeFn = boundTranscludeFn; + if (scope.$$destroyed) return; if (linkQueue) { - linkQueue.push(scope); - linkQueue.push(node); - linkQueue.push(rootElement); - linkQueue.push(boundTranscludeFn); + linkQueue.push(scope, + node, + rootElement, + childBoundTranscludeFn); } else { - afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, boundTranscludeFn); + if (afterTemplateNodeLinkFn.transcludeOnThisElement) { + childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude, boundTranscludeFn); + } + afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, childBoundTranscludeFn); } }; } @@ -6811,11 +10382,18 @@ return a.index - b.index; } - function assertNoDuplicate(what, previousDirective, directive, element) { + + function wrapModuleNameIfDefined(moduleName) { + return moduleName ? + (' (module: ' + moduleName + ')') : + ''; + } + if (previousDirective) { - throw $compileMinErr('multidir', 'Multiple directives [{0}, {1}] asking for {2} on: {3}', - previousDirective.name, directive.name, what, startingTag(element)); + throw $compileMinErr('multidir', 'Multiple directives [{0}{1}, {2}{3}] asking for {4} on: {5}', + previousDirective.name, wrapModuleNameIfDefined(previousDirective.$$moduleName), + directive.name, wrapModuleNameIfDefined(directive.$$moduleName), what, startingTag(element)); } } @@ -6825,87 +10403,127 @@ if (interpolateFn) { directives.push({ priority: 0, - compile: valueFn(function textInterpolateLinkFn(scope, node) { - var parent = node.parent(), - bindings = parent.data('$binding') || []; - bindings.push(interpolateFn); - safeAddClass(parent.data('$binding', bindings), 'ng-binding'); - scope.$watch(interpolateFn, function interpolateFnWatchAction(value) { - node[0].nodeValue = value; - }); - }) + compile: function textInterpolateCompileFn(templateNode) { + var templateNodeParent = templateNode.parent(), + hasCompileParent = !!templateNodeParent.length; + + // When transcluding a template that has bindings in the root + // we don't have a parent and thus need to add the class during linking fn. + if (hasCompileParent) compile.$$addBindingClass(templateNodeParent); + + return function textInterpolateLinkFn(scope, node) { + var parent = node.parent(); + if (!hasCompileParent) compile.$$addBindingClass(parent); + compile.$$addBindingInfo(parent, interpolateFn.expressions); + scope.$watch(interpolateFn, function interpolateFnWatchAction(value) { + node[0].nodeValue = value; + }); + }; + } }); } } + function wrapTemplate(type, template) { + type = lowercase(type || 'html'); + switch (type) { + case 'svg': + case 'math': + var wrapper = window.document.createElement('div'); + wrapper.innerHTML = '<' + type + '>' + template + ''; + return wrapper.childNodes[0].childNodes; + default: + return template; + } + } + + function getTrustedContext(node, attrNormalizedName) { - if (attrNormalizedName == "srcdoc") { + if (attrNormalizedName === 'srcdoc') { return $sce.HTML; } var tag = nodeName_(node); - // maction[xlink:href] can source SVG. It's not limited to . - if (attrNormalizedName == "xlinkHref" || - (tag == "FORM" && attrNormalizedName == "action") || - (tag != "IMG" && (attrNormalizedName == "src" || - attrNormalizedName == "ngSrc"))) { + // All tags with src attributes require a RESOURCE_URL value, except for + // img and various html5 media tags. + if (attrNormalizedName === 'src' || attrNormalizedName === 'ngSrc') { + if (['img', 'video', 'audio', 'source', 'track'].indexOf(tag) === -1) { + return $sce.RESOURCE_URL; + } + // maction[xlink:href] can source SVG. It's not limited to . + } else if (attrNormalizedName === 'xlinkHref' || + (tag === 'form' && attrNormalizedName === 'action') || + // links can be stylesheets or imports, which can run script in the current origin + (tag === 'link' && attrNormalizedName === 'href') + ) { return $sce.RESOURCE_URL; } } - function addAttrInterpolateDirective(node, directives, value, name) { - var interpolateFn = $interpolate(value, true); + function addAttrInterpolateDirective(node, directives, value, name, isNgAttr) { + var trustedContext = getTrustedContext(node, name); + var mustHaveExpression = !isNgAttr; + var allOrNothing = ALL_OR_NOTHING_ATTRS[name] || isNgAttr; + + var interpolateFn = $interpolate(value, mustHaveExpression, trustedContext, allOrNothing); // no interpolation found -> ignore if (!interpolateFn) return; - - if (name === "multiple" && nodeName_(node) === "SELECT") { - throw $compileMinErr("selmulti", - "Binding to the 'multiple' attribute is not supported. Element: {0}", + if (name === 'multiple' && nodeName_(node) === 'select') { + throw $compileMinErr('selmulti', + 'Binding to the \'multiple\' attribute is not supported. Element: {0}', startingTag(node)); } + if (EVENT_HANDLER_ATTR_REGEXP.test(name)) { + throw $compileMinErr('nodomevents', + 'Interpolations for HTML DOM event attributes are disallowed. Please use the ' + + 'ng- versions (such as ng-click instead of onclick) instead.'); + } + directives.push({ priority: 100, compile: function() { return { pre: function attrInterpolatePreLinkFn(scope, element, attr) { - var $$observers = (attr.$$observers || (attr.$$observers = {})); - - if (EVENT_HANDLER_ATTR_REGEXP.test(name)) { - throw $compileMinErr('nodomevents', - "Interpolations for HTML DOM event attributes are disallowed. Please use the " + - "ng- versions (such as ng-click instead of onclick) instead."); + var $$observers = (attr.$$observers || (attr.$$observers = createMap())); + + // If the attribute has changed since last $interpolate()ed + var newValue = attr[name]; + if (newValue !== value) { + // we need to interpolate again since the attribute value has been updated + // (e.g. by another directive's compile function) + // ensure unset/empty values make interpolateFn falsy + interpolateFn = newValue && $interpolate(newValue, true, trustedContext, allOrNothing); + value = newValue; } - // we need to interpolate again, in case the attribute value has been updated - // (e.g. by another directive's compile function) - interpolateFn = $interpolate(attr[name], true, getTrustedContext(node, name)); - // if attribute was updated so that there is no interpolation going on we don't want to // register any observers if (!interpolateFn) return; - // TODO(i): this should likely be attr.$set(name, iterpolateFn(scope) so that we reset the - // actual attr value + // initialize attr object so that it's ready in case we need the value for isolate + // scope initialization, otherwise the value would not be available from isolate + // directive's linking fn during linking phase attr[name] = interpolateFn(scope); + ($$observers[name] || ($$observers[name] = [])).$$inter = true; (attr.$$observers && attr.$$observers[name].$$scope || scope). - $watch(interpolateFn, function interpolateFnWatchAction(newValue, oldValue) { - //special case for class attribute addition + removal - //so that class changes can tap into the animation - //hooks provided by the $animate service. Be sure to - //skip animations when the first digest occurs (when - //both the new and the old values are the same) since - //the CSS classes are the non-interpolated values - if(name === 'class' && newValue != oldValue) { - attr.$updateClass(newValue, oldValue); - } else { - attr.$set(name, newValue); - } - }); + $watch(interpolateFn, function interpolateFnWatchAction(newValue, oldValue) { + //special case for class attribute addition + removal + //so that class changes can tap into the animation + //hooks provided by the $animate service. Be sure to + //skip animations when the first digest occurs (when + //both the new and the old values are the same) since + //the CSS classes are the non-interpolated values + if (name === 'class' && newValue !== oldValue) { + attr.$updateClass(newValue, oldValue); + } else { + attr.$set(name, newValue); + } + }); } }; } @@ -6930,8 +10548,8 @@ i, ii; if ($rootElement) { - for(i = 0, ii = $rootElement.length; i < ii; i++) { - if ($rootElement[i] == firstElementToRemove) { + for (i = 0, ii = $rootElement.length; i < ii; i++) { + if ($rootElement[i] === firstElementToRemove) { $rootElement[i++] = newNode; for (var j = i, j2 = j + removeCount - 1, jj = $rootElement.length; @@ -6943,6 +10561,13 @@ } } $rootElement.length -= removeCount - 1; + + // If the replaced element is also the jQuery .context then replace it + // .context is a deprecated jQuery api, so we should set it only when jQuery set it + // http://api.jquery.com/context/ + if ($rootElement.context === firstElementToRemove) { + $rootElement.context = newNode; + } break; } } @@ -6951,16 +10576,34 @@ if (parent) { parent.replaceChild(newNode, firstElementToRemove); } - var fragment = document.createDocumentFragment(); - fragment.appendChild(firstElementToRemove); - newNode[jqLite.expando] = firstElementToRemove[jqLite.expando]; - for (var k = 1, kk = elementsToRemove.length; k < kk; k++) { - var element = elementsToRemove[k]; - jqLite(element).remove(); // must do this way to clean up expando - fragment.appendChild(element); - delete elementsToRemove[k]; + + // Append all the `elementsToRemove` to a fragment. This will... + // - remove them from the DOM + // - allow them to still be traversed with .nextSibling + // - allow a single fragment.qSA to fetch all elements being removed + var fragment = window.document.createDocumentFragment(); + for (i = 0; i < removeCount; i++) { + fragment.appendChild(elementsToRemove[i]); + } + + if (jqLite.hasData(firstElementToRemove)) { + // Copy over user data (that includes AngularJS's $scope etc.). Don't copy private + // data here because there's no public interface in jQuery to do that and copying over + // event listeners (which is the main use of private data) wouldn't work anyway. + jqLite.data(newNode, jqLite.data(firstElementToRemove)); + + // Remove $destroy event listeners from `firstElementToRemove` + jqLite(firstElementToRemove).off('$destroy'); } + // Cleanup any data/listeners on the elements and children. + // This includes invoking the $destroy event on any elements with listeners. + jqLite.cleanData(fragment.querySelectorAll('*')); + + // Update the jqLite collection to only contain the `newNode` + for (i = 1; i < removeCount; i++) { + delete elementsToRemove[i]; + } elementsToRemove[0] = newNode; elementsToRemove.length = 1; } @@ -6969,102 +10612,332 @@ function cloneAndAnnotateFn(fn, annotation) { return extend(function() { return fn.apply(null, arguments); }, fn, annotation); } - }]; - } - - var PREFIX_REGEXP = /^(x[\:\-_]|data[\:\-_])/i; - /** - * Converts all accepted directives format into proper directive name. - * All of these will become 'myDirective': - * my:Directive - * my-directive - * x-my-directive - * data-my:directive - * - * Also there is special case for Moz prefix starting with upper case letter. - * @param name Name to normalize - */ - function directiveNormalize(name) { - return camelCase(name.replace(PREFIX_REGEXP, '')); - } - /** - * @ngdoc type - * @name $compile.directive.Attributes - * - * @description - * A shared object between directive compile / linking functions which contains normalized DOM - * element attributes. The values reflect current binding state `{{ }}`. The normalization is - * needed since all of these are treated as equivalent in Angular: - * - * - */ - /** - * @ngdoc property - * @name $compile.directive.Attributes#$attr - * @returns {object} A map of DOM element attribute names to the normalized name. This is - * needed to do reverse lookup from normalized name back to actual name. - */ + function invokeLinkFn(linkFn, scope, $element, attrs, controllers, transcludeFn) { + try { + linkFn(scope, $element, attrs, controllers, transcludeFn); + } catch (e) { + $exceptionHandler(e, startingTag($element)); + } + } + function strictBindingsCheck(attrName, directiveName) { + if (strictComponentBindingsEnabled) { + throw $compileMinErr('missingattr', + 'Attribute \'{0}\' of \'{1}\' is non-optional and must be set!', + attrName, directiveName); + } + } - /** - * @ngdoc method - * @name $compile.directive.Attributes#$set - * @function - * - * @description - * Set DOM element attribute value. - * - * - * @param {string} name Normalized element attribute name of the property to modify. The name is - * reverse-translated using the {@link ng.$compile.directive.Attributes#$attr $attr} - * property to the original name. - * @param {string} value Value to set the attribute to. The value can be an interpolated string. - */ + // Set up $watches for isolate scope and controller bindings. + function initializeDirectiveBindings(scope, attrs, destination, bindings, directive) { + var removeWatchCollection = []; + var initialChanges = {}; + var changes; + forEach(bindings, function initializeBinding(definition, scopeName) { + var attrName = definition.attrName, + optional = definition.optional, + mode = definition.mode, // @, =, <, or & + lastValue, + parentGet, parentSet, compare, removeWatch; + switch (mode) { - /** - * Closure compiler type information - */ + case '@': + if (!optional && !hasOwnProperty.call(attrs, attrName)) { + strictBindingsCheck(attrName, directive.name); + destination[scopeName] = attrs[attrName] = undefined; - function nodesetLinkingFn( - /* angular.Scope */ scope, - /* NodeList */ nodeList, - /* Element */ rootElement, - /* function(Function) */ boundTranscludeFn - ){} + } + removeWatch = attrs.$observe(attrName, function(value) { + if (isString(value) || isBoolean(value)) { + var oldValue = destination[scopeName]; + recordChanges(scopeName, value, oldValue); + destination[scopeName] = value; + } + }); + attrs.$$observers[attrName].$$scope = scope; + lastValue = attrs[attrName]; + if (isString(lastValue)) { + // If the attribute has been provided then we trigger an interpolation to ensure + // the value is there for use in the link fn + destination[scopeName] = $interpolate(lastValue)(scope); + } else if (isBoolean(lastValue)) { + // If the attributes is one of the BOOLEAN_ATTR then AngularJS will have converted + // the value to boolean rather than a string, so we special case this situation + destination[scopeName] = lastValue; + } + initialChanges[scopeName] = new SimpleChange(_UNINITIALIZED_VALUE, destination[scopeName]); + removeWatchCollection.push(removeWatch); + break; - function directiveLinkingFn( - /* nodesetLinkingFn */ nodesetLinkingFn, - /* angular.Scope */ scope, - /* Node */ node, - /* Element */ rootElement, - /* function(Function) */ boundTranscludeFn - ){} + case '=': + if (!hasOwnProperty.call(attrs, attrName)) { + if (optional) break; + strictBindingsCheck(attrName, directive.name); + attrs[attrName] = undefined; + } + if (optional && !attrs[attrName]) break; - function tokenDifference(str1, str2) { - var values = '', - tokens1 = str1.split(/\s+/), - tokens2 = str2.split(/\s+/); + parentGet = $parse(attrs[attrName]); + if (parentGet.literal) { + compare = equals; + } else { + compare = simpleCompare; + } + parentSet = parentGet.assign || function() { + // reset the change, or we will throw this exception on every $digest + lastValue = destination[scopeName] = parentGet(scope); + throw $compileMinErr('nonassign', + 'Expression \'{0}\' in attribute \'{1}\' used with directive \'{2}\' is non-assignable!', + attrs[attrName], attrName, directive.name); + }; + lastValue = destination[scopeName] = parentGet(scope); + var parentValueWatch = function parentValueWatch(parentValue) { + if (!compare(parentValue, destination[scopeName])) { + // we are out of sync and need to copy + if (!compare(parentValue, lastValue)) { + // parent changed and it has precedence + destination[scopeName] = parentValue; + } else { + // if the parent can be assigned then do so + parentSet(scope, parentValue = destination[scopeName]); + } + } + lastValue = parentValue; + return lastValue; + }; + parentValueWatch.$stateful = true; + if (definition.collection) { + removeWatch = scope.$watchCollection(attrs[attrName], parentValueWatch); + } else { + removeWatch = scope.$watch($parse(attrs[attrName], parentValueWatch), null, parentGet.literal); + } + removeWatchCollection.push(removeWatch); + break; + + case '<': + if (!hasOwnProperty.call(attrs, attrName)) { + if (optional) break; + strictBindingsCheck(attrName, directive.name); + attrs[attrName] = undefined; + } + if (optional && !attrs[attrName]) break; + + parentGet = $parse(attrs[attrName]); + var deepWatch = parentGet.literal; + + var initialValue = destination[scopeName] = parentGet(scope); + initialChanges[scopeName] = new SimpleChange(_UNINITIALIZED_VALUE, destination[scopeName]); + + removeWatch = scope.$watch(parentGet, function parentValueWatchAction(newValue, oldValue) { + if (oldValue === newValue) { + if (oldValue === initialValue || (deepWatch && equals(oldValue, initialValue))) { + return; + } + oldValue = initialValue; + } + recordChanges(scopeName, newValue, oldValue); + destination[scopeName] = newValue; + }, deepWatch); + + removeWatchCollection.push(removeWatch); + break; + + case '&': + if (!optional && !hasOwnProperty.call(attrs, attrName)) { + strictBindingsCheck(attrName, directive.name); + } + // Don't assign Object.prototype method to scope + parentGet = attrs.hasOwnProperty(attrName) ? $parse(attrs[attrName]) : noop; + + // Don't assign noop to destination if expression is not valid + if (parentGet === noop && optional) break; + + destination[scopeName] = function(locals) { + return parentGet(scope, locals); + }; + break; + } + }); + + function recordChanges(key, currentValue, previousValue) { + if (isFunction(destination.$onChanges) && !simpleCompare(currentValue, previousValue)) { + // If we have not already scheduled the top level onChangesQueue handler then do so now + if (!onChangesQueue) { + scope.$$postDigest(flushOnChangesQueue); + onChangesQueue = []; + } + // If we have not already queued a trigger of onChanges for this controller then do so now + if (!changes) { + changes = {}; + onChangesQueue.push(triggerOnChangesHook); + } + // If the has been a change on this property already then we need to reuse the previous value + if (changes[key]) { + previousValue = changes[key].previousValue; + } + // Store this change + changes[key] = new SimpleChange(previousValue, currentValue); + } + } + + function triggerOnChangesHook() { + destination.$onChanges(changes); + // Now clear the changes so that we schedule onChanges when more changes arrive + changes = undefined; + } + + return { + initialChanges: initialChanges, + removeWatches: removeWatchCollection.length && function removeWatches() { + for (var i = 0, ii = removeWatchCollection.length; i < ii; ++i) { + removeWatchCollection[i](); + } + } + }; + } + }]; + } + + function SimpleChange(previous, current) { + this.previousValue = previous; + this.currentValue = current; + } + SimpleChange.prototype.isFirstChange = function() { return this.previousValue === _UNINITIALIZED_VALUE; }; + + + var PREFIX_REGEXP = /^((?:x|data)[:\-_])/i; + var SPECIAL_CHARS_REGEXP = /[:\-_]+(.)/g; + + /** + * Converts all accepted directives format into proper directive name. + * @param name Name to normalize + */ + function directiveNormalize(name) { + return name + .replace(PREFIX_REGEXP, '') + .replace(SPECIAL_CHARS_REGEXP, function(_, letter, offset) { + return offset ? letter.toUpperCase() : letter; + }); + } + + /** + * @ngdoc type + * @name $compile.directive.Attributes + * + * @description + * A shared object between directive compile / linking functions which contains normalized DOM + * element attributes. The values reflect current binding state `{{ }}`. The normalization is + * needed since all of these are treated as equivalent in AngularJS: + * + * ``` + * + * ``` + */ + + /** + * @ngdoc property + * @name $compile.directive.Attributes#$attr + * + * @description + * A map of DOM element attribute names to the normalized name. This is + * needed to do reverse lookup from normalized name back to actual name. + */ + + + /** + * @ngdoc method + * @name $compile.directive.Attributes#$set + * @kind function + * + * @description + * Set DOM element attribute value. + * + * + * @param {string} name Normalized element attribute name of the property to modify. The name is + * reverse-translated using the {@link ng.$compile.directive.Attributes#$attr $attr} + * property to the original name. + * @param {string} value Value to set the attribute to. The value can be an interpolated string. + */ + + + + /** + * Closure compiler type information + */ + + function nodesetLinkingFn( + /* angular.Scope */ scope, + /* NodeList */ nodeList, + /* Element */ rootElement, + /* function(Function) */ boundTranscludeFn + ) {} + + function directiveLinkingFn( + /* nodesetLinkingFn */ nodesetLinkingFn, + /* angular.Scope */ scope, + /* Node */ node, + /* Element */ rootElement, + /* function(Function) */ boundTranscludeFn + ) {} + + function tokenDifference(str1, str2) { + var values = '', + tokens1 = str1.split(/\s+/), + tokens2 = str2.split(/\s+/); outer: - for(var i = 0; i < tokens1.length; i++) { + for (var i = 0; i < tokens1.length; i++) { var token = tokens1[i]; - for(var j = 0; j < tokens2.length; j++) { - if(token == tokens2[j]) continue outer; + for (var j = 0; j < tokens2.length; j++) { + if (token === tokens2[j]) continue outer; } values += (values.length > 0 ? ' ' : '') + token; } return values; } + function removeComments(jqNodes) { + jqNodes = jqLite(jqNodes); + var i = jqNodes.length; + + if (i <= 1) { + return jqNodes; + } + + while (i--) { + var node = jqNodes[i]; + if (node.nodeType === NODE_TYPE_COMMENT || + (node.nodeType === NODE_TYPE_TEXT && node.nodeValue.trim() === '')) { + splice.call(jqNodes, i, 1); + } + } + return jqNodes; + } + + var $controllerMinErr = minErr('$controller'); + + + var CNTRL_REG = /^(\S+)(\s+as\s+([\w$]+))?$/; + function identifierForController(controller, ident) { + if (ident && isString(ident)) return ident; + if (isString(controller)) { + var match = CNTRL_REG.exec(controller); + if (match) return match[3]; + } + } + + /** * @ngdoc provider * @name $controllerProvider + * @this + * * @description - * The {@link ng.$controller $controller service} is used by Angular to create new + * The {@link ng.$controller $controller service} is used by AngularJS to create new * controllers. * * This provider allows controller registration via the @@ -7072,8 +10945,16 @@ */ function $ControllerProvider() { var controllers = {}, - CNTRL_REG = /^(\S+)(\s+as\s+(\w+))?$/; + globals = false; + /** + * @ngdoc method + * @name $controllerProvider#has + * @param {string} name Controller name to check. + */ + this.has = function(name) { + return controllers.hasOwnProperty(name); + }; /** * @ngdoc method @@ -7092,6 +10973,20 @@ } }; + /** + * @ngdoc method + * @name $controllerProvider#allowGlobals + * @description If called, allows `$controller` to find controller constructors on `window` + * + * @deprecated + * sinceVersion="v1.3.0" + * removeVersion="v1.7.0" + * This method of finding controllers has been deprecated. + */ + this.allowGlobals = function() { + globals = true; + }; + this.$get = ['$injector', '$window', function($injector, $window) { @@ -7106,7 +11001,12 @@ * * * check if a controller with given name is registered via `$controllerProvider` * * check if evaluating the string on the current scope returns a constructor - * * check `window[constructor]` on the global `window` object + * * if $controllerProvider#allowGlobals, check `window[constructor]` on the global + * `window` object (deprecated, not recommended) + * + * The string can use the `controller as property` syntax, where the controller instance is published + * as the specified property on the `scope`; the `scope` must be injected into `locals` param for this + * to work correctly. * * @param {Object} locals Injection locals for Controller. * @return {Object} Instance of given controller. @@ -7117,34 +11017,95 @@ * It's just a simple call to {@link auto.$injector $injector}, but extracted into * a service, so that one can override this service with [BC version](https://gist.github.com/1649788). */ - return function(expression, locals) { + return function $controller(expression, locals, later, ident) { + // PRIVATE API: + // param `later` --- indicates that the controller's constructor is invoked at a later time. + // If true, $controller will allocate the object with the correct + // prototype chain, but will not invoke the controller until a returned + // callback is invoked. + // param `ident` --- An optional label which overrides the label parsed from the controller + // expression, if any. var instance, match, constructor, identifier; + later = later === true; + if (ident && isString(ident)) { + identifier = ident; + } - if(isString(expression)) { - match = expression.match(CNTRL_REG), - constructor = match[1], - identifier = match[3]; + if (isString(expression)) { + match = expression.match(CNTRL_REG); + if (!match) { + throw $controllerMinErr('ctrlfmt', + 'Badly formed controller string \'{0}\'. ' + + 'Must match `__name__ as __id__` or `__name__`.', expression); + } + constructor = match[1]; + identifier = identifier || match[3]; expression = controllers.hasOwnProperty(constructor) ? controllers[constructor] - : getter(locals.$scope, constructor, true) || getter($window, constructor, true); + : getter(locals.$scope, constructor, true) || + (globals ? getter($window, constructor, true) : undefined); + + if (!expression) { + throw $controllerMinErr('ctrlreg', + 'The controller with the name \'{0}\' is not registered.', constructor); + } assertArgFn(expression, constructor, true); } - instance = $injector.instantiate(expression, locals); - - if (identifier) { - if (!(locals && typeof locals.$scope == 'object')) { - throw minErr('$controller')('noscp', - "Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`.", - constructor || expression.name, identifier); + if (later) { + // Instantiate controller later: + // This machinery is used to create an instance of the object before calling the + // controller's constructor itself. + // + // This allows properties to be added to the controller before the constructor is + // invoked. Primarily, this is used for isolate scope bindings in $compile. + // + // This feature is not intended for use by applications, and is thus not documented + // publicly. + // Object creation: http://jsperf.com/create-constructor/2 + var controllerPrototype = (isArray(expression) ? + expression[expression.length - 1] : expression).prototype; + instance = Object.create(controllerPrototype || null); + + if (identifier) { + addIdentifier(locals, identifier, instance, constructor || expression.name); } - locals.$scope[identifier] = instance; + return extend(function $controllerInit() { + var result = $injector.invoke(expression, instance, locals, constructor); + if (result !== instance && (isObject(result) || isFunction(result))) { + instance = result; + if (identifier) { + // If result changed, re-assign controllerAs value to scope. + addIdentifier(locals, identifier, instance, constructor || expression.name); + } + } + return instance; + }, { + instance: instance, + identifier: identifier + }); + } + + instance = $injector.instantiate(expression, locals, constructor); + + if (identifier) { + addIdentifier(locals, identifier, instance, constructor || expression.name); } return instance; }; + + function addIdentifier(locals, identifier, instance, name) { + if (!(locals && isObject(locals.$scope))) { + throw minErr('$controller')('noscp', + 'Cannot export controller \'{0}\' as \'{1}\'! No $scope object provided via `locals`.', + name, identifier); + } + + locals.$scope[identifier] = instance; + } }]; } @@ -7152,39 +11113,69 @@ * @ngdoc service * @name $document * @requires $window + * @this * * @description * A {@link angular.element jQuery or jqLite} wrapper for the browser's `window.document` object. * * @example - + -
    +

    $document title:

    window.document title:

    - function MainCtrl($scope, $document) { - $scope.title = $document[0].title; - $scope.windowTitle = angular.element(window.document)[0].title; - } + angular.module('documentExample', []) + .controller('ExampleController', ['$scope', '$document', function($scope, $document) { + $scope.title = $document[0].title; + $scope.windowTitle = angular.element(window.document)[0].title; + }]); */ - function $DocumentProvider(){ - this.$get = ['$window', function(window){ + function $DocumentProvider() { + this.$get = ['$window', function(window) { return jqLite(window.document); }]; } + + /** + * @private + * @this + * Listens for document visibility change and makes the current status accessible. + */ + function $$IsDocumentHiddenProvider() { + this.$get = ['$document', '$rootScope', function($document, $rootScope) { + var doc = $document[0]; + var hidden = doc && doc.hidden; + + $document.on('visibilitychange', changeListener); + + $rootScope.$on('$destroy', function() { + $document.off('visibilitychange', changeListener); + }); + + function changeListener() { + hidden = doc.hidden; + } + + return function() { + return hidden; + }; + }]; + } + /** * @ngdoc service * @name $exceptionHandler * @requires ng.$log + * @this * * @description - * Any uncaught exception in angular expressions is delegated to this service. + * Any uncaught exception in AngularJS expressions is delegated to this service. * The default implementation simply delegates to `$log.error` which logs it into * the browser console. * @@ -7193,20 +11184,31 @@ * * ## Example: * + * The example below will overwrite the default `$exceptionHandler` in order to (a) log uncaught + * errors to the backend for later inspection by the developers and (b) to use `$log.warn()` instead + * of `$log.error()`. + * * ```js - * angular.module('exceptionOverride', []).factory('$exceptionHandler', function () { - * return function (exception, cause) { - * exception.message += ' (caused by "' + cause + '")'; - * throw exception; - * }; - * }); + * angular. + * module('exceptionOverwrite', []). + * factory('$exceptionHandler', ['$log', 'logErrorsToBackend', function($log, logErrorsToBackend) { + * return function myExceptionHandler(exception, cause) { + * logErrorsToBackend(exception, cause); + * $log.warn(exception, cause); + * }; + * }]); * ``` * - * This example will override the normal action of `$exceptionHandler`, to make angular - * exceptions fail hard when they happen, instead of just logging to the console. + *
    + * Note, that code executed in event-listeners (even those registered using jqLite's `on`/`bind` + * methods) does not delegate exceptions to the {@link ng.$exceptionHandler $exceptionHandler} + * (unless executed during a digest). + * + * If you wish, you can manually delegate exceptions, e.g. + * `try { ... } catch(e) { $exceptionHandler(e); }` * * @param {Error} exception Exception associated with the error. - * @param {string=} cause optional information about the context in which + * @param {string=} cause Optional information about the context in which * the error was thrown. * */ @@ -7218,110 +11220,349 @@ }]; } - /** - * Parse headers into key value object - * - * @param {string} headers Raw headers as a string - * @returns {Object} Parsed headers as key value object - */ - function parseHeaders(headers) { - var parsed = {}, key, val, i; - - if (!headers) return parsed; - - forEach(headers.split('\n'), function(line) { - i = line.indexOf(':'); - key = lowercase(trim(line.substr(0, i))); - val = trim(line.substr(i + 1)); - - if (key) { - if (parsed[key]) { - parsed[key] += ', ' + val; + var $$ForceReflowProvider = /** @this */ function() { + this.$get = ['$document', function($document) { + return function(domNode) { + //the line below will force the browser to perform a repaint so + //that all the animated elements within the animation frame will + //be properly updated and drawn on screen. This is required to + //ensure that the preparation animation is properly flushed so that + //the active state picks up from there. DO NOT REMOVE THIS LINE. + //DO NOT OPTIMIZE THIS LINE. THE MINIFIER WILL REMOVE IT OTHERWISE WHICH + //WILL RESULT IN AN UNPREDICTABLE BUG THAT IS VERY HARD TO TRACK DOWN AND + //WILL TAKE YEARS AWAY FROM YOUR LIFE. + if (domNode) { + if (!domNode.nodeType && domNode instanceof jqLite) { + domNode = domNode[0]; + } } else { - parsed[key] = val; + domNode = $document[0].body; } - } - }); - - return parsed; - } - - - /** - * Returns a function that provides access to parsed headers. - * - * Headers are lazy parsed when first requested. - * @see parseHeaders - * - * @param {(string|Object)} headers Headers to provide access to. - * @returns {function(string=)} Returns a getter function which if called with: - * - * - if called with single an argument returns a single header value or null - * - if called with no arguments returns an object containing all headers. - */ - function headersGetter(headers) { - var headersObj = isObject(headers) ? headers : undefined; - - return function(name) { - if (!headersObj) headersObj = parseHeaders(headers); + return domNode.offsetWidth + 1; + }; + }]; + }; - if (name) { - return headersObj[lowercase(name)] || null; - } + var APPLICATION_JSON = 'application/json'; + var CONTENT_TYPE_APPLICATION_JSON = {'Content-Type': APPLICATION_JSON + ';charset=utf-8'}; + var JSON_START = /^\[|^\{(?!\{)/; + var JSON_ENDS = { + '[': /]$/, + '{': /}$/ + }; + var JSON_PROTECTION_PREFIX = /^\)]\}',?\n/; + var $httpMinErr = minErr('$http'); - return headersObj; - }; + function serializeValue(v) { + if (isObject(v)) { + return isDate(v) ? v.toISOString() : toJson(v); + } + return v; } - /** - * Chain all given functions - * - * This function is used for both request and response transforming - * - * @param {*} data Data to transform. - * @param {function(string=)} headers Http headers getter fn. - * @param {(Function|Array.)} fns Function or an array of functions. - * @returns {*} Transformed data. - */ - function transformData(data, headers, fns) { - if (isFunction(fns)) - return fns(data, headers); - - forEach(fns, function(fn) { - data = fn(data, headers); - }); - - return data; - } + /** @this */ + function $HttpParamSerializerProvider() { + /** + * @ngdoc service + * @name $httpParamSerializer + * @description + * + * Default {@link $http `$http`} params serializer that converts objects to strings + * according to the following rules: + * + * * `{'foo': 'bar'}` results in `foo=bar` + * * `{'foo': Date.now()}` results in `foo=2015-04-01T09%3A50%3A49.262Z` (`toISOString()` and encoded representation of a Date object) + * * `{'foo': ['bar', 'baz']}` results in `foo=bar&foo=baz` (repeated key for each array element) + * * `{'foo': {'bar':'baz'}}` results in `foo=%7B%22bar%22%3A%22baz%22%7D` (stringified and encoded representation of an object) + * + * Note that serializer will sort the request parameters alphabetically. + * */ + this.$get = function() { + return function ngParamSerializer(params) { + if (!params) return ''; + var parts = []; + forEachSorted(params, function(value, key) { + if (value === null || isUndefined(value) || isFunction(value)) return; + if (isArray(value)) { + forEach(value, function(v) { + parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(serializeValue(v))); + }); + } else { + parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(serializeValue(value))); + } + }); - function isSuccess(status) { - return 200 <= status && status < 300; + return parts.join('&'); + }; + }; } + /** @this */ + function $HttpParamSerializerJQLikeProvider() { + /** + * @ngdoc service + * @name $httpParamSerializerJQLike + * + * @description + * + * Alternative {@link $http `$http`} params serializer that follows + * jQuery's [`param()`](http://api.jquery.com/jquery.param/) method logic. + * The serializer will also sort the params alphabetically. + * + * To use it for serializing `$http` request parameters, set it as the `paramSerializer` property: + * + * ```js + * $http({ + * url: myUrl, + * method: 'GET', + * params: myParams, + * paramSerializer: '$httpParamSerializerJQLike' + * }); + * ``` + * + * It is also possible to set it as the default `paramSerializer` in the + * {@link $httpProvider#defaults `$httpProvider`}. + * + * Additionally, you can inject the serializer and use it explicitly, for example to serialize + * form data for submission: + * + * ```js + * .controller(function($http, $httpParamSerializerJQLike) { + * //... + * + * $http({ + * url: myUrl, + * method: 'POST', + * data: $httpParamSerializerJQLike(myData), + * headers: { + * 'Content-Type': 'application/x-www-form-urlencoded' + * } + * }); + * + * }); + * ``` + * + * */ + this.$get = function() { + return function jQueryLikeParamSerializer(params) { + if (!params) return ''; + var parts = []; + serialize(params, '', true); + return parts.join('&'); + + function serialize(toSerialize, prefix, topLevel) { + if (toSerialize === null || isUndefined(toSerialize)) return; + if (isArray(toSerialize)) { + forEach(toSerialize, function(value, index) { + serialize(value, prefix + '[' + (isObject(value) ? index : '') + ']'); + }); + } else if (isObject(toSerialize) && !isDate(toSerialize)) { + forEachSorted(toSerialize, function(value, key) { + serialize(value, prefix + + (topLevel ? '' : '[') + + key + + (topLevel ? '' : ']')); + }); + } else { + parts.push(encodeUriQuery(prefix) + '=' + encodeUriQuery(serializeValue(toSerialize))); + } + } + }; + }; + } + + function defaultHttpResponseTransform(data, headers) { + if (isString(data)) { + // Strip json vulnerability protection prefix and trim whitespace + var tempData = data.replace(JSON_PROTECTION_PREFIX, '').trim(); + + if (tempData) { + var contentType = headers('Content-Type'); + var hasJsonContentType = contentType && (contentType.indexOf(APPLICATION_JSON) === 0); + + if (hasJsonContentType || isJsonLike(tempData)) { + try { + data = fromJson(tempData); + } catch (e) { + if (!hasJsonContentType) { + return data; + } + throw $httpMinErr('baddata', 'Data must be a valid JSON object. Received: "{0}". ' + + 'Parse error: "{1}"', data, e); + } + } + } + } + + return data; + } + + function isJsonLike(str) { + var jsonStart = str.match(JSON_START); + return jsonStart && JSON_ENDS[jsonStart[0]].test(str); + } + + /** + * Parse headers into key value object + * + * @param {string} headers Raw headers as a string + * @returns {Object} Parsed headers as key value object + */ + function parseHeaders(headers) { + var parsed = createMap(), i; + + function fillInParsed(key, val) { + if (key) { + parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val; + } + } + + if (isString(headers)) { + forEach(headers.split('\n'), function(line) { + i = line.indexOf(':'); + fillInParsed(lowercase(trim(line.substr(0, i))), trim(line.substr(i + 1))); + }); + } else if (isObject(headers)) { + forEach(headers, function(headerVal, headerKey) { + fillInParsed(lowercase(headerKey), trim(headerVal)); + }); + } + + return parsed; + } + + + /** + * Returns a function that provides access to parsed headers. + * + * Headers are lazy parsed when first requested. + * @see parseHeaders + * + * @param {(string|Object)} headers Headers to provide access to. + * @returns {function(string=)} Returns a getter function which if called with: + * + * - if called with an argument returns a single header value or null + * - if called with no arguments returns an object containing all headers. + */ + function headersGetter(headers) { + var headersObj; + + return function(name) { + if (!headersObj) headersObj = parseHeaders(headers); + + if (name) { + var value = headersObj[lowercase(name)]; + if (value === undefined) { + value = null; + } + return value; + } + + return headersObj; + }; + } + + + /** + * Chain all given functions + * + * This function is used for both request and response transforming + * + * @param {*} data Data to transform. + * @param {function(string=)} headers HTTP headers getter fn. + * @param {number} status HTTP status code of the response. + * @param {(Function|Array.)} fns Function or an array of functions. + * @returns {*} Transformed data. + */ + function transformData(data, headers, status, fns) { + if (isFunction(fns)) { + return fns(data, headers, status); + } + + forEach(fns, function(fn) { + data = fn(data, headers, status); + }); + + return data; + } + + + function isSuccess(status) { + return 200 <= status && status < 300; + } - function $HttpProvider() { - var JSON_START = /^\s*(\[|\{[^\{])/, - JSON_END = /[\}\]]\s*$/, - PROTECTION_PREFIX = /^\)\]\}',?\n/, - CONTENT_TYPE_APPLICATION_JSON = {'Content-Type': 'application/json;charset=utf-8'}; - + + /** + * @ngdoc provider + * @name $httpProvider + * @this + * + * @description + * Use `$httpProvider` to change the default behavior of the {@link ng.$http $http} service. + * */ + function $HttpProvider() { + /** + * @ngdoc property + * @name $httpProvider#defaults + * @description + * + * Object containing default values for all {@link ng.$http $http} requests. + * + * - **`defaults.cache`** - {boolean|Object} - A boolean value or object created with + * {@link ng.$cacheFactory `$cacheFactory`} to enable or disable caching of HTTP responses + * by default. See {@link $http#caching $http Caching} for more information. + * + * - **`defaults.headers`** - {Object} - Default headers for all $http requests. + * Refer to {@link ng.$http#setting-http-headers $http} for documentation on + * setting default headers. + * - **`defaults.headers.common`** + * - **`defaults.headers.post`** + * - **`defaults.headers.put`** + * - **`defaults.headers.patch`** + * + * - **`defaults.jsonpCallbackParam`** - `{string}` - the name of the query parameter that passes the name of the + * callback in a JSONP request. The value of this parameter will be replaced with the expression generated by the + * {@link $jsonpCallbacks} service. Defaults to `'callback'`. + * + * - **`defaults.paramSerializer`** - `{string|function(Object):string}` - A function + * used to the prepare string representation of request parameters (specified as an object). + * If specified as string, it is interpreted as a function registered with the {@link auto.$injector $injector}. + * Defaults to {@link ng.$httpParamSerializer $httpParamSerializer}. + * + * - **`defaults.transformRequest`** - + * `{Array|function(data, headersGetter)}` - + * An array of functions (or a single function) which are applied to the request data. + * By default, this is an array with one request transformation function: + * + * - If the `data` property of the request configuration object contains an object, serialize it + * into JSON format. + * + * - **`defaults.transformResponse`** - + * `{Array|function(data, headersGetter, status)}` - + * An array of functions (or a single function) which are applied to the response data. By default, + * this is an array which applies one response transformation function that does two things: + * + * - If XSRF prefix is detected, strip it + * (see {@link ng.$http#security-considerations Security Considerations in the $http docs}). + * - If the `Content-Type` is `application/json` or the response looks like JSON, + * deserialize it using a JSON parser. + * + * - **`defaults.xsrfCookieName`** - {string} - Name of cookie containing the XSRF token. + * Defaults value is `'XSRF-TOKEN'`. + * + * - **`defaults.xsrfHeaderName`** - {string} - Name of HTTP header to populate with the + * XSRF token. Defaults value is `'X-XSRF-TOKEN'`. + * + **/ var defaults = this.defaults = { // transform incoming response data - transformResponse: [function(data) { - if (isString(data)) { - // strip json vulnerability protection prefix - data = data.replace(PROTECTION_PREFIX, ''); - if (JSON_START.test(data) && JSON_END.test(data)) - data = fromJson(data); - } - return data; - }], + transformResponse: [defaultHttpResponseTransform], // transform outgoing request data transformRequest: [function(d) { - return isObject(d) && !isFile(d) && !isBlob(d) ? toJson(d) : d; + return isObject(d) && !isFile(d) && !isBlob(d) && !isFormData(d) ? toJson(d) : d; }], // default headers @@ -7329,32 +11570,73 @@ common: { 'Accept': 'application/json, text/plain, */*' }, - post: copy(CONTENT_TYPE_APPLICATION_JSON), - put: copy(CONTENT_TYPE_APPLICATION_JSON), - patch: copy(CONTENT_TYPE_APPLICATION_JSON) + post: shallowCopy(CONTENT_TYPE_APPLICATION_JSON), + put: shallowCopy(CONTENT_TYPE_APPLICATION_JSON), + patch: shallowCopy(CONTENT_TYPE_APPLICATION_JSON) }, xsrfCookieName: 'XSRF-TOKEN', - xsrfHeaderName: 'X-XSRF-TOKEN' + xsrfHeaderName: 'X-XSRF-TOKEN', + + paramSerializer: '$httpParamSerializer', + + jsonpCallbackParam: 'callback' }; + var useApplyAsync = false; /** - * Are ordered by request, i.e. they are applied in the same order as the - * array, on request, but reverse order, on response. - */ - var interceptorFactories = this.interceptors = []; + * @ngdoc method + * @name $httpProvider#useApplyAsync + * @description + * + * Configure $http service to combine processing of multiple http responses received at around + * the same time via {@link ng.$rootScope.Scope#$applyAsync $rootScope.$applyAsync}. This can result in + * significant performance improvement for bigger applications that make many HTTP requests + * concurrently (common during application bootstrap). + * + * Defaults to false. If no value is specified, returns the current configured value. + * + * @param {boolean=} value If true, when requests are loaded, they will schedule a deferred + * "apply" on the next tick, giving time for subsequent requests in a roughly ~10ms window + * to load and share the same digest cycle. + * + * @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining. + * otherwise, returns the current configured value. + **/ + this.useApplyAsync = function(value) { + if (isDefined(value)) { + useApplyAsync = !!value; + return this; + } + return useApplyAsync; + }; /** - * For historical reasons, response interceptors are ordered by the order in which - * they are applied to the response. (This is the opposite of interceptorFactories) - */ - var responseInterceptorFactories = this.responseInterceptors = []; + * @ngdoc property + * @name $httpProvider#interceptors + * @description + * + * Array containing service factories for all synchronous or asynchronous {@link ng.$http $http} + * pre-processing of request or postprocessing of responses. + * + * These service factories are ordered by request, i.e. they are applied in the same order as the + * array, on request, but reverse order, on response. + * + * {@link ng.$http#interceptors Interceptors detailed info} + **/ + var interceptorFactories = this.interceptors = []; - this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector', - function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector) { + this.$get = ['$browser', '$httpBackend', '$$cookieReader', '$cacheFactory', '$rootScope', '$q', '$injector', '$sce', + function($browser, $httpBackend, $$cookieReader, $cacheFactory, $rootScope, $q, $injector, $sce) { var defaultCache = $cacheFactory('$http'); + /** + * Make sure that default param serializer is exposed as a function + */ + defaults.paramSerializer = isString(defaults.paramSerializer) ? + $injector.get(defaults.paramSerializer) : defaults.paramSerializer; + /** * Interceptors stored in reverse order. Inner interceptors before outer interceptors. * The reversal is needed so that we can build up the interception chain around the @@ -7367,27 +11649,6 @@ ? $injector.get(interceptorFactory) : $injector.invoke(interceptorFactory)); }); - forEach(responseInterceptorFactories, function(interceptorFactory, index) { - var responseFn = isString(interceptorFactory) - ? $injector.get(interceptorFactory) - : $injector.invoke(interceptorFactory); - - /** - * Response interceptors go before "around" interceptors (no real reason, just - * had to pick one.) But they are already reversed, so we can't use unshift, hence - * the splice. - */ - reversedInterceptors.splice(index, 0, { - response: function(response) { - return responseFn($q.when(response)); - }, - responseError: function(response) { - return responseFn($q.reject(response)); - } - }); - }); - - /** * @ngdoc service * @kind function @@ -7399,7 +11660,7 @@ * @requires $injector * * @description - * The `$http` service is a core Angular service that facilitates communication with the remote + * The `$http` service is a core AngularJS service that facilitates communication with the remote * HTTP servers via the browser's [XMLHttpRequest](https://developer.mozilla.org/en/xmlhttprequest) * object or via [JSONP](http://en.wikipedia.org/wiki/JSONP). * @@ -7414,52 +11675,52 @@ * it is important to familiarize yourself with these APIs and the guarantees they provide. * * - * # General usage - * The `$http` service is a function which takes a single argument — a configuration object — - * that is used to generate an HTTP request and returns a {@link ng.$q promise} - * with two $http specific methods: `success` and `error`. + * ## General usage + * The `$http` service is a function which takes a single argument — a {@link $http#usage configuration object} — + * that is used to generate an HTTP request and returns a {@link ng.$q promise}. * * ```js - * $http({method: 'GET', url: '/someUrl'}). - * success(function(data, status, headers, config) { - * // this callback will be called asynchronously - * // when the response is available - * }). - * error(function(data, status, headers, config) { - * // called asynchronously if an error occurs - * // or server returns response with an error status. - * }); + * // Simple GET request example: + * $http({ + * method: 'GET', + * url: '/someUrl' + * }).then(function successCallback(response) { + * // this callback will be called asynchronously + * // when the response is available + * }, function errorCallback(response) { + * // called asynchronously if an error occurs + * // or server returns response with an error status. + * }); * ``` * - * Since the returned value of calling the $http function is a `promise`, you can also use - * the `then` method to register callbacks, and these callbacks will receive a single argument – - * an object representing the response. See the API signature and type info below for more - * details. + * The response object has these properties: * - * A response status code between 200 and 299 is considered a success status and - * will result in the success callback being called. Note that if the response is a redirect, - * XMLHttpRequest will transparently follow it, meaning that the error callback will not be - * called for such responses. + * - **data** – `{string|Object}` – The response body transformed with the transform + * functions. + * - **status** – `{number}` – HTTP status code of the response. + * - **headers** – `{function([headerName])}` – Header getter function. + * - **config** – `{Object}` – The configuration object that was used to generate the request. + * - **statusText** – `{string}` – HTTP status text of the response. + * - **xhrStatus** – `{string}` – Status of the XMLHttpRequest (`complete`, `error`, `timeout` or `abort`). * - * # Writing Unit Tests that use $http - * When unit testing (using {@link ngMock ngMock}), it is necessary to call - * {@link ngMock.$httpBackend#flush $httpBackend.flush()} to flush each pending - * request using trained responses. + * A response status code between 200 and 299 is considered a success status and will result in + * the success callback being called. Any response status code outside of that range is + * considered an error status and will result in the error callback being called. + * Also, status codes less than -1 are normalized to zero. -1 usually means the request was + * aborted, e.g. using a `config.timeout`. + * Note that if the response is a redirect, XMLHttpRequest will transparently follow it, meaning + * that the outcome (success or error) will be determined by the final response status code. * - * ``` - * $httpBackend.expectGET(...); - * $http.get(...); - * $httpBackend.flush(); - * ``` * - * # Shortcut methods + * ## Shortcut methods * * Shortcut methods are also available. All shortcut methods require passing in the URL, and - * request data must be passed in for POST/PUT requests. + * request data must be passed in for POST/PUT requests. An optional config can be passed as the + * last argument. * * ```js - * $http.get('/someUrl').success(successCallback); - * $http.post('/someUrl', data).success(successCallback); + * $http.get('/someUrl', config).then(successCallback, errorCallback); + * $http.post('/someUrl', data, config).then(successCallback, errorCallback); * ``` * * Complete list of shortcut methods: @@ -7470,16 +11731,28 @@ * - {@link ng.$http#put $http.put} * - {@link ng.$http#delete $http.delete} * - {@link ng.$http#jsonp $http.jsonp} + * - {@link ng.$http#patch $http.patch} + * * + * ## Writing Unit Tests that use $http + * When unit testing (using {@link ngMock ngMock}), it is necessary to call + * {@link ngMock.$httpBackend#flush $httpBackend.flush()} to flush each pending + * request using trained responses. + * + * ``` + * $httpBackend.expectGET(...); + * $http.get(...); + * $httpBackend.flush(); + * ``` * - * # Setting HTTP Headers + * ## Setting HTTP Headers * * The $http service will automatically add certain HTTP headers to all requests. These defaults * can be fully configured by accessing the `$httpProvider.defaults.headers` configuration * object, which currently contains this default configuration: * * - `$httpProvider.defaults.headers.common` (headers that are common for all requests): - * - `Accept: application/json, text/plain, * / *` + * - Accept: application/json, text/plain, \*/\* * - `$httpProvider.defaults.headers.post`: (header defaults for POST requests) * - `Content-Type: application/json` * - `$httpProvider.defaults.headers.put` (header defaults for PUT requests) @@ -7488,74 +11761,143 @@ * To add or overwrite these defaults, simply add or remove a property from these configuration * objects. To add headers for an HTTP method other than POST or PUT, simply add a new object * with the lowercased HTTP method name as the key, e.g. - * `$httpProvider.defaults.headers.get = { 'My-Header' : 'value' }. + * `$httpProvider.defaults.headers.get = { 'My-Header' : 'value' }`. * * The defaults can also be set at runtime via the `$http.defaults` object in the same * fashion. For example: * * ``` * module.run(function($http) { - * $http.defaults.headers.common.Authorization = 'Basic YmVlcDpib29w' - * }); + * $http.defaults.headers.common.Authorization = 'Basic YmVlcDpib29w'; + * }); * ``` * * In addition, you can supply a `headers` property in the config object passed when * calling `$http(config)`, which overrides the defaults without changing them globally. * + * To explicitly remove a header automatically added via $httpProvider.defaults.headers on a per request basis, + * Use the `headers` property, setting the desired header to `undefined`. For example: + * + * ```js + * var req = { + * method: 'POST', + * url: 'http://example.com', + * headers: { + * 'Content-Type': undefined + * }, + * data: { test: 'test' } + * } + * + * $http(req).then(function(){...}, function(){...}); + * ``` + * + * ## Transforming Requests and Responses + * + * Both requests and responses can be transformed using transformation functions: `transformRequest` + * and `transformResponse`. These properties can be a single function that returns + * the transformed value (`function(data, headersGetter, status)`) or an array of such transformation functions, + * which allows you to `push` or `unshift` a new transformation function into the transformation chain. * - * # Transforming Requests and Responses + *
    + * **Note:** AngularJS does not make a copy of the `data` parameter before it is passed into the `transformRequest` pipeline. + * That means changes to the properties of `data` are not local to the transform function (since Javascript passes objects by reference). + * For example, when calling `$http.get(url, $scope.myObject)`, modifications to the object's properties in a transformRequest + * function will be reflected on the scope and in any templates where the object is data-bound. + * To prevent this, transform functions should have no side-effects. + * If you need to modify properties, it is recommended to make a copy of the data, or create new object to return. + *
    + * + * ### Default Transformations + * + * The `$httpProvider` provider and `$http` service expose `defaults.transformRequest` and + * `defaults.transformResponse` properties. If a request does not provide its own transformations + * then these will be applied. * - * Both requests and responses can be transformed using transform functions. By default, Angular - * applies these transformations: + * You can augment or replace the default transformations by modifying these properties by adding to or + * replacing the array. * - * Request transformations: + * AngularJS provides the following default transformations: + * + * Request transformations (`$httpProvider.defaults.transformRequest` and `$http.defaults.transformRequest`) is + * an array with one function that does the following: * * - If the `data` property of the request configuration object contains an object, serialize it * into JSON format. * - * Response transformations: + * Response transformations (`$httpProvider.defaults.transformResponse` and `$http.defaults.transformResponse`) is + * an array with one function that does the following: * * - If XSRF prefix is detected, strip it (see Security Considerations section below). - * - If JSON response is detected, deserialize it using a JSON parser. + * - If the `Content-Type` is `application/json` or the response looks like JSON, + * deserialize it using a JSON parser. + * * - * To globally augment or override the default transforms, modify the - * `$httpProvider.defaults.transformRequest` and `$httpProvider.defaults.transformResponse` - * properties. These properties are by default an array of transform functions, which allows you - * to `push` or `unshift` a new transformation function into the transformation chain. You can - * also decide to completely override any default transformations by assigning your - * transformation functions to these properties directly without the array wrapper. These defaults - * are again available on the $http factory at run-time, which may be useful if you have run-time - * services you wish to be involved in your transformations. + * ### Overriding the Default Transformations Per Request * - * Similarly, to locally override the request/response transforms, augment the - * `transformRequest` and/or `transformResponse` properties of the configuration object passed + * If you wish to override the request/response transformations only for a single request then provide + * `transformRequest` and/or `transformResponse` properties on the configuration object passed * into `$http`. * + * Note that if you provide these properties on the config object the default transformations will be + * overwritten. If you wish to augment the default transformations then you must include them in your + * local transformation array. + * + * The following code demonstrates adding a new response transformation to be run after the default response + * transformations have been run. + * + * ```js + * function appendTransform(defaults, transform) { + * + * // We can't guarantee that the default transformation is an array + * defaults = angular.isArray(defaults) ? defaults : [defaults]; + * + * // Append the new transformation to the defaults + * return defaults.concat(transform); + * } + * + * $http({ + * url: '...', + * method: 'GET', + * transformResponse: appendTransform($http.defaults.transformResponse, function(value) { + * return doTransform(value); + * }) + * }); + * ``` + * + * + * ## Caching + * + * {@link ng.$http `$http`} responses are not cached by default. To enable caching, you must + * set the config.cache value or the default cache value to TRUE or to a cache object (created + * with {@link ng.$cacheFactory `$cacheFactory`}). If defined, the value of config.cache takes + * precedence over the default cache value. * - * # Caching + * In order to: + * * cache all responses - set the default cache value to TRUE or to a cache object + * * cache a specific response - set config.cache value to TRUE or to a cache object * - * To enable caching, set the request configuration `cache` property to `true` (to use default - * cache) or to a custom cache object (built with {@link ng.$cacheFactory `$cacheFactory`}). - * When the cache is enabled, `$http` stores the response from the server in the specified - * cache. The next time the same request is made, the response is served from the cache without - * sending a request to the server. + * If caching is enabled, but neither the default cache nor config.cache are set to a cache object, + * then the default `$cacheFactory("$http")` object is used. * - * Note that even if the response is served from cache, delivery of the data is asynchronous in - * the same way that real requests are. + * The default cache value can be set by updating the + * {@link ng.$http#defaults `$http.defaults.cache`} property or the + * {@link $httpProvider#defaults `$httpProvider.defaults.cache`} property. * - * If there are multiple GET requests for the same URL that should be cached using the same - * cache, but the cache is not populated yet, only one request to the server will be made and - * the remaining requests will be fulfilled using the response from the first request. + * When caching is enabled, {@link ng.$http `$http`} stores the response from the server using + * the relevant cache object. The next time the same request is made, the response is returned + * from the cache without sending a request to the server. * - * You can change the default cache to a new object (built with - * {@link ng.$cacheFactory `$cacheFactory`}) by updating the - * {@link ng.$http#properties_defaults `$http.defaults.cache`} property. All requests who set - * their `cache` property to `true` will now use this cache object. + * Take note that: * - * If you set the default cache to `false` then only requests that specify their own custom - * cache object will be cached. + * * Only GET and JSONP requests are cached. + * * The cache key is the request URL including search parameters; headers are not considered. + * * Cached responses are returned asynchronously, in the same way as responses from the server. + * * If multiple identical requests are made using the same cache, which is not yet populated, + * one request will be made to the server and remaining requests will return the same response. + * * A cache-control header on the response does not affect if or how responses are cached. * - * # Interceptors + * + * ## Interceptors * * Before you start creating interceptors, be sure to understand the * {@link ng.$q $q and deferred/promise APIs}. @@ -7573,14 +11915,14 @@ * * There are two kinds of interceptors (and two kinds of rejection interceptors): * - * * `request`: interceptors get called with http `config` object. The function is free to - * modify the `config` or create a new one. The function needs to return the `config` - * directly or as a promise. + * * `request`: interceptors get called with a http {@link $http#usage config} object. The function is free to + * modify the `config` object or create a new one. The function needs to return the `config` + * object directly, or a promise containing the `config` or a new `config` object. * * `requestError`: interceptor gets called when a previous interceptor threw an error or * resolved with a rejection. * * `response`: interceptors get called with http `response` object. The function is free to - * modify the `response` or create a new one. The function needs to return the `response` - * directly or as a promise. + * modify the `response` object or create a new one. The function needs to return the `response` + * object directly, or as a promise containing the `response` or a new `response` object. * * `responseError`: interceptor gets called when a previous interceptor threw an error or * resolved with a rejection. * @@ -7588,121 +11930,76 @@ * ```js * // register the interceptor as a service * $provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) { - * return { - * // optional method - * 'request': function(config) { - * // do something on success - * return config || $q.when(config); - * }, - * - * // optional method - * 'requestError': function(rejection) { - * // do something on error - * if (canRecover(rejection)) { - * return responseOrNewPromise - * } - * return $q.reject(rejection); - * }, - * - * - * - * // optional method - * 'response': function(response) { - * // do something on success - * return response || $q.when(response); - * }, - * - * // optional method - * 'responseError': function(rejection) { - * // do something on error - * if (canRecover(rejection)) { - * return responseOrNewPromise - * } - * return $q.reject(rejection); - * } - * }; - * }); + * return { + * // optional method + * 'request': function(config) { + * // do something on success + * return config; + * }, + * + * // optional method + * 'requestError': function(rejection) { + * // do something on error + * if (canRecover(rejection)) { + * return responseOrNewPromise + * } + * return $q.reject(rejection); + * }, + * + * + * + * // optional method + * 'response': function(response) { + * // do something on success + * return response; + * }, + * + * // optional method + * 'responseError': function(rejection) { + * // do something on error + * if (canRecover(rejection)) { + * return responseOrNewPromise + * } + * return $q.reject(rejection); + * } + * }; + * }); * * $httpProvider.interceptors.push('myHttpInterceptor'); * * * // alternatively, register the interceptor via an anonymous factory * $httpProvider.interceptors.push(function($q, dependency1, dependency2) { - * return { - * 'request': function(config) { - * // same as above - * }, - * - * 'response': function(response) { - * // same as above - * } - * }; - * }); + * return { + * 'request': function(config) { + * // same as above + * }, + * + * 'response': function(response) { + * // same as above + * } + * }; + * }); * ``` * - * # Response interceptors (DEPRECATED) + * ## Security Considerations * - * Before you start creating interceptors, be sure to understand the - * {@link ng.$q $q and deferred/promise APIs}. + * When designing web applications, consider security threats from: * - * For purposes of global error handling, authentication or any kind of synchronous or - * asynchronous preprocessing of received responses, it is desirable to be able to intercept - * responses for http requests before they are handed over to the application code that - * initiated these requests. The response interceptors leverage the {@link ng.$q - * promise apis} to fulfil this need for both synchronous and asynchronous preprocessing. + * - [JSON vulnerability](http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx) + * - [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) * - * The interceptors are service factories that are registered with the $httpProvider by - * adding them to the `$httpProvider.responseInterceptors` array. The factory is called and - * injected with dependencies (if specified) and returns the interceptor — a function that - * takes a {@link ng.$q promise} and returns the original or a new promise. + * Both server and the client must cooperate in order to eliminate these threats. AngularJS comes + * pre-configured with strategies that address these issues, but for this to work backend server + * cooperation is required. * - * ```js - * // register the interceptor as a service - * $provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) { - * return function(promise) { - * return promise.then(function(response) { - * // do something on success - * return response; - * }, function(response) { - * // do something on error - * if (canRecover(response)) { - * return responseOrNewPromise - * } - * return $q.reject(response); - * }); - * } - * }); - * - * $httpProvider.responseInterceptors.push('myHttpInterceptor'); - * - * - * // register the interceptor via an anonymous factory - * $httpProvider.responseInterceptors.push(function($q, dependency1, dependency2) { - * return function(promise) { - * // same as above - * } - * }); - * ``` - * - * - * # Security Considerations - * - * When designing web applications, consider security threats from: - * - * - [JSON vulnerability](http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx) - * - [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) - * - * Both server and the client must cooperate in order to eliminate these threats. Angular comes - * pre-configured with strategies that address these issues, but for this to work backend server - * cooperation is required. - * - * ## JSON Vulnerability Protection + * ### JSON Vulnerability Protection * * A [JSON vulnerability](http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx) * allows third party website to turn your JSON resource URL into * [JSONP](http://en.wikipedia.org/wiki/JSONP) request under some conditions. To * counter this your server can prefix all JSON requests with following string `")]}',\n"`. - * Angular will automatically strip the prefix before processing it as JSON. + * AngularJS will automatically strip the prefix before processing it as JSON. * * For example if your server needs to return: * ```js @@ -7715,18 +12012,18 @@ * ['one','two'] * ``` * - * Angular will strip the prefix, before processing the JSON. + * AngularJS will strip the prefix, before processing the JSON. * * - * ## Cross Site Request Forgery (XSRF) Protection + * ### Cross Site Request Forgery (XSRF) Protection * - * [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) is a technique by which - * an unauthorized site can gain your user's private data. Angular provides a mechanism - * to counter XSRF. When performing XHR requests, the $http service reads a token from a cookie - * (by default, `XSRF-TOKEN`) and sets it as an HTTP header (`X-XSRF-TOKEN`). Since only - * JavaScript that runs on your domain could read the cookie, your server can be assured that - * the XHR came from JavaScript running on your domain. The header will not be set for - * cross-domain requests. + * [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) is an attack technique by + * which the attacker can trick an authenticated user into unknowingly executing actions on your + * website. AngularJS provides a mechanism to counter XSRF. When performing XHR requests, the + * $http service reads a token from a cookie (by default, `XSRF-TOKEN`) and sets it as an HTTP + * header (`X-XSRF-TOKEN`). Since only JavaScript that runs on your domain could read the + * cookie, your server can be assured that the XHR came from JavaScript running on your domain. + * The header will not be set for cross-domain requests. * * To take advantage of this, your server needs to set a token in a JavaScript readable session * cookie called `XSRF-TOKEN` on the first HTTP GET request. On subsequent XHR requests the @@ -7734,85 +12031,92 @@ * that only JavaScript running on your domain could have sent the request. The token must be * unique for each user and must be verifiable by the server (to prevent the JavaScript from * making up its own tokens). We recommend that the token is a digest of your site's - * authentication cookie with a [salt](https://en.wikipedia.org/wiki/Salt_(cryptography)) + * authentication cookie with a [salt](https://en.wikipedia.org/wiki/Salt_(cryptography)) * for added security. * * The name of the headers can be specified using the xsrfHeaderName and xsrfCookieName * properties of either $httpProvider.defaults at config-time, $http.defaults at run-time, * or the per-request config object. * + * In order to prevent collisions in environments where multiple AngularJS apps share the + * same domain or subdomain, we recommend that each application uses unique cookie name. * * @param {object} config Object describing the request to be made and how it should be * processed. The object has following properties: * * - **method** – `{string}` – HTTP method (e.g. 'GET', 'POST', etc) - * - **url** – `{string}` – Absolute or relative URL of the resource that is being requested. - * - **params** – `{Object.}` – Map of strings or objects which will be turned - * to `?key1=value1&key2=value2` after the url. If the value is not a string, it will be - * JSONified. + * - **url** – `{string|TrustedObject}` – Absolute or relative URL of the resource that is being requested; + * or an object created by a call to `$sce.trustAsResourceUrl(url)`. + * - **params** – `{Object.}` – Map of strings or objects which will be serialized + * with the `paramSerializer` and appended as GET parameters. * - **data** – `{string|Object}` – Data to be sent as the request message data. * - **headers** – `{Object}` – Map of strings or functions which return strings representing * HTTP headers to send to the server. If the return value of a function is null, the - * header will not be sent. + * header will not be sent. Functions accept a config object as an argument. + * - **eventHandlers** - `{Object}` - Event listeners to be bound to the XMLHttpRequest object. + * To bind events to the XMLHttpRequest upload object, use `uploadEventHandlers`. + * The handler will be called in the context of a `$apply` block. + * - **uploadEventHandlers** - `{Object}` - Event listeners to be bound to the XMLHttpRequest upload + * object. To bind events to the XMLHttpRequest object, use `eventHandlers`. + * The handler will be called in the context of a `$apply` block. * - **xsrfHeaderName** – `{string}` – Name of HTTP header to populate with the XSRF token. * - **xsrfCookieName** – `{string}` – Name of cookie containing the XSRF token. * - **transformRequest** – * `{function(data, headersGetter)|Array.}` – * transform function or an array of such functions. The transform function takes the http * request body and headers and returns its transformed (typically serialized) version. + * See {@link ng.$http#overriding-the-default-transformations-per-request + * Overriding the Default Transformations} * - **transformResponse** – - * `{function(data, headersGetter)|Array.}` – + * `{function(data, headersGetter, status)|Array.}` – * transform function or an array of such functions. The transform function takes the http - * response body and headers and returns its transformed (typically deserialized) version. - * - **cache** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the - * GET request, otherwise if a cache instance built with - * {@link ng.$cacheFactory $cacheFactory}, this cache will be used for - * caching. + * response body, headers and status and returns its transformed (typically deserialized) version. + * See {@link ng.$http#overriding-the-default-transformations-per-request + * Overriding the Default Transformations} + * - **paramSerializer** - `{string|function(Object):string}` - A function used to + * prepare the string representation of request parameters (specified as an object). + * If specified as string, it is interpreted as function registered with the + * {@link $injector $injector}, which means you can create your own serializer + * by registering it as a {@link auto.$provide#service service}. + * The default serializer is the {@link $httpParamSerializer $httpParamSerializer}; + * alternatively, you can use the {@link $httpParamSerializerJQLike $httpParamSerializerJQLike} + * - **cache** – `{boolean|Object}` – A boolean value or object created with + * {@link ng.$cacheFactory `$cacheFactory`} to enable or disable caching of the HTTP response. + * See {@link $http#caching $http Caching} for more information. * - **timeout** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise} * that should abort the request when resolved. * - **withCredentials** - `{boolean}` - whether to set the `withCredentials` flag on the - * XHR object. See [requests with credentials]https://developer.mozilla.org/en/http_access_control#section_5 + * XHR object. See [requests with credentials](https://developer.mozilla.org/docs/Web/HTTP/Access_control_CORS#Requests_with_credentials) * for more information. * - **responseType** - `{string}` - see - * [requestType](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType). + * [XMLHttpRequest.responseType](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest#xmlhttprequest-responsetype). * - * @returns {HttpPromise} Returns a {@link ng.$q promise} object with the - * standard `then` method and two http specific methods: `success` and `error`. The `then` - * method takes two arguments a success and an error callback which will be called with a - * response object. The `success` and `error` methods take a single argument - a function that - * will be called when the request succeeds or fails respectively. The arguments passed into - * these functions are destructured representation of the response object passed into the - * `then` method. The response object has these properties: + * @returns {HttpPromise} Returns a {@link ng.$q `Promise}` that will be resolved to a response object + * when the request succeeds or fails. * - * - **data** – `{string|Object}` – The response body transformed with the transform - * functions. - * - **status** – `{number}` – HTTP status code of the response. - * - **headers** – `{function([headerName])}` – Header getter function. - * - **config** – `{Object}` – The configuration object that was used to generate the request. - * - **statusText** – `{string}` – HTTP status text of the response. * * @property {Array.} pendingRequests Array of config objects for currently pending * requests. This is primarily meant to be used for debugging purposes. * * * @example - + -
    - - +
    http status code: {{status}}
    @@ -7820,30 +12124,38 @@
    - function FetchCtrl($scope, $http, $templateCache) { - $scope.method = 'GET'; - $scope.url = 'http-hello.html'; - - $scope.fetch = function() { - $scope.code = null; - $scope.response = null; - - $http({method: $scope.method, url: $scope.url, cache: $templateCache}). - success(function(data, status) { - $scope.status = status; - $scope.data = data; - }). - error(function(data, status) { - $scope.data = data || "Request failed"; - $scope.status = status; - }); - }; + angular.module('httpExample', []) + .config(['$sceDelegateProvider', function($sceDelegateProvider) { + // We must whitelist the JSONP endpoint that we are using to show that we trust it + $sceDelegateProvider.resourceUrlWhitelist([ + 'self', + 'https://angularjs.org/**' + ]); + }]) + .controller('FetchController', ['$scope', '$http', '$templateCache', + function($scope, $http, $templateCache) { + $scope.method = 'GET'; + $scope.url = 'http-hello.html'; + + $scope.fetch = function() { + $scope.code = null; + $scope.response = null; + + $http({method: $scope.method, url: $scope.url, cache: $templateCache}). + then(function(response) { + $scope.status = response.status; + $scope.data = response.data; + }, function(response) { + $scope.data = response.data || 'Request failed'; + $scope.status = response.status; + }); + }; - $scope.updateModel = function(method, url) { - $scope.method = method; - $scope.url = url; - }; - } + $scope.updateModel = function(method, url) { + $scope.method = method; + $scope.url = url; + }; + }]); Hello, $http! @@ -7853,7 +12165,6 @@ var data = element(by.binding('data')); var fetchBtn = element(by.id('fetchbtn')); var sampleGetBtn = element(by.id('samplegetbtn')); - var sampleJsonpBtn = element(by.id('samplejsonpbtn')); var invalidJsonpBtn = element(by.id('invalidjsonpbtn')); it('should make an xhr GET request', function() { @@ -7863,12 +12174,14 @@ expect(data.getText()).toMatch(/Hello, \$http!/); }); - it('should make a JSONP request to angularjs.org', function() { - sampleJsonpBtn.click(); - fetchBtn.click(); - expect(status.getText()).toMatch('200'); - expect(data.getText()).toMatch(/Super Hero!/); - }); + // Commented out due to flakes. See https://github.com/angular/angular.js/issues/9185 + // it('should make a JSONP request to angularjs.org', function() { +// var sampleJsonpBtn = element(by.id('samplejsonpbtn')); +// sampleJsonpBtn.click(); +// fetchBtn.click(); +// expect(status.getText()).toMatch('200'); +// expect(data.getText()).toMatch(/Super Hero!/); +// }); it('should make JSONP request to invalid URL and invoke the error handler', function() { @@ -7881,90 +12194,84 @@
    */ function $http(requestConfig) { - var config = { - method: 'get', - transformRequest: defaults.transformRequest, - transformResponse: defaults.transformResponse - }; - var headers = mergeHeaders(requestConfig); - - extend(config, requestConfig); - config.headers = headers; - config.method = uppercase(config.method); - var xsrfValue = urlIsSameOrigin(config.url) - ? $browser.cookies()[config.xsrfCookieName || defaults.xsrfCookieName] - : undefined; - if (xsrfValue) { - headers[(config.xsrfHeaderName || defaults.xsrfHeaderName)] = xsrfValue; + if (!isObject(requestConfig)) { + throw minErr('$http')('badreq', 'Http request configuration must be an object. Received: {0}', requestConfig); } + if (!isString($sce.valueOf(requestConfig.url))) { + throw minErr('$http')('badreq', 'Http request configuration url must be a string or a $sce trusted object. Received: {0}', requestConfig.url); + } - var serverRequest = function(config) { - headers = config.headers; - var reqData = transformData(config.data, headersGetter(headers), config.transformRequest); - - // strip content-type if data is undefined - if (isUndefined(config.data)) { - forEach(headers, function(value, header) { - if (lowercase(header) === 'content-type') { - delete headers[header]; - } - }); - } + var config = extend({ + method: 'get', + transformRequest: defaults.transformRequest, + transformResponse: defaults.transformResponse, + paramSerializer: defaults.paramSerializer, + jsonpCallbackParam: defaults.jsonpCallbackParam + }, requestConfig); - if (isUndefined(config.withCredentials) && !isUndefined(defaults.withCredentials)) { - config.withCredentials = defaults.withCredentials; - } + config.headers = mergeHeaders(requestConfig); + config.method = uppercase(config.method); + config.paramSerializer = isString(config.paramSerializer) ? + $injector.get(config.paramSerializer) : config.paramSerializer; - // send request - return sendReq(config, reqData, headers).then(transformResponse, transformResponse); - }; + $browser.$$incOutstandingRequestCount(); - var chain = [serverRequest, undefined]; - var promise = $q.when(config); + var requestInterceptors = []; + var responseInterceptors = []; + var promise = $q.resolve(config); // apply interceptors forEach(reversedInterceptors, function(interceptor) { if (interceptor.request || interceptor.requestError) { - chain.unshift(interceptor.request, interceptor.requestError); + requestInterceptors.unshift(interceptor.request, interceptor.requestError); } if (interceptor.response || interceptor.responseError) { - chain.push(interceptor.response, interceptor.responseError); + responseInterceptors.push(interceptor.response, interceptor.responseError); } }); - while(chain.length) { - var thenFn = chain.shift(); - var rejectFn = chain.shift(); + promise = chainInterceptors(promise, requestInterceptors); + promise = promise.then(serverRequest); + promise = chainInterceptors(promise, responseInterceptors); + promise = promise.finally(completeOutstandingRequest); - promise = promise.then(thenFn, rejectFn); - } + return promise; - promise.success = function(fn) { - promise.then(function(response) { - fn(response.data, response.status, response.headers, config); - }); - return promise; - }; - promise.error = function(fn) { - promise.then(null, function(response) { - fn(response.data, response.status, response.headers, config); - }); + function chainInterceptors(promise, interceptors) { + for (var i = 0, ii = interceptors.length; i < ii;) { + var thenFn = interceptors[i++]; + var rejectFn = interceptors[i++]; + + promise = promise.then(thenFn, rejectFn); + } + + interceptors.length = 0; + return promise; - }; + } - return promise; + function completeOutstandingRequest() { + $browser.$$completeOutstandingRequest(noop); + } - function transformResponse(response) { - // make a copy since the response must be cacheable - var resp = extend({}, response, { - data: transformData(response.data, response.headers, config.transformResponse) + function executeHeaderFns(headers, config) { + var headerContent, processedHeaders = {}; + + forEach(headers, function(headerFn, header) { + if (isFunction(headerFn)) { + headerContent = headerFn(config); + if (headerContent != null) { + processedHeaders[header] = headerContent; + } + } else { + processedHeaders[header] = headerFn; + } }); - return (isSuccess(response.status)) - ? resp - : $q.reject(resp); + + return processedHeaders; } function mergeHeaders(config) { @@ -7974,11 +12281,7 @@ defHeaders = extend({}, defHeaders.common, defHeaders[lowercase(config.method)]); - // execute if header value is function - execHeaders(defHeaders); - execHeaders(reqHeaders); - - // using for-in instead of forEach to avoid unecessary iteration after header has been found + // using for-in instead of forEach to avoid unnecessary iteration after header has been found defaultHeadersIteration: for (defHeaderName in defHeaders) { lowercaseDefHeaderName = lowercase(defHeaderName); @@ -7992,22 +12295,39 @@ reqHeaders[defHeaderName] = defHeaders[defHeaderName]; } - return reqHeaders; + // execute if header value is a function for merged headers + return executeHeaderFns(reqHeaders, shallowCopy(config)); + } - function execHeaders(headers) { - var headerContent; + function serverRequest(config) { + var headers = config.headers; + var reqData = transformData(config.data, headersGetter(headers), undefined, config.transformRequest); - forEach(headers, function(headerFn, header) { - if (isFunction(headerFn)) { - headerContent = headerFn(); - if (headerContent != null) { - headers[header] = headerContent; - } else { - delete headers[header]; - } + // strip content-type if data is undefined + if (isUndefined(reqData)) { + forEach(headers, function(value, header) { + if (lowercase(header) === 'content-type') { + delete headers[header]; } }); } + + if (isUndefined(config.withCredentials) && !isUndefined(defaults.withCredentials)) { + config.withCredentials = defaults.withCredentials; + } + + // send request + return sendReq(config, reqData).then(transformResponse, transformResponse); + } + + function transformResponse(response) { + // make a copy since the response must be cacheable + var resp = extend({}, response); + resp.data = transformData(response.data, response.headers, response.status, + config.transformResponse); + return (isSuccess(response.status)) + ? resp + : $q.reject(resp); } } @@ -8020,8 +12340,9 @@ * @description * Shortcut method to perform `GET` request. * - * @param {string} url Relative or absolute URL specifying the destination of the request - * @param {Object=} config Optional configuration object + * @param {string|TrustedObject} url Absolute or relative URL of the resource that is being requested; + * or an object created by a call to `$sce.trustAsResourceUrl(url)`. + * @param {Object=} config Optional configuration object. See https://docs.angularjs.org/api/ng/service/$http#usage * @returns {HttpPromise} Future object */ @@ -8032,8 +12353,9 @@ * @description * Shortcut method to perform `DELETE` request. * - * @param {string} url Relative or absolute URL specifying the destination of the request - * @param {Object=} config Optional configuration object + * @param {string|TrustedObject} url Absolute or relative URL of the resource that is being requested; + * or an object created by a call to `$sce.trustAsResourceUrl(url)`. + * @param {Object=} config Optional configuration object. See https://docs.angularjs.org/api/ng/service/$http#usage * @returns {HttpPromise} Future object */ @@ -8044,8 +12366,9 @@ * @description * Shortcut method to perform `HEAD` request. * - * @param {string} url Relative or absolute URL specifying the destination of the request - * @param {Object=} config Optional configuration object + * @param {string|TrustedObject} url Absolute or relative URL of the resource that is being requested; + * or an object created by a call to `$sce.trustAsResourceUrl(url)`. + * @param {Object=} config Optional configuration object. See https://docs.angularjs.org/api/ng/service/$http#usage * @returns {HttpPromise} Future object */ @@ -8056,9 +12379,38 @@ * @description * Shortcut method to perform `JSONP` request. * - * @param {string} url Relative or absolute URL specifying the destination of the request. - * Should contain `JSON_CALLBACK` string. - * @param {Object=} config Optional configuration object + * Note that, since JSONP requests are sensitive because the response is given full access to the browser, + * the url must be declared, via {@link $sce} as a trusted resource URL. + * You can trust a URL by adding it to the whitelist via + * {@link $sceDelegateProvider#resourceUrlWhitelist `$sceDelegateProvider.resourceUrlWhitelist`} or + * by explicitly trusting the URL via {@link $sce#trustAsResourceUrl `$sce.trustAsResourceUrl(url)`}. + * + * You should avoid generating the URL for the JSONP request from user provided data. + * Provide additional query parameters via `params` property of the `config` parameter, rather than + * modifying the URL itself. + * + * JSONP requests must specify a callback to be used in the response from the server. This callback + * is passed as a query parameter in the request. You must specify the name of this parameter by + * setting the `jsonpCallbackParam` property on the request config object. + * + * ``` + * $http.jsonp('some/trusted/url', {jsonpCallbackParam: 'callback'}) + * ``` + * + * You can also specify a default callback parameter name in `$http.defaults.jsonpCallbackParam`. + * Initially this is set to `'callback'`. + * + *
    + * You can no longer use the `JSON_CALLBACK` string as a placeholder for specifying where the callback + * parameter value should go. + *
    + * + * If you would like to customise where and how the callbacks are stored then try overriding + * or decorating the {@link $jsonpCallbacks} service. + * + * @param {string|TrustedObject} url Absolute or relative URL of the resource that is being requested; + * or an object created by a call to `$sce.trustAsResourceUrl(url)`. + * @param {Object=} config Optional configuration object. See https://docs.angularjs.org/api/ng/service/$http#usage * @returns {HttpPromise} Future object */ createShortMethods('get', 'delete', 'head', 'jsonp'); @@ -8072,7 +12424,7 @@ * * @param {string} url Relative or absolute URL specifying the destination of the request * @param {*} data Request content - * @param {Object=} config Optional configuration object + * @param {Object=} config Optional configuration object. See https://docs.angularjs.org/api/ng/service/$http#usage * @returns {HttpPromise} Future object */ @@ -8085,10 +12437,23 @@ * * @param {string} url Relative or absolute URL specifying the destination of the request * @param {*} data Request content - * @param {Object=} config Optional configuration object + * @param {Object=} config Optional configuration object. See https://docs.angularjs.org/api/ng/service/$http#usage + * @returns {HttpPromise} Future object + */ + + /** + * @ngdoc method + * @name $http#patch + * + * @description + * Shortcut method to perform `PATCH` request. + * + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {*} data Request content + * @param {Object=} config Optional configuration object. See https://docs.angularjs.org/api/ng/service/$http#usage * @returns {HttpPromise} Future object */ - createShortMethodsWithData('post', 'put'); + createShortMethodsWithData('post', 'put', 'patch'); /** * @ngdoc property @@ -8109,7 +12474,7 @@ function createShortMethods(names) { forEach(arguments, function(name) { $http[name] = function(url, config) { - return $http(extend(config || {}, { + return $http(extend({}, config || {}, { method: name, url: url })); @@ -8121,7 +12486,7 @@ function createShortMethodsWithData(name) { forEach(arguments, function(name) { $http[name] = function(url, data, config) { - return $http(extend(config || {}, { + return $http(extend({}, config || {}, { method: name, url: url, data: data @@ -8137,36 +12502,54 @@ * !!! ACCESSES CLOSURE VARS: * $httpBackend, defaults, $log, $rootScope, defaultCache, $http.pendingRequests */ - function sendReq(config, reqData, reqHeaders) { + function sendReq(config, reqData) { var deferred = $q.defer(), promise = deferred.promise, cache, cachedResp, - url = buildUrl(config.url, config.params); + reqHeaders = config.headers, + isJsonp = lowercase(config.method) === 'jsonp', + url = config.url; + + if (isJsonp) { + // JSONP is a pretty sensitive operation where we're allowing a script to have full access to + // our DOM and JS space. So we require that the URL satisfies SCE.RESOURCE_URL. + url = $sce.getTrustedResourceUrl(url); + } else if (!isString(url)) { + // If it is not a string then the URL must be a $sce trusted object + url = $sce.valueOf(url); + } + + url = buildUrl(url, config.paramSerializer(config.params)); + + if (isJsonp) { + // Check the url and add the JSONP callback placeholder + url = sanitizeJsonpCallbackParam(url, config.jsonpCallbackParam); + } $http.pendingRequests.push(config); promise.then(removePendingReq, removePendingReq); - - if ((config.cache || defaults.cache) && config.cache !== false && config.method == 'GET') { + if ((config.cache || defaults.cache) && config.cache !== false && + (config.method === 'GET' || config.method === 'JSONP')) { cache = isObject(config.cache) ? config.cache - : isObject(defaults.cache) ? defaults.cache - : defaultCache; + : isObject(/** @type {?} */ (defaults).cache) + ? /** @type {?} */ (defaults).cache + : defaultCache; } if (cache) { cachedResp = cache.get(url); if (isDefined(cachedResp)) { - if (cachedResp.then) { + if (isPromiseLike(cachedResp)) { // cached request has already been sent, but there is no response yet - cachedResp.then(removePendingReq, removePendingReq); - return cachedResp; + cachedResp.then(resolvePromiseWithResult, resolvePromiseWithResult); } else { // serving from cache if (isArray(cachedResp)) { - resolvePromise(cachedResp[1], cachedResp[0], copy(cachedResp[2]), cachedResp[3]); + resolvePromise(cachedResp[1], cachedResp[0], shallowCopy(cachedResp[2]), cachedResp[3], cachedResp[4]); } else { - resolvePromise(cachedResp, 200, {}, 'OK'); + resolvePromise(cachedResp, 200, {}, 'OK', 'complete'); } } } else { @@ -8175,14 +12558,47 @@ } } - // if we won't have the response in cache, send the request to the backend + + // if we won't have the response in cache, set the xsrf headers and + // send the request to the backend if (isUndefined(cachedResp)) { + var xsrfValue = urlIsSameOrigin(config.url) + ? $$cookieReader()[config.xsrfCookieName || defaults.xsrfCookieName] + : undefined; + if (xsrfValue) { + reqHeaders[(config.xsrfHeaderName || defaults.xsrfHeaderName)] = xsrfValue; + } + $httpBackend(config.method, url, reqData, done, reqHeaders, config.timeout, - config.withCredentials, config.responseType); + config.withCredentials, config.responseType, + createApplyHandlers(config.eventHandlers), + createApplyHandlers(config.uploadEventHandlers)); } return promise; + function createApplyHandlers(eventHandlers) { + if (eventHandlers) { + var applyHandlers = {}; + forEach(eventHandlers, function(eventHandler, key) { + applyHandlers[key] = function(event) { + if (useApplyAsync) { + $rootScope.$applyAsync(callEventHandler); + } else if ($rootScope.$$phase) { + callEventHandler(); + } else { + $rootScope.$apply(callEventHandler); + } + + function callEventHandler() { + eventHandler(event); + } + }; + }); + return applyHandlers; + } + } + /** * Callback registered to $httpBackend(): @@ -8190,89 +12606,127 @@ * - resolves the raw $http promise * - calls $apply */ - function done(status, response, headersString, statusText) { + function done(status, response, headersString, statusText, xhrStatus) { if (cache) { if (isSuccess(status)) { - cache.put(url, [status, response, parseHeaders(headersString), statusText]); + cache.put(url, [status, response, parseHeaders(headersString), statusText, xhrStatus]); } else { // remove promise from the cache cache.remove(url); } } - resolvePromise(response, status, headersString, statusText); - if (!$rootScope.$$phase) $rootScope.$apply(); + function resolveHttpPromise() { + resolvePromise(response, status, headersString, statusText, xhrStatus); + } + + if (useApplyAsync) { + $rootScope.$applyAsync(resolveHttpPromise); + } else { + resolveHttpPromise(); + if (!$rootScope.$$phase) $rootScope.$apply(); + } } /** * Resolves the raw $http promise. */ - function resolvePromise(response, status, headers, statusText) { - // normalize internal statuses to 0 - status = Math.max(status, 0); + function resolvePromise(response, status, headers, statusText, xhrStatus) { + //status: HTTP response status code, 0, -1 (aborted by timeout / promise) + status = status >= -1 ? status : 0; (isSuccess(status) ? deferred.resolve : deferred.reject)({ data: response, status: status, headers: headersGetter(headers), config: config, - statusText : statusText + statusText: statusText, + xhrStatus: xhrStatus }); } + function resolvePromiseWithResult(result) { + resolvePromise(result.data, result.status, shallowCopy(result.headers()), result.statusText, result.xhrStatus); + } function removePendingReq() { - var idx = indexOf($http.pendingRequests, config); + var idx = $http.pendingRequests.indexOf(config); if (idx !== -1) $http.pendingRequests.splice(idx, 1); } } - function buildUrl(url, params) { - if (!params) return url; - var parts = []; - forEachSorted(params, function(value, key) { - if (value === null || isUndefined(value)) return; - if (!isArray(value)) value = [value]; - - forEach(value, function(v) { - if (isObject(v)) { - v = toJson(v); - } - parts.push(encodeUriQuery(key) + '=' + - encodeUriQuery(v)); - }); - }); - if(parts.length > 0) { - url += ((url.indexOf('?') == -1) ? '?' : '&') + parts.join('&'); + function buildUrl(url, serializedParams) { + if (serializedParams.length > 0) { + url += ((url.indexOf('?') === -1) ? '?' : '&') + serializedParams; } return url; } + function sanitizeJsonpCallbackParam(url, cbKey) { + var parts = url.split('?'); + if (parts.length > 2) { + // Throw if the url contains more than one `?` query indicator + throw $httpMinErr('badjsonp', 'Illegal use more than one "?", in url, "{1}"', url); + } + var params = parseKeyValue(parts[1]); + forEach(params, function(value, key) { + if (value === 'JSON_CALLBACK') { + // Throw if the url already contains a reference to JSON_CALLBACK + throw $httpMinErr('badjsonp', 'Illegal use of JSON_CALLBACK in url, "{0}"', url); + } + if (key === cbKey) { + // Throw if the callback param was already provided + throw $httpMinErr('badjsonp', 'Illegal use of callback param, "{0}", in url, "{1}"', cbKey, url); + } + }); + + // Add in the JSON_CALLBACK callback param value + url += ((url.indexOf('?') === -1) ? '?' : '&') + cbKey + '=JSON_CALLBACK'; + return url; + } }]; } - function createXhr(method) { - //if IE and the method is not RFC2616 compliant, or if XMLHttpRequest - //is not available, try getting an ActiveXObject. Otherwise, use XMLHttpRequest - //if it is available - if (msie <= 8 && (!method.match(/^(get|post|head|put|delete|options)$/i) || - !window.XMLHttpRequest)) { - return new window.ActiveXObject("Microsoft.XMLHTTP"); - } else if (window.XMLHttpRequest) { - return new window.XMLHttpRequest(); - } - - throw minErr('$httpBackend')('noxhr', "This browser does not support XMLHttpRequest."); + /** + * @ngdoc service + * @name $xhrFactory + * @this + * + * @description + * Factory function used to create XMLHttpRequest objects. + * + * Replace or decorate this service to create your own custom XMLHttpRequest objects. + * + * ``` + * angular.module('myApp', []) + * .factory('$xhrFactory', function() { + * return function createXhr(method, url) { + * return new window.XMLHttpRequest({mozSystem: true}); + * }; + * }); + * ``` + * + * @param {string} method HTTP method of the request (GET, POST, PUT, ..) + * @param {string} url URL of the request. + */ + function $xhrFactoryProvider() { + this.$get = function() { + return function createXhr() { + return new window.XMLHttpRequest(); + }; + }; } /** * @ngdoc service * @name $httpBackend - * @requires $window + * @requires $jsonpCallbacks * @requires $document + * @requires $xhrFactory + * @this * * @description * HTTP backend used by the {@link ng.$http service} that delegates to @@ -8285,38 +12739,27 @@ * $httpBackend} which can be trained with responses. */ function $HttpBackendProvider() { - this.$get = ['$browser', '$window', '$document', function($browser, $window, $document) { - return createHttpBackend($browser, createXhr, $browser.defer, $window.angular.callbacks, $document[0]); + this.$get = ['$browser', '$jsonpCallbacks', '$document', '$xhrFactory', function($browser, $jsonpCallbacks, $document, $xhrFactory) { + return createHttpBackend($browser, $xhrFactory, $browser.defer, $jsonpCallbacks, $document[0]); }]; } function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDocument) { - var ABORTED = -1; - // TODO(vojta): fix the signature - return function(method, url, post, callback, headers, timeout, withCredentials, responseType) { - var status; - $browser.$$incOutstandingRequestCount(); + return function(method, url, post, callback, headers, timeout, withCredentials, responseType, eventHandlers, uploadEventHandlers) { url = url || $browser.url(); - if (lowercase(method) == 'jsonp') { - var callbackId = '_' + (callbacks.counter++).toString(36); - callbacks[callbackId] = function(data) { - callbacks[callbackId].data = data; - }; - - var jsonpDone = jsonpReq(url.replace('JSON_CALLBACK', 'angular.callbacks.' + callbackId), - function() { - if (callbacks[callbackId].data) { - completeRequest(callback, 200, callbacks[callbackId].data); - } else { - completeRequest(callback, status || -2); - } - callbacks[callbackId] = angular.noop; - }); + if (lowercase(method) === 'jsonp') { + var callbackPath = callbacks.createCallback(url); + var jsonpDone = jsonpReq(url, callbackPath, function(status, text) { + // jsonpReq only ever sets status to 200 (OK), 404 (ERROR) or -1 (WAITING) + var response = (status === 200) && callbacks.getResponse(callbackPath); + completeRequest(callback, status, response, '', text, 'complete'); + callbacks.removeCallback(callbackPath); + }); } else { - var xhr = createXhr(method); + var xhr = createXhr(method, url); xhr.open(method, url, true); forEach(headers, function(value, key) { @@ -8325,37 +12768,59 @@ } }); - // In IE6 and 7, this might be called synchronously when xhr.send below is called and the - // response is in the cache. the promise api will ensure that to the app code the api is - // always async - xhr.onreadystatechange = function() { - // onreadystatechange might get called multiple times with readyState === 4 on mobile webkit caused by - // xhrs that are resolved while the app is in the background (see #5426). - // since calling completeRequest sets the `xhr` variable to null, we just check if it's not null before - // continuing - // - // we can't set xhr.onreadystatechange to undefined or delete it because that breaks IE8 (method=PATCH) and - // Safari respectively. - if (xhr && xhr.readyState == 4) { - var responseHeaders = null, - response = null; - - if(status !== ABORTED) { - responseHeaders = xhr.getAllResponseHeaders(); - - // responseText is the old-school way of retrieving response (supported by IE8 & 9) - // response/responseType properties were introduced in XHR Level2 spec (supported by IE10) - response = ('response' in xhr) ? xhr.response : xhr.responseText; - } + xhr.onload = function requestLoaded() { + var statusText = xhr.statusText || ''; - completeRequest(callback, - status || xhr.status, - response, - responseHeaders, - xhr.statusText || ''); + // responseText is the old-school way of retrieving response (supported by IE9) + // response/responseType properties were introduced in XHR Level2 spec (supported by IE10) + var response = ('response' in xhr) ? xhr.response : xhr.responseText; + + // normalize IE9 bug (http://bugs.jquery.com/ticket/1450) + var status = xhr.status === 1223 ? 204 : xhr.status; + + // fix status code when it is 0 (0 status is undocumented). + // Occurs when accessing file resources or on Android 4.1 stock browser + // while retrieving files from application cache. + if (status === 0) { + status = response ? 200 : urlResolve(url).protocol === 'file' ? 404 : 0; } + + completeRequest(callback, + status, + response, + xhr.getAllResponseHeaders(), + statusText, + 'complete'); + }; + + var requestError = function() { + // The response is always empty + // See https://xhr.spec.whatwg.org/#request-error-steps and https://fetch.spec.whatwg.org/#concept-network-error + completeRequest(callback, -1, null, null, '', 'error'); + }; + + var requestAborted = function() { + completeRequest(callback, -1, null, null, '', 'abort'); + }; + + var requestTimeout = function() { + // The response is always empty + // See https://xhr.spec.whatwg.org/#request-error-steps and https://fetch.spec.whatwg.org/#concept-network-error + completeRequest(callback, -1, null, null, '', 'timeout'); }; + xhr.onerror = requestError; + xhr.onabort = requestAborted; + xhr.ontimeout = requestTimeout; + + forEach(eventHandlers, function(value, key) { + xhr.addEventListener(key, value); + }); + + forEach(uploadEventHandlers, function(value, key) { + xhr.upload.addEventListener(key, value); + }); + if (withCredentials) { xhr.withCredentials = true; } @@ -8377,87 +12842,105 @@ } } - xhr.send(post || null); + xhr.send(isUndefined(post) ? null : post); } if (timeout > 0) { var timeoutId = $browserDefer(timeoutRequest, timeout); - } else if (timeout && timeout.then) { + } else if (isPromiseLike(timeout)) { timeout.then(timeoutRequest); } function timeoutRequest() { - status = ABORTED; - jsonpDone && jsonpDone(); - xhr && xhr.abort(); + if (jsonpDone) { + jsonpDone(); + } + if (xhr) { + xhr.abort(); + } } - function completeRequest(callback, status, response, headersString, statusText) { + function completeRequest(callback, status, response, headersString, statusText, xhrStatus) { // cancel timeout and subsequent timeout promise resolution - timeoutId && $browserDefer.cancel(timeoutId); - jsonpDone = xhr = null; - - // fix status code when it is 0 (0 status is undocumented). - // Occurs when accessing file resources or on Android 4.1 stock browser - // while retrieving files from application cache. - if (status === 0) { - status = response ? 200 : urlResolve(url).protocol == 'file' ? 404 : 0; + if (isDefined(timeoutId)) { + $browserDefer.cancel(timeoutId); } + jsonpDone = xhr = null; - // normalize IE bug (http://bugs.jquery.com/ticket/1450) - status = status === 1223 ? 204 : status; - statusText = statusText || ''; - - callback(status, response, headersString, statusText); - $browser.$$completeOutstandingRequest(noop); + callback(status, response, headersString, statusText, xhrStatus); } }; - function jsonpReq(url, done) { - // we can't use jQuery/jqLite here because jQuery does crazy shit with script elements, e.g.: + function jsonpReq(url, callbackPath, done) { + url = url.replace('JSON_CALLBACK', callbackPath); + // we can't use jQuery/jqLite here because jQuery does crazy stuff with script elements, e.g.: // - fetches local scripts via XHR and evals them // - adds and immediately removes script elements from the document - var script = rawDocument.createElement('script'), - doneWrapper = function() { - script.onreadystatechange = script.onload = script.onerror = null; - rawDocument.body.removeChild(script); - if (done) done(); - }; - + var script = rawDocument.createElement('script'), callback = null; script.type = 'text/javascript'; script.src = url; - - if (msie && msie <= 8) { - script.onreadystatechange = function() { - if (/loaded|complete/.test(script.readyState)) { - doneWrapper(); + script.async = true; + + callback = function(event) { + script.removeEventListener('load', callback); + script.removeEventListener('error', callback); + rawDocument.body.removeChild(script); + script = null; + var status = -1; + var text = 'unknown'; + + if (event) { + if (event.type === 'load' && !callbacks.wasCalled(callbackPath)) { + event = { type: 'error' }; } - }; - } else { - script.onload = script.onerror = function() { - doneWrapper(); - }; - } + text = event.type; + status = event.type === 'error' ? 404 : 200; + } + if (done) { + done(status, text); + } + }; + + script.addEventListener('load', callback); + script.addEventListener('error', callback); rawDocument.body.appendChild(script); - return doneWrapper; + return callback; } } - var $interpolateMinErr = minErr('$interpolate'); + var $interpolateMinErr = angular.$interpolateMinErr = minErr('$interpolate'); + $interpolateMinErr.throwNoconcat = function(text) { + throw $interpolateMinErr('noconcat', + 'Error while interpolating: {0}\nStrict Contextual Escaping disallows ' + + 'interpolations that concatenate multiple expressions when a trusted value is ' + + 'required. See http://docs.angularjs.org/api/ng.$sce', text); + }; + + $interpolateMinErr.interr = function(text, err) { + return $interpolateMinErr('interr', 'Can\'t interpolate: {0}\n{1}', text, err.toString()); + }; /** * @ngdoc provider * @name $interpolateProvider - * @function + * @this * * @description * * Used for configuring the interpolation markup. Defaults to `{{` and `}}`. * + *
    + * This feature is sometimes used to mix different markup languages, e.g. to wrap an AngularJS + * template within a Python Jinja template (or any other template language). Mixing templating + * languages is **very dangerous**. The embedding template language will not safely escape AngularJS + * expressions, so any user-controlled values in the template will cause Cross Site Scripting (XSS) + * security bugs! + *
    + * * @example - + -
    +
    //demo.label//
    @@ -8492,11 +12975,11 @@ * @name $interpolateProvider#startSymbol * @description * Symbol to denote start of expression in the interpolated string. Defaults to `{{`. - * - * @param {string=} value new value to set the starting symbol to. + * + * @param {string=} value new value to set the starting symbol to. * @returns {string|self} Returns the symbol when used as getter and self if used as setter. */ - this.startSymbol = function(value){ + this.startSymbol = function(value) { if (value) { startSymbol = value; return this; @@ -8514,7 +12997,7 @@ * @param {string=} value new value to set the ending symbol to. * @returns {string|self} Returns the symbol when used as getter and self if used as setter. */ - this.endSymbol = function(value){ + this.endSymbol = function(value) { if (value) { endSymbol = value; return this; @@ -8526,12 +13009,32 @@ this.$get = ['$parse', '$exceptionHandler', '$sce', function($parse, $exceptionHandler, $sce) { var startSymbolLength = startSymbol.length, - endSymbolLength = endSymbol.length; + endSymbolLength = endSymbol.length, + escapedStartRegexp = new RegExp(startSymbol.replace(/./g, escape), 'g'), + escapedEndRegexp = new RegExp(endSymbol.replace(/./g, escape), 'g'); + + function escape(ch) { + return '\\\\\\' + ch; + } + + function unescapeText(text) { + return text.replace(escapedStartRegexp, startSymbol). + replace(escapedEndRegexp, endSymbol); + } + + // TODO: this is the same as the constantWatchDelegate in parse.js + function constantWatchDelegate(scope, listener, objectEquality, constantInterp) { + var unwatch = scope.$watch(function constantInterpolateWatch(scope) { + unwatch(); + return constantInterp(scope); + }, listener, objectEquality); + return unwatch; + } /** * @ngdoc service * @name $interpolate - * @function + * @kind function * * @requires $parse * @requires $sce @@ -8547,9 +13050,89 @@ * ```js * var $interpolate = ...; // injected * var exp = $interpolate('Hello {{name | uppercase}}!'); - * expect(exp({name:'Angular'}).toEqual('Hello ANGULAR!'); + * expect(exp({name:'AngularJS'})).toEqual('Hello ANGULAR!'); + * ``` + * + * `$interpolate` takes an optional fourth argument, `allOrNothing`. If `allOrNothing` is + * `true`, the interpolation function will return `undefined` unless all embedded expressions + * evaluate to a value other than `undefined`. + * + * ```js + * var $interpolate = ...; // injected + * var context = {greeting: 'Hello', name: undefined }; + * + * // default "forgiving" mode + * var exp = $interpolate('{{greeting}} {{name}}!'); + * expect(exp(context)).toEqual('Hello !'); + * + * // "allOrNothing" mode + * exp = $interpolate('{{greeting}} {{name}}!', false, null, true); + * expect(exp(context)).toBeUndefined(); + * context.name = 'AngularJS'; + * expect(exp(context)).toEqual('Hello AngularJS!'); + * ``` + * + * `allOrNothing` is useful for interpolating URLs. `ngSrc` and `ngSrcset` use this behavior. + * + * #### Escaped Interpolation + * $interpolate provides a mechanism for escaping interpolation markers. Start and end markers + * can be escaped by preceding each of their characters with a REVERSE SOLIDUS U+005C (backslash). + * It will be rendered as a regular start/end marker, and will not be interpreted as an expression + * or binding. + * + * This enables web-servers to prevent script injection attacks and defacing attacks, to some + * degree, while also enabling code examples to work without relying on the + * {@link ng.directive:ngNonBindable ngNonBindable} directive. + * + * **For security purposes, it is strongly encouraged that web servers escape user-supplied data, + * replacing angle brackets (<, >) with &lt; and &gt; respectively, and replacing all + * interpolation start/end markers with their escaped counterparts.** + * + * Escaped interpolation markers are only replaced with the actual interpolation markers in rendered + * output when the $interpolate service processes the text. So, for HTML elements interpolated + * by {@link ng.$compile $compile}, or otherwise interpolated with the `mustHaveExpression` parameter + * set to `true`, the interpolated text must contain an unescaped interpolation expression. As such, + * this is typically useful only when user-data is used in rendering a template from the server, or + * when otherwise untrusted data is used by a directive. + * + * + * + *
    + *

    {{apptitle}}: \{\{ username = "defaced value"; \}\} + *

    + *

    {{username}} attempts to inject code which will deface the + * application, but fails to accomplish their task, because the server has correctly + * escaped the interpolation start/end markers with REVERSE SOLIDUS U+005C (backslash) + * characters.

    + *

    Instead, the result of the attempted script injection is visible, and can be removed + * from the database by an administrator.

    + *
    + *
    + *
    + * + * @knownIssue + * It is currently not possible for an interpolated expression to contain the interpolation end + * symbol. For example, `{{ '}}' }}` will be incorrectly interpreted as `{{ ' }}` + `' }}`, i.e. + * an interpolated expression consisting of a single-quote (`'`) and the `' }}` string. + * + * @knownIssue + * All directives and components must use the standard `{{` `}}` interpolation symbols + * in their templates. If you change the application interpolation symbols the {@link $compile} + * service will attempt to denormalize the standard symbols to the custom symbols. + * The denormalization process is not clever enough to know not to replace instances of the standard + * symbols where they would not normally be treated as interpolation symbols. For example in the following + * code snippet the closing braces of the literal object will get incorrectly denormalized: + * + * ``` + *
    * ``` * + * See https://github.com/angular/angular.js/pull/14610#issuecomment-219401099 for more information. * * @param {string} text The text with markup to interpolate. * @param {boolean=} mustHaveExpression if set to true then the interpolation string must have @@ -8559,43 +13142,57 @@ * result through {@link ng.$sce#getTrusted $sce.getTrusted(interpolatedResult, * trustedContext)} before returning it. Refer to the {@link ng.$sce $sce} service that * provides Strict Contextual Escaping for details. + * @param {boolean=} allOrNothing if `true`, then the returned function returns undefined + * unless all embedded expressions evaluate to a value other than `undefined`. * @returns {function(context)} an interpolation function which is used to compute the * interpolated string. The function has these parameters: * - * * `context`: an object against which any expressions embedded in the strings are evaluated - * against. - * + * - `context`: evaluation context for all expressions embedded in the interpolated text */ - function $interpolate(text, mustHaveExpression, trustedContext) { + function $interpolate(text, mustHaveExpression, trustedContext, allOrNothing) { + // Provide a quick exit and simplified result function for text with no interpolation + if (!text.length || text.indexOf(startSymbol) === -1) { + var constantInterp; + if (!mustHaveExpression) { + var unescapedText = unescapeText(text); + constantInterp = valueFn(unescapedText); + constantInterp.exp = text; + constantInterp.expressions = []; + constantInterp.$$watchDelegate = constantWatchDelegate; + } + return constantInterp; + } + + allOrNothing = !!allOrNothing; var startIndex, endIndex, index = 0, - parts = [], - length = text.length, - hasInterpolation = false, - fn, + expressions = [], + parseFns = [], + textLength = text.length, exp, - concat = []; - - while(index < length) { - if ( ((startIndex = text.indexOf(startSymbol, index)) != -1) && - ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1) ) { - (index != startIndex) && parts.push(text.substring(index, startIndex)); - parts.push(fn = $parse(exp = text.substring(startIndex + startSymbolLength, endIndex))); - fn.exp = exp; + concat = [], + expressionPositions = []; + + while (index < textLength) { + if (((startIndex = text.indexOf(startSymbol, index)) !== -1) && + ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) !== -1)) { + if (index !== startIndex) { + concat.push(unescapeText(text.substring(index, startIndex))); + } + exp = text.substring(startIndex + startSymbolLength, endIndex); + expressions.push(exp); + parseFns.push($parse(exp, parseStringifyInterceptor)); index = endIndex + endSymbolLength; - hasInterpolation = true; + expressionPositions.push(concat.length); + concat.push(''); } else { - // we did not find anything, so we have to add the remainder to the parts array - (index != length) && parts.push(text.substring(index)); - index = length; - } - } - - if (!(length = parts.length)) { - // we added, nothing, must have been an empty string. - parts.push(''); - length = 1; + // we did not find an interpolation, so we have to add the remainder to the separators array + if (index !== textLength) { + concat.push(unescapeText(text.substring(index))); + } + break; + } } // Concatenating expressions makes it hard to reason about whether some combination of @@ -8604,44 +13201,62 @@ // that's used is assigned or constructed by some JS code somewhere that is more testable or // make it obvious that you bound the value to some user controlled value. This helps reduce // the load when auditing for XSS issues. - if (trustedContext && parts.length > 1) { - throw $interpolateMinErr('noconcat', - "Error while interpolating: {0}\nStrict Contextual Escaping disallows " + - "interpolations that concatenate multiple expressions when a trusted value is " + - "required. See http://docs.angularjs.org/api/ng.$sce", text); + if (trustedContext && concat.length > 1) { + $interpolateMinErr.throwNoconcat(text); } - if (!mustHaveExpression || hasInterpolation) { - concat.length = length; - fn = function(context) { + if (!mustHaveExpression || expressions.length) { + var compute = function(values) { + for (var i = 0, ii = expressions.length; i < ii; i++) { + if (allOrNothing && isUndefined(values[i])) return; + concat[expressionPositions[i]] = values[i]; + } + return concat.join(''); + }; + + var getValue = function(value) { + return trustedContext ? + $sce.getTrusted(trustedContext, value) : + $sce.valueOf(value); + }; + + return extend(function interpolationFn(context) { + var i = 0; + var ii = expressions.length; + var values = new Array(ii); + try { - for(var i = 0, ii = length, part; i * - * @param {function()} fn A function that should be called repeatedly. + * @param {function()} fn A function that should be called repeatedly. If no additional arguments + * are passed (see below), the function is called with the current iteration count. * @param {number} delay Number of milliseconds between each function call. * @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat * indefinitely. * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. - * @returns {promise} A promise which will be notified on each iteration. + * @param {...*=} Pass additional parameters to the executed function. + * @returns {promise} A promise which will be notified on each iteration. It will resolve once all iterations of the interval complete. * * @example - * - * - * + * // used to update the UI + * function updateTime() { + * element.text(dateFilter(new Date(), format)); + * } * - *
    - *
    - * Date format:
    - * Current time is: - *
    - * Blood 1 : {{blood_1}} - * Blood 2 : {{blood_2}} - * - * - * - *
    + * // watch the expression, and update the UI on change. + * scope.$watch(attrs.myCurrentTime, function(value) { + * format = value; + * updateTime(); + * }); + * + * stopTime = $interval(updateTime, 1000); + * + * // listen on DOM destroy (removal) event, and cancel the next UI update + * // to prevent updating time after the DOM element was removed. + * element.on('$destroy', function() { + * $interval.cancel(stopTime); + * }); + * } + * }]); + * + * + *
    + *
    + *
    + * Current time is: + *
    + * Blood 1 : {{blood_1}} + * Blood 2 : {{blood_2}} + * + * + * *
    + *
    * - * + * * */ function interval(fn, delay, count, invokeApply) { - var setInterval = $window.setInterval, + var hasParams = arguments.length > 4, + args = hasParams ? sliceArgs(arguments, 4) : [], + setInterval = $window.setInterval, clearInterval = $window.clearInterval, - deferred = $q.defer(), - promise = deferred.promise, iteration = 0, - skipApply = (isDefined(invokeApply) && !invokeApply); + skipApply = (isDefined(invokeApply) && !invokeApply), + deferred = (skipApply ? $$q : $q).defer(), + promise = deferred.promise; count = isDefined(count) ? count : 0; - promise.then(null, null, fn); - promise.$$intervalId = setInterval(function tick() { + if (skipApply) { + $browser.defer(callback); + } else { + $rootScope.$evalAsync(callback); + } deferred.notify(iteration++); if (count > 0 && iteration >= count) { @@ -8838,6 +13462,14 @@ intervals[promise.$$intervalId] = deferred; return promise; + + function callback() { + if (!hasParams) { + fn(iteration); + } else { + fn.apply(null, args); + } + } } @@ -8848,13 +13480,15 @@ * @description * Cancels a task associated with the `promise`. * - * @param {promise} promise returned by the `$interval` function. + * @param {Promise=} promise returned by the `$interval` function. * @returns {boolean} Returns `true` if the task was successfully canceled. */ interval.cancel = function(promise) { if (promise && promise.$$intervalId in intervals) { + // Interval cancels should not report as unhandled promise. + markQExceptionHandled(intervals[promise.$$intervalId].promise); intervals[promise.$$intervalId].reject('canceled'); - clearInterval(promise.$$intervalId); + $window.clearInterval(promise.$$intervalId); delete intervals[promise.$$intervalId]; return true; } @@ -8867,77 +13501,97 @@ /** * @ngdoc service - * @name $locale - * + * @name $jsonpCallbacks + * @requires $window * @description - * $locale service provides localization rules for various Angular components. As of right now the - * only public api is: - * - * * `id` – `{string}` – locale id formatted as `languageId-countryId` (e.g. `en-us`) + * This service handles the lifecycle of callbacks to handle JSONP requests. + * Override this service if you wish to customise where the callbacks are stored and + * how they vary compared to the requested url. */ - function $LocaleProvider(){ + var $jsonpCallbacksProvider = /** @this */ function() { this.$get = function() { + var callbacks = angular.callbacks; + var callbackMap = {}; + + function createCallback(callbackId) { + var callback = function(data) { + callback.data = data; + callback.called = true; + }; + callback.id = callbackId; + return callback; + } + return { - id: 'en-us', - - NUMBER_FORMATS: { - DECIMAL_SEP: '.', - GROUP_SEP: ',', - PATTERNS: [ - { // Decimal Pattern - minInt: 1, - minFrac: 0, - maxFrac: 3, - posPre: '', - posSuf: '', - negPre: '-', - negSuf: '', - gSize: 3, - lgSize: 3 - },{ //Currency Pattern - minInt: 1, - minFrac: 2, - maxFrac: 2, - posPre: '\u00A4', - posSuf: '', - negPre: '(\u00A4', - negSuf: ')', - gSize: 3, - lgSize: 3 - } - ], - CURRENCY_SYM: '$' + /** + * @ngdoc method + * @name $jsonpCallbacks#createCallback + * @param {string} url the url of the JSONP request + * @returns {string} the callback path to send to the server as part of the JSONP request + * @description + * {@link $httpBackend} calls this method to create a callback and get hold of the path to the callback + * to pass to the server, which will be used to call the callback with its payload in the JSONP response. + */ + createCallback: function(url) { + var callbackId = '_' + (callbacks.$$counter++).toString(36); + var callbackPath = 'angular.callbacks.' + callbackId; + var callback = createCallback(callbackId); + callbackMap[callbackPath] = callbacks[callbackId] = callback; + return callbackPath; }, - - DATETIME_FORMATS: { - MONTH: - 'January,February,March,April,May,June,July,August,September,October,November,December' - .split(','), - SHORTMONTH: 'Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec'.split(','), - DAY: 'Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday'.split(','), - SHORTDAY: 'Sun,Mon,Tue,Wed,Thu,Fri,Sat'.split(','), - AMPMS: ['AM','PM'], - medium: 'MMM d, y h:mm:ss a', - short: 'M/d/yy h:mm a', - fullDate: 'EEEE, MMMM d, y', - longDate: 'MMMM d, y', - mediumDate: 'MMM d, y', - shortDate: 'M/d/yy', - mediumTime: 'h:mm:ss a', - shortTime: 'h:mm a' + /** + * @ngdoc method + * @name $jsonpCallbacks#wasCalled + * @param {string} callbackPath the path to the callback that was sent in the JSONP request + * @returns {boolean} whether the callback has been called, as a result of the JSONP response + * @description + * {@link $httpBackend} calls this method to find out whether the JSONP response actually called the + * callback that was passed in the request. + */ + wasCalled: function(callbackPath) { + return callbackMap[callbackPath].called; }, - - pluralCat: function(num) { - if (num === 1) { - return 'one'; - } - return 'other'; + /** + * @ngdoc method + * @name $jsonpCallbacks#getResponse + * @param {string} callbackPath the path to the callback that was sent in the JSONP request + * @returns {*} the data received from the response via the registered callback + * @description + * {@link $httpBackend} calls this method to get hold of the data that was provided to the callback + * in the JSONP response. + */ + getResponse: function(callbackPath) { + return callbackMap[callbackPath].data; + }, + /** + * @ngdoc method + * @name $jsonpCallbacks#removeCallback + * @param {string} callbackPath the path to the callback that was sent in the JSONP request + * @description + * {@link $httpBackend} calls this method to remove the callback after the JSONP request has + * completed or timed-out. + */ + removeCallback: function(callbackPath) { + var callback = callbackMap[callbackPath]; + delete callbacks[callback.id]; + delete callbackMap[callbackPath]; } }; }; - } + }; + + /** + * @ngdoc service + * @name $locale + * + * @description + * $locale service provides localization rules for various AngularJS components. As of right now the + * only public api is: + * + * * `id` – `{string}` – locale id formatted as `languageId-countryId` (e.g. `en-us`) + */ - var PATH_MATCH = /^([^\?#]*)(\?([^#]*))?(#(.*))?$/, + var PATH_MATCH = /^([^?#]*)(\?([^#]*))?(#(.*))?$/, DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21}; var $locationMinErr = minErr('$location'); @@ -8953,56 +13607,84 @@ i = segments.length; while (i--) { - segments[i] = encodeUriSegment(segments[i]); + // decode forward slashes to prevent them from being double encoded + segments[i] = encodeUriSegment(segments[i].replace(/%2F/g, '/')); + } + + return segments.join('/'); + } + + function decodePath(path, html5Mode) { + var segments = path.split('/'), + i = segments.length; + + while (i--) { + segments[i] = decodeURIComponent(segments[i]); + if (html5Mode) { + // encode forward slashes to prevent them from being mistaken for path separators + segments[i] = segments[i].replace(/\//g, '%2F'); + } } return segments.join('/'); } - function parseAbsoluteUrl(absoluteUrl, locationObj, appBase) { - var parsedUrl = urlResolve(absoluteUrl, appBase); + function parseAbsoluteUrl(absoluteUrl, locationObj) { + var parsedUrl = urlResolve(absoluteUrl); locationObj.$$protocol = parsedUrl.protocol; locationObj.$$host = parsedUrl.hostname; - locationObj.$$port = int(parsedUrl.port) || DEFAULT_PORTS[parsedUrl.protocol] || null; + locationObj.$$port = toInt(parsedUrl.port) || DEFAULT_PORTS[parsedUrl.protocol] || null; } + var DOUBLE_SLASH_REGEX = /^\s*[\\/]{2,}/; + function parseAppUrl(url, locationObj, html5Mode) { + + if (DOUBLE_SLASH_REGEX.test(url)) { + throw $locationMinErr('badpath', 'Invalid url "{0}".', url); + } - function parseAppUrl(relativeUrl, locationObj, appBase) { - var prefixed = (relativeUrl.charAt(0) !== '/'); + var prefixed = (url.charAt(0) !== '/'); if (prefixed) { - relativeUrl = '/' + relativeUrl; + url = '/' + url; } - var match = urlResolve(relativeUrl, appBase); - locationObj.$$path = decodeURIComponent(prefixed && match.pathname.charAt(0) === '/' ? - match.pathname.substring(1) : match.pathname); + var match = urlResolve(url); + var path = prefixed && match.pathname.charAt(0) === '/' ? match.pathname.substring(1) : match.pathname; + locationObj.$$path = decodePath(path, html5Mode); locationObj.$$search = parseKeyValue(match.search); locationObj.$$hash = decodeURIComponent(match.hash); // make sure path starts with '/'; - if (locationObj.$$path && locationObj.$$path.charAt(0) != '/') { + if (locationObj.$$path && locationObj.$$path.charAt(0) !== '/') { locationObj.$$path = '/' + locationObj.$$path; } } + function startsWith(str, search) { + return str.slice(0, search.length) === search; + } /** * - * @param {string} begin - * @param {string} whole - * @returns {string} returns text from whole after begin or undefined if it does not begin with - * expected string. + * @param {string} base + * @param {string} url + * @returns {string} returns text from `url` after `base` or `undefined` if it does not begin with + * the expected string. */ - function beginsWith(begin, whole) { - if (whole.indexOf(begin) === 0) { - return whole.substr(begin.length); + function stripBaseUrl(base, url) { + if (startsWith(url, base)) { + return url.substr(base.length); } } function stripHash(url) { var index = url.indexOf('#'); - return index == -1 ? url : url.substr(0, index); + return index === -1 ? url : url.substr(0, index); + } + + function trimEmptyHash(url) { + return url.replace(/(#.+)|#$/, '$1'); } @@ -9017,33 +13699,33 @@ /** - * LocationHtml5Url represents an url + * LocationHtml5Url represents a URL * This object is exposed as $location service when HTML5 mode is enabled and supported * * @constructor * @param {string} appBase application base URL - * @param {string} basePrefix url path prefix + * @param {string} appBaseNoFile application base URL stripped of any filename + * @param {string} basePrefix URL path prefix */ - function LocationHtml5Url(appBase, basePrefix) { + function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) { this.$$html5 = true; basePrefix = basePrefix || ''; - var appBaseNoFile = stripFile(appBase); - parseAbsoluteUrl(appBase, this, appBase); + parseAbsoluteUrl(appBase, this); /** - * Parse given html5 (regular) url string into properties - * @param {string} newAbsoluteUrl HTML5 url + * Parse given HTML5 (regular) URL string into properties + * @param {string} url HTML5 URL * @private */ this.$$parse = function(url) { - var pathUrl = beginsWith(appBaseNoFile, url); + var pathUrl = stripBaseUrl(appBaseNoFile, url); if (!isString(pathUrl)) { throw $locationMinErr('ipthprfx', 'Invalid url "{0}", missing path prefix "{1}".', url, appBaseNoFile); } - parseAppUrl(pathUrl, this, appBase); + parseAppUrl(pathUrl, this, true); if (!this.$$path) { this.$$path = '/'; @@ -9062,94 +13744,122 @@ this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; this.$$absUrl = appBaseNoFile + this.$$url.substr(1); // first char is always '/' + + this.$$urlUpdatedByLocation = true; }; - this.$$rewrite = function(url) { + this.$$parseLinkUrl = function(url, relHref) { + if (relHref && relHref[0] === '#') { + // special case for links to hash fragments: + // keep the old url and only replace the hash fragment + this.hash(relHref.slice(1)); + return true; + } var appUrl, prevAppUrl; + var rewrittenUrl; + - if ( (appUrl = beginsWith(appBase, url)) !== undefined ) { + if (isDefined(appUrl = stripBaseUrl(appBase, url))) { prevAppUrl = appUrl; - if ( (appUrl = beginsWith(basePrefix, appUrl)) !== undefined ) { - return appBaseNoFile + (beginsWith('/', appUrl) || appUrl); + if (basePrefix && isDefined(appUrl = stripBaseUrl(basePrefix, appUrl))) { + rewrittenUrl = appBaseNoFile + (stripBaseUrl('/', appUrl) || appUrl); } else { - return appBase + prevAppUrl; + rewrittenUrl = appBase + prevAppUrl; } - } else if ( (appUrl = beginsWith(appBaseNoFile, url)) !== undefined ) { - return appBaseNoFile + appUrl; - } else if (appBaseNoFile == url + '/') { - return appBaseNoFile; + } else if (isDefined(appUrl = stripBaseUrl(appBaseNoFile, url))) { + rewrittenUrl = appBaseNoFile + appUrl; + } else if (appBaseNoFile === url + '/') { + rewrittenUrl = appBaseNoFile; } + if (rewrittenUrl) { + this.$$parse(rewrittenUrl); + } + return !!rewrittenUrl; }; } /** - * LocationHashbangUrl represents url + * LocationHashbangUrl represents URL * This object is exposed as $location service when developer doesn't opt into html5 mode. * It also serves as the base class for html5 mode fallback on legacy browsers. * * @constructor * @param {string} appBase application base URL + * @param {string} appBaseNoFile application base URL stripped of any filename * @param {string} hashPrefix hashbang prefix */ - function LocationHashbangUrl(appBase, hashPrefix) { - var appBaseNoFile = stripFile(appBase); + function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { - parseAbsoluteUrl(appBase, this, appBase); + parseAbsoluteUrl(appBase, this); /** - * Parse given hashbang url into properties - * @param {string} url Hashbang url + * Parse given hashbang URL into properties + * @param {string} url Hashbang URL * @private */ this.$$parse = function(url) { - var withoutBaseUrl = beginsWith(appBase, url) || beginsWith(appBaseNoFile, url); - var withoutHashUrl = withoutBaseUrl.charAt(0) == '#' - ? beginsWith(hashPrefix, withoutBaseUrl) - : (this.$$html5) - ? withoutBaseUrl - : ''; + var withoutBaseUrl = stripBaseUrl(appBase, url) || stripBaseUrl(appBaseNoFile, url); + var withoutHashUrl; + + if (!isUndefined(withoutBaseUrl) && withoutBaseUrl.charAt(0) === '#') { - if (!isString(withoutHashUrl)) { - throw $locationMinErr('ihshprfx', 'Invalid url "{0}", missing hash prefix "{1}".', url, - hashPrefix); + // The rest of the URL starts with a hash so we have + // got either a hashbang path or a plain hash fragment + withoutHashUrl = stripBaseUrl(hashPrefix, withoutBaseUrl); + if (isUndefined(withoutHashUrl)) { + // There was no hashbang prefix so we just have a hash fragment + withoutHashUrl = withoutBaseUrl; + } + + } else { + // There was no hashbang path nor hash fragment: + // If we are in HTML5 mode we use what is left as the path; + // Otherwise we ignore what is left + if (this.$$html5) { + withoutHashUrl = withoutBaseUrl; + } else { + withoutHashUrl = ''; + if (isUndefined(withoutBaseUrl)) { + appBase = url; + /** @type {?} */ (this).replace(); + } + } } - parseAppUrl(withoutHashUrl, this, appBase); + + parseAppUrl(withoutHashUrl, this, false); this.$$path = removeWindowsDriveName(this.$$path, withoutHashUrl, appBase); this.$$compose(); /* - * In Windows, on an anchor node on documents loaded from - * the filesystem, the browser will return a pathname - * prefixed with the drive name ('/C:/path') when a - * pathname without a drive is set: - * * a.setAttribute('href', '/foo') - * * a.pathname === '/C:/foo' //true - * - * Inside of Angular, we're always using pathnames that - * do not include drive names for routing. - */ - function removeWindowsDriveName (path, url, base) { + * In Windows, on an anchor node on documents loaded from + * the filesystem, the browser will return a pathname + * prefixed with the drive name ('/C:/path') when a + * pathname without a drive is set: + * * a.setAttribute('href', '/foo') + * * a.pathname === '/C:/foo' //true + * + * Inside of AngularJS, we're always using pathnames that + * do not include drive names for routing. + */ + function removeWindowsDriveName(path, url, base) { /* - Matches paths for file protocol on windows, - such as /C:/foo/bar, and captures only /foo/bar. - */ - var windowsFilePathExp = /^\/?.*?:(\/.*)/; + Matches paths for file protocol on windows, + such as /C:/foo/bar, and captures only /foo/bar. + */ + var windowsFilePathExp = /^\/[A-Z]:(\/.*)/; var firstPathSegmentMatch; //Get the relative path from the input URL. - if (url.indexOf(base) === 0) { + if (startsWith(url, base)) { url = url.replace(base, ''); } - /* - * The input URL intentionally contains a - * first path segment that ends with a colon. - */ + // The input URL intentionally contains a first path segment that ends with a colon. if (windowsFilePathExp.exec(url)) { return path; } @@ -9160,7 +13870,7 @@ }; /** - * Compose hashbang url and update `absUrl` property + * Compose hashbang URL and update `absUrl` property * @private */ this.$$compose = function() { @@ -9169,250 +13879,415 @@ this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; this.$$absUrl = appBase + (this.$$url ? hashPrefix + this.$$url : ''); + + this.$$urlUpdatedByLocation = true; }; - this.$$rewrite = function(url) { - if(stripHash(appBase) == stripHash(url)) { - return url; + this.$$parseLinkUrl = function(url, relHref) { + if (stripHash(appBase) === stripHash(url)) { + this.$$parse(url); + return true; } + return false; }; } /** - * LocationHashbangUrl represents url + * LocationHashbangUrl represents URL * This object is exposed as $location service when html5 history api is enabled but the browser * does not support it. * * @constructor * @param {string} appBase application base URL + * @param {string} appBaseNoFile application base URL stripped of any filename * @param {string} hashPrefix hashbang prefix */ - function LocationHashbangInHtml5Url(appBase, hashPrefix) { + function LocationHashbangInHtml5Url(appBase, appBaseNoFile, hashPrefix) { this.$$html5 = true; LocationHashbangUrl.apply(this, arguments); - var appBaseNoFile = stripFile(appBase); + this.$$parseLinkUrl = function(url, relHref) { + if (relHref && relHref[0] === '#') { + // special case for links to hash fragments: + // keep the old url and only replace the hash fragment + this.hash(relHref.slice(1)); + return true; + } - this.$$rewrite = function(url) { + var rewrittenUrl; var appUrl; - if ( appBase == stripHash(url) ) { - return url; - } else if ( (appUrl = beginsWith(appBaseNoFile, url)) ) { - return appBase + hashPrefix + appUrl; - } else if ( appBaseNoFile === url + '/') { - return appBaseNoFile; + if (appBase === stripHash(url)) { + rewrittenUrl = url; + } else if ((appUrl = stripBaseUrl(appBaseNoFile, url))) { + rewrittenUrl = appBase + hashPrefix + appUrl; + } else if (appBaseNoFile === url + '/') { + rewrittenUrl = appBaseNoFile; } + if (rewrittenUrl) { + this.$$parse(rewrittenUrl); + } + return !!rewrittenUrl; }; - } + this.$$compose = function() { + var search = toKeyValue(this.$$search), + hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; - LocationHashbangInHtml5Url.prototype = - LocationHashbangUrl.prototype = - LocationHtml5Url.prototype = { + this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; + // include hashPrefix in $$absUrl when $$url is empty so IE9 does not reload page because of removal of '#' + this.$$absUrl = appBase + hashPrefix + this.$$url; - /** - * Are we in html5 mode? - * @private - */ - $$html5: false, + this.$$urlUpdatedByLocation = true; + }; - /** - * Has any change been replacing ? - * @private - */ - $$replace: false, + } - /** - * @ngdoc method - * @name $location#absUrl - * - * @description - * This method is getter only. - * - * Return full url representation with all segments encoded according to rules specified in - * [RFC 3986](http://www.ietf.org/rfc/rfc3986.txt). - * - * @return {string} full url - */ - absUrl: locationGetter('$$absUrl'), - /** - * @ngdoc method - * @name $location#url - * - * @description - * This method is getter / setter. - * - * Return url (e.g. `/path?a=b#hash`) when called without any parameter. - * - * Change path, search and hash, when called with parameter and return `$location`. - * - * @param {string=} url New url without base prefix (e.g. `/path?a=b#hash`) - * @param {string=} replace The path that will be changed - * @return {string} url - */ - url: function(url, replace) { - if (isUndefined(url)) - return this.$$url; + var locationPrototype = { - var match = PATH_MATCH.exec(url); - if (match[1]) this.path(decodeURIComponent(match[1])); - if (match[2] || match[1]) this.search(match[3] || ''); - this.hash(match[5] || '', replace); + /** + * Ensure absolute URL is initialized. + * @private + */ + $$absUrl:'', - return this; - }, + /** + * Are we in html5 mode? + * @private + */ + $$html5: false, - /** - * @ngdoc method - * @name $location#protocol - * - * @description - * This method is getter only. - * - * Return protocol of current url. - * - * @return {string} protocol of current url - */ - protocol: locationGetter('$$protocol'), + /** + * Has any change been replacing? + * @private + */ + $$replace: false, - /** - * @ngdoc method - * @name $location#host - * - * @description - * This method is getter only. - * - * Return host of current url. - * - * @return {string} host of current url. - */ - host: locationGetter('$$host'), + /** + * @ngdoc method + * @name $location#absUrl + * + * @description + * This method is getter only. + * + * Return full URL representation with all segments encoded according to rules specified in + * [RFC 3986](http://www.ietf.org/rfc/rfc3986.txt). + * + * + * ```js + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo + * var absUrl = $location.absUrl(); + * // => "http://example.com/#/some/path?foo=bar&baz=xoxo" + * ``` + * + * @return {string} full URL + */ + absUrl: locationGetter('$$absUrl'), - /** - * @ngdoc method - * @name $location#port - * - * @description - * This method is getter only. - * - * Return port of current url. - * - * @return {Number} port - */ - port: locationGetter('$$port'), + /** + * @ngdoc method + * @name $location#url + * + * @description + * This method is getter / setter. + * + * Return URL (e.g. `/path?a=b#hash`) when called without any parameter. + * + * Change path, search and hash, when called with parameter and return `$location`. + * + * + * ```js + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo + * var url = $location.url(); + * // => "/some/path?foo=bar&baz=xoxo" + * ``` + * + * @param {string=} url New URL without base prefix (e.g. `/path?a=b#hash`) + * @return {string} url + */ + url: function(url) { + if (isUndefined(url)) { + return this.$$url; + } - /** - * @ngdoc method - * @name $location#path - * - * @description - * This method is getter / setter. - * - * Return path of current url when called without any parameter. - * - * Change path when called with parameter and return `$location`. - * - * Note: Path should always begin with forward slash (/), this method will add the forward slash - * if it is missing. - * - * @param {string=} path New path - * @return {string} path - */ - path: locationGetterSetter('$$path', function(path) { - return path.charAt(0) == '/' ? path : '/' + path; - }), + var match = PATH_MATCH.exec(url); + if (match[1] || url === '') this.path(decodeURIComponent(match[1])); + if (match[2] || match[1] || url === '') this.search(match[3] || ''); + this.hash(match[5] || ''); - /** - * @ngdoc method - * @name $location#search - * - * @description - * This method is getter / setter. - * - * Return search part (as object) of current url when called without any parameter. - * - * Change search part when called with parameter and return `$location`. - * - * @param {string|Object.|Object.>} search New search params - string or - * hash object. Hash object may contain an array of values, which will be decoded as duplicates in - * the url. - * - * @param {(string|Array)=} paramValue If `search` is a string, then `paramValue` will override only a - * single search parameter. If `paramValue` is an array, it will set the parameter as a - * comma-separated value. If `paramValue` is `null`, the parameter will be deleted. - * - * @return {string} search - */ - search: function(search, paramValue) { - switch (arguments.length) { - case 0: - return this.$$search; - case 1: - if (isString(search)) { - this.$$search = parseKeyValue(search); - } else if (isObject(search)) { - this.$$search = search; - } else { - throw $locationMinErr('isrcharg', - 'The first argument of the `$location#search()` call must be a string or an object.'); - } - break; - default: - if (isUndefined(paramValue) || paramValue === null) { - delete this.$$search[search]; - } else { - this.$$search[search] = paramValue; - } - } + return this; + }, - this.$$compose(); - return this; - }, + /** + * @ngdoc method + * @name $location#protocol + * + * @description + * This method is getter only. + * + * Return protocol of current URL. + * + * + * ```js + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo + * var protocol = $location.protocol(); + * // => "http" + * ``` + * + * @return {string} protocol of current URL + */ + protocol: locationGetter('$$protocol'), - /** - * @ngdoc method - * @name $location#hash - * - * @description - * This method is getter / setter. - * - * Return hash fragment when called without any parameter. - * - * Change hash fragment when called with parameter and return `$location`. - * - * @param {string=} hash New hash fragment - * @return {string} hash - */ - hash: locationGetterSetter('$$hash', identity), + /** + * @ngdoc method + * @name $location#host + * + * @description + * This method is getter only. + * + * Return host of current URL. + * + * Note: compared to the non-AngularJS version `location.host` which returns `hostname:port`, this returns the `hostname` portion only. + * + * + * ```js + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo + * var host = $location.host(); + * // => "example.com" + * + * // given URL http://user:password@example.com:8080/#/some/path?foo=bar&baz=xoxo + * host = $location.host(); + * // => "example.com" + * host = location.host; + * // => "example.com:8080" + * ``` + * + * @return {string} host of current URL. + */ + host: locationGetter('$$host'), + + /** + * @ngdoc method + * @name $location#port + * + * @description + * This method is getter only. + * + * Return port of current URL. + * + * + * ```js + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo + * var port = $location.port(); + * // => 80 + * ``` + * + * @return {Number} port + */ + port: locationGetter('$$port'), + + /** + * @ngdoc method + * @name $location#path + * + * @description + * This method is getter / setter. + * + * Return path of current URL when called without any parameter. + * + * Change path when called with parameter and return `$location`. + * + * Note: Path should always begin with forward slash (/), this method will add the forward slash + * if it is missing. + * + * + * ```js + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo + * var path = $location.path(); + * // => "/some/path" + * ``` + * + * @param {(string|number)=} path New path + * @return {(string|object)} path if called with no parameters, or `$location` if called with a parameter + */ + path: locationGetterSetter('$$path', function(path) { + path = path !== null ? path.toString() : ''; + return path.charAt(0) === '/' ? path : '/' + path; + }), + + /** + * @ngdoc method + * @name $location#search + * + * @description + * This method is getter / setter. + * + * Return search part (as object) of current URL when called without any parameter. + * + * Change search part when called with parameter and return `$location`. + * + * + * ```js + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo + * var searchObject = $location.search(); + * // => {foo: 'bar', baz: 'xoxo'} + * + * // set foo to 'yipee' + * $location.search('foo', 'yipee'); + * // $location.search() => {foo: 'yipee', baz: 'xoxo'} + * ``` + * + * @param {string|Object.|Object.>} search New search params - string or + * hash object. + * + * When called with a single argument the method acts as a setter, setting the `search` component + * of `$location` to the specified value. + * + * If the argument is a hash object containing an array of values, these values will be encoded + * as duplicate search parameters in the URL. + * + * @param {(string|Number|Array|boolean)=} paramValue If `search` is a string or number, then `paramValue` + * will override only a single search property. + * + * If `paramValue` is an array, it will override the property of the `search` component of + * `$location` specified via the first argument. + * + * If `paramValue` is `null`, the property specified via the first argument will be deleted. + * + * If `paramValue` is `true`, the property specified via the first argument will be added with no + * value nor trailing equal sign. + * + * @return {Object} If called with no arguments returns the parsed `search` object. If called with + * one or more arguments returns `$location` object itself. + */ + search: function(search, paramValue) { + switch (arguments.length) { + case 0: + return this.$$search; + case 1: + if (isString(search) || isNumber(search)) { + search = search.toString(); + this.$$search = parseKeyValue(search); + } else if (isObject(search)) { + search = copy(search, {}); + // remove object undefined or null properties + forEach(search, function(value, key) { + if (value == null) delete search[key]; + }); + + this.$$search = search; + } else { + throw $locationMinErr('isrcharg', + 'The first argument of the `$location#search()` call must be a string or an object.'); + } + break; + default: + if (isUndefined(paramValue) || paramValue === null) { + delete this.$$search[search]; + } else { + this.$$search[search] = paramValue; + } + } + + this.$$compose(); + return this; + }, + + /** + * @ngdoc method + * @name $location#hash + * + * @description + * This method is getter / setter. + * + * Returns the hash fragment when called without any parameters. + * + * Changes the hash fragment when called with a parameter and returns `$location`. + * + * + * ```js + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo#hashValue + * var hash = $location.hash(); + * // => "hashValue" + * ``` + * + * @param {(string|number)=} hash New hash fragment + * @return {string} hash + */ + hash: locationGetterSetter('$$hash', function(hash) { + return hash !== null ? hash.toString() : ''; + }), + + /** + * @ngdoc method + * @name $location#replace + * + * @description + * If called, all changes to $location during the current `$digest` will replace the current history + * record, instead of adding a new one. + */ + replace: function() { + this.$$replace = true; + return this; + } + }; + + forEach([LocationHashbangInHtml5Url, LocationHashbangUrl, LocationHtml5Url], function(Location) { + Location.prototype = Object.create(locationPrototype); + + /** + * @ngdoc method + * @name $location#state + * + * @description + * This method is getter / setter. + * + * Return the history state object when called without any parameter. + * + * Change the history state object when called with one parameter and return `$location`. + * The state object is later passed to `pushState` or `replaceState`. + * + * NOTE: This method is supported only in HTML5 mode and only in browsers supporting + * the HTML5 History API (i.e. methods `pushState` and `replaceState`). If you need to support + * older browsers (like IE9 or Android < 4.0), don't use this method. + * + * @param {object=} state State object for pushState or replaceState + * @return {object} state + */ + Location.prototype.state = function(state) { + if (!arguments.length) { + return this.$$state; + } + + if (Location !== LocationHtml5Url || !this.$$html5) { + throw $locationMinErr('nostate', 'History API state support is available only ' + + 'in HTML5 mode and only in browsers supporting HTML5 History API'); + } + // The user might modify `stateObject` after invoking `$location.state(stateObject)` + // but we're changing the $$state reference to $browser.state() during the $digest + // so the modification window is narrow. + this.$$state = isUndefined(state) ? null : state; + this.$$urlUpdatedByLocation = true; + + return this; + }; + }); - /** - * @ngdoc method - * @name $location#replace - * - * @description - * If called, all changes to $location during current `$digest` will be replacing current history - * record, instead of adding new one. - */ - replace: function() { - this.$$replace = true; - return this; - } - }; function locationGetter(property) { - return function() { + return /** @this */ function() { return this[property]; }; } function locationGetterSetter(property, preprocess) { - return function(value) { - if (isUndefined(value)) + return /** @this */ function(value) { + if (isUndefined(value)) { return this[property]; + } this[property] = preprocess(value); this.$$compose(); @@ -9451,17 +14326,24 @@ /** * @ngdoc provider * @name $locationProvider + * @this + * * @description * Use the `$locationProvider` to configure how the application deep linking paths are stored. */ - function $LocationProvider(){ - var hashPrefix = '', - html5Mode = false; + function $LocationProvider() { + var hashPrefix = '!', + html5Mode = { + enabled: false, + requireBase: true, + rewriteLinks: true + }; /** - * @ngdoc property + * @ngdoc method * @name $locationProvider#hashPrefix * @description + * The default value for the prefix is `'!'`. * @param {string=} prefix Prefix for hash part (containing path and search) * @returns {*} current value if used as getter or itself (chaining) if used as setter */ @@ -9475,15 +14357,46 @@ }; /** - * @ngdoc property + * @ngdoc method * @name $locationProvider#html5Mode * @description - * @param {boolean=} mode Use HTML5 strategy if available. - * @returns {*} current value if used as getter or itself (chaining) if used as setter + * @param {(boolean|Object)=} mode If boolean, sets `html5Mode.enabled` to value. + * If object, sets `enabled`, `requireBase` and `rewriteLinks` to respective values. Supported + * properties: + * - **enabled** – `{boolean}` – (default: false) If true, will rely on `history.pushState` to + * change urls where supported. Will fall back to hash-prefixed paths in browsers that do not + * support `pushState`. + * - **requireBase** - `{boolean}` - (default: `true`) When html5Mode is enabled, specifies + * whether or not a tag is required to be present. If `enabled` and `requireBase` are + * true, and a base tag is not present, an error will be thrown when `$location` is injected. + * See the {@link guide/$location $location guide for more information} + * - **rewriteLinks** - `{boolean|string}` - (default: `true`) When html5Mode is enabled, + * enables/disables URL rewriting for relative links. If set to a string, URL rewriting will + * only happen on links with an attribute that matches the given string. For example, if set + * to `'internal-link'`, then the URL will only be rewritten for `` links. + * Note that [attribute name normalization](guide/directive#normalization) does not apply + * here, so `'internalLink'` will **not** match `'internal-link'`. + * + * @returns {Object} html5Mode object if used as getter or itself (chaining) if used as setter */ this.html5Mode = function(mode) { - if (isDefined(mode)) { - html5Mode = mode; + if (isBoolean(mode)) { + html5Mode.enabled = mode; + return this; + } else if (isObject(mode)) { + + if (isBoolean(mode.enabled)) { + html5Mode.enabled = mode.enabled; + } + + if (isBoolean(mode.requireBase)) { + html5Mode.requireBase = mode.requireBase; + } + + if (isBoolean(mode.rewriteLinks) || isString(mode.rewriteLinks)) { + html5Mode.rewriteLinks = mode.rewriteLinks; + } + return this; } else { return html5Mode; @@ -9495,14 +14408,21 @@ * @name $location#$locationChangeStart * @eventType broadcast on root scope * @description - * Broadcasted before a URL will change. This change can be prevented by calling + * Broadcasted before a URL will change. + * + * This change can be prevented by calling * `preventDefault` method of the event. See {@link ng.$rootScope.Scope#$on} for more * details about event object. Upon successful change - * {@link ng.$location#events_$locationChangeSuccess $locationChangeSuccess} is fired. + * {@link ng.$location#$locationChangeSuccess $locationChangeSuccess} is fired. + * + * The `newState` and `oldState` parameters may be defined only in HTML5 mode and when + * the browser supports the HTML5 History API. * * @param {Object} angularEvent Synthetic event object. * @param {string} newUrl New URL * @param {string=} oldUrl URL that was before it was changed. + * @param {string=} newState New history state object + * @param {string=} oldState History state object that was before it was changed. */ /** @@ -9512,44 +14432,84 @@ * @description * Broadcasted after a URL was changed. * + * The `newState` and `oldState` parameters may be defined only in HTML5 mode and when + * the browser supports the HTML5 History API. + * * @param {Object} angularEvent Synthetic event object. * @param {string} newUrl New URL * @param {string=} oldUrl URL that was before it was changed. + * @param {string=} newState New history state object + * @param {string=} oldState History state object that was before it was changed. */ - this.$get = ['$rootScope', '$browser', '$sniffer', '$rootElement', - function( $rootScope, $browser, $sniffer, $rootElement) { + this.$get = ['$rootScope', '$browser', '$sniffer', '$rootElement', '$window', + function($rootScope, $browser, $sniffer, $rootElement, $window) { var $location, LocationMode, baseHref = $browser.baseHref(), // if base[href] is undefined, it defaults to '' initialUrl = $browser.url(), appBase; - if (html5Mode) { + if (html5Mode.enabled) { + if (!baseHref && html5Mode.requireBase) { + throw $locationMinErr('nobase', + '$location in HTML5 mode requires a tag to be present!'); + } appBase = serverBase(initialUrl) + (baseHref || '/'); LocationMode = $sniffer.history ? LocationHtml5Url : LocationHashbangInHtml5Url; } else { appBase = stripHash(initialUrl); LocationMode = LocationHashbangUrl; } - $location = new LocationMode(appBase, '#' + hashPrefix); - $location.$$parse($location.$$rewrite(initialUrl)); + var appBaseNoFile = stripFile(appBase); + + $location = new LocationMode(appBase, appBaseNoFile, '#' + hashPrefix); + $location.$$parseLinkUrl(initialUrl, initialUrl); + + $location.$$state = $browser.state(); + + var IGNORE_URI_REGEXP = /^\s*(javascript|mailto):/i; + + function setBrowserUrlWithFallback(url, replace, state) { + var oldUrl = $location.url(); + var oldState = $location.$$state; + try { + $browser.url(url, replace, state); + + // Make sure $location.state() returns referentially identical (not just deeply equal) + // state object; this makes possible quick checking if the state changed in the digest + // loop. Checking deep equality would be too expensive. + $location.$$state = $browser.state(); + } catch (e) { + // Restore old values if pushState fails + $location.url(oldUrl); + $location.$$state = oldState; + + throw e; + } + } $rootElement.on('click', function(event) { + var rewriteLinks = html5Mode.rewriteLinks; // TODO(vojta): rewrite link when opening in new tab/window (in legacy browser) // currently we open nice url link and redirect then - if (event.ctrlKey || event.metaKey || event.which == 2) return; + if (!rewriteLinks || event.ctrlKey || event.metaKey || event.shiftKey || event.which === 2 || event.button === 2) return; var elm = jqLite(event.target); // traverse the DOM up to find first A tag - while (lowercase(elm[0].nodeName) !== 'a') { + while (nodeName_(elm[0]) !== 'a') { // ignore rewriting if no A tag (reached root element, or no parent - removed from document) if (elm[0] === $rootElement[0] || !(elm = elm.parent())[0]) return; } + if (isString(rewriteLinks) && isUndefined(elm.attr(rewriteLinks))) return; + var absHref = elm.prop('href'); + // get the actual href attribute - see + // http://msdn.microsoft.com/en-us/library/ie/dd347148(v=vs.85).aspx + var relHref = elm.attr('href') || elm.attr('xlink:href'); if (isObject(absHref) && absHref.toString() === '[object SVGAnimatedString]') { // SVGAnimatedString.animVal should be identical to SVGAnimatedString.baseVal, unless during @@ -9557,72 +14517,118 @@ absHref = urlResolve(absHref.animVal).href; } - var rewrittenUrl = $location.$$rewrite(absHref); + // Ignore when url is started with javascript: or mailto: + if (IGNORE_URI_REGEXP.test(absHref)) return; - if (absHref && !elm.attr('target') && rewrittenUrl && !event.isDefaultPrevented()) { - event.preventDefault(); - if (rewrittenUrl != $browser.url()) { + if (absHref && !elm.attr('target') && !event.isDefaultPrevented()) { + if ($location.$$parseLinkUrl(absHref, relHref)) { + // We do a preventDefault for all urls that are part of the AngularJS application, + // in html5mode and also without, so that we are able to abort navigation without + // getting double entries in the location history. + event.preventDefault(); // update location manually - $location.$$parse(rewrittenUrl); - $rootScope.$apply(); - // hack to work around FF6 bug 684208 when scenario runner clicks on links - window.angular['ff-684208-preventDefault'] = true; + if ($location.absUrl() !== $browser.url()) { + $rootScope.$apply(); + // hack to work around FF6 bug 684208 when scenario runner clicks on links + $window.angular['ff-684208-preventDefault'] = true; + } } } }); // rewrite hashbang url <> html5 url - if ($location.absUrl() != initialUrl) { + if (trimEmptyHash($location.absUrl()) !== trimEmptyHash(initialUrl)) { $browser.url($location.absUrl(), true); } + var initializing = true; + // update $location when $browser url changes - $browser.onUrlChange(function(newUrl) { - if ($location.absUrl() != newUrl) { - $rootScope.$evalAsync(function() { - var oldUrl = $location.absUrl(); + $browser.onUrlChange(function(newUrl, newState) { - $location.$$parse(newUrl); - if ($rootScope.$broadcast('$locationChangeStart', newUrl, - oldUrl).defaultPrevented) { - $location.$$parse(oldUrl); - $browser.url(oldUrl); - } else { - afterLocationChange(oldUrl); - } - }); - if (!$rootScope.$$phase) $rootScope.$digest(); + if (!startsWith(newUrl, appBaseNoFile)) { + // If we are navigating outside of the app then force a reload + $window.location.href = newUrl; + return; } + + $rootScope.$evalAsync(function() { + var oldUrl = $location.absUrl(); + var oldState = $location.$$state; + var defaultPrevented; + newUrl = trimEmptyHash(newUrl); + $location.$$parse(newUrl); + $location.$$state = newState; + + defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl, + newState, oldState).defaultPrevented; + + // if the location was changed by a `$locationChangeStart` handler then stop + // processing this location change + if ($location.absUrl() !== newUrl) return; + + if (defaultPrevented) { + $location.$$parse(oldUrl); + $location.$$state = oldState; + setBrowserUrlWithFallback(oldUrl, false, oldState); + } else { + initializing = false; + afterLocationChange(oldUrl, oldState); + } + }); + if (!$rootScope.$$phase) $rootScope.$digest(); }); // update browser - var changeCounter = 0; $rootScope.$watch(function $locationWatch() { - var oldUrl = $browser.url(); - var currentReplace = $location.$$replace; - - if (!changeCounter || oldUrl != $location.absUrl()) { - changeCounter++; - $rootScope.$evalAsync(function() { - if ($rootScope.$broadcast('$locationChangeStart', $location.absUrl(), oldUrl). - defaultPrevented) { - $location.$$parse(oldUrl); - } else { - $browser.url($location.absUrl(), currentReplace); - afterLocationChange(oldUrl); - } - }); + if (initializing || $location.$$urlUpdatedByLocation) { + $location.$$urlUpdatedByLocation = false; + + var oldUrl = trimEmptyHash($browser.url()); + var newUrl = trimEmptyHash($location.absUrl()); + var oldState = $browser.state(); + var currentReplace = $location.$$replace; + var urlOrStateChanged = oldUrl !== newUrl || + ($location.$$html5 && $sniffer.history && oldState !== $location.$$state); + + if (initializing || urlOrStateChanged) { + initializing = false; + + $rootScope.$evalAsync(function() { + var newUrl = $location.absUrl(); + var defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl, + $location.$$state, oldState).defaultPrevented; + + // if the location was changed by a `$locationChangeStart` handler then stop + // processing this location change + if ($location.absUrl() !== newUrl) return; + + if (defaultPrevented) { + $location.$$parse(oldUrl); + $location.$$state = oldState; + } else { + if (urlOrStateChanged) { + setBrowserUrlWithFallback(newUrl, currentReplace, + oldState === $location.$$state ? null : $location.$$state); + } + afterLocationChange(oldUrl, oldState); + } + }); + } } + $location.$$replace = false; - return changeCounter; + // we don't need to return anything because $evalAsync will make the digest loop dirty when + // there is a change }); return $location; - function afterLocationChange(oldUrl) { - $rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl); + function afterLocationChange(oldUrl, oldState) { + $rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl, + $location.$$state, oldState); } }]; } @@ -9638,26 +14644,36 @@ * * The main purpose of this service is to simplify debugging and troubleshooting. * + * To reveal the location of the calls to `$log` in the JavaScript console, + * you can "blackbox" the AngularJS source in your browser: + * + * [Mozilla description of blackboxing](https://developer.mozilla.org/en-US/docs/Tools/Debugger/How_to/Black_box_a_source). + * [Chrome description of blackboxing](https://developer.chrome.com/devtools/docs/blackboxing). + * + * Note: Not all browsers support blackboxing. + * * The default is to log `debug` messages. You can use * {@link ng.$logProvider ng.$logProvider#debugEnabled} to change this. * * @example - + - function LogCtrl($scope, $log) { - $scope.$log = $log; - $scope.message = 'Hello World!'; - } + angular.module('logExample', []) + .controller('LogController', ['$scope', '$log', function($scope, $log) { + $scope.$log = $log; + $scope.message = 'Hello World!'; + }]); -
    +

    Reload this page with open console, enter text and hit the log button...

    - Message: - + +
    @@ -9666,15 +14682,17 @@ /** * @ngdoc provider * @name $logProvider + * @this + * * @description * Use the `$logProvider` to configure how the application logs messages */ - function $LogProvider(){ + function $LogProvider() { var debug = true, self = this; /** - * @ngdoc property + * @ngdoc method * @name $logProvider#debugEnabled * @description * @param {boolean=} flag enable or disable debug level messages @@ -9689,7 +14707,16 @@ } }; - this.$get = ['$window', function($window){ + this.$get = ['$window', function($window) { + // Support: IE 9-11, Edge 12-14+ + // IE/Edge display errors in such a way that it requires the user to click in 4 places + // to see the stack trace. There is no way to feature-detect it so there's a chance + // of the user agent sniffing to go wrong but since it's only about logging, this shouldn't + // break apps. Other browsers display errors in a sensible way and some of them map stack + // traces along source maps if available so it makes sense to let browsers display it + // as they want. + var formatStackTrace = msie || /\bEdge\//.test($window.navigator && $window.navigator.userAgent); + return { /** * @ngdoc method @@ -9734,7 +14761,7 @@ * @description * Write a debug message */ - debug: (function () { + debug: (function() { var fn = consoleLog('debug'); return function() { @@ -9742,12 +14769,12 @@ fn.apply(self, arguments); } }; - }()) + })() }; function formatError(arg) { - if (arg instanceof Error) { - if (arg.stack) { + if (isError(arg)) { + if (arg.stack && formatStackTrace) { arg = (arg.message && arg.stack.indexOf(arg.message) === -1) ? 'Error: ' + arg.message + '\n' + arg.stack : arg.stack; @@ -9760,139 +14787,74 @@ function consoleLog(type) { var console = $window.console || {}, - logFn = console[type] || console.log || noop, - hasApply = false; - - // Note: reading logFn.apply throws an error in IE11 in IE8 document mode. - // The reason behind this is that console.log has type "object" in IE8... - try { - hasApply = !!logFn.apply; - } catch (e) {} + logFn = console[type] || console.log || noop; - if (hasApply) { - return function() { - var args = []; - forEach(arguments, function(arg) { - args.push(formatError(arg)); - }); - return logFn.apply(console, args); - }; - } - - // we are IE which either doesn't have window.console => this is noop and we do nothing, - // or we are IE where console.log doesn't have apply so we log at least first 2 args - return function(arg1, arg2) { - logFn(arg1, arg2 == null ? '' : arg2); + return function() { + var args = []; + forEach(arguments, function(arg) { + args.push(formatError(arg)); + }); + // Support: IE 9 only + // console methods don't inherit from Function.prototype in IE 9 so we can't + // call `logFn.apply(console, args)` directly. + return Function.prototype.apply.call(logFn, console, args); }; } }]; } + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Any commits to this file should be reviewed with security in mind. * + * Changes to this file can potentially create security vulnerabilities. * + * An approval from 2 Core members with history of modifying * + * this file is required. * + * * + * Does the change somehow allow for arbitrary javascript to be executed? * + * Or allows for someone to change the prototype of built-in objects? * + * Or gives undesired access to variables likes document or window? * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + var $parseMinErr = minErr('$parse'); - var promiseWarningCache = {}; - var promiseWarning; -// Sandboxing Angular Expressions + var objectValueOf = {}.constructor.prototype.valueOf; + +// Sandboxing AngularJS Expressions // ------------------------------ -// Angular expressions are generally considered safe because these expressions only have direct -// access to $scope and locals. However, one can obtain the ability to execute arbitrary JS code by -// obtaining a reference to native JS functions such as the Function constructor. -// -// As an example, consider the following Angular expression: -// -// {}.toString.constructor(alert("evil JS code")) -// -// We want to prevent this type of access. For the sake of performance, during the lexing phase we -// disallow any "dotted" access to any member named "constructor". +// AngularJS expressions are no longer sandboxed. So it is now even easier to access arbitrary JS code by +// various means such as obtaining a reference to native JS functions like the Function constructor. // -// For reflective calls (a[b]) we check that the value of the lookup is not the Function constructor -// while evaluating the expression, which is a stronger but more expensive test. Since reflective -// calls are expensive anyway, this is not such a big deal compared to static dereferencing. +// As an example, consider the following AngularJS expression: // -// This sandboxing technique is not perfect and doesn't aim to be. The goal is to prevent exploits -// against the expression language, but not to prevent exploits that were enabled by exposing -// sensitive JavaScript or browser apis on Scope. Exposing such objects on a Scope is never a good -// practice and therefore we are not even trying to protect against interaction with an object -// explicitly exposed in this way. +// {}.toString.constructor('alert("evil JS code")') // -// A developer could foil the name check by aliasing the Function constructor under a different -// name on the scope. +// It is important to realize that if you create an expression from a string that contains user provided +// content then it is possible that your application contains a security vulnerability to an XSS style attack. // -// In general, it is not possible to access a Window object from an angular expression unless a -// window or some DOM object that has a reference to window is published onto a Scope. - - function ensureSafeMemberName(name, fullExpression) { - if (name === "constructor") { - throw $parseMinErr('isecfld', - 'Referencing "constructor" field in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } - return name; +// See https://docs.angularjs.org/guide/security + + + function getStringValue(name) { + // Property names must be strings. This means that non-string objects cannot be used + // as keys in an object. Any non-string object, including a number, is typecasted + // into a string via the toString method. + // -- MDN, https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Property_accessors#Property_names + // + // So, to ensure that we are checking the same `name` that JavaScript would use, we cast it + // to a string. It's not always possible. If `name` is an object and its `toString` method is + // 'broken' (doesn't return a string, isn't a function, etc.), an error will be thrown: + // + // TypeError: Cannot convert object to primitive value + // + // For performance reasons, we don't catch this error here and allow it to propagate up the call + // stack. Note that you'll get the same error in JavaScript if you try to access a property using + // such a 'broken' object as a key. + return name + ''; } - function ensureSafeObject(obj, fullExpression) { - // nifty check if obj is Function that is fast and works across iframes and other contexts - if (obj) { - if (obj.constructor === obj) { - throw $parseMinErr('isecfn', - 'Referencing Function in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } else if (// isWindow(obj) - obj.document && obj.location && obj.alert && obj.setInterval) { - throw $parseMinErr('isecwindow', - 'Referencing the Window in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } else if (// isElement(obj) - obj.children && (obj.nodeName || (obj.prop && obj.attr && obj.find))) { - throw $parseMinErr('isecdom', - 'Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } - } - return obj; - } - var OPERATORS = { - /* jshint bitwise : false */ - 'null':function(){return null;}, - 'true':function(){return true;}, - 'false':function(){return false;}, - undefined:noop, - '+':function(self, locals, a,b){ - a=a(self, locals); b=b(self, locals); - if (isDefined(a)) { - if (isDefined(b)) { - return a + b; - } - return a; - } - return isDefined(b)?b:undefined;}, - '-':function(self, locals, a,b){ - a=a(self, locals); b=b(self, locals); - return (isDefined(a)?a:0)-(isDefined(b)?b:0); - }, - '*':function(self, locals, a,b){return a(self, locals)*b(self, locals);}, - '/':function(self, locals, a,b){return a(self, locals)/b(self, locals);}, - '%':function(self, locals, a,b){return a(self, locals)%b(self, locals);}, - '^':function(self, locals, a,b){return a(self, locals)^b(self, locals);}, - '=':noop, - '===':function(self, locals, a, b){return a(self, locals)===b(self, locals);}, - '!==':function(self, locals, a, b){return a(self, locals)!==b(self, locals);}, - '==':function(self, locals, a,b){return a(self, locals)==b(self, locals);}, - '!=':function(self, locals, a,b){return a(self, locals)!=b(self, locals);}, - '<':function(self, locals, a,b){return a(self, locals)':function(self, locals, a,b){return a(self, locals)>b(self, locals);}, - '<=':function(self, locals, a,b){return a(self, locals)<=b(self, locals);}, - '>=':function(self, locals, a,b){return a(self, locals)>=b(self, locals);}, - '&&':function(self, locals, a,b){return a(self, locals)&&b(self, locals);}, - '||':function(self, locals, a,b){return a(self, locals)||b(self, locals);}, - '&':function(self, locals, a,b){return a(self, locals)&b(self, locals);}, -// '|':function(self, locals, a,b){return a|b;}, - '|':function(self, locals, a,b){return b(self, locals)(self, locals, a(self, locals));}, - '!':function(self, locals, a){return !a(self, locals);} - }; - /* jshint bitwise: true */ - var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'}; + var OPERATORS = createMap(); + forEach('+ - * / % === !== == != < > <= >= && || ! = |'.split(' '), function(operator) { OPERATORS[operator] = true; }); + var ESCAPE = {'n':'\n', 'f':'\f', 'r':'\r', 't':'\t', 'v':'\v', '\'':'\'', '"':'"'}; ///////////////////////////////////////// @@ -9901,85 +14863,51 @@ /** * @constructor */ - var Lexer = function (options) { + var Lexer = function Lexer(options) { this.options = options; }; Lexer.prototype = { constructor: Lexer, - lex: function (text) { + lex: function(text) { this.text = text; - this.index = 0; - this.ch = undefined; - this.lastCh = ':'; // can start regexp - this.tokens = []; - var token; - var json = []; - while (this.index < this.text.length) { - this.ch = this.text.charAt(this.index); - if (this.is('"\'')) { - this.readString(this.ch); - } else if (this.isNumber(this.ch) || this.is('.') && this.isNumber(this.peek())) { + var ch = this.text.charAt(this.index); + if (ch === '"' || ch === '\'') { + this.readString(ch); + } else if (this.isNumber(ch) || ch === '.' && this.isNumber(this.peek())) { this.readNumber(); - } else if (this.isIdent(this.ch)) { + } else if (this.isIdentifierStart(this.peekMultichar())) { this.readIdent(); - // identifiers can only be if the preceding char was a { or , - if (this.was('{,') && json[0] === '{' && - (token = this.tokens[this.tokens.length - 1])) { - token.json = token.text.indexOf('.') === -1; - } - } else if (this.is('(){}[].,;:?')) { - this.tokens.push({ - index: this.index, - text: this.ch, - json: (this.was(':[,') && this.is('{[')) || this.is('}]:,') - }); - if (this.is('{[')) json.unshift(this.ch); - if (this.is('}]')) json.shift(); + } else if (this.is(ch, '(){}[].,;:?')) { + this.tokens.push({index: this.index, text: ch}); this.index++; - } else if (this.isWhitespace(this.ch)) { + } else if (this.isWhitespace(ch)) { this.index++; - continue; } else { - var ch2 = this.ch + this.peek(); + var ch2 = ch + this.peek(); var ch3 = ch2 + this.peek(2); - var fn = OPERATORS[this.ch]; - var fn2 = OPERATORS[ch2]; - var fn3 = OPERATORS[ch3]; - if (fn3) { - this.tokens.push({index: this.index, text: ch3, fn: fn3}); - this.index += 3; - } else if (fn2) { - this.tokens.push({index: this.index, text: ch2, fn: fn2}); - this.index += 2; - } else if (fn) { - this.tokens.push({ - index: this.index, - text: this.ch, - fn: fn, - json: (this.was('[,:') && this.is('+-')) - }); - this.index += 1; + var op1 = OPERATORS[ch]; + var op2 = OPERATORS[ch2]; + var op3 = OPERATORS[ch3]; + if (op1 || op2 || op3) { + var token = op3 ? ch3 : (op2 ? ch2 : ch); + this.tokens.push({index: this.index, text: token, operator: true}); + this.index += token.length; } else { this.throwError('Unexpected next character ', this.index, this.index + 1); } } - this.lastCh = this.ch; } return this.tokens; }, - is: function(chars) { - return chars.indexOf(this.ch) !== -1; - }, - - was: function(chars) { - return chars.indexOf(this.lastCh) !== -1; + is: function(ch, chars) { + return chars.indexOf(ch) !== -1; }, peek: function(i) { @@ -9988,19 +14916,55 @@ }, isNumber: function(ch) { - return ('0' <= ch && ch <= '9'); + return ('0' <= ch && ch <= '9') && typeof ch === 'string'; }, isWhitespace: function(ch) { // IE treats non-breaking space as \u00A0 return (ch === ' ' || ch === '\r' || ch === '\t' || - ch === '\n' || ch === '\v' || ch === '\u00A0'); + ch === '\n' || ch === '\v' || ch === '\u00A0'); + }, + + isIdentifierStart: function(ch) { + return this.options.isIdentifierStart ? + this.options.isIdentifierStart(ch, this.codePointAt(ch)) : + this.isValidIdentifierStart(ch); }, - isIdent: function(ch) { + isValidIdentifierStart: function(ch) { return ('a' <= ch && ch <= 'z' || - 'A' <= ch && ch <= 'Z' || - '_' === ch || ch === '$'); + 'A' <= ch && ch <= 'Z' || + '_' === ch || ch === '$'); + }, + + isIdentifierContinue: function(ch) { + return this.options.isIdentifierContinue ? + this.options.isIdentifierContinue(ch, this.codePointAt(ch)) : + this.isValidIdentifierContinue(ch); + }, + + isValidIdentifierContinue: function(ch, cp) { + return this.isValidIdentifierStart(ch, cp) || this.isNumber(ch); + }, + + codePointAt: function(ch) { + if (ch.length === 1) return ch.charCodeAt(0); + // eslint-disable-next-line no-bitwise + return (ch.charCodeAt(0) << 10) + ch.charCodeAt(1) - 0x35FDC00; + }, + + peekMultichar: function() { + var ch = this.text.charAt(this.index); + var peek = this.peek(); + if (!peek) { + return ch; + } + var cp1 = ch.charCodeAt(0); + var cp2 = peek.charCodeAt(0); + if (cp1 >= 0xD800 && cp1 <= 0xDBFF && cp2 >= 0xDC00 && cp2 <= 0xDFFF) { + return ch + peek; + } + return ch; }, isExpOperator: function(ch) { @@ -10021,19 +14985,19 @@ var start = this.index; while (this.index < this.text.length) { var ch = lowercase(this.text.charAt(this.index)); - if (ch == '.' || this.isNumber(ch)) { + if (ch === '.' || this.isNumber(ch)) { number += ch; } else { var peekCh = this.peek(); - if (ch == 'e' && this.isExpOperator(peekCh)) { + if (ch === 'e' && this.isExpOperator(peekCh)) { number += ch; } else if (this.isExpOperator(ch) && peekCh && this.isNumber(peekCh) && - number.charAt(number.length - 1) == 'e') { + number.charAt(number.length - 1) === 'e') { number += ch; } else if (this.isExpOperator(ch) && (!peekCh || !this.isNumber(peekCh)) && - number.charAt(number.length - 1) == 'e') { + number.charAt(number.length - 1) === 'e') { this.throwError('Invalid exponent'); } else { break; @@ -10041,89 +15005,30 @@ } this.index++; } - number = 1 * number; this.tokens.push({ index: start, text: number, - json: true, - fn: function() { return number; } + constant: true, + value: Number(number) }); }, readIdent: function() { - var parser = this; - - var ident = ''; var start = this.index; - - var lastDot, peekIndex, methodName, ch; - + this.index += this.peekMultichar().length; while (this.index < this.text.length) { - ch = this.text.charAt(this.index); - if (ch === '.' || this.isIdent(ch) || this.isNumber(ch)) { - if (ch === '.') lastDot = this.index; - ident += ch; - } else { + var ch = this.peekMultichar(); + if (!this.isIdentifierContinue(ch)) { break; } - this.index++; - } - - //check if this is not a method invocation and if it is back out to last dot - if (lastDot) { - peekIndex = this.index; - while (peekIndex < this.text.length) { - ch = this.text.charAt(peekIndex); - if (ch === '(') { - methodName = ident.substr(lastDot - start + 1); - ident = ident.substr(0, lastDot - start); - this.index = peekIndex; - break; - } - if (this.isWhitespace(ch)) { - peekIndex++; - } else { - break; - } - } + this.index += ch.length; } - - - var token = { + this.tokens.push({ index: start, - text: ident - }; - - // OPERATORS is our own object so we don't need to use special hasOwnPropertyFn - if (OPERATORS.hasOwnProperty(ident)) { - token.fn = OPERATORS[ident]; - token.json = OPERATORS[ident]; - } else { - var getter = getterFn(ident, this.options, this.text); - token.fn = extend(function(self, locals) { - return (getter(self, locals)); - }, { - assign: function(self, value) { - return setter(self, ident, value, parser.text, parser.options); - } - }); - } - - this.tokens.push(token); - - if (methodName) { - this.tokens.push({ - index:lastDot, - text: '.', - json: false - }); - this.tokens.push({ - index: lastDot + 1, - text: methodName, - json: false - }); - } - }, + text: this.text.slice(start, this.index), + identifier: true + }); + }, readString: function(quote) { var start = this.index; @@ -10137,17 +15042,14 @@ if (escape) { if (ch === 'u') { var hex = this.text.substring(this.index + 1, this.index + 5); - if (!hex.match(/[\da-f]{4}/i)) + if (!hex.match(/[\da-f]{4}/i)) { this.throwError('Invalid unicode escape [\\u' + hex + ']'); + } this.index += 4; string += String.fromCharCode(parseInt(hex, 16)); } else { var rep = ESCAPE[ch]; - if (rep) { - string += rep; - } else { - string += ch; - } + string = string + (rep || ch); } escape = false; } else if (ch === '\\') { @@ -10157,9 +15059,8 @@ this.tokens.push({ index: start, text: rawString, - string: string, - json: true, - fn: function() { return string; } + constant: true, + value: string }); return; } else { @@ -10171,219 +15072,66 @@ } }; - - /** - * @constructor - */ - var Parser = function (lexer, $filter, options) { + var AST = function AST(lexer, options) { this.lexer = lexer; - this.$filter = $filter; this.options = options; }; - Parser.ZERO = extend(function () { - return 0; - }, { - constant: true - }); - - Parser.prototype = { - constructor: Parser, - - parse: function (text, json) { + AST.Program = 'Program'; + AST.ExpressionStatement = 'ExpressionStatement'; + AST.AssignmentExpression = 'AssignmentExpression'; + AST.ConditionalExpression = 'ConditionalExpression'; + AST.LogicalExpression = 'LogicalExpression'; + AST.BinaryExpression = 'BinaryExpression'; + AST.UnaryExpression = 'UnaryExpression'; + AST.CallExpression = 'CallExpression'; + AST.MemberExpression = 'MemberExpression'; + AST.Identifier = 'Identifier'; + AST.Literal = 'Literal'; + AST.ArrayExpression = 'ArrayExpression'; + AST.Property = 'Property'; + AST.ObjectExpression = 'ObjectExpression'; + AST.ThisExpression = 'ThisExpression'; + AST.LocalsExpression = 'LocalsExpression'; + +// Internal use only + AST.NGValueParameter = 'NGValueParameter'; + + AST.prototype = { + ast: function(text) { this.text = text; - - //TODO(i): strip all the obsolte json stuff from this file - this.json = json; - this.tokens = this.lexer.lex(text); - if (json) { - // The extra level of aliasing is here, just in case the lexer misses something, so that - // we prevent any accidental execution in JSON. - this.assignment = this.logicalOR; - - this.functionCall = - this.fieldAccess = - this.objectIndex = - this.filterChain = function() { - this.throwError('is not valid json', {text: text, index: 0}); - }; - } - - var value = json ? this.primary() : this.statements(); + var value = this.program(); if (this.tokens.length !== 0) { this.throwError('is an unexpected token', this.tokens[0]); } - value.literal = !!value.literal; - value.constant = !!value.constant; - return value; }, - primary: function () { - var primary; - if (this.expect('(')) { - primary = this.filterChain(); - this.consume(')'); - } else if (this.expect('[')) { - primary = this.arrayDeclaration(); - } else if (this.expect('{')) { - primary = this.object(); - } else { - var token = this.expect(); - primary = token.fn; - if (!primary) { - this.throwError('not a primary expression', token); - } - if (token.json) { - primary.constant = true; - primary.literal = true; - } - } - - var next, context; - while ((next = this.expect('(', '[', '.'))) { - if (next.text === '(') { - primary = this.functionCall(primary, context); - context = null; - } else if (next.text === '[') { - context = primary; - primary = this.objectIndex(primary); - } else if (next.text === '.') { - context = primary; - primary = this.fieldAccess(primary); - } else { - this.throwError('IMPOSSIBLE'); - } - } - return primary; - }, - - throwError: function(msg, token) { - throw $parseMinErr('syntax', - 'Syntax Error: Token \'{0}\' {1} at column {2} of the expression [{3}] starting at [{4}].', - token.text, msg, (token.index + 1), this.text, this.text.substring(token.index)); - }, - - peekToken: function() { - if (this.tokens.length === 0) - throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text); - return this.tokens[0]; - }, - - peek: function(e1, e2, e3, e4) { - if (this.tokens.length > 0) { - var token = this.tokens[0]; - var t = token.text; - if (t === e1 || t === e2 || t === e3 || t === e4 || - (!e1 && !e2 && !e3 && !e4)) { - return token; - } - } - return false; - }, - - expect: function(e1, e2, e3, e4){ - var token = this.peek(e1, e2, e3, e4); - if (token) { - if (this.json && !token.json) { - this.throwError('is not valid json', token); - } - this.tokens.shift(); - return token; - } - return false; - }, - - consume: function(e1){ - if (!this.expect(e1)) { - this.throwError('is unexpected, expecting [' + e1 + ']', this.peek()); - } - }, - - unaryFn: function(fn, right) { - return extend(function(self, locals) { - return fn(self, locals, right); - }, { - constant:right.constant - }); - }, - - ternaryFn: function(left, middle, right){ - return extend(function(self, locals){ - return left(self, locals) ? middle(self, locals) : right(self, locals); - }, { - constant: left.constant && middle.constant && right.constant - }); - }, - - binaryFn: function(left, fn, right) { - return extend(function(self, locals) { - return fn(self, locals, left, right); - }, { - constant:left.constant && right.constant - }); - }, - - statements: function() { - var statements = []; + program: function() { + var body = []; while (true) { if (this.tokens.length > 0 && !this.peek('}', ')', ';', ']')) - statements.push(this.filterChain()); + body.push(this.expressionStatement()); if (!this.expect(';')) { - // optimize for the common case where there is only one statement. - // TODO(size): maybe we should not support multiple statements? - return (statements.length === 1) - ? statements[0] - : function(self, locals) { - var value; - for (var i = 0; i < statements.length; i++) { - var statement = statements[i]; - if (statement) { - value = statement(self, locals); - } - } - return value; - }; + return { type: AST.Program, body: body}; } } }, - filterChain: function() { - var left = this.expression(); - var token; - while (true) { - if ((token = this.expect('|'))) { - left = this.binaryFn(left, token.fn, this.filter()); - } else { - return left; - } - } + expressionStatement: function() { + return { type: AST.ExpressionStatement, expression: this.filterChain() }; }, - filter: function() { - var token = this.expect(); - var fn = this.$filter(token.text); - var argsFn = []; - while (true) { - if ((token = this.expect(':'))) { - argsFn.push(this.expression()); - } else { - var fnInvoke = function(self, locals, input) { - var args = [input]; - for (var i = 0; i < argsFn.length; i++) { - args.push(argsFn[i](self, locals)); - } - return fn.apply(self, args); - }; - return function() { - return fnInvoke; - }; - } + filterChain: function() { + var left = this.expression(); + while (this.expect('|')) { + left = this.filter(left); } + return left; }, expression: function() { @@ -10391,55 +15139,43 @@ }, assignment: function() { - var left = this.ternary(); - var right; - var token; - if ((token = this.expect('='))) { - if (!left.assign) { - this.throwError('implies assignment but [' + - this.text.substring(0, token.index) + '] can not be assigned to', token); - } - right = this.ternary(); - return function(scope, locals) { - return left.assign(scope, right(scope, locals), locals); - }; + var result = this.ternary(); + if (this.expect('=')) { + if (!isAssignable(result)) { + throw $parseMinErr('lval', 'Trying to assign a value to a non l-value'); + } + + result = { type: AST.AssignmentExpression, left: result, right: this.assignment(), operator: '='}; } - return left; + return result; }, ternary: function() { - var left = this.logicalOR(); - var middle; - var token; - if ((token = this.expect('?'))) { - middle = this.ternary(); - if ((token = this.expect(':'))) { - return this.ternaryFn(left, middle, this.ternary()); - } else { - this.throwError('expected :', token); + var test = this.logicalOR(); + var alternate; + var consequent; + if (this.expect('?')) { + alternate = this.expression(); + if (this.consume(':')) { + consequent = this.expression(); + return { type: AST.ConditionalExpression, test: test, alternate: alternate, consequent: consequent}; } - } else { - return left; } + return test; }, logicalOR: function() { var left = this.logicalAND(); - var token; - while (true) { - if ((token = this.expect('||'))) { - left = this.binaryFn(left, token.fn, this.logicalAND()); - } else { - return left; - } + while (this.expect('||')) { + left = { type: AST.LogicalExpression, operator: '||', left: left, right: this.logicalAND() }; } + return left; }, logicalAND: function() { var left = this.equality(); - var token; - if ((token = this.expect('&&'))) { - left = this.binaryFn(left, token.fn, this.logicalAND()); + while (this.expect('&&')) { + left = { type: AST.LogicalExpression, operator: '&&', left: left, right: this.equality()}; } return left; }, @@ -10447,8 +15183,8 @@ equality: function() { var left = this.relational(); var token; - if ((token = this.expect('==','!=','===','!=='))) { - left = this.binaryFn(left, token.fn, this.equality()); + while ((token = this.expect('==','!=','===','!=='))) { + left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.relational() }; } return left; }, @@ -10456,8 +15192,8 @@ relational: function() { var left = this.additive(); var token; - if ((token = this.expect('<', '>', '<=', '>='))) { - left = this.binaryFn(left, token.fn, this.relational()); + while ((token = this.expect('<', '>', '<=', '>='))) { + left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.additive() }; } return left; }, @@ -10466,7 +15202,7 @@ var left = this.multiplicative(); var token; while ((token = this.expect('+','-'))) { - left = this.binaryFn(left, token.fn, this.multiplicative()); + left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.multiplicative() }; } return left; }, @@ -10475,9029 +15211,15853 @@ var left = this.unary(); var token; while ((token = this.expect('*','/','%'))) { - left = this.binaryFn(left, token.fn, this.unary()); + left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.unary() }; } return left; }, unary: function() { var token; - if (this.expect('+')) { - return this.primary(); - } else if ((token = this.expect('-'))) { - return this.binaryFn(Parser.ZERO, token.fn, this.unary()); - } else if ((token = this.expect('!'))) { - return this.unaryFn(token.fn, this.unary()); + if ((token = this.expect('+', '-', '!'))) { + return { type: AST.UnaryExpression, operator: token.text, prefix: true, argument: this.unary() }; } else { return this.primary(); } }, - fieldAccess: function(object) { - var parser = this; - var field = this.expect().text; - var getter = getterFn(field, this.options, this.text); + primary: function() { + var primary; + if (this.expect('(')) { + primary = this.filterChain(); + this.consume(')'); + } else if (this.expect('[')) { + primary = this.arrayDeclaration(); + } else if (this.expect('{')) { + primary = this.object(); + } else if (this.selfReferential.hasOwnProperty(this.peek().text)) { + primary = copy(this.selfReferential[this.consume().text]); + } else if (this.options.literals.hasOwnProperty(this.peek().text)) { + primary = { type: AST.Literal, value: this.options.literals[this.consume().text]}; + } else if (this.peek().identifier) { + primary = this.identifier(); + } else if (this.peek().constant) { + primary = this.constant(); + } else { + this.throwError('not a primary expression', this.peek()); + } - return extend(function(scope, locals, self) { - return getter(self || object(scope, locals)); - }, { - assign: function(scope, value, locals) { - return setter(object(scope, locals), field, value, parser.text, parser.options); + var next; + while ((next = this.expect('(', '[', '.'))) { + if (next.text === '(') { + primary = {type: AST.CallExpression, callee: primary, arguments: this.parseArguments() }; + this.consume(')'); + } else if (next.text === '[') { + primary = { type: AST.MemberExpression, object: primary, property: this.expression(), computed: true }; + this.consume(']'); + } else if (next.text === '.') { + primary = { type: AST.MemberExpression, object: primary, property: this.identifier(), computed: false }; + } else { + this.throwError('IMPOSSIBLE'); } - }); + } + return primary; }, - objectIndex: function(obj) { - var parser = this; + filter: function(baseExpression) { + var args = [baseExpression]; + var result = {type: AST.CallExpression, callee: this.identifier(), arguments: args, filter: true}; - var indexFn = this.expression(); - this.consume(']'); + while (this.expect(':')) { + args.push(this.expression()); + } - return extend(function(self, locals) { - var o = obj(self, locals), - i = indexFn(self, locals), - v, p; - - if (!o) return undefined; - v = ensureSafeObject(o[i], parser.text); - if (v && v.then && parser.options.unwrapPromises) { - p = v; - if (!('$$v' in v)) { - p.$$v = undefined; - p.then(function(val) { p.$$v = val; }); - } - v = v.$$v; - } - return v; - }, { - assign: function(self, value, locals) { - var key = indexFn(self, locals); - // prevent overwriting of Function.constructor which would break ensureSafeObject check - var safe = ensureSafeObject(obj(self, locals), parser.text); - return safe[key] = value; - } - }); + return result; }, - functionCall: function(fn, contextGetter) { - var argsFn = []; + parseArguments: function() { + var args = []; if (this.peekToken().text !== ')') { do { - argsFn.push(this.expression()); + args.push(this.filterChain()); } while (this.expect(',')); } - this.consume(')'); - - var parser = this; - - return function(scope, locals) { - var args = []; - var context = contextGetter ? contextGetter(scope, locals) : scope; - - for (var i = 0; i < argsFn.length; i++) { - args.push(argsFn[i](scope, locals)); - } - var fnPtr = fn(scope, locals, context) || noop; - - ensureSafeObject(context, parser.text); - ensureSafeObject(fnPtr, parser.text); + return args; + }, - // IE stupidity! (IE doesn't have apply for some native functions) - var v = fnPtr.apply - ? fnPtr.apply(context, args) - : fnPtr(args[0], args[1], args[2], args[3], args[4]); + identifier: function() { + var token = this.consume(); + if (!token.identifier) { + this.throwError('is not a valid identifier', token); + } + return { type: AST.Identifier, name: token.text }; + }, - return ensureSafeObject(v, parser.text); - }; + constant: function() { + // TODO check that it is a constant + return { type: AST.Literal, value: this.consume().value }; }, - // This is used with json array declaration - arrayDeclaration: function () { - var elementFns = []; - var allConstant = true; + arrayDeclaration: function() { + var elements = []; if (this.peekToken().text !== ']') { do { if (this.peek(']')) { // Support trailing commas per ES5.1. break; } - var elementFn = this.expression(); - elementFns.push(elementFn); - if (!elementFn.constant) { - allConstant = false; - } + elements.push(this.expression()); } while (this.expect(',')); } this.consume(']'); - return extend(function(self, locals) { - var array = []; - for (var i = 0; i < elementFns.length; i++) { - array.push(elementFns[i](self, locals)); - } - return array; - }, { - literal: true, - constant: allConstant - }); + return { type: AST.ArrayExpression, elements: elements }; }, - object: function () { - var keyValues = []; - var allConstant = true; + object: function() { + var properties = [], property; if (this.peekToken().text !== '}') { do { if (this.peek('}')) { // Support trailing commas per ES5.1. break; } - var token = this.expect(), - key = token.string || token.text; - this.consume(':'); - var value = this.expression(); - keyValues.push({key: key, value: value}); - if (!value.constant) { - allConstant = false; + property = {type: AST.Property, kind: 'init'}; + if (this.peek().constant) { + property.key = this.constant(); + property.computed = false; + this.consume(':'); + property.value = this.expression(); + } else if (this.peek().identifier) { + property.key = this.identifier(); + property.computed = false; + if (this.peek(':')) { + this.consume(':'); + property.value = this.expression(); + } else { + property.value = property.key; + } + } else if (this.peek('[')) { + this.consume('['); + property.key = this.expression(); + this.consume(']'); + property.computed = true; + this.consume(':'); + property.value = this.expression(); + } else { + this.throwError('invalid key', this.peek()); } + properties.push(property); } while (this.expect(',')); } this.consume('}'); - return extend(function(self, locals) { - var object = {}; - for (var i = 0; i < keyValues.length; i++) { - var keyValue = keyValues[i]; - object[keyValue.key] = keyValue.value(self, locals); - } - return object; - }, { - literal: true, - constant: allConstant - }); - } - }; + return {type: AST.ObjectExpression, properties: properties }; + }, + throwError: function(msg, token) { + throw $parseMinErr('syntax', + 'Syntax Error: Token \'{0}\' {1} at column {2} of the expression [{3}] starting at [{4}].', + token.text, msg, (token.index + 1), this.text, this.text.substring(token.index)); + }, -////////////////////////////////////////////////// -// Parser helper functions -////////////////////////////////////////////////// + consume: function(e1) { + if (this.tokens.length === 0) { + throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text); + } - function setter(obj, path, setValue, fullExp, options) { - //needed? - options = options || {}; + var token = this.expect(e1); + if (!token) { + this.throwError('is unexpected, expecting [' + e1 + ']', this.peek()); + } + return token; + }, - var element = path.split('.'), key; - for (var i = 0; element.length > 1; i++) { - key = ensureSafeMemberName(element.shift(), fullExp); - var propertyObj = obj[key]; - if (!propertyObj) { - propertyObj = {}; - obj[key] = propertyObj; + peekToken: function() { + if (this.tokens.length === 0) { + throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text); } - obj = propertyObj; - if (obj.then && options.unwrapPromises) { - promiseWarning(fullExp); - if (!("$$v" in obj)) { - (function(promise) { - promise.then(function(val) { promise.$$v = val; }); } - )(obj); - } - if (obj.$$v === undefined) { - obj.$$v = {}; + return this.tokens[0]; + }, + + peek: function(e1, e2, e3, e4) { + return this.peekAhead(0, e1, e2, e3, e4); + }, + + peekAhead: function(i, e1, e2, e3, e4) { + if (this.tokens.length > i) { + var token = this.tokens[i]; + var t = token.text; + if (t === e1 || t === e2 || t === e3 || t === e4 || + (!e1 && !e2 && !e3 && !e4)) { + return token; } - obj = obj.$$v; } + return false; + }, + + expect: function(e1, e2, e3, e4) { + var token = this.peek(e1, e2, e3, e4); + if (token) { + this.tokens.shift(); + return token; + } + return false; + }, + + selfReferential: { + 'this': {type: AST.ThisExpression }, + '$locals': {type: AST.LocalsExpression } } - key = ensureSafeMemberName(element.shift(), fullExp); - obj[key] = setValue; - return setValue; + }; + + function ifDefined(v, d) { + return typeof v !== 'undefined' ? v : d; + } + + function plusFn(l, r) { + if (typeof l === 'undefined') return r; + if (typeof r === 'undefined') return l; + return l + r; } - var getterFnCache = {}; + function isStateless($filter, filterName) { + var fn = $filter(filterName); + return !fn.$stateful; + } - /** - * Implementation of the "Black Hole" variant from: - * - http://jsperf.com/angularjs-parse-getter/4 - * - http://jsperf.com/path-evaluation-simplified/7 - */ - function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp, options) { - ensureSafeMemberName(key0, fullExp); - ensureSafeMemberName(key1, fullExp); - ensureSafeMemberName(key2, fullExp); - ensureSafeMemberName(key3, fullExp); - ensureSafeMemberName(key4, fullExp); + var PURITY_ABSOLUTE = 1; + var PURITY_RELATIVE = 2; - return !options.unwrapPromises - ? function cspSafeGetter(scope, locals) { - var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope; +// Detect nodes which could depend on non-shallow state of objects + function isPure(node, parentIsPure) { + switch (node.type) { + // Computed members might invoke a stateful toString() + case AST.MemberExpression: + if (node.computed) { + return false; + } + break; - if (pathVal == null) return pathVal; - pathVal = pathVal[key0]; + // Unary always convert to primative + case AST.UnaryExpression: + return PURITY_ABSOLUTE; - if (!key1) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key1]; + // The binary + operator can invoke a stateful toString(). + case AST.BinaryExpression: + return node.operator !== '+' ? PURITY_ABSOLUTE : false; + + // Functions / filters probably read state from within objects + case AST.CallExpression: + return false; + } - if (!key2) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key2]; + return (undefined === parentIsPure) ? PURITY_RELATIVE : parentIsPure; + } - if (!key3) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key3]; + function findConstantAndWatchExpressions(ast, $filter, parentIsPure) { + var allConstants; + var argsToWatch; + var isStatelessFilter; - if (!key4) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key4]; + var astIsPure = ast.isPure = isPure(ast, parentIsPure); - return pathVal; + switch (ast.type) { + case AST.Program: + allConstants = true; + forEach(ast.body, function(expr) { + findConstantAndWatchExpressions(expr.expression, $filter, astIsPure); + allConstants = allConstants && expr.expression.constant; + }); + ast.constant = allConstants; + break; + case AST.Literal: + ast.constant = true; + ast.toWatch = []; + break; + case AST.UnaryExpression: + findConstantAndWatchExpressions(ast.argument, $filter, astIsPure); + ast.constant = ast.argument.constant; + ast.toWatch = ast.argument.toWatch; + break; + case AST.BinaryExpression: + findConstantAndWatchExpressions(ast.left, $filter, astIsPure); + findConstantAndWatchExpressions(ast.right, $filter, astIsPure); + ast.constant = ast.left.constant && ast.right.constant; + ast.toWatch = ast.left.toWatch.concat(ast.right.toWatch); + break; + case AST.LogicalExpression: + findConstantAndWatchExpressions(ast.left, $filter, astIsPure); + findConstantAndWatchExpressions(ast.right, $filter, astIsPure); + ast.constant = ast.left.constant && ast.right.constant; + ast.toWatch = ast.constant ? [] : [ast]; + break; + case AST.ConditionalExpression: + findConstantAndWatchExpressions(ast.test, $filter, astIsPure); + findConstantAndWatchExpressions(ast.alternate, $filter, astIsPure); + findConstantAndWatchExpressions(ast.consequent, $filter, astIsPure); + ast.constant = ast.test.constant && ast.alternate.constant && ast.consequent.constant; + ast.toWatch = ast.constant ? [] : [ast]; + break; + case AST.Identifier: + ast.constant = false; + ast.toWatch = [ast]; + break; + case AST.MemberExpression: + findConstantAndWatchExpressions(ast.object, $filter, astIsPure); + if (ast.computed) { + findConstantAndWatchExpressions(ast.property, $filter, astIsPure); + } + ast.constant = ast.object.constant && (!ast.computed || ast.property.constant); + ast.toWatch = ast.constant ? [] : [ast]; + break; + case AST.CallExpression: + isStatelessFilter = ast.filter ? isStateless($filter, ast.callee.name) : false; + allConstants = isStatelessFilter; + argsToWatch = []; + forEach(ast.arguments, function(expr) { + findConstantAndWatchExpressions(expr, $filter, astIsPure); + allConstants = allConstants && expr.constant; + argsToWatch.push.apply(argsToWatch, expr.toWatch); + }); + ast.constant = allConstants; + ast.toWatch = isStatelessFilter ? argsToWatch : [ast]; + break; + case AST.AssignmentExpression: + findConstantAndWatchExpressions(ast.left, $filter, astIsPure); + findConstantAndWatchExpressions(ast.right, $filter, astIsPure); + ast.constant = ast.left.constant && ast.right.constant; + ast.toWatch = [ast]; + break; + case AST.ArrayExpression: + allConstants = true; + argsToWatch = []; + forEach(ast.elements, function(expr) { + findConstantAndWatchExpressions(expr, $filter, astIsPure); + allConstants = allConstants && expr.constant; + argsToWatch.push.apply(argsToWatch, expr.toWatch); + }); + ast.constant = allConstants; + ast.toWatch = argsToWatch; + break; + case AST.ObjectExpression: + allConstants = true; + argsToWatch = []; + forEach(ast.properties, function(property) { + findConstantAndWatchExpressions(property.value, $filter, astIsPure); + allConstants = allConstants && property.value.constant; + argsToWatch.push.apply(argsToWatch, property.value.toWatch); + if (property.computed) { + //`{[key]: value}` implicitly does `key.toString()` which may be non-pure + findConstantAndWatchExpressions(property.key, $filter, /*parentIsPure=*/false); + allConstants = allConstants && property.key.constant; + argsToWatch.push.apply(argsToWatch, property.key.toWatch); + } + }); + ast.constant = allConstants; + ast.toWatch = argsToWatch; + break; + case AST.ThisExpression: + ast.constant = false; + ast.toWatch = []; + break; + case AST.LocalsExpression: + ast.constant = false; + ast.toWatch = []; + break; } - : function cspSafePromiseEnabledGetter(scope, locals) { - var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope, - promise; - - if (pathVal == null) return pathVal; - - pathVal = pathVal[key0]; - if (pathVal && pathVal.then) { - promiseWarning(fullExp); - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - - if (!key1) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key1]; - if (pathVal && pathVal.then) { - promiseWarning(fullExp); - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - - if (!key2) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key2]; - if (pathVal && pathVal.then) { - promiseWarning(fullExp); - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - - if (!key3) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key3]; - if (pathVal && pathVal.then) { - promiseWarning(fullExp); - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - - if (!key4) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key4]; - if (pathVal && pathVal.then) { - promiseWarning(fullExp); - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - return pathVal; - }; } - function simpleGetterFn1(key0, fullExp) { - ensureSafeMemberName(key0, fullExp); + function getInputs(body) { + if (body.length !== 1) return; + var lastExpression = body[0].expression; + var candidate = lastExpression.toWatch; + if (candidate.length !== 1) return candidate; + return candidate[0] !== lastExpression ? candidate : undefined; + } - return function simpleGetterFn1(scope, locals) { - if (scope == null) return undefined; - return ((locals && locals.hasOwnProperty(key0)) ? locals : scope)[key0]; - }; + function isAssignable(ast) { + return ast.type === AST.Identifier || ast.type === AST.MemberExpression; } - function simpleGetterFn2(key0, key1, fullExp) { - ensureSafeMemberName(key0, fullExp); - ensureSafeMemberName(key1, fullExp); + function assignableAST(ast) { + if (ast.body.length === 1 && isAssignable(ast.body[0].expression)) { + return {type: AST.AssignmentExpression, left: ast.body[0].expression, right: {type: AST.NGValueParameter}, operator: '='}; + } + } - return function simpleGetterFn2(scope, locals) { - if (scope == null) return undefined; - scope = ((locals && locals.hasOwnProperty(key0)) ? locals : scope)[key0]; - return scope == null ? undefined : scope[key1]; - }; + function isLiteral(ast) { + return ast.body.length === 0 || + ast.body.length === 1 && ( + ast.body[0].expression.type === AST.Literal || + ast.body[0].expression.type === AST.ArrayExpression || + ast.body[0].expression.type === AST.ObjectExpression); } - function getterFn(path, options, fullExp) { - // Check whether the cache has this getter already. - // We can use hasOwnProperty directly on the cache because we ensure, - // see below, that the cache never stores a path called 'hasOwnProperty' - if (getterFnCache.hasOwnProperty(path)) { - return getterFnCache[path]; - } + function isConstant(ast) { + return ast.constant; + } - var pathKeys = path.split('.'), - pathKeysLength = pathKeys.length, - fn; - - // When we have only 1 or 2 tokens, use optimized special case closures. - // http://jsperf.com/angularjs-parse-getter/6 - if (!options.unwrapPromises && pathKeysLength === 1) { - fn = simpleGetterFn1(pathKeys[0], fullExp); - } else if (!options.unwrapPromises && pathKeysLength === 2) { - fn = simpleGetterFn2(pathKeys[0], pathKeys[1], fullExp); - } else if (options.csp) { - if (pathKeysLength < 6) { - fn = cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp, - options); - } else { - fn = function(scope, locals) { - var i = 0, val; - do { - val = cspSafeGetterFn(pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], - pathKeys[i++], fullExp, options)(scope, locals); + function ASTCompiler($filter) { + this.$filter = $filter; + } - locals = undefined; // clear after first iteration - scope = val; - } while (i < pathKeysLength); - return val; - }; + ASTCompiler.prototype = { + compile: function(ast) { + var self = this; + this.state = { + nextId: 0, + filters: {}, + fn: {vars: [], body: [], own: {}}, + assign: {vars: [], body: [], own: {}}, + inputs: [] + }; + findConstantAndWatchExpressions(ast, self.$filter); + var extra = ''; + var assignable; + this.stage = 'assign'; + if ((assignable = assignableAST(ast))) { + this.state.computing = 'assign'; + var result = this.nextId(); + this.recurse(assignable, result); + this.return_(result); + extra = 'fn.assign=' + this.generateFunction('assign', 's,v,l'); } - } else { - var code = 'var p;\n'; - forEach(pathKeys, function(key, index) { - ensureSafeMemberName(key, fullExp); - code += 'if(s == null) return undefined;\n' + - 's='+ (index - // we simply dereference 's' on any .dot notation - ? 's' - // but if we are first then we check locals first, and if so read it first - : '((k&&k.hasOwnProperty("' + key + '"))?k:s)') + '["' + key + '"]' + ';\n' + - (options.unwrapPromises - ? 'if (s && s.then) {\n' + - ' pw("' + fullExp.replace(/(["\r\n])/g, '\\$1') + '");\n' + - ' if (!("$$v" in s)) {\n' + - ' p=s;\n' + - ' p.$$v = undefined;\n' + - ' p.then(function(v) {p.$$v=v;});\n' + - '}\n' + - ' s=s.$$v\n' + - '}\n' - : ''); + var toWatch = getInputs(ast.body); + self.stage = 'inputs'; + forEach(toWatch, function(watch, key) { + var fnKey = 'fn' + key; + self.state[fnKey] = {vars: [], body: [], own: {}}; + self.state.computing = fnKey; + var intoId = self.nextId(); + self.recurse(watch, intoId); + self.return_(intoId); + self.state.inputs.push({name: fnKey, isPure: watch.isPure}); + watch.watchId = key; }); - code += 'return s;'; - - /* jshint -W054 */ - var evaledFnGetter = new Function('s', 'k', 'pw', code); // s=scope, k=locals, pw=promiseWarning - /* jshint +W054 */ - evaledFnGetter.toString = valueFn(code); - fn = options.unwrapPromises ? function(scope, locals) { - return evaledFnGetter(scope, locals, promiseWarning); - } : evaledFnGetter; - } + this.state.computing = 'fn'; + this.stage = 'main'; + this.recurse(ast); + var fnString = + // The build and minification steps remove the string "use strict" from the code, but this is done using a regex. + // This is a workaround for this until we do a better job at only removing the prefix only when we should. + '"' + this.USE + ' ' + this.STRICT + '";\n' + + this.filterPrefix() + + 'var fn=' + this.generateFunction('fn', 's,l,a,i') + + extra + + this.watchFns() + + 'return fn;'; + + // eslint-disable-next-line no-new-func + var fn = (new Function('$filter', + 'getStringValue', + 'ifDefined', + 'plus', + fnString))( + this.$filter, + getStringValue, + ifDefined, + plusFn); + this.state = this.stage = undefined; + return fn; + }, - // Only cache the value if it's not going to mess up the cache object - // This is more performant that using Object.prototype.hasOwnProperty.call - if (path !== 'hasOwnProperty') { - getterFnCache[path] = fn; - } - return fn; - } + USE: 'use', -/////////////////////////////////// + STRICT: 'strict', - /** - * @ngdoc service - * @name $parse - * @kind function - * - * @description - * - * Converts Angular {@link guide/expression expression} into a function. - * - * ```js - * var getter = $parse('user.name'); - * var setter = getter.assign; - * var context = {user:{name:'angular'}}; - * var locals = {user:{name:'local'}}; - * - * expect(getter(context)).toEqual('angular'); - * setter(context, 'newValue'); - * expect(context.user.name).toEqual('newValue'); - * expect(getter(context, locals)).toEqual('local'); - * ``` - * - * - * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: - * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. - * - * The returned function also has the following properties: - * * `literal` – `{boolean}` – whether the expression's top-level node is a JavaScript - * literal. - * * `constant` – `{boolean}` – whether the expression is made entirely of JavaScript - * constant literals. - * * `assign` – `{?function(context, value)}` – if the expression is assignable, this will be - * set to a function to change its value on the given context. - * - */ + watchFns: function() { + var result = []; + var inputs = this.state.inputs; + var self = this; + forEach(inputs, function(input) { + result.push('var ' + input.name + '=' + self.generateFunction(input.name, 's')); + if (input.isPure) { + result.push(input.name, '.isPure=' + JSON.stringify(input.isPure) + ';'); + } + }); + if (inputs.length) { + result.push('fn.inputs=[' + inputs.map(function(i) { return i.name; }).join(',') + '];'); + } + return result.join(''); + }, + generateFunction: function(name, params) { + return 'function(' + params + '){' + + this.varsPrefix(name) + + this.body(name) + + '};'; + }, - /** - * @ngdoc provider - * @name $parseProvider - * @function - * - * @description - * `$parseProvider` can be used for configuring the default behavior of the {@link ng.$parse $parse} - * service. - */ - function $ParseProvider() { - var cache = {}; + filterPrefix: function() { + var parts = []; + var self = this; + forEach(this.state.filters, function(id, filter) { + parts.push(id + '=$filter(' + self.escape(filter) + ')'); + }); + if (parts.length) return 'var ' + parts.join(',') + ';'; + return ''; + }, - var $parseOptions = { - csp: false, - unwrapPromises: false, - logPromiseWarnings: true - }; + varsPrefix: function(section) { + return this.state[section].vars.length ? 'var ' + this.state[section].vars.join(',') + ';' : ''; + }, + body: function(section) { + return this.state[section].body.join(''); + }, - /** - * @deprecated Promise unwrapping via $parse is deprecated and will be removed in the future. - * - * @ngdoc method - * @name $parseProvider#unwrapPromises - * @description - * - * **This feature is deprecated, see deprecation notes below for more info** - * - * If set to true (default is false), $parse will unwrap promises automatically when a promise is - * found at any part of the expression. In other words, if set to true, the expression will always - * result in a non-promise value. - * - * While the promise is unresolved, it's treated as undefined, but once resolved and fulfilled, - * the fulfillment value is used in place of the promise while evaluating the expression. - * - * **Deprecation notice** - * - * This is a feature that didn't prove to be wildly useful or popular, primarily because of the - * dichotomy between data access in templates (accessed as raw values) and controller code - * (accessed as promises). - * - * In most code we ended up resolving promises manually in controllers anyway and thus unifying - * the model access there. - * - * Other downsides of automatic promise unwrapping: - * - * - when building components it's often desirable to receive the raw promises - * - adds complexity and slows down expression evaluation - * - makes expression code pre-generation unattractive due to the amount of code that needs to be - * generated - * - makes IDE auto-completion and tool support hard - * - * **Warning Logs** - * - * If the unwrapping is enabled, Angular will log a warning about each expression that unwraps a - * promise (to reduce the noise, each expression is logged only once). To disable this logging use - * `$parseProvider.logPromiseWarnings(false)` api. - * - * - * @param {boolean=} value New value. - * @returns {boolean|self} Returns the current setting when used as getter and self if used as - * setter. - */ - this.unwrapPromises = function(value) { - if (isDefined(value)) { - $parseOptions.unwrapPromises = !!value; - return this; + recurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) { + var left, right, self = this, args, expression, computed; + recursionFn = recursionFn || noop; + if (!skipWatchIdCheck && isDefined(ast.watchId)) { + intoId = intoId || this.nextId(); + this.if_('i', + this.lazyAssign(intoId, this.computedMember('i', ast.watchId)), + this.lazyRecurse(ast, intoId, nameId, recursionFn, create, true) + ); + return; + } + switch (ast.type) { + case AST.Program: + forEach(ast.body, function(expression, pos) { + self.recurse(expression.expression, undefined, undefined, function(expr) { right = expr; }); + if (pos !== ast.body.length - 1) { + self.current().body.push(right, ';'); + } else { + self.return_(right); + } + }); + break; + case AST.Literal: + expression = this.escape(ast.value); + this.assign(intoId, expression); + recursionFn(intoId || expression); + break; + case AST.UnaryExpression: + this.recurse(ast.argument, undefined, undefined, function(expr) { right = expr; }); + expression = ast.operator + '(' + this.ifDefined(right, 0) + ')'; + this.assign(intoId, expression); + recursionFn(expression); + break; + case AST.BinaryExpression: + this.recurse(ast.left, undefined, undefined, function(expr) { left = expr; }); + this.recurse(ast.right, undefined, undefined, function(expr) { right = expr; }); + if (ast.operator === '+') { + expression = this.plus(left, right); + } else if (ast.operator === '-') { + expression = this.ifDefined(left, 0) + ast.operator + this.ifDefined(right, 0); + } else { + expression = '(' + left + ')' + ast.operator + '(' + right + ')'; + } + this.assign(intoId, expression); + recursionFn(expression); + break; + case AST.LogicalExpression: + intoId = intoId || this.nextId(); + self.recurse(ast.left, intoId); + self.if_(ast.operator === '&&' ? intoId : self.not(intoId), self.lazyRecurse(ast.right, intoId)); + recursionFn(intoId); + break; + case AST.ConditionalExpression: + intoId = intoId || this.nextId(); + self.recurse(ast.test, intoId); + self.if_(intoId, self.lazyRecurse(ast.alternate, intoId), self.lazyRecurse(ast.consequent, intoId)); + recursionFn(intoId); + break; + case AST.Identifier: + intoId = intoId || this.nextId(); + if (nameId) { + nameId.context = self.stage === 'inputs' ? 's' : this.assign(this.nextId(), this.getHasOwnProperty('l', ast.name) + '?l:s'); + nameId.computed = false; + nameId.name = ast.name; + } + self.if_(self.stage === 'inputs' || self.not(self.getHasOwnProperty('l', ast.name)), + function() { + self.if_(self.stage === 'inputs' || 's', function() { + if (create && create !== 1) { + self.if_( + self.isNull(self.nonComputedMember('s', ast.name)), + self.lazyAssign(self.nonComputedMember('s', ast.name), '{}')); + } + self.assign(intoId, self.nonComputedMember('s', ast.name)); + }); + }, intoId && self.lazyAssign(intoId, self.nonComputedMember('l', ast.name)) + ); + recursionFn(intoId); + break; + case AST.MemberExpression: + left = nameId && (nameId.context = this.nextId()) || this.nextId(); + intoId = intoId || this.nextId(); + self.recurse(ast.object, left, undefined, function() { + self.if_(self.notNull(left), function() { + if (ast.computed) { + right = self.nextId(); + self.recurse(ast.property, right); + self.getStringValue(right); + if (create && create !== 1) { + self.if_(self.not(self.computedMember(left, right)), self.lazyAssign(self.computedMember(left, right), '{}')); + } + expression = self.computedMember(left, right); + self.assign(intoId, expression); + if (nameId) { + nameId.computed = true; + nameId.name = right; + } + } else { + if (create && create !== 1) { + self.if_(self.isNull(self.nonComputedMember(left, ast.property.name)), self.lazyAssign(self.nonComputedMember(left, ast.property.name), '{}')); + } + expression = self.nonComputedMember(left, ast.property.name); + self.assign(intoId, expression); + if (nameId) { + nameId.computed = false; + nameId.name = ast.property.name; + } + } + }, function() { + self.assign(intoId, 'undefined'); + }); + recursionFn(intoId); + }, !!create); + break; + case AST.CallExpression: + intoId = intoId || this.nextId(); + if (ast.filter) { + right = self.filter(ast.callee.name); + args = []; + forEach(ast.arguments, function(expr) { + var argument = self.nextId(); + self.recurse(expr, argument); + args.push(argument); + }); + expression = right + '(' + args.join(',') + ')'; + self.assign(intoId, expression); + recursionFn(intoId); + } else { + right = self.nextId(); + left = {}; + args = []; + self.recurse(ast.callee, right, left, function() { + self.if_(self.notNull(right), function() { + forEach(ast.arguments, function(expr) { + self.recurse(expr, ast.constant ? undefined : self.nextId(), undefined, function(argument) { + args.push(argument); + }); + }); + if (left.name) { + expression = self.member(left.context, left.name, left.computed) + '(' + args.join(',') + ')'; + } else { + expression = right + '(' + args.join(',') + ')'; + } + self.assign(intoId, expression); + }, function() { + self.assign(intoId, 'undefined'); + }); + recursionFn(intoId); + }); + } + break; + case AST.AssignmentExpression: + right = this.nextId(); + left = {}; + this.recurse(ast.left, undefined, left, function() { + self.if_(self.notNull(left.context), function() { + self.recurse(ast.right, right); + expression = self.member(left.context, left.name, left.computed) + ast.operator + right; + self.assign(intoId, expression); + recursionFn(intoId || expression); + }); + }, 1); + break; + case AST.ArrayExpression: + args = []; + forEach(ast.elements, function(expr) { + self.recurse(expr, ast.constant ? undefined : self.nextId(), undefined, function(argument) { + args.push(argument); + }); + }); + expression = '[' + args.join(',') + ']'; + this.assign(intoId, expression); + recursionFn(intoId || expression); + break; + case AST.ObjectExpression: + args = []; + computed = false; + forEach(ast.properties, function(property) { + if (property.computed) { + computed = true; + } + }); + if (computed) { + intoId = intoId || this.nextId(); + this.assign(intoId, '{}'); + forEach(ast.properties, function(property) { + if (property.computed) { + left = self.nextId(); + self.recurse(property.key, left); + } else { + left = property.key.type === AST.Identifier ? + property.key.name : + ('' + property.key.value); + } + right = self.nextId(); + self.recurse(property.value, right); + self.assign(self.member(intoId, left, property.computed), right); + }); + } else { + forEach(ast.properties, function(property) { + self.recurse(property.value, ast.constant ? undefined : self.nextId(), undefined, function(expr) { + args.push(self.escape( + property.key.type === AST.Identifier ? property.key.name : + ('' + property.key.value)) + + ':' + expr); + }); + }); + expression = '{' + args.join(',') + '}'; + this.assign(intoId, expression); + } + recursionFn(intoId || expression); + break; + case AST.ThisExpression: + this.assign(intoId, 's'); + recursionFn(intoId || 's'); + break; + case AST.LocalsExpression: + this.assign(intoId, 'l'); + recursionFn(intoId || 'l'); + break; + case AST.NGValueParameter: + this.assign(intoId, 'v'); + recursionFn(intoId || 'v'); + break; + } + }, + + getHasOwnProperty: function(element, property) { + var key = element + '.' + property; + var own = this.current().own; + if (!own.hasOwnProperty(key)) { + own[key] = this.nextId(false, element + '&&(' + this.escape(property) + ' in ' + element + ')'); + } + return own[key]; + }, + + assign: function(id, value) { + if (!id) return; + this.current().body.push(id, '=', value, ';'); + return id; + }, + + filter: function(filterName) { + if (!this.state.filters.hasOwnProperty(filterName)) { + this.state.filters[filterName] = this.nextId(true); + } + return this.state.filters[filterName]; + }, + + ifDefined: function(id, defaultValue) { + return 'ifDefined(' + id + ',' + this.escape(defaultValue) + ')'; + }, + + plus: function(left, right) { + return 'plus(' + left + ',' + right + ')'; + }, + + return_: function(id) { + this.current().body.push('return ', id, ';'); + }, + + if_: function(test, alternate, consequent) { + if (test === true) { + alternate(); } else { - return $parseOptions.unwrapPromises; + var body = this.current().body; + body.push('if(', test, '){'); + alternate(); + body.push('}'); + if (consequent) { + body.push('else{'); + consequent(); + body.push('}'); + } } - }; + }, + not: function(expression) { + return '!(' + expression + ')'; + }, - /** - * @deprecated Promise unwrapping via $parse is deprecated and will be removed in the future. - * - * @ngdoc method - * @name $parseProvider#logPromiseWarnings - * @description - * - * Controls whether Angular should log a warning on any encounter of a promise in an expression. - * - * The default is set to `true`. - * - * This setting applies only if `$parseProvider.unwrapPromises` setting is set to true as well. - * - * @param {boolean=} value New value. - * @returns {boolean|self} Returns the current setting when used as getter and self if used as - * setter. - */ - this.logPromiseWarnings = function(value) { - if (isDefined(value)) { - $parseOptions.logPromiseWarnings = value; - return this; + isNull: function(expression) { + return expression + '==null'; + }, + + notNull: function(expression) { + return expression + '!=null'; + }, + + nonComputedMember: function(left, right) { + var SAFE_IDENTIFIER = /^[$_a-zA-Z][$_a-zA-Z0-9]*$/; + var UNSAFE_CHARACTERS = /[^$_a-zA-Z0-9]/g; + if (SAFE_IDENTIFIER.test(right)) { + return left + '.' + right; } else { - return $parseOptions.logPromiseWarnings; + return left + '["' + right.replace(UNSAFE_CHARACTERS, this.stringEscapeFn) + '"]'; } - }; + }, + + computedMember: function(left, right) { + return left + '[' + right + ']'; + }, + member: function(left, right, computed) { + if (computed) return this.computedMember(left, right); + return this.nonComputedMember(left, right); + }, - this.$get = ['$filter', '$sniffer', '$log', function($filter, $sniffer, $log) { - $parseOptions.csp = $sniffer.csp; + getStringValue: function(item) { + this.assign(item, 'getStringValue(' + item + ')'); + }, - promiseWarning = function promiseWarningFn(fullExp) { - if (!$parseOptions.logPromiseWarnings || promiseWarningCache.hasOwnProperty(fullExp)) return; - promiseWarningCache[fullExp] = true; - $log.warn('[$parse] Promise found in the expression `' + fullExp + '`. ' + - 'Automatic unwrapping of promises in Angular expressions is deprecated.'); + lazyRecurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) { + var self = this; + return function() { + self.recurse(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck); }; + }, - return function(exp) { - var parsedExpression; + lazyAssign: function(id, value) { + var self = this; + return function() { + self.assign(id, value); + }; + }, - switch (typeof exp) { - case 'string': + stringEscapeRegex: /[^ a-zA-Z0-9]/g, - if (cache.hasOwnProperty(exp)) { - return cache[exp]; - } + stringEscapeFn: function(c) { + return '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4); + }, - var lexer = new Lexer($parseOptions); - var parser = new Parser(lexer, $filter, $parseOptions); - parsedExpression = parser.parse(exp, false); + escape: function(value) { + if (isString(value)) return '\'' + value.replace(this.stringEscapeRegex, this.stringEscapeFn) + '\''; + if (isNumber(value)) return value.toString(); + if (value === true) return 'true'; + if (value === false) return 'false'; + if (value === null) return 'null'; + if (typeof value === 'undefined') return 'undefined'; - if (exp !== 'hasOwnProperty') { - // Only cache the value if it's not going to mess up the cache object - // This is more performant that using Object.prototype.hasOwnProperty.call - cache[exp] = parsedExpression; - } + throw $parseMinErr('esc', 'IMPOSSIBLE'); + }, - return parsedExpression; + nextId: function(skip, init) { + var id = 'v' + (this.state.nextId++); + if (!skip) { + this.current().vars.push(id + (init ? '=' + init : '')); + } + return id; + }, - case 'function': - return exp; + current: function() { + return this.state[this.state.computing]; + } + }; - default: - return noop; - } - }; - }]; + + function ASTInterpreter($filter) { + this.$filter = $filter; } - /** - * @ngdoc service - * @name $q - * @requires $rootScope - * - * @description - * A promise/deferred implementation inspired by [Kris Kowal's Q](https://github.com/kriskowal/q). - * - * [The CommonJS Promise proposal](http://wiki.commonjs.org/wiki/Promises) describes a promise as an - * interface for interacting with an object that represents the result of an action that is - * performed asynchronously, and may or may not be finished at any given point in time. - * - * From the perspective of dealing with error handling, deferred and promise APIs are to - * asynchronous programming what `try`, `catch` and `throw` keywords are to synchronous programming. - * - * ```js - * // for the purpose of this example let's assume that variables `$q`, `scope` and `okToGreet` - * // are available in the current lexical scope (they could have been injected or passed in). - * - * function asyncGreet(name) { - * var deferred = $q.defer(); - * - * setTimeout(function() { - * // since this fn executes async in a future turn of the event loop, we need to wrap - * // our code into an $apply call so that the model changes are properly observed. - * scope.$apply(function() { - * deferred.notify('About to greet ' + name + '.'); - * - * if (okToGreet(name)) { - * deferred.resolve('Hello, ' + name + '!'); - * } else { - * deferred.reject('Greeting ' + name + ' is not allowed.'); - * } - * }); - * }, 1000); - * - * return deferred.promise; - * } - * - * var promise = asyncGreet('Robin Hood'); - * promise.then(function(greeting) { - * alert('Success: ' + greeting); - * }, function(reason) { - * alert('Failed: ' + reason); - * }, function(update) { - * alert('Got notification: ' + update); - * }); - * ``` - * - * At first it might not be obvious why this extra complexity is worth the trouble. The payoff - * comes in the way of guarantees that promise and deferred APIs make, see - * https://github.com/kriskowal/uncommonjs/blob/master/promises/specification.md. - * - * Additionally the promise api allows for composition that is very hard to do with the - * traditional callback ([CPS](http://en.wikipedia.org/wiki/Continuation-passing_style)) approach. - * For more on this please see the [Q documentation](https://github.com/kriskowal/q) especially the - * section on serial or parallel joining of promises. - * - * - * # The Deferred API - * - * A new instance of deferred is constructed by calling `$q.defer()`. - * - * The purpose of the deferred object is to expose the associated Promise instance as well as APIs - * that can be used for signaling the successful or unsuccessful completion, as well as the status - * of the task. - * - * **Methods** - * - * - `resolve(value)` – resolves the derived promise with the `value`. If the value is a rejection - * constructed via `$q.reject`, the promise will be rejected instead. - * - `reject(reason)` – rejects the derived promise with the `reason`. This is equivalent to - * resolving it with a rejection constructed via `$q.reject`. - * - `notify(value)` - provides updates on the status of the promise's execution. This may be called - * multiple times before the promise is either resolved or rejected. - * - * **Properties** - * - * - promise – `{Promise}` – promise object associated with this deferred. - * - * - * # The Promise API - * - * A new promise instance is created when a deferred instance is created and can be retrieved by - * calling `deferred.promise`. - * - * The purpose of the promise object is to allow for interested parties to get access to the result - * of the deferred task when it completes. - * - * **Methods** - * - * - `then(successCallback, errorCallback, notifyCallback)` – regardless of when the promise was or - * will be resolved or rejected, `then` calls one of the success or error callbacks asynchronously - * as soon as the result is available. The callbacks are called with a single argument: the result - * or rejection reason. Additionally, the notify callback may be called zero or more times to - * provide a progress indication, before the promise is resolved or rejected. - * - * This method *returns a new promise* which is resolved or rejected via the return value of the - * `successCallback`, `errorCallback`. It also notifies via the return value of the - * `notifyCallback` method. The promise can not be resolved or rejected from the notifyCallback - * method. - * - * - `catch(errorCallback)` – shorthand for `promise.then(null, errorCallback)` - * - * - `finally(callback)` – allows you to observe either the fulfillment or rejection of a promise, - * but to do so without modifying the final value. This is useful to release resources or do some - * clean-up that needs to be done whether the promise was rejected or resolved. See the [full - * specification](https://github.com/kriskowal/q/wiki/API-Reference#promisefinallycallback) for - * more information. - * - * Because `finally` is a reserved word in JavaScript and reserved keywords are not supported as - * property names by ES3, you'll need to invoke the method like `promise['finally'](callback)` to - * make your code IE8 and Android 2.x compatible. + ASTInterpreter.prototype = { + compile: function(ast) { + var self = this; + findConstantAndWatchExpressions(ast, self.$filter); + var assignable; + var assign; + if ((assignable = assignableAST(ast))) { + assign = this.recurse(assignable); + } + var toWatch = getInputs(ast.body); + var inputs; + if (toWatch) { + inputs = []; + forEach(toWatch, function(watch, key) { + var input = self.recurse(watch); + input.isPure = watch.isPure; + watch.input = input; + inputs.push(input); + watch.watchId = key; + }); + } + var expressions = []; + forEach(ast.body, function(expression) { + expressions.push(self.recurse(expression.expression)); + }); + var fn = ast.body.length === 0 ? noop : + ast.body.length === 1 ? expressions[0] : + function(scope, locals) { + var lastValue; + forEach(expressions, function(exp) { + lastValue = exp(scope, locals); + }); + return lastValue; + }; + if (assign) { + fn.assign = function(scope, value, locals) { + return assign(scope, locals, value); + }; + } + if (inputs) { + fn.inputs = inputs; + } + return fn; + }, + + recurse: function(ast, context, create) { + var left, right, self = this, args; + if (ast.input) { + return this.inputs(ast.input, ast.watchId); + } + switch (ast.type) { + case AST.Literal: + return this.value(ast.value, context); + case AST.UnaryExpression: + right = this.recurse(ast.argument); + return this['unary' + ast.operator](right, context); + case AST.BinaryExpression: + left = this.recurse(ast.left); + right = this.recurse(ast.right); + return this['binary' + ast.operator](left, right, context); + case AST.LogicalExpression: + left = this.recurse(ast.left); + right = this.recurse(ast.right); + return this['binary' + ast.operator](left, right, context); + case AST.ConditionalExpression: + return this['ternary?:']( + this.recurse(ast.test), + this.recurse(ast.alternate), + this.recurse(ast.consequent), + context + ); + case AST.Identifier: + return self.identifier(ast.name, context, create); + case AST.MemberExpression: + left = this.recurse(ast.object, false, !!create); + if (!ast.computed) { + right = ast.property.name; + } + if (ast.computed) right = this.recurse(ast.property); + return ast.computed ? + this.computedMember(left, right, context, create) : + this.nonComputedMember(left, right, context, create); + case AST.CallExpression: + args = []; + forEach(ast.arguments, function(expr) { + args.push(self.recurse(expr)); + }); + if (ast.filter) right = this.$filter(ast.callee.name); + if (!ast.filter) right = this.recurse(ast.callee, true); + return ast.filter ? + function(scope, locals, assign, inputs) { + var values = []; + for (var i = 0; i < args.length; ++i) { + values.push(args[i](scope, locals, assign, inputs)); + } + var value = right.apply(undefined, values, inputs); + return context ? {context: undefined, name: undefined, value: value} : value; + } : + function(scope, locals, assign, inputs) { + var rhs = right(scope, locals, assign, inputs); + var value; + if (rhs.value != null) { + var values = []; + for (var i = 0; i < args.length; ++i) { + values.push(args[i](scope, locals, assign, inputs)); + } + value = rhs.value.apply(rhs.context, values); + } + return context ? {value: value} : value; + }; + case AST.AssignmentExpression: + left = this.recurse(ast.left, true, 1); + right = this.recurse(ast.right); + return function(scope, locals, assign, inputs) { + var lhs = left(scope, locals, assign, inputs); + var rhs = right(scope, locals, assign, inputs); + lhs.context[lhs.name] = rhs; + return context ? {value: rhs} : rhs; + }; + case AST.ArrayExpression: + args = []; + forEach(ast.elements, function(expr) { + args.push(self.recurse(expr)); + }); + return function(scope, locals, assign, inputs) { + var value = []; + for (var i = 0; i < args.length; ++i) { + value.push(args[i](scope, locals, assign, inputs)); + } + return context ? {value: value} : value; + }; + case AST.ObjectExpression: + args = []; + forEach(ast.properties, function(property) { + if (property.computed) { + args.push({key: self.recurse(property.key), + computed: true, + value: self.recurse(property.value) + }); + } else { + args.push({key: property.key.type === AST.Identifier ? + property.key.name : + ('' + property.key.value), + computed: false, + value: self.recurse(property.value) + }); + } + }); + return function(scope, locals, assign, inputs) { + var value = {}; + for (var i = 0; i < args.length; ++i) { + if (args[i].computed) { + value[args[i].key(scope, locals, assign, inputs)] = args[i].value(scope, locals, assign, inputs); + } else { + value[args[i].key] = args[i].value(scope, locals, assign, inputs); + } + } + return context ? {value: value} : value; + }; + case AST.ThisExpression: + return function(scope) { + return context ? {value: scope} : scope; + }; + case AST.LocalsExpression: + return function(scope, locals) { + return context ? {value: locals} : locals; + }; + case AST.NGValueParameter: + return function(scope, locals, assign) { + return context ? {value: assign} : assign; + }; + } + }, + + 'unary+': function(argument, context) { + return function(scope, locals, assign, inputs) { + var arg = argument(scope, locals, assign, inputs); + if (isDefined(arg)) { + arg = +arg; + } else { + arg = 0; + } + return context ? {value: arg} : arg; + }; + }, + 'unary-': function(argument, context) { + return function(scope, locals, assign, inputs) { + var arg = argument(scope, locals, assign, inputs); + if (isDefined(arg)) { + arg = -arg; + } else { + arg = -0; + } + return context ? {value: arg} : arg; + }; + }, + 'unary!': function(argument, context) { + return function(scope, locals, assign, inputs) { + var arg = !argument(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary+': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var lhs = left(scope, locals, assign, inputs); + var rhs = right(scope, locals, assign, inputs); + var arg = plusFn(lhs, rhs); + return context ? {value: arg} : arg; + }; + }, + 'binary-': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var lhs = left(scope, locals, assign, inputs); + var rhs = right(scope, locals, assign, inputs); + var arg = (isDefined(lhs) ? lhs : 0) - (isDefined(rhs) ? rhs : 0); + return context ? {value: arg} : arg; + }; + }, + 'binary*': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) * right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary/': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) / right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary%': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) % right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary===': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) === right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary!==': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) !== right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary==': function(left, right, context) { + return function(scope, locals, assign, inputs) { + // eslint-disable-next-line eqeqeq + var arg = left(scope, locals, assign, inputs) == right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary!=': function(left, right, context) { + return function(scope, locals, assign, inputs) { + // eslint-disable-next-line eqeqeq + var arg = left(scope, locals, assign, inputs) != right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary<': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) < right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary>': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) > right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary<=': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) <= right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary>=': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) >= right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary&&': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) && right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary||': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) || right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'ternary?:': function(test, alternate, consequent, context) { + return function(scope, locals, assign, inputs) { + var arg = test(scope, locals, assign, inputs) ? alternate(scope, locals, assign, inputs) : consequent(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + value: function(value, context) { + return function() { return context ? {context: undefined, name: undefined, value: value} : value; }; + }, + identifier: function(name, context, create) { + return function(scope, locals, assign, inputs) { + var base = locals && (name in locals) ? locals : scope; + if (create && create !== 1 && base && base[name] == null) { + base[name] = {}; + } + var value = base ? base[name] : undefined; + if (context) { + return {context: base, name: name, value: value}; + } else { + return value; + } + }; + }, + computedMember: function(left, right, context, create) { + return function(scope, locals, assign, inputs) { + var lhs = left(scope, locals, assign, inputs); + var rhs; + var value; + if (lhs != null) { + rhs = right(scope, locals, assign, inputs); + rhs = getStringValue(rhs); + if (create && create !== 1) { + if (lhs && !(lhs[rhs])) { + lhs[rhs] = {}; + } + } + value = lhs[rhs]; + } + if (context) { + return {context: lhs, name: rhs, value: value}; + } else { + return value; + } + }; + }, + nonComputedMember: function(left, right, context, create) { + return function(scope, locals, assign, inputs) { + var lhs = left(scope, locals, assign, inputs); + if (create && create !== 1) { + if (lhs && lhs[right] == null) { + lhs[right] = {}; + } + } + var value = lhs != null ? lhs[right] : undefined; + if (context) { + return {context: lhs, name: right, value: value}; + } else { + return value; + } + }; + }, + inputs: function(input, watchId) { + return function(scope, value, locals, inputs) { + if (inputs) return inputs[watchId]; + return input(scope, value, locals); + }; + } + }; + + /** + * @constructor + */ + function Parser(lexer, $filter, options) { + this.ast = new AST(lexer, options); + this.astCompiler = options.csp ? new ASTInterpreter($filter) : + new ASTCompiler($filter); + } + + Parser.prototype = { + constructor: Parser, + + parse: function(text) { + var ast = this.getAst(text); + var fn = this.astCompiler.compile(ast.ast); + fn.literal = isLiteral(ast.ast); + fn.constant = isConstant(ast.ast); + fn.oneTime = ast.oneTime; + return fn; + }, + + getAst: function(exp) { + var oneTime = false; + exp = exp.trim(); + + if (exp.charAt(0) === ':' && exp.charAt(1) === ':') { + oneTime = true; + exp = exp.substring(2); + } + return { + ast: this.ast.ast(exp), + oneTime: oneTime + }; + } + }; + + function getValueOf(value) { + return isFunction(value.valueOf) ? value.valueOf() : objectValueOf.call(value); + } + +/////////////////////////////////// + + /** + * @ngdoc service + * @name $parse + * @kind function * - * # Chaining promises + * @description * - * Because calling the `then` method of a promise returns a new derived promise, it is easily - * possible to create a chain of promises: + * Converts AngularJS {@link guide/expression expression} into a function. * * ```js - * promiseB = promiseA.then(function(result) { - * return result + 1; - * }); + * var getter = $parse('user.name'); + * var setter = getter.assign; + * var context = {user:{name:'AngularJS'}}; + * var locals = {user:{name:'local'}}; * - * // promiseB will be resolved immediately after promiseA is resolved and its value - * // will be the result of promiseA incremented by 1 + * expect(getter(context)).toEqual('AngularJS'); + * setter(context, 'newValue'); + * expect(context.user.name).toEqual('newValue'); + * expect(getter(context, locals)).toEqual('local'); * ``` * - * It is possible to create chains of any length and since a promise can be resolved with another - * promise (which will defer its resolution further), it is possible to pause/defer resolution of - * the promises at any point in the chain. This makes it possible to implement powerful APIs like - * $http's response interceptors. * + * @param {string} expression String expression to compile. + * @returns {function(context, locals)} a function which represents the compiled expression: * - * # Differences between Kris Kowal's Q and $q - * - * There are two main differences: - * - * - $q is integrated with the {@link ng.$rootScope.Scope} Scope model observation - * mechanism in angular, which means faster propagation of resolution or rejection into your - * models and avoiding unnecessary browser repaints, which would result in flickering UI. - * - Q has many more features than $q, but that comes at a cost of bytes. $q is tiny, but contains - * all the important functionality needed for common async tasks. + * * `context` – `{object}` – an object against which any expressions embedded in the strings + * are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values in + * `context`. * - * # Testing + * The returned function also has the following properties: + * * `literal` – `{boolean}` – whether the expression's top-level node is a JavaScript + * literal. + * * `constant` – `{boolean}` – whether the expression is made entirely of JavaScript + * constant literals. + * * `assign` – `{?function(context, value)}` – if the expression is assignable, this will be + * set to a function to change its value on the given context. * - * ```js - * it('should simulate promise', inject(function($q, $rootScope) { - * var deferred = $q.defer(); - * var promise = deferred.promise; - * var resolvedValue; - * - * promise.then(function(value) { resolvedValue = value; }); - * expect(resolvedValue).toBeUndefined(); - * - * // Simulate resolving of promise - * deferred.resolve(123); - * // Note that the 'then' function does not get called synchronously. - * // This is because we want the promise API to always be async, whether or not - * // it got called synchronously or asynchronously. - * expect(resolvedValue).toBeUndefined(); - * - * // Propagate promise resolution to 'then' functions using $apply(). - * $rootScope.$apply(); - * expect(resolvedValue).toEqual(123); - * })); - * ``` */ - function $QProvider() { - - this.$get = ['$rootScope', '$exceptionHandler', function($rootScope, $exceptionHandler) { - return qFactory(function(callback) { - $rootScope.$evalAsync(callback); - }, $exceptionHandler); - }]; - } /** - * Constructs a promise manager. + * @ngdoc provider + * @name $parseProvider + * @this * - * @param {function(Function)} nextTick Function for executing functions in the next turn. - * @param {function(...*)} exceptionHandler Function into which unexpected exceptions are passed for - * debugging purposes. - * @returns {object} Promise manager. + * @description + * `$parseProvider` can be used for configuring the default behavior of the {@link ng.$parse $parse} + * service. */ - function qFactory(nextTick, exceptionHandler) { + function $ParseProvider() { + var cache = createMap(); + var literals = { + 'true': true, + 'false': false, + 'null': null, + 'undefined': undefined + }; + var identStart, identContinue; + + /** + * @ngdoc method + * @name $parseProvider#addLiteral + * @description + * + * Configure $parse service to add literal values that will be present as literal at expressions. + * + * @param {string} literalName Token for the literal value. The literal name value must be a valid literal name. + * @param {*} literalValue Value for this literal. All literal values must be primitives or `undefined`. + * + **/ + this.addLiteral = function(literalName, literalValue) { + literals[literalName] = literalValue; + }; /** * @ngdoc method - * @name $q#defer - * @function + * @name $parseProvider#setIdentifierFns * * @description - * Creates a `Deferred` object which represents a task which will finish in the future. * - * @returns {Deferred} Returns a new instance of deferred. + * Allows defining the set of characters that are allowed in AngularJS expressions. The function + * `identifierStart` will get called to know if a given character is a valid character to be the + * first character for an identifier. The function `identifierContinue` will get called to know if + * a given character is a valid character to be a follow-up identifier character. The functions + * `identifierStart` and `identifierContinue` will receive as arguments the single character to be + * identifier and the character code point. These arguments will be `string` and `numeric`. Keep in + * mind that the `string` parameter can be two characters long depending on the character + * representation. It is expected for the function to return `true` or `false`, whether that + * character is allowed or not. + * + * Since this function will be called extensively, keep the implementation of these functions fast, + * as the performance of these functions have a direct impact on the expressions parsing speed. + * + * @param {function=} identifierStart The function that will decide whether the given character is + * a valid identifier start character. + * @param {function=} identifierContinue The function that will decide whether the given character is + * a valid identifier continue character. */ - var defer = function() { - var pending = [], - value, deferred; - - deferred = { - - resolve: function(val) { - if (pending) { - var callbacks = pending; - pending = undefined; - value = ref(val); - - if (callbacks.length) { - nextTick(function() { - var callback; - for (var i = 0, ii = callbacks.length; i < ii; i++) { - callback = callbacks[i]; - value.then(callback[0], callback[1], callback[2]); - } - }); + this.setIdentifierFns = function(identifierStart, identifierContinue) { + identStart = identifierStart; + identContinue = identifierContinue; + return this; + }; + + this.$get = ['$filter', function($filter) { + var noUnsafeEval = csp().noUnsafeEval; + var $parseOptions = { + csp: noUnsafeEval, + literals: copy(literals), + isIdentifierStart: isFunction(identStart) && identStart, + isIdentifierContinue: isFunction(identContinue) && identContinue + }; + $parse.$$getAst = $$getAst; + return $parse; + + function $parse(exp, interceptorFn) { + var parsedExpression, cacheKey; + + switch (typeof exp) { + case 'string': + exp = exp.trim(); + cacheKey = exp; + + parsedExpression = cache[cacheKey]; + + if (!parsedExpression) { + var lexer = new Lexer($parseOptions); + var parser = new Parser(lexer, $filter, $parseOptions); + parsedExpression = parser.parse(exp); + if (parsedExpression.constant) { + parsedExpression.$$watchDelegate = constantWatchDelegate; + } else if (parsedExpression.oneTime) { + parsedExpression.$$watchDelegate = parsedExpression.literal ? + oneTimeLiteralWatchDelegate : oneTimeWatchDelegate; + } else if (parsedExpression.inputs) { + parsedExpression.$$watchDelegate = inputsWatchDelegate; + } + cache[cacheKey] = parsedExpression; } - } - }, + return addInterceptor(parsedExpression, interceptorFn); + case 'function': + return addInterceptor(exp, interceptorFn); - reject: function(reason) { - deferred.resolve(createInternalRejectedPromise(reason)); - }, + default: + return addInterceptor(noop, interceptorFn); + } + } + function $$getAst(exp) { + var lexer = new Lexer($parseOptions); + var parser = new Parser(lexer, $filter, $parseOptions); + return parser.getAst(exp).ast; + } - notify: function(progress) { - if (pending) { - var callbacks = pending; + function expressionInputDirtyCheck(newValue, oldValueOfValue, compareObjectIdentity) { - if (pending.length) { - nextTick(function() { - var callback; - for (var i = 0, ii = callbacks.length; i < ii; i++) { - callback = callbacks[i]; - callback[2](progress); - } - }); - } - } - }, + if (newValue == null || oldValueOfValue == null) { // null/undefined + return newValue === oldValueOfValue; + } + if (typeof newValue === 'object') { - promise: { - then: function(callback, errback, progressback) { - var result = defer(); + // attempt to convert the value to a primitive type + // TODO(docs): add a note to docs that by implementing valueOf even objects and arrays can + // be cheaply dirty-checked + newValue = getValueOf(newValue); - var wrappedCallback = function(value) { - try { - result.resolve((isFunction(callback) ? callback : defaultCallback)(value)); - } catch(e) { - result.reject(e); - exceptionHandler(e); - } - }; + if (typeof newValue === 'object' && !compareObjectIdentity) { + // objects/arrays are not supported - deep-watching them would be too expensive + return false; + } - var wrappedErrback = function(reason) { - try { - result.resolve((isFunction(errback) ? errback : defaultErrback)(reason)); - } catch(e) { - result.reject(e); - exceptionHandler(e); - } - }; + // fall-through to the primitive equality check + } - var wrappedProgressback = function(progress) { - try { - result.notify((isFunction(progressback) ? progressback : defaultCallback)(progress)); - } catch(e) { - exceptionHandler(e); - } - }; + //Primitive or NaN + // eslint-disable-next-line no-self-compare + return newValue === oldValueOfValue || (newValue !== newValue && oldValueOfValue !== oldValueOfValue); + } - if (pending) { - pending.push([wrappedCallback, wrappedErrback, wrappedProgressback]); - } else { - value.then(wrappedCallback, wrappedErrback, wrappedProgressback); + function inputsWatchDelegate(scope, listener, objectEquality, parsedExpression, prettyPrintExpression) { + var inputExpressions = parsedExpression.inputs; + var lastResult; + + if (inputExpressions.length === 1) { + var oldInputValueOf = expressionInputDirtyCheck; // init to something unique so that equals check fails + inputExpressions = inputExpressions[0]; + return scope.$watch(function expressionInputWatch(scope) { + var newInputValue = inputExpressions(scope); + if (!expressionInputDirtyCheck(newInputValue, oldInputValueOf, inputExpressions.isPure)) { + lastResult = parsedExpression(scope, undefined, undefined, [newInputValue]); + oldInputValueOf = newInputValue && getValueOf(newInputValue); } + return lastResult; + }, listener, objectEquality, prettyPrintExpression); + } - return result.promise; - }, - - "catch": function(callback) { - return this.then(null, callback); - }, + var oldInputValueOfValues = []; + var oldInputValues = []; + for (var i = 0, ii = inputExpressions.length; i < ii; i++) { + oldInputValueOfValues[i] = expressionInputDirtyCheck; // init to something unique so that equals check fails + oldInputValues[i] = null; + } - "finally": function(callback) { + return scope.$watch(function expressionInputsWatch(scope) { + var changed = false; - function makePromise(value, resolved) { - var result = defer(); - if (resolved) { - result.resolve(value); - } else { - result.reject(value); - } - return result.promise; - } - - function handleCallback(value, isResolved) { - var callbackOutput = null; - try { - callbackOutput = (callback ||defaultCallback)(); - } catch(e) { - return makePromise(e, false); - } - if (callbackOutput && isFunction(callbackOutput.then)) { - return callbackOutput.then(function() { - return makePromise(value, isResolved); - }, function(error) { - return makePromise(error, false); - }); - } else { - return makePromise(value, isResolved); - } + for (var i = 0, ii = inputExpressions.length; i < ii; i++) { + var newInputValue = inputExpressions[i](scope); + if (changed || (changed = !expressionInputDirtyCheck(newInputValue, oldInputValueOfValues[i], inputExpressions[i].isPure))) { + oldInputValues[i] = newInputValue; + oldInputValueOfValues[i] = newInputValue && getValueOf(newInputValue); } + } - return this.then(function(value) { - return handleCallback(value, true); - }, function(error) { - return handleCallback(error, false); - }); + if (changed) { + lastResult = parsedExpression(scope, undefined, undefined, oldInputValues); } - } - }; - return deferred; - }; + return lastResult; + }, listener, objectEquality, prettyPrintExpression); + } + function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression, prettyPrintExpression) { + var unwatch, lastValue; + if (parsedExpression.inputs) { + unwatch = inputsWatchDelegate(scope, oneTimeListener, objectEquality, parsedExpression, prettyPrintExpression); + } else { + unwatch = scope.$watch(oneTimeWatch, oneTimeListener, objectEquality); + } + return unwatch; - var ref = function(value) { - if (value && isFunction(value.then)) return value; - return { - then: function(callback) { - var result = defer(); - nextTick(function() { - result.resolve(callback(value)); - }); - return result.promise; + function oneTimeWatch(scope) { + return parsedExpression(scope); } - }; - }; + function oneTimeListener(value, old, scope) { + lastValue = value; + if (isFunction(listener)) { + listener(value, old, scope); + } + if (isDefined(value)) { + scope.$$postDigest(function() { + if (isDefined(lastValue)) { + unwatch(); + } + }); + } + } + } + function oneTimeLiteralWatchDelegate(scope, listener, objectEquality, parsedExpression) { + var unwatch, lastValue; + unwatch = scope.$watch(function oneTimeWatch(scope) { + return parsedExpression(scope); + }, function oneTimeListener(value, old, scope) { + lastValue = value; + if (isFunction(listener)) { + listener(value, old, scope); + } + if (isAllDefined(value)) { + scope.$$postDigest(function() { + if (isAllDefined(lastValue)) unwatch(); + }); + } + }, objectEquality); - /** - * @ngdoc method - * @name $q#reject - * @function - * - * @description - * Creates a promise that is resolved as rejected with the specified `reason`. This api should be - * used to forward rejection in a chain of promises. If you are dealing with the last promise in - * a promise chain, you don't need to worry about it. - * - * When comparing deferreds/promises to the familiar behavior of try/catch/throw, think of - * `reject` as the `throw` keyword in JavaScript. This also means that if you "catch" an error via - * a promise error callback and you want to forward the error to the promise derived from the - * current promise, you have to "rethrow" the error by returning a rejection constructed via - * `reject`. - * - * ```js - * promiseB = promiseA.then(function(result) { - * // success: do something and resolve promiseB - * // with the old or a new result - * return result; - * }, function(reason) { - * // error: handle the error if possible and - * // resolve promiseB with newPromiseOrValue, - * // otherwise forward the rejection to promiseB - * if (canHandle(reason)) { - * // handle the error and recover - * return newPromiseOrValue; - * } - * return $q.reject(reason); - * }); - * ``` - * - * @param {*} reason Constant, message, exception or an object representing the rejection reason. - * @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`. - */ - var reject = function(reason) { - var result = defer(); - result.reject(reason); - return result.promise; - }; + return unwatch; - var createInternalRejectedPromise = function(reason) { - return { - then: function(callback, errback) { - var result = defer(); - nextTick(function() { - try { - result.resolve((isFunction(errback) ? errback : defaultErrback)(reason)); - } catch(e) { - result.reject(e); - exceptionHandler(e); - } + function isAllDefined(value) { + var allDefined = true; + forEach(value, function(val) { + if (!isDefined(val)) allDefined = false; }); - return result.promise; + return allDefined; } - }; - }; - + } - /** - * @ngdoc method - * @name $q#when - * @function - * - * @description - * Wraps an object that might be a value or a (3rd party) then-able promise into a $q promise. - * This is useful when you are dealing with an object that might or might not be a promise, or if - * the promise comes from a source that can't be trusted. - * - * @param {*} value Value or a promise - * @returns {Promise} Returns a promise of the passed value or promise - */ - var when = function(value, callback, errback, progressback) { - var result = defer(), - done; + function constantWatchDelegate(scope, listener, objectEquality, parsedExpression) { + var unwatch = scope.$watch(function constantWatch(scope) { + unwatch(); + return parsedExpression(scope); + }, listener, objectEquality); + return unwatch; + } - var wrappedCallback = function(value) { - try { - return (isFunction(callback) ? callback : defaultCallback)(value); - } catch (e) { - exceptionHandler(e); - return reject(e); - } - }; + function addInterceptor(parsedExpression, interceptorFn) { + if (!interceptorFn) return parsedExpression; + var watchDelegate = parsedExpression.$$watchDelegate; + var useInputs = false; + + var regularWatch = + watchDelegate !== oneTimeLiteralWatchDelegate && + watchDelegate !== oneTimeWatchDelegate; + + var fn = regularWatch ? function regularInterceptedExpression(scope, locals, assign, inputs) { + var value = useInputs && inputs ? inputs[0] : parsedExpression(scope, locals, assign, inputs); + return interceptorFn(value, scope, locals); + } : function oneTimeInterceptedExpression(scope, locals, assign, inputs) { + var value = parsedExpression(scope, locals, assign, inputs); + var result = interceptorFn(value, scope, locals); + // we only return the interceptor's result if the + // initial value is defined (for bind-once) + return isDefined(value) ? result : value; + }; - var wrappedErrback = function(reason) { - try { - return (isFunction(errback) ? errback : defaultErrback)(reason); - } catch (e) { - exceptionHandler(e); - return reject(e); + // Propagate $$watchDelegates other then inputsWatchDelegate + useInputs = !parsedExpression.inputs; + if (watchDelegate && watchDelegate !== inputsWatchDelegate) { + fn.$$watchDelegate = watchDelegate; + fn.inputs = parsedExpression.inputs; + } else if (!interceptorFn.$stateful) { + // Treat interceptor like filters - assume non-stateful by default and use the inputsWatchDelegate + fn.$$watchDelegate = inputsWatchDelegate; + fn.inputs = parsedExpression.inputs ? parsedExpression.inputs : [parsedExpression]; } - }; - var wrappedProgressback = function(progress) { - try { - return (isFunction(progressback) ? progressback : defaultCallback)(progress); - } catch (e) { - exceptionHandler(e); + if (fn.inputs) { + fn.inputs = fn.inputs.map(function(e) { + // Remove the isPure flag of inputs when it is not absolute because they are now wrapped in a + // potentially non-pure interceptor function. + if (e.isPure === PURITY_RELATIVE) { + return function depurifier(s) { return e(s); }; + } + return e; + }); } - }; - - nextTick(function() { - ref(value).then(function(value) { - if (done) return; - done = true; - result.resolve(ref(value).then(wrappedCallback, wrappedErrback, wrappedProgressback)); - }, function(reason) { - if (done) return; - done = true; - result.resolve(wrappedErrback(reason)); - }, function(progress) { - if (done) return; - result.notify(wrappedProgressback(progress)); - }); - }); - - return result.promise; - }; - - - function defaultCallback(value) { - return value; - } - - - function defaultErrback(reason) { - return reject(reason); - } - - - /** - * @ngdoc method - * @name $q#all - * @function - * - * @description - * Combines multiple promises into a single promise that is resolved when all of the input - * promises are resolved. - * - * @param {Array.|Object.} promises An array or hash of promises. - * @returns {Promise} Returns a single promise that will be resolved with an array/hash of values, - * each value corresponding to the promise at the same index/key in the `promises` array/hash. - * If any of the promises is resolved with a rejection, this resulting promise will be rejected - * with the same rejection value. - */ - function all(promises) { - var deferred = defer(), - counter = 0, - results = isArray(promises) ? [] : {}; - - forEach(promises, function(promise, key) { - counter++; - ref(promise).then(function(value) { - if (results.hasOwnProperty(key)) return; - results[key] = value; - if (!(--counter)) deferred.resolve(results); - }, function(reason) { - if (results.hasOwnProperty(key)) return; - deferred.reject(reason); - }); - }); - - if (counter === 0) { - deferred.resolve(results); - } - - return deferred.promise; - } - - return { - defer: defer, - reject: reject, - when: when, - all: all - }; - } - - function $$RAFProvider(){ //rAF - this.$get = ['$window', '$timeout', function($window, $timeout) { - var requestAnimationFrame = $window.requestAnimationFrame || - $window.webkitRequestAnimationFrame || - $window.mozRequestAnimationFrame; - - var cancelAnimationFrame = $window.cancelAnimationFrame || - $window.webkitCancelAnimationFrame || - $window.mozCancelAnimationFrame || - $window.webkitCancelRequestAnimationFrame; - var rafSupported = !!requestAnimationFrame; - var raf = rafSupported - ? function(fn) { - var id = requestAnimationFrame(fn); - return function() { - cancelAnimationFrame(id); - }; + return fn; } - : function(fn) { - var timer = $timeout(fn, 16.66, false); // 1000 / 60 = 16.666 - return function() { - $timeout.cancel(timer); - }; - }; - - raf.supported = rafSupported; - - return raf; }]; } /** - * DESIGN NOTES + * @ngdoc service + * @name $q + * @requires $rootScope * - * The design decisions behind the scope are heavily favored for speed and memory consumption. + * @description + * A service that helps you run functions asynchronously, and use their return values (or exceptions) + * when they are done processing. * - * The typical use of scope is to watch the expressions, which most of the time return the same - * value as last time so we optimize the operation. + * This is a [Promises/A+](https://promisesaplus.com/)-compliant implementation of promises/deferred + * objects inspired by [Kris Kowal's Q](https://github.com/kriskowal/q). * - * Closures construction is expensive in terms of speed as well as memory: - * - No closures, instead use prototypical inheritance for API - * - Internal state needs to be stored on scope directly, which means that private state is - * exposed as $$____ properties + * $q can be used in two fashions --- one which is more similar to Kris Kowal's Q or jQuery's Deferred + * implementations, and the other which resembles ES6 (ES2015) promises to some degree. * - * Loop operations are optimized by using while(count--) { ... } - * - this means that in order to keep the same order of execution as addition we have to add - * items to the array at the beginning (shift) instead of at the end (push) + * ## $q constructor * - * Child scopes are created and removed often - * - Using an array would be slow since inserts in middle are expensive so we use linked list + * The streamlined ES6 style promise is essentially just using $q as a constructor which takes a `resolver` + * function as the first argument. This is similar to the native Promise implementation from ES6, + * see [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). * - * There are few watches then a lot of observers. This is why you don't want the observer to be - * implemented in the same way as watch. Watch requires return of initialization function which - * are expensive to construct. - */ - - - /** - * @ngdoc provider - * @name $rootScopeProvider - * @description + * While the constructor-style use is supported, not all of the supporting methods from ES6 promises are + * available yet. * - * Provider for the $rootScope service. - */ - - /** - * @ngdoc method - * @name $rootScopeProvider#digestTtl - * @description + * It can be used like so: * - * Sets the number of `$digest` iterations the scope should attempt to execute before giving up and - * assuming that the model is unstable. + * ```js + * // for the purpose of this example let's assume that variables `$q` and `okToGreet` + * // are available in the current lexical scope (they could have been injected or passed in). * - * The current default is 10 iterations. + * function asyncGreet(name) { + * // perform some asynchronous operation, resolve or reject the promise when appropriate. + * return $q(function(resolve, reject) { + * setTimeout(function() { + * if (okToGreet(name)) { + * resolve('Hello, ' + name + '!'); + * } else { + * reject('Greeting ' + name + ' is not allowed.'); + * } + * }, 1000); + * }); + * } * - * In complex applications it's possible that the dependencies between `$watch`s will result in - * several digest iterations. However if an application needs more than the default 10 digest - * iterations for its model to stabilize then you should investigate what is causing the model to - * continuously change during the digest. + * var promise = asyncGreet('Robin Hood'); + * promise.then(function(greeting) { + * alert('Success: ' + greeting); + * }, function(reason) { + * alert('Failed: ' + reason); + * }); + * ``` * - * Increasing the TTL could have performance implications, so you should not change it without - * proper justification. + * Note: progress/notify callbacks are not currently supported via the ES6-style interface. * - * @param {number} limit The number of digest iterations. - */ - - - /** - * @ngdoc service - * @name $rootScope - * @description + * Note: unlike ES6 behavior, an exception thrown in the constructor function will NOT implicitly reject the promise. * - * Every application has a single root {@link ng.$rootScope.Scope scope}. - * All other scopes are descendant scopes of the root scope. Scopes provide separation - * between the model and the view, via a mechanism for watching the model for changes. - * They also provide an event emission/broadcast and subscription facility. See the - * {@link guide/scope developer guide on scopes}. - */ - function $RootScopeProvider(){ - var TTL = 10; - var $rootScopeMinErr = minErr('$rootScope'); - var lastDirtyWatch = null; - - this.digestTtl = function(value) { - if (arguments.length) { - TTL = value; - } - return TTL; - }; - - this.$get = ['$injector', '$exceptionHandler', '$parse', '$browser', - function( $injector, $exceptionHandler, $parse, $browser) { - - /** - * @ngdoc type - * @name $rootScope.Scope - * - * @description - * A root scope can be retrieved using the {@link ng.$rootScope $rootScope} key from the - * {@link auto.$injector $injector}. Child scopes are created using the - * {@link ng.$rootScope.Scope#$new $new()} method. (Most scopes are created automatically when - * compiled HTML template is executed.) - * - * Here is a simple scope snippet to show how you can interact with the scope. - * ```html - * - * ``` - * - * # Inheritance - * A scope can inherit from a parent scope, as in this example: - * ```js - var parent = $rootScope; - var child = parent.$new(); - - parent.salutation = "Hello"; - child.name = "World"; - expect(child.salutation).toEqual('Hello'); - - child.salutation = "Welcome"; - expect(child.salutation).toEqual('Welcome'); - expect(parent.salutation).toEqual('Hello'); - * ``` - * - * - * @param {Object.=} providers Map of service factory which need to be - * provided for the current scope. Defaults to {@link ng}. - * @param {Object.=} instanceCache Provides pre-instantiated services which should - * append/override services provided by `providers`. This is handy - * when unit-testing and having the need to override a default - * service. - * @returns {Object} Newly created scope. - * - */ - function Scope() { - this.$id = nextUid(); - this.$$phase = this.$parent = this.$$watchers = - this.$$nextSibling = this.$$prevSibling = - this.$$childHead = this.$$childTail = null; - this['this'] = this.$root = this; - this.$$destroyed = false; - this.$$asyncQueue = []; - this.$$postDigestQueue = []; - this.$$listeners = {}; - this.$$listenerCount = {}; - this.$$isolateBindings = {}; - } + * However, the more traditional CommonJS-style usage is still available, and documented below. + * + * [The CommonJS Promise proposal](http://wiki.commonjs.org/wiki/Promises) describes a promise as an + * interface for interacting with an object that represents the result of an action that is + * performed asynchronously, and may or may not be finished at any given point in time. + * + * From the perspective of dealing with error handling, deferred and promise APIs are to + * asynchronous programming what `try`, `catch` and `throw` keywords are to synchronous programming. + * + * ```js + * // for the purpose of this example let's assume that variables `$q` and `okToGreet` + * // are available in the current lexical scope (they could have been injected or passed in). + * + * function asyncGreet(name) { + * var deferred = $q.defer(); + * + * setTimeout(function() { + * deferred.notify('About to greet ' + name + '.'); + * + * if (okToGreet(name)) { + * deferred.resolve('Hello, ' + name + '!'); + * } else { + * deferred.reject('Greeting ' + name + ' is not allowed.'); + * } + * }, 1000); + * + * return deferred.promise; + * } + * + * var promise = asyncGreet('Robin Hood'); + * promise.then(function(greeting) { + * alert('Success: ' + greeting); + * }, function(reason) { + * alert('Failed: ' + reason); + * }, function(update) { + * alert('Got notification: ' + update); + * }); + * ``` + * + * At first it might not be obvious why this extra complexity is worth the trouble. The payoff + * comes in the way of guarantees that promise and deferred APIs make, see + * https://github.com/kriskowal/uncommonjs/blob/master/promises/specification.md. + * + * Additionally the promise api allows for composition that is very hard to do with the + * traditional callback ([CPS](http://en.wikipedia.org/wiki/Continuation-passing_style)) approach. + * For more on this please see the [Q documentation](https://github.com/kriskowal/q) especially the + * section on serial or parallel joining of promises. + * + * ## The Deferred API + * + * A new instance of deferred is constructed by calling `$q.defer()`. + * + * The purpose of the deferred object is to expose the associated Promise instance as well as APIs + * that can be used for signaling the successful or unsuccessful completion, as well as the status + * of the task. + * + * **Methods** + * + * - `resolve(value)` – resolves the derived promise with the `value`. If the value is a rejection + * constructed via `$q.reject`, the promise will be rejected instead. + * - `reject(reason)` – rejects the derived promise with the `reason`. This is equivalent to + * resolving it with a rejection constructed via `$q.reject`. + * - `notify(value)` - provides updates on the status of the promise's execution. This may be called + * multiple times before the promise is either resolved or rejected. + * + * **Properties** + * + * - promise – `{Promise}` – promise object associated with this deferred. + * + * + * ## The Promise API + * + * A new promise instance is created when a deferred instance is created and can be retrieved by + * calling `deferred.promise`. + * + * The purpose of the promise object is to allow for interested parties to get access to the result + * of the deferred task when it completes. + * + * **Methods** + * + * - `then(successCallback, [errorCallback], [notifyCallback])` – regardless of when the promise was or + * will be resolved or rejected, `then` calls one of the success or error callbacks asynchronously + * as soon as the result is available. The callbacks are called with a single argument: the result + * or rejection reason. Additionally, the notify callback may be called zero or more times to + * provide a progress indication, before the promise is resolved or rejected. + * + * This method *returns a new promise* which is resolved or rejected via the return value of the + * `successCallback`, `errorCallback` (unless that value is a promise, in which case it is resolved + * with the value which is resolved in that promise using + * [promise chaining](http://www.html5rocks.com/en/tutorials/es6/promises/#toc-promises-queues)). + * It also notifies via the return value of the `notifyCallback` method. The promise cannot be + * resolved or rejected from the notifyCallback method. The errorCallback and notifyCallback + * arguments are optional. + * + * - `catch(errorCallback)` – shorthand for `promise.then(null, errorCallback)` + * + * - `finally(callback, notifyCallback)` – allows you to observe either the fulfillment or rejection of a promise, + * but to do so without modifying the final value. This is useful to release resources or do some + * clean-up that needs to be done whether the promise was rejected or resolved. See the [full + * specification](https://github.com/kriskowal/q/wiki/API-Reference#promisefinallycallback) for + * more information. + * + * ## Chaining promises + * + * Because calling the `then` method of a promise returns a new derived promise, it is easily + * possible to create a chain of promises: + * + * ```js + * promiseB = promiseA.then(function(result) { + * return result + 1; + * }); + * + * // promiseB will be resolved immediately after promiseA is resolved and its value + * // will be the result of promiseA incremented by 1 + * ``` + * + * It is possible to create chains of any length and since a promise can be resolved with another + * promise (which will defer its resolution further), it is possible to pause/defer resolution of + * the promises at any point in the chain. This makes it possible to implement powerful APIs like + * $http's response interceptors. + * + * + * ## Differences between Kris Kowal's Q and $q + * + * There are two main differences: + * + * - $q is integrated with the {@link ng.$rootScope.Scope} Scope model observation + * mechanism in AngularJS, which means faster propagation of resolution or rejection into your + * models and avoiding unnecessary browser repaints, which would result in flickering UI. + * - Q has many more features than $q, but that comes at a cost of bytes. $q is tiny, but contains + * all the important functionality needed for common async tasks. + * + * ## Testing + * + * ```js + * it('should simulate promise', inject(function($q, $rootScope) { + * var deferred = $q.defer(); + * var promise = deferred.promise; + * var resolvedValue; + * + * promise.then(function(value) { resolvedValue = value; }); + * expect(resolvedValue).toBeUndefined(); + * + * // Simulate resolving of promise + * deferred.resolve(123); + * // Note that the 'then' function does not get called synchronously. + * // This is because we want the promise API to always be async, whether or not + * // it got called synchronously or asynchronously. + * expect(resolvedValue).toBeUndefined(); + * + * // Propagate promise resolution to 'then' functions using $apply(). + * $rootScope.$apply(); + * expect(resolvedValue).toEqual(123); + * })); + * ``` + * + * @param {function(function, function)} resolver Function which is responsible for resolving or + * rejecting the newly created promise. The first parameter is a function which resolves the + * promise, the second parameter is a function which rejects the promise. + * + * @returns {Promise} The newly created promise. + */ + /** + * @ngdoc provider + * @name $qProvider + * @this + * + * @description + */ + function $QProvider() { + var errorOnUnhandledRejections = true; + this.$get = ['$rootScope', '$exceptionHandler', function($rootScope, $exceptionHandler) { + return qFactory(function(callback) { + $rootScope.$evalAsync(callback); + }, $exceptionHandler, errorOnUnhandledRejections); + }]; - /** - * @ngdoc property - * @name $rootScope.Scope#$id - * @returns {number} Unique scope ID (monotonically increasing alphanumeric sequence) useful for - * debugging. - */ + /** + * @ngdoc method + * @name $qProvider#errorOnUnhandledRejections + * @kind function + * + * @description + * Retrieves or overrides whether to generate an error when a rejected promise is not handled. + * This feature is enabled by default. + * + * @param {boolean=} value Whether to generate an error when a rejected promise is not handled. + * @returns {boolean|ng.$qProvider} Current value when called without a new value or self for + * chaining otherwise. + */ + this.errorOnUnhandledRejections = function(value) { + if (isDefined(value)) { + errorOnUnhandledRejections = value; + return this; + } else { + return errorOnUnhandledRejections; + } + }; + } + /** @this */ + function $$QProvider() { + var errorOnUnhandledRejections = true; + this.$get = ['$browser', '$exceptionHandler', function($browser, $exceptionHandler) { + return qFactory(function(callback) { + $browser.defer(callback); + }, $exceptionHandler, errorOnUnhandledRejections); + }]; - Scope.prototype = { - constructor: Scope, - /** - * @ngdoc method - * @name $rootScope.Scope#$new - * @function - * - * @description - * Creates a new child {@link ng.$rootScope.Scope scope}. - * - * The parent scope will propagate the {@link ng.$rootScope.Scope#$digest $digest()} and - * {@link ng.$rootScope.Scope#$digest $digest()} events. The scope can be removed from the - * scope hierarchy using {@link ng.$rootScope.Scope#$destroy $destroy()}. - * - * {@link ng.$rootScope.Scope#$destroy $destroy()} must be called on a scope when it is - * desired for the scope and its child scopes to be permanently detached from the parent and - * thus stop participating in model change detection and listener notification by invoking. - * - * @param {boolean} isolate If true, then the scope does not prototypically inherit from the - * parent scope. The scope is isolated, as it can not see parent scope properties. - * When creating widgets, it is useful for the widget to not accidentally read parent - * state. - * - * @returns {Object} The newly created child scope. - * - */ - $new: function(isolate) { - var ChildScope, - child; + this.errorOnUnhandledRejections = function(value) { + if (isDefined(value)) { + errorOnUnhandledRejections = value; + return this; + } else { + return errorOnUnhandledRejections; + } + }; + } - if (isolate) { - child = new Scope(); - child.$root = this.$root; - // ensure that there is just one async queue per $rootScope and its children - child.$$asyncQueue = this.$$asyncQueue; - child.$$postDigestQueue = this.$$postDigestQueue; - } else { - ChildScope = function() {}; // should be anonymous; This is so that when the minifier munges - // the name it does not become random set of chars. This will then show up as class - // name in the web inspector. - ChildScope.prototype = this; - child = new ChildScope(); - child.$id = nextUid(); - } - child['this'] = child; - child.$$listeners = {}; - child.$$listenerCount = {}; - child.$parent = this; - child.$$watchers = child.$$nextSibling = child.$$childHead = child.$$childTail = null; - child.$$prevSibling = this.$$childTail; - if (this.$$childHead) { - this.$$childTail.$$nextSibling = child; - this.$$childTail = child; - } else { - this.$$childHead = this.$$childTail = child; - } - return child; - }, + /** + * Constructs a promise manager. + * + * @param {function(function)} nextTick Function for executing functions in the next turn. + * @param {function(...*)} exceptionHandler Function into which unexpected exceptions are passed for + * debugging purposes. + * @param {boolean=} errorOnUnhandledRejections Whether an error should be generated on unhandled + * promises rejections. + * @returns {object} Promise manager. + */ + function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) { + var $qMinErr = minErr('$q', TypeError); + var queueSize = 0; + var checkQueue = []; - /** - * @ngdoc method - * @name $rootScope.Scope#$watch - * @function - * - * @description - * Registers a `listener` callback to be executed whenever the `watchExpression` changes. - * - * - The `watchExpression` is called on every call to {@link ng.$rootScope.Scope#$digest - * $digest()} and should return the value that will be watched. (Since - * {@link ng.$rootScope.Scope#$digest $digest()} reruns when it detects changes the - * `watchExpression` can execute multiple times per - * {@link ng.$rootScope.Scope#$digest $digest()} and should be idempotent.) - * - The `listener` is called only when the value from the current `watchExpression` and the - * previous call to `watchExpression` are not equal (with the exception of the initial run, - * see below). The inequality is determined according to - * {@link angular.equals} function. To save the value of the object for later comparison, - * the {@link angular.copy} function is used. It also means that watching complex options - * will have adverse memory and performance implications. - * - The watch `listener` may change the model, which may trigger other `listener`s to fire. - * This is achieved by rerunning the watchers until no changes are detected. The rerun - * iteration limit is 10 to prevent an infinite loop deadlock. - * - * - * If you want to be notified whenever {@link ng.$rootScope.Scope#$digest $digest} is called, - * you can register a `watchExpression` function with no `listener`. (Since `watchExpression` - * can execute multiple times per {@link ng.$rootScope.Scope#$digest $digest} cycle when a - * change is detected, be prepared for multiple calls to your listener.) - * - * After a watcher is registered with the scope, the `listener` fn is called asynchronously - * (via {@link ng.$rootScope.Scope#$evalAsync $evalAsync}) to initialize the - * watcher. In rare cases, this is undesirable because the listener is called when the result - * of `watchExpression` didn't change. To detect this scenario within the `listener` fn, you - * can compare the `newVal` and `oldVal`. If these two values are identical (`===`) then the - * listener was called due to initialization. - * - * The example below contains an illustration of using a function as your $watch listener - * - * - * # Example - * ```js - // let's assume that scope was dependency injected as the $rootScope - var scope = $rootScope; - scope.name = 'misko'; - scope.counter = 0; + /** + * @ngdoc method + * @name ng.$q#defer + * @kind function + * + * @description + * Creates a `Deferred` object which represents a task which will finish in the future. + * + * @returns {Deferred} Returns a new instance of deferred. + */ + function defer() { + return new Deferred(); + } - expect(scope.counter).toEqual(0); - scope.$watch('name', function(newValue, oldValue) { - scope.counter = scope.counter + 1; - }); - expect(scope.counter).toEqual(0); + function Deferred() { + var promise = this.promise = new Promise(); + //Non prototype methods necessary to support unbound execution :/ + this.resolve = function(val) { resolvePromise(promise, val); }; + this.reject = function(reason) { rejectPromise(promise, reason); }; + this.notify = function(progress) { notifyPromise(promise, progress); }; + } - scope.$digest(); - // no variable change - expect(scope.counter).toEqual(0); - scope.name = 'adam'; - scope.$digest(); - expect(scope.counter).toEqual(1); + function Promise() { + this.$$state = { status: 0 }; + } + extend(Promise.prototype, { + then: function(onFulfilled, onRejected, progressBack) { + if (isUndefined(onFulfilled) && isUndefined(onRejected) && isUndefined(progressBack)) { + return this; + } + var result = new Promise(); + this.$$state.pending = this.$$state.pending || []; + this.$$state.pending.push([result, onFulfilled, onRejected, progressBack]); + if (this.$$state.status > 0) scheduleProcessQueue(this.$$state); - // Using a listener function - var food; - scope.foodCounter = 0; - expect(scope.foodCounter).toEqual(0); - scope.$watch( - // This is the listener function - function() { return food; }, - // This is the change handler - function(newValue, oldValue) { - if ( newValue !== oldValue ) { - // Only increment the counter if the value changed - scope.foodCounter = scope.foodCounter + 1; - } - } - ); - // No digest has been run so the counter will be zero - expect(scope.foodCounter).toEqual(0); - - // Run the digest but since food has not changed count will still be zero - scope.$digest(); - expect(scope.foodCounter).toEqual(0); + return result; + }, - // Update food and run digest. Now the counter will increment - food = 'cheeseburger'; - scope.$digest(); - expect(scope.foodCounter).toEqual(1); + 'catch': function(callback) { + return this.then(null, callback); + }, - * ``` - * - * - * - * @param {(function()|string)} watchExpression Expression that is evaluated on each - * {@link ng.$rootScope.Scope#$digest $digest} cycle. A change in the return value triggers - * a call to the `listener`. - * - * - `string`: Evaluated as {@link guide/expression expression} - * - `function(scope)`: called with current `scope` as a parameter. - * @param {(function()|string)=} listener Callback called whenever the return value of - * the `watchExpression` changes. - * - * - `string`: Evaluated as {@link guide/expression expression} - * - `function(newValue, oldValue, scope)`: called with current and previous values as - * parameters. - * - * @param {boolean=} objectEquality Compare for object equality using {@link angular.equals} instead of - * comparing for reference equality. - * @returns {function()} Returns a deregistration function for this listener. - */ - $watch: function(watchExp, listener, objectEquality) { - var scope = this, - get = compileToFn(watchExp, 'watch'), - array = scope.$$watchers, - watcher = { - fn: listener, - last: initWatchVal, - get: get, - exp: watchExp, - eq: !!objectEquality - }; + 'finally': function(callback, progressBack) { + return this.then(function(value) { + return handleCallback(value, resolve, callback); + }, function(error) { + return handleCallback(error, reject, callback); + }, progressBack); + } + }); - lastDirtyWatch = null; + function processQueue(state) { + var fn, promise, pending; - // in the case user pass string, we need to compile it, do we really need this ? - if (!isFunction(listener)) { - var listenFn = compileToFn(listener || noop, 'listener'); - watcher.fn = function(newVal, oldVal, scope) {listenFn(scope);}; + pending = state.pending; + state.processScheduled = false; + state.pending = undefined; + try { + for (var i = 0, ii = pending.length; i < ii; ++i) { + markQStateExceptionHandled(state); + promise = pending[i][0]; + fn = pending[i][state.status]; + try { + if (isFunction(fn)) { + resolvePromise(promise, fn(state.value)); + } else if (state.status === 1) { + resolvePromise(promise, state.value); + } else { + rejectPromise(promise, state.value); } - - if (typeof watchExp == 'string' && get.constant) { - var originalFn = watcher.fn; - watcher.fn = function(newVal, oldVal, scope) { - originalFn.call(this, newVal, oldVal, scope); - arrayRemove(array, watcher); - }; + } catch (e) { + rejectPromise(promise, e); + // This error is explicitly marked for being passed to the $exceptionHandler + if (e && e.$$passToExceptionHandler === true) { + exceptionHandler(e); } + } + } + } finally { + --queueSize; + if (errorOnUnhandledRejections && queueSize === 0) { + nextTick(processChecks); + } + } + } - if (!array) { - array = scope.$$watchers = []; - } - // we use unshift since we use a while loop in $digest for speed. - // the while loop reads in reverse order. - array.unshift(watcher); + function processChecks() { + // eslint-disable-next-line no-unmodified-loop-condition + while (!queueSize && checkQueue.length) { + var toCheck = checkQueue.shift(); + if (!isStateExceptionHandled(toCheck)) { + markQStateExceptionHandled(toCheck); + var errorMessage = 'Possibly unhandled rejection: ' + toDebugString(toCheck.value); + if (isError(toCheck.value)) { + exceptionHandler(toCheck.value, errorMessage); + } else { + exceptionHandler(errorMessage); + } + } + } + } - return function() { - arrayRemove(array, watcher); - lastDirtyWatch = null; - }; - }, + function scheduleProcessQueue(state) { + if (errorOnUnhandledRejections && !state.pending && state.status === 2 && !isStateExceptionHandled(state)) { + if (queueSize === 0 && checkQueue.length === 0) { + nextTick(processChecks); + } + checkQueue.push(state); + } + if (state.processScheduled || !state.pending) return; + state.processScheduled = true; + ++queueSize; + nextTick(function() { processQueue(state); }); + } + function resolvePromise(promise, val) { + if (promise.$$state.status) return; + if (val === promise) { + $$reject(promise, $qMinErr( + 'qcycle', + 'Expected promise to be resolved with value other than itself \'{0}\'', + val)); + } else { + $$resolve(promise, val); + } - /** - * @ngdoc method - * @name $rootScope.Scope#$watchCollection - * @function - * - * @description - * Shallow watches the properties of an object and fires whenever any of the properties change - * (for arrays, this implies watching the array items; for object maps, this implies watching - * the properties). If a change is detected, the `listener` callback is fired. - * - * - The `obj` collection is observed via standard $watch operation and is examined on every - * call to $digest() to see if any items have been added, removed, or moved. - * - The `listener` is called whenever anything within the `obj` has changed. Examples include - * adding, removing, and moving items belonging to an object or array. - * - * - * # Example - * ```js - $scope.names = ['igor', 'matias', 'misko', 'james']; - $scope.dataCount = 4; + } - $scope.$watchCollection('names', function(newNames, oldNames) { - $scope.dataCount = newNames.length; - }); + function $$resolve(promise, val) { + var then; + var done = false; + try { + if (isObject(val) || isFunction(val)) then = val.then; + if (isFunction(then)) { + promise.$$state.status = -1; + then.call(val, doResolve, doReject, doNotify); + } else { + promise.$$state.value = val; + promise.$$state.status = 1; + scheduleProcessQueue(promise.$$state); + } + } catch (e) { + doReject(e); + } - expect($scope.dataCount).toEqual(4); - $scope.$digest(); + function doResolve(val) { + if (done) return; + done = true; + $$resolve(promise, val); + } + function doReject(val) { + if (done) return; + done = true; + $$reject(promise, val); + } + function doNotify(progress) { + notifyPromise(promise, progress); + } + } - //still at 4 ... no changes - expect($scope.dataCount).toEqual(4); + function rejectPromise(promise, reason) { + if (promise.$$state.status) return; + $$reject(promise, reason); + } - $scope.names.pop(); - $scope.$digest(); + function $$reject(promise, reason) { + promise.$$state.value = reason; + promise.$$state.status = 2; + scheduleProcessQueue(promise.$$state); + } - //now there's been a change - expect($scope.dataCount).toEqual(3); - * ``` - * - * - * @param {string|function(scope)} obj Evaluated as {@link guide/expression expression}. The - * expression value should evaluate to an object or an array which is observed on each - * {@link ng.$rootScope.Scope#$digest $digest} cycle. Any shallow change within the - * collection will trigger a call to the `listener`. - * - * @param {function(newCollection, oldCollection, scope)} listener a callback function called - * when a change is detected. - * - The `newCollection` object is the newly modified data obtained from the `obj` expression - * - The `oldCollection` object is a copy of the former collection data. - * Due to performance considerations, the`oldCollection` value is computed only if the - * `listener` function declares two or more arguments. - * - The `scope` argument refers to the current scope. - * - * @returns {function()} Returns a de-registration function for this listener. When the - * de-registration function is executed, the internal watch operation is terminated. - */ - $watchCollection: function(obj, listener) { - var self = this; - // the current value, updated on each dirty-check run - var newValue; - // a shallow copy of the newValue from the last dirty-check run, - // updated to match newValue during dirty-check run - var oldValue; - // a shallow copy of the newValue from when the last change happened - var veryOldValue; - // only track veryOldValue if the listener is asking for it - var trackVeryOldValue = (listener.length > 1); - var changeDetected = 0; - var objGetter = $parse(obj); - var internalArray = []; - var internalObject = {}; - var initRun = true; - var oldLength = 0; + function notifyPromise(promise, progress) { + var callbacks = promise.$$state.pending; - function $watchCollectionWatch() { - newValue = objGetter(self); - var newLength, key; + if ((promise.$$state.status <= 0) && callbacks && callbacks.length) { + nextTick(function() { + var callback, result; + for (var i = 0, ii = callbacks.length; i < ii; i++) { + result = callbacks[i][0]; + callback = callbacks[i][3]; + try { + notifyPromise(result, isFunction(callback) ? callback(progress) : progress); + } catch (e) { + exceptionHandler(e); + } + } + }); + } + } - if (!isObject(newValue)) { // if primitive - if (oldValue !== newValue) { - oldValue = newValue; - changeDetected++; - } - } else if (isArrayLike(newValue)) { - if (oldValue !== internalArray) { - // we are transitioning from something which was not an array into array. - oldValue = internalArray; - oldLength = oldValue.length = 0; - changeDetected++; - } + /** + * @ngdoc method + * @name $q#reject + * @kind function + * + * @description + * Creates a promise that is resolved as rejected with the specified `reason`. This api should be + * used to forward rejection in a chain of promises. If you are dealing with the last promise in + * a promise chain, you don't need to worry about it. + * + * When comparing deferreds/promises to the familiar behavior of try/catch/throw, think of + * `reject` as the `throw` keyword in JavaScript. This also means that if you "catch" an error via + * a promise error callback and you want to forward the error to the promise derived from the + * current promise, you have to "rethrow" the error by returning a rejection constructed via + * `reject`. + * + * ```js + * promiseB = promiseA.then(function(result) { + * // success: do something and resolve promiseB + * // with the old or a new result + * return result; + * }, function(reason) { + * // error: handle the error if possible and + * // resolve promiseB with newPromiseOrValue, + * // otherwise forward the rejection to promiseB + * if (canHandle(reason)) { + * // handle the error and recover + * return newPromiseOrValue; + * } + * return $q.reject(reason); + * }); + * ``` + * + * @param {*} reason Constant, message, exception or an object representing the rejection reason. + * @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`. + */ + function reject(reason) { + var result = new Promise(); + rejectPromise(result, reason); + return result; + } - newLength = newValue.length; + function handleCallback(value, resolver, callback) { + var callbackOutput = null; + try { + if (isFunction(callback)) callbackOutput = callback(); + } catch (e) { + return reject(e); + } + if (isPromiseLike(callbackOutput)) { + return callbackOutput.then(function() { + return resolver(value); + }, reject); + } else { + return resolver(value); + } + } - if (oldLength !== newLength) { - // if lengths do not match we need to trigger change notification - changeDetected++; - oldValue.length = oldLength = newLength; - } - // copy the items to oldValue and look for changes. - for (var i = 0; i < newLength; i++) { - var bothNaN = (oldValue[i] !== oldValue[i]) && - (newValue[i] !== newValue[i]); - if (!bothNaN && (oldValue[i] !== newValue[i])) { - changeDetected++; - oldValue[i] = newValue[i]; - } - } - } else { - if (oldValue !== internalObject) { - // we are transitioning from something which was not an object into object. - oldValue = internalObject = {}; - oldLength = 0; - changeDetected++; - } - // copy the items to oldValue and look for changes. - newLength = 0; - for (key in newValue) { - if (newValue.hasOwnProperty(key)) { - newLength++; - if (oldValue.hasOwnProperty(key)) { - if (oldValue[key] !== newValue[key]) { - changeDetected++; - oldValue[key] = newValue[key]; - } - } else { - oldLength++; - oldValue[key] = newValue[key]; - changeDetected++; - } - } - } - if (oldLength > newLength) { - // we used to have more keys, need to find them and destroy them. - changeDetected++; - for(key in oldValue) { - if (oldValue.hasOwnProperty(key) && !newValue.hasOwnProperty(key)) { - oldLength--; - delete oldValue[key]; - } - } - } - } - return changeDetected; - } + /** + * @ngdoc method + * @name $q#when + * @kind function + * + * @description + * Wraps an object that might be a value or a (3rd party) then-able promise into a $q promise. + * This is useful when you are dealing with an object that might or might not be a promise, or if + * the promise comes from a source that can't be trusted. + * + * @param {*} value Value or a promise + * @param {Function=} successCallback + * @param {Function=} errorCallback + * @param {Function=} progressCallback + * @returns {Promise} Returns a promise of the passed value or promise + */ - function $watchCollectionAction() { - if (initRun) { - initRun = false; - listener(newValue, newValue, self); - } else { - listener(newValue, veryOldValue, self); - } - - // make a copy for the next time a collection is changed - if (trackVeryOldValue) { - if (!isObject(newValue)) { - //primitive - veryOldValue = newValue; - } else if (isArrayLike(newValue)) { - veryOldValue = new Array(newValue.length); - for (var i = 0; i < newValue.length; i++) { - veryOldValue[i] = newValue[i]; - } - } else { // if object - veryOldValue = {}; - for (var key in newValue) { - if (hasOwnProperty.call(newValue, key)) { - veryOldValue[key] = newValue[key]; - } - } - } - } - } - return this.$watch($watchCollectionWatch, $watchCollectionAction); - }, - - /** - * @ngdoc method - * @name $rootScope.Scope#$digest - * @function - * - * @description - * Processes all of the {@link ng.$rootScope.Scope#$watch watchers} of the current scope and - * its children. Because a {@link ng.$rootScope.Scope#$watch watcher}'s listener can change - * the model, the `$digest()` keeps calling the {@link ng.$rootScope.Scope#$watch watchers} - * until no more listeners are firing. This means that it is possible to get into an infinite - * loop. This function will throw `'Maximum iteration limit exceeded.'` if the number of - * iterations exceeds 10. - * - * Usually, you don't call `$digest()` directly in - * {@link ng.directive:ngController controllers} or in - * {@link ng.$compileProvider#directive directives}. - * Instead, you should call {@link ng.$rootScope.Scope#$apply $apply()} (typically from within - * a {@link ng.$compileProvider#directive directives}), which will force a `$digest()`. - * - * If you want to be notified whenever `$digest()` is called, - * you can register a `watchExpression` function with - * {@link ng.$rootScope.Scope#$watch $watch()} with no `listener`. - * - * In unit tests, you may need to call `$digest()` to simulate the scope life cycle. - * - * # Example - * ```js - var scope = ...; - scope.name = 'misko'; - scope.counter = 0; + function when(value, callback, errback, progressBack) { + var result = new Promise(); + resolvePromise(result, value); + return result.then(callback, errback, progressBack); + } - expect(scope.counter).toEqual(0); - scope.$watch('name', function(newValue, oldValue) { - scope.counter = scope.counter + 1; - }); - expect(scope.counter).toEqual(0); + /** + * @ngdoc method + * @name $q#resolve + * @kind function + * + * @description + * Alias of {@link ng.$q#when when} to maintain naming consistency with ES6. + * + * @param {*} value Value or a promise + * @param {Function=} successCallback + * @param {Function=} errorCallback + * @param {Function=} progressCallback + * @returns {Promise} Returns a promise of the passed value or promise + */ + var resolve = when; - scope.$digest(); - // no variable change - expect(scope.counter).toEqual(0); + /** + * @ngdoc method + * @name $q#all + * @kind function + * + * @description + * Combines multiple promises into a single promise that is resolved when all of the input + * promises are resolved. + * + * @param {Array.|Object.} promises An array or hash of promises. + * @returns {Promise} Returns a single promise that will be resolved with an array/hash of values, + * each value corresponding to the promise at the same index/key in the `promises` array/hash. + * If any of the promises is resolved with a rejection, this resulting promise will be rejected + * with the same rejection value. + */ - scope.name = 'adam'; - scope.$digest(); - expect(scope.counter).toEqual(1); - * ``` - * - */ - $digest: function() { - var watch, value, last, - watchers, - asyncQueue = this.$$asyncQueue, - postDigestQueue = this.$$postDigestQueue, - length, - dirty, ttl = TTL, - next, current, target = this, - watchLog = [], - logIdx, logMsg, asyncTask; + function all(promises) { + var result = new Promise(), + counter = 0, + results = isArray(promises) ? [] : {}; - beginPhase('$digest'); + forEach(promises, function(promise, key) { + counter++; + when(promise).then(function(value) { + results[key] = value; + if (!(--counter)) resolvePromise(result, results); + }, function(reason) { + rejectPromise(result, reason); + }); + }); - lastDirtyWatch = null; + if (counter === 0) { + resolvePromise(result, results); + } - do { // "while dirty" loop - dirty = false; - current = target; + return result; + } - while(asyncQueue.length) { - try { - asyncTask = asyncQueue.shift(); - asyncTask.scope.$eval(asyncTask.expression); - } catch (e) { - clearPhase(); - $exceptionHandler(e); - } - lastDirtyWatch = null; - } + /** + * @ngdoc method + * @name $q#race + * @kind function + * + * @description + * Returns a promise that resolves or rejects as soon as one of those promises + * resolves or rejects, with the value or reason from that promise. + * + * @param {Array.|Object.} promises An array or hash of promises. + * @returns {Promise} a promise that resolves or rejects as soon as one of the `promises` + * resolves or rejects, with the value or reason from that promise. + */ - traverseScopesLoop: - do { // "traverse the scopes" loop - if ((watchers = current.$$watchers)) { - // process our watches - length = watchers.length; - while (length--) { - try { - watch = watchers[length]; - // Most common watches are on primitives, in which case we can short - // circuit it with === operator, only when === fails do we use .equals - if (watch) { - if ((value = watch.get(current)) !== (last = watch.last) && - !(watch.eq - ? equals(value, last) - : (typeof value == 'number' && typeof last == 'number' - && isNaN(value) && isNaN(last)))) { - dirty = true; - lastDirtyWatch = watch; - watch.last = watch.eq ? copy(value) : value; - watch.fn(value, ((last === initWatchVal) ? value : last), current); - if (ttl < 5) { - logIdx = 4 - ttl; - if (!watchLog[logIdx]) watchLog[logIdx] = []; - logMsg = (isFunction(watch.exp)) - ? 'fn: ' + (watch.exp.name || watch.exp.toString()) - : watch.exp; - logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last); - watchLog[logIdx].push(logMsg); - } - } else if (watch === lastDirtyWatch) { - // If the most recently dirty watcher is now clean, short circuit since the remaining watchers - // have already been tested. - dirty = false; - break traverseScopesLoop; - } - } - } catch (e) { - clearPhase(); - $exceptionHandler(e); - } - } - } + function race(promises) { + var deferred = defer(); - // Insanity Warning: scope depth-first traversal - // yes, this code is a bit crazy, but it works and we have tests to prove it! - // this piece should be kept in sync with the traversal in $broadcast - if (!(next = (current.$$childHead || - (current !== target && current.$$nextSibling)))) { - while(current !== target && !(next = current.$$nextSibling)) { - current = current.$parent; - } - } - } while ((current = next)); + forEach(promises, function(promise) { + when(promise).then(deferred.resolve, deferred.reject); + }); - // `break traverseScopesLoop;` takes us to here + return deferred.promise; + } - if((dirty || asyncQueue.length) && !(ttl--)) { - clearPhase(); - throw $rootScopeMinErr('infdig', - '{0} $digest() iterations reached. Aborting!\n' + - 'Watchers fired in the last 5 iterations: {1}', - TTL, toJson(watchLog)); - } + function $Q(resolver) { + if (!isFunction(resolver)) { + throw $qMinErr('norslvr', 'Expected resolverFn, got \'{0}\'', resolver); + } - } while (dirty || asyncQueue.length); + var promise = new Promise(); - clearPhase(); + function resolveFn(value) { + resolvePromise(promise, value); + } - while(postDigestQueue.length) { - try { - postDigestQueue.shift()(); - } catch (e) { - $exceptionHandler(e); - } - } - }, + function rejectFn(reason) { + rejectPromise(promise, reason); + } + resolver(resolveFn, rejectFn); - /** - * @ngdoc event - * @name $rootScope.Scope#$destroy - * @eventType broadcast on scope being destroyed - * - * @description - * Broadcasted when a scope and its children are being destroyed. - * - * Note that, in AngularJS, there is also a `$destroy` jQuery event, which can be used to - * clean up DOM bindings before an element is removed from the DOM. - */ + return promise; + } - /** - * @ngdoc method - * @name $rootScope.Scope#$destroy - * @function - * - * @description - * Removes the current scope (and all of its children) from the parent scope. Removal implies - * that calls to {@link ng.$rootScope.Scope#$digest $digest()} will no longer - * propagate to the current scope and its children. Removal also implies that the current - * scope is eligible for garbage collection. - * - * The `$destroy()` is usually used by directives such as - * {@link ng.directive:ngRepeat ngRepeat} for managing the - * unrolling of the loop. - * - * Just before a scope is destroyed, a `$destroy` event is broadcasted on this scope. - * Application code can register a `$destroy` event handler that will give it a chance to - * perform any necessary cleanup. - * - * Note that, in AngularJS, there is also a `$destroy` jQuery event, which can be used to - * clean up DOM bindings before an element is removed from the DOM. - */ - $destroy: function() { - // we can't destroy the root scope or a scope that has been already destroyed - if (this.$$destroyed) return; - var parent = this.$parent; + // Let's make the instanceof operator work for promises, so that + // `new $q(fn) instanceof $q` would evaluate to true. + $Q.prototype = Promise.prototype; - this.$broadcast('$destroy'); - this.$$destroyed = true; - if (this === $rootScope) return; + $Q.defer = defer; + $Q.reject = reject; + $Q.when = when; + $Q.resolve = resolve; + $Q.all = all; + $Q.race = race; - forEach(this.$$listenerCount, bind(null, decrementListenerCount, this)); + return $Q; + } - // sever all the references to parent scopes (after this cleanup, the current scope should - // not be retained by any of our references and should be eligible for garbage collection) - if (parent.$$childHead == this) parent.$$childHead = this.$$nextSibling; - if (parent.$$childTail == this) parent.$$childTail = this.$$prevSibling; - if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling; - if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling; + function isStateExceptionHandled(state) { + return !!state.pur; + } + function markQStateExceptionHandled(state) { + state.pur = true; + } + function markQExceptionHandled(q) { + markQStateExceptionHandled(q.$$state); + } + /** @this */ + function $$RAFProvider() { //rAF + this.$get = ['$window', '$timeout', function($window, $timeout) { + var requestAnimationFrame = $window.requestAnimationFrame || + $window.webkitRequestAnimationFrame; - // All of the code below is bogus code that works around V8's memory leak via optimized code - // and inline caches. - // - // see: - // - https://code.google.com/p/v8/issues/detail?id=2073#c26 - // - https://github.com/angular/angular.js/issues/6794#issuecomment-38648909 - // - https://github.com/angular/angular.js/issues/1313#issuecomment-10378451 + var cancelAnimationFrame = $window.cancelAnimationFrame || + $window.webkitCancelAnimationFrame || + $window.webkitCancelRequestAnimationFrame; - this.$parent = this.$$nextSibling = this.$$prevSibling = this.$$childHead = - this.$$childTail = this.$root = null; + var rafSupported = !!requestAnimationFrame; + var raf = rafSupported + ? function(fn) { + var id = requestAnimationFrame(fn); + return function() { + cancelAnimationFrame(id); + }; + } + : function(fn) { + var timer = $timeout(fn, 16.66, false); // 1000 / 60 = 16.666 + return function() { + $timeout.cancel(timer); + }; + }; - // don't reset these to null in case some async task tries to register a listener/watch/task - this.$$listeners = {}; - this.$$watchers = this.$$asyncQueue = this.$$postDigestQueue = []; + raf.supported = rafSupported; - // prevent NPEs since these methods have references to properties we nulled out - this.$destroy = this.$digest = this.$apply = noop; - this.$on = this.$watch = function() { return noop; }; - }, + return raf; + }]; + } - /** - * @ngdoc method - * @name $rootScope.Scope#$eval - * @function - * - * @description - * Executes the `expression` on the current scope and returns the result. Any exceptions in - * the expression are propagated (uncaught). This is useful when evaluating Angular - * expressions. - * - * # Example - * ```js - var scope = ng.$rootScope.Scope(); - scope.a = 1; - scope.b = 2; + /** + * DESIGN NOTES + * + * The design decisions behind the scope are heavily favored for speed and memory consumption. + * + * The typical use of scope is to watch the expressions, which most of the time return the same + * value as last time so we optimize the operation. + * + * Closures construction is expensive in terms of speed as well as memory: + * - No closures, instead use prototypical inheritance for API + * - Internal state needs to be stored on scope directly, which means that private state is + * exposed as $$____ properties + * + * Loop operations are optimized by using while(count--) { ... } + * - This means that in order to keep the same order of execution as addition we have to add + * items to the array at the beginning (unshift) instead of at the end (push) + * + * Child scopes are created and removed often + * - Using an array would be slow since inserts in the middle are expensive; so we use linked lists + * + * There are fewer watches than observers. This is why you don't want the observer to be implemented + * in the same way as watch. Watch requires return of the initialization function which is expensive + * to construct. + */ - expect(scope.$eval('a+b')).toEqual(3); - expect(scope.$eval(function(scope){ return scope.a + scope.b; })).toEqual(3); - * ``` - * - * @param {(string|function())=} expression An angular expression to be executed. - * - * - `string`: execute using the rules as defined in {@link guide/expression expression}. - * - `function(scope)`: execute the function with the current `scope` parameter. - * - * @param {(object)=} locals Local variables object, useful for overriding values in scope. - * @returns {*} The result of evaluating the expression. - */ - $eval: function(expr, locals) { - return $parse(expr)(this, locals); - }, + /** + * @ngdoc provider + * @name $rootScopeProvider + * @description + * + * Provider for the $rootScope service. + */ + + /** + * @ngdoc method + * @name $rootScopeProvider#digestTtl + * @description + * + * Sets the number of `$digest` iterations the scope should attempt to execute before giving up and + * assuming that the model is unstable. + * + * The current default is 10 iterations. + * + * In complex applications it's possible that the dependencies between `$watch`s will result in + * several digest iterations. However if an application needs more than the default 10 digest + * iterations for its model to stabilize then you should investigate what is causing the model to + * continuously change during the digest. + * + * Increasing the TTL could have performance implications, so you should not change it without + * proper justification. + * + * @param {number} limit The number of digest iterations. + */ + + + /** + * @ngdoc service + * @name $rootScope + * @this + * + * @description + * + * Every application has a single root {@link ng.$rootScope.Scope scope}. + * All other scopes are descendant scopes of the root scope. Scopes provide separation + * between the model and the view, via a mechanism for watching the model for changes. + * They also provide event emission/broadcast and subscription facility. See the + * {@link guide/scope developer guide on scopes}. + */ + function $RootScopeProvider() { + var TTL = 10; + var $rootScopeMinErr = minErr('$rootScope'); + var lastDirtyWatch = null; + var applyAsyncId = null; + + this.digestTtl = function(value) { + if (arguments.length) { + TTL = value; + } + return TTL; + }; + + function createChildScopeClass(parent) { + function ChildScope() { + this.$$watchers = this.$$nextSibling = + this.$$childHead = this.$$childTail = null; + this.$$listeners = {}; + this.$$listenerCount = {}; + this.$$watchersCount = 0; + this.$id = nextUid(); + this.$$ChildScope = null; + } + ChildScope.prototype = parent; + return ChildScope; + } + + this.$get = ['$exceptionHandler', '$parse', '$browser', + function($exceptionHandler, $parse, $browser) { + + function destroyChildScope($event) { + $event.currentScope.$$destroyed = true; + } + + function cleanUpScope($scope) { + + // Support: IE 9 only + if (msie === 9) { + // There is a memory leak in IE9 if all child scopes are not disconnected + // completely when a scope is destroyed. So this code will recurse up through + // all this scopes children + // + // See issue https://github.com/angular/angular.js/issues/10706 + if ($scope.$$childHead) { + cleanUpScope($scope.$$childHead); + } + if ($scope.$$nextSibling) { + cleanUpScope($scope.$$nextSibling); + } + } + + // The code below works around IE9 and V8's memory leaks + // + // See: + // - https://code.google.com/p/v8/issues/detail?id=2073#c26 + // - https://github.com/angular/angular.js/issues/6794#issuecomment-38648909 + // - https://github.com/angular/angular.js/issues/1313#issuecomment-10378451 + + $scope.$parent = $scope.$$nextSibling = $scope.$$prevSibling = $scope.$$childHead = + $scope.$$childTail = $scope.$root = $scope.$$watchers = null; + } + + /** + * @ngdoc type + * @name $rootScope.Scope + * + * @description + * A root scope can be retrieved using the {@link ng.$rootScope $rootScope} key from the + * {@link auto.$injector $injector}. Child scopes are created using the + * {@link ng.$rootScope.Scope#$new $new()} method. (Most scopes are created automatically when + * compiled HTML template is executed.) See also the {@link guide/scope Scopes guide} for + * an in-depth introduction and usage examples. + * + * + * ## Inheritance + * A scope can inherit from a parent scope, as in this example: + * ```js + var parent = $rootScope; + var child = parent.$new(); + + parent.salutation = "Hello"; + expect(child.salutation).toEqual('Hello'); + + child.salutation = "Welcome"; + expect(child.salutation).toEqual('Welcome'); + expect(parent.salutation).toEqual('Hello'); + * ``` + * + * When interacting with `Scope` in tests, additional helper methods are available on the + * instances of `Scope` type. See {@link ngMock.$rootScope.Scope ngMock Scope} for additional + * details. + * + * + * @param {Object.=} providers Map of service factory which need to be + * provided for the current scope. Defaults to {@link ng}. + * @param {Object.=} instanceCache Provides pre-instantiated services which should + * append/override services provided by `providers`. This is handy + * when unit-testing and having the need to override a default + * service. + * @returns {Object} Newly created scope. + * + */ + function Scope() { + this.$id = nextUid(); + this.$$phase = this.$parent = this.$$watchers = + this.$$nextSibling = this.$$prevSibling = + this.$$childHead = this.$$childTail = null; + this.$root = this; + this.$$destroyed = false; + this.$$listeners = {}; + this.$$listenerCount = {}; + this.$$watchersCount = 0; + this.$$isolateBindings = null; + } + + /** + * @ngdoc property + * @name $rootScope.Scope#$id + * + * @description + * Unique scope ID (monotonically increasing) useful for debugging. + */ + + /** + * @ngdoc property + * @name $rootScope.Scope#$parent + * + * @description + * Reference to the parent scope. + */ + + /** + * @ngdoc property + * @name $rootScope.Scope#$root + * + * @description + * Reference to the root scope. + */ + + Scope.prototype = { + constructor: Scope, /** * @ngdoc method - * @name $rootScope.Scope#$evalAsync - * @function + * @name $rootScope.Scope#$new + * @kind function * * @description - * Executes the expression on the current scope at a later point in time. - * - * The `$evalAsync` makes no guarantees as to when the `expression` will be executed, only - * that: + * Creates a new child {@link ng.$rootScope.Scope scope}. * - * - it will execute after the function that scheduled the evaluation (preferably before DOM - * rendering). - * - at least one {@link ng.$rootScope.Scope#$digest $digest cycle} will be performed after - * `expression` execution. + * The parent scope will propagate the {@link ng.$rootScope.Scope#$digest $digest()} event. + * The scope can be removed from the scope hierarchy using {@link ng.$rootScope.Scope#$destroy $destroy()}. * - * Any exceptions from the execution of the expression are forwarded to the - * {@link ng.$exceptionHandler $exceptionHandler} service. + * {@link ng.$rootScope.Scope#$destroy $destroy()} must be called on a scope when it is + * desired for the scope and its child scopes to be permanently detached from the parent and + * thus stop participating in model change detection and listener notification by invoking. * - * __Note:__ if this function is called outside of a `$digest` cycle, a new `$digest` cycle - * will be scheduled. However, it is encouraged to always call code that changes the model - * from within an `$apply` call. That includes code evaluated via `$evalAsync`. + * @param {boolean} isolate If true, then the scope does not prototypically inherit from the + * parent scope. The scope is isolated, as it can not see parent scope properties. + * When creating widgets, it is useful for the widget to not accidentally read parent + * state. * - * @param {(string|function())=} expression An angular expression to be executed. + * @param {Scope} [parent=this] The {@link ng.$rootScope.Scope `Scope`} that will be the `$parent` + * of the newly created scope. Defaults to `this` scope if not provided. + * This is used when creating a transclude scope to correctly place it + * in the scope hierarchy while maintaining the correct prototypical + * inheritance. * - * - `string`: execute using the rules as defined in {@link guide/expression expression}. - * - `function(scope)`: execute the function with the current `scope` parameter. + * @returns {Object} The newly created child scope. * */ - $evalAsync: function(expr) { - // if we are outside of an $digest loop and this is the first time we are scheduling async - // task also schedule async auto-flush - if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length) { - $browser.defer(function() { - if ($rootScope.$$asyncQueue.length) { - $rootScope.$digest(); - } - }); + $new: function(isolate, parent) { + var child; + + parent = parent || this; + + if (isolate) { + child = new Scope(); + child.$root = this.$root; + } else { + // Only create a child scope class if somebody asks for one, + // but cache it to allow the VM to optimize lookups. + if (!this.$$ChildScope) { + this.$$ChildScope = createChildScopeClass(this); + } + child = new this.$$ChildScope(); + } + child.$parent = parent; + child.$$prevSibling = parent.$$childTail; + if (parent.$$childHead) { + parent.$$childTail.$$nextSibling = child; + parent.$$childTail = child; + } else { + parent.$$childHead = parent.$$childTail = child; } - this.$$asyncQueue.push({scope: this, expression: expr}); - }, + // When the new scope is not isolated or we inherit from `this`, and + // the parent scope is destroyed, the property `$$destroyed` is inherited + // prototypically. In all other cases, this property needs to be set + // when the parent scope is destroyed. + // The listener needs to be added after the parent is set + if (isolate || parent !== this) child.$on('$destroy', destroyChildScope); - $$postDigest : function(fn) { - this.$$postDigestQueue.push(fn); + return child; }, /** * @ngdoc method - * @name $rootScope.Scope#$apply - * @function + * @name $rootScope.Scope#$watch + * @kind function * * @description - * `$apply()` is used to execute an expression in angular from outside of the angular - * framework. (For example from browser DOM events, setTimeout, XHR or third party libraries). - * Because we are calling into the angular framework we need to perform proper scope life - * cycle of {@link ng.$exceptionHandler exception handling}, - * {@link ng.$rootScope.Scope#$digest executing watches}. - * - * ## Life cycle + * Registers a `listener` callback to be executed whenever the `watchExpression` changes. * - * # Pseudo-Code of `$apply()` - * ```js - function $apply(expr) { - try { - return $eval(expr); - } catch (e) { - $exceptionHandler(e); - } finally { - $root.$digest(); - } - } - * ``` + * - The `watchExpression` is called on every call to {@link ng.$rootScope.Scope#$digest + * $digest()} and should return the value that will be watched. (`watchExpression` should not change + * its value when executed multiple times with the same input because it may be executed multiple + * times by {@link ng.$rootScope.Scope#$digest $digest()}. That is, `watchExpression` should be + * [idempotent](http://en.wikipedia.org/wiki/Idempotence).) + * - The `listener` is called only when the value from the current `watchExpression` and the + * previous call to `watchExpression` are not equal (with the exception of the initial run, + * see below). Inequality is determined according to reference inequality, + * [strict comparison](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Comparison_Operators) + * via the `!==` Javascript operator, unless `objectEquality == true` + * (see next point) + * - When `objectEquality == true`, inequality of the `watchExpression` is determined + * according to the {@link angular.equals} function. To save the value of the object for + * later comparison, the {@link angular.copy} function is used. This therefore means that + * watching complex objects will have adverse memory and performance implications. + * - This should not be used to watch for changes in objects that are + * or contain [File](https://developer.mozilla.org/docs/Web/API/File) objects due to limitations with {@link angular.copy `angular.copy`}. + * - The watch `listener` may change the model, which may trigger other `listener`s to fire. + * This is achieved by rerunning the watchers until no changes are detected. The rerun + * iteration limit is 10 to prevent an infinite loop deadlock. * * - * Scope's `$apply()` method transitions through the following stages: + * If you want to be notified whenever {@link ng.$rootScope.Scope#$digest $digest} is called, + * you can register a `watchExpression` function with no `listener`. (Be prepared for + * multiple calls to your `watchExpression` because it will execute multiple times in a + * single {@link ng.$rootScope.Scope#$digest $digest} cycle if a change is detected.) * - * 1. The {@link guide/expression expression} is executed using the - * {@link ng.$rootScope.Scope#$eval $eval()} method. - * 2. Any exceptions from the execution of the expression are forwarded to the - * {@link ng.$exceptionHandler $exceptionHandler} service. - * 3. The {@link ng.$rootScope.Scope#$watch watch} listeners are fired immediately after the - * expression was executed using the {@link ng.$rootScope.Scope#$digest $digest()} method. + * After a watcher is registered with the scope, the `listener` fn is called asynchronously + * (via {@link ng.$rootScope.Scope#$evalAsync $evalAsync}) to initialize the + * watcher. In rare cases, this is undesirable because the listener is called when the result + * of `watchExpression` didn't change. To detect this scenario within the `listener` fn, you + * can compare the `newVal` and `oldVal`. If these two values are identical (`===`) then the + * listener was called due to initialization. * * - * @param {(string|function())=} exp An angular expression to be executed. * - * - `string`: execute using the rules as defined in {@link guide/expression expression}. - * - `function(scope)`: execute the function with current `scope` parameter. - * - * @returns {*} The result of evaluating the expression. - */ - $apply: function(expr) { - try { - beginPhase('$apply'); - return this.$eval(expr); - } catch (e) { - $exceptionHandler(e); - } finally { - clearPhase(); - try { - $rootScope.$digest(); - } catch (e) { - $exceptionHandler(e); - throw e; - } - } - }, + * @example + * ```js + // let's assume that scope was dependency injected as the $rootScope + var scope = $rootScope; + scope.name = 'misko'; + scope.counter = 0; - /** - * @ngdoc method - * @name $rootScope.Scope#$on - * @function + expect(scope.counter).toEqual(0); + scope.$watch('name', function(newValue, oldValue) { + scope.counter = scope.counter + 1; + }); + expect(scope.counter).toEqual(0); + + scope.$digest(); + // the listener is always called during the first $digest loop after it was registered + expect(scope.counter).toEqual(1); + + scope.$digest(); + // but now it will not be called unless the value changes + expect(scope.counter).toEqual(1); + + scope.name = 'adam'; + scope.$digest(); + expect(scope.counter).toEqual(2); + + + + // Using a function as a watchExpression + var food; + scope.foodCounter = 0; + expect(scope.foodCounter).toEqual(0); + scope.$watch( + // This function returns the value being watched. It is called for each turn of the $digest loop + function() { return food; }, + // This is the change listener, called when the value returned from the above function changes + function(newValue, oldValue) { + if ( newValue !== oldValue ) { + // Only increment the counter if the value changed + scope.foodCounter = scope.foodCounter + 1; + } + } + ); + // No digest has been run so the counter will be zero + expect(scope.foodCounter).toEqual(0); + + // Run the digest but since food has not changed count will still be zero + scope.$digest(); + expect(scope.foodCounter).toEqual(0); + + // Update food and run digest. Now the counter will increment + food = 'cheeseburger'; + scope.$digest(); + expect(scope.foodCounter).toEqual(1); + + * ``` * - * @description - * Listens on events of a given type. See {@link ng.$rootScope.Scope#$emit $emit} for - * discussion of event life cycle. * - * The event listener function format is: `function(event, args...)`. The `event` object - * passed into the listener has the following attributes: * - * - `targetScope` - `{Scope}`: the scope on which the event was `$emit`-ed or - * `$broadcast`-ed. - * - `currentScope` - `{Scope}`: the current scope which is handling the event. - * - `name` - `{string}`: name of the event. - * - `stopPropagation` - `{function=}`: calling `stopPropagation` function will cancel - * further event propagation (available only for events that were `$emit`-ed). - * - `preventDefault` - `{function}`: calling `preventDefault` sets `defaultPrevented` flag - * to true. - * - `defaultPrevented` - `{boolean}`: true if `preventDefault` was called. + * @param {(function()|string)} watchExpression Expression that is evaluated on each + * {@link ng.$rootScope.Scope#$digest $digest} cycle. A change in the return value triggers + * a call to the `listener`. * - * @param {string} name Event name to listen on. - * @param {function(event, ...args)} listener Function to call when the event is emitted. + * - `string`: Evaluated as {@link guide/expression expression} + * - `function(scope)`: called with current `scope` as a parameter. + * @param {function(newVal, oldVal, scope)} listener Callback called whenever the value + * of `watchExpression` changes. + * + * - `newVal` contains the current value of the `watchExpression` + * - `oldVal` contains the previous value of the `watchExpression` + * - `scope` refers to the current scope + * @param {boolean=} [objectEquality=false] Compare for object equality using {@link angular.equals} instead of + * comparing for reference equality. * @returns {function()} Returns a deregistration function for this listener. */ - $on: function(name, listener) { - var namedListeners = this.$$listeners[name]; - if (!namedListeners) { - this.$$listeners[name] = namedListeners = []; + $watch: function(watchExp, listener, objectEquality, prettyPrintExpression) { + var get = $parse(watchExp); + var fn = isFunction(listener) ? listener : noop; + + if (get.$$watchDelegate) { + return get.$$watchDelegate(this, fn, objectEquality, get, watchExp); } - namedListeners.push(listener); + var scope = this, + array = scope.$$watchers, + watcher = { + fn: fn, + last: initWatchVal, + get: get, + exp: prettyPrintExpression || watchExp, + eq: !!objectEquality + }; - var current = this; - do { - if (!current.$$listenerCount[name]) { - current.$$listenerCount[name] = 0; - } - current.$$listenerCount[name]++; - } while ((current = current.$parent)); + lastDirtyWatch = null; - var self = this; - return function() { - namedListeners[indexOf(namedListeners, listener)] = null; - decrementListenerCount(self, 1, name); + if (!array) { + array = scope.$$watchers = []; + array.$$digestWatchIndex = -1; + } + // we use unshift since we use a while loop in $digest for speed. + // the while loop reads in reverse order. + array.unshift(watcher); + array.$$digestWatchIndex++; + incrementWatchersCount(this, 1); + + return function deregisterWatch() { + var index = arrayRemove(array, watcher); + if (index >= 0) { + incrementWatchersCount(scope, -1); + if (index < array.$$digestWatchIndex) { + array.$$digestWatchIndex--; + } + } + lastDirtyWatch = null; }; }, - /** * @ngdoc method - * @name $rootScope.Scope#$emit - * @function + * @name $rootScope.Scope#$watchGroup + * @kind function * * @description - * Dispatches an event `name` upwards through the scope hierarchy notifying the - * registered {@link ng.$rootScope.Scope#$on} listeners. + * A variant of {@link ng.$rootScope.Scope#$watch $watch()} where it watches an array of `watchExpressions`. + * If any one expression in the collection changes the `listener` is executed. * - * The event life cycle starts at the scope on which `$emit` was called. All - * {@link ng.$rootScope.Scope#$on listeners} listening for `name` event on this scope get - * notified. Afterwards, the event traverses upwards toward the root scope and calls all - * registered listeners along the way. The event will stop propagating if one of the listeners - * cancels it. + * - The items in the `watchExpressions` array are observed via the standard `$watch` operation. Their return + * values are examined for changes on every call to `$digest`. + * - The `listener` is called whenever any expression in the `watchExpressions` array changes. * - * Any exception emitted from the {@link ng.$rootScope.Scope#$on listeners} will be passed - * onto the {@link ng.$exceptionHandler $exceptionHandler} service. + * `$watchGroup` is more performant than watching each expression individually, and should be + * used when the listener does not need to know which expression has changed. + * If the listener needs to know which expression has changed, + * {@link ng.$rootScope.Scope#$watch $watch()} or + * {@link ng.$rootScope.Scope#$watchCollection $watchCollection()} should be used. * - * @param {string} name Event name to emit. - * @param {...*} args Optional one or more arguments which will be passed onto the event listeners. - * @return {Object} Event object (see {@link ng.$rootScope.Scope#$on}). + * @param {Array.} watchExpressions Array of expressions that will be individually + * watched using {@link ng.$rootScope.Scope#$watch $watch()} + * + * @param {function(newValues, oldValues, scope)} listener Callback called whenever the return value of any + * expression in `watchExpressions` changes + * The `newValues` array contains the current values of the `watchExpressions`, with the indexes matching + * those of `watchExpression` + * and the `oldValues` array contains the previous values of the `watchExpressions`, with the indexes matching + * those of `watchExpression`. + * + * Note that `newValues` and `oldValues` reflect the differences in each **individual** + * expression, and not the difference of the values between each call of the listener. + * That means the difference between `newValues` and `oldValues` cannot be used to determine + * which expression has changed / remained stable: + * + * ```js + * + * $scope.$watchGroup(['v1', 'v2'], function(newValues, oldValues) { + * console.log(newValues, oldValues); + * }); + * + * // newValues, oldValues initially + * // [undefined, undefined], [undefined, undefined] + * + * $scope.v1 = 'a'; + * $scope.v2 = 'a'; + * + * // ['a', 'a'], [undefined, undefined] + * + * $scope.v2 = 'b' + * + * // v1 hasn't changed since it became `'a'`, therefore its oldValue is still `undefined` + * // ['a', 'b'], [undefined, 'a'] + * + * ``` + * + * The `scope` refers to the current scope. + * @returns {function()} Returns a de-registration function for all listeners. */ - $emit: function(name, args) { - var empty = [], - namedListeners, - scope = this, - stopPropagation = false, - event = { - name: name, - targetScope: scope, - stopPropagation: function() {stopPropagation = true;}, - preventDefault: function() { - event.defaultPrevented = true; - }, - defaultPrevented: false - }, - listenerArgs = concat([event], arguments, 1), - i, length; + $watchGroup: function(watchExpressions, listener) { + var oldValues = new Array(watchExpressions.length); + var newValues = new Array(watchExpressions.length); + var deregisterFns = []; + var self = this; + var changeReactionScheduled = false; + var firstRun = true; + + if (!watchExpressions.length) { + // No expressions means we call the listener ASAP + var shouldCall = true; + self.$evalAsync(function() { + if (shouldCall) listener(newValues, newValues, self); + }); + return function deregisterWatchGroup() { + shouldCall = false; + }; + } - do { - namedListeners = scope.$$listeners[name] || empty; - event.currentScope = scope; - for (i=0, length=namedListeners.length; i 1); + var changeDetected = 0; + var changeDetector = $parse(obj, $watchCollectionInterceptor); + var internalArray = []; + var internalObject = {}; + var initRun = true; + var oldLength = 0; + function $watchCollectionInterceptor(_value) { + newValue = _value; + var newLength, key, bothNaN, newItem, oldItem; - function beginPhase(phase) { - if ($rootScope.$$phase) { - throw $rootScopeMinErr('inprog', '{0} already in progress', $rootScope.$$phase); - } + // If the new value is undefined, then return undefined as the watch may be a one-time watch + if (isUndefined(newValue)) return; - $rootScope.$$phase = phase; - } + if (!isObject(newValue)) { // if primitive + if (oldValue !== newValue) { + oldValue = newValue; + changeDetected++; + } + } else if (isArrayLike(newValue)) { + if (oldValue !== internalArray) { + // we are transitioning from something which was not an array into array. + oldValue = internalArray; + oldLength = oldValue.length = 0; + changeDetected++; + } - function clearPhase() { - $rootScope.$$phase = null; - } + newLength = newValue.length; - function compileToFn(exp, name) { - var fn = $parse(exp); - assertArgFn(fn, name); - return fn; - } + if (oldLength !== newLength) { + // if lengths do not match we need to trigger change notification + changeDetected++; + oldValue.length = oldLength = newLength; + } + // copy the items to oldValue and look for changes. + for (var i = 0; i < newLength; i++) { + oldItem = oldValue[i]; + newItem = newValue[i]; - function decrementListenerCount(current, count, name) { - do { - current.$$listenerCount[name] -= count; + // eslint-disable-next-line no-self-compare + bothNaN = (oldItem !== oldItem) && (newItem !== newItem); + if (!bothNaN && (oldItem !== newItem)) { + changeDetected++; + oldValue[i] = newItem; + } + } + } else { + if (oldValue !== internalObject) { + // we are transitioning from something which was not an object into object. + oldValue = internalObject = {}; + oldLength = 0; + changeDetected++; + } + // copy the items to oldValue and look for changes. + newLength = 0; + for (key in newValue) { + if (hasOwnProperty.call(newValue, key)) { + newLength++; + newItem = newValue[key]; + oldItem = oldValue[key]; - if (current.$$listenerCount[name] === 0) { - delete current.$$listenerCount[name]; + if (key in oldValue) { + // eslint-disable-next-line no-self-compare + bothNaN = (oldItem !== oldItem) && (newItem !== newItem); + if (!bothNaN && (oldItem !== newItem)) { + changeDetected++; + oldValue[key] = newItem; + } + } else { + oldLength++; + oldValue[key] = newItem; + changeDetected++; + } + } + } + if (oldLength > newLength) { + // we used to have more keys, need to find them and destroy them. + changeDetected++; + for (key in oldValue) { + if (!hasOwnProperty.call(newValue, key)) { + oldLength--; + delete oldValue[key]; + } + } + } + } + return changeDetected; } - } while ((current = current.$parent)); - } - /** - * function used as an initial value for watchers. - * because it's unique we can easily tell it apart from other values - */ - function initWatchVal() {} - }]; - } + function $watchCollectionAction() { + if (initRun) { + initRun = false; + listener(newValue, newValue, self); + } else { + listener(newValue, veryOldValue, self); + } - /** - * @description - * Private service to sanitize uris for links and images. Used by $compile and $sanitize. - */ - function $$SanitizeUriProvider() { - var aHrefSanitizationWhitelist = /^\s*(https?|ftp|mailto|tel|file):/, - imgSrcSanitizationWhitelist = /^\s*(https?|ftp|file):|data:image\//; + // make a copy for the next time a collection is changed + if (trackVeryOldValue) { + if (!isObject(newValue)) { + //primitive + veryOldValue = newValue; + } else if (isArrayLike(newValue)) { + veryOldValue = new Array(newValue.length); + for (var i = 0; i < newValue.length; i++) { + veryOldValue[i] = newValue[i]; + } + } else { // if object + veryOldValue = {}; + for (var key in newValue) { + if (hasOwnProperty.call(newValue, key)) { + veryOldValue[key] = newValue[key]; + } + } + } + } + } - /** - * @description - * Retrieves or overrides the default regular expression that is used for whitelisting of safe - * urls during a[href] sanitization. - * - * The sanitization is a security measure aimed at prevent XSS attacks via html links. - * - * Any url about to be assigned to a[href] via data-binding is first normalized and turned into - * an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist` - * regular expression. If a match is found, the original url is written into the dom. Otherwise, - * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. - * - * @param {RegExp=} regexp New regexp to whitelist urls with. - * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for - * chaining otherwise. - */ - this.aHrefSanitizationWhitelist = function(regexp) { - if (isDefined(regexp)) { - aHrefSanitizationWhitelist = regexp; - return this; - } - return aHrefSanitizationWhitelist; - }; + return this.$watch(changeDetector, $watchCollectionAction); + }, + /** + * @ngdoc method + * @name $rootScope.Scope#$digest + * @kind function + * + * @description + * Processes all of the {@link ng.$rootScope.Scope#$watch watchers} of the current scope and + * its children. Because a {@link ng.$rootScope.Scope#$watch watcher}'s listener can change + * the model, the `$digest()` keeps calling the {@link ng.$rootScope.Scope#$watch watchers} + * until no more listeners are firing. This means that it is possible to get into an infinite + * loop. This function will throw `'Maximum iteration limit exceeded.'` if the number of + * iterations exceeds 10. + * + * Usually, you don't call `$digest()` directly in + * {@link ng.directive:ngController controllers} or in + * {@link ng.$compileProvider#directive directives}. + * Instead, you should call {@link ng.$rootScope.Scope#$apply $apply()} (typically from within + * a {@link ng.$compileProvider#directive directive}), which will force a `$digest()`. + * + * If you want to be notified whenever `$digest()` is called, + * you can register a `watchExpression` function with + * {@link ng.$rootScope.Scope#$watch $watch()} with no `listener`. + * + * In unit tests, you may need to call `$digest()` to simulate the scope life cycle. + * + * @example + * ```js + var scope = ...; + scope.name = 'misko'; + scope.counter = 0; - /** - * @description - * Retrieves or overrides the default regular expression that is used for whitelisting of safe - * urls during img[src] sanitization. - * - * The sanitization is a security measure aimed at prevent XSS attacks via html links. - * - * Any url about to be assigned to img[src] via data-binding is first normalized and turned into - * an absolute url. Afterwards, the url is matched against the `imgSrcSanitizationWhitelist` - * regular expression. If a match is found, the original url is written into the dom. Otherwise, - * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. - * - * @param {RegExp=} regexp New regexp to whitelist urls with. - * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for - * chaining otherwise. - */ - this.imgSrcSanitizationWhitelist = function(regexp) { - if (isDefined(regexp)) { - imgSrcSanitizationWhitelist = regexp; - return this; - } - return imgSrcSanitizationWhitelist; - }; + expect(scope.counter).toEqual(0); + scope.$watch('name', function(newValue, oldValue) { + scope.counter = scope.counter + 1; + }); + expect(scope.counter).toEqual(0); - this.$get = function() { - return function sanitizeUri(uri, isImage) { - var regex = isImage ? imgSrcSanitizationWhitelist : aHrefSanitizationWhitelist; - var normalizedVal; - // NOTE: urlResolve() doesn't support IE < 8 so we don't sanitize for that case. - if (!msie || msie >= 8 ) { - normalizedVal = urlResolve(uri).href; - if (normalizedVal !== '' && !normalizedVal.match(regex)) { - return 'unsafe:'+normalizedVal; - } - } - return uri; - }; - }; - } + scope.$digest(); + // the listener is always called during the first $digest loop after it was registered + expect(scope.counter).toEqual(1); - var $sceMinErr = minErr('$sce'); - - var SCE_CONTEXTS = { - HTML: 'html', - CSS: 'css', - URL: 'url', - // RESOURCE_URL is a subtype of URL used in contexts where a privileged resource is sourced from a - // url. (e.g. ng-include, script src, templateUrl) - RESOURCE_URL: 'resourceUrl', - JS: 'js' - }; - -// Helper functions follow. - -// Copied from: -// http://docs.closure-library.googlecode.com/git/closure_goog_string_string.js.source.html#line962 -// Prereq: s is a string. - function escapeForRegexp(s) { - return s.replace(/([-()\[\]{}+?*.$\^|,:# -1) { - throw $sceMinErr('iwcard', - 'Illegal sequence *** in string matcher. String: {0}', matcher); - } - matcher = escapeForRegexp(matcher). - replace('\\*\\*', '.*'). - replace('\\*', '[^:/.?&;]*'); - return new RegExp('^' + matcher + '$'); - } else if (isRegExp(matcher)) { - // The only other type of matcher allowed is a Regexp. - // Match entire URL / disallow partial matches. - // Flags are reset (i.e. no global, ignoreCase or multiline) - return new RegExp('^' + matcher.source + '$'); - } else { - throw $sceMinErr('imatcher', - 'Matchers may only be "self", string patterns or RegExp objects'); - } - } + scope.$digest(); + // but now it will not be called unless the value changes + expect(scope.counter).toEqual(1); + scope.name = 'adam'; + scope.$digest(); + expect(scope.counter).toEqual(2); + * ``` + * + */ + $digest: function() { + var watch, value, last, fn, get, + watchers, + dirty, ttl = TTL, + next, current, target = this, + watchLog = [], + logIdx, asyncTask; - function adjustMatchers(matchers) { - var adjustedMatchers = []; - if (isDefined(matchers)) { - forEach(matchers, function(matcher) { - adjustedMatchers.push(adjustMatcher(matcher)); - }); - } - return adjustedMatchers; - } + beginPhase('$digest'); + // Check for changes to browser url that happened in sync before the call to $digest + $browser.$$checkUrlChange(); + + if (this === $rootScope && applyAsyncId !== null) { + // If this is the root scope, and $applyAsync has scheduled a deferred $apply(), then + // cancel the scheduled $apply and flush the queue of expressions to be evaluated. + $browser.defer.cancel(applyAsyncId); + flushApplyAsync(); + } + lastDirtyWatch = null; - /** - * @ngdoc service - * @name $sceDelegate - * @function - * - * @description - * - * `$sceDelegate` is a service that is used by the `$sce` service to provide {@link ng.$sce Strict - * Contextual Escaping (SCE)} services to AngularJS. - * - * Typically, you would configure or override the {@link ng.$sceDelegate $sceDelegate} instead of - * the `$sce` service to customize the way Strict Contextual Escaping works in AngularJS. This is - * because, while the `$sce` provides numerous shorthand methods, etc., you really only need to - * override 3 core functions (`trustAs`, `getTrusted` and `valueOf`) to replace the way things - * work because `$sce` delegates to `$sceDelegate` for these operations. - * - * Refer {@link ng.$sceDelegateProvider $sceDelegateProvider} to configure this service. - * - * The default instance of `$sceDelegate` should work out of the box with little pain. While you - * can override it completely to change the behavior of `$sce`, the common case would - * involve configuring the {@link ng.$sceDelegateProvider $sceDelegateProvider} instead by setting - * your own whitelists and blacklists for trusting URLs used for loading AngularJS resources such as - * templates. Refer {@link ng.$sceDelegateProvider#resourceUrlWhitelist - * $sceDelegateProvider.resourceUrlWhitelist} and {@link - * ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist} - */ + do { // "while dirty" loop + dirty = false; + current = target; - /** - * @ngdoc provider - * @name $sceDelegateProvider - * @description - * - * The `$sceDelegateProvider` provider allows developers to configure the {@link ng.$sceDelegate - * $sceDelegate} service. This allows one to get/set the whitelists and blacklists used to ensure - * that the URLs used for sourcing Angular templates are safe. Refer {@link - * ng.$sceDelegateProvider#resourceUrlWhitelist $sceDelegateProvider.resourceUrlWhitelist} and - * {@link ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist} - * - * For the general details about this service in Angular, read the main page for {@link ng.$sce - * Strict Contextual Escaping (SCE)}. - * - * **Example**: Consider the following case.
    - * - * - your app is hosted at url `http://myapp.example.com/` - * - but some of your templates are hosted on other domains you control such as - * `http://srv01.assets.example.com/`, `http://srv02.assets.example.com/`, etc. - * - and you have an open redirect at `http://myapp.example.com/clickThru?...`. - * - * Here is what a secure configuration for this scenario might look like: - * - *
    -     *    angular.module('myApp', []).config(function($sceDelegateProvider) {
    - *      $sceDelegateProvider.resourceUrlWhitelist([
    - *        // Allow same origin resource loads.
    - *        'self',
    - *        // Allow loading from our assets domain.  Notice the difference between * and **.
    - *        'http://srv*.assets.example.com/**']);
    - *
    - *      // The blacklist overrides the whitelist so the open redirect here is blocked.
    - *      $sceDelegateProvider.resourceUrlBlacklist([
    - *        'http://myapp.example.com/clickThru**']);
    - *      });
    -     * 
    - */ + // It's safe for asyncQueuePosition to be a local variable here because this loop can't + // be reentered recursively. Calling $digest from a function passed to $evalAsync would + // lead to a '$digest already in progress' error. + for (var asyncQueuePosition = 0; asyncQueuePosition < asyncQueue.length; asyncQueuePosition++) { + try { + asyncTask = asyncQueue[asyncQueuePosition]; + fn = asyncTask.fn; + fn(asyncTask.scope, asyncTask.locals); + } catch (e) { + $exceptionHandler(e); + } + lastDirtyWatch = null; + } + asyncQueue.length = 0; - function $SceDelegateProvider() { - this.SCE_CONTEXTS = SCE_CONTEXTS; + traverseScopesLoop: + do { // "traverse the scopes" loop + if ((watchers = current.$$watchers)) { + // process our watches + watchers.$$digestWatchIndex = watchers.length; + while (watchers.$$digestWatchIndex--) { + try { + watch = watchers[watchers.$$digestWatchIndex]; + // Most common watches are on primitives, in which case we can short + // circuit it with === operator, only when === fails do we use .equals + if (watch) { + get = watch.get; + if ((value = get(current)) !== (last = watch.last) && + !(watch.eq + ? equals(value, last) + : (isNumberNaN(value) && isNumberNaN(last)))) { + dirty = true; + lastDirtyWatch = watch; + watch.last = watch.eq ? copy(value, null) : value; + fn = watch.fn; + fn(value, ((last === initWatchVal) ? value : last), current); + if (ttl < 5) { + logIdx = 4 - ttl; + if (!watchLog[logIdx]) watchLog[logIdx] = []; + watchLog[logIdx].push({ + msg: isFunction(watch.exp) ? 'fn: ' + (watch.exp.name || watch.exp.toString()) : watch.exp, + newVal: value, + oldVal: last + }); + } + } else if (watch === lastDirtyWatch) { + // If the most recently dirty watcher is now clean, short circuit since the remaining watchers + // have already been tested. + dirty = false; + break traverseScopesLoop; + } + } + } catch (e) { + $exceptionHandler(e); + } + } + } - // Resource URLs can also be trusted by policy. - var resourceUrlWhitelist = ['self'], - resourceUrlBlacklist = []; + // Insanity Warning: scope depth-first traversal + // yes, this code is a bit crazy, but it works and we have tests to prove it! + // this piece should be kept in sync with the traversal in $broadcast + if (!(next = ((current.$$watchersCount && current.$$childHead) || + (current !== target && current.$$nextSibling)))) { + while (current !== target && !(next = current.$$nextSibling)) { + current = current.$parent; + } + } + } while ((current = next)); - /** - * @ngdoc method - * @name $sceDelegateProvider#resourceUrlWhitelist - * @function - * - * @param {Array=} whitelist When provided, replaces the resourceUrlWhitelist with the value - * provided. This must be an array or null. A snapshot of this array is used so further - * changes to the array are ignored. - * - * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items - * allowed in this array. - * - * Note: **an empty whitelist array will block all URLs**! - * - * @return {Array} the currently set whitelist array. - * - * The **default value** when no whitelist has been explicitly set is `['self']` allowing only - * same origin resource requests. - * - * @description - * Sets/Gets the whitelist of trusted resource URLs. - */ - this.resourceUrlWhitelist = function (value) { - if (arguments.length) { - resourceUrlWhitelist = adjustMatchers(value); - } - return resourceUrlWhitelist; - }; + // `break traverseScopesLoop;` takes us to here - /** - * @ngdoc method - * @name $sceDelegateProvider#resourceUrlBlacklist - * @function - * - * @param {Array=} blacklist When provided, replaces the resourceUrlBlacklist with the value - * provided. This must be an array or null. A snapshot of this array is used so further - * changes to the array are ignored. - * - * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items - * allowed in this array. - * - * The typical usage for the blacklist is to **block - * [open redirects](http://cwe.mitre.org/data/definitions/601.html)** served by your domain as - * these would otherwise be trusted but actually return content from the redirected domain. - * - * Finally, **the blacklist overrides the whitelist** and has the final say. - * - * @return {Array} the currently set blacklist array. - * - * The **default value** when no whitelist has been explicitly set is the empty array (i.e. there - * is no blacklist.) - * - * @description - * Sets/Gets the blacklist of trusted resource URLs. - */ + if ((dirty || asyncQueue.length) && !(ttl--)) { + clearPhase(); + throw $rootScopeMinErr('infdig', + '{0} $digest() iterations reached. Aborting!\n' + + 'Watchers fired in the last 5 iterations: {1}', + TTL, watchLog); + } - this.resourceUrlBlacklist = function (value) { - if (arguments.length) { - resourceUrlBlacklist = adjustMatchers(value); - } - return resourceUrlBlacklist; - }; + } while (dirty || asyncQueue.length); - this.$get = ['$injector', function($injector) { + clearPhase(); - var htmlSanitizer = function htmlSanitizer(html) { - throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.'); - }; + // postDigestQueuePosition isn't local here because this loop can be reentered recursively. + while (postDigestQueuePosition < postDigestQueue.length) { + try { + postDigestQueue[postDigestQueuePosition++](); + } catch (e) { + $exceptionHandler(e); + } + } + postDigestQueue.length = postDigestQueuePosition = 0; - if ($injector.has('$sanitize')) { - htmlSanitizer = $injector.get('$sanitize'); - } + // Check for changes to browser url that happened during the $digest + // (for which no event is fired; e.g. via `history.pushState()`) + $browser.$$checkUrlChange(); + }, - function matchUrl(matcher, parsedUrl) { - if (matcher === 'self') { - return urlIsSameOrigin(parsedUrl); - } else { - // definitely a regex. See adjustMatchers() - return !!matcher.exec(parsedUrl.href); - } - } + /** + * @ngdoc event + * @name $rootScope.Scope#$destroy + * @eventType broadcast on scope being destroyed + * + * @description + * Broadcasted when a scope and its children are being destroyed. + * + * Note that, in AngularJS, there is also a `$destroy` jQuery event, which can be used to + * clean up DOM bindings before an element is removed from the DOM. + */ - function isResourceUrlAllowedByPolicy(url) { - var parsedUrl = urlResolve(url.toString()); - var i, n, allowed = false; - // Ensure that at least one item from the whitelist allows this url. - for (i = 0, n = resourceUrlWhitelist.length; i < n; i++) { - if (matchUrl(resourceUrlWhitelist[i], parsedUrl)) { - allowed = true; - break; - } - } - if (allowed) { - // Ensure that no item from the blacklist blocked this url. - for (i = 0, n = resourceUrlBlacklist.length; i < n; i++) { - if (matchUrl(resourceUrlBlacklist[i], parsedUrl)) { - allowed = false; - break; - } - } - } - return allowed; - } + /** + * @ngdoc method + * @name $rootScope.Scope#$destroy + * @kind function + * + * @description + * Removes the current scope (and all of its children) from the parent scope. Removal implies + * that calls to {@link ng.$rootScope.Scope#$digest $digest()} will no longer + * propagate to the current scope and its children. Removal also implies that the current + * scope is eligible for garbage collection. + * + * The `$destroy()` is usually used by directives such as + * {@link ng.directive:ngRepeat ngRepeat} for managing the + * unrolling of the loop. + * + * Just before a scope is destroyed, a `$destroy` event is broadcasted on this scope. + * Application code can register a `$destroy` event handler that will give it a chance to + * perform any necessary cleanup. + * + * Note that, in AngularJS, there is also a `$destroy` jQuery event, which can be used to + * clean up DOM bindings before an element is removed from the DOM. + */ + $destroy: function() { + // We can't destroy a scope that has been already destroyed. + if (this.$$destroyed) return; + var parent = this.$parent; - function generateHolderType(Base) { - var holderType = function TrustedValueHolderType(trustedValue) { - this.$$unwrapTrustedValue = function() { - return trustedValue; - }; - }; - if (Base) { - holderType.prototype = new Base(); - } - holderType.prototype.valueOf = function sceValueOf() { - return this.$$unwrapTrustedValue(); - }; - holderType.prototype.toString = function sceToString() { - return this.$$unwrapTrustedValue().toString(); - }; - return holderType; - } + this.$broadcast('$destroy'); + this.$$destroyed = true; - var trustedValueHolderBase = generateHolderType(), - byType = {}; + if (this === $rootScope) { + //Remove handlers attached to window when $rootScope is removed + $browser.$$applicationDestroyed(); + } - byType[SCE_CONTEXTS.HTML] = generateHolderType(trustedValueHolderBase); - byType[SCE_CONTEXTS.CSS] = generateHolderType(trustedValueHolderBase); - byType[SCE_CONTEXTS.URL] = generateHolderType(trustedValueHolderBase); - byType[SCE_CONTEXTS.JS] = generateHolderType(trustedValueHolderBase); - byType[SCE_CONTEXTS.RESOURCE_URL] = generateHolderType(byType[SCE_CONTEXTS.URL]); + incrementWatchersCount(this, -this.$$watchersCount); + for (var eventName in this.$$listenerCount) { + decrementListenerCount(this, this.$$listenerCount[eventName], eventName); + } - /** - * @ngdoc method - * @name $sceDelegate#trustAs - * - * @description - * Returns an object that is trusted by angular for use in specified strict - * contextual escaping contexts (such as ng-bind-html, ng-include, any src - * attribute interpolation, any dom event binding attribute interpolation - * such as for onclick, etc.) that uses the provided value. - * See {@link ng.$sce $sce} for enabling strict contextual escaping. - * - * @param {string} type The kind of context in which this value is safe for use. e.g. url, - * resourceUrl, html, js and css. - * @param {*} value The value that that should be considered trusted/safe. - * @returns {*} A value that can be used to stand in for the provided `value` in places - * where Angular expects a $sce.trustAs() return value. - */ - function trustAs(type, trustedValue) { - var Constructor = (byType.hasOwnProperty(type) ? byType[type] : null); - if (!Constructor) { - throw $sceMinErr('icontext', - 'Attempted to trust a value in invalid context. Context: {0}; Value: {1}', - type, trustedValue); - } - if (trustedValue === null || trustedValue === undefined || trustedValue === '') { - return trustedValue; - } - // All the current contexts in SCE_CONTEXTS happen to be strings. In order to avoid trusting - // mutable objects, we ensure here that the value passed in is actually a string. - if (typeof trustedValue !== 'string') { - throw $sceMinErr('itype', - 'Attempted to trust a non-string value in a content requiring a string: Context: {0}', - type); - } - return new Constructor(trustedValue); - } + // sever all the references to parent scopes (after this cleanup, the current scope should + // not be retained by any of our references and should be eligible for garbage collection) + if (parent && parent.$$childHead === this) parent.$$childHead = this.$$nextSibling; + if (parent && parent.$$childTail === this) parent.$$childTail = this.$$prevSibling; + if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling; + if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling; - /** - * @ngdoc method - * @name $sceDelegate#valueOf - * - * @description - * If the passed parameter had been returned by a prior call to {@link ng.$sceDelegate#trustAs - * `$sceDelegate.trustAs`}, returns the value that had been passed to {@link - * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. - * - * If the passed parameter is not a value that had been returned by {@link - * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}, returns it as-is. - * - * @param {*} value The result of a prior {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`} - * call or anything else. - * @returns {*} The `value` that was originally provided to {@link ng.$sceDelegate#trustAs - * `$sceDelegate.trustAs`} if `value` is the result of such a call. Otherwise, returns - * `value` unchanged. - */ - function valueOf(maybeTrusted) { - if (maybeTrusted instanceof trustedValueHolderBase) { - return maybeTrusted.$$unwrapTrustedValue(); - } else { - return maybeTrusted; - } - } + // Disable listeners, watchers and apply/digest methods + this.$destroy = this.$digest = this.$apply = this.$evalAsync = this.$applyAsync = noop; + this.$on = this.$watch = this.$watchGroup = function() { return noop; }; + this.$$listeners = {}; - /** - * @ngdoc method - * @name $sceDelegate#getTrusted - * - * @description - * Takes the result of a {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`} call and - * returns the originally supplied value if the queried context type is a supertype of the - * created type. If this condition isn't satisfied, throws an exception. - * - * @param {string} type The kind of context in which this value is to be used. - * @param {*} maybeTrusted The result of a prior {@link ng.$sceDelegate#trustAs - * `$sceDelegate.trustAs`} call. - * @returns {*} The value the was originally provided to {@link ng.$sceDelegate#trustAs - * `$sceDelegate.trustAs`} if valid in this context. Otherwise, throws an exception. - */ - function getTrusted(type, maybeTrusted) { - if (maybeTrusted === null || maybeTrusted === undefined || maybeTrusted === '') { - return maybeTrusted; - } - var constructor = (byType.hasOwnProperty(type) ? byType[type] : null); - if (constructor && maybeTrusted instanceof constructor) { - return maybeTrusted.$$unwrapTrustedValue(); - } - // If we get here, then we may only take one of two actions. - // 1. sanitize the value for the requested type, or - // 2. throw an exception. - if (type === SCE_CONTEXTS.RESOURCE_URL) { - if (isResourceUrlAllowedByPolicy(maybeTrusted)) { - return maybeTrusted; - } else { - throw $sceMinErr('insecurl', - 'Blocked loading resource from url not allowed by $sceDelegate policy. URL: {0}', - maybeTrusted.toString()); - } - } else if (type === SCE_CONTEXTS.HTML) { - return htmlSanitizer(maybeTrusted); - } - throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.'); - } + // Disconnect the next sibling to prevent `cleanUpScope` destroying those too + this.$$nextSibling = null; + cleanUpScope(this); + }, - return { trustAs: trustAs, - getTrusted: getTrusted, - valueOf: valueOf }; - }]; - } + /** + * @ngdoc method + * @name $rootScope.Scope#$eval + * @kind function + * + * @description + * Executes the `expression` on the current scope and returns the result. Any exceptions in + * the expression are propagated (uncaught). This is useful when evaluating AngularJS + * expressions. + * + * @example + * ```js + var scope = ng.$rootScope.Scope(); + scope.a = 1; + scope.b = 2; + expect(scope.$eval('a+b')).toEqual(3); + expect(scope.$eval(function(scope){ return scope.a + scope.b; })).toEqual(3); + * ``` + * + * @param {(string|function())=} expression An AngularJS expression to be executed. + * + * - `string`: execute using the rules as defined in {@link guide/expression expression}. + * - `function(scope)`: execute the function with the current `scope` parameter. + * + * @param {(object)=} locals Local variables object, useful for overriding values in scope. + * @returns {*} The result of evaluating the expression. + */ + $eval: function(expr, locals) { + return $parse(expr)(this, locals); + }, - /** - * @ngdoc provider - * @name $sceProvider - * @description - * - * The $sceProvider provider allows developers to configure the {@link ng.$sce $sce} service. - * - enable/disable Strict Contextual Escaping (SCE) in a module - * - override the default implementation with a custom delegate - * - * Read more about {@link ng.$sce Strict Contextual Escaping (SCE)}. - */ + /** + * @ngdoc method + * @name $rootScope.Scope#$evalAsync + * @kind function + * + * @description + * Executes the expression on the current scope at a later point in time. + * + * The `$evalAsync` makes no guarantees as to when the `expression` will be executed, only + * that: + * + * - it will execute after the function that scheduled the evaluation (preferably before DOM + * rendering). + * - at least one {@link ng.$rootScope.Scope#$digest $digest cycle} will be performed after + * `expression` execution. + * + * Any exceptions from the execution of the expression are forwarded to the + * {@link ng.$exceptionHandler $exceptionHandler} service. + * + * __Note:__ if this function is called outside of a `$digest` cycle, a new `$digest` cycle + * will be scheduled. However, it is encouraged to always call code that changes the model + * from within an `$apply` call. That includes code evaluated via `$evalAsync`. + * + * @param {(string|function())=} expression An AngularJS expression to be executed. + * + * - `string`: execute using the rules as defined in {@link guide/expression expression}. + * - `function(scope)`: execute the function with the current `scope` parameter. + * + * @param {(object)=} locals Local variables object, useful for overriding values in scope. + */ + $evalAsync: function(expr, locals) { + // if we are outside of an $digest loop and this is the first time we are scheduling async + // task also schedule async auto-flush + if (!$rootScope.$$phase && !asyncQueue.length) { + $browser.defer(function() { + if (asyncQueue.length) { + $rootScope.$digest(); + } + }); + } - /* jshint maxlen: false*/ + asyncQueue.push({scope: this, fn: $parse(expr), locals: locals}); + }, - /** - * @ngdoc service - * @name $sce - * @function - * - * @description - * - * `$sce` is a service that provides Strict Contextual Escaping services to AngularJS. - * - * # Strict Contextual Escaping - * - * Strict Contextual Escaping (SCE) is a mode in which AngularJS requires bindings in certain - * contexts to result in a value that is marked as safe to use for that context. One example of - * such a context is binding arbitrary html controlled by the user via `ng-bind-html`. We refer - * to these contexts as privileged or SCE contexts. - * - * As of version 1.2, Angular ships with SCE enabled by default. - * - * Note: When enabled (the default), IE8 in quirks mode is not supported. In this mode, IE8 allows - * one to execute arbitrary javascript by the use of the expression() syntax. Refer - * to learn more about them. - * You can ensure your document is in standards mode and not quirks mode by adding `` - * to the top of your HTML document. - * - * SCE assists in writing code in way that (a) is secure by default and (b) makes auditing for - * security vulnerabilities such as XSS, clickjacking, etc. a lot easier. - * - * Here's an example of a binding in a privileged context: - * - *
    -     *     
    -     *     
    - *
    - * - * Notice that `ng-bind-html` is bound to `userHtml` controlled by the user. With SCE - * disabled, this application allows the user to render arbitrary HTML into the DIV. - * In a more realistic example, one may be rendering user comments, blog articles, etc. via - * bindings. (HTML is just one example of a context where rendering user controlled input creates - * security vulnerabilities.) - * - * For the case of HTML, you might use a library, either on the client side, or on the server side, - * to sanitize unsafe HTML before binding to the value and rendering it in the document. - * - * How would you ensure that every place that used these types of bindings was bound to a value that - * was sanitized by your library (or returned as safe for rendering by your server?) How can you - * ensure that you didn't accidentally delete the line that sanitized the value, or renamed some - * properties/fields and forgot to update the binding to the sanitized value? + $$postDigest: function(fn) { + postDigestQueue.push(fn); + }, + + /** + * @ngdoc method + * @name $rootScope.Scope#$apply + * @kind function + * + * @description + * `$apply()` is used to execute an expression in AngularJS from outside of the AngularJS + * framework. (For example from browser DOM events, setTimeout, XHR or third party libraries). + * Because we are calling into the AngularJS framework we need to perform proper scope life + * cycle of {@link ng.$exceptionHandler exception handling}, + * {@link ng.$rootScope.Scope#$digest executing watches}. + * + * **Life cycle: Pseudo-Code of `$apply()`** + * + * ```js + function $apply(expr) { + try { + return $eval(expr); + } catch (e) { + $exceptionHandler(e); + } finally { + $root.$digest(); + } + } + * ``` + * + * + * Scope's `$apply()` method transitions through the following stages: + * + * 1. The {@link guide/expression expression} is executed using the + * {@link ng.$rootScope.Scope#$eval $eval()} method. + * 2. Any exceptions from the execution of the expression are forwarded to the + * {@link ng.$exceptionHandler $exceptionHandler} service. + * 3. The {@link ng.$rootScope.Scope#$watch watch} listeners are fired immediately after the + * expression was executed using the {@link ng.$rootScope.Scope#$digest $digest()} method. + * + * + * @param {(string|function())=} exp An AngularJS expression to be executed. + * + * - `string`: execute using the rules as defined in {@link guide/expression expression}. + * - `function(scope)`: execute the function with current `scope` parameter. + * + * @returns {*} The result of evaluating the expression. + */ + $apply: function(expr) { + try { + beginPhase('$apply'); + try { + return this.$eval(expr); + } finally { + clearPhase(); + } + } catch (e) { + $exceptionHandler(e); + } finally { + try { + $rootScope.$digest(); + } catch (e) { + $exceptionHandler(e); + // eslint-disable-next-line no-unsafe-finally + throw e; + } + } + }, + + /** + * @ngdoc method + * @name $rootScope.Scope#$applyAsync + * @kind function + * + * @description + * Schedule the invocation of $apply to occur at a later time. The actual time difference + * varies across browsers, but is typically around ~10 milliseconds. + * + * This can be used to queue up multiple expressions which need to be evaluated in the same + * digest. + * + * @param {(string|function())=} exp An AngularJS expression to be executed. + * + * - `string`: execute using the rules as defined in {@link guide/expression expression}. + * - `function(scope)`: execute the function with current `scope` parameter. + */ + $applyAsync: function(expr) { + var scope = this; + if (expr) { + applyAsyncQueue.push($applyAsyncExpression); + } + expr = $parse(expr); + scheduleApplyAsync(); + + function $applyAsyncExpression() { + scope.$eval(expr); + } + }, + + /** + * @ngdoc method + * @name $rootScope.Scope#$on + * @kind function + * + * @description + * Listens on events of a given type. See {@link ng.$rootScope.Scope#$emit $emit} for + * discussion of event life cycle. + * + * The event listener function format is: `function(event, args...)`. The `event` object + * passed into the listener has the following attributes: + * + * - `targetScope` - `{Scope}`: the scope on which the event was `$emit`-ed or + * `$broadcast`-ed. + * - `currentScope` - `{Scope}`: the scope that is currently handling the event. Once the + * event propagates through the scope hierarchy, this property is set to null. + * - `name` - `{string}`: name of the event. + * - `stopPropagation` - `{function=}`: calling `stopPropagation` function will cancel + * further event propagation (available only for events that were `$emit`-ed). + * - `preventDefault` - `{function}`: calling `preventDefault` sets `defaultPrevented` flag + * to true. + * - `defaultPrevented` - `{boolean}`: true if `preventDefault` was called. + * + * @param {string} name Event name to listen on. + * @param {function(event, ...args)} listener Function to call when the event is emitted. + * @returns {function()} Returns a deregistration function for this listener. + */ + $on: function(name, listener) { + var namedListeners = this.$$listeners[name]; + if (!namedListeners) { + this.$$listeners[name] = namedListeners = []; + } + namedListeners.push(listener); + + var current = this; + do { + if (!current.$$listenerCount[name]) { + current.$$listenerCount[name] = 0; + } + current.$$listenerCount[name]++; + } while ((current = current.$parent)); + + var self = this; + return function() { + var indexOfListener = namedListeners.indexOf(listener); + if (indexOfListener !== -1) { + // Use delete in the hope of the browser deallocating the memory for the array entry, + // while not shifting the array indexes of other listeners. + // See issue https://github.com/angular/angular.js/issues/16135 + delete namedListeners[indexOfListener]; + decrementListenerCount(self, 1, name); + } + }; + }, + + + /** + * @ngdoc method + * @name $rootScope.Scope#$emit + * @kind function + * + * @description + * Dispatches an event `name` upwards through the scope hierarchy notifying the + * registered {@link ng.$rootScope.Scope#$on} listeners. + * + * The event life cycle starts at the scope on which `$emit` was called. All + * {@link ng.$rootScope.Scope#$on listeners} listening for `name` event on this scope get + * notified. Afterwards, the event traverses upwards toward the root scope and calls all + * registered listeners along the way. The event will stop propagating if one of the listeners + * cancels it. + * + * Any exception emitted from the {@link ng.$rootScope.Scope#$on listeners} will be passed + * onto the {@link ng.$exceptionHandler $exceptionHandler} service. + * + * @param {string} name Event name to emit. + * @param {...*} args Optional one or more arguments which will be passed onto the event listeners. + * @return {Object} Event object (see {@link ng.$rootScope.Scope#$on}). + */ + $emit: function(name, args) { + var empty = [], + namedListeners, + scope = this, + stopPropagation = false, + event = { + name: name, + targetScope: scope, + stopPropagation: function() {stopPropagation = true;}, + preventDefault: function() { + event.defaultPrevented = true; + }, + defaultPrevented: false + }, + listenerArgs = concat([event], arguments, 1), + i, length; + + do { + namedListeners = scope.$$listeners[name] || empty; + event.currentScope = scope; + for (i = 0, length = namedListeners.length; i < length; i++) { + + // if listeners were deregistered, defragment the array + if (!namedListeners[i]) { + namedListeners.splice(i, 1); + i--; + length--; + continue; + } + try { + //allow all listeners attached to the current scope to run + namedListeners[i].apply(null, listenerArgs); + } catch (e) { + $exceptionHandler(e); + } + } + //if any listener on the current scope stops propagation, prevent bubbling + if (stopPropagation) { + break; + } + //traverse upwards + scope = scope.$parent; + } while (scope); + + event.currentScope = null; + + return event; + }, + + + /** + * @ngdoc method + * @name $rootScope.Scope#$broadcast + * @kind function + * + * @description + * Dispatches an event `name` downwards to all child scopes (and their children) notifying the + * registered {@link ng.$rootScope.Scope#$on} listeners. + * + * The event life cycle starts at the scope on which `$broadcast` was called. All + * {@link ng.$rootScope.Scope#$on listeners} listening for `name` event on this scope get + * notified. Afterwards, the event propagates to all direct and indirect scopes of the current + * scope and calls all registered listeners along the way. The event cannot be canceled. + * + * Any exception emitted from the {@link ng.$rootScope.Scope#$on listeners} will be passed + * onto the {@link ng.$exceptionHandler $exceptionHandler} service. + * + * @param {string} name Event name to broadcast. + * @param {...*} args Optional one or more arguments which will be passed onto the event listeners. + * @return {Object} Event object, see {@link ng.$rootScope.Scope#$on} + */ + $broadcast: function(name, args) { + var target = this, + current = target, + next = target, + event = { + name: name, + targetScope: target, + preventDefault: function() { + event.defaultPrevented = true; + }, + defaultPrevented: false + }; + + if (!target.$$listenerCount[name]) return event; + + var listenerArgs = concat([event], arguments, 1), + listeners, i, length; + + //down while you can, then up and next sibling or up and next sibling until back at root + while ((current = next)) { + event.currentScope = current; + listeners = current.$$listeners[name] || []; + for (i = 0, length = listeners.length; i < length; i++) { + // if listeners were deregistered, defragment the array + if (!listeners[i]) { + listeners.splice(i, 1); + i--; + length--; + continue; + } + + try { + listeners[i].apply(null, listenerArgs); + } catch (e) { + $exceptionHandler(e); + } + } + + // Insanity Warning: scope depth-first traversal + // yes, this code is a bit crazy, but it works and we have tests to prove it! + // this piece should be kept in sync with the traversal in $digest + // (though it differs due to having the extra check for $$listenerCount) + if (!(next = ((current.$$listenerCount[name] && current.$$childHead) || + (current !== target && current.$$nextSibling)))) { + while (current !== target && !(next = current.$$nextSibling)) { + current = current.$parent; + } + } + } + + event.currentScope = null; + return event; + } + }; + + var $rootScope = new Scope(); + + //The internal queues. Expose them on the $rootScope for debugging/testing purposes. + var asyncQueue = $rootScope.$$asyncQueue = []; + var postDigestQueue = $rootScope.$$postDigestQueue = []; + var applyAsyncQueue = $rootScope.$$applyAsyncQueue = []; + + var postDigestQueuePosition = 0; + + return $rootScope; + + + function beginPhase(phase) { + if ($rootScope.$$phase) { + throw $rootScopeMinErr('inprog', '{0} already in progress', $rootScope.$$phase); + } + + $rootScope.$$phase = phase; + } + + function clearPhase() { + $rootScope.$$phase = null; + } + + function incrementWatchersCount(current, count) { + do { + current.$$watchersCount += count; + } while ((current = current.$parent)); + } + + function decrementListenerCount(current, count, name) { + do { + current.$$listenerCount[name] -= count; + + if (current.$$listenerCount[name] === 0) { + delete current.$$listenerCount[name]; + } + } while ((current = current.$parent)); + } + + /** + * function used as an initial value for watchers. + * because it's unique we can easily tell it apart from other values + */ + function initWatchVal() {} + + function flushApplyAsync() { + while (applyAsyncQueue.length) { + try { + applyAsyncQueue.shift()(); + } catch (e) { + $exceptionHandler(e); + } + } + applyAsyncId = null; + } + + function scheduleApplyAsync() { + if (applyAsyncId === null) { + applyAsyncId = $browser.defer(function() { + $rootScope.$apply(flushApplyAsync); + }); + } + } + }]; + } + + /** + * @ngdoc service + * @name $rootElement + * + * @description + * The root element of AngularJS application. This is either the element where {@link + * ng.directive:ngApp ngApp} was declared or the element passed into + * {@link angular.bootstrap}. The element represents the root element of application. It is also the + * location where the application's {@link auto.$injector $injector} service gets + * published, and can be retrieved using `$rootElement.injector()`. + */ + + +// the implementation is in angular.bootstrap + + /** + * @this + * @description + * Private service to sanitize uris for links and images. Used by $compile and $sanitize. + */ + function $$SanitizeUriProvider() { + var aHrefSanitizationWhitelist = /^\s*(https?|s?ftp|mailto|tel|file):/, + imgSrcSanitizationWhitelist = /^\s*((https?|ftp|file|blob):|data:image\/)/; + + /** + * @description + * Retrieves or overrides the default regular expression that is used for whitelisting of safe + * urls during a[href] sanitization. + * + * The sanitization is a security measure aimed at prevent XSS attacks via html links. + * + * Any url about to be assigned to a[href] via data-binding is first normalized and turned into + * an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist` + * regular expression. If a match is found, the original url is written into the dom. Otherwise, + * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. + * + * @param {RegExp=} regexp New regexp to whitelist urls with. + * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for + * chaining otherwise. + */ + this.aHrefSanitizationWhitelist = function(regexp) { + if (isDefined(regexp)) { + aHrefSanitizationWhitelist = regexp; + return this; + } + return aHrefSanitizationWhitelist; + }; + + + /** + * @description + * Retrieves or overrides the default regular expression that is used for whitelisting of safe + * urls during img[src] sanitization. + * + * The sanitization is a security measure aimed at prevent XSS attacks via html links. + * + * Any url about to be assigned to img[src] via data-binding is first normalized and turned into + * an absolute url. Afterwards, the url is matched against the `imgSrcSanitizationWhitelist` + * regular expression. If a match is found, the original url is written into the dom. Otherwise, + * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. + * + * @param {RegExp=} regexp New regexp to whitelist urls with. + * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for + * chaining otherwise. + */ + this.imgSrcSanitizationWhitelist = function(regexp) { + if (isDefined(regexp)) { + imgSrcSanitizationWhitelist = regexp; + return this; + } + return imgSrcSanitizationWhitelist; + }; + + this.$get = function() { + return function sanitizeUri(uri, isImage) { + var regex = isImage ? imgSrcSanitizationWhitelist : aHrefSanitizationWhitelist; + var normalizedVal; + normalizedVal = urlResolve(uri && uri.trim()).href; + if (normalizedVal !== '' && !normalizedVal.match(regex)) { + return 'unsafe:' + normalizedVal; + } + return uri; + }; + }; + } + + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Any commits to this file should be reviewed with security in mind. * + * Changes to this file can potentially create security vulnerabilities. * + * An approval from 2 Core members with history of modifying * + * this file is required. * + * * + * Does the change somehow allow for arbitrary javascript to be executed? * + * Or allows for someone to change the prototype of built-in objects? * + * Or gives undesired access to variables likes document or window? * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + + /* exported $SceProvider, $SceDelegateProvider */ + + var $sceMinErr = minErr('$sce'); + + var SCE_CONTEXTS = { + // HTML is used when there's HTML rendered (e.g. ng-bind-html, iframe srcdoc binding). + HTML: 'html', + + // Style statements or stylesheets. Currently unused in AngularJS. + CSS: 'css', + + // An URL used in a context where it does not refer to a resource that loads code. Currently + // unused in AngularJS. + URL: 'url', + + // RESOURCE_URL is a subtype of URL used where the referred-to resource could be interpreted as + // code. (e.g. ng-include, script src binding, templateUrl) + RESOURCE_URL: 'resourceUrl', + + // Script. Currently unused in AngularJS. + JS: 'js' + }; + +// Helper functions follow. + + var UNDERSCORE_LOWERCASE_REGEXP = /_([a-z])/g; + + function snakeToCamel(name) { + return name + .replace(UNDERSCORE_LOWERCASE_REGEXP, fnCamelCaseReplace); + } + + function adjustMatcher(matcher) { + if (matcher === 'self') { + return matcher; + } else if (isString(matcher)) { + // Strings match exactly except for 2 wildcards - '*' and '**'. + // '*' matches any character except those from the set ':/.?&'. + // '**' matches any character (like .* in a RegExp). + // More than 2 *'s raises an error as it's ill defined. + if (matcher.indexOf('***') > -1) { + throw $sceMinErr('iwcard', + 'Illegal sequence *** in string matcher. String: {0}', matcher); + } + matcher = escapeForRegexp(matcher). + replace(/\\\*\\\*/g, '.*'). + replace(/\\\*/g, '[^:/.?&;]*'); + return new RegExp('^' + matcher + '$'); + } else if (isRegExp(matcher)) { + // The only other type of matcher allowed is a Regexp. + // Match entire URL / disallow partial matches. + // Flags are reset (i.e. no global, ignoreCase or multiline) + return new RegExp('^' + matcher.source + '$'); + } else { + throw $sceMinErr('imatcher', + 'Matchers may only be "self", string patterns or RegExp objects'); + } + } + + + function adjustMatchers(matchers) { + var adjustedMatchers = []; + if (isDefined(matchers)) { + forEach(matchers, function(matcher) { + adjustedMatchers.push(adjustMatcher(matcher)); + }); + } + return adjustedMatchers; + } + + + /** + * @ngdoc service + * @name $sceDelegate + * @kind function + * + * @description + * + * `$sceDelegate` is a service that is used by the `$sce` service to provide {@link ng.$sce Strict + * Contextual Escaping (SCE)} services to AngularJS. + * + * For an overview of this service and the functionnality it provides in AngularJS, see the main + * page for {@link ng.$sce SCE}. The current page is targeted for developers who need to alter how + * SCE works in their application, which shouldn't be needed in most cases. + * + *
    + * AngularJS strongly relies on contextual escaping for the security of bindings: disabling or + * modifying this might cause cross site scripting (XSS) vulnerabilities. For libraries owners, + * changes to this service will also influence users, so be extra careful and document your changes. + *
    + * + * Typically, you would configure or override the {@link ng.$sceDelegate $sceDelegate} instead of + * the `$sce` service to customize the way Strict Contextual Escaping works in AngularJS. This is + * because, while the `$sce` provides numerous shorthand methods, etc., you really only need to + * override 3 core functions (`trustAs`, `getTrusted` and `valueOf`) to replace the way things + * work because `$sce` delegates to `$sceDelegate` for these operations. + * + * Refer {@link ng.$sceDelegateProvider $sceDelegateProvider} to configure this service. + * + * The default instance of `$sceDelegate` should work out of the box with little pain. While you + * can override it completely to change the behavior of `$sce`, the common case would + * involve configuring the {@link ng.$sceDelegateProvider $sceDelegateProvider} instead by setting + * your own whitelists and blacklists for trusting URLs used for loading AngularJS resources such as + * templates. Refer {@link ng.$sceDelegateProvider#resourceUrlWhitelist + * $sceDelegateProvider.resourceUrlWhitelist} and {@link + * ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist} + */ + + /** + * @ngdoc provider + * @name $sceDelegateProvider + * @this + * + * @description + * + * The `$sceDelegateProvider` provider allows developers to configure the {@link ng.$sceDelegate + * $sceDelegate service}, used as a delegate for {@link ng.$sce Strict Contextual Escaping (SCE)}. + * + * The `$sceDelegateProvider` allows one to get/set the whitelists and blacklists used to ensure + * that the URLs used for sourcing AngularJS templates and other script-running URLs are safe (all + * places that use the `$sce.RESOURCE_URL` context). See + * {@link ng.$sceDelegateProvider#resourceUrlWhitelist $sceDelegateProvider.resourceUrlWhitelist} + * and + * {@link ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist}, + * + * For the general details about this service in AngularJS, read the main page for {@link ng.$sce + * Strict Contextual Escaping (SCE)}. + * + * **Example**: Consider the following case. + * + * - your app is hosted at url `http://myapp.example.com/` + * - but some of your templates are hosted on other domains you control such as + * `http://srv01.assets.example.com/`, `http://srv02.assets.example.com/`, etc. + * - and you have an open redirect at `http://myapp.example.com/clickThru?...`. + * + * Here is what a secure configuration for this scenario might look like: + * + * ``` + * angular.module('myApp', []).config(function($sceDelegateProvider) { + * $sceDelegateProvider.resourceUrlWhitelist([ + * // Allow same origin resource loads. + * 'self', + * // Allow loading from our assets domain. Notice the difference between * and **. + * 'http://srv*.assets.example.com/**' + * ]); + * + * // The blacklist overrides the whitelist so the open redirect here is blocked. + * $sceDelegateProvider.resourceUrlBlacklist([ + * 'http://myapp.example.com/clickThru**' + * ]); + * }); + * ``` + * Note that an empty whitelist will block every resource URL from being loaded, and will require + * you to manually mark each one as trusted with `$sce.trustAsResourceUrl`. However, templates + * requested by {@link ng.$templateRequest $templateRequest} that are present in + * {@link ng.$templateCache $templateCache} will not go through this check. If you have a mechanism + * to populate your templates in that cache at config time, then it is a good idea to remove 'self' + * from that whitelist. This helps to mitigate the security impact of certain types of issues, like + * for instance attacker-controlled `ng-includes`. + */ + + function $SceDelegateProvider() { + this.SCE_CONTEXTS = SCE_CONTEXTS; + + // Resource URLs can also be trusted by policy. + var resourceUrlWhitelist = ['self'], + resourceUrlBlacklist = []; + + /** + * @ngdoc method + * @name $sceDelegateProvider#resourceUrlWhitelist + * @kind function + * + * @param {Array=} whitelist When provided, replaces the resourceUrlWhitelist with the value + * provided. This must be an array or null. A snapshot of this array is used so further + * changes to the array are ignored. + * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items + * allowed in this array. + * + * @return {Array} The currently set whitelist array. + * + * @description + * Sets/Gets the whitelist of trusted resource URLs. + * + * The **default value** when no whitelist has been explicitly set is `['self']` allowing only + * same origin resource requests. + * + *
    + * **Note:** the default whitelist of 'self' is not recommended if your app shares its origin + * with other apps! It is a good idea to limit it to only your application's directory. + *
    + */ + this.resourceUrlWhitelist = function(value) { + if (arguments.length) { + resourceUrlWhitelist = adjustMatchers(value); + } + return resourceUrlWhitelist; + }; + + /** + * @ngdoc method + * @name $sceDelegateProvider#resourceUrlBlacklist + * @kind function + * + * @param {Array=} blacklist When provided, replaces the resourceUrlBlacklist with the value + * provided. This must be an array or null. A snapshot of this array is used so further + * changes to the array are ignored.

    + * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items + * allowed in this array.

    + * The typical usage for the blacklist is to **block + * [open redirects](http://cwe.mitre.org/data/definitions/601.html)** served by your domain as + * these would otherwise be trusted but actually return content from the redirected domain. + *

    + * Finally, **the blacklist overrides the whitelist** and has the final say. + * + * @return {Array} The currently set blacklist array. + * + * @description + * Sets/Gets the blacklist of trusted resource URLs. + * + * The **default value** when no whitelist has been explicitly set is the empty array (i.e. there + * is no blacklist.) + */ + + this.resourceUrlBlacklist = function(value) { + if (arguments.length) { + resourceUrlBlacklist = adjustMatchers(value); + } + return resourceUrlBlacklist; + }; + + this.$get = ['$injector', function($injector) { + + var htmlSanitizer = function htmlSanitizer(html) { + throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.'); + }; + + if ($injector.has('$sanitize')) { + htmlSanitizer = $injector.get('$sanitize'); + } + + + function matchUrl(matcher, parsedUrl) { + if (matcher === 'self') { + return urlIsSameOrigin(parsedUrl); + } else { + // definitely a regex. See adjustMatchers() + return !!matcher.exec(parsedUrl.href); + } + } + + function isResourceUrlAllowedByPolicy(url) { + var parsedUrl = urlResolve(url.toString()); + var i, n, allowed = false; + // Ensure that at least one item from the whitelist allows this url. + for (i = 0, n = resourceUrlWhitelist.length; i < n; i++) { + if (matchUrl(resourceUrlWhitelist[i], parsedUrl)) { + allowed = true; + break; + } + } + if (allowed) { + // Ensure that no item from the blacklist blocked this url. + for (i = 0, n = resourceUrlBlacklist.length; i < n; i++) { + if (matchUrl(resourceUrlBlacklist[i], parsedUrl)) { + allowed = false; + break; + } + } + } + return allowed; + } + + function generateHolderType(Base) { + var holderType = function TrustedValueHolderType(trustedValue) { + this.$$unwrapTrustedValue = function() { + return trustedValue; + }; + }; + if (Base) { + holderType.prototype = new Base(); + } + holderType.prototype.valueOf = function sceValueOf() { + return this.$$unwrapTrustedValue(); + }; + holderType.prototype.toString = function sceToString() { + return this.$$unwrapTrustedValue().toString(); + }; + return holderType; + } + + var trustedValueHolderBase = generateHolderType(), + byType = {}; + + byType[SCE_CONTEXTS.HTML] = generateHolderType(trustedValueHolderBase); + byType[SCE_CONTEXTS.CSS] = generateHolderType(trustedValueHolderBase); + byType[SCE_CONTEXTS.URL] = generateHolderType(trustedValueHolderBase); + byType[SCE_CONTEXTS.JS] = generateHolderType(trustedValueHolderBase); + byType[SCE_CONTEXTS.RESOURCE_URL] = generateHolderType(byType[SCE_CONTEXTS.URL]); + + /** + * @ngdoc method + * @name $sceDelegate#trustAs + * + * @description + * Returns a trusted representation of the parameter for the specified context. This trusted + * object will later on be used as-is, without any security check, by bindings or directives + * that require this security context. + * For instance, marking a string as trusted for the `$sce.HTML` context will entirely bypass + * the potential `$sanitize` call in corresponding `$sce.HTML` bindings or directives, such as + * `ng-bind-html`. Note that in most cases you won't need to call this function: if you have the + * sanitizer loaded, passing the value itself will render all the HTML that does not pose a + * security risk. + * + * See {@link ng.$sceDelegate#getTrusted getTrusted} for the function that will consume those + * trusted values, and {@link ng.$sce $sce} for general documentation about strict contextual + * escaping. + * + * @param {string} type The context in which this value is safe for use, e.g. `$sce.URL`, + * `$sce.RESOURCE_URL`, `$sce.HTML`, `$sce.JS` or `$sce.CSS`. + * + * @param {*} value The value that should be considered trusted. + * @return {*} A trusted representation of value, that can be used in the given context. + */ + function trustAs(type, trustedValue) { + var Constructor = (byType.hasOwnProperty(type) ? byType[type] : null); + if (!Constructor) { + throw $sceMinErr('icontext', + 'Attempted to trust a value in invalid context. Context: {0}; Value: {1}', + type, trustedValue); + } + if (trustedValue === null || isUndefined(trustedValue) || trustedValue === '') { + return trustedValue; + } + // All the current contexts in SCE_CONTEXTS happen to be strings. In order to avoid trusting + // mutable objects, we ensure here that the value passed in is actually a string. + if (typeof trustedValue !== 'string') { + throw $sceMinErr('itype', + 'Attempted to trust a non-string value in a content requiring a string: Context: {0}', + type); + } + return new Constructor(trustedValue); + } + + /** + * @ngdoc method + * @name $sceDelegate#valueOf + * + * @description + * If the passed parameter had been returned by a prior call to {@link ng.$sceDelegate#trustAs + * `$sceDelegate.trustAs`}, returns the value that had been passed to {@link + * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. + * + * If the passed parameter is not a value that had been returned by {@link + * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}, it must be returned as-is. + * + * @param {*} value The result of a prior {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`} + * call or anything else. + * @return {*} The `value` that was originally provided to {@link ng.$sceDelegate#trustAs + * `$sceDelegate.trustAs`} if `value` is the result of such a call. Otherwise, returns + * `value` unchanged. + */ + function valueOf(maybeTrusted) { + if (maybeTrusted instanceof trustedValueHolderBase) { + return maybeTrusted.$$unwrapTrustedValue(); + } else { + return maybeTrusted; + } + } + + /** + * @ngdoc method + * @name $sceDelegate#getTrusted + * + * @description + * Takes any input, and either returns a value that's safe to use in the specified context, or + * throws an exception. + * + * In practice, there are several cases. When given a string, this function runs checks + * and sanitization to make it safe without prior assumptions. When given the result of a {@link + * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`} call, it returns the originally supplied + * value if that value's context is valid for this call's context. Finally, this function can + * also throw when there is no way to turn `maybeTrusted` in a safe value (e.g., no sanitization + * is available or possible.) + * + * @param {string} type The context in which this value is to be used (such as `$sce.HTML`). + * @param {*} maybeTrusted The result of a prior {@link ng.$sceDelegate#trustAs + * `$sceDelegate.trustAs`} call, or anything else (which will not be considered trusted.) + * @return {*} A version of the value that's safe to use in the given context, or throws an + * exception if this is impossible. + */ + function getTrusted(type, maybeTrusted) { + if (maybeTrusted === null || isUndefined(maybeTrusted) || maybeTrusted === '') { + return maybeTrusted; + } + var constructor = (byType.hasOwnProperty(type) ? byType[type] : null); + // If maybeTrusted is a trusted class instance or subclass instance, then unwrap and return + // as-is. + if (constructor && maybeTrusted instanceof constructor) { + return maybeTrusted.$$unwrapTrustedValue(); + } + // Otherwise, if we get here, then we may either make it safe, or throw an exception. This + // depends on the context: some are sanitizatible (HTML), some use whitelists (RESOURCE_URL), + // some are impossible to do (JS). This step isn't implemented for CSS and URL, as AngularJS + // has no corresponding sinks. + if (type === SCE_CONTEXTS.RESOURCE_URL) { + // RESOURCE_URL uses a whitelist. + if (isResourceUrlAllowedByPolicy(maybeTrusted)) { + return maybeTrusted; + } else { + throw $sceMinErr('insecurl', + 'Blocked loading resource from url not allowed by $sceDelegate policy. URL: {0}', + maybeTrusted.toString()); + } + } else if (type === SCE_CONTEXTS.HTML) { + // htmlSanitizer throws its own error when no sanitizer is available. + return htmlSanitizer(maybeTrusted); + } + // Default error when the $sce service has no way to make the input safe. + throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.'); + } + + return { trustAs: trustAs, + getTrusted: getTrusted, + valueOf: valueOf }; + }]; + } + + + /** + * @ngdoc provider + * @name $sceProvider + * @this + * + * @description + * + * The $sceProvider provider allows developers to configure the {@link ng.$sce $sce} service. + * - enable/disable Strict Contextual Escaping (SCE) in a module + * - override the default implementation with a custom delegate + * + * Read more about {@link ng.$sce Strict Contextual Escaping (SCE)}. + */ + + /** + * @ngdoc service + * @name $sce + * @kind function + * + * @description + * + * `$sce` is a service that provides Strict Contextual Escaping services to AngularJS. + * + * ## Strict Contextual Escaping + * + * Strict Contextual Escaping (SCE) is a mode in which AngularJS constrains bindings to only render + * trusted values. Its goal is to assist in writing code in a way that (a) is secure by default, and + * (b) makes auditing for security vulnerabilities such as XSS, clickjacking, etc. a lot easier. + * + * ### Overview + * + * To systematically block XSS security bugs, AngularJS treats all values as untrusted by default in + * HTML or sensitive URL bindings. When binding untrusted values, AngularJS will automatically + * run security checks on them (sanitizations, whitelists, depending on context), or throw when it + * cannot guarantee the security of the result. That behavior depends strongly on contexts: HTML + * can be sanitized, but template URLs cannot, for instance. + * + * To illustrate this, consider the `ng-bind-html` directive. It renders its value directly as HTML: + * we call that the *context*. When given an untrusted input, AngularJS will attempt to sanitize it + * before rendering if a sanitizer is available, and throw otherwise. To bypass sanitization and + * render the input as-is, you will need to mark it as trusted for that context before attempting + * to bind it. + * + * As of version 1.2, AngularJS ships with SCE enabled by default. + * + * ### In practice + * + * Here's an example of a binding in a privileged context: + * + * ``` + * + *

    + * ``` + * + * Notice that `ng-bind-html` is bound to `userHtml` controlled by the user. With SCE + * disabled, this application allows the user to render arbitrary HTML into the DIV, which would + * be an XSS security bug. In a more realistic example, one may be rendering user comments, blog + * articles, etc. via bindings. (HTML is just one example of a context where rendering user + * controlled input creates security vulnerabilities.) + * + * For the case of HTML, you might use a library, either on the client side, or on the server side, + * to sanitize unsafe HTML before binding to the value and rendering it in the document. + * + * How would you ensure that every place that used these types of bindings was bound to a value that + * was sanitized by your library (or returned as safe for rendering by your server?) How can you + * ensure that you didn't accidentally delete the line that sanitized the value, or renamed some + * properties/fields and forgot to update the binding to the sanitized value? + * + * To be secure by default, AngularJS makes sure bindings go through that sanitization, or + * any similar validation process, unless there's a good reason to trust the given value in this + * context. That trust is formalized with a function call. This means that as a developer, you + * can assume all untrusted bindings are safe. Then, to audit your code for binding security issues, + * you just need to ensure the values you mark as trusted indeed are safe - because they were + * received from your server, sanitized by your library, etc. You can organize your codebase to + * help with this - perhaps allowing only the files in a specific directory to do this. + * Ensuring that the internal API exposed by that code doesn't markup arbitrary values as safe then + * becomes a more manageable task. + * + * In the case of AngularJS' SCE service, one uses {@link ng.$sce#trustAs $sce.trustAs} + * (and shorthand methods such as {@link ng.$sce#trustAsHtml $sce.trustAsHtml}, etc.) to + * build the trusted versions of your values. + * + * ### How does it work? + * + * In privileged contexts, directives and code will bind to the result of {@link ng.$sce#getTrusted + * $sce.getTrusted(context, value)} rather than to the value directly. Think of this function as + * a way to enforce the required security context in your data sink. Directives use {@link + * ng.$sce#parseAs $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs + * the {@link ng.$sce#getTrusted $sce.getTrusted} behind the scenes on non-constant literals. Also, + * when binding without directives, AngularJS will understand the context of your bindings + * automatically. + * + * As an example, {@link ng.directive:ngBindHtml ngBindHtml} uses {@link + * ng.$sce#parseAsHtml $sce.parseAsHtml(binding expression)}. Here's the actual code (slightly + * simplified): + * + * ``` + * var ngBindHtmlDirective = ['$sce', function($sce) { + * return function(scope, element, attr) { + * scope.$watch($sce.parseAsHtml(attr.ngBindHtml), function(value) { + * element.html(value || ''); + * }); + * }; + * }]; + * ``` + * + * ### Impact on loading templates + * + * This applies both to the {@link ng.directive:ngInclude `ng-include`} directive as well as + * `templateUrl`'s specified by {@link guide/directive directives}. + * + * By default, AngularJS only loads templates from the same domain and protocol as the application + * document. This is done by calling {@link ng.$sce#getTrustedResourceUrl + * $sce.getTrustedResourceUrl} on the template URL. To load templates from other domains and/or + * protocols, you may either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist + * them} or {@link ng.$sce#trustAsResourceUrl wrap it} into a trusted value. + * + * *Please note*: + * The browser's + * [Same Origin Policy](https://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_XMLHttpRequest) + * and [Cross-Origin Resource Sharing (CORS)](http://www.w3.org/TR/cors/) + * policy apply in addition to this and may further restrict whether the template is successfully + * loaded. This means that without the right CORS policy, loading templates from a different domain + * won't work on all browsers. Also, loading templates from `file://` URL does not work on some + * browsers. + * + * ### This feels like too much overhead + * + * It's important to remember that SCE only applies to interpolation expressions. + * + * If your expressions are constant literals, they're automatically trusted and you don't need to + * call `$sce.trustAs` on them (e.g. + * `
    `) just works. The `$sceDelegate` will + * also use the `$sanitize` service if it is available when binding untrusted values to + * `$sce.HTML` context. AngularJS provides an implementation in `angular-sanitize.js`, and if you + * wish to use it, you will also need to depend on the {@link ngSanitize `ngSanitize`} module in + * your application. + * + * The included {@link ng.$sceDelegate $sceDelegate} comes with sane defaults to allow you to load + * templates in `ng-include` from your application's domain without having to even know about SCE. + * It blocks loading templates from other domains or loading templates over http from an https + * served document. You can change these by setting your own custom {@link + * ng.$sceDelegateProvider#resourceUrlWhitelist whitelists} and {@link + * ng.$sceDelegateProvider#resourceUrlBlacklist blacklists} for matching such URLs. + * + * This significantly reduces the overhead. It is far easier to pay the small overhead and have an + * application that's secure and can be audited to verify that with much more ease than bolting + * security onto an application later. + * + * + * ### What trusted context types are supported? + * + * | Context | Notes | + * |---------------------|----------------| + * | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtml ngBindHtml} directive uses this context for bindings. If an unsafe value is encountered, and the {@link ngSanitize.$sanitize $sanitize} service is available (implemented by the {@link ngSanitize ngSanitize} module) this will sanitize the value instead of throwing an error. | + * | `$sce.CSS` | For CSS that's safe to source into the application. Currently, no bindings require this context. Feel free to use it in your own directives. | + * | `$sce.URL` | For URLs that are safe to follow as links. Currently unused (`
    Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does (it's not just the URL that matters, but also what is at the end of it), and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. | + * | `$sce.JS` | For JavaScript that is safe to execute in your application's context. Currently, no bindings require this context. Feel free to use it in your own directives. | + * + * + * Be aware that `a[href]` and `img[src]` automatically sanitize their URLs and do not pass them + * through {@link ng.$sce#getTrusted $sce.getTrusted}. There's no CSS-, URL-, or JS-context bindings + * in AngularJS currently, so their corresponding `$sce.trustAs` functions aren't useful yet. This + * might evolve. + * + * ### Format of items in {@link ng.$sceDelegateProvider#resourceUrlWhitelist resourceUrlWhitelist}/{@link ng.$sceDelegateProvider#resourceUrlBlacklist Blacklist}
    + * + * Each element in these arrays must be one of the following: + * + * - **'self'** + * - The special **string**, `'self'`, can be used to match against all URLs of the **same + * domain** as the application document using the **same protocol**. + * - **String** (except the special value `'self'`) + * - The string is matched against the full *normalized / absolute URL* of the resource + * being tested (substring matches are not good enough.) + * - There are exactly **two wildcard sequences** - `*` and `**`. All other characters + * match themselves. + * - `*`: matches zero or more occurrences of any character other than one of the following 6 + * characters: '`:`', '`/`', '`.`', '`?`', '`&`' and '`;`'. It's a useful wildcard for use + * in a whitelist. + * - `**`: matches zero or more occurrences of *any* character. As such, it's not + * appropriate for use in a scheme, domain, etc. as it would match too much. (e.g. + * http://**.example.com/ would match http://evil.com/?ignore=.example.com/ and that might + * not have been the intention.) Its usage at the very end of the path is ok. (e.g. + * http://foo.example.com/templates/**). + * - **RegExp** (*see caveat below*) + * - *Caveat*: While regular expressions are powerful and offer great flexibility, their syntax + * (and all the inevitable escaping) makes them *harder to maintain*. It's easy to + * accidentally introduce a bug when one updates a complex expression (imho, all regexes should + * have good test coverage). For instance, the use of `.` in the regex is correct only in a + * small number of cases. A `.` character in the regex used when matching the scheme or a + * subdomain could be matched against a `:` or literal `.` that was likely not intended. It + * is highly recommended to use the string patterns and only fall back to regular expressions + * as a last resort. + * - The regular expression must be an instance of RegExp (i.e. not a string.) It is + * matched against the **entire** *normalized / absolute URL* of the resource being tested + * (even when the RegExp did not have the `^` and `$` codes.) In addition, any flags + * present on the RegExp (such as multiline, global, ignoreCase) are ignored. + * - If you are generating your JavaScript from some other templating engine (not + * recommended, e.g. in issue [#4006](https://github.com/angular/angular.js/issues/4006)), + * remember to escape your regular expression (and be aware that you might need more than + * one level of escaping depending on your templating engine and the way you interpolated + * the value.) Do make use of your platform's escaping mechanism as it might be good + * enough before coding your own. E.g. Ruby has + * [Regexp.escape(str)](http://www.ruby-doc.org/core-2.0.0/Regexp.html#method-c-escape) + * and Python has [re.escape](http://docs.python.org/library/re.html#re.escape). + * Javascript lacks a similar built in function for escaping. Take a look at Google + * Closure library's [goog.string.regExpEscape(s)]( + * http://docs.closure-library.googlecode.com/git/closure_goog_string_string.js.source.html#line962). + * + * Refer {@link ng.$sceDelegateProvider $sceDelegateProvider} for an example. + * + * ### Show me an example using SCE. + * + * + * + *
    + *

    + * User comments
    + * By default, HTML that isn't explicitly trusted (e.g. Alice's comment) is sanitized when + * $sanitize is available. If $sanitize isn't available, this results in an error instead of an + * exploit. + *
    + *
    + * {{userComment.name}}: + * + *
    + *
    + *
    + *
    + *
    + * + * + * angular.module('mySceApp', ['ngSanitize']) + * .controller('AppController', ['$http', '$templateCache', '$sce', + * function AppController($http, $templateCache, $sce) { + * var self = this; + * $http.get('test_data.json', {cache: $templateCache}).then(function(response) { + * self.userComments = response.data; + * }); + * self.explicitlyTrustedHtml = $sce.trustAsHtml( + * 'Hover over this text.'); + * }]); + * + * + * + * [ + * { "name": "Alice", + * "htmlComment": + * "Is anyone reading this?" + * }, + * { "name": "Bob", + * "htmlComment": "Yes! Am I the only other one?" + * } + * ] + * + * + * + * describe('SCE doc demo', function() { + * it('should sanitize untrusted values', function() { + * expect(element.all(by.css('.htmlComment')).first().getAttribute('innerHTML')) + * .toBe('Is anyone reading this?'); + * }); + * + * it('should NOT sanitize explicitly trusted values', function() { + * expect(element(by.id('explicitlyTrustedHtml')).getAttribute('innerHTML')).toBe( + * 'Hover over this text.'); + * }); + * }); + * + *
    + * + * + * + * ## Can I disable SCE completely? + * + * Yes, you can. However, this is strongly discouraged. SCE gives you a lot of security benefits + * for little coding overhead. It will be much harder to take an SCE disabled application and + * either secure it on your own or enable SCE at a later stage. It might make sense to disable SCE + * for cases where you have a lot of existing code that was written before SCE was introduced and + * you're migrating them a module at a time. Also do note that this is an app-wide setting, so if + * you are writing a library, you will cause security bugs applications using it. + * + * That said, here's how you can completely disable SCE: + * + * ``` + * angular.module('myAppWithSceDisabledmyApp', []).config(function($sceProvider) { + * // Completely disable SCE. For demonstration purposes only! + * // Do not use in new projects or libraries. + * $sceProvider.enabled(false); + * }); + * ``` + * + */ + + function $SceProvider() { + var enabled = true; + + /** + * @ngdoc method + * @name $sceProvider#enabled + * @kind function + * + * @param {boolean=} value If provided, then enables/disables SCE application-wide. + * @return {boolean} True if SCE is enabled, false otherwise. + * + * @description + * Enables/disables SCE and returns the current value. + */ + this.enabled = function(value) { + if (arguments.length) { + enabled = !!value; + } + return enabled; + }; + + + /* Design notes on the default implementation for SCE. + * + * The API contract for the SCE delegate + * ------------------------------------- + * The SCE delegate object must provide the following 3 methods: + * + * - trustAs(contextEnum, value) + * This method is used to tell the SCE service that the provided value is OK to use in the + * contexts specified by contextEnum. It must return an object that will be accepted by + * getTrusted() for a compatible contextEnum and return this value. + * + * - valueOf(value) + * For values that were not produced by trustAs(), return them as is. For values that were + * produced by trustAs(), return the corresponding input value to trustAs. Basically, if + * trustAs is wrapping the given values into some type, this operation unwraps it when given + * such a value. + * + * - getTrusted(contextEnum, value) + * This function should return the a value that is safe to use in the context specified by + * contextEnum or throw and exception otherwise. + * + * NOTE: This contract deliberately does NOT state that values returned by trustAs() must be + * opaque or wrapped in some holder object. That happens to be an implementation detail. For + * instance, an implementation could maintain a registry of all trusted objects by context. In + * such a case, trustAs() would return the same object that was passed in. getTrusted() would + * return the same object passed in if it was found in the registry under a compatible context or + * throw an exception otherwise. An implementation might only wrap values some of the time based + * on some criteria. getTrusted() might return a value and not throw an exception for special + * constants or objects even if not wrapped. All such implementations fulfill this contract. + * + * + * A note on the inheritance model for SCE contexts + * ------------------------------------------------ + * I've used inheritance and made RESOURCE_URL wrapped types a subtype of URL wrapped types. This + * is purely an implementation details. + * + * The contract is simply this: + * + * getTrusted($sce.RESOURCE_URL, value) succeeding implies that getTrusted($sce.URL, value) + * will also succeed. + * + * Inheritance happens to capture this in a natural way. In some future, we may not use + * inheritance anymore. That is OK because no code outside of sce.js and sceSpecs.js would need to + * be aware of this detail. + */ + + this.$get = ['$parse', '$sceDelegate', function( + $parse, $sceDelegate) { + // Support: IE 9-11 only + // Prereq: Ensure that we're not running in IE<11 quirks mode. In that mode, IE < 11 allow + // the "expression(javascript expression)" syntax which is insecure. + if (enabled && msie < 8) { + throw $sceMinErr('iequirks', + 'Strict Contextual Escaping does not support Internet Explorer version < 11 in quirks ' + + 'mode. You can fix this by adding the text to the top of your HTML ' + + 'document. See http://docs.angularjs.org/api/ng.$sce for more information.'); + } + + var sce = shallowCopy(SCE_CONTEXTS); + + /** + * @ngdoc method + * @name $sce#isEnabled + * @kind function + * + * @return {Boolean} True if SCE is enabled, false otherwise. If you want to set the value, you + * have to do it at module config time on {@link ng.$sceProvider $sceProvider}. + * + * @description + * Returns a boolean indicating if SCE is enabled. + */ + sce.isEnabled = function() { + return enabled; + }; + sce.trustAs = $sceDelegate.trustAs; + sce.getTrusted = $sceDelegate.getTrusted; + sce.valueOf = $sceDelegate.valueOf; + + if (!enabled) { + sce.trustAs = sce.getTrusted = function(type, value) { return value; }; + sce.valueOf = identity; + } + + /** + * @ngdoc method + * @name $sce#parseAs + * + * @description + * Converts AngularJS {@link guide/expression expression} into a function. This is like {@link + * ng.$parse $parse} and is identical when the expression is a literal constant. Otherwise, it + * wraps the expression in a call to {@link ng.$sce#getTrusted $sce.getTrusted(*type*, + * *result*)} + * + * @param {string} type The SCE context in which this result will be used. + * @param {string} expression String expression to compile. + * @return {function(context, locals)} A function which represents the compiled expression: + * + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. + */ + sce.parseAs = function sceParseAs(type, expr) { + var parsed = $parse(expr); + if (parsed.literal && parsed.constant) { + return parsed; + } else { + return $parse(expr, function(value) { + return sce.getTrusted(type, value); + }); + } + }; + + /** + * @ngdoc method + * @name $sce#trustAs + * + * @description + * Delegates to {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. As such, returns a + * wrapped object that represents your value, and the trust you have in its safety for the given + * context. AngularJS can then use that value as-is in bindings of the specified secure context. + * This is used in bindings for `ng-bind-html`, `ng-include`, and most `src` attribute + * interpolations. See {@link ng.$sce $sce} for strict contextual escaping. + * + * @param {string} type The context in which this value is safe for use, e.g. `$sce.URL`, + * `$sce.RESOURCE_URL`, `$sce.HTML`, `$sce.JS` or `$sce.CSS`. + * + * @param {*} value The value that that should be considered trusted. + * @return {*} A wrapped version of value that can be used as a trusted variant of your `value` + * in the context you specified. + */ + + /** + * @ngdoc method + * @name $sce#trustAsHtml + * + * @description + * Shorthand method. `$sce.trustAsHtml(value)` → + * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.HTML, value)`} + * + * @param {*} value The value to mark as trusted for `$sce.HTML` context. + * @return {*} A wrapped version of value that can be used as a trusted variant of your `value` + * in `$sce.HTML` context (like `ng-bind-html`). + */ + + /** + * @ngdoc method + * @name $sce#trustAsCss + * + * @description + * Shorthand method. `$sce.trustAsCss(value)` → + * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.CSS, value)`} + * + * @param {*} value The value to mark as trusted for `$sce.CSS` context. + * @return {*} A wrapped version of value that can be used as a trusted variant + * of your `value` in `$sce.CSS` context. This context is currently unused, so there are + * almost no reasons to use this function so far. + */ + + /** + * @ngdoc method + * @name $sce#trustAsUrl + * + * @description + * Shorthand method. `$sce.trustAsUrl(value)` → + * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.URL, value)`} + * + * @param {*} value The value to mark as trusted for `$sce.URL` context. + * @return {*} A wrapped version of value that can be used as a trusted variant of your `value` + * in `$sce.URL` context. That context is currently unused, so there are almost no reasons + * to use this function so far. + */ + + /** + * @ngdoc method + * @name $sce#trustAsResourceUrl + * + * @description + * Shorthand method. `$sce.trustAsResourceUrl(value)` → + * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.RESOURCE_URL, value)`} + * + * @param {*} value The value to mark as trusted for `$sce.RESOURCE_URL` context. + * @return {*} A wrapped version of value that can be used as a trusted variant of your `value` + * in `$sce.RESOURCE_URL` context (template URLs in `ng-include`, most `src` attribute + * bindings, ...) + */ + + /** + * @ngdoc method + * @name $sce#trustAsJs + * + * @description + * Shorthand method. `$sce.trustAsJs(value)` → + * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.JS, value)`} + * + * @param {*} value The value to mark as trusted for `$sce.JS` context. + * @return {*} A wrapped version of value that can be used as a trusted variant of your `value` + * in `$sce.JS` context. That context is currently unused, so there are almost no reasons to + * use this function so far. + */ + + /** + * @ngdoc method + * @name $sce#getTrusted + * + * @description + * Delegates to {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted`}. As such, + * takes any input, and either returns a value that's safe to use in the specified context, + * or throws an exception. This function is aware of trusted values created by the `trustAs` + * function and its shorthands, and when contexts are appropriate, returns the unwrapped value + * as-is. Finally, this function can also throw when there is no way to turn `maybeTrusted` in a + * safe value (e.g., no sanitization is available or possible.) + * + * @param {string} type The context in which this value is to be used. + * @param {*} maybeTrusted The result of a prior {@link ng.$sce#trustAs + * `$sce.trustAs`} call, or anything else (which will not be considered trusted.) + * @return {*} A version of the value that's safe to use in the given context, or throws an + * exception if this is impossible. + */ + + /** + * @ngdoc method + * @name $sce#getTrustedHtml + * + * @description + * Shorthand method. `$sce.getTrustedHtml(value)` → + * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.HTML, value)`} + * + * @param {*} value The value to pass to `$sce.getTrusted`. + * @return {*} The return value of `$sce.getTrusted($sce.HTML, value)` + */ + + /** + * @ngdoc method + * @name $sce#getTrustedCss + * + * @description + * Shorthand method. `$sce.getTrustedCss(value)` → + * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.CSS, value)`} + * + * @param {*} value The value to pass to `$sce.getTrusted`. + * @return {*} The return value of `$sce.getTrusted($sce.CSS, value)` + */ + + /** + * @ngdoc method + * @name $sce#getTrustedUrl + * + * @description + * Shorthand method. `$sce.getTrustedUrl(value)` → + * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.URL, value)`} + * + * @param {*} value The value to pass to `$sce.getTrusted`. + * @return {*} The return value of `$sce.getTrusted($sce.URL, value)` + */ + + /** + * @ngdoc method + * @name $sce#getTrustedResourceUrl + * + * @description + * Shorthand method. `$sce.getTrustedResourceUrl(value)` → + * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.RESOURCE_URL, value)`} + * + * @param {*} value The value to pass to `$sceDelegate.getTrusted`. + * @return {*} The return value of `$sce.getTrusted($sce.RESOURCE_URL, value)` + */ + + /** + * @ngdoc method + * @name $sce#getTrustedJs + * + * @description + * Shorthand method. `$sce.getTrustedJs(value)` → + * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.JS, value)`} + * + * @param {*} value The value to pass to `$sce.getTrusted`. + * @return {*} The return value of `$sce.getTrusted($sce.JS, value)` + */ + + /** + * @ngdoc method + * @name $sce#parseAsHtml + * + * @description + * Shorthand method. `$sce.parseAsHtml(expression string)` → + * {@link ng.$sce#parseAs `$sce.parseAs($sce.HTML, value)`} + * + * @param {string} expression String expression to compile. + * @return {function(context, locals)} A function which represents the compiled expression: + * + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. + */ + + /** + * @ngdoc method + * @name $sce#parseAsCss + * + * @description + * Shorthand method. `$sce.parseAsCss(value)` → + * {@link ng.$sce#parseAs `$sce.parseAs($sce.CSS, value)`} + * + * @param {string} expression String expression to compile. + * @return {function(context, locals)} A function which represents the compiled expression: + * + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. + */ + + /** + * @ngdoc method + * @name $sce#parseAsUrl + * + * @description + * Shorthand method. `$sce.parseAsUrl(value)` → + * {@link ng.$sce#parseAs `$sce.parseAs($sce.URL, value)`} + * + * @param {string} expression String expression to compile. + * @return {function(context, locals)} A function which represents the compiled expression: + * + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. + */ + + /** + * @ngdoc method + * @name $sce#parseAsResourceUrl + * + * @description + * Shorthand method. `$sce.parseAsResourceUrl(value)` → + * {@link ng.$sce#parseAs `$sce.parseAs($sce.RESOURCE_URL, value)`} + * + * @param {string} expression String expression to compile. + * @return {function(context, locals)} A function which represents the compiled expression: + * + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. + */ + + /** + * @ngdoc method + * @name $sce#parseAsJs + * + * @description + * Shorthand method. `$sce.parseAsJs(value)` → + * {@link ng.$sce#parseAs `$sce.parseAs($sce.JS, value)`} + * + * @param {string} expression String expression to compile. + * @return {function(context, locals)} A function which represents the compiled expression: + * + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. + */ + + // Shorthand delegations. + var parse = sce.parseAs, + getTrusted = sce.getTrusted, + trustAs = sce.trustAs; + + forEach(SCE_CONTEXTS, function(enumValue, name) { + var lName = lowercase(name); + sce[snakeToCamel('parse_as_' + lName)] = function(expr) { + return parse(enumValue, expr); + }; + sce[snakeToCamel('get_trusted_' + lName)] = function(value) { + return getTrusted(enumValue, value); + }; + sce[snakeToCamel('trust_as_' + lName)] = function(value) { + return trustAs(enumValue, value); + }; + }); + + return sce; + }]; + } + + /* exported $SnifferProvider */ + + /** + * !!! This is an undocumented "private" service !!! + * + * @name $sniffer + * @requires $window + * @requires $document + * @this + * + * @property {boolean} history Does the browser support html5 history api ? + * @property {boolean} transitions Does the browser support CSS transition events ? + * @property {boolean} animations Does the browser support CSS animation events ? + * + * @description + * This is very simple implementation of testing browser's features. + */ + function $SnifferProvider() { + this.$get = ['$window', '$document', function($window, $document) { + var eventSupport = {}, + // Chrome Packaged Apps are not allowed to access `history.pushState`. + // If not sandboxed, they can be detected by the presence of `chrome.app.runtime` + // (see https://developer.chrome.com/apps/api_index). If sandboxed, they can be detected by + // the presence of an extension runtime ID and the absence of other Chrome runtime APIs + // (see https://developer.chrome.com/apps/manifest/sandbox). + // (NW.js apps have access to Chrome APIs, but do support `history`.) + isNw = $window.nw && $window.nw.process, + isChromePackagedApp = + !isNw && + $window.chrome && + ($window.chrome.app && $window.chrome.app.runtime || + !$window.chrome.app && $window.chrome.runtime && $window.chrome.runtime.id), + hasHistoryPushState = !isChromePackagedApp && $window.history && $window.history.pushState, + android = + toInt((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]), + boxee = /Boxee/i.test(($window.navigator || {}).userAgent), + document = $document[0] || {}, + bodyStyle = document.body && document.body.style, + transitions = false, + animations = false; + + if (bodyStyle) { + // Support: Android <5, Blackberry Browser 10, default Chrome in Android 4.4.x + // Mentioned browsers need a -webkit- prefix for transitions & animations. + transitions = !!('transition' in bodyStyle || 'webkitTransition' in bodyStyle); + animations = !!('animation' in bodyStyle || 'webkitAnimation' in bodyStyle); + } + + + return { + // Android has history.pushState, but it does not update location correctly + // so let's not use the history API at all. + // http://code.google.com/p/android/issues/detail?id=17471 + // https://github.com/angular/angular.js/issues/904 + + // older webkit browser (533.9) on Boxee box has exactly the same problem as Android has + // so let's not use the history API also + // We are purposefully using `!(android < 4)` to cover the case when `android` is undefined + history: !!(hasHistoryPushState && !(android < 4) && !boxee), + hasEvent: function(event) { + // Support: IE 9-11 only + // IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have + // it. In particular the event is not fired when backspace or delete key are pressed or + // when cut operation is performed. + // IE10+ implements 'input' event but it erroneously fires under various situations, + // e.g. when placeholder changes, or a form is focused. + if (event === 'input' && msie) return false; + + if (isUndefined(eventSupport[event])) { + var divElm = document.createElement('div'); + eventSupport[event] = 'on' + event in divElm; + } + + return eventSupport[event]; + }, + csp: csp(), + transitions: transitions, + animations: animations, + android: android + }; + }]; + } + + var $templateRequestMinErr = minErr('$compile'); + + /** + * @ngdoc provider + * @name $templateRequestProvider + * @this + * + * @description + * Used to configure the options passed to the {@link $http} service when making a template request. + * + * For example, it can be used for specifying the "Accept" header that is sent to the server, when + * requesting a template. + */ + function $TemplateRequestProvider() { + + var httpOptions; + + /** + * @ngdoc method + * @name $templateRequestProvider#httpOptions + * @description + * The options to be passed to the {@link $http} service when making the request. + * You can use this to override options such as the "Accept" header for template requests. + * + * The {@link $templateRequest} will set the `cache` and the `transformResponse` properties of the + * options if not overridden here. + * + * @param {string=} value new value for the {@link $http} options. + * @returns {string|self} Returns the {@link $http} options when used as getter and self if used as setter. + */ + this.httpOptions = function(val) { + if (val) { + httpOptions = val; + return this; + } + return httpOptions; + }; + + /** + * @ngdoc service + * @name $templateRequest + * + * @description + * The `$templateRequest` service runs security checks then downloads the provided template using + * `$http` and, upon success, stores the contents inside of `$templateCache`. If the HTTP request + * fails or the response data of the HTTP request is empty, a `$compile` error will be thrown (the + * exception can be thwarted by setting the 2nd parameter of the function to true). Note that the + * contents of `$templateCache` are trusted, so the call to `$sce.getTrustedUrl(tpl)` is omitted + * when `tpl` is of type string and `$templateCache` has the matching entry. + * + * If you want to pass custom options to the `$http` service, such as setting the Accept header you + * can configure this via {@link $templateRequestProvider#httpOptions}. + * + * `$templateRequest` is used internally by {@link $compile}, {@link ngRoute.$route}, and directives such + * as {@link ngInclude} to download and cache templates. + * + * 3rd party modules should use `$templateRequest` if their services or directives are loading + * templates. + * + * @param {string|TrustedResourceUrl} tpl The HTTP request template URL + * @param {boolean=} ignoreRequestError Whether or not to ignore the exception when the request fails or the template is empty + * + * @return {Promise} a promise for the HTTP response data of the given URL. + * + * @property {number} totalPendingRequests total amount of pending template requests being downloaded. + */ + this.$get = ['$exceptionHandler', '$templateCache', '$http', '$q', '$sce', + function($exceptionHandler, $templateCache, $http, $q, $sce) { + + function handleRequestFn(tpl, ignoreRequestError) { + handleRequestFn.totalPendingRequests++; + + // We consider the template cache holds only trusted templates, so + // there's no need to go through whitelisting again for keys that already + // are included in there. This also makes AngularJS accept any script + // directive, no matter its name. However, we still need to unwrap trusted + // types. + if (!isString(tpl) || isUndefined($templateCache.get(tpl))) { + tpl = $sce.getTrustedResourceUrl(tpl); + } + + var transformResponse = $http.defaults && $http.defaults.transformResponse; + + if (isArray(transformResponse)) { + transformResponse = transformResponse.filter(function(transformer) { + return transformer !== defaultHttpResponseTransform; + }); + } else if (transformResponse === defaultHttpResponseTransform) { + transformResponse = null; + } + + return $http.get(tpl, extend({ + cache: $templateCache, + transformResponse: transformResponse + }, httpOptions)) + .finally(function() { + handleRequestFn.totalPendingRequests--; + }) + .then(function(response) { + $templateCache.put(tpl, response.data); + return response.data; + }, handleError); + + function handleError(resp) { + if (!ignoreRequestError) { + resp = $templateRequestMinErr('tpload', + 'Failed to load template: {0} (HTTP status: {1} {2})', + tpl, resp.status, resp.statusText); + + $exceptionHandler(resp); + } + + return $q.reject(resp); + } + } + + handleRequestFn.totalPendingRequests = 0; + + return handleRequestFn; + } + ]; + } + + /** @this */ + function $$TestabilityProvider() { + this.$get = ['$rootScope', '$browser', '$location', + function($rootScope, $browser, $location) { + + /** + * @name $testability + * + * @description + * The private $$testability service provides a collection of methods for use when debugging + * or by automated test and debugging tools. + */ + var testability = {}; + + /** + * @name $$testability#findBindings + * + * @description + * Returns an array of elements that are bound (via ng-bind or {{}}) + * to expressions matching the input. + * + * @param {Element} element The element root to search from. + * @param {string} expression The binding expression to match. + * @param {boolean} opt_exactMatch If true, only returns exact matches + * for the expression. Filters and whitespace are ignored. + */ + testability.findBindings = function(element, expression, opt_exactMatch) { + var bindings = element.getElementsByClassName('ng-binding'); + var matches = []; + forEach(bindings, function(binding) { + var dataBinding = angular.element(binding).data('$binding'); + if (dataBinding) { + forEach(dataBinding, function(bindingName) { + if (opt_exactMatch) { + var matcher = new RegExp('(^|\\s)' + escapeForRegexp(expression) + '(\\s|\\||$)'); + if (matcher.test(bindingName)) { + matches.push(binding); + } + } else { + if (bindingName.indexOf(expression) !== -1) { + matches.push(binding); + } + } + }); + } + }); + return matches; + }; + + /** + * @name $$testability#findModels + * + * @description + * Returns an array of elements that are two-way found via ng-model to + * expressions matching the input. + * + * @param {Element} element The element root to search from. + * @param {string} expression The model expression to match. + * @param {boolean} opt_exactMatch If true, only returns exact matches + * for the expression. + */ + testability.findModels = function(element, expression, opt_exactMatch) { + var prefixes = ['ng-', 'data-ng-', 'ng\\:']; + for (var p = 0; p < prefixes.length; ++p) { + var attributeEquals = opt_exactMatch ? '=' : '*='; + var selector = '[' + prefixes[p] + 'model' + attributeEquals + '"' + expression + '"]'; + var elements = element.querySelectorAll(selector); + if (elements.length) { + return elements; + } + } + }; + + /** + * @name $$testability#getLocation + * + * @description + * Shortcut for getting the location in a browser agnostic way. Returns + * the path, search, and hash. (e.g. /path?a=b#hash) + */ + testability.getLocation = function() { + return $location.url(); + }; + + /** + * @name $$testability#setLocation + * + * @description + * Shortcut for navigating to a location without doing a full page reload. + * + * @param {string} url The location url (path, search and hash, + * e.g. /path?a=b#hash) to go to. + */ + testability.setLocation = function(url) { + if (url !== $location.url()) { + $location.url(url); + $rootScope.$digest(); + } + }; + + /** + * @name $$testability#whenStable + * + * @description + * Calls the callback when $timeout and $http requests are completed. + * + * @param {function} callback + */ + testability.whenStable = function(callback) { + $browser.notifyWhenNoOutstandingRequests(callback); + }; + + return testability; + }]; + } + + /** @this */ + function $TimeoutProvider() { + this.$get = ['$rootScope', '$browser', '$q', '$$q', '$exceptionHandler', + function($rootScope, $browser, $q, $$q, $exceptionHandler) { + + var deferreds = {}; + + + /** + * @ngdoc service + * @name $timeout + * + * @description + * AngularJS's wrapper for `window.setTimeout`. The `fn` function is wrapped into a try/catch + * block and delegates any exceptions to + * {@link ng.$exceptionHandler $exceptionHandler} service. + * + * The return value of calling `$timeout` is a promise, which will be resolved when + * the delay has passed and the timeout function, if provided, is executed. + * + * To cancel a timeout request, call `$timeout.cancel(promise)`. + * + * In tests you can use {@link ngMock.$timeout `$timeout.flush()`} to + * synchronously flush the queue of deferred functions. + * + * If you only want a promise that will be resolved after some specified delay + * then you can call `$timeout` without the `fn` function. + * + * @param {function()=} fn A function, whose execution should be delayed. + * @param {number=} [delay=0] Delay in milliseconds. + * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise + * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. + * @param {...*=} Pass additional parameters to the executed function. + * @returns {Promise} Promise that will be resolved when the timeout is reached. The promise + * will be resolved with the return value of the `fn` function. + * + */ + function timeout(fn, delay, invokeApply) { + if (!isFunction(fn)) { + invokeApply = delay; + delay = fn; + fn = noop; + } + + var args = sliceArgs(arguments, 3), + skipApply = (isDefined(invokeApply) && !invokeApply), + deferred = (skipApply ? $$q : $q).defer(), + promise = deferred.promise, + timeoutId; + + timeoutId = $browser.defer(function() { + try { + deferred.resolve(fn.apply(null, args)); + } catch (e) { + deferred.reject(e); + $exceptionHandler(e); + } finally { + delete deferreds[promise.$$timeoutId]; + } + + if (!skipApply) $rootScope.$apply(); + }, delay); + + promise.$$timeoutId = timeoutId; + deferreds[timeoutId] = deferred; + + return promise; + } + + + /** + * @ngdoc method + * @name $timeout#cancel + * + * @description + * Cancels a task associated with the `promise`. As a result of this, the promise will be + * resolved with a rejection. + * + * @param {Promise=} promise Promise returned by the `$timeout` function. + * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully + * canceled. + */ + timeout.cancel = function(promise) { + if (promise && promise.$$timeoutId in deferreds) { + // Timeout cancels should not report an unhandled promise. + markQExceptionHandled(deferreds[promise.$$timeoutId].promise); + deferreds[promise.$$timeoutId].reject('canceled'); + delete deferreds[promise.$$timeoutId]; + return $browser.defer.cancel(promise.$$timeoutId); + } + return false; + }; + + return timeout; + }]; + } + +// NOTE: The usage of window and document instead of $window and $document here is +// deliberate. This service depends on the specific behavior of anchor nodes created by the +// browser (resolving and parsing URLs) that is unlikely to be provided by mock objects and +// cause us to break tests. In addition, when the browser resolves a URL for XHR, it +// doesn't know about mocked locations and resolves URLs to the real document - which is +// exactly the behavior needed here. There is little value is mocking these out for this +// service. + var urlParsingNode = window.document.createElement('a'); + var originUrl = urlResolve(window.location.href); + + + /** + * + * Implementation Notes for non-IE browsers + * ---------------------------------------- + * Assigning a URL to the href property of an anchor DOM node, even one attached to the DOM, + * results both in the normalizing and parsing of the URL. Normalizing means that a relative + * URL will be resolved into an absolute URL in the context of the application document. + * Parsing means that the anchor node's host, hostname, protocol, port, pathname and related + * properties are all populated to reflect the normalized URL. This approach has wide + * compatibility - Safari 1+, Mozilla 1+ etc. See + * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html + * + * Implementation Notes for IE + * --------------------------- + * IE <= 10 normalizes the URL when assigned to the anchor node similar to the other + * browsers. However, the parsed components will not be set if the URL assigned did not specify + * them. (e.g. if you assign a.href = "foo", then a.protocol, a.host, etc. will be empty.) We + * work around that by performing the parsing in a 2nd step by taking a previously normalized + * URL (e.g. by assigning to a.href) and assigning it a.href again. This correctly populates the + * properties such as protocol, hostname, port, etc. + * + * References: + * http://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement + * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html + * http://url.spec.whatwg.org/#urlutils + * https://github.com/angular/angular.js/pull/2902 + * http://james.padolsey.com/javascript/parsing-urls-with-the-dom/ + * + * @kind function + * @param {string} url The URL to be parsed. + * @description Normalizes and parses a URL. + * @returns {object} Returns the normalized URL as a dictionary. + * + * | member name | Description | + * |---------------|----------------| + * | href | A normalized version of the provided URL if it was not an absolute URL | + * | protocol | The protocol including the trailing colon | + * | host | The host and port (if the port is non-default) of the normalizedUrl | + * | search | The search params, minus the question mark | + * | hash | The hash string, minus the hash symbol + * | hostname | The hostname + * | port | The port, without ":" + * | pathname | The pathname, beginning with "/" + * + */ + function urlResolve(url) { + var href = url; + + // Support: IE 9-11 only + if (msie) { + // Normalize before parse. Refer Implementation Notes on why this is + // done in two steps on IE. + urlParsingNode.setAttribute('href', href); + href = urlParsingNode.href; + } + + urlParsingNode.setAttribute('href', href); + + // urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils + return { + href: urlParsingNode.href, + protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '', + host: urlParsingNode.host, + search: urlParsingNode.search ? urlParsingNode.search.replace(/^\?/, '') : '', + hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '', + hostname: urlParsingNode.hostname, + port: urlParsingNode.port, + pathname: (urlParsingNode.pathname.charAt(0) === '/') + ? urlParsingNode.pathname + : '/' + urlParsingNode.pathname + }; + } + + /** + * Parse a request URL and determine whether this is a same-origin request as the application document. + * + * @param {string|object} requestUrl The url of the request as a string that will be resolved + * or a parsed URL object. + * @returns {boolean} Whether the request is for the same origin as the application document. + */ + function urlIsSameOrigin(requestUrl) { + var parsed = (isString(requestUrl)) ? urlResolve(requestUrl) : requestUrl; + return (parsed.protocol === originUrl.protocol && + parsed.host === originUrl.host); + } + + /** + * @ngdoc service + * @name $window + * @this + * + * @description + * A reference to the browser's `window` object. While `window` + * is globally available in JavaScript, it causes testability problems, because + * it is a global variable. In AngularJS we always refer to it through the + * `$window` service, so it may be overridden, removed or mocked for testing. + * + * Expressions, like the one defined for the `ngClick` directive in the example + * below, are evaluated with respect to the current scope. Therefore, there is + * no risk of inadvertently coding in a dependency on a global value in such an + * expression. + * + * @example + + + +
    + + +
    +
    + + it('should display the greeting in the input box', function() { + element(by.model('greeting')).sendKeys('Hello, E2E Tests'); + // If we click the button it will block the test runner + // element(':button').click(); + }); + +
    + */ + function $WindowProvider() { + this.$get = valueFn(window); + } + + /** + * @name $$cookieReader + * @requires $document + * + * @description + * This is a private service for reading cookies used by $http and ngCookies + * + * @return {Object} a key/value map of the current cookies + */ + function $$CookieReader($document) { + var rawDocument = $document[0] || {}; + var lastCookies = {}; + var lastCookieString = ''; + + function safeGetCookie(rawDocument) { + try { + return rawDocument.cookie || ''; + } catch (e) { + return ''; + } + } + + function safeDecodeURIComponent(str) { + try { + return decodeURIComponent(str); + } catch (e) { + return str; + } + } + + return function() { + var cookieArray, cookie, i, index, name; + var currentCookieString = safeGetCookie(rawDocument); + + if (currentCookieString !== lastCookieString) { + lastCookieString = currentCookieString; + cookieArray = lastCookieString.split('; '); + lastCookies = {}; + + for (i = 0; i < cookieArray.length; i++) { + cookie = cookieArray[i]; + index = cookie.indexOf('='); + if (index > 0) { //ignore nameless cookies + name = safeDecodeURIComponent(cookie.substring(0, index)); + // the first value that is seen for a cookie is the most + // specific one. values for the same cookie name that + // follow are for less specific paths. + if (isUndefined(lastCookies[name])) { + lastCookies[name] = safeDecodeURIComponent(cookie.substring(index + 1)); + } + } + } + } + return lastCookies; + }; + } + + $$CookieReader.$inject = ['$document']; + + /** @this */ + function $$CookieReaderProvider() { + this.$get = $$CookieReader; + } + + /* global currencyFilter: true, + dateFilter: true, + filterFilter: true, + jsonFilter: true, + limitToFilter: true, + lowercaseFilter: true, + numberFilter: true, + orderByFilter: true, + uppercaseFilter: true, + */ + + /** + * @ngdoc provider + * @name $filterProvider + * @description + * + * Filters are just functions which transform input to an output. However filters need to be + * Dependency Injected. To achieve this a filter definition consists of a factory function which is + * annotated with dependencies and is responsible for creating a filter function. + * + *
    + * **Note:** Filter names must be valid AngularJS {@link expression} identifiers, such as `uppercase` or `orderBy`. + * Names with special characters, such as hyphens and dots, are not allowed. If you wish to namespace + * your filters, then you can use capitalization (`myappSubsectionFilterx`) or underscores + * (`myapp_subsection_filterx`). + *
    + * + * ```js + * // Filter registration + * function MyModule($provide, $filterProvider) { + * // create a service to demonstrate injection (not always needed) + * $provide.value('greet', function(name){ + * return 'Hello ' + name + '!'; + * }); + * + * // register a filter factory which uses the + * // greet service to demonstrate DI. + * $filterProvider.register('greet', function(greet){ + * // return the filter function which uses the greet service + * // to generate salutation + * return function(text) { + * // filters need to be forgiving so check input validity + * return text && greet(text) || text; + * }; + * }); + * } + * ``` + * + * The filter function is registered with the `$injector` under the filter name suffix with + * `Filter`. + * + * ```js + * it('should be the same instance', inject( + * function($filterProvider) { + * $filterProvider.register('reverse', function(){ + * return ...; + * }); + * }, + * function($filter, reverseFilter) { + * expect($filter('reverse')).toBe(reverseFilter); + * }); + * ``` + * + * + * For more information about how AngularJS filters work, and how to create your own filters, see + * {@link guide/filter Filters} in the AngularJS Developer Guide. + */ + + /** + * @ngdoc service + * @name $filter + * @kind function + * @description + * Filters are used for formatting data displayed to the user. + * + * They can be used in view templates, controllers or services. AngularJS comes + * with a collection of [built-in filters](api/ng/filter), but it is easy to + * define your own as well. + * + * The general syntax in templates is as follows: + * + * ```html + * {{ expression [| filter_name[:parameter_value] ... ] }} + * ``` + * + * @param {String} name Name of the filter function to retrieve + * @return {Function} the filter function + * @example + + +
    +

    {{ originalText }}

    +

    {{ filteredText }}

    +
    +
    + + + angular.module('filterExample', []) + .controller('MainCtrl', function($scope, $filter) { + $scope.originalText = 'hello'; + $scope.filteredText = $filter('uppercase')($scope.originalText); + }); + +
    + */ + $FilterProvider.$inject = ['$provide']; + /** @this */ + function $FilterProvider($provide) { + var suffix = 'Filter'; + + /** + * @ngdoc method + * @name $filterProvider#register + * @param {string|Object} name Name of the filter function, or an object map of filters where + * the keys are the filter names and the values are the filter factories. + * + *
    + * **Note:** Filter names must be valid AngularJS {@link expression} identifiers, such as `uppercase` or `orderBy`. + * Names with special characters, such as hyphens and dots, are not allowed. If you wish to namespace + * your filters, then you can use capitalization (`myappSubsectionFilterx`) or underscores + * (`myapp_subsection_filterx`). + *
    + * @param {Function} factory If the first argument was a string, a factory function for the filter to be registered. + * @returns {Object} Registered filter instance, or if a map of filters was provided then a map + * of the registered filter instances. + */ + function register(name, factory) { + if (isObject(name)) { + var filters = {}; + forEach(name, function(filter, key) { + filters[key] = register(key, filter); + }); + return filters; + } else { + return $provide.factory(name + suffix, factory); + } + } + this.register = register; + + this.$get = ['$injector', function($injector) { + return function(name) { + return $injector.get(name + suffix); + }; + }]; + + //////////////////////////////////////// + + /* global + currencyFilter: false, + dateFilter: false, + filterFilter: false, + jsonFilter: false, + limitToFilter: false, + lowercaseFilter: false, + numberFilter: false, + orderByFilter: false, + uppercaseFilter: false + */ + + register('currency', currencyFilter); + register('date', dateFilter); + register('filter', filterFilter); + register('json', jsonFilter); + register('limitTo', limitToFilter); + register('lowercase', lowercaseFilter); + register('number', numberFilter); + register('orderBy', orderByFilter); + register('uppercase', uppercaseFilter); + } + + /** + * @ngdoc filter + * @name filter + * @kind function + * + * @description + * Selects a subset of items from `array` and returns it as a new array. + * + * @param {Array} array The source array. + *
    + * **Note**: If the array contains objects that reference themselves, filtering is not possible. + *
    + * @param {string|Object|function()} expression The predicate to be used for selecting items from + * `array`. + * + * Can be one of: + * + * - `string`: The string is used for matching against the contents of the `array`. All strings or + * objects with string properties in `array` that match this string will be returned. This also + * applies to nested object properties. + * The predicate can be negated by prefixing the string with `!`. + * + * - `Object`: A pattern object can be used to filter specific properties on objects contained + * by `array`. For example `{name:"M", phone:"1"}` predicate will return an array of items + * which have property `name` containing "M" and property `phone` containing "1". A special + * property name (`$` by default) can be used (e.g. as in `{$: "text"}`) to accept a match + * against any property of the object or its nested object properties. That's equivalent to the + * simple substring match with a `string` as described above. The special property name can be + * overwritten, using the `anyPropertyKey` parameter. + * The predicate can be negated by prefixing the string with `!`. + * For example `{name: "!M"}` predicate will return an array of items which have property `name` + * not containing "M". + * + * Note that a named property will match properties on the same level only, while the special + * `$` property will match properties on the same level or deeper. E.g. an array item like + * `{name: {first: 'John', last: 'Doe'}}` will **not** be matched by `{name: 'John'}`, but + * **will** be matched by `{$: 'John'}`. + * + * - `function(value, index, array)`: A predicate function can be used to write arbitrary filters. + * The function is called for each element of the array, with the element, its index, and + * the entire array itself as arguments. + * + * The final result is an array of those elements that the predicate returned true for. + * + * @param {function(actual, expected)|true|false} [comparator] Comparator which is used in + * determining if values retrieved using `expression` (when it is not a function) should be + * considered a match based on the expected value (from the filter expression) and actual + * value (from the object in the array). + * + * Can be one of: + * + * - `function(actual, expected)`: + * The function will be given the object value and the predicate value to compare and + * should return true if both values should be considered equal. + * + * - `true`: A shorthand for `function(actual, expected) { return angular.equals(actual, expected)}`. + * This is essentially strict comparison of expected and actual. + * + * - `false`: A short hand for a function which will look for a substring match in a case + * insensitive way. Primitive values are converted to strings. Objects are not compared against + * primitives, unless they have a custom `toString` method (e.g. `Date` objects). + * + * + * Defaults to `false`. + * + * @param {string} [anyPropertyKey] The special property name that matches against any property. + * By default `$`. + * + * @example + + +
    + + + + + + + + +
    NamePhone
    {{friend.name}}{{friend.phone}}
    +
    +
    +
    +
    +
    + + + + + + +
    NamePhone
    {{friendObj.name}}{{friendObj.phone}}
    +
    + + var expectFriendNames = function(expectedNames, key) { + element.all(by.repeater(key + ' in friends').column(key + '.name')).then(function(arr) { + arr.forEach(function(wd, i) { + expect(wd.getText()).toMatch(expectedNames[i]); + }); + }); + }; + + it('should search across all fields when filtering with a string', function() { + var searchText = element(by.model('searchText')); + searchText.clear(); + searchText.sendKeys('m'); + expectFriendNames(['Mary', 'Mike', 'Adam'], 'friend'); + + searchText.clear(); + searchText.sendKeys('76'); + expectFriendNames(['John', 'Julie'], 'friend'); + }); + + it('should search in specific fields when filtering with a predicate object', function() { + var searchAny = element(by.model('search.$')); + searchAny.clear(); + searchAny.sendKeys('i'); + expectFriendNames(['Mary', 'Mike', 'Julie', 'Juliette'], 'friendObj'); + }); + it('should use a equal comparison when comparator is true', function() { + var searchName = element(by.model('search.name')); + var strict = element(by.model('strict')); + searchName.clear(); + searchName.sendKeys('Julie'); + strict.click(); + expectFriendNames(['Julie'], 'friendObj'); + }); + +
    + */ + + function filterFilter() { + return function(array, expression, comparator, anyPropertyKey) { + if (!isArrayLike(array)) { + if (array == null) { + return array; + } else { + throw minErr('filter')('notarray', 'Expected array but received: {0}', array); + } + } + + anyPropertyKey = anyPropertyKey || '$'; + var expressionType = getTypeForFilter(expression); + var predicateFn; + var matchAgainstAnyProp; + + switch (expressionType) { + case 'function': + predicateFn = expression; + break; + case 'boolean': + case 'null': + case 'number': + case 'string': + matchAgainstAnyProp = true; + // falls through + case 'object': + predicateFn = createPredicateFn(expression, comparator, anyPropertyKey, matchAgainstAnyProp); + break; + default: + return array; + } + + return Array.prototype.filter.call(array, predicateFn); + }; + } + +// Helper functions for `filterFilter` + function createPredicateFn(expression, comparator, anyPropertyKey, matchAgainstAnyProp) { + var shouldMatchPrimitives = isObject(expression) && (anyPropertyKey in expression); + var predicateFn; + + if (comparator === true) { + comparator = equals; + } else if (!isFunction(comparator)) { + comparator = function(actual, expected) { + if (isUndefined(actual)) { + // No substring matching against `undefined` + return false; + } + if ((actual === null) || (expected === null)) { + // No substring matching against `null`; only match against `null` + return actual === expected; + } + if (isObject(expected) || (isObject(actual) && !hasCustomToString(actual))) { + // Should not compare primitives against objects, unless they have custom `toString` method + return false; + } + + actual = lowercase('' + actual); + expected = lowercase('' + expected); + return actual.indexOf(expected) !== -1; + }; + } + + predicateFn = function(item) { + if (shouldMatchPrimitives && !isObject(item)) { + return deepCompare(item, expression[anyPropertyKey], comparator, anyPropertyKey, false); + } + return deepCompare(item, expression, comparator, anyPropertyKey, matchAgainstAnyProp); + }; + + return predicateFn; + } + + function deepCompare(actual, expected, comparator, anyPropertyKey, matchAgainstAnyProp, dontMatchWholeObject) { + var actualType = getTypeForFilter(actual); + var expectedType = getTypeForFilter(expected); + + if ((expectedType === 'string') && (expected.charAt(0) === '!')) { + return !deepCompare(actual, expected.substring(1), comparator, anyPropertyKey, matchAgainstAnyProp); + } else if (isArray(actual)) { + // In case `actual` is an array, consider it a match + // if ANY of it's items matches `expected` + return actual.some(function(item) { + return deepCompare(item, expected, comparator, anyPropertyKey, matchAgainstAnyProp); + }); + } + + switch (actualType) { + case 'object': + var key; + if (matchAgainstAnyProp) { + for (key in actual) { + // Under certain, rare, circumstances, key may not be a string and `charAt` will be undefined + // See: https://github.com/angular/angular.js/issues/15644 + if (key.charAt && (key.charAt(0) !== '$') && + deepCompare(actual[key], expected, comparator, anyPropertyKey, true)) { + return true; + } + } + return dontMatchWholeObject ? false : deepCompare(actual, expected, comparator, anyPropertyKey, false); + } else if (expectedType === 'object') { + for (key in expected) { + var expectedVal = expected[key]; + if (isFunction(expectedVal) || isUndefined(expectedVal)) { + continue; + } + + var matchAnyProperty = key === anyPropertyKey; + var actualVal = matchAnyProperty ? actual : actual[key]; + if (!deepCompare(actualVal, expectedVal, comparator, anyPropertyKey, matchAnyProperty, matchAnyProperty)) { + return false; + } + } + return true; + } else { + return comparator(actual, expected); + } + case 'function': + return false; + default: + return comparator(actual, expected); + } + } + +// Used for easily differentiating between `null` and actual `object` + function getTypeForFilter(val) { + return (val === null) ? 'null' : typeof val; + } + + var MAX_DIGITS = 22; + var DECIMAL_SEP = '.'; + var ZERO_CHAR = '0'; + + /** + * @ngdoc filter + * @name currency + * @kind function + * + * @description + * Formats a number as a currency (ie $1,234.56). When no currency symbol is provided, default + * symbol for current locale is used. + * + * @param {number} amount Input to filter. + * @param {string=} symbol Currency symbol or identifier to be displayed. + * @param {number=} fractionSize Number of decimal places to round the amount to, defaults to default max fraction size for current locale + * @returns {string} Formatted number. + * + * + * @example + + + +
    +
    + default currency symbol ($): {{amount | currency}}
    + custom currency identifier (USD$): {{amount | currency:"USD$"}}
    + no fractions (0): {{amount | currency:"USD$":0}} +
    +
    + + it('should init with 1234.56', function() { + expect(element(by.id('currency-default')).getText()).toBe('$1,234.56'); + expect(element(by.id('currency-custom')).getText()).toBe('USD$1,234.56'); + expect(element(by.id('currency-no-fractions')).getText()).toBe('USD$1,235'); + }); + it('should update', function() { + if (browser.params.browser === 'safari') { + // Safari does not understand the minus key. See + // https://github.com/angular/protractor/issues/481 + return; + } + element(by.model('amount')).clear(); + element(by.model('amount')).sendKeys('-1234'); + expect(element(by.id('currency-default')).getText()).toBe('-$1,234.00'); + expect(element(by.id('currency-custom')).getText()).toBe('-USD$1,234.00'); + expect(element(by.id('currency-no-fractions')).getText()).toBe('-USD$1,234'); + }); + +
    + */ + currencyFilter.$inject = ['$locale']; + function currencyFilter($locale) { + var formats = $locale.NUMBER_FORMATS; + return function(amount, currencySymbol, fractionSize) { + if (isUndefined(currencySymbol)) { + currencySymbol = formats.CURRENCY_SYM; + } + + if (isUndefined(fractionSize)) { + fractionSize = formats.PATTERNS[1].maxFrac; + } + + // If the currency symbol is empty, trim whitespace around the symbol + var currencySymbolRe = !currencySymbol ? /\s*\u00A4\s*/g : /\u00A4/g; + + // if null or undefined pass it through + return (amount == null) + ? amount + : formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, fractionSize). + replace(currencySymbolRe, currencySymbol); + }; + } + + /** + * @ngdoc filter + * @name number + * @kind function + * + * @description + * Formats a number as text. + * + * If the input is null or undefined, it will just be returned. + * If the input is infinite (Infinity or -Infinity), the Infinity symbol '∞' or '-∞' is returned, respectively. + * If the input is not a number an empty string is returned. + * + * + * @param {number|string} number Number to format. + * @param {(number|string)=} fractionSize Number of decimal places to round the number to. + * If this is not provided then the fraction size is computed from the current locale's number + * formatting pattern. In the case of the default locale, it will be 3. + * @returns {string} Number rounded to `fractionSize` appropriately formatted based on the current + * locale (e.g., in the en_US locale it will have "." as the decimal separator and + * include "," group separators after each third digit). + * + * @example + + + +
    +
    + Default formatting: {{val | number}}
    + No fractions: {{val | number:0}}
    + Negative number: {{-val | number:4}} +
    +
    + + it('should format numbers', function() { + expect(element(by.id('number-default')).getText()).toBe('1,234.568'); + expect(element(by.binding('val | number:0')).getText()).toBe('1,235'); + expect(element(by.binding('-val | number:4')).getText()).toBe('-1,234.5679'); + }); + + it('should update', function() { + element(by.model('val')).clear(); + element(by.model('val')).sendKeys('3374.333'); + expect(element(by.id('number-default')).getText()).toBe('3,374.333'); + expect(element(by.binding('val | number:0')).getText()).toBe('3,374'); + expect(element(by.binding('-val | number:4')).getText()).toBe('-3,374.3330'); + }); + +
    + */ + numberFilter.$inject = ['$locale']; + function numberFilter($locale) { + var formats = $locale.NUMBER_FORMATS; + return function(number, fractionSize) { + + // if null or undefined pass it through + return (number == null) + ? number + : formatNumber(number, formats.PATTERNS[0], formats.GROUP_SEP, formats.DECIMAL_SEP, + fractionSize); + }; + } + + /** + * Parse a number (as a string) into three components that can be used + * for formatting the number. + * + * (Significant bits of this parse algorithm came from https://github.com/MikeMcl/big.js/) + * + * @param {string} numStr The number to parse + * @return {object} An object describing this number, containing the following keys: + * - d : an array of digits containing leading zeros as necessary + * - i : the number of the digits in `d` that are to the left of the decimal point + * - e : the exponent for numbers that would need more than `MAX_DIGITS` digits in `d` + * + */ + function parse(numStr) { + var exponent = 0, digits, numberOfIntegerDigits; + var i, j, zeros; + + // Decimal point? + if ((numberOfIntegerDigits = numStr.indexOf(DECIMAL_SEP)) > -1) { + numStr = numStr.replace(DECIMAL_SEP, ''); + } + + // Exponential form? + if ((i = numStr.search(/e/i)) > 0) { + // Work out the exponent. + if (numberOfIntegerDigits < 0) numberOfIntegerDigits = i; + numberOfIntegerDigits += +numStr.slice(i + 1); + numStr = numStr.substring(0, i); + } else if (numberOfIntegerDigits < 0) { + // There was no decimal point or exponent so it is an integer. + numberOfIntegerDigits = numStr.length; + } + + // Count the number of leading zeros. + for (i = 0; numStr.charAt(i) === ZERO_CHAR; i++) { /* empty */ } + + if (i === (zeros = numStr.length)) { + // The digits are all zero. + digits = [0]; + numberOfIntegerDigits = 1; + } else { + // Count the number of trailing zeros + zeros--; + while (numStr.charAt(zeros) === ZERO_CHAR) zeros--; + + // Trailing zeros are insignificant so ignore them + numberOfIntegerDigits -= i; + digits = []; + // Convert string to array of digits without leading/trailing zeros. + for (j = 0; i <= zeros; i++, j++) { + digits[j] = +numStr.charAt(i); + } + } + + // If the number overflows the maximum allowed digits then use an exponent. + if (numberOfIntegerDigits > MAX_DIGITS) { + digits = digits.splice(0, MAX_DIGITS - 1); + exponent = numberOfIntegerDigits - 1; + numberOfIntegerDigits = 1; + } + + return { d: digits, e: exponent, i: numberOfIntegerDigits }; + } + + /** + * Round the parsed number to the specified number of decimal places + * This function changed the parsedNumber in-place + */ + function roundNumber(parsedNumber, fractionSize, minFrac, maxFrac) { + var digits = parsedNumber.d; + var fractionLen = digits.length - parsedNumber.i; + + // determine fractionSize if it is not specified; `+fractionSize` converts it to a number + fractionSize = (isUndefined(fractionSize)) ? Math.min(Math.max(minFrac, fractionLen), maxFrac) : +fractionSize; + + // The index of the digit to where rounding is to occur + var roundAt = fractionSize + parsedNumber.i; + var digit = digits[roundAt]; + + if (roundAt > 0) { + // Drop fractional digits beyond `roundAt` + digits.splice(Math.max(parsedNumber.i, roundAt)); + + // Set non-fractional digits beyond `roundAt` to 0 + for (var j = roundAt; j < digits.length; j++) { + digits[j] = 0; + } + } else { + // We rounded to zero so reset the parsedNumber + fractionLen = Math.max(0, fractionLen); + parsedNumber.i = 1; + digits.length = Math.max(1, roundAt = fractionSize + 1); + digits[0] = 0; + for (var i = 1; i < roundAt; i++) digits[i] = 0; + } + + if (digit >= 5) { + if (roundAt - 1 < 0) { + for (var k = 0; k > roundAt; k--) { + digits.unshift(0); + parsedNumber.i++; + } + digits.unshift(1); + parsedNumber.i++; + } else { + digits[roundAt - 1]++; + } + } + + // Pad out with zeros to get the required fraction length + for (; fractionLen < Math.max(0, fractionSize); fractionLen++) digits.push(0); + + + // Do any carrying, e.g. a digit was rounded up to 10 + var carry = digits.reduceRight(function(carry, d, i, digits) { + d = d + carry; + digits[i] = d % 10; + return Math.floor(d / 10); + }, 0); + if (carry) { + digits.unshift(carry); + parsedNumber.i++; + } + } + + /** + * Format a number into a string + * @param {number} number The number to format + * @param {{ + * minFrac, // the minimum number of digits required in the fraction part of the number + * maxFrac, // the maximum number of digits required in the fraction part of the number + * gSize, // number of digits in each group of separated digits + * lgSize, // number of digits in the last group of digits before the decimal separator + * negPre, // the string to go in front of a negative number (e.g. `-` or `(`)) + * posPre, // the string to go in front of a positive number + * negSuf, // the string to go after a negative number (e.g. `)`) + * posSuf // the string to go after a positive number + * }} pattern + * @param {string} groupSep The string to separate groups of number (e.g. `,`) + * @param {string} decimalSep The string to act as the decimal separator (e.g. `.`) + * @param {[type]} fractionSize The size of the fractional part of the number + * @return {string} The number formatted as a string + */ + function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { + + if (!(isString(number) || isNumber(number)) || isNaN(number)) return ''; + + var isInfinity = !isFinite(number); + var isZero = false; + var numStr = Math.abs(number) + '', + formattedText = '', + parsedNumber; + + if (isInfinity) { + formattedText = '\u221e'; + } else { + parsedNumber = parse(numStr); + + roundNumber(parsedNumber, fractionSize, pattern.minFrac, pattern.maxFrac); + + var digits = parsedNumber.d; + var integerLen = parsedNumber.i; + var exponent = parsedNumber.e; + var decimals = []; + isZero = digits.reduce(function(isZero, d) { return isZero && !d; }, true); + + // pad zeros for small numbers + while (integerLen < 0) { + digits.unshift(0); + integerLen++; + } + + // extract decimals digits + if (integerLen > 0) { + decimals = digits.splice(integerLen, digits.length); + } else { + decimals = digits; + digits = [0]; + } + + // format the integer digits with grouping separators + var groups = []; + if (digits.length >= pattern.lgSize) { + groups.unshift(digits.splice(-pattern.lgSize, digits.length).join('')); + } + while (digits.length > pattern.gSize) { + groups.unshift(digits.splice(-pattern.gSize, digits.length).join('')); + } + if (digits.length) { + groups.unshift(digits.join('')); + } + formattedText = groups.join(groupSep); + + // append the decimal digits + if (decimals.length) { + formattedText += decimalSep + decimals.join(''); + } + + if (exponent) { + formattedText += 'e+' + exponent; + } + } + if (number < 0 && !isZero) { + return pattern.negPre + formattedText + pattern.negSuf; + } else { + return pattern.posPre + formattedText + pattern.posSuf; + } + } + + function padNumber(num, digits, trim, negWrap) { + var neg = ''; + if (num < 0 || (negWrap && num <= 0)) { + if (negWrap) { + num = -num + 1; + } else { + num = -num; + neg = '-'; + } + } + num = '' + num; + while (num.length < digits) num = ZERO_CHAR + num; + if (trim) { + num = num.substr(num.length - digits); + } + return neg + num; + } + + + function dateGetter(name, size, offset, trim, negWrap) { + offset = offset || 0; + return function(date) { + var value = date['get' + name](); + if (offset > 0 || value > -offset) { + value += offset; + } + if (value === 0 && offset === -12) value = 12; + return padNumber(value, size, trim, negWrap); + }; + } + + function dateStrGetter(name, shortForm, standAlone) { + return function(date, formats) { + var value = date['get' + name](); + var propPrefix = (standAlone ? 'STANDALONE' : '') + (shortForm ? 'SHORT' : ''); + var get = uppercase(propPrefix + name); + + return formats[get][value]; + }; + } + + function timeZoneGetter(date, formats, offset) { + var zone = -1 * offset; + var paddedZone = (zone >= 0) ? '+' : ''; + + paddedZone += padNumber(Math[zone > 0 ? 'floor' : 'ceil'](zone / 60), 2) + + padNumber(Math.abs(zone % 60), 2); + + return paddedZone; + } + + function getFirstThursdayOfYear(year) { + // 0 = index of January + var dayOfWeekOnFirst = (new Date(year, 0, 1)).getDay(); + // 4 = index of Thursday (+1 to account for 1st = 5) + // 11 = index of *next* Thursday (+1 account for 1st = 12) + return new Date(year, 0, ((dayOfWeekOnFirst <= 4) ? 5 : 12) - dayOfWeekOnFirst); + } + + function getThursdayThisWeek(datetime) { + return new Date(datetime.getFullYear(), datetime.getMonth(), + // 4 = index of Thursday + datetime.getDate() + (4 - datetime.getDay())); + } + + function weekGetter(size) { + return function(date) { + var firstThurs = getFirstThursdayOfYear(date.getFullYear()), + thisThurs = getThursdayThisWeek(date); + + var diff = +thisThurs - +firstThurs, + result = 1 + Math.round(diff / 6.048e8); // 6.048e8 ms per week + + return padNumber(result, size); + }; + } + + function ampmGetter(date, formats) { + return date.getHours() < 12 ? formats.AMPMS[0] : formats.AMPMS[1]; + } + + function eraGetter(date, formats) { + return date.getFullYear() <= 0 ? formats.ERAS[0] : formats.ERAS[1]; + } + + function longEraGetter(date, formats) { + return date.getFullYear() <= 0 ? formats.ERANAMES[0] : formats.ERANAMES[1]; + } + + var DATE_FORMATS = { + yyyy: dateGetter('FullYear', 4, 0, false, true), + yy: dateGetter('FullYear', 2, 0, true, true), + y: dateGetter('FullYear', 1, 0, false, true), + MMMM: dateStrGetter('Month'), + MMM: dateStrGetter('Month', true), + MM: dateGetter('Month', 2, 1), + M: dateGetter('Month', 1, 1), + LLLL: dateStrGetter('Month', false, true), + dd: dateGetter('Date', 2), + d: dateGetter('Date', 1), + HH: dateGetter('Hours', 2), + H: dateGetter('Hours', 1), + hh: dateGetter('Hours', 2, -12), + h: dateGetter('Hours', 1, -12), + mm: dateGetter('Minutes', 2), + m: dateGetter('Minutes', 1), + ss: dateGetter('Seconds', 2), + s: dateGetter('Seconds', 1), + // while ISO 8601 requires fractions to be prefixed with `.` or `,` + // we can be just safely rely on using `sss` since we currently don't support single or two digit fractions + sss: dateGetter('Milliseconds', 3), + EEEE: dateStrGetter('Day'), + EEE: dateStrGetter('Day', true), + a: ampmGetter, + Z: timeZoneGetter, + ww: weekGetter(2), + w: weekGetter(1), + G: eraGetter, + GG: eraGetter, + GGG: eraGetter, + GGGG: longEraGetter + }; + + var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))([\s\S]*)/, + NUMBER_STRING = /^-?\d+$/; + + /** + * @ngdoc filter + * @name date + * @kind function + * + * @description + * Formats `date` to a string based on the requested `format`. + * + * `format` string can be composed of the following elements: + * + * * `'yyyy'`: 4 digit representation of year (e.g. AD 1 => 0001, AD 2010 => 2010) + * * `'yy'`: 2 digit representation of year, padded (00-99). (e.g. AD 2001 => 01, AD 2010 => 10) + * * `'y'`: 1 digit representation of year, e.g. (AD 1 => 1, AD 199 => 199) + * * `'MMMM'`: Month in year (January-December) + * * `'MMM'`: Month in year (Jan-Dec) + * * `'MM'`: Month in year, padded (01-12) + * * `'M'`: Month in year (1-12) + * * `'LLLL'`: Stand-alone month in year (January-December) + * * `'dd'`: Day in month, padded (01-31) + * * `'d'`: Day in month (1-31) + * * `'EEEE'`: Day in Week,(Sunday-Saturday) + * * `'EEE'`: Day in Week, (Sun-Sat) + * * `'HH'`: Hour in day, padded (00-23) + * * `'H'`: Hour in day (0-23) + * * `'hh'`: Hour in AM/PM, padded (01-12) + * * `'h'`: Hour in AM/PM, (1-12) + * * `'mm'`: Minute in hour, padded (00-59) + * * `'m'`: Minute in hour (0-59) + * * `'ss'`: Second in minute, padded (00-59) + * * `'s'`: Second in minute (0-59) + * * `'sss'`: Millisecond in second, padded (000-999) + * * `'a'`: AM/PM marker + * * `'Z'`: 4 digit (+sign) representation of the timezone offset (-1200-+1200) + * * `'ww'`: Week of year, padded (00-53). Week 01 is the week with the first Thursday of the year + * * `'w'`: Week of year (0-53). Week 1 is the week with the first Thursday of the year + * * `'G'`, `'GG'`, `'GGG'`: The abbreviated form of the era string (e.g. 'AD') + * * `'GGGG'`: The long form of the era string (e.g. 'Anno Domini') + * + * `format` string can also be one of the following predefined + * {@link guide/i18n localizable formats}: + * + * * `'medium'`: equivalent to `'MMM d, y h:mm:ss a'` for en_US locale + * (e.g. Sep 3, 2010 12:05:08 PM) + * * `'short'`: equivalent to `'M/d/yy h:mm a'` for en_US locale (e.g. 9/3/10 12:05 PM) + * * `'fullDate'`: equivalent to `'EEEE, MMMM d, y'` for en_US locale + * (e.g. Friday, September 3, 2010) + * * `'longDate'`: equivalent to `'MMMM d, y'` for en_US locale (e.g. September 3, 2010) + * * `'mediumDate'`: equivalent to `'MMM d, y'` for en_US locale (e.g. Sep 3, 2010) + * * `'shortDate'`: equivalent to `'M/d/yy'` for en_US locale (e.g. 9/3/10) + * * `'mediumTime'`: equivalent to `'h:mm:ss a'` for en_US locale (e.g. 12:05:08 PM) + * * `'shortTime'`: equivalent to `'h:mm a'` for en_US locale (e.g. 12:05 PM) + * + * `format` string can contain literal values. These need to be escaped by surrounding with single quotes (e.g. + * `"h 'in the morning'"`). In order to output a single quote, escape it - i.e., two single quotes in a sequence + * (e.g. `"h 'o''clock'"`). + * + * Any other characters in the `format` string will be output as-is. + * + * @param {(Date|number|string)} date Date to format either as Date object, milliseconds (string or + * number) or various ISO 8601 datetime string formats (e.g. yyyy-MM-ddTHH:mm:ss.sssZ and its + * shorter versions like yyyy-MM-ddTHH:mmZ, yyyy-MM-dd or yyyyMMddTHHmmssZ). If no timezone is + * specified in the string input, the time is considered to be in the local timezone. + * @param {string=} format Formatting rules (see Description). If not specified, + * `mediumDate` is used. + * @param {string=} timezone Timezone to be used for formatting. It understands UTC/GMT and the + * continental US time zone abbreviations, but for general use, use a time zone offset, for + * example, `'+0430'` (4 hours, 30 minutes east of the Greenwich meridian) + * If not specified, the timezone of the browser will be used. + * @returns {string} Formatted string or the input if input is not recognized as date/millis. + * + * @example + + + {{1288323623006 | date:'medium'}}: + {{1288323623006 | date:'medium'}}
    + {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}: + {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}
    + {{1288323623006 | date:'MM/dd/yyyy @ h:mma'}}: + {{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}
    + {{1288323623006 | date:"MM/dd/yyyy 'at' h:mma"}}: + {{'1288323623006' | date:"MM/dd/yyyy 'at' h:mma"}}
    +
    + + it('should format date', function() { + expect(element(by.binding("1288323623006 | date:'medium'")).getText()). + toMatch(/Oct 2\d, 2010 \d{1,2}:\d{2}:\d{2} (AM|PM)/); + expect(element(by.binding("1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'")).getText()). + toMatch(/2010-10-2\d \d{2}:\d{2}:\d{2} (-|\+)?\d{4}/); + expect(element(by.binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")).getText()). + toMatch(/10\/2\d\/2010 @ \d{1,2}:\d{2}(AM|PM)/); + expect(element(by.binding("'1288323623006' | date:\"MM/dd/yyyy 'at' h:mma\"")).getText()). + toMatch(/10\/2\d\/2010 at \d{1,2}:\d{2}(AM|PM)/); + }); + +
    + */ + dateFilter.$inject = ['$locale']; + function dateFilter($locale) { + + + var R_ISO8601_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/; + // 1 2 3 4 5 6 7 8 9 10 11 + function jsonStringToDate(string) { + var match; + if ((match = string.match(R_ISO8601_STR))) { + var date = new Date(0), + tzHour = 0, + tzMin = 0, + dateSetter = match[8] ? date.setUTCFullYear : date.setFullYear, + timeSetter = match[8] ? date.setUTCHours : date.setHours; + + if (match[9]) { + tzHour = toInt(match[9] + match[10]); + tzMin = toInt(match[9] + match[11]); + } + dateSetter.call(date, toInt(match[1]), toInt(match[2]) - 1, toInt(match[3])); + var h = toInt(match[4] || 0) - tzHour; + var m = toInt(match[5] || 0) - tzMin; + var s = toInt(match[6] || 0); + var ms = Math.round(parseFloat('0.' + (match[7] || 0)) * 1000); + timeSetter.call(date, h, m, s, ms); + return date; + } + return string; + } + + + return function(date, format, timezone) { + var text = '', + parts = [], + fn, match; + + format = format || 'mediumDate'; + format = $locale.DATETIME_FORMATS[format] || format; + if (isString(date)) { + date = NUMBER_STRING.test(date) ? toInt(date) : jsonStringToDate(date); + } + + if (isNumber(date)) { + date = new Date(date); + } + + if (!isDate(date) || !isFinite(date.getTime())) { + return date; + } + + while (format) { + match = DATE_FORMATS_SPLIT.exec(format); + if (match) { + parts = concat(parts, match, 1); + format = parts.pop(); + } else { + parts.push(format); + format = null; + } + } + + var dateTimezoneOffset = date.getTimezoneOffset(); + if (timezone) { + dateTimezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset); + date = convertTimezoneToLocal(date, timezone, true); + } + forEach(parts, function(value) { + fn = DATE_FORMATS[value]; + text += fn ? fn(date, $locale.DATETIME_FORMATS, dateTimezoneOffset) + : value === '\'\'' ? '\'' : value.replace(/(^'|'$)/g, '').replace(/''/g, '\''); + }); + + return text; + }; + } + + + /** + * @ngdoc filter + * @name json + * @kind function + * + * @description + * Allows you to convert a JavaScript object into JSON string. + * + * This filter is mostly useful for debugging. When using the double curly {{value}} notation + * the binding is automatically converted to JSON. + * + * @param {*} object Any JavaScript object (including arrays and primitive types) to filter. + * @param {number=} spacing The number of spaces to use per indentation, defaults to 2. + * @returns {string} JSON string. + * + * + * @example + + +
    {{ {'name':'value'} | json }}
    +
    {{ {'name':'value'} | json:4 }}
    +
    + + it('should jsonify filtered objects', function() { + expect(element(by.id('default-spacing')).getText()).toMatch(/\{\n {2}"name": ?"value"\n}/); + expect(element(by.id('custom-spacing')).getText()).toMatch(/\{\n {4}"name": ?"value"\n}/); + }); + +
    + * + */ + function jsonFilter() { + return function(object, spacing) { + if (isUndefined(spacing)) { + spacing = 2; + } + return toJson(object, spacing); + }; + } + + + /** + * @ngdoc filter + * @name lowercase + * @kind function + * @description + * Converts string to lowercase. + * + * See the {@link ng.uppercase uppercase filter documentation} for a functionally identical example. + * + * @see angular.lowercase + */ + var lowercaseFilter = valueFn(lowercase); + + + /** + * @ngdoc filter + * @name uppercase + * @kind function + * @description + * Converts string to uppercase. + * @example + + + +
    + +

    {{title}}

    + +

    {{title | uppercase}}

    +
    +
    +
    + */ + var uppercaseFilter = valueFn(uppercase); + + /** + * @ngdoc filter + * @name limitTo + * @kind function + * + * @description + * Creates a new array or string containing only a specified number of elements. The elements are + * taken from either the beginning or the end of the source array, string or number, as specified by + * the value and sign (positive or negative) of `limit`. Other array-like objects are also supported + * (e.g. array subclasses, NodeLists, jqLite/jQuery collections etc). If a number is used as input, + * it is converted to a string. + * + * @param {Array|ArrayLike|string|number} input - Array/array-like, string or number to be limited. + * @param {string|number} limit - The length of the returned array or string. If the `limit` number + * is positive, `limit` number of items from the beginning of the source array/string are copied. + * If the number is negative, `limit` number of items from the end of the source array/string + * are copied. The `limit` will be trimmed if it exceeds `array.length`. If `limit` is undefined, + * the input will be returned unchanged. + * @param {(string|number)=} begin - Index at which to begin limitation. As a negative index, + * `begin` indicates an offset from the end of `input`. Defaults to `0`. + * @returns {Array|string} A new sub-array or substring of length `limit` or less if the input had + * less than `limit` elements. + * + * @example + + + +
    + +

    Output numbers: {{ numbers | limitTo:numLimit }}

    + +

    Output letters: {{ letters | limitTo:letterLimit }}

    + +

    Output long number: {{ longNumber | limitTo:longNumberLimit }}

    +
    +
    + + var numLimitInput = element(by.model('numLimit')); + var letterLimitInput = element(by.model('letterLimit')); + var longNumberLimitInput = element(by.model('longNumberLimit')); + var limitedNumbers = element(by.binding('numbers | limitTo:numLimit')); + var limitedLetters = element(by.binding('letters | limitTo:letterLimit')); + var limitedLongNumber = element(by.binding('longNumber | limitTo:longNumberLimit')); + + it('should limit the number array to first three items', function() { + expect(numLimitInput.getAttribute('value')).toBe('3'); + expect(letterLimitInput.getAttribute('value')).toBe('3'); + expect(longNumberLimitInput.getAttribute('value')).toBe('3'); + expect(limitedNumbers.getText()).toEqual('Output numbers: [1,2,3]'); + expect(limitedLetters.getText()).toEqual('Output letters: abc'); + expect(limitedLongNumber.getText()).toEqual('Output long number: 234'); + }); + + // There is a bug in safari and protractor that doesn't like the minus key + // it('should update the output when -3 is entered', function() { + // numLimitInput.clear(); + // numLimitInput.sendKeys('-3'); + // letterLimitInput.clear(); + // letterLimitInput.sendKeys('-3'); + // longNumberLimitInput.clear(); + // longNumberLimitInput.sendKeys('-3'); + // expect(limitedNumbers.getText()).toEqual('Output numbers: [7,8,9]'); + // expect(limitedLetters.getText()).toEqual('Output letters: ghi'); + // expect(limitedLongNumber.getText()).toEqual('Output long number: 342'); + // }); + + it('should not exceed the maximum size of input array', function() { + numLimitInput.clear(); + numLimitInput.sendKeys('100'); + letterLimitInput.clear(); + letterLimitInput.sendKeys('100'); + longNumberLimitInput.clear(); + longNumberLimitInput.sendKeys('100'); + expect(limitedNumbers.getText()).toEqual('Output numbers: [1,2,3,4,5,6,7,8,9]'); + expect(limitedLetters.getText()).toEqual('Output letters: abcdefghi'); + expect(limitedLongNumber.getText()).toEqual('Output long number: 2345432342'); + }); + +
    + */ + function limitToFilter() { + return function(input, limit, begin) { + if (Math.abs(Number(limit)) === Infinity) { + limit = Number(limit); + } else { + limit = toInt(limit); + } + if (isNumberNaN(limit)) return input; + + if (isNumber(input)) input = input.toString(); + if (!isArrayLike(input)) return input; + + begin = (!begin || isNaN(begin)) ? 0 : toInt(begin); + begin = (begin < 0) ? Math.max(0, input.length + begin) : begin; + + if (limit >= 0) { + return sliceFn(input, begin, begin + limit); + } else { + if (begin === 0) { + return sliceFn(input, limit, input.length); + } else { + return sliceFn(input, Math.max(0, begin + limit), begin); + } + } + }; + } + + function sliceFn(input, begin, end) { + if (isString(input)) return input.slice(begin, end); + + return slice.call(input, begin, end); + } + + /** + * @ngdoc filter + * @name orderBy + * @kind function + * + * @description + * Returns an array containing the items from the specified `collection`, ordered by a `comparator` + * function based on the values computed using the `expression` predicate. + * + * For example, `[{id: 'foo'}, {id: 'bar'}] | orderBy:'id'` would result in + * `[{id: 'bar'}, {id: 'foo'}]`. + * + * The `collection` can be an Array or array-like object (e.g. NodeList, jQuery object, TypedArray, + * String, etc). + * + * The `expression` can be a single predicate, or a list of predicates each serving as a tie-breaker + * for the preceding one. The `expression` is evaluated against each item and the output is used + * for comparing with other items. + * + * You can change the sorting order by setting `reverse` to `true`. By default, items are sorted in + * ascending order. + * + * The comparison is done using the `comparator` function. If none is specified, a default, built-in + * comparator is used (see below for details - in a nutshell, it compares numbers numerically and + * strings alphabetically). + * + * ### Under the hood + * + * Ordering the specified `collection` happens in two phases: + * + * 1. All items are passed through the predicate (or predicates), and the returned values are saved + * along with their type (`string`, `number` etc). For example, an item `{label: 'foo'}`, passed + * through a predicate that extracts the value of the `label` property, would be transformed to: + * ``` + * { + * value: 'foo', + * type: 'string', + * index: ... + * } + * ``` + * 2. The comparator function is used to sort the items, based on the derived values, types and + * indices. + * + * If you use a custom comparator, it will be called with pairs of objects of the form + * `{value: ..., type: '...', index: ...}` and is expected to return `0` if the objects are equal + * (as far as the comparator is concerned), `-1` if the 1st one should be ranked higher than the + * second, or `1` otherwise. + * + * In order to ensure that the sorting will be deterministic across platforms, if none of the + * specified predicates can distinguish between two items, `orderBy` will automatically introduce a + * dummy predicate that returns the item's index as `value`. + * (If you are using a custom comparator, make sure it can handle this predicate as well.) + * + * If a custom comparator still can't distinguish between two items, then they will be sorted based + * on their index using the built-in comparator. + * + * Finally, in an attempt to simplify things, if a predicate returns an object as the extracted + * value for an item, `orderBy` will try to convert that object to a primitive value, before passing + * it to the comparator. The following rules govern the conversion: + * + * 1. If the object has a `valueOf()` method that returns a primitive, its return value will be + * used instead.
    + * (If the object has a `valueOf()` method that returns another object, then the returned object + * will be used in subsequent steps.) + * 2. If the object has a custom `toString()` method (i.e. not the one inherited from `Object`) that + * returns a primitive, its return value will be used instead.
    + * (If the object has a `toString()` method that returns another object, then the returned object + * will be used in subsequent steps.) + * 3. No conversion; the object itself is used. + * + * ### The default comparator + * + * The default, built-in comparator should be sufficient for most usecases. In short, it compares + * numbers numerically, strings alphabetically (and case-insensitively), for objects falls back to + * using their index in the original collection, and sorts values of different types by type. + * + * More specifically, it follows these steps to determine the relative order of items: + * + * 1. If the compared values are of different types, compare the types themselves alphabetically. + * 2. If both values are of type `string`, compare them alphabetically in a case- and + * locale-insensitive way. + * 3. If both values are objects, compare their indices instead. + * 4. Otherwise, return: + * - `0`, if the values are equal (by strict equality comparison, i.e. using `===`). + * - `-1`, if the 1st value is "less than" the 2nd value (compared using the `<` operator). + * - `1`, otherwise. + * + * **Note:** If you notice numbers not being sorted as expected, make sure they are actually being + * saved as numbers and not strings. + * **Note:** For the purpose of sorting, `null` values are treated as the string `'null'` (i.e. + * `type: 'string'`, `value: 'null'`). This may cause unexpected sort order relative to + * other values. + * + * @param {Array|ArrayLike} collection - The collection (array or array-like object) to sort. + * @param {(Function|string|Array.)=} expression - A predicate (or list of + * predicates) to be used by the comparator to determine the order of elements. + * + * Can be one of: + * + * - `Function`: A getter function. This function will be called with each item as argument and + * the return value will be used for sorting. + * - `string`: An AngularJS expression. This expression will be evaluated against each item and the + * result will be used for sorting. For example, use `'label'` to sort by a property called + * `label` or `'label.substring(0, 3)'` to sort by the first 3 characters of the `label` + * property.
    + * (The result of a constant expression is interpreted as a property name to be used for + * comparison. For example, use `'"special name"'` (note the extra pair of quotes) to sort by a + * property called `special name`.)
    + * An expression can be optionally prefixed with `+` or `-` to control the sorting direction, + * ascending or descending. For example, `'+label'` or `'-label'`. If no property is provided, + * (e.g. `'+'` or `'-'`), the collection element itself is used in comparisons. + * - `Array`: An array of function and/or string predicates. If a predicate cannot determine the + * relative order of two items, the next predicate is used as a tie-breaker. + * + * **Note:** If the predicate is missing or empty then it defaults to `'+'`. + * + * @param {boolean=} reverse - If `true`, reverse the sorting order. + * @param {(Function)=} comparator - The comparator function used to determine the relative order of + * value pairs. If omitted, the built-in comparator will be used. + * + * @returns {Array} - The sorted array. + * + * + * @example + * ### Ordering a table with `ngRepeat` + * + * The example below demonstrates a simple {@link ngRepeat ngRepeat}, where the data is sorted by + * age in descending order (expression is set to `'-age'`). The `comparator` is not set, which means + * it defaults to the built-in comparator. + * + + +
    + + + + + + + + + + + +
    NamePhone NumberAge
    {{friend.name}}{{friend.phone}}{{friend.age}}
    +
    +
    + + angular.module('orderByExample1', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.friends = [ + {name: 'John', phone: '555-1212', age: 10}, + {name: 'Mary', phone: '555-9876', age: 19}, + {name: 'Mike', phone: '555-4321', age: 21}, + {name: 'Adam', phone: '555-5678', age: 35}, + {name: 'Julie', phone: '555-8765', age: 29} + ]; + }]); + + + .friends { + border-collapse: collapse; + } + + .friends th { + border-bottom: 1px solid; + } + .friends td, .friends th { + border-left: 1px solid; + padding: 5px 10px; + } + .friends td:first-child, .friends th:first-child { + border-left: none; + } + + + // Element locators + var names = element.all(by.repeater('friends').column('friend.name')); + + it('should sort friends by age in reverse order', function() { + expect(names.get(0).getText()).toBe('Adam'); + expect(names.get(1).getText()).toBe('Julie'); + expect(names.get(2).getText()).toBe('Mike'); + expect(names.get(3).getText()).toBe('Mary'); + expect(names.get(4).getText()).toBe('John'); + }); + +
    + *
    * - * To be secure by default, you want to ensure that any such bindings are disallowed unless you can - * determine that something explicitly says it's safe to use a value for binding in that - * context. You can then audit your code (a simple grep would do) to ensure that this is only done - * for those values that you can easily tell are safe - because they were received from your server, - * sanitized by your library, etc. You can organize your codebase to help with this - perhaps - * allowing only the files in a specific directory to do this. Ensuring that the internal API - * exposed by that code doesn't markup arbitrary values as safe then becomes a more manageable task. + * @example + * ### Changing parameters dynamically * - * In the case of AngularJS' SCE service, one uses {@link ng.$sce#trustAs $sce.trustAs} - * (and shorthand methods such as {@link ng.$sce#trustAsHtml $sce.trustAsHtml}, etc.) to - * obtain values that will be accepted by SCE / privileged contexts. + * All parameters can be changed dynamically. The next example shows how you can make the columns of + * a table sortable, by binding the `expression` and `reverse` parameters to scope properties. + * + + +
    +
    Sort by = {{propertyName}}; reverse = {{reverse}}
    +
    + +
    + + + + + + + + + + + +
    + + + + + + + + +
    {{friend.name}}{{friend.phone}}{{friend.age}}
    +
    +
    + + angular.module('orderByExample2', []) + .controller('ExampleController', ['$scope', function($scope) { + var friends = [ + {name: 'John', phone: '555-1212', age: 10}, + {name: 'Mary', phone: '555-9876', age: 19}, + {name: 'Mike', phone: '555-4321', age: 21}, + {name: 'Adam', phone: '555-5678', age: 35}, + {name: 'Julie', phone: '555-8765', age: 29} + ]; + + $scope.propertyName = 'age'; + $scope.reverse = true; + $scope.friends = friends; + + $scope.sortBy = function(propertyName) { + $scope.reverse = ($scope.propertyName === propertyName) ? !$scope.reverse : false; + $scope.propertyName = propertyName; + }; + }]); + + + .friends { + border-collapse: collapse; + } + + .friends th { + border-bottom: 1px solid; + } + .friends td, .friends th { + border-left: 1px solid; + padding: 5px 10px; + } + .friends td:first-child, .friends th:first-child { + border-left: none; + } + + .sortorder:after { + content: '\25b2'; // BLACK UP-POINTING TRIANGLE + } + .sortorder.reverse:after { + content: '\25bc'; // BLACK DOWN-POINTING TRIANGLE + } + + + // Element locators + var unsortButton = element(by.partialButtonText('unsorted')); + var nameHeader = element(by.partialButtonText('Name')); + var phoneHeader = element(by.partialButtonText('Phone')); + var ageHeader = element(by.partialButtonText('Age')); + var firstName = element(by.repeater('friends').column('friend.name').row(0)); + var lastName = element(by.repeater('friends').column('friend.name').row(4)); + + it('should sort friends by some property, when clicking on the column header', function() { + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('John'); + + phoneHeader.click(); + expect(firstName.getText()).toBe('John'); + expect(lastName.getText()).toBe('Mary'); + + nameHeader.click(); + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('Mike'); + + ageHeader.click(); + expect(firstName.getText()).toBe('John'); + expect(lastName.getText()).toBe('Adam'); + }); + + it('should sort friends in reverse order, when clicking on the same column', function() { + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('John'); + + ageHeader.click(); + expect(firstName.getText()).toBe('John'); + expect(lastName.getText()).toBe('Adam'); + + ageHeader.click(); + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('John'); + }); + + it('should restore the original order, when clicking "Set to unsorted"', function() { + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('John'); + + unsortButton.click(); + expect(firstName.getText()).toBe('John'); + expect(lastName.getText()).toBe('Julie'); + }); + +
    + *
    + * + * @example + * ### Using `orderBy` inside a controller + * + * It is also possible to call the `orderBy` filter manually, by injecting `orderByFilter`, and + * calling it with the desired parameters. (Alternatively, you could inject the `$filter` factory + * and retrieve the `orderBy` filter with `$filter('orderBy')`.) + * + + +
    +
    Sort by = {{propertyName}}; reverse = {{reverse}}
    +
    + +
    + + + + + + + + + + + +
    + + + + + + + + +
    {{friend.name}}{{friend.phone}}{{friend.age}}
    +
    +
    + + angular.module('orderByExample3', []) + .controller('ExampleController', ['$scope', 'orderByFilter', function($scope, orderBy) { + var friends = [ + {name: 'John', phone: '555-1212', age: 10}, + {name: 'Mary', phone: '555-9876', age: 19}, + {name: 'Mike', phone: '555-4321', age: 21}, + {name: 'Adam', phone: '555-5678', age: 35}, + {name: 'Julie', phone: '555-8765', age: 29} + ]; + + $scope.propertyName = 'age'; + $scope.reverse = true; + $scope.friends = orderBy(friends, $scope.propertyName, $scope.reverse); + + $scope.sortBy = function(propertyName) { + $scope.reverse = (propertyName !== null && $scope.propertyName === propertyName) + ? !$scope.reverse : false; + $scope.propertyName = propertyName; + $scope.friends = orderBy(friends, $scope.propertyName, $scope.reverse); + }; + }]); + + + .friends { + border-collapse: collapse; + } + + .friends th { + border-bottom: 1px solid; + } + .friends td, .friends th { + border-left: 1px solid; + padding: 5px 10px; + } + .friends td:first-child, .friends th:first-child { + border-left: none; + } + + .sortorder:after { + content: '\25b2'; // BLACK UP-POINTING TRIANGLE + } + .sortorder.reverse:after { + content: '\25bc'; // BLACK DOWN-POINTING TRIANGLE + } + + + // Element locators + var unsortButton = element(by.partialButtonText('unsorted')); + var nameHeader = element(by.partialButtonText('Name')); + var phoneHeader = element(by.partialButtonText('Phone')); + var ageHeader = element(by.partialButtonText('Age')); + var firstName = element(by.repeater('friends').column('friend.name').row(0)); + var lastName = element(by.repeater('friends').column('friend.name').row(4)); + + it('should sort friends by some property, when clicking on the column header', function() { + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('John'); + + phoneHeader.click(); + expect(firstName.getText()).toBe('John'); + expect(lastName.getText()).toBe('Mary'); + + nameHeader.click(); + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('Mike'); + + ageHeader.click(); + expect(firstName.getText()).toBe('John'); + expect(lastName.getText()).toBe('Adam'); + }); + + it('should sort friends in reverse order, when clicking on the same column', function() { + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('John'); + + ageHeader.click(); + expect(firstName.getText()).toBe('John'); + expect(lastName.getText()).toBe('Adam'); + + ageHeader.click(); + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('John'); + }); + + it('should restore the original order, when clicking "Set to unsorted"', function() { + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('John'); + + unsortButton.click(); + expect(firstName.getText()).toBe('John'); + expect(lastName.getText()).toBe('Julie'); + }); + +
    + *
    + * + * @example + * ### Using a custom comparator + * + * If you have very specific requirements about the way items are sorted, you can pass your own + * comparator function. For example, you might need to compare some strings in a locale-sensitive + * way. (When specifying a custom comparator, you also need to pass a value for the `reverse` + * argument - passing `false` retains the default sorting order, i.e. ascending.) + * + + +
    +
    +

    Locale-sensitive Comparator

    + + + + + + + + + +
    NameFavorite Letter
    {{friend.name}}{{friend.favoriteLetter}}
    +
    +
    +

    Default Comparator

    + + + + + + + + + +
    NameFavorite Letter
    {{friend.name}}{{friend.favoriteLetter}}
    +
    +
    +
    + + angular.module('orderByExample4', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.friends = [ + {name: 'John', favoriteLetter: 'Ä'}, + {name: 'Mary', favoriteLetter: 'Ü'}, + {name: 'Mike', favoriteLetter: 'Ö'}, + {name: 'Adam', favoriteLetter: 'H'}, + {name: 'Julie', favoriteLetter: 'Z'} + ]; + + $scope.localeSensitiveComparator = function(v1, v2) { + // If we don't get strings, just compare by index + if (v1.type !== 'string' || v2.type !== 'string') { + return (v1.index < v2.index) ? -1 : 1; + } + + // Compare strings alphabetically, taking locale into account + return v1.value.localeCompare(v2.value); + }; + }]); + + + .friends-container { + display: inline-block; + margin: 0 30px; + } + + .friends { + border-collapse: collapse; + } + + .friends th { + border-bottom: 1px solid; + } + .friends td, .friends th { + border-left: 1px solid; + padding: 5px 10px; + } + .friends td:first-child, .friends th:first-child { + border-left: none; + } + + + // Element locators + var container = element(by.css('.custom-comparator')); + var names = container.all(by.repeater('friends').column('friend.name')); + + it('should sort friends by favorite letter (in correct alphabetical order)', function() { + expect(names.get(0).getText()).toBe('John'); + expect(names.get(1).getText()).toBe('Adam'); + expect(names.get(2).getText()).toBe('Mike'); + expect(names.get(3).getText()).toBe('Mary'); + expect(names.get(4).getText()).toBe('Julie'); + }); + +
    * + */ + orderByFilter.$inject = ['$parse']; + function orderByFilter($parse) { + return function(array, sortPredicate, reverseOrder, compareFn) { + + if (array == null) return array; + if (!isArrayLike(array)) { + throw minErr('orderBy')('notarray', 'Expected array but received: {0}', array); + } + + if (!isArray(sortPredicate)) { sortPredicate = [sortPredicate]; } + if (sortPredicate.length === 0) { sortPredicate = ['+']; } + + var predicates = processPredicates(sortPredicate); + + var descending = reverseOrder ? -1 : 1; + + // Define the `compare()` function. Use a default comparator if none is specified. + var compare = isFunction(compareFn) ? compareFn : defaultCompare; + + // The next three lines are a version of a Swartzian Transform idiom from Perl + // (sometimes called the Decorate-Sort-Undecorate idiom) + // See https://en.wikipedia.org/wiki/Schwartzian_transform + var compareValues = Array.prototype.map.call(array, getComparisonObject); + compareValues.sort(doComparison); + array = compareValues.map(function(item) { return item.value; }); + + return array; + + function getComparisonObject(value, index) { + // NOTE: We are adding an extra `tieBreaker` value based on the element's index. + // This will be used to keep the sort stable when none of the input predicates can + // distinguish between two elements. + return { + value: value, + tieBreaker: {value: index, type: 'number', index: index}, + predicateValues: predicates.map(function(predicate) { + return getPredicateValue(predicate.get(value), index); + }) + }; + } + + function doComparison(v1, v2) { + for (var i = 0, ii = predicates.length; i < ii; i++) { + var result = compare(v1.predicateValues[i], v2.predicateValues[i]); + if (result) { + return result * predicates[i].descending * descending; + } + } + + return (compare(v1.tieBreaker, v2.tieBreaker) || defaultCompare(v1.tieBreaker, v2.tieBreaker)) * descending; + } + }; + + function processPredicates(sortPredicates) { + return sortPredicates.map(function(predicate) { + var descending = 1, get = identity; + + if (isFunction(predicate)) { + get = predicate; + } else if (isString(predicate)) { + if ((predicate.charAt(0) === '+' || predicate.charAt(0) === '-')) { + descending = predicate.charAt(0) === '-' ? -1 : 1; + predicate = predicate.substring(1); + } + if (predicate !== '') { + get = $parse(predicate); + if (get.constant) { + var key = get(); + get = function(value) { return value[key]; }; + } + } + } + return {get: get, descending: descending}; + }); + } + + function isPrimitive(value) { + switch (typeof value) { + case 'number': /* falls through */ + case 'boolean': /* falls through */ + case 'string': + return true; + default: + return false; + } + } + + function objectValue(value) { + // If `valueOf` is a valid function use that + if (isFunction(value.valueOf)) { + value = value.valueOf(); + if (isPrimitive(value)) return value; + } + // If `toString` is a valid function and not the one from `Object.prototype` use that + if (hasCustomToString(value)) { + value = value.toString(); + if (isPrimitive(value)) return value; + } + + return value; + } + + function getPredicateValue(value, index) { + var type = typeof value; + if (value === null) { + type = 'string'; + value = 'null'; + } else if (type === 'object') { + value = objectValue(value); + } + return {value: value, type: type, index: index}; + } + + function defaultCompare(v1, v2) { + var result = 0; + var type1 = v1.type; + var type2 = v2.type; + + if (type1 === type2) { + var value1 = v1.value; + var value2 = v2.value; + + if (type1 === 'string') { + // Compare strings case-insensitively + value1 = value1.toLowerCase(); + value2 = value2.toLowerCase(); + } else if (type1 === 'object') { + // For basic objects, use the position of the object + // in the collection instead of the value + if (isObject(value1)) value1 = v1.index; + if (isObject(value2)) value2 = v2.index; + } + + if (value1 !== value2) { + result = value1 < value2 ? -1 : 1; + } + } else { + result = type1 < type2 ? -1 : 1; + } + + return result; + } + } + + function ngDirective(directive) { + if (isFunction(directive)) { + directive = { + link: directive + }; + } + directive.restrict = directive.restrict || 'AC'; + return valueFn(directive); + } + + /** + * @ngdoc directive + * @name a + * @restrict E + * + * @description + * Modifies the default behavior of the html a tag so that the default action is prevented when + * the href attribute is empty. + * + * For dynamically creating `href` attributes for a tags, see the {@link ng.ngHref `ngHref`} directive. + */ + var htmlAnchorDirective = valueFn({ + restrict: 'E', + compile: function(element, attr) { + if (!attr.href && !attr.xlinkHref) { + return function(scope, element) { + // If the linked element is not an anchor tag anymore, do nothing + if (element[0].nodeName.toLowerCase() !== 'a') return; + + // SVGAElement does not use the href attribute, but rather the 'xlinkHref' attribute. + var href = toString.call(element.prop('href')) === '[object SVGAnimatedString]' ? + 'xlink:href' : 'href'; + element.on('click', function(event) { + // if we have no href url, then don't navigate anywhere. + if (!element.attr(href)) { + event.preventDefault(); + } + }); + }; + } + } + }); + + /** + * @ngdoc directive + * @name ngHref + * @restrict A + * @priority 99 + * + * @description + * Using AngularJS markup like `{{hash}}` in an href attribute will + * make the link go to the wrong URL if the user clicks it before + * AngularJS has a chance to replace the `{{hash}}` markup with its + * value. Until AngularJS replaces the markup the link will be broken + * and will most likely return a 404 error. The `ngHref` directive + * solves this problem. + * + * The wrong way to write it: + * ```html + * link1 + * ``` + * + * The correct way to write it: + * ```html + * link1 + * ``` + * + * @element A + * @param {template} ngHref any string which can contain `{{}}` markup. + * + * @example + * This example shows various combinations of `href`, `ng-href` and `ng-click` attributes + * in links and their different behaviors: + + +
    + link 1 (link, don't reload)
    + link 2 (link, don't reload)
    + link 3 (link, reload!)
    + anchor (link, don't reload)
    + anchor (no link)
    + link (link, change location) +
    + + it('should execute ng-click but not reload when href without value', function() { + element(by.id('link-1')).click(); + expect(element(by.model('value')).getAttribute('value')).toEqual('1'); + expect(element(by.id('link-1')).getAttribute('href')).toBe(''); + }); + + it('should execute ng-click but not reload when href empty string', function() { + element(by.id('link-2')).click(); + expect(element(by.model('value')).getAttribute('value')).toEqual('2'); + expect(element(by.id('link-2')).getAttribute('href')).toBe(''); + }); + + it('should execute ng-click and change url when ng-href specified', function() { + expect(element(by.id('link-3')).getAttribute('href')).toMatch(/\/123$/); + + element(by.id('link-3')).click(); + + // At this point, we navigate away from an AngularJS page, so we need + // to use browser.driver to get the base webdriver. + + browser.wait(function() { + return browser.driver.getCurrentUrl().then(function(url) { + return url.match(/\/123$/); + }); + }, 5000, 'page should navigate to /123'); + }); + + it('should execute ng-click but not reload when href empty string and name specified', function() { + element(by.id('link-4')).click(); + expect(element(by.model('value')).getAttribute('value')).toEqual('4'); + expect(element(by.id('link-4')).getAttribute('href')).toBe(''); + }); + + it('should execute ng-click but not reload when no href but name specified', function() { + element(by.id('link-5')).click(); + expect(element(by.model('value')).getAttribute('value')).toEqual('5'); + expect(element(by.id('link-5')).getAttribute('href')).toBe(null); + }); + + it('should only change url when only ng-href', function() { + element(by.model('value')).clear(); + element(by.model('value')).sendKeys('6'); + expect(element(by.id('link-6')).getAttribute('href')).toMatch(/\/6$/); + + element(by.id('link-6')).click(); + + // At this point, we navigate away from an AngularJS page, so we need + // to use browser.driver to get the base webdriver. + browser.wait(function() { + return browser.driver.getCurrentUrl().then(function(url) { + return url.match(/\/6$/); + }); + }, 5000, 'page should navigate to /6'); + }); + +
    + */ + + /** + * @ngdoc directive + * @name ngSrc + * @restrict A + * @priority 99 * - * ## How does it work? + * @description + * Using AngularJS markup like `{{hash}}` in a `src` attribute doesn't + * work right: The browser will fetch from the URL with the literal + * text `{{hash}}` until AngularJS replaces the expression inside + * `{{hash}}`. The `ngSrc` directive solves this problem. * - * In privileged contexts, directives and code will bind to the result of {@link ng.$sce#getTrusted - * $sce.getTrusted(context, value)} rather than to the value directly. Directives use {@link - * ng.$sce#parse $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs the - * {@link ng.$sce#getTrusted $sce.getTrusted} behind the scenes on non-constant literals. + * The buggy way to write it: + * ```html + * Description + * ``` * - * As an example, {@link ng.directive:ngBindHtml ngBindHtml} uses {@link - * ng.$sce#parseAsHtml $sce.parseAsHtml(binding expression)}. Here's the actual code (slightly - * simplified): + * The correct way to write it: + * ```html + * Description + * ``` * - *
    -     *   var ngBindHtmlDirective = ['$sce', function($sce) {
    - *     return function(scope, element, attr) {
    - *       scope.$watch($sce.parseAsHtml(attr.ngBindHtml), function(value) {
    - *         element.html(value || '');
    - *       });
    - *     };
    - *   }];
    -     * 
    + * @element IMG + * @param {template} ngSrc any string which can contain `{{}}` markup. + */ + + /** + * @ngdoc directive + * @name ngSrcset + * @restrict A + * @priority 99 * - * ## Impact on loading templates + * @description + * Using AngularJS markup like `{{hash}}` in a `srcset` attribute doesn't + * work right: The browser will fetch from the URL with the literal + * text `{{hash}}` until AngularJS replaces the expression inside + * `{{hash}}`. The `ngSrcset` directive solves this problem. * - * This applies both to the {@link ng.directive:ngInclude `ng-include`} directive as well as - * `templateUrl`'s specified by {@link guide/directive directives}. + * The buggy way to write it: + * ```html + * Description + * ``` * - * By default, Angular only loads templates from the same domain and protocol as the application - * document. This is done by calling {@link ng.$sce#getTrustedResourceUrl - * $sce.getTrustedResourceUrl} on the template URL. To load templates from other domains and/or - * protocols, you may either either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist - * them} or {@link ng.$sce#trustAsResourceUrl wrap it} into a trusted value. + * The correct way to write it: + * ```html + * Description + * ``` * - * *Please note*: - * The browser's - * [Same Origin Policy](https://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_XMLHttpRequest) - * and [Cross-Origin Resource Sharing (CORS)](http://www.w3.org/TR/cors/) - * policy apply in addition to this and may further restrict whether the template is successfully - * loaded. This means that without the right CORS policy, loading templates from a different domain - * won't work on all browsers. Also, loading templates from `file://` URL does not work on some - * browsers. + * @element IMG + * @param {template} ngSrcset any string which can contain `{{}}` markup. + */ + + /** + * @ngdoc directive + * @name ngDisabled + * @restrict A + * @priority 100 * - * ## This feels like too much overhead for the developer? + * @description * - * It's important to remember that SCE only applies to interpolation expressions. + * This directive sets the `disabled` attribute on the element (typically a form control, + * e.g. `input`, `button`, `select` etc.) if the + * {@link guide/expression expression} inside `ngDisabled` evaluates to truthy. * - * If your expressions are constant literals, they're automatically trusted and you don't need to - * call `$sce.trustAs` on them (remember to include the `ngSanitize` module) (e.g. - * `
    `) just works. + * A special directive is necessary because we cannot use interpolation inside the `disabled` + * attribute. See the {@link guide/interpolation interpolation guide} for more info. * - * Additionally, `a[href]` and `img[src]` automatically sanitize their URLs and do not pass them - * through {@link ng.$sce#getTrusted $sce.getTrusted}. SCE doesn't play a role here. + * @example + + +
    + +
    + + it('should toggle button', function() { + expect(element(by.css('button')).getAttribute('disabled')).toBeFalsy(); + element(by.model('checked')).click(); + expect(element(by.css('button')).getAttribute('disabled')).toBeTruthy(); + }); + +
    * - * The included {@link ng.$sceDelegate $sceDelegate} comes with sane defaults to allow you to load - * templates in `ng-include` from your application's domain without having to even know about SCE. - * It blocks loading templates from other domains or loading templates over http from an https - * served document. You can change these by setting your own custom {@link - * ng.$sceDelegateProvider#resourceUrlWhitelist whitelists} and {@link - * ng.$sceDelegateProvider#resourceUrlBlacklist blacklists} for matching such URLs. + * @element INPUT + * @param {expression} ngDisabled If the {@link guide/expression expression} is truthy, + * then the `disabled` attribute will be set on the element + */ + + + /** + * @ngdoc directive + * @name ngChecked + * @restrict A + * @priority 100 * - * This significantly reduces the overhead. It is far easier to pay the small overhead and have an - * application that's secure and can be audited to verify that with much more ease than bolting - * security onto an application later. + * @description + * Sets the `checked` attribute on the element, if the expression inside `ngChecked` is truthy. * - * - * ## What trusted context types are supported? + * Note that this directive should not be used together with {@link ngModel `ngModel`}, + * as this can lead to unexpected behavior. * - * | Context | Notes | - * |---------------------|----------------| - * | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtml ngBindHtml} directive uses this context for bindings. | - * | `$sce.CSS` | For CSS that's safe to source into the application. Currently unused. Feel free to use it in your own directives. | - * | `$sce.URL` | For URLs that are safe to follow as links. Currently unused (`
    Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. | - * | `$sce.JS` | For JavaScript that is safe to execute in your application's context. Currently unused. Feel free to use it in your own directives. | + * A special directive is necessary because we cannot use interpolation inside the `checked` + * attribute. See the {@link guide/interpolation interpolation guide} for more info. * - * ## Format of items in {@link ng.$sceDelegateProvider#resourceUrlWhitelist resourceUrlWhitelist}/{@link ng.$sceDelegateProvider#resourceUrlBlacklist Blacklist}
    + * @example + + +
    + +
    + + it('should check both checkBoxes', function() { + expect(element(by.id('checkFollower')).getAttribute('checked')).toBeFalsy(); + element(by.model('leader')).click(); + expect(element(by.id('checkFollower')).getAttribute('checked')).toBeTruthy(); + }); + +
    * - * Each element in these arrays must be one of the following: + * @element INPUT + * @param {expression} ngChecked If the {@link guide/expression expression} is truthy, + * then the `checked` attribute will be set on the element + */ + + + /** + * @ngdoc directive + * @name ngReadonly + * @restrict A + * @priority 100 * - * - **'self'** - * - The special **string**, `'self'`, can be used to match against all URLs of the **same - * domain** as the application document using the **same protocol**. - * - **String** (except the special value `'self'`) - * - The string is matched against the full *normalized / absolute URL* of the resource - * being tested (substring matches are not good enough.) - * - There are exactly **two wildcard sequences** - `*` and `**`. All other characters - * match themselves. - * - `*`: matches zero or more occurrences of any character other than one of the following 6 - * characters: '`:`', '`/`', '`.`', '`?`', '`&`' and ';'. It's a useful wildcard for use - * in a whitelist. - * - `**`: matches zero or more occurrences of *any* character. As such, it's not - * not appropriate to use in for a scheme, domain, etc. as it would match too much. (e.g. - * http://**.example.com/ would match http://evil.com/?ignore=.example.com/ and that might - * not have been the intention.) It's usage at the very end of the path is ok. (e.g. - * http://foo.example.com/templates/**). - * - **RegExp** (*see caveat below*) - * - *Caveat*: While regular expressions are powerful and offer great flexibility, their syntax - * (and all the inevitable escaping) makes them *harder to maintain*. It's easy to - * accidentally introduce a bug when one updates a complex expression (imho, all regexes should - * have good test coverage.). For instance, the use of `.` in the regex is correct only in a - * small number of cases. A `.` character in the regex used when matching the scheme or a - * subdomain could be matched against a `:` or literal `.` that was likely not intended. It - * is highly recommended to use the string patterns and only fall back to regular expressions - * if they as a last resort. - * - The regular expression must be an instance of RegExp (i.e. not a string.) It is - * matched against the **entire** *normalized / absolute URL* of the resource being tested - * (even when the RegExp did not have the `^` and `$` codes.) In addition, any flags - * present on the RegExp (such as multiline, global, ignoreCase) are ignored. - * - If you are generating your JavaScript from some other templating engine (not - * recommended, e.g. in issue [#4006](https://github.com/angular/angular.js/issues/4006)), - * remember to escape your regular expression (and be aware that you might need more than - * one level of escaping depending on your templating engine and the way you interpolated - * the value.) Do make use of your platform's escaping mechanism as it might be good - * enough before coding your own. e.g. Ruby has - * [Regexp.escape(str)](http://www.ruby-doc.org/core-2.0.0/Regexp.html#method-c-escape) - * and Python has [re.escape](http://docs.python.org/library/re.html#re.escape). - * Javascript lacks a similar built in function for escaping. Take a look at Google - * Closure library's [goog.string.regExpEscape(s)]( - * http://docs.closure-library.googlecode.com/git/closure_goog_string_string.js.source.html#line962). + * @description * - * Refer {@link ng.$sceDelegateProvider $sceDelegateProvider} for an example. + * Sets the `readonly` attribute on the element, if the expression inside `ngReadonly` is truthy. + * Note that `readonly` applies only to `input` elements with specific types. [See the input docs on + * MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-readonly) for more information. * - * ## Show me an example using SCE. + * A special directive is necessary because we cannot use interpolation inside the `readonly` + * attribute. See the {@link guide/interpolation interpolation guide} for more info. * * @example - + -
    -

    - User comments
    - By default, HTML that isn't explicitly trusted (e.g. Alice's comment) is sanitized when - $sanitize is available. If $sanitize isn't available, this results in an error instead of an - exploit. -
    -
    - {{userComment.name}}: - -
    -
    -
    -
    +
    +
    - - - var mySceApp = angular.module('mySceApp', ['ngSanitize']); - - mySceApp.controller("myAppController", function myAppController($http, $templateCache, $sce) { - var self = this; - $http.get("test_data.json", {cache: $templateCache}).success(function(userComments) { - self.userComments = userComments; - }); - self.explicitlyTrustedHtml = $sce.trustAsHtml( - 'Hover over this text.'); - }); + + it('should toggle readonly attr', function() { + expect(element(by.css('[type="text"]')).getAttribute('readonly')).toBeFalsy(); + element(by.model('checked')).click(); + expect(element(by.css('[type="text"]')).getAttribute('readonly')).toBeTruthy(); + }); +
    + * + * @element INPUT + * @param {expression} ngReadonly If the {@link guide/expression expression} is truthy, + * then special attribute "readonly" will be set on the element + */ - - [ - { "name": "Alice", - "htmlComment": - "Is anyone reading this?" - }, - { "name": "Bob", - "htmlComment": "Yes! Am I the only other one?" - } - ] - + /** + * @ngdoc directive + * @name ngSelected + * @restrict A + * @priority 100 + * + * @description + * + * Sets the `selected` attribute on the element, if the expression inside `ngSelected` is truthy. + * + * A special directive is necessary because we cannot use interpolation inside the `selected` + * attribute. See the {@link guide/interpolation interpolation guide} for more info. + * + *
    + * **Note:** `ngSelected` does not interact with the `select` and `ngModel` directives, it only + * sets the `selected` attribute on the element. If you are using `ngModel` on the select, you + * should not use `ngSelected` on the options, as `ngModel` will set the select value and + * selected options. + *
    + * + * @example + + +
    + +
    - describe('SCE doc demo', function() { - it('should sanitize untrusted values', function() { - expect(element(by.css('.htmlComment')).getInnerHtml()) - .toBe('Is anyone reading this?'); - }); - - it('should NOT sanitize explicitly trusted values', function() { - expect(element(by.id('explicitlyTrustedHtml')).getInnerHtml()).toBe( - 'Hover over this text.'); - }); - }); + it('should select Greetings!', function() { + expect(element(by.id('greet')).getAttribute('selected')).toBeFalsy(); + element(by.model('selected')).click(); + expect(element(by.id('greet')).getAttribute('selected')).toBeTruthy(); + });
    * + * @element OPTION + * @param {expression} ngSelected If the {@link guide/expression expression} is truthy, + * then special attribute "selected" will be set on the element + */ + + /** + * @ngdoc directive + * @name ngOpen + * @restrict A + * @priority 100 + * + * @description * + * Sets the `open` attribute on the element, if the expression inside `ngOpen` is truthy. * - * ## Can I disable SCE completely? + * A special directive is necessary because we cannot use interpolation inside the `open` + * attribute. See the {@link guide/interpolation interpolation guide} for more info. * - * Yes, you can. However, this is strongly discouraged. SCE gives you a lot of security benefits - * for little coding overhead. It will be much harder to take an SCE disabled application and - * either secure it on your own or enable SCE at a later stage. It might make sense to disable SCE - * for cases where you have a lot of existing code that was written before SCE was introduced and - * you're migrating them a module at a time. + * ## A note about browser compatibility * - * That said, here's how you can completely disable SCE: + * Internet Explorer and Edge do not support the `details` element, it is + * recommended to use {@link ng.ngShow} and {@link ng.ngHide} instead. * - *
    -     *   angular.module('myAppWithSceDisabledmyApp', []).config(function($sceProvider) {
    - *     // Completely disable SCE.  For demonstration purposes only!
    - *     // Do not use in new projects.
    - *     $sceProvider.enabled(false);
    - *   });
    -     * 
    + * @example + + +
    +
    + List +
      +
    • Apple
    • +
    • Orange
    • +
    • Durian
    • +
    +
    +
    + + it('should toggle open', function() { + expect(element(by.id('details')).getAttribute('open')).toBeFalsy(); + element(by.model('open')).click(); + expect(element(by.id('details')).getAttribute('open')).toBeTruthy(); + }); + +
    * + * @element DETAILS + * @param {expression} ngOpen If the {@link guide/expression expression} is truthy, + * then special attribute "open" will be set on the element */ - /* jshint maxlen: 100 */ - - function $SceProvider() { - var enabled = true; - - /** - * @ngdoc method - * @name $sceProvider#enabled - * @function - * - * @param {boolean=} value If provided, then enables/disables SCE. - * @return {boolean} true if SCE is enabled, false otherwise. - * - * @description - * Enables/disables SCE and returns the current value. - */ - this.enabled = function (value) { - if (arguments.length) { - enabled = !!value; - } - return enabled; - }; - - - /* Design notes on the default implementation for SCE. - * - * The API contract for the SCE delegate - * ------------------------------------- - * The SCE delegate object must provide the following 3 methods: - * - * - trustAs(contextEnum, value) - * This method is used to tell the SCE service that the provided value is OK to use in the - * contexts specified by contextEnum. It must return an object that will be accepted by - * getTrusted() for a compatible contextEnum and return this value. - * - * - valueOf(value) - * For values that were not produced by trustAs(), return them as is. For values that were - * produced by trustAs(), return the corresponding input value to trustAs. Basically, if - * trustAs is wrapping the given values into some type, this operation unwraps it when given - * such a value. - * - * - getTrusted(contextEnum, value) - * This function should return the a value that is safe to use in the context specified by - * contextEnum or throw and exception otherwise. - * - * NOTE: This contract deliberately does NOT state that values returned by trustAs() must be - * opaque or wrapped in some holder object. That happens to be an implementation detail. For - * instance, an implementation could maintain a registry of all trusted objects by context. In - * such a case, trustAs() would return the same object that was passed in. getTrusted() would - * return the same object passed in if it was found in the registry under a compatible context or - * throw an exception otherwise. An implementation might only wrap values some of the time based - * on some criteria. getTrusted() might return a value and not throw an exception for special - * constants or objects even if not wrapped. All such implementations fulfill this contract. - * - * - * A note on the inheritance model for SCE contexts - * ------------------------------------------------ - * I've used inheritance and made RESOURCE_URL wrapped types a subtype of URL wrapped types. This - * is purely an implementation details. - * - * The contract is simply this: - * - * getTrusted($sce.RESOURCE_URL, value) succeeding implies that getTrusted($sce.URL, value) - * will also succeed. - * - * Inheritance happens to capture this in a natural way. In some future, we - * may not use inheritance anymore. That is OK because no code outside of - * sce.js and sceSpecs.js would need to be aware of this detail. - */ - this.$get = ['$parse', '$sniffer', '$sceDelegate', function( - $parse, $sniffer, $sceDelegate) { - // Prereq: Ensure that we're not running in IE8 quirks mode. In that mode, IE allows - // the "expression(javascript expression)" syntax which is insecure. - if (enabled && $sniffer.msie && $sniffer.msieDocumentMode < 8) { - throw $sceMinErr('iequirks', - 'Strict Contextual Escaping does not support Internet Explorer version < 9 in quirks ' + - 'mode. You can fix this by adding the text to the top of your HTML ' + - 'document. See http://docs.angularjs.org/api/ng.$sce for more information.'); - } + var ngAttributeAliasDirectives = {}; - var sce = copy(SCE_CONTEXTS); +// boolean attrs are evaluated + forEach(BOOLEAN_ATTR, function(propName, attrName) { + // binding to multiple is not supported + if (propName === 'multiple') return; - /** - * @ngdoc method - * @name $sce#isEnabled - * @function - * - * @return {Boolean} true if SCE is enabled, false otherwise. If you want to set the value, you - * have to do it at module config time on {@link ng.$sceProvider $sceProvider}. - * - * @description - * Returns a boolean indicating if SCE is enabled. - */ - sce.isEnabled = function () { - return enabled; - }; - sce.trustAs = $sceDelegate.trustAs; - sce.getTrusted = $sceDelegate.getTrusted; - sce.valueOf = $sceDelegate.valueOf; + function defaultLinkFn(scope, element, attr) { + scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) { + attr.$set(attrName, !!value); + }); + } - if (!enabled) { - sce.trustAs = sce.getTrusted = function(type, value) { return value; }; - sce.valueOf = identity; - } + var normalized = directiveNormalize('ng-' + attrName); + var linkFn = defaultLinkFn; - /** - * @ngdoc method - * @name $sce#parse - * - * @description - * Converts Angular {@link guide/expression expression} into a function. This is like {@link - * ng.$parse $parse} and is identical when the expression is a literal constant. Otherwise, it - * wraps the expression in a call to {@link ng.$sce#getTrusted $sce.getTrusted(*type*, - * *result*)} - * - * @param {string} type The kind of SCE context in which this result will be used. - * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: - * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. - */ - sce.parseAs = function sceParseAs(type, expr) { - var parsed = $parse(expr); - if (parsed.literal && parsed.constant) { - return parsed; - } else { - return function sceParseAsTrusted(self, locals) { - return sce.getTrusted(type, parsed(self, locals)); - }; + if (propName === 'checked') { + linkFn = function(scope, element, attr) { + // ensuring ngChecked doesn't interfere with ngModel when both are set on the same input + if (attr.ngModel !== attr[normalized]) { + defaultLinkFn(scope, element, attr); } }; + } - /** - * @ngdoc method - * @name $sce#trustAs - * - * @description - * Delegates to {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. As such, - * returns an object that is trusted by angular for use in specified strict contextual - * escaping contexts (such as ng-bind-html, ng-include, any src attribute - * interpolation, any dom event binding attribute interpolation such as for onclick, etc.) - * that uses the provided value. See * {@link ng.$sce $sce} for enabling strict contextual - * escaping. - * - * @param {string} type The kind of context in which this value is safe for use. e.g. url, - * resource_url, html, js and css. - * @param {*} value The value that that should be considered trusted/safe. - * @returns {*} A value that can be used to stand in for the provided `value` in places - * where Angular expects a $sce.trustAs() return value. - */ - - /** - * @ngdoc method - * @name $sce#trustAsHtml - * - * @description - * Shorthand method. `$sce.trustAsHtml(value)` → - * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.HTML, value)`} - * - * @param {*} value The value to trustAs. - * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedHtml - * $sce.getTrustedHtml(value)} to obtain the original value. (privileged directives - * only accept expressions that are either literal constants or are the - * return value of {@link ng.$sce#trustAs $sce.trustAs}.) - */ - - /** - * @ngdoc method - * @name $sce#trustAsUrl - * - * @description - * Shorthand method. `$sce.trustAsUrl(value)` → - * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.URL, value)`} - * - * @param {*} value The value to trustAs. - * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedUrl - * $sce.getTrustedUrl(value)} to obtain the original value. (privileged directives - * only accept expressions that are either literal constants or are the - * return value of {@link ng.$sce#trustAs $sce.trustAs}.) - */ - - /** - * @ngdoc method - * @name $sce#trustAsResourceUrl - * - * @description - * Shorthand method. `$sce.trustAsResourceUrl(value)` → - * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.RESOURCE_URL, value)`} - * - * @param {*} value The value to trustAs. - * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedResourceUrl - * $sce.getTrustedResourceUrl(value)} to obtain the original value. (privileged directives - * only accept expressions that are either literal constants or are the return - * value of {@link ng.$sce#trustAs $sce.trustAs}.) - */ - - /** - * @ngdoc method - * @name $sce#trustAsJs - * - * @description - * Shorthand method. `$sce.trustAsJs(value)` → - * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.JS, value)`} - * - * @param {*} value The value to trustAs. - * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedJs - * $sce.getTrustedJs(value)} to obtain the original value. (privileged directives - * only accept expressions that are either literal constants or are the - * return value of {@link ng.$sce#trustAs $sce.trustAs}.) - */ - - /** - * @ngdoc method - * @name $sce#getTrusted - * - * @description - * Delegates to {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted`}. As such, - * takes the result of a {@link ng.$sce#trustAs `$sce.trustAs`}() call and returns the - * originally supplied value if the queried context type is a supertype of the created type. - * If this condition isn't satisfied, throws an exception. - * - * @param {string} type The kind of context in which this value is to be used. - * @param {*} maybeTrusted The result of a prior {@link ng.$sce#trustAs `$sce.trustAs`} - * call. - * @returns {*} The value the was originally provided to - * {@link ng.$sce#trustAs `$sce.trustAs`} if valid in this context. - * Otherwise, throws an exception. - */ - - /** - * @ngdoc method - * @name $sce#getTrustedHtml - * - * @description - * Shorthand method. `$sce.getTrustedHtml(value)` → - * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.HTML, value)`} - * - * @param {*} value The value to pass to `$sce.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.HTML, value)` - */ - - /** - * @ngdoc method - * @name $sce#getTrustedCss - * - * @description - * Shorthand method. `$sce.getTrustedCss(value)` → - * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.CSS, value)`} - * - * @param {*} value The value to pass to `$sce.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.CSS, value)` - */ - - /** - * @ngdoc method - * @name $sce#getTrustedUrl - * - * @description - * Shorthand method. `$sce.getTrustedUrl(value)` → - * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.URL, value)`} - * - * @param {*} value The value to pass to `$sce.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.URL, value)` - */ - - /** - * @ngdoc method - * @name $sce#getTrustedResourceUrl - * - * @description - * Shorthand method. `$sce.getTrustedResourceUrl(value)` → - * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.RESOURCE_URL, value)`} - * - * @param {*} value The value to pass to `$sceDelegate.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.RESOURCE_URL, value)` - */ - - /** - * @ngdoc method - * @name $sce#getTrustedJs - * - * @description - * Shorthand method. `$sce.getTrustedJs(value)` → - * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.JS, value)`} - * - * @param {*} value The value to pass to `$sce.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.JS, value)` - */ + ngAttributeAliasDirectives[normalized] = function() { + return { + restrict: 'A', + priority: 100, + link: linkFn + }; + }; + }); - /** - * @ngdoc method - * @name $sce#parseAsHtml - * - * @description - * Shorthand method. `$sce.parseAsHtml(expression string)` → - * {@link ng.$sce#parse `$sce.parseAs($sce.HTML, value)`} - * - * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: - * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. - */ +// aliased input attrs are evaluated + forEach(ALIASED_ATTR, function(htmlAttr, ngAttr) { + ngAttributeAliasDirectives[ngAttr] = function() { + return { + priority: 100, + link: function(scope, element, attr) { + //special case ngPattern when a literal regular expression value + //is used as the expression (this way we don't have to watch anything). + if (ngAttr === 'ngPattern' && attr.ngPattern.charAt(0) === '/') { + var match = attr.ngPattern.match(REGEX_STRING_REGEXP); + if (match) { + attr.$set('ngPattern', new RegExp(match[1], match[2])); + return; + } + } - /** - * @ngdoc method - * @name $sce#parseAsCss - * - * @description - * Shorthand method. `$sce.parseAsCss(value)` → - * {@link ng.$sce#parse `$sce.parseAs($sce.CSS, value)`} - * - * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: - * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. - */ + scope.$watch(attr[ngAttr], function ngAttrAliasWatchAction(value) { + attr.$set(ngAttr, value); + }); + } + }; + }; + }); - /** - * @ngdoc method - * @name $sce#parseAsUrl - * - * @description - * Shorthand method. `$sce.parseAsUrl(value)` → - * {@link ng.$sce#parse `$sce.parseAs($sce.URL, value)`} - * - * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: - * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. - */ +// ng-src, ng-srcset, ng-href are interpolated + forEach(['src', 'srcset', 'href'], function(attrName) { + var normalized = directiveNormalize('ng-' + attrName); + ngAttributeAliasDirectives[normalized] = function() { + return { + priority: 99, // it needs to run after the attributes are interpolated + link: function(scope, element, attr) { + var propName = attrName, + name = attrName; - /** - * @ngdoc method - * @name $sce#parseAsResourceUrl - * - * @description - * Shorthand method. `$sce.parseAsResourceUrl(value)` → - * {@link ng.$sce#parse `$sce.parseAs($sce.RESOURCE_URL, value)`} - * - * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: - * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. - */ + if (attrName === 'href' && + toString.call(element.prop('href')) === '[object SVGAnimatedString]') { + name = 'xlinkHref'; + attr.$attr[name] = 'xlink:href'; + propName = null; + } - /** - * @ngdoc method - * @name $sce#parseAsJs - * - * @description - * Shorthand method. `$sce.parseAsJs(value)` → - * {@link ng.$sce#parse `$sce.parseAs($sce.JS, value)`} - * - * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: - * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. - */ + attr.$observe(normalized, function(value) { + if (!value) { + if (attrName === 'href') { + attr.$set(name, null); + } + return; + } - // Shorthand delegations. - var parse = sce.parseAs, - getTrusted = sce.getTrusted, - trustAs = sce.trustAs; + attr.$set(name, value); - forEach(SCE_CONTEXTS, function (enumValue, name) { - var lName = lowercase(name); - sce[camelCase("parse_as_" + lName)] = function (expr) { - return parse(enumValue, expr); - }; - sce[camelCase("get_trusted_" + lName)] = function (value) { - return getTrusted(enumValue, value); - }; - sce[camelCase("trust_as_" + lName)] = function (value) { - return trustAs(enumValue, value); - }; - }); + // Support: IE 9-11 only + // On IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist + // then calling element.setAttribute('src', 'foo') doesn't do anything, so we need + // to set the property as well to achieve the desired effect. + // We use attr[attrName] value since $set can sanitize the url. + if (msie && propName) element.prop(propName, attr[name]); + }); + } + }; + }; + }); - return sce; - }]; + /* global -nullFormCtrl, -PENDING_CLASS, -SUBMITTED_CLASS + */ + var nullFormCtrl = { + $addControl: noop, + $$renameControl: nullFormRenameControl, + $removeControl: noop, + $setValidity: noop, + $setDirty: noop, + $setPristine: noop, + $setSubmitted: noop + }, + PENDING_CLASS = 'ng-pending', + SUBMITTED_CLASS = 'ng-submitted'; + + function nullFormRenameControl(control, name) { + control.$name = name; } /** - * !!! This is an undocumented "private" service !!! + * @ngdoc type + * @name form.FormController * - * @name $sniffer - * @requires $window - * @requires $document + * @property {boolean} $pristine True if user has not interacted with the form yet. + * @property {boolean} $dirty True if user has already interacted with the form. + * @property {boolean} $valid True if all of the containing forms and controls are valid. + * @property {boolean} $invalid True if at least one containing control or form is invalid. + * @property {boolean} $submitted True if user has submitted the form even if its invalid. * - * @property {boolean} history Does the browser support html5 history api ? - * @property {boolean} hashchange Does the browser support hashchange event ? - * @property {boolean} transitions Does the browser support CSS transition events ? - * @property {boolean} animations Does the browser support CSS animation events ? + * @property {Object} $pending An object hash, containing references to controls or forms with + * pending validators, where: + * + * - keys are validations tokens (error names). + * - values are arrays of controls or forms that have a pending validator for the given error name. + * + * See {@link form.FormController#$error $error} for a list of built-in validation tokens. + * + * @property {Object} $error An object hash, containing references to controls or forms with failing + * validators, where: + * + * - keys are validation tokens (error names), + * - values are arrays of controls or forms that have a failing validator for the given error name. + * + * Built-in validation tokens: + * - `email` + * - `max` + * - `maxlength` + * - `min` + * - `minlength` + * - `number` + * - `pattern` + * - `required` + * - `url` + * - `date` + * - `datetimelocal` + * - `time` + * - `week` + * - `month` * * @description - * This is very simple implementation of testing browser's features. + * `FormController` keeps track of all its controls and nested forms as well as the state of them, + * such as being valid/invalid or dirty/pristine. + * + * Each {@link ng.directive:form form} directive creates an instance + * of `FormController`. + * */ - function $SnifferProvider() { - this.$get = ['$window', '$document', function($window, $document) { - var eventSupport = {}, - android = - int((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]), - boxee = /Boxee/i.test(($window.navigator || {}).userAgent), - document = $document[0] || {}, - documentMode = document.documentMode, - vendorPrefix, - vendorRegex = /^(Moz|webkit|O|ms)(?=[A-Z])/, - bodyStyle = document.body && document.body.style, - transitions = false, - animations = false, - match; - - if (bodyStyle) { - for(var prop in bodyStyle) { - if(match = vendorRegex.exec(prop)) { - vendorPrefix = match[0]; - vendorPrefix = vendorPrefix.substr(0, 1).toUpperCase() + vendorPrefix.substr(1); - break; - } - } - - if(!vendorPrefix) { - vendorPrefix = ('WebkitOpacity' in bodyStyle) && 'webkit'; - } +//asks for $scope to fool the BC controller module + FormController.$inject = ['$element', '$attrs', '$scope', '$animate', '$interpolate']; + function FormController($element, $attrs, $scope, $animate, $interpolate) { + this.$$controls = []; - transitions = !!(('transition' in bodyStyle) || (vendorPrefix + 'Transition' in bodyStyle)); - animations = !!(('animation' in bodyStyle) || (vendorPrefix + 'Animation' in bodyStyle)); + // init state + this.$error = {}; + this.$$success = {}; + this.$pending = undefined; + this.$name = $interpolate($attrs.name || $attrs.ngForm || '')($scope); + this.$dirty = false; + this.$pristine = true; + this.$valid = true; + this.$invalid = false; + this.$submitted = false; + this.$$parentForm = nullFormCtrl; + + this.$$element = $element; + this.$$animate = $animate; + + setupValidity(this); + } - if (android && (!transitions||!animations)) { - transitions = isString(document.body.style.webkitTransition); - animations = isString(document.body.style.webkitAnimation); - } - } + FormController.prototype = { + /** + * @ngdoc method + * @name form.FormController#$rollbackViewValue + * + * @description + * Rollback all form controls pending updates to the `$modelValue`. + * + * Updates may be pending by a debounced event or because the input is waiting for a some future + * event defined in `ng-model-options`. This method is typically needed by the reset button of + * a form that uses `ng-model-options` to pend updates. + */ + $rollbackViewValue: function() { + forEach(this.$$controls, function(control) { + control.$rollbackViewValue(); + }); + }, + /** + * @ngdoc method + * @name form.FormController#$commitViewValue + * + * @description + * Commit all form controls pending updates to the `$modelValue`. + * + * Updates may be pending by a debounced event or because the input is waiting for a some future + * event defined in `ng-model-options`. This method is rarely needed as `NgModelController` + * usually handles calling this in response to input events. + */ + $commitViewValue: function() { + forEach(this.$$controls, function(control) { + control.$commitViewValue(); + }); + }, - return { - // Android has history.pushState, but it does not update location correctly - // so let's not use the history API at all. - // http://code.google.com/p/android/issues/detail?id=17471 - // https://github.com/angular/angular.js/issues/904 + /** + * @ngdoc method + * @name form.FormController#$addControl + * @param {object} control control object, either a {@link form.FormController} or an + * {@link ngModel.NgModelController} + * + * @description + * Register a control with the form. Input elements using ngModelController do this automatically + * when they are linked. + * + * Note that the current state of the control will not be reflected on the new parent form. This + * is not an issue with normal use, as freshly compiled and linked controls are in a `$pristine` + * state. + * + * However, if the method is used programmatically, for example by adding dynamically created controls, + * or controls that have been previously removed without destroying their corresponding DOM element, + * it's the developers responsibility to make sure the current state propagates to the parent form. + * + * For example, if an input control is added that is already `$dirty` and has `$error` properties, + * calling `$setDirty()` and `$validate()` afterwards will propagate the state to the parent form. + */ + $addControl: function(control) { + // Breaking change - before, inputs whose name was "hasOwnProperty" were quietly ignored + // and not added to the scope. Now we throw an error. + assertNotHasOwnProperty(control.$name, 'input'); + this.$$controls.push(control); - // older webkit browser (533.9) on Boxee box has exactly the same problem as Android has - // so let's not use the history API also - // We are purposefully using `!(android < 4)` to cover the case when `android` is undefined - // jshint -W018 - history: !!($window.history && $window.history.pushState && !(android < 4) && !boxee), - // jshint +W018 - hashchange: 'onhashchange' in $window && - // IE8 compatible mode lies - (!documentMode || documentMode > 7), - hasEvent: function(event) { - // IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have - // it. In particular the event is not fired when backspace or delete key are pressed or - // when cut operation is performed. - if (event == 'input' && msie == 9) return false; + if (control.$name) { + this[control.$name] = control; + } - if (isUndefined(eventSupport[event])) { - var divElm = document.createElement('div'); - eventSupport[event] = 'on' + event in divElm; - } + control.$$parentForm = this; + }, - return eventSupport[event]; - }, - csp: csp(), - vendorPrefix: vendorPrefix, - transitions : transitions, - animations : animations, - android: android, - msie : msie, - msieDocumentMode: documentMode - }; - }]; - } + // Private API: rename a form control + $$renameControl: function(control, newName) { + var oldName = control.$name; - function $TimeoutProvider() { - this.$get = ['$rootScope', '$browser', '$q', '$exceptionHandler', - function($rootScope, $browser, $q, $exceptionHandler) { - var deferreds = {}; + if (this[oldName] === control) { + delete this[oldName]; + } + this[newName] = control; + control.$name = newName; + }, + /** + * @ngdoc method + * @name form.FormController#$removeControl + * @param {object} control control object, either a {@link form.FormController} or an + * {@link ngModel.NgModelController} + * + * @description + * Deregister a control from the form. + * + * Input elements using ngModelController do this automatically when they are destroyed. + * + * Note that only the removed control's validation state (`$errors`etc.) will be removed from the + * form. `$dirty`, `$submitted` states will not be changed, because the expected behavior can be + * different from case to case. For example, removing the only `$dirty` control from a form may or + * may not mean that the form is still `$dirty`. + */ + $removeControl: function(control) { + if (control.$name && this[control.$name] === control) { + delete this[control.$name]; + } + forEach(this.$pending, function(value, name) { + // eslint-disable-next-line no-invalid-this + this.$setValidity(name, null, control); + }, this); + forEach(this.$error, function(value, name) { + // eslint-disable-next-line no-invalid-this + this.$setValidity(name, null, control); + }, this); + forEach(this.$$success, function(value, name) { + // eslint-disable-next-line no-invalid-this + this.$setValidity(name, null, control); + }, this); + + arrayRemove(this.$$controls, control); + control.$$parentForm = nullFormCtrl; + }, - /** - * @ngdoc service - * @name $timeout - * - * @description - * Angular's wrapper for `window.setTimeout`. The `fn` function is wrapped into a try/catch - * block and delegates any exceptions to - * {@link ng.$exceptionHandler $exceptionHandler} service. - * - * The return value of registering a timeout function is a promise, which will be resolved when - * the timeout is reached and the timeout function is executed. - * - * To cancel a timeout request, call `$timeout.cancel(promise)`. - * - * In tests you can use {@link ngMock.$timeout `$timeout.flush()`} to - * synchronously flush the queue of deferred functions. - * - * @param {function()} fn A function, whose execution should be delayed. - * @param {number=} [delay=0] Delay in milliseconds. - * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise - * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. - * @returns {Promise} Promise that will be resolved when the timeout is reached. The value this - * promise will be resolved with is the return value of the `fn` function. - * - */ - function timeout(fn, delay, invokeApply) { - var deferred = $q.defer(), - promise = deferred.promise, - skipApply = (isDefined(invokeApply) && !invokeApply), - timeoutId; + /** + * @ngdoc method + * @name form.FormController#$setDirty + * + * @description + * Sets the form to a dirty state. + * + * This method can be called to add the 'ng-dirty' class and set the form to a dirty + * state (ng-dirty class). This method will also propagate to parent forms. + */ + $setDirty: function() { + this.$$animate.removeClass(this.$$element, PRISTINE_CLASS); + this.$$animate.addClass(this.$$element, DIRTY_CLASS); + this.$dirty = true; + this.$pristine = false; + this.$$parentForm.$setDirty(); + }, - timeoutId = $browser.defer(function() { - try { - deferred.resolve(fn()); - } catch(e) { - deferred.reject(e); - $exceptionHandler(e); - } - finally { - delete deferreds[promise.$$timeoutId]; - } + /** + * @ngdoc method + * @name form.FormController#$setPristine + * + * @description + * Sets the form to its pristine state. + * + * This method sets the form's `$pristine` state to true, the `$dirty` state to false, removes + * the `ng-dirty` class and adds the `ng-pristine` class. Additionally, it sets the `$submitted` + * state to false. + * + * This method will also propagate to all the controls contained in this form. + * + * Setting a form back to a pristine state is often useful when we want to 'reuse' a form after + * saving or resetting it. + */ + $setPristine: function() { + this.$$animate.setClass(this.$$element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS); + this.$dirty = false; + this.$pristine = true; + this.$submitted = false; + forEach(this.$$controls, function(control) { + control.$setPristine(); + }); + }, - if (!skipApply) $rootScope.$apply(); - }, delay); + /** + * @ngdoc method + * @name form.FormController#$setUntouched + * + * @description + * Sets the form to its untouched state. + * + * This method can be called to remove the 'ng-touched' class and set the form controls to their + * untouched state (ng-untouched class). + * + * Setting a form controls back to their untouched state is often useful when setting the form + * back to its pristine state. + */ + $setUntouched: function() { + forEach(this.$$controls, function(control) { + control.$setUntouched(); + }); + }, - promise.$$timeoutId = timeoutId; - deferreds[timeoutId] = deferred; + /** + * @ngdoc method + * @name form.FormController#$setSubmitted + * + * @description + * Sets the form to its submitted state. + */ + $setSubmitted: function() { + this.$$animate.addClass(this.$$element, SUBMITTED_CLASS); + this.$submitted = true; + this.$$parentForm.$setSubmitted(); + } + }; - return promise; + /** + * @ngdoc method + * @name form.FormController#$setValidity + * + * @description + * Change the validity state of the form, and notify the parent form (if any). + * + * Application developers will rarely need to call this method directly. It is used internally, by + * {@link ngModel.NgModelController#$setValidity NgModelController.$setValidity()}, to propagate a + * control's validity state to the parent `FormController`. + * + * @param {string} validationErrorKey Name of the validator. The `validationErrorKey` will be + * assigned to either `$error[validationErrorKey]` or `$pending[validationErrorKey]` (for + * unfulfilled `$asyncValidators`), so that it is available for data-binding. The + * `validationErrorKey` should be in camelCase and will get converted into dash-case for + * class name. Example: `myError` will result in `ng-valid-my-error` and + * `ng-invalid-my-error` classes and can be bound to as `{{ someForm.$error.myError }}`. + * @param {boolean} isValid Whether the current state is valid (true), invalid (false), pending + * (undefined), or skipped (null). Pending is used for unfulfilled `$asyncValidators`. + * Skipped is used by AngularJS when validators do not run because of parse errors and when + * `$asyncValidators` do not run because any of the `$validators` failed. + * @param {NgModelController | FormController} controller - The controller whose validity state is + * triggering the change. + */ + addSetValidityMethod({ + clazz: FormController, + set: function(object, property, controller) { + var list = object[property]; + if (!list) { + object[property] = [controller]; + } else { + var index = list.indexOf(controller); + if (index === -1) { + list.push(controller); } + } + }, + unset: function(object, property, controller) { + var list = object[property]; + if (!list) { + return; + } + arrayRemove(list, controller); + if (list.length === 0) { + delete object[property]; + } + } + }); - - /** - * @ngdoc method - * @name $timeout#cancel - * - * @description - * Cancels a task associated with the `promise`. As a result of this, the promise will be - * resolved with a rejection. - * - * @param {Promise=} promise Promise returned by the `$timeout` function. - * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully - * canceled. - */ - timeout.cancel = function(promise) { - if (promise && promise.$$timeoutId in deferreds) { - deferreds[promise.$$timeoutId].reject('canceled'); - delete deferreds[promise.$$timeoutId]; - return $browser.defer.cancel(promise.$$timeoutId); - } - return false; - }; - - return timeout; - }]; - } - -// NOTE: The usage of window and document instead of $window and $document here is -// deliberate. This service depends on the specific behavior of anchor nodes created by the -// browser (resolving and parsing URLs) that is unlikely to be provided by mock objects and -// cause us to break tests. In addition, when the browser resolves a URL for XHR, it -// doesn't know about mocked locations and resolves URLs to the real document - which is -// exactly the behavior needed here. There is little value is mocking these out for this -// service. - var urlParsingNode = document.createElement("a"); - var originUrl = urlResolve(window.location.href, true); - + /** + * @ngdoc directive + * @name ngForm + * @restrict EAC + * + * @description + * Nestable alias of {@link ng.directive:form `form`} directive. HTML + * does not allow nesting of form elements. It is useful to nest forms, for example if the validity of a + * sub-group of controls needs to be determined. + * + * Note: the purpose of `ngForm` is to group controls, + * but not to be a replacement for the `
    ` tag with all of its capabilities + * (e.g. posting to the server, ...). + * + * @param {string=} ngForm|name Name of the form. If specified, the form controller will be published into + * related scope, under this name. + * + */ /** + * @ngdoc directive + * @name form + * @restrict E * - * Implementation Notes for non-IE browsers - * ---------------------------------------- - * Assigning a URL to the href property of an anchor DOM node, even one attached to the DOM, - * results both in the normalizing and parsing of the URL. Normalizing means that a relative - * URL will be resolved into an absolute URL in the context of the application document. - * Parsing means that the anchor node's host, hostname, protocol, port, pathname and related - * properties are all populated to reflect the normalized URL. This approach has wide - * compatibility - Safari 1+, Mozilla 1+, Opera 7+,e etc. See - * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html + * @description + * Directive that instantiates + * {@link form.FormController FormController}. * - * Implementation Notes for IE - * --------------------------- - * IE >= 8 and <= 10 normalizes the URL when assigned to the anchor node similar to the other - * browsers. However, the parsed components will not be set if the URL assigned did not specify - * them. (e.g. if you assign a.href = "foo", then a.protocol, a.host, etc. will be empty.) We - * work around that by performing the parsing in a 2nd step by taking a previously normalized - * URL (e.g. by assigning to a.href) and assigning it a.href again. This correctly populates the - * properties such as protocol, hostname, port, etc. + * If the `name` attribute is specified, the form controller is published onto the current scope under + * this name. * - * IE7 does not normalize the URL when assigned to an anchor node. (Apparently, it does, if one - * uses the inner HTML approach to assign the URL as part of an HTML snippet - - * http://stackoverflow.com/a/472729) However, setting img[src] does normalize the URL. - * Unfortunately, setting img[src] to something like "javascript:foo" on IE throws an exception. - * Since the primary usage for normalizing URLs is to sanitize such URLs, we can't use that - * method and IE < 8 is unsupported. + * ## Alias: {@link ng.directive:ngForm `ngForm`} * - * References: - * http://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement - * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html - * http://url.spec.whatwg.org/#urlutils - * https://github.com/angular/angular.js/pull/2902 - * http://james.padolsey.com/javascript/parsing-urls-with-the-dom/ + * In AngularJS, forms can be nested. This means that the outer form is valid when all of the child + * forms are valid as well. However, browsers do not allow nesting of `` elements, so + * AngularJS provides the {@link ng.directive:ngForm `ngForm`} directive, which behaves identically to + * `form` but can be nested. Nested forms can be useful, for example, if the validity of a sub-group + * of controls needs to be determined. * - * @function - * @param {string} url The URL to be parsed. - * @description Normalizes and parses a URL. - * @returns {object} Returns the normalized URL as a dictionary. + * ## CSS classes + * - `ng-valid` is set if the form is valid. + * - `ng-invalid` is set if the form is invalid. + * - `ng-pending` is set if the form is pending. + * - `ng-pristine` is set if the form is pristine. + * - `ng-dirty` is set if the form is dirty. + * - `ng-submitted` is set if the form was submitted. * - * | member name | Description | - * |---------------|----------------| - * | href | A normalized version of the provided URL if it was not an absolute URL | - * | protocol | The protocol including the trailing colon | - * | host | The host and port (if the port is non-default) of the normalizedUrl | - * | search | The search params, minus the question mark | - * | hash | The hash string, minus the hash symbol - * | hostname | The hostname - * | port | The port, without ":" - * | pathname | The pathname, beginning with "/" + * Keep in mind that ngAnimate can detect each of these classes when added and removed. + * + * + * ## Submitting a form and preventing the default action + * + * Since the role of forms in client-side AngularJS applications is different than in classical + * roundtrip apps, it is desirable for the browser not to translate the form submission into a full + * page reload that sends the data to the server. Instead some javascript logic should be triggered + * to handle the form submission in an application-specific way. + * + * For this reason, AngularJS prevents the default action (form submission to the server) unless the + * `` element has an `action` attribute specified. + * + * You can use one of the following two ways to specify what javascript method should be called when + * a form is submitted: + * + * - {@link ng.directive:ngSubmit ngSubmit} directive on the form element + * - {@link ng.directive:ngClick ngClick} directive on the first + * button or input field of type submit (input[type=submit]) + * + * To prevent double execution of the handler, use only one of the {@link ng.directive:ngSubmit ngSubmit} + * or {@link ng.directive:ngClick ngClick} directives. + * This is because of the following form submission rules in the HTML specification: + * + * - If a form has only one input field then hitting enter in this field triggers form submit + * (`ngSubmit`) + * - if a form has 2+ input fields and no buttons or input[type=submit] then hitting enter + * doesn't trigger submit + * - if a form has one or more input fields and one or more buttons or input[type=submit] then + * hitting enter in any of the input fields will trigger the click handler on the *first* button or + * input[type=submit] (`ngClick`) *and* a submit handler on the enclosing form (`ngSubmit`) + * + * Any pending `ngModelOptions` changes will take place immediately when an enclosing form is + * submitted. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit` + * to have access to the updated model. + * + * @animations + * Animations in ngForm are triggered when any of the associated CSS classes are added and removed. + * These classes are: `.ng-pristine`, `.ng-dirty`, `.ng-invalid` and `.ng-valid` as well as any + * other validations that are performed within the form. Animations in ngForm are similar to how + * they work in ngClass and animations can be hooked into using CSS transitions, keyframes as well + * as JS animations. + * + * The following example shows a simple way to utilize CSS transitions to style a form element + * that has been rendered as invalid after it has been validated: + * + *
    +     * //be sure to include ngAnimate as a module to hook into more
    +     * //advanced animations
    +     * .my-form {
    +     *   transition:0.5s linear all;
    +     *   background: white;
    +     * }
    +     * .my-form.ng-invalid {
    +     *   background: red;
    +     *   color:white;
    +     * }
    +     * 
    + * + * @example + + + + + + userType: + Required!
    + userType = {{userType}}
    + myForm.input.$valid = {{myForm.input.$valid}}
    + myForm.input.$error = {{myForm.input.$error}}
    + myForm.$valid = {{myForm.$valid}}
    + myForm.$error.required = {{!!myForm.$error.required}}
    + +
    + + it('should initialize to model', function() { + var userType = element(by.binding('userType')); + var valid = element(by.binding('myForm.input.$valid')); + + expect(userType.getText()).toContain('guest'); + expect(valid.getText()).toContain('true'); + }); + + it('should be invalid if empty', function() { + var userType = element(by.binding('userType')); + var valid = element(by.binding('myForm.input.$valid')); + var userInput = element(by.model('userType')); + + userInput.clear(); + userInput.sendKeys(''); + + expect(userType.getText()).toEqual('userType ='); + expect(valid.getText()).toContain('false'); + }); + +
    * + * @param {string=} name Name of the form. If specified, the form controller will be published into + * related scope, under this name. */ - function urlResolve(url, base) { - var href = url; + var formDirectiveFactory = function(isNgForm) { + return ['$timeout', '$parse', function($timeout, $parse) { + var formDirective = { + name: 'form', + restrict: isNgForm ? 'EAC' : 'E', + require: ['form', '^^?form'], //first is the form's own ctrl, second is an optional parent form + controller: FormController, + compile: function ngFormCompile(formElement, attr) { + // Setup initial state of the control + formElement.addClass(PRISTINE_CLASS).addClass(VALID_CLASS); + + var nameAttr = attr.name ? 'name' : (isNgForm && attr.ngForm ? 'ngForm' : false); + + return { + pre: function ngFormPreLink(scope, formElement, attr, ctrls) { + var controller = ctrls[0]; + + // if `action` attr is not present on the form, prevent the default action (submission) + if (!('action' in attr)) { + // we can't use jq events because if a form is destroyed during submission the default + // action is not prevented. see #1238 + // + // IE 9 is not affected because it doesn't fire a submit event and try to do a full + // page reload if the form was destroyed by submission of the form via a click handler + // on a button in the form. Looks like an IE9 specific bug. + var handleFormSubmission = function(event) { + scope.$apply(function() { + controller.$commitViewValue(); + controller.$setSubmitted(); + }); + + event.preventDefault(); + }; + + formElement[0].addEventListener('submit', handleFormSubmission); + + // unregister the preventDefault listener so that we don't not leak memory but in a + // way that will achieve the prevention of the default action. + formElement.on('$destroy', function() { + $timeout(function() { + formElement[0].removeEventListener('submit', handleFormSubmission); + }, 0, false); + }); + } - if (msie) { - // Normalize before parse. Refer Implementation Notes on why this is - // done in two steps on IE. - urlParsingNode.setAttribute("href", href); - href = urlParsingNode.href; - } + var parentFormCtrl = ctrls[1] || controller.$$parentForm; + parentFormCtrl.$addControl(controller); - urlParsingNode.setAttribute('href', href); + var setter = nameAttr ? getSetter(controller.$name) : noop; - // urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils - return { - href: urlParsingNode.href, - protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '', - host: urlParsingNode.host, - search: urlParsingNode.search ? urlParsingNode.search.replace(/^\?/, '') : '', - hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '', - hostname: urlParsingNode.hostname, - port: urlParsingNode.port, - pathname: (urlParsingNode.pathname.charAt(0) === '/') - ? urlParsingNode.pathname - : '/' + urlParsingNode.pathname - }; - } + if (nameAttr) { + setter(scope, controller); + attr.$observe(nameAttr, function(newValue) { + if (controller.$name === newValue) return; + setter(scope, undefined); + controller.$$parentForm.$$renameControl(controller, newValue); + setter = getSetter(controller.$name); + setter(scope, controller); + }); + } + formElement.on('$destroy', function() { + controller.$$parentForm.$removeControl(controller); + setter(scope, undefined); + extend(controller, nullFormCtrl); //stop propagating child destruction handlers upwards + }); + } + }; + } + }; - /** - * Parse a request URL and determine whether this is a same-origin request as the application document. - * - * @param {string|object} requestUrl The url of the request as a string that will be resolved - * or a parsed URL object. - * @returns {boolean} Whether the request is for the same origin as the application document. - */ - function urlIsSameOrigin(requestUrl) { - var parsed = (isString(requestUrl)) ? urlResolve(requestUrl) : requestUrl; - return (parsed.protocol === originUrl.protocol && - parsed.host === originUrl.host); + return formDirective; + + function getSetter(expression) { + if (expression === '') { + //create an assignable expression, so forms with an empty name can be renamed later + return $parse('this[""]').assign; + } + return $parse(expression).assign || noop; + } + }]; + }; + + var formDirective = formDirectiveFactory(); + var ngFormDirective = formDirectiveFactory(true); + + + +// helper methods + function setupValidity(instance) { + instance.$$classCache = {}; + instance.$$classCache[INVALID_CLASS] = !(instance.$$classCache[VALID_CLASS] = instance.$$element.hasClass(VALID_CLASS)); } + function addSetValidityMethod(context) { + var clazz = context.clazz, + set = context.set, + unset = context.unset; + + clazz.prototype.$setValidity = function(validationErrorKey, state, controller) { + if (isUndefined(state)) { + createAndSet(this, '$pending', validationErrorKey, controller); + } else { + unsetAndCleanup(this, '$pending', validationErrorKey, controller); + } + if (!isBoolean(state)) { + unset(this.$error, validationErrorKey, controller); + unset(this.$$success, validationErrorKey, controller); + } else { + if (state) { + unset(this.$error, validationErrorKey, controller); + set(this.$$success, validationErrorKey, controller); + } else { + set(this.$error, validationErrorKey, controller); + unset(this.$$success, validationErrorKey, controller); + } + } + if (this.$pending) { + cachedToggleClass(this, PENDING_CLASS, true); + this.$valid = this.$invalid = undefined; + toggleValidationCss(this, '', null); + } else { + cachedToggleClass(this, PENDING_CLASS, false); + this.$valid = isObjectEmpty(this.$error); + this.$invalid = !this.$valid; + toggleValidationCss(this, '', this.$valid); + } - /** - * @ngdoc service - * @name $window - * - * @description - * A reference to the browser's `window` object. While `window` - * is globally available in JavaScript, it causes testability problems, because - * it is a global variable. In angular we always refer to it through the - * `$window` service, so it may be overridden, removed or mocked for testing. - * - * Expressions, like the one defined for the `ngClick` directive in the example - * below, are evaluated with respect to the current scope. Therefore, there is - * no risk of inadvertently coding in a dependency on a global value in such an - * expression. - * - * @example - - - -
    - - -
    -
    - - it('should display the greeting in the input box', function() { - element(by.model('greeting')).sendKeys('Hello, E2E Tests'); - // If we click the button it will block the test runner - // element(':button').click(); - }); - -
    - */ - function $WindowProvider(){ - this.$get = valueFn(window); + // re-read the state as the set/unset methods could have + // combined state in this.$error[validationError] (used for forms), + // where setting/unsetting only increments/decrements the value, + // and does not replace it. + var combinedState; + if (this.$pending && this.$pending[validationErrorKey]) { + combinedState = undefined; + } else if (this.$error[validationErrorKey]) { + combinedState = false; + } else if (this.$$success[validationErrorKey]) { + combinedState = true; + } else { + combinedState = null; + } + + toggleValidationCss(this, validationErrorKey, combinedState); + this.$$parentForm.$setValidity(validationErrorKey, combinedState, this); + }; + + function createAndSet(ctrl, name, value, controller) { + if (!ctrl[name]) { + ctrl[name] = {}; + } + set(ctrl[name], value, controller); + } + + function unsetAndCleanup(ctrl, name, value, controller) { + if (ctrl[name]) { + unset(ctrl[name], value, controller); + } + if (isObjectEmpty(ctrl[name])) { + ctrl[name] = undefined; + } + } + + function cachedToggleClass(ctrl, className, switchValue) { + if (switchValue && !ctrl.$$classCache[className]) { + ctrl.$$animate.addClass(ctrl.$$element, className); + ctrl.$$classCache[className] = true; + } else if (!switchValue && ctrl.$$classCache[className]) { + ctrl.$$animate.removeClass(ctrl.$$element, className); + ctrl.$$classCache[className] = false; + } + } + + function toggleValidationCss(ctrl, validationErrorKey, isValid) { + validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; + + cachedToggleClass(ctrl, VALID_CLASS + validationErrorKey, isValid === true); + cachedToggleClass(ctrl, INVALID_CLASS + validationErrorKey, isValid === false); + } } - /** - * @ngdoc provider - * @name $filterProvider - * @description - * - * Filters are just functions which transform input to an output. However filters need to be - * Dependency Injected. To achieve this a filter definition consists of a factory function which is - * annotated with dependencies and is responsible for creating a filter function. - * - * ```js - * // Filter registration - * function MyModule($provide, $filterProvider) { - * // create a service to demonstrate injection (not always needed) - * $provide.value('greet', function(name){ - * return 'Hello ' + name + '!'; - * }); - * - * // register a filter factory which uses the - * // greet service to demonstrate DI. - * $filterProvider.register('greet', function(greet){ - * // return the filter function which uses the greet service - * // to generate salutation - * return function(text) { - * // filters need to be forgiving so check input validity - * return text && greet(text) || text; - * }; - * }); - * } - * ``` - * - * The filter function is registered with the `$injector` under the filter name suffix with - * `Filter`. - * - * ```js - * it('should be the same instance', inject( - * function($filterProvider) { - * $filterProvider.register('reverse', function(){ - * return ...; - * }); - * }, - * function($filter, reverseFilter) { - * expect($filter('reverse')).toBe(reverseFilter); - * }); - * ``` - * - * - * For more information about how angular filters work, and how to create your own filters, see - * {@link guide/filter Filters} in the Angular Developer Guide. - */ - /** - * @ngdoc method - * @name $filterProvider#register - * @description - * Register filter factory function. - * - * @param {String} name Name of the filter. - * @param {Function} fn The filter factory function which is injectable. - */ + function isObjectEmpty(obj) { + if (obj) { + for (var prop in obj) { + if (obj.hasOwnProperty(prop)) { + return false; + } + } + } + return true; + } + /* global + VALID_CLASS: false, + INVALID_CLASS: false, + PRISTINE_CLASS: false, + DIRTY_CLASS: false, + ngModelMinErr: false +*/ + +// Regex code was initially obtained from SO prior to modification: https://stackoverflow.com/questions/3143070/javascript-regex-iso-datetime#answer-3143231 + var ISO_DATE_REGEXP = /^\d{4,}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+(?:[+-][0-2]\d:[0-5]\d|Z)$/; +// See valid URLs in RFC3987 (http://tools.ietf.org/html/rfc3987) +// Note: We are being more lenient, because browsers are too. +// 1. Scheme +// 2. Slashes +// 3. Username +// 4. Password +// 5. Hostname +// 6. Port +// 7. Path +// 8. Query +// 9. Fragment +// 1111111111111111 222 333333 44444 55555555555555555555555 666 77777777 8888888 999 + var URL_REGEXP = /^[a-z][a-z\d.+-]*:\/*(?:[^:@]+(?::[^@]+)?@)?(?:[^\s:/?#]+|\[[a-f\d:]+])(?::\d+)?(?:\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i; +// eslint-disable-next-line max-len + var EMAIL_REGEXP = /^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/; + var NUMBER_REGEXP = /^\s*(-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/; + var DATE_REGEXP = /^(\d{4,})-(\d{2})-(\d{2})$/; + var DATETIMELOCAL_REGEXP = /^(\d{4,})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/; + var WEEK_REGEXP = /^(\d{4,})-W(\d\d)$/; + var MONTH_REGEXP = /^(\d{4,})-(\d\d)$/; + var TIME_REGEXP = /^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/; + + var PARTIAL_VALIDATION_EVENTS = 'keydown wheel mousedown'; + var PARTIAL_VALIDATION_TYPES = createMap(); + forEach('date,datetime-local,month,time,week'.split(','), function(type) { + PARTIAL_VALIDATION_TYPES[type] = true; + }); - /** - * @ngdoc service - * @name $filter - * @function - * @description - * Filters are used for formatting data displayed to the user. - * - * The general syntax in templates is as follows: - * - * {{ expression [| filter_name[:parameter_value] ... ] }} - * - * @param {String} name Name of the filter function to retrieve - * @return {Function} the filter function - */ - $FilterProvider.$inject = ['$provide']; - function $FilterProvider($provide) { - var suffix = 'Filter'; + var inputType = { /** - * @ngdoc method - * @name $controllerProvider#register - * @param {string|Object} name Name of the filter function, or an object map of filters where - * the keys are the filter names and the values are the filter factories. - * @returns {Object} Registered filter instance, or if a map of filters was provided then a map - * of the registered filter instances. + * @ngdoc input + * @name input[text] + * + * @description + * Standard HTML text input with AngularJS data binding, inherited by most of the `input` elements. + * + * + * @param {string} ngModel Assignable AngularJS expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} required Adds `required` validation error key if the value is not entered. + * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to + * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of + * `required` when you want to data-bind to the `required` attribute. + * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than + * minlength. + * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than + * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of + * any length. + * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string + * that contains the regular expression body that will be converted to a regular expression + * as in the ngPattern directive. + * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue} + * does not match a RegExp found by evaluating the AngularJS expression given in the attribute value. + * If the expression evaluates to a RegExp object, then this is used directly. + * If the expression evaluates to a string, then it will be converted to a RegExp + * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to + * `new RegExp('^abc$')`.
    + * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to + * start at the index of the last search's match, thus not taking the whole input value into + * account. + * @param {string=} ngChange AngularJS expression to be executed when input changes due to user + * interaction with the input element. + * @param {boolean=} [ngTrim=true] If set to false AngularJS will not automatically trim the input. + * This parameter is ignored for input[type=password] controls, which will never trim the + * input. + * + * @example + + + +
    + +
    + + Required! + + Single word only! +
    + text = {{example.text}}
    + myForm.input.$valid = {{myForm.input.$valid}}
    + myForm.input.$error = {{myForm.input.$error}}
    + myForm.$valid = {{myForm.$valid}}
    + myForm.$error.required = {{!!myForm.$error.required}}
    +
    +
    + + var text = element(by.binding('example.text')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('example.text')); + + it('should initialize to model', function() { + expect(text.getText()).toContain('guest'); + expect(valid.getText()).toContain('true'); + }); + + it('should be invalid if empty', function() { + input.clear(); + input.sendKeys(''); + + expect(text.getText()).toEqual('text ='); + expect(valid.getText()).toContain('false'); + }); + + it('should be invalid if multi word', function() { + input.clear(); + input.sendKeys('hello world'); + + expect(valid.getText()).toContain('false'); + }); + +
    */ - function register(name, factory) { - if(isObject(name)) { - var filters = {}; - forEach(name, function(filter, key) { - filters[key] = register(key, filter); - }); - return filters; - } else { - return $provide.factory(name + suffix, factory); - } + 'text': textInputType, + + /** + * @ngdoc input + * @name input[date] + * + * @description + * Input with date validation and transformation. In browsers that do not yet support + * the HTML5 date input, a text element will be used. In that case, text must be entered in a valid ISO-8601 + * date format (yyyy-MM-dd), for example: `2009-01-06`. Since many + * modern browsers do not yet support this input type, it is important to provide cues to users on the + * expected input format via a placeholder or label. + * + * The model must always be a Date object, otherwise AngularJS will throw an error. + * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. + * + * The timezone to be used to read/write the `Date` instance in the model can be defined using + * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. + * + * @param {string} ngModel Assignable AngularJS expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a + * valid ISO date string (yyyy-MM-dd). You can also use interpolation inside this attribute + * (e.g. `min="{{minDate | date:'yyyy-MM-dd'}}"`). Note that `min` will also add native HTML5 + * constraint validation. + * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be + * a valid ISO date string (yyyy-MM-dd). You can also use interpolation inside this attribute + * (e.g. `max="{{maxDate | date:'yyyy-MM-dd'}}"`). Note that `max` will also add native HTML5 + * constraint validation. + * @param {(date|string)=} ngMin Sets the `min` validation constraint to the Date / ISO date string + * the `ngMin` expression evaluates to. Note that it does not set the `min` attribute. + * @param {(date|string)=} ngMax Sets the `max` validation constraint to the Date / ISO date string + * the `ngMax` expression evaluates to. Note that it does not set the `max` attribute. + * @param {string=} required Sets `required` validation error key if the value is not entered. + * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to + * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of + * `required` when you want to data-bind to the `required` attribute. + * @param {string=} ngChange AngularJS expression to be executed when input changes due to user + * interaction with the input element. + * + * @example + + + +
    + + +
    + + Required! + + Not a valid date! +
    + value = {{example.value | date: "yyyy-MM-dd"}}
    + myForm.input.$valid = {{myForm.input.$valid}}
    + myForm.input.$error = {{myForm.input.$error}}
    + myForm.$valid = {{myForm.$valid}}
    + myForm.$error.required = {{!!myForm.$error.required}}
    +
    +
    + + var value = element(by.binding('example.value | date: "yyyy-MM-dd"')); + var valid = element(by.binding('myForm.input.$valid')); + + // currently protractor/webdriver does not support + // sending keys to all known HTML5 input controls + // for various browsers (see https://github.com/angular/protractor/issues/562). + function setInput(val) { + // set the value of the element and force validation. + var scr = "var ipt = document.getElementById('exampleInput'); " + + "ipt.value = '" + val + "';" + + "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; + browser.executeScript(scr); } - this.register = register; - this.$get = ['$injector', function($injector) { - return function(name) { - return $injector.get(name + suffix); - }; - }]; + it('should initialize to model', function() { + expect(value.getText()).toContain('2013-10-22'); + expect(valid.getText()).toContain('myForm.input.$valid = true'); + }); - //////////////////////////////////////// + it('should be invalid if empty', function() { + setInput(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); - /* global - currencyFilter: false, - dateFilter: false, - filterFilter: false, - jsonFilter: false, - limitToFilter: false, - lowercaseFilter: false, - numberFilter: false, - orderByFilter: false, - uppercaseFilter: false, + it('should be invalid if over max', function() { + setInput('2015-01-01'); + expect(value.getText()).toContain(''); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + +
    */ + 'date': createDateInputType('date', DATE_REGEXP, + createDateParser(DATE_REGEXP, ['yyyy', 'MM', 'dd']), + 'yyyy-MM-dd'), - register('currency', currencyFilter); - register('date', dateFilter); - register('filter', filterFilter); - register('json', jsonFilter); - register('limitTo', limitToFilter); - register('lowercase', lowercaseFilter); - register('number', numberFilter); - register('orderBy', orderByFilter); - register('uppercase', uppercaseFilter); - } + /** + * @ngdoc input + * @name input[datetime-local] + * + * @description + * Input with datetime validation and transformation. In browsers that do not yet support + * the HTML5 date input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 + * local datetime format (yyyy-MM-ddTHH:mm:ss), for example: `2010-12-28T14:57:00`. + * + * The model must always be a Date object, otherwise AngularJS will throw an error. + * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. + * + * The timezone to be used to read/write the `Date` instance in the model can be defined using + * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. + * + * @param {string} ngModel Assignable AngularJS expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. + * This must be a valid ISO datetime format (yyyy-MM-ddTHH:mm:ss). You can also use interpolation + * inside this attribute (e.g. `min="{{minDatetimeLocal | date:'yyyy-MM-ddTHH:mm:ss'}}"`). + * Note that `min` will also add native HTML5 constraint validation. + * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. + * This must be a valid ISO datetime format (yyyy-MM-ddTHH:mm:ss). You can also use interpolation + * inside this attribute (e.g. `max="{{maxDatetimeLocal | date:'yyyy-MM-ddTHH:mm:ss'}}"`). + * Note that `max` will also add native HTML5 constraint validation. + * @param {(date|string)=} ngMin Sets the `min` validation error key to the Date / ISO datetime string + * the `ngMin` expression evaluates to. Note that it does not set the `min` attribute. + * @param {(date|string)=} ngMax Sets the `max` validation error key to the Date / ISO datetime string + * the `ngMax` expression evaluates to. Note that it does not set the `max` attribute. + * @param {string=} required Sets `required` validation error key if the value is not entered. + * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to + * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of + * `required` when you want to data-bind to the `required` attribute. + * @param {string=} ngChange AngularJS expression to be executed when input changes due to user + * interaction with the input element. + * + * @example + + + +
    + + +
    + + Required! + + Not a valid date! +
    + value = {{example.value | date: "yyyy-MM-ddTHH:mm:ss"}}
    + myForm.input.$valid = {{myForm.input.$valid}}
    + myForm.input.$error = {{myForm.input.$error}}
    + myForm.$valid = {{myForm.$valid}}
    + myForm.$error.required = {{!!myForm.$error.required}}
    +
    +
    + + var value = element(by.binding('example.value | date: "yyyy-MM-ddTHH:mm:ss"')); + var valid = element(by.binding('myForm.input.$valid')); - /** - * @ngdoc filter - * @name filter - * @function - * - * @description - * Selects a subset of items from `array` and returns it as a new array. - * - * @param {Array} array The source array. - * @param {string|Object|function()} expression The predicate to be used for selecting items from - * `array`. - * - * Can be one of: - * - * - `string`: The string is evaluated as an expression and the resulting value is used for substring match against - * the contents of the `array`. All strings or objects with string properties in `array` that contain this string - * will be returned. The predicate can be negated by prefixing the string with `!`. - * - * - `Object`: A pattern object can be used to filter specific properties on objects contained - * by `array`. For example `{name:"M", phone:"1"}` predicate will return an array of items - * which have property `name` containing "M" and property `phone` containing "1". A special - * property name `$` can be used (as in `{$:"text"}`) to accept a match against any - * property of the object. That's equivalent to the simple substring match with a `string` - * as described above. - * - * - `function(value)`: A predicate function can be used to write arbitrary filters. The function is - * called for each element of `array`. The final result is an array of those elements that - * the predicate returned true for. - * - * @param {function(actual, expected)|true|undefined} comparator Comparator which is used in - * determining if the expected value (from the filter expression) and actual value (from - * the object in the array) should be considered a match. - * - * Can be one of: - * - * - `function(actual, expected)`: - * The function will be given the object value and the predicate value to compare and - * should return true if the item should be included in filtered result. - * - * - `true`: A shorthand for `function(actual, expected) { return angular.equals(expected, actual)}`. - * this is essentially strict comparison of expected and actual. - * - * - `false|undefined`: A short hand for a function which will look for a substring match in case - * insensitive way. - * - * @example - - -
    + // currently protractor/webdriver does not support + // sending keys to all known HTML5 input controls + // for various browsers (https://github.com/angular/protractor/issues/562). + function setInput(val) { + // set the value of the element and force validation. + var scr = "var ipt = document.getElementById('exampleInput'); " + + "ipt.value = '" + val + "';" + + "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; + browser.executeScript(scr); + } - Search: - - - - - - -
    NamePhone
    {{friend.name}}{{friend.phone}}
    -
    - Any:
    - Name only
    - Phone only
    - Equality
    - - - - - - -
    NamePhone
    {{friendObj.name}}{{friendObj.phone}}
    -
    - - var expectFriendNames = function(expectedNames, key) { - element.all(by.repeater(key + ' in friends').column(key + '.name')).then(function(arr) { - arr.forEach(function(wd, i) { - expect(wd.getText()).toMatch(expectedNames[i]); - }); - }); - }; + it('should initialize to model', function() { + expect(value.getText()).toContain('2010-12-28T14:57:00'); + expect(valid.getText()).toContain('myForm.input.$valid = true'); + }); - it('should search across all fields when filtering with a string', function() { - var searchText = element(by.model('searchText')); - searchText.clear(); - searchText.sendKeys('m'); - expectFriendNames(['Mary', 'Mike', 'Adam'], 'friend'); + it('should be invalid if empty', function() { + setInput(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); - searchText.clear(); - searchText.sendKeys('76'); - expectFriendNames(['John', 'Julie'], 'friend'); - }); + it('should be invalid if over max', function() { + setInput('2015-01-01T23:59:00'); + expect(value.getText()).toContain(''); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + +
    + */ + 'datetime-local': createDateInputType('datetimelocal', DATETIMELOCAL_REGEXP, + createDateParser(DATETIMELOCAL_REGEXP, ['yyyy', 'MM', 'dd', 'HH', 'mm', 'ss', 'sss']), + 'yyyy-MM-ddTHH:mm:ss.sss'), - it('should search in specific fields when filtering with a predicate object', function() { - var searchAny = element(by.model('search.$')); - searchAny.clear(); - searchAny.sendKeys('i'); - expectFriendNames(['Mary', 'Mike', 'Julie', 'Juliette'], 'friendObj'); - }); - it('should use a equal comparison when comparator is true', function() { - var searchName = element(by.model('search.name')); - var strict = element(by.model('strict')); - searchName.clear(); - searchName.sendKeys('Julie'); - strict.click(); - expectFriendNames(['Julie'], 'friendObj'); - }); -
    -
    - */ - function filterFilter() { - return function(array, expression, comparator) { - if (!isArray(array)) return array; + /** + * @ngdoc input + * @name input[time] + * + * @description + * Input with time validation and transformation. In browsers that do not yet support + * the HTML5 time input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 + * local time format (HH:mm:ss), for example: `14:57:00`. Model must be a Date object. This binding will always output a + * Date object to the model of January 1, 1970, or local date `new Date(1970, 0, 1, HH, mm, ss)`. + * + * The model must always be a Date object, otherwise AngularJS will throw an error. + * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. + * + * The timezone to be used to read/write the `Date` instance in the model can be defined using + * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. + * + * @param {string} ngModel Assignable AngularJS expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. + * This must be a valid ISO time format (HH:mm:ss). You can also use interpolation inside this + * attribute (e.g. `min="{{minTime | date:'HH:mm:ss'}}"`). Note that `min` will also add + * native HTML5 constraint validation. + * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. + * This must be a valid ISO time format (HH:mm:ss). You can also use interpolation inside this + * attribute (e.g. `max="{{maxTime | date:'HH:mm:ss'}}"`). Note that `max` will also add + * native HTML5 constraint validation. + * @param {(date|string)=} ngMin Sets the `min` validation constraint to the Date / ISO time string the + * `ngMin` expression evaluates to. Note that it does not set the `min` attribute. + * @param {(date|string)=} ngMax Sets the `max` validation constraint to the Date / ISO time string the + * `ngMax` expression evaluates to. Note that it does not set the `max` attribute. + * @param {string=} required Sets `required` validation error key if the value is not entered. + * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to + * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of + * `required` when you want to data-bind to the `required` attribute. + * @param {string=} ngChange AngularJS expression to be executed when input changes due to user + * interaction with the input element. + * + * @example + + + +
    + + +
    + + Required! + + Not a valid date! +
    + value = {{example.value | date: "HH:mm:ss"}}
    + myForm.input.$valid = {{myForm.input.$valid}}
    + myForm.input.$error = {{myForm.input.$error}}
    + myForm.$valid = {{myForm.$valid}}
    + myForm.$error.required = {{!!myForm.$error.required}}
    +
    +
    + + var value = element(by.binding('example.value | date: "HH:mm:ss"')); + var valid = element(by.binding('myForm.input.$valid')); - var comparatorType = typeof(comparator), - predicates = []; + // currently protractor/webdriver does not support + // sending keys to all known HTML5 input controls + // for various browsers (https://github.com/angular/protractor/issues/562). + function setInput(val) { + // set the value of the element and force validation. + var scr = "var ipt = document.getElementById('exampleInput'); " + + "ipt.value = '" + val + "';" + + "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; + browser.executeScript(scr); + } - predicates.check = function(value) { - for (var j = 0; j < predicates.length; j++) { - if(!predicates[j](value)) { - return false; - } - } - return true; - }; + it('should initialize to model', function() { + expect(value.getText()).toContain('14:57:00'); + expect(valid.getText()).toContain('myForm.input.$valid = true'); + }); - if (comparatorType !== 'function') { - if (comparatorType === 'boolean' && comparator) { - comparator = function(obj, text) { - return angular.equals(obj, text); - }; - } else { - comparator = function(obj, text) { - if (obj && text && typeof obj === 'object' && typeof text === 'object') { - for (var objKey in obj) { - if (objKey.charAt(0) !== '$' && hasOwnProperty.call(obj, objKey) && - comparator(obj[objKey], text[objKey])) { - return true; - } - } - return false; - } - text = (''+text).toLowerCase(); - return (''+obj).toLowerCase().indexOf(text) > -1; - }; - } - } + it('should be invalid if empty', function() { + setInput(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); - var search = function(obj, text){ - if (typeof text == 'string' && text.charAt(0) === '!') { - return !search(obj, text.substr(1)); - } - switch (typeof obj) { - case "boolean": - case "number": - case "string": - return comparator(obj, text); - case "object": - switch (typeof text) { - case "object": - return comparator(obj, text); - default: - for ( var objKey in obj) { - if (objKey.charAt(0) !== '$' && search(obj[objKey], text)) { - return true; - } - } - break; - } - return false; - case "array": - for ( var i = 0; i < obj.length; i++) { - if (search(obj[i], text)) { - return true; - } - } - return false; - default: - return false; - } - }; - switch (typeof expression) { - case "boolean": - case "number": - case "string": - // Set up expression object and fall through - expression = {$:expression}; - // jshint -W086 - case "object": - // jshint +W086 - for (var key in expression) { - (function(path) { - if (typeof expression[path] == 'undefined') return; - predicates.push(function(value) { - return search(path == '$' ? value : (value && value[path]), expression[path]); - }); - })(key); - } - break; - case 'function': - predicates.push(expression); - break; - default: - return array; - } - var filtered = []; - for ( var j = 0; j < array.length; j++) { - var value = array[j]; - if (predicates.check(value)) { - filtered.push(value); - } - } - return filtered; - }; - } + it('should be invalid if over max', function() { + setInput('23:59:00'); + expect(value.getText()).toContain(''); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + +
    + */ + 'time': createDateInputType('time', TIME_REGEXP, + createDateParser(TIME_REGEXP, ['HH', 'mm', 'ss', 'sss']), + 'HH:mm:ss.sss'), + + /** + * @ngdoc input + * @name input[week] + * + * @description + * Input with week-of-the-year validation and transformation to Date. In browsers that do not yet support + * the HTML5 week input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 + * week format (yyyy-W##), for example: `2013-W02`. + * + * The model must always be a Date object, otherwise AngularJS will throw an error. + * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. + * + * The timezone to be used to read/write the `Date` instance in the model can be defined using + * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. + * + * @param {string} ngModel Assignable AngularJS expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. + * This must be a valid ISO week format (yyyy-W##). You can also use interpolation inside this + * attribute (e.g. `min="{{minWeek | date:'yyyy-Www'}}"`). Note that `min` will also add + * native HTML5 constraint validation. + * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. + * This must be a valid ISO week format (yyyy-W##). You can also use interpolation inside this + * attribute (e.g. `max="{{maxWeek | date:'yyyy-Www'}}"`). Note that `max` will also add + * native HTML5 constraint validation. + * @param {(date|string)=} ngMin Sets the `min` validation constraint to the Date / ISO week string + * the `ngMin` expression evaluates to. Note that it does not set the `min` attribute. + * @param {(date|string)=} ngMax Sets the `max` validation constraint to the Date / ISO week string + * the `ngMax` expression evaluates to. Note that it does not set the `max` attribute. + * @param {string=} required Sets `required` validation error key if the value is not entered. + * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to + * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of + * `required` when you want to data-bind to the `required` attribute. + * @param {string=} ngChange AngularJS expression to be executed when input changes due to user + * interaction with the input element. + * + * @example + + + +
    + +
    + + Required! + + Not a valid date! +
    + value = {{example.value | date: "yyyy-Www"}}
    + myForm.input.$valid = {{myForm.input.$valid}}
    + myForm.input.$error = {{myForm.input.$error}}
    + myForm.$valid = {{myForm.$valid}}
    + myForm.$error.required = {{!!myForm.$error.required}}
    +
    +
    + + var value = element(by.binding('example.value | date: "yyyy-Www"')); + var valid = element(by.binding('myForm.input.$valid')); - /** - * @ngdoc filter - * @name currency - * @function - * - * @description - * Formats a number as a currency (ie $1,234.56). When no currency symbol is provided, default - * symbol for current locale is used. - * - * @param {number} amount Input to filter. - * @param {string=} symbol Currency symbol or identifier to be displayed. - * @returns {string} Formatted number. - * - * - * @example - - - -
    -
    - default currency symbol ($): {{amount | currency}}
    - custom currency identifier (USD$): {{amount | currency:"USD$"}} -
    -
    - - it('should init with 1234.56', function() { - expect(element(by.id('currency-default')).getText()).toBe('$1,234.56'); - expect(element(by.binding('amount | currency:"USD$"')).getText()).toBe('USD$1,234.56'); - }); - it('should update', function() { - if (browser.params.browser == 'safari') { - // Safari does not understand the minus key. See - // https://github.com/angular/protractor/issues/481 - return; - } - element(by.model('amount')).clear(); - element(by.model('amount')).sendKeys('-1234'); - expect(element(by.id('currency-default')).getText()).toBe('($1,234.00)'); - expect(element(by.binding('amount | currency:"USD$"')).getText()).toBe('(USD$1,234.00)'); - }); - -
    - */ - currencyFilter.$inject = ['$locale']; - function currencyFilter($locale) { - var formats = $locale.NUMBER_FORMATS; - return function(amount, currencySymbol){ - if (isUndefined(currencySymbol)) currencySymbol = formats.CURRENCY_SYM; - return formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, 2). - replace(/\u00A4/g, currencySymbol); - }; - } + // currently protractor/webdriver does not support + // sending keys to all known HTML5 input controls + // for various browsers (https://github.com/angular/protractor/issues/562). + function setInput(val) { + // set the value of the element and force validation. + var scr = "var ipt = document.getElementById('exampleInput'); " + + "ipt.value = '" + val + "';" + + "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; + browser.executeScript(scr); + } - /** - * @ngdoc filter - * @name number - * @function - * - * @description - * Formats a number as text. - * - * If the input is not a number an empty string is returned. - * - * @param {number|string} number Number to format. - * @param {(number|string)=} fractionSize Number of decimal places to round the number to. - * If this is not provided then the fraction size is computed from the current locale's number - * formatting pattern. In the case of the default locale, it will be 3. - * @returns {string} Number rounded to decimalPlaces and places a “,” after each third digit. - * - * @example - - - -
    - Enter number:
    - Default formatting: {{val | number}}
    - No fractions: {{val | number:0}}
    - Negative number: {{-val | number:4}} -
    -
    - - it('should format numbers', function() { - expect(element(by.id('number-default')).getText()).toBe('1,234.568'); - expect(element(by.binding('val | number:0')).getText()).toBe('1,235'); - expect(element(by.binding('-val | number:4')).getText()).toBe('-1,234.5679'); - }); + it('should initialize to model', function() { + expect(value.getText()).toContain('2013-W01'); + expect(valid.getText()).toContain('myForm.input.$valid = true'); + }); - it('should update', function() { - element(by.model('val')).clear(); - element(by.model('val')).sendKeys('3374.333'); - expect(element(by.id('number-default')).getText()).toBe('3,374.333'); - expect(element(by.binding('val | number:0')).getText()).toBe('3,374'); - expect(element(by.binding('-val | number:4')).getText()).toBe('-3,374.3330'); + it('should be invalid if empty', function() { + setInput(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('myForm.input.$valid = false'); }); - -
    - */ + it('should be invalid if over max', function() { + setInput('2015-W01'); + expect(value.getText()).toContain(''); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); +
    +
    + */ + 'week': createDateInputType('week', WEEK_REGEXP, weekParser, 'yyyy-Www'), - numberFilter.$inject = ['$locale']; - function numberFilter($locale) { - var formats = $locale.NUMBER_FORMATS; - return function(number, fractionSize) { - return formatNumber(number, formats.PATTERNS[0], formats.GROUP_SEP, formats.DECIMAL_SEP, - fractionSize); - }; - } + /** + * @ngdoc input + * @name input[month] + * + * @description + * Input with month validation and transformation. In browsers that do not yet support + * the HTML5 month input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 + * month format (yyyy-MM), for example: `2009-01`. + * + * The model must always be a Date object, otherwise AngularJS will throw an error. + * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. + * If the model is not set to the first of the month, the next view to model update will set it + * to the first of the month. + * + * The timezone to be used to read/write the `Date` instance in the model can be defined using + * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. + * + * @param {string} ngModel Assignable AngularJS expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. + * This must be a valid ISO month format (yyyy-MM). You can also use interpolation inside this + * attribute (e.g. `min="{{minMonth | date:'yyyy-MM'}}"`). Note that `min` will also add + * native HTML5 constraint validation. + * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. + * This must be a valid ISO month format (yyyy-MM). You can also use interpolation inside this + * attribute (e.g. `max="{{maxMonth | date:'yyyy-MM'}}"`). Note that `max` will also add + * native HTML5 constraint validation. + * @param {(date|string)=} ngMin Sets the `min` validation constraint to the Date / ISO week string + * the `ngMin` expression evaluates to. Note that it does not set the `min` attribute. + * @param {(date|string)=} ngMax Sets the `max` validation constraint to the Date / ISO week string + * the `ngMax` expression evaluates to. Note that it does not set the `max` attribute. - var DECIMAL_SEP = '.'; - function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { - if (number == null || !isFinite(number) || isObject(number)) return ''; - - var isNegative = number < 0; - number = Math.abs(number); - var numStr = number + '', - formatedText = '', - parts = []; - - var hasExponent = false; - if (numStr.indexOf('e') !== -1) { - var match = numStr.match(/([\d\.]+)e(-?)(\d+)/); - if (match && match[2] == '-' && match[3] > fractionSize + 1) { - numStr = '0'; - } else { - formatedText = numStr; - hasExponent = true; - } - } + * @param {string=} required Sets `required` validation error key if the value is not entered. + * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to + * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of + * `required` when you want to data-bind to the `required` attribute. + * @param {string=} ngChange AngularJS expression to be executed when input changes due to user + * interaction with the input element. + * + * @example + + + +
    + + +
    + + Required! + + Not a valid month! +
    + value = {{example.value | date: "yyyy-MM"}}
    + myForm.input.$valid = {{myForm.input.$valid}}
    + myForm.input.$error = {{myForm.input.$error}}
    + myForm.$valid = {{myForm.$valid}}
    + myForm.$error.required = {{!!myForm.$error.required}}
    +
    +
    + + var value = element(by.binding('example.value | date: "yyyy-MM"')); + var valid = element(by.binding('myForm.input.$valid')); - if (!hasExponent) { - var fractionLen = (numStr.split(DECIMAL_SEP)[1] || '').length; + // currently protractor/webdriver does not support + // sending keys to all known HTML5 input controls + // for various browsers (https://github.com/angular/protractor/issues/562). + function setInput(val) { + // set the value of the element and force validation. + var scr = "var ipt = document.getElementById('exampleInput'); " + + "ipt.value = '" + val + "';" + + "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; + browser.executeScript(scr); + } - // determine fractionSize if it is not specified - if (isUndefined(fractionSize)) { - fractionSize = Math.min(Math.max(pattern.minFrac, fractionLen), pattern.maxFrac); - } + it('should initialize to model', function() { + expect(value.getText()).toContain('2013-10'); + expect(valid.getText()).toContain('myForm.input.$valid = true'); + }); - var pow = Math.pow(10, fractionSize); - number = Math.round(number * pow) / pow; - var fraction = ('' + number).split(DECIMAL_SEP); - var whole = fraction[0]; - fraction = fraction[1] || ''; + it('should be invalid if empty', function() { + setInput(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); - var i, pos = 0, - lgroup = pattern.lgSize, - group = pattern.gSize; + it('should be invalid if over max', function() { + setInput('2015-01'); + expect(value.getText()).toContain(''); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + +
    + */ + 'month': createDateInputType('month', MONTH_REGEXP, + createDateParser(MONTH_REGEXP, ['yyyy', 'MM']), + 'yyyy-MM'), - if (whole.length >= (lgroup + group)) { - pos = whole.length - lgroup; - for (i = 0; i < pos; i++) { - if ((pos - i)%group === 0 && i !== 0) { - formatedText += groupSep; - } - formatedText += whole.charAt(i); - } - } + /** + * @ngdoc input + * @name input[number] + * + * @description + * Text input with number validation and transformation. Sets the `number` validation + * error if not a valid number. + * + *
    + * The model must always be of type `number` otherwise AngularJS will throw an error. + * Be aware that a string containing a number is not enough. See the {@link ngModel:numfmt} + * error docs for more information and an example of how to convert your model if necessary. + *
    + * + * ## Issues with HTML5 constraint validation + * + * In browsers that follow the + * [HTML5 specification](https://html.spec.whatwg.org/multipage/forms.html#number-state-%28type=number%29), + * `input[number]` does not work as expected with {@link ngModelOptions `ngModelOptions.allowInvalid`}. + * If a non-number is entered in the input, the browser will report the value as an empty string, + * which means the view / model values in `ngModel` and subsequently the scope value + * will also be an empty string. + * + * + * @param {string} ngModel Assignable AngularJS expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. + * Can be interpolated. + * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. + * Can be interpolated. + * @param {string=} ngMin Like `min`, sets the `min` validation error key if the value entered is less than `ngMin`, + * but does not trigger HTML5 native validation. Takes an expression. + * @param {string=} ngMax Like `max`, sets the `max` validation error key if the value entered is greater than `ngMax`, + * but does not trigger HTML5 native validation. Takes an expression. + * @param {string=} step Sets the `step` validation error key if the value entered does not fit the `step` constraint. + * Can be interpolated. + * @param {string=} ngStep Like `step`, sets the `step` validation error key if the value entered does not fit the `ngStep` constraint, + * but does not trigger HTML5 native validation. Takes an expression. + * @param {string=} required Sets `required` validation error key if the value is not entered. + * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to + * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of + * `required` when you want to data-bind to the `required` attribute. + * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than + * minlength. + * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than + * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of + * any length. + * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string + * that contains the regular expression body that will be converted to a regular expression + * as in the ngPattern directive. + * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue} + * does not match a RegExp found by evaluating the AngularJS expression given in the attribute value. + * If the expression evaluates to a RegExp object, then this is used directly. + * If the expression evaluates to a string, then it will be converted to a RegExp + * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to + * `new RegExp('^abc$')`.
    + * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to + * start at the index of the last search's match, thus not taking the whole input value into + * account. + * @param {string=} ngChange AngularJS expression to be executed when input changes due to user + * interaction with the input element. + * + * @example + + + +
    + +
    + + Required! + + Not valid number! +
    + value = {{example.value}}
    + myForm.input.$valid = {{myForm.input.$valid}}
    + myForm.input.$error = {{myForm.input.$error}}
    + myForm.$valid = {{myForm.$valid}}
    + myForm.$error.required = {{!!myForm.$error.required}}
    +
    +
    + + var value = element(by.binding('example.value')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('example.value')); - for (i = pos; i < whole.length; i++) { - if ((whole.length - i)%lgroup === 0 && i !== 0) { - formatedText += groupSep; - } - formatedText += whole.charAt(i); - } + it('should initialize to model', function() { + expect(value.getText()).toContain('12'); + expect(valid.getText()).toContain('true'); + }); - // format fraction part. - while(fraction.length < fractionSize) { - fraction += '0'; - } + it('should be invalid if empty', function() { + input.clear(); + input.sendKeys(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('false'); + }); - if (fractionSize && fractionSize !== "0") formatedText += decimalSep + fraction.substr(0, fractionSize); - } else { + it('should be invalid if over max', function() { + input.clear(); + input.sendKeys('123'); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('false'); + }); + +
    + */ + 'number': numberInputType, - if (fractionSize > 0 && number > -1 && number < 1) { - formatedText = number.toFixed(fractionSize); - } - } - parts.push(isNegative ? pattern.negPre : pattern.posPre); - parts.push(formatedText); - parts.push(isNegative ? pattern.negSuf : pattern.posSuf); - return parts.join(''); - } + /** + * @ngdoc input + * @name input[url] + * + * @description + * Text input with URL validation. Sets the `url` validation error key if the content is not a + * valid URL. + * + *
    + * **Note:** `input[url]` uses a regex to validate urls that is derived from the regex + * used in Chromium. If you need stricter validation, you can use `ng-pattern` or modify + * the built-in validators (see the {@link guide/forms Forms guide}) + *
    + * + * @param {string} ngModel Assignable AngularJS expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} required Sets `required` validation error key if the value is not entered. + * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to + * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of + * `required` when you want to data-bind to the `required` attribute. + * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than + * minlength. + * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than + * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of + * any length. + * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string + * that contains the regular expression body that will be converted to a regular expression + * as in the ngPattern directive. + * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue} + * does not match a RegExp found by evaluating the AngularJS expression given in the attribute value. + * If the expression evaluates to a RegExp object, then this is used directly. + * If the expression evaluates to a string, then it will be converted to a RegExp + * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to + * `new RegExp('^abc$')`.
    + * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to + * start at the index of the last search's match, thus not taking the whole input value into + * account. + * @param {string=} ngChange AngularJS expression to be executed when input changes due to user + * interaction with the input element. + * + * @example + + + +
    +
    + + var text = element(by.binding('url.text')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('url.text')); + + it('should initialize to model', function() { + expect(text.getText()).toContain('http://google.com'); + expect(valid.getText()).toContain('true'); + }); - function padNumber(num, digits, trim) { - var neg = ''; - if (num < 0) { - neg = '-'; - num = -num; - } - num = '' + num; - while(num.length < digits) num = '0' + num; - if (trim) - num = num.substr(num.length - digits); - return neg + num; - } + it('should be invalid if empty', function() { + input.clear(); + input.sendKeys(''); + expect(text.getText()).toEqual('text ='); + expect(valid.getText()).toContain('false'); + }); - function dateGetter(name, size, offset, trim) { - offset = offset || 0; - return function(date) { - var value = date['get' + name](); - if (offset > 0 || value > -offset) - value += offset; - if (value === 0 && offset == -12 ) value = 12; - return padNumber(value, size, trim); - }; - } + it('should be invalid if not url', function() { + input.clear(); + input.sendKeys('box'); - function dateStrGetter(name, shortForm) { - return function(date, formats) { - var value = date['get' + name](); - var get = uppercase(shortForm ? ('SHORT' + name) : name); + expect(valid.getText()).toContain('false'); + }); + +
    + */ + 'url': urlInputType, - return formats[get][value]; - }; - } - function timeZoneGetter(date) { - var zone = -1 * date.getTimezoneOffset(); - var paddedZone = (zone >= 0) ? "+" : ""; + /** + * @ngdoc input + * @name input[email] + * + * @description + * Text input with email validation. Sets the `email` validation error key if not a valid email + * address. + * + *
    + * **Note:** `input[email]` uses a regex to validate email addresses that is derived from the regex + * used in Chromium. If you need stricter validation (e.g. requiring a top-level domain), you can + * use `ng-pattern` or modify the built-in validators (see the {@link guide/forms Forms guide}) + *
    + * + * @param {string} ngModel Assignable AngularJS expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} required Sets `required` validation error key if the value is not entered. + * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to + * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of + * `required` when you want to data-bind to the `required` attribute. + * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than + * minlength. + * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than + * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of + * any length. + * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string + * that contains the regular expression body that will be converted to a regular expression + * as in the ngPattern directive. + * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue} + * does not match a RegExp found by evaluating the AngularJS expression given in the attribute value. + * If the expression evaluates to a RegExp object, then this is used directly. + * If the expression evaluates to a string, then it will be converted to a RegExp + * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to + * `new RegExp('^abc$')`.
    + * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to + * start at the index of the last search's match, thus not taking the whole input value into + * account. + * @param {string=} ngChange AngularJS expression to be executed when input changes due to user + * interaction with the input element. + * + * @example + + + +
    + +
    + + Required! + + Not valid email! +
    + text = {{email.text}}
    + myForm.input.$valid = {{myForm.input.$valid}}
    + myForm.input.$error = {{myForm.input.$error}}
    + myForm.$valid = {{myForm.$valid}}
    + myForm.$error.required = {{!!myForm.$error.required}}
    + myForm.$error.email = {{!!myForm.$error.email}}
    +
    +
    + + var text = element(by.binding('email.text')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('email.text')); - paddedZone += padNumber(Math[zone > 0 ? 'floor' : 'ceil'](zone / 60), 2) + - padNumber(Math.abs(zone % 60), 2); + it('should initialize to model', function() { + expect(text.getText()).toContain('me@example.com'); + expect(valid.getText()).toContain('true'); + }); - return paddedZone; - } + it('should be invalid if empty', function() { + input.clear(); + input.sendKeys(''); + expect(text.getText()).toEqual('text ='); + expect(valid.getText()).toContain('false'); + }); - function ampmGetter(date, formats) { - return date.getHours() < 12 ? formats.AMPMS[0] : formats.AMPMS[1]; - } + it('should be invalid if not email', function() { + input.clear(); + input.sendKeys('xxx'); - var DATE_FORMATS = { - yyyy: dateGetter('FullYear', 4), - yy: dateGetter('FullYear', 2, 0, true), - y: dateGetter('FullYear', 1), - MMMM: dateStrGetter('Month'), - MMM: dateStrGetter('Month', true), - MM: dateGetter('Month', 2, 1), - M: dateGetter('Month', 1, 1), - dd: dateGetter('Date', 2), - d: dateGetter('Date', 1), - HH: dateGetter('Hours', 2), - H: dateGetter('Hours', 1), - hh: dateGetter('Hours', 2, -12), - h: dateGetter('Hours', 1, -12), - mm: dateGetter('Minutes', 2), - m: dateGetter('Minutes', 1), - ss: dateGetter('Seconds', 2), - s: dateGetter('Seconds', 1), - // while ISO 8601 requires fractions to be prefixed with `.` or `,` - // we can be just safely rely on using `sss` since we currently don't support single or two digit fractions - sss: dateGetter('Milliseconds', 3), - EEEE: dateStrGetter('Day'), - EEE: dateStrGetter('Day', true), - a: ampmGetter, - Z: timeZoneGetter - }; + expect(valid.getText()).toContain('false'); + }); + +
    + */ + 'email': emailInputType, - var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/, - NUMBER_STRING = /^\-?\d+$/; - /** - * @ngdoc filter - * @name date - * @function - * - * @description - * Formats `date` to a string based on the requested `format`. - * - * `format` string can be composed of the following elements: - * - * * `'yyyy'`: 4 digit representation of year (e.g. AD 1 => 0001, AD 2010 => 2010) - * * `'yy'`: 2 digit representation of year, padded (00-99). (e.g. AD 2001 => 01, AD 2010 => 10) - * * `'y'`: 1 digit representation of year, e.g. (AD 1 => 1, AD 199 => 199) - * * `'MMMM'`: Month in year (January-December) - * * `'MMM'`: Month in year (Jan-Dec) - * * `'MM'`: Month in year, padded (01-12) - * * `'M'`: Month in year (1-12) - * * `'dd'`: Day in month, padded (01-31) - * * `'d'`: Day in month (1-31) - * * `'EEEE'`: Day in Week,(Sunday-Saturday) - * * `'EEE'`: Day in Week, (Sun-Sat) - * * `'HH'`: Hour in day, padded (00-23) - * * `'H'`: Hour in day (0-23) - * * `'hh'`: Hour in am/pm, padded (01-12) - * * `'h'`: Hour in am/pm, (1-12) - * * `'mm'`: Minute in hour, padded (00-59) - * * `'m'`: Minute in hour (0-59) - * * `'ss'`: Second in minute, padded (00-59) - * * `'s'`: Second in minute (0-59) - * * `'.sss' or ',sss'`: Millisecond in second, padded (000-999) - * * `'a'`: am/pm marker - * * `'Z'`: 4 digit (+sign) representation of the timezone offset (-1200-+1200) - * - * `format` string can also be one of the following predefined - * {@link guide/i18n localizable formats}: - * - * * `'medium'`: equivalent to `'MMM d, y h:mm:ss a'` for en_US locale - * (e.g. Sep 3, 2010 12:05:08 pm) - * * `'short'`: equivalent to `'M/d/yy h:mm a'` for en_US locale (e.g. 9/3/10 12:05 pm) - * * `'fullDate'`: equivalent to `'EEEE, MMMM d,y'` for en_US locale - * (e.g. Friday, September 3, 2010) - * * `'longDate'`: equivalent to `'MMMM d, y'` for en_US locale (e.g. September 3, 2010) - * * `'mediumDate'`: equivalent to `'MMM d, y'` for en_US locale (e.g. Sep 3, 2010) - * * `'shortDate'`: equivalent to `'M/d/yy'` for en_US locale (e.g. 9/3/10) - * * `'mediumTime'`: equivalent to `'h:mm:ss a'` for en_US locale (e.g. 12:05:08 pm) - * * `'shortTime'`: equivalent to `'h:mm a'` for en_US locale (e.g. 12:05 pm) - * - * `format` string can contain literal values. These need to be quoted with single quotes (e.g. - * `"h 'in the morning'"`). In order to output single quote, use two single quotes in a sequence - * (e.g. `"h 'o''clock'"`). - * - * @param {(Date|number|string)} date Date to format either as Date object, milliseconds (string or - * number) or various ISO 8601 datetime string formats (e.g. yyyy-MM-ddTHH:mm:ss.SSSZ and its - * shorter versions like yyyy-MM-ddTHH:mmZ, yyyy-MM-dd or yyyyMMddTHHmmssZ). If no timezone is - * specified in the string input, the time is considered to be in the local timezone. - * @param {string=} format Formatting rules (see Description). If not specified, - * `mediumDate` is used. - * @returns {string} Formatted string or the input if input is not recognized as date/millis. - * - * @example - - - {{1288323623006 | date:'medium'}}: - {{1288323623006 | date:'medium'}}
    - {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}: - {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}
    - {{1288323623006 | date:'MM/dd/yyyy @ h:mma'}}: - {{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}
    -
    - - it('should format date', function() { - expect(element(by.binding("1288323623006 | date:'medium'")).getText()). - toMatch(/Oct 2\d, 2010 \d{1,2}:\d{2}:\d{2} (AM|PM)/); - expect(element(by.binding("1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'")).getText()). - toMatch(/2010\-10\-2\d \d{2}:\d{2}:\d{2} (\-|\+)?\d{4}/); - expect(element(by.binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")).getText()). - toMatch(/10\/2\d\/2010 @ \d{1,2}:\d{2}(AM|PM)/); - }); - -
    - */ - dateFilter.$inject = ['$locale']; - function dateFilter($locale) { + /** + * @ngdoc input + * @name input[radio] + * + * @description + * HTML radio button. + * + * @param {string} ngModel Assignable AngularJS expression to data-bind to. + * @param {string} value The value to which the `ngModel` expression should be set when selected. + * Note that `value` only supports `string` values, i.e. the scope model needs to be a string, + * too. Use `ngValue` if you need complex models (`number`, `object`, ...). + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} ngChange AngularJS expression to be executed when input changes due to user + * interaction with the input element. + * @param {string} ngValue AngularJS expression to which `ngModel` will be be set when the radio + * is selected. Should be used instead of the `value` attribute if you need + * a non-string `ngModel` (`boolean`, `array`, ...). + * + * @example + + + +
    +
    +
    +
    + color = {{color.name | json}}
    +
    + Note that `ng-value="specialValue"` sets radio item's value to be the value of `$scope.specialValue`. +
    + + it('should change state', function() { + var inputs = element.all(by.model('color.name')); + var color = element(by.binding('color.name')); + expect(color.getText()).toContain('blue'); - var R_ISO8601_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/; - // 1 2 3 4 5 6 7 8 9 10 11 - function jsonStringToDate(string) { - var match; - if (match = string.match(R_ISO8601_STR)) { - var date = new Date(0), - tzHour = 0, - tzMin = 0, - dateSetter = match[8] ? date.setUTCFullYear : date.setFullYear, - timeSetter = match[8] ? date.setUTCHours : date.setHours; + inputs.get(0).click(); + expect(color.getText()).toContain('red'); - if (match[9]) { - tzHour = int(match[9] + match[10]); - tzMin = int(match[9] + match[11]); - } - dateSetter.call(date, int(match[1]), int(match[2]) - 1, int(match[3])); - var h = int(match[4]||0) - tzHour; - var m = int(match[5]||0) - tzMin; - var s = int(match[6]||0); - var ms = Math.round(parseFloat('0.' + (match[7]||0)) * 1000); - timeSetter.call(date, h, m, s, ms); - return date; - } - return string; - } + inputs.get(1).click(); + expect(color.getText()).toContain('green'); + }); + +
    + */ + 'radio': radioInputType, + /** + * @ngdoc input + * @name input[range] + * + * @description + * Native range input with validation and transformation. + * + * The model for the range input must always be a `Number`. + * + * IE9 and other browsers that do not support the `range` type fall back + * to a text input without any default values for `min`, `max` and `step`. Model binding, + * validation and number parsing are nevertheless supported. + * + * Browsers that support range (latest Chrome, Safari, Firefox, Edge) treat `input[range]` + * in a way that never allows the input to hold an invalid value. That means: + * - any non-numerical value is set to `(max + min) / 2`. + * - any numerical value that is less than the current min val, or greater than the current max val + * is set to the min / max val respectively. + * - additionally, the current `step` is respected, so the nearest value that satisfies a step + * is used. + * + * See the [HTML Spec on input[type=range]](https://www.w3.org/TR/html5/forms.html#range-state-(type=range)) + * for more info. + * + * This has the following consequences for AngularJS: + * + * Since the element value should always reflect the current model value, a range input + * will set the bound ngModel expression to the value that the browser has set for the + * input element. For example, in the following input ``, + * if the application sets `model.value = null`, the browser will set the input to `'50'`. + * AngularJS will then set the model to `50`, to prevent input and model value being out of sync. + * + * That means the model for range will immediately be set to `50` after `ngModel` has been + * initialized. It also means a range input can never have the required error. + * + * This does not only affect changes to the model value, but also to the values of the `min`, + * `max`, and `step` attributes. When these change in a way that will cause the browser to modify + * the input value, AngularJS will also update the model value. + * + * Automatic value adjustment also means that a range input element can never have the `required`, + * `min`, or `max` errors. + * + * However, `step` is currently only fully implemented by Firefox. Other browsers have problems + * when the step value changes dynamically - they do not adjust the element value correctly, but + * instead may set the `stepMismatch` error. If that's the case, the AngularJS will set the `step` + * error on the input, and set the model to `undefined`. + * + * Note that `input[range]` is not compatible with`ngMax`, `ngMin`, and `ngStep`, because they do + * not set the `min` and `max` attributes, which means that the browser won't automatically adjust + * the input value based on their values, and will always assume min = 0, max = 100, and step = 1. + * + * @param {string} ngModel Assignable AngularJS expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} min Sets the `min` validation to ensure that the value entered is greater + * than `min`. Can be interpolated. + * @param {string=} max Sets the `max` validation to ensure that the value entered is less than `max`. + * Can be interpolated. + * @param {string=} step Sets the `step` validation to ensure that the value entered matches the `step` + * Can be interpolated. + * @param {string=} ngChange AngularJS expression to be executed when the ngModel value changes due + * to user interaction with the input element. + * @param {expression=} ngChecked If the expression is truthy, then the `checked` attribute will be set on the + * element. **Note** : `ngChecked` should not be used alongside `ngModel`. + * Checkout {@link ng.directive:ngChecked ngChecked} for usage. + * + * @example + + + +
    + + Model as range: +
    + Model as number:
    + Min:
    + Max:
    + value = {{value}}
    + myForm.range.$valid = {{myForm.range.$valid}}
    + myForm.range.$error = {{myForm.range.$error}} +
    +
    +
    - return function(date, format) { - var text = '', - parts = [], - fn, match; + * ## Range Input with ngMin & ngMax attributes - format = format || 'mediumDate'; - format = $locale.DATETIME_FORMATS[format] || format; - if (isString(date)) { - if (NUMBER_STRING.test(date)) { - date = int(date); - } else { - date = jsonStringToDate(date); - } - } + * @example + + + +
    + Model as range: +
    + Model as number:
    + Min:
    + Max:
    + value = {{value}}
    + myForm.range.$valid = {{myForm.range.$valid}}
    + myForm.range.$error = {{myForm.range.$error}} +
    +
    +
    - if (isNumber(date)) { - date = new Date(date); - } + */ + 'range': rangeInputType, - if (!isDate(date)) { - return date; - } + /** + * @ngdoc input + * @name input[checkbox] + * + * @description + * HTML checkbox. + * + * @param {string} ngModel Assignable AngularJS expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {expression=} ngTrueValue The value to which the expression should be set when selected. + * @param {expression=} ngFalseValue The value to which the expression should be set when not selected. + * @param {string=} ngChange AngularJS expression to be executed when input changes due to user + * interaction with the input element. + * + * @example + + + +
    +
    +
    + value1 = {{checkboxModel.value1}}
    + value2 = {{checkboxModel.value2}}
    +
    +
    + + it('should change state', function() { + var value1 = element(by.binding('checkboxModel.value1')); + var value2 = element(by.binding('checkboxModel.value2')); - while(format) { - match = DATE_FORMATS_SPLIT.exec(format); - if (match) { - parts = concat(parts, match, 1); - format = parts.pop(); - } else { - parts.push(format); - format = null; - } - } + expect(value1.getText()).toContain('true'); + expect(value2.getText()).toContain('YES'); - forEach(parts, function(value){ - fn = DATE_FORMATS[value]; - text += fn ? fn(date, $locale.DATETIME_FORMATS) - : value.replace(/(^'|'$)/g, '').replace(/''/g, "'"); - }); + element(by.model('checkboxModel.value1')).click(); + element(by.model('checkboxModel.value2')).click(); - return text; - }; - } + expect(value1.getText()).toContain('false'); + expect(value2.getText()).toContain('NO'); + }); + +
    + */ + 'checkbox': checkboxInputType, + 'hidden': noop, + 'button': noop, + 'submit': noop, + 'reset': noop, + 'file': noop + }; - /** - * @ngdoc filter - * @name json - * @function - * - * @description - * Allows you to convert a JavaScript object into JSON string. - * - * This filter is mostly useful for debugging. When using the double curly {{value}} notation - * the binding is automatically converted to JSON. - * - * @param {*} object Any JavaScript object (including arrays and primitive types) to filter. - * @returns {string} JSON string. - * - * - * @example - - -
    {{ {'name':'value'} | json }}
    -
    - - it('should jsonify filtered objects', function() { - expect(element(by.binding("{'name':'value'}")).getText()).toMatch(/\{\n "name": ?"value"\n}/); - }); - -
    - * - */ - function jsonFilter() { - return function(object) { - return toJson(object, true); - }; + function stringBasedInputType(ctrl) { + ctrl.$formatters.push(function(value) { + return ctrl.$isEmpty(value) ? value : value.toString(); + }); } + function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + stringBasedInputType(ctrl); + } - /** - * @ngdoc filter - * @name lowercase - * @function - * @description - * Converts string to lowercase. - * @see angular.lowercase - */ - var lowercaseFilter = valueFn(lowercase); - - - /** - * @ngdoc filter - * @name uppercase - * @function - * @description - * Converts string to uppercase. - * @see angular.uppercase - */ - var uppercaseFilter = valueFn(uppercase); - - /** - * @ngdoc filter - * @name limitTo - * @function - * - * @description - * Creates a new array or string containing only a specified number of elements. The elements - * are taken from either the beginning or the end of the source array or string, as specified by - * the value and sign (positive or negative) of `limit`. - * - * @param {Array|string} input Source array or string to be limited. - * @param {string|number} limit The length of the returned array or string. If the `limit` number - * is positive, `limit` number of items from the beginning of the source array/string are copied. - * If the number is negative, `limit` number of items from the end of the source array/string - * are copied. The `limit` will be trimmed if it exceeds `array.length` - * @returns {Array|string} A new sub-array or substring of length `limit` or less if input array - * had less than `limit` elements. - * - * @example - - - -
    - Limit {{numbers}} to: -

    Output numbers: {{ numbers | limitTo:numLimit }}

    - Limit {{letters}} to: -

    Output letters: {{ letters | limitTo:letterLimit }}

    -
    -
    - - var numLimitInput = element(by.model('numLimit')); - var letterLimitInput = element(by.model('letterLimit')); - var limitedNumbers = element(by.binding('numbers | limitTo:numLimit')); - var limitedLetters = element(by.binding('letters | limitTo:letterLimit')); + function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { + var type = lowercase(element[0].type); - it('should limit the number array to first three items', function() { - expect(numLimitInput.getAttribute('value')).toBe('3'); - expect(letterLimitInput.getAttribute('value')).toBe('3'); - expect(limitedNumbers.getText()).toEqual('Output numbers: [1,2,3]'); - expect(limitedLetters.getText()).toEqual('Output letters: abc'); - }); + // In composition mode, users are still inputting intermediate text buffer, + // hold the listener until composition is done. + // More about composition events: https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent + if (!$sniffer.android) { + var composing = false; - it('should update the output when -3 is entered', function() { - numLimitInput.clear(); - numLimitInput.sendKeys('-3'); - letterLimitInput.clear(); - letterLimitInput.sendKeys('-3'); - expect(limitedNumbers.getText()).toEqual('Output numbers: [7,8,9]'); - expect(limitedLetters.getText()).toEqual('Output letters: ghi'); - }); + element.on('compositionstart', function() { + composing = true; + }); - it('should not exceed the maximum size of input array', function() { - numLimitInput.clear(); - numLimitInput.sendKeys('100'); - letterLimitInput.clear(); - letterLimitInput.sendKeys('100'); - expect(limitedNumbers.getText()).toEqual('Output numbers: [1,2,3,4,5,6,7,8,9]'); - expect(limitedLetters.getText()).toEqual('Output letters: abcdefghi'); - }); - -
    - */ - function limitToFilter(){ - return function(input, limit) { - if (!isArray(input) && !isString(input)) return input; + element.on('compositionend', function() { + composing = false; + listener(); + }); + } - limit = int(limit); + var timeout; - if (isString(input)) { - //NaN check on limit - if (limit) { - return limit >= 0 ? input.slice(0, limit) : input.slice(limit, input.length); - } else { - return ""; - } + var listener = function(ev) { + if (timeout) { + $browser.defer.cancel(timeout); + timeout = null; } + if (composing) return; + var value = element.val(), + event = ev && ev.type; - var out = [], - i, n; - - // if abs(limit) exceeds maximum length, trim it - if (limit > input.length) - limit = input.length; - else if (limit < -input.length) - limit = -input.length; - - if (limit > 0) { - i = 0; - n = limit; - } else { - i = input.length + limit; - n = input.length; + // By default we will trim the value + // If the attribute ng-trim exists we will avoid trimming + // If input type is 'password', the value is never trimmed + if (type !== 'password' && (!attr.ngTrim || attr.ngTrim !== 'false')) { + value = trim(value); } - for (; i} expression A predicate to be - * used by the comparator to determine the order of elements. - * - * Can be one of: - * - * - `function`: Getter function. The result of this function will be sorted using the - * `<`, `=`, `>` operator. - * - `string`: An Angular expression which evaluates to an object to order by, such as 'name' - * to sort by a property called 'name'. Optionally prefixed with `+` or `-` to control - * ascending or descending sort order (for example, +name or -name). - * - `Array`: An array of function or string predicates. The first predicate in the array - * is used for sorting, but when two items are equivalent, the next predicate is used. - * - * @param {boolean=} reverse Reverse the order of the array. - * @returns {Array} Sorted copy of the source array. - * - * @example - - - -
    -
    Sorting predicate = {{predicate}}; reverse = {{reverse}}
    -
    - [ unsorted ] - - - - - - - - - - - -
    Name - (^)Phone NumberAge
    {{friend.name}}{{friend.phone}}{{friend.age}}
    -
    -
    -
    - */ - orderByFilter.$inject = ['$parse']; - function orderByFilter($parse){ - return function(array, sortPredicate, reverseOrder) { - if (!isArray(array)) return array; - if (!sortPredicate) return array; - sortPredicate = isArray(sortPredicate) ? sortPredicate: [sortPredicate]; - sortPredicate = map(sortPredicate, function(predicate){ - var descending = false, get = predicate || identity; - if (isString(predicate)) { - if ((predicate.charAt(0) == '+' || predicate.charAt(0) == '-')) { - descending = predicate.charAt(0) == '-'; - predicate = predicate.substring(1); - } - get = $parse(predicate); - if (get.constant) { - var key = get(); - return reverseComparator(function(a,b) { - return compare(a[key], b[key]); - }, descending); - } - } - return reverseComparator(function(a,b){ - return compare(get(a),get(b)); - }, descending); - }); - var arrayCopy = []; - for ( var i = 0; i < array.length; i++) { arrayCopy.push(array[i]); } - return arrayCopy.sort(reverseComparator(comparator, reverseOrder)); - - function comparator(o1, o2){ - for ( var i = 0; i < sortPredicate.length; i++) { - var comp = sortPredicate[i](o1, o2); - if (comp !== 0) return comp; - } - return 0; - } - function reverseComparator(comp, descending) { - return toBoolean(descending) - ? function(a,b){return comp(b,a);} - : comp; - } - function compare(v1, v2){ - var t1 = typeof v1; - var t2 = typeof v2; - if (t1 == t2) { - if (t1 == "string") { - v1 = v1.toLowerCase(); - v2 = v2.toLowerCase(); - } - if (v1 === v2) return 0; - return v1 < v2 ? -1 : 1; - } else { - return t1 < t2 ? -1 : 1; + // if the browser does support "input" event, we are fine - except on IE9 which doesn't fire the + // input event on backspace, delete or cut + if ($sniffer.hasEvent('input')) { + element.on('input', listener); + } else { + var deferListener = function(ev, input, origValue) { + if (!timeout) { + timeout = $browser.defer(function() { + timeout = null; + if (!input || input.value !== origValue) { + listener(ev); + } + }); } - } - }; - } - - function ngDirective(directive) { - if (isFunction(directive)) { - directive = { - link: directive }; - } - directive.restrict = directive.restrict || 'AC'; - return valueFn(directive); - } - /** - * @ngdoc directive - * @name a - * @restrict E - * - * @description - * Modifies the default behavior of the html A tag so that the default action is prevented when - * the href attribute is empty. - * - * This change permits the easy creation of action links with the `ngClick` directive - * without changing the location or causing page reloads, e.g.: - * `Add Item` - */ - var htmlAnchorDirective = valueFn({ - restrict: 'E', - compile: function(element, attr) { + element.on('keydown', /** @this */ function(event) { + var key = event.keyCode; - if (msie <= 8) { + // ignore + // command modifiers arrows + if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return; - // turn link into a stylable link in IE - // but only if it doesn't have name attribute, in which case it's an anchor - if (!attr.href && !attr.name) { - attr.$set('href', ''); - } + deferListener(event, this, this.value); + }); - // add a comment node to anchors to workaround IE bug that causes element content to be reset - // to new attribute content if attribute is updated with value containing @ and element also - // contains value with @ - // see issue #1949 - element.append(document.createComment('IE fix')); + // if user modifies input value using context menu in IE, we need "paste", "cut" and "drop" events to catch it + if ($sniffer.hasEvent('paste')) { + element.on('paste cut drop', deferListener); } + } - if (!attr.href && !attr.xlinkHref && !attr.name) { - return function(scope, element) { - // SVGAElement does not use the href attribute, but rather the 'xlinkHref' attribute. - var href = toString.call(element.prop('href')) === '[object SVGAnimatedString]' ? - 'xlink:href' : 'href'; - element.on('click', function(event){ - // if we have no href url, then don't navigate anywhere. - if (!element.attr(href)) { - event.preventDefault(); + // if user paste into input using mouse on older browser + // or form autocomplete on newer browser, we need "change" event to catch it + element.on('change', listener); + + // Some native input types (date-family) have the ability to change validity without + // firing any input/change events. + // For these event types, when native validators are present and the browser supports the type, + // check for validity changes on various DOM events. + if (PARTIAL_VALIDATION_TYPES[type] && ctrl.$$hasNativeValidators && type === attr.type) { + element.on(PARTIAL_VALIDATION_EVENTS, /** @this */ function(ev) { + if (!timeout) { + var validity = this[VALIDITY_STATE_PROPERTY]; + var origBadInput = validity.badInput; + var origTypeMismatch = validity.typeMismatch; + timeout = $browser.defer(function() { + timeout = null; + if (validity.badInput !== origBadInput || validity.typeMismatch !== origTypeMismatch) { + listener(ev); } }); - }; - } + } + }); } - }); - /** - * @ngdoc directive - * @name ngHref - * @restrict A - * @priority 99 - * - * @description - * Using Angular markup like `{{hash}}` in an href attribute will - * make the link go to the wrong URL if the user clicks it before - * Angular has a chance to replace the `{{hash}}` markup with its - * value. Until Angular replaces the markup the link will be broken - * and will most likely return a 404 error. - * - * The `ngHref` directive solves this problem. - * - * The wrong way to write it: - * ```html - * - * ``` - * - * The correct way to write it: - * ```html - * - * ``` - * - * @element A - * @param {template} ngHref any string which can contain `{{}}` markup. - * - * @example - * This example shows various combinations of `href`, `ng-href` and `ng-click` attributes - * in links and their different behaviors: - - -
    -
    link 1 (link, don't reload)
    - link 2 (link, don't reload)
    - link 3 (link, reload!)
    - anchor (link, don't reload)
    - anchor (no link)
    - link (link, change location) - - - it('should execute ng-click but not reload when href without value', function() { - element(by.id('link-1')).click(); - expect(element(by.model('value')).getAttribute('value')).toEqual('1'); - expect(element(by.id('link-1')).getAttribute('href')).toBe(''); - }); + ctrl.$render = function() { + // Workaround for Firefox validation #12102. + var value = ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue; + if (element.val() !== value) { + element.val(value); + } + }; + } - it('should execute ng-click but not reload when href empty string', function() { - element(by.id('link-2')).click(); - expect(element(by.model('value')).getAttribute('value')).toEqual('2'); - expect(element(by.id('link-2')).getAttribute('href')).toBe(''); - }); + function weekParser(isoWeek, existingDate) { + if (isDate(isoWeek)) { + return isoWeek; + } - it('should execute ng-click and change url when ng-href specified', function() { - expect(element(by.id('link-3')).getAttribute('href')).toMatch(/\/123$/); + if (isString(isoWeek)) { + WEEK_REGEXP.lastIndex = 0; + var parts = WEEK_REGEXP.exec(isoWeek); + if (parts) { + var year = +parts[1], + week = +parts[2], + hours = 0, + minutes = 0, + seconds = 0, + milliseconds = 0, + firstThurs = getFirstThursdayOfYear(year), + addDays = (week - 1) * 7; + + if (existingDate) { + hours = existingDate.getHours(); + minutes = existingDate.getMinutes(); + seconds = existingDate.getSeconds(); + milliseconds = existingDate.getMilliseconds(); + } - element(by.id('link-3')).click(); + return new Date(year, 0, firstThurs.getDate() + addDays, hours, minutes, seconds, milliseconds); + } + } - // At this point, we navigate away from an Angular page, so we need - // to use browser.driver to get the base webdriver. + return NaN; + } - browser.wait(function() { - return browser.driver.getCurrentUrl().then(function(url) { - return url.match(/\/123$/); - }); - }, 1000, 'page should navigate to /123'); - }); + function createDateParser(regexp, mapping) { + return function(iso, date) { + var parts, map; - xit('should execute ng-click but not reload when href empty string and name specified', function() { - element(by.id('link-4')).click(); - expect(element(by.model('value')).getAttribute('value')).toEqual('4'); - expect(element(by.id('link-4')).getAttribute('href')).toBe(''); - }); + if (isDate(iso)) { + return iso; + } - it('should execute ng-click but not reload when no href but name specified', function() { - element(by.id('link-5')).click(); - expect(element(by.model('value')).getAttribute('value')).toEqual('5'); - expect(element(by.id('link-5')).getAttribute('href')).toBe(null); - }); + if (isString(iso)) { + // When a date is JSON'ified to wraps itself inside of an extra + // set of double quotes. This makes the date parsing code unable + // to match the date string and parse it as a date. + if (iso.charAt(0) === '"' && iso.charAt(iso.length - 1) === '"') { + iso = iso.substring(1, iso.length - 1); + } + if (ISO_DATE_REGEXP.test(iso)) { + return new Date(iso); + } + regexp.lastIndex = 0; + parts = regexp.exec(iso); + + if (parts) { + parts.shift(); + if (date) { + map = { + yyyy: date.getFullYear(), + MM: date.getMonth() + 1, + dd: date.getDate(), + HH: date.getHours(), + mm: date.getMinutes(), + ss: date.getSeconds(), + sss: date.getMilliseconds() / 1000 + }; + } else { + map = { yyyy: 1970, MM: 1, dd: 1, HH: 0, mm: 0, ss: 0, sss: 0 }; + } - it('should only change url when only ng-href', function() { - element(by.model('value')).clear(); - element(by.model('value')).sendKeys('6'); - expect(element(by.id('link-6')).getAttribute('href')).toMatch(/\/6$/); + forEach(parts, function(part, index) { + if (index < mapping.length) { + map[mapping[index]] = +part; + } + }); + return new Date(map.yyyy, map.MM - 1, map.dd, map.HH, map.mm, map.ss || 0, map.sss * 1000 || 0); + } + } - element(by.id('link-6')).click(); + return NaN; + }; + } - // At this point, we navigate away from an Angular page, so we need - // to use browser.driver to get the base webdriver. - browser.wait(function() { - return browser.driver.getCurrentUrl().then(function(url) { - return url.match(/\/6$/); + function createDateInputType(type, regexp, parseDate, format) { + return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) { + badInputChecker(scope, element, attr, ctrl); + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + var timezone = ctrl && ctrl.$options.getOption('timezone'); + var previousDate; + + ctrl.$$parserName = type; + ctrl.$parsers.push(function(value) { + if (ctrl.$isEmpty(value)) return null; + if (regexp.test(value)) { + // Note: We cannot read ctrl.$modelValue, as there might be a different + // parser/formatter in the processing chain so that the model + // contains some different data format! + var parsedDate = parseDate(value, previousDate); + if (timezone) { + parsedDate = convertTimezoneToLocal(parsedDate, timezone); + } + return parsedDate; + } + return undefined; }); - }, 1000, 'page should navigate to /6'); - }); - -
    - */ - - /** - * @ngdoc directive - * @name ngSrc - * @restrict A - * @priority 99 - * - * @description - * Using Angular markup like `{{hash}}` in a `src` attribute doesn't - * work right: The browser will fetch from the URL with the literal - * text `{{hash}}` until Angular replaces the expression inside - * `{{hash}}`. The `ngSrc` directive solves this problem. - * - * The buggy way to write it: - * ```html - * - * ``` - * - * The correct way to write it: - * ```html - * - * ``` - * - * @element IMG - * @param {template} ngSrc any string which can contain `{{}}` markup. - */ - - /** - * @ngdoc directive - * @name ngSrcset - * @restrict A - * @priority 99 - * - * @description - * Using Angular markup like `{{hash}}` in a `srcset` attribute doesn't - * work right: The browser will fetch from the URL with the literal - * text `{{hash}}` until Angular replaces the expression inside - * `{{hash}}`. The `ngSrcset` directive solves this problem. - * - * The buggy way to write it: - * ```html - * - * ``` - * - * The correct way to write it: - * ```html - * - * ``` - * - * @element IMG - * @param {template} ngSrcset any string which can contain `{{}}` markup. - */ - /** - * @ngdoc directive - * @name ngDisabled - * @restrict A - * @priority 100 - * - * @description - * - * The following markup will make the button enabled on Chrome/Firefox but not on IE8 and older IEs: - * ```html - *
    - * - *
    - * ``` - * - * The HTML specification does not require browsers to preserve the values of boolean attributes - * such as disabled. (Their presence means true and their absence means false.) - * If we put an Angular interpolation expression into such an attribute then the - * binding information would be lost when the browser removes the attribute. - * The `ngDisabled` directive solves this problem for the `disabled` attribute. - * This complementary directive is not removed by the browser and so provides - * a permanent reliable place to store the binding information. - * - * @example - - - Click me to toggle:
    - -
    - - it('should toggle button', function() { - expect(element(by.css('button')).getAttribute('disabled')).toBeFalsy(); - element(by.model('checked')).click(); - expect(element(by.css('button')).getAttribute('disabled')).toBeTruthy(); - }); - -
    - * - * @element INPUT - * @param {expression} ngDisabled If the {@link guide/expression expression} is truthy, - * then special attribute "disabled" will be set on the element - */ + ctrl.$formatters.push(function(value) { + if (value && !isDate(value)) { + throw ngModelMinErr('datefmt', 'Expected `{0}` to be a date', value); + } + if (isValidDate(value)) { + previousDate = value; + if (previousDate && timezone) { + previousDate = convertTimezoneToLocal(previousDate, timezone, true); + } + return $filter('date')(value, format, timezone); + } else { + previousDate = null; + return ''; + } + }); + if (isDefined(attr.min) || attr.ngMin) { + var minVal; + ctrl.$validators.min = function(value) { + return !isValidDate(value) || isUndefined(minVal) || parseDate(value) >= minVal; + }; + attr.$observe('min', function(val) { + minVal = parseObservedDateValue(val); + ctrl.$validate(); + }); + } - /** - * @ngdoc directive - * @name ngChecked - * @restrict A - * @priority 100 - * - * @description - * The HTML specification does not require browsers to preserve the values of boolean attributes - * such as checked. (Their presence means true and their absence means false.) - * If we put an Angular interpolation expression into such an attribute then the - * binding information would be lost when the browser removes the attribute. - * The `ngChecked` directive solves this problem for the `checked` attribute. - * This complementary directive is not removed by the browser and so provides - * a permanent reliable place to store the binding information. - * @example - - - Check me to check both:
    - -
    - - it('should check both checkBoxes', function() { - expect(element(by.id('checkSlave')).getAttribute('checked')).toBeFalsy(); - element(by.model('master')).click(); - expect(element(by.id('checkSlave')).getAttribute('checked')).toBeTruthy(); - }); - -
    - * - * @element INPUT - * @param {expression} ngChecked If the {@link guide/expression expression} is truthy, - * then special attribute "checked" will be set on the element - */ + if (isDefined(attr.max) || attr.ngMax) { + var maxVal; + ctrl.$validators.max = function(value) { + return !isValidDate(value) || isUndefined(maxVal) || parseDate(value) <= maxVal; + }; + attr.$observe('max', function(val) { + maxVal = parseObservedDateValue(val); + ctrl.$validate(); + }); + } + function isValidDate(value) { + // Invalid Date: getTime() returns NaN + return value && !(value.getTime && value.getTime() !== value.getTime()); + } - /** - * @ngdoc directive - * @name ngReadonly - * @restrict A - * @priority 100 - * - * @description - * The HTML specification does not require browsers to preserve the values of boolean attributes - * such as readonly. (Their presence means true and their absence means false.) - * If we put an Angular interpolation expression into such an attribute then the - * binding information would be lost when the browser removes the attribute. - * The `ngReadonly` directive solves this problem for the `readonly` attribute. - * This complementary directive is not removed by the browser and so provides - * a permanent reliable place to store the binding information. - * @example - - - Check me to make text readonly:
    - -
    - - it('should toggle readonly attr', function() { - expect(element(by.css('[type="text"]')).getAttribute('readonly')).toBeFalsy(); - element(by.model('checked')).click(); - expect(element(by.css('[type="text"]')).getAttribute('readonly')).toBeTruthy(); - }); - -
    - * - * @element INPUT - * @param {expression} ngReadonly If the {@link guide/expression expression} is truthy, - * then special attribute "readonly" will be set on the element - */ + function parseObservedDateValue(val) { + return isDefined(val) && !isDate(val) ? parseDate(val) || undefined : val; + } + }; + } + function badInputChecker(scope, element, attr, ctrl) { + var node = element[0]; + var nativeValidation = ctrl.$$hasNativeValidators = isObject(node.validity); + if (nativeValidation) { + ctrl.$parsers.push(function(value) { + var validity = element.prop(VALIDITY_STATE_PROPERTY) || {}; + return validity.badInput || validity.typeMismatch ? undefined : value; + }); + } + } - /** - * @ngdoc directive - * @name ngSelected - * @restrict A - * @priority 100 - * - * @description - * The HTML specification does not require browsers to preserve the values of boolean attributes - * such as selected. (Their presence means true and their absence means false.) - * If we put an Angular interpolation expression into such an attribute then the - * binding information would be lost when the browser removes the attribute. - * The `ngSelected` directive solves this problem for the `selected` attribute. - * This complementary directive is not removed by the browser and so provides - * a permanent reliable place to store the binding information. - * - * @example - - - Check me to select:
    - -
    - - it('should select Greetings!', function() { - expect(element(by.id('greet')).getAttribute('selected')).toBeFalsy(); - element(by.model('selected')).click(); - expect(element(by.id('greet')).getAttribute('selected')).toBeTruthy(); + function numberFormatterParser(ctrl) { + ctrl.$$parserName = 'number'; + ctrl.$parsers.push(function(value) { + if (ctrl.$isEmpty(value)) return null; + if (NUMBER_REGEXP.test(value)) return parseFloat(value); + return undefined; }); - -
    - * - * @element OPTION - * @param {expression} ngSelected If the {@link guide/expression expression} is truthy, - * then special attribute "selected" will be set on the element - */ - /** - * @ngdoc directive - * @name ngOpen - * @restrict A - * @priority 100 - * - * @description - * The HTML specification does not require browsers to preserve the values of boolean attributes - * such as open. (Their presence means true and their absence means false.) - * If we put an Angular interpolation expression into such an attribute then the - * binding information would be lost when the browser removes the attribute. - * The `ngOpen` directive solves this problem for the `open` attribute. - * This complementary directive is not removed by the browser and so provides - * a permanent reliable place to store the binding information. - * @example - - - Check me check multiple:
    -
    - Show/Hide me -
    -
    - - it('should toggle open', function() { - expect(element(by.id('details')).getAttribute('open')).toBeFalsy(); - element(by.model('open')).click(); - expect(element(by.id('details')).getAttribute('open')).toBeTruthy(); - }); - -
    - * - * @element DETAILS - * @param {expression} ngOpen If the {@link guide/expression expression} is truthy, - * then special attribute "open" will be set on the element - */ + ctrl.$formatters.push(function(value) { + if (!ctrl.$isEmpty(value)) { + if (!isNumber(value)) { + throw ngModelMinErr('numfmt', 'Expected `{0}` to be a number', value); + } + value = value.toString(); + } + return value; + }); + } - var ngAttributeAliasDirectives = {}; + function parseNumberAttrVal(val) { + if (isDefined(val) && !isNumber(val)) { + val = parseFloat(val); + } + return !isNumberNaN(val) ? val : undefined; + } + function isNumberInteger(num) { + // See http://stackoverflow.com/questions/14636536/how-to-check-if-a-variable-is-an-integer-in-javascript#14794066 + // (minus the assumption that `num` is a number) -// boolean attrs are evaluated - forEach(BOOLEAN_ATTR, function(propName, attrName) { - // binding to multiple is not supported - if (propName == "multiple") return; + // eslint-disable-next-line no-bitwise + return (num | 0) === num; + } - var normalized = directiveNormalize('ng-' + attrName); - ngAttributeAliasDirectives[normalized] = function() { - return { - priority: 100, - link: function(scope, element, attr) { - scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) { - attr.$set(attrName, !!value); - }); + function countDecimals(num) { + var numString = num.toString(); + var decimalSymbolIndex = numString.indexOf('.'); + + if (decimalSymbolIndex === -1) { + if (-1 < num && num < 1) { + // It may be in the exponential notation format (`1e-X`) + var match = /e-(\d+)$/.exec(numString); + + if (match) { + return Number(match[1]); } - }; - }; - }); + } + return 0; + } -// ng-src, ng-srcset, ng-href are interpolated - forEach(['src', 'srcset', 'href'], function(attrName) { - var normalized = directiveNormalize('ng-' + attrName); - ngAttributeAliasDirectives[normalized] = function() { - return { - priority: 99, // it needs to run after the attributes are interpolated - link: function(scope, element, attr) { - var propName = attrName, - name = attrName; + return numString.length - decimalSymbolIndex - 1; + } - if (attrName === 'href' && - toString.call(element.prop('href')) === '[object SVGAnimatedString]') { - name = 'xlinkHref'; - attr.$attr[name] = 'xlink:href'; - propName = null; - } + function isValidForStep(viewValue, stepBase, step) { + // At this point `stepBase` and `step` are expected to be non-NaN values + // and `viewValue` is expected to be a valid stringified number. + var value = Number(viewValue); - attr.$observe(normalized, function(value) { - if (!value) - return; + var isNonIntegerValue = !isNumberInteger(value); + var isNonIntegerStepBase = !isNumberInteger(stepBase); + var isNonIntegerStep = !isNumberInteger(step); - attr.$set(name, value); + // Due to limitations in Floating Point Arithmetic (e.g. `0.3 - 0.2 !== 0.1` or + // `0.5 % 0.1 !== 0`), we need to convert all numbers to integers. + if (isNonIntegerValue || isNonIntegerStepBase || isNonIntegerStep) { + var valueDecimals = isNonIntegerValue ? countDecimals(value) : 0; + var stepBaseDecimals = isNonIntegerStepBase ? countDecimals(stepBase) : 0; + var stepDecimals = isNonIntegerStep ? countDecimals(step) : 0; - // on IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist - // then calling element.setAttribute('src', 'foo') doesn't do anything, so we need - // to set the property as well to achieve the desired effect. - // we use attr[attrName] value since $set can sanitize the url. - if (msie && propName) element.prop(propName, attr[name]); - }); - } + var decimalCount = Math.max(valueDecimals, stepBaseDecimals, stepDecimals); + var multiplier = Math.pow(10, decimalCount); + + value = value * multiplier; + stepBase = stepBase * multiplier; + step = step * multiplier; + + if (isNonIntegerValue) value = Math.round(value); + if (isNonIntegerStepBase) stepBase = Math.round(stepBase); + if (isNonIntegerStep) step = Math.round(step); + } + + return (value - stepBase) % step === 0; + } + + function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { + badInputChecker(scope, element, attr, ctrl); + numberFormatterParser(ctrl); + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + + var minVal; + var maxVal; + + if (isDefined(attr.min) || attr.ngMin) { + ctrl.$validators.min = function(value) { + return ctrl.$isEmpty(value) || isUndefined(minVal) || value >= minVal; }; - }; - }); - /* global -nullFormCtrl */ - var nullFormCtrl = { - $addControl: noop, - $removeControl: noop, - $setValidity: noop, - $setDirty: noop, - $setPristine: noop - }; + attr.$observe('min', function(val) { + minVal = parseNumberAttrVal(val); + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + }); + } - /** - * @ngdoc type - * @name form.FormController - * - * @property {boolean} $pristine True if user has not interacted with the form yet. - * @property {boolean} $dirty True if user has already interacted with the form. - * @property {boolean} $valid True if all of the containing forms and controls are valid. - * @property {boolean} $invalid True if at least one containing control or form is invalid. - * - * @property {Object} $error Is an object hash, containing references to all invalid controls or - * forms, where: - * - * - keys are validation tokens (error names), - * - values are arrays of controls or forms that are invalid for given error name. - * - * - * Built-in validation tokens: - * - * - `email` - * - `max` - * - `maxlength` - * - `min` - * - `minlength` - * - `number` - * - `pattern` - * - `required` - * - `url` - * - * @description - * `FormController` keeps track of all its controls and nested forms as well as state of them, - * such as being valid/invalid or dirty/pristine. - * - * Each {@link ng.directive:form form} directive creates an instance - * of `FormController`. - * - */ -//asks for $scope to fool the BC controller module - FormController.$inject = ['$element', '$attrs', '$scope', '$animate']; - function FormController(element, attrs, $scope, $animate) { - var form = this, - parentForm = element.parent().controller('form') || nullFormCtrl, - invalidCount = 0, // used to easily determine if we are valid - errors = form.$error = {}, - controls = []; + if (isDefined(attr.max) || attr.ngMax) { + ctrl.$validators.max = function(value) { + return ctrl.$isEmpty(value) || isUndefined(maxVal) || value <= maxVal; + }; - // init state - form.$name = attrs.name || attrs.ngForm; - form.$dirty = false; - form.$pristine = true; - form.$valid = true; - form.$invalid = false; + attr.$observe('max', function(val) { + maxVal = parseNumberAttrVal(val); + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + }); + } + + if (isDefined(attr.step) || attr.ngStep) { + var stepVal; + ctrl.$validators.step = function(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) || + isValidForStep(viewValue, minVal || 0, stepVal); + }; - parentForm.$addControl(form); + attr.$observe('step', function(val) { + stepVal = parseNumberAttrVal(val); + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + }); + } + } - // Setup initial state of the control - element.addClass(PRISTINE_CLASS); - toggleValidCss(true); + function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) { + badInputChecker(scope, element, attr, ctrl); + numberFormatterParser(ctrl); + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + + var supportsRange = ctrl.$$hasNativeValidators && element[0].type === 'range', + minVal = supportsRange ? 0 : undefined, + maxVal = supportsRange ? 100 : undefined, + stepVal = supportsRange ? 1 : undefined, + validity = element[0].validity, + hasMinAttr = isDefined(attr.min), + hasMaxAttr = isDefined(attr.max), + hasStepAttr = isDefined(attr.step); + + var originalRender = ctrl.$render; + + ctrl.$render = supportsRange && isDefined(validity.rangeUnderflow) && isDefined(validity.rangeOverflow) ? + //Browsers that implement range will set these values automatically, but reading the adjusted values after + //$render would cause the min / max validators to be applied with the wrong value + function rangeRender() { + originalRender(); + ctrl.$setViewValue(element.val()); + } : + originalRender; + + if (hasMinAttr) { + ctrl.$validators.min = supportsRange ? + // Since all browsers set the input to a valid value, we don't need to check validity + function noopMinValidator() { return true; } : + // non-support browsers validate the min val + function minValidator(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || isUndefined(minVal) || viewValue >= minVal; + }; - // convenience method for easy toggling of classes - function toggleValidCss(isValid, validationErrorKey) { - validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; - $animate.removeClass(element, (isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey); - $animate.addClass(element, (isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey); + setInitialValueAndObserver('min', minChange); } - /** - * @ngdoc method - * @name form.FormController#$addControl - * - * @description - * Register a control with the form. - * - * Input elements using ngModelController do this automatically when they are linked. - */ - form.$addControl = function(control) { - // Breaking change - before, inputs whose name was "hasOwnProperty" were quietly ignored - // and not added to the scope. Now we throw an error. - assertNotHasOwnProperty(control.$name, 'input'); - controls.push(control); + if (hasMaxAttr) { + ctrl.$validators.max = supportsRange ? + // Since all browsers set the input to a valid value, we don't need to check validity + function noopMaxValidator() { return true; } : + // non-support browsers validate the max val + function maxValidator(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || isUndefined(maxVal) || viewValue <= maxVal; + }; - if (control.$name) { - form[control.$name] = control; + setInitialValueAndObserver('max', maxChange); + } + + if (hasStepAttr) { + ctrl.$validators.step = supportsRange ? + function nativeStepValidator() { + // Currently, only FF implements the spec on step change correctly (i.e. adjusting the + // input element value to a valid value). It's possible that other browsers set the stepMismatch + // validity error instead, so we can at least report an error in that case. + return !validity.stepMismatch; + } : + // ngStep doesn't set the setp attr, so the browser doesn't adjust the input value as setting step would + function stepValidator(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) || + isValidForStep(viewValue, minVal || 0, stepVal); + }; + + setInitialValueAndObserver('step', stepChange); + } + + function setInitialValueAndObserver(htmlAttrName, changeFn) { + // interpolated attributes set the attribute value only after a digest, but we need the + // attribute value when the input is first rendered, so that the browser can adjust the + // input value based on the min/max value + element.attr(htmlAttrName, attr[htmlAttrName]); + attr.$observe(htmlAttrName, changeFn); + } + + function minChange(val) { + minVal = parseNumberAttrVal(val); + // ignore changes before model is initialized + if (isNumberNaN(ctrl.$modelValue)) { + return; } - }; - /** - * @ngdoc method - * @name form.FormController#$removeControl - * - * @description - * Deregister a control from the form. - * - * Input elements using ngModelController do this automatically when they are destroyed. - */ - form.$removeControl = function(control) { - if (control.$name && form[control.$name] === control) { - delete form[control.$name]; + if (supportsRange) { + var elVal = element.val(); + // IE11 doesn't set the el val correctly if the minVal is greater than the element value + if (minVal > elVal) { + elVal = minVal; + element.val(elVal); + } + ctrl.$setViewValue(elVal); + } else { + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); } - forEach(errors, function(queue, validationToken) { - form.$setValidity(validationToken, true, control); - }); + } - arrayRemove(controls, control); - }; + function maxChange(val) { + maxVal = parseNumberAttrVal(val); + // ignore changes before model is initialized + if (isNumberNaN(ctrl.$modelValue)) { + return; + } - /** - * @ngdoc method - * @name form.FormController#$setValidity - * - * @description - * Sets the validity of a form control. - * - * This method will also propagate to parent forms. - */ - form.$setValidity = function(validationToken, isValid, control) { - var queue = errors[validationToken]; - - if (isValid) { - if (queue) { - arrayRemove(queue, control); - if (!queue.length) { - invalidCount--; - if (!invalidCount) { - toggleValidCss(isValid); - form.$valid = true; - form.$invalid = false; - } - errors[validationToken] = false; - toggleValidCss(true, validationToken); - parentForm.$setValidity(validationToken, true, form); - } + if (supportsRange) { + var elVal = element.val(); + // IE11 doesn't set the el val correctly if the maxVal is less than the element value + if (maxVal < elVal) { + element.val(maxVal); + // IE11 and Chrome don't set the value to the minVal when max < min + elVal = maxVal < minVal ? minVal : maxVal; } + ctrl.$setViewValue(elVal); + } else { + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + } + } + + function stepChange(val) { + stepVal = parseNumberAttrVal(val); + // ignore changes before model is initialized + if (isNumberNaN(ctrl.$modelValue)) { + return; + } + // Some browsers don't adjust the input value correctly, but set the stepMismatch error + if (supportsRange && ctrl.$viewValue !== element.val()) { + ctrl.$setViewValue(element.val()); } else { - if (!invalidCount) { - toggleValidCss(isValid); - } - if (queue) { - if (includes(queue, control)) return; - } else { - errors[validationToken] = queue = []; - invalidCount++; - toggleValidCss(false, validationToken); - parentForm.$setValidity(validationToken, false, form); + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + } + } + } + + function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) { + // Note: no badInputChecker here by purpose as `url` is only a validation + // in browsers, i.e. we can always read out input.value even if it is not valid! + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + stringBasedInputType(ctrl); + + ctrl.$$parserName = 'url'; + ctrl.$validators.url = function(modelValue, viewValue) { + var value = modelValue || viewValue; + return ctrl.$isEmpty(value) || URL_REGEXP.test(value); + }; + } + + function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) { + // Note: no badInputChecker here by purpose as `url` is only a validation + // in browsers, i.e. we can always read out input.value even if it is not valid! + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + stringBasedInputType(ctrl); + + ctrl.$$parserName = 'email'; + ctrl.$validators.email = function(modelValue, viewValue) { + var value = modelValue || viewValue; + return ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value); + }; + } + + function radioInputType(scope, element, attr, ctrl) { + var doTrim = !attr.ngTrim || trim(attr.ngTrim) !== 'false'; + // make the name unique, if not defined + if (isUndefined(attr.name)) { + element.attr('name', nextUid()); + } + + var listener = function(ev) { + var value; + if (element[0].checked) { + value = attr.value; + if (doTrim) { + value = trim(value); } - queue.push(control); + ctrl.$setViewValue(value, ev && ev.type); + } + }; + + element.on('click', listener); + + ctrl.$render = function() { + var value = attr.value; + if (doTrim) { + value = trim(value); + } + element[0].checked = (value === ctrl.$viewValue); + }; - form.$valid = false; - form.$invalid = true; + attr.$observe('value', ctrl.$render); + } + + function parseConstantExpr($parse, context, name, expression, fallback) { + var parseFn; + if (isDefined(expression)) { + parseFn = $parse(expression); + if (!parseFn.constant) { + throw ngModelMinErr('constexpr', 'Expected constant expression for `{0}`, but saw ' + + '`{1}`.', name, expression); } + return parseFn(context); + } + return fallback; + } + + function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) { + var trueValue = parseConstantExpr($parse, scope, 'ngTrueValue', attr.ngTrueValue, true); + var falseValue = parseConstantExpr($parse, scope, 'ngFalseValue', attr.ngFalseValue, false); + + var listener = function(ev) { + ctrl.$setViewValue(element[0].checked, ev && ev.type); }; - /** - * @ngdoc method - * @name form.FormController#$setDirty - * - * @description - * Sets the form to a dirty state. - * - * This method can be called to add the 'ng-dirty' class and set the form to a dirty - * state (ng-dirty class). This method will also propagate to parent forms. - */ - form.$setDirty = function() { - $animate.removeClass(element, PRISTINE_CLASS); - $animate.addClass(element, DIRTY_CLASS); - form.$dirty = true; - form.$pristine = false; - parentForm.$setDirty(); + element.on('click', listener); + + ctrl.$render = function() { + element[0].checked = ctrl.$viewValue; }; - /** - * @ngdoc method - * @name form.FormController#$setPristine - * - * @description - * Sets the form to its pristine state. - * - * This method can be called to remove the 'ng-dirty' class and set the form to its pristine - * state (ng-pristine class). This method will also propagate to all the controls contained - * in this form. - * - * Setting a form back to a pristine state is often useful when we want to 'reuse' a form after - * saving or resetting it. - */ - form.$setPristine = function () { - $animate.removeClass(element, DIRTY_CLASS); - $animate.addClass(element, PRISTINE_CLASS); - form.$dirty = false; - form.$pristine = true; - forEach(controls, function(control) { - control.$setPristine(); - }); + // Override the standard `$isEmpty` because the $viewValue of an empty checkbox is always set to `false` + // This is because of the parser below, which compares the `$modelValue` with `trueValue` to convert + // it to a boolean. + ctrl.$isEmpty = function(value) { + return value === false; }; + + ctrl.$formatters.push(function(value) { + return equals(value, trueValue); + }); + + ctrl.$parsers.push(function(value) { + return value ? trueValue : falseValue; + }); } /** * @ngdoc directive - * @name ngForm - * @restrict EAC + * @name textarea + * @restrict E * * @description - * Nestable alias of {@link ng.directive:form `form`} directive. HTML - * does not allow nesting of form elements. It is useful to nest forms, for example if the validity of a - * sub-group of controls needs to be determined. + * HTML textarea element control with AngularJS data-binding. The data-binding and validation + * properties of this element are exactly the same as those of the + * {@link ng.directive:input input element}. * - * Note: the purpose of `ngForm` is to group controls, - * but not to be a replacement for the `
    ` tag with all of its capabilities - * (e.g. posting to the server, ...). + * @param {string} ngModel Assignable AngularJS expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} required Sets `required` validation error key if the value is not entered. + * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to + * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of + * `required` when you want to data-bind to the `required` attribute. + * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than + * minlength. + * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than + * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of any + * length. + * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue} + * does not match a RegExp found by evaluating the AngularJS expression given in the attribute value. + * If the expression evaluates to a RegExp object, then this is used directly. + * If the expression evaluates to a string, then it will be converted to a RegExp + * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to + * `new RegExp('^abc$')`.
    + * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to + * start at the index of the last search's match, thus not taking the whole input value into + * account. + * @param {string=} ngChange AngularJS expression to be executed when input changes due to user + * interaction with the input element. + * @param {boolean=} [ngTrim=true] If set to false AngularJS will not automatically trim the input. * - * @param {string=} ngForm|name Name of the form. If specified, the form controller will be published into - * related scope, under this name. + * @knownIssue * + * When specifying the `placeholder` attribute of ` -
    + + count: {{count}} - - it('should data-bind and become invalid', function() { - if (browser.params.browser == 'safari' || browser.params.browser == 'firefox') { - // SafariDriver can't handle contenteditable - // and Firefox driver can't clear contenteditables very well - return; - } - var contentEditable = element(by.css('[contenteditable]')); - var content = 'Change me!'; + + */ - expect(contentEditable.getText()).toEqual(content); - contentEditable.clear(); - contentEditable.sendKeys(protractor.Key.BACK_SPACE); - expect(contentEditable.getText()).toEqual(''); - expect(contentEditable.getAttribute('class')).toMatch(/ng-invalid-required/); - }); - - * + /** + * @ngdoc directive + * @name ngMouseleave + * @restrict A + * @element ANY + * @priority 0 + * + * @description + * Specify custom behavior on mouseleave event. * + * @param {expression} ngMouseleave {@link guide/expression Expression} to evaluate upon + * mouseleave. ({@link guide/expression#-event- Event object is available as `$event`}) * + * @example + + + + count: {{count}} + + */ - var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', - function($scope, $exceptionHandler, $attr, $element, $parse, $animate) { - this.$viewValue = Number.NaN; - this.$modelValue = Number.NaN; - this.$parsers = []; - this.$formatters = []; - this.$viewChangeListeners = []; - this.$pristine = true; - this.$dirty = false; - this.$valid = true; - this.$invalid = false; - this.$name = $attr.name; - - var ngModelGet = $parse($attr.ngModel), - ngModelSet = ngModelGet.assign; - - if (!ngModelSet) { - throw minErr('ngModel')('nonassign', "Expression '{0}' is non-assignable. Element: {1}", - $attr.ngModel, startingTag($element)); - } - - /** - * @ngdoc method - * @name ngModel.NgModelController#$render - * - * @description - * Called when the view needs to be updated. It is expected that the user of the ng-model - * directive will implement this method. - */ - this.$render = noop; - - /** - * @ngdoc method - * @name ngModel.NgModelController#$isEmpty - * - * @description - * This is called when we need to determine if the value of the input is empty. - * - * For instance, the required directive does this to work out if the input has data or not. - * The default `$isEmpty` function checks whether the value is `undefined`, `''`, `null` or `NaN`. - * - * You can override this for input directives whose concept of being empty is different to the - * default. The `checkboxInputType` directive does this because in its case a value of `false` - * implies empty. - * - * @param {*} value Reference to check. - * @returns {boolean} True if `value` is empty. - */ - this.$isEmpty = function(value) { - return isUndefined(value) || value === '' || value === null || value !== value; - }; - - var parentForm = $element.inheritedData('$formController') || nullFormCtrl, - invalidCount = 0, // used to easily determine if we are valid - $error = this.$error = {}; // keep invalid keys here - - - // Setup initial state of the control - $element.addClass(PRISTINE_CLASS); - toggleValidCss(true); - - // convenience method for easy toggling of classes - function toggleValidCss(isValid, validationErrorKey) { - validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; - $animate.removeClass($element, (isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey); - $animate.addClass($element, (isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey); - } - - /** - * @ngdoc method - * @name ngModel.NgModelController#$setValidity - * - * @description - * Change the validity state, and notifies the form when the control changes validity. (i.e. it - * does not notify form if given validator is already marked as invalid). - * - * This method should be called by validators - i.e. the parser or formatter functions. - * - * @param {string} validationErrorKey Name of the validator. the `validationErrorKey` will assign - * to `$error[validationErrorKey]=isValid` so that it is available for data-binding. - * The `validationErrorKey` should be in camelCase and will get converted into dash-case - * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error` - * class and can be bound to as `{{someForm.someControl.$error.myError}}` . - * @param {boolean} isValid Whether the current state is valid (true) or invalid (false). - */ - this.$setValidity = function(validationErrorKey, isValid) { - // Purposeful use of ! here to cast isValid to boolean in case it is undefined - // jshint -W018 - if ($error[validationErrorKey] === !isValid) return; - // jshint +W018 - - if (isValid) { - if ($error[validationErrorKey]) invalidCount--; - if (!invalidCount) { - toggleValidCss(true); - this.$valid = true; - this.$invalid = false; - } - } else { - toggleValidCss(false); - this.$invalid = true; - this.$valid = false; - invalidCount++; - } - - $error[validationErrorKey] = !isValid; - toggleValidCss(isValid, validationErrorKey); - - parentForm.$setValidity(validationErrorKey, isValid, this); - }; - - /** - * @ngdoc method - * @name ngModel.NgModelController#$setPristine - * - * @description - * Sets the control to its pristine state. - * - * This method can be called to remove the 'ng-dirty' class and set the control to its pristine - * state (ng-pristine class). - */ - this.$setPristine = function () { - this.$dirty = false; - this.$pristine = true; - $animate.removeClass($element, DIRTY_CLASS); - $animate.addClass($element, PRISTINE_CLASS); - }; - /** - * @ngdoc method - * @name ngModel.NgModelController#$setViewValue - * - * @description - * Update the view value. - * - * This method should be called when the view value changes, typically from within a DOM event handler. - * For example {@link ng.directive:input input} and - * {@link ng.directive:select select} directives call it. - * - * It will update the $viewValue, then pass this value through each of the functions in `$parsers`, - * which includes any validators. The value that comes out of this `$parsers` pipeline, be applied to - * `$modelValue` and the **expression** specified in the `ng-model` attribute. - * - * Lastly, all the registered change listeners, in the `$viewChangeListeners` list, are called. - * - * Note that calling this function does not trigger a `$digest`. - * - * @param {string} value Value from the view. - */ - this.$setViewValue = function(value) { - this.$viewValue = value; - - // change to dirty - if (this.$pristine) { - this.$dirty = true; - this.$pristine = false; - $animate.removeClass($element, PRISTINE_CLASS); - $animate.addClass($element, DIRTY_CLASS); - parentForm.$setDirty(); - } - forEach(this.$parsers, function(fn) { - value = fn(value); - }); - - if (this.$modelValue !== value) { - this.$modelValue = value; - ngModelSet($scope, value); - forEach(this.$viewChangeListeners, function(listener) { - try { - listener(); - } catch(e) { - $exceptionHandler(e); - } - }); - } - }; + /** + * @ngdoc directive + * @name ngMousemove + * @restrict A + * @element ANY + * @priority 0 + * + * @description + * Specify custom behavior on mousemove event. + * + * @param {expression} ngMousemove {@link guide/expression Expression} to evaluate upon + * mousemove. ({@link guide/expression#-event- Event object is available as `$event`}) + * + * @example + + + + count: {{count}} + + + */ - // model -> value - var ctrl = this; - $scope.$watch(function ngModelWatch() { - var value = ngModelGet($scope); + /** + * @ngdoc directive + * @name ngKeydown + * @restrict A + * @element ANY + * @priority 0 + * + * @description + * Specify custom behavior on keydown event. + * + * @param {expression} ngKeydown {@link guide/expression Expression} to evaluate upon + * keydown. (Event object is available as `$event` and can be interrogated for keyCode, altKey, etc.) + * + * @example + + + + key down count: {{count}} + + + */ - // if scope model value and ngModel value are out of sync - if (ctrl.$modelValue !== value) { - var formatters = ctrl.$formatters, - idx = formatters.length; + /** + * @ngdoc directive + * @name ngKeyup + * @restrict A + * @element ANY + * @priority 0 + * + * @description + * Specify custom behavior on keyup event. + * + * @param {expression} ngKeyup {@link guide/expression Expression} to evaluate upon + * keyup. (Event object is available as `$event` and can be interrogated for keyCode, altKey, etc.) + * + * @example + + +

    Typing in the input box below updates the key count

    + key up count: {{count}} - ctrl.$modelValue = value; - while(idx--) { - value = formatters[idx](value); - } +

    Typing in the input box below updates the keycode

    + +

    event keyCode: {{ event.keyCode }}

    +

    event altKey: {{ event.altKey }}

    +
    +
    + */ - if (ctrl.$viewValue !== value) { - ctrl.$viewValue = value; - ctrl.$render(); - } - } - return value; - }); - }]; + /** + * @ngdoc directive + * @name ngKeypress + * @restrict A + * @element ANY + * + * @description + * Specify custom behavior on keypress event. + * + * @param {expression} ngKeypress {@link guide/expression Expression} to evaluate upon + * keypress. ({@link guide/expression#-event- Event object is available as `$event`} + * and can be interrogated for keyCode, altKey, etc.) + * + * @example + + + + key press count: {{count}} + + + */ /** * @ngdoc directive - * @name ngModel - * - * @element input + * @name ngSubmit + * @restrict A + * @element form + * @priority 0 * * @description - * The `ngModel` directive binds an `input`,`select`, `textarea` (or custom form control) to a - * property on the scope using {@link ngModel.NgModelController NgModelController}, - * which is created and exposed by this directive. + * Enables binding AngularJS expressions to onsubmit events. * - * `ngModel` is responsible for: + * Additionally it prevents the default action (which for form means sending the request to the + * server and reloading the current page), but only if the form does not contain `action`, + * `data-action`, or `x-action` attributes. * - * - Binding the view into the model, which other directives such as `input`, `textarea` or `select` - * require. - * - Providing validation behavior (i.e. required, number, email, url). - * - Keeping the state of the control (valid/invalid, dirty/pristine, validation errors). - * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`) including animations. - * - Registering the control with its parent {@link ng.directive:form form}. + *
    + * **Warning:** Be careful not to cause "double-submission" by using both the `ngClick` and + * `ngSubmit` handlers together. See the + * {@link form#submitting-a-form-and-preventing-the-default-action `form` directive documentation} + * for a detailed discussion of when `ngSubmit` may be triggered. + *
    * - * Note: `ngModel` will try to bind to the property given by evaluating the expression on the - * current scope. If the property doesn't already exist on this scope, it will be created - * implicitly and added to the scope. + * @param {expression} ngSubmit {@link guide/expression Expression} to eval. + * ({@link guide/expression#-event- Event object is available as `$event`}) * - * For best practices on using `ngModel`, see: + * @example + + + +
    + Enter text and hit enter: + + +
    list={{list}}
    +
    +
    + + it('should check ng-submit', function() { + expect(element(by.binding('list')).getText()).toBe('list=[]'); + element(by.css('#submit')).click(); + expect(element(by.binding('list')).getText()).toContain('hello'); + expect(element(by.model('text')).getAttribute('value')).toBe(''); + }); + it('should ignore empty strings', function() { + expect(element(by.binding('list')).getText()).toBe('list=[]'); + element(by.css('#submit')).click(); + element(by.css('#submit')).click(); + expect(element(by.binding('list')).getText()).toContain('hello'); + }); + +
    + */ + + /** + * @ngdoc directive + * @name ngFocus + * @restrict A + * @element window, input, select, textarea, a + * @priority 0 * - * - [https://github.com/angular/angular.js/wiki/Understanding-Scopes] + * @description + * Specify custom behavior on focus event. * - * For basic examples, how to use `ngModel`, see: + * Note: As the `focus` event is executed synchronously when calling `input.focus()` + * AngularJS executes the expression using `scope.$evalAsync` if the event is fired + * during an `$apply` to ensure a consistent state. * - * - {@link ng.directive:input input} - * - {@link input[text] text} - * - {@link input[checkbox] checkbox} - * - {@link input[radio] radio} - * - {@link input[number] number} - * - {@link input[email] email} - * - {@link input[url] url} - * - {@link ng.directive:select select} - * - {@link ng.directive:textarea textarea} + * @param {expression} ngFocus {@link guide/expression Expression} to evaluate upon + * focus. ({@link guide/expression#-event- Event object is available as `$event`}) * - * # CSS classes - * The following CSS classes are added and removed on the associated input/select/textarea element - * depending on the validity of the model. + * @example + * See {@link ng.directive:ngClick ngClick} + */ + + /** + * @ngdoc directive + * @name ngBlur + * @restrict A + * @element window, input, select, textarea, a + * @priority 0 * - * - `ng-valid` is set if the model is valid. - * - `ng-invalid` is set if the model is invalid. - * - `ng-pristine` is set if the model is pristine. - * - `ng-dirty` is set if the model is dirty. + * @description + * Specify custom behavior on blur event. * - * Keep in mind that ngAnimate can detect each of these classes when added and removed. + * A [blur event](https://developer.mozilla.org/en-US/docs/Web/Events/blur) fires when + * an element has lost focus. * - * ## Animation Hooks + * Note: As the `blur` event is executed synchronously also during DOM manipulations + * (e.g. removing a focussed input), + * AngularJS executes the expression using `scope.$evalAsync` if the event is fired + * during an `$apply` to ensure a consistent state. * - * Animations within models are triggered when any of the associated CSS classes are added and removed - * on the input element which is attached to the model. These classes are: `.ng-pristine`, `.ng-dirty`, - * `.ng-invalid` and `.ng-valid` as well as any other validations that are performed on the model itself. - * The animations that are triggered within ngModel are similar to how they work in ngClass and - * animations can be hooked into using CSS transitions, keyframes as well as JS animations. + * @param {expression} ngBlur {@link guide/expression Expression} to evaluate upon + * blur. ({@link guide/expression#-event- Event object is available as `$event`}) * - * The following example shows a simple way to utilize CSS transitions to style an input element - * that has been rendered as invalid after it has been validated: + * @example + * See {@link ng.directive:ngClick ngClick} + */ + + /** + * @ngdoc directive + * @name ngCopy + * @restrict A + * @element window, input, select, textarea, a + * @priority 0 * - *
    -     * //be sure to include ngAnimate as a module to hook into more
    -     * //advanced animations
    -     * .my-input {
    - *   transition:0.5s linear all;
    - *   background: white;
    - * }
    -     * .my-input.ng-invalid {
    - *   background: red;
    - *   color:white;
    - * }
    -     * 
    + * @description + * Specify custom behavior on copy event. + * + * @param {expression} ngCopy {@link guide/expression Expression} to evaluate upon + * copy. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - * + - - - Update input to see transitions when valid/invalid. - Integer is a valid value. -
    - -
    + + copied: {{copied}}
    - *
    +
    */ - var ngModelDirective = function() { - return { - require: ['ngModel', '^?form'], - controller: NgModelController, - link: function(scope, element, attr, ctrls) { - // notify others, especially parent forms - - var modelCtrl = ctrls[0], - formCtrl = ctrls[1] || nullFormCtrl; - formCtrl.$addControl(modelCtrl); - - scope.$on('$destroy', function() { - formCtrl.$removeControl(modelCtrl); - }); - } - }; - }; + /** + * @ngdoc directive + * @name ngCut + * @restrict A + * @element window, input, select, textarea, a + * @priority 0 + * + * @description + * Specify custom behavior on cut event. + * + * @param {expression} ngCut {@link guide/expression Expression} to evaluate upon + * cut. ({@link guide/expression#-event- Event object is available as `$event`}) + * + * @example + + + + cut: {{cut}} + + + */ + /** + * @ngdoc directive + * @name ngPaste + * @restrict A + * @element window, input, select, textarea, a + * @priority 0 + * + * @description + * Specify custom behavior on paste event. + * + * @param {expression} ngPaste {@link guide/expression Expression} to evaluate upon + * paste. ({@link guide/expression#-event- Event object is available as `$event`}) + * + * @example + + + + pasted: {{paste}} + + + */ /** * @ngdoc directive - * @name ngChange + * @name ngIf + * @restrict A + * @multiElement * * @description - * Evaluate the given expression when the user changes the input. - * The expression is evaluated immediately, unlike the JavaScript onchange event - * which only triggers at the end of a change (usually, when the user leaves the - * form element or presses the return key). - * The expression is not evaluated when the value change is coming from the model. + * The `ngIf` directive removes or recreates a portion of the DOM tree based on an + * {expression}. If the expression assigned to `ngIf` evaluates to a false + * value then the element is removed from the DOM, otherwise a clone of the + * element is reinserted into the DOM. + * + * `ngIf` differs from `ngShow` and `ngHide` in that `ngIf` completely removes and recreates the + * element in the DOM rather than changing its visibility via the `display` css property. A common + * case when this difference is significant is when using css selectors that rely on an element's + * position within the DOM, such as the `:first-child` or `:last-child` pseudo-classes. * - * Note, this directive requires `ngModel` to be present. + * Note that when an element is removed using `ngIf` its scope is destroyed and a new scope + * is created when the element is restored. The scope created within `ngIf` inherits from + * its parent scope using + * [prototypal inheritance](https://github.com/angular/angular.js/wiki/Understanding-Scopes#javascript-prototypal-inheritance). + * An important implication of this is if `ngModel` is used within `ngIf` to bind to + * a javascript primitive defined in the parent scope. In this case any modifications made to the + * variable within the child scope will override (hide) the value in the parent scope. * - * @element input - * @param {expression} ngChange {@link guide/expression Expression} to evaluate upon change - * in input value. + * Also, `ngIf` recreates elements using their compiled state. An example of this behavior + * is if an element's class attribute is directly modified after it's compiled, using something like + * jQuery's `.addClass()` method, and the element is later removed. When `ngIf` recreates the element + * the added class will be lost because the original compiled state is used to regenerate the element. * - * @example - * - * - * - *
    - * - * - *
    - * debug = {{confirmed}}
    - * counter = {{counter}}
    - *
    - *
    - * - * var counter = element(by.binding('counter')); - * var debug = element(by.binding('confirmed')); + * Additionally, you can provide animations via the `ngAnimate` module to animate the `enter` + * and `leave` effects. * - * it('should evaluate the expression if changing from view', function() { - * expect(counter.getText()).toContain('0'); - * - * element(by.id('ng-change-example1')).click(); - * - * expect(counter.getText()).toContain('1'); - * expect(debug.getText()).toContain('true'); - * }); + * @animations + * | Animation | Occurs | + * |----------------------------------|-------------------------------------| + * | {@link ng.$animate#enter enter} | just after the `ngIf` contents change and a new DOM element is created and injected into the `ngIf` container | + * | {@link ng.$animate#leave leave} | just before the `ngIf` contents are removed from the DOM | * - * it('should not evaluate the expression if changing from model', function() { - * element(by.id('ng-change-example2')).click(); + * @element ANY + * @scope + * @priority 600 + * @param {expression} ngIf If the {@link guide/expression expression} is falsy then + * the element is removed from the DOM tree. If it is truthy a copy of the compiled + * element is added to the DOM tree. + * + * @example + + +
    + Show when checked: + + This is removed when the checkbox is unchecked. + +
    + + .animate-if { + background:white; + border:1px solid black; + padding:10px; + } - * expect(counter.getText()).toContain('0'); - * expect(debug.getText()).toContain('true'); - * }); - * - *
    - */ - var ngChangeDirective = valueFn({ - require: 'ngModel', - link: function(scope, element, attr, ctrl) { - ctrl.$viewChangeListeners.push(function() { - scope.$eval(attr.ngChange); - }); - } - }); + .animate-if.ng-enter, .animate-if.ng-leave { + transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; + } + .animate-if.ng-enter, + .animate-if.ng-leave.ng-leave-active { + opacity:0; + } - var requiredDirective = function() { + .animate-if.ng-leave, + .animate-if.ng-enter.ng-enter-active { + opacity:1; + } +
    +
    + */ + var ngIfDirective = ['$animate', '$compile', function($animate, $compile) { return { - require: '?ngModel', - link: function(scope, elm, attr, ctrl) { - if (!ctrl) return; - attr.required = true; // force truthy in case we are on non input element + multiElement: true, + transclude: 'element', + priority: 600, + terminal: true, + restrict: 'A', + $$tlb: true, + link: function($scope, $element, $attr, ctrl, $transclude) { + var block, childScope, previousElements; + $scope.$watch($attr.ngIf, function ngIfWatchAction(value) { - var validator = function(value) { - if (attr.required && ctrl.$isEmpty(value)) { - ctrl.$setValidity('required', false); - return; + if (value) { + if (!childScope) { + $transclude(function(clone, newScope) { + childScope = newScope; + clone[clone.length++] = $compile.$$createComment('end ngIf', $attr.ngIf); + // Note: We only need the first/last node of the cloned nodes. + // However, we need to keep the reference to the jqlite wrapper as it might be changed later + // by a directive with templateUrl when its template arrives. + block = { + clone: clone + }; + $animate.enter(clone, $element.parent(), $element); + }); + } } else { - ctrl.$setValidity('required', true); - return value; + if (previousElements) { + previousElements.remove(); + previousElements = null; + } + if (childScope) { + childScope.$destroy(); + childScope = null; + } + if (block) { + previousElements = getBlockNodes(block.clone); + $animate.leave(previousElements).done(function(response) { + if (response !== false) previousElements = null; + }); + block = null; + } } - }; - - ctrl.$formatters.push(validator); - ctrl.$parsers.unshift(validator); - - attr.$observe('required', function() { - validator(ctrl.$viewValue); }); } }; - }; - + }]; /** * @ngdoc directive - * @name ngList + * @name ngInclude + * @restrict ECA + * @scope + * @priority -400 * * @description - * Text input that converts between a delimited string and an array of strings. The delimiter - * can be a fixed string (by default a comma) or a regular expression. + * Fetches, compiles and includes an external HTML fragment. + * + * By default, the template URL is restricted to the same domain and protocol as the + * application document. This is done by calling {@link $sce#getTrustedResourceUrl + * $sce.getTrustedResourceUrl} on it. To load templates from other domains or protocols + * you may either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist them} or + * {@link $sce#trustAsResourceUrl wrap them} as trusted values. Refer to AngularJS's {@link + * ng.$sce Strict Contextual Escaping}. + * + * In addition, the browser's + * [Same Origin Policy](https://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_XMLHttpRequest) + * and [Cross-Origin Resource Sharing (CORS)](http://www.w3.org/TR/cors/) + * policy may further restrict whether the template is successfully loaded. + * For example, `ngInclude` won't work for cross-domain requests on all browsers and for `file://` + * access on some browsers. + * + * @animations + * | Animation | Occurs | + * |----------------------------------|-------------------------------------| + * | {@link ng.$animate#enter enter} | when the expression changes, on the new include | + * | {@link ng.$animate#leave leave} | when the expression changes, on the old include | + * + * The enter and leave animation occur concurrently. + * + * @param {string} ngInclude|src AngularJS expression evaluating to URL. If the source is a string constant, + * make sure you wrap it in **single** quotes, e.g. `src="'myPartialTemplate.html'"`. + * @param {string=} onload Expression to evaluate when a new partial is loaded. + *
    + * **Note:** When using onload on SVG elements in IE11, the browser will try to call + * a function with the name on the window element, which will usually throw a + * "function is undefined" error. To fix this, you can instead use `data-onload` or a + * different form that {@link guide/directive#normalization matches} `onload`. + *
    * - * @element input - * @param {string=} ngList optional delimiter that should be used to split the value. If - * specified in form `/something/` then the value will be converted into a regular expression. + * @param {string=} autoscroll Whether `ngInclude` should call {@link ng.$anchorScroll + * $anchorScroll} to scroll the viewport after the content is loaded. + * + * - If the attribute is not set, disable scrolling. + * - If the attribute is set without value, enable scrolling. + * - Otherwise enable scrolling only if the expression evaluates to truthy value. * * @example - + - -
    - List: - - Required! -
    - names = {{names}}
    - myForm.namesInput.$valid = {{myForm.namesInput.$valid}}
    - myForm.namesInput.$error = {{myForm.namesInput.$error}}
    - myForm.$valid = {{myForm.$valid}}
    - myForm.$error.required = {{!!myForm.$error.required}}
    -
    +
    + + url of the template: {{template.url}} +
    +
    +
    +
    +
    +
    + + angular.module('includeExample', ['ngAnimate']) + .controller('ExampleController', ['$scope', function($scope) { + $scope.templates = + [{ name: 'template1.html', url: 'template1.html'}, + { name: 'template2.html', url: 'template2.html'}]; + $scope.template = $scope.templates[0]; + }]); + + + Content of template1.html + + + Content of template2.html + + + .slide-animate-container { + position:relative; + background:white; + border:1px solid black; + height:40px; + overflow:hidden; + } + + .slide-animate { + padding:10px; + } + + .slide-animate.ng-enter, .slide-animate.ng-leave { + transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; + + position:absolute; + top:0; + left:0; + right:0; + bottom:0; + display:block; + padding:10px; + } + + .slide-animate.ng-enter { + top:-50px; + } + .slide-animate.ng-enter.ng-enter-active { + top:0; + } + + .slide-animate.ng-leave { + top:0; + } + .slide-animate.ng-leave.ng-leave-active { + top:50px; + } - var listInput = element(by.model('names')); - var names = element(by.binding('{{names}}')); - var valid = element(by.binding('myForm.namesInput.$valid')); - var error = element(by.css('span.error')); + var templateSelect = element(by.model('template')); + var includeElem = element(by.css('[ng-include]')); - it('should initialize to model', function() { - expect(names.getText()).toContain('["igor","misko","vojta"]'); - expect(valid.getText()).toContain('true'); - expect(error.getCssValue('display')).toBe('none'); - }); + it('should load template1.html', function() { + expect(includeElem.getText()).toMatch(/Content of template1.html/); + }); - it('should be invalid if empty', function() { - listInput.clear(); - listInput.sendKeys(''); + it('should load template2.html', function() { + if (browser.params.browser === 'firefox') { + // Firefox can't handle using selects + // See https://github.com/angular/protractor/issues/480 + return; + } + templateSelect.click(); + templateSelect.all(by.css('option')).get(2).click(); + expect(includeElem.getText()).toMatch(/Content of template2.html/); + }); - expect(names.getText()).toContain(''); - expect(valid.getText()).toContain('false'); - expect(error.getCssValue('display')).not.toBe('none'); }); + it('should change to blank', function() { + if (browser.params.browser === 'firefox') { + // Firefox can't handle using selects + return; + } + templateSelect.click(); + templateSelect.all(by.css('option')).get(0).click(); + expect(includeElem.isPresent()).toBe(false); + });
    */ - var ngListDirective = function() { - return { - require: 'ngModel', - link: function(scope, element, attr, ctrl) { - var match = /\/(.*)\//.exec(attr.ngList), - separator = match && new RegExp(match[1]) || attr.ngList || ','; - var parse = function(viewValue) { - // If the viewValue is invalid (say required but empty) it will be `undefined` - if (isUndefined(viewValue)) return; - var list = []; + /** + * @ngdoc event + * @name ngInclude#$includeContentRequested + * @eventType emit on the scope ngInclude was declared in + * @description + * Emitted every time the ngInclude content is requested. + * + * @param {Object} angularEvent Synthetic event object. + * @param {String} src URL of content to load. + */ + + + /** + * @ngdoc event + * @name ngInclude#$includeContentLoaded + * @eventType emit on the current ngInclude scope + * @description + * Emitted every time the ngInclude content is reloaded. + * + * @param {Object} angularEvent Synthetic event object. + * @param {String} src URL of content to load. + */ + + + /** + * @ngdoc event + * @name ngInclude#$includeContentError + * @eventType emit on the scope ngInclude was declared in + * @description + * Emitted when a template HTTP request yields an erroneous response (status < 200 || status > 299) + * + * @param {Object} angularEvent Synthetic event object. + * @param {String} src URL of content to load. + */ + var ngIncludeDirective = ['$templateRequest', '$anchorScroll', '$animate', + function($templateRequest, $anchorScroll, $animate) { + return { + restrict: 'ECA', + priority: 400, + terminal: true, + transclude: 'element', + controller: angular.noop, + compile: function(element, attr) { + var srcExp = attr.ngInclude || attr.src, + onloadExp = attr.onload || '', + autoScrollExp = attr.autoscroll; - if (viewValue) { - forEach(viewValue.split(separator), function(value) { - if (value) list.push(trim(value)); - }); - } + return function(scope, $element, $attr, ctrl, $transclude) { + var changeCounter = 0, + currentScope, + previousElement, + currentElement; - return list; - }; + var cleanupLastIncludeContent = function() { + if (previousElement) { + previousElement.remove(); + previousElement = null; + } + if (currentScope) { + currentScope.$destroy(); + currentScope = null; + } + if (currentElement) { + $animate.leave(currentElement).done(function(response) { + if (response !== false) previousElement = null; + }); + previousElement = currentElement; + currentElement = null; + } + }; - ctrl.$parsers.push(parse); - ctrl.$formatters.push(function(value) { - if (isArray(value)) { - return value.join(', '); - } + scope.$watch(srcExp, function ngIncludeWatchAction(src) { + var afterAnimation = function(response) { + if (response !== false && isDefined(autoScrollExp) && + (!autoScrollExp || scope.$eval(autoScrollExp))) { + $anchorScroll(); + } + }; + var thisChangeId = ++changeCounter; - return undefined; - }); + if (src) { + //set the 2nd param to true to ignore the template request error so that the inner + //contents and scope can be cleaned up. + $templateRequest(src, true).then(function(response) { + if (scope.$$destroyed) return; - // Override the standard $isEmpty because an empty array means the input is empty. - ctrl.$isEmpty = function(value) { - return !value || !value.length; - }; - } - }; - }; + if (thisChangeId !== changeCounter) return; + var newScope = scope.$new(); + ctrl.template = response; + // Note: This will also link all children of ng-include that were contained in the original + // html. If that content contains controllers, ... they could pollute/change the scope. + // However, using ng-include on an element with additional content does not make sense... + // Note: We can't remove them in the cloneAttchFn of $transclude as that + // function is called before linking the content, which would apply child + // directives to non existing elements. + var clone = $transclude(newScope, function(clone) { + cleanupLastIncludeContent(); + $animate.enter(clone, null, $element).done(afterAnimation); + }); - var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/; - /** - * @ngdoc directive - * @name ngValue - * - * @description - * Binds the given expression to the value of `input[select]` or `input[radio]`, so - * that when the element is selected, the `ngModel` of that element is set to the - * bound value. - * - * `ngValue` is useful when dynamically generating lists of radio buttons using `ng-repeat`, as - * shown below. - * - * @element input - * @param {string=} ngValue angular expression, whose value will be bound to the `value` attribute - * of the `input` element - * - * @example - - - -
    -

    Which is your favorite?

    - -
    You chose {{my.favorite}}
    -
    -
    - - var favorite = element(by.binding('my.favorite')); + currentScope = newScope; + currentElement = clone; - it('should initialize to model', function() { - expect(favorite.getText()).toContain('unicorns'); - }); - it('should bind the values to the inputs', function() { - element.all(by.model('my.favorite')).get(0).click(); - expect(favorite.getText()).toContain('pizza'); - }); - -
    - */ - var ngValueDirective = function() { - return { - priority: 100, - compile: function(tpl, tplAttr) { - if (CONSTANT_VALUE_REGEXP.test(tplAttr.ngValue)) { - return function ngValueConstantLink(scope, elm, attr) { - attr.$set('value', scope.$eval(attr.ngValue)); - }; - } else { - return function ngValueLink(scope, elm, attr) { - scope.$watch(attr.ngValue, function valueWatchAction(value) { - attr.$set('value', value); + currentScope.$emit('$includeContentLoaded', src); + scope.$eval(onloadExp); + }, function() { + if (scope.$$destroyed) return; + + if (thisChangeId === changeCounter) { + cleanupLastIncludeContent(); + scope.$emit('$includeContentError', src); + } + }); + scope.$emit('$includeContentRequested', src); + } else { + cleanupLastIncludeContent(); + ctrl.template = null; + } }); }; } - } - }; - }; + }; + }]; + +// This directive is called during the $transclude call of the first `ngInclude` directive. +// It will replace and compile the content of the element with the loaded template. +// We need this directive so that the element content is already filled when +// the link function of another directive on the same element as ngInclude +// is called. + var ngIncludeFillContentDirective = ['$compile', + function($compile) { + return { + restrict: 'ECA', + priority: -400, + require: 'ngInclude', + link: function(scope, $element, $attr, ctrl) { + if (toString.call($element[0]).match(/SVG/)) { + // WebKit: https://bugs.webkit.org/show_bug.cgi?id=135698 --- SVG elements do not + // support innerHTML, so detect this here and try to generate the contents + // specially. + $element.empty(); + $compile(jqLiteBuildFragment(ctrl.template, window.document).childNodes)(scope, + function namespaceAdaptedClone(clone) { + $element.append(clone); + }, {futureParentElement: $element}); + return; + } + + $element.html(ctrl.template); + $compile($element.contents())(scope); + } + }; + }]; /** * @ngdoc directive - * @name ngBind + * @name ngInit * @restrict AC - * - * @description - * The `ngBind` attribute tells Angular to replace the text content of the specified HTML element - * with the value of a given expression, and to update the text content when the value of that - * expression changes. - * - * Typically, you don't use `ngBind` directly, but instead you use the double curly markup like - * `{{ expression }}` which is similar but less verbose. - * - * It is preferable to use `ngBind` instead of `{{ expression }}` when a template is momentarily - * displayed by the browser in its raw state before Angular compiles it. Since `ngBind` is an - * element attribute, it makes the bindings invisible to the user while the page is loading. - * - * An alternative solution to this problem would be using the - * {@link ng.directive:ngCloak ngCloak} directive. - * - * + * @priority 450 * @element ANY - * @param {expression} ngBind {@link guide/expression Expression} to evaluate. * - * @example - * Enter a name in the Live Preview text box; the greeting below the text box changes instantly. - - - -
    - Enter name:
    - Hello ! -
    -
    - - it('should check ng-bind', function() { - var nameInput = element(by.model('name')); - - expect(element(by.binding('name')).getText()).toBe('Whirled'); - nameInput.clear(); - nameInput.sendKeys('world'); - expect(element(by.binding('name')).getText()).toBe('world'); - }); - -
    - */ - var ngBindDirective = ngDirective(function(scope, element, attr) { - element.addClass('ng-binding').data('$binding', attr.ngBind); - scope.$watch(attr.ngBind, function ngBindWatchAction(value) { - // We are purposefully using == here rather than === because we want to - // catch when value is "null or undefined" - // jshint -W041 - element.text(value == undefined ? '' : value); - }); - }); - - - /** - * @ngdoc directive - * @name ngBindTemplate + * @param {expression} ngInit {@link guide/expression Expression} to eval. * * @description - * The `ngBindTemplate` directive specifies that the element - * text content should be replaced with the interpolation of the template - * in the `ngBindTemplate` attribute. - * Unlike `ngBind`, the `ngBindTemplate` can contain multiple `{{` `}}` - * expressions. This directive is needed since some HTML elements - * (such as TITLE and OPTION) cannot contain SPAN elements. + * The `ngInit` directive allows you to evaluate an expression in the + * current scope. * - * @element ANY - * @param {string} ngBindTemplate template of form - * {{ expression }} to eval. + *
    + * This directive can be abused to add unnecessary amounts of logic into your templates. + * There are only a few appropriate uses of `ngInit`: + *
      + *
    • aliasing special properties of {@link ng.directive:ngRepeat `ngRepeat`}, + * as seen in the demo below.
    • + *
    • initializing data during development, or for examples, as seen throughout these docs.
    • + *
    • injecting data via server side scripting.
    • + *
    + * + * Besides these few cases, you should use {@link guide/component Components} or + * {@link guide/controller Controllers} rather than `ngInit` to initialize values on a scope. + *
    + * + *
    + * **Note**: If you have assignment in `ngInit` along with a {@link ng.$filter `filter`}, make + * sure you have parentheses to ensure correct operator precedence: + *
    +     * `
    ` + *
    + *
    * * @example - * Try it here: enter text in text box and watch the greeting change. - + -
    - Salutation:
    - Name:
    -
    
    +     
    +
    +
    + list[ {{outerIndex}} ][ {{innerIndex}} ] = {{value}}; +
    +
    - it('should check ng-bind', function() { - var salutationElem = element(by.binding('salutation')); - var salutationInput = element(by.model('salutation')); - var nameInput = element(by.model('name')); - - expect(salutationElem.getText()).toBe('Hello World!'); - - salutationInput.clear(); - salutationInput.sendKeys('Greetings'); - nameInput.clear(); - nameInput.sendKeys('user'); - - expect(salutationElem.getText()).toBe('Greetings user!'); + it('should alias index positions', function() { + var elements = element.all(by.css('.example-init')); + expect(elements.get(0).getText()).toBe('list[ 0 ][ 0 ] = a;'); + expect(elements.get(1).getText()).toBe('list[ 0 ][ 1 ] = b;'); + expect(elements.get(2).getText()).toBe('list[ 1 ][ 0 ] = c;'); + expect(elements.get(3).getText()).toBe('list[ 1 ][ 1 ] = d;'); }); */ - var ngBindTemplateDirective = ['$interpolate', function($interpolate) { - return function(scope, element, attr) { - // TODO: move this to scenario runner - var interpolateFn = $interpolate(element.attr(attr.$attr.ngBindTemplate)); - element.addClass('ng-binding').data('$binding', interpolateFn); - attr.$observe('ngBindTemplate', function(value) { - element.text(value); - }); - }; - }]; - + var ngInitDirective = ngDirective({ + priority: 450, + compile: function() { + return { + pre: function(scope, element, attrs) { + scope.$eval(attrs.ngInit); + } + }; + } + }); /** * @ngdoc directive - * @name ngBindHtml + * @name ngList + * @restrict A + * @priority 100 + * + * @param {string=} ngList optional delimiter that should be used to split the value. * * @description - * Creates a binding that will innerHTML the result of evaluating the `expression` into the current - * element in a secure way. By default, the innerHTML-ed content will be sanitized using the {@link - * ngSanitize.$sanitize $sanitize} service. To utilize this functionality, ensure that `$sanitize` - * is available, for example, by including {@link ngSanitize} in your module's dependencies (not in - * core Angular.) You may also bypass sanitization for values you know are safe. To do so, bind to - * an explicitly trusted value via {@link ng.$sce#trustAsHtml $sce.trustAsHtml}. See the example - * under {@link ng.$sce#Example Strict Contextual Escaping (SCE)}. + * Text input that converts between a delimited string and an array of strings. The default + * delimiter is a comma followed by a space - equivalent to `ng-list=", "`. You can specify a custom + * delimiter as the value of the `ngList` attribute - for example, `ng-list=" | "`. + * + * The behaviour of the directive is affected by the use of the `ngTrim` attribute. + * * If `ngTrim` is set to `"false"` then whitespace around both the separator and each + * list item is respected. This implies that the user of the directive is responsible for + * dealing with whitespace but also allows you to use whitespace as a delimiter, such as a + * tab or newline character. + * * Otherwise whitespace around the delimiter is ignored when splitting (although it is respected + * when joining the list items back together) and whitespace around each list item is stripped + * before it is added to the model. * - * Note: If a `$sanitize` service is unavailable and the bound value isn't explicitly trusted, you - * will have an exception (instead of an exploit.) + * @example + * ### Validation + * + * + * + * angular.module('listExample', []) + * .controller('ExampleController', ['$scope', function($scope) { + * $scope.names = ['morpheus', 'neo', 'trinity']; + * }]); + * + * + *
    + * + * + * + * Required! + * + *
    + * names = {{names}}
    + * myForm.namesInput.$valid = {{myForm.namesInput.$valid}}
    + * myForm.namesInput.$error = {{myForm.namesInput.$error}}
    + * myForm.$valid = {{myForm.$valid}}
    + * myForm.$error.required = {{!!myForm.$error.required}}
    + *
    + *
    + * + * var listInput = element(by.model('names')); + * var names = element(by.exactBinding('names')); + * var valid = element(by.binding('myForm.namesInput.$valid')); + * var error = element(by.css('span.error')); + * + * it('should initialize to model', function() { + * expect(names.getText()).toContain('["morpheus","neo","trinity"]'); + * expect(valid.getText()).toContain('true'); + * expect(error.getCssValue('display')).toBe('none'); + * }); * - * @element ANY - * @param {expression} ngBindHtml {@link guide/expression Expression} to evaluate. + * it('should be invalid if empty', function() { + * listInput.clear(); + * listInput.sendKeys(''); + * + * expect(names.getText()).toContain(''); + * expect(valid.getText()).toContain('false'); + * expect(error.getCssValue('display')).not.toBe('none'); + * }); + * + *
    * * @example - Try it here: enter text in text box and watch the greeting change. + * ### Splitting on newline + * + * + * + * + *
    {{ list | json }}
    + *
    + * + * it("should split the text by newlines", function() { + * var listInput = element(by.model('list')); + * var output = element(by.binding('list | json')); + * listInput.sendKeys('abc\ndef\nghi'); + * expect(output.getText()).toContain('[\n "abc",\n "def",\n "ghi"\n]'); + * }); + * + *
    + * + */ + var ngListDirective = function() { + return { + restrict: 'A', + priority: 100, + require: 'ngModel', + link: function(scope, element, attr, ctrl) { + var ngList = attr.ngList || ', '; + var trimValues = attr.ngTrim !== 'false'; + var separator = trimValues ? trim(ngList) : ngList; - - -
    -

    -
    -
    + var parse = function(viewValue) { + // If the viewValue is invalid (say required but empty) it will be `undefined` + if (isUndefined(viewValue)) return; - - angular.module('ngBindHtmlExample', ['ngSanitize']) + var list = []; - .controller('ngBindHtmlCtrl', ['$scope', function ngBindHtmlCtrl($scope) { - $scope.myHTML = - 'I am an HTMLstring with links! and other stuff'; - }]); - + if (viewValue) { + forEach(viewValue.split(separator), function(value) { + if (value) list.push(trimValues ? trim(value) : value); + }); + } - - it('should check ng-bind-html', function() { - expect(element(by.binding('myHTML')).getText()).toBe( - 'I am an HTMLstring with links! and other stuff'); - }); - -
    - */ - var ngBindHtmlDirective = ['$sce', '$parse', function($sce, $parse) { - return function(scope, element, attr) { - element.addClass('ng-binding').data('$binding', attr.ngBindHtml); + return list; + }; - var parsed = $parse(attr.ngBindHtml); - function getStringValue() { return (parsed(scope) || '').toString(); } + ctrl.$parsers.push(parse); + ctrl.$formatters.push(function(value) { + if (isArray(value)) { + return value.join(ngList); + } - scope.$watch(getStringValue, function ngBindHtmlWatchAction(value) { - element.html($sce.getTrustedHtml(parsed(scope)) || ''); - }); - }; - }]; + return undefined; + }); - function classDirective(name, selector) { - name = 'ngClass' + name; - return ['$animate', function($animate) { - return { - restrict: 'AC', - link: function(scope, element, attr) { - var oldVal; + // Override the standard $isEmpty because an empty array means the input is empty. + ctrl.$isEmpty = function(value) { + return !value || !value.length; + }; + } + }; + }; - scope.$watch(attr[name], ngClassWatchAction, true); + /* global VALID_CLASS: true, + INVALID_CLASS: true, + PRISTINE_CLASS: true, + DIRTY_CLASS: true, + UNTOUCHED_CLASS: true, + TOUCHED_CLASS: true, + PENDING_CLASS: true, + addSetValidityMethod: true, + setupValidity: true, + defaultModelOptions: false +*/ - attr.$observe('class', function(value) { - ngClassWatchAction(scope.$eval(attr[name])); - }); + var VALID_CLASS = 'ng-valid', + INVALID_CLASS = 'ng-invalid', + PRISTINE_CLASS = 'ng-pristine', + DIRTY_CLASS = 'ng-dirty', + UNTOUCHED_CLASS = 'ng-untouched', + TOUCHED_CLASS = 'ng-touched', + EMPTY_CLASS = 'ng-empty', + NOT_EMPTY_CLASS = 'ng-not-empty'; - if (name !== 'ngClass') { - scope.$watch('$index', function($index, old$index) { - // jshint bitwise: false - var mod = $index & 1; - if (mod !== old$index & 1) { - var classes = arrayClasses(scope.$eval(attr[name])); - mod === selector ? - addClasses(classes) : - removeClasses(classes); - } - }); - } + var ngModelMinErr = minErr('ngModel'); - function addClasses(classes) { - var newClasses = digestClassCounts(classes, 1); - attr.$addClass(newClasses); - } + /** + * @ngdoc type + * @name ngModel.NgModelController + * @property {*} $viewValue The actual value from the control's view. For `input` elements, this is a + * String. See {@link ngModel.NgModelController#$setViewValue} for information about when the $viewValue + * is set. + * + * @property {*} $modelValue The value in the model that the control is bound to. + * + * @property {Array.} $parsers Array of functions to execute, as a pipeline, whenever + * the control updates the ngModelController with a new {@link ngModel.NgModelController#$viewValue + `$viewValue`} from the DOM, usually via user input. + See {@link ngModel.NgModelController#$setViewValue `$setViewValue()`} for a detailed lifecycle explanation. + Note that the `$parsers` are not called when the bound ngModel expression changes programmatically. - function removeClasses(classes) { - var newClasses = digestClassCounts(classes, -1); - attr.$removeClass(newClasses); - } + The functions are called in array order, each passing + its return value through to the next. The last return value is forwarded to the + {@link ngModel.NgModelController#$validators `$validators`} collection. - function digestClassCounts (classes, count) { - var classCounts = element.data('$classCounts') || {}; - var classesToUpdate = []; - forEach(classes, function (className) { - if (count > 0 || classCounts[className]) { - classCounts[className] = (classCounts[className] || 0) + count; - if (classCounts[className] === +(count > 0)) { - classesToUpdate.push(className); - } - } - }); - element.data('$classCounts', classCounts); - return classesToUpdate.join(' '); - } + Parsers are used to sanitize / convert the {@link ngModel.NgModelController#$viewValue + `$viewValue`}. - function updateClasses (oldClasses, newClasses) { - var toAdd = arrayDifference(newClasses, oldClasses); - var toRemove = arrayDifference(oldClasses, newClasses); - toRemove = digestClassCounts(toRemove, -1); - toAdd = digestClassCounts(toAdd, 1); + Returning `undefined` from a parser means a parse error occurred. In that case, + no {@link ngModel.NgModelController#$validators `$validators`} will run and the `ngModel` + will be set to `undefined` unless {@link ngModelOptions `ngModelOptions.allowInvalid`} + is set to `true`. The parse error is stored in `ngModel.$error.parse`. - if (toAdd.length === 0) { - $animate.removeClass(element, toRemove); - } else if (toRemove.length === 0) { - $animate.addClass(element, toAdd); - } else { - $animate.setClass(element, toAdd, toRemove); - } - } + This simple example shows a parser that would convert text input value to lowercase: + * ```js + * function parse(value) { + * if (value) { + * return value.toLowerCase(); + * } + * } + * ngModelController.$parsers.push(parse); + * ``` - function ngClassWatchAction(newVal) { - if (selector === true || scope.$index % 2 === selector) { - var newClasses = arrayClasses(newVal || []); - if (!oldVal) { - addClasses(newClasses); - } else if (!equals(newVal,oldVal)) { - var oldClasses = arrayClasses(oldVal); - updateClasses(oldClasses, newClasses); - } - } - oldVal = copy(newVal); - } - } - }; + * + * @property {Array.} $formatters Array of functions to execute, as a pipeline, whenever + the bound ngModel expression changes programmatically. The `$formatters` are not called when the + value of the control is changed by user interaction. - function arrayDifference(tokens1, tokens2) { - var values = []; + Formatters are used to format / convert the {@link ngModel.NgModelController#$modelValue + `$modelValue`} for display in the control. - outer: - for(var i = 0; i < tokens1.length; i++) { - var token = tokens1[i]; - for(var j = 0; j < tokens2.length; j++) { - if(token == tokens2[j]) continue outer; - } - values.push(token); - } - return values; - } + The functions are called in reverse array order, each passing the value through to the + next. The last return value is used as the actual DOM value. - function arrayClasses (classVal) { - if (isArray(classVal)) { - return classVal; - } else if (isString(classVal)) { - return classVal.split(' '); - } else if (isObject(classVal)) { - var classes = [], i = 0; - forEach(classVal, function(v, k) { - if (v) { - classes.push(k); - } - }); - return classes; - } - return classVal; - } - }]; - } + This simple example shows a formatter that would convert the model value to uppercase: - /** - * @ngdoc directive - * @name ngClass - * @restrict AC + * ```js + * function format(value) { + * if (value) { + * return value.toUpperCase(); + * } + * } + * ngModel.$formatters.push(format); + * ``` * - * @description - * The `ngClass` directive allows you to dynamically set CSS classes on an HTML element by databinding - * an expression that represents all classes to be added. + * @property {Object.} $validators A collection of validators that are applied + * whenever the model value changes. The key value within the object refers to the name of the + * validator while the function refers to the validation operation. The validation operation is + * provided with the model value as an argument and must return a true or false value depending + * on the response of that validation. * - * The directive operates in three different ways, depending on which of three types the expression - * evaluates to: + * ```js + * ngModel.$validators.validCharacters = function(modelValue, viewValue) { + * var value = modelValue || viewValue; + * return /[0-9]+/.test(value) && + * /[a-z]+/.test(value) && + * /[A-Z]+/.test(value) && + * /\W+/.test(value); + * }; + * ``` * - * 1. If the expression evaluates to a string, the string should be one or more space-delimited class - * names. + * @property {Object.} $asyncValidators A collection of validations that are expected to + * perform an asynchronous validation (e.g. a HTTP request). The validation function that is provided + * is expected to return a promise when it is run during the model validation process. Once the promise + * is delivered then the validation status will be set to true when fulfilled and false when rejected. + * When the asynchronous validators are triggered, each of the validators will run in parallel and the model + * value will only be updated once all validators have been fulfilled. As long as an asynchronous validator + * is unfulfilled, its key will be added to the controllers `$pending` property. Also, all asynchronous validators + * will only run once all synchronous validators have passed. * - * 2. If the expression evaluates to an array, each element of the array should be a string that is - * one or more space-delimited class names. + * Please note that if $http is used then it is important that the server returns a success HTTP response code + * in order to fulfill the validation and a status level of `4xx` in order to reject the validation. * - * 3. If the expression evaluates to an object, then for each key-value pair of the - * object with a truthy value the corresponding key is used as a class name. + * ```js + * ngModel.$asyncValidators.uniqueUsername = function(modelValue, viewValue) { + * var value = modelValue || viewValue; + * + * // Lookup user by username + * return $http.get('/api/users/' + value). + * then(function resolved() { + * //username exists, this means validation fails + * return $q.reject('exists'); + * }, function rejected() { + * //username does not exist, therefore this validation passes + * return true; + * }); + * }; + * ``` * - * The directive won't add duplicate classes if a particular class was already set. + * @property {Array.} $viewChangeListeners Array of functions to execute whenever + * a change to {@link ngModel.NgModelController#$viewValue `$viewValue`} has caused a change + * to {@link ngModel.NgModelController#$modelValue `$modelValue`}. + * It is called with no arguments, and its return value is ignored. + * This can be used in place of additional $watches against the model value. * - * When the expression changes, the previously added classes are removed and only then the - * new classes are added. + * @property {Object} $error An object hash with all failing validator ids as keys. + * @property {Object} $pending An object hash with all pending validator ids as keys. * - * @animations - * add - happens just before the class is applied to the element - * remove - happens just before the class is removed from the element + * @property {boolean} $untouched True if control has not lost focus yet. + * @property {boolean} $touched True if control has lost focus. + * @property {boolean} $pristine True if user has not interacted with the control yet. + * @property {boolean} $dirty True if user has already interacted with the control. + * @property {boolean} $valid True if there is no error. + * @property {boolean} $invalid True if at least one error on the control. + * @property {string} $name The name attribute of the control. * - * @element ANY - * @param {expression} ngClass {@link guide/expression Expression} to eval. The result - * of the evaluation can be a string representing space delimited class - * names, an array, or a map of class names to boolean values. In the case of a map, the - * names of the properties whose values are truthy will be added as css classes to the - * element. + * @description + * + * `NgModelController` provides API for the {@link ngModel `ngModel`} directive. + * The controller contains services for data-binding, validation, CSS updates, and value formatting + * and parsing. It purposefully does not contain any logic which deals with DOM rendering or + * listening to DOM events. + * Such DOM related logic should be provided by other directives which make use of + * `NgModelController` for data-binding to control elements. + * AngularJS provides this DOM logic for most {@link input `input`} elements. + * At the end of this page you can find a {@link ngModel.NgModelController#custom-control-example + * custom control example} that uses `ngModelController` to bind to `contenteditable` elements. + * + * @example + * ### Custom Control Example + * This example shows how to use `NgModelController` with a custom control to achieve + * data-binding. Notice how different directives (`contenteditable`, `ng-model`, and `required`) + * collaborate together to achieve the desired result. + * + * `contenteditable` is an HTML5 attribute, which tells the browser to let the element + * contents be edited in place by the user. * - * @example Example that demonstrates basic bindings via ngClass directive. - + * We are using the {@link ng.service:$sce $sce} service here and include the {@link ngSanitize $sanitize} + * module to automatically remove "bad" content like inline event listener (e.g. ``). + * However, as we are using `$sce` the model can still decide to provide unsafe content if it marks + * that content using the `$sce` service. + * + * + + [contenteditable] { + border: 1px solid black; + background-color: white; + min-height: 20px; + } + + .ng-invalid { + border: 1px solid red; + } + + + + angular.module('customControl', ['ngSanitize']). + directive('contenteditable', ['$sce', function($sce) { + return { + restrict: 'A', // only activate on element attribute + require: '?ngModel', // get a hold of NgModelController + link: function(scope, element, attrs, ngModel) { + if (!ngModel) return; // do nothing if no ng-model + + // Specify how UI should be updated + ngModel.$render = function() { + element.html($sce.getTrustedHtml(ngModel.$viewValue || '')); + }; + + // Listen for change events to enable binding + element.on('blur keyup change', function() { + scope.$evalAsync(read); + }); + read(); // initialize + + // Write data to the model + function read() { + var html = element.html(); + // When we clear the content editable the browser leaves a
    behind + // If strip-br attribute is provided then we strip this out + if (attrs.stripBr && html === '
    ') { + html = ''; + } + ngModel.$setViewValue(html); + } + } + }; + }]); +
    -

    Map Syntax Example

    - deleted (apply "strike" class)
    - important (apply "bold" class)
    - error (apply "red" class) -
    -

    Using String Syntax

    - +
    +
    Change me!
    + Required!
    -

    Using Array Syntax

    -
    -
    -
    + +
    - - .strike { - text-decoration: line-through; - } - .bold { - font-weight: bold; - } - .red { - color: red; - } + + it('should data-bind and become invalid', function() { + if (browser.params.browser === 'safari' || browser.params.browser === 'firefox') { + // SafariDriver can't handle contenteditable + // and Firefox driver can't clear contenteditables very well + return; + } + var contentEditable = element(by.css('[contenteditable]')); + var content = 'Change me!'; + + expect(contentEditable.getText()).toEqual(content); + + contentEditable.clear(); + contentEditable.sendKeys(protractor.Key.BACK_SPACE); + expect(contentEditable.getText()).toEqual(''); + expect(contentEditable.getAttribute('class')).toMatch(/ng-invalid-required/); + }); - - var ps = element.all(by.css('p')); + *
    + * + * + */ + NgModelController.$inject = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$q', '$interpolate']; + function NgModelController($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $q, $interpolate) { + this.$viewValue = Number.NaN; + this.$modelValue = Number.NaN; + this.$$rawModelValue = undefined; // stores the parsed modelValue / model set from scope regardless of validity. + this.$validators = {}; + this.$asyncValidators = {}; + this.$parsers = []; + this.$formatters = []; + this.$viewChangeListeners = []; + this.$untouched = true; + this.$touched = false; + this.$pristine = true; + this.$dirty = false; + this.$valid = true; + this.$invalid = false; + this.$error = {}; // keep invalid keys here + this.$$success = {}; // keep valid keys here + this.$pending = undefined; // keep pending keys here + this.$name = $interpolate($attr.name || '', false)($scope); + this.$$parentForm = nullFormCtrl; + this.$options = defaultModelOptions; + this.$$updateEvents = ''; + // Attach the correct context to the event handler function for updateOn + this.$$updateEventHandler = this.$$updateEventHandler.bind(this); + + this.$$parsedNgModel = $parse($attr.ngModel); + this.$$parsedNgModelAssign = this.$$parsedNgModel.assign; + this.$$ngModelGet = this.$$parsedNgModel; + this.$$ngModelSet = this.$$parsedNgModelAssign; + this.$$pendingDebounce = null; + this.$$parserValid = undefined; + + this.$$currentValidationRunId = 0; + + // https://github.com/angular/angular.js/issues/15833 + // Prevent `$$scope` from being iterated over by `copy` when NgModelController is deep watched + Object.defineProperty(this, '$$scope', {value: $scope}); + this.$$attr = $attr; + this.$$element = $element; + this.$$animate = $animate; + this.$$timeout = $timeout; + this.$$parse = $parse; + this.$$q = $q; + this.$$exceptionHandler = $exceptionHandler; + + setupValidity(this); + setupModelWatcher(this); + } - it('should let you toggle the class', function() { + NgModelController.prototype = { + $$initGetterSetters: function() { + if (this.$options.getOption('getterSetter')) { + var invokeModelGetter = this.$$parse(this.$$attr.ngModel + '()'), + invokeModelSetter = this.$$parse(this.$$attr.ngModel + '($$$p)'); - expect(ps.first().getAttribute('class')).not.toMatch(/bold/); - expect(ps.first().getAttribute('class')).not.toMatch(/red/); + this.$$ngModelGet = function($scope) { + var modelValue = this.$$parsedNgModel($scope); + if (isFunction(modelValue)) { + modelValue = invokeModelGetter($scope); + } + return modelValue; + }; + this.$$ngModelSet = function($scope, newValue) { + if (isFunction(this.$$parsedNgModel($scope))) { + invokeModelSetter($scope, {$$$p: newValue}); + } else { + this.$$parsedNgModelAssign($scope, newValue); + } + }; + } else if (!this.$$parsedNgModel.assign) { + throw ngModelMinErr('nonassign', 'Expression \'{0}\' is non-assignable. Element: {1}', + this.$$attr.ngModel, startingTag(this.$$element)); + } + }, - element(by.model('important')).click(); - expect(ps.first().getAttribute('class')).toMatch(/bold/); - element(by.model('error')).click(); - expect(ps.first().getAttribute('class')).toMatch(/red/); - }); + /** + * @ngdoc method + * @name ngModel.NgModelController#$render + * + * @description + * Called when the view needs to be updated. It is expected that the user of the ng-model + * directive will implement this method. + * + * The `$render()` method is invoked in the following situations: + * + * * `$rollbackViewValue()` is called. If we are rolling back the view value to the last + * committed value then `$render()` is called to update the input control. + * * The value referenced by `ng-model` is changed programmatically and both the `$modelValue` and + * the `$viewValue` are different from last time. + * + * Since `ng-model` does not do a deep watch, `$render()` is only invoked if the values of + * `$modelValue` and `$viewValue` are actually different from their previous values. If `$modelValue` + * or `$viewValue` are objects (rather than a string or number) then `$render()` will not be + * invoked if you only change a property on the objects. + */ + $render: noop, - it('should let you toggle string example', function() { - expect(ps.get(1).getAttribute('class')).toBe(''); - element(by.model('style')).clear(); - element(by.model('style')).sendKeys('red'); - expect(ps.get(1).getAttribute('class')).toBe('red'); - }); + /** + * @ngdoc method + * @name ngModel.NgModelController#$isEmpty + * + * @description + * This is called when we need to determine if the value of an input is empty. + * + * For instance, the required directive does this to work out if the input has data or not. + * + * The default `$isEmpty` function checks whether the value is `undefined`, `''`, `null` or `NaN`. + * + * You can override this for input directives whose concept of being empty is different from the + * default. The `checkboxInputType` directive does this because in its case a value of `false` + * implies empty. + * + * @param {*} value The value of the input to check for emptiness. + * @returns {boolean} True if `value` is "empty". + */ + $isEmpty: function(value) { + // eslint-disable-next-line no-self-compare + return isUndefined(value) || value === '' || value === null || value !== value; + }, - it('array example should have 3 classes', function() { - expect(ps.last().getAttribute('class')).toBe(''); - element(by.model('style1')).sendKeys('bold'); - element(by.model('style2')).sendKeys('strike'); - element(by.model('style3')).sendKeys('red'); - expect(ps.last().getAttribute('class')).toBe('bold strike red'); - }); - -
    + $$updateEmptyClasses: function(value) { + if (this.$isEmpty(value)) { + this.$$animate.removeClass(this.$$element, NOT_EMPTY_CLASS); + this.$$animate.addClass(this.$$element, EMPTY_CLASS); + } else { + this.$$animate.removeClass(this.$$element, EMPTY_CLASS); + this.$$animate.addClass(this.$$element, NOT_EMPTY_CLASS); + } + }, - ## Animations + /** + * @ngdoc method + * @name ngModel.NgModelController#$setPristine + * + * @description + * Sets the control to its pristine state. + * + * This method can be called to remove the `ng-dirty` class and set the control to its pristine + * state (`ng-pristine` class). A model is considered to be pristine when the control + * has not been changed from when first compiled. + */ + $setPristine: function() { + this.$dirty = false; + this.$pristine = true; + this.$$animate.removeClass(this.$$element, DIRTY_CLASS); + this.$$animate.addClass(this.$$element, PRISTINE_CLASS); + }, - The example below demonstrates how to perform animations using ngClass. + /** + * @ngdoc method + * @name ngModel.NgModelController#$setDirty + * + * @description + * Sets the control to its dirty state. + * + * This method can be called to remove the `ng-pristine` class and set the control to its dirty + * state (`ng-dirty` class). A model is considered to be dirty when the control has been changed + * from when first compiled. + */ + $setDirty: function() { + this.$dirty = true; + this.$pristine = false; + this.$$animate.removeClass(this.$$element, PRISTINE_CLASS); + this.$$animate.addClass(this.$$element, DIRTY_CLASS); + this.$$parentForm.$setDirty(); + }, - - - - -
    - Sample Text -
    - - .base-class { - -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; - transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; - } + /** + * @ngdoc method + * @name ngModel.NgModelController#$setUntouched + * + * @description + * Sets the control to its untouched state. + * + * This method can be called to remove the `ng-touched` class and set the control to its + * untouched state (`ng-untouched` class). Upon compilation, a model is set as untouched + * by default, however this function can be used to restore that state if the model has + * already been touched by the user. + */ + $setUntouched: function() { + this.$touched = false; + this.$untouched = true; + this.$$animate.setClass(this.$$element, UNTOUCHED_CLASS, TOUCHED_CLASS); + }, - .base-class.my-class { - color: red; - font-size:3em; - } - - - it('should check ng-class', function() { - expect(element(by.css('.base-class')).getAttribute('class')).not. - toMatch(/my-class/); + /** + * @ngdoc method + * @name ngModel.NgModelController#$setTouched + * + * @description + * Sets the control to its touched state. + * + * This method can be called to remove the `ng-untouched` class and set the control to its + * touched state (`ng-touched` class). A model is considered to be touched when the user has + * first focused the control element and then shifted focus away from the control (blur event). + */ + $setTouched: function() { + this.$touched = true; + this.$untouched = false; + this.$$animate.setClass(this.$$element, TOUCHED_CLASS, UNTOUCHED_CLASS); + }, - element(by.id('setbtn')).click(); + /** + * @ngdoc method + * @name ngModel.NgModelController#$rollbackViewValue + * + * @description + * Cancel an update and reset the input element's value to prevent an update to the `$modelValue`, + * which may be caused by a pending debounced event or because the input is waiting for some + * future event. + * + * If you have an input that uses `ng-model-options` to set up debounced updates or updates that + * depend on special events such as `blur`, there can be a period when the `$viewValue` is out of + * sync with the ngModel's `$modelValue`. + * + * In this case, you can use `$rollbackViewValue()` to manually cancel the debounced / future update + * and reset the input to the last committed view value. + * + * It is also possible that you run into difficulties if you try to update the ngModel's `$modelValue` + * programmatically before these debounced/future events have resolved/occurred, because AngularJS's + * dirty checking mechanism is not able to tell whether the model has actually changed or not. + * + * The `$rollbackViewValue()` method should be called before programmatically changing the model of an + * input which may have such events pending. This is important in order to make sure that the + * input field will be updated with the new model value and any pending operations are cancelled. + * + * @example + * + * + * angular.module('cancel-update-example', []) + * + * .controller('CancelUpdateController', ['$scope', function($scope) { + * $scope.model = {value1: '', value2: ''}; + * + * $scope.setEmpty = function(e, value, rollback) { + * if (e.keyCode === 27) { + * e.preventDefault(); + * if (rollback) { + * $scope.myForm[value].$rollbackViewValue(); + * } + * $scope.model[value] = ''; + * } + * }; + * }]); + * + * + *
    + *

    Both of these inputs are only updated if they are blurred. Hitting escape should + * empty them. Follow these steps and observe the difference:

    + *
      + *
    1. Type something in the input. You will see that the model is not yet updated
    2. + *
    3. Press the Escape key. + *
        + *
      1. In the first example, nothing happens, because the model is already '', and no + * update is detected. If you blur the input, the model will be set to the current view. + *
      2. + *
      3. In the second example, the pending update is cancelled, and the input is set back + * to the last committed view value (''). Blurring the input does nothing. + *
      4. + *
      + *
    4. + *
    + * + *
    + *
    + *

    Without $rollbackViewValue():

    + * + * value1: "{{ model.value1 }}" + *
    + * + *
    + *

    With $rollbackViewValue():

    + * + * value2: "{{ model.value2 }}" + *
    + *
    + *
    + *
    + + div { + display: table-cell; + } + div:nth-child(1) { + padding-right: 30px; + } - expect(element(by.css('.base-class')).getAttribute('class')). - toMatch(/my-class/); + + *
    + */ + $rollbackViewValue: function() { + this.$$timeout.cancel(this.$$pendingDebounce); + this.$viewValue = this.$$lastCommittedViewValue; + this.$render(); + }, - element(by.id('clearbtn')).click(); + /** + * @ngdoc method + * @name ngModel.NgModelController#$validate + * + * @description + * Runs each of the registered validators (first synchronous validators and then + * asynchronous validators). + * If the validity changes to invalid, the model will be set to `undefined`, + * unless {@link ngModelOptions `ngModelOptions.allowInvalid`} is `true`. + * If the validity changes to valid, it will set the model to the last available valid + * `$modelValue`, i.e. either the last parsed value or the last value set from the scope. + */ + $validate: function() { + // ignore $validate before model is initialized + if (isNumberNaN(this.$modelValue)) { + return; + } - expect(element(by.css('.base-class')).getAttribute('class')).not. - toMatch(/my-class/); - }); -
    -
    + var viewValue = this.$$lastCommittedViewValue; + // Note: we use the $$rawModelValue as $modelValue might have been + // set to undefined during a view -> model update that found validation + // errors. We can't parse the view here, since that could change + // the model although neither viewValue nor the model on the scope changed + var modelValue = this.$$rawModelValue; + + var prevValid = this.$valid; + var prevModelValue = this.$modelValue; + + var allowInvalid = this.$options.getOption('allowInvalid'); + + var that = this; + this.$$runValidators(modelValue, viewValue, function(allValid) { + // If there was no change in validity, don't update the model + // This prevents changing an invalid modelValue to undefined + if (!allowInvalid && prevValid !== allValid) { + // Note: Don't check this.$valid here, as we could have + // external validators (e.g. calculated on the server), + // that just call $setValidity and need the model value + // to calculate their validity. + that.$modelValue = allValid ? modelValue : undefined; + + if (that.$modelValue !== prevModelValue) { + that.$$writeModelToScope(); + } + } + }); + }, + $$runValidators: function(modelValue, viewValue, doneCallback) { + this.$$currentValidationRunId++; + var localValidationRunId = this.$$currentValidationRunId; + var that = this; - ## ngClass and pre-existing CSS3 Transitions/Animations - The ngClass directive still supports CSS3 Transitions/Animations even if they do not follow the ngAnimate CSS naming structure. - Upon animation ngAnimate will apply supplementary CSS classes to track the start and end of an animation, but this will not hinder - any pre-existing CSS transitions already on the element. To get an idea of what happens during a class-based animation, be sure - to view the step by step details of {@link ngAnimate.$animate#addclass $animate.addClass} and - {@link ngAnimate.$animate#removeclass $animate.removeClass}. - */ - var ngClassDirective = classDirective('', true); + // check parser error + if (!processParseErrors()) { + validationDone(false); + return; + } + if (!processSyncValidators()) { + validationDone(false); + return; + } + processAsyncValidators(); - /** - * @ngdoc directive - * @name ngClassOdd - * @restrict AC - * - * @description - * The `ngClassOdd` and `ngClassEven` directives work exactly as - * {@link ng.directive:ngClass ngClass}, except they work in - * conjunction with `ngRepeat` and take effect only on odd (even) rows. - * - * This directive can be applied only within the scope of an - * {@link ng.directive:ngRepeat ngRepeat}. - * - * @element ANY - * @param {expression} ngClassOdd {@link guide/expression Expression} to eval. The result - * of the evaluation can be a string representing space delimited class names or an array. - * - * @example - - -
      -
    1. - - {{name}} - -
    2. -
    -
    - - .odd { - color: red; - } - .even { - color: blue; - } - - - it('should check ng-class-odd and ng-class-even', function() { - expect(element(by.repeater('name in names').row(0).column('name')).getAttribute('class')). - toMatch(/odd/); - expect(element(by.repeater('name in names').row(1).column('name')).getAttribute('class')). - toMatch(/even/); - }); - -
    - */ - var ngClassOddDirective = classDirective('Odd', 0); + function processParseErrors() { + var errorKey = that.$$parserName || 'parse'; + if (isUndefined(that.$$parserValid)) { + setValidity(errorKey, null); + } else { + if (!that.$$parserValid) { + forEach(that.$validators, function(v, name) { + setValidity(name, null); + }); + forEach(that.$asyncValidators, function(v, name) { + setValidity(name, null); + }); + } + // Set the parse error last, to prevent unsetting it, should a $validators key == parserName + setValidity(errorKey, that.$$parserValid); + return that.$$parserValid; + } + return true; + } - /** - * @ngdoc directive - * @name ngClassEven - * @restrict AC - * - * @description - * The `ngClassOdd` and `ngClassEven` directives work exactly as - * {@link ng.directive:ngClass ngClass}, except they work in - * conjunction with `ngRepeat` and take effect only on odd (even) rows. - * - * This directive can be applied only within the scope of an - * {@link ng.directive:ngRepeat ngRepeat}. - * - * @element ANY - * @param {expression} ngClassEven {@link guide/expression Expression} to eval. The - * result of the evaluation can be a string representing space delimited class names or an array. - * - * @example - - -
      -
    1. - - {{name}}       - -
    2. -
    -
    - - .odd { - color: red; - } - .even { - color: blue; - } - - - it('should check ng-class-odd and ng-class-even', function() { - expect(element(by.repeater('name in names').row(0).column('name')).getAttribute('class')). - toMatch(/odd/); - expect(element(by.repeater('name in names').row(1).column('name')).getAttribute('class')). - toMatch(/even/); - }); - -
    - */ - var ngClassEvenDirective = classDirective('Even', 1); + function processSyncValidators() { + var syncValidatorsValid = true; + forEach(that.$validators, function(validator, name) { + var result = Boolean(validator(modelValue, viewValue)); + syncValidatorsValid = syncValidatorsValid && result; + setValidity(name, result); + }); + if (!syncValidatorsValid) { + forEach(that.$asyncValidators, function(v, name) { + setValidity(name, null); + }); + return false; + } + return true; + } - /** - * @ngdoc directive - * @name ngCloak - * @restrict AC - * - * @description - * The `ngCloak` directive is used to prevent the Angular html template from being briefly - * displayed by the browser in its raw (uncompiled) form while your application is loading. Use this - * directive to avoid the undesirable flicker effect caused by the html template display. - * - * The directive can be applied to the `` element, but the preferred usage is to apply - * multiple `ngCloak` directives to small portions of the page to permit progressive rendering - * of the browser view. - * - * `ngCloak` works in cooperation with the following css rule embedded within `angular.js` and - * `angular.min.js`. - * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}). - * - * ```css - * [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak { - * display: none !important; - * } - * ``` - * - * When this css rule is loaded by the browser, all html elements (including their children) that - * are tagged with the `ngCloak` directive are hidden. When Angular encounters this directive - * during the compilation of the template it deletes the `ngCloak` element attribute, making - * the compiled element visible. - * - * For the best result, the `angular.js` script must be loaded in the head section of the html - * document; alternatively, the css rule above must be included in the external stylesheet of the - * application. - * - * Legacy browsers, like IE7, do not provide attribute selector support (added in CSS 2.1) so they - * cannot match the `[ng\:cloak]` selector. To work around this limitation, you must add the css - * class `ng-cloak` in addition to the `ngCloak` directive as shown in the example below. - * - * @element ANY - * - * @example - - -
    {{ 'hello' }}
    -
    {{ 'hello IE7' }}
    -
    - - it('should remove the template directive and css class', function() { - expect($('#template1').getAttribute('ng-cloak')). - toBeNull(); - expect($('#template2').getAttribute('ng-cloak')). - toBeNull(); - }); - -
    - * - */ - var ngCloakDirective = ngDirective({ - compile: function(element, attr) { - attr.$set('ngCloak', undefined); - element.removeClass('ng-cloak'); - } - }); + function processAsyncValidators() { + var validatorPromises = []; + var allValid = true; + forEach(that.$asyncValidators, function(validator, name) { + var promise = validator(modelValue, viewValue); + if (!isPromiseLike(promise)) { + throw ngModelMinErr('nopromise', + 'Expected asynchronous validator to return a promise but got \'{0}\' instead.', promise); + } + setValidity(name, undefined); + validatorPromises.push(promise.then(function() { + setValidity(name, true); + }, function() { + allValid = false; + setValidity(name, false); + })); + }); + if (!validatorPromises.length) { + validationDone(true); + } else { + that.$$q.all(validatorPromises).then(function() { + validationDone(allValid); + }, noop); + } + } - /** - * @ngdoc directive - * @name ngController - * - * @description - * The `ngController` directive attaches a controller class to the view. This is a key aspect of how angular - * supports the principles behind the Model-View-Controller design pattern. - * - * MVC components in angular: - * - * * Model — The Model is scope properties; scopes are attached to the DOM where scope properties - * are accessed through bindings. - * * View — The template (HTML with data bindings) that is rendered into the View. - * * Controller — The `ngController` directive specifies a Controller class; the class contains business - * logic behind the application to decorate the scope with functions and values - * - * Note that you can also attach controllers to the DOM by declaring it in a route definition - * via the {@link ngRoute.$route $route} service. A common mistake is to declare the controller - * again using `ng-controller` in the template itself. This will cause the controller to be attached - * and executed twice. - * - * @element ANY - * @scope - * @param {expression} ngController Name of a globally accessible constructor function or an - * {@link guide/expression expression} that on the current scope evaluates to a - * constructor function. The controller instance can be published into a scope property - * by specifying `as propertyName`. - * - * @example - * Here is a simple form for editing user contact information. Adding, removing, clearing, and - * greeting are methods declared on the controller (see source tab). These methods can - * easily be called from the angular markup. Notice that the scope becomes the `this` for the - * controller's instance. This allows for easy access to the view data from the controller. Also - * notice that any changes to the data are automatically reflected in the View without the need - * for a manual update. The example is shown in two different declaration styles you may use - * according to preference. - - - -
    - Name: - [ greet ]
    - Contact: -
      -
    • - - - [ clear - | X ] -
    • -
    • [ add ]
    • -
    -
    -
    - - it('should check controller as', function() { - var container = element(by.id('ctrl-as-exmpl')); + /** + * @ngdoc method + * @name ngModel.NgModelController#$commitViewValue + * + * @description + * Commit a pending update to the `$modelValue`. + * + * Updates may be pending by a debounced event or because the input is waiting for a some future + * event defined in `ng-model-options`. this method is rarely needed as `NgModelController` + * usually handles calling this in response to input events. + */ + $commitViewValue: function() { + var viewValue = this.$viewValue; - expect(container.findElement(by.model('settings.name')) - .getAttribute('value')).toBe('John Smith'); + this.$$timeout.cancel(this.$$pendingDebounce); - var firstRepeat = - container.findElement(by.repeater('contact in settings.contacts').row(0)); - var secondRepeat = - container.findElement(by.repeater('contact in settings.contacts').row(1)); + // If the view value has not changed then we should just exit, except in the case where there is + // a native validator on the element. In this case the validation state may have changed even though + // the viewValue has stayed empty. + if (this.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !this.$$hasNativeValidators)) { + return; + } + this.$$updateEmptyClasses(viewValue); + this.$$lastCommittedViewValue = viewValue; - expect(firstRepeat.findElement(by.model('contact.value')).getAttribute('value')) - .toBe('408 555 1212'); - expect(secondRepeat.findElement(by.model('contact.value')).getAttribute('value')) - .toBe('john.smith@example.org'); + // change to dirty + if (this.$pristine) { + this.$setDirty(); + } + this.$$parseAndValidate(); + }, - firstRepeat.findElement(by.linkText('clear')).click(); + $$parseAndValidate: function() { + var viewValue = this.$$lastCommittedViewValue; + var modelValue = viewValue; + var that = this; - expect(firstRepeat.findElement(by.model('contact.value')).getAttribute('value')) - .toBe(''); + this.$$parserValid = isUndefined(modelValue) ? undefined : true; - container.findElement(by.linkText('add')).click(); + if (this.$$parserValid) { + for (var i = 0; i < this.$parsers.length; i++) { + modelValue = this.$parsers[i](modelValue); + if (isUndefined(modelValue)) { + this.$$parserValid = false; + break; + } + } + } + if (isNumberNaN(this.$modelValue)) { + // this.$modelValue has not been touched yet... + this.$modelValue = this.$$ngModelGet(this.$$scope); + } + var prevModelValue = this.$modelValue; + var allowInvalid = this.$options.getOption('allowInvalid'); + this.$$rawModelValue = modelValue; - expect(container.findElement(by.repeater('contact in settings.contacts').row(2)) - .findElement(by.model('contact.value')) - .getAttribute('value')) - .toBe('yourname@example.org'); - }); - -
    - - - -
    - Name: - [ greet ]
    - Contact: -
      -
    • - - - [ clear - | X ] -
    • -
    • [ add ]
    • -
    -
    -
    - - it('should check controller', function() { - var container = element(by.id('ctrl-exmpl')); + $$writeModelToScope: function() { + this.$$ngModelSet(this.$$scope, this.$modelValue); + forEach(this.$viewChangeListeners, function(listener) { + try { + listener(); + } catch (e) { + // eslint-disable-next-line no-invalid-this + this.$$exceptionHandler(e); + } + }, this); + }, - expect(container.findElement(by.model('name')) - .getAttribute('value')).toBe('John Smith'); + /** + * @ngdoc method + * @name ngModel.NgModelController#$setViewValue + * + * @description + * Update the view value. + * + * This method should be called when a control wants to change the view value; typically, + * this is done from within a DOM event handler. For example, the {@link ng.directive:input input} + * directive calls it when the value of the input changes and {@link ng.directive:select select} + * calls it when an option is selected. + * + * When `$setViewValue` is called, the new `value` will be staged for committing through the `$parsers` + * and `$validators` pipelines. If there are no special {@link ngModelOptions} specified then the staged + * value is sent directly for processing through the `$parsers` pipeline. After this, the `$validators` and + * `$asyncValidators` are called and the value is applied to `$modelValue`. + * Finally, the value is set to the **expression** specified in the `ng-model` attribute and + * all the registered change listeners, in the `$viewChangeListeners` list are called. + * + * In case the {@link ng.directive:ngModelOptions ngModelOptions} directive is used with `updateOn` + * and the `default` trigger is not listed, all those actions will remain pending until one of the + * `updateOn` events is triggered on the DOM element. + * All these actions will be debounced if the {@link ng.directive:ngModelOptions ngModelOptions} + * directive is used with a custom debounce for this particular event. + * Note that a `$digest` is only triggered once the `updateOn` events are fired, or if `debounce` + * is specified, once the timer runs out. + * + * When used with standard inputs, the view value will always be a string (which is in some cases + * parsed into another type, such as a `Date` object for `input[date]`.) + * However, custom controls might also pass objects to this method. In this case, we should make + * a copy of the object before passing it to `$setViewValue`. This is because `ngModel` does not + * perform a deep watch of objects, it only looks for a change of identity. If you only change + * the property of the object then ngModel will not realize that the object has changed and + * will not invoke the `$parsers` and `$validators` pipelines. For this reason, you should + * not change properties of the copy once it has been passed to `$setViewValue`. + * Otherwise you may cause the model value on the scope to change incorrectly. + * + *
    + * In any case, the value passed to the method should always reflect the current value + * of the control. For example, if you are calling `$setViewValue` for an input element, + * you should pass the input DOM value. Otherwise, the control and the scope model become + * out of sync. It's also important to note that `$setViewValue` does not call `$render` or change + * the control's DOM value in any way. If we want to change the control's DOM value + * programmatically, we should update the `ngModel` scope expression. Its new value will be + * picked up by the model controller, which will run it through the `$formatters`, `$render` it + * to update the DOM, and finally call `$validate` on it. + *
    + * + * @param {*} value value from the view. + * @param {string} trigger Event that triggered the update. + */ + $setViewValue: function(value, trigger) { + this.$viewValue = value; + if (this.$options.getOption('updateOnDefault')) { + this.$$debounceViewValueCommit(trigger); + } + }, - var firstRepeat = - container.findElement(by.repeater('contact in contacts').row(0)); - var secondRepeat = - container.findElement(by.repeater('contact in contacts').row(1)); + $$debounceViewValueCommit: function(trigger) { + var debounceDelay = this.$options.getOption('debounce'); - expect(firstRepeat.findElement(by.model('contact.value')).getAttribute('value')) - .toBe('408 555 1212'); - expect(secondRepeat.findElement(by.model('contact.value')).getAttribute('value')) - .toBe('john.smith@example.org'); + if (isNumber(debounceDelay[trigger])) { + debounceDelay = debounceDelay[trigger]; + } else if (isNumber(debounceDelay['default'])) { + debounceDelay = debounceDelay['default']; + } - firstRepeat.findElement(by.linkText('clear')).click(); + this.$$timeout.cancel(this.$$pendingDebounce); + var that = this; + if (debounceDelay > 0) { // this fails if debounceDelay is an object + this.$$pendingDebounce = this.$$timeout(function() { + that.$commitViewValue(); + }, debounceDelay); + } else if (this.$$scope.$root.$$phase) { + this.$commitViewValue(); + } else { + this.$$scope.$apply(function() { + that.$commitViewValue(); + }); + } + }, - expect(firstRepeat.findElement(by.model('contact.value')).getAttribute('value')) - .toBe(''); + /** + * @ngdoc method + * + * @name ngModel.NgModelController#$overrideModelOptions + * + * @description + * + * Override the current model options settings programmatically. + * + * The previous `ModelOptions` value will not be modified. Instead, a + * new `ModelOptions` object will inherit from the previous one overriding + * or inheriting settings that are defined in the given parameter. + * + * See {@link ngModelOptions} for information about what options can be specified + * and how model option inheritance works. + * + *
    + * **Note:** this function only affects the options set on the `ngModelController`, + * and not the options on the {@link ngModelOptions} directive from which they might have been + * obtained initially. + *
    + * + *
    + * **Note:** it is not possible to override the `getterSetter` option. + *
    + * + * @param {Object} options a hash of settings to override the previous options + * + */ + $overrideModelOptions: function(options) { + this.$options = this.$options.createChild(options); + this.$$setUpdateOnEvents(); + }, - container.findElement(by.linkText('add')).click(); + /** + * @ngdoc method + * + * @name ngModel.NgModelController#$processModelValue - expect(container.findElement(by.repeater('contact in contacts').row(2)) - .findElement(by.model('contact.value')) - .getAttribute('value')) - .toBe('yourname@example.org'); - }); -
    -
    + * @description + * + * Runs the model -> view pipeline on the current + * {@link ngModel.NgModelController#$modelValue $modelValue}. + * + * The following actions are performed by this method: + * + * - the `$modelValue` is run through the {@link ngModel.NgModelController#$formatters $formatters} + * and the result is set to the {@link ngModel.NgModelController#$viewValue $viewValue} + * - the `ng-empty` or `ng-not-empty` class is set on the element + * - if the `$viewValue` has changed: + * - {@link ngModel.NgModelController#$render $render} is called on the control + * - the {@link ngModel.NgModelController#$validators $validators} are run and + * the validation status is set. + * + * This method is called by ngModel internally when the bound scope value changes. + * Application developers usually do not have to call this function themselves. + * + * This function can be used when the `$viewValue` or the rendered DOM value are not correctly + * formatted and the `$modelValue` must be run through the `$formatters` again. + * + * @example + * Consider a text input with an autocomplete list (for fruit), where the items are + * objects with a name and an id. + * A user enters `ap` and then selects `Apricot` from the list. + * Based on this, the autocomplete widget will call `$setViewValue({name: 'Apricot', id: 443})`, + * but the rendered value will still be `ap`. + * The widget can then call `ctrl.$processModelValue()` to run the model -> view + * pipeline again, which formats the object to the string `Apricot`, + * then updates the `$viewValue`, and finally renders it in the DOM. + * + * + +
    +
    + Search Fruit: + +
    +
    + Model:
    +
    {{selectedFruit | json}}
    +
    +
    +
    + + angular.module('inputExample', []) + .controller('inputController', function($scope) { + $scope.items = [ + {name: 'Apricot', id: 443}, + {name: 'Clementine', id: 972}, + {name: 'Durian', id: 169}, + {name: 'Jackfruit', id: 982}, + {name: 'Strawberry', id: 863} + ]; + }) + .component('basicAutocomplete', { + bindings: { + items: '<', + onSelect: '&' + }, + templateUrl: 'autocomplete.html', + controller: function($element, $scope) { + var that = this; + var ngModel; + + that.$postLink = function() { + ngModel = $element.find('input').controller('ngModel'); + + ngModel.$formatters.push(function(value) { + return (value && value.name) || value; + }); + + ngModel.$parsers.push(function(value) { + var match = value; + for (var i = 0; i < that.items.length; i++) { + if (that.items[i].name === value) { + match = that.items[i]; + break; + } + } + + return match; + }); + }; + + that.selectItem = function(item) { + ngModel.$setViewValue(item); + ngModel.$processModelValue(); + that.onSelect({item: item}); + }; + } + }); + + +
    + +
      +
    • + +
    • +
    +
    +
    + *
    + * + */ + $processModelValue: function() { + var viewValue = this.$$format(); + + if (this.$viewValue !== viewValue) { + this.$$updateEmptyClasses(viewValue); + this.$viewValue = this.$$lastCommittedViewValue = viewValue; + this.$render(); + // It is possible that model and view value have been updated during render + this.$$runValidators(this.$modelValue, this.$viewValue, noop); + } + }, + + /** + * This method is called internally to run the $formatters on the $modelValue + */ + $$format: function() { + var formatters = this.$formatters, + idx = formatters.length; + + var viewValue = this.$modelValue; + while (idx--) { + viewValue = formatters[idx](viewValue); + } + + return viewValue; + }, + + /** + * This method is called internally when the bound scope value changes. + */ + $$setModelValue: function(modelValue) { + this.$modelValue = this.$$rawModelValue = modelValue; + this.$$parserValid = undefined; + this.$processModelValue(); + }, + + $$setUpdateOnEvents: function() { + if (this.$$updateEvents) { + this.$$element.off(this.$$updateEvents, this.$$updateEventHandler); + } + + this.$$updateEvents = this.$options.getOption('updateOn'); + if (this.$$updateEvents) { + this.$$element.on(this.$$updateEvents, this.$$updateEventHandler); + } + }, + + $$updateEventHandler: function(ev) { + this.$$debounceViewValueCommit(ev && ev.type); + } + }; + + function setupModelWatcher(ctrl) { + // model -> value + // Note: we cannot use a normal scope.$watch as we want to detect the following: + // 1. scope value is 'a' + // 2. user enters 'b' + // 3. ng-change kicks in and reverts scope value to 'a' + // -> scope value did not change since the last digest as + // ng-change executes in apply phase + // 4. view should be changed back to 'a' + ctrl.$$scope.$watch(function ngModelWatch(scope) { + var modelValue = ctrl.$$ngModelGet(scope); + + // if scope model value and ngModel value are out of sync + // This cannot be moved to the action function, because it would not catch the + // case where the model is changed in the ngChange function or the model setter + if (modelValue !== ctrl.$modelValue && + // checks for NaN is needed to allow setting the model to NaN when there's an asyncValidator + // eslint-disable-next-line no-self-compare + (ctrl.$modelValue === ctrl.$modelValue || modelValue === modelValue) + ) { + ctrl.$$setModelValue(modelValue); + } + + return modelValue; + }); + } + /** + * @ngdoc method + * @name ngModel.NgModelController#$setValidity + * + * @description + * Change the validity state, and notify the form. + * + * This method can be called within $parsers/$formatters or a custom validation implementation. + * However, in most cases it should be sufficient to use the `ngModel.$validators` and + * `ngModel.$asyncValidators` collections which will call `$setValidity` automatically. + * + * @param {string} validationErrorKey Name of the validator. The `validationErrorKey` will be assigned + * to either `$error[validationErrorKey]` or `$pending[validationErrorKey]` + * (for unfulfilled `$asyncValidators`), so that it is available for data-binding. + * The `validationErrorKey` should be in camelCase and will get converted into dash-case + * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error` + * classes and can be bound to as `{{ someForm.someControl.$error.myError }}`. + * @param {boolean} isValid Whether the current state is valid (true), invalid (false), pending (undefined), + * or skipped (null). Pending is used for unfulfilled `$asyncValidators`. + * Skipped is used by AngularJS when validators do not run because of parse errors and + * when `$asyncValidators` do not run because any of the `$validators` failed. */ - var ngControllerDirective = [function() { - return { - scope: true, - controller: '@', - priority: 500 - }; - }]; + addSetValidityMethod({ + clazz: NgModelController, + set: function(object, property) { + object[property] = true; + }, + unset: function(object, property) { + delete object[property]; + } + }); + /** * @ngdoc directive - * @name ngCsp + * @name ngModel + * @restrict A + * @priority 1 + * @param {expression} ngModel assignable {@link guide/expression Expression} to bind to. * - * @element html * @description - * Enables [CSP (Content Security Policy)](https://developer.mozilla.org/en/Security/CSP) support. + * The `ngModel` directive binds an `input`,`select`, `textarea` (or custom form control) to a + * property on the scope using {@link ngModel.NgModelController NgModelController}, + * which is created and exposed by this directive. * - * This is necessary when developing things like Google Chrome Extensions. + * `ngModel` is responsible for: * - * CSP forbids apps to use `eval` or `Function(string)` generated functions (among other things). - * For us to be compatible, we just need to implement the "getterFn" in $parse without violating - * any of these restrictions. + * - Binding the view into the model, which other directives such as `input`, `textarea` or `select` + * require. + * - Providing validation behavior (i.e. required, number, email, url). + * - Keeping the state of the control (valid/invalid, dirty/pristine, touched/untouched, validation errors). + * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`, `ng-touched`, + * `ng-untouched`, `ng-empty`, `ng-not-empty`) including animations. + * - Registering the control with its parent {@link ng.directive:form form}. * - * AngularJS uses `Function(string)` generated functions as a speed optimization. Applying the `ngCsp` - * directive will cause Angular to use CSP compatibility mode. When this mode is on AngularJS will - * evaluate all expressions up to 30% slower than in non-CSP mode, but no security violations will - * be raised. + * Note: `ngModel` will try to bind to the property given by evaluating the expression on the + * current scope. If the property doesn't already exist on this scope, it will be created + * implicitly and added to the scope. * - * CSP forbids JavaScript to inline stylesheet rules. In non CSP mode Angular automatically - * includes some CSS rules (e.g. {@link ng.directive:ngCloak ngCloak}). - * To make those directives work in CSP mode, include the `angular-csp.css` manually. + * For best practices on using `ngModel`, see: * - * In order to use this feature put the `ngCsp` directive on the root element of the application. + * - [Understanding Scopes](https://github.com/angular/angular.js/wiki/Understanding-Scopes) * - * *Note: This directive is only available in the `ng-csp` and `data-ng-csp` attribute form.* + * For basic examples, how to use `ngModel`, see: * - * @example - * This example shows how to apply the `ngCsp` directive to the `html` tag. - ```html - - - ... - ... - - ``` - */ - -// ngCsp is not implemented as a proper directive any more, because we need it be processed while we bootstrap -// the system (before $parse is instantiated), for this reason we just have a csp() fn that looks for ng-csp attribute -// anywhere in the current doc - - /** - * @ngdoc directive - * @name ngClick + * - {@link ng.directive:input input} + * - {@link input[text] text} + * - {@link input[checkbox] checkbox} + * - {@link input[radio] radio} + * - {@link input[number] number} + * - {@link input[email] email} + * - {@link input[url] url} + * - {@link input[date] date} + * - {@link input[datetime-local] datetime-local} + * - {@link input[time] time} + * - {@link input[month] month} + * - {@link input[week] week} + * - {@link ng.directive:select select} + * - {@link ng.directive:textarea textarea} * - * @description - * The ngClick directive allows you to specify custom behavior when - * an element is clicked. + * ## Complex Models (objects or collections) * - * @element ANY - * @priority 0 - * @param {expression} ngClick {@link guide/expression Expression} to evaluate upon - * click. ({@link guide/expression#-event- Event object is available as `$event`}) + * By default, `ngModel` watches the model by reference, not value. This is important to know when + * binding inputs to models that are objects (e.g. `Date`) or collections (e.g. arrays). If only properties of the + * object or collection change, `ngModel` will not be notified and so the input will not be re-rendered. * - * @example - - - - count: {{count}} - - - it('should check ng-click', function() { - expect(element(by.binding('count')).getText()).toMatch('0'); - element(by.css('button')).click(); - expect(element(by.binding('count')).getText()).toMatch('1'); - }); - - - */ - /* - * A directive that allows creation of custom onclick handlers that are defined as angular - * expressions and are compiled and executed within the current scope. + * The model must be assigned an entirely new object or collection before a re-rendering will occur. * - * Events that are handled via these handler are always configured not to propagate further. - */ - var ngEventDirectives = {}; - forEach( - 'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '), - function(name) { - var directiveName = directiveNormalize('ng-' + name); - ngEventDirectives[directiveName] = ['$parse', function($parse) { - return { - compile: function($element, attr) { - var fn = $parse(attr[directiveName]); - return function(scope, element, attr) { - element.on(lowercase(name), function(event) { - scope.$apply(function() { - fn(scope, {$event:event}); - }); - }); - }; - } - }; - }]; - } - ); - - /** - * @ngdoc directive - * @name ngDblclick + * Some directives have options that will cause them to use a custom `$watchCollection` on the model expression + * - for example, `ngOptions` will do so when a `track by` clause is included in the comprehension expression or + * if the select is given the `multiple` attribute. * - * @description - * The `ngDblclick` directive allows you to specify custom behavior on a dblclick event. + * The `$watchCollection()` method only does a shallow comparison, meaning that changing properties deeper than the + * first level of the object (or only changing the properties of an item in the collection if it's an array) will still + * not trigger a re-rendering of the model. * - * @element ANY - * @priority 0 - * @param {expression} ngDblclick {@link guide/expression Expression} to evaluate upon - * a dblclick. (The Event object is available as `$event`) + * ## CSS classes + * The following CSS classes are added and removed on the associated input/select/textarea element + * depending on the validity of the model. * - * @example - - - - count: {{count}} - - - */ - - - /** - * @ngdoc directive - * @name ngMousedown + * - `ng-valid`: the model is valid + * - `ng-invalid`: the model is invalid + * - `ng-valid-[key]`: for each valid key added by `$setValidity` + * - `ng-invalid-[key]`: for each invalid key added by `$setValidity` + * - `ng-pristine`: the control hasn't been interacted with yet + * - `ng-dirty`: the control has been interacted with + * - `ng-touched`: the control has been blurred + * - `ng-untouched`: the control hasn't been blurred + * - `ng-pending`: any `$asyncValidators` are unfulfilled + * - `ng-empty`: the view does not contain a value or the value is deemed "empty", as defined + * by the {@link ngModel.NgModelController#$isEmpty} method + * - `ng-not-empty`: the view contains a non-empty value * - * @description - * The ngMousedown directive allows you to specify custom behavior on mousedown event. + * Keep in mind that ngAnimate can detect each of these classes when added and removed. * - * @element ANY - * @priority 0 - * @param {expression} ngMousedown {@link guide/expression Expression} to evaluate upon - * mousedown. ({@link guide/expression#-event- Event object is available as `$event`}) + * @animations + * Animations within models are triggered when any of the associated CSS classes are added and removed + * on the input element which is attached to the model. These classes include: `.ng-pristine`, `.ng-dirty`, + * `.ng-invalid` and `.ng-valid` as well as any other validations that are performed on the model itself. + * The animations that are triggered within ngModel are similar to how they work in ngClass and + * animations can be hooked into using CSS transitions, keyframes as well as JS animations. + * + * The following example shows a simple way to utilize CSS transitions to style an input element + * that has been rendered as invalid after it has been validated: + * + *
    +     * //be sure to include ngAnimate as a module to hook into more
    +     * //advanced animations
    +     * .my-input {
    +     *   transition:0.5s linear all;
    +     *   background: white;
    +     * }
    +     * .my-input.ng-invalid {
    +     *   background: red;
    +     *   color:white;
    +     * }
    +     * 
    * * @example - + * ### Basic Usage + * - - count: {{count}} + + +

    + Update input to see transitions when valid/invalid. + Integer is a valid value. +

    +
    + +
    -
    - */ - - - /** - * @ngdoc directive - * @name ngMouseup + *
    * - * @description - * Specify custom behavior on mouseup event. + * @example + * ### Binding to a getter/setter * - * @element ANY - * @priority 0 - * @param {expression} ngMouseup {@link guide/expression Expression} to evaluate upon - * mouseup. ({@link guide/expression#-event- Event object is available as `$event`}) + * Sometimes it's helpful to bind `ngModel` to a getter/setter function. A getter/setter is a + * function that returns a representation of the model when called with zero arguments, and sets + * the internal state of a model when called with an argument. It's sometimes useful to use this + * for models that have an internal representation that's different from what the model exposes + * to the view. + * + *
    + * **Best Practice:** It's best to keep getters fast because AngularJS is likely to call them more + * frequently than other parts of your code. + *
    + * + * You use this behavior by adding `ng-model-options="{ getterSetter: true }"` to an element that + * has `ng-model` attached to it. You can also add `ng-model-options="{ getterSetter: true }"` to + * a `
    `, which will enable this behavior for all ``s within it. See + * {@link ng.directive:ngModelOptions `ngModelOptions`} for more. + * + * The following example shows how to use `ngModel` with a getter/setter: * * @example - + * - - count: {{count}} +
    + + + +
    user.name = 
    +
    -
    + + angular.module('getterSetterExample', []) + .controller('ExampleController', ['$scope', function($scope) { + var _name = 'Brian'; + $scope.user = { + name: function(newName) { + // Note that newName can be undefined for two reasons: + // 1. Because it is called as a getter and thus called with no arguments + // 2. Because the property should actually be set to undefined. This happens e.g. if the + // input is invalid + return arguments.length ? (_name = newName) : _name; + } + }; + }]); + + *
    */ + var ngModelDirective = ['$rootScope', function($rootScope) { + return { + restrict: 'A', + require: ['ngModel', '^?form', '^?ngModelOptions'], + controller: NgModelController, + // Prelink needs to run before any input directive + // so that we can set the NgModelOptions in NgModelController + // before anyone else uses it. + priority: 1, + compile: function ngModelCompile(element) { + // Setup initial state of the control + element.addClass(PRISTINE_CLASS).addClass(UNTOUCHED_CLASS).addClass(VALID_CLASS); + + return { + pre: function ngModelPreLink(scope, element, attr, ctrls) { + var modelCtrl = ctrls[0], + formCtrl = ctrls[1] || modelCtrl.$$parentForm, + optionsCtrl = ctrls[2]; + + if (optionsCtrl) { + modelCtrl.$options = optionsCtrl.$options; + } + + modelCtrl.$$initGetterSetters(); + + // notify others, especially parent forms + formCtrl.$addControl(modelCtrl); + + attr.$observe('name', function(newValue) { + if (modelCtrl.$name !== newValue) { + modelCtrl.$$parentForm.$$renameControl(modelCtrl, newValue); + } + }); + + scope.$on('$destroy', function() { + modelCtrl.$$parentForm.$removeControl(modelCtrl); + }); + }, + post: function ngModelPostLink(scope, element, attr, ctrls) { + var modelCtrl = ctrls[0]; + modelCtrl.$$setUpdateOnEvents(); + + function setTouched() { + modelCtrl.$setTouched(); + } + + element.on('blur', function() { + if (modelCtrl.$touched) return; + + if ($rootScope.$$phase) { + scope.$evalAsync(setTouched); + } else { + scope.$apply(setTouched); + } + }); + } + }; + } + }; + }]; + + /* exported defaultModelOptions */ + var defaultModelOptions; + var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/; /** - * @ngdoc directive - * @name ngMouseover - * + * @ngdoc type + * @name ModelOptions * @description - * Specify custom behavior on mouseover event. - * - * @element ANY - * @priority 0 - * @param {expression} ngMouseover {@link guide/expression Expression} to evaluate upon - * mouseover. ({@link guide/expression#-event- Event object is available as `$event`}) - * - * @example - - - - count: {{count}} - - + * A container for the options set by the {@link ngModelOptions} directive */ + function ModelOptions(options) { + this.$$options = options; + } + + ModelOptions.prototype = { + + /** + * @ngdoc method + * @name ModelOptions#getOption + * @param {string} name the name of the option to retrieve + * @returns {*} the value of the option + * @description + * Returns the value of the given option + */ + getOption: function(name) { + return this.$$options[name]; + }, + + /** + * @ngdoc method + * @name ModelOptions#createChild + * @param {Object} options a hash of options for the new child that will override the parent's options + * @return {ModelOptions} a new `ModelOptions` object initialized with the given options. + */ + createChild: function(options) { + var inheritAll = false; + + // make a shallow copy + options = extend({}, options); + + // Inherit options from the parent if specified by the value `"$inherit"` + forEach(options, /* @this */ function(option, key) { + if (option === '$inherit') { + if (key === '*') { + inheritAll = true; + } else { + options[key] = this.$$options[key]; + // `updateOn` is special so we must also inherit the `updateOnDefault` option + if (key === 'updateOn') { + options.updateOnDefault = this.$$options.updateOnDefault; + } + } + } else { + if (key === 'updateOn') { + // If the `updateOn` property contains the `default` event then we have to remove + // it from the event list and set the `updateOnDefault` flag. + options.updateOnDefault = false; + options[key] = trim(option.replace(DEFAULT_REGEXP, function() { + options.updateOnDefault = true; + return ' '; + })); + } + } + }, this); + + if (inheritAll) { + // We have a property of the form: `"*": "$inherit"` + delete options['*']; + defaults(options, this.$$options); + } + + // Finally add in any missing defaults + defaults(options, defaultModelOptions.$$options); + + return new ModelOptions(options); + } + }; + + + defaultModelOptions = new ModelOptions({ + updateOn: '', + updateOnDefault: true, + debounce: 0, + getterSetter: false, + allowInvalid: false, + timezone: null + }); /** * @ngdoc directive - * @name ngMouseenter + * @name ngModelOptions + * @restrict A + * @priority 10 * * @description - * Specify custom behavior on mouseenter event. + * This directive allows you to modify the behaviour of {@link ngModel} directives within your + * application. You can specify an `ngModelOptions` directive on any element. All {@link ngModel} + * directives will use the options of their nearest `ngModelOptions` ancestor. * - * @element ANY - * @priority 0 - * @param {expression} ngMouseenter {@link guide/expression Expression} to evaluate upon - * mouseenter. ({@link guide/expression#-event- Event object is available as `$event`}) + * The `ngModelOptions` settings are found by evaluating the value of the attribute directive as + * an AngularJS expression. This expression should evaluate to an object, whose properties contain + * the settings. For example: `
    - - - count: {{count}} - - - */ - - - /** - * @ngdoc directive - * @name ngMouseleave + * ## Inheriting Options * - * @description - * Specify custom behavior on mouseleave event. + * You can specify that an `ngModelOptions` setting should be inherited from a parent `ngModelOptions` + * directive by giving it the value of `"$inherit"`. + * Then it will inherit that setting from the first `ngModelOptions` directive found by traversing up the + * DOM tree. If there is no ancestor element containing an `ngModelOptions` directive then default settings + * will be used. * - * @element ANY - * @priority 0 - * @param {expression} ngMouseleave {@link guide/expression Expression} to evaluate upon - * mouseleave. ({@link guide/expression#-event- Event object is available as `$event`}) + * For example given the following fragment of HTML * - * @example - - - - count: {{count}} - - - */ - - - /** - * @ngdoc directive - * @name ngMousemove * - * @description - * Specify custom behavior on mousemove event. + * ```html + *
    + *
    + * + *
    + *
    + * ``` * - * @element ANY - * @priority 0 - * @param {expression} ngMousemove {@link guide/expression Expression} to evaluate upon - * mousemove. ({@link guide/expression#-event- Event object is available as `$event`}) + * the `input` element will have the following settings * - * @example - - - - count: {{count}} - - - */ - - - /** - * @ngdoc directive - * @name ngKeydown + * ```js + * { allowInvalid: true, updateOn: 'default', debounce: 0 } + * ``` * - * @description - * Specify custom behavior on keydown event. + * Notice that the `debounce` setting was not inherited and used the default value instead. * - * @element ANY - * @priority 0 - * @param {expression} ngKeydown {@link guide/expression Expression} to evaluate upon - * keydown. (Event object is available as `$event` and can be interrogated for keyCode, altKey, etc.) + * You can specify that all undefined settings are automatically inherited from an ancestor by + * including a property with key of `"*"` and value of `"$inherit"`. * - * @example - - - - key down count: {{count}} - - - */ - - - /** - * @ngdoc directive - * @name ngKeyup + * For example given the following fragment of HTML * - * @description - * Specify custom behavior on keyup event. * - * @element ANY - * @priority 0 - * @param {expression} ngKeyup {@link guide/expression Expression} to evaluate upon - * keyup. (Event object is available as `$event` and can be interrogated for keyCode, altKey, etc.) + * ```html + *
    + *
    + * + *
    + *
    + * ``` * - * @example - - - - key up count: {{count}} - - - */ - - - /** - * @ngdoc directive - * @name ngKeypress + * the `input` element will have the following settings * - * @description - * Specify custom behavior on keypress event. + * ```js + * { allowInvalid: true, updateOn: 'default', debounce: 200 } + * ``` * - * @element ANY - * @param {expression} ngKeypress {@link guide/expression Expression} to evaluate upon - * keypress. ({@link guide/expression#-event- Event object is available as `$event`} - * and can be interrogated for keyCode, altKey, etc.) + * Notice that the `debounce` setting now inherits the value from the outer `
    ` element. * - * @example - - - - key press count: {{count}} - - - */ - - - /** - * @ngdoc directive - * @name ngSubmit + * If you are creating a reusable component then you should be careful when using `"*": "$inherit"` + * since you may inadvertently inherit a setting in the future that changes the behavior of your component. * - * @description - * Enables binding angular expressions to onsubmit events. * - * Additionally it prevents the default action (which for form means sending the request to the - * server and reloading the current page), but only if the form does not contain `action`, - * `data-action`, or `x-action` attributes. + * ## Triggering and debouncing model updates * - * @element form - * @priority 0 - * @param {expression} ngSubmit {@link guide/expression Expression} to eval. - * ({@link guide/expression#-event- Event object is available as `$event`}) + * The `updateOn` and `debounce` properties allow you to specify a custom list of events that will + * trigger a model update and/or a debouncing delay so that the actual update only takes place when + * a timer expires; this timer will be reset after another change takes place. * - * @example - - - -
    - Enter text and hit enter: - - -
    list={{list}}
    -
    -
    - - it('should check ng-submit', function() { - expect(element(by.binding('list')).getText()).toBe('list=[]'); - element(by.css('#submit')).click(); - expect(element(by.binding('list')).getText()).toContain('hello'); - expect(element(by.input('text')).getAttribute('value')).toBe(''); - }); - it('should ignore empty strings', function() { - expect(element(by.binding('list')).getText()).toBe('list=[]'); - element(by.css('#submit')).click(); - element(by.css('#submit')).click(); - expect(element(by.binding('list')).getText()).toContain('hello'); - }); - -
    - */ - - /** - * @ngdoc directive - * @name ngFocus + * Given the nature of `ngModelOptions`, the value displayed inside input fields in the view might + * be different from the value in the actual model. This means that if you update the model you + * should also invoke {@link ngModel.NgModelController#$rollbackViewValue} on the relevant input field in + * order to make sure it is synchronized with the model and that any debounced action is canceled. * - * @description - * Specify custom behavior on focus event. + * The easiest way to reference the control's {@link ngModel.NgModelController#$rollbackViewValue} + * method is by making sure the input is placed inside a form that has a `name` attribute. This is + * important because `form` controllers are published to the related scope under the name in their + * `name` attribute. * - * @element window, input, select, textarea, a - * @priority 0 - * @param {expression} ngFocus {@link guide/expression Expression} to evaluate upon - * focus. ({@link guide/expression#-event- Event object is available as `$event`}) + * Any pending changes will take place immediately when an enclosing form is submitted via the + * `submit` event. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit` + * to have access to the updated model. * - * @example - * See {@link ng.directive:ngClick ngClick} - */ - - /** - * @ngdoc directive - * @name ngBlur + * ### Overriding immediate updates * - * @description - * Specify custom behavior on blur event. + * The following example shows how to override immediate updates. Changes on the inputs within the + * form will update the model only when the control loses focus (blur event). If `escape` key is + * pressed while the input field is focused, the value is reset to the value in the current model. * - * @element window, input, select, textarea, a - * @priority 0 - * @param {expression} ngBlur {@link guide/expression Expression} to evaluate upon - * blur. ({@link guide/expression#-event- Event object is available as `$event`}) + * + * + *
    + *
    + *
    + *
    + *
    + *
    user.name = 
    + *
    + *
    + * + * angular.module('optionsExample', []) + * .controller('ExampleController', ['$scope', function($scope) { + * $scope.user = { name: 'say', data: '' }; + * + * $scope.cancel = function(e) { + * if (e.keyCode === 27) { + * $scope.userForm.userName.$rollbackViewValue(); + * } + * }; + * }]); + * + * + * var model = element(by.binding('user.name')); + * var input = element(by.model('user.name')); + * var other = element(by.model('user.data')); + * + * it('should allow custom events', function() { + * input.sendKeys(' hello'); + * input.click(); + * expect(model.getText()).toEqual('say'); + * other.click(); + * expect(model.getText()).toEqual('say hello'); + * }); * - * @example - * See {@link ng.directive:ngClick ngClick} - */ - - /** - * @ngdoc directive - * @name ngCopy + * it('should $rollbackViewValue when model changes', function() { + * input.sendKeys(' hello'); + * expect(input.getAttribute('value')).toEqual('say hello'); + * input.sendKeys(protractor.Key.ESCAPE); + * expect(input.getAttribute('value')).toEqual('say'); + * other.click(); + * expect(model.getText()).toEqual('say'); + * }); + * + *
    * - * @description - * Specify custom behavior on copy event. + * ### Debouncing updates * - * @element window, input, select, textarea, a - * @priority 0 - * @param {expression} ngCopy {@link guide/expression Expression} to evaluate upon - * copy. ({@link guide/expression#-event- Event object is available as `$event`}) + * The next example shows how to debounce model changes. Model will be updated only 1 sec after last change. + * If the `Clear` button is pressed, any debounced action is canceled and the value becomes empty. + * + * + * + *
    + *
    + * Name: + * + *
    + *
    + *
    user.name = 
    + *
    + *
    + * + * angular.module('optionsExample', []) + * .controller('ExampleController', ['$scope', function($scope) { + * $scope.user = { name: 'say' }; + * }]); + * + *
    + * + * + * ## Model updates and validation + * + * The default behaviour in `ngModel` is that the model value is set to `undefined` when the + * validation determines that the value is invalid. By setting the `allowInvalid` property to true, + * the model will still be updated even if the value is invalid. + * + * + * ## Connecting to the scope + * + * By setting the `getterSetter` property to true you are telling ngModel that the `ngModel` expression + * on the scope refers to a "getter/setter" function rather than the value itself. + * + * The following example shows how to bind to getter/setters: + * + * + * + *
    + *
    + * + *
    + *
    user.name = 
    + *
    + *
    + * + * angular.module('getterSetterExample', []) + * .controller('ExampleController', ['$scope', function($scope) { + * var _name = 'Brian'; + * $scope.user = { + * name: function(newName) { + * return angular.isDefined(newName) ? (_name = newName) : _name; + * } + * }; + * }]); + * + *
    + * + * + * ## Specifying timezones + * + * You can specify the timezone that date/time input directives expect by providing its name in the + * `timezone` property. + * + * + * ## Programmatically changing options + * + * The `ngModelOptions` expression is only evaluated once when the directive is linked; it is not + * watched for changes. However, it is possible to override the options on a single + * {@link ngModel.NgModelController} instance with + * {@link ngModel.NgModelController#$overrideModelOptions `NgModelController#$overrideModelOptions()`}. + * + * + * @param {Object} ngModelOptions options to apply to {@link ngModel} directives on this element and + * and its descendents. Valid keys are: + * - `updateOn`: string specifying which event should the input be bound to. You can set several + * events using an space delimited list. There is a special event called `default` that + * matches the default events belonging to the control. These are the events that are bound to + * the control, and when fired, update the `$viewValue` via `$setViewValue`. + * + * `ngModelOptions` considers every event that is not listed in `updateOn` a "default" event, + * since different control types use different default events. + * + * See also the section {@link ngModelOptions#triggering-and-debouncing-model-updates + * Triggering and debouncing model updates}. + * + * - `debounce`: integer value which contains the debounce model update value in milliseconds. A + * value of 0 triggers an immediate update. If an object is supplied instead, you can specify a + * custom value for each event. For example: + * ``` + * ng-model-options="{ + * updateOn: 'default blur click', + * debounce: { 'default': 500, 'blur': 0 } + * }" + * ``` + * + * "default" also applies to all events that are listed in `updateOn` but are not + * listed in `debounce`, i.e. "click" would also be debounced by 500 milliseconds. + * + * - `allowInvalid`: boolean value which indicates that the model can be set with values that did + * not validate correctly instead of the default behavior of setting the model to undefined. + * - `getterSetter`: boolean value which determines whether or not to treat functions bound to + * `ngModel` as getters/setters. + * - `timezone`: Defines the timezone to be used to read/write the `Date` instance in the model for + * ``, ``, ... . It understands UTC/GMT and the + * continental US time zone abbreviations, but for general use, use a time zone offset, for + * example, `'+0430'` (4 hours, 30 minutes east of the Greenwich meridian) + * If not specified, the timezone of the browser will be used. * - * @example - - - - copied: {{copied}} - - */ + var ngModelOptionsDirective = function() { + NgModelOptionsController.$inject = ['$attrs', '$scope']; + function NgModelOptionsController($attrs, $scope) { + this.$$attrs = $attrs; + this.$$scope = $scope; + } + NgModelOptionsController.prototype = { + $onInit: function() { + var parentOptions = this.parentCtrl ? this.parentCtrl.$options : defaultModelOptions; + var modelOptionsDefinition = this.$$scope.$eval(this.$$attrs.ngModelOptions); + + this.$options = parentOptions.createChild(modelOptionsDefinition); + } + }; + + return { + restrict: 'A', + // ngModelOptions needs to run before ngModel and input directives + priority: 10, + require: {parentCtrl: '?^^ngModelOptions'}, + bindToController: true, + controller: NgModelOptionsController + }; + }; + + +// shallow copy over values from `src` that are not already specified on `dst` + function defaults(dst, src) { + forEach(src, function(value, key) { + if (!isDefined(dst[key])) { + dst[key] = value; + } + }); + } /** * @ngdoc directive - * @name ngCut + * @name ngNonBindable + * @restrict AC + * @priority 1000 + * @element ANY * * @description - * Specify custom behavior on cut event. - * - * @element window, input, select, textarea, a - * @priority 0 - * @param {expression} ngCut {@link guide/expression Expression} to evaluate upon - * cut. ({@link guide/expression#-event- Event object is available as `$event`}) + * The `ngNonBindable` directive tells AngularJS not to compile or bind the contents of the current + * DOM element, including directives on the element itself that have a lower priority than + * `ngNonBindable`. This is useful if the element contains what appears to be AngularJS directives + * and bindings but which should be ignored by AngularJS. This could be the case if you have a site + * that displays snippets of code, for instance. * * @example - + * In this example there are two locations where a simple interpolation binding (`{{}}`) is present, + * but the one wrapped in `ngNonBindable` is left alone. + * + - - cut: {{cut}} +
    Normal: {{1 + 2}}
    +
    Ignored: {{1 + 2}}
    +
    + + it('should check ng-non-bindable', function() { + expect(element(by.binding('1 + 2')).getText()).toContain('3'); + expect(element.all(by.css('div')).last().getText()).toMatch(/1 \+ 2/); + });
    */ + var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 }); + + /* exported ngOptionsDirective */ + + /* global jqLiteRemove */ + + var ngOptionsMinErr = minErr('ngOptions'); /** * @ngdoc directive - * @name ngPaste + * @name ngOptions + * @restrict A * * @description - * Specify custom behavior on paste event. * - * @element window, input, select, textarea, a - * @priority 0 - * @param {expression} ngPaste {@link guide/expression Expression} to evaluate upon - * paste. ({@link guide/expression#-event- Event object is available as `$event`}) + * The `ngOptions` attribute can be used to dynamically generate a list of `
    - */ - - /** - * @ngdoc directive - * @name ngIf - * @restrict A + * In many cases, {@link ng.directive:ngRepeat ngRepeat} can be used on `` + * DOM element. + * * `disable`: The result of this expression will be used to disable the rendered `